@stacksjs/defaults 0.70.362 → 0.70.365

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 (137) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +2 -1
  2. package/ai/skills/stacks-browse/scripts/browse.ts +20 -2
  3. package/ai/skills/stacks-dashboard/SKILL.md +4 -0
  4. package/app/Actions/Auth/LogoutAction.ts +11 -4
  5. package/app/Actions/Auth/LogoutAllAction.ts +8 -4
  6. package/app/Actions/Dashboard/Models/ModelShowAction.ts +4 -1
  7. package/app/Actions/Dashboard/Models/model-write.ts +5 -0
  8. package/ide/vscode/package.json +1 -1
  9. package/package.json +1 -1
  10. package/resources/components/Buttons/Counter.stx +1 -1
  11. package/resources/components/Buttons/ToggleDark.stx +1 -1
  12. package/resources/components/Dashboard/App/RequestDetailsDialog.stx +42 -0
  13. package/resources/components/Dashboard/App/RequestsOverview.stx +1 -21
  14. package/resources/components/Dashboard/Auth/AccessTokenCreateDialog.stx +155 -0
  15. package/resources/components/Dashboard/Auth/AccessTokens.stx +41 -129
  16. package/resources/components/Dashboard/Ci/CiDashboard.stx +1 -157
  17. package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +127 -0
  18. package/resources/components/Dashboard/Commerce/CommerceCategoryDeleteDialog.stx +10 -8
  19. package/resources/components/Dashboard/Commerce/CommerceCategoryDetailsDialog.stx +22 -17
  20. package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +14 -13
  21. package/resources/components/Dashboard/Commerce/CommerceCouponDeleteDialog.stx +10 -7
  22. package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +25 -19
  23. package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +14 -11
  24. package/resources/components/Dashboard/Commerce/CommerceCustomerDeleteDialog.stx +10 -8
  25. package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +24 -16
  26. package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +14 -13
  27. package/resources/components/Dashboard/Commerce/CommerceGiftCardDeleteDialog.stx +10 -8
  28. package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +16 -14
  29. package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +17 -16
  30. package/resources/components/Dashboard/Commerce/CommerceManufacturerDeleteDialog.stx +14 -16
  31. package/resources/components/Dashboard/Commerce/CommerceManufacturerDialog.stx +17 -16
  32. package/resources/components/Dashboard/Commerce/CommerceOrderDeleteDialog.stx +10 -8
  33. package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +15 -10
  34. package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +14 -13
  35. package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +30 -25
  36. package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +27 -20
  37. package/resources/components/Dashboard/Commerce/CommerceProductDeleteDialog.stx +14 -14
  38. package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +14 -13
  39. package/resources/components/Dashboard/Commerce/CommerceReviewDeleteDialog.stx +10 -18
  40. package/resources/components/Dashboard/Commerce/CommerceReviewEditDialog.stx +17 -16
  41. package/resources/components/Dashboard/Commerce/CommerceTaxDeleteDialog.stx +14 -14
  42. package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +17 -16
  43. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +1 -1
  44. package/resources/components/Dashboard/Commerce/CommerceUnitDeleteDialog.stx +14 -17
  45. package/resources/components/Dashboard/Commerce/CommerceUnitDialog.stx +17 -16
  46. package/resources/components/Dashboard/Commerce/CommerceVariantDeleteDialog.stx +10 -8
  47. package/resources/components/Dashboard/Commerce/CommerceVariantDialog.stx +17 -16
  48. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +11 -15
  49. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +17 -16
  50. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDeleteDialog.stx +11 -15
  51. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDialog.stx +17 -16
  52. package/resources/components/Dashboard/Commerce/Delivery/DriverDeleteDialog.stx +11 -15
  53. package/resources/components/Dashboard/Commerce/Delivery/DriverDialog.stx +17 -16
  54. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDeleteDialog.stx +11 -15
  55. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDialog.stx +17 -16
  56. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDeleteDialog.stx +11 -15
  57. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +17 -16
  58. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDeleteDialog.stx +11 -15
  59. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +17 -16
  60. package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDeleteDialog.stx +11 -15
  61. package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDialog.stx +17 -16
  62. package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +16 -15
  63. package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +16 -15
  64. package/resources/components/Dashboard/Commerce/PrintDeviceDeleteDialog.stx +10 -11
  65. package/resources/components/Dashboard/Commerce/PrintDeviceDetailsDialog.stx +16 -14
  66. package/resources/components/Dashboard/Commerce/PrintDeviceDialog.stx +17 -16
  67. package/resources/components/Dashboard/Commerce/PrintLogDeleteDialog.stx +10 -7
  68. package/resources/components/Dashboard/Commerce/PrintLogDetailsDialog.stx +13 -10
  69. package/resources/components/Dashboard/Commerce/ProductWaitlistDeleteDialog.stx +10 -18
  70. package/resources/components/Dashboard/Commerce/ProductWaitlistEntryDialog.stx +96 -105
  71. package/resources/components/Dashboard/Commerce/RestaurantWaitlistDeleteDialog.stx +10 -18
  72. package/resources/components/Dashboard/Commerce/RestaurantWaitlistEntryDialog.stx +92 -95
  73. package/resources/components/Dashboard/Content/AuthorEditorDialog.stx +94 -0
  74. package/resources/components/Dashboard/Content/AuthorsDashboard.stx +39 -31
  75. package/resources/components/Dashboard/Content/BlogDashboard.stx +87 -176
  76. package/resources/components/Dashboard/Content/BlogPostEditorDialog.stx +195 -0
  77. package/resources/components/Dashboard/Content/CommentsDashboard.stx +14 -4
  78. package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +40 -55
  79. package/resources/components/Dashboard/Content/CreateFolderDialog.stx +45 -0
  80. package/resources/components/Dashboard/Content/FileDetailsDrawer.stx +31 -0
  81. package/resources/components/Dashboard/Content/FileManagerDashboard.stx +47 -107
  82. package/resources/components/Dashboard/Content/FileUploadDialog.stx +89 -0
  83. package/resources/components/Dashboard/Content/PageEditorDialog.stx +93 -0
  84. package/resources/components/Dashboard/Content/PagesDashboard.stx +36 -26
  85. package/resources/components/Dashboard/Content/PostEditorDialog.stx +176 -0
  86. package/resources/components/Dashboard/Content/PostsDashboard.stx +40 -64
  87. package/resources/components/Dashboard/Content/SeoDashboard.stx +18 -84
  88. package/resources/components/Dashboard/Content/SeoEditorDialog.stx +108 -0
  89. package/resources/components/Dashboard/Content/TaxonomyEditorDialog.stx +106 -0
  90. package/resources/components/Dashboard/CreateRecordModal.stx +72 -83
  91. package/resources/components/Dashboard/Email/InboxMessageList.stx +1 -1
  92. package/resources/components/Dashboard/GlobalSearch.stx +3 -9
  93. package/resources/components/Dashboard/Infrastructure/CloudDetailsDialog.stx +2 -16
  94. package/resources/components/Dashboard/Infrastructure/DnsRecordDialog.stx +4 -16
  95. package/resources/components/Dashboard/Infrastructure/LogDetailsDialog.stx +4 -19
  96. package/resources/components/Dashboard/Infrastructure/MailboxDetailsDialog.stx +6 -16
  97. package/resources/components/Dashboard/Infrastructure/ServerInspectorDialog.stx +105 -0
  98. package/resources/components/Dashboard/Infrastructure/ServersDashboard.stx +5 -59
  99. package/resources/components/Dashboard/Jobs/JobHistory.stx +1 -11
  100. package/resources/components/Dashboard/Library/ComponentsDashboard.stx +11 -60
  101. package/resources/components/Dashboard/Library/FunctionsDashboard.stx +11 -15
  102. package/resources/components/Dashboard/Library/SourceScaffoldDialog.stx +95 -0
  103. package/resources/components/Dashboard/Management/AssignmentDrawer.stx +93 -0
  104. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +47 -129
  105. package/resources/components/Dashboard/Marketing/CampaignDeliveryDialog.stx +5 -8
  106. package/resources/components/Dashboard/Marketing/CampaignDialog.stx +17 -17
  107. package/resources/components/Dashboard/Marketing/MarketingListDialog.stx +17 -17
  108. package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +1 -1
  109. package/resources/components/Dashboard/Marketing/SocialPostDialog.stx +17 -16
  110. package/resources/components/Dashboard/Models/EditRecordModal.stx +81 -0
  111. package/resources/components/Dashboard/Models/ModelRecordActions.stx +30 -0
  112. package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +42 -94
  113. package/resources/components/Dashboard/Navbar.stx +5 -5
  114. package/resources/components/Dashboard/Notifications/NotificationDeliveryDialog.stx +40 -60
  115. package/resources/components/Dashboard/UI/Button.stx +4 -0
  116. package/resources/components/Dashboard/UI/ConfirmDialog.stx +4 -0
  117. package/resources/components/Dashboard/UI/Drawer.stx +114 -0
  118. package/resources/components/Dashboard/UI/Modal.stx +19 -12
  119. package/resources/components/Docs/Demo/ComboboxDemo.stx +4 -5
  120. package/resources/components/Docs/Demo/DropdownDemo.stx +12 -5
  121. package/resources/components/Docs/Demo/ListboxDemo.stx +14 -6
  122. package/resources/components/Docs/Demo/PopoverDemo.stx +10 -5
  123. package/resources/components/Docs/Demo/RadioGroupDemo.stx +14 -6
  124. package/resources/components/Docs/Demo/StepperDemo.stx +4 -5
  125. package/resources/components/Docs/TeamMember.stx +16 -9
  126. package/resources/components/Forum/ForumReplyForm.stx +7 -7
  127. package/resources/components/Forum/ForumSidebar.stx +10 -6
  128. package/resources/components/Marketing/Faqs.stx +5 -5
  129. package/resources/components/Marketing/Feature.stx +11 -6
  130. package/resources/components/Marketing/Layout.stx +12 -8
  131. package/resources/components/Marketing/Pricing.stx +11 -5
  132. package/resources/components/Marketing/PrimaryFeatures.stx +4 -5
  133. package/resources/components/Marketing/PrimaryFeatures2.stx +6 -5
  134. package/resources/components/Marketing/SecondaryFeatures.stx +10 -5
  135. package/resources/components/Marketing/Testimonials.stx +8 -5
  136. package/resources/views/index.stx +4 -4
  137. package/views/system-tray/index.stx +1 -1
@@ -101,6 +101,7 @@ Extracts headings, links (`text -> href`), buttons, forms (action + field count)
101
101
  bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts scenario <url> \
102
102
  --step '{"action":"click","selector":"button[data-open]"}' \
103
103
  --step '{"action":"focus","selector":"input[name=name]"}' \
104
+ --step '{"action":"evaluate","expression":"window.__stxDevtools?.stats()"}' \
104
105
  --step '{"action":"fill","selector":"input[name=name]","value":"Example"}' \
105
106
  --step '{"action":"click","selector":"button[type=submit]"}' \
106
107
  --step '{"action":"assert","selector":"main","text":"Saved"}' \
@@ -108,7 +109,7 @@ bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts scenari
108
109
  --out storage/framework/runtime/shots/scenario.png
109
110
  ```
110
111
 
111
- Steps run in order within one isolated browser page, preserving reactive STX state and SPA navigation. Supported actions are `click`, `fill`, `focus`, `press`, `wait`, and `assert`. Each step is a JSON object passed through a repeatable `--step` flag. Click and focus steps may include `text` to select the matching control from their CSS selector. The command reports every completed step, the final URL and page text, console messages, console exceptions, failed requests, and an optional screenshot. It exits nonzero when the scenario assertion, browser console, or network fails.
112
+ Steps run in order within one isolated browser page, preserving reactive STX state and SPA navigation. Supported actions are `click`, `fill`, `focus`, `press`, `wait`, `evaluate`, and `assert`. Each step is a JSON object passed through a repeatable `--step` flag. Click and focus steps may include `text` to select the matching control from their CSS selector. They scroll matched controls into view before interacting, including controls inside horizontal overflow regions. Evaluate steps run a JavaScript expression in the isolated page and return its serialized value, which is useful for inspecting STX signals and DevTools state between interactions. The command reports every completed step, the final URL and page text, console messages, console exceptions, failed requests, and an optional screenshot. It exits nonzero when the scenario assertion, evaluation, browser console, or network fails.
112
113
  Use `{"action":"assert","selector":"...","absent":true}` to verify that an element is missing or hidden after an interaction.
113
114
  Use `{"action":"assert","selector":"button","text":"Save","focused":true}` to verify keyboard focus and focus restoration.
114
115
 
@@ -523,7 +523,7 @@ function parseViewport(value: string | undefined): { w: number, h: number } {
523
523
  return { w, h }
524
524
  }
525
525
 
526
- type ScenarioAction = 'assert' | 'click' | 'fill' | 'focus' | 'press' | 'wait'
526
+ type ScenarioAction = 'assert' | 'click' | 'evaluate' | 'fill' | 'focus' | 'press' | 'wait'
527
527
 
528
528
  interface ScenarioStep {
529
529
  action: ScenarioAction
@@ -531,6 +531,7 @@ interface ScenarioStep {
531
531
  text?: string
532
532
  absent?: boolean
533
533
  focused?: boolean
534
+ expression?: string
534
535
  value?: string
535
536
  key?: string
536
537
  ms?: number
@@ -550,7 +551,7 @@ function parseScenarioStep(value: string): ScenarioStep {
550
551
  throw new TypeError('Each scenario step must be a JSON object.')
551
552
 
552
553
  const step = parsed as Record<string, unknown>
553
- if (!['assert', 'click', 'fill', 'focus', 'press', 'wait'].includes(String(step.action)))
554
+ if (!['assert', 'click', 'evaluate', 'fill', 'focus', 'press', 'wait'].includes(String(step.action)))
554
555
  throw new TypeError(`Unsupported scenario action: ${String(step.action)}`)
555
556
 
556
557
  const action = step.action as ScenarioAction
@@ -558,6 +559,8 @@ function parseScenarioStep(value: string): ScenarioStep {
558
559
  throw new TypeError(`Scenario action "${action}" requires a selector.`)
559
560
  if (action === 'fill' && typeof step.value !== 'string')
560
561
  throw new TypeError('Scenario action "fill" requires a string value.')
562
+ if (action === 'evaluate' && typeof step.expression !== 'string')
563
+ throw new TypeError('Scenario action "evaluate" requires an expression.')
561
564
  if (step.absent !== undefined && (action !== 'assert' || typeof step.absent !== 'boolean'))
562
565
  throw new TypeError('Scenario "absent" is a boolean supported only by assert actions.')
563
566
  if (step.focused !== undefined && (action !== 'assert' || typeof step.focused !== 'boolean'))
@@ -584,6 +587,17 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
584
587
  return { action: step.action, key, ok: true }
585
588
  }
586
589
 
590
+ if (step.action === 'evaluate') {
591
+ const result = await cdp.send('Runtime.evaluate', {
592
+ expression: step.expression,
593
+ returnByValue: true,
594
+ awaitPromise: true,
595
+ })
596
+ if (result.exceptionDetails)
597
+ throw new Error(result.exceptionDetails.exception?.description || result.exceptionDetails.text || 'Evaluation failed')
598
+ return { action: step.action, value: result.result?.value, ok: true }
599
+ }
600
+
587
601
  const result = await cdp.send('Runtime.evaluate', {
588
602
  expression: `(() => {
589
603
  const step = ${JSON.stringify(step)}
@@ -598,6 +612,8 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
598
612
  if (!element)
599
613
  return { ok: false, error: 'Element not found', selector: step.selector }
600
614
 
615
+ if (step.action === 'click' || step.action === 'focus')
616
+ element.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' })
601
617
  const rect = element.getBoundingClientRect()
602
618
  const style = getComputedStyle(element)
603
619
  const visible = rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none'
@@ -612,6 +628,8 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
612
628
  if (step.action === 'click') {
613
629
  if (element.disabled || element.getAttribute('aria-disabled') === 'true')
614
630
  return { ok: false, error: 'Element is disabled', selector: step.selector }
631
+ if (rect.right <= 0 || rect.bottom <= 0 || rect.left >= innerWidth || rect.top >= innerHeight)
632
+ return { ok: false, error: 'Element could not be scrolled into the viewport', selector: step.selector }
615
633
  }
616
634
  else if (step.action === 'focus') {
617
635
  if (typeof element.focus !== 'function')
@@ -153,6 +153,9 @@ is hidden. Do not create separate `/deployments/scripts` or
153
153
  - Use `tag="a"` whenever `href` is reactive, for example
154
154
  `<Button tag="a" :href="detailsPath()">Open details</Button>`. Server rendering
155
155
  cannot infer an anchor from a client-only reactive URL.
156
+ - When a submit action lives in a shared Modal footer, give the form a stable
157
+ `id` and associate the action with `<Button type="submit" form="form-id">`.
158
+ Do not duplicate the footer inside the form or use script-driven submission.
156
159
  - Prefer component events and named slots over string callback props or
157
160
  `data-action` markers. A `data-action` attribute is only valid when an active
158
161
  host integration consumes that exact action.
@@ -325,6 +328,7 @@ the canonical primary action style across the dashboard.
325
328
  </Button>
326
329
 
327
330
  <Button :loading="saving()" type="submit">Save changes</Button>
331
+ <Button :loading="saving()" type="submit" form="settings-form">Save from modal footer</Button>
328
332
  <Button variant="secondary" @click="close">Cancel</Button>
329
333
  <Button :loading="deleting()" variant="danger" @click="destroy">Delete</Button>
330
334
  <Button tag="a" :href="exportHref()" :download="exportFilename()">Export</Button>
@@ -1,5 +1,5 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { Auth } from '@stacksjs/auth'
2
+ import { Auth, clearAuthCookie } from '@stacksjs/auth'
3
3
  import { response } from '@stacksjs/router'
4
4
 
5
5
  export default new Action({
@@ -7,10 +7,17 @@ export default new Action({
7
7
  description: 'Logout from the application',
8
8
  method: 'POST',
9
9
  async handle() {
10
+ // `Auth.logout()` resolves the token from the Authorization header or the
11
+ // auth cookie, so this revokes the session either way.
10
12
  await Auth.logout()
11
13
 
12
- return response.json({
13
- message: 'Successfully logged out',
14
- })
14
+ // Clearing is separate from revoking, and both are needed. Revoking alone
15
+ // leaves the browser sending a dead cookie on every request; clearing alone
16
+ // leaves a copied cookie valid for the token's whole lifetime, which would
17
+ // make "log out on a shared computer" mean only "hide the key".
18
+ return response.json(
19
+ { message: 'Successfully logged out' },
20
+ { headers: { 'Set-Cookie': clearAuthCookie() } },
21
+ )
15
22
  },
16
23
  })
@@ -1,5 +1,5 @@
1
1
  import { Action } from '@stacksjs/actions'
2
- import { revokeAllTokens, sessionDestroyAll } from '@stacksjs/auth'
2
+ import { clearAuthCookie, revokeAllTokens, sessionDestroyAll } from '@stacksjs/auth'
3
3
  import { response } from '@stacksjs/router'
4
4
 
5
5
  export default new Action({
@@ -21,8 +21,12 @@ export default new Action({
21
21
  await revokeAllTokens(Number(user.id))
22
22
  await sessionDestroyAll(Number(user.id))
23
23
 
24
- return response.json({
25
- message: 'Successfully logged out from all devices',
26
- })
24
+ // This device included: its cookie carries one of the tokens just revoked,
25
+ // so leaving it in place means the browser keeps presenting a dead
26
+ // credential on every request.
27
+ return response.json(
28
+ { message: 'Successfully logged out from all devices' },
29
+ { headers: { 'Set-Cookie': clearAuthCookie() } },
30
+ )
27
31
  },
28
32
  })
@@ -3,7 +3,7 @@ import { Action } from '@stacksjs/actions'
3
3
  import { response as routerResponse } from '@stacksjs/router'
4
4
  import { loadModelIfExists, safeGet } from '../../../../resources/functions/dashboard/data'
5
5
  import { dashboardOperationalError } from '../dashboard-response'
6
- import { isValidModelSlug, type ModelCreateField, type ModelWriteCapabilities, modelCreateFields, modelSchemaColumns, modelWriteCapabilities, slugToPascal } from './model-write'
6
+ import { isValidModelSlug, type ModelCreateField, type ModelWriteCapabilities, modelCreateFields, modelSchemaColumns, modelWritableColumns, modelWriteCapabilities, slugToPascal } from './model-write'
7
7
 
8
8
  /**
9
9
  * `GET /api/dashboard/models/{slug}` (stacksjs/stacks#1838).
@@ -53,6 +53,7 @@ interface ResponseShape {
53
53
  writable: boolean
54
54
  writeCapabilities: ModelWriteCapabilities
55
55
  createFields: ModelCreateField[]
56
+ updateColumns: string[]
56
57
  error: string | null
57
58
  }
58
59
 
@@ -214,6 +215,7 @@ export default new Action({
214
215
  writable: false,
215
216
  writeCapabilities: { create: false, update: false, destroy: false },
216
217
  createFields: [],
218
+ updateColumns: [],
217
219
  error: null,
218
220
  }
219
221
 
@@ -296,6 +298,7 @@ export default new Action({
296
298
  response.writeCapabilities = modelWriteCapabilities(Model)
297
299
  response.writable = Object.values(response.writeCapabilities).some(Boolean)
298
300
  response.createFields = response.writeCapabilities.create ? modelCreateFields(Model) : []
301
+ response.updateColumns = response.writeCapabilities.update ? modelWritableColumns(Model) : []
299
302
  response.displayColumns = response.columns.filter(col => !col.startsWith('_'))
300
303
  response.columnMeta = response.displayColumns.map(name => ({
301
304
  name,
@@ -204,6 +204,11 @@ export function modelCreateFields(Model: any): ModelCreateField[] {
204
204
  })
205
205
  }
206
206
 
207
+ /** Physical database columns accepted by the model's generic update API. */
208
+ export function modelWritableColumns(Model: any): string[] {
209
+ return modelCreateFields(Model).map(field => snakeCase(field.name))
210
+ }
211
+
207
212
  function normalizeModelValue(type: ModelCreateField['type'], value: unknown): unknown {
208
213
  if (type === 'number') {
209
214
  if (value === '' || value === null || value === undefined)
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.70.362",
5
+ "version": "0.70.365",
6
6
  "description": "A modern Stacks development environment.",
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.70.362",
5
+ "version": "0.70.365",
6
6
  "description": "The complete managed Stacks application scaffold, including runtime defaults, AI guidance, editor metadata, and npm-backed project support files.",
7
7
  "author": "Chris Breuer",
8
8
  "license": "MIT",
@@ -24,5 +24,5 @@ document.querySelector('[data-counter]')?.addEventListener('click', function() {
24
24
  if (countEl) {
25
25
  countEl.textContent = parseInt(countEl.textContent || '0') + 1
26
26
  }
27
- }
27
+ })
28
28
  </script>
@@ -7,5 +7,5 @@
7
7
  <script client>
8
8
  document.querySelector('[data-toggle-dark]')?.addEventListener('click', function() {
9
9
  document.documentElement.classList.toggle('dark')
10
- }
10
+ })
11
11
  </script>
@@ -0,0 +1,42 @@
1
+ <script client>
2
+ interface RequestRow {
3
+ id: number
4
+ method: string
5
+ path: string
6
+ status: number
7
+ durationMs: number
8
+ ipAddress: string
9
+ memoryUsageMb: number
10
+ userAgent: string
11
+ errorMessage: string
12
+ createdAt: string
13
+ }
14
+
15
+ const request = useReactiveProp('request', null as RequestRow | null)
16
+ const emit = defineEmits()
17
+
18
+ function formatDate(value: string | undefined): string {
19
+ if (!value)
20
+ return 'Unknown'
21
+ const date = new Date(value)
22
+ return Number.isNaN(date.getTime()) ? 'Unknown' : date.toLocaleString()
23
+ }
24
+ </script>
25
+
26
+ <Modal
27
+ :isOpen="Boolean(request())"
28
+ :title="request() ? 'Request #' + request()?.id : 'Request details'"
29
+ size="lg"
30
+ @close="emit('close')"
31
+ >
32
+ <p class="mb-5 break-all font-mono text-neutral-500 text-sm dark:text-neutral-400">{{ request()?.method }} {{ request()?.path }}</p>
33
+ <dl class="grid gap-4 grid-cols-1 sm:grid-cols-2 text-sm">
34
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Status</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ request()?.status }}</dd></div>
35
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Duration</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ request()?.durationMs?.toFixed(1) || '0.0' }} ms</dd></div>
36
+ <div><dt class="text-neutral-500 dark:text-neutral-400">IP address</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ request()?.ipAddress || 'Not recorded' }}</dd></div>
37
+ <div><dt class="text-neutral-500 dark:text-neutral-400">Memory usage</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ request()?.memoryUsageMb || 0 }} MB</dd></div>
38
+ <div class="sm:col-span-2"><dt class="text-neutral-500 dark:text-neutral-400">Recorded</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ formatDate(request()?.createdAt) }}</dd></div>
39
+ <div class="sm:col-span-2"><dt class="text-neutral-500 dark:text-neutral-400">User agent</dt><dd class="mt-1 break-words text-neutral-900 dark:text-white">{{ request()?.userAgent || 'Not recorded' }}</dd></div>
40
+ <div class="sm:col-span-2"><dt class="text-neutral-500 dark:text-neutral-400">Error</dt><dd class="mt-1 break-words text-neutral-900 dark:text-white">{{ request()?.errorMessage || 'No error recorded' }}</dd></div>
41
+ </dl>
42
+ </Modal>
@@ -229,25 +229,5 @@ onMount(() => {
229
229
  </div>
230
230
  </Card>
231
231
 
232
- <div :if="selectedRequest()" class="flex fixed inset-0 z-50 items-center justify-center p-4 bg-black/50 dashboard-modal-layer" @click.self="closeInspector">
233
- <div role="dialog" aria-modal="true" aria-labelledby="request-detail-title" class="p-6 max-w-2xl w-full bg-white dark:bg-neutral-900 border border-neutral-200 rounded-xl dark:border-neutral-700 shadow-xl">
234
- <div class="flex gap-4 items-start justify-between">
235
- <div>
236
- <h2 id="request-detail-title" class="font-semibold text-lg text-neutral-900 dark:text-white">Request #{{ selectedRequest()?.id }}</h2>
237
- <p class="mt-1 break-all font-mono text-neutral-500 text-sm dark:text-neutral-400">{{ selectedRequest()?.method }} {{ selectedRequest()?.path }}</p>
238
- </div>
239
- <Button variant="ghost" iconOnly ariaLabel="Close request details" @click="closeInspector">
240
- <span class="h-5 w-5 i-hugeicons-cancel-01"></span>
241
- </Button>
242
- </div>
243
- <dl class="grid gap-4 grid-cols-1 sm:grid-cols-2 mt-6 text-sm">
244
- <div><dt class="text-neutral-500 dark:text-neutral-400">Status</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedRequest()?.status }}</dd></div>
245
- <div><dt class="text-neutral-500 dark:text-neutral-400">Duration</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedRequest()?.durationMs.toFixed(1) }} ms</dd></div>
246
- <div><dt class="text-neutral-500 dark:text-neutral-400">IP address</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedRequest()?.ipAddress || 'Not recorded' }}</dd></div>
247
- <div><dt class="text-neutral-500 dark:text-neutral-400">Memory usage</dt><dd class="mt-1 font-medium text-neutral-900 dark:text-white">{{ selectedRequest()?.memoryUsageMb || 0 }} MB</dd></div>
248
- <div class="sm:col-span-2"><dt class="text-neutral-500 dark:text-neutral-400">User agent</dt><dd class="mt-1 break-words text-neutral-900 dark:text-white">{{ selectedRequest()?.userAgent || 'Not recorded' }}</dd></div>
249
- <div class="sm:col-span-2"><dt class="text-neutral-500 dark:text-neutral-400">Error</dt><dd class="mt-1 break-words text-neutral-900 dark:text-white">{{ selectedRequest()?.errorMessage || 'No error recorded' }}</dd></div>
250
- </dl>
251
- </div>
252
- </div>
232
+ <RequestDetailsDialog :request="selectedRequest()" @close="closeInspector" />
253
233
  </div>
@@ -0,0 +1,155 @@
1
+ <script client>
2
+ interface ScopeOption {
3
+ name: string
4
+ description: string
5
+ }
6
+
7
+ interface TokenDraft {
8
+ name: string
9
+ scopes: string[]
10
+ expiresInMinutes: number
11
+ }
12
+
13
+ const availableScopes: ScopeOption[] = [
14
+ { name: 'read', description: 'Read application resources' },
15
+ { name: 'write', description: 'Create and update resources' },
16
+ { name: 'delete', description: 'Delete application resources' },
17
+ { name: 'deploy', description: 'Run deployment operations' },
18
+ { name: 'admin', description: 'Use administrative operations' },
19
+ ]
20
+
21
+ const open = useReactiveProp('open', false)
22
+ const busy = useReactiveProp('busy', false)
23
+ const createdToken = useReactiveProp('createdToken', '')
24
+ const message = useReactiveProp('message', '')
25
+ const error = useReactiveProp('error', '')
26
+ const tokenName = state('')
27
+ const expiryMinutes = state('10080')
28
+ const selectedScopes = state<string[]>(['read'])
29
+ const localError = state('')
30
+ const copied = state(false)
31
+ const emit = defineEmits()
32
+ let wasOpen = false
33
+
34
+ effect(() => {
35
+ const isOpen = open()
36
+ if (isOpen && !wasOpen) {
37
+ tokenName.set('')
38
+ expiryMinutes.set('10080')
39
+ selectedScopes.set(['read'])
40
+ localError.set('')
41
+ copied.set(false)
42
+ }
43
+ wasOpen = isOpen
44
+ })
45
+
46
+ function toggleScope(event: Event, scope: string): void {
47
+ const enabled = (event.target as HTMLInputElement).checked
48
+ selectedScopes.set(enabled
49
+ ? [...new Set([...selectedScopes(), scope])]
50
+ : selectedScopes().filter(item => item !== scope))
51
+ }
52
+
53
+ function submit(): void {
54
+ localError.set('')
55
+ if (!tokenName().trim()) {
56
+ localError.set('Enter a name for this token.')
57
+ return
58
+ }
59
+ if (selectedScopes().length === 0) {
60
+ localError.set('Select at least one scope.')
61
+ return
62
+ }
63
+
64
+ const draft: TokenDraft = {
65
+ name: tokenName().trim(),
66
+ scopes: [...selectedScopes()],
67
+ expiresInMinutes: Number(expiryMinutes()),
68
+ }
69
+ emit('submit', draft)
70
+ }
71
+
72
+ function close(): void {
73
+ if (!busy())
74
+ emit('close')
75
+ }
76
+
77
+ async function copyToken(): Promise<void> {
78
+ localError.set('')
79
+ try {
80
+ await globalThis.navigator.clipboard.writeText(createdToken())
81
+ copied.set(true)
82
+ }
83
+ catch {
84
+ localError.set('Clipboard access is unavailable. Select and copy the token manually.')
85
+ }
86
+ }
87
+ </script>
88
+
89
+ <Modal
90
+ :isOpen="open()"
91
+ title="Create Access Token"
92
+ description="Grant only the scopes this integration needs."
93
+ :closable="!busy()"
94
+ @close="close"
95
+ >
96
+ <div :if="createdToken()" class="space-y-4">
97
+ <div role="status" class="px-4 py-3 text-amber-800 text-sm dark:text-amber-200 bg-amber-50 dark:bg-amber-950/40 border border-amber-200 rounded-lg dark:border-amber-900">
98
+ {{ message() }}
99
+ </div>
100
+ <label class="block">
101
+ <span class="font-medium text-neutral-700 text-sm dark:text-neutral-300">New token</span>
102
+ <textarea readonly rows="4" aria-label="New access token" class="block mt-2 p-3 w-full font-mono text-neutral-900 text-xs dark:text-white bg-neutral-50 dark:bg-neutral-950 border border-neutral-300 rounded-lg dark:border-neutral-700">{{ createdToken() }}</textarea>
103
+ </label>
104
+ </div>
105
+
106
+ <form :if="!createdToken()" id="access-token-create-form" class="space-y-5" @submit.prevent="submit">
107
+ <label class="block">
108
+ <span class="font-medium text-neutral-700 text-sm dark:text-neutral-300">Token name</span>
109
+ <input x-model="tokenName" name="name" type="text" maxlength="255" required placeholder="CI deployment" class="block mt-2 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-neutral-950 border border-neutral-300 rounded-lg dark:border-neutral-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20">
110
+ </label>
111
+
112
+ <label class="block">
113
+ <span class="font-medium text-neutral-700 text-sm dark:text-neutral-300">Expires after</span>
114
+ <select x-model="expiryMinutes" name="expires_in_minutes" class="block mt-2 px-3 py-2 w-full text-neutral-900 text-sm dark:text-white bg-white dark:bg-neutral-950 border border-neutral-300 rounded-lg dark:border-neutral-700 focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500/20">
115
+ <option value="60">1 hour</option>
116
+ <option value="1440">1 day</option>
117
+ <option value="10080">7 days</option>
118
+ <option value="43200">30 days</option>
119
+ </select>
120
+ </label>
121
+
122
+ <fieldset>
123
+ <legend class="font-medium text-neutral-700 text-sm dark:text-neutral-300">Scopes</legend>
124
+ <div class="grid gap-2 grid-cols-1 sm:grid-cols-2 mt-2">
125
+ <label :for="scope in availableScopes" class="flex gap-3 items-start p-3 border border-neutral-200 rounded-lg dark:border-neutral-700">
126
+ <input name="scopes" type="checkbox" :value="scope.name" :checked="selectedScopes().includes(scope.name)" class="mt-0.5 h-4 w-4 text-blue-600 rounded" @change="toggleScope($event, scope.name)">
127
+ <span>
128
+ <span class="block font-medium text-neutral-900 text-sm dark:text-white">{{ scope.name }}</span>
129
+ <span class="block mt-0.5 text-neutral-500 text-xs dark:text-neutral-400">{{ scope.description }}</span>
130
+ </span>
131
+ </label>
132
+ </div>
133
+ </fieldset>
134
+ </form>
135
+
136
+ <div :if="localError() || error()" role="alert" class="mt-4 px-3 py-2 text-red-700 text-sm dark:text-red-300 bg-red-50 dark:bg-red-950/40 rounded-lg">
137
+ {{ localError() || error() }}
138
+ </div>
139
+
140
+ <template #footer>
141
+ <div :if="createdToken()" class="flex gap-2 justify-end w-full">
142
+ <Button variant="secondary" @click="close">Done</Button>
143
+ <Button @click="copyToken">
144
+ <span class="h-4 w-4 i-hugeicons-copy-01"></span>
145
+ {{ copied() ? 'Copied' : 'Copy Token' }}
146
+ </Button>
147
+ </div>
148
+ <div :if="!createdToken()" class="flex gap-2 justify-end w-full">
149
+ <Button variant="secondary" :disabled="busy()" @click="close">Cancel</Button>
150
+ <Button type="submit" form="access-token-create-form" :loading="busy()">
151
+ {{ busy() ? 'Creating...' : 'Create Token' }}
152
+ </Button>
153
+ </div>
154
+ </template>
155
+ </Modal>