@pixerate/editor-svelte 0.24.0 → 0.25.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/ExportDocxButton.svelte +48 -0
- package/dist/ExportDocxButton.svelte.d.ts +13 -0
- package/dist/ExportDocxButton.svelte.d.ts.map +1 -0
- package/dist/canvas/AnimationTimeline.svelte +217 -0
- package/dist/canvas/AnimationTimeline.svelte.d.ts +20 -0
- package/dist/canvas/AnimationTimeline.svelte.d.ts.map +1 -0
- package/dist/canvas/CanvasComponentsPanel.svelte +73 -0
- package/dist/canvas/CanvasComponentsPanel.svelte.d.ts +13 -0
- package/dist/canvas/CanvasComponentsPanel.svelte.d.ts.map +1 -0
- package/dist/canvas/CanvasInstanceInspector.svelte +136 -0
- package/dist/canvas/CanvasInstanceInspector.svelte.d.ts +16 -0
- package/dist/canvas/CanvasInstanceInspector.svelte.d.ts.map +1 -0
- package/dist/canvas/createCanvasAnimation.svelte.d.ts +98 -0
- package/dist/canvas/createCanvasAnimation.svelte.d.ts.map +1 -0
- package/dist/canvas/createCanvasAnimation.svelte.js +79 -0
- package/dist/canvas/createCanvasArrange.svelte.d.ts.map +1 -1
- package/dist/canvas/createCanvasAutoLayout.svelte.d.ts.map +1 -1
- package/dist/canvas/createCanvasComponents.svelte.d.ts +68 -0
- package/dist/canvas/createCanvasComponents.svelte.d.ts.map +1 -0
- package/dist/canvas/createCanvasComponents.svelte.js +31 -0
- package/dist/canvas/index.d.ts +7 -0
- package/dist/canvas/index.d.ts.map +1 -1
- package/dist/canvas/index.js +9 -0
- package/dist/docxExport.svelte.d.ts +26 -0
- package/dist/docxExport.svelte.d.ts.map +1 -0
- package/dist/docxExport.svelte.js +36 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/spreadsheet/SpreadsheetEditor.svelte +77 -7
- package/dist/spreadsheet/SpreadsheetEditor.svelte.d.ts +6 -0
- package/dist/spreadsheet/SpreadsheetEditor.svelte.d.ts.map +1 -1
- package/dist/spreadsheet/SpreadsheetGroupRow.svelte +31 -3
- package/dist/spreadsheet/SpreadsheetGroupRow.svelte.d.ts +6 -0
- package/dist/spreadsheet/SpreadsheetGroupRow.svelte.d.ts.map +1 -1
- package/dist/spreadsheet/index.d.ts +1 -0
- package/dist/spreadsheet/index.d.ts.map +1 -1
- package/dist/spreadsheet/index.js +1 -0
- package/dist/spreadsheet/spreadsheet.svelte.d.ts +1 -1
- package/dist/spreadsheet/spreadsheet.svelte.d.ts.map +1 -1
- package/dist/spreadsheet/viewport.svelte.d.ts +26 -0
- package/dist/spreadsheet/viewport.svelte.d.ts.map +1 -0
- package/dist/spreadsheet/viewport.svelte.js +93 -0
- package/dist/tables/InsertTableGrid.svelte +85 -0
- package/dist/tables/InsertTableGrid.svelte.d.ts +21 -0
- package/dist/tables/InsertTableGrid.svelte.d.ts.map +1 -0
- package/dist/tables/TableBubbleMenu.svelte +43 -0
- package/dist/tables/TableBubbleMenu.svelte.d.ts +18 -0
- package/dist/tables/TableBubbleMenu.svelte.d.ts.map +1 -0
- package/dist/tables/TableToolbar.svelte +81 -0
- package/dist/tables/TableToolbar.svelte.d.ts +19 -0
- package/dist/tables/TableToolbar.svelte.d.ts.map +1 -0
- package/dist/tables/index.d.ts +12 -0
- package/dist/tables/index.d.ts.map +1 -0
- package/dist/tables/index.js +4 -0
- package/dist/tables/tableMenu.svelte.d.ts +21 -0
- package/dist/tables/tableMenu.svelte.d.ts.map +1 -0
- package/dist/tables/tableMenu.svelte.js +27 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -69,6 +69,8 @@ Requires **Svelte 5** (the components use runes). Components ship as `.svelte` s
|
|
|
69
69
|
|
|
70
70
|
`ReviewToolbar` steps through insertions, deletions, formatting and block changes and shows the current one's author and description (e.g. "Ada: Formatted: Bold"); hide it with `showDescription={false}` or restyle it with `descriptionClass`. Put `<CommentButton {editor} />` inside a `BubbleMenu` to comment on the selection. `createComments(editorOrStore)` and `createTrackChanges(editor)` are rune (and Svelte store) adapters for custom UIs.
|
|
71
71
|
|
|
72
|
+
Tables are on by default in `createRichTextPreset` (`table: false` turns them off). `<TableToolbar {editor} />` shows row, column, merge/split, header and alignment buttons while the selection is in a table (disabled buttons explain why in their tooltip; `items` picks the buttons, `showWhenOutside` keeps it visible). `<TableBubbleMenu {editor} />` shows it in a bubble menu next to the cursor, and `<InsertTableGrid {editor} oninsert={…} />` is a Word-style size picker. `createTableMenu(editor)` is the rune adapter for custom UIs.
|
|
73
|
+
|
|
72
74
|
`<FindReplaceBar {editor} />` is a Word-style find bar for editors with the `FindReplace` extension (`createRichTextPreset({ findReplace: true })`): it appears on Mod+F / Mod+H (or `editor.commands.openFindReplace()`), with find and replace fields, option toggles, a "3 of 12" counter, previous/next, Replace and Replace all; Enter / Shift+Enter step, Mod+Enter in the replace field replaces all, Escape closes (`onclose`). `createFindReplace(editor)` is the rune (and Svelte store) adapter for custom UIs.
|
|
73
75
|
|
|
74
76
|
## More exports
|
|
@@ -76,7 +78,7 @@ Requires **Svelte 5** (the components use runes). Components ship as `.svelte` s
|
|
|
76
78
|
| Import | Exports |
|
|
77
79
|
| :--- | :--- |
|
|
78
80
|
| `@pixerate/editor-svelte` | `EditableTextNodeEditor`, `BubbleMenu`, `TemplateRenderer`, `initiateEditor`, `createReactiveEditor`, `createHistory`, `createHistoryDirty`, `createHistoryShortcuts`, `navigationGuard` (action), `createNavigationGuard`, `SpreadsheetEditor`, `FormulaBar`, `FormulaHints`, `createReactiveSpreadsheet`, `DirtyTracker` |
|
|
79
|
-
| `@pixerate/editor-svelte/spreadsheet` | `SpreadsheetEditor
|
|
81
|
+
| `@pixerate/editor-svelte/spreadsheet` | `SpreadsheetEditor` (virtualised for large sheets, `virtualize` prop), `FormulaBar`, `FormulaHints`, `SpreadsheetContextMenu`, `SpreadsheetGroupRow`, `SheetTabs`, `NameManager`, `createReactiveSpreadsheet`, `createSpreadsheetViewport` (also re-exported from the root entry) |
|
|
80
82
|
| `@pixerate/editor-svelte/image-editor` | `ImageEditor`, `createReactiveImageEditor`: crop, rotate/flip, adjustments, annotations, depth masks, Select & Mask (`SelectionOptionsBar`, `RefineEdgePanel`: magic wand, marquees, lasso, Refine Edge, layer masks), and `onInpaint` / `onRemoveBackground` AI hooks |
|
|
81
83
|
| `@pixerate/editor-svelte/commands` | `CommandPalette`, `createCommandPalette` (`hotkey: "Mod+K"`; call `destroy()` when done), `createCommandShortcuts`, `useRegisterCommands` over a `CommandRegistry` from `@pixerate/editor/commands` (also re-exported from the root entry) |
|
|
82
84
|
| `@pixerate/editor-svelte/persistence` | `createAutosave` (crash-recovery snapshots bound to a history / dirty flag, flushed on `pagehide`; call `destroy()` when done) and `<RecoveryPrompt>` (also re-exported from the root entry) |
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Editor } from "@tiptap/core";
|
|
3
|
+
import type { DocxExportControllerOptions, DocxExportReport } from "@pixerate/editor/export";
|
|
4
|
+
import { createDocxExport } from "./docxExport.svelte.js";
|
|
5
|
+
|
|
6
|
+
interface Props extends DocxExportControllerOptions {
|
|
7
|
+
editor?: Editor | null;
|
|
8
|
+
class?: string;
|
|
9
|
+
children?: import("svelte").Snippet;
|
|
10
|
+
/** Called after the download with the export report. */
|
|
11
|
+
onExported?: (report: DocxExportReport) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
editor = null,
|
|
16
|
+
class: className = "rounded px-2 py-1 text-xs text-foreground hover:bg-muted disabled:opacity-50",
|
|
17
|
+
children,
|
|
18
|
+
onExported,
|
|
19
|
+
...options
|
|
20
|
+
}: Props = $props();
|
|
21
|
+
|
|
22
|
+
// Options are applied (and kept current) by the effect below.
|
|
23
|
+
const docx = createDocxExport(() => editor);
|
|
24
|
+
$effect(() => {
|
|
25
|
+
docx.setOptions({ ...options });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
async function exportNow() {
|
|
29
|
+
const report = await docx.download();
|
|
30
|
+
if (report) onExported?.(report);
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<!-- A button that downloads the editor's document as a Word file (`.docx`). -->
|
|
35
|
+
<button
|
|
36
|
+
type="button"
|
|
37
|
+
class={className}
|
|
38
|
+
disabled={!editor || docx.isExporting}
|
|
39
|
+
aria-busy={docx.isExporting || undefined}
|
|
40
|
+
title={docx.error ? `Export failed: ${docx.error.message}` : undefined}
|
|
41
|
+
onclick={exportNow}
|
|
42
|
+
>
|
|
43
|
+
{#if children}
|
|
44
|
+
{@render children()}
|
|
45
|
+
{:else}
|
|
46
|
+
Export to Word
|
|
47
|
+
{/if}
|
|
48
|
+
</button>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/core";
|
|
2
|
+
import type { DocxExportControllerOptions, DocxExportReport } from "@pixerate/editor/export";
|
|
3
|
+
interface Props extends DocxExportControllerOptions {
|
|
4
|
+
editor?: Editor | null;
|
|
5
|
+
class?: string;
|
|
6
|
+
children?: import("svelte").Snippet;
|
|
7
|
+
/** Called after the download with the export report. */
|
|
8
|
+
onExported?: (report: DocxExportReport) => void;
|
|
9
|
+
}
|
|
10
|
+
declare const ExportDocxButton: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type ExportDocxButton = ReturnType<typeof ExportDocxButton>;
|
|
12
|
+
export default ExportDocxButton;
|
|
13
|
+
//# sourceMappingURL=ExportDocxButton.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExportDocxButton.svelte.d.ts","sourceRoot":"","sources":["../src/ExportDocxButton.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAC3C,OAAO,KAAK,EAAE,2BAA2B,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAI3F,UAAU,KAAM,SAAQ,2BAA2B;IACjD,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACpC,wDAAwD;IACxD,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACjD;AAoCH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onDestroy } from 'svelte';
|
|
3
|
+
import {
|
|
4
|
+
ANIMATION_EASING_LABELS,
|
|
5
|
+
ANIMATION_EASING_PRESETS,
|
|
6
|
+
formatAnimationTime,
|
|
7
|
+
getTimelineTicks,
|
|
8
|
+
timeToX,
|
|
9
|
+
xToTime,
|
|
10
|
+
type AnimationLoopMode,
|
|
11
|
+
type KeyframeRef
|
|
12
|
+
} from '@pixerate/editor/canvas';
|
|
13
|
+
import type { SvelteCanvasAnimation } from './createCanvasAnimation.svelte.js';
|
|
14
|
+
import type { CanvasNode } from './types.js';
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
/** The `createCanvasAnimation` rune. */
|
|
18
|
+
animation: SvelteCanvasAnimation<any>;
|
|
19
|
+
/** Nodes whose tracks are listed (default: the selected nodes). */
|
|
20
|
+
nodeIds?: readonly string[];
|
|
21
|
+
/** Width of the keyframe lanes in px (default 360). */
|
|
22
|
+
width?: number;
|
|
23
|
+
/** Snap distance in screen px while dragging keyframes (default 8; hold Alt to turn snapping off). */
|
|
24
|
+
snapThreshold?: number;
|
|
25
|
+
labelOf?: (node: CanvasNode) => string;
|
|
26
|
+
/** Bump to re-read the rows after changing the nodes outside the rune. */
|
|
27
|
+
revision?: unknown;
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let { animation, nodeIds, width = 360, snapThreshold = 8, labelOf, revision, class: className = '' }: Props = $props();
|
|
32
|
+
|
|
33
|
+
const LOOP_LABELS: Record<AnimationLoopMode, string> = { none: 'Once', loop: 'Loop', 'ping-pong': 'Ping-pong' };
|
|
34
|
+
const labelWidth = 120;
|
|
35
|
+
|
|
36
|
+
const state = $derived(animation.state);
|
|
37
|
+
const duration = $derived(state.duration);
|
|
38
|
+
const rows = $derived.by(() => {
|
|
39
|
+
void state;
|
|
40
|
+
void revision;
|
|
41
|
+
void animation.revision;
|
|
42
|
+
return animation.timelineRows(nodeIds ?? undefined, labelOf ? { labelOf: labelOf as any } : {});
|
|
43
|
+
});
|
|
44
|
+
const ticks = $derived(getTimelineTicks(duration, width));
|
|
45
|
+
const playheadX = $derived(timeToX(state.time, duration, width));
|
|
46
|
+
const easingValue = (easing: unknown) => (typeof easing === 'string' ? easing : easing ? 'custom' : 'linear');
|
|
47
|
+
const selectedEasing = $derived.by(() => {
|
|
48
|
+
const first = state.selection[0];
|
|
49
|
+
if (!first) return '';
|
|
50
|
+
for (const row of rows) for (const k of row.keyframes) if (k.selected && k.ref.property === first.property && k.ref.nodeId === first.nodeId) return easingValue(k.easing);
|
|
51
|
+
return '';
|
|
52
|
+
});
|
|
53
|
+
const multiNode = $derived(new Set(rows.map((r) => r.nodeId)).size > 1);
|
|
54
|
+
|
|
55
|
+
let dragStartX: number | null = null;
|
|
56
|
+
const cleanups: (() => void)[] = [];
|
|
57
|
+
const listen = (move: (e: PointerEvent) => void, up: () => void) => {
|
|
58
|
+
const end = () => {
|
|
59
|
+
window.removeEventListener('pointermove', move);
|
|
60
|
+
window.removeEventListener('pointerup', end);
|
|
61
|
+
up();
|
|
62
|
+
};
|
|
63
|
+
window.addEventListener('pointermove', move);
|
|
64
|
+
window.addEventListener('pointerup', end);
|
|
65
|
+
cleanups.push(() => {
|
|
66
|
+
window.removeEventListener('pointermove', move);
|
|
67
|
+
window.removeEventListener('pointerup', end);
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function onRulerDown(event: PointerEvent) {
|
|
72
|
+
const left = (event.currentTarget as Element).getBoundingClientRect().left;
|
|
73
|
+
animation.scrub(xToTime(event.clientX - left, duration, width));
|
|
74
|
+
listen(
|
|
75
|
+
(e) => animation.scrub(xToTime(e.clientX - left, duration, width)),
|
|
76
|
+
() => animation.endScrub()
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onKeyframeDown(event: PointerEvent, ref: KeyframeRef) {
|
|
81
|
+
event.stopPropagation();
|
|
82
|
+
event.preventDefault();
|
|
83
|
+
if (event.shiftKey || event.metaKey || event.ctrlKey) {
|
|
84
|
+
animation.selectKeyframes([ref], 'toggle');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
animation.beginKeyframeDrag([ref]);
|
|
88
|
+
dragStartX = event.clientX;
|
|
89
|
+
const msPerPx = width > 0 ? duration / width : 0;
|
|
90
|
+
listen(
|
|
91
|
+
(e) => {
|
|
92
|
+
if (dragStartX === null) return;
|
|
93
|
+
animation.updateKeyframeDrag((e.clientX - dragStartX) * msPerPx, { snap: !e.altKey, thresholdMs: snapThreshold * msPerPx });
|
|
94
|
+
},
|
|
95
|
+
() => {
|
|
96
|
+
if (dragStartX === null) return;
|
|
97
|
+
dragStartX = null;
|
|
98
|
+
animation.endKeyframeDrag();
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function onWindowKey(event: KeyboardEvent) {
|
|
104
|
+
if (event.key === 'Escape' && dragStartX !== null) {
|
|
105
|
+
dragStartX = null;
|
|
106
|
+
animation.cancelKeyframeDrag();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function onTimelineKey(event: KeyboardEvent) {
|
|
111
|
+
if ((event.key === 'Delete' || event.key === 'Backspace') && state.selection.length > 0) {
|
|
112
|
+
event.preventDefault();
|
|
113
|
+
animation.deleteKeyframes();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
onDestroy(() => cleanups.forEach((fn) => fn()));
|
|
118
|
+
|
|
119
|
+
const btn = 'rounded border border-border px-2 py-0.5 text-xs hover:bg-muted disabled:opacity-50';
|
|
120
|
+
</script>
|
|
121
|
+
|
|
122
|
+
<svelte:window onkeydown={onWindowKey} />
|
|
123
|
+
|
|
124
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_no_static_element_interactions -->
|
|
125
|
+
<div data-animation-timeline="" class="flex flex-col gap-1 text-xs text-foreground {className}" tabindex="0" onkeydown={onTimelineKey}>
|
|
126
|
+
<div class="flex flex-wrap items-center gap-2" role="toolbar" aria-label="Animation">
|
|
127
|
+
<button type="button" class={btn} data-action="toggle-play" aria-label={state.playing ? 'Pause' : 'Play'} onclick={() => animation.toggle()}>
|
|
128
|
+
{state.playing ? '❚❚' : '▶'}
|
|
129
|
+
</button>
|
|
130
|
+
<button
|
|
131
|
+
type="button"
|
|
132
|
+
class="{btn} {state.recording ? 'text-red-500' : ''}"
|
|
133
|
+
data-action="record"
|
|
134
|
+
aria-pressed={state.recording}
|
|
135
|
+
aria-label="Record keyframes"
|
|
136
|
+
onclick={() => animation.toggleRecording()}
|
|
137
|
+
>
|
|
138
|
+
● Rec
|
|
139
|
+
</button>
|
|
140
|
+
<button type="button" class={btn} data-action="add-keyframe" onclick={() => animation.addKeyframe()}>◆ Key</button>
|
|
141
|
+
<span data-time="" class="tabular-nums text-muted-foreground">{formatAnimationTime(state.time)} / {formatAnimationTime(duration)}</span>
|
|
142
|
+
<select
|
|
143
|
+
aria-label="Easing"
|
|
144
|
+
data-easing-picker=""
|
|
145
|
+
class="rounded border border-border bg-background px-1 py-0.5"
|
|
146
|
+
value={selectedEasing}
|
|
147
|
+
disabled={state.selection.length === 0}
|
|
148
|
+
onchange={(e) => {
|
|
149
|
+
const value = (e.currentTarget as HTMLSelectElement).value;
|
|
150
|
+
if (value && value !== 'custom') animation.setEasing(value);
|
|
151
|
+
}}
|
|
152
|
+
>
|
|
153
|
+
<option value="">Easing…</option>
|
|
154
|
+
{#each ANIMATION_EASING_PRESETS as preset (preset)}
|
|
155
|
+
<option value={preset}>{ANIMATION_EASING_LABELS[preset]}</option>
|
|
156
|
+
{/each}
|
|
157
|
+
{#if selectedEasing === 'custom'}<option value="custom">Custom</option>{/if}
|
|
158
|
+
</select>
|
|
159
|
+
<select
|
|
160
|
+
aria-label="Loop"
|
|
161
|
+
data-loop-picker=""
|
|
162
|
+
class="rounded border border-border bg-background px-1 py-0.5"
|
|
163
|
+
value={state.loop}
|
|
164
|
+
onchange={(e) => animation.setLoop((e.currentTarget as HTMLSelectElement).value as AnimationLoopMode)}
|
|
165
|
+
>
|
|
166
|
+
{#each Object.keys(LOOP_LABELS) as mode (mode)}
|
|
167
|
+
<option value={mode}>{LOOP_LABELS[mode as AnimationLoopMode]}</option>
|
|
168
|
+
{/each}
|
|
169
|
+
</select>
|
|
170
|
+
</div>
|
|
171
|
+
<div class="flex">
|
|
172
|
+
<div style="width: {labelWidth}px" class="shrink-0"></div>
|
|
173
|
+
<div data-timeline-ruler="" class="relative h-5 cursor-ew-resize border-b border-border" style="width: {width}px" onpointerdown={onRulerDown}>
|
|
174
|
+
{#each ticks as tick (tick.time)}
|
|
175
|
+
<span class="absolute top-0 -translate-x-1/2 text-[10px] text-muted-foreground" style="left: {tick.x}px">{tick.label}</span>
|
|
176
|
+
{/each}
|
|
177
|
+
<div data-playhead="" class="absolute top-0 h-5 w-px bg-red-500" style="left: {playheadX}px"></div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
{#if rows.length === 0}
|
|
181
|
+
<p class="text-muted-foreground" data-timeline-empty="">Select an animated node, or press ◆ Key to add a keyframe.</p>
|
|
182
|
+
{:else}
|
|
183
|
+
{#each rows as row (row.id)}
|
|
184
|
+
<div class="flex items-center" data-track={row.id}>
|
|
185
|
+
<label class="flex shrink-0 items-center gap-1 truncate pr-2" style="width: {labelWidth}px" title="{row.nodeLabel} · {row.label}">
|
|
186
|
+
<input
|
|
187
|
+
type="checkbox"
|
|
188
|
+
aria-label="Enable {row.label}"
|
|
189
|
+
checked={row.enabled}
|
|
190
|
+
onchange={(e) => animation.setTrackEnabled(row.nodeId, row.property, (e.currentTarget as HTMLInputElement).checked)}
|
|
191
|
+
/>
|
|
192
|
+
<span class="truncate">{multiNode ? `${row.nodeLabel} · ` : ''}{row.label}</span>
|
|
193
|
+
</label>
|
|
194
|
+
<div data-lane="" class="relative h-5 rounded bg-muted/40 {row.enabled ? '' : 'opacity-50'}" style="width: {width}px">
|
|
195
|
+
<div class="pointer-events-none absolute top-0 h-5 w-px bg-red-500/60" style="left: {playheadX}px"></div>
|
|
196
|
+
{#if state.drag && state.drag.target !== null}
|
|
197
|
+
<div data-snap-target="" class="pointer-events-none absolute top-0 h-5 w-px bg-sky-400" style="left: {timeToX(state.drag.target, duration, width)}px"></div>
|
|
198
|
+
{/if}
|
|
199
|
+
{#each row.keyframes as k (k.ref.time)}
|
|
200
|
+
<button
|
|
201
|
+
type="button"
|
|
202
|
+
data-keyframe={k.ref.time}
|
|
203
|
+
aria-pressed={k.selected}
|
|
204
|
+
aria-label="{row.label} keyframe at {formatAnimationTime(k.ref.time)}"
|
|
205
|
+
title="{formatAnimationTime(k.time)} · {String(k.value)}"
|
|
206
|
+
class="absolute top-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rotate-45 border {k.selected
|
|
207
|
+
? 'border-sky-300 bg-sky-500'
|
|
208
|
+
: 'border-foreground/60 bg-background'} {k.easing === 'hold' ? 'rounded-none' : 'rounded-[1px]'}"
|
|
209
|
+
style="left: {timeToX(k.time, duration, width)}px"
|
|
210
|
+
onpointerdown={(e) => onKeyframeDown(e, k.ref)}
|
|
211
|
+
></button>
|
|
212
|
+
{/each}
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
{/each}
|
|
216
|
+
{/if}
|
|
217
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SvelteCanvasAnimation } from './createCanvasAnimation.svelte.js';
|
|
2
|
+
import type { CanvasNode } from './types.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** The `createCanvasAnimation` rune. */
|
|
5
|
+
animation: SvelteCanvasAnimation<any>;
|
|
6
|
+
/** Nodes whose tracks are listed (default: the selected nodes). */
|
|
7
|
+
nodeIds?: readonly string[];
|
|
8
|
+
/** Width of the keyframe lanes in px (default 360). */
|
|
9
|
+
width?: number;
|
|
10
|
+
/** Snap distance in screen px while dragging keyframes (default 8; hold Alt to turn snapping off). */
|
|
11
|
+
snapThreshold?: number;
|
|
12
|
+
labelOf?: (node: CanvasNode) => string;
|
|
13
|
+
/** Bump to re-read the rows after changing the nodes outside the rune. */
|
|
14
|
+
revision?: unknown;
|
|
15
|
+
class?: string;
|
|
16
|
+
}
|
|
17
|
+
declare const AnimationTimeline: import("svelte").Component<Props, {}, "">;
|
|
18
|
+
type AnimationTimeline = ReturnType<typeof AnimationTimeline>;
|
|
19
|
+
export default AnimationTimeline;
|
|
20
|
+
//# sourceMappingURL=AnimationTimeline.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AnimationTimeline.svelte.d.ts","sourceRoot":"","sources":["../../src/canvas/AnimationTimeline.svelte.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,mCAAmC,CAAC;AAC/E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG5C,UAAU,KAAK;IACd,wCAAwC;IACxC,SAAS,EAAE,qBAAqB,CAAC,GAAG,CAAC,CAAC;IACtC,mEAAmE;IACnE,OAAO,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sGAAsG;IACtG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,MAAM,CAAC;IACvC,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAoKF,QAAA,MAAM,iBAAiB,2CAAwC,CAAC;AAChE,KAAK,iBAAiB,GAAG,UAAU,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAC9D,eAAe,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { CANVAS_COMPONENT_DRAG_TYPE, listCanvasComponents } from '@pixerate/editor/canvas';
|
|
3
|
+
import type { CanvasNode } from './types.js';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
nodes: CanvasNode[];
|
|
7
|
+
/** Inserts an instance of a component (or a set's default variant); called on click. */
|
|
8
|
+
oninsert: (componentId: string) => void;
|
|
9
|
+
/** Show the search field (default `true`). */
|
|
10
|
+
search?: boolean;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { nodes, oninsert, search = true, class: className = '' }: Props = $props();
|
|
15
|
+
|
|
16
|
+
let query = $state('');
|
|
17
|
+
const listed = $derived(listCanvasComponents(nodes as any[]));
|
|
18
|
+
const q = $derived(query.trim().toLowerCase());
|
|
19
|
+
const match = (name: string) => !q || name.toLowerCase().includes(q);
|
|
20
|
+
const lone = $derived(listed.components.filter((c) => !c.setId && match(c.name)));
|
|
21
|
+
const sets = $derived(listed.sets.filter((s) => match(s.name) || s.variants.some((v) => match(v.name))));
|
|
22
|
+
|
|
23
|
+
function dragStart(event: DragEvent, id: string, name: string) {
|
|
24
|
+
if (!event.dataTransfer) return;
|
|
25
|
+
event.dataTransfer.setData(CANVAS_COMPONENT_DRAG_TYPE, id);
|
|
26
|
+
event.dataTransfer.setData('text/plain', name);
|
|
27
|
+
event.dataTransfer.effectAllowed = 'copy';
|
|
28
|
+
}
|
|
29
|
+
</script>
|
|
30
|
+
|
|
31
|
+
{#snippet item(id: string, name: string, count: number, kind: string)}
|
|
32
|
+
<button
|
|
33
|
+
type="button"
|
|
34
|
+
draggable="true"
|
|
35
|
+
data-component-id={id}
|
|
36
|
+
data-kind={kind}
|
|
37
|
+
class="flex w-full items-center justify-between gap-2 rounded px-2 py-1 text-left text-xs hover:bg-muted"
|
|
38
|
+
title="Insert an instance of {name}"
|
|
39
|
+
onclick={() => oninsert(id)}
|
|
40
|
+
ondragstart={(e) => dragStart(e, id, name)}
|
|
41
|
+
>
|
|
42
|
+
<span class="truncate"><span aria-hidden="true" class="mr-1 text-muted-foreground">◇</span>{name}</span>
|
|
43
|
+
<span class="text-[10px] text-muted-foreground" data-instance-count="">{count}</span>
|
|
44
|
+
</button>
|
|
45
|
+
{/snippet}
|
|
46
|
+
|
|
47
|
+
<div data-components-panel="" class="flex flex-col gap-1 text-xs text-foreground {className}">
|
|
48
|
+
{#if search}
|
|
49
|
+
<input
|
|
50
|
+
type="search"
|
|
51
|
+
aria-label="Search components"
|
|
52
|
+
placeholder="Search components"
|
|
53
|
+
class="rounded border border-border bg-background px-2 py-1 text-xs"
|
|
54
|
+
oninput={(e) => (query = e.currentTarget.value)}
|
|
55
|
+
/>
|
|
56
|
+
{/if}
|
|
57
|
+
{#each sets as set (set.id)}
|
|
58
|
+
<div data-component-set={set.id} class="flex flex-col">
|
|
59
|
+
{@render item(set.id, set.name, set.variants.reduce((sum, v) => sum + v.instanceCount, 0), 'set')}
|
|
60
|
+
<div class="ml-3 flex flex-col border-l border-border pl-1">
|
|
61
|
+
{#each set.variants as v (v.id)}
|
|
62
|
+
{@render item(v.id, v.name, v.instanceCount, 'variant')}
|
|
63
|
+
{/each}
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
{/each}
|
|
67
|
+
{#each lone as c (c.id)}
|
|
68
|
+
{@render item(c.id, c.name, c.instanceCount, 'component')}
|
|
69
|
+
{/each}
|
|
70
|
+
{#if listed.components.length === 0}
|
|
71
|
+
<p class="text-muted-foreground">No components yet. Select a frame and choose Create Component.</p>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CanvasNode } from './types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
nodes: CanvasNode[];
|
|
4
|
+
/** Inserts an instance of a component (or a set's default variant); called on click. */
|
|
5
|
+
oninsert: (componentId: string) => void;
|
|
6
|
+
/** Show the search field (default `true`). */
|
|
7
|
+
search?: boolean;
|
|
8
|
+
class?: string;
|
|
9
|
+
}
|
|
10
|
+
declare const CanvasComponentsPanel: import("svelte").Component<Props, {}, "">;
|
|
11
|
+
type CanvasComponentsPanel = ReturnType<typeof CanvasComponentsPanel>;
|
|
12
|
+
export default CanvasComponentsPanel;
|
|
13
|
+
//# sourceMappingURL=CanvasComponentsPanel.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CanvasComponentsPanel.svelte.d.ts","sourceRoot":"","sources":["../../src/canvas/CanvasComponentsPanel.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG5C,UAAU,KAAK;IACd,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,wFAAwF;IACxF,QAAQ,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,8CAA8C;IAC9C,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAsDF,QAAA,MAAM,qBAAqB,2CAAwC,CAAC;AACpE,KAAK,qBAAqB,GAAG,UAAU,CAAC,OAAO,qBAAqB,CAAC,CAAC;AACtE,eAAe,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { inspectCanvasInstance, listCanvasComponents, type CanvasComponentsController } from '@pixerate/editor/canvas';
|
|
3
|
+
import type { CanvasNode } from './types.js';
|
|
4
|
+
|
|
5
|
+
type Actions = Pick<
|
|
6
|
+
CanvasComponentsController<any>,
|
|
7
|
+
'setInstanceProperties' | 'resetOverrides' | 'detachInstance' | 'goToMainComponent' | 'pushOverrides' | 'restoreMainComponent'
|
|
8
|
+
>;
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
nodes: CanvasNode[];
|
|
12
|
+
/** An instance, or a layer inside one; nothing is shown otherwise. */
|
|
13
|
+
nodeId: string | null | undefined;
|
|
14
|
+
components: Actions;
|
|
15
|
+
/** Receives refusals (e.g. a property value that would nest a component in itself). */
|
|
16
|
+
onerror?: (reason: string) => void;
|
|
17
|
+
class?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let { nodes, nodeId, components, onerror, class: className = '' }: Props = $props();
|
|
21
|
+
|
|
22
|
+
const info = $derived(nodeId ? inspectCanvasInstance(nodes as any[], nodeId) : undefined);
|
|
23
|
+
const allComponents = $derived(listCanvasComponents(nodes as any[]).components);
|
|
24
|
+
const btn = 'rounded border border-border px-2 py-0.5 text-xs hover:bg-muted disabled:opacity-40';
|
|
25
|
+
|
|
26
|
+
function report(result: { ok: boolean; reason?: string } | undefined) {
|
|
27
|
+
if (result && !result.ok && result.reason) onerror?.(result.reason);
|
|
28
|
+
}
|
|
29
|
+
function setProp(name: string, value: unknown) {
|
|
30
|
+
if (info) report(components.setInstanceProperties({ [name]: value }, [info.id]));
|
|
31
|
+
}
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
{#if info}
|
|
35
|
+
<div data-instance-inspector="" data-instance-id={info.id} class="flex flex-col gap-2 text-xs text-foreground {className}">
|
|
36
|
+
<div class="flex items-center justify-between gap-2">
|
|
37
|
+
<span class="truncate font-medium" data-instance-main="">
|
|
38
|
+
{info.status === 'linked' ? info.componentName : 'Main component missing'}
|
|
39
|
+
</span>
|
|
40
|
+
{#if info.nested}<span class="text-muted-foreground">Nested</span>{/if}
|
|
41
|
+
</div>
|
|
42
|
+
{#each info.variantOptions as option (option.name)}
|
|
43
|
+
<label class="flex items-center gap-2" data-variant-property={option.name}>
|
|
44
|
+
<span class="w-24 shrink-0 truncate text-muted-foreground">{option.name}</span>
|
|
45
|
+
<select
|
|
46
|
+
aria-label="{option.name} variant"
|
|
47
|
+
class="min-w-0 flex-1 rounded border border-border bg-background px-1 py-0.5 text-xs"
|
|
48
|
+
value={info.variant[option.name] ?? ''}
|
|
49
|
+
onchange={(e) => setProp(option.name, e.currentTarget.value)}
|
|
50
|
+
>
|
|
51
|
+
{#each option.values as v (v)}
|
|
52
|
+
<option value={v}>{v}</option>
|
|
53
|
+
{/each}
|
|
54
|
+
</select>
|
|
55
|
+
</label>
|
|
56
|
+
{/each}
|
|
57
|
+
{#each info.properties as p (p.name)}
|
|
58
|
+
<label class="flex items-center gap-2" data-component-property={p.name}>
|
|
59
|
+
<span class="w-24 shrink-0 truncate text-muted-foreground">{p.name}</span>
|
|
60
|
+
{#if p.type === 'boolean'}
|
|
61
|
+
<input type="checkbox" aria-label={p.name} checked={p.value === true} onchange={(e) => setProp(p.name, e.currentTarget.checked)} />
|
|
62
|
+
{:else if p.type === 'text'}
|
|
63
|
+
{#key p.value}
|
|
64
|
+
<input
|
|
65
|
+
type="text"
|
|
66
|
+
aria-label={p.name}
|
|
67
|
+
value={String(p.value)}
|
|
68
|
+
class="min-w-0 flex-1 rounded border border-border bg-background px-1 py-0.5 text-xs"
|
|
69
|
+
onblur={(e) => {
|
|
70
|
+
if (e.currentTarget.value !== p.value) setProp(p.name, e.currentTarget.value);
|
|
71
|
+
}}
|
|
72
|
+
onkeydown={(e) => {
|
|
73
|
+
if (e.key === 'Enter') e.currentTarget.blur();
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
{/key}
|
|
77
|
+
{:else}
|
|
78
|
+
<select
|
|
79
|
+
aria-label={p.name}
|
|
80
|
+
class="min-w-0 flex-1 rounded border border-border bg-background px-1 py-0.5 text-xs"
|
|
81
|
+
value={String(p.value)}
|
|
82
|
+
onchange={(e) => setProp(p.name, e.currentTarget.value)}
|
|
83
|
+
>
|
|
84
|
+
{#each allComponents.filter((c) => !p.preferred?.length || p.preferred.includes(c.id) || c.id === p.value) as c (c.id)}
|
|
85
|
+
<option value={c.id}>{c.name}</option>
|
|
86
|
+
{/each}
|
|
87
|
+
</select>
|
|
88
|
+
{/if}
|
|
89
|
+
</label>
|
|
90
|
+
{/each}
|
|
91
|
+
{#if info.overrides.length}
|
|
92
|
+
<div class="flex flex-col gap-1" data-instance-overrides="">
|
|
93
|
+
<span class="text-muted-foreground">Overrides</span>
|
|
94
|
+
{#each info.overrides as o (o.id)}
|
|
95
|
+
<div class="flex flex-wrap items-center gap-1" data-override-layer={o.id}>
|
|
96
|
+
<span class="mr-1 truncate">{o.name}</span>
|
|
97
|
+
{#each o.groups as g (g)}
|
|
98
|
+
<button
|
|
99
|
+
type="button"
|
|
100
|
+
class="rounded bg-muted px-1 text-[10px]"
|
|
101
|
+
data-override-group={g}
|
|
102
|
+
title="Reset {g}"
|
|
103
|
+
onclick={() => report(components.resetOverrides(o.id, g))}>{g} ×</button
|
|
104
|
+
>
|
|
105
|
+
{/each}
|
|
106
|
+
</div>
|
|
107
|
+
{/each}
|
|
108
|
+
</div>
|
|
109
|
+
{/if}
|
|
110
|
+
<div class="flex flex-wrap gap-1">
|
|
111
|
+
{#if info.status === 'linked'}
|
|
112
|
+
<button type="button" class={btn} data-action="reset" onclick={() => report(components.resetOverrides(info!.id))}>Reset all</button>
|
|
113
|
+
<button type="button" class={btn} data-action="go-to-main" onclick={() => components.goToMainComponent(info!.id)}>Go to main</button>
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
class={btn}
|
|
117
|
+
data-action="push"
|
|
118
|
+
disabled={info.overrides.length === 0}
|
|
119
|
+
onclick={() => report(components.pushOverrides(info!.rootId))}>Push changes</button
|
|
120
|
+
>
|
|
121
|
+
{:else}
|
|
122
|
+
<button type="button" class={btn} data-action="restore" onclick={() => report(components.restoreMainComponent(info!.rootId))}
|
|
123
|
+
>Restore main component</button
|
|
124
|
+
>
|
|
125
|
+
{/if}
|
|
126
|
+
<button
|
|
127
|
+
type="button"
|
|
128
|
+
class={btn}
|
|
129
|
+
data-action="detach"
|
|
130
|
+
disabled={info.nested}
|
|
131
|
+
title={info.nested ? 'Detach the outer instance first' : undefined}
|
|
132
|
+
onclick={() => report(components.detachInstance([info!.id]))}>Detach</button
|
|
133
|
+
>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
{/if}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type CanvasComponentsController } from '@pixerate/editor/canvas';
|
|
2
|
+
import type { CanvasNode } from './types.js';
|
|
3
|
+
type Actions = Pick<CanvasComponentsController<any>, 'setInstanceProperties' | 'resetOverrides' | 'detachInstance' | 'goToMainComponent' | 'pushOverrides' | 'restoreMainComponent'>;
|
|
4
|
+
interface Props {
|
|
5
|
+
nodes: CanvasNode[];
|
|
6
|
+
/** An instance, or a layer inside one; nothing is shown otherwise. */
|
|
7
|
+
nodeId: string | null | undefined;
|
|
8
|
+
components: Actions;
|
|
9
|
+
/** Receives refusals (e.g. a property value that would nest a component in itself). */
|
|
10
|
+
onerror?: (reason: string) => void;
|
|
11
|
+
class?: string;
|
|
12
|
+
}
|
|
13
|
+
declare const CanvasInstanceInspector: import("svelte").Component<Props, {}, "">;
|
|
14
|
+
type CanvasInstanceInspector = ReturnType<typeof CanvasInstanceInspector>;
|
|
15
|
+
export default CanvasInstanceInspector;
|
|
16
|
+
//# sourceMappingURL=CanvasInstanceInspector.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CanvasInstanceInspector.svelte.d.ts","sourceRoot":"","sources":["../../src/canvas/CanvasInstanceInspector.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+C,KAAK,0BAA0B,EAAE,MAAM,yBAAyB,CAAC;AACvH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAG5C,KAAK,OAAO,GAAG,IAAI,CAClB,0BAA0B,CAAC,GAAG,CAAC,EAC/B,uBAAuB,GAAG,gBAAgB,GAAG,gBAAgB,GAAG,mBAAmB,GAAG,eAAe,GAAG,sBAAsB,CAC9H,CAAC;AAEF,UAAU,KAAK;IACd,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,sEAAsE;IACtE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,UAAU,EAAE,OAAO,CAAC;IACpB,uFAAuF;IACvF,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;CACf;AAyFF,QAAA,MAAM,uBAAuB,2CAAwC,CAAC;AACtE,KAAK,uBAAuB,GAAG,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAC1E,eAAe,uBAAuB,CAAC"}
|