@cratis/scene.react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/.storybook/main.ts +31 -0
  2. package/.storybook/preview.ts +12 -0
  3. package/NavBar/NavBar.tsx +53 -0
  4. package/NavBar/extractNavigationItem.ts +40 -0
  5. package/NavBar/for_NavBar/when_rendering_navigation_contributions.tsx +62 -0
  6. package/NavBar/for_extractNavigationItem/when_extracting_from_a_navigation_contribution.ts +53 -0
  7. package/NavBar/index.ts +5 -0
  8. package/README.md +40 -0
  9. package/SceneElementView.stories.tsx +67 -0
  10. package/SceneElementView.tsx +22 -0
  11. package/core/CoreButton.tsx +17 -0
  12. package/core/CoreCard.tsx +15 -0
  13. package/core/CoreText.tsx +13 -0
  14. package/core/coreComponents.ts +18 -0
  15. package/core/corePackage.ts +36 -0
  16. package/core/index.ts +8 -0
  17. package/dist/cjs/NavBar/NavBar.js +47 -0
  18. package/dist/cjs/NavBar/NavBar.js.map +1 -0
  19. package/dist/cjs/NavBar/extractNavigationItem.js +38 -0
  20. package/dist/cjs/NavBar/extractNavigationItem.js.map +1 -0
  21. package/dist/cjs/SceneElementView.js +21 -0
  22. package/dist/cjs/SceneElementView.js.map +1 -0
  23. package/dist/cjs/core/CoreButton.js +22 -0
  24. package/dist/cjs/core/CoreButton.js.map +1 -0
  25. package/dist/cjs/core/CoreCard.js +22 -0
  26. package/dist/cjs/core/CoreCard.js.map +1 -0
  27. package/dist/cjs/core/CoreText.js +19 -0
  28. package/dist/cjs/core/CoreText.js.map +1 -0
  29. package/dist/cjs/core/coreComponents.js +20 -0
  30. package/dist/cjs/core/coreComponents.js.map +1 -0
  31. package/dist/cjs/core/corePackage.js +41 -0
  32. package/dist/cjs/core/corePackage.js.map +1 -0
  33. package/dist/cjs/index.js +38 -0
  34. package/dist/cjs/index.js.map +1 -0
  35. package/dist/cjs/package.json +3 -0
  36. package/dist/cjs/packages/ScenePackageBundle.js +76 -0
  37. package/dist/cjs/packages/ScenePackageBundle.js.map +1 -0
  38. package/dist/cjs/renderer/UnresolvedComponent.js +28 -0
  39. package/dist/cjs/renderer/UnresolvedComponent.js.map +1 -0
  40. package/dist/cjs/renderer/createReactRenderer.js +48 -0
  41. package/dist/cjs/renderer/createReactRenderer.js.map +1 -0
  42. package/dist/cjs/theme/SceneThemeProvider.js +44 -0
  43. package/dist/cjs/theme/SceneThemeProvider.js.map +1 -0
  44. package/dist/cjs/theme/themeTokens.js +52 -0
  45. package/dist/cjs/theme/themeTokens.js.map +1 -0
  46. package/dist/esm/NavBar/NavBar.d.ts +7 -0
  47. package/dist/esm/NavBar/NavBar.d.ts.map +1 -0
  48. package/dist/esm/NavBar/NavBar.js +45 -0
  49. package/dist/esm/NavBar/NavBar.js.map +1 -0
  50. package/dist/esm/NavBar/extractNavigationItem.d.ts +3 -0
  51. package/dist/esm/NavBar/extractNavigationItem.d.ts.map +1 -0
  52. package/dist/esm/NavBar/extractNavigationItem.js +36 -0
  53. package/dist/esm/NavBar/extractNavigationItem.js.map +1 -0
  54. package/dist/esm/NavBar/for_NavBar/when_rendering_navigation_contributions.d.ts +2 -0
  55. package/dist/esm/NavBar/for_NavBar/when_rendering_navigation_contributions.d.ts.map +1 -0
  56. package/dist/esm/NavBar/for_NavBar/when_rendering_navigation_contributions.js +53 -0
  57. package/dist/esm/NavBar/for_NavBar/when_rendering_navigation_contributions.js.map +1 -0
  58. package/dist/esm/NavBar/for_extractNavigationItem/when_extracting_from_a_navigation_contribution.d.ts +2 -0
  59. package/dist/esm/NavBar/for_extractNavigationItem/when_extracting_from_a_navigation_contribution.d.ts.map +1 -0
  60. package/dist/esm/NavBar/for_extractNavigationItem/when_extracting_from_a_navigation_contribution.js +45 -0
  61. package/dist/esm/NavBar/for_extractNavigationItem/when_extracting_from_a_navigation_contribution.js.map +1 -0
  62. package/dist/esm/NavBar/index.d.ts +3 -0
  63. package/dist/esm/NavBar/index.d.ts.map +1 -0
  64. package/dist/esm/NavBar/index.js +3 -0
  65. package/dist/esm/NavBar/index.js.map +1 -0
  66. package/dist/esm/SceneElementView.d.ts +10 -0
  67. package/dist/esm/SceneElementView.d.ts.map +1 -0
  68. package/dist/esm/SceneElementView.js +19 -0
  69. package/dist/esm/SceneElementView.js.map +1 -0
  70. package/dist/esm/SceneElementView.stories.d.ts +15 -0
  71. package/dist/esm/SceneElementView.stories.d.ts.map +1 -0
  72. package/dist/esm/SceneElementView.stories.js +54 -0
  73. package/dist/esm/SceneElementView.stories.js.map +1 -0
  74. package/dist/esm/core/CoreButton.d.ts +3 -0
  75. package/dist/esm/core/CoreButton.d.ts.map +1 -0
  76. package/dist/esm/core/CoreButton.js +20 -0
  77. package/dist/esm/core/CoreButton.js.map +1 -0
  78. package/dist/esm/core/CoreCard.d.ts +3 -0
  79. package/dist/esm/core/CoreCard.d.ts.map +1 -0
  80. package/dist/esm/core/CoreCard.js +20 -0
  81. package/dist/esm/core/CoreCard.js.map +1 -0
  82. package/dist/esm/core/CoreText.d.ts +3 -0
  83. package/dist/esm/core/CoreText.d.ts.map +1 -0
  84. package/dist/esm/core/CoreText.js +17 -0
  85. package/dist/esm/core/CoreText.js.map +1 -0
  86. package/dist/esm/core/coreComponents.d.ts +3 -0
  87. package/dist/esm/core/coreComponents.d.ts.map +1 -0
  88. package/dist/esm/core/coreComponents.js +18 -0
  89. package/dist/esm/core/coreComponents.js.map +1 -0
  90. package/dist/esm/core/corePackage.d.ts +5 -0
  91. package/dist/esm/core/corePackage.d.ts.map +1 -0
  92. package/dist/esm/core/corePackage.js +38 -0
  93. package/dist/esm/core/corePackage.js.map +1 -0
  94. package/dist/esm/core/index.d.ts +6 -0
  95. package/dist/esm/core/index.d.ts.map +1 -0
  96. package/dist/esm/core/index.js +6 -0
  97. package/dist/esm/core/index.js.map +1 -0
  98. package/dist/esm/for_SceneElementView/when_rendering_a_tree.d.ts +2 -0
  99. package/dist/esm/for_SceneElementView/when_rendering_a_tree.d.ts.map +1 -0
  100. package/dist/esm/for_SceneElementView/when_rendering_a_tree.js +44 -0
  101. package/dist/esm/for_SceneElementView/when_rendering_a_tree.js.map +1 -0
  102. package/dist/esm/for_applyThemeTokens/when_switching_themes.d.ts +2 -0
  103. package/dist/esm/for_applyThemeTokens/when_switching_themes.d.ts.map +1 -0
  104. package/dist/esm/for_applyThemeTokens/when_switching_themes.js +59 -0
  105. package/dist/esm/for_applyThemeTokens/when_switching_themes.js.map +1 -0
  106. package/dist/esm/for_themeTokens/when_converting_a_token_name.d.ts +2 -0
  107. package/dist/esm/for_themeTokens/when_converting_a_token_name.d.ts.map +1 -0
  108. package/dist/esm/for_themeTokens/when_converting_a_token_name.js +16 -0
  109. package/dist/esm/for_themeTokens/when_converting_a_token_name.js.map +1 -0
  110. package/dist/esm/for_validatePackageBundle/when_the_bundle_and_manifest_disagree.d.ts +2 -0
  111. package/dist/esm/for_validatePackageBundle/when_the_bundle_and_manifest_disagree.d.ts.map +1 -0
  112. package/dist/esm/for_validatePackageBundle/when_the_bundle_and_manifest_disagree.js +59 -0
  113. package/dist/esm/for_validatePackageBundle/when_the_bundle_and_manifest_disagree.js.map +1 -0
  114. package/dist/esm/for_validatePackageBundle/when_the_bundle_matches_its_manifest.d.ts +2 -0
  115. package/dist/esm/for_validatePackageBundle/when_the_bundle_matches_its_manifest.d.ts.map +1 -0
  116. package/dist/esm/for_validatePackageBundle/when_the_bundle_matches_its_manifest.js +8 -0
  117. package/dist/esm/for_validatePackageBundle/when_the_bundle_matches_its_manifest.js.map +1 -0
  118. package/dist/esm/index.d.ts +7 -0
  119. package/dist/esm/index.d.ts.map +1 -0
  120. package/dist/esm/index.js +14 -0
  121. package/dist/esm/index.js.map +1 -0
  122. package/dist/esm/package.json +3 -0
  123. package/dist/esm/packages/ScenePackageBundle.d.ts +15 -0
  124. package/dist/esm/packages/ScenePackageBundle.d.ts.map +1 -0
  125. package/dist/esm/packages/ScenePackageBundle.js +72 -0
  126. package/dist/esm/packages/ScenePackageBundle.js.map +1 -0
  127. package/dist/esm/packages/index.d.ts +2 -0
  128. package/dist/esm/packages/index.d.ts.map +1 -0
  129. package/dist/esm/packages/index.js +2 -0
  130. package/dist/esm/packages/index.js.map +1 -0
  131. package/dist/esm/renderer/ComponentRegistry.d.ts +8 -0
  132. package/dist/esm/renderer/ComponentRegistry.d.ts.map +1 -0
  133. package/dist/esm/renderer/ComponentRegistry.js +2 -0
  134. package/dist/esm/renderer/ComponentRegistry.js.map +1 -0
  135. package/dist/esm/renderer/UnresolvedComponent.d.ts +3 -0
  136. package/dist/esm/renderer/UnresolvedComponent.d.ts.map +1 -0
  137. package/dist/esm/renderer/UnresolvedComponent.js +26 -0
  138. package/dist/esm/renderer/UnresolvedComponent.js.map +1 -0
  139. package/dist/esm/renderer/createReactRenderer.d.ts +5 -0
  140. package/dist/esm/renderer/createReactRenderer.d.ts.map +1 -0
  141. package/dist/esm/renderer/createReactRenderer.js +46 -0
  142. package/dist/esm/renderer/createReactRenderer.js.map +1 -0
  143. package/dist/esm/renderer/index.d.ts +4 -0
  144. package/dist/esm/renderer/index.d.ts.map +1 -0
  145. package/dist/esm/renderer/index.js +4 -0
  146. package/dist/esm/renderer/index.js.map +1 -0
  147. package/dist/esm/theme/SceneThemeProvider.d.ts +10 -0
  148. package/dist/esm/theme/SceneThemeProvider.d.ts.map +1 -0
  149. package/dist/esm/theme/SceneThemeProvider.js +41 -0
  150. package/dist/esm/theme/SceneThemeProvider.js.map +1 -0
  151. package/dist/esm/theme/index.d.ts +3 -0
  152. package/dist/esm/theme/index.d.ts.map +1 -0
  153. package/dist/esm/theme/index.js +3 -0
  154. package/dist/esm/theme/index.js.map +1 -0
  155. package/dist/esm/theme/themeTokens.d.ts +5 -0
  156. package/dist/esm/theme/themeTokens.d.ts.map +1 -0
  157. package/dist/esm/theme/themeTokens.js +48 -0
  158. package/dist/esm/theme/themeTokens.js.map +1 -0
  159. package/dist/esm/tsconfig.tsbuildinfo +1 -0
  160. package/for_SceneElementView/when_rendering_a_tree.tsx +53 -0
  161. package/for_applyThemeTokens/when_switching_themes.ts +74 -0
  162. package/for_themeTokens/when_converting_a_token_name.ts +22 -0
  163. package/for_validatePackageBundle/when_the_bundle_and_manifest_disagree.ts +72 -0
  164. package/for_validatePackageBundle/when_the_bundle_matches_its_manifest.ts +12 -0
  165. package/index.ts +9 -0
  166. package/package.json +60 -0
  167. package/packages/ScenePackageBundle.ts +137 -0
  168. package/packages/index.ts +4 -0
  169. package/renderer/ComponentRegistry.ts +23 -0
  170. package/renderer/UnresolvedComponent.tsx +20 -0
  171. package/renderer/createReactRenderer.tsx +35 -0
  172. package/renderer/index.ts +6 -0
  173. package/theme/SceneThemeProvider.tsx +58 -0
  174. package/theme/index.ts +5 -0
  175. package/theme/themeTokens.ts +59 -0
@@ -0,0 +1,53 @@
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 { expect } from 'chai';
5
+ import { render, screen } from '@testing-library/react';
6
+ import { Control, ExternalComponent, HorizontalAlignment, VerticalAlignment, Visibility } from '@cratis/scene.model';
7
+ import { SceneElementView } from '../SceneElementView';
8
+ import { coreComponents } from '../core';
9
+
10
+ function control(id: string): Control {
11
+ return {
12
+ id,
13
+ name: id,
14
+ properties: {},
15
+ visibility: Visibility.Visible,
16
+ isEnabled: true,
17
+ opacity: 1,
18
+ size: {},
19
+ zIndex: 0,
20
+ minimumSize: {},
21
+ maximumSize: {},
22
+ margin: { left: 0, top: 0, right: 0, bottom: 0 },
23
+ horizontalAlignment: HorizontalAlignment.Stretch,
24
+ verticalAlignment: VerticalAlignment.Stretch,
25
+ borderThickness: { left: 0, top: 0, right: 0, bottom: 0 },
26
+ padding: { left: 0, top: 0, right: 0, bottom: 0 },
27
+ tabIndex: 0,
28
+ };
29
+ }
30
+
31
+ describe('when rendering a tree', () => {
32
+ describe('and every component resolves', () => {
33
+ const text: ExternalComponent = { ...control('greeting'), componentName: 'core:text', properties: { text: 'Hello Scene' }, slots: {} };
34
+ const card: ExternalComponent = { ...control('card'), componentName: 'core:card', slots: { content: [text] } };
35
+
36
+ beforeEach(() => {
37
+ render(<SceneElementView element={card} registry={coreComponents} resolveBinding={() => undefined} />);
38
+ });
39
+
40
+ it('should render the nested core:text content', () => expect(screen.getByText('Hello Scene')).to.exist);
41
+ });
42
+
43
+ describe('and a component does not resolve', () => {
44
+ const missing: ExternalComponent = { ...control('missing'), componentName: 'vendor:fancy-widget', slots: {} };
45
+
46
+ beforeEach(() => {
47
+ render(<SceneElementView element={missing} registry={coreComponents} resolveBinding={() => undefined} />);
48
+ });
49
+
50
+ it('should render a visible unresolved-component placeholder', () =>
51
+ expect(screen.getByText('Unresolved component: vendor:fancy-widget')).to.exist);
52
+ });
53
+ });
@@ -0,0 +1,74 @@
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 { JSDOM } from 'jsdom';
5
+ import { Theme } from '@cratis/scene.model';
6
+ import { applyThemeTokens } from '../index';
7
+
8
+ const light: Theme = {
9
+ name: 'Daylight',
10
+ compatibleWith: ['core'],
11
+ tokens: { 'primary.color': '#2563eb', 'surface.background': '#ffffff' },
12
+ };
13
+
14
+ const dark: Theme = {
15
+ name: 'Midnight',
16
+ compatibleWith: ['core'],
17
+ isDark: true,
18
+ tokens: { 'primary.color': '#60a5fa' },
19
+ };
20
+
21
+ describe('when switching themes', () => {
22
+ const dom = new JSDOM('<div id="root"></div>');
23
+ const element = dom.window.document.getElementById('root') as unknown as HTMLElement;
24
+
25
+ applyThemeTokens(element, light);
26
+ const afterLight = {
27
+ primary: element.style.getPropertyValue('--scene-primary-color'),
28
+ surface: element.style.getPropertyValue('--scene-surface-background'),
29
+ theme: element.getAttribute('data-scene-theme'),
30
+ scheme: element.getAttribute('data-scene-color-scheme'),
31
+ };
32
+
33
+ applyThemeTokens(element, dark, light);
34
+ const afterDark = {
35
+ primary: element.style.getPropertyValue('--scene-primary-color'),
36
+ surface: element.style.getPropertyValue('--scene-surface-background'),
37
+ theme: element.getAttribute('data-scene-theme'),
38
+ scheme: element.getAttribute('data-scene-color-scheme'),
39
+ };
40
+
41
+ applyThemeTokens(element, undefined, dark);
42
+ const afterClearing = {
43
+ primary: element.style.getPropertyValue('--scene-primary-color'),
44
+ theme: element.getAttribute('data-scene-theme'),
45
+ };
46
+
47
+ it('should write the first theme tokens as custom properties', () => {
48
+ afterLight.primary.should.equal('#2563eb');
49
+ afterLight.surface.should.equal('#ffffff');
50
+ });
51
+
52
+ it('should mark the element with the first theme name and scheme', () => {
53
+ afterLight.theme!.should.equal('Daylight');
54
+ afterLight.scheme!.should.equal('light');
55
+ });
56
+
57
+ it('should overwrite a token the next theme also defines', () => {
58
+ afterDark.primary.should.equal('#60a5fa');
59
+ });
60
+
61
+ it('should remove a token the next theme does not define, rather than leaving it stale', () => {
62
+ afterDark.surface.should.equal('');
63
+ });
64
+
65
+ it('should mark the element as dark for a dark theme', () => {
66
+ afterDark.theme!.should.equal('Midnight');
67
+ afterDark.scheme!.should.equal('dark');
68
+ });
69
+
70
+ it('should clear every token and marker when the theme is removed', () => {
71
+ afterClearing.primary.should.equal('');
72
+ (afterClearing.theme === null).should.be.true;
73
+ });
74
+ });
@@ -0,0 +1,22 @@
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 { themeTokenProperty } from '../index';
5
+
6
+ describe('when converting a token name', () => {
7
+ it('should prefix a single-segment name', () => {
8
+ themeTokenProperty('primary').should.equal('--scene-primary');
9
+ });
10
+
11
+ it('should join dotted segments with dashes', () => {
12
+ themeTokenProperty('primary.color').should.equal('--scene-primary-color');
13
+ });
14
+
15
+ it('should split camelCase within a segment', () => {
16
+ themeTokenProperty('surface.borderColor').should.equal('--scene-surface-border-color');
17
+ });
18
+
19
+ it('should handle a segment with a digit boundary', () => {
20
+ themeTokenProperty('surface.level2Background').should.equal('--scene-surface-level2-background');
21
+ });
22
+ });
@@ -0,0 +1,72 @@
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 { PackageKind, ScenePackage } from '@cratis/scene.model';
5
+ import { ScenePackageBundle, componentRegistryKey, validatePackageBundle } from '../index';
6
+ import { CoreText } from '../core';
7
+
8
+ function manifestFor(components: string[], layouts: string[] = [], themes: string[] = [], screenTemplates: string[] = []): ScenePackage {
9
+ return {
10
+ name: 'Test',
11
+ version: '1.0.0',
12
+ kind: PackageKind.ComponentLibrary,
13
+ dependencies: [],
14
+ components,
15
+ layouts,
16
+ screenTemplates,
17
+ dialogTemplates: [],
18
+ themes,
19
+ };
20
+ }
21
+
22
+ describe('when the bundle and manifest disagree', () => {
23
+ const declaredButNotRegistered = validatePackageBundle({
24
+ manifest: manifestFor(['text', 'missing']),
25
+ components: { [componentRegistryKey('Test', 'text')]: CoreText },
26
+ } as ScenePackageBundle);
27
+
28
+ const registeredButNotDeclared = validatePackageBundle({
29
+ manifest: manifestFor([]),
30
+ components: { [componentRegistryKey('Test', 'text')]: CoreText },
31
+ } as ScenePackageBundle);
32
+
33
+ const layoutDeclaredButNotProvided = validatePackageBundle({
34
+ manifest: manifestFor([], ['AppShell']),
35
+ components: {},
36
+ } as ScenePackageBundle);
37
+
38
+ const screenTemplateDeclaredButNotProvided = validatePackageBundle({
39
+ manifest: manifestFor([], [], [], ['ModuleWorkspace']),
40
+ components: {},
41
+ } as ScenePackageBundle);
42
+
43
+ const themeDeclaredButNotProvided = validatePackageBundle({
44
+ manifest: manifestFor([], [], ['Midnight']),
45
+ components: {},
46
+ } as ScenePackageBundle);
47
+
48
+ it('should report a component the manifest declares without an implementation', () => {
49
+ declaredButNotRegistered.should.have.lengthOf(1);
50
+ declaredButNotRegistered[0].should.contain("'missing'");
51
+ });
52
+
53
+ it('should report a component registered without being declared', () => {
54
+ registeredButNotDeclared.should.have.lengthOf(1);
55
+ registeredButNotDeclared[0].should.contain('Test:text');
56
+ });
57
+
58
+ it('should report a layout the manifest declares without a definition', () => {
59
+ layoutDeclaredButNotProvided.should.have.lengthOf(1);
60
+ layoutDeclaredButNotProvided[0].should.contain("'AppShell'");
61
+ });
62
+
63
+ it('should report a screen template the manifest declares without a definition', () => {
64
+ screenTemplateDeclaredButNotProvided.should.have.lengthOf(1);
65
+ screenTemplateDeclaredButNotProvided[0].should.contain("'ModuleWorkspace'");
66
+ });
67
+
68
+ it('should report a theme the manifest declares without a definition', () => {
69
+ themeDeclaredButNotProvided.should.have.lengthOf(1);
70
+ themeDeclaredButNotProvided[0].should.contain("'Midnight'");
71
+ });
72
+ });
@@ -0,0 +1,12 @@
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 { corePackage, validatePackageBundle } from '../index';
5
+
6
+ describe('when the bundle matches its manifest', () => {
7
+ const problems = validatePackageBundle(corePackage);
8
+
9
+ it('should report no problems', () => {
10
+ problems.should.be.empty;
11
+ });
12
+ });
package/index.ts ADDED
@@ -0,0 +1,9 @@
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 './SceneElementView';
5
+ export * from './renderer';
6
+ export * from './core';
7
+ export * from './NavBar';
8
+ export * from './packages';
9
+ export * from './theme';
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@cratis/scene.react",
3
+ "version": "0.1.0",
4
+ "description": "The React renderer implementing Scene.Engine's Renderer contract against real React/DOM.",
5
+ "author": "Cratis",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/Cratis/Scene.git"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "**/*.ts",
17
+ "**/*.tsx"
18
+ ],
19
+ "main": "dist/cjs/index.js",
20
+ "module": "dist/esm/index.js",
21
+ "types": "dist/esm/index.d.ts",
22
+ "exports": {
23
+ "./package.json": "./package.json",
24
+ ".": {
25
+ "types": "./dist/esm/index.d.ts",
26
+ "require": "./dist/cjs/index.js",
27
+ "import": "./dist/esm/index.js"
28
+ }
29
+ },
30
+ "scripts": {
31
+ "prepare": "yarn g:build",
32
+ "clean": "yarn g:clean",
33
+ "build": "yarn g:build",
34
+ "lint": "yarn g:lint",
35
+ "lint:ci": "yarn g:lint:ci",
36
+ "test": "yarn g:test",
37
+ "ci": "yarn g:ci",
38
+ "up": "yarn g:up",
39
+ "dev": "storybook dev -p 6006",
40
+ "build-storybook": "storybook build"
41
+ },
42
+ "dependencies": {
43
+ "@cratis/scene.engine": "0.1.0",
44
+ "@cratis/scene.model": "0.1.0"
45
+ },
46
+ "devDependencies": {
47
+ "@cratis/scene.engine": "0.1.0",
48
+ "@cratis/scene.model": "0.1.0",
49
+ "@storybook/addon-links": "^10.4.1",
50
+ "@storybook/react": "^10.4.1",
51
+ "@storybook/react-vite": "^10.4.1",
52
+ "react": "^19.2.6",
53
+ "react-dom": "^19.2.6",
54
+ "storybook": "^10.4.1"
55
+ },
56
+ "peerDependencies": {
57
+ "react": "^18.0.0 || ^19.0.0",
58
+ "react-dom": "^18.0.0 || ^19.0.0"
59
+ }
60
+ }
@@ -0,0 +1,137 @@
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 { DialogTemplate, Layout, ScenePackage, Screen, ScreenTemplate, Theme } from '@cratis/scene.model';
5
+ import { ComponentRegistry } from '../renderer';
6
+
7
+ /**
8
+ * What a package actually ships to a React renderer, as opposed to what it *declares*.
9
+ *
10
+ * `ScenePackage` is the declaration — a name, a kind, dependencies, and the names of what it
11
+ * contributes. It is deliberately platform-agnostic and lives in `Scene.Model`, because Stage's build and
12
+ * Studio's design-time queries both read it without ever loading a component. A bundle is the other half:
13
+ * the real React components, layouts, screens and themes behind those names, which only a renderer needs.
14
+ *
15
+ * A package publishes exactly one bundle, as a named export following its own name —
16
+ * `primeReactPackage`, `cratisComponentsPackage`, `corePackage`. A host loads it from the module the
17
+ * declaration's `ScenePackage.module` names.
18
+ */
19
+ export interface ScenePackageBundle {
20
+ /**
21
+ * The package's own declaration. Its `components`, `layouts` and `themes` name lists must agree with
22
+ * what this bundle actually provides — {@link validatePackageBundle} is what proves they do.
23
+ */
24
+ manifest: ScenePackage;
25
+
26
+ /**
27
+ * The components this package provides, keyed by {@link componentRegistryKey}.
28
+ */
29
+ components: ComponentRegistry;
30
+
31
+ /**
32
+ * The application shells this package provides, if any. Only a blueprint has these.
33
+ */
34
+ layouts?: Layout[];
35
+
36
+ /**
37
+ * The screen templates this package provides — the shapes that go inside a layout, each declaring
38
+ * which of its parent's slots it fills.
39
+ */
40
+ screenTemplates?: ScreenTemplate[];
41
+
42
+ /**
43
+ * The dialog templates this package provides.
44
+ */
45
+ dialogTemplates?: DialogTemplate[];
46
+
47
+ /**
48
+ * Sample screens this package ships — a layout package's gallery, so a preview has real content to
49
+ * put through the real engine rather than a mockup.
50
+ */
51
+ screens?: Screen[];
52
+
53
+ /**
54
+ * The themes this package ships, if any.
55
+ */
56
+ themes?: Theme[];
57
+ }
58
+
59
+ /**
60
+ * The key a component is registered and looked up under: the package that provides it, then the bare name
61
+ * a screen refers to it by.
62
+ *
63
+ * A screen writes a bare name (`button`) or a package-qualified one (`PrimeReact.button`);
64
+ * `resolveComponentName` turns either into a `{ name, package }` pair, and this turns that pair into the
65
+ * registry key. Nothing should build this string by hand — the separator is an implementation detail of
66
+ * the registry, deliberately not the `.` used for qualification in a screen, so a package name containing
67
+ * dots stays unambiguous.
68
+ */
69
+ export function componentRegistryKey(packageName: string, componentName: string): string {
70
+ return `${packageName}:${componentName}`;
71
+ }
72
+
73
+ /**
74
+ * Checks that a bundle provides everything its manifest declares, and declares everything it provides.
75
+ *
76
+ * A manifest that promises a component the bundle does not register renders as `UnresolvedComponent` at
77
+ * runtime — a blank box, far from where the mistake was made. A component registered but not declared is
78
+ * invisible to `resolveComponentName`, so a screen can never name it. Both are silent failures, which is
79
+ * why every package's specs run this.
80
+ */
81
+ export function validatePackageBundle(bundle: ScenePackageBundle): string[] {
82
+ const problems: string[] = [];
83
+ const { manifest } = bundle;
84
+
85
+ for (const name of manifest.components) {
86
+ if (!(componentRegistryKey(manifest.name, name) in bundle.components)) {
87
+ problems.push(`declares the component '${name}' but registers no implementation for it`);
88
+ }
89
+ }
90
+
91
+ const declared = new Set(manifest.components.map((name) => componentRegistryKey(manifest.name, name)));
92
+ for (const key of Object.keys(bundle.components)) {
93
+ if (!declared.has(key)) {
94
+ problems.push(`registers '${key}', which the manifest does not declare`);
95
+ }
96
+ }
97
+
98
+ const providedLayouts = new Set((bundle.layouts ?? []).map((layout) => layout.name));
99
+ for (const name of manifest.layouts) {
100
+ if (!providedLayouts.has(name)) {
101
+ problems.push(`declares the layout '${name}' but provides no definition for it`);
102
+ }
103
+ }
104
+
105
+ const providedScreenTemplates = new Set((bundle.screenTemplates ?? []).map((template) => template.name));
106
+ for (const name of manifest.screenTemplates) {
107
+ if (!providedScreenTemplates.has(name)) {
108
+ problems.push(`declares the screen template '${name}' but provides no definition for it`);
109
+ }
110
+ }
111
+
112
+ const providedDialogTemplates = new Set((bundle.dialogTemplates ?? []).map((template) => template.name));
113
+ for (const name of manifest.dialogTemplates) {
114
+ if (!providedDialogTemplates.has(name)) {
115
+ problems.push(`declares the dialog template '${name}' but provides no definition for it`);
116
+ }
117
+ }
118
+
119
+ const providedThemes = new Set((bundle.themes ?? []).map((theme) => theme.name));
120
+ for (const name of manifest.themes) {
121
+ if (!providedThemes.has(name)) {
122
+ problems.push(`declares the theme '${name}' but provides no definition for it`);
123
+ }
124
+ }
125
+
126
+ return problems;
127
+ }
128
+
129
+ /**
130
+ * Merges the component registries of several bundles into the single registry a renderer consumes.
131
+ *
132
+ * Keys carry their package name, so nothing collides and merge order does not matter — which package wins
133
+ * a *bare* name is decided earlier, by `resolveComponentName` walking the profile's priority order.
134
+ */
135
+ export function mergePackageRegistries(bundles: ScenePackageBundle[]): ComponentRegistry {
136
+ return Object.assign({}, ...bundles.map((bundle) => bundle.components)) as ComponentRegistry;
137
+ }
@@ -0,0 +1,4 @@
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 './ScenePackageBundle';
@@ -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 { ComponentType, ReactNode } from 'react';
5
+ import { ExternalComponent } from '@cratis/scene.model';
6
+
7
+ /**
8
+ * The props every registered component receives.
9
+ */
10
+ export interface RegisteredComponentProps {
11
+ element: ExternalComponent;
12
+ slots: Record<string, ReactNode[]>;
13
+ }
14
+
15
+ /**
16
+ * Maps a resolved component name (as `ExternalComponent.componentName` already carries it, post
17
+ * `ui profile` package resolution) to the React component that renders it. Bare-name resolution against a
18
+ * profile's package list is `resolveComponentName` in `@cratis/scene.engine` (Cratis/Scene#3) - this
19
+ * registry is keyed by the already-resolved name; wiring the two together (resolving an entire element
20
+ * tree's component names before it reaches this registry) is Stage#39/StudioIssues#160's job, not this
21
+ * registry's.
22
+ */
23
+ export type ComponentRegistry = Record<string, ComponentType<RegisteredComponentProps>>;
@@ -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
+ import { RegisteredComponentProps } from './ComponentRegistry';
5
+
6
+ /**
7
+ * The fallback rendered for an {@link ExternalComponent} whose `componentName` is not in the active
8
+ * {@link ComponentRegistry} - a visibly broken placeholder, never a silent blank, since a missing
9
+ * component is a real configuration gap the renderer should surface, not hide.
10
+ */
11
+ export function UnresolvedComponent({ element }: RegisteredComponentProps) {
12
+ return (
13
+ <div
14
+ data-scene-unresolved-component={element.componentName}
15
+ style={{ border: '1px dashed red', color: 'red', padding: '0.25rem 0.5rem', fontFamily: 'monospace' }}
16
+ >
17
+ Unresolved component: {element.componentName}
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,35 @@
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 { ReactNode, createElement } from 'react';
5
+ import { Renderer } from '@cratis/scene.engine';
6
+ import { ContentControl, ExternalComponent, ItemsControl, Panel } from '@cratis/scene.model';
7
+ import { ComponentRegistry } from './ComponentRegistry';
8
+ import { UnresolvedComponent } from './UnresolvedComponent';
9
+
10
+ /**
11
+ * Creates a {@link Renderer} that turns a Scene element tree into React elements. `Panel`, `ItemsControl`
12
+ * and `ContentControl` render as plain wrapping `div`s here - arranging them according to a layout's
13
+ * `flow`/`freeform` arrangement is Scene#4's job, layered on top of this renderer rather than folded
14
+ * into it.
15
+ */
16
+ export function createReactRenderer(registry: ComponentRegistry): Renderer<ReactNode> {
17
+ return {
18
+ renderContentControl(element: ContentControl, content: ReactNode): ReactNode {
19
+ return createElement('div', { key: element.id, 'data-scene-id': element.id, 'data-scene-kind': 'ContentControl' }, content);
20
+ },
21
+
22
+ renderItemsControl(element: ItemsControl, items: ReactNode[]): ReactNode {
23
+ return createElement('div', { key: element.id, 'data-scene-id': element.id, 'data-scene-kind': 'ItemsControl' }, items);
24
+ },
25
+
26
+ renderExternalComponent(element: ExternalComponent, slots: Record<string, ReactNode[]>): ReactNode {
27
+ const Component = registry[element.componentName] ?? UnresolvedComponent;
28
+ return createElement(Component, { key: element.id, element, slots });
29
+ },
30
+
31
+ renderPanel(element: Panel, children: ReactNode[]): ReactNode {
32
+ return createElement('div', { key: element.id, 'data-scene-id': element.id, 'data-scene-kind': 'Panel' }, children);
33
+ },
34
+ };
35
+ }
@@ -0,0 +1,6 @@
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 './ComponentRegistry';
5
+ export * from './UnresolvedComponent';
6
+ export * from './createReactRenderer';
@@ -0,0 +1,58 @@
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 { ReactNode, createContext, useContext, useLayoutEffect, useRef } from 'react';
5
+ import { Theme } from '@cratis/scene.model';
6
+ import { applyThemeTokens } from './themeTokens';
7
+
8
+ const SceneThemeContext = createContext<Theme | undefined>(undefined);
9
+
10
+ /**
11
+ * The theme currently applied by the nearest {@link SceneThemeProvider}, for a package component that has
12
+ * to make a decision a token cannot express - picking a light or dark logo asset, say.
13
+ */
14
+ export function useSceneTheme(): Theme | undefined {
15
+ return useContext(SceneThemeContext);
16
+ }
17
+
18
+ export interface SceneThemePropsBase {
19
+ /**
20
+ * The theme to apply. Changing it re-resolves in place - no reload, no remount of the subtree.
21
+ */
22
+ theme?: Theme;
23
+
24
+ /**
25
+ * The content the theme applies to.
26
+ */
27
+ children?: ReactNode;
28
+
29
+ /**
30
+ * A class name for the element the tokens land on.
31
+ */
32
+ className?: string;
33
+ }
34
+
35
+ /**
36
+ * Applies a {@link Theme}'s tokens to a wrapping element and makes the theme available to everything
37
+ * inside it.
38
+ *
39
+ * Theme switching is live re-resolution: the tokens are written onto the same element rather than
40
+ * swapping a stylesheet, so a preview changes theme without a flash and without losing any state below
41
+ * it. That is what lets Studio's gallery switch themes as fast as you can click.
42
+ */
43
+ export function SceneThemeProvider({ theme, children, className }: SceneThemePropsBase) {
44
+ const elementRef = useRef<HTMLDivElement>(null);
45
+ const previousRef = useRef<Theme | undefined>(undefined);
46
+
47
+ useLayoutEffect(() => {
48
+ if (!elementRef.current) return;
49
+ applyThemeTokens(elementRef.current, theme, previousRef.current);
50
+ previousRef.current = theme;
51
+ }, [theme]);
52
+
53
+ return (
54
+ <div ref={elementRef} className={className} data-scene-theme-root=''>
55
+ <SceneThemeContext.Provider value={theme}>{children}</SceneThemeContext.Provider>
56
+ </div>
57
+ );
58
+ }
package/theme/index.ts ADDED
@@ -0,0 +1,5 @@
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 './themeTokens';
5
+ export * from './SceneThemeProvider';
@@ -0,0 +1,59 @@
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 { Theme } from '@cratis/scene.model';
5
+
6
+ /**
7
+ * The prefix every Scene design token gets as a CSS custom property. A package adapts these to whatever
8
+ * its own component library expects — the PrimeReact package maps them onto PrimeReact's variables, the
9
+ * same indirection `@cratis/components` uses for its `--cratis-*` layer — so layout and component CSS
10
+ * never hardcodes one library's variable names.
11
+ */
12
+ export const themeTokenPrefix = '--scene';
13
+
14
+ /**
15
+ * Turns a semantic token name into its CSS custom property name: `primary.color` becomes
16
+ * `--scene-primary-color`, `surface.borderColor` becomes `--scene-surface-border-color`.
17
+ *
18
+ * Token names are semantic and platform-neutral in `Scene.Model` on purpose; this is the one place that
19
+ * decides what they look like in CSS, so a non-DOM renderer can make an entirely different choice.
20
+ */
21
+ export function themeTokenProperty(token: string): string {
22
+ const kebab = token
23
+ .split('.')
24
+ .map((part) => part.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase())
25
+ .join('-');
26
+ return `${themeTokenPrefix}-${kebab}`;
27
+ }
28
+
29
+ /**
30
+ * Applies a theme to an element, in place.
31
+ *
32
+ * Every token becomes a custom property on the element, and the element is marked with the theme's name
33
+ * and colour scheme so CSS can key off either. Tokens the previous theme set and this one does not are
34
+ * removed, so switching themes cannot leave a stale value behind.
35
+ *
36
+ * This is deliberately a mutation of an existing element rather than a re-render: switching theme has to
37
+ * be live re-resolution against the running profile, with no reload and no remount, or a gallery preview
38
+ * flickers every time someone tries a different theme.
39
+ */
40
+ export function applyThemeTokens(element: HTMLElement, theme: Theme | undefined, previous?: Theme): void {
41
+ for (const token of Object.keys(previous?.tokens ?? {})) {
42
+ if (!theme?.tokens || !(token in theme.tokens)) {
43
+ element.style.removeProperty(themeTokenProperty(token));
44
+ }
45
+ }
46
+
47
+ if (!theme) {
48
+ element.removeAttribute('data-scene-theme');
49
+ element.removeAttribute('data-scene-color-scheme');
50
+ return;
51
+ }
52
+
53
+ for (const [token, value] of Object.entries(theme.tokens ?? {})) {
54
+ element.style.setProperty(themeTokenProperty(token), value);
55
+ }
56
+
57
+ element.setAttribute('data-scene-theme', theme.name);
58
+ element.setAttribute('data-scene-color-scheme', theme.isDark ? 'dark' : 'light');
59
+ }