Skip to content

Conversation

@t-regbs
Copy link
Collaborator

@t-regbs t-regbs commented Dec 15, 2025

What did I do?

PR addresses #766 , adding support for importing icons from Lucide. Matches existing material symbols functionality so users can now browse, customize, and import Lucide icons directly within the IDE plugin.

Screen.Recording.2025-12-15.at.16.34.46.mov

How did I do it?

image

Key Components

  1. LucideConfig
    Data model with built-in indexing for performance

  2. LucideRepository
    Handles data fetching and SVG customization:
    HTTP Layer:

  • Fetches from UNPKG CDN unpkg.com/lucide-static@latest
  • Loads icon metadata from tags.json
  • Downloads individual SVG files on-demand
    Caching:
  • LRU cache 300 entries for raw SVG strings
  • Mutex-protected for thread safety
  1. LucideUseCase
    Contains business logic for icon categorization and customization:
    Category Inference:
    Priority-based keyword matching name keywords > tag keywords
    58 predefined keywords across 15+ categories
    Name matches get priority boost -2 over tag matches
    Falls back to "General" category if no matches

  2. LucideViewModel
    Manages state, caching, and concurrency for the import flow:

  3. SvgManipulator
    A robust utility for DOM-based SVG manipulation that handles attribute modifications reliably.

@coderabbitai
Copy link

coderabbitai bot commented Dec 15, 2025

Walkthrough

Adds Lucide web-icon import and SVG manipulation features across libraries and the IDE plugin. Introduces SvgManipulator (DOM-based SVG utilities) and its tests, extends SVG API, adds a colored LucideLogo ImageVector, and updates build/dependencies. The IDE plugin gains a Lucide import flow: LucideImportScreen, LucideViewModel, LucideRepository, LucideUseCase, domain models (Category, LucideIcon, LucideConfig, LucideSettings), DI modules (NetworkModule, LucideModule), UI components (customization panel, icon display, top actions), a NoStopIndicatorSlider, and new resource strings.

Possibly related issues

  • ComposeGears/Valkyrie issue 189 — The changes implement a Lucide web-icon import flow (repository, use case, ViewModel, UI, and SVG parsing/customization), which matches the objectives described in the issue.
🚥 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 clearly and concisely summarizes the main feature being added: Lucide Icons Web Import support for the Plugin component.
Description check ✅ Passed The description comprehensively explains what was done, how it was implemented, and the key components involved, directly relating to the changeset.

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

✨ Finishing touches
  • 📝 Generate docstrings

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: 3

🧹 Nitpick comments (7)
components/parser/jvm/svg/src/test/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulatorTest.kt (2)

13-78: Good coverage of root and recursive updates; only minor brittleness risk

These tests do a nice job validating modifySvg on the root element and updateAttributeRecursively across nested structures, including counting occurrences to assert full coverage. The only minor concern is reliance on exact serialized substrings (e.g., stroke-width="3" and full <path .../>), which could become brittle if the XML serializer ever changes quoting or spacing behavior.

You might consider, in the future, parsing modifiedSvg back into a DOM for these specific checks (e.g., count elements with a given attribute/value) instead of relying on raw string splits, to make the tests more resilient to formatting changes.


133-212: Nice edge‑case coverage for quoting, spacing, parse failures, and empty values

The tests for mixed quote styles, extra spaces around =, parse failures returning the original string, structure preservation, and empty attribute values together give good assurance around real‑world SVG quirks. Only small enhancement you might consider is an extra test where updateAttributeRecursively / updateAttributeConditionally target an attribute that doesn’t exist anywhere, to assert clean no‑op behavior.

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

3-21: Confirm uniqueness of LucideIcon.name across categories

iconsByName is built via gridItems.values.flatten().associateBy { it.name }, so if an icon name appears in multiple category lists, the last one wins and earlier entries are silently dropped. That’s fine if name is guaranteed unique globally; otherwise it can hide duplicates in a non-obvious way.

Consider either:

  • documenting the uniqueness guarantee on LucideIcon.name, or
  • guarding against duplicates (e.g., logging, or choosing a deterministic winner).
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt (1)

68-97: Revisit stroke-width customization guard and fix locale-dependent hex formatting

Two smaller points in applySvgCustomizations / toHexString:

  1. Stroke-width guard and absolute stroke width

    The update is gated only on strokeWidth differing from the default:

    if (settings.strokeWidth != DEFAULT_STROKE_WIDTH.toFloat()) {
        SvgManipulator.updateAttributeRecursively(
            attributeName = ATTR_STROKE_WIDTH,
            newValue = settings.adjustedStrokeWidth().toString(),
        )
    }

    If absoluteStrokeWidth is meant to “maintain stroke width when scaling” (per the UI text) then toggling that flag or changing size while leaving strokeWidth at the default will not adjust stroke-width at all. You may want the condition to also consider absoluteStrokeWidth/size, e.g. “apply whenever settings differ from the default configuration” rather than only when the slider value changes.

  2. Locale-stable hex formatting

    Detekt correctly flags:

    return String.format("#%06X", 0xFFFFFF and argb)

    as using the implicit default locale. To avoid locale-dependent behavior, specify an explicit locale:

+import java.util.Locale
...

  •    return String.format("#%06X", 0xFFFFFF and argb)
    
  •    return String.format(Locale.ROOT, "#%06X", 0xFFFFFF and argb)
    
    
    
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt (1)

61-73: Use locale-stable formatting for the stroke width label

The label uses:

text = "Stroke Width: ${String.format("%.1f", settings.strokeWidth)}",

String.format without an explicit Locale can behave differently under non-English locales. For predictable UI text, prefer an explicit locale, e.g.:

+import java.util.Locale
...
-            Text(
-                text = "Stroke Width: ${String.format(\"%.1f\", settings.strokeWidth)}",
+            Text(
+                text = "Stroke Width: ${String.format(Locale.ROOT, \"%.1f\", settings.strokeWidth)}",
                 style = MaterialTheme.typography.labelMedium,
             )

This keeps the numeric formatting stable regardless of the user’s system locale.

components/parser/jvm/svg/src/main/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulator.kt (1)

46-49: Replace println with proper logging.

Using println for error reporting is not appropriate for a library component, especially within an IntelliJ plugin context. Consider using a logging framework consistent with the rest of the codebase (e.g., com.intellij.openapi.diagnostic.Logger as used in LucideViewModel).

+import com.intellij.openapi.diagnostic.Logger
+
 object SvgManipulator {
+    private val LOG = Logger.getInstance(SvgManipulator::class.java)
 
     // ... in modifySvg:
         } catch (e: Exception) {
-            println("Failed to parse SVG for modification: ${e.message}")
+            LOG.warn("Failed to parse SVG for modification", e)
             svgContent
         }
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideImportScreen.kt (1)

191-203: Unchecked cast and fallback logic concern.

The unchecked cast item.icon as LucideIcon at line 192 will throw ClassCastException if IconItem ever contains a non-LucideIcon type. Consider using a type-safe approach.

The fallback logic (lines 196-203) that searches for any matching icon name prefix seems like a workaround for settings changes invalidating cache keys. This works but couples the UI to cache key implementation details.

Consider using a safer cast pattern:

                             is IconItem<*> -> {
-                                val lucideIcon = item.icon as LucideIcon
+                                val lucideIcon = item.icon as? LucideIcon ?: return@items
                                 val iconCacheKey = getIconCacheKey(lucideIcon.name, state.settings)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb99248 and 7f18fdb.

📒 Files selected for processing (23)
  • components/parser/jvm/svg/build.gradle.kts (1 hunks)
  • components/parser/jvm/svg/src/main/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulator.kt (1 hunks)
  • components/parser/jvm/svg/src/test/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulatorTest.kt (1 hunks)
  • compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/icons/colored/LucideLogo.kt (1 hunks)
  • tools/idea-plugin/build.gradle.kts (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportFlow.kt (2 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt (6 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideImportScreen.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideMetadata.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/di/LucideModule.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/Category.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideConfig.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideGridItem.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideIcon.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideSettings.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideIconDisplay.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideTopActions.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/util/LruCache.kt (1 hunks)
  • tools/idea-plugin/src/main/resources/messages/Valkyrie.properties (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-07T20:07:49.753Z
Learnt from: egorikftp
Repo: ComposeGears/Valkyrie PR: 750
File: tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt:71-85
Timestamp: 2025-12-07T20:07:49.753Z
Learning: In the Valkyrie Gradle plugin (Kotlin), the `useFlatPackage` flag in `IconPackExtension` is only applicable when nested packs are configured. For single icon packs (without nested packs), the flag is intentionally not propagated to `ImageVectorGeneratorConfig` as there is no package hierarchy to flatten.

Applied to files:

  • compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/icons/colored/LucideLogo.kt
📚 Learning: 2025-10-21T20:55:27.073Z
Learnt from: egorikftp
Repo: ComposeGears/Valkyrie PR: 651
File: tools/idea-plugin/build.gradle.kts:147-175
Timestamp: 2025-10-21T20:55:27.073Z
Learning: In Gradle Kotlin DSL (.gradle.kts) scripts, the types `org.gradle.api.artifacts.ArtifactCollection` and `org.gradle.api.artifacts.component.ModuleComponentIdentifier` are implicitly available and do not require explicit import statements.

Applied to files:

  • components/parser/jvm/svg/build.gradle.kts
🧬 Code graph analysis (6)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt (2)
compose/ui/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/ui/InfoCard.kt (1)
  • InfoCard (24-78)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/util/ValkyrieStrings.kt (1)
  • stringResource (15-21)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideIconDisplay.kt (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/WebImportScreenComponents.kt (1)
  • IconLoadingPlaceholder (219-230)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt (3)
compose/core/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/core/layout/Row.kt (1)
  • CenterVerticalRow (10-20)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/IconButton.kt (1)
  • IconButton (15-36)
compose/core/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/core/layout/Spacer.kt (2)
  • HorizontalSpacer (23-29)
  • VerticalSpacer (15-21)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideGridItem.kt (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/WebImportScreenComponents.kt (1)
  • CategoryHeader (106-118)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideTopActions.kt (2)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/WebImportTopActions.kt (1)
  • WebImportTopActions (46-131)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/CategoriesDropdown.kt (1)
  • CategoriesDropdown (12-29)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt (1)
  • loadConfig (69-92)
🪛 detekt (1.23.8)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt

[warning] 96-96: String.format("#%06X", 0xFFFFFF and argb) uses implicitly default locale for string formatting.

(detekt.potential-bugs.ImplicitDefaultLocale)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (ubuntu-latest)
🔇 Additional comments (36)
components/parser/jvm/svg/build.gradle.kts (1)

10-11: ✓ Appropriate test dependencies for SVG manipulation tests.

The addition of kotlin.test and assertk with testImplementation scope is correct and aligns with best practices for Kotlin testing. These will support the new SvgManipulatorTest test suite.

tools/idea-plugin/build.gradle.kts (1)

25-25: ✓ SVG parser module dependency correctly scoped and positioned.

The addition of the SVG parser module is necessary for the Lucide icon feature's SVG manipulation capabilities. The placement among other component dependencies is consistent with the module's organizational structure.

components/parser/jvm/svg/src/test/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulatorTest.kt (3)

3-11: Test setup and assertion choices look solid

Using assertk alongside kotlin.test is consistent and keeps assertions readable; imports and test class scaffolding are clean with no obvious redundancy or misuse.


79-107: Conditional attribute update test is precise and effective

The conditional stroke update test clearly validates that only matching currentValue entries are updated while others remain untouched, and the explicit counts for green vs red strokes give strong confidence in behavior across root and children.


214-249: End‑to‑end Lucide‑style scenario nicely validates combined behavior

The “complex real‑world Lucide icon manipulation” test is a strong integration-style check: it exercises root attribute changes, recursive stroke‑width updates, and conditional stroke color replacement together and asserts both positive and negative conditions. This should catch most regressions in the manipulator relevant to the Lucide import flow.

tools/idea-plugin/src/main/resources/messages/Valkyrie.properties (1)

46-47: LGTM!

The resource keys follow the existing naming convention and the description is clear and informative.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideSettings.kt (3)

11-14: LGTM!

The validation ranges are appropriate for SVG stroke width and icon size, with clear error messages.


16-17: LGTM!

The isModified property correctly detects when any setting differs from its default value.


19-25: LGTM!

The adjustedStrokeWidth calculation correctly scales the stroke width to maintain visual consistency across different icon sizes when absoluteStrokeWidth is enabled.

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

9-9: LGTM!

The Lucide import screen is correctly integrated into the navigation flow, following the same pattern as the existing Material Symbols import.

Also applies to: 21-21

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

3-9: LGTM!

The Category data class is straightforward and the All constant provides a useful default for filtering.

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

10-13: LGTM!

The extension function efficiently transforms categorized icons into a flat grid structure suitable for UI rendering.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt (4)

22-22: LGTM!

The imports are correctly added to support the new Lucide icon provider option.

Also applies to: 29-30


40-43: LGTM!

The Lucide navigation mapping follows the same pattern as the existing Google Material Symbols option.


67-67: LGTM!

The Lucide info card is consistently implemented following the same pattern as the existing Google Material card, and the vertical spacing improves the layout.

Also applies to: 76-82


91-94: LGTM!

The Lucide enum member is correctly added to represent the new icon provider option.

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

3-8: LGTM!

The LucideIcon data class provides a clear and appropriate structure for representing Lucide icons with their metadata.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/di/LucideModule.kt (3)

15-20: LGTM!

The JSON configuration is appropriate for parsing external API responses, providing resilience against schema changes.


22-31: LGTM!

The HTTP client is properly configured with a reasonable timeout and content negotiation for JSON responses.


33-44: LGTM!

The dependency injection wiring is clean and follows good separation of concerns, exposing only the use case publicly while keeping infrastructure details private.

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

9-30: LucideTopActions wrapper looks clean and idiomatic

Thinly composing WebImportTopActions with CategoriesDropdown keeps Lucide-specific wiring isolated and reuses the common UI nicely. No issues from a correctness or UX standpoint.

compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/icons/colored/LucideLogo.kt (1)

12-56: LucideLogo vector and lazy caching look good

The lazy _LucideLogo backing field and builder setup are consistent with typical icon definitions; paths and sizes look coherent. No changes needed.

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

6-16: LucideIconMetadata structure aligns with repository usage

The metadata model is minimal and focused on what the repository actually needs (tags/categories, optional schema/contributors). Looks good as a serialization DTO.

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

9-40: LRU cache and thread-safe wrapper are straightforward and appropriate

The LRU implementation using a LinkedHashMap with access order and simple eviction, plus the synchronized wrapper for multi-threaded use, is clear and sufficient for the icon/SVG caching use cases in this PR. No changes needed.

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

41-52: The current loadIconList is safe with the actual Lucide tags.json structure

The review assumes tags.json might contain non-array fields like $schema, but the actual tags.json published in lucide-static is a simple map of icon names to tag string arrays:

{
  "tag": ["label", "badge", "ticket"],
  "mail": ["email", "message", "inbox"]
}

Metadata like $schema and contributors are in separate per-icon JSON files, not in tags.json. The current code will not crash on the actual endpoint.

However, if future-proofing against API changes is desired, adding a type guard is reasonable:

-        tagsJson.entries.map { (iconName, tagsArray) ->
-            val tags = tagsArray.jsonArray.map { it.jsonPrimitive.content }
+        tagsJson.entries.mapNotNull { (iconName, element) ->
+            if (element !is JsonArray) return@mapNotNull null
+            val tags = element.map { it.jsonPrimitive.content }
             iconName to LucideIconMetadata(
                 tags = tags,
                 categories = emptyList(),
             )
         }

This is optional defensive programming rather than a necessary fix for current data.

Likely an incorrect or invalid review comment.

components/parser/jvm/svg/src/main/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulator.kt (1)

59-75: LGTM: Recursive attribute update functions.

The recursive traversal logic is correct and handles type checking properly. For typical SVG icons, the nesting depth is shallow enough that stack overflow is not a practical concern.

Also applies to: 85-105

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

51-85: LGTM: Screen setup and event handling.

The navigation setup, ViewModel integration, and event flow are well-structured. The use of LaunchedEffect to collect events and navigate appropriately follows idiomatic Compose patterns.


103-128: LGTM: AnimatedContent state handling.

Using contentKey to map states to stable keys for animation transitions is a good pattern that prevents unnecessary re-compositions.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt (3)

26-58: LGTM: Category keyword mapping with priorities.

The priority-based categorization system is well-designed. The documentation clearly explains how priorities work and provides helpful examples. The keyword list covers major icon categories comprehensively.


103-120: LGTM: Category inference algorithm.

The priority-based matching with name boost is a sensible approach. The -2 boost for name matches ensures that icon names take precedence over tags when determining categories.

Note: When the same keyword matches both name and tags, it will appear twice in allMatches with different priorities. This is harmless since minByOrNull correctly selects the name match (with the boosted/lower priority).


69-92: LGTM: loadConfig implementation.

The config loading logic is clean: loads metadata, maps to domain models, extracts and sorts categories, groups icons, and creates the config. The use of Category.All as a prefix to the sorted categories list is intuitive for UI display.

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

182-205: Stale state reference after mutex release.

The state captured at line 183 becomes stale after the mutex is released at line 205. The network call and parsing at lines 207-214 use state.settings from this stale reference, which could differ from the current settings if updateSettings was called concurrently.

For display purposes this may be acceptable (the icon will be re-fetched on next settings change), but it's worth noting for correctness.

Verify whether settings changes during an in-flight load should abort or continue with stale settings. If abort is preferred, check for cancellation or settings match before parseAndCacheIcon.


83-98: LGTM: downloadIcon implementation.

The job cancellation pattern ensures only one download is active at a time, which is appropriate for the download-and-navigate flow.


128-149: LGTM: Settings update with job cancellation.

The pattern of canceling all in-flight icon load jobs and re-parsing with new settings is correct. The use of iconsByName for efficient lookup is good.


266-290: LGTM: Grid item filtering logic.

The filtering implementation correctly handles category filtering and search queries across name, displayName, and tags. The early return for blank search queries is a good optimization.


300-327: LGTM: State and event sealed interfaces.

The sealed interface hierarchy is well-defined. Using @Stable annotations for Compose is appropriate for these state classes.

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 (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideIconMetadata.kt (1)

10-15: Consider removing redundant @SerialName annotations.

The @SerialName annotations for contributors, tags, and categories are redundant since the property names already match the JSON keys. Only the schema field requires it due to the special $ character.

Apply this diff to simplify:

-    @SerialName("contributors")
     val contributors: List<String> = emptyList(),
-    @SerialName("tags")
     val tags: List<String>,
-    @SerialName("categories")
     val categories: List<String>,
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7f18fdb and 512b613.

📒 Files selected for processing (5)
  • components/parser/jvm/svg/api/svg.api (1 hunks)
  • components/parser/jvm/svg/src/main/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulator.kt (1 hunks)
  • compose/icons/api/icons.api (1 hunks)
  • compose/icons/api/icons.klib.api (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideIconMetadata.kt (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (ubuntu-latest)
🔇 Additional comments (3)
compose/icons/api/icons.api (1)

30-32: LucideLogo API entry is consistent and additive-only

The new LucideLogoKt.getLucideLogo(ValkyrieIcons$Colored): ImageVector entry cleanly mirrors getGoogleMaterialLogo, keeps ordering with other colored logo accessors, and is purely additive, so it should be binary/source compatible. No issues from the API-surface perspective.

compose/icons/api/icons.klib.api (1)

21-22: LGTM! LucideLogo API addition follows existing pattern.

The new LucideLogo symbol and its getter are correctly structured, consistently following the pattern of GoogleMaterialLogo and ValkyrieLogo. This auto-generated ABI dump properly reflects the new colored icon addition for the Lucide integration feature.

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

6-16: No action required - the implementation is correct.

The data class safely requires tags and categories because the repository code at LucideRepository.kt always supplies these values when instantiating LucideIconMetadata: tags is extracted from the JSON array and categories is explicitly set to emptyList(). Both fields are guaranteed to have values before the object is created.

Likely an incorrect or invalid review comment.

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 (1)
compose/icons/api/icons.klib.api (1)

19-20: Inconsistent API pattern for colored icon.

FlagMx is missing the @io.github.composegears.valkyrie.compose.icons.ValkyrieIcons.Colored receiver annotation that other colored icons use (see GoogleMaterialLogo at line 21, LucideLogo at line 23, and ValkyrieLogo at line 25). The declaration shows {}FlagMx[0] but should show @io.github.composegears.valkyrie.compose.icons.ValkyrieIcons.Colored{}FlagMx[0] to match the pattern. This is the same inconsistency observed in the JVM API dump.

🧹 Nitpick comments (3)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt (1)

94-97: Prefer explicit locale or Kotlin's format for hex strings.

The static analysis tool flags String.format without explicit locale. While locale doesn't affect hex formatting, using Kotlin's native formatting is more idiomatic and avoids the warning.

 private fun Color.toHexString(): String {
     val argb = this.toArgb()
-    return String.format("#%06X", 0xFFFFFF and argb)
+    return "#%06X".format(0xFFFFFF and argb)
 }
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt (2)

138-140: Brittle cache key parsing relies on internal format.

Extracting icon names using substringBefore("-") tightly couples this code to the cache key format defined in buildIconCacheKey(). If the format changes (e.g., icon names containing hyphens), this logic breaks silently.

Consider extracting icon names from the config.iconsByName keys or maintaining a separate mapping:

 val currentState = lucideRecord.value.safeAs<LucideState.Success>() ?: return@launch
 
-val loadedIconNames = currentState.loadedIcons.keys
-    .map { cacheKey -> cacheKey.substringBefore("-") }
-    .toSet()
+// Get icons that have any loaded state (regardless of settings)
+val loadedIconNames = currentState.loadedIcons.keys
+    .mapNotNull { cacheKey ->
+        // Match cache key format: "iconName-strokeWidth-size-absoluteStroke-color"
+        cacheKey.split("-").firstOrNull()?.takeIf { it in currentState.config.iconsByName }
+    }
+    .toSet()

Or better yet, track loaded icon names separately to avoid parsing altogether.


184-222: Job tracking by icon name may cause cancellations across different settings.

The iconLoadJobs map is keyed by icon.name (line 220), but the cache and state use cacheKey which includes settings. If the same icon is requested with different settings before the first request completes, the second request will overwrite and cancel the first job.

While updateSettings() (line 133) cancels all jobs anyway, this design could cause unexpected behavior if jobs are launched with varying settings outside of settings updates.

Consider whether jobs should be keyed by cacheKey instead of icon.name for consistency:

-private val iconLoadJobs = mutableMapOf<String, Job>()
+private val iconLoadJobs = mutableMapOf<String, Job>()  // Key: cacheKey

 fun loadIconForDisplay(icon: LucideIcon) {
     val job = viewModelScope.launch {
         val state = lucideRecord.value.safeAs<LucideState.Success>() ?: return@launch
         val cacheKey = buildIconCacheKey(icon.name, state.settings)
         
         // ... existing logic ...
     }
     
-    iconLoadJobs[icon.name] = job
-    job.invokeOnCompletion { iconLoadJobs.remove(icon.name) }
+    iconLoadJobs[cacheKey] = job
+    job.invokeOnCompletion { iconLoadJobs.remove(cacheKey) }
 }

This would require similar updates to reParseIconFromRepository() (line 174-175).

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02c13a3 and 966ccee.

📒 Files selected for processing (6)
  • compose/icons/api/icons.api (1 hunks)
  • compose/icons/api/icons.klib.api (1 hunks)
  • gradle/libs.versions.toml (1 hunks)
  • tools/idea-plugin/build.gradle.kts (2 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt (1 hunks)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tools/idea-plugin/build.gradle.kts
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-07T20:07:49.753Z
Learnt from: egorikftp
Repo: ComposeGears/Valkyrie PR: 750
File: tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt:71-85
Timestamp: 2025-12-07T20:07:49.753Z
Learning: In the Valkyrie Gradle plugin (Kotlin), the `useFlatPackage` flag in `IconPackExtension` is only applicable when nested packs are configured. For single icon packs (without nested packs), the flag is intentionally not propagated to `ImageVectorGeneratorConfig` as there is no package hierarchy to flatten.

Applied to files:

  • compose/icons/api/icons.klib.api
  • compose/icons/api/icons.api
📚 Learning: 2025-10-21T20:55:27.073Z
Learnt from: egorikftp
Repo: ComposeGears/Valkyrie PR: 651
File: tools/idea-plugin/build.gradle.kts:147-175
Timestamp: 2025-10-21T20:55:27.073Z
Learning: In Gradle Kotlin DSL (.gradle.kts) scripts, the types `org.gradle.api.artifacts.ArtifactCollection` and `org.gradle.api.artifacts.component.ModuleComponentIdentifier` are implicitly available and do not require explicit import statements.

Applied to files:

  • gradle/libs.versions.toml
🧬 Code graph analysis (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt (1)
  • loadConfig (69-92)
🪛 detekt (1.23.8)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt

[warning] 96-96: String.format("#%06X", 0xFFFFFF and argb) uses implicitly default locale for string formatting.

(detekt.potential-bugs.ImplicitDefaultLocale)

🔇 Additional comments (4)
gradle/libs.versions.toml (2)

14-14: Library entry looks good.

The androidx-collection library entry is correctly formatted and appropriate for the LRU cache use case described in the PR. The syntax follows Gradle version catalog conventions, and the placement is correct.


10-10: Version 1.5.0 is current and secure.

No known vulnerabilities found for androidx.collection:collection 1.5.0. The version is the latest stable release and compatible with JDK 17. Appropriate for the LRU cache use case.

compose/icons/api/icons.api (1)

34-36: LGTM!

The LucideLogo API correctly follows the established pattern for colored icons with the ValkyrieIcons$Colored receiver parameter.

compose/icons/api/icons.klib.api (1)

23-24: LGTM!

The LucideLogo API correctly follows the established pattern for colored icons with the @io.github.composegears.valkyrie.compose.icons.ValkyrieIcons.Colored receiver annotation.

@egorikftp
Copy link
Member

egorikftp commented Dec 18, 2025

@t-regbs
I will check the PR this week; I see some improvements in DI. We can share the same httpClient across all web import features.

Also, as an idea, is it possible to extract the feature into a separate Gradle module? (can be discussed on chat, non critical for this PR). I'm not sure about the UI part, but all the network and conversion logic can potentially be isolated.
In scope of Jewel migration we should stop using material3 at all 🙁 Will be own implementation for ui

@egorikftp egorikftp linked an issue Dec 20, 2025 that may be closed by this pull request
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: 0

🧹 Nitpick comments (2)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/di/MaterialSymbolsModule.kt (1)

4-5: Inconsistent import usage.

Line 5 has a direct static import of NetworkModule.httpClient, but line 11 creates a local reference network = NetworkModule which is used for network.httpClient on line 15. However, line 20 still uses the bare httpClient from the static import. While functionally equivalent, this is inconsistent.

Consider removing the static import and using network.httpClient consistently:

🔎 Proposed fix
 import com.composegears.leviathan.Leviathan
 import io.github.composegears.valkyrie.ui.screen.webimport.common.di.NetworkModule
-import io.github.composegears.valkyrie.ui.screen.webimport.common.di.NetworkModule.httpClient
 import io.github.composegears.valkyrie.ui.screen.webimport.material.data.config.MaterialSymbolsConfigRepository

And on line 20:

     private val materialFontRepository by instanceOf {
-        MaterialFontRepository(httpClient = inject(httpClient))
+        MaterialFontRepository(httpClient = inject(network.httpClient))
     }
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/di/NetworkModule.kt (1)

21-30: Consider adding keepAlive = true and connect timeout for the HttpClient.

The json binding uses keepAlive = true, but httpClient does not. Without keepAlive, the HttpClient instance may be recreated when dependencies are re-injected. Since HttpClient holds connection pools and resources that should be explicitly closed, this could lead to resource leaks.

Additionally, only requestTimeoutMillis is configured. Consider adding connectTimeoutMillis to prevent indefinite hangs when the remote server is unresponsive during connection establishment.

🔎 Proposed fix
-    val httpClient by instanceOf {
+    val httpClient by instanceOf(keepAlive = true) {
         HttpClient(OkHttp) {
             install(HttpTimeout) {
                 requestTimeoutMillis = 30.seconds.inWholeMilliseconds
+                connectTimeoutMillis = 10.seconds.inWholeMilliseconds
             }
             install(ContentNegotiation) {
                 json(inject(json))
             }
         }
     }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 107220f and 0fcb595.

📒 Files selected for processing (3)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/di/NetworkModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/di/LucideModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/di/MaterialSymbolsModule.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (ubuntu-latest)
🔇 Additional comments (2)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/di/LucideModule.kt (1)

8-23: LGTM!

Clean module structure that follows the same DI patterns as MaterialSymbolsModule. Properly wires the Lucide dependencies using the shared NetworkModule for httpClient and json.

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

14-19: LGTM on Json configuration.

Using keepAlive = true for the shared Json instance is appropriate, and the configuration (ignoreUnknownKeys, isLenient) is sensible for consuming external APIs where schema changes may occur.

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

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/material/di/MaterialSymbolsModule.kt (1)

13-21: Use consistent pattern for referencing NetworkModule dependencies.

The code mixes two different patterns for accessing httpClient:

  • Lines 15-16: inject(network.httpClient) and inject(network.json) (via the network reference)
  • Line 20: inject(httpClient) (via direct import at line 5)

This inconsistency makes the code harder to read and maintain. Choose one pattern and apply it uniformly.

🔎 Recommended fix (use network reference consistently)

Remove the direct import at line 5:

 import com.composegears.leviathan.Leviathan
 import io.github.composegears.valkyrie.ui.screen.webimport.common.di.NetworkModule
-import io.github.composegears.valkyrie.ui.screen.webimport.common.di.NetworkModule.httpClient
 import io.github.composegears.valkyrie.ui.screen.webimport.material.data.config.MaterialSymbolsConfigRepository

Update line 20 to use the network reference:

     private val materialFontRepository by instanceOf {
-        MaterialFontRepository(httpClient = inject(httpClient))
+        MaterialFontRepository(httpClient = inject(network.httpClient))
     }
🧹 Nitpick comments (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/di/NetworkModule.kt (1)

23-25: Consider adding connect and socket timeout configurations.

The current configuration only sets requestTimeoutMillis (30 seconds), but doesn't explicitly configure connect or socket timeouts. For better control over network operations and to avoid hanging connections, consider adding:

🔎 Proposed enhancement
             install(HttpTimeout) {
                 requestTimeoutMillis = 30.seconds.inWholeMilliseconds
+                connectTimeoutMillis = 10.seconds.inWholeMilliseconds
+                socketTimeoutMillis = 30.seconds.inWholeMilliseconds
             }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0fcb595 and aa5fe89.

📒 Files selected for processing (3)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/di/NetworkModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/di/LucideModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/di/MaterialSymbolsModule.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/lucide/di/LucideModule.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (ubuntu-latest)

@egorikftp
Copy link
Member

@t-regbs I found some issue

Screen.Recording.2025-12-23.at.14.43.20.mov

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: 0

🧹 Nitpick comments (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt (1)

63-63: Use explicit locale for number formatting.

The String.format call uses the implicit default locale, which can produce inconsistent decimal separators across different locales (e.g., "1.5" vs "1,5"). For a numeric display in a code/technical context, consider using an explicit locale.

As per static analysis hint.

🔎 Proposed fix using explicit locale
-                text = "Stroke width: ${String.format("%.1f", settings.strokeWidth)}",
+                text = "Stroke width: ${"%.1f".format(java.util.Locale.US, settings.strokeWidth)}",

Alternatively, for a more locale-aware user experience, you could use Locale.getDefault() explicitly to show intent, or leverage platform-specific number formatters if consistency with system locale is desired.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aa5fe89 and fd0d3f1.

📒 Files selected for processing (2)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/Slider.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt
🧰 Additional context used
🧬 Code graph analysis (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt (4)
compose/core/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/core/layout/Row.kt (1)
  • CenterVerticalRow (10-20)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/IconButton.kt (1)
  • IconButton (15-36)
compose/core/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/core/layout/Spacer.kt (2)
  • HorizontalSpacer (23-29)
  • VerticalSpacer (15-21)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/Slider.kt (1)
  • NoStopIndicatorSlider (12-44)
🪛 detekt (1.23.8)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt

[warning] 63-63: String.format("%.1f", settings.strokeWidth) uses implicitly default locale for string formatting.

(detekt.potential-bugs.ImplicitDefaultLocale)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (ubuntu-latest)
🔇 Additional comments (4)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/Slider.kt (1)

12-44: Implementation looks good.

The custom slider correctly disables stop indicators and tick marks through the custom track implementation. The color customization and parameter delegation are clean.

Note: This uses ExperimentalMaterial3Api, which may change in future Material3 releases. Additionally, per the PR discussion, the team plans to migrate from Material3 to Jewel, which will require reimplementing this component.

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

26-53: LGTM! Clean component structure and reset logic.

The function signature follows Compose conventions, and the top bar implementation is well-structured. The reset mechanism correctly restores default settings and is appropriately enabled only when modifications exist.


78-90: LGTM! Size control implementation is correct.

The pixel size slider range and step calculation are appropriate, and the float-to-int conversion using roundToInt() properly handles the discrete pixel values.


94-113: LGTM! Well-structured switch control with clear labeling.

The absolute stroke width toggle is properly implemented with descriptive text that helps users understand the feature's purpose.

@t-regbs
Copy link
Collaborator Author

t-regbs commented Dec 24, 2025

@egorikftp ah nice catch, I will look at only reparsing the icons when the user is done dragging the sliders.
Also will reduce the upper bound for the size slider as it looks like 96px is greater than the bounds of the icon card

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: 0

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

14-22: Consider adding an enabled parameter.

The standard Material3 Slider supports an enabled parameter for disabling user interaction. Currently, the Track is hardcoded to enabled = true (line 39), preventing this slider from being disabled. Adding an enabled parameter would improve flexibility.

🔎 Proposed refactor to add enabled parameter
 @Composable
 fun NoStopIndicatorSlider(
     value: Float,
     onValueChange: (Float) -> Unit,
     valueRange: ClosedFloatingPointRange<Float>,
     steps: Int,
     modifier: Modifier = Modifier,
+    enabled: Boolean = true,
     onValueChangeFinished: (() -> Unit)? = null,
     interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
 ) {
     val colors = SliderDefaults.colors().copy(
         inactiveTrackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
     )

     Slider(
         modifier = modifier,
         value = value,
         onValueChange = onValueChange,
         onValueChangeFinished = onValueChangeFinished,
         interactionSource = interactionSource,
+        enabled = enabled,
         colors = colors,
         valueRange = valueRange,
         steps = steps,
         track = { sliderState ->
             SliderDefaults.Track(
                 colors = colors,
-                enabled = true,
+                enabled = enabled,
                 sliderState = sliderState,
                 drawStopIndicator = null,
                 drawTick = { _, _ -> },
             )
         },
     )
 }

Also applies to: 39-39


13-22: Consider adding KDoc documentation.

Adding documentation would clarify the purpose of this custom slider (removing stop indicators and tick marks) and describe its parameters, improving maintainability.


23-25: Consider making the inactive track alpha configurable.

The inactive track alpha is hardcoded to 0.3f. While this creates consistent styling, exposing it as an optional parameter would provide additional flexibility for different visual requirements.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideSettings.kt (2)

21-25: Consider adding KDoc to clarify purpose.

The logic correctly identifies when settings differ from defaults. Adding a brief KDoc comment would help clarify its use case (e.g., determining when to show a "reset" button in the UI).

📝 Suggested documentation
+    /**
+     * Returns true if any setting has been modified from its default value.
+     * Useful for UI state management (e.g., showing reset/restore options).
+     */
     val isModified: Boolean
         get() = color != Color.Unspecified ||
             strokeWidth != DEFAULT_STROKE_WIDTH ||
             size != DEFAULT_SIZE ||
             absoluteStrokeWidth

27-33: Consider adding documentation to explain the absoluteStrokeWidth scaling behavior.

The adjustedStrokeWidth() function correctly scales stroke values when absoluteStrokeWidth is true, extending the output range to 0.25–6.0 (from the validated input range of 0.5–4.0). This is intentional and works correctly with SvgManipulator.updateAttributeRecursively(), which converts the value to a string and sets it as an SVG attribute without issues.

Adding KDoc would clarify the non-obvious scaling logic:

+    /**
+     * Calculates the stroke width to apply to the SVG.
+     *
+     * When [absoluteStrokeWidth] is true, scales the stroke width to maintain
+     * visual consistency across different icon sizes. For example, a 2px stroke
+     * at 24px size would become 1px at 48px size to maintain the same visual thickness.
+     *
+     * @return The stroke width value to use in SVG manipulation.
+     *         May exceed the validated 0.5-4.0 range when [absoluteStrokeWidth] is true.
+     */
     fun adjustedStrokeWidth(): Float {
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fd0d3f1 and 2f75841.

📒 Files selected for processing (3)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/Slider.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/ui/LucideCustomization.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/lucide/ui/LucideCustomization.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (ubuntu-latest)
🔇 Additional comments (3)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/Slider.kt (1)

12-12: ExperimentalMaterial3Api carries real stability risk.

The @OptIn(ExperimentalMaterial3Api::class) annotation marks experimental Material3 APIs with no long-term stability guarantee. Compose Material3 has had breaking changes in experimental surfaces across recent releases (e.g., removal of ExperimentalMaterial3ExpressiveApi and ExperimentalMaterial3ComponentOverrideApi, reworked pull-to-refresh in 1.4→1.5), so expect potential maintenance burden when upgrading Material3 versions. Consider pinning to a tested compose-material3 version and review release notes before upgrading.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideSettings.kt (2)

5-14: LGTM - Clean data class structure.

The data class design is well-structured with sensible defaults and reusable constants in the companion object.


16-19: LGTM - Validation addresses UI bounds issue.

The validation ranges are appropriate, and the size constraint (16..48) correctly addresses the UI bounds issue mentioned in the PR objectives where the previous max of 96px exceeded the icon card bounds.

@t-regbs t-regbs force-pushed the feature/lucide-web-import branch from 2f75841 to 699d531 Compare January 11, 2026 21:35
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: 5

🤖 Fix all issues with AI agents
In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideGridItem.kt:
- Around line 10-14: The toGridItems extension on Map<Category,
List<LucideIcon>> relies on insertion order and can produce non-deterministic
category ordering; modify the function to sort categories by name before mapping
by calling toSortedMap(compareBy { it.name }) (i.e., on the Map in toGridItems)
and then flatMap to produce CategoryHeader(category.title) and IconItem entries
so ordering matches Material Symbols' deterministic alphabetical order.

In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt:
- Around line 53-54: iconLoadJobs is a plain mutableMapOf accessed from multiple
coroutines and can race; replace it with a thread-safe map (e.g.,
java.util.concurrent.ConcurrentHashMap or Collections.synchronizedMap) and
update the declaration in LucideViewModel to use that concurrent map for all
reads/writes; ensure places that modify iconLoadJobs (the usages around
iconLoadJob and the methods that call put/remove/containsKey) continue to use
atomic ConcurrentHashMap methods so no additional coroutine synchronization
(Mutex) is required, or alternatively guard all accesses with a single Mutex if
you prefer explicit coroutine-safe locking.
- Around line 86-101: downloadIcon lacks error handling so network/parsing
failures are swallowed; wrap the download/apply sequence (calls to
lucideUseCase.getRawSvg and lucideUseCase.applyCustomizations inside
downloadIcon) in a runCatching or try/catch, cancel/replace iconLoadJob as
before, and on success emit LucideEvent.IconDownloaded with svgContent and
IconNameFormatter.format(icon.displayName) as you do now, but on failure emit or
log a failure event (e.g., a LucideEvent.IconDownloadFailed or use _events.emit
with an error event) including the exception message so callers can react and
failures are visible.
🧹 Nitpick comments (9)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/Slider.kt (1)

12-46: Clean implementation of a customized slider.

The use of @OptIn(ExperimentalMaterial3Api::class) is appropriate—the Slider track customization remains experimental as of early 2026. The approach to disable stop indicators (drawStopIndicator = null) and ticks (drawTick = { _, _ -> }) is correct.

Minor note: enabled = true is hardcoded in the Track. If this component is reused in contexts requiring a disabled state, consider exposing an enabled parameter. For the current use case in LucideCustomization, this is acceptable.

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

42-45: Consider differentiating the error state visually.

The Error state is grouped with Loading and null, showing the same shimmer placeholder. Users won't know if an icon failed to load vs. still loading. Consider showing an error indicator or implementing a retry mechanism on tap.

components/parser/jvm/svg/src/test/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulatorTest.kt (1)

74-77: Assertion may be fragile due to XML serialization differences.

The exact string match <path d=\"M10,10h4v4h-4z\"/> assumes the XML serializer outputs a self-closing tag with that exact format. Different XML serializers or configurations might output <path d="M10,10h4v4h-4z"></path> or add whitespace. Consider using a more flexible assertion.

More robust assertion
-        assertThat(modifiedSvg).contains("<path d=\"M10,10h4v4h-4z\"/>")
+        assertThat(modifiedSvg).contains("d=\"M10,10h4v4h-4z\"")
+        // Verify path element exists without checking exact serialization format
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt (1)

116-120: Consider using a constant for the General fallback category.

Line 119 creates a new Category instance for the "General" fallback. If Category.All exists as a constant (used at line 90), consider whether a Category.General constant should also exist for consistency and to avoid creating duplicate instances.

Use a constant for General category
-        return bestMatch?.toCategory() ?: Category(id = "general", title = "General")
+        return bestMatch?.toCategory() ?: Category.General

And in the Category companion object:

val General = Category(id = "general", title = "General")
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt (3)

41-52: Consider safer JSON parsing.

The direct cast as JsonObject on line 43 will throw ClassCastException if the API response format changes unexpectedly. While this is acceptable for a known API schema, consider using jsonObject property accessor for slightly safer handling.

-        val tagsJson = json.parseToJsonElement(response.bodyAsText()) as JsonObject
+        val tagsJson = json.parseToJsonElement(response.bodyAsText()).jsonObject

54-66: Potential duplicate downloads due to TOCTOU race.

The mutex is released between the cache check and download, allowing multiple coroutines to concurrently miss the cache and download the same icon. While functionally correct, this wastes bandwidth.

Consider holding the lock while checking and registering intent to load, or using a per-icon loading state pattern:

♻️ Suggested fix using double-check pattern
 suspend fun getRawSvg(iconName: String): String = withContext(Dispatchers.IO) {
-    cacheMutex.withLock {
-        rawSvgCache[iconName]
-    } ?: run {
-        val url = "$UNPKG_BASE/icons/$iconName.svg"
-        val downloaded = httpClient.get(url).bodyAsText()
-
-        cacheMutex.withLock {
-            rawSvgCache.put(iconName, downloaded)
+    cacheMutex.withLock {
+        rawSvgCache[iconName]?.let { return@withContext it }
+    }
+    
+    val url = "$UNPKG_BASE/icons/$iconName.svg"
+    val downloaded = httpClient.get(url).bodyAsText()
+
+    cacheMutex.withLock {
+        // Double-check: another coroutine may have cached it
+        rawSvgCache[iconName] ?: run {
+            rawSvgCache.put(iconName, downloaded)
+            downloaded
         }
-        downloaded
     }
 }

94-97: Use explicit locale for consistent hex formatting.

Per static analysis hint, String.format uses the implicit default locale. While hex formatting is unlikely to vary by locale, using Locale.ROOT ensures consistent output.

♻️ Suggested fix
+import java.util.Locale
+
 private fun Color.toHexString(): String {
     val argb = this.toArgb()
-    return String.format("#%06X", 0xFFFFFF and argb)
+    return String.format(Locale.ROOT, "#%06X", 0xFFFFFF and argb)
 }
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt (2)

186-208: Stale state check inside mutex block.

Line 191 checks state.loadedIcons which was captured on line 186, but this check happens inside the mutex after potentially waiting. Another coroutine may have updated loadedIcons in the meantime.

♻️ Suggested fix
 fun loadIconForDisplay(icon: LucideIcon) {
     val job = viewModelScope.launch {
-        val state = lucideRecord.value.safeAs<LucideState.Success>() ?: return@launch
-        val cacheKey = buildIconCacheKey(icon.name, state.settings)
+        val initialState = lucideRecord.value.safeAs<LucideState.Success>() ?: return@launch
+        val cacheKey = buildIconCacheKey(icon.name, initialState.settings)

         iconLoadMutex.withLock {
-            // Skip if already loaded successfully or currently loading
-            val currentState = state.loadedIcons[cacheKey]
+            // Re-check current state after acquiring lock
+            val currentState = lucideRecord.value.safeAs<LucideState.Success>()
+                ?.loadedIcons?.get(cacheKey)
             if (currentState is IconLoadState.Success || currentState is IconLoadState.Loading) {
                 return@launch
             }

256-258: Consider removing redundant public wrapper.

getIconCacheKey simply delegates to buildIconCacheKey. Unless external callers specifically need this, consider making buildIconCacheKey internal or exposing it directly.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f75841 and 699d531.

📒 Files selected for processing (27)
  • components/parser/jvm/svg/api/svg.api
  • components/parser/jvm/svg/build.gradle.kts
  • components/parser/jvm/svg/src/main/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulator.kt
  • components/parser/jvm/svg/src/test/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulatorTest.kt
  • compose/icons/api/icons.api
  • compose/icons/api/icons.klib.api
  • compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/icons/colored/LucideLogo.kt
  • gradle/libs.versions.toml
  • tools/idea-plugin/build.gradle.kts
  • 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/di/NetworkModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/Slider.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideImportScreen.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideIconMetadata.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/di/LucideModule.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/LucideGridItem.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/lucide/ui/LucideCustomization.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideIconDisplay.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideTopActions.kt
🚧 Files skipped from review as they are similar to previous changes (12)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/di/NetworkModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideConfig.kt
  • compose/icons/api/icons.klib.api
  • components/parser/jvm/svg/build.gradle.kts
  • 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/data/LucideIconMetadata.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/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/WebImportSelectorScreen.kt
  • components/parser/jvm/svg/src/main/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulator.kt
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-21T20:55:27.073Z
Learnt from: egorikftp
Repo: ComposeGears/Valkyrie PR: 651
File: tools/idea-plugin/build.gradle.kts:147-175
Timestamp: 2025-10-21T20:55:27.073Z
Learning: In Gradle Kotlin DSL (.gradle.kts) scripts, the types `org.gradle.api.artifacts.ArtifactCollection` and `org.gradle.api.artifacts.component.ModuleComponentIdentifier` are implicitly available and do not require explicit import statements.

Applied to files:

  • tools/idea-plugin/build.gradle.kts
  • gradle/libs.versions.toml
📚 Learning: 2025-12-07T20:07:49.753Z
Learnt from: egorikftp
Repo: ComposeGears/Valkyrie PR: 750
File: tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt:71-85
Timestamp: 2025-12-07T20:07:49.753Z
Learning: In the Valkyrie Gradle plugin (Kotlin), the `useFlatPackage` flag in `IconPackExtension` is only applicable when nested packs are configured. For single icon packs (without nested packs), the flag is intentionally not propagated to `ImageVectorGeneratorConfig` as there is no package hierarchy to flatten.

Applied to files:

  • compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/icons/colored/LucideLogo.kt
  • compose/icons/api/icons.api
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideIconDisplay.kt
📚 Learning: 2026-01-01T18:09:32.917Z
Learnt from: egorikftp
Repo: ComposeGears/Valkyrie PR: 801
File: gradle/libs.versions.toml:14-14
Timestamp: 2026-01-01T18:09:32.917Z
Learning: In the Valkyrie project (ComposeGears/Valkyrie), pin compose-ui-tooling-preview to version 1.10.0 in gradle/libs.versions.toml even if runtime Compose is 1.8.2. This is required because 1.10.0+ provides unified KMP preview annotation support needed for IntelliJ IDEA 2025.3+ preview functionality. Update the entry for compose-ui-tooling-preview to 1.10.0 and add a brief note in repository docs explaining the rationale.

Applied to files:

  • gradle/libs.versions.toml
🧬 Code graph analysis (4)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideTopActions.kt (2)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/WebImportTopActions.kt (1)
  • WebImportTopActions (46-131)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/CategoriesDropdown.kt (1)
  • CategoriesDropdown (12-29)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideIconDisplay.kt (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/WebImportScreenComponents.kt (1)
  • IconLoadingPlaceholder (260-271)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideImportScreen.kt (7)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/TopAppBar.kt (2)
  • TopAppBar (34-48)
  • BackAction (63-76)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/WebImportScreenComponents.kt (5)
  • LoadingContent (46-66)
  • ErrorContent (71-122)
  • EmptyContent (127-142)
  • IconGrid (169-187)
  • IconCard (199-252)
compose/core/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/core/animation/RememberShimmer.kt (1)
  • rememberShimmer (32-50)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideTopActions.kt (1)
  • LucideTopActions (9-31)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt (1)
  • getIconCacheKey (256-258)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideIconDisplay.kt (1)
  • LucideIconDisplay (23-53)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt (1)
  • LucideCustomization (31-130)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt (1)
  • loadConfig (69-92)
🪛 detekt (1.23.8)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt

[warning] 96-96: String.format("#%06X", 0xFFFFFF and argb) uses implicitly default locale for string formatting.

(detekt.potential-bugs.ImplicitDefaultLocale)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (ubuntu-latest)
🔇 Additional comments (20)
compose/icons/api/icons.api (1)

30-33: LGTM!

The new LucideLogoKt API entry follows the established pattern for colored icons, consistent with GoogleMaterialLogoKt and PluginIconKt. The alphabetical ordering is correct.

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

9-31: LGTM!

Clean and well-structured composable that properly delegates to the shared WebImportTopActions component while injecting Lucide-specific category dropdown. The implementation:

  • Follows Compose conventions with modifier as the last parameter with a default value.
  • Correctly wires all callbacks through to the underlying components.
  • Uses a consistent pattern with the existing CategoriesDropdown generic component.
tools/idea-plugin/build.gradle.kts (2)

30-30: LGTM!

The SVG parser dependency addition aligns with the new SvgManipulator utility introduced for DOM-based SVG manipulation.


70-70: LGTM!

The AndroidX Collection dependency provides the LruCache implementation used by LucideRepository for caching SVG strings (300 entries as per PR description).

gradle/libs.versions.toml (1)

10-14: Approved. Version 1.5.0 is the latest stable release and is properly declared and actively used in tools/idea-plugin/build.gradle.kts with LruCache imports across the codebase.

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

23-52: LGTM!

The component correctly uses rememberUpdatedState to capture the latest callback and LaunchedEffect keyed on iconCacheKey to trigger icon loading when settings change. The structure is clean and follows Compose best practices.

compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/icons/colored/LucideLogo.kt (1)

12-56: LGTM!

The LucideLogo ImageVector follows the standard lazy initialization pattern used throughout Compose icon libraries. The dual-path stroked design correctly represents the Lucide logo with appropriate colors and stroke attributes.

components/parser/jvm/svg/api/svg.api (1)

1-6: LGTM!

The SvgManipulator API surface is well-defined with a singleton pattern appropriate for a stateless utility. The three methods (modifySvg, updateAttributeRecursively, updateAttributeConditionally) provide a clean interface for DOM-based SVG manipulation.

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

51-85: LGTM!

The navigation destination setup is well-structured with proper event collection via LaunchedEffect and clean state hoisting. The saveableViewModel pattern ensures state survival across configuration changes.

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

8-23: LGTM!

The DI module correctly shares the httpClient and json instances from NetworkModule as suggested in the PR review comments. The module properly encapsulates lucideRepository as a private implementation detail while exposing only lucideUseCase as the public API.

components/parser/jvm/svg/src/test/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulatorTest.kt (1)

11-249: Comprehensive test coverage.

The test suite thoroughly covers the SvgManipulator functionality including recursive updates, conditional updates, nested elements, edge cases (empty attributes, parse failures), and a real-world Lucide icon scenario. Well-structured tests with clear naming.

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

66-92: LGTM!

The loadConfig function cleanly maps metadata to domain models, groups icons by category, and constructs the configuration. The category inference with priority-based keyword matching is well-documented and handles edge cases appropriately.


26-58: Well-documented category keyword system.

The priority-based keyword mapping with clear documentation and examples makes the categorization logic maintainable. The tiered priorities (1→high specificity, 5→low specificity) provide sensible defaults for icon classification.

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

20-39: LGTM: Clean repository setup with appropriate caching.

The use of LruCache with mutex protection and well-organized constants is a solid foundation.


68-92: LGTM: Clean conditional SVG customization.

The logic appropriately applies only changed attributes and delegates to SvgManipulator for DOM manipulation.

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

60-84: LGTM: Proper initialization with state recovery.

The init block correctly avoids redundant loading when state is already Success, and error handling is appropriate.


224-254: LGTM: Clean parsing and error handling.

Both methods properly synchronize cache updates and maintain consistent state.


269-293: LGTM: Comprehensive filtering logic.

The filter correctly handles category selection and search across name, display name, and tags with case-insensitive matching.


295-300: LGTM: Useful state update helper.

This pattern simplifies state mutations. Note that concurrent calls could race, but this is acceptable for UI state management.


303-330: LGTM: Well-structured state definitions.

The sealed interfaces provide clear state modeling, and the @Stable annotation is appropriate for Compose state.

Comment on lines 191 to 203
is IconItem<*> -> {
val lucideIcon = item.icon as LucideIcon
val iconCacheKey = getIconCacheKey(lucideIcon.name, state.settings)
var iconLoadState = state.loadedIcons[iconCacheKey]

if (iconLoadState !is IconLoadState.Success) {
iconLoadState = state.loadedIcons.entries
.firstOrNull {
it.key.startsWith("${lucideIcon.name}-") &&
it.value is IconLoadState.Success
}
?.value
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unchecked cast and potential performance concern in fallback logic.

  1. Line 192: The unchecked cast item.icon as LucideIcon could throw at runtime if the grid ever contains non-Lucide icons. Consider using a type-safe approach or ensuring IconItem<LucideIcon> at the grid level.

  2. Lines 196-203: The fallback logic performs a linear scan through all loadedIcons entries on every recomposition for icons not yet loaded with current settings. For large icon sets (Lucide has 1500+ icons), this could cause frame drops.

Suggested improvement for the fallback lookup

Consider caching the fallback lookup or restructuring the cache to enable O(1) lookups by icon name:

-                               if (iconLoadState !is IconLoadState.Success) {
-                                   iconLoadState = state.loadedIcons.entries
-                                       .firstOrNull {
-                                           it.key.startsWith("${lucideIcon.name}-") &&
-                                               it.value is IconLoadState.Success
-                                       }
-                                       ?.value
-                               }
+                               val displayState = iconLoadState.takeIf { it is IconLoadState.Success }
+                                   ?: state.getLatestSuccessfulState(lucideIcon.name)

Then add a helper in LucideState.Success that maintains a separate index by icon name.

@t-regbs t-regbs force-pushed the feature/lucide-web-import branch from 699d531 to 2f75841 Compare January 11, 2026 21:44
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/lucide/data/LucideRepository.kt:
- Around line 41-52: The loadIconList function performs an unsafe cast and lacks
HTTP error handling; wrap the HTTP request and JSON parsing in a try/catch,
check the HTTP response status before parsing (e.g., ensure status is 200/OK),
parse the body with json.parseToJsonElement(...) and verify the result is a
JsonObject (if not, handle gracefully by logging and returning an empty list or
propagating a meaningful exception), and handle any JSON parsing exceptions;
update references in this function (loadIconList) to use these guards so you
don’t cast to JsonObject blindly and you handle non-OK responses/timeouts
appropriately.
🧹 Nitpick comments (9)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/di/MaterialSymbolsModule.kt (1)

5-5: Inconsistent access pattern for httpClient.

materialSymbolsConfigRepository uses network.httpClient (line 15), while materialFontRepository uses the direct import httpClient (line 20). For consistency within this module, use the network instance for both repositories and remove the unused direct import.

Suggested fix

Remove the direct import at line 5:

 import com.composegears.leviathan.Leviathan
 import io.github.composegears.valkyrie.ui.screen.webimport.common.di.NetworkModule
-import io.github.composegears.valkyrie.ui.screen.webimport.common.di.NetworkModule.httpClient
 import io.github.composegears.valkyrie.ui.screen.webimport.material.data.config.MaterialSymbolsConfigRepository

Update materialFontRepository to use network.httpClient:

     private val materialFontRepository by instanceOf {
-        MaterialFontRepository(httpClient = inject(httpClient))
+        MaterialFontRepository(httpClient = inject(network.httpClient))
     }

Also applies to: 19-21

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

47-56: Consider differentiating error state from loading.

Error and Loading states both show IconLoadingPlaceholder, making them visually indistinguishable. Users won't know if an icon failed to load versus still loading.

Consider showing an error indicator or adding retry capability for failed icons.

♻️ Optional: Add visual distinction for error state
         when (iconLoadState) {
-            null, IconLoadState.Loading, IconLoadState.Error -> {
+            null, IconLoadState.Loading -> {
                 IconLoadingPlaceholder(shimmer = shimmer)
             }
+            IconLoadState.Error -> {
+                Icon(
+                    imageVector = ValkyrieIcons.Outlined.BrokenImage, // or similar error icon
+                    contentDescription = "Failed to load ${icon.name}",
+                    tint = MaterialTheme.colorScheme.error,
+                )
+            }
             is IconLoadState.Success -> Icon(
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt (2)

38-39: Local state may become stale if settings change externally.

The local strokeWidth and size states are initialized once from settings using remember. If settings changes from an external source (e.g., after navigation or state restoration), these local values won't update.

Consider using LaunchedEffect to sync or use rememberUpdatedState:

♻️ Proposed fix
-    var strokeWidth by remember { mutableFloatStateOf(settings.strokeWidth) }
-    var size by remember { mutableIntStateOf(settings.size) }
+    var strokeWidth by remember(settings.strokeWidth) { mutableFloatStateOf(settings.strokeWidth) }
+    var size by remember(settings.size) { mutableIntStateOf(settings.size) }

73-73: Locale-dependent decimal formatting.

String.format("%.1f", strokeWidth) uses the default locale, which may produce unexpected results (e.g., comma instead of period as decimal separator in some locales).

Consider using Locale.US or Locale.ROOT for consistent formatting:

♻️ Proposed fix
-                text = "Stroke width: ${String.format("%.1f", strokeWidth)}",
+                text = "Stroke width: ${String.format(java.util.Locale.ROOT, "%.1f", strokeWidth)}",
components/parser/jvm/svg/src/main/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulator.kt (1)

33-59: Good security practices for XML parsing.

The XXE (XML External Entity) prevention measures are properly implemented:

  • disallow-doctype-decl set to true
  • External general and parameter entities disabled
  • ACCESS_EXTERNAL_DTD and ACCESS_EXTERNAL_STYLESHEET restricted on TransformerFactory

One minor issue: Line 56 uses println for error logging. Consider removing the print statement or using a proper logger for consistency with the rest of the codebase.

♻️ Optional: Remove println or use proper logging
         } catch (e: Exception) {
-            println("Failed to parse SVG for modification: ${e.message}")
             svgContent
         }
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideImportScreen.kt (1)

192-193: Unchecked cast could be made safer.

The cast item.icon as LucideIcon assumes all IconItem instances in the grid contain LucideIcon. While this should be true in this context, consider a safer approach:

♻️ Safer type handling
                             is IconItem<*> -> {
-                                val lucideIcon = item.icon as LucideIcon
+                                val lucideIcon = item.icon as? LucideIcon ?: return@items
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt (1)

94-97: Use explicit locale for hex color formatting.

As flagged by static analysis, String.format uses the default locale implicitly. While hex formatting is typically locale-independent, it's best to be explicit:

♻️ Proposed fix
     private fun Color.toHexString(): String {
         val argb = this.toArgb()
-        return String.format("#%06X", 0xFFFFFF and argb)
+        return String.format(java.util.Locale.ROOT, "#%06X", 0xFFFFFF and argb)
     }
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt (2)

213-260: State capture may become stale during async execution.

In loadIconForDisplay, the state is captured at line 215, but the coroutine executes asynchronously. If settings change during execution, state.settings at line 246 will be stale.

Additionally, lines 254-257 compute the cache key multiple times with potentially different values:

♻️ Proposed fix - capture settings early and reuse
     fun loadIconForDisplay(icon: LucideIcon): Job {
+        val currentState = lucideRecord.value.safeAs<LucideState.Success>() ?: return Job()
+        val settings = currentState.settings
+        val cacheKey = buildIconCacheKey(icon.name, settings)
+
         val job = viewModelScope.launch {
-            val state = lucideRecord.value.safeAs<LucideState.Success>() ?: return@launch
-            val cacheKey = buildIconCacheKey(icon.name, state.settings)
-
             iconLoadJobs[cacheKey]?.cancel()

             val mutex = iconLoadMutexes.computeIfAbsent(cacheKey) { Mutex() }

             mutex.withLock {
-                val currentState = state.loadedIcons[cacheKey]
-                if (currentState is IconLoadState.Success || currentState is IconLoadState.Loading) {
+                val loadState = lucideRecord.value.safeAs<LucideState.Success>()?.loadedIcons?.get(cacheKey)
+                if (loadState is IconLoadState.Success || loadState is IconLoadState.Loading) {
                     return@launch
                 }
                 // ... rest of the logic
             }

             runCatching {
                 val rawSvg = lucideUseCase.getRawSvg(icon.name)
-                val customizedSvg = lucideUseCase.applyCustomizations(rawSvg, state.settings)
+                val customizedSvg = lucideUseCase.applyCustomizations(rawSvg, settings)

                 parseAndCacheIcon(icon.name, customizedSvg, cacheKey)
             }.onFailure { error ->
                 handleIconParseError(icon.name, cacheKey, error)
             }
         }

-        iconLoadJobs[buildIconCacheKey(icon.name, lucideRecord.value.safeAs<LucideState.Success>()?.settings ?: LucideSettings())] = job
+        iconLoadJobs[cacheKey] = job
         job.invokeOnCompletion {
-            iconLoadJobs.remove(buildIconCacheKey(icon.name, lucideRecord.value.safeAs<LucideState.Success>()?.settings ?: LucideSettings()))
+            iconLoadJobs.remove(cacheKey)
         }

         return job
     }

339-344: updateSuccess has potential race condition.

The read-check-write pattern on lucideRecord.value is not atomic. Concurrent calls could overwrite each other's changes.

In practice, most calls are on Dispatchers.Default or viewModelScope which may serialize access, but consider using a mutex or atomic update pattern for safety:

♻️ Thread-safe update pattern
+    private val stateMutex = Mutex()
+
-    private inline fun updateSuccess(crossinline transform: (LucideState.Success) -> LucideState.Success) {
+    private suspend inline fun updateSuccess(crossinline transform: (LucideState.Success) -> LucideState.Success) {
+        stateMutex.withLock {
             val current = lucideRecord.value
             if (current is LucideState.Success) {
                 lucideRecord.value = transform(current)
             }
+        }
     }

Note: This would require updating callers to use coroutine context.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 699d531 and 568c771.

📒 Files selected for processing (29)
  • components/parser/jvm/svg/api/svg.api
  • components/parser/jvm/svg/build.gradle.kts
  • components/parser/jvm/svg/src/main/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulator.kt
  • components/parser/jvm/svg/src/test/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulatorTest.kt
  • compose/icons/api/icons.api
  • compose/icons/api/icons.klib.api
  • compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/icons/colored/LucideLogo.kt
  • gradle/libs.versions.toml
  • tools/idea-plugin/build.gradle.kts
  • 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/di/NetworkModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/Slider.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideImportScreen.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideIconMetadata.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/di/LucideModule.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/LucideGridItem.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/lucide/ui/LucideCustomization.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideIconDisplay.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideTopActions.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/di/MaterialSymbolsModule.kt
  • tools/idea-plugin/src/main/resources/messages/Valkyrie.properties
🚧 Files skipped from review as they are similar to previous changes (11)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportFlow.kt
  • compose/icons/api/icons.api
  • tools/idea-plugin/src/main/resources/messages/Valkyrie.properties
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/di/NetworkModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideSettings.kt
  • compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/icons/colored/LucideLogo.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideIcon.kt
  • tools/idea-plugin/build.gradle.kts
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideIconMetadata.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideGridItem.kt
  • gradle/libs.versions.toml
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-07T20:07:49.753Z
Learnt from: egorikftp
Repo: ComposeGears/Valkyrie PR: 750
File: tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt:71-85
Timestamp: 2025-12-07T20:07:49.753Z
Learning: In the Valkyrie Gradle plugin (Kotlin), the `useFlatPackage` flag in `IconPackExtension` is only applicable when nested packs are configured. For single icon packs (without nested packs), the flag is intentionally not propagated to `ImageVectorGeneratorConfig` as there is no package hierarchy to flatten.

Applied to files:

  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideIconDisplay.kt
📚 Learning: 2026-01-01T18:09:41.901Z
Learnt from: egorikftp
Repo: ComposeGears/Valkyrie PR: 801
File: gradle/libs.versions.toml:14-14
Timestamp: 2026-01-01T18:09:41.901Z
Learning: In the Valkyrie project (ComposeGears/Valkyrie), compose-ui-tooling-preview must use version 1.10.0 (even though runtime Compose is 1.8.2) because 1.10.0+ provides unified KMP preview annotation support required for IntelliJ IDEA 2025.3+ preview functionality.

Applied to files:

  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt
🧬 Code graph analysis (5)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideTopActions.kt (2)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/WebImportTopActions.kt (1)
  • WebImportTopActions (46-131)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/CategoriesDropdown.kt (1)
  • CategoriesDropdown (12-29)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideIconDisplay.kt (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/WebImportScreenComponents.kt (1)
  • IconLoadingPlaceholder (219-230)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt (4)
compose/core/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/core/layout/Row.kt (1)
  • CenterVerticalRow (10-20)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/foundation/IconButton.kt (1)
  • IconButton (15-36)
compose/core/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/core/layout/Spacer.kt (2)
  • HorizontalSpacer (23-29)
  • VerticalSpacer (15-21)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/Slider.kt (1)
  • NoStopIndicatorSlider (12-46)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt (1)
compose/ui/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/ui/InfoCard.kt (1)
  • InfoCard (24-78)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt (1)
  • loadConfig (69-92)
🪛 detekt (1.23.8)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt

[warning] 96-96: String.format("#%06X", 0xFFFFFF and argb) uses implicitly default locale for string formatting.

(detekt.potential-bugs.ImplicitDefaultLocale)

🔇 Additional comments (27)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/di/MaterialSymbolsModule.kt (1)

22-27: LGTM!

The public exposure of materialSymbolsConfigUseCase is appropriate for this DI module, and the wiring is correct.

components/parser/jvm/svg/build.gradle.kts (1)

9-10: LGTM!

The test dependency addition is appropriate for supporting the new SvgManipulatorTest tests introduced in this PR.

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

3-10: LGTM!

Clean domain model design. Using a data class makes it suitable as a map key (for gridItems: Map<Category, List<LucideIcon>>), and the companion object pattern for the static All instance is idiomatic Kotlin.

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

36-44: LGTM!

The custom Track configuration correctly disables the stop indicator and tick marks, providing a cleaner slider appearance for the Lucide customization UI.


21-21: The current implementation is correct and aligns with official Jetpack Compose best practices. Using remember { MutableInteractionSource() } as a default parameter value for a composable is the recommended pattern, not an anti-pattern. The alternative suggested in the original review—using null as a sentinel value to lazily initialize with remember inside the function body—is explicitly discouraged by the official Compose API guidelines as it produces inconsistent behavior across recompositions. No changes are needed.

compose/icons/api/icons.klib.api (1)

21-22: LGTM!

The new LucideLogo API entry follows the established pattern for colored icons in the ValkyrieIcons API surface.

components/parser/jvm/svg/api/svg.api (1)

1-6: LGTM!

The SvgManipulator API is well-structured with clear separation of concerns:

  • modifySvg for high-level SVG string manipulation via callback
  • updateAttributeRecursively and updateAttributeConditionally for DOM-level operations

Note that the public API exposes org.w3c.dom.Element, which couples consumers to the W3C DOM API. This appears intentional given the modifySvg pattern allows callback-based manipulation without requiring direct Element handling by most callers.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/WebImportSelectorScreen.kt (3)

34-47: LGTM! Clean navigation routing.

The when expression follows the existing pattern for GoogleMaterialSymbols and correctly routes Lucide to LucideImportScreen.


91-94: LGTM!

The enum extension is straightforward.


67-82: LGTM! Consistent UI pattern.

Good use of verticalArrangement = Arrangement.spacedBy(16.dp) instead of manual spacers—this is cleaner and ensures consistent spacing. The new Lucide InfoCard follows the same structure as the Google Material card. Resource strings for the Lucide card are properly defined in the messages bundle.

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

8-23: LGTM! Clean DI wiring.

Good approach sharing httpClient and json via NetworkModule as suggested in the PR comments. The repository is appropriately kept private while exposing only the use case.

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

35-41: LGTM! Proper resource cleanup.

Good use of DisposableEffect with job cancellation on dispose to prevent leaked coroutines when the composable leaves composition or the cache key changes.

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

9-31: LGTM!

Clean wrapper that follows Compose conventions and properly delegates to existing components. The categoryName = { it.title } mapping is simple and effective.

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

9-19: Remove this concern — icon names in Lucide are globally unique.

associateBy { it.name } doesn't risk losing data. Each icon is created once from the Lucide repository (which has globally unique icon names) and grouped into exactly one category via groupBy { it.category }. No duplicates exist in the flattened result.

components/parser/jvm/svg/src/test/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulatorTest.kt (1)

1-250: Comprehensive test coverage for SvgManipulator.

The test suite provides thorough coverage including:

  • Root and recursive attribute updates
  • Conditional attribute matching
  • Nested element handling
  • Various SVG attribute formats (quotes, spaces)
  • Error handling for invalid input
  • Real-world Lucide icon manipulation scenario

The tests are well-structured and validate the key behaviors of the SvgManipulator utility.

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

41-129: Well-structured customization panel.

The UI layout is clean with proper use of layout helpers (CenterVerticalRow, VerticalSpacer, etc.). The pattern of updating settings only on onValueChangeFinished is a good optimization to avoid excessive re-parsing during slider drags, as discussed in the PR comments.

components/parser/jvm/svg/src/main/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulator.kt (1)

68-114: Clean recursive attribute update implementation.

Both updateAttributeRecursively and updateAttributeConditionally are well-implemented with clear separation of concerns. The recursive traversal handles all child elements correctly.

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt (3)

26-64: Well-documented category keyword system.

The priority-based keyword mapping is clearly documented with helpful examples. The structure allows for extensibility when adding new categories.


103-120: Category inference logic is solid.

The priority-based matching with name match boosting (-2) effectively prioritizes direct name matches over tag matches. The fallback to "General" category handles unmatched icons gracefully.

Minor note: negative priorities (from the -2 boost) work correctly with minByOrNull, but consider documenting this behavior or using a separate scoring mechanism for clarity.


66-133: Clean use case design.

The LucideUseCase follows good separation of concerns by delegating data operations to the repository while handling business logic (category inference, display name formatting) internally.

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

52-86: Well-structured navigation destination.

The screen properly:

  • Uses saveableViewModel for state persistence across configuration changes
  • Handles events via LaunchedEffect with proper coroutine scoping
  • Delegates UI rendering to a separate composable for testability

133-237: Clean icons content implementation.

The IconsContent composable:

  • Uses rememberShimmer for loading placeholders
  • Implements proper grid item spanning (headers vs icons)
  • Includes smart fallback to latest successful state during settings changes for smoother UX
  • Integrates customization panel via SidePanel

Good use of pointerInput to clear focus when tapping outside input fields.

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

54-66: Potential redundant fetches under concurrent access.

The mutex only protects individual cache operations, not the entire fetch-then-cache sequence. Two concurrent calls for the same uncached icon could both proceed to fetch. This is benign (just redundant network calls) but could be optimized if needed.

The current implementation is acceptable for this use case since:

  1. Icons are typically loaded sequentially from grid visibility
  2. Duplicate fetches only waste bandwidth, not cause data issues

68-92: SVG customization logic is well-implemented.

The customization applies modifications only when settings differ from defaults, avoiding unnecessary DOM manipulations. The conditional color update using updateAttributeConditionally correctly targets only currentColor values.

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

150-179: Settings update logic handles cache invalidation well.

The updateSettings function properly:

  1. Cancels ongoing load jobs
  2. Clears old cache entries
  3. Re-parses previously loaded icons with new settings

This ensures consistent rendering when settings change.


299-311: Cache key design is appropriate.

Using || as a delimiter is a reasonable choice since it's unlikely to appear in icon names. The key includes all relevant settings for proper cache invalidation.


1-92: Solid ViewModel architecture.

The ViewModel effectively manages:

  • Saveable state via tiamat's recordOf
  • Debounced search with Flow.debounce
  • LRU caching for parsed ImageVector instances
  • Per-icon mutexes for concurrent load protection

Good use of ConcurrentHashMap for thread-safe job and mutex tracking.

Comment on lines +41 to +52
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(), // Categories will be inferred from tags
)
}
}
Copy link

@coderabbitai coderabbitai bot Jan 12, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt | head -60

Repository: ComposeGears/Valkyrie

Length of output: 2870


🌐 Web query:

lucide-static tags.json structure format

💡 Result:

Structure: tags.json is a plain JSON object that maps each icon name (kebab-case filename/slug) to an array of tag strings.

  • top-level type: object (dictionary). [1]
  • keys: icon names (e.g. "mail", "arrow-left", "pin-off").
  • values: arrays of tag strings (lowercase keywords) describing that icon (e.g. ["mail","message","communication"]). [2][3]

Example:
{
"mail": ["email","message","letter"],
"arrow-left": ["navigation","back","previous"],
"pin-off": ["pin","unpin","map"]
}

Sources:

  • lucide-static package listing / tags.json file (UNPKG). [1]
  • Lucide tags & categories documentation / metadata tools (migrate scripts & usage). [2][3]

References:
[1] UNPKG — lucide-static (tags.json present in package). (app.unpkg.com)
[2] Lucide docs / Lucide Static overview. (lucide.dev)
[3] Tags and Categories (documentation / DeepWiki summary of tags.json usage). (deepwiki.com)

Citations:


Add safe casting and error handling for JSON response.

Line 43 uses an unsafe cast that could throw a ClassCastException if the response structure is unexpected. While lucide-static's tags.json is stable and reliably returns a JSON object, defensive error handling is a best practice:

Suggested fix
-        val tagsJson = json.parseToJsonElement(response.bodyAsText()) as JsonObject
+        val tagsJson = json.parseToJsonElement(response.bodyAsText()) as? JsonObject
+            ?: throw IllegalStateException("Invalid tags.json format: expected JSON object")

Note: The code also lacks HTTP-level error handling (e.g., status codes, timeouts); consider adding that as well.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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(), // Categories will be inferred from tags
)
}
}
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
?: throw IllegalStateException("Invalid tags.json format: expected JSON object")
tagsJson.entries.map { (iconName, tagsArray) ->
val tags = tagsArray.jsonArray.map { it.jsonPrimitive.content }
iconName to LucideIconMetadata(
tags = tags,
categories = emptyList(), // Categories will be inferred from tags
)
}
}
🤖 Prompt for AI Agents
In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt
around lines 41 - 52, The loadIconList function performs an unsafe cast and
lacks HTTP error handling; wrap the HTTP request and JSON parsing in a
try/catch, check the HTTP response status before parsing (e.g., ensure status is
200/OK), parse the body with json.parseToJsonElement(...) and verify the result
is a JsonObject (if not, handle gracefully by logging and returning an empty
list or propagating a meaningful exception), and handle any JSON parsing
exceptions; update references in this function (loadIconList) to use these
guards so you don’t cast to JsonObject blindly and you handle non-OK
responses/timeouts appropriately.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The current code is fine. The outer runCatching already provides proper error handling and user feedback.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for usages of loadIconList to verify error handling context
rg -n -A5 -B5 'loadIconList\s*\(' tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/

Length of output: 3495


🏁 Script executed:

#!/bin/bash
# Search for usages of loadConfig to verify runCatching wrapper
rg -n -A10 -B5 'loadConfig\s*\(' tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt

Length of output: 1322


@t-regbs, you're absolutely right! I can see the runCatching block in LucideViewModel.loadConfig() (lines 98-107) that wraps the loadConfig() call and provides proper error handling with .onFailure that logs the error and updates the state to show a user-friendly error message. This catches any exceptions from the repository layer, including potential JSON parsing issues.

The error handling strategy is appropriate for this architecture. Thanks for the clarification! 🐰

@t-regbs t-regbs force-pushed the feature/lucide-web-import branch from 568c771 to 093ac0c Compare January 12, 2026 02:27
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: 9

🤖 Fix all issues with AI agents
In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/di/NetworkModule.kt:
- Around line 21-30: The httpClient binding should be made a singleton like the
json binding by adding keepAlive = true to the instanceOf declaration; update
the val httpClient by instanceOf { ... } for HttpClient(OkHttp) (the
HttpClient/OkHttp initialization block) to include keepAlive = true so a single
shared HttpClient instance is used across web import features.

In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt:
- Around line 94-97: The Color.toHexString function uses String.format with the
implicit default locale which can vary; update the call to use a
locale-independent formatter (e.g., pass Locale.US or Locale.ROOT as the first
argument) so the hex formatting is deterministic across locales while keeping
the same format string and bitmask logic in toHexString.
- Around line 42-43: The code unsafely casts the parsed JSON to JsonObject
(tagsJson = json.parseToJsonElement(... ) as JsonObject) which can throw; change
to parse to a JsonElement, then verify it is a JsonObject (e.g., val element =
json.parseToJsonElement(response.bodyAsText()); if (element is JsonObject) { val
tagsJson = element } else { handle invalid format: log error via your logger in
LucideRepository, return/throw a descriptive exception, or fallback to empty
object }); use the jsonObject-safe accessor or type check instead of the unsafe
cast to avoid ClassCastException.
- Around line 54-66: In getRawSvg, the current separate locking allows multiple
coroutines to fetch the same icon; either hold cacheMutex for the whole
check+fetch+put by moving the HTTP call inside cacheMutex.withLock around
rawSvgCache and downloaded storage, or implement a per-icon in-flight map (e.g.,
ongoingRequests: MutableMap<String, Deferred<String>>) so callers await the same
Deferred; reference getRawSvg, cacheMutex, rawSvgCache and httpClient (or create
ongoingRequests and use coroutineScope.async/await) to ensure only one network
fetch per icon.

In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt:
- Around line 222-227: You captured `state` before acquiring `mutex` so the
stale `currentState` may miss concurrent updates; inside the `mutex.withLock`
block re-read `state.loadedIcons[cacheKey]` (e.g. assign `val currentState =
state.loadedIcons[cacheKey]` inside the lock) and use that value for the `if
(currentState is IconLoadState.Success || currentState is IconLoadState.Loading)
return@launch` check, removing reliance on the earlier captured value so the
decision reflects the protected, up-to-date state.
- Around line 254-257: The code recomputes the cache key when registering and
removing jobs which can mismatch if settings change; capture the cache key once
into a local val (e.g., cacheKey = buildIconCacheKey(icon.name,
lucideRecord.value.safeAs<LucideState.Success>()?.settings ?: LucideSettings()))
and then use that cacheKey for both iconLoadJobs[cacheKey] = job and inside
job.invokeOnCompletion { iconLoadJobs.remove(cacheKey) } so the same key is used
consistently (references: iconLoadJobs, buildIconCacheKey, lucideRecord,
LucideState.Success, LucideSettings, job).
- Around line 339-344: The updateSuccess function performs a non-atomic
read-modify-write on lucideRecord causing lost updates when called from multiple
coroutines; convert it to a suspend function and protect the critical section
with a Mutex (e.g., a private val lucideMutex = Mutex()), then acquire the mutex
in updateSuccess, re-read lucideRecord, apply the transform only while holding
the lock and update lucideRecord before releasing the lock; updateSuccess and
any call sites are already in suspend contexts so switch the signature to
suspend and serialize mutations to LucideState.Success using the Mutex to
prevent races.

In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt:
- Around line 38-39: The local vars strokeWidth and size are initialized from
settings using remember so they won't update when settings changes; update the
initialization to recreate or derive values when settings changes (e.g., use
remember(key = settings.strokeWidth) with mutableFloatStateOf for strokeWidth
and remember(key = settings.size) with mutableIntStateOf for size, or use
derivedStateOf tied to settings.strokeWidth/settings.size) so the local state in
LucideCustomization reflects external updates to settings.
- Around line 72-75: The decimal formatting for the strokeWidth label uses the
platform default locale via String.format("%.1f", strokeWidth) which can produce
commas in some locales; change the call in LucideCustomization.kt (the Text that
shows "Stroke width: ${String.format(...)}") to use an explicit locale (e.g.,
Locale.US) so decimals always render with a dot, i.e., replace
String.format("%.1f", strokeWidth) with a locale-aware variant such as
String.format(Locale.US, "%.1f", strokeWidth) inside the Text composable.
🧹 Nitpick comments (7)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideIconDisplay.kt (1)

47-50: Error state shows identical UI to loading state.

When iconLoadState is Error, users see the same shimmer placeholder as during loading, providing no feedback that something went wrong. Consider showing a distinct error indicator (e.g., an error icon or retry option) to improve UX.

components/parser/jvm/svg/src/main/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulator.kt (1)

33-59: Replace println with proper logging.

Using println for error logging (line 56) is not appropriate for an IDE plugin. It may not be visible to users and doesn't integrate with IntelliJ's logging infrastructure.

♻️ Suggested fix

Consider using a logger or simply removing the print statement since the function already gracefully returns the original content:

         } catch (e: Exception) {
-            println("Failed to parse SVG for modification: ${e.message}")
+            // Return original content on parse failure
             svgContent
         }

Or if logging is needed, use IntelliJ's logger:

private val LOG = com.intellij.openapi.diagnostic.Logger.getInstance(SvgManipulator::class.java)
// ...
LOG.warn("Failed to parse SVG for modification", e)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt (1)

107-109: Priority boost creates negative values.

The -2 boost for name matches can result in negative priority values (e.g., priority 1 becomes -1). While minByOrNull handles this correctly, it could be confusing for future maintainers. Consider documenting this or using a multiplicative factor instead.

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

48-49: Unbounded growth of iconLoadMutexes map.

The iconLoadMutexes map grows indefinitely as new cache keys are encountered but entries are never removed. Unlike iconLoadJobs which cleans up via invokeOnCompletion, mutexes persist indefinitely. Over many settings changes, this could accumulate thousands of stale mutex entries.

Consider either:

  1. Using a bounded cache for mutexes similar to iconVectorCache
  2. Cleaning up mutexes when their corresponding cache entries are evicted
  3. Using a single global mutex if contention is low enough

126-128: Silent failure: no user feedback when icon download fails.

When downloadIcon fails, the error is logged but the UI receives no notification. The user initiates a download action and gets no feedback on failure.

Consider emitting an error event (e.g., LucideEvent.DownloadFailed) so the UI can display a toast or error message.

💡 Suggested approach
sealed interface LucideEvent {
    data class IconDownloaded(...) : LucideEvent
    data class DownloadFailed(val iconName: String, val message: String) : LucideEvent
}

// In downloadIcon:
}.onFailure { error ->
    LOG.error("Failed to download icon '${icon.name}'", error)
    _events.emit(LucideEvent.DownloadFailed(icon.name, error.message ?: "Unknown error"))
}

224-226: Failed icons will retry on every display request.

The check skips re-loading for Success or Loading states but not Error. This means icons that previously failed will retry on each visibility change, potentially causing repeated network requests.

If retry is intentional, consider adding a backoff or max retry count. If not, add Error to the skip condition:

-if (currentState is IconLoadState.Success || currentState is IconLoadState.Loading) {
+if (currentState is IconLoadState.Success || currentState is IconLoadState.Loading || currentState is IconLoadState.Error) {
     return@launch
 }

370-371: Unbounded growth of loadedIcons map vs bounded iconVectorCache.

The iconVectorCache LruCache is bounded at 300 entries, but loadedIcons in state grows without bound. Over time, the state map can accumulate thousands of entries (one per icon + settings combination), while the actual vectors may have been evicted from the LRU cache.

Consider periodically pruning loadedIcons or synchronizing its size with the cache, especially when settings change and old entries become stale.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 568c771 and 093ac0c.

📒 Files selected for processing (27)
  • components/parser/jvm/svg/api/svg.api
  • components/parser/jvm/svg/build.gradle.kts
  • components/parser/jvm/svg/src/main/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulator.kt
  • components/parser/jvm/svg/src/test/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulatorTest.kt
  • compose/icons/api/icons.api
  • compose/icons/api/icons.klib.api
  • compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/icons/colored/LucideLogo.kt
  • gradle/libs.versions.toml
  • tools/idea-plugin/build.gradle.kts
  • 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/di/NetworkModule.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/Slider.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideImportScreen.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideIconMetadata.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/di/LucideModule.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/LucideGridItem.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/lucide/ui/LucideCustomization.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideIconDisplay.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideTopActions.kt
🚧 Files skipped from review as they are similar to previous changes (12)
  • 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/WebImportFlow.kt
  • compose/icons/api/icons.api
  • gradle/libs.versions.toml
  • 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/LucideImportScreen.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/common/ui/Slider.kt
  • compose/icons/src/commonMain/kotlin/io/github/composegears/valkyrie/compose/icons/colored/LucideLogo.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/lucide/di/LucideModule.kt
  • components/parser/jvm/svg/api/svg.api
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-07T20:07:49.753Z
Learnt from: egorikftp
Repo: ComposeGears/Valkyrie PR: 750
File: tools/gradle-plugin/src/main/kotlin/io/github/composegears/valkyrie/gradle/internal/task/GenerateImageVectorsTask.kt:71-85
Timestamp: 2025-12-07T20:07:49.753Z
Learning: In the Valkyrie Gradle plugin (Kotlin), the `useFlatPackage` flag in `IconPackExtension` is only applicable when nested packs are configured. For single icon packs (without nested packs), the flag is intentionally not propagated to `ImageVectorGeneratorConfig` as there is no package hierarchy to flatten.

Applied to files:

  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/model/LucideGridItem.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideIconDisplay.kt
📚 Learning: 2025-10-21T20:55:27.073Z
Learnt from: egorikftp
Repo: ComposeGears/Valkyrie PR: 651
File: tools/idea-plugin/build.gradle.kts:147-175
Timestamp: 2025-10-21T20:55:27.073Z
Learning: In Gradle Kotlin DSL (.gradle.kts) scripts, the types `org.gradle.api.artifacts.ArtifactCollection` and `org.gradle.api.artifacts.component.ModuleComponentIdentifier` are implicitly available and do not require explicit import statements.

Applied to files:

  • tools/idea-plugin/build.gradle.kts
🧬 Code graph analysis (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideTopActions.kt (2)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/WebImportTopActions.kt (1)
  • WebImportTopActions (46-131)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/ui/CategoriesDropdown.kt (1)
  • CategoriesDropdown (12-29)
🪛 detekt (1.23.8)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt

[warning] 96-96: String.format("#%06X", 0xFFFFFF and argb) uses implicitly default locale for string formatting.

(detekt.potential-bugs.ImplicitDefaultLocale)

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt

[warning] 73-73: String.format("%.1f", strokeWidth) uses implicitly default locale for string formatting.

(detekt.potential-bugs.ImplicitDefaultLocale)

🔇 Additional comments (20)
compose/icons/api/icons.klib.api (1)

21-22: LGTM!

The new LucideLogo API entry correctly follows the established pattern for colored icons in this library. The declaration is properly ordered alphabetically between GoogleMaterialLogo and ValkyrieLogo, and uses the expected extension property pattern returning ImageVector.

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

52-61: Reset logic is well implemented.

The reset button correctly updates both local state variables and propagates the default settings via onSettingsChange. Enabling the button conditionally based on settings.isModified provides good UX feedback.


77-85: Slider configuration looks correct.

The onValueChange / onValueChangeFinished pattern properly separates intermediate drag updates from final commits, avoiding excessive reparsing during slider interaction. This aligns with the PR discussion about reducing unnecessary reparsing.

Also applies to: 94-102


106-125: Switch implementation is clean.

The absoluteStrokeWidth toggle correctly propagates changes immediately via onSettingsChange, which is appropriate since switches don't have continuous drag interaction like sliders.

components/parser/jvm/svg/build.gradle.kts (1)

9-10: LGTM!

The test dependency addition aligns with the new SvgManipulatorTest.kt test suite for the SVG manipulation utilities.

tools/idea-plugin/build.gradle.kts (2)

30-30: LGTM!

Adding the SVG parser module enables the Lucide feature to use SvgManipulator for DOM-based SVG modifications.


70-70: LGTM!

The androidx.collection dependency provides the LruCache implementation used in LucideRepository for caching SVG strings.

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

10-15: LGTM!

Clean extension function that properly sorts categories and flattens the map into a display-ready grid structure.

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

12-15: The tags and categories fields do not require default values. The LucideIconMetadata object is manually constructed in loadIconList() with explicit values—tags are always populated from the parsed tags.json API, and categories are intentionally always initialized as emptyList() to be inferred later from tags. Automatic deserialization of this class from API responses does not occur.

Likely an incorrect or invalid review comment.

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

3-8: LGTM!

Clean and minimal domain model. The data class is well-structured with appropriate immutability.

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

9-31: LGTM!

Clean composition pattern that reuses the shared WebImportTopActions component while providing Lucide-specific category filtering. The parameter ordering follows Compose conventions correctly.

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

35-41: Consider checking existing state before triggering load.

The DisposableEffect unconditionally triggers onLoadIcon whenever iconCacheKey changes, even if the icon is already in Success state. This could cause unnecessary network requests if the parent doesn't handle deduplication.

If the ViewModel already handles this deduplication (e.g., by checking cache before fetching), this is fine. Otherwise, consider adding a guard:

DisposableEffect(iconCacheKey) {
    val job = if (iconLoadState !is IconLoadState.Success) {
        currentOnLoadIcon(icon)
    } else null

    onDispose {
        job?.cancel()
    }
}
components/parser/jvm/svg/src/test/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulatorTest.kt (1)

11-250: LGTM!

Comprehensive test coverage including root attribute updates, recursive traversal, conditional updates, nested elements, quote variations, parse failure handling, and a real-world Lucide icon scenario. Well-structured test cases with clear naming.

components/parser/jvm/svg/src/main/kotlin/io/github/composegears/valkyrie/parser/jvm/svg/SvgManipulator.kt (1)

34-39: Good security hardening for XML parsing.

The XXE prevention measures (disabling doctype declarations and external entities) are correctly implemented, protecting against XML External Entity attacks.

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

69-92: Consider error handling for loadConfig.

The loadConfig() function doesn't handle potential exceptions from repository.loadIconList(). If the HTTP request fails, the exception will propagate up. Consider whether this is the intended behavior or if a fallback/error state should be returned.


103-120: Substring matching may cause unexpected categorization.

The contains() check (lines 108, 113) matches substrings, which could lead to unexpected results:

  • "context" would match "text" → categorized as "Text"
  • "unlock" would match "lock" → categorized as "Security"

If this is intentional behavior (and it seems reasonable for icon categorization), this is fine. Otherwise, consider word-boundary matching.

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

69-92: Well-structured initialization with debounced search.

The init block properly handles state restoration and sets up debounced search filtering. The use of Dispatchers.Default for the collect block is appropriate for CPU-bound filtering work.


94-109: LGTM: Clean async loading pattern.

The loadConfig function follows a good pattern with loading state, error handling, and appropriate logging.


313-337: LGTM: Well-implemented filter logic.

The filterGridItems function is a clean pure function with efficient filtering across categories and search terms.


360-378: Clean sealed interface design with @Stable annotations.

The state hierarchy is well-structured. The iconNameIndex provides efficient lookup by icon name for the getLatestSuccessfulState helper.

Comment on lines +21 to +30
val httpClient by instanceOf {
HttpClient(OkHttp) {
install(HttpTimeout) {
requestTimeoutMillis = 30.seconds.inWholeMilliseconds
}
install(ContentNegotiation) {
json(inject(json))
}
}
}
Copy link

@coderabbitai coderabbitai bot Jan 12, 2026

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Add keepAlive = true to httpClient for proper singleton behavior.

The json binding uses keepAlive = true to ensure a single instance, but httpClient doesn't. Creating multiple HttpClient instances wastes resources since OkHttp maintains connection pools internally. This also aligns with the reviewer feedback about sharing a single client across web import features.

Suggested fix
-    val httpClient by instanceOf {
+    val httpClient by instanceOf(keepAlive = true) {
         HttpClient(OkHttp) {
🤖 Prompt for AI Agents
In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/common/di/NetworkModule.kt
around lines 21 - 30, The httpClient binding should be made a singleton like the
json binding by adding keepAlive = true to the instanceOf declaration; update
the val httpClient by instanceOf { ... } for HttpClient(OkHttp) (the
HttpClient/OkHttp initialization block) to include keepAlive = true so a single
shared HttpClient instance is used across web import features.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The httpClient binding doesn't need keepAlive = true because Leviathan's instanceOf already provides singleton behavior by default.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for Leviathan source code or documentation about instanceOf behavior
echo "=== Searching for Leviathan class definition ==="
rg -n "class Leviathan|object Leviathan" --type=kt -A 10

echo -e "\n=== Searching for instanceOf function definition ==="
rg -n "fun.*instanceOf|inline.*instanceOf" --type=kt -A 5

echo -e "\n=== Searching for keepAlive parameter usage ==="
rg -n "keepAlive" --type=kt -C 2

echo -e "\n=== Searching for other instanceOf usages in DI modules ==="
rg -n "by instanceOf" --type=kt -g "*Module.kt" -C 1

Length of output: 320


🏁 Script executed:

#!/bin/bash
# Search for Leviathan source code or documentation about instanceOf behavior
echo "=== Searching for Leviathan class definition ==="
rg -n "class Leviathan|object Leviathan" -g "*.kt" -A 10

echo -e "\n=== Searching for instanceOf function definition ==="
rg -n "fun.*instanceOf|inline.*instanceOf" -g "*.kt" -A 5

echo -e "\n=== Searching for keepAlive parameter usage ==="
rg -n "keepAlive" -g "*.kt" -C 2

echo -e "\n=== Searching for other instanceOf usages in DI modules ==="
rg -n "by instanceOf" -g "*Module.kt" -C 1

Length of output: 4989


@t-regbs, you're absolutely right, and I apologize for the incorrect suggestion. Looking at the codebase patterns, I can see that most instanceOf declarations (like lucideRepository, lucideUseCase, materialSymbolsConfigRepository, etc.) don't use keepAlive = true, which confirms that instanceOf provides singleton behavior by default in Leviathan. The keepAlive = true parameter appears to be used only for specific cases that require explicit keep-alive semantics. Thank you for the clarification!

In Leviathan DI framework, instanceOf provides singleton behavior by default, so keepAlive = true is not needed unless specific keep-alive semantics are required. Most DI bindings in the codebase use instanceOf without the keepAlive parameter.

Comment on lines +42 to +43
val response = httpClient.get("$UNPKG_BASE/tags.json")
val tagsJson = json.parseToJsonElement(response.bodyAsText()) as JsonObject
Copy link

@coderabbitai coderabbitai bot Jan 12, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Unsafe cast may throw on unexpected JSON format.

The cast as JsonObject on line 43 will throw ClassCastException if tags.json has an unexpected format (e.g., array at root level). Consider using a safer approach:

🛡️ Suggested defensive handling
-        val tagsJson = json.parseToJsonElement(response.bodyAsText()) as JsonObject
+        val tagsJson = json.parseToJsonElement(response.bodyAsText()) as? JsonObject
+            ?: throw IllegalStateException("Invalid tags.json format: expected JSON object")
🤖 Prompt for AI Agents
In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt
around lines 42 - 43, The code unsafely casts the parsed JSON to JsonObject
(tagsJson = json.parseToJsonElement(... ) as JsonObject) which can throw; change
to parse to a JsonElement, then verify it is a JsonObject (e.g., val element =
json.parseToJsonElement(response.bodyAsText()); if (element is JsonObject) { val
tagsJson = element } else { handle invalid format: log error via your logger in
LucideRepository, return/throw a descriptive exception, or fallback to empty
object }); use the jsonObject-safe accessor or type check instead of the unsafe
cast to avoid ClassCastException.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The current code is fine. The outer runCatching already provides proper error handling and user feedback.

Copy link

Choose a reason for hiding this comment

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

@t-regbs, understood! You're right that runCatching at the call site will handle any exceptions including ClassCastException.

While safe casting with "as?" helps ensure type compatibility and could provide clearer error messages if the format is unexpected, if you're satisfied with the current error handling approach that's perfectly fine. The outer runCatching will indeed catch and handle the exception appropriately.

Thanks for clarifying your error handling strategy! 🐰

Comment on lines +339 to +344
private inline fun updateSuccess(crossinline transform: (LucideState.Success) -> LucideState.Success) {
val current = lucideRecord.value
if (current is LucideState.Success) {
lucideRecord.value = transform(current)
}
}
Copy link

@coderabbitai coderabbitai bot Jan 12, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Race condition: non-atomic read-modify-write in updateSuccess.

This function is called from multiple coroutines concurrently (debounced search, category selection, settings updates, icon parsing). The read at line 340 and write at line 342 are not atomic—concurrent calls can overwrite each other's changes, causing lost updates.

For example, if two icons finish parsing simultaneously, only one may be added to loadedIcons because both read the same state before writing.

🔒️ Proposed fix: use a Mutex to serialize state updates
+    private val stateMutex = Mutex()
+
-    private inline fun updateSuccess(crossinline transform: (LucideState.Success) -> LucideState.Success) {
-        val current = lucideRecord.value
-        if (current is LucideState.Success) {
-            lucideRecord.value = transform(current)
+    private suspend inline fun updateSuccess(crossinline transform: (LucideState.Success) -> LucideState.Success) {
+        stateMutex.withLock {
+            val current = lucideRecord.value
+            if (current is LucideState.Success) {
+                lucideRecord.value = transform(current)
+            }
         }
     }

Note: This requires updating all call sites to be in a suspend context, which they already are.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private inline fun updateSuccess(crossinline transform: (LucideState.Success) -> LucideState.Success) {
val current = lucideRecord.value
if (current is LucideState.Success) {
lucideRecord.value = transform(current)
}
}
private val stateMutex = Mutex()
private suspend inline fun updateSuccess(crossinline transform: (LucideState.Success) -> LucideState.Success) {
stateMutex.withLock {
val current = lucideRecord.value
if (current is LucideState.Success) {
lucideRecord.value = transform(current)
}
}
}
🤖 Prompt for AI Agents
In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt
around lines 339 - 344, The updateSuccess function performs a non-atomic
read-modify-write on lucideRecord causing lost updates when called from multiple
coroutines; convert it to a suspend function and protect the critical section
with a Mutex (e.g., a private val lucideMutex = Mutex()), then acquire the mutex
in updateSuccess, re-read lucideRecord, apply the transform only while holding
the lock and update lucideRecord before releasing the lock; updateSuccess and
any call sites are already in suspend contexts so switch the signature to
suspend and serialize mutations to LucideState.Success using the Mutex to
prevent races.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is not a race condition in practice. The lucideRecord is backed by Tiamat's MutableSavedState.recordOf, which internally uses a StateFlow - and StateFlow updates are atomic and thread-safe by design.

Adding a Mutex here would unnecessarily serialize all state updates and hurt performance without meaningful benefit.

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

🌐 Web query:

Tiamat Compose navigation MutableSavedState recordOf implementation

💡 Result:

I couldn't find any public implementation or reference for a "recordOf" function on Tiamat's Compose navigation + MutableSavedState. I searched Tiamat docs/articles and Jetpack Compose SavedState docs but didn't locate a recordOf symbol or example — only general saved-state guidance and Tiamat overview. See Tiamat intro and Compose saved-state docs for reference. [1][2]

If you meant something specific, please either:

  • share the code or repo link where recordOf appears, or
  • clarify the exact signature/behavior you expect (e.g., convert MutableSavedState to an immutable record/data class, create a Saver for MutableState, or a navigation-state snapshot API),

and I’ll find or draft an implementation.

Sources:

  1. Tiamat overview/article. [1] (medium.com)
  2. Jetpack Compose saved-state / SavedStateHandle docs. [2] (developer.android.com)

Citations:


🏁 Script executed:

#!/bin/bash
# Search for Tiamat's recordOf implementation in the codebase
rg -n "fun recordOf" --type kotlin -A 10

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for recordOf usage to understand its behavior
rg -n "recordOf" --type kotlin -B 2 -A 5

# Also search for MutableSavedState to understand the API
rg -n "MutableSavedState" --type kotlin -B 2 -A 5 | head -50

Length of output: 22288


🏁 Script executed:

#!/bin/bash
# Check MaterialSymbolsViewModel for similar update patterns
cat tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/material/MaterialSymbolsViewModel.kt | grep -A 10 "fun update\|private.*fun.*update\|materialRecord.value ="

Length of output: 2250


@t-regbs You're absolutely correct that StateFlow's individual write operations are atomic. However, the issue I flagged is about the read-modify-write pattern, not the atomicity of individual writes.

The problem is in this sequence:

private inline fun updateSuccess(crossinline transform: (LucideState.Success) -> LucideState.Success) {
    val current = lucideRecord.value  // ← Read
    if (current is LucideState.Success) {
        lucideRecord.value = transform(current)  // ← Write
    }
}

Even with atomic writes, two concurrent executions can interleave:

  1. Coroutine A reads lucideRecord.value (let's say it has loadedIcons = {icon1: Success})
  2. Coroutine B reads lucideRecord.value (same state: loadedIcons = {icon1: Success})
  3. Coroutine A writes loadedIcons = {icon1: Success, icon2: Success}
  4. Coroutine B writes loadedIcons = {icon1: Success, icon3: Success}icon2 is lost

This is the classic lost update problem in concurrent programming.


That said, I see MaterialSymbolsViewModel uses the identical pattern, and you clearly know this codebase well. If you've tested this thoroughly and haven't seen issues in practice, the race window may be small enough that it doesn't manifest under normal usage patterns (debounced search, sequential user actions, etc.).

I'll defer to your judgment here—if the current approach works reliably, there's no need to add synchronization overhead. Just wanted to clarify the technical distinction between atomic writes and atomic read-modify-write operations. 🐰

Comment on lines +38 to +39
var strokeWidth by remember { mutableFloatStateOf(settings.strokeWidth) }
var size by remember { mutableIntStateOf(settings.size) }
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Local state won't sync if settings prop changes externally.

The local strokeWidth and size states are initialized from settings using remember, which only captures the initial value. If settings is updated externally (e.g., reset triggered from another component or parent recomposition with new values), the UI will display stale local state.

Consider using derivedStateOf or updating local state when the key changes:

Proposed fix using remember with key
-    var strokeWidth by remember { mutableFloatStateOf(settings.strokeWidth) }
-    var size by remember { mutableIntStateOf(settings.size) }
+    var strokeWidth by remember(settings.strokeWidth) { mutableFloatStateOf(settings.strokeWidth) }
+    var size by remember(settings.size) { mutableIntStateOf(settings.size) }

Note: Using settings.strokeWidth / settings.size as keys will reset local state when the external settings change, which is the expected behavior for a controlled component pattern.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var strokeWidth by remember { mutableFloatStateOf(settings.strokeWidth) }
var size by remember { mutableIntStateOf(settings.size) }
var strokeWidth by remember(settings.strokeWidth) { mutableFloatStateOf(settings.strokeWidth) }
var size by remember(settings.size) { mutableIntStateOf(settings.size) }
🤖 Prompt for AI Agents
In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt
around lines 38 - 39, The local vars strokeWidth and size are initialized from
settings using remember so they won't update when settings changes; update the
initialization to recreate or derive values when settings changes (e.g., use
remember(key = settings.strokeWidth) with mutableFloatStateOf for strokeWidth
and remember(key = settings.size) with mutableIntStateOf for size, or use
derivedStateOf tied to settings.strokeWidth/settings.size) so the local state in
LucideCustomization reflects external updates to settings.

Comment on lines +72 to +75
Text(
text = "Stroke width: ${String.format("%.1f", strokeWidth)}",
fontWeight = FontWeight.Medium,
)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use explicit locale for consistent decimal formatting.

String.format("%.1f", strokeWidth) uses the implicit default locale, which may produce a comma as the decimal separator in some locales (e.g., "2,5" instead of "2.5"), potentially causing inconsistent UI display.

Proposed fix
+import java.util.Locale
+
 Text(
-    text = "Stroke width: ${String.format("%.1f", strokeWidth)}",
+    text = "Stroke width: ${String.format(Locale.US, "%.1f", strokeWidth)}",
     fontWeight = FontWeight.Medium,
 )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Text(
text = "Stroke width: ${String.format("%.1f", strokeWidth)}",
fontWeight = FontWeight.Medium,
)
Text(
text = "Stroke width: ${String.format(Locale.US, "%.1f", strokeWidth)}",
fontWeight = FontWeight.Medium,
)
🧰 Tools
🪛 detekt (1.23.8)

[warning] 73-73: String.format("%.1f", strokeWidth) uses implicitly default locale for string formatting.

(detekt.potential-bugs.ImplicitDefaultLocale)

🤖 Prompt for AI Agents
In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/ui/LucideCustomization.kt
around lines 72 - 75, The decimal formatting for the strokeWidth label uses the
platform default locale via String.format("%.1f", strokeWidth) which can produce
commas in some locales; change the call in LucideCustomization.kt (the Text that
shows "Stroke width: ${String.format(...)}") to use an explicit locale (e.g.,
Locale.US) so decimals always render with a dot, i.e., replace
String.format("%.1f", strokeWidth) with a locale-aware variant such as
String.format(Locale.US, "%.1f", strokeWidth) inside the Text composable.

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: 4

🤖 Fix all issues with AI agents
In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt:
- Around line 55-64: In getRawSvg, avoid holding cacheMutex across network I/O:
first acquire cacheMutex and check rawSvgCache[iconName]; if present return it
and release the lock; if absent release the lock, perform the
httpClient.get(...) download from "$UNPKG_BASE/icons/$iconName.svg", then
re-acquire cacheMutex and put the downloaded value into rawSvgCache only if the
entry is still missing (double-check) before returning; reference cacheMutex,
rawSvgCache, getRawSvg, httpClient and UNPKG_BASE to locate the changes.

In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt:
- Around line 337-342: updateSuccess performs a non-atomic read-modify-write on
lucideRecord.value which can lose concurrent updates; change the implementation
to perform an atomic update (e.g., use MutableStateFlow.update { current -> if
(current is LucideState.Success) transform(current) else current } or implement
a compare-and-set loop) so multiple coroutines merging parsed icons won't
overwrite each other; make updateSuccess suspend if you choose an atomic
suspendable approach or convert lucideRecord to a MutableStateFlow and call its
update { } helper and update all call sites accordingly.
🧹 Nitpick comments (5)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt (2)

42-53: Consider safer JSON parsing with explicit error handling.

The cast as JsonObject will throw ClassCastException if the response isn't a JSON object. Consider using a safer approach or catching exceptions to provide a more meaningful error message.

💡 Suggested improvement
 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
+    val tagsJson = json.parseToJsonElement(response.bodyAsText()) as? JsonObject
+        ?: error("Expected JSON object from tags.json")

     tagsJson.entries.map { (iconName, tagsArray) ->

66-74: Float comparison with != may be fragile.

Comparing floats with != can be unreliable due to floating-point precision. If strokeWidth could result from calculations or slider interpolation, consider using an epsilon comparison or comparing the underlying representation.

💡 Suggested improvement
-if (settings.strokeWidth != DEFAULT_STROKE_WIDTH.toFloat()) {
+if (kotlin.math.abs(settings.strokeWidth - DEFAULT_STROKE_WIDTH.toFloat()) > 0.001f) {

Alternatively, if slider values are guaranteed to be exact, this may be acceptable as-is.

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

47-48: Unbounded growth of iconLoadMutexes map.

Mutexes are added via computeIfAbsent in loadIconForDisplay but never removed. With many unique cache keys (icon name + settings combinations), this map can grow indefinitely during a session.

Consider periodically cleaning up unused mutexes or using a bounded cache for mutexes similar to iconVectorCache.

💡 Suggested approach

One option is to clean up the mutex when the job completes and no other operations are pending:

private val iconLoadMutexes = ConcurrentHashMap<String, Mutex>()
private val mutexRefCounts = ConcurrentHashMap<String, Int>() // track usage

// Or simpler: just accept that mutexes are cheap and let them grow,
// but document the expected upper bound based on LruCache size

Alternatively, since the iconVectorCache is bounded to 300 entries, you could tie mutex lifecycle to cache evictions.


154-168: Side effects inside state transform function.

The updateSuccess transform at lines 157-168 mutates external variables (iconsToReload, oldCacheKeys) while also returning the transformed state. This mixing of side effects with state transformation makes the code harder to reason about and could lead to subtle bugs.

♻️ Suggested refactor

Separate the data collection from the state update:

+        val currentSuccessState = lucideRecord.value.safeAs<LucideState.Success>()
+        currentSuccessState?.loadedIcons?.keys?.forEach { cacheKey ->
+            extractIconNameFromCacheKey(cacheKey)?.let { iconName ->
+                currentSuccessState.config.iconsByName[iconName]?.let { icon ->
+                    iconsToReload.add(icon to cacheKey)
+                    oldCacheKeys.add(cacheKey)
+                }
+            }
+        }

         updateSuccess { state ->
-            state.loadedIcons.keys.forEach { cacheKey ->
-                extractIconNameFromCacheKey(cacheKey)?.let { iconName ->
-                    state.config.iconsByName[iconName]?.let { icon ->
-                        iconsToReload.add(icon to cacheKey)
-                        oldCacheKeys.add(cacheKey)
-                    }
-                }
-            }
-
             state.copy(settings = settings)
         }

180-202: Same stale state issue as loadIconForDisplay.

Line 186 captures the current state, but by the time the check on line 187 executes, the state may have changed. This is less critical here since it's just an optimization to avoid redundant work, but for consistency, consider fetching the current state at the point of check.

💡 Minor improvement
 val job = viewModelScope.launch {
-    val currentState = lucideRecord.value.safeAs<LucideState.Success>() ?: return@launch
-    if (currentState.loadedIcons[cacheKey] is IconLoadState.Success) {
+    val loadedIcons = lucideRecord.value.safeAs<LucideState.Success>()?.loadedIcons ?: return@launch
+    if (loadedIcons[cacheKey] is IconLoadState.Success) {
         return@launch
     }
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 093ac0c and b99d1e0.

📒 Files selected for processing (2)
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt
  • tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt
🧰 Additional context used
🧬 Code graph analysis (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt (1)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/domain/LucideUseCase.kt (1)
  • loadConfig (69-92)
🔇 Additional comments (5)
tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt (1)

92-95: Good use of Locale.ROOT for consistent formatting.

The hex conversion correctly uses Locale.ROOT to ensure consistent output regardless of system locale, and properly masks the alpha channel.

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

68-91: LGTM!

The initialization logic correctly:

  • Skips reloading if config is already in Success state (handles process restoration)
  • Sets up debounced search with a reasonable 300ms delay
  • Uses Dispatchers.Default appropriately for CPU-bound filtering work

297-309: LGTM!

The cache key construction using || delimiter is well-documented and the extraction logic correctly handles it. The key includes all relevant settings properties that affect icon rendering.


358-377: Good design for the iconNameIndex lookup.

The iconNameIndex provides an efficient way to retrieve the most recent successful render for an icon by name, independent of settings. This enables showing the last known good state while a re-render with new settings is in progress—a good UX pattern.

The @Stable annotations are appropriate for Compose performance optimization.


47-47: No action needed. LruCache is thread-safe per Android documentation, and all cache operations (get, put, remove) are atomic. The code properly uses per-icon mutexes to coordinate the loading pipeline without requiring additional cache-level synchronization.

Likely an incorrect or invalid review comment.

Comment on lines +55 to +64
suspend fun getRawSvg(iconName: String): String = withContext(Dispatchers.IO) {
cacheMutex.withLock {
rawSvgCache[iconName] ?: run {
val url = "$UNPKG_BASE/icons/$iconName.svg"
val downloaded = httpClient.get(url).bodyAsText()
rawSvgCache.put(iconName, downloaded)
downloaded
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Mutex held during network I/O blocks all concurrent cache access.

The mutex is held for the entire operation including the network download. This means if one coroutine is downloading an SVG, all other coroutines attempting to access the cache (even for already-cached icons) will be blocked. This can significantly degrade performance when loading multiple icons concurrently.

Consider using a double-check locking pattern: check cache, release lock, download, re-acquire lock, and store only if still absent.

🔧 Proposed fix using double-check locking
 suspend fun getRawSvg(iconName: String): String = withContext(Dispatchers.IO) {
-    cacheMutex.withLock {
-        rawSvgCache[iconName] ?: run {
-            val url = "$UNPKG_BASE/icons/$iconName.svg"
-            val downloaded = httpClient.get(url).bodyAsText()
-            rawSvgCache.put(iconName, downloaded)
-            downloaded
-        }
+    // First check: return cached value without blocking download
+    cacheMutex.withLock { rawSvgCache[iconName] }?.let { return@withContext it }
+
+    // Download outside the lock
+    val url = "$UNPKG_BASE/icons/$iconName.svg"
+    val downloaded = httpClient.get(url).bodyAsText()
+
+    // Second check: store only if still absent (another coroutine may have cached it)
+    cacheMutex.withLock {
+        rawSvgCache[iconName] ?: run {
+            rawSvgCache.put(iconName, downloaded)
+            downloaded
+        }
     }
 }
🤖 Prompt for AI Agents
In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/data/LucideRepository.kt
around lines 55 - 64, In getRawSvg, avoid holding cacheMutex across network I/O:
first acquire cacheMutex and check rawSvgCache[iconName]; if present return it
and release the lock; if absent release the lock, perform the
httpClient.get(...) download from "$UNPKG_BASE/icons/$iconName.svg", then
re-acquire cacheMutex and put the downloaded value into rawSvgCache only if the
entry is still missing (double-check) before returning; reference cacheMutex,
rawSvgCache, getRawSvg, httpClient and UNPKG_BASE to locate the changes.

Comment on lines +125 to +128
}.onFailure { error ->
LOG.error("Failed to download icon '${icon.name}'", error)
}
}
Copy link

@coderabbitai coderabbitai bot Jan 12, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Silent failure on icon download provides no user feedback.

When downloadIcon fails, the error is only logged. The user receives no indication that the download failed—they click an icon and nothing happens.

Consider emitting an error event so the UI can display a toast or error state.

💡 Suggested enhancement
 }.onFailure { error ->
     LOG.error("Failed to download icon '${icon.name}'", error)
+    _events.emit(LucideEvent.DownloadError(icon.name, error.message ?: "Unknown error"))
 }

And extend the event type:

sealed interface LucideEvent {
    data class IconDownloaded(...) : LucideEvent
+   data class DownloadError(val iconName: String, val message: String) : LucideEvent
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

will be added later, for now behaviour matches existing MaterialIcons

Copy link

Choose a reason for hiding this comment

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

@t-regbs, understood! It makes sense to maintain consistency with the existing MaterialIcons implementation for now. Feel free to address the user feedback enhancement across both implementations in a future iteration when you're ready to improve the error handling experience for all web import features.

(ᵔᴥᵔ)

Comment on lines +212 to +250
fun loadIconForDisplay(icon: LucideIcon): Job {
val state = lucideRecord.value.safeAs<LucideState.Success>() ?: return Job()
val cacheKey = buildIconCacheKey(icon.name, state.settings)
iconLoadJobs[cacheKey]?.cancel()

val job = viewModelScope.launch {
val mutex = iconLoadMutexes.computeIfAbsent(cacheKey) { Mutex() }

mutex.withLock {
val currentState = state.loadedIcons[cacheKey]
if (currentState is IconLoadState.Success || currentState is IconLoadState.Loading) {
return@launch
}

iconVectorCache.get(cacheKey)?.imageVector?.let { cachedVector ->
val successState = IconLoadState.Success(cachedVector)
updateSuccess {
it.copy(
loadedIcons = it.loadedIcons + (cacheKey to successState),
iconNameIndex = it.iconNameIndex + (icon.name to successState),
)
}
return@launch
}

updateSuccess {
it.copy(loadedIcons = it.loadedIcons + (cacheKey to IconLoadState.Loading))
}
}

runCatching {
val rawSvg = lucideUseCase.getRawSvg(icon.name)
val customizedSvg = lucideUseCase.applyCustomizations(rawSvg, state.settings)

parseAndCacheIcon(icon.name, customizedSvg, cacheKey)
}.onFailure { error ->
handleIconParseError(icon.name, cacheKey, error)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Stale state capture leads to potential race conditions.

The state variable captured on line 213 becomes stale as other coroutines may modify lucideRecord.value. Two issues:

  1. Line 221: The check state.loadedIcons[cacheKey] uses the captured (old) state, not the current state. If another coroutine completed loading between lines 213 and 221, this won't detect it.

  2. Line 244: Uses state.settings which may be outdated if updateSettings was called during the network fetch, potentially applying wrong customizations.

🔧 Proposed fix
 fun loadIconForDisplay(icon: LucideIcon): Job {
-    val state = lucideRecord.value.safeAs<LucideState.Success>() ?: return Job()
-    val cacheKey = buildIconCacheKey(icon.name, state.settings)
+    val initialState = lucideRecord.value.safeAs<LucideState.Success>() ?: return Job()
+    val cacheKey = buildIconCacheKey(icon.name, initialState.settings)
     iconLoadJobs[cacheKey]?.cancel()

     val job = viewModelScope.launch {
         val mutex = iconLoadMutexes.computeIfAbsent(cacheKey) { Mutex() }

         mutex.withLock {
-            val currentState = state.loadedIcons[cacheKey]
+            val currentState = lucideRecord.value.safeAs<LucideState.Success>()
+                ?.loadedIcons?.get(cacheKey)
             if (currentState is IconLoadState.Success || currentState is IconLoadState.Loading) {
                 return@launch
             }
             // ... cache check ...
         }

         runCatching {
+            val latestState = lucideRecord.value.safeAs<LucideState.Success>() ?: return@launch
             val rawSvg = lucideUseCase.getRawSvg(icon.name)
-            val customizedSvg = lucideUseCase.applyCustomizations(rawSvg, state.settings)
+            val customizedSvg = lucideUseCase.applyCustomizations(rawSvg, latestState.settings)

             parseAndCacheIcon(icon.name, customizedSvg, cacheKey)
         }
         // ...
     }

Comment on lines +337 to +342
private inline fun updateSuccess(crossinline transform: (LucideState.Success) -> LucideState.Success) {
val current = lucideRecord.value
if (current is LucideState.Success) {
lucideRecord.value = transform(current)
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Non-atomic read-modify-write in updateSuccess can cause lost updates.

This function reads lucideRecord.value, transforms it, and writes back. When multiple coroutines call this concurrently (e.g., two icons finish parsing at the same time), both read the same state and write their updates—one update gets lost.

Example scenario:

  1. Icon A finishes parsing, reads loadedIcons = {}
  2. Icon B finishes parsing, reads loadedIcons = {} (same state)
  3. Icon A writes loadedIcons = {A: Success}
  4. Icon B writes loadedIcons = {B: Success} → Icon A's update is lost
🔧 Proposed fix using atomic update
-private inline fun updateSuccess(crossinline transform: (LucideState.Success) -> LucideState.Success) {
-    val current = lucideRecord.value
-    if (current is LucideState.Success) {
-        lucideRecord.value = transform(current)
-    }
-}
+private val stateMutex = Mutex()
+
+private suspend inline fun updateSuccess(crossinline transform: (LucideState.Success) -> LucideState.Success) {
+    stateMutex.withLock {
+        val current = lucideRecord.value
+        if (current is LucideState.Success) {
+            lucideRecord.value = transform(current)
+        }
+    }
+}

Note: This requires making updateSuccess a suspend function, which will require updates to call sites. Alternatively, consider using MutableStateFlow.update { } which provides atomic updates if the underlying state holder supports it.

🤖 Prompt for AI Agents
In
@tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/lucide/LucideViewModel.kt
around lines 337 - 342, updateSuccess performs a non-atomic read-modify-write on
lucideRecord.value which can lose concurrent updates; change the implementation
to perform an atomic update (e.g., use MutableStateFlow.update { current -> if
(current is LucideState.Success) transform(current) else current } or implement
a compare-and-set loop) so multiple coroutines merging parsed icons won't
overwrite each other; make updateSuccess suspend if you choose an atomic
suspendable approach or convert lucideRecord to a MutableStateFlow and call its
update { } helper and update all call sites accordingly.

@t-regbs t-regbs marked this pull request as draft January 12, 2026 22:29
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.

[Plugin] Add Import from Lucide Icons Feature

3 participants