@mosetta/react 0.0.0-stage → 0.0.10
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/CHANGELOG.md +122 -0
- package/LICENSE +21 -0
- package/README.md +46 -2
- package/package.json +30 -3
- package/src/components/BlueprintNodeRenderer.tsx +147 -0
- package/src/components/ComponentErrorHandler.tsx +73 -0
- package/src/components/DefaultRootComponent.tsx +43 -0
- package/src/components/Link.tsx +54 -0
- package/src/hooks/NodeContext.tsx +174 -0
- package/src/hooks/clientState.ts +31 -0
- package/src/hooks/di.tsx +65 -0
- package/src/hooks/useAddToHead.ts +27 -0
- package/src/hooks/useEditorContracts.ts +217 -0
- package/src/hooks/useForceRerender.ts +29 -0
- package/src/hooks/useHref.ts +67 -0
- package/src/hooks/useLocalStorageKey.ts +57 -0
- package/src/hooks/useMediaUpload.ts +20 -0
- package/src/hooks/usePage.ts +34 -0
- package/src/hooks/usePreviewType.ts +31 -0
- package/src/hooks/useResolvedSettings.ts +104 -0
- package/src/hooks/useRoute.ts +19 -0
- package/src/hooks/useRouter.ts +38 -0
- package/src/hooks/useSettingsDraft.ts +110 -0
- package/src/hooks/useShippedSettings.ts +27 -0
- package/src/hooks/useStyler.ts +34 -0
- package/src/hooks/useTheme.ts +189 -0
- package/src/hooks/useTile.tsx +34 -0
- package/src/react.ts +34 -0
- package/src/serverAction.ts +163 -0
- package/src/ssr.tsx +124 -0
- package/src/start.ts +79 -0
- package/src/utils/ProvidedContext.tsx +54 -0
- package/src/utils/definers.ts +190 -0
- package/src/utils/previewSlots.tsx +117 -0
- package/src/utils/tileReference.ts +59 -0
- package/tsconfig.json +3 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// SLOT DISCOVERY for the editor's preview.
|
|
2
|
+
//
|
|
3
|
+
// A slot is not declared anywhere ahead of time — it exists exactly when a module renders
|
|
4
|
+
// `useChildren`, and only in the configuration where it does. That is a feature: the editor must
|
|
5
|
+
// offer an insertion point where one truly is, not where a manifest once promised it. But nothing
|
|
6
|
+
// observable comes out of a hook call, so under preview each rendered slot also plants a MARKER:
|
|
7
|
+
// an empty anchor that registers itself here on mount and takes itself off on unmount. Whatever is
|
|
8
|
+
// registered right now IS the set of places a block can go.
|
|
9
|
+
//
|
|
10
|
+
// Nothing of this happens outside preview: the registry stays disarmed, `useChildren` returns the
|
|
11
|
+
// children and not a thing more, and production pages carry no markers at all.
|
|
12
|
+
//
|
|
13
|
+
// ARMING is deliberate, not automatic: the editor's agent arms the registry only after its channel
|
|
14
|
+
// is open — that is, after mount. Markers appearing during the first client render would disagree
|
|
15
|
+
// with the server's HTML and break hydration.
|
|
16
|
+
import { useEffect } from 'react';
|
|
17
|
+
|
|
18
|
+
/** what a module told the editor about a place it opened for children */
|
|
19
|
+
export interface ISlotDeclaration {
|
|
20
|
+
/** the tree node whose module rendered the slot */
|
|
21
|
+
readonly ownerId: string;
|
|
22
|
+
readonly slot: string;
|
|
23
|
+
/** how many children fit — see IChildrenSlot */
|
|
24
|
+
readonly max: number | 'many';
|
|
25
|
+
readonly min: number;
|
|
26
|
+
/** how many the document has put there */
|
|
27
|
+
readonly filled: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** the marker's identity in the DOM; the editor measures where the "+" should go by it */
|
|
31
|
+
export const SLOT_ATTRIBUTE = 'data-mosetta-slot';
|
|
32
|
+
|
|
33
|
+
export const slotKey = (ownerId: string, slot: string): string => `${ownerId}:${slot}`;
|
|
34
|
+
|
|
35
|
+
class PreviewSlots {
|
|
36
|
+
private armed = false;
|
|
37
|
+
|
|
38
|
+
private readonly declarations = new Map<string, ISlotDeclaration>();
|
|
39
|
+
|
|
40
|
+
// two audiences, two subscriptions on purpose: `useChildren` only ever needs to know whether
|
|
41
|
+
// markers are wanted, and must NOT re-render every module on the page each time some other
|
|
42
|
+
// slot registers. The agent, which does want exactly that, subscribes to the declarations.
|
|
43
|
+
private readonly armedListeners = new Set<() => void>();
|
|
44
|
+
|
|
45
|
+
private readonly listeners = new Set<() => void>();
|
|
46
|
+
|
|
47
|
+
readonly isArmed = (): boolean => this.armed;
|
|
48
|
+
|
|
49
|
+
/** server snapshot: there are no markers in a server render, ever */
|
|
50
|
+
readonly neverArmed = (): boolean => false;
|
|
51
|
+
|
|
52
|
+
readonly subscribeArmed = (listener: () => void): (() => void) => {
|
|
53
|
+
this.armedListeners.add(listener);
|
|
54
|
+
return () => this.armedListeners.delete(listener);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
readonly subscribe = (listener: () => void): (() => void) => {
|
|
58
|
+
this.listeners.add(listener);
|
|
59
|
+
return () => this.listeners.delete(listener);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** the editor's agent is live — from now on rendered slots plant their markers */
|
|
63
|
+
arm(): void {
|
|
64
|
+
if (this.armed) return;
|
|
65
|
+
this.armed = true;
|
|
66
|
+
for (const listener of this.armedListeners) listener();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** a slot is being rendered; the returned callback is its death notice */
|
|
70
|
+
register(declaration: ISlotDeclaration): () => void {
|
|
71
|
+
const key = slotKey(declaration.ownerId, declaration.slot);
|
|
72
|
+
this.declarations.set(key, declaration);
|
|
73
|
+
this.emit();
|
|
74
|
+
return () => {
|
|
75
|
+
// a re-registration under the same key may already have replaced it (settings edit) —
|
|
76
|
+
// only the current entry may remove itself
|
|
77
|
+
if (this.declarations.get(key) === declaration) {
|
|
78
|
+
this.declarations.delete(key);
|
|
79
|
+
this.emit();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** the places a block can go, as of this moment */
|
|
85
|
+
list(): ISlotDeclaration[] {
|
|
86
|
+
return [...this.declarations.values()];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private emit(): void {
|
|
90
|
+
for (const listener of this.listeners) listener();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const previewSlots = new PreviewSlots();
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* How tall the marker is when the module says nothing. A sizeless anchor was the first attempt —
|
|
98
|
+
* layout-neutral, but an empty slot then encloses nothing, and the editor had a "+" hovering over
|
|
99
|
+
* a hairline. A block of a few pixels gives every rendered slot something to see and aim at.
|
|
100
|
+
*/
|
|
101
|
+
export const DEFAULT_MARKER_HEIGHT = 15;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The marker itself: an empty block at the exact spot where the next child would land. It exists
|
|
105
|
+
* only under an armed preview, so the room it takes is room in the EDITOR's view of the page,
|
|
106
|
+
* never in the published one.
|
|
107
|
+
*/
|
|
108
|
+
export function SlotMarker({ height, ...declaration }: ISlotDeclaration & { height?: number }) {
|
|
109
|
+
const { ownerId, slot, max, min, filled } = declaration;
|
|
110
|
+
useEffect(
|
|
111
|
+
() => previewSlots.register({ ownerId, slot, max, min, filled }),
|
|
112
|
+
[ownerId, slot, max, min, filled],
|
|
113
|
+
);
|
|
114
|
+
return <span aria-hidden="true"
|
|
115
|
+
{...{ [SLOT_ATTRIBUTE]: slotKey(ownerId, slot) }}
|
|
116
|
+
style={{ display: 'block', flex: '0 0 auto', minHeight: `${height ?? DEFAULT_MARKER_HEIGHT}px` }} />;
|
|
117
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The canonical reference encoding, in one place: the parser (a full id string → its parts) and
|
|
2
|
+
// the composer (parts → the css scope class, which IS the full id). Mirrors cli/runtime/naming —
|
|
3
|
+
// the react binding only ever needs these two directions.
|
|
4
|
+
import type { ITileReference } from '@mosetta/core';
|
|
5
|
+
|
|
6
|
+
enum EModuleNameTokens {
|
|
7
|
+
NAMESPACE = '_ns_',
|
|
8
|
+
MODULE_TYPE = '_t_',
|
|
9
|
+
MODULE_NAME = '_n_',
|
|
10
|
+
MODULE_VERSION = '_v_',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
enum EModuleNameRegexTokens {
|
|
14
|
+
MODULE_NAME = 'moduleName',
|
|
15
|
+
MODULE_TYPE = 'moduleType',
|
|
16
|
+
MODULE_VERSION = 'version',
|
|
17
|
+
MODULE_NAMESPACE = 'moduleNamespace',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MODULE_NAME_REGEX = new RegExp([
|
|
21
|
+
'^',
|
|
22
|
+
`${EModuleNameTokens.NAMESPACE}(?<${EModuleNameRegexTokens.MODULE_NAMESPACE}>.+)`,
|
|
23
|
+
`${EModuleNameTokens.MODULE_TYPE}(?<${EModuleNameRegexTokens.MODULE_TYPE}>.+)`,
|
|
24
|
+
`${EModuleNameTokens.MODULE_NAME}(?<${EModuleNameRegexTokens.MODULE_NAME}>.+)`,
|
|
25
|
+
`${EModuleNameTokens.MODULE_VERSION}(?<${EModuleNameRegexTokens.MODULE_VERSION}>.+)`,
|
|
26
|
+
'$',
|
|
27
|
+
].join(''));
|
|
28
|
+
|
|
29
|
+
export function parseReference(reference: string): ITileReference {
|
|
30
|
+
const tokens = MODULE_NAME_REGEX.exec(reference)?.groups;
|
|
31
|
+
|
|
32
|
+
if (!tokens) {
|
|
33
|
+
throw new Error(`Invalid module reference: ${reference}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
namespace: tokens[EModuleNameRegexTokens.MODULE_NAMESPACE],
|
|
38
|
+
name: tokens[EModuleNameRegexTokens.MODULE_NAME],
|
|
39
|
+
version: tokens[EModuleNameRegexTokens.MODULE_VERSION],
|
|
40
|
+
type: tokens[EModuleNameRegexTokens.MODULE_TYPE],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The class a module's own css is scoped by — the one the renderer's scope host carries.
|
|
46
|
+
*
|
|
47
|
+
* Needed by any module that renders part of itself OUTSIDE its own subtree: a portal (an overlay,
|
|
48
|
+
* a popover, anything that must escape a scrolling panel) lands somewhere else in the document,
|
|
49
|
+
* where `.<reference> .thing` no longer matches and the markup arrives with no styles at all.
|
|
50
|
+
* Wrap what the portal renders in an element carrying this, and the module's css applies again.
|
|
51
|
+
*/
|
|
52
|
+
export function tileScopeClass({ namespace, name, type, version }: ITileReference): string {
|
|
53
|
+
return [
|
|
54
|
+
EModuleNameTokens.NAMESPACE, namespace,
|
|
55
|
+
EModuleNameTokens.MODULE_TYPE, type,
|
|
56
|
+
EModuleNameTokens.MODULE_NAME, name,
|
|
57
|
+
EModuleNameTokens.MODULE_VERSION, version,
|
|
58
|
+
].join('');
|
|
59
|
+
}
|
package/tsconfig.json
ADDED