@godxjp/ui 18.12.20 → 18.13.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.
@@ -116,8 +116,15 @@ export declare namespace DataTable {
116
116
  placeholder?: string;
117
117
  className?: string;
118
118
  }) => React.JSX.Element;
119
- var ViewOptions: ({ className }: {
119
+ var ViewOptions: ({ className, label, }: {
120
120
  className?: string;
121
+ /**
122
+ * Overrides the button's text. The default ("View") is the shortest thing
123
+ * that fits a dense toolbar, but a product whose users know the control by a
124
+ * longer name — "View Options", "表示項目" — needs to say that, and hiding a
125
+ * bare text node from the outside is not something CSS can do.
126
+ */
127
+ label?: React.ReactNode;
121
128
  }) => React.JSX.Element | null;
122
129
  var SelectAll: () => React.JSX.Element | null;
123
130
  var BulkActions: ({ count, children, className, }: BulkActionsProps) => React.JSX.Element | null;
@@ -275,7 +275,10 @@ DataTable.Search = function DataTableSearch({
275
275
  );
276
276
  };
277
277
  DataTable.Search.displayName = "DataTable.Search";
278
- DataTable.ViewOptions = function DataTableViewOptions({ className }) {
278
+ DataTable.ViewOptions = function DataTableViewOptions({
279
+ className,
280
+ label
281
+ }) {
279
282
  const { table } = useDataTableContext();
280
283
  const { t } = useTranslation();
281
284
  const hideable = table.getAllLeafColumns().filter((c) => c.getCanHide());
@@ -283,7 +286,7 @@ DataTable.ViewOptions = function DataTableViewOptions({ className }) {
283
286
  return /* @__PURE__ */ jsxs(DropdownMenu, { children: [
284
287
  /* @__PURE__ */ jsx(DropdownMenuTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", className, children: [
285
288
  /* @__PURE__ */ jsx(SlidersHorizontal, { className: "size-4 shrink-0", "aria-hidden": "true" }),
286
- t("dataGrid.view")
289
+ label ?? t("dataGrid.view")
287
290
  ] }) }),
288
291
  /* @__PURE__ */ jsxs(DropdownMenuContent, { align: "end", children: [
289
292
  /* @__PURE__ */ jsx(DropdownMenuLabel, { children: t("dataGrid.toggleColumns") }),
@@ -301,6 +304,13 @@ DataTable.ViewOptions = function DataTableViewOptions({ className }) {
301
304
  ] });
302
305
  };
303
306
  DataTable.ViewOptions.displayName = "DataTable.ViewOptions";
307
+ function columnWidth(width) {
308
+ if (width === void 0 || width === "") return {};
309
+ if (/^(?:[\d.]|calc\(|var\(|clamp\(|min\(|max\()/.test(width.trim())) {
310
+ return { style: { width } };
311
+ }
312
+ return { className: width };
313
+ }
304
314
  function isEmptyHeader(header) {
305
315
  return header == null || header === "" || header === false;
306
316
  }
@@ -498,8 +508,9 @@ DataTable.Content = function DataTableContent() {
498
508
  priority: col.priority,
499
509
  "data-empty": headerEmpty || void 0,
500
510
  "aria-sort": isSortable ? isActiveSort ? activeSort?.direction === "asc" ? "ascending" : "descending" : "none" : void 0,
511
+ style: columnWidth(col.width).style,
501
512
  className: cn(
502
- col.width,
513
+ columnWidth(col.width).className,
503
514
  col.align === "right" && "text-end",
504
515
  col.align === "center" && "text-center",
505
516
  col.hiddenOnMobile && "hidden md:table-cell",
@@ -533,9 +544,10 @@ DataTable.Content = function DataTableContent() {
533
544
  TableCell,
534
545
  {
535
546
  priority: col.priority,
547
+ style: columnWidth(col.width).style,
536
548
  className: cn(
537
549
  cellPadding,
538
- col.width,
550
+ columnWidth(col.width).className,
539
551
  col.align === "right" && "text-end",
540
552
  col.align === "center" && "text-center",
541
553
  col.hiddenOnMobile && "hidden md:table-cell",
@@ -655,9 +667,10 @@ DataTable.Content = function DataTableContent() {
655
667
  TableCell,
656
668
  {
657
669
  priority: col.priority,
670
+ style: columnWidth(col.width).style,
658
671
  className: cn(
659
672
  cellPadding,
660
- col.width,
673
+ columnWidth(col.width).className,
661
674
  col.align === "right" && "text-end",
662
675
  col.align === "center" && "text-center",
663
676
  col.hiddenOnMobile && "hidden md:table-cell",
@@ -11,7 +11,7 @@ export interface DescriptionsProps {
11
11
  * name/email block above an editable role field, for example) can be told to match it. Applies
12
12
  * only in `layout="horizontal"` — a vertical label sits above the value and end-aligning it
13
13
  * there would read as a mistake, exactly like `Form`'s own contract. Default `"start"`, matching
14
- * this component's historical unconditional left-align — no existing consumer's render changes.
14
+ * this component's historical unconditional start-align — no existing consumer's render changes.
15
15
  */
16
16
  labelAlign?: "start" | "end";
17
17
  className?: string;
@@ -49,7 +49,7 @@ Descriptions.Item = function DescriptionsItem({
49
49
  className: cn(
50
50
  "text-muted-foreground text-xs",
51
51
  // `end`-align only ever applies in horizontal layout — same guard `Form` uses, so a
52
- // vertical label (already above its value) never mistakenly right-aligns (gh#294).
52
+ // vertical label (already above its value) never mistakenly end-aligns (gh#294).
53
53
  layout === "horizontal" && labelAlign === "end" && "text-end"
54
54
  ),
55
55
  children: label
@@ -58,8 +58,14 @@ function FormField({
58
58
  "aria-label": childProps?.["aria-label"] ?? (typeof label === "string" ? label : void 0),
59
59
  // Helper and error can coexist: helper stays on aria-describedby, the error on
60
60
  // aria-errormessage (surfaced when aria-invalid is true).
61
- "aria-describedby": mergeIds(childProps?.["aria-describedby"], helperId),
62
- "aria-errormessage": mergeIds(childProps?.["aria-errormessage"], errorId),
61
+ "aria-describedby": mergeIds(
62
+ childProps?.["aria-describedby"],
63
+ helperId
64
+ ),
65
+ "aria-errormessage": mergeIds(
66
+ childProps?.["aria-errormessage"],
67
+ errorId
68
+ ),
63
69
  "aria-required": required ? true : childProps?.["aria-required"],
64
70
  "aria-invalid": error ? true : childProps?.["aria-invalid"]
65
71
  }) : children;
@@ -80,21 +86,29 @@ function FormField({
80
86
  className: cn("ui-form-field", className),
81
87
  children: [
82
88
  /* @__PURE__ */ jsxs("div", { "data-slot": "form-field-label", className: "ui-form-field-label", children: [
83
- /* @__PURE__ */ jsx(Label, { asChild: true, id: labelId, className: "ui-inline-xs", children: /* @__PURE__ */ jsxs(
84
- "span",
89
+ /* @__PURE__ */ jsx(
90
+ Label,
85
91
  {
86
- onClick: () => {
87
- const el = document.getElementById(resolvedId);
88
- if (!(el instanceof HTMLElement)) return;
89
- const focusable = el.matches(FOCUSABLE_SELECTOR) ? el : el.querySelector(FOCUSABLE_SELECTOR);
90
- (focusable ?? el).focus();
91
- },
92
- children: [
93
- /* @__PURE__ */ jsx("span", { children: label }),
94
- required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-destructive", children: "*" })
95
- ]
92
+ asChild: true,
93
+ id: labelId,
94
+ className: "ui-inline-xs text-[length:var(--form-label-font-size)]",
95
+ children: /* @__PURE__ */ jsxs(
96
+ "span",
97
+ {
98
+ onClick: () => {
99
+ const el = document.getElementById(resolvedId);
100
+ if (!(el instanceof HTMLElement)) return;
101
+ const focusable = el.matches(FOCUSABLE_SELECTOR) ? el : el.querySelector(FOCUSABLE_SELECTOR);
102
+ (focusable ?? el).focus();
103
+ },
104
+ children: [
105
+ /* @__PURE__ */ jsx("span", { children: label }),
106
+ required && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-destructive", children: "*" })
107
+ ]
108
+ }
109
+ )
96
110
  }
97
- ) }),
111
+ ),
98
112
  labelAddon
99
113
  ] }),
100
114
  /* @__PURE__ */ jsxs("div", { "data-slot": "form-field-control", className: "ui-form-field-control", children: [
@@ -16,6 +16,13 @@ export declare function useFormLayout(): FormLayoutContextValue | null;
16
16
  * label/control width, label alignment, and a responsive collapse breakpoint to every FormField
17
17
  * inside it. A FormField may override any of these per-field. Optionally lays fields out in a
18
18
  * responsive multi-column grid via `columns` (reuses ResponsiveGrid — mobile-first, 1 col on small).
19
+ *
20
+ * `asChild` renders the caller's own element instead of a `<form>`, keeping only the layout
21
+ * context. Routing libraries own the form element — Inertia's `<Form action method>` and
22
+ * TanStack Form both render their own — so without this every such consumer has to choose
23
+ * between the router's submission handling and the design system's field layout, and ends up
24
+ * hand-rolling label columns per field. Two `<form>` elements cannot nest, so wrapping is not
25
+ * an option: `<Form asChild layout="horizontal" labelWidth={174}><InertiaForm …/></Form>`.
19
26
  */
20
27
  export declare const Form: React.ForwardRefExoticComponent<React.FormHTMLAttributes<HTMLFormElement> & {
21
28
  layout?: FormLayoutProp;
@@ -25,5 +32,6 @@ export declare const Form: React.ForwardRefExoticComponent<React.FormHTMLAttribu
25
32
  collapseBelow?: BreakpointProp | false;
26
33
  columns?: import("../../props/components/layout.prop.js").ResponsiveGridColumnsProp;
27
34
  density?: import("../../props/index.js").DensityProp;
35
+ asChild?: boolean;
28
36
  className?: import("../../props/index.js").ClassNameProp;
29
37
  } & React.RefAttributes<HTMLFormElement>>;
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
+ import { Slot } from "@radix-ui/react-slot";
3
4
  import * as React from "react";
4
5
  import { cn } from "../../lib/utils.js";
5
6
  import { ResponsiveGrid } from "../layout/responsive-grid.js";
@@ -15,6 +16,7 @@ const Form = React.forwardRef(function Form2({
15
16
  collapseBelow = "md",
16
17
  columns,
17
18
  density,
19
+ asChild = false,
18
20
  className,
19
21
  children,
20
22
  ...props
@@ -24,6 +26,19 @@ const Form = React.forwardRef(function Form2({
24
26
  [layout, labelWidth, controlWidth, labelAlign, collapseBelow]
25
27
  );
26
28
  const content = columns != null ? /* @__PURE__ */ jsx(ResponsiveGrid, { columns, children }) : children;
29
+ if (asChild) {
30
+ return /* @__PURE__ */ jsx(FormLayoutContext.Provider, { value: ctx, children: /* @__PURE__ */ jsx(
31
+ Slot,
32
+ {
33
+ ref,
34
+ "data-slot": "form",
35
+ "data-layout": layout,
36
+ className: cn("ui-form", density && `ui-density-${density}`, className),
37
+ ...props,
38
+ children
39
+ }
40
+ ) });
41
+ }
27
42
  return /* @__PURE__ */ jsx(
28
43
  "form",
29
44
  {
@@ -1,4 +1,4 @@
1
1
  import * as React from "react";
2
2
  import type { AppShellProp } from "../../props/components/layout.prop.js";
3
3
  export type { AppShellProp, AppShellProp as AppShellProps, } from "../../props/components/layout.prop.js";
4
- export declare function AppShell({ sidebar, topbar, topbarLeft, topbarRight, logo, breadcrumb, footer, children, sidebarCollapsed, responsiveNavigation, mobileNav, mobileNavLabel, mobileNavOpen, onMobileNavOpenChange, }: AppShellProp): React.JSX.Element;
4
+ export declare function AppShell({ sidebar, topbar, topbarLeft, topbarRight, logo, breadcrumb, footer, children, sidebarCollapsed, responsiveNavigation, topbarSpan, mobileNav, mobileNavLabel, mobileNavOpen, onMobileNavOpenChange, }: AppShellProp): React.JSX.Element;
@@ -16,6 +16,7 @@ function AppShell({
16
16
  children,
17
17
  sidebarCollapsed = false,
18
18
  responsiveNavigation = "drawer",
19
+ topbarSpan = "content",
19
20
  mobileNav,
20
21
  mobileNavLabel,
21
22
  mobileNavOpen,
@@ -40,51 +41,54 @@ function AppShell({
40
41
  /* @__PURE__ */ jsx("div", { className: "app-topbar-spacer" }),
41
42
  topbarRight !== void 0 && /* @__PURE__ */ jsx("div", { className: "app-topbar-right", children: topbarRight })
42
43
  ] });
44
+ const rail = /* @__PURE__ */ jsx("aside", { className: "app-sidebar", "aria-label": t("layout.appShell.sidebarLabel"), children: sidebar });
45
+ const bar = /* @__PURE__ */ jsxs("header", { className: "app-topbar ui-scale-fixed", "aria-label": t("layout.appShell.headerLabel"), children: [
46
+ hasDrawer && /* @__PURE__ */ jsxs(Sheet, { open: drawerOpen, onOpenChange: setDrawerOpen, children: [
47
+ /* @__PURE__ */ jsx(SheetTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
48
+ Button,
49
+ {
50
+ type: "button",
51
+ variant: "ghost",
52
+ size: "sm",
53
+ className: "app-mobile-nav-trigger hidden max-[900px]:inline-flex",
54
+ "aria-label": t("layout.appShell.openNav"),
55
+ "aria-haspopup": "dialog",
56
+ children: /* @__PURE__ */ jsx(Menu, { className: "size-5", "aria-hidden": "true" })
57
+ }
58
+ ) }),
59
+ /* @__PURE__ */ jsxs(
60
+ SheetContent,
61
+ {
62
+ side: "left",
63
+ width: "var(--app-shell-mobile-nav-width)",
64
+ className: "app-mobile-nav-drawer",
65
+ overlayClassName: "app-mobile-nav-overlay",
66
+ children: [
67
+ /* @__PURE__ */ jsx(SheetHeader, { title: mobileNavLabel ?? t("layout.appShell.navLabel") }),
68
+ /* @__PURE__ */ jsx(
69
+ SheetBody,
70
+ {
71
+ className: "app-mobile-nav-body px-[var(--app-shell-mobile-nav-inset)]",
72
+ onClick: handleDrawerClick,
73
+ children: drawerNav
74
+ }
75
+ )
76
+ ]
77
+ }
78
+ )
79
+ ] }),
80
+ resolvedTopbar
81
+ ] });
43
82
  return /* @__PURE__ */ jsxs(
44
83
  "div",
45
84
  {
46
85
  className: "app-root",
47
86
  "data-collapsed": sidebarCollapsed ? "true" : void 0,
48
87
  "data-responsive-navigation": responsiveNavigation,
88
+ "data-topbar-span": topbarSpan === "full" ? "full" : void 0,
49
89
  children: [
50
- /* @__PURE__ */ jsx("aside", { className: "app-sidebar", "aria-label": t("layout.appShell.sidebarLabel"), children: sidebar }),
51
- /* @__PURE__ */ jsxs("header", { className: "app-topbar ui-scale-fixed", "aria-label": t("layout.appShell.headerLabel"), children: [
52
- hasDrawer && /* @__PURE__ */ jsxs(Sheet, { open: drawerOpen, onOpenChange: setDrawerOpen, children: [
53
- /* @__PURE__ */ jsx(SheetTrigger, { asChild: true, children: /* @__PURE__ */ jsx(
54
- Button,
55
- {
56
- type: "button",
57
- variant: "ghost",
58
- size: "sm",
59
- className: "app-mobile-nav-trigger hidden max-[900px]:inline-flex",
60
- "aria-label": t("layout.appShell.openNav"),
61
- "aria-haspopup": "dialog",
62
- children: /* @__PURE__ */ jsx(Menu, { className: "size-5", "aria-hidden": "true" })
63
- }
64
- ) }),
65
- /* @__PURE__ */ jsxs(
66
- SheetContent,
67
- {
68
- side: "left",
69
- width: "var(--app-shell-mobile-nav-width)",
70
- className: "app-mobile-nav-drawer",
71
- overlayClassName: "app-mobile-nav-overlay",
72
- children: [
73
- /* @__PURE__ */ jsx(SheetHeader, { title: mobileNavLabel ?? t("layout.appShell.navLabel") }),
74
- /* @__PURE__ */ jsx(
75
- SheetBody,
76
- {
77
- className: "app-mobile-nav-body px-[var(--app-shell-mobile-nav-inset)]",
78
- onClick: handleDrawerClick,
79
- children: drawerNav
80
- }
81
- )
82
- ]
83
- }
84
- )
85
- ] }),
86
- resolvedTopbar
87
- ] }),
90
+ topbarSpan === "full" ? bar : rail,
91
+ topbarSpan === "full" ? rail : bar,
88
92
  /* @__PURE__ */ jsxs("main", { className: "app-main", "aria-label": t("layout.appShell.mainLabel"), tabIndex: 0, children: [
89
93
  breadcrumb !== void 0 && /* @__PURE__ */ jsx("div", { className: "app-breadcrumb", children: breadcrumb }),
90
94
  children
@@ -3,8 +3,8 @@ import type { ErrorSurfaceProp } from "../../props/components/layout.prop.js";
3
3
  export type { ErrorSurfaceMaintenanceProp, ErrorSurfaceProp, ErrorSurfaceProp as ErrorSurfaceProps, } from "../../props/components/layout.prop.js";
4
4
  export type { ErrorSurfaceModeProp, ErrorSurfaceStatusProp } from "../../props/vocabulary/index.js";
5
5
  /**
6
- * ErrorSurface — the package-owned semantic exception surface for **403 · 404 · 500 · 503**
7
- * (gh#221, gh#251).
6
+ * ErrorSurface — the package-owned semantic exception surface for **400 · 403 · 404 · 500 · 503**
7
+ * (gh#221, gh#251, gh#301).
8
8
  *
9
9
  * It exists because the alternative — every app composing `EmptyState` + `Flex` + `Text` inside a
10
10
  * hand-picked shell — is unimportable: a consumer cannot `import` a docs page, so the four
@@ -12,7 +12,7 @@ export type { ErrorSurfaceModeProp, ErrorSurfaceStatusProp } from "../../props/v
12
12
  * `.canonical-auth-card`, a bespoke `min-h-dvh` class). What the surface owns is the CONTRACT the
13
13
  * composition kept losing:
14
14
  *
15
- * - **`mode` is the shell contract.** `application` (403/404) renders the surface as the BODY you
15
+ * - **`mode` is the shell contract.** `application` (400/403/404) renders the surface as the BODY you
16
16
  * place inside the `AppShell` the route already provides, so the sidebar, topbar and breadcrumb
17
17
  * are PRESERVED — it never reconstructs navigation chrome, because nav data and the user menu are
18
18
  * consumer-owned and a component cannot manufacture them. `system` (500/503) owns the page and
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
- import { SearchX, ServerCrash, ShieldAlert, Wrench } from "lucide-react";
4
+ import { SearchX, ServerCrash, ShieldAlert, TriangleAlert, Wrench } from "lucide-react";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { cn } from "../../lib/utils.js";
7
7
  import { EmptyState } from "../data-display/empty-state.js";
@@ -9,6 +9,7 @@ import { Progress } from "../data-display/progress.js";
9
9
  import { Text } from "../general/typography.js";
10
10
  import { CenteredShell } from "./centered-shell.js";
11
11
  const STATUS_META = {
12
+ 400: { icon: TriangleAlert, tone: "warning" },
12
13
  403: { icon: ShieldAlert, tone: "warning" },
13
14
  404: { icon: SearchX, tone: "muted" },
14
15
  500: { icon: ServerCrash, tone: "destructive" },
@@ -149,7 +149,7 @@ const AppSettingPicker = React.forwardRef(
149
149
  // (`data-[state=open]:border-ring`, from controlTriggerClass) and the
150
150
  // focus-visible ring are untouched, so keyboard and "is this open" affordance
151
151
  // still hold.
152
- "ui-app-setting-picker-icon w-[length:var(--control-height)] justify-center border-transparent bg-transparent ps-0 pe-0 shadow-none hover:bg-accent hover:text-accent-foreground"
152
+ "ui-app-setting-picker-icon hover:bg-accent hover:text-accent-foreground w-[length:var(--control-height)] justify-center border-transparent bg-transparent ps-0 pe-0 shadow-none"
153
153
  ) : (
154
154
  // Labeled: sized to a per-kind width from `sm` up; below `sm` it hugs its content and
155
155
  // caps at the container (`w-auto max-w-full`) instead of the old UNCONDITIONAL
@@ -37,7 +37,7 @@ export declare const EMAIL_COLOR_SOURCE: {
37
37
  };
38
38
  readonly primary: {
39
39
  readonly cssVar: "--primary";
40
- readonly hsl: "204 100% 39%";
40
+ readonly hsl: "204 100% 37%";
41
41
  };
42
42
  readonly primaryForeground: {
43
43
  readonly cssVar: "--primary-foreground";
@@ -29,7 +29,7 @@ const EMAIL_COLOR_SOURCE = {
29
29
  },
30
30
  "primary": {
31
31
  "cssVar": "--primary",
32
- "hsl": "204 100% 39%"
32
+ "hsl": "204 100% 37%"
33
33
  },
34
34
  "primaryForeground": {
35
35
  "cssVar": "--primary-foreground",
@@ -88,6 +88,12 @@ export type FormProp = React.FormHTMLAttributes<HTMLFormElement> & {
88
88
  collapseBelow?: BreakpointProp | false;
89
89
  columns?: ResponsiveGridColumnsProp;
90
90
  density?: DensityProp;
91
+ /**
92
+ * Render the caller's own element instead of a `<form>`, keeping only the layout context.
93
+ * For routing libraries that own the form element (Inertia, TanStack Form) — two `<form>`
94
+ * elements cannot nest. `columns` does not apply in this mode; wrap fields in ResponsiveGrid.
95
+ */
96
+ asChild?: boolean;
91
97
  className?: ClassNameProp;
92
98
  };
93
99
  /**
@@ -222,6 +222,20 @@ export type AppShellProp = {
222
222
  * rail; it intentionally suppresses the redundant mobile drawer trigger.
223
223
  */
224
224
  responsiveNavigation?: "drawer" | "docked";
225
+ /**
226
+ * Which columns the topbar spans.
227
+ *
228
+ * - `"content"` (default) starts the topbar beside the sidebar, so the rail runs the full height
229
+ * of the window and the bar sits over the content only. The admin-console arrangement.
230
+ * - `"full"` runs the topbar edge to edge across the top with the sidebar starting beneath it —
231
+ * the arrangement products use when the bar carries space-level chrome (global search, account,
232
+ * notifications) that outranks the current section rather than belonging to it.
233
+ *
234
+ * Not cosmetic: it changes what the bar reads as owning. `"full"` also renders the `<header>`
235
+ * before the `<aside>` so keyboard order follows the visual order — a grid area alone would put
236
+ * focus in the rail while the eye starts at the bar (WCAG 2.4.3).
237
+ */
238
+ topbarSpan?: "content" | "full";
225
239
  /**
226
240
  * Navigation shown in the mobile drawer at the DXS 900px breakpoint, where the docked sidebar is
227
241
  * hidden. AppShell OWNS the drawer: it renders a hamburger trigger in the topbar and a focus-
@@ -431,10 +445,10 @@ export type ErrorSurfaceMaintenanceProp = {
431
445
  progress?: number;
432
446
  };
433
447
  /**
434
- * @see ErrorSurface — the package-owned semantic exception surface for 403 / 404 / 500 / 503.
448
+ * @see ErrorSurface — the package-owned semantic exception surface for 400 / 403 / 404 / 500 / 503.
435
449
  *
436
450
  * The `mode` is the SHELL CONTRACT, not a skin:
437
- * - `mode="application"` (403/404) renders the surface as the **body** you put inside the
451
+ * - `mode="application"` (400/403/404) renders the surface as the **body** you put inside the
438
452
  * `AppShell` the route already provides (normally within a `PageContainer`). It deliberately does
439
453
  * NOT reconstruct navigation chrome: the sidebar, topbar and user menu are consumer-owned data,
440
454
  * so the surface preserves the shell it is placed in instead of manufacturing a fake one.
@@ -450,7 +464,7 @@ export type ErrorSurfaceMaintenanceProp = {
450
464
  * `t()`); the surface owns only its own metadata labels, which it localizes itself.
451
465
  */
452
466
  export type ErrorSurfaceProp = {
453
- /** Where the surface lives — `application` = AppShell body (403/404), `system` = own page (500/503). */
467
+ /** Where the surface lives — `application` = AppShell body (400/403/404), `system` = own page (500/503). */
454
468
  mode: ErrorSurfaceModeProp;
455
469
  /** HTTP status presented. Drives the default `icon`, `tone` and the rendered status code. */
456
470
  status: ErrorSurfaceStatusProp;
@@ -464,9 +478,12 @@ export type ErrorSurfaceProp = {
464
478
  * development-time error.
465
479
  */
466
480
  action: ActionProp;
467
- /** Override the status-derived icon (403 ShieldAlert · 404 SearchX · 500 ServerCrash · 503 Wrench). */
481
+ /**
482
+ * Override the status-derived icon (400 TriangleAlert · 403 ShieldAlert · 404 SearchX ·
483
+ * 500 ServerCrash · 503 Wrench).
484
+ */
468
485
  icon?: IconProp;
469
- /** Override the status-derived tone (403/503 `warning` · 404 `muted` · 500 `destructive`). */
486
+ /** Override the status-derived tone (400/403/503 `warning` · 404 `muted` · 500 `destructive`). */
470
487
  tone?: EmptyStateToneProp;
471
488
  /**
472
489
  * Semantic heading level of `title`. Defaults to `2` in `application` mode (a `PageContainer`
@@ -217,12 +217,12 @@ export declare const VOCABULARY_REGISTRY: {
217
217
  readonly ErrorSurfaceModeProp: {
218
218
  readonly file: "vocabulary/layout.prop.ts";
219
219
  readonly category: "layout";
220
- readonly description: "ErrorSurface shell contract — application (403/404 body inside the route's existing AppShell) vs system (500/503 owns the page via CenteredShell align=center)";
220
+ readonly description: "ErrorSurface shell contract — application (400/403/404 body inside the route's existing AppShell) vs system (500/503 owns the page via CenteredShell align=center)";
221
221
  };
222
222
  readonly ErrorSurfaceStatusProp: {
223
223
  readonly file: "vocabulary/layout.prop.ts";
224
224
  readonly category: "layout";
225
- readonly description: "HTTP status an ErrorSurface presents — 403 | 404 | 500 | 503 (numeric); drives the default icon, tone and mode";
225
+ readonly description: "HTTP status an ErrorSurface presents — 400 | 403 | 404 | 500 | 503 (numeric); drives the default icon, tone and mode";
226
226
  };
227
227
  readonly AuthShellPresetProp: {
228
228
  readonly file: "vocabulary/layout.prop.ts";
@@ -215,12 +215,12 @@ const VOCABULARY_REGISTRY = {
215
215
  ErrorSurfaceModeProp: {
216
216
  file: "vocabulary/layout.prop.ts",
217
217
  category: "layout",
218
- description: "ErrorSurface shell contract \u2014 application (403/404 body inside the route's existing AppShell) vs system (500/503 owns the page via CenteredShell align=center)"
218
+ description: "ErrorSurface shell contract \u2014 application (400/403/404 body inside the route's existing AppShell) vs system (500/503 owns the page via CenteredShell align=center)"
219
219
  },
220
220
  ErrorSurfaceStatusProp: {
221
221
  file: "vocabulary/layout.prop.ts",
222
222
  category: "layout",
223
- description: "HTTP status an ErrorSurface presents \u2014 403 | 404 | 500 | 503 (numeric); drives the default icon, tone and mode"
223
+ description: "HTTP status an ErrorSurface presents \u2014 400 | 403 | 404 | 500 | 503 (numeric); drives the default icon, tone and mode"
224
224
  },
225
225
  AuthShellPresetProp: {
226
226
  file: "vocabulary/layout.prop.ts",
@@ -24,6 +24,14 @@ export type ColumnDefProp<T> = {
24
24
  ariaLabel?: string;
25
25
  render?: (row: T) => React.ReactNode;
26
26
  sortable?: boolean;
27
+ /**
28
+ * Column width: either a utility class (`"w-[300px]"`) or a CSS length
29
+ * (`"300px"`, `"20%"`, `"calc(50% - 1rem)"`). Both work — a length is
30
+ * applied inline, anything else is treated as a class.
31
+ *
32
+ * It used to be class-only, and a length silently became a dead class name,
33
+ * so the column fell back to auto layout with nothing to indicate why.
34
+ */
27
35
  width?: string;
28
36
  align?: ColumnAlignProp;
29
37
  hiddenOnMobile?: boolean;
@@ -32,12 +32,17 @@ export type CenteredShellAlignProp = "start" | "center";
32
32
  */
33
33
  export type ErrorSurfaceModeProp = "application" | "system";
34
34
  /**
35
- * The HTTP status an ErrorSurface presents. Deliberately closed to the four exception pages every
36
- * app ships (RFC 9110 §15.5.4 / §15.5.5 / §15.6.1 / §15.6.4); it drives the default icon, tone and
37
- * the recommended `mode`. A number, never a string — it is the numeric status, and it is rendered
38
- * with tabular figures.
35
+ * The HTTP status an ErrorSurface presents. Deliberately closed to the five exception pages every
36
+ * app ships (RFC 9110 §15.5.1 / §15.5.4 / §15.5.5 / §15.6.1 / §15.6.4); it drives the default icon,
37
+ * tone and the recommended `mode`. A number, never a string — it is the numeric status, and it is
38
+ * rendered with tabular figures.
39
+ *
40
+ * `400` (gh#301) is the malformed-request page: a route reached with parameters the server refuses
41
+ * to interpret (a bad id, a missing launch parameter, a hand-edited query string). It is an
42
+ * `application` failure like 403/404 — the app itself is healthy, so the shell stays and the
43
+ * viewer navigates away from it.
39
44
  */
40
- export type ErrorSurfaceStatusProp = 403 | 404 | 500 | 503;
45
+ export type ErrorSurfaceStatusProp = 400 | 403 | 404 | 500 | 503;
41
46
  /**
42
47
  * AuthShell named flow preset — the page MEASURE contract for a canonical hosted-identity flow
43
48
  * (card max-width plus the desktop and mobile page gutters), owned by component tokens.
@@ -39,8 +39,7 @@
39
39
  :root {
40
40
  --font-sans-base:
41
41
  "Noto Sans JP", "Hiragino Sans", "Hiragino Kaku Gothic ProN", "Yu Gothic Medium", YuGothic,
42
- "M PLUS 2", Meiryo, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui,
43
- sans-serif;
42
+ "M PLUS 2", Meiryo, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui, sans-serif;
44
43
  --font-sans-vi:
45
44
  "Noto Sans JP", "M PLUS 2", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, system-ui,
46
45
  sans-serif;
@@ -613,9 +613,24 @@
613
613
  gap: var(--space-inline-lg);
614
614
  }
615
615
 
616
+ /* `extra` sizes to its content but STAYS SHRINKABLE (gh#300). `width: auto` releases the
617
+ * base rule's full-width stretch so a one-button header hugs the end edge; the historical
618
+ * `flex-shrink: 0` next to it then froze the box at the action group's max-content width,
619
+ * which is unbounded — an admin header with 10–13 buttons asks for ~1594px inside a 1408px
620
+ * content column. Because the box could not shrink, its own `flex-wrap: wrap` never had a
621
+ * narrower width to wrap into, the whole deficit was charged to `.ui-page-header-heading`
622
+ * (`min-w-0`, so it collapses to 0), and the `<h1>` wrapped one CJK character per line while
623
+ * the action group still overflowed the page. The <=720px escape hatch above
624
+ * (`.ui-page-header-extra > .ui-flex { max-width: 100% }`) could not help either: a
625
+ * percentage resolved against a fit-content parent is circular.
626
+ *
627
+ * `flex-shrink: 1` + `min-inline-size: 0` keep the max-content base size (nothing moves on a
628
+ * header that already fits) but let the row charge the overflow to `extra` too, so the wrap
629
+ * it already declares finally engages and the title keeps a readable measure. */
616
630
  .ui-page-header-extra {
617
631
  width: auto;
618
- flex-shrink: 0;
632
+ flex-shrink: 1;
633
+ min-inline-size: 0;
619
634
  justify-content: flex-end;
620
635
  }
621
636
  }
@@ -25,6 +25,20 @@
25
25
  grid-template-columns: var(--app-shell-rail-width) minmax(0, 1fr);
26
26
  }
27
27
 
28
+ /* topbarSpan="full" — the bar runs edge to edge and the rail starts beneath it, for products
29
+ * whose bar carries space-level chrome (global search, account, notifications) that outranks the
30
+ * current section instead of belonging to it. Only the row assignment changes: both track sizes
31
+ * stay token-owned, so --app-shell-sidebar-width and the collapsed rail still apply untouched.
32
+ * Below the 900px breakpoint the grid already collapses to a single column, which IS this
33
+ * arrangement, so no narrow override is needed here — only the docked mode, which rebuilds the
34
+ * two-track grid, needs restating (see the breakpoint block). */
35
+ .app-root[data-topbar-span="full"] {
36
+ grid-template-areas:
37
+ "topbar topbar"
38
+ "sidebar main"
39
+ "sidebar footer";
40
+ }
41
+
28
42
  .app-sidebar {
29
43
  grid-area: sidebar;
30
44
  display: flex;
@@ -672,6 +686,15 @@
672
686
  grid-template-columns: var(--app-shell-rail-width) minmax(0, 1fr);
673
687
  }
674
688
 
689
+ /* Docked mode rebuilds the two-track grid at narrow widths, which would otherwise undo
690
+ * topbarSpan="full" exactly where the bar's space-level controls matter most. */
691
+ .app-root[data-responsive-navigation="docked"][data-topbar-span="full"] {
692
+ grid-template-areas:
693
+ "topbar topbar"
694
+ "sidebar main"
695
+ "sidebar footer";
696
+ }
697
+
675
698
  .app-root[data-responsive-navigation="docked"] > .app-sidebar {
676
699
  display: flex;
677
700
  }
@@ -932,7 +955,7 @@
932
955
  gap: var(--sidebar-nav-item-gap);
933
956
  padding-inline: var(--sidebar-nav-item-padding-x);
934
957
  border: 0;
935
- border-radius: calc(var(--radius) - 1px);
958
+ border-radius: var(--sidebar-nav-item-radius);
936
959
  background: transparent;
937
960
  /* gh#228 — the row/label foreground is its own knob (default = the muted role, unchanged). The
938
961
  * icon reads a SEPARATE knob on `.sb-icon` below, so a service can darken 16px nav icons
@@ -13,6 +13,13 @@
13
13
  /* Column gap between the label and its control in horizontal/inline layout. */
14
14
  --form-label-gap: var(--space-4); /* 16px */
15
15
 
16
+ /* Type in the label column. Inherits the body size by default, so nothing moves unless a
17
+ * service opts in. It is a knob because the label column already is one: --form-label-width
18
+ * lets a service align forms to its grid, and a service whose grid was drawn around a
19
+ * smaller label had no way to say so — leaving it to hand-write font-size per label, which
20
+ * is exactly what the width token exists to prevent. */
21
+ --form-label-font-size: var(--text-sm); /* 14px */
22
+
16
23
  /* Row rhythm between a Form's own top-level blocks (gh#295) — e.g. the field group's
17
24
  * CardContent and its CardFooter action row, or two FormFields that happen to be Form's OWN
18
25
  * direct children (no Card framing). Unchanged from the historical value. */
@@ -70,6 +70,13 @@
70
70
  --sidebar-nav-item-gap: 0.625rem;
71
71
  --sidebar-nav-item-padding-x: 0.625rem;
72
72
  --sidebar-nav-gap: 2px;
73
+ /* Corner radius of a nav row. Defaults to the global rail radius, which is what an inset pill
74
+ * wants — but a rail that sets --sidebar-nav-scroll-padding and --sidebar-nav-gap to 0 makes its
75
+ * rows full-bleed bands flush with both edges, and a band wants square corners. Every other
76
+ * geometry property of `.sb-nav-item` was already a knob; this one was pinned to a constant, so
77
+ * the only way to square a band was a consumer selector against a DS internal class — the exact
78
+ * coupling rule #45 exists to prevent. Set `0` for full-bleed bands. */
79
+ --sidebar-nav-item-radius: calc(var(--radius) - 1px);
73
80
  --sidebar-nav-scroll-padding: var(--space-3) var(--space-2);
74
81
  --sidebar-section-gap: var(--space-4);
75
82
  --sidebar-section-label-padding-x: var(--space-2);
@@ -9,7 +9,16 @@
9
9
  --card-foreground: 48 8% 13%;
10
10
  --popover: 60 33% 99%;
11
11
  --popover-foreground: 48 8% 13%;
12
- --primary: 204 100% 39%; /* SmartHR MAIN #0077c7 */
12
+ /* SmartHR MAIN, two lightness steps down from #0077c7 (gh#299).
13
+ *
14
+ * The published blue measures 4.62:1 as TEXT on --background — compliant,
15
+ * but with 0.12 of headroom, which every tint this system applies to a row
16
+ * then spends: the zebra stripe (muted/0.4) takes it to 4.46 and hover
17
+ * (muted/0.5) to 4.43. A link inside a striped table therefore failed AA
18
+ * while the same link on a card passed, which is not a defensible place to
19
+ * draw that line. 37% clears every one of those states with margin and is
20
+ * indistinguishable from the original side by side. */
21
+ --primary: 204 100% 37%; /* SmartHR MAIN #0071bd */
13
22
  --primary-foreground: 60 33% 99%;
14
23
  --secondary: 45 15% 95%; /* #f4f3f0 */
15
24
  --secondary-foreground: 48 8% 13%;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "18.12.20",
4
- "godxUiMcp": "18.12.20",
3
+ "version": "18.13.0",
4
+ "godxUiMcp": "18.13.0",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",