mockpit 0.0.0-stage → 1.0.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/LICENSE +22 -0
- package/README.md +232 -2
- package/bin/demoData.js +132 -0
- package/bin/initDesign.js +846 -0
- package/bin/mockpit.js +1830 -0
- package/bin/serveUrl.d.ts +1 -0
- package/bin/serveUrl.js +7 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +230 -0
- package/dist/server/apiViews.d.ts +329 -0
- package/dist/server/apiViews.js +135 -0
- package/dist/server/app.d.ts +53 -0
- package/dist/server/app.js +3112 -0
- package/dist/server/base64.d.ts +1 -0
- package/dist/server/base64.js +16 -0
- package/dist/server/cdn.d.ts +6 -0
- package/dist/server/cdn.js +33 -0
- package/dist/server/codeRunner.d.ts +5 -0
- package/dist/server/codeRunner.js +89 -0
- package/dist/server/codeWorker.d.ts +1 -0
- package/dist/server/codeWorker.js +196 -0
- package/dist/server/designGuide.d.ts +16 -0
- package/dist/server/designGuide.js +311 -0
- package/dist/server/events.d.ts +53 -0
- package/dist/server/events.js +14 -0
- package/dist/server/feedbackBatch.d.ts +43 -0
- package/dist/server/feedbackBatch.js +98 -0
- package/dist/server/icons.d.ts +50 -0
- package/dist/server/icons.js +225 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +64 -0
- package/dist/server/kits.d.ts +36 -0
- package/dist/server/kits.js +279 -0
- package/dist/server/knobs.d.ts +15 -0
- package/dist/server/knobs.js +333 -0
- package/dist/server/mcpHttp.d.ts +53 -0
- package/dist/server/mcpHttp.js +143 -0
- package/dist/server/mcpSpec.d.ts +191 -0
- package/dist/server/mcpSpec.js +402 -0
- package/dist/server/parts.d.ts +31 -0
- package/dist/server/parts.js +338 -0
- package/dist/server/postMarkdown.d.ts +10 -0
- package/dist/server/postMarkdown.js +271 -0
- package/dist/server/postSurfaces.d.ts +40 -0
- package/dist/server/postSurfaces.js +407 -0
- package/dist/server/public.d.ts +5 -0
- package/dist/server/public.js +5 -0
- package/dist/server/push.d.ts +45 -0
- package/dist/server/push.js +234 -0
- package/dist/server/richRender.d.ts +15 -0
- package/dist/server/richRender.js +392 -0
- package/dist/server/run.d.ts +82 -0
- package/dist/server/run.js +290 -0
- package/dist/server/runApi.d.ts +6 -0
- package/dist/server/runApi.js +82 -0
- package/dist/server/slots.d.ts +14 -0
- package/dist/server/slots.js +71 -0
- package/dist/server/sqlStore.d.ts +119 -0
- package/dist/server/sqlStore.js +954 -0
- package/dist/server/sqliteStorage.d.ts +2 -0
- package/dist/server/sqliteStorage.js +90 -0
- package/dist/server/surfacePage.d.ts +48 -0
- package/dist/server/surfacePage.js +1099 -0
- package/dist/server/themes.d.ts +48 -0
- package/dist/server/themes.js +305 -0
- package/dist/server/types.d.ts +599 -0
- package/dist/server/types.js +299 -0
- package/dist/workers/public.d.ts +1 -0
- package/dist/workers/public.js +3 -0
- package/extensions/mockpit.js +703 -0
- package/guide/AGENT_SETUP.md +23 -0
- package/guide/topics/asks.md +87 -0
- package/guide/topics/feedback.md +127 -0
- package/guide/topics/html.md +175 -0
- package/guide/topics/http.md +76 -0
- package/guide/topics/knobs.md +72 -0
- package/guide/topics/scripts.md +129 -0
- package/guide/topics/surfaces.md +100 -0
- package/package.json +165 -4
- package/skills/launch-video/SKILL.md +159 -0
- package/skills/mockpit/SKILL.md +40 -0
- package/viewer/dist/index.html +1922 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
// Icons by name: an agent writes `<i icon="lucide:check"></i>` and the server
|
|
2
|
+
// inlines the SVG from an Iconify JSON set at render time, so an icon costs the
|
|
3
|
+
// agent its name and the frame needs no fetch. Runtime-agnostic (no node
|
|
4
|
+
// imports): it runs inside renderHtmlPage on the Worker too.
|
|
5
|
+
// Served with the server rather than installed per project: lucide (ISC) is
|
|
6
|
+
// what agents reach for by default, mage (Apache-2.0) is what `mockpit init`
|
|
7
|
+
// used to upload as a sprite, so mocks written against it keep rendering.
|
|
8
|
+
export const BUNDLED_ICON_PREFIXES = ["lucide", "mage"];
|
|
9
|
+
const NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
10
|
+
export const isIconName = (s) => typeof s === "string" && NAME_RE.test(s);
|
|
11
|
+
/** A parsed set, or null when `raw` is not a usable Iconify JSON set. */
|
|
12
|
+
export function parseIconSet(raw) {
|
|
13
|
+
let data = raw;
|
|
14
|
+
if (typeof data === "string") {
|
|
15
|
+
try {
|
|
16
|
+
data = JSON.parse(data);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (!data || typeof data !== "object")
|
|
23
|
+
return null;
|
|
24
|
+
const set = data;
|
|
25
|
+
if (!isIconName(set.prefix) || !set.icons || typeof set.icons !== "object")
|
|
26
|
+
return null;
|
|
27
|
+
return set;
|
|
28
|
+
}
|
|
29
|
+
/** Icons a listing should show: hidden ones exist only so old names keep resolving. */
|
|
30
|
+
export const iconCount = (set) => Object.values(set.icons).filter((i) => !i.hidden).length;
|
|
31
|
+
// Bundled sets are imported on first use, not at module load: they are ~600 KB
|
|
32
|
+
// of JSON each, and a workspace that never renders an icon never pays for it.
|
|
33
|
+
let bundled;
|
|
34
|
+
export function bundledIconSets() {
|
|
35
|
+
bundled ??= (async () => {
|
|
36
|
+
const out = new Map();
|
|
37
|
+
const loaders = {
|
|
38
|
+
lucide: () => import("@iconify-json/lucide/icons.json", { with: { type: "json" } }),
|
|
39
|
+
mage: () => import("@iconify-json/mage/icons.json", { with: { type: "json" } }),
|
|
40
|
+
};
|
|
41
|
+
for (const prefix of BUNDLED_ICON_PREFIXES) {
|
|
42
|
+
try {
|
|
43
|
+
const mod = (await loaders[prefix]());
|
|
44
|
+
const set = parseIconSet(mod.default);
|
|
45
|
+
if (set)
|
|
46
|
+
out.set(prefix, set);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
// A missing package costs that set's icons, never the render.
|
|
50
|
+
console.warn(`[mockpit] bundled icon set ${prefix} unavailable`, err);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
})();
|
|
55
|
+
return bundled;
|
|
56
|
+
}
|
|
57
|
+
const MAX_ALIAS_DEPTH = 8;
|
|
58
|
+
/** Resolve `name` in `set`, following aliases and applying their transforms. */
|
|
59
|
+
export function iconFromSet(set, name) {
|
|
60
|
+
// Aliases stack: the outermost alias's box wins, flips toggle, rotations add.
|
|
61
|
+
let box = {};
|
|
62
|
+
let rotate = 0;
|
|
63
|
+
let hFlip = false;
|
|
64
|
+
let vFlip = false;
|
|
65
|
+
let current = name;
|
|
66
|
+
for (let depth = 0; depth <= MAX_ALIAS_DEPTH; depth++) {
|
|
67
|
+
const icon = Object.hasOwn(set.icons, current) ? set.icons[current] : undefined;
|
|
68
|
+
const alias = !icon && set.aliases && Object.hasOwn(set.aliases, current)
|
|
69
|
+
? set.aliases[current]
|
|
70
|
+
: undefined;
|
|
71
|
+
const props = icon ?? alias;
|
|
72
|
+
if (!props)
|
|
73
|
+
return null;
|
|
74
|
+
box = { width: props.width, height: props.height, left: props.left, top: props.top, ...box };
|
|
75
|
+
for (const k of ["width", "height", "left", "top"]) {
|
|
76
|
+
if (box[k] === undefined)
|
|
77
|
+
delete box[k];
|
|
78
|
+
}
|
|
79
|
+
rotate += props.rotate ?? 0;
|
|
80
|
+
if (props.hFlip)
|
|
81
|
+
hFlip = !hFlip;
|
|
82
|
+
if (props.vFlip)
|
|
83
|
+
vFlip = !vFlip;
|
|
84
|
+
if (icon) {
|
|
85
|
+
const width = box.width ?? set.width ?? 16;
|
|
86
|
+
const height = box.height ?? set.height ?? 16;
|
|
87
|
+
const left = box.left ?? set.left ?? 0;
|
|
88
|
+
const top = box.top ?? set.top ?? 0;
|
|
89
|
+
return transform({ body: icon.body, left, top, width, height }, rotate % 4, hFlip, vFlip);
|
|
90
|
+
}
|
|
91
|
+
current = alias.parent;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function transform(icon, rotate, hFlip, vFlip) {
|
|
96
|
+
let { body, left, top, width, height } = icon;
|
|
97
|
+
const flips = [
|
|
98
|
+
hFlip ? `translate(${2 * left + width} 0) scale(-1 1)` : "",
|
|
99
|
+
vFlip ? `translate(0 ${2 * top + height}) scale(1 -1)` : "",
|
|
100
|
+
].filter(Boolean);
|
|
101
|
+
if (flips.length)
|
|
102
|
+
body = `<g transform="${flips.join(" ")}">${body}</g>`;
|
|
103
|
+
if (rotate) {
|
|
104
|
+
const cx = left + width / 2;
|
|
105
|
+
const cy = top + height / 2;
|
|
106
|
+
body = `<g transform="rotate(${rotate * 90} ${cx} ${cy})">${body}</g>`;
|
|
107
|
+
// A quarter turn swaps the box's sides around its center.
|
|
108
|
+
if (rotate % 2 === 1) {
|
|
109
|
+
[width, height] = [height, width];
|
|
110
|
+
left = cx - width / 2;
|
|
111
|
+
top = cy - height / 2;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return { body, left, top, width, height };
|
|
115
|
+
}
|
|
116
|
+
/** A resolver over in-memory sets; earlier sets win a prefix. */
|
|
117
|
+
export function resolverFor(sets) {
|
|
118
|
+
const byPrefix = new Map();
|
|
119
|
+
for (const set of sets)
|
|
120
|
+
if (!byPrefix.has(set.prefix))
|
|
121
|
+
byPrefix.set(set.prefix, set);
|
|
122
|
+
return (prefix, name) => {
|
|
123
|
+
const set = byPrefix.get(prefix);
|
|
124
|
+
return set ? iconFromSet(set, name) : null;
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
// --- expansion -------------------------------------------------------------
|
|
128
|
+
// Script, style and comment bodies are not markup: an icon attribute inside a
|
|
129
|
+
// JS string must stay a string, or the inlined quotes would break the script.
|
|
130
|
+
const OPAQUE_RE = /<script\b[\s\S]*?<\/script\s*>|<style\b[\s\S]*?<\/style\s*>|<!--[\s\S]*?-->/gi;
|
|
131
|
+
const ATTRS = `((?:[^>"']|"[^"]*"|'[^']*')*?)`;
|
|
132
|
+
const TAG_RE = new RegExp(`<([a-zA-Z][\\w-]*)(?:\\s${ATTRS})?(\\/?)>`, "g");
|
|
133
|
+
// The pre-expansion form `mockpit init` taught: a sprite reference.
|
|
134
|
+
const SPRITE_RE = new RegExp(`<svg\\b${ATTRS}>\\s*<use\\b${ATTRS}\\/?>\\s*(?:<\\/use>\\s*)?<\\/svg\\s*>`, "gi");
|
|
135
|
+
const ATTR_RE = /([^\s"'>/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
|
|
136
|
+
function parseAttrs(text) {
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const m of text.matchAll(ATTR_RE)) {
|
|
139
|
+
out.push({ name: m[1].toLowerCase(), raw: m[0], value: m[2] ?? m[3] ?? m[4] ?? null });
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
const fmt = (n) => String(Math.round(n * 1000) / 1000);
|
|
144
|
+
const viewBox = (i) => `${fmt(i.left)} ${fmt(i.top)} ${fmt(i.width)} ${fmt(i.height)}`;
|
|
145
|
+
// The element's own attributes survive (data-part, style, title…); `class`
|
|
146
|
+
// gains `icon` for the 1em sizing, and a decorative icon is hidden from
|
|
147
|
+
// assistive tech unless the author labelled it.
|
|
148
|
+
function svgFor(attrs, icon) {
|
|
149
|
+
const kept = attrs.filter((a) => a.name !== "icon" && a.name !== "class");
|
|
150
|
+
const cls = attrs.find((a) => a.name === "class")?.value?.trim();
|
|
151
|
+
const has = (n) => attrs.some((a) => a.name === n);
|
|
152
|
+
const extra = [
|
|
153
|
+
`class="${!cls ? "icon" : cls.split(/\s+/).includes("icon") ? cls : `icon ${cls}`}"`,
|
|
154
|
+
...kept.map((a) => a.raw),
|
|
155
|
+
has("viewbox") ? "" : `viewBox="${icon ? viewBox(icon) : "0 0 24 24"}"`,
|
|
156
|
+
has("aria-hidden") || has("role")
|
|
157
|
+
? ""
|
|
158
|
+
: has("aria-label") || has("aria-labelledby")
|
|
159
|
+
? 'role="img"'
|
|
160
|
+
: 'aria-hidden="true"',
|
|
161
|
+
].filter(Boolean);
|
|
162
|
+
return `<svg ${extra.join(" ")}>${icon?.body ?? ""}</svg>`;
|
|
163
|
+
}
|
|
164
|
+
function expandSegment(html, resolve, unknown) {
|
|
165
|
+
const lookup = (prefix, name) => {
|
|
166
|
+
const icon = resolve(prefix, name);
|
|
167
|
+
if (!icon)
|
|
168
|
+
unknown.add(`${prefix}:${name}`);
|
|
169
|
+
return icon;
|
|
170
|
+
};
|
|
171
|
+
const sprites = html.replace(SPRITE_RE, (whole, svgAttrs, useAttrs) => {
|
|
172
|
+
const href = parseAttrs(useAttrs).find((a) => a.name === "href" || a.name === "xlink:href");
|
|
173
|
+
const m = href?.value?.match(/^#mage-(.+)$/);
|
|
174
|
+
if (!m || !isIconName(m[1]))
|
|
175
|
+
return whole;
|
|
176
|
+
const icon = lookup("mage", m[1]);
|
|
177
|
+
return icon ? svgFor(parseAttrs(svgAttrs), icon) : whole;
|
|
178
|
+
});
|
|
179
|
+
let out = "";
|
|
180
|
+
let last = 0;
|
|
181
|
+
for (const m of sprites.matchAll(TAG_RE)) {
|
|
182
|
+
const attrs = m[2] ? parseAttrs(m[2]) : [];
|
|
183
|
+
const spec = attrs.find((a) => a.name === "icon")?.value;
|
|
184
|
+
const colon = spec?.indexOf(":") ?? -1;
|
|
185
|
+
if (!spec || colon < 0)
|
|
186
|
+
continue;
|
|
187
|
+
const prefix = spec.slice(0, colon);
|
|
188
|
+
const name = spec.slice(colon + 1);
|
|
189
|
+
if (!isIconName(prefix) || !isIconName(name))
|
|
190
|
+
continue;
|
|
191
|
+
let end = m.index + m[0].length;
|
|
192
|
+
// Only an empty element is an icon; one with content is left alone so
|
|
193
|
+
// nothing the author wrote is dropped.
|
|
194
|
+
if (!m[3]) {
|
|
195
|
+
const close = new RegExp(`^\\s*<\\/${m[1]}\\s*>`, "i").exec(sprites.slice(end));
|
|
196
|
+
if (!close)
|
|
197
|
+
continue;
|
|
198
|
+
end += close[0].length;
|
|
199
|
+
}
|
|
200
|
+
out += sprites.slice(last, m.index) + svgFor(attrs, lookup(prefix, name));
|
|
201
|
+
last = end;
|
|
202
|
+
}
|
|
203
|
+
return out + sprites.slice(last);
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Inline every `icon="prefix:name"` element (and the older
|
|
207
|
+
* `<svg><use href="#mage-name"/></svg>` sprite form) as an `<svg>`. Names the
|
|
208
|
+
* resolver does not know render as an empty 1em box and are listed in
|
|
209
|
+
* `unknown`, in first-seen order.
|
|
210
|
+
*/
|
|
211
|
+
// Cheap pre-check so html without icons never loads a set.
|
|
212
|
+
export const mayHaveIcons = (html) => html.includes("icon=") || html.includes("#mage-");
|
|
213
|
+
export function expandIcons(html, resolve) {
|
|
214
|
+
if (!mayHaveIcons(html))
|
|
215
|
+
return { html, unknown: [] };
|
|
216
|
+
const unknown = new Set();
|
|
217
|
+
let out = "";
|
|
218
|
+
let last = 0;
|
|
219
|
+
for (const m of html.matchAll(OPAQUE_RE)) {
|
|
220
|
+
out += expandSegment(html.slice(last, m.index), resolve, unknown) + m[0];
|
|
221
|
+
last = m.index + m[0].length;
|
|
222
|
+
}
|
|
223
|
+
out += expandSegment(html.slice(last), resolve, unknown);
|
|
224
|
+
return { html: out, unknown: [...unknown] };
|
|
225
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { createApp } from "./app.js";
|
|
7
|
+
import { createNodeExecutor } from "./codeRunner.js";
|
|
8
|
+
import { GUIDE_TOPICS } from "./designGuide.js";
|
|
9
|
+
import { SqlStore } from "./sqlStore.js";
|
|
10
|
+
import { createSqliteStorage } from "./sqliteStorage.js";
|
|
11
|
+
// Source layout puts this file at server/index.ts; the published package runs
|
|
12
|
+
// the compiled copy at dist/server/index.js. viewer/ and guide/ live at the
|
|
13
|
+
// package root either way.
|
|
14
|
+
let root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
if (basename(root) === "dist")
|
|
16
|
+
root = join(root, "..");
|
|
17
|
+
const [viewerHtml, setupText, topicTexts, pkgJson] = await Promise.all([
|
|
18
|
+
readFile(join(root, "viewer", "dist", "index.html"), "utf8").catch(() => {
|
|
19
|
+
console.error("viewer build missing — run `npm run build:viewer` first");
|
|
20
|
+
return process.exit(1);
|
|
21
|
+
}),
|
|
22
|
+
readFile(join(root, "guide", "AGENT_SETUP.md"), "utf8"),
|
|
23
|
+
Promise.all(GUIDE_TOPICS.map((id) => readFile(join(root, "guide", "topics", `${id}.md`), "utf8"))),
|
|
24
|
+
readFile(join(root, "package.json"), "utf8"),
|
|
25
|
+
]);
|
|
26
|
+
const topics = Object.fromEntries(GUIDE_TOPICS.map((id, i) => [id, topicTexts[i]]));
|
|
27
|
+
const pr = process.env.MOCKPIT_PUBLIC_READ;
|
|
28
|
+
const publicRead = pr === "session" || pr === "full" ? pr : undefined;
|
|
29
|
+
// Storage: SQLite via node:sqlite, the same SqlStore the Cloudflare Durable
|
|
30
|
+
// Object runs, so local mirrors the deploy. MOCKPIT_DB names the file; the
|
|
31
|
+
// default lives in ~/.mockpit/, a user-owned dir that survives reinstalls and
|
|
32
|
+
// is writable however the package was installed.
|
|
33
|
+
const dbPath = process.env.MOCKPIT_DB ?? join(homedir(), ".mockpit", "mockpit.db");
|
|
34
|
+
const store = new SqlStore(createSqliteStorage(dbPath));
|
|
35
|
+
console.log(`mockpit store: SQLite at ${dbPath}`);
|
|
36
|
+
const app = createApp({
|
|
37
|
+
store,
|
|
38
|
+
viewerHtml,
|
|
39
|
+
setupText,
|
|
40
|
+
topics,
|
|
41
|
+
authToken: process.env.MOCKPIT_TOKEN,
|
|
42
|
+
publicRead,
|
|
43
|
+
// MOCKPIT_VERSION fakes the running version (manual testing of the
|
|
44
|
+
// notice); set it to the empty string to disable the update check
|
|
45
|
+
version: process.env.MOCKPIT_VERSION ?? JSON.parse(pkgJson).version,
|
|
46
|
+
upgradeCommand: "npm install -g mockpit",
|
|
47
|
+
// `run` scripts execute in QuickJS inside a worker thread per run.
|
|
48
|
+
executor: createNodeExecutor(),
|
|
49
|
+
});
|
|
50
|
+
const port = Number(process.env.PORT ?? 8228);
|
|
51
|
+
// MOCKPIT_HOST (or `serve --host`) restricts the listener to one address.
|
|
52
|
+
// Unset keeps the previous behaviour — node's default, every interface — because
|
|
53
|
+
// that is what a container or a LAN-shared instance needs. Set it to 127.0.0.1
|
|
54
|
+
// when the server shares a host with anything you don't want reaching it; that
|
|
55
|
+
// is stronger than the token, which is a single shared secret by design.
|
|
56
|
+
const hostname = process.env.MOCKPIT_HOST || undefined;
|
|
57
|
+
serve({ fetch: app.fetch, port, hostname }, (info) => {
|
|
58
|
+
// Report the address actually bound. Printing "localhost" unconditionally hid
|
|
59
|
+
// the fact that the default listens on every interface.
|
|
60
|
+
const shown = hostname ?? "localhost";
|
|
61
|
+
const authority = shown.includes(":") ? `[${shown}]` : shown;
|
|
62
|
+
console.log(`mockpit listening on http://${authority}:${info.port}` +
|
|
63
|
+
(hostname ? "" : " (all interfaces — set MOCKPIT_HOST to restrict)"));
|
|
64
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ProjectKit } from "./types.ts";
|
|
2
|
+
export interface Kit {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
summary: string;
|
|
6
|
+
classes: string;
|
|
7
|
+
css: string;
|
|
8
|
+
js?: string;
|
|
9
|
+
href?: string;
|
|
10
|
+
script?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const CORE_CSS = "\n.stack{display:flex;flex-direction:column;gap:8px}.stack.sm{gap:4px}.stack.lg{gap:16px}\n.row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.row.sm{gap:4px}\n.between{display:flex;align-items:center;justify-content:space-between;gap:12px}\n.grow{flex:1;min-width:0}\n.title{font:500 15px/1.35 var(--font-sans);color:var(--color-text-primary)}\n.dim{color:var(--color-text-secondary)}.faint{color:var(--color-text-tertiary)}\n.mono{font-family:var(--font-mono)}.num{font-variant-numeric:tabular-nums}\n.hr{height:1px;border:0;margin:2px 0;background:var(--color-border-secondary)}\n.kbd{font:400 12px/1 var(--font-mono);padding:2px 6px;border:1px solid var(--color-border-secondary);border-bottom-width:2px;border-radius:6px;background:var(--color-background-secondary);color:var(--color-text-secondary)}\n";
|
|
13
|
+
export declare const KITS: Kit[];
|
|
14
|
+
export declare const isKnownKit: (id: unknown) => id is string;
|
|
15
|
+
export declare const KIT_IDS: string[];
|
|
16
|
+
export declare const kitSummaries: (projectKits?: readonly ProjectKit[]) => ({
|
|
17
|
+
href?: string | undefined;
|
|
18
|
+
id: string;
|
|
19
|
+
source: "bundled";
|
|
20
|
+
label: string;
|
|
21
|
+
summary: string;
|
|
22
|
+
classes: string;
|
|
23
|
+
} | {
|
|
24
|
+
source: "project";
|
|
25
|
+
id: string;
|
|
26
|
+
href: string;
|
|
27
|
+
script?: string;
|
|
28
|
+
doc: string;
|
|
29
|
+
})[];
|
|
30
|
+
export declare const RESERVED_KIT_IDS: string[];
|
|
31
|
+
export declare const PROJECT_KIT_DOC_MAX = 1200;
|
|
32
|
+
export declare function checkProjectKit(raw: unknown): ProjectKit | string;
|
|
33
|
+
export declare function kitAssets(ids: readonly string[] | undefined): {
|
|
34
|
+
css: string;
|
|
35
|
+
js: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// Opt-in style/behavior bundles for html surfaces. An html surface may list kit ids
|
|
2
|
+
// in its `kits` field; renderHtmlPage concatenates each requested kit's CSS
|
|
3
|
+
// (and JS) into the sandboxed document AFTER the base KIT_CSS. A default html
|
|
4
|
+
// surface (no `kits`) is untouched — the richer vocabulary only ships when asked
|
|
5
|
+
// for, so kits never homogenize freeform html. Kits are a library you import
|
|
6
|
+
// per surface, not a frame every surface is locked into.
|
|
7
|
+
//
|
|
8
|
+
// Runtime-agnostic (no node imports): imported by surfacePage (server render),
|
|
9
|
+
// postSurfaces (id allowlist), and surfaced over HTTP/MCP for discovery. Every
|
|
10
|
+
// class resolves against the theme `--color-*` / `--font-*` / radius tokens, so
|
|
11
|
+
// kit output re-themes with the workspace like any other html surface.
|
|
12
|
+
import { checkCdnUrl } from "./cdn.js";
|
|
13
|
+
// Layout + text helpers shared by every kit — injected ONCE whenever any kit is
|
|
14
|
+
// requested, so kit-specific css below only declares its distinctive classes.
|
|
15
|
+
// Exported so surfacePage.ts can publish it as its own content-hashed
|
|
16
|
+
// stylesheet instead of inlining it into every surface document.
|
|
17
|
+
export const CORE_CSS = `
|
|
18
|
+
.stack{display:flex;flex-direction:column;gap:8px}.stack.sm{gap:4px}.stack.lg{gap:16px}
|
|
19
|
+
.row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}.row.sm{gap:4px}
|
|
20
|
+
.between{display:flex;align-items:center;justify-content:space-between;gap:12px}
|
|
21
|
+
.grow{flex:1;min-width:0}
|
|
22
|
+
.title{font:500 15px/1.35 var(--font-sans);color:var(--color-text-primary)}
|
|
23
|
+
.dim{color:var(--color-text-secondary)}.faint{color:var(--color-text-tertiary)}
|
|
24
|
+
.mono{font-family:var(--font-mono)}.num{font-variant-numeric:tabular-nums}
|
|
25
|
+
.hr{height:1px;border:0;margin:2px 0;background:var(--color-border-secondary)}
|
|
26
|
+
.kbd{font:400 12px/1 var(--font-mono);padding:2px 6px;border:1px solid var(--color-border-secondary);border-bottom-width:2px;border-radius:6px;background:var(--color-background-secondary);color:var(--color-text-secondary)}
|
|
27
|
+
`;
|
|
28
|
+
// issues: cards, status badges/dots, mono ref chips, rollup bars, and a nesting
|
|
29
|
+
// rail (.tree → nest another .tree to indent). Composes an issue/PR/CI tree,
|
|
30
|
+
// a status board, or a PR summary from generic primitives.
|
|
31
|
+
const ISSUES_CSS = `
|
|
32
|
+
.card{background:var(--color-background-primary);border:1px solid var(--color-border-secondary);border-radius:var(--border-radius-lg);padding:14px 16px}
|
|
33
|
+
.card.soft{background:var(--color-background-secondary)}
|
|
34
|
+
.badge{display:inline-flex;align-items:center;gap:5px;font:500 12px/1.4 var(--font-sans);padding:2px 9px;border-radius:999px;background:var(--color-background-secondary);color:var(--color-text-secondary)}
|
|
35
|
+
.badge.ok{background:var(--color-background-success);color:var(--color-text-success)}
|
|
36
|
+
.badge.info{background:var(--color-background-info);color:var(--color-text-info)}
|
|
37
|
+
.badge.warn{background:var(--color-background-warning);color:var(--color-text-warning)}
|
|
38
|
+
.badge.danger{background:var(--color-background-danger);color:var(--color-text-danger)}
|
|
39
|
+
.chip{display:inline-flex;align-items:center;gap:4px;font:400 12px/1.4 var(--font-mono);padding:1px 7px;border-radius:var(--border-radius-md);border:1px solid var(--color-border-secondary);color:var(--color-text-secondary)}
|
|
40
|
+
.dot{width:8px;height:8px;border-radius:999px;background:var(--color-text-tertiary);flex:none}
|
|
41
|
+
.dot.ok{background:var(--color-text-success)}.dot.info{background:var(--color-text-info)}.dot.warn{background:var(--color-text-warning)}.dot.danger{background:var(--color-text-danger)}
|
|
42
|
+
.bar{height:6px;border-radius:999px;background:var(--color-background-secondary);overflow:hidden}
|
|
43
|
+
.bar>i{display:block;height:100%;border-radius:inherit;background:var(--color-text-success)}
|
|
44
|
+
.tree{display:flex;flex-direction:column;gap:7px;margin:0;padding:0;list-style:none}
|
|
45
|
+
.tree .tree{margin-top:7px;margin-left:9px;padding-left:14px;border-left:1px solid var(--color-border-secondary)}
|
|
46
|
+
`;
|
|
47
|
+
// slides: a stepped deck. Author `.deck` with `.slide` children; the JS shows
|
|
48
|
+
// one at a time and injects prev/dots/counter/next controls. Arrow keys and
|
|
49
|
+
// PageUp/Down navigate (plain keys only — the host owns the meta/alt combos).
|
|
50
|
+
//
|
|
51
|
+
// The slides are GRID-STACKED (every `.slide` in the same `1/1` cell), not
|
|
52
|
+
// swapped with display:none or overlaid with position:absolute. That keeps them
|
|
53
|
+
// in normal flow — so the deck (and the document) sizes to the TALLEST slide and
|
|
54
|
+
// the height the surface reports stays stable — while still letting them overlap
|
|
55
|
+
// so inactive slides can cross-fade under the active one. An absolute overlay
|
|
56
|
+
// would grow scrollHeight without growing the measured box, and the frame's
|
|
57
|
+
// ResizeObserver (which watches the box, not scrollHeight) would go blind to it
|
|
58
|
+
// and clip the deck; grid-stacking avoids that trap. Inactive slides stay laid
|
|
59
|
+
// out (so they keep sizing the track) but are visibility:hidden — out of the tab
|
|
60
|
+
// order and the a11y tree — until they become `.on`.
|
|
61
|
+
const SLIDES_CSS = `
|
|
62
|
+
.deck{display:grid;min-height:140px}
|
|
63
|
+
.deck>.slide{grid-area:1/1;opacity:0;visibility:hidden;pointer-events:none;transition:opacity .3s ease,visibility 0s linear .3s}
|
|
64
|
+
.deck>.slide.on{opacity:1;visibility:visible;pointer-events:auto;transition:opacity .3s ease}
|
|
65
|
+
@media (prefers-reduced-motion:reduce){.deck>.slide{transition:none}}
|
|
66
|
+
.deck>.slide h2{font:500 22px/1.3 var(--font-sans);margin:0 0 14px}
|
|
67
|
+
.deck-ctl{display:flex;align-items:center;justify-content:center;gap:14px;margin-top:18px;padding-top:14px;border-top:1px solid var(--color-border-secondary)}
|
|
68
|
+
.deck-dots{display:inline-flex;gap:7px}
|
|
69
|
+
.deck-dots i{width:7px;height:7px;border-radius:999px;background:var(--color-border-primary);cursor:pointer}
|
|
70
|
+
.deck-dots i.on{background:var(--color-text-info)}
|
|
71
|
+
.deck-num{font:400 13px/1 var(--font-mono);color:var(--color-text-tertiary);min-width:46px;text-align:center}
|
|
72
|
+
`;
|
|
73
|
+
const SLIDES_JS = `
|
|
74
|
+
(function(){
|
|
75
|
+
var deck=document.querySelector('.deck');if(!deck)return;
|
|
76
|
+
var slides=[].slice.call(deck.querySelectorAll(':scope > .slide'));if(!slides.length)return;
|
|
77
|
+
var i=0;
|
|
78
|
+
var ctl=document.createElement('div');ctl.className='deck-ctl';
|
|
79
|
+
var prev=document.createElement('button');prev.type='button';prev.setAttribute('aria-label','Previous slide');prev.textContent='‹';
|
|
80
|
+
var next=document.createElement('button');next.type='button';next.setAttribute('aria-label','Next slide');next.textContent='›';
|
|
81
|
+
var dots=document.createElement('span');dots.className='deck-dots';
|
|
82
|
+
var num=document.createElement('span');num.className='deck-num';
|
|
83
|
+
slides.forEach(function(_,k){var d=document.createElement('i');d.setAttribute('role','button');d.addEventListener('click',function(){go(k);});dots.appendChild(d);});
|
|
84
|
+
ctl.appendChild(prev);ctl.appendChild(dots);ctl.appendChild(num);ctl.appendChild(next);
|
|
85
|
+
deck.appendChild(ctl);
|
|
86
|
+
function go(n){i=(n+slides.length)%slides.length;
|
|
87
|
+
slides.forEach(function(s,k){s.classList.toggle('on',k===i);});
|
|
88
|
+
[].forEach.call(dots.children,function(d,k){d.classList.toggle('on',k===i);});
|
|
89
|
+
num.textContent=(i+1)+' / '+slides.length;}
|
|
90
|
+
prev.addEventListener('click',function(){go(i-1);});
|
|
91
|
+
next.addEventListener('click',function(){go(i+1);});
|
|
92
|
+
document.addEventListener('keydown',function(e){
|
|
93
|
+
if(e.metaKey||e.altKey||e.ctrlKey)return;
|
|
94
|
+
if(e.key==='ArrowRight'||e.key==='PageDown'){e.preventDefault();go(i+1);}
|
|
95
|
+
else if(e.key==='ArrowLeft'||e.key==='PageUp'){e.preventDefault();go(i-1);}});
|
|
96
|
+
go(0);
|
|
97
|
+
})();
|
|
98
|
+
`;
|
|
99
|
+
// builtin: the fallback component vocabulary `mockpit init` injects when a
|
|
100
|
+
// project has no Tailwind/shadcn of its own. Class names mirror shadcn so an
|
|
101
|
+
// agent that knows one writes the other without a lookup, but every rule is
|
|
102
|
+
// CSS-only — no JS, no CDN — and every color comes from the `--color-*` tokens
|
|
103
|
+
// the wrapper already emits, so it re-themes with the workspace and with an
|
|
104
|
+
// imported project palette alike. Radius prefers the project's own `--radius`
|
|
105
|
+
// (imported verbatim by init) and falls back to the mockpit token.
|
|
106
|
+
//
|
|
107
|
+
// It deliberately redeclares `.card`/`.badge` from the `issues` kit: surfaces
|
|
108
|
+
// ask for one or the other, and renderHtmlPage appends builtin last so a
|
|
109
|
+
// project's component look wins if both are somehow present.
|
|
110
|
+
const BUILTIN_CSS = `
|
|
111
|
+
:root{--r:var(--radius,var(--border-radius-md))}
|
|
112
|
+
.btn{display:inline-flex;align-items:center;justify-content:center;gap:6px;font:500 14px/1 var(--font-sans);padding:9px 16px;border-radius:var(--r);border:1px solid var(--color-border-secondary);background:var(--color-background-primary);color:var(--color-text-primary);cursor:pointer;text-decoration:none;white-space:nowrap}
|
|
113
|
+
.btn:hover{background:var(--color-background-secondary)}
|
|
114
|
+
.btn:focus-visible{outline:2px solid var(--color-border-info);outline-offset:2px}
|
|
115
|
+
.btn[disabled],.btn.disabled{opacity:.5;pointer-events:none}
|
|
116
|
+
.btn-primary{background:var(--color-text-info);border-color:var(--color-text-info);color:var(--color-background-primary)}
|
|
117
|
+
.btn-primary:hover{opacity:.9;background:var(--color-text-info)}
|
|
118
|
+
.btn-secondary{background:var(--color-background-secondary);border-color:transparent}
|
|
119
|
+
.btn-ghost{background:none;border-color:transparent}
|
|
120
|
+
.btn-ghost:hover{background:var(--color-background-secondary)}
|
|
121
|
+
.btn-destructive{background:var(--color-text-danger);border-color:var(--color-text-danger);color:var(--color-background-primary)}
|
|
122
|
+
.btn-sm{font-size:13px;padding:6px 11px}
|
|
123
|
+
.btn-lg{font-size:15px;padding:12px 22px}
|
|
124
|
+
.card{background:var(--color-background-primary);border:1px solid var(--color-border-secondary);border-radius:calc(var(--r) + 4px);padding:20px;display:flex;flex-direction:column;gap:12px}
|
|
125
|
+
.card-header{display:flex;flex-direction:column;gap:4px}
|
|
126
|
+
.card-title{font:500 16px/1.3 var(--font-sans);margin:0}
|
|
127
|
+
.card-desc{font-size:13px;color:var(--color-text-secondary);margin:0}
|
|
128
|
+
.card-content{display:flex;flex-direction:column;gap:10px}
|
|
129
|
+
.card-footer{display:flex;align-items:center;gap:8px;padding-top:4px}
|
|
130
|
+
.label{font:500 13px/1.3 var(--font-sans);color:var(--color-text-primary)}
|
|
131
|
+
.input,.textarea,.select{width:100%;font:14px/1.4 var(--font-sans);color:var(--color-text-primary);background:var(--color-background-primary);border:1px solid var(--color-border-secondary);border-radius:var(--r);padding:8px 11px;outline:none}
|
|
132
|
+
.input:focus,.textarea:focus,.select:focus{border-color:var(--color-border-info);box-shadow:0 0 0 1px var(--color-border-info)}
|
|
133
|
+
.input::placeholder,.textarea::placeholder{color:var(--color-text-tertiary)}
|
|
134
|
+
.textarea{min-height:76px;resize:vertical}
|
|
135
|
+
.field{display:flex;flex-direction:column;gap:6px}
|
|
136
|
+
.badge{display:inline-flex;align-items:center;gap:5px;font:500 12px/1.4 var(--font-sans);padding:2px 9px;border-radius:999px;background:var(--color-text-info);color:var(--color-background-primary);border:1px solid transparent}
|
|
137
|
+
.badge-secondary{background:var(--color-background-secondary);color:var(--color-text-secondary)}
|
|
138
|
+
.badge-outline{background:none;border-color:var(--color-border-secondary);color:var(--color-text-secondary)}
|
|
139
|
+
.badge-destructive{background:var(--color-text-danger);color:var(--color-background-primary)}
|
|
140
|
+
.tabs{display:flex;flex-direction:column;gap:14px}
|
|
141
|
+
.tabs-list{display:inline-flex;gap:2px;padding:3px;border-radius:var(--r);background:var(--color-background-secondary);align-self:flex-start}
|
|
142
|
+
.tab{font:500 13px/1 var(--font-sans);padding:7px 13px;border:0;border-radius:calc(var(--r) - 2px);background:none;color:var(--color-text-secondary);cursor:pointer}
|
|
143
|
+
.tab:hover{color:var(--color-text-primary)}
|
|
144
|
+
.tab.on,.tab[aria-selected=true]{background:var(--color-background-primary);color:var(--color-text-primary)}
|
|
145
|
+
.tab-panel{display:none}.tab-panel.on{display:block}
|
|
146
|
+
.dialog{position:relative;margin:0 auto;max-width:440px;background:var(--color-background-primary);border:1px solid var(--color-border-secondary);border-radius:calc(var(--r) + 4px);padding:22px;display:flex;flex-direction:column;gap:14px;box-shadow:none}
|
|
147
|
+
.dialog-scrim{padding:26px;border-radius:calc(var(--r) + 6px);background:var(--color-background-tertiary)}
|
|
148
|
+
.dialog-header{display:flex;flex-direction:column;gap:5px}
|
|
149
|
+
.dialog-title{font:500 16px/1.3 var(--font-sans);margin:0}
|
|
150
|
+
.dialog-desc{font-size:13px;color:var(--color-text-secondary);margin:0}
|
|
151
|
+
.dialog-footer{display:flex;justify-content:flex-end;gap:8px}
|
|
152
|
+
.table{width:100%;border-collapse:collapse;font-size:14px}
|
|
153
|
+
.table th{text-align:left;font-weight:500;color:var(--color-text-secondary);padding:8px 12px;border-bottom:1px solid var(--color-border-secondary)}
|
|
154
|
+
.table td{padding:9px 12px;border-bottom:1px solid var(--color-border-tertiary)}
|
|
155
|
+
.table tbody tr:last-child td{border-bottom:0}
|
|
156
|
+
.table tbody tr:hover{background:var(--color-background-secondary)}
|
|
157
|
+
.table .num{text-align:right}
|
|
158
|
+
.sep{height:1px;border:0;margin:0;background:var(--color-border-secondary)}
|
|
159
|
+
.muted-fg{color:var(--color-text-secondary)}
|
|
160
|
+
`;
|
|
161
|
+
// Basecoat reads shadcn's variable names. A project that imported its own
|
|
162
|
+
// shadcn `:root` block overrides these (its vars are injected last); a project
|
|
163
|
+
// with none still gets a themed Basecoat in both modes, because each name
|
|
164
|
+
// resolves to a mockpit token that already flips with the mode.
|
|
165
|
+
const BASECOAT_THEME_CSS = `
|
|
166
|
+
:root{--radius:var(--border-radius-md);--background:var(--color-background-primary);--foreground:var(--color-text-primary);--card:var(--color-background-primary);--card-foreground:var(--color-text-primary);--popover:var(--color-background-primary);--popover-foreground:var(--color-text-primary);--primary:var(--color-text-primary);--primary-foreground:var(--color-background-primary);--secondary:var(--color-background-secondary);--secondary-foreground:var(--color-text-primary);--muted:var(--color-background-secondary);--muted-foreground:var(--color-text-secondary);--accent:var(--color-background-secondary);--accent-foreground:var(--color-text-primary);--destructive:var(--color-text-danger);--border:var(--color-border-secondary);--input:var(--color-border-secondary);--ring:var(--color-border-info)}
|
|
167
|
+
`;
|
|
168
|
+
// Pinned to a major so fixes arrive without a server release. 1.x moved
|
|
169
|
+
// variants from `.btn-primary` classes to `data-variant`.
|
|
170
|
+
const BASECOAT_CDN = "https://cdn.jsdelivr.net/npm/basecoat-css@1/dist";
|
|
171
|
+
export const KITS = [
|
|
172
|
+
{
|
|
173
|
+
id: "builtin",
|
|
174
|
+
label: "Builtin",
|
|
175
|
+
summary: "shadcn-shaped components (CSS only) for projects with no design system",
|
|
176
|
+
classes: "btn · card · input · label · badge · tabs · dialog · table",
|
|
177
|
+
css: BUILTIN_CSS,
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: "issues",
|
|
181
|
+
label: "Issues",
|
|
182
|
+
summary: "issue / PR / CI status — trees, badges, chips, rollup bars",
|
|
183
|
+
classes: "card · tree · badge · chip · dot · bar",
|
|
184
|
+
css: ISSUES_CSS,
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "slides",
|
|
188
|
+
label: "Slides",
|
|
189
|
+
summary: "a stepped, cross-fading deck with prev/next controls and a counter",
|
|
190
|
+
classes: "deck · slide (+ injected controls)",
|
|
191
|
+
css: SLIDES_CSS,
|
|
192
|
+
js: SLIDES_JS,
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "basecoat",
|
|
196
|
+
label: "Basecoat",
|
|
197
|
+
summary: "shadcn's look as plain CSS classes (Basecoat UI from jsDelivr); themes from shadcn vars",
|
|
198
|
+
classes: 'btn card input label badge alert table tabs dialog kbd; data-variant="secondary|outline|ghost|destructive"',
|
|
199
|
+
css: BASECOAT_THEME_CSS,
|
|
200
|
+
href: `${BASECOAT_CDN}/basecoat.cdn.min.css`,
|
|
201
|
+
// Tabs (advertised above) switch only with Basecoat's script; the bundle
|
|
202
|
+
// also drives dropdown-menu, select and popover markup.
|
|
203
|
+
script: `${BASECOAT_CDN}/js/all.min.js`,
|
|
204
|
+
},
|
|
205
|
+
];
|
|
206
|
+
const KIT_BY_ID = new Map(KITS.map((k) => [k.id, k]));
|
|
207
|
+
export const isKnownKit = (id) => typeof id === "string" && KIT_BY_ID.has(id);
|
|
208
|
+
export const KIT_IDS = KITS.map((k) => k.id);
|
|
209
|
+
// Compact descriptor for discovery (no CSS/JS payload), followed by the
|
|
210
|
+
// project's own kits when given.
|
|
211
|
+
export const kitSummaries = (projectKits = []) => [
|
|
212
|
+
...KITS.map((k) => ({
|
|
213
|
+
id: k.id,
|
|
214
|
+
source: "bundled",
|
|
215
|
+
label: k.label,
|
|
216
|
+
summary: k.summary,
|
|
217
|
+
classes: k.classes,
|
|
218
|
+
...(k.href ? { href: k.href } : {}),
|
|
219
|
+
})),
|
|
220
|
+
...projectKits.map((k) => ({ ...k, source: "project" })),
|
|
221
|
+
];
|
|
222
|
+
// Ids a project kit may not take: they already mean something in design.kit.
|
|
223
|
+
export const RESERVED_KIT_IDS = ["tailwind", "none", ...KIT_IDS];
|
|
224
|
+
export const PROJECT_KIT_DOC_MAX = 1200;
|
|
225
|
+
const PROJECT_KIT_ID = /^[a-z0-9][a-z0-9-]{0,31}$/;
|
|
226
|
+
// One project kit as it arrives over the API. Its URLs become tags in the
|
|
227
|
+
// sandboxed document, so they must pass the same allowlist as the CSP.
|
|
228
|
+
export function checkProjectKit(raw) {
|
|
229
|
+
if (!raw || typeof raw !== "object")
|
|
230
|
+
return "a kit must be an object {id, href, doc}";
|
|
231
|
+
const k = raw;
|
|
232
|
+
if (typeof k.id !== "string" || !PROJECT_KIT_ID.test(k.id))
|
|
233
|
+
return "kit id must be lowercase letters, digits and dashes, at most 32";
|
|
234
|
+
if (RESERVED_KIT_IDS.includes(k.id))
|
|
235
|
+
return `kit id "${k.id}" is taken by a bundled kit`;
|
|
236
|
+
const href = checkCdnUrl(k.href, "kit url");
|
|
237
|
+
if ("error" in href)
|
|
238
|
+
return href.error;
|
|
239
|
+
let script;
|
|
240
|
+
if (k.script !== undefined && k.script !== null && k.script !== "") {
|
|
241
|
+
const checked = checkCdnUrl(k.script, "kit script");
|
|
242
|
+
if ("error" in checked)
|
|
243
|
+
return checked.error;
|
|
244
|
+
script = checked.url;
|
|
245
|
+
}
|
|
246
|
+
const doc = typeof k.doc === "string" ? k.doc.trim() : "";
|
|
247
|
+
if (!doc)
|
|
248
|
+
return "a kit needs a doc: the class cheat sheet the brief prints";
|
|
249
|
+
if (doc.length > PROJECT_KIT_DOC_MAX)
|
|
250
|
+
return `kit doc is ${doc.length} chars; keep it under ${PROJECT_KIT_DOC_MAX}`;
|
|
251
|
+
return { id: k.id, href: href.url, ...(script ? { script } : {}), doc };
|
|
252
|
+
}
|
|
253
|
+
// Resolve a list of kit ids to the CSS/JS to inject. Unknown ids are ignored;
|
|
254
|
+
// duplicates collapse; CORE ships once when any known kit is present.
|
|
255
|
+
export function kitAssets(ids) {
|
|
256
|
+
if (!ids || ids.length === 0)
|
|
257
|
+
return { css: "", js: "" };
|
|
258
|
+
const seen = new Set();
|
|
259
|
+
const chosen = [];
|
|
260
|
+
for (const id of ids) {
|
|
261
|
+
const kit = KIT_BY_ID.get(id);
|
|
262
|
+
if (kit && !seen.has(id)) {
|
|
263
|
+
seen.add(id);
|
|
264
|
+
chosen.push(kit);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (chosen.length === 0)
|
|
268
|
+
return { css: "", js: "" };
|
|
269
|
+
// A reference kit brings its own vocabulary; CORE's `.kbd` would override
|
|
270
|
+
// the library's.
|
|
271
|
+
let css = chosen.some((k) => !k.href) ? CORE_CSS : "";
|
|
272
|
+
let js = "";
|
|
273
|
+
for (const kit of chosen) {
|
|
274
|
+
css += kit.css;
|
|
275
|
+
if (kit.js)
|
|
276
|
+
js += kit.js;
|
|
277
|
+
}
|
|
278
|
+
return { css, js };
|
|
279
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ExplicitKnob, KnobConfig, Knobs, KnobValue, Mock, Post } from "./types.ts";
|
|
2
|
+
export type Checked<T> = {
|
|
3
|
+
ok: true;
|
|
4
|
+
value: T;
|
|
5
|
+
} | {
|
|
6
|
+
ok: false;
|
|
7
|
+
error: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function explicitKnob(config: KnobConfig): ExplicitKnob;
|
|
10
|
+
export declare function checkKnobConfig(path: string, raw: unknown): Checked<KnobConfig>;
|
|
11
|
+
export declare function checkKnobs(raw: unknown): Checked<Knobs>;
|
|
12
|
+
export declare function checkKnobValue(path: string, config: KnobConfig, value: unknown): Checked<KnobValue>;
|
|
13
|
+
export declare function knobFor(path: string, mock: Pick<Mock, "knobs">, posts: Pick<Post, "knobs">[]): KnobConfig | undefined;
|
|
14
|
+
export declare function checkKnobValues(raw: unknown, mock: Pick<Mock, "knobs">, posts: Pick<Post, "knobs">[], what: string): Checked<Record<string, KnobValue>>;
|
|
15
|
+
export declare function discreteChoices(config: KnobConfig): number | null;
|