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.
- package/CHANGELOG.md +80 -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/presets.js +3 -0
- package/dist/router/src/providers/anthropic-usage.js +33 -1
- package/dist/router/src/providers/chatgpt/index.js +41 -11
- package/dist/router/src/providers/chatgpt/translate.js +147 -7
- 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/index.js +29 -5
- package/dist/router/src/providers/openai/translate.js +260 -27
- package/dist/router/src/providers/opencode-usage.js +116 -0
- package/dist/router/src/proxy.js +339 -258
- package/dist/router/src/reasoning.js +157 -0
- 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/thinking.js +20 -9
- 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 +164 -12
- package/package.json +7 -1
|
@@ -12,7 +12,8 @@ import { t } from '../i18n'
|
|
|
12
12
|
import type { RenderNode } from 'claude-code'
|
|
13
13
|
|
|
14
14
|
import type { HandoffKind, Settings } from '../../types'
|
|
15
|
-
import { CELL_PX,
|
|
15
|
+
import { CELL_PX, ROW_PX } from './art'
|
|
16
|
+
import { lookOf } from './look'
|
|
16
17
|
import type { Sea, Tone } from './art'
|
|
17
18
|
import { capsule, closeButton, pill, ripple, segmented as drawnSegmented, well } from './kit'
|
|
18
19
|
import type { SegmentOption } from './kit'
|
|
@@ -26,7 +27,7 @@ import { compactNumber, isRich, limitName, resetsLabel } from './view'
|
|
|
26
27
|
|
|
27
28
|
type Choice = { key: string; label: string; hotkey: string; onPress: () => void }
|
|
28
29
|
|
|
29
|
-
type Card = {
|
|
30
|
+
export type Card = {
|
|
30
31
|
key: string
|
|
31
32
|
/** 리플이's face; on the terminal its tone colours the title's mark. */
|
|
32
33
|
face: Face
|
|
@@ -44,7 +45,7 @@ type Card = {
|
|
|
44
45
|
|
|
45
46
|
const JUDGE_NAME = { haiku: 'settings.haiku', jev: 'settings.jev', model: 'alerts.routerModel' } as const
|
|
46
47
|
|
|
47
|
-
function cardOf(frame: Frame, alert: AlertView, a: AlertActions): Card {
|
|
48
|
+
export function cardOf(frame: Frame, alert: AlertView, a: AlertActions): Card {
|
|
48
49
|
const L = frame.lang
|
|
49
50
|
const notNow: Choice = { key: 'dismiss', label: t(L, 'alerts.notNow'), hotkey: 'n', onPress: a.dismiss }
|
|
50
51
|
const cancel: Choice = { key: 'dismiss', label: t(L, 'alerts.cancel'), hotkey: 'x', onPress: a.dismiss }
|
|
@@ -249,6 +250,7 @@ const CARD_FACE = 34
|
|
|
249
250
|
|
|
250
251
|
function drawnExtras(el: Els, frame: Frame, alert: AlertView, a: AlertActions, tone: Tone): RenderNode[] {
|
|
251
252
|
const { Box, Text } = el
|
|
253
|
+
const k = lookOf(frame)
|
|
252
254
|
if (alert.kind === 'compactNote' && 'Input' in el) {
|
|
253
255
|
const Field = el.Input
|
|
254
256
|
const name = t(frame.lang, 'alerts.keep').trim()
|
|
@@ -257,8 +259,8 @@ function drawnExtras(el: Els, frame: Frame, alert: AlertView, a: AlertActions, t
|
|
|
257
259
|
const cells = Math.max(20, frame.columns - Math.ceil(CARD_FACE / CELL_PX) - Math.ceil(chOf(name)) - 10)
|
|
258
260
|
return [
|
|
259
261
|
<Box key="note-row" flexDirection="row" alignItems="center" columnGap={2} width="100%" minWidth={0}>
|
|
260
|
-
<Text
|
|
261
|
-
{well(el, 'note', cells, (
|
|
262
|
+
<Text {...k.sub}>{name}</Text>
|
|
263
|
+
{well(el, frame, 'note', cells, (
|
|
262
264
|
<Field
|
|
263
265
|
key="note"
|
|
264
266
|
placeholder={placeholder}
|
|
@@ -287,29 +289,30 @@ function drawnExtras(el: Els, frame: Frame, alert: AlertView, a: AlertActions, t
|
|
|
287
289
|
const line = (key: string, name: string, control: RenderNode) => (
|
|
288
290
|
<Box key={key} flexDirection="row" alignItems="center" columnGap={2}>
|
|
289
291
|
<Box width={nameCells} flexShrink={0}>
|
|
290
|
-
<Text
|
|
292
|
+
<Text {...k.mute}>{name}</Text>
|
|
291
293
|
</Box>
|
|
292
294
|
{control}
|
|
293
295
|
</Box>
|
|
294
296
|
)
|
|
295
297
|
return [
|
|
296
298
|
<Box key="choices" flexDirection="column" rowGap={1}>
|
|
297
|
-
{line('kind', names[0]!, drawnSegmented(el, 'kind', kinds, alert.handoffKind,
|
|
298
|
-
{line('after', names[1]!, drawnSegmented(el, 'after', afters, alert.after,
|
|
299
|
+
{line('kind', names[0]!, drawnSegmented(el, frame, 'kind', kinds, alert.handoffKind, kind => a.setHandoffKind(kind), tone))}
|
|
300
|
+
{line('after', names[1]!, drawnSegmented(el, frame, 'after', afters, alert.after, after => a.setHandoffAfter(after), tone))}
|
|
299
301
|
<Box paddingLeft={nameCells + 2}>
|
|
300
|
-
<Text
|
|
302
|
+
<Text {...k.mute} wrap="wrap">{afterHint(frame, alert.after)}</Text>
|
|
301
303
|
</Box>
|
|
302
304
|
</Box>,
|
|
303
305
|
]
|
|
304
306
|
}
|
|
305
307
|
|
|
306
308
|
/** What the chosen "then" does, in a line: the names alone were read the wrong way (2026-10-13). */
|
|
307
|
-
function afterHint(frame: Frame, after: Settings['handoffAfter']): string {
|
|
309
|
+
export function afterHint(frame: Frame, after: Settings['handoffAfter']): string {
|
|
308
310
|
return t(frame.lang, after === 'continue' ? 'alerts.continueHint' : after === 'confirm' ? 'alerts.confirmHint' : 'alerts.copyHint')
|
|
309
311
|
}
|
|
310
312
|
|
|
311
313
|
function drawnAlert(el: Els, frame: Frame, alert: AlertView, a: AlertActions): RenderNode {
|
|
312
314
|
const { Box, Text } = el
|
|
315
|
+
const k = lookOf(frame)
|
|
313
316
|
const card = cardOf(frame, alert, a)
|
|
314
317
|
const sea = seaOf(card, alert)
|
|
315
318
|
const tone = sea.tone
|
|
@@ -319,7 +322,7 @@ function drawnAlert(el: Els, frame: Frame, alert: AlertView, a: AlertActions): R
|
|
|
319
322
|
const main = card.main && !fieldSubmits ? card.main : undefined
|
|
320
323
|
const more = drawnExtras(el, frame, alert, a, tone)
|
|
321
324
|
// The actions' cells, as drawn (each pill its own whole cells, a cell between, the way out last).
|
|
322
|
-
const actionCells = quiet.reduce((n, c) => n +
|
|
325
|
+
const actionCells = quiet.reduce((n, c) => n + k.art.pill(c.label, 'ghost', tone).cells + 1, 0) + (main ? k.art.pill(main.label, 'primary', tone).cells + 1 : 0) + (card.close ? 4 : 0)
|
|
323
326
|
const faceCells = Math.ceil(CARD_FACE / CELL_PX) + 2
|
|
324
327
|
const room = frame.columns - 4
|
|
325
328
|
const oneRow = !more.length && faceCells + chOf(card.title) * 1.12 + 2 + chOf(card.body) + 2 + actionCells <= room
|
|
@@ -330,14 +333,14 @@ function drawnAlert(el: Els, frame: Frame, alert: AlertView, a: AlertActions): R
|
|
|
330
333
|
// where a way out is looked for.
|
|
331
334
|
const buttons = (
|
|
332
335
|
<Box key="actions" flexDirection="row" alignItems="center" alignSelf={more.length ? 'flex-start' : 'center'} columnGap={1} flexShrink={0} flexWrap="wrap" rowGap={1}>
|
|
333
|
-
{quiet.map(c => pill(el, c.key, c.label, 'ghost', c.onPress, tone))}
|
|
334
|
-
{main ? pill(el, main.key, main.label, 'primary', main.onPress, tone) : null}
|
|
335
|
-
{card.close ? closeButton(el, card.close.key, card.close.label, card.close.onPress) : null}
|
|
336
|
+
{quiet.map(c => pill(el, frame, c.key, c.label, 'ghost', c.onPress, tone))}
|
|
337
|
+
{main ? pill(el, frame, main.key, main.label, 'primary', main.onPress, tone) : null}
|
|
338
|
+
{card.close ? closeButton(el, frame, card.close.key, card.close.label, card.close.onPress) : null}
|
|
336
339
|
</Box>
|
|
337
340
|
)
|
|
338
|
-
const face = ripple(el, card.key, card.face, card.title, CARD_FACE, card.percent)
|
|
339
|
-
const title = <Text key="title" bold color={
|
|
340
|
-
const body = <Text key="body"
|
|
341
|
+
const face = ripple(el, frame, card.key, card.face, card.title, CARD_FACE, card.percent)
|
|
342
|
+
const title = <Text key="title" bold color={k.tone(tone)} wrap="wrap">{card.title}</Text>
|
|
343
|
+
const body = <Text key="body" {...k.sub} wrap="wrap">{card.body}</Text>
|
|
341
344
|
const top = oneRow ? (
|
|
342
345
|
<Box key="top" flexDirection="row" alignItems="center" columnGap={2} width="100%" minWidth={0}>
|
|
343
346
|
{face}
|
package/dist/mod/hooks/ui/art.ts
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
// - The desktop sets text at 13 px with 19 px lines; a cell is 1ch, 7.917 px. Controls are sized in
|
|
14
14
|
// whole cells so a picture and the presses laid over it share one grid.
|
|
15
15
|
|
|
16
|
-
import {
|
|
16
|
+
import type { Skin } from '../../types/index.d.ts'
|
|
17
|
+
import { chOf } from './theme.ts'
|
|
17
18
|
|
|
18
19
|
/** One desktop cell (1ch of the host's 13 px sans), px. */
|
|
19
20
|
export const CELL_PX = 7.917
|
|
@@ -58,7 +59,7 @@ export const DEPTH = ['#67e8f9', '#7dd3fc', '#a5b4fc', '#a78bfa', '#c084fc'] as
|
|
|
58
59
|
|
|
59
60
|
const FONT = "-apple-system,BlinkMacSystemFont,'SF Pro Text','Apple SD Gothic Neo','Segoe UI',system-ui,sans-serif"
|
|
60
61
|
|
|
61
|
-
const r = (x: number): string => String(Math.round(x * 100) / 100)
|
|
62
|
+
export const r = (x: number): string => String(Math.round(x * 100) / 100)
|
|
62
63
|
|
|
63
64
|
/** One SVG document, `width` × `height` px; nothing in it moves when less motion is asked for. */
|
|
64
65
|
export function svg(width: number, height: number, body: string, css = '', extra = ''): string {
|
|
@@ -66,7 +67,7 @@ export function svg(width: number, height: number, body: string, css = '', extra
|
|
|
66
67
|
return '<svg xmlns="http://www.w3.org/2000/svg" width="' + r(width) + '" height="' + r(height) + '" viewBox="0 0 ' + r(width) + ' ' + r(height) + '"' + extra + '><style>' + base + css + '</style>' + body + '</svg>'
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
function esc(text: string): string {
|
|
70
|
+
export function esc(text: string): string {
|
|
70
71
|
return text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
71
72
|
}
|
|
72
73
|
|
|
@@ -320,7 +321,7 @@ export type PillLook = 'primary' | 'ghost'
|
|
|
320
321
|
/** Glyphs a button may lead with, drawn on a 12 px square. */
|
|
321
322
|
export type Glyph = 'open' | 'close' | 'gear' | 'back' | 'next' | 'copy' | 'save' | 'compact' | 'handoff'
|
|
322
323
|
|
|
323
|
-
const GLYPH: Record<Glyph, string> = {
|
|
324
|
+
export const GLYPH: Record<Glyph, string> = {
|
|
324
325
|
open: '<path d="M7 2.2h2.8V5M9.8 2.2L5.6 6.4M8.8 7.4v2.4H2.2V3.2h2.4"/>',
|
|
325
326
|
close: '<path d="M3 3l6 6M9 3L3 9"/>',
|
|
326
327
|
gear: '<circle cx="6" cy="6" r="1.7"/><path d="M6 1.3v1.3M6 9.4v1.3M1.3 6h1.3M9.4 6h1.3M2.7 2.7l.9.9M8.4 8.4l.9.9M2.7 9.3l.9-.9M8.4 3.6l.9-.9"/><circle cx="6" cy="6" r="3.5"/>',
|
|
@@ -555,14 +556,30 @@ export function gaugeArt(kind: string): string {
|
|
|
555
556
|
return svg(16, 16, arc + '<path d="' + needle + '" fill="none" stroke="' + stroke + '" stroke-width="1.4" stroke-linecap="round"/><circle cx="8" cy="11.4" r="1.2" fill="' + stroke + '"/>')
|
|
556
557
|
}
|
|
557
558
|
|
|
559
|
+
/** A limit window's share as a small horizontal bar, 36 × 6 px with round ends: the track white at
|
|
560
|
+
* low strength, the fill the ring's own teal-to-violet while all is well, amber, then red. Whole
|
|
561
|
+
* percents only, like the ring: the same figure is the same picture. */
|
|
562
|
+
export function barArt(percent: number, tone: Tone | 'ink'): string {
|
|
563
|
+
const W = 36
|
|
564
|
+
const H = 6
|
|
565
|
+
// Rounded to the integer: neighbouring readings draw byte-identical pictures.
|
|
566
|
+
const share = Math.max(0, Math.min(1, Math.round(percent) / 100))
|
|
567
|
+
const fill = tone === 'ink' ? 'url(#g)' : TONE[tone].hi
|
|
568
|
+
return svg(W, H,
|
|
569
|
+
'<defs><linearGradient id="g" x1="0" x2="1" y1="0" y2="0"><stop offset="0" stop-color="#67e8f9"/><stop offset="1" stop-color="#a78bfa"/></linearGradient>' +
|
|
570
|
+
'<clipPath id="c"><rect x="0" y="0" width="' + W + '" height="' + H + '" rx="3"/></clipPath></defs>' +
|
|
571
|
+
'<rect x="0" y="0" width="' + W + '" height="' + H + '" rx="3" fill="#fff" fill-opacity=".14"/>' +
|
|
572
|
+
'<g clip-path="url(#c)"><rect x="0" y="0" width="' + r(W * share) + '" height="' + H + '" fill="' + fill + '"/></g>')
|
|
573
|
+
}
|
|
574
|
+
|
|
558
575
|
export const ICON_PX = 16
|
|
559
576
|
|
|
560
577
|
/** Line icons of the band's controls, 16 px. */
|
|
561
578
|
export type Icon = 'flame' | 'gear' | 'agents' | 'log' | 'compact' | 'clear'
|
|
562
579
|
|
|
563
|
-
const FLAME = 'M8.1 14.3c2.8 0 4.5-1.9 4.5-4.4 0-2.8-2.4-4.2-2.8-7.2-1.7 1.1-2.6 2.8-2.5 4.3-1-.4-1.5-1.4-1.6-2.3-1.4 1.3-2 2.9-2 5.2 0 2.5 1.6 4.4 4.4 4.4z'
|
|
580
|
+
export const FLAME = 'M8.1 14.3c2.8 0 4.5-1.9 4.5-4.4 0-2.8-2.4-4.2-2.8-7.2-1.7 1.1-2.6 2.8-2.5 4.3-1-.4-1.5-1.4-1.6-2.3-1.4 1.3-2 2.9-2 5.2 0 2.5 1.6 4.4 4.4 4.4z'
|
|
564
581
|
|
|
565
|
-
const ICON: Record<Icon, string> = {
|
|
582
|
+
export const ICON: Record<Icon, string> = {
|
|
566
583
|
flame: '<path d="' + FLAME + '"/>',
|
|
567
584
|
gear: '<circle cx="8" cy="8" r="2.3"/><path d="M8 1.7v1.8M8 12.5v1.8M1.7 8h1.8M12.5 8h1.8M3.5 3.5l1.3 1.3M11.2 11.2l1.3 1.3M3.5 12.5l1.3-1.3M11.2 4.8l1.3-1.3"/><circle cx="8" cy="8" r="4.8"/>',
|
|
568
585
|
agents: '<circle cx="4.8" cy="5" r="2.1"/><circle cx="11.4" cy="5" r="2.1"/><path d="M1.6 13.4c.4-2.4 1.7-3.6 3.2-3.6s2.8 1.2 3.2 3.6M8.2 13.4c.4-2.4 1.7-3.6 3.2-3.6s2.8 1.2 3.2 3.6"/>',
|
|
@@ -617,11 +634,12 @@ export type TileArt =
|
|
|
617
634
|
| { kind: 'judge'; judge: 'haiku' | 'jev' | 'model' }
|
|
618
635
|
| { kind: 'mode'; on: boolean }
|
|
619
636
|
| { kind: 'warm'; on: boolean }
|
|
637
|
+
| { kind: 'skin'; skin: Skin }
|
|
620
638
|
|
|
621
639
|
/**
|
|
622
640
|
* A tile's picture, 44 px: a rounded square of its own colour with the setting drawn as it stands
|
|
623
641
|
* (rising bars sounding by themselves for automatic effort, the judge's letter, a stack of worker
|
|
624
|
-
* cards, a flame or a snowflake for keep-warm), alive while it is on.
|
|
642
|
+
* cards, a flame or a snowflake for keep-warm, a small panel for the skin), alive while it is on.
|
|
625
643
|
*/
|
|
626
644
|
export function tileArt(art: TileArt): string {
|
|
627
645
|
const s = TILE_PX
|
|
@@ -662,6 +680,14 @@ export function tileArt(art: TileArt): string {
|
|
|
662
680
|
return svg(s, s, square('#4a3410', '#24180a', '#fbbf24') + card(28, 0.18) + card(21.5, 0.32) + '<g class="fl">' + card(15, 0.95) + '<circle cx="15.5" cy="19.5" r="1.4" fill="#3a2604"/><rect x="19" y="18.6" width="10" height="1.8" rx=".9" fill="#3a2604" fill-opacity=".7"/></g>',
|
|
663
681
|
'.fl{animation:fl 4.4s ease-in-out infinite}@keyframes fl{0%,100%{transform:none}50%{transform:translateY(-1.2px)}}')
|
|
664
682
|
}
|
|
683
|
+
if (art.kind === 'skin') {
|
|
684
|
+
// A small panel in lavender: its head's dot, a title line, two lines of words.
|
|
685
|
+
const ink = '#c4b5fd'
|
|
686
|
+
return svg(s, s, square('#33227a', '#191146', '#a78bfa') +
|
|
687
|
+
'<rect x="10.5" y="12.5" width="23" height="19" rx="3.5" fill="' + ink + '" fill-opacity=".12" stroke="' + ink + '" stroke-width="1.6"/>' +
|
|
688
|
+
'<circle cx="16" cy="18.5" r="2" fill="' + ink + '"/>' +
|
|
689
|
+
'<path d="M20.5 18.5h8M15 24h12M15 27.5h8" fill="none" stroke="' + ink + '" stroke-width="1.6" stroke-linecap="round"/>')
|
|
690
|
+
}
|
|
665
691
|
if (art.on) {
|
|
666
692
|
const 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'
|
|
667
693
|
return svg(s, s,
|