@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.
- package/LICENSE +202 -0
- package/dist/components/data-display/badge.d.ts +7 -0
- package/dist/components/data-display/badge.js +1 -1
- package/dist/components/data-display/data-table.js +2 -2
- package/dist/components/data-display/index.d.ts +2 -0
- package/dist/components/data-display/index.js +2 -0
- package/dist/components/data-display/permission-matrix.d.ts +26 -0
- package/dist/components/data-display/permission-matrix.js +219 -0
- package/dist/components/data-display/table.d.ts +12 -0
- package/dist/components/data-entry/branch-scope-picker.d.ts +43 -0
- package/dist/components/data-entry/branch-scope-picker.js +200 -0
- package/dist/components/data-entry/index.d.ts +2 -0
- package/dist/components/data-entry/index.js +2 -0
- package/dist/components/data-entry/select.d.ts +7 -1
- package/dist/components/data-entry/select.js +46 -26
- package/dist/components/feedback/alert.d.ts +2 -20
- package/dist/components/feedback/alert.js +1 -11
- package/dist/components/feedback/banner.d.ts +21 -0
- package/dist/components/feedback/banner.js +19 -0
- package/dist/components/feedback/index.d.ts +4 -2
- package/dist/components/feedback/index.js +2 -2
- package/dist/components/general/typography.d.ts +1 -0
- package/dist/components/general/typography.js +32 -13
- package/dist/components/layout/auth-shell.d.ts +2 -2
- package/dist/components/layout/index.d.ts +2 -2
- package/dist/components/layout/index.js +2 -2
- package/dist/components/layout/page-container.d.ts +1 -1
- package/dist/components/layout/page-container.js +61 -15
- package/dist/components/layout/resizable.d.ts +1 -1
- package/dist/components/layout/resizable.js +26 -8
- package/dist/components/layout/service-role-panel.d.ts +25 -0
- package/dist/components/layout/service-role-panel.js +206 -0
- package/dist/components/navigation/filter-bar.d.ts +2 -2
- package/dist/components/navigation/filter-bar.js +113 -34
- package/dist/components/navigation/index.d.ts +1 -1
- package/dist/components/navigation/pagination.js +20 -1
- package/dist/components/navigation/tabs.js +6 -1
- package/dist/i18n/messages/en.json +59 -4
- package/dist/i18n/messages/ja.json +54 -5
- package/dist/i18n/messages/vi.json +53 -4
- package/dist/lib/field-a11y.d.ts +7 -0
- package/dist/lib/field-a11y.js +6 -0
- package/dist/lib/hooks.d.ts +15 -0
- package/dist/lib/hooks.js +37 -0
- package/dist/props/components/data-display.prop.d.ts +66 -0
- package/dist/props/components/data-entry.prop.d.ts +54 -0
- package/dist/props/components/feedback.prop.d.ts +3 -3
- package/dist/props/components/general.prop.d.ts +8 -1
- package/dist/props/components/layout.prop.d.ts +80 -51
- package/dist/props/components/navigation.prop.d.ts +77 -50
- package/dist/props/registry.d.ts +274 -54
- package/dist/props/registry.js +362 -73
- package/dist/props/vocabulary/content.prop.d.ts +5 -0
- package/dist/props/vocabulary/index.d.ts +1 -1
- package/dist/props/vocabulary/interaction.prop.d.ts +4 -7
- package/dist/props/vocabulary/layout.prop.d.ts +2 -2
- package/dist/styles/alert-layout.css +15 -32
- package/dist/styles/badge-layout.css +5 -0
- package/dist/styles/control.css +5 -0
- package/dist/styles/layout.css +118 -108
- package/dist/styles/shell-layout.css +16 -7
- package/dist/styles/table-layout.css +92 -2
- package/dist/styles/text-layout.css +13 -0
- package/dist/tokens/base.css +1 -0
- package/dist/tokens/components/badge.css +5 -0
- package/dist/tokens/components/banner.css +16 -0
- package/dist/tokens/components/navigation.css +10 -25
- package/dist/tokens/components/shell.css +20 -9
- package/dist/tokens/components/table.css +41 -1
- package/dist/tokens/semantic/layout.css +8 -7
- package/package.json +15 -2
- package/dist/components/layout/page-header.d.ts +0 -22
- 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
|
-
|
|
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__ */
|
|
78
|
-
|
|
79
|
+
/* @__PURE__ */ jsxs(
|
|
80
|
+
"header",
|
|
79
81
|
{
|
|
80
82
|
ref: headerRef,
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
}) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
27
|
-
"
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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(
|
|
46
|
-
|
|
134
|
+
/* @__PURE__ */ jsx(Badge, { variant: "outline", children: chip.label }),
|
|
135
|
+
onChipRemove && /* @__PURE__ */ jsx(
|
|
47
136
|
Button,
|
|
48
137
|
{
|
|
49
138
|
variant: "ghost",
|
|
50
|
-
size: "
|
|
51
|
-
|
|
52
|
-
"aria-label": t("navigation.filterBar.removeFilter", {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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.
|
|
148
|
+
] }, chip.value))
|
|
61
149
|
}
|
|
62
150
|
),
|
|
63
|
-
|
|
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
|
-
|
|
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(
|