@stacksjs/defaults 0.72.98 → 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
package/app/Middleware.ts CHANGED
@@ -1,13 +1,28 @@
1
- export interface Middleware {
2
- [key: string]: string
3
- }
1
+ import { defineMiddleware } from '@stacksjs/router'
4
2
 
5
3
  /**
6
4
  * The application's middleware aliases.
7
5
  *
8
- * Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
6
+ * Aliases may be used instead of class names to conveniently assign middleware
7
+ * to routes and groups. Each one names a class under `app/Middleware/`, or one
8
+ * of the framework defaults behind it, and the name is checked.
9
+ *
10
+ * This map is MERGED over the framework defaults rather than replacing them,
11
+ * so an alias the framework adds later is available here without an edit.
12
+ *
13
+ * Two forms are read off a reference before it is looked up:
14
+ *
15
+ * - `!alias` inverts it: the route passes only when that middleware refuses.
16
+ * `'!auth'` is a guests-only route; `'!env:production'` is everywhere but
17
+ * production.
18
+ * - `alias:params` passes `params` to the middleware, but only when the
19
+ * whole string is not itself an alias - so `'env:production'` is the alias
20
+ * below and `'throttle:60,1'` passes `60,1` to `throttle`.
21
+ *
22
+ * A middleware with no alias is still reachable by its class name, which is how
23
+ * `'signed'` worked before it was listed here.
9
24
  */
10
- export default {
25
+ export default defineMiddleware({
11
26
  'maintenance': 'Maintenance',
12
27
  'cors': 'Cors',
13
28
  'auth': 'Auth',
@@ -19,6 +34,7 @@ export default {
19
34
  'abilities': 'Abilities',
20
35
  'can': 'Can',
21
36
  'throttle': 'Throttle',
37
+ 'signed': 'Signed',
22
38
  'env': 'Env',
23
39
  'env:local': 'EnvLocal',
24
40
  'env:development': 'EnvDevelopment',
@@ -32,5 +48,4 @@ export default {
32
48
  'csrf': 'Csrf',
33
49
  'compress': 'Compress',
34
50
  // Add more middleware aliases here
35
- // Note: Use ! prefix for negation (e.g., '!auth', '!env:development')
36
- } satisfies Middleware
51
+ })
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.72.98",
5
+ "version": "0.72.99",
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.72.98",
5
+ "version": "0.72.99",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -51,7 +51,7 @@
51
51
  "dependencies": {
52
52
  "@iconify-json/f7": "^1.2.2",
53
53
  "@iconify-json/hugeicons": "^1.2.27",
54
- "@stacksjs/mobile": "^0.72.98",
54
+ "@stacksjs/mobile": "^0.72.99",
55
55
  "@stacksjs/sanitizer": "^0.2.113"
56
56
  },
57
57
  "scripts": {
@@ -11,8 +11,7 @@
11
11
  "Actions/*": ["../app/Actions/*"],
12
12
  "Jobs/*": ["../app/Jobs/*"],
13
13
  "./app/*": ["../app/*"], // it's outside the dist folder
14
- "~/*": ["../*"],
15
- "../../../env": ["../storage/framework/env.ts"]
14
+ "~/*": ["../*"]
16
15
  },
17
16
  "resolveJsonModule": true,
18
17
  "allowImportingTsExtensions": true,
@@ -1,28 +1,37 @@
1
- <script>
1
+ <script client>
2
+ /*
3
+ * A counter, in signals.
4
+ *
5
+ * This used to be a `<script client>` that ran `document.querySelector` on
6
+ * mount, read the count back out of `textContent`, `parseInt`ed it and wrote a
7
+ * number back into that string property. It worked, but as the component every
8
+ * new app has in `Buttons/`, it taught the one pattern the stx guidance rules
9
+ * out: reaching for the DOM instead of state. It also only ever bound the
10
+ * first `[data-counter]` on the page, so a second `<Counter />` was inert.
11
+ */
2
12
  interface CounterProps {
3
13
  count?: number
4
14
  }
5
15
 
6
16
  const { count = 0 } = defineProps<CounterProps>()
17
+
18
+ const total = state(count)
19
+
20
+ function increase(): void {
21
+ total.set(total() + 1)
22
+ }
7
23
  </script>
24
+
8
25
  <div>
9
- <button
10
- type="button"
11
- class="text-white text-xs bg-blue-500 hover:bg-blue-600 btn"
12
- data-counter
13
- >
14
- <i
15
- class="-ml-1 mr-3 h-4 w-4 i-hugeicons-outline-plus"
16
- aria-hidden="true"
17
- ></i>
18
- Counter: <span data-count>{{ count }}</span>
19
- </button>
20
- </div>
21
- <script client>
22
- document.querySelector('[data-counter]')?.addEventListener('click', function() {
23
- const countEl = document.querySelector('[data-count]')
24
- if (countEl) {
25
- countEl.textContent = parseInt(countEl.textContent || '0') + 1
26
- }
27
- })
28
- </script>
26
+ <button
27
+ type="button"
28
+ class="text-white text-xs bg-blue-500 hover:bg-blue-600 btn"
29
+ @click="increase"
30
+ >
31
+ <i
32
+ class="-ml-1 mr-3 h-4 w-4 i-hugeicons-outline-plus"
33
+ aria-hidden="true"
34
+ ></i>
35
+ Counter: <span>{{ total() }}</span>
36
+ </button>
37
+ </div>
@@ -211,11 +211,9 @@ import type {
211
211
  } from '~/storage/framework/defaults/functions/analytics'
212
212
  import { fetchWebAnalytics } from '~/storage/framework/defaults/functions/analytics'
213
213
  import { pushToast } from '~/storage/framework/defaults/functions/toasts'
214
+ import type { ChartCtor, ChartLike } from '~/storage/framework/defaults/views/dashboard/composables/useChart'
214
215
  import { useChart } from '~/storage/framework/defaults/views/dashboard/composables/useChart'
215
216
 
216
- interface ChartLike { destroy: () => void }
217
- type ChartCtor = new (canvas: HTMLElement, config: { type: string, data: unknown, options?: unknown }) => ChartLike
218
-
219
217
  const analyticsOverview = state<AnalyticsOverview>({
220
218
  realtimeVisitors: 0,
221
219
  uniqueVisitors: 0,
@@ -11,7 +11,7 @@ const isLoading = state(false)
11
11
  const error = state('')
12
12
  const success = state(false)
13
13
 
14
- async function handleForgotPassword(data) {
14
+ async function handleForgotPassword(data: { email: string }): Promise<void> {
15
15
  isLoading.set(true)
16
16
  error.set('')
17
17
 
@@ -1,4 +1,5 @@
1
1
  <script client>
2
+ import type { AuthUser } from '../../../../types/dashboard'
2
3
  import { describeAuthError, useAuth } from '../../../../functions/auth'
3
4
 
4
5
  const { login } = useAuth()
@@ -12,7 +13,7 @@ function safeRedirect(): string {
12
13
  return redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : '/'
13
14
  }
14
15
 
15
- async function handleLogin(credentials) {
16
+ async function handleLogin(credentials: AuthUser): Promise<void> {
16
17
  isLoading.set(true)
17
18
  error.set('')
18
19
 
@@ -1,4 +1,5 @@
1
1
  <script client>
2
+ import type { RegisterCredentials } from '../../../../types/dashboard'
2
3
  import { describeAuthError, useAuth } from '../../../../functions/auth'
3
4
 
4
5
  const { register } = useAuth()
@@ -12,7 +13,7 @@ function safeRedirect(): string {
12
13
  return redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : '/'
13
14
  }
14
15
 
15
- async function handleRegister(credentials) {
16
+ async function handleRegister(credentials: RegisterCredentials): Promise<void> {
16
17
  isLoading.set(true)
17
18
  error.set('')
18
19
 
@@ -53,7 +53,7 @@ async function verify(): Promise<void> {
53
53
  }
54
54
  }
55
55
 
56
- async function handleReset(data): Promise<void> {
56
+ async function handleReset(data: { password: string, password_confirmation: string }): Promise<void> {
57
57
  isLoading.set(true)
58
58
  error.set('')
59
59
 
@@ -41,33 +41,33 @@
41
41
  return 'Runners: ' + r.running + ' running, ' + r.queued + ' queued / ' + r.cap + ' cap'
42
42
  })
43
43
 
44
- function tabClass(id) {
44
+ function tabClass(id: string) {
45
45
  const base = 'px-4 py-2 font-medium text-sm whitespace-nowrap bg-transparent border-b-2 transition-colors cursor-pointer '
46
46
  return activeTab() === id
47
47
  ? base + 'border-violet-400 text-violet-600 dark:text-violet-400'
48
48
  : base + 'border-transparent text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'
49
49
  }
50
50
 
51
- function filterClass(id) {
51
+ function filterClass(id: string) {
52
52
  const base = 'px-3 py-1 text-xs rounded-full border transition-colors cursor-pointer '
53
53
  return statusFilter() === id
54
54
  ? base + 'bg-violet-500 border-violet-500 text-white'
55
55
  : base + 'bg-transparent border-zinc-300 dark:border-zinc-700 text-zinc-600 dark:text-zinc-400 hover:border-zinc-400'
56
56
  }
57
57
 
58
- function borderColor(status) {
58
+ function borderColor(status: string) {
59
59
  if (status === 'success') return 'border-color: rgba(16,185,129,0.3)'
60
60
  if (status === 'failure' || status === 'error') return 'border-color: rgba(239,68,68,0.3)'
61
61
  if (status === 'pending') return 'border-color: rgba(234,179,8,0.3)'
62
62
  return 'border-color: rgba(39,39,42,1)'
63
63
  }
64
64
 
65
- function isSuccess(status) { return status === 'success' }
66
- function isFailing(status) { return status === 'failure' || status === 'error' }
67
- function isPending(status) { return status === 'pending' }
68
- function isNoRuns(status) { return status === 'no_runs' }
65
+ function isSuccess(status: string) { return status === 'success' }
66
+ function isFailing(status: string) { return status === 'failure' || status === 'error' }
67
+ function isPending(status: string) { return status === 'pending' }
68
+ function isNoRuns(status: string) { return status === 'no_runs' }
69
69
 
70
- function timeAgo(dateStr) {
70
+ function timeAgo(dateStr: string | null | undefined) {
71
71
  if (!dateStr) return ''
72
72
  const diff = Date.now() - new Date(dateStr).getTime()
73
73
  const mins = Math.floor(diff / 60000)
@@ -78,7 +78,7 @@
78
78
  return days + 'd ago'
79
79
  }
80
80
 
81
- function truncate(str, len) {
81
+ function truncate(str: string, len: number) {
82
82
  if (!str) return ''
83
83
  return str.length > len ? str.slice(0, len) + '...' : str
84
84
  }
@@ -11,7 +11,7 @@ const jobsByRunId = derived(() => ci.drilldownJobsByRunId())
11
11
  const loadingJobsRunId = derived(() => ci.loadingJobsForRunId())
12
12
  const title = derived(() => target()?.fullName || 'Run history')
13
13
 
14
- function runRowClass(conclusion, status) {
14
+ function runRowClass(conclusion: string, status: string) {
15
15
  const base = 'px-3 py-2 -mx-3 border-l-2 cursor-pointer hover:bg-zinc-50 dark:hover:bg-zinc-900 transition-colors '
16
16
  if (status !== 'completed') return base + 'border-yellow-400'
17
17
  if (conclusion === 'success') return base + 'border-emerald-400'
@@ -20,14 +20,14 @@ function runRowClass(conclusion, status) {
20
20
  return base + 'border-zinc-200 dark:border-zinc-800'
21
21
  }
22
22
 
23
- function runStatusBadge(conclusion, status) {
23
+ function runStatusBadge(conclusion: string, status: string) {
24
24
  if (status !== 'completed') return { label: status, cls: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-300' }
25
25
  if (conclusion === 'success') return { label: 'success', cls: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300' }
26
26
  if (conclusion === 'failure') return { label: 'failure', cls: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300' }
27
27
  return { label: conclusion || 'unknown', cls: 'bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300' }
28
28
  }
29
29
 
30
- function formatDuration(ms) {
30
+ function formatDuration(ms: number) {
31
31
  if (ms == null) return ''
32
32
  const seconds = Math.floor(ms / 1000)
33
33
  if (seconds < 60) return seconds + 's'
@@ -38,7 +38,7 @@ function formatDuration(ms) {
38
38
  return hours + 'h ' + (minutes % 60) + 'm'
39
39
  }
40
40
 
41
- function timeAgo(dateStr) {
41
+ function timeAgo(dateStr: string | null | undefined) {
42
42
  if (!dateStr) return ''
43
43
  const diff = Date.now() - new Date(dateStr).getTime()
44
44
  const minutes = Math.floor(diff / 60000)
@@ -48,16 +48,16 @@ function timeAgo(dateStr) {
48
48
  return Math.floor(hours / 24) + 'd ago'
49
49
  }
50
50
 
51
- function truncate(value, length) {
51
+ function truncate(value: string, length: number) {
52
52
  if (!value) return ''
53
53
  return value.length > length ? value.slice(0, length) + '...' : value
54
54
  }
55
55
 
56
- function jobsForRun(runId) {
56
+ function jobsForRun(runId: number) {
57
57
  return jobsByRunId()[runId] || []
58
58
  }
59
59
 
60
- function failedJobs(runId) {
60
+ function failedJobs(runId: number) {
61
61
  return jobsForRun(runId).filter(job => job.conclusion && job.conclusion !== 'success' && job.conclusion !== 'skipped' && job.conclusion !== 'neutral')
62
62
  }
63
63
  </script>
@@ -1,5 +1,6 @@
1
1
  <script client>
2
2
  import type { CommerceDashboardChartSeries } from '../../../../app/Actions/Dashboard/Commerce/commerce-dashboard'
3
+ import type { ChartOptions } from '../../../../views/dashboard/composables'
3
4
  import { useLazyCharts } from '../../../../views/dashboard/composables'
4
5
 
5
6
  const series = useReactiveProp('series', {
@@ -41,7 +42,7 @@ const ordersData = {
41
42
  data: [] as number[],
42
43
  }],
43
44
  }
44
- const chartOptions = {
45
+ const chartOptions: ChartOptions = {
45
46
  responsive: true,
46
47
  maintainAspectRatio: false,
47
48
  plugins: { legend: { display: true, position: 'top' } },
@@ -1,5 +1,5 @@
1
1
  <script client>
2
- import type { PaymentRecord } from '../../../../app/Actions/Dashboard/Commerce/payment-records'
2
+ import type { PaymentRecord, PaymentSummary } from '../../../../app/Actions/Dashboard/Commerce/payment-records'
3
3
  import {
4
4
  paymentsToCsv,
5
5
  summarizePayments,
@@ -2,6 +2,10 @@
2
2
  import { publicApplicationUrl } from '../../../../functions/public-application-url'
3
3
 
4
4
  export const blogUrl = publicApplicationUrl('/blog')
5
+
6
+ // Declared so the client block below gets `blogUrl` typed rather than inferred
7
+ // off the implicit bridge.
8
+ defineClientPayload({ blogUrl })
5
9
  </script>
6
10
 
7
11
  <template>
@@ -198,6 +202,7 @@ export const blogUrl = publicApplicationUrl('/blog')
198
202
  <script client>
199
203
  // Plain async transport for /api/dashboard/blog, which reads and writes the
200
204
  // markdown files in content/blog. It owns no state; the signals below do.
205
+ import type { BlogPost, BlogPostDraft } from '~/storage/framework/defaults/functions/blog'
201
206
  import { deleteBlogPost, fetchBlogPost, fetchBlogPosts, saveBlogPost } from '~/storage/framework/defaults/functions/blog'
202
207
 
203
208
  const publicBlogBaseUrl = {{ blogUrl }}
@@ -205,7 +210,7 @@ const publicBlogBaseUrl = {{ blogUrl }}
205
210
  // Fixed-length list purely to render the loading skeleton rows.
206
211
  const skeletonRows = [1, 2, 3, 4]
207
212
 
208
- const posts = state([])
213
+ const posts = state<BlogPost[]>([])
209
214
  const isLoading = state(true)
210
215
  const isSaving = state(false)
211
216
  const loadError = state('')
@@ -213,18 +218,29 @@ const searchQuery = state('')
213
218
  const statusFilter = state('all')
214
219
 
215
220
  const isEditorOpen = state(false)
216
- const editingPost = state(null)
221
+ const editingPost = state<BlogPost | null>(null)
217
222
  const loadedBody = state('')
218
223
  const bodyLoading = state(false)
219
224
  const bodyLoadFailed = state(false)
220
225
  const formError = state('')
221
226
 
222
- const postToDelete = state(null)
227
+ const postToDelete = state<BlogPost | null>(null)
223
228
  const isDeleting = state(false)
224
229
  const deleteError = state('')
225
230
  let editorRequestId = 0
226
231
 
227
- function publicPostUrl(slug) {
232
+ /**
233
+ * A caught value's message.
234
+ *
235
+ * `catch (error)` binds `unknown`, and reading `.message` off it assumes every
236
+ * throw is an `Error` - a rejected fetch or a thrown string would render
237
+ * `undefined` into the banner the user reads.
238
+ */
239
+ function messageOf(error: unknown): string {
240
+ return error instanceof Error ? error.message : String(error)
241
+ }
242
+
243
+ function publicPostUrl(slug: string) {
228
244
  return `${publicBlogBaseUrl}/${encodeURIComponent(String(slug))}`
229
245
  }
230
246
 
@@ -256,13 +272,13 @@ const filteredPosts = derived(() => {
256
272
  })
257
273
  })
258
274
 
259
- function statusClass(draft) {
275
+ function statusClass(draft: boolean) {
260
276
  return draft
261
277
  ? 'bg-amber-100 text-amber-800 dark:bg-amber-900/40 dark:text-amber-200'
262
278
  : 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-200'
263
279
  }
264
280
 
265
- function formatDate(value) {
281
+ function formatDate(value: string | number | Date | null | undefined) {
266
282
  if (!value)
267
283
  return 'No date'
268
284
 
@@ -278,7 +294,7 @@ function clearFilters() {
278
294
  statusFilter.set('all')
279
295
  }
280
296
 
281
- async function openEditor(post) {
297
+ async function openEditor(post: BlogPost) {
282
298
  const requestId = ++editorRequestId
283
299
  formError.set('')
284
300
  editingPost.set(post)
@@ -293,7 +309,7 @@ async function openEditor(post) {
293
309
  await loadEditorBody(post, requestId)
294
310
  }
295
311
 
296
- async function loadEditorBody(post, requestId = ++editorRequestId) {
312
+ async function loadEditorBody(post: BlogPost, requestId = ++editorRequestId) {
297
313
  bodyLoading.set(true)
298
314
  bodyLoadFailed.set(false)
299
315
  formError.set('')
@@ -307,7 +323,7 @@ async function loadEditorBody(post, requestId = ++editorRequestId) {
307
323
  catch (error) {
308
324
  if (requestId === editorRequestId) {
309
325
  bodyLoadFailed.set(true)
310
- formError.set(`Couldn't load this post's content: ${error.message}`)
326
+ formError.set(`Couldn't load this post's content: ${messageOf(error)}`)
311
327
  }
312
328
  }
313
329
  finally {
@@ -335,7 +351,7 @@ function closeEditor() {
335
351
  formError.set('')
336
352
  }
337
353
 
338
- async function submit(draft) {
354
+ async function submit(draft: BlogPostDraft) {
339
355
  isSaving.set(true)
340
356
  formError.set('')
341
357
 
@@ -351,14 +367,14 @@ async function submit(draft) {
351
367
  }
352
368
  catch (error) {
353
369
  // Keep the editor open on failure so the author's work is not thrown away.
354
- formError.set(error.message)
370
+ formError.set(messageOf(error))
355
371
  }
356
372
  finally {
357
373
  isSaving.set(false)
358
374
  }
359
375
  }
360
376
 
361
- function askDelete(post) {
377
+ function askDelete(post: BlogPost) {
362
378
  deleteError.set('')
363
379
  postToDelete.set(post)
364
380
  }
@@ -384,7 +400,7 @@ async function confirmDelete() {
384
400
  postToDelete.set(null)
385
401
  }
386
402
  catch (error) {
387
- deleteError.set(`Couldn't delete "${post.title}": ${error.message}`)
403
+ deleteError.set(`Couldn't delete "${post.title}": ${messageOf(error)}`)
388
404
  }
389
405
  finally {
390
406
  isDeleting.set(false)
@@ -399,7 +415,7 @@ async function load() {
399
415
  posts.set(list)
400
416
  }
401
417
  catch (error) {
402
- loadError.set(error.message)
418
+ loadError.set(messageOf(error))
403
419
  }
404
420
  finally {
405
421
  isLoading.set(false)
@@ -408,5 +424,11 @@ async function load() {
408
424
 
409
425
  // onMount, not effect: load() writes the same `posts` signal the derived signals
410
426
  // read, so an effect would re-trigger itself on every load.
411
- onMount(load)
427
+ //
428
+ // Wrapped rather than passed directly. `onMount` takes `() => void | StxCleanup`,
429
+ // and an async function returns a promise - which stx would treat as the
430
+ // cleanup value if it took one, and which hides a rejection either way. `load`
431
+ // handles its own errors into `loadError`, so the promise is deliberately
432
+ // dropped here.
433
+ onMount(() => { void load() })
412
434
  </script>
@@ -1,6 +1,7 @@
1
1
  <script client>
2
2
  import { dashboardApi } from '../../../../functions/dashboard-api'
3
3
  import { pushToast } from '../../../../functions/toasts'
4
+ import type { ChartCtor, ChartDataset, ChartLike, ChartOptions } from '~/storage/framework/defaults/views/dashboard/composables/useChart'
4
5
  import { useChart } from '~/storage/framework/defaults/views/dashboard/composables/useChart'
5
6
 
6
7
  interface ContentPostSummary {
@@ -49,30 +50,6 @@ interface ContentOverview {
49
50
  }
50
51
  }
51
52
 
52
- interface ChartDataset {
53
- data: number[]
54
- label?: string
55
- backgroundColor?: string | string[]
56
- borderColor?: string | string[]
57
- borderWidth?: number
58
- fill?: boolean
59
- tension?: number
60
- }
61
-
62
- interface ChartLike {
63
- data: {
64
- labels: string[]
65
- datasets: ChartDataset[]
66
- }
67
- destroy: () => void
68
- update: () => void
69
- }
70
-
71
- type ChartCtor = new (
72
- canvas: HTMLElement,
73
- config: { type: string, data: unknown, options?: unknown },
74
- ) => ChartLike
75
-
76
53
  const emptyOverview = (): ContentOverview => ({
77
54
  rangeDays: 30,
78
55
  stats: { posts: 0, pages: 0, comments: 0, views: 0 },
@@ -162,7 +139,7 @@ function categoriesChartData() {
162
139
  }
163
140
  }
164
141
 
165
- const chartOptions = {
142
+ const chartOptions: ChartOptions = {
166
143
  responsive: true,
167
144
  maintainAspectRatio: false,
168
145
  plugins: {
@@ -178,7 +155,7 @@ const chartOptions = {
178
155
  },
179
156
  }
180
157
 
181
- const categoryChartOptions = {
158
+ const categoryChartOptions: ChartOptions = {
182
159
  responsive: true,
183
160
  maintainAspectRatio: false,
184
161
  plugins: {
@@ -1,22 +1,9 @@
1
1
  <script client>
2
2
  import type { InboxActivity, InboxActivityRange } from '../../../../functions/inbox'
3
3
  import { fetchInboxActivity } from '../../../../functions/inbox'
4
+ import type { ChartCtor, ChartLike, ChartOptions } from '../../../../views/dashboard/composables/useChart'
4
5
  import { useChart } from '../../../../views/dashboard/composables/useChart'
5
6
 
6
- interface ChartLike {
7
- destroy: () => void
8
- update: () => void
9
- data: {
10
- labels: string[]
11
- datasets: Array<{ data: number[] }>
12
- }
13
- }
14
-
15
- type ChartCtor = new (
16
- canvas: HTMLElement,
17
- config: { type: string, data: unknown, options?: unknown },
18
- ) => ChartLike
19
-
20
7
  const folders = [
21
8
  { id: 'inbox', name: 'Inbox', icon: 'inbox' },
22
9
  { id: 'starred', name: 'Starred', icon: 'star' },
@@ -165,7 +152,7 @@ function chartData(data: InboxActivity) {
165
152
  }
166
153
  }
167
154
 
168
- const chartOptions = {
155
+ const chartOptions: ChartOptions = {
169
156
  responsive: true,
170
157
  maintainAspectRatio: false,
171
158
  interaction: { mode: 'index', intersect: false },
@@ -27,6 +27,14 @@ const envValues = state('')
27
27
  const revision = state('')
28
28
  const backupContent = state('')
29
29
  const backupUpdatedAt = state<string | null>(null)
30
+
31
+ // Formatted here rather than in the template: `:if` is a sibling attribute on
32
+ // the same element, not a wrapper around the expression, so it cannot narrow
33
+ // `backupUpdatedAt()` away from null for the `:text` beside it.
34
+ const backupSavedLabel = derived(() => {
35
+ const savedAt = backupUpdatedAt()
36
+ return savedAt ? `Saved ${new Date(savedAt).toLocaleString()}` : ''
37
+ })
30
38
  const showAddModal = state(false)
31
39
  const showBackupModal = state(false)
32
40
  const isLoading = state(true)
@@ -287,7 +295,7 @@ onMount(() => void loadEnvironment())
287
295
  @close="showBackupModal.set(false)"
288
296
  >
289
297
  <div class="space-y-3">
290
- <p :if="backupUpdatedAt()" class="text-stone-500 text-xs dark:text-neutral-400" :text="`Saved ${new Date(backupUpdatedAt()).toLocaleString()}`"></p>
298
+ <p :if="backupUpdatedAt()" class="text-stone-500 text-xs dark:text-neutral-400" :text="backupSavedLabel()"></p>
291
299
  <textarea readonly rows="18" class="block p-4 w-full font-mono leading-6 text-[#c9d1d9] text-xs bg-[#0d1117] border border-[#30363d] rounded-lg resize-y" :value="backupContent()"></textarea>
292
300
  </div>
293
301
  <template #footer>