@stacksjs/defaults 0.70.358 → 0.70.363

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 (141) hide show
  1. package/ai/skills/stacks-browse/SKILL.md +7 -5
  2. package/ai/skills/stacks-browse/scripts/browse.ts +68 -9
  3. package/ai/skills/stacks-dashboard/SKILL.md +4 -0
  4. package/app/Actions/AI/AskAction.ts +12 -17
  5. package/app/Actions/AI/SummaryAction.ts +12 -17
  6. package/app/Actions/Dashboard/Models/ModelShowAction.ts +4 -1
  7. package/app/Actions/Dashboard/Models/model-write.ts +5 -0
  8. package/app/Actions/LogAction.ts +11 -11
  9. package/ide/vscode/package.json +1 -1
  10. package/package.json +1 -1
  11. package/resources/components/Buttons/Counter.stx +1 -1
  12. package/resources/components/Buttons/ToggleDark.stx +1 -1
  13. package/resources/components/Dashboard/App/RequestDetailsDialog.stx +42 -0
  14. package/resources/components/Dashboard/App/RequestsOverview.stx +1 -21
  15. package/resources/components/Dashboard/Auth/AccessTokenCreateDialog.stx +155 -0
  16. package/resources/components/Dashboard/Auth/AccessTokens.stx +41 -129
  17. package/resources/components/Dashboard/Ci/CiDashboard.stx +1 -157
  18. package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +127 -0
  19. package/resources/components/Dashboard/Commerce/CommerceCategoryDeleteDialog.stx +10 -8
  20. package/resources/components/Dashboard/Commerce/CommerceCategoryDetailsDialog.stx +22 -17
  21. package/resources/components/Dashboard/Commerce/CommerceCategoryDialog.stx +14 -13
  22. package/resources/components/Dashboard/Commerce/CommerceCouponDeleteDialog.stx +10 -7
  23. package/resources/components/Dashboard/Commerce/CommerceCouponDetailsDialog.stx +25 -19
  24. package/resources/components/Dashboard/Commerce/CommerceCouponDialog.stx +14 -11
  25. package/resources/components/Dashboard/Commerce/CommerceCustomerDeleteDialog.stx +10 -8
  26. package/resources/components/Dashboard/Commerce/CommerceCustomerDetailsDialog.stx +24 -16
  27. package/resources/components/Dashboard/Commerce/CommerceCustomerDialog.stx +14 -13
  28. package/resources/components/Dashboard/Commerce/CommerceGiftCardDeleteDialog.stx +10 -8
  29. package/resources/components/Dashboard/Commerce/CommerceGiftCardDetailsDialog.stx +16 -14
  30. package/resources/components/Dashboard/Commerce/CommerceGiftCardDialog.stx +17 -16
  31. package/resources/components/Dashboard/Commerce/CommerceManufacturerDeleteDialog.stx +14 -16
  32. package/resources/components/Dashboard/Commerce/CommerceManufacturerDialog.stx +17 -16
  33. package/resources/components/Dashboard/Commerce/CommerceOrderDeleteDialog.stx +10 -8
  34. package/resources/components/Dashboard/Commerce/CommerceOrderDetailsDialog.stx +15 -10
  35. package/resources/components/Dashboard/Commerce/CommerceOrderDialog.stx +14 -13
  36. package/resources/components/Dashboard/Commerce/CommercePosCheckoutDialog.stx +30 -25
  37. package/resources/components/Dashboard/Commerce/CommercePosReceiptDialog.stx +27 -20
  38. package/resources/components/Dashboard/Commerce/CommerceProductDeleteDialog.stx +14 -14
  39. package/resources/components/Dashboard/Commerce/CommerceProductDialog.stx +14 -13
  40. package/resources/components/Dashboard/Commerce/CommerceReviewDeleteDialog.stx +10 -18
  41. package/resources/components/Dashboard/Commerce/CommerceReviewEditDialog.stx +17 -16
  42. package/resources/components/Dashboard/Commerce/CommerceTaxDeleteDialog.stx +14 -14
  43. package/resources/components/Dashboard/Commerce/CommerceTaxDialog.stx +17 -16
  44. package/resources/components/Dashboard/Commerce/CommerceTaxesTable.stx +1 -1
  45. package/resources/components/Dashboard/Commerce/CommerceUnitDeleteDialog.stx +14 -17
  46. package/resources/components/Dashboard/Commerce/CommerceUnitDialog.stx +17 -16
  47. package/resources/components/Dashboard/Commerce/CommerceVariantDeleteDialog.stx +10 -8
  48. package/resources/components/Dashboard/Commerce/CommerceVariantDialog.stx +17 -16
  49. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDeleteDialog.stx +11 -15
  50. package/resources/components/Dashboard/Commerce/Delivery/DeliveryRouteDialog.stx +17 -16
  51. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDeleteDialog.stx +11 -15
  52. package/resources/components/Dashboard/Commerce/Delivery/DigitalDeliveryDialog.stx +17 -16
  53. package/resources/components/Dashboard/Commerce/Delivery/DriverDeleteDialog.stx +11 -15
  54. package/resources/components/Dashboard/Commerce/Delivery/DriverDialog.stx +17 -16
  55. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDeleteDialog.stx +11 -15
  56. package/resources/components/Dashboard/Commerce/Delivery/LicenseKeyDialog.stx +17 -16
  57. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDeleteDialog.stx +11 -15
  58. package/resources/components/Dashboard/Commerce/Delivery/ShippingMethodDialog.stx +17 -16
  59. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDeleteDialog.stx +11 -15
  60. package/resources/components/Dashboard/Commerce/Delivery/ShippingRateDialog.stx +17 -16
  61. package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDeleteDialog.stx +11 -15
  62. package/resources/components/Dashboard/Commerce/Delivery/ShippingZoneDialog.stx +17 -16
  63. package/resources/components/Dashboard/Commerce/PaymentDetailsDialog.stx +16 -15
  64. package/resources/components/Dashboard/Commerce/PaymentRefundDialog.stx +16 -15
  65. package/resources/components/Dashboard/Commerce/PrintDeviceDeleteDialog.stx +10 -11
  66. package/resources/components/Dashboard/Commerce/PrintDeviceDetailsDialog.stx +16 -14
  67. package/resources/components/Dashboard/Commerce/PrintDeviceDialog.stx +17 -16
  68. package/resources/components/Dashboard/Commerce/PrintLogDeleteDialog.stx +10 -7
  69. package/resources/components/Dashboard/Commerce/PrintLogDetailsDialog.stx +13 -10
  70. package/resources/components/Dashboard/Commerce/ProductWaitlistDeleteDialog.stx +10 -18
  71. package/resources/components/Dashboard/Commerce/ProductWaitlistEntryDialog.stx +96 -105
  72. package/resources/components/Dashboard/Commerce/RestaurantWaitlistDeleteDialog.stx +10 -18
  73. package/resources/components/Dashboard/Commerce/RestaurantWaitlistEntryDialog.stx +92 -95
  74. package/resources/components/Dashboard/Content/AuthorEditorDialog.stx +94 -0
  75. package/resources/components/Dashboard/Content/AuthorsDashboard.stx +39 -31
  76. package/resources/components/Dashboard/Content/BlogDashboard.stx +87 -176
  77. package/resources/components/Dashboard/Content/BlogPostEditorDialog.stx +195 -0
  78. package/resources/components/Dashboard/Content/CommentsDashboard.stx +14 -4
  79. package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +40 -55
  80. package/resources/components/Dashboard/Content/CreateFolderDialog.stx +45 -0
  81. package/resources/components/Dashboard/Content/FileDetailsDrawer.stx +31 -0
  82. package/resources/components/Dashboard/Content/FileManagerDashboard.stx +47 -107
  83. package/resources/components/Dashboard/Content/FileUploadDialog.stx +89 -0
  84. package/resources/components/Dashboard/Content/PageEditorDialog.stx +93 -0
  85. package/resources/components/Dashboard/Content/PagesDashboard.stx +36 -26
  86. package/resources/components/Dashboard/Content/PostEditorDialog.stx +176 -0
  87. package/resources/components/Dashboard/Content/PostsDashboard.stx +40 -64
  88. package/resources/components/Dashboard/Content/SeoDashboard.stx +18 -84
  89. package/resources/components/Dashboard/Content/SeoEditorDialog.stx +108 -0
  90. package/resources/components/Dashboard/Content/TaxonomyEditorDialog.stx +106 -0
  91. package/resources/components/Dashboard/CreateRecordModal.stx +72 -83
  92. package/resources/components/Dashboard/Email/InboxMessageList.stx +1 -1
  93. package/resources/components/Dashboard/GlobalSearch.stx +3 -9
  94. package/resources/components/Dashboard/Infrastructure/CloudDetailsDialog.stx +2 -16
  95. package/resources/components/Dashboard/Infrastructure/DnsRecordDialog.stx +4 -16
  96. package/resources/components/Dashboard/Infrastructure/LogDetailsDialog.stx +4 -19
  97. package/resources/components/Dashboard/Infrastructure/MailboxDetailsDialog.stx +6 -16
  98. package/resources/components/Dashboard/Infrastructure/ServerInspectorDialog.stx +105 -0
  99. package/resources/components/Dashboard/Infrastructure/ServersDashboard.stx +5 -59
  100. package/resources/components/Dashboard/Jobs/JobHistory.stx +1 -11
  101. package/resources/components/Dashboard/Library/ComponentsDashboard.stx +11 -60
  102. package/resources/components/Dashboard/Library/FunctionsDashboard.stx +11 -15
  103. package/resources/components/Dashboard/Library/SourceScaffoldDialog.stx +95 -0
  104. package/resources/components/Dashboard/Management/AssignmentDrawer.stx +93 -0
  105. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +47 -129
  106. package/resources/components/Dashboard/Marketing/CampaignDeleteDialog.stx +11 -12
  107. package/resources/components/Dashboard/Marketing/CampaignDeliveryDialog.stx +5 -8
  108. package/resources/components/Dashboard/Marketing/CampaignDialog.stx +17 -17
  109. package/resources/components/Dashboard/Marketing/MarketingListDeleteDialog.stx +15 -15
  110. package/resources/components/Dashboard/Marketing/MarketingListDialog.stx +17 -17
  111. package/resources/components/Dashboard/Marketing/MarketingListsTable.stx +1 -1
  112. package/resources/components/Dashboard/Marketing/SocialPostDeleteDialog.stx +11 -12
  113. package/resources/components/Dashboard/Marketing/SocialPostDialog.stx +17 -16
  114. package/resources/components/Dashboard/Models/EditRecordModal.stx +81 -0
  115. package/resources/components/Dashboard/Models/ModelRecordActions.stx +30 -0
  116. package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +42 -94
  117. package/resources/components/Dashboard/Navbar.stx +5 -5
  118. package/resources/components/Dashboard/Notifications/NotificationDeliveryDialog.stx +40 -60
  119. package/resources/components/Dashboard/UI/Button.stx +4 -0
  120. package/resources/components/Dashboard/UI/ConfirmDialog.stx +55 -53
  121. package/resources/components/Dashboard/UI/Drawer.stx +114 -0
  122. package/resources/components/Dashboard/UI/Modal.stx +32 -21
  123. package/resources/components/Docs/Demo/ComboboxDemo.stx +4 -5
  124. package/resources/components/Docs/Demo/DropdownDemo.stx +12 -5
  125. package/resources/components/Docs/Demo/ListboxDemo.stx +14 -6
  126. package/resources/components/Docs/Demo/PopoverDemo.stx +10 -5
  127. package/resources/components/Docs/Demo/RadioGroupDemo.stx +14 -6
  128. package/resources/components/Docs/Demo/StepperDemo.stx +4 -5
  129. package/resources/components/Docs/TeamMember.stx +16 -9
  130. package/resources/components/Forum/ForumReplyForm.stx +7 -7
  131. package/resources/components/Forum/ForumSidebar.stx +10 -6
  132. package/resources/components/Marketing/Faqs.stx +5 -5
  133. package/resources/components/Marketing/Feature.stx +11 -6
  134. package/resources/components/Marketing/Layout.stx +12 -8
  135. package/resources/components/Marketing/Pricing.stx +11 -5
  136. package/resources/components/Marketing/PrimaryFeatures.stx +4 -5
  137. package/resources/components/Marketing/PrimaryFeatures2.stx +6 -5
  138. package/resources/components/Marketing/SecondaryFeatures.stx +10 -5
  139. package/resources/components/Marketing/Testimonials.stx +8 -5
  140. package/resources/views/index.stx +4 -4
  141. 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,16 +109,16 @@ 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
 
115
116
  ### Crawl (whole-site browser audit)
116
117
  ```bash
117
- bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts crawl <url> [--viewport 1280x900] [--max 500] [--path /extra-route] [--settle 350] [--progress]
118
+ bun storage/framework/defaults/ai/skills/stacks-browse/scripts/browse.ts crawl <url> [--viewport 1280x900] [--max 500] [--path /extra-route] [--settle 350] [--progress] [--summary]
118
119
  ```
119
- Uses a fresh isolated page target per route, relaunching Chromium if its DevTools
120
- session exits during a long audit, while
120
+ Uses a fresh isolated page target per route, waits for each target to fully close,
121
+ and relaunches Chromium if its DevTools session or target lifecycle fails during a long audit, while
121
122
  following every same-origin link it discovers.
122
123
  The viewport is deterministic and defaults to `1280x900`; pass `--viewport 768x1024`
123
124
  to repeat the same route audit at a tablet breakpoint.
@@ -125,7 +126,8 @@ Each page is checked for a non-200 document, console exceptions, failed
125
126
  requests, and horizontal overflow. Repeat `--path` to seed routes that are not
126
127
  linked from the starting page. The command prints every crawled path and exits
127
128
  nonzero when any page fails. Add `--progress` to stream each completed page
128
- during a long audit.
129
+ during a long audit. Add `--summary` to report totals and compact failure
130
+ diagnostics without printing every visited path or overflowing element.
129
131
 
130
132
  ## Stacks-Specific QA
131
133
 
@@ -264,12 +264,38 @@ async function createPage(port: number): Promise<BrowserPage> {
264
264
  }
265
265
  }
266
266
 
267
- async function closePage(port: number, page: BrowserPage): Promise<void> {
268
- page.cdp.close()
267
+ async function closePage(port: number, page: BrowserPage): Promise<boolean> {
269
268
  try {
270
- await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(page.targetId)}`)
269
+ const response = await fetch(`http://127.0.0.1:${port}/json/close/${encodeURIComponent(page.targetId)}`)
270
+ if (!response.ok) {
271
+ page.cdp.close()
272
+ return false
273
+ }
274
+ }
275
+ catch {
276
+ page.cdp.close()
277
+ return false
278
+ }
279
+ page.cdp.close()
280
+
281
+ // Chromium acknowledges target closure before every renderer/helper has
282
+ // actually left the target list. Creating the next target immediately can
283
+ // accumulate closing renderers across a large crawl, eventually dropping
284
+ // Network and Runtime events. Wait for the target to disappear so each route
285
+ // starts with the isolation the command promises.
286
+ for (let attempt = 0; attempt < 40; attempt++) {
287
+ try {
288
+ const targets = await (await fetch(`http://127.0.0.1:${port}/json/list`)).json() as any[]
289
+ if (!targets.some(target => target.id === page.targetId))
290
+ return true
291
+ }
292
+ catch {
293
+ return false
294
+ }
295
+ await Bun.sleep(25)
271
296
  }
272
- catch { /* the target or browser may already have closed */ }
297
+
298
+ return false
273
299
  }
274
300
 
275
301
  function kill(s: Session): void {
@@ -497,7 +523,7 @@ function parseViewport(value: string | undefined): { w: number, h: number } {
497
523
  return { w, h }
498
524
  }
499
525
 
500
- type ScenarioAction = 'assert' | 'click' | 'fill' | 'focus' | 'press' | 'wait'
526
+ type ScenarioAction = 'assert' | 'click' | 'evaluate' | 'fill' | 'focus' | 'press' | 'wait'
501
527
 
502
528
  interface ScenarioStep {
503
529
  action: ScenarioAction
@@ -505,6 +531,7 @@ interface ScenarioStep {
505
531
  text?: string
506
532
  absent?: boolean
507
533
  focused?: boolean
534
+ expression?: string
508
535
  value?: string
509
536
  key?: string
510
537
  ms?: number
@@ -524,7 +551,7 @@ function parseScenarioStep(value: string): ScenarioStep {
524
551
  throw new TypeError('Each scenario step must be a JSON object.')
525
552
 
526
553
  const step = parsed as Record<string, unknown>
527
- if (!['assert', 'click', 'fill', 'focus', 'press', 'wait'].includes(String(step.action)))
554
+ if (!['assert', 'click', 'evaluate', 'fill', 'focus', 'press', 'wait'].includes(String(step.action)))
528
555
  throw new TypeError(`Unsupported scenario action: ${String(step.action)}`)
529
556
 
530
557
  const action = step.action as ScenarioAction
@@ -532,6 +559,8 @@ function parseScenarioStep(value: string): ScenarioStep {
532
559
  throw new TypeError(`Scenario action "${action}" requires a selector.`)
533
560
  if (action === 'fill' && typeof step.value !== 'string')
534
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.')
535
564
  if (step.absent !== undefined && (action !== 'assert' || typeof step.absent !== 'boolean'))
536
565
  throw new TypeError('Scenario "absent" is a boolean supported only by assert actions.')
537
566
  if (step.focused !== undefined && (action !== 'assert' || typeof step.focused !== 'boolean'))
@@ -558,6 +587,17 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
558
587
  return { action: step.action, key, ok: true }
559
588
  }
560
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
+
561
601
  const result = await cdp.send('Runtime.evaluate', {
562
602
  expression: `(() => {
563
603
  const step = ${JSON.stringify(step)}
@@ -572,6 +612,8 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
572
612
  if (!element)
573
613
  return { ok: false, error: 'Element not found', selector: step.selector }
574
614
 
615
+ if (step.action === 'click' || step.action === 'focus')
616
+ element.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' })
575
617
  const rect = element.getBoundingClientRect()
576
618
  const style = getComputedStyle(element)
577
619
  const visible = rect.width > 0 && rect.height > 0 && style.visibility !== 'hidden' && style.display !== 'none'
@@ -586,6 +628,8 @@ async function runScenarioStep(cdp: Cdp, step: ScenarioStep): Promise<Record<str
586
628
  if (step.action === 'click') {
587
629
  if (element.disabled || element.getAttribute('aria-disabled') === 'true')
588
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 }
589
633
  }
590
634
  else if (step.action === 'focus') {
591
635
  if (typeof element.focus !== 'function')
@@ -864,7 +908,11 @@ async function main() {
864
908
  }
865
909
  }
866
910
  const replacePage = async (): Promise<void> => {
867
- await closePage(session.port, browserPage)
911
+ const closed = await closePage(session.port, browserPage)
912
+ if (!closed) {
913
+ kill(session)
914
+ session = await launch()
915
+ }
868
916
  browserPage = await createReplacementPage()
869
917
  cdp = browserPage.cdp
870
918
  }
@@ -1019,6 +1067,16 @@ async function main() {
1019
1067
  || page.consoleErrors.length > 0
1020
1068
  || page.failedRequests.length > 0
1021
1069
  || page.horizontalOverflowPx > 0)
1070
+ const summaryOnly = Boolean(flags.summary)
1071
+ const reportedFailures = summaryOnly
1072
+ ? failures.map(page => ({
1073
+ path: page.path,
1074
+ status: page.status,
1075
+ consoleErrorCount: page.consoleErrors.length,
1076
+ failedRequestCount: page.failedRequests.length,
1077
+ horizontalOverflowPx: page.horizontalOverflowPx,
1078
+ }))
1079
+ : failures
1022
1080
  console.log(JSON.stringify({
1023
1081
  start: start.href,
1024
1082
  browser: session.browser,
@@ -1026,8 +1084,9 @@ async function main() {
1026
1084
  crawled: pages.length,
1027
1085
  remaining: queue.length,
1028
1086
  max,
1029
- paths: pages.map(page => page.path),
1030
- failures,
1087
+ failureCount: failures.length,
1088
+ ...(!summaryOnly ? { paths: pages.map(page => page.path) } : {}),
1089
+ failures: reportedFailures,
1031
1090
  }, null, 2))
1032
1091
  if (failures.length > 0)
1033
1092
  process.exitCode = 1
@@ -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,16 +1,15 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { ask } from '@stacksjs/ai'
4
+ import { log } from '@stacksjs/logging'
5
+ import { response } from '@stacksjs/router'
3
6
  import { schema } from '@stacksjs/validation'
4
7
 
5
- // TODO: this should have been auto-generated
6
- interface Request {
7
- question: string
8
- }
9
-
10
8
  export default new Action({
11
9
  name: 'AiAskAction',
12
10
  description: 'Ask AI',
13
11
  method: 'POST',
12
+ apiResponse: true,
14
13
 
15
14
  validations: {
16
15
  question: {
@@ -19,22 +18,18 @@ export default new Action({
19
18
  },
20
19
  },
21
20
 
22
- async handle(request: Request) {
23
- try {
24
- const question = request.question
25
-
26
- console.log(`Question received: ${question}`)
21
+ async handle(request: RequestInstance) {
22
+ await request.validate()
23
+ const question = String(request.get('question') || '').trim()
27
24
 
28
- return {
25
+ try {
26
+ return response.json({
29
27
  data: await ask(question),
30
- }
28
+ })
31
29
  }
32
30
  catch (error) {
33
- console.error('Error:', error)
34
-
35
- return {
36
- error: 'An error occurred while processing your request.',
37
- }
31
+ log.error('[ai] Ask request failed', { error })
32
+ return response.json({ message: 'The AI provider could not answer the question.' }, 502)
38
33
  }
39
34
  },
40
35
  })
@@ -1,16 +1,15 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { summarize } from '@stacksjs/ai'
4
+ import { log } from '@stacksjs/logging'
5
+ import { response } from '@stacksjs/router'
3
6
  import { schema } from '@stacksjs/validation'
4
7
 
5
- // TODO: this should have been auto-generated
6
- interface Request {
7
- text: string
8
- }
9
-
10
8
  export default new Action({
11
9
  name: 'AiSummaryAction',
12
10
  description: 'Summary AI',
13
11
  method: 'POST',
12
+ apiResponse: true,
14
13
 
15
14
  validations: {
16
15
  text: {
@@ -19,22 +18,18 @@ export default new Action({
19
18
  },
20
19
  },
21
20
 
22
- async handle(request: Request) {
23
- try {
24
- const text = request.text
25
-
26
- console.log(`Text received: ${text}`)
21
+ async handle(request: RequestInstance) {
22
+ await request.validate()
23
+ const text = String(request.get('text') || '').trim()
27
24
 
28
- return {
25
+ try {
26
+ return response.json({
29
27
  data: await summarize(text),
30
- }
28
+ })
31
29
  }
32
30
  catch (error) {
33
- console.error('Error:', error)
34
-
35
- return {
36
- error: 'An error occurred while processing your request.',
37
- }
31
+ log.error('[ai] Summary request failed', { error })
32
+ return response.json({ message: 'The AI provider could not summarize the text.' }, 502)
38
33
  }
39
34
  },
40
35
  })
@@ -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)
@@ -1,15 +1,16 @@
1
+ import type { RequestInstance } from '@stacksjs/types'
1
2
  import { Action } from '@stacksjs/actions'
2
3
  import { log } from '@stacksjs/logging'
4
+ import { response } from '@stacksjs/router'
3
5
  import { schema } from '@stacksjs/validation'
4
6
 
5
- interface Request {
6
- message: string
7
- level: 'info' | 'warn' | 'error'
8
- }
7
+ type LogLevel = 'info' | 'warn' | 'error'
9
8
 
10
9
  export default new Action({
11
10
  name: 'Dummy Logger',
12
11
  description: 'This action is used to demo how to POST to a server and upon success, log a message.',
12
+ method: 'POST',
13
+ apiResponse: true,
13
14
 
14
15
  // the request object is optional, but if it is provided, it will be used for validation
15
16
  validations: {
@@ -29,14 +30,13 @@ export default new Action({
29
30
  },
30
31
  },
31
32
 
32
- // handle(request: { message: string, level: 'info' | 'warn' | 'error' }) {
33
- handle(request?: Request) {
34
- if (!request)
35
- return 'No request was provided.'
33
+ async handle(request: RequestInstance) {
34
+ await request.validate()
36
35
 
37
- // TODO: need to validate with ts-validation
38
- log[request.level](request.message)
36
+ const message = String(request.get('message') || '').trim()
37
+ const level = String(request.get('level')) as LogLevel
38
+ log[level](message)
39
39
 
40
- return `Logged "${request.message}" at "${request.level}" level`
40
+ return response.json({ level, message: `Logged at ${level} level.` })
41
41
  },
42
42
  })
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.70.358",
5
+ "version": "0.70.363",
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.358",
5
+ "version": "0.70.363",
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>