@2lab.ai/ui 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +55 -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/components/tool-call-card/index.d.ts +7 -0
  35. package/dist/components/tool-call-card/logic.d.ts +87 -0
  36. package/dist/components/tool-call-card/strings.d.ts +94 -0
  37. package/dist/components/tool-call-card/styles.d.ts +5 -0
  38. package/dist/components/tool-call-card/tool-call-card.d.ts +49 -0
  39. package/dist/components/tool-call-card/tool-call-group.d.ts +32 -0
  40. package/dist/context-window.js +384 -0
  41. package/dist/context-window.js.map +1 -0
  42. package/dist/core/element.d.ts +41 -0
  43. package/dist/core/format.d.ts +14 -0
  44. package/dist/core/html.d.ts +10 -0
  45. package/dist/core/tokens.d.ts +14 -0
  46. package/dist/format-BBGWXPzk.js +51 -0
  47. package/dist/format-BBGWXPzk.js.map +1 -0
  48. package/dist/html-elD6Bb_u.js +175 -0
  49. package/dist/html-elD6Bb_u.js.map +1 -0
  50. package/dist/index.d.ts +16 -0
  51. package/dist/index.js +129 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/memory-panel.js +1255 -0
  54. package/dist/memory-panel.js.map +1 -0
  55. package/dist/model-picker.js +858 -0
  56. package/dist/model-picker.js.map +1 -0
  57. package/dist/tokens.css +4 -0
  58. package/dist/tool-call-card.js +1046 -0
  59. package/dist/tool-call-card.js.map +1 -0
  60. package/package.json +79 -4
@@ -0,0 +1,1255 @@
1
+ import { d as define, U as UiElement, e as esc, f as escAttr, p as prefersReducedMotion } from "./html-elD6Bb_u.js";
2
+ import { d as fmtInt } from "./format-BBGWXPzk.js";
3
+ const styles = `
4
+ :host { --mm-fold: 320ms; --mm-warn: var(--yellow); --mm-danger-bg: color-mix(in srgb, var(--red) 11%, transparent);
5
+ --mm-mark: color-mix(in srgb, var(--st-wait) 32%, transparent); --mm-well: var(--surface-raised); }
6
+ .mm { container: mm / inline-size; position: relative; border: 1px solid var(--border); border-radius: var(--radius); background: var(--surface); color: var(--text-secondary); font-size: 13px; }
7
+ .mm h3, .mm p { margin: 0; }
8
+ .mm ol { margin: 0; padding: 0; }
9
+ .mm :is(button, input, textarea):focus-visible { outline: 2px solid var(--border-focus); outline-offset: 1px; }
10
+ .mm-head { display: flex; align-items: center; gap: 8px; padding: 14px 16px 2px; }
11
+ .mm-title { font-size: 13.5px; font-weight: 600; color: var(--text); }
12
+ .mm-count { min-width: 20px; height: 20px; padding: 0 6px; border-radius: 5px; background: var(--mm-well); font-size: 12px; font-weight: 500; line-height: 20px; text-align: center; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
13
+ .mm-count:empty { display: none; }
14
+
15
+ /* filters: store chips (radiogroup) · search covering the chips */
16
+ .mm-filters { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 4px; padding: 10px 10px 10px 14px; border-bottom: 1px solid var(--border); }
17
+ .mm-chips, .mm-search { grid-area: 1 / 1; transition: opacity 220ms ease, filter 220ms ease; }
18
+ .mm-chips { display: flex; gap: 4px; min-width: 0; margin: -3px -4px; padding: 3px 4px; overflow-x: auto; scrollbar-width: none; -webkit-mask-image: linear-gradient(90deg, black calc(100% - 14px), transparent); mask-image: linear-gradient(90deg, black calc(100% - 14px), transparent); }
19
+ .mm-chips::-webkit-scrollbar { display: none; }
20
+ .mm-chip { display: inline-flex; flex: none; align-items: center; gap: 5px; height: 28px; padding: 0 9px; font-size: 12.5px; color: var(--text-secondary); background: none; border: 0; border-radius: 8px; white-space: nowrap; transition: background-color var(--dur-press) ease, color var(--dur-press) ease, box-shadow var(--dur-press) ease; }
21
+ .mm-chip[aria-checked="true"] { background: var(--mm-well); color: var(--text); font-weight: 600; box-shadow: inset 0 0 0 1px var(--border); }
22
+ .mm-chip-n { font-size: 11.5px; font-weight: 400; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
23
+ .mm-search { display: flex; align-items: center; gap: 6px; min-width: 0; height: 30px; padding: 0 2px 0 9px; border-radius: 8px; background: var(--mm-well); color: var(--text-tertiary); opacity: 0; visibility: hidden; }
24
+ .mm-search svg { flex: none; }
25
+ .mm-search input { flex: 1; min-width: 0; height: 28px; padding: 0; font-size: 13px; color: var(--text); background: none; border: 0; outline: none; -webkit-appearance: none; appearance: none; }
26
+ .mm-search input::placeholder { color: var(--text-tertiary); }
27
+ .mm-search input::-webkit-search-cancel-button { display: none; }
28
+ .mm .mm-search input:focus-visible { outline: none; }
29
+ .mm-search:focus-within { box-shadow: inset 0 0 0 1px var(--border-focus); }
30
+ .mm-filters[data-searching] .mm-chips { opacity: 0; filter: blur(2px); }
31
+ .mm-filters[data-searching] .mm-search { grid-area: 1 / 1 / 2 / 3; opacity: 1; visibility: visible; }
32
+ .mm-filters[data-searching] .mm-search-open { opacity: 0; visibility: hidden; }
33
+ .mm-icon { display: grid; flex: none; place-items: center; width: 28px; height: 28px; padding: 0; font-size: 16px; line-height: 1; color: var(--text-tertiary); background: none; border: 0; border-radius: 8px; transition: background-color var(--dur-press) ease, color var(--dur-press) ease, opacity 200ms ease; }
34
+ .mm-search-open { grid-area: 1 / 2; }
35
+ .mm-note { padding: 9px 16px; border-bottom: 1px solid var(--border); background: color-mix(in srgb, var(--text) 3%, transparent); font-size: 12.5px; color: var(--text-secondary); }
36
+
37
+ /* groups — one per store; folds are grid rows 0fr ↔ 1fr */
38
+ .mm-list { padding: 2px 8px 6px; }
39
+ .mm-fold { display: grid; grid-template-rows: 0fr; opacity: 0; transition: grid-template-rows var(--mm-fold) var(--ease-out), opacity var(--dur-fade) ease; }
40
+ .mm-fold[data-open] { grid-template-rows: 1fr; opacity: 1; transition: grid-template-rows var(--mm-fold) var(--ease-out), opacity 240ms ease 60ms; }
41
+ .mm-fold[data-leaving] { transition: grid-template-rows var(--mm-fold) var(--ease-out), opacity 200ms ease, filter 200ms ease; filter: blur(2px); }
42
+ .mm-clip { min-height: 0; overflow: hidden; }
43
+ .mm-ghead { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 4px 16px; padding: 12px 8px 4px; }
44
+ .mm-ghead h3 { min-width: 0; font-size: 13px; font-weight: 600; color: var(--text); overflow-wrap: anywhere; }
45
+ .mm-file { margin-left: 4px; font-size: 12px; font-weight: 400; color: var(--text-tertiary); }
46
+ .mm-usage { display: flex; flex: 0 1 280px; align-items: center; gap: 8px; min-width: 0; font-size: 12px; color: var(--text-secondary); font-variant-numeric: tabular-nums; }
47
+ .mm-bar { position: relative; flex: 1; min-width: 48px; height: 6px; border-radius: 3px; background: var(--ib-pill); overflow: hidden; }
48
+ .mm-bar > i { position: absolute; inset: 0 auto 0 0; border-radius: inherit; background: var(--st-done); }
49
+ .mm-usage[data-level="warn"] .mm-bar > i { background: var(--st-wait); }
50
+ .mm-usage[data-level="high"] .mm-bar > i, .mm-usage[data-level="full"] .mm-bar > i { background: var(--st-fail); }
51
+ .mm-usage[data-level="warn"], .mm-usage[data-level="high"], .mm-over { color: var(--mm-warn); }
52
+ .mm-usage[data-level="full"] { color: var(--red); }
53
+ .mm-uval { flex: none; white-space: nowrap; }
54
+ .mm-items { list-style: none; }
55
+ .mm-row { padding: 6px 8px 5px; border-radius: 8px; transition: background-color var(--dur-press) ease; }
56
+ .mm-row:hover, .mm-row:focus-within, .mm-row[data-editing] { background: var(--mm-well); }
57
+ .mm-text { font-size: 13px; line-height: 20px; color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere; }
58
+ .mm-text mark { border-radius: 3px; background: var(--mm-mark); box-shadow: 0 0 0 1px var(--mm-mark); color: inherit; }
59
+ .mm-meta { display: flex; align-items: center; gap: 6px; min-height: 26px; font-size: 12px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
60
+ .mm-src { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
61
+ .mm-acts { display: inline-flex; flex: none; align-items: center; gap: 2px; margin: -1px -4px -1px auto; opacity: 0; transition: opacity 180ms ease; }
62
+ .mm-row:hover .mm-acts, .mm-row:focus-within .mm-acts, .mm-acts.is-on { opacity: 1; }
63
+ .mm-act { height: 26px; padding: 0 9px; font-size: 12.5px; font-weight: 500; color: var(--text-secondary); background: none; border: 0; border-radius: 8px; white-space: nowrap; transition: background-color var(--dur-press) ease, color var(--dur-press) ease, opacity var(--dur-press) ease; }
64
+ .mm-act-main { background: var(--text); color: var(--surface); }
65
+ .mm-act:disabled { opacity: 0.4; cursor: default; }
66
+ .mm textarea.mm-edit { display: block; width: 100%; min-height: 20px; margin: 0; padding: 0; font-size: 13px; line-height: 20px; color: var(--text); background: none; border: 0; border-radius: 0; resize: none; overflow: hidden; }
67
+ .mm textarea.mm-edit:focus-visible { outline: none; }
68
+ .mm-row[data-editing]:focus-within, .mm-new:focus-within { box-shadow: inset 0 0 0 1px var(--border-focus); }
69
+ .mm-counter { font-size: 11.5px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; }
70
+ .mm-counter.is-over { color: var(--red); }
71
+ .mm-empty { padding: 4px 8px 8px; font-size: 12.5px; color: var(--text-tertiary); }
72
+ .mm-nomatch { padding: 18px 8px 12px; text-align: center; overflow-wrap: anywhere; }
73
+ /* 3px of room so a focus ring is not clipped by the folding parent */
74
+ .mm-addline { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 3px 3px 8px; }
75
+ .mm-add { display: flex; align-items: center; gap: 6px; min-width: 0; height: 32px; padding: 0 8px; font-size: 13px; color: var(--text-tertiary); background: none; border: 0; border-radius: 8px; text-align: left; transition: background-color var(--dur-press) ease, color var(--dur-press) ease; }
76
+ .mm-clear { flex: none; height: 28px; padding: 0 8px; font-size: 12px; color: var(--text-tertiary); background: none; border: 0; border-radius: 8px; white-space: nowrap; transition: background-color var(--dur-press) ease, color var(--dur-press) ease, box-shadow var(--dur-press) ease; }
77
+ .mm-clear[data-armed] { background: var(--mm-danger-bg); color: var(--red); font-weight: 600; box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--red) 40%, transparent); }
78
+ .mm-new { margin: 3px 3px 8px; padding: 8px 8px 6px; border-radius: 8px; background: var(--mm-well); }
79
+ .mm-label { display: block; margin-bottom: 4px; font-size: 12px; color: var(--text-tertiary); }
80
+ .mm-line { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px; margin-top: 6px; }
81
+
82
+ /* tray: caption (cross-fades) · polite status · Undo */
83
+ .mm-foot { display: flex; align-items: center; gap: 8px; min-height: 40px; padding: 4px 8px 4px 16px; border-top: 1px solid var(--border); border-radius: 0 0 calc(var(--radius) - 1px) calc(var(--radius) - 1px); background: var(--bg); }
84
+ .mm-caption { display: grid; flex: 1; min-width: 0; font-size: 12.5px; color: var(--text-tertiary); white-space: nowrap; font-variant-numeric: tabular-nums; }
85
+ .mm-caption b { font-weight: 600; color: var(--text); }
86
+ .mm-swap { grid-area: 1 / 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; animation: mm-swap-in 320ms ease both; }
87
+ .mm-swap[data-out] { animation: mm-swap-out 220ms ease both; }
88
+ .mm-swap.is-warn { color: var(--mm-warn); }
89
+ .mm-swap.is-fail { color: var(--red); }
90
+ .mm-forgot { display: flex; min-width: 0; }
91
+ .mm-flat { flex: none; white-space: pre; }
92
+ .mm-quote { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
93
+ .mm-undo { display: inline-flex; flex: none; align-items: center; height: 28px; padding: 0 10px; font-size: 12.5px; font-weight: 600; color: var(--text); background: var(--surface); border: 1px solid var(--border); border-radius: 8px; animation: mm-swap-in 300ms ease both; transition: border-color var(--dur-press) ease; }
94
+ .mm-undo[hidden] { display: none; }
95
+ @keyframes mm-swap-in { from { opacity: 0; filter: blur(2px); } }
96
+ @keyframes mm-swap-out { to { opacity: 0; filter: blur(2px); } }
97
+ @media (hover: hover) and (pointer: fine) {
98
+ .mm-chip:hover, .mm-icon:hover, .mm-add:hover { background: var(--mm-well); color: var(--text); }
99
+ .mm-act:hover { background: var(--surface); color: var(--text); }
100
+ .mm-act-main:hover { background: var(--text); color: var(--surface); opacity: 0.86; }
101
+ .mm-act-del:hover, .mm-clear:hover { background: var(--mm-danger-bg); color: var(--red); }
102
+ .mm-undo:hover { border-color: var(--accent); }
103
+ }
104
+ @container mm (max-width: 480px) { .mm-cap { display: none; } .mm-usage { flex-basis: 100%; } }
105
+ @container mm (max-width: 340px) { .mm-chip-n { display: none; } .mm-chip { padding: 0 7px; } }
106
+ @media (pointer: coarse) {
107
+ .mm-acts { opacity: 1; }
108
+ .mm-act, .mm-chip, .mm-clear, .mm-add, .mm-undo { min-height: 44px; }
109
+ .mm-icon { width: 44px; height: 44px; }
110
+ .mm-search { height: 44px; }
111
+ .mm-search input { height: 42px; font-size: 16px; }
112
+ .mm textarea.mm-edit { font-size: 16px; line-height: 22px; }
113
+ .mm-meta { min-height: 44px; }
114
+ }
115
+ `;
116
+ const UNDO_MS = 5e3;
117
+ const FOLD_MS = 320;
118
+ const NOTICE_MS = 3e3;
119
+ const ARM_MS = 4e3;
120
+ const ROLL_MS = 520;
121
+ const SWAP_OUT_MS = 260;
122
+ const REVEAL_SCROLL_MS = 120;
123
+ const SHORT = 40;
124
+ const STRINGS = {
125
+ en: {
126
+ panel: "Personal memory",
127
+ stores: "Stores",
128
+ all: "All",
129
+ search: "Search memories",
130
+ searchPh: "Search entries",
131
+ closeSearch: "Close search",
132
+ usage: (l) => `${l} usage`,
133
+ of: (u, l) => `${u} / ${l} chars`,
134
+ chars: (n) => `${n} chars`,
135
+ full: " · full",
136
+ empty: "Empty — add an entry below.",
137
+ emptyRo: "Empty.",
138
+ noMatch: (q) => `No entries match “${q}”`,
139
+ over: "over the per-entry limit",
140
+ edit: "Edit",
141
+ del: "Delete",
142
+ editName: (s) => `Edit “${s}”`,
143
+ delName: (s) => `Delete “${s}”`,
144
+ editField: (l, n) => `Edit ${l} #${n}`,
145
+ cancel: "Cancel",
146
+ save: "Save",
147
+ add: "Add",
148
+ addTo: (l) => `Add to ${l}`,
149
+ clear: "Clear all",
150
+ clearN: (n) => `Clear ${n} ${n === 1 ? "entry" : "entries"}`,
151
+ armed: "This cannot be undone — press again to clear",
152
+ armedSr: (l, n) => `Clear all ${n} ${n === 1 ? "entry" : "entries"} in ${l}? This cannot be undone — press again to clear`,
153
+ newLabel: (cap) => cap ? `New entry (up to ${cap} chars)` : "New entry",
154
+ after: (total, limit) => limit ? `After adding: ${total} / ${limit} chars` : `After adding: ${total} chars`,
155
+ overBy: (n) => ` — ${n} over`,
156
+ tooLong: " — over the per-entry limit",
157
+ deleted: ["Deleted “", "”"],
158
+ deletedSr: (s) => `Deleted “${s}” — Undo brings it back`,
159
+ undo: "Undo",
160
+ saved: "Saved",
161
+ added: "Added",
162
+ restored: "Restored",
163
+ cleared: "Cleared",
164
+ emptyEntry: "An entry cannot be empty — use Delete to remove it",
165
+ editHint: "Enter to save · Shift+Enter for a new line · Esc to cancel",
166
+ addHint: "Enter to add · Shift+Enter for a new line · Esc to close",
167
+ entries: (n) => n === 1 ? " entry" : " entries"
168
+ },
169
+ ko: {
170
+ panel: "개인 메모리",
171
+ stores: "저장소",
172
+ all: "전체",
173
+ search: "개인 메모리 검색",
174
+ searchPh: "항목 검색",
175
+ closeSearch: "검색 닫기",
176
+ usage: (l) => `${l} 사용량`,
177
+ of: (u, l) => `${u} / ${l}자`,
178
+ chars: (n) => `${n}자`,
179
+ full: " · 가득 참",
180
+ empty: "비어 있음 — 아래에서 추가하세요.",
181
+ emptyRo: "비어 있음.",
182
+ noMatch: (q) => `“${q}” 검색 결과가 없습니다`,
183
+ over: "한 항목 한도 초과",
184
+ edit: "수정",
185
+ del: "삭제",
186
+ editName: (s) => `수정: “${s}”`,
187
+ delName: (s) => `삭제: “${s}”`,
188
+ editField: (l, n) => `${l} ${n}번 수정`,
189
+ cancel: "취소",
190
+ save: "저장",
191
+ add: "추가",
192
+ addTo: (l) => `${l}에 추가`,
193
+ clear: "모두 지우기",
194
+ clearN: (n) => `${n}개 모두 지우기`,
195
+ armed: "되돌릴 수 없습니다 — 한 번 더 누르면 지웁니다",
196
+ armedSr: (l, n) => `${l} 항목 ${n}개를 모두 지울까요? 되돌릴 수 없습니다 — 한 번 더 누르면 지웁니다`,
197
+ newLabel: (cap) => cap ? `새 항목 (한 항목 최대 ${cap}자)` : "새 항목",
198
+ after: (total, limit) => limit ? `추가하면 ${total} / ${limit}자` : `추가하면 ${total}자`,
199
+ overBy: (n) => ` — ${n}자 초과`,
200
+ tooLong: " — 한 항목 한도 초과",
201
+ deleted: ["삭제함 “", "”"],
202
+ deletedSr: (s) => `삭제함 “${s}” — 되돌리기로 되돌릴 수 있습니다`,
203
+ undo: "되돌리기",
204
+ saved: "저장했습니다",
205
+ added: "추가했습니다",
206
+ restored: "되돌렸습니다",
207
+ cleared: "모두 지웠습니다",
208
+ emptyEntry: "비어 있는 항목은 저장할 수 없습니다 — 지우려면 삭제를 쓰세요",
209
+ editHint: "Enter 저장 · Shift+Enter 줄바꿈 · Esc 취소",
210
+ addHint: "Enter 추가 · Shift+Enter 줄바꿈 · Esc 닫기",
211
+ entries: (_n) => "개 항목"
212
+ }
213
+ };
214
+ const SEARCH_SVG = '<svg width="14" height="14" viewBox="0 0 14 14" fill="none" aria-hidden="true"><circle cx="6" cy="6" r="4.25" stroke="currentColor" stroke-width="1.5"/><path d="M9.25 9.25 12 12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>';
215
+ const num = (v) => {
216
+ const n = Number(v);
217
+ return Number.isFinite(n) && n > 0 ? n : 0;
218
+ };
219
+ function storeUsed(entries, separator = "") {
220
+ if (!entries.length) return 0;
221
+ let n = separator.length * (entries.length - 1);
222
+ for (const e of entries) n += String(e).length;
223
+ return n;
224
+ }
225
+ function usageLevel(used, limit) {
226
+ if (!(limit > 0)) return "ok";
227
+ const r = used / limit;
228
+ return r >= 1 ? "full" : r >= 0.95 ? "high" : r >= 0.8 ? "warn" : "ok";
229
+ }
230
+ function usageAfterAdd(entries, separator, text) {
231
+ const n = String(text ?? "").trim().length;
232
+ return storeUsed(entries, separator) + (n ? n + (entries.length ? separator.length : 0) : 0);
233
+ }
234
+ function isSameEdit(next, previous) {
235
+ return String(next ?? "").trim() === String(previous ?? "").trim();
236
+ }
237
+ function shortText(text, max = SHORT) {
238
+ const s = String(text ?? "");
239
+ return s.length > max ? `${s.slice(0, max)}…` : s;
240
+ }
241
+ function entryMatches(text, q, store) {
242
+ const needle = String(q ?? "").trim().toLowerCase();
243
+ if (!needle) return true;
244
+ if (String(text ?? "").toLowerCase().includes(needle))
245
+ return true;
246
+ return !!store && `${store.label ?? ""} ${store.file ?? ""}`.toLowerCase().includes(needle);
247
+ }
248
+ function highlight(text, q) {
249
+ const s = String(text ?? "");
250
+ const needle = String(q ?? "").trim().toLowerCase();
251
+ const lower = s.toLowerCase();
252
+ if (!needle || lower.length !== s.length) return esc(s);
253
+ let out = "";
254
+ let at = 0;
255
+ for (let i = lower.indexOf(needle); i >= 0; i = lower.indexOf(needle, at)) {
256
+ out += `${esc(s.slice(at, i))}<mark>${esc(s.slice(i, i + needle.length))}</mark>`;
257
+ at = i + needle.length;
258
+ }
259
+ return out + esc(s.slice(at));
260
+ }
261
+ function hash(s) {
262
+ let h = 2166136261;
263
+ for (let i = 0; i < s.length; i++) {
264
+ h ^= s.charCodeAt(i);
265
+ h = Math.imul(h, 16777619);
266
+ }
267
+ return (h >>> 0).toString(36);
268
+ }
269
+ function entryKeys(entries) {
270
+ const seen = /* @__PURE__ */ new Map();
271
+ return entries.map((e) => {
272
+ const h = hash(String(e));
273
+ const n = seen.get(h) ?? 0;
274
+ seen.set(h, n + 1);
275
+ return n ? `${h}.${n}` : h;
276
+ });
277
+ }
278
+ function fold(el, open) {
279
+ if (!el) return;
280
+ el.toggleAttribute("data-open", open);
281
+ el.toggleAttribute("inert", !open);
282
+ }
283
+ function grow(el) {
284
+ if (!el) return;
285
+ el.style.height = "auto";
286
+ const h = el.scrollHeight;
287
+ if (h > 0) el.style.height = `${h}px`;
288
+ }
289
+ class UiMemoryPanel extends UiElement {
290
+ static styles = [styles];
291
+ static get observedAttributes() {
292
+ return ["lang"];
293
+ }
294
+ #work = [];
295
+ /** Entry texts as the DOM shows them (a delete's fold can outlive the working copy by FOLD_MS). */
296
+ #painted = /* @__PURE__ */ new Map();
297
+ #view = { store: null, q: "", searching: false };
298
+ #editing = null;
299
+ #adding = {};
300
+ #drafts = {};
301
+ #undo = null;
302
+ #notice = null;
303
+ #armed = null;
304
+ #holds = { hover: false, panel: false };
305
+ #seq = 0;
306
+ #chrome = "";
307
+ #trayKey = "";
308
+ #trayItem = null;
309
+ #rolls = /* @__PURE__ */ new Map();
310
+ #timers = { notice: 0, armed: 0, settle: 0, scroll: 0 };
311
+ #swaps = /* @__PURE__ */ new Set();
312
+ constructor() {
313
+ super();
314
+ const r = this.root;
315
+ r.addEventListener("click", (e) => this.#onClick(e));
316
+ r.addEventListener("keydown", (e) => this.#onKey(e));
317
+ r.addEventListener("input", (e) => this.#onInput(e));
318
+ r.addEventListener("focusin", () => this.#hold("panel", true));
319
+ r.addEventListener("focusout", (e) => {
320
+ const to = e.relatedTarget;
321
+ this.#hold("panel", !!to && this.content.contains(to));
322
+ });
323
+ }
324
+ get data() {
325
+ return super.data;
326
+ }
327
+ /** Setting data ends every optimistic change: the panel shows exactly the stores you pass. */
328
+ set data(value) {
329
+ this.#adopt(value);
330
+ super.data = value;
331
+ }
332
+ attributeChangedCallback() {
333
+ this.#chrome = "";
334
+ this.requestRender();
335
+ }
336
+ connectedCallback() {
337
+ super.connectedCallback();
338
+ if (this.#undo && !this.#undo.timer) this.#armUndo();
339
+ }
340
+ disconnectedCallback() {
341
+ for (const k of ["notice", "armed", "settle", "scroll"]) {
342
+ clearTimeout(this.#timers[k]);
343
+ this.#timers[k] = 0;
344
+ }
345
+ const u = this.#undo;
346
+ if (u?.timer) {
347
+ clearTimeout(u.timer);
348
+ u.timer = 0;
349
+ u.left = Math.max(0, u.left - (Date.now() - u.started));
350
+ }
351
+ for (const id of this.#swaps) clearTimeout(id);
352
+ this.#swaps.clear();
353
+ for (const st of this.#rolls.values()) cancelAnimationFrame(st.raf);
354
+ this.#rolls.clear();
355
+ this.#notice = null;
356
+ this.#armed = null;
357
+ this.#holds = { hover: false, panel: false };
358
+ }
359
+ get #t() {
360
+ return (this.getAttribute("lang") ?? "").toLowerCase().startsWith("ko") ? STRINGS.ko : STRINGS.en;
361
+ }
362
+ get #ro() {
363
+ const r = this.data?.readOnly;
364
+ return r === true || typeof r === "string";
365
+ }
366
+ #store(id) {
367
+ return id == null ? void 0 : this.#work.find((s) => s.id === id);
368
+ }
369
+ // ── data ──
370
+ #adopt(d) {
371
+ const list = Array.isArray(d?.stores) ? d.stores : [];
372
+ this.#work = list.filter((s) => s && s.id != null).map((s) => ({
373
+ id: String(s.id),
374
+ label: String(s.label ?? s.id),
375
+ file: s.file ? String(s.file) : "",
376
+ limit: num(s.limit),
377
+ per: num(s.perEntryLimit),
378
+ sep: typeof s.separator === "string" ? s.separator : "",
379
+ entries: (Array.isArray(s.entries) ? s.entries : []).map((e2) => String(e2 ?? ""))
380
+ }));
381
+ const ro = d?.readOnly === true || typeof d?.readOnly === "string";
382
+ const e = this.#editing;
383
+ if (e) {
384
+ const s = this.#store(e.store);
385
+ const at = s ? s.entries.indexOf(e.orig) : -1;
386
+ if (!ro && s && at >= 0 && s.entries.lastIndexOf(e.orig) === at) e.index = at;
387
+ else {
388
+ this.#editing = null;
389
+ if (s && !ro && e.draft !== e.orig) this.#keepDraft(s.id, e.draft);
390
+ }
391
+ }
392
+ if (ro) {
393
+ this.#adding = {};
394
+ this.#armed = null;
395
+ this.#dropUndo();
396
+ }
397
+ if (this.#view.store && !this.#store(this.#view.store)) this.#view = { ...this.#view, store: null };
398
+ if (this.#undo && !this.#store(this.#undo.store)) this.#dropUndo();
399
+ const a = this.#armed;
400
+ if (a && this.#store(a.store)?.entries.length !== a.n) this.#armed = null;
401
+ }
402
+ // ── rendering ──
403
+ /** The static chrome: chips, search, tray. The groups are painted into `.mm-list`. */
404
+ render() {
405
+ const d = this.data;
406
+ const t = this.#t;
407
+ const title = d?.title ? String(d.title) : "";
408
+ const reason = typeof d?.readOnly === "string" ? d.readOnly : "";
409
+ let chips = `<button type="button" class="mm-chip" role="radio" data-act="chip" data-all data-key="chip-all" aria-checked="false" tabindex="-1">${esc(t.all)} <span class="mm-chip-n"></span></button>`;
410
+ for (const s of this.#work)
411
+ chips += `<button type="button" class="mm-chip" role="radio" data-act="chip" data-pick="${escAttr(s.id)}" data-key="${escAttr(`chip|${s.id}`)}" aria-checked="false" tabindex="-1">${esc(s.label)} <span class="mm-chip-n"></span></button>`;
412
+ return `<section class="mm" role="group" ${title ? 'aria-labelledby="mm-title"' : `aria-label="${escAttr(t.panel)}"`}>
413
+ ${title ? `<div class="mm-head"><span class="mm-title" id="mm-title">${esc(title)}</span><span class="mm-count" aria-hidden="true"></span></div>` : ""}<div class="mm-filters">
414
+ <div class="mm-chips" role="radiogroup" aria-label="${escAttr(t.stores)}">${chips}</div>
415
+ <div class="mm-search" id="mm-search" inert>${SEARCH_SVG}<input type="search" class="mm-q" data-key="q" placeholder="${escAttr(t.searchPh)}" aria-label="${escAttr(t.search)}" autocomplete="off" spellcheck="false"><button type="button" class="mm-icon" data-act="search-close" data-key="search-close" aria-label="${escAttr(t.closeSearch)}"><span aria-hidden="true">×</span></button></div>
416
+ <button type="button" class="mm-icon mm-search-open" data-act="search-open" data-key="search-open" aria-label="${escAttr(t.search)}" aria-expanded="false" aria-controls="mm-search">${SEARCH_SVG}</button>
417
+ </div>${reason ? `<p class="mm-note">${esc(reason)}</p>` : ""}
418
+ <div class="mm-list"></div>
419
+ <div class="mm-foot"><span class="mm-caption" aria-hidden="true"></span><span class="sr-only mm-live" role="status" aria-live="polite" aria-atomic="true">${esc(this.#trayState().live)}</span><button type="button" class="mm-undo" data-act="undo" data-key="undo" hidden>${esc(t.undo)}</button></div>
420
+ </section>`;
421
+ }
422
+ /** Renders are deferred while a deleted row folds away, so the host's answer never cuts the fold short. */
423
+ update() {
424
+ if (this.#timers.settle) return;
425
+ this.#paintAll();
426
+ }
427
+ #sig() {
428
+ const d = this.data;
429
+ return JSON.stringify([
430
+ this.#t.all,
431
+ d?.title ?? "",
432
+ typeof d?.readOnly === "string" ? d.readOnly : "",
433
+ this.#work.map((s) => [s.id, s.label])
434
+ ]);
435
+ }
436
+ #paintAll() {
437
+ if (this.#timers.settle) {
438
+ clearTimeout(this.#timers.settle);
439
+ this.#timers.settle = 0;
440
+ }
441
+ if (!this.data || !Array.isArray(this.data.stores)) {
442
+ this.content.innerHTML = "";
443
+ this.#chrome = "";
444
+ this.#trayItem = null;
445
+ return;
446
+ }
447
+ this.#keepFocus(() => {
448
+ const sig = this.#sig();
449
+ if (sig !== this.#chrome) this.#buildChrome(sig);
450
+ this.#paintList();
451
+ });
452
+ this.#applyView();
453
+ }
454
+ #buildChrome(sig) {
455
+ for (const st of this.#rolls.values()) cancelAnimationFrame(st.raf);
456
+ this.#rolls.clear();
457
+ this.#trayKey = "";
458
+ this.#trayItem = null;
459
+ this.#holds.hover = false;
460
+ this.content.innerHTML = this.render();
461
+ this.#chrome = sig;
462
+ const q = this.$(".mm-q");
463
+ if (q) q.value = this.#view.q;
464
+ const foot = this.$(".mm-foot");
465
+ foot?.addEventListener("pointerenter", (e) => {
466
+ if (e.pointerType !== "touch") this.#hold("hover", true);
467
+ });
468
+ foot?.addEventListener("pointerleave", () => this.#hold("hover", false));
469
+ }
470
+ #paintList() {
471
+ const list = this.$(".mm-list");
472
+ if (!list) return;
473
+ const t = this.#t;
474
+ const ro = this.#ro;
475
+ let html = "";
476
+ this.#work.forEach((s, si) => {
477
+ html += this.#groupHtml(s, si, t, ro);
478
+ });
479
+ html += '<div class="mm-fold mm-nomatch-f" inert><div class="mm-clip"><p class="mm-empty mm-nomatch"></p></div></div>';
480
+ list.innerHTML = html;
481
+ this.#painted = new Map(this.#work.map((s) => [s.id, s.entries.slice()]));
482
+ for (const s of this.#work) {
483
+ const box = this.#byKey(`new|${s.id}`);
484
+ if (box) {
485
+ box.value = this.#drafts[s.id] ?? "";
486
+ this.#paintCount(s);
487
+ if (this.#adding[s.id]) grow(box);
488
+ }
489
+ }
490
+ const e = this.#editing;
491
+ if (e) {
492
+ const ta = this.#byKey(`edit|${e.store}`);
493
+ if (ta) {
494
+ ta.value = e.draft;
495
+ grow(ta);
496
+ }
497
+ }
498
+ }
499
+ #groupHtml(s, si, t, ro) {
500
+ const v = this.#view;
501
+ const q = v.q.trim();
502
+ const keys = entryKeys(s.entries);
503
+ const used = storeUsed(s.entries, s.sep);
504
+ const level = usageLevel(used, s.limit);
505
+ let rows = "";
506
+ let shown = 0;
507
+ s.entries.forEach((text, i) => {
508
+ const show = this.#rowShown(s, i, text);
509
+ if (show) shown++;
510
+ const editing = !ro && this.#editing?.store === s.id && this.#editing.index === i;
511
+ rows += `<li><div class="mm-fold mm-rowf" data-row="${keys[i]}" data-i="${i}"${show ? " data-open" : " inert"}><div class="mm-clip">${editing ? this.#editorHtml(s, i, t) : this.#rowHtml(s, i, text, keys[i], t, ro)}</div></div></li>`;
512
+ });
513
+ const open = (!v.store || v.store === s.id) && (shown > 0 || !q);
514
+ const val = s.limit > 0 ? t.of(fmtInt(used), fmtInt(s.limit)) + (level === "full" ? t.full : "") : t.chars(fmtInt(used));
515
+ const bar = s.limit > 0 ? `<span class="mm-bar" role="meter" aria-valuemin="0" aria-valuemax="${s.limit}" aria-valuenow="${Math.min(used, s.limit)}" aria-valuetext="${escAttr(val)}" aria-label="${escAttr(t.usage(s.label))}"><i style="width:${Math.min(100, used / s.limit * 100).toFixed(2)}%"></i></span>` : "";
516
+ let html = `<section class="mm-group mm-fold" data-store="${escAttr(s.id)}"${open ? " data-open" : " inert"} aria-labelledby="mm-h-${si}"><div class="mm-clip">
517
+ <div class="mm-ghead"><h3 id="mm-h-${si}">${esc(s.label)}${s.file ? ` <span class="mm-file">${esc(s.file)}</span>` : ""}</h3><div class="mm-usage" data-level="${level}">${bar}<span class="mm-uval"${bar ? ' aria-hidden="true"' : ""}>${esc(val)}</span></div></div>`;
518
+ if (!s.entries.length) html += `<p class="mm-empty">${esc(ro ? t.emptyRo : t.empty)}</p>`;
519
+ html += `<ol class="mm-items">${rows}</ol>`;
520
+ if (!ro) {
521
+ const adding = !!this.#adding[s.id];
522
+ const armed = this.#armed?.store === s.id;
523
+ html += `<div class="mm-fold mm-addf" data-addline${adding ? " inert" : " data-open"}><div class="mm-clip"><div class="mm-addline"><button type="button" class="mm-add" data-act="add-open" data-key="${escAttr(`add|${s.id}`)}" aria-expanded="${adding}" aria-controls="mm-new-${si}"><span aria-hidden="true">+</span> ${esc(t.addTo(s.label))}</button>${s.entries.length ? `<button type="button" class="mm-clear" data-act="clear" data-key="${escAttr(`clear|${s.id}`)}"${armed ? " data-armed" : ""}>${esc(armed ? t.clearN(s.entries.length) : t.clear)}</button>` : ""}</div></div></div>
524
+ <div class="mm-fold mm-newf" id="mm-new-${si}" data-newbox${adding ? " data-open" : " inert"}><div class="mm-clip"><div class="mm-new"><label class="mm-label" for="mm-ta-${si}">${esc(
525
+ t.newLabel(s.per > 0 ? fmtInt(s.per) : "")
526
+ )}</label><textarea id="mm-ta-${si}" class="mm-edit" data-key="${escAttr(`new|${s.id}`)}" rows="2"></textarea><div class="mm-line"><span class="mm-counter" data-count></span><span class="mm-acts is-on"><button type="button" class="mm-act" data-act="add-close" data-key="${escAttr(`addc|${s.id}`)}">${esc(t.cancel)}</button><button type="button" class="mm-act mm-act-main" data-act="add-save" data-key="${escAttr(`adds|${s.id}`)}">${esc(t.add)}</button></span></div></div></div></div>`;
527
+ }
528
+ return `${html}</div></section>`;
529
+ }
530
+ #rowHtml(s, i, text, key, t, ro) {
531
+ const len = text.length;
532
+ const short = shortText(text);
533
+ const acts = ro ? "" : `<span class="mm-acts"><button type="button" class="mm-act" data-act="edit" data-key="${escAttr(`ed|${s.id}|${key}`)}" aria-label="${escAttr(t.editName(short))}">${esc(t.edit)}</button><button type="button" class="mm-act mm-act-del" data-act="delete" data-key="${escAttr(`del|${s.id}|${key}`)}" aria-label="${escAttr(t.delName(short))}">${esc(t.del)}</button></span>`;
534
+ return `<div class="mm-row"><p class="mm-text">${esc(text)}</p><div class="mm-meta"><span class="mm-src">#${i + 1} · ${esc(t.chars(fmtInt(len)))}${s.per > 0 && len > s.per ? ` · <span class="mm-over">${esc(t.over)}</span>` : ""}</span>${acts}</div></div>`;
535
+ }
536
+ #editorHtml(s, i, t) {
537
+ const c = this.#editCount(s, this.#editing?.draft ?? "");
538
+ return `<div class="mm-row" data-editing><textarea class="mm-edit" data-key="${escAttr(`edit|${s.id}`)}" rows="1" aria-label="${escAttr(t.editField(s.label, i + 1))}"></textarea><div class="mm-meta"><span class="mm-counter${c.over ? " is-over" : ""}" data-ecount>${esc(c.text)}</span><span class="mm-acts is-on"><button type="button" class="mm-act" data-act="cancel" data-key="${escAttr(`cancel|${s.id}`)}">${esc(t.cancel)}</button><button type="button" class="mm-act mm-act-main" data-act="save" data-key="${escAttr(`save|${s.id}`)}">${esc(t.save)}</button></span></div></div>`;
539
+ }
540
+ #editCount(s, draft) {
541
+ const n = draft.trim().length;
542
+ return {
543
+ text: s.per > 0 ? `${fmtInt(n)} / ${this.#t.chars(fmtInt(s.per))}` : this.#t.chars(fmtInt(n)),
544
+ over: s.per > 0 && n > s.per
545
+ };
546
+ }
547
+ #paintCount(s) {
548
+ const el = this.#group(s.id)?.querySelector("[data-count]");
549
+ if (!el) return;
550
+ const t = this.#t;
551
+ const draft = this.#drafts[s.id] ?? "";
552
+ const total = usageAfterAdd(s.entries, s.sep, draft);
553
+ const over = s.limit > 0 && total > s.limit ? total - s.limit : 0;
554
+ const long = s.per > 0 && draft.trim().length > s.per;
555
+ el.textContent = t.after(fmtInt(total), s.limit > 0 ? fmtInt(s.limit) : "") + (over ? t.overBy(fmtInt(over)) : "") + (long ? t.tooLong : "");
556
+ el.classList.toggle("is-over", !!over || long);
557
+ }
558
+ #rowShown(s, i, text) {
559
+ const v = this.#view;
560
+ if (v.store && v.store !== s.id) return false;
561
+ if (this.#editing?.store === s.id && this.#editing.index === i) return true;
562
+ return entryMatches(text, v.q, s);
563
+ }
564
+ /** Fold rows and groups to the chip + search without repainting (what is typed stays put). */
565
+ #applyView() {
566
+ const v = this.#view;
567
+ const q = v.q.trim();
568
+ let any = false;
569
+ for (const g of this.$$(".mm-group")) {
570
+ const s = this.#store(g.dataset.store);
571
+ if (!s) continue;
572
+ const texts = this.#painted.get(s.id) ?? [];
573
+ let shown = 0;
574
+ for (const f of Array.from(g.querySelectorAll(".mm-rowf"))) {
575
+ if (f.hasAttribute("data-leaving")) continue;
576
+ const i = Number(f.dataset.i);
577
+ const text = texts[i] ?? "";
578
+ const show = this.#rowShown(s, i, text);
579
+ if (show) shown++;
580
+ fold(f, show);
581
+ const p = f.querySelector(".mm-text");
582
+ const hl = show ? q : "";
583
+ if (p && (f.dataset.hl ?? "") !== hl) {
584
+ p.innerHTML = hl ? highlight(text, hl) : esc(text);
585
+ f.dataset.hl = hl;
586
+ }
587
+ }
588
+ if (shown) any = true;
589
+ fold(g, (!v.store || v.store === s.id) && (shown > 0 || !q));
590
+ }
591
+ fold(this.$(".mm-nomatch-f"), !!q && !any);
592
+ const nm = this.$(".mm-nomatch");
593
+ if (nm && q) nm.textContent = this.#t.noMatch(q);
594
+ this.#sync();
595
+ }
596
+ /** Chrome after any change: chip state (one tab stop) and counts, search state, head count, tray. */
597
+ #sync() {
598
+ if (!this.$(".mm")) return;
599
+ const v = this.#view;
600
+ const total = this.#work.reduce((n, s) => n + s.entries.length, 0);
601
+ for (const chip of this.$$(".mm-chip")) {
602
+ const id = chip.hasAttribute("data-all") ? null : chip.dataset.pick ?? null;
603
+ const on = id === v.store;
604
+ chip.setAttribute("aria-checked", on ? "true" : "false");
605
+ chip.tabIndex = on ? 0 : -1;
606
+ const n = chip.querySelector(".mm-chip-n");
607
+ if (n) n.textContent = fmtInt(id === null ? total : this.#store(id)?.entries.length ?? 0);
608
+ }
609
+ this.$(".mm-filters")?.toggleAttribute("data-searching", v.searching);
610
+ this.$(".mm-chips")?.toggleAttribute("inert", v.searching);
611
+ this.$(".mm-search")?.toggleAttribute("inert", !v.searching);
612
+ const opener = this.$(".mm-search-open");
613
+ if (opener) {
614
+ opener.setAttribute("aria-expanded", v.searching ? "true" : "false");
615
+ opener.toggleAttribute("inert", v.searching);
616
+ }
617
+ this.#roll(this.$(".mm-count"), total);
618
+ this.#paintTray();
619
+ }
620
+ // ── focus ──
621
+ #byKey(key) {
622
+ return this.$$("[data-key]").find((el) => el.dataset.key === key) ?? null;
623
+ }
624
+ #group(id) {
625
+ return this.$$(".mm-group").find((g) => g.dataset.store === id) ?? null;
626
+ }
627
+ #chip(id) {
628
+ return id === null ? this.#byKey("chip-all") : this.#byKey(`chip|${id}`);
629
+ }
630
+ #focus(el) {
631
+ const h = el;
632
+ if (!h || h.closest("[inert]") || h.disabled) return false;
633
+ h.focus({ preventScroll: true });
634
+ return true;
635
+ }
636
+ #focusKey(key) {
637
+ return this.#focus(this.#byKey(key));
638
+ }
639
+ #focusRow(kind, s, i) {
640
+ const key = entryKeys(s.entries)[i];
641
+ if (!key || !this.#focusKey(`${kind}|${s.id}|${key}`))
642
+ this.#focusKey(`add|${s.id}`) || this.#focus(this.#chip(this.#view.store));
643
+ }
644
+ /**
645
+ * Repaint without losing focus: the focused key comes back (rows are keyed by content, so a host answer that
646
+ * shifts indices keeps focus on the same entry); a key that is gone falls back to the same action on the row
647
+ * now at that place, then the store's add button, then the selected chip.
648
+ */
649
+ #keepFocus(fn) {
650
+ const a = this.getRootNode().activeElement === this ? this.root.activeElement : null;
651
+ const key = a?.dataset?.key ?? "";
652
+ const field = a;
653
+ const sel = a && "selectionStart" in a ? [field.selectionStart, field.selectionEnd] : null;
654
+ const store = a?.closest(".mm-group")?.dataset.store;
655
+ const at = Number(a?.closest(".mm-rowf")?.dataset.i ?? 0);
656
+ fn();
657
+ if (!a || a.isConnected || !key) return;
658
+ let next = this.#byKey(key);
659
+ if (!next || next.closest("[inert]")) {
660
+ next = null;
661
+ const kind = key.split("|")[0];
662
+ const s = this.#store(store);
663
+ if (s && (kind === "ed" || kind === "del")) {
664
+ const keys = entryKeys(s.entries);
665
+ for (let j = Math.min(at, s.entries.length - 1); j >= 0 && !next; j--) {
666
+ const el = this.#byKey(`${kind}|${s.id}|${keys[j]}`);
667
+ if (el && !el.closest("[inert]")) next = el;
668
+ }
669
+ }
670
+ if (!next && s) {
671
+ const add = this.#byKey(`add|${s.id}`);
672
+ if (add && !add.closest("[inert]")) next = add;
673
+ }
674
+ next ??= this.#chip(this.#view.store);
675
+ }
676
+ if (!next) return;
677
+ next.focus({ preventScroll: true });
678
+ if (sel && sel[0] != null && "setSelectionRange" in next) {
679
+ try {
680
+ next.setSelectionRange(sel[0], sel[1]);
681
+ } catch {
682
+ }
683
+ }
684
+ }
685
+ // ── events from the shadow tree ──
686
+ #onClick(e) {
687
+ const b = e.target?.closest?.("[data-act]");
688
+ if (!b || b.disabled) return;
689
+ const sid = b.closest(".mm-group")?.dataset.store ?? "";
690
+ const row = b.closest(".mm-rowf")?.dataset.row ?? "";
691
+ switch (b.dataset.act) {
692
+ case "chip":
693
+ this.#pick(b.hasAttribute("data-all") ? null : b.dataset.pick ?? null);
694
+ break;
695
+ case "search-open":
696
+ this.#searchOpen();
697
+ break;
698
+ case "search-close":
699
+ this.#searchClose();
700
+ break;
701
+ case "edit":
702
+ this.#editStart(sid, row);
703
+ break;
704
+ case "delete":
705
+ this.#delete(sid, row);
706
+ break;
707
+ case "cancel":
708
+ this.#editCancel();
709
+ break;
710
+ case "save":
711
+ this.#editSave();
712
+ break;
713
+ case "add-open":
714
+ this.#addOpen(sid);
715
+ break;
716
+ case "add-close":
717
+ this.#addClose(sid);
718
+ break;
719
+ case "add-save":
720
+ this.#add(sid);
721
+ break;
722
+ case "clear":
723
+ this.#clear(sid);
724
+ break;
725
+ case "undo":
726
+ this.#restore();
727
+ break;
728
+ }
729
+ }
730
+ #onKey(e) {
731
+ const el = e.target;
732
+ if (e.repeat && e.key === "Enter" && el.tagName === "BUTTON") {
733
+ e.preventDefault();
734
+ return;
735
+ }
736
+ if (el.closest(".mm-chips")) {
737
+ this.#chipKey(e);
738
+ return;
739
+ }
740
+ if (el.closest(".mm-search")) {
741
+ if (e.key === "Escape") {
742
+ e.preventDefault();
743
+ e.stopPropagation();
744
+ this.#searchClose();
745
+ }
746
+ return;
747
+ }
748
+ const key = el.dataset?.key ?? "";
749
+ const isEdit = key.startsWith("edit|");
750
+ if (isEdit || key.startsWith("new|")) {
751
+ if (e.isComposing || e.keyCode === 229) return;
752
+ const sid = el.closest(".mm-group")?.dataset.store ?? "";
753
+ if (e.key === "Enter" && !e.shiftKey) {
754
+ e.preventDefault();
755
+ if (isEdit) this.#editSave();
756
+ else this.#add(sid);
757
+ } else if (e.key === "Escape") {
758
+ e.preventDefault();
759
+ e.stopPropagation();
760
+ if (isEdit) this.#editCancel();
761
+ else this.#addClose(sid);
762
+ }
763
+ return;
764
+ }
765
+ if (e.key === "Escape" && this.#armed) {
766
+ e.stopPropagation();
767
+ this.#disarm();
768
+ }
769
+ }
770
+ #onInput(e) {
771
+ const el = e.target;
772
+ const key = el.dataset?.key ?? "";
773
+ if (key === "q") {
774
+ this.#view = { store: null, q: el.value, searching: true };
775
+ this.#applyView();
776
+ return;
777
+ }
778
+ const sid = el.closest(".mm-group")?.dataset.store ?? "";
779
+ const s = this.#store(sid);
780
+ if (!s) return;
781
+ if (key.startsWith("edit|") && this.#editing?.store === sid) {
782
+ this.#editing.draft = el.value;
783
+ grow(el);
784
+ const c = this.#editCount(s, el.value);
785
+ const out = el.closest(".mm-row")?.querySelector("[data-ecount]");
786
+ if (out) {
787
+ out.textContent = c.text;
788
+ out.classList.toggle("is-over", c.over);
789
+ }
790
+ } else if (key.startsWith("new|")) {
791
+ this.#drafts[sid] = el.value;
792
+ grow(el);
793
+ this.#paintCount(s);
794
+ }
795
+ }
796
+ // ── chips + search ──
797
+ #pick(id) {
798
+ this.#view = { ...this.#view, store: id && this.#store(id) ? id : null };
799
+ this.#applyView();
800
+ }
801
+ /** One tab stop; arrows move and select (wrapping), Home / End. */
802
+ #chipKey(e) {
803
+ const keys = ["ArrowRight", "ArrowDown", "ArrowLeft", "ArrowUp", "Home", "End"];
804
+ if (!keys.includes(e.key)) return;
805
+ e.preventDefault();
806
+ const order = [null, ...this.#work.map((s) => s.id)];
807
+ const cur = Math.max(0, order.indexOf(this.#view.store));
808
+ const step = e.key === "ArrowRight" || e.key === "ArrowDown" ? 1 : -1;
809
+ const next = e.key === "Home" ? 0 : e.key === "End" ? order.length - 1 : (cur + step + order.length) % order.length;
810
+ this.#pick(order[next]);
811
+ const chip = this.#chip(order[next]);
812
+ if (chip) {
813
+ chip.focus();
814
+ chip.scrollIntoView?.({ block: "nearest", inline: "nearest" });
815
+ }
816
+ }
817
+ #searchOpen() {
818
+ this.#view = { store: null, q: "", searching: true };
819
+ const q = this.$(".mm-q");
820
+ if (q) q.value = "";
821
+ this.#applyView();
822
+ q?.focus();
823
+ }
824
+ #searchClose() {
825
+ this.#view = { store: null, q: "", searching: false };
826
+ const q = this.$(".mm-q");
827
+ if (q) q.value = "";
828
+ this.#applyView();
829
+ this.#focusKey("search-open");
830
+ }
831
+ // ── edit ──
832
+ /** The open editor's index, re-checked against its text (rows above may have moved). */
833
+ #editIndex(s, e) {
834
+ if (s.entries[e.index] === e.orig) return e.index;
835
+ return s.entries.indexOf(e.orig);
836
+ }
837
+ #editStart(sid, key) {
838
+ const s = this.#store(sid);
839
+ if (!s || this.#ro) return;
840
+ const i = entryKeys(s.entries).indexOf(key);
841
+ if (i < 0) return;
842
+ const prev = this.#editing;
843
+ if (prev && prev.draft !== prev.orig && !(prev.store === sid && prev.index === i))
844
+ this.#keepDraft(prev.store, prev.draft);
845
+ this.#editing = { store: sid, index: i, orig: s.entries[i], draft: s.entries[i] };
846
+ this.#disarm(false);
847
+ this.#paintAll();
848
+ const ta = this.#byKey(`edit|${sid}`);
849
+ if (ta) {
850
+ ta.focus();
851
+ ta.setSelectionRange?.(ta.value.length, ta.value.length);
852
+ grow(ta);
853
+ }
854
+ }
855
+ #editCancel() {
856
+ const e = this.#editing;
857
+ if (!e) return;
858
+ this.#editing = null;
859
+ const s = this.#store(e.store);
860
+ this.#paintAll();
861
+ if (s) this.#focusRow("ed", s, Math.max(0, this.#indexOfText(s, e.orig, e.index)));
862
+ }
863
+ #indexOfText(s, text, near) {
864
+ return s.entries[near] === text ? near : s.entries.indexOf(text);
865
+ }
866
+ #editSave() {
867
+ const e = this.#editing;
868
+ const s = e ? this.#store(e.store) : void 0;
869
+ if (!e || !s) return;
870
+ const ta = this.#byKey(`edit|${s.id}`);
871
+ const text = ta ? ta.value : e.draft;
872
+ const t = this.#t;
873
+ if (!text.trim()) {
874
+ this.#say(t.emptyEntry, true);
875
+ return;
876
+ }
877
+ const i = this.#editIndex(s, e);
878
+ if (i < 0) return;
879
+ if (isSameEdit(text, e.orig)) {
880
+ this.#editing = null;
881
+ this.#paintAll();
882
+ this.#focusRow("ed", s, i);
883
+ return;
884
+ }
885
+ if (!this.emit("ui-edit", { store: s.id, index: i, text, previous: e.orig })) return;
886
+ s.entries[i] = text;
887
+ this.#editing = null;
888
+ this.#say(t.saved);
889
+ this.#paintAll();
890
+ this.#focusRow("ed", s, i);
891
+ }
892
+ // ── delete + undo ──
893
+ #delete(sid, key) {
894
+ if (this.#ro) return;
895
+ if (this.#timers.settle) this.#paintAll();
896
+ const s = this.#store(sid);
897
+ if (!s) return;
898
+ const i = entryKeys(s.entries).indexOf(key);
899
+ if (i < 0) return;
900
+ const text = s.entries[i];
901
+ if (!this.emit("ui-delete", { store: sid, index: i, text })) return;
902
+ const g = this.#group(sid);
903
+ const row = g?.querySelector(`.mm-rowf[data-i="${i}"]`) ?? null;
904
+ if (row) {
905
+ row.setAttribute("data-leaving", "");
906
+ fold(row, false);
907
+ }
908
+ if (g && row) this.#focusAfterDelete(g, row);
909
+ s.entries.splice(i, 1);
910
+ const e = this.#editing;
911
+ if (e?.store === sid && e.index > i) e.index--;
912
+ this.#dropUndo();
913
+ this.#undo = { store: sid, index: i, text, seq: ++this.#seq, left: UNDO_MS, started: 0, timer: 0 };
914
+ this.#armUndo();
915
+ this.#disarm(false);
916
+ this.#sync();
917
+ if (row && !prefersReducedMotion())
918
+ this.#timers.settle = window.setTimeout(() => {
919
+ this.#timers.settle = 0;
920
+ this.#paintAll();
921
+ }, FOLD_MS);
922
+ else this.#paintAll();
923
+ }
924
+ /**
925
+ * Focus after a delete: the next row the view shows, else the nearest shown row before it, else the store's add
926
+ * button, else the search box / the selected chip.
927
+ */
928
+ #focusAfterDelete(g, row) {
929
+ const rows = Array.from(g.querySelectorAll(".mm-rowf"));
930
+ const at = rows.indexOf(row);
931
+ const del = (f) => f !== row && !f.hasAttribute("data-leaving") && this.#focus(f.querySelector('[data-act="delete"]'));
932
+ for (let j = at + 1; j < rows.length; j++) if (del(rows[j])) return;
933
+ for (let j = at - 1; j >= 0; j--) if (del(rows[j])) return;
934
+ if (!g.hasAttribute("inert")) {
935
+ if (this.#focus(g.querySelector('[data-act="add-open"]'))) return;
936
+ if (this.#focus(g.querySelector('textarea[data-key^="new|"]'))) return;
937
+ }
938
+ if (this.#view.searching && this.#focus(this.$(".mm-q"))) return;
939
+ this.#focus(this.#chip(this.#view.store));
940
+ }
941
+ #held() {
942
+ return this.#holds.hover || this.#holds.panel;
943
+ }
944
+ /** The undo window: 5 s, paused while the pointer rests on the tray or focus is anywhere in the panel. */
945
+ #armUndo() {
946
+ const u = this.#undo;
947
+ if (!u) return;
948
+ clearTimeout(u.timer);
949
+ u.timer = 0;
950
+ u.started = Date.now();
951
+ if (this.#held() || !this.isConnected) return;
952
+ u.timer = window.setTimeout(
953
+ () => {
954
+ u.timer = 0;
955
+ if (this.#undo !== u) return;
956
+ this.#undo = null;
957
+ this.#paintTray();
958
+ },
959
+ Math.max(0, u.left)
960
+ );
961
+ }
962
+ #hold(kind, on) {
963
+ const was = this.#held();
964
+ this.#holds[kind] = on;
965
+ const held = this.#held();
966
+ const u = this.#undo;
967
+ if (held === was || !u) return;
968
+ if (!held) this.#armUndo();
969
+ else if (u.timer) {
970
+ clearTimeout(u.timer);
971
+ u.timer = 0;
972
+ u.left = Math.max(0, u.left - (Date.now() - u.started));
973
+ }
974
+ }
975
+ #dropUndo() {
976
+ if (this.#undo?.timer) clearTimeout(this.#undo.timer);
977
+ this.#undo = null;
978
+ }
979
+ #restore() {
980
+ const u = this.#undo;
981
+ if (!u || this.#ro) return;
982
+ const s = this.#store(u.store);
983
+ this.#dropUndo();
984
+ if (!s) return;
985
+ if (!this.emit("ui-restore", { store: u.store, index: u.index, text: u.text })) {
986
+ this.#keepDraft(s.id, u.text);
987
+ this.#paintAll();
988
+ this.#focusKey(`new|${s.id}`);
989
+ return;
990
+ }
991
+ const at = Math.min(u.index, s.entries.length);
992
+ s.entries.splice(at, 0, u.text);
993
+ const e = this.#editing;
994
+ if (e?.store === s.id && e.index >= at) e.index++;
995
+ this.#say(this.#t.restored);
996
+ this.#paintAll();
997
+ this.#reveal(s, at, true);
998
+ }
999
+ /** A row that came back or was added: shown under the view (else the view resets), unfolded, scrolled to. */
1000
+ #reveal(s, i, focusEdit) {
1001
+ const text = s.entries[i];
1002
+ if (text === void 0) return;
1003
+ const v = this.#view;
1004
+ if (v.store && v.store !== s.id || !entryMatches(text, v.q, s)) {
1005
+ this.#view = { store: null, q: "", searching: false };
1006
+ const q = this.$(".mm-q");
1007
+ if (q) q.value = "";
1008
+ this.#applyView();
1009
+ }
1010
+ const key = entryKeys(s.entries)[i];
1011
+ const f = this.#group(s.id)?.querySelector(`.mm-rowf[data-i="${i}"]`);
1012
+ const reduced = prefersReducedMotion();
1013
+ if (f && !reduced) {
1014
+ f.removeAttribute("data-open");
1015
+ void f.offsetHeight;
1016
+ f.setAttribute("data-open", "");
1017
+ }
1018
+ clearTimeout(this.#timers.scroll);
1019
+ if (f)
1020
+ this.#timers.scroll = window.setTimeout(() => {
1021
+ this.#timers.scroll = 0;
1022
+ f.scrollIntoView?.({ block: "nearest", behavior: reduced ? "auto" : "smooth" });
1023
+ }, REVEAL_SCROLL_MS);
1024
+ if (focusEdit) this.#focusKey(`ed|${s.id}|${key}`);
1025
+ }
1026
+ // ── add + clear ──
1027
+ /** A draft that has to leave its editor waits in the store's add box (appended when the box has text). */
1028
+ #keepDraft(sid, text) {
1029
+ if (!text.trim()) return;
1030
+ const cur = this.#drafts[sid] ?? "";
1031
+ if (cur === text) return;
1032
+ this.#drafts[sid] = cur.trim() ? `${cur}
1033
+
1034
+ ${text}` : text;
1035
+ this.#adding[sid] = true;
1036
+ }
1037
+ #addOpen(sid) {
1038
+ const s = this.#store(sid);
1039
+ const g = this.#group(sid);
1040
+ if (!s || !g || this.#ro) return;
1041
+ this.#adding[sid] = true;
1042
+ fold(g.querySelector("[data-addline]"), false);
1043
+ fold(g.querySelector("[data-newbox]"), true);
1044
+ g.querySelector('[data-act="add-open"]')?.setAttribute("aria-expanded", "true");
1045
+ const ta = g.querySelector('textarea[data-key^="new|"]');
1046
+ grow(ta);
1047
+ ta?.focus();
1048
+ this.#disarm(false);
1049
+ this.#paintTray();
1050
+ }
1051
+ /** Closing keeps the draft in the box. */
1052
+ #addClose(sid) {
1053
+ const g = this.#group(sid);
1054
+ this.#adding[sid] = false;
1055
+ if (!g) return;
1056
+ fold(g.querySelector("[data-newbox]"), false);
1057
+ fold(g.querySelector("[data-addline]"), true);
1058
+ const b = g.querySelector('[data-act="add-open"]');
1059
+ b?.setAttribute("aria-expanded", "false");
1060
+ b?.focus();
1061
+ this.#paintTray();
1062
+ }
1063
+ #add(sid) {
1064
+ const s = this.#store(sid);
1065
+ if (!s || this.#ro) return;
1066
+ const ta = this.#byKey(`new|${sid}`);
1067
+ const text = ta ? ta.value : this.#drafts[sid] ?? "";
1068
+ if (!text.trim()) return;
1069
+ if (!this.emit("ui-add", { store: sid, text })) return;
1070
+ s.entries.push(text);
1071
+ this.#drafts[sid] = "";
1072
+ this.#adding[sid] = false;
1073
+ this.#say(this.#t.added);
1074
+ this.#paintAll();
1075
+ this.#reveal(s, s.entries.length - 1, false);
1076
+ this.#focusKey(`add|${sid}`);
1077
+ }
1078
+ /** Two presses within 4 s: the first names the count, the second sends `ui-clear`. No undo. */
1079
+ #clear(sid) {
1080
+ const s = this.#store(sid);
1081
+ if (!s || this.#ro || !s.entries.length) return;
1082
+ const a = this.#armed;
1083
+ if (!a || a.store !== sid || a.n !== s.entries.length) {
1084
+ this.#armed = { store: sid, n: s.entries.length, seq: ++this.#seq };
1085
+ clearTimeout(this.#timers.armed);
1086
+ this.#timers.armed = window.setTimeout(() => {
1087
+ this.#timers.armed = 0;
1088
+ this.#disarm();
1089
+ }, ARM_MS);
1090
+ this.#paintClear();
1091
+ this.#paintTray();
1092
+ return;
1093
+ }
1094
+ this.#disarm(false);
1095
+ if (!this.emit("ui-clear", { store: sid })) {
1096
+ this.#paintClear();
1097
+ this.#paintTray();
1098
+ return;
1099
+ }
1100
+ const e = this.#editing;
1101
+ if (e?.store === sid) {
1102
+ this.#editing = null;
1103
+ if (e.draft !== e.orig) this.#keepDraft(sid, e.draft);
1104
+ }
1105
+ s.entries = [];
1106
+ this.#say(this.#t.cleared);
1107
+ this.#paintAll();
1108
+ if (!this.#focusKey(`add|${sid}`)) this.#focusKey(`new|${sid}`);
1109
+ }
1110
+ #disarm(paint = true) {
1111
+ clearTimeout(this.#timers.armed);
1112
+ this.#timers.armed = 0;
1113
+ if (!this.#armed) return;
1114
+ this.#armed = null;
1115
+ if (paint) {
1116
+ this.#paintClear();
1117
+ this.#paintTray();
1118
+ }
1119
+ }
1120
+ #paintClear() {
1121
+ const t = this.#t;
1122
+ for (const b of this.$$(".mm-clear")) {
1123
+ const s = this.#store(b.closest(".mm-group")?.dataset.store);
1124
+ const armed = !!s && this.#armed?.store === s.id;
1125
+ b.toggleAttribute("data-armed", armed);
1126
+ b.textContent = armed && s ? t.clearN(s.entries.length) : t.clear;
1127
+ }
1128
+ }
1129
+ // ── tray ──
1130
+ #say(text, fail = false) {
1131
+ const n = { text, seq: ++this.#seq, fail };
1132
+ this.#notice = n;
1133
+ clearTimeout(this.#timers.notice);
1134
+ this.#timers.notice = window.setTimeout(() => {
1135
+ this.#timers.notice = 0;
1136
+ if (this.#notice !== n) return;
1137
+ this.#notice = null;
1138
+ this.#paintTray();
1139
+ }, NOTICE_MS);
1140
+ this.#paintTray();
1141
+ }
1142
+ /** The tray line by priority: Clear armed > Deleted + Undo > a 3 s notice > editing / adding hints > counts. */
1143
+ #trayState() {
1144
+ const t = this.#t;
1145
+ const a = this.#armed;
1146
+ const as = a ? this.#store(a.store) : void 0;
1147
+ if (a && as) return { key: `armed-${a.seq}`, html: esc(t.armed), live: t.armedSr(as.label, a.n), tone: "warn" };
1148
+ const u = this.#undo;
1149
+ if (u)
1150
+ return {
1151
+ key: `undo-${u.seq}`,
1152
+ html: `<span class="mm-forgot"><span class="mm-flat">${esc(t.deleted[0])}</span><b class="mm-quote">${esc(u.text)}</b><span class="mm-flat">${esc(t.deleted[1])}</span></span>`,
1153
+ live: t.deletedSr(shortText(u.text))
1154
+ };
1155
+ const n = this.#notice;
1156
+ if (n) return { key: `notice-${n.seq}`, html: esc(n.text), live: n.text, tone: n.fail ? "fail" : "" };
1157
+ if (this.#editing) return { key: "editing", html: esc(t.editHint), live: t.editHint };
1158
+ if (!this.#ro && this.#work.some((s) => this.#adding[s.id]))
1159
+ return { key: "adding", html: esc(t.addHint), live: t.addHint };
1160
+ const total = this.#work.reduce((sum, s) => sum + s.entries.length, 0);
1161
+ const cap = this.#work.map((s) => {
1162
+ const used = fmtInt(storeUsed(s.entries, s.sep));
1163
+ return ` · ${s.label} ${s.limit > 0 ? t.of(used, fmtInt(s.limit)) : t.chars(used)}`;
1164
+ }).join("");
1165
+ return {
1166
+ key: "rest",
1167
+ total,
1168
+ cap,
1169
+ html: `<b class="mm-n">${fmtInt(total)}</b>${esc(t.entries(total))}<span class="mm-cap">${esc(cap)}</span>`,
1170
+ live: `${fmtInt(total)}${t.entries(total)}${cap}`
1171
+ };
1172
+ }
1173
+ /** Swap the caption only when the state changes (in 320 ms / out 220 ms); the same state rolls its count. */
1174
+ #paintTray() {
1175
+ const cap = this.$(".mm-caption");
1176
+ if (!cap) return;
1177
+ const s = this.#trayState();
1178
+ const undo = this.$(".mm-undo");
1179
+ if (undo) undo.hidden = !s.key.startsWith("undo-");
1180
+ const live = this.$(".mm-live");
1181
+ if (live && live.textContent !== s.live) live.textContent = s.live;
1182
+ const cur = this.#trayItem;
1183
+ if (cur && cur.isConnected && this.#trayKey === s.key) {
1184
+ if (s.key === "rest") {
1185
+ this.#roll(cur.querySelector(".mm-n"), s.total ?? 0);
1186
+ const c = cur.querySelector(".mm-cap");
1187
+ if (c && c.textContent !== s.cap) c.textContent = s.cap ?? "";
1188
+ }
1189
+ return;
1190
+ }
1191
+ this.#trayKey = s.key;
1192
+ if (cur) {
1193
+ cur.setAttribute("data-out", "");
1194
+ const id = window.setTimeout(
1195
+ () => {
1196
+ this.#swaps.delete(id);
1197
+ cur.remove();
1198
+ },
1199
+ prefersReducedMotion() ? 0 : SWAP_OUT_MS
1200
+ );
1201
+ this.#swaps.add(id);
1202
+ }
1203
+ const item = document.createElement("span");
1204
+ item.className = `mm-swap${s.tone ? ` is-${s.tone}` : ""}`;
1205
+ item.innerHTML = s.html;
1206
+ cap.append(item);
1207
+ this.#trayItem = item;
1208
+ for (const [node, st] of this.#rolls) {
1209
+ if (!node.isConnected) {
1210
+ cancelAnimationFrame(st.raf);
1211
+ this.#rolls.delete(node);
1212
+ }
1213
+ }
1214
+ const nb = item.querySelector(".mm-n");
1215
+ if (nb) this.#rolls.set(nb, { n: s.total ?? 0, raf: 0 });
1216
+ }
1217
+ /** Counts roll to their new value: 520 ms, ease-out quart; at once on first paint and with reduced motion. */
1218
+ #roll(el, to) {
1219
+ if (!el) return;
1220
+ const st = this.#rolls.get(el) ?? { n: Number.NaN, raf: 0 };
1221
+ if (st.raf && st.n === to) return;
1222
+ const from = Number.isFinite(st.n) ? st.n : to;
1223
+ st.n = to;
1224
+ if (st.raf) cancelAnimationFrame(st.raf);
1225
+ st.raf = 0;
1226
+ this.#rolls.set(el, st);
1227
+ if (from === to || prefersReducedMotion() || typeof requestAnimationFrame !== "function") {
1228
+ el.textContent = fmtInt(to);
1229
+ return;
1230
+ }
1231
+ let t0 = 0;
1232
+ const step = (now) => {
1233
+ if (!t0) t0 = now;
1234
+ const p = Math.min(1, (now - t0) / ROLL_MS);
1235
+ el.textContent = fmtInt(Math.round(from + (to - from) * (1 - (1 - p) ** 4)));
1236
+ st.raf = p < 1 ? requestAnimationFrame(step) : 0;
1237
+ };
1238
+ st.raf = requestAnimationFrame(step);
1239
+ }
1240
+ }
1241
+ define("ui-memory-panel", UiMemoryPanel);
1242
+ export {
1243
+ FOLD_MS,
1244
+ UNDO_MS,
1245
+ UiMemoryPanel,
1246
+ entryKeys,
1247
+ entryMatches,
1248
+ highlight,
1249
+ isSameEdit,
1250
+ shortText,
1251
+ storeUsed,
1252
+ usageAfterAdd,
1253
+ usageLevel
1254
+ };
1255
+ //# sourceMappingURL=memory-panel.js.map