@godxjp/ui 27.9.0 → 27.11.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.
@@ -4,4 +4,4 @@ export type { UploadProp, UploadProp as UploadProps, UploadFileItemProp, UploadV
4
4
  export type { UploadFileItem, UploadVariant, UploadCommitAction } from "./upload-types.js";
5
5
  export { collectUploadCommitActions, createUploadItem, UPLOAD_LIST_IGNORE } from "./upload-types.js";
6
6
  export { useUploadDraft } from "./use-upload-draft.js";
7
- export declare function Upload({ variant, listType, triggerSize, triggerVariant, value, defaultValue, onValueChange, accept: acceptProp, multiple: multipleProp, maxCount: maxCountProp, maxSizeBytes, disabled: disabledProp, readOnly, directory, pastable, openFileDialogOnClick, name, action, method, headers, data, withCredentials, beforeUpload, onReject, onRemove, onPreview, onDownload, previewFile, onDrop, showUploadList, itemRender, removable, onUpload, id, className, children, ...ariaProps }: UploadProp): React.JSX.Element;
7
+ export declare function Upload({ variant, listType, triggerSize, triggerVariant, triggerIcon: TriggerIcon, value, defaultValue, onValueChange, accept: acceptProp, multiple: multipleProp, maxCount: maxCountProp, maxSizeBytes, disabled: disabledProp, readOnly, directory, pastable, openFileDialogOnClick, name, action, method, headers, data, withCredentials, beforeUpload, onReject, onRemove, onPreview, onDownload, previewFile, onDrop, showUploadList, itemRender, removable, onUpload, id, className, children, ...ariaProps }: UploadProp): React.JSX.Element;
@@ -70,6 +70,7 @@ function Upload({
70
70
  listType,
71
71
  triggerSize,
72
72
  triggerVariant = "outline",
73
+ triggerIcon: TriggerIcon = UploadIcon,
73
74
  value,
74
75
  defaultValue,
75
76
  onValueChange,
@@ -498,7 +499,7 @@ function Upload({
498
499
  "aria-label": triggerAriaLabel,
499
500
  children: [
500
501
  /* @__PURE__ */ jsx(
501
- UploadIcon,
502
+ TriggerIcon,
502
503
  {
503
504
  className: "ui-upload-trigger-icon",
504
505
  "data-with-label": iconOnly ? void 0 : "",
@@ -21,5 +21,6 @@ export declare const Button: React.ForwardRefExoticComponent<React.ButtonHTMLAtt
21
21
  count?: number;
22
22
  overflowCount?: number;
23
23
  showZero?: boolean;
24
+ countLabel?: string;
24
25
  } & React.RefAttributes<HTMLButtonElement>>;
25
26
  export { buttonVariants };
@@ -71,6 +71,7 @@ const Button = React.forwardRef(
71
71
  count,
72
72
  overflowCount = 99,
73
73
  showZero = true,
74
+ countLabel,
74
75
  disabled,
75
76
  type,
76
77
  children,
@@ -84,8 +85,14 @@ const Button = React.forwardRef(
84
85
  loadingText ?? children
85
86
  ] }) : children;
86
87
  const showCount = !asChild && count != null && (count !== 0 || showZero);
87
- const countLabel = showCount && count != null && count > overflowCount ? `${numberFormat(locale).format(overflowCount)}+` : count != null ? numberFormat(locale).format(count) : "";
88
- const countNode = showCount ? /* @__PURE__ */ jsx("span", { "data-slot": "button-count", className: "ui-button-count", children: countLabel }) : null;
88
+ const countText = showCount && count != null && count > overflowCount ? `${numberFormat(locale).format(overflowCount)}+` : count != null ? numberFormat(locale).format(count) : "";
89
+ const spokenCount = countLabel ? `${countText} ${countLabel}` : countText;
90
+ const ariaLabel = props["aria-label"];
91
+ const foldedAriaLabel = showCount && ariaLabel != null ? { "aria-label": `${ariaLabel}, ${spokenCount}` } : null;
92
+ const countNode = showCount ? /* @__PURE__ */ jsxs(Fragment, { children: [
93
+ /* @__PURE__ */ jsx("span", { "data-slot": "button-count", className: "ui-button-count", "aria-hidden": "true", children: countText }),
94
+ /* @__PURE__ */ jsx("span", { className: "sr-only", children: `, ${spokenCount}` })
95
+ ] }) : null;
89
96
  return /* @__PURE__ */ jsx(
90
97
  Comp,
91
98
  {
@@ -109,6 +116,7 @@ const Button = React.forwardRef(
109
116
  ),
110
117
  ref,
111
118
  ...props,
119
+ ...foldedAriaLabel,
112
120
  children: asChild ? children : /* @__PURE__ */ jsxs(Fragment, { children: [
113
121
  content,
114
122
  countNode
@@ -1,5 +1,5 @@
1
1
  import type { PageContainerProp, PageInsetProp } from "../../props/components/layout.prop.js";
2
- export type { PageContainerProp, PageContainerProp as PageContainerProps, } from "../../props/components/layout.prop.js";
2
+ export type { PageContainerProp, PageContainerProp as PageContainerProps, PageContainerExtraProp, } from "../../props/components/layout.prop.js";
3
3
  export type { BreadcrumbItemProp, BreadcrumbItemProp as BreadcrumbItem, } from "../../props/vocabulary/navigation.prop.js";
4
4
  export declare function PageContainerInset({ className, children, ...props }: PageInsetProp): import("react").JSX.Element;
5
5
  declare function PageContainerRoot({ title, subtitle, status, extra, toolbar, footer, toolbarPad, footerPad, breadcrumb, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, headerLoading, density, variant, preset, headerLayout, headerScale, measure, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
- import { useEffect, useRef, useState } from "react";
3
+ import { isValidElement, useEffect, useRef, useState } from "react";
4
4
  import { ChevronRight } from "lucide-react";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { cn } from "../../lib/utils.js";
@@ -33,6 +33,12 @@ function useFooterReveal(enabled) {
33
33
  function PageContainerInset({ className, children, ...props }) {
34
34
  return /* @__PURE__ */ jsx("div", { className: cn("ui-page-container-inset", className), ...props, children });
35
35
  }
36
+ function resolvePageExtra(extra) {
37
+ if (extra === void 0 || extra === null || extra === false) return {};
38
+ const isSlotMap = typeof extra === "object" && !isValidElement(extra) && !Array.isArray(extra);
39
+ if (isSlotMap) return extra;
40
+ return { start: extra };
41
+ }
36
42
  function footerPadStyle(footerPad) {
37
43
  const style = padStyle(footerPad, void 0);
38
44
  const end = style?.paddingInlineEnd ?? style?.padding;
@@ -68,6 +74,7 @@ function PageContainerRoot({
68
74
  const reveal = stickyFooter && footer != null && footerReveal === "onScroll";
69
75
  const { headerRef, revealed } = useFooterReveal(reveal);
70
76
  const { t } = useTranslation();
77
+ const { start: extraStart, end: extraEnd } = resolvePageExtra(extra);
71
78
  return /* @__PURE__ */ jsxs(
72
79
  "div",
73
80
  {
@@ -149,7 +156,10 @@ function PageContainerRoot({
149
156
  )
150
157
  ) : subtitle && /* @__PURE__ */ jsx("p", { className: "ui-page-subtitle", children: subtitle })
151
158
  ] }),
152
- extra && /* @__PURE__ */ jsx("div", { className: "ui-page-header-extra", children: extra })
159
+ (extraStart != null || extraEnd != null) && /* @__PURE__ */ jsxs("div", { className: "ui-page-header-extra", children: [
160
+ extraStart,
161
+ extraEnd
162
+ ] })
153
163
  ] })
154
164
  ]
155
165
  }
@@ -2,13 +2,32 @@ import * as React from "react";
2
2
  import { type ToggleCountFields, type ToggleProp } from "./toggle.js";
3
3
  type ToggleGroupVariant = ToggleProp["variant"];
4
4
  type ToggleGroupSize = ToggleProp["size"];
5
+ type ToggleGroupShape = ToggleProp["shape"];
5
6
  type ToggleGroupBaseProp = Omit<React.ComponentPropsWithoutRef<"div">, "defaultValue" | "dir"> & {
6
7
  variant?: ToggleGroupVariant;
7
8
  size?: ToggleGroupSize;
9
+ shape?: ToggleGroupShape;
8
10
  /** Disable every item at once — RAC `isDisabled`. */
9
11
  disabled?: boolean;
10
12
  /** Which way the arrow keys walk the group. */
11
13
  orientation?: "horizontal" | "vertical";
14
+ /**
15
+ * Let the row break onto further lines instead of running past its rail (gh#741). Same name,
16
+ * same boolean shape and same `data-wrap` attribute as `Flex` — one spelling answers "what does
17
+ * this row do when it does not fit" across the library.
18
+ *
19
+ * OPT-IN (`false`), and deliberately the SAME default for every `variant`. `Flex`'s `wrap` is
20
+ * `false`, and one word must not mean two defaults. Measured in Chromium at a 320px rail:
21
+ * forcing `flex-wrap: wrap` on all 18 groups of the docs page left the 17 that FIT untouched
22
+ * (same line count, same height, at a 320px and a 1358px rail) — including every `soft` one, so
23
+ * paint is not the axis that decides. The only row it moves is the one that overflows, from
24
+ * 171.5px to 108px: a default of `true` reflows exactly those rows, silently, on an upgrade
25
+ * whose call sites did not change.
26
+ *
27
+ * Wrapping keeps the group's single `gap`, so the two axes stay equal (4px × 4px measured);
28
+ * `flex-wrap` reorders nothing, so arrow keys still walk the items in DOM order across lines.
29
+ */
30
+ wrap?: boolean;
12
31
  /**
13
32
  * Writing direction. It still lands on the DOM node, but React Aria reads the direction it
14
33
  * navigates by from the ambient locale (`I18nProvider`), NOT from this attribute — where Radix
@@ -51,6 +70,7 @@ export type ToggleGroupItemProp = Omit<React.ComponentPropsWithoutRef<"button">,
51
70
  value: string;
52
71
  variant?: ToggleGroupVariant;
53
72
  size?: ToggleGroupSize;
73
+ shape?: ToggleGroupShape;
54
74
  };
55
75
  export type ToggleGroupItemProps = ToggleGroupItemProp;
56
76
  export declare const ToggleGroupItem: React.ForwardRefExoticComponent<Omit<Omit<React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref">, "value"> & ToggleCountFields & {
@@ -61,5 +81,6 @@ export declare const ToggleGroupItem: React.ForwardRefExoticComponent<Omit<Omit<
61
81
  value: string;
62
82
  variant?: ToggleGroupVariant;
63
83
  size?: ToggleGroupSize;
84
+ shape?: ToggleGroupShape;
64
85
  } & React.RefAttributes<HTMLButtonElement>>;
65
86
  export {};
@@ -31,6 +31,8 @@ const ToggleGroup = React.forwardRef(
31
31
  className,
32
32
  variant,
33
33
  size,
34
+ shape,
35
+ wrap,
34
36
  children,
35
37
  type,
36
38
  value,
@@ -40,7 +42,7 @@ const ToggleGroup = React.forwardRef(
40
42
  loop: _loop,
41
43
  ...props
42
44
  }, ref) => {
43
- const context = React.useMemo(() => ({ variant, size }), [variant, size]);
45
+ const context = React.useMemo(() => ({ variant, size, shape }), [variant, size, shape]);
44
46
  const selectedKeys = React.useMemo(() => toSelectedKeys(value), [value]);
45
47
  const defaultSelectedKeys = React.useMemo(() => toSelectedKeys(defaultValue), [defaultValue]);
46
48
  const handleSelectionChange = (keys) => {
@@ -58,6 +60,8 @@ const ToggleGroup = React.forwardRef(
58
60
  "data-slot": "toggle-group",
59
61
  "data-variant": variant,
60
62
  "data-size": size,
63
+ "data-shape": shape,
64
+ "data-wrap": wrap ? "true" : void 0,
61
65
  className: cn("ui-toggle-group", className),
62
66
  ...props,
63
67
  selectionMode: type,
@@ -77,6 +81,7 @@ const ToggleGroupItem = React.forwardRef(
77
81
  className,
78
82
  variant,
79
83
  size,
84
+ shape,
80
85
  count,
81
86
  overflowCount,
82
87
  showZero,
@@ -90,6 +95,7 @@ const ToggleGroupItem = React.forwardRef(
90
95
  const context = React.useContext(ToggleGroupContext);
91
96
  const resolvedVariant = variant ?? context.variant;
92
97
  const resolvedSize = size ?? context.size;
98
+ const resolvedShape = shape ?? context.shape;
93
99
  const { pill, resolvedAriaLabel } = useCounterPill({
94
100
  count,
95
101
  overflowCount,
@@ -107,8 +113,12 @@ const ToggleGroupItem = React.forwardRef(
107
113
  "data-state": isPressed ? "on" : "off",
108
114
  "data-variant": resolvedVariant,
109
115
  "data-size": resolvedSize,
116
+ "data-shape": resolvedShape,
110
117
  "aria-label": resolvedAriaLabel,
111
- className: cn(toggleVariants({ variant: resolvedVariant, size: resolvedSize }), className),
118
+ className: cn(
119
+ toggleVariants({ variant: resolvedVariant, size: resolvedSize, shape: resolvedShape }),
120
+ className
121
+ ),
112
122
  ...props,
113
123
  id: value,
114
124
  isDisabled: disabled,
@@ -1,8 +1,9 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const toggleVariants: (props?: ({
4
- variant?: "default" | "outline" | null | undefined;
4
+ variant?: "default" | "outline" | "soft" | null | undefined;
5
5
  size?: "lg" | "md" | "sm" | "xs" | null | undefined;
6
+ shape?: "default" | "pill" | "sharp" | null | undefined;
6
7
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
8
  /**
8
9
  * Not exported on its own — it is folded into `ToggleProp` and `ToggleGroupItemProp` so there is
@@ -68,8 +69,9 @@ declare function restoreDomProps<P extends {
68
69
  export type ToggleProp = React.ComponentPropsWithoutRef<"button"> & TogglePressedFields & VariantProps<typeof toggleVariants> & ToggleCountFields;
69
70
  export type ToggleProps = ToggleProp;
70
71
  export declare const Toggle: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & TogglePressedFields & VariantProps<(props?: ({
71
- variant?: "default" | "outline" | null | undefined;
72
+ variant?: "default" | "outline" | "soft" | null | undefined;
72
73
  size?: "lg" | "md" | "sm" | "xs" | null | undefined;
74
+ shape?: "default" | "pill" | "sharp" | null | undefined;
73
75
  } & import("class-variance-authority/types").ClassProp) | undefined) => string> & ToggleCountFields & React.RefAttributes<HTMLButtonElement>>;
74
76
  export { toggleVariants, useCounterPill, restoreDomProps };
75
77
  export type { ToggleCountFields, TogglePressedFields };
@@ -10,7 +10,15 @@ const toggleVariants = cva("ui-toggle", {
10
10
  variants: {
11
11
  variant: {
12
12
  default: "ui-toggle-default",
13
- outline: "ui-toggle-outline"
13
+ outline: "ui-toggle-outline",
14
+ // The REST FILL (gh#734). `default` is a 1px TRANSPARENT border and `outline` a hairline on
15
+ // `--background`, so neither draws a chip that reads as filled before it is pressed — which
16
+ // is why a consumer building a tag-filter panel reported its tags "look transparent" and
17
+ // fell back to `Button variant="secondary" shape="pill" aria-pressed`, a control that paints
18
+ // no pressed state at all. `soft` is antd `Tag`'s resting surface, on the SAME
19
+ // `hsl(var(--secondary))` fill `Badge variant="secondary"` and `Button variant="secondary"`
20
+ // already paint, so a chip, a badge and a button on one row are one family.
21
+ soft: "ui-toggle-soft"
14
22
  },
15
23
  size: {
16
24
  // The fourth step of the shared control ladder (gh#716): 24px, `--control-height-xs`, the
@@ -20,11 +28,21 @@ const toggleVariants = cva("ui-toggle", {
20
28
  sm: "ui-toggle-sm",
21
29
  md: "ui-toggle-default-size",
22
30
  lg: "ui-toggle-lg"
31
+ },
32
+ // Corner shape at parity with `Button` and `Badge` — the same three values mapped to the same
33
+ // two radius tokens, so `shape="pill"` means one thing across the library. `default` is empty
34
+ // on purpose: it leaves `.ui-toggle`'s own `--radius-md` in place instead of restating it,
35
+ // exactly as `badgeVariants` does.
36
+ shape: {
37
+ default: "",
38
+ pill: "rounded-[var(--radius-pill)]",
39
+ sharp: "rounded-[var(--radius-sharp)]"
23
40
  }
24
41
  },
25
42
  defaultVariants: {
26
43
  variant: "default",
27
- size: "md"
44
+ size: "md",
45
+ shape: "default"
28
46
  }
29
47
  });
30
48
  function useCounterPill({
@@ -82,6 +100,7 @@ const Toggle = React.forwardRef(
82
100
  className,
83
101
  variant,
84
102
  size,
103
+ shape,
85
104
  count,
86
105
  overflowCount,
87
106
  showZero,
@@ -113,7 +132,7 @@ const Toggle = React.forwardRef(
113
132
  "data-slot": "toggle",
114
133
  "data-state": isPressed ? "on" : "off",
115
134
  "aria-label": resolvedAriaLabel,
116
- className: cn(toggleVariants({ variant, size }), className),
135
+ className: cn(toggleVariants({ variant, size, shape }), className),
117
136
  ...props,
118
137
  isSelected: isPressed,
119
138
  onChange: onSelectionChange,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$comment": "AUTO-GENERATED by scripts/gen-measurement-contract.mjs — do not edit. Read this instead of guessing: docs/MEASUREMENT-CONTRACT.md.",
3
- "version": "27.9.0",
3
+ "version": "27.11.0",
4
4
  "targetSize": {
5
5
  "standard": "WCAG 2.2 SC 2.5.8 Target Size (Minimum), level AA — 24×24 CSS px",
6
6
  "min": 24,
@@ -1546,6 +1546,22 @@ export type UploadProp = FieldA11yProps & {
1546
1546
  * @see Button — same variant scale
1547
1547
  */
1548
1548
  triggerVariant?: "default" | "destructive" | "outline" | "dashed" | "secondary" | "ghost" | "link";
1549
+ /**
1550
+ * `variant="button"` only — the glyph COMPONENT the trigger draws, `triggerIcon={Plus}` rather
1551
+ * than `<Plus />`. Defaults to the upload arrow.
1552
+ *
1553
+ * It exists because the glyph was hard-coded and `triggerVariant` only moves emphasis, so a
1554
+ * "create new" action that HAPPENS to be an upload could not carry a plus and had to announce
1555
+ * itself as an upload (gh#734). The component is rendered onto the `<svg>` itself, at the same
1556
+ * `--upload-row-icon-size` the built-in glyph uses, so a swapped icon cannot change the
1557
+ * trigger's metrics.
1558
+ *
1559
+ * Typed as the widened glyph component (SVG props + ref) rather than the narrower `IconProp`,
1560
+ * for the reason `IconGlyphProp.as` already records: the library hands the glyph its class, its
1561
+ * `aria-hidden` and its label-spacing attribute, none of which `IconProp` can accept. Every
1562
+ * `lucide-react` icon and every `Icon`-compatible component qualifies.
1563
+ */
1564
+ triggerIcon?: React.ComponentType<React.SVGProps<SVGSVGElement> & React.RefAttributes<SVGSVGElement>>;
1549
1565
  className?: ClassNameProp;
1550
1566
  children?: React.ReactNode;
1551
1567
  };
@@ -269,6 +269,18 @@ export type ButtonProp = React.ButtonHTMLAttributes<HTMLButtonElement> & {
269
269
  * `true` (a `0` pill shows); pass `false` to hide the pill at zero.
270
270
  */
271
271
  showZero?: boolean;
272
+ /**
273
+ * Localized description of what the count MEANS, folded into the accessible name so the control
274
+ * never announces as a bare digit run. The SAME prop and the same contract `Toggle` has carried
275
+ * since gh#312 — without it a counted button measured `"Git3"` in the accessibility tree, not
276
+ * "Git, 3 pages" (gh#734).
277
+ *
278
+ * The pill itself is `aria-hidden` and the spoken form is an `sr-only` sibling, so the name
279
+ * reads `"<label>, <count> <countLabel>"` from the contents; with an explicit `aria-label`
280
+ * (an icon-only counted button) the same clause is folded into that instead. Pass a
281
+ * `t()`-resolved string — the library does not own this wording.
282
+ */
283
+ countLabel?: string;
272
284
  };
273
285
  /**
274
286
  * @see Reveal — entrance-motion primitive (staggered fade-up). Wraps content in a real element
@@ -1,4 +1,4 @@
1
- export type { AppLauncherApp, AppLauncherGroup, AppLauncherLabels, AppLauncherProp, AppShellProp, AuthFooterProp, AuthIdentityProp, OrgSwitcherLabels, OrgSwitcherOrganization, OrgSwitcherProp, DragAxisProp, DragBoundsProp, DraggablePanelLabels, DraggablePanelPlacementProp, DraggablePanelPositionProp, DraggablePanelProp, PageContainerProp, PageInsetProp, SidebarItemProp, SidebarProductProp, SidebarProp, SidebarSectionProp, TopbarProp, } from "./layout.prop.js";
1
+ export type { AppLauncherApp, AppLauncherGroup, AppLauncherLabels, AppLauncherProp, AppShellProp, AuthFooterProp, AuthIdentityProp, OrgSwitcherLabels, OrgSwitcherOrganization, OrgSwitcherProp, DragAxisProp, DragBoundsProp, DraggablePanelLabels, DraggablePanelPlacementProp, DraggablePanelPositionProp, DraggablePanelProp, PageContainerExtraProp, PageContainerProp, PageInsetProp, SidebarItemProp, SidebarProductProp, SidebarProp, SidebarSectionProp, TopbarProp, } from "./layout.prop.js";
2
2
  export type { ButtonProp } from "./general.prop.js";
3
3
  export type { CommandProp, InputProp, TextareaProp, FormFieldProp, SearchInputProp, CheckboxProp, CheckboxGroupProp, ChoiceOptionProp, RadioProp, RadioGroupProp, SwitchProp, SliderProp, CalendarProp, DatePickerProp, TimePickerProp, ColorPickerProp, UploadProp, UploadFileItemProp, UploadVariantProp, TreeOptionProp, TreeFieldNamesProp, CascaderProp, TreeSelectProp, ShowCheckedStrategyProp, TransferProp, TransferItemProp, } from "./data-entry.prop.js";
4
4
  export type { AvatarProp, EmptyStateProp, DescriptionsProp, DescriptionsItemProp, BadgeProp, DataTableProp, QrCodeProp, } from "./data-display.prop.js";
@@ -22,6 +22,24 @@ export type PageContainerMeasureProp = "default" | "narrow" | "medium";
22
22
  * `document` (default) — the row is the page's TITLE: a record, a form, a collection, a report.
23
23
  */
24
24
  export type PageContainerHeaderScaleProp = "document" | "chrome";
25
+ /**
26
+ * @see PageContainer — the header's trailing slot, one node or two named sub-slots.
27
+ *
28
+ * The bare node is the shape every page already passes and stays the whole API for a single
29
+ * action cluster. `{ start, end }` splits that cluster in two so a page can put something AFTER
30
+ * the actions: the convention a consumer's four record screens share is identity → actions →
31
+ * pager LAST, and with one slot the pager had to live in a second header band inside the body,
32
+ * so the app carried two header shapes (gh#734).
33
+ *
34
+ * The shape is `TabsExtraProp`'s, deliberately — `Tabs.extra` already accepts exactly this union
35
+ * for exactly this reason, and a second spelling of one axis is what `check:prop-vocabulary`
36
+ * exists to prevent. Logical inline names (`start`/`end`, never `left`/`right`), so the slots
37
+ * swap sides under `dir="rtl"` with no second code path.
38
+ */
39
+ export type PageContainerExtraProp = ExtraProp | {
40
+ start?: ReactNode;
41
+ end?: ReactNode;
42
+ };
25
43
  /** @see PageContainer */
26
44
  export type PageContainerProp = {
27
45
  toolbarPad?: PadProp;
@@ -40,7 +58,8 @@ export type PageContainerProp = {
40
58
  * violation) so the page's heading outline never disappears mid-load.
41
59
  */
42
60
  headerLoading?: boolean;
43
- extra?: ExtraProp;
61
+ /** @see PageContainerExtraProp */
62
+ extra?: PageContainerExtraProp;
44
63
  /**
45
64
  * FIXED chrome band between the page header and the scrolling body — a filter strip, a status
46
65
  * bar, a "channel workflow" rail. It is a first-class page-chrome slot precisely because the
@@ -711,6 +711,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
711
711
  readonly file: "components/layout.prop.ts";
712
712
  readonly vocabulary: readonly [];
713
713
  };
714
+ readonly PageContainerExtraProp: {
715
+ readonly group: "layout";
716
+ readonly file: "components/layout.prop.ts";
717
+ readonly vocabulary: readonly ["ExtraProp"];
718
+ };
714
719
  readonly PageContainerMeasureProp: {
715
720
  readonly group: "layout";
716
721
  readonly file: "components/layout.prop.ts";
@@ -2449,7 +2454,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
2449
2454
  readonly ToggleProp: {
2450
2455
  readonly group: "data-entry";
2451
2456
  readonly file: "components/ui/toggle.tsx";
2452
- readonly vocabulary: readonly ["SizeProp", "ClassNameProp", {
2457
+ readonly vocabulary: readonly ["SizeProp", "ShapeProp", "ClassNameProp", {
2453
2458
  readonly field: "count";
2454
2459
  readonly local: true;
2455
2460
  readonly reason: "Counter-pill vocabulary shared VERBATIM with ButtonProp (count/overflowCount/showZero) so a counted filter tab and a counted pressed chip are one vocabulary — gh#312.";
@@ -2459,10 +2464,19 @@ export declare const COMPONENT_PROP_REGISTRY: {
2459
2464
  readonly reason: "Localized unit folded into the accessible name so a counted icon/emoji chip never announces as a bare number.";
2460
2465
  }];
2461
2466
  };
2467
+ readonly ToggleGroupProp: {
2468
+ readonly group: "data-entry";
2469
+ readonly file: "components/ui/toggle-group.tsx";
2470
+ readonly vocabulary: readonly ["ValueProp", "DefaultValueProp", "OnValueChangeProp", "DisabledProp", "SizeProp", "ShapeProp", "ClassNameProp", {
2471
+ readonly field: "wrap";
2472
+ readonly local: true;
2473
+ readonly reason: "The SAME boolean shorthand for flex-wrap that FlexProp carries, on the row that owns a chip set — one spelling for 'what does this row do when it does not fit' (gh#741). Opt-in, and the same default for every variant: paint must not decide geometry.";
2474
+ }];
2475
+ };
2462
2476
  readonly ToggleGroupItemProp: {
2463
2477
  readonly group: "data-entry";
2464
2478
  readonly file: "components/ui/toggle-group.tsx";
2465
- readonly vocabulary: readonly ["SizeProp", "ClassNameProp", {
2479
+ readonly vocabulary: readonly ["SizeProp", "ShapeProp", "ClassNameProp", {
2466
2480
  readonly field: "count";
2467
2481
  readonly local: true;
2468
2482
  readonly reason: "Per-item counter pill — variant/size come from group context, but the number is per-item data (gh#312).";
@@ -722,6 +722,11 @@ const COMPONENT_PROP_REGISTRY = {
722
722
  file: "components/layout.prop.ts",
723
723
  vocabulary: []
724
724
  },
725
+ PageContainerExtraProp: {
726
+ group: "layout",
727
+ file: "components/layout.prop.ts",
728
+ vocabulary: ["ExtraProp"]
729
+ },
725
730
  PageContainerMeasureProp: {
726
731
  group: "layout",
727
732
  file: "components/layout.prop.ts",
@@ -2842,6 +2847,9 @@ const COMPONENT_PROP_REGISTRY = {
2842
2847
  file: "components/ui/toggle.tsx",
2843
2848
  vocabulary: [
2844
2849
  "SizeProp",
2850
+ // The control `ShapeProp` verbatim — same three values, same two radius tokens as Button and
2851
+ // Badge, so `shape="pill"` means one thing library-wide (gh#734).
2852
+ "ShapeProp",
2845
2853
  "ClassNameProp",
2846
2854
  {
2847
2855
  field: "count",
@@ -2855,11 +2863,31 @@ const COMPONENT_PROP_REGISTRY = {
2855
2863
  }
2856
2864
  ]
2857
2865
  },
2866
+ ToggleGroupProp: {
2867
+ group: "data-entry",
2868
+ file: "components/ui/toggle-group.tsx",
2869
+ vocabulary: [
2870
+ "ValueProp",
2871
+ "DefaultValueProp",
2872
+ "OnValueChangeProp",
2873
+ "DisabledProp",
2874
+ "SizeProp",
2875
+ "ShapeProp",
2876
+ "ClassNameProp",
2877
+ {
2878
+ field: "wrap",
2879
+ local: true,
2880
+ reason: "The SAME boolean shorthand for flex-wrap that FlexProp carries, on the row that owns a chip set \u2014 one spelling for 'what does this row do when it does not fit' (gh#741). Opt-in, and the same default for every variant: paint must not decide geometry."
2881
+ }
2882
+ ]
2883
+ },
2858
2884
  ToggleGroupItemProp: {
2859
2885
  group: "data-entry",
2860
2886
  file: "components/ui/toggle-group.tsx",
2861
2887
  vocabulary: [
2862
2888
  "SizeProp",
2889
+ // Reaches the item through the group's context, exactly as `variant`/`size` do (gh#734).
2890
+ "ShapeProp",
2863
2891
  "ClassNameProp",
2864
2892
  {
2865
2893
  field: "count",
@@ -26,6 +26,12 @@
26
26
  background: hsl(var(--background));
27
27
  }
28
28
 
29
+ .ui-toggle-soft {
30
+ border: 1px solid transparent;
31
+ background: var(--toggle-soft-background, hsl(var(--secondary)));
32
+ color: var(--toggle-soft-color, hsl(var(--secondary-foreground)));
33
+ }
34
+
29
35
  .ui-toggle-default-size {
30
36
  min-height: var(--control-height);
31
37
  padding-inline: var(--space-3);
@@ -53,6 +59,11 @@
53
59
  color: hsl(var(--accent-foreground));
54
60
  }
55
61
 
62
+ .ui-toggle-soft:hover {
63
+ background: var(--toggle-soft-hover-background, hsl(var(--secondary-hover)));
64
+ color: var(--toggle-soft-color, hsl(var(--secondary-foreground)));
65
+ }
66
+
56
67
  .ui-toggle[data-state="on"] {
57
68
  background: var(--toggle-on-background, hsl(var(--primary)));
58
69
  color: hsl(var(--primary-foreground));
@@ -67,9 +78,14 @@
67
78
  .ui-toggle-group {
68
79
  display: inline-flex;
69
80
  align-items: center;
81
+
70
82
  gap: var(--space-1);
71
83
  }
72
84
 
85
+ .ui-toggle-group[data-wrap="true"] {
86
+ flex-wrap: wrap;
87
+ }
88
+
73
89
  .ui-button {
74
90
  display: inline-flex;
75
91
  align-items: center;
@@ -16,6 +16,11 @@
16
16
  font-variant-numeric: tabular-nums;
17
17
  }
18
18
 
19
+ .ui-toggle-soft .ui-toggle-count {
20
+ background: var(--toggle-soft-count-background, hsl(var(--background)));
21
+ color: var(--toggle-soft-count-color, hsl(var(--foreground)));
22
+ }
23
+
19
24
  .ui-toggle[data-state="on"] {
20
25
  border-color: var(--toggle-pressed-border-color, hsl(var(--primary)));
21
26
  }
@@ -11,6 +11,13 @@
11
11
  --toggle-count-background: initial;
12
12
  --toggle-count-color: initial;
13
13
 
14
+ --toggle-soft-background: initial;
15
+ --toggle-soft-color: initial;
16
+ --toggle-soft-hover-background: initial;
17
+
18
+ --toggle-soft-count-background: initial;
19
+ --toggle-soft-count-color: initial;
20
+
14
21
  --toggle-pressed-count-background: initial;
15
22
  --toggle-pressed-count-color: initial;
16
23
 
@@ -72,6 +72,14 @@ const TICKETS: Ticket[] = [
72
72
  },
73
73
  ];
74
74
 
75
+ const TAGS = [
76
+ { value: "design", label: "設計", count: 12 },
77
+ { value: "runbook", label: "運用手順", count: 148 },
78
+ { value: "onboarding", label: "入社手続き", count: 7 },
79
+ { value: "security", label: "セキュリティ", count: 31 },
80
+ { value: "archive", label: "アーカイブ", count: 1240 },
81
+ ];
82
+
75
83
  const STATUS_LABEL: Record<Ticket["status"], string> = {
76
84
  open: "未対応",
77
85
  waiting: "保留中",
@@ -93,6 +101,8 @@ const COLUMNS = [
93
101
  ];
94
102
 
95
103
  export default function Demo() {
104
+ // The tag panel of a Wiki/document screen — the chip this vocabulary exists for.
105
+ const [tags, setTags] = useState<string[]>(["design"]);
96
106
  // Faceted filter chips — pressed = the facet is applied. Counts are the facet sizes.
97
107
  const [facets, setFacets] = useState<string[]>(["open"]);
98
108
  // A reaction row: the count is how many people reacted, the pressed state is whether YOU did.
@@ -112,6 +122,47 @@ export default function Demo() {
112
122
  subtitle="カウント付きの押下チップ · faceted filter · リアクション"
113
123
  >
114
124
  <Flex direction="col" gap="lg">
125
+ <Card>
126
+ <CardHeader>
127
+ <CardTitle level={2}>タグで絞り込む · soft + pill のチップ</CardTitle>
128
+ <CardDescription>
129
+ Wiki / ドキュメントのタグパネル。variant=&quot;soft&quot; が休止時の面(--secondary、
130
+ Badge variant=&quot;secondary&quot; と同じ塗り)、shape=&quot;pill&quot; が角、count
131
+ が件数、押下が選択。variant / size / shape はグループに一度だけ書けば context
132
+ で全アイテムに届く。default や outline
133
+ だとチップは押されるまで透明に見えるので、チップ列にはこの組み合わせを使う。
134
+ </CardDescription>
135
+ </CardHeader>
136
+ <CardContent>
137
+ <Flex direction="col" gap="md">
138
+ <ToggleGroup
139
+ type="multiple"
140
+ variant="soft"
141
+ shape="pill"
142
+ size="xs"
143
+ value={tags}
144
+ onValueChange={setTags}
145
+ aria-label="タグで絞り込み"
146
+ >
147
+ {TAGS.map((tag) => (
148
+ <ToggleGroupItem
149
+ key={tag.value}
150
+ value={tag.value}
151
+ count={tag.count}
152
+ countLabel="ページ"
153
+ >
154
+ {tag.label}
155
+ </ToggleGroupItem>
156
+ ))}
157
+ </ToggleGroup>
158
+ <Text tone="muted" size="sm">
159
+ 読み上げは「設計, 12 ページ」。数字はピル側が aria-hidden で、単位つきの一文が
160
+ sr-only で並ぶので、ラベルと数字がくっついて「設計12」になることはない。
161
+ </Text>
162
+ </Flex>
163
+ </CardContent>
164
+ </Card>
165
+
115
166
  <Card>
116
167
  <CardHeader>
117
168
  <CardTitle level={2}>問い合わせ受信箱</CardTitle>
@@ -5,6 +5,22 @@ import { FormField, ToggleGroup, ToggleGroupItem } from "@godxjp/ui/data-entry";
5
5
  import { Text } from "@godxjp/ui/general";
6
6
  import { Flex, PageContainer } from "@godxjp/ui/layout";
7
7
 
8
+ /** A folder's tag facets — more chips than any rail is wide, which is the `wrap` case (gh#741). */
9
+ const folderTags = [
10
+ { value: "design", label: "設計", count: 12 },
11
+ { value: "runbook", label: "運用手順", count: 148 },
12
+ { value: "security", label: "セキュリティ", count: 37 },
13
+ { value: "onboarding", label: "オンボーディング", count: 9 },
14
+ { value: "incident", label: "障害報告", count: 64 },
15
+ { value: "contract", label: "契約", count: 21 },
16
+ { value: "billing", label: "請求", count: 83 },
17
+ { value: "release", label: "リリース", count: 156 },
18
+ { value: "meeting", label: "議事録", count: 402 },
19
+ { value: "hr", label: "人事", count: 18 },
20
+ { value: "legal", label: "法務", count: 6 },
21
+ { value: "archive", label: "アーカイブ", count: 311 },
22
+ ];
23
+
8
24
  /**
9
25
  * ToggleGroup — single or multi-select toggle set. type='single' for mutually
10
26
  * exclusive modes; type='multiple' for independent selections. Never raw radio
@@ -14,6 +30,7 @@ export default function Demo() {
14
30
  const [period, setPeriod] = useState<string>("monthly");
15
31
  const [formats, setFormats] = useState<string[]>(["pdf"]);
16
32
  const [view, setView] = useState<string>("list");
33
+ const [tags, setTags] = useState<string[]>(["design", "runbook"]);
17
34
 
18
35
  return (
19
36
  <PageContainer
@@ -95,10 +112,11 @@ export default function Demo() {
95
112
 
96
113
  <Card>
97
114
  <CardHeader>
98
- <CardTitle level={2}>variant · default / outline</CardTitle>
115
+ <CardTitle level={2}>variant · default / outline / soft</CardTitle>
99
116
  <CardDescription>
100
117
  variant は ToggleGroup にだけ指定すれば全アイテムへ伝播する(各 ToggleGroupItem
101
- への繰り返し指定は不要)。default は選択時に背景が塗られ、outline は枠線で示す。
118
+ への繰り返し指定は不要)。default は選択時に背景が塗られ、outline は枠線で示す。soft
119
+ は休止時から --secondary で塗られるので、チップの列に使う。
102
120
  </CardDescription>
103
121
  </CardHeader>
104
122
  <CardContent>
@@ -123,6 +141,109 @@ export default function Demo() {
123
141
  <ToggleGroupItem value="xlsx">Excel</ToggleGroupItem>
124
142
  </ToggleGroup>
125
143
  </Flex>
144
+ <Flex direction="col" gap="sm">
145
+ <Text as="p" size="sm" tone="muted">
146
+ variant=&quot;soft&quot;
147
+ </Text>
148
+ <ToggleGroup type="single" variant="soft" defaultValue="pdf">
149
+ <ToggleGroupItem value="pdf">PDF</ToggleGroupItem>
150
+ <ToggleGroupItem value="csv">CSV</ToggleGroupItem>
151
+ <ToggleGroupItem value="xlsx">Excel</ToggleGroupItem>
152
+ </ToggleGroup>
153
+ </Flex>
154
+ </Flex>
155
+ </CardContent>
156
+ </Card>
157
+
158
+ <Card>
159
+ <CardHeader>
160
+ <CardTitle level={2}>shape · default / pill / sharp</CardTitle>
161
+ <CardDescription>
162
+ shape も variant / size と同じく行の決定なので、ToggleGroup
163
+ に一度書けば全アイテムへ伝播する。値は Button / Badge と同じ三つで、読む radius
164
+ トークンも同じ。チップの列は soft + pill。
165
+ </CardDescription>
166
+ </CardHeader>
167
+ <CardContent>
168
+ <Flex direction="col" gap="md">
169
+ {(["default", "pill", "sharp"] as const).map((shape) => (
170
+ <Flex key={shape} direction="col" gap="sm">
171
+ <Text as="p" size="sm" tone="muted">
172
+ shape=&quot;{shape}&quot;
173
+ </Text>
174
+ <ToggleGroup type="single" variant="soft" shape={shape} defaultValue="pdf">
175
+ <ToggleGroupItem value="pdf">PDF</ToggleGroupItem>
176
+ <ToggleGroupItem value="csv">CSV</ToggleGroupItem>
177
+ <ToggleGroupItem value="xlsx">Excel</ToggleGroupItem>
178
+ </ToggleGroup>
179
+ </Flex>
180
+ ))}
181
+ </Flex>
182
+ </CardContent>
183
+ </Card>
184
+
185
+ <Card>
186
+ <CardHeader>
187
+ <CardTitle level={2}>wrap · 折り返すタグ絞り込みの行</CardTitle>
188
+ <CardDescription>
189
+ wrap
190
+ を付けた行は、レールに収まらなくなった時点で次の行へ折り返す(gh#741)。名前も真偽値も
191
+ data-wrap 属性も Flex と同じ。既定は false で、variant
192
+ によって既定を変えることはしない。320px のレールで測ると 4 項目のセグメントは wrap
193
+ の有無に関わらず 1 行 32px
194
+ のままで、変わるのは元から溢れていた行だけだからである。折り返しても gap は 1
195
+ つなので、行間と列間は同じ 4px になる。
196
+ </CardDescription>
197
+ </CardHeader>
198
+ <CardContent>
199
+ <Flex direction="col" gap="md">
200
+ <Flex direction="col" gap="sm">
201
+ <Text as="p" size="sm" tone="muted">
202
+ wrap · variant=&quot;soft&quot; shape=&quot;pill&quot; size=&quot;xs&quot; ·
203
+ タグが 12 個あっても ToggleGroup は 1 つのまま(value / onValueChange も 1
204
+ つ、矢印キーは折り返した行をまたいで DOM 順に進む)
205
+ </Text>
206
+ <ToggleGroup
207
+ type="multiple"
208
+ variant="soft"
209
+ shape="pill"
210
+ size="xs"
211
+ wrap
212
+ value={tags}
213
+ onValueChange={(v) => setTags(v as string[])}
214
+ aria-label="タグで絞り込み"
215
+ >
216
+ {folderTags.map((tag) => (
217
+ <ToggleGroupItem
218
+ key={tag.value}
219
+ value={tag.value}
220
+ count={tag.count}
221
+ countLabel="ページ"
222
+ >
223
+ {tag.label}
224
+ </ToggleGroupItem>
225
+ ))}
226
+ </ToggleGroup>
227
+ <Text as="p" size="sm" tone="muted">
228
+ 選択中: {tags.length === 0 ? "なし" : tags.join(" / ")}
229
+ </Text>
230
+ </Flex>
231
+ <Flex direction="col" gap="sm">
232
+ <Text as="p" size="sm" tone="muted">
233
+ wrap なし(既定)· セグメントは 1 行 32px のまま、何も変わらない
234
+ </Text>
235
+ <ToggleGroup
236
+ type="single"
237
+ variant="outline"
238
+ defaultValue="daily"
239
+ aria-label="集計単位"
240
+ >
241
+ <ToggleGroupItem value="daily">日次</ToggleGroupItem>
242
+ <ToggleGroupItem value="monthly">月次</ToggleGroupItem>
243
+ <ToggleGroupItem value="quarterly">四半期</ToggleGroupItem>
244
+ <ToggleGroupItem value="yearly">年次</ToggleGroupItem>
245
+ </ToggleGroup>
246
+ </Flex>
126
247
  </Flex>
127
248
  </CardContent>
128
249
  </Card>
@@ -46,11 +46,14 @@ export default function Demo() {
46
46
  <CardHeader>
47
47
  <CardTitle level={2}>Variants</CardTitle>
48
48
  <CardDescription>
49
- default (filled background when pressed) vs outline (border style).
49
+ default(押下時だけ面が出る・休止時は透明な 1px 枠)· outline(--background
50
+ の上の細い枠)· soft(休止時から --secondary で塗られる)。
51
+ 最初の二つはツールバー向けで、休止しているチップは「何もない」ように見える。
52
+ チップとして並べるときは soft。
50
53
  </CardDescription>
51
54
  </CardHeader>
52
55
  <CardContent>
53
- <Flex direction="row" gap="md">
56
+ <Flex direction="row" gap="md" wrap>
54
57
  <Toggle
55
58
  variant="default"
56
59
  pressed={pinned}
@@ -67,6 +70,55 @@ export default function Demo() {
67
70
  >
68
71
  消費税対象のみ
69
72
  </Toggle>
73
+ <Toggle variant="soft" aria-label="草案のみ">
74
+ 草案のみ
75
+ </Toggle>
76
+ </Flex>
77
+ </CardContent>
78
+ </Card>
79
+
80
+ <Card>
81
+ <CardHeader>
82
+ <CardTitle level={2}>
83
+ soft + shape=&quot;pill&quot; · チェック可能なタグチップ
84
+ </CardTitle>
85
+ <CardDescription>
86
+ antd の Tag.CheckableTag にあたる形。soft が休止時の面を、shape が角を担う。shape は
87
+ Button / Badge と同じ default / pill / sharp の三値で、同じ radius
88
+ トークンを読む。押下すると面が --primary に反転するので、状態は色だけに依存しない。
89
+ 取り消せる(×つきの)タグは選択ではなく削除なので Badge の onRemove
90
+ 側、つまり別のコンポーネントになる。
91
+ </CardDescription>
92
+ </CardHeader>
93
+ <CardContent>
94
+ <Flex direction="col" gap="md">
95
+ <Flex direction="row" gap="sm" wrap align="center">
96
+ <Toggle variant="soft" shape="pill" size="xs" defaultPressed>
97
+ 設計
98
+ </Toggle>
99
+ <Toggle variant="soft" shape="pill" size="xs">
100
+ 実装
101
+ </Toggle>
102
+ <Toggle variant="soft" shape="pill" size="xs">
103
+ 検証
104
+ </Toggle>
105
+ </Flex>
106
+ <Flex direction="row" gap="sm" wrap align="center">
107
+ <Toggle variant="soft" shape="default">
108
+ default(--radius-md)
109
+ </Toggle>
110
+ <Toggle variant="soft" shape="pill">
111
+ pill
112
+ </Toggle>
113
+ <Toggle variant="soft" shape="sharp">
114
+ sharp
115
+ </Toggle>
116
+ </Flex>
117
+ <Text as="p" size="xs" tone="muted">
118
+ soft の面は Badge variant=&quot;secondary&quot; / Button
119
+ variant=&quot;secondary&quot; と同じ --secondary。ホバーは不透明な --secondary-hover
120
+ なので、ページの上でもカードの上でも同じ色になる。
121
+ </Text>
70
122
  </Flex>
71
123
  </CardContent>
72
124
  </Card>
@@ -9,6 +9,7 @@ import {
9
9
  } from "@godxjp/ui/data-entry";
10
10
  import { Button, Text } from "@godxjp/ui/general";
11
11
  import { Flex, PageContainer } from "@godxjp/ui/layout";
12
+ import { FilePlus2, Plus } from "lucide-react";
12
13
 
13
14
  /**
14
15
  * Upload — drag-and-drop / button / avatar / picture file uploader (6 variants).
@@ -32,6 +33,7 @@ export default function Demo() {
32
33
  const [submittedFiles, setSubmittedFiles] = useState<string[]>([]);
33
34
  const [dropzoneItems, setDropzoneItems] = useState<UploadFileItem[]>([]);
34
35
  const [buttonItems, setButtonItems] = useState<UploadFileItem[]>([]);
36
+ const [triggerIconItems, setTriggerIconItems] = useState<UploadFileItem[]>([]);
35
37
  const [pictureCardItems, setPictureCardItems] = useState<UploadFileItem[]>([]);
36
38
  const [pictureItem, setPictureItem] = useState<UploadFileItem[]>([]);
37
39
  const [avatarItem, setAvatarItem] = useState<UploadFileItem[]>([]);
@@ -187,6 +189,48 @@ export default function Demo() {
187
189
  </CardContent>
188
190
  </Card>
189
191
 
192
+ <Card>
193
+ <CardHeader>
194
+ <CardTitle level={2}>triggerIcon · 「新規作成」がアップロードのとき</CardTitle>
195
+ <CardDescription>
196
+ variant=&quot;button&quot;
197
+ のトリガーが描くグリフを差し替える。コンポーネントを渡す(triggerIcon=&#123;Plus&#125;、
198
+ 要素ではない)。triggerVariant
199
+ は強弱しか変えられないので、実体はアップロードでも「新規作成」として並ぶボタンは
200
+ これまで矢印を背負うしかなかった。クラスと余白と aria-hidden
201
+ はライブラリ側が持ったままなので、差し替えても大きさは --upload-row-icon-size
202
+ のまま、読み上げにも出ない。
203
+ </CardDescription>
204
+ </CardHeader>
205
+ <CardContent>
206
+ <Flex direction="row" gap="md" wrap align="center">
207
+ <Upload
208
+ variant="button"
209
+ triggerIcon={Plus}
210
+ triggerVariant="default"
211
+ value={triggerIconItems}
212
+ onValueChange={setTriggerIconItems}
213
+ accept=".pdf"
214
+ onUpload={demoUpload}
215
+ >
216
+ 資料を追加
217
+ </Upload>
218
+ <Upload
219
+ variant="button"
220
+ triggerIcon={FilePlus2}
221
+ triggerSize="icon-sm"
222
+ triggerVariant="ghost"
223
+ value={[]}
224
+ onValueChange={() => {}}
225
+ aria-label="請求書を添付"
226
+ />
227
+ <Upload variant="button" value={[]} onValueChange={() => {}}>
228
+ 既定のグリフ
229
+ </Upload>
230
+ </Flex>
231
+ </CardContent>
232
+ </Card>
233
+
190
234
  <Card>
191
235
  <CardHeader>
192
236
  <CardTitle level={2}>Picture Card</CardTitle>
@@ -8,7 +8,7 @@ import {
8
8
  } from "@godxjp/ui/data-display";
9
9
  import { Button } from "@godxjp/ui/general";
10
10
  import { Flex, PageContainer } from "@godxjp/ui/layout";
11
- import { Pencil, Plus, Trash2 } from "lucide-react";
11
+ import { Bell, Pencil, Plus, Trash2 } from "lucide-react";
12
12
 
13
13
  /**
14
14
  * Button — the core action primitive. Composed only from real @godxjp/ui
@@ -26,23 +26,35 @@ export default function Demo() {
26
26
  <CardHeader>
27
27
  <CardTitle level={2}>Count</CardTitle>
28
28
  <CardDescription>
29
- Trailing totals, overflow cap, and explicit zero visibility.
29
+ Trailing totals, overflow cap, and explicit zero visibility. countLabel
30
+ は数字が何を数えているかを読み上げに渡す(Toggle と同じ語彙)。ピルは aria-hidden
31
+ で、単位つきの一文が sr-only で並ぶので、名前は「保留中, 18 件」。countLabel
32
+ を渡さなくても「保留中, 18」までは分かれる。
30
33
  </CardDescription>
31
34
  </CardHeader>
32
35
  <CardContent>
33
36
  <Flex direction="row" wrap align="center" gap="md">
34
- <Button variant="outline" count={18}>
37
+ <Button variant="outline" count={18} countLabel="件">
35
38
  保留中
36
39
  </Button>
37
- <Button variant="outline" count={128} overflowCount={99}>
40
+ <Button variant="outline" count={128} overflowCount={99} countLabel="件">
38
41
  受信トレイ
39
42
  </Button>
40
- <Button variant="ghost" count={0} showZero>
43
+ <Button variant="ghost" count={0} showZero countLabel="件">
41
44
  完了
42
45
  </Button>
43
- <Button variant="ghost" count={0} showZero={false}>
46
+ <Button variant="ghost" count={0} showZero={false} countLabel="件">
44
47
  非表示のゼロ
45
48
  </Button>
49
+ <Button
50
+ variant="outline"
51
+ size="icon-sm"
52
+ aria-label="通知"
53
+ count={3}
54
+ countLabel="件の未読"
55
+ >
56
+ <Bell aria-hidden="true" />
57
+ </Button>
46
58
  </Flex>
47
59
  </CardContent>
48
60
  </Card>
@@ -17,7 +17,7 @@ import type { ColumnDef } from "@godxjp/ui/data-display";
17
17
  import { Button, Text } from "@godxjp/ui/general";
18
18
  import { SearchInput } from "@godxjp/ui/data-entry";
19
19
  import { ResponsiveGrid } from "@godxjp/ui/layout";
20
- import { Toolbar, ToolbarGroup } from "@godxjp/ui/navigation";
20
+ import { Pagination, Toolbar, ToolbarGroup } from "@godxjp/ui/navigation";
21
21
  import { Plus, Download, Filter, Search } from "lucide-react";
22
22
 
23
23
  /**
@@ -129,6 +129,9 @@ export default function Demo() {
129
129
  { key: "ghost", label: "ghost" },
130
130
  ] as const;
131
131
 
132
+ /** The record pager that must sit AFTER the header's action cluster (`extra.end`). */
133
+ const [recordPage, setRecordPage] = useState(12);
134
+
132
135
  const densities = [
133
136
  { key: "compact", label: "compact" },
134
137
  { key: "default", label: "default" },
@@ -165,6 +168,69 @@ export default function Demo() {
165
168
  </ResponsiveGrid>
166
169
  </PageContainer>
167
170
 
171
+ {/* ── 1a. `extra` as { start, end } · 本人確認 → 操作 → ページャが最後 ── */}
172
+ {/* `extra` はノード 1 つでも、start / end の 2 スロットでも受ける(Tabs.extra と同じ形)。
173
+ レコード画面の並びは「誰の何か → 操作の束 → ページャ」で、ページャは操作の“あと”に
174
+ 来る。1 スロットしかなかったころは、この最後の一つを本文の中に第二のヘッダ帯として
175
+ 置くしかなく、アプリがヘッダを 2 種類抱えることになっていた。両方とも同じ
176
+ .ui-page-header-extra の直下に並ぶので、DOM 順がそのまま読み上げ順になる。 */}
177
+ <PageContainer
178
+ title="不具合 #1042 · 請求書の消費税区分が誤っている"
179
+ subtitle="Issue detail · 担当 佐藤 · 更新 2026-09-14"
180
+ breadcrumb={[
181
+ { label: "ホーム", to: "/" },
182
+ { label: "不具合", to: "/issues" },
183
+ { label: "#1042" },
184
+ ]}
185
+ breadcrumbLabel="不具合詳細のパンくず"
186
+ extra={{
187
+ start: (
188
+ <Flex gap="sm">
189
+ <Button variant="outline" size="sm">
190
+ <Download />
191
+ エクスポート
192
+ </Button>
193
+ <Button size="sm">
194
+ <Plus />
195
+ コメント
196
+ </Button>
197
+ </Flex>
198
+ ),
199
+ end: (
200
+ <Pagination
201
+ size="sm"
202
+ value={recordPage}
203
+ onValueChange={setRecordPage}
204
+ total={48}
205
+ pageSize={1}
206
+ simple
207
+ ariaLabel="不具合の前後移動"
208
+ />
209
+ ),
210
+ }}
211
+ >
212
+ <Card>
213
+ <CardHeader>
214
+ <CardTitle level={2}>ページャはヘッダの最後に置く</CardTitle>
215
+ <CardDescription>
216
+ extra にノードを 1 つ渡したときは start に入るので、既存のページは 1
217
+ 文字も変わらない。end を足したときだけ、その後ろにもう 1
218
+ スロットが増える。名前は論理方向(start / end)なので dir=&quot;rtl&quot;
219
+ でそのまま入れ替わる。本文の中にもう一段ヘッダ帯を作る必要はない。
220
+ </CardDescription>
221
+ </CardHeader>
222
+ <CardContent>
223
+ <Descriptions
224
+ items={[
225
+ { label: "状態", value: "未対応" },
226
+ { label: "優先度", value: "高" },
227
+ { label: "報告者", value: "鈴木" },
228
+ ]}
229
+ />
230
+ </CardContent>
231
+ </Card>
232
+ </PageContainer>
233
+
168
234
  {/* ── 1b. Canonical page-header contract · status/meta band ── */}
169
235
  {/* PageContainer's embedded header IS the DXS PageHeader: breadcrumbs + title +
170
236
  subtitle + status/meta + actions + responsive overflow on ONE renderer. The
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "27.9.0",
4
- "godxUiMcp": "27.9.0",
3
+ "version": "27.11.0",
4
+ "godxUiMcp": "27.11.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",