@cratis/scene.blueprint.default 4.13.0 → 4.15.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 (79) 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/navigation.js +7 -0
  9. package/dist/cjs/gallery/navigation.js.map +1 -1
  10. package/dist/cjs/gallery/screens.js +10 -1
  11. package/dist/cjs/gallery/screens.js.map +1 -1
  12. package/dist/cjs/gallery/statusTemplates.js +2 -1
  13. package/dist/cjs/gallery/statusTemplates.js.map +1 -1
  14. package/dist/cjs/gallery/supportTemplates.js +2 -1
  15. package/dist/cjs/gallery/supportTemplates.js.map +1 -1
  16. package/dist/cjs/gallery/widgets.js +20 -6
  17. package/dist/cjs/gallery/widgets.js.map +1 -1
  18. package/dist/cjs/gallery/workspaceTemplates.js +116 -3
  19. package/dist/cjs/gallery/workspaceTemplates.js.map +1 -1
  20. package/dist/cjs/index.js +7 -0
  21. package/dist/cjs/index.js.map +1 -1
  22. package/dist/esm/gallery/ApplicationScreens.stories.d.ts +1 -0
  23. package/dist/esm/gallery/ApplicationScreens.stories.d.ts.map +1 -1
  24. package/dist/esm/gallery/ApplicationScreens.stories.js +2 -1
  25. package/dist/esm/gallery/ApplicationScreens.stories.js.map +1 -1
  26. package/dist/esm/gallery/GalleryScreenPreview.d.ts +3 -1
  27. package/dist/esm/gallery/GalleryScreenPreview.d.ts.map +1 -1
  28. package/dist/esm/gallery/GalleryScreenPreview.js +6 -3
  29. package/dist/esm/gallery/GalleryScreenPreview.js.map +1 -1
  30. package/dist/esm/gallery/PreviewStates.stories.d.ts +36 -0
  31. package/dist/esm/gallery/PreviewStates.stories.d.ts.map +1 -0
  32. package/dist/esm/gallery/PreviewStates.stories.js +27 -0
  33. package/dist/esm/gallery/PreviewStates.stories.js.map +1 -0
  34. package/dist/esm/gallery/TemplatePreviewState.d.ts +7 -0
  35. package/dist/esm/gallery/TemplatePreviewState.d.ts.map +1 -0
  36. package/dist/esm/gallery/TemplatePreviewState.js +15 -0
  37. package/dist/esm/gallery/TemplatePreviewState.js.map +1 -0
  38. package/dist/esm/gallery/applyTemplatePreviewState.d.ts +6 -0
  39. package/dist/esm/gallery/applyTemplatePreviewState.d.ts.map +1 -0
  40. package/dist/esm/gallery/applyTemplatePreviewState.js +87 -0
  41. package/dist/esm/gallery/applyTemplatePreviewState.js.map +1 -0
  42. package/dist/esm/gallery/index.d.ts +2 -0
  43. package/dist/esm/gallery/index.d.ts.map +1 -1
  44. package/dist/esm/gallery/index.js +2 -0
  45. package/dist/esm/gallery/index.js.map +1 -1
  46. package/dist/esm/gallery/navigation.d.ts.map +1 -1
  47. package/dist/esm/gallery/navigation.js +7 -0
  48. package/dist/esm/gallery/navigation.js.map +1 -1
  49. package/dist/esm/gallery/screens.d.ts.map +1 -1
  50. package/dist/esm/gallery/screens.js +10 -1
  51. package/dist/esm/gallery/screens.js.map +1 -1
  52. package/dist/esm/gallery/statusTemplates.js +2 -1
  53. package/dist/esm/gallery/statusTemplates.js.map +1 -1
  54. package/dist/esm/gallery/supportTemplates.js +2 -1
  55. package/dist/esm/gallery/supportTemplates.js.map +1 -1
  56. package/dist/esm/gallery/widgets.d.ts +1 -1
  57. package/dist/esm/gallery/widgets.d.ts.map +1 -1
  58. package/dist/esm/gallery/widgets.js +20 -6
  59. package/dist/esm/gallery/widgets.js.map +1 -1
  60. package/dist/esm/gallery/workspaceTemplates.d.ts +1 -0
  61. package/dist/esm/gallery/workspaceTemplates.d.ts.map +1 -1
  62. package/dist/esm/gallery/workspaceTemplates.js +118 -6
  63. package/dist/esm/gallery/workspaceTemplates.js.map +1 -1
  64. package/dist/esm/index.js +3 -1
  65. package/dist/esm/index.js.map +1 -1
  66. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  67. package/gallery/ApplicationScreens.stories.tsx +4 -1
  68. package/gallery/GalleryScreenPreview.tsx +8 -3
  69. package/gallery/PreviewStates.stories.tsx +56 -0
  70. package/gallery/TemplatePreviewState.ts +20 -0
  71. package/gallery/applyTemplatePreviewState.ts +69 -0
  72. package/gallery/index.ts +2 -0
  73. package/gallery/navigation.ts +1 -0
  74. package/gallery/screens.ts +2 -1
  75. package/gallery/statusTemplates.ts +1 -1
  76. package/gallery/supportTemplates.ts +1 -1
  77. package/gallery/widgets.ts +14 -5
  78. package/gallery/workspaceTemplates.ts +58 -4
  79. package/package.json +8 -7
@@ -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
+ }
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 },
@@ -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' }],
@@ -75,7 +75,7 @@ export const notFoundTemplate: ScreenTemplate = {
75
75
  externalComponent('not-found-tag', 'tag', { value: '404', severity: 'info' }),
76
76
  text('not-found-title', 'That page is not here'),
77
77
  text('not-found-message', 'The link may be old, or the thing it pointed at may have been removed.'),
78
- externalComponent('not-found-search', 'inputText', { placeholder: 'Search instead' }),
78
+ externalComponent('not-found-search', 'inputText', { placeholder: 'Search instead', ariaLabel: 'Search' }),
79
79
  button('not-found-home', 'Back to the dashboard', { severity: 'secondary', targetScreen: 'Dashboard' }),
80
80
  ]),
81
81
  ],
@@ -150,7 +150,7 @@ export const helpTemplate: ScreenTemplate = {
150
150
  content: {
151
151
  [TemplateSlotName.Header]: [pageHeader('help-header', 'Help', 'Search the answers, or ask us directly')],
152
152
  [TemplateSlotName.Body]: [
153
- externalComponent('help-search', 'inputText', { placeholder: 'Search help' }),
153
+ externalComponent('help-search', 'inputText', { placeholder: 'Search help', ariaLabel: 'Search help' }),
154
154
  widget('help-popular', 'Popular answers', [
155
155
  text('help-1', 'How do I invite someone to the workspace?'),
156
156
  text('help-2', 'Why can I not see the sidebar on my phone?'),
@@ -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. */
@@ -9,8 +9,8 @@ import { emptyState, field, formActions, pageHeader, statCard, table, widget } f
9
9
  import { defaultBlueprintTemplateProvenance, sakaiCompositionProvenance } from '../templateProvenance';
10
10
 
11
11
  /**
12
- * The five workhorse shapes inside an application shell: a dashboard, a list, a detail, a form and the
13
- * 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.
14
14
  *
15
15
  * Every one fits the layout's `content` slot, and every one carries realistic seeded content rather than
16
16
  * placeholder text. A gallery whose dashboard shows four boxes labeled "Card" proves the renderer runs; a
@@ -103,7 +103,7 @@ export const crudListTemplate: ScreenTemplate = {
103
103
  button('crud-new', 'New product', { severity: 'primary', icon: 'pi pi-plus' }),
104
104
  button('crud-import', 'Import', { severity: 'secondary', icon: 'pi pi-upload' }),
105
105
  ]),
106
- externalComponent('crud-search', 'inputText', { placeholder: 'Search products' }),
106
+ externalComponent('crud-search', 'inputText', { placeholder: 'Search products', ariaLabel: 'Search products' }),
107
107
  ],
108
108
  [TemplateSlotName.Body]: [
109
109
  table(
@@ -161,6 +161,60 @@ export const detailViewTemplate: ScreenTemplate = {
161
161
  metadata: { ...defaultBlueprintTemplateProvenance },
162
162
  };
163
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' },
216
+ };
217
+
164
218
  /** The form: fields, grouped, with the actions that close them. */
165
219
  export const formPageTemplate: ScreenTemplate = {
166
220
  name: 'FormPage',
@@ -201,4 +255,4 @@ export const emptyTemplate: ScreenTemplate = {
201
255
  };
202
256
 
203
257
  /** The five workspace templates. */
204
- export const workspaceTemplates: ScreenTemplate[] = [dashboardTemplate, crudListTemplate, detailViewTemplate, formPageTemplate, emptyTemplate];
258
+ export const workspaceTemplates: ScreenTemplate[] = [dashboardTemplate, crudListTemplate, masterDetailTemplate, detailViewTemplate, formPageTemplate, emptyTemplate];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/scene.blueprint.default",
3
- "version": "4.13.0",
3
+ "version": "4.15.0",
4
4
  "description": "The default blueprint: application shell layouts, the screen and dialog templates that go inside them, the components that fill their slots, and two themes.",
5
5
  "keywords": [
6
6
  "cratis",
@@ -54,14 +54,15 @@
54
54
  "build-storybook": "storybook build"
55
55
  },
56
56
  "dependencies": {
57
- "@cratis/scene.engine": "4.13.0",
58
- "@cratis/scene.model": "4.13.0",
59
- "@cratis/scene.react": "4.13.0"
57
+ "@cratis/scene.engine": "4.15.0",
58
+ "@cratis/scene.model": "4.15.0",
59
+ "@cratis/scene.react": "4.15.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@cratis/scene.engine": "4.13.0",
63
- "@cratis/scene.model": "4.13.0",
64
- "@cratis/scene.react": "4.13.0",
62
+ "@cratis/scene.engine": "4.15.0",
63
+ "@cratis/scene.model": "4.15.0",
64
+ "@cratis/scene.primereact": "4.15.0",
65
+ "@cratis/scene.react": "4.15.0",
65
66
  "@primereact/core": "11.2.0",
66
67
  "@primereact/headless": "11.2.0",
67
68
  "@primeuix/themes": "^3.0.1",