@cratis/scene.blueprint.default 4.33.0 → 4.35.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 (134) hide show
  1. package/dist/cjs/index.js +53 -4
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/preview-host/ApplicationDocument.js +17 -0
  4. package/dist/cjs/preview-host/ApplicationDocument.js.map +1 -0
  5. package/dist/cjs/preview-host/ApplicationHost.js +134 -0
  6. package/dist/cjs/preview-host/ApplicationHost.js.map +1 -0
  7. package/dist/cjs/preview-host/FlowArrangementView.js +75 -0
  8. package/dist/cjs/preview-host/FlowArrangementView.js.map +1 -0
  9. package/dist/cjs/preview-host/PreviewProblem.js +32 -0
  10. package/dist/cjs/preview-host/PreviewProblem.js.map +1 -0
  11. package/dist/cjs/preview-host/PreviewProblemsView.js +51 -0
  12. package/dist/cjs/preview-host/PreviewProblemsView.js.map +1 -0
  13. package/dist/cjs/preview-host/PreviewSliceFragment.js +55 -0
  14. package/dist/cjs/preview-host/PreviewSliceFragment.js.map +1 -0
  15. package/dist/cjs/preview-host/PreviewTarget.js +17 -0
  16. package/dist/cjs/preview-host/PreviewTarget.js.map +1 -0
  17. package/dist/cjs/preview-host/PreviewTemplate.js +49 -0
  18. package/dist/cjs/preview-host/PreviewTemplate.js.map +1 -0
  19. package/dist/cjs/preview-host/PreviewToast.js +45 -0
  20. package/dist/cjs/preview-host/PreviewToast.js.map +1 -0
  21. package/dist/cjs/preview-host/ScenePreviewHost.js +290 -0
  22. package/dist/cjs/preview-host/ScenePreviewHost.js.map +1 -0
  23. package/dist/cjs/preview-host/composeApplication.js +249 -0
  24. package/dist/cjs/preview-host/composeApplication.js.map +1 -0
  25. package/dist/cjs/preview-host/defaultApplicationChrome.js +43 -0
  26. package/dist/cjs/preview-host/defaultApplicationChrome.js.map +1 -0
  27. package/dist/cjs/preview-host/diagnosePreview.js +100 -0
  28. package/dist/cjs/preview-host/diagnosePreview.js.map +1 -0
  29. package/dist/cjs/preview-host/previewProfile.js +53 -0
  30. package/dist/cjs/preview-host/previewProfile.js.map +1 -0
  31. package/dist/cjs/preview-host/previewSampleData.js +116 -0
  32. package/dist/cjs/preview-host/previewSampleData.js.map +1 -0
  33. package/dist/cjs/preview-host/resolvePreviewPlan.js +126 -0
  34. package/dist/cjs/preview-host/resolvePreviewPlan.js.map +1 -0
  35. package/dist/cjs/preview-host/useSizeClass.js +23 -0
  36. package/dist/cjs/preview-host/useSizeClass.js.map +1 -0
  37. package/dist/esm/index.d.ts +1 -0
  38. package/dist/esm/index.d.ts.map +1 -1
  39. package/dist/esm/index.js +17 -0
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/preview-host/ApplicationDocument.d.ts +7 -0
  42. package/dist/esm/preview-host/ApplicationDocument.d.ts.map +1 -0
  43. package/dist/esm/preview-host/ApplicationDocument.js +15 -0
  44. package/dist/esm/preview-host/ApplicationDocument.js.map +1 -0
  45. package/dist/esm/preview-host/ApplicationHost.d.ts +24 -0
  46. package/dist/esm/preview-host/ApplicationHost.d.ts.map +1 -0
  47. package/dist/esm/preview-host/ApplicationHost.js +132 -0
  48. package/dist/esm/preview-host/ApplicationHost.js.map +1 -0
  49. package/dist/esm/preview-host/FlowArrangementView.d.ts +7 -0
  50. package/dist/esm/preview-host/FlowArrangementView.d.ts.map +1 -0
  51. package/dist/esm/preview-host/FlowArrangementView.js +73 -0
  52. package/dist/esm/preview-host/FlowArrangementView.js.map +1 -0
  53. package/dist/esm/preview-host/PreviewProblem.d.ts +26 -0
  54. package/dist/esm/preview-host/PreviewProblem.d.ts.map +1 -0
  55. package/dist/esm/preview-host/PreviewProblem.js +28 -0
  56. package/dist/esm/preview-host/PreviewProblem.js.map +1 -0
  57. package/dist/esm/preview-host/PreviewProblemsView.d.ts +7 -0
  58. package/dist/esm/preview-host/PreviewProblemsView.d.ts.map +1 -0
  59. package/dist/esm/preview-host/PreviewProblemsView.js +49 -0
  60. package/dist/esm/preview-host/PreviewProblemsView.js.map +1 -0
  61. package/dist/esm/preview-host/PreviewSliceFragment.d.ts +3 -0
  62. package/dist/esm/preview-host/PreviewSliceFragment.d.ts.map +1 -0
  63. package/dist/esm/preview-host/PreviewSliceFragment.js +53 -0
  64. package/dist/esm/preview-host/PreviewSliceFragment.js.map +1 -0
  65. package/dist/esm/preview-host/PreviewTarget.d.ts +10 -0
  66. package/dist/esm/preview-host/PreviewTarget.d.ts.map +1 -0
  67. package/dist/esm/preview-host/PreviewTarget.js +15 -0
  68. package/dist/esm/preview-host/PreviewTarget.js.map +1 -0
  69. package/dist/esm/preview-host/PreviewTemplate.d.ts +5 -0
  70. package/dist/esm/preview-host/PreviewTemplate.d.ts.map +1 -0
  71. package/dist/esm/preview-host/PreviewTemplate.js +45 -0
  72. package/dist/esm/preview-host/PreviewTemplate.js.map +1 -0
  73. package/dist/esm/preview-host/PreviewToast.d.ts +10 -0
  74. package/dist/esm/preview-host/PreviewToast.d.ts.map +1 -0
  75. package/dist/esm/preview-host/PreviewToast.js +43 -0
  76. package/dist/esm/preview-host/PreviewToast.js.map +1 -0
  77. package/dist/esm/preview-host/ScenePreviewHost.d.ts +32 -0
  78. package/dist/esm/preview-host/ScenePreviewHost.d.ts.map +1 -0
  79. package/dist/esm/preview-host/ScenePreviewHost.js +288 -0
  80. package/dist/esm/preview-host/ScenePreviewHost.js.map +1 -0
  81. package/dist/esm/preview-host/composeApplication.d.ts +34 -0
  82. package/dist/esm/preview-host/composeApplication.d.ts.map +1 -0
  83. package/dist/esm/preview-host/composeApplication.js +239 -0
  84. package/dist/esm/preview-host/composeApplication.js.map +1 -0
  85. package/dist/esm/preview-host/defaultApplicationChrome.d.ts +4 -0
  86. package/dist/esm/preview-host/defaultApplicationChrome.d.ts.map +1 -0
  87. package/dist/esm/preview-host/defaultApplicationChrome.js +41 -0
  88. package/dist/esm/preview-host/defaultApplicationChrome.js.map +1 -0
  89. package/dist/esm/preview-host/diagnosePreview.d.ts +22 -0
  90. package/dist/esm/preview-host/diagnosePreview.d.ts.map +1 -0
  91. package/dist/esm/preview-host/diagnosePreview.js +98 -0
  92. package/dist/esm/preview-host/diagnosePreview.js.map +1 -0
  93. package/dist/esm/preview-host/index.d.ts +18 -0
  94. package/dist/esm/preview-host/index.d.ts.map +1 -0
  95. package/dist/esm/preview-host/index.js +18 -0
  96. package/dist/esm/preview-host/index.js.map +1 -0
  97. package/dist/esm/preview-host/previewProfile.d.ts +6 -0
  98. package/dist/esm/preview-host/previewProfile.d.ts.map +1 -0
  99. package/dist/esm/preview-host/previewProfile.js +49 -0
  100. package/dist/esm/preview-host/previewProfile.js.map +1 -0
  101. package/dist/esm/preview-host/previewSampleData.d.ts +22 -0
  102. package/dist/esm/preview-host/previewSampleData.d.ts.map +1 -0
  103. package/dist/esm/preview-host/previewSampleData.js +113 -0
  104. package/dist/esm/preview-host/previewSampleData.js.map +1 -0
  105. package/dist/esm/preview-host/resolvePreviewPlan.d.ts +18 -0
  106. package/dist/esm/preview-host/resolvePreviewPlan.d.ts.map +1 -0
  107. package/dist/esm/preview-host/resolvePreviewPlan.js +124 -0
  108. package/dist/esm/preview-host/resolvePreviewPlan.js.map +1 -0
  109. package/dist/esm/preview-host/useSizeClass.d.ts +3 -0
  110. package/dist/esm/preview-host/useSizeClass.d.ts.map +1 -0
  111. package/dist/esm/preview-host/useSizeClass.js +21 -0
  112. package/dist/esm/preview-host/useSizeClass.js.map +1 -0
  113. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  114. package/index.ts +1 -0
  115. package/package.json +8 -8
  116. package/preview-host/ApplicationDocument.ts +31 -0
  117. package/preview-host/ApplicationHost.tsx +155 -0
  118. package/preview-host/FlowArrangementView.tsx +59 -0
  119. package/preview-host/PreviewProblem.ts +68 -0
  120. package/preview-host/PreviewProblemsView.tsx +31 -0
  121. package/preview-host/PreviewSliceFragment.ts +43 -0
  122. package/preview-host/PreviewTarget.ts +19 -0
  123. package/preview-host/PreviewTemplate.tsx +45 -0
  124. package/preview-host/PreviewToast.tsx +32 -0
  125. package/preview-host/ScenePreviewHost.tsx +250 -0
  126. package/preview-host/composeApplication.ts +273 -0
  127. package/preview-host/defaultApplicationChrome.ts +33 -0
  128. package/preview-host/diagnosePreview.ts +132 -0
  129. package/preview-host/index.ts +20 -0
  130. package/preview-host/previewProfile.ts +47 -0
  131. package/preview-host/previewSampleData.ts +144 -0
  132. package/preview-host/resolvePreviewPlan.ts +100 -0
  133. package/preview-host/useSizeClass.ts +23 -0
  134. package/shell/layout.css +22 -0
@@ -0,0 +1,132 @@
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 { ApplicationStructure, NavigationModel, Screen, SampleDataPack, Toolbar, ToolbarButton } from '@cratis/scene.model';
5
+ import {
6
+ ApplicationStructureIndex, collectComponents, diagnoseNavigationRegions, resolveScreenTemplates,
7
+ toolbarButtonFromItem, validateSampleDataSelection, validateToolbarButtons,
8
+ } from '@cratis/scene.engine';
9
+ import { ApplicationDocument } from './ApplicationDocument';
10
+ import { CompositionOptions, findLayout, templatePoolFor, templatesOf } from './composeApplication';
11
+ import { PreviewPlan, resolvePreviewPlan } from './resolvePreviewPlan';
12
+ import { PreviewTarget } from './PreviewTarget';
13
+ import { PreviewProblem, PreviewProblemCode, PreviewProblemSeverity } from './PreviewProblem';
14
+ import { previewProfile } from './previewProfile';
15
+
16
+ /** What {@link diagnosePreview} reads. */
17
+ export interface PreviewDiagnosisInput {
18
+ document: ApplicationDocument;
19
+ structure: ApplicationStructure | ApplicationStructureIndex;
20
+ navigation?: NavigationModel;
21
+ target: PreviewTarget;
22
+
23
+ /** The packs available, for checking the profile's sample data selection. */
24
+ packs?: SampleDataPack[];
25
+
26
+ /** The composition options the host renders with, for the blueprint's layouts and templates. */
27
+ composition?: CompositionOptions;
28
+ }
29
+
30
+ export interface PreviewDiagnosis {
31
+ /** The plan the target resolved to; absent when it did not resolve. */
32
+ plan?: PreviewPlan;
33
+
34
+ /** The screen the preview's page is, when the plan has a feature. */
35
+ screen?: Screen;
36
+
37
+ problems: PreviewProblem[];
38
+ }
39
+
40
+ const make = (code: PreviewProblemCode, severity: PreviewProblemSeverity, message: string, nodeId?: string): PreviewProblem =>
41
+ ({ code, severity, message, ...(nodeId === undefined ? {} : { nodeId }) });
42
+
43
+ /**
44
+ * Finds what would break or mislead a preview before it renders (PRV8), using the same engine validators the editor and the
45
+ * compiler use - nothing is judged here that the engine does not already judge:
46
+ *
47
+ * - the target must be in the structure, and a module must have a feature;
48
+ * - the page - the screen named by the target's feature - must exist, with a layout and a template that can be found and placed;
49
+ * - navigation (`diagnoseNavigationRegions`), toolbar buttons (`validateToolbarButtons`, whatever their severity), the structure's own
50
+ * ids and the sample data selection are reported as warnings: the preview still renders, an item or button just will not behave
51
+ * as authored.
52
+ *
53
+ * Errors block the preview - it shows these instead of a blank screen; the node id is what the host links to.
54
+ */
55
+ export function diagnosePreview(input: PreviewDiagnosisInput): PreviewDiagnosis {
56
+ const index = input.structure instanceof ApplicationStructureIndex ? input.structure : new ApplicationStructureIndex(input.structure);
57
+ const options = input.composition ?? {};
58
+ const problems: PreviewProblem[] = [];
59
+
60
+ for (const diagnostic of index.diagnostics) {
61
+ problems.push(make(PreviewProblemCode.StructureInvalid, PreviewProblemSeverity.Warning, diagnostic.message, diagnostic.id));
62
+ }
63
+
64
+ const { plan, problems: targetProblems } = resolvePreviewPlan(index, input.target);
65
+ problems.push(...targetProblems);
66
+
67
+ let screen: Screen | undefined;
68
+ if (plan?.feature !== undefined) {
69
+ screen = input.document.screens.find(candidate => candidate.name === plan.feature);
70
+ // A slice is previewed over a minimal host page when its feature has no screen; a module or feature has nothing to show without one.
71
+ if (!screen && plan.slice === undefined) {
72
+ problems.push(make(PreviewProblemCode.ScreenMissing, PreviewProblemSeverity.Error, `The feature '${plan.feature}' has no screen in the document, so there is nothing to render.`, plan.feature));
73
+ } else if (screen) {
74
+ problems.push(...diagnoseScreen(input.document, screen, options));
75
+ }
76
+ }
77
+
78
+ if (input.navigation) {
79
+ for (const diagnostic of diagnoseNavigationRegions(index, input.navigation)) {
80
+ problems.push(make(PreviewProblemCode.NavigationInvalid, PreviewProblemSeverity.Warning, diagnostic.message, diagnostic.entry));
81
+ }
82
+ }
83
+
84
+ const buttons = input.document.screens.flatMap(candidate => collectComponents(candidate)
85
+ .filter(component => component.componentName.endsWith('toolbarItem'))
86
+ .map(component => toolbarButtonFromItem(component.id, component.properties as Record<string, unknown>))
87
+ .filter((button): button is ToolbarButton => button !== undefined));
88
+ const toolbar: Toolbar = { buttons };
89
+ for (const diagnostic of validateToolbarButtons(index, toolbar)) {
90
+ problems.push(make(PreviewProblemCode.ToolbarInvalid, PreviewProblemSeverity.Warning, diagnostic.message, diagnostic.button));
91
+ }
92
+
93
+ const selection = previewProfile(input.document.uiProfiles).sampleData;
94
+ if (selection) {
95
+ for (const message of validateSampleDataSelection(selection, input.packs ?? [])) {
96
+ problems.push(make(PreviewProblemCode.SampleDataInvalid, PreviewProblemSeverity.Warning, message));
97
+ }
98
+ }
99
+
100
+ return { plan, screen, problems };
101
+ }
102
+
103
+ function diagnoseScreen(document: ApplicationDocument, screen: Screen, options: CompositionOptions): PreviewProblem[] {
104
+ const problems: PreviewProblem[] = [];
105
+ const layout = findLayout(document, screen.layout, options);
106
+ if (!layout) {
107
+ problems.push(make(PreviewProblemCode.LayoutMissing, PreviewProblemSeverity.Error, `The screen '${screen.name}' uses the layout '${screen.layout}', which neither the document nor the blueprint provides.`, screen.name));
108
+ }
109
+
110
+ if (screen.screenTemplate === undefined) return problems;
111
+
112
+ const templates = templatesOf(document, options);
113
+ if (!templates.some(candidate => candidate.name === screen.screenTemplate)) {
114
+ problems.push(make(PreviewProblemCode.TemplateMissing, PreviewProblemSeverity.Error, `The screen '${screen.name}' uses the template '${screen.screenTemplate}', which neither the document nor the blueprint provides.`, screen.name));
115
+ return problems;
116
+ }
117
+
118
+ if (layout) {
119
+ const resolution = resolveScreenTemplates(layout, templatePoolFor(document, screen, options));
120
+ const unplaced = resolution.unplaced.find(candidate => candidate.template === screen.screenTemplate);
121
+ if (unplaced) {
122
+ problems.push(make(PreviewProblemCode.TemplateUnplaced, PreviewProblemSeverity.Error, `The template '${unplaced.template}' fits the slot '${unplaced.slot}', which ${unplaced.candidates.length === 0 ? 'nothing declares' : `${unplaced.candidates.join(' and ')} all declare`}.`, unplaced.template));
123
+ }
124
+
125
+ const cycle = resolution.cycles.find(candidate => candidate.includes(screen.screenTemplate!));
126
+ if (cycle) {
127
+ problems.push(make(PreviewProblemCode.TemplateUnplaced, PreviewProblemSeverity.Error, `The template '${screen.screenTemplate}' nests inside itself (${cycle.join(' → ')}).`, screen.screenTemplate));
128
+ }
129
+ }
130
+
131
+ return problems;
132
+ }
@@ -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
+ export * from './PreviewTarget';
5
+ export * from './PreviewProblem';
6
+ export * from './ApplicationDocument';
7
+ export * from './previewProfile';
8
+ export * from './defaultApplicationChrome';
9
+ export * from './FlowArrangementView';
10
+ export * from './useSizeClass';
11
+ export * from './PreviewTemplate';
12
+ export * from './composeApplication';
13
+ export * from './resolvePreviewPlan';
14
+ export * from './diagnosePreview';
15
+ export * from './previewSampleData';
16
+ export * from './PreviewSliceFragment';
17
+ export * from './PreviewToast';
18
+ export * from './PreviewProblemsView';
19
+ export * from './ApplicationHost';
20
+ export * from './ScenePreviewHost';
@@ -0,0 +1,47 @@
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 { UiProfile } from '@cratis/scene.model';
5
+ import type { PackageCatalog } from '@cratis/scene.engine';
6
+ import { ComponentName } from '../ComponentName';
7
+ import { defaultBlueprintName } from '../packageName';
8
+ import { coreComponents } from '@cratis/scene.react';
9
+
10
+ /** The packages every preview and Stage application resolves bare component names against, in ascending priority. */
11
+ export const requiredPackages = ['core', defaultBlueprintName];
12
+
13
+ /** What the packages every host has declare, so a bare name resolves to the package that provides it. */
14
+ export const blueprintPackageCatalog: PackageCatalog = {
15
+ core: Object.keys(coreComponents).map(key => key.slice(key.indexOf(':') + 1)),
16
+ [defaultBlueprintName]: Object.values(ComponentName),
17
+ };
18
+
19
+ /**
20
+ * The profile an application renders against: the document's own web profile (else its first), with the packages every host
21
+ * renders with admitted even when the model never named them - without that, a template written against the blueprint's
22
+ * vocabulary would come out as a column of dashed placeholders. A host with more packages (PrimeReact, a custom set) lists them
23
+ * in `additionalPackages`.
24
+ *
25
+ * The profile's `sampleData` is carried through: it is where the application chose its active pack (spec 11, SD3).
26
+ *
27
+ * @param profiles The profiles the document declares.
28
+ * @param additionalPackages Packages the host renders with beyond the required ones, lowest priority first.
29
+ * @returns The profile to resolve component names with.
30
+ */
31
+ export function previewProfile(profiles: UiProfile[] | undefined, additionalPackages: string[] = []): UiProfile {
32
+ const declared = profiles?.find(profile => profile.targetPlatform === 'web') ?? profiles?.[0];
33
+ const packages = [...(declared?.packages ?? [])];
34
+ for (const required of [...requiredPackages, ...additionalPackages]) {
35
+ if (!packages.includes(required)) packages.push(required);
36
+ }
37
+
38
+ return {
39
+ name: declared?.name ?? 'Preview',
40
+ targetPlatform: declared?.targetPlatform ?? 'web',
41
+ packages,
42
+ defaultSizeClass: declared?.defaultSizeClass,
43
+ layout: declared?.layout,
44
+ theme: declared?.theme,
45
+ sampleData: declared?.sampleData,
46
+ };
47
+ }
@@ -0,0 +1,144 @@
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 { BindingExpression, SampleDataPack, SampleShape } from '@cratis/scene.model';
5
+ import { BindingResolver, SampleDataResolution, resolveSampleData } from '@cratis/scene.engine';
6
+ import type { SliceCommandField } from '@cratis/scene.react';
7
+ import { ApplicationDocument } from './ApplicationDocument';
8
+ import { previewProfile } from './previewProfile';
9
+
10
+ /**
11
+ * Which sample data a preview shows (spec 11, PRV6). The active pack is the application profile's (`sampleData.activePack`),
12
+ * chosen once for the whole application; `sessionActivePack` is the preview strip's selector, a session-only override of that
13
+ * setting - not a second source of truth. Resolution is `resolveSampleData`'s: bound pack, active pack, the blueprint's default
14
+ * pack, then generated data, and the same seed gives the same rows every time.
15
+ */
16
+ export interface PreviewSampleData {
17
+ /** Every pack available: the blueprints' and the author's own. */
18
+ packs: SampleDataPack[];
19
+
20
+ /** The preview strip's choice for this session: a pack id, or the empty string for generated data. Absent leaves the profile's. */
21
+ sessionActivePack?: string;
22
+
23
+ /** The default pack of the blueprint the templates came from. */
24
+ blueprintDefaultPack?: string;
25
+
26
+ /** The locale of generated data when no pack supplies one. */
27
+ locale?: string;
28
+
29
+ /** The seed for generated data; 1 by default, so previews are stable. */
30
+ seed?: number;
31
+
32
+ /** The number of generated rows. */
33
+ rowCount?: number;
34
+
35
+ /** The shape of the rows of each read model or query, by name, for generating rows where no pack has them. */
36
+ shapes?: Record<string, SampleShape>;
37
+ }
38
+
39
+ /** What a preview reads its data from. */
40
+ export interface PreviewSampleDataSource {
41
+ /** The rows for a read model or query; empty when nothing supplies any. */
42
+ rows(target: string, shape?: SampleShape): SampleDataResolution;
43
+
44
+ /** The rows of every query the document binds, by query name - the `queryResults` of the render scope. */
45
+ queryResults: Record<string, unknown>;
46
+
47
+ /** Resolves a binding that names a query against the sample rows. */
48
+ resolveBinding: BindingResolver;
49
+
50
+ /** Starting values for a command's fields from the sample rows, for the task dialog of a slice. */
51
+ commandFields(command: string, fields: SliceCommandField[]): SliceCommandField[];
52
+ }
53
+
54
+ const noSample: PreviewSampleData = { packs: [] };
55
+
56
+ /** The names of the queries a document binds: every `query` it names, in bindings and on components. */
57
+ export function collectQueryNames(value: unknown): string[] {
58
+ const names = new Set<string>();
59
+ const visit = (candidate: unknown) => {
60
+ if (candidate === null || typeof candidate !== 'object') return;
61
+ if (Array.isArray(candidate)) {
62
+ candidate.forEach(visit);
63
+ return;
64
+ }
65
+
66
+ for (const [key, entry] of Object.entries(candidate as Record<string, unknown>)) {
67
+ if (key === 'query' && typeof entry === 'string' && entry.length > 0) names.add(entry);
68
+ else visit(entry);
69
+ }
70
+ };
71
+
72
+ visit(value);
73
+ return [...names];
74
+ }
75
+
76
+ function readPath(source: unknown, path: string): unknown {
77
+ if (!path) return source;
78
+ return path.split('.').reduce<unknown>((current, segment) => {
79
+ if (current === null || current === undefined || typeof current !== 'object') return undefined;
80
+ if (Array.isArray(current)) return /^(0|[1-9][0-9]*)$/.test(segment) ? current[Number(segment)] : undefined;
81
+ return Object.hasOwn(current, segment) ? (current as Record<string, unknown>)[segment] : undefined;
82
+ }, source);
83
+ }
84
+
85
+ function shapeOfFields(name: string, fields: SliceCommandField[]): SampleShape {
86
+ return { name, fields: fields.map(field => ({ name: field.name, type: field.type })) };
87
+ }
88
+
89
+ function scalar(value: unknown): string | number | boolean | undefined {
90
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? value : undefined;
91
+ }
92
+
93
+ /**
94
+ * Plugs the sample data resolution into what a preview renders with (PRV6). Nothing here decides what a row looks like - that is
95
+ * `resolveSampleData` - this only asks it, with the application profile's selection, for the queries the document binds and the
96
+ * commands its slices accept. It is design/preview time only: a production host never builds one (SD6).
97
+ *
98
+ * @param application The document whose profile carries the selection and whose bindings are scanned.
99
+ * @param sample The packs, session override and seed.
100
+ */
101
+ export function createPreviewSampleData(application: ApplicationDocument, sample: PreviewSampleData = noSample): PreviewSampleDataSource {
102
+ const profile = previewProfile(application.uiProfiles);
103
+ const context = {
104
+ packs: sample.packs,
105
+ profile,
106
+ sessionActivePack: sample.sessionActivePack,
107
+ blueprintDefaultPack: sample.blueprintDefaultPack,
108
+ locale: sample.locale,
109
+ seed: sample.seed,
110
+ rowCount: sample.rowCount,
111
+ isDesignOrPreview: true,
112
+ };
113
+
114
+ const rows = (target: string, shape?: SampleShape): SampleDataResolution => {
115
+ const known = shape ?? sample.shapes?.[target];
116
+ const resolved = resolveSampleData({ target, ...(known === undefined ? {} : { shape: known }) }, context);
117
+ if (resolved.rows.length > 0 || !target.includes('.')) return resolved;
118
+
119
+ const short = target.slice(target.lastIndexOf('.') + 1);
120
+ return resolveSampleData({ target: short, ...(known === undefined ? {} : { shape: known }) }, context);
121
+ };
122
+
123
+ const queryResults: Record<string, unknown> = {};
124
+ for (const name of collectQueryNames([application.screens, application.screenTemplates, application.dialogTemplates ?? [], application.layouts])) {
125
+ const resolved = rows(name);
126
+ if (resolved.rows.length > 0) queryResults[name] = resolved.rows;
127
+ }
128
+
129
+ return {
130
+ rows,
131
+ queryResults,
132
+ resolveBinding: (binding: BindingExpression) => binding.query ? readPath(queryResults[binding.query], binding.path) : undefined,
133
+ commandFields: (command, fields) => {
134
+ const first = rows(command, shapeOfFields(command, fields)).rows[0];
135
+ if (!first) return fields;
136
+
137
+ return fields.map(field => {
138
+ const key = Object.keys(first).find(candidate => candidate.toLowerCase() === field.name.toLowerCase());
139
+ const value = key === undefined ? undefined : scalar(first[key]);
140
+ return value === undefined ? field : { ...field, initialValue: value };
141
+ });
142
+ },
143
+ };
144
+ }
@@ -0,0 +1,100 @@
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 { ApplicationStructure, FeatureNode, SliceKind } from '@cratis/scene.model';
5
+ import { ApplicationStructureIndex } from '@cratis/scene.engine';
6
+ import { PreviewTarget, isSliceTarget } from './PreviewTarget';
7
+ import { PreviewProblem, PreviewProblemCode, PreviewProblemSeverity } from './PreviewProblem';
8
+
9
+ /** What a target resolved to in the application structure. */
10
+ export interface PreviewPlan {
11
+ /** `module`, `feature`, `stateChange` or `stateView`. */
12
+ kind: 'module' | 'feature' | 'stateChange' | 'stateView';
13
+
14
+ /** The module the preview is in. */
15
+ module: string;
16
+
17
+ /** The qualified id of the feature whose screen is the page: the module's first feature, the feature itself, or the slice's feature. */
18
+ feature?: string;
19
+
20
+ /** The qualified id of the slice, for a slice target. */
21
+ slice?: string;
22
+
23
+ /** The command a State Change slice accepts. */
24
+ command?: string;
25
+
26
+ /** The display names from the module down to the target - what the preview strip shows as `Sales › Orders` (PRV7). */
27
+ breadcrumb: string[];
28
+ }
29
+
30
+ export interface PreviewPlanResult {
31
+ plan?: PreviewPlan;
32
+ problems: PreviewProblem[];
33
+ }
34
+
35
+ const error = (code: PreviewProblemCode, message: string, nodeId?: string): PreviewProblem => ({ code, severity: PreviewProblemSeverity.Error, message, ...(nodeId === undefined ? {} : { nodeId }) });
36
+
37
+ /** The feature a qualified id names, and the display names of the features above it, outermost first. */
38
+ function featurePath(structure: ApplicationStructure, qualified: string): { feature: FeatureNode; names: string[] } | undefined {
39
+ const [moduleId, ...rest] = qualified.split('.');
40
+ let features = structure.modules.find(module => module.id === moduleId)?.features;
41
+ const names: string[] = [];
42
+ let found: FeatureNode | undefined;
43
+ for (const id of rest) {
44
+ found = features?.find(candidate => candidate.id === id);
45
+ if (!found) return undefined;
46
+ names.push(found.name);
47
+ features = found.features;
48
+ }
49
+
50
+ return found ? { feature: found, names } : undefined;
51
+ }
52
+
53
+ /**
54
+ * Resolves a preview target against the application structure (PRV3): a module previews its first feature, a feature itself,
55
+ * a slice the page of the feature it belongs to with the slice itself opened (State Change) or isolated (State View).
56
+ * A target that is not in the structure resolves to a problem, not to a blank screen.
57
+ */
58
+ export function resolvePreviewPlan(structure: ApplicationStructure | ApplicationStructureIndex, target: PreviewTarget): PreviewPlanResult {
59
+ const index = structure instanceof ApplicationStructureIndex ? structure : new ApplicationStructureIndex(structure);
60
+
61
+ if (isSliceTarget(target)) {
62
+ const slice = index.findSlice(target.slice);
63
+ if (!slice) return { problems: [error(PreviewProblemCode.TargetNotFound, `The slice '${target.slice}' is not in the application.`, target.slice)] };
64
+ if (slice.kind === SliceKind.Automation) {
65
+ return { problems: [error(PreviewProblemCode.SliceNotPreviewable, `The slice '${target.slice}' is an automation: it reacts to events and has nothing to show.`, target.slice)] };
66
+ }
67
+
68
+ const segments = target.slice.split('.');
69
+ const module = segments[0];
70
+ const featureId = segments.length > 2 ? segments.slice(0, -1).join('.') : undefined;
71
+ const moduleNode = index.findModule(module);
72
+ const path = featureId ? featurePath(index.structure, featureId) : undefined;
73
+ const containerFeature = featureId ?? index.firstFeatureId(module);
74
+ return {
75
+ plan: {
76
+ kind: slice.kind === SliceKind.StateChange ? 'stateChange' : 'stateView',
77
+ module,
78
+ feature: containerFeature,
79
+ slice: target.slice,
80
+ ...(slice.kind === SliceKind.StateChange ? { command: slice.command ?? slice.id } : {}),
81
+ breadcrumb: [moduleNode?.name ?? module, ...(path?.names ?? []), slice.name],
82
+ },
83
+ problems: [],
84
+ };
85
+ }
86
+
87
+ const moduleNode = index.findModule(target.module);
88
+ if (!moduleNode) return { problems: [error(PreviewProblemCode.TargetNotFound, `The module '${target.module}' is not in the application.`, target.module)] };
89
+
90
+ if (target.feature === undefined) {
91
+ const first = index.firstFeatureId(target.module);
92
+ if (first === undefined) return { problems: [error(PreviewProblemCode.ModuleWithoutFeatures, `The module '${moduleNode.name}' has no feature to show.`, target.module)] };
93
+ return { plan: { kind: 'module', module: target.module, feature: first, breadcrumb: [moduleNode.name, ...(featurePath(index.structure, first)?.names ?? [])] }, problems: [] };
94
+ }
95
+
96
+ const qualified = target.feature.startsWith(`${target.module}.`) ? target.feature : `${target.module}.${target.feature}`;
97
+ const path = featurePath(index.structure, qualified);
98
+ if (!path) return { problems: [error(PreviewProblemCode.TargetNotFound, `The feature '${qualified}' is not in the application.`, qualified)] };
99
+ return { plan: { kind: 'feature', module: target.module, feature: qualified, breadcrumb: [moduleNode.name, ...path.names] }, problems: [] };
100
+ }
@@ -0,0 +1,23 @@
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 { useEffect, useState } from 'react';
5
+ import type { SizeClass } from '@cratis/scene.model';
6
+ import { computeSizeClass } from '@cratis/scene.engine';
7
+
8
+ /**
9
+ * Tracks the browser window's {@link SizeClass}, recomputing whenever it resizes across a boundary the
10
+ * shared engine cares about. This is what lets a modeled `when width compact` override actually take
11
+ * effect on a narrow window instead of the arrangement engine's size-aware logic never being asked.
12
+ */
13
+ export function useSizeClass(): SizeClass {
14
+ const [sizeClass, setSizeClass] = useState<SizeClass>(() => computeSizeClass(globalThis.innerWidth, globalThis.innerHeight));
15
+
16
+ useEffect(() => {
17
+ const onResize = () => setSizeClass(computeSizeClass(globalThis.innerWidth, globalThis.innerHeight));
18
+ globalThis.addEventListener('resize', onResize);
19
+ return () => globalThis.removeEventListener('resize', onResize);
20
+ }, []);
21
+
22
+ return sizeClass;
23
+ }
package/shell/layout.css CHANGED
@@ -1059,6 +1059,28 @@
1059
1059
  align-items: center;
1060
1060
  gap: 0.5rem;
1061
1061
  }
1062
+ /* The template wrapper and the preview host's own chrome. The shell, the navigation regions and the slice dialog are styled by
1063
+ `@cratis/scene.blueprint.default/styles.css`; import both. */
1064
+ .flow-container { display: flex; min-width: 0; min-height: 0; }
1065
+ .flow-container--row { flex-direction: row; }
1066
+ .flow-container--column { flex-direction: column; }
1067
+ .flow-container--grid { display: grid; }
1068
+ .flow-child { display: flex; min-width: 0; min-height: 0; }
1069
+ .flow-child > .flow-container,
1070
+ .flow-child > .scene-template-slot { flex: 1 1 auto; }
1071
+ .scene-template { display: flex; flex-direction: column; gap: var(--layout-gap, 1rem); min-width: 0; }
1072
+ .scene-template-slot { display: grid; gap: var(--layout-gap, 1rem); min-width: 0; width: 100%; align-content: start; }
1073
+
1074
+ .scene-preview-problems { margin: 1rem; padding: 1rem 1.25rem; border: 1px solid #e11d48; border-radius: 0.75rem; background: #fff1f2; color: #881337; }
1075
+ .scene-preview-problems h2 { margin: 0 0 0.5rem; font-size: 1.1rem; }
1076
+ .scene-preview-problems ul { margin: 0; padding-left: 1.25rem; }
1077
+ .scene-preview-problems li[data-severity='warning'] { color: #7a4b00; }
1078
+ .scene-preview-problems code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 0.8rem; }
1079
+
1080
+ .scene-preview-toast { position: fixed; right: 1rem; bottom: 1rem; z-index: 1300; max-width: 26rem; padding: 0.75rem 1rem; border-radius: 0.75rem; background: #161b22; color: #fff; box-shadow: 0 0.75rem 2rem rgb(0 0 0 / 0.35); }
1081
+ .scene-preview-toast summary { cursor: pointer; margin-top: 0.25rem; color: #c9d1d9; }
1082
+ .scene-preview-toast pre { margin: 0.5rem 0 0; max-height: 14rem; overflow: auto; font-size: 0.75rem; }
1083
+
1062
1084
 
1063
1085
  /*
1064
1086
  * FOUNDATION COMPONENTS