@godxjp/ui 18.9.0 → 18.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +202 -0
  2. package/dist/components/data-display/badge.d.ts +7 -0
  3. package/dist/components/data-display/badge.js +1 -1
  4. package/dist/components/data-display/data-table.js +2 -2
  5. package/dist/components/data-display/index.d.ts +2 -0
  6. package/dist/components/data-display/index.js +2 -0
  7. package/dist/components/data-display/permission-matrix.d.ts +26 -0
  8. package/dist/components/data-display/permission-matrix.js +219 -0
  9. package/dist/components/data-display/table.d.ts +12 -0
  10. package/dist/components/data-entry/branch-scope-picker.d.ts +43 -0
  11. package/dist/components/data-entry/branch-scope-picker.js +200 -0
  12. package/dist/components/data-entry/index.d.ts +2 -0
  13. package/dist/components/data-entry/index.js +2 -0
  14. package/dist/components/data-entry/select.d.ts +7 -1
  15. package/dist/components/data-entry/select.js +46 -26
  16. package/dist/components/feedback/alert.d.ts +2 -20
  17. package/dist/components/feedback/alert.js +1 -11
  18. package/dist/components/feedback/banner.d.ts +21 -0
  19. package/dist/components/feedback/banner.js +19 -0
  20. package/dist/components/feedback/index.d.ts +4 -2
  21. package/dist/components/feedback/index.js +2 -2
  22. package/dist/components/general/typography.d.ts +1 -0
  23. package/dist/components/general/typography.js +32 -13
  24. package/dist/components/layout/auth-shell.d.ts +2 -2
  25. package/dist/components/layout/index.d.ts +2 -2
  26. package/dist/components/layout/index.js +2 -2
  27. package/dist/components/layout/page-container.d.ts +1 -1
  28. package/dist/components/layout/page-container.js +61 -15
  29. package/dist/components/layout/resizable.d.ts +1 -1
  30. package/dist/components/layout/resizable.js +26 -8
  31. package/dist/components/layout/service-role-panel.d.ts +25 -0
  32. package/dist/components/layout/service-role-panel.js +206 -0
  33. package/dist/components/navigation/filter-bar.d.ts +2 -2
  34. package/dist/components/navigation/filter-bar.js +113 -34
  35. package/dist/components/navigation/index.d.ts +1 -1
  36. package/dist/components/navigation/pagination.js +20 -1
  37. package/dist/components/navigation/tabs.js +6 -1
  38. package/dist/i18n/messages/en.json +59 -4
  39. package/dist/i18n/messages/ja.json +54 -5
  40. package/dist/i18n/messages/vi.json +53 -4
  41. package/dist/lib/field-a11y.d.ts +7 -0
  42. package/dist/lib/field-a11y.js +6 -0
  43. package/dist/lib/hooks.d.ts +15 -0
  44. package/dist/lib/hooks.js +37 -0
  45. package/dist/props/components/data-display.prop.d.ts +66 -0
  46. package/dist/props/components/data-entry.prop.d.ts +54 -0
  47. package/dist/props/components/feedback.prop.d.ts +3 -3
  48. package/dist/props/components/general.prop.d.ts +8 -1
  49. package/dist/props/components/layout.prop.d.ts +80 -51
  50. package/dist/props/components/navigation.prop.d.ts +77 -50
  51. package/dist/props/registry.d.ts +274 -54
  52. package/dist/props/registry.js +362 -73
  53. package/dist/props/vocabulary/content.prop.d.ts +5 -0
  54. package/dist/props/vocabulary/index.d.ts +1 -1
  55. package/dist/props/vocabulary/interaction.prop.d.ts +4 -7
  56. package/dist/props/vocabulary/layout.prop.d.ts +2 -2
  57. package/dist/styles/alert-layout.css +15 -32
  58. package/dist/styles/badge-layout.css +5 -0
  59. package/dist/styles/control.css +5 -0
  60. package/dist/styles/layout.css +118 -108
  61. package/dist/styles/shell-layout.css +16 -7
  62. package/dist/styles/table-layout.css +92 -2
  63. package/dist/styles/text-layout.css +13 -0
  64. package/dist/tokens/base.css +1 -0
  65. package/dist/tokens/components/badge.css +5 -0
  66. package/dist/tokens/components/banner.css +16 -0
  67. package/dist/tokens/components/navigation.css +10 -25
  68. package/dist/tokens/components/shell.css +20 -9
  69. package/dist/tokens/components/table.css +41 -1
  70. package/dist/tokens/semantic/layout.css +8 -7
  71. package/package.json +15 -2
  72. package/dist/components/layout/page-header.d.ts +0 -22
  73. package/dist/components/layout/page-header.js +0 -89
@@ -1,9 +1,10 @@
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";
4
6
  import { cn } from "../../lib/utils.js";
5
7
  import { densityClass, pageContainerVariantClass } from "../../lib/variants.js";
6
- import { PageHeader } from "./page-header.js";
7
8
  function scrollParent(el) {
8
9
  let node = el?.parentElement ?? null;
9
10
  while (node) {
@@ -35,14 +36,14 @@ function PageContainerInset({ className, children, ...props }) {
35
36
  function PageContainerRoot({
36
37
  title,
37
38
  subtitle,
38
- meta,
39
- headerLoading,
39
+ status,
40
40
  extra,
41
41
  footer,
42
42
  breadcrumb,
43
43
  breadcrumbLabel,
44
44
  breadcrumbAriaLabel,
45
45
  linkComponent: LinkComponent = "a",
46
+ headerLoading = false,
46
47
  density,
47
48
  variant = "default",
48
49
  preset = "default",
@@ -56,6 +57,7 @@ function PageContainerRoot({
56
57
  }) {
57
58
  const reveal = stickyFooter && footer != null && footerReveal === "onScroll";
58
59
  const { headerRef, revealed } = useFooterReveal(reveal);
60
+ const { t } = useTranslation();
59
61
  return /* @__PURE__ */ jsxs(
60
62
  "div",
61
63
  {
@@ -74,20 +76,64 @@ function PageContainerRoot({
74
76
  className
75
77
  ),
76
78
  children: [
77
- /* @__PURE__ */ jsx(
78
- PageHeader,
79
+ /* @__PURE__ */ jsxs(
80
+ "header",
79
81
  {
80
82
  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
83
+ className: "ui-page-header",
84
+ "data-layout": headerLayout,
85
+ "aria-busy": headerLoading ? "true" : void 0,
86
+ children: [
87
+ breadcrumb && breadcrumb.length > 0 && /* @__PURE__ */ jsx(
88
+ "nav",
89
+ {
90
+ "aria-label": breadcrumbLabel ?? breadcrumbAriaLabel ?? t("navigation.breadcrumb.ariaLabel"),
91
+ className: "ui-breadcrumb",
92
+ children: /* @__PURE__ */ jsx("ol", { className: "ui-breadcrumb-list", children: breadcrumb.map((item, i) => {
93
+ const isLast = i === breadcrumb.length - 1;
94
+ return /* @__PURE__ */ jsxs("li", { className: "ui-inline-xs", children: [
95
+ item.to && !isLast ? /* @__PURE__ */ jsx(
96
+ LinkComponent,
97
+ {
98
+ href: item.to,
99
+ to: item.to,
100
+ className: "hover:text-foreground hover:underline",
101
+ children: item.label
102
+ }
103
+ ) : /* @__PURE__ */ jsx(
104
+ "span",
105
+ {
106
+ className: isLast ? "text-foreground" : "",
107
+ "aria-current": isLast ? "page" : void 0,
108
+ children: item.label
109
+ }
110
+ ),
111
+ !isLast && /* @__PURE__ */ jsx(ChevronRight, { className: "size-3", "aria-hidden": "true" })
112
+ ] }, i);
113
+ }) })
114
+ }
115
+ ),
116
+ /* @__PURE__ */ jsxs("div", { className: "ui-page-header-row", children: [
117
+ /* @__PURE__ */ jsxs("div", { className: "ui-page-header-heading min-w-0", children: [
118
+ headerLoading ? /* @__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") }) }) : status != null ? /* @__PURE__ */ jsxs("div", { className: "ui-page-header-title-row", children: [
119
+ /* @__PURE__ */ jsx("h1", { className: "ui-page-title", children: title }),
120
+ /* @__PURE__ */ jsx("div", { className: "ui-page-header-status", children: status })
121
+ ] }) : /* @__PURE__ */ jsx("h1", { className: "ui-page-title", children: title }),
122
+ headerLoading ? (
123
+ // Decorative only — the pending state is already announced once by the heading above,
124
+ // so a second live placeholder here would double-announce it.
125
+ /* @__PURE__ */ jsx(
126
+ "p",
127
+ {
128
+ className: "ui-page-subtitle ui-skeleton-block ui-page-subtitle-placeholder",
129
+ "aria-hidden": "true"
130
+ }
131
+ )
132
+ ) : subtitle && /* @__PURE__ */ jsx("p", { className: "ui-page-subtitle", children: subtitle })
133
+ ] }),
134
+ extra && /* @__PURE__ */ jsx("div", { className: "ui-page-header-extra", children: extra })
135
+ ] })
136
+ ]
91
137
  }
92
138
  ),
93
139
  children != null && /* @__PURE__ */ jsx("div", { className: "ui-page-body", children }),
@@ -5,7 +5,7 @@ export declare const ResizablePanelGroup: {
5
5
  displayName: string;
6
6
  };
7
7
  export declare const ResizablePanel: {
8
- ({ className, ...props }: React.ComponentPropsWithoutRef<typeof ResizablePrimitive.Panel>): React.JSX.Element;
8
+ ({ className, elementRef, ...props }: React.ComponentPropsWithoutRef<typeof ResizablePrimitive.Panel>): React.JSX.Element;
9
9
  displayName: string;
10
10
  };
11
11
  export declare const ResizableHandle: {
@@ -1,7 +1,9 @@
1
1
  "use client";
2
2
  import { jsx } from "react/jsx-runtime";
3
+ import * as React from "react";
3
4
  import * as ResizablePrimitive from "react-resizable-panels";
4
5
  import { cn } from "../../lib/utils.js";
6
+ import { useScrollableRegionTabIndex } from "../../lib/hooks.js";
5
7
  const ResizablePanelGroup = ({
6
8
  className,
7
9
  ...props
@@ -16,15 +18,31 @@ const ResizablePanelGroup = ({
16
18
  ResizablePanelGroup.displayName = "ResizablePanelGroup";
17
19
  const ResizablePanel = ({
18
20
  className,
21
+ elementRef,
19
22
  ...props
20
- }) => /* @__PURE__ */ jsx(
21
- ResizablePrimitive.Panel,
22
- {
23
- "data-slot": "resizable-panel",
24
- className: cn("ui-resizable-panel", className),
25
- ...props
26
- }
27
- );
23
+ }) => {
24
+ const [scrollRegion, setScrollRegion] = React.useState(null);
25
+ const setRootElement = React.useCallback(
26
+ (node) => {
27
+ setScrollRegion(
28
+ node?.firstElementChild instanceof HTMLElement ? node.firstElementChild : null
29
+ );
30
+ if (typeof elementRef === "function") elementRef(node);
31
+ else if (elementRef) elementRef.current = node;
32
+ },
33
+ [elementRef]
34
+ );
35
+ useScrollableRegionTabIndex(scrollRegion);
36
+ return /* @__PURE__ */ jsx(
37
+ ResizablePrimitive.Panel,
38
+ {
39
+ "data-slot": "resizable-panel",
40
+ className: cn("ui-resizable-panel", className),
41
+ elementRef: setRootElement,
42
+ ...props
43
+ }
44
+ );
45
+ };
28
46
  ResizablePanel.displayName = "ResizablePanel";
29
47
  const ResizableHandle = ({
30
48
  className,
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ import type { ServiceRolePanelProp } from "../../props/components/layout.prop.js";
3
+ export type { ServiceRoleItemProp, ServiceRolePanelProp, ServiceRolePanelProp as ServiceRolePanelProps, } from "../../props/components/layout.prop.js";
4
+ /**
5
+ * ServiceRolePanel — the canonical role-collection ⇄ role-detail surface (gh#257 / DXS
6
+ * platform#311).
7
+ *
8
+ * A THIN formalized composition over `MasterDetail` (which owns ALL geometry: rail tracks, gap,
9
+ * stacking below the threshold — 1440/1024 two-track, 390 stacked list-then-detail) plus real
10
+ * primitives, exported because a consumer cannot import a docs page (the gh#251 lesson). What the
11
+ * panel adds is exactly the contract every service-role screen kept re-deriving:
12
+ *
13
+ * - **Selection** as the controlled triad (`value`/`defaultValue`/`onValueChange`, defaulting to
14
+ * the first role). Each role row is a real `Button` carrying `aria-current`, and selecting moves
15
+ * focus-target wiring through `MasterDetail`'s `detailId`/`tabIndex={-1}` detail region.
16
+ * - **Destructive confirmation built in**: `onDeleteRole`'s presence arms a per-role delete
17
+ * affordance behind the package `AlertDialog` (variant `destructive`); the handler fires only
18
+ * after the user confirms. Roles marked `locked` (system roles) never offer deletion and show a
19
+ * localized lock badge; `readOnly` hides every mutating affordance.
20
+ * - **Lifecycle states** mirror the DataTable #216 vocabulary and precedence:
21
+ * `loading` → `denied` → `error` → `empty` → content.
22
+ * - **Domain-neutral**: the roles and the detail surface (`children`, node or render-function) are
23
+ * consumer-supplied; no platform role or permission is encoded.
24
+ */
25
+ export declare const ServiceRolePanel: React.ForwardRefExoticComponent<ServiceRolePanelProp & React.RefAttributes<HTMLDivElement>>;
@@ -0,0 +1,206 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import * as React from "react";
4
+ import { AlertCircle, Lock, RefreshCw, ShieldAlert, Trash2 } from "lucide-react";
5
+ import { useTranslation } from "../../i18n/use-translation.js";
6
+ import { cn } from "../../lib/utils.js";
7
+ import { Badge } from "../data-display/badge.js";
8
+ import { EmptyState } from "../data-display/empty-state.js";
9
+ import { AlertDialog } from "../feedback/dialog.js";
10
+ import { SkeletonRows } from "../feedback/skeleton.js";
11
+ import { Button } from "../general/button.js";
12
+ import { Text } from "../general/typography.js";
13
+ import { MasterDetail } from "./master-detail.js";
14
+ const ServiceRolePanel = React.forwardRef(
15
+ function ServiceRolePanel2({
16
+ roles,
17
+ value: controlledValue,
18
+ defaultValue,
19
+ onValueChange,
20
+ children,
21
+ onDeleteRole,
22
+ readOnly = false,
23
+ loading = false,
24
+ empty,
25
+ error,
26
+ denied,
27
+ onRetry,
28
+ masterLabel,
29
+ detailLabel,
30
+ railWidth,
31
+ masterViewport,
32
+ collapseBelow,
33
+ id,
34
+ className
35
+ }, ref) {
36
+ const { t } = useTranslation();
37
+ const reactId = React.useId();
38
+ const rootId = id ?? `service-role-panel-${reactId}`;
39
+ const detailId = `${rootId}-detail`;
40
+ const [uncontrolled, setUncontrolled] = React.useState(
41
+ defaultValue ?? roles[0]?.id
42
+ );
43
+ const value = controlledValue ?? uncontrolled;
44
+ const selectedRole = roles.find((role) => role.id === value);
45
+ const select = (roleId) => {
46
+ if (controlledValue === void 0) setUncontrolled(roleId);
47
+ onValueChange?.(roleId);
48
+ };
49
+ const [pendingDelete, setPendingDelete] = React.useState(null);
50
+ const isDenied = denied !== void 0 && denied !== null && denied !== false;
51
+ const isError = error !== void 0 && error !== null && error !== false;
52
+ const deletable = typeof onDeleteRole === "function" && !readOnly;
53
+ const surface = (state) => /* @__PURE__ */ jsx(
54
+ "div",
55
+ {
56
+ ref,
57
+ id: rootId,
58
+ "data-slot": "service-role-panel",
59
+ className: cn("ui-service-role-panel", className),
60
+ children: state
61
+ }
62
+ );
63
+ if (loading) {
64
+ return surface(
65
+ /* @__PURE__ */ jsx("div", { "aria-busy": "true", "aria-label": t("layout.serviceRolePanel.loading"), role: "status", children: /* @__PURE__ */ jsx(SkeletonRows, { rows: 5, columns: 3 }) })
66
+ );
67
+ }
68
+ if (isDenied) {
69
+ return surface(
70
+ /* @__PURE__ */ jsx("div", { "aria-live": "polite", children: denied === true ? /* @__PURE__ */ jsx(
71
+ EmptyState,
72
+ {
73
+ icon: ShieldAlert,
74
+ tone: "warning",
75
+ variant: "section",
76
+ title: t("layout.serviceRolePanel.denied"),
77
+ description: t("layout.serviceRolePanel.deniedDescription"),
78
+ titleAs: "p"
79
+ }
80
+ ) : denied })
81
+ );
82
+ }
83
+ if (isError) {
84
+ return surface(
85
+ /* @__PURE__ */ jsx("div", { role: "alert", children: error === true ? /* @__PURE__ */ jsx(
86
+ EmptyState,
87
+ {
88
+ icon: AlertCircle,
89
+ tone: "destructive",
90
+ variant: "section",
91
+ title: t("layout.serviceRolePanel.error"),
92
+ description: t("layout.serviceRolePanel.errorDescription"),
93
+ titleAs: "p",
94
+ action: onRetry ? /* @__PURE__ */ jsxs(Button, { variant: "outline", size: "sm", onClick: onRetry, children: [
95
+ /* @__PURE__ */ jsx(RefreshCw, { "aria-hidden": "true" }),
96
+ t("common.retry")
97
+ ] }) : void 0
98
+ }
99
+ ) : error })
100
+ );
101
+ }
102
+ if (roles.length === 0) {
103
+ return surface(
104
+ /* @__PURE__ */ jsx("div", { "aria-live": "polite", children: empty ?? /* @__PURE__ */ jsx(
105
+ EmptyState,
106
+ {
107
+ variant: "section",
108
+ title: t("layout.serviceRolePanel.empty"),
109
+ description: t("layout.serviceRolePanel.emptyDescription"),
110
+ titleAs: "p"
111
+ }
112
+ ) })
113
+ );
114
+ }
115
+ const detail = typeof children === "function" ? children(selectedRole) : children ?? /* @__PURE__ */ jsx(
116
+ EmptyState,
117
+ {
118
+ variant: "compact",
119
+ title: t("layout.serviceRolePanel.noDetail"),
120
+ titleAs: "p"
121
+ }
122
+ );
123
+ return surface(
124
+ /* @__PURE__ */ jsxs(Fragment, { children: [
125
+ /* @__PURE__ */ jsx(
126
+ MasterDetail,
127
+ {
128
+ rail: "master",
129
+ railWidth,
130
+ masterViewport,
131
+ collapseBelow,
132
+ masterLabel: masterLabel ?? t("layout.serviceRolePanel.rolesLabel"),
133
+ detailLabel: detailLabel ?? t("layout.serviceRolePanel.detailLabel"),
134
+ detailId,
135
+ master: /* @__PURE__ */ jsx("ul", { className: "flex list-none flex-col gap-1", "data-slot": "service-role-panel-list", children: roles.map((role) => {
136
+ const selected = role.id === value;
137
+ return /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1", children: [
138
+ /* @__PURE__ */ jsx(
139
+ Button,
140
+ {
141
+ variant: selected ? "secondary" : "ghost",
142
+ className: "h-auto flex-1 justify-start py-2 text-start",
143
+ "aria-current": selected ? "true" : void 0,
144
+ "aria-controls": detailId,
145
+ onClick: () => select(role.id),
146
+ children: /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-col items-start leading-tight", children: [
147
+ /* @__PURE__ */ jsxs("span", { className: "inline-flex max-w-full items-center gap-1.5", children: [
148
+ /* @__PURE__ */ jsx(Text, { weight: "medium", className: "truncate", children: role.name }),
149
+ role.locked && /* @__PURE__ */ jsxs(Badge, { tone: "default", variant: "outline", children: [
150
+ /* @__PURE__ */ jsx(Lock, { "aria-hidden": "true" }),
151
+ t("layout.serviceRolePanel.locked")
152
+ ] })
153
+ ] }),
154
+ (role.description !== void 0 || role.memberCount !== void 0) && /* @__PURE__ */ jsx(Text, { size: "2xs", tone: "muted", className: "max-w-full truncate", children: [
155
+ role.description,
156
+ role.memberCount !== void 0 ? (
157
+ // `{count}` is Intl.NumberFormat-grouped by `interpolate`,
158
+ // pluralized by CLDR category — no hand-built formatting.
159
+ t("layout.serviceRolePanel.memberCount", {
160
+ count: role.memberCount
161
+ })
162
+ ) : void 0
163
+ ].filter((part) => part !== void 0).join(" \xB7 ") })
164
+ ] })
165
+ }
166
+ ),
167
+ deletable && !role.locked && /* @__PURE__ */ jsx(
168
+ Button,
169
+ {
170
+ variant: "ghost",
171
+ size: "icon-sm",
172
+ "aria-label": t("layout.serviceRolePanel.deleteRole", { role: role.name }),
173
+ onClick: () => setPendingDelete(role),
174
+ children: /* @__PURE__ */ jsx(Trash2, { "aria-hidden": "true" })
175
+ }
176
+ )
177
+ ] }, role.id);
178
+ }) }),
179
+ children: detail
180
+ }
181
+ ),
182
+ deletable && /* @__PURE__ */ jsx(
183
+ AlertDialog,
184
+ {
185
+ open: pendingDelete !== null,
186
+ onOpenChange: (open) => {
187
+ if (!open) setPendingDelete(null);
188
+ },
189
+ title: t("layout.serviceRolePanel.deleteTitle"),
190
+ description: pendingDelete ? t("layout.serviceRolePanel.deleteDescription", { role: pendingDelete.name }) : void 0,
191
+ confirmLabel: t("layout.serviceRolePanel.deleteConfirm"),
192
+ variant: "destructive",
193
+ onConfirm: () => {
194
+ if (pendingDelete) onDeleteRole?.(pendingDelete.id);
195
+ setPendingDelete(null);
196
+ }
197
+ }
198
+ )
199
+ ] })
200
+ );
201
+ }
202
+ );
203
+ ServiceRolePanel.displayName = "ServiceRolePanel";
204
+ export {
205
+ ServiceRolePanel
206
+ };
@@ -1,6 +1,6 @@
1
1
  import type { ToolbarGroupProp, ToolbarProp } from "../../props/components/navigation.prop.js";
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;
2
+ export type { FilterBarChipProp, FilterBarChipProp as FilterBarChipProps, FilterBarFilterProp, FilterBarFilterProp as FilterBarFilterProps, FilterBarOverflowProp, FilterBarSearchProp, FilterBarSearchProp as FilterBarSearchProps, 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, filters, chips, onChipRemove, actions, resultCount, loading, disabled, error, 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.
@@ -4,6 +4,9 @@ import { useId } from "react";
4
4
  import { X } from "lucide-react";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
6
  import { Button } from "../general/button.js";
7
+ import { Badge } from "../data-display/badge.js";
8
+ import { SearchInput } from "../data-entry/search-input.js";
9
+ import { Select } from "../data-entry/select.js";
7
10
  import { cn } from "../../lib/utils.js";
8
11
  function Toolbar({
9
12
  onClear,
@@ -11,65 +14,141 @@ function Toolbar({
11
14
  sticky = false,
12
15
  overflow = "wrap",
13
16
  search,
17
+ filters,
14
18
  chips,
15
19
  onChipRemove,
16
- resultCount,
17
20
  actions,
21
+ resultCount,
22
+ loading,
23
+ disabled,
24
+ error,
18
25
  className,
19
26
  children
20
27
  }) {
21
28
  const { t } = useTranslation();
22
- const hasChips = chips != null && chips.length > 0;
29
+ const reactId = useId();
30
+ const hasModel = search !== void 0 || filters !== void 0 || chips !== void 0 || onChipRemove !== void 0 || actions !== void 0 || resultCount !== void 0 || loading !== void 0 || disabled !== void 0 || error !== void 0;
31
+ if (!hasModel) {
32
+ return /* @__PURE__ */ jsxs(
33
+ "div",
34
+ {
35
+ role: "toolbar",
36
+ "aria-label": t("navigation.toolbar.ariaLabel"),
37
+ "data-overflow": overflow,
38
+ className: cn("ui-toolbar", sticky && "ui-toolbar-sticky", className),
39
+ children: [
40
+ children,
41
+ onClear && hasActiveFilters && /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: onClear, className: "ui-toolbar-clear", children: [
42
+ /* @__PURE__ */ jsx(X, { "aria-hidden": "true" }),
43
+ t("common.clearFilters")
44
+ ] })
45
+ ]
46
+ }
47
+ );
48
+ }
49
+ const showClear = Boolean(onClear && hasActiveFilters);
50
+ const showChips = Boolean(chips && chips.length > 0);
51
+ const showMeta = error != null || resultCount !== void 0;
23
52
  return /* @__PURE__ */ jsxs(
24
53
  "div",
25
54
  {
26
- role: "toolbar",
27
- "aria-label": t("navigation.toolbar.ariaLabel"),
28
- "data-overflow": overflow,
29
- className: cn("ui-toolbar", sticky && "ui-toolbar-sticky", className),
55
+ className: cn("ui-filter-bar", sticky && "ui-toolbar-sticky", className),
56
+ "data-loading": loading || void 0,
30
57
  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 }),
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(
58
+ /* @__PURE__ */ jsxs(
59
+ "div",
60
+ {
61
+ role: "toolbar",
62
+ "aria-label": t("navigation.toolbar.ariaLabel"),
63
+ "aria-busy": loading || void 0,
64
+ "data-overflow": overflow,
65
+ className: "ui-toolbar",
66
+ children: [
67
+ search && /* @__PURE__ */ jsx(
68
+ SearchInput,
69
+ {
70
+ id: search.id,
71
+ className: "ui-filter-bar-search",
72
+ label: search.label,
73
+ ariaLabel: search.ariaLabel,
74
+ placeholder: search.placeholder,
75
+ value: search.value,
76
+ defaultValue: search.defaultValue,
77
+ onValueChange: search.onValueChange,
78
+ onSearch: search.onSearch,
79
+ disabled: disabled || search.disabled
80
+ }
81
+ ),
82
+ filters?.map((filter) => {
83
+ const controlId = `${reactId}-filter-${filter.value}`;
84
+ return /* @__PURE__ */ jsx(
85
+ ToolbarGroup,
86
+ {
87
+ label: filter.label,
88
+ controlId,
89
+ className: "ui-filter-bar-filter",
90
+ children: /* @__PURE__ */ jsx(
91
+ Select,
92
+ {
93
+ id: controlId,
94
+ options: filter.options,
95
+ value: filter.selected,
96
+ defaultValue: filter.defaultSelected,
97
+ onValueChange: (selected) => filter.onSelectedChange?.(selected),
98
+ placeholder: filter.placeholder,
99
+ disabled: disabled || filter.disabled
100
+ }
101
+ )
102
+ },
103
+ filter.value
104
+ );
105
+ }),
106
+ children,
107
+ (showClear || actions != null) && /* @__PURE__ */ jsxs("div", { className: "ui-filter-bar-end", children: [
108
+ showClear && /* @__PURE__ */ jsxs(
109
+ Button,
110
+ {
111
+ variant: "ghost",
112
+ size: "sm",
113
+ onClick: onClear,
114
+ disabled,
115
+ className: "ui-toolbar-clear",
116
+ children: [
117
+ /* @__PURE__ */ jsx(X, { "aria-hidden": "true" }),
118
+ t("common.clearFilters")
119
+ ]
120
+ }
121
+ ),
122
+ actions != null && /* @__PURE__ */ jsx("div", { className: "ui-filter-bar-actions", children: actions })
123
+ ] })
124
+ ]
125
+ }
126
+ ),
127
+ showChips && /* @__PURE__ */ jsx(
39
128
  "div",
40
129
  {
41
130
  role: "group",
42
131
  "aria-label": t("navigation.filterBar.appliedFilters"),
43
132
  className: "ui-filter-bar-chips",
44
133
  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(
134
+ /* @__PURE__ */ jsx(Badge, { variant: "outline", children: chip.label }),
135
+ onChipRemove && /* @__PURE__ */ jsx(
47
136
  Button,
48
137
  {
49
138
  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
- },
139
+ size: "icon-sm",
140
+ disabled: disabled || chip.disabled,
141
+ "aria-label": t("navigation.filterBar.removeFilter", {
142
+ label: typeof chip.label === "string" ? chip.label : chip.value
143
+ }),
144
+ onClick: () => onChipRemove(chip.value),
57
145
  children: /* @__PURE__ */ jsx(X, { "aria-hidden": "true" })
58
146
  }
59
147
  )
60
- ] }, chip.id))
148
+ ] }, chip.value))
61
149
  }
62
150
  ),
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 }) }),
68
- onClear && hasActiveFilters && /* @__PURE__ */ jsxs(Button, { variant: "ghost", size: "sm", onClick: onClear, className: "ui-toolbar-clear", children: [
69
- /* @__PURE__ */ jsx(X, { "aria-hidden": "true" }),
70
- t("common.clearFilters")
71
- ] }),
72
- actions != null && /* @__PURE__ */ jsx("div", { className: "ui-filter-bar-actions", children: actions })
151
+ showMeta && /* @__PURE__ */ jsx("div", { className: "ui-filter-bar-meta", children: error != null ? /* @__PURE__ */ jsx("p", { role: "alert", className: "ui-filter-bar-error", children: error }) : /* @__PURE__ */ jsx("p", { role: "status", className: "ui-filter-bar-count", children: t("navigation.filterBar.resultCount", { count: resultCount }) }) })
73
152
  ]
74
153
  }
75
154
  );
@@ -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 { FilterBarChipProp, FilterBarChipProps, FilterBarGroupProp, FilterBarGroupProps, FilterBarProp, FilterBarProps, ToolbarGroupProps, ToolbarProps, } from "./filter-bar.js";
8
+ export type { FilterBarChipProp, FilterBarChipProps, FilterBarFilterProp, FilterBarFilterProps, FilterBarGroupProp, FilterBarGroupProps, FilterBarOverflowProp, FilterBarProp, FilterBarProps, FilterBarSearchProp, FilterBarSearchProps, 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";
@@ -3,6 +3,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react";
5
5
  import { useTranslation } from "../../i18n/use-translation.js";
6
+ import { useScrollableRegionTabIndex } from "../../lib/hooks.js";
6
7
  import { cn } from "../../lib/utils.js";
7
8
  import { Button } from "../general/button.js";
8
9
  import {
@@ -14,7 +15,25 @@ import {
14
15
  } from "../data-entry/select.js";
15
16
  import { buildPageRange } from "./pagination-utils.js";
16
17
  const PaginationContent = React.forwardRef(({ className, ...props }, ref) => {
17
- return /* @__PURE__ */ jsx("ul", { ref, className: cn("ui-pagination-list", className), role: "list", ...props });
18
+ const [scrollRegion, setScrollRegion] = React.useState(null);
19
+ const setListElement = React.useCallback(
20
+ (node) => {
21
+ setScrollRegion(node);
22
+ if (typeof ref === "function") ref(node);
23
+ else if (ref) ref.current = node;
24
+ },
25
+ [ref]
26
+ );
27
+ useScrollableRegionTabIndex(scrollRegion);
28
+ return /* @__PURE__ */ jsx(
29
+ "ul",
30
+ {
31
+ ref: setListElement,
32
+ className: cn("ui-pagination-list", className),
33
+ role: "list",
34
+ ...props
35
+ }
36
+ );
18
37
  });
19
38
  PaginationContent.displayName = "PaginationContent";
20
39
  const PaginationItem = React.forwardRef(