@stacksjs/defaults 0.75.231 → 0.75.236

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 (75) hide show
  1. package/ai/skills/stacks-dashboard/SKILL.md +2 -2
  2. package/ai/skills/stacks-database/SKILL.md +1 -1
  3. package/ai/skills/stacks-deploy/SKILL.md +3 -2
  4. package/ai/skills/stacks-migrations/SKILL.md +2 -2
  5. package/ai/skills/stacks-native/CATALOG.md +10 -10
  6. package/ai/skills/stacks-realtime/SKILL.md +4 -2
  7. package/ai/skills/stacks-storage/SKILL.md +43 -0
  8. package/ai/skills/stacks-testing/SKILL.md +38 -10
  9. package/app/Actions/Monitoring/error-provider.ts +129 -12
  10. package/app/Models/Error.ts +21 -0
  11. package/functions/dashboard-form.test.ts +41 -0
  12. package/functions/dashboard-form.ts +30 -0
  13. package/functions/dashboard-list.test.ts +228 -0
  14. package/functions/dashboard-list.ts +257 -0
  15. package/ide/vscode/package.json +1 -1
  16. package/package.json +2 -2
  17. package/project/storage/framework/types/browser-composables.d.ts +56 -0
  18. package/resources/components/Dashboard/Auth/AuthCard.stx +29 -0
  19. package/resources/components/Dashboard/Auth/ForgotPassword.stx +65 -79
  20. package/resources/components/Dashboard/Auth/Login.stx +73 -82
  21. package/resources/components/Dashboard/Auth/Register.stx +121 -135
  22. package/resources/components/Dashboard/Auth/ResetPassword.stx +88 -102
  23. package/resources/components/Dashboard/Buddy/BuddyAssistant.stx +11 -15
  24. package/resources/components/Dashboard/Commerce/CommerceCategoriesDashboard.stx +10 -27
  25. package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +2 -4
  26. package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +2 -4
  27. package/resources/components/Dashboard/Commerce/CommerceCouponsDashboard.stx +10 -27
  28. package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +2 -4
  29. package/resources/components/Dashboard/Commerce/CommerceCustomersDashboard.stx +10 -27
  30. package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +2 -4
  31. package/resources/components/Dashboard/Commerce/CommerceGiftCardsDashboard.stx +10 -27
  32. package/resources/components/Dashboard/Commerce/CommerceManufacturerDialog.stx +2 -4
  33. package/resources/components/Dashboard/Commerce/CommerceManufacturersDashboard.stx +13 -24
  34. package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +2 -4
  35. package/resources/components/Dashboard/Commerce/CommerceOrdersDashboard.stx +10 -27
  36. package/resources/components/Dashboard/Commerce/CommerceOverviewDashboard.stx +17 -21
  37. package/resources/components/Dashboard/Commerce/CommercePaymentsDashboard.stx +7 -19
  38. package/resources/components/Dashboard/Commerce/CommercePosDashboard.stx +4 -16
  39. package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +2 -4
  40. package/resources/components/Dashboard/Commerce/CommerceProductsDashboard.stx +10 -27
  41. package/resources/components/Dashboard/Commerce/CommerceReviewEditDialog.stx +2 -4
  42. package/resources/components/Dashboard/Commerce/CommerceReviewsDashboard.stx +7 -18
  43. package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +2 -4
  44. package/resources/components/Dashboard/Commerce/CommerceTaxesDashboard.stx +13 -24
  45. package/resources/components/Dashboard/Commerce/CommerceUnitDialog.stx +2 -4
  46. package/resources/components/Dashboard/Commerce/CommerceUnitsDashboard.stx +13 -24
  47. package/resources/components/Dashboard/Commerce/CommerceVariantDialog.stx +2 -4
  48. package/resources/components/Dashboard/Commerce/CommerceVariantsDashboard.stx +7 -18
  49. package/resources/components/Dashboard/Commerce/Delivery/CouriersDashboard.stx +52 -105
  50. package/resources/components/Dashboard/Commerce/Delivery/DeliveryOverviewDashboard.stx +12 -16
  51. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRoutesDashboard.stx +53 -105
  52. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveriesDashboard.stx +7 -13
  53. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeysDashboard.stx +53 -105
  54. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodsDashboard.stx +7 -13
  55. package/resources/components/Dashboard/Commerce/Delivery/ShippingRatesDashboard.stx +51 -103
  56. package/resources/components/Dashboard/Commerce/Delivery/ShippingZonesDashboard.stx +52 -104
  57. package/resources/components/Dashboard/Commerce/PrintDeviceDialog.stx +2 -4
  58. package/resources/components/Dashboard/Commerce/PrintDevicesDashboard.stx +10 -27
  59. package/resources/components/Dashboard/Commerce/PrintLogsDashboard.stx +7 -19
  60. package/resources/components/Dashboard/Commerce/ProductWaitlistDashboard.stx +12 -16
  61. package/resources/components/Dashboard/Commerce/ProductWaitlistEntryDialog.stx +2 -4
  62. package/resources/components/Dashboard/Commerce/RestaurantWaitlistDashboard.stx +12 -16
  63. package/resources/components/Dashboard/Commerce/RestaurantWaitlistEntryDialog.stx +2 -4
  64. package/resources/components/Dashboard/Deployments/DeploymentList.stx +17 -21
  65. package/resources/components/Dashboard/Marketing/AbandonedCartsDashboard.stx +8 -19
  66. package/resources/components/Dashboard/Marketing/CampaignsDashboard.stx +8 -19
  67. package/resources/components/Dashboard/Marketing/MarketingListsDashboard.stx +11 -22
  68. package/resources/components/Dashboard/Marketing/SocialPostsDashboard.stx +8 -19
  69. package/resources/components/Dashboard/Queue/QueueDashboard.stx +12 -16
  70. package/resources/components/Dashboard/Realtime/RealtimeDashboard.stx +6 -10
  71. package/resources/components/Dashboard/Releases/ReleaseDashboard.stx +6 -10
  72. package/resources/components/Dashboard/UI/MetricCards.stx +32 -0
  73. package/resources/components/Dashboard/UI/PageHeader.stx +25 -66
  74. package/resources/components/Dashboard/UI/Table.stx +79 -6
  75. package/resources/plugins/preloader.ts +9 -1
@@ -0,0 +1,228 @@
1
+ import { describe, expect, it } from 'bun:test'
2
+ import { pageControls, useRecordsDashboard } from './dashboard-list'
3
+
4
+ /** The smallest thing with the two members `pageControls` reads. */
5
+ function pageSignal(initial: number) {
6
+ let value = initial
7
+ const signal = (() => value) as { (): number, set: (next: number) => void }
8
+ signal.set = (next: number) => { value = next }
9
+ return signal
10
+ }
11
+
12
+ describe('pageControls', () => {
13
+ it('moves a page back and forward', () => {
14
+ const page = pageSignal(2)
15
+ const { previousPage, nextPage } = pageControls(page, () => 5)
16
+ previousPage()
17
+ expect(page()).toBe(1)
18
+ nextPage()
19
+ nextPage()
20
+ expect(page()).toBe(3)
21
+ })
22
+
23
+ it('will not go below the first page', () => {
24
+ const page = pageSignal(1)
25
+ const { previousPage } = pageControls(page, () => 5)
26
+ previousPage()
27
+ previousPage()
28
+ expect(page()).toBe(1)
29
+ })
30
+
31
+ it('will not go past the last page', () => {
32
+ const page = pageSignal(5)
33
+ const { nextPage } = pageControls(page, () => 5)
34
+ nextPage()
35
+ expect(page()).toBe(5)
36
+ })
37
+
38
+ it('reads the last page at click time, not when it was wired up', () => {
39
+ // A filter that shrinks the result set has to shrink the range too.
40
+ const page = pageSignal(1)
41
+ let last = 9
42
+ const { nextPage } = pageControls(page, () => last)
43
+ last = 2
44
+ nextPage()
45
+ nextPage()
46
+ nextPage()
47
+ expect(page()).toBe(2)
48
+ })
49
+
50
+ it('clamps an out-of-range page back into the range', () => {
51
+ // The guard style the Marketing dashboards used could not do this: a page
52
+ // already past the end stayed there.
53
+ const page = pageSignal(40)
54
+ const { nextPage, previousPage } = pageControls(page, () => 3)
55
+ nextPage()
56
+ expect(page()).toBe(3)
57
+ page.set(40)
58
+ previousPage()
59
+ expect(page()).toBe(39)
60
+ })
61
+
62
+ it('survives an empty result set, where the last page is zero', () => {
63
+ const page = pageSignal(1)
64
+ const { nextPage } = pageControls(page, () => 0)
65
+ nextPage()
66
+ expect(page()).toBe(1)
67
+ })
68
+ })
69
+
70
+ interface Row { id: number, name: string }
71
+
72
+ const byName = (rows: Row[], query: string) => rows.filter(r => r.name.toLowerCase().includes(query))
73
+
74
+ function seeded(count: number, perPage = 10) {
75
+ const d = useRecordsDashboard<Row>({ filter: byName, perPage })
76
+ d.records.set(Array.from({ length: count }, (_, i) => ({ id: i + 1, name: `row ${i + 1}` })))
77
+ return d
78
+ }
79
+
80
+ describe('useRecordsDashboard', () => {
81
+ it('starts loading, empty, on the first page', () => {
82
+ const d = useRecordsDashboard<Row>({ filter: byName })
83
+ expect(d.loading()).toBe(true)
84
+ expect(d.records()).toEqual([])
85
+ expect(d.page()).toBe(1)
86
+ expect(d.pageCount()).toBe(1)
87
+ expect(d.paginatedRecords()).toEqual([])
88
+ })
89
+
90
+ it('pages through the records', () => {
91
+ const d = seeded(25)
92
+ expect(d.pageCount()).toBe(3)
93
+ expect(d.paginatedRecords().map(r => r.id)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
94
+ d.nextPage()
95
+ expect(d.paginatedRecords()[0]!.id).toBe(11)
96
+ d.page.set(3)
97
+ expect(d.paginatedRecords()).toHaveLength(5)
98
+ })
99
+
100
+ it('searching returns to the first page, because page 3 of a new result set is meaningless', () => {
101
+ const d = seeded(25)
102
+ d.page.set(3)
103
+ d.handleSearch('row 1')
104
+ expect(d.page()).toBe(1)
105
+ // row 1, 10-19
106
+ expect(d.filteredRecords()).toHaveLength(11)
107
+ })
108
+
109
+ it('an edit replaces in place and does not move the page', () => {
110
+ const d = seeded(25)
111
+ d.page.set(2)
112
+ const target = d.records()[4]!
113
+ d.upsert({ id: target.id, name: 'renamed' }, target)
114
+ expect(d.records()[4]!.name).toBe('renamed')
115
+ expect(d.records()).toHaveLength(25)
116
+ expect(d.page()).toBe(2)
117
+ })
118
+
119
+ it('a create appends and lands on the page the new row is on', () => {
120
+ const d = seeded(20)
121
+ expect(d.pageCount()).toBe(2)
122
+ d.upsert({ id: 99, name: 'new' }, null)
123
+ expect(d.records()).toHaveLength(21)
124
+ expect(d.page()).toBe(3)
125
+ expect(d.paginatedRecords()).toEqual([{ id: 99, name: 'new' }])
126
+ })
127
+
128
+ it('removing the last row on the last page pulls the page back', () => {
129
+ const d = seeded(11)
130
+ d.page.set(2)
131
+ expect(d.paginatedRecords()).toHaveLength(1)
132
+ d.remove(d.records()[10]!)
133
+ expect(d.page()).toBe(1)
134
+ expect(d.records()).toHaveLength(10)
135
+ })
136
+
137
+ it('removing elsewhere leaves the page alone', () => {
138
+ const d = seeded(25)
139
+ d.page.set(2)
140
+ d.remove(d.records()[0]!)
141
+ expect(d.page()).toBe(2)
142
+ expect(d.records()).toHaveLength(24)
143
+ })
144
+
145
+ it('clampPage pulls a page back into range after a filter shrinks the list', () => {
146
+ const d = seeded(25)
147
+ d.page.set(3)
148
+ d.search.set('row 7')
149
+ d.clampPage()
150
+ expect(d.page()).toBe(1)
151
+ })
152
+
153
+ it('the editor opens for a create and for an edit, and remembers which', () => {
154
+ const d = seeded(3)
155
+ d.openCreate()
156
+ expect(d.editorOpen()).toBe(true)
157
+ expect(d.editorMode()).toBe('create')
158
+ expect(d.editing()).toBeNull()
159
+
160
+ const row = d.records()[1]!
161
+ d.openEdit(row)
162
+ expect(d.editorMode()).toBe('edit')
163
+ expect(d.editing()).toBe(row)
164
+ })
165
+
166
+ it('will not close the editor mid-save', () => {
167
+ // Otherwise a slow PATCH can land on a dialog the user already dismissed.
168
+ const d = seeded(3)
169
+ d.openEdit(d.records()[0]!)
170
+ d.editorError.set('something went wrong')
171
+ d.saving.set(true)
172
+ d.closeEditor()
173
+ expect(d.editorOpen()).toBe(true)
174
+ expect(d.editorError()).toBe('something went wrong')
175
+
176
+ d.saving.set(false)
177
+ d.closeEditor()
178
+ expect(d.editorOpen()).toBe(false)
179
+ expect(d.editing()).toBeNull()
180
+ expect(d.editorError()).toBe('')
181
+ })
182
+
183
+ it('will not close the delete confirmation mid-delete', () => {
184
+ const d = seeded(3)
185
+ d.requestDelete(d.records()[0]!)
186
+ d.deleteBusy.set(true)
187
+ d.closeDelete()
188
+ expect(d.deleting()).not.toBeNull()
189
+
190
+ d.deleteBusy.set(false)
191
+ d.closeDelete()
192
+ expect(d.deleting()).toBeNull()
193
+ })
194
+
195
+ it('honours a custom identity instead of id', () => {
196
+ const d = useRecordsDashboard<{ sku: string }>({
197
+ filter: rows => rows,
198
+ sameRecord: (a, b) => a.sku === b.sku,
199
+ })
200
+ d.records.set([{ sku: 'a' }, { sku: 'b' }])
201
+ d.remove({ sku: 'a' })
202
+ expect(d.records()).toEqual([{ sku: 'b' }])
203
+ })
204
+ })
205
+
206
+ describe('useRecordsDashboard goToPage', () => {
207
+ it('jumps to a page', () => {
208
+ const d = seeded(25)
209
+ d.goToPage(3)
210
+ expect(d.page()).toBe(3)
211
+ })
212
+
213
+ it('clamps a page that does not exist', () => {
214
+ const d = seeded(25)
215
+ d.goToPage(99)
216
+ expect(d.page()).toBe(3)
217
+ d.goToPage(0)
218
+ expect(d.page()).toBe(1)
219
+ d.goToPage(-4)
220
+ expect(d.page()).toBe(1)
221
+ })
222
+
223
+ it('survives a non-number from a component that emitted one', () => {
224
+ const d = seeded(25)
225
+ d.goToPage(Number.NaN)
226
+ expect(d.page()).toBe(1)
227
+ })
228
+ })
@@ -0,0 +1,257 @@
1
+ import type { Signal } from '@stacksjs/stx'
2
+ import { state } from '@stacksjs/stx'
3
+
4
+ /**
5
+ * Shared pieces of the dashboard's list screens.
6
+ *
7
+ * Seventeen list dashboards each wrote their own `previousPage` and
8
+ * `nextPage` (stacksjs/stacks#2918), in four different styles. Twelve of them
9
+ * mean the same thing - move a page number, keep it between one and the last
10
+ * page - and this is that, once.
11
+ */
12
+
13
+ /** The shape `state()` returns, narrowed to what paging needs. */
14
+ interface PageSignal {
15
+ (): number
16
+ set: (value: number) => void
17
+ }
18
+
19
+ export interface PageControls {
20
+ /** Back one page, never past the first. */
21
+ previousPage: () => void
22
+ /** Forward one page, never past the last. */
23
+ nextPage: () => void
24
+ }
25
+
26
+ /**
27
+ * Previous and next for a page number bounded by a last page.
28
+ *
29
+ * `lastPage` is an accessor rather than a number because every call site
30
+ * derives it, and reading it at click time is what keeps the clamp correct
31
+ * when a filter shrinks the result set under the reader's feet.
32
+ *
33
+ * Clamps rather than guards. The twelve call sites split between
34
+ * `page.set(Math.max(1, page() - 1))` and `if (page() > 1) page.set(page() - 1)`,
35
+ * which do the same thing from a user's point of view; clamping is the one
36
+ * that stays correct if a page number is ever set out of range by something
37
+ * other than these two functions.
38
+ *
39
+ * Not for the dashboards that page server-side. `Jobs/JobHistory` reloads
40
+ * inside its `nextPage`, and a navigation that triggers a fetch is a
41
+ * different thing wearing the same name.
42
+ */
43
+ export function pageControls(page: PageSignal, lastPage: () => number): PageControls {
44
+ return {
45
+ previousPage: () => page.set(Math.max(1, page() - 1)),
46
+ nextPage: () => page.set(Math.min(Math.max(1, lastPage()), page() + 1)),
47
+ }
48
+ }
49
+
50
+ /** How a list screen decides what the current search matches. */
51
+ export interface RecordsDashboardOptions<T> {
52
+ /**
53
+ * Rows matching `query`, which arrives trimmed and lowercased.
54
+ *
55
+ * The caller's, because the fields worth searching are the only part of a
56
+ * list dashboard that is genuinely about its own records.
57
+ */
58
+ filter: (records: T[], query: string) => T[]
59
+ /** Rows per page. Default 10, which is what all five callers used. */
60
+ perPage?: number
61
+ /**
62
+ * Whether two rows are the same row. Defaults to matching `id`, which is
63
+ * what every current caller means.
64
+ */
65
+ sameRecord?: (a: T, b: T) => boolean
66
+ }
67
+
68
+ export interface RecordsDashboard<T> {
69
+ records: Signal<T[]>
70
+ loading: Signal<boolean>
71
+ loadError: Signal<string>
72
+ search: Signal<string>
73
+ page: Signal<number>
74
+ perPage: Signal<number>
75
+ editorOpen: Signal<boolean>
76
+ editorMode: Signal<'create' | 'edit'>
77
+ editing: Signal<T | null>
78
+ saving: Signal<boolean>
79
+ editorError: Signal<string>
80
+ deleting: Signal<T | null>
81
+ deleteBusy: Signal<boolean>
82
+ deleteError: Signal<string>
83
+ filteredRecords: () => T[]
84
+ paginatedRecords: () => T[]
85
+ pageCount: () => number
86
+ previousPage: () => void
87
+ nextPage: () => void
88
+ /**
89
+ * Jump to a page, clamped into range.
90
+ *
91
+ * The five callers each wrote a bare `page.set(value)` and trusted the
92
+ * pagination component to emit only pages that exist. Clamping costs
93
+ * nothing when that is true and keeps the list rendering when it is not.
94
+ */
95
+ goToPage: (value: number) => void
96
+ /** Set the search and return to the first page, which is the only sane page to land on. */
97
+ handleSearch: (value: string) => void
98
+ openCreate: () => void
99
+ openEdit: (record: T) => void
100
+ /** Refuses while a save is in flight, so a half-written row cannot be abandoned by accident. */
101
+ closeEditor: () => void
102
+ requestDelete: (record: T) => void
103
+ closeDelete: () => void
104
+ /**
105
+ * Put a saved row into the list.
106
+ *
107
+ * `replacing` is the row the editor was opened on, or null for a create.
108
+ * Passed rather than inferred so the behaviour matches what the five
109
+ * callers did: an edit replaces in place and a create appends and jumps to
110
+ * the page the new row landed on.
111
+ */
112
+ upsert: (record: T, replacing: T | null) => void
113
+ /** Drop a row and pull the page back if that emptied the last one. */
114
+ remove: (record: T) => void
115
+ /** Pull `page` back into range after the row count changes under it. */
116
+ clampPage: () => void
117
+ }
118
+
119
+ /**
120
+ * The state and lifecycle every list screen in the dashboard repeats.
121
+ *
122
+ * Five Delivery dashboards carried byte-identical copies of fourteen signals,
123
+ * `handleSearch`, `openCreate`, `openEdit`, `closeEditor`, `requestDelete`,
124
+ * `closeDelete`, and the page arithmetic around saving and deleting
125
+ * (stacksjs/stacks#2918). Five copies of an off-by-one is four chances to be
126
+ * right in four places and wrong in the fifth.
127
+ *
128
+ * Deliberately not here: fetching, toasts, and the search predicate. Those
129
+ * differ per screen, and a composable that owned them would be a framework
130
+ * each caller had to fight. The caller writes its own `load`, `save` and
131
+ * `delete`, and calls `upsert`, `remove` and `clampPage` from inside them.
132
+ */
133
+ export function useRecordsDashboard<T>(options: RecordsDashboardOptions<T>): RecordsDashboard<T> {
134
+ const sameRecord = options.sameRecord
135
+ ?? ((a: T, b: T) => (a as { id?: unknown }).id === (b as { id?: unknown }).id)
136
+
137
+ const records = state<T[]>([])
138
+ const loading = state(true)
139
+ const loadError = state('')
140
+ const search = state('')
141
+ const page = state(1)
142
+ const perPage = state(options.perPage ?? 10)
143
+ const editorOpen = state(false)
144
+ const editorMode = state<'create' | 'edit'>('create')
145
+ const editing = state<T | null>(null)
146
+ const saving = state(false)
147
+ const editorError = state('')
148
+ const deleting = state<T | null>(null)
149
+ const deleteBusy = state(false)
150
+ const deleteError = state('')
151
+
152
+ const filteredRecords = (): T[] => {
153
+ const query = search().trim().toLowerCase()
154
+ return query ? options.filter(records(), query) : records()
155
+ }
156
+
157
+ const pageCount = (): number => Math.max(1, Math.ceil(filteredRecords().length / perPage()))
158
+
159
+ const paginatedRecords = (): T[] => {
160
+ const start = (page() - 1) * perPage()
161
+ return filteredRecords().slice(start, start + perPage())
162
+ }
163
+
164
+ const clampPage = (): void => {
165
+ const lastPage = pageCount()
166
+ if (page() > lastPage)
167
+ page.set(lastPage)
168
+ }
169
+
170
+ const { previousPage, nextPage } = pageControls(page, pageCount)
171
+
172
+ const goToPage = (value: number): void => {
173
+ page.set(Math.min(Math.max(1, Math.trunc(value) || 1), pageCount()))
174
+ }
175
+
176
+ return {
177
+ records,
178
+ loading,
179
+ loadError,
180
+ search,
181
+ page,
182
+ perPage,
183
+ editorOpen,
184
+ editorMode,
185
+ editing,
186
+ saving,
187
+ editorError,
188
+ deleting,
189
+ deleteBusy,
190
+ deleteError,
191
+ filteredRecords,
192
+ paginatedRecords,
193
+ pageCount,
194
+ previousPage,
195
+ nextPage,
196
+ goToPage,
197
+
198
+ handleSearch(value: string): void {
199
+ search.set(value)
200
+ page.set(1)
201
+ },
202
+
203
+ openCreate(): void {
204
+ editing.set(null)
205
+ editorMode.set('create')
206
+ editorError.set('')
207
+ editorOpen.set(true)
208
+ },
209
+
210
+ openEdit(record: T): void {
211
+ editing.set(record)
212
+ editorMode.set('edit')
213
+ editorError.set('')
214
+ editorOpen.set(true)
215
+ },
216
+
217
+ closeEditor(): void {
218
+ if (saving())
219
+ return
220
+ editorOpen.set(false)
221
+ editing.set(null)
222
+ editorError.set('')
223
+ },
224
+
225
+ requestDelete(record: T): void {
226
+ deleting.set(record)
227
+ deleteError.set('')
228
+ },
229
+
230
+ closeDelete(): void {
231
+ if (deleteBusy())
232
+ return
233
+ deleting.set(null)
234
+ deleteError.set('')
235
+ },
236
+
237
+ upsert(record: T, replacing: T | null): void {
238
+ if (replacing) {
239
+ records.set(records().map(existing => sameRecord(existing, replacing) ? record : existing))
240
+ return
241
+ }
242
+ const next = [...records(), record]
243
+ records.set(next)
244
+ // Land on the page the new row is actually on, not the last page of the
245
+ // filtered view: a create while a search is active would otherwise jump
246
+ // somewhere the row is not.
247
+ page.set(Math.max(1, Math.ceil(next.length / perPage())))
248
+ },
249
+
250
+ remove(record: T): void {
251
+ records.set(records().filter(existing => !sameRecord(existing, record)))
252
+ clampPage()
253
+ },
254
+
255
+ clampPage,
256
+ }
257
+ }
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.231",
5
+ "version": "0.75.236",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.231",
5
+ "version": "0.75.236",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "@iconify-json/f7": "^1.2.2",
46
46
  "@iconify-json/hugeicons": "^1.2.35",
47
- "@stacksjs/mobile": "0.75.231",
47
+ "@stacksjs/mobile": "0.75.236",
48
48
  "@stacksjs/sanitizer": "^0.2.534",
49
49
  "@xterm/addon-fit": "^0.11.0",
50
50
  "@xterm/xterm": "^6.0.0",
@@ -0,0 +1,56 @@
1
+ // Hand-written, like `request-context.d.ts` beside it, and handed to
2
+ // `stx typecheck` with `--lib` by the `typecheck:views` script.
3
+ //
4
+ // What a `<script client>` block can reach from stx's DEMAND-INLINE
5
+ // composables (stacksjs/stacks#2918 follow-up). These are not ordinary
6
+ // auto-imports: stx scans a client script for the names it calls and inlines
7
+ // the implementation of just those, so they exist at runtime without an
8
+ // import and are invisible to any checker that is only given the module
9
+ // declarations. `node_modules/@stacksjs/stx/dist/composables/*.d.ts` declares
10
+ // them as module exports, which is the wrong shape for a global, so nothing
11
+ // typed them here.
12
+ //
13
+ // That is how `MembershipCard.stx` turned `typecheck:views` red while working
14
+ // correctly in a browser: it calls `useMouse({ target, resetOnLeave })` and
15
+ // subscribes, which is exactly the demand-inlined API, while the only
16
+ // `useMouse` a checker could see was the unrelated no-argument one in
17
+ // `browser-composables.d.ts`.
18
+ //
19
+ // Mirrors `dist/composables/use-mouse.d.ts` rather than inventing a shape. If
20
+ // stx changes it, this drifts, and the symptom is a type error on code that
21
+ // runs - so correct it here against that file rather than loosening it.
22
+
23
+ export {}
24
+
25
+ declare global {
26
+ interface StxMouseState {
27
+ x: number
28
+ y: number
29
+ pageX: number
30
+ pageY: number
31
+ clientX: number
32
+ clientY: number
33
+ movementX: number
34
+ movementY: number
35
+ buttons: number
36
+ isPressed: boolean
37
+ isInside: boolean
38
+ }
39
+
40
+ interface StxMouseOptions {
41
+ target?: Window | HTMLElement | null
42
+ touch?: boolean
43
+ resetOnLeave?: boolean
44
+ type?: 'page' | 'client' | 'screen' | 'movement'
45
+ }
46
+
47
+ interface StxMouseRef {
48
+ get: () => StxMouseState
49
+ /** Returns its own unsubscribe, which is what callers store. */
50
+ subscribe: (fn: (state: StxMouseState) => void) => () => void
51
+ stop: () => void
52
+ }
53
+
54
+ /** Reactive mouse or pointer position, globally or within a target. */
55
+ function useMouse(options?: StxMouseOptions): StxMouseRef
56
+ }
@@ -0,0 +1,29 @@
1
+ <script client>
2
+ /**
3
+ * The centred card the signed-out auth pages sit in.
4
+ *
5
+ * Login, Register, ForgotPassword and ResetPassword each carried this
6
+ * markup (stacksjs/stacks#2917): the same full-height centring, the same
7
+ * 24rem column, the same logo tile, and the same heading pair. They differ
8
+ * only in what goes below it, which is the slot.
9
+ *
10
+ * Four pages the user moves between in one flow is the worst place for a
11
+ * shell to drift, because the drift is visible as a jump between steps.
12
+ */
13
+ const title = useReactiveProp('title', '')
14
+ const subtitle = useReactiveProp('subtitle', '')
15
+ </script>
16
+
17
+ <div class="flex items-center justify-center px-4 py-12 min-h-screen">
18
+ <div class="max-w-sm w-full">
19
+ <div class="mb-8 text-center">
20
+ <div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl shadow-blue-500/25 shadow-lg">
21
+ <span class="h-7 w-7 text-white i-hugeicons-cube"></span>
22
+ </div>
23
+ <h1 class="font-semibold text-2xl text-neutral-900 dark:text-white">{{ title() }}</h1>
24
+ <p class="mt-2 text-neutral-500 text-sm dark:text-neutral-400">{{ subtitle() }}</p>
25
+ </div>
26
+
27
+ <slot />
28
+ </div>
29
+ </div>