You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1071 lines
37 KiB
1071 lines
37 KiB
/**
|
|
* words.css — eidos visual for the Words editor (content layer).
|
|
*
|
|
* Targets the `data-words-*` attributes the engine's render tree emits
|
|
* (see soma `engine/render.ts`) plus the editor chrome that layers over
|
|
* the content: the left block gutter and the floating selection bubble
|
|
* (their Svelte lives in `words-block-gutter.svelte` /
|
|
* `words-bubble.svelte`; the visuals live here). The settings drawer
|
|
* keeps its own recipe.
|
|
*
|
|
* Tokens are the `--words-*` recipe surface (eidos/lib/recipes/base.ts).
|
|
* The reading typography is deliberately document-grade (generous size +
|
|
* line-height + block rhythm), not the tight UI scale.
|
|
*/
|
|
|
|
/* ── Frame ────────────────────────────────────────────────────────────── */
|
|
|
|
[data-words] {
|
|
/* size cascade — md is the default; sm / lg override below.
|
|
content-px is gutter-aware (leaves room for the block handle in the
|
|
left margin + the settings button in the right margin) with enough
|
|
clearance that the dashed block outline never crowds the grip. */
|
|
--_words-content-px: 3rem;
|
|
--_words-content-py: var(--words-content-py-md);
|
|
--_words-content-min: var(--words-content-min-block-size-md);
|
|
--_words-content-max: var(--words-content-max-block-size-md);
|
|
--_words-content-font-size: var(--words-font-size-lg);
|
|
--_words-radius: var(--words-radius-md);
|
|
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
inline-size: 100%;
|
|
font-family: var(--words-font-family);
|
|
color: var(--words-color);
|
|
background: var(--words-bg);
|
|
border: var(--words-border-width) solid var(--words-border);
|
|
border-radius: var(--_words-radius);
|
|
box-shadow: var(--words-shadow);
|
|
transition:
|
|
border-color var(--words-transition-duration) var(--words-transition-ease),
|
|
box-shadow var(--words-transition-duration) var(--words-transition-ease);
|
|
}
|
|
|
|
[data-words][data-size='sm'] {
|
|
--_words-content-px: 2.5rem;
|
|
--_words-content-py: var(--words-content-py-sm);
|
|
--_words-content-min: var(--words-content-min-block-size-sm);
|
|
--_words-content-max: var(--words-content-max-block-size-sm);
|
|
--_words-content-font-size: var(--words-font-size-md);
|
|
--_words-radius: var(--words-radius-sm);
|
|
}
|
|
|
|
[data-words][data-size='lg'] {
|
|
--_words-content-px: 3.5rem;
|
|
--_words-content-py: var(--words-content-py-lg);
|
|
--_words-content-min: var(--words-content-min-block-size-lg);
|
|
--_words-content-max: var(--words-content-max-block-size-lg);
|
|
--_words-content-font-size: calc(var(--words-font-size-lg) + 2px);
|
|
--_words-radius: var(--words-radius-lg);
|
|
}
|
|
|
|
[data-words]:focus-within {
|
|
border-color: var(--words-border-hover);
|
|
box-shadow: var(--words-focus-shadow);
|
|
}
|
|
|
|
[data-words][data-invalid] {
|
|
border-color: var(--words-invalid-border);
|
|
}
|
|
|
|
[data-words][data-disabled] {
|
|
opacity: var(--words-disabled-opacity);
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Pre-mount frame (client-only editor — see words.svelte). Holds the
|
|
min height so swapping in the live editor doesn't shift layout. */
|
|
[data-words][aria-busy='true'] {
|
|
min-block-size: calc(var(--_words-content-min) + 2 * var(--_words-content-py));
|
|
}
|
|
|
|
/* ── Left gutter — block handle ───────────────────────────────────────── */
|
|
|
|
[data-words-block-gutter] {
|
|
position: absolute;
|
|
inset-inline-start: 0.35rem;
|
|
z-index: 2;
|
|
/* The script sets `top` to the block's vertical midpoint; this
|
|
transform pulls the grip up by half its own height so the icon
|
|
lands ON the midpoint rather than starting from it. Works
|
|
regardless of block height (paragraph, code, table, image). */
|
|
transform: translateY(-50%);
|
|
/* slide smoothly between blocks as the cursor moves */
|
|
transition: top 120ms var(--words-transition-ease);
|
|
animation: words-gutter-in 140ms var(--words-transition-ease);
|
|
}
|
|
@keyframes words-gutter-in {
|
|
/* `translateY(-50%)` keeps the grip vertically centered against the
|
|
block midpoint — the keyframe MUST preserve it on both frames, or
|
|
the end state snaps the grip back to its `top` anchor and the
|
|
centering is lost the moment the animation completes. */
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(-50%) translateX(-4px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(-50%) translateX(0);
|
|
}
|
|
}
|
|
[data-words-block-gutter] [data-words-block-handle] {
|
|
color: var(--words-placeholder-color);
|
|
cursor: grab;
|
|
transition:
|
|
transform var(--words-transition-duration) var(--words-transition-ease),
|
|
color var(--words-transition-duration) var(--words-transition-ease);
|
|
}
|
|
/* subtle displacement when the cursor lands on the handle */
|
|
[data-words-block-gutter] [data-words-block-handle]:hover {
|
|
color: var(--words-content-color);
|
|
transform: translateX(1px) scale(1.18);
|
|
}
|
|
|
|
/* dashed outline emphasising the block the handle acts on */
|
|
[data-words-block-outline] {
|
|
position: absolute;
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
border: 1px dashed var(--words-border-hover);
|
|
border-radius: var(--words-radius-sm);
|
|
animation: words-outline-in var(--words-transition-duration) var(--words-transition-ease);
|
|
}
|
|
@keyframes words-outline-in {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
/* ── Selection bubble toolbar ─────────────────────────────────────────── */
|
|
|
|
/* Floating card, viewport-anchored (the soma BubbleMenu writes
|
|
--_words-bubble-x / -y from the selection's client rect, so position
|
|
is fixed). Centred horizontally on the selection; sits above it for
|
|
side='top', below for side='bottom'. */
|
|
[data-words-bubble-menu] {
|
|
position: fixed;
|
|
inset-block-start: var(--_words-bubble-y, 0);
|
|
inset-inline-start: var(--_words-bubble-x, 0);
|
|
z-index: 30;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--words-command-gap);
|
|
padding: var(--space-1);
|
|
background: var(--words-bg);
|
|
border: var(--words-border-width) solid var(--words-border);
|
|
border-radius: var(--words-radius-md);
|
|
box-shadow: var(--shadow-overlay);
|
|
animation: words-bubble-in 120ms var(--words-transition-ease);
|
|
}
|
|
[data-words-bubble-menu][data-side='top'] {
|
|
transform: translate(-50%, calc(-100% - 0.5rem));
|
|
}
|
|
[data-words-bubble-menu][data-side='bottom'] {
|
|
transform: translate(-50%, 0.5rem);
|
|
}
|
|
[data-words-bubble-menu][hidden] {
|
|
display: none;
|
|
}
|
|
/* Opacity-only so the keyframe doesn't fight the data-side transform. */
|
|
@keyframes words-bubble-in {
|
|
from {
|
|
opacity: 0;
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
/* Active mark — the soma CommandButton stamps data-active / aria-pressed
|
|
on the composed eidos Button; tint it to read as "on". (The Button
|
|
recipe owns every other state: hover, focus, disabled, sizing.) */
|
|
[data-words-bubble-menu] [data-button][data-active] {
|
|
background: var(--words-primary-solid);
|
|
color: var(--words-command-solid-color);
|
|
}
|
|
|
|
/* Thin vertical rule between groups. */
|
|
[data-words-bubble-menu] [data-words-bubble-divider] {
|
|
flex: 0 0 auto;
|
|
inline-size: 1px;
|
|
block-size: 1.15rem;
|
|
margin-inline: var(--space-0-5);
|
|
background: var(--words-border);
|
|
}
|
|
|
|
/* Turn-into trigger (an eidos Button) — keep it slim in the bubble. */
|
|
[data-words-bubble-menu] [data-words-bubble-trigger] {
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Inline link editor row (replaces the toolbar while editing a link). */
|
|
[data-words-bubble-menu] [data-words-bubble-link] {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--space-1);
|
|
}
|
|
[data-words-bubble-menu] [data-words-bubble-link] input {
|
|
inline-size: 15rem;
|
|
padding: var(--space-1) var(--space-2);
|
|
border: var(--words-border-width) solid var(--words-border);
|
|
border-radius: var(--words-command-radius);
|
|
background: var(--words-bg);
|
|
color: var(--words-content-color);
|
|
font-family: inherit;
|
|
font-size: var(--words-font-size-md);
|
|
outline: none;
|
|
}
|
|
[data-words-bubble-menu] [data-words-bubble-link] input:focus {
|
|
border-color: var(--words-border-hover);
|
|
}
|
|
|
|
/* Turn-into menu item — label + trailing active tick. */
|
|
[data-words-bubble-into-item] {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--space-4);
|
|
}
|
|
/* trailing active tick (eidos Icon) */
|
|
[data-words-bubble-into-item] [data-icon] {
|
|
color: var(--words-primary-text);
|
|
}
|
|
|
|
/* ── Block inspector ──────────────────────────────────────────────────── */
|
|
|
|
/* Shared body — a vertical stack of labelled property rows. Reused by
|
|
every inspector presentation (sidebar / drawer / popover). The vertical
|
|
layout gives each control a full-width row, so nothing crowds. */
|
|
/* The inspector body lives inside a soma `Words.Drawer` (for live provider
|
|
reactivity); that host adds no layout — only the inner panel does. */
|
|
[data-words-inspector-host] {
|
|
display: contents;
|
|
}
|
|
[data-words-inspector] {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-3);
|
|
/* Tightened from space-4 → space-2 (16px → 8px) — the sidebar is narrow
|
|
and the previous padding ate ~20% of the usable inline width. */
|
|
padding: var(--space-2);
|
|
/* Force the system sans stack on EVERY descendant of the inspector
|
|
(labels, values, subsection titles, NumberField inputs, ToggleGroup
|
|
chips, accordion triggers, table cell counters, etc.). The editor's
|
|
content font can be serif when the surface is themed for reading
|
|
(`Words` body inherits `--words-font-family`); the inspector chrome
|
|
must stay sans regardless or the controls render with mixed
|
|
typography.
|
|
|
|
The container declaration alone is NOT enough — eidos primitives
|
|
(Accordion.Trigger, Button, NumberField.Input, ToggleGroup.Item,
|
|
…) set their own `font-family` in their recipe CSS, breaking the
|
|
cascade. The `* { font-family: inherit }` rule forces every
|
|
descendant to inherit from the container regardless of recipe
|
|
overrides. This is the standard pattern when you want a subtree
|
|
to share one typeface against shipped components that ship their
|
|
own font choices. */
|
|
font-family:
|
|
system-ui,
|
|
-apple-system,
|
|
'Segoe UI',
|
|
Roboto,
|
|
Helvetica,
|
|
Arial,
|
|
sans-serif;
|
|
}
|
|
[data-words-inspector] *,
|
|
[data-words-inspector] *::before,
|
|
[data-words-inspector] *::after {
|
|
font-family: inherit;
|
|
}
|
|
[data-words-inspector-title] {
|
|
font-size: var(--words-font-size-md);
|
|
font-weight: var(--words-strong-font-weight);
|
|
color: var(--words-content-color);
|
|
}
|
|
[data-words-inspector-section] {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-2-5);
|
|
}
|
|
[data-words-inspector-section-title] {
|
|
font-size: var(--words-font-size-sm);
|
|
font-weight: var(--words-strong-font-weight);
|
|
color: var(--words-placeholder-color);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
[data-words-inspector-row] {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-1-5);
|
|
}
|
|
/* Inspector row gated by a sibling toggle (e.g. image Width / Height /
|
|
Align gated by Fullwidth). Reduce contrast + block pointer events so
|
|
the user sees the field is dimmed but not removed (gives context for
|
|
what Fullwidth controls). The actual controls inside still set
|
|
`disabled`/`aria-disabled`; this is the visual sugar. */
|
|
[data-words-inspector-row][data-disabled] {
|
|
opacity: 0.45;
|
|
pointer-events: none;
|
|
}
|
|
/* Image src field shares its row with an inline `Upload` button. The
|
|
wrapping group is the row's "value" slot — `data-inline` gives it
|
|
`justify-content: space-between` against the label, and the inner
|
|
flex distributes input (stretch) + button (auto) along the row. */
|
|
[data-words-inspector-src-group] {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: var(--space-1);
|
|
flex: 1;
|
|
min-inline-size: 0;
|
|
}
|
|
[data-words-inspector-src-group] input[data-words-inspector-text] {
|
|
flex: 1;
|
|
min-inline-size: 0;
|
|
}
|
|
/* Single-line row variant — label on the left, control on the right.
|
|
Used by `WordsColorRow` (each color = `Label [Picker]` per the
|
|
inspector's compact spec). Sliders / toggle groups keep the stacked
|
|
layout (label above, control below) because the control is wider. */
|
|
[data-words-inspector-row][data-inline] {
|
|
flex-direction: row;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--space-2);
|
|
}
|
|
/* The eidos ColorPicker root declares `inline-size: 100%` (so it fills
|
|
form rows by default). Inside an inline inspector row that override
|
|
collapses the label-to-picker distribution because the picker stretches
|
|
to the full remaining width. Pin to auto here so `justify-content:
|
|
space-between` actually pushes the picker's trigger to the right edge. */
|
|
[data-words-inspector-row][data-inline] [data-color-picker] {
|
|
inline-size: auto;
|
|
}
|
|
/* Pin the trigger to a fixed inline size so every color row's picker has
|
|
the same visual width regardless of the hex string length (6 chars vs
|
|
8 chars with alpha). `tabular-nums` keeps the hex characters at a
|
|
consistent advance width so the text doesn't jitter while the picker
|
|
value oscillates during drag. */
|
|
[data-words-inspector-row][data-inline] [data-color-picker-trigger] {
|
|
inline-size: 7.5rem;
|
|
justify-content: flex-start;
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
[data-words-inspector-rowhead] {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
}
|
|
[data-words-inspector-label] {
|
|
font-size: var(--words-font-size-sm);
|
|
color: var(--words-command-color);
|
|
/* Font-family is inherited from `[data-words-inspector]` (forced sans
|
|
at the container level). */
|
|
}
|
|
[data-words-inspector-value] {
|
|
font-size: var(--words-font-size-sm);
|
|
color: var(--words-content-color);
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
/* Compact numeric input in `WordsNumRow` rowhead — pins width so the
|
|
label / slider layout doesn't shift as the user types longer numbers.
|
|
`tabular-nums` keeps the digits at uniform advance width. */
|
|
[data-words-inspector-rowhead] [data-number-field] {
|
|
inline-size: 4.5rem;
|
|
}
|
|
[data-words-inspector-rowhead] [data-number-field-input] {
|
|
inline-size: 100%;
|
|
text-align: end;
|
|
font-variant-numeric: tabular-nums;
|
|
font-size: var(--words-font-size-sm);
|
|
}
|
|
|
|
/* WordsSpacingRow — 4-side editor with PAIR chain.
|
|
Layout: a vertical stack of TWO pair rows. Each pair holds two cells
|
|
(label-above-input) plus its OWN link toggle. The two pairs are
|
|
independent — chaining one does not chain the other. This makes the
|
|
pair semantics unmistakable visually. */
|
|
[data-words-inspector-spacing] {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-2);
|
|
}
|
|
[data-words-inspector-spacing-head] {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: var(--space-2);
|
|
}
|
|
[data-words-inspector-spacing-grid] {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-2);
|
|
}
|
|
[data-words-inspector-spacing-pair] {
|
|
display: grid;
|
|
/* Two equal-width inputs + a small fixed slot for the link toggle. */
|
|
grid-template-columns: 1fr 1fr auto;
|
|
align-items: end;
|
|
gap: var(--space-1);
|
|
/* Bump icon size in this scope so the link/unlink chip is visible at
|
|
the Toggle xs size (default `--icon-size` is too small here). */
|
|
--icon-size: 14px;
|
|
}
|
|
[data-words-inspector-spacing-cell] {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
min-inline-size: 0;
|
|
}
|
|
[data-words-inspector-spacing-cell-label] {
|
|
font-size: var(--words-font-size-xs, 0.75rem);
|
|
color: var(--words-content-color);
|
|
opacity: 0.7;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
[data-words-inspector-spacing-cell] [data-number-field] {
|
|
inline-size: 100%;
|
|
min-inline-size: 0;
|
|
}
|
|
[data-words-inspector-spacing-cell] [data-number-field-input] {
|
|
inline-size: 100%;
|
|
text-align: end;
|
|
font-variant-numeric: tabular-nums;
|
|
font-size: var(--words-font-size-sm);
|
|
}
|
|
|
|
/* Block panel — bare text inputs (code language, image alt + caption).
|
|
Styled to match the inspector chrome (sans, sm size, ghost
|
|
background) so they sit among the ToggleGroups without visual noise.
|
|
`<datalist>` suggestions render natively on focus. The shared
|
|
`[data-words-inspector-text]` selector is for plain text edits;
|
|
`[data-words-inspector-code-language]` keeps the original selector
|
|
for backward compatibility — both inherit the same base rules. */
|
|
[data-words-inspector-text],
|
|
[data-words-inspector-code-language] {
|
|
flex: 1 1 0;
|
|
min-inline-size: 0;
|
|
font: inherit;
|
|
font-size: var(--words-font-size-sm);
|
|
padding: 4px 8px;
|
|
border: 1px solid var(--words-border);
|
|
border-radius: var(--radius-2, 4px);
|
|
background: transparent;
|
|
color: inherit;
|
|
}
|
|
[data-words-inspector-text]:focus-visible,
|
|
[data-words-inspector-code-language]:focus-visible {
|
|
outline: 2px solid var(--color-primary, currentColor);
|
|
outline-offset: -1px;
|
|
}
|
|
|
|
/* Callout intent swatch — small color dot rendered INSIDE the
|
|
ToggleGroup chip alongside its localized label. Color comes from
|
|
the doctrinal palette token for each intent (so a `risk` swatch
|
|
matches the actual `risk` callout's accent). Lives in the chip
|
|
button's flex flow; sits at the inline-start. */
|
|
[data-words-intent-swatch] {
|
|
display: inline-block;
|
|
inline-size: 0.625rem;
|
|
block-size: 0.625rem;
|
|
border-radius: 50%;
|
|
margin-inline-end: 4px;
|
|
vertical-align: -1px;
|
|
background: var(--color-neutral-solid);
|
|
}
|
|
[data-words-intent-swatch][data-intent='neutral'] {
|
|
background: var(--color-neutral-solid);
|
|
}
|
|
[data-words-intent-swatch][data-intent='affirm'] {
|
|
background: var(--color-affirm-solid);
|
|
}
|
|
[data-words-intent-swatch][data-intent='fulfill'] {
|
|
background: var(--color-fulfill-solid);
|
|
}
|
|
[data-words-intent-swatch][data-intent='risk'] {
|
|
background: var(--color-risk-solid);
|
|
}
|
|
[data-words-intent-swatch][data-intent='threat'] {
|
|
background: var(--color-threat-solid);
|
|
}
|
|
[data-words-intent-swatch][data-intent='loss'] {
|
|
background: var(--color-loss-solid);
|
|
}
|
|
|
|
/* Border-style swatch — the chip IS the line preview. No lucide icon
|
|
for solid/dashed/dotted is fitting, so we render a short top border
|
|
in the current ink. The swatch line scales with the chip width so
|
|
bigger sizes show a longer sample. */
|
|
[data-words-border-style-swatch] {
|
|
display: inline-block;
|
|
inline-size: 1.25rem;
|
|
block-size: 0;
|
|
border-block-start-width: 2px;
|
|
border-block-start-color: currentColor;
|
|
}
|
|
[data-words-border-style-swatch='solid'] {
|
|
border-block-start-style: solid;
|
|
}
|
|
[data-words-border-style-swatch='dashed'] {
|
|
border-block-start-style: dashed;
|
|
}
|
|
[data-words-border-style-swatch='dotted'] {
|
|
border-block-start-style: dotted;
|
|
}
|
|
|
|
/* WordsBlockPanel hierarchical subsections (table cell + table).
|
|
Sits inside `data-words-inspector-section` and groups related rows
|
|
under a small label. Keeps the visual separation between scopes
|
|
(cell-level vs table-level) without the heaviness of a second
|
|
accordion item. */
|
|
[data-words-inspector-subsection] {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-2);
|
|
padding-block: var(--space-1);
|
|
}
|
|
[data-words-inspector-subsection] + [data-words-inspector-subsection] {
|
|
border-block-start: 1px dashed var(--words-border);
|
|
padding-block-start: var(--space-2);
|
|
}
|
|
[data-words-inspector-subsection-title] {
|
|
font-size: var(--words-font-size-xs, 0.75rem);
|
|
color: var(--words-content-color);
|
|
opacity: 0.75;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
font-weight: 600;
|
|
}
|
|
/* Table insert/delete ops row — 3 small buttons in a horizontal flex
|
|
group (above/below/delete for rows; left/right/delete for columns).
|
|
Sits below the count display so the user reads "Rows: 3" then sees
|
|
the actions, mirroring Notion/Word table panels. */
|
|
[data-words-inspector-table-ops] {
|
|
display: flex;
|
|
gap: var(--space-1);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
[data-words-inspector-empty] {
|
|
padding: var(--space-2) 0;
|
|
font-size: var(--words-font-size-sm);
|
|
color: var(--words-placeholder-color);
|
|
}
|
|
|
|
/* Sidebar — docked, always visible (builder-style). The rail reserves a
|
|
right gutter; the sidebar itself is absolutely positioned so the frame's
|
|
height follows the CONTENT, not the (usually taller) inspector. A flex
|
|
row would stretch the frame to the inspector's height and leave dead
|
|
space below the text — the inspector instead scrolls within the content
|
|
height. */
|
|
[data-words][data-inspector='sidebar'] {
|
|
padding-inline-end: 16rem;
|
|
}
|
|
[data-words-sidebar] {
|
|
position: absolute;
|
|
inset-block: 0;
|
|
inset-inline-end: 0;
|
|
inline-size: 16rem;
|
|
overflow-y: auto;
|
|
border-inline-start: var(--words-border-width) solid var(--words-border);
|
|
border-start-end-radius: var(--_words-radius);
|
|
border-end-end-radius: var(--_words-radius);
|
|
background: var(--words-bg);
|
|
}
|
|
|
|
/* Drawer — slide-in panel from the right edge + a fixed toggle. */
|
|
[data-words-drawer-toggle] {
|
|
position: absolute;
|
|
inset-block-start: var(--space-3);
|
|
inset-inline-end: var(--space-3);
|
|
z-index: 21;
|
|
}
|
|
[data-words-drawer-panel] {
|
|
position: absolute;
|
|
inset-block: 0;
|
|
inset-inline-end: 0;
|
|
z-index: 20;
|
|
inline-size: 16rem;
|
|
max-inline-size: 85%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow-y: auto;
|
|
background: var(--words-bg);
|
|
border-inline-start: var(--words-border-width) solid var(--words-border);
|
|
border-start-end-radius: var(--_words-radius);
|
|
border-end-end-radius: var(--_words-radius);
|
|
box-shadow: var(--shadow-overlay);
|
|
transform: translateX(100%);
|
|
transition: transform var(--words-transition-duration) var(--words-transition-ease);
|
|
}
|
|
[data-words-drawer-panel][data-open] {
|
|
transform: translateX(0);
|
|
}
|
|
[data-words-drawer-panel-head] {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: var(--space-2) var(--space-2) var(--space-2) var(--space-4);
|
|
border-block-end: var(--words-border-width) solid var(--words-border);
|
|
font-size: var(--words-font-size-md);
|
|
font-weight: var(--words-strong-font-weight);
|
|
color: var(--words-content-color);
|
|
}
|
|
|
|
/* Popover — a corner toggle floats the inspector. */
|
|
[data-words-popover-anchor] {
|
|
position: absolute;
|
|
inset-block-start: var(--space-3);
|
|
inset-inline-end: var(--space-3);
|
|
z-index: 21;
|
|
}
|
|
[data-words-inspector-popover] {
|
|
inline-size: 16rem;
|
|
padding: 0;
|
|
}
|
|
|
|
/* ── Content surface (the contenteditable) ───────────────────────────── */
|
|
|
|
[data-words-content] {
|
|
position: relative;
|
|
flex: 1 1 auto;
|
|
min-block-size: var(--_words-content-min);
|
|
max-block-size: var(--_words-content-max);
|
|
overflow-y: auto;
|
|
padding: var(--_words-content-py) var(--_words-content-px);
|
|
font-size: var(--_words-content-font-size);
|
|
line-height: 1.7;
|
|
color: var(--words-content-color);
|
|
outline: none;
|
|
-webkit-font-smoothing: antialiased;
|
|
text-rendering: optimizeLegibility;
|
|
}
|
|
|
|
[data-words-content] :where([data-words-node='block']):first-child {
|
|
margin-block-start: 0;
|
|
}
|
|
[data-words-content] :where([data-words-node='block']):last-child {
|
|
margin-block-end: 0;
|
|
}
|
|
|
|
/* ── Placeholder ──────────────────────────────────────────────────────── */
|
|
|
|
[data-words-placeholder] {
|
|
position: absolute;
|
|
inset-block-start: var(--_words-content-py);
|
|
inset-inline-start: var(--_words-content-px);
|
|
font-size: var(--_words-content-font-size);
|
|
line-height: 1.7;
|
|
color: var(--words-placeholder-color);
|
|
pointer-events: none;
|
|
user-select: none;
|
|
}
|
|
[data-words-placeholder][data-hidden] {
|
|
display: none;
|
|
}
|
|
|
|
/* ── Block rhythm ─────────────────────────────────────────────────────── */
|
|
|
|
[data-words-content] :where(p, h1, h2, h3, blockquote, pre, ul, ol, table, figure, hr, [data-words-block='callout']) {
|
|
margin-block: var(--words-block-gap) 0;
|
|
}
|
|
|
|
/* Columns block — flex layout for side-by-side content. Each child
|
|
`[data-words-column]` is a flex item; the engine sets `flex: <user-
|
|
chosen value or 1>` inline so equal-width is the default and the
|
|
user can resize via the inspector. Gap reuses the block-gap token
|
|
so columns breathe the same vertical rhythm as the surrounding
|
|
content. Each column is also a flex container itself (column
|
|
direction) so its nested blocks stack vertically. */
|
|
[data-words-content] [data-words-block='columns'] {
|
|
display: flex;
|
|
flex-direction: row;
|
|
gap: var(--words-block-gap);
|
|
margin-block: var(--words-block-gap) 0;
|
|
align-items: stretch;
|
|
}
|
|
[data-words-content] [data-words-block='columns'] [data-words-column] {
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-inline-size: 0;
|
|
/* Visual cue while editing — without this, an empty columns block
|
|
collapses to 0 height and the user can't see where the layout is.
|
|
The dashed outline + breathing min-height make the structure
|
|
discoverable; the soft tone fades into the background once the
|
|
user fills the columns with real content. No padding — child
|
|
blocks own their own padding via the inspector's Spacing section
|
|
so the column dashed border traces exactly the content edge. */
|
|
min-block-size: 3em;
|
|
padding: 0;
|
|
border: 1px dashed var(--words-border);
|
|
border-radius: var(--radius-2, 4px);
|
|
transition: border-color var(--words-transition-duration) var(--words-transition-ease);
|
|
}
|
|
[data-words-content] [data-words-block='columns'] [data-words-column]:hover,
|
|
[data-words-content] [data-words-block='columns'] [data-words-column]:focus-within {
|
|
border-color: var(--words-content-color);
|
|
}
|
|
/* Reset the universal block-rhythm `margin-block` for the FIRST nested
|
|
block of each column — otherwise that extra gap pushes the column
|
|
contents down relative to the column's frame, breaking vertical
|
|
alignment with siblings. */
|
|
[data-words-content] [data-words-block='columns'] [data-words-column] > *:first-child {
|
|
margin-block-start: 0;
|
|
}
|
|
|
|
|
|
/* ── Paragraph ────────────────────────────────────────────────────────── */
|
|
|
|
[data-words-block='paragraph'] {
|
|
margin-block: var(--words-block-gap) 0;
|
|
}
|
|
|
|
/* ── Headings ─────────────────────────────────────────────────────────── */
|
|
|
|
[data-words-block='heading'] {
|
|
margin-block: calc(var(--words-block-gap) * 1.5) 0;
|
|
color: var(--words-heading-color);
|
|
font-weight: var(--words-heading-font-weight);
|
|
line-height: var(--words-heading-line-height);
|
|
letter-spacing: -0.01em;
|
|
}
|
|
h1[data-words-block='heading'] {
|
|
font-size: var(--words-heading-h1-font-size);
|
|
}
|
|
h2[data-words-block='heading'] {
|
|
font-size: var(--words-heading-h2-font-size);
|
|
}
|
|
h3[data-words-block='heading'] {
|
|
font-size: var(--words-heading-h3-font-size);
|
|
}
|
|
|
|
/* ── Quote ────────────────────────────────────────────────────────────── */
|
|
|
|
blockquote[data-words-block='quote'] {
|
|
padding-inline-start: var(--words-quote-px);
|
|
border-inline-start: var(--words-quote-border-width) solid var(--words-primary-border);
|
|
color: var(--words-quote-color);
|
|
font-style: italic;
|
|
}
|
|
|
|
/* ── Code block ───────────────────────────────────────────────────────── */
|
|
|
|
pre[data-words-block='code'] {
|
|
padding: calc(var(--words-code-py) * 3) calc(var(--words-code-px) * 3);
|
|
border-radius: var(--words-radius-sm);
|
|
background: var(--words-code-bg);
|
|
color: var(--words-code-color);
|
|
font-family: var(--words-code-font-family);
|
|
font-size: 0.9em;
|
|
line-height: 1.6;
|
|
overflow-x: auto;
|
|
tab-size: 2;
|
|
}
|
|
pre[data-words-block='code'] code {
|
|
font-family: inherit;
|
|
white-space: pre;
|
|
}
|
|
|
|
/* syntax tokens (highlightWordsCode) */
|
|
[data-words-code-token='keyword'] {
|
|
color: var(--words-primary-text);
|
|
font-weight: var(--words-strong-font-weight);
|
|
}
|
|
[data-words-code-token='string'] {
|
|
color: var(--words-affirm-text);
|
|
}
|
|
[data-words-code-token='number'] {
|
|
color: var(--words-fulfill-text);
|
|
}
|
|
[data-words-code-token='comment'] {
|
|
color: var(--words-content-color);
|
|
opacity: 0.6;
|
|
font-style: italic;
|
|
}
|
|
[data-words-code-token='function'] {
|
|
color: var(--words-secondary-text);
|
|
}
|
|
|
|
/* ── Lists ────────────────────────────────────────────────────────────── */
|
|
|
|
[data-words-block='list'] {
|
|
padding-inline-start: var(--words-list-padding-inline-start);
|
|
}
|
|
[data-words-block='list'][data-words-list-kind='check'] {
|
|
padding-inline-start: 0;
|
|
list-style: none;
|
|
}
|
|
li[data-words-node='list-item'] {
|
|
margin-block: 0.25em;
|
|
}
|
|
li[data-words-node='list-item'][data-words-indent='1'] {
|
|
margin-inline-start: var(--words-list-padding-inline-start);
|
|
}
|
|
li[data-words-node='list-item'][data-words-indent='2'] {
|
|
margin-inline-start: calc(var(--words-list-padding-inline-start) * 2);
|
|
}
|
|
|
|
/* check toggle */
|
|
[data-words-list-kind='check'] li[data-words-node='list-item'] {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: var(--space-2);
|
|
}
|
|
[data-words-check-toggle] {
|
|
flex: 0 0 auto;
|
|
inline-size: 1.1em;
|
|
block-size: 1.1em;
|
|
margin-block-start: 0.3em;
|
|
border: 1.5px solid var(--words-border-hover);
|
|
border-radius: var(--words-radius-sm);
|
|
background: var(--words-bg);
|
|
cursor: pointer;
|
|
transition:
|
|
background var(--words-transition-duration) var(--words-transition-ease),
|
|
border-color var(--words-transition-duration) var(--words-transition-ease);
|
|
}
|
|
[data-words-check-toggle][data-words-checked='true'] {
|
|
border-color: var(--words-primary-solid);
|
|
background: var(--words-primary-solid)
|
|
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='white' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 8.5l3 3 6-7'/%3E%3C/svg%3E")
|
|
center / 70% no-repeat;
|
|
}
|
|
[data-words-checked='true'] :where(span[data-words-node='text']) {
|
|
color: var(--words-content-color);
|
|
opacity: 0.55;
|
|
text-decoration: line-through;
|
|
}
|
|
|
|
/* ── Table ────────────────────────────────────────────────────────────── */
|
|
|
|
table[data-words-block='table'] {
|
|
inline-size: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 0.95em;
|
|
}
|
|
table[data-words-block='table'] :where(td, th) {
|
|
padding: var(--space-1-5) var(--space-2-5);
|
|
border: var(--words-border-width) solid var(--words-border);
|
|
text-align: start;
|
|
vertical-align: top;
|
|
}
|
|
table[data-words-block='table'] th {
|
|
background: var(--words-code-bg);
|
|
font-weight: var(--words-strong-font-weight);
|
|
}
|
|
[data-words-node='table-cell'][data-words-cell-align='center'] {
|
|
text-align: center;
|
|
}
|
|
[data-words-node='table-cell'][data-words-cell-align='right'] {
|
|
text-align: end;
|
|
}
|
|
[data-words-node='table-cell'][data-words-cell-vertical='middle'] {
|
|
vertical-align: middle;
|
|
}
|
|
[data-words-node='table-cell'][data-words-cell-vertical='bottom'] {
|
|
vertical-align: bottom;
|
|
}
|
|
|
|
/* ── Image ────────────────────────────────────────────────────────────── */
|
|
|
|
figure[data-words-block='image'] {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: var(--space-2);
|
|
/* Browser default for <figure> is `margin-inline: 40px` — it eats
|
|
~80px from every column / container the figure sits in. Reset to 0
|
|
so the image can use the full content area of its parent (the
|
|
block-gap-aware margin-block reset above only touched the vertical
|
|
axis). */
|
|
margin-inline: 0;
|
|
}
|
|
/* When no explicit width attr is set, cap at the container so the image
|
|
doesn't break layout. When the user typed an explicit width in the
|
|
inspector (renders as `<img width="X">`), let that win — including
|
|
values LARGER than the natural / container size (intentional upscale
|
|
for hero use-cases). */
|
|
figure[data-words-block='image'] img:not([width]) {
|
|
max-inline-size: 100%;
|
|
block-size: auto;
|
|
border-radius: var(--words-radius-sm);
|
|
}
|
|
figure[data-words-block='image'] img[width] {
|
|
block-size: auto;
|
|
border-radius: var(--words-radius-sm);
|
|
}
|
|
figure[data-words-block='image'][data-words-image-align='left'] {
|
|
align-items: flex-start;
|
|
}
|
|
figure[data-words-block='image'][data-words-image-align='right'] {
|
|
align-items: flex-end;
|
|
}
|
|
/* Fullwidth: drop the figure's flex layout (which sized it to its
|
|
content + caused overflow on `inline-size: 100%` chains with the
|
|
column's flex parent) and force the figure to fill its container.
|
|
The column is `display: flex; align-items: normal` and its child
|
|
flex items default to `flex: 0 1 auto` — that auto basis collapses
|
|
block-level children to their intrinsic content (= the img's natural
|
|
width). Setting `inline-size: 100%` explicitly + `box-sizing:
|
|
border-box` is what makes the figure fill the column's content area
|
|
without overflowing past its padding. The img inside then stretches
|
|
inside the figure. */
|
|
figure[data-words-block='image'][data-words-image-full-width] {
|
|
display: block;
|
|
inline-size: 100%;
|
|
max-inline-size: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
figure[data-words-block='image'][data-words-image-full-width] img {
|
|
display: block;
|
|
inline-size: 100%;
|
|
max-inline-size: 100%;
|
|
block-size: auto;
|
|
box-sizing: border-box;
|
|
}
|
|
figure[data-words-block='image'] figcaption {
|
|
font-size: 0.85em;
|
|
color: var(--words-quote-color);
|
|
text-align: center;
|
|
}
|
|
figure[data-words-block='image'][data-words-block-selected] {
|
|
outline: 2px solid var(--words-primary-solid);
|
|
outline-offset: 3px;
|
|
border-radius: var(--words-radius-sm);
|
|
}
|
|
figure[data-words-block='image'][data-words-image-status='uploading'] {
|
|
opacity: 0.6;
|
|
}
|
|
figure[data-words-block='image'][data-words-image-status='error'] {
|
|
outline: 2px solid var(--words-threat-solid);
|
|
outline-offset: 3px;
|
|
}
|
|
|
|
/* ── Divider ──────────────────────────────────────────────────────────── */
|
|
|
|
hr[data-words-block='divider'] {
|
|
block-size: 0;
|
|
border: none;
|
|
border-block-start: var(--words-border-width) solid var(--words-border);
|
|
margin-block: calc(var(--words-block-gap) * 1.25) 0;
|
|
}
|
|
hr[data-words-block='divider'][data-words-block-selected] {
|
|
border-block-start-color: var(--words-primary-solid);
|
|
border-block-start-width: 2px;
|
|
}
|
|
|
|
/* ── Callout ──────────────────────────────────────────────────────────── */
|
|
|
|
[data-words-block='callout'] {
|
|
padding: var(--space-3) var(--space-4);
|
|
border: var(--words-border-width) solid var(--words-neutral-border);
|
|
border-inline-start-width: 3px;
|
|
border-radius: var(--words-radius-sm);
|
|
background: var(--words-neutral-track);
|
|
}
|
|
[data-words-callout-title] {
|
|
margin-block-end: var(--space-1);
|
|
font-weight: var(--words-strong-font-weight);
|
|
}
|
|
[data-words-block='callout'] :where([data-words-node='block']):first-child {
|
|
margin-block-start: 0;
|
|
}
|
|
[data-words-callout-intent='affirm'] {
|
|
border-color: var(--words-affirm-border);
|
|
background: var(--words-affirm-track);
|
|
}
|
|
[data-words-callout-intent='affirm'] [data-words-callout-title] {
|
|
color: var(--words-affirm-text);
|
|
}
|
|
[data-words-callout-intent='fulfill'] {
|
|
border-color: var(--words-fulfill-border);
|
|
background: var(--words-fulfill-track);
|
|
}
|
|
[data-words-callout-intent='fulfill'] [data-words-callout-title] {
|
|
color: var(--words-fulfill-text);
|
|
}
|
|
[data-words-callout-intent='risk'] {
|
|
border-color: var(--words-risk-border);
|
|
background: var(--words-risk-track);
|
|
}
|
|
[data-words-callout-intent='risk'] [data-words-callout-title] {
|
|
color: var(--words-risk-text);
|
|
}
|
|
[data-words-callout-intent='threat'] {
|
|
border-color: var(--words-threat-border);
|
|
background: var(--words-threat-track);
|
|
}
|
|
[data-words-callout-intent='threat'] [data-words-callout-title] {
|
|
color: var(--words-threat-text);
|
|
}
|
|
[data-words-callout-intent='loss'] {
|
|
border-color: var(--words-loss-border);
|
|
background: var(--words-loss-track);
|
|
}
|
|
[data-words-callout-intent='loss'] [data-words-callout-title] {
|
|
color: var(--words-loss-text);
|
|
}
|
|
|
|
/* ── Inline marks ─────────────────────────────────────────────────────── */
|
|
|
|
[data-words-marks~='bold'] {
|
|
font-weight: var(--words-strong-font-weight);
|
|
}
|
|
[data-words-marks~='italic'] {
|
|
font-style: italic;
|
|
}
|
|
[data-words-marks~='underline'] {
|
|
text-decoration-line: underline;
|
|
text-underline-offset: 0.18em;
|
|
}
|
|
[data-words-marks~='strike'] {
|
|
text-decoration-line: line-through;
|
|
}
|
|
[data-words-marks~='underline'][data-words-marks~='strike'] {
|
|
text-decoration-line: underline line-through;
|
|
}
|
|
[data-words-marks~='code'] {
|
|
padding: var(--words-code-py) var(--words-code-px);
|
|
border-radius: var(--words-code-radius);
|
|
background: var(--words-code-bg);
|
|
font-family: var(--words-code-font-family);
|
|
font-size: 0.9em;
|
|
}
|
|
|
|
/* ── Link ─────────────────────────────────────────────────────────────── */
|
|
|
|
a[data-words-node='link'] {
|
|
color: var(--words-primary-text);
|
|
text-decoration-line: underline;
|
|
text-underline-offset: 0.15em;
|
|
text-decoration-thickness: 1px;
|
|
cursor: pointer;
|
|
}
|
|
a[data-words-node='link']:hover {
|
|
text-decoration-thickness: 2px;
|
|
}
|
|
|
|
/* ── Find/replace matches ─────────────────────────────────────────────── */
|
|
|
|
mark[data-words-find-match] {
|
|
border-radius: 2px;
|
|
background: var(--words-fulfill-track);
|
|
color: inherit;
|
|
}
|
|
mark[data-words-find-match][data-words-find-active] {
|
|
background: var(--words-fulfill-solid);
|
|
color: var(--words-fulfill-text);
|
|
}
|
|
|
|
/* ── Selection ────────────────────────────────────────────────────────── */
|
|
|
|
[data-words-content] ::selection {
|
|
background: color-mix(in srgb, var(--words-primary-solid) 22%, transparent);
|
|
}
|