Skip to content

[Plugin] Add Bootstrap Icons and woff2 to ttf native decoder#858

Merged
egorikftp merged 14 commits intomainfrom
feature/bootstrap-icons
Feb 24, 2026
Merged

[Plugin] Add Bootstrap Icons and woff2 to ttf native decoder#858
egorikftp merged 14 commits intomainfrom
feature/bootstrap-icons

Conversation

@t-regbs
Copy link
Collaborator

@t-regbs t-regbs commented Feb 13, 2026


@coderabbitai
Copy link

coderabbitai bot commented Feb 13, 2026

Walkthrough

Adds a provider-based "standard" web-import architecture and Bootstrap support. Introduces StandardIconProvider and implementations for Bootstrap and Lucide, new models (StandardIcon, StandardIconConfig, SizeSettings, InferredCategory), category inference and display-name utilities, and UI components (CustomizationToolbar, IconSizeCustomization, StandardImport screens and destinations). Adds a colored SDK icon BootstrapLogo. Removes legacy Lucide domain files and migrates many lucide paths to the standard layout. Adds native WOFF2 decoding (JNI C++ bridge, NativeLibraryLoader, Woff2Decoder Kotlin) and a GitHub Actions workflow to build native libraries. Localization keys updated.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding Bootstrap Icons support and implementing a woff2 to TTF native decoder for the IntelliJ plugin.
Description check ✅ Passed The PR description confirms all required changelog sections (CLI, Gradle Plugin, IntelliJ Plugin) have been updated in their respective 'Unreleased' sections, fulfilling the template requirements.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/bootstrap-icons

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/IconSizeCustomization.kt`:
- Around line 35-43: The KDoc for IconSizeCustomization.kt contains a stale
example key "web.import.font.customize.lucide.size" for the sizeLabel parameter;
update the KDoc in the IconSizeCustomization file to use the new key
"web.import.font.customize.size" (update the `@param` sizeLabel example text) so
the documentation matches the renamed resource key and ensure the comment
mentions a generic example rather than the Lucide-specific one.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/CategoryInferrer.kt`:
- Around line 20-52: The "key" entry in CATEGORY_KEYWORDS
(CategoryKeyword("key", "Security", priority = 1)) causes substring
false-positives (e.g., "keyboard"); change the keyword to a more specific token
and/or add delimiter-aware matching: replace the CategoryKeyword("key", ...)
with a more precise keyword like "key-" or "padlock" (and/or "lock") and update
the matching code that consumes CATEGORY_KEYWORDS to perform word-boundary or
delimiter (e.g., '-' or '_') based matching instead of raw substring checks so
only exact token matches map to "Security".
🧹 Nitpick comments (9)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt (2)

55-57: Consider sanitizing or validating iconName before URL interpolation.

While iconName values originate from the parsed JSON keys (so they should be safe in practice), directly interpolating user-selectable strings into URLs without validation is a defensive coding concern. A simple check (e.g., verifying the name matches [a-z0-9-]+) would guard against unexpected input.


20-25: Consider pinning bootstrap-icons to a specific version for reproducible builds.

UNPKG_BASE uses @latest, making the plugin's behavior non-deterministic—if Bootstrap Icons publishes a breaking change, the plugin could break without any code change on your side. However, this pattern is consistent with LucideRepository, which also uses @latest for lucide-static. If pinning is desired, apply it across all icon repositories and use the current version (1.13.1 rather than 1.11.3).

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/CustomizationToolbar.kt (1)

28-38: Consider renaming string resource keys to remove "font" prefix.

The toolbar is reused in both FontCustomization and IconSizeCustomization contexts, but the resource keys web.import.font.customize.header and web.import.font.customize.reset contain "font" in their names. While the displayed text ("Customize", "Reset") is already generic and functions correctly in both contexts, renaming these keys to web.import.customize.header and web.import.customize.reset would improve clarity for future maintainers.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/domain/BootstrapUseCase.kt (1)

25-33: Category inference relies solely on icon name for Bootstrap.

Since Bootstrap icons lack tag metadata (tags = emptyList()), inferCategoryFromTags will only match on icon names. This means many Bootstrap icons may fall into the "General" fallback category if their names don't contain keywords from CATEGORY_KEYWORDS. This may result in a less useful category filter compared to Lucide.

Consider whether this is acceptable UX or whether Bootstrap-specific keyword tuning (or disabling category filtering for Bootstrap) would be worthwhile.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/CategoryInferrer.kt (1)

73-90: Priority boosting for name matches can produce negative priorities.

Line 79: it.copy(priority = it.priority - 2) means priority-1 keywords matched by name become priority -1. This works correctly since lower = higher priority, but the negative values and the implicit coupling between the boost value (-2) and the priority scale (1–5) could be fragile if new priority levels are added.

A minor readability improvement would be to document the effective priority range (or use a named constant for the boost).

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardIconViewModel.kt (1)

154-159: Minor: updateSuccess has a potential read-modify-write race.

selectCategory, updateSearchQuery, and updateSettings all dispatch on Dispatchers.Default and call updateSuccess concurrently. Two concurrent calls can read the same snapshot and one update gets lost. In practice, UI-driven actions are near-sequential so the risk is low, but worth noting.

A Mutex or funneling all updates through a single-threaded dispatcher (e.g., Dispatchers.Main) would eliminate the race.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt (2)

113-117: Modifier ordering: fillMaxSize() before external modifier.

The external modifier is applied after fillMaxSize(), which means caller-supplied size constraints are ignored. If this is intentional (screen must always fill), consider documenting it; otherwise, swap the order.

♻️ Suggested fix (if caller sizing should be respected)
-        modifier = Modifier
-            .fillMaxSize()
-            .then(modifier),
+        modifier = modifier
+            .fillMaxSize(),

297-301: Unchecked cast: item.icon as StandardIcon.

IconItem<*> is erased at runtime, so this cast is unchecked. It works as long as only StandardIcon items are added to gridItems, but a future misuse could produce a ClassCastException with no compile-time guard.

Consider using a typed IconItem<StandardIcon> alias or a safe-cast with a fallback.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/LucideImportScreen.kt (1)

20-28: navController.parent?.navigate silently no-ops if parent is null.

If parent is ever null, the download completes but the user sees no navigation — a silent failure. This is likely safe given the screen is always nested inside WebImportFlow, but a log or assertion would make debugging easier if the hierarchy changes.

@t-regbs t-regbs force-pushed the feature/bootstrap-icons branch from b28d2ae to b69ae5d Compare February 13, 2026 01:46
@t-regbs t-regbs force-pushed the feature/bootstrap-icons branch from b69ae5d to d93bbad Compare February 13, 2026 08:38
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/BootstrapImportScreen.kt`:
- Around line 20-28: The current onIconDownload uses
navController.parent?.navigate which silently drops navigation when parent is
null; update BootstrapImportScreen's onIconDownload to handle a null parent
explicitly by either using a non-null assertion (e.g.,
requireNotNull/navController.parent!!) to fail fast during development or by
checking navController.parent and logging/showing an error before returning,
then calling navigate(dest = SimpleConversionScreen, navArgs = TextSource(...))
on the non-null parent to ensure the user gets feedback instead of a silent
no-op.
🧹 Nitpick comments (4)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/CategoryInferrer.kt (1)

73-90: Duplicate name+tag matches are harmless but create unnecessary allocations.

If a keyword (e.g., "arrow") matches both the icon name and a tag, it appears twice in allMatches — once boosted, once at original priority. minByOrNull still picks the correct (boosted) entry, so correctness is fine. The extra allocation is negligible for the expected dataset sizes.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardIconViewModel.kt (1)

154-159: Non-atomic read-modify-write on stateRecord.value.

updateSuccess reads the current value, checks the type, and writes back. If two coroutines call this concurrently (e.g., selectCategory on Dispatchers.Default and downloadFont completing simultaneously), the second write can silently overwrite changes from the first. The practical risk is low since user interactions are sequential, but be aware this is inherently racy.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt (2)

113-116: Modifier ordering: fillMaxSize() is applied before the caller's modifier.

The external modifier is applied via .then(modifier) after .fillMaxSize(), which means callers cannot override the size constraint. Typically, the incoming modifier is applied first. If the intent is to always fill max size, consider documenting this or reordering.

🔧 Suggested reorder
     Column(
-        modifier = Modifier
-            .fillMaxSize()
-            .then(modifier),
+        modifier = modifier
+            .fillMaxSize(),
     ) {

297-303: Unchecked cast item.icon as StandardIcon.

IconItem<*> erases the type parameter, so this cast is unchecked and will only fail at runtime if a non-StandardIcon item is ever added to the grid. This is safe today since the ViewModel only supplies StandardIcon items, but it's fragile.

Consider using a typed alias (e.g., typealias StandardIconItem = IconItem<StandardIcon>) and filtering/casting at the data boundary instead of in the UI layer.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/WoffToTtfConverter.kt`:
- Around line 127-129: The loop in WoffToTtfConverter that repeatedly calls
inflater.inflate(buffer) can spin forever if inflate() returns 0 (e.g.,
truncated stream or dictionary required); change the loop to capture the
returned int (e.g., int read = inflater.inflate(buffer)) and if read == 0 then
detect the cause: if inflater.needsDictionary() throw an IOException indicating
a missing preset dictionary, if inflater.needsInput() either feed more input or
throw an IOException for truncated input, otherwise break/throw to avoid an
infinite loop; keep the rest of the inflater.finished() handling intact so you
still write non-zero reads to output.
🧹 Nitpick comments (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/WoffToTtfConverter.kt (1)

149-149: Consider using an integer-based log2 instead of floating-point ln.

Since the argument is always an exact power of 2 (from highestPowerOf2), Integer.numberOfTrailingZeros is exact and avoids any floating-point edge cases.

Proposed fix
-    private fun log2(n: Int): Int = (ln(n.toDouble()) / ln(2.0)).toInt()
+    private fun log2(n: Int): Int = Integer.numberOfTrailingZeros(n)

This also removes the import kotlin.math.ln on line 8.

@egorikftp egorikftp marked this pull request as draft February 19, 2026 10:32
@egorikftp egorikftp marked this pull request as draft February 19, 2026 10:32
@t-regbs
Copy link
Collaborator Author

t-regbs commented Feb 19, 2026

@egorikftp Been exploring the JNI binding idea and I like it. See latests commits on this PR, I added:

  • JNI bridge (~35 lines of C++)
  • Pre-built binaries for macOS (universal arm64+x86_64), Linux x86_64, and Windows x86_64. Think it is safe to just commit in the project and bundle. I am only able to test for macOS though, will need testing on windows and linux.
  • Platform-detecting loader that extracts the correct binary from JAR resources at runtime
  • GitHub Actions workflow (build-native-libs.yml) for rebuilding the native binaries when needed (manual trigger). Shouldn't really need to run this ever since the woff2 spec does not change. It is how I built the windows and linux binaries since I couldn't do it locally, so i just left it in.

Let me know your thoughts

@egorikftp
Copy link
Member

@egorikftp Been exploring the JNI binding idea and I like it. See latests commits on this PR, I added:

  • JNI bridge (~35 lines of C++)
  • Pre-built binaries for macOS (universal arm64+x86_64), Linux x86_64, and Windows x86_64. Think it is safe to just commit in the project and bundle. I am only able to test for macOS though, will need testing on windows and linux.
  • Platform-detecting loader that extracts the correct binary from JAR resources at runtime
  • GitHub Actions workflow (build-native-libs.yml) for rebuilding the native binaries when needed (manual trigger). Shouldn't really need to run this ever since the woff2 spec does not change. It is how I built the windows and linux binaries since I couldn't do it locally, so i just left it in.

Let me know your thoughts

Amazing work you’ve done! I’ll test it on all platforms and get back with feedback ❤️
The power of open source!

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tools/idea-plugin/native/woff2decoder.cpp`:
- Around line 11-33: Get and check JNI allocations and exceptions: after calling
env->GetByteArrayElements (input_data) validate input_data != nullptr and call
env->ExceptionCheck() and return nullptr if either indicates failure; use
env->GetArrayLength only after confirming input_data is valid. After creating
the output jbyteArray via env->NewByteArray check for nullptr and
env->ExceptionCheck() and if failed release the input bytes with
env->ReleaseByteArrayElements(..., JNI_ABORT) and return appropriately; never
call env->SetByteArrayRegion or further JNI calls when an exception is pending.
Replace ReleaseByteArrayElements(..., 0) with JNI_ABORT for the read-only input
buffer and ensure any cleanup on failure uses the same JNI_ABORT flag; reference
functions/objects: GetByteArrayElements, NewByteArray, ReleaseByteArrayElements,
SetByteArrayRegion, ConvertWOFF2ToTTF, WOFF2StringOut and out.Size() to locate
where to add these checks and early returns.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/NativeLibraryLoader.kt`:
- Around line 31-40: The current extraction writes the native library into a
predictable temp directory and fixed filename (tempDir / tempFile) which risks
collisions and hijacking; update NativeLibraryLoader.kt to create a unique,
secure temporary directory and file for each load (use Files.createTempDirectory
or File.createTempFile semantics), write the resourceStream into that temp file
with restrictive permissions, atomically move if needed, call deleteOnExit, and
ensure the code paths around tempDir/tempFile (the variables and the
resourceStream.use block) reference the new unique temp path so each load uses
its own non-predictable file.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8a2822 and f15e925.

⛔ Files ignored due to path filters (3)
  • tools/idea-plugin/src/main/resources/native/linux-x64/libwoff2decoder.so is excluded by !**/*.so
  • tools/idea-plugin/src/main/resources/native/macos/libwoff2decoder.dylib is excluded by !**/*.dylib
  • tools/idea-plugin/src/main/resources/native/windows-x64/woff2decoder.dll is excluded by !**/*.dll
📒 Files selected for processing (5)
  • .github/workflows/build-native-libs.yml
  • tools/idea-plugin/native/woff2decoder.cpp
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/NativeLibraryLoader.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/Woff2Decoder.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
tools/idea-plugin/native/woff2decoder.cpp (1)

11-33: JNI allocation error handling is missing.

This issue was flagged in a previous review. The code lacks null checks after GetByteArrayElements (line 11) and NewByteArray (line 29), and doesn't check for pending exceptions. Additionally, ReleaseByteArrayElements should use JNI_ABORT instead of 0 for read-only buffers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/idea-plugin/native/woff2decoder.cpp` around lines 11 - 33, Get and use
JNI resources defensively: after calling env->GetByteArrayElements (input_bytes)
check for a null return and env->ExceptionCheck() and return nullptr if
allocation/attachment failed; after creating the Java output buffer with
env->NewByteArray check for null and pending exceptions and clean up (release
input) before returning; always call env->ReleaseByteArrayElements(input_bytes,
...) with JNI_ABORT (not 0) since we only read from the input; ensure any early
returns release resources and propagate failure by returning nullptr. Target
these changes around GetByteArrayElements, GetArrayLength, NewByteArray, and
ReleaseByteArrayElements in woff2decoder.cpp and add appropriate
env->ExceptionCheck() checks and cleanup paths.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/NativeLibraryLoader.kt (1)

31-40: Predictable temp path creates security and concurrency risks.

This was flagged in a previous review. The fixed path valkyrie-native/$fileName allows collisions between JVM instances and potential symlink attacks.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/NativeLibraryLoader.kt`
around lines 31 - 40, The code in NativeLibraryLoader creates a predictable temp
path (tempDir = File(System.getProperty("java.io.tmpdir"), "valkyrie-native")
and tempFile = File(tempDir, fileName), which risks collisions and symlink
attacks; change to create a secure, unique temp location using the platform temp
APIs (e.g., Files.createTempDirectory or Files.createTempFile) and write the
resource into that securely-created file, set restrictive permissions, and call
deleteOnExit; update symbols: tempDir, tempFile, fileName, resourceStream, and
the code path in NativeLibraryLoader that copies the stream so it uses the
unique temp file instead of the fixed "valkyrie-native/$fileName" path.
🧹 Nitpick comments (2)
.github/workflows/build-native-libs.yml (2)

57-61: Consider adding artifact retention and a consolidation job.

The artifacts are uploaded separately without retention configuration. Consider:

  1. Adding retention-days to control artifact storage
  2. Adding a final job that downloads all artifacts and uploads a combined release artifact for easier consumption

Also applies to: 129-133, 218-222

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build-native-libs.yml around lines 57 - 61, Update each
actions/upload-artifact@v4 step (the "Upload artifact" step blocks) to include a
retention-days input (e.g., retention-days: 30) to control artifact storage, and
add a new final job (e.g., "consolidate-artifacts") that depends on the build
jobs, uses actions/download-artifact to fetch each uploaded artifact by name,
consolidates them into a single archive, and then uploads that archive with
actions/upload-artifact (or creates a release) for easier consumption; apply the
retention-days change to the other upload steps referenced (lines 129-133 and
218-222) as well.

18-19: Pin the woff2 repository to a specific commit or tag for reproducible builds.

Cloning google/woff2 without a version pin means builds could break or produce different binaries if upstream changes. Pin to a specific commit or release tag for reproducibility.

-      - name: Clone google/woff2
-        run: git clone --recursive --depth 1 https://github.com/google/woff2.git /tmp/woff2
+      - name: Clone google/woff2
+        run: |
+          git clone --recursive --depth 1 https://github.com/google/woff2.git /tmp/woff2
+          cd /tmp/woff2 && git checkout v1.0.2  # or specific commit SHA

Also applies to: 77-78, 146-147

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build-native-libs.yml around lines 18 - 19, The workflow
clones google/woff2 without pinning, making builds unreproducible; update each
git clone step that runs "git clone --recursive --depth 1
https://github.com/google/woff2.git /tmp/woff2" (the "Clone google/woff2" steps)
to check out a specific commit or tag by either using a branch/tag ref (e.g.,
add "--branch <tag-or-commit>" or clone then run "git checkout <commit-or-tag>")
or appending "@<commit>" in the URL, ensuring all occurrences (the three clone
steps) are changed to the same pinned commit/tag for reproducible builds.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt`:
- Around line 62-66: The parseCodepoints function is treating Bootstrap
codepoint strings as decimal; update parseCodepoints (in BootstrapRepository) to
parse values as hexadecimal by calling toIntOrNull(16), first trimming any "0x"
prefix and whitespace from value.jsonPrimitive.content; ensure the map still
filters nulls the same way so downstream code that constructs Chars from
icon.codepoint receives correct integer codepoints.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/NativeLibraryLoader.kt`:
- Around line 31-32: The code currently calls tempDir.mkdirs() without checking
its return; update the NativeLibraryLoader logic to verify the directory exists
after attempting creation by checking tempDir.exists() or tempDir.isDirectory(),
and if mkdirs() returned false and the directory does not exist, throw or log a
descriptive exception (including the path in tempDir) so the failure is explicit
before any file writes; reference the tempDir variable and the mkdirs() call in
NativeLibraryLoader.kt when making this change.

---

Duplicate comments:
In `@tools/idea-plugin/native/woff2decoder.cpp`:
- Around line 11-33: Get and use JNI resources defensively: after calling
env->GetByteArrayElements (input_bytes) check for a null return and
env->ExceptionCheck() and return nullptr if allocation/attachment failed; after
creating the Java output buffer with env->NewByteArray check for null and
pending exceptions and clean up (release input) before returning; always call
env->ReleaseByteArrayElements(input_bytes, ...) with JNI_ABORT (not 0) since we
only read from the input; ensure any early returns release resources and
propagate failure by returning nullptr. Target these changes around
GetByteArrayElements, GetArrayLength, NewByteArray, and ReleaseByteArrayElements
in woff2decoder.cpp and add appropriate env->ExceptionCheck() checks and cleanup
paths.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/NativeLibraryLoader.kt`:
- Around line 31-40: The code in NativeLibraryLoader creates a predictable temp
path (tempDir = File(System.getProperty("java.io.tmpdir"), "valkyrie-native")
and tempFile = File(tempDir, fileName), which risks collisions and symlink
attacks; change to create a secure, unique temp location using the platform temp
APIs (e.g., Files.createTempDirectory or Files.createTempFile) and write the
resource into that securely-created file, set restrictive permissions, and call
deleteOnExit; update symbols: tempDir, tempFile, fileName, resourceStream, and
the code path in NativeLibraryLoader that copies the stream so it uses the
unique temp file instead of the fixed "valkyrie-native/$fileName" path.

---

Nitpick comments:
In @.github/workflows/build-native-libs.yml:
- Around line 57-61: Update each actions/upload-artifact@v4 step (the "Upload
artifact" step blocks) to include a retention-days input (e.g., retention-days:
30) to control artifact storage, and add a new final job (e.g.,
"consolidate-artifacts") that depends on the build jobs, uses
actions/download-artifact to fetch each uploaded artifact by name, consolidates
them into a single archive, and then uploads that archive with
actions/upload-artifact (or creates a release) for easier consumption; apply the
retention-days change to the other upload steps referenced (lines 129-133 and
218-222) as well.
- Around line 18-19: The workflow clones google/woff2 without pinning, making
builds unreproducible; update each git clone step that runs "git clone
--recursive --depth 1 https://github.com/google/woff2.git /tmp/woff2" (the
"Clone google/woff2" steps) to check out a specific commit or tag by either
using a branch/tag ref (e.g., add "--branch <tag-or-commit>" or clone then run
"git checkout <commit-or-tag>") or appending "@<commit>" in the URL, ensuring
all occurrences (the three clone steps) are changed to the same pinned
commit/tag for reproducible builds.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8a2822 and f15e925.

⛔ Files ignored due to path filters (3)
  • tools/idea-plugin/src/main/resources/native/linux-x64/libwoff2decoder.so is excluded by !**/*.so
  • tools/idea-plugin/src/main/resources/native/macos/libwoff2decoder.dylib is excluded by !**/*.dylib
  • tools/idea-plugin/src/main/resources/native/windows-x64/woff2decoder.dll is excluded by !**/*.dll
📒 Files selected for processing (5)
  • .github/workflows/build-native-libs.yml
  • tools/idea-plugin/native/woff2decoder.cpp
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/NativeLibraryLoader.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/Woff2Decoder.kt

@egorikftp
Copy link
Member

@t-regbs We verified on different platforms (also migrated all icon providers to use woff2 + conversion) and everything is working without crash. Let's process with this solution!

@t-regbs t-regbs force-pushed the feature/bootstrap-icons branch from 9a84ca1 to 7b4e36f Compare February 24, 2026 00:58
@t-regbs t-regbs marked this pull request as ready for review February 24, 2026 00:58
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/idea-plugin/CHANGELOG.md (1)

32-39: ⚠️ Potential issue | 🟡 Minor

Changelog entry appears under a past release section.

Line 38 adds the Bootstrap provider under 1.1.0 – 2026-02-13, which is already in the past (today is 2026-02-24). If this change is for the upcoming release, move it to Unreleased; otherwise please confirm it’s a backport.

✏️ Suggested changelog placement
@@
 ### Added
@@
+- Add `Bootstrap` icon provider into Web import feature
@@
 ## 1.1.0 - 2026-02-13
 ### Added
@@
-- Add `Bootstrap` icon provider into Web import feature
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/idea-plugin/CHANGELOG.md` around lines 32 - 39, The changelog entry
adding the Bootstrap icon provider is placed under the past release header "##
1.1.0 - 2026-02-13" but appears to be for an upcoming change; move the bullet "-
Add `Bootstrap` icon provider into Web import feature" out of the "## 1.1.0 -
2026-02-13" section and place it under the "## Unreleased" section (or create an
"## Unreleased" section at the top if missing), or if this is intentionally a
backport, annotate the entry to indicate it is a backport to 1.1.0 (e.g., add
"(backport)" after the bullet) so the placement is explicit; update only the
CHANGELOG.md contents accordingly.
🧹 Nitpick comments (9)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/domain/model/font/FontSettings.kt (1)

18-19: Consider including iconFontFamily in isModified.

Line 18–19 currently ignores iconFontFamily, so changing the family won’t enable Reset. If family selection is user-editable, add it to the predicate (or document why it’s intentionally excluded).

Proposed tweak
-    get() = fill || weight != 400 || grade != 0 || opticalSize != 24f
+    get() = fill || weight != 400 || grade != 0 || opticalSize != 24f ||
+        iconFontFamily != IconFontFamily.OUTLINED
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/domain/model/font/FontSettings.kt`
around lines 18 - 19, The isModified getter in FontSettings currently checks
fill, weight, grade, and opticalSize but omits iconFontFamily, so changes to the
family won't enable the Reset state; update the override val isModified: Boolean
getter to include a comparison against the default iconFontFamily (e.g., add "||
iconFontFamily != <defaultValue>") or, if iconFontFamily is nullable/use a
specific default constant, compare appropriately (reference isModified and
iconFontFamily in class FontSettings) so selecting a different family toggles
modification; alternatively, document why iconFontFamily is intentionally
excluded if that is desired behavior.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/data/LucideRepository.kt (1)

32-43: loadIconList() silently hits the network on every call — consider adding a cache.

loadFontBytes() and loadCodepoints() are both guarded by a mutex + nullable-cache pattern, but loadIconList() has no equivalent. If the caller invokes it on repeated screen entries or recompositions, it will perform redundant network round-trips to tags.json.

♻️ Suggested cache addition (consistent with existing pattern)
 private val fontMutex = Mutex()
 private val codepointMutex = Mutex()
+private val iconListMutex = Mutex()
 private var fontBytesCache: ByteArray? = null
 private var codepointsCache: Map<String, Int>? = null
+private var iconListCache: List<Pair<String, LucideIconMetadata>>? = null
-    suspend fun loadIconList(): List<Pair<String, LucideIconMetadata>> = withContext(Dispatchers.IO) {
-        val response = httpClient.get("$UNPKG_BASE/tags.json")
-        val tagsJson = json.parseToJsonElement(response.bodyAsText()) as JsonObject
-
-        tagsJson.entries.map { (iconName, tagsArray) ->
-            val tags = tagsArray.jsonArray.map { it.jsonPrimitive.content }
-            iconName to LucideIconMetadata(
-                tags = tags,
-                categories = emptyList(),
-            )
-        }
-    }
+    suspend fun loadIconList(): List<Pair<String, LucideIconMetadata>> = withContext(Dispatchers.IO) {
+        iconListMutex.withLock {
+            iconListCache ?: run {
+                val response = httpClient.get("$UNPKG_BASE/tags.json")
+                val tagsJson = json.parseToJsonElement(response.bodyAsText()) as JsonObject
+                val result = tagsJson.entries.map { (iconName, tagsArray) ->
+                    val tags = tagsArray.jsonArray.map { it.jsonPrimitive.content }
+                    iconName to LucideIconMetadata(
+                        tags = tags,
+                        categories = emptyList(),
+                    )
+                }
+                iconListCache = result
+                result
+            }
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/data/LucideRepository.kt`
around lines 32 - 43, loadIconList() currently performs a network request on
every call; add the same mutex + nullable-cache pattern used by
loadFontBytes()/loadCodepoints() to avoid redundant requests. Introduce a
private nullable cache (e.g., iconListCache: List<Pair<String,
LucideIconMetadata>>?) and a Mutex (e.g., iconListMutex = Mutex()), then change
loadIconList() to first run iconListMutex.withLock { if (iconListCache == null)
{ perform the current withContext(Dispatchers.IO) fetch/parse and assign
iconListCache } return iconListCache!! } so the network is only hit once and
subsequent callers return the cached list.
.github/workflows/build-native-libs.yml (4)

6-222: No smoke test to verify the built libraries are actually loadable.

Each job verifies the output file exists (file, ls -lh, Get-Item), but none checks that the library exports the expected JNI symbol. A quick nm/objdump/dumpbin check would catch missing symbols or link errors early:

# Linux
nm -D libwoff2decoder.so | grep Java_io_github_composegears_valkyrie_util_font_Woff2Decoder_decodeBytes

# macOS
nm libwoff2decoder.dylib | grep Java_io_github_composegears_valkyrie_util_font_Woff2Decoder_decodeBytes

# Windows (in MSVC shell)
dumpbin /exports woff2decoder.dll | findstr decodeBytes
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build-native-libs.yml around lines 6 - 222, Add a
smoke-test step after each "Verify library" step that checks the JNI export
Java_io_github_composegears_valkyrie_util_font_Woff2Decoder_decodeBytes is
present and fails the job if not found: for Linux run nm -D against
libwoff2decoder.so and grep the symbol; for macOS run nm against
libwoff2decoder.dylib and grep the symbol; for Windows run dumpbin /exports
against woff2decoder.dll and findstr the symbol; ensure the step uses the same
output filenames used in Build JNI shared library / Create universal binary /
Build JNI DLL to locate the files.

99-123: Windows library discovery is fragile.

The Get-ChildItem -Recurse -Filter approach (lines 105–108) for locating .lib files works but is sensitive to build output layout changes across CMake/MSVC versions. The Where-Object filter on line 107–108 matching 'static' or an exact name could also miss renamed outputs. Consider logging a failure and exiting early if any library is not found:

🛡️ Suggested guard
+          if (-not $woff2dec) { throw "woff2dec.lib not found" }
+          if (-not $woff2common) { throw "woff2common.lib not found" }
+          if (-not $brotlidec) { throw "brotlidec lib not found" }
+          if (-not $brotlicommon) { throw "brotlicommon lib not found" }
+
           Write-Host "woff2dec: $($woff2dec.FullName)"

Without this, if a library isn't found, the cl invocation receives an empty argument and fails with a cryptic error.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build-native-libs.yml around lines 99 - 123, The
PowerShell step currently uses $woff2dec, $woff2common, $brotlidec, and
$brotlicommon and then calls cl with their FullName, but it doesn't check
whether those Get-ChildItem lookups returned anything; if any is null cl will
get an empty argument and fail cryptically. Add a guard after the lookups that
checks each of $woff2dec, $woff2common, $brotlidec, $brotlicommon for
null/empty, Write-Host/Write-Error with a clear message including which variable
is missing and the search patterns used, and then exit the script early
(non-zero) so the job fails fast; keep the existing diagnostic Write-Host lines
and consider widening the matching logic or logging the paths you searched to
aid debugging.

19-19: The google/woff2 clone is unpinned — builds are not reproducible.

git clone --recursive --depth 1 fetches whatever HEAD is at the time. If google/woff2 pushes a breaking change or introduces a bug, subsequent workflow runs will silently pick it up. Consider pinning to a specific commit or tag.

♻️ Pin to a specific commit
-        run: git clone --recursive --depth 1 https://github.com/google/woff2.git /tmp/woff2
+        run: |
+          git clone --recursive https://github.com/google/woff2.git /tmp/woff2
+          cd /tmp/woff2 && git checkout <known-good-commit-sha>

This applies to lines 19, 78, and 147 (all three jobs).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build-native-libs.yml at line 19, The git clone of
google/woff2 uses an unpinned checkout ("git clone --recursive --depth 1
https://github.com/google/woff2.git /tmp/woff2") in three places; replace each
unpinned clone with a reproducible checkout by cloning then checking out a
specific commit or tag (e.g., run: git clone --recursive
https://github.com/google/woff2.git /tmp/woff2 && cd /tmp/woff2 && git checkout
<COMMIT_SHA> && git submodule update --init --recursive), or use --branch <TAG>
--single-branch if you pin to a tag, ensuring the same commit is used every
workflow run.

42-42: Inconsistent C++ standard: Linux uses c++11, Windows uses c++14.

Line 42 (Linux) uses -std=c++11 and line 115 (Windows) uses /std:c++14. Line 169/199 (macOS) uses -std=c++11. While google/woff2 should compile with either, the inconsistency could mask platform-specific compilation issues. Consider aligning all three to the same standard.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/build-native-libs.yml at line 42, The workflow uses mixed
C++ standards: change all occurrences of the compiler standard flags so they are
consistent (e.g., use C++14 everywhere); specifically replace the g++ flags
"-std=c++11" (used in the Linux and macOS g++ command lines such as the g++
-shared -std=c++11 -O2 -fPIC invocation) with "-std=c++14", and update the
Windows cl/MSVC flag "/std:c++14" if you choose a different standard so all
three platforms use the same flag (either all "/std:c++14" for MSVC and
"-std=c++14" for gcc/clang, or all on c++11 consistently) to ensure uniform
compilation semantics across Linux, macOS and Windows.
tools/idea-plugin/native/woff2decoder.cpp (1)

37-37: Narrowing cast from size_t to jsize (signed 32-bit).

out.Size() returns size_t (up to 64-bit unsigned), while jsize is jint (signed 32-bit, max ~2 GB). The allocation is capped by woff2::kDefaultMaxSize (256 MB), so this is safe in practice, but a defensive bounds check would make the intent explicit.

🛡️ Optional defensive check
+    if (out.Size() > static_cast<size_t>(std::numeric_limits<jsize>::max())) {
+        fprintf(stderr, "woff2decoder: output too large for JNI array\n");
+        env->ReleaseByteArrayElements(input_bytes, reinterpret_cast<jbyte *>(input_data), JNI_ABORT);
+        return nullptr;
+    }
+
     jbyteArray arr = env->NewByteArray(static_cast<jsize>(out.Size()));

(Requires #include <limits>)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/idea-plugin/native/woff2decoder.cpp` at line 37, The call
env->NewByteArray(static_cast<jsize>(out.Size())) narrows a size_t to jsize; add
a defensive bounds check before allocating: verify out.Size() <=
std::min<std::size_t>(static_cast<std::size_t>(std::numeric_limits<jsize>::max()),
woff2::kDefaultMaxSize) (include <limits>), and if it exceeds the limit, handle
gracefully (e.g., throw a Java exception via env->ThrowNew or return nullptr)
instead of performing the unsafe cast; keep the allocation using
env->NewByteArray with a safe static_cast<jsize> after the check and reference
out.Size(), env->NewByteArray, and woff2::kDefaultMaxSize in your change.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/domain/BootstrapUseCase.kt (1)

25-33: Consider wiring real tags when Bootstrap metadata becomes available.

tags = emptyList() is passed hardcoded, so category inference falls back to name-only matching. The Bootstrap Icons font package includes per-icon tag metadata (in bootstrap-icons.json). If BootstrapRepository.loadIconList() is ever extended to expose those tags, substituting them here would noticeably improve category accuracy — especially for icons where the name alone is ambiguous.

No change required now; just flagging as a natural follow-up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/domain/BootstrapUseCase.kt`
around lines 25 - 33, The current mapping that builds StandardIcon objects uses
tags = emptyList(), which forces inferCategoryFromTags(name, emptyList()) to
only use the name; when BootstrapRepository.loadIconList() is later extended to
return per-icon tag metadata, replace the emptyList() usage in the
codepoints.map block that constructs StandardIcon (the block creating
StandardIcon with name/toDisplayName/codepoint) so that you pass the real tag
list into both the StandardIcon.tags field and into inferCategoryFromTags(name,
tags); this ensures category inference uses the real tags instead of falling
back to name-only matching.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt (1)

281-303: Runtime cast on IconItem content is safe but unguarded.

Line 300 casts item.icon to StandardIcon at runtime. Since StandardIconGrid is private and exclusively fed by StandardState.Success.gridItems (which always contains StandardIcon items), this cannot fail today. However, there's no compile-time guarantee — a future refactor that passes differently typed GridItems would surface as a ClassCastException at runtime rather than a build error.

Consider a defensive cast to make the failure mode explicit:

♻️ Defensive cast proposal
- is IconItem<*> -> iconContent(item.icon as StandardIcon)
+ is IconItem<*> -> (item.icon as? StandardIcon)?.let { iconContent(it) }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt`
around lines 281 - 303, The code performs an unchecked cast item.icon as
StandardIcon inside StandardIconGrid which can throw at runtime; update the when
branch for IconItem to perform a safe cast (e.g., val icon = (item as
IconItem<*>).icon as? StandardIcon) and only call iconContent(icon) when the
cast succeeds (otherwise skip or render a fallback), or more strongly, change
the gridItems typing so IconItem is IconItem<StandardIcon> to make the contract
compile-time enforced; reference StandardIconGrid, GridItem, IconItem,
iconContent and StandardIcon when applying the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@tools/idea-plugin/CHANGELOG.md`:
- Around line 32-39: The changelog entry adding the Bootstrap icon provider is
placed under the past release header "## 1.1.0 - 2026-02-13" but appears to be
for an upcoming change; move the bullet "- Add `Bootstrap` icon provider into
Web import feature" out of the "## 1.1.0 - 2026-02-13" section and place it
under the "## Unreleased" section (or create an "## Unreleased" section at the
top if missing), or if this is intentionally a backport, annotate the entry to
indicate it is a backport to 1.1.0 (e.g., add "(backport)" after the bullet) so
the placement is explicit; update only the CHANGELOG.md contents accordingly.

---

Nitpick comments:
In @.github/workflows/build-native-libs.yml:
- Around line 6-222: Add a smoke-test step after each "Verify library" step that
checks the JNI export
Java_io_github_composegears_valkyrie_util_font_Woff2Decoder_decodeBytes is
present and fails the job if not found: for Linux run nm -D against
libwoff2decoder.so and grep the symbol; for macOS run nm against
libwoff2decoder.dylib and grep the symbol; for Windows run dumpbin /exports
against woff2decoder.dll and findstr the symbol; ensure the step uses the same
output filenames used in Build JNI shared library / Create universal binary /
Build JNI DLL to locate the files.
- Around line 99-123: The PowerShell step currently uses $woff2dec,
$woff2common, $brotlidec, and $brotlicommon and then calls cl with their
FullName, but it doesn't check whether those Get-ChildItem lookups returned
anything; if any is null cl will get an empty argument and fail cryptically. Add
a guard after the lookups that checks each of $woff2dec, $woff2common,
$brotlidec, $brotlicommon for null/empty, Write-Host/Write-Error with a clear
message including which variable is missing and the search patterns used, and
then exit the script early (non-zero) so the job fails fast; keep the existing
diagnostic Write-Host lines and consider widening the matching logic or logging
the paths you searched to aid debugging.
- Line 19: The git clone of google/woff2 uses an unpinned checkout ("git clone
--recursive --depth 1 https://github.com/google/woff2.git /tmp/woff2") in three
places; replace each unpinned clone with a reproducible checkout by cloning then
checking out a specific commit or tag (e.g., run: git clone --recursive
https://github.com/google/woff2.git /tmp/woff2 && cd /tmp/woff2 && git checkout
<COMMIT_SHA> && git submodule update --init --recursive), or use --branch <TAG>
--single-branch if you pin to a tag, ensuring the same commit is used every
workflow run.
- Line 42: The workflow uses mixed C++ standards: change all occurrences of the
compiler standard flags so they are consistent (e.g., use C++14 everywhere);
specifically replace the g++ flags "-std=c++11" (used in the Linux and macOS g++
command lines such as the g++ -shared -std=c++11 -O2 -fPIC invocation) with
"-std=c++14", and update the Windows cl/MSVC flag "/std:c++14" if you choose a
different standard so all three platforms use the same flag (either all
"/std:c++14" for MSVC and "-std=c++14" for gcc/clang, or all on c++11
consistently) to ensure uniform compilation semantics across Linux, macOS and
Windows.

In `@tools/idea-plugin/native/woff2decoder.cpp`:
- Line 37: The call env->NewByteArray(static_cast<jsize>(out.Size())) narrows a
size_t to jsize; add a defensive bounds check before allocating: verify
out.Size() <=
std::min<std::size_t>(static_cast<std::size_t>(std::numeric_limits<jsize>::max()),
woff2::kDefaultMaxSize) (include <limits>), and if it exceeds the limit, handle
gracefully (e.g., throw a Java exception via env->ThrowNew or return nullptr)
instead of performing the unsafe cast; keep the allocation using
env->NewByteArray with a safe static_cast<jsize> after the check and reference
out.Size(), env->NewByteArray, and woff2::kDefaultMaxSize in your change.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/domain/model/font/FontSettings.kt`:
- Around line 18-19: The isModified getter in FontSettings currently checks
fill, weight, grade, and opticalSize but omits iconFontFamily, so changes to the
family won't enable the Reset state; update the override val isModified: Boolean
getter to include a comparison against the default iconFontFamily (e.g., add "||
iconFontFamily != <defaultValue>") or, if iconFontFamily is nullable/use a
specific default constant, compare appropriately (reference isModified and
iconFontFamily in class FontSettings) so selecting a different family toggles
modification; alternatively, document why iconFontFamily is intentionally
excluded if that is desired behavior.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/domain/BootstrapUseCase.kt`:
- Around line 25-33: The current mapping that builds StandardIcon objects uses
tags = emptyList(), which forces inferCategoryFromTags(name, emptyList()) to
only use the name; when BootstrapRepository.loadIconList() is later extended to
return per-icon tag metadata, replace the emptyList() usage in the
codepoints.map block that constructs StandardIcon (the block creating
StandardIcon with name/toDisplayName/codepoint) so that you pass the real tag
list into both the StandardIcon.tags field and into inferCategoryFromTags(name,
tags); this ensures category inference uses the real tags instead of falling
back to name-only matching.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/data/LucideRepository.kt`:
- Around line 32-43: loadIconList() currently performs a network request on
every call; add the same mutex + nullable-cache pattern used by
loadFontBytes()/loadCodepoints() to avoid redundant requests. Introduce a
private nullable cache (e.g., iconListCache: List<Pair<String,
LucideIconMetadata>>?) and a Mutex (e.g., iconListMutex = Mutex()), then change
loadIconList() to first run iconListMutex.withLock { if (iconListCache == null)
{ perform the current withContext(Dispatchers.IO) fetch/parse and assign
iconListCache } return iconListCache!! } so the network is only hit once and
subsequent callers return the cached list.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt`:
- Around line 281-303: The code performs an unchecked cast item.icon as
StandardIcon inside StandardIconGrid which can throw at runtime; update the when
branch for IconItem to perform a safe cast (e.g., val icon = (item as
IconItem<*>).icon as? StandardIcon) and only call iconContent(icon) when the
cast succeeds (otherwise skip or render a fallback), or more strongly, change
the gridItems typing so IconItem is IconItem<StandardIcon> to make the contract
compile-time enforced; reference StandardIconGrid, GridItem, IconItem,
iconContent and StandardIcon when applying the fix.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c8a2822 and 7b4e36f.

⛔ Files ignored due to path filters (3)
  • tools/idea-plugin/src/main/resources/native/linux-x64/libwoff2decoder.so is excluded by !**/*.so
  • tools/idea-plugin/src/main/resources/native/macos/libwoff2decoder.dylib is excluded by !**/*.dylib
  • tools/idea-plugin/src/main/resources/native/windows-x64/woff2decoder.dll is excluded by !**/*.dll
📒 Files selected for processing (42)
  • .github/workflows/build-native-libs.yml
  • sdk/compose/icons/api/icons.api
  • sdk/compose/icons/api/icons.klib.api
  • sdk/compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/sdk/compose/icons/colored/BootstrapLogo.kt
  • tools/idea-plugin/CHANGELOG.md
  • tools/idea-plugin/native/woff2decoder.cpp
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportFlow.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/model/IconSettings.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/CustomizationToolbar.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/IconSizeCustomization.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/Category.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideConfig.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideIcon.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideSettings.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/domain/model/font/FontSettings.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/ui/FontCustomization.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardIconViewModel.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/BootstrapImportScreen.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/di/BootstrapModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/domain/BootstrapUseCase.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/CategoryInferrer.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/DisplayNameFormatter.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/StandardIconProvider.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/SvgSizeCustomizer.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/LucideImportScreen.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/data/LucideIconMetadata.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/data/LucideRepository.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/di/LucideModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/domain/LucideUseCase.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/InferredCategory.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/SizeSettings.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/StandardIcon.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/StandardIconConfig.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/ui/StandardTopActions.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/util/StandardGridFiltering.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/NativeLibraryLoader.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/Woff2Decoder.kt
  • tools/idea-plugin/src/main/resources/messages/Valkyrie.properties
💤 Files with no reviewable changes (5)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideSettings.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideIcon.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/Category.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideConfig.kt
🚧 Files skipped from review as they are similar to previous changes (14)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/CustomizationToolbar.kt
  • tools/idea-plugin/src/main/resources/messages/Valkyrie.properties
  • sdk/compose/icons/api/icons.klib.api
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/util/StandardGridFiltering.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/StandardIconConfig.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/domain/LucideUseCase.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportFlow.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/LucideImportScreen.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/di/LucideModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/StandardIcon.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/model/IconSettings.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/SizeSettings.kt

@egorikftp egorikftp force-pushed the feature/bootstrap-icons branch from 7b4e36f to bba0bd7 Compare February 24, 2026 15:56
@egorikftp egorikftp changed the title [Plugin] Add Bootstrap Icons [Plugin] Add Bootstrap Icons and woff2 to ttf native decoder Feb 24, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt (1)

62-68: Codepoint parsing — decimal format confirmed correct.

As previously discussed, the Bootstrap Icons JSON from unpkg uses decimal integers (e.g., 61697), so toIntOrNull() without a radix is the correct approach here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt`
around lines 62 - 68, The parseCodepoints function correctly parses decimal
codepoints using toIntOrNull(), so no code change is needed; instead resolve the
duplicate review note by removing the duplicate comment marker or marking the
earlier comment as addressed in the PR discussion and confirm parseCodepoints
(function name parseCodepoints and usage of toIntOrNull on
value.jsonPrimitive.content) remains unchanged.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/CategoryInferrer.kt (1)

20-52: "key" substring matching — already acknowledged.

The "key" keyword (Line 28) will match icon names like "keyboard", but this was previously discussed and accepted as best-effort categorization.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/CategoryInferrer.kt`
around lines 20 - 52, The CATEGORY_KEYWORDS list contains an entry
CategoryKeyword("key", "Security", priority = 1) which will match substrings
like "keyboard"; leave the keyword but make matching explicit to avoid
unintended hits by changing the matching logic to prefer whole-token matches or
boundary-aware checks: update the code that uses CATEGORY_KEYWORDS (the function
that performs keyword matching against icon names—search for where
CATEGORY_KEYWORDS is consumed) to check for whole-word boundaries or tokenized
icon names before accepting "key" as a match, and ensure fallback behavior still
works when no boundary match is found; keep the existing CATEGORY_KEYWORDS
entries unchanged.
🧹 Nitpick comments (3)
tools/idea-plugin/src/main/resources/messages/Valkyrie.properties (1)

129-130: Nitpick: unit spacing inconsistency with the existing dp suffix key.

{0}px has no space before the unit, while the pre-existing web.import.font.customize.material.dp.suffix uses {0} dp (with a space). Neither is wrong on its own, but it's worth aligning them for consistency if this customization panel ever renders both units side-by-side.

✏️ Option to align spacing
-web.import.font.customize.px.suffix={0}px
+web.import.font.customize.px.suffix={0} px
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/idea-plugin/src/main/resources/messages/Valkyrie.properties` around
lines 129 - 130, The px suffix property is inconsistent with the dp suffix
spacing; update the property value for web.import.font.customize.px.suffix from
"{0}px" to "{0} px" so it matches the spacing style used by
web.import.font.customize.material.dp.suffix — edit the messages entry for
web.import.font.customize.px.suffix accordingly.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardIconViewModel.kt (1)

154-159: Potential race condition in updateSuccess with concurrent callers.

selectCategory (Line 123) and updateSearchQuery (Line 133) both launch on Dispatchers.Default and call updateSuccess, which performs a non-atomic read-modify-write on stateRecord.value. If a user types a query while a category selection is still processing, one update could be lost.

In practice this is unlikely since UI interactions are mostly sequential, but worth noting if concurrency increases.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardIconViewModel.kt`
around lines 154 - 159, The updateSuccess function does a non-atomic
read-modify-write on stateRecord.value which can lose updates when multiple
coroutines (e.g., selectCategory and updateSearchQuery) call it concurrently;
change updateSuccess to perform an atomic update by using a compare-and-set loop
(CAS) or the atomic update helper for your state type (e.g.,
MutableStateFlow.update or AtomicReference.compareAndSet): read current, compute
new via transform(current), then attempt to atomically replace the value and
retry on failure; keep the function name updateSuccess and ensure callers
selectCategory and updateSearchQuery keep calling it unchanged.
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/domain/BootstrapUseCase.kt (1)

25-33: Bootstrap icons lack tag metadata, reducing categorization and searchability

BootstrapRepository.loadIconList() returns only a Map<String, Int> (name→codepoint), while the semantically equivalent LucideRepository.loadIconList() returns List<Pair<String, LucideIconMetadata>> with tags. This causes StandardIcon instances for Bootstrap to carry empty tags, which means:

  1. Filtering limitation: No tag-based search/filtering in the UI
  2. Categorization gap: inferCategoryFromTags(name, emptyList()) can only match on icon names; the tag-matching branch never fires. Icons with non-descriptive names fall back to "General" category instead of being properly categorized

For consistency with Lucide and to improve UX, extend BootstrapRepository to parse tags from the bootstrap-icons.json source and update loadIconList() to return a richer type that includes them.

♻️ Required changes
  1. Create a metadata DTO for Bootstrap icons (similar to LucideIconMetadata)
  2. Update BootstrapRepository.loadIconList() to return structured metadata with tags
  3. Update BootstrapUseCase.loadConfig() to destructure tags and pass them to both StandardIcon.tags and inferCategoryFromTags()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/domain/BootstrapUseCase.kt`
around lines 25 - 33, Bootstrap icons currently lose tag metadata because
BootstrapRepository.loadIconList() returns Map<String,Int>; update it to parse
bootstrap-icons.json into a metadata DTO (e.g., BootstrapIconMetadata with tags:
List<String>) and change BootstrapRepository.loadIconList() to return
List<Pair<String,BootstrapIconMetadata>> (matching
LucideRepository.loadIconList() shape). Then update
BootstrapUseCase.loadConfig() to destructure the returned pairs and populate
StandardIcon(tags = metadata.tags) and call inferCategoryFromTags(name,
metadata.tags) so tags are used for both filtering and category inference.
Ensure DTO name and functions referenced are BootstrapIconMetadata,
BootstrapRepository.loadIconList(), BootstrapUseCase.loadConfig(), StandardIcon
and inferCategoryFromTags.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/build-native-libs.yml:
- Around line 18-19: The workflow clones google/woff2 without pinning, causing
non-reproducible builds; update each job that runs the git clone (the steps in
the build-linux, build-windows, and build-macos jobs that run the command "git
clone --recursive --depth 1 https://github.com/google/woff2.git /tmp/woff2") to
pin to a fixed ref by using an environment variable (e.g. WOFF2_REF) and
appending "@<ref>" or switching to "git clone --branch $WOFF2_REF --depth 1
https://github.com/google/woff2.git /tmp/woff2" (choose a stable tag like v1.0.2
or a specific commit SHA) and ensure the same WOFF2_REF is used in all three
clone steps (repeat this change for the other two clone occurrences).

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/domain/model/font/FontSettings.kt`:
- Around line 18-19: The isModified getter in FontSettings currently ignores
changes to iconFontFamily; update the override of isModified to include a
comparison for iconFontFamily (e.g. add "|| iconFontFamily !=
<defaultIconFontFamily>" or compare against the initial/default value used by
FontSettings) so any change to iconFontFamily flips isModified to true; modify
the boolean expression in the isModified property in FontSettings.kt to include
that check.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt`:
- Around line 21-26: The companion object constants UNPKG_BASE, FONT_URL and
JSON_URL in BootstrapRepository currently use the unstable "@latest" tag which
can cause font/codepoint mismatches; change the approach to pin a stable version
(e.g., replace "@latest" with a constant VERSION like "bootstrap-icons@1.11.3")
or implement logic in BootstrapRepository to resolve and cache the exact version
from the first fetched URL and reuse that resolved base for FONT_URL and
JSON_URL (update UNPKG_BASE, FONT_URL, JSON_URL and any consumers such as
ICONS_BASE_URL to reference the pinned VERSION or the resolved cached base so
both JSON and font use the same exact version).

---

Duplicate comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt`:
- Around line 62-68: The parseCodepoints function correctly parses decimal
codepoints using toIntOrNull(), so no code change is needed; instead resolve the
duplicate review note by removing the duplicate comment marker or marking the
earlier comment as addressed in the PR discussion and confirm parseCodepoints
(function name parseCodepoints and usage of toIntOrNull on
value.jsonPrimitive.content) remains unchanged.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/CategoryInferrer.kt`:
- Around line 20-52: The CATEGORY_KEYWORDS list contains an entry
CategoryKeyword("key", "Security", priority = 1) which will match substrings
like "keyboard"; leave the keyword but make matching explicit to avoid
unintended hits by changing the matching logic to prefer whole-token matches or
boundary-aware checks: update the code that uses CATEGORY_KEYWORDS (the function
that performs keyword matching against icon names—search for where
CATEGORY_KEYWORDS is consumed) to check for whole-word boundaries or tokenized
icon names before accepting "key" as a match, and ensure fallback behavior still
works when no boundary match is found; keep the existing CATEGORY_KEYWORDS
entries unchanged.

---

Nitpick comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/domain/BootstrapUseCase.kt`:
- Around line 25-33: Bootstrap icons currently lose tag metadata because
BootstrapRepository.loadIconList() returns Map<String,Int>; update it to parse
bootstrap-icons.json into a metadata DTO (e.g., BootstrapIconMetadata with tags:
List<String>) and change BootstrapRepository.loadIconList() to return
List<Pair<String,BootstrapIconMetadata>> (matching
LucideRepository.loadIconList() shape). Then update
BootstrapUseCase.loadConfig() to destructure the returned pairs and populate
StandardIcon(tags = metadata.tags) and call inferCategoryFromTags(name,
metadata.tags) so tags are used for both filtering and category inference.
Ensure DTO name and functions referenced are BootstrapIconMetadata,
BootstrapRepository.loadIconList(), BootstrapUseCase.loadConfig(), StandardIcon
and inferCategoryFromTags.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardIconViewModel.kt`:
- Around line 154-159: The updateSuccess function does a non-atomic
read-modify-write on stateRecord.value which can lose updates when multiple
coroutines (e.g., selectCategory and updateSearchQuery) call it concurrently;
change updateSuccess to perform an atomic update by using a compare-and-set loop
(CAS) or the atomic update helper for your state type (e.g.,
MutableStateFlow.update or AtomicReference.compareAndSet): read current, compute
new via transform(current), then attempt to atomically replace the value and
retry on failure; keep the function name updateSuccess and ensure callers
selectCategory and updateSearchQuery keep calling it unchanged.

In `@tools/idea-plugin/src/main/resources/messages/Valkyrie.properties`:
- Around line 129-130: The px suffix property is inconsistent with the dp suffix
spacing; update the property value for web.import.font.customize.px.suffix from
"{0}px" to "{0} px" so it matches the spacing style used by
web.import.font.customize.material.dp.suffix — edit the messages entry for
web.import.font.customize.px.suffix accordingly.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7b4e36f and bba0bd7.

⛔ Files ignored due to path filters (3)
  • tools/idea-plugin/src/main/resources/native/linux-x64/libwoff2decoder.so is excluded by !**/*.so
  • tools/idea-plugin/src/main/resources/native/macos/libwoff2decoder.dylib is excluded by !**/*.dylib
  • tools/idea-plugin/src/main/resources/native/windows-x64/woff2decoder.dll is excluded by !**/*.dll
📒 Files selected for processing (42)
  • .github/workflows/build-native-libs.yml
  • sdk/compose/icons/api/icons.api
  • sdk/compose/icons/api/icons.klib.api
  • sdk/compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/sdk/compose/icons/colored/BootstrapLogo.kt
  • tools/idea-plugin/CHANGELOG.md
  • tools/idea-plugin/native/woff2decoder.cpp
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportFlow.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/model/IconSettings.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/CustomizationToolbar.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/IconSizeCustomization.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/Category.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideConfig.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideIcon.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideSettings.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/domain/model/font/FontSettings.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/ui/FontCustomization.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardIconViewModel.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/BootstrapImportScreen.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/di/BootstrapModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/domain/BootstrapUseCase.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/CategoryInferrer.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/DisplayNameFormatter.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/StandardIconProvider.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/SvgSizeCustomizer.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/LucideImportScreen.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/data/LucideIconMetadata.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/data/LucideRepository.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/di/LucideModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/domain/LucideUseCase.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/InferredCategory.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/SizeSettings.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/StandardIcon.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/StandardIconConfig.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/ui/StandardTopActions.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/util/StandardGridFiltering.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/NativeLibraryLoader.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/Woff2Decoder.kt
  • tools/idea-plugin/src/main/resources/messages/Valkyrie.properties
💤 Files with no reviewable changes (5)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/Category.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideSettings.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideConfig.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideIcon.kt
✅ Files skipped from review due to trivial changes (2)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/SizeSettings.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/lucide/data/LucideRepository.kt
🚧 Files skipped from review as they are similar to previous changes (13)
  • sdk/compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/sdk/compose/icons/colored/BootstrapLogo.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/Woff2Decoder.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/StandardIconProvider.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportFlow.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/di/BootstrapModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt
  • sdk/compose/icons/api/icons.klib.api
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/CustomizationToolbar.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/font/NativeLibraryLoader.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/StandardIconConfig.kt
  • tools/idea-plugin/CHANGELOG.md
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/model/InferredCategory.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/domain/DisplayNameFormatter.kt

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt (3)

72-80: Redundant rememberUpdatedStateObserveEvent already handles it internally.

ObserveEvent wraps its onEach lambda with rememberUpdatedState internally (see Effect.kt). The outer rememberUpdatedState(onIconDownload) on line 72 and the extra currentOnIconDownloaded indirection serve no additional purpose.

♻️ Simplify by removing the redundant wrap
-    val currentOnIconDownloaded by rememberUpdatedState(onIconDownload)
-
     ObserveEvent(viewModel.events) { event ->
         when (event) {
             is StandardIconEvent.IconDownloaded -> {
-                currentOnIconDownloaded(event)
+                onIconDownload(event)
             }
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt`
around lines 72 - 80, Remove the redundant rememberUpdatedState wrapper around
onIconDownload: the ObserveEvent helper already uses rememberUpdatedState
internally, so delete the line declaring currentOnIconDownloaded and call
onIconDownload directly inside the ObserveEvent handler for
StandardIconEvent.IconDownloaded; update the when branch in the
ObserveEvent(viewModel.events) block to invoke onIconDownload(event) and remove
any unused currentOnIconDownloaded symbol.

290-296: Replace unchecked cast as StandardIcon with a safe cast.

IconItem<*> erases the icon's type to Any?, so item.icon as StandardIcon is an unchecked cast that generates a Kotlin compiler warning today and will silently throw ClassCastException if a non-StandardIcon IconItem ever appears in gridItems.

♻️ Proposed fix using safe cast
-                is IconItem<*> -> iconContent(item.icon as StandardIcon)
+                is IconItem<*> -> (item.icon as? StandardIcon)?.let { iconContent(it) }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt`
around lines 290 - 296, The lambda handling grid items uses an unchecked cast
"item.icon as StandardIcon" which can throw; change it to a safe cast and guard
before calling iconContent: cast item.icon to StandardIcon? (using a safe cast)
and only call iconContent when the result is non-null (otherwise handle or
skip), so update the branch for IconItem in the when expression to safely cast
item.icon to StandardIcon and call iconContent with the safely-casted value,
referencing IconItem, StandardIcon, iconContent, CategoryHeader and the
gridItems source.

107-111: Prefer modifier.fillMaxSize() over Modifier.fillMaxSize().then(modifier).

The standard Compose convention is to chain internal modifiers onto the passed-in modifier, so the caller's constraints take precedence. The current order discards any size/layout intentions from the caller.

♻️ Proposed fix
-        Column(
-            modifier = Modifier
-                .fillMaxSize()
-                .then(modifier),
-        ) {
+        Column(modifier = modifier.fillMaxSize()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt`
around lines 107 - 111, The Column currently uses
Modifier.fillMaxSize().then(modifier) which makes the local fillMaxSize override
caller constraints; change the modifier expression to start from the incoming
modifier and apply fillMaxSize onto it (i.e., use modifier.fillMaxSize()) in the
Column call so the caller's modifiers take precedence; update the Column
declaration that references modifier and fillMaxSize accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/IconSizeCustomization.kt`:
- Around line 35-42: The KDoc for IconSizeCustomization incorrectly calls
sizeLabel a "resource key" while the composable uses it as resolved text
(Text(text = sizeLabel) and previews pass a literal string); update the
declaration and KDoc for the IconSizeCustomization composable to state that
sizeLabel is already resolved display text (or alternatively, if you intended a
string resource id, change usages to call stringResource(sizeLabel) and make
sizeLabel an `@StringRes` Int); reference the IconSizeCustomization composable and
the sizeLabel parameter when making the change.
- Around line 52-79: The local slider state var size (created with remember {
mutableFloatStateOf(settings.size.toFloat()) }) can drift when the parent
updates settings.size; add a LaunchedEffect keyed on settings.size that updates
size = settings.size.toFloat() so the Slider and label stay in sync with
external changes while preserving in-flight slider precision; reference the size
var, the remember/mutableFloatStateOf initialization, and
LaunchedEffect(settings.size) when applying the change.

---

Nitpick comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt`:
- Around line 72-80: Remove the redundant rememberUpdatedState wrapper around
onIconDownload: the ObserveEvent helper already uses rememberUpdatedState
internally, so delete the line declaring currentOnIconDownloaded and call
onIconDownload directly inside the ObserveEvent handler for
StandardIconEvent.IconDownloaded; update the when branch in the
ObserveEvent(viewModel.events) block to invoke onIconDownload(event) and remove
any unused currentOnIconDownloaded symbol.
- Around line 290-296: The lambda handling grid items uses an unchecked cast
"item.icon as StandardIcon" which can throw; change it to a safe cast and guard
before calling iconContent: cast item.icon to StandardIcon? (using a safe cast)
and only call iconContent when the result is non-null (otherwise handle or
skip), so update the branch for IconItem in the when expression to safely cast
item.icon to StandardIcon and call iconContent with the safely-casted value,
referencing IconItem, StandardIcon, iconContent, CategoryHeader and the
gridItems source.
- Around line 107-111: The Column currently uses
Modifier.fillMaxSize().then(modifier) which makes the local fillMaxSize override
caller constraints; change the modifier expression to start from the incoming
modifier and apply fillMaxSize onto it (i.e., use modifier.fillMaxSize()) in the
Column call so the caller's modifiers take precedence; update the Column
declaration that references modifier and fillMaxSize accordingly.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bba0bd7 and 6957fe0.

📒 Files selected for processing (5)
  • sdk/compose/icons/api/icons.api
  • sdk/compose/icons/api/icons.klib.api
  • tools/idea-plugin/CHANGELOG.md
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/IconSizeCustomization.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt

@egorikftp egorikftp force-pushed the feature/bootstrap-icons branch from 6957fe0 to 5bafdc0 Compare February 24, 2026 16:39
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt (1)

285-296: ⚠️ Potential issue | 🟡 Minor

Unsafe unchecked cast risks a ClassCastException

item.icon as StandardIcon on IconItem<*> is an unchecked cast — JVM type erasure means the type argument is invisible at runtime. If the grid is ever accidentally fed an IconItem whose payload is not a StandardIcon, this will crash with no compile-time warning.

Prefer the safe-cast + early-return form:

🛡️ Proposed fix
-                is IconItem<*> -> iconContent(item.icon as StandardIcon)
+                is IconItem<*> -> {
+                    val icon = item.icon as? StandardIcon ?: return@items
+                    iconContent(icon)
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt`
around lines 285 - 296, The branch handling IconItem in
StandardImportScreenUI.kt currently does an unsafe cast (item.icon as
StandardIcon) which can throw ClassCastException; replace it with a safe cast
and early-return/fallback: inside the IconItem branch of the grid content
lambda, use a safe cast (item.icon as? StandardIcon) and if null either skip
rendering or render a fallback (e.g., placeholder) instead of casting; update
the call site to pass the safely-cast value to iconContent (refer to IconItem,
StandardIcon, iconContent, and CategoryHeader to locate the code).
♻️ Duplicate comments (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/IconSizeCustomization.kt (1)

52-79: ⚠️ Potential issue | 🟡 Minor

Missing LaunchedEffect to re-sync slider when settings.size changes externally.

The onReset lambda explicitly resets the local size variable (line 58), so the reset path is safe. However, size is initialised once via remember and will silently drift if the parent changes settings.size through any path other than onReset (e.g., ViewModel state restoration, external preset selection). Adding a LaunchedEffect keyed on settings.size guards against this without affecting in-flight slider precision.

🔧 Proposed fix
+import androidx.compose.runtime.LaunchedEffect
 ...
     var size by remember { mutableFloatStateOf(settings.size.toFloat()) }

+    LaunchedEffect(settings.size) {
+        if (settings.size != size.roundToInt()) {
+            size = settings.size.toFloat()
+        }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/IconSizeCustomization.kt`
around lines 52 - 79, The local mutable "size" (created with remember {
mutableFloatStateOf(settings.size.toFloat()) }) can drift if settings.size
changes externally; add a LaunchedEffect keyed on settings.size that updates the
local size (e.g., set size = settings.size.toFloat()) so the Slider and UI
re-sync when the parent/VM updates settings, while leaving onReset and
onSettingsChange behavior intact; place this LaunchedEffect near the size
declaration in IconSizeCustomization.kt to ensure correct scoping.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt`:
- Around line 285-296: The branch handling IconItem in StandardImportScreenUI.kt
currently does an unsafe cast (item.icon as StandardIcon) which can throw
ClassCastException; replace it with a safe cast and early-return/fallback:
inside the IconItem branch of the grid content lambda, use a safe cast
(item.icon as? StandardIcon) and if null either skip rendering or render a
fallback (e.g., placeholder) instead of casting; update the call site to pass
the safely-cast value to iconContent (refer to IconItem, StandardIcon,
iconContent, and CategoryHeader to locate the code).

---

Duplicate comments:
In
`@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/IconSizeCustomization.kt`:
- Around line 52-79: The local mutable "size" (created with remember {
mutableFloatStateOf(settings.size.toFloat()) }) can drift if settings.size
changes externally; add a LaunchedEffect keyed on settings.size that updates the
local size (e.g., set size = settings.size.toFloat()) so the Slider and UI
re-sync when the parent/VM updates settings, while leaving onReset and
onSettingsChange behavior intact; place this LaunchedEffect near the size
declaration in IconSizeCustomization.kt to ensure correct scoping.

ℹ️ Review info

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6957fe0 and 5bafdc0.

📒 Files selected for processing (5)
  • sdk/compose/icons/api/icons.api
  • sdk/compose/icons/api/icons.klib.api
  • tools/idea-plugin/CHANGELOG.md
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/IconSizeCustomization.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/StandardImportScreenUI.kt
🚧 Files skipped from review as they are similar to previous changes (2)
  • tools/idea-plugin/CHANGELOG.md
  • sdk/compose/icons/api/icons.klib.api

@egorikftp egorikftp merged commit d90b5b9 into main Feb 24, 2026
3 checks passed
@egorikftp egorikftp deleted the feature/bootstrap-icons branch February 24, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants