@datalayer/primer-addons 1.0.21 → 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.
- package/README.md +14 -0
- package/lib/components/appearance/AppearanceMenuWithStore.d.ts +26 -0
- package/lib/components/appearance/AppearanceMenuWithStore.js +21 -0
- package/lib/components/appearance/CollaboratorPalette.d.ts +65 -0
- package/lib/components/appearance/CollaboratorPalette.js +160 -0
- package/lib/components/appearance/index.d.ts +2 -0
- package/lib/components/appearance/index.js +2 -0
- package/lib/components/box/Box.d.ts +17 -5
- package/lib/components/box/Box.js +3 -5
- package/lib/components/calendar-picker/CalendarPicker.d.ts +35 -0
- package/lib/components/calendar-picker/CalendarPicker.js +146 -0
- package/lib/components/calendar-picker/index.d.ts +1 -0
- package/lib/components/calendar-picker/index.js +5 -0
- package/lib/components/card/Card.d.ts +6 -1
- package/lib/components/card/Card.js +1 -1
- package/lib/components/color/ColorSwatch.d.ts +1 -1
- package/lib/components/color-picker/ColorPicker.d.ts +97 -0
- package/lib/components/color-picker/ColorPicker.js +358 -0
- package/lib/components/color-picker/index.d.ts +1 -0
- package/lib/components/color-picker/index.js +5 -0
- package/lib/components/content-loader/ContentLoader.d.ts +1 -1
- package/lib/components/content-loader/ContentLoader.stories.d.ts +1 -1
- package/lib/components/date-picker/DatePicker.d.ts +46 -0
- package/lib/components/date-picker/DatePicker.js +98 -0
- package/lib/components/date-picker/index.d.ts +1 -0
- package/lib/components/date-picker/index.js +5 -0
- package/lib/components/icons/CircleIcon.d.ts +1 -1
- package/lib/components/icons/CircleIcon.stories.d.ts +1 -1
- package/lib/components/index.d.ts +5 -0
- package/lib/components/index.js +5 -0
- package/lib/components/logo/DatalayerText.d.ts +1 -1
- package/lib/components/logo/index.d.ts +0 -1
- package/lib/components/logo/index.js +0 -1
- package/lib/components/side-overlay/SideOverlay.d.ts +5 -5
- package/lib/components/side-overlay/SideOverlay.stories.d.ts +1 -1
- package/lib/components/slider/Slider.d.ts +1 -1
- package/lib/components/slider/Slider.stories.d.ts +1 -1
- package/lib/components/sliding-panel/SlidingPanel.d.ts +1 -1
- package/lib/components/sliding-panel/SlidingPanel.stories.d.ts +1 -1
- package/lib/components/toolbar/Toolbar.d.ts +2 -2
- package/lib/components/toolbar/Toolbar.js +0 -0
- package/lib/components/toolbar/ToolbarButton.d.ts +3 -1
- package/lib/components/toolbar/ToolbarButton.js +54 -22
- package/lib/components/toolbar/ToolbarDivider.d.ts +1 -1
- package/lib/components/toolbar/ToolbarDropdown.d.ts +3 -3
- package/lib/components/toolbar/ToolbarDropdown.js +66 -35
- package/lib/components/toolbar/ToolbarRenderer.d.ts +20 -3
- package/lib/components/toolbar/ToolbarRenderer.js +16 -16
- package/lib/components/toolbar/types.d.ts +8 -8
- package/lib/reactor/AppearancePlugin.d.ts +31 -0
- package/lib/reactor/AppearancePlugin.js +63 -0
- package/lib/reactor/ThemePlugin.d.ts +5 -0
- package/lib/reactor/ThemePlugin.js +98 -0
- package/lib/reactor/index.d.ts +30 -0
- package/lib/reactor/index.js +34 -0
- package/lib/reactor/page-layout/PageLayout.d.ts +148 -0
- package/lib/reactor/page-layout/PageLayout.js +310 -0
- package/lib/reactor/page-layout/PageLayoutPlugin.d.ts +59 -0
- package/lib/reactor/page-layout/PageLayoutPlugin.js +105 -0
- package/lib/reactor/page-layout/PagePanelToggle.d.ts +21 -0
- package/lib/reactor/page-layout/PagePanelToggle.js +29 -0
- package/lib/reactor/page-layout/SlotPageLayout.d.ts +36 -0
- package/lib/reactor/page-layout/SlotPageLayout.js +25 -0
- package/lib/reactor/page-layout/panelState.d.ts +28 -0
- package/lib/reactor/page-layout/panelState.js +49 -0
- package/lib/story-helpers.d.ts +2 -2
- package/lib/theme/DatalayerBrandThemeProvider.d.ts +1 -1
- package/lib/theme/DatalayerThemeProvider.d.ts +5 -5
- package/lib/theme/DatalayerThemeProvider.js +119 -58
- package/lib/theme/css/createThemeCSSVars.d.ts +1 -0
- package/lib/theme/css/createThemeCSSVars.js +9 -0
- package/lib/theme/fontStacks.d.ts +2 -0
- package/lib/theme/fontStacks.js +6 -0
- package/lib/theme/index.d.ts +2 -0
- package/lib/theme/index.js +2 -0
- package/lib/theme/palettes/ColorPalette.d.ts +3 -3
- package/lib/theme/portableTheme.d.ts +32 -0
- package/lib/theme/portableTheme.js +45 -0
- package/lib/utils/Helper.d.ts +2 -2
- package/lib/utils/Styles.d.ts +1 -1
- package/package.json +15 -14
- package/lib/components/logo/DI.d.ts +0 -19
- 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";
|
|
@@ -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
|
|
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
|
|
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";
|
|
@@ -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
|
|
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
|
|
5
|
+
component: ({ color, variant, }: CircleIconProps) => import("react").JSX.Element;
|
|
6
6
|
tags: string[];
|
|
7
7
|
parameters: {
|
|
8
8
|
layout: string;
|