@poirazis/supercomponents-shared 1.2.14 → 1.2.16
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/index.js +11177 -11403
- package/dist/index.umd.cjs +15 -14
- package/package.json +8 -8
- package/src/lib/SuperButton/SuperButton.svelte +16 -21
- package/src/lib/SuperField/SuperField.svelte +13 -14
- package/src/lib/SuperForm/InnerForm.svelte +9 -2
- package/src/lib/SuperForm/SuperForm.svelte +5 -5
- package/src/lib/SuperPopover/SuperPopover.svelte +1 -1
- package/src/lib/SuperTable/SuperTable.svelte +4 -3
- package/src/lib/SuperTable/controls/RowButtonsColumn.svelte +1 -1
- package/src/lib/SuperTable/controls/SelectionColumn.svelte +1 -1
- package/src/lib/SuperTableCells/CellCommon.css +97 -35
- package/src/lib/SuperTableCells/CellDatetime.svelte +2 -3
- package/src/lib/SuperTableCells/CellLink.svelte +8 -8
- package/src/lib/SuperTableCells/CellOptions.svelte +321 -237
- package/src/lib/SuperTableCells/CellOptionsAdvanced.svelte +209 -186
- package/src/lib/SuperTableCells/CellSkeleton.svelte +1 -1
- package/src/lib/SuperTableCells/CellString.svelte +19 -39
- package/src/lib/SuperTableCells/CellStringSimple.svelte +1 -4
- package/src/lib/SuperTableColumn/SuperTableColumn.svelte +2 -2
- package/src/lib/SuperTableColumn/parts/SuperColumnRow.svelte +1 -1
- package/src/lib/SuperTableCells/JSDOC_GUIDE.md +0 -869
|
@@ -1,869 +0,0 @@
|
|
|
1
|
-
# Using JSDoc for Type Safety in SuperTableCells
|
|
2
|
-
|
|
3
|
-
This guide demonstrates how to use JSDoc annotations for **type definitions only** (CellOptions, CellApi) - not for annotating every variable and function.
|
|
4
|
-
|
|
5
|
-
## Benefits of JSDoc
|
|
6
|
-
|
|
7
|
-
1. **No TypeScript compilation** - Pure JavaScript
|
|
8
|
-
2. **IntelliSense for important types** - Autocomplete for cellOptions and cellApi
|
|
9
|
-
3. **Simple and clean** - Only annotate what matters
|
|
10
|
-
4. **Better debugging** - Source code matches what runs
|
|
11
|
-
|
|
12
|
-
## What to Annotate
|
|
13
|
-
|
|
14
|
-
✅ **DO annotate:**
|
|
15
|
-
|
|
16
|
-
- `cellOptions` prop - so you get autocomplete for options
|
|
17
|
-
- `cellApi` export - so consumers know what methods are available
|
|
18
|
-
- Component props (`value`, `formattedValue`, etc.)
|
|
19
|
-
|
|
20
|
-
❌ **DON'T annotate:**
|
|
21
|
-
|
|
22
|
-
- Local variables (let timer, let editor, etc.)
|
|
23
|
-
- Function parameters in FSM methods
|
|
24
|
-
- Helper functions
|
|
25
|
-
- Event handlers
|
|
26
|
-
|
|
27
|
-
## Type Definitions
|
|
28
|
-
|
|
29
|
-
All types are defined in `types.js` using a **unified CellOptions type**:
|
|
30
|
-
|
|
31
|
-
- `CellOptions` - Single type with common base properties + optional type-specific extensions
|
|
32
|
-
- `CellApi` - Standard API interface exported by all cells
|
|
33
|
-
- `CellRole`, `CellState`, `CellAlign` - Enum types
|
|
34
|
-
- `FieldSchema` - Budibase field schema type
|
|
35
|
-
|
|
36
|
-
### CellOptions Structure
|
|
37
|
-
|
|
38
|
-
The unified `CellOptions` type contains:
|
|
39
|
-
|
|
40
|
-
- **Common properties** (role, readonly, disabled, placeholder, etc.) - used by all cells
|
|
41
|
-
- **Type-specific properties** organized by category (Number, Boolean, Options, Datetime, etc.)
|
|
42
|
-
|
|
43
|
-
All properties are optional except where noted. Components only use the properties relevant to them.
|
|
44
|
-
|
|
45
|
-
## Cell FSM State Machine
|
|
46
|
-
|
|
47
|
-
All cell components use a finite state machine (FSM) managed by `svelte-fsm` to handle user interactions and data loading states. The FSM provides a clean, declarative way to manage component state transitions.
|
|
48
|
-
|
|
49
|
-
### Core States
|
|
50
|
-
|
|
51
|
-
The cell FSM supports three core reachable states:
|
|
52
|
-
|
|
53
|
-
| State | Usage | Transitions |
|
|
54
|
-
| ----------- | --------------------------------------- | --------------------------------------- |
|
|
55
|
-
| **View** | Default display state, read-only | → Editing (on focus) |
|
|
56
|
-
| **Editing** | User is actively editing the cell value | → View (on focusout, submit, or cancel) |
|
|
57
|
-
| **Loading** | Data is being fetched or processed | → View (when data loads) |
|
|
58
|
-
|
|
59
|
-
### State Diagram
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
┌──────────────────────────────────┐
|
|
63
|
-
│ │
|
|
64
|
-
│ View (Display Mode) │
|
|
65
|
-
│ - Shows formatted value │
|
|
66
|
-
│ - Read-only or ready to edit │
|
|
67
|
-
│ │
|
|
68
|
-
└──────────────────────────────────┘
|
|
69
|
-
↑
|
|
70
|
-
│ focusout()
|
|
71
|
-
│ submit()
|
|
72
|
-
│ cancel()
|
|
73
|
-
│
|
|
74
|
-
↓
|
|
75
|
-
┌──────────────────────────────────┐
|
|
76
|
-
│ │
|
|
77
|
-
│ Editing (Edit Mode) │
|
|
78
|
-
│ - Input field active │
|
|
79
|
-
│ - User can modify value │
|
|
80
|
-
│ - Dispatch change events │
|
|
81
|
-
│ │
|
|
82
|
-
└──────────────────────────────────┘
|
|
83
|
-
↑
|
|
84
|
-
│ focus()
|
|
85
|
-
│ (when not readonly)
|
|
86
|
-
│
|
|
87
|
-
┌──────────────────────────────────┐
|
|
88
|
-
│ │
|
|
89
|
-
│ Loading (Async Mode) │
|
|
90
|
-
│ - Fetching data from server │
|
|
91
|
-
│ - Data-dependent cells only │
|
|
92
|
-
│ - Auto-transitions to View │
|
|
93
|
-
│ │
|
|
94
|
-
└──────────────────────────────────┘
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
### Global Transitions (Available in All States)
|
|
98
|
-
|
|
99
|
-
All states support these global methods via the `"*"` state:
|
|
100
|
-
|
|
101
|
-
```javascript
|
|
102
|
-
export const cellState = fsm("View", {
|
|
103
|
-
"*": {
|
|
104
|
-
goTo(state) {
|
|
105
|
-
// Manual state transition (for external control)
|
|
106
|
-
return state;
|
|
107
|
-
},
|
|
108
|
-
reset(value) {
|
|
109
|
-
// Reset to initial value and View state
|
|
110
|
-
localValue = undefined;
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
// ... state-specific transitions
|
|
114
|
-
});
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
### Lifecycle Hooks
|
|
118
|
-
|
|
119
|
-
State transitions can trigger lifecycle methods:
|
|
120
|
-
|
|
121
|
-
- **`_enter()`** - Called when entering a state
|
|
122
|
-
- **`_exit()`** - Called when exiting a state
|
|
123
|
-
|
|
124
|
-
Example from CellString:
|
|
125
|
-
|
|
126
|
-
```javascript
|
|
127
|
-
Editing: {
|
|
128
|
-
_enter() {
|
|
129
|
-
originalValue = JSON.stringify(localValue); // Save for rollback
|
|
130
|
-
dispatch("enteredit"); // Notify parent
|
|
131
|
-
},
|
|
132
|
-
_exit() {
|
|
133
|
-
dispatch("exitedit"); // Cleanup
|
|
134
|
-
},
|
|
135
|
-
focusout() {
|
|
136
|
-
this.submit(); // Submit on blur
|
|
137
|
-
},
|
|
138
|
-
submit() {
|
|
139
|
-
dispatch("change", localValue);
|
|
140
|
-
return "View"; // Transition back to View
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### Usage Pattern
|
|
146
|
-
|
|
147
|
-
```javascript
|
|
148
|
-
// Declare FSM with initial state
|
|
149
|
-
export const cellState = fsm(cellOptions.initialState ?? "View", {
|
|
150
|
-
View: {
|
|
151
|
-
/* ... */
|
|
152
|
-
},
|
|
153
|
-
Editing: {
|
|
154
|
-
/* ... */
|
|
155
|
-
},
|
|
156
|
-
Loading: {
|
|
157
|
-
/* ... */
|
|
158
|
-
}, // Optional, for data-dependent cells
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
// Subscribe to state changes in component
|
|
162
|
-
$: inEdit = $cellState === "Editing";
|
|
163
|
-
$: isDirty = inEdit && originalValue !== JSON.stringify(localValue);
|
|
164
|
-
|
|
165
|
-
// Trigger transitions from event handlers
|
|
166
|
-
const handleFocus = () => cellState.focus();
|
|
167
|
-
const handleBlur = () => cellState.focusout();
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
### Data-Dependent Cells
|
|
171
|
-
|
|
172
|
-
Cells that fetch data (CellOptions, CellTags, CellOptionsAdvanced) use the Loading state:
|
|
173
|
-
|
|
174
|
-
```javascript
|
|
175
|
-
Loading: {
|
|
176
|
-
_enter() {
|
|
177
|
-
// Fetch data from server
|
|
178
|
-
loadOptions();
|
|
179
|
-
},
|
|
180
|
-
syncFetch(fetch) {
|
|
181
|
-
// Handle synchronous fetch results
|
|
182
|
-
return "View"; // Auto-transition when done
|
|
183
|
-
},
|
|
184
|
-
focus() {
|
|
185
|
-
// Allow editing while loading
|
|
186
|
-
return "Editing";
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
## JavaScript Example: CellString.svelte (Reference Implementation)
|
|
192
|
-
|
|
193
|
-
```svelte
|
|
194
|
-
<script>
|
|
195
|
-
import { createEventDispatcher, getContext, onMount, onDestroy } from "svelte";
|
|
196
|
-
import fsm from "svelte-fsm";
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* @typedef {import('./types.js').CellOptions} CellOptions
|
|
200
|
-
* @typedef {import('./types.js').CellApi} CellApi
|
|
201
|
-
*/
|
|
202
|
-
|
|
203
|
-
const dispatch = createEventDispatcher();
|
|
204
|
-
const { processStringSync } = getContext("sdk");
|
|
205
|
-
|
|
206
|
-
/** @type {string | null} */
|
|
207
|
-
export let value;
|
|
208
|
-
|
|
209
|
-
/** @type {string | undefined} */
|
|
210
|
-
export let formattedValue = undefined;
|
|
211
|
-
|
|
212
|
-
/** @type {CellOptions} */
|
|
213
|
-
export let cellOptions = {
|
|
214
|
-
role: "formInput",
|
|
215
|
-
initialState: "Editing",
|
|
216
|
-
debounce: 250,
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
export let autofocus = false;
|
|
220
|
-
|
|
221
|
-
// Local state - no annotations needed
|
|
222
|
-
let timer;
|
|
223
|
-
let originalValue;
|
|
224
|
-
let editor;
|
|
225
|
-
let lastEdit;
|
|
226
|
-
let localValue = value;
|
|
227
|
-
let state = (cellOptions?.initialState === "Loading" ? "View" : cellOptions?.initialState) ?? "View";
|
|
228
|
-
let errors = [];
|
|
229
|
-
|
|
230
|
-
// Reactive declarations
|
|
231
|
-
$: error = cellOptions?.error || errors.length > 0;
|
|
232
|
-
$: inEdit = $cellState === "Editing";
|
|
233
|
-
$: isDirty = !!lastEdit && originalValue !== localValue;
|
|
234
|
-
|
|
235
|
-
// FSM - no annotations needed for methods
|
|
236
|
-
export const cellState = fsm(state ?? "View", {
|
|
237
|
-
"*": {
|
|
238
|
-
goTo(state) {
|
|
239
|
-
return state;
|
|
240
|
-
},
|
|
241
|
-
reset(newValue) {
|
|
242
|
-
if (newValue === localValue) return;
|
|
243
|
-
localValue = value;
|
|
244
|
-
lastEdit = undefined;
|
|
245
|
-
originalValue = undefined;
|
|
246
|
-
errors = [];
|
|
247
|
-
return state;
|
|
248
|
-
},
|
|
249
|
-
},
|
|
250
|
-
View: {
|
|
251
|
-
_enter() {
|
|
252
|
-
localValue = value;
|
|
253
|
-
},
|
|
254
|
-
focus() {
|
|
255
|
-
if (!cellOptions.readonly && !cellOptions.disabled) {
|
|
256
|
-
return "Editing";
|
|
257
|
-
}
|
|
258
|
-
},
|
|
259
|
-
},
|
|
260
|
-
Editing: {
|
|
261
|
-
_enter() {
|
|
262
|
-
originalValue = value;
|
|
263
|
-
setTimeout(() => editor?.focus(), 50);
|
|
264
|
-
dispatch("enteredit");
|
|
265
|
-
},
|
|
266
|
-
_exit() {
|
|
267
|
-
originalValue = undefined;
|
|
268
|
-
lastEdit = undefined;
|
|
269
|
-
dispatch("exitedit");
|
|
270
|
-
},
|
|
271
|
-
focusout(e) {
|
|
272
|
-
dispatch("focusout");
|
|
273
|
-
this.submit();
|
|
274
|
-
},
|
|
275
|
-
submit() {
|
|
276
|
-
if (isDirty) {
|
|
277
|
-
dispatch("change", localValue);
|
|
278
|
-
}
|
|
279
|
-
return state;
|
|
280
|
-
},
|
|
281
|
-
cancel() {
|
|
282
|
-
value = originalValue ?? null;
|
|
283
|
-
dispatch("cancel");
|
|
284
|
-
return state;
|
|
285
|
-
},
|
|
286
|
-
debounce(e) {
|
|
287
|
-
const target = e.target;
|
|
288
|
-
localValue = target.value;
|
|
289
|
-
lastEdit = new Date();
|
|
290
|
-
if (cellOptions?.debounce) {
|
|
291
|
-
clearTimeout(timer);
|
|
292
|
-
timer = setTimeout(() => {
|
|
293
|
-
dispatch("change", localValue);
|
|
294
|
-
}, cellOptions.debounce ?? 0);
|
|
295
|
-
}
|
|
296
|
-
},
|
|
297
|
-
handleKeyboard(e) {
|
|
298
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
299
|
-
this.submit();
|
|
300
|
-
}
|
|
301
|
-
if (e.key === "Escape") {
|
|
302
|
-
this.cancel();
|
|
303
|
-
}
|
|
304
|
-
},
|
|
305
|
-
},
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
// Annotate the public API
|
|
309
|
-
/** @type {CellApi} */
|
|
310
|
-
export const cellApi = {
|
|
311
|
-
focus: () => cellState.focus(),
|
|
312
|
-
reset: () => cellState.reset(value),
|
|
313
|
-
isEditing: () => $cellState === "Editing",
|
|
314
|
-
isDirty: () => isDirty,
|
|
315
|
-
getValue: () => localValue,
|
|
316
|
-
setError: (err) => {
|
|
317
|
-
errors = [...errors, err];
|
|
318
|
-
},
|
|
319
|
-
clearError: () => {
|
|
320
|
-
errors = [];
|
|
321
|
-
},
|
|
322
|
-
setValue: (val) => {
|
|
323
|
-
localValue = val;
|
|
324
|
-
},
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
// No annotations needed for helpers
|
|
328
|
-
const focus = (node) => {
|
|
329
|
-
node?.focus();
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
onMount(() => {
|
|
333
|
-
if (autofocus) {
|
|
334
|
-
setTimeout(() => cellState.focus(), 50);
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
onDestroy(() => {
|
|
339
|
-
if (timer) clearTimeout(timer);
|
|
340
|
-
});
|
|
341
|
-
</script>
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
```svelte
|
|
345
|
-
<script>
|
|
346
|
-
// @ts-check - Enable TypeScript checking in JavaScript
|
|
347
|
-
import { createEventDispatcher, getContext, onMount, onDestroy } from "svelte";
|
|
348
|
-
import fsm from "svelte-fsm";
|
|
349
|
-
|
|
350
|
-
// Import types for IntelliSense (no runtime impact)
|
|
351
|
-
/**
|
|
352
|
-
* @typedef {import('./types.js').CellStringOptions} CellStringOptions
|
|
353
|
-
* @typedef {import('./types.js').CellApi} CellApi
|
|
354
|
-
*/
|
|
355
|
-
|
|
356
|
-
const { processStringSync } = getContext("sdk");
|
|
357
|
-
const dispatch = createEventDispatcher();
|
|
358
|
-
|
|
359
|
-
// Typed props with JSDoc
|
|
360
|
-
/** @type {string | null} */
|
|
361
|
-
export let value;
|
|
362
|
-
|
|
363
|
-
/** @type {string | undefined} */
|
|
364
|
-
export let formattedValue = undefined;
|
|
365
|
-
|
|
366
|
-
/** @type {CellStringOptions} */
|
|
367
|
-
export let cellOptions = {
|
|
368
|
-
role: "formInput",
|
|
369
|
-
initialState: "Editing",
|
|
370
|
-
debounce: 250,
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
/** @type {boolean} */
|
|
374
|
-
export let autofocus = false;
|
|
375
|
-
|
|
376
|
-
// Typed local variables
|
|
377
|
-
/** @type {ReturnType<typeof setTimeout> | undefined} */
|
|
378
|
-
let timer;
|
|
379
|
-
|
|
380
|
-
/** @type {string | null | undefined} */
|
|
381
|
-
let originalValue;
|
|
382
|
-
|
|
383
|
-
/** @type {HTMLInputElement | HTMLTextAreaElement | undefined} */
|
|
384
|
-
let editor;
|
|
385
|
-
|
|
386
|
-
/** @type {Date | undefined} */
|
|
387
|
-
let lastEdit;
|
|
388
|
-
|
|
389
|
-
/** @type {string | null} */
|
|
390
|
-
let localValue = value;
|
|
391
|
-
|
|
392
|
-
/** @type {"View" | "Editing"} */
|
|
393
|
-
let state = (cellOptions?.initialState === "Loading" ? "View" : cellOptions?.initialState) ?? "View";
|
|
394
|
-
|
|
395
|
-
/** @type {string[]} */
|
|
396
|
-
let errors = [];
|
|
397
|
-
|
|
398
|
-
// Reactive declarations
|
|
399
|
-
$: error = cellOptions?.error || errors.length > 0;
|
|
400
|
-
$: icon = error ? "ph ph-warning" : cellOptions?.icon;
|
|
401
|
-
$: inEdit = $cellState === "Editing";
|
|
402
|
-
$: isDirty = !!lastEdit && originalValue !== localValue;
|
|
403
|
-
|
|
404
|
-
// FSM with typed transitions
|
|
405
|
-
export const cellState = fsm(state ?? "View", {
|
|
406
|
-
"*": {
|
|
407
|
-
/** @param {string} state */
|
|
408
|
-
goTo(state) {
|
|
409
|
-
return state;
|
|
410
|
-
},
|
|
411
|
-
/** @param {string | null} newValue */
|
|
412
|
-
reset(newValue) {
|
|
413
|
-
if (newValue === localValue) return;
|
|
414
|
-
localValue = value;
|
|
415
|
-
lastEdit = undefined;
|
|
416
|
-
originalValue = undefined;
|
|
417
|
-
errors = [];
|
|
418
|
-
return state;
|
|
419
|
-
},
|
|
420
|
-
},
|
|
421
|
-
View: {
|
|
422
|
-
_enter() {
|
|
423
|
-
localValue = value;
|
|
424
|
-
},
|
|
425
|
-
focus() {
|
|
426
|
-
if (!cellOptions.readonly && !cellOptions.disabled) {
|
|
427
|
-
return "Editing";
|
|
428
|
-
}
|
|
429
|
-
},
|
|
430
|
-
},
|
|
431
|
-
Editing: {
|
|
432
|
-
_enter() {
|
|
433
|
-
originalValue = value;
|
|
434
|
-
setTimeout(() => editor?.focus(), 50);
|
|
435
|
-
dispatch("enteredit");
|
|
436
|
-
},
|
|
437
|
-
_exit() {
|
|
438
|
-
originalValue = undefined;
|
|
439
|
-
lastEdit = undefined;
|
|
440
|
-
dispatch("exitedit");
|
|
441
|
-
},
|
|
442
|
-
/** @param {FocusEvent} e */
|
|
443
|
-
focusout(e) {
|
|
444
|
-
dispatch("focusout");
|
|
445
|
-
this.submit();
|
|
446
|
-
},
|
|
447
|
-
submit() {
|
|
448
|
-
if (isDirty) {
|
|
449
|
-
dispatch("change", localValue);
|
|
450
|
-
}
|
|
451
|
-
return state;
|
|
452
|
-
},
|
|
453
|
-
cancel() {
|
|
454
|
-
value = originalValue ?? null;
|
|
455
|
-
dispatch("cancel");
|
|
456
|
-
return state;
|
|
457
|
-
},
|
|
458
|
-
/** @param {Event} e */
|
|
459
|
-
debounce(e) {
|
|
460
|
-
const target = /** @type {HTMLInputElement | HTMLTextAreaElement} */ (e.target);
|
|
461
|
-
localValue = target.value;
|
|
462
|
-
lastEdit = new Date();
|
|
463
|
-
if (cellOptions?.debounce) {
|
|
464
|
-
clearTimeout(timer);
|
|
465
|
-
timer = setTimeout(() => {
|
|
466
|
-
dispatch("change", localValue);
|
|
467
|
-
}, cellOptions.debounce ?? 0);
|
|
468
|
-
}
|
|
469
|
-
},
|
|
470
|
-
/** @param {KeyboardEvent} e */
|
|
471
|
-
handleKeyboard(e) {
|
|
472
|
-
if (e.key === "Enter" && !e.shiftKey) {
|
|
473
|
-
this.submit();
|
|
474
|
-
}
|
|
475
|
-
if (e.key === "Escape") {
|
|
476
|
-
this.cancel();
|
|
477
|
-
}
|
|
478
|
-
},
|
|
479
|
-
},
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
// Typed API object
|
|
483
|
-
/** @type {CellApi} */
|
|
484
|
-
export const cellApi = {
|
|
485
|
-
focus: () => cellState.focus(),
|
|
486
|
-
reset: () => cellState.reset(value),
|
|
487
|
-
isEditing: () => $cellState === "Editing",
|
|
488
|
-
isDirty: () => isDirty,
|
|
489
|
-
getValue: () => localValue,
|
|
490
|
-
/** @param {string} err */
|
|
491
|
-
setError: (err) => {
|
|
492
|
-
errors = [...errors, err];
|
|
493
|
-
},
|
|
494
|
-
clearError: () => {
|
|
495
|
-
errors = [];
|
|
496
|
-
},
|
|
497
|
-
/** @param {string | null} val */
|
|
498
|
-
setValue: (val) => {
|
|
499
|
-
localValue = val;
|
|
500
|
-
},
|
|
501
|
-
};
|
|
502
|
-
|
|
503
|
-
// Lifecycle
|
|
504
|
-
onMount(() => {
|
|
505
|
-
if (autofocus) {
|
|
506
|
-
setTimeout(() => cellState.focus(), 50);
|
|
507
|
-
}
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
onDestroy(() => {
|
|
511
|
-
if (timer) clearTimeout(timer);
|
|
512
|
-
});
|
|
513
|
-
</script>
|
|
514
|
-
```
|
|
515
|
-
|
|
516
|
-
```svelte
|
|
517
|
-
<script>
|
|
518
|
-
import { createEventDispatcher, getContext, onMount, onDestroy } from "svelte";
|
|
519
|
-
import fsm from "svelte-fsm";
|
|
520
|
-
|
|
521
|
-
// Import types for IntelliSense (no runtime impact)
|
|
522
|
-
/**
|
|
523
|
-
* @typedef {import('./types.js').CellBooleanOptions} CellBooleanOptions
|
|
524
|
-
* @typedef {import('./types.js').CellApi<boolean>} CellBooleanApi
|
|
525
|
-
* @typedef {import('./types.js').CellEvents<boolean>} CellBooleanEvents
|
|
526
|
-
*/
|
|
527
|
-
|
|
528
|
-
const { processStringSync } = getContext("sdk");
|
|
529
|
-
const dispatch = createEventDispatcher();
|
|
530
|
-
|
|
531
|
-
// Typed props with JSDoc
|
|
532
|
-
/** @type {boolean | null} */
|
|
533
|
-
export let value;
|
|
534
|
-
|
|
535
|
-
/** @type {string | undefined} */
|
|
536
|
-
export let formattedValue;
|
|
537
|
-
|
|
538
|
-
/** @type {CellBooleanOptions} */
|
|
539
|
-
export let cellOptions;
|
|
540
|
-
|
|
541
|
-
/** @type {boolean} */
|
|
542
|
-
export let autofocus;
|
|
543
|
-
|
|
544
|
-
// Typed local variables
|
|
545
|
-
/** @type {boolean | null} */
|
|
546
|
-
let originalValue = value;
|
|
547
|
-
|
|
548
|
-
/** @type {boolean} */
|
|
549
|
-
let localValue = value ?? false;
|
|
550
|
-
|
|
551
|
-
// FSM with typed states
|
|
552
|
-
export let cellState = fsm(cellOptions.initialState ?? "View", {
|
|
553
|
-
"*": {
|
|
554
|
-
/** @param {string} state */
|
|
555
|
-
goTo(state) {
|
|
556
|
-
return state;
|
|
557
|
-
},
|
|
558
|
-
},
|
|
559
|
-
View: {
|
|
560
|
-
_enter() {
|
|
561
|
-
localValue = value ?? false;
|
|
562
|
-
originalValue = value;
|
|
563
|
-
},
|
|
564
|
-
/** @param {boolean | null} val */
|
|
565
|
-
reset(val) {
|
|
566
|
-
localValue = val ?? false;
|
|
567
|
-
originalValue = val;
|
|
568
|
-
return cellOptions.initialState ?? "View";
|
|
569
|
-
},
|
|
570
|
-
focus() {
|
|
571
|
-
if (!cellOptions.readonly && !cellOptions.disabled) return "Editing";
|
|
572
|
-
},
|
|
573
|
-
},
|
|
574
|
-
Editing: {
|
|
575
|
-
_enter() {
|
|
576
|
-
dispatch("enteredit");
|
|
577
|
-
},
|
|
578
|
-
_exit() {
|
|
579
|
-
dispatch("exitedit");
|
|
580
|
-
},
|
|
581
|
-
submit() {
|
|
582
|
-
value = localValue;
|
|
583
|
-
dispatch("change", localValue);
|
|
584
|
-
return "View";
|
|
585
|
-
},
|
|
586
|
-
cancel() {
|
|
587
|
-
localValue = originalValue ?? false;
|
|
588
|
-
dispatch("cancel");
|
|
589
|
-
return "View";
|
|
590
|
-
},
|
|
591
|
-
},
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
// Typed API object
|
|
595
|
-
/** @type {CellBooleanApi} */
|
|
596
|
-
export const cellApi = {
|
|
597
|
-
focus: () => cellState.focus(),
|
|
598
|
-
reset: () => cellState.reset(value),
|
|
599
|
-
isEditing: () => cellState.current === "Editing",
|
|
600
|
-
isDirty: () => localValue !== originalValue,
|
|
601
|
-
getValue: () => localValue,
|
|
602
|
-
/** @param {string} err */
|
|
603
|
-
setError: (err) => {},
|
|
604
|
-
clearError: () => {},
|
|
605
|
-
/** @param {boolean} val */
|
|
606
|
-
setValue: (val) => {
|
|
607
|
-
localValue = val;
|
|
608
|
-
},
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
// Typed event handlers
|
|
612
|
-
/** @param {Event} e */
|
|
613
|
-
const handleClick = (e) => {
|
|
614
|
-
if (cellState.current === "View") {
|
|
615
|
-
cellState.focus();
|
|
616
|
-
}
|
|
617
|
-
};
|
|
618
|
-
|
|
619
|
-
/** @param {KeyboardEvent} e */
|
|
620
|
-
const handleKeyboard = (e) => {
|
|
621
|
-
if (e.key === "Escape") {
|
|
622
|
-
cellState.cancel();
|
|
623
|
-
} else if (e.key === "Enter") {
|
|
624
|
-
cellState.submit();
|
|
625
|
-
} else if (e.key === " ") {
|
|
626
|
-
e.preventDefault();
|
|
627
|
-
localValue = !localValue;
|
|
628
|
-
}
|
|
629
|
-
};
|
|
630
|
-
</script>
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
## TypeScript Example: CellString.svelte
|
|
634
|
-
|
|
635
|
-
**Note:** We recommend using JSDoc instead of TypeScript for simplicity. This example is shown for comparison only.
|
|
636
|
-
|
|
637
|
-
```svelte
|
|
638
|
-
<script lang="ts">
|
|
639
|
-
import { createEventDispatcher, getContext, onMount, onDestroy } from "svelte";
|
|
640
|
-
import fsm from "svelte-fsm";
|
|
641
|
-
import type { CellStringOptions, CellApi, CellEvents } from "./types";
|
|
642
|
-
|
|
643
|
-
const { processStringSync } = getContext("sdk");
|
|
644
|
-
const dispatch = createEventDispatcher<CellEvents<string | null>>();
|
|
645
|
-
|
|
646
|
-
export let value: string | null;
|
|
647
|
-
export let formattedValue: string | undefined;
|
|
648
|
-
export let cellOptions: CellStringOptions;
|
|
649
|
-
export let autofocus: boolean;
|
|
650
|
-
|
|
651
|
-
let originalValue: string | null = value;
|
|
652
|
-
let localValue: string = value ?? "";
|
|
653
|
-
let editor: HTMLInputElement | HTMLTextAreaElement;
|
|
654
|
-
|
|
655
|
-
export let cellState = fsm(cellOptions.initialState ?? "View", {
|
|
656
|
-
"*": {
|
|
657
|
-
goTo(state: string) {
|
|
658
|
-
return state;
|
|
659
|
-
},
|
|
660
|
-
},
|
|
661
|
-
View: {
|
|
662
|
-
_enter() {
|
|
663
|
-
localValue = value ?? "";
|
|
664
|
-
originalValue = value;
|
|
665
|
-
},
|
|
666
|
-
reset(val: string | null) {
|
|
667
|
-
localValue = val ?? "";
|
|
668
|
-
originalValue = val;
|
|
669
|
-
return cellOptions.initialState ?? "View";
|
|
670
|
-
},
|
|
671
|
-
focus() {
|
|
672
|
-
if (!cellOptions.readonly && !cellOptions.disabled) return "Editing";
|
|
673
|
-
},
|
|
674
|
-
},
|
|
675
|
-
Editing: {
|
|
676
|
-
_enter() {
|
|
677
|
-
dispatch("enteredit");
|
|
678
|
-
editor?.focus();
|
|
679
|
-
},
|
|
680
|
-
_exit() {
|
|
681
|
-
dispatch("exitedit");
|
|
682
|
-
},
|
|
683
|
-
submit() {
|
|
684
|
-
value = localValue || null;
|
|
685
|
-
dispatch("change", value);
|
|
686
|
-
return "View";
|
|
687
|
-
},
|
|
688
|
-
cancel() {
|
|
689
|
-
localValue = originalValue ?? "";
|
|
690
|
-
dispatch("cancel");
|
|
691
|
-
return "View";
|
|
692
|
-
},
|
|
693
|
-
},
|
|
694
|
-
});
|
|
695
|
-
|
|
696
|
-
export const cellApi: CellApi<string> = {
|
|
697
|
-
focus: () => cellState.focus(),
|
|
698
|
-
reset: () => cellState.reset(value),
|
|
699
|
-
isEditing: () => cellState.current === "Editing",
|
|
700
|
-
isDirty: () => localValue !== originalValue,
|
|
701
|
-
getValue: () => localValue,
|
|
702
|
-
setError: (err: string) => {},
|
|
703
|
-
clearError: () => {},
|
|
704
|
-
setValue: (val: string) => {
|
|
705
|
-
localValue = val;
|
|
706
|
-
},
|
|
707
|
-
};
|
|
708
|
-
</script>
|
|
709
|
-
```
|
|
710
|
-
|
|
711
|
-
## Usage in Parent Components
|
|
712
|
-
|
|
713
|
-
### JavaScript Parent
|
|
714
|
-
|
|
715
|
-
```svelte
|
|
716
|
-
<script>
|
|
717
|
-
import { CellString } from "@poirazis/supercomponents-shared";
|
|
718
|
-
|
|
719
|
-
/**
|
|
720
|
-
* @typedef {import('@poirazis/supercomponents-shared').CellStringOptions} CellStringOptions
|
|
721
|
-
* @typedef {import('@poirazis/supercomponents-shared').CellApi<string>} CellStringApi
|
|
722
|
-
*/
|
|
723
|
-
|
|
724
|
-
/** @type {string | null} */
|
|
725
|
-
let value = "Hello";
|
|
726
|
-
|
|
727
|
-
/** @type {CellStringOptions} */
|
|
728
|
-
const options = {
|
|
729
|
-
role: "tableCell",
|
|
730
|
-
placeholder: "Enter text...",
|
|
731
|
-
debounce: 300,
|
|
732
|
-
align: "flex-start",
|
|
733
|
-
};
|
|
734
|
-
|
|
735
|
-
/** @type {CellStringApi} */
|
|
736
|
-
let api;
|
|
737
|
-
|
|
738
|
-
function handleChange(event) {
|
|
739
|
-
console.log("Value changed:", event.detail);
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function focusCell() {
|
|
743
|
-
api?.focus();
|
|
744
|
-
}
|
|
745
|
-
</script>
|
|
746
|
-
|
|
747
|
-
<CellString
|
|
748
|
-
bind:value
|
|
749
|
-
bind:cellApi={api}
|
|
750
|
-
cellOptions={options}
|
|
751
|
-
on:change={handleChange}
|
|
752
|
-
/>
|
|
753
|
-
```
|
|
754
|
-
|
|
755
|
-
### TypeScript Parent
|
|
756
|
-
|
|
757
|
-
```svelte
|
|
758
|
-
<script lang="ts">
|
|
759
|
-
import { CellString, type CellStringOptions, type CellApi } from "@poirazis/supercomponents-shared";
|
|
760
|
-
|
|
761
|
-
let value: string | null = "Hello";
|
|
762
|
-
|
|
763
|
-
const options: CellStringOptions = {
|
|
764
|
-
role: "tableCell",
|
|
765
|
-
placeholder: "Enter text...",
|
|
766
|
-
debounce: 300,
|
|
767
|
-
align: "flex-start",
|
|
768
|
-
};
|
|
769
|
-
|
|
770
|
-
let api: CellApi<string>;
|
|
771
|
-
|
|
772
|
-
function handleChange(event: CustomEvent<string | null>) {
|
|
773
|
-
console.log("Value changed:", event.detail);
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
function focusCell() {
|
|
777
|
-
api?.focus();
|
|
778
|
-
}
|
|
779
|
-
</script>
|
|
780
|
-
|
|
781
|
-
<CellString
|
|
782
|
-
bind:value
|
|
783
|
-
bind:cellApi={api}
|
|
784
|
-
cellOptions={options}
|
|
785
|
-
on:change={handleChange}
|
|
786
|
-
/>
|
|
787
|
-
```
|
|
788
|
-
|
|
789
|
-
## Common Patterns
|
|
790
|
-
|
|
791
|
-
### Minimal JSDoc - Just the Essentials
|
|
792
|
-
|
|
793
|
-
````javascript
|
|
794
|
-
/**
|
|
795
|
-
* @typedef {import('./types.js').CellNumberOptions} CellNumberOptions
|
|
796
|
-
* @typedef {import('./types.js').CellApi} CellApi
|
|
797
|
-
*/
|
|
798
|
-
|
|
799
|
-
/** @type {number | null} */
|
|
800
|
-
export let value;
|
|
801
|
-
|
|
802
|
-
/** @type {CellNumberOptions} */
|
|
803
|
-
export let cellOptions = {};
|
|
804
|
-
|
|
805
|
-
// Everything else - no annotations
|
|
806
|
-
let localValue = value;
|
|
807
|
-
let editor;
|
|
808
|
-
|
|
809
|
-
export const cellState = fsm("View", {
|
|
810
|
-
View: {
|
|
811
|
-
focus() {
|
|
812
|
-
return "Editing";
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
});
|
|
816
|
-
|
|
817
|
-
/** @type {CellApi} */
|
|
818
|
-
export const cellApi = {
|
|
819
|
-
focus: () => cellState.focus(),
|
|
820
|
-
getValue: () => localValue,
|
|
821
|
-
// ... other methods
|
|
822
|
-
};
|
|
823
|
-
## Type Enforcement
|
|
824
|
-
|
|
825
|
-
### VS Code Settings
|
|
826
|
-
|
|
827
|
-
Add to `.vscode/settings.json`:
|
|
828
|
-
|
|
829
|
-
```json
|
|
830
|
-
{
|
|
831
|
-
"js/ts.implicitProjectConfig.checkJs": true,
|
|
832
|
-
"javascript.validate.enable": true,
|
|
833
|
-
"typescript.tsdk": "node_modules/typescript/lib"
|
|
834
|
-
}
|
|
835
|
-
````
|
|
836
|
-
|
|
837
|
-
### Component-Level Enforcement
|
|
838
|
-
|
|
839
|
-
Add to top of JavaScript files:
|
|
840
|
-
|
|
841
|
-
```javascript
|
|
842
|
-
// @ts-check
|
|
843
|
-
```
|
|
844
|
-
|
|
845
|
-
This enables TypeScript checking in JavaScript files.
|
|
846
|
-
|
|
847
|
-
## Migration Strategy
|
|
848
|
-
|
|
849
|
-
1. **Add typedef imports** at top of script
|
|
850
|
-
2. **Annotate cellOptions prop** with component-specific options type
|
|
851
|
-
3. **Annotate cellApi export** with CellApi type
|
|
852
|
-
4. **Annotate main props** (value, formattedValue)
|
|
853
|
-
5. **Done!** - Don't annotate anything else
|
|
854
|
-
|
|
855
|
-
## IntelliSense Benefits
|
|
856
|
-
|
|
857
|
-
With just these 3-4 JSDoc annotations, you get:
|
|
858
|
-
|
|
859
|
-
- **Autocomplete for cellOptions** - VS Code shows all available options
|
|
860
|
-
- **cellApi IntelliSense** - Consumers see all available methods
|
|
861
|
-
- **Prop type checking** - Errors if wrong types are passed
|
|
862
|
-
- **Clean code** - No clutter from over-annotation
|
|
863
|
-
|
|
864
|
-
## Best Practices
|
|
865
|
-
|
|
866
|
-
1. **Only annotate public interfaces** - cellOptions and cellApi
|
|
867
|
-
2. **Keep it minimal** - Less is more
|
|
868
|
-
3. **No @ts-check needed** - Too strict, unnecessary noise
|
|
869
|
-
4. **Trust JavaScript** - Let Svelte handle the rest
|