@camstack/ui-library 0.1.43 → 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.
@@ -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 {};
@@ -883,6 +883,8 @@ export declare const useUserManagementUpdateUser: typeof trpc.userManagement.upd
883
883
  export declare const useUserManagementDeleteUser: typeof trpc.userManagement.deleteUser.useMutation;
884
884
  /** Generated alias around `trpc.userManagement.resetPassword.useMutation`. */
885
885
  export declare const useUserManagementResetPassword: typeof trpc.userManagement.resetPassword.useMutation;
886
+ /** Generated alias around `trpc.userManagement.setUserScopes.useMutation`. */
887
+ export declare const useUserManagementSetUserScopes: typeof trpc.userManagement.setUserScopes.useMutation;
886
888
  /** Generated alias around `trpc.userManagement.validateCredentials.useMutation`. */
887
889
  export declare const useUserManagementValidateCredentials: typeof trpc.userManagement.validateCredentials.useMutation;
888
890
  /** Generated alias around `trpc.userManagement.listApiKeys.useQuery`. */
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"
@@ -16492,6 +16499,8 @@ var useUserManagementUpdateUser = trpc.userManagement.updateUser.useMutation;
16492
16499
  var useUserManagementDeleteUser = trpc.userManagement.deleteUser.useMutation;
16493
16500
  /** Generated alias around `trpc.userManagement.resetPassword.useMutation`. */
16494
16501
  var useUserManagementResetPassword = trpc.userManagement.resetPassword.useMutation;
16502
+ /** Generated alias around `trpc.userManagement.setUserScopes.useMutation`. */
16503
+ var useUserManagementSetUserScopes = trpc.userManagement.setUserScopes.useMutation;
16495
16504
  /** Generated alias around `trpc.userManagement.validateCredentials.useMutation`. */
16496
16505
  var useUserManagementValidateCredentials = trpc.userManagement.validateCredentials.useMutation;
16497
16506
  /** Generated alias around `trpc.userManagement.listApiKeys.useQuery`. */
@@ -22999,6 +23008,196 @@ function KebabMenu({ items, header, triggerClassName, title = "More actions" })
22999
23008
  });
23000
23009
  }
23001
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
23002
23201
  //#region src/contexts/zone-editing.tsx
23003
23202
  /**
23004
23203
  * Per-device zone-editing state shared between the player overlay
@@ -23828,6 +24027,7 @@ exports.SECTION_HEADER = SECTION_HEADER;
23828
24027
  exports.SPLIT_PANEL_OUTER = SPLIT_PANEL_OUTER;
23829
24028
  exports.SPLIT_PANEL_SIDE = SPLIT_PANEL_SIDE;
23830
24029
  exports.STACK_GAP = STACK_GAP;
24030
+ exports.ScopePicker = ScopePicker;
23831
24031
  exports.ScrollArea = ScrollArea;
23832
24032
  exports.Select = Select;
23833
24033
  exports.SemanticBadge = SemanticBadge;
@@ -24365,6 +24565,7 @@ exports.useUserManagementListUsers = useUserManagementListUsers;
24365
24565
  exports.useUserManagementResetPassword = useUserManagementResetPassword;
24366
24566
  exports.useUserManagementRevokeApiKey = useUserManagementRevokeApiKey;
24367
24567
  exports.useUserManagementRevokeScopedToken = useUserManagementRevokeScopedToken;
24568
+ exports.useUserManagementSetUserScopes = useUserManagementSetUserScopes;
24368
24569
  exports.useUserManagementUpdateUser = useUserManagementUpdateUser;
24369
24570
  exports.useUserManagementValidateApiKey = useUserManagementValidateApiKey;
24370
24571
  exports.useUserManagementValidateCredentials = useUserManagementValidateCredentials;
@@ -24388,5 +24589,6 @@ exports.useZonesAddZone = useZonesAddZone;
24388
24589
  exports.useZonesListZones = useZonesListZones;
24389
24590
  exports.useZonesRemoveZone = useZonesRemoveZone;
24390
24591
  exports.useZonesUpdateZone = useZonesUpdateZone;
24592
+ exports.validateScopes = validateScopes;
24391
24593
 
24392
24594
  //# sourceMappingURL=index.cjs.map