@motion-proto/live-tokens 0.5.0 → 0.6.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.
@@ -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>
@@ -1,188 +0,0 @@
1
- /* Form Controls - Global Styling for Dropdowns, Selects, Inputs */
2
- /* Ensures consistent, legible styling across the application */
3
-
4
- /* ========================================
5
- Form Field Layouts
6
- ======================================== */
7
-
8
- /* Vertical Layout - Label Above Input/Select */
9
- /* Usage: Add .form-field-vertical to container div wrapping label + input/select */
10
- .form-field-vertical {
11
- display: flex;
12
- flex-direction: column;
13
- align-items: stretch;
14
- gap: var(--space-4);
15
- }
16
-
17
- .form-label {
18
- margin-bottom: 0;
19
- font-size: var(--font-size-md);
20
- color: var(--text-primary);
21
- font-weight: var(--font-weight-light);
22
- }
23
-
24
- /* Horizontal Layout - Label Beside Input/Select */
25
- /* Usage: Add .form-field-horizontal to container div wrapping label + input/select */
26
- .form-field-horizontal {
27
- display: flex;
28
- justify-content: space-between;
29
- align-items: center;
30
- gap: var(--space-8);
31
- }
32
-
33
- .form-field-horizontal .form-label {
34
- white-space: nowrap;
35
- }
36
-
37
- /* Inline Layout - Label and Input/Select Close Together */
38
- /* Usage: Add .form-field-inline to container div wrapping label + input/select */
39
- .form-field-inline {
40
- display: flex;
41
- align-items: center;
42
- gap: var(--space-12);
43
- }
44
-
45
- .form-field-inline .form-label {
46
- white-space: nowrap;
47
- flex-shrink: 0;
48
- }
49
-
50
- .form-field-inline .form-select,
51
- .form-field-inline .form-input {
52
- flex-shrink: 0;
53
- }
54
-
55
- /* ========================================
56
- Form Control Styling
57
- ======================================== */
58
-
59
- /* Select/Dropdown Styling - Design System Compliant */
60
- .form-select {
61
- /* Base styling using design system colors */
62
- /* No vertical padding - let min-height and line-height center text naturally */
63
- padding: 0 var(--space-16);
64
- min-height: 2.375rem; /* ~38px to match button height */
65
- background: var(--surface-neutral-lowest) !important;
66
- border: 1px solid var(--border-neutral) !important;
67
- border-radius: var(--radius-md);
68
- color: var(--text-primary) !important;
69
- font-family: var(--font-sans);
70
- font-size: var(--font-size-md);
71
- font-weight: var(--font-weight-light);
72
- line-height: var(--line-height-md);
73
- vertical-align: middle;
74
- cursor: pointer;
75
- transition: all var(--duration-150);
76
- /* Prevent clipping */
77
- overflow: visible !important;
78
- box-sizing: border-box !important;
79
- /* Reset browser defaults */
80
- -webkit-appearance: none;
81
- -moz-appearance: none;
82
- appearance: none;
83
- /* Custom dropdown arrow */
84
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23888' d='M6 8L1 3h10z'/%3E%3C/svg%3E") !important;
85
- background-repeat: no-repeat !important;
86
- background-position: right var(--space-12) center !important;
87
- padding-right: var(--space-32) !important;
88
- }
89
-
90
- .form-select:hover:not(:disabled) {
91
- background-color: var(--surface-neutral-low) !important;
92
- border-color: var(--border-neutral-medium) !important;
93
- }
94
-
95
- .form-select:focus {
96
- outline: none;
97
- border-color: var(--border-neutral-strong) !important;
98
- box-shadow: 0 0 0 2px hsla(0, 58%, 50%, 0.2);
99
- }
100
-
101
- .form-select:focus-visible {
102
- outline: 2px solid var(--color-brand-500);
103
- outline-offset: 2px;
104
- }
105
-
106
- .form-select:active:not(:disabled) {
107
- background-color: var(--surface-neutral) !important;
108
- }
109
-
110
- .form-select:disabled {
111
- background-color: var(--surface-neutral-lowest) !important;
112
- border-color: var(--border-neutral-faint) !important;
113
- color: var(--text-disabled) !important;
114
- cursor: not-allowed;
115
- }
116
-
117
- /* Option Styling - Critical for Legibility */
118
- /* Note: Most option styling is controlled by browser/OS and cannot be fully overridden */
119
- /* These styles apply where browsers allow (limited support in Chrome/Firefox) */
120
- .form-select option {
121
- background-color: var(--surface-neutral-lowest) !important;
122
- color: var(--text-primary) !important;
123
- padding: var(--space-8) var(--space-12);
124
- min-height: 2rem;
125
- font-size: var(--font-size-md);
126
- font-family: var(--font-sans);
127
- line-height: var(--line-height-md);
128
- }
129
-
130
- /* Disabled options */
131
- .form-select option:disabled {
132
- color: var(--text-disabled) !important;
133
- }
134
-
135
- /* Input Field Styling */
136
- .form-input {
137
- padding: var(--space-8);
138
- background: var(--surface-neutral-lowest);
139
- border: 1px solid var(--border-neutral);
140
- border-radius: var(--radius-md);
141
- color: var(--text-primary);
142
- font-family: var(--font-sans);
143
- font-size: var(--font-size-md);
144
- transition: border-color var(--duration-150);
145
- }
146
-
147
- .form-input:hover:not(:disabled) {
148
- border-color: var(--border-neutral-strong);
149
- background: var(--surface-neutral-low);
150
- }
151
-
152
- .form-input:focus {
153
- outline: none;
154
- border-color: var(--border-neutral-strong);
155
- box-shadow: 0 0 0 3px hsla(240, 5%, 38%, 0.2);
156
- }
157
-
158
- .form-input:disabled {
159
- background: var(--surface-neutral-lowest);
160
- border-color: var(--border-neutral-faint);
161
- color: var(--text-disabled);
162
- cursor: not-allowed;
163
- }
164
-
165
- /* Placeholder text styling */
166
- .form-input::placeholder {
167
- color: var(--text-muted);
168
- }
169
-
170
- /* Number input spinner buttons */
171
- .form-input[type="number"]::-webkit-inner-spin-button,
172
- .form-input[type="number"]::-webkit-outer-spin-button {
173
- opacity: 1;
174
- }
175
-
176
- /* Checkbox and Radio Styling */
177
- .form-checkbox,
178
- .form-radio {
179
- cursor: pointer;
180
- width: var(--space-16);
181
- height: var(--space-16);
182
- accent-color: var(--color-neutral-400);
183
- }
184
-
185
- .form-checkbox:disabled,
186
- .form-radio:disabled {
187
- cursor: not-allowed;
188
- }