@a4ui/core 0.34.0 → 0.35.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.
Files changed (42) hide show
  1. package/dist/elements.css +119 -0
  2. package/dist/elements.iife.js +2 -2
  3. package/dist/elements.js +115 -114
  4. package/dist/full.css +119 -0
  5. package/dist/index.d.ts +15 -1
  6. package/dist/index.js +8175 -5443
  7. package/dist/ui/ActivityFeed.d.ts +49 -0
  8. package/dist/ui/AvailabilityPicker.d.ts +35 -0
  9. package/dist/ui/CodeEditor.d.ts +25 -0
  10. package/dist/ui/EmojiPicker.d.ts +18 -0
  11. package/dist/ui/EventScheduler.d.ts +41 -0
  12. package/dist/ui/InteractiveMap.d.ts +50 -0
  13. package/dist/ui/JsonViewer.d.ts +24 -0
  14. package/dist/ui/LocationPicker.d.ts +25 -0
  15. package/dist/ui/MaskedInput.d.ts +26 -0
  16. package/dist/ui/OtpInput.d.ts +29 -0
  17. package/dist/ui/PresenceAvatars.d.ts +54 -0
  18. package/dist/ui/QueryBuilder.d.ts +56 -0
  19. package/dist/ui/RingProgress.d.ts +2 -0
  20. package/dist/ui/SignaturePad.d.ts +19 -0
  21. package/dist/ui/SpreadsheetGrid.d.ts +27 -0
  22. package/package.json +1 -1
  23. package/src/index.ts +36 -1
  24. package/src/ui/ActivityFeed.tsx +178 -0
  25. package/src/ui/AudioWaveform.tsx +1 -3
  26. package/src/ui/AvailabilityPicker.tsx +163 -0
  27. package/src/ui/CodeEditor.tsx +277 -0
  28. package/src/ui/EmojiPicker.tsx +424 -0
  29. package/src/ui/EventScheduler.tsx +286 -0
  30. package/src/ui/GanttChart.tsx +9 -1
  31. package/src/ui/InteractiveMap.tsx +349 -0
  32. package/src/ui/JsonViewer.tsx +189 -0
  33. package/src/ui/LocationPicker.tsx +96 -0
  34. package/src/ui/MaskedInput.tsx +108 -0
  35. package/src/ui/OnboardingChecklist.tsx +1 -0
  36. package/src/ui/OtpInput.tsx +153 -0
  37. package/src/ui/PivotTable.tsx +0 -0
  38. package/src/ui/PresenceAvatars.tsx +142 -0
  39. package/src/ui/QueryBuilder.tsx +281 -0
  40. package/src/ui/RingProgress.tsx +3 -0
  41. package/src/ui/SignaturePad.tsx +221 -0
  42. package/src/ui/SpreadsheetGrid.tsx +307 -0
@@ -0,0 +1,307 @@
1
+ // Excel-like editable grid: header row + row-number gutter, keyboard navigation,
2
+ // rectangular range selection, and TSV copy/paste. Fixed rows×cols for v1 (no
3
+ // add-row); for huge grids, virtualize with @tanstack/solid-virtual instead.
4
+ import type { JSX } from 'solid-js'
5
+ import { createMemo, createSignal, For } from 'solid-js'
6
+
7
+ import { cn } from '../lib/cn'
8
+
9
+ export interface SpreadsheetGridProps {
10
+ /** Number of rows in the grid (fixed for the component's lifetime). */
11
+ rows: number
12
+ /** Number of columns in the grid (fixed for the component's lifetime). */
13
+ cols: number
14
+ /** Initial cell values, seeded once on mount (missing cells default to `''`). */
15
+ data?: string[][]
16
+ /** Called with the full `rows×cols` matrix after any edit or paste. */
17
+ onChange?: (data: string[][]) => void
18
+ /** Column header labels; defaults to `A, B, C, …, Z, AA, AB, …`. */
19
+ columnHeaders?: string[]
20
+ class?: string
21
+ }
22
+
23
+ interface CellPos {
24
+ r: number
25
+ c: number
26
+ }
27
+
28
+ /** Converts a zero-based column index to a spreadsheet-style letter label. */
29
+ function colLabel(index: number): string {
30
+ let n = index
31
+ let label = ''
32
+ while (n >= 0) {
33
+ label = String.fromCharCode(65 + (n % 26)) + label
34
+ n = Math.floor(n / 26) - 1
35
+ }
36
+ return label
37
+ }
38
+
39
+ function seedData(props: SpreadsheetGridProps): string[][] {
40
+ const grid: string[][] = []
41
+ for (let r = 0; r < props.rows; r++) {
42
+ const row: string[] = []
43
+ for (let c = 0; c < props.cols; c++) row.push(props.data?.[r]?.[c] ?? '')
44
+ grid.push(row)
45
+ }
46
+ return grid
47
+ }
48
+
49
+ function cloneData(data: string[][]): string[][] {
50
+ return data.map((row) => [...row])
51
+ }
52
+
53
+ /**
54
+ * Excel-like editable grid. Arrow keys move the active cell; typing or Enter
55
+ * opens an inline editor that commits on Enter/blur and cancels on Escape.
56
+ * Shift+click / Shift+arrow extend a rectangular selection (Ctrl/Cmd+C copies
57
+ * it as TSV, Ctrl/Cmd+V pastes TSV starting at the active cell).
58
+ *
59
+ * @example
60
+ * ```tsx
61
+ * const [sheet, setSheet] = createSignal<string[][]>([])
62
+ * <SpreadsheetGrid rows={10} cols={6} onChange={setSheet} />
63
+ * ```
64
+ */
65
+ export function SpreadsheetGrid(props: SpreadsheetGridProps): JSX.Element {
66
+ const [data, setData] = createSignal<string[][]>(seedData(props))
67
+ const [active, setActive] = createSignal<CellPos>({ r: 0, c: 0 })
68
+ const [anchor, setAnchor] = createSignal<CellPos>({ r: 0, c: 0 })
69
+ const [editing, setEditing] = createSignal<CellPos | null>(null)
70
+ const [editValue, setEditValue] = createSignal('')
71
+ let containerRef: HTMLDivElement | undefined
72
+
73
+ const headers = createMemo(
74
+ () => props.columnHeaders ?? Array.from({ length: props.cols }, (_, i) => colLabel(i)),
75
+ )
76
+ const rowIndices = createMemo(() => Array.from({ length: props.rows }, (_, i) => i))
77
+ const colIndices = createMemo(() => Array.from({ length: props.cols }, (_, i) => i))
78
+
79
+ const bounds = createMemo(() => {
80
+ const a = anchor()
81
+ const b = active()
82
+ return {
83
+ minR: Math.min(a.r, b.r),
84
+ maxR: Math.max(a.r, b.r),
85
+ minC: Math.min(a.c, b.c),
86
+ maxC: Math.max(a.c, b.c),
87
+ }
88
+ })
89
+
90
+ function inSelection(r: number, c: number): boolean {
91
+ const b = bounds()
92
+ return r >= b.minR && r <= b.maxR && c >= b.minC && c <= b.maxC
93
+ }
94
+
95
+ function isActive(r: number, c: number): boolean {
96
+ const a = active()
97
+ return a.r === r && a.c === c
98
+ }
99
+
100
+ function isAnchor(r: number, c: number): boolean {
101
+ const a = anchor()
102
+ return a.r === r && a.c === c
103
+ }
104
+
105
+ function isEditing(r: number, c: number): boolean {
106
+ const e = editing()
107
+ return e !== null && e.r === r && e.c === c
108
+ }
109
+
110
+ function commitCell(r: number, c: number, value: string) {
111
+ const next = cloneData(data())
112
+ next[r][c] = value
113
+ setData(next)
114
+ props.onChange?.(next)
115
+ }
116
+
117
+ function startEdit(r: number, c: number, initial?: string) {
118
+ setEditing({ r, c })
119
+ setEditValue(initial ?? data()[r][c])
120
+ }
121
+
122
+ function commitEdit() {
123
+ const e = editing()
124
+ if (!e) return
125
+ commitCell(e.r, e.c, editValue())
126
+ setEditing(null)
127
+ }
128
+
129
+ function cancelEdit() {
130
+ setEditing(null)
131
+ }
132
+
133
+ function moveActive(dr: number, dc: number, extend: boolean) {
134
+ const cur = active()
135
+ const r = Math.min(Math.max(cur.r + dr, 0), props.rows - 1)
136
+ const c = Math.min(Math.max(cur.c + dc, 0), props.cols - 1)
137
+ setActive({ r, c })
138
+ if (!extend) setAnchor({ r, c })
139
+ }
140
+
141
+ function selectCell(r: number, c: number, extend: boolean) {
142
+ setActive({ r, c })
143
+ if (!extend) setAnchor({ r, c })
144
+ }
145
+
146
+ function copySelection() {
147
+ const b = bounds()
148
+ const tsv = data()
149
+ .slice(b.minR, b.maxR + 1)
150
+ .map((row) => row.slice(b.minC, b.maxC + 1).join('\t'))
151
+ .join('\n')
152
+ void navigator.clipboard.writeText(tsv)
153
+ }
154
+
155
+ function pasteAtActive() {
156
+ void navigator.clipboard.readText().then((text) => {
157
+ const lines = text.replace(/\r/g, '').split('\n')
158
+ while (lines.length > 1 && lines[lines.length - 1] === '') lines.pop()
159
+ const grid = lines.map((line) => line.split('\t'))
160
+ const start = active()
161
+ const next = cloneData(data())
162
+ grid.forEach((line, ri) => {
163
+ line.forEach((value, ci) => {
164
+ const r = start.r + ri
165
+ const c = start.c + ci
166
+ if (r < props.rows && c < props.cols) next[r][c] = value
167
+ })
168
+ })
169
+ setData(next)
170
+ props.onChange?.(next)
171
+ })
172
+ }
173
+
174
+ function handleKeyDown(ev: KeyboardEvent) {
175
+ if (editing()) {
176
+ if (ev.key === 'Enter') {
177
+ ev.preventDefault()
178
+ commitEdit()
179
+ moveActive(1, 0, false)
180
+ containerRef?.focus()
181
+ } else if (ev.key === 'Escape') {
182
+ ev.preventDefault()
183
+ cancelEdit()
184
+ containerRef?.focus()
185
+ }
186
+ return
187
+ }
188
+
189
+ const mod = ev.ctrlKey || ev.metaKey
190
+ if (mod && ev.key.toLowerCase() === 'c') {
191
+ ev.preventDefault()
192
+ copySelection()
193
+ return
194
+ }
195
+ if (mod && ev.key.toLowerCase() === 'v') {
196
+ ev.preventDefault()
197
+ pasteAtActive()
198
+ return
199
+ }
200
+
201
+ switch (ev.key) {
202
+ case 'ArrowUp':
203
+ ev.preventDefault()
204
+ moveActive(-1, 0, ev.shiftKey)
205
+ break
206
+ case 'ArrowDown':
207
+ ev.preventDefault()
208
+ moveActive(1, 0, ev.shiftKey)
209
+ break
210
+ case 'ArrowLeft':
211
+ ev.preventDefault()
212
+ moveActive(0, -1, ev.shiftKey)
213
+ break
214
+ case 'ArrowRight':
215
+ ev.preventDefault()
216
+ moveActive(0, 1, ev.shiftKey)
217
+ break
218
+ case 'Tab':
219
+ ev.preventDefault()
220
+ moveActive(0, ev.shiftKey ? -1 : 1, false)
221
+ break
222
+ case 'Enter':
223
+ ev.preventDefault()
224
+ startEdit(active().r, active().c)
225
+ break
226
+ default:
227
+ if (ev.key.length === 1 && !mod && !ev.altKey) {
228
+ startEdit(active().r, active().c, ev.key)
229
+ }
230
+ }
231
+ }
232
+
233
+ return (
234
+ <div
235
+ ref={containerRef}
236
+ tabIndex={0}
237
+ role="grid"
238
+ aria-label="Spreadsheet"
239
+ aria-rowcount={props.rows}
240
+ aria-colcount={props.cols}
241
+ class={cn(
242
+ 'inline-block select-none overflow-auto rounded-md border border-border bg-card text-foreground outline-none focus-visible:ring-2 focus-visible:ring-primary',
243
+ props.class,
244
+ )}
245
+ onKeyDown={handleKeyDown}
246
+ >
247
+ <div class="grid" style={{ 'grid-template-columns': `48px repeat(${props.cols}, minmax(80px, 1fr))` }}>
248
+ <div role="row" class="contents">
249
+ <div aria-hidden="true" class="border-b border-r border-border bg-muted" />
250
+ <For each={headers()}>
251
+ {(h) => (
252
+ <div
253
+ role="columnheader"
254
+ class="border-b border-r border-border bg-muted px-2 py-1 text-center text-xs font-semibold text-muted-foreground"
255
+ >
256
+ {h}
257
+ </div>
258
+ )}
259
+ </For>
260
+ </div>
261
+ <For each={rowIndices()}>
262
+ {(r) => (
263
+ <div role="row" class="contents">
264
+ <div
265
+ role="rowheader"
266
+ class="border-b border-r border-border bg-muted px-2 py-1 text-center text-xs text-muted-foreground"
267
+ >
268
+ {r + 1}
269
+ </div>
270
+ <For each={colIndices()}>
271
+ {(c) => (
272
+ <div
273
+ role="gridcell"
274
+ aria-selected={isActive(r, c)}
275
+ class={cn(
276
+ 'relative h-8 border-b border-r border-border px-2 py-1 text-sm cursor-cell',
277
+ inSelection(r, c) && 'bg-primary/10',
278
+ isActive(r, c) && 'ring-2 ring-inset ring-primary',
279
+ isAnchor(r, c) && !isActive(r, c) && 'border-primary',
280
+ )}
281
+ onClick={(ev) => selectCell(r, c, ev.shiftKey)}
282
+ onDblClick={() => startEdit(r, c)}
283
+ >
284
+ {isEditing(r, c) ? (
285
+ <input
286
+ ref={(el) => {
287
+ el.focus()
288
+ el.select()
289
+ }}
290
+ class="absolute inset-0 h-full w-full border-none bg-transparent px-2 py-1 text-sm text-foreground outline-none"
291
+ value={editValue()}
292
+ onInput={(ev) => setEditValue(ev.currentTarget.value)}
293
+ onBlur={() => commitEdit()}
294
+ />
295
+ ) : (
296
+ <span class="block truncate">{data()[r][c]}</span>
297
+ )}
298
+ </div>
299
+ )}
300
+ </For>
301
+ </div>
302
+ )}
303
+ </For>
304
+ </div>
305
+ </div>
306
+ )
307
+ }