@cratis/scene.blueprint.default 4.12.0 → 4.14.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 (121) hide show
  1. package/README.md +2 -2
  2. package/dist/cjs/gallery/GalleryScreenPreview.js +6 -3
  3. package/dist/cjs/gallery/GalleryScreenPreview.js.map +1 -1
  4. package/dist/cjs/gallery/TemplatePreviewState.js +17 -0
  5. package/dist/cjs/gallery/TemplatePreviewState.js.map +1 -0
  6. package/dist/cjs/gallery/applyTemplatePreviewState.js +91 -0
  7. package/dist/cjs/gallery/applyTemplatePreviewState.js.map +1 -0
  8. package/dist/cjs/gallery/authTemplates.js +25 -6
  9. package/dist/cjs/gallery/authTemplates.js.map +1 -1
  10. package/dist/cjs/gallery/dialogTemplates.js +13 -3
  11. package/dist/cjs/gallery/dialogTemplates.js.map +1 -1
  12. package/dist/cjs/gallery/navigation.js +7 -0
  13. package/dist/cjs/gallery/navigation.js.map +1 -1
  14. package/dist/cjs/gallery/nesting.js +13 -3
  15. package/dist/cjs/gallery/nesting.js.map +1 -1
  16. package/dist/cjs/gallery/screens.js +10 -1
  17. package/dist/cjs/gallery/screens.js.map +1 -1
  18. package/dist/cjs/gallery/statusTemplates.js +19 -5
  19. package/dist/cjs/gallery/statusTemplates.js.map +1 -1
  20. package/dist/cjs/gallery/supportTemplates.js +23 -6
  21. package/dist/cjs/gallery/supportTemplates.js.map +1 -1
  22. package/dist/cjs/gallery/widgets.js +20 -6
  23. package/dist/cjs/gallery/widgets.js.map +1 -1
  24. package/dist/cjs/gallery/workspaceTemplates.js +137 -8
  25. package/dist/cjs/gallery/workspaceTemplates.js.map +1 -1
  26. package/dist/cjs/index.js +10 -0
  27. package/dist/cjs/index.js.map +1 -1
  28. package/dist/cjs/layouts/appShell.js +5 -1
  29. package/dist/cjs/layouts/appShell.js.map +1 -1
  30. package/dist/cjs/layouts/fullPage.js +5 -1
  31. package/dist/cjs/layouts/fullPage.js.map +1 -1
  32. package/dist/cjs/templateProvenance.js +46 -0
  33. package/dist/cjs/templateProvenance.js.map +1 -0
  34. package/dist/esm/gallery/ApplicationScreens.stories.d.ts +1 -0
  35. package/dist/esm/gallery/ApplicationScreens.stories.d.ts.map +1 -1
  36. package/dist/esm/gallery/ApplicationScreens.stories.js +2 -1
  37. package/dist/esm/gallery/ApplicationScreens.stories.js.map +1 -1
  38. package/dist/esm/gallery/GalleryScreenPreview.d.ts +3 -1
  39. package/dist/esm/gallery/GalleryScreenPreview.d.ts.map +1 -1
  40. package/dist/esm/gallery/GalleryScreenPreview.js +6 -3
  41. package/dist/esm/gallery/GalleryScreenPreview.js.map +1 -1
  42. package/dist/esm/gallery/PreviewStates.stories.d.ts +36 -0
  43. package/dist/esm/gallery/PreviewStates.stories.d.ts.map +1 -0
  44. package/dist/esm/gallery/PreviewStates.stories.js +27 -0
  45. package/dist/esm/gallery/PreviewStates.stories.js.map +1 -0
  46. package/dist/esm/gallery/TemplatePreviewState.d.ts +7 -0
  47. package/dist/esm/gallery/TemplatePreviewState.d.ts.map +1 -0
  48. package/dist/esm/gallery/TemplatePreviewState.js +15 -0
  49. package/dist/esm/gallery/TemplatePreviewState.js.map +1 -0
  50. package/dist/esm/gallery/applyTemplatePreviewState.d.ts +6 -0
  51. package/dist/esm/gallery/applyTemplatePreviewState.d.ts.map +1 -0
  52. package/dist/esm/gallery/applyTemplatePreviewState.js +87 -0
  53. package/dist/esm/gallery/applyTemplatePreviewState.js.map +1 -0
  54. package/dist/esm/gallery/authTemplates.d.ts.map +1 -1
  55. package/dist/esm/gallery/authTemplates.js +25 -6
  56. package/dist/esm/gallery/authTemplates.js.map +1 -1
  57. package/dist/esm/gallery/dialogTemplates.d.ts.map +1 -1
  58. package/dist/esm/gallery/dialogTemplates.js +13 -3
  59. package/dist/esm/gallery/dialogTemplates.js.map +1 -1
  60. package/dist/esm/gallery/index.d.ts +2 -0
  61. package/dist/esm/gallery/index.d.ts.map +1 -1
  62. package/dist/esm/gallery/index.js +2 -0
  63. package/dist/esm/gallery/index.js.map +1 -1
  64. package/dist/esm/gallery/navigation.d.ts.map +1 -1
  65. package/dist/esm/gallery/navigation.js +7 -0
  66. package/dist/esm/gallery/navigation.js.map +1 -1
  67. package/dist/esm/gallery/nesting.d.ts.map +1 -1
  68. package/dist/esm/gallery/nesting.js +13 -3
  69. package/dist/esm/gallery/nesting.js.map +1 -1
  70. package/dist/esm/gallery/screens.d.ts.map +1 -1
  71. package/dist/esm/gallery/screens.js +10 -1
  72. package/dist/esm/gallery/screens.js.map +1 -1
  73. package/dist/esm/gallery/statusTemplates.d.ts.map +1 -1
  74. package/dist/esm/gallery/statusTemplates.js +19 -5
  75. package/dist/esm/gallery/statusTemplates.js.map +1 -1
  76. package/dist/esm/gallery/supportTemplates.d.ts.map +1 -1
  77. package/dist/esm/gallery/supportTemplates.js +23 -6
  78. package/dist/esm/gallery/supportTemplates.js.map +1 -1
  79. package/dist/esm/gallery/widgets.d.ts +1 -1
  80. package/dist/esm/gallery/widgets.d.ts.map +1 -1
  81. package/dist/esm/gallery/widgets.js +20 -6
  82. package/dist/esm/gallery/widgets.js.map +1 -1
  83. package/dist/esm/gallery/workspaceTemplates.d.ts +1 -0
  84. package/dist/esm/gallery/workspaceTemplates.d.ts.map +1 -1
  85. package/dist/esm/gallery/workspaceTemplates.js +139 -11
  86. package/dist/esm/gallery/workspaceTemplates.js.map +1 -1
  87. package/dist/esm/index.d.ts +1 -0
  88. package/dist/esm/index.d.ts.map +1 -1
  89. package/dist/esm/index.js +4 -1
  90. package/dist/esm/index.js.map +1 -1
  91. package/dist/esm/layouts/appShell.d.ts.map +1 -1
  92. package/dist/esm/layouts/appShell.js +5 -1
  93. package/dist/esm/layouts/appShell.js.map +1 -1
  94. package/dist/esm/layouts/fullPage.d.ts.map +1 -1
  95. package/dist/esm/layouts/fullPage.js +5 -1
  96. package/dist/esm/layouts/fullPage.js.map +1 -1
  97. package/dist/esm/templateProvenance.d.ts +4 -0
  98. package/dist/esm/templateProvenance.d.ts.map +1 -0
  99. package/dist/esm/templateProvenance.js +43 -0
  100. package/dist/esm/templateProvenance.js.map +1 -0
  101. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  102. package/gallery/ApplicationScreens.stories.tsx +4 -1
  103. package/gallery/GalleryScreenPreview.tsx +8 -3
  104. package/gallery/PreviewStates.stories.tsx +56 -0
  105. package/gallery/TemplatePreviewState.ts +20 -0
  106. package/gallery/applyTemplatePreviewState.ts +69 -0
  107. package/gallery/authTemplates.ts +7 -0
  108. package/gallery/dialogTemplates.ts +4 -0
  109. package/gallery/index.ts +2 -0
  110. package/gallery/navigation.ts +1 -0
  111. package/gallery/nesting.ts +4 -0
  112. package/gallery/screens.ts +2 -1
  113. package/gallery/statusTemplates.ts +6 -1
  114. package/gallery/supportTemplates.ts +7 -1
  115. package/gallery/widgets.ts +14 -5
  116. package/gallery/workspaceTemplates.ts +64 -4
  117. package/index.ts +1 -0
  118. package/layouts/appShell.ts +2 -0
  119. package/layouts/fullPage.ts +2 -0
  120. package/package.json +8 -7
  121. package/templateProvenance.ts +37 -0
@@ -19,7 +19,7 @@ const meta = {
19
19
  layout: 'fullscreen',
20
20
  docs: {
21
21
  description: {
22
- component: 'The ten in-application screen templates plus the three-level nesting chain, each booted through the real engine as a screen.',
22
+ component: 'The eleven in-application screen templates plus the three-level nesting chain, each booted through the real engine as a screen.',
23
23
  },
24
24
  },
25
25
  },
@@ -36,6 +36,9 @@ export const Dashboard: Story = { args: { screenName: 'Dashboard' } };
36
36
  /** A searchable product table with a header and a primary action. */
37
37
  export const List: Story = { args: { screenName: 'CrudList' } };
38
38
 
39
+ /** A list beside the selected record, stacking on narrow screens. */
40
+ export const MasterDetail: Story = { args: { screenName: 'MasterDetail' } };
41
+
39
42
  /** One record: header, sections and a summary panel. */
40
43
  export const Detail: Story = { args: { screenName: 'DetailView' } };
41
44
 
@@ -11,6 +11,8 @@ import { composeScreenElement } from './composeScreen';
11
11
  import { galleryComponentCatalog, galleryPreviewProfile } from './previewProfile';
12
12
  import { resolveElementComponentNames } from './resolveElementNames';
13
13
  import { galleryScreen } from './screens';
14
+ import { TemplatePreviewState } from './TemplatePreviewState';
15
+ import { applyTemplatePreviewState } from './applyTemplatePreviewState';
14
16
 
15
17
  /**
16
18
  * The registry a preview resolves against when a host does not supply one: `core` plus this blueprint.
@@ -33,6 +35,9 @@ export interface GalleryScreenPreviewProps {
33
35
 
34
36
  /** The component registry to render against. Defaults to `core` plus this blueprint's own components. */
35
37
  registry?: ComponentRegistry;
38
+
39
+ /** The data state to show. Defaults to the seeded, populated records. */
40
+ state?: TemplatePreviewState;
36
41
  }
37
42
 
38
43
  /**
@@ -48,15 +53,15 @@ export interface GalleryScreenPreviewProps {
48
53
  * a preview of a blueprint against half a profile should look visibly incomplete rather than quietly
49
54
  * wrong.
50
55
  */
51
- export function GalleryScreenPreview({ screenName, initialConfig, themes = defaultBlueprintThemes, registry = galleryPreviewRegistry }: GalleryScreenPreviewProps) {
56
+ export function GalleryScreenPreview({ screenName, initialConfig, themes = defaultBlueprintThemes, registry = galleryPreviewRegistry, state = TemplatePreviewState.Populated }: GalleryScreenPreviewProps) {
52
57
  const element = useMemo(() => {
53
58
  const screen = galleryScreen(screenName);
54
59
  if (!screen) {
55
60
  throw new Error(`The gallery has no screen named '${screenName}'.`);
56
61
  }
57
62
 
58
- return resolveElementComponentNames(composeScreenElement(screen), galleryPreviewProfile, galleryComponentCatalog);
59
- }, [screenName]);
63
+ return applyTemplatePreviewState(resolveElementComponentNames(composeScreenElement(screen), galleryPreviewProfile, galleryComponentCatalog), state);
64
+ }, [screenName, state]);
60
65
 
61
66
  return (
62
67
  <LayoutConfigProvider initialConfig={initialConfig} storage={null}>
@@ -0,0 +1,56 @@
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
+ import type { Meta, StoryObj } from '@storybook/react';
5
+ import { corePackage, mergePackageRegistries } from '@cratis/scene.react';
6
+ import { primeReactPackage } from '@cratis/scene.primereact';
7
+ import { defaultBlueprint } from '../defaultBlueprint';
8
+ import { GalleryScreenPreview } from './GalleryScreenPreview';
9
+ import { TemplatePreviewState } from './TemplatePreviewState';
10
+
11
+ /**
12
+ * The data templates in each data state, rendered against the packages the blueprint depends on - `core`,
13
+ * PrimeReact and the blueprint itself - so the tables are PrimeReact's own and each state is the table's own
14
+ * empty message, loading status or error alert rather than a placeholder.
15
+ */
16
+ const registry = mergePackageRegistries([corePackage, primeReactPackage, defaultBlueprint]);
17
+
18
+ const meta = {
19
+ title: 'Blueprint/Preview states',
20
+ component: GalleryScreenPreview,
21
+ parameters: {
22
+ layout: 'fullscreen',
23
+ docs: { description: { component: 'Populated, empty, loading and error states of the data templates, through the real packages.' } },
24
+ },
25
+ args: { registry },
26
+ argTypes: { state: { control: 'select', options: Object.values(TemplatePreviewState) } },
27
+ tags: ['autodocs'],
28
+ } satisfies Meta<typeof GalleryScreenPreview>;
29
+
30
+ export default meta;
31
+
32
+ type Story = StoryObj<typeof meta>;
33
+
34
+ /** The list with its seeded products. */
35
+ export const ListPopulated: Story = { args: { screenName: 'CrudList', state: TemplatePreviewState.Populated } };
36
+
37
+ /** The list once the query returns nothing. */
38
+ export const ListEmpty: Story = { args: { screenName: 'CrudList', state: TemplatePreviewState.Empty } };
39
+
40
+ /** The list while the query has not answered. */
41
+ export const ListLoading: Story = { args: { screenName: 'CrudList', state: TemplatePreviewState.Loading } };
42
+
43
+ /** The list when the query failed. */
44
+ export const ListError: Story = { args: { screenName: 'CrudList', state: TemplatePreviewState.Error } };
45
+
46
+ /** Master/detail with its customers. */
47
+ export const MasterDetailPopulated: Story = { args: { screenName: 'MasterDetail', state: TemplatePreviewState.Populated } };
48
+
49
+ /** Master/detail while loading. */
50
+ export const MasterDetailLoading: Story = { args: { screenName: 'MasterDetail', state: TemplatePreviewState.Loading } };
51
+
52
+ /** The dashboard when its widgets' data failed to load. */
53
+ export const DashboardError: Story = { args: { screenName: 'Dashboard', state: TemplatePreviewState.Error } };
54
+
55
+ /** User administration with no users yet. */
56
+ export const UsersEmpty: Story = { args: { screenName: 'UserManagement', state: TemplatePreviewState.Empty } };
@@ -0,0 +1,20 @@
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
+ /**
5
+ * The data states a template preview can show. A template is only worth choosing if it looks right in all
6
+ * four, because an application spends real time in each of them.
7
+ */
8
+ export enum TemplatePreviewState {
9
+ /** The seeded, realistic records. */
10
+ Populated = 'populated',
11
+
12
+ /** The query returned nothing; each table shows its empty message. */
13
+ Empty = 'empty',
14
+
15
+ /** The query has not answered yet; each table announces that it is loading. */
16
+ Loading = 'loading',
17
+
18
+ /** The query failed; each table announces the error in place of its rows. */
19
+ Error = 'error',
20
+ }
@@ -0,0 +1,69 @@
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
+ import { ExternalComponent, Panel, SceneElement } from '@cratis/scene.model';
5
+ import { TemplatePreviewState } from './TemplatePreviewState';
6
+
7
+ /** What a table in a preview says when its data could not be loaded. */
8
+ export const previewErrorMessage = 'The data could not be loaded. Try again in a moment.';
9
+
10
+ /**
11
+ * Puts every data-bearing element of a composed screen into one data state, leaving everything else - the
12
+ * shell, headers, actions and static content - exactly as authored.
13
+ *
14
+ * A data-bearing element is one whose `rows` are records: the seeded rows are what a query would return,
15
+ * so the states are expressed as a query's outcomes in the table's own vocabulary - no rows, `loading`, or
16
+ * an `error` message - rather than by swapping in a different component. The authored tree is not changed.
17
+ */
18
+ export function applyTemplatePreviewState<T extends SceneElement>(element: T, state: TemplatePreviewState): T {
19
+ if (state === TemplatePreviewState.Populated) return element;
20
+ return transform(element, state) as T;
21
+ }
22
+
23
+ /** Whether a composed screen contains anything a data state changes. */
24
+ export function hasPreviewData(element: SceneElement): boolean {
25
+ const properties = (element as Partial<ExternalComponent>).properties;
26
+ if (isDataBearing(properties)) return true;
27
+ return childrenOf(element).some(hasPreviewData);
28
+ }
29
+
30
+ function transform(element: SceneElement, state: TemplatePreviewState): SceneElement {
31
+ const external = element as Partial<ExternalComponent>;
32
+ const panel = element as Partial<Panel>;
33
+ let result: SceneElement = element;
34
+
35
+ if (isDataBearing(external.properties)) {
36
+ result = { ...result, properties: { ...external.properties, ...stateProperties(state) } } as SceneElement;
37
+ }
38
+
39
+ if (external.slots) {
40
+ const slots = Object.fromEntries(Object.entries(external.slots).map(([name, children]) => [name, children.map(child => transform(child, state))]));
41
+ result = { ...result, slots } as SceneElement;
42
+ }
43
+
44
+ if (Array.isArray(panel.children)) {
45
+ result = { ...result, children: panel.children.map(child => transform(child, state)) } as SceneElement;
46
+ }
47
+
48
+ return result;
49
+ }
50
+
51
+ function stateProperties(state: TemplatePreviewState): Record<string, unknown> {
52
+ switch (state) {
53
+ case TemplatePreviewState.Empty: return { rows: [] };
54
+ case TemplatePreviewState.Loading: return { rows: [], loading: true };
55
+ case TemplatePreviewState.Error: return { rows: [], error: previewErrorMessage };
56
+ default: return {};
57
+ }
58
+ }
59
+
60
+ function isDataBearing(properties: Record<string, unknown> | undefined): boolean {
61
+ const rows = properties?.rows;
62
+ return Array.isArray(rows) && rows.every(row => typeof row === 'object' && row !== null && !Array.isArray(row));
63
+ }
64
+
65
+ function childrenOf(element: SceneElement): SceneElement[] {
66
+ const external = element as Partial<ExternalComponent>;
67
+ const panel = element as Partial<Panel>;
68
+ return [...Object.values(external.slots ?? {}).flat(), ...(Array.isArray(panel.children) ? panel.children : [])];
69
+ }
@@ -7,6 +7,7 @@ import { SlotName } from '../layouts';
7
7
  import { TemplateSlotName } from './TemplateSlotName';
8
8
  import { button, card, externalComponent, text } from './elements';
9
9
  import { field, formActions } from './widgets';
10
+ import { defaultBlueprintTemplateProvenance } from '../templateProvenance';
10
11
 
11
12
  /**
12
13
  * The six screens that stand between someone and the application: sign in, register, forgotten password,
@@ -47,6 +48,7 @@ export const loginTemplate: ScreenTemplate = {
47
48
  },
48
49
  displayName: 'Sign in',
49
50
  description: 'Email and password beside the branding panel.',
51
+ metadata: { ...defaultBlueprintTemplateProvenance },
50
52
  };
51
53
 
52
54
  /** Register. */
@@ -69,6 +71,7 @@ export const registerTemplate: ScreenTemplate = {
69
71
  },
70
72
  displayName: 'Register',
71
73
  description: 'Account creation with a password strength meter and the terms checkbox.',
74
+ metadata: { ...defaultBlueprintTemplateProvenance },
72
75
  };
73
76
 
74
77
  /** Forgotten password: ask for the address. */
@@ -88,6 +91,7 @@ export const forgotPasswordTemplate: ScreenTemplate = {
88
91
  },
89
92
  displayName: 'Forgotten password',
90
93
  description: 'One field and one button - the whole point is that it asks for nothing else.',
94
+ metadata: { ...defaultBlueprintTemplateProvenance },
91
95
  };
92
96
 
93
97
  /** New password: the other end of the link. */
@@ -108,6 +112,7 @@ export const newPasswordTemplate: ScreenTemplate = {
108
112
  },
109
113
  displayName: 'New password',
110
114
  description: 'Where a reset link lands: choose it, confirm it, done.',
115
+ metadata: { ...defaultBlueprintTemplateProvenance },
111
116
  };
112
117
 
113
118
  /** Verification: the code from the email. */
@@ -128,6 +133,7 @@ export const verificationTemplate: ScreenTemplate = {
128
133
  },
129
134
  displayName: 'Verification',
130
135
  description: 'The code step, with the progress indicator that tells you how much is left.',
136
+ metadata: { ...defaultBlueprintTemplateProvenance },
131
137
  };
132
138
 
133
139
  /** Lock screen: the session is still there, the person has to prove they are. */
@@ -148,6 +154,7 @@ export const lockScreenTemplate: ScreenTemplate = {
148
154
  },
149
155
  displayName: 'Lock screen',
150
156
  description: 'One person, one password, and no way to lose what was open.',
157
+ metadata: { ...defaultBlueprintTemplateProvenance },
151
158
  };
152
159
 
153
160
  /** The six authentication templates. */
@@ -5,6 +5,7 @@ import { DialogTemplate } from '@cratis/scene.model';
5
5
  import { TemplateSlotName } from './TemplateSlotName';
6
6
  import { button, card, externalComponent, panel, text } from './elements';
7
7
  import { field } from './widgets';
8
+ import { defaultBlueprintTemplateProvenance } from '../templateProvenance';
8
9
 
9
10
  /**
10
11
  * The three dialog shapes an application repeats: confirm something destructive, capture a short form, and
@@ -32,6 +33,7 @@ export const confirmDialogTemplate: DialogTemplate = {
32
33
  },
33
34
  displayName: 'Confirmation dialog',
34
35
  description: 'One question with the consequence spelled out, and a way back.',
36
+ metadata: { ...defaultBlueprintTemplateProvenance },
35
37
  };
36
38
 
37
39
  /** Form: a short capture that does not deserve a page of its own. */
@@ -51,6 +53,7 @@ export const formDialogTemplate: DialogTemplate = {
51
53
  },
52
54
  displayName: 'Form dialog',
53
55
  description: 'A handful of fields captured without leaving the page underneath.',
56
+ metadata: { ...defaultBlueprintTemplateProvenance },
54
57
  };
55
58
 
56
59
  /** Detail: a record shown over the list it came from. */
@@ -73,6 +76,7 @@ export const detailDialogTemplate: DialogTemplate = {
73
76
  },
74
77
  displayName: 'Detail dialog',
75
78
  description: 'A record over the list it came from, with a route to the full page.',
79
+ metadata: { ...defaultBlueprintTemplateProvenance },
76
80
  };
77
81
 
78
82
  /** The dialog templates this blueprint provides. */
package/gallery/index.ts CHANGED
@@ -20,3 +20,5 @@ export * from './componentNames';
20
20
  export * from './previewProfile';
21
21
  export * from './resolveElementNames';
22
22
  export * from './GalleryScreenPreview';
23
+ export * from './TemplatePreviewState';
24
+ export * from './applyTemplatePreviewState';
@@ -21,6 +21,7 @@ export const navigationEntries: NavigationEntry[] = [
21
21
  { label: 'Dashboard', targetScreen: 'Dashboard', icon: 'pi pi-home', group: 'Workspace', order: 10 },
22
22
  { label: 'Products', targetScreen: 'CrudList', icon: 'pi pi-box', group: 'Workspace', order: 20 },
23
23
  { label: 'Product detail', targetScreen: 'DetailView', icon: 'pi pi-file', group: 'Workspace', order: 30 },
24
+ { label: 'Customers', targetScreen: 'MasterDetail', icon: 'pi pi-users', group: 'Workspace', order: 35 },
24
25
  { label: 'New product', targetScreen: 'FormPage', icon: 'pi pi-plus-circle', group: 'Workspace', order: 40 },
25
26
  { label: 'Invoices', targetScreen: 'Invoice', icon: 'pi pi-receipt', group: 'Workspace', order: 50 },
26
27
  { label: 'Nothing yet', targetScreen: 'Empty', icon: 'pi pi-inbox', group: 'Workspace', order: 60 },
@@ -7,6 +7,7 @@ import { SlotName, column, slotLeaf } from '../layouts';
7
7
  import { TemplateSlotName } from './TemplateSlotName';
8
8
  import { button, externalComponent } from './elements';
9
9
  import { pageHeader } from './widgets';
10
+ import { defaultBlueprintTemplateProvenance } from '../templateProvenance';
10
11
 
11
12
  /**
12
13
  * A worked three-level chain, from the application layout down to a slice.
@@ -36,6 +37,7 @@ export const moduleWorkspaceTemplate: ScreenTemplate = {
36
37
  },
37
38
  displayName: 'Module workspace',
38
39
  description: 'Module level: fits the application layout content slot and offers a body for one feature.',
40
+ metadata: { ...defaultBlueprintTemplateProvenance },
39
41
  };
40
42
 
41
43
  /**
@@ -56,6 +58,7 @@ export const featureSectionTemplate: ScreenTemplate = {
56
58
  },
57
59
  displayName: 'Feature section',
58
60
  description: 'Feature level: fits a module workspace body slot and brings the action toolbar.',
61
+ metadata: { ...defaultBlueprintTemplateProvenance },
59
62
  };
60
63
 
61
64
  /**
@@ -76,6 +79,7 @@ export const sliceSectionTemplate: ScreenTemplate = {
76
79
  },
77
80
  displayName: 'Slice section',
78
81
  description: 'Slice level: fits a feature section body slot and hosts one behavior.',
82
+ metadata: { ...defaultBlueprintTemplateProvenance },
79
83
  };
80
84
 
81
85
  /** The chain, outermost first - what a spec walks to prove `fitsSlot` resolves at every level. */
@@ -15,7 +15,7 @@ import { workspaceTemplates } from './workspaceTemplates';
15
15
  /**
16
16
  * The screen templates this blueprint provides.
17
17
  *
18
- * Ten shapes for inside the application shell, ten for the full-page shell, and the three-level nesting
18
+ * Eleven shapes for inside the application shell, ten for the full-page shell, and the three-level nesting
19
19
  * chain that demonstrates how `fitsSlot` composes. The list is what the manifest's `screenTemplates` names
20
20
  * and what the bundle provides; `validatePackageBundle` proves the two agree.
21
21
  */
@@ -34,6 +34,7 @@ const fullPageTemplateNames = new Set([...authTemplates, ...statusTemplates].map
34
34
  const breadcrumbs: Record<string, BreadcrumbEntry[]> = {
35
35
  Dashboard: [{ label: 'Dashboard' }],
36
36
  CrudList: [{ label: 'Catalog', targetScreen: 'Dashboard' }, { label: 'Products' }],
37
+ MasterDetail: [{ label: 'Sales', targetScreen: 'Dashboard' }, { label: 'Customers' }],
37
38
  DetailView: [{ label: 'Catalog', targetScreen: 'Dashboard' }, { label: 'Products', targetScreen: 'CrudList' }, { label: 'Bamboo Watch' }],
38
39
  FormPage: [{ label: 'Catalog', targetScreen: 'Dashboard' }, { label: 'Products', targetScreen: 'CrudList' }, { label: 'New product' }],
39
40
  Empty: [{ label: 'Catalog', targetScreen: 'Dashboard' }, { label: 'Products' }],
@@ -7,6 +7,7 @@ import { SlotName } from '../layouts';
7
7
  import { TemplateSlotName } from './TemplateSlotName';
8
8
  import { button, card, externalComponent, panel, text } from './elements';
9
9
  import { widget } from './widgets';
10
+ import { defaultBlueprintTemplateProvenance } from '../templateProvenance';
10
11
 
11
12
  /**
12
13
  * The four screens that are not part of anyone's plan: a server error, a refusal, a wrong address, and the
@@ -37,6 +38,7 @@ export const errorTemplate: ScreenTemplate = {
37
38
  },
38
39
  displayName: 'Error',
39
40
  description: 'A server-side failure, said plainly, with a way onward.',
41
+ metadata: { ...defaultBlueprintTemplateProvenance },
40
42
  };
41
43
 
42
44
  /** Signed in, and still not allowed. */
@@ -59,6 +61,7 @@ export const accessDeniedTemplate: ScreenTemplate = {
59
61
  },
60
62
  displayName: 'Access denied',
61
63
  description: 'A refusal that distinguishes "not signed in" from "not allowed".',
64
+ metadata: { ...defaultBlueprintTemplateProvenance },
62
65
  };
63
66
 
64
67
  /** No such address. */
@@ -72,13 +75,14 @@ export const notFoundTemplate: ScreenTemplate = {
72
75
  externalComponent('not-found-tag', 'tag', { value: '404', severity: 'info' }),
73
76
  text('not-found-title', 'That page is not here'),
74
77
  text('not-found-message', 'The link may be old, or the thing it pointed at may have been removed.'),
75
- externalComponent('not-found-search', 'inputText', { placeholder: 'Search instead' }),
78
+ externalComponent('not-found-search', 'inputText', { placeholder: 'Search instead', ariaLabel: 'Search' }),
76
79
  button('not-found-home', 'Back to the dashboard', { severity: 'secondary', targetScreen: 'Dashboard' }),
77
80
  ]),
78
81
  ],
79
82
  },
80
83
  displayName: 'Not found',
81
84
  description: 'A wrong address, with a search box rather than a dead end.',
85
+ metadata: { ...defaultBlueprintTemplateProvenance },
82
86
  };
83
87
 
84
88
  /** The page that has to do the selling. */
@@ -112,6 +116,7 @@ export const landingTemplate: ScreenTemplate = {
112
116
  },
113
117
  displayName: 'Landing',
114
118
  description: 'The marketing front door, with its own navigation because it has no application chrome.',
119
+ metadata: { ...defaultBlueprintTemplateProvenance },
115
120
  };
116
121
 
117
122
  /** The four status and marketing templates. */
@@ -6,6 +6,7 @@ import { SlotName } from '../layouts';
6
6
  import { TemplateSlotName } from './TemplateSlotName';
7
7
  import { button, card, externalComponent, text } from './elements';
8
8
  import { field, formActions, pageHeader, table, widget } from './widgets';
9
+ import { defaultBlueprintTemplateProvenance } from '../templateProvenance';
9
10
 
10
11
  /**
11
12
  * The five shapes an application needs that are not the CRUD loop: documentation, the signed-in user's own
@@ -40,6 +41,7 @@ export const documentationTemplate: ScreenTemplate = {
40
41
  },
41
42
  displayName: 'Documentation',
42
43
  description: 'Prose with a table of contents beside it.',
44
+ metadata: { ...defaultBlueprintTemplateProvenance },
43
45
  };
44
46
 
45
47
  /** Profile settings: the signed-in user editing their own account. */
@@ -67,6 +69,7 @@ export const profileSettingsTemplate: ScreenTemplate = {
67
69
  },
68
70
  displayName: 'Profile settings',
69
71
  description: 'The signed-in user editing their own name, photo, password and notifications.',
72
+ metadata: { ...defaultBlueprintTemplateProvenance },
70
73
  };
71
74
 
72
75
  /** User management: administering everybody else. */
@@ -99,6 +102,7 @@ export const userManagementTemplate: ScreenTemplate = {
99
102
  },
100
103
  displayName: 'User management',
101
104
  description: 'The people table, with roles, status and an invitation action.',
105
+ metadata: { ...defaultBlueprintTemplateProvenance },
102
106
  };
103
107
 
104
108
  /** Invoice: a printable document, which is a different shape from a screen. */
@@ -135,6 +139,7 @@ export const invoiceTemplate: ScreenTemplate = {
135
139
  },
136
140
  displayName: 'Invoice',
137
141
  description: 'A printable document: parties, line items and totals.',
142
+ metadata: { ...defaultBlueprintTemplateProvenance },
138
143
  };
139
144
 
140
145
  /** Help: the answers, and a way to ask when they are not there. */
@@ -145,7 +150,7 @@ export const helpTemplate: ScreenTemplate = {
145
150
  content: {
146
151
  [TemplateSlotName.Header]: [pageHeader('help-header', 'Help', 'Search the answers, or ask us directly')],
147
152
  [TemplateSlotName.Body]: [
148
- externalComponent('help-search', 'inputText', { placeholder: 'Search help' }),
153
+ externalComponent('help-search', 'inputText', { placeholder: 'Search help', ariaLabel: 'Search help' }),
149
154
  widget('help-popular', 'Popular answers', [
150
155
  text('help-1', 'How do I invite someone to the workspace?'),
151
156
  text('help-2', 'Why can I not see the sidebar on my phone?'),
@@ -158,6 +163,7 @@ export const helpTemplate: ScreenTemplate = {
158
163
  },
159
164
  displayName: 'Help',
160
165
  description: 'Searchable answers with a route to a human.',
166
+ metadata: { ...defaultBlueprintTemplateProvenance },
161
167
  };
162
168
 
163
169
  /** The five support templates. */
@@ -32,19 +32,28 @@ export function widget(id: string, title: string, content: SceneElement[]): Scen
32
32
  return card(id, [text(`${id}-title`, title), ...content], { title });
33
33
  }
34
34
 
35
- /** A table with real columns and real rows, so a list template looks like a list. */
36
- export function table(id: string, columns: { field: string; header: string }[], rows: Record<string, unknown>[]): SceneElement {
35
+ /**
36
+ * A table with real columns and real rows, so a list template looks like a list.
37
+ *
38
+ * The properties are the table's own vocabulary - `rows` for the records and `pageSize` to page them - so
39
+ * a host with the real table loaded shows the rows rather than an empty grid. `emptyMessage` is what the
40
+ * same table says once the rows are gone, which is how a preview shows a template's empty state.
41
+ */
42
+ export function table(id: string, columns: { field: string; header: string }[], rows: Record<string, unknown>[], emptyMessage = 'Nothing to show yet'): SceneElement {
37
43
  return externalComponent(
38
44
  id,
39
45
  'dataTable',
40
- { value: rows, dataKey: columns[0]?.field ?? 'id', paginator: rows.length > 8, rows: 8 },
46
+ { rows, dataKey: columns[0]?.field ?? 'id', emptyMessage, ...(rows.length > 8 ? { pageSize: 8 } : {}) },
41
47
  { columns: columns.map(column => externalComponent(`${id}-${column.field}`, 'column', { field: column.field, header: column.header, sortable: true })) },
42
48
  );
43
49
  }
44
50
 
45
- /** One labeled input in a form template. */
51
+ /**
52
+ * One labeled input in a form template. The visible label is a separate text element, so the input carries
53
+ * the same words as its accessible name - otherwise a screen reader announces an unnamed field.
54
+ */
46
55
  export function field(id: string, label: string, componentName: string, properties: Record<string, unknown> = {}): SceneElement {
47
- return panel(id, [text(`${id}-label`, label), externalComponent(`${id}-input`, componentName, properties)]);
56
+ return panel(id, [text(`${id}-label`, label), externalComponent(`${id}-input`, componentName, { ariaLabel: label, ...properties })]);
48
57
  }
49
58
 
50
59
  /** A row of buttons closing a form. */
@@ -6,10 +6,11 @@ import { SlotName, column, grid, slotLeaf } from '../layouts';
6
6
  import { TemplateSlotName } from './TemplateSlotName';
7
7
  import { button, card, externalComponent, text } from './elements';
8
8
  import { emptyState, field, formActions, pageHeader, statCard, table, widget } from './widgets';
9
+ import { defaultBlueprintTemplateProvenance, sakaiCompositionProvenance } from '../templateProvenance';
9
10
 
10
11
  /**
11
- * The five workhorse shapes inside an application shell: a dashboard, a list, a detail, a form and the
12
- * designed empty state.
12
+ * The six workhorse shapes inside an application shell: a dashboard, a list, master/detail, a detail, a
13
+ * form and the designed empty state.
13
14
  *
14
15
  * Every one fits the layout's `content` slot, and every one carries realistic seeded content rather than
15
16
  * placeholder text. A gallery whose dashboard shows four boxes labeled "Card" proves the renderer runs; a
@@ -88,6 +89,7 @@ export const dashboardTemplate: ScreenTemplate = {
88
89
  },
89
90
  displayName: 'Dashboard',
90
91
  description: 'Four stat cards over two columns of widgets - the composition every template line opens with.',
92
+ metadata: { ...sakaiCompositionProvenance },
91
93
  };
92
94
 
93
95
  /** The list: a filter toolbar, a real table, and the row actions a list needs. */
@@ -101,7 +103,7 @@ export const crudListTemplate: ScreenTemplate = {
101
103
  button('crud-new', 'New product', { severity: 'primary', icon: 'pi pi-plus' }),
102
104
  button('crud-import', 'Import', { severity: 'secondary', icon: 'pi pi-upload' }),
103
105
  ]),
104
- externalComponent('crud-search', 'inputText', { placeholder: 'Search products' }),
106
+ externalComponent('crud-search', 'inputText', { placeholder: 'Search products', ariaLabel: 'Search products' }),
105
107
  ],
106
108
  [TemplateSlotName.Body]: [
107
109
  table(
@@ -125,6 +127,7 @@ export const crudListTemplate: ScreenTemplate = {
125
127
  },
126
128
  displayName: 'List',
127
129
  description: 'A searchable table with a header, primary action and row data.',
130
+ metadata: { ...defaultBlueprintTemplateProvenance },
128
131
  };
129
132
 
130
133
  /** The detail: a summary panel beside the record's own sections. */
@@ -155,6 +158,61 @@ export const detailViewTemplate: ScreenTemplate = {
155
158
  },
156
159
  displayName: 'Detail',
157
160
  description: 'One record: a header with actions, its sections, and a summary panel.',
161
+ metadata: { ...defaultBlueprintTemplateProvenance },
162
+ };
163
+
164
+ /**
165
+ * Master/detail: the list and the selected record side by side, so working through records does not mean
166
+ * leaving the list. The detail region is an outlet that accepts detail screens, which is what lets a row's
167
+ * destination open a full detail screen in place.
168
+ *
169
+ * At a compact width the two stack, list first, because side-by-side on a phone is two unreadable halves.
170
+ */
171
+ export const masterDetailTemplate: ScreenTemplate = {
172
+ name: 'MasterDetail',
173
+ fitsSlot: SlotName.Content,
174
+ slots: [{ name: TemplateSlotName.Header }, { name: TemplateSlotName.Primary }, { name: TemplateSlotName.SidePanel }],
175
+ outlets: [{ name: 'master-detail.detail', description: 'The selected record.', accepts: ['Detail'] }],
176
+ arrangement: {
177
+ root: column([slotLeaf(TemplateSlotName.Header), grid([slotLeaf(TemplateSlotName.Primary), slotLeaf(TemplateSlotName.SidePanel)], 2, 16)], 16),
178
+ overrides: [
179
+ {
180
+ width: WidthSizeClass.Compact,
181
+ root: column([slotLeaf(TemplateSlotName.Header), slotLeaf(TemplateSlotName.Primary), slotLeaf(TemplateSlotName.SidePanel)], 16),
182
+ },
183
+ ],
184
+ },
185
+ content: {
186
+ [TemplateSlotName.Header]: [pageHeader('master-header', 'Customers', '1,204 customers, 38 with overdue invoices')],
187
+ [TemplateSlotName.Primary]: [
188
+ table(
189
+ 'customers-table',
190
+ [
191
+ { field: 'name', header: 'Customer' },
192
+ { field: 'city', header: 'City' },
193
+ { field: 'balance', header: 'Balance' },
194
+ ],
195
+ [
196
+ { name: 'Northwind Traders', city: 'Seattle', balance: '$4,200.00' },
197
+ { name: 'Contoso Ltd', city: 'Oslo', balance: '$0.00' },
198
+ { name: 'Fabrikam', city: 'Lyon', balance: '$12,940.50' },
199
+ { name: 'Adventure Works', city: 'Denver', balance: '$318.20' },
200
+ ],
201
+ 'No customers match the search',
202
+ ),
203
+ ],
204
+ [TemplateSlotName.SidePanel]: [
205
+ widget('customer-detail', 'Northwind Traders', [
206
+ text('customer-contact', 'Contact: Amelia Nyquist'),
207
+ text('customer-terms', 'Payment terms: 30 days'),
208
+ externalComponent('customer-status', 'tag', { value: 'Overdue', severity: 'warning' }),
209
+ button('customer-open', 'Open customer', { severity: 'primary' }),
210
+ ]),
211
+ ],
212
+ },
213
+ displayName: 'Master/detail',
214
+ description: 'A list beside the selected record, stacking on narrow screens.',
215
+ metadata: { ...defaultBlueprintTemplateProvenance, type: 'Workspace' },
158
216
  };
159
217
 
160
218
  /** The form: fields, grouped, with the actions that close them. */
@@ -178,6 +236,7 @@ export const formPageTemplate: ScreenTemplate = {
178
236
  },
179
237
  displayName: 'Form',
180
238
  description: 'A grouped form with the field types an application actually uses.',
239
+ metadata: { ...defaultBlueprintTemplateProvenance },
181
240
  };
182
241
 
183
242
  /** The empty state: what a list looks like before anything exists, designed rather than apologized for. */
@@ -192,7 +251,8 @@ export const emptyTemplate: ScreenTemplate = {
192
251
  },
193
252
  displayName: 'Empty state',
194
253
  description: 'The designed empty state for a list that has nothing in it yet.',
254
+ metadata: { ...defaultBlueprintTemplateProvenance },
195
255
  };
196
256
 
197
257
  /** The five workspace templates. */
198
- export const workspaceTemplates: ScreenTemplate[] = [dashboardTemplate, crudListTemplate, detailViewTemplate, formPageTemplate, emptyTemplate];
258
+ export const workspaceTemplates: ScreenTemplate[] = [dashboardTemplate, crudListTemplate, masterDetailTemplate, detailViewTemplate, formPageTemplate, emptyTemplate];
package/index.ts CHANGED
@@ -10,3 +10,4 @@ export * from './shell';
10
10
  export * from './themes';
11
11
  export * from './defaultBlueprintComponents';
12
12
  export * from './defaultBlueprint';
13
+ export * from './templateProvenance';
@@ -5,6 +5,7 @@ import { FlowArrangement, HeightSizeClass, Layout, WidthSizeClass } from '@crati
5
5
  import { LayoutName } from './LayoutName';
6
6
  import { SlotName } from './SlotName';
7
7
  import { column, row, slotLeaf } from './flowBuilders';
8
+ import { sakaiCompositionProvenance } from '../templateProvenance';
8
9
 
9
10
  /**
10
11
  * The tree at a regular width and height: a topbar across the top, the sidebar column beside the main
@@ -103,4 +104,5 @@ export const appShellLayout: Layout = {
103
104
  { name: SlotName.ConfigPanel },
104
105
  ],
105
106
  arrangement: appShellArrangement,
107
+ metadata: { ...sakaiCompositionProvenance },
106
108
  };
@@ -5,6 +5,7 @@ import { FlowArrangement, Layout, WidthSizeClass } from '@cratis/scene.model';
5
5
  import { LayoutName } from './LayoutName';
6
6
  import { SlotName } from './SlotName';
7
7
  import { column, row, slotLeaf } from './flowBuilders';
8
+ import { defaultBlueprintTemplateProvenance } from '../templateProvenance';
8
9
 
9
10
  /**
10
11
  * The tree at a regular width: the branding aside beside the content, with the configurator over both.
@@ -45,4 +46,5 @@ export const fullPageLayout: Layout = {
45
46
  name: LayoutName.FullPage,
46
47
  slots: [{ name: SlotName.Aside }, { name: SlotName.Content }, { name: SlotName.ConfigPanel }],
47
48
  arrangement: fullPageArrangement,
49
+ metadata: { ...defaultBlueprintTemplateProvenance },
48
50
  };