terminoir 0.9.0 → 0.11.0
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/README.md +3 -1
- package/dist/components/Inline.svelte +13 -1
- package/dist/components/Inline.svelte.d.ts +2 -0
- package/dist/components/ListItem.svelte +13 -1
- package/dist/components/ListItem.svelte.d.ts +4 -0
- package/dist/components/MarkdownEditor.svelte +81 -3
- package/dist/components/PanelHeader.svelte +96 -0
- package/dist/components/PanelHeader.svelte.d.ts +20 -0
- package/dist/components/StatusChip.svelte +21 -0
- package/dist/components/StatusChip.svelte.d.ts +9 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/utils/markdownEditor/blockDrag.d.ts +21 -0
- package/dist/utils/markdownEditor/blockDrag.js +34 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,6 +123,7 @@ Import from **`terminoir`**:
|
|
|
123
123
|
| `Button`, `IconButton` | Variants: primary, secondary, ghost, destructive, outline · sizes sm/md/lg · `Button href` renders a link |
|
|
124
124
|
| `Input`, `Textarea`, `Select`, `Checkbox`, `Switch` | `hideLabel` (screen-reader only), `hint`, `error`, `quiet` (borderless until hover/focus, for inline property editing); `Input` `width` for toolbar fields |
|
|
125
125
|
| `Chip`, `Badge` | Status tones; `Chip` `color` dot, `onclick`, removable `onremove` |
|
|
126
|
+
| `StatusChip` | On/off status: green dot + `activeLabel`, or muted `inactiveLabel` (defaults Connected / Not connected) |
|
|
126
127
|
| `Card`, `CardGrid` | Accent tint, badge, chips, progress, meta, hover lift |
|
|
127
128
|
| `Panel` | Glass panel surface |
|
|
128
129
|
| `Dialog` | Modal with focus trap-ish behavior + Escape |
|
|
@@ -147,7 +148,8 @@ Building blocks so apps compose screens without CSS of their own. See the demo's
|
|
|
147
148
|
| Component | Notes |
|
|
148
149
|
|-----------|--------|
|
|
149
150
|
| `Page`, `PageHeader`, `Section` | Readable page column (`narrow`/`default`/`wide`/`full`, `fill` for canvases); title + meta + actions; titled section with actions |
|
|
150
|
-
| `
|
|
151
|
+
| `PanelHeader` | Heading inside a `Panel`: `logo` (round avatar, initials until `src`), `icon` or `leading` mark, title, description, `actions` (stacks under on phones) |
|
|
152
|
+
| `Stack`, `Inline`, `Spacer`, `Centered` | Vertical / horizontal layout with `--space-*` gaps (`Inline fill` splits evenly, `collapse` stacks on phones); fill space; full-viewport centred column |
|
|
151
153
|
| `Text`, `Prose`, `Disclosure` | Tone/size/weight text; long-form typography; collapsible details |
|
|
152
154
|
| `NavSection` | Sidebar section with title and action |
|
|
153
155
|
| `DataTable` | Columns, `cell` snippet, clickable rows, `hideOnMobile`, empty state |
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
justify = 'start',
|
|
11
11
|
wrap = true,
|
|
12
12
|
fill = false,
|
|
13
|
+
collapse = false,
|
|
13
14
|
class: className = '',
|
|
14
15
|
children,
|
|
15
16
|
...rest
|
|
@@ -21,6 +22,8 @@
|
|
|
21
22
|
wrap?: boolean;
|
|
22
23
|
/** Children share the row equally (e.g. two inputs side by side). */
|
|
23
24
|
fill?: boolean;
|
|
25
|
+
/** Stack the children full-width on narrow screens (phones), e.g. a row of form fields. */
|
|
26
|
+
collapse?: boolean;
|
|
24
27
|
class?: string;
|
|
25
28
|
children?: Snippet;
|
|
26
29
|
} & Record<string, unknown> = $props();
|
|
@@ -30,7 +33,7 @@
|
|
|
30
33
|
</script>
|
|
31
34
|
|
|
32
35
|
<div
|
|
33
|
-
class={cn('terminoir-inline', fill && 'terminoir-inline--fill', className)}
|
|
36
|
+
class={cn('terminoir-inline', fill && 'terminoir-inline--fill', collapse && 'terminoir-inline--collapse', className)}
|
|
34
37
|
style:gap={`var(--space-${gap})`}
|
|
35
38
|
style:align-items={ALIGN[align]}
|
|
36
39
|
style:justify-content={JUSTIFY[justify]}
|
|
@@ -49,4 +52,13 @@
|
|
|
49
52
|
flex: 1 1 0;
|
|
50
53
|
min-width: 0;
|
|
51
54
|
}
|
|
55
|
+
@media (max-width: 700px) {
|
|
56
|
+
.terminoir-inline--collapse {
|
|
57
|
+
flex-direction: column;
|
|
58
|
+
}
|
|
59
|
+
.terminoir-inline--collapse > :global(*) {
|
|
60
|
+
flex: 0 0 auto;
|
|
61
|
+
width: 100%;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
52
64
|
</style>
|
|
@@ -8,6 +8,8 @@ type $$ComponentProps = {
|
|
|
8
8
|
wrap?: boolean;
|
|
9
9
|
/** Children share the row equally (e.g. two inputs side by side). */
|
|
10
10
|
fill?: boolean;
|
|
11
|
+
/** Stack the children full-width on narrow screens (phones), e.g. a row of form fields. */
|
|
12
|
+
collapse?: boolean;
|
|
11
13
|
class?: string;
|
|
12
14
|
children?: Snippet;
|
|
13
15
|
} & Record<string, unknown>;
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
class: className = '',
|
|
11
11
|
leading,
|
|
12
12
|
trailing,
|
|
13
|
+
expanded,
|
|
14
|
+
details,
|
|
13
15
|
children,
|
|
14
16
|
...rest
|
|
15
17
|
}: {
|
|
@@ -22,6 +24,10 @@
|
|
|
22
24
|
class?: string;
|
|
23
25
|
leading?: Snippet;
|
|
24
26
|
trailing?: Snippet;
|
|
27
|
+
/** For a row that opens `details`: whether they are shown (sets `aria-expanded`). */
|
|
28
|
+
expanded?: boolean;
|
|
29
|
+
/** Content under the row, such as the settings it opens. Shown only while `expanded` is not false. */
|
|
30
|
+
details?: Snippet;
|
|
25
31
|
/** Replaces title/description for custom content. */
|
|
26
32
|
children?: Snippet;
|
|
27
33
|
} & Record<string, unknown> = $props();
|
|
@@ -42,12 +48,15 @@
|
|
|
42
48
|
|
|
43
49
|
<li class={cn('terminoir-list-item', active && 'terminoir-list-item--active', className)} {...rest}>
|
|
44
50
|
{#if onclick}
|
|
45
|
-
<button type="button" class="terminoir-list-item__row terminoir-list-item__button terminoir-focusable" {onclick}>
|
|
51
|
+
<button type="button" class="terminoir-list-item__row terminoir-list-item__button terminoir-focusable" aria-expanded={expanded} {onclick}>
|
|
46
52
|
{@render body()}
|
|
47
53
|
</button>
|
|
48
54
|
{:else}
|
|
49
55
|
<div class="terminoir-list-item__row">{@render body()}</div>
|
|
50
56
|
{/if}
|
|
57
|
+
{#if details && expanded !== false}
|
|
58
|
+
<div class="terminoir-list-item__details">{@render details()}</div>
|
|
59
|
+
{/if}
|
|
51
60
|
</li>
|
|
52
61
|
|
|
53
62
|
<style>
|
|
@@ -103,6 +112,9 @@
|
|
|
103
112
|
text-overflow: ellipsis;
|
|
104
113
|
white-space: nowrap;
|
|
105
114
|
}
|
|
115
|
+
.terminoir-list-item__details {
|
|
116
|
+
padding: var(--space-1) 0.85rem var(--space-4) calc(0.85rem + 1.25rem + var(--space-3));
|
|
117
|
+
}
|
|
106
118
|
.terminoir-list-item__trailing {
|
|
107
119
|
display: inline-flex;
|
|
108
120
|
align-items: center;
|
|
@@ -9,6 +9,10 @@ type $$ComponentProps = {
|
|
|
9
9
|
class?: string;
|
|
10
10
|
leading?: Snippet;
|
|
11
11
|
trailing?: Snippet;
|
|
12
|
+
/** For a row that opens `details`: whether they are shown (sets `aria-expanded`). */
|
|
13
|
+
expanded?: boolean;
|
|
14
|
+
/** Content under the row, such as the settings it opens. Shown only while `expanded` is not false. */
|
|
15
|
+
details?: Snippet;
|
|
12
16
|
/** Replaces title/description for custom content. */
|
|
13
17
|
children?: Snippet;
|
|
14
18
|
} & Record<string, unknown>;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { cn } from '../utils/cn.js';
|
|
14
14
|
import type { PickerOption } from '../utils/componentTypes.js';
|
|
15
15
|
import { moveIndex } from '../utils/listNav.js';
|
|
16
|
+
import { findDropTarget, moveBlock } from '../utils/markdownEditor/blockDrag.js';
|
|
16
17
|
import { matchBlocks, placeholderPlugin, taskListItemView, tidyMarkdown, type BlockItem } from '../utils/markdownEditor/blocks.js';
|
|
17
18
|
import { triggerPlugin, type Trigger } from '../utils/markdownEditor/triggers.js';
|
|
18
19
|
import { wikilink, wikilinkNode } from '../utils/markdownEditor/wikilink.js';
|
|
@@ -21,6 +22,8 @@
|
|
|
21
22
|
type MenuEntry = { id: string; label: string; icon?: string; iconColor?: string; description?: string; choose: () => void };
|
|
22
23
|
|
|
23
24
|
const LINK_SEARCH_DEBOUNCE_MS = 120;
|
|
25
|
+
/** How far left of the text a dragged block can still be dropped (the handle's gutter), and above or below it. */
|
|
26
|
+
const DROP_MARGIN_PX = 72;
|
|
24
27
|
|
|
25
28
|
let {
|
|
26
29
|
value,
|
|
@@ -50,7 +53,12 @@
|
|
|
50
53
|
let host = $state<HTMLDivElement>();
|
|
51
54
|
let handle = $state<HTMLDivElement>();
|
|
52
55
|
let editor: Editor | undefined;
|
|
56
|
+
let ready = $state(false);
|
|
53
57
|
let provider: BlockProvider | undefined;
|
|
58
|
+
/** Position of the block being dragged by its handle. */
|
|
59
|
+
let dragFrom: number | null = null;
|
|
60
|
+
/** Where it would land, as a line in the editor's own coordinates. */
|
|
61
|
+
let dropLine = $state<{ pos: number; top: number; left: number; width: number } | null>(null);
|
|
54
62
|
/** Normalized markdown of what the editor last loaded or emitted; changes equal to it are not edits. */
|
|
55
63
|
let known = '';
|
|
56
64
|
|
|
@@ -142,10 +150,10 @@
|
|
|
142
150
|
onchange?.(tidy);
|
|
143
151
|
}
|
|
144
152
|
|
|
145
|
-
/** Load outside changes, but never under the writer's caret. */
|
|
153
|
+
/** Load outside changes, but never under the writer's caret. Also runs once the editor exists, for a value that changed while it was being created. */
|
|
146
154
|
$effect(() => {
|
|
147
155
|
const next = value;
|
|
148
|
-
if (!
|
|
156
|
+
if (!ready || tidyMarkdown(next) === known) return;
|
|
149
157
|
const focused = act((ctx) => ctx.get(editorViewCtx).hasFocus());
|
|
150
158
|
if (focused) return;
|
|
151
159
|
act(replaceAll(next));
|
|
@@ -166,6 +174,62 @@
|
|
|
166
174
|
});
|
|
167
175
|
}
|
|
168
176
|
|
|
177
|
+
function nearEditor(event: DragEvent): boolean {
|
|
178
|
+
const rect = root!.getBoundingClientRect();
|
|
179
|
+
return (
|
|
180
|
+
event.clientX >= rect.left - DROP_MARGIN_PX &&
|
|
181
|
+
event.clientX <= rect.right &&
|
|
182
|
+
event.clientY >= rect.top - DROP_MARGIN_PX &&
|
|
183
|
+
event.clientY <= rect.bottom + DROP_MARGIN_PX
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Blocks land between their siblings, wherever along the line the pointer is (the gutter included). */
|
|
188
|
+
function onBlockDragOver(event: DragEvent) {
|
|
189
|
+
if (dragFrom === null) return;
|
|
190
|
+
if (!nearEditor(event)) {
|
|
191
|
+
dropLine = null;
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
event.preventDefault();
|
|
195
|
+
event.stopPropagation();
|
|
196
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
|
|
197
|
+
const from = dragFrom;
|
|
198
|
+
const target = act((ctx) => findDropTarget(ctx.get(editorViewCtx), from, event.clientY));
|
|
199
|
+
const origin = root!.getBoundingClientRect();
|
|
200
|
+
dropLine = target ? { ...target, top: target.top - origin.top, left: target.left - origin.left } : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function onBlockDrop(event: DragEvent) {
|
|
204
|
+
const from = dragFrom;
|
|
205
|
+
const line = dropLine;
|
|
206
|
+
if (from === null || !line) return;
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
event.stopPropagation();
|
|
209
|
+
act((ctx) => {
|
|
210
|
+
const view = ctx.get(editorViewCtx);
|
|
211
|
+
const tr = moveBlock(view.state, from, line.pos);
|
|
212
|
+
if (tr) view.dispatch(tr.scrollIntoView());
|
|
213
|
+
view.dragging = null;
|
|
214
|
+
view.focus();
|
|
215
|
+
});
|
|
216
|
+
endBlockDrag();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function startBlockDrag() {
|
|
220
|
+
dragFrom = provider?.active?.$pos.pos ?? null;
|
|
221
|
+
if (dragFrom === null) return;
|
|
222
|
+
document.addEventListener('dragover', onBlockDragOver, true);
|
|
223
|
+
document.addEventListener('drop', onBlockDrop, true);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function endBlockDrag() {
|
|
227
|
+
dragFrom = null;
|
|
228
|
+
dropLine = null;
|
|
229
|
+
document.removeEventListener('dragover', onBlockDragOver, true);
|
|
230
|
+
document.removeEventListener('drop', onBlockDrop, true);
|
|
231
|
+
}
|
|
232
|
+
|
|
169
233
|
function onHostClick(event: MouseEvent) {
|
|
170
234
|
const link = (event.target as HTMLElement).closest<HTMLElement>('[data-wikilink]');
|
|
171
235
|
if (link && onlinkopen) onlinkopen(link.dataset.wikilink ?? '');
|
|
@@ -204,10 +268,12 @@
|
|
|
204
268
|
known = tidyMarkdown(act(getMarkdown()) ?? value);
|
|
205
269
|
provider = new BlockProvider({ ctx: created.ctx, content: handle!, root, getOffset: () => 6 });
|
|
206
270
|
provider.update();
|
|
271
|
+
ready = true;
|
|
207
272
|
});
|
|
208
273
|
|
|
209
274
|
return () => {
|
|
210
275
|
disposed = true;
|
|
276
|
+
endBlockDrag();
|
|
211
277
|
provider?.destroy();
|
|
212
278
|
void editor?.destroy();
|
|
213
279
|
};
|
|
@@ -218,12 +284,15 @@
|
|
|
218
284
|
<!-- Link chips are also reachable as text; the click only adds a shortcut to open them. -->
|
|
219
285
|
<!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
|
|
220
286
|
<div bind:this={host} onclick={onHostClick}></div>
|
|
221
|
-
<div class="terminoir-md__handle" bind:this={handle}>
|
|
287
|
+
<div class="terminoir-md__handle" bind:this={handle} ondragstart={startBlockDrag} ondragend={endBlockDrag} role="presentation">
|
|
222
288
|
<button type="button" class="terminoir-md__handle-button" aria-label="Add block below" onmousedown={(e) => e.preventDefault()} onclick={addBlockBelow}>
|
|
223
289
|
<NavIcon name="plus" />
|
|
224
290
|
</button>
|
|
225
291
|
<span class="terminoir-md__handle-grip" aria-hidden="true" title="Drag to move"><NavIcon name="grip-vertical" /></span>
|
|
226
292
|
</div>
|
|
293
|
+
{#if dropLine}
|
|
294
|
+
<div class="terminoir-md__drop-line" aria-hidden="true" style:top={`${dropLine.top}px`} style:left={`${dropLine.left}px`} style:width={`${dropLine.width}px`}></div>
|
|
295
|
+
{/if}
|
|
227
296
|
</div>
|
|
228
297
|
|
|
229
298
|
{#if trigger && (entries.length || trigger.kind === 'link')}
|
|
@@ -336,6 +405,15 @@
|
|
|
336
405
|
.terminoir-md__handle-button:hover,
|
|
337
406
|
.terminoir-md__handle-grip:hover { background: color-mix(in srgb, var(--foreground) 8%, transparent); color: var(--foreground); }
|
|
338
407
|
|
|
408
|
+
.terminoir-md__drop-line {
|
|
409
|
+
position: absolute;
|
|
410
|
+
height: 2px;
|
|
411
|
+
margin-top: -1px;
|
|
412
|
+
border-radius: 1px;
|
|
413
|
+
background: var(--primary);
|
|
414
|
+
pointer-events: none;
|
|
415
|
+
}
|
|
416
|
+
|
|
339
417
|
.terminoir-md__menu {
|
|
340
418
|
position: fixed;
|
|
341
419
|
z-index: 60;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { cn } from '../utils/cn.js';
|
|
4
|
+
import Avatar from './Avatar.svelte';
|
|
5
|
+
import NavIcon from './NavIcon.svelte';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Heading inside a panel or card: a mark, a title, a one-line description and actions on the right.
|
|
9
|
+
* The mark is `logo` (an image, or the name's initials while there is no `src`), `icon`, or `leading`.
|
|
10
|
+
*/
|
|
11
|
+
let {
|
|
12
|
+
title,
|
|
13
|
+
description,
|
|
14
|
+
icon,
|
|
15
|
+
logo,
|
|
16
|
+
class: className = '',
|
|
17
|
+
leading,
|
|
18
|
+
actions,
|
|
19
|
+
}: {
|
|
20
|
+
title: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
/** Font Awesome name, as for `NavIcon`. */
|
|
23
|
+
icon?: string;
|
|
24
|
+
/** A service or brand mark, shown as a round `Avatar`. */
|
|
25
|
+
logo?: { name: string; src?: string };
|
|
26
|
+
class?: string;
|
|
27
|
+
/** Custom mark; overrides `logo` and `icon`. */
|
|
28
|
+
leading?: Snippet;
|
|
29
|
+
/** Status chips or buttons aligned to the right. */
|
|
30
|
+
actions?: Snippet;
|
|
31
|
+
} = $props();
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<header class={cn('terminoir-panel-header', className)}>
|
|
35
|
+
{#if leading}
|
|
36
|
+
<span class="terminoir-panel-header__mark">{@render leading()}</span>
|
|
37
|
+
{:else if logo}
|
|
38
|
+
<Avatar name={logo.name} src={logo.src} size={36} />
|
|
39
|
+
{:else if icon}
|
|
40
|
+
<span class="terminoir-panel-header__mark"><NavIcon name={icon} /></span>
|
|
41
|
+
{/if}
|
|
42
|
+
<div class="terminoir-panel-header__text">
|
|
43
|
+
<h3 class="terminoir-panel-header__title">{title}</h3>
|
|
44
|
+
{#if description}<p class="terminoir-panel-header__description">{description}</p>{/if}
|
|
45
|
+
</div>
|
|
46
|
+
{#if actions}
|
|
47
|
+
<div class="terminoir-panel-header__actions">{@render actions()}</div>
|
|
48
|
+
{/if}
|
|
49
|
+
</header>
|
|
50
|
+
|
|
51
|
+
<style>
|
|
52
|
+
.terminoir-panel-header {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
gap: var(--space-3);
|
|
56
|
+
min-width: 0;
|
|
57
|
+
}
|
|
58
|
+
.terminoir-panel-header__mark {
|
|
59
|
+
display: inline-flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
justify-content: center;
|
|
62
|
+
flex-shrink: 0;
|
|
63
|
+
min-width: 1.25rem;
|
|
64
|
+
}
|
|
65
|
+
.terminoir-panel-header__text {
|
|
66
|
+
flex: 1;
|
|
67
|
+
min-width: 0;
|
|
68
|
+
display: flex;
|
|
69
|
+
flex-direction: column;
|
|
70
|
+
gap: 0.15rem;
|
|
71
|
+
}
|
|
72
|
+
.terminoir-panel-header__title {
|
|
73
|
+
margin: 0;
|
|
74
|
+
font-size: 0.9375rem;
|
|
75
|
+
font-weight: 600;
|
|
76
|
+
line-height: 1.35;
|
|
77
|
+
overflow-wrap: anywhere;
|
|
78
|
+
}
|
|
79
|
+
.terminoir-panel-header__description {
|
|
80
|
+
margin: 0;
|
|
81
|
+
color: var(--muted-foreground);
|
|
82
|
+
font-size: 0.8125rem;
|
|
83
|
+
line-height: 1.45;
|
|
84
|
+
}
|
|
85
|
+
.terminoir-panel-header__actions {
|
|
86
|
+
display: flex;
|
|
87
|
+
flex-wrap: wrap;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: var(--space-3);
|
|
90
|
+
flex-shrink: 0;
|
|
91
|
+
}
|
|
92
|
+
@media (max-width: 700px) {
|
|
93
|
+
.terminoir-panel-header { flex-wrap: wrap; }
|
|
94
|
+
.terminoir-panel-header__actions { width: 100%; }
|
|
95
|
+
}
|
|
96
|
+
</style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type $$ComponentProps = {
|
|
3
|
+
title: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
/** Font Awesome name, as for `NavIcon`. */
|
|
6
|
+
icon?: string;
|
|
7
|
+
/** A service or brand mark, shown as a round `Avatar`. */
|
|
8
|
+
logo?: {
|
|
9
|
+
name: string;
|
|
10
|
+
src?: string;
|
|
11
|
+
};
|
|
12
|
+
class?: string;
|
|
13
|
+
/** Custom mark; overrides `logo` and `icon`. */
|
|
14
|
+
leading?: Snippet;
|
|
15
|
+
/** Status chips or buttons aligned to the right. */
|
|
16
|
+
actions?: Snippet;
|
|
17
|
+
};
|
|
18
|
+
declare const PanelHeader: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
19
|
+
type PanelHeader = ReturnType<typeof PanelHeader>;
|
|
20
|
+
export default PanelHeader;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Chip from './Chip.svelte';
|
|
3
|
+
|
|
4
|
+
/** On/off status: `active` shows a green dot and `activeLabel`; otherwise a muted `inactiveLabel`. */
|
|
5
|
+
let {
|
|
6
|
+
active,
|
|
7
|
+
activeLabel = 'Connected',
|
|
8
|
+
inactiveLabel = 'Not connected',
|
|
9
|
+
class: className = '',
|
|
10
|
+
...rest
|
|
11
|
+
}: {
|
|
12
|
+
active: boolean;
|
|
13
|
+
activeLabel?: string;
|
|
14
|
+
inactiveLabel?: string;
|
|
15
|
+
class?: string;
|
|
16
|
+
} & Record<string, unknown> = $props();
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<Chip tone={active ? 'default' : 'done'} color={active ? 'green' : undefined} class={className} {...rest}>
|
|
20
|
+
{active ? activeLabel : inactiveLabel}
|
|
21
|
+
</Chip>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
active: boolean;
|
|
3
|
+
activeLabel?: string;
|
|
4
|
+
inactiveLabel?: string;
|
|
5
|
+
class?: string;
|
|
6
|
+
} & Record<string, unknown>;
|
|
7
|
+
declare const StatusChip: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
8
|
+
type StatusChip = ReturnType<typeof StatusChip>;
|
|
9
|
+
export default StatusChip;
|
package/dist/index.d.ts
CHANGED
|
@@ -34,6 +34,7 @@ export { default as Panel } from './components/Panel.svelte';
|
|
|
34
34
|
export { default as Popover } from './components/Popover.svelte';
|
|
35
35
|
export { default as Select } from './components/Select.svelte';
|
|
36
36
|
export { default as Skeleton } from './components/Skeleton.svelte';
|
|
37
|
+
export { default as StatusChip } from './components/StatusChip.svelte';
|
|
37
38
|
export { default as Switch } from './components/Switch.svelte';
|
|
38
39
|
export { default as Tabs } from './components/Tabs.svelte';
|
|
39
40
|
export { default as Textarea } from './components/Textarea.svelte';
|
|
@@ -41,6 +42,7 @@ export { default as Toaster } from './components/Toaster.svelte';
|
|
|
41
42
|
export { default as Tooltip } from './components/Tooltip.svelte';
|
|
42
43
|
export { default as Page } from './components/Page.svelte';
|
|
43
44
|
export { default as PageHeader } from './components/PageHeader.svelte';
|
|
45
|
+
export { default as PanelHeader } from './components/PanelHeader.svelte';
|
|
44
46
|
export { default as Section } from './components/Section.svelte';
|
|
45
47
|
export { default as Stack } from './components/Stack.svelte';
|
|
46
48
|
export { default as Inline } from './components/Inline.svelte';
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ export { default as Panel } from './components/Panel.svelte';
|
|
|
32
32
|
export { default as Popover } from './components/Popover.svelte';
|
|
33
33
|
export { default as Select } from './components/Select.svelte';
|
|
34
34
|
export { default as Skeleton } from './components/Skeleton.svelte';
|
|
35
|
+
export { default as StatusChip } from './components/StatusChip.svelte';
|
|
35
36
|
export { default as Switch } from './components/Switch.svelte';
|
|
36
37
|
export { default as Tabs } from './components/Tabs.svelte';
|
|
37
38
|
export { default as Textarea } from './components/Textarea.svelte';
|
|
@@ -40,6 +41,7 @@ export { default as Tooltip } from './components/Tooltip.svelte';
|
|
|
40
41
|
// Layout
|
|
41
42
|
export { default as Page } from './components/Page.svelte';
|
|
42
43
|
export { default as PageHeader } from './components/PageHeader.svelte';
|
|
44
|
+
export { default as PanelHeader } from './components/PanelHeader.svelte';
|
|
43
45
|
export { default as Section } from './components/Section.svelte';
|
|
44
46
|
export { default as Stack } from './components/Stack.svelte';
|
|
45
47
|
export { default as Inline } from './components/Inline.svelte';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Moving a block by its drag handle: where it lands, and the transaction that moves it. */
|
|
2
|
+
import type { EditorState, Transaction } from '@milkdown/kit/prose/state';
|
|
3
|
+
import type { EditorView } from '@milkdown/kit/prose/view';
|
|
4
|
+
type Span = {
|
|
5
|
+
top: number;
|
|
6
|
+
bottom: number;
|
|
7
|
+
};
|
|
8
|
+
/** A place a dragged block can land: document position, and the line drawn there. */
|
|
9
|
+
export type DropTarget = {
|
|
10
|
+
pos: number;
|
|
11
|
+
top: number;
|
|
12
|
+
left: number;
|
|
13
|
+
width: number;
|
|
14
|
+
};
|
|
15
|
+
/** Index of the gap (0 = before the first block, n = after the last) nearest to `y`. */
|
|
16
|
+
export declare function dropGap(blocks: readonly Span[], y: number): number;
|
|
17
|
+
/** Moves the node at `from` to document position `to`; null when that would leave it in place. */
|
|
18
|
+
export declare function moveBlock(state: EditorState, from: number, to: number): Transaction | null;
|
|
19
|
+
/** The gap among the dragged block's siblings closest to `y`, in viewport coordinates. */
|
|
20
|
+
export declare function findDropTarget(view: EditorView, from: number, y: number): DropTarget | null;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Index of the gap (0 = before the first block, n = after the last) nearest to `y`. */
|
|
2
|
+
export function dropGap(blocks, y) {
|
|
3
|
+
const index = blocks.findIndex((block) => y < (block.top + block.bottom) / 2);
|
|
4
|
+
return index === -1 ? blocks.length : index;
|
|
5
|
+
}
|
|
6
|
+
/** Moves the node at `from` to document position `to`; null when that would leave it in place. */
|
|
7
|
+
export function moveBlock(state, from, to) {
|
|
8
|
+
const node = state.doc.nodeAt(from);
|
|
9
|
+
if (!node || (to >= from && to <= from + node.nodeSize))
|
|
10
|
+
return null;
|
|
11
|
+
const tr = state.tr.delete(from, from + node.nodeSize);
|
|
12
|
+
return tr.insert(tr.mapping.map(to), node);
|
|
13
|
+
}
|
|
14
|
+
/** The gap among the dragged block's siblings closest to `y`, in viewport coordinates. */
|
|
15
|
+
export function findDropTarget(view, from, y) {
|
|
16
|
+
const $from = view.state.doc.resolve(from);
|
|
17
|
+
const start = $from.start();
|
|
18
|
+
const siblings = [];
|
|
19
|
+
$from.parent.forEach((_, offset) => {
|
|
20
|
+
const dom = view.nodeDOM(start + offset);
|
|
21
|
+
if (dom instanceof HTMLElement)
|
|
22
|
+
siblings.push({ pos: start + offset, rect: dom.getBoundingClientRect() });
|
|
23
|
+
});
|
|
24
|
+
if (!siblings.length)
|
|
25
|
+
return null;
|
|
26
|
+
const gap = dropGap(siblings.map((s) => s.rect), y);
|
|
27
|
+
const { left, width } = siblings[0].rect;
|
|
28
|
+
if (gap < siblings.length) {
|
|
29
|
+
const { pos, rect } = siblings[gap];
|
|
30
|
+
const above = siblings[gap - 1]?.rect.bottom ?? rect.top;
|
|
31
|
+
return { pos, top: (above + rect.top) / 2, left, width };
|
|
32
|
+
}
|
|
33
|
+
return { pos: start + $from.parent.content.size, top: siblings[siblings.length - 1].rect.bottom, left, width };
|
|
34
|
+
}
|