@dowel-ui/react 0.4.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 +9 -5
- 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/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/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/toast/toast.d.ts +1 -1
- package/dist/index.d.ts +25 -1
- package/dist/index.js +16 -2
- package/dist/registry/components.d.ts.map +1 -1
- package/dist/registry/components.js +73 -53
- package/dist/registry/components.js.map +1 -1
- package/package.json +4 -4
- 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/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/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/index.ts +10 -0
- package/src/registry/components.ts +20 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/components/permission-matrix/permission-model.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Roles, permissions and who has what — resolved, so the grid only draws.
|
|
4
|
+
*
|
|
5
|
+
* Pure, so inheritance and the group states are tested without rendering
|
|
6
|
+
* anything, and so a server can answer "may this role do this" from the same
|
|
7
|
+
* grants the matrix edits, with the same rule for inheritance.
|
|
8
|
+
*/
|
|
9
|
+
interface Permission {
|
|
10
|
+
id: string;
|
|
11
|
+
label: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
/** Permissions with the same group share a heading and an all-or-none toggle. */
|
|
14
|
+
group?: string;
|
|
15
|
+
}
|
|
16
|
+
interface Role {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
/** Roles whose grants this one includes. Resolved transitively. */
|
|
20
|
+
inherits?: string[];
|
|
21
|
+
/** Has every permission, and none of them can be changed. An Owner. */
|
|
22
|
+
locked?: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Explicit grants, by role id. Inherited ones are not listed here. */
|
|
25
|
+
type Grants = Record<string, string[]>;
|
|
26
|
+
interface CellState {
|
|
27
|
+
/** Whether the role can do it, by any route. */
|
|
28
|
+
granted: boolean;
|
|
29
|
+
/** The role it comes from, when it is not explicit here. */
|
|
30
|
+
inheritedFrom: Role | null;
|
|
31
|
+
/** Cannot be changed here — locked role, or inherited. */
|
|
32
|
+
fixed: boolean;
|
|
33
|
+
}
|
|
34
|
+
type GroupState = "all" | "some" | "none";
|
|
35
|
+
/**
|
|
36
|
+
* The role a permission is inherited from, or null. Depth-first through
|
|
37
|
+
* `inherits`, cycle-safe, so two roles that inherit each other resolve rather
|
|
38
|
+
* than recurse forever — a configuration a form can produce by accident.
|
|
39
|
+
*/
|
|
40
|
+
declare function inheritedFrom(roles: Role[], grants: Grants, role: Role, permissionId: string, seen?: Set<string>): Role | null;
|
|
41
|
+
declare function cellState(roles: Role[], grants: Grants, role: Role, permissionId: string): CellState;
|
|
42
|
+
declare function groupState(roles: Role[], grants: Grants, role: Role, permissionIds: string[]): GroupState;
|
|
43
|
+
declare function countGranted(roles: Role[], grants: Grants, role: Role, permissions: Permission[]): number;
|
|
44
|
+
interface PermissionGroup {
|
|
45
|
+
/** Undefined for permissions declared without one. */
|
|
46
|
+
group: string | undefined;
|
|
47
|
+
permissions: Permission[];
|
|
48
|
+
}
|
|
49
|
+
/** Permissions by group, in the order the groups first appear. */
|
|
50
|
+
declare function groupPermissions(permissions: Permission[]): PermissionGroup[];
|
|
51
|
+
//#endregion
|
|
52
|
+
export { CellState, Grants, GroupState, Permission, PermissionGroup, Role, cellState, countGranted, groupPermissions, groupState, inheritedFrom };
|
|
53
|
+
//# sourceMappingURL=permission-model.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-model.d.ts","names":[],"sources":["../../../src/components/permission-matrix/permission-model.ts"],"mappings":";;;;;;;;UAQiB;EACf;EACA;EACA;;EAEA;;UAGe;EACf;EACA;;EAEA;;EAEA;;;KAIU,SAAS;UAEJ;;EAEf;;EAEA,eAAe;;EAEf;;KAGU;;;;;;iBAWI,cACd,OAAO,QACP,QAAQ,QACR,MAAM,MACN,sBACA,OAAM,cACL;iBAaa,UACd,OAAO,QACP,QAAQ,QACR,MAAM,MACN,uBACC;iBAQa,WACd,OAAO,QACP,QAAQ,QACR,MAAM,MACN,0BACC;iBAQa,aACd,OAAO,QACP,QAAQ,QACR,MAAM,MACN,aAAa;UAOE;;EAEf;EACA,aAAa;;;iBAIC,iBAAiB,aAAa,eAAe"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
//#region src/components/permission-matrix/permission-model.ts
|
|
2
|
+
function roleById(roles, id) {
|
|
3
|
+
return roles.find((role) => role.id === id);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* The role a permission is inherited from, or null. Depth-first through
|
|
7
|
+
* `inherits`, cycle-safe, so two roles that inherit each other resolve rather
|
|
8
|
+
* than recurse forever — a configuration a form can produce by accident.
|
|
9
|
+
*/
|
|
10
|
+
function inheritedFrom(roles, grants, role, permissionId, seen = /* @__PURE__ */ new Set()) {
|
|
11
|
+
seen.add(role.id);
|
|
12
|
+
for (const parentId of role.inherits ?? []) {
|
|
13
|
+
if (seen.has(parentId)) continue;
|
|
14
|
+
const parent = roleById(roles, parentId);
|
|
15
|
+
if (!parent) continue;
|
|
16
|
+
if (parent.locked || (grants[parent.id] ?? []).includes(permissionId)) return parent;
|
|
17
|
+
const further = inheritedFrom(roles, grants, parent, permissionId, seen);
|
|
18
|
+
if (further) return further;
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
function cellState(roles, grants, role, permissionId) {
|
|
23
|
+
if (role.locked) return {
|
|
24
|
+
granted: true,
|
|
25
|
+
inheritedFrom: null,
|
|
26
|
+
fixed: true
|
|
27
|
+
};
|
|
28
|
+
if ((grants[role.id] ?? []).includes(permissionId)) return {
|
|
29
|
+
granted: true,
|
|
30
|
+
inheritedFrom: null,
|
|
31
|
+
fixed: false
|
|
32
|
+
};
|
|
33
|
+
const from = inheritedFrom(roles, grants, role, permissionId);
|
|
34
|
+
return {
|
|
35
|
+
granted: from !== null,
|
|
36
|
+
inheritedFrom: from,
|
|
37
|
+
fixed: from !== null
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function groupState(roles, grants, role, permissionIds) {
|
|
41
|
+
const granted = permissionIds.filter((id) => cellState(roles, grants, role, id).granted).length;
|
|
42
|
+
if (granted === 0) return "none";
|
|
43
|
+
return granted === permissionIds.length ? "all" : "some";
|
|
44
|
+
}
|
|
45
|
+
function countGranted(roles, grants, role, permissions) {
|
|
46
|
+
return permissions.filter((permission) => cellState(roles, grants, role, permission.id).granted).length;
|
|
47
|
+
}
|
|
48
|
+
/** Permissions by group, in the order the groups first appear. */
|
|
49
|
+
function groupPermissions(permissions) {
|
|
50
|
+
const groups = [];
|
|
51
|
+
for (const permission of permissions) {
|
|
52
|
+
const existing = groups.find((candidate) => candidate.group === permission.group);
|
|
53
|
+
if (existing) existing.permissions.push(permission);
|
|
54
|
+
else groups.push({
|
|
55
|
+
group: permission.group,
|
|
56
|
+
permissions: [permission]
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return groups;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { cellState, countGranted, groupPermissions, groupState, inheritedFrom };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=permission-model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"permission-model.js","names":[],"sources":["../../../src/components/permission-matrix/permission-model.ts"],"sourcesContent":["/**\n * Roles, permissions and who has what — resolved, so the grid only draws.\n *\n * Pure, so inheritance and the group states are tested without rendering\n * anything, and so a server can answer \"may this role do this\" from the same\n * grants the matrix edits, with the same rule for inheritance.\n */\n\nexport interface Permission {\n id: string;\n label: string;\n description?: string;\n /** Permissions with the same group share a heading and an all-or-none toggle. */\n group?: string;\n}\n\nexport interface Role {\n id: string;\n label: string;\n /** Roles whose grants this one includes. Resolved transitively. */\n inherits?: string[];\n /** Has every permission, and none of them can be changed. An Owner. */\n locked?: boolean;\n}\n\n/** Explicit grants, by role id. Inherited ones are not listed here. */\nexport type Grants = Record<string, string[]>;\n\nexport interface CellState {\n /** Whether the role can do it, by any route. */\n granted: boolean;\n /** The role it comes from, when it is not explicit here. */\n inheritedFrom: Role | null;\n /** Cannot be changed here — locked role, or inherited. */\n fixed: boolean;\n}\n\nexport type GroupState = \"all\" | \"some\" | \"none\";\n\nfunction roleById(roles: Role[], id: string): Role | undefined {\n return roles.find((role) => role.id === id);\n}\n\n/**\n * The role a permission is inherited from, or null. Depth-first through\n * `inherits`, cycle-safe, so two roles that inherit each other resolve rather\n * than recurse forever — a configuration a form can produce by accident.\n */\nexport function inheritedFrom(\n roles: Role[],\n grants: Grants,\n role: Role,\n permissionId: string,\n seen: Set<string> = new Set(),\n): Role | null {\n seen.add(role.id);\n for (const parentId of role.inherits ?? []) {\n if (seen.has(parentId)) continue;\n const parent = roleById(roles, parentId);\n if (!parent) continue;\n if (parent.locked || (grants[parent.id] ?? []).includes(permissionId)) return parent;\n const further = inheritedFrom(roles, grants, parent, permissionId, seen);\n if (further) return further;\n }\n return null;\n}\n\nexport function cellState(\n roles: Role[],\n grants: Grants,\n role: Role,\n permissionId: string,\n): CellState {\n if (role.locked) return { granted: true, inheritedFrom: null, fixed: true };\n const explicit = (grants[role.id] ?? []).includes(permissionId);\n if (explicit) return { granted: true, inheritedFrom: null, fixed: false };\n const from = inheritedFrom(roles, grants, role, permissionId);\n return { granted: from !== null, inheritedFrom: from, fixed: from !== null };\n}\n\nexport function groupState(\n roles: Role[],\n grants: Grants,\n role: Role,\n permissionIds: string[],\n): GroupState {\n const granted = permissionIds.filter(\n (id) => cellState(roles, grants, role, id).granted,\n ).length;\n if (granted === 0) return \"none\";\n return granted === permissionIds.length ? \"all\" : \"some\";\n}\n\nexport function countGranted(\n roles: Role[],\n grants: Grants,\n role: Role,\n permissions: Permission[],\n): number {\n return permissions.filter(\n (permission) => cellState(roles, grants, role, permission.id).granted,\n ).length;\n}\n\nexport interface PermissionGroup {\n /** Undefined for permissions declared without one. */\n group: string | undefined;\n permissions: Permission[];\n}\n\n/** Permissions by group, in the order the groups first appear. */\nexport function groupPermissions(permissions: Permission[]): PermissionGroup[] {\n const groups: PermissionGroup[] = [];\n for (const permission of permissions) {\n const existing = groups.find((candidate) => candidate.group === permission.group);\n if (existing) existing.permissions.push(permission);\n else groups.push({ group: permission.group, permissions: [permission] });\n }\n return groups;\n}\n"],"mappings":";AAuCA,SAAS,SAAS,OAAe,IAA8B;CAC7D,OAAO,MAAM,MAAM,SAAS,KAAK,OAAO,EAAE;AAC5C;;;;;;AAOA,SAAgB,cACd,OACA,QACA,MACA,cACA,uBAAoB,IAAI,IAAI,GACf;CACb,KAAK,IAAI,KAAK,EAAE;CAChB,KAAK,MAAM,YAAY,KAAK,YAAY,CAAC,GAAG;EAC1C,IAAI,KAAK,IAAI,QAAQ,GAAG;EACxB,MAAM,SAAS,SAAS,OAAO,QAAQ;EACvC,IAAI,CAAC,QAAQ;EACb,IAAI,OAAO,WAAW,OAAO,OAAO,OAAO,CAAC,EAAA,CAAG,SAAS,YAAY,GAAG,OAAO;EAC9E,MAAM,UAAU,cAAc,OAAO,QAAQ,QAAQ,cAAc,IAAI;EACvE,IAAI,SAAS,OAAO;CACtB;CACA,OAAO;AACT;AAEA,SAAgB,UACd,OACA,QACA,MACA,cACW;CACX,IAAI,KAAK,QAAQ,OAAO;EAAE,SAAS;EAAM,eAAe;EAAM,OAAO;CAAK;CAE1E,KADkB,OAAO,KAAK,OAAO,CAAC,EAAA,CAAG,SAAS,YACvC,GAAG,OAAO;EAAE,SAAS;EAAM,eAAe;EAAM,OAAO;CAAM;CACxE,MAAM,OAAO,cAAc,OAAO,QAAQ,MAAM,YAAY;CAC5D,OAAO;EAAE,SAAS,SAAS;EAAM,eAAe;EAAM,OAAO,SAAS;CAAK;AAC7E;AAEA,SAAgB,WACd,OACA,QACA,MACA,eACY;CACZ,MAAM,UAAU,cAAc,QAC3B,OAAO,UAAU,OAAO,QAAQ,MAAM,EAAE,CAAC,CAAC,OAC7C,CAAC,CAAC;CACF,IAAI,YAAY,GAAG,OAAO;CAC1B,OAAO,YAAY,cAAc,SAAS,QAAQ;AACpD;AAEA,SAAgB,aACd,OACA,QACA,MACA,aACQ;CACR,OAAO,YAAY,QAChB,eAAe,UAAU,OAAO,QAAQ,MAAM,WAAW,EAAE,CAAC,CAAC,OAChE,CAAC,CAAC;AACJ;;AASA,SAAgB,iBAAiB,aAA8C;CAC7E,MAAM,SAA4B,CAAC;CACnC,KAAK,MAAM,cAAc,aAAa;EACpC,MAAM,WAAW,OAAO,MAAM,cAAc,UAAU,UAAU,WAAW,KAAK;EAChF,IAAI,UAAU,SAAS,YAAY,KAAK,UAAU;OAC7C,OAAO,KAAK;GAAE,OAAO,WAAW;GAAO,aAAa,CAAC,UAAU;EAAE,CAAC;CACzE;CACA,OAAO;AACT"}
|
|
@@ -4,7 +4,7 @@ import { ComponentPropsWithRef } from "react";
|
|
|
4
4
|
//#region src/components/progress/progress.d.ts
|
|
5
5
|
declare const progressVariants: (props?: ({
|
|
6
6
|
size?: "sm" | "md" | "lg" | null | undefined;
|
|
7
|
-
tone?: "
|
|
7
|
+
tone?: "primary" | "success" | "warning" | "destructive" | null | undefined;
|
|
8
8
|
} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
9
9
|
interface ProgressProps extends ComponentPropsWithRef<typeof Progress.Root>, VariantProps<typeof progressVariants> {}
|
|
10
10
|
/**
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMeta } from "../../registry/schema.js";
|
|
2
|
+
//#region src/components/secret-field/meta.ts
|
|
3
|
+
const meta = defineMeta({
|
|
4
|
+
name: "secret-field",
|
|
5
|
+
title: "Secret Field",
|
|
6
|
+
description: "An API key in its three states: shown once, hidden but revealable, and gone.",
|
|
7
|
+
category: "form",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: [],
|
|
10
|
+
registryDependencies: ["button", "input"],
|
|
11
|
+
files: ["secret-field.tsx"],
|
|
12
|
+
a11y: "The value sits in a read-only input rather than a disabled one, so it can be focused, selected and read, and focusing a shown value selects all of it for copying by hand. The field's state — shown once, hidden, revealed, gone — is a sentence attached as its description, never a colour or an icon. Copying is announced through a status region that exists before the first copy, and failure is announced too, with what to do instead, because the clipboard can be refused. Every button carries the field's label in its name, so a settings page with several keys has several distinct Copy buttons. Regenerating is confirmed inline in a named group that states the consequence, since it revokes the current key."
|
|
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/secret-field/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"secret-field\",\n title: \"Secret Field\",\n description: \"An API key in its three states: shown once, hidden but revealable, and gone.\",\n category: \"form\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [\"button\", \"input\"],\n files: [\"secret-field.tsx\"],\n a11y:\n \"The value sits in a read-only input rather than a disabled one, so it can be focused, \" +\n \"selected and read, and focusing a shown value selects all of it for copying by hand. The \" +\n \"field's state — shown once, hidden, revealed, gone — is a sentence attached as its \" +\n \"description, never a colour or an icon. Copying is announced through a status region that \" +\n \"exists before the first copy, and failure is announced too, with what to do instead, because \" +\n \"the clipboard can be refused. Every button carries the field's label in its name, so a \" +\n \"settings page with several keys has several distinct Copy buttons. Regenerating is confirmed \" +\n \"inline in a named group that states the consequence, since it revokes the current key.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aAAa;CACb,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC,UAAU,OAAO;CACxC,OAAO,CAAC,kBAAkB;CAC1B,MACE;AAQJ,CAAC"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
//#region src/components/secret-field/secret-field.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* An API key, token or signing secret, in the three states it actually has.
|
|
5
|
+
*
|
|
6
|
+
* Stripe, GitHub, OpenAI, Vercel and every developer product with a settings
|
|
7
|
+
* page draw this by hand, and the states are the same everywhere: shown once
|
|
8
|
+
* at creation and never again; hidden but revealable, for a secret the server
|
|
9
|
+
* can show again; and gone, where only a prefix and the last four remain and
|
|
10
|
+
* the only thing left to do is regenerate. No component library ships it —
|
|
11
|
+
* the nearest thing is a password input, which is a control for entering a
|
|
12
|
+
* secret you know, not for handling one you have just been given.
|
|
13
|
+
*
|
|
14
|
+
* "Shown once" is a first-class state rather than a toast, because it is the
|
|
15
|
+
* one that costs people money: the key is on screen, the tab closes, and the
|
|
16
|
+
* next hour is spent regenerating it and updating every client. So the field
|
|
17
|
+
* says it in a sentence beside the value, and the way out is a button that
|
|
18
|
+
* says what it means — "I have saved it" — rather than the value quietly
|
|
19
|
+
* vanishing on navigation.
|
|
20
|
+
*
|
|
21
|
+
* While hidden, the secret is not in the DOM. The preview is what is
|
|
22
|
+
* rendered, so a screenshot, an extension or a devtools pane sees the prefix
|
|
23
|
+
* and the last four, which is what the server itself keeps. Reveals are
|
|
24
|
+
* reported, since an audit log of who looked is the reason the hidden state
|
|
25
|
+
* exists. Copy works while hidden, because the point of the key is to be
|
|
26
|
+
* pasted, not read.
|
|
27
|
+
*/
|
|
28
|
+
type SecretFieldState = "shown" | "hidden" | "revealed" | "gone";
|
|
29
|
+
interface SecretFieldProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
|
|
30
|
+
label: string;
|
|
31
|
+
/** The secret. Omit once it can no longer be shown. */
|
|
32
|
+
value?: string;
|
|
33
|
+
/** What stands in for it while hidden or gone. Derived from the value when there is one. */
|
|
34
|
+
preview?: string;
|
|
35
|
+
/** This is the only time the value can be seen. */
|
|
36
|
+
once?: boolean;
|
|
37
|
+
/** The reader has saved a shown-once value. Drop `value` in response. */
|
|
38
|
+
onAcknowledge?: () => void;
|
|
39
|
+
/** Called each time the value is shown or hidden, so reveals can be logged. */
|
|
40
|
+
onRevealChange?: (revealed: boolean) => void;
|
|
41
|
+
/** Offers to regenerate. Confirmed inline first, because it revokes the current one. */
|
|
42
|
+
onRegenerate?: () => void;
|
|
43
|
+
/** Said before regenerating. */
|
|
44
|
+
regenerateWarning?: string;
|
|
45
|
+
/** Metadata — when it was created, when it was last used. */
|
|
46
|
+
children?: ReactNode;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A prefix and the last four, the way a server keeps a key it has hashed.
|
|
50
|
+
* The prefix is kept through the last underscore in the first twelve
|
|
51
|
+
* characters — `sk_live_`, `ghp_`, `whsec_` — so the reader can tell which
|
|
52
|
+
* key this is without being shown any of it.
|
|
53
|
+
*/
|
|
54
|
+
declare function maskSecret(value: string, keepEnd?: number): string;
|
|
55
|
+
declare function SecretField({ className, label, value, preview, once, onAcknowledge, onRevealChange, onRegenerate, regenerateWarning, children, ...props }: SecretFieldProps): import("react").JSX.Element;
|
|
56
|
+
//#endregion
|
|
57
|
+
export { SecretField, SecretFieldProps, SecretFieldState, maskSecret };
|
|
58
|
+
//# sourceMappingURL=secret-field.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-field.d.ts","names":[],"sources":["../../../src/components/secret-field/secret-field.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;KAyCY;UAEK,yBAAyB,KAAK;EAC7C;;EAEA;;EAEA;;EAEA;;EAEA;;EAEA,kBAAkB;;EAElB;;EAEA;;EAEA,WAAW;;;;;;;;iBASG,WAAW,eAAe;iBAS1B,cACd,WACA,OACA,OACA,SACA,MACA,eACA,gBACA,cACA,mBACA,aACG,SACF,mCAAgB,IAAA"}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { Button } from "../button/button.js";
|
|
4
|
+
import { Input } from "../input/input.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
7
|
+
//#region src/components/secret-field/secret-field.tsx
|
|
8
|
+
/**
|
|
9
|
+
* A prefix and the last four, the way a server keeps a key it has hashed.
|
|
10
|
+
* The prefix is kept through the last underscore in the first twelve
|
|
11
|
+
* characters — `sk_live_`, `ghp_`, `whsec_` — so the reader can tell which
|
|
12
|
+
* key this is without being shown any of it.
|
|
13
|
+
*/
|
|
14
|
+
function maskSecret(value, keepEnd = 4) {
|
|
15
|
+
const underscore = value.slice(0, 12).lastIndexOf("_");
|
|
16
|
+
let prefix = underscore > 0 ? value.slice(0, underscore + 1) : value.slice(0, 4);
|
|
17
|
+
if (prefix.length + keepEnd + 4 > value.length) prefix = "";
|
|
18
|
+
if (value.length < keepEnd + 4) return "••••••••";
|
|
19
|
+
return `${prefix}…${value.slice(-keepEnd)}`;
|
|
20
|
+
}
|
|
21
|
+
function SecretField({ className, label, value, preview, once = false, onAcknowledge, onRevealChange, onRegenerate, regenerateWarning = "Regenerating revokes this one immediately. Anything still using it stops working.", children, ...props }) {
|
|
22
|
+
const id = useId();
|
|
23
|
+
const [revealed, setRevealed] = useState(false);
|
|
24
|
+
const [confirming, setConfirming] = useState(false);
|
|
25
|
+
const [copy, setCopy] = useState("idle");
|
|
26
|
+
const timer = useRef(null);
|
|
27
|
+
useEffect(() => () => {
|
|
28
|
+
if (timer.current) clearTimeout(timer.current);
|
|
29
|
+
}, []);
|
|
30
|
+
const state = value === void 0 ? "gone" : once ? "shown" : revealed ? "revealed" : "hidden";
|
|
31
|
+
const visible = state === "shown" || state === "revealed";
|
|
32
|
+
const masked = preview ?? (value !== void 0 ? maskSecret(value) : "••••••••");
|
|
33
|
+
const copyValue = () => {
|
|
34
|
+
if (value === void 0) return;
|
|
35
|
+
const settle = (next) => {
|
|
36
|
+
setCopy(next);
|
|
37
|
+
if (timer.current) clearTimeout(timer.current);
|
|
38
|
+
timer.current = setTimeout(() => {
|
|
39
|
+
setCopy("idle");
|
|
40
|
+
}, 2e3);
|
|
41
|
+
};
|
|
42
|
+
Promise.resolve().then(() => navigator.clipboard.writeText(value)).then(() => {
|
|
43
|
+
settle("copied");
|
|
44
|
+
}, () => {
|
|
45
|
+
settle("error");
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const setReveal = (next) => {
|
|
49
|
+
setRevealed(next);
|
|
50
|
+
onRevealChange?.(next);
|
|
51
|
+
};
|
|
52
|
+
const note = state === "shown" ? "Shown once. Copy it now — it cannot be shown again." : state === "gone" ? "Cannot be shown again." : state === "hidden" ? "Hidden. Copy works without revealing it." : "Revealed.";
|
|
53
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
54
|
+
"data-slot": "secret-field",
|
|
55
|
+
"data-state": state,
|
|
56
|
+
className: cn("flex flex-col gap-2", className),
|
|
57
|
+
...props,
|
|
58
|
+
children: [
|
|
59
|
+
/* @__PURE__ */ jsx("label", {
|
|
60
|
+
htmlFor: `${id}-value`,
|
|
61
|
+
className: "text-xs font-medium",
|
|
62
|
+
children: label
|
|
63
|
+
}),
|
|
64
|
+
/* @__PURE__ */ jsxs("div", {
|
|
65
|
+
className: "flex flex-wrap items-center gap-2",
|
|
66
|
+
children: [
|
|
67
|
+
/* @__PURE__ */ jsx(Input, {
|
|
68
|
+
id: `${id}-value`,
|
|
69
|
+
readOnly: true,
|
|
70
|
+
inputSize: "sm",
|
|
71
|
+
value: visible ? value : masked,
|
|
72
|
+
spellCheck: false,
|
|
73
|
+
autoComplete: "off",
|
|
74
|
+
"aria-describedby": `${id}-note`,
|
|
75
|
+
className: "min-w-0 flex-1 basis-64 font-mono",
|
|
76
|
+
onFocus: (event) => {
|
|
77
|
+
if (visible) event.currentTarget.select();
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
value !== void 0 && !once ? /* @__PURE__ */ jsx(Button, {
|
|
81
|
+
variant: "outline",
|
|
82
|
+
size: "sm",
|
|
83
|
+
"aria-pressed": revealed,
|
|
84
|
+
"aria-label": `${revealed ? "Hide" : "Reveal"} — ${label}`,
|
|
85
|
+
onClick: () => {
|
|
86
|
+
setReveal(!revealed);
|
|
87
|
+
},
|
|
88
|
+
children: revealed ? "Hide" : "Reveal"
|
|
89
|
+
}) : null,
|
|
90
|
+
value !== void 0 ? /* @__PURE__ */ jsx(Button, {
|
|
91
|
+
variant: "outline",
|
|
92
|
+
size: "sm",
|
|
93
|
+
"data-state": copy,
|
|
94
|
+
"aria-label": `Copy — ${label}`,
|
|
95
|
+
onClick: copyValue,
|
|
96
|
+
children: copy === "copied" ? "Copied" : "Copy"
|
|
97
|
+
}) : null,
|
|
98
|
+
state === "shown" && onAcknowledge ? /* @__PURE__ */ jsx(Button, {
|
|
99
|
+
variant: "primary",
|
|
100
|
+
size: "sm",
|
|
101
|
+
onClick: onAcknowledge,
|
|
102
|
+
children: "I have saved it"
|
|
103
|
+
}) : null,
|
|
104
|
+
onRegenerate && !confirming ? /* @__PURE__ */ jsx(Button, {
|
|
105
|
+
variant: "outline",
|
|
106
|
+
size: "sm",
|
|
107
|
+
"aria-label": `Regenerate — ${label}`,
|
|
108
|
+
onClick: () => {
|
|
109
|
+
setConfirming(true);
|
|
110
|
+
},
|
|
111
|
+
children: "Regenerate…"
|
|
112
|
+
}) : null
|
|
113
|
+
]
|
|
114
|
+
}),
|
|
115
|
+
/* @__PURE__ */ jsx("p", {
|
|
116
|
+
id: `${id}-note`,
|
|
117
|
+
"data-slot": "secret-field-note",
|
|
118
|
+
className: cn("text-xs", state === "shown" ? "text-warning" : "text-muted-foreground"),
|
|
119
|
+
children: note
|
|
120
|
+
}),
|
|
121
|
+
/* @__PURE__ */ jsx("span", {
|
|
122
|
+
role: "status",
|
|
123
|
+
"aria-live": "polite",
|
|
124
|
+
className: "sr-only",
|
|
125
|
+
children: copy === "copied" ? "Copied to the clipboard" : copy === "error" ? "Could not copy. Select the value and copy it by hand." : ""
|
|
126
|
+
}),
|
|
127
|
+
copy === "error" ? /* @__PURE__ */ jsx("p", {
|
|
128
|
+
"data-slot": "secret-field-copy-error",
|
|
129
|
+
className: "text-xs text-destructive",
|
|
130
|
+
children: "Could not copy. Select the value and copy it by hand."
|
|
131
|
+
}) : null,
|
|
132
|
+
children,
|
|
133
|
+
confirming ? /* @__PURE__ */ jsxs("div", {
|
|
134
|
+
"data-slot": "secret-field-confirm",
|
|
135
|
+
role: "group",
|
|
136
|
+
"aria-labelledby": `${id}-confirm`,
|
|
137
|
+
className: "flex flex-col gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-3",
|
|
138
|
+
children: [/* @__PURE__ */ jsx("p", {
|
|
139
|
+
id: `${id}-confirm`,
|
|
140
|
+
className: "text-xs text-destructive",
|
|
141
|
+
children: regenerateWarning
|
|
142
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
143
|
+
className: "flex flex-wrap gap-2",
|
|
144
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
145
|
+
variant: "destructive",
|
|
146
|
+
size: "sm",
|
|
147
|
+
onClick: () => {
|
|
148
|
+
setConfirming(false);
|
|
149
|
+
onRegenerate?.();
|
|
150
|
+
},
|
|
151
|
+
children: "Regenerate"
|
|
152
|
+
}), /* @__PURE__ */ jsx(Button, {
|
|
153
|
+
variant: "outline",
|
|
154
|
+
size: "sm",
|
|
155
|
+
onClick: () => {
|
|
156
|
+
setConfirming(false);
|
|
157
|
+
},
|
|
158
|
+
children: "Cancel"
|
|
159
|
+
})]
|
|
160
|
+
})]
|
|
161
|
+
}) : null
|
|
162
|
+
]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
export { SecretField, maskSecret };
|
|
167
|
+
|
|
168
|
+
//# sourceMappingURL=secret-field.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"secret-field.js","names":[],"sources":["../../../src/components/secret-field/secret-field.tsx"],"sourcesContent":["\"use client\";\n\nimport {\n useEffect,\n useId,\n useRef,\n useState,\n type ComponentPropsWithRef,\n type ReactNode,\n} from \"react\";\n\nimport { Button } from \"@/components/button\";\nimport { Input } from \"@/components/input\";\nimport { cn } from \"@/lib/utils\";\n\n/**\n * An API key, token or signing secret, in the three states it actually has.\n *\n * Stripe, GitHub, OpenAI, Vercel and every developer product with a settings\n * page draw this by hand, and the states are the same everywhere: shown once\n * at creation and never again; hidden but revealable, for a secret the server\n * can show again; and gone, where only a prefix and the last four remain and\n * the only thing left to do is regenerate. No component library ships it —\n * the nearest thing is a password input, which is a control for entering a\n * secret you know, not for handling one you have just been given.\n *\n * \"Shown once\" is a first-class state rather than a toast, because it is the\n * one that costs people money: the key is on screen, the tab closes, and the\n * next hour is spent regenerating it and updating every client. So the field\n * says it in a sentence beside the value, and the way out is a button that\n * says what it means — \"I have saved it\" — rather than the value quietly\n * vanishing on navigation.\n *\n * While hidden, the secret is not in the DOM. The preview is what is\n * rendered, so a screenshot, an extension or a devtools pane sees the prefix\n * and the last four, which is what the server itself keeps. Reveals are\n * reported, since an audit log of who looked is the reason the hidden state\n * exists. Copy works while hidden, because the point of the key is to be\n * pasted, not read.\n */\n\nexport type SecretFieldState = \"shown\" | \"hidden\" | \"revealed\" | \"gone\";\n\nexport interface SecretFieldProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n label: string;\n /** The secret. Omit once it can no longer be shown. */\n value?: string;\n /** What stands in for it while hidden or gone. Derived from the value when there is one. */\n preview?: string;\n /** This is the only time the value can be seen. */\n once?: boolean;\n /** The reader has saved a shown-once value. Drop `value` in response. */\n onAcknowledge?: () => void;\n /** Called each time the value is shown or hidden, so reveals can be logged. */\n onRevealChange?: (revealed: boolean) => void;\n /** Offers to regenerate. Confirmed inline first, because it revokes the current one. */\n onRegenerate?: () => void;\n /** Said before regenerating. */\n regenerateWarning?: string;\n /** Metadata — when it was created, when it was last used. */\n children?: ReactNode;\n}\n\n/**\n * A prefix and the last four, the way a server keeps a key it has hashed.\n * The prefix is kept through the last underscore in the first twelve\n * characters — `sk_live_`, `ghp_`, `whsec_` — so the reader can tell which\n * key this is without being shown any of it.\n */\nexport function maskSecret(value: string, keepEnd = 4): string {\n const underscore = value.slice(0, 12).lastIndexOf(\"_\");\n let prefix = underscore > 0 ? value.slice(0, underscore + 1) : value.slice(0, 4);\n // Never show enough of a short secret to reconstruct it.\n if (prefix.length + keepEnd + 4 > value.length) prefix = \"\";\n if (value.length < keepEnd + 4) return \"••••••••\";\n return `${prefix}…${value.slice(-keepEnd)}`;\n}\n\nexport function SecretField({\n className,\n label,\n value,\n preview,\n once = false,\n onAcknowledge,\n onRevealChange,\n onRegenerate,\n regenerateWarning = \"Regenerating revokes this one immediately. Anything still using it stops working.\",\n children,\n ...props\n}: SecretFieldProps) {\n const id = useId();\n const [revealed, setRevealed] = useState(false);\n const [confirming, setConfirming] = useState(false);\n const [copy, setCopy] = useState<\"idle\" | \"copied\" | \"error\">(\"idle\");\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n useEffect(\n () => () => {\n if (timer.current) clearTimeout(timer.current);\n },\n [],\n );\n\n const state: SecretFieldState =\n value === undefined ? \"gone\" : once ? \"shown\" : revealed ? \"revealed\" : \"hidden\";\n const visible = state === \"shown\" || state === \"revealed\";\n const masked = preview ?? (value !== undefined ? maskSecret(value) : \"••••••••\");\n\n const copyValue = () => {\n if (value === undefined) return;\n const settle = (next: \"copied\" | \"error\") => {\n setCopy(next);\n if (timer.current) clearTimeout(timer.current);\n timer.current = setTimeout(() => {\n setCopy(\"idle\");\n }, 2000);\n };\n // Wrapped so a missing clipboard API throws into the same failure path\n // as a refused one, rather than throwing out of the click handler.\n Promise.resolve()\n .then(() => navigator.clipboard.writeText(value))\n .then(\n () => {\n settle(\"copied\");\n },\n () => {\n settle(\"error\");\n },\n );\n };\n\n const setReveal = (next: boolean) => {\n setRevealed(next);\n onRevealChange?.(next);\n };\n\n const note =\n state === \"shown\"\n ? \"Shown once. Copy it now — it cannot be shown again.\"\n : state === \"gone\"\n ? \"Cannot be shown again.\"\n : state === \"hidden\"\n ? \"Hidden. Copy works without revealing it.\"\n : \"Revealed.\";\n\n return (\n <div\n data-slot=\"secret-field\"\n data-state={state}\n className={cn(\"flex flex-col gap-2\", className)}\n {...props}\n >\n <label htmlFor={`${id}-value`} className=\"text-xs font-medium\">\n {label}\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n {/* Read-only rather than disabled, so it can be focused, selected and\n read. While hidden the DOM holds the preview, not the secret. */}\n <Input\n id={`${id}-value`}\n readOnly\n inputSize=\"sm\"\n value={visible ? value : masked}\n spellCheck={false}\n autoComplete=\"off\"\n aria-describedby={`${id}-note`}\n // Claims a readable width first; the buttons wrap under it when the\n // row is short, rather than the key being squeezed to a fragment.\n className=\"min-w-0 flex-1 basis-64 font-mono\"\n onFocus={(event) => {\n if (visible) event.currentTarget.select();\n }}\n />\n\n {value !== undefined && !once ? (\n <Button\n variant=\"outline\"\n size=\"sm\"\n aria-pressed={revealed}\n aria-label={`${revealed ? \"Hide\" : \"Reveal\"} — ${label}`}\n onClick={() => {\n setReveal(!revealed);\n }}\n >\n {revealed ? \"Hide\" : \"Reveal\"}\n </Button>\n ) : null}\n\n {value !== undefined ? (\n <Button\n variant=\"outline\"\n size=\"sm\"\n data-state={copy}\n aria-label={`Copy — ${label}`}\n onClick={copyValue}\n >\n {copy === \"copied\" ? \"Copied\" : \"Copy\"}\n </Button>\n ) : null}\n\n {state === \"shown\" && onAcknowledge ? (\n <Button variant=\"primary\" size=\"sm\" onClick={onAcknowledge}>\n I have saved it\n </Button>\n ) : null}\n\n {onRegenerate && !confirming ? (\n <Button\n variant=\"outline\"\n size=\"sm\"\n aria-label={`Regenerate — ${label}`}\n onClick={() => {\n setConfirming(true);\n }}\n >\n Regenerate…\n </Button>\n ) : null}\n </div>\n\n <p\n id={`${id}-note`}\n data-slot=\"secret-field-note\"\n className={cn(\"text-xs\", state === \"shown\" ? \"text-warning\" : \"text-muted-foreground\")}\n >\n {note}\n </p>\n\n {/* Present from the start, so the first copy is heard. Failure is said\n too: the clipboard can be refused, and a button that only ever says\n \"Copied\" would be lying the one time it matters. */}\n <span role=\"status\" aria-live=\"polite\" className=\"sr-only\">\n {copy === \"copied\"\n ? \"Copied to the clipboard\"\n : copy === \"error\"\n ? \"Could not copy. Select the value and copy it by hand.\"\n : \"\"}\n </span>\n {copy === \"error\" ? (\n <p data-slot=\"secret-field-copy-error\" className=\"text-xs text-destructive\">\n Could not copy. Select the value and copy it by hand.\n </p>\n ) : null}\n\n {children}\n\n {confirming ? (\n <div\n data-slot=\"secret-field-confirm\"\n role=\"group\"\n aria-labelledby={`${id}-confirm`}\n className=\"flex flex-col gap-2 rounded-md border border-destructive/40 bg-destructive/5 p-3\"\n >\n <p id={`${id}-confirm`} className=\"text-xs text-destructive\">\n {regenerateWarning}\n </p>\n <div className=\"flex flex-wrap gap-2\">\n <Button\n variant=\"destructive\"\n size=\"sm\"\n onClick={() => {\n setConfirming(false);\n onRegenerate?.();\n }}\n >\n Regenerate\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={() => {\n setConfirming(false);\n }}\n >\n Cancel\n </Button>\n </div>\n </div>\n ) : null}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { SessionExpiry, SessionExpiryProps, SessionPhase, describeCountdown, formatCountdown, secondsLeft, sessionPhase } from "./session-expiry.js";
|
|
2
|
+
export { SessionExpiry, type SessionExpiryProps, type SessionPhase, describeCountdown, formatCountdown, secondsLeft, sessionPhase };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { defineMeta } from "../../registry/schema.js";
|
|
2
|
+
//#region src/components/session-expiry/meta.ts
|
|
3
|
+
const meta = defineMeta({
|
|
4
|
+
name: "session-expiry",
|
|
5
|
+
title: "Session Expiry",
|
|
6
|
+
description: "The idle-timeout warning WCAG 2.2.1 asks for: warned, extendable, and not dismissable by accident.",
|
|
7
|
+
category: "feedback",
|
|
8
|
+
status: "stable",
|
|
9
|
+
dependencies: [],
|
|
10
|
+
registryDependencies: ["button", "dialog"],
|
|
11
|
+
files: ["session-expiry.tsx"],
|
|
12
|
+
a11y: "An alert dialog, named by its heading and described by the sentence that says how long is left. Escape and the backdrop do nothing, because dismissing a session warning without choosing is choosing nothing; the two buttons are the way out, and focus opens on the safe one. The visible countdown is not live — a number every second for two minutes drowns the question — and a separate status region speaks four times: when the warning opens, at one minute, thirty seconds and ten, still saying a threshold a slow tick skipped over. The clock is read in an effect, so the server renders nothing rather than a countdown from the wrong instant. This satisfies the warn-and-extend half of WCAG 2.2.1 Timing Adjustable; the twenty-second minimum is the default window's business and the application's."
|
|
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/session-expiry/meta.ts"],"sourcesContent":["import { defineMeta } from \"@/registry/schema\";\n\nexport const meta = defineMeta({\n name: \"session-expiry\",\n title: \"Session Expiry\",\n description:\n \"The idle-timeout warning WCAG 2.2.1 asks for: warned, extendable, and not dismissable by accident.\",\n category: \"feedback\",\n status: \"stable\",\n dependencies: [],\n registryDependencies: [\"button\", \"dialog\"],\n files: [\"session-expiry.tsx\"],\n a11y:\n \"An alert dialog, named by its heading and described by the sentence that says how long is \" +\n \"left. Escape and the backdrop do nothing, because dismissing a session warning without \" +\n \"choosing is choosing nothing; the two buttons are the way out, and focus opens on the safe \" +\n \"one. The visible countdown is not live — a number every second for two minutes drowns the \" +\n \"question — and a separate status region speaks four times: when the warning opens, at one \" +\n \"minute, thirty seconds and ten, still saying a threshold a slow tick skipped over. The \" +\n \"clock is read in an effect, so the server renders nothing rather than a countdown from the \" +\n \"wrong instant. This satisfies the warn-and-extend half of WCAG 2.2.1 Timing Adjustable; the \" +\n \"twenty-second minimum is the default window's business and the application's.\",\n});\n"],"mappings":";;AAEA,MAAa,OAAO,WAAW;CAC7B,MAAM;CACN,OAAO;CACP,aACE;CACF,UAAU;CACV,QAAQ;CACR,cAAc,CAAC;CACf,sBAAsB,CAAC,UAAU,QAAQ;CACzC,OAAO,CAAC,oBAAoB;CAC5B,MACE;AASJ,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/components/session-expiry/session-expiry.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* "Your session ends in two minutes. Stay signed in?"
|
|
5
|
+
*
|
|
6
|
+
* Every product with an idle timeout builds this, and WCAG 2.2.1 (Timing
|
|
7
|
+
* Adjustable) says what it has to do: warn before the time runs out, and
|
|
8
|
+
* give at least twenty seconds to extend it with a simple action. Most
|
|
9
|
+
* implementations get the first half — a toast, a modal — and fail the
|
|
10
|
+
* second in one of two ways: the warning can be dismissed without choosing,
|
|
11
|
+
* so a reader who closed it to see the page underneath is signed out with
|
|
12
|
+
* no further word; or the countdown is a live region, so a screen reader
|
|
13
|
+
* user hears a number every second for two minutes and cannot hear the
|
|
14
|
+
* choice they are being asked to make.
|
|
15
|
+
*
|
|
16
|
+
* So this is an alert dialog that cannot be waved away. Escape and the
|
|
17
|
+
* backdrop do nothing, because dismissing a session warning without
|
|
18
|
+
* choosing is choosing nothing, and the two buttons are the two things a
|
|
19
|
+
* person can actually do. The countdown ticks on screen and is announced at
|
|
20
|
+
* four moments — when the warning opens, at one minute, thirty seconds and
|
|
21
|
+
* ten — which is enough to know how long is left and quiet enough to hear
|
|
22
|
+
* the question.
|
|
23
|
+
*
|
|
24
|
+
* When time runs out, `onExpire` fires once and the dialog says so, with a
|
|
25
|
+
* slot for whatever the application offers next. It does not sign anyone
|
|
26
|
+
* out; the server did that, and this is the part that tells the person.
|
|
27
|
+
*/
|
|
28
|
+
type SessionPhase = "active" | "warning" | "expired";
|
|
29
|
+
/** Where a session stands, given the clock. Pure, for tests and for the server. */
|
|
30
|
+
declare function sessionPhase(expiresAt: Date, now: Date, warnBefore: number): SessionPhase;
|
|
31
|
+
/** Whole seconds left, never negative. */
|
|
32
|
+
declare function secondsLeft(expiresAt: Date, now: Date): number;
|
|
33
|
+
/** "1:59" for the screen. */
|
|
34
|
+
declare function formatCountdown(seconds: number): string;
|
|
35
|
+
/** "1 minute 59 seconds" for the ear. */
|
|
36
|
+
declare function describeCountdown(seconds: number): string;
|
|
37
|
+
interface SessionExpiryProps {
|
|
38
|
+
/** When the session ends. Move it forward after a successful extend. */
|
|
39
|
+
expiresAt: Date;
|
|
40
|
+
/** How long before the end to warn, in milliseconds. Two minutes by default. */
|
|
41
|
+
warnBefore?: number;
|
|
42
|
+
/**
|
|
43
|
+
* The clock. Supply it to drive the countdown yourself, or in tests; omit
|
|
44
|
+
* and the component ticks once a second while mounted.
|
|
45
|
+
*/
|
|
46
|
+
now?: Date;
|
|
47
|
+
/** Keep the session going. May return a promise; the button waits for it. */
|
|
48
|
+
onExtend: () => void | Promise<void>;
|
|
49
|
+
/** Called once when the time runs out. */
|
|
50
|
+
onExpire?: () => void;
|
|
51
|
+
/** Offered as the other choice. Omit to offer only staying signed in. */
|
|
52
|
+
onSignOut?: () => void;
|
|
53
|
+
/** The warning's heading. */
|
|
54
|
+
heading?: string;
|
|
55
|
+
/** What is at stake, in the reader's language: "Unsaved edits will be lost." */
|
|
56
|
+
children?: ReactNode;
|
|
57
|
+
/** What to offer once the session has ended: a sign-in link, usually. */
|
|
58
|
+
expiredAction?: ReactNode;
|
|
59
|
+
className?: string;
|
|
60
|
+
}
|
|
61
|
+
declare function SessionExpiry({ expiresAt, warnBefore, now: nowProp, onExtend, onExpire, onSignOut, heading, children, expiredAction, className }: SessionExpiryProps): import("react").JSX.Element;
|
|
62
|
+
//#endregion
|
|
63
|
+
export { SessionExpiry, SessionExpiryProps, SessionPhase, describeCountdown, formatCountdown, secondsLeft, sessionPhase };
|
|
64
|
+
//# sourceMappingURL=session-expiry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-expiry.d.ts","names":[],"sources":["../../../src/components/session-expiry/session-expiry.tsx"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;KAiDY;;iBAGI,aAAa,WAAW,MAAM,KAAK,MAAM,qBAAqB;;iBAQ9D,YAAY,WAAW,MAAM,KAAK;;iBAKlC,gBAAgB;;iBAOhB,kBAAkB;UAuBjB;;EAEf,WAAW;;EAEX;;;;;EAKA,MAAM;;EAEN,uBAAuB;;EAEvB;;EAEA;;EAEA;;EAEA,WAAW;;EAEX,gBAAgB;EAChB;;iBAGc,gBACd,WACA,YACA,KAAK,SACL,UACA,UACA,WACA,SACA,UACA,eACA,aACC,qCAAkB,IAAA"}
|