words: active-block rail — one colour, fixed gutter column, always present

Reworks the active-block marker per feedback (it changed colour by depth,
shifted right for nested blocks, and showed nothing on load):

- New `words-active-rail.svelte`: a single vertical bar in the gutter whose
  top/height track the CLICKED block, re-measured on activation / render /
  scroll. It is FRAME-relative at a fixed gutter column (CSS
  `--_words-content-px - 1.4rem`), so it never shifts right for nested blocks,
  and uses ONE fixed colour (`--color-primary-solid`).
- Removed the block-anchored `[data-words-active]::before` rail (block-relative
  → shifted; depth-coloured → changed colour).
- `words-activate.svelte`: there is now ALWAYS an active block — on first load
  it seeds the first top-level block, so the inspector + rail have a target
  instead of "nothing selected".

Verified: on load the first block is active and the rail shows at a fixed 27px
gutter column, 3px, single colour, height = active block. Check clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
active-uix
dev 4 months ago
parent 986d61bde6
commit dd7d67d2a1

@ -104,4 +104,16 @@
el.setAttribute('data-words-depth', String(segments.length)); el.setAttribute('data-words-depth', String(segments.length));
} }
}); });
// There is ALWAYS an active block: on first load (no click yet) select the
// first top-level block so the inspector + the gutter rail have a target
// from the start instead of "nothing selected".
let seeded = false;
$effect(() => {
if (seeded || !enabled) return;
const first = api.document.children[0];
if (!first?.id) return;
if (!api.activeBlockId) api.setActiveBlock(first.id);
seeded = true;
});
</script> </script>

@ -0,0 +1,55 @@
<script lang="ts">
/**
* Active-block rail — ONE vertical bar in the left gutter that tracks the
* CLICKED (active) block. Frame-relative + a fixed gutter column (CSS), so
* it never shifts right for nested blocks, and a single fixed colour. Its
* top/height follow the active block, re-measured on activation / render /
* scroll. Hidden when the active block is scrolled out of view.
*/
import type { ActiveDom } from '$adom';
import type { ProviderSnippetProps } from '$soma/components/words';
let {
api,
content,
dom
}: {
api: ProviderSnippetProps;
content: HTMLElement;
dom: ActiveDom;
} = $props();
const frame = $derived(content.closest('[data-words]') as HTMLElement | null);
let rail = $state<{ top: number; height: number } | null>(null);
function measure() {
const el = frame ? content.querySelector('[data-words-active]') : null;
if (!frame || !(el instanceof HTMLElement)) {
rail = null;
return;
}
const br = el.getBoundingClientRect();
const fr = frame.getBoundingClientRect();
const cr = content.getBoundingClientRect();
// Hide when the active block is scrolled out of the content viewport.
if (br.bottom < cr.top + 2 || br.top > cr.bottom - 2) {
rail = null;
return;
}
rail = { top: br.top - fr.top, height: br.height };
}
// Re-measure when the active block changes or the doc re-renders (typing
// resizes a block; `{@html}` recreates its element).
$effect(() => {
void api.activeBlockId;
void api.html;
dom.raf(measure, content);
});
// Re-measure on internal content scroll (the rail is frame-relative).
$effect(() => dom.listen(content, 'scroll', measure, { capture: true }));
</script>
{#if rail}
<div data-words-active-rail style="top: {rail.top}px; height: {rail.height}px;"></div>
{/if}

@ -1064,32 +1064,21 @@ button[data-words-breadcrumb-crumb]:focus-visible {
(not border → zero reflow) shows the inspector's current target on the (not border → zero reflow) shows the inspector's current target on the
canvas. The hue shifts by depth so nested structure reads at a glance: canvas. The hue shifts by depth so nested structure reads at a glance:
blue at top level, then teal / violet / amber descending into columns. */ blue at top level, then teal / violet / amber descending into columns. */
[data-words-content] [data-words-active] { /* Active-block rail — a single vertical bar in the GUTTER (left margin), at a
position: relative; FIXED column (frame-relative, so it never shifts right for nested blocks),
} one fixed colour. Its top/height track the active block; the rail element is
/* The active-block marker lives in the GUTTER, not on the block: a vertical rendered by `words-active-rail.svelte` and positioned here. */
accent rail in the left margin (`::before`, just left of the block) spanning [data-words-active-rail] {
the block height. Depth-coloured via `--_words-active-outline`. The block
stays `position: relative` so the rail anchors to it. */
[data-words-content] [data-words-active]::before {
content: '';
position: absolute; position: absolute;
inset-block: 0; inset-inline-start: calc(var(--_words-content-px) - 1.4rem);
inset-inline-start: -0.7rem; z-index: 1;
inline-size: 3px; inline-size: 3px;
border-radius: 2px; border-radius: 2px;
background: var(--_words-active-outline, var(--color-primary-solid)); background: var(--color-primary-solid);
pointer-events: none; pointer-events: none;
animation: words-outline-in var(--words-transition-duration) var(--words-transition-ease); transition:
} top 120ms var(--words-transition-ease),
[data-words-content] [data-words-active][data-words-depth='2'] { height 120ms var(--words-transition-ease);
--_words-active-outline: var(--color-affirm-solid); /* teal — column level */
}
[data-words-content] [data-words-active][data-words-depth='3'] {
--_words-active-outline: var(--color-loss-solid); /* violet — block in a column */
}
[data-words-content] [data-words-active][data-words-depth='4'] {
--_words-active-outline: var(--color-risk-solid); /* amber — deeper nesting */
} }
[data-words-content] :where([data-words-node='block']):first-child { [data-words-content] :where([data-words-node='block']):first-child {

@ -19,6 +19,7 @@
import { Popover } from '$uix/eidos/components/popover'; import { Popover } from '$uix/eidos/components/popover';
import { SlidersHorizontal, X } from '$uix/eidos/components/icon'; import { SlidersHorizontal, X } from '$uix/eidos/components/icon';
import WordsActivate from './words-activate.svelte'; import WordsActivate from './words-activate.svelte';
import WordsActiveRail from './words-active-rail.svelte';
import WordsBlockDrag from './words-block-drag.svelte'; import WordsBlockDrag from './words-block-drag.svelte';
import WordsBlockGutter from './words-block-gutter.svelte'; import WordsBlockGutter from './words-block-gutter.svelte';
import WordsBubble from './words-bubble.svelte'; import WordsBubble from './words-bubble.svelte';
@ -111,6 +112,7 @@
{#if contentEl} {#if contentEl}
<WordsActivate {api} content={contentEl} {dom} enabled={mode === 'edit'} /> <WordsActivate {api} content={contentEl} {dom} enabled={mode === 'edit'} />
{#if mode === 'edit'} {#if mode === 'edit'}
<WordsActiveRail {api} content={contentEl} {dom} />
<WordsBlockGutter {api} content={contentEl} {dom} /> <WordsBlockGutter {api} content={contentEl} {dom} />
<WordsBlockDrag {api} content={contentEl} {dom} /> <WordsBlockDrag {api} content={contentEl} {dom} />
{/if} {/if}

Loading…
Cancel
Save

Powered by TurnKey Linux.