@dowel-ui/react 0.3.0 → 0.5.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/README.md +13 -8
- package/dist/components/ai-disclosure/ai-disclosure.d.ts +92 -0
- package/dist/components/ai-disclosure/ai-disclosure.d.ts.map +1 -0
- package/dist/components/ai-disclosure/ai-disclosure.js +122 -0
- package/dist/components/ai-disclosure/ai-disclosure.js.map +1 -0
- package/dist/components/ai-disclosure/index.d.ts +2 -0
- package/dist/components/ai-disclosure/index.js +2 -0
- package/dist/components/ai-disclosure/meta.js +17 -0
- package/dist/components/ai-disclosure/meta.js.map +1 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.d.ts +52 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.d.ts.map +1 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.js +249 -0
- package/dist/components/ai-extraction-review/ai-extraction-review.js.map +1 -0
- package/dist/components/ai-extraction-review/extraction-model.d.ts +94 -0
- package/dist/components/ai-extraction-review/extraction-model.d.ts.map +1 -0
- package/dist/components/ai-extraction-review/extraction-model.js +91 -0
- package/dist/components/ai-extraction-review/extraction-model.js.map +1 -0
- package/dist/components/ai-extraction-review/index.d.ts +3 -0
- package/dist/components/ai-extraction-review/index.js +3 -0
- package/dist/components/ai-extraction-review/meta.js +17 -0
- package/dist/components/ai-extraction-review/meta.js.map +1 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.d.ts +72 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.d.ts.map +1 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.js +126 -0
- package/dist/components/ai-suggested-value/ai-suggested-value.js.map +1 -0
- package/dist/components/ai-suggested-value/index.d.ts +2 -0
- package/dist/components/ai-suggested-value/index.js +2 -0
- package/dist/components/ai-suggested-value/meta.js +17 -0
- package/dist/components/ai-suggested-value/meta.js.map +1 -0
- package/dist/components/alert/alert.d.ts +1 -1
- package/dist/components/avatar/avatar.d.ts +1 -1
- package/dist/components/badge/badge.d.ts +1 -1
- package/dist/components/button/button.d.ts +1 -1
- package/dist/components/confirm-typed/confirm-typed.d.ts +47 -0
- package/dist/components/confirm-typed/confirm-typed.d.ts.map +1 -0
- package/dist/components/confirm-typed/confirm-typed.js +106 -0
- package/dist/components/confirm-typed/confirm-typed.js.map +1 -0
- package/dist/components/confirm-typed/index.d.ts +2 -0
- package/dist/components/confirm-typed/index.js +2 -0
- package/dist/components/confirm-typed/meta.js +17 -0
- package/dist/components/confirm-typed/meta.js.map +1 -0
- package/dist/components/cron-editor/cron-editor.d.ts +36 -0
- package/dist/components/cron-editor/cron-editor.d.ts.map +1 -0
- package/dist/components/cron-editor/cron-editor.js +493 -0
- package/dist/components/cron-editor/cron-editor.js.map +1 -0
- package/dist/components/cron-editor/cron-expression.d.ts +82 -0
- package/dist/components/cron-editor/cron-expression.d.ts.map +1 -0
- package/dist/components/cron-editor/cron-expression.js +352 -0
- package/dist/components/cron-editor/cron-expression.js.map +1 -0
- package/dist/components/cron-editor/index.d.ts +3 -0
- package/dist/components/cron-editor/index.js +3 -0
- package/dist/components/cron-editor/meta.js +17 -0
- package/dist/components/cron-editor/meta.js.map +1 -0
- package/dist/components/date-picker/date-picker.js +1 -1
- package/dist/components/diff-viewer/diff-model.d.ts +69 -0
- package/dist/components/diff-viewer/diff-model.d.ts.map +1 -0
- package/dist/components/diff-viewer/diff-model.js +180 -0
- package/dist/components/diff-viewer/diff-model.js.map +1 -0
- package/dist/components/diff-viewer/diff-viewer.d.ts +42 -0
- package/dist/components/diff-viewer/diff-viewer.d.ts.map +1 -0
- package/dist/components/diff-viewer/diff-viewer.js +212 -0
- package/dist/components/diff-viewer/diff-viewer.js.map +1 -0
- package/dist/components/diff-viewer/index.d.ts +3 -0
- package/dist/components/diff-viewer/index.js +3 -0
- package/dist/components/diff-viewer/meta.js +17 -0
- package/dist/components/diff-viewer/meta.js.map +1 -0
- package/dist/components/dns-record/dns-record.d.ts +61 -0
- package/dist/components/dns-record/dns-record.d.ts.map +1 -0
- package/dist/components/dns-record/dns-record.js +227 -0
- package/dist/components/dns-record/dns-record.js.map +1 -0
- package/dist/components/dns-record/index.d.ts +2 -0
- package/dist/components/dns-record/index.js +2 -0
- package/dist/components/dns-record/meta.js +17 -0
- package/dist/components/dns-record/meta.js.map +1 -0
- package/dist/components/log-viewer/index.d.ts +3 -0
- package/dist/components/log-viewer/index.js +3 -0
- package/dist/components/log-viewer/log-stream.d.ts +68 -0
- package/dist/components/log-viewer/log-stream.d.ts.map +1 -0
- package/dist/components/log-viewer/log-stream.js +120 -0
- package/dist/components/log-viewer/log-stream.js.map +1 -0
- package/dist/components/log-viewer/log-viewer.d.ts +51 -0
- package/dist/components/log-viewer/log-viewer.d.ts.map +1 -0
- package/dist/components/log-viewer/log-viewer.js +284 -0
- package/dist/components/log-viewer/log-viewer.js.map +1 -0
- package/dist/components/log-viewer/meta.js +17 -0
- package/dist/components/log-viewer/meta.js.map +1 -0
- package/dist/components/permission-matrix/index.d.ts +3 -0
- package/dist/components/permission-matrix/index.js +3 -0
- package/dist/components/permission-matrix/meta.js +17 -0
- package/dist/components/permission-matrix/meta.js.map +1 -0
- package/dist/components/permission-matrix/permission-matrix.d.ts +46 -0
- package/dist/components/permission-matrix/permission-matrix.d.ts.map +1 -0
- package/dist/components/permission-matrix/permission-matrix.js +199 -0
- package/dist/components/permission-matrix/permission-matrix.js.map +1 -0
- package/dist/components/permission-matrix/permission-model.d.ts +53 -0
- package/dist/components/permission-matrix/permission-model.d.ts.map +1 -0
- package/dist/components/permission-matrix/permission-model.js +64 -0
- package/dist/components/permission-matrix/permission-model.js.map +1 -0
- package/dist/components/progress/progress.d.ts +1 -1
- package/dist/components/secret-field/index.d.ts +2 -0
- package/dist/components/secret-field/index.js +2 -0
- package/dist/components/secret-field/meta.js +17 -0
- package/dist/components/secret-field/meta.js.map +1 -0
- package/dist/components/secret-field/secret-field.d.ts +58 -0
- package/dist/components/secret-field/secret-field.d.ts.map +1 -0
- package/dist/components/secret-field/secret-field.js +168 -0
- package/dist/components/secret-field/secret-field.js.map +1 -0
- package/dist/components/session-expiry/index.d.ts +2 -0
- package/dist/components/session-expiry/index.js +2 -0
- package/dist/components/session-expiry/meta.js +17 -0
- package/dist/components/session-expiry/meta.js.map +1 -0
- package/dist/components/session-expiry/session-expiry.d.ts +64 -0
- package/dist/components/session-expiry/session-expiry.d.ts.map +1 -0
- package/dist/components/session-expiry/session-expiry.js +185 -0
- package/dist/components/session-expiry/session-expiry.js.map +1 -0
- package/dist/components/shortcut-recorder/index.d.ts +3 -0
- package/dist/components/shortcut-recorder/index.js +3 -0
- package/dist/components/shortcut-recorder/meta.js +17 -0
- package/dist/components/shortcut-recorder/meta.js.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-model.d.ts +67 -0
- package/dist/components/shortcut-recorder/shortcut-model.d.ts.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-model.js +168 -0
- package/dist/components/shortcut-recorder/shortcut-model.js.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.d.ts +47 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.d.ts.map +1 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.js +212 -0
- package/dist/components/shortcut-recorder/shortcut-recorder.js.map +1 -0
- package/dist/components/spinner/spinner.d.ts +1 -1
- package/dist/components/sync-status/index.d.ts +2 -0
- package/dist/components/sync-status/index.js +2 -0
- package/dist/components/sync-status/meta.js +17 -0
- package/dist/components/sync-status/meta.js.map +1 -0
- package/dist/components/sync-status/sync-status.d.ts +65 -0
- package/dist/components/sync-status/sync-status.d.ts.map +1 -0
- package/dist/components/sync-status/sync-status.js +103 -0
- package/dist/components/sync-status/sync-status.js.map +1 -0
- package/dist/components/time-range-picker/index.d.ts +3 -0
- package/dist/components/time-range-picker/index.js +3 -0
- package/dist/components/time-range-picker/meta.js +22 -0
- package/dist/components/time-range-picker/meta.js.map +1 -0
- package/dist/components/time-range-picker/time-expression.d.ts +88 -0
- package/dist/components/time-range-picker/time-expression.d.ts.map +1 -0
- package/dist/components/time-range-picker/time-expression.js +335 -0
- package/dist/components/time-range-picker/time-expression.js.map +1 -0
- package/dist/components/time-range-picker/time-range-picker.d.ts +65 -0
- package/dist/components/time-range-picker/time-range-picker.d.ts.map +1 -0
- package/dist/components/time-range-picker/time-range-picker.js +377 -0
- package/dist/components/time-range-picker/time-range-picker.js.map +1 -0
- package/dist/components/toast/toast.d.ts +1 -1
- package/dist/index.d.ts +36 -1
- package/dist/index.js +24 -3
- package/dist/registry/components.d.ts.map +1 -1
- package/dist/registry/components.js +78 -50
- package/dist/registry/components.js.map +1 -1
- package/package.json +6 -4
- package/src/components/ai-disclosure/ai-disclosure.tsx +244 -0
- package/src/components/ai-disclosure/index.ts +9 -0
- package/src/components/ai-disclosure/meta.ts +23 -0
- package/src/components/ai-extraction-review/ai-extraction-review.tsx +492 -0
- package/src/components/ai-extraction-review/extraction-model.ts +159 -0
- package/src/components/ai-extraction-review/index.ts +13 -0
- package/src/components/ai-extraction-review/meta.ts +24 -0
- package/src/components/ai-suggested-value/ai-suggested-value.tsx +295 -0
- package/src/components/ai-suggested-value/index.ts +6 -0
- package/src/components/ai-suggested-value/meta.ts +24 -0
- package/src/components/confirm-typed/confirm-typed.tsx +191 -0
- package/src/components/confirm-typed/index.ts +1 -0
- package/src/components/confirm-typed/meta.ts +22 -0
- package/src/components/cron-editor/cron-editor.tsx +676 -0
- package/src/components/cron-editor/cron-expression.ts +552 -0
- package/src/components/cron-editor/index.ts +23 -0
- package/src/components/cron-editor/meta.ts +23 -0
- package/src/components/diff-viewer/diff-model.ts +256 -0
- package/src/components/diff-viewer/diff-viewer.tsx +339 -0
- package/src/components/diff-viewer/index.ts +17 -0
- package/src/components/diff-viewer/meta.ts +20 -0
- package/src/components/dns-record/dns-record.tsx +315 -0
- package/src/components/dns-record/index.ts +7 -0
- package/src/components/dns-record/meta.ts +21 -0
- package/src/components/log-viewer/index.ts +21 -0
- package/src/components/log-viewer/log-stream.ts +176 -0
- package/src/components/log-viewer/log-viewer.tsx +424 -0
- package/src/components/log-viewer/meta.ts +21 -0
- package/src/components/permission-matrix/index.ts +14 -0
- package/src/components/permission-matrix/meta.ts +23 -0
- package/src/components/permission-matrix/permission-matrix.tsx +368 -0
- package/src/components/permission-matrix/permission-model.ts +120 -0
- package/src/components/secret-field/index.ts +6 -0
- package/src/components/secret-field/meta.ts +21 -0
- package/src/components/secret-field/secret-field.tsx +284 -0
- package/src/components/session-expiry/index.ts +9 -0
- package/src/components/session-expiry/meta.ts +23 -0
- package/src/components/session-expiry/session-expiry.tsx +321 -0
- package/src/components/shortcut-recorder/index.ts +19 -0
- package/src/components/shortcut-recorder/meta.ts +22 -0
- package/src/components/shortcut-recorder/shortcut-model.ts +204 -0
- package/src/components/shortcut-recorder/shortcut-recorder.tsx +336 -0
- package/src/components/sync-status/index.ts +9 -0
- package/src/components/sync-status/meta.ts +21 -0
- package/src/components/sync-status/sync-status.tsx +217 -0
- package/src/components/time-range-picker/index.ts +25 -0
- package/src/components/time-range-picker/meta.ts +22 -0
- package/src/components/time-range-picker/time-expression.ts +491 -0
- package/src/components/time-range-picker/time-range-picker.tsx +522 -0
- package/src/index.ts +14 -0
- package/src/registry/components.ts +28 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { disabledStyles, focusRing } from "../../lib/styles.js";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import { LOG_LEVELS, segment } from "./log-stream.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
7
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
8
|
+
//#region src/components/log-viewer/log-viewer.tsx
|
|
9
|
+
/**
|
|
10
|
+
* A streaming console: filter, follow, expand.
|
|
11
|
+
*
|
|
12
|
+
* The incumbent is react-lazylog, which was last published in 2022, is built on
|
|
13
|
+
* react-virtualized, and cannot run on React 19 — while still taking about
|
|
14
|
+
* 15,000 downloads a week. This is for those people.
|
|
15
|
+
*
|
|
16
|
+
* Two accessibility decisions worth stating rather than discovering.
|
|
17
|
+
*
|
|
18
|
+
* `role="log"` implies `aria-live="polite"`, which is right for a handful of
|
|
19
|
+
* events and catastrophic for a console: a screen reader would read every line
|
|
20
|
+
* of a build as it scrolls past, and nothing else would be audible. Announcing
|
|
21
|
+
* is therefore off by default and opt-in through `announce`. The role stays,
|
|
22
|
+
* because it still describes what the region is.
|
|
23
|
+
*
|
|
24
|
+
* And virtualization means most rows are not in the DOM. That is a real trade,
|
|
25
|
+
* not an implementation detail: assistive technology cannot reach what is not
|
|
26
|
+
* rendered, so a reader who needs the whole log needs an escape — which is what
|
|
27
|
+
* `onDownload` is for. A log viewer with no way out of the virtual window is
|
|
28
|
+
* not accessible however good its ARIA is.
|
|
29
|
+
*/
|
|
30
|
+
const LEVEL_STYLES = {
|
|
31
|
+
trace: "text-muted-foreground",
|
|
32
|
+
debug: "text-muted-foreground",
|
|
33
|
+
info: "text-foreground",
|
|
34
|
+
warn: "text-warning",
|
|
35
|
+
error: "text-destructive",
|
|
36
|
+
fatal: "text-destructive font-semibold"
|
|
37
|
+
};
|
|
38
|
+
/** Distance from the bottom, in pixels, that still counts as "at the end". */
|
|
39
|
+
const FOLLOW_THRESHOLD = 24;
|
|
40
|
+
function LogViewer({ className, lines, label, rowHeight = 22, height = 360, announce = false, onDownload, children, ...props }) {
|
|
41
|
+
const scrollRef = useRef(null);
|
|
42
|
+
const [following, setFollowing] = useState(true);
|
|
43
|
+
const [expanded, setExpanded] = useState(/* @__PURE__ */ new Set());
|
|
44
|
+
const labelId = useId();
|
|
45
|
+
const virtualizer = useVirtualizer({
|
|
46
|
+
count: lines.length,
|
|
47
|
+
getScrollElement: () => scrollRef.current,
|
|
48
|
+
estimateSize: () => rowHeight,
|
|
49
|
+
overscan: 12,
|
|
50
|
+
initialRect: {
|
|
51
|
+
width: 0,
|
|
52
|
+
height: typeof height === "number" ? height : 360
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
useEffect(() => {
|
|
56
|
+
if (!following || lines.length === 0) return;
|
|
57
|
+
virtualizer.scrollToIndex(lines.length - 1, { align: "end" });
|
|
58
|
+
}, [
|
|
59
|
+
following,
|
|
60
|
+
lines.length,
|
|
61
|
+
virtualizer
|
|
62
|
+
]);
|
|
63
|
+
function handleScroll() {
|
|
64
|
+
const element = scrollRef.current;
|
|
65
|
+
if (!element) return;
|
|
66
|
+
const distance = element.scrollHeight - element.scrollTop - element.clientHeight;
|
|
67
|
+
setFollowing(distance <= FOLLOW_THRESHOLD);
|
|
68
|
+
}
|
|
69
|
+
function toggleExpanded(id) {
|
|
70
|
+
setExpanded((current) => {
|
|
71
|
+
const next = new Set(current);
|
|
72
|
+
if (next.has(id)) next.delete(id);
|
|
73
|
+
else next.add(id);
|
|
74
|
+
return next;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const items = virtualizer.getVirtualItems();
|
|
78
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
79
|
+
"data-slot": "log-viewer",
|
|
80
|
+
className: cn("flex flex-col gap-2", className),
|
|
81
|
+
...props,
|
|
82
|
+
children: [
|
|
83
|
+
/* @__PURE__ */ jsxs("div", {
|
|
84
|
+
className: "flex flex-wrap items-center justify-between gap-2",
|
|
85
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
86
|
+
id: labelId,
|
|
87
|
+
className: "text-sm font-medium",
|
|
88
|
+
children: label
|
|
89
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
90
|
+
className: "flex items-center gap-2",
|
|
91
|
+
children: [!following ? /* @__PURE__ */ jsx("button", {
|
|
92
|
+
type: "button",
|
|
93
|
+
"data-slot": "log-viewer-jump",
|
|
94
|
+
onClick: () => {
|
|
95
|
+
setFollowing(true);
|
|
96
|
+
},
|
|
97
|
+
className: cn("rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium", "transition-colors hover:bg-accent hover:text-accent-foreground", focusRing),
|
|
98
|
+
children: "Jump to latest"
|
|
99
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
100
|
+
className: "text-xs text-muted-foreground",
|
|
101
|
+
children: "Following"
|
|
102
|
+
}), onDownload ? /* @__PURE__ */ jsx("button", {
|
|
103
|
+
type: "button",
|
|
104
|
+
"data-slot": "log-viewer-download",
|
|
105
|
+
onClick: onDownload,
|
|
106
|
+
className: cn("rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium", "transition-colors hover:bg-accent hover:text-accent-foreground", focusRing),
|
|
107
|
+
children: "Download full log"
|
|
108
|
+
}) : null]
|
|
109
|
+
})]
|
|
110
|
+
}),
|
|
111
|
+
children,
|
|
112
|
+
/* @__PURE__ */ jsx("div", {
|
|
113
|
+
ref: scrollRef,
|
|
114
|
+
onScroll: handleScroll,
|
|
115
|
+
role: "log",
|
|
116
|
+
"aria-labelledby": labelId,
|
|
117
|
+
"aria-live": announce ? "polite" : "off",
|
|
118
|
+
tabIndex: 0,
|
|
119
|
+
style: { height },
|
|
120
|
+
className: cn("overflow-auto rounded-lg border border-border bg-muted/30 font-mono text-xs", focusRing),
|
|
121
|
+
children: lines.length === 0 ? /* @__PURE__ */ jsx("p", {
|
|
122
|
+
className: "p-3 text-muted-foreground",
|
|
123
|
+
children: "No lines match the current filter."
|
|
124
|
+
}) : /* @__PURE__ */ jsx("div", {
|
|
125
|
+
style: {
|
|
126
|
+
height: virtualizer.getTotalSize(),
|
|
127
|
+
position: "relative"
|
|
128
|
+
},
|
|
129
|
+
children: items.map((item) => {
|
|
130
|
+
const line = lines[item.index];
|
|
131
|
+
if (!line) return null;
|
|
132
|
+
return /* @__PURE__ */ jsx("div", {
|
|
133
|
+
ref: virtualizer.measureElement,
|
|
134
|
+
"data-index": item.index,
|
|
135
|
+
"data-slot": "log-viewer-row",
|
|
136
|
+
"data-level": line.level,
|
|
137
|
+
className: "absolute top-0 left-0 w-full",
|
|
138
|
+
style: { transform: `translateY(${String(item.start)}px)` },
|
|
139
|
+
children: /* @__PURE__ */ jsx(LogViewerRow, {
|
|
140
|
+
line,
|
|
141
|
+
expanded: expanded.has(line.id),
|
|
142
|
+
onToggle: () => {
|
|
143
|
+
toggleExpanded(line.id);
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}, line.id);
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
})
|
|
150
|
+
]
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* One line.
|
|
155
|
+
*
|
|
156
|
+
* Exported because the virtualized container cannot be measured outside a real
|
|
157
|
+
* browser — jsdom reports every box as zero — so the row's behaviour would
|
|
158
|
+
* otherwise be untestable. It is also genuinely useful on its own for anyone
|
|
159
|
+
* rendering a short log without virtualization.
|
|
160
|
+
*/
|
|
161
|
+
function LogViewerRow({ line, expanded = false, onToggle }) {
|
|
162
|
+
const hasFields = line.fields !== void 0 && Object.keys(line.fields).length > 0;
|
|
163
|
+
const parts = segment(line.message, line.matches);
|
|
164
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
165
|
+
className: "px-3 py-0.5 hover:bg-accent/40",
|
|
166
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
167
|
+
className: "flex items-baseline gap-2",
|
|
168
|
+
children: [
|
|
169
|
+
line.timestamp ? /* @__PURE__ */ jsx("time", {
|
|
170
|
+
className: "shrink-0 text-muted-foreground tabular-nums",
|
|
171
|
+
children: line.timestamp
|
|
172
|
+
}) : null,
|
|
173
|
+
line.level ? /* @__PURE__ */ jsx("span", {
|
|
174
|
+
className: cn("w-11 shrink-0 uppercase", LEVEL_STYLES[line.level]),
|
|
175
|
+
children: line.level
|
|
176
|
+
}) : null,
|
|
177
|
+
/* @__PURE__ */ jsx("span", {
|
|
178
|
+
className: cn("min-w-0 flex-1 break-all whitespace-pre-wrap", line.level && LEVEL_STYLES[line.level]),
|
|
179
|
+
children: parts.map((part, index) => part.match ? /* @__PURE__ */ jsx("mark", {
|
|
180
|
+
className: "rounded-[2px] bg-warning/35 text-inherit",
|
|
181
|
+
children: part.text
|
|
182
|
+
}, index) : /* @__PURE__ */ jsx("span", { children: part.text }, index))
|
|
183
|
+
}),
|
|
184
|
+
hasFields ? /* @__PURE__ */ jsx("button", {
|
|
185
|
+
type: "button",
|
|
186
|
+
"data-slot": "log-viewer-expand",
|
|
187
|
+
"aria-expanded": expanded,
|
|
188
|
+
onClick: onToggle,
|
|
189
|
+
className: cn("shrink-0 rounded px-1 text-2xs text-muted-foreground", "transition-colors hover:text-foreground", focusRing, disabledStyles),
|
|
190
|
+
children: expanded ? "Hide fields" : "Fields"
|
|
191
|
+
}) : null
|
|
192
|
+
]
|
|
193
|
+
}), expanded && line.fields ? /* @__PURE__ */ jsx("dl", {
|
|
194
|
+
"data-slot": "log-viewer-fields",
|
|
195
|
+
className: "mt-1 mb-1 ml-4 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 border-l border-border pl-3",
|
|
196
|
+
children: Object.entries(line.fields).map(([key, value]) => /* @__PURE__ */ jsxs("div", {
|
|
197
|
+
className: "contents",
|
|
198
|
+
children: [/* @__PURE__ */ jsx("dt", {
|
|
199
|
+
className: "text-muted-foreground",
|
|
200
|
+
children: key
|
|
201
|
+
}), /* @__PURE__ */ jsx("dd", {
|
|
202
|
+
className: "m-0 break-all",
|
|
203
|
+
children: typeof value === "string" ? value : JSON.stringify(value)
|
|
204
|
+
})]
|
|
205
|
+
}, key))
|
|
206
|
+
}) : null]
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function LogViewerToolbar({ className, query, onQueryChange, regex, onRegexChange, levels, onToggleLevel, counts, invalidPattern = false, showing, total, ...props }) {
|
|
210
|
+
const filterId = useId();
|
|
211
|
+
const errorId = useId();
|
|
212
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
213
|
+
"data-slot": "log-viewer-toolbar",
|
|
214
|
+
className: cn("flex flex-wrap items-center gap-2", className),
|
|
215
|
+
...props,
|
|
216
|
+
children: [
|
|
217
|
+
/* @__PURE__ */ jsxs("div", {
|
|
218
|
+
className: "flex min-w-0 flex-1 items-center gap-2",
|
|
219
|
+
children: [
|
|
220
|
+
/* @__PURE__ */ jsx("label", {
|
|
221
|
+
htmlFor: filterId,
|
|
222
|
+
className: "sr-only",
|
|
223
|
+
children: "Filter log"
|
|
224
|
+
}),
|
|
225
|
+
/* @__PURE__ */ jsx("input", {
|
|
226
|
+
id: filterId,
|
|
227
|
+
type: "search",
|
|
228
|
+
value: query,
|
|
229
|
+
placeholder: regex ? "Pattern…" : "Filter…",
|
|
230
|
+
"aria-invalid": invalidPattern || void 0,
|
|
231
|
+
"aria-describedby": invalidPattern ? errorId : void 0,
|
|
232
|
+
onChange: (event) => {
|
|
233
|
+
onQueryChange(event.target.value);
|
|
234
|
+
},
|
|
235
|
+
className: cn("min-w-32 flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm", invalidPattern && "border-destructive", focusRing)
|
|
236
|
+
}),
|
|
237
|
+
/* @__PURE__ */ jsxs("label", {
|
|
238
|
+
className: "flex shrink-0 items-center gap-1 text-xs text-muted-foreground",
|
|
239
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
240
|
+
type: "checkbox",
|
|
241
|
+
checked: regex,
|
|
242
|
+
onChange: (event) => {
|
|
243
|
+
onRegexChange(event.target.checked);
|
|
244
|
+
},
|
|
245
|
+
className: cn("size-3.5 rounded border-input", focusRing)
|
|
246
|
+
}), "Regex"]
|
|
247
|
+
})
|
|
248
|
+
]
|
|
249
|
+
}),
|
|
250
|
+
/* @__PURE__ */ jsx("div", {
|
|
251
|
+
className: "flex flex-wrap items-center gap-1",
|
|
252
|
+
children: LOG_LEVELS.filter((level) => counts.has(level)).map((level) => {
|
|
253
|
+
const on = levels.size === 0 || levels.has(level);
|
|
254
|
+
return /* @__PURE__ */ jsxs("button", {
|
|
255
|
+
type: "button",
|
|
256
|
+
"data-slot": "log-viewer-facet",
|
|
257
|
+
"aria-pressed": on,
|
|
258
|
+
onClick: () => {
|
|
259
|
+
onToggleLevel(level);
|
|
260
|
+
},
|
|
261
|
+
className: cn("rounded-md border px-1.5 py-0.5 font-mono text-2xs uppercase transition-colors", on ? "border-transparent bg-secondary text-secondary-foreground" : "border-input bg-background text-muted-foreground line-through", focusRing),
|
|
262
|
+
children: [
|
|
263
|
+
level,
|
|
264
|
+
" ",
|
|
265
|
+
counts.get(level) ?? 0
|
|
266
|
+
]
|
|
267
|
+
}, level);
|
|
268
|
+
})
|
|
269
|
+
}),
|
|
270
|
+
/* @__PURE__ */ jsx("p", {
|
|
271
|
+
className: "w-full text-xs text-muted-foreground tabular-nums",
|
|
272
|
+
children: invalidPattern ? /* @__PURE__ */ jsx("span", {
|
|
273
|
+
id: errorId,
|
|
274
|
+
className: "text-destructive",
|
|
275
|
+
children: "Incomplete pattern — showing everything until it is valid"
|
|
276
|
+
}) : `${String(showing)} of ${String(total)} lines`
|
|
277
|
+
})
|
|
278
|
+
]
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
//#endregion
|
|
282
|
+
export { LogViewer, LogViewerRow, LogViewerToolbar };
|
|
283
|
+
|
|
284
|
+
//# sourceMappingURL=log-viewer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-viewer.js","names":[],"sources":["../../../src/components/log-viewer/log-viewer.tsx"],"sourcesContent":["\"use client\";\n\nimport { useVirtualizer } from \"@tanstack/react-virtual\";\nimport {\n useEffect,\n useId,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type ReactNode,\n} from \"react\";\n\nimport { disabledStyles, focusRing } from \"@/lib/styles\";\nimport { cn } from \"@/lib/utils\";\n\nimport { LOG_LEVELS, segment, type LogLevel, type VisibleLine } from \"./log-stream\";\n\n/**\n * A streaming console: filter, follow, expand.\n *\n * The incumbent is react-lazylog, which was last published in 2022, is built on\n * react-virtualized, and cannot run on React 19 — while still taking about\n * 15,000 downloads a week. This is for those people.\n *\n * Two accessibility decisions worth stating rather than discovering.\n *\n * `role=\"log\"` implies `aria-live=\"polite\"`, which is right for a handful of\n * events and catastrophic for a console: a screen reader would read every line\n * of a build as it scrolls past, and nothing else would be audible. Announcing\n * is therefore off by default and opt-in through `announce`. The role stays,\n * because it still describes what the region is.\n *\n * And virtualization means most rows are not in the DOM. That is a real trade,\n * not an implementation detail: assistive technology cannot reach what is not\n * rendered, so a reader who needs the whole log needs an escape — which is what\n * `onDownload` is for. A log viewer with no way out of the virtual window is\n * not accessible however good its ARIA is.\n */\n\nconst LEVEL_STYLES: Record<LogLevel, string> = {\n trace: \"text-muted-foreground\",\n debug: \"text-muted-foreground\",\n info: \"text-foreground\",\n warn: \"text-warning\",\n error: \"text-destructive\",\n fatal: \"text-destructive font-semibold\",\n};\n\n/** Distance from the bottom, in pixels, that still counts as \"at the end\". */\nconst FOLLOW_THRESHOLD = 24;\n\nexport interface LogViewerProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n lines: VisibleLine[];\n /** Names the region. */\n label: string;\n /** Row height in pixels. Rows are one line; expansion is measured. */\n rowHeight?: number;\n height?: number | string;\n /**\n * Read new lines aloud. Off by default — a console that announces every line\n * makes a screen reader useless for anything else.\n */\n announce?: boolean;\n /** The way out of the virtual window, for anyone who needs the whole log. */\n onDownload?: () => void;\n children?: ReactNode;\n}\n\nexport function LogViewer({\n className,\n lines,\n label,\n rowHeight = 22,\n height = 360,\n announce = false,\n onDownload,\n children,\n ...props\n}: LogViewerProps) {\n const scrollRef = useRef<HTMLDivElement | null>(null);\n const [following, setFollowing] = useState(true);\n const [expanded, setExpanded] = useState<Set<string>>(new Set());\n const labelId = useId();\n\n const virtualizer = useVirtualizer({\n count: lines.length,\n getScrollElement: () => scrollRef.current,\n estimateSize: () => rowHeight,\n overscan: 12,\n // Without a starting rect the virtualizer renders nothing until a\n // ResizeObserver fires, so the first paint is an empty box. Seeding it from\n // the declared height means rows are there immediately, and it is measured\n // properly a moment later.\n initialRect: { width: 0, height: typeof height === \"number\" ? height : 360 },\n });\n\n // Follow mode, hand-written rather than delegated. A spring-anchoring library\n // wants to own the scroll container and so does the virtualizer, and the two\n // fight; this is scrollToIndex on append plus a proximity check.\n useEffect(() => {\n if (!following || lines.length === 0) return;\n virtualizer.scrollToIndex(lines.length - 1, { align: \"end\" });\n }, [following, lines.length, virtualizer]);\n\n function handleScroll() {\n const element = scrollRef.current;\n if (!element) return;\n\n const distance = element.scrollHeight - element.scrollTop - element.clientHeight;\n // Scrolling up detaches, because the reader is looking at something and\n // yanking them back to the tail would lose it. Returning to the bottom\n // re-attaches, which is the gesture people already expect.\n setFollowing(distance <= FOLLOW_THRESHOLD);\n }\n\n function toggleExpanded(id: string) {\n setExpanded((current) => {\n const next = new Set(current);\n if (next.has(id)) next.delete(id);\n else next.add(id);\n return next;\n });\n }\n\n const items = virtualizer.getVirtualItems();\n\n return (\n <div data-slot=\"log-viewer\" className={cn(\"flex flex-col gap-2\", className)} {...props}>\n <div className=\"flex flex-wrap items-center justify-between gap-2\">\n <span id={labelId} className=\"text-sm font-medium\">\n {label}\n </span>\n <div className=\"flex items-center gap-2\">\n {!following ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-jump\"\n onClick={() => {\n setFollowing(true);\n }}\n className={cn(\n \"rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium\",\n \"transition-colors hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n )}\n >\n Jump to latest\n </button>\n ) : (\n <span className=\"text-xs text-muted-foreground\">Following</span>\n )}\n {onDownload ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-download\"\n onClick={onDownload}\n className={cn(\n \"rounded-md border border-input bg-background px-2 py-0.5 text-xs font-medium\",\n \"transition-colors hover:bg-accent hover:text-accent-foreground\",\n focusRing,\n )}\n >\n Download full log\n </button>\n ) : null}\n </div>\n </div>\n\n {children}\n\n <div\n ref={scrollRef}\n onScroll={handleScroll}\n role=\"log\"\n aria-labelledby={labelId}\n // Explicitly off unless asked for: role=\"log\" implies polite, and a\n // console that reads every line aloud drowns out everything else.\n aria-live={announce ? \"polite\" : \"off\"}\n // A scrollable region must be focusable, or its content is unreachable\n // without a pointer — WCAG 2.1.1, and axe's scrollable-region-focusable\n // requires exactly this. jsx-a11y's heuristic cannot see that this\n // element scrolls, so here the requirement outranks the rule.\n // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex\n tabIndex={0}\n style={{ height }}\n className={cn(\n \"overflow-auto rounded-lg border border-border bg-muted/30 font-mono text-xs\",\n focusRing,\n )}\n >\n {lines.length === 0 ? (\n <p className=\"p-3 text-muted-foreground\">No lines match the current filter.</p>\n ) : (\n <div style={{ height: virtualizer.getTotalSize(), position: \"relative\" }}>\n {items.map((item) => {\n const line = lines[item.index];\n if (!line) return null;\n\n return (\n <div\n key={line.id}\n ref={virtualizer.measureElement}\n data-index={item.index}\n data-slot=\"log-viewer-row\"\n data-level={line.level}\n className=\"absolute top-0 left-0 w-full\"\n style={{ transform: `translateY(${String(item.start)}px)` }}\n >\n <LogViewerRow\n line={line}\n expanded={expanded.has(line.id)}\n onToggle={() => {\n toggleExpanded(line.id);\n }}\n />\n </div>\n );\n })}\n </div>\n )}\n </div>\n </div>\n );\n}\n\nexport interface LogViewerRowProps {\n line: VisibleLine;\n expanded?: boolean;\n onToggle?: () => void;\n}\n\n/**\n * One line.\n *\n * Exported because the virtualized container cannot be measured outside a real\n * browser — jsdom reports every box as zero — so the row's behaviour would\n * otherwise be untestable. It is also genuinely useful on its own for anyone\n * rendering a short log without virtualization.\n */\nexport function LogViewerRow({ line, expanded = false, onToggle }: LogViewerRowProps) {\n const hasFields = line.fields !== undefined && Object.keys(line.fields).length > 0;\n const parts = segment(line.message, line.matches);\n\n return (\n <div className=\"px-3 py-0.5 hover:bg-accent/40\">\n <div className=\"flex items-baseline gap-2\">\n {line.timestamp ? (\n <time className=\"shrink-0 text-muted-foreground tabular-nums\">{line.timestamp}</time>\n ) : null}\n\n {line.level ? (\n // The level in text, not a coloured bar. Colour alone cannot\n // distinguish warn from error for everyone reading.\n <span className={cn(\"w-11 shrink-0 uppercase\", LEVEL_STYLES[line.level])}>\n {line.level}\n </span>\n ) : null}\n\n <span\n className={cn(\n \"min-w-0 flex-1 break-all whitespace-pre-wrap\",\n line.level && LEVEL_STYLES[line.level],\n )}\n >\n {parts.map((part, index) =>\n part.match ? (\n // A mark element, so the match is conveyed structurally rather\n // than only as a background colour.\n <mark key={index} className=\"rounded-[2px] bg-warning/35 text-inherit\">\n {part.text}\n </mark>\n ) : (\n <span key={index}>{part.text}</span>\n ),\n )}\n </span>\n\n {hasFields ? (\n <button\n type=\"button\"\n data-slot=\"log-viewer-expand\"\n aria-expanded={expanded}\n onClick={onToggle}\n className={cn(\n \"shrink-0 rounded px-1 text-2xs text-muted-foreground\",\n \"transition-colors hover:text-foreground\",\n focusRing,\n disabledStyles,\n )}\n >\n {expanded ? \"Hide fields\" : \"Fields\"}\n </button>\n ) : null}\n </div>\n\n {expanded && line.fields ? (\n <dl\n data-slot=\"log-viewer-fields\"\n className=\"mt-1 mb-1 ml-4 grid grid-cols-[auto_1fr] gap-x-3 gap-y-0.5 border-l border-border pl-3\"\n >\n {Object.entries(line.fields).map(([key, value]) => (\n <div key={key} className=\"contents\">\n <dt className=\"text-muted-foreground\">{key}</dt>\n <dd className=\"m-0 break-all\">\n {typeof value === \"string\" ? value : JSON.stringify(value)}\n </dd>\n </div>\n ))}\n </dl>\n ) : null}\n </div>\n );\n}\n\nexport interface LogViewerToolbarProps extends Omit<ComponentPropsWithRef<\"div\">, \"onChange\"> {\n query: string;\n onQueryChange: (query: string) => void;\n regex: boolean;\n onRegexChange: (regex: boolean) => void;\n levels: Set<LogLevel>;\n onToggleLevel: (level: LogLevel) => void;\n counts: Map<LogLevel, number>;\n invalidPattern?: boolean;\n /** Shown as \"N of M lines\". */\n showing: number;\n total: number;\n}\n\nexport function LogViewerToolbar({\n className,\n query,\n onQueryChange,\n regex,\n onRegexChange,\n levels,\n onToggleLevel,\n counts,\n invalidPattern = false,\n showing,\n total,\n ...props\n}: LogViewerToolbarProps) {\n const filterId = useId();\n const errorId = useId();\n\n return (\n <div\n data-slot=\"log-viewer-toolbar\"\n className={cn(\"flex flex-wrap items-center gap-2\", className)}\n {...props}\n >\n <div className=\"flex min-w-0 flex-1 items-center gap-2\">\n <label htmlFor={filterId} className=\"sr-only\">\n Filter log\n </label>\n <input\n id={filterId}\n type=\"search\"\n value={query}\n placeholder={regex ? \"Pattern…\" : \"Filter…\"}\n aria-invalid={invalidPattern || undefined}\n aria-describedby={invalidPattern ? errorId : undefined}\n onChange={(event) => {\n onQueryChange(event.target.value);\n }}\n className={cn(\n \"min-w-32 flex-1 rounded-md border border-input bg-background px-2 py-1 text-sm\",\n invalidPattern && \"border-destructive\",\n focusRing,\n )}\n />\n <label className=\"flex shrink-0 items-center gap-1 text-xs text-muted-foreground\">\n <input\n type=\"checkbox\"\n checked={regex}\n onChange={(event) => {\n onRegexChange(event.target.checked);\n }}\n className={cn(\"size-3.5 rounded border-input\", focusRing)}\n />\n Regex\n </label>\n </div>\n\n <div className=\"flex flex-wrap items-center gap-1\">\n {LOG_LEVELS.filter((level) => counts.has(level)).map((level) => {\n const on = levels.size === 0 || levels.has(level);\n return (\n <button\n key={level}\n type=\"button\"\n data-slot=\"log-viewer-facet\"\n aria-pressed={on}\n onClick={() => {\n onToggleLevel(level);\n }}\n className={cn(\n \"rounded-md border px-1.5 py-0.5 font-mono text-2xs uppercase transition-colors\",\n on\n ? \"border-transparent bg-secondary text-secondary-foreground\"\n : \"border-input bg-background text-muted-foreground line-through\",\n focusRing,\n )}\n >\n {level} {counts.get(level) ?? 0}\n </button>\n );\n })}\n </div>\n\n <p className=\"w-full text-xs text-muted-foreground tabular-nums\">\n {invalidPattern ? (\n // Said out loud rather than shown as an empty log, which reads as\n // \"nothing matched\" and sends the reader looking for the wrong thing.\n <span id={errorId} className=\"text-destructive\">\n Incomplete pattern — showing everything until it is valid\n </span>\n ) : (\n `${String(showing)} of ${String(total)} lines`\n )}\n </p>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMeta } from "../../registry/schema.js";
|
|
2
|
+
//#region src/components/log-viewer/meta.ts
|
|
3
|
+
const meta = defineMeta({
|
|
4
|
+
name: "log-viewer",
|
|
5
|
+
title: "Log Viewer",
|
|
6
|
+
description: "A streaming console with filtering, level facets and follow mode.",
|
|
7
|
+
category: "data",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: ["@tanstack/react-virtual"],
|
|
10
|
+
registryDependencies: [],
|
|
11
|
+
files: ["log-stream.ts", "log-viewer.tsx"],
|
|
12
|
+
a11y: "role=\"log\" implies aria-live=\"polite\", which is right for a few events and unusable for a console — a screen reader would read every line of a build and nothing else would be audible. Announcing is off by default and opt-in. Virtualization is the harder trade: most rows are not in the DOM, so assistive technology cannot reach them, which is why the component takes an onDownload escape rather than pretending the virtual window is the whole log. Levels are written in text as well as coloured, matches use mark elements rather than a background colour, and an invalid pattern is announced instead of silently showing an empty log that reads as \"nothing matched\"."
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { meta };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/log-viewer/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"log-viewer\",\n title: \"Log Viewer\",\n description: \"A streaming console with filtering, level facets and follow mode.\",\n category: \"data\",\n status: \"stable\",\n dependencies: [\"@tanstack/react-virtual\"],\n registryDependencies: [],\n files: [\"log-stream.ts\", \"log-viewer.tsx\"],\n a11y:\n 'role=\"log\" implies aria-live=\"polite\", which is right for a few events and unusable for a ' +\n \"console — a screen reader would read every line of a build and nothing else would be \" +\n \"audible. Announcing is off by default and opt-in. Virtualization is the harder trade: most \" +\n \"rows are not in the DOM, so assistive technology cannot reach them, which is why the \" +\n \"component takes an onDownload escape rather than pretending the virtual window is the whole \" +\n \"log. Levels are written in text as well as coloured, matches use mark elements rather than \" +\n \"a background colour, and an invalid pattern is announced instead of silently showing an \" +\n 'empty log that reads as \"nothing matched\".',\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aAAa;CACb,UAAU;CACV,QAAQ;CACR,cAAc,CAAC,yBAAyB;CACxC,sBAAsB,CAAC;CACvB,OAAO,CAAC,iBAAiB,gBAAgB;CACzC,MACE;AAQJ,CAAC"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { CellState, Grants, GroupState, Permission, PermissionGroup, Role, cellState, countGranted, groupPermissions, groupState, inheritedFrom } from "./permission-model.js";
|
|
2
|
+
import { PermissionMatrix, PermissionMatrixProps } from "./permission-matrix.js";
|
|
3
|
+
export { type CellState, type Grants, type GroupState, type Permission, type PermissionGroup, PermissionMatrix, type PermissionMatrixProps, type Role, cellState, countGranted, groupPermissions, groupState, inheritedFrom };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMeta } from "../../registry/schema.js";
|
|
2
|
+
//#region src/components/permission-matrix/meta.ts
|
|
3
|
+
const meta = defineMeta({
|
|
4
|
+
name: "permission-matrix",
|
|
5
|
+
title: "Permission Matrix",
|
|
6
|
+
description: "Roles across, permissions down, with inheritance, locked roles and one tab stop.",
|
|
7
|
+
category: "data",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: [],
|
|
10
|
+
registryDependencies: ["checkbox"],
|
|
11
|
+
files: ["permission-model.ts", "permission-matrix.tsx"],
|
|
12
|
+
a11y: "A grid in the WAI-ARIA sense: role=\"grid\" with one tab stop, arrow keys between cells, Home and End along a row and Ctrl with them across the whole matrix — the right call for a matrix that is operated, where it was the wrong one for a diff that is read. Every checkbox is named by both coordinates, \"Delete projects for Editor\", so a reader arriving by arrow key knows where they are without re-reading the headers. An inherited grant is a checked box that cannot be changed here, marked aria-disabled rather than disabled so it stays in the keyboard path, with the role it came from as its description. A locked role says in each cell's description that nothing can be changed. Group toggles are tri-state, and the column header counts each role's grants in text."
|
|
13
|
+
});
|
|
14
|
+
//#endregion
|
|
15
|
+
export { meta };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=meta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"meta.js","names":[],"sources":["../../../src/components/permission-matrix/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"permission-matrix\",\n title: \"Permission Matrix\",\n description:\n \"Roles across, permissions down, with inheritance, locked roles and one tab stop.\",\n category: \"data\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [\"checkbox\"],\n files: [\"permission-model.ts\", \"permission-matrix.tsx\"],\n a11y:\n 'A grid in the WAI-ARIA sense: role=\"grid\" with one tab stop, arrow keys between cells, Home ' +\n \"and End along a row and Ctrl with them across the whole matrix — the right call for a \" +\n \"matrix that is operated, where it was the wrong one for a diff that is read. Every checkbox \" +\n 'is named by both coordinates, \"Delete projects for Editor\", so a reader arriving by arrow ' +\n \"key knows where they are without re-reading the headers. An inherited grant is a checked \" +\n \"box that cannot be changed here, marked aria-disabled rather than disabled so it stays in \" +\n \"the keyboard path, with the role it came from as its description. A locked role says in \" +\n \"each cell's description that nothing can be changed. Group toggles are tri-state, and the \" +\n \"column header counts each role's grants in text.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aACE;CACF,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC,UAAU;CACjC,OAAO,CAAC,uBAAuB,uBAAuB;CACtD,MACE;AASJ,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Grants, Permission, Role } from "./permission-model.js";
|
|
2
|
+
import { ComponentPropsWithRef } from "react";
|
|
3
|
+
//#region src/components/permission-matrix/permission-matrix.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Roles across, permissions down, a checkbox at every crossing.
|
|
6
|
+
*
|
|
7
|
+
* Every admin panel has one and every admin panel builds it, because the
|
|
8
|
+
* hard part is not the checkboxes. It is that a role can inherit from
|
|
9
|
+
* another, so a box can be ticked without anyone having ticked it; that an
|
|
10
|
+
* Owner has everything and none of it can be unticked; that a section of
|
|
11
|
+
* eight permissions wants one control for all of them; and that sixty
|
|
12
|
+
* checkboxes are sixty tab stops unless something is done about it.
|
|
13
|
+
*
|
|
14
|
+
* Something is done about it. This is a grid in the WAI-ARIA sense — one tab
|
|
15
|
+
* stop, arrow keys between cells, Home and End along a row — which is the
|
|
16
|
+
* right call here and was the wrong one for the diff viewer: a diff is read,
|
|
17
|
+
* a matrix is operated. Every checkbox is named by both coordinates, "Delete
|
|
18
|
+
* projects for Editor", so a reader arriving by arrow key knows where they
|
|
19
|
+
* are without re-reading the headers.
|
|
20
|
+
*
|
|
21
|
+
* A grant that arrived by inheritance is shown as a checked box that cannot
|
|
22
|
+
* be unchecked here, with the role it came from said beside it and in the
|
|
23
|
+
* box's description. A disabled control would be the obvious rendering, and
|
|
24
|
+
* it would take the box out of the tab order and the arrow-key path, so a
|
|
25
|
+
* keyboard user would step over the one cell whose state needs explaining.
|
|
26
|
+
*
|
|
27
|
+
* Changes are reported, never applied. A group toggle reports every
|
|
28
|
+
* permission it touched in one call, so an application can save it as one
|
|
29
|
+
* change rather than eight.
|
|
30
|
+
*/
|
|
31
|
+
interface PermissionMatrixProps extends Omit<ComponentPropsWithRef<"table">, "onChange"> {
|
|
32
|
+
/** Names the matrix: "Permissions for Acme". */
|
|
33
|
+
label: string;
|
|
34
|
+
roles: Role[];
|
|
35
|
+
permissions: Permission[];
|
|
36
|
+
grants: Grants;
|
|
37
|
+
/**
|
|
38
|
+
* A change to make. Several ids arrive together when a group toggle was
|
|
39
|
+
* used. Omit to render the matrix as a record.
|
|
40
|
+
*/
|
|
41
|
+
onChange?: (roleId: string, permissionIds: string[], granted: boolean) => void;
|
|
42
|
+
}
|
|
43
|
+
declare function PermissionMatrix({ className, label, roles, permissions, grants, onChange, ...props }: PermissionMatrixProps): import("react").JSX.Element;
|
|
44
|
+
//#endregion
|
|
45
|
+
export { PermissionMatrix, PermissionMatrixProps };
|
|
46
|
+
//# sourceMappingURL=permission-matrix.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-matrix.d.ts","names":[],"sources":["../../../src/components/permission-matrix/permission-matrix.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAmDiB,8BAA8B,KAC7C;;EAIA;EACA,OAAO;EACP,aAAa;EACb,QAAQ;;;;;EAKR,YAAY,gBAAgB,yBAAyB;;iBAcvC,mBACd,WACA,OACA,OACA,aACA,QACA,aACG,SACF,wCAAqB,IAAA"}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { Checkbox } from "../checkbox/checkbox.js";
|
|
4
|
+
import { cellState, countGranted, groupPermissions, groupState } from "./permission-model.js";
|
|
5
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { useId, useMemo, useState } from "react";
|
|
7
|
+
//#region src/components/permission-matrix/permission-matrix.tsx
|
|
8
|
+
const ARROWS = {
|
|
9
|
+
ArrowUp: [-1, 0],
|
|
10
|
+
ArrowDown: [1, 0],
|
|
11
|
+
ArrowLeft: [0, -1],
|
|
12
|
+
ArrowRight: [0, 1]
|
|
13
|
+
};
|
|
14
|
+
function PermissionMatrix({ className, label, roles, permissions, grants, onChange, ...props }) {
|
|
15
|
+
const uid = useId();
|
|
16
|
+
const rows = useMemo(() => groupPermissions(permissions).flatMap((group) => [...group.group !== void 0 ? [{
|
|
17
|
+
kind: "group",
|
|
18
|
+
label: group.group,
|
|
19
|
+
permissions: group.permissions
|
|
20
|
+
}] : [], ...group.permissions.map((permission) => ({
|
|
21
|
+
kind: "permission",
|
|
22
|
+
permission
|
|
23
|
+
}))]), [permissions]);
|
|
24
|
+
const [active, setActive] = useState([0, 0]);
|
|
25
|
+
const onKeyDown = (event) => {
|
|
26
|
+
const target = event.target;
|
|
27
|
+
const row = Number(target.dataset.row);
|
|
28
|
+
const col = Number(target.dataset.col);
|
|
29
|
+
if (Number.isNaN(row) || Number.isNaN(col)) return;
|
|
30
|
+
let next = null;
|
|
31
|
+
const arrow = ARROWS[event.key];
|
|
32
|
+
if (arrow) next = [row + arrow[0], col + arrow[1]];
|
|
33
|
+
else if (event.key === "Home") next = event.ctrlKey ? [0, 0] : [row, 0];
|
|
34
|
+
else if (event.key === "End") next = event.ctrlKey ? [rows.length - 1, roles.length - 1] : [row, roles.length - 1];
|
|
35
|
+
if (!next) return;
|
|
36
|
+
const [nextRow, nextCol] = next;
|
|
37
|
+
if (nextRow < 0 || nextRow >= rows.length || nextCol < 0 || nextCol >= roles.length) {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
event.currentTarget.querySelector(`[data-row="${String(nextRow)}"][data-col="${String(nextCol)}"]`)?.focus();
|
|
43
|
+
};
|
|
44
|
+
return /* @__PURE__ */ jsx("div", {
|
|
45
|
+
className: "relative w-full overflow-x-auto",
|
|
46
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
47
|
+
"data-slot": "permission-matrix",
|
|
48
|
+
role: "grid",
|
|
49
|
+
"aria-label": label,
|
|
50
|
+
"aria-readonly": onChange ? void 0 : true,
|
|
51
|
+
onKeyDown,
|
|
52
|
+
className: cn("w-full border-collapse text-sm", className),
|
|
53
|
+
...props,
|
|
54
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
|
|
55
|
+
role: "row",
|
|
56
|
+
className: "border-b border-border",
|
|
57
|
+
children: [/* @__PURE__ */ jsx("th", {
|
|
58
|
+
role: "columnheader",
|
|
59
|
+
scope: "col",
|
|
60
|
+
className: "px-3 py-2 text-left text-xs font-medium",
|
|
61
|
+
children: "Permission"
|
|
62
|
+
}), roles.map((role) => {
|
|
63
|
+
const granted = countGranted(roles, grants, role, permissions);
|
|
64
|
+
return /* @__PURE__ */ jsxs("th", {
|
|
65
|
+
role: "columnheader",
|
|
66
|
+
scope: "col",
|
|
67
|
+
className: "px-3 py-2 text-center text-xs font-medium",
|
|
68
|
+
children: [role.label, /* @__PURE__ */ jsx("span", {
|
|
69
|
+
className: "block font-normal text-muted-foreground",
|
|
70
|
+
children: role.locked ? "all, locked" : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
71
|
+
String(granted),
|
|
72
|
+
"/",
|
|
73
|
+
String(permissions.length),
|
|
74
|
+
/* @__PURE__ */ jsx("span", {
|
|
75
|
+
className: "sr-only",
|
|
76
|
+
children: " granted"
|
|
77
|
+
})
|
|
78
|
+
] })
|
|
79
|
+
})]
|
|
80
|
+
}, role.id);
|
|
81
|
+
})]
|
|
82
|
+
}) }), /* @__PURE__ */ jsx("tbody", { children: rows.map((row, rowIndex) => row.kind === "group" ? /* @__PURE__ */ jsxs("tr", {
|
|
83
|
+
role: "row",
|
|
84
|
+
className: "border-b border-border bg-muted/40",
|
|
85
|
+
children: [/* @__PURE__ */ jsxs("th", {
|
|
86
|
+
role: "rowheader",
|
|
87
|
+
scope: "row",
|
|
88
|
+
className: "px-3 py-1.5 text-left text-xs font-medium",
|
|
89
|
+
children: [row.label, /* @__PURE__ */ jsx("span", {
|
|
90
|
+
className: "sr-only",
|
|
91
|
+
children: ", all"
|
|
92
|
+
})]
|
|
93
|
+
}), roles.map((role, colIndex) => /* @__PURE__ */ jsx(GroupCell, {
|
|
94
|
+
role,
|
|
95
|
+
roles,
|
|
96
|
+
grants,
|
|
97
|
+
group: row.label,
|
|
98
|
+
permissions: row.permissions,
|
|
99
|
+
position: [rowIndex, colIndex],
|
|
100
|
+
active: active[0] === rowIndex && active[1] === colIndex,
|
|
101
|
+
onActivate: setActive,
|
|
102
|
+
onChange
|
|
103
|
+
}, role.id))]
|
|
104
|
+
}, `group-${row.label}`) : /* @__PURE__ */ jsxs("tr", {
|
|
105
|
+
role: "row",
|
|
106
|
+
className: "border-b border-border last:border-0 hover:bg-muted/30",
|
|
107
|
+
children: [/* @__PURE__ */ jsxs("th", {
|
|
108
|
+
role: "rowheader",
|
|
109
|
+
scope: "row",
|
|
110
|
+
className: cn("px-3 py-2 text-left font-normal", row.permission.group !== void 0 && "pl-6"),
|
|
111
|
+
children: [row.permission.label, row.permission.description ? /* @__PURE__ */ jsx("span", {
|
|
112
|
+
className: "block text-xs text-muted-foreground",
|
|
113
|
+
children: row.permission.description
|
|
114
|
+
}) : null]
|
|
115
|
+
}), roles.map((role, colIndex) => /* @__PURE__ */ jsx(PermissionCell, {
|
|
116
|
+
id: `${uid}-${role.id}-${row.permission.id}`,
|
|
117
|
+
role,
|
|
118
|
+
roles,
|
|
119
|
+
grants,
|
|
120
|
+
permission: row.permission,
|
|
121
|
+
position: [rowIndex, colIndex],
|
|
122
|
+
active: active[0] === rowIndex && active[1] === colIndex,
|
|
123
|
+
onActivate: setActive,
|
|
124
|
+
onChange
|
|
125
|
+
}, role.id))]
|
|
126
|
+
}, row.permission.id)) })]
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
function PermissionCell({ id, role, roles, grants, permission, position, active, onActivate, onChange }) {
|
|
131
|
+
const state = cellState(roles, grants, role, permission.id);
|
|
132
|
+
const fixed = state.fixed || !onChange;
|
|
133
|
+
const noteId = `${id}-note`;
|
|
134
|
+
const note = role.locked ? `${role.label} has every permission; this cannot be changed.` : state.inheritedFrom ? `Inherited from ${state.inheritedFrom.label}.` : null;
|
|
135
|
+
return /* @__PURE__ */ jsxs("td", {
|
|
136
|
+
role: "gridcell",
|
|
137
|
+
"data-slot": "permission-cell",
|
|
138
|
+
"data-granted": state.granted || void 0,
|
|
139
|
+
"data-inherited": state.inheritedFrom ? "" : void 0,
|
|
140
|
+
className: "px-3 py-2 text-center align-middle",
|
|
141
|
+
children: [/* @__PURE__ */ jsx(Checkbox, {
|
|
142
|
+
id,
|
|
143
|
+
checked: state.granted,
|
|
144
|
+
"aria-label": `${permission.label} for ${role.label}`,
|
|
145
|
+
"aria-describedby": note ? noteId : void 0,
|
|
146
|
+
"aria-disabled": fixed || void 0,
|
|
147
|
+
tabIndex: active ? 0 : -1,
|
|
148
|
+
"data-row": position[0],
|
|
149
|
+
"data-col": position[1],
|
|
150
|
+
className: cn(fixed && "opacity-55"),
|
|
151
|
+
onFocus: () => {
|
|
152
|
+
onActivate(position);
|
|
153
|
+
},
|
|
154
|
+
onCheckedChange: (next) => {
|
|
155
|
+
if (fixed) return;
|
|
156
|
+
onChange?.(role.id, [permission.id], next === true);
|
|
157
|
+
}
|
|
158
|
+
}), state.inheritedFrom ? /* @__PURE__ */ jsxs("span", {
|
|
159
|
+
id: noteId,
|
|
160
|
+
className: "mt-0.5 block text-2xs text-muted-foreground",
|
|
161
|
+
children: ["via ", state.inheritedFrom.label]
|
|
162
|
+
}) : note ? /* @__PURE__ */ jsx("span", {
|
|
163
|
+
id: noteId,
|
|
164
|
+
className: "sr-only",
|
|
165
|
+
children: note
|
|
166
|
+
}) : null]
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
function GroupCell({ role, roles, grants, group, permissions, position, active, onActivate, onChange }) {
|
|
170
|
+
const ids = permissions.map((permission) => permission.id);
|
|
171
|
+
const state = groupState(roles, grants, role, ids);
|
|
172
|
+
const changeable = permissions.filter((permission) => !cellState(roles, grants, role, permission.id).fixed).map((permission) => permission.id);
|
|
173
|
+
const fixed = role.locked || !onChange || changeable.length === 0;
|
|
174
|
+
return /* @__PURE__ */ jsx("td", {
|
|
175
|
+
role: "gridcell",
|
|
176
|
+
"data-slot": "permission-group-cell",
|
|
177
|
+
className: "px-3 py-1.5 text-center align-middle",
|
|
178
|
+
children: /* @__PURE__ */ jsx(Checkbox, {
|
|
179
|
+
checked: state === "all" ? true : state === "some" ? "indeterminate" : false,
|
|
180
|
+
"aria-label": `All ${group} for ${role.label}`,
|
|
181
|
+
"aria-disabled": fixed || void 0,
|
|
182
|
+
tabIndex: active ? 0 : -1,
|
|
183
|
+
"data-row": position[0],
|
|
184
|
+
"data-col": position[1],
|
|
185
|
+
className: cn(fixed && "opacity-55"),
|
|
186
|
+
onFocus: () => {
|
|
187
|
+
onActivate(position);
|
|
188
|
+
},
|
|
189
|
+
onCheckedChange: (next) => {
|
|
190
|
+
if (fixed) return;
|
|
191
|
+
onChange?.(role.id, changeable, next === true);
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
export { PermissionMatrix };
|
|
198
|
+
|
|
199
|
+
//# sourceMappingURL=permission-matrix.js.map
|