@godxjp/ui 27.1.0 → 27.4.1
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/dist/components/data-display/badge.d.ts +10 -3
- package/dist/components/data-display/badge.js +14 -3
- package/dist/components/data-display/data-table.d.ts +20 -12
- package/dist/components/data-display/data-table.js +53 -5
- package/dist/components/data-display/range-timeline.d.ts +16 -0
- package/dist/components/data-display/range-timeline.js +115 -80
- package/dist/components/data-display/table.d.ts +16 -0
- package/dist/components/data-display/table.js +2 -0
- package/dist/components/data-entry/checkbox.js +23 -5
- package/dist/components/data-entry/command.d.ts +3 -1
- package/dist/components/data-entry/command.js +10 -2
- package/dist/components/data-entry/form-field.js +9 -8
- package/dist/components/data-entry/index.d.ts +1 -0
- package/dist/components/data-entry/search-select.js +145 -136
- package/dist/components/data-entry/select.js +38 -3
- package/dist/components/general/icon.d.ts +31 -0
- package/dist/components/general/icon.js +23 -0
- package/dist/components/general/index.d.ts +2 -0
- package/dist/components/general/index.js +2 -0
- package/dist/components/layout/flex.d.ts +2 -2
- package/dist/components/layout/flex.js +7 -1
- package/dist/components/layout/org-switcher.js +1 -1
- package/dist/components/layout/responsive-grid.d.ts +8 -2
- package/dist/components/layout/responsive-grid.js +2 -0
- package/dist/components/layout/topbar-item.d.ts +1 -0
- package/dist/components/layout/topbar-item.js +3 -1
- package/dist/contracts/measurement.json +1 -1
- package/dist/form/form-field-control.d.ts +1 -1
- package/dist/form/form-field-control.js +32 -23
- package/dist/i18n/messages/en.json +2 -1
- package/dist/i18n/messages/ja.json +2 -1
- package/dist/i18n/messages/vi.json +2 -1
- package/dist/props/components/data-display.prop.d.ts +12 -0
- package/dist/props/components/data-entry.prop.d.ts +10 -0
- package/dist/props/components/form.prop.d.ts +34 -2
- package/dist/props/components/general.prop.d.ts +42 -1
- package/dist/props/components/index.d.ts +1 -1
- package/dist/props/components/layout.prop.d.ts +49 -0
- package/dist/props/registry.d.ts +57 -3
- package/dist/props/registry.js +62 -2
- package/dist/props/vocabulary/data.prop.d.ts +27 -1
- package/dist/props/vocabulary/index.d.ts +2 -2
- package/dist/props/vocabulary/interaction.prop.d.ts +11 -0
- package/dist/styles/control.css +44 -1
- package/dist/styles/core.css +2 -0
- package/dist/styles/data-display-layout.css +58 -1
- package/dist/styles/form-layout.css +21 -0
- package/dist/styles/icon-layout.css +37 -0
- package/dist/styles/index.css +2 -0
- package/dist/styles/layout.css +7 -0
- package/dist/styles/navigation-layout.css +5 -1
- package/dist/styles/shell-layout.css +11 -14
- package/dist/styles/table-layout.css +80 -8
- package/dist/styles/text-layout.css +16 -8
- package/dist/tokens/components/control.css +5 -0
- package/dist/tokens/components/data-display.css +6 -0
- package/dist/tokens/components/navigation.css +1 -1
- package/dist/tokens/components/table.css +2 -0
- package/docs/CONSUMER-RULES.md +2 -2
- package/docs/DESIGN-AUTHORITY.md +32 -1
- package/docs/FRAME-COVERAGE-REPORT.md +3 -2
- package/docs/data-display/data-table/examples/server-paged.tsx +156 -0
- package/docs/data-display/data-table/examples/striped-list.tsx +173 -0
- package/docs/data-display/popover.tsx +43 -1
- package/docs/data-display/timeline.tsx +73 -9
- package/docs/data-entry/command.tsx +80 -1
- package/docs/data-entry/form-root.tsx +12 -2
- package/docs/general/icon.tsx +215 -0
- package/docs/layout/flex.tsx +59 -0
- package/docs/layout/responsive-grid.tsx +100 -0
- package/docs/layout/topbar-item.tsx +29 -1
- package/package.json +3 -2
- package/scripts/consumer-rule.md +18 -17
- package/scripts/ui-audit.mjs +128 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Checkbox as AriaCheckbox } from "react-aria-components";
|
|
5
5
|
import { Check, Minus } from "lucide-react";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
|
-
import { useFieldIdentity } from "../../lib/field-a11y.js";
|
|
7
|
+
import { mergeAriaIds, useFieldIdentity } from "../../lib/field-a11y.js";
|
|
8
|
+
import { Label } from "./label.js";
|
|
8
9
|
import { CheckboxGroup } from "./checkbox-group.js";
|
|
9
10
|
import { withOwnHitTarget } from "./choice-hit-target.js";
|
|
10
11
|
function CheckboxGlyph({ state }) {
|
|
@@ -39,9 +40,14 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
|
|
|
39
40
|
required,
|
|
40
41
|
value,
|
|
41
42
|
tabIndex,
|
|
43
|
+
children,
|
|
42
44
|
"data-field": ownField,
|
|
43
45
|
...rest
|
|
44
46
|
} = props;
|
|
47
|
+
const autoId = React.useId();
|
|
48
|
+
const hasInlineLabel = !(children === void 0 || children === null || children === false || children === "");
|
|
49
|
+
const inputId = props.id ?? (hasInlineLabel ? autoId : void 0);
|
|
50
|
+
const inlineLabelId = `${inputId}-inline-label`;
|
|
45
51
|
const identity = useFieldIdentity({
|
|
46
52
|
id: props.id,
|
|
47
53
|
name: props.name,
|
|
@@ -63,7 +69,10 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
|
|
|
63
69
|
);
|
|
64
70
|
const state = checked ?? uncontrolled;
|
|
65
71
|
const invalid = props["aria-invalid"];
|
|
66
|
-
|
|
72
|
+
const isInvalid = invalid !== void 0 && invalid !== false && invalid !== "false";
|
|
73
|
+
const describedBy = isInvalid ? mergeAriaIds(props["aria-describedby"], props["aria-errormessage"]) : props["aria-describedby"];
|
|
74
|
+
const labelledBy = hasInlineLabel && props["aria-labelledby"] !== void 0 ? mergeAriaIds(props["aria-labelledby"], inlineLabelId) : props["aria-labelledby"];
|
|
75
|
+
const box = /* @__PURE__ */ jsx(
|
|
67
76
|
AriaCheckbox,
|
|
68
77
|
{
|
|
69
78
|
...rest,
|
|
@@ -77,7 +86,10 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
|
|
|
77
86
|
isIndeterminate: state === "indeterminate",
|
|
78
87
|
isDisabled: disabled,
|
|
79
88
|
isRequired: required,
|
|
80
|
-
isInvalid
|
|
89
|
+
isInvalid,
|
|
90
|
+
id: inputId,
|
|
91
|
+
"aria-describedby": describedBy,
|
|
92
|
+
"aria-labelledby": labelledBy,
|
|
81
93
|
excludeFromTabOrder: tabIndex !== void 0 && tabIndex < 0,
|
|
82
94
|
className: cn(
|
|
83
95
|
// `disabled:cursor-not-allowed disabled:opacity-50` and the checked fill are DELETED, not
|
|
@@ -95,7 +107,8 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
|
|
|
95
107
|
// 16px box collapses to nothing. CheckboxVisual already carries the same three for the
|
|
96
108
|
// same reason.
|
|
97
109
|
"peer ui-checkbox data-[invalid]:border-destructive data-[state=checked]:border-primary data-[state=checked]:text-primary-foreground inline-flex shrink-0 items-center justify-center shadow-xs transition-shadow",
|
|
98
|
-
|
|
110
|
+
// With an inline label the row is what a caller styles; the box keeps its own classes.
|
|
111
|
+
hasInlineLabel ? void 0 : className
|
|
99
112
|
),
|
|
100
113
|
render: (domProps) => /* @__PURE__ */ jsx("label", { ...domProps, children: withOwnHitTarget(domProps.children) }),
|
|
101
114
|
onChange: (next) => {
|
|
@@ -105,6 +118,11 @@ const CheckboxRoot = React.forwardRef((props, ref) => {
|
|
|
105
118
|
children: /* @__PURE__ */ jsx("span", { "data-slot": "checkbox-indicator", className: "ui-choice-indicator", children: state ? /* @__PURE__ */ jsx(CheckboxGlyph, { state }) : null })
|
|
106
119
|
}
|
|
107
120
|
);
|
|
121
|
+
if (!hasInlineLabel) return box;
|
|
122
|
+
return /* @__PURE__ */ jsxs("div", { "data-slot": "checkbox-field", className: cn("ui-choice-field", className), children: [
|
|
123
|
+
/* @__PURE__ */ jsx("div", { className: "ui-choice-control", children: box }),
|
|
124
|
+
/* @__PURE__ */ jsx("div", { className: "ui-choice-content", children: /* @__PURE__ */ jsx(Label, { id: inlineLabelId, htmlFor: inputId, className: "ui-choice-label", children }) })
|
|
125
|
+
] });
|
|
108
126
|
});
|
|
109
127
|
CheckboxRoot.displayName = "Checkbox";
|
|
110
128
|
const Checkbox = Object.assign(CheckboxRoot, {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
+
import type { CommandProp } from "../../props/components/data-entry.prop.js";
|
|
3
|
+
export type { CommandProp, CommandProp as CommandProps };
|
|
2
4
|
export declare const Command: React.ForwardRefExoticComponent<Omit<{
|
|
3
5
|
children?: React.ReactNode;
|
|
4
6
|
} & Pick<Pick<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "key" | keyof React.HTMLAttributes<HTMLDivElement>> & {
|
|
@@ -15,7 +17,7 @@ export declare const Command: React.ForwardRefExoticComponent<Omit<{
|
|
|
15
17
|
loop?: boolean;
|
|
16
18
|
disablePointerSelection?: boolean;
|
|
17
19
|
vimBindings?: boolean;
|
|
18
|
-
} & React.RefAttributes<HTMLDivElement>, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
20
|
+
} & React.RefAttributes<HTMLDivElement>, "ref"> & CommandProp & React.RefAttributes<HTMLDivElement>>;
|
|
19
21
|
export declare const CommandInput: React.ForwardRefExoticComponent<Omit<Omit<Pick<Pick<React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>, "key" | keyof React.InputHTMLAttributes<HTMLInputElement>> & {
|
|
20
22
|
ref?: React.Ref<HTMLInputElement>;
|
|
21
23
|
} & {
|
|
@@ -5,7 +5,7 @@ import { Command as CommandPrimitive } from "cmdk";
|
|
|
5
5
|
import { Search } from "lucide-react";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
7
|
import { controlIconLeadingClass } from "../../lib/control-styles.js";
|
|
8
|
-
const Command = React.forwardRef(({ className, ...props }, ref) => {
|
|
8
|
+
const Command = React.forwardRef(({ className, split = false, ...props }, ref) => {
|
|
9
9
|
const rootRef = React.useRef(null);
|
|
10
10
|
const setRefs = (node) => {
|
|
11
11
|
rootRef.current = node;
|
|
@@ -22,7 +22,15 @@ const Command = React.forwardRef(({ className, ...props }, ref) => {
|
|
|
22
22
|
if (input && expectedId && !input.id) input.id = expectedId;
|
|
23
23
|
else if (!input || input.id !== expectedId) label.removeAttribute("for");
|
|
24
24
|
});
|
|
25
|
-
return /* @__PURE__ */ jsx(
|
|
25
|
+
return /* @__PURE__ */ jsx(
|
|
26
|
+
CommandPrimitive,
|
|
27
|
+
{
|
|
28
|
+
ref: setRefs,
|
|
29
|
+
className: cn("ui-command", className),
|
|
30
|
+
"data-split": split ? "" : void 0,
|
|
31
|
+
...props
|
|
32
|
+
}
|
|
33
|
+
);
|
|
26
34
|
});
|
|
27
35
|
Command.displayName = CommandPrimitive.displayName;
|
|
28
36
|
const CommandInput = React.forwardRef(({ className, ...props }, ref) => (
|
|
@@ -47,6 +47,7 @@ function FormField({
|
|
|
47
47
|
const autoId = React.useId();
|
|
48
48
|
const resolvedId = id ?? autoId;
|
|
49
49
|
const labelId = `${resolvedId}-label`;
|
|
50
|
+
const hasLabel = !(label === void 0 || label === null || label === false || label === "");
|
|
50
51
|
const helperId = helper ? `${resolvedId}-helper` : void 0;
|
|
51
52
|
const helperNode = helper ? /* @__PURE__ */ jsx("p", { id: helperId, className: "ui-form-field-note text-muted-foreground text-xs", children: helper }) : null;
|
|
52
53
|
const validationStatus = error ? "error" : validateStatus;
|
|
@@ -62,8 +63,8 @@ function FormField({
|
|
|
62
63
|
);
|
|
63
64
|
}
|
|
64
65
|
const fieldNameContext = React.useMemo(
|
|
65
|
-
() =>
|
|
66
|
-
[labelId, label]
|
|
66
|
+
() => hasLabel ? { labelId, label: typeof label === "string" ? label : void 0 } : null,
|
|
67
|
+
[hasLabel, labelId, label]
|
|
67
68
|
);
|
|
68
69
|
const fieldIdentityContext = React.useMemo(
|
|
69
70
|
() => fieldKey === void 0 ? null : { emitName: emitFieldNames },
|
|
@@ -88,12 +89,12 @@ function FormField({
|
|
|
88
89
|
// `"data-field": fieldKey` would erase a value the control set for itself.
|
|
89
90
|
"data-field": childProps?.["data-field"] ?? fieldKey,
|
|
90
91
|
...emitFieldNames ? { name: childProps?.name ?? fieldKey } : void 0,
|
|
91
|
-
"aria-labelledby": childProps?.["aria-labelledby"] ?? labelId,
|
|
92
|
+
"aria-labelledby": childProps?.["aria-labelledby"] ?? (hasLabel ? labelId : void 0),
|
|
92
93
|
// Redundant `aria-label` fallback (belt-and-suspenders): the accessible name is the
|
|
93
94
|
// SAME string as the visible label, just reachable even if an aria-labelledby lookup
|
|
94
95
|
// ever comes back empty (id-ref timing, AT quirks). Only when `label` is plain text and
|
|
95
96
|
// the child hasn't already set its own aria-label.
|
|
96
|
-
"aria-label": childProps?.["aria-label"] ?? (typeof label === "string" ? label : void 0),
|
|
97
|
+
"aria-label": childProps?.["aria-label"] ?? (hasLabel && typeof label === "string" ? label : void 0),
|
|
97
98
|
// Helper and error can coexist: helper stays on aria-describedby, the error on
|
|
98
99
|
// aria-errormessage (surfaced when aria-invalid is true).
|
|
99
100
|
"aria-describedby": mergeIds(
|
|
@@ -124,8 +125,8 @@ function FormField({
|
|
|
124
125
|
style: Object.keys(style).length ? style : void 0,
|
|
125
126
|
className: cn("ui-form-field", className),
|
|
126
127
|
children: [
|
|
127
|
-
/* @__PURE__ */ jsxs("div", { "data-slot": "form-field-label", className: "ui-form-field-label", children: [
|
|
128
|
-
/* @__PURE__ */ jsx(
|
|
128
|
+
hasLabel || labelAddon != null ? /* @__PURE__ */ jsxs("div", { "data-slot": "form-field-label", className: "ui-form-field-label", children: [
|
|
129
|
+
hasLabel ? /* @__PURE__ */ jsx(
|
|
129
130
|
Label,
|
|
130
131
|
{
|
|
131
132
|
asChild: true,
|
|
@@ -151,9 +152,9 @@ function FormField({
|
|
|
151
152
|
}
|
|
152
153
|
)
|
|
153
154
|
}
|
|
154
|
-
),
|
|
155
|
+
) : null,
|
|
155
156
|
labelAddon
|
|
156
|
-
] }),
|
|
157
|
+
] }) : null,
|
|
157
158
|
/* @__PURE__ */ jsxs("div", { "data-slot": "form-field-control", className: "ui-form-field-control", children: [
|
|
158
159
|
helperPlacement === "before" ? helperNode : null,
|
|
159
160
|
isStatic ? childWithA11y : /* @__PURE__ */ jsx(FieldNameContext.Provider, { value: fieldNameContext, children: /* @__PURE__ */ jsx(FieldIdentityContext.Provider, { value: fieldIdentityContext, children: /* @__PURE__ */ jsx(
|
|
@@ -49,6 +49,7 @@ export type { TreeSelectProps } from "./tree-select.js";
|
|
|
49
49
|
export { Transfer } from "./transfer.js";
|
|
50
50
|
export type { TransferProps, TransferItemProp } from "./transfer.js";
|
|
51
51
|
export { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./command.js";
|
|
52
|
+
export type { CommandProps } from "./command.js";
|
|
52
53
|
export { CommandPalette } from "./command-palette.js";
|
|
53
54
|
export type { CommandPaletteGroup, CommandPaletteItem, CommandPaletteLabels, CommandPaletteProps, } from "./command-palette.js";
|
|
54
55
|
export { PasswordInput } from "./password-input.js";
|
|
@@ -511,144 +511,153 @@ function SearchSelect(props) {
|
|
|
511
511
|
style: typeof popupMatchSelectWidth === "number" ? {
|
|
512
512
|
"--search-select-panel-inline-size": `${popupMatchSelectWidth}px`
|
|
513
513
|
} : void 0,
|
|
514
|
-
children: /* @__PURE__ */ jsx(PopupRender, { render: popupRender, children: /* @__PURE__ */ jsxs(
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
"
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
514
|
+
children: /* @__PURE__ */ jsx(PopupRender, { render: popupRender, children: /* @__PURE__ */ jsxs(
|
|
515
|
+
Command,
|
|
516
|
+
{
|
|
517
|
+
value,
|
|
518
|
+
shouldFilter: false,
|
|
519
|
+
className: "ui-search-select-command",
|
|
520
|
+
split: multiple,
|
|
521
|
+
children: [
|
|
522
|
+
/* @__PURE__ */ jsx("div", { className: "ui-search-select-search", children: /* @__PURE__ */ jsx(
|
|
523
|
+
Input,
|
|
524
|
+
{
|
|
525
|
+
autoFocus: true,
|
|
526
|
+
"aria-controls": listId,
|
|
527
|
+
"aria-autocomplete": "list",
|
|
528
|
+
"aria-activedescendant": activeOptionId,
|
|
529
|
+
"aria-label": searchPlaceholder ?? t("dataEntry.searchSelect.search"),
|
|
530
|
+
value: query,
|
|
531
|
+
onChange: (event) => {
|
|
532
|
+
const next = event.target.value;
|
|
533
|
+
if (multiple && tokenSeparators?.length && tokenSeparators.some((separator) => next.includes(separator))) {
|
|
534
|
+
selectTokens(next);
|
|
535
|
+
setQuery("");
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
setQuery(next);
|
|
539
|
+
},
|
|
540
|
+
onPaste: (event) => {
|
|
541
|
+
if (!multiple || !tokenSeparators?.length) return;
|
|
542
|
+
const text = event.clipboardData.getData("text");
|
|
543
|
+
if (!tokenSeparators.some((separator) => text.includes(separator))) return;
|
|
544
|
+
event.preventDefault();
|
|
545
|
+
selectTokens(text);
|
|
546
|
+
setQuery("");
|
|
547
|
+
},
|
|
548
|
+
onKeyDown,
|
|
549
|
+
placeholder: searchPlaceholder ?? t("dataEntry.searchSelect.search"),
|
|
550
|
+
className: "ui-search-select-search-input"
|
|
530
551
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
"data-tone": "destructive",
|
|
622
|
-
children: errorMessage ?? t("dataEntry.searchSelect.error")
|
|
623
|
-
}
|
|
624
|
-
) : displayOptions.length === 0 ? /* @__PURE__ */ jsx(
|
|
625
|
-
"div",
|
|
626
|
-
{
|
|
627
|
-
role: "option",
|
|
628
|
-
"aria-disabled": "true",
|
|
629
|
-
"aria-selected": false,
|
|
630
|
-
className: "ui-search-select-placeholder",
|
|
631
|
-
children: notFoundContent ?? emptyMessage ?? t("dataEntry.searchSelect.empty")
|
|
632
|
-
}
|
|
633
|
-
) : null
|
|
634
|
-
]
|
|
635
|
-
}
|
|
636
|
-
),
|
|
637
|
-
renderLoadMore && hasMore ? /* @__PURE__ */ jsx(
|
|
638
|
-
"div",
|
|
639
|
-
{
|
|
640
|
-
className: "ui-search-select-footer",
|
|
641
|
-
onKeyDown: (event) => event.stopPropagation(),
|
|
642
|
-
children: renderLoadMore({
|
|
643
|
-
hasMore,
|
|
644
|
-
loading,
|
|
645
|
-
loadMore: () => {
|
|
646
|
-
if (!loading) void fetchPage(page + 1, debouncedQuery, true);
|
|
552
|
+
) }),
|
|
553
|
+
/* @__PURE__ */ jsxs(
|
|
554
|
+
"div",
|
|
555
|
+
{
|
|
556
|
+
id: listId,
|
|
557
|
+
role: "listbox",
|
|
558
|
+
"aria-multiselectable": multiple || void 0,
|
|
559
|
+
"aria-busy": loading,
|
|
560
|
+
className: "ui-search-select-list",
|
|
561
|
+
style: listHeight === void 0 ? void 0 : {
|
|
562
|
+
"--search-select-list-max-height": `${listHeight}px`
|
|
563
|
+
},
|
|
564
|
+
onScroll: (event) => {
|
|
565
|
+
onPopupScroll?.(event);
|
|
566
|
+
onScroll(event);
|
|
567
|
+
},
|
|
568
|
+
children: [
|
|
569
|
+
grouped.map((group) => {
|
|
570
|
+
const rows = group.items.map(({ option, index }) => /* @__PURE__ */ jsxs(
|
|
571
|
+
"div",
|
|
572
|
+
{
|
|
573
|
+
id: optionDomId(option.value),
|
|
574
|
+
role: "option",
|
|
575
|
+
"data-testid": optionTestId(option.value),
|
|
576
|
+
"aria-selected": isSelected(option.value),
|
|
577
|
+
"aria-disabled": option.disabled || atMaxCount && !isSelected(option.value) || void 0,
|
|
578
|
+
"data-disabled": option.disabled || atMaxCount && !isSelected(option.value) || void 0,
|
|
579
|
+
className: cn(
|
|
580
|
+
"ui-command-item",
|
|
581
|
+
// Selected = persistent bg-accent + medium weight (NO check icon — saves width),
|
|
582
|
+
// matching the plain SelectItem's `data-[state=checked]` convention; active =
|
|
583
|
+
// hover/keyboard accent. Same bg so selection stays coherent across both Selects.
|
|
584
|
+
isSelected(option.value) && "bg-accent text-foreground font-medium",
|
|
585
|
+
activeIndex === index && "bg-accent text-accent-foreground"
|
|
586
|
+
),
|
|
587
|
+
onMouseEnter: () => !option.disabled && setActiveIndex(index),
|
|
588
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
589
|
+
onClick: () => select(option),
|
|
590
|
+
children: [
|
|
591
|
+
optionRender ? /* @__PURE__ */ jsx("div", { className: "ui-search-select-option-slot", children: optionRender(option, { index }) }) : renderOption ? /* @__PURE__ */ jsx("div", { className: "ui-search-select-option-slot", children: renderOption(option) }) : /* @__PURE__ */ jsxs("div", { className: "ui-search-select-option-body", children: [
|
|
592
|
+
option.icon ? /* @__PURE__ */ jsx("span", { className: "flex shrink-0 items-center", "aria-hidden": "true", children: option.icon }) : null,
|
|
593
|
+
/* @__PURE__ */ jsxs("div", { className: "ui-search-select-option-text", children: [
|
|
594
|
+
/* @__PURE__ */ jsx("span", { className: "ui-search-select-option-label", children: option.label }),
|
|
595
|
+
option.sublabel ? /* @__PURE__ */ jsx("span", { className: "ui-search-select-option-sublabel", children: option.sublabel }) : null
|
|
596
|
+
] })
|
|
597
|
+
] }),
|
|
598
|
+
menuItemSelectedIcon && isSelected(option.value) ? /* @__PURE__ */ jsx(
|
|
599
|
+
"span",
|
|
600
|
+
{
|
|
601
|
+
"data-slot": "search-select-selected-icon",
|
|
602
|
+
className: "ui-search-select-selected-icon",
|
|
603
|
+
"aria-hidden": "true",
|
|
604
|
+
children: menuItemSelectedIcon
|
|
605
|
+
}
|
|
606
|
+
) : null
|
|
607
|
+
]
|
|
608
|
+
},
|
|
609
|
+
option.value
|
|
610
|
+
));
|
|
611
|
+
return group.heading ? /* @__PURE__ */ jsx(CommandGroup, { heading: group.heading, children: rows }, group.heading) : /* @__PURE__ */ jsx(React.Fragment, { children: rows }, "__ungrouped");
|
|
612
|
+
}),
|
|
613
|
+
loading ? /* @__PURE__ */ jsxs("div", { role: "status", className: "ui-search-select-status", children: [
|
|
614
|
+
/* @__PURE__ */ jsx(Loader2, { className: "ui-search-select-spinner animate-spin", "aria-hidden": "true" }),
|
|
615
|
+
loadingMessage ?? t("dataEntry.searchSelect.loading")
|
|
616
|
+
] }) : error ? renderError ? renderError({
|
|
617
|
+
message: errorMessage ?? t("dataEntry.searchSelect.error"),
|
|
618
|
+
// Retry always reloads from the first page (a defined, predictable recovery —
|
|
619
|
+
// NOT a resume of a failed page-N append, which would need to re-derive state).
|
|
620
|
+
retry: () => void fetchPage(1, debouncedQuery, false)
|
|
621
|
+
}) : /* @__PURE__ */ jsx(
|
|
622
|
+
"div",
|
|
623
|
+
{
|
|
624
|
+
role: "option",
|
|
625
|
+
"aria-disabled": "true",
|
|
626
|
+
"aria-selected": false,
|
|
627
|
+
className: "ui-search-select-placeholder",
|
|
628
|
+
"data-tone": "destructive",
|
|
629
|
+
children: errorMessage ?? t("dataEntry.searchSelect.error")
|
|
630
|
+
}
|
|
631
|
+
) : displayOptions.length === 0 ? /* @__PURE__ */ jsx(
|
|
632
|
+
"div",
|
|
633
|
+
{
|
|
634
|
+
role: "option",
|
|
635
|
+
"aria-disabled": "true",
|
|
636
|
+
"aria-selected": false,
|
|
637
|
+
className: "ui-search-select-placeholder",
|
|
638
|
+
children: notFoundContent ?? emptyMessage ?? t("dataEntry.searchSelect.empty")
|
|
639
|
+
}
|
|
640
|
+
) : null
|
|
641
|
+
]
|
|
647
642
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
643
|
+
),
|
|
644
|
+
renderLoadMore && hasMore ? /* @__PURE__ */ jsx(
|
|
645
|
+
"div",
|
|
646
|
+
{
|
|
647
|
+
className: "ui-search-select-footer",
|
|
648
|
+
onKeyDown: (event) => event.stopPropagation(),
|
|
649
|
+
children: renderLoadMore({
|
|
650
|
+
hasMore,
|
|
651
|
+
loading,
|
|
652
|
+
loadMore: () => {
|
|
653
|
+
if (!loading) void fetchPage(page + 1, debouncedQuery, true);
|
|
654
|
+
}
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
) : null
|
|
658
|
+
]
|
|
659
|
+
}
|
|
660
|
+
) })
|
|
652
661
|
}
|
|
653
662
|
)
|
|
654
663
|
]
|
|
@@ -55,6 +55,35 @@ function itemTextValue(node) {
|
|
|
55
55
|
}
|
|
56
56
|
return "";
|
|
57
57
|
}
|
|
58
|
+
function useSettledDefaultOpen(rootRef, defaultOpen, open) {
|
|
59
|
+
const [deferred] = React.useState(() => defaultOpen === true && open === void 0);
|
|
60
|
+
const [settledOpen, setSettledOpen] = React.useState(false);
|
|
61
|
+
React.useLayoutEffect(() => {
|
|
62
|
+
if (!deferred) return;
|
|
63
|
+
let cancelled = false;
|
|
64
|
+
const moving = [];
|
|
65
|
+
for (let node = rootRef.current?.parentElement; node; node = node.parentElement) {
|
|
66
|
+
if (typeof node.getAnimations !== "function") break;
|
|
67
|
+
for (const animation of node.getAnimations()) {
|
|
68
|
+
const end = animation.effect?.getComputedTiming().endTime;
|
|
69
|
+
if (animation.playState === "running" && Number.isFinite(Number(end))) {
|
|
70
|
+
moving.push(animation.finished);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (moving.length === 0) {
|
|
75
|
+
setSettledOpen(true);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
void Promise.allSettled(moving).then(() => {
|
|
79
|
+
if (!cancelled) setSettledOpen(true);
|
|
80
|
+
});
|
|
81
|
+
return () => {
|
|
82
|
+
cancelled = true;
|
|
83
|
+
};
|
|
84
|
+
}, [deferred, rootRef]);
|
|
85
|
+
return deferred ? { open: settledOpen, setOpen: setSettledOpen } : null;
|
|
86
|
+
}
|
|
58
87
|
function SelectRoot({
|
|
59
88
|
id,
|
|
60
89
|
value,
|
|
@@ -76,9 +105,12 @@ function SelectRoot({
|
|
|
76
105
|
children
|
|
77
106
|
}) {
|
|
78
107
|
void dir;
|
|
108
|
+
const rootRef = React.useRef(null);
|
|
109
|
+
const settled = useSettledDefaultOpen(rootRef, defaultOpen, open);
|
|
79
110
|
return /* @__PURE__ */ jsx(
|
|
80
111
|
AriaSelect,
|
|
81
112
|
{
|
|
113
|
+
ref: rootRef,
|
|
82
114
|
"data-slot": "select",
|
|
83
115
|
className: "ui-select-root",
|
|
84
116
|
id,
|
|
@@ -87,9 +119,12 @@ function SelectRoot({
|
|
|
87
119
|
onChange: (key) => {
|
|
88
120
|
if (key != null) onValueChange?.(String(key));
|
|
89
121
|
},
|
|
90
|
-
isOpen: open,
|
|
91
|
-
defaultOpen,
|
|
92
|
-
onOpenChange
|
|
122
|
+
isOpen: open ?? settled?.open,
|
|
123
|
+
defaultOpen: settled ? void 0 : defaultOpen,
|
|
124
|
+
onOpenChange: (next) => {
|
|
125
|
+
settled?.setOpen(next);
|
|
126
|
+
onOpenChange?.(next);
|
|
127
|
+
},
|
|
93
128
|
isDisabled: disabled || isDisabled,
|
|
94
129
|
isRequired: required,
|
|
95
130
|
name,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type { IconGlyphProp, IconGlyphProp as IconProps, } from "../../props/components/general.prop.js";
|
|
3
|
+
/**
|
|
4
|
+
* Icon — a glyph on the `--icon-size-*` scale, and the only supported way to draw a standalone one.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS (gh#712). A lucide component ships `width="24" height="24"`, and exactly four
|
|
7
|
+
* rules in this library ever override that: `.ui-button svg`, `.ui-dropdown-menu-item > svg`,
|
|
8
|
+
* `.ui-topbar-item > svg` and `[data-slot="list-row-leading"] > svg`. Anywhere else — in a `Text`,
|
|
9
|
+
* in a table cell, in an `<a>` — the glyph draws at 24px beside 14px type, 1.71× the text it
|
|
10
|
+
* annotates. A consumer cannot fix that themselves: `size-4` and `w-[16px]` are what
|
|
11
|
+
* docs/CONSUMER-RULES.md §3/§8 forbid, and `size={16}` hard-codes a number the theme owns. One
|
|
12
|
+
* consumer measured 38 such glyphs in a single app.
|
|
13
|
+
*
|
|
14
|
+
* RENDERS ONTO THE GLYPH, NOT AROUND IT. `as={Lock}` is invoked with the class, the size and the
|
|
15
|
+
* a11y wiring, so the element that carries the metric IS the `<svg>`. A wrapper `<span>` would add
|
|
16
|
+
* a box to every flex row that holds an icon, and would break `Button`'s own `svg` rule by putting
|
|
17
|
+
* an element between the two.
|
|
18
|
+
*
|
|
19
|
+
* DECORATIVE BY DEFAULT. Without `label` the glyph is `aria-hidden` — a glyph beside a visible
|
|
20
|
+
* label is decoration, and announcing it twice is the usual defect (WCAG 2.2 SC 1.1.1). Pass
|
|
21
|
+
* `label` only when the glyph is the ONLY thing saying what it says; it then renders as
|
|
22
|
+
* `role="img"` + that name, which is how a graphic gets a name in WAI-ARIA 1.2. The string is the
|
|
23
|
+
* consumer's to localize — the library ships no copy for it.
|
|
24
|
+
*/
|
|
25
|
+
export declare const Icon: React.ForwardRefExoticComponent<Omit<React.SVGProps<SVGSVGElement>, "children" | "ref"> & {
|
|
26
|
+
as: React.ComponentType<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>;
|
|
27
|
+
size?: import("../../props/index.js").IconSizeProp;
|
|
28
|
+
tone?: import("../../props/index.js").TextToneProp;
|
|
29
|
+
label?: string;
|
|
30
|
+
className?: import("../../props/index.js").ClassNameProp;
|
|
31
|
+
} & React.RefAttributes<SVGSVGElement>>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
const Icon = React.forwardRef(function Icon2({ as: Glyph, size = "md", tone, label, className, ...props }, ref) {
|
|
5
|
+
const named = label !== void 0 && label !== "";
|
|
6
|
+
return /* @__PURE__ */ jsx(
|
|
7
|
+
Glyph,
|
|
8
|
+
{
|
|
9
|
+
ref,
|
|
10
|
+
"data-slot": "icon",
|
|
11
|
+
"data-size": size,
|
|
12
|
+
"data-tone": tone,
|
|
13
|
+
className: cn("ui-icon", className),
|
|
14
|
+
role: named ? "img" : void 0,
|
|
15
|
+
"aria-label": named ? label : void 0,
|
|
16
|
+
"aria-hidden": named ? void 0 : true,
|
|
17
|
+
...props
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
});
|
|
21
|
+
export {
|
|
22
|
+
Icon
|
|
23
|
+
};
|
|
@@ -2,6 +2,8 @@ export { Button, buttonVariants } from "./button.js";
|
|
|
2
2
|
export type { ButtonProps } from "./button.js";
|
|
3
3
|
export { Text, Heading, Typography, Title, Paragraph, Link } from "./typography.js";
|
|
4
4
|
export type { TextProps, HeadingProps, TextProp, HeadingProp, TypographyProp, TypographyProps, TypographyBlockProp, TypographyTitleProp, TitleProps, ParagraphProp, ParagraphProps, LinkProp, LinkProps, } from "./typography.js";
|
|
5
|
+
export { Icon } from "./icon.js";
|
|
6
|
+
export type { IconGlyphProp, IconProps } from "./icon.js";
|
|
5
7
|
export { Logo } from "./logo.js";
|
|
6
8
|
export type { LogoMark, LogoProps, LogoSize, LogoTone } from "./logo.js";
|
|
7
9
|
export { Reveal } from "./reveal.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Button, buttonVariants } from "./button.js";
|
|
2
2
|
import { Text, Heading, Typography, Title, Paragraph, Link } from "./typography.js";
|
|
3
|
+
import { Icon } from "./icon.js";
|
|
3
4
|
import { Logo } from "./logo.js";
|
|
4
5
|
import { Reveal } from "./reveal.js";
|
|
5
6
|
import { FloatButton } from "./float-button.js";
|
|
@@ -15,6 +16,7 @@ export {
|
|
|
15
16
|
Button,
|
|
16
17
|
FloatButton,
|
|
17
18
|
Heading,
|
|
19
|
+
Icon,
|
|
18
20
|
Link,
|
|
19
21
|
Logo,
|
|
20
22
|
Paragraph,
|