@godxjp/ui 18.15.4 → 19.1.0
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/app/app-provider.d.ts +1 -1
- package/dist/app/app-provider.js +3 -0
- package/dist/app/date-format-labels.d.ts +1 -1
- package/dist/app/time-format-labels.d.ts +1 -1
- package/dist/components/data-display/badge.d.ts +1 -1
- package/dist/components/data-display/card.d.ts +4 -13
- package/dist/components/data-display/card.js +2 -15
- package/dist/components/data-display/data-table.d.ts +3 -8
- package/dist/components/data-display/data-table.js +106 -68
- package/dist/components/data-display/descriptions.js +34 -5
- package/dist/components/data-display/empty-state.js +1 -1
- package/dist/components/data-display/permission-matrix.js +7 -7
- package/dist/components/data-display/popover.js +2 -9
- package/dist/components/data-display/qr-code.d.ts +1 -1
- package/dist/components/data-display/scroll-area.d.ts +3 -1
- package/dist/components/data-display/scroll-area.js +150 -16
- package/dist/components/data-display/table.js +12 -3
- package/dist/components/data-entry/branch-scope-picker.js +4 -4
- package/dist/components/data-entry/calendar.js +15 -24
- package/dist/components/data-entry/cascader.js +70 -72
- package/dist/components/data-entry/checkbox-group.js +12 -5
- package/dist/components/data-entry/checkbox.js +30 -13
- package/dist/components/data-entry/command.d.ts +6 -6
- package/dist/components/data-entry/date-picker.js +34 -17
- package/dist/components/data-entry/date-range-picker.js +19 -13
- package/dist/components/data-entry/form-field.d.ts +1 -1
- package/dist/components/data-entry/form-field.js +22 -5
- package/dist/components/data-entry/index.d.ts +2 -1
- package/dist/components/data-entry/input.js +18 -22
- package/dist/components/data-entry/label.js +4 -2
- package/dist/components/data-entry/month-picker.js +20 -17
- package/dist/components/data-entry/month-range-picker.js +26 -19
- package/dist/components/data-entry/radio.js +25 -6
- package/dist/components/data-entry/search-select.d.ts +1 -1
- package/dist/components/data-entry/search-select.js +37 -31
- package/dist/components/data-entry/select.d.ts +8 -0
- package/dist/components/data-entry/select.js +68 -26
- package/dist/components/data-entry/switch.js +16 -2
- package/dist/components/data-entry/textarea.d.ts +56 -0
- package/dist/components/data-entry/textarea.js +110 -40
- package/dist/components/data-entry/time-picker.js +52 -52
- package/dist/components/data-entry/toggle-group.d.ts +1 -0
- package/dist/components/data-entry/toggle.d.ts +1 -1
- package/dist/components/data-entry/transfer.js +14 -15
- package/dist/components/data-entry/tree-select.js +19 -23
- package/dist/components/data-entry/upload-crop-dialog.js +4 -4
- package/dist/components/data-entry/upload.d.ts +1 -1
- package/dist/components/data-entry/upload.js +46 -46
- package/dist/components/feedback/alert.js +7 -14
- package/dist/components/feedback/dialog.d.ts +23 -32
- package/dist/components/feedback/dialog.js +16 -13
- package/dist/components/feedback/sheet.d.ts +1 -1
- package/dist/components/feedback/sheet.js +14 -14
- package/dist/components/feedback/skeleton.js +13 -10
- package/dist/components/feedback/sonner.d.ts +1 -1
- package/dist/components/feedback/sonner.js +17 -8
- package/dist/components/feedback/tooltip.js +1 -1
- package/dist/components/general/activity.d.ts +44 -0
- package/dist/components/general/activity.js +65 -0
- package/dist/components/general/button.d.ts +2 -2
- package/dist/components/general/button.js +11 -24
- package/dist/components/general/index.d.ts +2 -0
- package/dist/components/general/index.js +2 -0
- package/dist/components/layout/app-shell.js +5 -3
- package/dist/components/layout/auth-divider.d.ts +9 -2
- package/dist/components/layout/auth-divider.js +7 -11
- package/dist/components/layout/index.d.ts +1 -0
- package/dist/components/layout/page-container.d.ts +1 -1
- package/dist/components/layout/page-container.js +32 -20
- package/dist/components/layout/resizable.d.ts +12 -12
- package/dist/components/layout/separator.d.ts +1 -0
- package/dist/components/layout/service-role-panel.js +47 -42
- package/dist/components/layout/sidebar.js +13 -1
- package/dist/components/layout/split-pane.d.ts +12 -1
- package/dist/components/layout/split-pane.js +13 -4
- package/dist/components/navigation/app-setting-picker.js +5 -14
- package/dist/components/navigation/dropdown-menu.js +10 -16
- package/dist/components/navigation/menubar.d.ts +3 -6
- package/dist/components/navigation/pagination-utils.d.ts +15 -2
- package/dist/components/navigation/pagination-utils.js +27 -21
- package/dist/components/navigation/pagination.d.ts +4 -4
- package/dist/components/navigation/pagination.js +2 -2
- package/dist/components/navigation/steps.js +31 -67
- package/dist/components/navigation/tabs.js +17 -5
- package/dist/components/query/infinite-query-state.js +2 -2
- package/dist/components/ui/accordion.d.ts +1 -1
- package/dist/components/ui/avatar.d.ts +1 -1
- package/dist/components/ui/avatar.js +29 -10
- package/dist/components/ui/input-otp.d.ts +8 -6
- package/dist/components/ui/rating.js +1 -1
- package/dist/components/ui/separator.d.ts +27 -2
- package/dist/components/ui/separator.js +42 -11
- package/dist/components/ui/toggle-group.d.ts +15 -5
- package/dist/components/ui/toggle-group.js +43 -18
- package/dist/components/ui/toggle.d.ts +66 -5
- package/dist/components/ui/toggle.js +64 -9
- package/dist/i18n/messages/en.json +8 -0
- package/dist/i18n/messages/ja.json +8 -0
- package/dist/i18n/messages/vi.json +8 -0
- package/dist/lib/control-styles.d.ts +14 -0
- package/dist/lib/control-styles.js +2 -0
- package/dist/lib/field-a11y.d.ts +52 -0
- package/dist/lib/field-a11y.js +17 -2
- package/dist/props/components/app.prop.d.ts +15 -0
- package/dist/props/components/data-display.prop.d.ts +85 -3
- package/dist/props/components/data-entry.prop.d.ts +35 -2
- package/dist/props/components/general.prop.d.ts +52 -1
- package/dist/props/components/layout.prop.d.ts +137 -2
- package/dist/props/registry.d.ts +116 -5
- package/dist/props/registry.js +165 -4
- package/dist/props/vocabulary/index.d.ts +2 -2
- package/dist/props/vocabulary/interaction.prop.d.ts +15 -0
- package/dist/props/vocabulary/layout.prop.d.ts +8 -0
- package/dist/styles/alert-layout.css +69 -3
- package/dist/styles/badge-layout.css +2 -2
- package/dist/styles/base.css +6 -0
- package/dist/styles/card-layout.css +25 -8
- package/dist/styles/control.css +1150 -29
- package/dist/styles/data-display-layout.css +185 -11
- package/dist/styles/data-entry-layout.css +352 -1
- package/dist/styles/density.css +2 -2
- package/dist/styles/dialog-layout.css +203 -4
- package/dist/styles/focus-ring.css +155 -0
- package/dist/styles/form-layout.css +50 -5
- package/dist/styles/index.css +1 -0
- package/dist/styles/layout.css +412 -23
- package/dist/styles/motion.css +178 -0
- package/dist/styles/navigation-layout.css +398 -22
- package/dist/styles/shell-layout.css +137 -39
- package/dist/styles/table-layout.css +153 -22
- package/dist/styles/toggle.css +59 -0
- package/dist/tokens/base.css +5 -0
- package/dist/tokens/components/activity.css +64 -0
- package/dist/tokens/components/badge.css +7 -0
- package/dist/tokens/components/card.css +14 -10
- package/dist/tokens/components/chart.css +1 -1
- package/dist/tokens/components/control.css +358 -8
- package/dist/tokens/components/data-display.css +75 -0
- package/dist/tokens/components/data-entry.css +25 -0
- package/dist/tokens/components/descriptions.css +22 -0
- package/dist/tokens/components/feedback.css +143 -4
- package/dist/tokens/components/form.css +16 -0
- package/dist/tokens/components/legal-document.css +5 -1
- package/dist/tokens/components/navigation.css +109 -0
- package/dist/tokens/components/scroll-area.css +21 -0
- package/dist/tokens/components/separator.css +52 -0
- package/dist/tokens/components/shell.css +132 -20
- package/dist/tokens/components/table.css +52 -3
- package/dist/tokens/components/toggle.css +82 -0
- package/dist/tokens/components/upload.css +107 -0
- package/dist/tokens/foundation.css +171 -10
- package/dist/tokens/semantic/layout.css +120 -1
- package/package.json +85 -68
- package/scripts/ui-audit.mjs +51 -2
|
@@ -1,21 +1,52 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
|
-
const Separator = React.forwardRef(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"data-slot": "separator",
|
|
11
|
-
"data-orientation": orientation,
|
|
12
|
-
orientation,
|
|
6
|
+
const Separator = React.forwardRef(
|
|
7
|
+
({
|
|
8
|
+
className,
|
|
9
|
+
orientation = "horizontal",
|
|
13
10
|
decorative,
|
|
14
|
-
|
|
11
|
+
label,
|
|
12
|
+
labelAlign = "center",
|
|
13
|
+
tone = "default",
|
|
15
14
|
...props
|
|
15
|
+
}, ref) => {
|
|
16
|
+
const hasLabel = typeof label === "string" && label.trim() !== "";
|
|
17
|
+
const labelled = hasLabel && orientation === "horizontal";
|
|
18
|
+
if (typeof process !== "undefined" && process.env?.NODE_ENV !== "production") {
|
|
19
|
+
if (hasLabel && orientation === "vertical") {
|
|
20
|
+
console.warn(
|
|
21
|
+
'Separator: `label` is rendered on orientation="horizontal" only; it was ignored. Use a horizontal rule for a day divider / unread watermark.'
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const isDecorative = decorative ?? !labelled;
|
|
26
|
+
return /* @__PURE__ */ jsx(
|
|
27
|
+
SeparatorPrimitive.Root,
|
|
28
|
+
{
|
|
29
|
+
ref,
|
|
30
|
+
"data-slot": "separator",
|
|
31
|
+
"data-orientation": orientation,
|
|
32
|
+
"data-tone": tone,
|
|
33
|
+
"data-labelled": labelled ? "" : void 0,
|
|
34
|
+
"data-label-align": labelled ? labelAlign : void 0,
|
|
35
|
+
orientation,
|
|
36
|
+
decorative: isDecorative,
|
|
37
|
+
"aria-label": labelled && !isDecorative ? label : void 0,
|
|
38
|
+
className: cn("ui-separator", className),
|
|
39
|
+
...props,
|
|
40
|
+
children: labelled ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
41
|
+
/* @__PURE__ */ jsx("span", { className: "ui-separator-rule", "aria-hidden": "true" }),
|
|
42
|
+
/* @__PURE__ */ jsx("span", { className: "ui-separator-label", "aria-hidden": isDecorative ? void 0 : true, children: label }),
|
|
43
|
+
/* @__PURE__ */ jsx("span", { className: "ui-separator-rule", "aria-hidden": "true" })
|
|
44
|
+
] }) : null
|
|
45
|
+
}
|
|
46
|
+
);
|
|
16
47
|
}
|
|
17
|
-
)
|
|
18
|
-
Separator.displayName =
|
|
48
|
+
);
|
|
49
|
+
Separator.displayName = "Separator";
|
|
19
50
|
export {
|
|
20
51
|
Separator
|
|
21
52
|
};
|
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
|
3
|
-
import { type
|
|
4
|
-
type ToggleGroupVariant =
|
|
5
|
-
type ToggleGroupSize =
|
|
6
|
-
export declare const ToggleGroup: React.ForwardRefExoticComponent<((Omit<ToggleGroupPrimitive.
|
|
3
|
+
import { type ToggleCountFields, type ToggleProp } from "./toggle.js";
|
|
4
|
+
type ToggleGroupVariant = ToggleProp["variant"];
|
|
5
|
+
type ToggleGroupSize = ToggleProp["size"];
|
|
6
|
+
export declare const ToggleGroup: React.ForwardRefExoticComponent<((Omit<ToggleGroupPrimitive.ToggleGroupMultipleProps & React.RefAttributes<HTMLDivElement>, "ref"> | Omit<ToggleGroupPrimitive.ToggleGroupSingleProps & React.RefAttributes<HTMLDivElement>, "ref">) & {
|
|
7
7
|
variant?: ToggleGroupVariant;
|
|
8
8
|
size?: ToggleGroupSize;
|
|
9
9
|
}) & React.RefAttributes<HTMLDivElement>>;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* `variant`/`size` come from the group through context; the COUNT stays per item, because the
|
|
12
|
+
* number is per-item data (gh#312). Same vocabulary as `Toggle` and `Button` — one counter pill
|
|
13
|
+
* for the whole library.
|
|
14
|
+
*/
|
|
15
|
+
export type ToggleGroupItemProp = React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> & ToggleCountFields & {
|
|
16
|
+
variant?: ToggleGroupVariant;
|
|
17
|
+
size?: ToggleGroupSize;
|
|
18
|
+
};
|
|
19
|
+
export type ToggleGroupItemProps = ToggleGroupItemProp;
|
|
20
|
+
export declare const ToggleGroupItem: React.ForwardRefExoticComponent<Omit<ToggleGroupPrimitive.ToggleGroupItemProps & React.RefAttributes<HTMLButtonElement>, "ref"> & ToggleCountFields & {
|
|
11
21
|
variant?: ToggleGroupVariant;
|
|
12
22
|
size?: ToggleGroupSize;
|
|
13
23
|
} & React.RefAttributes<HTMLButtonElement>>;
|
|
@@ -1,9 +1,9 @@
|
|
|
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 * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
|
-
import { toggleVariants } from "./toggle.js";
|
|
6
|
+
import { toggleVariants, useCounterPill } from "./toggle.js";
|
|
7
7
|
const ToggleGroupContext = React.createContext({});
|
|
8
8
|
const ToggleGroup = React.forwardRef(({ className, variant, size, children, ...props }, ref) => {
|
|
9
9
|
const context = React.useMemo(() => ({ variant, size }), [variant, size]);
|
|
@@ -21,22 +21,47 @@ const ToggleGroup = React.forwardRef(({ className, variant, size, children, ...p
|
|
|
21
21
|
);
|
|
22
22
|
});
|
|
23
23
|
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
|
|
24
|
-
const ToggleGroupItem = React.forwardRef(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
24
|
+
const ToggleGroupItem = React.forwardRef(
|
|
25
|
+
({
|
|
26
|
+
className,
|
|
27
|
+
variant,
|
|
28
|
+
size,
|
|
29
|
+
count,
|
|
30
|
+
overflowCount,
|
|
31
|
+
showZero,
|
|
32
|
+
countLabel,
|
|
33
|
+
children,
|
|
34
|
+
"aria-label": ariaLabel,
|
|
35
|
+
...props
|
|
36
|
+
}, ref) => {
|
|
37
|
+
const context = React.useContext(ToggleGroupContext);
|
|
38
|
+
const resolvedVariant = variant ?? context.variant;
|
|
39
|
+
const resolvedSize = size ?? context.size;
|
|
40
|
+
const { pill, resolvedAriaLabel } = useCounterPill({
|
|
41
|
+
count,
|
|
42
|
+
overflowCount,
|
|
43
|
+
showZero,
|
|
44
|
+
countLabel,
|
|
45
|
+
ariaLabel
|
|
46
|
+
});
|
|
47
|
+
return /* @__PURE__ */ jsxs(
|
|
48
|
+
ToggleGroupPrimitive.Item,
|
|
49
|
+
{
|
|
50
|
+
ref,
|
|
51
|
+
"data-slot": "toggle-group-item",
|
|
52
|
+
"data-variant": resolvedVariant,
|
|
53
|
+
"data-size": resolvedSize,
|
|
54
|
+
"aria-label": resolvedAriaLabel,
|
|
55
|
+
className: cn(toggleVariants({ variant: resolvedVariant, size: resolvedSize }), className),
|
|
56
|
+
...props,
|
|
57
|
+
children: [
|
|
58
|
+
children,
|
|
59
|
+
pill
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
);
|
|
40
65
|
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
|
|
41
66
|
export {
|
|
42
67
|
ToggleGroup,
|
|
@@ -3,11 +3,72 @@ import * as TogglePrimitive from "@radix-ui/react-toggle";
|
|
|
3
3
|
import { type VariantProps } from "class-variance-authority";
|
|
4
4
|
declare const toggleVariants: (props?: ({
|
|
5
5
|
variant?: "default" | "outline" | null | undefined;
|
|
6
|
-
size?: "
|
|
6
|
+
size?: "lg" | "md" | "sm" | null | undefined;
|
|
7
7
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* The counter-pill vocabulary, lifted VERBATIM from `Button` (`count` / `overflowCount` /
|
|
10
|
+
* `showZero`) so a counted filter tab and a counted, pressed filter chip are one vocabulary rather
|
|
11
|
+
* than two that drift (gh#312). Not exported on its own — it is folded into `ToggleProp` and
|
|
12
|
+
* `ToggleGroupItemProp` so there is one public name per component.
|
|
13
|
+
*/
|
|
14
|
+
type ToggleCountFields = {
|
|
15
|
+
/**
|
|
16
|
+
* Optional numeric count rendered as a borderless counter pill after the label — the SAME
|
|
17
|
+
* vocabulary `Button` already defines, so a counted segmented toggle and a counted filter tab
|
|
18
|
+
* read identically. Formatted with `Intl.NumberFormat` in the active locale (never `String(n)`,
|
|
19
|
+
* never a hand-rolled thousands separator). Never nest a `Badge` inside a Toggle for this: a
|
|
20
|
+
* Badge is a status chip with its own surface, so it double-borders the chip and puts two boxes
|
|
21
|
+
* where there is one control. The pill takes its colour from the toggle's OWN pressed state, so
|
|
22
|
+
* pressed and unpressed chips differ without reading the number.
|
|
23
|
+
*/
|
|
24
|
+
count?: number;
|
|
25
|
+
/** Cap for `count` — beyond it the pill shows `{overflowCount}+` (e.g. `99+`). Default `99`. */
|
|
26
|
+
overflowCount?: number;
|
|
27
|
+
/** Render the pill when `count` is 0. Default `true`, matching Button. */
|
|
28
|
+
showZero?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Localized description of what the count MEANS, folded into the accessible name so the control
|
|
31
|
+
* never announces as a bare number ("👍 3" → "thumbs up, 3 reactions"). Supply it whenever the
|
|
32
|
+
* visible label is an icon or an emoji; with a text label the label itself already carries the
|
|
33
|
+
* meaning. Pass a `t()`-resolved string — the library does not own this wording.
|
|
34
|
+
*/
|
|
35
|
+
countLabel?: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Counter pill shared by `Toggle` and `ToggleGroupItem`.
|
|
39
|
+
*
|
|
40
|
+
* ACCESSIBLE NAME. The digits live INSIDE the same `button[aria-pressed]` — one tab stop, one
|
|
41
|
+
* focus ring, one name. The name always comes out as "<label>, <count> <unit>" ("Unread, 12",
|
|
42
|
+
* "Thích, 3 lượt"), by one of two routes:
|
|
43
|
+
*
|
|
44
|
+
* • Name from CONTENTS (the usual text chip). The visible digits are `aria-hidden`, and an
|
|
45
|
+
* `sr-only` sibling carries ", 12 unit". The separator is explicit on purpose: the accessible
|
|
46
|
+
* name is a CONCATENATION of descendant text, and whether a browser inserts a space between
|
|
47
|
+
* two inline boxes is a layout detail no test can rely on — without it the control can
|
|
48
|
+
* announce "Unread12".
|
|
49
|
+
* • Name from `aria-label` (an icon or emoji chip). Contents are ignored for the name, so the
|
|
50
|
+
* same ", 12 unit" tail is folded into the label instead.
|
|
51
|
+
*
|
|
52
|
+
* Either way the count is announced exactly ONCE — the visible digits are hidden from the a11y
|
|
53
|
+
* tree precisely so they cannot be counted twice — and there is deliberately no `aria-live`: a
|
|
54
|
+
* count that ticks up as other people react is not this control's status, and a live region here
|
|
55
|
+
* would fire on every socket message. A product that wants the change announced owns its own
|
|
56
|
+
* live region.
|
|
57
|
+
*/
|
|
58
|
+
declare function useCounterPill({ count, overflowCount, showZero, countLabel, ariaLabel, }: ToggleCountFields & {
|
|
59
|
+
ariaLabel?: string;
|
|
60
|
+
}): {
|
|
61
|
+
pill: null;
|
|
62
|
+
resolvedAriaLabel: string | undefined;
|
|
63
|
+
} | {
|
|
64
|
+
pill: React.JSX.Element;
|
|
65
|
+
resolvedAriaLabel: string | undefined;
|
|
66
|
+
};
|
|
67
|
+
export type ToggleProp = React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> & VariantProps<typeof toggleVariants> & ToggleCountFields;
|
|
68
|
+
export type ToggleProps = ToggleProp;
|
|
9
69
|
export declare const Toggle: React.ForwardRefExoticComponent<Omit<TogglePrimitive.ToggleProps & React.RefAttributes<HTMLButtonElement>, "ref"> & VariantProps<(props?: ({
|
|
10
70
|
variant?: "default" | "outline" | null | undefined;
|
|
11
|
-
size?: "
|
|
12
|
-
} & import("class-variance-authority/types").ClassProp) | undefined) => string> & React.RefAttributes<HTMLButtonElement>>;
|
|
13
|
-
export { toggleVariants };
|
|
71
|
+
size?: "lg" | "md" | "sm" | null | undefined;
|
|
72
|
+
} & import("class-variance-authority/types").ClassProp) | undefined) => string> & ToggleCountFields & React.RefAttributes<HTMLButtonElement>>;
|
|
73
|
+
export { toggleVariants, useCounterPill };
|
|
74
|
+
export type { ToggleCountFields };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import * as TogglePrimitive from "@radix-ui/react-toggle";
|
|
5
5
|
import { cva } from "class-variance-authority";
|
|
6
6
|
import { cn } from "../../lib/utils.js";
|
|
7
|
+
import { useTranslation } from "../../i18n/use-translation.js";
|
|
7
8
|
const toggleVariants = cva("ui-toggle", {
|
|
8
9
|
variants: {
|
|
9
10
|
variant: {
|
|
@@ -21,17 +22,71 @@ const toggleVariants = cva("ui-toggle", {
|
|
|
21
22
|
size: "md"
|
|
22
23
|
}
|
|
23
24
|
});
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
function useCounterPill({
|
|
26
|
+
count,
|
|
27
|
+
overflowCount = 99,
|
|
28
|
+
showZero = true,
|
|
29
|
+
countLabel,
|
|
30
|
+
ariaLabel
|
|
31
|
+
}) {
|
|
32
|
+
const { locale } = useTranslation();
|
|
33
|
+
const visible = count != null && (count !== 0 || showZero);
|
|
34
|
+
const formatted = React.useMemo(() => {
|
|
35
|
+
if (count == null) return "";
|
|
36
|
+
const format = new Intl.NumberFormat(locale);
|
|
37
|
+
return count > overflowCount ? `${format.format(overflowCount)}+` : format.format(count);
|
|
38
|
+
}, [count, locale, overflowCount]);
|
|
39
|
+
if (!visible) {
|
|
40
|
+
return { pill: null, resolvedAriaLabel: ariaLabel };
|
|
41
|
+
}
|
|
42
|
+
const spoken = countLabel ? `${formatted} ${countLabel}` : formatted;
|
|
43
|
+
return {
|
|
44
|
+
pill: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
45
|
+
/* @__PURE__ */ jsx("span", { "data-slot": "toggle-count", className: "ui-toggle-count", "aria-hidden": "true", children: formatted }),
|
|
46
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: `, ${spoken}` })
|
|
47
|
+
] }),
|
|
48
|
+
resolvedAriaLabel: ariaLabel == null ? ariaLabel : `${ariaLabel}, ${spoken}`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const Toggle = React.forwardRef(
|
|
52
|
+
({
|
|
53
|
+
className,
|
|
54
|
+
variant,
|
|
55
|
+
size,
|
|
56
|
+
count,
|
|
57
|
+
overflowCount,
|
|
58
|
+
showZero,
|
|
59
|
+
countLabel,
|
|
60
|
+
children,
|
|
61
|
+
"aria-label": ariaLabel,
|
|
30
62
|
...props
|
|
63
|
+
}, ref) => {
|
|
64
|
+
const { pill, resolvedAriaLabel } = useCounterPill({
|
|
65
|
+
count,
|
|
66
|
+
overflowCount,
|
|
67
|
+
showZero,
|
|
68
|
+
countLabel,
|
|
69
|
+
ariaLabel
|
|
70
|
+
});
|
|
71
|
+
return /* @__PURE__ */ jsxs(
|
|
72
|
+
TogglePrimitive.Root,
|
|
73
|
+
{
|
|
74
|
+
ref,
|
|
75
|
+
"data-slot": "toggle",
|
|
76
|
+
"aria-label": resolvedAriaLabel,
|
|
77
|
+
className: cn(toggleVariants({ variant, size }), className),
|
|
78
|
+
...props,
|
|
79
|
+
children: [
|
|
80
|
+
children,
|
|
81
|
+
pill
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
);
|
|
31
85
|
}
|
|
32
|
-
)
|
|
86
|
+
);
|
|
33
87
|
Toggle.displayName = TogglePrimitive.Root.displayName;
|
|
34
88
|
export {
|
|
35
89
|
Toggle,
|
|
36
|
-
toggleVariants
|
|
90
|
+
toggleVariants,
|
|
91
|
+
useCounterPill
|
|
37
92
|
};
|
|
@@ -213,6 +213,14 @@
|
|
|
213
213
|
}
|
|
214
214
|
},
|
|
215
215
|
"dataDisplay": {
|
|
216
|
+
"avatar": {
|
|
217
|
+
"presence": {
|
|
218
|
+
"online": "Online",
|
|
219
|
+
"away": "Away",
|
|
220
|
+
"busy": "Busy",
|
|
221
|
+
"offline": "Offline"
|
|
222
|
+
}
|
|
223
|
+
},
|
|
216
224
|
"carousel": {
|
|
217
225
|
"roleDescription": "carousel",
|
|
218
226
|
"ariaLabel": "Carousel",
|
|
@@ -207,6 +207,14 @@
|
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
209
|
"dataDisplay": {
|
|
210
|
+
"avatar": {
|
|
211
|
+
"presence": {
|
|
212
|
+
"online": "オンライン",
|
|
213
|
+
"away": "離席中",
|
|
214
|
+
"busy": "取り込み中",
|
|
215
|
+
"offline": "オフライン"
|
|
216
|
+
}
|
|
217
|
+
},
|
|
210
218
|
"carousel": {
|
|
211
219
|
"roleDescription": "カルーセル",
|
|
212
220
|
"ariaLabel": "カルーセル",
|
|
@@ -207,6 +207,14 @@
|
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
209
|
"dataDisplay": {
|
|
210
|
+
"avatar": {
|
|
211
|
+
"presence": {
|
|
212
|
+
"online": "Trực tuyến",
|
|
213
|
+
"away": "Vắng mặt",
|
|
214
|
+
"busy": "Bận",
|
|
215
|
+
"offline": "Ngoại tuyến"
|
|
216
|
+
}
|
|
217
|
+
},
|
|
210
218
|
"carousel": {
|
|
211
219
|
"roleDescription": "băng chuyền",
|
|
212
220
|
"ariaLabel": "Băng chuyền",
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
/** Shared control sizing — reads `--control-height`, `--font-size-*` from density / theme. */
|
|
2
2
|
export declare const controlFieldClass = "ui-control w-full rounded-[var(--control-radius)] border border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:opacity-50";
|
|
3
3
|
export declare const controlMultilineClass = "ui-control-multiline w-full rounded-[var(--control-radius)] border border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:opacity-50";
|
|
4
|
+
/**
|
|
5
|
+
* Multiline control with its own chrome removed, for a textarea EMBEDDED in a surface that
|
|
6
|
+
* already draws the box — a chat composer inside a Card, an inline edit cell, a comment box in
|
|
7
|
+
* a panel. Two nested rounded borders is the tell that this was missing.
|
|
8
|
+
*
|
|
9
|
+
* These are utilities, not tokens, ON PURPOSE. `.ui-control-multiline` reads
|
|
10
|
+
* `--control-border-width` / `--control-shadow` from `@layer components`, but the default class
|
|
11
|
+
* ALSO carries Tailwind's `border` in `@layer utilities`, which wins whatever the token says —
|
|
12
|
+
* the same structural inertness gh#260 found on Badge's font size. So the variant cannot be a
|
|
13
|
+
* token override by a consumer; it has to drop the utilities here.
|
|
14
|
+
*
|
|
15
|
+
* The focus ring goes with them: the surface owns focus, via `focus-within` on the wrapper.
|
|
16
|
+
*/
|
|
17
|
+
export declare const controlMultilineGhostClass = "ui-control-multiline w-full min-h-0 border-0 bg-transparent shadow-none placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50";
|
|
4
18
|
/**
|
|
5
19
|
* Open-state ring for popup-style triggers (Select / Cascader / TreeSelect / SearchSelect).
|
|
6
20
|
* Radix moves focus INTO the popup on open, so the trigger loses `:focus-visible` and — without
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const controlFieldClass = "ui-control w-full rounded-[var(--control-radius)] border border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:opacity-50";
|
|
2
2
|
const controlMultilineClass = "ui-control-multiline w-full rounded-[var(--control-radius)] border border-input bg-background ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/25 disabled:cursor-not-allowed disabled:opacity-50";
|
|
3
|
+
const controlMultilineGhostClass = "ui-control-multiline w-full min-h-0 border-0 bg-transparent shadow-none placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0 disabled:cursor-not-allowed disabled:opacity-50";
|
|
3
4
|
const controlOpenRingClass = "data-[state=open]:border-ring data-[state=open]:ring-ring/50 data-[state=open]:ring-[3px]";
|
|
4
5
|
const controlTriggerClass = "ui-control flex w-full items-center justify-between gap-2 whitespace-nowrap rounded-[var(--control-radius)] border border-input bg-background shadow-sm transition-[color,box-shadow] focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/25 data-[state=open]:border-ring data-[state=open]:ring-ring/50 data-[state=open]:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1";
|
|
5
6
|
const controlIconClass = "size-[length:var(--control-height)] shrink-0";
|
|
@@ -21,6 +22,7 @@ export {
|
|
|
21
22
|
controlIconLeadingClass,
|
|
22
23
|
controlIconSmClass,
|
|
23
24
|
controlMultilineClass,
|
|
25
|
+
controlMultilineGhostClass,
|
|
24
26
|
controlOpenRingClass,
|
|
25
27
|
controlTriggerClass,
|
|
26
28
|
tableCellPaddingClass,
|
package/dist/lib/field-a11y.d.ts
CHANGED
|
@@ -13,6 +13,12 @@ import * as React from "react";
|
|
|
13
13
|
* - `aria-errormessage` + `aria-invalid` — the validation message, announced when invalid.
|
|
14
14
|
* - `aria-required` — required-field semantics.
|
|
15
15
|
* - `aria-label` — a name supplied directly (used when there is no visible label element).
|
|
16
|
+
*
|
|
17
|
+
* `data-field` travels the SAME route (gh#337) and is therefore part of this bag even though it is
|
|
18
|
+
* not an ARIA attribute. It is the field's stable machine key — the `data-testid` role, standardised
|
|
19
|
+
* on one attribute so a control never has to be found by a generated id or by its visible Japanese
|
|
20
|
+
* label. It is inert (read-only metadata) and must land on the same semantic focus target the aria
|
|
21
|
+
* relationships do; routing it through a second, parallel mechanism is how a control gets missed.
|
|
16
22
|
*/
|
|
17
23
|
export interface FieldA11yProps {
|
|
18
24
|
"aria-label"?: string;
|
|
@@ -21,6 +27,8 @@ export interface FieldA11yProps {
|
|
|
21
27
|
"aria-errormessage"?: string;
|
|
22
28
|
"aria-invalid"?: React.AriaAttributes["aria-invalid"];
|
|
23
29
|
"aria-required"?: React.AriaAttributes["aria-required"];
|
|
30
|
+
/** Stable machine key of the field (gh#337) — see the interface doc above. */
|
|
31
|
+
"data-field"?: string;
|
|
24
32
|
}
|
|
25
33
|
/**
|
|
26
34
|
* Merge one or more space-separated id-reference lists (`aria-describedby`, `aria-labelledby`,
|
|
@@ -81,6 +89,49 @@ export declare const FieldNameContext: React.Context<FieldNameContextValue | nul
|
|
|
81
89
|
* inside a FormField AND still nameless after its own props and the cloned contract are applied.
|
|
82
90
|
*/
|
|
83
91
|
export declare function useFieldNameFallback(name: Pick<FieldA11yProps, "aria-label" | "aria-labelledby">): Pick<FieldA11yProps, "aria-label" | "aria-labelledby">;
|
|
92
|
+
/**
|
|
93
|
+
* The enclosing `FormField`, made reachable by NESTED controls (gh#337).
|
|
94
|
+
*
|
|
95
|
+
* `cloneElement` reaches FormField's single DIRECT child only. In the real screens this library
|
|
96
|
+
* serves, 322 of 1,410 controls (23%) sit one level deeper — the direct child is a `Flex` holding a
|
|
97
|
+
* from/to pair, a 年/月 combo, or a value + 「不明」 checkbox — so the machine key stopped on the
|
|
98
|
+
* wrapper `div` and the controls inside stayed anonymous. The customer's acceptance condition is
|
|
99
|
+
* 「画面に見えている入力欄の100%に付与されていること」; 77% does not pass.
|
|
100
|
+
*
|
|
101
|
+
* The key for such a control is its OWN `id`, and that is a finding, not a convention invented
|
|
102
|
+
* here: of those 322, **250 already carry a static id** and the ambiguous case the wrapper creates
|
|
103
|
+
* — two controls under one field — is already distinguished at the call site
|
|
104
|
+
* (`search_billing_date_from` / `..._to`, `fax` / `fax_unknown`). So a nested control names itself
|
|
105
|
+
* and no two controls can ever end up sharing a key. The remaining 72 (48 computed ids, 24 with no
|
|
106
|
+
* id at all) get NOTHING: a fabricated key is worse than a missing one, because automation would
|
|
107
|
+
* bind to it and break silently on the next render.
|
|
108
|
+
*/
|
|
109
|
+
export interface FieldIdentityContextValue {
|
|
110
|
+
/** Whether the app opted into a native `name` (@see AppProviderProp.emitFieldNames). */
|
|
111
|
+
emitName: boolean;
|
|
112
|
+
}
|
|
113
|
+
export declare const FieldIdentityContext: React.Context<FieldIdentityContextValue | null>;
|
|
114
|
+
/**
|
|
115
|
+
* Resolve `data-field` / `name` for a control nested under a `FormField` (see
|
|
116
|
+
* {@link FieldIdentityContext}). Returns `{}` — adding nothing — in every case but the one it
|
|
117
|
+
* exists for, which is what keeps it safe to call from every control:
|
|
118
|
+
*
|
|
119
|
+
* - **outside a FormField** → `{}`. A control elsewhere on the page is untouched.
|
|
120
|
+
* - **`data-field` already present** → `{}`. Either `FormField` cloned it onto its direct child, or
|
|
121
|
+
* a composite (DatePicker, Select) already resolved this field and is passing it down. Whoever
|
|
122
|
+
* owns the field owns BOTH attributes, so `name` is not second-guessed here either — that is what
|
|
123
|
+
* keeps DatePicker's ISO mirror (`name` belongs to the hidden `yyyy-MM-dd` input, not the visible
|
|
124
|
+
* `yyyy/MM/dd` one) and Select's native `<select>` intact.
|
|
125
|
+
* - **no `id` of its own** → `{}`. Nothing to derive a stable key from; see the interface doc.
|
|
126
|
+
*/
|
|
127
|
+
export declare function useFieldIdentity(own: {
|
|
128
|
+
id?: string;
|
|
129
|
+
name?: string;
|
|
130
|
+
"data-field"?: string;
|
|
131
|
+
}): {
|
|
132
|
+
"data-field"?: string;
|
|
133
|
+
name?: string;
|
|
134
|
+
};
|
|
84
135
|
/**
|
|
85
136
|
* The field-a11y attributes valid on a **group container** (`role="group"`), used by composite
|
|
86
137
|
* controls that have no single labelable focus target — CheckboxGroup, and the two-input range
|
|
@@ -98,4 +149,5 @@ export declare function useFieldNameFallback(name: Pick<FieldA11yProps, "aria-la
|
|
|
98
149
|
export declare function pickGroupFieldA11y(props: FieldA11yProps): {
|
|
99
150
|
"aria-labelledby"?: string;
|
|
100
151
|
"aria-describedby"?: string;
|
|
152
|
+
"data-field"?: string;
|
|
101
153
|
};
|
package/dist/lib/field-a11y.js
CHANGED
|
@@ -6,7 +6,8 @@ const FIELD_A11Y_KEYS = [
|
|
|
6
6
|
"aria-describedby",
|
|
7
7
|
"aria-errormessage",
|
|
8
8
|
"aria-invalid",
|
|
9
|
-
"aria-required"
|
|
9
|
+
"aria-required",
|
|
10
|
+
"data-field"
|
|
10
11
|
];
|
|
11
12
|
function mergeAriaIds(...values) {
|
|
12
13
|
return Array.from(new Set(values.flatMap((value) => value?.split(/\s+/).filter(Boolean) ?? []))).join(
|
|
@@ -47,19 +48,33 @@ function useFieldNameFallback(name) {
|
|
|
47
48
|
...field.label !== void 0 ? { "aria-label": field.label } : {}
|
|
48
49
|
};
|
|
49
50
|
}
|
|
51
|
+
const FieldIdentityContext = React.createContext(null);
|
|
52
|
+
function useFieldIdentity(own) {
|
|
53
|
+
const ctx = React.useContext(FieldIdentityContext);
|
|
54
|
+
if (!ctx || own["data-field"] !== void 0 || own.id === void 0) return {};
|
|
55
|
+
return {
|
|
56
|
+
"data-field": own.id,
|
|
57
|
+
...ctx.emitName && own.name === void 0 ? { name: own.id } : {}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
50
60
|
function pickGroupFieldA11y(props) {
|
|
51
61
|
const describedBy = mergeAriaIds(props["aria-describedby"], props["aria-errormessage"]);
|
|
52
62
|
return {
|
|
53
63
|
...props["aria-labelledby"] !== void 0 ? { "aria-labelledby": props["aria-labelledby"] } : {},
|
|
54
|
-
...describedBy !== void 0 ? { "aria-describedby": describedBy } : {}
|
|
64
|
+
...describedBy !== void 0 ? { "aria-describedby": describedBy } : {},
|
|
65
|
+
// Not an ARIA attribute and so not subject to the role="group" restriction above — a group
|
|
66
|
+
// still has to be findable by its field key (gh#337).
|
|
67
|
+
...props["data-field"] !== void 0 ? { "data-field": props["data-field"] } : {}
|
|
55
68
|
};
|
|
56
69
|
}
|
|
57
70
|
export {
|
|
71
|
+
FieldIdentityContext,
|
|
58
72
|
FieldNameContext,
|
|
59
73
|
mergeAriaIds,
|
|
60
74
|
omitFieldA11y,
|
|
61
75
|
pickFieldA11y,
|
|
62
76
|
pickGroupFieldA11y,
|
|
63
77
|
resolveFieldA11y,
|
|
78
|
+
useFieldIdentity,
|
|
64
79
|
useFieldNameFallback
|
|
65
80
|
};
|
|
@@ -49,6 +49,19 @@ export type AppProviderProp = {
|
|
|
49
49
|
* number (e.g. `0.95`) overrides it. Type is a separate axis — not scaled.
|
|
50
50
|
*/
|
|
51
51
|
scaling?: number | null;
|
|
52
|
+
/**
|
|
53
|
+
* Emit a native `name` on every control a `FormField` wraps, taken from the field's key
|
|
54
|
+
* (`field` → `name` → `id`). Default `false`, and deliberately opt-in (gh#337).
|
|
55
|
+
*
|
|
56
|
+
* `data-field` is inert metadata and is always emitted; `name` is NOT — it changes what a
|
|
57
|
+
* native `<form>` submit sends. An app whose controls have never carried a `name` would start
|
|
58
|
+
* posting extra keys to its backend the moment it upgraded the library. That is a behaviour
|
|
59
|
+
* change no shared package may make by default, so the app that WANTS it (a screen-automation /
|
|
60
|
+
* RPA contract, native form posts) turns it on here, once, and every FormField in it obeys.
|
|
61
|
+
*
|
|
62
|
+
* A `name` written on the control itself always wins over the injected one.
|
|
63
|
+
*/
|
|
64
|
+
emitFieldNames?: boolean;
|
|
52
65
|
onLocaleChange?: (locale: AppLocale) => void;
|
|
53
66
|
onTimezoneChange?: (timezone: AppTimezone) => void;
|
|
54
67
|
onTimeFormatChange?: (timeFormat: AppTimeFormat) => void;
|
|
@@ -117,6 +130,8 @@ export type AppContextValue = {
|
|
|
117
130
|
density: AppDensity;
|
|
118
131
|
fontSize: AppFontSize;
|
|
119
132
|
scaling: number | null;
|
|
133
|
+
/** Whether `FormField` injects a native `name` onto its control (gh#337). Default `false`. */
|
|
134
|
+
emitFieldNames: boolean;
|
|
120
135
|
setLocale: (locale: AppLocale) => void;
|
|
121
136
|
setTimezone: (timezone: AppTimezone) => void;
|
|
122
137
|
setTimeFormat: (timeFormat: AppTimeFormat) => void;
|