@loomweaver/shell 0.7.2
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 +49 -0
- package/fesm2022/loomweaver-shell.mjs +17016 -0
- package/fesm2022/loomweaver-shell.mjs.map +1 -0
- package/i18n/de.json +293 -0
- package/i18n/en.json +293 -0
- package/package.json +68 -0
- package/styles/shell.css +9 -0
- package/styles/theme.css +709 -0
- package/types/loomweaver-shell.d.ts +2280 -0
- package/types/loomweaver-shell.d.ts.map +1 -0
package/styles/theme.css
ADDED
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
/* LoomWeaver design tokens (seed of the primitive → semantic token system, see design doc).
|
|
2
|
+
Semantic Tailwind colors map onto the --lw-* ladder; :root / :root.dark flip the ladder.
|
|
3
|
+
Components use ONLY semantic tokens (bg-surface, text-content, text-brand …), never raw hex.
|
|
4
|
+
ThemeService toggles the `dark` class on <html> (light / dark / system).
|
|
5
|
+
Shared by every distribution — imported from each app's styles.css. */
|
|
6
|
+
|
|
7
|
+
/* Token precedence: later layer wins → Product default < Plugin < Tenant. Product tokens
|
|
8
|
+
live in lw-base; ctx.contributeTheme injects into lw-plugin-theme; a tenant/distribution theme uses
|
|
9
|
+
lw-tenant-theme. Declared here so runtime-injected styles slot in by layer, not by source order. */
|
|
10
|
+
@layer lw-base, lw-plugin-theme, lw-tenant-theme;
|
|
11
|
+
|
|
12
|
+
@theme inline {
|
|
13
|
+
--color-brand: var(--lw-brand);
|
|
14
|
+
--color-brand-strong: var(--lw-brand-strong);
|
|
15
|
+
--color-brand-text: var(--lw-brand-text);
|
|
16
|
+
--color-brand-fill: var(--lw-brand-fill);
|
|
17
|
+
--color-on-brand: var(--lw-on-brand);
|
|
18
|
+
--color-accent: var(--lw-accent);
|
|
19
|
+
--color-accent-strong: var(--lw-accent-strong);
|
|
20
|
+
|
|
21
|
+
--color-surface: var(--lw-surface);
|
|
22
|
+
--color-surface-raised: var(--lw-surface-raised);
|
|
23
|
+
--color-surface-overlay: var(--lw-surface-overlay);
|
|
24
|
+
--color-field: var(--lw-field);
|
|
25
|
+
--color-border: var(--lw-border);
|
|
26
|
+
|
|
27
|
+
--color-content: var(--lw-content);
|
|
28
|
+
--color-content-muted: var(--lw-content-muted);
|
|
29
|
+
--color-content-faint: var(--lw-content-faint);
|
|
30
|
+
|
|
31
|
+
--color-tooltip: var(--lw-tooltip);
|
|
32
|
+
--color-tooltip-content: var(--lw-tooltip-content);
|
|
33
|
+
|
|
34
|
+
--color-positive: var(--lw-positive);
|
|
35
|
+
--color-on-positive: var(--lw-on-positive);
|
|
36
|
+
--color-negative: var(--lw-negative);
|
|
37
|
+
--color-negative-fill: var(--lw-negative-fill);
|
|
38
|
+
--color-on-negative: var(--lw-on-negative);
|
|
39
|
+
--color-caution: var(--lw-caution);
|
|
40
|
+
--color-on-caution: var(--lw-on-caution);
|
|
41
|
+
--color-info: var(--lw-info);
|
|
42
|
+
--color-on-info: var(--lw-on-info);
|
|
43
|
+
|
|
44
|
+
--color-scrim: var(--lw-scrim);
|
|
45
|
+
|
|
46
|
+
--font-sans: var(--lw-font-sans);
|
|
47
|
+
--font-mono: var(--lw-font-mono);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/* Tailwind Typography (`prose`) mapped onto our tokens — used by <lw-markdown>. Because the
|
|
51
|
+
--lw-* ladder flips in :root.dark, prose adopts dark mode automatically (no prose-invert). */
|
|
52
|
+
@utility prose-lw {
|
|
53
|
+
--tw-prose-body: var(--lw-content);
|
|
54
|
+
--tw-prose-headings: var(--lw-content);
|
|
55
|
+
--tw-prose-lead: var(--lw-content-muted);
|
|
56
|
+
--tw-prose-links: var(--lw-brand);
|
|
57
|
+
--tw-prose-bold: var(--lw-content);
|
|
58
|
+
--tw-prose-counters: var(--lw-content-muted);
|
|
59
|
+
--tw-prose-bullets: var(--lw-border);
|
|
60
|
+
--tw-prose-hr: var(--lw-border);
|
|
61
|
+
--tw-prose-quotes: var(--lw-content);
|
|
62
|
+
--tw-prose-quote-borders: var(--lw-border);
|
|
63
|
+
--tw-prose-captions: var(--lw-content-muted);
|
|
64
|
+
--tw-prose-code: var(--lw-content);
|
|
65
|
+
--tw-prose-pre-code: var(--lw-content);
|
|
66
|
+
--tw-prose-pre-bg: var(--lw-surface-overlay);
|
|
67
|
+
--tw-prose-th-borders: var(--lw-border);
|
|
68
|
+
--tw-prose-td-borders: var(--lw-border);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@layer lw-base {
|
|
72
|
+
/* Light (default) — brand = LoomWeaver blue, accent = gold. */
|
|
73
|
+
:root {
|
|
74
|
+
color-scheme: light;
|
|
75
|
+
|
|
76
|
+
--lw-brand: #2e96c9;
|
|
77
|
+
--lw-brand-strong: #2680b0;
|
|
78
|
+
/* Brand blue used as *text*: a deeper shade so it stays WCAG AA (>=4.5:1) on light surfaces.
|
|
79
|
+
The #2e96c9 brand stays for the logo, fills, borders and icons (non-text, 3:1). */
|
|
80
|
+
--lw-brand-text: #256f92;
|
|
81
|
+
/* Brand blue used as a *filled* surface with on-brand text (e.g. the primary button): a deeper
|
|
82
|
+
shade so white label text stays WCAG AA. The #2e96c9 brand stays as the identity colour. */
|
|
83
|
+
--lw-brand-fill: #1d7099;
|
|
84
|
+
--lw-on-brand: #ffffff;
|
|
85
|
+
--lw-accent: #c59a2f;
|
|
86
|
+
--lw-accent-strong: #a8821f;
|
|
87
|
+
|
|
88
|
+
--lw-surface: #f7f9fb;
|
|
89
|
+
--lw-surface-raised: #ffffff;
|
|
90
|
+
--lw-surface-overlay: #eef2f6;
|
|
91
|
+
--lw-field: #ffffff;
|
|
92
|
+
--lw-border: #e2e8f0;
|
|
93
|
+
|
|
94
|
+
--lw-content: #1f2933;
|
|
95
|
+
--lw-content-muted: #5b6b7a;
|
|
96
|
+
/* faint stays WCAG AA (>=4.5:1) on surface/raised/overlay — verified by the a11y E2E net. */
|
|
97
|
+
--lw-content-faint: #5f7082;
|
|
98
|
+
|
|
99
|
+
--lw-tooltip: #2b3542;
|
|
100
|
+
--lw-tooltip-content: #f7f9fb;
|
|
101
|
+
|
|
102
|
+
/* Status fills carry on-* label text (buttons) and double as icon colours. Depths are tuned so
|
|
103
|
+
the filled variant's label clears WCAG AA (>=4.5:1) — verified by the a11y token audit. */
|
|
104
|
+
--lw-positive: #1f7a43;
|
|
105
|
+
--lw-on-positive: #ffffff;
|
|
106
|
+
--lw-negative: #d14343;
|
|
107
|
+
/* Danger *button* fill: deeper than --lw-negative so white text stays AA even on hover; the
|
|
108
|
+
lighter --lw-negative remains the error-text/icon colour (which needs surface contrast). */
|
|
109
|
+
--lw-negative-fill: #c62d2d;
|
|
110
|
+
--lw-on-negative: #ffffff;
|
|
111
|
+
--lw-caution: #b8841f;
|
|
112
|
+
/* Near-black on-caution so the warning button's label clears AA on the gold fill (which itself
|
|
113
|
+
stays #b8841f so the caution icon keeps 3:1 on light surfaces). */
|
|
114
|
+
--lw-on-caution: #0f1419;
|
|
115
|
+
--lw-info: #1d7099;
|
|
116
|
+
--lw-on-info: #ffffff;
|
|
117
|
+
|
|
118
|
+
/* Modal/drawer backdrop — semi-transparent, so it adapts if a tenant retints. */
|
|
119
|
+
--lw-scrim: rgb(0 0 0 / 0.4);
|
|
120
|
+
|
|
121
|
+
/* Typography — themeable via ctx.contributeTheme / tenant CSS. Font family is a brand
|
|
122
|
+
lever, so it lives on :root (both schemes) and rarely differs light/dark. Font *size* is a user
|
|
123
|
+
preference (the shell text-size setting), not a theme token. */
|
|
124
|
+
--lw-font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
125
|
+
--lw-font-mono: ui-monospace, 'SF Mono', 'Cascadia Code', 'Roboto Mono', monospace;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Dark — surfaces in cool near-black, lighter blue brand so it stays legible. */
|
|
129
|
+
:root.dark {
|
|
130
|
+
color-scheme: dark;
|
|
131
|
+
|
|
132
|
+
--lw-brand: #4aa9d6;
|
|
133
|
+
--lw-brand-strong: #6cbce0;
|
|
134
|
+
/* On dark surfaces the brand blue already clears AA as text/fill, so both mirror the brand. */
|
|
135
|
+
--lw-brand-text: #4aa9d6;
|
|
136
|
+
--lw-brand-fill: #4aa9d6;
|
|
137
|
+
--lw-on-brand: #0f1419;
|
|
138
|
+
--lw-accent: #d9b250;
|
|
139
|
+
--lw-accent-strong: #e6c878;
|
|
140
|
+
|
|
141
|
+
--lw-surface: #14191f;
|
|
142
|
+
--lw-surface-raised: #1b222a;
|
|
143
|
+
--lw-surface-overlay: #232c36;
|
|
144
|
+
--lw-field: #1b222a;
|
|
145
|
+
--lw-border: #2d3743;
|
|
146
|
+
|
|
147
|
+
--lw-content: #e6edf3;
|
|
148
|
+
--lw-content-muted: #9aa7b4;
|
|
149
|
+
/* faint stays WCAG AA (>=4.5:1) on dark surface/raised/overlay — verified by the a11y E2E net. */
|
|
150
|
+
--lw-content-faint: #8695a4;
|
|
151
|
+
|
|
152
|
+
--lw-tooltip: #e6edf3;
|
|
153
|
+
--lw-tooltip-content: #14191f;
|
|
154
|
+
|
|
155
|
+
--lw-positive: #46c178;
|
|
156
|
+
--lw-on-positive: #0f1419;
|
|
157
|
+
--lw-negative: #e06464;
|
|
158
|
+
/* Danger button fill: deeper than the lighter --lw-negative (which stays the error-text colour,
|
|
159
|
+
needing contrast against the dark surface) so white button text clears AA. */
|
|
160
|
+
--lw-negative-fill: #c62d2d;
|
|
161
|
+
--lw-on-negative: #ffffff;
|
|
162
|
+
--lw-caution: #d9b250;
|
|
163
|
+
--lw-on-caution: #14191f;
|
|
164
|
+
--lw-info: #4aa9d6;
|
|
165
|
+
--lw-on-info: #0f1419;
|
|
166
|
+
|
|
167
|
+
/* Darker on the near-black dark surfaces so the backdrop still reads. */
|
|
168
|
+
--lw-scrim: rgb(0 0 0 / 0.6);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* ─────────────────────────────────────────────────────────────────────────────
|
|
173
|
+
`.lw-btn` — the host button as a CSS-class contract.
|
|
174
|
+
The button LOOK lives here (one source of truth); the `LwButton` Angular directive
|
|
175
|
+
is just a thin wrapper that emits these class names. SDK-only plugins (scope:weaver)
|
|
176
|
+
cannot import the directive across the Nx boundary, but they MAY consume the look via
|
|
177
|
+
these class names — `<button class="lw-btn lw-btn--primary">` — because a CSS class is
|
|
178
|
+
contract, not code. This is the general pattern for host `<lw-*>` primitives that plugins
|
|
179
|
+
should be able to reuse. Uses only semantic tokens, so it adopts any theme/tenant override.
|
|
180
|
+
Keep the modifier names in sync with LwButtonVariant / LwButtonSize in @loomweaver/plugin-sdk. */
|
|
181
|
+
@layer components {
|
|
182
|
+
.lw-btn {
|
|
183
|
+
@apply inline-flex cursor-pointer items-center justify-center gap-1.5 rounded-md font-medium
|
|
184
|
+
transition-colors focus-visible:outline-2 focus-visible:outline-offset-2
|
|
185
|
+
focus-visible:outline-brand;
|
|
186
|
+
/* md is the DEFAULT size, baked into the base — so `lw-btn lw-btn--<variant>` alone is a
|
|
187
|
+
correctly-sized button and a plugin author can't forget a size. `lw-btn--sm` overrides. */
|
|
188
|
+
@apply h-9 px-3 text-sm;
|
|
189
|
+
}
|
|
190
|
+
.lw-btn:disabled,
|
|
191
|
+
.lw-btn[aria-disabled='true'] {
|
|
192
|
+
@apply pointer-events-none opacity-50;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* variants (visual weight) */
|
|
196
|
+
.lw-btn--primary {
|
|
197
|
+
@apply bg-brand-fill text-on-brand hover:opacity-90;
|
|
198
|
+
}
|
|
199
|
+
.lw-btn--default {
|
|
200
|
+
@apply border border-border bg-surface-raised text-content hover:bg-surface-overlay;
|
|
201
|
+
}
|
|
202
|
+
.lw-btn--success {
|
|
203
|
+
@apply bg-positive text-on-positive hover:opacity-90;
|
|
204
|
+
}
|
|
205
|
+
.lw-btn--danger {
|
|
206
|
+
@apply bg-negative-fill text-on-negative hover:opacity-90;
|
|
207
|
+
}
|
|
208
|
+
.lw-btn--warning {
|
|
209
|
+
@apply bg-caution text-on-caution hover:opacity-90;
|
|
210
|
+
}
|
|
211
|
+
.lw-btn--info {
|
|
212
|
+
@apply bg-info text-on-info hover:opacity-90;
|
|
213
|
+
}
|
|
214
|
+
.lw-btn--ghost {
|
|
215
|
+
@apply text-content-muted hover:bg-surface-overlay hover:text-content;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* smaller size — overrides the base (md) geometry */
|
|
219
|
+
.lw-btn--sm {
|
|
220
|
+
@apply h-7 px-2 text-xs;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/* icon-only: square (width follows the size's height via aspect-ratio), no horizontal
|
|
224
|
+
padding. Declared AFTER the sizes so its px-0 wins over the size's padding. */
|
|
225
|
+
.lw-btn--icon {
|
|
226
|
+
@apply aspect-square px-0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* `.lw-icon-btn` — the quiet chrome icon-button core shared by every host toolbar/strip control
|
|
230
|
+
(pane toolbar, tab-strip trailing controls, sidebar header, panel actions). Size and radius stay
|
|
231
|
+
per-site utilities (`h-6 w-6`, `rounded-md`, …); tone/hover live here once. */
|
|
232
|
+
.lw-icon-btn {
|
|
233
|
+
@apply relative flex shrink-0 items-center justify-center rounded text-content-faint transition-colors hover:bg-surface-overlay hover:text-content;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/* `.lw-rail-initials` — one or two letters drawn where a rail item would draw its icon, for an entry
|
|
237
|
+
the user named (a saved workspace). Sized and weighted to sit beside 16px outline icons of stroke
|
|
238
|
+
width 1.5 without shouting: measured against the neighbouring icons at 3x, weight 500 read visibly
|
|
239
|
+
heavier than their strokes, so the badge is normal weight. Colour is
|
|
240
|
+
inherited, so hover and the active state behave exactly as they do for an icon. Uses the app font
|
|
241
|
+
token, so a product that changes its typeface takes the badge with it. */
|
|
242
|
+
.lw-rail-initials {
|
|
243
|
+
@apply select-none font-sans text-[0.75rem] leading-none font-normal tracking-tight uppercase;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* `.lw-segmented` — a segmented single-choice control (theme mode, text size) as a CSS-class contract
|
|
247
|
+
(like `.lw-btn`): a native `<fieldset>` of native `<button>`s, so semantics and
|
|
248
|
+
keyboard come for free. Put `.lw-segmented-item` on each button and drive the selected look from its
|
|
249
|
+
`aria-pressed` — the accessible state IS the styling hook, so the two can never drift apart.
|
|
250
|
+
`h-8.5` is the shared bar-control height: `.lw-segmented` and `.lw-select-trigger` pin the same
|
|
251
|
+
utility so controls sitting side by side in a bar can never drift apart in height. */
|
|
252
|
+
.lw-segmented {
|
|
253
|
+
@apply m-0 inline-flex h-8.5 items-center gap-0.5 rounded-md border border-border bg-surface-overlay p-0.5;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.lw-segmented-item {
|
|
257
|
+
@apply relative flex h-7 w-7 items-center justify-center rounded text-content-muted transition-colors hover:text-content;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.lw-segmented-item[aria-pressed='true'] {
|
|
261
|
+
@apply bg-surface-raised text-brand shadow-sm;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* `.lw-field` — the host text-input/textarea look as a CSS-class contract (like
|
|
265
|
+
`.lw-btn`): put it on a NATIVE `<input>` / `<textarea>` / `<input type="date">` and it themes via the
|
|
266
|
+
`--lw-*` tokens (mandant-/theme-reactive for free). A native control already carries full semantics,
|
|
267
|
+
keyboard and a11y, so — unlike `<lw-select>`/`<lw-button>` which wrap non-native elements — a text field
|
|
268
|
+
needs only styling, not a wrapping web component. Works in an Angular weaver body and (tokens permitting)
|
|
269
|
+
a sandboxed surface alike. `.lw-field--invalid` marks a validation error. */
|
|
270
|
+
.lw-field {
|
|
271
|
+
@apply block w-full rounded-md border border-border bg-field px-3 py-2 text-sm text-content
|
|
272
|
+
placeholder:text-content-faint transition-colors
|
|
273
|
+
focus-visible:border-brand focus-visible:outline-2 focus-visible:outline-offset-0 focus-visible:outline-brand
|
|
274
|
+
disabled:cursor-not-allowed disabled:opacity-60;
|
|
275
|
+
}
|
|
276
|
+
.lw-field--invalid {
|
|
277
|
+
@apply border-negative focus-visible:border-negative focus-visible:outline-negative;
|
|
278
|
+
}
|
|
279
|
+
textarea.lw-field {
|
|
280
|
+
@apply min-h-20 resize-y;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* `.lw-checkbox` — the host checkbox look on a NATIVE `<input type="checkbox">` (class contract, like
|
|
284
|
+
`.lw-field`): `accent-color` themes the native tick/fill with the brand, keeping full native semantics
|
|
285
|
+
and a11y. Just styling, no wrapper WC (WC tranche ①). */
|
|
286
|
+
.lw-checkbox {
|
|
287
|
+
@apply h-4 w-4 shrink-0 cursor-pointer;
|
|
288
|
+
accent-color: var(--lw-brand);
|
|
289
|
+
}
|
|
290
|
+
.lw-checkbox:disabled {
|
|
291
|
+
@apply cursor-not-allowed opacity-60;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/* `.lw-progress` — a determinate progress BAR on a NATIVE `<progress value max>` (class contract, WC tranche
|
|
295
|
+
②): native `role="progressbar"` + a11y for free; only the fill/track need theming via the vendor pseudo-
|
|
296
|
+
elements. For a circular ring use `<lw-progress-ring>`; for indeterminate use `<lw-spinner>`. */
|
|
297
|
+
.lw-progress {
|
|
298
|
+
@apply h-2 w-full appearance-none overflow-hidden rounded-full border-0;
|
|
299
|
+
background: var(--lw-border);
|
|
300
|
+
}
|
|
301
|
+
.lw-progress::-webkit-progress-bar {
|
|
302
|
+
background: var(--lw-border);
|
|
303
|
+
}
|
|
304
|
+
.lw-progress::-webkit-progress-value {
|
|
305
|
+
background: var(--lw-brand);
|
|
306
|
+
border-radius: 9999px;
|
|
307
|
+
}
|
|
308
|
+
.lw-progress::-moz-progress-bar {
|
|
309
|
+
background: var(--lw-brand);
|
|
310
|
+
border-radius: 9999px;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/* `<lw-progress-ring>` — determinate circular progress: the LOOK lives here (tokened strokes),
|
|
314
|
+
the SVG geometry in the element. */
|
|
315
|
+
lw-progress-ring {
|
|
316
|
+
@apply inline-block align-middle;
|
|
317
|
+
}
|
|
318
|
+
.lw-progress-ring-svg {
|
|
319
|
+
@apply block h-full w-full;
|
|
320
|
+
}
|
|
321
|
+
.lw-progress-ring-track {
|
|
322
|
+
fill: none;
|
|
323
|
+
stroke: var(--lw-border);
|
|
324
|
+
stroke-width: 3.5;
|
|
325
|
+
}
|
|
326
|
+
.lw-progress-ring-value {
|
|
327
|
+
fill: none;
|
|
328
|
+
stroke: var(--lw-brand);
|
|
329
|
+
stroke-width: 3.5;
|
|
330
|
+
stroke-linecap: round;
|
|
331
|
+
transition: stroke-dashoffset 300ms ease;
|
|
332
|
+
}
|
|
333
|
+
.lw-progress-ring-text {
|
|
334
|
+
fill: var(--lw-content);
|
|
335
|
+
font-size: 10px;
|
|
336
|
+
font-weight: 600;
|
|
337
|
+
text-anchor: middle;
|
|
338
|
+
dominant-baseline: central;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* `.lw-divider` — a thematic separator on a NATIVE `<hr>` (class contract, WC tranche ④): `<hr>` already
|
|
342
|
+
carries `role="separator"`; this only tokens the line. Add `.lw-divider--vertical` on an `<hr>` inside a
|
|
343
|
+
flex row for a vertical rule. */
|
|
344
|
+
.lw-divider {
|
|
345
|
+
@apply border-0 border-t border-border;
|
|
346
|
+
margin: 0;
|
|
347
|
+
}
|
|
348
|
+
.lw-divider--vertical {
|
|
349
|
+
@apply h-full self-stretch border-t-0 border-l border-border;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* `.lw-collapsible` — a disclosure/accordion on a NATIVE `<details>`/`<summary>` (class contract, WC tranche
|
|
353
|
+
④): native open/close, keyboard (Enter/Space on the focusable summary) and a11y for free — no JS. This
|
|
354
|
+
tokens the header + content and swaps the default marker for a chevron that rotates when `[open]`. */
|
|
355
|
+
.lw-collapsible {
|
|
356
|
+
@apply overflow-hidden rounded-lg border border-border;
|
|
357
|
+
}
|
|
358
|
+
.lw-collapsible > summary {
|
|
359
|
+
/* gap-3 (not gap-2) so the chevron keeps air from the label even when the summary is shrink-to-fit
|
|
360
|
+
(width:auto) and `ml-auto` has no free space to consume. */
|
|
361
|
+
@apply flex cursor-pointer list-none items-center gap-3 bg-surface-raised px-4 py-2.5 text-sm
|
|
362
|
+
font-medium text-content transition-colors hover:bg-surface-overlay
|
|
363
|
+
focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-brand;
|
|
364
|
+
}
|
|
365
|
+
.lw-collapsible > summary::-webkit-details-marker {
|
|
366
|
+
display: none;
|
|
367
|
+
}
|
|
368
|
+
.lw-collapsible > summary::after {
|
|
369
|
+
content: '';
|
|
370
|
+
@apply ml-auto h-2 w-2 shrink-0 border-r-2 border-b-2 border-content-muted transition-transform;
|
|
371
|
+
/* Border-trick chevron: the visible "ink" is the box's bottom-right corner, so it reads a touch low —
|
|
372
|
+
nudge it up ~1px to look vertically centred (and down ~1px when flipped up on [open]). */
|
|
373
|
+
transform: translateY(-1px) rotate(45deg); /* chevron pointing down */
|
|
374
|
+
}
|
|
375
|
+
.lw-collapsible[open] > summary::after {
|
|
376
|
+
transform: translateY(1px) rotate(-135deg); /* …flips up when open */
|
|
377
|
+
}
|
|
378
|
+
.lw-collapsible-body {
|
|
379
|
+
@apply border-t border-border px-4 py-3 text-sm text-content-muted;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* `.lw-badge` — a small status/count pill on a `<span>` (class contract; pure presentation, no WC). The
|
|
383
|
+
base is a neutral chip; tone modifiers reuse the semantic fills. */
|
|
384
|
+
.lw-badge {
|
|
385
|
+
@apply inline-flex shrink-0 items-center justify-center gap-1 whitespace-nowrap rounded-full
|
|
386
|
+
bg-surface-overlay px-2 py-0.5 text-xs font-medium text-content-muted;
|
|
387
|
+
min-width: 1.5rem;
|
|
388
|
+
}
|
|
389
|
+
.lw-badge--brand {
|
|
390
|
+
@apply bg-brand-fill text-on-brand;
|
|
391
|
+
}
|
|
392
|
+
.lw-badge--success {
|
|
393
|
+
@apply bg-positive text-on-positive;
|
|
394
|
+
}
|
|
395
|
+
.lw-badge--danger {
|
|
396
|
+
@apply bg-negative-fill text-on-negative;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/* `.lw-radio` — the host radio look on a NATIVE `<input type="radio">` (class contract, like `.lw-checkbox`):
|
|
400
|
+
`accent-color` themes the native dot with the brand; native name-grouping keeps arrow-key + a11y. */
|
|
401
|
+
.lw-radio {
|
|
402
|
+
@apply h-4 w-4 shrink-0 cursor-pointer;
|
|
403
|
+
accent-color: var(--lw-brand);
|
|
404
|
+
}
|
|
405
|
+
.lw-radio:disabled {
|
|
406
|
+
@apply cursor-not-allowed opacity-60;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/* `.lw-range` — a slider on a NATIVE `<input type="range">` (class contract, WC tranche ③): the track is the
|
|
410
|
+
(appearance-reset) input, the thumb its vendor pseudo-element. Native → full keyboard + a11y, no WC. */
|
|
411
|
+
.lw-range {
|
|
412
|
+
@apply h-2 w-full cursor-pointer appearance-none rounded-full bg-border
|
|
413
|
+
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand;
|
|
414
|
+
}
|
|
415
|
+
.lw-range::-webkit-slider-thumb {
|
|
416
|
+
-webkit-appearance: none;
|
|
417
|
+
@apply h-4 w-4 rounded-full bg-brand shadow-sm;
|
|
418
|
+
}
|
|
419
|
+
.lw-range::-moz-range-thumb {
|
|
420
|
+
@apply h-4 w-4 rounded-full border-0 bg-brand shadow-sm;
|
|
421
|
+
}
|
|
422
|
+
.lw-range:disabled {
|
|
423
|
+
@apply cursor-not-allowed opacity-60;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/* `.lw-switch` — an on/off toggle drawn from a NATIVE `<input type="checkbox" role="switch">`: the track is
|
|
427
|
+
the (appearance-reset) input, the knob is its `::before`. Class contract → themes via tokens; native
|
|
428
|
+
control keeps keyboard + a11y (add `role="switch"` in markup for the switch semantic). */
|
|
429
|
+
.lw-switch {
|
|
430
|
+
@apply relative h-5 w-9 shrink-0 cursor-pointer appearance-none rounded-full bg-border transition-colors
|
|
431
|
+
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand;
|
|
432
|
+
}
|
|
433
|
+
.lw-switch::before {
|
|
434
|
+
content: '';
|
|
435
|
+
@apply absolute top-0.5 left-0.5 h-4 w-4 rounded-full bg-surface shadow-sm transition-transform;
|
|
436
|
+
}
|
|
437
|
+
.lw-switch:checked {
|
|
438
|
+
@apply bg-brand;
|
|
439
|
+
}
|
|
440
|
+
.lw-switch:checked::before {
|
|
441
|
+
@apply translate-x-4;
|
|
442
|
+
}
|
|
443
|
+
.lw-switch:disabled {
|
|
444
|
+
@apply cursor-not-allowed opacity-60;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/* `.lw-scrim` — the one central backdrop treatment behind any modal surface (dialogs, the
|
|
448
|
+
compact overlay drawer). Combines the `--lw-scrim` dim with a blur so the treatment lives
|
|
449
|
+
in ONE place instead of being spelled out per element. Callers add their own layout/animation
|
|
450
|
+
(e.g. `transition-opacity` on the drawer); this class owns only the appearance. */
|
|
451
|
+
.lw-scrim {
|
|
452
|
+
@apply bg-scrim backdrop-blur-xs;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* The CDK's live region for screen-reader announcements ("… moved to the right activity bar"). It is
|
|
456
|
+
announced, never seen, so it has to be removed from the layout without being removed from the
|
|
457
|
+
accessibility tree: `display: none` or `visibility: hidden` would silence it. The rule is the CDK's
|
|
458
|
+
own `a11y-prebuilt.css`, carried here so that a consumer gets it from our stylesheet alone. */
|
|
459
|
+
.cdk-visually-hidden {
|
|
460
|
+
position: absolute;
|
|
461
|
+
left: 0;
|
|
462
|
+
width: 1px;
|
|
463
|
+
height: 1px;
|
|
464
|
+
margin: -1px;
|
|
465
|
+
padding: 0;
|
|
466
|
+
border: 0;
|
|
467
|
+
overflow: hidden;
|
|
468
|
+
clip: rect(0 0 0 0);
|
|
469
|
+
white-space: nowrap;
|
|
470
|
+
outline: 0;
|
|
471
|
+
appearance: none;
|
|
472
|
+
}
|
|
473
|
+
[dir='rtl'] .cdk-visually-hidden {
|
|
474
|
+
left: auto;
|
|
475
|
+
right: 0;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* Drag-reorder feedback via Angular CDK drag-drop: the floating preview under the cursor,
|
|
479
|
+
the gap the item leaves behind, and the smooth shift of the other items as it sorts. */
|
|
480
|
+
.cdk-drag-preview {
|
|
481
|
+
@apply rounded-md border border-border bg-surface-raised shadow-lg;
|
|
482
|
+
}
|
|
483
|
+
.cdk-drag-placeholder {
|
|
484
|
+
@apply opacity-30;
|
|
485
|
+
}
|
|
486
|
+
.cdk-drag-animating,
|
|
487
|
+
.cdk-drop-list-dragging .cdk-drag:not(.cdk-drag-placeholder) {
|
|
488
|
+
transition: transform 200ms cubic-bezier(0, 0, 0.2, 1);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/* Pane edge drop zones: while a tab is dragged, every pane offers four
|
|
492
|
+
edge zones (drop = split WITH the tab). The zone is only the HIT area; hovering it lights up the
|
|
493
|
+
separate `.lw-pane-drop-preview` child, which spans the exact half of the pane the new group would
|
|
494
|
+
occupy (top/bottom = full width, left/right = full height — Obsidian behaviour). A pane holding no
|
|
495
|
+
tabs offers one zone spanning the whole pane instead, because splitting against it would only make
|
|
496
|
+
an empty half. CDK marks the hovered list `.cdk-drop-list-dragging`. Geometry lives in the
|
|
497
|
+
component; look lives here. */
|
|
498
|
+
.lw-pane-drop-preview {
|
|
499
|
+
@apply pointer-events-none absolute rounded-sm border border-dashed border-transparent transition-colors;
|
|
500
|
+
}
|
|
501
|
+
.lw-pane-drop-zone.cdk-drop-list-dragging .lw-pane-drop-preview {
|
|
502
|
+
@apply border-brand bg-brand/15;
|
|
503
|
+
}
|
|
504
|
+
.lw-pane-drop-preview--left {
|
|
505
|
+
inset: 0 50% 0 0;
|
|
506
|
+
}
|
|
507
|
+
.lw-pane-drop-preview--right {
|
|
508
|
+
inset: 0 0 0 50%;
|
|
509
|
+
}
|
|
510
|
+
.lw-pane-drop-preview--top {
|
|
511
|
+
inset: 0 0 50% 0;
|
|
512
|
+
}
|
|
513
|
+
.lw-pane-drop-preview--bottom {
|
|
514
|
+
inset: 50% 0 0 0;
|
|
515
|
+
}
|
|
516
|
+
.lw-pane-drop-preview--fill {
|
|
517
|
+
inset: 0;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/* Minimize/restore easing: the pane's flex (its share of the split) glides instead of snapping.
|
|
521
|
+
The parent turns this OFF while the splitter is being dragged (see `PaneTreeView`), so a live
|
|
522
|
+
resize stays 1:1 with the pointer. Reduced-motion collapses it via the global rule below. */
|
|
523
|
+
.lw-pane-animate {
|
|
524
|
+
transition:
|
|
525
|
+
flex-grow 160ms ease,
|
|
526
|
+
flex-basis 160ms ease;
|
|
527
|
+
}
|
|
528
|
+
.lw-pane-minimized-strip {
|
|
529
|
+
animation: lw-fade-in 160ms ease-out;
|
|
530
|
+
overflow: hidden;
|
|
531
|
+
}
|
|
532
|
+
lw-content-grid.fixed {
|
|
533
|
+
animation: lw-maximize-in 140ms ease-out;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
@keyframes lw-fade-in {
|
|
537
|
+
from {
|
|
538
|
+
opacity: 0;
|
|
539
|
+
}
|
|
540
|
+
to {
|
|
541
|
+
opacity: 1;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
@keyframes lw-maximize-in {
|
|
545
|
+
from {
|
|
546
|
+
opacity: 0.4;
|
|
547
|
+
}
|
|
548
|
+
to {
|
|
549
|
+
opacity: 1;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/* `<lw-tooltip>` — the host tooltip primitive. The element (lw-tooltip.element.ts) renders
|
|
554
|
+
the bubble, reveals it (JS `showPopover`) and positions it (JS `left`/`top`); its LOOK lives here so
|
|
555
|
+
it themes via tokens and crosses the Nx/iframe boundary as contract, not code — the same pattern as
|
|
556
|
+
`.lw-btn`. Author it as the last child of a `position: relative` trigger; the element shows the bubble
|
|
557
|
+
on the trigger's hover/focus (mouse only). */
|
|
558
|
+
lw-tooltip {
|
|
559
|
+
position: absolute;
|
|
560
|
+
inset: 0;
|
|
561
|
+
pointer-events: none;
|
|
562
|
+
}
|
|
563
|
+
/* The bubble is a `[popover]` (top layer, #13): the UA hides it when closed, so no `display`/`opacity`
|
|
564
|
+
toggle here. The element sets `left`/`top` in JS (viewport coords) — reset the UA popover box
|
|
565
|
+
(centering border/inset/margin) so those win. */
|
|
566
|
+
.lw-tooltip-bubble {
|
|
567
|
+
@apply rounded-md bg-tooltip px-2 py-1 text-xs font-normal text-tooltip-content shadow-lg;
|
|
568
|
+
position: fixed;
|
|
569
|
+
inset: auto;
|
|
570
|
+
margin: 0;
|
|
571
|
+
border: 0;
|
|
572
|
+
width: max-content;
|
|
573
|
+
white-space: normal;
|
|
574
|
+
pointer-events: none;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/* While a menu is open, suppress hover tooltips — a right-click that pops the context menu should not
|
|
578
|
+
also pop the tab's tooltip. Hides any bubble that is currently open in the top layer. */
|
|
579
|
+
body.lw-menu-open .lw-tooltip-bubble {
|
|
580
|
+
display: none !important;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/* `<lw-select>` — the host single-value choice primitive (like `<lw-tooltip>`): behaviour in
|
|
584
|
+
lw-select.element.ts, LOOK here so it themes via tokens and crosses the Nx/iframe boundary as contract,
|
|
585
|
+
not code. The `<lw-option>` children are data holders — kept out of layout. */
|
|
586
|
+
lw-select {
|
|
587
|
+
@apply relative inline-block;
|
|
588
|
+
}
|
|
589
|
+
lw-select > lw-option {
|
|
590
|
+
display: none;
|
|
591
|
+
}
|
|
592
|
+
.lw-select-trigger {
|
|
593
|
+
@apply flex h-8.5 items-center gap-1.5 rounded-md border border-border bg-surface-overlay px-2 text-sm
|
|
594
|
+
text-content transition-colors hover:bg-surface-raised
|
|
595
|
+
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand
|
|
596
|
+
disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface-overlay;
|
|
597
|
+
}
|
|
598
|
+
.lw-select-value {
|
|
599
|
+
@apply flex items-center gap-1.5;
|
|
600
|
+
}
|
|
601
|
+
.lw-select-chevron {
|
|
602
|
+
@apply text-xs text-content-muted;
|
|
603
|
+
}
|
|
604
|
+
.lw-select-listbox {
|
|
605
|
+
@apply z-50 min-w-40 overflow-auto rounded-md border border-border bg-surface-overlay py-1 text-sm shadow-lg;
|
|
606
|
+
position: fixed;
|
|
607
|
+
position-area: block-end span-inline-end;
|
|
608
|
+
position-try-fallbacks: flip-block, flip-inline;
|
|
609
|
+
max-height: 16rem;
|
|
610
|
+
margin-top: 0.25rem;
|
|
611
|
+
}
|
|
612
|
+
.lw-select-option {
|
|
613
|
+
@apply flex cursor-pointer items-center gap-2 px-3 py-1.5 text-content transition-colors;
|
|
614
|
+
outline: none;
|
|
615
|
+
}
|
|
616
|
+
.lw-select-option:hover,
|
|
617
|
+
.lw-select-option.is-active {
|
|
618
|
+
@apply bg-surface-raised;
|
|
619
|
+
}
|
|
620
|
+
.lw-select-option[aria-selected='true'] {
|
|
621
|
+
@apply text-brand-text;
|
|
622
|
+
}
|
|
623
|
+
.lw-select-option[aria-disabled='true'] {
|
|
624
|
+
@apply pointer-events-none opacity-50;
|
|
625
|
+
}
|
|
626
|
+
.lw-select-glyph {
|
|
627
|
+
@apply text-base leading-none;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/* `<lw-menu>` — the host menu/context-menu primitive. Behaviour in lw-menu.element.ts, LOOK
|
|
631
|
+
here (tokened) so it themes and crosses the Nx/iframe boundary as contract. The MenuService sets
|
|
632
|
+
`left`/`top` at the cursor (clamped to the viewport) — a context menu anchors to a point, not an
|
|
633
|
+
element, so direct fixed positioning is simpler + more robust than CSS anchor positioning here. */
|
|
634
|
+
lw-menu {
|
|
635
|
+
@apply z-50 min-w-44 overflow-auto rounded-md border border-border bg-surface-overlay py-1 text-sm shadow-lg;
|
|
636
|
+
position: fixed;
|
|
637
|
+
max-height: 24rem;
|
|
638
|
+
outline: none;
|
|
639
|
+
}
|
|
640
|
+
lw-menu-item {
|
|
641
|
+
@apply flex cursor-pointer items-center gap-2 px-3 py-1.5 text-content transition-colors;
|
|
642
|
+
outline: none;
|
|
643
|
+
}
|
|
644
|
+
lw-menu-item:hover,
|
|
645
|
+
lw-menu-item:focus {
|
|
646
|
+
@apply bg-surface-raised;
|
|
647
|
+
}
|
|
648
|
+
lw-menu-item[aria-disabled='true'] {
|
|
649
|
+
@apply pointer-events-none opacity-50;
|
|
650
|
+
}
|
|
651
|
+
.lw-menu-item-label {
|
|
652
|
+
@apply flex-1 truncate;
|
|
653
|
+
}
|
|
654
|
+
/* Trailing keyboard-shortcut hint — muted, pushed to the row end. */
|
|
655
|
+
.lw-menu-item-shortcut {
|
|
656
|
+
@apply ml-4 shrink-0 text-xs text-content-muted;
|
|
657
|
+
}
|
|
658
|
+
/* Leading indicator column (icon, or a check on a checkbox item) — reserved (fixed width) only when the
|
|
659
|
+
menu actually has leading content, so plain menus stay flush-left (`.lw-menu--leading` set by the host). */
|
|
660
|
+
.lw-menu-item-lead {
|
|
661
|
+
@apply hidden w-4 shrink-0 items-center justify-center text-content-muted;
|
|
662
|
+
}
|
|
663
|
+
.lw-menu--leading .lw-menu-item-lead {
|
|
664
|
+
@apply flex;
|
|
665
|
+
}
|
|
666
|
+
.lw-menu-separator {
|
|
667
|
+
@apply my-1 border-t border-border;
|
|
668
|
+
height: 0;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/* Clickable controls show the pointer cursor (browsers default buttons to an arrow).
|
|
673
|
+
In the base layer so utilities like `cursor-default` (e.g. the drawer scrim) win. */
|
|
674
|
+
@layer base {
|
|
675
|
+
button:not(:disabled),
|
|
676
|
+
[role='button']:not([aria-disabled='true']) {
|
|
677
|
+
cursor: pointer;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
html,
|
|
682
|
+
body {
|
|
683
|
+
margin: 0;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
body {
|
|
687
|
+
font-family: var(--lw-font-sans);
|
|
688
|
+
background: var(--lw-surface);
|
|
689
|
+
color: var(--lw-content);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/* Respect the user's reduced-motion preference (WCAG 2.3.3 / 2.2.2): collapse non-essential
|
|
693
|
+
transitions and animations to near-zero. The loading spinner is exempt — a busy indicator is
|
|
694
|
+
essential status feedback, not decorative motion. */
|
|
695
|
+
@media (prefers-reduced-motion: reduce) {
|
|
696
|
+
*,
|
|
697
|
+
::before,
|
|
698
|
+
::after {
|
|
699
|
+
animation-duration: 0.01ms !important;
|
|
700
|
+
animation-iteration-count: 1 !important;
|
|
701
|
+
transition-duration: 0.01ms !important;
|
|
702
|
+
scroll-behavior: auto !important;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
.lw-spinner-ring {
|
|
706
|
+
animation-duration: 0.8s !important;
|
|
707
|
+
animation-iteration-count: infinite !important;
|
|
708
|
+
}
|
|
709
|
+
}
|