mockpit 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/README.md +11 -23
  2. package/bin/initDesign.js +11 -16
  3. package/bin/mockpit.js +242 -52
  4. package/bin/poster.js +64 -7
  5. package/dist/mcp/server.js +48 -6
  6. package/dist/server/apiViews.d.ts +562 -557
  7. package/dist/server/apiViews.js +18 -9
  8. package/dist/server/app.js +723 -206
  9. package/dist/server/cdnInline.js +6 -4
  10. package/dist/server/codeWorker.js +2 -2
  11. package/dist/server/contrast.d.ts +4 -0
  12. package/dist/server/contrast.js +157 -0
  13. package/dist/server/decisionList.d.ts +4 -1
  14. package/dist/server/decisionList.js +43 -5
  15. package/dist/server/designGuide.d.ts +13 -6
  16. package/dist/server/designGuide.js +206 -41
  17. package/dist/server/events.d.ts +6 -0
  18. package/dist/server/feedbackBatch.d.ts +4 -0
  19. package/dist/server/feedbackBatch.js +19 -1
  20. package/dist/server/guidance.d.ts +1 -1
  21. package/dist/server/guidance.js +2 -2
  22. package/dist/server/kits.d.ts +2 -2
  23. package/dist/server/kits.js +54 -1
  24. package/dist/server/mcpHttp.d.ts +2 -0
  25. package/dist/server/mcpHttp.js +13 -1
  26. package/dist/server/mcpSpec.d.ts +27 -27
  27. package/dist/server/mcpSpec.js +78 -10
  28. package/dist/server/run.js +22 -1
  29. package/dist/server/runApi.d.ts +2 -2
  30. package/dist/server/runApi.js +9 -2
  31. package/dist/server/sqlStore.d.ts +68 -39
  32. package/dist/server/sqlStore.js +163 -26
  33. package/dist/server/surfacePage.d.ts +4 -2
  34. package/dist/server/surfacePage.js +80 -32
  35. package/dist/server/system.d.ts +234 -0
  36. package/dist/server/system.js +1508 -0
  37. package/dist/server/systemChecks.d.ts +15 -0
  38. package/dist/server/systemChecks.js +224 -0
  39. package/dist/server/systemSpecimen.d.ts +2 -0
  40. package/dist/server/systemSpecimen.js +49 -0
  41. package/dist/server/themes.d.ts +9 -7
  42. package/dist/server/themes.js +43 -90
  43. package/dist/server/types.d.ts +43 -24
  44. package/dist/server/types.js +3 -1
  45. package/guide/AGENT_SETUP.md +3 -1
  46. package/guide/packs/foundations.md +35 -0
  47. package/guide/topics/asks.md +2 -1
  48. package/guide/topics/feedback.md +5 -3
  49. package/guide/topics/html.md +2 -2
  50. package/guide/topics/http.md +3 -1
  51. package/guide/topics/system.md +157 -0
  52. package/package.json +12 -12
  53. package/skills/mockpit/SKILL.md +7 -4
  54. package/viewer/dist/index.html +9 -8
@@ -1,13 +1,9 @@
1
1
  import { collectAssetIds, collectAttachedIds, DEFAULT_PROJECT, DEFAULT_VARIANT, hashAssetId, HISTORY_LIMIT, htmlSurface, MAX_WORKSPACE_ASSET_BYTES, isDesignKind, newId, normalizeSurfaceIds, isLive, projectFromCwd, AREA_ORDER_SETTING, DECISION_EDITS_SETTING, reservedAgent, sameScreen, selectEvictions, slugify, stripNul, summarizeProjects, } from "./types.js";
2
2
  import { renameSlugRefs } from "./embeds.js";
3
+ import { CURRENT_CONTRACT, normalizeSystem, SYSTEM_LIMITS, systemFromDesignSettings, } from "./system.js";
3
4
  import { RULES_SETTING } from "./guidance.js";
4
5
  // The settings keyed `<prefix>:<project>`, which a project rename carries.
5
- const PROJECT_SETTINGS = [
6
- "design",
7
- AREA_ORDER_SETTING,
8
- RULES_SETTING,
9
- DECISION_EDITS_SETTING,
10
- ];
6
+ const PROJECT_SETTINGS = [AREA_ORDER_SETTING, RULES_SETTING, DECISION_EDITS_SETTING];
11
7
  // An optional free-text label: blank means none.
12
8
  const label = (v) => stripNul(v ?? "").trim() || null;
13
9
  function parseJson(raw, fallback) {
@@ -59,6 +55,9 @@ const surfaceHeads = (surfaces) => JSON.stringify(surfaces.map((s) => {
59
55
  kind: s.kind,
60
56
  ...(s.poster ? { poster: s.poster } : {}),
61
57
  ...(s.poster && s.posterMode ? { posterMode: s.posterMode } : {}),
58
+ ...(s.poster && s.posterMode && s.posterSystem !== undefined
59
+ ? { posterSystem: s.posterSystem }
60
+ : {}),
62
61
  ...(/<mockpit-embed|data-fill/i.test(s.html) ? { marked: true } : {}),
63
62
  };
64
63
  }));
@@ -350,6 +349,14 @@ export class SqlStore {
350
349
  createdAt TEXT NOT NULL, updatedAt TEXT NOT NULL,
351
350
  PRIMARY KEY (project, name)
352
351
  );
352
+ CREATE TABLE IF NOT EXISTS systems (
353
+ project TEXT PRIMARY KEY, rev INTEGER NOT NULL, json TEXT NOT NULL
354
+ );
355
+ CREATE TABLE IF NOT EXISTS system_revisions (
356
+ project TEXT NOT NULL, rev INTEGER NOT NULL, json TEXT NOT NULL, at TEXT NOT NULL,
357
+ sessionId TEXT, summary TEXT NOT NULL DEFAULT '',
358
+ PRIMARY KEY (project, rev)
359
+ );
353
360
  `);
354
361
  this.addMissing("sessions", {
355
362
  agentSeq: "INTEGER NOT NULL DEFAULT 0",
@@ -377,7 +384,12 @@ export class SqlStore {
377
384
  });
378
385
  this.migrateSituations();
379
386
  // Before the legacy lift, whose history rows are written with every column.
380
- this.addMissing("post_versions", { sessionId: "TEXT", amendedAt: "TEXT" });
387
+ this.addMissing("post_versions", {
388
+ sessionId: "TEXT",
389
+ amendedAt: "TEXT",
390
+ contract: "INTEGER",
391
+ system: "INTEGER",
392
+ });
381
393
  if (legacy)
382
394
  this.migrateLegacy();
383
395
  // After the legacy lift, which rebuilds posts from POST_COLUMNS alone.
@@ -397,6 +409,8 @@ export class SqlStore {
397
409
  archiveReason: "TEXT",
398
410
  stayedVersion: "INTEGER",
399
411
  archivedAt: "TEXT",
412
+ curContract: "INTEGER",
413
+ curSystem: "INTEGER",
400
414
  });
401
415
  // Before the pin, accepted meant "its latest": freeze that as the version in use.
402
416
  if (!pinned) {
@@ -410,6 +424,7 @@ export class SqlStore {
410
424
  }
411
425
  if (this.settingSync("sketchKind") !== "1")
412
426
  this.migrateToSketches();
427
+ this.migrateSystems();
413
428
  if (this.settingSync("contractTerms") !== "1")
414
429
  this.migrateToContractTerms();
415
430
  if (this.settingSync("designWords") !== "1")
@@ -753,7 +768,7 @@ export class SqlStore {
753
768
  }
754
769
  // --- rows ---
755
770
  insertVersion(postId, v) {
756
- this.sql.exec("INSERT OR REPLACE INTO post_versions (postId, version, title, surfaces, head, at, fromVersion, prompt, author, sessionId, amendedAt) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", postId, v.version, v.title, JSON.stringify(v.surfaces ?? []), surfaceHeads(v.surfaces ?? []), v.at, v.from ?? null, v.prompt ?? null, v.author ?? null, v.publishedBy ?? null, v.amendedAt ?? null);
771
+ this.sql.exec("INSERT OR REPLACE INTO post_versions (postId, version, title, surfaces, head, at, fromVersion, prompt, author, sessionId, amendedAt, contract, system) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", postId, v.version, v.title, JSON.stringify(v.surfaces ?? []), surfaceHeads(v.surfaces ?? []), v.at, v.from ?? null, v.prompt ?? null, v.author ?? null, v.publishedBy ?? null, v.amendedAt ?? null, v.contract ?? null, v.system ?? null);
757
772
  }
758
773
  rowToVersion(r) {
759
774
  return {
@@ -767,6 +782,8 @@ export class SqlStore {
767
782
  ...(r.author == null ? {} : { author: r.author }),
768
783
  ...(r.sessionId == null ? {} : { publishedBy: r.sessionId }),
769
784
  ...(r.amendedAt == null ? {} : { amendedAt: r.amendedAt }),
785
+ ...(r.contract == null ? {} : { contract: r.contract }),
786
+ ...(r.system == null ? {} : { system: r.system }),
770
787
  };
771
788
  }
772
789
  historiesFor(ids, weigh) {
@@ -865,6 +882,8 @@ export class SqlStore {
865
882
  ...(r.curAuthor == null ? {} : { author: r.curAuthor }),
866
883
  ...(r.curSession == null ? {} : { publishedBy: r.curSession }),
867
884
  ...(r.amendedAt == null ? {} : { amendedAt: r.amendedAt }),
885
+ ...(r.curContract == null ? {} : { contract: r.curContract }),
886
+ ...(r.curSystem == null ? {} : { system: r.curSystem }),
868
887
  ...(r.rewrites ? { rewrites: r.rewrites } : {}),
869
888
  };
870
889
  }
@@ -999,8 +1018,6 @@ export class SqlStore {
999
1018
  return this.settingSync(key);
1000
1019
  }
1001
1020
  async setSetting(key, value) {
1002
- if (key.startsWith("design:"))
1003
- this.invalidateAssetRefs();
1004
1021
  this.sql.exec("INSERT INTO settings (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", stripNul(key), stripNul(value));
1005
1022
  }
1006
1023
  // --- projects / designs ---
@@ -1246,12 +1263,98 @@ export class SqlStore {
1246
1263
  continue;
1247
1264
  this.sql.exec("INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)", `${prefix}:${target}`, value);
1248
1265
  this.sql.exec("DELETE FROM settings WHERE key = ?", `${prefix}:${from}`);
1249
- if (prefix === "design")
1250
- this.invalidateAssetRefs();
1266
+ }
1267
+ if (this.sql.exec("SELECT 1 FROM systems WHERE project = ?", from).toArray().length) {
1268
+ this.sql.exec("DELETE FROM systems WHERE project = ?", target);
1269
+ this.sql.exec("DELETE FROM system_revisions WHERE project = ?", target);
1270
+ for (const table of ["systems", "system_revisions"]) {
1271
+ for (const r of this.sql
1272
+ .exec(`SELECT rowid AS id, json FROM ${table} WHERE project = ?`, from)
1273
+ .toArray()) {
1274
+ const moved = { ...parseJson(r.json, {}), project: target };
1275
+ this.sql.exec(`UPDATE ${table} SET project = ?, json = ? WHERE rowid = ?`, target, JSON.stringify(moved), r.id);
1276
+ }
1277
+ }
1251
1278
  }
1252
1279
  return true;
1253
1280
  });
1254
1281
  }
1282
+ // --- systems ---
1283
+ async getSystem(project) {
1284
+ const rows = this.sql.exec("SELECT json FROM systems WHERE project = ?", project).toArray();
1285
+ return rows.length ? normalizeSystem(project, parseJson(rows[0].json, null)) : null;
1286
+ }
1287
+ async putSystem(system, meta) {
1288
+ this.atomic(() => this.writeSystem(system, meta));
1289
+ this.invalidateAssetRefs();
1290
+ return system;
1291
+ }
1292
+ async commitSystemDecision(input) {
1293
+ const tell = input.tell;
1294
+ const comment = this.atomic(() => {
1295
+ this.writeSystem(input.system, { sessionId: null, summary: input.summary });
1296
+ return tell
1297
+ ? this.insertComment({
1298
+ sessionId: tell.sessionId,
1299
+ author: "user",
1300
+ text: "",
1301
+ kind: "reply",
1302
+ payload: tell.payload,
1303
+ })
1304
+ : null;
1305
+ });
1306
+ this.invalidateAssetRefs();
1307
+ return comment;
1308
+ }
1309
+ // Unwrapped, so a reply's transaction can carry it.
1310
+ writeSystem(system, meta) {
1311
+ const json = JSON.stringify(system);
1312
+ const at = new Date().toISOString();
1313
+ {
1314
+ this.sql.exec("INSERT INTO systems (project, rev, json) VALUES (?, ?, ?) ON CONFLICT(project) DO UPDATE SET rev = excluded.rev, json = excluded.json", system.project, system.rev, json);
1315
+ this.sql.exec("INSERT OR REPLACE INTO system_revisions (project, rev, json, at, sessionId, summary) VALUES (?, ?, ?, ?, ?, ?)", system.project, system.rev, json, at, meta.sessionId, stripNul(meta.summary).slice(0, SYSTEM_LIMITS.summary));
1316
+ this.sql.exec("DELETE FROM system_revisions WHERE project = ? AND rev <= ?", system.project, system.rev - SYSTEM_LIMITS.revisions);
1317
+ }
1318
+ }
1319
+ async listSystemRevisions(project) {
1320
+ return this.sql
1321
+ .exec("SELECT rev, at, sessionId, summary FROM system_revisions WHERE project = ? ORDER BY rev DESC", project)
1322
+ .toArray()
1323
+ .map((r) => ({
1324
+ rev: r.rev,
1325
+ at: r.at,
1326
+ sessionId: r.sessionId ?? null,
1327
+ summary: r.summary,
1328
+ }));
1329
+ }
1330
+ async getSystemRevision(project, rev) {
1331
+ const rows = this.sql
1332
+ .exec("SELECT json FROM system_revisions WHERE project = ? AND rev = ?", project, rev)
1333
+ .toArray();
1334
+ return rows.length ? normalizeSystem(project, parseJson(rows[0].json, null)) : null;
1335
+ }
1336
+ // Each project's old design settings become its system, with the repo's
1337
+ // tokens derived, and the setting goes.
1338
+ migrateSystems() {
1339
+ const rows = this.sql
1340
+ .exec("SELECT key, value FROM settings WHERE key LIKE 'design:%'")
1341
+ .toArray();
1342
+ if (rows.length === 0)
1343
+ return;
1344
+ const at = new Date().toISOString();
1345
+ for (const r of rows) {
1346
+ const project = r.key.slice("design:".length);
1347
+ const have = this.sql.exec("SELECT 1 FROM systems WHERE project = ?", project).toArray();
1348
+ if (have.length === 0) {
1349
+ const system = { ...systemFromDesignSettings(project, parseJson(r.value, null)), rev: 1 };
1350
+ const json = JSON.stringify(system);
1351
+ this.sql.exec("INSERT INTO systems (project, rev, json) VALUES (?, 1, ?)", project, json);
1352
+ this.sql.exec("INSERT INTO system_revisions (project, rev, json, at, sessionId, summary) VALUES (?, 1, ?, ?, NULL, ?)", project, json, at, "from what the project stored before");
1353
+ }
1354
+ this.sql.exec("DELETE FROM settings WHERE key = ?", r.key);
1355
+ }
1356
+ this.invalidateAssetRefs();
1357
+ }
1255
1358
  // --- flows ---
1256
1359
  flowsOf(project) {
1257
1360
  return this.sql
@@ -1505,8 +1608,10 @@ export class SqlStore {
1505
1608
  ...(input.prompt === undefined ? {} : { prompt: stripNul(input.prompt) }),
1506
1609
  ...(input.author === undefined ? {} : { author: stripNul(input.author) }),
1507
1610
  publishedBy: input.publishedBy ?? input.sessionId,
1611
+ contract: input.contract ?? CURRENT_CONTRACT,
1612
+ ...(input.system === undefined ? {} : { system: input.system }),
1508
1613
  };
1509
- this.writingPosts(post.design, [post.id], () => this.sql.exec("INSERT INTO posts (id, sessionId, designId, state, variant, status, title, surfaces, head, knobs, slots, createdAt, updatedAt, version, curFrom, curPrompt, curAuthor, description, forkedFrom, copiedFrom, curSession) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?)", post.id, post.sessionId, post.design, post.state, post.variant, post.title, JSON.stringify(post.surfaces), surfaceHeads(post.surfaces), post.knobs ? JSON.stringify(post.knobs) : null, JSON.stringify(post.embeds), post.createdAt, post.updatedAt, post.from ?? null, post.prompt ?? null, post.author ?? null, post.description ?? null, post.forkedFrom ? JSON.stringify(post.forkedFrom) : null, post.copiedFrom ? JSON.stringify(post.copiedFrom) : null, post.publishedBy ?? null));
1614
+ this.writingPosts(post.design, [post.id], () => this.sql.exec("INSERT INTO posts (id, sessionId, designId, state, variant, status, title, surfaces, head, knobs, slots, createdAt, updatedAt, version, curFrom, curPrompt, curAuthor, description, forkedFrom, copiedFrom, curSession, curContract, curSystem) VALUES (?, ?, ?, ?, ?, 'open', ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?)", post.id, post.sessionId, post.design, post.state, post.variant, post.title, JSON.stringify(post.surfaces), surfaceHeads(post.surfaces), post.knobs ? JSON.stringify(post.knobs) : null, JSON.stringify(post.embeds), post.createdAt, post.updatedAt, post.from ?? null, post.prompt ?? null, post.author ?? null, post.description ?? null, post.forkedFrom ? JSON.stringify(post.forkedFrom) : null, post.copiedFrom ? JSON.stringify(post.copiedFrom) : null, post.publishedBy ?? null, post.contract ?? null, post.system ?? null));
1510
1615
  this.touch(input.sessionId);
1511
1616
  this.touchDesign(post.design, now);
1512
1617
  this.addAssetRefs(post.surfaces);
@@ -1529,6 +1634,8 @@ export class SqlStore {
1529
1634
  ...(post.author === undefined ? {} : { author: post.author }),
1530
1635
  ...(post.publishedBy === undefined ? {} : { publishedBy: post.publishedBy }),
1531
1636
  ...(post.amendedAt === undefined ? {} : { amendedAt: post.amendedAt }),
1637
+ ...(post.contract === undefined ? {} : { contract: post.contract }),
1638
+ ...(post.system === undefined ? {} : { system: post.system }),
1532
1639
  };
1533
1640
  // The version in use is kept past the limit: export and embeds resolve to it.
1534
1641
  const pin = post.acceptedVersion ?? -1;
@@ -1551,8 +1658,10 @@ export class SqlStore {
1551
1658
  const prompt = stripNul(patch.prompt ?? "");
1552
1659
  const author = patch.author === undefined ? null : stripNul(patch.author);
1553
1660
  const publishedBy = patch.publishedBy ?? null;
1661
+ const contract = patch.contract ?? CURRENT_CONTRACT;
1662
+ const system = patch.system ?? null;
1554
1663
  const wrote = this.writingPosts(post.design, [id], () => {
1555
- this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, head = ?, knobs = ?, description = ?, slots = ?, state = ?, updatedAt = ?, version = ?, curFrom = ?, curPrompt = ?, curAuthor = ?, curSession = ?, amendedAt = NULL WHERE id = ? AND version = ?", title, JSON.stringify(surfaces), surfaceHeads(surfaces), knobs ? JSON.stringify(knobs) : null, description ?? null, JSON.stringify(embeds), state, updatedAt, version, from, prompt, author, publishedBy, id, post.version);
1664
+ this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, head = ?, knobs = ?, description = ?, slots = ?, state = ?, updatedAt = ?, version = ?, curFrom = ?, curPrompt = ?, curAuthor = ?, curSession = ?, amendedAt = NULL, curContract = ?, curSystem = ? WHERE id = ? AND version = ?", title, JSON.stringify(surfaces), surfaceHeads(surfaces), knobs ? JSON.stringify(knobs) : null, description ?? null, JSON.stringify(embeds), state, updatedAt, version, from, prompt, author, publishedBy, contract, system, id, post.version);
1556
1665
  if (this.sql.exec("SELECT changes() AS n").one().n === 0)
1557
1666
  return false;
1558
1667
  this.insertVersion(id, archived);
@@ -1564,9 +1673,11 @@ export class SqlStore {
1564
1673
  this.touchDesign(post.design, updatedAt);
1565
1674
  if (patch.surfaces !== undefined)
1566
1675
  this.addAssetRefs(surfaces);
1567
- const { knobs: _knobs, description: _description, author: _author, publishedBy: _by, amendedAt: _amended, ...rest } = post;
1676
+ const { knobs: _knobs, description: _description, author: _author, publishedBy: _by, amendedAt: _amended, contract: _contract, system: _system, ...rest } = post;
1568
1677
  return {
1569
1678
  ...rest,
1679
+ contract,
1680
+ ...(system === null ? {} : { system }),
1570
1681
  title,
1571
1682
  surfaces,
1572
1683
  ...(knobs ? { knobs } : {}),
@@ -1601,8 +1712,10 @@ export class SqlStore {
1601
1712
  const author = patch.author === undefined ? post.author : stripNul(patch.author);
1602
1713
  const amendedAt = new Date().toISOString();
1603
1714
  const rewrites = (post.rewrites ?? 0) + 1;
1715
+ const contract = patch.contract ?? CURRENT_CONTRACT;
1716
+ const system = patch.system ?? post.system;
1604
1717
  const wrote = this.writingPosts(post.design, [id], () => {
1605
- this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, head = ?, knobs = ?, description = ?, slots = ?, curFrom = ?, curPrompt = ?, curAuthor = ?, amendedAt = ?, rewrites = ? WHERE id = ? AND version = ?", title, JSON.stringify(surfaces), surfaceHeads(surfaces), knobs ? JSON.stringify(knobs) : null, description ?? null, JSON.stringify(embeds), from ?? null, prompt ?? null, author ?? null, amendedAt, rewrites, id, version);
1718
+ this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, head = ?, knobs = ?, description = ?, slots = ?, curFrom = ?, curPrompt = ?, curAuthor = ?, amendedAt = ?, rewrites = ?, curContract = ?, curSystem = ? WHERE id = ? AND version = ?", title, JSON.stringify(surfaces), surfaceHeads(surfaces), knobs ? JSON.stringify(knobs) : null, description ?? null, JSON.stringify(embeds), from ?? null, prompt ?? null, author ?? null, amendedAt, rewrites, contract, system ?? null, id, version);
1606
1719
  return this.sql.exec("SELECT changes() AS n").one().n > 0;
1607
1720
  });
1608
1721
  if (!wrote)
@@ -1624,17 +1737,33 @@ export class SqlStore {
1624
1737
  ...(author === undefined ? {} : { author }),
1625
1738
  amendedAt,
1626
1739
  rewrites,
1740
+ contract,
1741
+ ...(system === undefined ? {} : { system }),
1627
1742
  };
1628
1743
  }
1629
- async setPosterOnce(id, version, poster, mode) {
1744
+ async setPosterOnce(id, version, poster, mode, system) {
1630
1745
  const post = await this.getPost(id);
1631
1746
  if (!post || post.version !== version)
1632
1747
  return false;
1633
1748
  const at = post.surfaces.findIndex((s) => s.kind === "html");
1634
1749
  const page = post.surfaces[at];
1635
- if (!page || page.kind !== "html" || page.poster)
1750
+ if (!page || page.kind !== "html")
1751
+ return false;
1752
+ // A still mockpit took under another system revision no longer shows, so a new
1753
+ // one may take its place; an agent's own, or a current one, stays.
1754
+ const stale = page.posterMode !== undefined &&
1755
+ system !== undefined &&
1756
+ (page.posterSystem === undefined ? system > 1 : page.posterSystem !== system);
1757
+ if (page.poster && !stale)
1636
1758
  return false;
1637
- const surfaces = post.surfaces.map((s, i) => i === at ? { ...page, poster, ...(mode ? { posterMode: mode } : {}) } : s);
1759
+ const surfaces = post.surfaces.map((s, i) => i === at
1760
+ ? {
1761
+ ...page,
1762
+ poster,
1763
+ ...(mode ? { posterMode: mode } : {}),
1764
+ ...(mode && system !== undefined ? { posterSystem: system } : {}),
1765
+ }
1766
+ : s);
1638
1767
  const wrote = this.writingPosts(post.design, [id], () => {
1639
1768
  this.sql.exec("UPDATE posts SET surfaces = ?, head = ? WHERE id = ? AND version = ?", JSON.stringify(surfaces), surfaceHeads(surfaces), id, version);
1640
1769
  return this.sql.exec("SELECT changes() AS n").one().n > 0;
@@ -1664,12 +1793,12 @@ export class SqlStore {
1664
1793
  const rewrites = (post.rewrites ?? 0) + 1;
1665
1794
  const hidden = (post.hiddenVersions ?? []).filter((v) => v !== version);
1666
1795
  this.writingPosts(post.design, [id], () => this.atomic(() => {
1667
- this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, head = ?, slots = ?, updatedAt = ?, version = ?, curFrom = ?, curPrompt = ?, curAuthor = ?, curSession = ?, amendedAt = ?, rewrites = ?, hiddenVersions = ? WHERE id = ? AND version = ?", previous.title, JSON.stringify(previous.surfaces), surfaceHeads(previous.surfaces), JSON.stringify(embeds), previous.at, previous.version, previous.from ?? null, previous.prompt ?? null, previous.author ?? null, previous.publishedBy ?? null, previous.amendedAt ?? null, rewrites, hidden.length ? JSON.stringify(hidden) : null, id, version);
1796
+ this.sql.exec("UPDATE posts SET title = ?, surfaces = ?, head = ?, slots = ?, updatedAt = ?, version = ?, curFrom = ?, curPrompt = ?, curAuthor = ?, curSession = ?, amendedAt = ?, rewrites = ?, hiddenVersions = ?, curContract = ?, curSystem = ? WHERE id = ? AND version = ?", previous.title, JSON.stringify(previous.surfaces), surfaceHeads(previous.surfaces), JSON.stringify(embeds), previous.at, previous.version, previous.from ?? null, previous.prompt ?? null, previous.author ?? null, previous.publishedBy ?? null, previous.amendedAt ?? null, rewrites, hidden.length ? JSON.stringify(hidden) : null, previous.contract ?? null, previous.system ?? null, id, version);
1668
1797
  this.sql.exec("DELETE FROM post_versions WHERE postId = ? AND version = ?", id, previous.version);
1669
1798
  }));
1670
1799
  this.touchDesign(post.design, now);
1671
1800
  this.invalidateAssetRefs();
1672
- const { from: _from, prompt: _prompt, author: _author, publishedBy: _by, amendedAt: _amended, hiddenVersions: _hidden, ...rest } = post;
1801
+ const { from: _from, prompt: _prompt, author: _author, publishedBy: _by, amendedAt: _amended, hiddenVersions: _hidden, contract: _contract, system: _system, ...rest } = post;
1673
1802
  return {
1674
1803
  post: {
1675
1804
  ...rest,
@@ -1685,6 +1814,8 @@ export class SqlStore {
1685
1814
  ...(previous.publishedBy === undefined ? {} : { publishedBy: previous.publishedBy }),
1686
1815
  ...(previous.amendedAt === undefined ? {} : { amendedAt: previous.amendedAt }),
1687
1816
  ...(hidden.length ? { hiddenVersions: hidden } : {}),
1817
+ ...(previous.contract === undefined ? {} : { contract: previous.contract }),
1818
+ ...(previous.system === undefined ? {} : { system: previous.system }),
1688
1819
  rewrites,
1689
1820
  },
1690
1821
  };
@@ -1809,6 +1940,8 @@ export class SqlStore {
1809
1940
  async listComments(query) {
1810
1941
  const clauses = [];
1811
1942
  const params = [];
1943
+ if (query.noDesign)
1944
+ clauses.push("designId IS NULL");
1812
1945
  if (query.sessionId !== undefined) {
1813
1946
  clauses.push("sessionId = ?");
1814
1947
  params.push(query.sessionId);
@@ -1916,6 +2049,12 @@ export class SqlStore {
1916
2049
  postVersion: input.payload.version,
1917
2050
  payload: input.payload,
1918
2051
  });
2052
+ if (input.system) {
2053
+ this.writeSystem(input.system.next, {
2054
+ sessionId: input.sessionId,
2055
+ summary: input.system.summary,
2056
+ });
2057
+ }
1919
2058
  this.sql.exec(input.keepDraft
1920
2059
  ? "UPDATE designs SET asks = ? WHERE id = ?"
1921
2060
  : "UPDATE designs SET asks = ?, draft = NULL WHERE id = ?", JSON.stringify(input.asks), input.designId);
@@ -1971,12 +2110,10 @@ export class SqlStore {
1971
2110
  .toArray()) {
1972
2111
  collectAttachedIds(parseJson(r.payload, null), out);
1973
2112
  }
1974
- // An installed icon set is referenced by its project's design, not by a
2113
+ // An installed icon set is referenced by its project's system, not by a
1975
2114
  // surface; evicting it would blank that project's icons.
1976
- for (const r of this.sql
1977
- .exec("SELECT value FROM settings WHERE key LIKE 'design:%'")
1978
- .toArray()) {
1979
- const sets = parseJson(r.value, null)?.iconSets;
2115
+ for (const r of this.sql.exec("SELECT json FROM systems").toArray()) {
2116
+ const sets = parseJson(r.json, null)?.iconSets;
1980
2117
  for (const ref of Array.isArray(sets) ? sets : []) {
1981
2118
  if (typeof ref?.assetId === "string")
1982
2119
  out.add(ref.assetId);
@@ -1,5 +1,5 @@
1
1
  import { type IconResolver } from "./icons.ts";
2
- import { type DesignSystem } from "./types.ts";
2
+ import { type System } from "./system.ts";
3
3
  import { type Mode, type Theme } from "./themes.ts";
4
4
  export declare const BRIDGE_JS = "\nwindow.sendPrompt = function (text) {\n parent.postMessage({ __mockpit: true, type: 'send-prompt', text: String(text) }, '*');\n};\nwindow.openLink = function (url) {\n parent.postMessage({ __mockpit: true, type: 'open-link', url: String(url) }, '*');\n};\nwindow.copyToClipboard = function (text) {\n parent.postMessage({ __mockpit: true, type: 'copy', text: String(text) }, '*');\n};\ndocument.addEventListener('click', function (e) {\n var a = e.target && e.target.closest ? e.target.closest('a[href]') : null;\n if (a && /^https?:/.test(a.href)) { e.preventDefault(); window.openLink(a.href); }\n});\n// Cmd+Option+Up/Down switches sessions in the sidebar, but keydowns fire in\n// whichever document holds focus \u2014 once the user clicks into a surface, this\n// sandboxed iframe swallows them. Forward just that combo to the host.\ndocument.addEventListener('keydown', function (e) {\n if (!e.metaKey || !e.altKey || e.ctrlKey || e.shiftKey) return;\n if (e.key !== 'ArrowUp' && e.key !== 'ArrowDown') return;\n e.preventDefault();\n parent.postMessage({ __mockpit: true, type: 'switch-session', key: e.key }, '*');\n});\n// Report content height to the parent so it can size this iframe, while\n// breaking a feedback loop that can peg a CPU core.\n//\n// The loop: the parent sets the iframe's height to whatever we report, but some\n// content's height *inverts* with the frame's height \u2014 a scrollbar that appears\n// at height A reflows the content to height B, then disappears at B and reflows\n// back to A (or any 100vh / percentage-derived layout). The ResizeObserver then\n// fires on every flip, so reported heights alternate A, B, A, B... forever. With\n// a cheap surface that's a brief blip; with a heavy one (a big syntax-highlighted\n// diff/markdown surface) each relayout is expensive and the tab sits at 100% CPU\n// until the surface unmounts.\n//\n// A plain h !== __lastH guard can't stop this: in a 2-cycle every value differs\n// from the one immediately before it. So we remember the previous height too and\n// defer a return to it *if it recurs faster than a human could* (< 250ms) \u2014 that's\n// the runaway. The deferred pass keeps one trailing re-measure and reports the\n// taller height in the pair, so an ordinary font/image reflow can't leave the\n// frame permanently clipped.\nvar __lastH = 0;\nvar __prevH = 0;\nvar __lastT = 0;\nvar __seenH = 0;\nvar __trailTimer = 0;\nvar __trailH = 0;\nvar __FLIP_MS = 250;\nvar __TRAIL_MS = 350;\nfunction __now() {\n return typeof performance !== 'undefined' && performance.now ? performance.now() : Date.now();\n}\nfunction __measureHeight() {\n return document.body\n ? document.body.scrollHeight\n : document.documentElement.scrollHeight;\n}\nfunction __postHeight(h, t) {\n __prevH = __lastH;\n __lastH = h;\n __lastT = t;\n parent.postMessage({ __mockpit: true, type: 'resize', height: h }, '*');\n}\nfunction __clearTrailing() {\n if (__trailTimer && typeof clearTimeout !== 'undefined') clearTimeout(__trailTimer);\n __trailTimer = 0;\n __trailH = 0;\n}\nfunction __flushTrailing() {\n __trailTimer = 0;\n var measured = __measureHeight();\n if (measured > 0) __seenH = measured;\n var target = Math.max(__trailH || 0, measured || 0);\n __trailH = 0;\n if (target <= 0 || target === __lastH) return;\n __postHeight(target, __now());\n}\nfunction __scheduleTrailing(h, reset) {\n __trailH = Math.max(__trailH || 0, h || 0, __lastH || 0);\n if (__trailTimer) {\n if (!reset) return;\n if (typeof clearTimeout !== 'undefined') clearTimeout(__trailTimer);\n }\n __trailTimer = setTimeout(__flushTrailing, __TRAIL_MS);\n}\nfunction __report() {\n var h = __measureHeight();\n if (h <= 0) return; // no content yet\n var changed = h !== __seenH;\n __seenH = h;\n if (h === __lastH) return; // unchanged\n var t = __now();\n if (h === __prevH && (__trailTimer || t - __lastT < __FLIP_MS)) {\n __scheduleTrailing(h, true); // rapid A<->B flip: defer one settled report\n return;\n }\n __clearTrailing();\n __postHeight(h, t);\n}\nif (document.readyState === 'complete') __report();\nelse window.addEventListener('load', function () { requestAnimationFrame(__report); });\nsetTimeout(__report, 60);\nsetTimeout(__report, 350);\nsetTimeout(__report, 1500);\nsetTimeout(__report, 3000);\nsetTimeout(__report, 6000);\nsetTimeout(__report, 10000);\nif (document.fonts && document.fonts.ready && document.fonts.ready.then) {\n document.fonts.ready.then(function () { __report(); });\n}\nif (window.ResizeObserver) {\n window.__ssRO = new ResizeObserver(__report);\n window.__ssRO.observe(document.documentElement);\n if (document.body) window.__ssRO.observe(document.body);\n}\n";
5
5
  export declare const FREEZE_JS = "\n(function () {\n if (location.hash !== '#small' || !document.getAnimations) return;\n var QUIET_MS = 800, CAP_MS = 10000, LOAD_CAP_MS = 15000, MAX_STEP_MS = 100, RUN_MAX_MS = 30000;\n var HELD_MAX = 32;\n var raf = window.requestAnimationFrame.bind(window);\n var caf = window.cancelAnimationFrame.bind(window);\n var asked = new Map(), nextId = 0;\n var frozen = false, loaded = false;\n var run = 0, last = 0, settleFrom = 0, active = 0, runUntil = RUN_MAX_MS;\n var paused = [], sweepQueued = false;\n window.addEventListener('load', function () { loaded = true; settleFrom = run; active = run; });\n\n function fire(id, t) {\n var entry = asked.get(id);\n if (!entry) return;\n entry.native = 0;\n if (frozen) return;\n asked.delete(id);\n entry.cb(t);\n }\n function queue(id, entry) {\n entry.native = raf(function (t) { fire(id, t); });\n }\n window.requestAnimationFrame = function (cb) {\n if (frozen && run < runUntil) thaw();\n var id = ++nextId, entry = { cb: cb, native: 0 };\n if (frozen && asked.size >= HELD_MAX) return id;\n asked.set(id, entry);\n active = run;\n if (!frozen) queue(id, entry);\n return id;\n };\n window.cancelAnimationFrame = function (id) {\n var entry = asked.get(id);\n if (!entry) return;\n if (entry.native) caf(entry.native);\n asked.delete(id);\n };\n if (window.webkitRequestAnimationFrame) {\n window.webkitRequestAnimationFrame = window.requestAnimationFrame;\n window.webkitCancelAnimationFrame = window.cancelAnimationFrame;\n }\n\n function own(a) { return a.timeline === document.timeline && a.effect; }\n function ends(a) { return isFinite(a.effect.getComputedTiming().endTime); }\n function stillDrawing() {\n return document.getAnimations().some(function (a) {\n return own(a) && a.playState === 'running' && ends(a);\n });\n }\n function sweep() {\n sweepQueued = false;\n if (!frozen) return;\n document.getAnimations().forEach(function (a) {\n if (!own(a)) return;\n try {\n if (ends(a)) a.finish();\n else if (a.playState !== 'paused') { a.pause(); a.currentTime = 0; paused.push(a); }\n } catch (e) {}\n });\n Array.prototype.forEach.call(document.querySelectorAll('svg'), function (svg) {\n if (svg.pauseAnimations) svg.pauseAnimations();\n });\n }\n function queueSweep() {\n if (frozen && !sweepQueued) { sweepQueued = true; setTimeout(sweep, 0); }\n }\n function freeze() { frozen = true; sweep(); }\n function thaw() {\n frozen = false;\n active = settleFrom = run;\n paused.splice(0).forEach(function (a) { try { a.play(); } catch (e) {} });\n Array.prototype.forEach.call(document.querySelectorAll('svg'), function (svg) {\n if (svg.unpauseAnimations) svg.unpauseAnimations();\n });\n asked.forEach(function (entry, id) { if (!entry.native) queue(id, entry); });\n last = 0;\n raf(tick);\n }\n function settled() {\n return run - active >= QUIET_MS && !stillDrawing();\n }\n function tick(t) {\n if (frozen) return;\n run += last ? Math.min(t - last, MAX_STEP_MS) : 0;\n last = t;\n if (loaded ? settled() || run - settleFrom >= CAP_MS : run >= LOAD_CAP_MS) freeze();\n else raf(tick);\n }\n new MutationObserver(function () {\n active = run;\n queueSweep();\n }).observe(document, { subtree: true, childList: true, attributes: true, characterData: true });\n document.addEventListener('animationstart', queueSweep, true);\n document.addEventListener('transitionstart', queueSweep, true);\n window.addEventListener('message', function (e) {\n var d = e.data;\n if (e.source !== window.parent || !d || d.__mockpit !== true || d.type !== 'knobs') return;\n runUntil = run + RUN_MAX_MS;\n if (frozen) thaw();\n });\n raf(tick);\n})();\n";
@@ -42,7 +42,9 @@ export declare function renderHtmlPage(doc: {
42
42
  theme?: Theme | string;
43
43
  mode?: Mode;
44
44
  kits?: string[];
45
- designSystem?: DesignSystem | null;
45
+ system?: System | null;
46
+ palette?: string;
47
+ contract?: number;
46
48
  version?: number;
47
49
  knobs?: Record<string, unknown>;
48
50
  resolveIcon?: IconResolver;
@@ -2,6 +2,7 @@ import { scanFills, slotCss } from "./fills.js";
2
2
  import { CDN_ALLOWLIST, checkCdnUrl } from "./cdn.js";
3
3
  import { CORE_CSS, KITS, kitAssets } from "./kits.js";
4
4
  import { expandIcons } from "./icons.js";
5
+ import { CURRENT_CONTRACT, systemCss } from "./system.js";
5
6
  import { PARAGRAPH_TEXT_MAX, PARAGRAPHS_MAX } from "./types.js";
6
7
  import { schemeCss, themeById, tokenThemeCss, viewerThemeCss, } from "./themes.js";
7
8
  // The kit's two custom SVG accent ramps (teal, coral) aren't in the theme
@@ -1126,7 +1127,7 @@ export function knobAttributes(knobs) {
1126
1127
  const PART_HIGHLIGHT_CSS = `.mockpit-part-hl{outline:2px solid #8b7bff !important;outline-offset:2px}html.mockpit-try [data-goto]{outline:2px dashed #3fb27f !important;outline-offset:3px;cursor:pointer}`;
1127
1128
  // Tailwind's browser build, pinned to a major (jsdelivr is already on the CDN
1128
1129
  // allowlist, so this needs no CSP widening). Injected only when a project's
1129
- // design settings say `kit: "tailwind"`, i.e. the repo itself is a Tailwind
1130
+ // system says `kit: "tailwind"`, i.e. the repo itself is a Tailwind
1130
1131
  // repo — so the agent writes the same classes it writes in the codebase.
1131
1132
  const TAILWIND_CDN = "https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4";
1132
1133
  // Sizing for the `<svg class="icon">` that icons.ts inlines for every
@@ -1163,6 +1164,14 @@ const PARAGRAPHS_TAG = inlineScript(PARAGRAPHS_JS);
1163
1164
  // The base html-surface stylesheet: static design tokens + the surface kit.
1164
1165
  const BASE_CSS_TAG = inlineStyle(`${TOKENS_CSS}${KIT_CSS}${PART_HIGHLIGHT_CSS}${ICON_CSS}`);
1165
1166
  const KIT_CORE_TAG = inlineStyle(CORE_CSS);
1167
+ const LAYER_ORDER_TAG = inlineStyle("@layer mockpit, kit, system;");
1168
+ // Tailwind's browser build declares `theme, base, components, utilities` after
1169
+ // this tag, so layers it names first would come last and preflight (`base`)
1170
+ // would undo the sheet. Naming them here keeps preflight under the sheet and
1171
+ // utilities over it, as in the repo.
1172
+ const LAYER_ORDER_TAILWIND_TAG = inlineStyle("@layer theme, base, mockpit, kit, system, components, utilities;");
1173
+ const BASE_CSS_LAYERED_TAG = inlineStyle(`@layer mockpit{${TOKENS_CSS}${KIT_CSS}${PART_HIGHLIGHT_CSS}${ICON_CSS}}`);
1174
+ const KIT_CORE_LAYERED_TAG = inlineStyle(`@layer kit{${CORE_CSS}}`);
1166
1175
  const versionAttr = (version) => version === undefined ? "" : ` data-mockpit-version="${version}"`;
1167
1176
  // The same resolution kitAssets does (known ids, first occurrence wins), but
1168
1177
  // yielding one stylesheet per kit instead of one concatenated string. Project
@@ -1194,33 +1203,49 @@ function resolveKits(ids, projectKits = []) {
1194
1203
  }
1195
1204
  return chosen;
1196
1205
  }
1197
- // Everything a project's DesignSystem contributes to one html-surface doc.
1198
- // Kept in one place so the ordering rule is visible: the project's own tokens
1199
- // land AFTER mockpit's, because a repo that declares `--radius` or a brand
1200
- // color should win inside its own project's surfaces.
1201
- function designAssets(designSystem, mode) {
1202
- if (!designSystem)
1203
- return { css: "", kits: [], projectKits: [], headScripts: "", dark: false };
1204
- const tailwindCss = designSystem.kit === "tailwind" ? (designSystem.tailwindCss?.trim() ?? "") : "";
1205
- const raw = tailwindCss ? "" : (designSystem.cssVars?.trim() ?? "");
1206
+ // Everything a project's system contributes to one html-surface doc.
1207
+ function systemAssets(system, opts) {
1208
+ if (!system)
1209
+ return {
1210
+ repoVars: "",
1211
+ sheet: "",
1212
+ tokens: "",
1213
+ fonts: [],
1214
+ kits: [],
1215
+ projectKits: [],
1216
+ headScripts: "",
1217
+ tailwind: false,
1218
+ dark: false,
1219
+ attrs: "",
1220
+ };
1221
+ const repo = system.repo;
1222
+ const tailwindCss = system.kit === "tailwind" ? (repo?.tailwindCss?.trim() ?? "") : "";
1223
+ const raw = tailwindCss ? "" : (repo?.cssVars?.trim() ?? "");
1206
1224
  // `cssVars` is stored as the repo's raw block; accept either the full
1207
1225
  // `:root{…}` text or a bare declaration list.
1208
1226
  const vars = raw ? (raw.includes("{") ? raw : `:root{${raw}}`) : "";
1209
- const kit = designSystem.kit;
1227
+ const kit = system.kit;
1210
1228
  // The browser build reads its input from the style tags present when its
1211
1229
  // script runs, so the stylesheet goes first. Its compiled output already
1212
1230
  // carries the repo's `:root` block, hence no `cssVars` beside it.
1213
1231
  const tailwind = kit !== "tailwind"
1214
1232
  ? ""
1215
1233
  : `${tailwindCss ? `<style type="text/tailwindcss">${tailwindCss.replace(/<\/style/gi, "<\\/style")}</style>\n` : ""}<script src="${TAILWIND_CDN}"></script>`;
1234
+ const css = systemCss(system, opts);
1216
1235
  return {
1217
- css: vars,
1236
+ repoVars: vars,
1237
+ sheet: css.sheet.replace(/<\/style/gi, "<\\/style"),
1238
+ tokens: css.tokens,
1239
+ fonts: css.fonts,
1218
1240
  kits: kit && kit !== "tailwind" && kit !== "none" ? [kit] : [],
1219
- projectKits: Array.isArray(designSystem.projectKits) ? designSystem.projectKits : [],
1241
+ projectKits: Array.isArray(system.projectKits) ? system.projectKits : [],
1220
1242
  headScripts: tailwind,
1243
+ tailwind: kit === "tailwind",
1221
1244
  // Repos theme dark through a `.dark` ancestor (shadcn's `.dark {…}` block,
1222
1245
  // `@custom-variant dark (&:is(.dark *))`), so the frame wears the class.
1223
- dark: mode === "dark" && /\.dark\b/.test(tailwindCss || raw),
1246
+ dark: (css.attrs.scheme ?? opts.mode) === "dark" && /\.dark\b/.test(tailwindCss || raw),
1247
+ attrs: `${css.attrs.scheme ? ` data-scheme="${css.attrs.scheme}"` : ""}${css.attrs.palette ? ` data-palette="${escapeHtml(css.attrs.palette)}"` : ""}`,
1248
+ ...(css.attrs.scheme ? { scheme: css.attrs.scheme } : {}),
1224
1249
  };
1225
1250
  }
1226
1251
  export const escapeHtml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@@ -1453,29 +1478,52 @@ ${PARAGRAPHS_TAG}
1453
1478
  }
1454
1479
  export function renderHtmlPage(doc) {
1455
1480
  const theme = typeof doc.theme === "string" || doc.theme == null ? themeById(doc.theme) : doc.theme;
1456
- const designSystem = designAssets(doc.designSystem, doc.mode);
1481
+ const contract = doc.contract ?? CURRENT_CONTRACT;
1482
+ const project = systemAssets(doc.system, { mode: doc.mode, palette: doc.palette, contract });
1457
1483
  const preamble = knobPreamble(doc.version, doc.knobs);
1458
- const htmlTag = designSystem.dark
1484
+ let htmlTag = project.dark
1459
1485
  ? preamble.htmlTag.replace("<html ", '<html class="dark" ')
1460
1486
  : preamble.htmlTag;
1487
+ if (project.attrs && contract >= 2)
1488
+ htmlTag = htmlTag.replace("<html ", `<html${project.attrs} `);
1461
1489
  // The project kit is appended, so with both present its components win over
1462
1490
  // a surface-requested kit's same-named classes.
1463
- const kitIds = [...(doc.kits ?? []), ...designSystem.kits];
1464
- const kits = resolveKits(kitIds, designSystem.projectKits);
1491
+ const kitIds = [...(doc.kits ?? []), ...project.kits];
1492
+ const kits = resolveKits(kitIds, project.projectKits);
1465
1493
  const kit = kitAssets(kitIds);
1466
- // Document order decides the cascade: theme tokens, base, kit accents, kit(s),
1467
- // then the project's own vars (which must win last).
1468
- const styles = [
1469
- `<style>${tokenThemeCss(theme, doc.mode)}</style>`,
1470
- BASE_CSS_TAG,
1471
- `<style>${kitAccentCss(doc.mode)}</style>`,
1472
- ...(kits.some((k) => !k.href) ? [KIT_CORE_TAG] : []),
1473
- ...kits.flatMap((k) => [
1474
- ...(k.href ? [`<link rel="stylesheet" href="${escapeHtml(k.href)}">`] : []),
1475
- ...(k.css ? [inlineStyle(k.css)] : []),
1476
- ]),
1477
- `<style>${designSystem.css}${colorSchemeCss(doc.mode)}</style>`,
1478
- ].join("\n");
1494
+ const scheme = colorSchemeCss(project.scheme ?? doc.mode);
1495
+ const fonts = project.fonts.map((href) => `<link rel="stylesheet" href="${escapeHtml(href)}">`);
1496
+ const styles = contract >= 2
1497
+ ? // Layers decide the cascade: mockpit's own styles, then kits, then the
1498
+ // project sheet. The tokens and the page are unlayered, so they win
1499
+ // over all three without extra specificity.
1500
+ [
1501
+ project.tailwind ? LAYER_ORDER_TAILWIND_TAG : LAYER_ORDER_TAG,
1502
+ `<style>@layer mockpit{${tokenThemeCss(theme, doc.mode)}${kitAccentCss(doc.mode)}}</style>`,
1503
+ BASE_CSS_LAYERED_TAG,
1504
+ ...(kits.some((k) => !k.href) ? [KIT_CORE_LAYERED_TAG] : []),
1505
+ ...kits.flatMap((k) => [
1506
+ ...(k.href ? [`<style>@import url("${escapeHtml(k.href)}") layer(kit);</style>`] : []),
1507
+ ...(k.css ? [inlineStyle(`@layer kit{${k.css}}`)] : []),
1508
+ ]),
1509
+ ...(project.sheet ? [`<style>@layer system{${project.sheet}}</style>`] : []),
1510
+ `<style>${project.repoVars}${project.tokens}${scheme}</style>`,
1511
+ ...fonts,
1512
+ ].join("\n")
1513
+ : // Document order decides the cascade: theme tokens, base, kit accents,
1514
+ // kit(s), then the project's own vars (which must win last).
1515
+ [
1516
+ `<style>${tokenThemeCss(theme, doc.mode)}</style>`,
1517
+ BASE_CSS_TAG,
1518
+ `<style>${kitAccentCss(doc.mode)}</style>`,
1519
+ ...(kits.some((k) => !k.href) ? [KIT_CORE_TAG] : []),
1520
+ ...kits.flatMap((k) => [
1521
+ ...(k.href ? [`<link rel="stylesheet" href="${escapeHtml(k.href)}">`] : []),
1522
+ ...(k.css ? [inlineStyle(k.css)] : []),
1523
+ ]),
1524
+ `<style>${project.repoVars}${project.sheet}${project.tokens}${scheme}</style>`,
1525
+ ...fonts,
1526
+ ].join("\n");
1479
1527
  // Slots (fills.ts): only the chosen fill of each shows, keyed on the knob
1480
1528
  // attribute; with no value yet, the default fill.
1481
1529
  const fillCss = slotCss(scanFills(doc.html).slots);
@@ -1491,7 +1539,7 @@ ${preamble.headScript}
1491
1539
  ${KNOBS_TAG}
1492
1540
  ${styles}
1493
1541
  ${fillCss ? `<style>${fillCss}</style>` : ""}
1494
- ${designSystem.headScripts}
1542
+ ${project.headScripts}
1495
1543
  ${kits.flatMap((k) => (k.script ? [`<script src="${escapeHtml(k.script)}" defer></script>`] : [])).join("\n")}
1496
1544
  </head>
1497
1545
  <body>