viberoom 0.6.0 → 0.7.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 (110) hide show
  1. package/NOTICE +14 -0
  2. package/dist/hub.js +45 -4
  3. package/dist/main.js +1 -1
  4. package/dist/recipes.js +2 -1
  5. package/dist/shortcuts.js +1 -1
  6. package/package.json +1 -1
  7. package/ui/app.css +131 -107
  8. package/ui/app.js +334 -87
  9. package/ui/avatars.js +7 -14
  10. package/ui/components.css +116 -21
  11. package/ui/components.js +202 -1
  12. package/ui/fonts/anonymous-pro-cyrillic-400.woff2 +0 -0
  13. package/ui/fonts/anonymous-pro-cyrillic-700.woff2 +0 -0
  14. package/ui/fonts/anonymous-pro-latin-400.woff2 +0 -0
  15. package/ui/fonts/anonymous-pro-latin-700.woff2 +0 -0
  16. package/ui/fonts/anonymous-pro-latin-ext-400.woff2 +0 -0
  17. package/ui/fonts/anonymous-pro-latin-ext-700.woff2 +0 -0
  18. package/ui/fonts/anonymous-pro.css +7 -0
  19. package/ui/fonts/cascadia-code-cyrillic-ext.woff2 +0 -0
  20. package/ui/fonts/cascadia-code-cyrillic.woff2 +0 -0
  21. package/ui/fonts/cascadia-code-latin-ext.woff2 +0 -0
  22. package/ui/fonts/cascadia-code-latin.woff2 +0 -0
  23. package/ui/fonts/cascadia-code.css +5 -0
  24. package/ui/fonts/comfortaa-cyrillic-ext.woff2 +0 -0
  25. package/ui/fonts/comfortaa-cyrillic.woff2 +0 -0
  26. package/ui/fonts/comfortaa-latin-ext.woff2 +0 -0
  27. package/ui/fonts/comfortaa-latin.woff2 +0 -0
  28. package/ui/fonts/comfortaa.css +5 -0
  29. package/ui/fonts/exo-2-cyrillic-ext.woff2 +0 -0
  30. package/ui/fonts/exo-2-cyrillic.woff2 +0 -0
  31. package/ui/fonts/exo-2-latin-ext.woff2 +0 -0
  32. package/ui/fonts/exo-2-latin.woff2 +0 -0
  33. package/ui/fonts/exo-2.css +5 -0
  34. package/ui/fonts/golos-text-cyrillic-ext.woff2 +0 -0
  35. package/ui/fonts/golos-text-cyrillic.woff2 +0 -0
  36. package/ui/fonts/golos-text-latin-ext.woff2 +0 -0
  37. package/ui/fonts/golos-text-latin.woff2 +0 -0
  38. package/ui/fonts/golos-text.css +5 -0
  39. package/ui/fonts/ibm-plex-mono-cyrillic-400.woff2 +0 -0
  40. package/ui/fonts/ibm-plex-mono-cyrillic-600.woff2 +0 -0
  41. package/ui/fonts/ibm-plex-mono-cyrillic-700.woff2 +0 -0
  42. package/ui/fonts/ibm-plex-mono-cyrillic-ext-400.woff2 +0 -0
  43. package/ui/fonts/ibm-plex-mono-cyrillic-ext-600.woff2 +0 -0
  44. package/ui/fonts/ibm-plex-mono-cyrillic-ext-700.woff2 +0 -0
  45. package/ui/fonts/ibm-plex-mono-latin-400.woff2 +0 -0
  46. package/ui/fonts/ibm-plex-mono-latin-600.woff2 +0 -0
  47. package/ui/fonts/ibm-plex-mono-latin-700.woff2 +0 -0
  48. package/ui/fonts/ibm-plex-mono-latin-ext-400.woff2 +0 -0
  49. package/ui/fonts/ibm-plex-mono-latin-ext-600.woff2 +0 -0
  50. package/ui/fonts/ibm-plex-mono-latin-ext-700.woff2 +0 -0
  51. package/ui/fonts/ibm-plex-mono.css +13 -0
  52. package/ui/fonts/ibm-plex-sans-cyrillic-400.woff2 +0 -0
  53. package/ui/fonts/ibm-plex-sans-cyrillic-600.woff2 +0 -0
  54. package/ui/fonts/ibm-plex-sans-cyrillic-700.woff2 +0 -0
  55. package/ui/fonts/ibm-plex-sans-cyrillic-ext-400.woff2 +0 -0
  56. package/ui/fonts/ibm-plex-sans-cyrillic-ext-600.woff2 +0 -0
  57. package/ui/fonts/ibm-plex-sans-cyrillic-ext-700.woff2 +0 -0
  58. package/ui/fonts/ibm-plex-sans-latin-400.woff2 +0 -0
  59. package/ui/fonts/ibm-plex-sans-latin-600.woff2 +0 -0
  60. package/ui/fonts/ibm-plex-sans-latin-700.woff2 +0 -0
  61. package/ui/fonts/ibm-plex-sans-latin-ext-400.woff2 +0 -0
  62. package/ui/fonts/ibm-plex-sans-latin-ext-600.woff2 +0 -0
  63. package/ui/fonts/ibm-plex-sans-latin-ext-700.woff2 +0 -0
  64. package/ui/fonts/ibm-plex-sans.css +13 -0
  65. package/ui/fonts/manrope-cyrillic-ext.woff2 +0 -0
  66. package/ui/fonts/manrope-cyrillic.woff2 +0 -0
  67. package/ui/fonts/manrope-latin-ext.woff2 +0 -0
  68. package/ui/fonts/manrope-latin.woff2 +0 -0
  69. package/ui/fonts/manrope.css +5 -0
  70. package/ui/fonts/montserrat-cyrillic-ext.woff2 +0 -0
  71. package/ui/fonts/montserrat-cyrillic.woff2 +0 -0
  72. package/ui/fonts/montserrat-latin-ext.woff2 +0 -0
  73. package/ui/fonts/montserrat-latin.woff2 +0 -0
  74. package/ui/fonts/montserrat.css +5 -0
  75. package/ui/fonts/open-sans-cyrillic-ext.woff2 +0 -0
  76. package/ui/fonts/open-sans-cyrillic.woff2 +0 -0
  77. package/ui/fonts/open-sans-latin-ext.woff2 +0 -0
  78. package/ui/fonts/open-sans-latin.woff2 +0 -0
  79. package/ui/fonts/open-sans.css +5 -0
  80. package/ui/fonts/pt-mono-cyrillic-ext.woff2 +0 -0
  81. package/ui/fonts/pt-mono-cyrillic.woff2 +0 -0
  82. package/ui/fonts/pt-mono-latin-ext.woff2 +0 -0
  83. package/ui/fonts/pt-mono-latin.woff2 +0 -0
  84. package/ui/fonts/pt-mono.css +5 -0
  85. package/ui/fonts/rubik-cyrillic-ext.woff2 +0 -0
  86. package/ui/fonts/rubik-cyrillic.woff2 +0 -0
  87. package/ui/fonts/rubik-latin-ext.woff2 +0 -0
  88. package/ui/fonts/rubik-latin.woff2 +0 -0
  89. package/ui/fonts/rubik.css +5 -0
  90. package/ui/fonts/source-sans-3-cyrillic-ext.woff2 +0 -0
  91. package/ui/fonts/source-sans-3-cyrillic.woff2 +0 -0
  92. package/ui/fonts/source-sans-3-latin-ext.woff2 +0 -0
  93. package/ui/fonts/source-sans-3-latin.woff2 +0 -0
  94. package/ui/fonts/source-sans-3.css +5 -0
  95. package/ui/fonts/ubuntu-sans-cyrillic-ext.woff2 +0 -0
  96. package/ui/fonts/ubuntu-sans-cyrillic.woff2 +0 -0
  97. package/ui/fonts/ubuntu-sans-latin-ext.woff2 +0 -0
  98. package/ui/fonts/ubuntu-sans-latin.woff2 +0 -0
  99. package/ui/fonts/ubuntu-sans.css +5 -0
  100. package/ui/fonts/victor-mono-cyrillic-ext.woff2 +0 -0
  101. package/ui/fonts/victor-mono-cyrillic.woff2 +0 -0
  102. package/ui/fonts/victor-mono-latin-ext.woff2 +0 -0
  103. package/ui/fonts/victor-mono-latin.woff2 +0 -0
  104. package/ui/fonts/victor-mono.css +5 -0
  105. package/ui/index.html +15 -6
  106. package/ui/looks.css +2446 -0
  107. package/ui/manifest.json +3 -3
  108. package/ui/theme.css +114 -84
  109. package/ui/tokens.js +315 -29
  110. package/ui/ui.js +2 -0
package/ui/avatars.js CHANGED
@@ -2,7 +2,7 @@
2
2
  (() => {
3
3
  "use strict";
4
4
 
5
- const tokens = () => globalThis.VIBEROOM_TOKENS.current.elements.participant;
5
+ const tokens = () => globalThis.VIBEROOM_TOKENS.active().elements.face;
6
6
 
7
7
  function hash(text) {
8
8
  let h = 2166136261;
@@ -52,25 +52,18 @@
52
52
 
53
53
  function avatarHtml(participant, size, options) {
54
54
  const opts = options || {};
55
+ const UI = globalThis.UI;
55
56
  const color = participant.color || tokens().avatarDefault;
56
57
  const s = size || 40;
57
58
  const emoji = participant.avatar;
58
- const label = emoji ? emoji : initials(participant.name);
59
- const fontSize = Math.round(emoji ? s * 0.56 : s * 0.38);
60
- const radius = Math.round(s * 0.32);
61
- const svg = `<span class="av-tile" role="img" aria-label="${escapeAttr(participant.name)}" style="background:color-mix(in srgb, ${escapeAttr(color)} 16%, ${tokens().avatarBlend});color:${escapeAttr(color)};font-size:${fontSize}px;border-radius:${radius}px">${escapeText(label)}</span>`;
62
- const status = opts.status ? `<span class="avatar-status status-${escapeAttr(participant.status || "idle")}"></span>` : "";
63
- let badge = "";
59
+ let badge = null;
64
60
  if (opts.vendor && participant.kind === "agent") {
65
61
  const recipe = opts.recipes ? opts.recipes.find((r) => r.id === participant.agentType) : null;
66
- const letter = escapeText((participant.agentVendor || participant.agentType || "?").slice(0, 1).toUpperCase());
67
- const title = escapeAttr(participant.agentLabel || participant.agentType || "");
68
- badge = recipe && recipe.icon
69
- ? `<span class="avatar-badge" title="${title}"><img src="${escapeAttr(recipe.icon)}" alt="" onerror="this.replaceWith(document.createTextNode('${letter}'))"></span>`
70
- : `<span class="avatar-badge" title="${title}">${letter}</span>`;
62
+ badge = UI.raw(UI.html("logo-tile", { icon: recipe && recipe.icon ? recipe.icon : "", letter: (participant.agentVendor || participant.agentType || "?").slice(0, 1), size: "badge", title: participant.agentLabel || participant.agentType || "" }));
71
63
  }
72
- if (opts.muted && participant.kind === "agent" && window.Icons) badge = `<span class="avatar-badge muted" title="muted: receives no prompts">${window.Icons.svg("mute")}</span>`;
73
- return `<span class="avatar" style="width:${size}px;height:${size}px">${svg}${badge}${status}</span>`;
64
+ if (opts.muted && participant.kind === "agent") badge = UI.raw(UI.html("logo-tile", { glyph: "mute", size: "badge", tone: "muted", title: "muted: receives no prompts" }));
65
+ const status = opts.status === true ? participant.status || "idle" : typeof opts.status === "string" ? opts.status : undefined;
66
+ return UI.html("face", { name: participant.name, label: emoji ? emoji : initials(participant.name), emoji: !!emoji, color, size: s, badge, status, me: !!opts.me, kind: opts.kind || "tile", ring: !!opts.ring, dim: opts.dim, title: opts.title });
74
67
  }
75
68
 
76
69
  const GALLERY = [
package/ui/components.css CHANGED
@@ -28,41 +28,41 @@
28
28
  [data-ui="button"][data-kind="danger"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--btn-danger-hover-bg); color: var(--btn-danger-ink); }
29
29
  [data-ui="button"][data-kind="danger-solid"] { background: var(--btn-danger-ink); color: var(--btn-danger-solid-ink); box-shadow: var(--btn-danger-solid-shadow); }
30
30
  [data-ui="button"][data-kind="danger-solid"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--btn-danger-solid-hover-bg); color: var(--btn-danger-solid-ink); }
31
- [data-ui="button"][data-kind="dark"] { background: var(--btn-dark-bg); color: var(--btn-dark-ink); border-radius: 7px; }
31
+ [data-ui="button"][data-kind="dark"] { background: var(--btn-dark-bg); color: var(--btn-dark-ink); border-radius: calc(7px * var(--r-scale)); }
32
32
  [data-ui="button"][data-kind="dark"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--btn-dark-hover-bg); color: var(--btn-dark-ink); }
33
33
  [data-ui="button"][data-kind="warn"] { background: transparent; color: var(--btn-warn-ink); letter-spacing: 0.02em; border-radius: var(--r-pill); }
34
34
  [data-ui="button"][data-kind="warn"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--btn-warn-hover-bg); color: var(--btn-warn-ink); }
35
- [data-ui="button"][data-kind="inverse"] { background: var(--btn-inverse-bg); color: var(--btn-inverse-ink); border-radius: 10px; }
35
+ [data-ui="button"][data-kind="inverse"] { background: var(--btn-inverse-bg); color: var(--btn-inverse-ink); border-radius: calc(10px * var(--r-scale)); }
36
36
  [data-ui="button"][data-kind="inverse"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--btn-inverse-hover-bg); color: var(--btn-inverse-ink); }
37
37
  [data-ui="button"][data-kind="link"] { background: none; color: var(--btn-link-ink); height: auto; padding: 2px 0; border-radius: 0; }
38
38
  [data-ui="button"][data-kind="link"]:is(:hover, [data-state="hover"]):not(:disabled) { background: none; color: var(--btn-link-ink); text-decoration: underline; }
39
- [data-ui="button"][data-kind="ok"] { background: var(--btn-ok-bg); color: var(--btn-ok-ink); border-radius: 10px; }
39
+ [data-ui="button"][data-kind="ok"] { background: var(--btn-ok-bg); color: var(--btn-ok-ink); border-radius: calc(10px * var(--r-scale)); }
40
40
  [data-ui="button"][data-kind="ok"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--btn-ok-hover-bg); color: var(--btn-ok-ink); transform: translateY(-1px); }
41
- [data-ui="button"][data-kind="paper"] { background: var(--btn-paper-bg); color: var(--btn-paper-ink); border-radius: 10px; }
41
+ [data-ui="button"][data-kind="paper"] { background: var(--btn-paper-bg); color: var(--btn-paper-ink); border-radius: calc(10px * var(--r-scale)); }
42
42
  [data-ui="button"][data-kind="paper"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--btn-paper-hover-bg); color: var(--btn-paper-ink); transform: translateY(-1px); }
43
- [data-ui="button"][data-size="sm"] { height: var(--btn-h-sm); padding: 0 12px; font-size: var(--btn-fs-sm); border-radius: 10px; }
43
+ [data-ui="button"][data-size="sm"] { height: var(--btn-h-sm); padding: 0 12px; font-size: var(--btn-fs-sm); border-radius: calc(10px * var(--r-scale)); }
44
44
  [data-ui="button"][data-size="sm"] .i { width: 15px; height: 15px; }
45
- [data-ui="button"][data-size="xs"] { height: var(--btn-h-xs); padding: 0 9px; font-size: var(--fs-xs); border-radius: 7px; }
45
+ [data-ui="button"][data-size="xs"] { height: var(--btn-h-xs); padding: 0 9px; font-size: var(--fs-xs); border-radius: calc(7px * var(--r-scale)); }
46
46
  [data-ui="button"][data-size="xs"] .i { width: 13px; height: 13px; }
47
- [data-ui="button"][data-size="lg"] { height: var(--btn-h-lg); padding: 0 22px; font-size: var(--btn-fs-lg); border-radius: 14px; }
47
+ [data-ui="button"][data-size="lg"] { height: var(--btn-h-lg); padding: 0 22px; font-size: var(--btn-fs-lg); border-radius: calc(14px * var(--r-scale)); }
48
48
  [data-ui="button"][data-size="cta"] { height: var(--btn-h-cta); border-radius: var(--r-md); font-size: var(--btn-fs-cta); padding: 0 18px; }
49
49
  [data-ui="button"][data-full] { width: 100%; }
50
50
  [data-ui="button"][data-pill] { border-radius: var(--r-pill); }
51
51
  [data-ui="button"][data-state="loading"] { position: relative; color: transparent !important; pointer-events: none; }
52
- [data-ui="button"][data-state="loading"]::after { content: ""; position: absolute; width: 15px; height: 15px; border-radius: 50%; border: 2px solid var(--btn-spinner-track); border-top-color: var(--btn-spinner-head); animation: spin 0.8s linear infinite; }
52
+ [data-ui="button"][data-state="loading"]::after { content: ""; position: absolute; width: 15px; height: 15px; border-radius: calc(50% * var(--r-scale)); border: 2px solid var(--btn-spinner-track); border-top-color: var(--btn-spinner-head); animation: spin 0.8s linear infinite; }
53
53
  [data-ui="button"][data-state="loading"]:not([data-kind="primary"]):not([data-kind="danger-solid"])::after { border-color: var(--btn-spinner-track-quiet); border-top-color: var(--btn-spinner-head-quiet); }
54
- [data-ui="icon-button"] { width: var(--icon-button-size); height: var(--icon-button-size); border-radius: 12px; border: 0; background: transparent; color: var(--icon-button-ink); display: inline-flex; align-items: center; justify-content: center; padding: 0; flex: none; cursor: pointer; transition: background var(--t-fast), color var(--t-fast), transform var(--t-fast) var(--ease-out); }
54
+ [data-ui="icon-button"] { width: var(--icon-button-size); height: var(--icon-button-size); border-radius: calc(12px * var(--r-scale)); border: 0; background: transparent; color: var(--icon-button-ink); display: inline-flex; align-items: center; justify-content: center; padding: 0; flex: none; cursor: pointer; transition: background var(--t-fast), color var(--t-fast), transform var(--t-fast) var(--ease-out); }
55
55
  [data-ui="icon-button"] .i { width: var(--icon-button-icon); height: var(--icon-button-icon); }
56
56
  [data-ui="icon-button"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--icon-button-hover-bg); color: var(--icon-button-hover-ink); }
57
57
  [data-ui="icon-button"]:is(:active, [data-state="active"]):not(:disabled) { transform: translateY(1px); }
58
58
  [data-ui="icon-button"]:is(:disabled, [data-state="disabled"]) { opacity: var(--icon-button-disabled-opacity); cursor: default; }
59
- [data-ui="icon-button"][data-size="sm"] { width: var(--icon-button-size-sm); height: var(--icon-button-size-sm); border-radius: 10px; }
59
+ [data-ui="icon-button"][data-size="sm"] { width: var(--icon-button-size-sm); height: var(--icon-button-size-sm); border-radius: calc(10px * var(--r-scale)); }
60
60
  [data-ui="icon-button"][data-size="sm"] .i { width: var(--icon-button-icon-sm); height: var(--icon-button-icon-sm); }
61
- [data-ui="icon-button"][data-kind="primary"] { width: var(--icon-button-size-primary); height: var(--icon-button-size-primary); border-radius: 14px; background: var(--grad-primary); color: var(--btn-on-primary); box-shadow: var(--shadow-primary); }
61
+ [data-ui="icon-button"][data-kind="primary"] { width: var(--icon-button-size-primary); height: var(--icon-button-size-primary); border-radius: calc(14px * var(--r-scale)); background: var(--grad-primary); color: var(--btn-on-primary); box-shadow: var(--shadow-primary); }
62
62
  [data-ui="icon-button"][data-kind="primary"]:is(:hover, [data-state="hover"]):not(:disabled) { color: var(--btn-on-primary); background: var(--grad-primary); filter: brightness(1.05); }
63
63
  [data-ui="icon-button"][data-kind="danger"] { color: var(--btn-danger-ink); }
64
64
  [data-ui="icon-button"][data-kind="danger"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--btn-danger-bg); color: var(--btn-danger-ink); }
65
- [data-ui="icon-button"][data-size="xs"] { width: var(--icon-button-size-xs); height: var(--icon-button-size-xs); border-radius: 6px; }
65
+ [data-ui="icon-button"][data-size="xs"] { width: var(--icon-button-size-xs); height: var(--icon-button-size-xs); border-radius: calc(6px * var(--r-scale)); }
66
66
  [data-ui="icon-button"][data-size="xs"] .i { width: var(--icon-button-icon-xs); height: var(--icon-button-icon-xs); }
67
67
  [data-ui="icon-button"][data-kind="ghost"] { background: transparent; color: var(--icon-button-ghost-ink); }
68
68
  [data-ui="icon-button"][data-kind="inline"] { background: var(--icon-button-inline-bg); color: var(--icon-button-inline-ink); vertical-align: middle; position: relative; top: -1px; margin-left: 5px; }
@@ -73,7 +73,7 @@
73
73
  [data-ui="choice"][data-state="on"] { background: var(--choice-hover-bg); color: var(--choice-on-ink); border-color: var(--choice-on-border); }
74
74
  [data-ui="choice"][data-quiet] { color: var(--choice-quiet-ink); font-weight: 700; }
75
75
  [data-ui="badge"] { display: inline-flex; align-items: center; gap: 4px; font-size: var(--fs-xs); padding: 3px 10px; border-radius: var(--r-pill); background: var(--badge-bg); color: var(--badge-ink); font-weight: 800; white-space: nowrap; vertical-align: middle; }
76
- [data-ui="badge"] .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
76
+ [data-ui="badge"] .dot { width: 6px; height: 6px; border-radius: calc(50% * var(--r-scale)); background: currentColor; }
77
77
  [data-ui="badge"][data-tone="ready"] { background: var(--st-ready); color: var(--st-ready-ink); }
78
78
  [data-ui="badge"][data-tone="waiting"] { background: var(--st-waiting); color: var(--st-waiting-ink); }
79
79
  [data-ui="badge"][data-tone="thinking"] { background: var(--st-thinking); color: var(--st-thinking-ink); }
@@ -93,7 +93,7 @@
93
93
  [data-ui="chip"][data-tone="waiting"] { background: var(--st-waiting); color: var(--st-waiting-ink); }
94
94
  [data-ui="chip"][data-button] { cursor: pointer; transition: background var(--t-fast); }
95
95
  [data-ui="chip"][data-button]:not([data-tone]):is(:hover, [data-state="hover"]) { background: var(--chip-hover-bg); }
96
- [data-ui="file-card"] { border-radius: 12px; overflow: hidden; background: var(--file-card-bg); box-shadow: var(--edge); }
96
+ [data-ui="file-card"] { border-radius: calc(12px * var(--r-scale)); overflow: hidden; background: var(--file-card-bg); box-shadow: var(--edge); }
97
97
  [data-ui="file-card"] > .head { display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--file-card-head-bg); color: var(--file-card-head-ink); font-size: var(--fs-xs); font-weight: 800; }
98
98
  [data-ui="file-card"] > .head .name { display: inline-flex; align-items: center; gap: 5px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
99
99
  [data-ui="file-card"] > .head .name .i { width: 13px; height: 13px; flex: none; }
@@ -103,13 +103,13 @@
103
103
  [data-ui="file-card"] > .body .code-view { border-radius: 0; max-height: 320px; }
104
104
  [data-ui="file-card"] > .body .note { margin: 8px 10px; color: var(--file-card-note-ink); font-size: var(--fs-sm); font-weight: 700; line-height: 1.45; }
105
105
  [data-ui="file-card"][data-kind="image"] > .body { padding: 8px 10px 10px; background: var(--file-card-image-body-bg); }
106
- [data-ui="file-card"][data-kind="image"] > .body img { display: block; margin: 0 auto; max-width: 100%; max-height: 420px; border-radius: 8px; cursor: zoom-in; background: var(--file-card-image-bg); }
106
+ [data-ui="file-card"][data-kind="image"] > .body img { display: block; margin: 0 auto; max-width: 100%; max-height: 420px; border-radius: calc(8px * var(--r-scale)); cursor: zoom-in; background: var(--file-card-image-bg); }
107
107
  [data-ui="tool-call"] { display: inline-flex; flex-direction: column; max-width: 100%; min-width: 0; }
108
108
  [data-ui="tool-call"] > [data-ui="chip"]::after { content: "▾"; margin-left: auto; padding-left: 8px; opacity: 0.55; transition: transform var(--t-fast); }
109
109
  [data-ui="tool-call"][data-state="open"] { flex-basis: 100%; }
110
- [data-ui="tool-call"][data-state="open"] > [data-ui="chip"] { width: 100%; border-radius: 12px 12px 0 0; padding: 6px 12px; white-space: normal; }
110
+ [data-ui="tool-call"][data-state="open"] > [data-ui="chip"] { width: 100%; border-radius: calc(12px * var(--r-scale)) calc(12px * var(--r-scale)) 0 0; padding: 6px 12px; white-space: normal; }
111
111
  [data-ui="tool-call"][data-state="open"] > [data-ui="chip"]::after { transform: rotate(180deg); }
112
- [data-ui="tool-call"] > .body { margin: 0; padding: 8px 10px 10px; background: var(--tool-call-body-bg); border: 2px solid var(--tool-call-pending-bg); border-top: 0; border-radius: 0 0 12px 12px; font-size: var(--tool-call-fs); max-height: 340px; overflow: auto; }
112
+ [data-ui="tool-call"] > .body { margin: 0; padding: 8px 10px 10px; background: var(--tool-call-body-bg); border: 2px solid var(--tool-call-pending-bg); border-top: 0; border-radius: 0 0 calc(12px * var(--r-scale)) calc(12px * var(--r-scale)); font-size: var(--tool-call-fs); max-height: 340px; overflow: auto; }
113
113
  [data-ui="tool-call"][data-status="completed"] > .body { border-color: var(--tool-call-ready-bg); }
114
114
  [data-ui="tool-call"][data-status="failed"] > .body { border-color: var(--tool-call-error-bg); }
115
115
  [data-ui="tool-call"][data-status="in_progress"] > .body { border-color: var(--tool-call-thinking-bg); }
@@ -119,7 +119,7 @@
119
119
  [data-ui="tool-call"][data-status="failed"] .sec b { color: var(--tool-call-error-ink); }
120
120
  [data-ui="tool-call"][data-status="in_progress"] .sec b { color: var(--tool-call-thinking-ink); }
121
121
  [data-ui="tool-call"] .sec.quiet { color: var(--tool-call-quiet-ink); font-style: italic; }
122
- [data-ui="tool-call"] .sec pre { margin: 3px 0 0; padding: 6px 8px; border-radius: 8px; background: var(--tool-call-pre-bg); color: var(--tool-call-pre-ink); white-space: pre-wrap; word-break: break-word; font-size: var(--tool-call-pre-fs); line-height: 1.45; }
122
+ [data-ui="tool-call"] .sec pre { margin: 3px 0 0; padding: 6px 8px; border-radius: calc(8px * var(--r-scale)); background: var(--tool-call-pre-bg); color: var(--tool-call-pre-ink); white-space: pre-wrap; word-break: break-word; font-size: var(--tool-call-pre-fs); line-height: 1.45; }
123
123
  [data-ui="tool-fold"] { flex-basis: 100%; min-width: 0; }
124
124
  [data-ui="tool-fold"] > summary { list-style: none; display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none; font-size: var(--tool-fold-fs); font-weight: 800; color: var(--tool-fold-ink); background: var(--tool-fold-bg); border-radius: var(--r-pill); padding: 4px 12px; transition: background var(--t-fast), color var(--t-fast); }
125
125
  [data-ui="tool-fold"] > summary::-webkit-details-marker { display: none; }
@@ -129,12 +129,107 @@
129
129
  [data-ui="tool-fold"][open] > summary::after { transform: rotate(180deg); }
130
130
  [data-ui="tool-fold"] > summary .failed { color: var(--tool-fold-failed-ink); }
131
131
  [data-ui="tool-fold"] > .list { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
132
- [data-ui="ask-card"] { background: var(--ask-card-bg); color: var(--ask-card-ink); border-radius: 12px; padding: 10px 12px; font-size: var(--ask-card-fs); font-weight: 600; }
132
+ [data-ui="ask-card"] { background: var(--ask-card-bg); color: var(--ask-card-ink); border-radius: calc(12px * var(--r-scale)); padding: 10px 12px; font-size: var(--ask-card-fs); font-weight: 600; }
133
133
  [data-ui="ask-card"][data-state="resolved"] { opacity: var(--ask-card-resolved-opacity); }
134
134
  [data-ui="ask-card"] > .title .kind { font-size: var(--ask-card-kind-fs); color: var(--ask-card-quiet-ink); }
135
135
  [data-ui="ask-card"] > .input { margin: 6px 0; max-height: 160px; overflow: auto; font-size: var(--ask-card-input-fs); }
136
136
  [data-ui="ask-card"] > .choices { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
137
137
  [data-ui="ask-card"] > .choices .outcome { color: var(--ask-card-quiet-ink); font-size: var(--ask-card-outcome-fs); }
138
- [data-ui="reply-note"] { display: flex; align-items: flex-start; gap: 6px; font-size: var(--reply-note-fs); font-weight: 700; color: var(--reply-note-ink); background: var(--reply-note-bg); border-radius: 10px; padding: 5px 10px; }
139
- [data-ui="reply-note"] .i { width: 14px; height: 14px; flex: none; margin-top: 1px; }
138
+ [data-ui="logo-tile"] { position: relative; display: inline-grid; place-items: center; width: var(--logo-tile-md); height: var(--logo-tile-md); flex: none; border-radius: calc(12px * var(--r-scale)); background: var(--logo-tile-bg); color: var(--logo-tile-ink); box-shadow: var(--shadow-tile); font-weight: 800; font-size: 14px; overflow: hidden; user-select: none; }
139
+ [data-ui="logo-tile"] .mark { width: 60%; height: 60%; background: currentColor; -webkit-mask: var(--logo) center / contain no-repeat; mask: var(--logo) center / contain no-repeat; }
140
+ [data-ui="logo-tile"] .i { width: 66%; height: 66%; }
141
+ [data-ui="logo-tile"][data-size="sm"] { width: var(--logo-tile-sm); height: var(--logo-tile-sm); border-radius: calc(10px * var(--r-scale)); font-size: 12px; }
142
+ [data-ui="logo-tile"][data-size="lg"] { width: var(--logo-tile-lg); height: var(--logo-tile-lg); border-radius: calc(13px * var(--r-scale)); font-size: 16px; }
143
+ [data-ui="logo-tile"][data-size="badge"] { width: 18px; height: 18px; min-width: 15px; min-height: 15px; box-sizing: border-box; border-radius: calc(6px * var(--r-scale)); background: var(--logo-tile-badge-bg); border: 2px solid var(--logo-tile-badge-border); color: var(--logo-tile-badge-ink); font-size: 9px; }
144
+ [data-ui="logo-tile"][data-size="badge"] .mark { width: 72%; height: 72%; }
145
+ [data-ui="logo-tile"][data-size="badge"] .i { width: 78%; height: 78%; }
146
+ [data-ui="logo-tile"][data-tone="muted"] { background: var(--logo-tile-muted-bg); color: var(--logo-tile-muted-ink); border-color: var(--logo-tile-badge-border); }
147
+ [data-ui="face"] { position: relative; display: inline-block; flex: none; }
148
+ [data-ui="face"] .tile { display: grid; place-items: center; width: 100%; height: 100%; font-weight: 800; line-height: 1; font-family: var(--font); user-select: none; color: var(--face-color); background: color-mix(in srgb, var(--face-color) var(--face-tint), var(--face-paper)); border-radius: calc(var(--face-size) * var(--face-corner) * var(--r-scale)); }
149
+ [data-ui="face"] > [data-ui="logo-tile"] { position: absolute; right: -4px; bottom: -4px; width: 44%; height: 44%; z-index: 2; }
150
+ [data-ui="face"] .status { position: absolute; left: -3px; bottom: -3px; width: 12px; height: 12px; z-index: 2; border-radius: calc(50% * var(--r-scale)); border: 2px solid var(--face-ring); background: var(--face-dot-bg); }
151
+ [data-ui="face"] .status[data-status="idle"] { background: var(--st-ready-dot); }
152
+ [data-ui="face"] .status[data-status="thinking"] { background: var(--st-thinking-dot); animation: pulse 1.2s ease-in-out infinite; }
153
+ [data-ui="face"] .status[data-status="writing"] { background: var(--st-writing-dot); animation: pulse 1.2s ease-in-out infinite; }
154
+ [data-ui="face"] .status:is([data-status="offline"], [data-status="left"]) { background: var(--st-asleep-dot); }
155
+ [data-ui="face"] .status[data-status="error"] { background: var(--st-error-dot); }
156
+ [data-ui="face"] .status:is([data-status="starting"], [data-status="queued"]) { background: var(--st-waiting-dot); }
157
+ [data-ui="face"] .status[data-status="unstaffed"] { background: var(--attention); }
158
+ [data-ui="face"][data-me] .tile { box-shadow: 0 0 0 2px var(--face-ring), 0 0 0 4px var(--face-me-ring); }
159
+ [data-ui="face"][data-ring] .tile { box-shadow: 0 0 0 2px var(--face-ring); }
160
+ [data-ui="face"][data-dim="asleep"] .tile { filter: grayscale(0.6); opacity: 0.8; }
161
+ [data-ui="face"][data-dim="unstaffed"] .tile { filter: grayscale(0.8); opacity: 0.55; }
162
+ [data-ui="face"][data-kind="card"] .tile { background: var(--face-card-bg); border-radius: calc(26px * var(--r-scale)); box-shadow: var(--shadow-pop); }
163
+ [data-ui="face"][data-kind="bare"] .tile { background: transparent; }
164
+ [data-ui="room-mark"] { width: 44px; height: 44px; border-radius: calc(14px * var(--r-scale)); display: grid; place-content: center; color: var(--room-mark-ink); background: linear-gradient(var(--room-mark-grad-angle), hsl(var(--room-hue) var(--room-mark-grad-from)), hsl(var(--room-hue-2) var(--room-mark-grad-to))); font-weight: 800; font-size: 18px; flex: none; text-transform: uppercase; user-select: none; }
165
+ [data-ui="room-mark"][data-size="lg"] { width: 52px; height: 52px; border-radius: calc(16px * var(--r-scale)); font-size: 22px; }
166
+ [data-ui="room-mark"][data-kind="emoji"] { background: hsl(var(--room-hue) var(--room-mark-emoji)); font-size: 24px; text-transform: none; }
167
+ [data-ui="room-mark"][data-kind="emoji"][data-size="lg"] { font-size: 28px; }
168
+ [data-ui="room-mark"][data-kind="accent"] { background: var(--grad-primary); }
169
+ [data-ui="room-mark"] .i { width: 22px; height: 22px; }
170
+ [data-ui="unseen-line"] { display: flex; align-items: center; gap: 10px; margin: 4px 0; }
171
+ [data-ui="unseen-line"] .line { flex: 1; height: var(--unseen-line-line-h); background: var(--unseen-line-line); opacity: var(--unseen-line-line-opacity); }
172
+ [data-ui="unseen-line"] .label { color: var(--unseen-line-ink); background: var(--unseen-line-bg); border-radius: var(--r-pill); padding: 4px 12px; font-size: var(--fs-xs); font-weight: 800; white-space: nowrap; max-width: 70%; overflow: hidden; text-overflow: ellipsis; }
173
+ [data-ui="number-field"] { position: relative; display: inline-flex; width: 100%; min-width: 0; vertical-align: middle; }
174
+ [data-ui="number-field"] > input { flex: 1; min-width: 0; padding-right: calc(var(--number-field-step-w) + 10px) !important; appearance: textfield; -moz-appearance: textfield; }
175
+ [data-ui="number-field"] > input::-webkit-inner-spin-button, [data-ui="number-field"] > input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
176
+ [data-ui="number-field"] > .steps { position: absolute; right: 5px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 1px; width: var(--number-field-step-w); }
177
+ [data-ui="number-field"] > .steps > button { height: 14px; padding: 0; border: 0; background: transparent; color: var(--number-field-step-ink); cursor: pointer; border-radius: calc(5px * var(--r-scale)); display: grid; place-items: center; transition: background var(--t-fast), color var(--t-fast); }
178
+ [data-ui="number-field"] > .steps > button::before { content: ""; width: 0; height: 0; border-left: 4px solid transparent; border-right: 4px solid transparent; }
179
+ [data-ui="number-field"] > .steps > button[data-act="up"]::before { border-bottom: 5px solid currentColor; }
180
+ [data-ui="number-field"] > .steps > button[data-act="down"]::before { border-top: 5px solid currentColor; }
181
+ [data-ui="number-field"] > .steps > button:hover { background: var(--number-field-step-hover-bg); color: var(--number-field-step-hover-ink); }
182
+ [data-ui="number-field"]:is([data-state="disabled"], :has(> input:disabled)) > .steps { opacity: 0.4; pointer-events: none; }
183
+ [data-ui="adjust-row"] { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 7px 0; }
184
+ [data-ui="adjust-row"] .what { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
185
+ [data-ui="adjust-row"] .what .label { font-size: var(--fs-sm); font-weight: 800; color: var(--ink); }
186
+ [data-ui="adjust-row"] .what .hint { font-size: var(--fs-xs); font-weight: 600; color: var(--adjust-row-hint-ink); margin: 0; }
187
+ [data-ui="adjust-row"] .ctl { display: inline-flex; align-items: center; gap: 8px; flex: none; }
188
+ [data-ui="adjust-row"][data-kind="colour"] .ctl > input[type="color"] { appearance: none; -webkit-appearance: none; width: 30px; height: 30px; min-width: 0; padding: 0; border: 2px solid var(--adjust-row-swatch-border); border-radius: calc(9px * var(--r-scale)); background: none; cursor: pointer; }
189
+ [data-ui="adjust-row"][data-kind="colour"] .ctl > input[type="color"]::-webkit-color-swatch-wrapper { padding: 0; }
190
+ [data-ui="adjust-row"][data-kind="colour"] .ctl > input[type="color"]::-webkit-color-swatch { border: 0; border-radius: calc(6px * var(--r-scale)); }
191
+ [data-ui="adjust-row"][data-kind="scale"] .ctl > input[type="range"] { width: 120px; height: auto; min-width: 0; padding: 0; border: 0; background: none; accent-color: var(--primary); cursor: pointer; }
192
+ [data-ui="adjust-row"] .ctl .value { font-family: var(--mono); font-size: var(--fs-xs); font-weight: 700; color: var(--adjust-row-value-ink); min-width: 58px; text-align: right; white-space: nowrap; padding: 0; background: none; overflow-wrap: normal; }
193
+ [data-ui="adjust-row"] .ctl .back { display: inline-grid; place-items: center; width: 24px; height: 24px; padding: 0; border: 0; border-radius: calc(8px * var(--r-scale)); background: transparent; color: var(--adjust-row-back-ink); cursor: pointer; visibility: hidden; }
194
+ [data-ui="adjust-row"] .ctl .back .i { width: 14px; height: 14px; }
195
+ [data-ui="adjust-row"] .ctl .back:hover { background: var(--adjust-row-back-hover-bg); color: var(--ink); }
196
+ [data-ui="adjust-row"][data-state="changed"] .ctl .back { visibility: visible; }
197
+ [data-ui="adjust-row"][data-state="changed"] .ctl .value { color: var(--adjust-row-changed-ink); }
198
+ [data-look="terminal"] [data-ui="button"] { border: 1px solid currentColor; text-transform: uppercase; letter-spacing: 0.06em; }
199
+ [data-look="terminal"] [data-ui="button"][data-kind="link"] { border: 0; text-transform: none; letter-spacing: 0; text-decoration: underline; }
200
+ [data-look="terminal"] [data-ui="badge"] { background: transparent; border: 1px solid currentColor; text-transform: uppercase; letter-spacing: 0.04em; }
201
+ [data-look="terminal"] [data-ui="chip"] { background: transparent; border: 1px solid currentColor; }
202
+ [data-look="terminal"] [data-ui="choice"] { border-color: currentColor; }
203
+ [data-look="terminal"] [data-ui="hub-row"]::before { content: ">"; margin-right: 6px; opacity: 0.6; }
204
+ [data-look="terminal"] [data-ui="unseen-line"] .label { background: transparent; border: 1px solid currentColor; text-transform: uppercase; letter-spacing: 0.04em; }
205
+ [data-look="plush"] :is([data-ui="button"], [data-ui="icon-button"], [data-ui="row-button"], [data-ui="logo-tile"], [data-ui="look-card"], [data-ui="file-card"], [data-ui="ask-card"], [data-ui="tool-fold"] > summary) { box-shadow: var(--shadow-tile); background-image: var(--bevel); }
206
+ [data-look="plush"] :is([data-ui="chip"], [data-ui="badge"], [data-ui="choice"], [data-ui="hub-row"][data-ref], [data-ui="unseen-line"] .label) { box-shadow: var(--shadow-soft); background-image: var(--bevel); }
207
+ [data-look="plush"] [data-ui="room-mark"] { box-shadow: var(--shadow-soft); }
208
+ [data-look="plush"] :is([data-ui="button"], [data-ui="icon-button"], [data-ui="chip"], [data-ui="badge"], [data-ui="choice"]) { border-radius: var(--r-pill); }
209
+ [data-look="plush"] [data-ui="button"]:is([data-kind="primary"], [data-kind="ok"], [data-kind="danger-solid"], [data-kind="warn"]) { box-shadow: var(--shadow-primary); }
210
+ [data-look="plush"] [data-ui="button"][data-kind="primary"] { background-image: var(--bevel), var(--grad-primary); }
211
+ [data-look="plush"] [data-ui="icon-button"]:is([data-kind="ghost"], [data-kind="plain"]), [data-look="plush"] [data-ui="button"]:is([data-kind="ghost"], [data-kind="plain"], [data-kind="soft"], [data-kind="secondary"], [data-kind="paper"]) { background-color: var(--btn-bg); }
212
+ [data-look="plush"] :is([data-ui="button"], [data-ui="icon-button"], [data-ui="chip"], [data-ui="choice"]):is(:hover, [data-state="hover"]):not(:disabled) { box-shadow: var(--shadow-ctl-hover); background-image: var(--bevel); transform: translateY(-1px); }
213
+ [data-look="plush"] [data-ui="button"][data-kind="primary"]:is(:hover, [data-state="hover"]):not(:disabled) { box-shadow: var(--shadow-primary); background-image: var(--bevel), var(--grad-primary); transform: translateY(-1px); filter: none; }
214
+ [data-look="plush"] :is([data-ui="button"], [data-ui="icon-button"], [data-ui="row-button"], [data-ui="choice"], [data-ui="chip"]):is(:active, [data-state="active"]):not(:disabled) { box-shadow: var(--shadow-inset); background-image: none; transform: translateY(1px); }
215
+ [data-look="plush"] [data-ui="choice"]:is([data-state="on"], [aria-pressed="true"]) { box-shadow: var(--shadow-inset); background-image: none; }
216
+ [data-look="plush"] :is([data-ui="number-field"] > input, [data-ui="adjust-row"] .ctl > input[type="color"]) { box-shadow: var(--input-shadow); }
217
+ [data-look="plush"] [data-ui="face"] .tile { box-shadow: var(--shadow-soft); background-image: var(--bevel); }
218
+ [data-look="plush"] [data-ui="room-mark"][data-kind] { border-radius: calc(10px * var(--r-scale)); }
219
+ [data-look="plush"] [data-ui="room-mark"][data-kind][data-size="lg"] { border-radius: calc(12px * var(--r-scale)); }
220
+ [data-look="plush"] [data-ui="logo-tile"] { border-radius: calc(8px * var(--r-scale)); }
221
+ [data-look="plush"] [data-ui="logo-tile"][data-size="sm"] { border-radius: calc(7px * var(--r-scale)); }
222
+ [data-look="plush"] [data-ui="logo-tile"][data-size="lg"] { border-radius: calc(9px * var(--r-scale)); }
223
+ [data-look="plush"] [data-ui="face"][data-me] .tile { box-shadow: 0 0 0 2px var(--face-ring), 0 0 0 4px var(--face-me-ring), var(--shadow-soft); }
224
+ [data-look="terminal"] :is([data-ui="button"], [data-ui="icon-button"], [data-ui="row-button"], [data-ui="choice"], [data-ui="badge"], [data-ui="chip"], [data-ui="hub-row"], [data-ui="file-card"], [data-ui="ask-card"], [data-ui="reply-note"], [data-ui="tool-fold"] > summary, [data-ui="tool-call"] > [data-ui="chip"], [data-ui="tool-call"] > .body) { border-radius: 0; }
225
+ [data-ui="look-card"] { display: inline-flex; flex-direction: column; align-items: stretch; gap: 6px; width: var(--look-card-w); padding: 6px; border: 2px solid var(--look-card-border); border-radius: var(--r-md); background: var(--look-card-bg); color: var(--look-card-ink); cursor: pointer; font: inherit; text-align: center; transition: border-color var(--t-fast), transform var(--t-fast) var(--ease-out); }
226
+ [data-ui="look-card"] .paper { position: relative; display: block; height: var(--look-card-paper-h); overflow: hidden; }
227
+ [data-ui="look-card"] .reply { position: absolute; left: 10px; top: 10px; width: 50%; height: 24px; display: flex; align-items: center; padding: 0 8px; font-size: 11px; font-weight: 800; }
228
+ [data-ui="look-card"] .mine { position: absolute; right: 12px; top: 42px; width: 36%; height: 18px; }
229
+ [data-ui="look-card"] .accent { position: absolute; left: 10px; bottom: 10px; width: 34px; height: 12px; }
230
+ [data-ui="look-card"] .name { font-size: var(--fs-sm); font-weight: 800; }
231
+ [data-ui="look-card"]:is(:hover, [data-state="hover"]) { border-color: var(--look-card-hover-border); transform: translateY(-1px); }
232
+ [data-ui="look-card"]:is([data-state="on"], [aria-pressed="true"]) { border-color: var(--look-card-on-border); }
233
+ [data-ui="reply-note"] { display: flex; align-items: flex-start; gap: 6px; font-size: var(--reply-note-fs); font-weight: 700; line-height: 1.5; color: var(--reply-note-ink); background: var(--reply-note-bg); border-radius: calc(10px * var(--r-scale)); padding: 5px 10px; }
234
+ [data-ui="reply-note"] .i { width: 14px; height: 14px; flex: none; margin-top: calc((1.5em - 14px) / 2); }
140
235
  [data-ui="reply-note"][data-tone="attention"] { color: var(--reply-note-attention-ink); background: var(--reply-note-attention-bg); }
package/ui/components.js CHANGED
@@ -3,7 +3,170 @@
3
3
  "use strict";
4
4
  const UI = globalThis.UI;
5
5
 
6
+ UI.define("logo-tile", {
7
+ group: "logoTile",
8
+ describe: "A vendor's mark on a tile: the vendor's own drawing laid over the look's ink through a mask, so it reads on any paper; a letter stands in where there is no drawing, a glyph from the icon set for the marks the room adds (muted). As a badge it sits in a face's corner.",
9
+ props: {
10
+ icon: { type: "string", note: "the drawing's url (a recipe's icon); empty for a letter" },
11
+ letter: { type: "string", note: "the letter shown without a drawing" },
12
+ glyph: { type: "icon", note: "an icon from the set instead of a drawing (the mute mark)" },
13
+ size: { type: "enum", values: ["sm", "md", "lg", "badge"], default: "md", note: "badge: the corner of a face, sized by the face" },
14
+ tone: { type: "enum", values: ["plain", "muted"], default: "plain" },
15
+ title: { type: "string" },
16
+ },
17
+ build: ({ icon, letter, glyph, size, tone, title }, ui) =>
18
+ ui.h("span", { "data-size": size, "data-tone": tone !== "plain" ? tone : null, title, style: icon ? `--logo:url("${icon}")` : null, role: title ? "img" : null, "aria-label": title || null },
19
+ icon ? ui.h("span", { class: "mark" }) : glyph ? ui.icon(glyph) : ui.h("span", { class: "letter" }, (letter || "?").slice(0, 1).toUpperCase())),
20
+ states: ["rest"],
21
+ samples: [
22
+ { label: "a drawing", props: { icon: "/vendor-icons/claude.svg", title: "Claude" } },
23
+ { label: "another", props: { icon: "/vendor-icons/codex.svg", size: "lg", title: "Codex" } },
24
+ { label: "a letter", props: { letter: "F", size: "sm", title: "Fake" } },
25
+ { label: "a badge", props: { icon: "/vendor-icons/gemini.svg", size: "badge", title: "Gemini" } },
26
+ { label: "muted", props: { glyph: "mute", size: "badge", tone: "muted", title: "muted: receives no prompts" } },
27
+ ],
28
+ });
29
+
30
+ const FACE_STATUSES = ["idle", "queued", "starting", "thinking", "writing", "error", "offline", "left", "unstaffed"];
31
+ UI.define("face", {
32
+ group: "face",
33
+ describe: "A participant's face: a tile in its colour's tint with the initials or the emoji in the colour itself; a vendor's mark in the corner (a logo-tile), a status dot at the foot (the roster patches its data-status on its own), the accent ring on the human's own. The size is the caller's (px); the colours are the look's. Composed through the .avatar hook: where it sits, how big, the life ring the roster lays over it.",
34
+ props: {
35
+ name: { type: "string", required: true },
36
+ label: { type: "string", required: true, note: "the initials or the emoji" },
37
+ color: { type: "string", required: true, note: "the participant's colour, #rrggbb" },
38
+ size: { type: "number", required: true, note: "px" },
39
+ emoji: { type: "boolean", default: false, note: "the label is an emoji: bigger" },
40
+ badge: { type: "node", note: "markup of the mark in the corner (a logo-tile), as ui.raw" },
41
+ status: { type: "enum", values: FACE_STATUSES, note: "the dot at the foot" },
42
+ me: { type: "boolean", default: false, note: "the human's own face: wears the accent ring" },
43
+ kind: { type: "enum", values: ["tile", "card", "bare"], default: "tile", note: "card: on the profile card, on paper with a shadow; bare: no tile (the rail)" },
44
+ ring: { type: "boolean", default: false, note: "a ring in the paper's colour, for faces that overlap in a stack" },
45
+ dim: { type: "enum", values: ["asleep", "unstaffed"], note: "greyed: asleep a little, unstaffed more" },
46
+ title: { type: "string" },
47
+ },
48
+ build: ({ name, label, color, size, emoji, badge, status, me, kind, ring, dim, title }, ui) =>
49
+ ui.h("span", { class: "avatar", role: "img", "aria-label": name, title, "data-kind": kind !== "tile" ? kind : null, "data-me": me || null, "data-ring": ring || null, "data-dim": dim || null, style: `--face-color:${color};--face-size:${size}px;width:${size}px;height:${size}px` },
50
+ ui.h("span", { class: "tile", "data-emoji": emoji || null, style: `font-size:${Math.round(emoji ? size * 0.56 : size * 0.38)}px` }, label),
51
+ badge,
52
+ status ? ui.h("span", { class: "status", "data-status": status }) : null),
53
+ states: ["rest"],
54
+ samples: (() => {
55
+ const p = globalThis.VIBEROOM_TOKENS.current.palette;
56
+ return [
57
+ { label: "initials", props: { name: "Maken", label: "MA", color: p.primary, size: 44 } },
58
+ { label: "an emoji, a vendor", props: { name: "Sam", label: "🦊", emoji: true, color: p.orange, size: 44, badge: UI.raw(UI.html("logo-tile", { icon: "/vendor-icons/claude.svg", size: "badge", title: "Claude" })), status: "thinking" } },
59
+ { label: "you", props: { name: "You", label: "🧑‍💻", emoji: true, color: p.indigo, size: 44, me: true } },
60
+ { label: "asleep", props: { name: "Rex", label: "🦖", emoji: true, color: p.green, size: 44, dim: "asleep", status: "offline", badge: UI.raw(UI.html("logo-tile", { glyph: "mute", size: "badge", tone: "muted", title: "muted" })) } },
61
+ { label: "on the card", props: { name: "You", label: "🧑‍💻", emoji: true, color: p.indigo, size: 64, kind: "card" } },
62
+ ];
63
+ })(),
64
+ });
65
+
66
+ UI.define("room-mark", {
67
+ group: "roomMark",
68
+ describe: "A room's mark: a square in the room's own hue with the first letter of its name, or its emoji on a paler square; the hue is the room's (from its id, set on the element), the recipe (how light, how saturated, the ink) is the look's. The accent kind is the mark of the whole, on the accent (all skills, no rooms yet).",
69
+ props: {
70
+ hue: { type: "number", note: "0–359, the room's own; not for the accent kind" },
71
+ letter: { type: "string", note: "the first letter of the room's name" },
72
+ emoji: { type: "string", note: "the room's emoji, instead of the letter" },
73
+ icon: { type: "icon", note: "the accent kind: an icon on the accent gradient" },
74
+ size: { type: "enum", values: ["md", "lg"], default: "md" },
75
+ title: { type: "string" },
76
+ },
77
+ build: ({ hue, letter, emoji, icon, size, title }, ui) => {
78
+ const kind = icon ? "accent" : emoji ? "emoji" : "letter";
79
+ const style = icon ? null : `--room-hue:${Number(hue) || 0};--room-hue-2:${((Number(hue) || 0) + 30) % 360}`;
80
+ return ui.h("span", { "data-kind": kind, "data-size": size !== "md" ? size : null, style, title }, icon ? ui.icon(icon) : emoji ? emoji : (letter || "?").slice(0, 1).toUpperCase());
81
+ },
82
+ states: ["rest"],
83
+ samples: [
84
+ { label: "a letter", props: { hue: 200, letter: "V", title: "Vibes" } },
85
+ { label: "an emoji", props: { hue: 40, emoji: "🎭", title: "Theatre" } },
86
+ { label: "large", props: { hue: 300, letter: "P", size: "lg" } },
87
+ { label: "the accent", props: { icon: "skills", title: "All skills" } },
88
+ ],
89
+ });
90
+
91
+ UI.define("unseen-line", {
92
+ group: "unseenLine",
93
+ describe: "The line across the room above which a vibemate (or several) has seen nothing: a rule on either side of a small label. The rule's drawing is the look's (a wave in VibeClassic, a dashed rule in Terminal); the label names who, and its tooltip says why.",
94
+ props: {
95
+ label: { type: "string", required: true, note: "who has not seen what is above" },
96
+ title: { type: "string", note: "the tooltip: where a vibemate's knowledge of the room starts" },
97
+ },
98
+ build: ({ label, title }, ui) => ui.h("div", { title }, ui.h("span", { class: "line" }), ui.h("span", { class: "label" }, `↑ ${label}`), ui.h("span", { class: "line" })),
99
+ states: ["rest"],
100
+ samples: [
101
+ { label: "one vibemate", props: { label: "Maken has not seen anything above this line" } },
102
+ { label: "two", props: { label: "Maken and Sam have not seen anything above this line" } },
103
+ ],
104
+ });
105
+
106
+ UI.define("number-field", {
107
+ group: "numberField",
108
+ describe: "A number to type or step: the field with two small steps at its right edge, drawn from the look instead of the browser's own spinner (U126). A step raises the same input and change events a typed value does, so a form saves on it. The id, the bounds and the step are the caller's.",
109
+ props: {
110
+ id: { type: "string" },
111
+ value: { type: "string", note: "the current value, as text; empty for none" },
112
+ min: { type: "number" },
113
+ max: { type: "number" },
114
+ step: { type: "number" },
115
+ placeholder: { type: "string" },
116
+ hook: { type: "string", note: "a class the page composes with (where it sits, how wide)" },
117
+ data: { type: "object", note: "data-* marks on the input, for the page's own handlers" },
118
+ disabled: { type: "boolean", default: false },
119
+ },
120
+ build: ({ id, value, min, max, step, placeholder, hook, data, disabled }, ui) =>
121
+ ui.h("span", { class: hook },
122
+ ui.h("input", { type: "number", id, value, min, max, step, placeholder, disabled, ...ui.dataAttrs(data) }),
123
+ ui.h("span", { class: "steps", "aria-hidden": "true" },
124
+ ui.h("button", { type: "button", "data-act": "up", tabindex: "-1", title: "More" }),
125
+ ui.h("button", { type: "button", "data-act": "down", tabindex: "-1", title: "Less" }))),
126
+ states: ["rest", "disabled"],
127
+ samples: [
128
+ { label: "seconds", props: { value: "4", min: 0, max: 120, step: 0.5 } },
129
+ { label: "empty, with a placeholder", props: { value: "", min: 1, max: 100, placeholder: "no limit" } },
130
+ ],
131
+ });
132
+
133
+ UI.define("adjust-row", {
134
+ group: "adjustRow",
135
+ describe: "One thing the human may adjust in a look (U125): its name and what it colours on the left, the control on the right — a swatch with its value for a colour, a slider with its value for a scale — and, once the value differs from the look's own, the way back. Settings → Appearance lists them by group from the adjustables in tokens.js; the page keeps data-state and the value in step as the human picks.",
136
+ props: {
137
+ key: { type: "string", required: true, note: "the adjustable's key (data-key), what the hub keeps it under" },
138
+ label: { type: "string", required: true },
139
+ hint: { type: "string", note: "what it colours" },
140
+ kind: { type: "enum", values: ["colour", "scale"], default: "colour" },
141
+ value: { type: "string", required: true, note: "#rrggbb, or the scale as a number" },
142
+ own: { type: "string", required: true, note: "the look's own value: the row shows the way back when value differs" },
143
+ min: { type: "number", note: "scale only" },
144
+ max: { type: "number", note: "scale only" },
145
+ step: { type: "number", note: "scale only" },
146
+ },
147
+ build: ({ key, label, hint, kind, value, own, min, max, step }, ui) => {
148
+ const changed = String(value).toLowerCase() !== String(own).toLowerCase();
149
+ const input = kind === "scale"
150
+ ? ui.h("input", { type: "range", value, min, max, step, "aria-label": label })
151
+ : ui.h("input", { type: "color", value, "aria-label": label });
152
+ return ui.h("div", { "data-key": key, "data-kind": kind, "data-state": changed ? "changed" : null },
153
+ ui.h("span", { class: "what" }, ui.h("span", { class: "label" }, label), hint ? ui.h("span", { class: "hint" }, hint) : null),
154
+ ui.h("span", { class: "ctl" }, input, ui.h("code", { class: "value" }, value),
155
+ ui.h("button", { type: "button", class: "back", "data-act": "reset", title: `Back to the look's own, ${own}`, "aria-label": "Back to the look's own" }, ui.icon("refresh"))));
156
+ },
157
+ states: ["rest", "changed"],
158
+ samples: (() => {
159
+ const p = globalThis.VIBEROOM_TOKENS.current.palette;
160
+ return [
161
+ { label: "a colour, as designed", props: { key: "canvas", label: "Chat paper", hint: "behind the messages", value: p.bg, own: p.bg } },
162
+ { label: "a colour, changed", props: { key: "accent", label: "Accent", hint: "buttons, links", value: p.orange, own: p.primary } },
163
+ { label: "a scale", props: { key: "corners", label: "Corners", hint: "0 is square", kind: "scale", value: "1", own: "1", min: 0, max: 1.5, step: 0.05 } },
164
+ ];
165
+ })(),
166
+ });
167
+
6
168
  UI.define("row-button", {
169
+ group: "rowButton",
7
170
  describe: "A small action on a vibemate's row: open its panel, go to its last reply, wake it up. The row reveals the buttons on hover; all three wear one pill.",
8
171
  props: {
9
172
  icon: { type: "icon", required: true },
@@ -21,6 +184,7 @@
21
184
  });
22
185
 
23
186
  UI.define("hub-row", {
187
+ group: "hubRow",
24
188
  describe: "A line the hub writes into the room (U114): news in muted words; a tone colours it (attention, error, hush); the one row that leads somewhere carries a ref and is a button with a backing, the only row that ever wears one.",
25
189
  props: {
26
190
  text: { type: "string", required: true },
@@ -39,13 +203,14 @@
39
203
  { label: "attention", props: { text: "Maken is at 84% of its context; it will leave notes with its next reply.", tone: "attention" } },
40
204
  { label: "error", props: { text: "Maken could not answer: no result", tone: "error" } },
41
205
  { label: "hush", props: { text: "Hush: everyone waits until you write again.", tone: "hush", face: UI.raw('<span class="hush-face">🤫</span>') } },
42
- { label: "leads to a reply", props: { text: "Maken finished the reply started at 16:21 · 2m 30s", ref: "sample", title: "go to the reply", face: UI.raw('<span class="avatar" style="width:20px;height:20px"><span class="av-tile" role="img" aria-label="Maken" style="font-size:11px;border-radius:6px">🔨</span></span>') } },
206
+ { label: "leads to a reply", props: { text: "Maken finished the reply started at 16:21 · 2m 30s", ref: "sample", title: "go to the reply", face: UI.raw(UI.html("face", { name: "Maken", label: "🔨", emoji: true, color: globalThis.VIBEROOM_TOKENS.current.palette.primary, size: 20 })) } },
43
207
  ],
44
208
  });
45
209
 
46
210
  const BUTTON_KINDS = ["plain", "primary", "secondary", "soft", "ghost", "danger", "danger-solid", "dark", "warn", "inverse", "link", "ok", "paper"];
47
211
  const BUTTON_SIZES = ["md", "sm", "xs", "lg", "cta"];
48
212
  UI.define("button", {
213
+ group: "btn",
49
214
  describe: "A button with words on it: the one type behind every Cancel, Summon, Save and Erase. Its kind says how loud it is and where it can sit (primary is the one thing to do; ghost is the way out; danger warns; danger-solid is the point of no return; dark sits on a code card; warn is a quiet amber word such as Stop; inverse sits on the accent; link is a word that acts), its size where it stands.",
50
215
  props: {
51
216
  label: { type: "string", required: true, note: "the words on it (wrapped in .label, so the page may change them)" },
@@ -113,6 +278,7 @@
113
278
  });
114
279
 
115
280
  UI.define("icon-button", {
281
+ group: "iconButton",
116
282
  describe: "A button that is only a glyph: close, settings, back, fold. Its title is its whole meaning, so it is required and doubles as the accessible name.",
117
283
  props: {
118
284
  icon: { type: "icon", required: true },
@@ -140,6 +306,7 @@
140
306
  });
141
307
 
142
308
  UI.define("choice", {
309
+ group: "choice",
143
310
  describe: "One option of a few, as a pill: the chosen one is lit (on). What choosing does is the page's: a data mark says which option this is.",
144
311
  props: {
145
312
  label: { type: "string", required: true },
@@ -164,6 +331,7 @@
164
331
  });
165
332
 
166
333
  UI.define("badge", {
334
+ group: "badge",
167
335
  describe: "A small label that states a fact: a vibemate's state, its vendor, a mark on a skill or a template. Its tone is the room's state family (ready, waiting, thinking, writing, error, asleep) or a quiet look (plain, muted, outline) or attention; it never does anything by itself.",
168
336
  props: {
169
337
  label: { type: "string", required: true },
@@ -191,6 +359,7 @@
191
359
  });
192
360
 
193
361
  UI.define("chip", {
362
+ group: "chip",
194
363
  describe: "A small tag that names a thing: the room's folder, a tool call, a template's run. As a button it opens or unfolds what it names; its tone follows the state of the thing (a tool call that completed, failed, is still running).",
195
364
  props: {
196
365
  label: { type: "string", required: true },
@@ -217,6 +386,7 @@
217
386
  });
218
387
 
219
388
  UI.define("file-card", {
389
+ group: "fileCard",
220
390
  describe: "A fragment of a file, or a picture, under the message that names it: a dark card whose head says the file's name and the lines shown and offers to open the whole file; its body is the code or the drawing. What could not be shown says why in the body instead.",
221
391
  props: {
222
392
  name: { type: "string", required: true },
@@ -243,6 +413,7 @@
243
413
 
244
414
  const TOOL_TONE = { completed: "ready", failed: "error", in_progress: "thinking" };
245
415
  UI.define("tool-call", {
416
+ group: "toolCall",
246
417
  describe: "A tool call in a reply: a chip with the tool's title, its kind and its status. Open, the chip is the head of a card that shows the call, its input and its output, bordered in the colour of its status.",
247
418
  props: {
248
419
  id: { type: "string", required: true, note: "the call's id; the chip carries it as data-tool for the page's handler" },
@@ -273,6 +444,7 @@
273
444
  });
274
445
 
275
446
  UI.define("tool-fold", {
447
+ group: "toolFold",
276
448
  describe: "The tool calls of a finished reply, folded into one line: how many there were and how many failed. It opens to the calls, which the page puts in its list; the page remembers it open per message.",
277
449
  props: {
278
450
  count: { type: "number", required: true },
@@ -293,6 +465,7 @@
293
465
 
294
466
  const CHOICE_KIND = { ok: "ok", no: "danger" };
295
467
  UI.define("ask-card", {
468
+ group: "askCard",
296
469
  describe: "A card the room puts in front of you for a decision: a permission a vibemate asks for, a proposal it makes. It says who asks and what, shows the details, and offers the choices as buttons; decided, it dims and keeps the outcome in the choices' place.",
297
470
  props: {
298
471
  kind: { type: "enum", values: ["permission", "proposal"], required: true },
@@ -324,6 +497,7 @@
324
497
  });
325
498
 
326
499
  UI.define("reply-note", {
500
+ group: "replyNote",
327
501
  describe: "A line the hub attaches to a reply, inside its bubble: an adapter's notice before the words, or the fact that the reply was stopped, right after them. The words stay; the note says what happened to them.",
328
502
  props: {
329
503
  text: { type: "string", required: true },
@@ -339,4 +513,31 @@
339
513
  { label: "stopped", props: { text: "Stopped by Sam", tone: "attention", icon: "stop" } },
340
514
  ],
341
515
  });
516
+
517
+ UI.define("look-card", {
518
+ group: "lookCard",
519
+ describe: "A look, shown as a small picture of itself: its paper, a reply and your bubble on it, its accent, its corners, its name in its own font. The picker in Settings and the style guide's own switch are rows of these; the colours come from the look's tokens, so the card is right whatever look the page wears.",
520
+ props: {
521
+ look: { type: "object", required: true, note: "a look from VIBEROOM_TOKENS.looks: id, label, palette, shape, type, elements" },
522
+ on: { type: "boolean", default: false },
523
+ act: { type: "string" },
524
+ title: { type: "string" },
525
+ data: { type: "object" },
526
+ },
527
+ build: ({ look, on, act, title, data }, ui) => {
528
+ const p = look.palette;
529
+ const e = look.elements;
530
+ const r = (px) => `calc(${px}px * ${look.shape.rScale || 1})`;
531
+ const canvas = (look.canvas && look.canvas.gradCanvas) || p.bg;
532
+ const shadows = look.elevation || {};
533
+ return ui.h("button", { type: "button", "data-look": look.id, "data-state": on ? "on" : null, "aria-pressed": on ? "true" : "false", "data-act": act, title: title || look.label, ...ui.dataAttrs(data) },
534
+ ui.h("span", { class: "paper", style: `background:${canvas};border-radius:${r(10)}` },
535
+ ui.h("span", { class: "reply", style: `background:${e.bubble.bg};color:${e.bubble.ink};border-radius:${r(8)};font-family:${look.type.font};box-shadow:${e.bubble.shadow || "none"}` }, "Aa"),
536
+ ui.h("span", { class: "mine", style: `background:${e.bubble.mineBg};border-radius:${r(8)};box-shadow:${e.bubble.mineShadow || "none"}` }),
537
+ ui.h("span", { class: "accent", style: `background:${p.primary};border-radius:${r(6)};box-shadow:${shadows.shadowPrimary || "none"}` })),
538
+ ui.h("span", { class: "name", style: `font-family:${look.type.font}` }, look.label));
539
+ },
540
+ states: ["rest", "hover", "on"],
541
+ samples: Object.values(globalThis.VIBEROOM_TOKENS.looks).map((l) => ({ label: l.label, props: { look: l, on: l.id === globalThis.VIBEROOM_TOKENS.current.id } })),
542
+ });
342
543
  })();
@@ -0,0 +1,7 @@
1
+ /* viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE */
2
+ @font-face { font-family: "Anonymous Pro"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/anonymous-pro-cyrillic-400.woff2") format("woff2"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
3
+ @font-face { font-family: "Anonymous Pro"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/anonymous-pro-latin-ext-400.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
4
+ @font-face { font-family: "Anonymous Pro"; font-style: normal; font-weight: 400; font-display: swap; src: url("/fonts/anonymous-pro-latin-400.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
5
+ @font-face { font-family: "Anonymous Pro"; font-style: normal; font-weight: 700; font-display: swap; src: url("/fonts/anonymous-pro-cyrillic-700.woff2") format("woff2"); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
6
+ @font-face { font-family: "Anonymous Pro"; font-style: normal; font-weight: 700; font-display: swap; src: url("/fonts/anonymous-pro-latin-ext-700.woff2") format("woff2"); unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
7
+ @font-face { font-family: "Anonymous Pro"; font-style: normal; font-weight: 700; font-display: swap; src: url("/fonts/anonymous-pro-latin-700.woff2") format("woff2"); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }