@seekalfred/primitives 0.7.3 → 0.7.4
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 +60 -1
- package/dist/alert/alert.d.ts +2 -2
- package/dist/alert-dialog/alert-dialog.d.ts +14 -0
- package/dist/alert-dialog/alert-dialog.js +51 -26
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/announcement-bar/announcement-bar.d.ts +15 -4
- package/dist/announcement-bar/announcement-bar.js +31 -6
- package/dist/announcement-bar/announcement-bar.js.map +1 -1
- package/dist/avatar/avatar.d.ts +7 -0
- package/dist/avatar/avatar.js +33 -12
- package/dist/avatar/avatar.js.map +1 -1
- package/dist/badge/badge.d.ts +1 -1
- package/dist/bar-chart/bar-chart-series.d.ts +75 -0
- package/dist/bar-chart/bar-chart-series.js +401 -0
- package/dist/bar-chart/bar-chart-series.js.map +1 -0
- package/dist/bar-chart/bar-chart.d.ts +25 -1
- package/dist/bar-chart/bar-chart.js +26 -1
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/index.d.ts +2 -1
- package/dist/button/button.d.ts +2 -2
- package/dist/chip-group/chip-group.d.ts +11 -0
- package/dist/chip-group/chip-group.js +50 -34
- package/dist/chip-group/chip-group.js.map +1 -1
- package/dist/chip-group/index.d.ts +2 -2
- package/dist/copy-button/copy-button.d.ts +19 -3
- package/dist/copy-button/copy-button.js +7 -4
- package/dist/copy-button/copy-button.js.map +1 -1
- package/dist/file-drop-zone/file-drop-zone.d.ts +29 -3
- package/dist/file-drop-zone/file-drop-zone.js +26 -8
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
- package/dist/gauge/gauge.d.ts +18 -1
- package/dist/gauge/gauge.js +35 -21
- package/dist/gauge/gauge.js.map +1 -1
- package/dist/gauge/index.d.ts +2 -2
- package/dist/index.d.ts +8 -7
- package/dist/index.js +4 -4
- package/dist/input/input.d.ts +7 -0
- package/dist/input/input.js +3 -1
- package/dist/input/input.js.map +1 -1
- package/dist/lib/field-shell.js +2 -6
- package/dist/lib/field-shell.js.map +1 -1
- package/dist/lib/required-marker.js +20 -0
- package/dist/lib/required-marker.js.map +1 -0
- package/dist/line-chart/index.d.ts +2 -2
- package/dist/line-chart/line-chart.d.ts +105 -3
- package/dist/line-chart/line-chart.js +183 -44
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/money-input/index.d.ts +2 -2
- package/dist/money-input/money-input.d.ts +14 -0
- package/dist/money-input/money-input.js +17 -7
- package/dist/money-input/money-input.js.map +1 -1
- package/dist/morph-select/morph-select.d.ts +8 -1
- package/dist/morph-select/morph-select.js +8 -5
- package/dist/morph-select/morph-select.js.map +1 -1
- package/dist/multi-select/multi-select.d.ts +31 -0
- package/dist/multi-select/multi-select.js +122 -5
- package/dist/multi-select/multi-select.js.map +1 -1
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/index.js +2 -2
- package/dist/progress/progress.d.ts +53 -2
- package/dist/progress/progress.js +146 -36
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/radio-group.d.ts +1 -1
- package/dist/scrub-input/scrub-input.d.ts +11 -1
- package/dist/scrub-input/scrub-input.js +22 -10
- package/dist/scrub-input/scrub-input.js.map +1 -1
- package/dist/select/select.d.ts +9 -0
- package/dist/select/select.js +3 -2
- package/dist/select/select.js.map +1 -1
- package/dist/sparkline/sparkline.d.ts +27 -2
- package/dist/sparkline/sparkline.js +115 -18
- package/dist/sparkline/sparkline.js.map +1 -1
- package/dist/stat-card/index.d.ts +2 -2
- package/dist/stat-card/index.js +2 -2
- package/dist/stat-card/stat-card.d.ts +32 -1
- package/dist/stat-card/stat-card.js +52 -9
- package/dist/stat-card/stat-card.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +114 -4
- package/dist/table/table.js +374 -94
- package/dist/table/table.js.map +1 -1
- package/dist/text-shimmer/text-shimmer.d.ts +18 -2
- package/dist/text-shimmer/text-shimmer.js +5 -2
- package/dist/text-shimmer/text-shimmer.js.map +1 -1
- package/dist/textarea/textarea.d.ts +7 -0
- package/dist/textarea/textarea.js +3 -1
- package/dist/textarea/textarea.js.map +1 -1
- package/dist/toast/toast.d.ts +2 -2
- package/package.json +3 -3
|
@@ -12,6 +12,11 @@ import { Key, SelectProps } from "react-aria-components";
|
|
|
12
12
|
* in beside the chevron when anything is picked. Options are `SelectItem`s, whose
|
|
13
13
|
* tick pops in as an option is picked; `MultiSelectGroup` gathers them under a
|
|
14
14
|
* heading.
|
|
15
|
+
*
|
|
16
|
+
* Opt in to `searchable` for a search field at the top of the list that filters
|
|
17
|
+
* the options as you type (React Aria's Autocomplete, so the arrow keys and Enter
|
|
18
|
+
* work from the field). Pass `onCreate` to also offer a "Create “…”" row when the
|
|
19
|
+
* typed text matches no option.
|
|
15
20
|
*/
|
|
16
21
|
export interface MultiSelectProps extends Omit<SelectProps<object, "multiple">, "className" | "style" | "children" | "items" | "selectionMode"> {
|
|
17
22
|
/** `SelectItem` children. */
|
|
@@ -43,6 +48,32 @@ export interface MultiSelectProps extends Omit<SelectProps<object, "multiple">,
|
|
|
43
48
|
size?: "sm" | "md" | "lg";
|
|
44
49
|
/** Extra classes for the wrapper. Merged last. */
|
|
45
50
|
className?: string;
|
|
51
|
+
/**
|
|
52
|
+
* Puts a search field at the top of the list. Typing filters the options (and
|
|
53
|
+
* groups) by their text, ignoring case and accents; the arrow keys and Enter
|
|
54
|
+
* work from the field. Cleared each time the list closes. Off by default.
|
|
55
|
+
* Turned on by `onCreate` too.
|
|
56
|
+
*
|
|
57
|
+
* @default false
|
|
58
|
+
*/
|
|
59
|
+
searchable?: boolean;
|
|
60
|
+
/** Accessible name of the search field. @default "Search options" */
|
|
61
|
+
searchLabel?: string;
|
|
62
|
+
/** Placeholder of the search field. @default "Search" */
|
|
63
|
+
searchPlaceholder?: string;
|
|
64
|
+
/** Shown in the list while the search matches nothing. @default "No options found" */
|
|
65
|
+
emptyLabel?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Makes the list creatable, and searchable. While the typed text (trimmed)
|
|
68
|
+
* matches no option's text or id nor a picked value, ignoring case, the list ends
|
|
69
|
+
* with a "Create “text”" row. Picking it calls `onCreate` with the text, adds
|
|
70
|
+
* the text itself as a picked key (through `onChange`, as any pick) and clears
|
|
71
|
+
* the search. Render a `SelectItem` whose `id` is that text so it lists like
|
|
72
|
+
* the others; until then its chip shows the text.
|
|
73
|
+
*/
|
|
74
|
+
onCreate?: (value: string) => void;
|
|
75
|
+
/** The create row's text for the typed value. @default (value) => `Create “${value}”` */
|
|
76
|
+
createLabel?: (value: string) => string;
|
|
46
77
|
}
|
|
47
78
|
export declare const MultiSelect: import("react").ForwardRefExoticComponent<MultiSelectProps & import("react").RefAttributes<HTMLButtonElement>>;
|
|
48
79
|
export declare const multiSelectGroupVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
@@ -4,12 +4,95 @@ import { SwapText } from "../lib/swap-text.js";
|
|
|
4
4
|
import { useFlip } from "../lib/flip.js";
|
|
5
5
|
import { Tooltip as Tooltip$1, TooltipTrigger as TooltipTrigger$1 } from "../tooltip/tooltip.js";
|
|
6
6
|
import { labelVariants } from "../label/label.js";
|
|
7
|
-
import { selectPopoverVariants, selectTriggerVariants, selectVariants } from "../select/select.js";
|
|
7
|
+
import { selectItemVariants, selectPopoverVariants, selectTriggerVariants, selectVariants } from "../select/select.js";
|
|
8
8
|
import { cva } from "class-variance-authority";
|
|
9
9
|
import { forwardRef, useContext, useRef, useState } from "react";
|
|
10
|
-
import { Button, ButtonContext, FieldError, Header, Label, ListBox, ListBoxSection, Popover, Select, SelectStateContext, SelectValue } from "react-aria-components";
|
|
10
|
+
import { Autocomplete, Button, ButtonContext, FieldError, Header, Input, Label, ListBox, ListBoxItem, ListBoxSection, Popover, SearchField, Select, SelectStateContext, SelectValue, useFilter } from "react-aria-components";
|
|
11
11
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
12
12
|
//#region src/multi-select/multi-select.tsx
|
|
13
|
+
/** The key of the create row. Never reaches `onChange`. */
|
|
14
|
+
const CREATE_KEY = "__multi-select-create__";
|
|
15
|
+
const defaultCreateLabel = (value) => `Create \u201c${value}\u201d`;
|
|
16
|
+
/** Whether `text` already names an option (by its text or its key) or a pick, ignoring case. */
|
|
17
|
+
function isTaken(state, text) {
|
|
18
|
+
const want = text.toLocaleLowerCase();
|
|
19
|
+
if ((state?.value ?? []).some((key) => String(key).toLocaleLowerCase() === want)) return true;
|
|
20
|
+
if (!state) return false;
|
|
21
|
+
for (const key of state.collection.getKeys()) {
|
|
22
|
+
const node = state.collection.getItem(key);
|
|
23
|
+
if (node?.type === "item" && node.key !== CREATE_KEY && (node.textValue.toLocaleLowerCase() === want || String(node.key).toLocaleLowerCase() === want)) return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The list with a search field over it (React Aria's Autocomplete): the field
|
|
29
|
+
* keeps focus and drives the options with the arrow keys, Enter picks.
|
|
30
|
+
*/
|
|
31
|
+
function SearchList({ query, onQueryChange, creatable, searchLabel, searchPlaceholder, emptyLabel, children }) {
|
|
32
|
+
const state = useContext(SelectStateContext);
|
|
33
|
+
const { contains } = useFilter({ sensitivity: "base" });
|
|
34
|
+
const text = query.trim();
|
|
35
|
+
const offerCreate = creatable && text !== "" && !isTaken(state, text);
|
|
36
|
+
return /* @__PURE__ */ jsxs(Autocomplete, {
|
|
37
|
+
inputValue: query,
|
|
38
|
+
onInputChange: onQueryChange,
|
|
39
|
+
filter: (textValue, input, node) => node.key === CREATE_KEY ? offerCreate : contains(textValue, input),
|
|
40
|
+
children: [/* @__PURE__ */ jsxs(SearchField, {
|
|
41
|
+
"aria-label": searchLabel,
|
|
42
|
+
autoFocus: true,
|
|
43
|
+
"data-slot": "multi-select-search",
|
|
44
|
+
className: multiSelectSearchClass,
|
|
45
|
+
children: [/* @__PURE__ */ jsxs("svg", {
|
|
46
|
+
"aria-hidden": "true",
|
|
47
|
+
viewBox: "0 0 16 16",
|
|
48
|
+
fill: "none",
|
|
49
|
+
stroke: "currentColor",
|
|
50
|
+
strokeWidth: "1.75",
|
|
51
|
+
strokeLinecap: "round",
|
|
52
|
+
className: "size-4 flex-none text-muted-foreground",
|
|
53
|
+
children: [/* @__PURE__ */ jsx("circle", {
|
|
54
|
+
cx: "7",
|
|
55
|
+
cy: "7",
|
|
56
|
+
r: "4.5"
|
|
57
|
+
}), /* @__PURE__ */ jsx("path", { d: "m10.5 10.5 3 3" })]
|
|
58
|
+
}), /* @__PURE__ */ jsx(Input, {
|
|
59
|
+
placeholder: searchPlaceholder,
|
|
60
|
+
"data-slot": "multi-select-search-input",
|
|
61
|
+
className: "min-w-0 flex-1 appearance-none border-0 bg-transparent p-0 font-sans text-sm text-foreground outline-none placeholder:text-muted-foreground [&::-webkit-search-cancel-button]:hidden"
|
|
62
|
+
})]
|
|
63
|
+
}), /* @__PURE__ */ jsx(ListBox, {
|
|
64
|
+
"data-slot": "multi-select-list",
|
|
65
|
+
renderEmptyState: () => /* @__PURE__ */ jsx("p", {
|
|
66
|
+
"data-slot": "multi-select-empty",
|
|
67
|
+
className: "m-0 px-2 py-1.5 text-sm text-muted-foreground",
|
|
68
|
+
children: emptyLabel
|
|
69
|
+
}),
|
|
70
|
+
className: "min-h-0 flex-1 overflow-y-auto outline-none",
|
|
71
|
+
children
|
|
72
|
+
})]
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/** The search row: a quiet field over the list, underlined while focused. */
|
|
76
|
+
const multiSelectSearchClass = "relative mb-1 flex h-(--density-control-h-sm) flex-none items-center gap-2 border-b-(length:--border-width) border-border px-2 after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-(--focus-ring-width) after:bg-transparent has-[input:focus]:after:bg-(--color-control-edge)";
|
|
77
|
+
/** The row that creates the typed value. Kept in the collection; the filter shows it. */
|
|
78
|
+
function CreateItem({ label }) {
|
|
79
|
+
return /* @__PURE__ */ jsxs(ListBoxItem, {
|
|
80
|
+
id: CREATE_KEY,
|
|
81
|
+
textValue: label,
|
|
82
|
+
"data-slot": "multi-select-create",
|
|
83
|
+
className: cnAll(selectItemVariants(), "text-text-secondary data-[focused]:text-foreground"),
|
|
84
|
+
children: [/* @__PURE__ */ jsx("svg", {
|
|
85
|
+
"aria-hidden": "true",
|
|
86
|
+
viewBox: "0 0 16 16",
|
|
87
|
+
fill: "none",
|
|
88
|
+
stroke: "currentColor",
|
|
89
|
+
strokeWidth: "1.75",
|
|
90
|
+
strokeLinecap: "round",
|
|
91
|
+
className: "size-3.5 flex-none",
|
|
92
|
+
children: /* @__PURE__ */ jsx("path", { d: "M8 3.5v9M3.5 8h9" })
|
|
93
|
+
}), label]
|
|
94
|
+
});
|
|
95
|
+
}
|
|
13
96
|
/** The picked options as chips, then "+N", reading Select's own state. */
|
|
14
97
|
function Chips({ placeholder, maxVisible, moreLabel, moreRef }) {
|
|
15
98
|
const state = useContext(SelectStateContext);
|
|
@@ -117,8 +200,29 @@ function Clear({ label, disabled }) {
|
|
|
117
200
|
})
|
|
118
201
|
});
|
|
119
202
|
}
|
|
120
|
-
const MultiSelect = forwardRef(function MultiSelect({ children, label, labelHidden, placeholder = "Select options", errorMessage, maxVisible = 2, moreLabel, overflowTooltip = false, clearLabel = "Clear selections", size = "md", className, ...props }, ref) {
|
|
203
|
+
const MultiSelect = forwardRef(function MultiSelect({ children, label, labelHidden, placeholder = "Select options", errorMessage, maxVisible = 2, moreLabel, overflowTooltip = false, clearLabel = "Clear selections", size = "md", className, searchable = false, searchLabel = "Search options", searchPlaceholder = "Search", emptyLabel = "No options found", onCreate, createLabel = defaultCreateLabel, ...props }, ref) {
|
|
121
204
|
const moreRef = useRef(null);
|
|
205
|
+
const creatable = onCreate != null;
|
|
206
|
+
const withSearch = searchable || creatable;
|
|
207
|
+
const [query, setQuery] = useState("");
|
|
208
|
+
const [ownValue, setOwnValue] = useState(() => props.defaultValue ?? []);
|
|
209
|
+
const value = props.value ?? ownValue;
|
|
210
|
+
const creatableProps = creatable ? {
|
|
211
|
+
value,
|
|
212
|
+
defaultValue: void 0,
|
|
213
|
+
onChange: (keys) => {
|
|
214
|
+
let next = keys;
|
|
215
|
+
if (keys.includes(CREATE_KEY)) {
|
|
216
|
+
const text = query.trim();
|
|
217
|
+
next = keys.filter((key) => key !== CREATE_KEY);
|
|
218
|
+
if (text !== "" && !next.includes(text)) next.push(text);
|
|
219
|
+
setQuery("");
|
|
220
|
+
onCreate(text);
|
|
221
|
+
}
|
|
222
|
+
setOwnValue(next);
|
|
223
|
+
props.onChange?.(next);
|
|
224
|
+
}
|
|
225
|
+
} : {};
|
|
122
226
|
const trigger = /* @__PURE__ */ jsxs(Button, {
|
|
123
227
|
ref,
|
|
124
228
|
"data-slot": "multi-select-trigger",
|
|
@@ -150,6 +254,11 @@ const MultiSelect = forwardRef(function MultiSelect({ children, label, labelHidd
|
|
|
150
254
|
});
|
|
151
255
|
return /* @__PURE__ */ jsxs(Select, {
|
|
152
256
|
...props,
|
|
257
|
+
...creatableProps,
|
|
258
|
+
onOpenChange: (isOpen) => {
|
|
259
|
+
if (!isOpen) setQuery("");
|
|
260
|
+
props.onOpenChange?.(isOpen);
|
|
261
|
+
},
|
|
153
262
|
selectionMode: "multiple",
|
|
154
263
|
"data-slot": "multi-select",
|
|
155
264
|
className: cn(selectVariants(), className),
|
|
@@ -178,8 +287,16 @@ const MultiSelect = forwardRef(function MultiSelect({ children, label, labelHidd
|
|
|
178
287
|
}),
|
|
179
288
|
/* @__PURE__ */ jsx(Popover, {
|
|
180
289
|
"data-slot": "multi-select-popover",
|
|
181
|
-
className: selectPopoverVariants(),
|
|
182
|
-
children: /* @__PURE__ */
|
|
290
|
+
className: withSearch ? cnAll(selectPopoverVariants({ reveal: "fade" }), "flex flex-col overflow-y-hidden") : selectPopoverVariants(),
|
|
291
|
+
children: withSearch ? /* @__PURE__ */ jsxs(SearchList, {
|
|
292
|
+
query,
|
|
293
|
+
onQueryChange: setQuery,
|
|
294
|
+
creatable,
|
|
295
|
+
searchLabel,
|
|
296
|
+
searchPlaceholder,
|
|
297
|
+
emptyLabel,
|
|
298
|
+
children: [children, creatable ? /* @__PURE__ */ jsx(CreateItem, { label: createLabel(query.trim()) }) : null]
|
|
299
|
+
}) : /* @__PURE__ */ jsx(ListBox, {
|
|
183
300
|
"data-slot": "multi-select-list",
|
|
184
301
|
className: "outline-none",
|
|
185
302
|
children
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multi-select.js","names":["TooltipTrigger","Tooltip","AriaButton","AriaSelectValue","AriaSelect","AriaLabel","AriaFieldError","AriaPopover","AriaListBox","AriaListBoxSection","AriaHeader"],"sources":["../../src/multi-select/multi-select.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useContext, useRef, useState, type ReactNode, type RefObject } from \"react\";\nimport {\n Button as AriaButton,\n ButtonContext,\n FieldError as AriaFieldError,\n Header as AriaHeader,\n Label as AriaLabel,\n ListBox as AriaListBox,\n ListBoxSection as AriaListBoxSection,\n Popover as AriaPopover,\n Select as AriaSelect,\n SelectStateContext,\n SelectValue as AriaSelectValue,\n type Key,\n type SelectProps as AriaSelectProps,\n} from \"react-aria-components\";\n\nimport { labelVariants } from \"../label/label.js\";\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { useFlip } from \"../lib/flip.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\nimport { selectPopoverVariants, selectTriggerVariants, selectVariants } from \"../select/select.js\";\nimport { Tooltip, TooltipTrigger } from \"../tooltip/tooltip.js\";\n\n/**\n * MultiSelect: pick several options from a list. A Select in multiple mode\n * (React Aria), so the keyboard, typeahead, focus and screen-reader behaviour\n * are Select's own; the list stays open while picking.\n *\n * The trigger shows the first picks as chips and the rest as \"+N\". A new chip\n * fades up from a blur and grows in on the snappy spring while the chips beside\n * it glide over; the count rolls up or down as it changes. A clear button scales\n * in beside the chevron when anything is picked. Options are `SelectItem`s, whose\n * tick pops in as an option is picked; `MultiSelectGroup` gathers them under a\n * heading.\n */\nexport interface MultiSelectProps extends Omit<\n AriaSelectProps<object, \"multiple\">,\n \"className\" | \"style\" | \"children\" | \"items\" | \"selectionMode\"\n> {\n /** `SelectItem` children. */\n children?: ReactNode;\n /** What is being chosen, and the control's accessible name. */\n label: ReactNode;\n labelHidden?: boolean;\n /** Shown while nothing is picked. @default \"Select options\" */\n placeholder?: string;\n /** Why the selection is wrong. Rendered under the control. */\n errorMessage?: ReactNode;\n /** Chips shown in the trigger before the rest become \"+N\". @default 2 */\n maxVisible?: number;\n /**\n * Words after the overflow count, so `\"more\"` reads \"+2 more\". Off by default:\n * the count alone, \"+2\".\n */\n moreLabel?: string;\n /**\n * While some picks are folded into \"+N\", hovering or focusing the trigger shows\n * a tooltip listing them. Off by default.\n *\n * @default false\n */\n overflowTooltip?: boolean;\n /** Accessible name of the clear button. @default \"Clear selections\" */\n clearLabel?: string;\n /** @default \"md\" */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n}\n\n/** The picked options as chips, then \"+N\", reading Select's own state. */\nfunction Chips({\n placeholder,\n maxVisible,\n moreLabel,\n moreRef,\n}: {\n placeholder: string;\n maxVisible: number;\n moreLabel: string | undefined;\n moreRef: RefObject<HTMLSpanElement | null>;\n}) {\n const state = useContext(SelectStateContext);\n const keys = (state?.value as Key[] | undefined) ?? [];\n const labelOf = (key: Key) => state?.collection.getItem(key)?.textValue ?? String(key);\n const shown = keys.slice(0, maxVisible);\n const rest = keys.length - shown.length;\n const row = useRef<HTMLSpanElement>(null);\n // The chips beside a new or removed one glide to their new place.\n useFlip(row, keys.join(\"|\"), \"var(--duration-smooth)\", \"var(--ease-smooth)\");\n // Chips there at first paint never animate; later ones grow in.\n const [initial] = useState(() => new Set(keys));\n const [count, setCount] = useState({ rest, dir: 1 });\n if (count.rest !== rest) setCount({ rest, dir: rest > count.rest ? 1 : -1 });\n return (\n <span\n ref={row}\n aria-hidden=\"true\"\n className=\"relative flex min-h-6.5 min-w-0 flex-1 items-center gap-1.5 overflow-hidden\"\n >\n {keys.length === 0 ? (\n <span\n data-slot=\"multi-select-placeholder\"\n className=\"truncate text-muted-foreground motion-safe:animate-fade-in\"\n >\n {placeholder}\n </span>\n ) : null}\n {shown.map((key) => (\n <span\n key={String(key)}\n data-flip=\"\"\n data-slot=\"multi-select-chip\"\n className={cnAll(\n \"multi-select-chip\",\n !initial.has(key) && \"motion-safe:animate-chip-reveal\",\n )}\n >\n {labelOf(key)}\n </span>\n ))}\n {rest > 0 ? (\n <span\n ref={moreRef}\n data-flip=\"\"\n data-slot=\"multi-select-more\"\n className=\"multi-select-chip text-text-secondary tabular-nums\"\n >\n +\n <SwapText swapKey={String(rest)} dir={count.dir} motion=\"rise\">\n {rest}\n </SwapText>\n {/* The chip is a flex box, which drops a leading space in a bare text\n node; whitespace-pre keeps \"+2 more\" from reading \"+2more\". */}\n {moreLabel ? (\n <span data-slot=\"multi-select-more-label\" className=\"whitespace-pre\">\n {` ${moreLabel}`}\n </span>\n ) : null}\n </span>\n ) : null}\n </span>\n );\n}\n\n/** The labels of the picks folded into \"+N\", in the order they were picked. */\nfunction useHiddenLabels(maxVisible: number) {\n const state = useContext(SelectStateContext);\n const keys = (state?.value as Key[] | undefined) ?? [];\n return keys\n .slice(maxVisible)\n .map((key) => state?.collection.getItem(key)?.textValue ?? String(key));\n}\n\n/**\n * Wraps the trigger in a tooltip naming the picks hidden behind \"+N\", placed\n * against the +N chip rather than the whole field. Always rendered, so turning\n * `overflowTooltip` on or off never swaps the trigger element (and drops its\n * focus); disabled while off or while nothing is hidden, so it never opens empty.\n */\nfunction OverflowTooltip({\n enabled,\n maxVisible,\n anchorRef,\n children,\n}: {\n enabled: boolean;\n maxVisible: number;\n anchorRef: RefObject<HTMLSpanElement | null>;\n children: ReactNode;\n}) {\n const hidden = useHiddenLabels(maxVisible);\n return (\n <TooltipTrigger isDisabled={!enabled || hidden.length === 0}>\n {children}\n <Tooltip triggerRef={anchorRef} data-slot=\"multi-select-overflow-tooltip\">\n {hidden.join(\", \")}\n </Tooltip>\n </TooltipTrigger>\n );\n}\n\n/** Clears every pick. It sits outside the trigger, so it is its own tab stop. */\nfunction Clear({ label, disabled }: { label: string; disabled: boolean }) {\n const state = useContext(SelectStateContext);\n const keys = (state?.value as Key[] | undefined) ?? [];\n if (!state || keys.length === 0 || disabled) return null;\n // Select hands its trigger props to every Button inside it; clear that, or this\n // button would become a second trigger named by the label.\n return (\n <ButtonContext.Provider value={null}>\n <AriaButton\n aria-label={label}\n data-slot=\"multi-select-clear\"\n onPress={() => {\n state.setValue([]);\n state.close();\n }}\n className=\"multi-select-clear motion-safe:animate-field-clear-in motion-reduce:animate-fade-in\"\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n className=\"size-3.5\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n </ButtonContext.Provider>\n );\n}\n\nexport const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(function MultiSelect(\n {\n children,\n label,\n labelHidden,\n placeholder = \"Select options\",\n errorMessage,\n maxVisible = 2,\n moreLabel,\n overflowTooltip = false,\n clearLabel = \"Clear selections\",\n size = \"md\",\n className,\n ...props\n },\n ref,\n) {\n const moreRef = useRef<HTMLSpanElement>(null);\n const trigger = (\n <AriaButton\n ref={ref}\n data-slot=\"multi-select-trigger\"\n className={cnAll(selectTriggerVariants({ size }), \"pe-16 multi-select-trigger\")}\n >\n <AriaSelectValue data-slot=\"multi-select-value\" className=\"sr-only\">\n {({ isPlaceholder, selectedText }) => (isPlaceholder ? placeholder : selectedText)}\n </AriaSelectValue>\n <Chips\n placeholder={placeholder}\n maxVisible={maxVisible}\n moreLabel={moreLabel}\n moreRef={moreRef}\n />\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"absolute end-(--density-control-pad-md) text-muted-foreground motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-aria-expanded/trigger:rotate-180\"\n >\n <path d=\"m4 6 4 4 4-4\" />\n </svg>\n </AriaButton>\n );\n return (\n <AriaSelect<object, \"multiple\">\n {...props}\n selectionMode=\"multiple\"\n data-slot=\"multi-select\"\n className={cn(selectVariants(), className)}\n >\n <AriaLabel\n data-slot=\"multi-select-label\"\n className={cnAll(labelVariants(), labelHidden && \"sr-only\")}\n >\n {label}\n </AriaLabel>\n <div className=\"relative\">\n <OverflowTooltip enabled={overflowTooltip} maxVisible={maxVisible} anchorRef={moreRef}>\n {trigger}\n </OverflowTooltip>\n <Clear label={clearLabel} disabled={props.isDisabled ?? false} />\n </div>\n {errorMessage == null ? null : (\n <AriaFieldError\n data-slot=\"multi-select-error\"\n className=\"text-sm leading-(--leading-normal) text-destructive-strong\"\n >\n {errorMessage}\n </AriaFieldError>\n )}\n <AriaPopover data-slot=\"multi-select-popover\" className={selectPopoverVariants()}>\n <AriaListBox data-slot=\"multi-select-list\" className=\"outline-none\">\n {children}\n </AriaListBox>\n </AriaPopover>\n </AriaSelect>\n );\n});\n\nexport const multiSelectGroupVariants = cva([\"flex flex-col not-first:mt-1\"]);\n\nexport const multiSelectGroupHeaderVariants = cva([\n \"px-2 pt-1.5 pb-1 text-xs leading-(--leading-normal) font-semibold text-muted-foreground\",\n]);\n\n/**\n * A run of options under a heading, such as \"Countries\" and \"Regions\". React\n * Aria's ListBoxSection, so the heading names the group for assistive tech and\n * the arrow keys move straight through from one group to the next.\n *\n * @example\n * <MultiSelect label=\"Markets\">\n * <MultiSelectGroup title=\"Countries\">\n * <SelectItem id=\"IN\">India</SelectItem>\n * </MultiSelectGroup>\n * <MultiSelectGroup title=\"Regions\">\n * <SelectItem id=\"europe\">Europe</SelectItem>\n * </MultiSelectGroup>\n * </MultiSelect>\n */\nexport interface MultiSelectGroupProps {\n /** The heading drawn above the group, and the group's accessible name. */\n title: ReactNode;\n /** `SelectItem` children. */\n children?: ReactNode;\n /** Identifies the group in the collection. Defaults to a generated id. */\n id?: Key;\n /** Extra classes for the group. Merged last. */\n className?: string;\n}\n\nexport const MultiSelectGroup = forwardRef<HTMLElement, MultiSelectGroupProps>(\n function MultiSelectGroup({ title, children, id, className }, ref) {\n return (\n <AriaListBoxSection\n ref={ref}\n id={id}\n data-slot=\"multi-select-group\"\n className={cn(multiSelectGroupVariants(), className)}\n >\n <AriaHeader\n data-slot=\"multi-select-group-header\"\n className={multiSelectGroupHeaderVariants()}\n >\n {title}\n </AriaHeader>\n {children}\n </AriaListBoxSection>\n );\n },\n);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"multi-select.js","names":["AriaAutocomplete","AriaSearchField","AriaInput","AriaListBox","AriaListBoxItem","TooltipTrigger","Tooltip","AriaButton","AriaSelectValue","AriaSelect","AriaLabel","AriaFieldError","AriaPopover","AriaListBoxSection","AriaHeader"],"sources":["../../src/multi-select/multi-select.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport {\n forwardRef,\n useContext,\n useRef,\n useState,\n type ContextType,\n type ReactNode,\n type RefObject,\n} from \"react\";\nimport {\n Autocomplete as AriaAutocomplete,\n Button as AriaButton,\n ButtonContext,\n FieldError as AriaFieldError,\n Header as AriaHeader,\n Input as AriaInput,\n Label as AriaLabel,\n ListBox as AriaListBox,\n ListBoxItem as AriaListBoxItem,\n ListBoxSection as AriaListBoxSection,\n Popover as AriaPopover,\n SearchField as AriaSearchField,\n Select as AriaSelect,\n SelectStateContext,\n SelectValue as AriaSelectValue,\n type Key,\n type SelectProps as AriaSelectProps,\n useFilter,\n} from \"react-aria-components\";\n\nimport { labelVariants } from \"../label/label.js\";\nimport { cn, cnAll } from \"../lib/cn.js\";\nimport { useFlip } from \"../lib/flip.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\nimport {\n selectItemVariants,\n selectPopoverVariants,\n selectTriggerVariants,\n selectVariants,\n} from \"../select/select.js\";\nimport { Tooltip, TooltipTrigger } from \"../tooltip/tooltip.js\";\n\n/**\n * MultiSelect: pick several options from a list. A Select in multiple mode\n * (React Aria), so the keyboard, typeahead, focus and screen-reader behaviour\n * are Select's own; the list stays open while picking.\n *\n * The trigger shows the first picks as chips and the rest as \"+N\". A new chip\n * fades up from a blur and grows in on the snappy spring while the chips beside\n * it glide over; the count rolls up or down as it changes. A clear button scales\n * in beside the chevron when anything is picked. Options are `SelectItem`s, whose\n * tick pops in as an option is picked; `MultiSelectGroup` gathers them under a\n * heading.\n *\n * Opt in to `searchable` for a search field at the top of the list that filters\n * the options as you type (React Aria's Autocomplete, so the arrow keys and Enter\n * work from the field). Pass `onCreate` to also offer a \"Create “…”\" row when the\n * typed text matches no option.\n */\nexport interface MultiSelectProps extends Omit<\n AriaSelectProps<object, \"multiple\">,\n \"className\" | \"style\" | \"children\" | \"items\" | \"selectionMode\"\n> {\n /** `SelectItem` children. */\n children?: ReactNode;\n /** What is being chosen, and the control's accessible name. */\n label: ReactNode;\n labelHidden?: boolean;\n /** Shown while nothing is picked. @default \"Select options\" */\n placeholder?: string;\n /** Why the selection is wrong. Rendered under the control. */\n errorMessage?: ReactNode;\n /** Chips shown in the trigger before the rest become \"+N\". @default 2 */\n maxVisible?: number;\n /**\n * Words after the overflow count, so `\"more\"` reads \"+2 more\". Off by default:\n * the count alone, \"+2\".\n */\n moreLabel?: string;\n /**\n * While some picks are folded into \"+N\", hovering or focusing the trigger shows\n * a tooltip listing them. Off by default.\n *\n * @default false\n */\n overflowTooltip?: boolean;\n /** Accessible name of the clear button. @default \"Clear selections\" */\n clearLabel?: string;\n /** @default \"md\" */\n size?: \"sm\" | \"md\" | \"lg\";\n /** Extra classes for the wrapper. Merged last. */\n className?: string;\n /**\n * Puts a search field at the top of the list. Typing filters the options (and\n * groups) by their text, ignoring case and accents; the arrow keys and Enter\n * work from the field. Cleared each time the list closes. Off by default.\n * Turned on by `onCreate` too.\n *\n * @default false\n */\n searchable?: boolean;\n /** Accessible name of the search field. @default \"Search options\" */\n searchLabel?: string;\n /** Placeholder of the search field. @default \"Search\" */\n searchPlaceholder?: string;\n /** Shown in the list while the search matches nothing. @default \"No options found\" */\n emptyLabel?: string;\n /**\n * Makes the list creatable, and searchable. While the typed text (trimmed)\n * matches no option's text or id nor a picked value, ignoring case, the list ends\n * with a \"Create “text”\" row. Picking it calls `onCreate` with the text, adds\n * the text itself as a picked key (through `onChange`, as any pick) and clears\n * the search. Render a `SelectItem` whose `id` is that text so it lists like\n * the others; until then its chip shows the text.\n */\n onCreate?: (value: string) => void;\n /** The create row's text for the typed value. @default (value) => `Create “${value}”` */\n createLabel?: (value: string) => string;\n}\n\n/** The key of the create row. Never reaches `onChange`. */\nconst CREATE_KEY = \"__multi-select-create__\";\n\nconst defaultCreateLabel = (value: string) => `Create \\u201c${value}\\u201d`;\n\ntype SelectState = NonNullable<ContextType<typeof SelectStateContext>>;\n\n/** Whether `text` already names an option (by its text or its key) or a pick, ignoring case. */\nfunction isTaken(state: SelectState | null, text: string) {\n const want = text.toLocaleLowerCase();\n const keys = (state?.value as Key[] | undefined) ?? [];\n if (keys.some((key) => String(key).toLocaleLowerCase() === want)) return true;\n if (!state) return false;\n for (const key of state.collection.getKeys()) {\n const node = state.collection.getItem(key);\n if (\n node?.type === \"item\" &&\n node.key !== CREATE_KEY &&\n // The key too: creating text equal to an unpicked option's id would pick\n // that option AND tell the owner to create a duplicate.\n (node.textValue.toLocaleLowerCase() === want || String(node.key).toLocaleLowerCase() === want)\n ) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * The list with a search field over it (React Aria's Autocomplete): the field\n * keeps focus and drives the options with the arrow keys, Enter picks.\n */\nfunction SearchList({\n query,\n onQueryChange,\n creatable,\n searchLabel,\n searchPlaceholder,\n emptyLabel,\n children,\n}: {\n query: string;\n onQueryChange: (query: string) => void;\n creatable: boolean;\n searchLabel: string;\n searchPlaceholder: string;\n emptyLabel: string;\n children: ReactNode;\n}) {\n const state = useContext(SelectStateContext);\n const { contains } = useFilter({ sensitivity: \"base\" });\n const text = query.trim();\n const offerCreate = creatable && text !== \"\" && !isTaken(state, text);\n return (\n <AriaAutocomplete\n inputValue={query}\n onInputChange={onQueryChange}\n filter={(textValue, input, node) =>\n node.key === CREATE_KEY ? offerCreate : contains(textValue, input)\n }\n >\n <AriaSearchField\n aria-label={searchLabel}\n autoFocus\n data-slot=\"multi-select-search\"\n className={multiSelectSearchClass}\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n className=\"size-4 flex-none text-muted-foreground\"\n >\n <circle cx=\"7\" cy=\"7\" r=\"4.5\" />\n <path d=\"m10.5 10.5 3 3\" />\n </svg>\n <AriaInput\n placeholder={searchPlaceholder}\n data-slot=\"multi-select-search-input\"\n className=\"min-w-0 flex-1 appearance-none border-0 bg-transparent p-0 font-sans text-sm text-foreground outline-none placeholder:text-muted-foreground [&::-webkit-search-cancel-button]:hidden\"\n />\n </AriaSearchField>\n <AriaListBox\n data-slot=\"multi-select-list\"\n renderEmptyState={() => (\n <p\n data-slot=\"multi-select-empty\"\n className=\"m-0 px-2 py-1.5 text-sm text-muted-foreground\"\n >\n {emptyLabel}\n </p>\n )}\n className=\"min-h-0 flex-1 overflow-y-auto outline-none\"\n >\n {children}\n </AriaListBox>\n </AriaAutocomplete>\n );\n}\n\n/** The search row: a quiet field over the list, underlined while focused. */\nconst multiSelectSearchClass =\n \"relative mb-1 flex h-(--density-control-h-sm) flex-none items-center gap-2 border-b-(length:--border-width) border-border px-2 after:pointer-events-none after:absolute after:inset-x-0 after:bottom-0 after:h-(--focus-ring-width) after:bg-transparent has-[input:focus]:after:bg-(--color-control-edge)\";\n\n/** The row that creates the typed value. Kept in the collection; the filter shows it. */\nfunction CreateItem({ label }: { label: string }) {\n return (\n <AriaListBoxItem\n id={CREATE_KEY}\n textValue={label}\n data-slot=\"multi-select-create\"\n className={cnAll(selectItemVariants(), \"text-text-secondary data-[focused]:text-foreground\")}\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n className=\"size-3.5 flex-none\"\n >\n <path d=\"M8 3.5v9M3.5 8h9\" />\n </svg>\n {label}\n </AriaListBoxItem>\n );\n}\n\n/** The picked options as chips, then \"+N\", reading Select's own state. */\nfunction Chips({\n placeholder,\n maxVisible,\n moreLabel,\n moreRef,\n}: {\n placeholder: string;\n maxVisible: number;\n moreLabel: string | undefined;\n moreRef: RefObject<HTMLSpanElement | null>;\n}) {\n const state = useContext(SelectStateContext);\n const keys = (state?.value as Key[] | undefined) ?? [];\n const labelOf = (key: Key) => state?.collection.getItem(key)?.textValue ?? String(key);\n const shown = keys.slice(0, maxVisible);\n const rest = keys.length - shown.length;\n const row = useRef<HTMLSpanElement>(null);\n // The chips beside a new or removed one glide to their new place.\n useFlip(row, keys.join(\"|\"), \"var(--duration-smooth)\", \"var(--ease-smooth)\");\n // Chips there at first paint never animate; later ones grow in.\n const [initial] = useState(() => new Set(keys));\n const [count, setCount] = useState({ rest, dir: 1 });\n if (count.rest !== rest) setCount({ rest, dir: rest > count.rest ? 1 : -1 });\n return (\n <span\n ref={row}\n aria-hidden=\"true\"\n className=\"relative flex min-h-6.5 min-w-0 flex-1 items-center gap-1.5 overflow-hidden\"\n >\n {keys.length === 0 ? (\n <span\n data-slot=\"multi-select-placeholder\"\n className=\"truncate text-muted-foreground motion-safe:animate-fade-in\"\n >\n {placeholder}\n </span>\n ) : null}\n {shown.map((key) => (\n <span\n key={String(key)}\n data-flip=\"\"\n data-slot=\"multi-select-chip\"\n className={cnAll(\n \"multi-select-chip\",\n !initial.has(key) && \"motion-safe:animate-chip-reveal\",\n )}\n >\n {labelOf(key)}\n </span>\n ))}\n {rest > 0 ? (\n <span\n ref={moreRef}\n data-flip=\"\"\n data-slot=\"multi-select-more\"\n className=\"multi-select-chip text-text-secondary tabular-nums\"\n >\n +\n <SwapText swapKey={String(rest)} dir={count.dir} motion=\"rise\">\n {rest}\n </SwapText>\n {/* The chip is a flex box, which drops a leading space in a bare text\n node; whitespace-pre keeps \"+2 more\" from reading \"+2more\". */}\n {moreLabel ? (\n <span data-slot=\"multi-select-more-label\" className=\"whitespace-pre\">\n {` ${moreLabel}`}\n </span>\n ) : null}\n </span>\n ) : null}\n </span>\n );\n}\n\n/** The labels of the picks folded into \"+N\", in the order they were picked. */\nfunction useHiddenLabels(maxVisible: number) {\n const state = useContext(SelectStateContext);\n const keys = (state?.value as Key[] | undefined) ?? [];\n return keys\n .slice(maxVisible)\n .map((key) => state?.collection.getItem(key)?.textValue ?? String(key));\n}\n\n/**\n * Wraps the trigger in a tooltip naming the picks hidden behind \"+N\", placed\n * against the +N chip rather than the whole field. Always rendered, so turning\n * `overflowTooltip` on or off never swaps the trigger element (and drops its\n * focus); disabled while off or while nothing is hidden, so it never opens empty.\n */\nfunction OverflowTooltip({\n enabled,\n maxVisible,\n anchorRef,\n children,\n}: {\n enabled: boolean;\n maxVisible: number;\n anchorRef: RefObject<HTMLSpanElement | null>;\n children: ReactNode;\n}) {\n const hidden = useHiddenLabels(maxVisible);\n return (\n <TooltipTrigger isDisabled={!enabled || hidden.length === 0}>\n {children}\n <Tooltip triggerRef={anchorRef} data-slot=\"multi-select-overflow-tooltip\">\n {hidden.join(\", \")}\n </Tooltip>\n </TooltipTrigger>\n );\n}\n\n/** Clears every pick. It sits outside the trigger, so it is its own tab stop. */\nfunction Clear({ label, disabled }: { label: string; disabled: boolean }) {\n const state = useContext(SelectStateContext);\n const keys = (state?.value as Key[] | undefined) ?? [];\n if (!state || keys.length === 0 || disabled) return null;\n // Select hands its trigger props to every Button inside it; clear that, or this\n // button would become a second trigger named by the label.\n return (\n <ButtonContext.Provider value={null}>\n <AriaButton\n aria-label={label}\n data-slot=\"multi-select-clear\"\n onPress={() => {\n state.setValue([]);\n state.close();\n }}\n className=\"multi-select-clear motion-safe:animate-field-clear-in motion-reduce:animate-fade-in\"\n >\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n className=\"size-3.5\"\n >\n <path d=\"m4 4 8 8M12 4l-8 8\" />\n </svg>\n </AriaButton>\n </ButtonContext.Provider>\n );\n}\n\nexport const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(function MultiSelect(\n {\n children,\n label,\n labelHidden,\n placeholder = \"Select options\",\n errorMessage,\n maxVisible = 2,\n moreLabel,\n overflowTooltip = false,\n clearLabel = \"Clear selections\",\n size = \"md\",\n className,\n searchable = false,\n searchLabel = \"Search options\",\n searchPlaceholder = \"Search\",\n emptyLabel = \"No options found\",\n onCreate,\n createLabel = defaultCreateLabel,\n ...props\n },\n ref,\n) {\n const moreRef = useRef<HTMLSpanElement>(null);\n const creatable = onCreate != null;\n const withSearch = searchable || creatable;\n const [query, setQuery] = useState(\"\");\n // Creating adds a key no option has yet, so the value is held here: the create\n // row's own key is swapped for the typed text before anyone sees it.\n const [ownValue, setOwnValue] = useState<readonly Key[]>(() => props.defaultValue ?? []);\n const value = props.value ?? ownValue;\n const creatableProps: Partial<AriaSelectProps<object, \"multiple\">> = creatable\n ? {\n value,\n defaultValue: undefined,\n onChange: (keys) => {\n let next = keys;\n if (keys.includes(CREATE_KEY)) {\n const text = query.trim();\n next = keys.filter((key) => key !== CREATE_KEY);\n if (text !== \"\" && !next.includes(text)) next.push(text);\n setQuery(\"\");\n onCreate(text);\n }\n setOwnValue(next);\n props.onChange?.(next);\n },\n }\n : {};\n const trigger = (\n <AriaButton\n ref={ref}\n data-slot=\"multi-select-trigger\"\n className={cnAll(selectTriggerVariants({ size }), \"pe-16 multi-select-trigger\")}\n >\n <AriaSelectValue data-slot=\"multi-select-value\" className=\"sr-only\">\n {({ isPlaceholder, selectedText }) => (isPlaceholder ? placeholder : selectedText)}\n </AriaSelectValue>\n <Chips\n placeholder={placeholder}\n maxVisible={maxVisible}\n moreLabel={moreLabel}\n moreRef={moreRef}\n />\n <svg\n aria-hidden=\"true\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"1.75\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"absolute end-(--density-control-pad-md) text-muted-foreground motion-safe:transition-[rotate] motion-safe:duration-(--duration-gentle) motion-safe:ease-gentle group-aria-expanded/trigger:rotate-180\"\n >\n <path d=\"m4 6 4 4 4-4\" />\n </svg>\n </AriaButton>\n );\n return (\n <AriaSelect<object, \"multiple\">\n {...props}\n {...creatableProps}\n onOpenChange={(isOpen) => {\n // Each opening starts from the whole list.\n if (!isOpen) setQuery(\"\");\n props.onOpenChange?.(isOpen);\n }}\n selectionMode=\"multiple\"\n data-slot=\"multi-select\"\n className={cn(selectVariants(), className)}\n >\n <AriaLabel\n data-slot=\"multi-select-label\"\n className={cnAll(labelVariants(), labelHidden && \"sr-only\")}\n >\n {label}\n </AriaLabel>\n <div className=\"relative\">\n <OverflowTooltip enabled={overflowTooltip} maxVisible={maxVisible} anchorRef={moreRef}>\n {trigger}\n </OverflowTooltip>\n <Clear label={clearLabel} disabled={props.isDisabled ?? false} />\n </div>\n {errorMessage == null ? null : (\n <AriaFieldError\n data-slot=\"multi-select-error\"\n className=\"text-sm leading-(--leading-normal) text-destructive-strong\"\n >\n {errorMessage}\n </AriaFieldError>\n )}\n <AriaPopover\n data-slot=\"multi-select-popover\"\n className={\n withSearch\n ? cnAll(selectPopoverVariants({ reveal: \"fade\" }), \"flex flex-col overflow-y-hidden\")\n : selectPopoverVariants()\n }\n >\n {withSearch ? (\n <SearchList\n query={query}\n onQueryChange={setQuery}\n creatable={creatable}\n searchLabel={searchLabel}\n searchPlaceholder={searchPlaceholder}\n emptyLabel={emptyLabel}\n >\n {children}\n {creatable ? <CreateItem label={createLabel(query.trim())} /> : null}\n </SearchList>\n ) : (\n <AriaListBox data-slot=\"multi-select-list\" className=\"outline-none\">\n {children}\n </AriaListBox>\n )}\n </AriaPopover>\n </AriaSelect>\n );\n});\n\nexport const multiSelectGroupVariants = cva([\"flex flex-col not-first:mt-1\"]);\n\nexport const multiSelectGroupHeaderVariants = cva([\n \"px-2 pt-1.5 pb-1 text-xs leading-(--leading-normal) font-semibold text-muted-foreground\",\n]);\n\n/**\n * A run of options under a heading, such as \"Countries\" and \"Regions\". React\n * Aria's ListBoxSection, so the heading names the group for assistive tech and\n * the arrow keys move straight through from one group to the next.\n *\n * @example\n * <MultiSelect label=\"Markets\">\n * <MultiSelectGroup title=\"Countries\">\n * <SelectItem id=\"IN\">India</SelectItem>\n * </MultiSelectGroup>\n * <MultiSelectGroup title=\"Regions\">\n * <SelectItem id=\"europe\">Europe</SelectItem>\n * </MultiSelectGroup>\n * </MultiSelect>\n */\nexport interface MultiSelectGroupProps {\n /** The heading drawn above the group, and the group's accessible name. */\n title: ReactNode;\n /** `SelectItem` children. */\n children?: ReactNode;\n /** Identifies the group in the collection. Defaults to a generated id. */\n id?: Key;\n /** Extra classes for the group. Merged last. */\n className?: string;\n}\n\nexport const MultiSelectGroup = forwardRef<HTMLElement, MultiSelectGroupProps>(\n function MultiSelectGroup({ title, children, id, className }, ref) {\n return (\n <AriaListBoxSection\n ref={ref}\n id={id}\n data-slot=\"multi-select-group\"\n className={cn(multiSelectGroupVariants(), className)}\n >\n <AriaHeader\n data-slot=\"multi-select-group-header\"\n className={multiSelectGroupHeaderVariants()}\n >\n {title}\n </AriaHeader>\n {children}\n </AriaListBoxSection>\n );\n },\n);\n"],"mappings":"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"}
|
package/dist/progress/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Progress, ProgressProps, progressFillVariants, progressTrackVariants, progressVariants } from "./progress.js";
|
|
2
|
-
export { Progress, type ProgressProps, progressFillVariants, progressTrackVariants, progressVariants };
|
|
1
|
+
import { Progress, ProgressProps, progressFillVariants, progressTargetVariants, progressTrackVariants, progressVariants } from "./progress.js";
|
|
2
|
+
export { Progress, type ProgressProps, progressFillVariants, progressTargetVariants, progressTrackVariants, progressVariants };
|
package/dist/progress/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { Progress, progressFillVariants, progressTrackVariants, progressVariants } from "./progress.js";
|
|
2
|
-
export { Progress, progressFillVariants, progressTrackVariants, progressVariants };
|
|
1
|
+
import { Progress, progressFillVariants, progressTargetVariants, progressTrackVariants, progressVariants } from "./progress.js";
|
|
2
|
+
export { Progress, progressFillVariants, progressTargetVariants, progressTrackVariants, progressVariants };
|
|
@@ -31,13 +31,35 @@ import { ProgressBarProps } from "react-aria-components";
|
|
|
31
31
|
* the track against the page at 5.25:1.
|
|
32
32
|
*
|
|
33
33
|
* Both are in the build-failing contrast contract.
|
|
34
|
+
*
|
|
35
|
+
* A TARGET MARKER is opt-in (`target`): a short upright tick that stands
|
|
36
|
+
* proud of the track at the value the bar should have reached -- "where the
|
|
37
|
+
* goal should be by now". The tick is decorative; its meaning is read to
|
|
38
|
+
* assistive technology as the bar's description (`targetLabel` plus the
|
|
39
|
+
* formatted value), and `showTargetLabel` also prints it under the tick.
|
|
40
|
+
*
|
|
41
|
+
* `surface="inverse"` is for a bar on the inverted (foreground-coloured)
|
|
42
|
+
* surface, such as a dark summary strip: the track is the inverse text colour
|
|
43
|
+
* mixed into the surface, the default fill and the copy take the inverse text
|
|
44
|
+
* colour, so the bar keeps its contrast where the default greys would vanish.
|
|
34
45
|
*/
|
|
35
46
|
export declare const progressVariants: (props?: ({
|
|
36
47
|
headerEmpty?: boolean | null | undefined;
|
|
37
48
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
38
|
-
export declare const progressTrackVariants: (props?:
|
|
49
|
+
export declare const progressTrackVariants: (props?: ({
|
|
50
|
+
surface?: "inverse" | "default" | null | undefined;
|
|
51
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
52
|
+
/**
|
|
53
|
+
* The target tick: 2px wide, standing 4px proud of the track on both sides,
|
|
54
|
+
* with a 1px halo of the surface colour so it reads over the fill as well as
|
|
55
|
+
* over the track. Placed by an inline `inset-inline-start`, so it mirrors in
|
|
56
|
+
* right-to-left text.
|
|
57
|
+
*/
|
|
58
|
+
export declare const progressTargetVariants: (props?: ({
|
|
59
|
+
surface?: "inverse" | "default" | null | undefined;
|
|
60
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
39
61
|
export declare const progressFillVariants: (props?: ({
|
|
40
|
-
tone?: "
|
|
62
|
+
tone?: "neutral" | "warning" | "success" | "info" | "danger" | null | undefined;
|
|
41
63
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
42
64
|
/**
|
|
43
65
|
* A determinate or indeterminate progress bar.
|
|
@@ -84,6 +106,35 @@ export interface ProgressProps extends Omit<ProgressBarProps, "className" | "sty
|
|
|
84
106
|
* @default true
|
|
85
107
|
*/
|
|
86
108
|
showCompletion?: boolean;
|
|
109
|
+
/**
|
|
110
|
+
* Where the bar should be by now, on the same scale as `value`
|
|
111
|
+
* (`minValue`..`maxValue`, clamped). Draws an upright tick across the track
|
|
112
|
+
* at that point. The tick is decorative; `targetLabel` and the formatted
|
|
113
|
+
* target are read as the bar's description. Leave it out (the default) for
|
|
114
|
+
* no marker and the bar exactly as before. Ignored while indeterminate.
|
|
115
|
+
*/
|
|
116
|
+
target?: number;
|
|
117
|
+
/**
|
|
118
|
+
* What the target means, such as "Expected by now". Read with the formatted
|
|
119
|
+
* target ("Expected by now: 40%") as the bar's description, and printed
|
|
120
|
+
* under the tick with `showTargetLabel`.
|
|
121
|
+
* @default "Target"
|
|
122
|
+
*/
|
|
123
|
+
targetLabel?: string;
|
|
124
|
+
/**
|
|
125
|
+
* Print `targetLabel` and the target under the tick. It follows the tick and
|
|
126
|
+
* stays inside the bar near either end.
|
|
127
|
+
* @default false
|
|
128
|
+
*/
|
|
129
|
+
showTargetLabel?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* The surface the bar sits on. `inverse` is the inverted, foreground-coloured
|
|
132
|
+
* surface (a dark strip in the light theme, a light one in the dark theme):
|
|
133
|
+
* the track, the default-toned fill, the target tick and the copy take
|
|
134
|
+
* colours that hold their contrast there. Explicit tones keep their colours.
|
|
135
|
+
* @default "default"
|
|
136
|
+
*/
|
|
137
|
+
surface?: "default" | "inverse";
|
|
87
138
|
/** Extra classes for the track. */
|
|
88
139
|
trackClassName?: string;
|
|
89
140
|
/** Extra classes for the fill -- a per-call-site colour the tone axis cannot express. */
|
|
@@ -3,8 +3,8 @@ import { cn, cnAll } from "../lib/cn.js";
|
|
|
3
3
|
import { useAfter, useCountTo } from "../lib/count-to.js";
|
|
4
4
|
import { SwapText } from "../lib/swap-text.js";
|
|
5
5
|
import { cva } from "class-variance-authority";
|
|
6
|
-
import { forwardRef } from "react";
|
|
7
|
-
import { Label, ProgressBar } from "react-aria-components";
|
|
6
|
+
import { forwardRef, useId, useMemo } from "react";
|
|
7
|
+
import { Label, ProgressBar, useLocale } from "react-aria-components";
|
|
8
8
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/progress/progress.tsx
|
|
10
10
|
/**
|
|
@@ -36,6 +36,17 @@ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
|
36
36
|
* the track against the page at 5.25:1.
|
|
37
37
|
*
|
|
38
38
|
* Both are in the build-failing contrast contract.
|
|
39
|
+
*
|
|
40
|
+
* A TARGET MARKER is opt-in (`target`): a short upright tick that stands
|
|
41
|
+
* proud of the track at the value the bar should have reached -- "where the
|
|
42
|
+
* goal should be by now". The tick is decorative; its meaning is read to
|
|
43
|
+
* assistive technology as the bar's description (`targetLabel` plus the
|
|
44
|
+
* formatted value), and `showTargetLabel` also prints it under the tick.
|
|
45
|
+
*
|
|
46
|
+
* `surface="inverse"` is for a bar on the inverted (foreground-coloured)
|
|
47
|
+
* surface, such as a dark summary strip: the track is the inverse text colour
|
|
48
|
+
* mixed into the surface, the default fill and the copy take the inverse text
|
|
49
|
+
* colour, so the bar keeps its contrast where the default greys would vanish.
|
|
39
50
|
*/
|
|
40
51
|
const progressVariants = cva(["flex w-full flex-col box-border font-sans"], {
|
|
41
52
|
variants: { headerEmpty: {
|
|
@@ -44,7 +55,28 @@ const progressVariants = cva(["flex w-full flex-col box-border font-sans"], {
|
|
|
44
55
|
} },
|
|
45
56
|
defaultVariants: { headerEmpty: false }
|
|
46
57
|
});
|
|
47
|
-
const progressTrackVariants = cva(["relative h-2 w-full overflow-hidden rounded-pill box-border", "
|
|
58
|
+
const progressTrackVariants = cva(["relative h-2 w-full overflow-hidden rounded-pill box-border", "border-(length:--border-width)"], {
|
|
59
|
+
variants: {
|
|
60
|
+
/** The surface the bar sits on. */
|
|
61
|
+
surface: {
|
|
62
|
+
default: "bg-muted border-border",
|
|
63
|
+
inverse: "bg-[color-mix(in_oklab,var(--color-text-inverse)_28%,var(--color-foreground))] border-transparent"
|
|
64
|
+
} },
|
|
65
|
+
defaultVariants: { surface: "default" }
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* The target tick: 2px wide, standing 4px proud of the track on both sides,
|
|
69
|
+
* with a 1px halo of the surface colour so it reads over the fill as well as
|
|
70
|
+
* over the track. Placed by an inline `inset-inline-start`, so it mirrors in
|
|
71
|
+
* right-to-left text.
|
|
72
|
+
*/
|
|
73
|
+
const progressTargetVariants = cva("pointer-events-none absolute -top-1 -bottom-1 w-0.5 -ms-px rounded-pill", {
|
|
74
|
+
variants: { surface: {
|
|
75
|
+
default: "bg-foreground shadow-[0_0_0_1px_var(--color-surface)]",
|
|
76
|
+
inverse: "bg-text-inverse shadow-[0_0_0_1px_var(--color-foreground)]"
|
|
77
|
+
} },
|
|
78
|
+
defaultVariants: { surface: "default" }
|
|
79
|
+
});
|
|
48
80
|
const progressFillVariants = cva(["h-full rounded-pill", "progress-fill"], {
|
|
49
81
|
variants: { tone: {
|
|
50
82
|
neutral: "bg-primary-strong",
|
|
@@ -86,47 +118,125 @@ function ProgressValue({ percentage, valueText, showCompletion }) {
|
|
|
86
118
|
})
|
|
87
119
|
}) : null] });
|
|
88
120
|
}
|
|
89
|
-
|
|
121
|
+
/** The track's frame and the tick. With no marker it renders the track alone, as before. */
|
|
122
|
+
function TargetFrame({ marker, surface, children }) {
|
|
123
|
+
if (!marker) return /* @__PURE__ */ jsx(Fragment$1, { children });
|
|
124
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
125
|
+
"data-slot": "progress-target-frame",
|
|
126
|
+
className: "relative",
|
|
127
|
+
children: [children, /* @__PURE__ */ jsx("span", {
|
|
128
|
+
"data-slot": "progress-target",
|
|
129
|
+
"aria-hidden": "true",
|
|
130
|
+
className: progressTargetVariants({ surface }),
|
|
131
|
+
style: { insetInlineStart: `${marker.share * 100}%` }
|
|
132
|
+
})]
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The target's meaning, the bar's description. Printed, it hangs from a
|
|
137
|
+
* zero-width anchor at the tick: centred on it, or running inward near either
|
|
138
|
+
* end, in either text direction. Otherwise it is for screen readers only.
|
|
139
|
+
*/
|
|
140
|
+
function TargetText({ id, marker, shown, inverse }) {
|
|
141
|
+
if (!shown) return /* @__PURE__ */ jsx("span", {
|
|
142
|
+
id,
|
|
143
|
+
className: "sr-only",
|
|
144
|
+
children: marker.text
|
|
145
|
+
});
|
|
146
|
+
return /* @__PURE__ */ jsx("div", {
|
|
147
|
+
"data-slot": "progress-target-label",
|
|
148
|
+
"aria-hidden": "true",
|
|
149
|
+
className: "relative h-[1lh] text-xs leading-(--leading-normal)",
|
|
150
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
151
|
+
className: cnAll("absolute top-0 flex w-0 whitespace-nowrap", marker.share < .2 ? "justify-start" : marker.share > .8 ? "justify-end" : "justify-center", inverse ? "text-text-inverse" : "text-muted-foreground"),
|
|
152
|
+
style: { insetInlineStart: `${marker.share * 100}%` },
|
|
153
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
154
|
+
id,
|
|
155
|
+
children: marker.text
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
const Progress = forwardRef(function Progress({ label, labelHidden, showValue, showCompletion = true, tone, target, targetLabel = "Target", showTargetLabel = false, surface = "default", trackClassName, fillClassName, className, ...props }, ref) {
|
|
161
|
+
const { locale } = useLocale();
|
|
162
|
+
const targetId = useId();
|
|
163
|
+
const { minValue = 0, maxValue = 100, formatOptions, isIndeterminate: indeterminate } = props;
|
|
164
|
+
const marker = useMemo(() => {
|
|
165
|
+
if (target === void 0 || !Number.isFinite(target) || indeterminate) return null;
|
|
166
|
+
const span = maxValue - minValue;
|
|
167
|
+
const share = span > 0 ? Math.min(1, Math.max(0, (target - minValue) / span)) : 0;
|
|
168
|
+
const options = formatOptions ?? { style: "percent" };
|
|
169
|
+
const shown = options.style === "percent" ? share : Math.min(maxValue, Math.max(minValue, target));
|
|
170
|
+
return {
|
|
171
|
+
share,
|
|
172
|
+
text: `${targetLabel}: ${new Intl.NumberFormat(locale, options).format(shown)}`
|
|
173
|
+
};
|
|
174
|
+
}, [
|
|
175
|
+
target,
|
|
176
|
+
indeterminate,
|
|
177
|
+
minValue,
|
|
178
|
+
maxValue,
|
|
179
|
+
formatOptions,
|
|
180
|
+
locale,
|
|
181
|
+
targetLabel
|
|
182
|
+
]);
|
|
183
|
+
const inverse = surface === "inverse";
|
|
184
|
+
const describedBy = marker ? [props["aria-describedby"], targetId].filter(Boolean).join(" ") : props["aria-describedby"];
|
|
90
185
|
return /* @__PURE__ */ jsx(ProgressBar, {
|
|
91
186
|
ref,
|
|
92
187
|
...props,
|
|
188
|
+
"aria-describedby": describedBy,
|
|
93
189
|
"data-slot": "progress",
|
|
190
|
+
"data-surface": inverse ? "inverse" : void 0,
|
|
94
191
|
className: cn(progressVariants({ headerEmpty: Boolean(labelHidden && !showValue) }), className),
|
|
95
|
-
children: ({ percentage, isIndeterminate, valueText }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
192
|
+
children: ({ percentage, isIndeterminate, valueText }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
193
|
+
/* @__PURE__ */ jsxs("div", {
|
|
194
|
+
"data-slot": "progress-header",
|
|
195
|
+
className: cnAll(showValue && "flex items-baseline justify-between gap-2"),
|
|
196
|
+
children: [/* @__PURE__ */ jsx(Label, {
|
|
197
|
+
"data-slot": "progress-label",
|
|
198
|
+
className: cnAll("text-sm leading-(--leading-normal)", inverse ? "text-text-inverse" : "text-foreground", labelHidden && "sr-only"),
|
|
199
|
+
children: typeof label === "string" ? /* @__PURE__ */ jsx(SwapText, {
|
|
200
|
+
swapKey: label,
|
|
201
|
+
className: "block",
|
|
202
|
+
children: label
|
|
203
|
+
}) : label
|
|
204
|
+
}), showValue ? /* @__PURE__ */ jsx("span", {
|
|
205
|
+
"data-slot": "progress-value",
|
|
206
|
+
"aria-hidden": "true",
|
|
207
|
+
className: cnAll("text-sm leading-(--leading-normal) tabular-nums", inverse ? "text-text-inverse" : "text-muted-foreground"),
|
|
208
|
+
children: /* @__PURE__ */ jsx(ProgressValue, {
|
|
209
|
+
percentage: percentage ?? 0,
|
|
210
|
+
valueText,
|
|
211
|
+
showCompletion
|
|
212
|
+
})
|
|
213
|
+
}) : null]
|
|
214
|
+
}),
|
|
215
|
+
/* @__PURE__ */ jsx(TargetFrame, {
|
|
216
|
+
marker,
|
|
217
|
+
surface,
|
|
218
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
219
|
+
"data-slot": "progress-track",
|
|
220
|
+
className: cn(progressTrackVariants({ surface }), trackClassName),
|
|
221
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
222
|
+
"data-slot": "progress-fill",
|
|
223
|
+
"data-indeterminate": isIndeterminate || void 0,
|
|
224
|
+
"data-complete": showCompletion && (!tone || tone === "neutral") && !isIndeterminate && (percentage ?? 0) >= 100 ? "" : void 0,
|
|
225
|
+
className: cn([progressFillVariants({ tone }), inverse && (!tone || tone === "neutral") && "bg-text-inverse"], fillClassName),
|
|
226
|
+
style: isIndeterminate ? void 0 : { width: `${percentage ?? 0}%` }
|
|
227
|
+
})
|
|
114
228
|
})
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
className: cn(progressFillVariants({ tone }), fillClassName),
|
|
124
|
-
style: isIndeterminate ? void 0 : { width: `${percentage ?? 0}%` }
|
|
125
|
-
})
|
|
126
|
-
})] })
|
|
229
|
+
}),
|
|
230
|
+
marker ? /* @__PURE__ */ jsx(TargetText, {
|
|
231
|
+
id: targetId,
|
|
232
|
+
marker,
|
|
233
|
+
shown: showTargetLabel,
|
|
234
|
+
inverse
|
|
235
|
+
}) : null
|
|
236
|
+
] })
|
|
127
237
|
});
|
|
128
238
|
});
|
|
129
239
|
//#endregion
|
|
130
|
-
export { Progress, progressFillVariants, progressTrackVariants, progressVariants };
|
|
240
|
+
export { Progress, progressFillVariants, progressTargetVariants, progressTrackVariants, progressVariants };
|
|
131
241
|
|
|
132
242
|
//# sourceMappingURL=progress.js.map
|