@cratis/scene.blueprint.default 4.35.0 → 4.37.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.
Files changed (175) hide show
  1. package/ComponentName.ts +21 -0
  2. package/defaultBlueprintComponents.ts +8 -1
  3. package/dist/cjs/ComponentName.js +7 -0
  4. package/dist/cjs/ComponentName.js.map +1 -1
  5. package/dist/cjs/defaultBlueprint.js +2 -0
  6. package/dist/cjs/defaultBlueprint.js.map +1 -1
  7. package/dist/cjs/defaultBlueprintComponentMetadata.js +2 -0
  8. package/dist/cjs/defaultBlueprintComponentMetadata.js.map +1 -1
  9. package/dist/cjs/defaultBlueprintComponents.js +15 -1
  10. package/dist/cjs/defaultBlueprintComponents.js.map +1 -1
  11. package/dist/cjs/foundation/ActivityFeed.js +122 -0
  12. package/dist/cjs/foundation/ActivityFeed.js.map +1 -0
  13. package/dist/cjs/foundation/ApprovalChainStepper.js +130 -0
  14. package/dist/cjs/foundation/ApprovalChainStepper.js.map +1 -0
  15. package/dist/cjs/foundation/AvatarStack.js +73 -0
  16. package/dist/cjs/foundation/AvatarStack.js.map +1 -0
  17. package/dist/cjs/foundation/ChecklistSection.js +174 -0
  18. package/dist/cjs/foundation/ChecklistSection.js.map +1 -0
  19. package/dist/cjs/foundation/FileUploader.js +218 -0
  20. package/dist/cjs/foundation/FileUploader.js.map +1 -0
  21. package/dist/cjs/foundation/FileUploaderHandler.js +17 -0
  22. package/dist/cjs/foundation/FileUploaderHandler.js.map +1 -0
  23. package/dist/cjs/foundation/OrgTree.js +188 -0
  24. package/dist/cjs/foundation/OrgTree.js.map +1 -0
  25. package/dist/cjs/foundation/PermissionMatrix.js +136 -0
  26. package/dist/cjs/foundation/PermissionMatrix.js.map +1 -0
  27. package/dist/cjs/foundation/activityModel.js +63 -0
  28. package/dist/cjs/foundation/activityModel.js.map +1 -0
  29. package/dist/cjs/foundation/approvalModel.js +87 -0
  30. package/dist/cjs/foundation/approvalModel.js.map +1 -0
  31. package/dist/cjs/foundation/avatarModel.js +42 -0
  32. package/dist/cjs/foundation/avatarModel.js.map +1 -0
  33. package/dist/cjs/foundation/checklistModel.js +59 -0
  34. package/dist/cjs/foundation/checklistModel.js.map +1 -0
  35. package/dist/cjs/foundation/formatting.js +11 -0
  36. package/dist/cjs/foundation/formatting.js.map +1 -1
  37. package/dist/cjs/foundation/foundationComponentMetadata.js +47 -0
  38. package/dist/cjs/foundation/foundationComponentMetadata.js.map +1 -1
  39. package/dist/cjs/foundation/foundationDescriptors.js +246 -0
  40. package/dist/cjs/foundation/foundationDescriptors.js.map +1 -1
  41. package/dist/cjs/foundation/foundationSamples.js +230 -0
  42. package/dist/cjs/foundation/foundationSamples.js.map +1 -1
  43. package/dist/cjs/foundation/foundationStrings.js +27 -1
  44. package/dist/cjs/foundation/foundationStrings.js.map +1 -1
  45. package/dist/cjs/foundation/orgModel.js +106 -0
  46. package/dist/cjs/foundation/orgModel.js.map +1 -0
  47. package/dist/cjs/foundation/permissionModel.js +84 -0
  48. package/dist/cjs/foundation/permissionModel.js.map +1 -0
  49. package/dist/cjs/foundation/uploadModel.js +71 -0
  50. package/dist/cjs/foundation/uploadModel.js.map +1 -0
  51. package/dist/cjs/index.js +59 -0
  52. package/dist/cjs/index.js.map +1 -1
  53. package/dist/esm/ComponentName.d.ts +8 -1
  54. package/dist/esm/ComponentName.d.ts.map +1 -1
  55. package/dist/esm/ComponentName.js +7 -0
  56. package/dist/esm/ComponentName.js.map +1 -1
  57. package/dist/esm/defaultBlueprint.js +2 -0
  58. package/dist/esm/defaultBlueprint.js.map +1 -1
  59. package/dist/esm/defaultBlueprintComponentMetadata.js +2 -0
  60. package/dist/esm/defaultBlueprintComponentMetadata.js.map +1 -1
  61. package/dist/esm/defaultBlueprintComponents.d.ts.map +1 -1
  62. package/dist/esm/defaultBlueprintComponents.js +15 -1
  63. package/dist/esm/defaultBlueprintComponents.js.map +1 -1
  64. package/dist/esm/foundation/ActivityFeed.d.ts +3 -0
  65. package/dist/esm/foundation/ActivityFeed.d.ts.map +1 -0
  66. package/dist/esm/foundation/ActivityFeed.js +120 -0
  67. package/dist/esm/foundation/ActivityFeed.js.map +1 -0
  68. package/dist/esm/foundation/ApprovalChainStepper.d.ts +3 -0
  69. package/dist/esm/foundation/ApprovalChainStepper.d.ts.map +1 -0
  70. package/dist/esm/foundation/ApprovalChainStepper.js +128 -0
  71. package/dist/esm/foundation/ApprovalChainStepper.js.map +1 -0
  72. package/dist/esm/foundation/AvatarStack.d.ts +3 -0
  73. package/dist/esm/foundation/AvatarStack.d.ts.map +1 -0
  74. package/dist/esm/foundation/AvatarStack.js +71 -0
  75. package/dist/esm/foundation/AvatarStack.js.map +1 -0
  76. package/dist/esm/foundation/ChecklistSection.d.ts +3 -0
  77. package/dist/esm/foundation/ChecklistSection.d.ts.map +1 -0
  78. package/dist/esm/foundation/ChecklistSection.js +172 -0
  79. package/dist/esm/foundation/ChecklistSection.js.map +1 -0
  80. package/dist/esm/foundation/FileUploader.d.ts +3 -0
  81. package/dist/esm/foundation/FileUploader.d.ts.map +1 -0
  82. package/dist/esm/foundation/FileUploader.js +216 -0
  83. package/dist/esm/foundation/FileUploader.js.map +1 -0
  84. package/dist/esm/foundation/FileUploaderHandler.d.ts +8 -0
  85. package/dist/esm/foundation/FileUploaderHandler.d.ts.map +1 -0
  86. package/dist/esm/foundation/FileUploaderHandler.js +14 -0
  87. package/dist/esm/foundation/FileUploaderHandler.js.map +1 -0
  88. package/dist/esm/foundation/Foundation.stories.d.ts +16 -0
  89. package/dist/esm/foundation/Foundation.stories.d.ts.map +1 -1
  90. package/dist/esm/foundation/Foundation.stories.js +16 -0
  91. package/dist/esm/foundation/Foundation.stories.js.map +1 -1
  92. package/dist/esm/foundation/OrgTree.d.ts +3 -0
  93. package/dist/esm/foundation/OrgTree.d.ts.map +1 -0
  94. package/dist/esm/foundation/OrgTree.js +186 -0
  95. package/dist/esm/foundation/OrgTree.js.map +1 -0
  96. package/dist/esm/foundation/PermissionMatrix.d.ts +3 -0
  97. package/dist/esm/foundation/PermissionMatrix.d.ts.map +1 -0
  98. package/dist/esm/foundation/PermissionMatrix.js +134 -0
  99. package/dist/esm/foundation/PermissionMatrix.js.map +1 -0
  100. package/dist/esm/foundation/activityModel.d.ts +22 -0
  101. package/dist/esm/foundation/activityModel.d.ts.map +1 -0
  102. package/dist/esm/foundation/activityModel.js +58 -0
  103. package/dist/esm/foundation/activityModel.js.map +1 -0
  104. package/dist/esm/foundation/approvalModel.d.ts +27 -0
  105. package/dist/esm/foundation/approvalModel.d.ts.map +1 -0
  106. package/dist/esm/foundation/approvalModel.js +81 -0
  107. package/dist/esm/foundation/approvalModel.js.map +1 -0
  108. package/dist/esm/foundation/avatarModel.d.ts +11 -0
  109. package/dist/esm/foundation/avatarModel.d.ts.map +1 -0
  110. package/dist/esm/foundation/avatarModel.js +38 -0
  111. package/dist/esm/foundation/avatarModel.js.map +1 -0
  112. package/dist/esm/foundation/checklistModel.d.ts +17 -0
  113. package/dist/esm/foundation/checklistModel.d.ts.map +1 -0
  114. package/dist/esm/foundation/checklistModel.js +53 -0
  115. package/dist/esm/foundation/checklistModel.js.map +1 -0
  116. package/dist/esm/foundation/formatting.d.ts +1 -0
  117. package/dist/esm/foundation/formatting.d.ts.map +1 -1
  118. package/dist/esm/foundation/formatting.js +11 -1
  119. package/dist/esm/foundation/formatting.js.map +1 -1
  120. package/dist/esm/foundation/foundationComponentMetadata.d.ts.map +1 -1
  121. package/dist/esm/foundation/foundationComponentMetadata.js +47 -0
  122. package/dist/esm/foundation/foundationComponentMetadata.js.map +1 -1
  123. package/dist/esm/foundation/foundationDescriptors.d.ts.map +1 -1
  124. package/dist/esm/foundation/foundationDescriptors.js +246 -0
  125. package/dist/esm/foundation/foundationDescriptors.js.map +1 -1
  126. package/dist/esm/foundation/foundationSamples.d.ts.map +1 -1
  127. package/dist/esm/foundation/foundationSamples.js +230 -0
  128. package/dist/esm/foundation/foundationSamples.js.map +1 -1
  129. package/dist/esm/foundation/foundationStrings.d.ts +26 -0
  130. package/dist/esm/foundation/foundationStrings.d.ts.map +1 -1
  131. package/dist/esm/foundation/foundationStrings.js +27 -1
  132. package/dist/esm/foundation/foundationStrings.js.map +1 -1
  133. package/dist/esm/foundation/index.d.ts +15 -0
  134. package/dist/esm/foundation/index.d.ts.map +1 -1
  135. package/dist/esm/foundation/index.js +15 -0
  136. package/dist/esm/foundation/index.js.map +1 -1
  137. package/dist/esm/foundation/orgModel.d.ts +20 -0
  138. package/dist/esm/foundation/orgModel.d.ts.map +1 -0
  139. package/dist/esm/foundation/orgModel.js +99 -0
  140. package/dist/esm/foundation/orgModel.js.map +1 -0
  141. package/dist/esm/foundation/permissionModel.d.ts +24 -0
  142. package/dist/esm/foundation/permissionModel.d.ts.map +1 -0
  143. package/dist/esm/foundation/permissionModel.js +77 -0
  144. package/dist/esm/foundation/permissionModel.js.map +1 -0
  145. package/dist/esm/foundation/uploadModel.d.ts +31 -0
  146. package/dist/esm/foundation/uploadModel.d.ts.map +1 -0
  147. package/dist/esm/foundation/uploadModel.js +65 -0
  148. package/dist/esm/foundation/uploadModel.js.map +1 -0
  149. package/dist/esm/index.js +16 -1
  150. package/dist/esm/index.js.map +1 -1
  151. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  152. package/foundation/ActivityFeed.tsx +61 -0
  153. package/foundation/ApprovalChainStepper.tsx +66 -0
  154. package/foundation/AvatarStack.tsx +43 -0
  155. package/foundation/ChecklistSection.tsx +80 -0
  156. package/foundation/FileUploader.tsx +122 -0
  157. package/foundation/FileUploaderHandler.tsx +22 -0
  158. package/foundation/Foundation.stories.tsx +16 -0
  159. package/foundation/OrgTree.tsx +101 -0
  160. package/foundation/PermissionMatrix.tsx +82 -0
  161. package/foundation/activityModel.ts +65 -0
  162. package/foundation/approvalModel.ts +88 -0
  163. package/foundation/avatarModel.ts +36 -0
  164. package/foundation/checklistModel.ts +62 -0
  165. package/foundation/formatting.ts +9 -0
  166. package/foundation/foundationComponentMetadata.ts +7 -0
  167. package/foundation/foundationDescriptors.ts +153 -0
  168. package/foundation/foundationSamples.ts +59 -0
  169. package/foundation/foundationStrings.ts +26 -0
  170. package/foundation/index.ts +15 -0
  171. package/foundation/orgModel.ts +85 -0
  172. package/foundation/permissionModel.ts +85 -0
  173. package/foundation/uploadModel.ts +66 -0
  174. package/package.json +8 -8
  175. package/shell/layout.css +418 -0
@@ -0,0 +1,65 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** What kind of thing happened, which decides the icon and the word that tells it apart without color. */
5
+ export enum ActivityKind {
6
+ Comment = 'comment',
7
+ Change = 'change',
8
+ Status = 'status',
9
+ System = 'system',
10
+ }
11
+
12
+ /** One thing that happened to a record. */
13
+ export interface ActivityEntry {
14
+ id: string;
15
+ actor: string;
16
+ action: string;
17
+ kind: ActivityKind;
18
+ at: string;
19
+ body?: string;
20
+ }
21
+
22
+ /** The entries of one day, in the order the feed shows them. */
23
+ export interface ActivityDay {
24
+ day: string;
25
+ entries: ActivityEntry[];
26
+ }
27
+
28
+ /** Reads entries out of a property value, dropping those without an id or a time that parses. */
29
+ export function readActivity(value: unknown): ActivityEntry[] {
30
+ if (!Array.isArray(value)) return [];
31
+
32
+ return value.flatMap((entry): ActivityEntry[] => {
33
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) return [];
34
+ const { id, actor, action, kind, at, body } = entry as Record<string, unknown>;
35
+ if (typeof id !== 'string' || typeof at !== 'string' || Number.isNaN(new Date(at).getTime())) return [];
36
+
37
+ return [{
38
+ id, at,
39
+ actor: typeof actor === 'string' ? actor : '',
40
+ action: typeof action === 'string' ? action : '',
41
+ kind: (Object.values(ActivityKind) as string[]).includes(kind as string) ? (kind as ActivityKind) : ActivityKind.System,
42
+ ...(typeof body === 'string' && body.length > 0 ? { body } : {}),
43
+ }];
44
+ });
45
+ }
46
+
47
+ /** The calendar day of an instant in a time zone, as `YYYY-MM-DD`. */
48
+ export function dayOf(at: string, timeZone?: string): string {
49
+ return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(at));
50
+ }
51
+
52
+ /** Sorts the entries by time (newest first unless `ascending`) and groups them by calendar day, so a long feed has headings to scan by. */
53
+ export function groupByDay(entries: ActivityEntry[], ascending = false, timeZone?: string): ActivityDay[] {
54
+ const sorted = [...entries].sort((first, second) => (new Date(first.at).getTime() - new Date(second.at).getTime()) * (ascending ? 1 : -1));
55
+ const days: ActivityDay[] = [];
56
+
57
+ for (const entry of sorted) {
58
+ const day = dayOf(entry.at, timeZone);
59
+ const last = days[days.length - 1];
60
+ if (last?.day === day) last.entries.push(entry);
61
+ else days.push({ day, entries: [entry] });
62
+ }
63
+
64
+ return days;
65
+ }
@@ -0,0 +1,88 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** Where one step of an approval chain stands. */
5
+ export enum ApprovalState {
6
+ Pending = 'pending',
7
+ Current = 'current',
8
+ Approved = 'approved',
9
+ Rejected = 'rejected',
10
+ Skipped = 'skipped',
11
+ }
12
+
13
+ /** One step of an approval chain: who decides, and what they decided. */
14
+ export interface ApprovalStep {
15
+ label: string;
16
+ approver?: string;
17
+ state: ApprovalState;
18
+ at?: string;
19
+ comment?: string;
20
+ }
21
+
22
+ /** A step as it is drawn, after the chain's own rules are applied. */
23
+ export interface EffectiveStep extends ApprovalStep {
24
+ /** True when an earlier rejection stopped the chain before this step was reached. */
25
+ notReached: boolean;
26
+ }
27
+
28
+ /** How the chain as a whole stands. */
29
+ export enum ChainOutcome {
30
+ /** Every step approved. */
31
+ Approved = 'approved',
32
+
33
+ /** A step rejected: the chain stops there. */
34
+ Rejected = 'rejected',
35
+
36
+ /** At least one step is decided and the rest are waiting. */
37
+ InProgress = 'inProgress',
38
+
39
+ /** Nothing decided yet. */
40
+ Pending = 'pending',
41
+ }
42
+
43
+ /** Reads steps out of a property value, dropping entries without a label; an unknown state is pending. */
44
+ export function readSteps(value: unknown): ApprovalStep[] {
45
+ if (!Array.isArray(value)) return [];
46
+
47
+ return value.flatMap((entry): ApprovalStep[] => {
48
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) return [];
49
+ const { label, approver, state, at, comment } = entry as Record<string, unknown>;
50
+ if (typeof label !== 'string' || label.length === 0) return [];
51
+
52
+ return [{
53
+ label,
54
+ state: (Object.values(ApprovalState) as string[]).includes(state as string) ? (state as ApprovalState) : ApprovalState.Pending,
55
+ ...(typeof approver === 'string' ? { approver } : {}),
56
+ ...(typeof at === 'string' ? { at } : {}),
57
+ ...(typeof comment === 'string' && comment.length > 0 ? { comment } : {}),
58
+ }];
59
+ });
60
+ }
61
+
62
+ /**
63
+ * Applies the chain's rules to the steps as written.
64
+ *
65
+ * A rejection stops the chain: every step after the first rejected one has not been reached, so it is drawn skipped however it was written.
66
+ * With no step marked current and no rejection, the first step that is not yet decided is the current one - the person the chain is waiting
67
+ * for - so a chain written only as approved and pending steps still says whose turn it is. Only one step is ever current.
68
+ */
69
+ export function effectiveSteps(steps: ApprovalStep[]): EffectiveStep[] {
70
+ const rejected = steps.findIndex(step => step.state === ApprovalState.Rejected);
71
+ const explicitCurrent = steps.findIndex(step => step.state === ApprovalState.Current);
72
+ const waiting = rejected >= 0 ? -1 : explicitCurrent >= 0 ? explicitCurrent : steps.findIndex(step => step.state === ApprovalState.Pending);
73
+
74
+ return steps.map((step, index) => {
75
+ if (rejected >= 0 && index > rejected) return { ...step, state: ApprovalState.Skipped, notReached: true };
76
+ if (step.state === ApprovalState.Current && index !== waiting) return { ...step, state: ApprovalState.Pending, notReached: false };
77
+ if (index === waiting && step.state === ApprovalState.Pending) return { ...step, state: ApprovalState.Current, notReached: false };
78
+ return { ...step, notReached: false };
79
+ });
80
+ }
81
+
82
+ /** The state of the chain as a whole. A skipped step counts as decided, because it was never going to be asked. */
83
+ export function chainOutcome(steps: ApprovalStep[]): ChainOutcome {
84
+ const effective = effectiveSteps(steps);
85
+ if (effective.some(step => step.state === ApprovalState.Rejected)) return ChainOutcome.Rejected;
86
+ if (effective.length > 0 && effective.every(step => step.state === ApprovalState.Approved || step.state === ApprovalState.Skipped)) return ChainOutcome.Approved;
87
+ return effective.some(step => step.state === ApprovalState.Approved) ? ChainOutcome.InProgress : ChainOutcome.Pending;
88
+ }
@@ -0,0 +1,36 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** A person shown as an avatar. */
5
+ export interface AvatarPerson {
6
+ name: string;
7
+ imageUrl?: string;
8
+ }
9
+
10
+ /** Reads people out of a property value, dropping entries without a name. */
11
+ export function readPeople(value: unknown): AvatarPerson[] {
12
+ if (!Array.isArray(value)) return [];
13
+
14
+ return value.flatMap((entry): AvatarPerson[] => {
15
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) return [];
16
+ const { name, imageUrl } = entry as Record<string, unknown>;
17
+ if (typeof name !== 'string' || name.trim().length === 0) return [];
18
+ return [{ name: name.trim(), ...(typeof imageUrl === 'string' && imageUrl.length > 0 ? { imageUrl } : {}) }];
19
+ });
20
+ }
21
+
22
+ /** The initials of a name: the first letter of the first and last words, so "Ada King Lovelace" is AL and "Ada" is A. */
23
+ export function initialsOf(name: string): string {
24
+ const words = name.split(/\s+/).filter(word => word.length > 0);
25
+ if (words.length === 0) return '';
26
+
27
+ const first = Array.from(words[0])[0] ?? '';
28
+ const last = words.length > 1 ? Array.from(words[words.length - 1])[0] ?? '' : '';
29
+ return `${first}${last}`.toLocaleUpperCase();
30
+ }
31
+
32
+ /** Splits people into those shown and how many are folded into the "+N" chip; at least one is always shown when there are any. */
33
+ export function visiblePeople(people: AvatarPerson[], maximum: number): { shown: AvatarPerson[]; hidden: number } {
34
+ const limit = Math.max(1, Math.floor(maximum));
35
+ return people.length <= limit ? { shown: people, hidden: 0 } : { shown: people.slice(0, limit), hidden: people.length - limit };
36
+ }
@@ -0,0 +1,62 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ /** One task of a checklist. */
5
+ export interface ChecklistItem {
6
+ id: string;
7
+ label: string;
8
+ owner?: string;
9
+ due?: string;
10
+ done: boolean;
11
+ }
12
+
13
+ /** A titled group of tasks. */
14
+ export interface ChecklistGroup {
15
+ title: string;
16
+ items: ChecklistItem[];
17
+ }
18
+
19
+ /** Reads checklist groups out of a property value; a task needs an id and a label, and is open unless it says it is done. */
20
+ export function readChecklist(value: unknown): ChecklistGroup[] {
21
+ if (!Array.isArray(value)) return [];
22
+
23
+ return value.flatMap((entry): ChecklistGroup[] => {
24
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) return [];
25
+ const { title, items } = entry as Record<string, unknown>;
26
+ if (typeof title !== 'string' || !Array.isArray(items)) return [];
27
+
28
+ return [{
29
+ title,
30
+ items: items.flatMap((item): ChecklistItem[] => {
31
+ if (typeof item !== 'object' || item === null || Array.isArray(item)) return [];
32
+ const { id, label, owner, due, done } = item as Record<string, unknown>;
33
+ if (typeof id !== 'string' || typeof label !== 'string') return [];
34
+ return [{ id, label, done: done === true, ...(typeof owner === 'string' ? { owner } : {}), ...(typeof due === 'string' ? { due } : {}) }];
35
+ }),
36
+ }];
37
+ });
38
+ }
39
+
40
+ /** How many of a group's tasks are done. */
41
+ export const completed = (group: ChecklistGroup): number => group.items.filter(item => item.done).length;
42
+
43
+ /** Whether an open task is past its due date, today counting as not yet late. */
44
+ export function isOverdue(item: ChecklistItem, now: Date): boolean {
45
+ if (item.done || item.due === undefined) return false;
46
+
47
+ const due = new Date(item.due);
48
+ if (Number.isNaN(due.getTime())) return false;
49
+
50
+ const endOfDue = new Date(due);
51
+ endOfDue.setUTCHours(23, 59, 59, 999);
52
+ return endOfDue.getTime() < now.getTime();
53
+ }
54
+
55
+ /** Marks one task done or open, leaving every other as it was. */
56
+ export const toggleTask = (groups: ChecklistGroup[], id: string): ChecklistGroup[] =>
57
+ groups.map(group => (group.items.some(item => item.id === id)
58
+ ? { ...group, items: group.items.map(item => (item.id === id ? { ...item, done: !item.done } : item)) }
59
+ : group));
60
+
61
+ /** The ids of every done task: what the checklist publishes. */
62
+ export const doneTaskIds = (groups: ChecklistGroup[]): string[] => groups.flatMap(group => group.items.filter(item => item.done).map(item => item.id));
@@ -67,3 +67,12 @@ export function formatDate(value: unknown, locale: string, withTime = false): st
67
67
  return date.toISOString();
68
68
  }
69
69
  }
70
+
71
+ /** Formats a calendar day (`YYYY-MM-DD`) for a locale, as that day and never shifted by the reader's time zone. */
72
+ export function formatDay(day: string, locale: string): string {
73
+ try {
74
+ return new Intl.DateTimeFormat(locale, { dateStyle: 'full', timeZone: 'UTC' }).format(new Date(`${day}T12:00:00Z`));
75
+ } catch {
76
+ return day;
77
+ }
78
+ }
@@ -18,4 +18,11 @@ export const foundationComponentMetadata: Record<string, ComponentMetadata> = {
18
18
  [ComponentName.FilterBuilder]: m('Filter builder', ComponentCategory.Input, 'list-filter', ['filter', 'query', 'conditions', 'and or', 'search']),
19
19
  [ComponentName.ColumnChooser]: m('Column chooser', ComponentCategory.Actions, 'columns-3', ['columns', 'fields', 'show hide', 'table']),
20
20
  [ComponentName.SavedViewsMenu]: m('Saved views menu', ComponentCategory.Actions, 'bookmark', ['views', 'presets', 'saved filters']),
21
+ [ComponentName.ChecklistSection]: m('Checklist', ComponentCategory.DataDisplay, 'list-checks', ['tasks', 'todo', 'onboarding', 'progress', 'steps']),
22
+ [ComponentName.ActivityFeed]: m('Activity feed', ComponentCategory.DataDisplay, 'activity', ['timeline', 'history', 'comments', 'audit', 'log']),
23
+ [ComponentName.AvatarStack]: m('Avatar stack', ComponentCategory.DataDisplay, 'users', ['people', 'members', 'assignees', 'team']),
24
+ [ComponentName.FileUploader]: m('File uploader', ComponentCategory.Input, 'upload', ['attachments', 'files', 'drop zone', 'documents']),
25
+ [ComponentName.ApprovalChainStepper]: m('Approval chain', ComponentCategory.Navigation, 'workflow', ['approvals', 'steps', 'workflow', 'sign off', 'stepper']),
26
+ [ComponentName.OrgTree]: m('Org tree', ComponentCategory.DataDisplay, 'network', ['organization', 'hierarchy', 'org chart', 'reports to', 'tree']),
27
+ [ComponentName.PermissionMatrix]: m('Permission matrix', ComponentCategory.Input, 'shield-check', ['roles', 'permissions', 'access', 'grants', 'rbac']),
21
28
  };
@@ -5,6 +5,8 @@ import { BindingSourceKind, ComponentDescriptor, PropertyDescriptor, PropertyEdi
5
5
  import { componentRegistryKey, valueOutputProperty } from '@cratis/scene.react';
6
6
  import { ComponentName } from '../ComponentName';
7
7
  import { defaultBlueprintName } from '../packageName';
8
+ import { ActivityKind } from './activityModel';
9
+ import { ApprovalState } from './approvalModel';
8
10
  import { ChartKind } from './chartGeometry';
9
11
  import { ComponentState } from './ComponentState';
10
12
  import { FilterFieldType } from './filterModel';
@@ -200,5 +202,156 @@ export const foundationDescriptors: ComponentDescriptor[] = [
200
202
  ...standardStateProperties,
201
203
  ],
202
204
  },
205
+ {
206
+ component: key(ComponentName.ChecklistSection),
207
+ displayName: 'Checklist',
208
+ description: 'Sections of tasks with an owner and due date each and a progress bar per section. Publishes the ids of the done tasks as the value output.',
209
+ properties: [
210
+ {
211
+ ...list('sections', 'Sections', 'Data', 'Section', [
212
+ text('title', 'Title', 'Content', undefined, { constraints: { required: true } }),
213
+ list('items', 'Tasks', 'Data', 'Task', [
214
+ text('id', 'Id', 'Data', undefined, { constraints: { required: true } }),
215
+ text('label', 'Task', 'Content', undefined, { constraints: { required: true } }),
216
+ text('owner', 'Owner', 'Data'),
217
+ text('due', 'Due date', 'Data', 'An ISO 8601 date.'),
218
+ flag('done', 'Done', 'Data'),
219
+ ]),
220
+ ], 'The groups of tasks.'),
221
+ acceptedBindingKinds: bindable,
222
+ },
223
+ text('today', 'Today', 'Content', 'The date overdue is judged against; empty is the current date.'),
224
+ rule('editPermission', 'Edit permission', 'Enabled lets the user tick tasks; otherwise they are shown read-only.'),
225
+ valueOutputProperty(PropertyValueType.Json, 'The ids of the done tasks, published as the user ticks them.'),
226
+ ...standardStateProperties,
227
+ ],
228
+ },
229
+ {
230
+ component: key(ComponentName.ActivityFeed),
231
+ displayName: 'Activity feed',
232
+ description: 'What happened to a record, grouped by day. Content is supplied by the application; this lays it out.',
233
+ properties: [
234
+ {
235
+ ...list('entries', 'Entries', 'Data', 'Entry', [
236
+ text('id', 'Id', 'Data', undefined, { constraints: { required: true } }),
237
+ text('actor', 'Who', 'Data'),
238
+ text('action', 'What', 'Data'),
239
+ choice('kind', 'Kind', 'Data', {
240
+ [ActivityKind.Comment]: 'Comment', [ActivityKind.Change]: 'Change', [ActivityKind.Status]: 'Status', [ActivityKind.System]: 'System',
241
+ }, ActivityKind.System),
242
+ text('at', 'When', 'Data', 'An ISO 8601 date and time.', { constraints: { required: true } }),
243
+ text('body', 'Text', 'Data', 'A comment\'s text or the detail of a change.', { editorOptions: { multiline: true } }),
244
+ ], 'What happened.'),
245
+ acceptedBindingKinds: bindable,
246
+ },
247
+ choice('order', 'Order', 'Appearance', { descending: 'Newest first', ascending: 'Oldest first' }, 'descending'),
248
+ text('timeZone', 'Time zone', 'Content', 'The IANA zone days are cut in; empty is the reader\'s.'),
249
+ ...standardStateProperties,
250
+ ],
251
+ },
252
+ {
253
+ component: key(ComponentName.AvatarStack),
254
+ displayName: 'Avatar stack',
255
+ description: 'Overlapping avatars for the people on a record, with a "+N" chip for the rest.',
256
+ properties: [
257
+ {
258
+ ...list('people', 'People', 'Data', 'Person', [
259
+ text('name', 'Name', 'Data', undefined, { constraints: { required: true } }),
260
+ text('imageUrl', 'Picture', 'Data', 'Without one the initials are shown.'),
261
+ ], 'Who is shown.'),
262
+ acceptedBindingKinds: bindable,
263
+ },
264
+ number('max', 'Shown at most', 'Appearance', 'How many avatars before the rest fold into "+N".', { default: 4, constraints: { minimum: 1, integer: true } }),
265
+ choice('size', 'Size', 'Appearance', { small: 'Small', medium: 'Medium' }, 'medium'),
266
+ ...standardStateProperties,
267
+ ],
268
+ },
269
+ {
270
+ component: key(ComponentName.FileUploader),
271
+ displayName: 'File uploader',
272
+ description: 'A drop zone and a button to choose files, checked against type and size before anything is sent. Publishes the selection as the value output.',
273
+ properties: [
274
+ text('accept', 'Accepted types', 'Behavior', 'Extensions, MIME types and wildcards separated by commas, for example ".pdf, image/*".'),
275
+ number('maxSizeMb', 'Largest file (MB)', 'Behavior', undefined, { constraints: { minimum: 0 } }),
276
+ flag('multiple', 'Several files', 'Behavior', undefined, { default: true }),
277
+ text('serverUrl', 'Upload address', 'Behavior', 'Handed to the host\'s upload handler.'),
278
+ rule('actionPermission', 'Upload permission', 'Whether files can be chosen or dropped, refused with a reason, or the control is hidden.'),
279
+ valueOutputProperty(PropertyValueType.Json, 'The files chosen - name, size, type and status - published as they change.'),
280
+ ...standardStateProperties.filter(property => property.path !== 'actionPermission'),
281
+ ],
282
+ },
283
+ {
284
+ component: key(ComponentName.ApprovalChainStepper),
285
+ displayName: 'Approval chain',
286
+ description: 'The steps a request goes through, who decides each and where it stands. A rejection stops the chain; the first undecided step is the current one.',
287
+ properties: [
288
+ {
289
+ ...list('steps', 'Steps', 'Data', 'Step', [
290
+ text('label', 'Step', 'Content', undefined, { constraints: { required: true } }),
291
+ text('approver', 'Approver', 'Data'),
292
+ choice('state', 'State', 'Data', {
293
+ [ApprovalState.Pending]: 'Pending', [ApprovalState.Current]: 'Awaiting decision', [ApprovalState.Approved]: 'Approved', [ApprovalState.Rejected]: 'Rejected', [ApprovalState.Skipped]: 'Skipped',
294
+ }, ApprovalState.Pending),
295
+ text('at', 'Decided at', 'Data', 'An ISO 8601 date and time.'),
296
+ text('comment', 'Comment', 'Data'),
297
+ ], 'The approvers in order.'),
298
+ acceptedBindingKinds: bindable,
299
+ },
300
+ choice('orientation', 'Orientation', 'Appearance', { horizontal: 'Horizontal', vertical: 'Vertical' }, 'horizontal', 'A phone always reads vertically.'),
301
+ ...standardStateProperties,
302
+ ],
303
+ },
304
+ {
305
+ component: key(ComponentName.OrgTree),
306
+ displayName: 'Org tree',
307
+ description: 'An organization as an expandable tree built from rows of who reports to whom. Publishes the selected id as the value output.',
308
+ properties: [
309
+ {
310
+ ...list('nodes', 'People', 'Data', 'Person', [
311
+ text('id', 'Id', 'Data', undefined, { constraints: { required: true }, editorKind: PropertyEditorKind.Field }),
312
+ text('label', 'Name', 'Data', undefined, { editorKind: PropertyEditorKind.Field }),
313
+ text('title', 'Title', 'Data', undefined, { editorKind: PropertyEditorKind.Field }),
314
+ text('parentId', 'Reports to', 'Data', 'The id of the person above; empty for the top.', { editorKind: PropertyEditorKind.Field }),
315
+ ], 'One row per person or unit.'),
316
+ acceptedBindingKinds: bindable,
317
+ },
318
+ number('expandedDepth', 'Levels open at first', 'Appearance', undefined, { default: 2, constraints: { minimum: 1, integer: true } }),
319
+ text('selected', 'Selected', 'Data', 'The id of the selected node; bind it to start from one.'),
320
+ valueOutputProperty(PropertyValueType.String, 'The id of the selected node, published as the user selects.'),
321
+ ...standardStateProperties,
322
+ ],
323
+ },
324
+ {
325
+ component: key(ComponentName.PermissionMatrix),
326
+ displayName: 'Permission matrix',
327
+ description: 'Roles across, permissions down, a checkbox where they meet. Publishes the grants as the value output.',
328
+ properties: [
329
+ {
330
+ ...list('roles', 'Roles', 'Data', 'Role', [
331
+ text('id', 'Id', 'Data', undefined, { constraints: { required: true } }),
332
+ text('label', 'Name', 'Data'),
333
+ ], 'The columns.'),
334
+ acceptedBindingKinds: bindable,
335
+ },
336
+ {
337
+ ...list('permissions', 'Permissions', 'Data', 'Permission', [
338
+ text('id', 'Id', 'Data', undefined, { constraints: { required: true } }),
339
+ text('label', 'Name', 'Data'),
340
+ text('group', 'Group', 'Data', 'A heading the permission is listed under.'),
341
+ ], 'The rows.'),
342
+ acceptedBindingKinds: bindable,
343
+ },
344
+ {
345
+ ...list('grants', 'Grants', 'Data', 'Grant', [
346
+ text('role', 'Role', 'Data', 'The id of a role.', { constraints: { required: true } }),
347
+ text('permission', 'Permission', 'Data', 'The id of a permission.', { constraints: { required: true } }),
348
+ ], 'What each role holds.'),
349
+ acceptedBindingKinds: bindable,
350
+ },
351
+ rule('editPermission', 'Edit permission', 'Enabled lets the user change grants; otherwise the matrix is read-only.'),
352
+ valueOutputProperty(PropertyValueType.Json, 'The grants as the user has changed them: rows of role and permission, published as they change.'),
353
+ ...standardStateProperties,
354
+ ],
355
+ },
203
356
  ];
204
357
 
@@ -49,6 +49,65 @@ export const foundationSamples: Record<string, FoundationSample> = {
49
49
  properties: { views: [{ id: 'all', label: 'All leads' }, { id: 'mine', label: 'Mine' }, { id: 'overdue', label: 'Overdue' }], activeView: 'mine' },
50
50
  data: ['Mine'], stateful: true,
51
51
  },
52
+ [ComponentName.ChecklistSection]: {
53
+ properties: {
54
+ today: '2026-03-10', locale: 'en-US',
55
+ sections: [
56
+ { title: 'Before day one', items: [{ id: 't1', label: 'Send the contract', owner: 'Grace', due: '2026-03-01', done: true }, { id: 't2', label: 'Order a laptop', owner: 'Linus', due: '2026-03-05' }] },
57
+ { title: 'First week', items: [{ id: 't3', label: 'Security training', owner: 'Ada', due: '2026-03-20' }] },
58
+ ],
59
+ },
60
+ data: ['Send the contract', '1 of 2 complete'], stateful: true,
61
+ },
62
+ [ComponentName.ActivityFeed]: {
63
+ properties: {
64
+ locale: 'en-US', timeZone: 'UTC',
65
+ entries: [
66
+ { id: 'a1', actor: 'Ada Lovelace', action: 'commented', kind: 'comment', at: '2026-03-10T09:30:00Z', body: 'Sent the revised quote.' },
67
+ { id: 'a2', actor: 'Grace Hopper', action: 'changed the owner to Linus', kind: 'change', at: '2026-03-09T16:00:00Z' },
68
+ { id: 'a3', actor: 'System', action: 'marked the deal as qualified', kind: 'status', at: '2026-03-09T08:00:00Z' },
69
+ ],
70
+ },
71
+ data: ['Sent the revised quote.'], stateful: true,
72
+ },
73
+ [ComponentName.AvatarStack]: {
74
+ properties: { people: [{ name: 'Ada Lovelace' }, { name: 'Grace Hopper' }, { name: 'Linus Torvalds' }, { name: 'Margaret Hamilton' }, { name: 'Alan Turing' }, { name: 'Barbara Liskov' }], max: 4 },
75
+ data: ['+2'], stateful: true,
76
+ },
77
+ [ComponentName.FileUploader]: {
78
+ properties: { accept: '.pdf, image/*', maxSizeMb: 10, multiple: true },
79
+ data: [], stateful: true,
80
+ },
81
+ [ComponentName.ApprovalChainStepper]: {
82
+ properties: {
83
+ locale: 'en-US',
84
+ steps: [
85
+ { label: 'Team lead', approver: 'Grace Hopper', state: 'approved', at: '2026-03-09T10:00:00Z', comment: 'Looks fine.' },
86
+ { label: 'Finance', approver: 'Linus Torvalds', state: 'current' },
87
+ { label: 'Director', approver: 'Ada Lovelace', state: 'pending' },
88
+ ],
89
+ },
90
+ data: ['Grace Hopper', 'Looks fine.'], stateful: true,
91
+ },
92
+ [ComponentName.OrgTree]: {
93
+ properties: {
94
+ nodes: [
95
+ { id: 'ceo', label: 'Ada Lovelace', title: 'Chief executive' },
96
+ { id: 'cto', label: 'Grace Hopper', title: 'Chief technology officer', parentId: 'ceo' },
97
+ { id: 'cfo', label: 'Linus Torvalds', title: 'Chief financial officer', parentId: 'ceo' },
98
+ { id: 'dev', label: 'Margaret Hamilton', title: 'Engineering manager', parentId: 'cto' },
99
+ ],
100
+ },
101
+ data: ['Chief technology officer'], stateful: true,
102
+ },
103
+ [ComponentName.PermissionMatrix]: {
104
+ properties: {
105
+ roles: [{ id: 'admin', label: 'Administrator' }, { id: 'sales', label: 'Sales' }],
106
+ permissions: [{ id: 'view', label: 'View deals', group: 'Deals' }, { id: 'edit', label: 'Edit deals', group: 'Deals' }, { id: 'users', label: 'Manage users', group: 'Administration' }],
107
+ grants: [{ role: 'admin', permission: 'view' }, { role: 'admin', permission: 'edit' }, { role: 'admin', permission: 'users' }, { role: 'sales', permission: 'view' }],
108
+ },
109
+ data: ['Edit deals', 'Manage users'], stateful: true,
110
+ },
52
111
  };
53
112
 
54
113
  /** A scene element for a foundation component, populated with its sample unless properties are given, with the state forced when asked. */
@@ -80,11 +80,26 @@ export const foundationStrings = {
80
80
  viewsNone: 'There are no saved views',
81
81
  checklistProgress: '{done} of {total} complete',
82
82
  checklistOwner: 'Owner',
83
+ checklistOverdue: 'Overdue',
84
+ checklistDone: 'Done',
85
+ checklistOpen: 'Open',
86
+ checklistItemProgress: 'Progress of {title}',
87
+ checklistNone: 'This checklist has no tasks',
83
88
  checklistDue: 'Due',
84
89
  activityEmpty: 'No activity yet',
90
+ activityComment: 'Comment',
91
+ activityChange: 'Change',
92
+ activityStatus: 'Status',
93
+ activitySystem: 'System',
94
+ activityBy: 'by {actor}',
85
95
  avatarOverflow: '{count} more',
86
96
  avatarPeople: '{count} people',
87
97
  uploadDrop: 'Drop files here or',
98
+ uploadLimits: 'Up to {limit} each',
99
+ uploadAccepts: 'Accepts {types}',
100
+ uploadSelected: 'Selected files',
101
+ uploadQueued: 'Ready to upload',
102
+ uploadSingle: 'Only one file can be uploaded at a time',
88
103
  uploadBrowse: 'Browse files',
89
104
  uploadRemove: 'Remove {name}',
90
105
  uploadTooLarge: '{name} is larger than {limit}',
@@ -98,13 +113,24 @@ export const foundationStrings = {
98
113
  approvalRejected: 'Rejected',
99
114
  approvalSkipped: 'Skipped',
100
115
  approvalChain: 'Approval chain',
116
+ approvalStep: 'Step {n} of {total}',
117
+ approvalNotReached: 'Not reached',
118
+ approvalOutcomeApproved: 'Approved by everyone',
119
+ approvalOutcomeRejected: 'Rejected',
120
+ approvalOutcomeInProgress: 'Waiting for a decision',
121
+ approvalOutcomePending: 'Not started',
101
122
  orgExpand: 'Expand {name}',
102
123
  orgCollapse: 'Collapse {name}',
103
124
  orgReports: '{count} reports',
125
+ orgTree: 'Organization',
126
+ orgNone: 'There is nobody in the organization yet',
104
127
  matrixAllowed: 'Allowed',
105
128
  matrixDenied: 'Not allowed',
106
129
  matrixGrant: 'Grant {permission} to {role}',
107
130
  matrixReadOnly: 'Read-only',
131
+ matrixPermission: 'Permission',
132
+ matrixScroll: 'Permission matrix, scrolls sideways',
133
+ matrixGroupAll: 'Other',
108
134
  } as const;
109
135
 
110
136
  /** The name of one of the foundation strings. */
@@ -21,6 +21,21 @@ export * from './columnModel';
21
21
  export * from './ColumnChooser';
22
22
  export * from './viewModel';
23
23
  export * from './SavedViewsMenu';
24
+ export * from './checklistModel';
25
+ export * from './ChecklistSection';
26
+ export * from './activityModel';
27
+ export * from './ActivityFeed';
28
+ export * from './avatarModel';
29
+ export * from './AvatarStack';
30
+ export * from './uploadModel';
31
+ export * from './FileUploaderHandler';
32
+ export * from './FileUploader';
33
+ export * from './approvalModel';
34
+ export * from './ApprovalChainStepper';
35
+ export * from './orgModel';
36
+ export * from './OrgTree';
37
+ export * from './permissionModel';
38
+ export * from './PermissionMatrix';
24
39
  export * from './foundationDescriptors';
25
40
  export * from './foundationComponentMetadata';
26
41
  export * from './foundationSamples';