@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.
- package/dist/components/feedback/alert.d.ts +20 -2
- package/dist/components/feedback/alert.js +11 -1
- package/dist/components/feedback/index.d.ts +2 -2
- package/dist/components/feedback/index.js +3 -1
- package/dist/components/layout/auth-shell.d.ts +4 -2
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.js +2 -0
- package/dist/components/layout/page-container.d.ts +1 -1
- package/dist/components/layout/page-container.js +19 -41
- package/dist/components/layout/page-header.d.ts +22 -0
- package/dist/components/layout/page-header.js +89 -0
- package/dist/components/navigation/filter-bar.d.ts +2 -2
- package/dist/components/navigation/filter-bar.js +44 -1
- package/dist/components/navigation/index.d.ts +1 -1
- package/dist/i18n/messages/en.json +11 -0
- package/dist/i18n/messages/ja.json +8 -0
- package/dist/i18n/messages/vi.json +8 -0
- package/dist/props/components/feedback.prop.d.ts +6 -0
- package/dist/props/components/layout.prop.d.ts +53 -0
- package/dist/props/components/navigation.prop.d.ts +52 -0
- package/dist/props/registry.d.ts +60 -1
- package/dist/props/registry.js +81 -1
- package/dist/props/vocabulary/interaction.prop.d.ts +10 -2
- package/dist/props/vocabulary/layout.prop.d.ts +5 -2
- package/dist/styles/alert-layout.css +36 -0
- package/dist/styles/layout.css +113 -0
- package/dist/styles/shell-layout.css +34 -0
- package/dist/tokens/components/feedback.css +20 -0
- package/dist/tokens/components/navigation.css +26 -0
- package/dist/tokens/components/shell.css +37 -0
- package/dist/tokens/semantic/layout.css +13 -0
- 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; `"
|
|
14
|
-
*
|
|
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__ */
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
package/dist/props/registry.d.ts
CHANGED
|
@@ -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";
|
package/dist/props/registry.js
CHANGED
|
@@ -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
|
-
/**
|
|
58
|
-
|
|
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;
|
package/dist/styles/layout.css
CHANGED
|
@@ -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
|