@cratis/scene.react 4.2.1 → 4.4.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.
- package/SceneElementView.tsx +13 -4
- package/core/CoreNavigationBar.tsx +59 -0
- package/core/coreComponents.ts +2 -0
- package/core/coreDescriptors.ts +75 -0
- package/core/corePackage.ts +3 -0
- package/core/index.ts +2 -0
- package/dist/cjs/SceneElementView.js +6 -2
- package/dist/cjs/SceneElementView.js.map +1 -1
- package/dist/cjs/core/CoreNavigationBar.js +59 -0
- package/dist/cjs/core/CoreNavigationBar.js.map +1 -0
- package/dist/cjs/core/coreComponents.js +3 -1
- package/dist/cjs/core/coreComponents.js.map +1 -1
- package/dist/cjs/core/coreDescriptors.js +162 -0
- package/dist/cjs/core/coreDescriptors.js.map +1 -0
- package/dist/cjs/core/corePackage.js +5 -2
- package/dist/cjs/core/corePackage.js.map +1 -1
- package/dist/cjs/icons/IconAdapterContext.js +21 -0
- package/dist/cjs/icons/IconAdapterContext.js.map +1 -0
- package/dist/cjs/icons/IconAdapterRegistry.js +45 -0
- package/dist/cjs/icons/IconAdapterRegistry.js.map +1 -0
- package/dist/cjs/icons/SceneIcon.js +42 -0
- package/dist/cjs/icons/SceneIcon.js.map +1 -0
- package/dist/cjs/icons/useIconGlyph.js +86 -0
- package/dist/cjs/icons/useIconGlyph.js.map +1 -0
- package/dist/cjs/index.js +15 -0
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/packages/ScenePackageBundle.js +74 -1
- package/dist/cjs/packages/ScenePackageBundle.js.map +1 -1
- package/dist/esm/SceneElementView.d.ts +3 -2
- package/dist/esm/SceneElementView.d.ts.map +1 -1
- package/dist/esm/SceneElementView.js +7 -3
- package/dist/esm/SceneElementView.js.map +1 -1
- package/dist/esm/core/CoreNavigationBar.d.ts +3 -0
- package/dist/esm/core/CoreNavigationBar.d.ts.map +1 -0
- package/dist/esm/core/CoreNavigationBar.js +57 -0
- package/dist/esm/core/CoreNavigationBar.js.map +1 -0
- package/dist/esm/core/coreComponents.d.ts.map +1 -1
- package/dist/esm/core/coreComponents.js +3 -1
- package/dist/esm/core/coreComponents.js.map +1 -1
- package/dist/esm/core/coreDescriptors.d.ts +3 -0
- package/dist/esm/core/coreDescriptors.d.ts.map +1 -0
- package/dist/esm/core/coreDescriptors.js +160 -0
- package/dist/esm/core/coreDescriptors.js.map +1 -0
- package/dist/esm/core/corePackage.d.ts.map +1 -1
- package/dist/esm/core/corePackage.js +5 -2
- package/dist/esm/core/corePackage.js.map +1 -1
- package/dist/esm/core/index.d.ts +2 -0
- package/dist/esm/core/index.d.ts.map +1 -1
- package/dist/esm/core/index.js +2 -0
- package/dist/esm/core/index.js.map +1 -1
- package/dist/esm/icons/IconAdapter.d.ts +13 -0
- package/dist/esm/icons/IconAdapter.d.ts.map +1 -0
- package/dist/esm/icons/IconAdapter.js +2 -0
- package/dist/esm/icons/IconAdapter.js.map +1 -0
- package/dist/esm/icons/IconAdapterContext.d.ts +8 -0
- package/dist/esm/icons/IconAdapterContext.d.ts.map +1 -0
- package/dist/esm/icons/IconAdapterContext.js +18 -0
- package/dist/esm/icons/IconAdapterContext.js.map +1 -0
- package/dist/esm/icons/IconAdapterRegistry.d.ts +9 -0
- package/dist/esm/icons/IconAdapterRegistry.d.ts.map +1 -0
- package/dist/esm/icons/IconAdapterRegistry.js +42 -0
- package/dist/esm/icons/IconAdapterRegistry.js.map +1 -0
- package/dist/esm/icons/IconLibraryBundle.d.ts +7 -0
- package/dist/esm/icons/IconLibraryBundle.d.ts.map +1 -0
- package/dist/esm/icons/IconLibraryBundle.js +2 -0
- package/dist/esm/icons/IconLibraryBundle.js.map +1 -0
- package/dist/esm/icons/SceneIcon.d.ts +14 -0
- package/dist/esm/icons/SceneIcon.d.ts.map +1 -0
- package/dist/esm/icons/SceneIcon.js +40 -0
- package/dist/esm/icons/SceneIcon.js.map +1 -0
- package/dist/esm/icons/index.d.ts +7 -0
- package/dist/esm/icons/index.d.ts.map +1 -0
- package/dist/esm/icons/index.js +7 -0
- package/dist/esm/icons/index.js.map +1 -0
- package/dist/esm/icons/useIconGlyph.d.ts +14 -0
- package/dist/esm/icons/useIconGlyph.d.ts.map +1 -0
- package/dist/esm/icons/useIconGlyph.js +84 -0
- package/dist/esm/icons/useIconGlyph.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +7 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/packages/ScenePackageBundle.d.ts +6 -1
- package/dist/esm/packages/ScenePackageBundle.d.ts.map +1 -1
- package/dist/esm/packages/ScenePackageBundle.js +74 -2
- package/dist/esm/packages/ScenePackageBundle.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/icons/IconAdapter.ts +52 -0
- package/icons/IconAdapterContext.tsx +21 -0
- package/icons/IconAdapterRegistry.ts +50 -0
- package/icons/IconLibraryBundle.ts +25 -0
- package/icons/SceneIcon.tsx +73 -0
- package/icons/index.ts +9 -0
- package/icons/useIconGlyph.ts +79 -0
- package/index.ts +1 -0
- package/package.json +5 -5
- package/packages/ScenePackageBundle.ts +106 -2
|
@@ -0,0 +1,52 @@
|
|
|
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 } from 'react';
|
|
5
|
+
import { IconReference } from '@cratis/scene.model';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What an icon library's glyph is handed to draw itself. The glyph owns only its artwork: `SceneIcon`
|
|
9
|
+
* has already sized and colored the box it sits in and made it accessible, so a glyph just fills it.
|
|
10
|
+
*/
|
|
11
|
+
export interface IconGlyphProps {
|
|
12
|
+
/**
|
|
13
|
+
* The width and height to draw at - a number of pixels or any CSS length.
|
|
14
|
+
*/
|
|
15
|
+
size: number | string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The color to draw in; `currentColor` unless the caller chose one.
|
|
19
|
+
*/
|
|
20
|
+
color: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* An optional class for the artwork element.
|
|
24
|
+
*/
|
|
25
|
+
className?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* One icon's artwork, as a component.
|
|
30
|
+
*/
|
|
31
|
+
export type IconGlyph = ComponentType<IconGlyphProps>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* How one icon library's references become pixels in React. The model persists only an
|
|
35
|
+
* {@link IconReference}; an adapter is the library's own answer to "what does this one look like here",
|
|
36
|
+
* so no markup, class name or component is ever persisted - and a library can be re-rendered, or replaced
|
|
37
|
+
* by one with an adapter for another renderer, without touching a single stored value.
|
|
38
|
+
*
|
|
39
|
+
* Artwork is loaded per icon and on demand, so a library with thousands of icons costs a screen only the
|
|
40
|
+
* ones it shows.
|
|
41
|
+
*/
|
|
42
|
+
export interface IconAdapter {
|
|
43
|
+
/**
|
|
44
|
+
* The library this adapter renders - an {@link IconReference.library}.
|
|
45
|
+
*/
|
|
46
|
+
library: string;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Loads the artwork for one reference. Resolves to `undefined` when the library has no such icon.
|
|
50
|
+
*/
|
|
51
|
+
loadGlyph(reference: IconReference): Promise<IconGlyph | undefined> | IconGlyph | undefined;
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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 } from 'react';
|
|
5
|
+
import { IconAdapterRegistry } from './IconAdapterRegistry';
|
|
6
|
+
|
|
7
|
+
const IconAdapterContext = createContext<IconAdapterRegistry | undefined>(undefined);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Supplies the icon adapters every `SceneIcon` below it renders through.
|
|
11
|
+
*/
|
|
12
|
+
export const IconAdapterProvider = ({ registry, children }: { registry: IconAdapterRegistry; children?: ReactNode }) => (
|
|
13
|
+
<IconAdapterContext.Provider value={registry}>{children}</IconAdapterContext.Provider>
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The icon adapters in scope, or `undefined` outside an {@link IconAdapterProvider}.
|
|
18
|
+
*/
|
|
19
|
+
export function useIconAdapters(): IconAdapterRegistry | undefined {
|
|
20
|
+
return useContext(IconAdapterContext);
|
|
21
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
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 { IconAdapter } from './IconAdapter';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The icon adapters a renderer has, one per library. Registering is explicit and never replaces: two
|
|
8
|
+
* adapters for one library would mean an icon renders differently depending on registration order, which
|
|
9
|
+
* is the silent swapping the library-qualified reference exists to prevent.
|
|
10
|
+
*/
|
|
11
|
+
export class IconAdapterRegistry {
|
|
12
|
+
readonly #adapters = new Map<string, IconAdapter>();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Adds an adapter.
|
|
16
|
+
*
|
|
17
|
+
* @throws Error when an adapter for the same library is already registered.
|
|
18
|
+
*/
|
|
19
|
+
register(adapter: IconAdapter): this {
|
|
20
|
+
if (this.#adapters.has(adapter.library)) {
|
|
21
|
+
throw new Error(`An icon adapter for the library '${adapter.library}' is already registered`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.#adapters.set(adapter.library, adapter);
|
|
25
|
+
return this;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The adapter for a library, or `undefined` when this renderer has none.
|
|
30
|
+
*/
|
|
31
|
+
get(library: string): IconAdapter | undefined {
|
|
32
|
+
return this.#adapters.get(library);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The libraries this registry can render.
|
|
37
|
+
*/
|
|
38
|
+
get libraries(): string[] {
|
|
39
|
+
return [...this.#adapters.keys()];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Creates a registry holding the given adapters.
|
|
45
|
+
*/
|
|
46
|
+
export function createIconAdapterRegistry(adapters: IconAdapter[] = []): IconAdapterRegistry {
|
|
47
|
+
const registry = new IconAdapterRegistry();
|
|
48
|
+
adapters.forEach((adapter) => registry.register(adapter));
|
|
49
|
+
return registry;
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
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 { IconCatalogSource } from '@cratis/scene.engine';
|
|
5
|
+
import { IconAdapter } from './IconAdapter';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What an icon-library package ships to a React renderer, as opposed to what its manifest declares.
|
|
9
|
+
*
|
|
10
|
+
* The manifest says the library exists, what it is licensed under and which renderers it supports; this
|
|
11
|
+
* is the code behind it. The catalog is the metadata a picker searches, loaded on demand; the adapter
|
|
12
|
+
* turns one reference at a time into artwork. They are separate because a catalog is renderer-neutral
|
|
13
|
+
* and an adapter is not.
|
|
14
|
+
*/
|
|
15
|
+
export interface IconLibraryBundle {
|
|
16
|
+
/**
|
|
17
|
+
* Where the library's icon catalog is loaded from.
|
|
18
|
+
*/
|
|
19
|
+
catalog: IconCatalogSource;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* How the library's references are drawn in React.
|
|
23
|
+
*/
|
|
24
|
+
adapter: IconAdapter;
|
|
25
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
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 } from 'react';
|
|
5
|
+
import { IconReference } from '@cratis/scene.model';
|
|
6
|
+
import { IconAdapterRegistry } from './IconAdapterRegistry';
|
|
7
|
+
import { useIconAdapters } from './IconAdapterContext';
|
|
8
|
+
import { useIconGlyph } from './useIconGlyph';
|
|
9
|
+
|
|
10
|
+
export interface SceneIconProps {
|
|
11
|
+
/**
|
|
12
|
+
* The icon to draw.
|
|
13
|
+
*/
|
|
14
|
+
reference: IconReference;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The adapters to render through. Defaults to the ones an `IconAdapterProvider` supplies.
|
|
18
|
+
*/
|
|
19
|
+
adapters?: IconAdapterRegistry;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The width and height of the icon: a number of pixels or any CSS length. Defaults to `1em`, so an
|
|
23
|
+
* icon scales with the text it sits in.
|
|
24
|
+
*/
|
|
25
|
+
size?: number | string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The color to draw in. Defaults to `currentColor`.
|
|
29
|
+
*/
|
|
30
|
+
color?: string;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The accessible name. With a label the icon is announced as an image; without one it is decorative
|
|
34
|
+
* and hidden from assistive technology - an icon next to its own text label should not be read twice.
|
|
35
|
+
*/
|
|
36
|
+
label?: string;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* An optional class for the box the icon sits in.
|
|
40
|
+
*/
|
|
41
|
+
className?: string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* What to show when the icon cannot be drawn - no adapter, no such icon, a failed load. Nothing by
|
|
45
|
+
* default; the box keeps its size so layout does not shift.
|
|
46
|
+
*/
|
|
47
|
+
fallback?: ReactNode;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Draws a renderer-neutral {@link IconReference} through the adapter registered for its library, honoring
|
|
52
|
+
* size, color and accessible label. The reference is all that is persisted; this is where it becomes pixels.
|
|
53
|
+
*/
|
|
54
|
+
export const SceneIcon = ({ reference, adapters, size = '1em', color = 'currentColor', label, className, fallback }: SceneIconProps) => {
|
|
55
|
+
const inScope = useIconAdapters();
|
|
56
|
+
const state = useIconGlyph(reference, adapters ?? inScope);
|
|
57
|
+
const Glyph = state.status === 'ready' ? state.glyph : undefined;
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<span
|
|
61
|
+
className={className}
|
|
62
|
+
role={label ? 'img' : undefined}
|
|
63
|
+
aria-label={label}
|
|
64
|
+
aria-hidden={label ? undefined : true}
|
|
65
|
+
data-icon-library={reference.library}
|
|
66
|
+
data-icon-key={reference.key}
|
|
67
|
+
data-icon-variant={reference.variant}
|
|
68
|
+
data-icon-state={state.status}
|
|
69
|
+
style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center', width: size, height: size, color, flexShrink: 0 }}>
|
|
70
|
+
{Glyph ? <Glyph size={size} color={color} /> : state.status === 'unavailable' ? fallback : null}
|
|
71
|
+
</span>
|
|
72
|
+
);
|
|
73
|
+
};
|
package/icons/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 './IconAdapter';
|
|
5
|
+
export * from './IconAdapterRegistry';
|
|
6
|
+
export * from './IconAdapterContext';
|
|
7
|
+
export * from './useIconGlyph';
|
|
8
|
+
export * from './SceneIcon';
|
|
9
|
+
export * from './IconLibraryBundle';
|
|
@@ -0,0 +1,79 @@
|
|
|
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 { IconReference } from '@cratis/scene.model';
|
|
6
|
+
import { IconAdapter, IconGlyph } from './IconAdapter';
|
|
7
|
+
import { IconAdapterRegistry } from './IconAdapterRegistry';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Where loading one icon's artwork stands.
|
|
11
|
+
*/
|
|
12
|
+
export type IconGlyphState =
|
|
13
|
+
| { status: 'loading' }
|
|
14
|
+
| { status: 'ready'; glyph: IconGlyph }
|
|
15
|
+
| { status: 'unavailable'; reason: string };
|
|
16
|
+
|
|
17
|
+
const loads = new WeakMap<IconAdapter, Map<string, Promise<IconGlyph | undefined>>>();
|
|
18
|
+
|
|
19
|
+
function keyOf(reference: IconReference): string {
|
|
20
|
+
return JSON.stringify([reference.key, reference.variant ?? null]);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One load per adapter and icon, shared between every place the icon is drawn; a failed load is
|
|
25
|
+
* forgotten so the next render can try again.
|
|
26
|
+
*/
|
|
27
|
+
function load(adapter: IconAdapter, reference: IconReference): Promise<IconGlyph | undefined> {
|
|
28
|
+
let perAdapter = loads.get(adapter);
|
|
29
|
+
if (!perAdapter) {
|
|
30
|
+
perAdapter = new Map();
|
|
31
|
+
loads.set(adapter, perAdapter);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const key = keyOf(reference);
|
|
35
|
+
let pending = perAdapter.get(key);
|
|
36
|
+
if (!pending) {
|
|
37
|
+
pending = Promise.resolve().then(() => adapter.loadGlyph(reference));
|
|
38
|
+
perAdapter.set(key, pending);
|
|
39
|
+
pending.catch(() => perAdapter.delete(key));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return pending;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Loads the artwork for a reference through the adapter registered for its library. An absent registry,
|
|
47
|
+
* a library with no adapter, an icon the library lacks and a failed load are all `unavailable`, each with
|
|
48
|
+
* its reason - never a different icon.
|
|
49
|
+
*/
|
|
50
|
+
export function useIconGlyph(reference: IconReference, adapters: IconAdapterRegistry | undefined): IconGlyphState {
|
|
51
|
+
const [state, setState] = useState<IconGlyphState>({ status: 'loading' });
|
|
52
|
+
const { library, key, variant } = reference;
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
let isCurrent = true;
|
|
56
|
+
const adapter = adapters?.get(library);
|
|
57
|
+
if (!adapter) {
|
|
58
|
+
setState({ status: 'unavailable', reason: `No icon adapter is registered for the library '${library}'` });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
setState({ status: 'loading' });
|
|
63
|
+
load(adapter, { library, key, variant }).then(
|
|
64
|
+
(glyph) => {
|
|
65
|
+
if (!isCurrent) return;
|
|
66
|
+
setState(glyph ? { status: 'ready', glyph } : { status: 'unavailable', reason: `The library '${library}' has no icon '${key}'` });
|
|
67
|
+
},
|
|
68
|
+
(error: unknown) => {
|
|
69
|
+
if (isCurrent) setState({ status: 'unavailable', reason: error instanceof Error ? error.message : String(error) });
|
|
70
|
+
}
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return () => {
|
|
74
|
+
isCurrent = false;
|
|
75
|
+
};
|
|
76
|
+
}, [adapters, library, key, variant]);
|
|
77
|
+
|
|
78
|
+
return state;
|
|
79
|
+
}
|
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/scene.react",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.0",
|
|
4
4
|
"description": "The React renderer implementing Scene.Engine's Renderer contract against real React/DOM.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cratis",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"build-storybook": "storybook build"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@cratis/scene.engine": "4.
|
|
55
|
-
"@cratis/scene.model": "4.
|
|
54
|
+
"@cratis/scene.engine": "4.4.0",
|
|
55
|
+
"@cratis/scene.model": "4.4.0"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
|
-
"@cratis/scene.engine": "4.
|
|
59
|
-
"@cratis/scene.model": "4.
|
|
58
|
+
"@cratis/scene.engine": "4.4.0",
|
|
59
|
+
"@cratis/scene.model": "4.4.0",
|
|
60
60
|
"@storybook/addon-links": "^10.6.1",
|
|
61
61
|
"@storybook/react": "^10.6.1",
|
|
62
62
|
"@storybook/react-vite": "^10.6.1",
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Copyright (c) Cratis. All rights reserved.
|
|
2
2
|
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
3
|
|
|
4
|
-
import { DialogTemplate, Layout, ScenePackage, Screen, ScreenTemplate, Theme } from '@cratis/scene.model';
|
|
4
|
+
import { ComponentDescriptor, DialogTemplate, Layout, PackageKind, PropertyValueType, ScenePackage, Screen, ScreenTemplate, Theme } from '@cratis/scene.model';
|
|
5
|
+
import { IconAdapterRegistry, createIconAdapterRegistry } from '../icons/IconAdapterRegistry';
|
|
6
|
+
import { IconLibraryBundle } from '../icons/IconLibraryBundle';
|
|
5
7
|
import { ComponentRegistry } from '../renderer';
|
|
6
8
|
|
|
7
9
|
/**
|
|
@@ -50,10 +52,24 @@ export interface ScenePackageBundle {
|
|
|
50
52
|
*/
|
|
51
53
|
screens?: Screen[];
|
|
52
54
|
|
|
55
|
+
/**
|
|
56
|
+
* What can be edited on this package's components - one {@link ComponentDescriptor} per component that is
|
|
57
|
+
* editable at design time. They are data, so Studio reads them without rendering anything, and they travel with
|
|
58
|
+
* the components they describe rather than in a registry of their own. {@link validatePackageBundle} checks
|
|
59
|
+
* that every descriptor names a component the manifest declares.
|
|
60
|
+
*/
|
|
61
|
+
descriptors?: ComponentDescriptor[];
|
|
62
|
+
|
|
53
63
|
/**
|
|
54
64
|
* The themes this package ships, if any.
|
|
55
65
|
*/
|
|
56
66
|
themes?: Theme[];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The icon catalog and renderer adapter of an icon-library package. Present exactly when the
|
|
70
|
+
* manifest declares an icon library.
|
|
71
|
+
*/
|
|
72
|
+
iconLibrary?: IconLibraryBundle;
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
/**
|
|
@@ -71,7 +87,8 @@ export function componentRegistryKey(packageName: string, componentName: string)
|
|
|
71
87
|
}
|
|
72
88
|
|
|
73
89
|
/**
|
|
74
|
-
* Checks that a bundle provides everything its manifest declares, and declares everything it provides
|
|
90
|
+
* Checks that a bundle provides everything its manifest declares, and declares everything it provides, and that
|
|
91
|
+
* every descriptor it ships is for a component it declares and is well formed.
|
|
75
92
|
*
|
|
76
93
|
* A manifest that promises a component the bundle does not register renders as `UnresolvedComponent` at
|
|
77
94
|
* runtime — a blank box, far from where the mistake was made. A component registered but not declared is
|
|
@@ -123,6 +140,84 @@ export function validatePackageBundle(bundle: ScenePackageBundle): string[] {
|
|
|
123
140
|
}
|
|
124
141
|
}
|
|
125
142
|
|
|
143
|
+
problems.push(...validateIconLibrary(bundle));
|
|
144
|
+
problems.push(...validateDescriptors(bundle));
|
|
145
|
+
|
|
146
|
+
return problems;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function validateIconLibrary(bundle: ScenePackageBundle): string[] {
|
|
150
|
+
const problems: string[] = [];
|
|
151
|
+
const { manifest, iconLibrary } = bundle;
|
|
152
|
+
const declaresIconLibrary = manifest.kind === PackageKind.IconLibrary || manifest.iconLibrary !== undefined;
|
|
153
|
+
|
|
154
|
+
if (manifest.kind === PackageKind.IconLibrary && !manifest.iconLibrary) {
|
|
155
|
+
problems.push('is an icon library but declares no iconLibrary metadata');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (manifest.kind !== PackageKind.IconLibrary && manifest.iconLibrary) {
|
|
159
|
+
problems.push('declares iconLibrary metadata but is not of kind IconLibrary');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (declaresIconLibrary && !iconLibrary) {
|
|
163
|
+
problems.push('declares an icon library but the bundle provides no catalog or adapter for it');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!declaresIconLibrary && iconLibrary) {
|
|
167
|
+
problems.push('provides an icon catalog and adapter, which the manifest does not declare');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (iconLibrary) {
|
|
171
|
+
if (iconLibrary.catalog.library !== manifest.name) {
|
|
172
|
+
problems.push(`provides an icon catalog for '${iconLibrary.catalog.library}' instead of '${manifest.name}'`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (iconLibrary.adapter.library !== manifest.name) {
|
|
176
|
+
problems.push(`provides an icon adapter for '${iconLibrary.adapter.library}' instead of '${manifest.name}'`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (manifest.iconLibrary && !manifest.iconLibrary.renderers.includes('react')) {
|
|
180
|
+
problems.push("provides a React icon adapter but does not list 'react' among its icon renderers");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return problems;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function validateDescriptors(bundle: ScenePackageBundle): string[] {
|
|
188
|
+
const problems: string[] = [];
|
|
189
|
+
const declared = new Set(bundle.manifest.components.map((name) => componentRegistryKey(bundle.manifest.name, name)));
|
|
190
|
+
const described = new Set<string>();
|
|
191
|
+
|
|
192
|
+
for (const descriptor of bundle.descriptors ?? []) {
|
|
193
|
+
if (!declared.has(descriptor.component)) {
|
|
194
|
+
problems.push(`describes '${descriptor.component}', which the manifest does not declare as a component`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (described.has(descriptor.component)) {
|
|
198
|
+
problems.push(`describes '${descriptor.component}' more than once`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
described.add(descriptor.component);
|
|
202
|
+
|
|
203
|
+
const paths = new Set<string>();
|
|
204
|
+
for (const property of descriptor.properties) {
|
|
205
|
+
if (paths.has(property.path)) {
|
|
206
|
+
problems.push(`'${descriptor.component}' describes the property '${property.path}' more than once`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
paths.add(property.path);
|
|
210
|
+
|
|
211
|
+
if (property.valueType === PropertyValueType.Collection && property.item === undefined) {
|
|
212
|
+
problems.push(`'${descriptor.component}' describes the collection '${property.path}' without describing its items`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (property.valueType === PropertyValueType.Enum && (property.choices ?? []).length === 0) {
|
|
216
|
+
problems.push(`'${descriptor.component}' describes the enum '${property.path}' without choices`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
126
221
|
return problems;
|
|
127
222
|
}
|
|
128
223
|
|
|
@@ -135,3 +230,12 @@ export function validatePackageBundle(bundle: ScenePackageBundle): string[] {
|
|
|
135
230
|
export function mergePackageRegistries(bundles: ScenePackageBundle[]): ComponentRegistry {
|
|
136
231
|
return Object.assign({}, ...bundles.map((bundle) => bundle.components)) as ComponentRegistry;
|
|
137
232
|
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Collects the icon adapters of several bundles into the registry `SceneIcon` renders through.
|
|
236
|
+
*
|
|
237
|
+
* @throws Error when two bundles provide an adapter for the same library.
|
|
238
|
+
*/
|
|
239
|
+
export function mergeIconAdapters(bundles: ScenePackageBundle[]): IconAdapterRegistry {
|
|
240
|
+
return createIconAdapterRegistry(bundles.flatMap((bundle) => (bundle.iconLibrary ? [bundle.iconLibrary.adapter] : [])));
|
|
241
|
+
}
|