clauderipple 0.9.5 → 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.
- package/CHANGELOG.md +67 -0
- package/README.md +31 -0
- package/dist/app/package.json +1 -1
- package/dist/cli/src/hooks/agent-title.js +7 -4
- package/dist/cli/src/index.js +43 -0
- package/dist/cli/src/remote.js +263 -0
- package/dist/cli/src/schtasks.js +48 -40
- package/dist/cli/src/settings.js +2 -1
- package/dist/mod/hooks/i18n.ts +252 -6
- package/dist/mod/hooks/judge.ts +35 -17
- package/dist/mod/hooks/modes.ts +56 -1
- package/dist/mod/hooks/register.tsx +238 -32
- package/dist/mod/hooks/session.ts +98 -1
- package/dist/mod/hooks/settings.js +7 -3
- package/dist/mod/hooks/settings.ts +9 -4
- package/dist/mod/hooks/stats.ts +25 -1
- package/dist/mod/hooks/ui/agents.tsx +858 -79
- package/dist/mod/hooks/ui/alerts.tsx +20 -17
- package/dist/mod/hooks/ui/art.ts +33 -7
- package/dist/mod/hooks/ui/band.tsx +262 -81
- package/dist/mod/hooks/ui/flat.ts +265 -0
- package/dist/mod/hooks/ui/kit.tsx +70 -55
- package/dist/mod/hooks/ui/look.ts +129 -0
- package/dist/mod/hooks/ui/settings.tsx +71 -46
- package/dist/mod/hooks/ui/setup.tsx +15 -13
- package/dist/mod/hooks/ui/theme.ts +3 -0
- package/dist/mod/hooks/ui/tools.ts +124 -0
- package/dist/mod/hooks/ui/view.ts +116 -3
- package/dist/mod/hooks/workers.ts +33 -6
- package/dist/mod/tests/register.test.tsx +155 -11
- package/dist/mod/tests/ui.test.tsx +375 -27
- package/dist/mod/types/index.d.ts +68 -2
- package/dist/remote/package.json +28 -0
- package/dist/remote/src/away.js +85 -0
- package/dist/remote/src/bridge-client.js +295 -0
- package/dist/remote/src/composer-menu.js +11 -0
- package/dist/remote/src/config.js +102 -0
- package/dist/remote/src/desktop.js +1018 -0
- package/dist/remote/src/e2ee.js +126 -0
- package/dist/remote/src/e2ee.ts +126 -0
- package/dist/remote/src/eventlog.js +298 -0
- package/dist/remote/src/expo-push.js +127 -0
- package/dist/remote/src/folders.js +79 -0
- package/dist/remote/src/fsutil.js +39 -0
- package/dist/remote/src/handoff.js +38 -0
- package/dist/remote/src/host-service.js +168 -0
- package/dist/remote/src/images.js +215 -0
- package/dist/remote/src/local-control.js +138 -0
- package/dist/remote/src/main.js +264 -0
- package/dist/remote/src/permissions.js +124 -0
- package/dist/remote/src/power.js +76 -0
- package/dist/remote/src/processes.js +24 -0
- package/dist/remote/src/protocol.js +32 -0
- package/dist/remote/src/protocol.ts +284 -0
- package/dist/remote/src/push-content.js +32 -0
- package/dist/remote/src/push.js +134 -0
- package/dist/remote/src/receipts.js +111 -0
- package/dist/remote/src/relay.js +438 -0
- package/dist/remote/src/router.js +208 -0
- package/dist/remote/src/sdk-launcher.js +18 -0
- package/dist/remote/src/server.js +706 -0
- package/dist/remote/src/service.js +157 -0
- package/dist/remote/src/sessions.js +1800 -0
- package/dist/remote/src/sidebar-storage.js +211 -0
- package/dist/remote/src/sidebar.js +173 -0
- package/dist/remote/src/tool-details.js +205 -0
- package/dist/remote/src/tool-details.ts +153 -0
- package/dist/remote/src/worktree.js +99 -0
- package/dist/remote/web/app.ts +746 -0
- package/dist/remote/web/conn.ts +193 -0
- package/dist/remote/web/demo.ts +134 -0
- package/dist/remote/web/dom.ts +124 -0
- package/dist/remote/web/esc.ts +4 -0
- package/dist/remote/web/i18n.ts +374 -0
- package/dist/remote/web/icon.svg +1 -0
- package/dist/remote/web/index.html +24 -0
- package/dist/remote/web/manifest.webmanifest +10 -0
- package/dist/remote/web/mascot.ts +60 -0
- package/dist/remote/web/md.ts +78 -0
- package/dist/remote/web/notify.ts +55 -0
- package/dist/remote/web/pushstate.ts +27 -0
- package/dist/remote/web/secure.ts +109 -0
- package/dist/remote/web/sidebar.ts +106 -0
- package/dist/remote/web/style.css +311 -0
- package/dist/remote/web/sw.ts +48 -0
- package/dist/remote/web/timeline.ts +224 -0
- package/dist/remote/web/views.ts +323 -0
- package/dist/router/src/admin.js +227 -4
- package/dist/router/src/config.js +30 -0
- package/dist/router/src/index.js +39 -0
- package/dist/router/src/ingress/translate.js +6 -2
- package/dist/router/src/phone.js +82 -0
- package/dist/router/src/providers/anthropic-usage.js +33 -1
- package/dist/router/src/providers/google/index.js +26 -0
- package/dist/router/src/providers/google/prompt-refusal.js +29 -0
- package/dist/router/src/providers/google/usage.js +257 -0
- package/dist/router/src/providers/grok/index.js +18 -0
- package/dist/router/src/providers/grok/usage.js +77 -0
- package/dist/router/src/providers/openai/translate.js +13 -2
- package/dist/router/src/providers/opencode-usage.js +116 -0
- package/dist/router/src/proxy.js +339 -258
- package/dist/router/src/remote-host.js +320 -0
- package/dist/router/src/remote-tunnel.js +320 -0
- package/dist/router/src/requestlog.js +48 -39
- package/dist/router/src/usage-history.js +227 -0
- package/dist/router/src/version.js +1 -1
- package/dist/ui/app.js +436 -59
- package/dist/ui/i18n.js +184 -0
- package/dist/ui/index.html +24 -5
- package/dist/ui/style.css +39 -0
- package/docs/ARCHITECTURE.md +108 -8
- 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
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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,
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
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
|
|
93
|
-
* plain image in a Box of its own that never shrinks. `percent`
|
|
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={
|
|
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
|
|
107
|
-
* hover, holding what it shows and, last, its press. `scope` joins a hover group, so pointing at
|
|
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}
|
|
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,
|
|
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
|
|
129
|
-
* its top left, and everything it holds in one positioned layer above
|
|
130
|
-
* tall it draws; the
|
|
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"
|
|
143
|
-
|
|
144
|
-
{
|
|
145
|
-
|
|
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 =
|
|
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,
|
|
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',
|
|
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
|
|
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 (
|
|
202
|
-
*
|
|
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 =
|
|
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,
|
|
225
|
-
<Text key={key + '-word'}
|
|
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 (
|
|
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,
|
|
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 (
|
|
260
|
-
* own option (`key-value`). `colors` lights the chosen one in its own colour (the
|
|
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,
|
|
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,
|
|
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}>
|