@godxjp/ui 18.6.0 → 18.7.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.
Files changed (32) hide show
  1. package/dist/components/feedback/alert.d.ts +20 -2
  2. package/dist/components/feedback/alert.js +11 -1
  3. package/dist/components/feedback/index.d.ts +2 -2
  4. package/dist/components/feedback/index.js +3 -1
  5. package/dist/components/layout/auth-shell.d.ts +4 -2
  6. package/dist/components/layout/index.d.ts +2 -0
  7. package/dist/components/layout/index.js +2 -0
  8. package/dist/components/layout/page-container.d.ts +1 -1
  9. package/dist/components/layout/page-container.js +19 -41
  10. package/dist/components/layout/page-header.d.ts +22 -0
  11. package/dist/components/layout/page-header.js +89 -0
  12. package/dist/components/navigation/filter-bar.d.ts +2 -2
  13. package/dist/components/navigation/filter-bar.js +44 -1
  14. package/dist/components/navigation/index.d.ts +1 -1
  15. package/dist/i18n/messages/en.json +11 -0
  16. package/dist/i18n/messages/ja.json +8 -0
  17. package/dist/i18n/messages/vi.json +8 -0
  18. package/dist/props/components/feedback.prop.d.ts +6 -0
  19. package/dist/props/components/layout.prop.d.ts +53 -0
  20. package/dist/props/components/navigation.prop.d.ts +52 -0
  21. package/dist/props/registry.d.ts +60 -1
  22. package/dist/props/registry.js +81 -1
  23. package/dist/props/vocabulary/interaction.prop.d.ts +10 -2
  24. package/dist/props/vocabulary/layout.prop.d.ts +5 -2
  25. package/dist/styles/alert-layout.css +36 -0
  26. package/dist/styles/layout.css +113 -0
  27. package/dist/styles/shell-layout.css +34 -0
  28. package/dist/tokens/components/feedback.css +20 -0
  29. package/dist/tokens/components/navigation.css +26 -0
  30. package/dist/tokens/components/shell.css +37 -0
  31. package/dist/tokens/semantic/layout.css +13 -0
  32. package/package.json +2 -2
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import type { ToneProp } from "../../props/vocabulary/index.js";
3
- import type { AlertQueryErrorProp } from "../../props/components/feedback.prop.js";
4
- export type { AlertProp, AlertProp as AlertProps, AlertTitleProp, AlertTitleProp as AlertTitleProps, AlertContentProp, AlertContentProp as AlertContentProps, AlertDescriptionProp, AlertDescriptionProp as AlertDescriptionProps, AlertActionsProp, AlertActionsProp as AlertActionsProps, AlertQueryErrorProp, AlertQueryErrorProp as AlertQueryErrorProps, } from "../../props/components/feedback.prop.js";
3
+ import type { AlertQueryErrorProp, BannerProp } from "../../props/components/feedback.prop.js";
4
+ export type { BannerProp, BannerProp as BannerProps, AlertProp, AlertProp as AlertProps, AlertTitleProp, AlertTitleProp as AlertTitleProps, AlertContentProp, AlertContentProp as AlertContentProps, AlertDescriptionProp, AlertDescriptionProp as AlertDescriptionProps, AlertActionsProp, AlertActionsProp as AlertActionsProps, AlertQueryErrorProp, AlertQueryErrorProp as AlertQueryErrorProps, } from "../../props/components/feedback.prop.js";
5
5
  declare const AlertBase: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & {
6
6
  variant?: import("../../props/index.js").AlertVariantProp;
7
7
  tone?: ToneProp;
@@ -63,4 +63,22 @@ export declare const Alert: React.ForwardRefExoticComponent<React.HTMLAttributes
63
63
  } & React.RefAttributes<HTMLDivElement>>;
64
64
  QueryError: typeof AlertQueryError;
65
65
  };
66
+ export declare const Banner: React.ForwardRefExoticComponent<BannerProp & React.RefAttributes<HTMLDivElement>> & {
67
+ Title: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & {
68
+ className?: import("../../props/index.js").ClassNameProp;
69
+ children?: import("../../props/index.js").ChildrenProp;
70
+ } & React.RefAttributes<HTMLParagraphElement>>;
71
+ Content: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & {
72
+ className?: import("../../props/index.js").ClassNameProp;
73
+ children?: import("../../props/index.js").ChildrenProp;
74
+ } & React.RefAttributes<HTMLDivElement>>;
75
+ Description: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLParagraphElement> & {
76
+ className?: import("../../props/index.js").ClassNameProp;
77
+ children?: import("../../props/index.js").ChildrenProp;
78
+ } & React.RefAttributes<HTMLParagraphElement>>;
79
+ Actions: React.ForwardRefExoticComponent<React.HTMLAttributes<HTMLDivElement> & {
80
+ className?: import("../../props/index.js").ClassNameProp;
81
+ children?: import("../../props/index.js").ChildrenProp;
82
+ } & React.RefAttributes<HTMLDivElement>>;
83
+ };
66
84
  export { AlertBase };
@@ -159,6 +159,15 @@ const Alert = Object.assign(AlertBase, {
159
159
  Actions: AlertActions,
160
160
  QueryError: AlertQueryError
161
161
  });
162
+ const BannerBase = React.forwardRef(function Banner(props, ref) {
163
+ return /* @__PURE__ */ jsx(AlertBase, { ...props, ref, variant: "banner" });
164
+ });
165
+ const Banner2 = Object.assign(BannerBase, {
166
+ Title: AlertTitle,
167
+ Content: AlertContent,
168
+ Description: AlertDescription,
169
+ Actions: AlertActions
170
+ });
162
171
  export {
163
172
  Alert,
164
173
  AlertActions,
@@ -166,5 +175,6 @@ export {
166
175
  AlertContent,
167
176
  AlertDescription,
168
177
  AlertQueryError,
169
- AlertTitle
178
+ AlertTitle,
179
+ Banner2 as Banner
170
180
  };
@@ -8,6 +8,6 @@ export { Toaster } from "./sonner.js";
8
8
  export { toast } from "./use-toast.js";
9
9
  export { Skeleton, SkeletonRows, SkeletonTable, SkeletonDetail, SkeletonStat } from "./skeleton.js";
10
10
  export type { SkeletonProps } from "./skeleton.js";
11
- export { Alert, AlertTitle, AlertContent, AlertDescription, AlertActions, AlertQueryError, } from "./alert.js";
12
- export type { AlertProp, AlertProps, AlertTitleProp, AlertTitleProps, AlertContentProp, AlertContentProps, AlertDescriptionProp, AlertDescriptionProps, AlertActionsProp, AlertActionsProps, AlertQueryErrorProp, AlertQueryErrorProps, } from "./alert.js";
11
+ export { Alert, AlertTitle, AlertContent, AlertDescription, AlertActions, AlertQueryError, Banner, } from "./alert.js";
12
+ export type { BannerProp, BannerProps, AlertProp, AlertProps, AlertTitleProp, AlertTitleProps, AlertContentProp, AlertContentProps, AlertDescriptionProp, AlertDescriptionProps, AlertActionsProp, AlertActionsProps, AlertQueryErrorProp, AlertQueryErrorProps, } from "./alert.js";
13
13
  export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./tooltip.js";
@@ -49,7 +49,8 @@ import {
49
49
  AlertContent,
50
50
  AlertDescription,
51
51
  AlertActions,
52
- AlertQueryError
52
+ AlertQueryError,
53
+ Banner
53
54
  } from "./alert.js";
54
55
  import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from "./tooltip.js";
55
56
  export {
@@ -71,6 +72,7 @@ export {
71
72
  AlertDialogTrigger,
72
73
  AlertQueryError,
73
74
  AlertTitle,
75
+ Banner,
74
76
  Dialog,
75
77
  DialogAction,
76
78
  DialogBody,
@@ -10,8 +10,10 @@ export type { AuthShellProp, AuthShellProp as AuthShellProps, } from "../../prop
10
10
  *
11
11
  * `preset` names the flow MEASURE (card max-width + desktop/mobile page gutters) so a consumer
12
12
  * never overrides page geometry by hand: `"login"` = SCR-001's stable identity/card/footer anchor
13
- * for standalone and real requester states; `"device-authorization"` = 380px card with a 5px
14
- * inline gutter at 390px; `"context-selection"` = 25rem card, edge-to-edge on mobile;
13
+ * for standalone and real requester states; `"registration"` = the 360px sign-up measure, the only
14
+ * START-aligned preset (a tall sign-up card must scroll, not clip its own top above the scroll
15
+ * origin) and the only one with its own footer clearance; `"device-authorization"` = 380px card
16
+ * with a 5px inline gutter at 390px; `"context-selection"` = 25rem card, edge-to-edge on mobile;
15
17
  * `"account-recovery"` = the 432px SCR-008 panel measure shared by password recovery and the
16
18
  * sign-in MFA challenge, with a 15px inline gutter at 390px. It is orthogonal to `variant` —
17
19
  * combine `variant="canonical"` with any preset.
@@ -1,5 +1,7 @@
1
1
  export { PageContainer } from "./page-container.js";
2
2
  export type { PageContainerProp, PageContainerProps, BreadcrumbItem, BreadcrumbItemProp, } from "./page-container.js";
3
+ export { PageHeader } from "./page-header.js";
4
+ export type { PageHeaderProp, PageHeaderProps } from "./page-header.js";
3
5
  export { Flex } from "./flex.js";
4
6
  export type { FlexAlignProp, FlexDirectionProp, FlexJustifyProp, FlexProp, FlexProps, } from "./flex.js";
5
7
  export { ResizablePanel, ResizablePanelGroup, ResizableHandle } from "./resizable.js";
@@ -1,4 +1,5 @@
1
1
  import { PageContainer } from "./page-container.js";
2
+ import { PageHeader } from "./page-header.js";
2
3
  import { Flex } from "./flex.js";
3
4
  import { ResizablePanel, ResizablePanelGroup, ResizableHandle } from "./resizable.js";
4
5
  import { AppShell } from "./app-shell.js";
@@ -37,6 +38,7 @@ export {
37
38
  MasterDetail,
38
39
  OrgSwitcher,
39
40
  PageContainer,
41
+ PageHeader,
40
42
  ResizableHandle,
41
43
  ResizablePanel,
42
44
  ResizablePanelGroup,
@@ -2,7 +2,7 @@ import type { PageContainerProp, PageInsetProp } from "../../props/components/la
2
2
  export type { PageContainerProp, PageContainerProp as PageContainerProps, } from "../../props/components/layout.prop.js";
3
3
  export type { BreadcrumbItemProp, BreadcrumbItemProp as BreadcrumbItem, } from "../../props/vocabulary/navigation.prop.js";
4
4
  export declare function PageContainerInset({ className, children, ...props }: PageInsetProp): import("react").JSX.Element;
5
- declare function PageContainerRoot({ title, subtitle, extra, footer, breadcrumb, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, density, variant, preset, headerLayout, measure, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react").JSX.Element;
5
+ declare function PageContainerRoot({ title, subtitle, meta, headerLoading, extra, footer, breadcrumb, breadcrumbLabel, breadcrumbAriaLabel, linkComponent: LinkComponent, density, variant, preset, headerLayout, measure, stickyFooter, footerReveal, fill, children, className, }: PageContainerProp): import("react").JSX.Element;
6
6
  export declare const PageContainer: typeof PageContainerRoot & {
7
7
  Inset: typeof PageContainerInset;
8
8
  };
@@ -1,10 +1,9 @@
1
1
  "use client";
2
2
  import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import { useEffect, useRef, useState } from "react";
4
- import { ChevronRight } from "lucide-react";
5
- import { useTranslation } from "../../i18n/use-translation.js";
6
4
  import { cn } from "../../lib/utils.js";
7
5
  import { densityClass, pageContainerVariantClass } from "../../lib/variants.js";
6
+ import { PageHeader } from "./page-header.js";
8
7
  function scrollParent(el) {
9
8
  let node = el?.parentElement ?? null;
10
9
  while (node) {
@@ -36,6 +35,8 @@ function PageContainerInset({ className, children, ...props }) {
36
35
  function PageContainerRoot({
37
36
  title,
38
37
  subtitle,
38
+ meta,
39
+ headerLoading,
39
40
  extra,
40
41
  footer,
41
42
  breadcrumb,
@@ -55,7 +56,6 @@ function PageContainerRoot({
55
56
  }) {
56
57
  const reveal = stickyFooter && footer != null && footerReveal === "onScroll";
57
58
  const { headerRef, revealed } = useFooterReveal(reveal);
58
- const { t } = useTranslation();
59
59
  return /* @__PURE__ */ jsxs(
60
60
  "div",
61
61
  {
@@ -74,44 +74,22 @@ function PageContainerRoot({
74
74
  className
75
75
  ),
76
76
  children: [
77
- /* @__PURE__ */ jsxs("header", { ref: headerRef, className: "ui-page-header", "data-layout": headerLayout, children: [
78
- breadcrumb && breadcrumb.length > 0 && /* @__PURE__ */ jsx(
79
- "nav",
80
- {
81
- "aria-label": breadcrumbLabel ?? breadcrumbAriaLabel ?? t("navigation.breadcrumb.ariaLabel"),
82
- className: "ui-breadcrumb",
83
- children: /* @__PURE__ */ jsx("ol", { className: "ui-breadcrumb-list", children: breadcrumb.map((item, i) => {
84
- const isLast = i === breadcrumb.length - 1;
85
- return /* @__PURE__ */ jsxs("li", { className: "ui-inline-xs", children: [
86
- item.to && !isLast ? /* @__PURE__ */ jsx(
87
- LinkComponent,
88
- {
89
- href: item.to,
90
- to: item.to,
91
- className: "hover:text-foreground hover:underline",
92
- children: item.label
93
- }
94
- ) : /* @__PURE__ */ jsx(
95
- "span",
96
- {
97
- className: isLast ? "text-foreground" : "",
98
- "aria-current": isLast ? "page" : void 0,
99
- children: item.label
100
- }
101
- ),
102
- !isLast && /* @__PURE__ */ jsx(ChevronRight, { className: "size-3", "aria-hidden": "true" })
103
- ] }, i);
104
- }) })
105
- }
106
- ),
107
- /* @__PURE__ */ jsxs("div", { className: "ui-page-header-row", children: [
108
- /* @__PURE__ */ jsxs("div", { className: "ui-page-header-heading min-w-0", children: [
109
- /* @__PURE__ */ jsx("h1", { className: "ui-page-title", children: title }),
110
- subtitle && /* @__PURE__ */ jsx("p", { className: "ui-page-subtitle", children: subtitle })
111
- ] }),
112
- extra && /* @__PURE__ */ jsx("div", { className: "ui-page-header-extra", children: extra })
113
- ] })
114
- ] }),
77
+ /* @__PURE__ */ jsx(
78
+ PageHeader,
79
+ {
80
+ ref: headerRef,
81
+ title,
82
+ subtitle,
83
+ meta,
84
+ extra,
85
+ breadcrumb,
86
+ breadcrumbLabel,
87
+ breadcrumbAriaLabel,
88
+ linkComponent: LinkComponent,
89
+ layout: headerLayout,
90
+ loading: headerLoading
91
+ }
92
+ ),
115
93
  children != null && /* @__PURE__ */ jsx("div", { className: "ui-page-body", children }),
116
94
  footer && /* @__PURE__ */ jsx("footer", { className: "ui-page-footer", children: footer })
117
95
  ]
@@ -0,0 +1,22 @@
1
+ /**
2
+ * PageHeader — the canonical page title band (gh#255).
3
+ *
4
+ * This is the geometry `PageContainer` has always rendered, lifted into its own export so a
5
+ * consumer can mount it OUTSIDE a full page shell — a Sheet detail, a `MasterDetail` detail pane, a
6
+ * tab body, a nested route header — without re-authoring `.ui-page-header` CSS locally. There is
7
+ * exactly ONE implementation: `PageContainer` renders this component, so the two can never drift.
8
+ *
9
+ * The band owns: breadcrumbs (a named `<nav>` landmark with `aria-current="page"` on the leaf), the
10
+ * `<h1>` title, the subtitle/description, a status/meta slot, the `extra` action region, the
11
+ * responsive overflow arrangement (`layout`), and the pending state (`loading`). It owns no page
12
+ * chrome — padding, measure and body rhythm stay with `PageContainer`.
13
+ *
14
+ * DENIED / ERROR are deliberately NOT states of this component: a page the user may not see must
15
+ * not render its title, breadcrumb trail or actions at all (they leak the resource's existence and
16
+ * its name). That whole-surface contract belongs to `ErrorSurface`, which is the canonical public
17
+ * replacement — see `docs/CANONICAL-CONTRACTS.md`.
18
+ */
19
+ import * as React from "react";
20
+ import type { PageHeaderProp } from "../../props/components/layout.prop.js";
21
+ export type { PageHeaderProp, PageHeaderProp as PageHeaderProps, } from "../../props/components/layout.prop.js";
22
+ export declare const PageHeader: React.ForwardRefExoticComponent<PageHeaderProp & React.RefAttributes<HTMLElement>>;
@@ -0,0 +1,89 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { ChevronRight } from "lucide-react";
5
+ import { useTranslation } from "../../i18n/use-translation.js";
6
+ import { cn } from "../../lib/utils.js";
7
+ const PageHeader = React.forwardRef(function PageHeader2({
8
+ title,
9
+ subtitle,
10
+ meta,
11
+ extra,
12
+ breadcrumb,
13
+ breadcrumbLabel,
14
+ breadcrumbAriaLabel,
15
+ linkComponent: LinkComponent = "a",
16
+ layout = "stack",
17
+ loading = false,
18
+ className
19
+ }, ref) {
20
+ const { t } = useTranslation();
21
+ return (
22
+ // `data-layout` is the only hook the header arrangement needs: `stack` (the default)
23
+ // matches no rule, so the historical geometry is untouched, while `responsive-inline`
24
+ // selects the compact-range rules that keep `extra` beside the title band (gh#231).
25
+ /* @__PURE__ */ jsxs(
26
+ "header",
27
+ {
28
+ ref,
29
+ className: cn("ui-page-header", className),
30
+ "data-layout": layout,
31
+ "aria-busy": loading ? "true" : void 0,
32
+ children: [
33
+ breadcrumb && breadcrumb.length > 0 && /* @__PURE__ */ jsx(
34
+ "nav",
35
+ {
36
+ "aria-label": breadcrumbLabel ?? breadcrumbAriaLabel ?? t("navigation.breadcrumb.ariaLabel"),
37
+ className: "ui-breadcrumb",
38
+ children: /* @__PURE__ */ jsx("ol", { className: "ui-breadcrumb-list", children: breadcrumb.map((item, i) => {
39
+ const isLast = i === breadcrumb.length - 1;
40
+ return /* @__PURE__ */ jsxs("li", { className: "ui-inline-xs", children: [
41
+ item.to && !isLast ? /* @__PURE__ */ jsx(
42
+ LinkComponent,
43
+ {
44
+ href: item.to,
45
+ to: item.to,
46
+ className: "hover:text-foreground hover:underline",
47
+ children: item.label
48
+ }
49
+ ) : /* @__PURE__ */ jsx(
50
+ "span",
51
+ {
52
+ className: isLast ? "text-foreground" : "",
53
+ "aria-current": isLast ? "page" : void 0,
54
+ children: item.label
55
+ }
56
+ ),
57
+ !isLast && /* @__PURE__ */ jsx(ChevronRight, { className: "size-3", "aria-hidden": "true" })
58
+ ] }, i);
59
+ }) })
60
+ }
61
+ ),
62
+ /* @__PURE__ */ jsxs("div", { className: "ui-page-header-row", children: [
63
+ /* @__PURE__ */ jsxs("div", { className: "ui-page-header-heading min-w-0", children: [
64
+ loading ? /* @__PURE__ */ jsx("h1", { className: "ui-page-title ui-skeleton-block ui-page-title-placeholder", children: /* @__PURE__ */ jsx("span", { className: "sr-only", children: t("layout.pageHeader.loading") }) }) : meta != null ? /* @__PURE__ */ jsxs("div", { className: "ui-page-title-band", children: [
65
+ /* @__PURE__ */ jsx("h1", { className: "ui-page-title", children: title }),
66
+ /* @__PURE__ */ jsx("div", { className: "ui-page-header-meta", children: meta })
67
+ ] }) : /* @__PURE__ */ jsx("h1", { className: "ui-page-title", children: title }),
68
+ loading ? (
69
+ // Decorative only — the pending state is already announced once by the heading above,
70
+ // so a second live placeholder here would double-announce it.
71
+ /* @__PURE__ */ jsx(
72
+ "p",
73
+ {
74
+ className: "ui-page-subtitle ui-skeleton-block ui-page-subtitle-placeholder",
75
+ "aria-hidden": "true"
76
+ }
77
+ )
78
+ ) : subtitle && /* @__PURE__ */ jsx("p", { className: "ui-page-subtitle", children: subtitle })
79
+ ] }),
80
+ extra && /* @__PURE__ */ jsx("div", { className: "ui-page-header-extra", children: extra })
81
+ ] })
82
+ ]
83
+ }
84
+ )
85
+ );
86
+ });
87
+ export {
88
+ PageHeader
89
+ };
@@ -1,6 +1,6 @@
1
1
  import type { ToolbarGroupProp, ToolbarProp } from "../../props/components/navigation.prop.js";
2
- export type { FilterBarOverflowProp, ToolbarGroupProp, ToolbarGroupProp as ToolbarGroupProps, ToolbarGroupProp as FilterBarGroupProp, ToolbarGroupProp as FilterBarGroupProps, ToolbarProp, ToolbarProp as ToolbarProps, ToolbarProp as FilterBarProp, ToolbarProp as FilterBarProps, } from "../../props/components/navigation.prop.js";
3
- export declare function Toolbar({ onClear, hasActiveFilters, sticky, overflow, className, children, }: ToolbarProp): import("react").JSX.Element;
2
+ export type { FilterBarChipProp, FilterBarChipProp as FilterBarChipProps, FilterBarOverflowProp, ToolbarGroupProp, ToolbarGroupProp as ToolbarGroupProps, ToolbarGroupProp as FilterBarGroupProp, ToolbarGroupProp as FilterBarGroupProps, ToolbarProp, ToolbarProp as ToolbarProps, ToolbarProp as FilterBarProp, ToolbarProp as FilterBarProps, } from "../../props/components/navigation.prop.js";
3
+ export declare function Toolbar({ onClear, hasActiveFilters, sticky, overflow, search, chips, onChipRemove, resultCount, actions, className, children, }: ToolbarProp): import("react").JSX.Element;
4
4
  export declare function ToolbarGroup({ label, controlId, className, children }: ToolbarGroupProp): import("react").JSX.Element;
5
5
  /**
6
6
  * Canonical list-page filter strip.
@@ -10,10 +10,16 @@ function Toolbar({
10
10
  hasActiveFilters = true,
11
11
  sticky = false,
12
12
  overflow = "wrap",
13
+ search,
14
+ chips,
15
+ onChipRemove,
16
+ resultCount,
17
+ actions,
13
18
  className,
14
19
  children
15
20
  }) {
16
21
  const { t } = useTranslation();
22
+ const hasChips = chips != null && chips.length > 0;
17
23
  return /* @__PURE__ */ jsxs(
18
24
  "div",
19
25
  {
@@ -22,11 +28,48 @@ function Toolbar({
22
28
  "data-overflow": overflow,
23
29
  className: cn("ui-toolbar", sticky && "ui-toolbar-sticky", className),
24
30
  children: [
31
+ search != null && // A region, not a bare child, so the search control gets ONE measure
32
+ // (--filter-bar-search-width) across every list page instead of whatever width the
33
+ // caller's control happened to have.
34
+ /* @__PURE__ */ jsx("div", { className: "ui-filter-bar-search", children: search }),
25
35
  children,
36
+ hasChips && // `group` (not a second toolbar) — a toolbar may own a group, and the chips are a labelled
37
+ // set of related controls rather than an independent toolbar competing for the same role.
38
+ /* @__PURE__ */ jsx(
39
+ "div",
40
+ {
41
+ role: "group",
42
+ "aria-label": t("navigation.filterBar.appliedFilters"),
43
+ className: "ui-filter-bar-chips",
44
+ children: chips.map((chip) => /* @__PURE__ */ jsxs("span", { className: "ui-filter-bar-chip", children: [
45
+ /* @__PURE__ */ jsx("span", { className: "ui-filter-bar-chip-label", children: chip.label }),
46
+ (chip.onRemove || onChipRemove) && /* @__PURE__ */ jsx(
47
+ Button,
48
+ {
49
+ variant: "ghost",
50
+ size: "xs",
51
+ className: "ui-filter-bar-chip-remove",
52
+ "aria-label": t("navigation.filterBar.removeFilter", { label: chip.label }),
53
+ onClick: () => {
54
+ chip.onRemove?.();
55
+ onChipRemove?.(chip.id);
56
+ },
57
+ children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
58
+ }
59
+ )
60
+ ] }, chip.id))
61
+ }
62
+ ),
63
+ resultCount !== void 0 && // The whole reason this is a prop rather than caller markup: a sighted user SEES the table
64
+ // change, and a polite live region is what tells everyone else that filtering happened.
65
+ // The number is grouped by `Intl.NumberFormat` and the noun selected by CLDR plural rules
66
+ // for the active locale — never a hand-built "N items".
67
+ /* @__PURE__ */ jsx("div", { role: "status", "aria-live": "polite", className: "ui-filter-bar-count", children: t("navigation.filterBar.resultCount", { count: resultCount }) }),
26
68
  onClear && hasActiveFilters && /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: onClear, className: "ui-toolbar-clear", children: [
27
69
  /* @__PURE__ */ jsx(X, { "aria-hidden": "true" }),
28
70
  t("common.clearFilters")
29
- ] })
71
+ ] }),
72
+ actions != null && /* @__PURE__ */ jsx("div", { className: "ui-filter-bar-actions", children: actions })
30
73
  ]
31
74
  }
32
75
  );
@@ -5,7 +5,7 @@ export { Menubar, MenubarCheckboxItem, MenubarContent, MenubarItem, MenubarLabel
5
5
  export { NavigationMenu, NavigationMenuContent, NavigationMenuIndicator, NavigationMenuItem, NavigationMenuLink, NavigationMenuList, NavigationMenuTrigger, NavigationMenuViewport, } from "./navigation-menu.js";
6
6
  export { Steps } from "./steps.js";
7
7
  export { FilterBar, FilterBarGroup, Toolbar, ToolbarGroup } from "./filter-bar.js";
8
- export type { FilterBarGroupProp, FilterBarGroupProps, FilterBarProp, FilterBarProps, ToolbarGroupProps, ToolbarProps, } from "./filter-bar.js";
8
+ export type { FilterBarChipProp, FilterBarChipProps, FilterBarGroupProp, FilterBarGroupProps, FilterBarProp, FilterBarProps, ToolbarGroupProps, ToolbarProps, } from "./filter-bar.js";
9
9
  export { AppSettingPicker } from "./app-setting-picker.js";
10
10
  export type { AppSettingPickerProp, AppSettingPickerProps, AppSettingKind, } from "./app-setting-picker.js";
11
11
  export type { BreadcrumbItemProp as BreadcrumbItem } from "../../props/vocabulary/navigation.prop.js";
@@ -135,6 +135,9 @@
135
135
  "openNav": "Open navigation menu",
136
136
  "navLabel": "Menu"
137
137
  },
138
+ "pageHeader": {
139
+ "loading": "Loading page…"
140
+ },
138
141
  "authShell": {
139
142
  "brandLabel": "Brand",
140
143
  "mainLabel": "Main content",
@@ -227,6 +230,14 @@
227
230
  "toolbar": {
228
231
  "ariaLabel": "Filters"
229
232
  },
233
+ "filterBar": {
234
+ "appliedFilters": "Applied filters",
235
+ "removeFilter": "Remove filter {label}",
236
+ "resultCount": {
237
+ "one": "{count} result",
238
+ "other": "{count} results"
239
+ }
240
+ },
230
241
  "localePicker": {
231
242
  "ariaLabel": "Language"
232
243
  },
@@ -135,6 +135,9 @@
135
135
  "openNav": "ナビゲーションメニューを開く",
136
136
  "navLabel": "メニュー"
137
137
  },
138
+ "pageHeader": {
139
+ "loading": "ページを読み込んでいます…"
140
+ },
138
141
  "authShell": {
139
142
  "brandLabel": "ブランド",
140
143
  "mainLabel": "メインコンテンツ",
@@ -227,6 +230,11 @@
227
230
  "toolbar": {
228
231
  "ariaLabel": "フィルター"
229
232
  },
233
+ "filterBar": {
234
+ "appliedFilters": "適用中のフィルター",
235
+ "removeFilter": "フィルター「{label}」を解除",
236
+ "resultCount": "{count} 件"
237
+ },
230
238
  "localePicker": {
231
239
  "ariaLabel": "言語"
232
240
  },
@@ -135,6 +135,9 @@
135
135
  "openNav": "Mở menu điều hướng",
136
136
  "navLabel": "Menu"
137
137
  },
138
+ "pageHeader": {
139
+ "loading": "Đang tải trang…"
140
+ },
138
141
  "authShell": {
139
142
  "brandLabel": "Thương hiệu",
140
143
  "mainLabel": "Nội dung chính",
@@ -227,6 +230,11 @@
227
230
  "toolbar": {
228
231
  "ariaLabel": "Bộ lọc"
229
232
  },
233
+ "filterBar": {
234
+ "appliedFilters": "Bộ lọc đang áp dụng",
235
+ "removeFilter": "Bỏ bộ lọc {label}",
236
+ "resultCount": "{count} kết quả"
237
+ },
230
238
  "localePicker": {
231
239
  "ariaLabel": "Ngôn ngữ"
232
240
  },
@@ -42,6 +42,12 @@ export type AlertProp = React.HTMLAttributes<HTMLDivElement> & {
42
42
  className?: ClassNameProp;
43
43
  children?: ChildrenProp;
44
44
  };
45
+ /**
46
+ * @see Banner — page-level Alert. `variant` is fixed to `"banner"` by the alias, so it is the one
47
+ * prop a Banner does not take; everything else (tone, icon, onDismiss, children) is Alert's
48
+ * contract verbatim. gh#255.
49
+ */
50
+ export type BannerProp = Omit<AlertProp, "variant">;
45
51
  /** @see AlertTitle */
46
52
  export type AlertTitleProp = React.HTMLAttributes<HTMLParagraphElement> & {
47
53
  className?: ClassNameProp;
@@ -24,10 +24,55 @@ export type PageContainerPresetProp = "default" | "admin-collection";
24
24
  * fluid at the compact gutter.
25
25
  */
26
26
  export type PageContainerMeasureProp = "default" | "narrow" | "medium";
27
+ /**
28
+ * @see PageHeader — the canonical page title band, extracted from PageContainer so a consumer can
29
+ * mount the SAME token-owned geometry outside a full page shell (a Sheet detail, a MasterDetail
30
+ * pane, a tab body) instead of re-authoring `.ui-page-header` CSS locally (gh#255).
31
+ */
32
+ export type PageHeaderProp = {
33
+ title: TitleProp;
34
+ subtitle?: SubtitleProp;
35
+ /**
36
+ * Status / meta band rendered INLINE with the title (a `Badge` tone, an ID, a timestamp).
37
+ * Omitted → the title band emits its historical single-`<h1>` DOM, so no existing page moves.
38
+ */
39
+ meta?: ReactNode;
40
+ /** Trailing action region — buttons, a search field, an overflow menu. */
41
+ extra?: ExtraProp;
42
+ breadcrumb?: BreadcrumbProp;
43
+ /**
44
+ * Override the breadcrumb `<nav>` landmark's accessible name. Defaults to a localized
45
+ * "Breadcrumb". Needed when more than one header (each with its own `breadcrumb`) renders on the
46
+ * same page/view — two `<nav>` landmarks sharing one name/role fail axe's `landmark-unique`
47
+ * (WCAG 2.4.1 / 1.3.1).
48
+ */
49
+ breadcrumbLabel?: string;
50
+ /** Kebab/DOM-style alias of `breadcrumbLabel` (same landmark-unique override). */
51
+ breadcrumbAriaLabel?: string;
52
+ linkComponent?: React.ElementType;
53
+ /**
54
+ * How the title band and `extra` share the header row below the 640px step. Defaults to
55
+ * `stack`. @see PageContainerHeaderLayoutProp
56
+ */
57
+ layout?: PageContainerHeaderLayoutProp;
58
+ /**
59
+ * Pending state for the title band while the page's own data resolves. Renders the title/subtitle
60
+ * /meta as `Skeleton` placeholders and marks the header `aria-busy`, keeping the `<h1>` in the
61
+ * document with an accessible name (an empty heading is an axe violation) so the page's heading
62
+ * outline never disappears mid-load. Breadcrumbs and `extra` are NOT skeletonised — they come
63
+ * from the route, not the record.
64
+ */
65
+ loading?: boolean;
66
+ className?: ClassNameProp;
67
+ };
27
68
  /** @see PageContainer */
28
69
  export type PageContainerProp = {
29
70
  title: TitleProp;
30
71
  subtitle?: SubtitleProp;
72
+ /** Status / meta band rendered inline with the title. @see PageHeaderProp.meta */
73
+ meta?: ReactNode;
74
+ /** Skeletonise the title band while the page record loads. @see PageHeaderProp.loading */
75
+ headerLoading?: boolean;
31
76
  extra?: ExtraProp;
32
77
  footer?: FooterProp;
33
78
  breadcrumb?: BreadcrumbProp;
@@ -240,6 +285,14 @@ export type AuthShellProp = {
240
285
  * so standalone, one-line requester and wrapped two-line requester states keep the same card
241
286
  * position without truncating or inventing requester data. Pass AuthIdentity, Card and
242
287
  * AuthFooter as direct children (an anchor may wrap AuthIdentity).
288
+ * - `"registration"` — the 360px sign-up measure with a 15px inline gutter at 390px (the same
289
+ * page rhythm as `"login"`, so sign-in → sign-up never jumps on a phone). The ONLY preset whose
290
+ * column is START-aligned: a registration card is the tallest surface in the hosted-identity
291
+ * set (name · email · password · confirm · strength · consent · submit · providers), and a
292
+ * vertically centred tall card overflows ABOVE the scroll origin on a short viewport, putting
293
+ * its first field out of reach. It is also the only preset with a footer-clearance knob of its
294
+ * own, so the legal/consent footer never sits flush against the submit button. Carries the full
295
+ * password form and the pending-email confirmation state with no consumer geometry CSS.
243
296
  * - `"device-authorization"` — 380px card measure with a 5px inline page gutter at a 390px
244
297
  * viewport (canonical device-grant artboard).
245
298
  * - `"context-selection"` — 25rem card measure on desktop/tablet, edge-to-edge on mobile, and a
@@ -7,10 +7,62 @@ import type { ChildrenProp, ClassNameProp, DisabledProp, HasActiveFiltersProp, I
7
7
  * bounded row that scrolls inline, so a wide filter set never pushes the list below the fold.
8
8
  */
9
9
  export type FilterBarOverflowProp = "wrap" | "scroll";
10
+ /**
11
+ * One APPLIED filter, rendered as a removable chip under the control row (#258).
12
+ *
13
+ * The chip is the visible record of a filter that is already in effect — it is NOT the control that
14
+ * sets it. `label` is caller-localized text (the bar never interprets or formats domain values);
15
+ * omit `onRemove` for a chip the user may not lift (a scope locked by the route or by permission),
16
+ * and the chip renders as a static token with no dead button.
17
+ */
18
+ export type FilterBarChipProp = {
19
+ /** Stable identity — also the argument passed back to {@link ToolbarProp.onChipRemove}. */
20
+ id: string;
21
+ /** Already-localized chip text, e.g. "状態: 未払い". */
22
+ label: string;
23
+ /**
24
+ * Per-chip removal. Omit for a chip the user cannot lift; the bar then renders no remove control
25
+ * rather than a disabled one, so nothing focusable is dead.
26
+ */
27
+ onRemove?: () => void;
28
+ };
10
29
  /** @see Toolbar */
11
30
  export type ToolbarProp = {
12
31
  onClear?: OnClearFiltersProp;
13
32
  hasActiveFilters?: HasActiveFiltersProp;
33
+ /**
34
+ * The search control (#258). A slot, not a rendered input: the bar owns its MEASURE
35
+ * (`--filter-bar-search-width`) and its position at the START of the row, while the caller
36
+ * still chooses the real primitive (`SearchInput`, or a `Select showSearch` for scoped search)
37
+ * and owns its debounce and value. Passing the control as a child instead is what produced the
38
+ * inconsistent search widths the issue was filed against.
39
+ */
40
+ search?: ChildrenProp;
41
+ /**
42
+ * Applied filters as removable chips (#258). The bar owns the CHIP LIFECYCLE — chips render
43
+ * below the control row, each with an accessible remove control, and the row disappears entirely
44
+ * when the array is empty (never an empty reserved strip). It owns no state: removing a chip
45
+ * calls back and the caller re-renders the array.
46
+ */
47
+ chips?: readonly FilterBarChipProp[];
48
+ /**
49
+ * Called with the chip's `id` when its remove control is activated. Fires in addition to that
50
+ * chip's own `onRemove`, so a caller may handle removal per chip, centrally, or both.
51
+ */
52
+ onChipRemove?: (id: string) => void;
53
+ /**
54
+ * Number of records the current filters resolve to (#258). Rendered in a POLITE LIVE REGION and
55
+ * formatted with `Intl.NumberFormat` + CLDR plurals for the active locale — which is the point:
56
+ * a sighted user sees the table change, and this is what tells everyone else that filtering
57
+ * happened. Omit when the count is unknown or still loading.
58
+ */
59
+ resultCount?: number;
60
+ /**
61
+ * Trailing action region (#258) — export, column settings, a saved-view menu. Rendered AFTER the
62
+ * reset control, so "clear filters" never sits at the end of the row beside an unrelated primary
63
+ * action. The bar owns the ordering; the caller owns the controls.
64
+ */
65
+ actions?: ChildrenProp;
14
66
  /**
15
67
  * Pin the strip to the top of its scroll container while the list scrolls beneath it
16
68
  * (list-page filter bars, #197). Opt-in — default `false` keeps the toolbar quiet. Tune
@@ -461,10 +461,35 @@ export declare const COMPONENT_PROP_REGISTRY: {
461
461
  readonly file: "components/layout.prop.ts";
462
462
  readonly vocabulary: readonly [];
463
463
  };
464
+ readonly PageHeaderProp: {
465
+ readonly group: "layout";
466
+ readonly file: "components/layout.prop.ts";
467
+ readonly vocabulary: readonly ["TitleProp", "SubtitleProp", "ExtraProp", "BreadcrumbProp", {
468
+ readonly field: "meta";
469
+ readonly local: true;
470
+ readonly reason: "Status/meta band rendered inline with the `<h1>` (a Badge tone, a record id, a timestamp). A free ReactNode slot, not a controlled value — the header never interprets what it holds.";
471
+ }, {
472
+ readonly field: "layout";
473
+ readonly local: true;
474
+ readonly reason: "Header ARRANGEMENT of the title band vs the extra slot below the 640px step (stack | responsive-inline) — the standalone spelling of PageContainer's `headerLayout`.";
475
+ }, {
476
+ readonly field: "loading";
477
+ readonly local: true;
478
+ readonly reason: "Pending state of the TITLE BAND only (skeleton title/subtitle + aria-busy), distinct from a page-wide DataState: breadcrumbs and actions come from the route and stay live while the record resolves.";
479
+ }];
480
+ };
464
481
  readonly PageContainerProp: {
465
482
  readonly group: "layout";
466
483
  readonly file: "components/layout.prop.ts";
467
484
  readonly vocabulary: readonly ["TitleProp", "SubtitleProp", "ExtraProp", "FooterProp", "BreadcrumbProp", "DensityProp", "PageContainerVariantProp", {
485
+ readonly field: "meta";
486
+ readonly local: true;
487
+ readonly reason: "Forwarded verbatim to PageHeader's `meta` slot — see PageHeaderProp.";
488
+ }, {
489
+ readonly field: "headerLoading";
490
+ readonly local: true;
491
+ readonly reason: "Forwarded to PageHeader's `loading`. Named for the BAND it skeletonises, so it can never be read as a page-wide loading flag (that is DataState's job).";
492
+ }, {
468
493
  readonly field: "headerLayout";
469
494
  readonly local: true;
470
495
  readonly reason: "Header ARRANGEMENT of the title band vs the extra slot below the 640px step (stack | responsive-inline) — orthogonal to PageContainerVariantProp, which selects the page shell layout.";
@@ -1350,6 +1375,11 @@ export declare const COMPONENT_PROP_REGISTRY: {
1350
1375
  readonly file: "components/feedback.prop.ts";
1351
1376
  readonly vocabulary: readonly ["AlertVariantProp", "ToneProp", "IconProp", "OnValueChangeProp", "ClassNameProp", "ChildrenProp"];
1352
1377
  };
1378
+ readonly BannerProp: {
1379
+ readonly group: "feedback";
1380
+ readonly file: "components/feedback.prop.ts";
1381
+ readonly vocabulary: readonly ["ToneProp", "IconProp", "OnValueChangeProp", "ClassNameProp", "ChildrenProp"];
1382
+ };
1353
1383
  readonly AlertTitleProp: {
1354
1384
  readonly group: "feedback";
1355
1385
  readonly file: "components/feedback.prop.ts";
@@ -1383,7 +1413,36 @@ export declare const COMPONENT_PROP_REGISTRY: {
1383
1413
  readonly ToolbarProp: {
1384
1414
  readonly group: "navigation";
1385
1415
  readonly file: "components/navigation.prop.ts";
1386
- readonly vocabulary: readonly ["OnClearFiltersProp", "HasActiveFiltersProp", "StickyProp", "ClassNameProp", "ChildrenProp"];
1416
+ readonly vocabulary: readonly ["OnClearFiltersProp", "HasActiveFiltersProp", "StickyProp", "ClassNameProp", "ChildrenProp", {
1417
+ readonly field: "search";
1418
+ readonly local: true;
1419
+ readonly reason: "SLOT for the caller's own search control, positioned at the start of the row and measured by --filter-bar-search-width. Not a value/onValueChange pair: the bar owns the search's PLACE and WIDTH, never its state (gh#258).";
1420
+ }, {
1421
+ readonly field: "chips";
1422
+ readonly local: true;
1423
+ readonly reason: "APPLIED filters as removable chips — the visible record of filters already in effect, distinct from the controls that set them. A bar-specific shape (id/label/onRemove), not a shared vocabulary type (gh#258).";
1424
+ }, {
1425
+ readonly field: "onChipRemove";
1426
+ readonly local: true;
1427
+ readonly reason: "Chip-removal callback keyed by chip id. Not `onValueChange`: it reports ONE lifted filter rather than the bar's whole value, which the bar deliberately does not hold (gh#258).";
1428
+ }, {
1429
+ readonly field: "resultCount";
1430
+ readonly local: true;
1431
+ readonly reason: "Number of records the current filters resolve to, announced in a polite live region and formatted via Intl.NumberFormat + CLDR plurals. Presentational output, not a controlled value (gh#258).";
1432
+ }, {
1433
+ readonly field: "actions";
1434
+ readonly local: true;
1435
+ readonly reason: "Trailing action SLOT (export, column settings, saved views), ordered AFTER the reset control by the bar so `clear filters` never sits beside an unrelated primary action (gh#258).";
1436
+ }];
1437
+ };
1438
+ readonly FilterBarChipProp: {
1439
+ readonly group: "navigation";
1440
+ readonly file: "components/navigation.prop.ts";
1441
+ readonly vocabulary: readonly ["IdProp", "LabelProp", {
1442
+ readonly field: "onRemove";
1443
+ readonly local: true;
1444
+ readonly reason: "Per-chip removal. Omitting it is meaningful — the chip renders with NO remove control (a filter the user may not lift), rather than a disabled button that is a dead tab stop (gh#258).";
1445
+ }];
1387
1446
  };
1388
1447
  readonly FilterBarOverflowProp: {
1389
1448
  readonly group: "navigation";
@@ -465,6 +465,31 @@ const COMPONENT_PROP_REGISTRY = {
465
465
  file: "components/layout.prop.ts",
466
466
  vocabulary: []
467
467
  },
468
+ PageHeaderProp: {
469
+ group: "layout",
470
+ file: "components/layout.prop.ts",
471
+ vocabulary: [
472
+ "TitleProp",
473
+ "SubtitleProp",
474
+ "ExtraProp",
475
+ "BreadcrumbProp",
476
+ {
477
+ field: "meta",
478
+ local: true,
479
+ reason: "Status/meta band rendered inline with the `<h1>` (a Badge tone, a record id, a timestamp). A free ReactNode slot, not a controlled value \u2014 the header never interprets what it holds."
480
+ },
481
+ {
482
+ field: "layout",
483
+ local: true,
484
+ reason: "Header ARRANGEMENT of the title band vs the extra slot below the 640px step (stack | responsive-inline) \u2014 the standalone spelling of PageContainer's `headerLayout`."
485
+ },
486
+ {
487
+ field: "loading",
488
+ local: true,
489
+ reason: "Pending state of the TITLE BAND only (skeleton title/subtitle + aria-busy), distinct from a page-wide DataState: breadcrumbs and actions come from the route and stay live while the record resolves."
490
+ }
491
+ ]
492
+ },
468
493
  PageContainerProp: {
469
494
  group: "layout",
470
495
  file: "components/layout.prop.ts",
@@ -476,6 +501,16 @@ const COMPONENT_PROP_REGISTRY = {
476
501
  "BreadcrumbProp",
477
502
  "DensityProp",
478
503
  "PageContainerVariantProp",
504
+ {
505
+ field: "meta",
506
+ local: true,
507
+ reason: "Forwarded verbatim to PageHeader's `meta` slot \u2014 see PageHeaderProp."
508
+ },
509
+ {
510
+ field: "headerLoading",
511
+ local: true,
512
+ reason: "Forwarded to PageHeader's `loading`. Named for the BAND it skeletonises, so it can never be read as a page-wide loading flag (that is DataState's job)."
513
+ },
479
514
  {
480
515
  field: "headerLayout",
481
516
  local: true,
@@ -1483,6 +1518,13 @@ const COMPONENT_PROP_REGISTRY = {
1483
1518
  "ChildrenProp"
1484
1519
  ]
1485
1520
  },
1521
+ BannerProp: {
1522
+ group: "feedback",
1523
+ file: "components/feedback.prop.ts",
1524
+ // `Omit<AlertProp, "variant">` — the alias fixes the one prop it drops, so it inherits Alert's
1525
+ // vocabulary exactly. Listing it again here is what keeps the guard honest if Alert's API moves.
1526
+ vocabulary: ["ToneProp", "IconProp", "OnValueChangeProp", "ClassNameProp", "ChildrenProp"]
1527
+ },
1486
1528
  AlertTitleProp: {
1487
1529
  group: "feedback",
1488
1530
  file: "components/feedback.prop.ts",
@@ -1513,7 +1555,45 @@ const COMPONENT_PROP_REGISTRY = {
1513
1555
  "HasActiveFiltersProp",
1514
1556
  "StickyProp",
1515
1557
  "ClassNameProp",
1516
- "ChildrenProp"
1558
+ "ChildrenProp",
1559
+ {
1560
+ field: "search",
1561
+ local: true,
1562
+ reason: "SLOT for the caller's own search control, positioned at the start of the row and measured by --filter-bar-search-width. Not a value/onValueChange pair: the bar owns the search's PLACE and WIDTH, never its state (gh#258)."
1563
+ },
1564
+ {
1565
+ field: "chips",
1566
+ local: true,
1567
+ reason: "APPLIED filters as removable chips \u2014 the visible record of filters already in effect, distinct from the controls that set them. A bar-specific shape (id/label/onRemove), not a shared vocabulary type (gh#258)."
1568
+ },
1569
+ {
1570
+ field: "onChipRemove",
1571
+ local: true,
1572
+ reason: "Chip-removal callback keyed by chip id. Not `onValueChange`: it reports ONE lifted filter rather than the bar's whole value, which the bar deliberately does not hold (gh#258)."
1573
+ },
1574
+ {
1575
+ field: "resultCount",
1576
+ local: true,
1577
+ reason: "Number of records the current filters resolve to, announced in a polite live region and formatted via Intl.NumberFormat + CLDR plurals. Presentational output, not a controlled value (gh#258)."
1578
+ },
1579
+ {
1580
+ field: "actions",
1581
+ local: true,
1582
+ reason: "Trailing action SLOT (export, column settings, saved views), ordered AFTER the reset control by the bar so `clear filters` never sits beside an unrelated primary action (gh#258)."
1583
+ }
1584
+ ]
1585
+ },
1586
+ FilterBarChipProp: {
1587
+ group: "navigation",
1588
+ file: "components/navigation.prop.ts",
1589
+ vocabulary: [
1590
+ "IdProp",
1591
+ "LabelProp",
1592
+ {
1593
+ field: "onRemove",
1594
+ local: true,
1595
+ reason: "Per-chip removal. Omitting it is meaningful \u2014 the chip renders with NO remove control (a filter the user may not lift), rather than a disabled button that is a dead tab stop (gh#258)."
1596
+ }
1517
1597
  ]
1518
1598
  },
1519
1599
  FilterBarOverflowProp: {
@@ -54,8 +54,16 @@ export type BreakpointProp = "sm" | "md" | "lg" | "xl";
54
54
  export type ConfirmVariantProp = "default" | "destructive";
55
55
  /** Semantic color/status intent. */
56
56
  export type ToneProp = "default" | "success" | "warning" | "destructive" | "info" | "muted" | "neutral";
57
- /** Inline Alert visual treatment. */
58
- export type AlertVariantProp = "default";
57
+ /**
58
+ * Alert visual treatment.
59
+ *
60
+ * `"default"` is the inset, rounded, fully-bordered INLINE alert that sits inside a page body.
61
+ * `"banner"` is the page-level strip the `Banner` alias ships (gh#255): square, edge-to-edge, ruled
62
+ * on the block-end edge only, so it reads as page chrome above the content rather than as a card
63
+ * inside it. Both share one implementation, one tone scale and one dismiss/actions contract —
64
+ * `banner` re-measures the box through `--banner-*` tokens and changes nothing else.
65
+ */
66
+ export type AlertVariantProp = "default" | "banner";
59
67
  /** Sort direction for table columns. */
60
68
  export type SortDirectionProp = "asc" | "desc";
61
69
  /** Table column text alignment. */
@@ -45,10 +45,13 @@ export type ErrorSurfaceStatusProp = 403 | 404 | 500 | 503;
45
45
  * standalone and requester flows; `"device-authorization"` is the 380px device-grant measure with
46
46
  * a 5px mobile inline gutter; `"context-selection"` is the 25rem organisation/context picker
47
47
  * measure that goes edge-to-edge on mobile; `"account-recovery"` is the 432px SCR-008 measure
48
- * shared by the password-recovery and sign-in MFA challenge panels (15px mobile inline gutter).
48
+ * shared by the password-recovery and sign-in MFA challenge panels (15px mobile inline gutter);
49
+ * `"registration"` is the 360px sign-up measure — the only START-aligned preset, because a
50
+ * registration card is the tallest surface in the set and a centred tall card overflows above the
51
+ * scroll origin, and the only one with its own footer-clearance knob.
49
52
  * Orthogonal to AuthShell's `variant` (which owns control density and heading size) — combine them.
50
53
  */
51
- export type AuthShellPresetProp = "default" | "login" | "device-authorization" | "context-selection" | "account-recovery";
54
+ export type AuthShellPresetProp = "default" | "login" | "registration" | "device-authorization" | "context-selection" | "account-recovery";
52
55
  /** Shared gap between layout children; components may document subsets. */
53
56
  export type GapProp = "xs" | "sm" | "md" | "lg" | "xl";
54
57
  /** DataTable row density subset. */
@@ -51,6 +51,42 @@
51
51
  padding-inline-end: calc(var(--alert-space-inset) + var(--space-8));
52
52
  }
53
53
 
54
+ /*
55
+ * BANNER variant (gh#255) — the same Alert, re-measured as page chrome: edge-to-edge, square,
56
+ * ruled only on the block-end edge so it separates itself from the content BELOW it. Declared
57
+ * after the base box (equal specificity → source order wins) and it touches geometry only, so
58
+ * every tone, the icon treatment, the actions grid and the dismiss control are inherited
59
+ * unchanged from the inline alert above.
60
+ */
61
+ [data-slot="alert"][data-variant="banner"] {
62
+ border-radius: var(--banner-radius);
63
+ border-width: var(--banner-border-width);
64
+ border-block-end-width: var(--banner-border-block-end-width);
65
+ padding: var(--banner-space-block) var(--banner-space-inline);
66
+ }
67
+
68
+ /* The dismissible reserve is re-derived from the BANNER inset — reusing the inline-alert
69
+ * calc would leave a ✕-sized hole on a strip whose own inset is half as deep. */
70
+ [data-slot="alert"][data-variant="banner"][data-dismissible] {
71
+ padding-inline-end: calc(var(--banner-space-inline) + var(--space-8));
72
+ }
73
+
74
+ [data-slot="alert"][data-variant="banner"] > [data-slot="alert-dismiss"] {
75
+ top: var(--banner-dismiss-space-offset);
76
+ inset-inline-end: var(--banner-dismiss-space-offset);
77
+ }
78
+
79
+ /* The compact page gutter. `.ui-page-container` steps `--space-page-active-x` down at this same
80
+ * breakpoint, and a banner mounted ABOVE the container (the normal case — page/app chrome) does
81
+ * not inherit that, so without this rule its text would sit 8px outside the page title at 390px.
82
+ * Bound to its own `--banner-space-inline-compact` knob rather than a literal, so a service
83
+ * retunes the mobile inset the same way it retunes the desktop one. */
84
+ @media (max-width: 720px) {
85
+ [data-slot="alert"][data-variant="banner"] {
86
+ --banner-space-inline: var(--banner-space-inline-compact);
87
+ }
88
+ }
89
+
54
90
  [data-slot="alert-icon"] {
55
91
  width: 1.25rem;
56
92
  height: 1.25rem;
@@ -529,6 +529,41 @@
529
529
  overflow-wrap: anywhere;
530
530
  }
531
531
 
532
+ /* Title + `meta` share one baseline-aligned band (gh#255). The band only exists when `meta` is
533
+ * passed, so a header without it keeps its historical bare-`<h1>` DOM. `wrap` is what keeps a
534
+ * long JA/VI title and a status Badge legible at 390px instead of squeezing the title column. */
535
+ .ui-page-title-band {
536
+ display: flex;
537
+ flex-wrap: wrap;
538
+ align-items: baseline;
539
+ gap: var(--page-header-meta-gap);
540
+ min-inline-size: 0;
541
+ }
542
+
543
+ .ui-page-header-meta {
544
+ display: flex;
545
+ flex-wrap: wrap;
546
+ align-items: center;
547
+ gap: var(--page-header-meta-gap);
548
+ min-inline-size: 0;
549
+ }
550
+
551
+ /* `loading` placeholders — the real `<h1>`/`<p>` wearing the shared `.ui-skeleton-block` skin, so
552
+ * the pending band occupies the same rhythm the settled band will. `max-inline-size: 100%` keeps
553
+ * them inside a 390px column; the block size replaces the line box the (sr-only) text no longer
554
+ * fills. */
555
+ .ui-page-title-placeholder {
556
+ inline-size: var(--page-title-placeholder-measure);
557
+ max-inline-size: 100%;
558
+ min-block-size: var(--page-title-placeholder-block-size);
559
+ }
560
+
561
+ .ui-page-subtitle-placeholder {
562
+ inline-size: var(--page-subtitle-placeholder-measure);
563
+ max-inline-size: 100%;
564
+ min-block-size: var(--page-subtitle-placeholder-block-size);
565
+ }
566
+
532
567
  /* Compact viewports use a responsive type step, not compact density: body/control text and
533
568
  * touch targets remain readable while page chrome stops competing with primary actions. A
534
569
  * single Flex action group must wrap even when the consumer omitted `wrap`, because clipping
@@ -918,6 +953,84 @@
918
953
  font-size: var(--filter-label-font-size);
919
954
  }
920
955
 
956
+ /* ── FilterBar typed model (#258) ──────────────────────────────────────────────────────────
957
+ * Every rule below is scoped to a region the bar only emits when its prop is passed, so a bar
958
+ * built the old way (children + onClear) matches none of them and is geometrically untouched.
959
+ *
960
+ * SEARCH — full-width while the strip is stacked (a bounded search box on a 390px screen wastes
961
+ * the row), then one shared token-owned measure from the 640px step up. This single measure is
962
+ * what the issue was filed against: as a plain child, every page picked its own width. */
963
+ .ui-filter-bar-search {
964
+ width: 100%;
965
+ min-width: 0;
966
+ }
967
+
968
+ @media (min-width: 640px) {
969
+ .ui-filter-bar-search {
970
+ width: var(--filter-bar-search-width);
971
+ max-width: 100%;
972
+ flex: 0 0 auto;
973
+ }
974
+ }
975
+
976
+ /* CHIPS — always their own full-width line. Applied filters are a record of state, not another
977
+ * control in the row; wrapping them in beside the pickers makes both harder to scan. */
978
+ .ui-filter-bar-chips {
979
+ display: flex;
980
+ width: 100%;
981
+ flex-basis: 100%;
982
+ flex-wrap: wrap;
983
+ align-items: center;
984
+ gap: var(--filter-bar-chip-row-gap);
985
+ min-width: 0;
986
+ }
987
+
988
+ .ui-filter-bar-chip {
989
+ display: inline-flex;
990
+ max-width: 100%;
991
+ align-items: center;
992
+ gap: var(--filter-bar-chip-gap);
993
+ padding: var(--filter-bar-chip-padding-block) var(--filter-bar-chip-padding-inline);
994
+ border-radius: var(--filter-bar-chip-radius);
995
+ background: hsl(var(--filter-bar-chip-background, var(--muted)));
996
+ color: hsl(var(--filter-bar-chip-foreground, var(--muted-foreground)));
997
+ font-size: var(--filter-bar-chip-font-size);
998
+ }
999
+
1000
+ /* A long JA/VI chip value wraps inside the chip rather than pushing the row into a horizontal
1001
+ * scroll — the remove control must stay reachable at 390px. */
1002
+ .ui-filter-bar-chip-label {
1003
+ min-width: 0;
1004
+ overflow-wrap: anywhere;
1005
+ }
1006
+
1007
+ .ui-filter-bar-chip-remove {
1008
+ flex: none;
1009
+ }
1010
+
1011
+ .ui-filter-bar-count {
1012
+ color: hsl(var(--filter-bar-count-foreground, var(--muted-foreground)));
1013
+ font-size: var(--filter-bar-count-font-size);
1014
+ /* The count is a number that changes in place — tabular figures stop the row from twitching
1015
+ * as it goes 9 → 10 → 100. */
1016
+ font-variant-numeric: tabular-nums;
1017
+ }
1018
+
1019
+ .ui-filter-bar-actions {
1020
+ display: flex;
1021
+ width: 100%;
1022
+ flex-wrap: wrap;
1023
+ align-items: center;
1024
+ gap: var(--space-inline-sm);
1025
+ }
1026
+
1027
+ @media (min-width: 640px) {
1028
+ .ui-filter-bar-actions {
1029
+ width: auto;
1030
+ flex: 0 0 auto;
1031
+ }
1032
+ }
1033
+
921
1034
  /* Toolbar/FilterBar `overflow="scroll"` (#216) — one bounded row that scrolls INLINE instead of
922
1035
  * wrapping onto extra rows. Below 640px the strip still stacks (the `wrap` base rules above own
923
1036
  * that): a 390px viewport must never hide a filter behind a horizontal scroll it cannot see.
@@ -212,6 +212,30 @@
212
212
  --auth-shell-main-padding: var(--auth-shell-recovery-main-padding);
213
213
  }
214
214
 
215
+ /* Registration preset (gh#256) — the sign-up measure. `flex-start` is the load-bearing line: a
216
+ * registration card is the tallest surface in the hosted-identity set, and a vertically CENTRED
217
+ * tall card overflows ABOVE the scroll origin on a short viewport, putting its first field out of
218
+ * reach. Start-aligned, the block-start inset is ordinary page padding the user scrolls past, and
219
+ * the block-end inset is the footer clearance the form keeps at the end of a long scroll. */
220
+ .ui-auth-shell[data-preset="registration"] {
221
+ --auth-shell-card-max-width: var(--auth-shell-registration-card-max-width);
222
+ --auth-shell-main-align: flex-start;
223
+ --auth-shell-main-padding: var(--auth-shell-registration-main-padding-block-start)
224
+ var(--auth-shell-registration-main-padding-inline)
225
+ var(--auth-shell-registration-main-padding-block-end);
226
+ --auth-shell-card-stack-gap: var(--auth-shell-registration-card-stack-gap);
227
+ }
228
+
229
+ /* Fixed identity track — the mechanism `login` proved (gh#237), applied to the identity block
230
+ * wherever it sits in the column rather than through a grid row, so the preset does not constrain
231
+ * how many sections the page stacks. Content aligns to the slot END, so absent / one-line /
232
+ * wrapped two-line identity copy changes only the empty space ABOVE it and never moves the card
233
+ * off its canonical anchor. */
234
+ .ui-auth-shell[data-preset="registration"] .ui-auth-shell-card > .ui-auth-identity {
235
+ block-size: var(--auth-shell-registration-identity-slot-block-size);
236
+ justify-content: flex-end;
237
+ }
238
+
215
239
  .ui-auth-identity {
216
240
  display: flex;
217
241
  flex-direction: column;
@@ -391,6 +415,16 @@
391
415
  .ui-auth-shell[data-preset="account-recovery"] {
392
416
  --auth-shell-main-padding: var(--auth-shell-recovery-main-padding-mobile);
393
417
  }
418
+
419
+ /* registration = 15px inline at 390 (card x=15, width=360 — the same page rhythm as Login, so
420
+ * sign-in → sign-up never jumps on a phone). The block insets tighten but the footer clearance
421
+ * stays deliberately larger than the block-start inset: at 390 the form is at its tallest and
422
+ * the consent/legal footer must not sit flush against the submit button. */
423
+ .ui-auth-shell[data-preset="registration"] {
424
+ --auth-shell-main-padding: var(--auth-shell-registration-main-padding-block-start-mobile)
425
+ var(--auth-shell-registration-main-padding-inline-mobile)
426
+ var(--auth-shell-registration-main-padding-block-end-mobile);
427
+ }
394
428
  }
395
429
 
396
430
  /*
@@ -29,6 +29,26 @@
29
29
  * (a brand's success-bg/-border are often more present than the faint 5%/30% default). */
30
30
  --alert-bg-alpha: 0.05;
31
31
  --alert-border-alpha: 0.3;
32
+ /* BANNER — the page-level Alert treatment (gh#255). Every constant a service would want to
33
+ * match to its page grid is a knob (rule #45): a banner that must keep the app's rounded card
34
+ * language sets --banner-radius, one that rules its top edge too sets --banner-border-width.
35
+ * The inline inset tracks the live page gutter, so a banner mounted above a PageContainer lines
36
+ * its text up with the page title instead of sitting at a second, unrelated margin. */
37
+ --banner-radius: 0;
38
+ --banner-border-width: 0;
39
+ --banner-border-block-end-width: 1px;
40
+ --banner-space-block: var(--space-3);
41
+ /* Inline inset tracks the page gutter so a banner's text lines up with the page title.
42
+ * `--space-page-active-x` steps down to the compact gutter on `.ui-page-container` only, and
43
+ * custom properties inherit — so a banner rendered INSIDE the container picks the compact value
44
+ * up for free, while the normal case (a banner mounted ABOVE the container, or in AppShell) would
45
+ * keep the desktop gutter and sit 8px out at 390px. The compact step below is what actually keeps
46
+ * the two aligned; retune it rather than hard-coding a mobile inset at the call site. */
47
+ --banner-space-inline: var(--space-page-active-x);
48
+ --banner-space-inline-compact: var(--space-page-compact-x);
49
+ /* Dismiss offset is measured from the banner's own (shorter) block inset, so the ✕ stays
50
+ * optically centred on a one-line strip instead of floating at the inline-alert offset. */
51
+ --banner-dismiss-space-offset: var(--space-2);
32
52
  /* Brand glow layer for the raised dialog/sheet panel — invisible no-op at rest (rule #44).
33
53
  * Paired AFTER --shadow-lg in the surface box-shadow so a service can wash the overlay with the
34
54
  * global glow, e.g. --dialog-content-glow: var(--shadow-glow), with no markup change. */
@@ -55,4 +55,30 @@
55
55
  * scrollbar never overlaps the controls. Rule #45 — a service theme retunes it to its grid
56
56
  * (0 on an overlay-scrollbar platform). */
57
57
  --filter-bar-scroll-padding-y: var(--space-1);
58
+
59
+ /* FilterBar typed model (#258) — the search / chip / result-count regions the bar now OWNS, so a
60
+ * list page never re-decides them. Every constant is a knob (rule #45): a service matches its own
61
+ * grid by retuning these once instead of per page.
62
+ *
63
+ * Search MEASURE — the whole point of the `search` slot. Below the 640px step the control is
64
+ * full-width (a bounded search box on a 390px screen wastes the row), so this binds only at and
65
+ * above it. 18rem = 288px: comfortable for a JA/VI query without crowding the filter groups. */
66
+ --filter-bar-search-width: 18rem;
67
+
68
+ /* Applied-filter chips. */
69
+ --filter-bar-chip-gap: var(--space-inline-xs);
70
+ --filter-bar-chip-row-gap: var(--space-inline-xs);
71
+ --filter-bar-chip-padding-inline: var(--space-2);
72
+ --filter-bar-chip-padding-block: var(--space-1);
73
+ --filter-bar-chip-radius: var(--radius-sm);
74
+ --filter-bar-chip-font-size: var(--font-size-xs);
75
+ /* Role-mirror knobs (docs/TOKENS.md) — `initial` at :root so --muted / --muted-foreground
76
+ * re-resolve at the CALL SITE under a scoped [data-tenant]/.dark theme. Binding a role var here
77
+ * would freeze the chip at the :root palette and a tenant override would never reach it. */
78
+ --filter-bar-chip-background: initial;
79
+ --filter-bar-chip-foreground: initial;
80
+
81
+ /* Result-count live region. */
82
+ --filter-bar-count-font-size: var(--font-size-xs);
83
+ --filter-bar-count-foreground: initial;
58
84
  }
@@ -201,6 +201,43 @@
201
201
  --auth-shell-recovery-main-padding: 1rem;
202
202
  --auth-shell-recovery-main-padding-mobile: 0.9375rem;
203
203
 
204
+ /* Registration preset (gh#256) — the canonical sign-up measure. Two things make it structurally
205
+ * different from every preset above, and both are the reason it could not be expressed by
206
+ * re-using `login`:
207
+ *
208
+ * 1. LONG-FORM SCROLLING. A registration card is the tallest surface in the hosted-identity set
209
+ * (name · email · password · confirm · strength meter · consent · submit · provider row), and
210
+ * a vertically CENTRED tall card clips its own top on a short viewport — the top overflows
211
+ * above the scroll origin and becomes unreachable. So the column is start-aligned and the
212
+ * block-start inset becomes ordinary page padding the user can scroll past.
213
+ * 2. FOOTER CLEARANCE. The legal/consent footer must never be flush against the submit button
214
+ * at the end of a long scroll, so the block-end inset is a knob of its own rather than
215
+ * mirroring the block-start one.
216
+ *
217
+ * The 360px form measure and the 15px mobile inline gutter match the canonical Login flow exactly
218
+ * (22.5rem card, x=15 at 390 ⇒ width=360), so a user moving sign-in → sign-up on a phone never
219
+ * sees the surface jump. */
220
+ --auth-shell-registration-card-max-width: 22.5rem;
221
+ /* Block-start offsets are DERIVED from the canonical SCR-002 artboard, not chosen: the card
222
+ * anchors at y=284 (1440x900) and y=274 (390x844), and the column is
223
+ * card y = padding-block-start + identity slot + stack gap
224
+ * so 284 - 112 - 20 = 152px (9.5rem) and 274 - 112 - 20 = 142px (8.875rem). Like every canonical
225
+ * screen in this family the page passes NO brand bar (the mark lives INSIDE the column as
226
+ * AuthIdentity), so `main` starts at the viewport top and the offset is the whole anchor. */
227
+ --auth-shell-registration-main-padding-block-start: 9.5rem;
228
+ --auth-shell-registration-main-padding-block-start-mobile: 8.875rem;
229
+ --auth-shell-registration-main-padding-inline: 1rem;
230
+ --auth-shell-registration-main-padding-inline-mobile: 0.9375rem;
231
+ /* Footer clearance — the gap the legal footer keeps below the end of a long form. */
232
+ --auth-shell-registration-main-padding-block-end: 3rem;
233
+ --auth-shell-registration-main-padding-block-end-mobile: 2rem;
234
+ --auth-shell-registration-card-stack-gap: 1.25rem;
235
+ /* Fixed identity track, the same 112px `login` proved (gh#237). Without it the card anchor moves
236
+ * with the title/requester line count — measured here at 82.69px for one wrapped requester — so
237
+ * the canonical y above would hold for exactly one copy length and drift for every other. The
238
+ * slot absorbs absent / one-line / two-line identity content instead. */
239
+ --auth-shell-registration-identity-slot-block-size: 7rem;
240
+
204
241
  /* Labelled auth divider geometry. */
205
242
  --auth-shell-divider-gap: 0.625rem;
206
243
  --auth-shell-divider-label-font-size: 0.6875rem;
@@ -83,6 +83,19 @@
83
83
  * page moves. 11rem = 176px, i.e. the control ends flush with the compact page gutter. */
84
84
  --page-header-extra-measure: 11rem; /* 176px */
85
85
 
86
+ /* Inline gap between the `<h1>` and the PageHeader `meta` slot (a status Badge, a record ID) when
87
+ * they share the title band (gh#255). Only consumed when `meta` is passed — a header without it
88
+ * emits no band wrapper at all, so no existing page moves. */
89
+ --page-header-meta-gap: var(--space-inline-sm);
90
+
91
+ /* PageHeader `loading` placeholders (gh#255). The title/subtitle elements wear the shared
92
+ * `.ui-skeleton-block` skin, so they need only a MEASURE and a block size — a service retunes the
93
+ * pending band's proportions here instead of per page. */
94
+ --page-title-placeholder-measure: 14rem; /* 224px */
95
+ --page-title-placeholder-block-size: 1.5em;
96
+ --page-subtitle-placeholder-measure: 22rem; /* 352px */
97
+ --page-subtitle-placeholder-block-size: 1.25em;
98
+
86
99
  /* Bounded page MEASURE (gh#245 / gh#247) — the shared inline cap applied to the page HEADER and
87
100
  * BODY together, so the header `extra` action ends on the same edge as the body surface (the
88
101
  * `variant="narrow"` complaint: it caps only `.ui-page-body`, leaving the header action out at
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@godxjp/ui",
3
- "version": "18.6.0",
4
- "godxUiMcp": "18.6.0",
3
+ "version": "18.7.0",
4
+ "godxUiMcp": "18.7.0",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@10.29.1",
7
7
  "pnpm": {