@stacksjs/defaults 0.72.97 → 0.72.99

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 (87) hide show
  1. package/ai/skills/stacks-auth/SKILL.md +35 -5
  2. package/ai/skills/stacks-config/SKILL.md +11 -1
  3. package/ai/skills/stacks-desktop/SKILL.md +46 -0
  4. package/ai/skills/stacks-env/SKILL.md +26 -0
  5. package/ai/skills/stacks-events/SKILL.md +76 -60
  6. package/ai/skills/stacks-jobs/SKILL.md +3 -0
  7. package/ai/skills/stacks-listeners/SKILL.md +39 -9
  8. package/ai/skills/stacks-middleware/SKILL.md +50 -12
  9. package/ai/skills/stacks-queue/SKILL.md +5 -0
  10. package/ai/skills/stacks-router/SKILL.md +17 -7
  11. package/ai/skills/stacks-scheduler/SKILL.md +1 -1
  12. package/app/Actions/Dashboard/Commerce/CommerceCustomersAction.ts +1 -1
  13. package/app/Actions/Dashboard/Commerce/CommerceDeliveryAction.ts +1 -1
  14. package/app/Actions/Dashboard/Commerce/CommerceOrdersAction.ts +1 -1
  15. package/app/Actions/Dashboard/Commerce/CommercePosAction.ts +1 -1
  16. package/app/Actions/Dashboard/Commerce/CommercePosCheckoutAction.ts +1 -1
  17. package/app/Actions/Dashboard/Commerce/CommerceProductDetailAction.ts +1 -1
  18. package/app/Actions/Dashboard/Commerce/CommerceProductsAction.ts +1 -1
  19. package/app/Actions/Dashboard/Content/content-input.ts +2 -2
  20. package/app/Actions/Dashboard/Content/post-input.ts +4 -4
  21. package/app/Actions/Dashboard/Email/inbox-request.ts +1 -1
  22. package/app/Actions/Dashboard/Email/mail-preference.ts +1 -1
  23. package/app/Actions/Dashboard/Marketing/CampaignIndexAction.ts +1 -1
  24. package/app/Actions/Dashboard/Marketing/CampaignStoreAction.ts +1 -1
  25. package/app/Actions/Dashboard/Marketing/CampaignUpdateAction.ts +1 -1
  26. package/app/Actions/Dashboard/Teams/TeamInvitationDestroyAction.ts +2 -2
  27. package/app/Actions/Dashboard/Teams/TeamInvitationResendAction.ts +2 -2
  28. package/app/Actions/Dashboard/Teams/TeamInviteAction.ts +5 -5
  29. package/app/Actions/Dashboard/Teams/TeamMemberDestroyAction.ts +2 -2
  30. package/app/Actions/Dashboard/Teams/TeamMemberUpdateAction.ts +2 -2
  31. package/app/Actions/Dashboard/Teams/TeamPeopleIndexAction.ts +2 -2
  32. package/app/Actions/Dashboard/Teams/team-invitation-delivery.ts +2 -2
  33. package/app/Events.ts +9 -4
  34. package/app/Gates.ts +83 -85
  35. package/app/Listener.ts +34 -81
  36. package/app/Middleware.ts +22 -7
  37. package/ide/vscode/package.json +1 -1
  38. package/package.json +2 -2
  39. package/project/storage/framework/server/tsconfig.docker.json +1 -2
  40. package/resources/components/Buttons/Counter.stx +30 -21
  41. package/resources/components/Dashboard/Analytics/WebAnalyticsDashboard.stx +1 -3
  42. package/resources/components/Dashboard/Auth/ForgotPasswordDashboard.stx +1 -1
  43. package/resources/components/Dashboard/Auth/LoginDashboard.stx +2 -1
  44. package/resources/components/Dashboard/Auth/RegisterDashboard.stx +2 -1
  45. package/resources/components/Dashboard/Auth/ResetPasswordDashboard.stx +1 -1
  46. package/resources/components/Dashboard/Ci/CiDashboard.stx +9 -9
  47. package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +7 -7
  48. package/resources/components/Dashboard/Commerce/CommerceOverviewCharts.stx +2 -1
  49. package/resources/components/Dashboard/Commerce/CommercePaymentsDashboard.stx +1 -1
  50. package/resources/components/Dashboard/Content/BlogDashboard.stx +37 -15
  51. package/resources/components/Dashboard/Content/ContentDashboard.stx +3 -26
  52. package/resources/components/Dashboard/Email/EmailActivityDashboard.stx +2 -15
  53. package/resources/components/Dashboard/Environment/EnvironmentDashboard.stx +9 -1
  54. package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +15 -13
  55. package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +10 -8
  56. package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +10 -10
  57. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +3 -3
  58. package/resources/components/Dashboard/Modals/Popups/Alert.stx +4 -1
  59. package/resources/components/Dashboard/Navbar.stx +1 -1
  60. package/resources/components/Dashboard/Settings/AppearanceSettingsDashboard.stx +11 -6
  61. package/resources/components/Dashboard/Settings/SettingsDashboard.stx +2 -2
  62. package/resources/components/Dashboard/Transaction/index.stx +6 -6
  63. package/resources/components/Dashboard/UI/Avatar.stx +2 -2
  64. package/resources/components/Dashboard/UI/DropdownItem.stx +4 -4
  65. package/resources/components/Dashboard/UI/WindowControls.stx +17 -2
  66. package/resources/components/Docs/Demo/ComboboxDemo.stx +8 -3
  67. package/resources/components/Docs/Demo/DropdownDemo.stx +3 -3
  68. package/resources/components/Docs/Demo/StepperDemo.stx +1 -1
  69. package/resources/components/Forum/ForumLayout.stx +1 -1
  70. package/resources/components/Forum/ForumReplyForm.stx +1 -1
  71. package/resources/components/Marketing/ComingSoon.stx +5 -5
  72. package/resources/components/Storefront/CartDrawer.stx +44 -22
  73. package/resources/components/Storefront/ProductGallery.stx +3 -2
  74. package/resources/components/Storefront/QuantitySelector.stx +12 -7
  75. package/resources/functions/storefront/cart-cookie.ts +58 -0
  76. package/resources/layouts/storefront.stx +3 -2
  77. package/resources/views/cart.stx +19 -3
  78. package/resources/views/checkout/contact.stx +14 -3
  79. package/resources/views/checkout/payment.stx +14 -3
  80. package/resources/views/checkout/shipping.stx +14 -3
  81. package/resources/views/cms/blocks/form.stx +16 -11
  82. package/resources/views/cms/page.stx +28 -0
  83. package/resources/views/coming-soon.stx +0 -6
  84. package/resources/views/orders/[id].stx +19 -3
  85. package/views/dashboard/composables/index.ts +1 -1
  86. package/views/dashboard/composables/useChart.ts +26 -14
  87. package/views/dashboard/layouts/default.stx +42 -25
@@ -63,24 +63,44 @@
63
63
 
64
64
  <script data-stx-scoped>
65
65
  ;(() => {
66
+ /*
67
+ * The two properties this block puts on `window`, named in one place.
68
+ *
69
+ * A cast rather than a `declare global`: an ambient declaration has to sit at
70
+ * the top level of a module, and this block is not one. A cast is an ordinary
71
+ * annotation, which is what the rest of this script already relies on.
72
+ */
73
+ const w = window as Window & {
74
+ /** Set by this block so a fragment swap cannot stack a second set of listeners. */
75
+ __stxCartWired?: boolean
76
+ /** The hooks this drawer publishes for other components to drive it. */
77
+ stxCart?: { open: () => void, shut: () => void, refresh: () => Promise<void> }
78
+ }
79
+
66
80
  // Idempotency guard. SPA fragment swaps re-inject scoped scripts when
67
81
  // the cart link is followed; without this, every navigation stacked
68
82
  // another set of click listeners on [data-stx-add-to-cart] buttons,
69
83
  // and a single click would fire two/three/N XHRs — the user-visible
70
84
  // tell was a brief flicker as the drawer opened, refreshed, then
71
85
  // refreshed again from the duplicate handlers.
72
- if (window.__stxCartWired) return
73
- window.__stxCartWired = true
86
+ if (w.__stxCartWired) return
87
+ w.__stxCartWired = true
74
88
 
75
- const root = document.querySelector('[data-stx-cart-drawer]')
89
+ const root = document.querySelector<HTMLElement>('[data-stx-cart-drawer]')
76
90
  if (!root) return
77
- const backdrop = root.querySelector('[data-stx-cart-backdrop]')
78
- const panel = root.querySelector('[data-stx-cart-panel]')
79
- const closeBtn = root.querySelector('[data-stx-cart-close]')
80
- const list = root.querySelector('[data-stx-cart-list]')
81
- const empty = root.querySelector('[data-stx-cart-empty]')
82
- const footer = root.querySelector('[data-stx-cart-footer]')
83
- const subtotalEl = root.querySelector('[data-stx-cart-subtotal]')
91
+ const backdrop = root.querySelector<HTMLElement>('[data-stx-cart-backdrop]')
92
+ const panel = root.querySelector<HTMLElement>('[data-stx-cart-panel]')
93
+ const closeBtn = root.querySelector<HTMLElement>('[data-stx-cart-close]')
94
+ const list = root.querySelector<HTMLElement>('[data-stx-cart-list]')
95
+ const empty = root.querySelector<HTMLElement>('[data-stx-cart-empty]')
96
+ const footer = root.querySelector<HTMLElement>('[data-stx-cart-footer]')
97
+ const subtotalEl = root.querySelector<HTMLElement>('[data-stx-cart-subtotal]')
98
+
99
+ // `open()` and `shut()` drive both of these on every call. A drawer missing
100
+ // either is not a drawer, so this leaves rather than throwing halfway
101
+ // through the first open.
102
+ if (!backdrop || !panel)
103
+ return
84
104
 
85
105
  // Reserve the scrollbar gutter on <html> so locking page scroll while
86
106
  // the drawer is open doesn't cause the page width to jump (~17px on
@@ -89,10 +109,10 @@
89
109
  // because the scrollbar disappeared.
90
110
  document.documentElement.style.scrollbarGutter = 'stable'
91
111
 
92
- let lastFocus = null
112
+ let lastFocus: HTMLElement | null = null
93
113
 
94
- function open() {
95
- lastFocus = document.activeElement
114
+ const open = () => {
115
+ lastFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null
96
116
  root.style.pointerEvents = 'auto'
97
117
  root.setAttribute('aria-hidden', 'false')
98
118
  backdrop.style.opacity = '1'
@@ -101,7 +121,7 @@
101
121
  // Move focus into the panel so Esc + Tab work as expected.
102
122
  requestAnimationFrame(() => { panel.focus({ preventScroll: true }) })
103
123
  }
104
- function shut() {
124
+ const shut = () => {
105
125
  root.style.pointerEvents = 'none'
106
126
  root.setAttribute('aria-hidden', 'true')
107
127
  backdrop.style.opacity = '0'
@@ -118,11 +138,12 @@
118
138
  if (e.key === 'Escape' && root.getAttribute('aria-hidden') === 'false') shut()
119
139
  })
120
140
 
121
- function escapeHtml(s) {
122
- return String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c])
141
+ function escapeHtml(s: string) {
142
+ const entities: Record<string, string> = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }
143
+ return String(s ?? '').replace(/[&<>"']/g, c => entities[c] ?? c)
123
144
  }
124
145
 
125
- function render(items, subtotal) {
146
+ const render = (items: Array<{ slug?: string, name?: string, image?: string, qty?: number | string, price?: number | string, lineTotal?: number | string }> | null | undefined, subtotal: number | string) => {
126
147
  if (!list || !empty || !footer) return
127
148
  if (!items || items.length === 0) {
128
149
  list.classList.add('hidden')
@@ -156,13 +177,14 @@
156
177
  if (subtotalEl) subtotalEl.textContent = '$' + (Number(subtotal) || 0).toFixed(2)
157
178
  }
158
179
 
159
- let inflightRefresh = null
160
- async function refresh() {
180
+ let inflightRefresh: Promise<void> | null = null
181
+ const refresh = async (): Promise<void> => {
161
182
  // Coalesce concurrent refreshes — if a click fires two refresh() in a
162
183
  // row (e.g. add-to-cart + open both call refresh), they reuse the
163
184
  // same network request and the drawer doesn't briefly toggle between
164
185
  // empty and populated.
165
- if (inflightRefresh) return inflightRefresh
186
+ if (inflightRefresh)
187
+ return inflightRefresh
166
188
  inflightRefresh = (async () => {
167
189
  try {
168
190
  const res = await fetch('/api/cart', {
@@ -174,7 +196,7 @@
174
196
  render(data.items || [], data.subtotal || 0)
175
197
  const badge = document.querySelector('[data-stx-cart-count]')
176
198
  if (badge) {
177
- const total = (data.items || []).reduce((s, i) => s + (Number(i.qty) || 0), 0)
199
+ const total = (data.items || []).reduce((sum: number, item: { qty?: number | string }) => sum + (Number(item.qty) || 0), 0)
178
200
  badge.textContent = String(total)
179
201
  badge.classList.toggle('hidden', total === 0)
180
202
  }
@@ -186,7 +208,7 @@
186
208
  }
187
209
 
188
210
  // Public hooks for other components to call.
189
- window.stxCart = {
211
+ w.stxCart = {
190
212
  open: () => { open(); refresh() },
191
213
  shut,
192
214
  refresh,
@@ -64,8 +64,9 @@ const {
64
64
  // so clicking a thumb in gallery B updated gallery A's hero.
65
65
  // 2. SPA fragment swaps re-injected the scoped script and stacked
66
66
  // a duplicate listener on every existing thumb.
67
- if (window.__stxGalleryWired) return
68
- window.__stxGalleryWired = true
67
+ const w = window as Window & { __stxGalleryWired?: boolean }
68
+ if (w.__stxGalleryWired) return
69
+ w.__stxGalleryWired = true
69
70
 
70
71
  document.addEventListener('click', (e) => {
71
72
  const target = e.target instanceof Element ? e.target : null
@@ -43,8 +43,13 @@ const {
43
43
  // = two listeners per button = doubled increments per click.
44
44
  // 2. SPA fragment swaps that re-inject our scoped script don't
45
45
  // keep multiplying handlers; the guard below early-returns.
46
- if (window.__stxQtyWired) return
47
- window.__stxQtyWired = true
46
+ const w = window as Window & {
47
+ /** Set by this block so a fragment swap cannot stack a second set of listeners. */
48
+ __stxQtyWired?: boolean
49
+ }
50
+
51
+ if (w.__stxQtyWired) return
52
+ w.__stxQtyWired = true
48
53
 
49
54
  // Default min/max chosen to match the component's defineProps fallbacks.
50
55
  // STX SSR doesn't reliably propagate defineProps default values into
@@ -53,13 +58,13 @@ const {
53
58
  // these fallbacks Math.min(0, qty+1) capped every increment at 0 and
54
59
  // the +/- buttons appeared dead. Read either the data-* on the wrapper
55
60
  // or the input attribute, whichever is non-empty.
56
- function bound(wrap, kind, fallback) {
61
+ function bound(wrap: HTMLElement, kind: string, fallback: number) {
57
62
  const fromData = wrap.getAttribute('data-' + kind)
58
63
  if (fromData && fromData.trim() !== '') {
59
64
  const n = Number(fromData)
60
65
  if (Number.isFinite(n)) return n
61
66
  }
62
- const input = wrap.querySelector('input[type="number"]')
67
+ const input = wrap.querySelector<HTMLInputElement>('input[type="number"]')
63
68
  const fromAttr = input && input.getAttribute(kind)
64
69
  if (fromAttr && fromAttr.trim() !== '') {
65
70
  const n = Number(fromAttr)
@@ -68,8 +73,8 @@ const {
68
73
  return fallback
69
74
  }
70
75
 
71
- function step(wrap, delta) {
72
- const input = wrap.querySelector('input[type="number"]')
76
+ function step(wrap: HTMLElement, delta: number) {
77
+ const input = wrap.querySelector<HTMLInputElement>('input[type="number"]')
73
78
  if (!input) return
74
79
  const min = bound(wrap, 'min', 1)
75
80
  const max = bound(wrap, 'max', 99)
@@ -88,7 +93,7 @@ const {
88
93
  const dec = target.closest('[data-stx-qty-dec]')
89
94
  const inc = target.closest('[data-stx-qty-inc]')
90
95
  if (!dec && !inc) return
91
- const wrap = (dec || inc).closest('[data-stx-qty]')
96
+ const wrap = (dec ?? inc)?.closest<HTMLElement>('[data-stx-qty]')
92
97
  if (!wrap) return
93
98
  e.preventDefault()
94
99
  step(wrap, dec ? -1 : 1)
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Read the storefront cart token out of the request.
3
+ *
4
+ * The cart and the three checkout steps each opened with
5
+ *
6
+ * const token = (typeof requestContext !== 'undefined')
7
+ * ? requestContext.cookie('stacks_cart')
8
+ * : null
9
+ *
10
+ * and `requestContext` is not a binding stx has ever put in a server script's
11
+ * scope. It is the NAME OF A PARAMETER inside the template hydrator, and the
12
+ * page context a server assembles carries `params`, `request` and `method`.
13
+ * So the `typeof` guard was always false, `token` was always null, and every
14
+ * one of those pages rendered its empty-cart branch no matter what was in the
15
+ * cart. The guard is what hid it: without it the pages would have thrown on
16
+ * the first request instead of quietly showing nothing.
17
+ *
18
+ * `request` is really provided, so the cookie is read from its header.
19
+ */
20
+
21
+ /** The cookie the storefront stores its cart session token in. */
22
+ export const CART_COOKIE = 'stacks_cart'
23
+
24
+ /**
25
+ * The value of `name` in a request's Cookie header, or null.
26
+ *
27
+ * Values are percent-encoded on the way in, so they are decoded here. A
28
+ * malformed encoding yields the raw value rather than throwing, since a
29
+ * cookie is attacker-supplied and a render must not fail on one.
30
+ */
31
+ export function cookieFromRequest(request: Request | undefined, name: string): string | null {
32
+ const header = request?.headers?.get('cookie')
33
+ if (!header)
34
+ return null
35
+
36
+ for (const part of header.split(';')) {
37
+ const index = part.indexOf('=')
38
+ if (index < 0)
39
+ continue
40
+ if (part.slice(0, index).trim() !== name)
41
+ continue
42
+
43
+ const raw = part.slice(index + 1).trim()
44
+ try {
45
+ return decodeURIComponent(raw)
46
+ }
47
+ catch {
48
+ return raw
49
+ }
50
+ }
51
+
52
+ return null
53
+ }
54
+
55
+ /** The storefront cart token carried by this request, or null. */
56
+ export function cartTokenFromRequest(request: Request | undefined): string | null {
57
+ return cookieFromRequest(request, CART_COOKIE)
58
+ }
@@ -22,6 +22,7 @@
22
22
  * badge is correct on first paint, before any client-side JS runs.
23
23
  */
24
24
 
25
+ const { cartTokenFromRequest } = require('../functions/storefront/cart-cookie')
25
26
  const Database = require('bun:sqlite').Database
26
27
  const path = require('path')
27
28
 
@@ -29,7 +30,7 @@ const dbPath = path.resolve(process.cwd(), process.env.DB_DATABASE_PATH || 'data
29
30
 
30
31
  let cartCount = 0
31
32
  try {
32
- const sessionToken = (typeof requestContext !== 'undefined') ? requestContext.cookie('stacks_cart') : null
33
+ const sessionToken = cartTokenFromRequest(request)
33
34
  if (sessionToken) {
34
35
  const db = new Database(dbPath, { readonly: true })
35
36
  const cart = db.query(`SELECT total_items FROM carts WHERE session_token = ?1 AND status = 'active' LIMIT 1`).get(sessionToken)
@@ -52,7 +53,7 @@ catch { /* schema not migrated yet, or db missing — render an empty header */
52
53
  injects __stxRouterConfig with container:'body', which would erase
53
54
  the header/footer on every navigation. STX_ROUTER_OPTIONS is checked
54
55
  last by the router's Object.assign, so this wins. --}}
55
- <script>window.STX_ROUTER_OPTIONS = { container: 'main' };</script>
56
+ <script>(window as Window & { STX_ROUTER_OPTIONS?: { container: string } }).STX_ROUTER_OPTIONS = { container: 'main' };</script>
56
57
  </head>
57
58
  <body class="antialiased font-serif text-stone-900 bg-stone-50">
58
59
 
@@ -3,13 +3,29 @@
3
3
  @section('title', 'Your cart')
4
4
 
5
5
  <script server>
6
+ const { cartTokenFromRequest } = require('../../functions/storefront/cart-cookie')
7
+ // The columns this query selects. bun:sqlite returns `any`, so nothing
8
+ // downstream of the map was checked either.
9
+ interface CartItemRow {
10
+ id: number
11
+ quantity: number | string
12
+ unit_price: number | string
13
+ total_price: number | string
14
+ product_name: string
15
+ product_image: string | null
16
+ product_sku: string
17
+ weight_grams: number | null
18
+ }
19
+
20
+ interface StorefrontLineItem { lineTotal: number }
21
+
6
22
  const Database = require('bun:sqlite').Database
7
23
  const path = require('path')
8
24
 
9
25
  const dbPath = path.resolve(process.cwd(), process.env.DB_DATABASE_PATH || 'database/stacks.sqlite')
10
26
  const db = new Database(dbPath, { readonly: true })
11
27
 
12
- const token = (typeof requestContext !== 'undefined') ? requestContext.cookie('stacks_cart') : null
28
+ const token = cartTokenFromRequest(request)
13
29
  let items = []
14
30
  let cart = null
15
31
 
@@ -23,7 +39,7 @@ if (token) {
23
39
  LEFT JOIN products p ON p.slug = ci.product_sku
24
40
  WHERE ci.cart_id = ?1
25
41
  ORDER BY ci.id ASC
26
- `).all(cart.id).map((row) => {
42
+ `).all(cart.id).map((row: CartItemRow) => {
27
43
  const qty = Number(row.quantity)
28
44
  const price = Number(row.unit_price)
29
45
  const lineTotal = Number(row.total_price)
@@ -49,7 +65,7 @@ if (token) {
49
65
  // app/Actions/Storefront/_shipping.ts when changing.
50
66
  const FREE_SHIPPING_THRESHOLD = Number(process.env.STOREFRONT_FREE_SHIPPING_THRESHOLD ?? 40) || 40
51
67
  const FLAT_SHIPPING = Number(process.env.STOREFRONT_FLAT_SHIPPING ?? 5) || 5
52
- const subtotal = items.reduce((sum, i) => sum + i.lineTotal, 0)
68
+ const subtotal = items.reduce((sum: number, i: StorefrontLineItem) => sum + i.lineTotal, 0)
53
69
  const shipping = subtotal <= 0 ? 0 : (subtotal >= FREE_SHIPPING_THRESHOLD ? 0 : FLAT_SHIPPING)
54
70
  const total = subtotal + shipping
55
71
  const remainingForFree = Math.max(0, FREE_SHIPPING_THRESHOLD - subtotal)
@@ -3,13 +3,24 @@
3
3
  @section('title', 'Checkout — Contact')
4
4
 
5
5
  <script server>
6
+ const { cartTokenFromRequest } = require('../../../functions/storefront/cart-cookie')
7
+ // The columns this query selects. bun:sqlite returns `any`, so nothing
8
+ // downstream of the map was checked either.
9
+ interface CartSummaryRow {
10
+ product_name: string
11
+ quantity: number | string
12
+ total_price: number | string
13
+ }
14
+
15
+ interface StorefrontLineItem { lineTotal: number }
16
+
6
17
  const Database = require('bun:sqlite').Database
7
18
  const path = require('path')
8
19
 
9
20
  const dbPath = path.resolve(process.cwd(), process.env.DB_DATABASE_PATH || 'database/stacks.sqlite')
10
21
  const db = new Database(dbPath, { readonly: true })
11
22
 
12
- const token = (typeof requestContext !== 'undefined') ? requestContext.cookie('stacks_cart') : null
23
+ const token = cartTokenFromRequest(request)
13
24
  let items = []
14
25
  let cart = null
15
26
 
@@ -19,7 +30,7 @@ if (token) {
19
30
  items = db.query(`
20
31
  SELECT product_name, quantity, total_price
21
32
  FROM cart_items WHERE cart_id = ?1 ORDER BY id ASC
22
- `).all(cart.id).map((row) => {
33
+ `).all(cart.id).map((row: CartSummaryRow) => {
23
34
  const qty = Number(row.quantity)
24
35
  const lineTotal = Number(row.total_price)
25
36
  return {
@@ -36,7 +47,7 @@ if (token) {
36
47
  // totals on every checkout step match what gets charged.
37
48
  const FREE_SHIPPING_THRESHOLD = Number(process.env.STOREFRONT_FREE_SHIPPING_THRESHOLD ?? 40) || 40
38
49
  const FLAT_SHIPPING = Number(process.env.STOREFRONT_FLAT_SHIPPING ?? 5) || 5
39
- const subtotal = items.reduce((s, i) => s + i.lineTotal, 0)
50
+ const subtotal = items.reduce((s: number, i: StorefrontLineItem) => s + i.lineTotal, 0)
40
51
  const shipping = subtotal <= 0 ? 0 : (subtotal >= FREE_SHIPPING_THRESHOLD ? 0 : FLAT_SHIPPING)
41
52
  const total = subtotal + shipping
42
53
  const remainingForFree = Math.max(0, FREE_SHIPPING_THRESHOLD - subtotal)
@@ -3,13 +3,24 @@
3
3
  @section('title', 'Checkout — Payment')
4
4
 
5
5
  <script server>
6
+ const { cartTokenFromRequest } = require('../../../functions/storefront/cart-cookie')
7
+ // The columns this query selects. bun:sqlite returns `any`, so nothing
8
+ // downstream of the map was checked either.
9
+ interface CartSummaryRow {
10
+ product_name: string
11
+ quantity: number | string
12
+ total_price: number | string
13
+ }
14
+
15
+ interface StorefrontLineItem { lineTotal: number }
16
+
6
17
  const Database = require('bun:sqlite').Database
7
18
  const path = require('path')
8
19
 
9
20
  const dbPath = path.resolve(process.cwd(), process.env.DB_DATABASE_PATH || 'database/stacks.sqlite')
10
21
  const db = new Database(dbPath, { readonly: true })
11
22
 
12
- const token = (typeof requestContext !== 'undefined') ? requestContext.cookie('stacks_cart') : null
23
+ const token = cartTokenFromRequest(request)
13
24
  let items = []
14
25
  let cart = null
15
26
 
@@ -19,7 +30,7 @@ if (token) {
19
30
  items = db.query(`
20
31
  SELECT product_name, quantity, total_price
21
32
  FROM cart_items WHERE cart_id = ?1 ORDER BY id ASC
22
- `).all(cart.id).map((row) => {
33
+ `).all(cart.id).map((row: CartSummaryRow) => {
23
34
  const qty = Number(row.quantity)
24
35
  const lineTotal = Number(row.total_price)
25
36
  return {
@@ -34,7 +45,7 @@ if (token) {
34
45
 
35
46
  const FREE_SHIPPING_THRESHOLD = Number(process.env.STOREFRONT_FREE_SHIPPING_THRESHOLD ?? 40) || 40
36
47
  const FLAT_SHIPPING = Number(process.env.STOREFRONT_FLAT_SHIPPING ?? 5) || 5
37
- const subtotal = items.reduce((s, i) => s + i.lineTotal, 0)
48
+ const subtotal = items.reduce((s: number, i: StorefrontLineItem) => s + i.lineTotal, 0)
38
49
  const shipping = subtotal <= 0 ? 0 : (subtotal >= FREE_SHIPPING_THRESHOLD ? 0 : FLAT_SHIPPING)
39
50
  const total = subtotal + shipping
40
51
  const remainingForFree = Math.max(0, FREE_SHIPPING_THRESHOLD - subtotal)
@@ -3,13 +3,24 @@
3
3
  @section('title', 'Checkout — Shipping')
4
4
 
5
5
  <script server>
6
+ const { cartTokenFromRequest } = require('../../../functions/storefront/cart-cookie')
7
+ // The columns this query selects. bun:sqlite returns `any`, so nothing
8
+ // downstream of the map was checked either.
9
+ interface CartSummaryRow {
10
+ product_name: string
11
+ quantity: number | string
12
+ total_price: number | string
13
+ }
14
+
15
+ interface StorefrontLineItem { lineTotal: number }
16
+
6
17
  const Database = require('bun:sqlite').Database
7
18
  const path = require('path')
8
19
 
9
20
  const dbPath = path.resolve(process.cwd(), process.env.DB_DATABASE_PATH || 'database/stacks.sqlite')
10
21
  const db = new Database(dbPath, { readonly: true })
11
22
 
12
- const token = (typeof requestContext !== 'undefined') ? requestContext.cookie('stacks_cart') : null
23
+ const token = cartTokenFromRequest(request)
13
24
  let items = []
14
25
  let cart = null
15
26
 
@@ -19,7 +30,7 @@ if (token) {
19
30
  items = db.query(`
20
31
  SELECT product_name, quantity, total_price
21
32
  FROM cart_items WHERE cart_id = ?1 ORDER BY id ASC
22
- `).all(cart.id).map((row) => {
33
+ `).all(cart.id).map((row: CartSummaryRow) => {
23
34
  const qty = Number(row.quantity)
24
35
  const lineTotal = Number(row.total_price)
25
36
  return {
@@ -34,7 +45,7 @@ if (token) {
34
45
 
35
46
  const FREE_SHIPPING_THRESHOLD = Number(process.env.STOREFRONT_FREE_SHIPPING_THRESHOLD ?? 40) || 40
36
47
  const FLAT_SHIPPING = Number(process.env.STOREFRONT_FLAT_SHIPPING ?? 5) || 5
37
- const subtotal = items.reduce((s, i) => s + i.lineTotal, 0)
48
+ const subtotal = items.reduce((s: number, i: StorefrontLineItem) => s + i.lineTotal, 0)
38
49
  const shipping = subtotal <= 0 ? 0 : (subtotal >= FREE_SHIPPING_THRESHOLD ? 0 : FLAT_SHIPPING)
39
50
  const total = subtotal + shipping
40
51
  const remainingForFree = Math.max(0, FREE_SHIPPING_THRESHOLD - subtotal)
@@ -6,10 +6,15 @@
6
6
  --}}
7
7
  <div class="cms-form mx-auto max-w-xl px-6 py-8" data-form-uuid="{{ props.formUuid }}">
8
8
  <script client>
9
- const host = useRef('host')
10
- const definition = state(null)
11
- const values = reactive({})
12
- const errors = state({})
9
+ import type { PublicFormDefinition, PublicFormField, SubmissionErrors } from '@stacksjs/forms'
10
+
11
+ // `_form` carries a whole-form failure; the rest are keyed by field name.
12
+ type FormErrors = SubmissionErrors & { _form?: string }
13
+
14
+ const host = useRef<HTMLElement>('host')
15
+ const definition = state<PublicFormDefinition | null>(null)
16
+ const values = reactive<Record<string, string | number | boolean>>({})
17
+ const errors = state<FormErrors>({})
13
18
  const sending = state(false)
14
19
  const done = state('')
15
20
  const openedAt = Date.now()
@@ -17,7 +22,7 @@
17
22
  effect(() => {
18
23
  if (!host() || definition())
19
24
  return
20
- const uuid = host().dataset.formUuid
25
+ const uuid = host()?.dataset.formUuid
21
26
  if (!uuid)
22
27
  return
23
28
  fetch(`/api/forms/${uuid}`, { credentials: 'same-origin' })
@@ -26,7 +31,7 @@
26
31
  .catch(() => {})
27
32
  })
28
33
 
29
- function fieldVisible(field) {
34
+ function fieldVisible(field: PublicFormField): boolean {
30
35
  const conditions = field.conditions
31
36
  if (!conditions || !conditions.rules || conditions.rules.length === 0)
32
37
  return true
@@ -47,7 +52,7 @@
47
52
  return conditions.action === 'show' ? matched : !matched
48
53
  }
49
54
 
50
- async function submit(event) {
55
+ async function submit(event: Event): Promise<void> {
51
56
  event.preventDefault()
52
57
  if (sending())
53
58
  return
@@ -55,7 +60,7 @@
55
60
  errors.set({})
56
61
 
57
62
  try {
58
- const uuid = host().dataset.formUuid
63
+ const uuid = host()?.dataset.formUuid
59
64
  const reply = await fetch(`/api/forms/${uuid}/submissions`, {
60
65
  method: 'POST',
61
66
  headers: { 'Content-Type': 'application/json' },
@@ -90,11 +95,11 @@
90
95
  <div ref="host" data-form-uuid="{{ props.formUuid }}">
91
96
  <div :if="done()" class="rounded-lg border border-green-200 bg-green-50 px-5 py-4 text-[15px] text-green-900">{{ done() }}</div>
92
97
  <form :else-if="definition()" @submit="submit" novalidate>
93
- <h2 class="text-2xl font-semibold tracking-tight">{{ definition().name }}</h2>
98
+ <h2 class="text-2xl font-semibold tracking-tight">{{ definition()?.name }}</h2>
94
99
  <p :if="errors()._form" class="mt-3 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-[14px] text-red-800">{{ errors()._form }}</p>
95
100
 
96
101
  <div class="mt-6 grid grid-cols-1 gap-5 sm:grid-cols-2">
97
- <template :for="field in definition().fields">
102
+ <template :for="field in definition()?.fields ?? []">
98
103
  <div :if="fieldVisible(field)" class="{{ field.width === 'half' ? '' : 'sm:col-span-2' }}">
99
104
  <h3 :if="field.type === 'section_break'" class="mt-2 border-t border-gray-200 pt-4 text-lg font-medium">{{ field.label }}</h3>
100
105
  <template :else>
@@ -124,7 +129,7 @@
124
129
  <input type="text" name="website" tabindex="-1" autocomplete="off" aria-hidden="true" class="hidden" @input="values._hp = $event.target.value">
125
130
 
126
131
  <button type="submit" :disabled="sending()" class="mt-7 rounded-lg px-6 py-3 font-medium bg-gray-900 text-white transition-transform active:scale-[0.98] disabled:opacity-60">
127
- {{ sending() ? 'Sending...' : (definition().submitLabel ?? 'Submit') }}
132
+ {{ sending() ? 'Sending...' : (definition()?.submitLabel ?? 'Submit') }}
128
133
  </button>
129
134
  </form>
130
135
  <p :else class="text-[15px] text-gray-500">Loading form...</p>
@@ -10,6 +10,34 @@
10
10
  Deliberately minimal chrome: real sites override this to add their own
11
11
  navigation (fetchMenuTree), theme tokens and footer.
12
12
  --}}
13
+ <script server>
14
+ /*
15
+ * The bindings `renderCmsPage` puts in this template's context, declared so
16
+ * the checker sees the same contract the docblock above describes. They are
17
+ * supplied by the renderer rather than imported, so `declare` is the accurate
18
+ * spelling: it types them and emits nothing.
19
+ *
20
+ * See `storage/framework/core/cms/src/public/render.ts`.
21
+ */
22
+ declare const site: {
23
+ id: number
24
+ name: string
25
+ subdomain: string | null
26
+ settings: Record<string, unknown>
27
+ }
28
+
29
+ declare const page: {
30
+ id: number
31
+ title: string
32
+ path: string
33
+ template: string
34
+ metaDescription: string
35
+ }
36
+
37
+ /** Pre-rendered, sanitized block HTML. */
38
+ declare const content: string
39
+ </script>
40
+
13
41
  <!DOCTYPE html>
14
42
  <html lang="en">
15
43
  <head>
@@ -1,9 +1,3 @@
1
- <script>
2
- defineOptions({
3
- name: 'ComingSoonPage',
4
- })
5
- </script>
6
-
7
1
  <main>
8
2
  <ComingSoon />
9
3
  </main>
@@ -3,13 +3,29 @@
3
3
  @section('title', 'Order confirmed')
4
4
 
5
5
  <script server>
6
+ // The columns this query selects. bun:sqlite returns `any`, so nothing
7
+ // downstream of the map was checked either.
8
+ interface OrderItemRow {
9
+ quantity: number | string
10
+ price: number | string
11
+ name: string
12
+ image_url: string | null
13
+ slug: string
14
+ weight_grams: number | null
15
+ }
16
+
17
+ interface StorefrontLineItem { lineTotal: number }
18
+
6
19
  const Database = require('bun:sqlite').Database
7
20
  const path = require('path')
8
21
 
9
22
  const dbPath = path.resolve(process.cwd(), process.env.DB_DATABASE_PATH || 'database/stacks.sqlite')
10
23
  const db = new Database(dbPath, { readonly: true })
11
24
 
12
- const orderId = Number((typeof id !== 'undefined' && id) || 0)
25
+ // From `params`, which is what stx puts in a server script's scope for a
26
+ // `[id]` route. A bare `id` has never been a binding, so the `typeof` guard
27
+ // was always false and this page rendered "order not found" for every order.
28
+ const orderId = Number(params?.id ?? 0) || 0
13
29
 
14
30
  const order = orderId
15
31
  ? db.query(`
@@ -27,7 +43,7 @@ const items = order
27
43
  LEFT JOIN products p ON p.id = oi.product_id
28
44
  WHERE oi.order_id = ?1
29
45
  ORDER BY oi.id ASC
30
- `).all(order.id).map((row) => ({
46
+ `).all(order.id).map((row: OrderItemRow) => ({
31
47
  name: row.name,
32
48
  slug: row.slug,
33
49
  image: row.image_url,
@@ -38,7 +54,7 @@ const items = order
38
54
  }))
39
55
  : []
40
56
 
41
- const subtotal = items.reduce((s, i) => s + i.lineTotal, 0)
57
+ const subtotal = items.reduce((s: number, i: StorefrontLineItem) => s + i.lineTotal, 0)
42
58
  const shipping = order ? Number(order.delivery_fee || 0) : 0
43
59
  const total = order ? Number(order.total_amount || 0) : 0
44
60
  </script>
@@ -30,7 +30,7 @@ export {
30
30
  sidebarThemeFor,
31
31
  useAppearance,
32
32
  } from './useAppearance'
33
- export type { ChartCtor, ChartHandle, ChartLike, LazyChartsHandle, UseChartOptions } from './useChart'
33
+ export type { ChartConfig, ChartCtor, ChartData, ChartDataset, ChartHandle, ChartLike, ChartOptions, ChartType, LazyChartsHandle, UseChartOptions } from './useChart'
34
34
  export { useChart, useCharts, useLazyCharts } from './useChart'
35
35
  export type { NavigationSnapshot } from './useNavigation'
36
36
  export { useNavigation } from './useNavigation'