@godxjp/ui 31.3.0 → 31.4.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/agent/START-HERE.md +5 -5
- package/agent/components/Tabs.json +5 -0
- package/agent/components.json +5 -0
- package/agent/index.json +5 -5
- package/agent/llms.txt +5 -5
- package/agent/tokens.json +33 -3
- package/dist/components/data-entry/upload.js +12 -1
- package/dist/components/navigation/tabs.d.ts +2 -2
- package/dist/components/navigation/tabs.js +57 -10
- package/dist/contracts/measurement.json +1 -1
- package/dist/props/components/navigation.prop.d.ts +19 -1
- package/dist/props/registry.d.ts +1 -1
- package/dist/props/registry.js +1 -0
- package/dist/styles/alert-layout.css +12 -12
- package/dist/styles/control.css +18 -2
- package/dist/styles/data-entry-layout.css +4 -0
- package/dist/styles/form-layout.css +47 -5
- package/dist/styles/layers.json +1 -1
- package/dist/styles/layout.css +2 -1
- package/dist/tokens/components/segmented.css +4 -0
- package/dist/tokens/semantic/layout.css +2 -0
- package/docs/data-entry/segmented.tsx +2 -0
- package/docs/navigation/tabs.tsx +43 -0
- package/docs/query/data-state.tsx +32 -8
- package/docs/showcase/table-view-tabs.tsx +15 -13
- package/docs/themes/flat.css +4 -0
- package/docs/themes/glassmorphism.css +2 -0
- package/docs/themes/neubrutalism.css +6 -0
- package/package.json +2 -2
package/agent/START-HERE.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
You are about to write code against a design system you did not author. This file is the whole
|
|
4
4
|
contract. Read it before you write JSX.
|
|
5
5
|
|
|
6
|
-
**This catalog describes `@godxjp/ui` 31.
|
|
6
|
+
**This catalog describes `@godxjp/ui` 31.4.0.** If the project you are editing has a different
|
|
7
7
|
version in its `package.json`, read the pinned catalog for THAT version instead
|
|
8
8
|
(`…/v<their-version>/agent/…`). A catalog newer than the installed package describes props that do
|
|
9
9
|
not exist yet; older, and it hides props that do. Neither failure announces itself.
|
|
@@ -56,10 +56,10 @@ Then ask it: `search_components`, `get_component`, `get_tokens`, `get_rule`, `li
|
|
|
56
56
|
its `importPath`, and its examples. Fetch only the handful you picked in step 1.
|
|
57
57
|
3. `rules.json` — 50 cardinal rules. The ones about raw HTML and hardcoded colour are not
|
|
58
58
|
style advice.
|
|
59
|
-
4. `tokens.json` —
|
|
59
|
+
4. `tokens.json` — 2080 design tokens, each tagged with its `tier`. **If you were handed a
|
|
60
60
|
brand, read the 211 `foundation` entries first** — `--primary`, `--background`,
|
|
61
61
|
`--radius`, `--font-size-base` are the handful everything else derives from. The
|
|
62
|
-
|
|
62
|
+
1765 `component` entries are per-part knobs; reach for one only when a role is
|
|
63
63
|
right everywhere except one component.
|
|
64
64
|
5. `anti-ai-tells.json` — 26 shapes that make generated UI look generated, each with the
|
|
65
65
|
fix. Read before you reach for a gradient hero or a wall of coloured chips.
|
|
@@ -144,8 +144,8 @@ has stopped following the brand.
|
|
|
144
144
|
| `tier` | count | what it is | set it? |
|
|
145
145
|
|---|---|---|---|
|
|
146
146
|
| `foundation` | 211 | the seeds — `--primary`, `--background`, `--foreground`, `--radius`, `--font-size-base`, `--shadow-color`. Everything below is derived from these | **yes — this is the main road.** Handed a brand colour, this is where it goes: `:root { --primary: <H> <S>% <L>%; }` (HSL components, no `hsl()` wrapper) |
|
|
147
|
-
| `semantic` |
|
|
148
|
-
| `component` |
|
|
147
|
+
| `semantic` | 104 | named roles that follow the seeds — `--ring`, `--text-link`, `--primary-hover`, `--overlay-background` | only when the seed is right and ONE role must differ. That role then stops following a later brand change |
|
|
148
|
+
| `component` | 1765 | per-part knobs, `--{component}-{part}-{property}` | rarely. Most are declared `initial` with the real default at the call site — deliberate, so a scoped override re-resolves instead of freezing at `:root` |
|
|
149
149
|
|
|
150
150
|
A token whose `value` is `initial` is not empty and not broken: `initial` is the guaranteed-invalid
|
|
151
151
|
value, so the real default is computed where the element paints it. Set it and yours wins.
|
|
@@ -24,6 +24,11 @@
|
|
|
24
24
|
"name": "onValueChange",
|
|
25
25
|
"type": "(value: string) => void"
|
|
26
26
|
},
|
|
27
|
+
{
|
|
28
|
+
"description": "Strip-only mode (gh#1021): the tabs drive content OUTSIDE the strip. No tabpanel is rendered, and the selected tab's `aria-controls` points at this id, which is the consumer's own region (give it `role=\"region\"` and a name). Content there is never remounted on a tab switch, so a typed draft survives. With the compound form it applies to triggers without a declared `TabsContent` (a declared panel wins). A pure filter with NO region to point at is not a tab strip; use `Segmented`. The root's `aria-label`/`aria-labelledby` name the `role=\"tablist\"` (gh#1020), and a compound `TabsTrigger` takes `count`/`overflowCount`/`showZero`/`countLabel`, drawn exactly like `items[].count`.",
|
|
29
|
+
"name": "controls",
|
|
30
|
+
"type": "IdProp"
|
|
31
|
+
},
|
|
27
32
|
{
|
|
28
33
|
"defaultValue": "\"default\"",
|
|
29
34
|
"description": "Trigger-strip appearance — this is Ant Design's `type` under the library's own `variant` vocabulary. `default` is the pill strip (antd has no equivalent). `line` is UNDERLINE-ONLY: the selected trigger gets no ring or card border at all — only the token-owned 2px primary bar (--tabs-indicator-{background,size,offset}) — so the `:focus-visible` keyboard ring stays visible and clearly distinct from selection. `card` gives each tab a boxed face on a rail (--tabs-card-*). `editable-card` is `card` plus the add button and per-tab remove shortcut, and needs `onEdit` to do anything. With `items`, the variant is forwarded to the list; when composing manually, pass the same value to `<TabsList variant=\"line\">`.",
|
package/agent/components.json
CHANGED
|
@@ -7880,6 +7880,11 @@
|
|
|
7880
7880
|
"name": "onValueChange",
|
|
7881
7881
|
"type": "(value: string) => void"
|
|
7882
7882
|
},
|
|
7883
|
+
{
|
|
7884
|
+
"description": "Strip-only mode (gh#1021): the tabs drive content OUTSIDE the strip. No tabpanel is rendered, and the selected tab's `aria-controls` points at this id, which is the consumer's own region (give it `role=\"region\"` and a name). Content there is never remounted on a tab switch, so a typed draft survives. With the compound form it applies to triggers without a declared `TabsContent` (a declared panel wins). A pure filter with NO region to point at is not a tab strip; use `Segmented`. The root's `aria-label`/`aria-labelledby` name the `role=\"tablist\"` (gh#1020), and a compound `TabsTrigger` takes `count`/`overflowCount`/`showZero`/`countLabel`, drawn exactly like `items[].count`.",
|
|
7885
|
+
"name": "controls",
|
|
7886
|
+
"type": "IdProp"
|
|
7887
|
+
},
|
|
7883
7888
|
{
|
|
7884
7889
|
"defaultValue": "\"default\"",
|
|
7885
7890
|
"description": "Trigger-strip appearance — this is Ant Design's `type` under the library's own `variant` vocabulary. `default` is the pill strip (antd has no equivalent). `line` is UNDERLINE-ONLY: the selected trigger gets no ring or card border at all — only the token-owned 2px primary bar (--tabs-indicator-{background,size,offset}) — so the `:focus-visible` keyboard ring stays visible and clearly distinct from selection. `card` gives each tab a boxed face on a rail (--tabs-card-*). `editable-card` is `card` plus the add button and per-tab remove shortcut, and needs `onEdit` to do anything. With `items`, the variant is forwarded to the list; when composing manually, pass the same value to `<TabsList variant=\"line\">`.",
|
package/agent/index.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"components": 176,
|
|
5
5
|
"patterns": 21,
|
|
6
6
|
"rules": 50,
|
|
7
|
-
"tokens":
|
|
7
|
+
"tokens": 2080,
|
|
8
8
|
"vocabulary": 14
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
@@ -48,19 +48,19 @@
|
|
|
48
48
|
"note": "Pin to the tag that matches the @godxjp/ui version you installed. A catalog newer than your package describes props you do not have; older, and it hides props you do.",
|
|
49
49
|
"read": {
|
|
50
50
|
"live": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/index.json",
|
|
51
|
-
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v31.
|
|
51
|
+
"pinned": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/v31.4.0/agent/index.json"
|
|
52
52
|
},
|
|
53
53
|
"source": "mcp/src/data — the same data @godxjp/ui-mcp serves — plus the foundation and semantic token tiers, read from src/tokens/*.css",
|
|
54
54
|
"start": "https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/START-HERE.md",
|
|
55
55
|
"tokenTiers": {
|
|
56
56
|
"component": "per-part knobs, --{component}-{part}-{property}; usually leave these alone",
|
|
57
57
|
"counts": {
|
|
58
|
-
"component":
|
|
58
|
+
"component": 1765,
|
|
59
59
|
"foundation": 211,
|
|
60
|
-
"semantic":
|
|
60
|
+
"semantic": 104
|
|
61
61
|
},
|
|
62
62
|
"foundation": "the seeds a consumer is invited to set — --primary, --background, --radius",
|
|
63
63
|
"semantic": "named roles that follow the seeds — --ring, --text-link, --overlay-background"
|
|
64
64
|
},
|
|
65
|
-
"version": "31.
|
|
65
|
+
"version": "31.4.0"
|
|
66
66
|
}
|
package/agent/llms.txt
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @godxjp/ui
|
|
2
2
|
|
|
3
|
-
> A Japanese-enterprise React design system: 176 components,
|
|
4
|
-
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.
|
|
3
|
+
> A Japanese-enterprise React design system: 176 components, 2080 design tokens,
|
|
4
|
+
> 50 cardinal rules. This file is the entry point for AI agents. Catalog version 31.4.0.
|
|
5
5
|
|
|
6
6
|
If your client can run a process, do not read these files — run the MCP server instead
|
|
7
|
-
(`npx @godxjp/ui-mcp@31.
|
|
7
|
+
(`npx @godxjp/ui-mcp@31.4.0`). It is searchable and version-locked. These files exist for agents
|
|
8
8
|
that can only fetch URLs.
|
|
9
9
|
|
|
10
10
|
## Start
|
|
@@ -18,7 +18,7 @@ that can only fetch URLs.
|
|
|
18
18
|
- [components-index.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components-index.json): 47 KB — all 176 components as name, group, tagline, plus `absorbed`: the names that do NOT exist and map to it (`Combobox` → `Select`).
|
|
19
19
|
- [components/<Name>.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components/Select.json): one file per component (1 KB–35 KB, median 6 KB). Read the index, then fetch only the ones you chose — this is the selective route, and the reason you do not need the blob.
|
|
20
20
|
- [components.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/components.json): 1.2 MB — every entry in one file. Most URL fetchers truncate a response this size without saying so; prefer the per-component files.
|
|
21
|
-
- [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand),
|
|
21
|
+
- [tokens.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/tokens.json): every design token with its value, the reason it exists, and its `tier` — 211 `foundation` seeds (`--primary`, `--background`, `--radius`: set these when you are handed a brand), 104 `semantic` roles, 1765 `component` knobs.
|
|
22
22
|
- [vocabulary.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/vocabulary.json): the controlled prop vocabulary — which prop name means what, across every component.
|
|
23
23
|
- [rules.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/rules.json): 50 cardinal rules.
|
|
24
24
|
- [anti-ai-tells.json](https://raw.githubusercontent.com/godx-jp/godxjp-ui/main/agent/anti-ai-tells.json): 26 shapes that make generated UI look generated, each with its fix.
|
|
@@ -26,7 +26,7 @@ that can only fetch URLs.
|
|
|
26
26
|
## Pinning
|
|
27
27
|
|
|
28
28
|
Every URL above tracks `main`. To pin to the release a project actually installed, swap `main` for
|
|
29
|
-
the tag: `.../godx-jp/godxjp-ui/v31.
|
|
29
|
+
the tag: `.../godx-jp/godxjp-ui/v31.4.0/agent/...`. A catalog that does not match the installed
|
|
30
30
|
package describes props that are absent, or hides props that are present, and says nothing either way.
|
|
31
31
|
|
|
32
32
|
Pinned catalogs only exist for releases whose tag actually contains `agent/`. If `…/v<version>/agent/index.json` returns 404, that release predates this catalog: read `…/main/…` instead and compare `index.json` → `version` against the package you have, so you at least know which way it drifted.
|
package/agent/tokens.json
CHANGED
|
@@ -1625,6 +1625,12 @@
|
|
|
1625
1625
|
"tier": "semantic",
|
|
1626
1626
|
"value": "none"
|
|
1627
1627
|
},
|
|
1628
|
+
{
|
|
1629
|
+
"description": "The widest share of the header row the actions (`extra`) keep on ONE row before they wrap (gh#1025); the title keeps the rest. `initial` — default 60% at the call site.",
|
|
1630
|
+
"name": "--page-header-extra-max-measure",
|
|
1631
|
+
"tier": "semantic",
|
|
1632
|
+
"value": "initial"
|
|
1633
|
+
},
|
|
1628
1634
|
{
|
|
1629
1635
|
"description": "PageContainer `toolbar` — the fixed chrome band between the header and the (scrolling) body. ALL THREE knobs are quiet by default (rule #44): background — `transparent`, i.e. the band shows the page ground exactly as it did before the knob existed. It is bound at `:root` and NOT `initial`, unlike the divider below, because the default is a plain CSS keyword rather than another role token: there is nothing for a scoped [data-tenant]/.dark override to re-resolve, so the `initial` + call-site-fallback dance would buy nothing and only hide the default from anyone reading this file. A service that wants the band to read as its own surface — the design that asked for this knob puts a chat channel's workflow rail on the card ground — sets `--page-toolbar-background: hsl(var(--card));` ONCE in its theme. Never `className=\"bg-card\"` at the call site: that is hand-laid page chrome, it paints only under `variant=\"flush\"` gutters correctly by accident, and it cannot be re-themed per tenant. pad-block 0 — the band's ONLY breathing room, and the only one it should ever have. The band is chrome, so it sits FLUSH against the header above and the body below (src/styles/layout.css cancels the container's --page-band-gap from the band itself); there is deliberately no outside space to tune, because a ruled, painted band floating in a void divides nothing. A service that opts into the background or the divider gives the band its inset HERE (`--page-toolbar-pad-block: var(--space-2)`) instead of padding the strip at the call site. Still 0 by default: a transparent band is not a surface and has no inside to breathe, and under `fill` every pixel of it comes straight off the scroll viewport the slot exists to protect. divider — declared `initial` so the CALL SITE can fall back to --page-header-divider (`var(--page-toolbar-divider, var(--page-header-divider))`). One theme declaration therefore rules the whole page chrome consistently, and a scoped [data-tenant]/.dark override of the header divider still reaches the band — a `:root` binding would freeze it at the :root value (docs/TOKENS.md · \"Role-mirror knobs MUST be `initial`\"). Set `--page-toolbar-divider: none` to silence just this band.",
|
|
1630
1636
|
"name": "--page-toolbar-background",
|
|
@@ -9384,7 +9390,7 @@
|
|
|
9384
9390
|
"value": "initial"
|
|
9385
9391
|
},
|
|
9386
9392
|
{
|
|
9387
|
-
"description": "Counter pill beside the label (gh#602). Geometry matches Button/Toggle count pills;
|
|
9393
|
+
"description": "Counter pill beside the label (gh#602). Geometry matches Button/Toggle count pills; COLOUR follows `Tabs` (gh#1019): resting = `--background` / `--muted-foreground`, byte-identical to a resting `.ui-tabs-count` on a CARD tab — the Tabs rule for a pill whose host surface is `--muted`, which the Segmented track is (a `--muted` pill there is 1.00:1, shape gone). The selected segment's pill = `--primary` / `--primary-foreground`, identical to the active tab's. gh#602 had painted EVERY pill primary — four identical blobs that dropped the one thing a count pill in a one-of-N control can say, \"this is the chosen one\". The resting number is held to SC 1.4.3 (≥ 4.5:1 in both themes, measured in segmented-count-tabs-convention-1019.test.ts); it is not the selection indicator, so its fill only has to stand off the track, not reach 3:1. antd has no `count` on SegmentedItemType; this knob exists because every faceted filter in the wild needs one and nesting Badge in `label` was the only API until now.",
|
|
9388
9394
|
"name": "--segmented-count-min-width",
|
|
9389
9395
|
"tier": "component",
|
|
9390
9396
|
"value": "var(--button-count-min-width)"
|
|
@@ -9414,17 +9420,41 @@
|
|
|
9414
9420
|
"value": "var(--space-1)"
|
|
9415
9421
|
},
|
|
9416
9422
|
{
|
|
9417
|
-
"description": "
|
|
9423
|
+
"description": "default = hsl(var(--background)) at the call site",
|
|
9418
9424
|
"name": "--segmented-count-background",
|
|
9419
9425
|
"tier": "component",
|
|
9420
9426
|
"value": "initial"
|
|
9421
9427
|
},
|
|
9422
9428
|
{
|
|
9423
|
-
"description": "
|
|
9429
|
+
"description": "default = hsl(var(--muted-foreground)) at the call site",
|
|
9424
9430
|
"name": "--segmented-count-color",
|
|
9425
9431
|
"tier": "component",
|
|
9426
9432
|
"value": "initial"
|
|
9427
9433
|
},
|
|
9434
|
+
{
|
|
9435
|
+
"description": "default = hsl(var(--primary)) at the call site",
|
|
9436
|
+
"name": "--segmented-count-selected-background",
|
|
9437
|
+
"tier": "component",
|
|
9438
|
+
"value": "initial"
|
|
9439
|
+
},
|
|
9440
|
+
{
|
|
9441
|
+
"description": "default = 100% at the call site",
|
|
9442
|
+
"name": "--segmented-count-selected-background-alpha",
|
|
9443
|
+
"tier": "component",
|
|
9444
|
+
"value": "initial"
|
|
9445
|
+
},
|
|
9446
|
+
{
|
|
9447
|
+
"description": "default = hsl(var(--primary-foreground)) at the call site",
|
|
9448
|
+
"name": "--segmented-count-selected-color",
|
|
9449
|
+
"tier": "component",
|
|
9450
|
+
"value": "initial"
|
|
9451
|
+
},
|
|
9452
|
+
{
|
|
9453
|
+
"description": "default = var(--stroke-hairline) at the call site",
|
|
9454
|
+
"name": "--segmented-count-forced-outline-width",
|
|
9455
|
+
"tier": "component",
|
|
9456
|
+
"value": "initial"
|
|
9457
|
+
},
|
|
9428
9458
|
{
|
|
9429
9459
|
"description": "Rule weight, both orientations and both halves of a labelled rule. default = var(--stroke-hairline), resolved at the call site (gh#906)",
|
|
9430
9460
|
"name": "--separator-rule-size",
|
|
@@ -497,6 +497,7 @@ function Upload({
|
|
|
497
497
|
disabled,
|
|
498
498
|
onClick: openPicker,
|
|
499
499
|
"aria-label": triggerAriaLabel,
|
|
500
|
+
className: "ui-upload-trigger",
|
|
500
501
|
children: [
|
|
501
502
|
/* @__PURE__ */ jsx(
|
|
502
503
|
TriggerIcon,
|
|
@@ -553,7 +554,17 @@ function Upload({
|
|
|
553
554
|
hiddenInput,
|
|
554
555
|
liveRegion,
|
|
555
556
|
rejection && /* @__PURE__ */ jsx("p", { role: "alert", className: "text-error-strong", children: rejection }),
|
|
556
|
-
/* @__PURE__ */ jsx(
|
|
557
|
+
/* @__PURE__ */ jsx(
|
|
558
|
+
Button,
|
|
559
|
+
{
|
|
560
|
+
type: "button",
|
|
561
|
+
disabled,
|
|
562
|
+
variant: triggerVariant,
|
|
563
|
+
onClick: openPicker,
|
|
564
|
+
className: "ui-upload-trigger",
|
|
565
|
+
children: children ?? t("dataEntry.upload.addImage")
|
|
566
|
+
}
|
|
567
|
+
),
|
|
557
568
|
list
|
|
558
569
|
] });
|
|
559
570
|
}
|
|
@@ -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, bodied, 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, controls, listClassName, contentClassName, children, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, ...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
|
/**
|
|
@@ -24,7 +24,7 @@ export declare const TabsTrigger: React.ForwardRefExoticComponent<Omit<Omit<Reac
|
|
|
24
24
|
value: string;
|
|
25
25
|
/** RAC `isDisabled`; ALSO written to the button so `disabled:` utilities keep matching. */
|
|
26
26
|
disabled?: boolean;
|
|
27
|
-
} & React.RefAttributes<HTMLButtonElement>>;
|
|
27
|
+
} & Pick<TabItemProp, "count" | "countLabel" | "overflowCount" | "showZero"> & React.RefAttributes<HTMLButtonElement>>;
|
|
28
28
|
export declare const TabsContent: React.ForwardRefExoticComponent<Omit<Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref">, "value"> & {
|
|
29
29
|
/** The panel's key — must match a `TabsTrigger value`. RAC `id`. */
|
|
30
30
|
value: string;
|
|
@@ -41,11 +41,14 @@ function TabCount({
|
|
|
41
41
|
const { pill } = useCounterPill({ count, overflowCount, showZero, countLabel, slot: "tabs" });
|
|
42
42
|
return pill;
|
|
43
43
|
}
|
|
44
|
+
const NO_LIST_LABEL = {};
|
|
44
45
|
const TabsFrameContext = React.createContext({
|
|
45
46
|
orientation: "horizontal",
|
|
46
47
|
selectionSuppressed: false,
|
|
47
48
|
size: "md",
|
|
48
|
-
panels: null
|
|
49
|
+
panels: null,
|
|
50
|
+
controls: void 0,
|
|
51
|
+
listLabel: NO_LIST_LABEL
|
|
49
52
|
});
|
|
50
53
|
function resolveFallbackTabValue(items, requested) {
|
|
51
54
|
if (!items || items.length === 0) return requested;
|
|
@@ -122,9 +125,12 @@ function Tabs({
|
|
|
122
125
|
indicator,
|
|
123
126
|
moreIcon,
|
|
124
127
|
onTabScroll,
|
|
128
|
+
controls,
|
|
125
129
|
listClassName,
|
|
126
130
|
contentClassName,
|
|
127
131
|
children,
|
|
132
|
+
"aria-label": ariaLabel,
|
|
133
|
+
"aria-labelledby": ariaLabelledby,
|
|
128
134
|
...props
|
|
129
135
|
}) {
|
|
130
136
|
const { t } = useTranslation();
|
|
@@ -151,9 +157,20 @@ function Tabs({
|
|
|
151
157
|
return next;
|
|
152
158
|
});
|
|
153
159
|
}, []);
|
|
160
|
+
const listLabel = React.useMemo(
|
|
161
|
+
() => ariaLabel === void 0 && ariaLabelledby === void 0 ? NO_LIST_LABEL : { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby },
|
|
162
|
+
[ariaLabel, ariaLabelledby]
|
|
163
|
+
);
|
|
154
164
|
const frame = React.useMemo(
|
|
155
|
-
() => ({
|
|
156
|
-
|
|
165
|
+
() => ({
|
|
166
|
+
orientation: resolvedOrientation,
|
|
167
|
+
selectionSuppressed,
|
|
168
|
+
size,
|
|
169
|
+
panels,
|
|
170
|
+
controls,
|
|
171
|
+
listLabel
|
|
172
|
+
}),
|
|
173
|
+
[resolvedOrientation, selectionSuppressed, size, panels, controls, listLabel]
|
|
157
174
|
);
|
|
158
175
|
const editable = variant === "editable-card";
|
|
159
176
|
const collapsible = items != null && overflow === "menu";
|
|
@@ -444,7 +461,7 @@ function Tabs({
|
|
|
444
461
|
] }) : null,
|
|
445
462
|
extraEnd ? /* @__PURE__ */ jsx("div", { "data-slot": "tabs-extra", "data-side": "end", className: "ui-tabs-extra", children: extraEnd }) : null
|
|
446
463
|
] }) : list,
|
|
447
|
-
items.map((item) => {
|
|
464
|
+
controls !== void 0 ? null : items.map((item) => {
|
|
448
465
|
const keepMounted = !destroyOnHidden || item.forceRender === true;
|
|
449
466
|
return /* @__PURE__ */ jsx(
|
|
450
467
|
TabsContent,
|
|
@@ -465,6 +482,9 @@ function Tabs({
|
|
|
465
482
|
}
|
|
466
483
|
const TabsList = React.forwardRef(
|
|
467
484
|
({ className, variant = "default", loop: _loop, children, ...props }, ref) => {
|
|
485
|
+
const { listLabel } = React.useContext(TabsFrameContext);
|
|
486
|
+
const ownName = props["aria-label"] !== void 0 || props["aria-labelledby"] !== void 0;
|
|
487
|
+
const name = ownName ? { "aria-label": props["aria-label"], "aria-labelledby": props["aria-labelledby"] } : listLabel;
|
|
468
488
|
const listRef = React.useRef(null);
|
|
469
489
|
const setListRef = React.useCallback(
|
|
470
490
|
(node) => {
|
|
@@ -479,6 +499,8 @@ const TabsList = React.forwardRef(
|
|
|
479
499
|
AriaTabList,
|
|
480
500
|
{
|
|
481
501
|
ref: setListRef,
|
|
502
|
+
"aria-label": name["aria-label"],
|
|
503
|
+
"aria-labelledby": name["aria-labelledby"],
|
|
482
504
|
className: cn(
|
|
483
505
|
// `min-w-0 max-w-full` let the list shrink to (and never exceed) whatever width its
|
|
484
506
|
// ancestors actually give it instead of forcing them wider; horizontal orientation then
|
|
@@ -512,10 +534,22 @@ const TabsList = React.forwardRef(
|
|
|
512
534
|
);
|
|
513
535
|
TabsList.displayName = "TabsList";
|
|
514
536
|
const TabsTrigger = React.forwardRef(
|
|
515
|
-
({
|
|
516
|
-
|
|
537
|
+
({
|
|
538
|
+
className,
|
|
539
|
+
value,
|
|
540
|
+
disabled,
|
|
541
|
+
count,
|
|
542
|
+
overflowCount,
|
|
543
|
+
showZero,
|
|
544
|
+
countLabel,
|
|
545
|
+
children,
|
|
546
|
+
onKeyDown,
|
|
547
|
+
onClick,
|
|
548
|
+
...props
|
|
549
|
+
}, ref) => {
|
|
550
|
+
const { orientation, selectionSuppressed, size, panels, controls } = React.useContext(TabsFrameContext);
|
|
517
551
|
const controlsAPanel = panels === null || panels.has(value);
|
|
518
|
-
return /* @__PURE__ */
|
|
552
|
+
return /* @__PURE__ */ jsxs(
|
|
519
553
|
AriaTab,
|
|
520
554
|
{
|
|
521
555
|
ref,
|
|
@@ -565,8 +599,10 @@ const TabsTrigger = React.forwardRef(
|
|
|
565
599
|
// here for BOTH states, exactly as Radix did.
|
|
566
600
|
"data-state": renderProps.isSelected && !selectionSuppressed ? "active" : "inactive",
|
|
567
601
|
"aria-selected": renderProps.isSelected && !selectionSuppressed,
|
|
568
|
-
// RAC writes this unconditionally; see `TabsFrame.panels` (gh#643).
|
|
569
|
-
|
|
602
|
+
// RAC writes this unconditionally; see `TabsFrame.panels` (gh#643). With no panel
|
|
603
|
+
// declared, a strip-only root's `controls` region takes its place — on the SELECTED
|
|
604
|
+
// tab only, the same rule RAC applies to a real panel (gh#1021).
|
|
605
|
+
"aria-controls": controlsAPanel ? domProps["aria-controls"] : renderProps.isSelected && !selectionSuppressed ? controls : void 0,
|
|
570
606
|
"data-orientation": orientation,
|
|
571
607
|
disabled,
|
|
572
608
|
// CHAINED, not spread — the same reason `onKeyDown` below is. `withDomProps` lays the
|
|
@@ -591,7 +627,18 @@ const TabsTrigger = React.forwardRef(
|
|
|
591
627
|
},
|
|
592
628
|
tabIndex: props.tabIndex ?? domProps.tabIndex
|
|
593
629
|
}),
|
|
594
|
-
children
|
|
630
|
+
children: [
|
|
631
|
+
children,
|
|
632
|
+
/* @__PURE__ */ jsx(
|
|
633
|
+
TabCount,
|
|
634
|
+
{
|
|
635
|
+
count,
|
|
636
|
+
overflowCount,
|
|
637
|
+
showZero,
|
|
638
|
+
countLabel
|
|
639
|
+
}
|
|
640
|
+
)
|
|
641
|
+
]
|
|
595
642
|
}
|
|
596
643
|
);
|
|
597
644
|
}
|
|
@@ -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": "31.
|
|
3
|
+
"version": "31.4.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,
|
|
@@ -255,7 +255,11 @@ export type StepsSeparatorProp = "chevron" | "arrow";
|
|
|
255
255
|
export type TabItemProp = {
|
|
256
256
|
value: string;
|
|
257
257
|
label: React.ReactNode;
|
|
258
|
-
|
|
258
|
+
/**
|
|
259
|
+
* The panel body. Optional because a strip-only `Tabs` (`controls`, gh#1021) renders no panel
|
|
260
|
+
* at all — its content lives in the consumer's own region.
|
|
261
|
+
*/
|
|
262
|
+
content?: React.ReactNode;
|
|
259
263
|
disabled?: boolean;
|
|
260
264
|
/** Leading glyph inside the trigger (Ant Design `Tab.icon`). */
|
|
261
265
|
icon?: React.ReactNode;
|
|
@@ -514,6 +518,20 @@ export type TabsProp = {
|
|
|
514
518
|
moreIcon?: React.ReactNode;
|
|
515
519
|
/** Ant Design `onTabScroll`, on the logical axis. @see TabsOnScrollProp */
|
|
516
520
|
onTabScroll?: TabsOnScrollProp;
|
|
521
|
+
/**
|
|
522
|
+
* STRIP-ONLY MODE (gh#1021) — the `id` of the element OUTSIDE this `Tabs` that the strip
|
|
523
|
+
* drives: a status strip (未対応 / 対応中 / 完了) over a MasterDetail, a saved-view ribbon over a
|
|
524
|
+
* grid. With it set the `items` API renders the tablist ONLY — no `tabpanel`, empty or
|
|
525
|
+
* otherwise — and the selected tab's `aria-controls` points at this id, so the WAI-ARIA APG
|
|
526
|
+
* tab → controlled-region relationship stays real. The consumer's region is never mounted by
|
|
527
|
+
* the strip, so switching tabs cannot remount it (an unsent reply draft survives).
|
|
528
|
+
*
|
|
529
|
+
* Give the region an accessible name (`aria-label`/`aria-labelledby`, e.g. a `<section>`). In
|
|
530
|
+
* the compound form it applies to every trigger with no `TabsContent` declared for its value; a
|
|
531
|
+
* declared panel always wins. A pure FILTER with no region to point at is a `Segmented`
|
|
532
|
+
* (`radiogroup`), not a strip-only `Tabs`.
|
|
533
|
+
*/
|
|
534
|
+
controls?: IdProp;
|
|
517
535
|
className?: ClassNameProp;
|
|
518
536
|
listClassName?: ClassNameProp;
|
|
519
537
|
contentClassName?: ClassNameProp;
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -2626,7 +2626,7 @@ export declare const COMPONENT_PROP_REGISTRY: {
|
|
|
2626
2626
|
readonly TabsProp: {
|
|
2627
2627
|
readonly group: "navigation";
|
|
2628
2628
|
readonly file: "components/navigation.prop.ts";
|
|
2629
|
-
readonly vocabulary: readonly ["ValueProp", "DefaultValueProp", "OnValueChangeProp", "ClassNameProp", "SizeProp", "ExtraProp", {
|
|
2629
|
+
readonly vocabulary: readonly ["ValueProp", "DefaultValueProp", "OnValueChangeProp", "ClassNameProp", "SizeProp", "ExtraProp", "IdProp", {
|
|
2630
2630
|
readonly field: "bodied";
|
|
2631
2631
|
readonly local: true;
|
|
2632
2632
|
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.";
|
package/dist/props/registry.js
CHANGED
|
@@ -289,6 +289,18 @@
|
|
|
289
289
|
background: var(--skeleton-background, hsl(var(--muted)));
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
.ui-skeleton-line {
|
|
293
|
+
block-size: var(--skeleton-block-height);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.ui-skeleton-caption {
|
|
297
|
+
block-size: var(--skeleton-caption-height);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.ui-skeleton-title {
|
|
301
|
+
block-size: var(--skeleton-title-height);
|
|
302
|
+
}
|
|
303
|
+
|
|
292
304
|
.ui-skeleton-table {
|
|
293
305
|
overflow: hidden;
|
|
294
306
|
border: var(--stroke-hairline) solid hsl(var(--border));
|
|
@@ -327,18 +339,6 @@
|
|
|
327
339
|
border-radius: var(--skeleton-radius, var(--radius));
|
|
328
340
|
padding: var(--skeleton-card-inset);
|
|
329
341
|
|
|
330
|
-
.ui-skeleton-line {
|
|
331
|
-
block-size: var(--skeleton-block-height);
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
.ui-skeleton-caption {
|
|
335
|
-
block-size: var(--skeleton-caption-height);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
.ui-skeleton-title {
|
|
339
|
-
block-size: var(--skeleton-title-height);
|
|
340
|
-
}
|
|
341
|
-
|
|
342
342
|
.ui-skeleton-detail-label {
|
|
343
343
|
block-size: var(--skeleton-caption-height);
|
|
344
344
|
inline-size: var(--skeleton-label-width);
|
package/dist/styles/control.css
CHANGED
|
@@ -1282,13 +1282,29 @@
|
|
|
1282
1282
|
margin-inline-start: var(--segmented-count-gap);
|
|
1283
1283
|
border-radius: var(--segmented-count-radius);
|
|
1284
1284
|
padding-inline: var(--segmented-count-space-inline);
|
|
1285
|
-
|
|
1286
|
-
|
|
1285
|
+
|
|
1286
|
+
background: var(--segmented-count-background, hsl(var(--background)));
|
|
1287
|
+
color: var(--segmented-count-color, hsl(var(--muted-foreground)));
|
|
1287
1288
|
font-size: var(--segmented-count-font-size);
|
|
1288
1289
|
line-height: 1;
|
|
1289
1290
|
font-variant-numeric: tabular-nums;
|
|
1290
1291
|
}
|
|
1291
1292
|
|
|
1293
|
+
.ui-segmented-item[data-state="checked"] .ui-segmented-count {
|
|
1294
|
+
background: var(
|
|
1295
|
+
--segmented-count-selected-background,
|
|
1296
|
+
hsl(var(--primary) / var(--segmented-count-selected-background-alpha, 100%))
|
|
1297
|
+
);
|
|
1298
|
+
color: var(--segmented-count-selected-color, hsl(var(--primary-foreground)));
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
@media (forced-colors: active) {
|
|
1302
|
+
.ui-segmented-item[data-state="checked"] .ui-segmented-count {
|
|
1303
|
+
outline: var(--segmented-count-forced-outline-width, var(--stroke-hairline)) solid
|
|
1304
|
+
currentColor;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1292
1308
|
.ui-tag-input {
|
|
1293
1309
|
display: flex;
|
|
1294
1310
|
flex-wrap: wrap;
|
|
@@ -60,6 +60,9 @@
|
|
|
60
60
|
inline-size: 100%;
|
|
61
61
|
|
|
62
62
|
align-self: start;
|
|
63
|
+
|
|
64
|
+
--form-field-control-band: initial;
|
|
65
|
+
--form-field-first-line: initial;
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
.ui-form-field[data-layout="inline"] {
|
|
@@ -84,6 +87,40 @@
|
|
|
84
87
|
|
|
85
88
|
min-height: var(--control-height);
|
|
86
89
|
align-content: center;
|
|
90
|
+
|
|
91
|
+
grid-template-rows: minmax(var(--form-field-control-band, auto), auto);
|
|
92
|
+
align-items: center;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.ui-form-field-control:has(> .ui-form-field-note:first-child) {
|
|
96
|
+
grid-template-rows: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.ui-form-field-control
|
|
100
|
+
> fieldset
|
|
101
|
+
> :is(.ui-choice-group[data-orientation="vertical"], .ui-choice-field) {
|
|
102
|
+
align-self: start;
|
|
103
|
+
margin-block-start: max(
|
|
104
|
+
0px,
|
|
105
|
+
calc(
|
|
106
|
+
(
|
|
107
|
+
var(--form-field-control-band, 0px) - var(--font-size-base) *
|
|
108
|
+
var(--control-label-line-height)
|
|
109
|
+
) /
|
|
110
|
+
2
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.ui-form-field:has(
|
|
116
|
+
> .ui-form-field-control
|
|
117
|
+
> fieldset
|
|
118
|
+
> :is(.ui-control-multiline, [data-slot="textarea-affix-wrapper"])
|
|
119
|
+
) {
|
|
120
|
+
--form-field-first-line: calc(
|
|
121
|
+
var(--control-border-width, var(--stroke-hairline)) + var(--control-multiline-padding-block) +
|
|
122
|
+
0.5lh
|
|
123
|
+
);
|
|
87
124
|
}
|
|
88
125
|
|
|
89
126
|
.ui-form-field-control > * {
|
|
@@ -96,13 +133,14 @@
|
|
|
96
133
|
) {
|
|
97
134
|
grid-template-columns: var(--form-label-width, max-content) minmax(0, 1fr);
|
|
98
135
|
align-items: start;
|
|
136
|
+
--form-field-control-band: var(--control-height);
|
|
99
137
|
column-gap: var(--form-label-gap);
|
|
100
138
|
row-gap: var(--space-stack-xs);
|
|
101
139
|
}
|
|
102
140
|
|
|
103
141
|
.ui-form-field[data-collapse-below="false"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
104
142
|
> .ui-form-field-label {
|
|
105
|
-
min-block-size: var(--control-height);
|
|
143
|
+
min-block-size: calc(2 * var(--form-field-first-line, var(--control-height) / 2));
|
|
106
144
|
flex-wrap: wrap;
|
|
107
145
|
}
|
|
108
146
|
.ui-form-field[data-collapse-below="false"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
@@ -131,12 +169,13 @@
|
|
|
131
169
|
) {
|
|
132
170
|
grid-template-columns: var(--form-label-width, max-content) minmax(0, 1fr);
|
|
133
171
|
align-items: start;
|
|
172
|
+
--form-field-control-band: var(--control-height);
|
|
134
173
|
column-gap: var(--form-label-gap);
|
|
135
174
|
row-gap: var(--space-stack-xs);
|
|
136
175
|
}
|
|
137
176
|
.ui-form-field[data-collapse-below="sm"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
138
177
|
> .ui-form-field-label {
|
|
139
|
-
min-block-size: var(--control-height);
|
|
178
|
+
min-block-size: calc(2 * var(--form-field-first-line, var(--control-height) / 2));
|
|
140
179
|
flex-wrap: wrap;
|
|
141
180
|
}
|
|
142
181
|
.ui-form-field[data-collapse-below="sm"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
@@ -165,12 +204,13 @@
|
|
|
165
204
|
) {
|
|
166
205
|
grid-template-columns: var(--form-label-width, max-content) minmax(0, 1fr);
|
|
167
206
|
align-items: start;
|
|
207
|
+
--form-field-control-band: var(--control-height);
|
|
168
208
|
column-gap: var(--form-label-gap);
|
|
169
209
|
row-gap: var(--space-stack-xs);
|
|
170
210
|
}
|
|
171
211
|
.ui-form-field[data-collapse-below="md"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
172
212
|
> .ui-form-field-label {
|
|
173
|
-
min-block-size: var(--control-height);
|
|
213
|
+
min-block-size: calc(2 * var(--form-field-first-line, var(--control-height) / 2));
|
|
174
214
|
flex-wrap: wrap;
|
|
175
215
|
}
|
|
176
216
|
.ui-form-field[data-collapse-below="md"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
@@ -199,12 +239,13 @@
|
|
|
199
239
|
) {
|
|
200
240
|
grid-template-columns: var(--form-label-width, max-content) minmax(0, 1fr);
|
|
201
241
|
align-items: start;
|
|
242
|
+
--form-field-control-band: var(--control-height);
|
|
202
243
|
column-gap: var(--form-label-gap);
|
|
203
244
|
row-gap: var(--space-stack-xs);
|
|
204
245
|
}
|
|
205
246
|
.ui-form-field[data-collapse-below="lg"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
206
247
|
> .ui-form-field-label {
|
|
207
|
-
min-block-size: var(--control-height);
|
|
248
|
+
min-block-size: calc(2 * var(--form-field-first-line, var(--control-height) / 2));
|
|
208
249
|
flex-wrap: wrap;
|
|
209
250
|
}
|
|
210
251
|
.ui-form-field[data-collapse-below="lg"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
@@ -233,12 +274,13 @@
|
|
|
233
274
|
) {
|
|
234
275
|
grid-template-columns: var(--form-label-width, max-content) minmax(0, 1fr);
|
|
235
276
|
align-items: start;
|
|
277
|
+
--form-field-control-band: var(--control-height);
|
|
236
278
|
column-gap: var(--form-label-gap);
|
|
237
279
|
row-gap: var(--space-stack-xs);
|
|
238
280
|
}
|
|
239
281
|
.ui-form-field[data-collapse-below="xl"]:is([data-layout="horizontal"], [data-layout="inline"])
|
|
240
282
|
> .ui-form-field-label {
|
|
241
|
-
min-block-size: var(--control-height);
|
|
283
|
+
min-block-size: calc(2 * var(--form-field-first-line, var(--control-height) / 2));
|
|
242
284
|
flex-wrap: wrap;
|
|
243
285
|
}
|
|
244
286
|
.ui-form-field[data-collapse-below="xl"]:is([data-layout="horizontal"], [data-layout="inline"])
|
package/dist/styles/layers.json
CHANGED
package/dist/styles/layout.css
CHANGED
|
@@ -46,4 +46,8 @@
|
|
|
46
46
|
--segmented-count-gap: var(--space-1);
|
|
47
47
|
--segmented-count-background: initial;
|
|
48
48
|
--segmented-count-color: initial;
|
|
49
|
+
--segmented-count-selected-background: initial;
|
|
50
|
+
--segmented-count-selected-background-alpha: initial;
|
|
51
|
+
--segmented-count-selected-color: initial;
|
|
52
|
+
--segmented-count-forced-outline-width: initial;
|
|
49
53
|
}
|
|
@@ -90,6 +90,8 @@ export default function Demo() {
|
|
|
90
90
|
<CardDescription>
|
|
91
91
|
一覧の上の状態フィルター。4 つが 1 行に収まらない幅では、トラックが 2 行目に折り返し
|
|
92
92
|
ます。ラベルも件数も切り詰めません。`check:segmented-wrap` がこのカードを測ります。
|
|
93
|
+
件数は Tabs と同じ扱いで、選択中の項目だけ強調し、ほかは控えめな色です。0 件を隠す
|
|
94
|
+
ときは `showZero: false`。
|
|
93
95
|
</CardDescription>
|
|
94
96
|
</CardHeader>
|
|
95
97
|
<CardContent>
|
package/docs/navigation/tabs.tsx
CHANGED
|
@@ -146,6 +146,7 @@ export default function Demo() {
|
|
|
146
146
|
},
|
|
147
147
|
]);
|
|
148
148
|
const [editableTab, setEditableTab] = useState("je-0042");
|
|
149
|
+
const [inboxStatus, setInboxStatus] = useState("open");
|
|
149
150
|
|
|
150
151
|
return (
|
|
151
152
|
<PageContainer
|
|
@@ -646,6 +647,48 @@ export default function Demo() {
|
|
|
646
647
|
</CardContent>
|
|
647
648
|
</Card>
|
|
648
649
|
|
|
650
|
+
{/* STRIP-ONLY (gh#1021). The strip drives a region OUTSIDE the Tabs, so no tabpanel is
|
|
651
|
+
rendered and the view below is never remounted by a tab switch. */}
|
|
652
|
+
<Card>
|
|
653
|
+
<CardHeader>
|
|
654
|
+
<CardTitle level={2}>controls · タブ列だけ(外部の領域を切り替える)</CardTitle>
|
|
655
|
+
<CardDescription>
|
|
656
|
+
controls に外部領域の id を渡すと tabpanel を描かず、選択中のタブの aria-controls
|
|
657
|
+
がその領域を指す。領域は Tabs の外にあるのでタブを切り替えても再マウントされない。
|
|
658
|
+
compound の TabsTrigger も items と同じ count を描く。
|
|
659
|
+
</CardDescription>
|
|
660
|
+
</CardHeader>
|
|
661
|
+
<CardContent>
|
|
662
|
+
<Flex direction="col" gap="md">
|
|
663
|
+
<Tabs
|
|
664
|
+
aria-label="対応状況"
|
|
665
|
+
controls="tabs-inbox-view"
|
|
666
|
+
variant="line"
|
|
667
|
+
value={inboxStatus}
|
|
668
|
+
onValueChange={setInboxStatus}
|
|
669
|
+
items={[
|
|
670
|
+
{ value: "open", label: "未対応", count: 12, countLabel: "件" },
|
|
671
|
+
{ value: "doing", label: "対応中", count: 3, countLabel: "件" },
|
|
672
|
+
{ value: "done", label: "完了", count: 128, countLabel: "件" },
|
|
673
|
+
]}
|
|
674
|
+
/>
|
|
675
|
+
<section id="tabs-inbox-view" aria-label="スレッド一覧">
|
|
676
|
+
<Text as="p">表示中: {inboxStatus}</Text>
|
|
677
|
+
</section>
|
|
678
|
+
<Tabs aria-label="保存ビュー" controls="tabs-inbox-view" defaultValue="mine">
|
|
679
|
+
<TabsList variant="line">
|
|
680
|
+
<TabsTrigger value="mine" count={4} countLabel="件">
|
|
681
|
+
自分の担当
|
|
682
|
+
</TabsTrigger>
|
|
683
|
+
<TabsTrigger value="all" count={143} countLabel="件">
|
|
684
|
+
すべて
|
|
685
|
+
</TabsTrigger>
|
|
686
|
+
</TabsList>
|
|
687
|
+
</Tabs>
|
|
688
|
+
</Flex>
|
|
689
|
+
</CardContent>
|
|
690
|
+
</Card>
|
|
691
|
+
|
|
649
692
|
<Card>
|
|
650
693
|
<CardHeader>
|
|
651
694
|
<CardTitle level={2}>indicator · animated</CardTitle>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
|
-
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { QueryClient, QueryClientProvider, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
Badge,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type ColumnDef,
|
|
14
14
|
} from "@godxjp/ui/data-display";
|
|
15
15
|
import { SkeletonTable } from "@godxjp/ui/feedback";
|
|
16
|
-
import { Text } from "@godxjp/ui/general";
|
|
16
|
+
import { Button, Text } from "@godxjp/ui/general";
|
|
17
17
|
import { Flex, PageContainer } from "@godxjp/ui/layout";
|
|
18
18
|
import { AuthExpiryProvider, DataState } from "@godxjp/ui/query";
|
|
19
19
|
|
|
@@ -51,15 +51,36 @@ function SuccessBlock() {
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/** Loads for 2s and comes back empty — the whole lifecycle: skeleton → EmptyState. */
|
|
55
|
+
const LOAD_MS = 2000;
|
|
56
|
+
|
|
54
57
|
function LoadingBlock() {
|
|
58
|
+
const client = useQueryClient();
|
|
55
59
|
const query = useQuery({
|
|
56
60
|
queryKey: ["ds-loading"],
|
|
57
|
-
queryFn: () => new Promise<Invoice[]>(() =>
|
|
61
|
+
queryFn: () => new Promise<Invoice[]>((resolve) => setTimeout(() => resolve([]), LOAD_MS)),
|
|
58
62
|
});
|
|
59
63
|
return (
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
<Flex direction="col" gap="sm">
|
|
65
|
+
<DataState
|
|
66
|
+
query={query}
|
|
67
|
+
skeleton={<SkeletonTable rows={4} columns={3} />}
|
|
68
|
+
isEmpty={(d) => d.length === 0}
|
|
69
|
+
empty={
|
|
70
|
+
<EmptyState title="データがありません" description="条件に合う請求書はありません。" />
|
|
71
|
+
}
|
|
72
|
+
>
|
|
73
|
+
{(d) => <DataTable data={d} columns={columns} getRowId={(r) => r.id} />}
|
|
74
|
+
</DataState>
|
|
75
|
+
{/* resetQueries puts the query back to pending, so the skeleton plays again. */}
|
|
76
|
+
<Button
|
|
77
|
+
variant="outline"
|
|
78
|
+
size="sm"
|
|
79
|
+
onClick={() => void client.resetQueries({ queryKey: ["ds-loading"] })}
|
|
80
|
+
>
|
|
81
|
+
もう一度読み込む
|
|
82
|
+
</Button>
|
|
83
|
+
</Flex>
|
|
63
84
|
);
|
|
64
85
|
}
|
|
65
86
|
|
|
@@ -174,8 +195,11 @@ export default function Demo() {
|
|
|
174
195
|
|
|
175
196
|
<Card>
|
|
176
197
|
<CardHeader>
|
|
177
|
-
<CardTitle level={2}>Loading
|
|
178
|
-
<CardDescription>
|
|
198
|
+
<CardTitle level={2}>Loading → empty</CardTitle>
|
|
199
|
+
<CardDescription>
|
|
200
|
+
The skeleton (animated) renders while pending; when the data arrives empty the
|
|
201
|
+
skeleton goes away and the `empty` slot renders.
|
|
202
|
+
</CardDescription>
|
|
179
203
|
</CardHeader>
|
|
180
204
|
<CardContent flush>
|
|
181
205
|
<LoadingBlock />
|
|
@@ -367,19 +367,14 @@ const DOT_CLASS: Record<SavedView["dot"], string> = {
|
|
|
367
367
|
neutral: "bg-muted-foreground",
|
|
368
368
|
};
|
|
369
369
|
|
|
370
|
-
// TabsTrigger renders a <button>, whose content model is phrasing content only — so the row
|
|
371
|
-
//
|
|
372
|
-
//
|
|
373
|
-
function ViewTrigger({ view
|
|
370
|
+
// TabsTrigger renders a <button>, whose content model is phrasing content only — so the row takes
|
|
371
|
+
// `as="span"`. The count is the trigger's own `count` slot (gh#1021), drawn exactly as the `items`
|
|
372
|
+
// API draws it, rather than a hand-placed Badge.
|
|
373
|
+
function ViewTrigger({ view }: { view: SavedView }) {
|
|
374
374
|
return (
|
|
375
375
|
<Flex as="span" align="center" gap="sm">
|
|
376
376
|
<span aria-hidden="true" className={`size-2 rounded-full ${DOT_CLASS[view.dot]}`} />
|
|
377
377
|
<span>{view.label}</span>
|
|
378
|
-
<Badge as="span" tone="neutral" shape="pill">
|
|
379
|
-
<Text as="span" size="2xs" weight="medium" tabular>
|
|
380
|
-
{count}
|
|
381
|
-
</Text>
|
|
382
|
-
</Badge>
|
|
383
378
|
</Flex>
|
|
384
379
|
);
|
|
385
380
|
}
|
|
@@ -432,23 +427,30 @@ export default function Demo() {
|
|
|
432
427
|
}
|
|
433
428
|
>
|
|
434
429
|
<Flex direction="col" gap="md">
|
|
435
|
-
{/* Saved-view ribbon — controlled Tabs
|
|
436
|
-
|
|
430
|
+
{/* Saved-view ribbon — controlled, strip-only Tabs (gh#1021): `controls` points the
|
|
431
|
+
selected tab at the grid below, which lives outside the Tabs and is never remounted. */}
|
|
432
|
+
<Tabs
|
|
433
|
+
aria-label="保存ビュー"
|
|
434
|
+
controls="attendance-grid"
|
|
435
|
+
value={view}
|
|
436
|
+
onValueChange={(v) => setView(v as ViewId)}
|
|
437
|
+
>
|
|
437
438
|
<TabsList variant="line" className="h-auto w-full flex-wrap justify-start border-b">
|
|
438
439
|
{VIEWS.map((v) => (
|
|
439
440
|
<TabsTrigger
|
|
440
441
|
key={v.id}
|
|
441
442
|
value={v.id}
|
|
443
|
+
count={counts[v.id]}
|
|
442
444
|
className="rounded-[var(--tabs-trigger-line-radius)] px-[var(--tabs-trigger-line-padding-x)] py-[var(--tabs-trigger-line-padding-y)]"
|
|
443
445
|
>
|
|
444
|
-
<ViewTrigger view={v}
|
|
446
|
+
<ViewTrigger view={v} />
|
|
445
447
|
</TabsTrigger>
|
|
446
448
|
))}
|
|
447
449
|
</TabsList>
|
|
448
450
|
</Tabs>
|
|
449
451
|
|
|
450
452
|
{/* The grid — columns + data both come from the active saved view. */}
|
|
451
|
-
<Card>
|
|
453
|
+
<Card id="attendance-grid" role="region" aria-label={active.label}>
|
|
452
454
|
<CardContent flush>
|
|
453
455
|
<DataTable
|
|
454
456
|
data={rows}
|
package/docs/themes/flat.css
CHANGED
|
@@ -235,7 +235,9 @@
|
|
|
235
235
|
--conversations-item-active-background: hsl(220 20% 90%);
|
|
236
236
|
--conversations-item-active-color: hsl(220 26% 12%);
|
|
237
237
|
--tabs-count-active-background: hsl(220 20% 86%);
|
|
238
|
+
--segmented-count-selected-background: hsl(220 20% 86%);
|
|
238
239
|
--tabs-count-active-color: hsl(220 26% 12%);
|
|
240
|
+
--segmented-count-selected-color: hsl(220 26% 12%);
|
|
239
241
|
--toggle-soft-hover-background: hsl(220 16% 90%);
|
|
240
242
|
--toggle-pressed-count-background: hsl(220 20% 84%);
|
|
241
243
|
--toggle-pressed-count-color: hsl(220 26% 12%);
|
|
@@ -419,7 +421,9 @@
|
|
|
419
421
|
--conversations-item-active-background: hsl(220 16% 22%);
|
|
420
422
|
--conversations-item-active-color: hsl(220 20% 94%);
|
|
421
423
|
--tabs-count-active-background: hsl(220 16% 26%);
|
|
424
|
+
--segmented-count-selected-background: hsl(220 16% 26%);
|
|
422
425
|
--tabs-count-active-color: hsl(220 20% 94%);
|
|
426
|
+
--segmented-count-selected-color: hsl(220 20% 94%);
|
|
423
427
|
--toggle-soft-hover-background: hsl(220 14% 22%);
|
|
424
428
|
--toggle-pressed-count-background: hsl(220 16% 26%);
|
|
425
429
|
--toggle-pressed-count-color: hsl(220 20% 94%);
|
|
@@ -613,7 +613,9 @@
|
|
|
613
613
|
--conversations-item-active-background-alpha: 18%;
|
|
614
614
|
--conversations-item-active-color: hsl(var(--glass-ink));
|
|
615
615
|
--tabs-count-active-background-alpha: 24%;
|
|
616
|
+
--segmented-count-selected-background-alpha: 24%;
|
|
616
617
|
--tabs-count-active-color: hsl(var(--glass-ink));
|
|
618
|
+
--segmented-count-selected-color: hsl(var(--glass-ink));
|
|
617
619
|
--toggle-pressed-count-background-alpha: 26%;
|
|
618
620
|
--toggle-pressed-count-color: hsl(var(--glass-ink));
|
|
619
621
|
--toggle-pressed-border-color-alpha: 45%;
|
|
@@ -279,7 +279,9 @@
|
|
|
279
279
|
--conversations-item-active-background: hsl(48 90% 78%);
|
|
280
280
|
--conversations-item-active-color: hsl(0 0% 0%);
|
|
281
281
|
--tabs-count-active-background: hsl(48 90% 74%);
|
|
282
|
+
--segmented-count-selected-background: hsl(48 90% 74%);
|
|
282
283
|
--tabs-count-active-color: hsl(0 0% 0%);
|
|
284
|
+
--segmented-count-selected-color: hsl(0 0% 0%);
|
|
283
285
|
--toggle-soft-hover-background: hsl(48 90% 84%);
|
|
284
286
|
--toggle-pressed-count-background: hsl(48 90% 74%);
|
|
285
287
|
--toggle-pressed-count-color: hsl(0 0% 0%);
|
|
@@ -331,6 +333,7 @@
|
|
|
331
333
|
--mega-menu-link-hover-background: hsl(from hsl(var(--primary)) h 90% 82%);
|
|
332
334
|
--conversations-item-active-background: hsl(from hsl(var(--primary)) h 90% 78%);
|
|
333
335
|
--tabs-count-active-background: hsl(from hsl(var(--primary)) h 90% 74%);
|
|
336
|
+
--segmented-count-selected-background: hsl(from hsl(var(--primary)) h 90% 74%);
|
|
334
337
|
--toggle-soft-hover-background: hsl(from hsl(var(--primary)) h 90% 84%);
|
|
335
338
|
--toggle-pressed-count-background: hsl(from hsl(var(--primary)) h 90% 74%);
|
|
336
339
|
}
|
|
@@ -423,7 +426,9 @@
|
|
|
423
426
|
--conversations-item-active-background: hsl(48 90% 26%);
|
|
424
427
|
--conversations-item-active-color: hsl(0 0% 100%);
|
|
425
428
|
--tabs-count-active-background: hsl(48 90% 26%);
|
|
429
|
+
--segmented-count-selected-background: hsl(48 90% 26%);
|
|
426
430
|
--tabs-count-active-color: hsl(0 0% 100%);
|
|
431
|
+
--segmented-count-selected-color: hsl(0 0% 100%);
|
|
427
432
|
--toggle-soft-hover-background: hsl(48 90% 24%);
|
|
428
433
|
--toggle-pressed-count-background: hsl(48 90% 26%);
|
|
429
434
|
--toggle-pressed-count-color: hsl(0 0% 100%);
|
|
@@ -469,6 +474,7 @@
|
|
|
469
474
|
--mega-menu-link-hover-background: hsl(from hsl(var(--primary)) h 90% 26%);
|
|
470
475
|
--conversations-item-active-background: hsl(from hsl(var(--primary)) h 90% 26%);
|
|
471
476
|
--tabs-count-active-background: hsl(from hsl(var(--primary)) h 90% 26%);
|
|
477
|
+
--segmented-count-selected-background: hsl(from hsl(var(--primary)) h 90% 26%);
|
|
472
478
|
--toggle-soft-hover-background: hsl(from hsl(var(--primary)) h 90% 24%);
|
|
473
479
|
--toggle-pressed-count-background: hsl(from hsl(var(--primary)) h 90% 26%);
|
|
474
480
|
}
|