@dfosco/storyboard-react 4.2.0-beta.1 → 4.2.0-beta.17

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 (48) hide show
  1. package/package.json +5 -4
  2. package/src/AuthModal/AuthModal.jsx +6 -2
  3. package/src/BranchBar/BranchBar.jsx +17 -5
  4. package/src/BranchBar/BranchBar.module.css +11 -2
  5. package/src/CommandPalette/CommandPalette.jsx +267 -164
  6. package/src/CommandPalette/command-palette.css +130 -78
  7. package/src/Icon.jsx +112 -48
  8. package/src/Viewfinder.jsx +511 -61
  9. package/src/Viewfinder.module.css +414 -2
  10. package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
  11. package/src/canvas/CanvasPage.dragdrop.test.jsx +10 -6
  12. package/src/canvas/CanvasPage.jsx +157 -174
  13. package/src/canvas/CanvasPage.module.css +0 -15
  14. package/src/canvas/CanvasPage.multiselect.test.jsx +10 -6
  15. package/src/canvas/ConnectorLayer.jsx +5 -5
  16. package/src/canvas/PageSelector.test.jsx +15 -6
  17. package/src/canvas/useCanvas.js +1 -1
  18. package/src/canvas/widgets/ActionWidget.jsx +200 -0
  19. package/src/canvas/widgets/ActionWidget.module.css +122 -0
  20. package/src/canvas/widgets/FigmaEmbed.jsx +97 -29
  21. package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
  22. package/src/canvas/widgets/ImageWidget.jsx +1 -1
  23. package/src/canvas/widgets/LinkPreview.jsx +64 -5
  24. package/src/canvas/widgets/LinkPreview.module.css +127 -0
  25. package/src/canvas/widgets/MarkdownBlock.jsx +39 -17
  26. package/src/canvas/widgets/MarkdownBlock.module.css +123 -0
  27. package/src/canvas/widgets/PrototypeEmbed.jsx +183 -20
  28. package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
  29. package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
  30. package/src/canvas/widgets/SplitExpandModal.jsx +234 -0
  31. package/src/canvas/widgets/SplitExpandModal.module.css +335 -0
  32. package/src/canvas/widgets/SplitScreenTopBar.jsx +30 -0
  33. package/src/canvas/widgets/SplitScreenTopBar.module.css +58 -0
  34. package/src/canvas/widgets/StoryWidget.jsx +7 -4
  35. package/src/canvas/widgets/TerminalReadWidget.jsx +140 -0
  36. package/src/canvas/widgets/TerminalReadWidget.module.css +92 -0
  37. package/src/canvas/widgets/TerminalWidget.jsx +299 -49
  38. package/src/canvas/widgets/TerminalWidget.module.css +155 -1
  39. package/src/canvas/widgets/WidgetChrome.jsx +19 -14
  40. package/src/canvas/widgets/WidgetChrome.module.css +10 -0
  41. package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
  42. package/src/canvas/widgets/expandUtils.js +188 -0
  43. package/src/canvas/widgets/index.js +5 -0
  44. package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
  45. package/src/canvas/widgets/widgetConfig.js +19 -1
  46. package/src/hooks/useConfig.js +14 -0
  47. package/src/index.js +4 -0
  48. package/src/vite/data-plugin.js +264 -14
@@ -1,116 +1,168 @@
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
+ }
14
+
15
+ /* ─── Dialog content (Radix Content wrapper) ─── */
16
+ [cmdk-dialog] {
17
+ position: fixed;
18
+ top: 20%;
19
+ left: 50%;
20
+ transform: translateX(-50%);
21
+ width: calc(100% - 2rem);
22
+ max-width: 560px;
23
+ z-index: 10001;
24
+ border-radius: 12px;
25
+ overflow: hidden;
26
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
27
+ background: var(--bgColor-default, #ffffff);
28
+ color: var(--fgColor-default, #1f2328);
29
+ border: 1px solid var(--borderColor-muted, #d1d9e0);
7
30
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
31
+ outline: none;
8
32
  }
9
33
 
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;
34
+ /* ─── Input ─── */
35
+ [cmdk-input] {
36
+ width: 100%;
37
+ padding: 14px 16px;
38
+ font-size: 15px;
39
+ border: none;
40
+ border-bottom: 1px solid var(--borderColor-muted, #d1d9e0);
41
+ outline: none;
42
+ background: transparent;
43
+ color: var(--fgColor-default, #1f2328);
44
+ font-family: inherit;
20
45
  }
21
46
 
22
- html[data-color-mode="light"] .command-palette .dark\:text-white {
23
- color: inherit !important;
47
+ [cmdk-input]::placeholder {
48
+ color: var(--fgColor-muted, #656d76);
24
49
  }
25
50
 
26
- html[data-color-mode="light"] .command-palette .dark\:text-gray-600 {
27
- color: rgb(107 114 128) !important;
51
+ /* ─── List ─── */
52
+ [cmdk-list] {
53
+ max-height: 400px;
54
+ overflow-y: auto;
55
+ padding: 8px;
28
56
  }
29
57
 
30
- html[data-color-mode="light"] .command-palette .dark\:hover\:bg-gray-800:hover {
31
- background-color: rgb(243 244 246) !important;
58
+ /* ─── Group ─── */
59
+ [cmdk-group-heading] {
60
+ font-size: 11px;
61
+ font-weight: 600;
62
+ text-transform: uppercase;
63
+ letter-spacing: 0.05em;
64
+ color: var(--fgColor-muted, #656d76);
65
+ padding: 8px 8px 4px;
66
+ user-select: none;
32
67
  }
33
68
 
34
- html[data-color-mode="light"] .command-palette .dark\:hover\:text-gray-300:hover {
35
- color: rgb(107 114 128) !important;
69
+ /* ─── Item ─── */
70
+ [cmdk-item] {
71
+ display: flex;
72
+ align-items: center;
73
+ gap: 8px;
74
+ padding: 8px 10px;
75
+ border-radius: 6px;
76
+ font-size: 14px;
77
+ /* font-weight: 500; */
78
+ cursor: pointer;
79
+ user-select: none;
80
+ color: var(--fgColor-default, #1f2328);
81
+ transition: background 0.1s;
36
82
  }
37
83
 
38
- html[data-color-mode="light"] .command-palette .dark\:divide-gray-800 > :not([hidden]) ~ :not([hidden]) {
39
- border-color: rgb(229 231 235) !important;
84
+ [cmdk-item][data-selected="true"] {
85
+ background: var(--bgColor-neutral-muted, rgba(175, 184, 193, 0.2));
40
86
  }
41
87
 
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;
88
+ [cmdk-item]:active {
89
+ background: var(--bgColor-neutral-muted, rgba(175, 184, 193, 0.3));
51
90
  }
52
91
 
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;
92
+ /* ─── Separator ─── */
93
+ [cmdk-separator] {
94
+ height: 1px;
95
+ margin: 4px 14px;
96
+ background: var(--borderColor-muted, #d1d9e0);
56
97
  }
57
98
 
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;
99
+ /* ─── Empty state ─── */
100
+ [cmdk-empty] {
101
+ padding: 2rem;
102
+ text-align: center;
103
+ color: var(--fgColor-muted, #656d76);
104
+ font-size: 14px;
62
105
  }
63
106
 
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;
107
+ /* ─── Dark mode ─── */
108
+ html[data-color-mode="dark"] [cmdk-dialog],
109
+ body[data-color-mode="dark"] [cmdk-dialog] {
110
+ background: #2d333b;
111
+ color: #e6edf3;
112
+ border-color: #444c56;
68
113
  }
69
114
 
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;
115
+ html[data-color-mode="dark"] [cmdk-input],
116
+ body[data-color-mode="dark"] [cmdk-input] {
117
+ color: #e6edf3;
118
+ border-bottom-color: #444c56;
74
119
  }
75
120
 
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;
121
+ html[data-color-mode="dark"] [cmdk-input]::placeholder,
122
+ body[data-color-mode="dark"] [cmdk-input]::placeholder {
123
+ color: #636e7b;
82
124
  }
83
125
 
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;
126
+ html[data-color-mode="dark"] [cmdk-group-heading],
127
+ body[data-color-mode="dark"] [cmdk-group-heading] {
128
+ color: #768390;
88
129
  }
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;
130
+
131
+ html[data-color-mode="dark"] [cmdk-item],
132
+ body[data-color-mode="dark"] [cmdk-item] {
133
+ color: #e6edf3;
92
134
  }
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;
135
+
136
+ html[data-color-mode="dark"] [cmdk-item][data-selected="true"],
137
+ body[data-color-mode="dark"] [cmdk-item][data-selected="true"] {
138
+ background: rgba(99, 110, 123, 0.4);
96
139
  }
97
140
 
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;
141
+ html[data-color-mode="dark"] [cmdk-separator],
142
+ body[data-color-mode="dark"] [cmdk-separator] {
143
+ background: #444c56;
106
144
  }
107
145
 
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;
146
+ html[data-color-mode="dark"] [cmdk-empty],
147
+ body[data-color-mode="dark"] [cmdk-empty] {
148
+ color: #768390;
111
149
  }
112
150
 
113
- /* Medium weight for item text */
114
- .command-palette .command-palette-list-item {
115
- font-weight: 500 !important;
151
+ html[data-color-mode="dark"] [cmdk-overlay],
152
+ body[data-color-mode="dark"] [cmdk-overlay] {
153
+ background: rgba(0, 0, 0, 0.7);
154
+ }
155
+
156
+ /* ─── Mobile responsiveness ─── */
157
+ @media (max-width: 640px) {
158
+ [cmdk-dialog] {
159
+ max-width: 100%;
160
+ border-radius: 0;
161
+ top: 0;
162
+ width: 100%;
163
+ }
164
+
165
+ [cmdk-list] {
166
+ max-height: 70vh;
167
+ }
116
168
  }
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',
@@ -52,6 +60,14 @@ const customIcons = {
52
60
  '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
61
  ],
54
62
  },
63
+ 'agents': {
64
+ viewBox: '0 0 32 32',
65
+ 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',
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,6 +111,11 @@ 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',
@@ -104,77 +125,120 @@ const iconoirIcons = {
104
125
 
105
126
  /* ─── React Component ─── */
106
127
 
128
+ import octicons from '@primer/octicons'
129
+ import feather from 'feather-icons'
130
+
107
131
  /**
108
132
  * @param {object} props
109
- * @param {string} props.name - Icon name. Use "iconoir/{name}" for iconoir, plain name for custom.
133
+ * @param {string} props.name - Namespaced icon name: primer/, feather/, iconoir/, or plain custom name
110
134
  * @param {number} [props.size=16]
111
135
  * @param {string} [props.label] - Accessible label (sets aria-label instead of aria-hidden)
112
136
  * @param {string} [props.color]
137
+ * @param {number} [props.offsetX=0]
138
+ * @param {number} [props.offsetY=0]
139
+ * @param {number} [props.rotate=0]
140
+ * @param {boolean} [props.flipX=false]
141
+ * @param {boolean} [props.flipY=false]
113
142
  * @param {number} [props.strokeWeight] - Override stroke width
143
+ * @param {number} [props.scale=1]
114
144
  * @param {string} [props.className]
115
145
  */
116
- export default function Icon({ name, size = 16, label, color, strokeWeight, className }) {
146
+ export default function Icon({
147
+ name, size = 16, label, color,
148
+ offsetX = 0, offsetY = 0, rotate = 0,
149
+ flipX = false, flipY = false,
150
+ strokeWeight, scale = 1, className,
151
+ }) {
117
152
  const source = name.includes('/') ? name.split('/')[0] : null
118
153
  const iconName = name.includes('/') ? name.slice(name.indexOf('/') + 1) : name
119
154
 
120
155
  const ariaProps = label ? { 'aria-label': label, role: 'img' } : { 'aria-hidden': true }
121
- const style = color ? { color } : undefined
122
156
 
123
- // Custom icons
157
+ // Build wrapper style with all transform props
158
+ const scaleX = (flipX ? -1 : 1) * scale
159
+ const scaleY = (flipY ? -1 : 1) * scale
160
+ const hasTransform = offsetX || offsetY || rotate || flipX || flipY || scale !== 1
161
+ const wrapperStyle = {
162
+ ...(color ? { color } : {}),
163
+ display: 'inline-flex',
164
+ ...(hasTransform ? {
165
+ translate: (offsetX || offsetY) ? `${offsetX}px ${offsetY}px` : undefined,
166
+ rotate: rotate ? `${rotate}deg` : undefined,
167
+ scale: (flipX || flipY || scale !== 1) ? `${scaleX} ${scaleY}` : undefined,
168
+ } : {}),
169
+ }
170
+
171
+ let svgContent = null
172
+
173
+ // Custom icons (no source prefix)
124
174
  const custom = !source ? customIcons[iconName] : null
125
175
  if (custom) {
126
- // Simple fill-based path icon
127
176
  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>
177
+ svgContent = (
178
+ <svg width={size} height={size} viewBox={custom.viewBox} fill="currentColor" {...ariaProps}>
179
+ <path d={custom.path} />
180
+ </svg>
134
181
  )
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>
182
+ } else if (custom.strokePaths) {
183
+ svgContent = (
184
+ <svg width={size} height={size} viewBox={custom.viewBox} fill="none" stroke="currentColor" strokeWidth={strokeWeight ?? custom.strokeWidth ?? '1.5'} strokeLinecap="round" strokeLinejoin="round" {...ariaProps}>
185
+ {custom.strokePaths.map((d, i) => <path key={i} d={d} />)}
186
+ </svg>
144
187
  )
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>
188
+ } else if (custom.strokeRect || custom.fillPaths) {
189
+ svgContent = (
190
+ <svg width={size} height={size} viewBox={custom.viewBox} fill="none" stroke="currentColor" strokeWidth={strokeWeight ?? '2'} {...ariaProps}>
191
+ {custom.strokeRect && <rect {...custom.strokeRect} />}
192
+ {custom.fillPaths?.map((d, i) => <path key={i} d={d} fill="currentColor" stroke="none" />)}
193
+ </svg>
155
194
  )
156
195
  }
157
196
  }
158
197
 
198
+ // Primer Octicons
199
+ if (!svgContent && source === 'primer') {
200
+ const octicon = octicons[iconName]
201
+ if (octicon) {
202
+ const html = octicon.toSVG({
203
+ width: size, height: size,
204
+ fill: 'currentColor',
205
+ ...(label ? { 'aria-label': label } : { 'aria-hidden': 'true' }),
206
+ })
207
+ svgContent = <span dangerouslySetInnerHTML={{ __html: html }} />
208
+ }
209
+ }
210
+
211
+ // Feather Icons
212
+ if (!svgContent && source === 'feather') {
213
+ const icon = feather.icons[iconName]
214
+ if (icon) {
215
+ const html = icon.toSvg({
216
+ width: size, height: size,
217
+ 'stroke-width': strokeWeight ?? 2,
218
+ ...(label ? { 'aria-label': label } : { 'aria-hidden': 'true' }),
219
+ })
220
+ svgContent = <span dangerouslySetInnerHTML={{ __html: html }} />
221
+ }
222
+ }
223
+
159
224
  // 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}>
225
+ if (!svgContent && source === 'iconoir') {
226
+ const iconoir = iconoirIcons[iconName]
227
+ if (iconoir) {
228
+ const sw = strokeWeight ?? iconoir.strokeWidth
229
+ svgContent = (
165
230
  <svg
166
- width={size}
167
- height={size}
168
- viewBox={iconoir.viewBox}
231
+ width={size} height={size} viewBox={iconoir.viewBox}
169
232
  fill={iconoir.fill ? 'currentColor' : 'none'}
170
233
  strokeWidth={iconoir.fill ? undefined : sw}
171
234
  {...ariaProps}
172
235
  dangerouslySetInnerHTML={{ __html: iconoir.content }}
173
236
  />
174
- </span>
175
- )
237
+ )
238
+ }
176
239
  }
177
240
 
178
- // Unknown icon
179
- return null
241
+ if (!svgContent) return null
242
+
243
+ return <span className={className} style={wrapperStyle}>{svgContent}</span>
180
244
  }