mockpit 1.2.0 → 2.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.
Files changed (59) hide show
  1. package/README.md +69 -48
  2. package/bin/demoData.js +3 -3
  3. package/bin/mockpit.js +426 -51
  4. package/dist/mcp/server.js +45 -7
  5. package/dist/server/apiViews.d.ts +407 -21
  6. package/dist/server/apiViews.js +110 -11
  7. package/dist/server/app.d.ts +5 -1
  8. package/dist/server/app.js +1454 -192
  9. package/dist/server/codeWorker.js +2 -1
  10. package/dist/server/decisions.d.ts +23 -0
  11. package/dist/server/decisions.js +100 -0
  12. package/dist/server/designGuide.d.ts +6 -2
  13. package/dist/server/designGuide.js +96 -27
  14. package/dist/server/embeds.d.ts +26 -0
  15. package/dist/server/embeds.js +144 -0
  16. package/dist/server/eventLog.js +2 -1
  17. package/dist/server/feedbackBatch.d.ts +15 -4
  18. package/dist/server/feedbackBatch.js +31 -7
  19. package/dist/server/fills.d.ts +41 -0
  20. package/dist/server/fills.js +328 -0
  21. package/dist/server/guidance.d.ts +23 -0
  22. package/dist/server/guidance.js +71 -0
  23. package/dist/server/index.js +25 -2
  24. package/dist/server/knobs.d.ts +6 -2
  25. package/dist/server/knobs.js +11 -1
  26. package/dist/server/mcpHttp.d.ts +6 -0
  27. package/dist/server/mcpHttp.js +35 -0
  28. package/dist/server/mcpSpec.d.ts +14 -13
  29. package/dist/server/mcpSpec.js +133 -18
  30. package/dist/server/postSurfaces.js +6 -12
  31. package/dist/server/richRender.js +3 -1
  32. package/dist/server/run.js +10 -1
  33. package/dist/server/runApi.d.ts +3 -3
  34. package/dist/server/runApi.js +18 -13
  35. package/dist/server/sqlStore.d.ts +97 -5
  36. package/dist/server/sqlStore.js +367 -27
  37. package/dist/server/surfacePage.d.ts +6 -2
  38. package/dist/server/surfacePage.js +142 -10
  39. package/dist/server/themes.d.ts +2 -0
  40. package/dist/server/themes.js +23 -3
  41. package/dist/server/types.d.ts +107 -16
  42. package/dist/server/types.js +73 -10
  43. package/dist/server/viewerRequest.d.ts +2 -0
  44. package/dist/server/viewerRequest.js +14 -0
  45. package/extensions/mockpit.js +179 -16
  46. package/guide/packs/design-craft.md +35 -0
  47. package/guide/topics/asks.md +82 -31
  48. package/guide/topics/feedback.md +151 -43
  49. package/guide/topics/html.md +8 -10
  50. package/guide/topics/http.md +8 -4
  51. package/guide/topics/knobs.md +4 -3
  52. package/guide/topics/scripts.md +2 -2
  53. package/guide/topics/surfaces.md +45 -11
  54. package/package.json +3 -15
  55. package/skills/launch-video/SKILL.md +2 -2
  56. package/skills/mockpit/SKILL.md +23 -7
  57. package/viewer/dist/index.html +9 -8
  58. package/dist/server/slots.d.ts +0 -14
  59. package/dist/server/slots.js +0 -71
@@ -3,8 +3,8 @@ import type { DesignSettings } from "./types.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 HIT_TEST_JS = "\n(function () {\n var MAX_DEPTH = 6;\n var MAX_TEXT = 60;\n function docBox() {\n var w = document.documentElement.clientWidth || 1;\n var h = (document.body && document.body.scrollHeight) || document.documentElement.scrollHeight || 1;\n return { w: w, h: h };\n }\n function ident(v) { return typeof v === 'string' && /^[A-Za-z][\\w-]*$/.test(v); }\n function cssPath(el) {\n var parts = [];\n while (el && el.nodeType === 1 && el !== document.body && el !== document.documentElement) {\n var tag = el.tagName.toLowerCase();\n if (ident(el.id)) { parts.unshift(tag + '#' + el.id); break; }\n var sel = tag;\n var cls = (el.getAttribute('class') || '').split(/\\s+/).filter(ident).slice(0, 2);\n if (cls.length) sel += '.' + cls.join('.');\n var p = el.parentElement;\n if (p) {\n var sibs = [];\n for (var i = 0; i < p.children.length; i++) {\n if (p.children[i].tagName === el.tagName) sibs.push(p.children[i]);\n }\n if (sibs.length > 1) sel += ':nth-of-type(' + (sibs.indexOf(el) + 1) + ')';\n }\n parts.unshift(sel);\n if (parts.length >= MAX_DEPTH) break;\n el = p;\n }\n return parts.join(' > ');\n }\n function firstLine(el) {\n var t = (el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim();\n var line = t.split('\\n')[0].trim();\n return line.length > MAX_TEXT ? line.slice(0, MAX_TEXT - 1) + '\\u2026' : line;\n }\n window.addEventListener('message', function (e) {\n if (e.source !== parent || parent === window) return;\n var d = e.data;\n if (!d || d.__mockpit !== true || d.type !== 'hit-test') return;\n var box = docBox();\n var px = Math.max(0, Math.min(1, Number(d.x) || 0)) * box.w;\n var py = Math.max(0, Math.min(1, Number(d.y) || 0)) * box.h;\n var el = document.elementFromPoint(px - (window.scrollX || 0), py - (window.scrollY || 0));\n var reply = { __mockpit: true, type: 'hit-test-result', ref: d.ref, path: '', text: '', rect: [0, 0, 0, 0] };\n if (el && el.nodeType === 1) {\n var r = el.getBoundingClientRect();\n reply.path = cssPath(el);\n reply.text = firstLine(el);\n reply.rect = [\n (r.left + (window.scrollX || 0)) / box.w,\n (r.top + (window.scrollY || 0)) / box.h,\n r.width / box.w,\n r.height / box.h,\n ];\n }\n parent.postMessage(reply, '*');\n });\n})();\n";
6
- export declare const PARTS_JS = "\n(function () {\n if (window.parent === window) return;\n var HL = 'mockpit-part-hl';\n var MAX_PARTS = 200;\n var MAX_TEXT = 200;\n var doc = window.__mockpitDoc || {};\n var version = typeof doc.version === 'number' ? doc.version : null;\n function text(v) { return v == null ? null : String(v).slice(0, MAX_TEXT); }\n function isFixed(el) {\n for (var n = el; n && n.nodeType === 1; n = n.parentElement) {\n if (getComputedStyle(n).position === 'fixed') return true;\n }\n return false;\n }\n function collect() {\n var sx = window.scrollX || 0, sy = window.scrollY || 0;\n var els = document.querySelectorAll('[data-part]');\n var parts = [];\n for (var i = 0; i < els.length && parts.length < MAX_PARTS; i++) {\n var el = els[i];\n var name = text(el.getAttribute('data-part'));\n if (!name) continue;\n var r = el.getBoundingClientRect();\n var depth = 0;\n for (var p = el.parentElement; p; p = p.parentElement) if (p.hasAttribute('data-part')) depth++;\n var outer = el.parentElement && el.parentElement.closest('[data-part]');\n parts.push({\n name: name,\n label: text(el.getAttribute('data-part-label')) || name,\n key: text(el.getAttribute('data-part-key')),\n box: { x: r.left + sx, y: r.top + sy, w: r.width, h: r.height },\n visible: (r.width > 0 || r.height > 0) && getComputedStyle(el).visibility !== 'hidden',\n fixed: isFixed(el),\n depth: depth,\n parent: outer ? text(outer.getAttribute('data-part')) : null,\n order: i,\n });\n }\n return {\n version: version,\n parts: parts,\n height: document.body ? document.body.scrollHeight : document.documentElement.scrollHeight,\n scroll: { x: sx, y: sy },\n viewport: { w: innerWidth, h: innerHeight },\n };\n }\n var last = '';\n var timer = 0;\n // setTimeout, not rAF: a hidden cross-origin frame may have rendering (and\n // with it rAF and ResizeObserver) throttled, but timers still run.\n function schedule() {\n if (timer) return;\n timer = setTimeout(function () {\n timer = 0;\n var m = collect();\n var key = JSON.stringify(m);\n if (key === last) return;\n last = key;\n m.__mockpit = true;\n m.type = 'parts';\n parent.postMessage(m, '*');\n }, 0);\n }\n\n var ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(schedule) : null;\n var observed = typeof WeakSet !== 'undefined' ? new WeakSet() : null;\n function observeAll() {\n if (!ro || !observed) return;\n // html/body too: a non-part sibling growing moves parts without resizing them.\n var els = [document.documentElement, document.body].concat([].slice.call(document.querySelectorAll('[data-part]')));\n for (var i = 0; i < els.length; i++) {\n var el = els[i];\n if (el && !observed.has(el)) { observed.add(el); ro.observe(el); }\n }\n }\n var moTimer = 0;\n var mo = new MutationObserver(function () {\n clearTimeout(moTimer);\n moTimer = setTimeout(function () { observeAll(); schedule(); }, 40);\n });\n mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });\n\n // Transforms and transitions move boxes without resizing or mutating anything.\n document.addEventListener('animationend', schedule, true);\n document.addEventListener('transitionend', schedule, true);\n window.addEventListener('scroll', schedule, { passive: true });\n window.addEventListener('resize', schedule);\n if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule);\n observeAll();\n schedule();\n // WebKit: RO's initial callback may never fire in a sandboxed frame.\n function settle() {\n schedule();\n [50, 150, 400, 1000].forEach(function (ms) { setTimeout(schedule, ms); });\n }\n if (document.readyState === 'complete') settle();\n else window.addEventListener('load', settle);\n\n function setHighlight(names) {\n var on = document.querySelectorAll('.' + HL);\n for (var i = 0; i < on.length; i++) on[i].classList.remove(HL);\n if (names && names.length) {\n var want = {};\n for (var k = 0; k < names.length && k < MAX_PARTS; k++) want[String(names[k])] = true;\n var els = document.querySelectorAll('[data-part]');\n for (var j = 0; j < els.length; j++) {\n if (want[els[j].getAttribute('data-part')] === true) els[j].classList.add(HL);\n }\n }\n // Our own class flip must not look like the agent's page changing.\n mo.takeRecords();\n }\n\n window.addEventListener('message', function (e) {\n if (e.source !== parent) return;\n var d = e.data;\n if (!d || d.__mockpit !== true) return;\n if (d.type === 'highlight') setHighlight(Array.isArray(d.parts) ? d.parts : []);\n else if (d.type === 'clear') setHighlight(null);\n else if (d.type === 'scroll') window.scrollBy(Number(d.dx) || 0, Number(d.dy) || 0);\n else if (d.type === 'hit') {\n var el = document.elementFromPoint(\n (Number(d.x) || 0) - (window.scrollX || 0),\n (Number(d.y) || 0) - (window.scrollY || 0)\n );\n var part = el && el.closest ? el.closest('[data-part]') : null;\n parent.postMessage({\n __mockpit: true,\n type: 'hit',\n ref: d.ref,\n part: part ? text(part.getAttribute('data-part')) : null,\n key: part ? text(part.getAttribute('data-part-key')) : null,\n }, '*');\n }\n });\n})();\n";
7
- export declare const KNOBS_JS = "\n(function () {\n\nvar __kPathRe = /^[A-Za-z_][\\w-]{0,63}(\\.[A-Za-z_][\\w-]{0,63}){0,3}$/;\nvar __kTokenRe = /^[#\\w(),.%/+-][#\\w\\s(),.%/+-]*$/;\nfunction __kName(path) { return path.replace(/\\./g, '-'); }\nfunction __kCssString(s) {\n return '\"' + s.replace(/[\\\\\"]/g, function (c) { return '\\\\' + c; })\n .replace(/[\\u0000-\\u001f\\u007f]/g, function (c) { return '\\\\' + c.charCodeAt(0).toString(16) + ' '; }) + '\"';\n}\nfunction __kCss(v) {\n if (typeof v === 'number') return isFinite(v) ? String(v) : null;\n if (typeof v === 'boolean') return v ? '1' : '0';\n if (typeof v === 'string') return __kTokenRe.test(v) ? v : __kCssString(v);\n return null;\n}\nfunction __kVars(path, v) {\n var name = __kName(path);\n if (v && typeof v === 'object' && !Array.isArray(v)) {\n var out = [];\n for (var f in v) {\n if (!Object.prototype.hasOwnProperty.call(v, f) || !/^[A-Za-z_][\\w-]{0,63}$/.test(f)) continue;\n var c = __kCss(v[f]);\n if (c !== null) out.push([name + '-' + f, c]);\n }\n return out;\n }\n var css = __kCss(v);\n return css === null ? [] : [[name, css]];\n}\nfunction __kAttr(v) {\n if (typeof v === 'number') return isFinite(v) ? String(v) : null;\n if (typeof v === 'boolean') return v ? 'true' : 'false';\n if (typeof v === 'string') return v;\n return null;\n}\n\n var doc = window.__mockpitDoc || {};\n var current = {};\n function own(o, k) { return Object.prototype.hasOwnProperty.call(o, k); }\n function bind(path, v) {\n var els = document.querySelectorAll('[data-k-bind]');\n for (var i = 0; i < els.length; i++) {\n if (els[i].getAttribute('data-k-bind') !== path) continue;\n els[i].textContent = v && typeof v === 'object' ? JSON.stringify(v) : String(v);\n }\n }\n function apply(values, live) {\n var root = document.documentElement;\n for (var path in values) {\n if (!own(values, path) || !__kPathRe.test(path)) continue;\n var v = values[path];\n if (v === null || v === undefined) continue;\n current[path] = v;\n if (live) {\n var vars = __kVars(path, v);\n if (!vars.length) root.style.removeProperty('--k-' + __kName(path));\n for (var i = 0; i < vars.length; i++) root.style.setProperty('--k-' + vars[i][0], vars[i][1]);\n var attr = __kAttr(v);\n if (attr === null) root.removeAttribute('data-k-' + __kName(path));\n else root.setAttribute('data-k-' + __kName(path), attr);\n }\n bind(path, v);\n }\n window.dispatchEvent(new CustomEvent('mockpit:knobs', {\n detail: { values: JSON.parse(JSON.stringify(current)) },\n }));\n }\n function initial() { apply(doc.knobs && typeof doc.knobs === 'object' ? doc.knobs : {}, false); }\n if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initial);\n else initial();\n window.addEventListener('message', function (e) {\n if (e.source !== parent || parent === window) return;\n var d = e.data;\n if (!d || d.__mockpit !== true || d.type !== 'knobs') return;\n if (d.values && typeof d.values === 'object' && !Array.isArray(d.values)) apply(d.values, true);\n });\n})();\n";
6
+ export declare const PARTS_JS = "\n(function () {\n if (window.parent === window) return;\n var HL = 'mockpit-part-hl';\n var MAX_PARTS = 200;\n var MAX_TEXT = 200;\n var doc = window.__mockpitDoc || {};\n var version = typeof doc.version === 'number' ? doc.version : null;\n function text(v) { return v == null ? null : String(v).slice(0, MAX_TEXT); }\n function isFixed(el) {\n for (var n = el; n && n.nodeType === 1; n = n.parentElement) {\n if (getComputedStyle(n).position === 'fixed') return true;\n }\n return false;\n }\n function collect() {\n var sx = window.scrollX || 0, sy = window.scrollY || 0;\n var els = document.querySelectorAll('[data-part]');\n var parts = [];\n for (var i = 0; i < els.length && parts.length < MAX_PARTS; i++) {\n var el = els[i];\n var name = text(el.getAttribute('data-part'));\n if (!name) continue;\n var r = el.getBoundingClientRect();\n var depth = 0;\n for (var p = el.parentElement; p; p = p.parentElement) if (p.hasAttribute('data-part')) depth++;\n var outer = el.parentElement && el.parentElement.closest('[data-part]');\n parts.push({\n name: name,\n label: text(el.getAttribute('data-part-label')) || name,\n key: text(el.getAttribute('data-part-key')),\n box: { x: r.left + sx, y: r.top + sy, w: r.width, h: r.height },\n visible: (r.width > 0 || r.height > 0) && getComputedStyle(el).visibility !== 'hidden',\n fixed: isFixed(el),\n depth: depth,\n parent: outer ? text(outer.getAttribute('data-part')) : null,\n order: i,\n });\n }\n return {\n version: version,\n parts: parts,\n height: document.body ? document.body.scrollHeight : document.documentElement.scrollHeight,\n scroll: { x: sx, y: sy },\n // scrollWidth past innerWidth: the design is wider than the frame.\n viewport: { w: innerWidth, h: innerHeight, scrollW: document.documentElement.scrollWidth },\n };\n }\n var last = '';\n var timer = 0;\n // setTimeout, not rAF: a hidden cross-origin frame may have rendering (and\n // with it rAF and ResizeObserver) throttled, but timers still run.\n function schedule() {\n if (timer) return;\n timer = setTimeout(function () {\n timer = 0;\n var m = collect();\n var key = JSON.stringify(m);\n if (key === last) return;\n last = key;\n m.__mockpit = true;\n m.type = 'parts';\n parent.postMessage(m, '*');\n }, 0);\n }\n\n var ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(schedule) : null;\n var observed = typeof WeakSet !== 'undefined' ? new WeakSet() : null;\n function observeAll() {\n if (!ro || !observed) return;\n // html/body too: a non-part sibling growing moves parts without resizing them.\n var els = [document.documentElement, document.body].concat([].slice.call(document.querySelectorAll('[data-part]')));\n for (var i = 0; i < els.length; i++) {\n var el = els[i];\n if (el && !observed.has(el)) { observed.add(el); ro.observe(el); }\n }\n }\n var moTimer = 0;\n var mo = new MutationObserver(function () {\n clearTimeout(moTimer);\n moTimer = setTimeout(function () { observeAll(); schedule(); }, 40);\n });\n mo.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });\n\n // Transforms and transitions move boxes without resizing or mutating anything.\n document.addEventListener('animationend', schedule, true);\n document.addEventListener('transitionend', schedule, true);\n window.addEventListener('scroll', schedule, { passive: true });\n window.addEventListener('resize', schedule);\n if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule);\n observeAll();\n schedule();\n // WebKit: RO's initial callback may never fire in a sandboxed frame.\n function settle() {\n schedule();\n [50, 150, 400, 1000].forEach(function (ms) { setTimeout(schedule, ms); });\n }\n if (document.readyState === 'complete') settle();\n else window.addEventListener('load', settle);\n\n function setHighlight(names) {\n var on = document.querySelectorAll('.' + HL);\n for (var i = 0; i < on.length; i++) on[i].classList.remove(HL);\n if (names && names.length) {\n var want = {};\n for (var k = 0; k < names.length && k < MAX_PARTS; k++) want[String(names[k])] = true;\n var els = document.querySelectorAll('[data-part]');\n for (var j = 0; j < els.length; j++) {\n if (want[els[j].getAttribute('data-part')] === true) els[j].classList.add(HL);\n }\n }\n // Our own class flip must not look like the agent's page changing.\n mo.takeRecords();\n }\n\n // Try: the host lets the user click through the design; an element marked\n // data-goto moves them to another screen. Only the host knows what that\n // names, so the frame just reports it, and the click goes no further.\n var trying = false;\n function setTrying(on) {\n trying = on;\n document.documentElement.classList.toggle('mockpit-try', on);\n mo.takeRecords();\n }\n window.addEventListener('click', function (e) {\n if (!trying) return;\n var t = e.target && e.target.closest ? e.target.closest('[data-goto]') : null;\n if (!t) return;\n e.preventDefault();\n e.stopPropagation();\n parent.postMessage({\n __mockpit: true,\n type: 'goto',\n version: version,\n to: text(t.getAttribute('data-goto')),\n }, '*');\n }, true);\n\n // A finger's tap, in document px, for a host that lets touches through to the\n // page (its canvas, where the page scrolls natively). A drag is a scroll: the\n // browser cancels the pointer or it moves too far. A mouse clicks the host's\n // own overlay instead, so it never reports here.\n var tapAt = null;\n window.addEventListener('pointerdown', function (e) {\n tapAt = e.isPrimary && e.pointerType !== 'mouse' ? { x: e.clientX, y: e.clientY, t: Date.now() } : null;\n }, true);\n window.addEventListener('pointercancel', function () { tapAt = null; }, true);\n window.addEventListener('pointerup', function (e) {\n var a = tapAt;\n tapAt = null;\n if (!a || !e.isPrimary) return;\n if (Math.abs(e.clientX - a.x) > 10 || Math.abs(e.clientY - a.y) > 10 || Date.now() - a.t > 600) return;\n parent.postMessage({\n __mockpit: true,\n type: 'tap',\n x: e.clientX + (window.scrollX || 0),\n y: e.clientY + (window.scrollY || 0),\n }, '*');\n }, true);\n\n // Two fingers are the host's: a page laid on its canvas takes one-finger\n // scrolls itself, but a pinch zooms the canvas, not this page or the browser.\n var pinching = false;\n function pinch(phase, e) {\n var t = e.touches;\n parent.postMessage({\n __mockpit: true,\n type: 'pinch',\n phase: phase,\n a: t[0] ? { x: t[0].clientX, y: t[0].clientY } : null,\n b: t[1] ? { x: t[1].clientX, y: t[1].clientY } : null,\n }, '*');\n }\n window.addEventListener('touchstart', function (e) {\n if (e.touches.length !== 2) return;\n e.preventDefault();\n pinching = true;\n pinch('start', e);\n }, { capture: true, passive: false });\n window.addEventListener('touchmove', function (e) {\n if (!pinching || e.touches.length < 2) return;\n e.preventDefault();\n pinch('move', e);\n }, { capture: true, passive: false });\n function release(e) {\n if (!pinching || e.touches.length >= 2) return;\n pinching = false;\n pinch('end', e);\n }\n window.addEventListener('touchend', release, true);\n window.addEventListener('touchcancel', release, true);\n // \u2318/Ctrl+scroll and a trackpad pinch (ctrl+wheel) are the host's too: they\n // zoom the canvas around the pointer, never this page or the browser.\n window.addEventListener('wheel', function (e) {\n if (!e.ctrlKey && !e.metaKey) return;\n e.preventDefault();\n parent.postMessage({ __mockpit: true, type: 'zoom', x: e.clientX, y: e.clientY, dy: e.deltaY }, '*');\n }, { capture: true, passive: false });\n\n window.addEventListener('message', function (e) {\n if (e.source !== parent) return;\n var d = e.data;\n if (!d || d.__mockpit !== true) return;\n if (d.type === 'highlight') setHighlight(Array.isArray(d.parts) ? d.parts : []);\n else if (d.type === 'clear') setHighlight(null);\n else if (d.type === 'try') setTrying(d.on === true);\n else if (d.type === 'scroll') window.scrollBy(Number(d.dx) || 0, Number(d.dy) || 0);\n else if (d.type === 'hit') {\n var el = document.elementFromPoint(\n (Number(d.x) || 0) - (window.scrollX || 0),\n (Number(d.y) || 0) - (window.scrollY || 0)\n );\n var part = el && el.closest ? el.closest('[data-part]') : null;\n parent.postMessage({\n __mockpit: true,\n type: 'hit',\n ref: d.ref,\n part: part ? text(part.getAttribute('data-part')) : null,\n key: part ? text(part.getAttribute('data-part-key')) : null,\n }, '*');\n }\n });\n})();\n";
7
+ export declare const KNOBS_JS = "\n(function () {\n\nvar __kPathRe = /^[A-Za-z_][\\w-]{0,63}(\\.[A-Za-z_][\\w-]{0,63}){0,3}$/;\nvar __kTokenRe = /^[#\\w(),.%/+-][#\\w\\s(),.%/+-]*$/;\nfunction __kName(path) { return path.replace(/\\./g, '-'); }\nfunction __kCssString(s) {\n return '\"' + s.replace(/[\\\\\"]/g, function (c) { return '\\\\' + c; })\n .replace(/[\\u0000-\\u001f\\u007f]/g, function (c) { return '\\\\' + c.charCodeAt(0).toString(16) + ' '; }) + '\"';\n}\nfunction __kCss(v) {\n if (typeof v === 'number') return isFinite(v) ? String(v) : null;\n if (typeof v === 'boolean') return v ? '1' : '0';\n if (typeof v === 'string') return __kTokenRe.test(v) ? v : __kCssString(v);\n return null;\n}\nfunction __kVars(path, v) {\n var name = __kName(path);\n if (v && typeof v === 'object' && !Array.isArray(v)) {\n var out = [];\n for (var f in v) {\n if (!Object.prototype.hasOwnProperty.call(v, f) || !/^[A-Za-z_][\\w-]{0,63}$/.test(f)) continue;\n var c = __kCss(v[f]);\n if (c !== null) out.push([name + '-' + f, c]);\n }\n return out;\n }\n var css = __kCss(v);\n return css === null ? [] : [[name, css]];\n}\nfunction __kAttr(v) {\n if (typeof v === 'number') return isFinite(v) ? String(v) : null;\n if (typeof v === 'boolean') return v ? 'true' : 'false';\n if (typeof v === 'string') return v;\n return null;\n}\n\n var doc = window.__mockpitDoc || {};\n var current = {};\n function own(o, k) { return Object.prototype.hasOwnProperty.call(o, k); }\n function bind(path, v) {\n var els = document.querySelectorAll('[data-k-bind]');\n for (var i = 0; i < els.length; i++) {\n if (els[i].getAttribute('data-k-bind') !== path) continue;\n els[i].textContent = v && typeof v === 'object' ? JSON.stringify(v) : String(v);\n }\n }\n function apply(values, live) {\n var root = document.documentElement;\n for (var path in values) {\n if (!own(values, path) || !__kPathRe.test(path)) continue;\n var v = values[path];\n if (v === null || v === undefined) continue;\n current[path] = v;\n if (live) {\n var vars = __kVars(path, v);\n if (!vars.length) root.style.removeProperty('--k-' + __kName(path));\n for (var i = 0; i < vars.length; i++) root.style.setProperty('--k-' + vars[i][0], vars[i][1]);\n var attr = __kAttr(v);\n if (attr === null) root.removeAttribute('data-k-' + __kName(path));\n else root.setAttribute('data-k-' + __kName(path), attr);\n }\n bind(path, v);\n }\n window.dispatchEvent(new CustomEvent('mockpit:knobs', {\n detail: { values: JSON.parse(JSON.stringify(current)) },\n }));\n }\n // The URL's values are already on <html>; a live command may also have come\n // in before the body existed, so both are bound once it does.\n function initial() {\n var start = doc.knobs && typeof doc.knobs === 'object' ? doc.knobs : {};\n for (var k in start) if (own(start, k) && !own(current, k)) current[k] = start[k];\n apply(current, false);\n }\n if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initial);\n else initial();\n // Runs from <head>, before the page's own markup and anything slow it loads\n // (a web font, a CDN stylesheet), so the host can send values at once rather\n // than wait for the first parts report, which those can hold back.\n if (parent !== window) parent.postMessage({ __mockpit: true, type: 'ready', version: doc.version }, '*');\n window.addEventListener('message', function (e) {\n if (e.source !== parent || parent === window) return;\n var d = e.data;\n if (!d || d.__mockpit !== true || d.type !== 'knobs') return;\n if (d.values && typeof d.values === 'object' && !Array.isArray(d.values)) apply(d.values, true);\n });\n})();\n";
8
8
  export declare function knobCss(v: unknown): string | null;
9
9
  export declare function knobVars(path: string, v: unknown): [string, string][];
10
10
  export declare function knobAttr(v: unknown): string | null;
@@ -13,6 +13,10 @@ export declare function knobPreamble(version: number | undefined, knobs: Record<
13
13
  htmlTag: string;
14
14
  headScript: string;
15
15
  };
16
+ export declare function knobAttributes(knobs: Record<string, unknown> | undefined): {
17
+ attrs: string;
18
+ values: Record<string, unknown>;
19
+ };
16
20
  export interface StaticAsset {
17
21
  path: string;
18
22
  contentType: string;
@@ -1,3 +1,4 @@
1
+ import { scanFills, slotCss } from "./fills.js";
1
2
  import { CDN_ALLOWLIST, checkCdnUrl } from "./cdn.js";
2
3
  import { CORE_CSS, KITS, kitAssets } from "./kits.js";
3
4
  import { expandIcons } from "./icons.js";
@@ -63,7 +64,11 @@ const TOKENS_CSS = `
63
64
  --border-radius-lg: 12px;
64
65
  --border-radius-xl: 16px;
65
66
  }
66
- html { box-sizing: border-box; scrollbar-width: none; }
67
+ /* A page that scrolls inside a host's frame stops at its own ends instead of
68
+ handing the rest of a swipe to the host (Safari reads the root's value), and
69
+ a pinch on it can't zoom the whole viewer around it. */
70
+ html { box-sizing: border-box; scrollbar-width: none; overscroll-behavior: contain; touch-action: pan-x pan-y; }
71
+ body { overscroll-behavior: contain; }
67
72
  html::-webkit-scrollbar, body::-webkit-scrollbar { display: none; }
68
73
  *, *::before, *::after { box-sizing: inherit; }
69
74
  body {
@@ -356,7 +361,7 @@ export const HIT_TEST_JS = `
356
361
  // copy {text} window.copyToClipboard (BRIDGE_JS)
357
362
  // switch-session {key} Cmd+Opt+ArrowUp/Down (BRIDGE_JS)
358
363
  // hit-test-result {ref, path, text, rect} reply to hit-test (HIT_TEST_JS)
359
- // parts {version, parts, height, scroll:{x,y}, viewport:{w,h}} (PARTS_JS)
364
+ // parts {version, parts, height, scroll:{x,y}, viewport:{w,h,scrollW}} (PARTS_JS)
360
365
  // parts[i] = {name, label, key, box:{x,y,w,h}, visible, fixed, depth, parent, order}
361
366
  // box is in DOCUMENT px (viewport rect + scroll), so the host draws
362
367
  // (box − scroll) × scale, which is also right for fixed and sticky parts.
@@ -370,6 +375,23 @@ export const HIT_TEST_JS = `
370
375
  // 50/150/400/1000ms timers, and on any layout-affecting change; never
371
376
  // twice in a row with identical content.
372
377
  // hit {ref, part, key} reply to hit; part = name|null
378
+ // goto {version, to} a click on [data-goto] while the host
379
+ // has Try on (PARTS_JS); to is the
380
+ // attribute as written ("next", "prev",
381
+ // "<mock>:<state>"), ≤200 chars. The host
382
+ // honours it only while Try is on, from
383
+ // the frame on stage at its version, and
384
+ // only to a screen of this project:
385
+ // never a URL
386
+ // tap {x, y} a finger's tap in document px (PARTS_JS);
387
+ // a drag never reports
388
+ // ready {version} the knob runtime is listening (KNOBS_JS,
389
+ // from <head>); values can be sent now
390
+ // pinch {phase, a:{x,y}, b:{x,y}} two fingers on the page, in viewport px
391
+ // zoom {x, y, dy} ⌘/Ctrl+scroll or a trackpad pinch, in viewport px
392
+ // (PARTS_JS); phase start|move|end. The
393
+ // page's own two-finger touches are
394
+ // cancelled: the host zooms its canvas
373
395
  //
374
396
  // host → frame
375
397
  // hit-test {ref, x, y} x,y normalized 0..1 of the document box (Mark tool)
@@ -383,6 +405,9 @@ export const HIT_TEST_JS = `
383
405
  // overlay, forwarded)
384
406
  // knobs {values} {path: value}, merged into the current values
385
407
  // and applied exactly like the `?k=` preamble
408
+ // try {on} Try is on or off: while on, [data-goto] elements are
409
+ // outlined and a click on one reports goto instead
410
+ // of reaching the page; a new document starts off
386
411
  //
387
412
  // Knobs (`/s/:id?k=`, validated by the route against the mock's declared knobs,
388
413
  // and the `knobs` command): for each path, dots become "-" in names, and
@@ -482,7 +507,8 @@ export const PARTS_JS = `
482
507
  parts: parts,
483
508
  height: document.body ? document.body.scrollHeight : document.documentElement.scrollHeight,
484
509
  scroll: { x: sx, y: sy },
485
- viewport: { w: innerWidth, h: innerHeight },
510
+ // scrollWidth past innerWidth: the design is wider than the frame.
511
+ viewport: { w: innerWidth, h: innerHeight, scrollW: document.documentElement.scrollWidth },
486
512
  };
487
513
  }
488
514
  var last = '';
@@ -552,12 +578,97 @@ export const PARTS_JS = `
552
578
  mo.takeRecords();
553
579
  }
554
580
 
581
+ // Try: the host lets the user click through the design; an element marked
582
+ // data-goto moves them to another screen. Only the host knows what that
583
+ // names, so the frame just reports it, and the click goes no further.
584
+ var trying = false;
585
+ function setTrying(on) {
586
+ trying = on;
587
+ document.documentElement.classList.toggle('mockpit-try', on);
588
+ mo.takeRecords();
589
+ }
590
+ window.addEventListener('click', function (e) {
591
+ if (!trying) return;
592
+ var t = e.target && e.target.closest ? e.target.closest('[data-goto]') : null;
593
+ if (!t) return;
594
+ e.preventDefault();
595
+ e.stopPropagation();
596
+ parent.postMessage({
597
+ __mockpit: true,
598
+ type: 'goto',
599
+ version: version,
600
+ to: text(t.getAttribute('data-goto')),
601
+ }, '*');
602
+ }, true);
603
+
604
+ // A finger's tap, in document px, for a host that lets touches through to the
605
+ // page (its canvas, where the page scrolls natively). A drag is a scroll: the
606
+ // browser cancels the pointer or it moves too far. A mouse clicks the host's
607
+ // own overlay instead, so it never reports here.
608
+ var tapAt = null;
609
+ window.addEventListener('pointerdown', function (e) {
610
+ tapAt = e.isPrimary && e.pointerType !== 'mouse' ? { x: e.clientX, y: e.clientY, t: Date.now() } : null;
611
+ }, true);
612
+ window.addEventListener('pointercancel', function () { tapAt = null; }, true);
613
+ window.addEventListener('pointerup', function (e) {
614
+ var a = tapAt;
615
+ tapAt = null;
616
+ if (!a || !e.isPrimary) return;
617
+ if (Math.abs(e.clientX - a.x) > 10 || Math.abs(e.clientY - a.y) > 10 || Date.now() - a.t > 600) return;
618
+ parent.postMessage({
619
+ __mockpit: true,
620
+ type: 'tap',
621
+ x: e.clientX + (window.scrollX || 0),
622
+ y: e.clientY + (window.scrollY || 0),
623
+ }, '*');
624
+ }, true);
625
+
626
+ // Two fingers are the host's: a page laid on its canvas takes one-finger
627
+ // scrolls itself, but a pinch zooms the canvas, not this page or the browser.
628
+ var pinching = false;
629
+ function pinch(phase, e) {
630
+ var t = e.touches;
631
+ parent.postMessage({
632
+ __mockpit: true,
633
+ type: 'pinch',
634
+ phase: phase,
635
+ a: t[0] ? { x: t[0].clientX, y: t[0].clientY } : null,
636
+ b: t[1] ? { x: t[1].clientX, y: t[1].clientY } : null,
637
+ }, '*');
638
+ }
639
+ window.addEventListener('touchstart', function (e) {
640
+ if (e.touches.length !== 2) return;
641
+ e.preventDefault();
642
+ pinching = true;
643
+ pinch('start', e);
644
+ }, { capture: true, passive: false });
645
+ window.addEventListener('touchmove', function (e) {
646
+ if (!pinching || e.touches.length < 2) return;
647
+ e.preventDefault();
648
+ pinch('move', e);
649
+ }, { capture: true, passive: false });
650
+ function release(e) {
651
+ if (!pinching || e.touches.length >= 2) return;
652
+ pinching = false;
653
+ pinch('end', e);
654
+ }
655
+ window.addEventListener('touchend', release, true);
656
+ window.addEventListener('touchcancel', release, true);
657
+ // ⌘/Ctrl+scroll and a trackpad pinch (ctrl+wheel) are the host's too: they
658
+ // zoom the canvas around the pointer, never this page or the browser.
659
+ window.addEventListener('wheel', function (e) {
660
+ if (!e.ctrlKey && !e.metaKey) return;
661
+ e.preventDefault();
662
+ parent.postMessage({ __mockpit: true, type: 'zoom', x: e.clientX, y: e.clientY, dy: e.deltaY }, '*');
663
+ }, { capture: true, passive: false });
664
+
555
665
  window.addEventListener('message', function (e) {
556
666
  if (e.source !== parent) return;
557
667
  var d = e.data;
558
668
  if (!d || d.__mockpit !== true) return;
559
669
  if (d.type === 'highlight') setHighlight(Array.isArray(d.parts) ? d.parts : []);
560
670
  else if (d.type === 'clear') setHighlight(null);
671
+ else if (d.type === 'try') setTrying(d.on === true);
561
672
  else if (d.type === 'scroll') window.scrollBy(Number(d.dx) || 0, Number(d.dy) || 0);
562
673
  else if (d.type === 'hit') {
563
674
  var el = document.elementFromPoint(
@@ -612,9 +723,19 @@ ${KNOB_VALUE_JS}
612
723
  detail: { values: JSON.parse(JSON.stringify(current)) },
613
724
  }));
614
725
  }
615
- function initial() { apply(doc.knobs && typeof doc.knobs === 'object' ? doc.knobs : {}, false); }
726
+ // The URL's values are already on <html>; a live command may also have come
727
+ // in before the body existed, so both are bound once it does.
728
+ function initial() {
729
+ var start = doc.knobs && typeof doc.knobs === 'object' ? doc.knobs : {};
730
+ for (var k in start) if (own(start, k) && !own(current, k)) current[k] = start[k];
731
+ apply(current, false);
732
+ }
616
733
  if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', initial);
617
734
  else initial();
735
+ // Runs from <head>, before the page's own markup and anything slow it loads
736
+ // (a web font, a CDN stylesheet), so the host can send values at once rather
737
+ // than wait for the first parts report, which those can hold back.
738
+ if (parent !== window) parent.postMessage({ __mockpit: true, type: 'ready', version: doc.version }, '*');
618
739
  window.addEventListener('message', function (e) {
619
740
  if (e.source !== parent || parent === window) return;
620
741
  var d = e.data;
@@ -680,6 +801,16 @@ const scriptJson = (v) => JSON.stringify(v)
680
801
  // knobCss (quoted unless plain tokens) and then escapeHtml as the style
681
802
  // attribute, and the JSON through scriptJson.
682
803
  export function knobPreamble(version, knobs) {
804
+ const { attrs, values } = knobAttributes(knobs);
805
+ return {
806
+ htmlTag: `<html lang="en"${attrs}>`,
807
+ headScript: `<script>window.__mockpitDoc=${scriptJson({ version: version ?? null, knobs: values })};</script>`,
808
+ };
809
+ }
810
+ // Knob values as an element's `data-k-*` attributes and `--k-*` style vars, the
811
+ // same marks the preamble puts on <html>; an embedded component's wrapper
812
+ // carries its own settled values this way.
813
+ export function knobAttributes(knobs) {
683
814
  const attrs = [];
684
815
  const vars = [];
685
816
  const values = {};
@@ -694,14 +825,11 @@ export function knobPreamble(version, knobs) {
694
825
  vars.push(`--k-${name}:${css}`);
695
826
  }
696
827
  const style = vars.length ? ` style="${escapeHtml(vars.join(";"))}"` : "";
697
- return {
698
- htmlTag: `<html lang="en"${attrs.join("")}${style}>`,
699
- headScript: `<script>window.__mockpitDoc=${scriptJson({ version: version ?? null, knobs: values })};</script>`,
700
- };
828
+ return { attrs: `${attrs.join("")}${style}`, values };
701
829
  }
702
830
  // Our own highlight, shipped in the base stylesheet so turning it on is a class
703
831
  // flip rather than a DOM insertion the parts observer would report.
704
- const PART_HIGHLIGHT_CSS = `.mockpit-part-hl{outline:2px solid #8b7bff !important;outline-offset:2px}`;
832
+ 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}`;
705
833
  // Tailwind's browser build, pinned to a major (jsdelivr is already on the CDN
706
834
  // allowlist, so this needs no CSP widening). Injected only when a project's
707
835
  // design settings say `kit: "tailwind"`, i.e. the repo itself is a Tailwind
@@ -1074,6 +1202,9 @@ export function renderHtmlPage(doc) {
1074
1202
  ]),
1075
1203
  `<style>${design.css}${colorSchemeCss(doc.mode)}</style>`,
1076
1204
  ].join("\n");
1205
+ // Slots (fills.ts): only the chosen fill of each shows, keyed on the knob
1206
+ // attribute; with no value yet, the default fill.
1207
+ const fillCss = slotCss(scanFills(doc.html).slots);
1077
1208
  return `<!doctype html>
1078
1209
  ${htmlTag}
1079
1210
  <head>
@@ -1082,7 +1213,9 @@ ${htmlTag}
1082
1213
  <meta http-equiv="Content-Security-Policy" content="${buildCsp(doc.origin)}">
1083
1214
  <title>${escapeHtml(doc.title)}</title>
1084
1215
  ${preamble.headScript}
1216
+ ${scriptTag(doc.origin, KNOBS_PATH)}
1085
1217
  ${styles}
1218
+ ${fillCss ? `<style>${fillCss}</style>` : ""}
1086
1219
  ${design.headScripts}
1087
1220
  ${kits.flatMap((k) => (k.script ? [`<script src="${escapeHtml(k.script)}" defer></script>`] : [])).join("\n")}
1088
1221
  </head>
@@ -1091,7 +1224,6 @@ ${SVG_DEFS}
1091
1224
  ${expandIcons(doc.html, doc.resolveIcon ?? (() => null)).html}
1092
1225
  ${scriptTag(doc.origin, BRIDGE_PATH)}
1093
1226
  ${scriptTag(doc.origin, HIT_TEST_PATH)}
1094
- ${scriptTag(doc.origin, KNOBS_PATH)}
1095
1227
  ${scriptTag(doc.origin, PARTS_PATH)}
1096
1228
  ${kit.js ? `<script>${kit.js}</script>` : ""}
1097
1229
  </body>
@@ -2,6 +2,8 @@ export interface Accent {
2
2
  bg: string;
3
3
  text: string;
4
4
  border: string;
5
+ solid?: string;
6
+ onSolid?: string;
5
7
  }
6
8
  export interface Palette {
7
9
  bg: string;
@@ -21,6 +21,8 @@ function viewerVars(p) {
21
21
  "border-2": p.border2,
22
22
  accent: p.info.text,
23
23
  "accent-bg": p.info.bg,
24
+ "accent-solid": p.info.solid ?? p.info.text,
25
+ "on-accent": p.info.onSolid ?? "#fff",
24
26
  hover: p.hover,
25
27
  danger: p.danger.text,
26
28
  };
@@ -111,7 +113,14 @@ export const DIALKIT = {
111
113
  border: "#e6e6e6",
112
114
  border2: "#d6d6d6",
113
115
  hover: "#ebebeb",
114
- info: { bg: "rgba(37, 99, 235, 0.1)", text: "#2563eb", border: "#2563eb" },
116
+ // White on #2563eb is already 5.2:1, so the fill is the accent itself.
117
+ info: {
118
+ bg: "rgba(37, 99, 235, 0.1)",
119
+ text: "#2563eb",
120
+ border: "#2563eb",
121
+ solid: "#2563eb",
122
+ onSolid: "#fff",
123
+ },
115
124
  success: { bg: "rgba(43, 147, 72, 0.12)", text: "#2b9348", border: "#2b9348" },
116
125
  warning: { bg: "#fff8e6", text: "#e8590c", border: "#f0e0b8" },
117
126
  danger: { bg: "rgba(220, 38, 38, 0.1)", text: "#dc2626", border: "#dc2626" },
@@ -126,7 +135,14 @@ export const DIALKIT = {
126
135
  border: "#242424",
127
136
  border2: "#3a3a3a",
128
137
  hover: "#262626",
129
- info: { bg: "rgba(139, 123, 255, 0.16)", text: "#8b7bff", border: "#8b7bff" },
138
+ // #8b7bff under white text is 3.3:1; the same hue, darker, is 5.0:1.
139
+ info: {
140
+ bg: "rgba(139, 123, 255, 0.16)",
141
+ text: "#8b7bff",
142
+ border: "#8b7bff",
143
+ solid: "#6b5ae0",
144
+ onSolid: "#fff",
145
+ },
130
146
  success: { bg: "rgba(74, 222, 128, 0.14)", text: "#4ade80", border: "#4ade80" },
131
147
  warning: { bg: "#2a2312", text: "#e8590c", border: "#4a3c1c" },
132
148
  danger: { bg: "rgba(248, 113, 113, 0.14)", text: "#f87171", border: "#f87171" },
@@ -238,10 +254,14 @@ function pick(vars, names) {
238
254
  return null;
239
255
  }
240
256
  function accentFrom(vars, bg, text, border, fallback) {
257
+ const ink = pick(vars, text);
241
258
  return {
242
259
  bg: pick(vars, bg) ?? fallback.bg,
243
- text: pick(vars, text) ?? fallback.text,
260
+ text: ink ?? fallback.text,
244
261
  border: pick(vars, border) ?? fallback.border,
262
+ // The default's solid pair belongs to the default's ink; a repo's own ink
263
+ // fills with itself.
264
+ ...(ink ? {} : { solid: fallback.solid, onSolid: fallback.onSolid }),
245
265
  };
246
266
  }
247
267
  function paletteFrom(vars, fallback) {