clauderipple 0.9.4 → 0.10.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 (118) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +31 -0
  3. package/dist/app/package.json +1 -1
  4. package/dist/cli/src/hooks/agent-title.js +7 -4
  5. package/dist/cli/src/index.js +43 -0
  6. package/dist/cli/src/remote.js +263 -0
  7. package/dist/cli/src/schtasks.js +48 -40
  8. package/dist/cli/src/settings.js +2 -1
  9. package/dist/mod/hooks/i18n.ts +252 -6
  10. package/dist/mod/hooks/judge.ts +35 -17
  11. package/dist/mod/hooks/modes.ts +56 -1
  12. package/dist/mod/hooks/register.tsx +238 -32
  13. package/dist/mod/hooks/session.ts +98 -1
  14. package/dist/mod/hooks/settings.js +7 -3
  15. package/dist/mod/hooks/settings.ts +9 -4
  16. package/dist/mod/hooks/stats.ts +25 -1
  17. package/dist/mod/hooks/ui/agents.tsx +858 -79
  18. package/dist/mod/hooks/ui/alerts.tsx +20 -17
  19. package/dist/mod/hooks/ui/art.ts +33 -7
  20. package/dist/mod/hooks/ui/band.tsx +262 -81
  21. package/dist/mod/hooks/ui/flat.ts +265 -0
  22. package/dist/mod/hooks/ui/kit.tsx +70 -55
  23. package/dist/mod/hooks/ui/look.ts +129 -0
  24. package/dist/mod/hooks/ui/settings.tsx +71 -46
  25. package/dist/mod/hooks/ui/setup.tsx +15 -13
  26. package/dist/mod/hooks/ui/theme.ts +3 -0
  27. package/dist/mod/hooks/ui/tools.ts +124 -0
  28. package/dist/mod/hooks/ui/view.ts +116 -3
  29. package/dist/mod/hooks/workers.ts +33 -6
  30. package/dist/mod/tests/register.test.tsx +155 -11
  31. package/dist/mod/tests/ui.test.tsx +375 -27
  32. package/dist/mod/types/index.d.ts +68 -2
  33. package/dist/remote/package.json +28 -0
  34. package/dist/remote/src/away.js +85 -0
  35. package/dist/remote/src/bridge-client.js +295 -0
  36. package/dist/remote/src/composer-menu.js +11 -0
  37. package/dist/remote/src/config.js +102 -0
  38. package/dist/remote/src/desktop.js +1018 -0
  39. package/dist/remote/src/e2ee.js +126 -0
  40. package/dist/remote/src/e2ee.ts +126 -0
  41. package/dist/remote/src/eventlog.js +298 -0
  42. package/dist/remote/src/expo-push.js +127 -0
  43. package/dist/remote/src/folders.js +79 -0
  44. package/dist/remote/src/fsutil.js +39 -0
  45. package/dist/remote/src/handoff.js +38 -0
  46. package/dist/remote/src/host-service.js +168 -0
  47. package/dist/remote/src/images.js +215 -0
  48. package/dist/remote/src/local-control.js +138 -0
  49. package/dist/remote/src/main.js +264 -0
  50. package/dist/remote/src/permissions.js +124 -0
  51. package/dist/remote/src/power.js +76 -0
  52. package/dist/remote/src/processes.js +24 -0
  53. package/dist/remote/src/protocol.js +32 -0
  54. package/dist/remote/src/protocol.ts +284 -0
  55. package/dist/remote/src/push-content.js +32 -0
  56. package/dist/remote/src/push.js +134 -0
  57. package/dist/remote/src/receipts.js +111 -0
  58. package/dist/remote/src/relay.js +438 -0
  59. package/dist/remote/src/router.js +208 -0
  60. package/dist/remote/src/sdk-launcher.js +18 -0
  61. package/dist/remote/src/server.js +706 -0
  62. package/dist/remote/src/service.js +157 -0
  63. package/dist/remote/src/sessions.js +1800 -0
  64. package/dist/remote/src/sidebar-storage.js +211 -0
  65. package/dist/remote/src/sidebar.js +173 -0
  66. package/dist/remote/src/tool-details.js +205 -0
  67. package/dist/remote/src/tool-details.ts +153 -0
  68. package/dist/remote/src/worktree.js +99 -0
  69. package/dist/remote/web/app.ts +746 -0
  70. package/dist/remote/web/conn.ts +193 -0
  71. package/dist/remote/web/demo.ts +134 -0
  72. package/dist/remote/web/dom.ts +124 -0
  73. package/dist/remote/web/esc.ts +4 -0
  74. package/dist/remote/web/i18n.ts +374 -0
  75. package/dist/remote/web/icon.svg +1 -0
  76. package/dist/remote/web/index.html +24 -0
  77. package/dist/remote/web/manifest.webmanifest +10 -0
  78. package/dist/remote/web/mascot.ts +60 -0
  79. package/dist/remote/web/md.ts +78 -0
  80. package/dist/remote/web/notify.ts +55 -0
  81. package/dist/remote/web/pushstate.ts +27 -0
  82. package/dist/remote/web/secure.ts +109 -0
  83. package/dist/remote/web/sidebar.ts +106 -0
  84. package/dist/remote/web/style.css +311 -0
  85. package/dist/remote/web/sw.ts +48 -0
  86. package/dist/remote/web/timeline.ts +224 -0
  87. package/dist/remote/web/views.ts +323 -0
  88. package/dist/router/src/admin.js +227 -4
  89. package/dist/router/src/config.js +30 -0
  90. package/dist/router/src/index.js +39 -0
  91. package/dist/router/src/ingress/translate.js +6 -2
  92. package/dist/router/src/phone.js +82 -0
  93. package/dist/router/src/presets.js +3 -0
  94. package/dist/router/src/providers/anthropic-usage.js +33 -1
  95. package/dist/router/src/providers/chatgpt/index.js +41 -11
  96. package/dist/router/src/providers/chatgpt/translate.js +147 -7
  97. package/dist/router/src/providers/google/index.js +26 -0
  98. package/dist/router/src/providers/google/prompt-refusal.js +29 -0
  99. package/dist/router/src/providers/google/usage.js +257 -0
  100. package/dist/router/src/providers/grok/index.js +18 -0
  101. package/dist/router/src/providers/grok/usage.js +77 -0
  102. package/dist/router/src/providers/openai/index.js +29 -5
  103. package/dist/router/src/providers/openai/translate.js +260 -27
  104. package/dist/router/src/providers/opencode-usage.js +116 -0
  105. package/dist/router/src/proxy.js +339 -258
  106. package/dist/router/src/reasoning.js +157 -0
  107. package/dist/router/src/remote-host.js +320 -0
  108. package/dist/router/src/remote-tunnel.js +320 -0
  109. package/dist/router/src/requestlog.js +48 -39
  110. package/dist/router/src/thinking.js +20 -9
  111. package/dist/router/src/usage-history.js +227 -0
  112. package/dist/router/src/version.js +1 -1
  113. package/dist/ui/app.js +436 -59
  114. package/dist/ui/i18n.js +184 -0
  115. package/dist/ui/index.html +24 -5
  116. package/dist/ui/style.css +39 -0
  117. package/docs/ARCHITECTURE.md +164 -12
  118. package/package.json +7 -1
@@ -0,0 +1,265 @@
1
+ // Flat (2026-10-14): the desktop's second skin, the app's own surfaces. Nothing is painted behind a
2
+ // capsule: it is a Box of the host's, a hairline edge and a tint of the page, so it follows the
3
+ // app's light or dark theme as the panes do, and every host Text on it is the host's own ink. The
4
+ // pictures paint with the palette variables of theme.ts (svgDoc), which follow the theme through
5
+ // `prefers-color-scheme` as the deep's well does, and the accent marks what is on, chosen or the one
6
+ // thing to press: nothing else has a hue of its own, and amber, red and green keep their meanings.
7
+ // No gradient, glow, water or weather; 리플이 keeps its life. Every size is art.ts's, so the presses
8
+ // laid over the pictures line up (kit.tsx). Pure: SVG only, so node tests can load it.
9
+
10
+ import { AGENTS_W, CELL_PX, CLOSE_PX, CONTROL_PX, FLAME, GLYPH, ICON, ICON_PX, METER_H, METER_W, SWITCH_H, SWITCH_W, TILE_PX, esc, labelPx, r } from './art.ts'
11
+ import type { Tone } from './art.ts'
12
+ import type { Paint } from './look.ts'
13
+ import { svgDoc } from './theme.ts'
14
+ import type { Appearance } from './theme.ts'
15
+
16
+ const INK = 'var(--ink)'
17
+ const MUTED = 'var(--muted)'
18
+ const FAINT = 'var(--faint)'
19
+ const FILL = 'var(--fill)'
20
+ const STROKE = 'var(--stroke)'
21
+ const ACCENT = 'var(--accent)'
22
+ const WASH = 'var(--accentWash)'
23
+ const LINE = 'var(--accentLine)'
24
+ const PRIMARY = 'var(--primary)'
25
+
26
+ /** The corners of a drawn control: the host's own buttons are about this round. */
27
+ const RX = 6
28
+
29
+ const STILL = '@media (prefers-reduced-motion:reduce){*{animation:none!important}}'
30
+
31
+ /** A tone's colour: amber, red and green mean what they mean; every other tone is the accent. */
32
+ function toneVar(tone: Tone): string {
33
+ return tone === 'amber' || tone === 'ember' ? 'var(--warn)' : tone === 'red' ? 'var(--bad)' : tone === 'green' ? 'var(--ok)' : ACCENT
34
+ }
35
+
36
+ /** The 44 px flame of the keep-warm tile (the deep's path). */
37
+ const TILE_FLAME = 'M22 35.5c6.4 0 10-4.2 10-9.6 0-6-5.2-9-6.1-15.4-3.6 2.4-5.6 6-5.4 9.3-2.2-.9-3.3-3-3.5-4.9-3 2.9-4.4 6.4-4.4 11.3 0 5.4 3.6 9.3 9.4 9.3z'
38
+
39
+ /**
40
+ * A text's colour as a style: svgDoc's `text{fill:var(--ink)}` rule beats a `fill` attribute, so an
41
+ * attribute left every label and the agents' count in ink (black on the accent, 2026-10-16).
42
+ */
43
+ function fillOf(fill: string): string {
44
+ return ' style="fill:' + fill + '"'
45
+ }
46
+
47
+ /** A label drawn at 12 px, centred on `cx`, on a control `h` high. */
48
+ function label(text: string, cx: number, h: number, fill: string, weight: number): string {
49
+ return '<text x="' + r(cx) + '" y="' + r(h / 2 + 12 * 0.36) + '" text-anchor="middle" font-size="12" font-weight="' + weight + '"' + fillOf(fill) + '>' + esc(text) + '</text>'
50
+ }
51
+
52
+ /** The pictures of the flat skin for one appearance: the theme's palette, fixed or following the app. */
53
+ export function flatPictures(appearance: Appearance): Paint {
54
+ const doc = (w: number, h: number, body: string, css = ''): string => svgDoc(w, h, appearance, body, STILL + css)
55
+ /** A rule for `sel` that follows the theme: `light` on a light app, `dark` on a dark one. */
56
+ const themed = (sel: string, light: string, dark: string): string =>
57
+ appearance === 'dark' ? sel + '{' + dark + '}' : appearance === 'light' ? sel + '{' + light + '}' : sel + '{' + light + '}@media (prefers-color-scheme:dark){' + sel + '{' + dark + '}}'
58
+
59
+ return {
60
+ // The capsule is the app's own surface, as its prompt box is: white on a light app, its dark
61
+ // surface on a dark one (the app's bg-000, read from its stylesheet 2026-10-16). A grey tint
62
+ // over the host's slot read as a dark block beside the prompt box.
63
+ sea: (width, height) => doc(width, height, '<rect class="s" width="' + r(width) + '" height="' + r(height) + '" rx="9"/>', themed('.s', 'fill:#ffffff', 'fill:#30302e')),
64
+
65
+ // The capsule's own surface: it hides the words beneath a tip.
66
+ plate: (width, height) => doc(width, height, '<rect class="p" width="' + r(width) + '" height="' + r(height) + '"/>', themed('.p', 'fill:#ffffff', 'fill:#30302e')),
67
+
68
+ // On: the accent's track and a white knob at the right; off: an outline and a quiet knob at the left.
69
+ switch: (on, busy = false) =>
70
+ doc(SWITCH_W, SWITCH_H, on
71
+ ? '<rect x="2" y="2" width="32" height="18" rx="9" fill="' + ACCENT + '"/><circle class="' + (busy ? 'pu' : '') + '" cx="25" cy="11" r="6" fill="#fff"/>'
72
+ : '<rect x="2.5" y="2.5" width="31" height="17" rx="8.5" fill="none" stroke="' + MUTED + '"/><circle cx="11" cy="11" r="5" fill="' + MUTED + '"/>',
73
+ '.pu{animation:pu 1.3s ease-in-out infinite}@keyframes pu{50%{opacity:.5}}'),
74
+
75
+ // The Auto word: on, the accent's wash with the word in the accent; off, a quiet outline.
76
+ word: (text, on, busy = false) => {
77
+ const cells = Math.ceil((labelPx(text, 12) + 22) / CELL_PX)
78
+ const w = cells * CELL_PX
79
+ const h = CONTROL_PX
80
+ const frame = '<rect class="' + (busy && on ? 'pu' : '') + '" x="1" y="1" width="' + r(w - 2) + '" height="' + (h - 2) + '" rx="' + RX + '" fill="' + (on ? WASH : FILL) + '" stroke="' + (on ? LINE : STROKE) + '"/>'
81
+ return { source: doc(w, h, frame + label(text, w / 2, h, on ? ACCENT : MUTED, on ? 650 : 500), '.pu{animation:pu 1.3s ease-in-out infinite}@keyframes pu{50%{stroke-opacity:.25}}'), cells }
82
+ },
83
+
84
+ extraHover: widthPx => doc(widthPx, CONTROL_PX, '<rect x="0" y="1" width="' + r(widthPx) + '" height="' + (CONTROL_PX - 2) + '" fill="' + INK + '" fill-opacity=".06"/>'),
85
+
86
+ // A ghost button is the page's tint with a hairline; the one thing to press is the accent's fill.
87
+ pill: (text, look, _tone, glyph) => {
88
+ const gw = glyph ? 18 : 0
89
+ const cells = Math.ceil((labelPx(text, 12) + 26 + gw) / CELL_PX)
90
+ const w = cells * CELL_PX
91
+ const h = CONTROL_PX
92
+ const ink = look === 'primary' ? '#fff' : INK
93
+ let body = look === 'primary'
94
+ ? '<rect x="1" y="1" width="' + r(w - 2) + '" height="' + (h - 2) + '" rx="' + RX + '" fill="' + PRIMARY + '"/>'
95
+ : '<rect x="1" y="1" width="' + r(w - 2) + '" height="' + (h - 2) + '" rx="' + RX + '" fill="' + FILL + '" stroke="' + STROKE + '"/>'
96
+ const textW = labelPx(text, 12)
97
+ const x0 = (w - textW - gw) / 2
98
+ if (glyph) body += '<g transform="translate(' + r(x0) + ' ' + (h / 2 - 6) + ')" fill="none" stroke="' + ink + '" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round">' + GLYPH[glyph] + '</g>'
99
+ body += label(text, x0 + gw + textW / 2, h, ink, look === 'primary' ? 650 : 550)
100
+ return { source: doc(w, h, body), cells }
101
+ },
102
+
103
+ // One track, the chosen segment a lozenge in the accent's wash, named in the accent.
104
+ segmented: (labels, cells, chosen) => {
105
+ const h = CONTROL_PX
106
+ const w = cells.reduce((n, c) => n + c, 0) * CELL_PX
107
+ let body = '<rect x=".5" y=".5" width="' + r(w - 1) + '" height="' + (h - 1) + '" rx="' + (RX + 1) + '" fill="' + FILL + '" stroke="' + STROKE + '"/>'
108
+ let x = 0
109
+ const starts = cells.map(c => {
110
+ const at = x
111
+ x += c * CELL_PX
112
+ return at
113
+ })
114
+ for (let i = 1; i < labels.length; i++) {
115
+ if (i === chosen || i - 1 === chosen) continue
116
+ body += '<rect x="' + r(starts[i]! - 0.5) + '" y="7" width="1" height="' + (h - 14) + '" fill="' + STROKE + '"/>'
117
+ }
118
+ if (chosen >= 0 && chosen < labels.length) {
119
+ body += '<rect x="' + r(starts[chosen]! + 3) + '" y="3" width="' + r(cells[chosen]! * CELL_PX - 6) + '" height="' + (h - 6) + '" rx="' + (RX - 1) + '" fill="' + WASH + '" stroke="' + LINE + '"/>'
120
+ }
121
+ labels.forEach((text, i) => {
122
+ const lit = i === chosen
123
+ body += label(text, starts[i]! + (cells[i]! * CELL_PX) / 2, h, lit ? ACCENT : MUTED, lit ? 650 : 500)
124
+ })
125
+ return doc(w, h, body)
126
+ },
127
+
128
+ // A quiet track, the stretch from the middle to the chosen stop in the accent, the knob the accent.
129
+ slider: (count, cellsPerStop, chosen) => {
130
+ const part = cellsPerStop * CELL_PX
131
+ const w = count * part
132
+ const h = 22
133
+ const at = (i: number) => part * (i + 0.5)
134
+ const y = h / 2
135
+ const mid = Math.floor(count / 2)
136
+ const lo = Math.min(mid, chosen)
137
+ const hi = Math.max(mid, chosen)
138
+ let body = '<rect x="' + r(at(0)) + '" y="' + (y - 1.5) + '" width="' + r(at(count - 1) - at(0)) + '" height="3" rx="1.5" fill="' + FAINT + '"/>'
139
+ if (hi > lo) body += '<rect x="' + r(at(lo)) + '" y="' + (y - 1.5) + '" width="' + r(at(hi) - at(lo)) + '" height="3" rx="1.5" fill="' + ACCENT + '"/>'
140
+ for (let i = 0; i < count; i++) {
141
+ if (i === chosen) continue
142
+ body += i === mid
143
+ ? '<rect x="' + r(at(i) - 1) + '" y="' + (y - 5) + '" width="2" height="10" rx="1" fill="' + MUTED + '"/>'
144
+ : '<circle cx="' + r(at(i)) + '" cy="' + y + '" r="2.2" fill="' + MUTED + '" fill-opacity=".6"/>'
145
+ }
146
+ const c = at(chosen)
147
+ body += '<circle cx="' + r(c) + '" cy="' + y + '" r="6.5" fill="' + ACCENT + '"/><circle cx="' + r(c) + '" cy="' + y + '" r="2.4" fill="#fff" fill-opacity=".95"/>'
148
+ return doc(w, h, body)
149
+ },
150
+
151
+ well: (width, height) => doc(width, height, '<rect class="w" x=".5" y=".5" width="' + r(width - 1) + '" height="' + r(height - 1) + '" rx="' + RX + '"/>', themed('.w', 'fill:#fdfcfa;stroke:#a8a49b;stroke-opacity:.7', 'fill:#262624;stroke:#a8a49b;stroke-opacity:.45')),
152
+
153
+ close: (lit = false) =>
154
+ doc(CLOSE_PX, CLOSE_PX, (lit ? '<circle cx="11" cy="11" r="10" fill="' + FILL + '" stroke="' + STROKE + '"/>' : '') +
155
+ '<path d="M7.6 7.6l6.8 6.8M14.4 7.6l-6.8 6.8" stroke="' + (lit ? INK : MUTED) + '" stroke-width="1.6" stroke-linecap="round"/>'),
156
+
157
+ // Five bars: the accent when Auto chose, ink when the person did, quiet when Auto is off.
158
+ meter: (lit, look) => {
159
+ let bars = ''
160
+ for (let i = 0; i < 5; i++) {
161
+ const bh = 5 + i * 2.75
162
+ const on = look === 'deciding' || i < lit
163
+ const fill = !on ? FAINT : look === 'manual' ? INK : look === 'quiet' ? MUTED : ACCENT
164
+ const wave = look === 'deciding' ? ' class="wa" style="animation-delay:' + r(i * 0.13) + 's"' : ''
165
+ bars += '<rect x="' + r(i * 4.8 + 0.5) + '" y="' + r(METER_H - bh) + '" width="3.4" height="' + r(bh) + '" rx="1.2" fill="' + fill + '"' + wave + '/>'
166
+ }
167
+ return doc(METER_W, METER_H, bars, '.wa{animation:wa 1.15s ease-in-out infinite;opacity:.3}@keyframes wa{0%,100%{opacity:.25}40%{opacity:1}}')
168
+ },
169
+
170
+ ring: (percent, tone) => {
171
+ const rad = 6
172
+ const c = 2 * Math.PI * rad
173
+ const share = Math.max(0, Math.min(1, percent / 100))
174
+ const dash = r((c * Math.round(share * 100)) / 100)
175
+ return doc(16, 16,
176
+ '<circle cx="8" cy="8" r="' + rad + '" fill="none" stroke="' + FAINT + '" stroke-width="2.4"/>' +
177
+ '<circle cx="8" cy="8" r="' + rad + '" fill="none" stroke="' + (tone === 'ink' ? INK : toneVar(tone)) + '" stroke-width="2.4" stroke-linecap="round" stroke-dasharray="' + dash + ' ' + r(c) + '" transform="rotate(-90 8 8)"/>')
178
+ },
179
+
180
+ clock: (share, tone) => {
181
+ const c = 8
182
+ const rad = 6.4
183
+ const left = Math.max(0, Math.min(1, share))
184
+ const fill = tone === 'soft' ? MUTED : toneVar(tone)
185
+ const pr = rad - 1.8
186
+ let pie = ''
187
+ if (left >= 0.999) pie = '<circle cx="' + c + '" cy="' + c + '" r="' + r(pr) + '" fill="' + fill + '"/>'
188
+ else if (left > 0.001) {
189
+ const a = (2 * Math.PI * Math.round(left * 12)) / 12
190
+ pie = '<path d="M' + c + ' ' + c + 'V' + r(c - pr) + 'A' + r(pr) + ' ' + r(pr) + ' 0 ' + (a > Math.PI ? 1 : 0) + ' 1 ' + r(c + pr * Math.sin(a)) + ' ' + r(c - pr * Math.cos(a)) + 'Z" fill="' + fill + '"/>'
191
+ }
192
+ return doc(16, 16, '<circle cx="' + c + '" cy="' + c + '" r="' + rad + '" fill="none" stroke="' + fill + '" stroke-opacity=".75" stroke-width="1.3"/>' + pie)
193
+ },
194
+
195
+ gauge: kind =>
196
+ doc(16, 16,
197
+ '<path d="M1.8 11.4A6.2 6.2 0 0 1 14.2 11.4" fill="none" stroke="' + MUTED + '" stroke-opacity=".8" stroke-width="1.3" stroke-linecap="round"/>' +
198
+ '<path d="' + (kind === 'seven_day' ? 'M8 11.4L11.4 7.6' : 'M8 11.4L8 6.5') + '" fill="none" stroke="' + MUTED + '" stroke-width="1.4" stroke-linecap="round"/><circle cx="8" cy="11.4" r="1.2" fill="' + MUTED + '"/>'),
199
+
200
+ // Quiet ink at rest, the accent when on: the lit flame is filled, still.
201
+ icon: (icon, on) => {
202
+ if (icon === 'flame' && on) return doc(ICON_PX, ICON_PX, '<path fill="' + ACCENT + '" d="' + FLAME + '"/>')
203
+ return doc(ICON_PX, ICON_PX, '<g fill="none" stroke="' + (on ? ACCENT : MUTED) + '" stroke-width="1.45" stroke-linecap="round" stroke-linejoin="round">' + ICON[icon] + '</g>')
204
+ },
205
+
206
+ agents: (count, on) => {
207
+ const text = count > 9 ? '9+' : String(count)
208
+ const badge = count > 0
209
+ ? '<circle cx="20" cy="5.5" r="5.5" fill="' + ACCENT + '"/><text x="20" y="8.6" text-anchor="middle" font-size="' + (text.length > 1 ? 7 : 8.5) + '" font-weight="750"' + fillOf('#fff') + '>' + text + '</text>'
210
+ : ''
211
+ return doc(AGENTS_W, 16, '<g transform="translate(0 .5)" fill="none" stroke="' + (on ? ACCENT : MUTED) + '" stroke-width="1.45" stroke-linecap="round" stroke-linejoin="round">' + ICON.agents + '</g>' + badge)
212
+ },
213
+
214
+ rule: () => doc(9, 16, '<rect x="4" y="1" width="1" height="14" rx=".5" fill="' + STROKE + '"/>'),
215
+
216
+ // A tile's picture: a rounded square of the page's tint, the setting drawn in the accent while it
217
+ // is on (or chosen), in quiet ink while it is off. Still.
218
+ tile: art => {
219
+ const s = TILE_PX
220
+ const square = '<rect x="1" y="1" width="' + (s - 2) + '" height="' + (s - 2) + '" rx="11" fill="' + FILL + '" stroke="' + STROKE + '"/>'
221
+ if (art.kind === 'effort') {
222
+ let bars = ''
223
+ for (let i = 0; i < 5; i++) {
224
+ const bh = 7 + i * 4.2
225
+ bars += '<rect x="' + r(9.2 + i * 5.4) + '" y="' + r(34 - bh) + '" width="3.6" height="' + r(bh) + '" rx="1.4" fill="' + (art.on ? ACCENT : MUTED) + '" fill-opacity="' + (art.on ? 1 : 0.5) + '"/>'
226
+ }
227
+ return doc(s, s, square + bars)
228
+ }
229
+ if (art.kind === 'judge') {
230
+ const mark = art.judge === 'model'
231
+ ? '<g fill="none" stroke="' + ACCENT + '" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><circle cx="13" cy="29" r="3.2"/><circle cx="31" cy="15" r="3.2"/><path d="M16 27.5c6-1.5 4-9 12-11"/></g>'
232
+ : '<text x="22" y="29.5" text-anchor="middle" font-size="21" font-weight="760"' + fillOf(ACCENT) + '>' + (art.judge === 'haiku' ? 'H' : 'J') + '</text>'
233
+ return doc(s, s, square + mark)
234
+ }
235
+ if (art.kind === 'mode') {
236
+ const card = (y: number, top: boolean) =>
237
+ '<rect x="11" y="' + y + '" width="22" height="9" rx="2.6" fill="' + (top && art.on ? WASH : 'none') + '" stroke="' + (top && art.on ? ACCENT : MUTED) + '" stroke-opacity="' + (top && art.on ? 1 : 0.55) + '" stroke-width="1.4"/>'
238
+ const face = art.on ? '<circle cx="15.5" cy="19.5" r="1.4" fill="' + ACCENT + '"/><rect x="19" y="18.6" width="10" height="1.8" rx=".9" fill="' + ACCENT + '" fill-opacity=".7"/>' : ''
239
+ return doc(s, s, square + card(28, false) + card(21.5, false) + card(15, true) + face)
240
+ }
241
+ if (art.kind === 'skin') {
242
+ return doc(s, s, square +
243
+ '<rect x="10.5" y="12.5" width="23" height="19" rx="3.5" fill="' + WASH + '" stroke="' + ACCENT + '" stroke-width="1.6"/>' +
244
+ '<circle cx="16" cy="18.5" r="2" fill="' + ACCENT + '"/>' +
245
+ '<path d="M20.5 18.5h8M15 24h12M15 27.5h8" fill="none" stroke="' + ACCENT + '" stroke-width="1.6" stroke-linecap="round"/>')
246
+ }
247
+ if (art.on) return doc(s, s, square + '<path d="' + TILE_FLAME + '" fill="' + ACCENT + '"/>')
248
+ const arm = (a: number) => '<g transform="rotate(' + a + ' 22 22)"><path d="M22 9v26M22 13l-3-3M22 13l3-3M22 31l-3 3M22 31l3 3"/></g>'
249
+ return doc(s, s, square + '<g fill="none" stroke="' + MUTED + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">' + arm(0) + arm(60) + arm(120) + '</g>')
250
+ },
251
+
252
+ beads: (step, total) => {
253
+ const gap = 16
254
+ const w = gap * (total - 1) + 16
255
+ let body = '<rect x="8" y="7.4" width="' + (w - 16) + '" height="1.2" rx=".6" fill="' + FAINT + '"/>'
256
+ for (let i = 1; i <= total; i++) {
257
+ const cx = 8 + (i - 1) * gap
258
+ if (i < step) body += '<circle cx="' + cx + '" cy="8" r="3.6" fill="' + ACCENT + '"/>'
259
+ else if (i === step) body += '<circle cx="' + cx + '" cy="8" r="7" fill="' + WASH + '"/><circle cx="' + cx + '" cy="8" r="4.2" fill="' + ACCENT + '"/>'
260
+ else body += '<circle cx="' + cx + '" cy="8" r="3.4" fill="' + FILL + '" stroke="' + STROKE + '"/>'
261
+ }
262
+ return { source: doc(w, 16, body), width: w }
263
+ },
264
+ }
265
+ }
@@ -1,6 +1,7 @@
1
- // The desktop's capsules and drawn controls, put together: a picture from art.ts, the host's text in
2
- // our own colours beside it, and a blank native button laid over both to take the press. Desktop and
3
- // the editor only (an Svg in the table); the terminal keeps the text controls of parts.tsx.
1
+ // The desktop's capsules and drawn controls, put together: a picture from the skin's pictures
2
+ // (look.ts: art.ts for the deep, flat.ts for flat), the host's text in the skin's colours beside
3
+ // it, and a blank native button laid over both to take the press. Desktop and the editor only (an
4
+ // Svg in the table); the terminal keeps the text controls of parts.tsx.
4
5
  //
5
6
  // Host facts this is built on (2026-10):
6
7
  // - A Box drawn `position: "absolute"` is painted over every sibling that is not itself positioned,
@@ -15,28 +16,32 @@
15
16
  // the drawing (an ellipsis would show) and sits in an absolute Box over the control, clipped to it.
16
17
  // - A plain Button's label takes the host's ink, which a dark capsule would swallow; nothing on a
17
18
  // capsule is a labelled host Button. A keyed Box's `hover` lights a control on our own terms.
19
+ // - A skin that has no backdrop (flat) is the Box's own `backgroundColor` and `borderStyle`, which
20
+ // the host paints with the theme.
18
21
 
19
22
  import type { RenderNode } from 'claude-code'
20
23
 
21
- import { rippleSource } from './parts'
22
24
  import type { Els } from './parts'
23
25
  import type { Face } from './ripple'
24
26
  import type { Frame } from './view'
25
- import { CELL_PX, CLOSE_PX, INK, ROW_PX, SUB, SWITCH_H, SWITCH_W, WELL_PX, closeArt, extraHoverArt, labelPx, pillArt, seaArt, segmentedArt, sliderArt, switchArt, wellArt, wordArt } from './art'
27
+ import { CELL_PX, CLOSE_PX, CONTROL_PX, ROW_PX, SWITCH_H, SWITCH_W, WELL_PX, labelPx } from './art'
26
28
  import type { Glyph, PillLook, Sea, Tone } from './art'
29
+ import { lookOf } from './look'
27
30
  import { chOf } from './theme'
28
31
 
29
32
  /** Width of one U+00A0 in the host's 12 px button font, px, taken a little wide. */
30
33
  const NBSP_PX = 3.6
31
34
  /** The host's padding either side of a plain Button's label, px. */
32
35
  const BUTTON_PAD = 6
33
- /** The capsule's edge: a warm grey that reads against a light page and a dark one alike. */
34
- const EDGE = 'rgba(128,124,116,0.55)'
35
- /** A control under the pointer: a soft light on the water. */
36
- const HOVER = 'rgba(255,255,255,0.09)'
37
- const HOVER_EDGE = 'rgba(255,255,255,0.16)'
38
- /** A drawn control's height, px: pills, segments (art.ts holds the same figure). */
39
- const CONTROL_PX = 26
36
+
37
+ /**
38
+ * The props that light a Box under the pointer the way `lit` lights a control: the skin's light
39
+ * (a transparent round edge at rest, so the box does not move when lit), joined to `scope`.
40
+ */
41
+ export function hoverLight(frame: Frame, scope?: string): Record<string, unknown> {
42
+ const look = lookOf(frame).lit
43
+ return { ...look, hover: { ...look.hover, ...(scope ? { scope } : {}) } }
44
+ }
40
45
 
41
46
  /** Cells a run of px takes, rounded up. */
42
47
  function cellsOf(px: number): number {
@@ -89,48 +94,52 @@ export function pic(el: Els, key: string, source: string, alt: string, width: nu
89
94
  }
90
95
 
91
96
  /**
92
- * 리플이 on a capsule: its face drawn for the deep (the dark palette: light ink, lavender tone), a
93
- * plain image in a Box of its own that never shrinks. `percent` fills a gauge face's ring.
97
+ * 리플이 on a capsule: its face drawn in the skin's palette (the deep's dark one, the theme's for
98
+ * flat), a plain image in a Box of its own that never shrinks. `percent`
99
+ * fills a gauge face's ring.
94
100
  */
95
- export function ripple(el: Els, key: string, face: Face, alt: string, size: number, percent?: number, dim = false): RenderNode {
101
+ export function ripple(el: Els, frame: Frame, key: string, face: Face, alt: string, size: number, percent?: number, dim = false): RenderNode {
96
102
  if (!('Svg' in el)) return ''
97
103
  const { Box, Svg } = el
98
104
  return (
99
105
  <Box key={'ripple-' + key} flexShrink={0}>
100
- <Svg source={rippleSource(face, size, 'dark', percent, dim)} alt={alt} width={size} height={size} />
106
+ <Svg source={lookOf(frame).art.mascot(face, size, percent, dim)} alt={alt} width={size} height={size} />
101
107
  </Box>
102
108
  )
103
109
  }
104
110
 
105
111
  /**
106
- * A control that lights under the pointer: a keyed, positioned Box with a soft rounded light as its
107
- * hover, holding what it shows and, last, its press. `scope` joins a hover group, so pointing at the
108
- * control also shows what it does elsewhere (the band's tip).
112
+ * A control that lights under the pointer: a keyed, positioned Box with the skin's light as its
113
+ * hover, holding what it shows and, last, its press. `scope` joins a hover group, so pointing at
114
+ * the control also shows what it does elsewhere (the band's tip).
109
115
  */
110
- export function lit(el: Els, key: string, cells: number, children: RenderNode[], paddingX = 0.5, scope?: string): RenderNode {
116
+ export function lit(el: Els, frame: Frame, key: string, cells: number, children: RenderNode[], paddingX = 0.5, scope?: string): RenderNode {
111
117
  const { Box } = el
118
+ const look = lookOf(frame).lit
112
119
  return (
113
- <Box key={'hv-' + key} position="relative" flexDirection="row" alignItems="center" columnGap={1} paddingX={paddingX} paddingY={0} width={cells} flexShrink={0} borderStyle="round" borderColor="rgba(0,0,0,0)" hover={{ backgroundColor: HOVER, borderColor: HOVER_EDGE, ...(scope ? { scope } : {}) }}>
120
+ <Box key={'hv-' + key} position="relative" flexDirection="row" alignItems="center" columnGap={1} paddingX={paddingX} paddingY={0} width={cells} flexShrink={0} {...look} hover={{ ...look.hover, ...(scope ? { scope } : {}) }}>
114
121
  {children}
115
122
  </Box>
116
123
  )
117
124
  }
118
125
 
119
126
  /** The drawn light over `cells`, in absolute Boxes of whole cells each. */
120
- function extraHover(el: Els, cells: number): RenderNode[] {
127
+ function extraHover(el: Els, frame: Frame, cells: number): RenderNode[] {
121
128
  const { Box } = el
122
129
  const parts: RenderNode[] = []
123
- for (let i = 0; i < cells; i++) parts.push(<Box key={'extra-' + i} position="absolute" top={0} bottom={0} left={i} width={1}>{pic(el, 'extra-' + i, extraHoverArt(CELL_PX), '', CELL_PX, CONTROL_PX)}</Box>)
130
+ for (let i = 0; i < cells; i++) parts.push(<Box key={'extra-' + i} position="absolute" top={0} bottom={0} left={i} width={1}>{pic(el, 'extra-' + i, lookOf(frame).art.extraHover(CELL_PX), '', CELL_PX, CONTROL_PX)}</Box>)
124
131
  return parts
125
132
  }
126
133
 
127
134
  /**
128
- * A capsule: a rounded Box edged in violet light, the deep (art.ts `seaArt`) painted behind it from
129
- * its top left, and everything it holds in one positioned layer above the sea. `rows` is about how
130
- * tall it draws; the sea is taken taller and wider than that, and the capsule clips it.
135
+ * A capsule: a Box in the skin's edge and colour, the skin's backdrop (the deep's sea; flat has
136
+ * none) painted behind it from its top left, and everything it holds in one positioned layer above
137
+ * that. `rows` is about how tall it draws; the backdrop is taken taller and wider than that, and
138
+ * the capsule clips it.
131
139
  */
132
140
  export function capsule(el: Els, frame: Frame, c: { key: string; sea: Omit<Sea, 'at'> & { at?: Sea['at'] }; rows: number; paddingX?: number; paddingY?: number; gap?: number; children: RenderNode[] }): RenderNode {
133
141
  const { Box } = el
142
+ const look = lookOf(frame)
134
143
  const padX = c.paddingX ?? 1.5
135
144
  const padY = c.paddingY ?? 0.5
136
145
  const width = Math.max(40, frame.columns) * CELL_PX + 48
@@ -138,11 +147,14 @@ export function capsule(el: Els, frame: Frame, c: { key: string; sea: Omit<Sea,
138
147
  const visible = Math.round(c.rows * ROW_PX)
139
148
  const height = visible + 220
140
149
  const at = c.sea.at ?? { x: padX * CELL_PX + 12, y: padY * ROW_PX * 0.5 + 13 }
150
+ const sea = look.art.sea(width, height, { ...c.sea, at, visible })
141
151
  return (
142
- <Box key={c.key} position="relative" flexDirection="column" borderStyle="round" borderColor={EDGE} paddingX={0} paddingY={0} overflow="hidden" width="100%" minWidth={0}>
143
- <Box key={c.key + '-sea'} position="absolute" top={0} left={0}>
144
- {pic(el, c.key + '-sea', seaArt(width, height, { ...c.sea, at, visible }), 'ClaudeRipple', width, height)}
145
- </Box>
152
+ <Box key={c.key} position="relative" flexDirection="column" paddingX={0} paddingY={0} overflow="hidden" width="100%" minWidth={0} {...look.capsule}>
153
+ {sea ? (
154
+ <Box key={c.key + '-sea'} position="absolute" top={0} left={0}>
155
+ {pic(el, c.key + '-sea', sea, 'ClaudeRipple', width, height)}
156
+ </Box>
157
+ ) : null}
146
158
  <Box key={c.key + '-layer'} position="relative" flexDirection="column" rowGap={c.gap ?? 0} paddingX={padX} paddingY={padY} width="100%" minWidth={0}>
147
159
  {lift(el)}
148
160
  {c.children}
@@ -155,9 +167,9 @@ export function capsule(el: Els, frame: Frame, c: { key: string; sea: Omit<Sea,
155
167
  * A drawn button with its label in it: `primary` the one thing to press, `ghost` a quiet one. Its
156
168
  * Box key starts `pill-` or `ghost-`, its press is `key`.
157
169
  */
158
- export function pill(el: Els, key: string, label: string, look: PillLook, onPress: () => void, tone: Tone = 'violet', glyph?: Glyph): RenderNode {
170
+ export function pill(el: Els, frame: Frame, key: string, label: string, look: PillLook, onPress: () => void, tone: Tone = 'violet', glyph?: Glyph): RenderNode {
159
171
  const { Box } = el
160
- const art = pillArt(label, look, tone, glyph)
172
+ const art = lookOf(frame).art.pill(label, look, tone, glyph)
161
173
  return (
162
174
  <Box key={(look === 'primary' ? 'pill-' : 'ghost-') + key} position="relative" width={art.cells} height={rowsOf(CONTROL_PX)} flexShrink={0} alignItems="center" justifyContent="center">
163
175
  {pic(el, key, art.source, label, art.cells * CELL_PX, CONTROL_PX)}
@@ -174,13 +186,14 @@ export const CLOSE_CELLS = cellsOf(CLOSE_PX + 4)
174
186
  * its own light under the pointer: `lit`'s rounded box around it read as a second button
175
187
  * (2026-10-14).
176
188
  */
177
- export function closeButton(el: Els, key: string, label: string, onPress: () => void, scope?: string): RenderNode {
189
+ export function closeButton(el: Els, frame: Frame, key: string, label: string, onPress: () => void, scope?: string): RenderNode {
178
190
  const { Box } = el
191
+ const art = lookOf(frame).art
179
192
  return (
180
193
  <Box key={'hv-' + key} position="relative" flexDirection="row" alignItems="center" justifyContent="center" width={CLOSE_CELLS} flexShrink={0} {...(scope ? { hover: { scope } } : {})}>
181
- {pic(el, key, closeArt(), label, CLOSE_PX, CLOSE_PX)}
194
+ {pic(el, key, art.close(), label, CLOSE_PX, CLOSE_PX)}
182
195
  <Box position="absolute" top={0} bottom={0} left={0} right={0} alignItems="center" justifyContent="center" display="none" hover={{ display: 'flex' }}>
183
- {pic(el, key + '-lit', closeArt(true), label, CLOSE_PX, CLOSE_PX)}
196
+ {pic(el, key + '-lit', art.close(true), label, CLOSE_PX, CLOSE_PX)}
184
197
  </Box>
185
198
  {press(el, key, CLOSE_CELLS * CELL_PX, onPress)}
186
199
  </Box>
@@ -193,23 +206,23 @@ export function toggleCells(label: string): number {
193
206
  }
194
207
 
195
208
  /** Cells the Auto word takes as its own control: its picture's width (the pill's own air drawn in). */
196
- export function wordCells(label: string): number {
197
- return wordArt(label, true).cells
209
+ export function wordCells(frame: Frame, label: string): number {
210
+ return lookOf(frame).art.word(label, true).cells
198
211
  }
199
212
 
200
213
  /**
201
- * The Auto word as the control itself (art.ts `wordArt`): lit violet while Auto is on, a quiet
202
- * ghost while off, pulsing while the judge decides; the whole pressable.
214
+ * The Auto word as the control itself (the skin's `word`): lit while Auto is on, a quiet ghost
215
+ * while off, pulsing while the judge decides; the whole pressable.
203
216
  */
204
- export function word(el: Els, key: string, on: boolean, label: string, onPress: () => void, busy = false, scope?: string): RenderNode {
217
+ export function word(el: Els, frame: Frame, key: string, on: boolean, label: string, onPress: () => void, busy = false, scope?: string): RenderNode {
205
218
  const { Box } = el
206
- const art = wordArt(label, on, busy)
219
+ const art = lookOf(frame).art.word(label, on, busy)
207
220
  return (
208
221
  <Box key={(on ? 'word-on-' : 'ghost-') + key} position="relative" width={art.cells} height={rowsOf(CONTROL_PX)} flexShrink={0} alignItems="center" justifyContent="center">
209
222
  {pic(el, key, art.source, label, art.cells * CELL_PX, CONTROL_PX)}
210
223
  <Box position="absolute" top={0} bottom={0} left={0} right={0} display="none" hover={{ scope }}>
211
224
  {lift(el)}
212
- {extraHover(el, art.cells)}
225
+ {extraHover(el, frame, art.cells)}
213
226
  </Box>
214
227
  {press(el, key, art.cells * CELL_PX, onPress)}
215
228
  </Box>
@@ -217,28 +230,29 @@ export function word(el: Els, key: string, on: boolean, label: string, onPress:
217
230
  }
218
231
 
219
232
  /** A switch and its word (On / Off), the whole pressable; lit while on. */
220
- export function toggle(el: Els, key: string, on: boolean, label: string, onPress: () => void, busy = false, scope?: string): RenderNode {
233
+ export function toggle(el: Els, frame: Frame, key: string, on: boolean, label: string, onPress: () => void, busy = false, scope?: string): RenderNode {
221
234
  const { Text } = el
235
+ const look = lookOf(frame)
222
236
  const cells = toggleCells(label)
223
- return lit(el, key, cells, [
224
- pic(el, key, switchArt(on, busy), label, SWITCH_W, SWITCH_H),
225
- <Text key={key + '-word'} color={on ? INK : SUB} bold={on}>{label}</Text>,
237
+ return lit(el, frame, key, cells, [
238
+ pic(el, key, look.art.switch(on, busy), label, SWITCH_W, SWITCH_H),
239
+ <Text key={key + '-word'} {...(on ? look.ink : look.sub)} bold={on}>{label}</Text>,
226
240
  press(el, key, cells * CELL_PX, onPress),
227
241
  ], 0.5, scope)
228
242
  }
229
243
 
230
244
  /**
231
245
  * A field's well: a native field (Input) on a plate of the app's own page colour, light or dark as
232
- * the app is (art.ts `wellArt`), so the host's own text colour reads in it on either theme. The
246
+ * the app is (the skin's `well`), so the host's own text colour reads in it on either theme. The
233
247
  * field sits in a positioned layer above the plate.
234
248
  */
235
- export function well(el: Els, key: string, cells: number, field: RenderNode, alt: string): RenderNode {
249
+ export function well(el: Els, frame: Frame, key: string, cells: number, field: RenderNode, alt: string): RenderNode {
236
250
  const { Box } = el
237
251
  const w = cells * CELL_PX
238
252
  return (
239
253
  <Box key={'well-' + key} position="relative" flexDirection="row" alignItems="center" width={cells} flexShrink={1} minWidth={16} height={rowsOf(WELL_PX)} paddingX={1} overflow="hidden">
240
254
  <Box position="absolute" top={0} left={0}>
241
- {pic(el, 'well-' + key, wellArt(w, WELL_PX), alt, w, WELL_PX)}
255
+ {pic(el, 'well-' + key, lookOf(frame).art.well(w, WELL_PX), alt, w, WELL_PX)}
242
256
  </Box>
243
257
  <Box key={'well-field-' + key} position="relative" flexGrow={1} flexShrink={1} minWidth={0}>
244
258
  {lift(el)}
@@ -256,10 +270,11 @@ function segmentCells(labels: readonly string[]): number[] {
256
270
  }
257
271
 
258
272
  /**
259
- * A segmented control drawn whole (art.ts), the chosen segment lit, each segment the press of its
260
- * own option (`key-value`). `colors` lights the chosen one in its own colour (the efforts' depths).
273
+ * A segmented control drawn whole (the skin's `segmented`), the chosen segment lit, each segment
274
+ * the press of its own option (`key-value`). `colors` lights the chosen one in its own colour (the
275
+ * efforts' depths) where the skin takes colours.
261
276
  */
262
- export function segmented<T extends string>(el: Els, key: string, options: readonly SegmentOption<T>[], value: T | null, onPick: (value: T) => void, tone: Tone = 'violet', colors?: readonly string[]): RenderNode {
277
+ export function segmented<T extends string>(el: Els, frame: Frame, key: string, options: readonly SegmentOption<T>[], value: T | null, onPick: (value: T) => void, tone: Tone = 'violet', colors?: readonly string[]): RenderNode {
263
278
  const { Box } = el
264
279
  const labels = options.map(o => o.label)
265
280
  const cells = segmentCells(labels)
@@ -269,7 +284,7 @@ export function segmented<T extends string>(el: Els, key: string, options: reado
269
284
  return (
270
285
  <Box key={'seg-' + key} position="relative" flexDirection="row" width={total} height={rowsOf(CONTROL_PX)} flexShrink={0}>
271
286
  <Box position="absolute" top={0} left={0}>
272
- {pic(el, 'seg-' + key, segmentedArt(labels, cells, chosen, tone, colors), alt, total * CELL_PX, CONTROL_PX)}
287
+ {pic(el, 'seg-' + key, lookOf(frame).art.segmented(labels, cells, chosen, tone, colors), alt, total * CELL_PX, CONTROL_PX)}
273
288
  </Box>
274
289
  {options.map((o, i) => (
275
290
  <Box key={'sg-' + key + '-' + o.value} position="relative" width={cells[i]} height={rowsOf(CONTROL_PX)} flexShrink={0}>
@@ -284,14 +299,14 @@ export function segmented<T extends string>(el: Els, key: string, options: reado
284
299
  * A stepped slider: `values` as stops on one drawn track, the chosen one lit, each stop the press of
285
300
  * an equal part of the track (`key-value`).
286
301
  */
287
- export function slider<T extends string | number>(el: Els, key: string, values: readonly T[], value: T, onPick: (value: T) => void, cellsPerStop = 4): RenderNode {
302
+ export function slider<T extends string | number>(el: Els, frame: Frame, key: string, values: readonly T[], value: T, onPick: (value: T) => void, cellsPerStop = 4): RenderNode {
288
303
  const { Box } = el
289
304
  const chosen = Math.max(0, values.indexOf(value))
290
305
  const total = values.length * cellsPerStop
291
306
  return (
292
307
  <Box key={'sl-' + key} position="relative" flexDirection="row" width={total} height={rowsOf(CONTROL_PX)} flexShrink={0} alignItems="center">
293
308
  <Box position="absolute" top={0} bottom={0} left={0} alignItems="center" justifyContent="center">
294
- {pic(el, 'sl-' + key, sliderArt(values.length, cellsPerStop, chosen), String(value), total * CELL_PX, 22)}
309
+ {pic(el, 'sl-' + key, lookOf(frame).art.slider(values.length, cellsPerStop, chosen), String(value), total * CELL_PX, 22)}
295
310
  </Box>
296
311
  {values.map(v => (
297
312
  <Box key={'st-' + key + '-' + v} position="relative" width={cellsPerStop} height={rowsOf(CONTROL_PX)} flexShrink={0}>