@motion-proto/live-tokens 0.6.0 → 0.6.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.
@@ -0,0 +1,315 @@
1
+ <script lang="ts">
2
+ import { selectedComponent } from '../lib/editorViewStore';
3
+ import { componentRegistryEntries } from '../component-editor/registry';
4
+ import { saveActiveComponentConfig } from '../lib/componentPersist';
5
+ import UIDialog from './UIDialog.svelte';
6
+
7
+ interface Props {
8
+ show?: boolean;
9
+ /** Component IDs that have unsaved edits. */
10
+ dirtyComponents?: string[];
11
+ /** Called when the user chooses to capture the preset anyway (after handling, or skipping, unsaved components). */
12
+ onproceed?: () => void;
13
+ }
14
+
15
+ let { show = $bindable(false), dirtyComponents = [], onproceed }: Props = $props();
16
+
17
+ // Map id → registry entry (label, icon). Components not in the registry get
18
+ // a fallback so the dialog never renders an empty row. Typed as a plain
19
+ // string-keyed record so a stray dirty id (one we don't recognise) doesn't
20
+ // trip the typed `ComponentId` key.
21
+ const entryById: Record<string, { id: string; label: string; icon: string }> =
22
+ Object.fromEntries(
23
+ componentRegistryEntries.map((e) => [e.id, { id: e.id, label: e.label, icon: e.icon }]),
24
+ );
25
+
26
+ type RowStatus = 'idle' | 'saving' | 'saved' | 'default' | 'error';
27
+ let rowStatus: Record<string, RowStatus> = $state({});
28
+ let savingAll = $state(false);
29
+ /** Snapshot the dirty list at open-time so rows don't vanish as the user
30
+ * saves them one-by-one (componentDirty updates instantly on save). */
31
+ let rowIds: string[] = $state([]);
32
+
33
+ function entryFor(id: string): { id: string; label: string; icon: string } {
34
+ return entryById[id] ?? { id, label: id, icon: 'fas fa-cube' };
35
+ }
36
+
37
+ async function saveOne(id: string): Promise<RowStatus> {
38
+ rowStatus[id] = 'saving';
39
+ const result = await saveActiveComponentConfig(id);
40
+ if (result.ok) {
41
+ rowStatus[id] = 'saved';
42
+ return 'saved';
43
+ }
44
+ rowStatus[id] = result.reason === 'default' ? 'default' : 'error';
45
+ return rowStatus[id];
46
+ }
47
+
48
+ async function handleSaveAll() {
49
+ savingAll = true;
50
+ for (const id of rowIds) {
51
+ // Skip rows the user already resolved this session.
52
+ if (rowStatus[id] === 'saved') continue;
53
+ await saveOne(id);
54
+ }
55
+ savingAll = false;
56
+ }
57
+
58
+ function jumpTo(id: string) {
59
+ selectedComponent.set(id);
60
+ show = false;
61
+ }
62
+
63
+ function handleProceed() {
64
+ show = false;
65
+ onproceed?.();
66
+ }
67
+
68
+ // Snapshot the dirty list and reset row status whenever the dialog re-opens.
69
+ // Snapshotting keeps rows stable while the user saves them one-by-one (the
70
+ // underlying componentDirty store drops ids as soon as save completes).
71
+ $effect(() => {
72
+ if (show) {
73
+ rowIds = [...dirtyComponents];
74
+ rowStatus = {};
75
+ }
76
+ });
77
+
78
+ let allSaved = $derived(
79
+ rowIds.length > 0 && rowIds.every((id) => rowStatus[id] === 'saved'),
80
+ );
81
+ </script>
82
+
83
+ <UIDialog
84
+ bind:show
85
+ title="Unsaved component changes"
86
+ cancelLabel="Cancel"
87
+ width="440px"
88
+ >
89
+ <div class="ucd-body">
90
+ <p class="ucd-lede">
91
+ {rowIds.length}
92
+ {rowIds.length === 1 ? 'component has' : 'components have'}
93
+ unsaved edits. Presets capture the files on disk, so these edits won't be
94
+ included until they're saved.
95
+ </p>
96
+
97
+ <ul class="ucd-list">
98
+ {#each rowIds as id}
99
+ {@const entry = entryFor(id)}
100
+ {@const status = rowStatus[id] ?? 'idle'}
101
+ <li class="ucd-row" class:saved={status === 'saved'}>
102
+ <span class="ucd-dot" class:saved={status === 'saved'} aria-hidden="true"></span>
103
+ <button
104
+ type="button"
105
+ class="ucd-name"
106
+ onclick={() => jumpTo(id)}
107
+ title="Open {entry.label} editor"
108
+ >
109
+ <i class={entry.icon}></i>
110
+ <span>{entry.label}</span>
111
+ </button>
112
+ <span class="ucd-status" class:saved={status === 'saved'} class:default={status === 'default'} class:error={status === 'error'}>
113
+ {#if status === 'saving'}Saving…
114
+ {:else if status === 'saved'}Saved
115
+ {:else if status === 'default'}On default — rename first
116
+ {:else if status === 'error'}Error
117
+ {/if}
118
+ </span>
119
+ <button
120
+ type="button"
121
+ class="ucd-row-btn"
122
+ onclick={() => saveOne(id)}
123
+ disabled={status === 'saving' || status === 'saved' || savingAll}
124
+ title="Save this component"
125
+ >
126
+ <i class="fas fa-save"></i>
127
+ </button>
128
+ </li>
129
+ {/each}
130
+ </ul>
131
+
132
+ <div class="ucd-actions">
133
+ <button
134
+ type="button"
135
+ class="ucd-btn primary"
136
+ onclick={handleSaveAll}
137
+ disabled={savingAll || allSaved}
138
+ >
139
+ {savingAll ? 'Saving…' : allSaved ? 'All saved' : 'Save all'}
140
+ </button>
141
+ <button
142
+ type="button"
143
+ class="ucd-btn"
144
+ onclick={handleProceed}
145
+ title="Save the preset using the files currently on disk"
146
+ >
147
+ Continue without saving
148
+ </button>
149
+ </div>
150
+ </div>
151
+ </UIDialog>
152
+
153
+ <style>
154
+ .ucd-body {
155
+ display: flex;
156
+ flex-direction: column;
157
+ gap: var(--ui-space-12);
158
+ }
159
+
160
+ .ucd-lede {
161
+ margin: 0;
162
+ font-size: var(--ui-font-size-sm);
163
+ line-height: 1.5;
164
+ color: var(--ui-text-secondary);
165
+ }
166
+
167
+ .ucd-list {
168
+ list-style: none;
169
+ margin: 0;
170
+ padding: 0;
171
+ display: flex;
172
+ flex-direction: column;
173
+ gap: var(--ui-space-2);
174
+ max-height: 240px;
175
+ overflow-y: auto;
176
+ border: 1px solid var(--ui-border-faint);
177
+ border-radius: var(--ui-radius-md);
178
+ }
179
+
180
+ .ucd-row {
181
+ display: grid;
182
+ grid-template-columns: 14px 1fr auto auto;
183
+ align-items: center;
184
+ gap: var(--ui-space-8);
185
+ padding: var(--ui-space-6) var(--ui-space-10);
186
+ border-bottom: 1px solid var(--ui-border-faint);
187
+ }
188
+
189
+ .ucd-row:last-child {
190
+ border-bottom: none;
191
+ }
192
+
193
+ .ucd-dot {
194
+ width: 8px;
195
+ height: 8px;
196
+ border-radius: 50%;
197
+ background: var(--ui-highlight);
198
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--ui-highlight) 22%, transparent);
199
+ justify-self: center;
200
+ }
201
+
202
+ .ucd-dot.saved {
203
+ background: var(--ui-text-success, #5aa85e);
204
+ box-shadow: none;
205
+ }
206
+
207
+ .ucd-name {
208
+ display: inline-flex;
209
+ align-items: center;
210
+ gap: var(--ui-space-6);
211
+ padding: var(--ui-space-2) var(--ui-space-4);
212
+ background: none;
213
+ border: none;
214
+ color: var(--ui-text-primary);
215
+ font-size: var(--ui-font-size-md);
216
+ text-align: left;
217
+ cursor: pointer;
218
+ border-radius: var(--ui-radius-sm);
219
+ min-width: 0;
220
+ }
221
+
222
+ .ucd-name i {
223
+ width: 1rem;
224
+ text-align: center;
225
+ color: var(--ui-text-tertiary);
226
+ }
227
+
228
+ .ucd-name:hover {
229
+ background: var(--ui-hover);
230
+ }
231
+
232
+ .ucd-status {
233
+ font-size: var(--ui-font-size-xs);
234
+ color: var(--ui-text-tertiary);
235
+ white-space: nowrap;
236
+ }
237
+
238
+ .ucd-status.saved {
239
+ color: var(--ui-text-success, #5aa85e);
240
+ }
241
+
242
+ .ucd-status.default {
243
+ color: var(--ui-highlight);
244
+ }
245
+
246
+ .ucd-status.error {
247
+ color: var(--ui-text-muted);
248
+ }
249
+
250
+ .ucd-row-btn {
251
+ display: inline-flex;
252
+ align-items: center;
253
+ justify-content: center;
254
+ width: 28px;
255
+ height: 28px;
256
+ padding: 0;
257
+ background: var(--ui-surface-low);
258
+ border: 1px solid var(--ui-border-subtle);
259
+ border-radius: var(--ui-radius-sm);
260
+ color: var(--ui-text-secondary);
261
+ cursor: pointer;
262
+ transition: all var(--ui-transition-fast);
263
+ }
264
+
265
+ .ucd-row-btn:hover:not(:disabled) {
266
+ background: var(--ui-surface);
267
+ color: var(--ui-text-primary);
268
+ border-color: var(--ui-border-default);
269
+ }
270
+
271
+ .ucd-row-btn:disabled {
272
+ opacity: 0.4;
273
+ cursor: not-allowed;
274
+ }
275
+
276
+ .ucd-actions {
277
+ display: flex;
278
+ flex-wrap: wrap;
279
+ justify-content: flex-end;
280
+ gap: var(--ui-space-8);
281
+ }
282
+
283
+ .ucd-btn {
284
+ padding: var(--ui-space-6) var(--ui-space-12);
285
+ background: var(--ui-surface-low);
286
+ border: 1px solid var(--ui-border-subtle);
287
+ border-radius: var(--ui-radius-md);
288
+ color: var(--ui-text-secondary);
289
+ font-size: var(--ui-font-size-md);
290
+ cursor: pointer;
291
+ transition: all var(--ui-transition-fast);
292
+ }
293
+
294
+ .ucd-btn:hover:not(:disabled) {
295
+ background: var(--ui-surface);
296
+ color: var(--ui-text-primary);
297
+ border-color: var(--ui-border-default);
298
+ }
299
+
300
+ .ucd-btn:disabled {
301
+ opacity: 0.45;
302
+ cursor: not-allowed;
303
+ }
304
+
305
+ .ucd-btn.primary {
306
+ background: var(--ui-surface-high);
307
+ border-color: var(--ui-border-medium);
308
+ color: var(--ui-text-primary);
309
+ }
310
+
311
+ .ucd-btn.primary:hover:not(:disabled) {
312
+ background: var(--ui-surface-higher);
313
+ border-color: var(--ui-border-strong);
314
+ }
315
+ </style>