@godxjp/ui 27.12.1 → 28.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.
@@ -97,6 +97,11 @@ const Card = React.forwardRef(
97
97
  value: item.key,
98
98
  label: item.tab,
99
99
  disabled: item.disabled,
100
+ // antd's per-tab remove control. Forwarded rather than dropped: `items` is built
101
+ // AFTER `...tabProps` above, so a consumer cannot supply these from outside —
102
+ // this mapping is the only place they can arrive (gh#757).
103
+ closable: item.closable,
104
+ closeIcon: item.closeIcon,
100
105
  // The body belongs to the SELECTED tab and to no other: the card has exactly one
101
106
  // set of children, so handing them to every panel would render them N times the
102
107
  // moment `destroyOnHidden={false}` kept the hidden panels mounted.
@@ -8,7 +8,7 @@ export type TabsProps = Omit<React.ComponentPropsWithoutRef<"div">, "defaultValu
8
8
  dir?: "ltr" | "rtl";
9
9
  activationMode?: "manual" | "automatic";
10
10
  };
11
- export declare function Tabs({ className, orientation, items, value, defaultValue, onValueChange, activationMode, dir, variant, tabPlacement, size, centered, extra, destroyOnHidden, onEdit, addIcon, hideAdd, closeIcon, onTabClick, overflow, animated, indicator, moreIcon, onTabScroll, listClassName, contentClassName, children, ...props }: TabsProps): React.JSX.Element;
11
+ export declare function Tabs({ className, orientation, items, value, defaultValue, onValueChange, activationMode, dir, variant, tabPlacement, size, centered, bodied, extra, destroyOnHidden, onEdit, addIcon, hideAdd, closeIcon, onTabClick, overflow, animated, indicator, moreIcon, onTabScroll, listClassName, contentClassName, children, ...props }: TabsProps): React.JSX.Element;
12
12
  export declare const TabsList: React.ForwardRefExoticComponent<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
13
13
  variant?: "default" | "line";
14
14
  /**
@@ -8,6 +8,7 @@ import {
8
8
  Tabs as AriaTabs
9
9
  } from "react-aria-components";
10
10
  import { MoreHorizontal, Plus, X } from "lucide-react";
11
+ import { useCounterPill } from "../ui/toggle.js";
11
12
  import { useTranslation } from "../../i18n/use-translation.js";
12
13
  import { useMaxWidthBreakpoint } from "../../lib/breakpoint-token.js";
13
14
  import { cn } from "../../lib/utils.js";
@@ -31,6 +32,15 @@ function triggerSizeClassName(size) {
31
32
  size === "lg" && "min-h-[var(--tabs-trigger-height-lg)] px-[var(--tabs-trigger-padding-x-lg)] text-[length:var(--tabs-trigger-font-size-lg)]"
32
33
  );
33
34
  }
35
+ function TabCount({
36
+ count,
37
+ overflowCount,
38
+ showZero,
39
+ countLabel
40
+ }) {
41
+ const { pill } = useCounterPill({ count, overflowCount, showZero, countLabel, slot: "tabs" });
42
+ return pill;
43
+ }
34
44
  const TabsFrameContext = React.createContext({
35
45
  orientation: "horizontal",
36
46
  selectionSuppressed: false,
@@ -99,6 +109,7 @@ function Tabs({
99
109
  tabPlacement,
100
110
  size = "md",
101
111
  centered,
112
+ bodied,
102
113
  extra,
103
114
  destroyOnHidden = true,
104
115
  onEdit,
@@ -178,12 +189,13 @@ function Tabs({
178
189
  const showAdd = editable && !hideAdd && Boolean(onEdit);
179
190
  const needsBar = Boolean(extraStart || extraEnd || showAdd || collapsible);
180
191
  const card = variant === "card" || variant === "editable-card";
192
+ const bodiedCard = card && Boolean(bodied);
181
193
  const sizeTriggerClassName = triggerSizeClassName(size);
182
194
  const CARD_FACE = {
183
- top: "rounded-[var(--tabs-card-radius)_var(--tabs-card-radius)_0_0] data-[state=active]:border-b-background",
184
- bottom: "rounded-[0_0_var(--tabs-card-radius)_var(--tabs-card-radius)] data-[state=active]:border-t-background",
185
- start: "rounded-s-[var(--tabs-card-radius)] rounded-e-none data-[state=active]:border-e-background",
186
- end: "rounded-e-[var(--tabs-card-radius)] rounded-s-none data-[state=active]:border-s-background"
195
+ top: "rounded-[var(--tabs-card-radius)_var(--tabs-card-radius)_0_0] data-[state=active]:border-b-[hsl(var(--tabs-panel-background,var(--background)))]",
196
+ bottom: "rounded-[0_0_var(--tabs-card-radius)_var(--tabs-card-radius)] data-[state=active]:border-t-[hsl(var(--tabs-panel-background,var(--background)))]",
197
+ start: "rounded-s-[var(--tabs-card-radius)] rounded-e-none data-[state=active]:border-e-[hsl(var(--tabs-panel-background,var(--background)))]",
198
+ end: "rounded-e-[var(--tabs-card-radius)] rounded-s-none data-[state=active]:border-s-[hsl(var(--tabs-panel-background,var(--background)))]"
187
199
  };
188
200
  const list = items ? /* @__PURE__ */ jsx(
189
201
  TabsList,
@@ -258,6 +270,16 @@ function Tabs({
258
270
  // pixel and `justify-content` has nothing left to centre — measured, the leading and
259
271
  // trailing gaps were both 0px with `centered` on and off.
260
272
  centered && "flex-none",
273
+ // LINE face: same argument as the card face below, and from the same source — antd
274
+ // puts no `flex-grow` on a tab of ANY `type`, so a line tab is content-width too.
275
+ // `card` was fixed on that reasoning (gh#736); `line` was left on the base `flex-1`
276
+ // and kept stretching. Measured by a consumer on a 1200px strip: three tabs at
277
+ // 321px each for labels needing 83 / 84 / 56px, so the active underline ran 321px
278
+ // under a 56px label.
279
+ //
280
+ // `centered` is NOT the escape hatch for this: it centres, which trades one wrong
281
+ // geometry for another, and it is antd's prop for centring rather than for width.
282
+ variant === "line" && "flex-none",
261
283
  // The merged edge is the SURFACE colour, never `transparent`: the rail is an inset
262
284
  // shadow at the strip's padding-box edge, so a see-through border would let that 1px
263
285
  // of rail run straight across the tab it is joined to.
@@ -271,7 +293,14 @@ function Tabs({
271
293
  // the × ended up 90px from the tab's own trailing edge (179px on a two-tab
272
294
  // strip). antd's is 8px away. This is also why the face read as a free-standing
273
295
  // bordered box rather than a tab.
274
- card && "flex-none border-[color:hsl(var(--border))] bg-[hsl(var(--tabs-card-background,var(--muted)))]",
296
+ //
297
+ // THE ACTIVE FACE IS THE BODY'S SURFACE, from the same knob the body reads
298
+ // (gh#762). The base trigger's `data-[state=active]:bg-background` was right only
299
+ // while the two could not disagree; once a service can retint the body, an active
300
+ // tab still painting `--background` reads as a DIFFERENT box sitting on the panel
301
+ // — which is the "two stacked boxes" complaint in one property. Same utilities
302
+ // group, so tailwind-merge drops the base step instead of stacking two fills.
303
+ card && "flex-none border-[color:hsl(var(--border))] bg-[hsl(var(--tabs-card-background,var(--muted)))] data-[state=active]:bg-[hsl(var(--tabs-panel-background,var(--background)))]",
275
304
  card && CARD_FACE[placement]
276
305
  ),
277
306
  children: [
@@ -285,6 +314,15 @@ function Tabs({
285
314
  }
286
315
  ) : null,
287
316
  item.label,
317
+ /* @__PURE__ */ jsx(
318
+ TabCount,
319
+ {
320
+ count: item.count,
321
+ overflowCount: item.overflowCount,
322
+ showZero: item.showZero,
323
+ countLabel: item.countLabel
324
+ }
325
+ ),
288
326
  removable ? (
289
327
  // A POINTER SHORTCUT, not a control — and that is measured, not stylistic. antd's own
290
328
  // TabNode puts a real <button> beside the role="tab" element; rendered through
@@ -334,6 +372,7 @@ function Tabs({
334
372
  "data-placement": placement,
335
373
  "data-size": size,
336
374
  "data-centered": centered ? "true" : void 0,
375
+ "data-bodied": bodiedCard ? "true" : void 0,
337
376
  "data-animated-ink-bar": motion.inkBar ? "true" : "false",
338
377
  "data-animated-tab-pane": motion.tabPane ? "true" : "false",
339
378
  "data-indicator-size": indicatorSize,
@@ -363,6 +402,7 @@ function Tabs({
363
402
  "data-placement": placement,
364
403
  "data-size": size,
365
404
  "data-centered": centered ? "true" : void 0,
405
+ "data-bodied": bodiedCard ? "true" : void 0,
366
406
  "data-animated-ink-bar": motion.inkBar ? "true" : "false",
367
407
  "data-animated-tab-pane": motion.tabPane ? "true" : "false",
368
408
  "data-indicator-size": indicatorSize,
@@ -493,7 +533,15 @@ const TabsTrigger = React.forwardRef(
493
533
  // The line indicator lives in src/styles/navigation-layout.css so it reads --tabs-indicator-*.
494
534
  // Selected and focused stay visually distinct (WCAG 2.4.7): selected is a 1px hairline in the
495
535
  // border, focused is the 2px ring plus its halo outside it.
496
- "text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs:group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:flex-none group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs:group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
536
+ // `group-data-[variant=line]/tabs-list:flex-none` is the COMPOUND half of gh#757's
537
+ // width fix (gh#762). That fix landed on the `items` renderer only, which is the path
538
+ // the reporter measured; a hand-composed `<TabsList variant="line" className="w-full">`
539
+ // — what a saved-view strip that has to reach its own triggers writes — still ran on
540
+ // the base `flex-1`. Measured on a 1200px strip: three triggers at 394.66px each for
541
+ // labels needing 107 / 95 / 53px, i.e. the SAME defect one API away. antd puts no
542
+ // `flex-grow` on a tab of any `type`, so the rule belongs to the line STRIP rather than
543
+ // to one construction path; the pill strip keeps `flex-1`, which is what fills its box.
544
+ "text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring data-[state=active]:bg-background data-[state=active]:text-foreground group-data-[variant=default]/tabs:group-data-[variant=default]/tabs-list:data-[state=active]:border-primary/25 relative inline-flex flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:flex-none group-data-[orientation=vertical]/tabs:justify-start group-data-[variant=line]/tabs-list:flex-none group-data-[variant=line]/tabs-list:border-e-0 group-data-[variant=line]/tabs-list:border-b-0 disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs:group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none",
497
545
  // The tier the root was given. Without this the compound form ignored `size` outright.
498
546
  triggerSizeClassName(size),
499
547
  className
@@ -11,6 +11,28 @@ type ToggleGroupBaseProp = Omit<React.ComponentPropsWithoutRef<"div">, "defaultV
11
11
  disabled?: boolean;
12
12
  /** Which way the arrow keys walk the group. */
13
13
  orientation?: "horizontal" | "vertical";
14
+ /**
15
+ * May the group end up with NOTHING selected? Omitted (`false`) it may, which is what a tag
16
+ * filter wants — pressing the selected chip again clears it and reports `""`. Set it and the
17
+ * selected item stays selected when it is pressed again.
18
+ *
19
+ * **It also decides the ARIA role of a `type="single"` group (gh#744)**, because emptiness is
20
+ * exactly what the two roles disagree about. ARIA has no "press again to deselect" for a radio:
21
+ * a radiogroup that has a selection always has exactly one checked item, so a radiogroup that
22
+ * the user just emptied is a state a screen reader cannot read out. Therefore:
23
+ *
24
+ * - omitted → `role="group"` + `aria-pressed` per item (a row of toggle buttons, which MAY be
25
+ * all-off) — this is the default, so no existing single group changes behaviour;
26
+ * - set → `role="radiogroup"` + `role="radio"` / `aria-checked` per item, and the arrow keys
27
+ * move the SELECTION as APG's radio-group pattern requires, not just the focus.
28
+ *
29
+ * The name is React Aria's own (`useToggleGroupState`, which this component is built on) —
30
+ * neither antd nor Radix names the capability. See `docs/DESIGN-AUTHORITY.md`.
31
+ *
32
+ * On `type="multiple"` it only keeps the last remaining item selected; the roles do not move
33
+ * (a row of `aria-pressed` buttons is already the right reading there).
34
+ */
35
+ disallowEmptySelection?: boolean;
14
36
  /**
15
37
  * Let the row break onto further lines instead of running past its rail (gh#741). Same name,
16
38
  * same boolean shape and same `data-wrap` attribute as `Flex` — one spelling answers "what does
@@ -13,6 +13,7 @@ import {
13
13
  useCounterPill
14
14
  } from "./toggle.js";
15
15
  const ToggleGroupContext = React.createContext({});
16
+ const ARROW_KEYS = /* @__PURE__ */ new Set(["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"]);
16
17
  function toSelectedKeys(value) {
17
18
  if (value == null) {
18
19
  return void 0;
@@ -39,10 +40,21 @@ const ToggleGroup = React.forwardRef(
39
40
  defaultValue,
40
41
  onValueChange,
41
42
  disabled,
43
+ disallowEmptySelection,
42
44
  loop: _loop,
43
45
  ...props
44
46
  }, ref) => {
45
- const context = React.useMemo(() => ({ variant, size, shape }), [variant, size, shape]);
47
+ const radioSemantics = type === "single" && disallowEmptySelection === true;
48
+ const arrowNav = React.useRef(false);
49
+ const groupElement = React.useRef(null);
50
+ const [inToolbar, setInToolbar] = React.useState(false);
51
+ React.useLayoutEffect(() => {
52
+ setInToolbar(!!groupElement.current?.parentElement?.closest('[role="toolbar"]'));
53
+ }, []);
54
+ const context = React.useMemo(
55
+ () => ({ variant, size, shape, radioSemantics, arrowNav, inToolbar }),
56
+ [variant, size, shape, radioSemantics, inToolbar]
57
+ );
46
58
  const selectedKeys = React.useMemo(() => toSelectedKeys(value), [value]);
47
59
  const defaultSelectedKeys = React.useMemo(() => toSelectedKeys(defaultValue), [defaultValue]);
48
60
  const handleSelectionChange = (keys) => {
@@ -69,7 +81,33 @@ const ToggleGroup = React.forwardRef(
69
81
  defaultSelectedKeys,
70
82
  onSelectionChange: handleSelectionChange,
71
83
  isDisabled: disabled,
72
- render: (domProps) => /* @__PURE__ */ jsx("div", { ...props, ...domProps, tabIndex: props.tabIndex ?? domProps.tabIndex }),
84
+ disallowEmptySelection,
85
+ render: (domProps) => /* @__PURE__ */ jsx(
86
+ "div",
87
+ {
88
+ ...props,
89
+ ...domProps,
90
+ ref: (node) => {
91
+ groupElement.current = node;
92
+ const aria = domProps.ref;
93
+ if (typeof aria === "function") aria(node);
94
+ else if (aria) aria.current = node;
95
+ },
96
+ ...type === "single" && !radioSemantics ? { role: "group", "aria-orientation": void 0 } : null,
97
+ onKeyDownCapture: (event) => {
98
+ if (radioSemantics) {
99
+ arrowNav.current = ARROW_KEYS.has(event.key);
100
+ if (arrowNav.current) {
101
+ queueMicrotask(() => {
102
+ arrowNav.current = false;
103
+ });
104
+ }
105
+ }
106
+ domProps.onKeyDownCapture?.(event);
107
+ },
108
+ tabIndex: props.tabIndex ?? domProps.tabIndex
109
+ }
110
+ ),
73
111
  children: /* @__PURE__ */ jsx(ToggleGroupContext.Provider, { value: context, children })
74
112
  }
75
113
  );
@@ -105,6 +143,9 @@ const ToggleGroupItem = React.forwardRef(
105
143
  });
106
144
  const groupState = React.useContext(ToggleGroupStateContext);
107
145
  const isPressed = groupState?.selectedKeys.has(value) ?? false;
146
+ const isSingle = groupState?.selectionMode === "single";
147
+ const radioSemantics = context.radioSemantics === true;
148
+ const rovingTabIndex = radioSemantics && !context.inToolbar && groupState && groupState.selectedKeys.size > 0 ? isPressed ? 0 : -1 : void 0;
108
149
  return /* @__PURE__ */ jsxs(
109
150
  ToggleButton,
110
151
  {
@@ -122,7 +163,22 @@ const ToggleGroupItem = React.forwardRef(
122
163
  ...props,
123
164
  id: value,
124
165
  isDisabled: disabled,
125
- render: (domProps) => restoreDomProps(props, domProps),
166
+ render: (domProps) => restoreDomProps(props, {
167
+ ...domProps,
168
+ ...isSingle && !radioSemantics ? { role: void 0, "aria-checked": void 0, "aria-pressed": isPressed } : null,
169
+ ...rovingTabIndex == null ? null : { tabIndex: rovingTabIndex },
170
+ // Selection follows focus, as APG requires of a radio group — but only when an arrow
171
+ // key is what moved it. Measured otherwise: RAC's toolbar Tab handler focuses the
172
+ // LAST item on the way out, which silently moved the selection to it.
173
+ ...radioSemantics ? {
174
+ onFocus: (event) => {
175
+ if (context.arrowNav?.current && groupState && groupState.selectedKeys.size > 0 && !isPressed) {
176
+ groupState.setSelected(value, true);
177
+ }
178
+ domProps.onFocus?.(event);
179
+ }
180
+ } : null
181
+ }),
126
182
  children: [
127
183
  children,
128
184
  pill
@@ -42,9 +42,18 @@ type TogglePressedFields = {
42
42
  /** Fired with the NEW pressed state — RAC `onChange`. */
43
43
  onPressedChange?: (pressed: boolean) => void;
44
44
  };
45
- /** Counter pill shared by `Toggle` and `ToggleGroupItem`. ACCESSIBLE NAME. */
46
- declare function useCounterPill({ count, overflowCount, showZero, countLabel, ariaLabel, }: ToggleCountFields & {
45
+ /**
46
+ * Counter pill shared by `Toggle`, `ToggleGroupItem` and `Tabs` items. ACCESSIBLE NAME.
47
+ *
48
+ * `slot` names the hooks the pill emits (`data-slot="<slot>-count"` / `.ui-<slot>-count`) so a
49
+ * third component can reuse the whole construction — the Intl formatting, the `overflowCount`
50
+ * cap, and above all the `aria-hidden` + `sr-only` split that keeps «未対応12» out of the
51
+ * accessible name (gh#734) — without a second counting API growing beside this one (gh#762).
52
+ * It defaults to `toggle`, so every existing call emits the identical markup it always has.
53
+ */
54
+ declare function useCounterPill({ count, overflowCount, showZero, countLabel, ariaLabel, slot, }: ToggleCountFields & {
47
55
  ariaLabel?: string;
56
+ slot?: string;
48
57
  }): {
49
58
  pill: null;
50
59
  resolvedAriaLabel: string | undefined;
@@ -50,7 +50,8 @@ function useCounterPill({
50
50
  overflowCount = 99,
51
51
  showZero = true,
52
52
  countLabel,
53
- ariaLabel
53
+ ariaLabel,
54
+ slot = "toggle"
54
55
  }) {
55
56
  const { locale } = useTranslation();
56
57
  const visible = count != null && (count !== 0 || showZero);
@@ -65,7 +66,7 @@ function useCounterPill({
65
66
  const spoken = countLabel ? `${formatted} ${countLabel}` : formatted;
66
67
  return {
67
68
  pill: /* @__PURE__ */ jsxs(Fragment, { children: [
68
- /* @__PURE__ */ jsx("span", { "data-slot": "toggle-count", className: "ui-toggle-count", "aria-hidden": "true", children: formatted }),
69
+ /* @__PURE__ */ jsx("span", { "data-slot": `${slot}-count`, className: `ui-${slot}-count`, "aria-hidden": "true", children: formatted }),
69
70
  /* @__PURE__ */ jsx("span", { className: "sr-only", children: `, ${spoken}` })
70
71
  ] }),
71
72
  resolvedAriaLabel: ariaLabel == null ? ariaLabel : `${ariaLabel}, ${spoken}`
@@ -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.12.1",
3
+ "version": "28.1.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,
@@ -972,4 +972,19 @@ export type CardTabItemProp = {
972
972
  tab: React.ReactNode;
973
973
  /** Ant Design `disabled` — the tab stays in the strip and in the roving focus, unselectable. */
974
974
  disabled?: boolean;
975
+ /**
976
+ * Ant Design `closable` — honoured by `tabProps={{ variant: "editable-card" }}`, where it
977
+ * decides whether THIS tab gets a remove button (gh#757).
978
+ *
979
+ * Without it the strip is all-or-nothing: `editable-card` puts a × on every tab, including the
980
+ * one that is not deletable — a consumer measured 3 of 3 removable where 2 was right. It could
981
+ * not be patched from outside either, because `Card` builds `items` AFTER spreading
982
+ * `...tabProps`, so a hand-written `items` never survived.
983
+ *
984
+ * Default (undefined) stays removable, which is antd's own default and `Tabs`' (`closable !==
985
+ * false`) — this only adds the way to say no.
986
+ */
987
+ closable?: boolean;
988
+ /** Ant Design `closeIcon` — replaces the default × on this tab's remove button. */
989
+ closeIcon?: React.ReactNode;
975
990
  };
@@ -264,6 +264,32 @@ export type TabItemProp = {
264
264
  closable?: boolean;
265
265
  /** Ant Design `Tab.closeIcon` — replaces the default × on this item's remove button. */
266
266
  closeIcon?: React.ReactNode;
267
+ /**
268
+ * The count beside the label — 「未対応 **12**」, the list-screen pattern (gh#762).
269
+ *
270
+ * It is the SAME counter vocabulary `Button` (gh#312) and `Toggle`/`ToggleGroupItem` (gh#734)
271
+ * already publish, drawn by the same helper, so a counted tab and the counted chip under it
272
+ * are one thing at one size and one accessible shape. Formatted with `Intl.NumberFormat` in
273
+ * the active locale — never `String(n)`, never a hand-rolled thousands separator.
274
+ *
275
+ * WHY IT IS A SLOT AND NOT `label: <span>未対応 <Badge count={12} /></span>` (antd's answer):
276
+ * with the number inside the label the PACKAGE owns none of it — not the size, not the gap,
277
+ * not the tone as the tab moves between selected / unselected / disabled — so every consumer
278
+ * lands somewhere different. The slot is what lets the tab state drive the pill.
279
+ */
280
+ count?: number;
281
+ /** Cap for `count` — beyond it the pill shows `{overflowCount}+` (e.g. `99+`). Default `99`. */
282
+ overflowCount?: number;
283
+ /** Render the pill when `count` is 0. Default `true`, matching Button and Toggle. */
284
+ showZero?: boolean;
285
+ /**
286
+ * Localized description of what the count MEANS, folded into the tab's accessible name so it
287
+ * never announces as label-then-bare-digits. The digits concatenate straight onto the label in
288
+ * the accessibility tree («未対応12», gh#734's defect), so the pill is `aria-hidden` and the
289
+ * spoken clause is an `sr-only` sibling: `countLabel="未対応の課題"` reads
290
+ * 「未対応, 12 未対応の課題」. Already localized when it arrives — pass a `t()` string.
291
+ */
292
+ countLabel?: string;
267
293
  /**
268
294
  * Ant Design `Tab.forceRender`. Mounts THIS panel up front and keeps it mounted while another
269
295
  * tab is selected, without turning that on for the whole strip the way `destroyOnHidden={false}`
@@ -404,6 +430,30 @@ export type TabsProp = {
404
430
  size?: "sm" | "md" | "lg";
405
431
  /** Ant Design `centered` — centre the trigger strip on its own inline axis. */
406
432
  centered?: boolean;
433
+ /**
434
+ * Draw the PANEL BODY the card strip opens into, so the strip and the panel are ONE object
435
+ * (gh#762): a surface, a border continuous with the rail, and the radius on the two corners
436
+ * away from the strip. The active tab's block-end edge is repainted in that body's own colour,
437
+ * which is what makes it read as opened into the body rather than closed off above it.
438
+ *
439
+ * WHY IT EXISTS. `variant="card"` already repaints the active face's merged edge — antd's
440
+ * `genCardStyle` — but the package shipped no body for it to merge INTO, so the consumer had to
441
+ * supply one and every choice was wrong. Measured on 28.0.0 against a `variant="card"` strip at
442
+ * 1440px: `<Card>` left an 8px gap AND added a second 1px border at a 9.708px radius (two
443
+ * stacked boxes), `<Card variant="borderless">` left the same 8px gap with no line at all, and
444
+ * no box at all left the 8px gap over a transparent panel. The gap is `--tabs-root-gap`, which
445
+ * is correct for every OTHER variant — a body is not a gap tweak, it is the missing half of the
446
+ * card face.
447
+ *
448
+ * HONOURED ONLY BY `card` / `editable-card`, and that is a statement, not a limitation. The
449
+ * pill strip is a free-floating control and the `line` strip's own body is the CARD around it —
450
+ * `<Card tabList>` is that pairing, already built and already joined. Passing `bodied` to any
451
+ * other variant emits no attribute and changes no pixel.
452
+ *
453
+ * OPT-IN, so no strip on 28.0.0 moves. A service that wants every card strip bodied has the
454
+ * `card`-variant defaults as a theme decision, not a prop it must repeat.
455
+ */
456
+ bodied?: boolean;
407
457
  /** Ant Design `tabBarExtraContent`, renamed and made logical. @see TabsExtraProp */
408
458
  extra?: TabsExtraProp;
409
459
  /**
@@ -2350,7 +2350,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
2350
2350
  readonly TabsProp: {
2351
2351
  readonly group: "navigation";
2352
2352
  readonly file: "components/navigation.prop.ts";
2353
- readonly vocabulary: readonly ["ValueProp", "DefaultValueProp", "OnValueChangeProp", "ClassNameProp", "SizeProp", "ExtraProp"];
2353
+ readonly vocabulary: readonly ["ValueProp", "DefaultValueProp", "OnValueChangeProp", "ClassNameProp", "SizeProp", "ExtraProp", {
2354
+ readonly field: "bodied";
2355
+ readonly local: true;
2356
+ readonly reason: "A positive boolean for the card strip's PANEL BODY (gh#762) — an axis on Tabs rather than a `CardTabs` export, because the thing missing was a surface the existing card face already repaints its edge into, not a second component. No vocabulary type: no other component draws a body for its own panel.";
2357
+ }];
2354
2358
  };
2355
2359
  readonly TabsVariantProp: {
2356
2360
  readonly group: "navigation";
@@ -2414,7 +2418,15 @@ export declare const COMPONENT_PROP_REGISTRY: {
2414
2418
  readonly TabItemProp: {
2415
2419
  readonly group: "navigation";
2416
2420
  readonly file: "components/navigation.prop.ts";
2417
- readonly vocabulary: readonly ["ValueProp", "LabelProp", "ChildrenProp", "DisabledProp", "IconProp"];
2421
+ readonly vocabulary: readonly ["ValueProp", "LabelProp", "ChildrenProp", "DisabledProp", "IconProp", {
2422
+ readonly field: "count";
2423
+ readonly local: true;
2424
+ readonly reason: "Counter-pill vocabulary shared VERBATIM with ButtonProp and ToggleProp (count/overflowCount/showZero) and drawn by the same `useCounterPill`, so a counted tab, a counted chip and a counted button are one thing — gh#312 / gh#734 / gh#762.";
2425
+ }, {
2426
+ readonly field: "countLabel";
2427
+ readonly local: true;
2428
+ readonly reason: "Localized unit folded into the tab's accessible name («未対応, 12 件の課題»), so the digits never concatenate onto the label — gh#734's measured defect, same field name as Button/Toggle.";
2429
+ }];
2418
2430
  };
2419
2431
  readonly FormFieldArrayProp: {
2420
2432
  readonly group: "form";
@@ -2471,6 +2483,10 @@ export declare const COMPONENT_PROP_REGISTRY: {
2471
2483
  readonly field: "wrap";
2472
2484
  readonly local: true;
2473
2485
  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.";
2486
+ }, {
2487
+ readonly field: "disallowEmptySelection";
2488
+ readonly local: true;
2489
+ readonly reason: "React Aria's own name for the capability (useToggleGroupState), which this component is built on; neither antd nor Radix names it (docs/DESIGN-AUTHORITY.md). It is not just a state guard: on type=single it DECIDES the ARIA role, because emptiness is the one thing role=group + aria-pressed and role=radiogroup + aria-checked disagree about (gh#744).";
2474
2490
  }];
2475
2491
  };
2476
2492
  readonly ToggleGroupItemProp: {
@@ -2735,7 +2735,12 @@ const COMPONENT_PROP_REGISTRY = {
2735
2735
  "OnValueChangeProp",
2736
2736
  "ClassNameProp",
2737
2737
  "SizeProp",
2738
- "ExtraProp"
2738
+ "ExtraProp",
2739
+ {
2740
+ field: "bodied",
2741
+ local: true,
2742
+ reason: "A positive boolean for the card strip's PANEL BODY (gh#762) \u2014 an axis on Tabs rather than a `CardTabs` export, because the thing missing was a surface the existing card face already repaints its edge into, not a second component. No vocabulary type: no other component draws a body for its own panel."
2743
+ }
2739
2744
  ]
2740
2745
  },
2741
2746
  TabsVariantProp: {
@@ -2803,7 +2808,23 @@ const COMPONENT_PROP_REGISTRY = {
2803
2808
  TabItemProp: {
2804
2809
  group: "navigation",
2805
2810
  file: "components/navigation.prop.ts",
2806
- vocabulary: ["ValueProp", "LabelProp", "ChildrenProp", "DisabledProp", "IconProp"]
2811
+ vocabulary: [
2812
+ "ValueProp",
2813
+ "LabelProp",
2814
+ "ChildrenProp",
2815
+ "DisabledProp",
2816
+ "IconProp",
2817
+ {
2818
+ field: "count",
2819
+ local: true,
2820
+ reason: "Counter-pill vocabulary shared VERBATIM with ButtonProp and ToggleProp (count/overflowCount/showZero) and drawn by the same `useCounterPill`, so a counted tab, a counted chip and a counted button are one thing \u2014 gh#312 / gh#734 / gh#762."
2821
+ },
2822
+ {
2823
+ field: "countLabel",
2824
+ local: true,
2825
+ reason: "Localized unit folded into the tab's accessible name (\xAB\u672A\u5BFE\u5FDC, 12 \u4EF6\u306E\u8AB2\u984C\xBB), so the digits never concatenate onto the label \u2014 gh#734's measured defect, same field name as Button/Toggle."
2826
+ }
2827
+ ]
2807
2828
  },
2808
2829
  FormFieldArrayProp: {
2809
2830
  group: "form",
@@ -2878,6 +2899,11 @@ const COMPONENT_PROP_REGISTRY = {
2878
2899
  field: "wrap",
2879
2900
  local: true,
2880
2901
  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."
2902
+ },
2903
+ {
2904
+ field: "disallowEmptySelection",
2905
+ local: true,
2906
+ reason: "React Aria's own name for the capability (useToggleGroupState), which this component is built on; neither antd nor Radix names it (docs/DESIGN-AUTHORITY.md). It is not just a state guard: on type=single it DECIDES the ARIA role, because emptiness is the one thing role=group + aria-pressed and role=radiogroup + aria-checked disagree about (gh#744)."
2881
2907
  }
2882
2908
  ]
2883
2909
  },
@@ -85,15 +85,14 @@
85
85
 
86
86
  [data-slot="card"][data-accent] > [data-slot="card-header"],
87
87
  [data-slot="card"][data-accent] > [data-slot="card-content"]:not([data-flush]),
88
+ [data-slot="card"][data-accent] > [data-slot="card-footer"]:not([data-flush]) {
89
+ padding-inline-start: calc(var(--card-space-inset) - var(--card-accent-rail-width));
90
+ }
88
91
 
89
92
  [data-slot="card"] > [data-slot="card-footer"] {
90
93
  margin-block-start: auto;
91
94
  }
92
95
 
93
- [data-slot="card"][data-accent] > [data-slot="card-footer"]:not([data-flush]) {
94
- padding-inline-start: calc(var(--card-space-inset) - var(--card-accent-rail-width));
95
- }
96
-
97
96
  [data-slot="card"][data-accent][data-accent-placement="perimeter"] {
98
97
  border-width: var(--card-accent-perimeter-width);
99
98
  border-color: var(--card-accent-color);