@camstack/ui-library 0.1.44 → 0.1.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/composites/index.d.ts +2 -0
- package/dist/composites/scope-picker.d.ts +25 -0
- package/dist/index.cjs +199 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +199 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -97,3 +97,5 @@ export type { KebabMenuProps, KebabMenuItem } from './kebab-menu';
|
|
|
97
97
|
export { ConfirmDialogProvider, useConfirm } from './confirm-dialog';
|
|
98
98
|
export { WidgetSlot } from './widget-slot';
|
|
99
99
|
export type { WidgetSlotProps } from './widget-slot';
|
|
100
|
+
export { ScopePicker, validateScopes } from './scope-picker';
|
|
101
|
+
export type { ScopeAccess } from './scope-picker';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
import { TokenScope } from '@camstack/types';
|
|
3
|
+
export type ScopeAccess = 'view' | 'create' | 'delete';
|
|
4
|
+
interface ScopePickerProps {
|
|
5
|
+
readonly value: readonly TokenScope[];
|
|
6
|
+
readonly onChange: (next: TokenScope[]) => void;
|
|
7
|
+
/**
|
|
8
|
+
* When set, the caller's own scope set. Each row's target dropdown
|
|
9
|
+
* is narrowed to entries the caller already grants, and access
|
|
10
|
+
* checkboxes are disabled for flavours the caller lacks. Admins
|
|
11
|
+
* (unscoped) should pass `null` to allow everything.
|
|
12
|
+
*/
|
|
13
|
+
readonly clampToParent?: readonly TokenScope[] | null;
|
|
14
|
+
/** Optional empty-state CTA when `value.length === 0`. */
|
|
15
|
+
readonly emptyHint?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
export declare function ScopePicker({ value, onChange, clampToParent, emptyHint }: ScopePickerProps): React.ReactElement;
|
|
18
|
+
/**
|
|
19
|
+
* Validation helper. The wire schema (`TokenScopeSchema`) requires every
|
|
20
|
+
* scope to carry a non-empty target and `access.length >= 1`. Use this
|
|
21
|
+
* before passing the picker output to a mutate call so the user sees a
|
|
22
|
+
* clean inline error instead of a tRPC ZodError.
|
|
23
|
+
*/
|
|
24
|
+
export declare function validateScopes(scopes: readonly TokenScope[]): string | null;
|
|
25
|
+
export {};
|
package/dist/index.cjs
CHANGED
|
@@ -7937,6 +7937,13 @@ var Network = createLucideIcon("network", [
|
|
|
7937
7937
|
key: "2874zd"
|
|
7938
7938
|
}]
|
|
7939
7939
|
]);
|
|
7940
|
+
var Pencil = createLucideIcon("pencil", [["path", {
|
|
7941
|
+
d: "M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z",
|
|
7942
|
+
key: "1a8usu"
|
|
7943
|
+
}], ["path", {
|
|
7944
|
+
d: "m15 5 4 4",
|
|
7945
|
+
key: "1mk7zo"
|
|
7946
|
+
}]]);
|
|
7940
7947
|
var Play = createLucideIcon("play", [["path", {
|
|
7941
7948
|
d: "M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z",
|
|
7942
7949
|
key: "10ikf1"
|
|
@@ -23001,6 +23008,196 @@ function KebabMenu({ items, header, triggerClassName, title = "More actions" })
|
|
|
23001
23008
|
});
|
|
23002
23009
|
}
|
|
23003
23010
|
//#endregion
|
|
23011
|
+
//#region src/composites/scope-picker.tsx
|
|
23012
|
+
/**
|
|
23013
|
+
* <ScopePicker> — operator-facing editor for a `TokenScope[]` value.
|
|
23014
|
+
*
|
|
23015
|
+
* Two consumers: the user-scope editor on `/system/users` (admin grants
|
|
23016
|
+
* a baseline scope set to a viewer / agent / scoped user) and the
|
|
23017
|
+
* scoped-token create modal on `/system/api-keys` (a caller carves
|
|
23018
|
+
* out a subset of THEIR scopes for a `cst_*` token).
|
|
23019
|
+
*
|
|
23020
|
+
* Each row is `{ type, target, access }`:
|
|
23021
|
+
* - `type`: `addon` or `capability` (route-prefix retired in the
|
|
23022
|
+
* caps-only refactor — every endpoint lives behind a cap).
|
|
23023
|
+
* - `target`: a known addon id (live from `useAddonsList`) or a known
|
|
23024
|
+
* cap name (from `KNOWN_CAP_NAMES` emitted by codegen).
|
|
23025
|
+
* - `access`: three independent checkboxes (`view` / `create` /
|
|
23026
|
+
* `delete`). At least one must be selected; an empty grant is
|
|
23027
|
+
* rejected by the server-side Zod schema (`access.min(1)`).
|
|
23028
|
+
*
|
|
23029
|
+
* `clampToParent` narrows the target dropdown + access checkboxes to
|
|
23030
|
+
* what the CALLER possesses — this is the subset check rendered in
|
|
23031
|
+
* the UI, mirroring the structural check in `createScopedToken`.
|
|
23032
|
+
* Admins should pass `null` to allow everything.
|
|
23033
|
+
*/
|
|
23034
|
+
var ACCESS_OPTIONS = [
|
|
23035
|
+
{
|
|
23036
|
+
value: "view",
|
|
23037
|
+
label: "View",
|
|
23038
|
+
icon: Eye,
|
|
23039
|
+
hint: "Read-only — queries + subscriptions"
|
|
23040
|
+
},
|
|
23041
|
+
{
|
|
23042
|
+
value: "create",
|
|
23043
|
+
label: "Create",
|
|
23044
|
+
icon: Pencil,
|
|
23045
|
+
hint: "Mutations that create or update"
|
|
23046
|
+
},
|
|
23047
|
+
{
|
|
23048
|
+
value: "delete",
|
|
23049
|
+
label: "Delete",
|
|
23050
|
+
icon: Trash2,
|
|
23051
|
+
hint: "Destructive ops (delete/revoke/reset/…)"
|
|
23052
|
+
}
|
|
23053
|
+
];
|
|
23054
|
+
function defaultRow() {
|
|
23055
|
+
return {
|
|
23056
|
+
type: "capability",
|
|
23057
|
+
target: "",
|
|
23058
|
+
access: ["view"]
|
|
23059
|
+
};
|
|
23060
|
+
}
|
|
23061
|
+
function clampedTargetsForType(type, parent, addonChoices, capChoices) {
|
|
23062
|
+
const universe = type === "addon" ? addonChoices : capChoices;
|
|
23063
|
+
if (parent == null) return universe;
|
|
23064
|
+
const allowed = new Set(parent.filter((s) => s.type === type).map((s) => s.target));
|
|
23065
|
+
return universe.filter((t) => allowed.has(t));
|
|
23066
|
+
}
|
|
23067
|
+
function clampedAccessForRow(row, parent) {
|
|
23068
|
+
if (parent == null) return [
|
|
23069
|
+
"view",
|
|
23070
|
+
"create",
|
|
23071
|
+
"delete"
|
|
23072
|
+
];
|
|
23073
|
+
const matches = parent.filter((s) => s.type === row.type && s.target === row.target);
|
|
23074
|
+
if (matches.length === 0) return [];
|
|
23075
|
+
const out = /* @__PURE__ */ new Set();
|
|
23076
|
+
for (const m of matches) for (const a of m.access) out.add(a);
|
|
23077
|
+
return [...out];
|
|
23078
|
+
}
|
|
23079
|
+
function ScopePicker({ value, onChange, clampToParent, emptyHint }) {
|
|
23080
|
+
const { data: addons } = useAddonsList();
|
|
23081
|
+
const addonChoices = (0, react.useMemo)(() => (addons ?? []).map((a) => a.manifest.id).sort(), [addons]);
|
|
23082
|
+
const capChoices = (0, react.useMemo)(() => [..._camstack_types.KNOWN_CAP_NAMES].sort(), []);
|
|
23083
|
+
const update = (idx, patch) => {
|
|
23084
|
+
onChange(value.map((s, i) => {
|
|
23085
|
+
if (i !== idx) return s;
|
|
23086
|
+
const next = {
|
|
23087
|
+
...s,
|
|
23088
|
+
...patch
|
|
23089
|
+
};
|
|
23090
|
+
if (patch.type !== void 0 && patch.type !== s.type) next.target = "";
|
|
23091
|
+
return next;
|
|
23092
|
+
}));
|
|
23093
|
+
};
|
|
23094
|
+
const removeAt = (idx) => {
|
|
23095
|
+
onChange(value.filter((_, i) => i !== idx));
|
|
23096
|
+
};
|
|
23097
|
+
const append = () => {
|
|
23098
|
+
onChange([...value, defaultRow()]);
|
|
23099
|
+
};
|
|
23100
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
23101
|
+
className: "space-y-2",
|
|
23102
|
+
children: [
|
|
23103
|
+
value.length === 0 && emptyHint && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
23104
|
+
className: "text-[10px] text-foreground-subtle italic px-2 py-2",
|
|
23105
|
+
children: emptyHint
|
|
23106
|
+
}),
|
|
23107
|
+
value.map((row, idx) => {
|
|
23108
|
+
const targetChoices = clampedTargetsForType(row.type, clampToParent, addonChoices, capChoices);
|
|
23109
|
+
const allowedAccess = clampedAccessForRow(row, clampToParent);
|
|
23110
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
23111
|
+
className: "flex flex-wrap items-center gap-1.5 rounded border border-border bg-surface px-2 py-1.5",
|
|
23112
|
+
children: [
|
|
23113
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
23114
|
+
value: row.type,
|
|
23115
|
+
onChange: (e) => update(idx, { type: e.target.value }),
|
|
23116
|
+
className: "rounded border border-border bg-background px-2 py-1 text-[11px] focus:outline-none focus:ring-1 focus:ring-primary",
|
|
23117
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
23118
|
+
value: "capability",
|
|
23119
|
+
children: "capability"
|
|
23120
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
23121
|
+
value: "addon",
|
|
23122
|
+
children: "addon"
|
|
23123
|
+
})]
|
|
23124
|
+
}),
|
|
23125
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
23126
|
+
value: row.target,
|
|
23127
|
+
onChange: (e) => update(idx, { target: e.target.value }),
|
|
23128
|
+
className: "flex-1 min-w-[160px] rounded border border-border bg-background px-2 py-1 text-[11px] font-mono focus:outline-none focus:ring-1 focus:ring-primary",
|
|
23129
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("option", {
|
|
23130
|
+
value: "",
|
|
23131
|
+
children: [
|
|
23132
|
+
"— pick ",
|
|
23133
|
+
row.type,
|
|
23134
|
+
" —"
|
|
23135
|
+
]
|
|
23136
|
+
}), targetChoices.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
23137
|
+
value: t,
|
|
23138
|
+
children: t
|
|
23139
|
+
}, t))]
|
|
23140
|
+
}),
|
|
23141
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
23142
|
+
className: "flex items-center gap-1",
|
|
23143
|
+
children: ACCESS_OPTIONS.map(({ value: access, label, icon: Icon, hint }) => {
|
|
23144
|
+
const checked = row.access.includes(access);
|
|
23145
|
+
const disabled = clampToParent != null && !allowedAccess.includes(access) && !checked;
|
|
23146
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
23147
|
+
title: disabled ? `${hint} — not granted by your scope` : hint,
|
|
23148
|
+
className: `inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] cursor-pointer transition-colors ${checked ? "bg-primary/15 text-primary font-medium" : "text-foreground-subtle hover:bg-foreground-subtle/10"} ${disabled ? "opacity-40 cursor-not-allowed" : ""}`,
|
|
23149
|
+
children: [
|
|
23150
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
23151
|
+
type: "checkbox",
|
|
23152
|
+
checked,
|
|
23153
|
+
disabled,
|
|
23154
|
+
onChange: (e) => {
|
|
23155
|
+
const nextSet = new Set(row.access);
|
|
23156
|
+
if (e.target.checked) nextSet.add(access);
|
|
23157
|
+
else nextSet.delete(access);
|
|
23158
|
+
update(idx, { access: [...nextSet] });
|
|
23159
|
+
},
|
|
23160
|
+
className: "sr-only"
|
|
23161
|
+
}),
|
|
23162
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon, { className: "h-3 w-3" }),
|
|
23163
|
+
label
|
|
23164
|
+
]
|
|
23165
|
+
}, access);
|
|
23166
|
+
})
|
|
23167
|
+
}),
|
|
23168
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
23169
|
+
type: "button",
|
|
23170
|
+
onClick: () => removeAt(idx),
|
|
23171
|
+
className: "rounded p-1 text-foreground-subtle hover:bg-danger/10 hover:text-danger transition-colors",
|
|
23172
|
+
title: "Remove scope",
|
|
23173
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(X, { className: "h-3 w-3" })
|
|
23174
|
+
})
|
|
23175
|
+
]
|
|
23176
|
+
}, idx);
|
|
23177
|
+
}),
|
|
23178
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
23179
|
+
type: "button",
|
|
23180
|
+
onClick: append,
|
|
23181
|
+
className: "inline-flex items-center gap-1 rounded px-2 py-1 text-[10px] text-foreground-subtle hover:text-foreground hover:bg-foreground-subtle/10 transition-colors",
|
|
23182
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Plus, { className: "h-3 w-3" }), "Add scope"]
|
|
23183
|
+
})
|
|
23184
|
+
]
|
|
23185
|
+
});
|
|
23186
|
+
}
|
|
23187
|
+
/**
|
|
23188
|
+
* Validation helper. The wire schema (`TokenScopeSchema`) requires every
|
|
23189
|
+
* scope to carry a non-empty target and `access.length >= 1`. Use this
|
|
23190
|
+
* before passing the picker output to a mutate call so the user sees a
|
|
23191
|
+
* clean inline error instead of a tRPC ZodError.
|
|
23192
|
+
*/
|
|
23193
|
+
function validateScopes(scopes) {
|
|
23194
|
+
for (const s of scopes) {
|
|
23195
|
+
if (s.target.trim().length === 0) return `Scope ${s.type}: pick a target`;
|
|
23196
|
+
if (s.access.length === 0) return `Scope ${s.type}:${s.target}: pick at least one access`;
|
|
23197
|
+
}
|
|
23198
|
+
return null;
|
|
23199
|
+
}
|
|
23200
|
+
//#endregion
|
|
23004
23201
|
//#region src/contexts/zone-editing.tsx
|
|
23005
23202
|
/**
|
|
23006
23203
|
* Per-device zone-editing state shared between the player overlay
|
|
@@ -23830,6 +24027,7 @@ exports.SECTION_HEADER = SECTION_HEADER;
|
|
|
23830
24027
|
exports.SPLIT_PANEL_OUTER = SPLIT_PANEL_OUTER;
|
|
23831
24028
|
exports.SPLIT_PANEL_SIDE = SPLIT_PANEL_SIDE;
|
|
23832
24029
|
exports.STACK_GAP = STACK_GAP;
|
|
24030
|
+
exports.ScopePicker = ScopePicker;
|
|
23833
24031
|
exports.ScrollArea = ScrollArea;
|
|
23834
24032
|
exports.Select = Select;
|
|
23835
24033
|
exports.SemanticBadge = SemanticBadge;
|
|
@@ -24391,5 +24589,6 @@ exports.useZonesAddZone = useZonesAddZone;
|
|
|
24391
24589
|
exports.useZonesListZones = useZonesListZones;
|
|
24392
24590
|
exports.useZonesRemoveZone = useZonesRemoveZone;
|
|
24393
24591
|
exports.useZonesUpdateZone = useZonesUpdateZone;
|
|
24592
|
+
exports.validateScopes = validateScopes;
|
|
24394
24593
|
|
|
24395
24594
|
//# sourceMappingURL=index.cjs.map
|