@signal9/era-ui 3.13.0 → 3.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/apps/notes/editor/bubble-menu.svelte.js +31 -3
- package/dist/apps/notes/editor/bubble-toolbar.svelte +8 -2
- package/dist/apps/notes/editor/floating.d.ts +12 -8
- package/dist/apps/notes/editor/floating.js +15 -5
- package/dist/apps/notes/note-editor.svelte +13 -3
- package/dist/apps/notes/notes.svelte +16 -5
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { Extension } from '@tiptap/core';
|
|
11
11
|
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
12
|
-
import { mount, unmount } from 'svelte';
|
|
12
|
+
import { flushSync, mount, unmount } from 'svelte';
|
|
13
13
|
import Bold from '@lucide/svelte/icons/bold';
|
|
14
14
|
import Italic from '@lucide/svelte/icons/italic';
|
|
15
15
|
import Strikethrough from '@lucide/svelte/icons/strikethrough';
|
|
@@ -83,15 +83,43 @@ class BubbleMenuView {
|
|
|
83
83
|
return;
|
|
84
84
|
}
|
|
85
85
|
this.#view.active = Object.fromEntries(buttons.map((b) => [b.mark, this.#editor.isActive(b.mark)]));
|
|
86
|
-
// Measuring requires a laid-out box, so unhide before placing.
|
|
86
|
+
// Measuring requires a laid-out box, so unhide before placing. flushSync
|
|
87
|
+
// applies the `active` change above FIRST — measuring a Svelte component
|
|
88
|
+
// before its pending render lands reads the previous frame's box, which is
|
|
89
|
+
// the same trap the slash menu documents.
|
|
87
90
|
this.#host.style.display = 'block';
|
|
88
|
-
|
|
91
|
+
flushSync();
|
|
92
|
+
placeOverSelection(this.#host, selectionRect(view));
|
|
89
93
|
}
|
|
90
94
|
destroy() {
|
|
91
95
|
unmount(this.#toolbar);
|
|
92
96
|
this.#host.remove();
|
|
93
97
|
}
|
|
94
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* The selection's bounding box in viewport coordinates.
|
|
101
|
+
*
|
|
102
|
+
* Prefers the live DOM selection, whose rect already unions every line of a
|
|
103
|
+
* multi-line selection. ProseMirror's coordsAtPos only describes ONE position,
|
|
104
|
+
* so reconstructing a tall selection's box from its two endpoints is what put
|
|
105
|
+
* the toolbar 305px off centre. The endpoint union is kept as a fallback for the
|
|
106
|
+
* case where the DOM selection is unavailable or collapsed to a zero rect (it
|
|
107
|
+
* still beats nothing, and it is exactly right on a single line).
|
|
108
|
+
*/
|
|
109
|
+
function selectionRect(view) {
|
|
110
|
+
const dom = window.getSelection();
|
|
111
|
+
if (dom && dom.rangeCount > 0) {
|
|
112
|
+
const rect = dom.getRangeAt(0).getBoundingClientRect();
|
|
113
|
+
if (rect.width > 0 || rect.height > 0)
|
|
114
|
+
return rect;
|
|
115
|
+
}
|
|
116
|
+
const { from, to } = view.state.selection;
|
|
117
|
+
const a = view.coordsAtPos(from);
|
|
118
|
+
const b = view.coordsAtPos(to);
|
|
119
|
+
const left = Math.min(a.left, b.left);
|
|
120
|
+
const top = Math.min(a.top, b.top);
|
|
121
|
+
return new DOMRect(left, top, Math.max(a.right, b.right) - left, Math.max(a.bottom, b.bottom) - top);
|
|
122
|
+
}
|
|
95
123
|
export const BubbleMenu = Extension.create({
|
|
96
124
|
name: 'notesBubbleMenu',
|
|
97
125
|
addProseMirrorPlugins() {
|
|
@@ -21,12 +21,18 @@
|
|
|
21
21
|
let { items, active, onCommand }: Props = $props();
|
|
22
22
|
</script>
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
<!-- md with xxs buttons, matching the app's other bars. It was sm + size="icon"
|
|
25
|
+
— a 22px bar of 14px buttons around 10px glyphs, the smallest combination in
|
|
26
|
+
the ladder, which is why the formatting controls read as unhittable. -->
|
|
27
|
+
<Bar
|
|
28
|
+
size="md"
|
|
29
|
+
class="w-max gap-(--era-xxs-inset-md) bg-elevated px-(--era-xxs-inset-md) shadow-lg glass-blur"
|
|
30
|
+
>
|
|
25
31
|
{#each items as item (item.mark)}
|
|
26
32
|
{@const Icon = item.icon}
|
|
27
33
|
<Button
|
|
28
34
|
icon
|
|
29
|
-
size="
|
|
35
|
+
size="xxs"
|
|
30
36
|
active={active[item.mark] ?? false}
|
|
31
37
|
aria-label={item.label}
|
|
32
38
|
title={item.label}
|
|
@@ -11,11 +11,15 @@
|
|
|
11
11
|
export declare function createOverlayHost(): HTMLDivElement;
|
|
12
12
|
/** Anchor below `rect`, flipping above when the bottom edge would run off. */
|
|
13
13
|
export declare function placeBelow(host: HTMLElement, rect: DOMRect, gap?: number): void;
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Centre over a selection's BOUNDING BOX, flipping below when there is no room
|
|
16
|
+
* above.
|
|
17
|
+
*
|
|
18
|
+
* Takes the whole rect on purpose. This used to take the two endpoint coords and
|
|
19
|
+
* centre on `(start.left + end.right) / 2`, which is only correct while the
|
|
20
|
+
* selection sits on one line: across several lines it averages an x from the
|
|
21
|
+
* FIRST line with an x from the LAST, and the result points at neither. Measured
|
|
22
|
+
* on a 7-line selection, the toolbar landed 305px from the selection's centre —
|
|
23
|
+
* one line was 0px, two lines 4px, so it looked fine until a selection got tall.
|
|
24
|
+
*/
|
|
25
|
+
export declare function placeOverSelection(host: HTMLElement, rect: DOMRect, gap?: number): void;
|
|
@@ -28,14 +28,24 @@ export function placeBelow(host, rect, gap = EDGE) {
|
|
|
28
28
|
host.style.left = `${Math.max(EDGE, left)}px`;
|
|
29
29
|
host.style.top = `${Math.max(EDGE, top)}px`;
|
|
30
30
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Centre over a selection's BOUNDING BOX, flipping below when there is no room
|
|
33
|
+
* above.
|
|
34
|
+
*
|
|
35
|
+
* Takes the whole rect on purpose. This used to take the two endpoint coords and
|
|
36
|
+
* centre on `(start.left + end.right) / 2`, which is only correct while the
|
|
37
|
+
* selection sits on one line: across several lines it averages an x from the
|
|
38
|
+
* FIRST line with an x from the LAST, and the result points at neither. Measured
|
|
39
|
+
* on a 7-line selection, the toolbar landed 305px from the selection's centre —
|
|
40
|
+
* one line was 0px, two lines 4px, so it looked fine until a selection got tall.
|
|
41
|
+
*/
|
|
42
|
+
export function placeOverSelection(host, rect, gap = 6) {
|
|
33
43
|
const box = host.getBoundingClientRect();
|
|
34
|
-
const centerX =
|
|
44
|
+
const centerX = rect.left + rect.width / 2;
|
|
35
45
|
const left = clamp(centerX - box.width / 2, EDGE, window.innerWidth - box.width - EDGE);
|
|
36
|
-
let top =
|
|
46
|
+
let top = rect.top - box.height - gap;
|
|
37
47
|
if (top < EDGE)
|
|
38
|
-
top =
|
|
48
|
+
top = rect.bottom + gap;
|
|
39
49
|
host.style.left = `${Math.max(EDGE, left)}px`;
|
|
40
50
|
host.style.top = `${Math.max(EDGE, top)}px`;
|
|
41
51
|
}
|
|
@@ -457,7 +457,17 @@
|
|
|
457
457
|
.era-notes-prose :global(.tiptap .column-resize-handle) {
|
|
458
458
|
background-color: var(--color-primary);
|
|
459
459
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
/* NO ::selection rule here, deliberately.
|
|
461
|
+
*
|
|
462
|
+
* era owns text selection as a surface token contract — --era-selection-bg /
|
|
463
|
+
* -fg / -shadow (surfaces/base.css), which is how flat gets inverse video,
|
|
464
|
+
* glass a glowing wash, and bevel an opaque accent bar. This file used to
|
|
465
|
+
* override it with `background: var(--color-4)` and no `color`, which threw
|
|
466
|
+
* all of that away AND left body text on a mid-scale grey: the band barely
|
|
467
|
+
* separated from the page and the glyphs sat on top of it at whatever
|
|
468
|
+
* contrast happened to fall out. Dropping the override restores both the
|
|
469
|
+
* contrast (bright fill, --color-1 glyphs) and the per-surface treatment.
|
|
470
|
+
*
|
|
471
|
+
* The ProseMirror overrides above are still needed — those replace hardcoded
|
|
472
|
+
* library chrome that no era token would otherwise reach. */
|
|
463
473
|
</style>
|
|
@@ -223,7 +223,7 @@
|
|
|
223
223
|
{#if note.icon}
|
|
224
224
|
{note.icon}
|
|
225
225
|
{:else}
|
|
226
|
-
<FileText class="size-
|
|
226
|
+
<FileText class="size-(--era-icon-xxs) text-muted" />
|
|
227
227
|
{/if}
|
|
228
228
|
</span>
|
|
229
229
|
|
|
@@ -232,7 +232,9 @@
|
|
|
232
232
|
<!-- One cell, three occupants: the age at rest, the actions on
|
|
233
233
|
hover. Swapping them in place keeps the row from reflowing. -->
|
|
234
234
|
<span class="shrink-0 text-muted group-hover:hidden">
|
|
235
|
-
{#if note.pinned}<Pin class="size-
|
|
235
|
+
{#if note.pinned}<Pin class="size-(--era-icon-xxs)" />{:else}{formatAge(
|
|
236
|
+
note.updatedAt
|
|
237
|
+
)}{/if}
|
|
236
238
|
</span>
|
|
237
239
|
<span class="hidden shrink-0 items-center gap-(--era-xs-inset-md) group-hover:flex">
|
|
238
240
|
<Button
|
|
@@ -276,9 +278,15 @@
|
|
|
276
278
|
<!-- Document pane -->
|
|
277
279
|
<div class="flex min-w-0 flex-1 flex-col">
|
|
278
280
|
{#if selected}
|
|
279
|
-
|
|
281
|
+
<!-- md and xxs-padded, exactly like the sidebar's filter bar: the two
|
|
282
|
+
headers sit side by side across the top of the app, so an lg bar
|
|
283
|
+
here left the document header 8px taller than the list header. -->
|
|
284
|
+
<Bar
|
|
285
|
+
size="md"
|
|
286
|
+
class="shrink-0 gap-(--era-xxs-inset-md) rounded-none border-b border-divider-faded px-(--era-xxs-inset-md)"
|
|
287
|
+
>
|
|
280
288
|
<Popover.Root bind:open={iconPickerOpen}>
|
|
281
|
-
<Popover.Trigger icon aria-label="Change icon" title="Change icon">
|
|
289
|
+
<Popover.Trigger icon size="xxs" aria-label="Change icon" title="Change icon">
|
|
282
290
|
{#if selected.icon}
|
|
283
291
|
<span aria-hidden="true">{selected.icon}</span>
|
|
284
292
|
{:else}
|
|
@@ -321,8 +329,11 @@
|
|
|
321
329
|
</Popover.Content>
|
|
322
330
|
</Popover.Root>
|
|
323
331
|
|
|
332
|
+
<!-- Chromeless, like the filter input: an h-md Input inside an h-md bar
|
|
333
|
+
fills it edge to edge, so the well would draw a box with no room
|
|
334
|
+
around it. The bar is the container. -->
|
|
324
335
|
<Input
|
|
325
|
-
class="min-w-0 flex-1 bg-transparent shadow-none"
|
|
336
|
+
class="h-full min-w-0 flex-1 bg-transparent px-0 shadow-none"
|
|
326
337
|
aria-label="Note title"
|
|
327
338
|
placeholder="Untitled"
|
|
328
339
|
value={selected.title}
|