@levo-so/site-runtime 0.1.113
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/README.md +93 -0
- package/dist/LevoSiteProvider.d.ts +25 -0
- package/dist/LevoSiteProvider.js +23 -0
- package/dist/client.d.ts +26 -0
- package/dist/client.js +14 -0
- package/dist/data/index.d.ts +101 -0
- package/dist/data/index.js +79 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +6 -0
- package/dist/inspector/dom.d.ts +56 -0
- package/dist/inspector/dom.js +224 -0
- package/dist/inspector/editorChannel.d.ts +7 -0
- package/dist/inspector/editorChannel.js +19 -0
- package/dist/inspector/historyWatcher.d.ts +8 -0
- package/dist/inspector/historyWatcher.js +24 -0
- package/dist/inspector/index.d.ts +2 -0
- package/dist/inspector/index.js +191 -0
- package/dist/inspector/messageRouter.d.ts +38 -0
- package/dist/inspector/messageRouter.js +134 -0
- package/dist/inspector/originalsStash.d.ts +12 -0
- package/dist/inspector/originalsStash.js +24 -0
- package/dist/inspector/protocol.d.ts +180 -0
- package/dist/inspector/stagedOverrides.d.ts +28 -0
- package/dist/inspector/stagedOverrides.js +60 -0
- package/dist/inspector/textEditController.d.ts +24 -0
- package/dist/inspector/textEditController.js +62 -0
- package/dist/pageHead.d.ts +75 -0
- package/dist/pageHead.js +102 -0
- package/package.json +47 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
//#region src/inspector/originalsStash.ts
|
|
2
|
+
var e = () => {
|
|
3
|
+
let e = /* @__PURE__ */ new Map(), t = () => {
|
|
4
|
+
if (e.size > 50) {
|
|
5
|
+
let t = e.keys().next().value;
|
|
6
|
+
t !== void 0 && e.delete(t);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
return {
|
|
10
|
+
capture: (n, r) => {
|
|
11
|
+
e.set(n, r), t();
|
|
12
|
+
},
|
|
13
|
+
captureOnce: (n, r) => {
|
|
14
|
+
e.has(n) || (e.set(n, r), t());
|
|
15
|
+
},
|
|
16
|
+
get: (t) => e.get(t),
|
|
17
|
+
restore: (t) => {
|
|
18
|
+
let n = e.get(t);
|
|
19
|
+
return e.delete(t), n;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { e as createOriginalsStash };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
export interface IStagedStyleEntry {
|
|
2
|
+
kind: "style";
|
|
3
|
+
source: string;
|
|
4
|
+
index: number;
|
|
5
|
+
styles: Record<string, string>;
|
|
6
|
+
media?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface IStagedTextEntry {
|
|
9
|
+
kind: "text";
|
|
10
|
+
source: string;
|
|
11
|
+
index: number;
|
|
12
|
+
value: string;
|
|
13
|
+
}
|
|
14
|
+
export type IStagedEntry = IStagedStyleEntry | IStagedTextEntry;
|
|
15
|
+
export interface ILayersNode {
|
|
16
|
+
tag: string;
|
|
17
|
+
source?: string;
|
|
18
|
+
index?: number;
|
|
19
|
+
text?: string;
|
|
20
|
+
/** The element carries the `hidden` utility (display:none) — a reachability hint the
|
|
21
|
+
* Layers panel surfaces. Live + class-based, so it covers component internals too
|
|
22
|
+
* (the dashboard no longer parses JSX for this). Present only when true. */
|
|
23
|
+
hidden?: boolean;
|
|
24
|
+
childCount: number;
|
|
25
|
+
children: ILayersNode[];
|
|
26
|
+
}
|
|
27
|
+
export type IHistoryFn = History["pushState"];
|
|
28
|
+
export interface IPatchedHistoryFn {
|
|
29
|
+
(this: History, ...args: Parameters<IHistoryFn>): ReturnType<IHistoryFn>;
|
|
30
|
+
__inspectorPatched?: boolean;
|
|
31
|
+
__orig?: IHistoryFn;
|
|
32
|
+
}
|
|
33
|
+
/** A nested element locator — `file:line:col` + occurrence index. Never top-level. */
|
|
34
|
+
export interface IElementLocator {
|
|
35
|
+
source: string;
|
|
36
|
+
index?: number;
|
|
37
|
+
}
|
|
38
|
+
/** The element-selected payload — see `selectedPayload`. */
|
|
39
|
+
export interface ISelectedElement {
|
|
40
|
+
tag: string;
|
|
41
|
+
classes: string;
|
|
42
|
+
text: string;
|
|
43
|
+
label: string;
|
|
44
|
+
path: string;
|
|
45
|
+
/** null when the element carries no `data-tsd-source` stamp. */
|
|
46
|
+
source: string | null;
|
|
47
|
+
index: number;
|
|
48
|
+
occurrences: number;
|
|
49
|
+
ancestors: {
|
|
50
|
+
tag: string;
|
|
51
|
+
text: string;
|
|
52
|
+
source?: string;
|
|
53
|
+
}[];
|
|
54
|
+
/** Whole normalized textContent — present ONLY for text-mode-editable nodes. */
|
|
55
|
+
fullText?: string;
|
|
56
|
+
/**
|
|
57
|
+
* The nearest `data-levo-source` binding container above the selection (inclusive) — a
|
|
58
|
+
* platform-data-bound region (`useLevoSource`/`useLevoRecord`). Present only when the
|
|
59
|
+
* selection sits inside one. `config` is the parsed binding ({mode, module, identifier,
|
|
60
|
+
* query,…}), omitted when the attribute is malformed JSON. `element` is the binding
|
|
61
|
+
* CONTAINER's own locator, so a data edit targets the hook call site — not the clicked
|
|
62
|
+
* child. Lets the editor's "Integrate" dialog open pre-filled without parsing JSX.
|
|
63
|
+
*/
|
|
64
|
+
sourceBinding?: {
|
|
65
|
+
raw: string;
|
|
66
|
+
config?: unknown;
|
|
67
|
+
element: IElementLocator;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** The element-text-changed payload — see `commit`. */
|
|
71
|
+
export interface ITextChangedElement {
|
|
72
|
+
source: string | null;
|
|
73
|
+
index: number;
|
|
74
|
+
tag: string;
|
|
75
|
+
value: string;
|
|
76
|
+
previous?: string;
|
|
77
|
+
occurrences: number;
|
|
78
|
+
}
|
|
79
|
+
/** Editor → preview (received by `onMessage`; dashboard's `IOutboundMessage`). */
|
|
80
|
+
export type IEditorToPreview = {
|
|
81
|
+
type: "set-inspect";
|
|
82
|
+
enabled: boolean;
|
|
83
|
+
} | {
|
|
84
|
+
type: "set-text-edit";
|
|
85
|
+
enabled: boolean;
|
|
86
|
+
} | {
|
|
87
|
+
type: "clear-selection";
|
|
88
|
+
} | {
|
|
89
|
+
type: "select-element";
|
|
90
|
+
element: IElementLocator;
|
|
91
|
+
} | {
|
|
92
|
+
type: "edit-element";
|
|
93
|
+
element: IElementLocator;
|
|
94
|
+
} | {
|
|
95
|
+
type: "highlight-element";
|
|
96
|
+
element: IElementLocator;
|
|
97
|
+
} | {
|
|
98
|
+
type: "clear-highlight";
|
|
99
|
+
} | {
|
|
100
|
+
type: "request-layers";
|
|
101
|
+
} | {
|
|
102
|
+
type: "request-sources";
|
|
103
|
+
} | {
|
|
104
|
+
type: "revert-text";
|
|
105
|
+
element: IElementLocator;
|
|
106
|
+
} | {
|
|
107
|
+
type: "navigate";
|
|
108
|
+
route: string;
|
|
109
|
+
} | {
|
|
110
|
+
type: "stage-style";
|
|
111
|
+
stageId: string;
|
|
112
|
+
element: IElementLocator;
|
|
113
|
+
styles: Record<string, string>;
|
|
114
|
+
media?: string;
|
|
115
|
+
} | {
|
|
116
|
+
type: "stage-text";
|
|
117
|
+
stageId: string;
|
|
118
|
+
element: IElementLocator;
|
|
119
|
+
value: string;
|
|
120
|
+
} | {
|
|
121
|
+
type: "clear-staged";
|
|
122
|
+
stageId?: string;
|
|
123
|
+
};
|
|
124
|
+
/** Preview → editor (emitted by `send`; dashboard's `IInboundMessage`). */
|
|
125
|
+
export type IPreviewToEditor = {
|
|
126
|
+
type: "ready";
|
|
127
|
+
} | {
|
|
128
|
+
type: "inspect-state";
|
|
129
|
+
enabled: boolean;
|
|
130
|
+
} | {
|
|
131
|
+
type: "text-edit-state";
|
|
132
|
+
enabled: boolean;
|
|
133
|
+
} | {
|
|
134
|
+
type: "element-selected";
|
|
135
|
+
element: ISelectedElement;
|
|
136
|
+
} | {
|
|
137
|
+
type: "element-text-changed";
|
|
138
|
+
element: ITextChangedElement;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* `route` is the pathname (always present). `file` is the route's repo-relative source
|
|
142
|
+
* file (e.g. `src/routes/pricing.tsx`, no leading slash, no `:line:col`) — reserved:
|
|
143
|
+
* the runtime doesn't emit it yet; the dashboard falls back to url → file mapping.
|
|
144
|
+
*/
|
|
145
|
+
| {
|
|
146
|
+
type: "route";
|
|
147
|
+
route: string;
|
|
148
|
+
file?: string;
|
|
149
|
+
} | {
|
|
150
|
+
type: "staged-ack";
|
|
151
|
+
stageId: string;
|
|
152
|
+
} | {
|
|
153
|
+
type: "layers-tree";
|
|
154
|
+
tree: ILayersNode;
|
|
155
|
+
truncated: boolean;
|
|
156
|
+
} | {
|
|
157
|
+
type: "sources-list";
|
|
158
|
+
sources: IPageSource[];
|
|
159
|
+
} | {
|
|
160
|
+
type: "select-miss";
|
|
161
|
+
element: IElementLocator;
|
|
162
|
+
} | {
|
|
163
|
+
type: "highlight-miss";
|
|
164
|
+
element: IElementLocator;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* One platform-data binding found on the page — a `data-levo-source` container. Emitted in
|
|
168
|
+
* `sources-list` (reply to `request-sources`) so the editor's page-level Data manager can list
|
|
169
|
+
* every binding without a selection. `element` is the container's own `data-tsd-source` locator
|
|
170
|
+
* (a data edit targets the hook call site); `config` is the parsed binding, omitted when the
|
|
171
|
+
* attribute is malformed JSON.
|
|
172
|
+
*/
|
|
173
|
+
export interface IPageSource {
|
|
174
|
+
raw: string;
|
|
175
|
+
config?: unknown;
|
|
176
|
+
element: IElementLocator;
|
|
177
|
+
tag: string;
|
|
178
|
+
label: string;
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { IStagedEntry } from './protocol';
|
|
2
|
+
import { IOriginalsStash } from './originalsStash';
|
|
3
|
+
export type IStagedOverrides = ReturnType<typeof createStagedOverrides>;
|
|
4
|
+
export declare const createStagedOverrides: (deps: {
|
|
5
|
+
originals: IOriginalsStash;
|
|
6
|
+
/**
|
|
7
|
+
* The node currently in contenteditable (or null). A text override is never re-asserted
|
|
8
|
+
* onto the node being edited — see applyStagedEntry.
|
|
9
|
+
*/
|
|
10
|
+
editingNode: () => Element | null;
|
|
11
|
+
}) => {
|
|
12
|
+
upsert: (stageId: string, entry: IStagedEntry) => void;
|
|
13
|
+
clearOne: (stageId: string) => void;
|
|
14
|
+
clearAll: () => void;
|
|
15
|
+
scheduleReapply: () => void;
|
|
16
|
+
/** Registered override count — the DOM-observer arbitration reads this. */
|
|
17
|
+
count: () => number;
|
|
18
|
+
/** All registered stageIds — the clear-ALL ack loop reads these before clearing. */
|
|
19
|
+
ids: () => string[];
|
|
20
|
+
/**
|
|
21
|
+
* Unmount teardown: cancel the reapply rAF and hard-remove the injected sheet + every
|
|
22
|
+
* stamp. The registry can't survive a remount, and stale stamps/rules must not outlive
|
|
23
|
+
* it — the dashboard re-sends the full staged set on the next `ready`, so an
|
|
24
|
+
* HMR/StrictMode remount self-heals.
|
|
25
|
+
*/
|
|
26
|
+
teardown: () => void;
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=stagedOverrides.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { addStagedKey as e, cssRuleFor as t, nodeForSource as n, removeStagedKey as r } from "./dom.js";
|
|
2
|
+
//#region src/inspector/stagedOverrides.ts
|
|
3
|
+
var i = (i) => {
|
|
4
|
+
let { originals: a, editingNode: o } = i, s = /* @__PURE__ */ new Map(), c = () => {
|
|
5
|
+
let e = document.getElementById("studio-staged");
|
|
6
|
+
return e || (e = document.createElement("style"), e.id = "studio-staged", document.head.appendChild(e)), e;
|
|
7
|
+
}, l = () => {
|
|
8
|
+
let e = [];
|
|
9
|
+
for (let [n, r] of s) if (r.kind === "style") {
|
|
10
|
+
let i = t(n, r);
|
|
11
|
+
i && e.push(i);
|
|
12
|
+
}
|
|
13
|
+
let n = e.join("\n");
|
|
14
|
+
if (!n && !document.getElementById("studio-staged")) return;
|
|
15
|
+
let r = c();
|
|
16
|
+
r.textContent !== n && (r.textContent = n);
|
|
17
|
+
}, u = (t, r) => {
|
|
18
|
+
let i = n(r.source, r.index);
|
|
19
|
+
if (i) if (r.kind === "style") e(i, t);
|
|
20
|
+
else {
|
|
21
|
+
if (i === o()) return;
|
|
22
|
+
let e = `${r.source}#${r.index}`;
|
|
23
|
+
a.captureOnce(e, i.textContent ?? ""), i.textContent !== r.value && (i.textContent = r.value);
|
|
24
|
+
}
|
|
25
|
+
}, d = (e, t) => {
|
|
26
|
+
s.set(e, t), u(e, t), t.kind === "style" && l();
|
|
27
|
+
}, f = (e) => {
|
|
28
|
+
let t = s.get(e);
|
|
29
|
+
if (!t) return;
|
|
30
|
+
s.delete(e);
|
|
31
|
+
let i = n(t.source, t.index);
|
|
32
|
+
if (t.kind === "style") i && r(i, e), l();
|
|
33
|
+
else {
|
|
34
|
+
let e = a.restore(`${t.source}#${t.index}`);
|
|
35
|
+
i && e != null && (i.textContent = e);
|
|
36
|
+
}
|
|
37
|
+
}, p = () => {
|
|
38
|
+
for (let e of [...s.keys()]) f(e);
|
|
39
|
+
for (let e of Array.from(document.querySelectorAll("[data-staged-key]"))) e.removeAttribute("data-staged-key");
|
|
40
|
+
}, m = 0;
|
|
41
|
+
return {
|
|
42
|
+
upsert: d,
|
|
43
|
+
clearOne: f,
|
|
44
|
+
clearAll: p,
|
|
45
|
+
scheduleReapply: () => {
|
|
46
|
+
m || !s.size || (m = requestAnimationFrame(() => {
|
|
47
|
+
m = 0;
|
|
48
|
+
for (let [e, t] of s) u(e, t);
|
|
49
|
+
}));
|
|
50
|
+
},
|
|
51
|
+
count: () => s.size,
|
|
52
|
+
ids: () => [...s.keys()],
|
|
53
|
+
teardown: () => {
|
|
54
|
+
m && cancelAnimationFrame(m), document.getElementById("studio-staged")?.remove();
|
|
55
|
+
for (let e of Array.from(document.querySelectorAll("[data-staged-key]"))) e.removeAttribute("data-staged-key");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
//#endregion
|
|
60
|
+
export { i as createStagedOverrides };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { IEditorChannel } from './editorChannel';
|
|
2
|
+
import { IOriginalsStash } from './originalsStash';
|
|
3
|
+
export type ITextEditController = ReturnType<typeof createTextEditController>;
|
|
4
|
+
export declare const createTextEditController: (deps: {
|
|
5
|
+
channel: IEditorChannel;
|
|
6
|
+
originals: IOriginalsStash;
|
|
7
|
+
/**
|
|
8
|
+
* Drop the text-mode hover affordance (outline + tracked hover target). Cursor is NOT
|
|
9
|
+
* reset here — the pointer tracker owns it (hovering the edited node clears it there).
|
|
10
|
+
*/
|
|
11
|
+
clearTextHover: () => void;
|
|
12
|
+
}) => {
|
|
13
|
+
startEdit: (el: Element, caretAtEnd?: boolean) => void;
|
|
14
|
+
/** The node currently in contenteditable, or null — read by hover/click/staged logic. */
|
|
15
|
+
editingNode: () => Element | null;
|
|
16
|
+
/** Commit any in-flight edit (mode exit, defensive re-entry). No-op when idle. */
|
|
17
|
+
commitIfEditing: () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Teardown for unmount: end any in-flight edit WITHOUT committing (no report on
|
|
20
|
+
* teardown — matches the original closure's cleanup, which called endEdit directly).
|
|
21
|
+
*/
|
|
22
|
+
teardown: () => void;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=textEditController.d.ts.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { COLOR as e, nodesForSource as t, sourceIndex as n, sourceOf as r } from "./dom.js";
|
|
2
|
+
//#region src/inspector/textEditController.ts
|
|
3
|
+
var i = (i) => {
|
|
4
|
+
let { channel: a, originals: o, clearTextHover: s } = i, c = null, l = "", u = 0, d = (() => {
|
|
5
|
+
try {
|
|
6
|
+
let e = document.createElement("div");
|
|
7
|
+
return e.setAttribute("contenteditable", "plaintext-only"), e.contentEditable === "plaintext-only";
|
|
8
|
+
} catch {
|
|
9
|
+
return !1;
|
|
10
|
+
}
|
|
11
|
+
})(), f = () => {
|
|
12
|
+
c && _(c);
|
|
13
|
+
}, p = (e) => {
|
|
14
|
+
e.key === "Enter" ? (e.preventDefault(), c && _(c)) : e.key === "Escape" && (e.preventDefault(), c && v(c));
|
|
15
|
+
}, m = (e) => {
|
|
16
|
+
let t = window.getSelection();
|
|
17
|
+
if (!t) return;
|
|
18
|
+
let n = document.createRange();
|
|
19
|
+
n.selectNodeContents(e), n.collapse(!1), t.removeAllRanges(), t.addRange(n);
|
|
20
|
+
}, h = (t, i = !1) => {
|
|
21
|
+
if (c === t) return;
|
|
22
|
+
c && _(c), c = t, l = r(t) || "", u = n(t, l), s(), o.capture(`${l}#${u}`, t.textContent ?? ""), t.setAttribute("contenteditable", d ? "plaintext-only" : "true");
|
|
23
|
+
let a = t;
|
|
24
|
+
a.style.outline = `2px solid ${e.hover}`, a.style.outlineOffset = "3px", a.style.borderRadius = "3px", a.focus(), i && m(t), a.addEventListener("blur", f, !0), a.addEventListener("keydown", p, !0);
|
|
25
|
+
}, g = (e) => {
|
|
26
|
+
e.removeAttribute("contenteditable");
|
|
27
|
+
let t = e;
|
|
28
|
+
t.style.outline = "", t.style.outlineOffset = "", t.style.borderRadius = "", t.removeEventListener("blur", f, !0), t.removeEventListener("keydown", p, !0), c = null;
|
|
29
|
+
}, _ = (e) => {
|
|
30
|
+
let n = r(e), i = u, s = o.get(`${l}#${u}`);
|
|
31
|
+
g(e);
|
|
32
|
+
let c = (e.textContent ?? "").replace(/\s+/g, " ").trim();
|
|
33
|
+
a.send({
|
|
34
|
+
type: "element-text-changed",
|
|
35
|
+
element: {
|
|
36
|
+
source: n,
|
|
37
|
+
index: i,
|
|
38
|
+
tag: e.tagName.toLowerCase(),
|
|
39
|
+
value: c,
|
|
40
|
+
previous: s,
|
|
41
|
+
occurrences: n ? t(n).length : 1
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}, v = (e) => {
|
|
45
|
+
let t = `${l}#${u}`;
|
|
46
|
+
g(e);
|
|
47
|
+
let n = o.restore(t);
|
|
48
|
+
n != null && (e.textContent = n);
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
startEdit: h,
|
|
52
|
+
editingNode: () => c,
|
|
53
|
+
commitIfEditing: () => {
|
|
54
|
+
c && _(c);
|
|
55
|
+
},
|
|
56
|
+
teardown: () => {
|
|
57
|
+
c && g(c);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
//#endregion
|
|
62
|
+
export { i as createTextEditController };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** One page's head metadata. Every field optional — site defaults fill gaps per-field. */
|
|
2
|
+
export interface IPageMeta {
|
|
3
|
+
title?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
/** Absolute URL. Rendered as `<link rel="canonical">`. */
|
|
6
|
+
canonical?: string;
|
|
7
|
+
/** e.g. "index,follow" / "noindex,nofollow". */
|
|
8
|
+
robots?: string;
|
|
9
|
+
og?: {
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
image?: string;
|
|
13
|
+
/** Defaults to "website" when any og field is present. */
|
|
14
|
+
type?: string;
|
|
15
|
+
};
|
|
16
|
+
twitter?: {
|
|
17
|
+
/** Defaults to "summary_large_image" when any twitter field is present. */
|
|
18
|
+
card?: string;
|
|
19
|
+
title?: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
image?: string;
|
|
22
|
+
};
|
|
23
|
+
/** JSON-LD object(s) — each becomes a `<script type="application/ld+json">`. */
|
|
24
|
+
ldJson?: Record<string, unknown> | Array<Record<string, unknown>>;
|
|
25
|
+
/**
|
|
26
|
+
* Raw head snippet for THIS page. v1 supports `<script>`, `<meta>` and `<link>` tags
|
|
27
|
+
* only (the analytics / verification / preload cases) — anything else is dropped by
|
|
28
|
+
* `parseRawHead`, since arbitrary markup can't be expressed as document metadata.
|
|
29
|
+
*/
|
|
30
|
+
customHead?: string;
|
|
31
|
+
}
|
|
32
|
+
/** Site-wide defaults + site-wide raw head, from `src/pages/_site.meta.ts`. */
|
|
33
|
+
export interface ISiteMeta extends IPageMeta {
|
|
34
|
+
/** "%s · Acme" — %s is the page title. Applied only when the page provides a title. */
|
|
35
|
+
titleTemplate?: string;
|
|
36
|
+
}
|
|
37
|
+
interface IHeadEntry {
|
|
38
|
+
[attr: string]: string | undefined;
|
|
39
|
+
}
|
|
40
|
+
export interface IComposedHead {
|
|
41
|
+
meta: IHeadEntry[];
|
|
42
|
+
links: IHeadEntry[];
|
|
43
|
+
scripts: IHeadEntry[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Tolerant extraction of `<script>` / `<meta>` / `<link>` tags from a raw head snippet.
|
|
47
|
+
* Unsupported tags are DROPPED (documented v1 limitation) — silently at runtime; the
|
|
48
|
+
* editor warns at save time.
|
|
49
|
+
*/
|
|
50
|
+
export declare const parseRawHead: (raw: string) => IComposedHead;
|
|
51
|
+
/**
|
|
52
|
+
* Compose one page's document metadata from its sidecar + the site defaults. Page
|
|
53
|
+
* values win per-FIELD (an empty page og.title still inherits the site og.title).
|
|
54
|
+
* The object shape doubles as a TanStack Start route `head()` result for when the
|
|
55
|
+
* scaffold wires routes directly.
|
|
56
|
+
*/
|
|
57
|
+
export declare const pageHead: (meta?: IPageMeta, site?: ISiteMeta) => IComposedHead;
|
|
58
|
+
/**
|
|
59
|
+
* Renders a page's metadata as document tags. Pages include it anywhere in their JSX —
|
|
60
|
+
* React 19 hoists <title>/<meta>/<link> into <head> (SSR too); scripts render in place
|
|
61
|
+
* (see the file header caveat). The studio wires this automatically on a page's first
|
|
62
|
+
* meta save:
|
|
63
|
+
*
|
|
64
|
+
* import meta from "./about.meta";
|
|
65
|
+
* import siteMeta from "./_site.meta";
|
|
66
|
+
* import { PageHead } from "../lib/pageHead";
|
|
67
|
+
* …
|
|
68
|
+
* <PageHead meta={meta} site={siteMeta} />
|
|
69
|
+
*/
|
|
70
|
+
export declare function PageHead({ meta, site }: {
|
|
71
|
+
meta?: IPageMeta;
|
|
72
|
+
site?: ISiteMeta;
|
|
73
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
74
|
+
export {};
|
|
75
|
+
//# sourceMappingURL=pageHead.d.ts.map
|
package/dist/pageHead.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Fragment as e, jsx as t, jsxs as n } from "react/jsx-runtime";
|
|
2
|
+
//#region src/pageHead.tsx
|
|
3
|
+
var r = (e) => JSON.stringify(e).replace(/<\//g, "<\\/"), i = (e) => {
|
|
4
|
+
let t = {}, n = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*("([^"]*)"|'([^']*)'|[^\s"'>]+))?/g;
|
|
5
|
+
for (let r = n.exec(e); r; r = n.exec(e)) {
|
|
6
|
+
let e = r[1];
|
|
7
|
+
!e || /^on/i.test(e) || (t[e] = r[3] ?? r[4] ?? (r[2] ? r[2] : ""));
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
}, a = (e) => {
|
|
11
|
+
let t = {
|
|
12
|
+
meta: [],
|
|
13
|
+
links: [],
|
|
14
|
+
scripts: []
|
|
15
|
+
};
|
|
16
|
+
if (!e?.trim()) return t;
|
|
17
|
+
let n = e.replace(/<script\b([^>]*)>([\s\S]*?)<\/script\s*>/gi, (e, n, r) => {
|
|
18
|
+
let a = i(n), o = r.trim();
|
|
19
|
+
return o && (a.children = o), t.scripts.push(a), "";
|
|
20
|
+
});
|
|
21
|
+
return n = n.replace(/<(meta|link)\b([^>]*?)\/?>/gi, (e, n, r) => ((n.toLowerCase() === "meta" ? t.meta : t.links).push(i(r)), "")), t;
|
|
22
|
+
}, o = (e = {}, t = {}) => {
|
|
23
|
+
let n = {
|
|
24
|
+
meta: [],
|
|
25
|
+
links: [],
|
|
26
|
+
scripts: []
|
|
27
|
+
}, i = e.title?.trim(), o = t.title?.trim(), s = t.titleTemplate?.includes("%s") ? t.titleTemplate : void 0, c = i ? s ? s.replace("%s", i) : i : o;
|
|
28
|
+
c && n.meta.push({ title: c });
|
|
29
|
+
let l = e.description ?? t.description;
|
|
30
|
+
l && n.meta.push({
|
|
31
|
+
name: "description",
|
|
32
|
+
content: l
|
|
33
|
+
});
|
|
34
|
+
let u = e.robots ?? t.robots;
|
|
35
|
+
u && n.meta.push({
|
|
36
|
+
name: "robots",
|
|
37
|
+
content: u
|
|
38
|
+
});
|
|
39
|
+
let d = e.canonical ?? t.canonical;
|
|
40
|
+
d && n.links.push({
|
|
41
|
+
rel: "canonical",
|
|
42
|
+
href: d
|
|
43
|
+
});
|
|
44
|
+
let f = {
|
|
45
|
+
...t.og,
|
|
46
|
+
...e.og
|
|
47
|
+
}, p = f.title ?? i ?? o, m = f.description ?? l;
|
|
48
|
+
(p || m || f.image) && (n.meta.push({
|
|
49
|
+
property: "og:type",
|
|
50
|
+
content: f.type ?? "website"
|
|
51
|
+
}), p && n.meta.push({
|
|
52
|
+
property: "og:title",
|
|
53
|
+
content: p
|
|
54
|
+
}), m && n.meta.push({
|
|
55
|
+
property: "og:description",
|
|
56
|
+
content: m
|
|
57
|
+
}), f.image && n.meta.push({
|
|
58
|
+
property: "og:image",
|
|
59
|
+
content: f.image
|
|
60
|
+
}));
|
|
61
|
+
let h = {
|
|
62
|
+
...t.twitter,
|
|
63
|
+
...e.twitter
|
|
64
|
+
}, g = h.title ?? p, _ = h.description ?? m, v = h.image ?? f.image;
|
|
65
|
+
(g || _ || v) && (n.meta.push({
|
|
66
|
+
name: "twitter:card",
|
|
67
|
+
content: h.card ?? "summary_large_image"
|
|
68
|
+
}), g && n.meta.push({
|
|
69
|
+
name: "twitter:title",
|
|
70
|
+
content: g
|
|
71
|
+
}), _ && n.meta.push({
|
|
72
|
+
name: "twitter:description",
|
|
73
|
+
content: _
|
|
74
|
+
}), v && n.meta.push({
|
|
75
|
+
name: "twitter:image",
|
|
76
|
+
content: v
|
|
77
|
+
}));
|
|
78
|
+
let y = e.ldJson ? Array.isArray(e.ldJson) ? e.ldJson : [e.ldJson] : [];
|
|
79
|
+
for (let e of y) n.scripts.push({
|
|
80
|
+
type: "application/ld+json",
|
|
81
|
+
children: r(e)
|
|
82
|
+
});
|
|
83
|
+
for (let r of [t.customHead, e.customHead]) {
|
|
84
|
+
if (!r) continue;
|
|
85
|
+
let e = a(r);
|
|
86
|
+
n.meta.push(...e.meta), n.links.push(...e.links), n.scripts.push(...e.scripts);
|
|
87
|
+
}
|
|
88
|
+
return n;
|
|
89
|
+
};
|
|
90
|
+
function s({ meta: r, site: i }) {
|
|
91
|
+
let a = o(r, i);
|
|
92
|
+
return /* @__PURE__ */ n(e, { children: [
|
|
93
|
+
a.meta.map((e, n) => e.title === void 0 ? /* @__PURE__ */ t("meta", { ...e }, `m${n}`) : /* @__PURE__ */ t("title", { children: e.title }, `t${n}`)),
|
|
94
|
+
a.links.map((e, n) => /* @__PURE__ */ t("link", { ...e }, `l${n}`)),
|
|
95
|
+
a.scripts.map(({ children: e, ...n }, r) => e === void 0 ? /* @__PURE__ */ t("script", { ...n }, `s${r}`) : /* @__PURE__ */ t("script", {
|
|
96
|
+
...n,
|
|
97
|
+
dangerouslySetInnerHTML: { __html: e }
|
|
98
|
+
}, `s${r}`))
|
|
99
|
+
] });
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
export { s as PageHead, o as pageHead, a as parseRawHead };
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@levo-so/site-runtime",
|
|
3
|
+
"version": "0.1.113",
|
|
4
|
+
"description": "Runtime for Levo Studio v3 sites — the editor preview inspector, page head metadata, and the Levo data client.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"module": "./dist/index.js",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"default": "./dist/index.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@levo-so/core": "0.1.113",
|
|
19
|
+
"@levo-so/insights": "0.1.113"
|
|
20
|
+
},
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@tanstack/react-query": ">=5",
|
|
23
|
+
"react": ">=19"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@tanstack/react-query": "5.91.2",
|
|
27
|
+
"@types/react": "19.2.14",
|
|
28
|
+
"@vitejs/plugin-react": "6.0.1",
|
|
29
|
+
"typescript": "5.9.3",
|
|
30
|
+
"vite": "8.0.8",
|
|
31
|
+
"vite-plugin-dts": "4.5.4",
|
|
32
|
+
"@levo/ts-config": "0.0.0"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist/**/*",
|
|
36
|
+
"!dist/**/*.d.ts.map",
|
|
37
|
+
"!dist/**/*.js.map",
|
|
38
|
+
"package.json"
|
|
39
|
+
],
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "vite build",
|
|
43
|
+
"check-types": "tsc -b --noEmit",
|
|
44
|
+
"clean": "npx rimraf dist node_modules .turbo",
|
|
45
|
+
"dev": "vite build --watch"
|
|
46
|
+
}
|
|
47
|
+
}
|