@crossworks/share-ui 0.230.43
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/LICENSE.md +135 -0
- package/package.json +68 -0
- package/src/app-bridge-protocol.ts +115 -0
- package/src/app-presenter.tsx +25 -0
- package/src/app-sandbox.tsx +552 -0
- package/src/appearance.ts +192 -0
- package/src/avatar.test.ts +229 -0
- package/src/avatar.ts +731 -0
- package/src/backgrounds.test.ts +119 -0
- package/src/backgrounds.ts +118 -0
- package/src/draw-presenter.tsx +39 -0
- package/src/event-presenter.tsx +62 -0
- package/src/file-presenter.tsx +76 -0
- package/src/formula-calculator.tsx +209 -0
- package/src/formula-presenter.test.ts +128 -0
- package/src/formula-presenter.tsx +301 -0
- package/src/help-topics.ts +104 -0
- package/src/lib/ink-audit.test.ts +314 -0
- package/src/lib/theme-css-blocks.ts +26 -0
- package/src/lib/theme-generator.test.ts +179 -0
- package/src/lib/theme-registry.gen.ts +352 -0
- package/src/lib/themes.test.ts +308 -0
- package/src/lib/themes.ts +75 -0
- package/src/lib/utils.ts +6 -0
- package/src/nav-items.ts +225 -0
- package/src/note-presenter.tsx +14 -0
- package/src/page-outline.tsx +127 -0
- package/src/table-presenter.tsx +226 -0
- package/src/task-presenter.tsx +60 -0
- package/src/ui/button.tsx +50 -0
- package/src/ui/input.tsx +18 -0
- package/src/ui/label.tsx +20 -0
- package/src/view-payload.ts +82 -0
- package/styles/app.css +1098 -0
- package/styles/themes.css +6198 -0
- package/themes/generate.d.mts +11 -0
- package/themes/generate.mjs +618 -0
- package/themes/model.d.mts +24 -0
- package/themes/model.mjs +213 -0
- package/themes/preview.html +145 -0
- package/themes/seeds.d.mts +16 -0
- package/themes/seeds.mjs +3694 -0
- package/tsconfig.json +15 -0
- package/tsconfig.tsbuildinfo +1 -0
package/styles/app.css
ADDED
|
@@ -0,0 +1,1098 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shared application stylesheet — everything both Mantle front ends need that
|
|
3
|
+
* is NOT a design token.
|
|
4
|
+
*
|
|
5
|
+
* Tokens live next door in themes.css (generated; never hand-edited). This file
|
|
6
|
+
* is the other half: the editor and prose styling, syntax highlighting, KaTeX,
|
|
7
|
+
* React Flow and scrollbar rules that the owner UI (client/web) and the
|
|
8
|
+
* server-rendered share + print surfaces (server/web) must agree on.
|
|
9
|
+
*
|
|
10
|
+
* It exists because they did not agree by accident. The two globals.css files
|
|
11
|
+
* held 943 identical lines, kept in step by hand — until one of them was missed,
|
|
12
|
+
* and every Switch and Checkbox in the owner UI silently lost its checked state
|
|
13
|
+
* because a rule had only been added to the other side.
|
|
14
|
+
*
|
|
15
|
+
* Each app keeps its own entrypoint for the parts that genuinely differ: the
|
|
16
|
+
* Tailwind import, its @source registrations and its plugins.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
@custom-variant dark (&:is(.dark *));
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* Theme: Clean Slate (generated with tweakcn.com).
|
|
23
|
+
* Tokens live in :root / .dark; the @theme inline block maps them to
|
|
24
|
+
* Tailwind utilities (bg-background, text-muted-foreground, etc.).
|
|
25
|
+
* --font-sans is owned by next/font (see lib/fonts.ts) and overridden
|
|
26
|
+
* on <body>; the value here is just a fallback.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
@layer base {
|
|
30
|
+
* {
|
|
31
|
+
@apply border-border outline-ring/50;
|
|
32
|
+
}
|
|
33
|
+
html,
|
|
34
|
+
body {
|
|
35
|
+
overflow: hidden;
|
|
36
|
+
}
|
|
37
|
+
/* ── Typography: the four slots ──────────────────────────────────────────
|
|
38
|
+
Settings → Appearance picks a face and a size for each of the interface,
|
|
39
|
+
the wordmark, the peer name and Pages/Notes prose. The FACES arrive as
|
|
40
|
+
inline style on <html> (server-rendered, see @mantle/web-ui/appearance) and
|
|
41
|
+
override the vars declared here; the SIZES arrive as attributes and are
|
|
42
|
+
resolved to numbers below. This file owns every multiplier, so there is one
|
|
43
|
+
place holding the scale and no TS copy to drift from it.
|
|
44
|
+
|
|
45
|
+
`--font-brand` is the exception that is never overridden: it is Mantle's
|
|
46
|
+
own wordmark face, used by the persistent product mark in the footer, which
|
|
47
|
+
must keep reading "mantle" in the product's typeface no matter what the
|
|
48
|
+
owner sets their own wordmark to. */
|
|
49
|
+
:root {
|
|
50
|
+
--font-brand: 'Bricolage Grotesque', ui-sans-serif, sans-serif;
|
|
51
|
+
--font-wordmark: var(--font-brand);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Interface scale. Setting the ROOT font-size scales every rem in the app,
|
|
55
|
+
and the shell is rem-based throughout — h-16 header, rem paddings, rem
|
|
56
|
+
control heights — so Large is a genuinely roomier interface rather than
|
|
57
|
+
bigger text crammed into the same boxes. That is what OS display scaling
|
|
58
|
+
does, and it is why this is one declaration instead of a type scale.
|
|
59
|
+
|
|
60
|
+
Percentages, not px, so a visitor who has raised their browser's default
|
|
61
|
+
font size keeps that as the baseline and this stays relative to it —
|
|
62
|
+
hard-coding 18px would quietly OVERRIDE an accessibility setting with our
|
|
63
|
+
own. 'medium' is the absence of the attribute. */
|
|
64
|
+
html[data-font-size='xsmall'] {
|
|
65
|
+
font-size: 87.5%; /* 14px at a 16px default */
|
|
66
|
+
}
|
|
67
|
+
html[data-font-size='small'] {
|
|
68
|
+
font-size: 93.75%; /* 15px at a 16px default */
|
|
69
|
+
}
|
|
70
|
+
html[data-font-size='large'] {
|
|
71
|
+
font-size: 112.5%; /* 18px at a 16px default */
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* The other three are LOCAL multipliers on one element's own size. A wordmark
|
|
75
|
+
that rescaled the shell would be a bug, not a feature — which is why these
|
|
76
|
+
are separate vars rather than more root font-size rules. Medium is the
|
|
77
|
+
absence of the attribute, so the var() fallback of 1 applies.
|
|
78
|
+
|
|
79
|
+
ONE authoritative scale: the per-slot rules below are pure wiring onto
|
|
80
|
+
these three tokens, so retuning what "small" means is one edit and the
|
|
81
|
+
three slots can never disagree about it. */
|
|
82
|
+
:root {
|
|
83
|
+
--font-scale-xsmall: 0.85;
|
|
84
|
+
--font-scale-small: 0.925;
|
|
85
|
+
--font-scale-large: 1.15;
|
|
86
|
+
}
|
|
87
|
+
html[data-logo-size='xsmall'] {
|
|
88
|
+
--wordmark-scale: var(--font-scale-xsmall);
|
|
89
|
+
}
|
|
90
|
+
html[data-logo-size='small'] {
|
|
91
|
+
--wordmark-scale: var(--font-scale-small);
|
|
92
|
+
}
|
|
93
|
+
html[data-logo-size='large'] {
|
|
94
|
+
--wordmark-scale: var(--font-scale-large);
|
|
95
|
+
}
|
|
96
|
+
html[data-title-size='xsmall'] {
|
|
97
|
+
--peer-name-scale: var(--font-scale-xsmall);
|
|
98
|
+
}
|
|
99
|
+
html[data-title-size='small'] {
|
|
100
|
+
--peer-name-scale: var(--font-scale-small);
|
|
101
|
+
}
|
|
102
|
+
html[data-title-size='large'] {
|
|
103
|
+
--peer-name-scale: var(--font-scale-large);
|
|
104
|
+
}
|
|
105
|
+
html[data-prose-size='xsmall'] {
|
|
106
|
+
--prose-scale: var(--font-scale-xsmall);
|
|
107
|
+
}
|
|
108
|
+
html[data-prose-size='small'] {
|
|
109
|
+
--prose-scale: var(--font-scale-small);
|
|
110
|
+
}
|
|
111
|
+
html[data-prose-size='large'] {
|
|
112
|
+
--prose-scale: var(--font-scale-large);
|
|
113
|
+
}
|
|
114
|
+
body {
|
|
115
|
+
@apply bg-background text-foreground;
|
|
116
|
+
letter-spacing: var(--tracking-normal);
|
|
117
|
+
}
|
|
118
|
+
/* Native <select> option lists: Chrome/Edge render the dropdown popup with
|
|
119
|
+
system colours, so a themed (e.g. near-white dark-theme) foreground lands
|
|
120
|
+
on the browser's light popup — invisible options. Pin both the option fill
|
|
121
|
+
and text to the popover token pair so the native list tracks the theme. */
|
|
122
|
+
select option {
|
|
123
|
+
background-color: var(--popover);
|
|
124
|
+
color: var(--popover-foreground);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* ============================================================================
|
|
129
|
+
Typography slots — the three that are NOT the interface font.
|
|
130
|
+
|
|
131
|
+
Each pairs its chosen face with its chosen scale in one class, so a call site
|
|
132
|
+
opts into both by naming the thing it is ("this is the wordmark") rather than
|
|
133
|
+
by remembering two vars. The `1` fallbacks mean an unstyled document, a stub
|
|
134
|
+
page, or a surface that never got the attributes still renders at full size.
|
|
135
|
+
========================================================================== */
|
|
136
|
+
|
|
137
|
+
/* In @layer components so a call site CAN override deliberately with a text-*
|
|
138
|
+
utility (utilities outrank components); with these unlayered, `wordmark
|
|
139
|
+
text-lg` would silently lose and invite an inline-style workaround that
|
|
140
|
+
bypasses the user's size setting. */
|
|
141
|
+
@layer components {
|
|
142
|
+
/* The header wordmark. Script and display faces overshoot the em box, so the
|
|
143
|
+
call sites pair this with overflow-y-visible rather than `truncate`. */
|
|
144
|
+
.wordmark {
|
|
145
|
+
font-family: var(--font-wordmark);
|
|
146
|
+
font-size: calc(1.5rem * var(--wordmark-scale, 1));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/* Mantle's own mark (footer, login, onboarding, team chrome, the share
|
|
150
|
+
footer) — the product's face, never the owner's choice. Family only, no
|
|
151
|
+
size: each mark keeps its own text-* size, and it is chrome, not content,
|
|
152
|
+
so the user scale does not apply. */
|
|
153
|
+
.wordmark-brand {
|
|
154
|
+
font-family: var(--font-brand);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* The header-centre peer name. */
|
|
158
|
+
.peer-name {
|
|
159
|
+
font-family: var(--font-page-title);
|
|
160
|
+
font-size: calc(1.125rem * var(--peer-name-scale, 1));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Long-form DOCUMENTS: the Pages editor, the Notes reader, their share pages,
|
|
165
|
+
and /print — which is what headless Chromium turns into the PDF export. This
|
|
166
|
+
is the one typography choice that leaves the browser.
|
|
167
|
+
|
|
168
|
+
Deliberately NOT `.prose` itself: chat bubbles, the changelog, help panels
|
|
169
|
+
and agent output are all prose too, and they are interface, not documents.
|
|
170
|
+
Scaling the font-size on the container rather than per element is what makes
|
|
171
|
+
the whole type scale move together — Tailwind Typography sizes its children
|
|
172
|
+
in em, so they follow this root proportionally.
|
|
173
|
+
|
|
174
|
+
UNLAYERED on purpose, unlike the header classes above: Typography's own
|
|
175
|
+
`prose` sets font-size in the utilities layer, and the scale has to outrank
|
|
176
|
+
it or it could never take effect. That in turn means every Typography size
|
|
177
|
+
modifier used WITH this class needs its base restated here — hence the
|
|
178
|
+
prose-sm pairing below, which keeps the Notes compact preview at its 0.875rem
|
|
179
|
+
base instead of silently inflating it to 1rem. */
|
|
180
|
+
.prose-document {
|
|
181
|
+
font-family: var(--font-prose, var(--font-sans));
|
|
182
|
+
font-size: calc(1rem * var(--prose-scale, 1));
|
|
183
|
+
}
|
|
184
|
+
.prose-sm.prose-document {
|
|
185
|
+
font-size: calc(0.875rem * var(--prose-scale, 1));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* ============================================================================
|
|
189
|
+
Scrollbar utilities — used by <Scrollable> and fixed shell regions.
|
|
190
|
+
========================================================================== */
|
|
191
|
+
.scrollbar-hidden {
|
|
192
|
+
-ms-overflow-style: none;
|
|
193
|
+
scrollbar-width: none;
|
|
194
|
+
}
|
|
195
|
+
.scrollbar-hidden::-webkit-scrollbar {
|
|
196
|
+
display: none;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.scrollbar-thin {
|
|
200
|
+
scrollbar-width: thin;
|
|
201
|
+
scrollbar-color: color-mix(in oklab, var(--muted-foreground) 30%, transparent) transparent;
|
|
202
|
+
}
|
|
203
|
+
.scrollbar-thin::-webkit-scrollbar {
|
|
204
|
+
width: 6px;
|
|
205
|
+
height: 6px;
|
|
206
|
+
}
|
|
207
|
+
.scrollbar-thin::-webkit-scrollbar-track {
|
|
208
|
+
background: transparent;
|
|
209
|
+
}
|
|
210
|
+
.scrollbar-thin::-webkit-scrollbar-thumb {
|
|
211
|
+
background-color: color-mix(in oklab, var(--muted-foreground) 30%, transparent);
|
|
212
|
+
border-radius: 3px;
|
|
213
|
+
transition: background-color 0.2s ease;
|
|
214
|
+
}
|
|
215
|
+
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
|
216
|
+
background-color: color-mix(in oklab, var(--muted-foreground) 50%, transparent);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* React Flow controls (trace step graph). The library's default control
|
|
220
|
+
buttons render white-on-white in some of our themes; pin them to theme
|
|
221
|
+
tokens so the zoom/fit icons stay legible in every theme + light/dark.
|
|
222
|
+
The double-class selector outranks the library's single-class rule
|
|
223
|
+
regardless of stylesheet load order. */
|
|
224
|
+
.react-flow .react-flow__controls-button {
|
|
225
|
+
background-color: var(--card);
|
|
226
|
+
color: var(--card-foreground);
|
|
227
|
+
border-bottom-color: var(--border);
|
|
228
|
+
}
|
|
229
|
+
.react-flow .react-flow__controls-button:hover {
|
|
230
|
+
background-color: var(--accent);
|
|
231
|
+
color: var(--accent-foreground);
|
|
232
|
+
}
|
|
233
|
+
.react-flow .react-flow__controls-button svg {
|
|
234
|
+
fill: currentColor;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* Drag-handle / focus-marker gutter (pages editor only). The block drag handle
|
|
238
|
+
+ focus-marker strip live in this left padding band, INSIDE the editing
|
|
239
|
+
surface, so moving the mouse toward them never leaves .ProseMirror — which is
|
|
240
|
+
what made the handle vanish before you could grab it (the library hides on
|
|
241
|
+
the editor's mouseleave). Scoped to the `page-gutter` class (added only by
|
|
242
|
+
PageEditor) so the read-only PageView + public render aren't indented — and,
|
|
243
|
+
unlike the old [contenteditable='true'] scope, the padding STAYS while the
|
|
244
|
+
editor is locked during an AI run (setEditable(false) flips contenteditable
|
|
245
|
+
to 'false', which used to collapse the padding and slide text under the
|
|
246
|
+
gutter). The page title + tags are padded to match in page-detail-client. */
|
|
247
|
+
.ProseMirror.page-gutter {
|
|
248
|
+
/* Widened to a two-tool lane: the block drag handle sits at the far left,
|
|
249
|
+
leaving room next to the content for per-block tools (e.g. the table
|
|
250
|
+
row/column trash). Keep focus-gutter's strip width + the pl-* on any
|
|
251
|
+
gutter-aligned helper text in sync with this value. */
|
|
252
|
+
padding-left: 4rem;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/* TipTap empty-canvas placeholder (pages editor). Shows the hint text on the
|
|
256
|
+
first empty line only; theme-aware via the muted-foreground token. */
|
|
257
|
+
.ProseMirror p.is-editor-empty:first-child::before {
|
|
258
|
+
content: attr(data-placeholder);
|
|
259
|
+
color: var(--muted-foreground);
|
|
260
|
+
opacity: 0.55;
|
|
261
|
+
float: left;
|
|
262
|
+
height: 0;
|
|
263
|
+
pointer-events: none;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* Pages blockquotes keep their accent border ("highlight") but drop the
|
|
267
|
+
curly quotation marks that `prose` adds before/after the text. Scoped to
|
|
268
|
+
the TipTap editor so Notes' markdown blockquotes are unaffected. */
|
|
269
|
+
.ProseMirror blockquote p:first-of-type::before,
|
|
270
|
+
.ProseMirror blockquote p:last-of-type::after {
|
|
271
|
+
content: none;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* Divider: a soft full-width rule that fades to transparent at both ends
|
|
275
|
+
(nicer than prose's flat hairline). Themed via --border so it tracks the
|
|
276
|
+
active theme + light/dark. Halved spacing vs prose's default (3em → 1.5em).
|
|
277
|
+
Applies to the editor, read-only PageView, and the public render. */
|
|
278
|
+
.ProseMirror hr {
|
|
279
|
+
margin-top: 1.5em;
|
|
280
|
+
margin-bottom: 1.5em;
|
|
281
|
+
height: 1px;
|
|
282
|
+
border: 0;
|
|
283
|
+
background: linear-gradient(to right, transparent, var(--border), transparent);
|
|
284
|
+
}
|
|
285
|
+
.ProseMirror hr.ProseMirror-selectednode {
|
|
286
|
+
outline: none;
|
|
287
|
+
background: linear-gradient(to right, transparent, var(--primary), transparent);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/* Pages multi-column layout (2–4 columns). CSS grid with equal auto-columns
|
|
291
|
+
guarantees identical widths regardless of content (flex sized unevenly).
|
|
292
|
+
Each column is a lightly shaded, rounded panel so its bounds are visible.
|
|
293
|
+
Stacks on narrow widths (mobile / narrow public pages). */
|
|
294
|
+
.ProseMirror .column-list {
|
|
295
|
+
display: grid;
|
|
296
|
+
grid-auto-flow: column;
|
|
297
|
+
grid-auto-columns: minmax(0, 1fr);
|
|
298
|
+
gap: 1rem;
|
|
299
|
+
margin-block: 1rem;
|
|
300
|
+
}
|
|
301
|
+
.ProseMirror .column-list > .column {
|
|
302
|
+
border-radius: var(--radius, 0.5rem);
|
|
303
|
+
background-color: var(--muted);
|
|
304
|
+
padding: 0.75rem 1rem;
|
|
305
|
+
}
|
|
306
|
+
.ProseMirror .column > :first-child {
|
|
307
|
+
margin-top: 0;
|
|
308
|
+
}
|
|
309
|
+
.ProseMirror .column > :last-child {
|
|
310
|
+
margin-bottom: 0;
|
|
311
|
+
}
|
|
312
|
+
@media (max-width: 640px) {
|
|
313
|
+
.ProseMirror .column-list {
|
|
314
|
+
grid-auto-flow: row;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* Pages tables (TipTap / prosemirror-tables) — themed borders + header row,
|
|
319
|
+
column-resize handle, and cell-selection highlight. */
|
|
320
|
+
/* The wrapper carries the outer border + theme radius and clips (overflow-x:auto
|
|
321
|
+
makes it a clipping box), so the table reads as a rounded, bordered card that
|
|
322
|
+
tracks --radius (sharp on radius-0 themes). */
|
|
323
|
+
.ProseMirror .tableWrapper {
|
|
324
|
+
overflow-x: auto;
|
|
325
|
+
margin-block: 1rem;
|
|
326
|
+
border: 1px solid var(--border);
|
|
327
|
+
border-radius: var(--radius);
|
|
328
|
+
}
|
|
329
|
+
.ProseMirror table {
|
|
330
|
+
border-collapse: collapse;
|
|
331
|
+
table-layout: fixed;
|
|
332
|
+
width: 100%;
|
|
333
|
+
margin: 0;
|
|
334
|
+
}
|
|
335
|
+
.ProseMirror table td,
|
|
336
|
+
.ProseMirror table th {
|
|
337
|
+
position: relative;
|
|
338
|
+
min-width: 4rem;
|
|
339
|
+
border: 1px solid var(--border);
|
|
340
|
+
padding: 0.4rem 0.6rem;
|
|
341
|
+
vertical-align: top;
|
|
342
|
+
}
|
|
343
|
+
/* Drop the cells' outer borders — the rounded wrapper provides the outer edge,
|
|
344
|
+
so it follows the corner radius cleanly; internal grid lines stay. */
|
|
345
|
+
.ProseMirror table tr:first-child > * {
|
|
346
|
+
border-top: 0;
|
|
347
|
+
}
|
|
348
|
+
.ProseMirror table tr:last-child > * {
|
|
349
|
+
border-bottom: 0;
|
|
350
|
+
}
|
|
351
|
+
.ProseMirror table tr > :first-child {
|
|
352
|
+
border-left: 0;
|
|
353
|
+
}
|
|
354
|
+
.ProseMirror table tr > :last-child {
|
|
355
|
+
border-right: 0;
|
|
356
|
+
}
|
|
357
|
+
.ProseMirror table th {
|
|
358
|
+
background-color: var(--muted);
|
|
359
|
+
font-weight: 600;
|
|
360
|
+
text-align: left;
|
|
361
|
+
}
|
|
362
|
+
.ProseMirror table td > p,
|
|
363
|
+
.ProseMirror table th > p {
|
|
364
|
+
margin: 0;
|
|
365
|
+
}
|
|
366
|
+
.ProseMirror table .selectedCell::after {
|
|
367
|
+
content: '';
|
|
368
|
+
position: absolute;
|
|
369
|
+
inset: 0;
|
|
370
|
+
background: color-mix(in oklab, var(--primary) 14%, transparent);
|
|
371
|
+
pointer-events: none;
|
|
372
|
+
}
|
|
373
|
+
.ProseMirror table .column-resize-handle {
|
|
374
|
+
position: absolute;
|
|
375
|
+
right: -2px;
|
|
376
|
+
top: 0;
|
|
377
|
+
bottom: 0;
|
|
378
|
+
width: 4px;
|
|
379
|
+
background: var(--primary);
|
|
380
|
+
cursor: col-resize;
|
|
381
|
+
}
|
|
382
|
+
.ProseMirror.resize-cursor {
|
|
383
|
+
cursor: col-resize;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* Pages inline marks: themed links + highlight. */
|
|
387
|
+
.ProseMirror a {
|
|
388
|
+
color: var(--primary-ink);
|
|
389
|
+
text-decoration: underline;
|
|
390
|
+
text-underline-offset: 2px;
|
|
391
|
+
cursor: pointer;
|
|
392
|
+
}
|
|
393
|
+
.ProseMirror mark {
|
|
394
|
+
background-color: color-mix(in oklab, var(--primary) 22%, transparent);
|
|
395
|
+
border-radius: 0.2rem;
|
|
396
|
+
padding: 0 0.1em;
|
|
397
|
+
color: inherit;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/* Tighter lists. Tailwind Typography spaces list items almost as far apart as
|
|
401
|
+
headings (ul/ol ~1.25em, li ~0.5em, plus the <p> TipTap wraps each item in).
|
|
402
|
+
Pull bullets / numbers / items together so a list reads like normal running
|
|
403
|
+
text. em-based to track the prose scale; applies to every .prose surface
|
|
404
|
+
(Pages editor + read view + public share + notes + assistant). */
|
|
405
|
+
.prose :is(ul, ol) {
|
|
406
|
+
margin-block: 0.5em;
|
|
407
|
+
}
|
|
408
|
+
.prose :is(ul, ol) :is(ul, ol) {
|
|
409
|
+
margin-block: 0.15em;
|
|
410
|
+
}
|
|
411
|
+
.prose li {
|
|
412
|
+
margin-block: 0.15em;
|
|
413
|
+
}
|
|
414
|
+
/* TipTap wraps each item's content in a <p>; that paragraph's own margins are
|
|
415
|
+
the biggest gap. Collapse them so the line sits snug against its marker. */
|
|
416
|
+
.prose li > p {
|
|
417
|
+
margin-block: 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/* Tighter heading-to-body gap. Typography leaves a lot of room below a heading
|
|
421
|
+
(h1 ~0.89em/32px, h2 ~1em/24px, h3 ~0.6em/12px); trim the bottom margin so a
|
|
422
|
+
heading sits closer to the text it introduces. Top margins (section
|
|
423
|
+
separation above a heading) are left as-is. em-based, so it scales per
|
|
424
|
+
level — h1 0.5em→18px, h2 0.5em→12px, h3 0.4em→8px, h4 0.4em→~6px. */
|
|
425
|
+
.prose :is(h1, h2) {
|
|
426
|
+
margin-bottom: 0.5em;
|
|
427
|
+
}
|
|
428
|
+
.prose :is(h3, h4, h5, h6) {
|
|
429
|
+
margin-bottom: 0.4em;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* Pages to-do lists: no bullets, themed checkbox aligned to the first line;
|
|
433
|
+
completed items dim + strike. */
|
|
434
|
+
.ProseMirror ul[data-type='taskList'] {
|
|
435
|
+
list-style: none;
|
|
436
|
+
padding-left: 0;
|
|
437
|
+
margin-block: 0.5rem;
|
|
438
|
+
}
|
|
439
|
+
.ProseMirror ul[data-type='taskList'] li {
|
|
440
|
+
display: flex;
|
|
441
|
+
align-items: flex-start;
|
|
442
|
+
gap: 0.5rem;
|
|
443
|
+
}
|
|
444
|
+
.ProseMirror ul[data-type='taskList'] li > label {
|
|
445
|
+
margin-top: 0.2rem;
|
|
446
|
+
flex-shrink: 0;
|
|
447
|
+
user-select: none;
|
|
448
|
+
}
|
|
449
|
+
.ProseMirror ul[data-type='taskList'] li > div {
|
|
450
|
+
flex: 1 1 auto;
|
|
451
|
+
min-width: 0;
|
|
452
|
+
}
|
|
453
|
+
.ProseMirror ul[data-type='taskList'] li > div > p {
|
|
454
|
+
margin: 0;
|
|
455
|
+
}
|
|
456
|
+
.ProseMirror ul[data-type='taskList'] input[type='checkbox'] {
|
|
457
|
+
cursor: pointer;
|
|
458
|
+
accent-color: var(--primary);
|
|
459
|
+
}
|
|
460
|
+
.ProseMirror ul[data-type='taskList'] li[data-checked='true'] > div {
|
|
461
|
+
color: var(--muted-foreground);
|
|
462
|
+
text-decoration: line-through;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/* Pages @-mention chips. Entities tint with primary; page/note links use a
|
|
466
|
+
distinct categorical colour so the two reference kinds read differently. */
|
|
467
|
+
.ProseMirror .mention {
|
|
468
|
+
border-radius: 0.3rem;
|
|
469
|
+
background-color: color-mix(in oklab, var(--primary) 14%, transparent);
|
|
470
|
+
color: var(--primary-ink);
|
|
471
|
+
padding: 0.05em 0.3em;
|
|
472
|
+
font-weight: 500;
|
|
473
|
+
white-space: nowrap;
|
|
474
|
+
}
|
|
475
|
+
.ProseMirror .mention[data-ref='node'] {
|
|
476
|
+
background-color: color-mix(in oklab, var(--info) 16%, transparent);
|
|
477
|
+
color: var(--info-ink);
|
|
478
|
+
cursor: pointer;
|
|
479
|
+
}
|
|
480
|
+
.ProseMirror .mention[data-ref='node']:hover {
|
|
481
|
+
background-color: color-mix(in oklab, var(--info) 26%, transparent);
|
|
482
|
+
text-decoration: underline;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* Code-block syntax highlighting (CodeBlockLowlight / highlight.js). Token
|
|
486
|
+
classes map onto the DERIVED code palette (--code-*), which is solved per
|
|
487
|
+
theme to stay legible on every neutral surface — chart tokens are data ink
|
|
488
|
+
at a 3:1 bar and rendered function names invisible here (1.02:1 on `claude`
|
|
489
|
+
dark) before the split. Scoped to .ProseMirror (Pages editor + assistant
|
|
490
|
+
rich renderer) and .code-view (the Apps source viewer). Unhighlighted code
|
|
491
|
+
falls back to the foreground. */
|
|
492
|
+
:is(.ProseMirror, .code-view) .hljs-comment,
|
|
493
|
+
:is(.ProseMirror, .code-view) .hljs-quote {
|
|
494
|
+
color: var(--muted-foreground);
|
|
495
|
+
font-style: italic;
|
|
496
|
+
}
|
|
497
|
+
:is(.ProseMirror, .code-view) .hljs-keyword,
|
|
498
|
+
:is(.ProseMirror, .code-view) .hljs-selector-tag,
|
|
499
|
+
:is(.ProseMirror, .code-view) .hljs-built_in,
|
|
500
|
+
:is(.ProseMirror, .code-view) .hljs-name,
|
|
501
|
+
:is(.ProseMirror, .code-view) .hljs-tag {
|
|
502
|
+
color: var(--code-keyword);
|
|
503
|
+
}
|
|
504
|
+
:is(.ProseMirror, .code-view) .hljs-string,
|
|
505
|
+
:is(.ProseMirror, .code-view) .hljs-attr,
|
|
506
|
+
:is(.ProseMirror, .code-view) .hljs-regexp,
|
|
507
|
+
:is(.ProseMirror, .code-view) .hljs-addition,
|
|
508
|
+
:is(.ProseMirror, .code-view) .hljs-meta-string {
|
|
509
|
+
color: var(--code-string);
|
|
510
|
+
}
|
|
511
|
+
:is(.ProseMirror, .code-view) .hljs-number,
|
|
512
|
+
:is(.ProseMirror, .code-view) .hljs-literal,
|
|
513
|
+
:is(.ProseMirror, .code-view) .hljs-type,
|
|
514
|
+
:is(.ProseMirror, .code-view) .hljs-symbol,
|
|
515
|
+
:is(.ProseMirror, .code-view) .hljs-bullet,
|
|
516
|
+
:is(.ProseMirror, .code-view) .hljs-link {
|
|
517
|
+
color: var(--code-number);
|
|
518
|
+
}
|
|
519
|
+
:is(.ProseMirror, .code-view) .hljs-title,
|
|
520
|
+
:is(.ProseMirror, .code-view) .hljs-section,
|
|
521
|
+
:is(.ProseMirror, .code-view) .hljs-function .hljs-title,
|
|
522
|
+
:is(.ProseMirror, .code-view) .hljs-class .hljs-title {
|
|
523
|
+
color: var(--code-title);
|
|
524
|
+
}
|
|
525
|
+
:is(.ProseMirror, .code-view) .hljs-attribute,
|
|
526
|
+
:is(.ProseMirror, .code-view) .hljs-variable,
|
|
527
|
+
:is(.ProseMirror, .code-view) .hljs-template-variable,
|
|
528
|
+
:is(.ProseMirror, .code-view) .hljs-params,
|
|
529
|
+
:is(.ProseMirror, .code-view) .hljs-meta {
|
|
530
|
+
color: var(--code-variable);
|
|
531
|
+
}
|
|
532
|
+
:is(.ProseMirror, .code-view) .hljs-deletion {
|
|
533
|
+
color: var(--destructive-ink);
|
|
534
|
+
}
|
|
535
|
+
:is(.ProseMirror, .code-view) .hljs-emphasis {
|
|
536
|
+
font-style: italic;
|
|
537
|
+
}
|
|
538
|
+
:is(.ProseMirror, .code-view) .hljs-strong {
|
|
539
|
+
font-weight: 600;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/* Embedded images (page-editor `image` node) — fit the column, themed radius,
|
|
543
|
+
and a clear selection ring when picked. */
|
|
544
|
+
.ProseMirror img {
|
|
545
|
+
max-width: 100%;
|
|
546
|
+
height: auto;
|
|
547
|
+
border-radius: var(--radius);
|
|
548
|
+
display: block;
|
|
549
|
+
margin-block: 0.75rem;
|
|
550
|
+
}
|
|
551
|
+
.ProseMirror img.ProseMirror-selectednode {
|
|
552
|
+
outline: 2px solid var(--primary);
|
|
553
|
+
outline-offset: 2px;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/* Math nodes (KaTeX). Block formulas get breathing room + horizontal scroll on
|
|
557
|
+
overflow; inline ones sit in the text run. KaTeX colours inherit currentColor
|
|
558
|
+
so they track the theme foreground. */
|
|
559
|
+
.ProseMirror [data-type='block-math'] {
|
|
560
|
+
margin-block: 1rem;
|
|
561
|
+
text-align: center;
|
|
562
|
+
overflow-x: auto;
|
|
563
|
+
}
|
|
564
|
+
.ProseMirror [data-type='inline-math'] {
|
|
565
|
+
padding: 0 0.1em;
|
|
566
|
+
}
|
|
567
|
+
.ProseMirror [data-type='block-math'].ProseMirror-selectednode,
|
|
568
|
+
.ProseMirror [data-type='inline-math'].ProseMirror-selectednode {
|
|
569
|
+
outline: 2px solid var(--primary);
|
|
570
|
+
outline-offset: 2px;
|
|
571
|
+
border-radius: 0.2rem;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/* Callouts on the raw/public render. In-app callouts use a React NodeView; the
|
|
575
|
+
public page surface (server-rendered HTML) emits raw [data-callout] divs, so
|
|
576
|
+
these rules style only that path — a tinted panel + left accent, themed per
|
|
577
|
+
variant (matching the in-app icon colours). */
|
|
578
|
+
.ProseMirror [data-callout] {
|
|
579
|
+
margin-block: 0.75rem;
|
|
580
|
+
border-left: 3px solid var(--info);
|
|
581
|
+
border-radius: var(--radius);
|
|
582
|
+
background-color: color-mix(in oklab, var(--info) 8%, transparent);
|
|
583
|
+
padding: 0.5rem 0.9rem;
|
|
584
|
+
}
|
|
585
|
+
.ProseMirror [data-callout] > :first-child {
|
|
586
|
+
margin-top: 0;
|
|
587
|
+
}
|
|
588
|
+
.ProseMirror [data-callout] > :last-child {
|
|
589
|
+
margin-bottom: 0;
|
|
590
|
+
}
|
|
591
|
+
.ProseMirror [data-callout][data-variant='success'] {
|
|
592
|
+
border-left-color: var(--success);
|
|
593
|
+
background-color: color-mix(in oklab, var(--success) 8%, transparent);
|
|
594
|
+
}
|
|
595
|
+
.ProseMirror [data-callout][data-variant='warning'] {
|
|
596
|
+
border-left-color: var(--warning);
|
|
597
|
+
background-color: color-mix(in oklab, var(--warning) 8%, transparent);
|
|
598
|
+
}
|
|
599
|
+
.ProseMirror [data-callout][data-variant='danger'] {
|
|
600
|
+
border-left-color: var(--destructive);
|
|
601
|
+
background-color: color-mix(in oklab, var(--destructive) 8%, transparent);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/* Asides on the raw/public render. The fancier cousin of the callout: a rounded
|
|
605
|
+
gradient panel (no icon, no left bar). The gradient `background` + themed
|
|
606
|
+
`border-color` are emitted INLINE by render-page-doc.ts (and the in-app
|
|
607
|
+
NodeView) so they track the selected chart colour + angle; these rules only
|
|
608
|
+
own the box geometry so the two paths look identical. */
|
|
609
|
+
.ProseMirror [data-aside] {
|
|
610
|
+
margin-block: 0.75rem;
|
|
611
|
+
border: 1px solid transparent;
|
|
612
|
+
border-radius: 0.75rem;
|
|
613
|
+
padding: 1rem 1.25rem;
|
|
614
|
+
box-shadow: 0 1px 2px 0 color-mix(in oklab, var(--foreground) 8%, transparent);
|
|
615
|
+
}
|
|
616
|
+
.ProseMirror [data-aside] > :first-child {
|
|
617
|
+
margin-top: 0;
|
|
618
|
+
}
|
|
619
|
+
.ProseMirror [data-aside] > :last-child {
|
|
620
|
+
margin-bottom: 0;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/* Embedded file chip on the raw/public render. */
|
|
624
|
+
.ProseMirror a.file-embed {
|
|
625
|
+
display: inline-flex;
|
|
626
|
+
align-items: center;
|
|
627
|
+
gap: 0.5rem;
|
|
628
|
+
margin-block: 0.5rem;
|
|
629
|
+
border: 1px solid var(--border);
|
|
630
|
+
border-radius: var(--radius);
|
|
631
|
+
background-color: color-mix(in oklab, var(--muted) 40%, transparent);
|
|
632
|
+
padding: 0.5rem 0.85rem;
|
|
633
|
+
font-size: 0.875rem;
|
|
634
|
+
text-decoration: none;
|
|
635
|
+
color: var(--foreground);
|
|
636
|
+
}
|
|
637
|
+
.ProseMirror a.file-embed::before {
|
|
638
|
+
content: '📎';
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/* Diagram degrade block on the raw/public render (slice 1: the Mermaid source
|
|
642
|
+
as labelled code; slice 2 replaces it with server-rendered SVG). The in-app
|
|
643
|
+
editor path uses the Diagram NodeView instead. */
|
|
644
|
+
.ProseMirror .diagram[data-diagram-source] {
|
|
645
|
+
margin-block: 0.75rem;
|
|
646
|
+
border: 1px solid var(--border);
|
|
647
|
+
border-radius: var(--radius);
|
|
648
|
+
background-color: color-mix(in oklab, var(--muted) 40%, transparent);
|
|
649
|
+
padding: 0.75rem 1rem;
|
|
650
|
+
}
|
|
651
|
+
.ProseMirror .diagram[data-diagram-source] .diagram-label {
|
|
652
|
+
display: block;
|
|
653
|
+
margin-bottom: 0.35rem;
|
|
654
|
+
font-size: 0.7rem;
|
|
655
|
+
font-weight: 600;
|
|
656
|
+
letter-spacing: 0.08em;
|
|
657
|
+
text-transform: uppercase;
|
|
658
|
+
color: var(--muted-foreground);
|
|
659
|
+
}
|
|
660
|
+
.ProseMirror .diagram[data-diagram-source] pre {
|
|
661
|
+
margin: 0;
|
|
662
|
+
overflow-x: auto;
|
|
663
|
+
font-size: 0.8125rem;
|
|
664
|
+
line-height: 1.5;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/* A diagram upgraded to real SVG (the /print surface renders Mermaid
|
|
668
|
+
client-side in the PDF sidecar's Chromium; slice 2 will inline cached SVG
|
|
669
|
+
on /s the same shape). */
|
|
670
|
+
.ProseMirror .diagram-render {
|
|
671
|
+
display: flex;
|
|
672
|
+
justify-content: center;
|
|
673
|
+
margin-block: 0.75rem;
|
|
674
|
+
}
|
|
675
|
+
.ProseMirror .diagram-render svg {
|
|
676
|
+
max-width: 100%;
|
|
677
|
+
height: auto;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/* Focus marker (pages editor). Blocks the user marked via the left-gutter
|
|
681
|
+
marker get a primary accent bar + faint tint so a multi-section selection
|
|
682
|
+
reads at a glance. Editor-only (decorations); never serialized into the doc.
|
|
683
|
+
The negative-margin / padding pair pulls the tint into the gutter band and
|
|
684
|
+
keeps the text from shifting when a block becomes marked. */
|
|
685
|
+
.ProseMirror .is-focus-marked {
|
|
686
|
+
position: relative;
|
|
687
|
+
border-radius: var(--radius);
|
|
688
|
+
background-color: color-mix(in oklab, var(--primary) 8%, transparent);
|
|
689
|
+
box-shadow: inset 3px 0 0 0 var(--primary);
|
|
690
|
+
margin-left: -0.75rem;
|
|
691
|
+
padding-left: 0.75rem;
|
|
692
|
+
padding-right: 0.4rem;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/* Edited (green) — blocks Pages changed in the current draft. Same shape as the
|
|
696
|
+
marked highlight, distinct colour (the success role), so a post-run page
|
|
697
|
+
reads as "this is what moved" at a glance. Cleared on commit. */
|
|
698
|
+
.ProseMirror .is-edited {
|
|
699
|
+
position: relative;
|
|
700
|
+
border-radius: var(--radius);
|
|
701
|
+
background-color: color-mix(in oklab, var(--success) 12%, transparent);
|
|
702
|
+
box-shadow: inset 3px 0 0 0 var(--success);
|
|
703
|
+
margin-left: -0.75rem;
|
|
704
|
+
padding-left: 0.75rem;
|
|
705
|
+
padding-right: 0.4rem;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/* ── Visual diff (review mode, Phase 3a Pass 2) ────────────────────────────
|
|
709
|
+
Added blocks (success) and changed blocks (warning) get a
|
|
710
|
+
left accent bar + faint tint. Each carries a hover-reveal "Discard" pill
|
|
711
|
+
(placed as a widget at the block's start, absolutely positioned). Removed
|
|
712
|
+
blocks aren't in the doc — they render as struck "ghost" widget cards. All
|
|
713
|
+
themed-token only, so they track the active theme + light/dark. */
|
|
714
|
+
.ProseMirror .is-diff-added,
|
|
715
|
+
.ProseMirror .is-diff-changed {
|
|
716
|
+
position: relative;
|
|
717
|
+
border-radius: var(--radius);
|
|
718
|
+
margin-left: -0.75rem;
|
|
719
|
+
padding-left: 0.75rem;
|
|
720
|
+
padding-right: 0.4rem;
|
|
721
|
+
}
|
|
722
|
+
.ProseMirror .is-diff-added {
|
|
723
|
+
background-color: color-mix(in oklab, var(--success) 12%, transparent);
|
|
724
|
+
box-shadow: inset 3px 0 0 0 var(--success);
|
|
725
|
+
}
|
|
726
|
+
.ProseMirror .is-diff-changed {
|
|
727
|
+
background-color: color-mix(in oklab, var(--warning) 12%, transparent);
|
|
728
|
+
box-shadow: inset 3px 0 0 0 var(--warning);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/* The per-block action pill (Discard / Restore). Lives inside a positioned diff
|
|
732
|
+
block (added/changed) or a ghost card. Hidden until the block is hovered so
|
|
733
|
+
it doesn't clutter the read. */
|
|
734
|
+
.ProseMirror .diff-pill {
|
|
735
|
+
position: absolute;
|
|
736
|
+
top: 0.15rem;
|
|
737
|
+
right: 0.2rem;
|
|
738
|
+
z-index: 2;
|
|
739
|
+
border: 1px solid var(--border);
|
|
740
|
+
border-radius: 999px;
|
|
741
|
+
background-color: var(--popover);
|
|
742
|
+
color: var(--popover-foreground);
|
|
743
|
+
padding: 0.05rem 0.5rem;
|
|
744
|
+
font-size: 0.68rem;
|
|
745
|
+
font-weight: 600;
|
|
746
|
+
line-height: 1.4;
|
|
747
|
+
cursor: pointer;
|
|
748
|
+
opacity: 0;
|
|
749
|
+
transition: opacity 0.12s ease;
|
|
750
|
+
user-select: none;
|
|
751
|
+
}
|
|
752
|
+
.ProseMirror .is-diff-added:hover > .diff-pill,
|
|
753
|
+
.ProseMirror .is-diff-changed:hover > .diff-pill,
|
|
754
|
+
.ProseMirror .diff-pill:focus-visible {
|
|
755
|
+
opacity: 1;
|
|
756
|
+
}
|
|
757
|
+
.ProseMirror .diff-pill-discard:hover {
|
|
758
|
+
border-color: var(--destructive);
|
|
759
|
+
color: var(--destructive-ink);
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/* Removed-block ghost — a read-only card showing deleted content where it used
|
|
763
|
+
to be. Always shows its Restore pill (it's the only handle on a ghost). */
|
|
764
|
+
.ProseMirror .diff-removed-ghost {
|
|
765
|
+
position: relative;
|
|
766
|
+
margin: 0.4rem 0;
|
|
767
|
+
border: 1px dashed color-mix(in oklab, var(--destructive) 45%, transparent);
|
|
768
|
+
border-radius: var(--radius);
|
|
769
|
+
background-color: color-mix(in oklab, var(--destructive) 7%, transparent);
|
|
770
|
+
padding: 0.4rem 0.6rem;
|
|
771
|
+
user-select: none;
|
|
772
|
+
}
|
|
773
|
+
.ProseMirror .diff-removed-head {
|
|
774
|
+
display: flex;
|
|
775
|
+
align-items: center;
|
|
776
|
+
justify-content: space-between;
|
|
777
|
+
gap: 0.5rem;
|
|
778
|
+
}
|
|
779
|
+
.ProseMirror .diff-removed-tag {
|
|
780
|
+
font-size: 0.66rem;
|
|
781
|
+
font-weight: 700;
|
|
782
|
+
text-transform: uppercase;
|
|
783
|
+
letter-spacing: 0.04em;
|
|
784
|
+
color: var(--destructive-ink);
|
|
785
|
+
}
|
|
786
|
+
.ProseMirror .diff-removed-ghost .diff-pill {
|
|
787
|
+
position: static;
|
|
788
|
+
opacity: 1;
|
|
789
|
+
}
|
|
790
|
+
.ProseMirror .diff-pill-restore:hover {
|
|
791
|
+
border-color: var(--success);
|
|
792
|
+
color: var(--success-ink);
|
|
793
|
+
}
|
|
794
|
+
.ProseMirror .diff-removed-body {
|
|
795
|
+
margin-top: 0.2rem;
|
|
796
|
+
font-size: 0.9rem;
|
|
797
|
+
color: color-mix(in oklab, var(--foreground) 65%, transparent);
|
|
798
|
+
text-decoration: line-through;
|
|
799
|
+
text-decoration-color: color-mix(in oklab, var(--destructive) 55%, transparent);
|
|
800
|
+
white-space: pre-wrap;
|
|
801
|
+
overflow-wrap: anywhere;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/* Sub-page card on the raw/public render. In-app it's a React NodeView (a
|
|
805
|
+
Link); the public surface emits an inert [data-child-page] div (sub-pages
|
|
806
|
+
aren't part of a shared subtree), styled to match the in-app card. */
|
|
807
|
+
.ProseMirror [data-child-page] {
|
|
808
|
+
display: flex;
|
|
809
|
+
align-items: center;
|
|
810
|
+
gap: 0.625rem;
|
|
811
|
+
margin-block: 0.5rem;
|
|
812
|
+
border: 1px solid var(--border);
|
|
813
|
+
border-radius: var(--radius);
|
|
814
|
+
background-color: var(--card);
|
|
815
|
+
padding: 0.625rem 0.75rem;
|
|
816
|
+
font-size: 0.875rem;
|
|
817
|
+
font-weight: 500;
|
|
818
|
+
color: var(--foreground);
|
|
819
|
+
}
|
|
820
|
+
.ProseMirror [data-child-page] .child-page-icon {
|
|
821
|
+
flex: none;
|
|
822
|
+
}
|
|
823
|
+
.ProseMirror [data-child-page] .child-page-title {
|
|
824
|
+
min-width: 0;
|
|
825
|
+
overflow: hidden;
|
|
826
|
+
text-overflow: ellipsis;
|
|
827
|
+
white-space: nowrap;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/* ============================================================================
|
|
831
|
+
Theme-accented prose (`.prose-accent`) — opt-in markdown brightening.
|
|
832
|
+
Used by the docs reader (/docs) and Notes (read view + public share).
|
|
833
|
+
Brightens the otherwise black-and-white markdown with the ACTIVE theme's
|
|
834
|
+
tokens (primary + chart palette + border). Everything resolves from CSS
|
|
835
|
+
vars, so it recolours with every theme × light/dark; nothing is hardcoded.
|
|
836
|
+
Opt-in via the `.prose-accent` class, so the Pages editor and any prose that
|
|
837
|
+
doesn't add it are untouched. Selectors are class+element (specificity
|
|
838
|
+
0,1,1+), which outranks Tailwind Typography's `:where()`-wrapped rules
|
|
839
|
+
(0,1,0) without `!important`.
|
|
840
|
+
========================================================================== */
|
|
841
|
+
|
|
842
|
+
/* h1 — solid brand colour. (Was a primary→chart-3 gradient via background-clip:
|
|
843
|
+
text, but transparent glyph fill caused headaches with PDF export, editing,
|
|
844
|
+
and general fragility — a flat colour is robust everywhere.) */
|
|
845
|
+
.prose-accent h1 {
|
|
846
|
+
color: var(--primary-ink);
|
|
847
|
+
letter-spacing: -0.01em;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
/* h2 — solid primary with a hairline divider for section structure. */
|
|
851
|
+
.prose-accent h2 {
|
|
852
|
+
color: var(--primary-ink);
|
|
853
|
+
border-bottom: 1px solid var(--border);
|
|
854
|
+
padding-bottom: 0.3em;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/* h3 — the primary ink (AA by construction; the old color-mix() of primary
|
|
858
|
+
toward foreground measured below AA in 52 of 164 theme surfaces, and being a
|
|
859
|
+
computed value it hid from the ink audit). Hierarchy vs h2 reads from size. */
|
|
860
|
+
.prose-accent h3 {
|
|
861
|
+
color: var(--primary-ink);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/* h4 — a distinct accent so a fourth level doesn't fall back to grey; the
|
|
865
|
+
info ink is AA-safe everywhere and reads as documentation. */
|
|
866
|
+
.prose-accent h4 {
|
|
867
|
+
color: var(--info-ink);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/* Links — brand colour, underline only on hover (themed, not prose blue). */
|
|
871
|
+
.prose-accent a {
|
|
872
|
+
color: var(--primary-ink);
|
|
873
|
+
font-weight: 500;
|
|
874
|
+
text-decoration: none;
|
|
875
|
+
}
|
|
876
|
+
.prose-accent a:hover {
|
|
877
|
+
text-decoration: underline;
|
|
878
|
+
text-underline-offset: 2px;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/* Inline code — a tinted accent chip. Scoped to NOT touch code blocks (pre),
|
|
882
|
+
and strip the backtick quotes Typography injects via ::before/::after. */
|
|
883
|
+
.prose-accent :not(pre) > code {
|
|
884
|
+
background-color: color-mix(in oklab, var(--primary) 12%, transparent);
|
|
885
|
+
color: var(--primary-ink);
|
|
886
|
+
padding: 0.15em 0.4em;
|
|
887
|
+
border-radius: 0.35rem;
|
|
888
|
+
font-weight: 500;
|
|
889
|
+
}
|
|
890
|
+
.prose-accent :not(pre) > code::before,
|
|
891
|
+
.prose-accent :not(pre) > code::after {
|
|
892
|
+
content: none;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/* Blockquote — accent bar + faint primary tint, upright (not italic). */
|
|
896
|
+
.prose-accent blockquote {
|
|
897
|
+
border-left-color: var(--primary);
|
|
898
|
+
background-color: color-mix(in oklab, var(--primary) 6%, transparent);
|
|
899
|
+
border-radius: 0 0.4rem 0.4rem 0;
|
|
900
|
+
padding: 0.5em 1em;
|
|
901
|
+
font-style: normal;
|
|
902
|
+
color: var(--foreground);
|
|
903
|
+
}
|
|
904
|
+
.prose-accent blockquote p:first-of-type::before,
|
|
905
|
+
.prose-accent blockquote p:last-of-type::after {
|
|
906
|
+
content: none;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/* List markers in the accent colour (bullets + ordered numbers). */
|
|
910
|
+
.prose-accent ul > li::marker,
|
|
911
|
+
.prose-accent ol > li::marker {
|
|
912
|
+
color: var(--primary-ink);
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/* hr — a soft accent-fading rule instead of a flat hairline. */
|
|
916
|
+
.prose-accent hr {
|
|
917
|
+
border: none;
|
|
918
|
+
height: 1px;
|
|
919
|
+
background-image: linear-gradient(
|
|
920
|
+
to right,
|
|
921
|
+
transparent,
|
|
922
|
+
color-mix(in oklab, var(--primary) 60%, var(--border)),
|
|
923
|
+
transparent
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/* ============================================================================
|
|
928
|
+
Pages (TipTap) — extra flourishes on top of `.prose-accent`.
|
|
929
|
+
The editor, read view, and public render all carry `.ProseMirror` +
|
|
930
|
+
`.prose-accent`, so the heading / link / code / quote accents come from the
|
|
931
|
+
shared block above. These add Pages-only polish the flat markdown surfaces
|
|
932
|
+
don't need. All theme-token driven.
|
|
933
|
+
========================================================================== */
|
|
934
|
+
|
|
935
|
+
/* Brand-coloured caret + selection. */
|
|
936
|
+
.ProseMirror {
|
|
937
|
+
caret-color: var(--primary);
|
|
938
|
+
}
|
|
939
|
+
.ProseMirror ::selection {
|
|
940
|
+
background-color: color-mix(in oklab, var(--primary) 25%, transparent);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/* Code blocks — a themed card with a primary accent spine instead of prose's
|
|
944
|
+
flat slab. The .hljs-* syntax tokens (chart palette, above) sit on top. The
|
|
945
|
+
class+element selector outranks Typography's `:where(pre)` without
|
|
946
|
+
`!important`; inline code keeps its own `.prose-accent` chip. */
|
|
947
|
+
.ProseMirror pre {
|
|
948
|
+
background-color: var(--muted);
|
|
949
|
+
color: var(--foreground);
|
|
950
|
+
border: 1px solid var(--border);
|
|
951
|
+
border-left: 3px solid var(--primary);
|
|
952
|
+
border-radius: var(--radius);
|
|
953
|
+
padding: 0.85rem 1rem;
|
|
954
|
+
overflow-x: auto;
|
|
955
|
+
}
|
|
956
|
+
.ProseMirror pre code {
|
|
957
|
+
background: none;
|
|
958
|
+
color: inherit;
|
|
959
|
+
padding: 0;
|
|
960
|
+
font-weight: inherit;
|
|
961
|
+
font-size: 0.875em;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/* ============================================================================
|
|
965
|
+
Spinner — the indeterminate <Spinner> arc (components/ui/spinner.tsx).
|
|
966
|
+
The ring rotates while the dash grows/shrinks + drifts, so it reads as a
|
|
967
|
+
living sweep rather than a flat spin. Honours reduced-motion.
|
|
968
|
+
========================================================================== */
|
|
969
|
+
.mantle-spinner {
|
|
970
|
+
animation: mantle-spin 1.4s linear infinite;
|
|
971
|
+
transform-origin: center;
|
|
972
|
+
}
|
|
973
|
+
.mantle-spinner-arc {
|
|
974
|
+
stroke-dasharray: 1 150;
|
|
975
|
+
stroke-dashoffset: 0;
|
|
976
|
+
transform-origin: center;
|
|
977
|
+
animation: mantle-spin-dash 1.4s ease-in-out infinite;
|
|
978
|
+
}
|
|
979
|
+
@keyframes mantle-spin {
|
|
980
|
+
to {
|
|
981
|
+
transform: rotate(360deg);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
@keyframes mantle-spin-dash {
|
|
985
|
+
0% {
|
|
986
|
+
stroke-dasharray: 1 150;
|
|
987
|
+
stroke-dashoffset: 0;
|
|
988
|
+
}
|
|
989
|
+
50% {
|
|
990
|
+
stroke-dasharray: 90 150;
|
|
991
|
+
stroke-dashoffset: -35;
|
|
992
|
+
}
|
|
993
|
+
100% {
|
|
994
|
+
stroke-dasharray: 90 150;
|
|
995
|
+
stroke-dashoffset: -124;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
@media (prefers-reduced-motion: reduce) {
|
|
999
|
+
.mantle-spinner {
|
|
1000
|
+
animation-duration: 2.4s;
|
|
1001
|
+
}
|
|
1002
|
+
.mantle-spinner-arc {
|
|
1003
|
+
animation: none;
|
|
1004
|
+
stroke-dasharray: 70 150;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
/* ============================================================================
|
|
1009
|
+
Live thought trail (components/assistant/thought-trail.tsx). The narrator
|
|
1010
|
+
line shimmers warmly while the turn runs (a highlight sweeps the text);
|
|
1011
|
+
completed steps fade in beneath it. Theme-safe (uses --foreground /
|
|
1012
|
+
--muted-foreground) and honours reduced-motion.
|
|
1013
|
+
========================================================================== */
|
|
1014
|
+
.mantle-shimmer-text {
|
|
1015
|
+
/* Text stays solid (--foreground is the base); a soft glint sweeps across it —
|
|
1016
|
+
a shine over solid letters, never the letters dimming or disappearing. The
|
|
1017
|
+
gradient is sized 300% so it ALWAYS fully covers the text at every position
|
|
1018
|
+
(0%→100%): no uncovered region can fall back to the transparent color, so
|
|
1019
|
+
the text never blanks out. The glint band is narrow and the sweep slow, so
|
|
1020
|
+
it reads as a subtle, occasional shine rather than a constant wipe. */
|
|
1021
|
+
background-image: linear-gradient(
|
|
1022
|
+
100deg,
|
|
1023
|
+
var(--foreground) 44%,
|
|
1024
|
+
var(--muted-foreground) 50%,
|
|
1025
|
+
var(--foreground) 56%
|
|
1026
|
+
);
|
|
1027
|
+
background-size: 300% 100%;
|
|
1028
|
+
background-repeat: no-repeat;
|
|
1029
|
+
-webkit-background-clip: text;
|
|
1030
|
+
background-clip: text;
|
|
1031
|
+
color: transparent;
|
|
1032
|
+
animation: mantle-shimmer 3.6s linear infinite;
|
|
1033
|
+
}
|
|
1034
|
+
@keyframes mantle-shimmer {
|
|
1035
|
+
0% {
|
|
1036
|
+
background-position: 100% 0;
|
|
1037
|
+
}
|
|
1038
|
+
100% {
|
|
1039
|
+
background-position: 0% 0;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
.mantle-trail-step {
|
|
1043
|
+
animation: mantle-trail-in 0.35s ease-out both;
|
|
1044
|
+
}
|
|
1045
|
+
@keyframes mantle-trail-in {
|
|
1046
|
+
from {
|
|
1047
|
+
opacity: 0;
|
|
1048
|
+
transform: translateY(3px);
|
|
1049
|
+
}
|
|
1050
|
+
to {
|
|
1051
|
+
opacity: 1;
|
|
1052
|
+
transform: translateY(0);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1056
|
+
.mantle-shimmer-text {
|
|
1057
|
+
animation: none;
|
|
1058
|
+
background-image: none;
|
|
1059
|
+
-webkit-background-clip: border-box;
|
|
1060
|
+
background-clip: border-box;
|
|
1061
|
+
-webkit-text-fill-color: var(--foreground);
|
|
1062
|
+
color: var(--foreground);
|
|
1063
|
+
}
|
|
1064
|
+
.mantle-trail-step {
|
|
1065
|
+
animation: none;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
/*
|
|
1070
|
+
* Marker pick mode. While the assistant is in pick mode the shell sets
|
|
1071
|
+
* `data-picking` on <body>; every markable row (data-mark-id) then shows a
|
|
1072
|
+
* dashed primary outline + copy cursor, so it's obvious what can be sent to the
|
|
1073
|
+
* assistant. Inert otherwise — the attributes carry no styling on their own.
|
|
1074
|
+
*/
|
|
1075
|
+
body[data-picking='true'] [data-mark-id] {
|
|
1076
|
+
outline: 2px dashed var(--primary);
|
|
1077
|
+
outline-offset: 2px;
|
|
1078
|
+
border-radius: var(--radius);
|
|
1079
|
+
cursor: copy;
|
|
1080
|
+
}
|
|
1081
|
+
body[data-picking='true'] [data-mark-id]:hover {
|
|
1082
|
+
outline-color: var(--ring);
|
|
1083
|
+
background-color: color-mix(in oklch, var(--primary) 12%, transparent);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/* Attention pulse on the assistant button when a run is blocked on a question.
|
|
1087
|
+
Tailwind's `animate-pulse` is unguarded, and this one persists until the
|
|
1088
|
+
panel is opened — WCAG 2.2.2 covers motion that runs past 5s, so honour the
|
|
1089
|
+
OS setting and fall back to a static ring (the count badge still carries
|
|
1090
|
+
the signal). */
|
|
1091
|
+
.mantle-attention-pulse {
|
|
1092
|
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
1093
|
+
}
|
|
1094
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1095
|
+
.mantle-attention-pulse {
|
|
1096
|
+
animation: none;
|
|
1097
|
+
}
|
|
1098
|
+
}
|