@godxjp/ui 33.8.0 → 33.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/agent/START-HERE.md +2 -2
- package/agent/components/RichTextEditor.json +73 -0
- package/agent/components/Tabs.json +9 -2
- package/agent/components/TranslatableField.json +69 -0
- package/agent/components/TranslatableRichText.json +70 -0
- package/agent/components-index.json +15 -0
- package/agent/components.json +221 -2
- package/agent/index.json +4 -4
- package/agent/llms.txt +5 -5
- package/dist/components/data-entry/index.d.ts +2 -2
- package/dist/components/data-entry/index.js +2 -1
- package/dist/components/data-entry/translatable-input.d.ts +24 -20
- package/dist/components/data-entry/translatable-input.js +8 -21
- package/dist/components/navigation/tabs-scroll.d.ts +1 -5
- package/dist/components/navigation/tabs-scroll.js +31 -8
- package/dist/components/navigation/tabs.d.ts +1 -1
- package/dist/components/navigation/tabs.js +264 -89
- package/dist/components/navigation/toolbar.js +1 -1
- package/dist/contracts/measurement.json +1 -1
- package/dist/i18n/messages/en.json +2 -1
- package/dist/i18n/messages/ja.json +2 -1
- package/dist/i18n/messages/vi.json +2 -1
- package/dist/props/components/data-entry.prop.d.ts +49 -5
- package/dist/props/components/navigation.prop.d.ts +20 -1
- package/dist/props/registry.d.ts +59 -0
- package/dist/props/registry.js +86 -0
- package/dist/styles/data-display-layout.css +41 -0
- package/dist/styles/layers.json +39 -1
- package/dist/styles/layout.css +9 -0
- package/dist/styles.css +1 -1
- package/package.json +2 -2
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
DropdownMenuTrigger
|
|
20
20
|
} from "./dropdown-menu.js";
|
|
21
21
|
import {
|
|
22
|
+
syncActiveTabIntoView,
|
|
22
23
|
useKeepActiveTabVisible,
|
|
23
24
|
useTabsOverflowValues,
|
|
24
25
|
useTabsScrollReporter
|
|
@@ -41,6 +42,8 @@ function TabCount({
|
|
|
41
42
|
const { pill } = useCounterPill({ count, overflowCount, showZero, countLabel, slot: "tabs" });
|
|
42
43
|
return pill;
|
|
43
44
|
}
|
|
45
|
+
const TABS_LIST_BASE_CLASS_NAME = "group/tabs-list text-muted-foreground inline-flex w-fit max-w-full min-w-0 items-center justify-center-safe rounded-lg p-1 group-data-[orientation=vertical]/tabs:flex-col data-[orientation=horizontal]:[scrollbar-width:none] data-[orientation=horizontal]:overflow-x-auto data-[orientation=horizontal]:overflow-y-hidden data-[variant=line]:gap-[var(--tabs-list-line-space-gap)] data-[variant=line]:rounded-none data-[variant=line]:bg-transparent [&[data-orientation=horizontal]::-webkit-scrollbar]:hidden";
|
|
46
|
+
const TABS_TRIGGER_BASE_CLASS_NAME = "text-muted-foreground ring-offset-background hover:text-foreground ui-focus-ring 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-transparent px-3 py-1 text-sm font-medium whitespace-nowrap transition-all data-[overflow-hidden]:transition-none data-[overflow-hidden]:opacity-0 data-[overflow-hidden]:pointer-events-none 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";
|
|
44
47
|
const NO_LIST_LABEL = {};
|
|
45
48
|
const TabsFrameContext = React.createContext({
|
|
46
49
|
orientation: "horizontal",
|
|
@@ -126,6 +129,7 @@ function Tabs({
|
|
|
126
129
|
moreIcon,
|
|
127
130
|
onTabScroll,
|
|
128
131
|
controls,
|
|
132
|
+
linkComponent,
|
|
129
133
|
listClassName,
|
|
130
134
|
contentClassName,
|
|
131
135
|
children,
|
|
@@ -173,7 +177,8 @@ function Tabs({
|
|
|
173
177
|
[resolvedOrientation, selectionSuppressed, size, panels, controls, listLabel]
|
|
174
178
|
);
|
|
175
179
|
const editable = variant === "editable-card";
|
|
176
|
-
const
|
|
180
|
+
const routeStrip = items != null && items.some((item) => item.href !== void 0);
|
|
181
|
+
const collapsible = items != null && overflow === "menu" && !routeStrip;
|
|
177
182
|
const listRef = React.useRef(null);
|
|
178
183
|
const [hiddenValues, setHiddenValues] = React.useState([]);
|
|
179
184
|
const itemValues = React.useMemo(
|
|
@@ -214,53 +219,99 @@ function Tabs({
|
|
|
214
219
|
start: "rounded-s-[var(--tabs-card-radius)] rounded-e-none data-[state=active]:border-e-[hsl(var(--tabs-panel-background,var(--background)))]",
|
|
215
220
|
end: "rounded-e-[var(--tabs-card-radius)] rounded-s-none data-[state=active]:border-s-[hsl(var(--tabs-panel-background,var(--background)))]"
|
|
216
221
|
};
|
|
222
|
+
const itemsListClassName = cn(
|
|
223
|
+
// The inset override is an arbitrary value reading the knob (never `p-0`): it must
|
|
224
|
+
// stay a UTILITY so tailwind-merge still drops the strip's own padding step.
|
|
225
|
+
//
|
|
226
|
+
// IT IS SPLIT INTO `px-`/`py-` FOR THE FOCUS RING (gh#376). The strip clips its
|
|
227
|
+
// block axis (`overflow-y: hidden` beside `overflow-x: auto`), and this variant's
|
|
228
|
+
// inset is 0px, so a focused trigger had NO block headroom at all — measured in
|
|
229
|
+
// Chromium, 0px top and bottom, i.e. the entire ring was shaved. The headroom has to
|
|
230
|
+
// come from layout: `overflow-y: visible` next to `overflow-x: auto` computes back
|
|
231
|
+
// to `auto` per spec (a second scroll container, not a ring), and Chromium honours
|
|
232
|
+
// `overflow-clip-margin` only when BOTH axes are `clip`. So the block padding carries
|
|
233
|
+
// the ring's outer reach and an equal negative block margin hands it straight back —
|
|
234
|
+
// the ring paints inside the scrollport and the strip's OUTER box is unchanged
|
|
235
|
+
// (measured: 38px before and after). Both values read the same token the ring is
|
|
236
|
+
// sized from, so the two can never drift apart.
|
|
237
|
+
//
|
|
238
|
+
// The cost, recorded rather than hidden: the rail hairline (`border-b`, drawn at the
|
|
239
|
+
// border box) now sits the ring's reach below the triggers instead of flush. That is
|
|
240
|
+
// where a hand-composed <TabsList variant="line"> already put it — its `p-1` gives
|
|
241
|
+
// the same block inset — so the two construction paths now agree. A service that
|
|
242
|
+
// wants the active bar parked back on the hairline raises `--tabs-indicator-offset`,
|
|
243
|
+
// which exists for exactly that.
|
|
244
|
+
// `w-full` is scoped to the HORIZONTAL axis. Unscoped it also applied when the root is
|
|
245
|
+
// vertical, where the strip is a COLUMN beside the panel: `width: 100%` made the strip
|
|
246
|
+
// claim the whole row and the panel collapsed to its own min-content floor. Measured at a
|
|
247
|
+
// 1232px root: strip 1133.72px (92%) / panel 90.28px (7%). And it is not a layer problem —
|
|
248
|
+
// tailwind-merge drops the base `w-fit` as a same-group conflict, so `w-fit` never reached
|
|
249
|
+
// the DOM at all. Scoping puts it back on the axis it was written for.
|
|
250
|
+
variant === "line" && "my-[calc(-1_*_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] h-auto justify-start border-b px-[var(--tabs-list-line-space-inset)] py-[calc(var(--tabs-list-line-space-inset)_+_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] data-[orientation=horizontal]:w-full",
|
|
251
|
+
// CARD strip. The list keeps `data-variant="default"` on purpose (a hand-composed
|
|
252
|
+
// <TabsList> must be unaffected), so the card face is selected from the ROOT — but the
|
|
253
|
+
// three properties the base list already claims as utilities (`bg-muted`, `p-1`,
|
|
254
|
+
// `rounded-lg`) have to be replaced with utilities too, or the components layer loses.
|
|
255
|
+
card && "items-end justify-start gap-[var(--tabs-card-list-space-gap)] rounded-[var(--tabs-card-list-radius)] p-[var(--tabs-card-list-space-inset)] data-[orientation=horizontal]:w-full data-[variant=default]:bg-transparent",
|
|
256
|
+
// CENTERED. Two independent moves, because the strip has two shapes: the pill/card strip
|
|
257
|
+
// is `w-fit` (auto inline margins centre the BOX) and the line strip is `w-full`
|
|
258
|
+
// (`justify-content` centres its CONTENT). `safe` keeps the overflow rule the strip
|
|
259
|
+
// already depends on — plain `center` strands the leading tab outside the scrollport.
|
|
260
|
+
centered && "mx-auto justify-center-safe",
|
|
261
|
+
listClassName
|
|
262
|
+
);
|
|
263
|
+
const itemTriggerClassName = cn(
|
|
264
|
+
// Geometry only — the active underline is the token-owned `::after` bar the
|
|
265
|
+
// line variant already owns (never a second, hand-rolled border-b indicator).
|
|
266
|
+
// They are arbitrary-value utilities so tailwind-merge keeps dropping the pill
|
|
267
|
+
// trigger's own radius / padding steps from the base class list.
|
|
268
|
+
variant === "line" && "rounded-[var(--tabs-trigger-line-radius)] px-[var(--tabs-trigger-line-padding-x)] py-[var(--tabs-trigger-line-padding-y)]",
|
|
269
|
+
sizeTriggerClassName,
|
|
270
|
+
// A centred strip must let its triggers SHRINK to their labels first. The base
|
|
271
|
+
// trigger is `flex-1`, so on a full-width (`line`) strip they already fill every
|
|
272
|
+
// pixel and `justify-content` has nothing left to centre — measured, the leading and
|
|
273
|
+
// trailing gaps were both 0px with `centered` on and off.
|
|
274
|
+
centered && "flex-none",
|
|
275
|
+
// LINE face: same argument as the card face below, and from the same source — antd
|
|
276
|
+
// puts no `flex-grow` on a tab of ANY `type`, so a line tab is content-width too.
|
|
277
|
+
// `card` was fixed on that reasoning (gh#736); `line` was left on the base `flex-1`
|
|
278
|
+
// and kept stretching. Measured by a consumer on a 1200px strip: three tabs at
|
|
279
|
+
// 321px each for labels needing 83 / 84 / 56px, so the active underline ran 321px
|
|
280
|
+
// under a 56px label.
|
|
281
|
+
//
|
|
282
|
+
// `centered` is NOT the escape hatch for this: it centres, which trades one wrong
|
|
283
|
+
// geometry for another, and it is antd's prop for centring rather than for width.
|
|
284
|
+
variant === "line" && "flex-none",
|
|
285
|
+
// The merged edge is the SURFACE colour, never `transparent`: the rail is an inset
|
|
286
|
+
// shadow at the strip's padding-box edge, so a see-through border would let that 1px
|
|
287
|
+
// of rail run straight across the tab it is joined to.
|
|
288
|
+
// CARD face: a real boundary on every side, opened toward the panel by
|
|
289
|
+
// `CARD_FACE`. `flex-none` is antd's geometry, not a preference — its
|
|
290
|
+
// `nav-list` is a plain `display: flex` with no `flex-grow` on the tab, so a
|
|
291
|
+
// card tab is CONTENT-WIDTH. The base trigger is `flex-1`, which made every
|
|
292
|
+
// card tab stretch to an equal share of the strip: measured, three tabs in a
|
|
293
|
+
// 942px strip came out 311px each for labels needing ~70px, and with
|
|
294
|
+
// `justify-center` the label and its × floated in the middle of that box —
|
|
295
|
+
// the × ended up 90px from the tab's own trailing edge (179px on a two-tab
|
|
296
|
+
// strip). antd's is 8px away. This is also why the face read as a free-standing
|
|
297
|
+
// bordered box rather than a tab.
|
|
298
|
+
//
|
|
299
|
+
// THE ACTIVE FACE IS THE BODY'S SURFACE, from the same knob the body reads
|
|
300
|
+
// (gh#762). The base trigger's `data-[state=active]:bg-background` was right only
|
|
301
|
+
// while the two could not disagree; once a service can retint the body, an active
|
|
302
|
+
// tab still painting `--background` reads as a DIFFERENT box sitting on the panel
|
|
303
|
+
// — which is the "two stacked boxes" complaint in one property. Same utilities
|
|
304
|
+
// group, so tailwind-merge drops the base step instead of stacking two fills.
|
|
305
|
+
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)))]",
|
|
306
|
+
card && CARD_FACE[placement]
|
|
307
|
+
);
|
|
217
308
|
const list = items ? /* @__PURE__ */ jsx(
|
|
218
309
|
TabsList,
|
|
219
310
|
{
|
|
220
311
|
ref: listRef,
|
|
221
312
|
"data-slot": "tabs-list",
|
|
222
313
|
variant: variant === "line" ? "line" : "default",
|
|
223
|
-
className:
|
|
224
|
-
// The inset override is an arbitrary value reading the knob (never `p-0`): it must
|
|
225
|
-
// stay a UTILITY so tailwind-merge still drops the strip's own padding step.
|
|
226
|
-
//
|
|
227
|
-
// IT IS SPLIT INTO `px-`/`py-` FOR THE FOCUS RING (gh#376). The strip clips its
|
|
228
|
-
// block axis (`overflow-y: hidden` beside `overflow-x: auto`), and this variant's
|
|
229
|
-
// inset is 0px, so a focused trigger had NO block headroom at all — measured in
|
|
230
|
-
// Chromium, 0px top and bottom, i.e. the entire ring was shaved. The headroom has to
|
|
231
|
-
// come from layout: `overflow-y: visible` next to `overflow-x: auto` computes back
|
|
232
|
-
// to `auto` per spec (a second scroll container, not a ring), and Chromium honours
|
|
233
|
-
// `overflow-clip-margin` only when BOTH axes are `clip`. So the block padding carries
|
|
234
|
-
// the ring's outer reach and an equal negative block margin hands it straight back —
|
|
235
|
-
// the ring paints inside the scrollport and the strip's OUTER box is unchanged
|
|
236
|
-
// (measured: 38px before and after). Both values read the same token the ring is
|
|
237
|
-
// sized from, so the two can never drift apart.
|
|
238
|
-
//
|
|
239
|
-
// The cost, recorded rather than hidden: the rail hairline (`border-b`, drawn at the
|
|
240
|
-
// border box) now sits the ring's reach below the triggers instead of flush. That is
|
|
241
|
-
// where a hand-composed <TabsList variant="line"> already put it — its `p-1` gives
|
|
242
|
-
// the same block inset — so the two construction paths now agree. A service that
|
|
243
|
-
// wants the active bar parked back on the hairline raises `--tabs-indicator-offset`,
|
|
244
|
-
// which exists for exactly that.
|
|
245
|
-
// `w-full` is scoped to the HORIZONTAL axis. Unscoped it also applied when the root is
|
|
246
|
-
// vertical, where the strip is a COLUMN beside the panel: `width: 100%` made the strip
|
|
247
|
-
// claim the whole row and the panel collapsed to its own min-content floor. Measured at a
|
|
248
|
-
// 1232px root: strip 1133.72px (92%) / panel 90.28px (7%). And it is not a layer problem —
|
|
249
|
-
// tailwind-merge drops the base `w-fit` as a same-group conflict, so `w-fit` never reached
|
|
250
|
-
// the DOM at all. Scoping puts it back on the axis it was written for.
|
|
251
|
-
variant === "line" && "my-[calc(-1_*_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] h-auto justify-start border-b px-[var(--tabs-list-line-space-inset)] py-[calc(var(--tabs-list-line-space-inset)_+_var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))] data-[orientation=horizontal]:w-full",
|
|
252
|
-
// CARD strip. The list keeps `data-variant="default"` on purpose (a hand-composed
|
|
253
|
-
// <TabsList> must be unaffected), so the card face is selected from the ROOT — but the
|
|
254
|
-
// three properties the base list already claims as utilities (`bg-muted`, `p-1`,
|
|
255
|
-
// `rounded-lg`) have to be replaced with utilities too, or the components layer loses.
|
|
256
|
-
card && "items-end justify-start gap-[var(--tabs-card-list-space-gap)] rounded-[var(--tabs-card-list-radius)] p-[var(--tabs-card-list-space-inset)] data-[orientation=horizontal]:w-full data-[variant=default]:bg-transparent",
|
|
257
|
-
// CENTERED. Two independent moves, because the strip has two shapes: the pill/card strip
|
|
258
|
-
// is `w-fit` (auto inline margins centre the BOX) and the line strip is `w-full`
|
|
259
|
-
// (`justify-content` centres its CONTENT). `safe` keeps the overflow rule the strip
|
|
260
|
-
// already depends on — plain `center` strands the leading tab outside the scrollport.
|
|
261
|
-
centered && "mx-auto justify-center-safe",
|
|
262
|
-
listClassName
|
|
263
|
-
),
|
|
314
|
+
className: itemsListClassName,
|
|
264
315
|
children: items.map((item) => {
|
|
265
316
|
const removable = editable && Boolean(onEdit) && item.closable !== false && !item.disabled;
|
|
266
317
|
return /* @__PURE__ */ jsxs(
|
|
@@ -275,51 +326,7 @@ function Tabs({
|
|
|
275
326
|
event.preventDefault();
|
|
276
327
|
onEdit?.(item.value, "remove");
|
|
277
328
|
} : void 0,
|
|
278
|
-
className:
|
|
279
|
-
// Geometry only — the active underline is the token-owned `::after` bar the
|
|
280
|
-
// line variant already owns (never a second, hand-rolled border-b indicator).
|
|
281
|
-
// They are arbitrary-value utilities so tailwind-merge keeps dropping the pill
|
|
282
|
-
// trigger's own radius / padding steps from the base class list.
|
|
283
|
-
variant === "line" && "rounded-[var(--tabs-trigger-line-radius)] px-[var(--tabs-trigger-line-padding-x)] py-[var(--tabs-trigger-line-padding-y)]",
|
|
284
|
-
sizeTriggerClassName,
|
|
285
|
-
// A centred strip must let its triggers SHRINK to their labels first. The base
|
|
286
|
-
// trigger is `flex-1`, so on a full-width (`line`) strip they already fill every
|
|
287
|
-
// pixel and `justify-content` has nothing left to centre — measured, the leading and
|
|
288
|
-
// trailing gaps were both 0px with `centered` on and off.
|
|
289
|
-
centered && "flex-none",
|
|
290
|
-
// LINE face: same argument as the card face below, and from the same source — antd
|
|
291
|
-
// puts no `flex-grow` on a tab of ANY `type`, so a line tab is content-width too.
|
|
292
|
-
// `card` was fixed on that reasoning (gh#736); `line` was left on the base `flex-1`
|
|
293
|
-
// and kept stretching. Measured by a consumer on a 1200px strip: three tabs at
|
|
294
|
-
// 321px each for labels needing 83 / 84 / 56px, so the active underline ran 321px
|
|
295
|
-
// under a 56px label.
|
|
296
|
-
//
|
|
297
|
-
// `centered` is NOT the escape hatch for this: it centres, which trades one wrong
|
|
298
|
-
// geometry for another, and it is antd's prop for centring rather than for width.
|
|
299
|
-
variant === "line" && "flex-none",
|
|
300
|
-
// The merged edge is the SURFACE colour, never `transparent`: the rail is an inset
|
|
301
|
-
// shadow at the strip's padding-box edge, so a see-through border would let that 1px
|
|
302
|
-
// of rail run straight across the tab it is joined to.
|
|
303
|
-
// CARD face: a real boundary on every side, opened toward the panel by
|
|
304
|
-
// `CARD_FACE`. `flex-none` is antd's geometry, not a preference — its
|
|
305
|
-
// `nav-list` is a plain `display: flex` with no `flex-grow` on the tab, so a
|
|
306
|
-
// card tab is CONTENT-WIDTH. The base trigger is `flex-1`, which made every
|
|
307
|
-
// card tab stretch to an equal share of the strip: measured, three tabs in a
|
|
308
|
-
// 942px strip came out 311px each for labels needing ~70px, and with
|
|
309
|
-
// `justify-center` the label and its × floated in the middle of that box —
|
|
310
|
-
// the × ended up 90px from the tab's own trailing edge (179px on a two-tab
|
|
311
|
-
// strip). antd's is 8px away. This is also why the face read as a free-standing
|
|
312
|
-
// bordered box rather than a tab.
|
|
313
|
-
//
|
|
314
|
-
// THE ACTIVE FACE IS THE BODY'S SURFACE, from the same knob the body reads
|
|
315
|
-
// (gh#762). The base trigger's `data-[state=active]:bg-background` was right only
|
|
316
|
-
// while the two could not disagree; once a service can retint the body, an active
|
|
317
|
-
// tab still painting `--background` reads as a DIFFERENT box sitting on the panel
|
|
318
|
-
// — which is the "two stacked boxes" complaint in one property. Same utilities
|
|
319
|
-
// group, so tailwind-merge drops the base step instead of stacking two fills.
|
|
320
|
-
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)))]",
|
|
321
|
-
card && CARD_FACE[placement]
|
|
322
|
-
),
|
|
329
|
+
className: itemTriggerClassName,
|
|
323
330
|
children: [
|
|
324
331
|
item.icon ? /* @__PURE__ */ jsx(
|
|
325
332
|
"span",
|
|
@@ -380,6 +387,41 @@ function Tabs({
|
|
|
380
387
|
})
|
|
381
388
|
}
|
|
382
389
|
) : null;
|
|
390
|
+
if (routeStrip) {
|
|
391
|
+
return /* @__PURE__ */ jsx(
|
|
392
|
+
TabsRouteStrip,
|
|
393
|
+
{
|
|
394
|
+
items,
|
|
395
|
+
activeValue,
|
|
396
|
+
overflow,
|
|
397
|
+
orientation: resolvedOrientation,
|
|
398
|
+
linkComponent,
|
|
399
|
+
onTabClick,
|
|
400
|
+
onTabScroll,
|
|
401
|
+
listLabel,
|
|
402
|
+
listClassName: itemsListClassName,
|
|
403
|
+
listVariant: variant === "line" ? "line" : "default",
|
|
404
|
+
triggerClassName: itemTriggerClassName,
|
|
405
|
+
extraStart,
|
|
406
|
+
extraEnd,
|
|
407
|
+
className: cn("group/tabs flex data-[orientation=horizontal]:flex-col", className),
|
|
408
|
+
rootProps: {
|
|
409
|
+
...props,
|
|
410
|
+
dir,
|
|
411
|
+
"data-slot": "tabs",
|
|
412
|
+
"data-orientation": resolvedOrientation,
|
|
413
|
+
"data-variant": variant,
|
|
414
|
+
"data-placement": placement,
|
|
415
|
+
"data-size": size,
|
|
416
|
+
"data-centered": centered ? "true" : void 0,
|
|
417
|
+
"data-animated-ink-bar": motion.inkBar ? "true" : "false",
|
|
418
|
+
"data-animated-tab-pane": motion.tabPane ? "true" : "false",
|
|
419
|
+
"data-indicator-size": indicatorSize,
|
|
420
|
+
"data-indicator-align": indicatorAlign
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
}
|
|
383
425
|
return /* @__PURE__ */ jsx(TabsFrameContext.Provider, { value: frame, children: /* @__PURE__ */ jsx(TabsPanelRegistryContext.Provider, { value: registerPanel, children: /* @__PURE__ */ jsx(
|
|
384
426
|
AriaTabs,
|
|
385
427
|
{
|
|
@@ -480,6 +522,139 @@ function Tabs({
|
|
|
480
522
|
}
|
|
481
523
|
) }) });
|
|
482
524
|
}
|
|
525
|
+
function TabsRouteStrip({
|
|
526
|
+
overflow,
|
|
527
|
+
items,
|
|
528
|
+
activeValue,
|
|
529
|
+
orientation,
|
|
530
|
+
linkComponent,
|
|
531
|
+
onTabClick,
|
|
532
|
+
onTabScroll,
|
|
533
|
+
listLabel,
|
|
534
|
+
listClassName,
|
|
535
|
+
listVariant,
|
|
536
|
+
triggerClassName,
|
|
537
|
+
extraStart,
|
|
538
|
+
extraEnd,
|
|
539
|
+
className,
|
|
540
|
+
rootProps
|
|
541
|
+
}) {
|
|
542
|
+
const { t } = useTranslation();
|
|
543
|
+
const navRef = React.useRef(null);
|
|
544
|
+
useKeepActiveTabVisible(navRef);
|
|
545
|
+
useTabsScrollReporter(navRef, orientation === "vertical", onTabScroll);
|
|
546
|
+
const [hiddenValues, setHiddenValues] = React.useState([]);
|
|
547
|
+
useTabsOverflowValues(
|
|
548
|
+
navRef,
|
|
549
|
+
overflow === "menu" ? items.map((item) => item.value) : void 0,
|
|
550
|
+
setHiddenValues
|
|
551
|
+
);
|
|
552
|
+
const hiddenItems = overflow === "menu" ? items.filter((item) => hiddenValues.includes(item.value)) : [];
|
|
553
|
+
const MenuLink = linkComponent ?? "a";
|
|
554
|
+
const Link = linkComponent ?? "a";
|
|
555
|
+
const named = listLabel["aria-label"] !== void 0 || listLabel["aria-labelledby"] !== void 0;
|
|
556
|
+
const nav = /* @__PURE__ */ jsx(
|
|
557
|
+
"nav",
|
|
558
|
+
{
|
|
559
|
+
ref: navRef,
|
|
560
|
+
"data-slot": "tabs-list",
|
|
561
|
+
"data-variant": listVariant,
|
|
562
|
+
"data-orientation": orientation,
|
|
563
|
+
"aria-label": named ? listLabel["aria-label"] : t("navigation.tabs.routeNav"),
|
|
564
|
+
"aria-labelledby": listLabel["aria-labelledby"],
|
|
565
|
+
onFocus: () => syncActiveTabIntoView(navRef.current),
|
|
566
|
+
className: cn(
|
|
567
|
+
TABS_LIST_BASE_CLASS_NAME,
|
|
568
|
+
listClassName,
|
|
569
|
+
// Keep the first/last link's focus ring inside the clipped inline scrollport too.
|
|
570
|
+
listVariant === "line" && "scroll-px-[var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width)))] px-[max(var(--tabs-list-line-space-inset),var(--tabs-list-focus-ring-space-inset,calc(var(--focus-ring-width)_+_var(--focus-ring-glow-width))))]"
|
|
571
|
+
),
|
|
572
|
+
children: items.map((item) => {
|
|
573
|
+
const active = item.value === activeValue;
|
|
574
|
+
const inert = item.disabled === true || item.href === void 0;
|
|
575
|
+
return /* @__PURE__ */ jsxs(
|
|
576
|
+
Link,
|
|
577
|
+
{
|
|
578
|
+
href: inert ? void 0 : item.href,
|
|
579
|
+
role: inert && linkComponent === void 0 ? "link" : void 0,
|
|
580
|
+
"aria-current": active ? "page" : void 0,
|
|
581
|
+
"aria-disabled": inert ? true : void 0,
|
|
582
|
+
"data-active": active ? "true" : void 0,
|
|
583
|
+
"data-slot": "tabs-trigger",
|
|
584
|
+
"data-state": active ? "active" : "inactive",
|
|
585
|
+
"data-orientation": orientation,
|
|
586
|
+
"data-value": item.value,
|
|
587
|
+
className: cn(
|
|
588
|
+
TABS_TRIGGER_BASE_CLASS_NAME,
|
|
589
|
+
// `disabled:` never matches an anchor; an inert route reads `aria-disabled` instead.
|
|
590
|
+
"aria-disabled:pointer-events-none aria-disabled:opacity-[var(--disabled-opacity)]",
|
|
591
|
+
triggerClassName
|
|
592
|
+
),
|
|
593
|
+
onClick: onTabClick ? (event) => (
|
|
594
|
+
// The published event type names the tablist's <button>; here currentTarget
|
|
595
|
+
// is the <a>. Never `preventDefault()` — the browser owns the link.
|
|
596
|
+
onTabClick(item.value, event)
|
|
597
|
+
) : void 0,
|
|
598
|
+
children: [
|
|
599
|
+
item.icon ? /* @__PURE__ */ jsx(
|
|
600
|
+
"span",
|
|
601
|
+
{
|
|
602
|
+
"data-slot": "tabs-trigger-icon",
|
|
603
|
+
className: "ui-tabs-trigger-icon",
|
|
604
|
+
"aria-hidden": "true",
|
|
605
|
+
children: item.icon
|
|
606
|
+
}
|
|
607
|
+
) : null,
|
|
608
|
+
item.label,
|
|
609
|
+
/* @__PURE__ */ jsx(
|
|
610
|
+
TabCount,
|
|
611
|
+
{
|
|
612
|
+
count: item.count,
|
|
613
|
+
overflowCount: item.overflowCount,
|
|
614
|
+
showZero: item.showZero,
|
|
615
|
+
countLabel: item.countLabel
|
|
616
|
+
}
|
|
617
|
+
)
|
|
618
|
+
]
|
|
619
|
+
},
|
|
620
|
+
item.value
|
|
621
|
+
);
|
|
622
|
+
})
|
|
623
|
+
}
|
|
624
|
+
);
|
|
625
|
+
return /* @__PURE__ */ jsx("div", { ...rootProps, className, children: extraStart || extraEnd || overflow === "menu" ? /* @__PURE__ */ jsxs("div", { "data-slot": "tabs-bar", className: "ui-tabs-bar", children: [
|
|
626
|
+
extraStart ? /* @__PURE__ */ jsx("div", { "data-slot": "tabs-extra", "data-side": "start", className: "ui-tabs-extra", children: extraStart }) : null,
|
|
627
|
+
nav,
|
|
628
|
+
hiddenItems.length > 0 ? /* @__PURE__ */ jsxs(DropdownMenu, { children: [
|
|
629
|
+
/* @__PURE__ */ jsx(
|
|
630
|
+
DropdownMenuTrigger,
|
|
631
|
+
{
|
|
632
|
+
className: "ui-tabs-overflow",
|
|
633
|
+
"aria-label": t("navigation.tabs.moreTabs"),
|
|
634
|
+
children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "ui-tabs-overflow-icon", "aria-hidden": "true" })
|
|
635
|
+
}
|
|
636
|
+
),
|
|
637
|
+
/* @__PURE__ */ jsx(DropdownMenuContent, { placement: "bottomEnd", children: hiddenItems.map((item) => /* @__PURE__ */ jsx(
|
|
638
|
+
DropdownMenuItem,
|
|
639
|
+
{
|
|
640
|
+
asChild: true,
|
|
641
|
+
disabled: item.disabled || item.href === void 0,
|
|
642
|
+
children: /* @__PURE__ */ jsx(
|
|
643
|
+
MenuLink,
|
|
644
|
+
{
|
|
645
|
+
className: "",
|
|
646
|
+
href: item.disabled ? void 0 : item.href,
|
|
647
|
+
"aria-current": item.value === activeValue ? "page" : void 0,
|
|
648
|
+
children: item.label
|
|
649
|
+
}
|
|
650
|
+
)
|
|
651
|
+
},
|
|
652
|
+
item.value
|
|
653
|
+
)) })
|
|
654
|
+
] }) : null,
|
|
655
|
+
extraEnd ? /* @__PURE__ */ jsx("div", { "data-slot": "tabs-extra", "data-side": "end", className: "ui-tabs-extra", children: extraEnd }) : null
|
|
656
|
+
] }) : nav });
|
|
657
|
+
}
|
|
483
658
|
const TabsList = React.forwardRef(
|
|
484
659
|
({ className, variant = "default", loop: _loop, children, ...props }, ref) => {
|
|
485
660
|
const { listLabel } = React.useContext(TabsFrameContext);
|
|
@@ -523,7 +698,7 @@ const TabsList = React.forwardRef(
|
|
|
523
698
|
// stayed opaque `--muted`. Nothing about the card/line strips changes: both clear the
|
|
524
699
|
// track with utilities of their own (`data-[variant=default]:bg-transparent`,
|
|
525
700
|
// `data-[variant=line]:bg-transparent`), and a utility still outranks the new rule.
|
|
526
|
-
|
|
701
|
+
TABS_LIST_BASE_CLASS_NAME,
|
|
527
702
|
className
|
|
528
703
|
),
|
|
529
704
|
render: (domProps) => withDomProps("div", "tabs-list", props, domProps, { "data-variant": variant }),
|
|
@@ -588,7 +763,7 @@ const TabsTrigger = React.forwardRef(
|
|
|
588
763
|
// — Segmented — has been `--segmented-item-selected-background` since gh#848, and these
|
|
589
764
|
// two are documented as the same pair. The card face and the line strip both restate the
|
|
590
765
|
// fill as utilities, which still outrank the new rule, so neither moves.
|
|
591
|
-
|
|
766
|
+
TABS_TRIGGER_BASE_CLASS_NAME,
|
|
592
767
|
// The tier the root was given. Without this the compound form ignored `size` outright.
|
|
593
768
|
triggerSizeClassName(size),
|
|
594
769
|
className
|
|
@@ -182,7 +182,7 @@ function ToolbarGroup({ label, controlId, className, children }) {
|
|
|
182
182
|
className: cn("ui-stack-xs ui-toolbar-group", className),
|
|
183
183
|
children: [
|
|
184
184
|
label ? controlId ? /* @__PURE__ */ jsx("label", { id: labelId, htmlFor: controlId, className: "ui-toolbar-label", children: label }) : /* @__PURE__ */ jsx("div", { id: labelId, className: "ui-toolbar-label", children: label }) : null,
|
|
185
|
-
children
|
|
185
|
+
/* @__PURE__ */ jsx("div", { className: "ui-toolbar-control", children })
|
|
186
186
|
]
|
|
187
187
|
}
|
|
188
188
|
);
|
|
@@ -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": "33.
|
|
3
|
+
"version": "33.9.1",
|
|
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,
|
|
@@ -235,8 +235,12 @@ export type TextareaProp = React.TextareaHTMLAttributes<HTMLTextAreaElement> & {
|
|
|
235
235
|
* Spatie translatable, Rails Mobility) already sends.
|
|
236
236
|
*/
|
|
237
237
|
export type TranslatableValue = Partial<Record<string, string>>;
|
|
238
|
-
/**
|
|
239
|
-
|
|
238
|
+
/**
|
|
239
|
+
* The locale + value contract shared by every translatable control: {@link TranslatableInputProp},
|
|
240
|
+
* {@link TranslatableTextareaProp}, {@link TranslatableFieldProp} and `@godxjp/block-editor`'s
|
|
241
|
+
* `TranslatableRichText`.
|
|
242
|
+
*/
|
|
243
|
+
export type TranslatableLocaleProp = {
|
|
240
244
|
/** Controlled per-locale values. */
|
|
241
245
|
value?: ValueProp<TranslatableValue>;
|
|
242
246
|
/** Uncontrolled initial per-locale values. */
|
|
@@ -266,10 +270,51 @@ type TranslatableFieldProp = {
|
|
|
266
270
|
*/
|
|
267
271
|
errors?: Partial<Record<string, ErrorProp>>;
|
|
268
272
|
};
|
|
273
|
+
/**
|
|
274
|
+
* What {@link TranslatableFieldProp}'s `renderControl` hands the ONE control in the panel: the
|
|
275
|
+
* active locale's text and setter, its `lang`, the resolved placeholder, and the field-a11y contract
|
|
276
|
+
* with the active locale's error folded in. Every prop the field does not own (`id`, `disabled`,
|
|
277
|
+
* `aria-describedby`, `rows`, …) is passed through as well.
|
|
278
|
+
*/
|
|
279
|
+
export type TranslatableFieldControlProp = FieldA11yProps & Record<string, unknown> & {
|
|
280
|
+
/** The active locale's BCP-47 tag — the control's `lang`. */
|
|
281
|
+
lang: string;
|
|
282
|
+
/** The active locale's text (`""` when empty). */
|
|
283
|
+
value: string;
|
|
284
|
+
/** Writes the active locale's text; the field reports the whole map. */
|
|
285
|
+
onValueChange: (text: string) => void;
|
|
286
|
+
/** The fallback locale's text while the active locale is empty, else the field's `placeholder`. */
|
|
287
|
+
placeholder?: string;
|
|
288
|
+
/** `"error"` while the active locale has an error. */
|
|
289
|
+
status?: ControlStatusProp;
|
|
290
|
+
};
|
|
291
|
+
/**
|
|
292
|
+
* @see TranslatableField — the locale-tab engine behind every translatable control (gh#1336,
|
|
293
|
+
* gh#1365): the tab strip, the per-locale error marks, the fallback placeholder and the FormField
|
|
294
|
+
* error report, around ONE control the caller renders. `TranslatableInput`, `TranslatableTextarea`
|
|
295
|
+
* and `@godxjp/block-editor`'s `TranslatableRichText` are this field around an `Input`, a
|
|
296
|
+
* `Textarea` and a `RichTextEditor`.
|
|
297
|
+
*/
|
|
298
|
+
export type TranslatableFieldProp = TranslatableLocaleProp & FieldA11yProps & {
|
|
299
|
+
/** Renders the one control in the panel from the active locale's props. */
|
|
300
|
+
renderControl: (control: TranslatableFieldControlProp) => React.ReactNode;
|
|
301
|
+
/**
|
|
302
|
+
* Turns the fallback locale's stored text into the plain text an EMPTY locale shows as its
|
|
303
|
+
* placeholder — HTML to its text, for a rich-text control. Default: the text as stored.
|
|
304
|
+
*/
|
|
305
|
+
toPlaceholder?: (fallbackText: string) => string;
|
|
306
|
+
/** The control's tier; the tab strip uses the Tabs `sm` tier (`md` for an `lg` field). */
|
|
307
|
+
size?: SizeProp;
|
|
308
|
+
/** Shown while the active locale is empty and there is no fallback text to show. */
|
|
309
|
+
placeholder?: PlaceholderProp;
|
|
310
|
+
status?: ControlStatusProp;
|
|
311
|
+
id?: IdProp;
|
|
312
|
+
className?: ClassNameProp;
|
|
313
|
+
};
|
|
269
314
|
/** @see TranslatableInput — one `Input` holding a value per locale, switched by locale tabs. */
|
|
270
|
-
export type TranslatableInputProp = Omit<InputProp, "value" | "defaultValue" | "onValueChange" | "lang"> &
|
|
315
|
+
export type TranslatableInputProp = Omit<InputProp, "value" | "defaultValue" | "onValueChange" | "lang"> & TranslatableLocaleProp;
|
|
271
316
|
/** @see TranslatableTextarea — the `Textarea` counterpart of {@link TranslatableInputProp}. */
|
|
272
|
-
export type TranslatableTextareaProp = Omit<TextareaProp, "value" | "defaultValue" | "onValueChange" | "lang"> &
|
|
317
|
+
export type TranslatableTextareaProp = Omit<TextareaProp, "value" | "defaultValue" | "onValueChange" | "lang"> & TranslatableLocaleProp;
|
|
273
318
|
/**
|
|
274
319
|
* @see NumberInput — localized numeric spinbutton (composes `Input` + step `Button`s).
|
|
275
320
|
* `value`/`defaultValue`/`onValueChange` carry a `number | null` (null = empty). `step` drives both
|
|
@@ -2524,4 +2569,3 @@ export type EmojiPickerProp = {
|
|
|
2524
2569
|
id?: IdProp;
|
|
2525
2570
|
className?: ClassNameProp;
|
|
2526
2571
|
};
|
|
2527
|
-
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Navigation component prop types — @see docs/COMPONENTS.md#navigation */
|
|
2
2
|
import type * as React from "react";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
-
import type { AffixProp, AffixTargetProp, ResponsiveGridColumnsProp } from "./layout.prop.js";
|
|
4
|
+
import type { AffixProp, AffixTargetProp, ResponsiveGridColumnsProp, SidebarLinkComponentProp } from "./layout.prop.js";
|
|
5
5
|
import type { ActionsProp, ChildrenProp, ClassNameProp, DisabledProp, ErrorProp, HasActiveFiltersProp, IdProp, LabelProp, OnClearFiltersProp, OnOpenChangeProp, OpenProp, DefaultOpenProp, PendingProp, PlaceholderProp, StickyProp, TextAlignProp } from "../vocabulary/index.js";
|
|
6
6
|
import type { FormProp, SearchSelectOptionProp } from "./data-entry.prop.js";
|
|
7
7
|
/**
|
|
@@ -321,6 +321,17 @@ export type TabItemProp = {
|
|
|
321
321
|
*/
|
|
322
322
|
content?: React.ReactNode;
|
|
323
323
|
disabled?: boolean;
|
|
324
|
+
/**
|
|
325
|
+
* ROUTE TAB (gh#1364). The tab's destination. When ANY item carries an `href` the strip is a
|
|
326
|
+
* route strip: it renders a `<nav>` landmark of real links instead of a tablist — the current
|
|
327
|
+
* route (`value`) gets `aria-current="page"`, no `tabpanel` is rendered (`content` is ignored;
|
|
328
|
+
* the route renders its own page), and a plain, modified or middle click is never intercepted,
|
|
329
|
+
* so "open in new tab" works. An item without `href` (or a `disabled` one) is an inert anchor.
|
|
330
|
+
* A route strip always scrolls its overflow. `overflow="menu"` also exposes hidden routes
|
|
331
|
+
* as native links in a menu, preserving browser link actions.
|
|
332
|
+
* Links go through `TabsProp.linkComponent` when set.
|
|
333
|
+
*/
|
|
334
|
+
href?: string;
|
|
324
335
|
/** Leading glyph inside the trigger (Ant Design `Tab.icon`). */
|
|
325
336
|
icon?: React.ReactNode;
|
|
326
337
|
/**
|
|
@@ -592,6 +603,14 @@ export type TabsProp = {
|
|
|
592
603
|
* (`radiogroup`), not a strip-only `Tabs`.
|
|
593
604
|
*/
|
|
594
605
|
controls?: IdProp;
|
|
606
|
+
/**
|
|
607
|
+
* Router link component for a ROUTE strip (items with `href`, gh#1364) — the SAME contract and
|
|
608
|
+
* the same value as `Sidebar.linkComponent` / `NavList.linkComponent`: Inertia's `<Link>`, or a
|
|
609
|
+
* React Router / TanStack link wrapped by `createSidebarLink(Link, "to")`. It must forward every
|
|
610
|
+
* prop to its `<a>` — the strip's `data-slot` / `data-state` styling hooks travel with them.
|
|
611
|
+
* Without it each route is a plain `<a href>`.
|
|
612
|
+
*/
|
|
613
|
+
linkComponent?: SidebarLinkComponentProp;
|
|
595
614
|
className?: ClassNameProp;
|
|
596
615
|
listClassName?: ClassNameProp;
|
|
597
616
|
contentClassName?: ClassNameProp;
|