@tangle-network/sandbox-ui 0.124.0 → 0.125.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/README.md +11 -0
- package/dist/globals.css +17 -110
- package/dist/styles.css +17 -110
- package/dist/tailwind.css +1002 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
/* Tailwind SOURCE entry: `@tangle-network/sandbox-ui/tailwind.css`.
|
|
2
|
+
*
|
|
3
|
+
* Import it from a Tailwind v4 stylesheet, after `tailwindcss`:
|
|
4
|
+
*
|
|
5
|
+
* @import "tailwindcss";
|
|
6
|
+
* @import "@tangle-network/sandbox-ui/tailwind.css";
|
|
7
|
+
*
|
|
8
|
+
* It carries Brand's tokens, named themes and utility registrations, this
|
|
9
|
+
* package's runtime CSS, and the `@source` lines that compile every utility
|
|
10
|
+
* sandbox-ui and @tangle-network/ui components write. The published copy in
|
|
11
|
+
* dist/ appends those `@source` lines (see scripts/copy-styles.mjs); Tailwind
|
|
12
|
+
* resolves them relative to the installed file, so a consumer lists no
|
|
13
|
+
* node_modules paths of its own.
|
|
14
|
+
*
|
|
15
|
+
* The same file is the source of the precompiled `./styles` bundle:
|
|
16
|
+
* src/styles/globals.css wraps it with `@import "tailwindcss"` and this repo's
|
|
17
|
+
* own source globs. Edit runtime CSS here, never in globals.css. */
|
|
18
|
+
|
|
19
|
+
/* Fonts are loaded by the consumer, not this library. See README
|
|
20
|
+
* "Fonts" for the list of font families sandbox-ui references
|
|
21
|
+
* (Geist, Geist Mono, Outfit, Manrope, Inter) and the recommended
|
|
22
|
+
* loading strategies (@fontsource packages or an HTML <link>).
|
|
23
|
+
*
|
|
24
|
+
* Historical note: previous versions shipped
|
|
25
|
+
* @import url("https://fonts.googleapis.com/css2?family=Geist...")
|
|
26
|
+
* at the top of this file. That pattern broke in downstream apps that
|
|
27
|
+
* used CSS chain `@import`s — once the dist CSS is inlined after any
|
|
28
|
+
* rule, the URL @import is no longer at the top of the merged
|
|
29
|
+
* stylesheet and the CSS spec disallows it (PostCSS rejects the
|
|
30
|
+
* build). Shipping fonts as a library concern is also undesirable
|
|
31
|
+
* for privacy (third-party Google Fonts request) and network-failure
|
|
32
|
+
* reasons. Consumers own font loading now. */
|
|
33
|
+
|
|
34
|
+
@import "@tangle-network/brand/styles/tokens.css";
|
|
35
|
+
/* Brand's named themes (`data-theme="<name>"`). They re-skin the surface ladder
|
|
36
|
+
* and nothing else — the Tangle accent stays put. Imported here so a consuming
|
|
37
|
+
* app opting into one resolves it through the single composed entry point,
|
|
38
|
+
* rather than reaching into brand for a second stylesheet. */
|
|
39
|
+
@import "@tangle-network/brand/styles/named-themes.css";
|
|
40
|
+
|
|
41
|
+
/* Canonical font, radius and semantic-color utility registrations, imported as
|
|
42
|
+
* DEFAULT theme values. A default registration still emits every Brand utility,
|
|
43
|
+
* and it still outranks Tailwind's own defaults (it is declared later), but a
|
|
44
|
+
* consumer's `@config` preset can override a name it also maps. That is the
|
|
45
|
+
* precedence a consumer had when this package shipped only a precompiled bundle:
|
|
46
|
+
* the app's own compile loaded last and its preset mapping won. Without
|
|
47
|
+
* `default`, Brand's `--color-border` and `--radius-lg` would silently replace
|
|
48
|
+
* the Agent App preset's `border-border` and `rounded-lg` in every app that
|
|
49
|
+
* compiles this entry. */
|
|
50
|
+
@import "@tangle-network/brand/styles/theme.css" theme(default);
|
|
51
|
+
|
|
52
|
+
/* Brand defines these aliases on :root and [data-sandbox-ui]. Rebind them on
|
|
53
|
+
* explicit theme scopes so nested themes resolve their own HSL colors. A host
|
|
54
|
+
* can still override either alias on a nearer wrapper. */
|
|
55
|
+
:is(.dark, .light, [data-theme], [data-sandbox-theme="vault"]) {
|
|
56
|
+
--foreground: var(--hsl-foreground);
|
|
57
|
+
--muted-foreground: var(--hsl-muted-foreground);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* `--text-dim` arrives from brand and is NOT overridden here.
|
|
61
|
+
*
|
|
62
|
+
* `--text-dim` is the quietest text tier: an Input/Textarea hint, a StatCard
|
|
63
|
+
* subtitle, a TerminalLine timestamp. Its value is only correct when it is tuned
|
|
64
|
+
* against the surface ladder it lands on, so the token and the ladder must move
|
|
65
|
+
* together and must come from the same place. brand ships both, and
|
|
66
|
+
* `scripts/text-dim-contrast.test.mjs` holds the resolved pair to AA on every
|
|
67
|
+
* plane this package paints content on.
|
|
68
|
+
*
|
|
69
|
+
* A local override here would decouple the two: the ladder would keep arriving
|
|
70
|
+
* from brand while the text tier stopped tracking it, and the next ladder change
|
|
71
|
+
* would silently move every ratio. Correct the value in brand instead. */
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
/* The text ramp, emitted unconditionally.
|
|
75
|
+
*
|
|
76
|
+
* Everything above is a SCAN: Tailwind emits a utility only if it found that
|
|
77
|
+
* exact string in a scanned file, and the `@tangle-network/ui` glob scans
|
|
78
|
+
* whatever version happens to be installed HERE, at build time. A consumer
|
|
79
|
+
* installs its own `@tangle-network/ui` and can perfectly well resolve a newer
|
|
80
|
+
* one than this package was built against — the peer range permits it — and
|
|
81
|
+
* every utility that version introduced is then missing from the precompiled
|
|
82
|
+
* bundle.
|
|
83
|
+
*
|
|
84
|
+
* That is not hypothetical. `@tangle-network/ui` 11.2.4 restyled the
|
|
85
|
+
* Input/Textarea hint, the StatCard subtitle and the TerminalLine timestamp onto
|
|
86
|
+
* `text-[var(--text-dim)]`; this package resolved 11.2.0; `dist/globals.css`
|
|
87
|
+
* emitted no rule for it. Apps that run their own Tailwind/UnoCSS scan over
|
|
88
|
+
* `@tangle-network/ui` were fine. Apps on the precompiled bundle (physim,
|
|
89
|
+
* blueprint-agent) rendered those three at inherited body colour — 15-17:1,
|
|
90
|
+
* indistinguishable from body copy. A contrast audit cannot see it, because
|
|
91
|
+
* nothing is under-contrast; the tier is simply gone.
|
|
92
|
+
*
|
|
93
|
+
* A scan cannot fix that, because the missing string is by definition one this
|
|
94
|
+
* build never saw. So the four text tiers are declared inline: they are the
|
|
95
|
+
* brand text ramp, they are a closed set, and each one's absence degrades
|
|
96
|
+
* silently to the inherited colour rather than to something visibly wrong.
|
|
97
|
+
* Pinned by `scripts/text-dim-contrast.test.mjs`.
|
|
98
|
+
*
|
|
99
|
+
* The test for belonging here is who WRITES the class, not which token it
|
|
100
|
+
* names: a class a consumer writes and this package does not. Anything this
|
|
101
|
+
* package's own components write is already covered by the `@source` globs
|
|
102
|
+
* above and needs no entry — `text-[var(--accent-text)]` in the connector
|
|
103
|
+
* catalog, for one, which the scan sees and emits. */
|
|
104
|
+
@source inline("text-[var(--text-primary)]");
|
|
105
|
+
@source inline("text-[var(--text-secondary)]");
|
|
106
|
+
@source inline("text-[var(--text-muted)]");
|
|
107
|
+
@source inline("text-[var(--text-dim)]");
|
|
108
|
+
|
|
109
|
+
/* The run-mix ramp, emitted for the same reason the text ramp is: a consuming
|
|
110
|
+
* app paints a proportional status bar with these, and a scan of THIS package
|
|
111
|
+
* never sees the class. */
|
|
112
|
+
@source inline("bg-[var(--run-mix-succeeded)]");
|
|
113
|
+
@source inline("bg-[var(--run-mix-waiting)]");
|
|
114
|
+
@source inline("bg-[var(--run-mix-failed)]");
|
|
115
|
+
@source inline("bg-[var(--run-mix-in-flight)]");
|
|
116
|
+
@source inline("bg-[var(--run-mix-cancelled)]");
|
|
117
|
+
|
|
118
|
+
/* Compatibility names not registered by Brand 1.10.0. The runtime values
|
|
119
|
+
* still belong to Brand; do not duplicate its other @theme registrations. */
|
|
120
|
+
@theme inline default {
|
|
121
|
+
--color-surface: var(--md3-surface);
|
|
122
|
+
--color-surface-dim: var(--md3-surface-dim);
|
|
123
|
+
--color-surface-bright: var(--md3-surface-bright);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* ── Motion ────────────────────────────────────────────────────────────────
|
|
127
|
+
*
|
|
128
|
+
* `@tangle-network/brand` ships exactly one motion token (`--transition-default:
|
|
129
|
+
* 0.16s ease`) and no curves, so every animated surface in this package picked
|
|
130
|
+
* its own number: `duration-150` on a rail button, `duration-200` on the rail
|
|
131
|
+
* itself, `0.18s ease-out` on a session row, Tailwind's implicit default
|
|
132
|
+
* everywhere a bare `transition-colors` was written. Nothing was wrong;
|
|
133
|
+
* nothing was decidable either, and none of it was reachable by a reduced-motion
|
|
134
|
+
* preference.
|
|
135
|
+
*
|
|
136
|
+
* These are the SAME names and the SAME values `@tangle-network/agent-app`
|
|
137
|
+
* declares in its own `theme/tokens.css` (0.45.46+), deliberately duplicated
|
|
138
|
+
* rather than imported: sandbox-ui does not depend on agent-app, and a product
|
|
139
|
+
* that loads both stylesheets must not get two different answers for
|
|
140
|
+
* `--duration-base`. Identical declarations are order-independent; divergent
|
|
141
|
+
* ones are a coin flip decided by import order. If agent-app retunes the
|
|
142
|
+
* ladder, retune it here in the same change.
|
|
143
|
+
*
|
|
144
|
+
* They live in `globals.css` and not in `dist/tokens.css` because that file is
|
|
145
|
+
* a byte-for-byte copy of brand's (see scripts/copy-styles.mjs) — the tokens
|
|
146
|
+
* that are ours to declare belong in the one stylesheet this package authors,
|
|
147
|
+
* next to the utilities that consume them.
|
|
148
|
+
*
|
|
149
|
+
* instant a control acknowledging a pointer (hover/active colour)
|
|
150
|
+
* fast a state change on a control already on screen
|
|
151
|
+
* base a surface arriving or leaving (popover, panel, disclosure)
|
|
152
|
+
* slow a full-height surface travelling (drawer, sheet, the rail)
|
|
153
|
+
*
|
|
154
|
+
* Curves: entrances decelerate hard and land softly; state changes decelerate
|
|
155
|
+
* normally; exits ACCELERATE away, because a user who dismissed something is
|
|
156
|
+
* done with it and should not wait for it to glide out. */
|
|
157
|
+
:root {
|
|
158
|
+
--duration-instant: 90ms;
|
|
159
|
+
--duration-fast: 150ms;
|
|
160
|
+
--duration-base: 240ms;
|
|
161
|
+
--duration-slow: 360ms;
|
|
162
|
+
--ease-standard: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
163
|
+
--ease-entrance: cubic-bezier(0.22, 1, 0.36, 1);
|
|
164
|
+
--ease-exit: cubic-bezier(0.4, 0, 1, 1);
|
|
165
|
+
/* Drop-in duration+easing pairs: `transition: background-color var(--motion-control)`. */
|
|
166
|
+
--motion-control: var(--duration-fast) var(--ease-standard);
|
|
167
|
+
--motion-surface: var(--duration-base) var(--ease-entrance);
|
|
168
|
+
--motion-dismiss: var(--duration-instant) var(--ease-exit);
|
|
169
|
+
|
|
170
|
+
/* Entrance choreography. The durations above answer "how fast does an
|
|
171
|
+
* existing thing change"; these answer "how does a thing that was not there
|
|
172
|
+
* ARRIVE" — a nav rail unfolding, a session appearing in the list, a
|
|
173
|
+
* disclosure revealing its sub-items.
|
|
174
|
+
*
|
|
175
|
+
* `--motion-stream` is longer than `base` on purpose: streamed text arrives
|
|
176
|
+
* continuously, so each chunk overlaps its neighbours and a fast curve makes
|
|
177
|
+
* the paragraph flicker. `--motion-arrive` is the row/card entrance and is
|
|
178
|
+
* meant to be staggered by index (see `--stagger-step`). */
|
|
179
|
+
--duration-stream: 420ms;
|
|
180
|
+
--duration-arrive: 600ms;
|
|
181
|
+
--ease-expo: cubic-bezier(0.23, 1, 0.32, 1);
|
|
182
|
+
--motion-stream: var(--duration-stream) var(--ease-expo);
|
|
183
|
+
--motion-arrive: var(--duration-arrive) var(--ease-expo);
|
|
184
|
+
/* Per-item delay in a staggered group. 5 items = 200ms tail, which stays
|
|
185
|
+
* under the 250ms an interface can add before it reads as latency. */
|
|
186
|
+
--stagger-step: 50ms;
|
|
187
|
+
/* Position in a staggered group, overridden PER ELEMENT by the caller. It is
|
|
188
|
+
* declared here with a zero default rather than read through a `var()`
|
|
189
|
+
* fallback so the file never references a token it does not define — an
|
|
190
|
+
* undefined `var()` resolves to nothing and paints silently, which is the
|
|
191
|
+
* failure the token contract exists to catch. */
|
|
192
|
+
--stagger-index: 0;
|
|
193
|
+
/* How far an arriving surface travels, and how far a streamed chunk resolves
|
|
194
|
+
* from. Both are small: motion that announces itself is motion you notice
|
|
195
|
+
* twice. */
|
|
196
|
+
--arrive-distance: 8px;
|
|
197
|
+
--stream-blur: 4px;
|
|
198
|
+
/* Period of the `.agent-shimmer` sweep. Deliberately NOT named
|
|
199
|
+
* `--duration-*`: that prefix is the collapsible ladder, and the
|
|
200
|
+
* reduced-motion block sets every member of it to 1ms at `:root`. A custom
|
|
201
|
+
* property collapsed at the root cannot be excluded per element, so naming
|
|
202
|
+
* this one `--duration-shimmer` would freeze the sweep for the very users the
|
|
203
|
+
* `data-motion="essential"` exemption exists to keep informed. The sweep is a
|
|
204
|
+
* looping SIGNAL, not a transition, so it sits with the other non-collapsible
|
|
205
|
+
* motion constants above. */
|
|
206
|
+
--shimmer-period: 1400ms;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* `@pierre/trees` (RichFileTree) is CSS-var driven and ships LIGHT defaults with
|
|
210
|
+
* no dark switching — `--trees-bg: var(--trees-bg-override, <light>)`. Without the
|
|
211
|
+
* `*-override` vars it paints a near-white `#f8f8f8` container even inside a dark
|
|
212
|
+
* subtree (the file-tree-rail-looks-light bug). Map the overrides onto the design
|
|
213
|
+
* tokens so the tree follows light/vault ↔ dark automatically (the surface +
|
|
214
|
+
* foreground vars already flip per theme). Apply via `.sandbox-tree-surface`. */
|
|
215
|
+
.sandbox-tree-surface {
|
|
216
|
+
--trees-bg-override: var(--md3-surface-container-lowest);
|
|
217
|
+
--trees-bg-muted-override: var(--md3-surface-container-low);
|
|
218
|
+
--trees-search-bg-override: var(--md3-surface-container);
|
|
219
|
+
--trees-input-bg-override: var(--md3-surface-container);
|
|
220
|
+
--trees-hover-bg-override: var(--md3-surface-container-high);
|
|
221
|
+
--trees-selected-bg-override: hsl(var(--primary) / 0.16);
|
|
222
|
+
--trees-selected-fg-override: hsl(var(--foreground));
|
|
223
|
+
--trees-fg-override: hsl(var(--foreground));
|
|
224
|
+
--trees-fg-muted-override: hsl(var(--muted-foreground));
|
|
225
|
+
--trees-muted-fg-override: hsl(var(--muted-foreground));
|
|
226
|
+
--trees-search-fg-override: hsl(var(--foreground));
|
|
227
|
+
--trees-border-color-override: var(--md3-outline-variant);
|
|
228
|
+
--trees-indent-guide-bg-override: var(--md3-outline-variant);
|
|
229
|
+
--trees-accent-override: hsl(var(--primary));
|
|
230
|
+
--trees-focus-ring-color-override: var(--focus-border);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.sandbox-code-view { container-type: inline-size; }
|
|
234
|
+
.sandbox-code-view-mobile-toolbar { display: none; }
|
|
235
|
+
|
|
236
|
+
@container (max-width: 560px) {
|
|
237
|
+
.sandbox-code-view-layout { flex-direction: column; }
|
|
238
|
+
.sandbox-code-view-mobile-toolbar {
|
|
239
|
+
display: flex;
|
|
240
|
+
gap: 0.25rem;
|
|
241
|
+
padding: 0.375rem 0.75rem;
|
|
242
|
+
border-bottom: 1px solid var(--md3-outline-variant);
|
|
243
|
+
background: var(--md3-surface-container-lowest);
|
|
244
|
+
}
|
|
245
|
+
.sandbox-code-view-mobile-toolbar button {
|
|
246
|
+
padding: 0.25rem 0.625rem;
|
|
247
|
+
border-radius: 0.375rem;
|
|
248
|
+
color: hsl(var(--muted-foreground));
|
|
249
|
+
font-size: 0.75rem;
|
|
250
|
+
}
|
|
251
|
+
.sandbox-code-view-mobile-toolbar button[aria-pressed="true"] {
|
|
252
|
+
background: var(--md3-surface-container-high);
|
|
253
|
+
color: hsl(var(--foreground));
|
|
254
|
+
}
|
|
255
|
+
.sandbox-code-view-tree { display: none; width: 100%; min-height: 0; flex: 1; border-right: 0; }
|
|
256
|
+
.sandbox-code-view-tree[data-mobile-open="true"] { display: flex; }
|
|
257
|
+
.sandbox-code-view-editor[data-mobile-hidden="true"] { display: none; }
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
@layer base {
|
|
261
|
+
:root {
|
|
262
|
+
--sandbox: var(--primary);
|
|
263
|
+
--sandbox-glow: var(--info);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
* {
|
|
267
|
+
border-color: hsl(var(--border));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* The default keyboard focus for any control that does not style its own,
|
|
271
|
+
* the same line and halo as ui's `focusRing` (mirrors brand's globals.css,
|
|
272
|
+
* which this bundle does not import). Base layer, so component utilities
|
|
273
|
+
* still win. Text fields have their own fallback below. */
|
|
274
|
+
:where(
|
|
275
|
+
a[href],
|
|
276
|
+
button,
|
|
277
|
+
summary,
|
|
278
|
+
input:is([type="checkbox"], [type="radio"]),
|
|
279
|
+
[role="button"],
|
|
280
|
+
[role="checkbox"],
|
|
281
|
+
[role="link"],
|
|
282
|
+
[role="menuitem"],
|
|
283
|
+
[role="option"],
|
|
284
|
+
[role="radio"],
|
|
285
|
+
[role="switch"],
|
|
286
|
+
[role="tab"],
|
|
287
|
+
[tabindex]:not([tabindex="-1"])
|
|
288
|
+
):focus-visible {
|
|
289
|
+
outline: 1px solid var(--focus-border);
|
|
290
|
+
outline-offset: 0;
|
|
291
|
+
box-shadow: 0 0 0 4px var(--focus-halo);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* A native text field that styles no focus: a 1px --focus-border line over
|
|
295
|
+
* its border, instead of the browser's blue outline (mirrors brand's
|
|
296
|
+
* globals.css). An outline, because a border-colour utility on the field
|
|
297
|
+
* outranks a base-layer border colour. No halo: a field that hands its
|
|
298
|
+
* focus to a container clears the line with `outline-none`. */
|
|
299
|
+
:where(
|
|
300
|
+
input:not(
|
|
301
|
+
[type="button"],
|
|
302
|
+
[type="checkbox"],
|
|
303
|
+
[type="color"],
|
|
304
|
+
[type="file"],
|
|
305
|
+
[type="hidden"],
|
|
306
|
+
[type="image"],
|
|
307
|
+
[type="radio"],
|
|
308
|
+
[type="range"],
|
|
309
|
+
[type="reset"],
|
|
310
|
+
[type="submit"]
|
|
311
|
+
),
|
|
312
|
+
select,
|
|
313
|
+
textarea
|
|
314
|
+
):focus-visible {
|
|
315
|
+
outline: 1px solid var(--focus-border);
|
|
316
|
+
outline-offset: -1px;
|
|
317
|
+
/* A field with an explicit tabindex also matches the control rule above;
|
|
318
|
+
* drop that rule's halo. */
|
|
319
|
+
box-shadow: none;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/* The rounded composite owns the focus line, not its inner editor. */
|
|
323
|
+
.focus-field-within :is(input:not([type=button],[type=submit],[type=reset],[type=image],[type=checkbox],[type=radio],[type=range],[type=color],[type=file]),textarea,select,[contenteditable]:not([contenteditable=false])):focus-visible {
|
|
324
|
+
outline: none;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
body {
|
|
328
|
+
-webkit-font-smoothing: antialiased;
|
|
329
|
+
-moz-osx-font-smoothing: grayscale;
|
|
330
|
+
background-color: hsl(var(--background));
|
|
331
|
+
color: hsl(var(--foreground));
|
|
332
|
+
font-family: var(--font-sans);
|
|
333
|
+
font-feature-settings:
|
|
334
|
+
"rlig" 1,
|
|
335
|
+
"calt" 1,
|
|
336
|
+
"ss01" 1;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
::selection {
|
|
340
|
+
background: var(--bg-selection);
|
|
341
|
+
color: hsl(var(--hsl-foreground));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/* Thin neutral scrollbar — recedes until hovered. */
|
|
345
|
+
::-webkit-scrollbar {
|
|
346
|
+
width: 6px;
|
|
347
|
+
height: 6px;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
::-webkit-scrollbar-track {
|
|
351
|
+
background: transparent;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
::-webkit-scrollbar-thumb {
|
|
355
|
+
background: hsl(var(--border) / 0.6);
|
|
356
|
+
border-radius: 9999px;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
::-webkit-scrollbar-thumb:hover {
|
|
360
|
+
background: hsl(var(--muted-foreground) / 0.4);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/* Firefox: thin neutral track. */
|
|
364
|
+
* {
|
|
365
|
+
scrollbar-width: thin;
|
|
366
|
+
scrollbar-color: hsl(var(--border) / 0.6) transparent;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
@layer utilities {
|
|
371
|
+
/* Gradient text -- Tangle default accent */
|
|
372
|
+
.text-gradient-sandbox,
|
|
373
|
+
.text-gradient-brand {
|
|
374
|
+
background-image: var(--tangle-gradient);
|
|
375
|
+
background-clip: text;
|
|
376
|
+
-webkit-background-clip: text;
|
|
377
|
+
color: transparent;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* Tangle Quiet has no glow chrome. Class retained as a flat no-op so any
|
|
381
|
+
stray consumer renders without a halo. */
|
|
382
|
+
.glow-sandbox {
|
|
383
|
+
box-shadow: none;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* Resting card — L2 surface in the MD3 elevation ladder. */
|
|
387
|
+
.glass-card {
|
|
388
|
+
background: var(--md3-surface-container);
|
|
389
|
+
border: 1px solid var(--md3-outline-variant);
|
|
390
|
+
transition: border-color var(--transition-default), box-shadow var(--transition-default);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.glass-card:hover {
|
|
394
|
+
border-color: var(--md3-outline-variant);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/* Emphasized / nested card — one step up the ladder (L3). */
|
|
398
|
+
.glass-card-strong {
|
|
399
|
+
background: var(--md3-surface-container-high);
|
|
400
|
+
border: 1px solid var(--md3-outline-variant);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/* Flat panels — no backdrop blur, edge-light hairline, flat card shadow.
|
|
404
|
+
.glass-panel is the resting L2 surface; -heavy lifts one ladder step (L3). */
|
|
405
|
+
.glass-panel {
|
|
406
|
+
background: var(--md3-surface-container);
|
|
407
|
+
border: 1px solid var(--md3-outline-variant);
|
|
408
|
+
box-shadow: var(--shadow-card);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.glass-panel-heavy {
|
|
412
|
+
background: var(--md3-surface-container-high);
|
|
413
|
+
border: 1px solid var(--md3-outline-variant);
|
|
414
|
+
box-shadow: var(--shadow-card);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.glass-panel:hover {
|
|
418
|
+
border-color: var(--md3-outline-variant);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* Glow utilities retained as flat no-ops — Tangle Quiet has no halos. */
|
|
422
|
+
.glow-primary {
|
|
423
|
+
box-shadow: none;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.glow-primary:hover {
|
|
427
|
+
box-shadow: none;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.text-glow {
|
|
431
|
+
text-shadow: none;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* Radiant gradient text */
|
|
435
|
+
.gradient-text {
|
|
436
|
+
background: linear-gradient(135deg, hsl(var(--primary) / 0.7) 0%, hsl(var(--primary)) 100%);
|
|
437
|
+
-webkit-background-clip: text;
|
|
438
|
+
-webkit-text-fill-color: transparent;
|
|
439
|
+
background-clip: text;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
.gradient-bg {
|
|
443
|
+
background: linear-gradient(135deg, hsl(var(--primary) / 0.1) 0%, hsl(var(--primary) / 0.05) 100%);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/* Retained as a flat no-op — the mesh texture belongs to the glow era. */
|
|
447
|
+
.neural-mesh {
|
|
448
|
+
background-image: none;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.nav-active-indicator {
|
|
452
|
+
position: relative;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
.nav-active-indicator::before {
|
|
456
|
+
content: "";
|
|
457
|
+
position: absolute;
|
|
458
|
+
left: 0;
|
|
459
|
+
top: 50%;
|
|
460
|
+
transform: translateY(-50%);
|
|
461
|
+
height: 60%;
|
|
462
|
+
width: 3px;
|
|
463
|
+
background-color: hsl(var(--primary));
|
|
464
|
+
border-radius: 0 4px 4px 0;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/* Legacy alias — resting L2 surface. */
|
|
468
|
+
.glass {
|
|
469
|
+
background-color: var(--md3-surface-container);
|
|
470
|
+
border: 1px solid var(--md3-outline-variant);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* Atmospheric mesh gradient background */
|
|
474
|
+
.bg-mesh {
|
|
475
|
+
position: relative;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* Mesh + noise retained as flat no-ops — atmospheric texture is the glow era. */
|
|
479
|
+
.bg-mesh::before {
|
|
480
|
+
content: none;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.noise {
|
|
484
|
+
background-image: none;
|
|
485
|
+
pointer-events: none;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* Status dots — operational state indicators */
|
|
489
|
+
.status-dot {
|
|
490
|
+
width: 8px;
|
|
491
|
+
height: 8px;
|
|
492
|
+
border-radius: 50%;
|
|
493
|
+
display: inline-block;
|
|
494
|
+
flex-shrink: 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.status-dot-running {
|
|
498
|
+
background: var(--status-running);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.status-dot-creating {
|
|
502
|
+
background: var(--status-creating);
|
|
503
|
+
animation: status-pulse 2s ease-in-out infinite;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.status-dot-stopped {
|
|
507
|
+
background: var(--status-stopped);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.status-dot-warm {
|
|
511
|
+
background: var(--status-warm);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.status-dot-cold {
|
|
515
|
+
background: var(--status-cold);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.status-dot-error {
|
|
519
|
+
background: var(--status-error);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.status-dot-deleted {
|
|
523
|
+
background: var(--status-deleted);
|
|
524
|
+
opacity: 0.6;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/* Terminal cursor */
|
|
528
|
+
.terminal-cursor {
|
|
529
|
+
display: inline-block;
|
|
530
|
+
width: 0.5rem;
|
|
531
|
+
height: 1.25rem;
|
|
532
|
+
background-color: hsl(var(--foreground));
|
|
533
|
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* Shimmer animation */
|
|
537
|
+
.shimmer {
|
|
538
|
+
background: linear-gradient(
|
|
539
|
+
90deg,
|
|
540
|
+
hsl(var(--muted)) 0%,
|
|
541
|
+
hsl(var(--accent)) 50%,
|
|
542
|
+
hsl(var(--muted)) 100%
|
|
543
|
+
);
|
|
544
|
+
background-size: 200% 100%;
|
|
545
|
+
animation: shimmer 2s infinite;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.shimmer-text {
|
|
549
|
+
background-image: linear-gradient(
|
|
550
|
+
90deg,
|
|
551
|
+
hsl(var(--muted-foreground) / 0.5) 0%,
|
|
552
|
+
hsl(var(--muted-foreground) / 0.5) 40%,
|
|
553
|
+
hsl(var(--primary)) 50%,
|
|
554
|
+
hsl(var(--muted-foreground) / 0.5) 60%,
|
|
555
|
+
hsl(var(--muted-foreground) / 0.5) 100%
|
|
556
|
+
);
|
|
557
|
+
background-size: 200% 100%;
|
|
558
|
+
-webkit-background-clip: text;
|
|
559
|
+
background-clip: text;
|
|
560
|
+
-webkit-text-fill-color: transparent;
|
|
561
|
+
animation: shimmer 1.1s linear infinite;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/* Silenced outright rather than floored, and kept that way: this is shipped
|
|
565
|
+
behaviour of a published class. It is also the correct default under the
|
|
566
|
+
rule stated with the reduced-motion block below — a bare class carries no
|
|
567
|
+
claim about whether the thing it labels is still working. A caller that
|
|
568
|
+
needs the sweep to survive the preference uses `.agent-shimmer` on an
|
|
569
|
+
element that declares `data-motion="essential"`, which is where that claim
|
|
570
|
+
can actually be made. */
|
|
571
|
+
@media (prefers-reduced-motion: reduce) {
|
|
572
|
+
.shimmer-text {
|
|
573
|
+
animation: none;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* Fade in animation */
|
|
578
|
+
.animate-in {
|
|
579
|
+
animation: fade-in 0.25s ease-out;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/* A workflow node's content when its density swap lands mid layout-morph:
|
|
583
|
+
the incoming content materializes inside the moving box instead of popping
|
|
584
|
+
in. Opacity only — movement belongs to the layout tween — and applied to
|
|
585
|
+
CONTENT, never to a node's border/surface shell, so the node itself never
|
|
586
|
+
blinks out during the swap. */
|
|
587
|
+
.wf-node-body-in {
|
|
588
|
+
animation: wf-node-body-in 150ms ease-out both;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
@keyframes wf-node-body-in {
|
|
592
|
+
from { opacity: 0; }
|
|
593
|
+
to { opacity: 1; }
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
@media (prefers-reduced-motion: reduce) {
|
|
597
|
+
.wf-node-body-in {
|
|
598
|
+
animation: none;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/* Quiet enter for optimistically-inserted list rows.
|
|
603
|
+
PUBLISHED CLASS — FROZEN TIMING. Ships in `dist/globals.css`, so consumers
|
|
604
|
+
time their own list insertions against these exact 180ms and this exact
|
|
605
|
+
4px travel DOWNWARD from above. Retiming it to the 600ms `agent-arrive`
|
|
606
|
+
entrance is a 3.3x slowdown and a reversed travel direction in every app
|
|
607
|
+
that already uses it, delivered as a patch with no call site to change; the
|
|
608
|
+
motion vocabulary is not worth that. It stays as shipped.
|
|
609
|
+
|
|
610
|
+
`--motion-arrive` is not read here for the same reason: a retune of the
|
|
611
|
+
token ladder must not reach a class whose timing is the contract. Reduced
|
|
612
|
+
motion still covers it — the universal floor below collapses any animation
|
|
613
|
+
on an element that has not declared itself essential, token or not.
|
|
614
|
+
|
|
615
|
+
New code writes `.agent-arrive` (plus `--stagger-index` for a list), which
|
|
616
|
+
is the vocabulary the rail and the session list ride. */
|
|
617
|
+
.animate-row-in {
|
|
618
|
+
animation: row-in 0.18s ease-out;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
@keyframes row-in {
|
|
622
|
+
from { opacity: 0; transform: translateY(-4px); }
|
|
623
|
+
to { opacity: 1; transform: translateY(0); }
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/* Pulse ring animation */
|
|
627
|
+
.pulse-ring {
|
|
628
|
+
position: relative;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.pulse-ring::before {
|
|
632
|
+
content: "";
|
|
633
|
+
position: absolute;
|
|
634
|
+
inset: 0;
|
|
635
|
+
border-radius: 9999px;
|
|
636
|
+
animation: pulse-ring 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
@keyframes shimmer {
|
|
640
|
+
0% { background-position: -200% 0; }
|
|
641
|
+
100% { background-position: 200% 0; }
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
@keyframes fade-in {
|
|
645
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
646
|
+
to { opacity: 1; transform: translateY(0); }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
@keyframes pulse-ring {
|
|
650
|
+
0%, 100% { box-shadow: 0 0 0 0 currentColor; opacity: 0.4; }
|
|
651
|
+
50% { box-shadow: 0 0 0 8px currentColor; opacity: 0; }
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
@keyframes status-pulse {
|
|
655
|
+
0%, 100% { opacity: 1; }
|
|
656
|
+
50% { opacity: 0.35; }
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Entrance choreography — the keyframes and the classes that ride them.
|
|
662
|
+
*
|
|
663
|
+
* Unlayered on purpose. Tailwind's own utilities live in its `utilities` layer,
|
|
664
|
+
* and unlayered rules beat every layer, so `.agent-disclose`'s grid rows and
|
|
665
|
+
* `.agent-arrive`'s animation cannot be knocked out by a utility that happens to
|
|
666
|
+
* sort later.
|
|
667
|
+
*
|
|
668
|
+
* Every duration below is a TOKEN, so no class here carries a media query of its
|
|
669
|
+
* own. They are not all reached by the SAME rule, and the difference is
|
|
670
|
+
* deliberate. The entrance classes read the `--duration-*` ladder (through the
|
|
671
|
+
* `--motion-*` pairs) and `--stagger-step`, which the reduced-motion block
|
|
672
|
+
* collapses at `:root`: that reaches every element, unconditionally.
|
|
673
|
+
* `.agent-shimmer` reads `--shimmer-period`, which is deliberately NOT in that
|
|
674
|
+
* ladder, because a value collapsed at the root cannot be un-inherited by the
|
|
675
|
+
* elements that declare `data-motion="essential"` and whose sweep is the only
|
|
676
|
+
* evidence their agent is still working. Everywhere else, that sweep is silenced
|
|
677
|
+
* by the universal floor in the same block. @see `--shimmer-period`.
|
|
678
|
+
*
|
|
679
|
+
* Same names, same keyframes, same values as `@tangle-network/agent-app`'s
|
|
680
|
+
* `theme/tokens.css` — @see the token block at the top of this file for why they
|
|
681
|
+
* are duplicated rather than imported.
|
|
682
|
+
*/
|
|
683
|
+
|
|
684
|
+
/* Streamed text: each chunk resolves out of a small blur instead of popping in. */
|
|
685
|
+
@keyframes agent-stream-in {
|
|
686
|
+
from {
|
|
687
|
+
opacity: 0;
|
|
688
|
+
filter: blur(var(--stream-blur));
|
|
689
|
+
}
|
|
690
|
+
to {
|
|
691
|
+
opacity: 1;
|
|
692
|
+
filter: blur(0);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* A row, card or chip that was not there a moment ago. Travels a short distance
|
|
697
|
+
so the eye is told WHERE it came from. */
|
|
698
|
+
@keyframes agent-arrive {
|
|
699
|
+
from {
|
|
700
|
+
opacity: 0;
|
|
701
|
+
transform: translateY(var(--arrive-distance));
|
|
702
|
+
}
|
|
703
|
+
to {
|
|
704
|
+
opacity: 1;
|
|
705
|
+
transform: none;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/* A control appearing in place — a chip, a badge, a rail tooltip, a flyout. No
|
|
710
|
+
travel, because it did not come from anywhere. */
|
|
711
|
+
@keyframes agent-pop-in {
|
|
712
|
+
from {
|
|
713
|
+
opacity: 0;
|
|
714
|
+
transform: scale(0.96);
|
|
715
|
+
}
|
|
716
|
+
to {
|
|
717
|
+
opacity: 1;
|
|
718
|
+
transform: none;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/* The insertion caret at the end of streaming text. A hard step blink, not a
|
|
723
|
+
fade: a caret that eases is a caret that reads as a loading placeholder. */
|
|
724
|
+
@keyframes agent-caret {
|
|
725
|
+
0%,
|
|
726
|
+
49% {
|
|
727
|
+
opacity: 1;
|
|
728
|
+
}
|
|
729
|
+
50%,
|
|
730
|
+
100% {
|
|
731
|
+
opacity: 0;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/* A label that is WAITING. The sweep is what separates "the agent is working"
|
|
736
|
+
from "the interface is stuck": a static label cannot tell you which. */
|
|
737
|
+
@keyframes agent-shimmer {
|
|
738
|
+
from {
|
|
739
|
+
background-position: 150% center;
|
|
740
|
+
}
|
|
741
|
+
to {
|
|
742
|
+
background-position: -50% center;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.agent-stream-in {
|
|
747
|
+
animation: agent-stream-in var(--motion-stream) both;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.agent-arrive {
|
|
751
|
+
animation: agent-arrive var(--motion-arrive) both;
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
.agent-pop-in {
|
|
755
|
+
animation: agent-pop-in var(--motion-control) both;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/* Stagger by index: set `--stagger-index` and a group arrives as a sequence
|
|
759
|
+
rather than as a flash. Capped at 8 so a 200-session list does not open with a
|
|
760
|
+
10-second cascade — past the cap every row shares the last delay. */
|
|
761
|
+
.agent-arrive {
|
|
762
|
+
animation-delay: calc(min(var(--stagger-index), 8) * var(--stagger-step));
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/* The waiting label. `background-clip: text` over a moving gradient, so the
|
|
766
|
+
shimmer runs THROUGH the glyphs rather than behind them.
|
|
767
|
+
|
|
768
|
+
The class alone is decorative and IS silenced by the reduced-motion floor.
|
|
769
|
+
It becomes essential only where the element also declares
|
|
770
|
+
`data-motion="essential"`, which is the caller's assertion that this
|
|
771
|
+
particular label's agent is still working. Class-level exemption would be
|
|
772
|
+
wrong: the same sweep is used as a plain skeleton treatment, and a rule
|
|
773
|
+
cannot tell the two apart. @see the reduced-motion block below. */
|
|
774
|
+
.agent-shimmer {
|
|
775
|
+
background: linear-gradient(
|
|
776
|
+
90deg,
|
|
777
|
+
hsl(var(--muted-foreground)) 0%,
|
|
778
|
+
hsl(var(--foreground)) 45%,
|
|
779
|
+
hsl(var(--muted-foreground)) 60%
|
|
780
|
+
);
|
|
781
|
+
background-size: 250% auto;
|
|
782
|
+
background-clip: text;
|
|
783
|
+
-webkit-background-clip: text;
|
|
784
|
+
color: transparent;
|
|
785
|
+
animation: agent-shimmer var(--shimmer-period) linear infinite;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/* Height-auto disclosure without measuring anything in JavaScript. The row
|
|
789
|
+
animates between 0fr and 1fr; the single child needs `min-height: 0` and
|
|
790
|
+
`overflow: hidden` to be clipped while it travels — it needs no class of its
|
|
791
|
+
own. A max-height guess is the usual alternative and it either clips tall
|
|
792
|
+
content or eases against a number the content never reaches, which for a nav
|
|
793
|
+
disclosure holding an unknown number of sessions is both. */
|
|
794
|
+
.agent-disclose {
|
|
795
|
+
display: grid;
|
|
796
|
+
grid-template-rows: 0fr;
|
|
797
|
+
transition: grid-template-rows var(--motion-surface), opacity var(--motion-control);
|
|
798
|
+
opacity: 0;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
.agent-disclose > * {
|
|
802
|
+
min-height: 0;
|
|
803
|
+
overflow: hidden;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
.agent-disclose[data-open="true"] {
|
|
807
|
+
grid-template-rows: 1fr;
|
|
808
|
+
opacity: 1;
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Reduced motion, at the token layer and at the floor.
|
|
813
|
+
*
|
|
814
|
+
* Two rules, because they cover two different populations. Collapsing the
|
|
815
|
+
* duration TOKENS is what makes motion built on this system honour the
|
|
816
|
+
* preference for free — a component that wrote `var(--motion-surface)` or
|
|
817
|
+
* `duration-[var(--duration-fast)]` needs no media query of its own, and gets no
|
|
818
|
+
* chance to forget one. The universal rule under it is the floor for everything
|
|
819
|
+
* that predates the tokens: this package still carries transitions on Tailwind's
|
|
820
|
+
* implicit duration, and a consuming app carries its own. Neither reads a token,
|
|
821
|
+
* so neither would be reached.
|
|
822
|
+
*
|
|
823
|
+
* 1ms rather than 0: a zero-duration transition fires no `transitionend`, and
|
|
824
|
+
* any component that awaits one to unmount or advance a state machine hangs
|
|
825
|
+
* forever. 1ms is imperceptible AND still fires.
|
|
826
|
+
*
|
|
827
|
+
* THE RULE, so the next element is classified the same way this one was:
|
|
828
|
+
*
|
|
829
|
+
* Motion is ESSENTIAL when it is the only evidence that something is
|
|
830
|
+
* happening RIGHT NOW — an operation in flight, a resource actively running.
|
|
831
|
+
* Freeze it and "working" becomes indistinguishable from "stuck", which is
|
|
832
|
+
* information removed. Everything else is DECORATIVE and is silenced.
|
|
833
|
+
*
|
|
834
|
+
* Two consequences make it decidable rather than a matter of taste:
|
|
835
|
+
*
|
|
836
|
+
* - A static label beside the motion does NOT make an in-flight indicator
|
|
837
|
+
* decorative. "Loading processes…" stays on screen whether the fetch is
|
|
838
|
+
* alive or hung; only the spinner separates those.
|
|
839
|
+
* - A state that is NOT in flight is carried by its label, its colour and its
|
|
840
|
+
* shape, so motion on it is emphasis. The `animate-ping` beside "Error
|
|
841
|
+
* fetching logs" says nothing the sentence does not, and it is silenced.
|
|
842
|
+
* Skeleton placeholders are the same case: a frozen skeleton still reads as
|
|
843
|
+
* a placeholder, because the shape is the message.
|
|
844
|
+
*
|
|
845
|
+
* The exemption is declared PER ELEMENT with `data-motion="essential"`, never on
|
|
846
|
+
* a class. `.agent-shimmer` is the reason: the identical sweep marks a session
|
|
847
|
+
* whose agent is mid-turn and dresses a plain skeleton, and only the caller
|
|
848
|
+
* knows which one it built. The exclusion covers the element and its subtree,
|
|
849
|
+
* and `:where()` keeps the selector at zero specificity so a consumer's own rule
|
|
850
|
+
* can still win. Reduced motion is not a design preference to be overridden, it
|
|
851
|
+
* is a request from the person using the product; the only defensible exemption
|
|
852
|
+
* is motion whose absence removes information.
|
|
853
|
+
*
|
|
854
|
+
* Note the asymmetry with the `:root` block: a duration collapsed there reaches
|
|
855
|
+
* every element, `data-motion` included, because a custom property cannot be
|
|
856
|
+
* un-inherited. That is why an essential animation's period is not a
|
|
857
|
+
* `--duration-*` token — @see `--shimmer-period`.
|
|
858
|
+
*/
|
|
859
|
+
@media (prefers-reduced-motion: reduce) {
|
|
860
|
+
:root {
|
|
861
|
+
--duration-instant: 1ms;
|
|
862
|
+
--duration-fast: 1ms;
|
|
863
|
+
--duration-base: 1ms;
|
|
864
|
+
--duration-slow: 1ms;
|
|
865
|
+
--duration-stream: 1ms;
|
|
866
|
+
--duration-arrive: 1ms;
|
|
867
|
+
/* The stagger is a delay, not a duration, and a collapsed animation with a
|
|
868
|
+
live delay still arrives as a visible cascade. */
|
|
869
|
+
--stagger-step: 0ms;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/* `::before` and `::after` are listed because `*` matches elements only, and
|
|
873
|
+
an animation declared on a pseudo-element is therefore invisible to the
|
|
874
|
+
floor. `.pulse-ring::before` in this very file drew its 2s ring infinitely
|
|
875
|
+
under the preference — measured in Chromium against the built stylesheet —
|
|
876
|
+
while every element around it was collapsed to 1ms. The exemption still
|
|
877
|
+
applies: a pseudo-element of an essential element, or of a descendant of
|
|
878
|
+
one, is not floored. */
|
|
879
|
+
*:where(:not([data-motion="essential"], [data-motion="essential"] *)),
|
|
880
|
+
*:where(:not([data-motion="essential"], [data-motion="essential"] *))::before,
|
|
881
|
+
*:where(:not([data-motion="essential"], [data-motion="essential"] *))::after {
|
|
882
|
+
animation-duration: 1ms !important;
|
|
883
|
+
animation-iteration-count: 1 !important;
|
|
884
|
+
transition-duration: 1ms !important;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
html {
|
|
888
|
+
scroll-behavior: auto !important;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/* Light theme overrides — solid surfaces, no glassmorphism */
|
|
893
|
+
[data-sandbox-theme="vault"] .glass-panel {
|
|
894
|
+
background: var(--md3-surface-container);
|
|
895
|
+
backdrop-filter: none;
|
|
896
|
+
-webkit-backdrop-filter: none;
|
|
897
|
+
border-color: var(--md3-outline-variant);
|
|
898
|
+
box-shadow: var(--shadow-card);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
[data-sandbox-theme="vault"] .glass-panel:hover {
|
|
902
|
+
border-color: var(--md3-outline);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
[data-sandbox-theme="vault"] .glass-panel-heavy {
|
|
906
|
+
background: var(--md3-surface-container-high);
|
|
907
|
+
backdrop-filter: none;
|
|
908
|
+
-webkit-backdrop-filter: none;
|
|
909
|
+
border-color: var(--md3-outline-variant);
|
|
910
|
+
box-shadow: var(--shadow-card);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
[data-sandbox-theme="vault"] .glow-primary {
|
|
914
|
+
box-shadow: none;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
[data-sandbox-theme="vault"] .glow-primary:hover {
|
|
918
|
+
box-shadow: none;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/* Markdown prose — styles the `tangle-prose` surface that @tangle-network/ui's
|
|
922
|
+
* Markdown renderer emits (re-exported from this library's /markdown, /chat and
|
|
923
|
+
* /run entries). ui ships no @tailwindcss/typography, so without these rules
|
|
924
|
+
* headings, lists, tables, and block spacing carry no styling in consumers that
|
|
925
|
+
* source prose CSS solely from @tangle-network/sandbox-ui/styles. Self-contained
|
|
926
|
+
* and token-driven so it re-skins with host, light, vault, and dark colors. */
|
|
927
|
+
.tangle-prose {
|
|
928
|
+
color: hsl(var(--foreground));
|
|
929
|
+
line-height: 1.7;
|
|
930
|
+
}
|
|
931
|
+
/* Zero the browser's default block margins at zero specificity (:where) so the
|
|
932
|
+
* owl selector below owns all vertical rhythm — a plain `.tangle-prose p` reset
|
|
933
|
+
* (0,1,1) would outrank the owl (0,1,0) and collapse the gap between blocks. */
|
|
934
|
+
.tangle-prose :where(p, ul, ol) { margin: 0; }
|
|
935
|
+
.tangle-prose > * + * { margin-top: 0.85em; }
|
|
936
|
+
.tangle-prose h1, .tangle-prose h2, .tangle-prose h3, .tangle-prose h4 {
|
|
937
|
+
font-weight: 600;
|
|
938
|
+
line-height: 1.3;
|
|
939
|
+
margin-top: 1.4em;
|
|
940
|
+
margin-bottom: 0.5em;
|
|
941
|
+
color: inherit;
|
|
942
|
+
}
|
|
943
|
+
.tangle-prose h1 { font-size: 1.4em; }
|
|
944
|
+
.tangle-prose h2 { font-size: 1.25em; }
|
|
945
|
+
.tangle-prose h3 { font-size: 1.1em; }
|
|
946
|
+
.tangle-prose ul, .tangle-prose ol { padding-left: 1.4em; }
|
|
947
|
+
.tangle-prose li { margin: 0.25em 0; }
|
|
948
|
+
.tangle-prose li > ul, .tangle-prose li > ol { margin-top: 0.25em; }
|
|
949
|
+
.tangle-prose a {
|
|
950
|
+
color: var(--accent-text);
|
|
951
|
+
text-decoration: underline;
|
|
952
|
+
text-underline-offset: 2px;
|
|
953
|
+
}
|
|
954
|
+
.tangle-prose blockquote {
|
|
955
|
+
border-left: 2px solid var(--border-default);
|
|
956
|
+
padding-left: 0.9em;
|
|
957
|
+
color: hsl(var(--muted-foreground));
|
|
958
|
+
}
|
|
959
|
+
.tangle-prose hr { border: 0; border-top: 1px solid var(--border-subtle); margin: 1.2em 0; }
|
|
960
|
+
.tangle-prose :not(pre) > code {
|
|
961
|
+
background: var(--md3-surface-container-high);
|
|
962
|
+
padding: 0.12em 0.36em;
|
|
963
|
+
border-radius: 4px;
|
|
964
|
+
font-size: 0.9em;
|
|
965
|
+
font-family: var(--font-mono);
|
|
966
|
+
}
|
|
967
|
+
.tangle-prose pre { margin: 0.85em 0; }
|
|
968
|
+
|
|
969
|
+
/* Tables — hairline dividers + real cell padding, so columns read as a grid
|
|
970
|
+
* instead of colliding. `display: block` + `overflow-x` lets wide tables scroll
|
|
971
|
+
* on narrow viewports; that also makes `border-collapse` inert (it only applies
|
|
972
|
+
* to `display: table`), so dividers come from the per-cell `border-top` /
|
|
973
|
+
* `border-left` and the `thead` underline below — not from border collapsing. */
|
|
974
|
+
.tangle-prose table {
|
|
975
|
+
width: 100%;
|
|
976
|
+
margin: 0.85em 0;
|
|
977
|
+
font-size: 0.95em;
|
|
978
|
+
display: block;
|
|
979
|
+
overflow-x: auto;
|
|
980
|
+
color: inherit;
|
|
981
|
+
}
|
|
982
|
+
.tangle-prose thead { border-bottom: 1px solid var(--border-default); }
|
|
983
|
+
.tangle-prose th {
|
|
984
|
+
text-align: left;
|
|
985
|
+
font-weight: 600;
|
|
986
|
+
padding: 0.5em 0.85em;
|
|
987
|
+
color: inherit;
|
|
988
|
+
white-space: nowrap;
|
|
989
|
+
}
|
|
990
|
+
.tangle-prose td {
|
|
991
|
+
padding: 0.5em 0.85em;
|
|
992
|
+
border-top: 1px solid var(--border-subtle);
|
|
993
|
+
vertical-align: top;
|
|
994
|
+
}
|
|
995
|
+
.tangle-prose th:not(:first-child), .tangle-prose td:not(:first-child) {
|
|
996
|
+
border-left: 1px solid var(--border-subtle);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
/* Published sources: this package's dist and its @tangle-network/ui peer. */
|
|
1000
|
+
@source "./**/*.js";
|
|
1001
|
+
@source "../../ui/dist/**/*.js";
|
|
1002
|
+
@source "../node_modules/@tangle-network/ui/dist/**/*.js";
|