viberoom 0.5.9 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ui/avatars.js CHANGED
@@ -2,6 +2,8 @@
2
2
  (() => {
3
3
  "use strict";
4
4
 
5
+ const tokens = () => globalThis.VIBEROOM_TOKENS.current.elements.participant;
6
+
5
7
  function hash(text) {
6
8
  let h = 2166136261;
7
9
  for (const ch of String(text)) {
@@ -36,8 +38,8 @@
36
38
  return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${s} ${s}" width="${s}" height="${s}" role="img" aria-label="${escapeAttr(name)}">
37
39
  <defs><linearGradient id="${id}" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="${c1}"/><stop offset="1" stop-color="${c2}"/></linearGradient></defs>
38
40
  <circle cx="${s / 2}" cy="${s / 2}" r="${s / 2}" fill="url(#${id})"/>
39
- <circle cx="${s / 2}" cy="${s * 0.36}" r="${s * 0.34}" fill="#ffffff" opacity="0.10"/>
40
- <text x="50%" y="53%" dominant-baseline="middle" text-anchor="middle" font-family="system-ui, 'Segoe UI Emoji', sans-serif" font-size="${fontSize}" font-weight="700" fill="#ffffff">${escapeText(label)}</text>
41
+ <circle cx="${s / 2}" cy="${s * 0.36}" r="${s * 0.34}" fill="${tokens().avatarGloss}" opacity="0.10"/>
42
+ <text x="50%" y="53%" dominant-baseline="middle" text-anchor="middle" font-family="system-ui, 'Segoe UI Emoji', sans-serif" font-size="${fontSize}" font-weight="700" fill="${tokens().avatarLabelInk}">${escapeText(label)}</text>
41
43
  </svg>`;
42
44
  }
43
45
 
@@ -50,13 +52,13 @@
50
52
 
51
53
  function avatarHtml(participant, size, options) {
52
54
  const opts = options || {};
53
- const color = participant.color || "#5b5bf0";
55
+ const color = participant.color || tokens().avatarDefault;
54
56
  const s = size || 40;
55
57
  const emoji = participant.avatar;
56
58
  const label = emoji ? emoji : initials(participant.name);
57
59
  const fontSize = Math.round(emoji ? s * 0.56 : s * 0.38);
58
60
  const radius = Math.round(s * 0.32);
59
- const svg = `<span class="av-tile" role="img" aria-label="${escapeAttr(participant.name)}" style="background:color-mix(in srgb, ${escapeAttr(color)} 16%, #ffffff);color:${escapeAttr(color)};font-size:${fontSize}px;border-radius:${radius}px">${escapeText(label)}</span>`;
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>`;
60
62
  const status = opts.status ? `<span class="avatar-status status-${escapeAttr(participant.status || "idle")}"></span>` : "";
61
63
  let badge = "";
62
64
  if (opts.vendor && participant.kind === "agent") {
@@ -0,0 +1,140 @@
1
+ /* viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE */
2
+ [data-ui="row-button"] { display: inline-grid; place-items: center; width: var(--row-button-size); height: var(--row-button-size); padding: 0; border: 0; border-radius: var(--row-button-radius); background: var(--row-button-bg); color: var(--row-button-ink); cursor: pointer; transition: background var(--t-fast), color var(--t-fast), opacity var(--t-fast); }
3
+ [data-ui="row-button"] .i { width: var(--row-button-icon); height: var(--row-button-icon); }
4
+ [data-ui="row-button"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--row-button-hover-bg); color: var(--row-button-hover-ink); }
5
+ [data-ui="row-button"]:is(:active, [data-state="active"]):not(:disabled) { transform: translateY(1px); }
6
+ [data-ui="row-button"]:is(:disabled, [data-state="disabled"]) { opacity: var(--row-button-disabled-opacity); cursor: default; }
7
+ [data-ui="hub-row"] { display: inline-flex; flex-direction: row; align-items: center; gap: 8px; max-width: 80%; padding: 2px 10px; border: 0; background: none; color: var(--hub-row-ink); font: inherit; font-size: var(--fs-xs); font-weight: 700; text-align: left; }
8
+ [data-ui="hub-row"] .hush-face { font-size: var(--hub-row-face-size); line-height: 1; }
9
+ [data-ui="hub-row"][data-tone="attention"], [data-ui="hub-row"][data-tone="hush"] { color: var(--hub-row-attention-ink); font-weight: 800; }
10
+ [data-ui="hub-row"][data-tone="error"] { color: var(--hub-row-error-ink); font-weight: 800; }
11
+ [data-ui="hub-row"][data-ref] { padding: 6px 14px 6px 8px; border-radius: var(--r-pill); background: var(--hub-row-action-bg); color: var(--hub-row-action-ink); font-size: var(--fs-sm); font-weight: 800; cursor: pointer; box-shadow: var(--edge); transition: background var(--t-fast); }
12
+ [data-ui="hub-row"][data-ref]:is(:hover, [data-state="hover"]) { background: var(--hub-row-action-hover-bg); }
13
+ [data-ui="button"] { display: inline-flex; align-items: center; justify-content: center; gap: 8px; border: 0; background: var(--btn-bg); color: var(--btn-ink); border-radius: var(--r-sm); height: var(--btn-h); padding: 0 16px; font-weight: 800; font-size: var(--btn-fs); line-height: 1; white-space: nowrap; cursor: pointer; transition: background var(--t-fast), color var(--t-fast), transform var(--t-fast) var(--ease-out), box-shadow var(--t-fast); }
14
+ [data-ui="button"] .i { width: 16px; height: 16px; }
15
+ [data-ui="button"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--btn-hover-bg); color: var(--btn-hover-ink); }
16
+ [data-ui="button"]:is(:active, [data-state="active"]):not(:disabled) { transform: translateY(1px); }
17
+ [data-ui="button"]:is(:disabled, [data-state="disabled"]) { opacity: var(--btn-disabled-opacity); cursor: default; }
18
+ [data-ui="button"][data-kind="primary"] { background: var(--grad-primary); color: var(--btn-on-primary); box-shadow: var(--shadow-primary); }
19
+ [data-ui="button"][data-kind="primary"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--grad-primary); color: var(--btn-on-primary); filter: brightness(1.05); transform: translateY(-1px); }
20
+ [data-ui="button"][data-kind="primary"]:is(:active, [data-state="active"]):not(:disabled) { transform: translateY(1px); }
21
+ [data-ui="button"][data-kind="secondary"] { background: var(--btn-secondary-bg); color: var(--btn-ink); box-shadow: var(--shadow-tile); }
22
+ [data-ui="button"][data-kind="secondary"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--btn-secondary-bg); color: var(--btn-hover-ink); }
23
+ [data-ui="button"][data-kind="soft"] { background: var(--btn-soft-bg); color: var(--btn-hover-ink); }
24
+ [data-ui="button"][data-kind="soft"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--btn-soft-hover-bg); }
25
+ [data-ui="button"][data-kind="ghost"] { background: transparent; color: var(--btn-ghost-ink); font-weight: 700; }
26
+ [data-ui="button"][data-kind="ghost"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--btn-ghost-hover-bg); color: var(--btn-hover-ink); }
27
+ [data-ui="button"][data-kind="danger"] { background: var(--btn-danger-bg); color: var(--btn-danger-ink); }
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
+ [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
+ [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; }
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
+ [data-ui="button"][data-kind="warn"] { background: transparent; color: var(--btn-warn-ink); letter-spacing: 0.02em; border-radius: var(--r-pill); }
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; }
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
+ [data-ui="button"][data-kind="link"] { background: none; color: var(--btn-link-ink); height: auto; padding: 2px 0; border-radius: 0; }
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; }
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; }
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; }
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; }
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; }
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
+ [data-ui="button"][data-full] { width: 100%; }
50
+ [data-ui="button"][data-pill] { border-radius: var(--r-pill); }
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; }
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); }
55
+ [data-ui="icon-button"] .i { width: var(--icon-button-icon); height: var(--icon-button-icon); }
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
+ [data-ui="icon-button"]:is(:active, [data-state="active"]):not(:disabled) { transform: translateY(1px); }
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; }
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); }
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
+ [data-ui="icon-button"][data-kind="danger"] { color: var(--btn-danger-ink); }
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; }
66
+ [data-ui="icon-button"][data-size="xs"] .i { width: var(--icon-button-icon-xs); height: var(--icon-button-icon-xs); }
67
+ [data-ui="icon-button"][data-kind="ghost"] { background: transparent; color: var(--icon-button-ghost-ink); }
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; }
69
+ [data-ui="icon-button"][data-kind="inline"]:is(:hover, [data-state="hover"]):not(:disabled) { background: var(--icon-button-hover-bg-strong); color: var(--btn-on-primary); }
70
+ [data-ui="icon-button"][data-kind="inline"][data-state="on"] { background: var(--icon-button-inline-on-bg); color: var(--icon-button-inline-on-ink); }
71
+ [data-ui="choice"] { display: inline-flex; align-items: center; gap: 6px; border: 2px solid transparent; background: var(--choice-bg); color: var(--choice-ink); border-radius: var(--r-pill); padding: 0 12px; height: var(--choice-h); font: inherit; font-size: var(--fs-sm); font-weight: 800; white-space: nowrap; max-width: 100%; overflow: hidden; text-overflow: ellipsis; cursor: pointer; transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast); }
72
+ [data-ui="choice"]:is(:hover, [data-state="hover"]) { background: var(--choice-hover-bg); color: var(--choice-on-ink); }
73
+ [data-ui="choice"][data-state="on"] { background: var(--choice-hover-bg); color: var(--choice-on-ink); border-color: var(--choice-on-border); }
74
+ [data-ui="choice"][data-quiet] { color: var(--choice-quiet-ink); font-weight: 700; }
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; }
77
+ [data-ui="badge"][data-tone="ready"] { background: var(--st-ready); color: var(--st-ready-ink); }
78
+ [data-ui="badge"][data-tone="waiting"] { background: var(--st-waiting); color: var(--st-waiting-ink); }
79
+ [data-ui="badge"][data-tone="thinking"] { background: var(--st-thinking); color: var(--st-thinking-ink); }
80
+ [data-ui="badge"][data-tone="writing"] { background: var(--st-writing); color: var(--st-writing-ink); }
81
+ [data-ui="badge"][data-tone="error"] { background: var(--st-error); color: var(--st-error-ink); }
82
+ [data-ui="badge"][data-tone="asleep"] { background: var(--st-asleep); color: var(--st-asleep-ink); }
83
+ [data-ui="badge"][data-tone="attention"] { background: var(--badge-attention-bg); color: var(--badge-attention-ink); }
84
+ [data-ui="badge"][data-tone="muted"] { background: var(--badge-muted-bg); color: var(--badge-muted-ink); }
85
+ [data-ui="badge"][data-tone="outline"] { background: transparent; border: 2px solid var(--badge-outline-border); color: var(--badge-outline-ink); }
86
+ [data-ui="badge"]:is([data-tone="thinking"], [data-tone="writing"]) .dot { animation: pulse 1.2s ease-in-out infinite; }
87
+ [data-ui="badge"][data-size="xs"] { font-size: var(--badge-fs-xs); padding: 1px 7px; }
88
+ [data-ui="chip"] { display: inline-flex; align-items: center; gap: 5px; font: inherit; font-size: var(--fs-xs); font-weight: 800; background: var(--chip-bg); color: var(--chip-ink); border: 0; border-radius: var(--r-pill); padding: 4px 10px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; text-align: left; }
89
+ [data-ui="chip"] .i { width: 13px; height: 13px; flex: none; }
90
+ [data-ui="chip"][data-tone="ready"] { background: var(--st-ready); color: var(--st-ready-ink); }
91
+ [data-ui="chip"][data-tone="error"] { background: var(--st-error); color: var(--st-error-ink); }
92
+ [data-ui="chip"][data-tone="thinking"] { background: var(--st-thinking); color: var(--st-thinking-ink); }
93
+ [data-ui="chip"][data-tone="waiting"] { background: var(--st-waiting); color: var(--st-waiting-ink); }
94
+ [data-ui="chip"][data-button] { cursor: pointer; transition: background var(--t-fast); }
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); }
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
+ [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
+ [data-ui="file-card"] > .head .name .i { width: 13px; height: 13px; flex: none; }
100
+ [data-ui="file-card"] > .head .lines { color: var(--file-card-head-meta); font-weight: 700; }
101
+ [data-ui="file-card"] > .head [data-ui="button"] { margin-left: auto; }
102
+ [data-ui="file-card"] > .body { background: transparent; color: var(--file-card-ink); }
103
+ [data-ui="file-card"] > .body .code-view { border-radius: 0; max-height: 320px; }
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
+ [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); }
107
+ [data-ui="tool-call"] { display: inline-flex; flex-direction: column; max-width: 100%; min-width: 0; }
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
+ [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; }
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; }
113
+ [data-ui="tool-call"][data-status="completed"] > .body { border-color: var(--tool-call-ready-bg); }
114
+ [data-ui="tool-call"][data-status="failed"] > .body { border-color: var(--tool-call-error-bg); }
115
+ [data-ui="tool-call"][data-status="in_progress"] > .body { border-color: var(--tool-call-thinking-bg); }
116
+ [data-ui="tool-call"] .sec + .sec { margin-top: 8px; }
117
+ [data-ui="tool-call"] .sec b { display: block; font-size: var(--tool-call-label-fs); font-weight: 800; letter-spacing: 0.04em; text-transform: uppercase; color: var(--tool-call-pending-ink); }
118
+ [data-ui="tool-call"][data-status="completed"] .sec b { color: var(--tool-call-ready-ink); }
119
+ [data-ui="tool-call"][data-status="failed"] .sec b { color: var(--tool-call-error-ink); }
120
+ [data-ui="tool-call"][data-status="in_progress"] .sec b { color: var(--tool-call-thinking-ink); }
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; }
123
+ [data-ui="tool-fold"] { flex-basis: 100%; min-width: 0; }
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
+ [data-ui="tool-fold"] > summary::-webkit-details-marker { display: none; }
126
+ [data-ui="tool-fold"] > summary:hover, [data-ui="tool-fold"][data-state="hover"] > summary { background: var(--tool-fold-hover-bg); color: var(--tool-fold-hover-ink); }
127
+ [data-ui="tool-fold"] > summary .i { width: 13px; height: 13px; }
128
+ [data-ui="tool-fold"] > summary::after { content: "▾"; opacity: 0.55; transition: transform var(--t-fast); }
129
+ [data-ui="tool-fold"][open] > summary::after { transform: rotate(180deg); }
130
+ [data-ui="tool-fold"] > summary .failed { color: var(--tool-fold-failed-ink); }
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; }
133
+ [data-ui="ask-card"][data-state="resolved"] { opacity: var(--ask-card-resolved-opacity); }
134
+ [data-ui="ask-card"] > .title .kind { font-size: var(--ask-card-kind-fs); color: var(--ask-card-quiet-ink); }
135
+ [data-ui="ask-card"] > .input { margin: 6px 0; max-height: 160px; overflow: auto; font-size: var(--ask-card-input-fs); }
136
+ [data-ui="ask-card"] > .choices { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
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; }
140
+ [data-ui="reply-note"][data-tone="attention"] { color: var(--reply-note-attention-ink); background: var(--reply-note-attention-bg); }
@@ -0,0 +1,342 @@
1
+ // viberoom - Copyright (c) 2026 Todor Rusev - AGPL-3.0-or-later; see LICENSE
2
+ (() => {
3
+ "use strict";
4
+ const UI = globalThis.UI;
5
+
6
+ UI.define("row-button", {
7
+ 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
+ props: {
9
+ icon: { type: "icon", required: true },
10
+ title: { type: "string", required: true, note: "what it does, in words: the tooltip and the accessible name" },
11
+ act: { type: "string", required: true, note: "the action app.js answers: panel | last-reply | wake" },
12
+ disabled: { type: "boolean", default: false },
13
+ },
14
+ build: ({ icon, title, act, disabled }, ui) => ui.h("button", { type: "button", "data-act": act, title, "aria-label": title, disabled }, ui.icon(icon)),
15
+ states: ["rest", "hover", "active", "disabled"],
16
+ samples: [
17
+ { label: "the panel", props: { icon: "settings", title: "Open the panel", act: "panel" } },
18
+ { label: "its last reply", props: { icon: "last-reply", title: "Go to the last reply", act: "last-reply" } },
19
+ { label: "wake it up", props: { icon: "refresh", title: "Wake it up: reconnect it to the room", act: "wake" } },
20
+ ],
21
+ });
22
+
23
+ UI.define("hub-row", {
24
+ 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
+ props: {
26
+ text: { type: "string", required: true },
27
+ tone: { type: "enum", values: ["news", "attention", "error", "hush"], default: "news", note: "what kind of news the hub says it is" },
28
+ ref: { type: "string", note: "the id of the message the row leads to; with it the row is a button" },
29
+ face: { type: "node", note: "markup of a face before the words (ui.raw): the vibemate's, or the shushing one" },
30
+ title: { type: "string", note: "the tooltip: when, and where a click goes" },
31
+ },
32
+ build: ({ text, tone, ref, face, title }, ui) =>
33
+ ref
34
+ ? ui.h("button", { type: "button", "data-tone": tone, "data-ref": ref, title }, face, ui.h("span", {}, text))
35
+ : ui.h("div", { "data-tone": tone, title }, face, ui.h("span", {}, text)),
36
+ states: ["rest", "hover"],
37
+ samples: [
38
+ { label: "news", props: { text: "Maken is back in the room (session restored)." } },
39
+ { label: "attention", props: { text: "Maken is at 84% of its context; it will leave notes with its next reply.", tone: "attention" } },
40
+ { label: "error", props: { text: "Maken could not answer: no result", tone: "error" } },
41
+ { 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>') } },
43
+ ],
44
+ });
45
+
46
+ const BUTTON_KINDS = ["plain", "primary", "secondary", "soft", "ghost", "danger", "danger-solid", "dark", "warn", "inverse", "link", "ok", "paper"];
47
+ const BUTTON_SIZES = ["md", "sm", "xs", "lg", "cta"];
48
+ UI.define("button", {
49
+ 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
+ props: {
51
+ label: { type: "string", required: true, note: "the words on it (wrapped in .label, so the page may change them)" },
52
+ icon: { type: "icon", note: "a glyph before the words" },
53
+ lead: { type: "node", note: "markup before the words that is not a glyph (a face), as ui.raw" },
54
+ trail: { type: "node", note: "markup after the words (a hint), as ui.raw" },
55
+ kind: { type: "enum", values: BUTTON_KINDS, default: "plain" },
56
+ size: { type: "enum", values: BUTTON_SIZES, default: "md" },
57
+ full: { type: "boolean", default: false, note: "as wide as its row" },
58
+ pill: { type: "boolean", default: false },
59
+ type: { type: "enum", values: ["button", "submit"], default: "button" },
60
+ id: { type: "string" },
61
+ act: { type: "string", note: "the action a delegated handler answers (data-act)" },
62
+ value: { type: "string", note: "what a dialog form returns when this button submits it" },
63
+ title: { type: "string" },
64
+ disabled: { type: "boolean", default: false },
65
+ hidden: { type: "boolean", default: false },
66
+ autofocus: { type: "boolean", default: false },
67
+ hook: { type: "string", note: "classes the page composes with (where it sits, its page states); no look of its own" },
68
+ data: { type: "object", note: "data-* marks for the page's own handlers: { close: true, approveSkill: name }" },
69
+ },
70
+ build: ({ label, icon, lead, trail, kind, size, full, pill, type, id, act, value, title, disabled, hidden, autofocus, hook, data }, ui) =>
71
+ ui.h(
72
+ "button",
73
+ {
74
+ type,
75
+ "data-kind": kind === "plain" ? null : kind,
76
+ "data-size": size === "md" ? null : size,
77
+ "data-full": full || null,
78
+ "data-pill": pill || null,
79
+ "data-act": act,
80
+ id,
81
+ value,
82
+ title,
83
+ disabled,
84
+ hidden,
85
+ autofocus,
86
+ class: hook || null,
87
+ ...ui.dataAttrs(data),
88
+ },
89
+ icon ? ui.icon(icon) : null,
90
+ lead || null,
91
+ ui.h("span", { class: "label" }, label),
92
+ trail || null,
93
+ ),
94
+ states: ["rest", "hover", "active", "disabled", "loading"],
95
+ samples: [
96
+ { label: "plain", props: { label: "Check now" } },
97
+ { label: "primary", props: { label: "Summon", kind: "primary" } },
98
+ { label: "secondary", props: { label: "Later", kind: "secondary" } },
99
+ { label: "soft", props: { label: "Browse", kind: "soft", icon: "folder" } },
100
+ { label: "ghost", props: { label: "Cancel", kind: "ghost" } },
101
+ { label: "danger", props: { label: "Respawn", kind: "danger", icon: "bolt", size: "sm" } },
102
+ { label: "danger-solid", props: { label: "Erase everything", kind: "danger-solid" } },
103
+ { label: "small", props: { label: "Save", kind: "primary", size: "sm" } },
104
+ { label: "large", props: { label: "Start vibing", kind: "primary", size: "lg" } },
105
+ { label: "call to action", props: { label: "Summon a Vibemate", kind: "primary", size: "cta", icon: "spark" } },
106
+ { label: "on a code card", props: { label: "open", kind: "dark", size: "xs" } },
107
+ { label: "a quiet warning", props: { label: "Stop", kind: "warn", size: "xs" } },
108
+ { label: "on the accent", props: { label: "Stop Ana and send now", kind: "inverse", size: "sm" } },
109
+ { label: "a word that acts", props: { label: "Show more", kind: "link" } },
110
+ { label: "yes, on a card", props: { label: "Allow", kind: "ok", size: "sm" } },
111
+ { label: "plain, on a card", props: { label: "Dismiss", kind: "paper", size: "sm" } },
112
+ ],
113
+ });
114
+
115
+ UI.define("icon-button", {
116
+ 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
+ props: {
118
+ icon: { type: "icon", required: true },
119
+ title: { type: "string", required: true, note: "what it does, in words: the tooltip and the accessible name" },
120
+ kind: { type: "enum", values: ["plain", "ghost", "primary", "danger", "inline"], default: "plain", note: "inline: a glyph in running text (the preview eye), with an on state" },
121
+ size: { type: "enum", values: ["md", "sm", "xs"], default: "md" },
122
+ id: { type: "string" },
123
+ act: { type: "string" },
124
+ disabled: { type: "boolean", default: false },
125
+ hidden: { type: "boolean", default: false },
126
+ hook: { type: "string", note: "classes the page composes with; no look of its own" },
127
+ data: { type: "object" },
128
+ },
129
+ build: ({ icon, title, kind, size, id, act, disabled, hidden, hook, data }, ui) =>
130
+ ui.h("button", { type: "button", "data-kind": kind === "plain" ? null : kind, "data-size": size === "md" ? null : size, "data-act": act, id, title, "aria-label": title, disabled, hidden, class: hook || null, ...ui.dataAttrs(data) }, ui.icon(icon)),
131
+ states: ["rest", "hover", "active", "disabled", "on"],
132
+ samples: [
133
+ { label: "plain", props: { icon: "settings", title: "Room settings" } },
134
+ { label: "ghost, small", props: { icon: "close", title: "Close", kind: "ghost", size: "sm" } },
135
+ { label: "ghost, tiny (a bubble's head)", props: { icon: "pin", title: "Pin this message", kind: "ghost", size: "xs" } },
136
+ { label: "primary", props: { icon: "send", title: "Send", kind: "primary" } },
137
+ { label: "danger", props: { icon: "trash", title: "Remove", kind: "danger" } },
138
+ { label: "inline (the preview eye)", props: { icon: "eye", title: "Preview", kind: "inline", size: "xs" } },
139
+ ],
140
+ });
141
+
142
+ UI.define("choice", {
143
+ 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
+ props: {
145
+ label: { type: "string", required: true },
146
+ lead: { type: "node", note: "markup before the words (a colour swatch), as ui.raw" },
147
+ trail: { type: "node", note: "markup after the words (a hint), as ui.raw" },
148
+ on: { type: "boolean", default: false, note: "the chosen one" },
149
+ quiet: { type: "boolean", default: false, note: "the option that means none" },
150
+ act: { type: "string" },
151
+ id: { type: "string" },
152
+ title: { type: "string" },
153
+ hook: { type: "string" },
154
+ data: { type: "object", note: "which option this is: { mode: 'auto' }, { preset: 'pop' }" },
155
+ },
156
+ build: ({ label, lead, trail, on, quiet, act, id, title, hook, data }, ui) =>
157
+ ui.h("button", { type: "button", "data-state": on ? "on" : null, "data-quiet": quiet || null, "data-act": act, id, title, "aria-pressed": on ? "true" : "false", class: hook || null, ...ui.dataAttrs(data) }, lead || null, ui.h("span", { class: "label" }, label), trail || null),
158
+ states: ["rest", "hover", "on"],
159
+ samples: [
160
+ { label: "an option", props: { label: "The default app" } },
161
+ { label: "the chosen one", props: { label: "Auto", on: true } },
162
+ { label: "none", props: { label: "No preset", quiet: true } },
163
+ ],
164
+ });
165
+
166
+ UI.define("badge", {
167
+ 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
+ props: {
169
+ label: { type: "string", required: true },
170
+ tone: { type: "enum", values: ["plain", "ready", "waiting", "thinking", "writing", "error", "asleep", "attention", "muted", "outline"], default: "plain" },
171
+ dot: { type: "boolean", default: false, note: "a dot before the words; it pulses while thinking or writing" },
172
+ size: { type: "enum", values: ["md", "xs"], default: "md" },
173
+ title: { type: "string" },
174
+ hook: { type: "string" },
175
+ },
176
+ build: ({ label, tone, dot, size, title, hook }, ui) => ui.h("span", { "data-tone": tone === "plain" ? null : tone, "data-size": size === "md" ? null : size, title, class: hook || null }, dot ? ui.h("span", { class: "dot" }) : null, label),
177
+ states: ["rest"],
178
+ samples: [
179
+ { label: "plain", props: { label: "vibemate" } },
180
+ { label: "ready", props: { label: "ready", tone: "ready", dot: true } },
181
+ { label: "waiting", props: { label: "waiting…", tone: "waiting" } },
182
+ { label: "thinking", props: { label: "thinking…", tone: "thinking", dot: true } },
183
+ { label: "writing", props: { label: "writing…", tone: "writing", dot: true } },
184
+ { label: "error", props: { label: "error", tone: "error" } },
185
+ { label: "asleep", props: { label: "offline", tone: "asleep" } },
186
+ { label: "attention", props: { label: "summon", tone: "attention" } },
187
+ { label: "muted", props: { label: "muted", tone: "muted" } },
188
+ { label: "outline", props: { label: "built-in", tone: "outline" } },
189
+ { label: "tiny", props: { label: "recommended", tone: "attention", size: "xs" } },
190
+ ],
191
+ });
192
+
193
+ UI.define("chip", {
194
+ 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
+ props: {
196
+ label: { type: "string", required: true },
197
+ icon: { type: "icon" },
198
+ tone: { type: "enum", values: ["plain", "ready", "error", "thinking", "waiting"], default: "plain" },
199
+ button: { type: "boolean", default: false, note: "a chip that does something when clicked" },
200
+ act: { type: "string" },
201
+ id: { type: "string" },
202
+ title: { type: "string" },
203
+ hidden: { type: "boolean", default: false },
204
+ hook: { type: "string" },
205
+ data: { type: "object" },
206
+ },
207
+ build: ({ label, icon, tone, button, act, id, title, hidden, hook, data }, ui) =>
208
+ ui.h(button ? "button" : "span", { ...(button ? { type: "button" } : {}), "data-tone": tone === "plain" ? null : tone, "data-button": button || null, "data-act": act, id, title, hidden, class: hook || null, ...ui.dataAttrs(data) }, icon ? ui.icon(icon) : null, label),
209
+ states: ["rest", "hover"],
210
+ samples: [
211
+ { label: "a name", props: { label: "src", icon: "folder" } },
212
+ { label: "a button", props: { label: "3 pinned", icon: "pin", button: true } },
213
+ { label: "a tool call, done", props: { label: "Read file · completed", icon: "tool", tone: "ready", button: true } },
214
+ { label: "a tool call, failed", props: { label: "Bash · failed", icon: "tool", tone: "error", button: true } },
215
+ { label: "a tool call, running", props: { label: "Grep · in_progress", icon: "tool", tone: "thinking", button: true } },
216
+ ],
217
+ });
218
+
219
+ UI.define("file-card", {
220
+ 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
+ props: {
222
+ name: { type: "string", required: true },
223
+ lines: { type: "string", note: "what part is shown, in words: 'lines 120–160', or a picture's size once it loaded" },
224
+ kind: { type: "enum", values: ["code", "image"], default: "code" },
225
+ body: { type: "html", note: "the code view or the picture: markup the caller vouches for" },
226
+ error: { type: "string", note: "why the file could not be shown; takes the body's place" },
227
+ act: { type: "string", default: "open-file", note: "the head button's act" },
228
+ openTitle: { type: "string", default: "Open the whole file in the room" },
229
+ hook: { type: "string" },
230
+ data: { type: "object" },
231
+ },
232
+ build: ({ name, lines, kind, body, error, act, openTitle, hook, data }, ui) =>
233
+ ui.h("div", { "data-kind": kind === "code" ? null : kind, class: hook || null, ...ui.dataAttrs(data) },
234
+ ui.h("div", { class: "head" }, ui.h("span", { class: "name" }, ui.icon("link"), name), ui.h("span", { class: "lines" }, lines || ""), ui.build("button", { label: "open", kind: "dark", size: "xs", act, title: openTitle })),
235
+ ui.h("div", { class: "body" }, error ? ui.h("div", { class: "note" }, error) : body ? ui.raw(body) : null)),
236
+ states: ["rest"],
237
+ samples: [
238
+ { label: "a fragment", props: { name: "room.ts", lines: "lines 120–124", body: "<pre style=\"margin:0;padding:10px 12px\">120 const key = randomUUID();\n121 const entry = { key, ts: Date.now() };\n122 this.pending.set(key, entry);\n123 this.push({ type: \"permission\", key });\n124 return entry;</pre>" } },
239
+ { label: "a picture", props: { name: "mockup.png", lines: "640×400", kind: "image", openTitle: "Open it big", body: "<div style=\"width:200px;height:90px;border-radius:8px;background:var(--lav)\"></div>" } },
240
+ { label: "could not be shown", props: { name: "gone.ts", error: "gone.ts could not be shown here: no such file." } },
241
+ ],
242
+ });
243
+
244
+ const TOOL_TONE = { completed: "ready", failed: "error", in_progress: "thinking" };
245
+ UI.define("tool-call", {
246
+ 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
+ props: {
248
+ id: { type: "string", required: true, note: "the call's id; the chip carries it as data-tool for the page's handler" },
249
+ title: { type: "string", required: true },
250
+ kind: { type: "string" },
251
+ status: { type: "enum", values: ["pending", "in_progress", "completed", "failed"], default: "pending" },
252
+ open: { type: "boolean", default: false, note: "open, the card shows the call; the body is built only then" },
253
+ input: { type: "string", note: "shown up to 4000 characters" },
254
+ output: { type: "string" },
255
+ },
256
+ build: ({ id, title, kind, status, open, input, output }, ui) =>
257
+ ui.h("div", { "data-status": status, "data-state": open ? "open" : null },
258
+ ui.build("chip", { label: `${title}${kind ? ` · ${kind}` : ""} · ${status}`, icon: "tool", tone: TOOL_TONE[status] || "plain", button: true, title: open ? "Collapse" : "Expand", data: { tool: id } }),
259
+ open
260
+ ? ui.h("div", { class: "body" },
261
+ ui.h("div", { class: "sec" }, ui.h("b", null, "call"), ui.h("pre", null, title)),
262
+ input ? ui.h("div", { class: "sec" }, ui.h("b", null, "input"), ui.h("pre", null, input.slice(0, 4000))) : null,
263
+ output ? ui.h("div", { class: "sec" }, ui.h("b", null, "output"), ui.h("pre", null, output)) : ui.h("div", { class: "sec quiet" }, "no output recorded"))
264
+ : null),
265
+ states: ["rest"],
266
+ samples: [
267
+ { label: "done", props: { id: "t1", title: "Read file", kind: "read", status: "completed" } },
268
+ { label: "failed", props: { id: "t2", title: "Bash", kind: "execute", status: "failed" } },
269
+ { label: "running", props: { id: "t3", title: "Grep", kind: "search", status: "in_progress" } },
270
+ { label: "pending", props: { id: "t4", title: "Edit", kind: "edit" } },
271
+ { label: "open", props: { id: "t5", title: "Read file", kind: "read", status: "completed", open: true, input: "{ \"path\": \"src/room.ts\" }", output: "export class Room { … }" } },
272
+ ],
273
+ });
274
+
275
+ UI.define("tool-fold", {
276
+ 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
+ props: {
278
+ count: { type: "number", required: true },
279
+ failed: { type: "number", default: 0 },
280
+ open: { type: "boolean", default: false },
281
+ },
282
+ build: ({ count, failed, open }, ui) =>
283
+ ui.h("details", { open: !!open },
284
+ ui.h("summary", { title: open ? "Fold the tool calls away" : "Show every tool call" }, ui.icon("tool"), ui.h("span", null, `${count} tool call${count === 1 ? "" : "s"}`), failed ? ui.h("span", { class: "failed" }, `· ${failed} failed`) : null),
285
+ ui.h("div", { class: "list" })),
286
+ states: ["rest", "hover"],
287
+ samples: [
288
+ { label: "one call", props: { count: 1 } },
289
+ { label: "eight, one failed", props: { count: 8, failed: 1 } },
290
+ { label: "open", props: { count: 3, open: true } },
291
+ ],
292
+ });
293
+
294
+ const CHOICE_KIND = { ok: "ok", no: "danger" };
295
+ UI.define("ask-card", {
296
+ 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
+ props: {
298
+ kind: { type: "enum", values: ["permission", "proposal"], required: true },
299
+ who: { type: "string", required: true, note: "who asks, by name" },
300
+ lead: { type: "string", note: "what is asked, in words after the name; the default fits the kind" },
301
+ subject: { type: "string", note: "what the ask is about, in bold: the tool call's title" },
302
+ subjectKind: { type: "string", note: "a small word after the subject: the tool call's kind" },
303
+ input: { type: "string", note: "the tool call's input, as text; shown up to 1200 characters" },
304
+ body: { type: "html", note: "the details: a proposal's reason, its diff, its warnings; markup the caller vouches for" },
305
+ choices: { type: "array", note: "[{ label, tone: ok | no | plain, act, data }]: a button each, answering with its act and data" },
306
+ outcome: { type: "string", note: "what was decided; given, the card is resolved and shows this instead of its choices" },
307
+ data: { type: "object" },
308
+ },
309
+ build: ({ kind, who, lead, subject, subjectKind, input, body, choices, outcome, data }, ui) =>
310
+ ui.h("div", { "data-kind": kind, "data-state": outcome ? "resolved" : null, ...ui.dataAttrs(data) },
311
+ ui.h("div", { class: "title" }, ui.icon(kind === "permission" ? "lock" : "pencil"), " ", who, " ", lead || (kind === "permission" ? "asks for permission:" : "proposes changes to the room"), subject ? [" ", ui.h("strong", null, subject)] : null, subjectKind ? [" ", ui.h("span", { class: "kind" }, subjectKind)] : null),
312
+ input ? ui.h("pre", { class: "input" }, input.slice(0, 1200)) : null,
313
+ body ? ui.raw(body) : null,
314
+ ui.h("div", { class: "choices" },
315
+ outcome
316
+ ? ui.h("span", { class: "outcome" }, outcome)
317
+ : (choices || []).map((c) => ui.build("button", { label: c.label, kind: CHOICE_KIND[c.tone] || "paper", size: "sm", act: c.act, title: c.title, data: c.data })))),
318
+ states: ["rest"],
319
+ samples: [
320
+ { label: "a permission", props: { kind: "permission", who: "Maken", subject: "Bash", subjectKind: "execute", input: "{ \"command\": \"npm test\" }", choices: [{ label: "Allow", tone: "ok", act: "permit", data: { option: "allow" } }, { label: "Deny", tone: "no", act: "permit", data: { option: "deny" } }, { label: "Dismiss (cancelled)", act: "permit" }] } },
321
+ { label: "a proposal", props: { kind: "proposal", who: "Ana", body: "<div class=\"why\">The room keeps forgetting the deploy steps.</div>", choices: [{ label: "Apply", tone: "ok", act: "decide", data: { answer: "apply" } }, { label: "Reject", tone: "no", act: "decide", data: { answer: "reject" } }] } },
322
+ { label: "decided", props: { kind: "permission", who: "Maken", subject: "Bash", outcome: "chosen: allow" } },
323
+ ],
324
+ });
325
+
326
+ UI.define("reply-note", {
327
+ 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
+ props: {
329
+ text: { type: "string", required: true },
330
+ tone: { type: "enum", values: ["info", "attention"], default: "info", note: "info: a quiet fact; attention: something that changed the reply" },
331
+ icon: { type: "icon", default: "info" },
332
+ hook: { type: "string" },
333
+ },
334
+ build: ({ text, tone, icon, hook }, ui) => ui.h("div", { "data-tone": tone === "info" ? null : tone, class: hook || null }, ui.icon(icon), ui.h("span", null, text)),
335
+ states: ["rest"],
336
+ samples: [
337
+ { label: "a quiet fact", props: { text: "This reply was written in plan mode." } },
338
+ { label: "an adapter's notice", props: { text: "Auto mode is unavailable for this account; running in the default mode.", tone: "attention" } },
339
+ { label: "stopped", props: { text: "Stopped by Sam", tone: "attention", icon: "stop" } },
340
+ ],
341
+ });
342
+ })();
package/ui/icons.js CHANGED
@@ -32,6 +32,11 @@
32
32
  trash: `<path d="M4 7h16M10 11v6M14 11v6M6 7l1 13h10l1-13M9 7V4h6v3"/>`,
33
33
  pencil: `<path d="M17 3a2.8 2.8 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"/>`,
34
34
  pin: `<path d="M9 4h6l-.8 6.5L17 13v2H7v-2l2.8-2.5L9 4z"/><path d="M12 15v6"/>`,
35
+ quote: `<path d="M9 6.5C6.5 7.8 5 10 5 12.6V17h5v-5H7.4c.2-1.4 1-2.5 2.4-3.2zM19 6.5c-2.5 1.3-4 3.5-4 6.1V17h5v-5h-2.6c.2-1.4 1-2.5 2.4-3.2z"/>`,
36
+ maximize: `<path d="M15 3h6v6M21 3l-7 7M9 21H3v-6M3 21l7-7"/>`,
37
+ minimize: `<path d="M20 4l-6 6M14 4v6h6M4 20l6-6M10 20v-6H4"/>`,
38
+ "zoom-in": `<circle cx="11" cy="11" r="6"/><path d="M11 8.5v5M8.5 11h5M20 20l-4.3-4.3"/>`,
39
+ "zoom-out": `<circle cx="11" cy="11" r="6"/><path d="M8.5 11h5M20 20l-4.3-4.3"/>`,
35
40
  send: `<path d="M4 12l16-8-6 16-2.5-6.5L4 12z"/>`,
36
41
  folder: `<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/>`,
37
42
  spark: `<path d="M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8L12 3z"/><path d="M19 15l.6 1.6 1.6.6-1.6.6L19 19.4l-.6-1.6-1.6-.6 1.6-.6z"/>`,
@@ -44,6 +49,7 @@
44
49
  wand: `<path d="M15 4V2M15 16v-2M8 9h2M20 9h2M17.8 11.8L19 13M17.8 6.2L19 5M3 21l9-9M12.2 6.2L11 5"/>`,
45
50
  bolt: `<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>`,
46
51
  save: `<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><path d="M17 21v-8H7v8M7 3v5h8"/>`,
52
+ copy: `<rect x="9" y="9" width="12" height="12" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>`,
47
53
  eye: `<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>`,
48
54
  lock: `<rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>`,
49
55
  sun: `<circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/>`,
@@ -53,6 +59,9 @@
53
59
  smile: `<circle cx="12" cy="12" r="9"/><path d="M8 14s1.5 2 4 2 4-2 4-2M9 9h.01M15 9h.01"/>`,
54
60
  "arrow-down": `<path d="M12 5v14M19 12l-7 7-7-7"/>`,
55
61
  "arrow-up": `<path d="M12 19V5M5 12l7-7 7 7"/>`,
62
+ "chevrons-down": `<path d="M7 6l5 5 5-5M7 13l5 5 5-5"/>`,
63
+ "last-reply": `<path d="M20 12a8 8 0 0 1-8 8H5l-1.5 1.5V12a8 8 0 1 1 16 0z"/><path d="M12 8v7M9 12.5l3 3 3-3"/>`,
64
+ "chevrons-up": `<path d="M7 11l5-5 5 5M7 18l5-5 5 5"/>`,
56
65
  database: `<ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M21 12c0 1.7-4 3-9 3s-9-1.3-9-3M3 5v14c0 1.7 4 3 9 3s9-1.3 9-3V5"/>`,
57
66
  hand: `<path d="M18 11V6a2 2 0 0 0-4 0v1M14 10V4a2 2 0 0 0-4 0v2M10 10.5V6a2 2 0 0 0-4 0v8"/><path d="M18 8a2 2 0 0 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.9-5.9-2.3L2.6 15.6a2 2 0 0 1 2.8-2.8L7 14.4"/>`,
58
67
  };