@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,223 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { ControlFieldLabel } from "../../control-layout";
6
+ import { cn } from "../../../lib/utils";
7
+ import {
8
+ Field,
9
+ ScrollFade,
10
+ Select,
11
+ SelectContent,
12
+ SelectGroup,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from "../../primitives";
17
+ import type { ControlOption } from "../control-types";
18
+ import { useMeasuredElementWidth } from "../use-measured-element-width";
19
+
20
+ export type SelectControlInput = {
21
+ name: string;
22
+ onValueChange?: (value: string) => void;
23
+ options: readonly ControlOption[];
24
+ value: string;
25
+ };
26
+
27
+ type StaticSelectSize = "sm" | "default" | "lg" | "xl";
28
+ type SelectControlFieldLayout = "auto" | "stacked";
29
+
30
+ type SelectControlSingleProps = SelectControlInput & {
31
+ inputs?: never;
32
+ inputsPerRow?: never;
33
+ layout?: SelectControlFieldLayout;
34
+ };
35
+
36
+ type SelectControlGroupProps = {
37
+ inputs: readonly SelectControlInput[];
38
+ inputsPerRow?: number;
39
+ layout?: never;
40
+ };
41
+
42
+ export type SelectControlProps =
43
+ | SelectControlSingleProps
44
+ | SelectControlGroupProps;
45
+
46
+ const compactHorizontalFieldClassName = "h-fit min-w-0 items-center justify-between gap-3";
47
+ const WIDE_SELECT_OPTION_LABEL_LENGTH = 32;
48
+
49
+ function hasWideSelectContent(options: readonly ControlOption[]): boolean {
50
+ return options.some((option) => option.label.length >= WIDE_SELECT_OPTION_LABEL_LENGTH);
51
+ }
52
+
53
+ function isSelectControlGroupProps(
54
+ props: SelectControlProps,
55
+ ): props is SelectControlGroupProps {
56
+ return Array.isArray((props as SelectControlGroupProps).inputs);
57
+ }
58
+
59
+ function getInputsPerRow(inputsPerRow: number | undefined): number {
60
+ if (typeof inputsPerRow !== "number" || !Number.isFinite(inputsPerRow)) {
61
+ return 1;
62
+ }
63
+
64
+ return Math.min(2, Math.max(1, Math.floor(inputsPerRow)));
65
+ }
66
+
67
+ export function StaticSelect({
68
+ ariaLabel,
69
+ disabled,
70
+ onValueChange,
71
+ options,
72
+ popupMaxWidth,
73
+ scrollFadeValue = true,
74
+ size = "default",
75
+ triggerClassName,
76
+ value,
77
+ }: {
78
+ ariaLabel?: string;
79
+ disabled?: boolean;
80
+ onValueChange?: (value: string) => void;
81
+ options: readonly ControlOption[];
82
+ popupMaxWidth?: number;
83
+ scrollFadeValue?: boolean;
84
+ size?: StaticSelectSize;
85
+ triggerClassName?: string;
86
+ value: string;
87
+ }): React.JSX.Element {
88
+ const selected = options.find((option) => option.value === value) ?? options[0];
89
+ const shouldConstrainPopup = hasWideSelectContent(options);
90
+
91
+ return (
92
+ <Select
93
+ items={options.map((option) => ({ label: option.label, value: option.value }))}
94
+ onValueChange={(nextValue) => onValueChange?.(String(nextValue))}
95
+ value={selected?.value}
96
+ >
97
+ <SelectTrigger
98
+ aria-label={ariaLabel}
99
+ className={cn("w-full justify-between", triggerClassName)}
100
+ disabled={disabled}
101
+ size={size}
102
+ title={selected?.label}
103
+ >
104
+ <SelectValue>
105
+ {() =>
106
+ scrollFadeValue ? (
107
+ <ScrollFade
108
+ className="no-scrollbar min-w-0"
109
+ containerClassName="min-w-0 flex-1"
110
+ preset="compact"
111
+ side="right"
112
+ watch={[selected?.label]}
113
+ >
114
+ <span className="block min-w-max whitespace-nowrap pr-2" title={selected?.label}>
115
+ {selected?.label ?? ""}
116
+ </span>
117
+ </ScrollFade>
118
+ ) : (
119
+ <span className="block min-w-0 flex-1 whitespace-nowrap pr-2" title={selected?.label}>
120
+ {selected?.label ?? ""}
121
+ </span>
122
+ )
123
+ }
124
+ </SelectValue>
125
+ </SelectTrigger>
126
+ <SelectContent
127
+ align="end"
128
+ alignItemWithTrigger={false}
129
+ style={shouldConstrainPopup && popupMaxWidth ? { maxWidth: popupMaxWidth } : undefined}
130
+ >
131
+ <SelectGroup>
132
+ {options.map((item) => (
133
+ <SelectItem key={item.value} title={item.label} value={item.value}>
134
+ {item.label}
135
+ </SelectItem>
136
+ ))}
137
+ </SelectGroup>
138
+ </SelectContent>
139
+ </Select>
140
+ );
141
+ }
142
+
143
+ function SelectControlField({
144
+ layout = "auto",
145
+ name,
146
+ onValueChange,
147
+ options,
148
+ value,
149
+ }: SelectControlInput & {
150
+ layout?: SelectControlFieldLayout;
151
+ }): React.JSX.Element {
152
+ const fieldRef = React.useRef<HTMLDivElement>(null);
153
+ const popupMaxWidth = useMeasuredElementWidth(fieldRef);
154
+ const [currentValue, setCurrentValue] = React.useState(value);
155
+ const selected = options.find((option) => option.value === currentValue) ?? options[0];
156
+ const hasLongSelectedValue =
157
+ (selected?.label.length ?? 0) >= WIDE_SELECT_OPTION_LABEL_LENGTH;
158
+ const isStacked = layout === "stacked" || hasLongSelectedValue;
159
+
160
+ React.useEffect(() => {
161
+ setCurrentValue(value);
162
+ }, [value]);
163
+
164
+ function updateValue(nextValue: string): void {
165
+ setCurrentValue(nextValue);
166
+ onValueChange?.(nextValue);
167
+ }
168
+
169
+ return (
170
+ <Field
171
+ className={cn(
172
+ isStacked
173
+ ? "h-fit min-w-0 gap-2"
174
+ : compactHorizontalFieldClassName,
175
+ )}
176
+ orientation={isStacked ? "vertical" : "horizontal"}
177
+ ref={fieldRef}
178
+ >
179
+ <ControlFieldLabel>{name}</ControlFieldLabel>
180
+ <div
181
+ className={cn(
182
+ "min-w-0",
183
+ isStacked ? "w-full" : "w-1/2 shrink-0",
184
+ )}
185
+ >
186
+ <StaticSelect
187
+ onValueChange={updateValue}
188
+ options={options}
189
+ popupMaxWidth={popupMaxWidth}
190
+ value={currentValue}
191
+ />
192
+ </div>
193
+ </Field>
194
+ );
195
+ }
196
+
197
+ export function SelectControl(
198
+ props: SelectControlProps,
199
+ ): React.JSX.Element {
200
+ if (isSelectControlGroupProps(props)) {
201
+ const inputsPerRow = getInputsPerRow(props.inputsPerRow);
202
+
203
+ return (
204
+ <div
205
+ className="grid min-w-0 gap-x-2.5 gap-y-3"
206
+ data-slot="select-control-grid"
207
+ style={{
208
+ gridTemplateColumns: `repeat(${inputsPerRow}, minmax(0, 1fr))`,
209
+ }}
210
+ >
211
+ {props.inputs.map((input, index) => (
212
+ <SelectControlField
213
+ key={`${input.name}-${index}`}
214
+ layout="stacked"
215
+ {...input}
216
+ />
217
+ ))}
218
+ </div>
219
+ );
220
+ }
221
+
222
+ return <SelectControlField {...props} />;
223
+ }
@@ -0,0 +1,4 @@
1
+ "use client";
2
+
3
+ export { SliderControl } from "./slider-control";
4
+ export type { SliderControlProps } from "./slider-control";
@@ -0,0 +1,150 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import {
6
+ EditableSliderValueLabel,
7
+ Field,
8
+ getNumericValueLabelWidthReference,
9
+ Slider,
10
+ } from "../../primitives";
11
+ import { ControlFieldLabel } from "../../control-layout";
12
+ import { cn } from "../../../lib/utils";
13
+ import {
14
+ clampSliderValue,
15
+ applySliderValueLabelUnit,
16
+ formatSliderValueWithUnit,
17
+ getSliderControlValue,
18
+ parseSliderValueLabel,
19
+ } from "./slider-value";
20
+ import {
21
+ createControlHistoryGroupId,
22
+ type ControlChangeMeta,
23
+ type ControlValueChangeHandler,
24
+ } from "../control-types";
25
+
26
+ export type SliderControlProps = {
27
+ baseValue?: number;
28
+ className?: string;
29
+ disabled?: boolean;
30
+ markerCount?: number;
31
+ max?: number;
32
+ min?: number;
33
+ name: string;
34
+ onValueChange?: ControlValueChangeHandler<number>;
35
+ showFill?: boolean;
36
+ step?: number;
37
+ unit?: string;
38
+ value: number;
39
+ valueLabel?: string;
40
+ variant?: "continuous" | "discrete";
41
+ };
42
+
43
+ export function SliderControl({
44
+ baseValue,
45
+ className,
46
+ disabled = false,
47
+ markerCount,
48
+ max = 100,
49
+ min = 0,
50
+ name,
51
+ onValueChange,
52
+ showFill,
53
+ step = 1,
54
+ unit,
55
+ value,
56
+ valueLabel,
57
+ variant = "continuous",
58
+ }: SliderControlProps): React.JSX.Element {
59
+ const [currentValue, setCurrentValue] = React.useState(value);
60
+ const liveHistoryGroupRef = React.useRef<string | null>(null);
61
+
62
+ React.useEffect(() => {
63
+ setCurrentValue(value);
64
+ }, [value]);
65
+
66
+ const displayValueLabel =
67
+ valueLabel && currentValue === value
68
+ ? applySliderValueLabelUnit(valueLabel, unit)
69
+ : formatSliderValueWithUnit(currentValue, step, unit);
70
+
71
+ function getLiveHistoryMeta(): ControlChangeMeta {
72
+ liveHistoryGroupRef.current ??= createControlHistoryGroupId(`slider:${name}`);
73
+
74
+ return {
75
+ history: "merge",
76
+ historyGroup: liveHistoryGroupRef.current,
77
+ };
78
+ }
79
+
80
+ function finishLiveHistoryGroup(): void {
81
+ liveHistoryGroupRef.current = null;
82
+ }
83
+
84
+ function commitValue(nextValue: number, meta?: ControlChangeMeta): void {
85
+ const clampedValue = clampSliderValue(nextValue, min, max);
86
+
87
+ setCurrentValue(clampedValue);
88
+ onValueChange?.(clampedValue, meta);
89
+ }
90
+
91
+ function stepEditableValue(direction: -1 | 1, currentDraft: string): string | undefined {
92
+ const parsedDraftValue = parseSliderValueLabel(currentDraft);
93
+ const baseValue =
94
+ typeof parsedDraftValue === "number" ? parsedDraftValue : currentValue;
95
+ const nextValue = clampSliderValue(baseValue + direction * step, min, max);
96
+
97
+ commitValue(nextValue, getLiveHistoryMeta());
98
+
99
+ return formatSliderValueWithUnit(nextValue, step, unit);
100
+ }
101
+
102
+ return (
103
+ <Field className={cn("min-w-0 gap-1!", className)} data-disabled={disabled}>
104
+ <div className="flex w-full min-w-0 items-center justify-between gap-3">
105
+ <ControlFieldLabel>{name}</ControlFieldLabel>
106
+ <div className="inline-flex h-5 shrink-0 items-center gap-1.5">
107
+ <EditableSliderValueLabel
108
+ ariaLabel={`${name} value`}
109
+ disabled={disabled}
110
+ maxValueLabel={getNumericValueLabelWidthReference(
111
+ displayValueLabel,
112
+ { max, min },
113
+ )}
114
+ onCommit={(nextValueLabel) => {
115
+ const parsedValue = parseSliderValueLabel(nextValueLabel);
116
+
117
+ if (typeof parsedValue === "number") {
118
+ commitValue(parsedValue);
119
+ }
120
+
121
+ finishLiveHistoryGroup();
122
+ }}
123
+ onStep={stepEditableValue}
124
+ valueLabel={displayValueLabel}
125
+ />
126
+ </div>
127
+ </div>
128
+ <Slider
129
+ getAriaLabel={() => name}
130
+ markerCount={markerCount}
131
+ max={max}
132
+ min={min}
133
+ disabled={disabled}
134
+ onValueChange={(nextValue) => {
135
+ const resolvedValue = getSliderControlValue(nextValue);
136
+
137
+ if (typeof resolvedValue === "number") {
138
+ commitValue(resolvedValue, getLiveHistoryMeta());
139
+ }
140
+ }}
141
+ onValueCommitted={finishLiveHistoryGroup}
142
+ resetValue={typeof baseValue === "number" ? [baseValue] : undefined}
143
+ showFill={showFill}
144
+ step={step}
145
+ value={[currentValue]}
146
+ variant={variant}
147
+ />
148
+ </Field>
149
+ );
150
+ }
@@ -0,0 +1,56 @@
1
+ export function clampSliderValue(
2
+ value: number,
3
+ min: number,
4
+ max: number,
5
+ ): number {
6
+ return Math.min(max, Math.max(min, value));
7
+ }
8
+
9
+ export function getDecimalPrecision(step: number): number {
10
+ return String(step).split(".")[1]?.length ?? 0;
11
+ }
12
+
13
+ export function formatSliderValue(value: number, step: number): string {
14
+ const decimals = getDecimalPrecision(step);
15
+ const rounded = Number(value.toFixed(decimals));
16
+
17
+ return String(rounded);
18
+ }
19
+
20
+ export function applySliderValueLabelUnit(
21
+ valueLabel: string,
22
+ unit?: string,
23
+ ): string {
24
+ if (!unit || typeof parseSliderValueLabel(valueLabel) !== "number") {
25
+ return valueLabel;
26
+ }
27
+
28
+ return valueLabel.replaceAll(/-?\d+(?:\.\d+)?/g, (match, offset) => {
29
+ const textAfterMatch = valueLabel.slice(offset + match.length).trimStart();
30
+
31
+ return textAfterMatch.startsWith(unit) ? match : `${match}${unit}`;
32
+ });
33
+ }
34
+
35
+ export function formatSliderValueWithUnit(
36
+ value: number,
37
+ step: number,
38
+ unit?: string,
39
+ ): string {
40
+ return applySliderValueLabelUnit(formatSliderValue(value, step), unit);
41
+ }
42
+
43
+ export function parseSliderValueLabel(valueLabel: string): number | undefined {
44
+ const match = valueLabel.match(/-?\d+(?:\.\d+)?/);
45
+ const parsedValue = match ? Number.parseFloat(match[0]) : Number.NaN;
46
+
47
+ return Number.isFinite(parsedValue) ? parsedValue : undefined;
48
+ }
49
+
50
+ export function getSliderControlValue(
51
+ nextValue: number | readonly number[],
52
+ ): number | undefined {
53
+ const resolvedValue = Array.isArray(nextValue) ? nextValue[0] : nextValue;
54
+
55
+ return typeof resolvedValue === "number" ? resolvedValue : undefined;
56
+ }
@@ -0,0 +1,4 @@
1
+ "use client";
2
+
3
+ export { TextInputControl } from "./text-input-control";
4
+ export type { TextInputControlProps } from "./text-input-control";
@@ -0,0 +1,158 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ import { ControlFieldLabel } from "../../control-layout";
6
+ import { Field, Input } from "../../primitives";
7
+ import {
8
+ createControlHistoryGroupId,
9
+ type ControlChangeMeta,
10
+ type ControlValueChangeHandler,
11
+ } from "../control-types";
12
+
13
+ export type TextInputControlInput = {
14
+ commitOnBlur?: boolean;
15
+ defaultValue?: string;
16
+ name: string;
17
+ onValueChange?: ControlValueChangeHandler<string>;
18
+ value: string;
19
+ };
20
+
21
+ type TextInputControlSingleProps = TextInputControlInput & {
22
+ inputs?: never;
23
+ inputsPerRow?: never;
24
+ };
25
+
26
+ type TextInputControlGroupProps = {
27
+ inputs: readonly TextInputControlInput[];
28
+ inputsPerRow?: number;
29
+ };
30
+
31
+ export type TextInputControlProps =
32
+ | TextInputControlSingleProps
33
+ | TextInputControlGroupProps;
34
+
35
+ function isTextInputControlGroupProps(
36
+ props: TextInputControlProps,
37
+ ): props is TextInputControlGroupProps {
38
+ return Array.isArray((props as TextInputControlGroupProps).inputs);
39
+ }
40
+
41
+ function getInputsPerRow(inputsPerRow: number | undefined): number {
42
+ if (typeof inputsPerRow !== "number" || !Number.isFinite(inputsPerRow)) {
43
+ return 1;
44
+ }
45
+
46
+ return Math.max(1, Math.floor(inputsPerRow));
47
+ }
48
+
49
+ function TextInputControlField({
50
+ commitOnBlur = false,
51
+ defaultValue,
52
+ name,
53
+ onValueChange,
54
+ value,
55
+ }: TextInputControlInput): React.JSX.Element {
56
+ const [currentValue, setCurrentValue] = React.useState(value);
57
+ const valueRef = React.useRef(value);
58
+ const defaultValueRef = React.useRef(defaultValue ?? value);
59
+ const liveHistoryGroupRef = React.useRef<string | null>(null);
60
+
61
+ React.useEffect(() => {
62
+ valueRef.current = value;
63
+ setCurrentValue(value);
64
+ }, [value]);
65
+
66
+ React.useEffect(() => {
67
+ defaultValueRef.current = defaultValue ?? value;
68
+ }, [defaultValue, value]);
69
+
70
+ function getLiveHistoryMeta(): ControlChangeMeta {
71
+ liveHistoryGroupRef.current ??= createControlHistoryGroupId(`text:${name}`);
72
+
73
+ return {
74
+ history: "merge",
75
+ historyGroup: liveHistoryGroupRef.current,
76
+ };
77
+ }
78
+
79
+ function finishLiveHistoryGroup(): void {
80
+ liveHistoryGroupRef.current = null;
81
+ }
82
+
83
+ function updateValue(nextValue: string): void {
84
+ setCurrentValue(nextValue);
85
+
86
+ if (!commitOnBlur) {
87
+ onValueChange?.(nextValue, getLiveHistoryMeta());
88
+ }
89
+ }
90
+
91
+ function commitValue(nextValue = currentValue): void {
92
+ const committedValue =
93
+ nextValue.trim() === "" ? defaultValueRef.current : nextValue;
94
+
95
+ setCurrentValue(committedValue);
96
+
97
+ if (committedValue !== valueRef.current) {
98
+ onValueChange?.(committedValue);
99
+ }
100
+ }
101
+
102
+ return (
103
+ <Field className="min-w-0 gap-2">
104
+ <ControlFieldLabel>{name}</ControlFieldLabel>
105
+ <Input
106
+ className="font-mono"
107
+ onBlur={commitOnBlur ? () => commitValue() : finishLiveHistoryGroup}
108
+ onChange={(event) => updateValue(event.target.value)}
109
+ onKeyDown={
110
+ commitOnBlur
111
+ ? (event) => {
112
+ if (event.key === "Enter") {
113
+ event.preventDefault();
114
+ commitValue(event.currentTarget.value);
115
+ event.currentTarget.blur();
116
+ }
117
+
118
+ if (event.key === "Escape") {
119
+ event.preventDefault();
120
+ setCurrentValue(valueRef.current);
121
+ event.currentTarget.blur();
122
+ }
123
+ }
124
+ : undefined
125
+ }
126
+ size="default"
127
+ value={currentValue}
128
+ />
129
+ </Field>
130
+ );
131
+ }
132
+
133
+ export function TextInputControl(
134
+ props: TextInputControlProps,
135
+ ): React.JSX.Element {
136
+ if (isTextInputControlGroupProps(props)) {
137
+ const inputsPerRow = getInputsPerRow(props.inputsPerRow);
138
+
139
+ return (
140
+ <div
141
+ className="grid min-w-0 gap-2"
142
+ data-slot="text-input-control-grid"
143
+ style={{
144
+ gridTemplateColumns: `repeat(${inputsPerRow}, minmax(0, 1fr))`,
145
+ }}
146
+ >
147
+ {props.inputs.map((input, index) => (
148
+ <TextInputControlField
149
+ key={`${input.name}-${index}`}
150
+ {...input}
151
+ />
152
+ ))}
153
+ </div>
154
+ );
155
+ }
156
+
157
+ return <TextInputControlField {...props} />;
158
+ }
@@ -0,0 +1,42 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+
5
+ function getMeasuredWidth(element: HTMLElement): number | undefined {
6
+ const width = Math.round(element.getBoundingClientRect().width);
7
+
8
+ return width > 0 ? width : undefined;
9
+ }
10
+
11
+ export function useMeasuredElementWidth(
12
+ ref: React.RefObject<HTMLElement | null>,
13
+ ): number | undefined {
14
+ const [width, setWidth] = React.useState<number | undefined>(undefined);
15
+
16
+ React.useEffect(() => {
17
+ const element = ref.current;
18
+
19
+ if (!element) {
20
+ return undefined;
21
+ }
22
+
23
+ const updateWidth = () => {
24
+ setWidth(getMeasuredWidth(element));
25
+ };
26
+
27
+ updateWidth();
28
+
29
+ if (typeof ResizeObserver === "undefined") {
30
+ return undefined;
31
+ }
32
+
33
+ const observer = new ResizeObserver(updateWidth);
34
+ observer.observe(element);
35
+
36
+ return () => {
37
+ observer.disconnect();
38
+ };
39
+ }, [ref]);
40
+
41
+ return width;
42
+ }
@@ -0,0 +1,8 @@
1
+ "use client";
2
+
3
+ export { VectorControl } from "./vector-control";
4
+ export type {
5
+ VectorControlProps,
6
+ VectorControlValue,
7
+ VectorPadVariant,
8
+ } from "./vector-control";