@datalayer/primer-addons 1.0.20 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +14 -0
  2. package/lib/components/appearance/AppearanceMenuWithStore.d.ts +26 -0
  3. package/lib/components/appearance/AppearanceMenuWithStore.js +21 -0
  4. package/lib/components/appearance/CollaboratorPalette.d.ts +65 -0
  5. package/lib/components/appearance/CollaboratorPalette.js +160 -0
  6. package/lib/components/appearance/index.d.ts +2 -0
  7. package/lib/components/appearance/index.js +2 -0
  8. package/lib/components/box/Box.d.ts +17 -5
  9. package/lib/components/box/Box.js +3 -5
  10. package/lib/components/calendar-picker/CalendarPicker.d.ts +35 -0
  11. package/lib/components/calendar-picker/CalendarPicker.js +146 -0
  12. package/lib/components/calendar-picker/index.d.ts +1 -0
  13. package/lib/components/calendar-picker/index.js +5 -0
  14. package/lib/components/card/Card.d.ts +6 -1
  15. package/lib/components/card/Card.js +1 -1
  16. package/lib/components/color/ColorSwatch.d.ts +1 -1
  17. package/lib/components/color-picker/ColorPicker.d.ts +97 -0
  18. package/lib/components/color-picker/ColorPicker.js +358 -0
  19. package/lib/components/color-picker/index.d.ts +1 -0
  20. package/lib/components/color-picker/index.js +5 -0
  21. package/lib/components/content-loader/ContentLoader.d.ts +1 -1
  22. package/lib/components/content-loader/ContentLoader.stories.d.ts +1 -1
  23. package/lib/components/date-picker/DatePicker.d.ts +46 -0
  24. package/lib/components/date-picker/DatePicker.js +98 -0
  25. package/lib/components/date-picker/index.d.ts +1 -0
  26. package/lib/components/date-picker/index.js +5 -0
  27. package/lib/components/icons/CircleIcon.d.ts +1 -1
  28. package/lib/components/icons/CircleIcon.stories.d.ts +1 -1
  29. package/lib/components/index.d.ts +5 -0
  30. package/lib/components/index.js +5 -0
  31. package/lib/components/logo/DatalayerText.d.ts +1 -1
  32. package/lib/components/logo/index.d.ts +0 -1
  33. package/lib/components/logo/index.js +0 -1
  34. package/lib/components/side-overlay/SideOverlay.d.ts +5 -5
  35. package/lib/components/side-overlay/SideOverlay.stories.d.ts +1 -1
  36. package/lib/components/slider/Slider.d.ts +1 -1
  37. package/lib/components/slider/Slider.stories.d.ts +1 -1
  38. package/lib/components/sliding-panel/SlidingPanel.d.ts +1 -1
  39. package/lib/components/sliding-panel/SlidingPanel.stories.d.ts +1 -1
  40. package/lib/components/toolbar/Toolbar.d.ts +2 -2
  41. package/lib/components/toolbar/Toolbar.js +0 -0
  42. package/lib/components/toolbar/ToolbarButton.d.ts +3 -1
  43. package/lib/components/toolbar/ToolbarButton.js +54 -22
  44. package/lib/components/toolbar/ToolbarDivider.d.ts +1 -1
  45. package/lib/components/toolbar/ToolbarDropdown.d.ts +3 -3
  46. package/lib/components/toolbar/ToolbarDropdown.js +66 -35
  47. package/lib/components/toolbar/ToolbarRenderer.d.ts +20 -3
  48. package/lib/components/toolbar/ToolbarRenderer.js +16 -16
  49. package/lib/components/toolbar/types.d.ts +15 -8
  50. package/lib/reactor/AppearancePlugin.d.ts +31 -0
  51. package/lib/reactor/AppearancePlugin.js +63 -0
  52. package/lib/reactor/ThemePlugin.d.ts +5 -0
  53. package/lib/reactor/ThemePlugin.js +98 -0
  54. package/lib/reactor/index.d.ts +30 -0
  55. package/lib/reactor/index.js +34 -0
  56. package/lib/reactor/page-layout/PageLayout.d.ts +148 -0
  57. package/lib/reactor/page-layout/PageLayout.js +310 -0
  58. package/lib/reactor/page-layout/PageLayoutPlugin.d.ts +59 -0
  59. package/lib/reactor/page-layout/PageLayoutPlugin.js +105 -0
  60. package/lib/reactor/page-layout/PagePanelToggle.d.ts +21 -0
  61. package/lib/reactor/page-layout/PagePanelToggle.js +29 -0
  62. package/lib/reactor/page-layout/SlotPageLayout.d.ts +36 -0
  63. package/lib/reactor/page-layout/SlotPageLayout.js +25 -0
  64. package/lib/reactor/page-layout/panelState.d.ts +28 -0
  65. package/lib/reactor/page-layout/panelState.js +49 -0
  66. package/lib/story-helpers.d.ts +2 -2
  67. package/lib/theme/DatalayerBrandThemeProvider.d.ts +1 -1
  68. package/lib/theme/DatalayerThemeProvider.d.ts +5 -5
  69. package/lib/theme/DatalayerThemeProvider.js +119 -58
  70. package/lib/theme/css/createThemeCSSVars.d.ts +1 -0
  71. package/lib/theme/css/createThemeCSSVars.js +9 -0
  72. package/lib/theme/fontStacks.d.ts +2 -0
  73. package/lib/theme/fontStacks.js +6 -0
  74. package/lib/theme/index.d.ts +2 -0
  75. package/lib/theme/index.js +2 -0
  76. package/lib/theme/palettes/ColorPalette.d.ts +3 -3
  77. package/lib/theme/portableTheme.d.ts +32 -0
  78. package/lib/theme/portableTheme.js +45 -0
  79. package/lib/utils/Helper.d.ts +2 -2
  80. package/lib/utils/Portals.d.ts +15 -0
  81. package/lib/utils/Portals.js +83 -28
  82. package/lib/utils/Styles.d.ts +1 -1
  83. package/package.json +15 -14
  84. package/lib/components/logo/DI.d.ts +0 -19
  85. package/lib/components/logo/DI.js +0 -20
@@ -0,0 +1,97 @@
1
+ /**
2
+ * A colour, picked.
3
+ *
4
+ * A saturation-and-brightness field, a hue rail under it, a hex field and a
5
+ * row of presets — the shape a colour picker has had since Photoshop, drawn
6
+ * with Primer's form controls and Primer's custom properties so it sits in a
7
+ * Primer page without a stylesheet of its own.
8
+ *
9
+ * The chrome follows the theme (`--borderColor-default`, `--bgColor-muted`).
10
+ * The colour surfaces do not: a hue rail is the hue rail in every theme, and
11
+ * tinting it would lie about the colour being picked.
12
+ *
13
+ * Pointer events throughout, so a drag works the same with a mouse, a finger
14
+ * or a stylus, and keeps following the pointer outside the element.
15
+ *
16
+ * @module components/color-picker/ColorPicker
17
+ */
18
+ import type React from "react";
19
+ import { type ThemeVariant } from "../../theme/themeRegistry";
20
+ export interface RGB {
21
+ r: number;
22
+ g: number;
23
+ b: number;
24
+ }
25
+ export interface HSV {
26
+ /** 0–360 */
27
+ h: number;
28
+ /** 0–100 */
29
+ s: number;
30
+ /** 0–100 */
31
+ v: number;
32
+ }
33
+ /** `#rgb`, `#rrggbb` or `rgb(r, g, b)` as channels; black when unreadable. */
34
+ export declare function toRgb(input: string): RGB;
35
+ export declare function toHex({ r, g, b }: RGB): string;
36
+ export declare function rgbToHsv({ r, g, b }: RGB): HSV;
37
+ export declare function hsvToRgb({ h, s, v }: HSV): RGB;
38
+ /** Whether text reads as a colour this picker can hold. */
39
+ export declare function isColor(input: string): boolean;
40
+ /** Black or white, whichever stays legible on the colour given. */
41
+ export declare function readableOn(color: RGB): string;
42
+ /** A spread of hues and neutrals to start from. */
43
+ export declare const DEFAULT_COLOR_PRESETS: string[];
44
+ /**
45
+ * A named row of swatches under the field.
46
+ *
47
+ * More than one can be offered at a time — the theme's own colours, the
48
+ * colours a room gives its collaborators, a product's brand set — each under
49
+ * its own heading, so a person picking a colour can reach for one that
50
+ * already belongs somewhere rather than inventing a hex value.
51
+ */
52
+ export interface ColorPresetGroup {
53
+ /** The heading over the row. */
54
+ label: string;
55
+ /** The colours, as anything CSS accepts. */
56
+ colors: readonly string[];
57
+ }
58
+ /**
59
+ * The active theme's own colours, as a group.
60
+ *
61
+ * Surfaces and text first, then the semantic four, then the vivid palette the
62
+ * theme keeps for illustration — the order someone reaches for them in.
63
+ */
64
+ export declare function themeColorPresets(variant?: ThemeVariant, colorMode?: "light" | "dark" | "auto"): ColorPresetGroup;
65
+ /** The colours a room gives its collaborators, as a group. */
66
+ export declare function collaboratorColorPresets(variant?: ThemeVariant, colorMode?: "light" | "dark" | "auto"): ColorPresetGroup;
67
+ export interface ColorPickerProps {
68
+ /** The colour held, as `#rrggbb` or `rgb(r, g, b)`. */
69
+ color: string;
70
+ /** Told the new colour, as `#rrggbb`, while it is dragged and when it is set. */
71
+ onChange?: (color: string) => void;
72
+ /** The swatches offered under the field. {@link DEFAULT_COLOR_PRESETS} by default. */
73
+ presets?: readonly string[];
74
+ /** Hide the preset row. */
75
+ showPresets?: boolean;
76
+ /**
77
+ * Further rows under the presets, each with its own heading. Give it
78
+ * {@link themeColorPresets} or {@link collaboratorColorPresets}, or rows of
79
+ * your own.
80
+ */
81
+ presetGroups?: readonly ColorPresetGroup[];
82
+ /**
83
+ * Offer the active theme's own colours as a row. On by default: a colour
84
+ * picked in a themed page usually wants to be one of the theme's.
85
+ */
86
+ showThemeColors?: boolean;
87
+ /** Offer the collaborator colours as a row. Off by default. */
88
+ showCollaboratorColors?: boolean;
89
+ /** Hide the hex field. */
90
+ showInput?: boolean;
91
+ /** How wide the whole picker is, in pixels. */
92
+ width?: number;
93
+ /** The picker's accessible name. */
94
+ "aria-label"?: string;
95
+ }
96
+ export declare function ColorPicker({ color, onChange, presets, showPresets, presetGroups, showThemeColors, showCollaboratorColors, showInput, width, "aria-label": ariaLabel, }: ColorPickerProps): React.JSX.Element;
97
+ export default ColorPicker;
@@ -0,0 +1,358 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useId, useMemo, useRef, useState, } from "react";
3
+ import { Box, Text, TextInput } from "@primer/react";
4
+ import { getBrightPalette, themeConfigs, } from "../../theme/themeRegistry";
5
+ import { useThemeStore } from "../../theme/useThemeStore";
6
+ import { useSystemColorMode } from "../../theme/useSystemColorMode";
7
+ import { collaboratorColors } from "../appearance/CollaboratorPalette";
8
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
9
+ /** `#rgb`, `#rrggbb` or `rgb(r, g, b)` as channels; black when unreadable. */
10
+ export function toRgb(input) {
11
+ const text = input.trim();
12
+ const fromFunction = /^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)/i.exec(text);
13
+ if (fromFunction) {
14
+ const [, r, g, b] = fromFunction;
15
+ return { r: Number(r), g: Number(g), b: Number(b) };
16
+ }
17
+ let hex = text.replace(/^#/, "");
18
+ if (hex.length === 3) {
19
+ hex = hex
20
+ .split("")
21
+ .map((channel) => channel + channel)
22
+ .join("");
23
+ }
24
+ if (!/^[0-9a-f]{6}$/i.test(hex)) {
25
+ return { r: 0, g: 0, b: 0 };
26
+ }
27
+ return {
28
+ r: parseInt(hex.slice(0, 2), 16),
29
+ g: parseInt(hex.slice(2, 4), 16),
30
+ b: parseInt(hex.slice(4, 6), 16),
31
+ };
32
+ }
33
+ export function toHex({ r, g, b }) {
34
+ const channel = (value) => clamp(Math.round(value), 0, 255).toString(16).padStart(2, "0");
35
+ return `#${channel(r)}${channel(g)}${channel(b)}`;
36
+ }
37
+ export function rgbToHsv({ r, g, b }) {
38
+ const red = r / 255;
39
+ const green = g / 255;
40
+ const blue = b / 255;
41
+ const max = Math.max(red, green, blue);
42
+ const min = Math.min(red, green, blue);
43
+ const span = max - min;
44
+ let h = 0;
45
+ if (span !== 0) {
46
+ if (max === red) {
47
+ h = 60 * (((green - blue) / span) % 6);
48
+ }
49
+ else if (max === green) {
50
+ h = 60 * ((blue - red) / span + 2);
51
+ }
52
+ else {
53
+ h = 60 * ((red - green) / span + 4);
54
+ }
55
+ }
56
+ if (h < 0) {
57
+ h += 360;
58
+ }
59
+ return { h, s: max === 0 ? 0 : (span / max) * 100, v: max * 100 };
60
+ }
61
+ export function hsvToRgb({ h, s, v }) {
62
+ const saturation = s / 100;
63
+ const value = v / 100;
64
+ const chroma = value * saturation;
65
+ const second = chroma * (1 - Math.abs(((h / 60) % 2) - 1));
66
+ const base = value - chroma;
67
+ const sextant = Math.floor((((h % 360) + 360) % 360) / 60);
68
+ const [r, g, b] = [
69
+ [chroma, second, 0],
70
+ [second, chroma, 0],
71
+ [0, chroma, second],
72
+ [0, second, chroma],
73
+ [second, 0, chroma],
74
+ [chroma, 0, second],
75
+ ][sextant];
76
+ return {
77
+ r: Math.round((r + base) * 255),
78
+ g: Math.round((g + base) * 255),
79
+ b: Math.round((b + base) * 255),
80
+ };
81
+ }
82
+ /** Whether text reads as a colour this picker can hold. */
83
+ export function isColor(input) {
84
+ const text = input.trim();
85
+ return (/^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(text) ||
86
+ /^rgba?\(\s*\d+[,\s]+\d+[,\s]+\d+/i.test(text));
87
+ }
88
+ /** Black or white, whichever stays legible on the colour given. */
89
+ export function readableOn(color) {
90
+ // Rec. 601 luma, the usual quick test for "is this a light colour".
91
+ const luma = (color.r * 299 + color.g * 587 + color.b * 114) / 1000;
92
+ return luma > 150 ? "#000000" : "#ffffff";
93
+ }
94
+ /* ── The picker ────────────────────────────────────────────────────── */
95
+ /** A spread of hues and neutrals to start from. */
96
+ export const DEFAULT_COLOR_PRESETS = [
97
+ "#cf222e",
98
+ "#bc4c00",
99
+ "#9a6700",
100
+ "#1a7f37",
101
+ "#0969da",
102
+ "#8250df",
103
+ "#bf3989",
104
+ "#24292f",
105
+ "#57606a",
106
+ "#8c959f",
107
+ "#d0d7de",
108
+ "#ffffff",
109
+ ];
110
+ /**
111
+ * The active theme's own colours, as a group.
112
+ *
113
+ * Surfaces and text first, then the semantic four, then the vivid palette the
114
+ * theme keeps for illustration — the order someone reaches for them in.
115
+ */
116
+ export function themeColorPresets(variant = "datalayer", colorMode = "light") {
117
+ const bright = getBrightPalette(variant, colorMode);
118
+ const config = themeConfigs[variant] ?? themeConfigs.datalayer;
119
+ const mode = colorMode === "auto"
120
+ ? typeof window !== "undefined" &&
121
+ window.matchMedia("(prefers-color-scheme: dark)").matches
122
+ ? "dark"
123
+ : "light"
124
+ : colorMode;
125
+ const styles = (mode === "dark" ? config.themeStyles.dark : config.themeStyles.light);
126
+ const fromStyles = (name) => styles[name];
127
+ return {
128
+ label: config.label,
129
+ colors: [
130
+ fromStyles("--bgColor-default"),
131
+ fromStyles("--bgColor-muted"),
132
+ fromStyles("--fgColor-default"),
133
+ fromStyles("--fgColor-muted"),
134
+ fromStyles("--fgColor-accent"),
135
+ fromStyles("--fgColor-success"),
136
+ fromStyles("--fgColor-attention"),
137
+ fromStyles("--fgColor-danger"),
138
+ bright.glow,
139
+ bright.pop,
140
+ bright.spark,
141
+ bright.blaze,
142
+ bright.surge,
143
+ bright.flame,
144
+ bright.gold,
145
+ ].filter(Boolean),
146
+ };
147
+ }
148
+ /** The colours a room gives its collaborators, as a group. */
149
+ export function collaboratorColorPresets(variant = "datalayer", colorMode = "dark") {
150
+ return {
151
+ label: "Collaborators",
152
+ colors: collaboratorColors(variant, colorMode),
153
+ };
154
+ }
155
+ const FIELD_HEIGHT = 150;
156
+ const RAIL_HEIGHT = 12;
157
+ /**
158
+ * Follows a pointer across an element, in fractions of its own box.
159
+ *
160
+ * The listeners go on the window once a drag starts, so the pointer can leave
161
+ * the element — down the page, out of the window — without the handle being
162
+ * dropped where it was last seen.
163
+ */
164
+ function useDrag(onMove) {
165
+ const ref = useRef(null);
166
+ const moving = useRef(false);
167
+ const report = useCallback((event) => {
168
+ const element = ref.current;
169
+ if (!element) {
170
+ return;
171
+ }
172
+ const box = element.getBoundingClientRect();
173
+ onMove(clamp((event.clientX - box.left) / box.width, 0, 1), clamp((event.clientY - box.top) / box.height, 0, 1));
174
+ }, [onMove]);
175
+ useEffect(() => {
176
+ const move = (event) => moving.current && report(event);
177
+ const up = () => {
178
+ moving.current = false;
179
+ };
180
+ window.addEventListener("pointermove", move);
181
+ window.addEventListener("pointerup", up);
182
+ window.addEventListener("pointercancel", up);
183
+ return () => {
184
+ window.removeEventListener("pointermove", move);
185
+ window.removeEventListener("pointerup", up);
186
+ window.removeEventListener("pointercancel", up);
187
+ };
188
+ }, [report]);
189
+ const onPointerDown = (event) => {
190
+ moving.current = true;
191
+ report(event);
192
+ };
193
+ return { ref, onPointerDown };
194
+ }
195
+ export function ColorPicker({ color, onChange, presets = DEFAULT_COLOR_PRESETS, showPresets = true, presetGroups, showThemeColors = true, showCollaboratorColors = false, showInput = true, width = 232, "aria-label": ariaLabel = "Colour picker", }) {
196
+ const [hsv, setHsv] = useState(() => rgbToHsv(toRgb(color)));
197
+ const [text, setText] = useState(() => toHex(toRgb(color)));
198
+ /* A drag reports a colour, which comes back as a prop; without this the
199
+ answer would overwrite the hue whenever grey is picked (grey has none). */
200
+ const own = useRef(false);
201
+ useEffect(() => {
202
+ if (own.current) {
203
+ own.current = false;
204
+ return;
205
+ }
206
+ const rgb = toRgb(color);
207
+ setHsv(rgbToHsv(rgb));
208
+ setText(toHex(rgb));
209
+ }, [color]);
210
+ const rgb = useMemo(() => hsvToRgb(hsv), [hsv]);
211
+ const hex = useMemo(() => toHex(rgb), [rgb]);
212
+ const announce = useCallback((next) => {
213
+ own.current = true;
214
+ setHsv(next);
215
+ const value = toHex(hsvToRgb(next));
216
+ setText(value);
217
+ onChange?.(value);
218
+ }, [onChange]);
219
+ const field = useDrag(useCallback((x, y) => announce({ ...hsv, s: x * 100, v: (1 - y) * 100 }), [announce, hsv]));
220
+ const rail = useDrag(useCallback((x) => announce({ ...hsv, h: x * 360 }), [announce, hsv]));
221
+ /*
222
+ The rows under the field: the caller's presets, then the theme's own
223
+ colours, then the collaborators' — each only when asked for, and each
224
+ read from whichever theme is on rather than from a fixed list.
225
+ */
226
+ const { theme: activeTheme, colorMode: activeMode } = useThemeStore();
227
+ const systemMode = useSystemColorMode();
228
+ const resolvedMode = activeMode === "auto" ? systemMode : activeMode;
229
+ const rows = useMemo(() => {
230
+ const groups = [];
231
+ if (showPresets && presets.length > 0) {
232
+ groups.push({ label: "Presets", colors: presets });
233
+ }
234
+ if (showThemeColors) {
235
+ groups.push(themeColorPresets(activeTheme, resolvedMode));
236
+ }
237
+ if (showCollaboratorColors) {
238
+ groups.push(collaboratorColorPresets(activeTheme, resolvedMode));
239
+ }
240
+ return groups.concat(presetGroups ?? []);
241
+ }, [
242
+ activeTheme,
243
+ presetGroups,
244
+ presets,
245
+ resolvedMode,
246
+ showCollaboratorColors,
247
+ showPresets,
248
+ showThemeColors,
249
+ ]);
250
+ /* Each row's heading names its own swatches, so the ids must not collide. */
251
+ const rowIdPrefix = useId();
252
+ const setFromText = (entry) => {
253
+ setText(entry);
254
+ if (isColor(entry)) {
255
+ const next = rgbToHsv(toRgb(entry));
256
+ own.current = true;
257
+ setHsv(next);
258
+ onChange?.(toHex(toRgb(entry)));
259
+ }
260
+ };
261
+ const nudge = (event, axis) => {
262
+ const step = event.shiftKey ? 10 : 1;
263
+ const back = event.key === "ArrowLeft" || event.key === "ArrowDown";
264
+ const forward = event.key === "ArrowRight" || event.key === "ArrowUp";
265
+ if (!back && !forward) {
266
+ return;
267
+ }
268
+ event.preventDefault();
269
+ const by = forward ? step : -step;
270
+ const limit = axis === "h" ? 360 : 100;
271
+ announce({ ...hsv, [axis]: clamp(hsv[axis] + by, 0, limit) });
272
+ };
273
+ const handle = {
274
+ position: "absolute",
275
+ width: 14,
276
+ height: 14,
277
+ borderRadius: "50%",
278
+ border: "2px solid #ffffff",
279
+ boxShadow: "0 0 0 1px rgba(0, 0, 0, 0.35)",
280
+ transform: "translate(-50%, -50%)",
281
+ pointerEvents: "none",
282
+ };
283
+ return (_jsxs(Box, { role: "group", "aria-label": ariaLabel, sx: { display: "grid", gap: 2, width, minWidth: 0 }, children: [_jsx(Box, { ref: field.ref, role: "slider", tabIndex: 0, "aria-label": "Saturation and brightness", "aria-valuetext": `saturation ${Math.round(hsv.s)}%, brightness ${Math.round(hsv.v)}%`, onPointerDown: field.onPointerDown, onKeyDown: (event) => nudge(event, event.key === "ArrowUp" || event.key === "ArrowDown" ? "v" : "s"), sx: {
284
+ position: "relative",
285
+ height: FIELD_HEIGHT,
286
+ borderRadius: 2,
287
+ border: "1px solid",
288
+ borderColor: "var(--borderColor-default)",
289
+ cursor: "crosshair",
290
+ touchAction: "none",
291
+ backgroundColor: toHex(hsvToRgb({ h: hsv.h, s: 100, v: 100 })),
292
+ backgroundImage: "linear-gradient(to top, #000, transparent), linear-gradient(to right, #fff, transparent)",
293
+ "&:focus-visible": {
294
+ outline: "2px solid var(--borderColor-accent-emphasis)",
295
+ outlineOffset: "1px",
296
+ },
297
+ }, children: _jsx(Box, { sx: {
298
+ ...handle,
299
+ left: `${hsv.s}%`,
300
+ top: `${100 - hsv.v}%`,
301
+ backgroundColor: hex,
302
+ } }) }), _jsx(Box, { ref: rail.ref, role: "slider", tabIndex: 0, "aria-label": "Hue", "aria-valuemin": 0, "aria-valuemax": 360, "aria-valuenow": Math.round(hsv.h), onPointerDown: rail.onPointerDown, onKeyDown: (event) => nudge(event, "h"), sx: {
303
+ position: "relative",
304
+ height: RAIL_HEIGHT,
305
+ borderRadius: 6,
306
+ border: "1px solid",
307
+ borderColor: "var(--borderColor-default)",
308
+ cursor: "ew-resize",
309
+ touchAction: "none",
310
+ backgroundImage: "linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)",
311
+ "&:focus-visible": {
312
+ outline: "2px solid var(--borderColor-accent-emphasis)",
313
+ outlineOffset: "1px",
314
+ },
315
+ }, children: _jsx(Box, { sx: {
316
+ ...handle,
317
+ left: `${(hsv.h / 360) * 100}%`,
318
+ top: "50%",
319
+ backgroundColor: toHex(hsvToRgb({ h: hsv.h, s: 100, v: 100 })),
320
+ } }) }), showInput ? (_jsxs(Box, { sx: { display: "flex", gap: 2, alignItems: "center" }, children: [_jsx(Box, { "aria-hidden": "true", sx: {
321
+ width: 28,
322
+ height: 28,
323
+ flex: "0 0 auto",
324
+ borderRadius: 2,
325
+ border: "1px solid",
326
+ borderColor: "var(--borderColor-default)",
327
+ backgroundColor: hex,
328
+ } }), _jsx(TextInput, { "aria-label": "Colour, as hex", value: text, onChange: (event) => setFromText(event.target.value), onBlur: () => setText(hex), size: "small", sx: { flex: "1 1 auto", fontFamily: "mono" }, validationStatus: isColor(text) ? undefined : "error" })] })) : null, rows.map((row) => (_jsxs(Box, { children: [_jsx(Text, { id: `${rowIdPrefix}-${row.label}`, sx: {
329
+ fontSize: 0,
330
+ color: "var(--fgColor-muted)",
331
+ display: "block",
332
+ mb: 1,
333
+ }, children: row.label }), _jsx(Box, { role: "group", "aria-labelledby": `${rowIdPrefix}-${row.label}`, sx: {
334
+ display: "grid",
335
+ gridTemplateColumns: "repeat(auto-fill, minmax(22px, 1fr))",
336
+ gap: 1,
337
+ }, children: row.colors.map((preset, index) => {
338
+ const chosen = preset.toLowerCase() === hex.toLowerCase();
339
+ return (_jsx(Box, { as: "button", type: "button", "aria-label": preset, "aria-pressed": chosen, onClick: () => setFromText(preset), sx: {
340
+ height: 22,
341
+ borderRadius: 2,
342
+ cursor: "pointer",
343
+ backgroundColor: preset,
344
+ border: "1px solid",
345
+ borderColor: chosen
346
+ ? "var(--borderColor-accent-emphasis)"
347
+ : "var(--borderColor-default)",
348
+ boxShadow: chosen
349
+ ? "inset 0 0 0 2px var(--bgColor-default)"
350
+ : "none",
351
+ "&:focus-visible": {
352
+ outline: "2px solid var(--borderColor-accent-emphasis)",
353
+ outlineOffset: "1px",
354
+ },
355
+ } }, `${preset}-${index}`));
356
+ }) })] }, row.label)))] }));
357
+ }
358
+ export default ColorPicker;
@@ -0,0 +1 @@
1
+ export * from "./ColorPicker";
@@ -0,0 +1,5 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export * from "./ColorPicker";
@@ -2,5 +2,5 @@ import 'react-loading-skeleton/dist/skeleton.css';
2
2
  export interface ContentLoaderProps {
3
3
  count?: number;
4
4
  }
5
- export declare const ContentLoader: (props: ContentLoaderProps) => import("react/jsx-runtime").JSX.Element;
5
+ export declare const ContentLoader: (props: ContentLoaderProps) => import("react").JSX.Element;
6
6
  export default ContentLoader;
@@ -2,7 +2,7 @@ import type { StoryObj } from '@storybook/react';
2
2
  import { ContentLoaderProps } from '../..';
3
3
  declare const meta: {
4
4
  title: string;
5
- component: (props: ContentLoaderProps) => import("react/jsx-runtime").JSX.Element;
5
+ component: (props: ContentLoaderProps) => import("react").JSX.Element;
6
6
  tags: string[];
7
7
  parameters: {
8
8
  layout: string;
@@ -0,0 +1,46 @@
1
+ import { type CalendarPickerProps } from "../calendar-picker/CalendarPicker";
2
+ /** `YYYY-MM-DD`, the one format that means the same thing everywhere. */
3
+ export declare function formatISODate(date: Date): string;
4
+ /**
5
+ * Reads `YYYY-MM-DD` as a local date.
6
+ *
7
+ * `new Date('2026-01-31')` is UTC midnight, which is the day before in the
8
+ * Americas; the parts are given to the constructor separately so the date
9
+ * means the same day in every zone.
10
+ */
11
+ export declare function parseISODate(text: string): Date | null;
12
+ export interface DatePickerProps extends Pick<CalendarPickerProps, "min" | "max" | "isDisabled" | "weekStartsOn" | "locale"> {
13
+ /** The chosen date, or nothing chosen. */
14
+ value?: Date | null;
15
+ /** Told when a date is picked or typed. `null` when the field is emptied. */
16
+ onChange?: (date: Date | null) => void;
17
+ /** What the empty field says. */
18
+ placeholder?: string;
19
+ /** The field's accessible name. */
20
+ "aria-label"?: string;
21
+ /** Nothing can be picked or typed. */
22
+ disabled?: boolean;
23
+ /** The field's width. Primer's `medium` by default. */
24
+ size?: "small" | "medium" | "large";
25
+ /**
26
+ * Marks the field as holding something invalid, or as settled. Primer's own
27
+ * two, spelled out rather than imported from a path inside the package that
28
+ * not every consumer's module resolution can see.
29
+ */
30
+ validationStatus?: "error" | "success";
31
+ /**
32
+ * How a chosen date is written into the field. ISO `YYYY-MM-DD` by
33
+ * default, which is also the only shape the field parses back.
34
+ */
35
+ format?: (date: Date) => string;
36
+ /** An id for the field, for a label that names it from outside. */
37
+ id?: string;
38
+ }
39
+ /**
40
+ * A date field with a calendar behind it.
41
+ *
42
+ * Controlled: `value` is the caller's, and every pick and every parsed entry
43
+ * arrives through `onChange`.
44
+ */
45
+ export declare function DatePicker({ value, onChange, placeholder, "aria-label": ariaLabel, disabled, size, validationStatus, format, id, min, max, isDisabled, weekStartsOn, locale, }: DatePickerProps): import("react").JSX.Element;
46
+ export default DatePicker;
@@ -0,0 +1,98 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from "react";
3
+ import { AnchoredOverlay, Box, TextInput } from "@primer/react";
4
+ import { CalendarIcon } from "@primer/octicons-react";
5
+ import { CalendarPicker } from "../calendar-picker/CalendarPicker";
6
+ /** `YYYY-MM-DD`, the one format that means the same thing everywhere. */
7
+ export function formatISODate(date) {
8
+ const month = `${date.getMonth() + 1}`.padStart(2, "0");
9
+ const day = `${date.getDate()}`.padStart(2, "0");
10
+ return `${date.getFullYear()}-${month}-${day}`;
11
+ }
12
+ /**
13
+ * Reads `YYYY-MM-DD` as a local date.
14
+ *
15
+ * `new Date('2026-01-31')` is UTC midnight, which is the day before in the
16
+ * Americas; the parts are given to the constructor separately so the date
17
+ * means the same day in every zone.
18
+ */
19
+ export function parseISODate(text) {
20
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text.trim());
21
+ if (!match) {
22
+ return null;
23
+ }
24
+ const [, year, month, day] = match;
25
+ const date = new Date(Number(year), Number(month) - 1, Number(day));
26
+ return Number.isNaN(date.getTime()) ? null : date;
27
+ }
28
+ /**
29
+ * A date field with a calendar behind it.
30
+ *
31
+ * Controlled: `value` is the caller's, and every pick and every parsed entry
32
+ * arrives through `onChange`.
33
+ */
34
+ export function DatePicker({ value = null, onChange, placeholder = "YYYY-MM-DD", "aria-label": ariaLabel = "Date", disabled = false, size = "medium", validationStatus, format = formatISODate, id, min, max, isDisabled, weekStartsOn, locale, }) {
35
+ const [open, setOpen] = useState(false);
36
+ const [text, setText] = useState(() => (value ? format(value) : ""));
37
+ const anchorRef = useRef(null);
38
+ // The field follows the value, except while it is being typed into: a
39
+ // controlled rewrite mid-word moves the caret to the end.
40
+ const typing = useRef(false);
41
+ useEffect(() => {
42
+ if (!typing.current) {
43
+ setText(value ? format(value) : "");
44
+ }
45
+ }, [value, format]);
46
+ const commit = (next) => {
47
+ typing.current = false;
48
+ const trimmed = next.trim();
49
+ if (trimmed === "") {
50
+ onChange?.(null);
51
+ return;
52
+ }
53
+ const parsed = parseISODate(trimmed);
54
+ if (parsed) {
55
+ onChange?.(parsed);
56
+ setText(format(parsed));
57
+ }
58
+ else {
59
+ // Unreadable: put back what is actually held, rather than keeping a
60
+ // date on screen that the caller never received.
61
+ setText(value ? format(value) : "");
62
+ }
63
+ };
64
+ const calendar = useMemo(() => (_jsx(Box, { sx: { p: 3 }, children: _jsx(CalendarPicker, { value: value, defaultMonth: value ?? undefined, onChange: (date) => {
65
+ onChange?.(date);
66
+ setText(format(date));
67
+ setOpen(false);
68
+ }, min: min, max: max, isDisabled: isDisabled, weekStartsOn: weekStartsOn, locale: locale, "aria-label": `${ariaLabel}, choose a day` }) })), [
69
+ ariaLabel,
70
+ format,
71
+ isDisabled,
72
+ locale,
73
+ max,
74
+ min,
75
+ onChange,
76
+ value,
77
+ weekStartsOn,
78
+ ]);
79
+ return (_jsxs(Box, { ref: anchorRef, sx: { display: "inline-block" }, children: [_jsx(AnchoredOverlay, { open: open, onOpen: () => !disabled && setOpen(true), onClose: () => setOpen(false),
80
+ // The field's own box is the anchor; `useRef` types its current as
81
+ // nullable, which is not how the overlay declares what it takes.
82
+ anchorRef: anchorRef,
83
+ // The field is the anchor; the button inside it does the opening.
84
+ renderAnchor: null, overlayProps: { role: "dialog", "aria-label": `${ariaLabel} calendar` }, children: calendar }), _jsx(TextInput, { id: id, "aria-label": ariaLabel, "aria-haspopup": "dialog", "aria-expanded": open, value: text, placeholder: placeholder, disabled: disabled, size: size, validationStatus: validationStatus, onChange: (event) => {
85
+ typing.current = true;
86
+ setText(event.target.value);
87
+ }, onBlur: (event) => commit(event.target.value), onKeyDown: (event) => {
88
+ if (event.key === "Enter") {
89
+ commit(event.target.value);
90
+ setOpen(false);
91
+ }
92
+ if (event.key === "ArrowDown" && !open) {
93
+ event.preventDefault();
94
+ setOpen(true);
95
+ }
96
+ }, trailingAction: _jsx(TextInput.Action, { onClick: () => !disabled && setOpen((was) => !was), icon: CalendarIcon, "aria-label": open ? "Close the calendar" : "Open the calendar", disabled: disabled }) })] }));
97
+ }
98
+ export default DatePicker;
@@ -0,0 +1 @@
1
+ export * from "./DatePicker";
@@ -0,0 +1,5 @@
1
+ /*
2
+ * Copyright (c) 2023-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ export * from "./DatePicker";
@@ -2,5 +2,5 @@ export type CircleIconProps = {
2
2
  color?: string;
3
3
  variant?: 'fg' | 'default' | 'muted' | 'onEmphasis' | 'subtle';
4
4
  };
5
- export declare const CircleIcon: ({ color, variant, }: CircleIconProps) => import("react/jsx-runtime").JSX.Element;
5
+ export declare const CircleIcon: ({ color, variant, }: CircleIconProps) => import("react").JSX.Element;
6
6
  export default CircleIcon;
@@ -2,7 +2,7 @@ import type { StoryObj } from '@storybook/react';
2
2
  import { CircleIconProps } from '../..';
3
3
  declare const meta: {
4
4
  title: string;
5
- component: ({ color, variant, }: CircleIconProps) => import("react/jsx-runtime").JSX.Element;
5
+ component: ({ color, variant, }: CircleIconProps) => import("react").JSX.Element;
6
6
  tags: string[];
7
7
  parameters: {
8
8
  layout: string;