@signal9/era-ui 3.13.2 → 3.14.1
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 +37 -6
- package/dist/apps/notes/notes.svelte +6 -14
- package/dist/generated-docs/input.md +2 -0
- package/dist/generated-docs/llms-full.txt +2 -0
- package/dist/generated-docs/manifest.json +1 -1
- package/dist/ui/input/input.svelte +73 -14
- package/dist/ui/input/input.svelte.d.ts +52 -1
- package/package.json +1 -1
|
@@ -57,6 +57,11 @@ class BubbleMenuView {
|
|
|
57
57
|
#host;
|
|
58
58
|
#toolbar;
|
|
59
59
|
#editor;
|
|
60
|
+
/**
|
|
61
|
+
* The DOCUMENT range the toolbar is currently parked over, or null while it is
|
|
62
|
+
* hidden. This is what the toolbar is anchored to — see update().
|
|
63
|
+
*/
|
|
64
|
+
#anchor = null;
|
|
60
65
|
#view = $state({
|
|
61
66
|
items,
|
|
62
67
|
active: {},
|
|
@@ -80,16 +85,42 @@ class BubbleMenuView {
|
|
|
80
85
|
!state.doc.textBetween(selection.from, selection.to).length ||
|
|
81
86
|
this.#editor.isActive('codeBlock')) {
|
|
82
87
|
this.#host.style.display = 'none';
|
|
88
|
+
this.#anchor = null;
|
|
83
89
|
return;
|
|
84
90
|
}
|
|
85
91
|
this.#view.active = Object.fromEntries(buttons.map((b) => [b.mark, this.#editor.isActive(b.mark)]));
|
|
86
|
-
|
|
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.
|
|
92
|
+
const wasHidden = this.#host.style.display === 'none';
|
|
90
93
|
this.#host.style.display = 'block';
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
/*
|
|
95
|
+
* The toolbar is anchored to the SELECTION RANGE, not to its pixel box.
|
|
96
|
+
*
|
|
97
|
+
* Those are not the same thing, and the difference is the whole bug:
|
|
98
|
+
* applying a mark re-renders the selected text — code sets it monospace, a
|
|
99
|
+
* link adds an underline — so its rect moves while the selection itself
|
|
100
|
+
* does not change by a single character. Re-centring on the new rect slides
|
|
101
|
+
* the toolbar out from under the pointer that just clicked it, and the next
|
|
102
|
+
* click lands on nothing. The user has to chase the menu to keep using it.
|
|
103
|
+
*
|
|
104
|
+
* So: reposition only when the range actually changes (or when the toolbar
|
|
105
|
+
* is appearing fresh). While `from`/`to` hold steady the toolbar is pinned,
|
|
106
|
+
* and a whole run of formatting happens under a motionless pointer.
|
|
107
|
+
*
|
|
108
|
+
* The trade is deliberate: if a format reflows the text far enough, the
|
|
109
|
+
* toolbar can end up slightly off its selection until the selection next
|
|
110
|
+
* changes. Being a few pixels stale beats moving while it is being used —
|
|
111
|
+
* a toolbar is a target first and an indicator second.
|
|
112
|
+
*/
|
|
113
|
+
const { from, to } = selection;
|
|
114
|
+
const movedRange = !this.#anchor || this.#anchor.from !== from || this.#anchor.to !== to;
|
|
115
|
+
if (wasHidden || movedRange) {
|
|
116
|
+
// flushSync applies the `active` change above BEFORE measuring — reading
|
|
117
|
+
// a Svelte component's box before its pending render lands gives the
|
|
118
|
+
// previous frame's, the same trap the slash menu documents. Only needed
|
|
119
|
+
// on the frames that actually measure.
|
|
120
|
+
flushSync();
|
|
121
|
+
placeOverSelection(this.#host, selectionRect(view));
|
|
122
|
+
this.#anchor = { from, to };
|
|
123
|
+
}
|
|
93
124
|
}
|
|
94
125
|
destroy() {
|
|
95
126
|
unmount(this.#toolbar);
|
|
@@ -158,16 +158,10 @@
|
|
|
158
158
|
size="md"
|
|
159
159
|
class="shrink-0 gap-(--era-xxs-inset-md) rounded-none border-b border-divider-faded px-(--era-xxs-inset-md)"
|
|
160
160
|
>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
the glyph itself stays icon-sized inside it. -->
|
|
166
|
-
<span class="flex size-xxs shrink-0 items-center justify-center">
|
|
167
|
-
<Search class="size-(--era-icon-xxs) text-muted" />
|
|
168
|
-
</span>
|
|
169
|
-
<input
|
|
170
|
-
class="h-full min-w-0 flex-1 bg-transparent text-body text-fg outline-none placeholder:text-muted"
|
|
161
|
+
<Input
|
|
162
|
+
icon={Search}
|
|
163
|
+
bare
|
|
164
|
+
class="min-w-0 flex-1"
|
|
171
165
|
type="text"
|
|
172
166
|
placeholder="Filter…"
|
|
173
167
|
aria-label="Filter notes"
|
|
@@ -329,11 +323,9 @@
|
|
|
329
323
|
</Popover.Content>
|
|
330
324
|
</Popover.Root>
|
|
331
325
|
|
|
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. -->
|
|
335
326
|
<Input
|
|
336
|
-
|
|
327
|
+
bare
|
|
328
|
+
class="min-w-0 flex-1"
|
|
337
329
|
aria-label="Note title"
|
|
338
330
|
placeholder="Untitled"
|
|
339
331
|
value={selected.title}
|
|
@@ -15,4 +15,6 @@ Inherits all props from `HTMLInputAttributes`.
|
|
|
15
15
|
| Prop | Type | Default | Notes |
|
|
16
16
|
|------|------|---------|-------|
|
|
17
17
|
| `ref?` | `HTMLInputElement \| null` | `null` | bindable |
|
|
18
|
+
| `icon?` | `Component<IconProps> \| null` | `null` | Lucide glyph rendered inside the field, left of the text. |
|
|
19
|
+
| `bare?` | `boolean` | `false` | Drop the field chrome — for an input inside a container that is already the field. |
|
|
18
20
|
| `value?` | `(forwarded)` | `''` | bindable |
|
|
@@ -2753,6 +2753,8 @@ Inherits all props from `HTMLInputAttributes`.
|
|
|
2753
2753
|
| Prop | Type | Default | Notes |
|
|
2754
2754
|
|------|------|---------|-------|
|
|
2755
2755
|
| `ref?` | `HTMLInputElement \| null` | `null` | bindable |
|
|
2756
|
+
| `icon?` | `Component<IconProps> \| null` | `null` | Lucide glyph rendered inside the field, left of the text. |
|
|
2757
|
+
| `bare?` | `boolean` | `false` | Drop the field chrome — for an input inside a container that is already the field. |
|
|
2756
2758
|
| `value?` | `(forwarded)` | `''` | bindable |
|
|
2757
2759
|
|
|
2758
2760
|
<!-- end: input -->
|
|
@@ -1,27 +1,86 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { cn, tv } from '../../utils/index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The field shell — the chrome an input paints, minus the text styling.
|
|
6
|
+
*
|
|
7
|
+
* Split out as a variant because an `icon` input has to move that chrome onto
|
|
8
|
+
* a WRAPPER (the glyph and the <input> sit inside one field), while a plain
|
|
9
|
+
* one keeps it on the <input> itself. Both spellings therefore have to
|
|
10
|
+
* describe the same box, and a variant is how they stay one description.
|
|
11
|
+
*/
|
|
12
|
+
export const inputVariants = tv({
|
|
13
|
+
base: 'flex w-full era-interactive text-body text-fg',
|
|
14
|
+
variants: {
|
|
15
|
+
/**
|
|
16
|
+
* `bare` drops the field chrome entirely — no fill, no well, no side
|
|
17
|
+
* padding. For an input nested in a container that is ALREADY the field:
|
|
18
|
+
* a Bar acting as a toolbar, a header row. Without it the input draws a
|
|
19
|
+
* second box inside the first, and at the same tier it fills its parent
|
|
20
|
+
* edge to edge so the two have no gap between them.
|
|
21
|
+
*/
|
|
22
|
+
bare: {
|
|
23
|
+
false:
|
|
24
|
+
'h-(--era-h-md) rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) px-(--era-field-px) shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) focus:bg-(--era-highlight)',
|
|
25
|
+
// h-full, NOT the md tier: a bare input's container IS the field, so it
|
|
26
|
+
// takes that container's height. Keeping h-md here made a bare input
|
|
27
|
+
// inside an h-md Bar overflow it by the bar's border — a −0.5px gap,
|
|
28
|
+
// which is the even-gap law failing by a hair rather than obviously.
|
|
29
|
+
// Implies a parent with a definite height, which is the only place
|
|
30
|
+
// `bare` makes sense anyway.
|
|
31
|
+
true: 'h-full bg-transparent px-0 shadow-none'
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
defaultVariants: { bare: false }
|
|
35
|
+
});
|
|
36
|
+
</script>
|
|
37
|
+
|
|
1
38
|
<script lang="ts">
|
|
39
|
+
import type { Component } from 'svelte';
|
|
40
|
+
import type { IconProps } from '@lucide/svelte';
|
|
2
41
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
-
import { cn } from '../../utils/index.js';
|
|
4
42
|
|
|
5
43
|
let {
|
|
6
44
|
ref = $bindable(null),
|
|
7
45
|
value = $bindable(''),
|
|
46
|
+
icon = null,
|
|
47
|
+
bare = false,
|
|
8
48
|
class: className,
|
|
9
49
|
...restProps
|
|
10
50
|
}: HTMLInputAttributes & {
|
|
11
51
|
ref?: HTMLInputElement | null;
|
|
52
|
+
/** Lucide glyph rendered inside the field, left of the text. */
|
|
53
|
+
icon?: Component<IconProps> | null;
|
|
54
|
+
/** Drop the field chrome — for an input inside a container that is already the field. */
|
|
55
|
+
bare?: boolean;
|
|
12
56
|
} = $props();
|
|
57
|
+
|
|
58
|
+
// No era-text-trim on the <input> itself, in either branch: an <input> clips to
|
|
59
|
+
// a UA-owned inner editor box that IS the trimmed line box, so the trim slices
|
|
60
|
+
// the ascenders and descenders off what you type (a "d" renders as an "o"). The
|
|
61
|
+
// fixed tier height centres the text on its own. See styles/index.css.
|
|
62
|
+
const textClass = 'text-body text-fg placeholder:text-muted';
|
|
13
63
|
</script>
|
|
14
64
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/>
|
|
65
|
+
{#if icon}
|
|
66
|
+
{@const Icon = icon}
|
|
67
|
+
<!-- The chrome moves to the wrapper so the glyph sits INSIDE the field. The
|
|
68
|
+
glyph is on the icon tier of the field's own height, and the gap between
|
|
69
|
+
it and the text is the universal inter-element gap. -->
|
|
70
|
+
<div class={cn(inputVariants({ bare }), 'items-center gap-(--era-gap)', className)}>
|
|
71
|
+
<Icon class="size-(--era-h-xs) shrink-0 text-muted" />
|
|
72
|
+
<input
|
|
73
|
+
bind:this={ref}
|
|
74
|
+
bind:value
|
|
75
|
+
class={cn('h-full w-full min-w-0 bg-transparent outline-none', textClass)}
|
|
76
|
+
{...restProps}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
{:else}
|
|
80
|
+
<input
|
|
81
|
+
bind:this={ref}
|
|
82
|
+
bind:value
|
|
83
|
+
class={cn(inputVariants({ bare }), textClass, className)}
|
|
84
|
+
{...restProps}
|
|
85
|
+
/>
|
|
86
|
+
{/if}
|
|
@@ -1,7 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The field shell — the chrome an input paints, minus the text styling.
|
|
3
|
+
*
|
|
4
|
+
* Split out as a variant because an `icon` input has to move that chrome onto
|
|
5
|
+
* a WRAPPER (the glyph and the <input> sit inside one field), while a plain
|
|
6
|
+
* one keeps it on the <input> itself. Both spellings therefore have to
|
|
7
|
+
* describe the same box, and a variant is how they stay one description.
|
|
8
|
+
*/
|
|
9
|
+
export declare const inputVariants: import("tailwind-variants").TVReturnType<{
|
|
10
|
+
/**
|
|
11
|
+
* `bare` drops the field chrome entirely — no fill, no well, no side
|
|
12
|
+
* padding. For an input nested in a container that is ALREADY the field:
|
|
13
|
+
* a Bar acting as a toolbar, a header row. Without it the input draws a
|
|
14
|
+
* second box inside the first, and at the same tier it fills its parent
|
|
15
|
+
* edge to edge so the two have no gap between them.
|
|
16
|
+
*/
|
|
17
|
+
bare: {
|
|
18
|
+
false: "h-(--era-h-md) rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) px-(--era-field-px) shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) focus:bg-(--era-highlight)";
|
|
19
|
+
true: "h-full bg-transparent px-0 shadow-none";
|
|
20
|
+
};
|
|
21
|
+
}, undefined, "flex w-full era-interactive text-body text-fg", {
|
|
22
|
+
/**
|
|
23
|
+
* `bare` drops the field chrome entirely — no fill, no well, no side
|
|
24
|
+
* padding. For an input nested in a container that is ALREADY the field:
|
|
25
|
+
* a Bar acting as a toolbar, a header row. Without it the input draws a
|
|
26
|
+
* second box inside the first, and at the same tier it fills its parent
|
|
27
|
+
* edge to edge so the two have no gap between them.
|
|
28
|
+
*/
|
|
29
|
+
bare: {
|
|
30
|
+
false: "h-(--era-h-md) rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) px-(--era-field-px) shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) focus:bg-(--era-highlight)";
|
|
31
|
+
true: "h-full bg-transparent px-0 shadow-none";
|
|
32
|
+
};
|
|
33
|
+
}, undefined, import("tailwind-variants").TVReturnTypeLike<{
|
|
34
|
+
/**
|
|
35
|
+
* `bare` drops the field chrome entirely — no fill, no well, no side
|
|
36
|
+
* padding. For an input nested in a container that is ALREADY the field:
|
|
37
|
+
* a Bar acting as a toolbar, a header row. Without it the input draws a
|
|
38
|
+
* second box inside the first, and at the same tier it fills its parent
|
|
39
|
+
* edge to edge so the two have no gap between them.
|
|
40
|
+
*/
|
|
41
|
+
bare: {
|
|
42
|
+
false: "h-(--era-h-md) rounded-(--era-rd-md) bg-(--era-surface-bg-elevated) px-(--era-field-px) shadow-(--era-shadow-well) hover:bg-(--era-highlight) focus-within:bg-(--era-highlight) focus:bg-(--era-highlight)";
|
|
43
|
+
true: "h-full bg-transparent px-0 shadow-none";
|
|
44
|
+
};
|
|
45
|
+
}, undefined>>;
|
|
46
|
+
import type { Component } from 'svelte';
|
|
47
|
+
import type { IconProps } from '@lucide/svelte';
|
|
1
48
|
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
2
49
|
type $$ComponentProps = HTMLInputAttributes & {
|
|
3
50
|
ref?: HTMLInputElement | null;
|
|
51
|
+
/** Lucide glyph rendered inside the field, left of the text. */
|
|
52
|
+
icon?: Component<IconProps> | null;
|
|
53
|
+
/** Drop the field chrome — for an input inside a container that is already the field. */
|
|
54
|
+
bare?: boolean;
|
|
4
55
|
};
|
|
5
|
-
declare const Input:
|
|
56
|
+
declare const Input: Component<$$ComponentProps, {}, "value" | "ref">;
|
|
6
57
|
type Input = ReturnType<typeof Input>;
|
|
7
58
|
export default Input;
|