@dfosco/storyboard-react 4.2.0-beta.2 → 4.2.0-beta.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/package.json +9 -4
  2. package/src/AuthModal/AuthModal.jsx +6 -2
  3. package/src/BranchBar/BranchBar.jsx +20 -6
  4. package/src/BranchBar/BranchBar.module.css +13 -4
  5. package/src/BranchBar/useBranches.js +20 -6
  6. package/src/BranchBar/useBranches.test.js +68 -0
  7. package/src/CommandPalette/CommandPalette.jsx +478 -186
  8. package/src/CommandPalette/command-palette.css +142 -78
  9. package/src/Icon.jsx +157 -58
  10. package/src/Viewfinder.jsx +561 -191
  11. package/src/Viewfinder.module.css +434 -93
  12. package/src/Workspace.jsx +7 -0
  13. package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
  14. package/src/canvas/CanvasPage.dragdrop.test.jsx +10 -6
  15. package/src/canvas/CanvasPage.jsx +738 -216
  16. package/src/canvas/CanvasPage.module.css +13 -15
  17. package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
  18. package/src/canvas/ConnectorLayer.jsx +121 -153
  19. package/src/canvas/ConnectorLayer.module.css +69 -0
  20. package/src/canvas/PageSelector.test.jsx +15 -6
  21. package/src/canvas/canvasApi.js +68 -2
  22. package/src/canvas/connectorGeometry.js +132 -0
  23. package/src/canvas/hotPoolDevLogs.js +25 -0
  24. package/src/canvas/useCanvas.js +1 -1
  25. package/src/canvas/useMarqueeSelect.js +30 -4
  26. package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
  27. package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
  28. package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
  29. package/src/canvas/widgets/ComponentWidget.jsx +1 -0
  30. package/src/canvas/widgets/CropOverlay.jsx +219 -0
  31. package/src/canvas/widgets/CropOverlay.module.css +118 -0
  32. package/src/canvas/widgets/ExpandedPane.jsx +472 -0
  33. package/src/canvas/widgets/ExpandedPane.module.css +179 -0
  34. package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
  35. package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
  36. package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
  37. package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
  38. package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
  39. package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
  40. package/src/canvas/widgets/ImageWidget.jsx +130 -9
  41. package/src/canvas/widgets/ImageWidget.module.css +30 -0
  42. package/src/canvas/widgets/LinkPreview.jsx +112 -4
  43. package/src/canvas/widgets/LinkPreview.module.css +127 -0
  44. package/src/canvas/widgets/MarkdownBlock.jsx +164 -17
  45. package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
  46. package/src/canvas/widgets/PromptWidget.jsx +414 -0
  47. package/src/canvas/widgets/PromptWidget.module.css +273 -0
  48. package/src/canvas/widgets/PrototypeEmbed.jsx +77 -38
  49. package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
  50. package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
  51. package/src/canvas/widgets/ResizeHandle.jsx +17 -6
  52. package/src/canvas/widgets/StoryWidget.jsx +72 -15
  53. package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
  54. package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
  55. package/src/canvas/widgets/TerminalWidget.jsx +496 -69
  56. package/src/canvas/widgets/TerminalWidget.module.css +271 -8
  57. package/src/canvas/widgets/TilesWidget.jsx +302 -0
  58. package/src/canvas/widgets/TilesWidget.module.css +133 -0
  59. package/src/canvas/widgets/WidgetChrome.jsx +73 -153
  60. package/src/canvas/widgets/WidgetChrome.module.css +30 -1
  61. package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
  62. package/src/canvas/widgets/expandUtils.js +557 -0
  63. package/src/canvas/widgets/expandUtils.test.js +155 -0
  64. package/src/canvas/widgets/index.js +9 -0
  65. package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
  66. package/src/canvas/widgets/tilePool.js +23 -0
  67. package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
  68. package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
  69. package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
  70. package/src/canvas/widgets/tiles/leaf.png +0 -0
  71. package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
  72. package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
  73. package/src/canvas/widgets/tiles/solid-a.png +0 -0
  74. package/src/canvas/widgets/tiles/solid-b.png +0 -0
  75. package/src/canvas/widgets/widgetConfig.js +55 -4
  76. package/src/canvas/widgets/widgetIcons.jsx +190 -0
  77. package/src/canvas/widgets/widgetProps.js +1 -0
  78. package/src/context.jsx +47 -19
  79. package/src/hooks/useConfig.js +14 -0
  80. package/src/hooks/usePrototypeReloadGuard.js +64 -0
  81. package/src/index.js +8 -2
  82. package/src/story/ComponentSetPage.jsx +186 -0
  83. package/src/story/ComponentSetPage.module.css +121 -0
  84. package/src/story/StoryPage.jsx +32 -2
  85. package/src/vite/data-plugin.js +324 -30
@@ -1,116 +1,180 @@
1
1
  /*
2
- * Theme overrides for react-cmdk to match storyboard core-ui styling.
3
- * The z-index is set above the CoreUIBar (9999) so the palette overlays everything.
2
+ * Styles for cmdk command palette.
3
+ * cmdk is headless all visual styling is provided here.
4
+ * Uses [cmdk-*] attribute selectors and data-color-mode for theming.
4
5
  */
5
- .command-palette {
6
- z-index: 10001 !important;
6
+
7
+ /* ─── Overlay ─── */
8
+ [cmdk-overlay] {
9
+ position: fixed;
10
+ inset: 0;
11
+ background: rgba(0, 0, 0, 0.5);
12
+ z-index: 10000;
13
+ animation: cmdk-overlay-in 120ms ease-out;
14
+ }
15
+
16
+ /* ─── Dialog content (Radix Content wrapper) ─── */
17
+ [cmdk-dialog] {
18
+ position: fixed;
19
+ top: 20%;
20
+ left: 50%;
21
+ transform: translateX(-50%);
22
+ width: calc(100% - 2rem);
23
+ max-width: 560px;
24
+ z-index: 10001;
25
+ border-radius: 12px;
26
+ overflow: hidden;
27
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
28
+ background: var(--bgColor-default, #ffffff);
29
+ color: var(--fgColor-default, #1f2328);
30
+ border: 1px solid var(--borderColor-muted, #d1d9e0);
7
31
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
32
+ outline: none;
33
+ animation: cmdk-dialog-in 120ms ease-out;
8
34
  }
9
35
 
10
- /*
11
- * Light mode overrides for react-cmdk.
12
- * react-cmdk uses @media (prefers-color-scheme: dark) with Tailwind dark:
13
- * utilities. When the OS is dark but the app theme is light, those media
14
- * queries fire and the palette renders dark. We override every dark: utility
15
- * back to light values when data-color-mode="light".
16
- */
17
- html[data-color-mode="light"] .command-palette .dark\:bg-gray-900,
18
- html[data-color-mode="light"] .command-palette .dark\:bg-gray-800 {
19
- background-color: rgb(255 255 255) !important;
36
+ @keyframes cmdk-overlay-in {
37
+ from { opacity: 0; }
38
+ to { opacity: 1; }
20
39
  }
21
40
 
22
- html[data-color-mode="light"] .command-palette .dark\:text-white {
23
- color: inherit !important;
41
+ @keyframes cmdk-dialog-in {
42
+ from { opacity: 0; transform: translateX(-50%) scale(0.97); }
43
+ to { opacity: 1; transform: translateX(-50%) scale(1); }
24
44
  }
25
45
 
26
- html[data-color-mode="light"] .command-palette .dark\:text-gray-600 {
27
- color: rgb(107 114 128) !important;
46
+ /* ─── Input ─── */
47
+ [cmdk-input] {
48
+ width: 100%;
49
+ padding: 14px 16px;
50
+ font-size: 15px;
51
+ border: none;
52
+ border-bottom: 1px solid var(--borderColor-muted, #d1d9e0);
53
+ outline: none;
54
+ background: transparent;
55
+ color: var(--fgColor-default, #1f2328);
56
+ font-family: inherit;
28
57
  }
29
58
 
30
- html[data-color-mode="light"] .command-palette .dark\:hover\:bg-gray-800:hover {
31
- background-color: rgb(243 244 246) !important;
59
+ [cmdk-input]::placeholder {
60
+ color: var(--fgColor-muted, #656d76);
32
61
  }
33
62
 
34
- html[data-color-mode="light"] .command-palette .dark\:hover\:text-gray-300:hover {
35
- color: rgb(107 114 128) !important;
63
+ /* ─── List ─── */
64
+ [cmdk-list] {
65
+ max-height: 400px;
66
+ overflow-y: auto;
67
+ padding: 8px;
36
68
  }
37
69
 
38
- html[data-color-mode="light"] .command-palette .dark\:divide-gray-800 > :not([hidden]) ~ :not([hidden]) {
39
- border-color: rgb(229 231 235) !important;
70
+ /* ─── Group ─── */
71
+ [cmdk-group-heading] {
72
+ font-size: 11px;
73
+ font-weight: 600;
74
+ text-transform: uppercase;
75
+ letter-spacing: 0.05em;
76
+ color: var(--fgColor-muted, #656d76);
77
+ padding: 8px 8px 4px;
78
+ user-select: none;
40
79
  }
41
80
 
42
- /*
43
- * Dark mode overrides for react-cmdk.
44
- * react-cmdk uses @media (prefers-color-scheme: dark) internally,
45
- * but we drive dark mode via data-color-mode on <html> (set by themeStore).
46
- * Primer may also mirror it to <body>, so we target both.
47
- */
48
- body[data-color-mode="dark"] .command-palette,
49
- html[data-color-mode="dark"] .command-palette {
50
- color-scheme: dark;
81
+ /* ─── Item ─── */
82
+ [cmdk-item] {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 8px;
86
+ padding: 8px 10px;
87
+ border-radius: 6px;
88
+ font-size: 14px;
89
+ /* font-weight: 500; */
90
+ cursor: pointer;
91
+ user-select: none;
92
+ color: var(--fgColor-default, #1f2328);
93
+ transition: background 0.1s;
94
+ }
95
+
96
+ [cmdk-item][data-selected="true"] {
97
+ background: var(--bgColor-neutral-muted, rgba(175, 184, 193, 0.2));
51
98
  }
52
99
 
53
- body[data-color-mode="dark"] .command-palette .command-palette-content,
54
- html[data-color-mode="dark"] .command-palette .command-palette-content {
55
- color: #e6edf3 !important;
100
+ [cmdk-item]:active {
101
+ background: var(--bgColor-neutral-muted, rgba(175, 184, 193, 0.3));
56
102
  }
57
103
 
58
- /* Panel background */
59
- body[data-color-mode="dark"] .command-palette .bg-white,
60
- html[data-color-mode="dark"] .command-palette .bg-white {
61
- background-color: #2d333b !important;
104
+ /* ─── Separator ─── */
105
+ [cmdk-separator] {
106
+ height: 1px;
107
+ margin: 4px 14px;
108
+ background: var(--borderColor-muted, #d1d9e0);
62
109
  }
63
110
 
64
- /* Hover highlight */
65
- body[data-color-mode="dark"] .command-palette .hover\:bg-gray-100:hover,
66
- html[data-color-mode="dark"] .command-palette .hover\:bg-gray-100:hover {
67
- background-color: #373e47 !important;
111
+ /* ─── Empty state ─── */
112
+ [cmdk-empty] {
113
+ padding: 2rem;
114
+ text-align: center;
115
+ color: var(--fgColor-muted, #656d76);
116
+ font-size: 14px;
68
117
  }
69
118
 
70
- /* Selected/active item highlight */
71
- body[data-color-mode="dark"] .command-palette .bg-gray-200\/50,
72
- html[data-color-mode="dark"] .command-palette .bg-gray-200\/50 {
73
- background-color: rgba(99, 110, 123, 0.4) !important;
119
+ /* ─── Dark mode ─── */
120
+ html[data-color-mode="dark"] [cmdk-dialog],
121
+ body[data-color-mode="dark"] [cmdk-dialog] {
122
+ background: #2d333b;
123
+ color: #e6edf3;
124
+ border-color: #444c56;
74
125
  }
75
126
 
76
- /* Muted text (headings, badges, "Action" labels) */
77
- body[data-color-mode="dark"] .command-palette .text-gray-400,
78
- body[data-color-mode="dark"] .command-palette .text-gray-500,
79
- html[data-color-mode="dark"] .command-palette .text-gray-400,
80
- html[data-color-mode="dark"] .command-palette .text-gray-500 {
81
- color: #768390 !important;
127
+ html[data-color-mode="dark"] [cmdk-input],
128
+ body[data-color-mode="dark"] [cmdk-input] {
129
+ color: #e6edf3;
130
+ border-bottom-color: #444c56;
82
131
  }
83
132
 
84
- /* Input text */
85
- body[data-color-mode="dark"] .command-palette input,
86
- html[data-color-mode="dark"] .command-palette input {
87
- color: #e6edf3 !important;
133
+ html[data-color-mode="dark"] [cmdk-input]::placeholder,
134
+ body[data-color-mode="dark"] [cmdk-input]::placeholder {
135
+ color: #636e7b;
88
136
  }
89
- body[data-color-mode="dark"] .command-palette .placeholder-gray-500::placeholder,
90
- html[data-color-mode="dark"] .command-palette .placeholder-gray-500::placeholder {
91
- color: #636e7b !important;
137
+
138
+ html[data-color-mode="dark"] [cmdk-group-heading],
139
+ body[data-color-mode="dark"] [cmdk-group-heading] {
140
+ color: #768390;
141
+ }
142
+
143
+ html[data-color-mode="dark"] [cmdk-item],
144
+ body[data-color-mode="dark"] [cmdk-item] {
145
+ color: #e6edf3;
92
146
  }
93
- body[data-color-mode="dark"] .command-palette .placeholder-gray-500::-moz-placeholder,
94
- html[data-color-mode="dark"] .command-palette .placeholder-gray-500::-moz-placeholder {
95
- color: #636e7b !important;
147
+
148
+ html[data-color-mode="dark"] [cmdk-item][data-selected="true"],
149
+ body[data-color-mode="dark"] [cmdk-item][data-selected="true"] {
150
+ background: rgba(99, 110, 123, 0.4);
96
151
  }
97
152
 
98
- /* Borders / dividers */
99
- body[data-color-mode="dark"] .command-palette .divide-y > :not([hidden]) ~ :not([hidden]),
100
- body[data-color-mode="dark"] .command-palette .border-t,
101
- body[data-color-mode="dark"] .command-palette .border-b,
102
- html[data-color-mode="dark"] .command-palette .divide-y > :not([hidden]) ~ :not([hidden]),
103
- html[data-color-mode="dark"] .command-palette .border-t,
104
- html[data-color-mode="dark"] .command-palette .border-b {
105
- border-color: #444c56 !important;
153
+ html[data-color-mode="dark"] [cmdk-separator],
154
+ body[data-color-mode="dark"] [cmdk-separator] {
155
+ background: #444c56;
106
156
  }
107
157
 
108
- /* Hide "Action" / "Link" type labels on all list items */
109
- .command-palette .command-palette-list-item .text-gray-500.text-sm {
110
- display: none !important;
158
+ html[data-color-mode="dark"] [cmdk-empty],
159
+ body[data-color-mode="dark"] [cmdk-empty] {
160
+ color: #768390;
111
161
  }
112
162
 
113
- /* Medium weight for item text */
114
- .command-palette .command-palette-list-item {
115
- font-weight: 500 !important;
163
+ html[data-color-mode="dark"] [cmdk-overlay],
164
+ body[data-color-mode="dark"] [cmdk-overlay] {
165
+ background: rgba(0, 0, 0, 0.7);
166
+ }
167
+
168
+ /* ─── Mobile responsiveness ─── */
169
+ @media (max-width: 640px) {
170
+ [cmdk-dialog] {
171
+ max-width: 100%;
172
+ border-radius: 0;
173
+ top: 0;
174
+ width: 100%;
175
+ }
176
+
177
+ [cmdk-list] {
178
+ max-height: 70vh;
179
+ }
116
180
  }
package/src/Icon.jsx CHANGED
@@ -1,20 +1,28 @@
1
1
  /**
2
- * Icon — React port of the Svelte Icon component.
2
+ * Icon — renders icons from multiple sources using namespaced names.
3
3
  *
4
- * Renders icons from multiple sources using namespaced names:
4
+ * primer/ → Primer Octicons (fill-based)
5
+ * feather/ → Feather Icons (stroke-based)
5
6
  * iconoir/ → Iconoir (stroke-based, manually registered)
6
- * (no prefix) → Custom overrides (folder, folder-open, prototype, canvas, component)
7
+ * (no prefix) → Custom (folder, prototype, canvas, component, etc.)
7
8
  *
8
9
  * Usage:
9
- * <Icon name="folder" color="#54aeff" />
10
- * <Icon name="iconoir/key-command" size={16} />
10
+ * <Icon name="primer/repo" />
11
+ * <Icon name="feather/flag" size={16} />
12
+ * <Icon name="iconoir/key-command" size={16} strokeWeight={2} />
11
13
  * <Icon name="prototype" size={14} />
12
- * <Icon name="iconoir/plus-circle" size={20} strokeWeight={2} />
14
+ * <Icon name="feather/tablet" rotate={90} />
15
+ * <Icon name="primer/lock" offsetX={1} offsetY={-1} />
16
+ * <Icon name="feather/arrow-right" flipX />
13
17
  */
14
18
 
15
19
  /* ─── Custom SVG paths (fill-based, no namespace prefix) ─── */
16
20
 
17
21
  const customIcons = {
22
+ 'home': {
23
+ viewBox: '0 0 16 16',
24
+ path: 'M6.906.664a1.749 1.749 0 0 1 2.187 0l5.25 4.2c.415.332.657.835.657 1.367v7.019A1.75 1.75 0 0 1 13.25 15h-3.5a.75.75 0 0 1-.75-.75V9H7v5.25a.75.75 0 0 1-.75.75h-3.5A1.75 1.75 0 0 1 1 13.25V6.23c0-.531.242-1.034.657-1.366l5.25-4.2Zm1.25 1.171a.25.25 0 0 0-.312 0l-5.25 4.2a.25.25 0 0 0-.094.196v7.019c0 .138.112.25.25.25H5.5V8.25a.75.75 0 0 1 .75-.75h3.5a.75.75 0 0 1 .75.75v5.25h2.75a.25.25 0 0 0 .25-.25V6.23a.25.25 0 0 0-.094-.195Z',
25
+ },
18
26
  'folder': {
19
27
  viewBox: '0 0 24 24',
20
28
  path: 'M4 20q-.825 0-1.412-.587T2 18V6q0-.825.588-1.412T4 4h5.175q.4 0 .763.15t.637.425L12 6h8q.825 0 1.413.588T22 8v10q0 .825-.587 1.413T20 20z',
@@ -32,16 +40,6 @@ const customIcons = {
32
40
  'M4 12H20',
33
41
  ],
34
42
  },
35
- // Diamond grid icon (from StoryWidget/ComponentWidget title bar)
36
- 'component': {
37
- viewBox: '0 0 24 24',
38
- strokePaths: [
39
- 'M5.21173 15.1113L2.52473 12.4243C2.29041 12.1899 2.29041 11.8101 2.52473 11.5757L5.21173 8.88873C5.44605 8.65442 5.82595 8.65442 6.06026 8.88873L8.74727 11.5757C8.98158 11.8101 8.98158 12.1899 8.74727 12.4243L6.06026 15.1113C5.82595 15.3456 5.44605 15.3456 5.21173 15.1113Z',
40
- 'M11.5757 21.475L8.88874 18.788C8.65443 18.5537 8.65443 18.1738 8.88874 17.9395L11.5757 15.2525C11.8101 15.0182 12.19 15.0182 12.4243 15.2525L15.1113 17.9395C15.3456 18.1738 15.3456 18.5537 15.1113 18.788L12.4243 21.475C12.19 21.7094 11.8101 21.7094 11.5757 21.475Z',
41
- 'M17.9395 15.1113L15.2525 12.4243C15.0182 12.1899 15.0182 11.8101 15.2525 11.5757L17.9395 8.88873C18.1738 8.65442 18.5537 8.65442 18.788 8.88873L21.475 11.5757C21.7094 11.8101 21.7094 12.1899 21.475 12.4243L18.788 15.1113C18.5537 15.3456 18.1738 15.3456 17.9395 15.1113Z',
42
- 'M11.5757 8.74727L8.88874 6.06026C8.65443 5.82595 8.65443 5.44605 8.88874 5.21173L11.5757 2.52473C11.8101 2.29041 12.19 2.29041 12.4243 2.52473L15.1113 5.21173C15.3456 5.44605 15.3456 5.82595 15.1113 6.06026L12.4243 8.74727C12.19 8.98158 11.8101 8.98158 11.5757 8.74727Z',
43
- ],
44
- },
45
43
  // Smiley face icon (from assets/icons/canvas.svg)
46
44
  'canvas': {
47
45
  viewBox: '0 0 28 23',
@@ -52,6 +50,24 @@ const customIcons = {
52
50
  'M17.6103 6.5C18.2731 6.5 18.8104 7.03731 18.8104 7.70012C18.8104 8.36293 18.2731 8.90025 17.6103 8.90025H17.5986C16.9358 8.90025 16.3984 8.36293 16.3984 7.70012C16.3984 7.03731 16.9358 6.5 17.5986 6.5H17.6103Z',
53
51
  ],
54
52
  },
53
+ 'sticky-note': {
54
+ viewBox: '0 0 14 14',
55
+ fillRule: 'evenodd',
56
+ path: 'M3.709.471C4.763.353 5.867.25 7 .25s2.237.104 3.29.22h.003a3.694 3.694 0 0 1 3.247 3.256v.004c.113 1.049.21 2.145.21 3.27q-.001.595-.032 1.176l-.008.067c-.556 3.437-3.804 5.226-6.192 5.498h-.006l-.048.006a.6.6 0 0 1-.126 0q-.168.003-.338.003c-1.133 0-2.236-.103-3.29-.221h-.003A3.695 3.695 0 0 1 .46 10.272v-.004C.346 9.221.25 8.126.25 7s.097-2.222.21-3.27v-.003A3.694 3.694 0 0 1 3.707.472zm8.784 7.047h-2.37c-.854 0-1.514.656-1.628 1.442c-.22 1.52-.706 2.546-1.413 3.54H7c-1.061 0-2.108-.097-3.15-.213a2.445 2.445 0 0 1-2.148-2.153C1.592 9.1 1.5 8.057 1.5 7s.091-2.1.202-3.134A2.444 2.444 0 0 1 3.85 1.714C4.89 1.597 5.938 1.5 7 1.5s2.107.098 3.151.213a2.444 2.444 0 0 1 2.147 2.152C12.408 4.9 12.5 5.944 12.5 7q0 .259-.007.518',
57
+ },
58
+ 'agents': {
59
+ viewBox: '0 0 32 32',
60
+ path: 'M27.2 16c0-6.19-5.01-11.2-11.2-11.2S4.8 9.81 4.8 16S9.81 27.2 16 27.2S27.2 22.19 27.2 16m-5.6 2.1a1.4 1.4 0 0 1 0 2.8h-4.2a1.4 1.4 0 0 1 0-2.8zm-11.2-6.8a1.397 1.397 0 0 1 1.84.361l.08.119l2.1 3.5l.087.171a1.4 1.4 0 0 1 0 1.1l-.088.171l-2.1 3.5a1.4 1.4 0 0 1-2.4-1.44l1.67-2.78l-1.67-2.78l-.067-.127a1.394 1.394 0 0 1 .547-1.79zM30 16c0 7.73-6.27 14-14 14S2 23.73 2 16S8.27 2 16 2s14 6.27 14 14',
61
+ },
62
+ 'codex': {
63
+ viewBox: '0 0 24 24',
64
+ fillRule: 'evenodd',
65
+ path: 'M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z',
66
+ },
67
+ 'claude': {
68
+ viewBox: '0 0 24 24',
69
+ path: 'm4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z',
70
+ },
55
71
  'flow': {
56
72
  viewBox: '0 0 24 24',
57
73
  strokeWidth: '2.5',
@@ -95,86 +111,169 @@ const iconoirIcons = {
95
111
  strokeWidth: '1.5',
96
112
  content: '<path d="M14 20.4V14.6C14 14.2686 14.2686 14 14.6 14H20.4C20.7314 14 21 14.2686 21 14.6V20.4C21 20.7314 20.7314 21 20.4 21H14.6C14.2686 21 14 20.7314 14 20.4Z" stroke="currentColor" stroke-width="1.5"/><path d="M3 20.4V14.6C3 14.2686 3.26863 14 3.6 14H9.4C9.73137 14 10 14.2686 10 14.6V20.4C10 20.7314 9.73137 21 9.4 21H3.6C3.26863 21 3 20.7314 3 20.4Z" stroke="currentColor" stroke-width="1.5"/><path d="M14 9.4V3.6C14 3.26863 14.2686 3 14.6 3H20.4C20.7314 3 21 3.26863 21 3.6V9.4C21 9.73137 20.7314 10 20.4 10H14.6C14.2686 10 14 9.73137 14 9.4Z" stroke="currentColor" stroke-width="1.5"/><path d="M3 9.4V3.6C3 3.26863 3.26863 3 3.6 3H9.4C9.73137 3 10 3.26863 10 3.6V9.4C10 9.73137 9.73137 10 9.4 10H3.6C3.26863 10 3 9.73137 3 9.4Z" stroke="currentColor" stroke-width="1.5"/>',
97
113
  },
114
+ 'select-point-3d': {
115
+ viewBox: '0 0 24 24',
116
+ strokeWidth: '1.5',
117
+ content: '<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 7.35304L21 16.647C21 16.8649 20.8819 17.0656 20.6914 17.1715L12.2914 21.8381C12.1102 21.9388 11.8898 21.9388 11.7086 21.8381L3.30861 17.1715C3.11814 17.0656 3 16.8649 3 16.647L2.99998 7.35304C2.99998 7.13514 3.11812 6.93437 3.3086 6.82855L11.7086 2.16188C11.8898 2.06121 12.1102 2.06121 12.2914 2.16188L20.6914 6.82855C20.8818 6.93437 21 7.13514 21 7.35304Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>',
118
+ },
98
119
  'square-3d-three-points': {
99
120
  viewBox: '0 0 24 24',
100
121
  strokeWidth: '1.5',
101
122
  content: '<path d="M3 21V3.6C3 3.26863 3.26863 3 3.6 3H21" stroke="currentColor"/><path d="M17 21H20.4C20.7314 21 21 20.7314 21 20.4V17" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 7V9" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 12V14" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M7 21H9" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 21H14" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 4C3.55228 4 4 3.55228 4 3C4 2.44772 3.55228 2 3 2C2.44772 2 2 2.44772 2 3C2 3.55228 2.44772 4 3 4Z" fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 22C3.55228 22 4 21.5523 4 21C4 20.4477 3.55228 20 3 20C2.44772 20 2 20.4477 2 21C2 21.5523 2.44772 22 3 22Z" fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 4C21.5523 4 22 3.55228 22 3C22 2.44772 21.5523 2 21 2C20.4477 2 20 2.44772 20 3C20 3.55228 20.4477 4 21 4Z" fill="currentColor" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>',
102
123
  },
124
+ 'white-flag': {
125
+ viewBox: '0 0 24 24',
126
+ strokeWidth: '1.5',
127
+ content: '<path d="M5 15L5.95039 4.54568C5.97849 4.23663 6.23761 4 6.54793 4H20.343C20.6958 4 20.9725 4.30295 20.9405 4.65432L20.0496 14.4543C20.0215 14.7634 19.7624 15 19.4521 15H5ZM5 15L4.4 21" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>',
128
+ },
129
+ 'light-bulb': {
130
+ viewBox: '0 0 24 24',
131
+ strokeWidth: '1.5',
132
+ content: '<path d="M9 18H15" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 21H14" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M9.00082 15C9.00098 13 8.50098 12.5 7.50082 11.5C6.50067 10.5 6.02422 9.48689 6.00082 8C5.95284 4.95029 8.00067 3 12.0008 3C16.001 3 18.0488 4.95029 18.0008 8C17.9774 9.48689 17.5007 10.5 16.5008 11.5C15.501 12.5 15.001 13 15.0008 15" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>',
133
+ },
134
+ 'light-bulb-off': {
135
+ viewBox: '0 0 24 24',
136
+ strokeWidth: '1.5',
137
+ content: '<path d="M9 18H15" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M10 21H14" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M16.4999 11.5C17.4997 10.5 17.9765 9.48689 17.9999 8C18.0479 4.95029 16 3 11.9999 3C10.8324 3 9.83119 3.16613 8.99988 3.47724" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M8.99985 15C9 13 8.5 12.5 7.49985 11.5C6.4997 10.5 6.02324 9.48689 5.99985 8C5.99142 7.46458 6.0476 6.96304 6.1676 6.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M3 3L21 21" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>',
138
+ },
139
+ 'keyframes': {
140
+ viewBox: '0 0 24 24',
141
+ strokeWidth: '1.5',
142
+ content: '<path d="M13.8476 13.317L9.50515 18.2798C8.70833 19.1905 7.29167 19.1905 6.49485 18.2798L2.15238 13.317C1.49259 12.563 1.49259 11.437 2.15238 10.683L6.49485 5.72018C7.29167 4.80952 8.70833 4.80952 9.50515 5.72017L13.8476 10.683C14.5074 11.437 14.5074 12.563 13.8476 13.317Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M13 19L17.8844 13.3016C18.5263 12.5526 18.5263 11.4474 17.8844 10.6984L13 5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M17 19L21.8844 13.3016C22.5263 12.5526 22.5263 11.4474 21.8844 10.6984L17 5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>',
143
+ },
144
+ 'keyframes-couple-solid': {
145
+ viewBox: '0 0 24 24',
146
+ fill: true,
147
+ content: '<path fill-rule="evenodd" clip-rule="evenodd" d="M14.0658 5.18029L12.5606 6.87362L11.4395 5.87707L12.9447 4.18374C14.0386 2.95308 15.9614 2.95308 17.0554 4.18373L22.3794 10.1733C23.3056 11.2152 23.3057 12.7855 22.3795 13.8273L17.0554 19.8169C15.9614 21.0476 14.0386 21.0477 12.9447 19.8169L11.4395 18.1236L12.5606 17.1271L14.0658 18.8204C14.563 19.3798 15.437 19.3798 15.9342 18.8204L21.2583 12.8308C21.6793 12.3572 21.6793 11.6435 21.2584 11.17L15.9343 5.18029C15.437 4.6209 14.563 4.6209 14.0658 5.18029" fill="currentColor"/><path d="M6.94474 4.18374C8.03866 2.95307 9.96152 2.95308 11.0555 4.18374L16.3795 10.1733C17.3057 11.2152 17.3058 12.7855 16.3796 13.8273L11.0555 19.8169C9.96155 21.0476 8.03866 21.0477 6.94474 19.8169L1.62067 13.8273C0.694507 12.7855 0.694485 11.2152 1.62064 10.1734L6.94474 4.18374Z" fill="currentColor"/>',
148
+ },
149
+ 'keyframe': {
150
+ viewBox: '0 0 24 24',
151
+ strokeWidth: '1.5',
152
+ content: '<path d="M20.777 13.3453L13.4799 21.3721C12.6864 22.245 11.3136 22.245 10.5201 21.3721L3.22304 13.3453C2.52955 12.5825 2.52955 11.4175 3.22304 10.6547L10.5201 2.62787C11.3136 1.755 12.6864 1.755 13.4799 2.62787L20.777 10.6547C21.4705 11.4175 21.4705 12.5825 20.777 13.3453Z" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>',
153
+ },
154
+ 'wrench': {
155
+ viewBox: '0 0 24 24',
156
+ strokeWidth: '1.5',
157
+ content: '<path d="M10.0503 10.6066L2.97923 17.6777C2.19818 18.4587 2.19818 19.725 2.97923 20.5061V20.5061C3.76027 21.2871 5.0266 21.2871 5.80765 20.5061L12.8787 13.435" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M10.0502 10.6066C9.20638 8.45358 9.37134 5.6286 11.1109 3.88909C12.8504 2.14957 16.0606 1.76777 17.8284 2.82843L14.7877 5.8691L14.5051 8.98014L17.6161 8.69753L20.6568 5.65685C21.7175 7.42462 21.3357 10.6349 19.5961 12.3744C17.8566 14.1139 15.0316 14.2789 12.8786 13.435" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/>',
158
+ },
103
159
  }
104
160
 
105
161
  /* ─── React Component ─── */
106
162
 
163
+ import octicons from '@primer/octicons'
164
+ import feather from 'feather-icons'
165
+
107
166
  /**
108
167
  * @param {object} props
109
- * @param {string} props.name - Icon name. Use "iconoir/{name}" for iconoir, plain name for custom.
168
+ * @param {string} props.name - Namespaced icon name: primer/, feather/, iconoir/, or plain custom name
110
169
  * @param {number} [props.size=16]
111
170
  * @param {string} [props.label] - Accessible label (sets aria-label instead of aria-hidden)
112
171
  * @param {string} [props.color]
172
+ * @param {number} [props.offsetX=0]
173
+ * @param {number} [props.offsetY=0]
174
+ * @param {number} [props.rotate=0]
175
+ * @param {boolean} [props.flipX=false]
176
+ * @param {boolean} [props.flipY=false]
113
177
  * @param {number} [props.strokeWeight] - Override stroke width
178
+ * @param {number} [props.scale=1]
114
179
  * @param {string} [props.className]
115
180
  */
116
- export default function Icon({ name, size = 16, label, color, strokeWeight, className }) {
181
+ export default function Icon({
182
+ name, size = 16, label, color,
183
+ offsetX = 0, offsetY = 0, rotate = 0,
184
+ flipX = false, flipY = false,
185
+ strokeWeight, scale = 1, className,
186
+ }) {
117
187
  const source = name.includes('/') ? name.split('/')[0] : null
118
188
  const iconName = name.includes('/') ? name.slice(name.indexOf('/') + 1) : name
119
189
 
120
190
  const ariaProps = label ? { 'aria-label': label, role: 'img' } : { 'aria-hidden': true }
121
- const style = color ? { color } : undefined
122
191
 
123
- // Custom icons
192
+ // Build wrapper style with all transform props
193
+ const scaleX = (flipX ? -1 : 1) * scale
194
+ const scaleY = (flipY ? -1 : 1) * scale
195
+ const hasTransform = offsetX || offsetY || rotate || flipX || flipY || scale !== 1
196
+ const wrapperStyle = {
197
+ ...(color ? { color } : {}),
198
+ display: 'inline-flex',
199
+ ...(hasTransform ? {
200
+ translate: (offsetX || offsetY) ? `${offsetX}px ${offsetY}px` : undefined,
201
+ rotate: rotate ? `${rotate}deg` : undefined,
202
+ scale: (flipX || flipY || scale !== 1) ? `${scaleX} ${scaleY}` : undefined,
203
+ } : {}),
204
+ }
205
+
206
+ let svgContent = null
207
+
208
+ // Custom icons (no source prefix)
124
209
  const custom = !source ? customIcons[iconName] : null
125
210
  if (custom) {
126
- // Simple fill-based path icon
127
211
  if (custom.path) {
128
- return (
129
- <span className={className} style={style}>
130
- <svg width={size} height={size} viewBox={custom.viewBox} fill="currentColor" {...ariaProps}>
131
- <path d={custom.path} />
132
- </svg>
133
- </span>
212
+ svgContent = (
213
+ <svg width={size} height={size} viewBox={custom.viewBox} fill="currentColor" {...ariaProps}>
214
+ <path d={custom.path} fillRule={custom.fillRule} clipRule={custom.fillRule} />
215
+ </svg>
134
216
  )
135
- }
136
- // Stroke-based icon (prototype, component)
137
- if (custom.strokePaths) {
138
- return (
139
- <span className={className} style={style}>
140
- <svg width={size} height={size} viewBox={custom.viewBox} fill="none" stroke="currentColor" strokeWidth={custom.strokeWidth || "1.5"} strokeLinecap="round" strokeLinejoin="round" {...ariaProps}>
141
- {custom.strokePaths.map((d, i) => <path key={i} d={d} />)}
142
- </svg>
143
- </span>
217
+ } else if (custom.strokePaths) {
218
+ svgContent = (
219
+ <svg width={size} height={size} viewBox={custom.viewBox} fill="none" stroke="currentColor" strokeWidth={strokeWeight ?? custom.strokeWidth ?? '1.5'} strokeLinecap="round" strokeLinejoin="round" {...ariaProps}>
220
+ {custom.strokePaths.map((d, i) => <path key={i} d={d} />)}
221
+ </svg>
144
222
  )
145
- }
146
- // Mixed icon (canvas — stroke rect + fill paths)
147
- if (custom.strokeRect || custom.fillPaths) {
148
- return (
149
- <span className={className} style={style}>
150
- <svg width={size} height={size} viewBox={custom.viewBox} fill="none" stroke="currentColor" strokeWidth="2" {...ariaProps}>
151
- {custom.strokeRect && <rect {...custom.strokeRect} />}
152
- {custom.fillPaths?.map((d, i) => <path key={i} d={d} fill="currentColor" stroke="none" />)}
153
- </svg>
154
- </span>
223
+ } else if (custom.strokeRect || custom.fillPaths) {
224
+ svgContent = (
225
+ <svg width={size} height={size} viewBox={custom.viewBox} fill="none" stroke="currentColor" strokeWidth={strokeWeight ?? '2'} {...ariaProps}>
226
+ {custom.strokeRect && <rect {...custom.strokeRect} />}
227
+ {custom.fillPaths?.map((d, i) => <path key={i} d={d} fill="currentColor" stroke="none" />)}
228
+ </svg>
155
229
  )
156
230
  }
157
231
  }
158
232
 
233
+ // Primer Octicons
234
+ if (!svgContent && source === 'primer') {
235
+ const octicon = octicons[iconName]
236
+ if (octicon) {
237
+ const html = octicon.toSVG({
238
+ width: size, height: size,
239
+ fill: 'currentColor',
240
+ ...(label ? { 'aria-label': label } : { 'aria-hidden': 'true' }),
241
+ })
242
+ svgContent = <span dangerouslySetInnerHTML={{ __html: html }} />
243
+ }
244
+ }
245
+
246
+ // Feather Icons
247
+ if (!svgContent && source === 'feather') {
248
+ const icon = feather.icons[iconName]
249
+ if (icon) {
250
+ const html = icon.toSvg({
251
+ width: size, height: size,
252
+ 'stroke-width': strokeWeight ?? 2,
253
+ ...(label ? { 'aria-label': label } : { 'aria-hidden': 'true' }),
254
+ })
255
+ svgContent = <span dangerouslySetInnerHTML={{ __html: html }} />
256
+ }
257
+ }
258
+
159
259
  // Iconoir icons
160
- const iconoir = source === 'iconoir' ? iconoirIcons[iconName] : null
161
- if (iconoir) {
162
- const sw = strokeWeight ?? iconoir.strokeWidth
163
- return (
164
- <span className={className} style={style}>
260
+ if (!svgContent && source === 'iconoir') {
261
+ const iconoir = iconoirIcons[iconName]
262
+ if (iconoir) {
263
+ const sw = strokeWeight ?? iconoir.strokeWidth
264
+ svgContent = (
165
265
  <svg
166
- width={size}
167
- height={size}
168
- viewBox={iconoir.viewBox}
266
+ width={size} height={size} viewBox={iconoir.viewBox}
169
267
  fill={iconoir.fill ? 'currentColor' : 'none'}
170
268
  strokeWidth={iconoir.fill ? undefined : sw}
171
269
  {...ariaProps}
172
270
  dangerouslySetInnerHTML={{ __html: iconoir.content }}
173
271
  />
174
- </span>
175
- )
272
+ )
273
+ }
176
274
  }
177
275
 
178
- // Unknown icon
179
- return null
276
+ if (!svgContent) return null
277
+
278
+ return <span className={className} style={wrapperStyle}>{svgContent}</span>
180
279
  }