@2lab.ai/ui 0.0.0-stage → 0.1.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.
Files changed (52) hide show
  1. package/README.md +54 -2
  2. package/dist/agent-inbox.js +1061 -0
  3. package/dist/agent-inbox.js.map +1 -0
  4. package/dist/agent-queue.js +1610 -0
  5. package/dist/agent-queue.js.map +1 -0
  6. package/dist/agent-run.js +859 -0
  7. package/dist/agent-run.js.map +1 -0
  8. package/dist/ai-answer.js +687 -0
  9. package/dist/ai-answer.js.map +1 -0
  10. package/dist/components/agent-inbox/agent-inbox.d.ts +158 -0
  11. package/dist/components/agent-inbox/index.d.ts +2 -0
  12. package/dist/components/agent-inbox/styles.d.ts +6 -0
  13. package/dist/components/agent-queue/agent-queue.d.ts +41 -0
  14. package/dist/components/agent-queue/index.d.ts +6 -0
  15. package/dist/components/agent-queue/logic.d.ts +261 -0
  16. package/dist/components/agent-queue/strings.d.ts +473 -0
  17. package/dist/components/agent-queue/styles.d.ts +8 -0
  18. package/dist/components/agent-run/agent-run.d.ts +273 -0
  19. package/dist/components/agent-run/index.d.ts +2 -0
  20. package/dist/components/agent-run/styles.d.ts +6 -0
  21. package/dist/components/ai-answer/ai-answer.d.ts +92 -0
  22. package/dist/components/ai-answer/index.d.ts +2 -0
  23. package/dist/components/ai-answer/styles.d.ts +2 -0
  24. package/dist/components/context-window/context-window.d.ts +156 -0
  25. package/dist/components/context-window/index.d.ts +2 -0
  26. package/dist/components/context-window/styles.d.ts +2 -0
  27. package/dist/components/memory-panel/index.d.ts +2 -0
  28. package/dist/components/memory-panel/memory-panel.d.ts +101 -0
  29. package/dist/components/memory-panel/styles.d.ts +2 -0
  30. package/dist/components/model-picker/index.d.ts +2 -0
  31. package/dist/components/model-picker/model-picker.d.ts +136 -0
  32. package/dist/components/model-picker/morph.d.ts +9 -0
  33. package/dist/components/model-picker/styles.d.ts +6 -0
  34. package/dist/context-window.js +384 -0
  35. package/dist/context-window.js.map +1 -0
  36. package/dist/core/element.d.ts +39 -0
  37. package/dist/core/format.d.ts +14 -0
  38. package/dist/core/html.d.ts +10 -0
  39. package/dist/core/tokens.d.ts +14 -0
  40. package/dist/format-BBGWXPzk.js +51 -0
  41. package/dist/format-BBGWXPzk.js.map +1 -0
  42. package/dist/html-DZWnrdcg.js +167 -0
  43. package/dist/html-DZWnrdcg.js.map +1 -0
  44. package/dist/index.d.ts +15 -0
  45. package/dist/index.js +116 -0
  46. package/dist/index.js.map +1 -0
  47. package/dist/memory-panel.js +1255 -0
  48. package/dist/memory-panel.js.map +1 -0
  49. package/dist/model-picker.js +858 -0
  50. package/dist/model-picker.js.map +1 -0
  51. package/dist/tokens.css +4 -0
  52. package/package.json +75 -4
@@ -0,0 +1,858 @@
1
+ import { d as define, U as UiElement, f as escAttr, e as esc, p as prefersReducedMotion } from "./html-DZWnrdcg.js";
2
+ function morphInto(el, html) {
3
+ const tpl = document.createElement("template");
4
+ tpl.innerHTML = html;
5
+ morphChildren(el, tpl.content);
6
+ }
7
+ function keyOf(n) {
8
+ if (n.nodeType !== 1) return `#${n.nodeType}`;
9
+ const e = n;
10
+ const k = e.getAttribute("data-mk");
11
+ if (k !== null) return `${e.tagName}|@${k}`;
12
+ const c = e.getAttribute("class") || "";
13
+ const sp = c.indexOf(" ");
14
+ return `${e.tagName}|${e.id}.${sp < 0 ? c : c.slice(0, sp)}`;
15
+ }
16
+ function keyLater(n, key) {
17
+ for (; n; n = n.nextSibling) if (keyOf(n) === key) return true;
18
+ return false;
19
+ }
20
+ function morphNode(a, b) {
21
+ if (a.nodeType !== 1) {
22
+ if (a.nodeValue !== b.nodeValue) a.nodeValue = b.nodeValue;
23
+ return;
24
+ }
25
+ const ae = a;
26
+ const be = b;
27
+ for (let i = ae.attributes.length - 1; i >= 0; i--) {
28
+ const name = ae.attributes[i].name;
29
+ if (!be.hasAttribute(name)) ae.removeAttribute(name);
30
+ }
31
+ for (let i = 0; i < be.attributes.length; i++) {
32
+ const at = be.attributes[i];
33
+ if (ae.getAttribute(at.name) !== at.value) ae.setAttribute(at.name, at.value);
34
+ }
35
+ morphChildren(ae, be);
36
+ }
37
+ function morphChildren(a, b) {
38
+ let cur = a.firstChild;
39
+ let next = b.firstChild;
40
+ while (next) {
41
+ if (cur && keyOf(cur) !== keyOf(next) && !keyLater(next, keyOf(cur))) {
42
+ const dropped = cur;
43
+ cur = cur.nextSibling;
44
+ a.removeChild(dropped);
45
+ continue;
46
+ }
47
+ const after = next.nextSibling;
48
+ if (cur && keyOf(cur) === keyOf(next)) {
49
+ morphNode(cur, next);
50
+ cur = cur.nextSibling;
51
+ } else {
52
+ a.insertBefore(next, cur);
53
+ }
54
+ next = after;
55
+ }
56
+ while (cur) {
57
+ const gone = cur;
58
+ cur = cur.nextSibling;
59
+ a.removeChild(gone);
60
+ }
61
+ }
62
+ const styles = `
63
+ :host { --mp-well: color-mix(in srgb, var(--ib-text) 6%, var(--ib-card)); --mp-bar: color-mix(in srgb, var(--ib-text) 24%, var(--ib-card));
64
+ --mp-bar-off: color-mix(in srgb, var(--ib-text) 9%, var(--ib-card));
65
+ display: inline-block; max-width: 100%; vertical-align: middle; }
66
+ .mp { display: flex; flex-direction: column; align-items: flex-start; gap: 4px; min-width: 0; max-width: 100%; }
67
+ .mp-trigger { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; min-width: 0; min-height: 30px; padding: 0 9px 0 5px; font: inherit; font-size: 13px; font-weight: 500; color: var(--ib-text); background: var(--ib-seg); border: 1px solid var(--ib-card-border); border-radius: 8px; cursor: pointer; white-space: nowrap; transition: background-color var(--dur-press) var(--ease-out); }
68
+ .mp-trigger:not([aria-disabled="true"]):hover, .mp-trigger[aria-expanded="true"] { background: var(--mp-well); }
69
+ .mp-trigger:focus-visible { outline: 2px solid var(--border-focus); outline-offset: 2px; }
70
+ .mp-trigger[aria-busy="true"] { cursor: progress; }
71
+ .mp-trigger[aria-disabled="true"] { cursor: default; background: transparent; border-color: transparent; }
72
+ .mp-trigger[aria-disabled="true"] .mp-chev { display: none; }
73
+ .mp-trigger .mp-glyph { width: 18px; height: 18px; border-radius: 5px; font-size: 10px; }
74
+ .mp-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
75
+ .mp-level { flex: none; font-weight: 400; color: var(--ib-text-2); }
76
+ .mp-chev { flex: none; width: 6px; height: 6px; margin: 0 1px 0 2px; border-right: 1.5px solid var(--ib-muted); border-bottom: 1.5px solid var(--ib-muted); transform: translateY(-2px) rotate(45deg); transition: transform 300ms var(--ease-out); }
77
+ .mp-trigger[aria-expanded="true"] .mp-chev { transform: translateY(1px) rotate(225deg); }
78
+ .mp-glyph { display: inline-grid; flex: none; place-items: center; width: 22px; height: 22px; border-radius: 6px; background: var(--ib-card); box-shadow: inset 0 0 0 1px var(--ib-card-border); font-size: 10.5px; font-weight: 600; line-height: 1; color: var(--ib-text); }
79
+ .mp-note { max-width: 100%; font-size: 12px; line-height: 17px; color: var(--ib-text-2); overflow-wrap: anywhere; }
80
+ .mp-note.is-error { color: var(--st-fail-text); }
81
+ .mp-note:empty { display: none; }
82
+ /* A fixed 100px box at 0,0: tells the menu its containing block's origin and scale (transformed ancestors). */
83
+ .mp-probe { position: fixed; left: 0; top: 0; width: 100px; height: 0; visibility: hidden; pointer-events: none; }
84
+ /* The menu: position fixed (an ancestor's overflow clip never cuts it), placed from the trigger in script. */
85
+ .mp-pop { position: fixed; z-index: 150; left: 0; top: 0; box-sizing: border-box; width: min(336px, calc(100vw - 32px)); max-height: calc(100vh - 24px); overflow-y: auto; overscroll-behavior: contain; container: mp / inline-size; padding: 4px; border-radius: 16px; background: var(--ib-col); box-shadow: 0 0 0 1px var(--ib-card-border), var(--ib-shadow-raised); color: var(--ib-text-2); font-size: 13px; line-height: 20px; text-align: left; white-space: normal; transform-origin: top left; opacity: 0; visibility: hidden; pointer-events: none; transform: translateY(-4px) scale(0.96); transition: opacity 160ms ease, transform 220ms var(--ease-out), visibility 0s linear 220ms; }
86
+ .mp-pop[data-side="top"] { transform-origin: bottom left; transform: translateY(4px) scale(0.96); }
87
+ .mp-pop.is-open { opacity: 1; visibility: visible; transform: none; pointer-events: auto; transition: opacity 200ms ease, transform var(--dur-move) var(--ease-out), visibility 0s; }
88
+ .mp-card { padding: 6px; border-radius: 12px; background: var(--ib-card); box-shadow: 0 0 0 1px var(--ib-card-border), var(--ib-shadow); }
89
+ .mp-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; height: 24px; padding: 0 10px; font-size: 11px; line-height: 16px; color: var(--ib-muted); }
90
+ .mp-head-t { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
91
+ .mp-cols, .mp-bars { display: grid; flex: none; grid-template-columns: repeat(3, 36px); gap: 8px; }
92
+ .mp-cols { font-size: 10px; letter-spacing: -0.01em; }
93
+ .mp-cols > span { display: flex; justify-content: center; white-space: nowrap; }
94
+ .mp-wait { margin: 0 10px 4px; font-size: 12px; line-height: 17px; color: var(--st-wait); overflow-wrap: anywhere; }
95
+ .mp-list { display: grid; gap: 1px; border-radius: 10px; outline: none; }
96
+ .mp-opt { display: grid; grid-template-columns: 22px minmax(0, 1fr) auto; grid-template-areas: "g t b" "g d d"; align-items: center; column-gap: 10px; min-height: 48px; padding: 6px 10px; border-radius: 10px; cursor: pointer; transition: background-color var(--dur-press) ease, opacity 220ms ease; }
97
+ .mp-opt > .mp-glyph { grid-area: g; }
98
+ .mp-opt > .mp-bars { grid-area: b; }
99
+ .mp-opt[aria-disabled="true"] { cursor: default; }
100
+ .mp-list[aria-disabled="true"] .mp-opt:not([aria-selected="true"]) .mp-title { color: var(--ib-text-2); }
101
+ .mp-list[data-pointing] .mp-opt[data-active] { background: var(--mp-well); }
102
+ .mp-list[data-pointing] .mp-opt:not([data-active]) { opacity: 0.5; }
103
+ .mp-list:focus-visible .mp-opt[data-active] { background: var(--mp-well); box-shadow: inset 0 0 0 1.5px var(--border-focus); }
104
+ .mp-title { grid-area: t; display: flex; align-items: center; gap: 4px; min-width: 0; font-weight: 500; color: var(--ib-text); }
105
+ .mp-title-name { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
106
+ .mp-check { flex: none; font-size: 12px; line-height: 1; color: var(--accent); animation: mp-pop 320ms var(--ease-out) both; }
107
+ .mp-desc { grid-area: d; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; font-size: 12px; line-height: 17px; color: var(--ib-muted); overflow-wrap: anywhere; }
108
+ .mp-meter { display: grid; grid-template-columns: repeat(5, 1fr); gap: 2px; height: 5px; }
109
+ .mp-meter.is-none { display: block; height: auto; font-size: 11px; line-height: 1; text-align: center; color: var(--ib-muted); }
110
+ .mp-seg { border-radius: 1.5px; background: var(--mp-bar-off); transition: background-color 180ms ease; }
111
+ .mp-seg[data-on] { background: var(--mp-bar); }
112
+ .mp-opt[data-lit] .mp-seg[data-on] { background: var(--accent); transition-delay: calc(var(--c) * 70ms + var(--s) * 45ms); animation: mp-fill 360ms var(--ease-out) both; animation-delay: calc(var(--c) * 70ms + var(--s) * 45ms); }
113
+ .mp-settings { margin-top: 6px; padding: 6px 10px 4px; border-top: 1px solid var(--ib-card-border); }
114
+ .mp-effort-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; min-height: 22px; }
115
+ .mp-effort-label { flex: none; font-size: 12.5px; color: var(--ib-text-2); }
116
+ .mp-effort-cap { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; color: var(--st-wait); }
117
+ .mp-seg-ctl { position: relative; display: grid; grid-template-columns: repeat(var(--n), minmax(0, 1fr)); margin-top: 4px; padding: 2px; border-radius: 9px; background: var(--ib-seg); box-shadow: inset 0 0 0 1px var(--ib-card-border); }
118
+ .mp-thumb { position: absolute; top: 2px; bottom: 2px; left: 2px; width: calc((100% - 4px) / var(--n)); border-radius: 7px; background: var(--ib-seg-on); box-shadow: 0 0 0 1px var(--ib-card-border), 0 1px 2px color-mix(in srgb, #000 12%, transparent); transform: translateX(calc(var(--i) * 100%)); transition: transform var(--dur-move) var(--ease-out); }
119
+ .mp-seg-btn { position: relative; min-width: 0; min-height: 26px; padding: 0 2px; font: inherit; font-size: 12px; font-weight: 500; color: var(--ib-muted); background: none; border: 0; border-radius: 7px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: color 200ms ease; }
120
+ .mp-seg-btn[aria-checked="true"], .mp-seg-btn:hover { color: var(--ib-text); }
121
+ .mp-seg-btn:focus-visible { outline: 2px solid var(--border-focus); outline-offset: -2px; }
122
+ .mp-seg-ctl[aria-disabled="true"] .mp-seg-btn { cursor: progress; }
123
+ .mp-err { margin: 6px 10px 2px; font-size: 12px; line-height: 17px; color: var(--st-fail-text); overflow-wrap: anywhere; }
124
+ .mp-err:empty { display: none; }
125
+ .mp-foot { display: flex; align-items: center; gap: 6px; min-height: 38px; padding: 4px 6px 0 10px; }
126
+ .mp-caption { flex: 1; min-width: 0; overflow: hidden; font-size: 12px; color: var(--ib-muted); white-space: nowrap; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
127
+ .mp-caption b { font-weight: 500; color: var(--ib-text); }
128
+ .mp-keys { display: flex; flex: none; gap: 2px; }
129
+ .mp-keys kbd { display: grid; place-items: center; min-width: 18px; height: 18px; padding: 0 3px; border-radius: 5px; background: var(--ib-card); box-shadow: 0 0 0 1px var(--ib-card-border); font-family: inherit; font-size: 10.5px; line-height: 1; color: var(--ib-muted); }
130
+ @keyframes mp-pop { from { opacity: 0; transform: scale(0.6); } }
131
+ @keyframes mp-fill { 0% { transform: scaleY(1); } 45% { transform: scaleY(1.6); } 100% { transform: scaleY(1); } }
132
+ @container mp (max-width: 300px) {
133
+ .mp-opt .mp-glyph, .mp-keys { display: none; }
134
+ .mp-opt { grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "t b" "d d"; }
135
+ }
136
+ /* Touch / narrow: the dashboard's own block — trigger and levels ≥44px, rows ≥54px, no key hints. */
137
+ @media (pointer: coarse), (max-width: 639px) {
138
+ .mp-trigger, .mp-seg-btn { min-height: 44px; }
139
+ .mp-opt { min-height: 54px; }
140
+ .mp-keys { display: none; }
141
+ }
142
+ /* The global rule shortens durations; scale, the staggered fill and the check pop stop here too. */
143
+ @media (prefers-reduced-motion: reduce) {
144
+ .mp-pop, .mp-pop[data-side="top"] { transform: none; }
145
+ .mp-opt[data-lit] .mp-seg[data-on] { animation: none; transition-delay: 0s; }
146
+ .mp-check { animation: none; }
147
+ }
148
+ `;
149
+ const MODEL_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
150
+ const EFFORT_LABELS = {
151
+ low: "Low",
152
+ medium: "Medium",
153
+ high: "High",
154
+ xhigh: "Extra high",
155
+ max: "Max"
156
+ };
157
+ const EFFORT_SHORT = { low: "Low", medium: "Medium", high: "High", xhigh: "XHigh", max: "Max" };
158
+ const ROLL_MS = 560;
159
+ const WATCH_MS = 150;
160
+ const STRINGS = {
161
+ en: {
162
+ model: "Model",
163
+ trigger: (name, level) => `Model ${name}${level ? `, effort ${level}` : ""} — change`,
164
+ dialog: "Model and effort for this session",
165
+ head: "This session model",
166
+ cols: ["Window", "Compact", "Cost"],
167
+ list: "Models",
168
+ after: "switches after compaction",
169
+ outside: "Current · not in the switch list",
170
+ outsideTray: "Not in the switch list · price unknown",
171
+ outsideSr: "Not in the switch list, price unknown",
172
+ win: (w) => `window ${w}`,
173
+ winSr: (w) => `window ${w}`,
174
+ tray: (i, o) => `Input ${i} · Output ${o} / M`,
175
+ noPrice: "Price —",
176
+ priceWords: (i, o) => `input ${i} · output ${o} per million tokens`,
177
+ noPriceWords: "price unknown",
178
+ switching: "Switching model…",
179
+ effort: "Effort",
180
+ cap: (set, runs) => `Set ${set} → this model runs ${runs}`
181
+ },
182
+ ko: {
183
+ model: "모델",
184
+ trigger: (name, level) => `모델 ${name}${level ? `, effort ${level}` : ""} — 바꾸기`,
185
+ dialog: "이 세션의 모델 · effort",
186
+ head: "이 세션 모델",
187
+ cols: ["창", "압축", "비용"],
188
+ list: "모델",
189
+ after: "compact 후 전환",
190
+ outside: "현재 · 전환 목록 밖",
191
+ outsideTray: "전환 목록 밖 모델 · 가격 정보 없음",
192
+ outsideSr: "전환 목록 밖 모델, 가격 정보 없음",
193
+ win: (w) => `창 ${w}`,
194
+ winSr: (w) => `창 ${w}`,
195
+ tray: (i, o) => `입력 ${i} · 출력 ${o} / M`,
196
+ noPrice: "가격 정보 없음",
197
+ priceWords: (i, o) => `입력 ${i} · 출력 ${o} / 백만 토큰`,
198
+ noPriceWords: "가격 정보 없음",
199
+ switching: "모델 전환 중…",
200
+ effort: "Effort",
201
+ cap: (set, runs) => `설정 ${set} → 이 모델은 ${runs}까지 실행`
202
+ }
203
+ };
204
+ const CHECK = "&#x2713;";
205
+ const num = (v) => {
206
+ const n = Number(v);
207
+ return Number.isFinite(n) ? n : 0;
208
+ };
209
+ function meterSegments(v, max) {
210
+ const a = num(v);
211
+ const m = num(max);
212
+ if (!(a > 0) || !(m > 0)) return 0;
213
+ return Math.max(1, Math.min(5, Math.round(5 * a / m)));
214
+ }
215
+ function barOr(v, fallback) {
216
+ if (v === void 0 || v === null) return fallback;
217
+ return Math.max(0, Math.min(5, Math.round(num(v))));
218
+ }
219
+ function validModelPrice(p) {
220
+ if (!p) return null;
221
+ const input = Number(p.input);
222
+ const output = Number(p.output);
223
+ return Number.isFinite(input) && Number.isFinite(output) && input >= 0 && output >= 0 ? { input, output } : null;
224
+ }
225
+ function modelMeters(models) {
226
+ let win = 0;
227
+ let ac = 0;
228
+ let out = 0;
229
+ for (const m of models) {
230
+ win = Math.max(win, num(m.window));
231
+ ac = Math.max(ac, num(m.autoCompactAt));
232
+ out = Math.max(out, validModelPrice(m.price)?.output ?? 0);
233
+ }
234
+ return models.map((m) => {
235
+ const p = validModelPrice(m.price);
236
+ return [
237
+ barOr(m.bars?.window, meterSegments(m.window, win)),
238
+ barOr(m.bars?.compaction, meterSegments(m.autoCompactAt, ac)),
239
+ barOr(m.bars?.cost, p ? meterSegments(p.output, out) : 0)
240
+ ];
241
+ });
242
+ }
243
+ function fmtModelTokens(n) {
244
+ let v = Math.max(0, num(n));
245
+ if (v < 999.5) return String(Math.round(v));
246
+ for (const unit of ["K", "M", "B", "T"]) {
247
+ v /= 1e3;
248
+ if (v < 999.95) return `${v.toFixed(1)}${unit}`;
249
+ }
250
+ return "999.9T+";
251
+ }
252
+ function fmtModelPrice(v, ref) {
253
+ const n = num(v);
254
+ const t = ref === void 0 ? n : num(ref);
255
+ return `$${Math.round(t) === t ? String(Math.round(n)) : n.toFixed(2)}`;
256
+ }
257
+ function modelEffortLevels(model) {
258
+ if (!model || !Array.isArray(model.effortLevels)) return MODEL_EFFORT_LEVELS.slice();
259
+ const out = [];
260
+ for (const l of model.effortLevels) {
261
+ const s = String(l ?? "");
262
+ if (s && !out.includes(s)) out.push(s);
263
+ }
264
+ return out;
265
+ }
266
+ function modelEffortLabel(level) {
267
+ return level ? EFFORT_LABELS[level] ?? level : "";
268
+ }
269
+ function modelGlyph(m) {
270
+ const src = String(m.short || m.label || m.id || "?");
271
+ return (Array.from(src)[0] ?? "?").toUpperCase();
272
+ }
273
+ const MIN_ROOM = 180;
274
+ function placeModelMenu(r, w, h, vw, vh, side = "bottom") {
275
+ const gap = 6;
276
+ const edge = 12;
277
+ const below = vh - r.bottom - gap - edge;
278
+ const above = r.top - gap - edge;
279
+ const s = side === "top" ? h > above && below > above ? "bottom" : "top" : h > below && above > below ? "top" : "bottom";
280
+ const room = s === "top" ? above : below;
281
+ const full = Math.max(0, vh - 2 * edge);
282
+ const fit = h > room && room >= MIN_ROOM ? room : Math.min(h, full);
283
+ const left = Math.max(edge, Math.min(r.left, vw - edge - w));
284
+ let top = s === "top" ? r.top - gap - fit : r.bottom + gap;
285
+ top = Math.max(edge, Math.min(top, vh - edge - fit));
286
+ return {
287
+ left: Math.round(left),
288
+ top: Math.round(top),
289
+ side: s,
290
+ maxHeight: Math.floor(h > room && room >= MIN_ROOM ? room : full)
291
+ };
292
+ }
293
+ function flatParent(n) {
294
+ if (n.assignedSlot) return n.assignedSlot;
295
+ const p = n.parentNode;
296
+ if (!p) return null;
297
+ if (p.nodeType === 11) return p.host ?? null;
298
+ return p.nodeType === 1 ? p : null;
299
+ }
300
+ function anchorSpot(el) {
301
+ const r = el.getBoundingClientRect();
302
+ let x = r.left;
303
+ let y = r.top;
304
+ for (let n = flatParent(el); n; n = flatParent(n)) {
305
+ x += n.scrollLeft || 0;
306
+ y += n.scrollTop || 0;
307
+ if (getComputedStyle(n).position === "fixed") break;
308
+ }
309
+ return { left: x, top: y };
310
+ }
311
+ function anchorMoved(at, now) {
312
+ if (!at || !now) return false;
313
+ return Math.abs(now.left - at.left) > 2 || Math.abs(now.top - at.top) > 2;
314
+ }
315
+ function pickerOptions(d) {
316
+ const seen = /* @__PURE__ */ new Set();
317
+ const models = [];
318
+ for (const m of Array.isArray(d.models) ? d.models : []) {
319
+ if (!m || typeof m.id !== "string" || !m.id || seen.has(m.id)) continue;
320
+ seen.add(m.id);
321
+ models.push(m);
322
+ }
323
+ const meters2 = modelMeters(models);
324
+ const cur = d.value?.model ?? "";
325
+ const out = [];
326
+ if (cur && !seen.has(cur)) out.push({ key: "", model: { id: cur, label: cur }, listed: false, meters: [0, 0, 0] });
327
+ models.forEach((m, i) => {
328
+ out.push({ key: m.id, model: m, listed: true, meters: meters2[i] });
329
+ });
330
+ return out;
331
+ }
332
+ function currentIndex(opts, cur) {
333
+ if (!cur) return -1;
334
+ return opts.findIndex((o) => o.key === "" || o.key === cur);
335
+ }
336
+ class UiModelPicker extends UiElement {
337
+ static styles = [styles];
338
+ static get observedAttributes() {
339
+ return ["lang", "open", "side"];
340
+ }
341
+ #open = false;
342
+ #reflecting = false;
343
+ #activeKey = null;
344
+ #pointing = false;
345
+ #shown = null;
346
+ #raf = 0;
347
+ #placeRaf = 0;
348
+ #watch = 0;
349
+ #attached = false;
350
+ #at = null;
351
+ #last = null;
352
+ #pos = null;
353
+ constructor() {
354
+ super();
355
+ this.root.addEventListener("click", (e) => this.#onClick(e));
356
+ this.root.addEventListener("keydown", (e) => this.#onKey(e));
357
+ this.root.addEventListener("pointerover", (e) => this.#onPointer(e));
358
+ this.root.addEventListener("pointerout", (e) => this.#onPointerOut(e));
359
+ }
360
+ /** Whether the menu is open. Setting it opens/closes without moving focus and without `ui-open-change`. */
361
+ get open() {
362
+ return this.#open;
363
+ }
364
+ set open(v) {
365
+ this.#setOpen(!!v, { user: false });
366
+ }
367
+ attributeChangedCallback(name, _old, value) {
368
+ if (name === "open") {
369
+ if (!this.#reflecting) this.#setOpen(value !== null, { user: false });
370
+ return;
371
+ }
372
+ this.requestRender();
373
+ }
374
+ connectedCallback() {
375
+ super.connectedCallback();
376
+ if (this.#open) this.#attach();
377
+ }
378
+ disconnectedCallback() {
379
+ this.#detach();
380
+ }
381
+ get #t() {
382
+ return this.getAttribute("lang") === "ko" ? STRINGS.ko : STRINGS.en;
383
+ }
384
+ get #side() {
385
+ return this.getAttribute("side") === "top" ? "top" : "bottom";
386
+ }
387
+ #off() {
388
+ return !!this.data?.disabled;
389
+ }
390
+ #opts() {
391
+ return this.data ? pickerOptions(this.data) : [];
392
+ }
393
+ /** The active row: the model last pointed at while it is still listed, else the session's model. */
394
+ #activeIndex(opts, cur) {
395
+ const i = this.#activeKey === null ? -1 : opts.findIndex((o) => o.key === this.#activeKey);
396
+ return i >= 0 ? i : Math.max(cur, 0);
397
+ }
398
+ // ── open / close ──────────────────────────────────────────────────────────
399
+ #setOpen(v, o) {
400
+ if (v === this.#open) {
401
+ if (v && o.focusList) this.#focusList();
402
+ return;
403
+ }
404
+ if (v) {
405
+ if (this.#off()) {
406
+ this.#reflect(false);
407
+ return;
408
+ }
409
+ this.#open = true;
410
+ this.#activeKey = null;
411
+ this.#pointing = false;
412
+ this.#shown = null;
413
+ this.#at = null;
414
+ this.#last = null;
415
+ this.#reflect(true);
416
+ if (this.isConnected) {
417
+ this.renderNow();
418
+ if (!this.#open) return;
419
+ this.#attach();
420
+ if (o.focusList) this.#focusList();
421
+ }
422
+ } else {
423
+ if (o.returnFocus) this.$(".mp-trigger")?.focus();
424
+ this.#open = false;
425
+ this.#pointing = false;
426
+ this.#at = null;
427
+ this.#last = null;
428
+ this.#reflect(false);
429
+ this.#detach();
430
+ if (this.isConnected) this.renderNow();
431
+ }
432
+ if (o.user) this.emit("ui-open-change", { open: v });
433
+ }
434
+ #reflect(v) {
435
+ if (this.hasAttribute("open") === v) return;
436
+ this.#reflecting = true;
437
+ this.toggleAttribute("open", v);
438
+ this.#reflecting = false;
439
+ }
440
+ #focusList() {
441
+ this.$(".mp-list")?.focus({ preventScroll: true });
442
+ }
443
+ #onOutside = (e) => {
444
+ if (this.#open && !e.composedPath().includes(this)) this.#setOpen(false, { user: true });
445
+ };
446
+ #onViewport = () => this.#queuePlace();
447
+ #tick = () => {
448
+ if (!this.#open) return;
449
+ const trig = this.$(".mp-trigger");
450
+ if (!trig) return;
451
+ if (!this.#at) {
452
+ this.#place();
453
+ return;
454
+ }
455
+ const r = trig.getBoundingClientRect();
456
+ if (!r.width && !r.height || anchorMoved(this.#at, anchorSpot(trig))) {
457
+ this.#setOpen(false, { user: true });
458
+ return;
459
+ }
460
+ if (!this.#last || r.left !== this.#last.left || r.top !== this.#last.top) this.#place();
461
+ };
462
+ #attach() {
463
+ if (this.#attached || !this.#open || !this.isConnected) return;
464
+ this.#attached = true;
465
+ document.addEventListener("pointerdown", this.#onOutside, true);
466
+ document.addEventListener("focusin", this.#onOutside);
467
+ document.addEventListener("scroll", this.#onViewport, { capture: true, passive: true });
468
+ window.addEventListener("resize", this.#onViewport);
469
+ this.#watch = window.setInterval(this.#tick, WATCH_MS);
470
+ this.#queuePlace();
471
+ }
472
+ #detach() {
473
+ if (this.#attached) {
474
+ document.removeEventListener("pointerdown", this.#onOutside, true);
475
+ document.removeEventListener("focusin", this.#onOutside);
476
+ document.removeEventListener("scroll", this.#onViewport, { capture: true });
477
+ window.removeEventListener("resize", this.#onViewport);
478
+ this.#attached = false;
479
+ }
480
+ clearInterval(this.#watch);
481
+ this.#watch = 0;
482
+ if (this.#placeRaf) cancelAnimationFrame(this.#placeRaf);
483
+ this.#placeRaf = 0;
484
+ if (this.#raf) cancelAnimationFrame(this.#raf);
485
+ this.#raf = 0;
486
+ }
487
+ #queuePlace() {
488
+ if (!this.#open || this.#placeRaf || typeof requestAnimationFrame !== "function") return;
489
+ this.#placeRaf = requestAnimationFrame(() => {
490
+ this.#placeRaf = 0;
491
+ this.#place();
492
+ });
493
+ }
494
+ /**
495
+ * Fixed positioning from the trigger's rect. The menu is `position: fixed`, so the open card's clip never
496
+ * cuts it; when an ancestor is a containing block for fixed elements (a transform, a filter) the probe —
497
+ * a fixed 100px box at 0,0 — tells its origin and scale and the viewport position is mapped into it.
498
+ */
499
+ #place() {
500
+ if (!this.#open || !this.isConnected) return;
501
+ const trig = this.$(".mp-trigger");
502
+ const pop = this.$(".mp-pop");
503
+ const probe = this.$(".mp-probe");
504
+ if (!trig || !pop) return;
505
+ const r = trig.getBoundingClientRect();
506
+ if (!r.width && !r.height) {
507
+ if (this.#at) this.#setOpen(false, { user: true });
508
+ return;
509
+ }
510
+ const vw = document.documentElement.clientWidth || window.innerWidth;
511
+ const vh = window.innerHeight;
512
+ if (r.bottom < 0 || r.top > vh || r.right < 0 || r.left > vw) {
513
+ this.#setOpen(false, { user: true });
514
+ return;
515
+ }
516
+ const o = probe?.getBoundingClientRect();
517
+ const k = o && o.width > 0 ? o.width / 100 : 1;
518
+ const ox = o?.left ?? 0;
519
+ const oy = o?.top ?? 0;
520
+ const p = placeModelMenu(r, pop.offsetWidth * k, pop.scrollHeight * k, vw, vh, this.#side);
521
+ this.#pos = {
522
+ left: Math.round((p.left - ox) / k),
523
+ top: Math.round((p.top - oy) / k),
524
+ side: p.side,
525
+ maxHeight: Math.floor(p.maxHeight / k)
526
+ };
527
+ pop.style.left = `${this.#pos.left}px`;
528
+ pop.style.top = `${this.#pos.top}px`;
529
+ pop.style.maxHeight = `${this.#pos.maxHeight}px`;
530
+ pop.setAttribute("data-side", p.side);
531
+ this.#last = { left: r.left, top: r.top };
532
+ if (!this.#at) this.#at = anchorSpot(trig);
533
+ }
534
+ // ── picks ─────────────────────────────────────────────────────────────────
535
+ /** A row picks its model — by id, never by position. The current row only closes; locked rows do nothing. */
536
+ #pickKey(key) {
537
+ const d = this.data;
538
+ if (!d || this.#off()) return;
539
+ const opts = this.#opts();
540
+ const i = opts.findIndex((o2) => o2.key === key);
541
+ if (i < 0) return;
542
+ if (i === currentIndex(opts, d.value?.model ?? "")) {
543
+ this.#setOpen(false, { user: true, returnFocus: true });
544
+ return;
545
+ }
546
+ const o = opts[i];
547
+ if (!o.listed || d.busy?.model) return;
548
+ this.#setOpen(false, { user: true, returnFocus: true });
549
+ const detail = { model: o.model.id, effort: d.value?.effort, changed: "model" };
550
+ if (this.emit("ui-change", detail) && this.data && this.data.value === d.value)
551
+ this.data = { ...this.data, value: { model: o.model.id, effort: d.value?.effort } };
552
+ }
553
+ /** A level keeps the menu open (it is a setting of the same choice). */
554
+ #pickEffort(level) {
555
+ const d = this.data;
556
+ if (!d || this.#off() || d.busy?.effort || !level || level === d.value?.effort) return;
557
+ const model = d.value?.model ?? "";
558
+ const detail = { model, effort: level, changed: "effort" };
559
+ if (this.emit("ui-change", detail) && this.data && this.data.value === d.value)
560
+ this.data = { ...this.data, value: { model, effort: level } };
561
+ }
562
+ // ── input ─────────────────────────────────────────────────────────────────
563
+ #onClick(e) {
564
+ const t = e.target;
565
+ if (!t?.closest) return;
566
+ if (t.closest(".mp-trigger")) {
567
+ if (this.#off()) return;
568
+ const opening = !this.#open;
569
+ this.#setOpen(opening, { user: true, focusList: opening, returnFocus: !opening });
570
+ return;
571
+ }
572
+ if (!this.#open) return;
573
+ const opt = t.closest(".mp-opt");
574
+ if (opt) {
575
+ this.#pickKey(opt.getAttribute("data-k") ?? "");
576
+ return;
577
+ }
578
+ const b = t.closest(".mp-seg-btn");
579
+ if (b && b.parentElement?.getAttribute("aria-disabled") !== "true")
580
+ this.#pickEffort(b.getAttribute("data-effort") ?? "");
581
+ }
582
+ #onKey(e) {
583
+ const t = e.target;
584
+ if (!t?.closest) return;
585
+ if (t.closest(".mp-trigger")) {
586
+ this.#triggerKey(e);
587
+ return;
588
+ }
589
+ if (!t.closest(".mp-pop") || !this.#open) return;
590
+ e.stopPropagation();
591
+ const onList = t.classList.contains("mp-list");
592
+ if (e.key === "Escape") {
593
+ e.preventDefault();
594
+ this.#setOpen(false, { user: true, returnFocus: true });
595
+ } else if (e.key === "Tab") {
596
+ e.preventDefault();
597
+ const level = this.$('.mp-seg-btn[tabindex="0"]');
598
+ const list = this.$(".mp-list");
599
+ (onList ? level ?? list : list)?.focus();
600
+ } else if (onList) {
601
+ this.#listKey(e);
602
+ } else {
603
+ const radio = t.closest(".mp-seg-btn");
604
+ if (radio) this.#effortKey(e, radio);
605
+ }
606
+ }
607
+ /** ↓ / ↑ on the trigger open the menu (Enter and Space are the button's own click); Esc closes it. */
608
+ #triggerKey(e) {
609
+ if (e.key === "Escape" && this.#open) {
610
+ e.preventDefault();
611
+ e.stopPropagation();
612
+ this.#setOpen(false, { user: true, returnFocus: true });
613
+ return;
614
+ }
615
+ if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
616
+ e.preventDefault();
617
+ if (this.#off()) return;
618
+ this.#setOpen(true, { user: true, focusList: true });
619
+ }
620
+ /** ↑ ↓ (wrapping) · Home · End point at a row; Enter · Space pick it. */
621
+ #listKey(e) {
622
+ const opts = this.#opts();
623
+ const n = opts.length;
624
+ if (!n) return;
625
+ const k = e.key;
626
+ const cur = currentIndex(opts, this.data?.value?.model ?? "");
627
+ if (k === "ArrowDown" || k === "ArrowUp" || k === "Home" || k === "End") {
628
+ e.preventDefault();
629
+ const i = k === "Home" ? 0 : k === "End" ? n - 1 : this.#activeIndex(opts, cur) + (k === "ArrowDown" ? 1 : -1);
630
+ this.#activeKey = opts[(i % n + n) % n].key;
631
+ this.#pointing = true;
632
+ this.renderNow();
633
+ this.$(".mp-opt[data-active]")?.scrollIntoView?.({ block: "nearest" });
634
+ } else if (k === "Enter" || k === " ") {
635
+ e.preventDefault();
636
+ this.#pickKey(opts[this.#activeIndex(opts, cur)].key);
637
+ }
638
+ }
639
+ /** Effort: the arrows move and select at once (radio pattern); Home · End. */
640
+ #effortKey(e, radio) {
641
+ const group = radio.parentElement;
642
+ if (!group) return;
643
+ const btns = Array.from(group.querySelectorAll(".mp-seg-btn"));
644
+ const n = btns.length;
645
+ const i = btns.indexOf(radio);
646
+ const k = e.key;
647
+ const j = k === "ArrowRight" || k === "ArrowDown" ? i + 1 : k === "ArrowLeft" || k === "ArrowUp" ? i - 1 : k === "Home" ? 0 : k === "End" ? n - 1 : null;
648
+ if (j === null || !n) return;
649
+ e.preventDefault();
650
+ if (group.getAttribute("aria-disabled") === "true") return;
651
+ const next = btns[(j % n + n) % n];
652
+ next.focus();
653
+ this.#pickEffort(next.getAttribute("data-effort") ?? "");
654
+ }
655
+ /** The mouse points at a row: it lights, the others step back; leaving the list lets go. */
656
+ #onPointer(e) {
657
+ if (!this.#open || e.pointerType && e.pointerType !== "mouse") return;
658
+ const opt = e.target?.closest?.(".mp-opt");
659
+ if (!opt) return;
660
+ const key = opt.getAttribute("data-k") ?? "";
661
+ if (this.#pointing && this.#activeKey === key) return;
662
+ this.#activeKey = key;
663
+ this.#pointing = true;
664
+ this.renderNow();
665
+ }
666
+ #onPointerOut(e) {
667
+ if (!this.#open || !this.#pointing || e.pointerType && e.pointerType !== "mouse") return;
668
+ const list = this.$(".mp-list");
669
+ const to = e.relatedTarget;
670
+ if (!list || !list.contains(e.target) || to && list.contains(to)) return;
671
+ this.#pointing = false;
672
+ this.renderNow();
673
+ }
674
+ // ── render ────────────────────────────────────────────────────────────────
675
+ /** Patch the live tree instead of replacing it: the open/close transition, the thumb and focus survive. */
676
+ update() {
677
+ morphInto(this.content, this.render());
678
+ this.updated();
679
+ }
680
+ render() {
681
+ const d = this.data;
682
+ if (!d) return "";
683
+ const t = this.#t;
684
+ const v = d.value ?? { model: "" };
685
+ const opts = pickerOptions(d);
686
+ const cur = currentIndex(opts, v.model ?? "");
687
+ const curOpt = cur >= 0 ? opts[cur] : null;
688
+ const open = this.#open;
689
+ const off = !!d.disabled;
690
+ const why = typeof d.disabled === "string" ? d.disabled : "";
691
+ const busy = !!(d.busy?.model || d.busy?.effort);
692
+ const name = curOpt ? curOpt.model.label || curOpt.model.id : t.model;
693
+ const level = modelEffortLabel(v.effectiveEffort || v.effort);
694
+ const pending = typeof d.busy?.model === "string" ? d.busy.model : "";
695
+ const trigger = `<button type="button" class="mp-trigger" part="trigger" data-key="mp-trigger" aria-haspopup="dialog" aria-expanded="${open ? "true" : "false"}" aria-controls="mp-menu" aria-label="${escAttr(t.trigger(name, level))}"${busy ? ' aria-busy="true"' : ""}${off ? ` aria-disabled="true"${why ? ` title="${escAttr(why)}" aria-describedby="mp-why"` : ""}` : ""}><span class="mp-glyph" aria-hidden="true">${esc(modelGlyph(curOpt ? curOpt.model : { label: name }))}</span><span class="mp-name">${esc(
696
+ name
697
+ )}</span>${level ? `<span class="mp-level">&middot; ${esc(level)}</span>` : ""}<span class="mp-chev" aria-hidden="true"></span></button>`;
698
+ return `<div class="mp">${trigger}${why ? `<span class="sr-only" id="mp-why">${esc(why)}</span>` : ""}
699
+ <div class="mp-note" role="status">${!open && pending ? esc(pending) : ""}</div>
700
+ <div class="mp-note is-error" role="alert">${!open && d.error ? esc(d.error) : ""}</div>
701
+ <i class="mp-probe" aria-hidden="true"></i>${this.#menu(d, opts, cur, t)}</div>`;
702
+ }
703
+ #menu(d, opts, cur, t) {
704
+ const open = this.#open;
705
+ const active = opts.length ? this.#activeIndex(opts, cur) : -1;
706
+ const lit = this.#pointing ? active : cur;
707
+ const used = num(d.contextTokens);
708
+ const locked = !!d.busy?.model;
709
+ const wait = typeof d.busy?.model === "string" && d.busy.model ? d.busy.model : t.switching;
710
+ const pos = this.#pos;
711
+ let rows = "";
712
+ opts.forEach((o, i) => {
713
+ const m = o.model;
714
+ const isCur = i === cur;
715
+ const name = m.label || m.id;
716
+ const ac = num(m.autoCompactAt);
717
+ const win = num(m.window);
718
+ const price = validModelPrice(m.price);
719
+ const desc = o.listed ? [
720
+ m.short || m.id,
721
+ ac > 0 ? `autocompact ${fmtModelTokens(ac)}` : "",
722
+ !isCur && ac > 0 && used > ac ? t.after : ""
723
+ ].filter(Boolean).join(" · ") : t.outside;
724
+ const facts = o.listed ? [
725
+ win > 0 ? t.winSr(fmtModelTokens(win)) : "",
726
+ price ? t.priceWords(fmtModelPrice(price.input), fmtModelPrice(price.output)) : t.noPriceWords
727
+ ].filter(Boolean).join(", ") : "";
728
+ const label = `${name}, ${desc}${facts ? `. ${facts}` : ""}`;
729
+ rows += `<div class="mp-opt" id="mp-opt-${i}" role="option" data-mk="opt:${escAttr(o.key)}" data-k="${escAttr(o.key)}" aria-selected="${isCur ? "true" : "false"}"${locked || !o.listed ? ' aria-disabled="true"' : ""}${i === active ? ' data-active=""' : ""}${i === lit ? ' data-lit=""' : ""} aria-label="${escAttr(label)}"><span class="mp-glyph" aria-hidden="true">${esc(modelGlyph(m))}</span><span class="mp-title"><span class="mp-title-name">${esc(
730
+ name
731
+ )}</span>${isCur ? `<span class="mp-check" aria-hidden="true">${CHECK}</span>` : ""}</span>${meters(o.meters)}<span class="mp-desc">${esc(desc)}</span></div>`;
732
+ });
733
+ const tray = this.#tray(lit >= 0 ? opts[lit] : void 0, t);
734
+ return `<div class="mp-pop${open ? " is-open" : ""}" part="menu" id="mp-menu" role="dialog" aria-label="${escAttr(t.dialog)}"${open ? "" : " inert"} data-side="${pos?.side ?? this.#side}"${pos ? ` style="left:${pos.left}px;top:${pos.top}px;max-height:${pos.maxHeight}px"` : ""}><div class="mp-card"><div class="mp-head" aria-hidden="true"><span class="mp-head-t">${esc(
735
+ t.head
736
+ )}</span><span class="mp-cols">${t.cols.map((c) => `<span>${esc(c)}</span>`).join("")}</span></div>${locked ? `<div class="mp-wait" id="mp-wait">${esc(wait)}</div>` : ""}<div class="mp-list" role="listbox" aria-label="${escAttr(t.list)}" tabindex="0" data-key="mp-list"${active >= 0 ? ` aria-activedescendant="mp-opt-${active}"` : ""}${locked ? ' aria-disabled="true" aria-describedby="mp-wait"' : ""}${this.#pointing ? ' data-pointing=""' : ""}>${rows}</div>${this.#effort(
737
+ d,
738
+ cur >= 0 ? opts[cur] : void 0,
739
+ t
740
+ )}<div class="mp-err" role="alert">${open && d.error ? esc(d.error) : ""}</div></div><div class="mp-foot"><span class="mp-caption" aria-hidden="true">${tray.html}</span><span class="mp-keys" aria-hidden="true"><kbd>&#x2191;&#x2193;</kbd><kbd>&#x21B5;</kbd></span><span class="sr-only mp-sr" aria-live="polite">${esc(
741
+ tray.sr
742
+ )}</span></div></div>`;
743
+ }
744
+ /** Effort as a radiogroup with one tab stop; checked = the level that runs; only the model's levels. */
745
+ #effort(d, cur, t) {
746
+ const levels = modelEffortLevels(cur?.listed ? cur.model : null);
747
+ if (!levels.length) return "";
748
+ const v = d.value ?? {};
749
+ const on = v.effectiveEffort || v.effort || "";
750
+ const idx = levels.indexOf(on);
751
+ const capped = !!(v.effectiveEffort && v.effort && v.effectiveEffort !== v.effort);
752
+ const busy = !!d.busy?.effort;
753
+ let btns = "";
754
+ levels.forEach((l, i) => {
755
+ const checked = l === on;
756
+ btns += `<button type="button" class="mp-seg-btn" role="radio" data-mk="eff:${escAttr(l)}" data-key="eff:${escAttr(
757
+ l
758
+ )}" data-effort="${escAttr(l)}" aria-checked="${checked ? "true" : "false"}" tabindex="${checked || idx < 0 && i === 0 ? "0" : "-1"}" aria-label="${escAttr(modelEffortLabel(l))}">${esc(EFFORT_SHORT[l] ?? l)}</button>`;
759
+ });
760
+ return `<div class="mp-settings"><div class="mp-effort-head"><span class="mp-effort-label" id="mp-effort-label">${esc(
761
+ t.effort
762
+ )}</span>${capped ? `<span class="mp-effort-cap">${esc(t.cap(modelEffortLabel(v.effort), modelEffortLabel(v.effectiveEffort)))}</span>` : ""}</div><div class="mp-seg-ctl" role="radiogroup" aria-labelledby="mp-effort-label"${busy ? ' aria-disabled="true" aria-busy="true"' : ""} style="--i:${Math.max(0, idx)};--n:${levels.length}">${idx >= 0 ? '<span class="mp-thumb" aria-hidden="true"></span>' : ""}${btns}</div></div>`;
763
+ }
764
+ /** The tray of the lit row, and the sentence the live region reads once. */
765
+ #tray(o, t) {
766
+ if (!o) return { html: "", sr: "" };
767
+ if (!o.listed) return { html: esc(t.outsideTray), sr: t.outsideSr };
768
+ const m = o.model;
769
+ const p = validModelPrice(m.price);
770
+ const win = num(m.window) > 0 ? fmtModelTokens(m.window) : "";
771
+ const html = [
772
+ p ? t.tray(
773
+ `<b class="mp-roll" data-roll="input">${fmtModelPrice(p.input)}</b>`,
774
+ `<b class="mp-roll" data-roll="output">${fmtModelPrice(p.output)}</b>`
775
+ ) : esc(t.noPrice),
776
+ win ? esc(t.win(win)) : ""
777
+ ].filter(Boolean).join(" · ");
778
+ const sr = `${m.label || m.id}: ${[
779
+ p ? t.priceWords(fmtModelPrice(p.input), fmtModelPrice(p.output)) : t.noPriceWords,
780
+ win ? t.winSr(win) : ""
781
+ ].filter(Boolean).join(", ")}`;
782
+ return { html, sr };
783
+ }
784
+ updated() {
785
+ if (!this.#open) {
786
+ if (this.#raf) cancelAnimationFrame(this.#raf);
787
+ this.#raf = 0;
788
+ return;
789
+ }
790
+ if (this.#off()) {
791
+ this.#setOpen(false, { user: true });
792
+ return;
793
+ }
794
+ this.#place();
795
+ this.#rollTray();
796
+ }
797
+ /** The tray's prices roll from what is shown to the lit row's (560ms, ease-out quart); reduced motion jumps. */
798
+ #rollTray() {
799
+ if (this.#raf) cancelAnimationFrame(this.#raf);
800
+ this.#raf = 0;
801
+ const opts = this.#opts();
802
+ const cur = currentIndex(opts, this.data?.value?.model ?? "");
803
+ const lit = this.#pointing ? this.#activeIndex(opts, cur) : cur;
804
+ const o = lit >= 0 ? opts[lit] : void 0;
805
+ const to = o?.listed ? validModelPrice(o.model.price) : null;
806
+ const from = this.#shown;
807
+ this.#shown = to;
808
+ if (!to || !from || from.input === to.input && from.output === to.output) return;
809
+ if (prefersReducedMotion() || typeof requestAnimationFrame !== "function") return;
810
+ const els = this.$$(".mp-roll");
811
+ const t0 = performance.now();
812
+ const step = (now) => {
813
+ const p = Math.min(1, Math.max(0, (now - t0) / ROLL_MS));
814
+ const e = 1 - (1 - p) ** 4;
815
+ const at = {
816
+ input: from.input + (to.input - from.input) * e,
817
+ output: from.output + (to.output - from.output) * e
818
+ };
819
+ this.#shown = p < 1 ? at : to;
820
+ for (const el of els) {
821
+ const k = el.getAttribute("data-roll") === "input" ? "input" : "output";
822
+ el.textContent = fmtModelPrice(at[k], to[k]);
823
+ }
824
+ this.#raf = p < 1 ? requestAnimationFrame(step) : 0;
825
+ };
826
+ step(t0);
827
+ }
828
+ }
829
+ function meters(vals) {
830
+ let html = '<span class="mp-bars" aria-hidden="true">';
831
+ vals.forEach((v, c) => {
832
+ if (!v) {
833
+ html += '<span class="mp-meter is-none">&mdash;</span>';
834
+ return;
835
+ }
836
+ html += `<span class="mp-meter" style="--c:${c}">`;
837
+ for (let k = 0; k < 5; k++) html += `<span class="mp-seg" style="--s:${k}"${k < v ? ' data-on=""' : ""}></span>`;
838
+ html += "</span>";
839
+ });
840
+ return `${html}</span>`;
841
+ }
842
+ define("ui-model-picker", UiModelPicker);
843
+ export {
844
+ MODEL_EFFORT_LEVELS,
845
+ UiModelPicker,
846
+ anchorMoved,
847
+ anchorSpot,
848
+ fmtModelPrice,
849
+ fmtModelTokens,
850
+ meterSegments,
851
+ modelEffortLabel,
852
+ modelEffortLevels,
853
+ modelGlyph,
854
+ modelMeters,
855
+ placeModelMenu,
856
+ validModelPrice
857
+ };
858
+ //# sourceMappingURL=model-picker.js.map