@changfenhuang/dsh-genui 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +367 -0
- package/LICENSE +21 -0
- package/README.md +261 -0
- package/README.zh-CN.md +261 -0
- package/SKILL.md +115 -0
- package/cordis.patch.yml +9 -0
- package/demo-prompts.md +118 -0
- package/lib/assets/echarts.js +64 -0
- package/lib/assets/mermaid.js +3591 -0
- package/lib/assets/three.js +3844 -0
- package/lib/client.js +5 -0
- package/lib/index.js +1895 -0
- package/lib/invariant.js +19 -0
- package/lib/types/client/EChartNode.d.ts +4 -0
- package/lib/types/client/ErrorBoundary.d.ts +30 -0
- package/lib/types/client/GenuiBlock.d.ts +7 -0
- package/lib/types/client/PlotBlock.d.ts +44 -0
- package/lib/types/client/action-context.d.ts +7 -0
- package/lib/types/client/asset-echarts.d.ts +15 -0
- package/lib/types/client/asset-loader.d.ts +31 -0
- package/lib/types/client/asset-mermaid.d.ts +1 -0
- package/lib/types/client/asset-three.d.ts +1 -0
- package/lib/types/client/blocks/advanced.d.ts +104 -0
- package/lib/types/client/blocks/basic.d.ts +27 -0
- package/lib/types/client/blocks/charts.d.ts +38 -0
- package/lib/types/client/blocks/diagram/geometry.d.ts +73 -0
- package/lib/types/client/blocks/diagram/index.d.ts +5 -0
- package/lib/types/client/blocks/diagram/layout.d.ts +41 -0
- package/lib/types/client/blocks/diagram/theme.d.ts +35 -0
- package/lib/types/client/blocks/forms.d.ts +83 -0
- package/lib/types/client/blocks/render-node.d.ts +10 -0
- package/lib/types/client/blocks/state.d.ts +58 -0
- package/lib/types/client/dom-fence.d.ts +14 -0
- package/lib/types/client/echarts-lazy.d.ts +17 -0
- package/lib/types/client/fence-render.d.ts +58 -0
- package/lib/types/client/gallery.d.ts +9 -0
- package/lib/types/client/guard.d.ts +126 -0
- package/lib/types/client/index.d.ts +36 -0
- package/lib/types/client/interaction-store.d.ts +43 -0
- package/lib/types/client/mermaid-core.d.ts +8 -0
- package/lib/types/client/mermaid-lazy.d.ts +9 -0
- package/lib/types/client/mermaid-safe.d.ts +52 -0
- package/lib/types/client/panel-command.d.ts +29 -0
- package/lib/types/client/panel-store.d.ts +94 -0
- package/lib/types/client/panel.d.ts +15 -0
- package/lib/types/client/parse-partial.d.ts +37 -0
- package/lib/types/client/safe-math.d.ts +43 -0
- package/lib/types/client/scene3d-core.d.ts +21 -0
- package/lib/types/client/scene3d-lazy.d.ts +8 -0
- package/lib/types/client/spec.d.ts +610 -0
- package/lib/types/client/toolview.d.ts +7 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/plugin/index.d.ts +25 -0
- package/lib/types/plugin/invariant.d.ts +15 -0
- package/lib/types/plugin/tool.d.ts +27 -0
- package/lib/types/shared/fence-repair.d.ts +67 -0
- package/package.json +121 -0
- package/src/client/EChartNode.tsx +242 -0
- package/src/client/ErrorBoundary.tsx +66 -0
- package/src/client/GenuiBlock.module.css +1263 -0
- package/src/client/GenuiBlock.tsx +166 -0
- package/src/client/PlotBlock.module.css +155 -0
- package/src/client/PlotBlock.tsx +431 -0
- package/src/client/action-context.ts +32 -0
- package/src/client/asset-echarts.ts +25 -0
- package/src/client/asset-loader.ts +79 -0
- package/src/client/asset-mermaid.ts +13 -0
- package/src/client/asset-three.ts +12 -0
- package/src/client/blocks/advanced.tsx +435 -0
- package/src/client/blocks/basic.tsx +116 -0
- package/src/client/blocks/charts.tsx +319 -0
- package/src/client/blocks/diagram/geometry.ts +151 -0
- package/src/client/blocks/diagram/index.tsx +284 -0
- package/src/client/blocks/diagram/layout.ts +199 -0
- package/src/client/blocks/diagram/theme.ts +108 -0
- package/src/client/blocks/forms.tsx +523 -0
- package/src/client/blocks/render-node.tsx +260 -0
- package/src/client/blocks/state.ts +61 -0
- package/src/client/dom-fence.tsx +641 -0
- package/src/client/echarts-lazy.ts +35 -0
- package/src/client/fence-render.tsx +230 -0
- package/src/client/gallery.ts +128 -0
- package/src/client/guard.ts +1450 -0
- package/src/client/index.tsx +195 -0
- package/src/client/interaction-store.ts +122 -0
- package/src/client/mermaid-core.ts +121 -0
- package/src/client/mermaid-lazy.ts +31 -0
- package/src/client/mermaid-safe.ts +122 -0
- package/src/client/panel-command.ts +119 -0
- package/src/client/panel-store.ts +471 -0
- package/src/client/panel.tsx +185 -0
- package/src/client/parse-partial.ts +162 -0
- package/src/client/safe-math.ts +270 -0
- package/src/client/scene3d-core.ts +192 -0
- package/src/client/scene3d-lazy.ts +26 -0
- package/src/client/spec.ts +767 -0
- package/src/client/toolview.tsx +66 -0
- package/src/css-modules.d.ts +5 -0
- package/src/index.ts +12 -0
- package/src/plugin/index.ts +173 -0
- package/src/plugin/invariant.ts +30 -0
- package/src/plugin/node-shims.d.ts +31 -0
- package/src/plugin/tool.ts +330 -0
- package/src/shared/fence-repair.ts +244 -0
- package/tsconfig.json +35 -0
- package/tsdown.config.ts +193 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenuiBlock: renders a declarative GenUI spec (from a ```dsh-ui fence in an
|
|
3
|
+
* assistant reply) as real interactive components inline in the conversation.
|
|
4
|
+
* The component tree is white-listed and mapped to DOM directly — no raw HTML.
|
|
5
|
+
* The block shell holds the shared interaction state (answers registry,
|
|
6
|
+
* durable localStorage persistence, action debounce); the per-family
|
|
7
|
+
* components live in src/client/blocks/*.
|
|
8
|
+
*/
|
|
9
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
10
|
+
import { useGenuiAction } from './action-context.ts'
|
|
11
|
+
import css from './GenuiBlock.module.css'
|
|
12
|
+
import { loadBlockState, saveBlockState } from './interaction-store.ts'
|
|
13
|
+
import { renderNode } from './blocks/render-node.tsx'
|
|
14
|
+
import type { AnswersState, GenuiBlockProps, QuestionMeta } from './blocks/state.ts'
|
|
15
|
+
import type { GenuiSpec } from './spec.ts'
|
|
16
|
+
|
|
17
|
+
export const GENUI_ACTION_DEBOUNCE_MS = 300
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Wrap the harness action callback with the per-action trailing debounce.
|
|
21
|
+
* Absent provider = v1 behavior (components are display-only, callback
|
|
22
|
+
* stays undefined). Pending timers are cleared on unmount so a click that
|
|
23
|
+
* never fired does not leak into the next mount.
|
|
24
|
+
*/
|
|
25
|
+
function useDebouncedAction(onAction: GenuiBlockProps['onAction'] | undefined): GenuiBlockProps['onAction'] {
|
|
26
|
+
const pending = useRef<Map<string, ReturnType<typeof setTimeout>> | null>(null)
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
return () => {
|
|
29
|
+
const timers = pending.current
|
|
30
|
+
if (timers === null) return
|
|
31
|
+
for (const timer of timers.values()) clearTimeout(timer)
|
|
32
|
+
timers.clear()
|
|
33
|
+
}
|
|
34
|
+
}, [])
|
|
35
|
+
return useMemo(() => {
|
|
36
|
+
if (onAction === undefined) return undefined
|
|
37
|
+
const timers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
38
|
+
pending.current = timers
|
|
39
|
+
return (action: string, payload: Record<string, unknown>): void => {
|
|
40
|
+
const existing = timers.get(action)
|
|
41
|
+
if (existing !== undefined) clearTimeout(existing)
|
|
42
|
+
timers.set(action, setTimeout(() => {
|
|
43
|
+
timers.delete(action)
|
|
44
|
+
onAction(action, payload)
|
|
45
|
+
}, GENUI_ACTION_DEBOUNCE_MS))
|
|
46
|
+
}
|
|
47
|
+
}, [onAction])
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Structural spec equality for the memo comparator: the fence path re-parses
|
|
52
|
+
* the body on every streaming chunk and produces a FRESH object even when the
|
|
53
|
+
* repaired content is unchanged (a chunk that closed no new component). The
|
|
54
|
+
* default shallow memo would then re-render the whole tree per chunk — up to
|
|
55
|
+
* ~200 full-tree renders for a max-size fence. Stringify equality makes the
|
|
56
|
+
* memo skip renders whose content did not actually change; the cost is one
|
|
57
|
+
* JSON.stringify per chunk (≤200 nodes, negligible next to a React tree
|
|
58
|
+
* reconciliation). `stateKey` already embeds the content fingerprint, so when
|
|
59
|
+
* both keys are equal and non-undefined the specs necessarily stringify
|
|
60
|
+
* equal — the stringify branch matters for the streaming path (stateKey
|
|
61
|
+
* undefined).
|
|
62
|
+
*/
|
|
63
|
+
function specEquivalent(a: GenuiSpec, b: GenuiSpec): boolean {
|
|
64
|
+
if (a === b) return true
|
|
65
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Render a GenUI spec as an inline block. Falls back to nothing when the spec
|
|
70
|
+
* carries no items (the fence renderer already refused non-specs before us).
|
|
71
|
+
*/
|
|
72
|
+
export const GenuiBlock = memo(function GenuiBlock({ spec, stateKey }: GenuiBlockProps) {
|
|
73
|
+
const gap = spec.gap ?? 16
|
|
74
|
+
const onAction = useDebouncedAction(useGenuiAction())
|
|
75
|
+
// v2.5/v2.6 answers registry: grouped radios record selections + question
|
|
76
|
+
// metadata here; `submit` nodes grade locally (locked until 重新作答) or
|
|
77
|
+
// collect into one action. Block-local state survives re-renders (streaming
|
|
78
|
+
// settle, panel updates) — selections persist while the block is mounted.
|
|
79
|
+
// v2.7 durability: with a stateKey the state ALSO survives refresh/reopen —
|
|
80
|
+
// loaded once at mount (seed for re-renders of the same content) and saved
|
|
81
|
+
// on every change.
|
|
82
|
+
const [persisted] = useState(() => (stateKey === undefined ? null : loadBlockState(stateKey)))
|
|
83
|
+
const [answers, setAnswers] = useState<Record<string, string>>(persisted?.answers ?? {})
|
|
84
|
+
const [fields, setFields] = useState<Record<string, string>>(persisted?.fields ?? {})
|
|
85
|
+
const [meta, setMeta] = useState<Record<string, QuestionMeta>>({})
|
|
86
|
+
const [locked, setLocked] = useState(persisted?.locked === true)
|
|
87
|
+
const [round, setRound] = useState(0)
|
|
88
|
+
// Secret (password) field ids: their values never persist and never join
|
|
89
|
+
// submit collection — the input itself stays masked and its own action
|
|
90
|
+
// still delivers the value on explicit user submit.
|
|
91
|
+
const [secretFields, setSecretFields] = useState<ReadonlySet<string>>(new Set())
|
|
92
|
+
const setAnswer = useCallback((group: string, choice: string) => {
|
|
93
|
+
setAnswers(prev => (prev[group] === choice ? prev : { ...prev, [group]: choice }))
|
|
94
|
+
}, [])
|
|
95
|
+
const setField = useCallback((id: string, value: string) => {
|
|
96
|
+
// Field invariant: a blank (trim-empty) value leaves the shared registry.
|
|
97
|
+
setFields(prev => {
|
|
98
|
+
if (value.trim() === '') {
|
|
99
|
+
if (!(id in prev)) return prev
|
|
100
|
+
const next = { ...prev }
|
|
101
|
+
delete next[id]
|
|
102
|
+
return next
|
|
103
|
+
}
|
|
104
|
+
return prev[id] === value ? prev : { ...prev, [id]: value }
|
|
105
|
+
})
|
|
106
|
+
}, [])
|
|
107
|
+
const registerSecretField = useCallback((id: string) => {
|
|
108
|
+
setSecretFields(prev => (prev.has(id) ? prev : new Set(prev).add(id)))
|
|
109
|
+
}, [])
|
|
110
|
+
const registerMeta = useCallback((group: string, m: QuestionMeta) => {
|
|
111
|
+
setMeta(prev => {
|
|
112
|
+
const existing = prev[group]
|
|
113
|
+
if (existing !== undefined && existing.label === m.label && existing.answer === m.answer
|
|
114
|
+
&& existing.explanation === m.explanation) return prev
|
|
115
|
+
return { ...prev, [group]: m }
|
|
116
|
+
})
|
|
117
|
+
}, [])
|
|
118
|
+
const clear = useCallback(() => {
|
|
119
|
+
setAnswers({})
|
|
120
|
+
setLocked(false)
|
|
121
|
+
setRound(r => r + 1) // radios remount (key carries the round) with clean selections
|
|
122
|
+
}, [])
|
|
123
|
+
const answersState = useMemo<AnswersState>(
|
|
124
|
+
() => ({
|
|
125
|
+
answers, fields, secretFields, meta, locked, round,
|
|
126
|
+
setAnswer, setField, registerSecretField, registerMeta, clear, setLocked,
|
|
127
|
+
}),
|
|
128
|
+
[answers, fields, secretFields, meta, locked, round, setAnswer, setField, registerSecretField, registerMeta, clear],
|
|
129
|
+
)
|
|
130
|
+
// Durable save (debounced 300ms — typing in a field fires per keystroke).
|
|
131
|
+
// Secret field values are stripped before writing: passwords never persist.
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (stateKey === undefined) return
|
|
134
|
+
const timer = setTimeout(() => {
|
|
135
|
+
const safeFields = Object.fromEntries(
|
|
136
|
+
Object.entries(fields).filter(([id]) => !secretFields.has(id)),
|
|
137
|
+
)
|
|
138
|
+
saveBlockState(stateKey, {
|
|
139
|
+
answers,
|
|
140
|
+
locked,
|
|
141
|
+
...(Object.keys(safeFields).length > 0 ? { fields: safeFields } : {}),
|
|
142
|
+
})
|
|
143
|
+
}, 300)
|
|
144
|
+
return () => clearTimeout(timer)
|
|
145
|
+
}, [stateKey, answers, locked, fields, secretFields])
|
|
146
|
+
return (
|
|
147
|
+
<div className={css.block} data-genui>
|
|
148
|
+
{spec.title !== undefined && <div className={css.banner}>{spec.title}</div>}
|
|
149
|
+
<div className={css.col} style={{ gap: `${gap}px` }}>
|
|
150
|
+
{spec.items.map((c, i) => (
|
|
151
|
+
// Staggered reveal: each root item fades/slides in after its
|
|
152
|
+
// predecessors, so the block assembles piece by piece instead of
|
|
153
|
+
// popping in as one slab. Delay capped so long specs still settle
|
|
154
|
+
// quickly; prefers-reduced-motion disables it (see CSS).
|
|
155
|
+
<div
|
|
156
|
+
key={i}
|
|
157
|
+
className={css.reveal}
|
|
158
|
+
style={{ animationDelay: `${Math.min(i * 90, 720)}ms` }}
|
|
159
|
+
>
|
|
160
|
+
{renderNode(c, i, onAction, 0, answersState)}
|
|
161
|
+
</div>
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
)
|
|
166
|
+
}, (prev, next) => prev.stateKey === next.stateKey && specEquivalent(prev.spec, next.spec))
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/* PlotBlock: SVG function plot inside a genui block. Design system v2
|
|
2
|
+
(0.5.0): same token vocabulary as GenuiBlock.module.css — this root
|
|
3
|
+
carries its own copy because the plot can also mount standalone. */
|
|
4
|
+
|
|
5
|
+
.block {
|
|
6
|
+
--dsl-g-radius-surface: 12px;
|
|
7
|
+
--dsl-g-radius-control: 8px;
|
|
8
|
+
|
|
9
|
+
--dsl-g-font-title: 13px;
|
|
10
|
+
--dsl-g-font-meta: 12px;
|
|
11
|
+
--dsl-g-font-data: 11px;
|
|
12
|
+
|
|
13
|
+
--dsl-g-gap-md: 12px;
|
|
14
|
+
--dsl-g-gap-sm: 8px;
|
|
15
|
+
--dsl-g-gap-xs: 4px;
|
|
16
|
+
|
|
17
|
+
--dsl-g-border: var(--dsw-alias-border-l1, var(--dsw-alias-border-l2, rgba(255, 255, 255, 0.12)));
|
|
18
|
+
--dsl-g-accent: var(--dsw-alias-state-business-primary, #4f8ef7);
|
|
19
|
+
--dsl-g-accent-soft: color-mix(in srgb, var(--dsl-g-accent) var(--dsl-g-tint), transparent);
|
|
20
|
+
--dsl-g-tint: 8%;
|
|
21
|
+
--dsl-g-tint-strong: 14%;
|
|
22
|
+
--dsl-g-font-mono: var(--ds-font-family-code, ui-monospace, 'SF Mono', monospace);
|
|
23
|
+
|
|
24
|
+
padding: 14px 16px;
|
|
25
|
+
background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-markdown-code-block));
|
|
26
|
+
border: 1px solid var(--dsl-g-border);
|
|
27
|
+
border-radius: var(--dsl-g-radius-surface);
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
gap: var(--dsl-g-gap-sm);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.title {
|
|
34
|
+
font-size: var(--dsl-g-font-title);
|
|
35
|
+
font-weight: 600;
|
|
36
|
+
letter-spacing: 0.01em;
|
|
37
|
+
color: var(--dsw-alias-label-primary);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.gridLine {
|
|
41
|
+
stroke: var(--dsw-alias-border-l1, rgba(255, 255, 255, 0.07));
|
|
42
|
+
stroke-width: 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.axis {
|
|
46
|
+
stroke: var(--dsw-alias-label-tertiary);
|
|
47
|
+
stroke-width: 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.tick {
|
|
51
|
+
fill: var(--dsw-alias-label-tertiary);
|
|
52
|
+
font-size: var(--dsl-g-font-data);
|
|
53
|
+
font-family: var(--dsl-g-font-mono);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.line {
|
|
57
|
+
fill: none;
|
|
58
|
+
stroke: var(--dsl-g-accent);
|
|
59
|
+
stroke-width: 2;
|
|
60
|
+
stroke-linejoin: round;
|
|
61
|
+
stroke-linecap: round;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* v2.9 draw shapes */
|
|
65
|
+
.area {
|
|
66
|
+
stroke: none;
|
|
67
|
+
fill-opacity: 0.16;
|
|
68
|
+
}
|
|
69
|
+
.scatterDot {
|
|
70
|
+
stroke: var(--dsw-alias-bg-base, #17171a);
|
|
71
|
+
stroke-width: 1;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.legend {
|
|
75
|
+
display: flex;
|
|
76
|
+
flex-wrap: wrap;
|
|
77
|
+
gap: var(--dsl-g-gap-md);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.legendItem {
|
|
81
|
+
display: inline-flex;
|
|
82
|
+
align-items: center;
|
|
83
|
+
gap: var(--dsl-g-gap-xs);
|
|
84
|
+
font-size: var(--dsl-g-font-meta);
|
|
85
|
+
color: var(--dsw-alias-label-secondary);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.legendSwatch {
|
|
89
|
+
width: 10px;
|
|
90
|
+
height: 3px;
|
|
91
|
+
border-radius: 2px;
|
|
92
|
+
background: var(--dsl-g-accent);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.empty {
|
|
96
|
+
padding: 14px 0;
|
|
97
|
+
color: var(--dsw-alias-label-tertiary);
|
|
98
|
+
font-size: var(--dsl-g-font-title);
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
gap: var(--dsl-g-gap-xs);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.emptyRow {
|
|
105
|
+
font-family: var(--dsl-g-font-mono);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.surface { cursor: grab; touch-action: none; }
|
|
109
|
+
.surface:active { cursor: grabbing; }
|
|
110
|
+
|
|
111
|
+
.sliders { display: flex; flex-direction: column; gap: var(--dsl-g-gap-sm); }
|
|
112
|
+
.sliderRow { display: flex; align-items: center; gap: var(--dsl-g-gap-sm); font-size: var(--dsl-g-font-meta); }
|
|
113
|
+
.sliderName { color: var(--dsw-alias-label-secondary); min-width: 90px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
114
|
+
.slider { flex: 1; accent-color: var(--dsl-g-accent); height: 4px; cursor: pointer; }
|
|
115
|
+
.sliderValue { color: var(--dsw-alias-label-primary); font-family: var(--dsl-g-font-mono); min-width: 34px; text-align: right; }
|
|
116
|
+
|
|
117
|
+
/* ---------- animation bar ---------- */
|
|
118
|
+
|
|
119
|
+
.animBar { display: flex; align-items: center; gap: var(--dsl-g-gap-md); }
|
|
120
|
+
.playBtn {
|
|
121
|
+
width: fit-content;
|
|
122
|
+
background: var(--dsl-g-accent-soft);
|
|
123
|
+
border: 1px solid color-mix(in srgb, var(--dsl-g-accent) 30%, transparent);
|
|
124
|
+
color: var(--dsl-g-accent);
|
|
125
|
+
border-radius: var(--dsl-g-radius-control);
|
|
126
|
+
padding: 6px 14px;
|
|
127
|
+
font-size: var(--dsl-g-font-title);
|
|
128
|
+
font-weight: 600;
|
|
129
|
+
font-family: inherit;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
}
|
|
132
|
+
.playBtn:hover { filter: brightness(1.1); }
|
|
133
|
+
.animTrack {
|
|
134
|
+
flex: 1;
|
|
135
|
+
height: 5px;
|
|
136
|
+
border-radius: 999px;
|
|
137
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
138
|
+
box-shadow: inset 0 0 0 1px var(--dsl-g-border);
|
|
139
|
+
overflow: hidden;
|
|
140
|
+
}
|
|
141
|
+
.animFill { height: 100%; border-radius: 999px; background: var(--dsl-g-accent); }
|
|
142
|
+
.resetBtn {
|
|
143
|
+
width: fit-content;
|
|
144
|
+
background: transparent;
|
|
145
|
+
border: 1px solid var(--dsl-g-border);
|
|
146
|
+
color: var(--dsw-alias-label-secondary);
|
|
147
|
+
border-radius: var(--dsl-g-radius-control);
|
|
148
|
+
padding: 6px 14px;
|
|
149
|
+
font-size: var(--dsl-g-font-title);
|
|
150
|
+
font-family: inherit;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
}
|
|
153
|
+
.resetBtn:hover { border-color: var(--dsl-g-accent); color: var(--dsl-g-accent); }
|
|
154
|
+
.slidersHead { display: flex; align-items: center; justify-content: space-between; }
|
|
155
|
+
.slidersTitle { font-size: var(--dsl-g-font-meta); color: var(--dsw-alias-label-tertiary); }
|