@exsto/ds 0.0.1-rc.23 → 0.0.1-rc.24
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 +44 -0
- package/dist/color-field.js +3 -0
- package/dist/date-time.js +2 -0
- package/dist/ds/src/color-field.d.ts +9 -0
- package/dist/ds/src/date-time.d.ts +8 -0
- package/dist/source-hash.txt +1 -1
- package/dist/styles.css +216 -7
- package/dist/ui/src/components/edyn/animated-number/animated-number.js +5 -4
- package/dist/ui/src/components/edyn/animated-number/animated-number.js.map +1 -1
- package/dist/ui/src/components/edyn/color-field/color-field.d.ts +47 -0
- package/dist/ui/src/components/edyn/color-field/color-field.js +200 -0
- package/dist/ui/src/components/edyn/color-field/color-field.js.map +1 -0
- package/dist/ui/src/components/edyn/color-field/color-field.state.test.d.ts +6 -0
- package/dist/ui/src/components/edyn/color-field/color-value.d.ts +14 -0
- package/dist/ui/src/components/edyn/color-field/color-value.js +103 -0
- package/dist/ui/src/components/edyn/color-field/color-value.js.map +1 -0
- package/dist/ui/src/components/edyn/color-field/color-value.test.d.ts +1 -0
- package/dist/ui/src/components/edyn/color-field/index.d.ts +2 -0
- package/dist/ui/src/components/edyn/date-picker/date-picker.js +1 -0
- package/dist/ui/src/components/edyn/date-picker/date-picker.js.map +1 -1
- package/dist/ui/src/components/edyn/date-range-picker/date-range-picker.js +1 -0
- package/dist/ui/src/components/edyn/date-range-picker/date-range-picker.js.map +1 -1
- package/dist/ui/src/components/edyn/date-time/date-time-value.d.ts +23 -0
- package/dist/ui/src/components/edyn/date-time/date-time-value.js +26 -0
- package/dist/ui/src/components/edyn/date-time/date-time-value.js.map +1 -0
- package/dist/ui/src/components/edyn/date-time/date-time-value.test.d.ts +1 -0
- package/dist/ui/src/components/edyn/date-time/date-time.d.ts +73 -0
- package/dist/ui/src/components/edyn/date-time/date-time.js +119 -0
- package/dist/ui/src/components/edyn/date-time/date-time.js.map +1 -0
- package/dist/ui/src/components/edyn/date-time/date-time.state.test.d.ts +6 -0
- package/dist/ui/src/components/edyn/date-time/index.d.ts +1 -0
- package/dist/ui/src/components/edyn/multi-select/multi-select.js +2 -0
- package/dist/ui/src/components/edyn/multi-select/multi-select.js.map +1 -1
- package/dist/ui/src/components/edyn/rich-text-editor/toolbar.js +1 -0
- package/dist/ui/src/components/edyn/rich-text-editor/toolbar.js.map +1 -1
- package/dist/ui/src/components/edyn/time-field/time-field.js +7 -3
- package/dist/ui/src/components/edyn/time-field/time-field.js.map +1 -1
- package/dist/ui/src/components/ui/checkbox/checkbox.d.ts +1 -0
- package/dist/ui/src/components/ui/checkbox/checkbox.js +3 -2
- package/dist/ui/src/components/ui/checkbox/checkbox.js.map +1 -1
- package/dist/ui/src/components/ui/command/command.d.ts +1 -16
- package/dist/ui/src/components/ui/command/command.js +5 -23
- package/dist/ui/src/components/ui/command/command.js.map +1 -1
- package/dist/ui/src/components/ui/indicator/indicator.d.ts +1 -1
- package/dist/ui/src/components/ui/input-group/input-group.d.ts +1 -1
- package/dist/ui/src/components/ui/scroll-area/popup-list-scroll.d.ts +3 -1
- package/dist/ui/src/components/ui/scroll-area/popup-list-scroll.js +2 -1
- package/dist/ui/src/components/ui/scroll-area/popup-list-scroll.js.map +1 -1
- package/dist/ui/src/components/ui/sidebar/sidebar.js +1 -1
- package/dist/ui/src/components/ui/sidebar/sidebar.js.map +1 -1
- package/package.json +9 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { cn } from "../../../lib/utils.js";
|
|
2
|
+
import { Button } from "../../ui/button/button.js";
|
|
3
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover/popover.js";
|
|
4
|
+
import { Input } from "../../ui/input/input.js";
|
|
5
|
+
import { DEFAULT_COLOR_PALETTE, normalizeHex, readableOn } from "./color-value.js";
|
|
6
|
+
import { useId, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { IconCheck } from "@tabler/icons-react";
|
|
9
|
+
//#region ../ui/src/components/edyn/color-field/color-field.tsx
|
|
10
|
+
var DEFAULT_COLOR_FIELD_LABELS = {
|
|
11
|
+
choose: "Choose colour",
|
|
12
|
+
hex: "Hex colour",
|
|
13
|
+
palette: "Colours",
|
|
14
|
+
none: "No colour",
|
|
15
|
+
invalid: "Enter a colour as #RRGGBB."
|
|
16
|
+
};
|
|
17
|
+
var COLUMNS = 6;
|
|
18
|
+
var asSwatch = (entry) => typeof entry === "string" ? {
|
|
19
|
+
value: normalizeHex(entry) ?? entry,
|
|
20
|
+
label: entry
|
|
21
|
+
} : entry;
|
|
22
|
+
/** The label, then what the trigger shows, so that the visible text is part of the name (WCAG 2.5.3). */
|
|
23
|
+
var nameWith = (label, shown) => label === void 0 ? void 0 : label.toLowerCase().includes(shown.toLowerCase()) ? label : `${label}: ${shown}`;
|
|
24
|
+
/**
|
|
25
|
+
* A colour typed as hex or picked from a palette, in the kit's field: a trigger with a swatch and the colour, and a popup with the
|
|
26
|
+
* palette and a hex field. It exists so that an app does not fall back to `<input type="color">`, whose chooser is the browser's own.
|
|
27
|
+
* It has an entry of its own, `@exsto/ds/color-field`.
|
|
28
|
+
*
|
|
29
|
+
* The trigger opens the popup with Enter, Space or the pointer. In the palette the arrow keys move between swatches (up and down by
|
|
30
|
+
* a row), Home and End go to the ends, and Enter or Space chooses one, closes the popup and gives the focus back to the trigger.
|
|
31
|
+
* In the hex field Enter chooses the colour typed; leaving the field chooses it too, and text that is not a colour marks the field
|
|
32
|
+
* invalid, says so under it and is not reported.
|
|
33
|
+
*/
|
|
34
|
+
var ColorField = ({ id, value, onValueChange, palette = DEFAULT_COLOR_PALETTE, disabled = false, placeholder, name, "aria-label": ariaLabel, "aria-invalid": ariaInvalid, "aria-describedby": ariaDescribedBy, labels: labelOverrides, className }) => {
|
|
35
|
+
const labels = useMemo(() => ({
|
|
36
|
+
...DEFAULT_COLOR_FIELD_LABELS,
|
|
37
|
+
...labelOverrides
|
|
38
|
+
}), [labelOverrides]);
|
|
39
|
+
const swatches = useMemo(() => palette.map(asSwatch), [palette]);
|
|
40
|
+
const current = value === void 0 || value === "" ? void 0 : normalizeHex(value);
|
|
41
|
+
const [open, setOpen] = useState(false);
|
|
42
|
+
const [text, setText] = useState(current ?? "");
|
|
43
|
+
const [bad, setBad] = useState(false);
|
|
44
|
+
const [seen, setSeen] = useState(current);
|
|
45
|
+
const refOptions = useRef([]);
|
|
46
|
+
const errorId = useId();
|
|
47
|
+
if (current !== seen) {
|
|
48
|
+
setSeen(current);
|
|
49
|
+
setText(current ?? "");
|
|
50
|
+
setBad(false);
|
|
51
|
+
}
|
|
52
|
+
const commit = (raw) => {
|
|
53
|
+
if (raw.trim() === "") {
|
|
54
|
+
setBad(false);
|
|
55
|
+
setText("");
|
|
56
|
+
if (current !== void 0) onValueChange(void 0);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
const hex = normalizeHex(raw);
|
|
60
|
+
if (hex === void 0) {
|
|
61
|
+
setBad(true);
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
setBad(false);
|
|
65
|
+
setText(hex);
|
|
66
|
+
if (hex !== current) onValueChange(hex);
|
|
67
|
+
return true;
|
|
68
|
+
};
|
|
69
|
+
const choose = (hex) => {
|
|
70
|
+
setBad(false);
|
|
71
|
+
setText(hex);
|
|
72
|
+
onValueChange(hex);
|
|
73
|
+
setOpen(false);
|
|
74
|
+
};
|
|
75
|
+
const chosenIndex = swatches.findIndex((swatch) => swatch.value === current);
|
|
76
|
+
const tabbable = chosenIndex >= 0 ? chosenIndex : 0;
|
|
77
|
+
const move = (to) => {
|
|
78
|
+
refOptions.current[Math.max(0, Math.min(swatches.length - 1, to))]?.focus();
|
|
79
|
+
};
|
|
80
|
+
const onOptionKeyDown = (event, index) => {
|
|
81
|
+
const delta = {
|
|
82
|
+
ArrowRight: 1,
|
|
83
|
+
ArrowLeft: -1,
|
|
84
|
+
ArrowDown: COLUMNS,
|
|
85
|
+
ArrowUp: -6
|
|
86
|
+
}[event.key];
|
|
87
|
+
if (delta !== void 0) {
|
|
88
|
+
event.preventDefault();
|
|
89
|
+
move(index + delta);
|
|
90
|
+
} else if (event.key === "Home" || event.key === "End") {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
move(event.key === "Home" ? 0 : swatches.length - 1);
|
|
93
|
+
} else if (event.key === "Enter" || event.key === " ") {
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
choose(swatches[index].value);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const shown = current ?? placeholder ?? labels.none;
|
|
99
|
+
const invalid = ariaInvalid === true || ariaInvalid === "true";
|
|
100
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Popover, {
|
|
101
|
+
open,
|
|
102
|
+
onOpenChange: setOpen,
|
|
103
|
+
children: [/* @__PURE__ */ jsxs(PopoverTrigger, {
|
|
104
|
+
render: /* @__PURE__ */ jsx(Button, {
|
|
105
|
+
id,
|
|
106
|
+
variant: "outline",
|
|
107
|
+
"data-slot": "color-field-trigger",
|
|
108
|
+
disabled,
|
|
109
|
+
"aria-label": nameWith(ariaLabel, shown),
|
|
110
|
+
"aria-invalid": invalid ? true : void 0,
|
|
111
|
+
"aria-describedby": ariaDescribedBy,
|
|
112
|
+
className: cn("w-40 min-w-0 justify-start font-normal", className)
|
|
113
|
+
}),
|
|
114
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
115
|
+
"aria-hidden": "true",
|
|
116
|
+
"data-slot": "color-field-swatch",
|
|
117
|
+
className: cn("size-4 shrink-0 rounded-sm border border-control", current === void 0 && "border-dashed"),
|
|
118
|
+
style: current === void 0 ? void 0 : { backgroundColor: current }
|
|
119
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
120
|
+
className: cn("truncate tabular-nums", current === void 0 && "text-muted-foreground"),
|
|
121
|
+
children: shown
|
|
122
|
+
})]
|
|
123
|
+
}), /* @__PURE__ */ jsxs(PopoverContent, {
|
|
124
|
+
align: "start",
|
|
125
|
+
"aria-label": ariaLabel ?? labels.choose,
|
|
126
|
+
className: "w-64",
|
|
127
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
128
|
+
role: "listbox",
|
|
129
|
+
"aria-label": labels.palette,
|
|
130
|
+
"data-slot": "color-field-palette",
|
|
131
|
+
className: "grid grid-cols-[repeat(6,1.5rem)] justify-between gap-y-1.5",
|
|
132
|
+
children: swatches.map((swatch, index) => {
|
|
133
|
+
const selected = index === chosenIndex;
|
|
134
|
+
return /* @__PURE__ */ jsx("div", {
|
|
135
|
+
ref: (node) => {
|
|
136
|
+
refOptions.current[index] = node;
|
|
137
|
+
},
|
|
138
|
+
role: "option",
|
|
139
|
+
"aria-selected": selected,
|
|
140
|
+
"aria-label": swatch.label,
|
|
141
|
+
tabIndex: index === tabbable ? 0 : -1,
|
|
142
|
+
"data-slot": "color-field-option",
|
|
143
|
+
"data-value": swatch.value,
|
|
144
|
+
className: cn("flex size-6 cursor-default items-center justify-center rounded-md border border-control outline-none", readableOn(swatch.value) === "black" ? "text-black" : "text-white"),
|
|
145
|
+
style: { backgroundColor: swatch.value },
|
|
146
|
+
onClick: () => choose(swatch.value),
|
|
147
|
+
onKeyDown: (event) => onOptionKeyDown(event, index),
|
|
148
|
+
children: selected && /* @__PURE__ */ jsx(IconCheck, {
|
|
149
|
+
"aria-hidden": "true",
|
|
150
|
+
className: "size-3.5"
|
|
151
|
+
})
|
|
152
|
+
}, swatch.value);
|
|
153
|
+
})
|
|
154
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
155
|
+
className: "flex flex-col gap-1",
|
|
156
|
+
children: [/* @__PURE__ */ jsx(Input, {
|
|
157
|
+
type: "text",
|
|
158
|
+
"data-slot": "color-field-hex",
|
|
159
|
+
autoComplete: "off",
|
|
160
|
+
autoCapitalize: "none",
|
|
161
|
+
spellCheck: false,
|
|
162
|
+
maxLength: 7,
|
|
163
|
+
value: text,
|
|
164
|
+
placeholder: "#RRGGBB",
|
|
165
|
+
"aria-label": labels.hex,
|
|
166
|
+
"aria-invalid": bad ? true : void 0,
|
|
167
|
+
"aria-describedby": bad ? errorId : void 0,
|
|
168
|
+
onChange: (event) => {
|
|
169
|
+
setText(event.target.value);
|
|
170
|
+
setBad(false);
|
|
171
|
+
},
|
|
172
|
+
onBlur: () => {
|
|
173
|
+
commit(text);
|
|
174
|
+
},
|
|
175
|
+
onKeyDown: (event) => {
|
|
176
|
+
if (event.key === "Enter") {
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
if (commit(text)) setOpen(false);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}), bad && /* @__PURE__ */ jsx("p", {
|
|
182
|
+
id: errorId,
|
|
183
|
+
"data-slot": "color-field-error",
|
|
184
|
+
role: "alert",
|
|
185
|
+
className: "text-sm text-destructive",
|
|
186
|
+
children: labels.invalid
|
|
187
|
+
})]
|
|
188
|
+
})]
|
|
189
|
+
})]
|
|
190
|
+
}), name !== void 0 && /* @__PURE__ */ jsx("input", {
|
|
191
|
+
type: "hidden",
|
|
192
|
+
name,
|
|
193
|
+
value: current ?? "",
|
|
194
|
+
disabled
|
|
195
|
+
})] });
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
198
|
+
export { ColorField, DEFAULT_COLOR_FIELD_LABELS };
|
|
199
|
+
|
|
200
|
+
//# sourceMappingURL=color-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"color-field.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/color-field/color-field.tsx"],"sourcesContent":["import { Button } from '@edyn/ui/components/ui/button';\nimport { Input } from '@edyn/ui/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@edyn/ui/components/ui/popover';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconCheck } from '@tabler/icons-react';\nimport { type ChangeEvent, type KeyboardEvent, useId, useMemo, useRef, useState } from 'react';\nimport { DEFAULT_COLOR_PALETTE, type IColorSwatch, normalizeHex, readableOn } from './color-value';\n\nexport interface IColorFieldLabels {\n /** The name of the popup, when the field has no label of its own. */\n choose: string;\n /** The name of the hex field in the popup. */\n hex: string;\n /** The name of the palette. */\n palette: string;\n /** What the trigger says when no colour is chosen and there is no placeholder. */\n none: string;\n /** The message under the hex field when its text is not a colour. */\n invalid: string;\n}\n\nexport const DEFAULT_COLOR_FIELD_LABELS: IColorFieldLabels = {\n choose: 'Choose colour',\n hex: 'Hex colour',\n palette: 'Colours',\n none: 'No colour',\n invalid: 'Enter a colour as #RRGGBB.',\n};\n\nexport interface IColorFieldProps {\n /** For a Label's htmlFor: it goes to the trigger. */\n id?: string;\n /** The colour as `#rrggbb`, as `<input type=\"color\">` writes it, or `undefined` for none. `#rgb` is read as `#rrggbb`. */\n value: string | undefined;\n /** Called with `#rrggbb` (lower case), or `undefined` when the hex field is emptied. Text that is not a colour is not reported. */\n onValueChange: (value: string | undefined) => void;\n /** The swatches offered: hex strings, or a hex and the name a screen reader says. Tailwind's hues by default. */\n palette?: readonly (string | IColorSwatch)[];\n disabled?: boolean;\n /** What the trigger says when no colour is chosen. */\n placeholder?: string;\n /** Submits the colour with a form, under this name, as `#rrggbb`; empty for none. */\n name?: string;\n /** An accessible name, when there is no visible label to point at. The chosen colour is added to it. */\n 'aria-label'?: string;\n 'aria-invalid'?: boolean | 'true' | 'false';\n 'aria-describedby'?: string;\n labels?: Partial<IColorFieldLabels>;\n /** Layout only (a width); the look is the kit's. */\n className?: string;\n}\n\nconst COLUMNS = 6;\n\nconst asSwatch = (entry: string | IColorSwatch): IColorSwatch =>\n typeof entry === 'string' ? { value: normalizeHex(entry) ?? entry, label: entry } : entry;\n\n/** The label, then what the trigger shows, so that the visible text is part of the name (WCAG 2.5.3). */\nconst nameWith = (label: string | undefined, shown: string): string | undefined =>\n label === undefined ? undefined : label.toLowerCase().includes(shown.toLowerCase()) ? label : `${label}: ${shown}`;\n\n/**\n * A colour typed as hex or picked from a palette, in the kit's field: a trigger with a swatch and the colour, and a popup with the\n * palette and a hex field. It exists so that an app does not fall back to `<input type=\"color\">`, whose chooser is the browser's own.\n * It has an entry of its own, `@exsto/ds/color-field`.\n *\n * The trigger opens the popup with Enter, Space or the pointer. In the palette the arrow keys move between swatches (up and down by\n * a row), Home and End go to the ends, and Enter or Space chooses one, closes the popup and gives the focus back to the trigger.\n * In the hex field Enter chooses the colour typed; leaving the field chooses it too, and text that is not a colour marks the field\n * invalid, says so under it and is not reported.\n */\nexport const ColorField = ({\n id,\n value,\n onValueChange,\n palette = DEFAULT_COLOR_PALETTE,\n disabled = false,\n placeholder,\n name,\n 'aria-label': ariaLabel,\n 'aria-invalid': ariaInvalid,\n 'aria-describedby': ariaDescribedBy,\n labels: labelOverrides,\n className,\n}: IColorFieldProps) => {\n const labels = useMemo(() => ({ ...DEFAULT_COLOR_FIELD_LABELS, ...labelOverrides }), [labelOverrides]);\n const swatches = useMemo(() => palette.map(asSwatch), [palette]);\n const current = value === undefined || value === '' ? undefined : normalizeHex(value);\n const [open, setOpen] = useState(false);\n const [text, setText] = useState(current ?? '');\n const [bad, setBad] = useState(false);\n const [seen, setSeen] = useState(current);\n const refOptions = useRef<Array<HTMLDivElement | null>>([]);\n const errorId = useId();\n if (current !== seen) {\n setSeen(current);\n setText(current ?? '');\n setBad(false);\n }\n\n const commit = (raw: string): boolean => {\n if (raw.trim() === '') {\n setBad(false);\n setText('');\n if (current !== undefined) {\n onValueChange(undefined);\n }\n return true;\n }\n const hex = normalizeHex(raw);\n if (hex === undefined) {\n setBad(true);\n return false;\n }\n setBad(false);\n setText(hex);\n if (hex !== current) {\n onValueChange(hex);\n }\n return true;\n };\n\n const choose = (hex: string): void => {\n setBad(false);\n setText(hex);\n onValueChange(hex);\n setOpen(false);\n };\n\n const chosenIndex = swatches.findIndex((swatch) => swatch.value === current);\n const tabbable = chosenIndex >= 0 ? chosenIndex : 0;\n const move = (to: number): void => {\n refOptions.current[Math.max(0, Math.min(swatches.length - 1, to))]?.focus();\n };\n const onOptionKeyDown = (event: KeyboardEvent<HTMLDivElement>, index: number): void => {\n const step: Record<string, number | undefined> = { ArrowRight: 1, ArrowLeft: -1, ArrowDown: COLUMNS, ArrowUp: -COLUMNS };\n const delta = step[event.key];\n if (delta !== undefined) {\n event.preventDefault();\n move(index + delta);\n } else if (event.key === 'Home' || event.key === 'End') {\n event.preventDefault();\n move(event.key === 'Home' ? 0 : swatches.length - 1);\n } else if (event.key === 'Enter' || event.key === ' ') {\n event.preventDefault();\n choose((swatches[index] as IColorSwatch).value);\n }\n };\n\n const shown = current ?? placeholder ?? labels.none;\n const invalid = ariaInvalid === true || ariaInvalid === 'true';\n return (\n <>\n <Popover open={open} onOpenChange={setOpen}>\n <PopoverTrigger\n render={\n <Button\n id={id}\n variant=\"outline\"\n data-slot=\"color-field-trigger\"\n disabled={disabled}\n aria-label={nameWith(ariaLabel, shown)}\n aria-invalid={invalid ? true : undefined}\n aria-describedby={ariaDescribedBy}\n className={cn('w-40 min-w-0 justify-start font-normal', className)}\n />\n }\n >\n <span\n aria-hidden=\"true\"\n data-slot=\"color-field-swatch\"\n className={cn('size-4 shrink-0 rounded-sm border border-control', current === undefined && 'border-dashed')}\n style={current === undefined ? undefined : { backgroundColor: current }}\n />\n <span className={cn('truncate tabular-nums', current === undefined && 'text-muted-foreground')}>{shown}</span>\n </PopoverTrigger>\n <PopoverContent align=\"start\" aria-label={ariaLabel ?? labels.choose} className=\"w-64\">\n <div\n role=\"listbox\"\n aria-label={labels.palette}\n data-slot=\"color-field-palette\"\n className=\"grid grid-cols-[repeat(6,1.5rem)] justify-between gap-y-1.5\"\n >\n {swatches.map((swatch, index) => {\n const selected = index === chosenIndex;\n return (\n <div\n key={swatch.value}\n ref={(node) => {\n refOptions.current[index] = node;\n }}\n role=\"option\"\n aria-selected={selected}\n aria-label={swatch.label}\n tabIndex={index === tabbable ? 0 : -1}\n data-slot=\"color-field-option\"\n data-value={swatch.value}\n className={cn(\n 'flex size-6 cursor-default items-center justify-center rounded-md border border-control outline-none',\n readableOn(swatch.value) === 'black' ? 'text-black' : 'text-white',\n )}\n style={{ backgroundColor: swatch.value }}\n onClick={() => choose(swatch.value)}\n onKeyDown={(event) => onOptionKeyDown(event, index)}\n >\n {selected && <IconCheck aria-hidden=\"true\" className=\"size-3.5\" />}\n </div>\n );\n })}\n </div>\n <div className=\"flex flex-col gap-1\">\n <Input\n type=\"text\"\n data-slot=\"color-field-hex\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n spellCheck={false}\n maxLength={7}\n value={text}\n placeholder=\"#RRGGBB\"\n aria-label={labels.hex}\n aria-invalid={bad ? true : undefined}\n aria-describedby={bad ? errorId : undefined}\n onChange={(event: ChangeEvent<HTMLInputElement>) => {\n setText(event.target.value);\n setBad(false);\n }}\n onBlur={() => {\n commit(text);\n }}\n onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'Enter') {\n event.preventDefault();\n if (commit(text)) {\n setOpen(false);\n }\n }\n }}\n />\n {bad && (\n <p id={errorId} data-slot=\"color-field-error\" role=\"alert\" className=\"text-sm text-destructive\">\n {labels.invalid}\n </p>\n )}\n </div>\n </PopoverContent>\n </Popover>\n {name !== undefined && <input type=\"hidden\" name={name} value={current ?? ''} disabled={disabled} />}\n </>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a ColorField tells the page and what it shows: the colour typed or picked is reported as #rrggbb, text that is not a colour is
|
|
3
|
+
* not reported and marks the hex field invalid, and the trigger says the colour. happy-dom is enough; `e2e/color-field.spec.ts` covers
|
|
4
|
+
* the popup, the keys and the focus in a browser.
|
|
5
|
+
*/
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** A colour kept as the text `<input type="color">` writes: `#rrggbb`, lower case. */
|
|
2
|
+
/** `#abc`, `abc`, `#AABBCC` and `aabbcc` as `#aabbcc`; undefined for anything that is not a colour. */
|
|
3
|
+
export declare const normalizeHex: (text: string) => string | undefined;
|
|
4
|
+
/** The WCAG contrast of white or of black on a colour. */
|
|
5
|
+
export declare const contrastOn: (hex: string, text: "black" | "white") => number;
|
|
6
|
+
/** The colour of a mark on a swatch (the check): white where it reaches 3:1 (WCAG 1.4.11), black where it does not, which is 7:1 or more. */
|
|
7
|
+
export declare const readableOn: (hex: string) => "black" | "white";
|
|
8
|
+
export interface IColorSwatch {
|
|
9
|
+
value: string;
|
|
10
|
+
/** What a screen reader says for it. */
|
|
11
|
+
label: string;
|
|
12
|
+
}
|
|
13
|
+
/** The hues of Tailwind's palette at 500, six to a row. */
|
|
14
|
+
export declare const DEFAULT_COLOR_PALETTE: readonly IColorSwatch[];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
//#region ../ui/src/components/edyn/color-field/color-value.ts
|
|
2
|
+
/** A colour kept as the text `<input type="color">` writes: `#rrggbb`, lower case. */
|
|
3
|
+
var HEX = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;
|
|
4
|
+
/** `#abc`, `abc`, `#AABBCC` and `aabbcc` as `#aabbcc`; undefined for anything that is not a colour. */
|
|
5
|
+
var normalizeHex = (text) => {
|
|
6
|
+
const found = HEX.exec(text.trim());
|
|
7
|
+
if (!found) return;
|
|
8
|
+
const digits = found[1].toLowerCase();
|
|
9
|
+
return digits.length === 3 ? `#${digits[0]}${digits[0]}${digits[1]}${digits[1]}${digits[2]}${digits[2]}` : `#${digits}`;
|
|
10
|
+
};
|
|
11
|
+
var channel = (value) => {
|
|
12
|
+
const c = value / 255;
|
|
13
|
+
return c <= .04045 ? c / 12.92 : ((c + .055) / 1.055) ** 2.4;
|
|
14
|
+
};
|
|
15
|
+
/** The WCAG contrast of white or of black on a colour. */
|
|
16
|
+
var contrastOn = (hex, text) => {
|
|
17
|
+
const r = channel(Number.parseInt(hex.slice(1, 3), 16));
|
|
18
|
+
const g = channel(Number.parseInt(hex.slice(3, 5), 16));
|
|
19
|
+
const b = channel(Number.parseInt(hex.slice(5, 7), 16));
|
|
20
|
+
const luminance = .2126 * r + .7152 * g + .0722 * b;
|
|
21
|
+
return text === "white" ? 1.05 / (luminance + .05) : (luminance + .05) / .05;
|
|
22
|
+
};
|
|
23
|
+
/** The colour of a mark on a swatch (the check): white where it reaches 3:1 (WCAG 1.4.11), black where it does not, which is 7:1 or more. */
|
|
24
|
+
var readableOn = (hex) => contrastOn(hex, "white") >= 3 ? "white" : "black";
|
|
25
|
+
/** The hues of Tailwind's palette at 500, six to a row. */
|
|
26
|
+
var DEFAULT_COLOR_PALETTE = [
|
|
27
|
+
{
|
|
28
|
+
value: "#ef4444",
|
|
29
|
+
label: "Red"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
value: "#f97316",
|
|
33
|
+
label: "Orange"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
value: "#f59e0b",
|
|
37
|
+
label: "Amber"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
value: "#eab308",
|
|
41
|
+
label: "Yellow"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
value: "#84cc16",
|
|
45
|
+
label: "Lime"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
value: "#22c55e",
|
|
49
|
+
label: "Green"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
value: "#10b981",
|
|
53
|
+
label: "Emerald"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
value: "#14b8a6",
|
|
57
|
+
label: "Teal"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
value: "#06b6d4",
|
|
61
|
+
label: "Cyan"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
value: "#0ea5e9",
|
|
65
|
+
label: "Sky"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
value: "#3b82f6",
|
|
69
|
+
label: "Blue"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
value: "#6366f1",
|
|
73
|
+
label: "Indigo"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
value: "#8b5cf6",
|
|
77
|
+
label: "Violet"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
value: "#a855f7",
|
|
81
|
+
label: "Purple"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
value: "#d946ef",
|
|
85
|
+
label: "Fuchsia"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
value: "#ec4899",
|
|
89
|
+
label: "Pink"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
value: "#f43f5e",
|
|
93
|
+
label: "Rose"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
value: "#64748b",
|
|
97
|
+
label: "Slate"
|
|
98
|
+
}
|
|
99
|
+
];
|
|
100
|
+
//#endregion
|
|
101
|
+
export { DEFAULT_COLOR_PALETTE, contrastOn, normalizeHex, readableOn };
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=color-value.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"color-value.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/color-field/color-value.ts"],"sourcesContent":["/** A colour kept as the text `<input type=\"color\">` writes: `#rrggbb`, lower case. */\n\nconst HEX = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i;\n\n/** `#abc`, `abc`, `#AABBCC` and `aabbcc` as `#aabbcc`; undefined for anything that is not a colour. */\nexport const normalizeHex = (text: string): string | undefined => {\n const found = HEX.exec(text.trim());\n if (!found) {\n return undefined;\n }\n const digits = (found[1] as string).toLowerCase();\n return digits.length === 3 ? `#${digits[0]}${digits[0]}${digits[1]}${digits[1]}${digits[2]}${digits[2]}` : `#${digits}`;\n};\n\nconst channel = (value: number): number => {\n const c = value / 255;\n return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;\n};\n\n/** The WCAG contrast of white or of black on a colour. */\nexport const contrastOn = (hex: string, text: 'black' | 'white'): number => {\n const r = channel(Number.parseInt(hex.slice(1, 3), 16));\n const g = channel(Number.parseInt(hex.slice(3, 5), 16));\n const b = channel(Number.parseInt(hex.slice(5, 7), 16));\n const luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b;\n return text === 'white' ? 1.05 / (luminance + 0.05) : (luminance + 0.05) / 0.05;\n};\n\n/** The colour of a mark on a swatch (the check): white where it reaches 3:1 (WCAG 1.4.11), black where it does not, which is 7:1 or more. */\nexport const readableOn = (hex: string): 'black' | 'white' => (contrastOn(hex, 'white') >= 3 ? 'white' : 'black');\n\nexport interface IColorSwatch {\n value: string;\n /** What a screen reader says for it. */\n label: string;\n}\n\n/** The hues of Tailwind's palette at 500, six to a row. */\nexport const DEFAULT_COLOR_PALETTE: readonly IColorSwatch[] = [\n { value: '#ef4444', label: 'Red' },\n { value: '#f97316', label: 'Orange' },\n { value: '#f59e0b', label: 'Amber' },\n { value: '#eab308', label: 'Yellow' },\n { value: '#84cc16', label: 'Lime' },\n { value: '#22c55e', label: 'Green' },\n { value: '#10b981', label: 'Emerald' },\n { value: '#14b8a6', label: 'Teal' },\n { value: '#06b6d4', label: 'Cyan' },\n { value: '#0ea5e9', label: 'Sky' },\n { value: '#3b82f6', label: 'Blue' },\n { value: '#6366f1', label: 'Indigo' },\n { value: '#8b5cf6', label: 'Violet' },\n { value: '#a855f7', label: 'Purple' },\n { value: '#d946ef', label: 'Fuchsia' },\n { value: '#ec4899', label: 'Pink' },\n { value: '#f43f5e', label: 'Rose' },\n { value: '#64748b', label: 'Slate' },\n];\n"],"mappings":";;AAEA,IAAM,MAAM;;AAGZ,IAAa,gBAAgB,SAAqC;CAChE,MAAM,QAAQ,IAAI,KAAK,KAAK,KAAK,CAAC;CAClC,IAAI,CAAC,OACH;CAEF,MAAM,SAAU,MAAM,EAAE,CAAY,YAAY;CAChD,OAAO,OAAO,WAAW,IAAI,IAAI,OAAO,KAAK,OAAO,KAAK,OAAO,KAAK,OAAO,KAAK,OAAO,KAAK,OAAO,OAAO,IAAI;AACjH;AAEA,IAAM,WAAW,UAA0B;CACzC,MAAM,IAAI,QAAQ;CAClB,OAAO,KAAK,SAAU,IAAI,UAAU,IAAI,QAAS,UAAU;AAC7D;;AAGA,IAAa,cAAc,KAAa,SAAoC;CAC1E,MAAM,IAAI,QAAQ,OAAO,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC;CACtD,MAAM,IAAI,QAAQ,OAAO,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC;CACtD,MAAM,IAAI,QAAQ,OAAO,SAAS,IAAI,MAAM,GAAG,CAAC,GAAG,EAAE,CAAC;CACtD,MAAM,YAAY,QAAS,IAAI,QAAS,IAAI,QAAS;CACrD,OAAO,SAAS,UAAU,QAAQ,YAAY,QAAS,YAAY,OAAQ;AAC7E;;AAGA,IAAa,cAAc,QAAoC,WAAW,KAAK,OAAO,KAAK,IAAI,UAAU;;AASzG,IAAa,wBAAiD;CAC5D;EAAE,OAAO;EAAW,OAAO;CAAM;CACjC;EAAE,OAAO;EAAW,OAAO;CAAS;CACpC;EAAE,OAAO;EAAW,OAAO;CAAQ;CACnC;EAAE,OAAO;EAAW,OAAO;CAAS;CACpC;EAAE,OAAO;EAAW,OAAO;CAAO;CAClC;EAAE,OAAO;EAAW,OAAO;CAAQ;CACnC;EAAE,OAAO;EAAW,OAAO;CAAU;CACrC;EAAE,OAAO;EAAW,OAAO;CAAO;CAClC;EAAE,OAAO;EAAW,OAAO;CAAO;CAClC;EAAE,OAAO;EAAW,OAAO;CAAM;CACjC;EAAE,OAAO;EAAW,OAAO;CAAO;CAClC;EAAE,OAAO;EAAW,OAAO;CAAS;CACpC;EAAE,OAAO;EAAW,OAAO;CAAS;CACpC;EAAE,OAAO;EAAW,OAAO;CAAS;CACpC;EAAE,OAAO;EAAW,OAAO;CAAU;CACrC;EAAE,OAAO;EAAW,OAAO;CAAO;CAClC;EAAE,OAAO;EAAW,OAAO;CAAO;CAClC;EAAE,OAAO;EAAW,OAAO;CAAQ;AACrC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -268,6 +268,7 @@ var DatePicker = ({ id, value, onValueChange, min, max, disabledDates, disabled
|
|
|
268
268
|
children: /* @__PURE__ */ jsx(IconCalendar, { "aria-hidden": "true" })
|
|
269
269
|
}), /* @__PURE__ */ jsx(PopoverContent, {
|
|
270
270
|
anchor: refField,
|
|
271
|
+
"aria-label": labels.choose,
|
|
271
272
|
align: "start",
|
|
272
273
|
className: "w-auto p-0",
|
|
273
274
|
finalFocus: focusAfterClose,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"date-picker.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/date-picker/date-picker.tsx"],"sourcesContent":["import { dateToDay, dayToDate } from '@edyn/ui/components/edyn/date-range-picker';\nimport { Button } from '@edyn/ui/components/ui/button';\nimport { Calendar } from '@edyn/ui/components/ui/calendar';\nimport { Input } from '@edyn/ui/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@edyn/ui/components/ui/popover';\nimport { firstWeekdayOf } from '@edyn/ui/lib/first-weekday';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconCalendar } from '@tabler/icons-react';\nimport { type ChangeEvent, type FocusEvent, type KeyboardEvent, useEffect, useId, useMemo, useRef, useState } from 'react';\nimport type { Locale } from 'react-day-picker';\nimport { datePatternOf, dayProblem, formatDay, parseDateText, type TDateProblem, type TDisabledDays } from './date-text';\n\n/** Every word the component prints, so a product can translate it. */\nexport interface IDatePickerLabels {\n /** The calendar button's accessible name. */\n choose: string;\n /** Said when the text is not a date; `pattern` is what to type, written the locale's way (`DD/MM/YYYY`). */\n format: (pattern: string) => string;\n notReal: string;\n /** `min` and `max` are written the locale's way. */\n beforeMin: (min: string) => string;\n afterMax: (max: string) => string;\n unavailable: string;\n}\n\nexport const DEFAULT_DATE_PICKER_LABELS: IDatePickerLabels = {\n choose: 'Choose date',\n format: (pattern) => `Enter a date as ${pattern}.`,\n notReal: 'That date does not exist.',\n beforeMin: (min) => `Choose ${min} or later.`,\n afterMax: (max) => `Choose ${max} or earlier.`,\n unavailable: 'That date is not available.',\n};\n\n/**\n * What the field holds when it reports, so a parent can tell apart things that would otherwise look alike:\n * - `day`: a usable day, typed or picked.\n * - `empty`: the field was emptied, or nothing has been put in it. A required field is right to ask for a date.\n * - `refused`: the field holds text that is not a day (not a day, or not a real one). The day reported is `undefined`.\n * `text` is what was typed and `problem` the message the picker shows under the field, in the picker's `labels`.\n * - `out-of-range`: the field holds a real day that `min`, `max` or `disabledDates` refuse. The day IS reported (as the\n * first argument and as `day`), so a form's own rule can judge it and show its message; the field is marked invalid.\n * Check `kind` before treating the day as a usable one.\n *\n * Inside a Field that renders a FieldError, that message is the only one: the picker does not show its own.\n */\nexport type TDatePickerState =\n | { kind: 'day' }\n | { kind: 'empty' }\n | { kind: 'refused'; text: string; problem: string }\n | { kind: 'out-of-range'; day: string; text: string; problem: string };\n\nexport interface IDatePickerProps {\n /** For a Label's htmlFor; inside a Field the Field's label is already wired. */\n id?: string;\n /**\n * The chosen day as `yyyy-mm-dd`, or `undefined` for none (an empty string counts as none, so a value kept for an\n * `<input type=\"date\">` drops in). A string and not a Date, on purpose: a Date is an instant, and the instant for\n * \"5 March\" is another calendar day in another time zone, so a Date serialised for an API drifts a day. The text is\n * what `<input type=\"date\">`, JSON and a .NET `DateOnly` use, and it compares as a day.\n */\n value: string | undefined;\n /**\n * Called with a valid day, or with `undefined` when the field is emptied or its text is not a usable day. The second\n * argument says which of the two it was (see `TDatePickerState`); a caller that has no use for it can ignore it.\n * Text that is refused is reported each time it changes, also when the field held no day before, so a form that\n * tracks the state hears about it. Emptying the field after that reports `empty`.\n */\n onValueChange: (value: string | undefined, state: TDatePickerState) => void;\n /** The earliest and latest day that can be chosen, as `yyyy-mm-dd`, both included. */\n min?: string;\n max?: string;\n /** Days that cannot be chosen: a list of `yyyy-mm-dd`, or a function that says so for one (weekends, say). */\n disabledDates?: TDisabledDays;\n disabled?: boolean;\n /** How a day is written and in which order it is typed (BCP 47); en-GB reads 05/03/2026. */\n locale?: string;\n /** The calendar's own words (month and weekday names), as a date-fns locale; English by default. The first weekday follows `locale`. */\n calendarLocale?: Locale;\n /** What the empty field shows; the pattern for the locale (`DD/MM/YYYY`) by default. */\n placeholder?: string;\n /** An accessible name, when there is no visible label to point at. */\n 'aria-label'?: string;\n /** Marks the field invalid (the kit's destructive border), as a Field's invalid state does. */\n 'aria-invalid'?: boolean | 'true' | 'false';\n /** The id of the text that explains it. Added to what a Field names already, and to the picker's own error. */\n 'aria-describedby'?: string;\n labels?: Partial<IDatePickerLabels>;\n /** Layout only (a width); the look is the kit's. */\n className?: string;\n}\n\nconst problemText = (\n problem: TDateProblem,\n labels: IDatePickerLabels,\n locale: string,\n limits: { min?: string; max?: string },\n pattern: string,\n): string => {\n switch (problem) {\n case 'format':\n return labels.format(pattern);\n case 'notReal':\n return labels.notReal;\n case 'beforeMin':\n return labels.beforeMin(limits.min === undefined ? '' : formatDay(limits.min, locale));\n case 'afterMax':\n return labels.afterMax(limits.max === undefined ? '' : formatDay(limits.max, locale));\n default:\n return labels.unavailable;\n }\n};\n\n/**\n * One calendar day, typed or picked. The field takes the day in the locale's order (05/03/2026 in en-GB) and reads it\n * when it loses focus, saying inline what is wrong with it; the button, or Alt+ArrowDown in the field, opens a calendar.\n * Escape closes the calendar and gives the focus back to whichever of the two opened it; a day picked puts it on the field.\n */\nexport const DatePicker = ({\n id,\n value,\n onValueChange,\n min,\n max,\n disabledDates,\n disabled = false,\n locale = 'en-GB',\n calendarLocale,\n placeholder,\n 'aria-label': ariaLabel,\n 'aria-invalid': ariaInvalid,\n 'aria-describedby': ariaDescribedBy,\n labels: labelOverrides,\n className,\n}: IDatePickerProps) => {\n const labels = useMemo(() => ({ ...DEFAULT_DATE_PICKER_LABELS, ...labelOverrides }), [labelOverrides]);\n const pattern = useMemo(() => datePatternOf(locale), [locale]);\n const current = value === '' ? undefined : value;\n const errorId = useId();\n const refField = useRef<HTMLDivElement | null>(null);\n const refInput = useRef<HTMLInputElement | null>(null);\n const refButton = useRef<HTMLButtonElement | null>(null);\n // What opened the calendar, so that Escape gives the focus back to it: the button when the button did it, the field\n // when Alt+ArrowDown did. Choosing a day is different (see `pick`): the field then shows the day, so it gets the focus.\n const refOpener = useRef<'button' | 'field'>('button');\n const refPicked = useRef(false);\n // The mouse is down inside the picker, so a blur of the field is a click on something in it, not a Tab.\n const refPointer = useRef(false);\n // The day the field last agreed on with the page, so a change from outside is told from our own report of it.\n const refKnown = useRef<string | undefined>(current);\n // The refused text last reported (with its message), or null: told apart from a fresh refusal so it is reported once,\n // and so that emptying a field that held a refused text is reported although it held no day.\n const refRefused = useRef<string | null>(null);\n const [text, setText] = useState(current === undefined ? '' : formatDay(current, locale));\n const [problem, setProblem] = useState<TDateProblem | null>(null);\n const [open, setOpen] = useState(false);\n // The id of the FieldError of the Field around the picker, when it renders one: that message is the only one for the field.\n const [formError, setFormError] = useState<string | null>(null);\n\n useEffect(() => {\n const field = refField.current?.closest('[data-slot=\"field\"]');\n if (!field) {\n return;\n }\n const read = () => setFormError(field.querySelector('[data-slot=\"field-error\"]')?.id || null);\n read();\n const observer = new MutationObserver(read);\n observer.observe(field, { childList: true, subtree: true, attributes: true, attributeFilter: ['id'] });\n return () => {\n observer.disconnect();\n };\n }, []);\n\n useEffect(() => {\n if (current !== refKnown.current) {\n refKnown.current = current;\n refRefused.current = null;\n setText(current === undefined ? '' : formatDay(current, locale));\n setProblem(null);\n }\n }, [current, locale]);\n\n const report = (day: string | undefined, state: TDatePickerState) => {\n refKnown.current = day;\n refRefused.current = state.kind === 'refused' || state.kind === 'out-of-range' ? `${state.text}\\u0000${state.problem}` : null;\n onValueChange(day, state);\n };\n\n /**\n * Reads the text: a day is reported and written back in full, anything else is said inline and reported as no day,\n * with the state saying whether the field is empty or holds text it refused.\n */\n const commit = (typed: string) => {\n const parsed = parseDateText(typed, locale);\n if (parsed.kind === 'empty') {\n setProblem(null);\n if (current !== undefined || refRefused.current !== null) {\n report(undefined, { kind: 'empty' });\n }\n return;\n }\n const found = parsed.kind === 'problem' ? parsed.problem : dayProblem(parsed.day, { min, max, disabledDates });\n if (found !== undefined) {\n setProblem(found);\n const message = problemText(found, labels, locale, { min, max }, pattern);\n const seen = refRefused.current === `${typed}\\u0000${message}`;\n if (parsed.kind === 'day') {\n // A real day that the limits refuse: the text stays as typed and the day is reported, flagged, so that the form's own\n // rule can show its message instead of seeing an empty field.\n if (parsed.day !== current || !seen) {\n report(parsed.day, { kind: 'out-of-range', day: parsed.day, text: typed, problem: message });\n }\n return;\n }\n if (current !== undefined || !seen) {\n report(undefined, { kind: 'refused', text: typed, problem: message });\n }\n return;\n }\n if (parsed.kind === 'day') {\n setProblem(null);\n setText(formatDay(parsed.day, locale));\n if (parsed.day !== current || refRefused.current !== null) {\n report(parsed.day, { kind: 'day' });\n }\n }\n };\n\n const pick = (date: Date | undefined) => {\n const day = dateToDay(date);\n if (day === undefined) {\n return;\n }\n setProblem(null);\n setText(formatDay(day, locale));\n if (day !== current || refRefused.current !== null) {\n report(day, { kind: 'day' });\n }\n refPicked.current = true;\n setOpen(false);\n };\n\n const openFrom = (opener: 'button' | 'field') => {\n refOpener.current = opener;\n refPicked.current = false;\n setOpen(true);\n };\n\n /** Where the focus goes when the calendar closes: the field after a day is chosen, else whatever opened it. */\n const focusAfterClose = (): HTMLElement | null => (refPicked.current || refOpener.current === 'field' ? refInput.current : refButton.current);\n\n /**\n * Text is read as the person leaves the field. A day (or nothing) is taken at once; a message about text that is not a\n * day is shown too, unless they are only on the way to the calendar: a press of the mouse on the button, or focus\n * going into the calendar that Alt+ArrowDown opened, with the text half typed. Leaving the picker for anywhere else\n * reads whatever is typed.\n */\n const handleBlur = (event: FocusEvent<HTMLElement>) => {\n const next = event.relatedTarget as Element | null;\n const intoCalendar = next?.closest('[data-slot=\"popover-content\"]') != null;\n const intoButton = next?.closest('[data-slot=\"date-picker-trigger\"]') != null;\n const typed = refInput.current?.value ?? '';\n if (!(intoCalendar || intoButton || (next !== null && refField.current?.contains(next) === true))) {\n commit(typed);\n return;\n }\n if (event.target !== refInput.current) {\n return;\n }\n const onTheWay = intoCalendar || (intoButton && refPointer.current);\n const parsed = parseDateText(typed, locale);\n const usable = parsed.kind === 'empty' || (parsed.kind === 'day' && dayProblem(parsed.day, { min, max, disabledDates }) === undefined);\n if (usable || !onTheWay) {\n commit(typed);\n }\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'ArrowDown' && event.altKey && !disabled) {\n event.preventDefault();\n openFrom('field');\n return;\n }\n // The field has no key handler of Base UI's to close the calendar, so the key would reach the Sheet or Dialog around\n // it and close that too. While the calendar is open it is this field's key; when it is closed it is the page's.\n if (event.key === 'Escape' && open) {\n event.preventDefault();\n event.stopPropagation();\n // The focus is on the field already, whatever opened the calendar: it stays there.\n refOpener.current = 'field';\n setOpen(false);\n return;\n }\n if (event.key === 'Enter') {\n const parsed = parseDateText(event.currentTarget.value, locale);\n if (parsed.kind !== 'empty' && (parsed.kind === 'problem' || dayProblem(parsed.day, { min, max, disabledDates }) !== undefined)) {\n event.preventDefault();\n }\n commit(event.currentTarget.value);\n }\n };\n\n const selected = dayToDate(current);\n const limits = { min, max };\n const invalid = problem !== null || ariaInvalid === true || ariaInvalid === 'true';\n const ownMessage = problem !== null && formError === null;\n const describedBy =\n [...new Set([ariaDescribedBy, ownMessage ? errorId : undefined, formError ?? undefined].filter(Boolean))].join(' ') || undefined;\n const isOff = (date: Date): boolean => {\n const day = dateToDay(date);\n return day !== undefined && dayProblem(day, { min, max, disabledDates }) !== undefined;\n };\n const today = new Date();\n const start = dayToDate(min);\n const end = dayToDate(max);\n const month = selected ?? (start !== undefined && today < start ? start : end !== undefined && today > end ? end : today);\n\n return (\n <div data-slot=\"date-picker\" className={cn('flex flex-col gap-1.5', className)}>\n <div\n ref={refField}\n className=\"relative\"\n onPointerDownCapture={() => {\n refPointer.current = true;\n }}\n onPointerUpCapture={() => {\n refPointer.current = false;\n }}\n >\n <Input\n ref={refInput}\n id={id}\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"off\"\n data-slot=\"date-picker-input\"\n value={text}\n placeholder={placeholder ?? pattern}\n disabled={disabled}\n aria-label={ariaLabel}\n aria-invalid={invalid ? true : undefined}\n aria-describedby={describedBy}\n className=\"pr-9\"\n onChange={(event: ChangeEvent<HTMLInputElement>) => {\n setText(event.target.value);\n setProblem(null);\n }}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n />\n <Popover\n open={open}\n onOpenChange={(next) => {\n // Only the button opens it from here (Alt+ArrowDown in the field sets the state itself, through `openFrom`).\n if (next) {\n openFrom('button');\n return;\n }\n setOpen(false);\n }}\n >\n <PopoverTrigger\n render={\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n ref={refButton}\n data-slot=\"date-picker-trigger\"\n aria-label={labels.choose}\n disabled={disabled}\n className=\"absolute top-0.5 right-0.5\"\n onBlur={handleBlur}\n />\n }\n >\n <IconCalendar aria-hidden=\"true\" />\n </PopoverTrigger>\n <PopoverContent anchor={refField} align=\"start\" className=\"w-auto p-0\" finalFocus={focusAfterClose} onBlur={handleBlur}>\n <Calendar\n mode=\"single\"\n required\n autoFocus\n locale={calendarLocale}\n weekStartsOn={firstWeekdayOf(locale)}\n defaultMonth={month}\n startMonth={start}\n endMonth={end}\n selected={selected}\n disabled={isOff}\n onSelect={pick}\n />\n </PopoverContent>\n </Popover>\n </div>\n {ownMessage && (\n <p id={errorId} data-slot=\"date-picker-error\" role=\"alert\" className=\"text-sm text-destructive\">\n {problemText(problem, labels, locale, limits, pattern)}\n </p>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"date-picker.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/date-picker/date-picker.tsx"],"sourcesContent":["import { dateToDay, dayToDate } from '@edyn/ui/components/edyn/date-range-picker';\nimport { Button } from '@edyn/ui/components/ui/button';\nimport { Calendar } from '@edyn/ui/components/ui/calendar';\nimport { Input } from '@edyn/ui/components/ui/input';\nimport { Popover, PopoverContent, PopoverTrigger } from '@edyn/ui/components/ui/popover';\nimport { firstWeekdayOf } from '@edyn/ui/lib/first-weekday';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconCalendar } from '@tabler/icons-react';\nimport { type ChangeEvent, type FocusEvent, type KeyboardEvent, useEffect, useId, useMemo, useRef, useState } from 'react';\nimport type { Locale } from 'react-day-picker';\nimport { datePatternOf, dayProblem, formatDay, parseDateText, type TDateProblem, type TDisabledDays } from './date-text';\n\n/** Every word the component prints, so a product can translate it. */\nexport interface IDatePickerLabels {\n /** The calendar button's accessible name. */\n choose: string;\n /** Said when the text is not a date; `pattern` is what to type, written the locale's way (`DD/MM/YYYY`). */\n format: (pattern: string) => string;\n notReal: string;\n /** `min` and `max` are written the locale's way. */\n beforeMin: (min: string) => string;\n afterMax: (max: string) => string;\n unavailable: string;\n}\n\nexport const DEFAULT_DATE_PICKER_LABELS: IDatePickerLabels = {\n choose: 'Choose date',\n format: (pattern) => `Enter a date as ${pattern}.`,\n notReal: 'That date does not exist.',\n beforeMin: (min) => `Choose ${min} or later.`,\n afterMax: (max) => `Choose ${max} or earlier.`,\n unavailable: 'That date is not available.',\n};\n\n/**\n * What the field holds when it reports, so a parent can tell apart things that would otherwise look alike:\n * - `day`: a usable day, typed or picked.\n * - `empty`: the field was emptied, or nothing has been put in it. A required field is right to ask for a date.\n * - `refused`: the field holds text that is not a day (not a day, or not a real one). The day reported is `undefined`.\n * `text` is what was typed and `problem` the message the picker shows under the field, in the picker's `labels`.\n * - `out-of-range`: the field holds a real day that `min`, `max` or `disabledDates` refuse. The day IS reported (as the\n * first argument and as `day`), so a form's own rule can judge it and show its message; the field is marked invalid.\n * Check `kind` before treating the day as a usable one.\n *\n * Inside a Field that renders a FieldError, that message is the only one: the picker does not show its own.\n */\nexport type TDatePickerState =\n | { kind: 'day' }\n | { kind: 'empty' }\n | { kind: 'refused'; text: string; problem: string }\n | { kind: 'out-of-range'; day: string; text: string; problem: string };\n\nexport interface IDatePickerProps {\n /** For a Label's htmlFor; inside a Field the Field's label is already wired. */\n id?: string;\n /**\n * The chosen day as `yyyy-mm-dd`, or `undefined` for none (an empty string counts as none, so a value kept for an\n * `<input type=\"date\">` drops in). A string and not a Date, on purpose: a Date is an instant, and the instant for\n * \"5 March\" is another calendar day in another time zone, so a Date serialised for an API drifts a day. The text is\n * what `<input type=\"date\">`, JSON and a .NET `DateOnly` use, and it compares as a day.\n */\n value: string | undefined;\n /**\n * Called with a valid day, or with `undefined` when the field is emptied or its text is not a usable day. The second\n * argument says which of the two it was (see `TDatePickerState`); a caller that has no use for it can ignore it.\n * Text that is refused is reported each time it changes, also when the field held no day before, so a form that\n * tracks the state hears about it. Emptying the field after that reports `empty`.\n */\n onValueChange: (value: string | undefined, state: TDatePickerState) => void;\n /** The earliest and latest day that can be chosen, as `yyyy-mm-dd`, both included. */\n min?: string;\n max?: string;\n /** Days that cannot be chosen: a list of `yyyy-mm-dd`, or a function that says so for one (weekends, say). */\n disabledDates?: TDisabledDays;\n disabled?: boolean;\n /** How a day is written and in which order it is typed (BCP 47); en-GB reads 05/03/2026. */\n locale?: string;\n /** The calendar's own words (month and weekday names), as a date-fns locale; English by default. The first weekday follows `locale`. */\n calendarLocale?: Locale;\n /** What the empty field shows; the pattern for the locale (`DD/MM/YYYY`) by default. */\n placeholder?: string;\n /** An accessible name, when there is no visible label to point at. */\n 'aria-label'?: string;\n /** Marks the field invalid (the kit's destructive border), as a Field's invalid state does. */\n 'aria-invalid'?: boolean | 'true' | 'false';\n /** The id of the text that explains it. Added to what a Field names already, and to the picker's own error. */\n 'aria-describedby'?: string;\n labels?: Partial<IDatePickerLabels>;\n /** Layout only (a width); the look is the kit's. */\n className?: string;\n}\n\nconst problemText = (\n problem: TDateProblem,\n labels: IDatePickerLabels,\n locale: string,\n limits: { min?: string; max?: string },\n pattern: string,\n): string => {\n switch (problem) {\n case 'format':\n return labels.format(pattern);\n case 'notReal':\n return labels.notReal;\n case 'beforeMin':\n return labels.beforeMin(limits.min === undefined ? '' : formatDay(limits.min, locale));\n case 'afterMax':\n return labels.afterMax(limits.max === undefined ? '' : formatDay(limits.max, locale));\n default:\n return labels.unavailable;\n }\n};\n\n/**\n * One calendar day, typed or picked. The field takes the day in the locale's order (05/03/2026 in en-GB) and reads it\n * when it loses focus, saying inline what is wrong with it; the button, or Alt+ArrowDown in the field, opens a calendar.\n * Escape closes the calendar and gives the focus back to whichever of the two opened it; a day picked puts it on the field.\n */\nexport const DatePicker = ({\n id,\n value,\n onValueChange,\n min,\n max,\n disabledDates,\n disabled = false,\n locale = 'en-GB',\n calendarLocale,\n placeholder,\n 'aria-label': ariaLabel,\n 'aria-invalid': ariaInvalid,\n 'aria-describedby': ariaDescribedBy,\n labels: labelOverrides,\n className,\n}: IDatePickerProps) => {\n const labels = useMemo(() => ({ ...DEFAULT_DATE_PICKER_LABELS, ...labelOverrides }), [labelOverrides]);\n const pattern = useMemo(() => datePatternOf(locale), [locale]);\n const current = value === '' ? undefined : value;\n const errorId = useId();\n const refField = useRef<HTMLDivElement | null>(null);\n const refInput = useRef<HTMLInputElement | null>(null);\n const refButton = useRef<HTMLButtonElement | null>(null);\n // What opened the calendar, so that Escape gives the focus back to it: the button when the button did it, the field\n // when Alt+ArrowDown did. Choosing a day is different (see `pick`): the field then shows the day, so it gets the focus.\n const refOpener = useRef<'button' | 'field'>('button');\n const refPicked = useRef(false);\n // The mouse is down inside the picker, so a blur of the field is a click on something in it, not a Tab.\n const refPointer = useRef(false);\n // The day the field last agreed on with the page, so a change from outside is told from our own report of it.\n const refKnown = useRef<string | undefined>(current);\n // The refused text last reported (with its message), or null: told apart from a fresh refusal so it is reported once,\n // and so that emptying a field that held a refused text is reported although it held no day.\n const refRefused = useRef<string | null>(null);\n const [text, setText] = useState(current === undefined ? '' : formatDay(current, locale));\n const [problem, setProblem] = useState<TDateProblem | null>(null);\n const [open, setOpen] = useState(false);\n // The id of the FieldError of the Field around the picker, when it renders one: that message is the only one for the field.\n const [formError, setFormError] = useState<string | null>(null);\n\n useEffect(() => {\n const field = refField.current?.closest('[data-slot=\"field\"]');\n if (!field) {\n return;\n }\n const read = () => setFormError(field.querySelector('[data-slot=\"field-error\"]')?.id || null);\n read();\n const observer = new MutationObserver(read);\n observer.observe(field, { childList: true, subtree: true, attributes: true, attributeFilter: ['id'] });\n return () => {\n observer.disconnect();\n };\n }, []);\n\n useEffect(() => {\n if (current !== refKnown.current) {\n refKnown.current = current;\n refRefused.current = null;\n setText(current === undefined ? '' : formatDay(current, locale));\n setProblem(null);\n }\n }, [current, locale]);\n\n const report = (day: string | undefined, state: TDatePickerState) => {\n refKnown.current = day;\n refRefused.current = state.kind === 'refused' || state.kind === 'out-of-range' ? `${state.text}\\u0000${state.problem}` : null;\n onValueChange(day, state);\n };\n\n /**\n * Reads the text: a day is reported and written back in full, anything else is said inline and reported as no day,\n * with the state saying whether the field is empty or holds text it refused.\n */\n const commit = (typed: string) => {\n const parsed = parseDateText(typed, locale);\n if (parsed.kind === 'empty') {\n setProblem(null);\n if (current !== undefined || refRefused.current !== null) {\n report(undefined, { kind: 'empty' });\n }\n return;\n }\n const found = parsed.kind === 'problem' ? parsed.problem : dayProblem(parsed.day, { min, max, disabledDates });\n if (found !== undefined) {\n setProblem(found);\n const message = problemText(found, labels, locale, { min, max }, pattern);\n const seen = refRefused.current === `${typed}\\u0000${message}`;\n if (parsed.kind === 'day') {\n // A real day that the limits refuse: the text stays as typed and the day is reported, flagged, so that the form's own\n // rule can show its message instead of seeing an empty field.\n if (parsed.day !== current || !seen) {\n report(parsed.day, { kind: 'out-of-range', day: parsed.day, text: typed, problem: message });\n }\n return;\n }\n if (current !== undefined || !seen) {\n report(undefined, { kind: 'refused', text: typed, problem: message });\n }\n return;\n }\n if (parsed.kind === 'day') {\n setProblem(null);\n setText(formatDay(parsed.day, locale));\n if (parsed.day !== current || refRefused.current !== null) {\n report(parsed.day, { kind: 'day' });\n }\n }\n };\n\n const pick = (date: Date | undefined) => {\n const day = dateToDay(date);\n if (day === undefined) {\n return;\n }\n setProblem(null);\n setText(formatDay(day, locale));\n if (day !== current || refRefused.current !== null) {\n report(day, { kind: 'day' });\n }\n refPicked.current = true;\n setOpen(false);\n };\n\n const openFrom = (opener: 'button' | 'field') => {\n refOpener.current = opener;\n refPicked.current = false;\n setOpen(true);\n };\n\n /** Where the focus goes when the calendar closes: the field after a day is chosen, else whatever opened it. */\n const focusAfterClose = (): HTMLElement | null => (refPicked.current || refOpener.current === 'field' ? refInput.current : refButton.current);\n\n /**\n * Text is read as the person leaves the field. A day (or nothing) is taken at once; a message about text that is not a\n * day is shown too, unless they are only on the way to the calendar: a press of the mouse on the button, or focus\n * going into the calendar that Alt+ArrowDown opened, with the text half typed. Leaving the picker for anywhere else\n * reads whatever is typed.\n */\n const handleBlur = (event: FocusEvent<HTMLElement>) => {\n const next = event.relatedTarget as Element | null;\n const intoCalendar = next?.closest('[data-slot=\"popover-content\"]') != null;\n const intoButton = next?.closest('[data-slot=\"date-picker-trigger\"]') != null;\n const typed = refInput.current?.value ?? '';\n if (!(intoCalendar || intoButton || (next !== null && refField.current?.contains(next) === true))) {\n commit(typed);\n return;\n }\n if (event.target !== refInput.current) {\n return;\n }\n const onTheWay = intoCalendar || (intoButton && refPointer.current);\n const parsed = parseDateText(typed, locale);\n const usable = parsed.kind === 'empty' || (parsed.kind === 'day' && dayProblem(parsed.day, { min, max, disabledDates }) === undefined);\n if (usable || !onTheWay) {\n commit(typed);\n }\n };\n\n const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n if (event.key === 'ArrowDown' && event.altKey && !disabled) {\n event.preventDefault();\n openFrom('field');\n return;\n }\n // The field has no key handler of Base UI's to close the calendar, so the key would reach the Sheet or Dialog around\n // it and close that too. While the calendar is open it is this field's key; when it is closed it is the page's.\n if (event.key === 'Escape' && open) {\n event.preventDefault();\n event.stopPropagation();\n // The focus is on the field already, whatever opened the calendar: it stays there.\n refOpener.current = 'field';\n setOpen(false);\n return;\n }\n if (event.key === 'Enter') {\n const parsed = parseDateText(event.currentTarget.value, locale);\n if (parsed.kind !== 'empty' && (parsed.kind === 'problem' || dayProblem(parsed.day, { min, max, disabledDates }) !== undefined)) {\n event.preventDefault();\n }\n commit(event.currentTarget.value);\n }\n };\n\n const selected = dayToDate(current);\n const limits = { min, max };\n const invalid = problem !== null || ariaInvalid === true || ariaInvalid === 'true';\n const ownMessage = problem !== null && formError === null;\n const describedBy =\n [...new Set([ariaDescribedBy, ownMessage ? errorId : undefined, formError ?? undefined].filter(Boolean))].join(' ') || undefined;\n const isOff = (date: Date): boolean => {\n const day = dateToDay(date);\n return day !== undefined && dayProblem(day, { min, max, disabledDates }) !== undefined;\n };\n const today = new Date();\n const start = dayToDate(min);\n const end = dayToDate(max);\n const month = selected ?? (start !== undefined && today < start ? start : end !== undefined && today > end ? end : today);\n\n return (\n <div data-slot=\"date-picker\" className={cn('flex flex-col gap-1.5', className)}>\n <div\n ref={refField}\n className=\"relative\"\n onPointerDownCapture={() => {\n refPointer.current = true;\n }}\n onPointerUpCapture={() => {\n refPointer.current = false;\n }}\n >\n <Input\n ref={refInput}\n id={id}\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"off\"\n data-slot=\"date-picker-input\"\n value={text}\n placeholder={placeholder ?? pattern}\n disabled={disabled}\n aria-label={ariaLabel}\n aria-invalid={invalid ? true : undefined}\n aria-describedby={describedBy}\n className=\"pr-9\"\n onChange={(event: ChangeEvent<HTMLInputElement>) => {\n setText(event.target.value);\n setProblem(null);\n }}\n onBlur={handleBlur}\n onKeyDown={handleKeyDown}\n />\n <Popover\n open={open}\n onOpenChange={(next) => {\n // Only the button opens it from here (Alt+ArrowDown in the field sets the state itself, through `openFrom`).\n if (next) {\n openFrom('button');\n return;\n }\n setOpen(false);\n }}\n >\n <PopoverTrigger\n render={\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n ref={refButton}\n data-slot=\"date-picker-trigger\"\n aria-label={labels.choose}\n disabled={disabled}\n className=\"absolute top-0.5 right-0.5\"\n onBlur={handleBlur}\n />\n }\n >\n <IconCalendar aria-hidden=\"true\" />\n </PopoverTrigger>\n <PopoverContent\n anchor={refField}\n aria-label={labels.choose}\n align=\"start\"\n className=\"w-auto p-0\"\n finalFocus={focusAfterClose}\n onBlur={handleBlur}\n >\n <Calendar\n mode=\"single\"\n required\n autoFocus\n locale={calendarLocale}\n weekStartsOn={firstWeekdayOf(locale)}\n defaultMonth={month}\n startMonth={start}\n endMonth={end}\n selected={selected}\n disabled={isOff}\n onSelect={pick}\n />\n </PopoverContent>\n </Popover>\n </div>\n {ownMessage && (\n <p id={errorId} data-slot=\"date-picker-error\" role=\"alert\" className=\"text-sm text-destructive\">\n {problemText(problem, labels, locale, limits, pattern)}\n </p>\n )}\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -47,6 +47,7 @@ var DateRangePicker = ({ id, value, onValueChange, placeholder = "Select date ra
|
|
|
47
47
|
})]
|
|
48
48
|
}), /* @__PURE__ */ jsx(PopoverContent, {
|
|
49
49
|
align: "start",
|
|
50
|
+
"aria-label": ariaLabel ?? placeholder,
|
|
50
51
|
className: "w-auto p-0",
|
|
51
52
|
children: /* @__PURE__ */ jsx(Calendar, {
|
|
52
53
|
mode: "range",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"date-range-picker.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/date-range-picker/date-range-picker.tsx"],"sourcesContent":["import { Button } from '@edyn/ui/components/ui/button';\nimport { Calendar } from '@edyn/ui/components/ui/calendar';\nimport { Popover, PopoverContent, PopoverTrigger } from '@edyn/ui/components/ui/popover';\nimport { useIsMobile } from '@edyn/ui/hooks/use-mobile';\nimport { firstWeekdayOf } from '@edyn/ui/lib/first-weekday';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconCalendar } from '@tabler/icons-react';\nimport type { ComponentPropsWithoutRef } from 'react';\n\n/** A range of calendar days as yyyy-mm-dd, either end optional. */\nexport type TDateRange = { from?: string; to?: string };\n\n// A calendar day is read and written at midday local time, so no time zone or daylight\n// saving change can move it onto the day before or after.\nexport const dayToDate = (value: string | undefined): Date | undefined => (value === undefined ? undefined : new Date(`${value}T12:00:00`));\n\nexport const dateToDay = (date: Date | undefined): string | undefined =>\n date === undefined ? undefined : `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;\n\n/** Anything else a button takes (data-*, aria-*, title) goes to the trigger. */\nexport interface IDateRangePickerProps\n extends Omit<ComponentPropsWithoutRef<'button'>, 'value' | 'defaultValue' | 'placeholder' | 'id' | 'className' | 'children' | 'onChange' | 'type'> {\n id?: string;\n value: TDateRange;\n onValueChange: (value: TDateRange) => void;\n placeholder?: string;\n /**\n * What the trigger says once a range is set. `range` (default) writes the ends; `label` keeps the label, so a\n * range the page shows elsewhere (as a removable chip, say) is not repeated here. In both, the trigger carries\n * `data-set` while either end is set, for a look of its own.\n */\n summary?: 'range' | 'label';\n /** How the ends are written on the trigger, and which weekday the calendar starts on (BCP 47); en-GB reads 05 Mar 2026 and starts on Monday. */\n locale?: string;\n /** An accessible name for the trigger, when there is no visible label to point at. */\n 'aria-label'?: string;\n className?: string;\n}\n\n/**\n * The trigger's accessible name when it is given an `aria-label`: the label, then what the button shows (\"Created between:\n * 01 Mar 2026 – 05 Mar 2026\"). An aria-label replaces the text inside the button, so on its own it hides the chosen dates from\n * a screen reader and breaks WCAG 2.5.3 (Label in Name: the visible text must be part of the name, so that a person who says\n * what they see can operate it). A label that already contains the visible text is left as it is.\n */\nconst nameWith = (label: string | undefined, shown: string): string | undefined => {\n if (label === undefined) {\n return undefined;\n }\n return label.toLowerCase().includes(shown.toLowerCase()) ? label : `${label}: ${shown}`;\n};\n\n/** A range of calendar days, either end optional; two months side by side, one on a phone. */\nexport const DateRangePicker = ({\n id,\n value,\n onValueChange,\n placeholder = 'Select date range',\n locale = 'en-GB',\n summary = 'range',\n 'aria-label': ariaLabel,\n className,\n ...props\n}: IDateRangePickerProps) => {\n const isMobile = useIsMobile();\n const from = dayToDate(value.from);\n const to = dayToDate(value.to);\n const format = new Intl.DateTimeFormat(locale, { day: '2-digit', month: 'short', year: 'numeric' });\n const ends = [from, to].filter((date) => date !== undefined).map((date) => format.format(date));\n const shown = ends.length === 0 || summary === 'label' ? placeholder : ends.join(' – ');\n return (\n <Popover>\n <PopoverTrigger\n render={\n <Button\n {...props}\n id={id}\n aria-label={nameWith(ariaLabel, shown)}\n data-slot=\"date-range-picker-trigger\"\n data-set={ends.length > 0 ? '' : undefined}\n variant=\"outline\"\n className={cn('w-full min-w-0 justify-start font-normal', ends.length === 0 && 'text-muted-foreground', className)}\n />\n }\n >\n <IconCalendar aria-hidden=\"true\" />\n <span className=\"truncate\">{shown}</span>\n </PopoverTrigger>\n <PopoverContent align=\"start\" className=\"w-auto p-0\">\n <Calendar\n mode=\"range\"\n defaultMonth={from}\n numberOfMonths={isMobile ? 1 : 2}\n weekStartsOn={firstWeekdayOf(locale)}\n selected={{ from, to }}\n onSelect={(range) => onValueChange({ from: dateToDay(range?.from), to: dateToDay(range?.to) })}\n />\n </PopoverContent>\n </Popover>\n );\n};\n"],"mappings":";;;;;;;;;AAcA,IAAa,aAAa,UAAiD,UAAU,KAAA,IAAY,KAAA,oBAAY,IAAI,KAAK,GAAG,MAAM,UAAU;AAEzI,IAAa,aAAa,SACxB,SAAS,KAAA,IAAY,KAAA,IAAY,GAAG,KAAK,YAAY,EAAE,GAAG,OAAO,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,GAAG,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;;;;;;;AA4BlJ,IAAM,YAAY,OAA2B,UAAsC;CACjF,IAAI,UAAU,KAAA,GACZ;CAEF,OAAO,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,YAAY,CAAC,IAAI,QAAQ,GAAG,MAAM,IAAI;AAClF;;AAGA,IAAa,mBAAmB,EAC9B,IACA,OACA,eACA,cAAc,qBACd,SAAS,SACT,UAAU,SACV,cAAc,WACd,WACA,GAAG,YACwB;CAC3B,MAAM,WAAW,YAAY;CAC7B,MAAM,OAAO,UAAU,MAAM,IAAI;CACjC,MAAM,KAAK,UAAU,MAAM,EAAE;CAC7B,MAAM,SAAS,IAAI,KAAK,eAAe,QAAQ;EAAE,KAAK;EAAW,OAAO;EAAS,MAAM;CAAU,CAAC;CAClG,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,SAAS,SAAS,KAAA,CAAS,CAAC,CAAC,KAAK,SAAS,OAAO,OAAO,IAAI,CAAC;CAC9F,MAAM,QAAQ,KAAK,WAAW,KAAK,YAAY,UAAU,cAAc,KAAK,KAAK,KAAK;CACtF,OACE,qBAAC,SAAD,EAAA,UAAA,CACE,qBAAC,gBAAD;EACE,QACE,oBAAC,QAAD;GACE,GAAI;GACA;GACJ,cAAY,SAAS,WAAW,KAAK;GACrC,aAAU;GACV,YAAU,KAAK,SAAS,IAAI,KAAK,KAAA;GACjC,SAAQ;GACR,WAAW,GAAG,4CAA4C,KAAK,WAAW,KAAK,yBAAyB,SAAS;EAClH,CAAA;EAVL,UAAA,CAaE,oBAAC,cAAD,EAAc,eAAY,OAAQ,CAAA,GAClC,oBAAC,QAAD;GAAM,WAAU;GAAY,UAAA;EAAY,CAAA,CAC1B;CAChB,CAAA,GAAA,oBAAC,gBAAD;EAAgB,OAAM;EAAQ,WAAU;
|
|
1
|
+
{"version":3,"file":"date-range-picker.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/date-range-picker/date-range-picker.tsx"],"sourcesContent":["import { Button } from '@edyn/ui/components/ui/button';\nimport { Calendar } from '@edyn/ui/components/ui/calendar';\nimport { Popover, PopoverContent, PopoverTrigger } from '@edyn/ui/components/ui/popover';\nimport { useIsMobile } from '@edyn/ui/hooks/use-mobile';\nimport { firstWeekdayOf } from '@edyn/ui/lib/first-weekday';\nimport { cn } from '@edyn/ui/lib/utils';\nimport { IconCalendar } from '@tabler/icons-react';\nimport type { ComponentPropsWithoutRef } from 'react';\n\n/** A range of calendar days as yyyy-mm-dd, either end optional. */\nexport type TDateRange = { from?: string; to?: string };\n\n// A calendar day is read and written at midday local time, so no time zone or daylight\n// saving change can move it onto the day before or after.\nexport const dayToDate = (value: string | undefined): Date | undefined => (value === undefined ? undefined : new Date(`${value}T12:00:00`));\n\nexport const dateToDay = (date: Date | undefined): string | undefined =>\n date === undefined ? undefined : `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;\n\n/** Anything else a button takes (data-*, aria-*, title) goes to the trigger. */\nexport interface IDateRangePickerProps\n extends Omit<ComponentPropsWithoutRef<'button'>, 'value' | 'defaultValue' | 'placeholder' | 'id' | 'className' | 'children' | 'onChange' | 'type'> {\n id?: string;\n value: TDateRange;\n onValueChange: (value: TDateRange) => void;\n placeholder?: string;\n /**\n * What the trigger says once a range is set. `range` (default) writes the ends; `label` keeps the label, so a\n * range the page shows elsewhere (as a removable chip, say) is not repeated here. In both, the trigger carries\n * `data-set` while either end is set, for a look of its own.\n */\n summary?: 'range' | 'label';\n /** How the ends are written on the trigger, and which weekday the calendar starts on (BCP 47); en-GB reads 05 Mar 2026 and starts on Monday. */\n locale?: string;\n /** An accessible name for the trigger, when there is no visible label to point at. */\n 'aria-label'?: string;\n className?: string;\n}\n\n/**\n * The trigger's accessible name when it is given an `aria-label`: the label, then what the button shows (\"Created between:\n * 01 Mar 2026 – 05 Mar 2026\"). An aria-label replaces the text inside the button, so on its own it hides the chosen dates from\n * a screen reader and breaks WCAG 2.5.3 (Label in Name: the visible text must be part of the name, so that a person who says\n * what they see can operate it). A label that already contains the visible text is left as it is.\n */\nconst nameWith = (label: string | undefined, shown: string): string | undefined => {\n if (label === undefined) {\n return undefined;\n }\n return label.toLowerCase().includes(shown.toLowerCase()) ? label : `${label}: ${shown}`;\n};\n\n/** A range of calendar days, either end optional; two months side by side, one on a phone. */\nexport const DateRangePicker = ({\n id,\n value,\n onValueChange,\n placeholder = 'Select date range',\n locale = 'en-GB',\n summary = 'range',\n 'aria-label': ariaLabel,\n className,\n ...props\n}: IDateRangePickerProps) => {\n const isMobile = useIsMobile();\n const from = dayToDate(value.from);\n const to = dayToDate(value.to);\n const format = new Intl.DateTimeFormat(locale, { day: '2-digit', month: 'short', year: 'numeric' });\n const ends = [from, to].filter((date) => date !== undefined).map((date) => format.format(date));\n const shown = ends.length === 0 || summary === 'label' ? placeholder : ends.join(' – ');\n return (\n <Popover>\n <PopoverTrigger\n render={\n <Button\n {...props}\n id={id}\n aria-label={nameWith(ariaLabel, shown)}\n data-slot=\"date-range-picker-trigger\"\n data-set={ends.length > 0 ? '' : undefined}\n variant=\"outline\"\n className={cn('w-full min-w-0 justify-start font-normal', ends.length === 0 && 'text-muted-foreground', className)}\n />\n }\n >\n <IconCalendar aria-hidden=\"true\" />\n <span className=\"truncate\">{shown}</span>\n </PopoverTrigger>\n <PopoverContent align=\"start\" aria-label={ariaLabel ?? placeholder} className=\"w-auto p-0\">\n <Calendar\n mode=\"range\"\n defaultMonth={from}\n numberOfMonths={isMobile ? 1 : 2}\n weekStartsOn={firstWeekdayOf(locale)}\n selected={{ from, to }}\n onSelect={(range) => onValueChange({ from: dateToDay(range?.from), to: dateToDay(range?.to) })}\n />\n </PopoverContent>\n </Popover>\n );\n};\n"],"mappings":";;;;;;;;;AAcA,IAAa,aAAa,UAAiD,UAAU,KAAA,IAAY,KAAA,oBAAY,IAAI,KAAK,GAAG,MAAM,UAAU;AAEzI,IAAa,aAAa,SACxB,SAAS,KAAA,IAAY,KAAA,IAAY,GAAG,KAAK,YAAY,EAAE,GAAG,OAAO,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,GAAG,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;;;;;;;AA4BlJ,IAAM,YAAY,OAA2B,UAAsC;CACjF,IAAI,UAAU,KAAA,GACZ;CAEF,OAAO,MAAM,YAAY,CAAC,CAAC,SAAS,MAAM,YAAY,CAAC,IAAI,QAAQ,GAAG,MAAM,IAAI;AAClF;;AAGA,IAAa,mBAAmB,EAC9B,IACA,OACA,eACA,cAAc,qBACd,SAAS,SACT,UAAU,SACV,cAAc,WACd,WACA,GAAG,YACwB;CAC3B,MAAM,WAAW,YAAY;CAC7B,MAAM,OAAO,UAAU,MAAM,IAAI;CACjC,MAAM,KAAK,UAAU,MAAM,EAAE;CAC7B,MAAM,SAAS,IAAI,KAAK,eAAe,QAAQ;EAAE,KAAK;EAAW,OAAO;EAAS,MAAM;CAAU,CAAC;CAClG,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,QAAQ,SAAS,SAAS,KAAA,CAAS,CAAC,CAAC,KAAK,SAAS,OAAO,OAAO,IAAI,CAAC;CAC9F,MAAM,QAAQ,KAAK,WAAW,KAAK,YAAY,UAAU,cAAc,KAAK,KAAK,KAAK;CACtF,OACE,qBAAC,SAAD,EAAA,UAAA,CACE,qBAAC,gBAAD;EACE,QACE,oBAAC,QAAD;GACE,GAAI;GACA;GACJ,cAAY,SAAS,WAAW,KAAK;GACrC,aAAU;GACV,YAAU,KAAK,SAAS,IAAI,KAAK,KAAA;GACjC,SAAQ;GACR,WAAW,GAAG,4CAA4C,KAAK,WAAW,KAAK,yBAAyB,SAAS;EAClH,CAAA;EAVL,UAAA,CAaE,oBAAC,cAAD,EAAc,eAAY,OAAQ,CAAA,GAClC,oBAAC,QAAD;GAAM,WAAU;GAAY,UAAA;EAAY,CAAA,CAC1B;CAChB,CAAA,GAAA,oBAAC,gBAAD;EAAgB,OAAM;EAAQ,cAAY,aAAa;EAAa,WAAU;EAC5E,UAAA,oBAAC,UAAD;GACE,MAAK;GACL,cAAc;GACd,gBAAgB,WAAW,IAAI;GAC/B,cAAc,eAAe,MAAM;GACnC,UAAU;IAAE;IAAM;GAAG;GACrB,WAAW,UAAU,cAAc;IAAE,MAAM,UAAU,OAAO,IAAI;IAAG,IAAI,UAAU,OAAO,EAAE;GAAE,CAAC;EAC9F,CAAA;CACa,CAAA,CACT,EAAA,CAAA;AAEb"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** A date and a time of day kept as the text `<input type="datetime-local">` writes: `yyyy-mm-ddTHH:mm`, or `yyyy-mm-ddTHH:mm:ss`. */
|
|
2
|
+
export interface IDateTimeParts {
|
|
3
|
+
/** `yyyy-mm-dd`, or '' for none. */
|
|
4
|
+
day: string;
|
|
5
|
+
/** `HH:mm` or `HH:mm:ss`, or '' for none. */
|
|
6
|
+
time: string;
|
|
7
|
+
}
|
|
8
|
+
/** The two halves of a value; either can be missing. */
|
|
9
|
+
export declare const splitDateTime: (value: string | undefined) => IDateTimeParts;
|
|
10
|
+
/** The value both halves make, or undefined while one is missing. */
|
|
11
|
+
export declare const joinDateTime: ({ day, time }: IDateTimeParts) => string | undefined;
|
|
12
|
+
/** The day part of a limit, which may be a bare day. */
|
|
13
|
+
export declare const dayOf: (limit: string | undefined) => string | undefined;
|
|
14
|
+
/** The times a day allows: a limit's time holds on the limit's own day only. */
|
|
15
|
+
export declare const timeLimits: (day: string, min: string | undefined, max: string | undefined) => {
|
|
16
|
+
min?: string;
|
|
17
|
+
max?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Whether a time (`HH:mm` or `HH:mm:ss`) is before the earliest or after the latest. */
|
|
20
|
+
export declare const timeOutside: (time: string, { min, max }: {
|
|
21
|
+
min?: string;
|
|
22
|
+
max?: string;
|
|
23
|
+
}) => boolean;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region ../ui/src/components/edyn/date-time/date-time-value.ts
|
|
2
|
+
/** The two halves of a value; either can be missing. */
|
|
3
|
+
var splitDateTime = (value) => {
|
|
4
|
+
const [day = "", time = ""] = (value ?? "").split("T");
|
|
5
|
+
return {
|
|
6
|
+
day,
|
|
7
|
+
time
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
/** The value both halves make, or undefined while one is missing. */
|
|
11
|
+
var joinDateTime = ({ day, time }) => day !== "" && time !== "" ? `${day}T${time}` : void 0;
|
|
12
|
+
/** The day part of a limit, which may be a bare day. */
|
|
13
|
+
var dayOf = (limit) => limit === void 0 || limit === "" ? void 0 : limit.slice(0, 10);
|
|
14
|
+
/** The times a day allows: a limit's time holds on the limit's own day only. */
|
|
15
|
+
var timeLimits = (day, min, max) => {
|
|
16
|
+
const limits = {};
|
|
17
|
+
if (min !== void 0 && min.length > 11 && day === min.slice(0, 10)) limits.min = min.slice(11);
|
|
18
|
+
if (max !== void 0 && max.length > 11 && day === max.slice(0, 10)) limits.max = max.slice(11);
|
|
19
|
+
return limits;
|
|
20
|
+
};
|
|
21
|
+
/** Whether a time (`HH:mm` or `HH:mm:ss`) is before the earliest or after the latest. */
|
|
22
|
+
var timeOutside = (time, { min, max }) => time !== "" && (min !== void 0 && time.slice(0, min.length) < min || max !== void 0 && time.slice(0, max.length) > max);
|
|
23
|
+
//#endregion
|
|
24
|
+
export { dayOf, joinDateTime, splitDateTime, timeLimits, timeOutside };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=date-time-value.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"date-time-value.js","names":[],"sources":["../../../../../../../ui/src/components/edyn/date-time/date-time-value.ts"],"sourcesContent":["/** A date and a time of day kept as the text `<input type=\"datetime-local\">` writes: `yyyy-mm-ddTHH:mm`, or `yyyy-mm-ddTHH:mm:ss`. */\n\nexport interface IDateTimeParts {\n /** `yyyy-mm-dd`, or '' for none. */\n day: string;\n /** `HH:mm` or `HH:mm:ss`, or '' for none. */\n time: string;\n}\n\n/** The two halves of a value; either can be missing. */\nexport const splitDateTime = (value: string | undefined): IDateTimeParts => {\n const [day = '', time = ''] = (value ?? '').split('T');\n return { day, time };\n};\n\n/** The value both halves make, or undefined while one is missing. */\nexport const joinDateTime = ({ day, time }: IDateTimeParts): string | undefined => (day !== '' && time !== '' ? `${day}T${time}` : undefined);\n\n/** The day part of a limit, which may be a bare day. */\nexport const dayOf = (limit: string | undefined): string | undefined => (limit === undefined || limit === '' ? undefined : limit.slice(0, 10));\n\n/** The times a day allows: a limit's time holds on the limit's own day only. */\nexport const timeLimits = (day: string, min: string | undefined, max: string | undefined): { min?: string; max?: string } => {\n const limits: { min?: string; max?: string } = {};\n if (min !== undefined && min.length > 11 && day === min.slice(0, 10)) {\n limits.min = min.slice(11);\n }\n if (max !== undefined && max.length > 11 && day === max.slice(0, 10)) {\n limits.max = max.slice(11);\n }\n return limits;\n};\n\n/** Whether a time (`HH:mm` or `HH:mm:ss`) is before the earliest or after the latest. */\nexport const timeOutside = (time: string, { min, max }: { min?: string; max?: string }): boolean =>\n time !== '' && ((min !== undefined && time.slice(0, min.length) < min) || (max !== undefined && time.slice(0, max.length) > max));\n"],"mappings":";;AAUA,IAAa,iBAAiB,UAA8C;CAC1E,MAAM,CAAC,MAAM,IAAI,OAAO,OAAO,SAAS,GAAA,CAAI,MAAM,GAAG;CACrD,OAAO;EAAE;EAAK;CAAK;AACrB;;AAGA,IAAa,gBAAgB,EAAE,KAAK,WAAgD,QAAQ,MAAM,SAAS,KAAK,GAAG,IAAI,GAAG,SAAS,KAAA;;AAGnI,IAAa,SAAS,UAAmD,UAAU,KAAA,KAAa,UAAU,KAAK,KAAA,IAAY,MAAM,MAAM,GAAG,EAAE;;AAG5I,IAAa,cAAc,KAAa,KAAyB,QAA4D;CAC3H,MAAM,SAAyC,CAAC;CAChD,IAAI,QAAQ,KAAA,KAAa,IAAI,SAAS,MAAM,QAAQ,IAAI,MAAM,GAAG,EAAE,GACjE,OAAO,MAAM,IAAI,MAAM,EAAE;CAE3B,IAAI,QAAQ,KAAA,KAAa,IAAI,SAAS,MAAM,QAAQ,IAAI,MAAM,GAAG,EAAE,GACjE,OAAO,MAAM,IAAI,MAAM,EAAE;CAE3B,OAAO;AACT;;AAGA,IAAa,eAAe,MAAc,EAAE,KAAK,UAC/C,SAAS,OAAQ,QAAQ,KAAA,KAAa,KAAK,MAAM,GAAG,IAAI,MAAM,IAAI,OAAS,QAAQ,KAAA,KAAa,KAAK,MAAM,GAAG,IAAI,MAAM,IAAI"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|