@pixel-point/toolcraft 0.0.2

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 (257) hide show
  1. package/LICENSE.md +98 -0
  2. package/README.md +41 -0
  3. package/bin/create-toolcraft-app.mjs +8 -0
  4. package/bin/toolcraft.mjs +8 -0
  5. package/package.json +24 -0
  6. package/scripts/prepare-pack.mjs +29 -0
  7. package/src/cli.mjs +392 -0
  8. package/src/cli.test.mjs +284 -0
  9. package/src/copy-recursive.mjs +86 -0
  10. package/src/generate.mjs +212 -0
  11. package/src/generate.test.mjs +322 -0
  12. package/src/import-map.mjs +14 -0
  13. package/src/package-json.mjs +80 -0
  14. package/src/package-json.test.mjs +67 -0
  15. package/src/rewrite-imports.mjs +85 -0
  16. package/src/rewrite-imports.test.mjs +58 -0
  17. package/templates/runtime/contracts/component-contracts.test.ts +1165 -0
  18. package/templates/runtime/contracts/component-contracts.ts +1340 -0
  19. package/templates/runtime/contracts/decision-contracts.test.ts +206 -0
  20. package/templates/runtime/contracts/decision-contracts.ts +283 -0
  21. package/templates/runtime/contracts/index.test.ts +14 -0
  22. package/templates/runtime/contracts/index.ts +3 -0
  23. package/templates/runtime/contracts/types.ts +56 -0
  24. package/templates/runtime/export/export.test.ts +203 -0
  25. package/templates/runtime/export/export.ts +132 -0
  26. package/templates/runtime/export/index.ts +1 -0
  27. package/templates/runtime/index.ts +14 -0
  28. package/templates/runtime/react/canvas-shell.test.tsx +424 -0
  29. package/templates/runtime/react/canvas-shell.tsx +408 -0
  30. package/templates/runtime/react/control-renderers.ts +31 -0
  31. package/templates/runtime/react/controls-panel.test.tsx +3736 -0
  32. package/templates/runtime/react/controls-panel.tsx +2327 -0
  33. package/templates/runtime/react/curve-geometry.test.ts +70 -0
  34. package/templates/runtime/react/index.ts +15 -0
  35. package/templates/runtime/react/layer-tree.ts +96 -0
  36. package/templates/runtime/react/layers-panel.test.tsx +487 -0
  37. package/templates/runtime/react/layers-panel.tsx +1348 -0
  38. package/templates/runtime/react/media-file.ts +82 -0
  39. package/templates/runtime/react/panel-host-config.ts +80 -0
  40. package/templates/runtime/react/panel-host-geometry.test.ts +66 -0
  41. package/templates/runtime/react/panel-host-geometry.ts +109 -0
  42. package/templates/runtime/react/panel-host-types.ts +74 -0
  43. package/templates/runtime/react/panel-host.test.tsx +102 -0
  44. package/templates/runtime/react/panel-host.tsx +353 -0
  45. package/templates/runtime/react/runtime-public-api.test.tsx +132 -0
  46. package/templates/runtime/react/settings-transfer.test.ts +150 -0
  47. package/templates/runtime/react/settings-transfer.ts +279 -0
  48. package/templates/runtime/react/storage-key-migration.ts +48 -0
  49. package/templates/runtime/react/theme-runtime.tsx +177 -0
  50. package/templates/runtime/react/timeline-panel.test.tsx +668 -0
  51. package/templates/runtime/react/timeline-panel.tsx +2953 -0
  52. package/templates/runtime/react/toolbar-panel.test.tsx +212 -0
  53. package/templates/runtime/react/toolbar-panel.tsx +205 -0
  54. package/templates/runtime/react/toolcraft-app.integration.test.tsx +350 -0
  55. package/templates/runtime/react/toolcraft-app.test.tsx +339 -0
  56. package/templates/runtime/react/toolcraft-app.tsx +81 -0
  57. package/templates/runtime/react/toolcraft-root.test.tsx +347 -0
  58. package/templates/runtime/react/toolcraft-root.tsx +203 -0
  59. package/templates/runtime/react/use-toolcraft.ts +41 -0
  60. package/templates/runtime/schema/define-toolcraft.test.ts +1524 -0
  61. package/templates/runtime/schema/define-toolcraft.ts +1442 -0
  62. package/templates/runtime/schema/keyframe-capability.test.ts +90 -0
  63. package/templates/runtime/schema/keyframe-capability.ts +51 -0
  64. package/templates/runtime/schema/runtime-targets.ts +40 -0
  65. package/templates/runtime/schema/types.ts +370 -0
  66. package/templates/runtime/state/canvas-zoom.ts +8 -0
  67. package/templates/runtime/state/create-template-state.test.ts +242 -0
  68. package/templates/runtime/state/create-template-state.ts +95 -0
  69. package/templates/runtime/state/keyframe-evaluation.test.ts +141 -0
  70. package/templates/runtime/state/keyframe-evaluation.ts +203 -0
  71. package/templates/runtime/state/persistence.test.ts +217 -0
  72. package/templates/runtime/state/persistence.ts +511 -0
  73. package/templates/runtime/state/reducer.test.ts +937 -0
  74. package/templates/runtime/state/reducer.ts +1212 -0
  75. package/templates/runtime/state/timeline-readiness.ts +43 -0
  76. package/templates/runtime/state/types.ts +242 -0
  77. package/templates/runtime/styles.css +125 -0
  78. package/templates/runtime/testing/performance.test.ts +1058 -0
  79. package/templates/runtime/testing/performance.ts +1078 -0
  80. package/templates/starter/AGENTS.md +186 -0
  81. package/templates/starter/LICENSE.md +98 -0
  82. package/templates/starter/NOTICE.md +8 -0
  83. package/templates/starter/docs/toolcraft/README.md +41 -0
  84. package/templates/starter/docs/toolcraft/acceptance-testing.md +205 -0
  85. package/templates/starter/docs/toolcraft/agent-worklog.md +81 -0
  86. package/templates/starter/docs/toolcraft/assembly-workflow.md +206 -0
  87. package/templates/starter/docs/toolcraft/component-rules.md +299 -0
  88. package/templates/starter/docs/toolcraft/custom-controls.md +71 -0
  89. package/templates/starter/docs/toolcraft/decision-contract.md +71 -0
  90. package/templates/starter/docs/toolcraft/performance.md +112 -0
  91. package/templates/starter/docs/toolcraft/renderer-technique.md +48 -0
  92. package/templates/starter/docs/toolcraft/schema-reference.md +265 -0
  93. package/templates/starter/docs/toolcraft/workflow.md +87 -0
  94. package/templates/starter/e2e/app-browser-acceptance.spec.ts +785 -0
  95. package/templates/starter/e2e/app-controls.spec.ts +41 -0
  96. package/templates/starter/e2e/app-performance.spec.ts +326 -0
  97. package/templates/starter/e2e/canvas-handle-helpers.ts +244 -0
  98. package/templates/starter/e2e/performance-helpers.ts +612 -0
  99. package/templates/starter/e2e/product-observable-helpers.ts +170 -0
  100. package/templates/starter/index.html +12 -0
  101. package/templates/starter/package.json +52 -0
  102. package/templates/starter/playwright.config.ts +43 -0
  103. package/templates/starter/scripts/check-ai-skills.mjs +95 -0
  104. package/templates/starter/scripts/check-toolcraft-docs.mjs +159 -0
  105. package/templates/starter/scripts/check-toolcraft-integrity.mjs +232 -0
  106. package/templates/starter/scripts/run-vite-on-free-port.mjs +48 -0
  107. package/templates/starter/scripts/toolcraft-port.mjs +54 -0
  108. package/templates/starter/scripts/toolcraft-port.test.mjs +73 -0
  109. package/templates/starter/src/app/starter-acceptance.test.ts +5959 -0
  110. package/templates/starter/src/app/starter-acceptance.ts +2646 -0
  111. package/templates/starter/src/app/starter-performance.test.ts +1390 -0
  112. package/templates/starter/src/app/starter-performance.ts +12 -0
  113. package/templates/starter/src/app/starter-schema.test.ts +70 -0
  114. package/templates/starter/src/app/starter-schema.ts +15 -0
  115. package/templates/starter/src/main.tsx +18 -0
  116. package/templates/starter/src/router.tsx +16 -0
  117. package/templates/starter/src/routes/index.tsx +7 -0
  118. package/templates/starter/src/routes/root.tsx +19 -0
  119. package/templates/starter/src/styles.css +120 -0
  120. package/templates/starter/tsconfig.json +11 -0
  121. package/templates/starter/vite.config.ts +13 -0
  122. package/templates/ui/components/composites/accordion.tsx +73 -0
  123. package/templates/ui/components/composites/alert-dialog.tsx +190 -0
  124. package/templates/ui/components/composites/alert.tsx +74 -0
  125. package/templates/ui/components/composites/aspect-ratio.tsx +22 -0
  126. package/templates/ui/components/composites/avatar.tsx +98 -0
  127. package/templates/ui/components/composites/badge.tsx +69 -0
  128. package/templates/ui/components/composites/breadcrumb.tsx +106 -0
  129. package/templates/ui/components/composites/card.tsx +91 -0
  130. package/templates/ui/components/composites/combobox.tsx +486 -0
  131. package/templates/ui/components/composites/command.tsx +296 -0
  132. package/templates/ui/components/composites/context-menu.tsx +247 -0
  133. package/templates/ui/components/composites/dialog.tsx +282 -0
  134. package/templates/ui/components/composites/dropdown-menu.tsx +299 -0
  135. package/templates/ui/components/composites/empty.tsx +110 -0
  136. package/templates/ui/components/composites/hover-card.tsx +44 -0
  137. package/templates/ui/components/composites/index.ts +30 -0
  138. package/templates/ui/components/composites/menubar.tsx +214 -0
  139. package/templates/ui/components/composites/navigation-menu.tsx +167 -0
  140. package/templates/ui/components/composites/pagination.tsx +131 -0
  141. package/templates/ui/components/composites/progress.tsx +72 -0
  142. package/templates/ui/components/composites/radio-group.tsx +84 -0
  143. package/templates/ui/components/composites/resizable.tsx +42 -0
  144. package/templates/ui/components/composites/sheet.tsx +153 -0
  145. package/templates/ui/components/composites/sidebar-structural.tsx +310 -0
  146. package/templates/ui/components/composites/sidebar.tsx +431 -0
  147. package/templates/ui/components/composites/sonner.tsx +35 -0
  148. package/templates/ui/components/composites/spinner.tsx +43 -0
  149. package/templates/ui/components/composites/table.tsx +108 -0
  150. package/templates/ui/components/composites/tabs.tsx +83 -0
  151. package/templates/ui/components/control-layout/index.tsx +437 -0
  152. package/templates/ui/components/controls/actions/actions-control.tsx +139 -0
  153. package/templates/ui/components/controls/actions/index.ts +9 -0
  154. package/templates/ui/components/controls/anchor-grid/anchor-grid-control.tsx +107 -0
  155. package/templates/ui/components/controls/anchor-grid/index.ts +4 -0
  156. package/templates/ui/components/controls/boolean/boolean-controls.tsx +79 -0
  157. package/templates/ui/components/controls/boolean/index.ts +4 -0
  158. package/templates/ui/components/controls/channel-mixer/channel-mixer-control.tsx +95 -0
  159. package/templates/ui/components/controls/channel-mixer/index.ts +4 -0
  160. package/templates/ui/components/controls/channel-tabs/channel-tabs.tsx +42 -0
  161. package/templates/ui/components/controls/channel-tabs/index.ts +6 -0
  162. package/templates/ui/components/controls/code-textarea/code-textarea-control.tsx +90 -0
  163. package/templates/ui/components/controls/code-textarea/index.ts +4 -0
  164. package/templates/ui/components/controls/color/color-control.tsx +571 -0
  165. package/templates/ui/components/controls/color/color-picker-popover.tsx +104 -0
  166. package/templates/ui/components/controls/color/index.ts +41 -0
  167. package/templates/ui/components/controls/color/palette-control-data.ts +436 -0
  168. package/templates/ui/components/controls/color/palette-control.tsx +535 -0
  169. package/templates/ui/components/controls/color/style-guide-color-picker-channel-utils.ts +162 -0
  170. package/templates/ui/components/controls/color/style-guide-color-picker-interactions.ts +190 -0
  171. package/templates/ui/components/controls/color/style-guide-color-picker-logic.ts +485 -0
  172. package/templates/ui/components/controls/color/style-guide-color-picker-parts.tsx +710 -0
  173. package/templates/ui/components/controls/color/style-guide-color-picker.tsx +503 -0
  174. package/templates/ui/components/controls/control-types.ts +43 -0
  175. package/templates/ui/components/controls/curves/curve-geometry.ts +355 -0
  176. package/templates/ui/components/controls/curves/curve-graph.tsx +390 -0
  177. package/templates/ui/components/controls/curves/curves-control.tsx +445 -0
  178. package/templates/ui/components/controls/curves/index.ts +6 -0
  179. package/templates/ui/components/controls/file-drop/file-drop-control.tsx +191 -0
  180. package/templates/ui/components/controls/file-drop/index.ts +5 -0
  181. package/templates/ui/components/controls/font-picker/font-catalog.json +15360 -0
  182. package/templates/ui/components/controls/font-picker/font-catalog.ts +116 -0
  183. package/templates/ui/components/controls/font-picker/font-picker-control.tsx +1202 -0
  184. package/templates/ui/components/controls/font-picker/font-preview-loader.ts +336 -0
  185. package/templates/ui/components/controls/font-picker/index.ts +24 -0
  186. package/templates/ui/components/controls/font-picker/use-hover-intent.ts +46 -0
  187. package/templates/ui/components/controls/gradient/gradient-control-utils.ts +190 -0
  188. package/templates/ui/components/controls/gradient/gradient-control.tsx +612 -0
  189. package/templates/ui/components/controls/gradient/gradient-stop-list.tsx +400 -0
  190. package/templates/ui/components/controls/gradient/gradient-toolbar.tsx +152 -0
  191. package/templates/ui/components/controls/gradient/index.ts +4 -0
  192. package/templates/ui/components/controls/image-picker/image-picker-control.tsx +139 -0
  193. package/templates/ui/components/controls/image-picker/index.ts +7 -0
  194. package/templates/ui/components/controls/index.ts +192 -0
  195. package/templates/ui/components/controls/range-input/index.ts +4 -0
  196. package/templates/ui/components/controls/range-input/range-input-control.tsx +173 -0
  197. package/templates/ui/components/controls/range-slider/index.ts +4 -0
  198. package/templates/ui/components/controls/range-slider/range-slider-control.tsx +122 -0
  199. package/templates/ui/components/controls/range-slider/range-slider-value.ts +61 -0
  200. package/templates/ui/components/controls/segmented/index.ts +8 -0
  201. package/templates/ui/components/controls/segmented/segmented-control.tsx +94 -0
  202. package/templates/ui/components/controls/select/index.ts +4 -0
  203. package/templates/ui/components/controls/select/select-control.tsx +223 -0
  204. package/templates/ui/components/controls/slider/index.ts +4 -0
  205. package/templates/ui/components/controls/slider/slider-control.tsx +150 -0
  206. package/templates/ui/components/controls/slider/slider-value.ts +56 -0
  207. package/templates/ui/components/controls/text-input/index.ts +4 -0
  208. package/templates/ui/components/controls/text-input/text-input-control.tsx +158 -0
  209. package/templates/ui/components/controls/use-measured-element-width.ts +42 -0
  210. package/templates/ui/components/controls/vector/index.ts +8 -0
  211. package/templates/ui/components/controls/vector/vector-control.tsx +401 -0
  212. package/templates/ui/components/panel/index.ts +19 -0
  213. package/templates/ui/components/panel/panel-actions.tsx +165 -0
  214. package/templates/ui/components/panel/panel-header.tsx +61 -0
  215. package/templates/ui/components/panel/panel-icon-button.tsx +96 -0
  216. package/templates/ui/components/panel/panel-section.tsx +168 -0
  217. package/templates/ui/components/panel/panel-surface.tsx +206 -0
  218. package/templates/ui/components/panel/panel.tsx +210 -0
  219. package/templates/ui/components/primitives/animated-loader.tsx +61 -0
  220. package/templates/ui/components/primitives/button-group.tsx +134 -0
  221. package/templates/ui/components/primitives/button.tsx +429 -0
  222. package/templates/ui/components/primitives/checkbox.tsx +62 -0
  223. package/templates/ui/components/primitives/editable-slider-value-label.tsx +337 -0
  224. package/templates/ui/components/primitives/field.tsx +225 -0
  225. package/templates/ui/components/primitives/index.ts +82 -0
  226. package/templates/ui/components/primitives/input-group.tsx +298 -0
  227. package/templates/ui/components/primitives/input.tsx +61 -0
  228. package/templates/ui/components/primitives/internal/button-loading.tsx +178 -0
  229. package/templates/ui/components/primitives/label.tsx +16 -0
  230. package/templates/ui/components/primitives/popover.tsx +126 -0
  231. package/templates/ui/components/primitives/portal-layer-context.tsx +33 -0
  232. package/templates/ui/components/primitives/primitive-arrow-icon.tsx +38 -0
  233. package/templates/ui/components/primitives/scroll-fade-logic.ts +441 -0
  234. package/templates/ui/components/primitives/scroll-fade-render.tsx +75 -0
  235. package/templates/ui/components/primitives/scroll-fade-types.ts +41 -0
  236. package/templates/ui/components/primitives/scroll-fade.tsx +72 -0
  237. package/templates/ui/components/primitives/select.tsx +408 -0
  238. package/templates/ui/components/primitives/selection-state.ts +31 -0
  239. package/templates/ui/components/primitives/separator.tsx +21 -0
  240. package/templates/ui/components/primitives/slider/index.ts +4 -0
  241. package/templates/ui/components/primitives/slider/slider-interaction.tsx +96 -0
  242. package/templates/ui/components/primitives/slider/slider-parts.tsx +303 -0
  243. package/templates/ui/components/primitives/slider/slider-reset.ts +152 -0
  244. package/templates/ui/components/primitives/slider/slider-value.ts +114 -0
  245. package/templates/ui/components/primitives/slider/slider.tsx +511 -0
  246. package/templates/ui/components/primitives/switch.tsx +35 -0
  247. package/templates/ui/components/primitives/textarea.tsx +49 -0
  248. package/templates/ui/components/primitives/toggle-group.tsx +114 -0
  249. package/templates/ui/components/primitives/toggle.tsx +46 -0
  250. package/templates/ui/components/primitives/tooltip.tsx +100 -0
  251. package/templates/ui/hooks/use-mobile.ts +21 -0
  252. package/templates/ui/index.ts +31 -0
  253. package/templates/ui/lib/control-outline.ts +3 -0
  254. package/templates/ui/lib/input-control-style.ts +131 -0
  255. package/templates/ui/lib/style-guide-color-utils.ts +111 -0
  256. package/templates/ui/lib/utils.ts +6 -0
  257. package/templates/ui/styles.css +291 -0
@@ -0,0 +1,336 @@
1
+ import {
2
+ buildFontPickerGoogleStylesheetHref,
3
+ getFontPickerFontById,
4
+ type FontPickerFontCatalogEntry,
5
+ } from "./font-catalog";
6
+
7
+ export type FontPickerPreviewLoadPriority = "high" | "normal";
8
+
9
+ type QueuedPreviewTask = {
10
+ fontEntry: FontPickerFontCatalogEntry;
11
+ order: number;
12
+ priorityScore: number;
13
+ };
14
+
15
+ const loadedHrefSet = new Set<string>();
16
+ const pendingHrefMap = new Map<string, Promise<void>>();
17
+ const loadedFontFaceSet = new Set<string>();
18
+ const pendingFontFaceMap = new Map<string, Promise<void>>();
19
+ const queuedPreviewTaskMap = new Map<string, QueuedPreviewTask>();
20
+ const activeQueuedFontIdSet = new Set<string>();
21
+ const maxPreviewLoadConcurrency = 6;
22
+
23
+ let activeQueuedTaskCount = 0;
24
+ let queuedTaskOrderCounter = 0;
25
+
26
+ function resolvePriorityScore(priority: FontPickerPreviewLoadPriority): number {
27
+ return priority === "high" ? 1 : 0;
28
+ }
29
+
30
+ function resolvePreviewWeights(entry: FontPickerFontCatalogEntry): string[] {
31
+ const uniqueWeights = Array.from(
32
+ new Set(
33
+ entry.weights
34
+ .map((value) => Number.parseInt(value, 10))
35
+ .filter((value) => Number.isFinite(value))
36
+ .sort((left, right) => left - right),
37
+ ),
38
+ );
39
+
40
+ if (!uniqueWeights.length) {
41
+ return ["400"];
42
+ }
43
+
44
+ const pickNearest = (target: number): number =>
45
+ uniqueWeights.reduce((closest, candidate) =>
46
+ Math.abs(candidate - target) < Math.abs(closest - target)
47
+ ? candidate
48
+ : closest,
49
+ );
50
+
51
+ return Array.from(new Set([pickNearest(400), pickNearest(600)])).map((value) =>
52
+ String(value),
53
+ );
54
+ }
55
+
56
+ function buildFontFaceDescriptor(
57
+ entry: FontPickerFontCatalogEntry,
58
+ weight: string,
59
+ ): string {
60
+ return `${weight} 16px "${entry.family.trim().replace(/"/g, '\\"')}"`;
61
+ }
62
+
63
+ function buildFontFaceKey(
64
+ entry: FontPickerFontCatalogEntry,
65
+ weight: string,
66
+ ): string {
67
+ return `${entry.id}:${weight}`;
68
+ }
69
+
70
+ function escapeSelectorValue(value: string): string {
71
+ return typeof CSS !== "undefined" && typeof CSS.escape === "function"
72
+ ? CSS.escape(value)
73
+ : value.replace(/["\\]/g, "\\$&");
74
+ }
75
+
76
+ function ensurePreviewFontStylesheet(href: string): Promise<void> {
77
+ if (typeof document === "undefined") {
78
+ return Promise.resolve();
79
+ }
80
+
81
+ if (loadedHrefSet.has(href)) {
82
+ return Promise.resolve();
83
+ }
84
+
85
+ const existing = document.head.querySelector<HTMLLinkElement>(
86
+ `link[data-toolcraft-font-href="${escapeSelectorValue(href)}"]`,
87
+ );
88
+
89
+ if (existing) {
90
+ if (existing.dataset.loaded === "true") {
91
+ loadedHrefSet.add(href);
92
+ return Promise.resolve();
93
+ }
94
+
95
+ const pending = pendingHrefMap.get(href);
96
+ if (pending) {
97
+ return pending;
98
+ }
99
+
100
+ const nextPending = new Promise<void>((resolve) => {
101
+ existing.addEventListener(
102
+ "load",
103
+ () => {
104
+ existing.dataset.loaded = "true";
105
+ loadedHrefSet.add(href);
106
+ pendingHrefMap.delete(href);
107
+ resolve();
108
+ },
109
+ { once: true },
110
+ );
111
+ existing.addEventListener(
112
+ "error",
113
+ () => {
114
+ pendingHrefMap.delete(href);
115
+ resolve();
116
+ },
117
+ { once: true },
118
+ );
119
+ });
120
+
121
+ pendingHrefMap.set(href, nextPending);
122
+ return nextPending;
123
+ }
124
+
125
+ const link = document.createElement("link");
126
+ link.rel = "stylesheet";
127
+ link.href = href;
128
+ link.crossOrigin = "anonymous";
129
+ link.dataset.toolcraftFontHref = href;
130
+
131
+ const pending = new Promise<void>((resolve) => {
132
+ link.addEventListener(
133
+ "load",
134
+ () => {
135
+ link.dataset.loaded = "true";
136
+ loadedHrefSet.add(href);
137
+ pendingHrefMap.delete(href);
138
+ resolve();
139
+ },
140
+ { once: true },
141
+ );
142
+ link.addEventListener(
143
+ "error",
144
+ () => {
145
+ pendingHrefMap.delete(href);
146
+ resolve();
147
+ },
148
+ { once: true },
149
+ );
150
+ });
151
+
152
+ pendingHrefMap.set(href, pending);
153
+ document.head.appendChild(link);
154
+
155
+ return pending;
156
+ }
157
+
158
+ function ensurePreviewFontFaces(entry: FontPickerFontCatalogEntry): Promise<void> {
159
+ if (typeof document === "undefined" || !("fonts" in document)) {
160
+ return Promise.resolve();
161
+ }
162
+
163
+ const fontFaceSet = document.fonts;
164
+ const load =
165
+ typeof fontFaceSet.load === "function"
166
+ ? fontFaceSet.load.bind(fontFaceSet)
167
+ : null;
168
+ const check =
169
+ typeof fontFaceSet.check === "function"
170
+ ? fontFaceSet.check.bind(fontFaceSet)
171
+ : null;
172
+
173
+ if (!load) {
174
+ return Promise.resolve();
175
+ }
176
+
177
+ return Promise.all(
178
+ resolvePreviewWeights(entry).map((weight) => {
179
+ const key = buildFontFaceKey(entry, weight);
180
+
181
+ if (loadedFontFaceSet.has(key)) {
182
+ return Promise.resolve();
183
+ }
184
+
185
+ const descriptor = buildFontFaceDescriptor(entry, weight);
186
+ if (check?.(descriptor)) {
187
+ loadedFontFaceSet.add(key);
188
+ return Promise.resolve();
189
+ }
190
+
191
+ const pending = pendingFontFaceMap.get(key);
192
+ if (pending) {
193
+ return pending;
194
+ }
195
+
196
+ const nextPending = load(descriptor)
197
+ .then(() => {
198
+ loadedFontFaceSet.add(key);
199
+ })
200
+ .catch(() => undefined)
201
+ .finally(() => {
202
+ pendingFontFaceMap.delete(key);
203
+ });
204
+
205
+ pendingFontFaceMap.set(key, nextPending);
206
+ return nextPending;
207
+ }),
208
+ ).then(() => undefined);
209
+ }
210
+
211
+ export async function ensureFontPickerPreviewLoaded(
212
+ entry: FontPickerFontCatalogEntry,
213
+ ): Promise<void> {
214
+ await ensurePreviewFontStylesheet(buildFontPickerGoogleStylesheetHref(entry));
215
+ await ensurePreviewFontFaces(entry);
216
+ }
217
+
218
+ export function isFontPickerPreviewLoaded(
219
+ fontEntry: FontPickerFontCatalogEntry | null | undefined,
220
+ ): boolean {
221
+ if (!fontEntry) {
222
+ return true;
223
+ }
224
+
225
+ const href = buildFontPickerGoogleStylesheetHref(fontEntry);
226
+ if (!loadedHrefSet.has(href)) {
227
+ return false;
228
+ }
229
+
230
+ if (typeof document === "undefined" || !("fonts" in document)) {
231
+ return true;
232
+ }
233
+
234
+ const fontFaceSet = document.fonts;
235
+ const check =
236
+ typeof fontFaceSet.check === "function"
237
+ ? fontFaceSet.check.bind(fontFaceSet)
238
+ : null;
239
+
240
+ return resolvePreviewWeights(fontEntry).every((weight) => {
241
+ const key = buildFontFaceKey(fontEntry, weight);
242
+ const descriptor = buildFontFaceDescriptor(fontEntry, weight);
243
+ return loadedFontFaceSet.has(key) || check?.(descriptor) === true;
244
+ });
245
+ }
246
+
247
+ function pickNextQueuedTask(): QueuedPreviewTask | null {
248
+ let nextTask: QueuedPreviewTask | null = null;
249
+
250
+ queuedPreviewTaskMap.forEach((task) => {
251
+ if (!nextTask) {
252
+ nextTask = task;
253
+ return;
254
+ }
255
+
256
+ if (task.priorityScore > nextTask.priorityScore) {
257
+ nextTask = task;
258
+ return;
259
+ }
260
+
261
+ if (task.priorityScore === nextTask.priorityScore && task.order < nextTask.order) {
262
+ nextTask = task;
263
+ }
264
+ });
265
+
266
+ return nextTask;
267
+ }
268
+
269
+ function drainPreviewQueue(): void {
270
+ while (activeQueuedTaskCount < maxPreviewLoadConcurrency) {
271
+ const nextTask = pickNextQueuedTask();
272
+
273
+ if (!nextTask) {
274
+ return;
275
+ }
276
+
277
+ const taskId = nextTask.fontEntry.id;
278
+ queuedPreviewTaskMap.delete(taskId);
279
+
280
+ if (isFontPickerPreviewLoaded(nextTask.fontEntry)) {
281
+ continue;
282
+ }
283
+
284
+ activeQueuedFontIdSet.add(taskId);
285
+ activeQueuedTaskCount += 1;
286
+
287
+ void ensureFontPickerPreviewLoaded(nextTask.fontEntry).finally(() => {
288
+ activeQueuedFontIdSet.delete(taskId);
289
+ activeQueuedTaskCount = Math.max(0, activeQueuedTaskCount - 1);
290
+ drainPreviewQueue();
291
+ });
292
+ }
293
+ }
294
+
295
+ export function queueFontPickerPreviewLoad(
296
+ fontEntryOrId: FontPickerFontCatalogEntry | string | null | undefined,
297
+ options: { priority?: FontPickerPreviewLoadPriority } = {},
298
+ ): void {
299
+ const fontEntry =
300
+ typeof fontEntryOrId === "string"
301
+ ? getFontPickerFontById(fontEntryOrId)
302
+ : fontEntryOrId;
303
+
304
+ if (
305
+ !fontEntry ||
306
+ isFontPickerPreviewLoaded(fontEntry) ||
307
+ activeQueuedFontIdSet.has(fontEntry.id)
308
+ ) {
309
+ return;
310
+ }
311
+
312
+ const priorityScore = resolvePriorityScore(options.priority ?? "normal");
313
+ const existing = queuedPreviewTaskMap.get(fontEntry.id);
314
+
315
+ if (existing && existing.priorityScore >= priorityScore) {
316
+ return;
317
+ }
318
+
319
+ queuedTaskOrderCounter += 1;
320
+ queuedPreviewTaskMap.set(fontEntry.id, {
321
+ fontEntry,
322
+ order: queuedTaskOrderCounter,
323
+ priorityScore,
324
+ });
325
+
326
+ drainPreviewQueue();
327
+ }
328
+
329
+ export function queueFontPickerPreviewLoadBatch(
330
+ fontEntries: readonly FontPickerFontCatalogEntry[],
331
+ options: { priority?: FontPickerPreviewLoadPriority } = {},
332
+ ): void {
333
+ for (const fontEntry of fontEntries) {
334
+ queueFontPickerPreviewLoad(fontEntry, options);
335
+ }
336
+ }
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ export {
4
+ FontPickerControl,
5
+ FontPickerControl as FontPicker,
6
+ } from "./font-picker-control";
7
+ export type {
8
+ FontPickerControlProps,
9
+ FontPickerLetterSpacingPreset,
10
+ FontPickerLineHeightPreset,
11
+ FontPickerTextCasePreset,
12
+ FontPickerValue,
13
+ } from "./font-picker-control";
14
+ export {
15
+ getDefaultFontPickerFontId,
16
+ getFontPickerCatalog,
17
+ getFontPickerFontById,
18
+ resolveFontPickerFontId,
19
+ } from "./font-catalog";
20
+ export type {
21
+ FontPickerFontCatalogEntry,
22
+ FontPickerFontCategory,
23
+ FontPickerFontFilterValue,
24
+ } from "./font-catalog";
@@ -0,0 +1,46 @@
1
+ import * as React from "react";
2
+
3
+ const hoverIntentDwellMs = 160;
4
+
5
+ export function useHoverIntent<T>({
6
+ dwellMs = hoverIntentDwellMs,
7
+ onIntent,
8
+ }: {
9
+ dwellMs?: number;
10
+ onIntent: (value: T) => void;
11
+ }): {
12
+ cancelIntent: () => void;
13
+ scheduleIntent: (value: T) => void;
14
+ } {
15
+ const timeoutRef = React.useRef<number | null>(null);
16
+ const pendingValueRef = React.useRef<T | undefined>(undefined);
17
+
18
+ const cancelIntent = React.useCallback(() => {
19
+ if (timeoutRef.current !== null) {
20
+ window.clearTimeout(timeoutRef.current);
21
+ timeoutRef.current = null;
22
+ }
23
+
24
+ pendingValueRef.current = undefined;
25
+ }, []);
26
+
27
+ const scheduleIntent = React.useCallback(
28
+ (value: T) => {
29
+ cancelIntent();
30
+ pendingValueRef.current = value;
31
+ timeoutRef.current = window.setTimeout(() => {
32
+ timeoutRef.current = null;
33
+ const pendingValue = pendingValueRef.current;
34
+ pendingValueRef.current = undefined;
35
+ if (pendingValue !== undefined) {
36
+ onIntent(pendingValue);
37
+ }
38
+ }, dwellMs);
39
+ },
40
+ [cancelIntent, dwellMs, onIntent],
41
+ );
42
+
43
+ React.useEffect(() => cancelIntent, [cancelIntent]);
44
+
45
+ return { cancelIntent, scheduleIntent };
46
+ }
@@ -0,0 +1,190 @@
1
+ import type { GradientStop, GradientType } from "../control-types";
2
+
3
+ export type IndexedGradientStop = GradientStop & { originalIndex: number };
4
+
5
+ export const maxGradientStops = 5;
6
+ export const minGradientStops = 2;
7
+ export const gradientTypeOptions = [
8
+ { label: "Linear", value: "linear" },
9
+ { label: "Radial", value: "radial" },
10
+ { label: "Angular", value: "angular" },
11
+ { label: "Diamond", value: "diamond" },
12
+ ] as const satisfies readonly { label: string; value: GradientType }[];
13
+
14
+ function clamp(value: number, min = 0, max = 1): number {
15
+ return Math.min(max, Math.max(min, value));
16
+ }
17
+
18
+ export function getGradientType(type: GradientType | undefined): GradientType {
19
+ return type ?? "linear";
20
+ }
21
+
22
+ export function getGradientAngle(angle: number | undefined): number {
23
+ return typeof angle === "number" && Number.isFinite(angle) ? Math.round(angle) : 90;
24
+ }
25
+
26
+ export function normalizeGradientAngle(angle: string): number {
27
+ const parsedValue = Number.parseFloat(angle);
28
+
29
+ return Number.isFinite(parsedValue) ? Math.round(parsedValue) : 90;
30
+ }
31
+
32
+ export function parseStopPosition(position: string): number {
33
+ const parsedValue = Number.parseFloat(position);
34
+
35
+ return Number.isFinite(parsedValue) ? clamp(parsedValue / 100) : 0;
36
+ }
37
+
38
+ export function formatStopPosition(position: number): string {
39
+ return `${Math.round(clamp(position) * 100)}%`;
40
+ }
41
+
42
+ export function parseStopOpacity(opacity: number | undefined): number {
43
+ return clamp(opacity ?? 100, 0, 100);
44
+ }
45
+
46
+ export function normalizeStopOpacity(opacity: string): number {
47
+ const parsedValue = Number.parseFloat(opacity);
48
+
49
+ return Number.isFinite(parsedValue) ? Math.round(clamp(parsedValue, 0, 100)) : 100;
50
+ }
51
+
52
+ export function normalizeColorInput(value: string): string {
53
+ const trimmedValue = value.trim();
54
+
55
+ if (/^[\da-f]{6}$/i.test(trimmedValue)) {
56
+ return `#${trimmedValue.toUpperCase()}`;
57
+ }
58
+
59
+ if (/^#[\da-f]{6}$/i.test(trimmedValue)) {
60
+ return trimmedValue.toUpperCase();
61
+ }
62
+
63
+ return getNativeColorPickerValue(trimmedValue).toUpperCase();
64
+ }
65
+
66
+ export function formatColorInputValue(color: string): string {
67
+ const normalizedColor = getNativeColorPickerValue(color).toUpperCase();
68
+
69
+ return normalizedColor.slice(1);
70
+ }
71
+
72
+ export function getNativeColorPickerValue(color: string): string {
73
+ const trimmedColor = color.trim();
74
+
75
+ if (/^#[\da-f]{6}$/i.test(trimmedColor)) {
76
+ return trimmedColor.toUpperCase();
77
+ }
78
+
79
+ if (/^[\da-f]{6}$/i.test(trimmedColor)) {
80
+ return `#${trimmedColor.toUpperCase()}`;
81
+ }
82
+
83
+ return "#000000";
84
+ }
85
+
86
+ export function sortStops(stops: readonly GradientStop[]): GradientStop[] {
87
+ return [...stops].sort(
88
+ (left, right) => parseStopPosition(left.position) - parseStopPosition(right.position),
89
+ );
90
+ }
91
+
92
+ export function getIndexedStops(stops: readonly GradientStop[]): IndexedGradientStop[] {
93
+ return stops
94
+ .map((stop, originalIndex) => ({ ...stop, originalIndex }))
95
+ .sort((left, right) => parseStopPosition(left.position) - parseStopPosition(right.position));
96
+ }
97
+
98
+ export function getStopCssColor(stop: GradientStop): string {
99
+ const opacity = parseStopOpacity(stop.opacity);
100
+
101
+ if (opacity >= 100) {
102
+ return stop.color;
103
+ }
104
+
105
+ return `color-mix(in oklab, ${stop.color} ${opacity}%, transparent)`;
106
+ }
107
+
108
+ function getGradientStopList(stops: readonly GradientStop[]): string {
109
+ return sortStops(stops)
110
+ .map(
111
+ (stop) => `${getStopCssColor(stop)} ${formatStopPosition(parseStopPosition(stop.position))}`,
112
+ )
113
+ .join(", ");
114
+ }
115
+
116
+ export function getGradientBackground(
117
+ type: GradientType,
118
+ stops: readonly GradientStop[],
119
+ angle = 90,
120
+ ): string {
121
+ const stopList = getGradientStopList(stops);
122
+ const gradientAngle = getGradientAngle(angle);
123
+
124
+ switch (type) {
125
+ case "angular":
126
+ return `conic-gradient(from 90deg, ${stopList})`;
127
+ case "diamond":
128
+ return `radial-gradient(closest-corner at 50% 50%, ${stopList})`;
129
+ case "radial":
130
+ return `radial-gradient(circle at 50% 50%, ${stopList})`;
131
+ case "linear":
132
+ return `linear-gradient(${gradientAngle}deg, ${stopList})`;
133
+ }
134
+ }
135
+
136
+ export function getNextGradientType(type: GradientType): GradientType {
137
+ const typeIndex = gradientTypeOptions.findIndex((option) => option.value === type);
138
+ const nextOption = gradientTypeOptions[(typeIndex + 1) % gradientTypeOptions.length];
139
+
140
+ return nextOption?.value ?? "linear";
141
+ }
142
+
143
+ export function isButtonTarget(target: EventTarget): boolean {
144
+ return target instanceof HTMLElement && target.closest("button") !== null;
145
+ }
146
+
147
+ export function getPositionFromTrack(track: HTMLDivElement | null, clientX: number): string {
148
+ const rect = track?.getBoundingClientRect();
149
+
150
+ if (!rect) {
151
+ return "0%";
152
+ }
153
+
154
+ return formatStopPosition((clientX - rect.left) / rect.width);
155
+ }
156
+
157
+ export function updateStopAt(
158
+ stops: readonly GradientStop[],
159
+ index: number,
160
+ nextStop: Partial<GradientStop>,
161
+ ): GradientStop[] {
162
+ return stops.map((stop, stopIndex) => (stopIndex === index ? { ...stop, ...nextStop } : stop));
163
+ }
164
+
165
+ export function addGradientStop(
166
+ stops: readonly GradientStop[],
167
+ activeStop: GradientStop | null,
168
+ position: string,
169
+ ): { nextStop: GradientStop; nextStops: GradientStop[] } {
170
+ const nextStop = {
171
+ color: getNativeColorPickerValue(activeStop?.color ?? "#D9D9D9"),
172
+ opacity: parseStopOpacity(activeStop?.opacity),
173
+ position,
174
+ };
175
+
176
+ return { nextStop, nextStops: sortStops([...stops, nextStop]) };
177
+ }
178
+
179
+ export function removeGradientStop(stops: readonly GradientStop[], index: number): GradientStop[] {
180
+ return stops.filter((_, stopIndex) => stopIndex !== index);
181
+ }
182
+
183
+ export function reverseGradientStops(stops: readonly GradientStop[]): GradientStop[] {
184
+ return sortStops(
185
+ stops.map((stop) => ({
186
+ ...stop,
187
+ position: formatStopPosition(1 - parseStopPosition(stop.position)),
188
+ })),
189
+ );
190
+ }