@godxjp/ui 19.3.1 → 19.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/components/data-display/badge.d.ts +9 -1
  2. package/dist/components/data-display/badge.js +3 -2
  3. package/dist/components/data-display/index.d.ts +2 -0
  4. package/dist/components/data-display/index.js +2 -0
  5. package/dist/components/data-display/popover.d.ts +13 -1
  6. package/dist/components/data-display/popover.js +3 -1
  7. package/dist/components/data-display/table.d.ts +19 -2
  8. package/dist/components/data-display/table.js +4 -1
  9. package/dist/components/data-display/timeline-grid.d.ts +18 -0
  10. package/dist/components/data-display/timeline-grid.js +222 -0
  11. package/dist/components/data-entry/cascader.js +4 -6
  12. package/dist/components/data-entry/checkbox.js +1 -1
  13. package/dist/components/data-entry/search-select.js +5 -7
  14. package/dist/components/data-entry/select.js +32 -28
  15. package/dist/components/data-entry/tree-select.js +4 -6
  16. package/dist/components/general/inert-background.d.ts +5 -0
  17. package/dist/components/general/inert-background.js +48 -0
  18. package/dist/components/layout/auth-shell.d.ts +4 -2
  19. package/dist/components/layout/auth-shell.js +7 -1
  20. package/dist/components/layout/centered-shell.d.ts +2 -2
  21. package/dist/components/layout/flex.d.ts +1 -1
  22. package/dist/components/layout/flex.js +2 -1
  23. package/dist/components/layout/index.d.ts +3 -0
  24. package/dist/components/layout/index.js +2 -0
  25. package/dist/components/layout/mobile-shell.d.ts +21 -0
  26. package/dist/components/layout/mobile-shell.js +43 -0
  27. package/dist/components/layout/sidebar-link.d.ts +1 -1
  28. package/dist/components/navigation/context-menu.js +13 -9
  29. package/dist/components/navigation/dropdown-menu.js +17 -13
  30. package/dist/inertia/index.d.ts +1 -1
  31. package/dist/lib/variants.d.ts +3 -3
  32. package/dist/lib/variants.js +1 -0
  33. package/dist/props/components/data-display.prop.d.ts +65 -0
  34. package/dist/props/components/data-entry.prop.d.ts +1 -1
  35. package/dist/props/components/layout.prop.d.ts +93 -6
  36. package/dist/props/registry.d.ts +109 -2
  37. package/dist/props/registry.js +143 -0
  38. package/dist/props/vocabulary/data.prop.d.ts +6 -0
  39. package/dist/props/vocabulary/index.d.ts +3 -3
  40. package/dist/props/vocabulary/layout.prop.d.ts +17 -2
  41. package/dist/props/vocabulary/shared.prop.d.ts +7 -0
  42. package/dist/styles/control.css +18 -1
  43. package/dist/styles/data-display-layout.css +167 -0
  44. package/dist/styles/layout.css +7 -0
  45. package/dist/styles/shell-layout.css +148 -0
  46. package/dist/styles/table-layout.css +23 -4
  47. package/dist/tokens/components/control.css +1 -0
  48. package/dist/tokens/components/data-display.css +20 -0
  49. package/dist/tokens/components/shell.css +22 -0
  50. package/dist/tokens/components/table.css +2 -0
  51. package/docs/FRAME-A11Y-CI.md +99 -17
  52. package/docs/FRAME-COVERAGE-REPORT.md +4 -2
  53. package/docs/SPACING.md +19 -6
  54. package/docs/charts/compact-bar-trend.tsx +5 -6
  55. package/docs/data-display/avatar.tsx +7 -9
  56. package/docs/data-display/badge.tsx +1 -1
  57. package/docs/data-display/card/index.tsx +6 -4
  58. package/docs/data-display/data-table/examples/approval-queue.tsx +1 -1
  59. package/docs/data-display/data-table/index.tsx +1 -1
  60. package/docs/data-display/permission-matrix.tsx +2 -2
  61. package/docs/data-display/popover.tsx +4 -1
  62. package/docs/data-display/scroll-area.tsx +1 -1
  63. package/docs/data-display/table.tsx +1 -1
  64. package/docs/data-display/timeline-grid.tsx +119 -0
  65. package/docs/data-entry/branch-scope-picker.tsx +1 -1
  66. package/docs/data-entry/date-picker.tsx +2 -1
  67. package/docs/data-entry/form-field/index.tsx +2 -2
  68. package/docs/data-entry/input-otp.tsx +1 -1
  69. package/docs/data-entry/select-matrix.tsx +2 -1
  70. package/docs/data-entry/select.tsx +3 -1
  71. package/docs/data-entry/textarea.tsx +1 -1
  72. package/docs/data-entry/toggle-count.tsx +1 -1
  73. package/docs/feedback/alert.tsx +1 -1
  74. package/docs/feedback/banner.tsx +1 -1
  75. package/docs/feedback/dialog.tsx +2 -1
  76. package/docs/feedback/sheet.tsx +2 -1
  77. package/docs/foundation/email-tokens.tsx +1 -1
  78. package/docs/general/activity.tsx +1 -1
  79. package/docs/layout/app-shell.tsx +4 -4
  80. package/docs/layout/auth-account-summary.tsx +1 -1
  81. package/docs/layout/auth-recovery/examples/mobile-390.tsx +1 -1
  82. package/docs/layout/auth-recovery/examples/password-recovery.tsx +0 -2
  83. package/docs/layout/auth-shell-context.tsx +1 -1
  84. package/docs/layout/auth-shell-device.tsx +2 -2
  85. package/docs/layout/auth-shell-registration.tsx +1 -1
  86. package/docs/layout/auth-shell.tsx +1 -1
  87. package/docs/layout/master-detail.tsx +2 -2
  88. package/docs/layout/mobile-shell.tsx +101 -0
  89. package/docs/layout/page-container.tsx +6 -6
  90. package/docs/layout/responsive-grid.tsx +5 -5
  91. package/docs/layout/separator.tsx +2 -2
  92. package/docs/layout/service-role-panel.tsx +1 -1
  93. package/docs/layout/sidebar.tsx +9 -9
  94. package/docs/layout/split-pane.tsx +5 -6
  95. package/docs/layout/topbar.tsx +2 -8
  96. package/docs/navigation/context-menu.tsx +6 -1
  97. package/docs/navigation/dropdown-menu.tsx +2 -1
  98. package/docs/navigation/filter-bar.tsx +2 -2
  99. package/docs/navigation/pagination.tsx +2 -2
  100. package/docs/navigation/steps.tsx +1 -1
  101. package/docs/navigation/tabs.tsx +3 -3
  102. package/docs/showcase/acme-portal.tsx +11 -12
  103. package/docs/showcase/acme-website.tsx +7 -7
  104. package/docs/showcase/case1-warehouse-dashboard.tsx +11 -12
  105. package/docs/showcase/case2-employee-me.tsx +10 -16
  106. package/docs/showcase/case4-login.tsx +144 -156
  107. package/docs/showcase/case5-shift-calendar.tsx +181 -280
  108. package/docs/showcase/case6-agency-handy.tsx +446 -443
  109. package/docs/showcase/futurelastic-web.tsx +9 -8
  110. package/docs/showcase/org-switcher.tsx +18 -11
  111. package/docs/showcase/permission-matrix.tsx +13 -13
  112. package/docs/showcase/public-landing.tsx +1 -1
  113. package/docs/showcase/settings-account-sections.tsx +4 -4
  114. package/docs/showcase/settings-security-mfa.tsx +3 -3
  115. package/docs/showcase/table-approval-queue.tsx +1 -1
  116. package/docs/showcase/table-bulk-actions.tsx +49 -52
  117. package/docs/showcase/table-conditional-format.tsx +1 -1
  118. package/docs/showcase/table-crud-list.tsx +1 -1
  119. package/docs/showcase/table-expandable-rows.tsx +101 -98
  120. package/docs/showcase/table-filter-chips.tsx +78 -66
  121. package/docs/showcase/table-footer-totals.tsx +23 -19
  122. package/docs/showcase/table-grouped-subtotals.tsx +7 -4
  123. package/docs/showcase/table-master-detail.tsx +13 -15
  124. package/docs/showcase/table-states.tsx +34 -29
  125. package/docs/showcase/table-sticky-columns.tsx +3 -2
  126. package/docs/showcase/table-tree-rows.tsx +22 -15
  127. package/docs/showcase/table-view-tabs.tsx +12 -14
  128. package/package.json +7 -6
@@ -0,0 +1,48 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ const TABBABLE = 'a[href], area[href], button, input, select, textarea, iframe, object, embed, details, audio[controls], video[controls], [contenteditable="true"], [tabindex]:not([tabindex^="-"])';
4
+ const owned = /* @__PURE__ */ new Set();
5
+ let observer = null;
6
+ let mounted = 0;
7
+ function sync() {
8
+ for (const el of document.querySelectorAll("[data-aria-hidden]")) {
9
+ if (owned.has(el) || el.hasAttribute("inert")) continue;
10
+ if (el.querySelector(TABBABLE)) {
11
+ el.setAttribute("inert", "");
12
+ owned.add(el);
13
+ }
14
+ }
15
+ for (const el of [...owned]) {
16
+ if (el.isConnected && el.hasAttribute("data-aria-hidden")) continue;
17
+ el.removeAttribute("inert");
18
+ owned.delete(el);
19
+ }
20
+ }
21
+ function release() {
22
+ for (const el of owned) el.removeAttribute("inert");
23
+ owned.clear();
24
+ }
25
+ function useInertHiddenBackground() {
26
+ React.useEffect(() => {
27
+ mounted += 1;
28
+ if (!observer) {
29
+ observer = new MutationObserver(sync);
30
+ observer.observe(document.body, {
31
+ subtree: true,
32
+ attributes: true,
33
+ attributeFilter: ["data-aria-hidden"]
34
+ });
35
+ }
36
+ sync();
37
+ return () => {
38
+ mounted -= 1;
39
+ if (mounted > 0) return;
40
+ observer?.disconnect();
41
+ observer = null;
42
+ release();
43
+ };
44
+ }, []);
45
+ }
46
+ export {
47
+ useInertHiddenBackground
48
+ };
@@ -3,6 +3,8 @@ export type { AuthShellProp, AuthShellProp as AuthShellProps, } from "../../prop
3
3
  /**
4
4
  * AuthShell — centred auth/login page shell (login · mfa · passkey · device · reset). A top brand
5
5
  * bar (banner), a centred `main` that holds the auth `Card`, and an optional footer (contentinfo),
6
- * over a `min-h-dvh` surface.
6
+ * over a `min-h-dvh` surface. The banner also carries page-level `actions` at its inline end (a
7
+ * locale picker, a theme toggle), and `measure="wide"` opens the content slot to the split login
8
+ * (a brand panel beside the card) that used to have no shell of its own.
7
9
  */
8
- export declare function AuthShell({ brand, footer, children, variant, preset, density, className, }: AuthShellProp): import("react").JSX.Element;
10
+ export declare function AuthShell({ brand, actions, footer, children, variant, preset, measure, density, className, }: AuthShellProp): import("react").JSX.Element;
@@ -4,10 +4,12 @@ import { useTranslation } from "../../i18n/use-translation.js";
4
4
  import { cn } from "../../lib/utils.js";
5
5
  function AuthShell({
6
6
  brand,
7
+ actions,
7
8
  footer,
8
9
  children,
9
10
  variant = "default",
10
11
  preset = "default",
12
+ measure = "default",
11
13
  density,
12
14
  className
13
15
  }) {
@@ -19,10 +21,14 @@ function AuthShell({
19
21
  "data-slot": "auth-shell",
20
22
  "data-variant": variant,
21
23
  "data-preset": preset === "default" ? void 0 : preset,
24
+ "data-measure": measure === "default" ? void 0 : measure,
22
25
  "data-density": resolvedDensity,
23
26
  className: cn("ui-auth-shell", className),
24
27
  children: [
25
- brand !== void 0 && /* @__PURE__ */ jsx("header", { className: "ui-auth-shell-bar", "aria-label": t("layout.authShell.brandLabel"), children: brand }),
28
+ (brand !== void 0 || actions !== void 0) && /* @__PURE__ */ jsxs("header", { className: "ui-auth-shell-bar", "aria-label": t("layout.authShell.brandLabel"), children: [
29
+ brand,
30
+ actions !== void 0 && /* @__PURE__ */ jsx("div", { className: "ui-auth-shell-bar-actions", children: actions })
31
+ ] }),
26
32
  /* @__PURE__ */ jsx("main", { className: "ui-auth-shell-main", "aria-label": t("layout.authShell.mainLabel"), children: /* @__PURE__ */ jsx("div", { className: "ui-auth-shell-card", children }) }),
27
33
  footer !== void 0 && /* @__PURE__ */ jsx("footer", { className: "ui-auth-shell-footer", "aria-label": t("layout.authShell.footerLabel"), children: footer })
28
34
  ]
@@ -5,7 +5,7 @@ export type { CenteredShellProp, CenteredShellProp as CenteredShellProps, } from
5
5
  * account / self-service, and standalone-settings shape. It fills the gap between the two existing
6
6
  * shells: - `AppShell` REQUIRES a sidebar — its padded topbar chrome (`.app-topbar`) is a grid
7
7
  * area beside the nav rail, so you cannot get a padded top bar without a rail. - `AuthShell` is
8
- * the UNAUTHENTICATED root (login/mfa/reset); it centres a narrow ~24rem card VERTICALLY and has
9
- * no top-right actions slot.
8
+ * the UNAUTHENTICATED root (login/mfa/reset); it centres its card VERTICALLY, so a page that
9
+ * scrolls its own content belongs here instead.
10
10
  */
11
11
  export declare function CenteredShell({ topbar, footer, children, width, align, preset, className, }: CenteredShellProp): import("react").JSX.Element;
@@ -1,3 +1,3 @@
1
1
  import type { FlexProp } from "../../props/components/layout.prop.js";
2
2
  export type { FlexAlignProp, FlexDirectionProp, FlexJustifyProp, FlexProp, FlexProp as FlexProps, } from "../../props/components/layout.prop.js";
3
- export declare function Flex({ direction, gap, align, justify, wrap, hideBelow, hideFrom, className, children, ...props }: FlexProp): import("react").JSX.Element;
3
+ export declare function Flex({ as: Element, direction, gap, align, justify, wrap, hideBelow, hideFrom, className, children, ...props }: FlexProp): import("react").JSX.Element;
@@ -4,6 +4,7 @@ import { mergeAriaIds } from "../../lib/field-a11y.js";
4
4
  import { cn } from "../../lib/utils.js";
5
5
  import { flexGapClass } from "../../lib/variants.js";
6
6
  function Flex({
7
+ as: Element = "div",
7
8
  direction = "row",
8
9
  gap = "md",
9
10
  align,
@@ -30,7 +31,7 @@ function Flex({
30
31
  };
31
32
  }
32
33
  return /* @__PURE__ */ jsx(
33
- "div",
34
+ Element,
34
35
  {
35
36
  "data-direction": direction,
36
37
  "data-align": align,
@@ -22,6 +22,9 @@ export { AuthFooter } from "./auth-footer.js";
22
22
  export type { AuthFooterProp, AuthFooterProps } from "./auth-footer.js";
23
23
  export { AuthStack } from "./auth-stack.js";
24
24
  export type { AuthStackProps } from "./auth-stack.js";
25
+ export { MobileShell } from "./mobile-shell.js";
26
+ export type { MobileShellProp, MobileShellProps } from "./mobile-shell.js";
27
+ export type { MobileShellHeightProp } from "../../props/vocabulary/index.js";
25
28
  export { CenteredShell } from "./centered-shell.js";
26
29
  export type { CenteredShellProp, CenteredShellProps } from "./centered-shell.js";
27
30
  export type { CenteredShellWidthProp, CenteredShellAlignProp } from "../../props/vocabulary/index.js";
@@ -10,6 +10,7 @@ import { AccountChip } from "./account-chip.js";
10
10
  import { AuthAccountSummary } from "./auth-account-summary.js";
11
11
  import { AuthFooter } from "./auth-footer.js";
12
12
  import { AuthStack } from "./auth-stack.js";
13
+ import { MobileShell } from "./mobile-shell.js";
13
14
  import { CenteredShell } from "./centered-shell.js";
14
15
  import { ErrorSurface } from "./error-surface.js";
15
16
  import { Breadcrumb } from "./breadcrumb.js";
@@ -38,6 +39,7 @@ export {
38
39
  Flex,
39
40
  LegalDocumentShell,
40
41
  MasterDetail,
42
+ MobileShell,
41
43
  OrgSwitcher,
42
44
  PageContainer,
43
45
  ResizableHandle,
@@ -0,0 +1,21 @@
1
+ import type { MobileShellProp } from "../../props/components/layout.prop.js";
2
+ export type { MobileShellProp, MobileShellProp as MobileShellProps, } from "../../props/components/layout.prop.js";
3
+ /**
4
+ * MobileShell — the HANDHELD app shell: a status band, an app bar, ONE scroll region,
5
+ * a sticky action bar and a bottom tab bar, in that fixed order.
6
+ *
7
+ * The fourth root shell. The other three cannot express a phone app: `AppShell` REQUIRES a sidebar
8
+ * (its bar is a grid area beside the nav rail), `AuthShell` is the UNAUTHENTICATED root and centres
9
+ * a ~24rem card, and `CenteredShell` is a scrolling DOCUMENT — its `main` scrolls the page, which
10
+ * is precisely what a handheld app must not do.
11
+ *
12
+ * SOURCE ORDER IS THE LAYOUT. The bands are a flex column and nothing repositions them, so the DOM
13
+ * order and the reading order are the same order — the accessibility property AppShell has to buy
14
+ * back with grid areas. `actions` therefore comes before `tabBar` in the DOM as well as on screen.
15
+ *
16
+ * i18n: the three landmarks reuse the SHARED shell labels (`layout.appShell.headerLabel` /
17
+ * `.mainLabel` for the bar and the scroll region, `layout.sidebar.ariaLabel` for the tab bar, which
18
+ * IS the app's primary navigation). They are the same strings in all three locales, and a fourth
19
+ * identical copy under `layout.mobileShell.*` would be three files of duplication to keep in sync.
20
+ */
21
+ export declare function MobileShell({ statusBar, header, children, actions, tabBar, height, className, }: MobileShellProp): import("react").JSX.Element;
@@ -0,0 +1,43 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useTranslation } from "../../i18n/use-translation.js";
4
+ import { cn } from "../../lib/utils.js";
5
+ function MobileShell({
6
+ statusBar,
7
+ header,
8
+ children,
9
+ actions,
10
+ tabBar,
11
+ height = "viewport",
12
+ className
13
+ }) {
14
+ const { t } = useTranslation();
15
+ return /* @__PURE__ */ jsxs(
16
+ "div",
17
+ {
18
+ "data-slot": "mobile-shell",
19
+ "data-height": height === "viewport" ? void 0 : height,
20
+ className: cn("ui-mobile-shell", className),
21
+ children: [
22
+ (statusBar !== void 0 || header !== void 0) && /* @__PURE__ */ jsxs("header", { className: "ui-mobile-shell-chrome", "aria-label": t("layout.appShell.headerLabel"), children: [
23
+ statusBar !== void 0 && /* @__PURE__ */ jsx("div", { className: "ui-mobile-shell-status", children: statusBar }),
24
+ header !== void 0 && /* @__PURE__ */ jsx("div", { className: "ui-mobile-shell-header", children: header })
25
+ ] }),
26
+ /* @__PURE__ */ jsx(
27
+ "main",
28
+ {
29
+ className: "ui-mobile-shell-main",
30
+ "aria-label": t("layout.appShell.mainLabel"),
31
+ tabIndex: 0,
32
+ children
33
+ }
34
+ ),
35
+ actions !== void 0 && /* @__PURE__ */ jsx("footer", { className: "ui-mobile-shell-actions", children: actions }),
36
+ tabBar !== void 0 && /* @__PURE__ */ jsx("nav", { className: "ui-mobile-shell-tabbar", "aria-label": t("layout.sidebar.ariaLabel"), children: tabBar })
37
+ ]
38
+ }
39
+ );
40
+ }
41
+ export {
42
+ MobileShell
43
+ };
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import type { SidebarLinkComponentProp } from "../../props/components/layout.prop.js";
3
3
  /**
4
- * Adapt a framework router `Link` to {@link SidebarLinkComponentProp} (gh#213) — a dependency-free
4
+ * Adapt a framework router `Link` to {@link SidebarLinkComponentProp} — a dependency-free
5
5
  * factory, so `@godxjp/ui` still ships ZERO router/framework dependencies.
6
6
  *
7
7
  * It lives in its own module (imports only React + the prop types) so `@godxjp/ui/inertia` can build
@@ -4,20 +4,24 @@ import * as React from "react";
4
4
  import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
5
5
  import { Check, ChevronRight, Circle } from "lucide-react";
6
6
  import { cn } from "../../lib/utils.js";
7
+ import { useInertHiddenBackground } from "../general/inert-background.js";
7
8
  const ContextMenu = ContextMenuPrimitive.Root;
8
9
  function ContextMenuTrigger(props) {
9
10
  return /* @__PURE__ */ jsx(ContextMenuPrimitive.Trigger, { "data-slot": "context-menu-trigger", ...props });
10
11
  }
11
12
  const ContextMenuPortal = ContextMenuPrimitive.Portal;
12
- const ContextMenuContent = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(ContextMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx(
13
- ContextMenuPrimitive.Content,
14
- {
15
- ref,
16
- "data-slot": "context-menu-content",
17
- className: cn("ui-context-menu-content", className),
18
- ...props
19
- }
20
- ) }));
13
+ const ContextMenuContent = React.forwardRef(({ className, ...props }, ref) => {
14
+ useInertHiddenBackground();
15
+ return /* @__PURE__ */ jsx(ContextMenuPrimitive.Portal, { children: /* @__PURE__ */ jsx(
16
+ ContextMenuPrimitive.Content,
17
+ {
18
+ ref,
19
+ "data-slot": "context-menu-content",
20
+ className: cn("ui-context-menu-content", className),
21
+ ...props
22
+ }
23
+ ) });
24
+ });
21
25
  ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
22
26
  const ContextMenuItem = React.forwardRef(({ className, inset, variant = "default", ...props }, ref) => /* @__PURE__ */ jsx(
23
27
  ContextMenuPrimitive.Item,
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5
5
  import { Check, ChevronRight } from "lucide-react";
6
6
  import { cn } from "../../lib/utils.js";
7
+ import { useInertHiddenBackground } from "../general/inert-background.js";
7
8
  function DropdownMenu(props) {
8
9
  return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Root, { "data-slot": "dropdown-menu", ...props });
9
10
  }
@@ -22,19 +23,22 @@ function DropdownMenuRadioGroup(props) {
22
23
  function DropdownMenuSub(props) {
23
24
  return /* @__PURE__ */ jsx(DropdownMenuPrimitive.Sub, { "data-slot": "dropdown-menu-sub", ...props });
24
25
  }
25
- const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => /* @__PURE__ */ jsx(DropdownMenuPortal, { children: /* @__PURE__ */ jsx(
26
- DropdownMenuPrimitive.Content,
27
- {
28
- ref,
29
- "data-slot": "dropdown-menu-content",
30
- sideOffset,
31
- className: cn(
32
- "ui-dropdown-menu-content data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 origin-[var(--radix-dropdown-menu-content-transform-origin)]",
33
- className
34
- ),
35
- ...props
36
- }
37
- ) }));
26
+ const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => {
27
+ useInertHiddenBackground();
28
+ return /* @__PURE__ */ jsx(DropdownMenuPortal, { children: /* @__PURE__ */ jsx(
29
+ DropdownMenuPrimitive.Content,
30
+ {
31
+ ref,
32
+ "data-slot": "dropdown-menu-content",
33
+ sideOffset,
34
+ className: cn(
35
+ "ui-dropdown-menu-content data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 origin-[var(--radix-dropdown-menu-content-transform-origin)]",
36
+ className
37
+ ),
38
+ ...props
39
+ }
40
+ ) });
41
+ });
38
42
  DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
39
43
  const DropdownMenuItem = React.forwardRef(({ className, inset, variant = "default", ...props }, ref) => /* @__PURE__ */ jsx(
40
44
  DropdownMenuPrimitive.Item,
@@ -68,7 +68,7 @@ export interface InertiaLinkLike {
68
68
  children?: ReactNode;
69
69
  }
70
70
  /**
71
- * Bind Inertia's `<Link>` as the Sidebar's row element (gh#213).
71
+ * Bind Inertia's `<Link>` as the Sidebar's row element.
72
72
  *
73
73
  * The LIBRARY still composes every row — the 16px icon slot, the label, the badge, `data-active` /
74
74
  * `aria-current`, the icon-only collapsed rail and its tooltip name — and hands it to the link as
@@ -3,10 +3,10 @@ import type { GapProp, PageContainerVariantProp, PageDensityProp } from "../prop
3
3
  export type Density = PageDensityProp;
4
4
  export type StackGap = GapProp;
5
5
  export type FlexGap = GapProp;
6
- export type InlineGap = Exclude<GapProp, "xl">;
6
+ export type InlineGap = Exclude<GapProp, "xl" | "none">;
7
7
  export type PageContainerVariant = PageContainerVariantProp;
8
8
  export declare const densityClass: Record<PageDensityProp, string>;
9
9
  export declare const pageContainerVariantClass: Record<PageContainerVariantProp, string | undefined>;
10
- export declare const stackGapClass: Record<GapProp, string>;
11
- export declare const inlineGapClass: Record<Exclude<GapProp, "xl">, string>;
10
+ export declare const stackGapClass: Record<Exclude<GapProp, "none">, string>;
11
+ export declare const inlineGapClass: Record<InlineGap, string>;
12
12
  export declare const flexGapClass: Record<GapProp, string>;
@@ -23,6 +23,7 @@ const inlineGapClass = {
23
23
  lg: "ui-inline-lg"
24
24
  };
25
25
  const flexGapClass = {
26
+ none: "ui-flex-gap-none",
26
27
  xs: "ui-flex-gap-xs",
27
28
  sm: "ui-flex-gap-sm",
28
29
  md: "ui-flex-gap-md",
@@ -137,6 +137,12 @@ export type AvatarPresenceProp = "online" | "away" | "busy" | "offline";
137
137
  export type AvatarAppearanceProp = "default" | "tinted";
138
138
  /** @see Badge */
139
139
  export type BadgeProp = {
140
+ /**
141
+ * Render element — `div` (default) or `span` when the chip sits in a phrasing context where a
142
+ * `<div>` is invalid HTML (inside a `<button>` rendered by TabsTrigger/PopoverTrigger/Button,
143
+ * a `<label>`, a `<p>`). Swaps the tag only.
144
+ */
145
+ as?: "div" | "span";
140
146
  variant?: "default" | "secondary" | "outline";
141
147
  /** Status tones plus a brand `primary` tone (soft brand pill); solid brand = `variant="default"`. */
142
148
  tone?: ToneProp | "primary";
@@ -360,3 +366,62 @@ export type ScrollAreaProp = {
360
366
  */
361
367
  onAnchoredChange?: (anchored: boolean) => void;
362
368
  };
369
+ /**
370
+ * @see TimelineGrid — one COLUMN of the grid (a day, a room, a machine). The label names the
371
+ * column visually AND names the column's event list for assistive technology.
372
+ */
373
+ export type TimelineGridColumnProp = {
374
+ /** Stable column id — the `columnId` an event points at. */
375
+ id: string;
376
+ /** Column head. Also the accessible name of that column's event list, so keep it text. */
377
+ label: LabelProp;
378
+ /** Secondary line under the head (a date, a capacity, a room number). */
379
+ description?: DescriptionProp;
380
+ /** Marks the column as "now" (today's column): it carries the tint and hosts the `now` marker. */
381
+ current?: boolean;
382
+ };
383
+ /**
384
+ * @see TimelineGrid — one event BLOCK. `start`/`end` are clock times in the column's own day,
385
+ * `"HH:MM"` 24-hour (`"24:00"` = end of day). An `end` at or before `start` continues into the
386
+ * next day (22:00–06:00), and the block is drawn to the end of the window and marked clipped.
387
+ */
388
+ export type TimelineGridEventProp = {
389
+ /** Stable event id — the React key, and the `data-event-id` on the block. */
390
+ id: string;
391
+ /** Id of the column this event belongs to. An event pointing at no column is not drawn. */
392
+ columnId: string;
393
+ /** Start clock time, `"HH:MM"`. */
394
+ start: string;
395
+ /** End clock time, `"HH:MM"`. At or before `start` = continues into the next day. */
396
+ end: string;
397
+ /** Block title. The block also renders the time range as text, so the range is never colour or position alone. */
398
+ title: TitleProp;
399
+ /** Secondary line inside the block (who is on the shift, a room, a customer). */
400
+ description?: DescriptionProp;
401
+ /** The record's own colour, washed exactly like `Badge color` — decorative, never the only signal. */
402
+ color?: string;
403
+ };
404
+ /** @see TimelineGrid */
405
+ export type TimelineGridProp = {
406
+ /**
407
+ * Accessible name of the grid. Required, and a plain `string`: the grid is a focusable scroll
408
+ * region, so its name has to survive as an `aria-label`.
409
+ */
410
+ label: Extract<LabelProp, string>;
411
+ /** Columns in render order. */
412
+ columns: readonly TimelineGridColumnProp[];
413
+ /** Events in any order; the grid sorts each column by start time and lays overlaps out side by side. */
414
+ events: readonly TimelineGridEventProp[];
415
+ /** First clock time on the axis, `"HH:MM"`. Defaults to the earliest event, on the hour. */
416
+ start?: string;
417
+ /** Last clock time on the axis, `"HH:MM"`. Defaults to the latest event, on the hour. */
418
+ end?: string;
419
+ /** Hours between hour rules and axis labels. Default `1`. */
420
+ interval?: number;
421
+ /** Current clock time, `"HH:MM"`. Draws the now marker in the columns marked `current`. */
422
+ now?: string;
423
+ /** Block click handler. Its PRESENCE turns every block into a real `button`. */
424
+ onEventSelect?: (event: TimelineGridEventProp) => void;
425
+ className?: ClassNameProp;
426
+ id?: IdProp;
427
+ };
@@ -106,7 +106,7 @@ export type FormProp = React.FormHTMLAttributes<HTMLFormElement> & {
106
106
  className?: ClassNameProp;
107
107
  };
108
108
  /**
109
- * @see FormField — exactly one of `children` (an interactive control) or `staticText` (gh#294):
109
+ * @see FormField — exactly one of `children` (an interactive control) or `staticText`:
110
110
  * a read-only VALUE row inside the same Form, styled to match `Descriptions.Item`'s value
111
111
  * typography (`text-sm break-all`) byte-for-byte. This is the "mixed read-only + editable fields
112
112
  * on one form" case (an immutable name/email row above an editable role Select, for example) —
@@ -1,7 +1,7 @@
1
1
  /** Layout component prop types — @see docs/COMPONENTS.md#layout */
2
2
  import type * as React from "react";
3
3
  import type { ComponentType, ReactNode, SVGProps } from "react";
4
- import type { BreadcrumbProp, TitleProp, SubtitleProp, StatusProp, ExtraProp, FooterProp, PageDensityProp, PageContainerVariantProp, CenteredShellWidthProp, CenteredShellAlignProp, CenteredShellPresetProp, ErrorSurfaceModeProp, ErrorSurfaceStatusProp, AuthShellPresetProp, OrientationProp, TextAlignProp, TextToneProp, BreakpointProp, GapProp, ClassNameProp, ChildrenProp, IdProp, DisabledProp, DescriptionProp, ActionProp, IconProp, HeadingLevelProp, ToneProp } from "../vocabulary/index.js";
4
+ import type { BreadcrumbProp, TitleProp, SubtitleProp, StatusProp, ExtraProp, FooterProp, PageDensityProp, PageContainerVariantProp, CenteredShellWidthProp, CenteredShellAlignProp, CenteredShellPresetProp, ErrorSurfaceModeProp, ErrorSurfaceStatusProp, AuthShellPresetProp, MobileShellHeightProp, OrientationProp, TextAlignProp, TextToneProp, BreakpointProp, GapProp, ClassNameProp, ChildrenProp, IdProp, DisabledProp, DescriptionProp, ActionProp, IconProp, HeadingLevelProp, ToneProp } from "../vocabulary/index.js";
5
5
  import type { EmptyStateToneProp } from "./data-display.prop.js";
6
6
  /**
7
7
  * Arrangement of the page header's title band and its `extra` slot below the 640px step.
@@ -102,6 +102,14 @@ export type FlexAlignProp = "start" | "center" | "end" | "stretch" | "baseline";
102
102
  export type FlexJustifyProp = "start" | "center" | "end" | "between" | "around" | "evenly";
103
103
  /** @see Flex */
104
104
  export type FlexProp = React.HTMLAttributes<HTMLDivElement> & {
105
+ /**
106
+ * Render element — `div` (default) or `span` when the Flex sits in a PHRASING context and a
107
+ * `<div>` would be invalid HTML: inside a `TabsTrigger`/`PopoverTrigger`/`Button` (all of which
108
+ * render a `<button>`, whose content model is phrasing content only), inside a `<label>`, or
109
+ * inside a `<p>`. Same closed shape as `ListRow`'s `as` — it swaps the TAG, nothing else: the
110
+ * `.ui-flex` rules carry `display: flex`, so the box is identical either way.
111
+ */
112
+ as?: "div" | "span";
105
113
  direction?: FlexDirectionProp;
106
114
  gap?: GapProp;
107
115
  align?: FlexAlignProp;
@@ -226,6 +234,13 @@ export type AuthShellProp = {
226
234
  children: ReactNode;
227
235
  /** Brand bar slot pinned to the top (e.g. a `<Logo>` / product mark). */
228
236
  brand?: ReactNode;
237
+ /**
238
+ * Page-level controls pinned to the TOP-RIGHT of the same banner row as `brand` — a locale
239
+ * picker, a theme toggle, a "need help?" link. These belong to the PAGE, not to the auth form,
240
+ * which is why they sit in the bar rather than inside the card. The banner renders as soon as
241
+ * `brand` OR `actions` is present, so an actions-only bar is a legal shape.
242
+ */
243
+ actions?: ActionProp;
229
244
  /** Footer slot pinned to the bottom (legal links, locale switch, support). */
230
245
  footer?: ReactNode;
231
246
  /**
@@ -242,6 +257,16 @@ export type AuthShellProp = {
242
257
  * canonical 1440x900, 1024x900 and 390x844 viewports.
243
258
  */
244
259
  preset?: AuthShellPresetProp;
260
+ /**
261
+ * Inline MEASURE of the shell's content slot. `"default"` keeps the single auth card (24rem, or
262
+ * the canonical variant's 22.5rem). `"wide"` opens the slot to
263
+ * `--auth-shell-wide-card-max-width` so a SPLIT login fits — a brand/marketing panel beside the
264
+ * auth card, the shape that previously had no shell and reached for `CenteredShell`. The wide
265
+ * slot centres itself with auto margins, so a tall two-column layout starts at the top instead
266
+ * of overflowing above the scroll origin. Ignored under a `preset`: a preset already owns its
267
+ * flow geometry.
268
+ */
269
+ measure?: "default" | "wide";
245
270
  /**
246
271
  * Vertical density scoped to auth-card descendants. The canonical variant defaults to
247
272
  * `"compact"`; the default variant defaults to `"comfortable"`.
@@ -249,6 +274,68 @@ export type AuthShellProp = {
249
274
  density?: "comfortable" | "compact";
250
275
  className?: ClassNameProp;
251
276
  };
277
+ /**
278
+ * @see MobileShell — the HANDHELD app shell: a status band, an app bar, ONE scroll region, a
279
+ * sticky action bar and a bottom tab bar, in that fixed order.
280
+ *
281
+ * It is the fourth root shell, and it exists because the other three cannot express a phone app:
282
+ * `AppShell` REQUIRES a sidebar (its bar is a grid area beside the nav rail), `AuthShell` is the
283
+ * UNAUTHENTICATED root and centres a ~24rem card, and `CenteredShell` is a scrolling DOCUMENT —
284
+ * its `main` scrolls the page, which is exactly what a handheld app must not do. Composing one out
285
+ * of `Card` + `ui-card-inset*` (what docs/showcase/case6 once did) reproduces the look
286
+ * and none of the two behaviours that matter on a real device:
287
+ *
288
+ * 1. The shell is the only scroll container. The root is exactly one screen tall, so the DOCUMENT
289
+ * never scrolls and the tab bar never slides away under a collapsing URL bar.
290
+ * 2. Every band pads itself out of `env(safe-area-inset-*)`, so the notch never covers the app
291
+ * bar and the home indicator never covers the primary verb.
292
+ *
293
+ * Layout-only, like the other shells: it paints chrome and owns geometry, and delegates motion to
294
+ * `Reveal`. Never nest it inside another shell — it is a ROOT.
295
+ */
296
+ export type MobileShellProp = {
297
+ /**
298
+ * The scrolling screen body — the ONLY scroll container in the shell, and the only elastic band.
299
+ * Everything else is fixed chrome, so a long list scrolls under a stationary app bar and tab bar.
300
+ */
301
+ children: ReactNode;
302
+ /**
303
+ * App bar (banner) pinned to the top: the screen title plus its inline actions. When there is no
304
+ * `statusBar` this band absorbs the top safe-area inset itself. Omit → no banner.
305
+ *
306
+ * On a screen with a MODE (multi-select, search, edit) swap the whole node rather than stacking a
307
+ * second strip under it — replacing the bar's contents is the platform pattern on both iOS and
308
+ * Android, and it keeps one bar to read instead of two.
309
+ */
310
+ header?: ReactNode;
311
+ /**
312
+ * The band that sits IN the OS status-bar strip above the app bar — the carrier/clock row of a
313
+ * `display-mode: standalone` PWA, or the simulated one in a device-frame preview. It absorbs the
314
+ * top safe-area inset when present. Omit it in an ordinary browser tab, where the OS already owns
315
+ * that strip: the header then takes the inset.
316
+ */
317
+ statusBar?: ReactNode;
318
+ /**
319
+ * The sticky action bar pinned above the tab bar — the screen's primary verb (Scan, Save, Hand
320
+ * over) and at most one secondary. It sits OUTSIDE the scroll region, so it is always reachable
321
+ * without `position: sticky` and without a scroll-padding hack, and it takes the home-indicator
322
+ * inset whenever no tab bar follows it.
323
+ */
324
+ actions?: ActionProp;
325
+ /**
326
+ * Bottom tab bar (navigation) — the app's top-level destinations. Its children TILE: equal width,
327
+ * no seam, no page gutter, so the consumer never hand-rolls a grid with a column count. Always
328
+ * the last band, so it owns the home-indicator inset.
329
+ */
330
+ tabBar?: ReactNode;
331
+ /**
332
+ * Where the shell's one-screen height comes from. `"viewport"` (default) is the real app —
333
+ * exactly `100dvh`, document never scrolls. `"fill"` fills a BOUNDED parent instead (a
334
+ * device-frame preview, a phone view embedded in a wider page).
335
+ */
336
+ height?: MobileShellHeightProp;
337
+ className?: ClassNameProp;
338
+ };
252
339
  /**
253
340
  * @see Separator — the tokenized rule, optionally INTERRUPTED by a localized label.
254
341
  *
@@ -256,7 +343,7 @@ export type AuthShellProp = {
256
343
  * what they have always been: an inert Radix rule, `decorative` by default, nothing announced.
257
344
  * With a `label` the root becomes the three-cell grid `rule · label · rule` and, because
258
345
  * "new messages" is CONTENT rather than decoration, a real `role="separator"` carrying the label
259
- * as its accessible name (gh#308). Every constant — rule weight, label gap, label inset, label
346
+ * as its accessible name. Every constant — rule weight, label gap, label inset, label
260
347
  * type ramp, rule and label colour per tone — is a `--separator-*` component token (rules #44/#45),
261
348
  * so a service retunes a day divider or an unread watermark from its theme and never forks CSS.
262
349
  */
@@ -284,7 +371,7 @@ export type SeparatorProp = Omit<React.HTMLAttributes<HTMLDivElement>, "children
284
371
  decorative?: boolean;
285
372
  className?: ClassNameProp;
286
373
  };
287
- /** @see AuthDivider — the auth-scoped preset over `Separator label` (gh#308). */
374
+ /** @see AuthDivider — the auth-scoped preset over `Separator label`. */
288
375
  export type AuthDividerProp = {
289
376
  /** Short localized conjunction rendered between the two separator rules (for example, "or"). */
290
377
  label: string;
@@ -577,7 +664,7 @@ export type SidebarLinkComponentProp = ComponentType<SidebarLinkProp>;
577
664
  * @deprecated Prefer `Sidebar.linkComponent` (or `SidebarItem asChild`), where the library composes
578
665
  * the row and the consumer supplies only the element. `renderItem` leaves row CONTENT to the
579
666
  * consumer, which is how a `<Link>{item.label}</Link>` silently dropped every icon and badge
580
- * (gh#213). Spreading `rowProps` — including its `children` — now yields the canonical row.
667
+ *. Spreading `rowProps` — including its `children` — now yields the canonical row.
581
668
  */
582
669
  export type SidebarRenderItemProp = {
583
670
  className: string;
@@ -662,7 +749,7 @@ export type SidebarProp = {
662
749
  collapsed?: boolean;
663
750
  children?: ChildrenProp;
664
751
  /**
665
- * THE framework-router contract (gh#213). Supply only the LINK ELEMENT TYPE; the Sidebar still
752
+ * THE framework-router contract. Supply only the LINK ELEMENT TYPE; the Sidebar still
666
753
  * composes the row — icon slot, label, badge, `data-active`/`aria-current`, the icon-only
667
754
  * collapsed rail and its tooltip name — and passes it as {@link SidebarLinkProp} `children`.
668
755
  * Used for every row that carries an `href`: top-level leaves, submenu children, collapsed-rail
@@ -690,7 +777,7 @@ export type SidebarProp = {
690
777
  *
691
778
  * Legacy escape hatch: return a SINGLE interactive element and the Sidebar merges the row styling
692
779
  * + active state onto it via Slot. Because row CONTENT stayed consumer-authored, a
693
- * `<Link>{item.label}</Link>` silently dropped every icon and badge (the gh#213 production
780
+ * `<Link>{item.label}</Link>` silently dropped every icon and badge (the production
694
781
  * regression). `rowProps` now also carries the composed `children`, so spreading it restores the
695
782
  * canonical row.
696
783
  */