@ctrl-ui/react 0.8.20 → 0.9.0

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 (161) hide show
  1. package/README.md +1 -0
  2. package/dist/action-bar.d.ts +1 -2
  3. package/dist/action-bar.js +7 -8
  4. package/dist/blocks/design-canvas-inspector.js +4 -1
  5. package/dist/blocks/file-explorer.js +1 -1
  6. package/dist/chat-message.js +4 -13
  7. package/dist/hooks/use-textarea-trigger-menu.d.ts +21 -3
  8. package/dist/hooks/use-textarea-trigger-menu.js +36 -40
  9. package/dist/hooks/use-user-ask.d.ts +1 -0
  10. package/dist/hooks/use-user-ask.js +5 -1
  11. package/dist/knob-contracts/audio-recorder-knobs.d.ts +1 -1
  12. package/dist/knob-contracts/audio-recorder-knobs.js +2 -1
  13. package/dist/knob-contracts/chart-knobs.d.ts +4 -0
  14. package/dist/knob-contracts/chart-knobs.js +24 -0
  15. package/dist/knob-contracts/choice-knobs.d.ts +1 -1
  16. package/dist/knob-contracts/choice-knobs.js +1 -0
  17. package/dist/knob-contracts/dockable-panel-knobs.d.ts +1 -1
  18. package/dist/knob-contracts/dockable-panel-knobs.js +2 -1
  19. package/dist/knob-contracts/emoji-picker-knobs.d.ts +1 -1
  20. package/dist/knob-contracts/emoji-picker-knobs.js +1 -0
  21. package/dist/knob-contracts/empty-knobs.d.ts +1 -1
  22. package/dist/knob-contracts/empty-knobs.js +2 -1
  23. package/dist/knob-contracts/icon-picker-knobs.d.ts +1 -1
  24. package/dist/knob-contracts/icon-picker-knobs.js +3 -1
  25. package/dist/knob-contracts/resizable-knobs.d.ts +1 -1
  26. package/dist/knob-contracts/resizable-knobs.js +2 -2
  27. package/dist/knob-contracts/sidebar-knobs.d.ts +1 -1
  28. package/dist/knob-contracts/sidebar-knobs.js +3 -1
  29. package/dist/knob-contracts/tabs-knobs.d.ts +1 -1
  30. package/dist/knob-contracts/tabs-knobs.js +2 -1
  31. package/dist/knob-contracts/tree-knobs.d.ts +1 -1
  32. package/dist/knob-contracts/tree-knobs.js +2 -1
  33. package/dist/lib/color.d.ts +2 -6
  34. package/dist/lib/color.js +45 -28
  35. package/dist/lib/contrast.d.ts +6 -0
  36. package/dist/lib/contrast.js +33 -1
  37. package/dist/lib/gradient.d.ts +17 -0
  38. package/dist/lib/gradient.js +69 -0
  39. package/dist/markdown-editor/drag-handle.js +73 -10
  40. package/dist/markdown-editor/drag-preview.d.ts +3 -0
  41. package/dist/markdown-editor/drag-preview.js +31 -0
  42. package/dist/markdown-editor/extensions.d.ts +2 -2
  43. package/dist/markdown-editor/extensions.js +25 -3
  44. package/dist/markdown-editor/suggestions.js +5 -5
  45. package/dist/markdown-editor/task-item.d.ts +3 -0
  46. package/dist/markdown-editor/task-item.js +50 -0
  47. package/dist/markdown-editor/toolbar.js +122 -63
  48. package/dist/markdown-editor.js +3 -3
  49. package/dist/skin.d.ts +0 -1
  50. package/dist/styles/index.css +3 -2
  51. package/dist/styles/recipes/action-bar.css +1 -1
  52. package/dist/styles/recipes/activity.css +1 -1
  53. package/dist/styles/recipes/alert.css +9 -10
  54. package/dist/styles/recipes/app-shell-header.css +2 -2
  55. package/dist/styles/recipes/audio-recorder.css +15 -4
  56. package/dist/styles/recipes/audio-visualizer.css +4 -4
  57. package/dist/styles/recipes/avatar.css +3 -3
  58. package/dist/styles/recipes/badge.css +2 -2
  59. package/dist/styles/recipes/button.css +2 -5
  60. package/dist/styles/recipes/card.css +0 -2
  61. package/dist/styles/recipes/chart.css +378 -0
  62. package/dist/styles/recipes/chat-composer-attachment.css +3 -3
  63. package/dist/styles/recipes/chat-composer.css +3 -3
  64. package/dist/styles/recipes/chat-layout.css +2 -2
  65. package/dist/styles/recipes/chat-message-social.css +1 -1
  66. package/dist/styles/recipes/chat-message.css +1 -1
  67. package/dist/styles/recipes/choice.css +9 -3
  68. package/dist/styles/recipes/code-diff.css +1 -1
  69. package/dist/styles/recipes/code.css +1 -1
  70. package/dist/styles/recipes/color-picker-details.css +17 -11
  71. package/dist/styles/recipes/color-picker-values.css +17 -7
  72. package/dist/styles/recipes/color-picker.css +88 -70
  73. package/dist/styles/recipes/context.css +2 -2
  74. package/dist/styles/recipes/dockable-panel.css +9 -2
  75. package/dist/styles/recipes/dynamic-notification-motion.css +1 -55
  76. package/dist/styles/recipes/dynamic-notification.css +0 -1
  77. package/dist/styles/recipes/emoji-picker.css +35 -19
  78. package/dist/styles/recipes/empty.css +19 -14
  79. package/dist/styles/recipes/field-parts.css +2 -2
  80. package/dist/styles/recipes/field.css +6 -4
  81. package/dist/styles/recipes/filter-bar.css +10 -10
  82. package/dist/styles/recipes/flow.css +5 -3
  83. package/dist/styles/recipes/gradient-editor.css +8 -0
  84. package/dist/styles/recipes/icon-picker.css +45 -10
  85. package/dist/styles/recipes/inline-attachment.css +4 -4
  86. package/dist/styles/recipes/inline-citation.css +2 -2
  87. package/dist/styles/recipes/item.css +4 -4
  88. package/dist/styles/recipes/markdown-editor.css +35 -24
  89. package/dist/styles/recipes/markdown.css +23 -27
  90. package/dist/styles/recipes/page-layout.css +13 -11
  91. package/dist/styles/recipes/popup-controls.css +2 -2
  92. package/dist/styles/recipes/popup-lists.css +3 -3
  93. package/dist/styles/recipes/popup.css +11 -9
  94. package/dist/styles/recipes/range.css +4 -5
  95. package/dist/styles/recipes/resizable.css +15 -10
  96. package/dist/styles/recipes/resize-handle.css +2 -2
  97. package/dist/styles/recipes/scroll-area.css +1 -1
  98. package/dist/styles/recipes/sidebar-menu.css +5 -15
  99. package/dist/styles/recipes/sidebar-mobile.css +4 -4
  100. package/dist/styles/recipes/sidebar-parts.css +26 -1
  101. package/dist/styles/recipes/sidebar.css +18 -31
  102. package/dist/styles/recipes/skeleton.css +10 -5
  103. package/dist/styles/recipes/stepper.css +1 -1
  104. package/dist/styles/recipes/switch.css +12 -11
  105. package/dist/styles/recipes/table-of-contents.css +6 -8
  106. package/dist/styles/recipes/table.css +4 -4
  107. package/dist/styles/recipes/tabs.css +10 -5
  108. package/dist/styles/recipes/task-list.css +3 -3
  109. package/dist/styles/recipes/tree.css +10 -3
  110. package/dist/styles/recipes/user-ask-details.css +5 -4
  111. package/dist/styles/recipes/user-ask.css +5 -5
  112. package/dist/styles/theme.css +158 -177
  113. package/dist/ui/chart-cartesian.d.ts +801 -0
  114. package/dist/ui/chart-cartesian.js +420 -0
  115. package/dist/ui/chart-colors.d.ts +7 -0
  116. package/dist/ui/chart-colors.js +27 -0
  117. package/dist/ui/chart-curves.d.ts +7 -0
  118. package/dist/ui/chart-curves.js +87 -0
  119. package/dist/ui/chart-marks.d.ts +68 -0
  120. package/dist/ui/chart-marks.js +227 -0
  121. package/dist/ui/chart-polar.d.ts +103 -0
  122. package/dist/ui/chart-polar.js +124 -0
  123. package/dist/ui/chart-series.d.ts +36 -0
  124. package/dist/ui/chart-series.js +71 -0
  125. package/dist/ui/chart.d.ts +32 -0
  126. package/dist/ui/chart.js +257 -0
  127. package/dist/ui/color-picker-area.d.ts +16 -0
  128. package/dist/ui/color-picker-area.js +175 -0
  129. package/dist/ui/color-picker-context.d.ts +27 -0
  130. package/dist/ui/color-picker-context.js +87 -0
  131. package/dist/ui/color-picker-contrast.d.ts +20 -0
  132. package/dist/ui/color-picker-contrast.js +129 -0
  133. package/dist/ui/color-picker-sliders.d.ts +15 -0
  134. package/dist/ui/color-picker-sliders.js +146 -0
  135. package/dist/ui/color-picker-swatches.d.ts +23 -0
  136. package/dist/ui/color-picker-swatches.js +94 -0
  137. package/dist/ui/color-picker-values.d.ts +59 -0
  138. package/dist/ui/color-picker-values.js +244 -0
  139. package/dist/ui/color-picker.d.ts +15 -110
  140. package/dist/ui/color-picker.js +30 -759
  141. package/dist/ui/emoji-picker.d.ts +10 -1
  142. package/dist/ui/emoji-picker.js +71 -14
  143. package/dist/ui/flow.js +1 -1
  144. package/dist/ui/gradient-editor-controls.d.ts +19 -0
  145. package/dist/ui/gradient-editor-controls.js +75 -0
  146. package/dist/ui/gradient-editor.d.ts +23 -22
  147. package/dist/ui/gradient-editor.js +77 -123
  148. package/dist/ui/icon-picker.d.ts +1 -1
  149. package/dist/ui/icon-picker.js +11 -6
  150. package/dist/ui/markdown-elements.d.ts +3 -1
  151. package/dist/ui/markdown-elements.js +62 -22
  152. package/dist/ui/markdown-heading.d.ts +9 -0
  153. package/dist/ui/markdown-heading.js +25 -0
  154. package/dist/ui/resizable.js +24 -9
  155. package/dist/ui/sidebar-provider.js +3 -12
  156. package/dist/ui/typography.d.ts +1 -0
  157. package/dist/ui/typography.js +12 -3
  158. package/dist/user-ask.js +4 -8
  159. package/package.json +7 -3
  160. package/dist/ui/emoji-icon-picker.d.ts +0 -13
  161. package/dist/ui/emoji-icon-picker.js +0 -50
@@ -1,4 +1,6 @@
1
1
  import { EmojiPickerKnobStyle } from "../knob-contracts/emoji-picker-knobs.js";
2
+ import { IconPicker } from "./icon-picker.js";
3
+ import { Tabs, TabsList } from "./tabs.js";
2
4
  import { CSSProperties, ComponentProps } from "react";
3
5
  import { Emoji, EmojiPicker as EmojiPicker$1 } from "frimousse";
4
6
  //#region src/ui/emoji-picker.d.ts
@@ -21,9 +23,16 @@ export declare function EmojiPickerRecent({ emojis, label, ...props }: EmojiPick
21
23
  export type EmojiPickerSearchProps = Omit<ComponentProps<typeof EmojiPicker$1.Search>, "style"> & EmojiPickerStyle;
22
24
  export declare function EmojiPickerSearch({ className, placeholder, "aria-label": ariaLabel, ...props }: EmojiPickerSearchProps): import("react").JSX.Element;
23
25
  export type EmojiPickerContentProps = Omit<ComponentProps<typeof EmojiPicker$1.Viewport>, "style"> & EmojiPickerStyle;
24
- export declare function EmojiPickerContent({ className, ref, ...props }: EmojiPickerContentProps): import("react").JSX.Element;
26
+ export declare function EmojiPickerContent({ className, ref, children, ...props }: EmojiPickerContentProps): import("react").JSX.Element;
25
27
  export type EmojiPickerFooterProps = Omit<ComponentProps<"div">, "style" | "children"> & EmojiPickerStyle & {
26
28
  placeholder?: string;
27
29
  };
28
30
  export declare function EmojiPickerFooter({ className, placeholder, ...props }: EmojiPickerFooterProps): import("react").JSX.Element;
31
+ export declare function EmojiIconPicker({ children, defaultValue, ...props }: ComponentProps<typeof Tabs<"emoji" | "icons">>): import("react").JSX.Element;
32
+ export declare function EmojiIconPickerTabs({ emojiLabel, iconsLabel, ...props }: Omit<ComponentProps<typeof TabsList>, "children"> & {
33
+ emojiLabel?: string;
34
+ iconsLabel?: string;
35
+ }): import("react").JSX.Element;
36
+ export declare function EmojiIconPickerEmoji(props: ComponentProps<typeof EmojiPicker>): import("react").JSX.Element;
37
+ export declare function EmojiIconPickerIcons(props: ComponentProps<typeof IconPicker>): import("react").JSX.Element;
29
38
  //#endregion
@@ -1,6 +1,10 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
+ import { ScrollArea } from "./scroll-area.js";
3
4
  import { Spinner } from "./spinner.js";
5
+ import { PopoverViewport } from "./popover.js";
6
+ import { IconPicker } from "./icon-picker.js";
7
+ import { Tabs, TabsList, TabsPanel, TabsTab } from "./tabs.js";
4
8
  import { Toolbar } from "@base-ui/react/toolbar";
5
9
  import { createContext, useContext, useImperativeHandle, useRef, useState } from "react";
6
10
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -16,6 +20,7 @@ function useEmojiPicker() {
16
20
  function EmojiPicker({ className, columns = 8, style, ref, onEmojiSelect, resolveEmojiData = defaultEmojiDataResolver, ...props }) {
17
21
  const rootRef = useRef(null);
18
22
  const viewportRef = useRef(null);
23
+ const listRef = useRef(null);
19
24
  const [loadState, setLoadState] = useState({ status: "loading" });
20
25
  const [retryAttempt, setRetryAttempt] = useState(0);
21
26
  useImperativeHandle(ref, () => {
@@ -50,6 +55,7 @@ function EmojiPicker({ className, columns = 8, style, ref, onEmojiSelect, resolv
50
55
  columns,
51
56
  rootRef,
52
57
  viewportRef,
58
+ listRef,
53
59
  loadState,
54
60
  retryEmojiData,
55
61
  selectEmoji
@@ -96,17 +102,18 @@ function emojiCategoryRows(emojiData, columns) {
96
102
  });
97
103
  }
98
104
  function EmojiPickerCategories({ "aria-label": ariaLabel = "Emoji categories", onKeyDown, ...props }) {
99
- const { columns, loadState, rootRef, viewportRef } = useEmojiPicker();
105
+ const { columns, loadState, rootRef, viewportRef, listRef } = useEmojiPicker();
100
106
  if (loadState.status !== "ready") return null;
101
107
  const categories = emojiCategoryRows(loadState.data, columns);
102
108
  const scrollToCategory = (startRow, categoryIndex) => {
103
109
  const root = rootRef.current;
104
110
  const viewport = viewportRef.current;
105
- if (!root || !viewport) return;
111
+ const list = listRef.current;
112
+ if (!root || !viewport || !list) return;
106
113
  const metrics = getComputedStyle(root);
107
114
  const rowHeight = Number.parseFloat(metrics.getPropertyValue("--frimousse-row-height"));
108
115
  const headerHeight = Number.parseFloat(metrics.getPropertyValue("--frimousse-category-header-height"));
109
- viewport.scrollTo({ top: startRow * rowHeight + categoryIndex * headerHeight });
116
+ viewport.scrollTo({ top: list.offsetTop + startRow * rowHeight + categoryIndex * headerHeight });
110
117
  };
111
118
  return /* @__PURE__ */ jsx(Toolbar.Root, {
112
119
  "data-control-ui": "emoji-picker",
@@ -204,7 +211,7 @@ function EmojiPickerRow({ children, className, ...props }) {
204
211
  "data-control-ui": "emoji-picker",
205
212
  "data-control-family": "emoji-picker",
206
213
  "data-slot": "row",
207
- className: cn("scroll-my-1", className),
214
+ className,
208
215
  ...props,
209
216
  children
210
217
  });
@@ -231,19 +238,23 @@ function EmojiPickerCategoryHeader({ category, className, ...props }) {
231
238
  children: category.label
232
239
  });
233
240
  }
234
- function EmojiPickerContent({ className, ref, ...props }) {
235
- const { viewportRef, loadState, retryEmojiData } = useEmojiPicker();
241
+ function EmojiPickerContent({ className, ref, children, ...props }) {
242
+ const { viewportRef, listRef, loadState, retryEmojiData } = useEmojiPicker();
236
243
  useImperativeHandle(ref, () => {
237
244
  if (!viewportRef.current) throw new Error("EmojiPicker viewport is not mounted.");
238
245
  return viewportRef.current;
239
246
  });
240
- return /* @__PURE__ */ jsx(EmojiPicker$1.Viewport, {
241
- "data-control-ui": "emoji-picker",
242
- "data-control-family": "emoji-picker",
243
- "data-slot": "content",
244
- className: cn("relative min-h-0 flex-1 outline-none", className),
245
- ...props,
246
- ref: viewportRef,
247
+ return /* @__PURE__ */ jsx(ScrollArea, {
248
+ lockAxis: "x",
249
+ className: cn("min-h-0 flex-1", className),
250
+ viewportRef,
251
+ viewportProps: {
252
+ "data-control-ui": "emoji-picker",
253
+ "data-control-family": "emoji-picker",
254
+ "data-slot": "content",
255
+ ...props,
256
+ render: renderEmojiViewport
257
+ },
247
258
  children: loadState.status === "error" ? /* @__PURE__ */ jsxs("div", {
248
259
  "data-control-ui": "emoji-picker",
249
260
  "data-control-family": "emoji-picker",
@@ -255,6 +266,7 @@ function EmojiPickerContent({ className, ref, ...props }) {
255
266
  children: "Try again"
256
267
  })]
257
268
  }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
269
+ children,
258
270
  /* @__PURE__ */ jsx(EmojiPicker$1.Loading, {
259
271
  "data-control-ui": "emoji-picker",
260
272
  "data-control-family": "emoji-picker",
@@ -274,6 +286,7 @@ function EmojiPickerContent({ className, ref, ...props }) {
274
286
  "data-control-family": "emoji-picker",
275
287
  "data-slot": "list",
276
288
  className: "select-none",
289
+ ref: listRef,
277
290
  components: {
278
291
  Row: EmojiPickerRow,
279
292
  Emoji: EmojiPickerEmoji,
@@ -283,6 +296,9 @@ function EmojiPickerContent({ className, ref, ...props }) {
283
296
  ] })
284
297
  });
285
298
  }
299
+ function renderEmojiViewport(viewportProps) {
300
+ return /* @__PURE__ */ jsx(EmojiPicker$1.Viewport, { ...viewportProps });
301
+ }
286
302
  function EmojiPickerFooter({ className, placeholder = "Pick an emoji…", ...props }) {
287
303
  return /* @__PURE__ */ jsx("div", {
288
304
  "data-control-ui": "emoji-picker",
@@ -311,5 +327,46 @@ function EmojiPickerFooter({ className, placeholder = "Pick an emoji…", ...pro
311
327
  }) })
312
328
  });
313
329
  }
330
+ function EmojiIconPicker({ children, defaultValue = "emoji", ...props }) {
331
+ return /* @__PURE__ */ jsx(Tabs, {
332
+ "data-control-ui": "emoji-icon-picker",
333
+ "data-control-family": "tabs",
334
+ "data-slot": "root",
335
+ defaultValue,
336
+ ...props,
337
+ children: /* @__PURE__ */ jsx(PopoverViewport, { children })
338
+ });
339
+ }
340
+ function EmojiIconPickerTabs({ emojiLabel = "Emoji", iconsLabel = "Icons", ...props }) {
341
+ return /* @__PURE__ */ jsxs(TabsList, {
342
+ size: "xs",
343
+ className: "mx-2 mt-2",
344
+ "aria-label": "Choose emoji or icon",
345
+ ...props,
346
+ children: [/* @__PURE__ */ jsx(TabsTab, {
347
+ value: "emoji",
348
+ children: emojiLabel
349
+ }), /* @__PURE__ */ jsx(TabsTab, {
350
+ value: "icons",
351
+ children: iconsLabel
352
+ })]
353
+ });
354
+ }
355
+ function EmojiIconPickerEmoji(props) {
356
+ return /* @__PURE__ */ jsx(TabsPanel, {
357
+ value: "emoji",
358
+ "data-slide": void 0,
359
+ className: "mt-0",
360
+ children: /* @__PURE__ */ jsx(EmojiPicker, { ...props })
361
+ });
362
+ }
363
+ function EmojiIconPickerIcons(props) {
364
+ return /* @__PURE__ */ jsx(TabsPanel, {
365
+ value: "icons",
366
+ "data-slide": void 0,
367
+ className: "mt-0",
368
+ children: /* @__PURE__ */ jsx(IconPicker, { ...props })
369
+ });
370
+ }
314
371
  //#endregion
315
- export { EmojiPicker, EmojiPickerCategories, EmojiPickerContent, EmojiPickerFooter, EmojiPickerReactions, EmojiPickerRecent, EmojiPickerSearch };
372
+ export { EmojiIconPicker, EmojiIconPickerEmoji, EmojiIconPickerIcons, EmojiIconPickerTabs, EmojiPicker, EmojiPickerCategories, EmojiPickerContent, EmojiPickerFooter, EmojiPickerReactions, EmojiPickerRecent, EmojiPickerSearch };
package/dist/ui/flow.js CHANGED
@@ -146,7 +146,7 @@ function FlowEdgeLabelChip({ className, ...props }) {
146
146
  "data-control-ui": "flow",
147
147
  "data-control-family": "flow",
148
148
  "data-slot": "edge-label-chip",
149
- className: cn("inline-flex max-w-40 items-center truncate", className)
149
+ className: cn("max-w-40 truncate", className)
150
150
  });
151
151
  }
152
152
  //#endregion
@@ -0,0 +1,19 @@
1
+ import { ControlSize } from "../control-variants.js";
2
+ //#region src/ui/gradient-editor-controls.d.ts
3
+ type GradientEditorSelectProps = {
4
+ size?: ControlSize;
5
+ className?: string;
6
+ "aria-label"?: string;
7
+ "aria-labelledby"?: string;
8
+ };
9
+ export type GradientEditorTypeSelectProps = GradientEditorSelectProps;
10
+ export type GradientEditorInterpolationSelectProps = GradientEditorSelectProps;
11
+ export type GradientEditorAngleProps = {
12
+ size?: ControlSize;
13
+ className?: string;
14
+ "aria-label"?: string;
15
+ };
16
+ export declare function GradientEditorTypeSelect(props: GradientEditorTypeSelectProps): import("react").JSX.Element;
17
+ export declare function GradientEditorInterpolationSelect(props: GradientEditorInterpolationSelectProps): import("react").JSX.Element;
18
+ export declare function GradientEditorAngle({ size, className, "aria-label": ariaLabel }: GradientEditorAngleProps): import("react").JSX.Element | null;
19
+ //#endregion
@@ -0,0 +1,75 @@
1
+ "use client";
2
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select.js";
3
+ import { NumberField, NumberFieldGroup, NumberFieldInput, NumberFieldScrubArea } from "./number-field.js";
4
+ import { useGradientEditor } from "./gradient-editor.js";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/ui/gradient-editor-controls.tsx
7
+ const GRADIENT_TYPE_LABELS = {
8
+ linear: "Linear",
9
+ radial: "Radial",
10
+ conic: "Conic"
11
+ };
12
+ const GRADIENT_INTERPOLATION_LABELS = {
13
+ srgb: "sRGB",
14
+ oklab: "OKLab",
15
+ oklch: "OKLCH"
16
+ };
17
+ const WHOLE_DEGREES = { maximumFractionDigits: 0 };
18
+ function GradientEditorSelect({ value, onValueChange, labels, defaultLabel, size = "sm", className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy }) {
19
+ return /* @__PURE__ */ jsxs(Select, {
20
+ value,
21
+ onValueChange,
22
+ items: labels,
23
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
24
+ size,
25
+ "aria-label": ariaLabelledBy === void 0 ? ariaLabel ?? defaultLabel : ariaLabel,
26
+ "aria-labelledby": ariaLabelledBy,
27
+ className,
28
+ children: /* @__PURE__ */ jsx(SelectValue, {})
29
+ }), /* @__PURE__ */ jsx(SelectContent, { children: Object.entries(labels).map(([option, label]) => /* @__PURE__ */ jsx(SelectItem, {
30
+ value: option,
31
+ children: label
32
+ }, option)) })]
33
+ });
34
+ }
35
+ function GradientEditorTypeSelect(props) {
36
+ const { type, setType } = useGradientEditor();
37
+ return /* @__PURE__ */ jsx(GradientEditorSelect, {
38
+ value: type,
39
+ onValueChange: setType,
40
+ labels: GRADIENT_TYPE_LABELS,
41
+ defaultLabel: "Gradient type",
42
+ ...props
43
+ });
44
+ }
45
+ function GradientEditorInterpolationSelect(props) {
46
+ const { interpolation, setInterpolation } = useGradientEditor();
47
+ return /* @__PURE__ */ jsx(GradientEditorSelect, {
48
+ value: interpolation,
49
+ onValueChange: setInterpolation,
50
+ labels: GRADIENT_INTERPOLATION_LABELS,
51
+ defaultLabel: "Gradient color space",
52
+ ...props
53
+ });
54
+ }
55
+ function GradientEditorAngle({ size = "sm", className, "aria-label": ariaLabel = "Gradient angle" }) {
56
+ const { type, angle, setAngle } = useGradientEditor();
57
+ if (type === "radial") return null;
58
+ return /* @__PURE__ */ jsx(NumberField, {
59
+ size,
60
+ className,
61
+ value: angle,
62
+ min: 0,
63
+ max: 360,
64
+ format: WHOLE_DEGREES,
65
+ onValueChange: (next) => {
66
+ if (next !== null) setAngle(next);
67
+ },
68
+ children: /* @__PURE__ */ jsxs(NumberFieldGroup, {
69
+ className: "w-full",
70
+ children: [/* @__PURE__ */ jsx(NumberFieldInput, { "aria-label": ariaLabel }), /* @__PURE__ */ jsx(NumberFieldScrubArea, { children: "°" })]
71
+ })
72
+ });
73
+ }
74
+ //#endregion
75
+ export { GradientEditorAngle, GradientEditorInterpolationSelect, GradientEditorTypeSelect };
@@ -1,19 +1,11 @@
1
- import { ControlSize } from "../control-variants.js";
2
1
  import { GradientEditorKnobStyle } from "../knob-contracts/gradient-editor-knobs.js";
3
- import { CSSProperties, ComponentProps } from "react";
2
+ import { GradientInterpolation, GradientStop, GradientType, GradientValue } from "../lib/gradient.js";
3
+ import { CSSProperties, ComponentProps, RefObject } from "react";
4
4
  //#region src/ui/gradient-editor.d.ts
5
- export type GradientType = "linear" | "radial" | "conic";
6
- export type GradientStop = {
7
- id: string;
8
- position: number;
9
- color: string;
10
- };
11
5
  export type GradientEditorProps = Omit<ComponentProps<"div">, "onChange" | "defaultValue"> & {
12
- value?: string;
13
- defaultStops?: GradientStop[];
14
- defaultType?: GradientType;
15
- defaultAngle?: number;
16
- onValueChange?: (value: string) => void;
6
+ value?: GradientValue;
7
+ defaultValue?: GradientValue;
8
+ onValueChange?: (value: GradientValue) => void;
17
9
  } & {
18
10
  style?: CSSProperties & GradientEditorKnobStyle;
19
11
  };
@@ -30,17 +22,26 @@ export type GradientEditorStopProps = Omit<ComponentProps<"button">, "style"> &
30
22
  export type GradientEditorStopAddProps = Omit<ComponentProps<"button">, "onClick" | "style"> & {
31
23
  style?: CSSProperties & GradientEditorKnobStyle;
32
24
  };
33
- export type GradientEditorTypeSelectProps = {
34
- size?: ControlSize;
35
- className?: string;
36
- "aria-label"?: string;
37
- "aria-labelledby"?: string;
25
+ type GradientEditorContextValue = GradientValue & {
26
+ gradient: string;
27
+ selectedStop: GradientStop | undefined;
28
+ canRemoveStop: boolean;
29
+ trackRef: RefObject<HTMLFieldSetElement | null>;
30
+ stopNodes: Map<string, HTMLButtonElement>;
31
+ select: (id: string) => void;
32
+ setStopColor: (id: string, color: string) => void;
33
+ setStopPosition: (id: string, position: number) => void;
34
+ addStop: (position: number) => void;
35
+ removeStop: (id: string) => void;
36
+ setType: (type: GradientType) => void;
37
+ setAngle: (angle: number) => void;
38
+ setInterpolation: (interpolation: GradientInterpolation) => void;
38
39
  };
39
- export declare function GradientEditor({ defaultStops, defaultType, defaultAngle, onValueChange, className, children, value: _value, ...props }: GradientEditorProps): import("react").JSX.Element;
40
+ export declare function useGradientEditor(): GradientEditorContextValue;
41
+ export declare function GradientEditor({ value: controlledValue, defaultValue, onValueChange, className, children, ...props }: GradientEditorProps): import("react").JSX.Element;
40
42
  export declare function GradientEditorPreview({ className, style, "aria-label": ariaLabel, ...props }: GradientEditorPreviewProps): import("react").JSX.Element;
41
43
  export declare function GradientEditorTrack({ className, children, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...props }: GradientEditorTrackProps): import("react").JSX.Element;
42
44
  export declare function GradientEditorStop({ stop, className, style, onPointerDown, onKeyDown, onFocus, onDoubleClick, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ref, ...props }: GradientEditorStopProps): import("react").JSX.Element;
43
45
  export declare function GradientEditorStopAdd({ className, children, ...props }: GradientEditorStopAddProps): import("react").JSX.Element;
44
- export declare function GradientEditorStopColor(): import("react").JSX.Element | null;
45
- export declare function GradientEditorTypeSelect({ size, className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy }: GradientEditorTypeSelectProps): import("react").JSX.Element;
46
- //#endregion
46
+ //#endregion
47
+ export type { GradientInterpolation, GradientStop, GradientType, GradientValue };
@@ -1,85 +1,86 @@
1
1
  "use client";
2
2
  import { cn } from "../lib/cn.js";
3
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select.js";
4
3
  import { useColorArea } from "../hooks/use-color-area.js";
5
- import { formatGradient } from "../lib/color.js";
6
- import { ColorPicker, ColorPickerAlpha, ColorPickerArea, ColorPickerContent, ColorPickerFormatSelect, ColorPickerHue, ColorPickerInput, ColorPickerTrigger } from "./color-picker.js";
4
+ import { formatGradient, gradientColorAt } from "../lib/gradient.js";
7
5
  import { createContext, useContext, useRef, useState } from "react";
8
- import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { jsx } from "react/jsx-runtime";
9
7
  //#region src/ui/gradient-editor.tsx
10
- const DEFAULT_STOPS = [{
11
- id: "stop-1",
12
- position: 0,
13
- color: "#7c3aed"
14
- }, {
15
- id: "stop-2",
16
- position: 1,
17
- color: "#3b82f6"
18
- }];
19
- const isGradientType = (v) => v === "linear" || v === "radial" || v === "conic";
8
+ const DEFAULT_GRADIENT = {
9
+ type: "linear",
10
+ angle: 90,
11
+ interpolation: "oklab",
12
+ stops: [{
13
+ id: "stop-1",
14
+ position: 0,
15
+ color: "#7c3aed"
16
+ }, {
17
+ id: "stop-2",
18
+ position: 1,
19
+ color: "#3b82f6"
20
+ }]
21
+ };
22
+ const MIN_GRADIENT_STOPS = 2;
20
23
  const clamp01 = (n) => Math.min(1, Math.max(0, n));
24
+ function unusedStopId(stops) {
25
+ const taken = new Set(stops.map((stop) => stop.id));
26
+ let ordinal = stops.length + 1;
27
+ while (taken.has(`stop-${ordinal}`)) ordinal += 1;
28
+ return `stop-${ordinal}`;
29
+ }
21
30
  const GradientEditorContext = createContext(null);
22
31
  function useGradientEditor() {
23
32
  const ctx = useContext(GradientEditorContext);
24
33
  if (!ctx) throw new Error("GradientEditor parts must be rendered inside <GradientEditor>.");
25
34
  return ctx;
26
35
  }
27
- function GradientEditor({ defaultStops = DEFAULT_STOPS, defaultType = "linear", defaultAngle = 90, onValueChange, className, children, value: _value, ...props }) {
28
- const [stops, setStops] = useState(defaultStops);
29
- const [type, setTypeState] = useState(defaultType);
30
- const [angle] = useState(defaultAngle);
31
- const [selectedId, setSelectedId] = useState(defaultStops[0]?.id ?? "stop-1");
32
- const idCounter = useRef(defaultStops.length);
36
+ function GradientEditor({ value: controlledValue, defaultValue = DEFAULT_GRADIENT, onValueChange, className, children, ...props }) {
37
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue);
38
+ const value = controlledValue ?? uncontrolledValue;
39
+ const { stops } = value;
40
+ const [selectedId, setSelectedId] = useState(stops[0]?.id);
33
41
  const trackRef = useRef(null);
34
42
  const [stopNodes] = useState(() => /* @__PURE__ */ new Map());
35
- const gradient = formatGradient(stops, type, angle);
36
- function updateStops(nextStops) {
37
- setStops(nextStops);
38
- onValueChange?.(formatGradient(nextStops, type, angle));
43
+ function change(patch) {
44
+ const next = {
45
+ ...value,
46
+ ...patch
47
+ };
48
+ if (controlledValue === void 0) setUncontrolledValue(next);
49
+ onValueChange?.(next);
39
50
  }
40
- const setStopColor = (id, color) => updateStops(stops.map((stop) => stop.id === id ? {
41
- ...stop,
42
- color
43
- } : stop));
44
- const setStopPosition = (id, position) => updateStops(stops.map((stop) => stop.id === id ? {
51
+ const canRemoveStop = stops.length > MIN_GRADIENT_STOPS;
52
+ const changeStop = (id, patch) => change({ stops: stops.map((stop) => stop.id === id ? {
45
53
  ...stop,
46
- position: clamp01(position)
47
- } : stop));
54
+ ...patch
55
+ } : stop) });
48
56
  const addStop = (position) => {
49
- idCounter.current += 1;
50
- const id = `stop-${idCounter.current}`;
51
- const near = [...stops].sort((a, b) => Math.abs(a.position - position) - Math.abs(b.position - position))[0];
52
- updateStops([...stops, {
57
+ const id = unusedStopId(stops);
58
+ const clamped = clamp01(position);
59
+ change({ stops: [...stops, {
53
60
  id,
54
- position: clamp01(position),
55
- color: near?.color ?? "#ffffff"
56
- }]);
61
+ position: clamped,
62
+ color: gradientColorAt(value, clamped)
63
+ }] });
57
64
  setSelectedId(id);
58
65
  };
59
66
  const removeStop = (id) => {
60
- if (stops.length <= 2) return;
61
- const nextStops = stops.filter((stop) => stop.id !== id);
62
- updateStops(nextStops);
63
- setSelectedId((current) => current === id ? nextStops[0]?.id ?? current : current);
64
- };
65
- const setType = (nextType) => {
66
- setTypeState(nextType);
67
- onValueChange?.(formatGradient(stops, nextType, angle));
67
+ if (canRemoveStop) change({ stops: stops.filter((stop) => stop.id !== id) });
68
68
  };
69
69
  const ctx = {
70
- stops,
71
- type,
72
- angle,
73
- selectedId,
74
- gradient,
70
+ ...value,
71
+ gradient: formatGradient(value),
72
+ selectedStop: stops.find((stop) => stop.id === selectedId) ?? stops[0],
73
+ canRemoveStop,
75
74
  trackRef,
76
75
  stopNodes,
77
76
  select: setSelectedId,
78
- setStopColor,
79
- setStopPosition,
77
+ setStopColor: (id, color) => changeStop(id, { color }),
78
+ setStopPosition: (id, position) => changeStop(id, { position: clamp01(position) }),
80
79
  addStop,
81
80
  removeStop,
82
- setType
81
+ setType: (type) => change({ type }),
82
+ setAngle: (angle) => change({ angle }),
83
+ setInterpolation: (interpolation) => change({ interpolation })
83
84
  };
84
85
  return /* @__PURE__ */ jsx(GradientEditorContext.Provider, {
85
86
  value: ctx,
@@ -95,6 +96,10 @@ function GradientEditor({ defaultStops = DEFAULT_STOPS, defaultType = "linear",
95
96
  }
96
97
  function GradientEditorPreview({ className, style, "aria-label": ariaLabel = "Gradient preview", ...props }) {
97
98
  const { gradient } = useGradientEditor();
99
+ const imageStyle = {
100
+ ...style,
101
+ "--_gradient-editor-image": gradient
102
+ };
98
103
  return /* @__PURE__ */ jsx("div", {
99
104
  "data-control-ui": "gradient-editor",
100
105
  "data-control-family": "gradient-editor",
@@ -102,16 +107,21 @@ function GradientEditorPreview({ className, style, "aria-label": ariaLabel = "Gr
102
107
  role: "img",
103
108
  "aria-label": ariaLabel,
104
109
  className: cn("w-full", className),
105
- style: {
106
- ...style,
107
- backgroundImage: gradient
108
- },
110
+ style: imageStyle,
109
111
  ...props
110
112
  });
111
113
  }
112
114
  function GradientEditorTrack({ className, children, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ...props }) {
113
- const { stops, trackRef, addStop } = useGradientEditor();
114
- const stripe = formatGradient(stops, "linear", 90);
115
+ const { stops, interpolation, trackRef, addStop } = useGradientEditor();
116
+ const imageStyle = {
117
+ ...style,
118
+ "--_gradient-editor-image": formatGradient({
119
+ type: "linear",
120
+ angle: 90,
121
+ interpolation,
122
+ stops
123
+ })
124
+ };
115
125
  return /* @__PURE__ */ jsx("fieldset", {
116
126
  ref: trackRef,
117
127
  "data-control-ui": "gradient-editor",
@@ -120,12 +130,9 @@ function GradientEditorTrack({ className, children, style, "aria-label": ariaLab
120
130
  "aria-label": ariaLabelledBy === void 0 ? ariaLabel ?? "Gradient stops" : ariaLabel,
121
131
  "aria-labelledby": ariaLabelledBy,
122
132
  className: cn("relative m-0 min-w-0 w-full cursor-copy touch-pan-y p-0", className),
123
- style: {
124
- ...style,
125
- backgroundImage: stripe
126
- },
133
+ style: imageStyle,
127
134
  onPointerDown: (event) => {
128
- if (event.target !== event.currentTarget) return;
135
+ if (!(event.target === event.currentTarget)) return;
129
136
  const rect = event.currentTarget.getBoundingClientRect();
130
137
  addStop((event.clientX - rect.left) / rect.width);
131
138
  },
@@ -134,9 +141,7 @@ function GradientEditorTrack({ className, children, style, "aria-label": ariaLab
134
141
  });
135
142
  }
136
143
  function GradientEditorStop({ stop, className, style, onPointerDown, onKeyDown, onFocus, onDoubleClick, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy, ref, ...props }) {
137
- const { stops, selectedId, select, setStopPosition, removeStop, trackRef, stopNodes } = useGradientEditor();
138
- const selected = stop.id === selectedId;
139
- const canRemove = stops.length > 2;
144
+ const { stops, selectedStop, canRemoveStop, select, setStopPosition, removeStop, trackRef, stopNodes } = useGradientEditor();
140
145
  const { onPointerDown: startDrag } = useColorArea((offset, rect) => setStopPosition(stop.id, offset.x / rect.width), trackRef);
141
146
  function handlePointerDown(event) {
142
147
  onPointerDown?.(event);
@@ -157,7 +162,7 @@ function GradientEditorStop({ stop, className, style, onPointerDown, onKeyDown,
157
162
  };
158
163
  }
159
164
  function remove() {
160
- if (!canRemove) return;
165
+ if (!canRemoveStop) return;
161
166
  const index = stops.findIndex((candidate) => candidate.id === stop.id);
162
167
  const neighbour = stops[index + 1] ?? stops[index - 1];
163
168
  if (neighbour) stopNodes.get(neighbour.id)?.focus();
@@ -200,7 +205,7 @@ function GradientEditorStop({ stop, className, style, onPointerDown, onKeyDown,
200
205
  break;
201
206
  case "Backspace":
202
207
  case "Delete":
203
- if (!canRemove) return;
208
+ if (!canRemoveStop) return;
204
209
  event.preventDefault();
205
210
  remove();
206
211
  }
@@ -218,7 +223,7 @@ function GradientEditorStop({ stop, className, style, onPointerDown, onKeyDown,
218
223
  "data-control-ui": "gradient-editor",
219
224
  "data-control-family": "gradient-editor",
220
225
  "data-slot": "stop",
221
- "data-selected": selected ? "true" : void 0,
226
+ "data-selected": stop.id === selectedStop?.id ? "true" : void 0,
222
227
  "aria-label": ariaLabelledBy === void 0 ? ariaLabel ?? defaultLabel : ariaLabel,
223
228
  "aria-labelledby": ariaLabelledBy,
224
229
  "aria-orientation": "horizontal",
@@ -262,56 +267,5 @@ function GradientEditorStopAdd({ className, children, ...props }) {
262
267
  })
263
268
  });
264
269
  }
265
- function GradientEditorStopColor() {
266
- const { stops, selectedId, setStopColor } = useGradientEditor();
267
- const selected = stops.find((s) => s.id === selectedId) ?? stops[0];
268
- if (!selected) return null;
269
- return /* @__PURE__ */ jsxs(ColorPicker, {
270
- value: selected.color,
271
- onValueChange: (color) => setStopColor(selected.id, color),
272
- defaultFormat: "hex",
273
- children: [/* @__PURE__ */ jsx(ColorPickerTrigger, {}), /* @__PURE__ */ jsxs(ColorPickerContent, { children: [
274
- /* @__PURE__ */ jsx(ColorPickerArea, {}),
275
- /* @__PURE__ */ jsx(ColorPickerHue, {}),
276
- /* @__PURE__ */ jsx(ColorPickerAlpha, {}),
277
- /* @__PURE__ */ jsxs("div", {
278
- "data-control-ui": "gradient-editor",
279
- "data-control-family": "gradient-editor",
280
- "data-slot": "value-row",
281
- className: "flex",
282
- children: [/* @__PURE__ */ jsx(ColorPickerFormatSelect, {}), /* @__PURE__ */ jsx(ColorPickerInput, { className: "flex-1" })]
283
- })
284
- ] })]
285
- });
286
- }
287
- function GradientEditorTypeSelect({ size = "sm", className, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledBy }) {
288
- const { type, setType } = useGradientEditor();
289
- return /* @__PURE__ */ jsxs(Select, {
290
- value: type,
291
- onValueChange: (value) => {
292
- if (isGradientType(value)) setType(value);
293
- },
294
- children: [/* @__PURE__ */ jsx(SelectTrigger, {
295
- size,
296
- "aria-label": ariaLabelledBy === void 0 ? ariaLabel ?? "Gradient type" : ariaLabel,
297
- "aria-labelledby": ariaLabelledBy,
298
- className,
299
- children: /* @__PURE__ */ jsx(SelectValue, {})
300
- }), /* @__PURE__ */ jsxs(SelectContent, { children: [
301
- /* @__PURE__ */ jsx(SelectItem, {
302
- value: "linear",
303
- children: "Linear"
304
- }),
305
- /* @__PURE__ */ jsx(SelectItem, {
306
- value: "radial",
307
- children: "Radial"
308
- }),
309
- /* @__PURE__ */ jsx(SelectItem, {
310
- value: "conic",
311
- children: "Conic"
312
- })
313
- ] })]
314
- });
315
- }
316
270
  //#endregion
317
- export { GradientEditor, GradientEditorPreview, GradientEditorStop, GradientEditorStopAdd, GradientEditorStopColor, GradientEditorTrack, GradientEditorTypeSelect };
271
+ export { GradientEditor, GradientEditorPreview, GradientEditorStop, GradientEditorStopAdd, GradientEditorTrack, useGradientEditor };
@@ -27,7 +27,7 @@ export declare function IconPickerSearch({ placeholder, "aria-label": ariaLabel,
27
27
  export type IconPickerContentProps = Omit<ComponentProps<"div">, "style" | "children"> & IconPickerStyle & {
28
28
  empty?: ReactNode;
29
29
  };
30
- export declare function IconPickerContent({ className, empty, "aria-label": ariaLabel, ...props }: IconPickerContentProps): import("react").JSX.Element;
30
+ export declare function IconPickerContent({ className, ref, empty, "aria-label": ariaLabel, ...props }: IconPickerContentProps): import("react").JSX.Element;
31
31
  export type IconPickerColorsProps = Omit<ComponentProps<"div">, "style" | "defaultValue" | "onChange" | "children"> & IconPickerStyle & ControlledChoice & {
32
32
  colors: readonly IconPickerColor[];
33
33
  };