@stacksjs/defaults 0.75.241 → 0.75.243

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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: stacks-actions
3
- description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the 672 default framework actions (auth, dashboard, commerce, content, deployment, jobs), action request/response handling, or action registration. Covers @stacksjs/actions and storage/framework/defaults/app/Actions/.
3
+ description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the 673 default framework actions (auth, dashboard, commerce, content, deployment, jobs), action request/response handling, or action registration. Covers @stacksjs/actions and storage/framework/defaults/app/Actions/.
4
4
  license: MIT
5
5
  compatibility: Bun >= 1.3.0, TypeScript
6
6
  allowed-tools: Read Edit Write Bash Grep Glob
@@ -11,7 +11,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
11
11
  | Package | Task guidance | Source entry | Retained tests |
12
12
  |---|---|---|---|
13
13
  | `@stacksjs/action-runner` | [`stacks-actions`](../stacks-actions/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/action-runner/src/index.ts) | 1 |
14
- | `@stacksjs/actions` | [`stacks-actions`](../stacks-actions/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/index.ts) | 74 |
14
+ | `@stacksjs/actions` | [`stacks-actions`](../stacks-actions/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/index.ts) | 75 |
15
15
  | `@stacksjs/ai` | [`stacks-ai`](../stacks-ai/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/ai/src/index.ts) | 17 |
16
16
  | `@stacksjs/alias` | [`stacks-alias`](../stacks-alias/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/alias/src/index.ts) | 1 |
17
17
  | `@stacksjs/analytics` | [`stacks-analytics`](../stacks-analytics/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/analytics/src/index.ts) | 6 |
@@ -21,7 +21,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
21
21
  | `@stacksjs/auth` | [`stacks-auth`](../stacks-auth/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/auth/src/index.ts) | 119 |
22
22
  | `@stacksjs/browser` | [`stacks-browser`](../stacks-browser/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser/src/index.ts) | 8 |
23
23
  | `@stacksjs/browser-extension` | [`stacks-browser-extension`](../stacks-browser-extension/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/browser-extension/src/index.ts) | 7 |
24
- | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 151 |
24
+ | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 152 |
25
25
  | `@stacksjs/build` | [`stacks-build`](../stacks-build/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/build/src/index.ts) | 2 |
26
26
  | `@stacksjs/cache` | [`stacks-cache`](../stacks-cache/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/cache/src/index.ts) | 11 |
27
27
  | `@stacksjs/calendar-api` | [`stacks-calendar`](../stacks-calendar/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/calendar-api/src/index.ts) | 2 |
@@ -65,7 +65,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
65
65
  | `@stacksjs/newsletter` | [`stacks-newsletter`](../stacks-newsletter/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/newsletter/src/index.ts) | 7 |
66
66
  | `@stacksjs/notifications` | [`stacks-notifications`](../stacks-notifications/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/notifications/src/index.ts) | 16 |
67
67
  | `@stacksjs/objects` | [`stacks-objects`](../stacks-objects/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/objects/src/index.ts) | 4 |
68
- | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 62 |
68
+ | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 63 |
69
69
  | `@stacksjs/pagination` | [`stacks-pagination`](../stacks-pagination/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/pagination/src/index.ts) | 0 |
70
70
  | `@stacksjs/path` | [`stacks-path`](../stacks-path/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/src/index.ts) | 11 |
71
71
  | `@stacksjs/payments` | [`stacks-payments`](../stacks-payments/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/payments/src/index.ts) | 28 |
@@ -119,7 +119,7 @@ Declared runtime exports: `BlogAdminError`, `DEFAULT_IOS_DEPENDENCIES`, `DEFAULT
119
119
 
120
120
  Local source modules: [src/action.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/action.ts), [src/add.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/add.ts), [src/blog-admin.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/blog-admin.ts), [src/build/ios-toolchain.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/build/ios-toolchain.ts), [src/commit.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/commit.ts), [src/dev/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/dev/index.ts), [src/discover-packages.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/discover-packages.ts), [src/generate/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/generate/index.ts), [src/github-release.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/github-release.ts), [src/helpers/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/helpers/index.ts), [src/library/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/library/index.ts), [src/lint/lint.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/lint/lint.ts), [src/lint/stx-gate.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/lint/stx-gate.ts), [src/make-command.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-command.ts), [src/make-job.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-job.ts), [src/make-policy.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-policy.ts), [src/make-resource.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make-resource.ts), [src/make.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/make.ts), [src/scaffold-crud.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/scaffold-crud.ts), [src/setup/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/setup/index.ts), [src/stacks.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/stacks.ts), [src/upgrade/package-project.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/upgrade/package-project.ts), [src/upgrade/route-actions.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/src/upgrade/route-actions.ts).
121
121
 
122
- Evidence directory: [74 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
122
+ Evidence directory: [75 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/actions/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
123
123
 
124
124
  ### @stacksjs/ai
125
125
 
@@ -215,7 +215,7 @@ Read the entrypoint and package export map for re-exported or type-only surfaces
215
215
 
216
216
  Local source modules: [src/commands/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/commands/index.ts).
217
217
 
218
- Evidence directory: [151 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
218
+ Evidence directory: [152 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
219
219
 
220
220
  ### @stacksjs/build
221
221
 
@@ -633,7 +633,7 @@ Declared runtime exports: `Activity`, `AnalyticsEvent`, `Auction`, `AuctionItem`
633
633
 
634
634
  Local source modules: [src/auto-crud.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/auto-crud.ts), [src/batch-loader.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/batch-loader.ts), [src/db.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/db.ts), [src/define-model.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/define-model.ts), [src/extend-model.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/extend-model.ts), [src/gdpr/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/gdpr/index.ts), [src/generate-database-schema.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/generate-database-schema.ts), [src/locked-row.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/locked-row.ts), [src/messaging.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/messaging.ts), [src/model-types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/model-types.ts), [src/ownership.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/ownership.ts), [src/paginator-request.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/paginator-request.ts), [src/row-token.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/row-token.ts), [src/subquery.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/subquery.ts), [src/traits/audit.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/audit.ts), [src/traits/billable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/billable.ts), [src/traits/categorizable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/categorizable.ts), [src/traits/commentable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/commentable.ts), [src/traits/likeable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/likeable.ts), [src/traits/taggable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/traits/taggable.ts), [src/transaction.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/transaction.ts), [src/types.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/types.ts), [src/utils/encrypted.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/utils/encrypted.ts), [src/utils/prunable.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/utils/prunable.ts).
635
635
 
636
- Evidence directory: [62 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
636
+ Evidence directory: [63 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
637
637
 
638
638
  ### @stacksjs/pagination
639
639
 
@@ -0,0 +1,144 @@
1
+ import type { DashboardHqDriverOptions } from '@stacksjs/types'
2
+
3
+ /**
4
+ * The one way a dashboard section talks to a hosted HQ product.
5
+ *
6
+ * `dashboard-provider.ts` decides *which* provider a section reads from and
7
+ * guarantees that a remote one cannot take the page down. This module is the
8
+ * other half: how a remote read is actually performed, so that each domain
9
+ * driver is a path and a shape rather than its own copy of base-URL joining,
10
+ * header handling and failure translation.
11
+ *
12
+ * Everything here exists to make one of `readThroughProvider`'s three degrade
13
+ * paths real:
14
+ *
15
+ * it rejects — an unreachable host, a non-2xx answer, or a body that
16
+ * is not JSON, all raised as {@link HqReadError}
17
+ * it never answers — a hung connection, aborted at `timeoutMs`. Nothing
18
+ * else in the seam imposes a deadline, so without this
19
+ * the "never answers" case was documented rather than
20
+ * handled: the request simply hung and took the
21
+ * section's request with it
22
+ * the wrong shape — left to the caller, which knows what its section
23
+ * renders; this module parses JSON and no more
24
+ *
25
+ * The read key only ever travels in `Authorization`. A URL is logged by
26
+ * proxies, kept in browser history and attached to error reports, and a
27
+ * read-scoped key is still a credential, so it never becomes a query
28
+ * parameter even though several hosted APIs would accept one.
29
+ */
30
+
31
+ /** How long a hosted read may take before it is treated as unreachable. */
32
+ export const HQ_DEFAULT_TIMEOUT_MS = 10_000
33
+
34
+ /** The upper bound on a configured timeout, so one cannot hold a section open. */
35
+ export const HQ_MAX_TIMEOUT_MS = 60_000
36
+
37
+ /**
38
+ * A hosted read that did not produce a payload.
39
+ *
40
+ * Carries `status` when the product answered and the answer was not a 2xx, so
41
+ * an operator reading the log can tell a misconfigured key (401/403) from a
42
+ * wrong project id (404) from the product being unwell (5xx). `status` is
43
+ * absent when no answer arrived at all.
44
+ */
45
+ export class HqReadError extends Error {
46
+ readonly status?: number
47
+ readonly url: string
48
+
49
+ constructor(message: string, url: string, status?: number) {
50
+ super(message)
51
+ this.name = 'HqReadError'
52
+ this.url = url
53
+ this.status = status
54
+ }
55
+ }
56
+
57
+ /** Whether the options carry enough to attempt a read at all. */
58
+ export function hqConfigured(options: DashboardHqDriverOptions | null | undefined): boolean {
59
+ return Boolean(options?.baseUrl && options?.token)
60
+ }
61
+
62
+ /**
63
+ * `base` and `path` joined into one URL, with exactly one slash between them.
64
+ *
65
+ * A base URL is hand-written in a config file, so it arrives with or without
66
+ * a trailing slash, and a driver's path is written as a literal that reads
67
+ * best with a leading one. Both spellings have to land on the same address.
68
+ */
69
+ export function hqUrl(base: string, path: string, query?: Record<string, string | number | undefined>): string {
70
+ const url = new URL(path.replace(/^\/+/, ''), base.endsWith('/') ? base : `${base}/`)
71
+ for (const [key, value] of Object.entries(query ?? {})) {
72
+ if (value !== undefined && value !== '')
73
+ url.searchParams.set(key, String(value))
74
+ }
75
+ return url.toString()
76
+ }
77
+
78
+ /** A configured timeout, clamped so neither a typo nor a zero disables it. */
79
+ export function hqTimeout(options: DashboardHqDriverOptions | null | undefined): number {
80
+ const asked = Number(options?.timeoutMs)
81
+ if (!Number.isFinite(asked) || asked <= 0)
82
+ return HQ_DEFAULT_TIMEOUT_MS
83
+ return Math.min(HQ_MAX_TIMEOUT_MS, asked)
84
+ }
85
+
86
+ /**
87
+ * One read from a hosted HQ product, as JSON.
88
+ *
89
+ * Throws {@link HqReadError} for every failure, which is what the caller
90
+ * wants: `readThroughProvider` turns a rejection into the section's empty
91
+ * payload with a reason, so a driver never has to handle a failure itself.
92
+ *
93
+ * `fetchImpl` exists for tests. Nothing in the application passes it.
94
+ */
95
+ export async function hqRead<T = unknown>(
96
+ options: DashboardHqDriverOptions | null | undefined,
97
+ path: string,
98
+ query?: Record<string, string | number | undefined>,
99
+ fetchImpl: typeof fetch = fetch,
100
+ ): Promise<T> {
101
+ if (!hqConfigured(options)) {
102
+ // Not a network failure, and worth saying so differently: a section that
103
+ // reads empty because nobody filled in a base URL is a configuration
104
+ // mistake an operator can fix, not an outage to wait out.
105
+ throw new HqReadError('The hosted provider has no baseUrl and token configured.', '')
106
+ }
107
+
108
+ const url = hqUrl(options!.baseUrl!, path, query)
109
+ let answer: Response
110
+ try {
111
+ answer = await fetchImpl(url, {
112
+ headers: {
113
+ // The read key, and only here. See the note at the top of this file.
114
+ authorization: `Bearer ${options!.token}`,
115
+ accept: 'application/json',
116
+ },
117
+ signal: AbortSignal.timeout(hqTimeout(options)),
118
+ })
119
+ }
120
+ catch (cause) {
121
+ // An abort and a refused connection are the same thing to a section: no
122
+ // data arrived. They are told apart in the message so the log is useful.
123
+ const aborted = (cause as { name?: string })?.name === 'TimeoutError'
124
+ || (cause as { name?: string })?.name === 'AbortError'
125
+ throw new HqReadError(
126
+ aborted
127
+ ? `The hosted provider did not answer within ${hqTimeout(options)}ms.`
128
+ : `The hosted provider could not be reached: ${(cause as Error)?.message ?? String(cause)}`,
129
+ url,
130
+ )
131
+ }
132
+
133
+ if (!answer.ok)
134
+ throw new HqReadError(`The hosted provider answered ${answer.status}.`, url, answer.status)
135
+
136
+ try {
137
+ return await answer.json() as T
138
+ }
139
+ catch {
140
+ // A 2xx that is not JSON is usually a sign-in page or a proxy's error
141
+ // document, which is a configuration problem wearing a success code.
142
+ throw new HqReadError('The hosted provider answered with something that is not JSON.', url, answer.status)
143
+ }
144
+ }
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.241",
5
+ "version": "0.75.243",
6
6
  "description": "Stacks, built into VS Code: stx templates, pickier linting and formatting, encrypted env files, the dev server preview and every buddy command. No other extensions required.",
7
7
  "license": "MIT",
8
8
  "funding": "https://github.com/sponsors/chrisbbreuer",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/defaults",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.241",
5
+ "version": "0.75.243",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/stacksjs/stacks.git",
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "@iconify-json/f7": "^1.2.2",
46
46
  "@iconify-json/hugeicons": "^1.2.35",
47
- "@stacksjs/mobile": "0.75.241",
47
+ "@stacksjs/mobile": "0.75.243",
48
48
  "@stacksjs/sanitizer": "^0.2.534",
49
49
  "@xterm/addon-fit": "^0.11.0",
50
50
  "@xterm/xterm": "^6.0.0",
@@ -1,3 +1,5 @@
1
+ @import('../UI/Table')
2
+
1
3
  <script client>
2
4
  import type { ManufacturerRecord } from '../../../../app/Actions/Dashboard/Commerce/manufacturer-records'
3
5
 
@@ -5,67 +7,62 @@ const records = useReactiveProp('records', [] as ManufacturerRecord[])
5
7
  const loading = useReactiveProp('loading', false)
6
8
  const emit = defineEmits()
7
9
 
8
- function formatDate(value: string): string {
9
- if (!value)
10
- return 'Not recorded'
11
- const normalized = /^\d{4}-\d{2}-\d{2} \d/.test(value)
12
- ? `${value.replace(' ', 'T')}Z`
13
- : value
14
- const date = new Date(normalized)
15
- return Number.isNaN(date.getTime())
16
- ? value
17
- : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(date)
10
+ /**
11
+ * `featured` becomes a flag badge beside the name rather than the starred line
12
+ * it used to be under it. The information is the same and it reads as a table
13
+ * column; the star is dropped because a `cell` slot cannot carry markup yet
14
+ * (stacksjs/stx#2095, stacksjs/stx#2096).
15
+ *
16
+ * `createdAt` is a `date` column, and `UI/Table` now normalises SQLite's
17
+ * space-separated timestamps the way this component used to do for itself.
18
+ */
19
+ const COLUMNS = [
20
+ { key: 'name', label: 'Manufacturer', sortable: true, emptyText: 'Unnamed manufacturer', class: 'min-w-48 font-medium text-gray-900 dark:text-white' },
21
+ {
22
+ key: 'featuredFlag',
23
+ label: '',
24
+ type: 'badge' as const,
25
+ emptyText: '',
26
+ badgeClasses: { featured: 'text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40' },
27
+ badgeLabels: { featured: 'Featured' },
28
+ searchable: false,
29
+ },
30
+ { key: 'description', label: 'Description', emptyText: 'No description recorded.', class: 'max-w-md min-w-64 line-clamp-2 text-gray-500 dark:text-neutral-400' },
31
+ { key: 'country', label: 'Country', sortable: true, emptyText: 'Not recorded', class: 'whitespace-nowrap' },
32
+ { key: 'productCount', label: 'Products', type: 'number' as const, sortable: true, align: 'right' as const, class: 'font-mono' },
33
+ { key: 'createdAt', label: 'Created', type: 'date' as const, sortable: true, class: 'whitespace-nowrap' },
34
+ ]
35
+
36
+ const ROW_ACTIONS = [
37
+ { action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
38
+ { action: 'delete', label: 'Delete', icon: 'i-hugeicons-waste', class: 'text-red-500' },
39
+ ]
40
+
41
+ /** A badge column maps a row field, so the flag needs one. */
42
+ const rows = derived(() => records().map(record => ({
43
+ ...record,
44
+ featuredFlag: record.featured ? 'featured' : '',
45
+ })))
46
+
47
+ function handleRowAction(payload: { action: string, row: ManufacturerRecord }): void {
48
+ emit(payload.action, payload.row)
18
49
  }
19
50
  </script>
20
51
 
21
- <div class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
22
- <div :if="loading() && records().length === 0" class="flex items-center justify-center p-10 text-gray-500 text-sm dark:text-neutral-400">
23
- <span class="mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>
24
- Loading manufacturers
25
- </div>
26
- <div :else-if="records().length === 0" class="p-10 text-center">
27
- <p class="font-medium text-gray-900 text-sm dark:text-white">No manufacturers found</p>
28
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Add a manufacturer or adjust the current filters.</p>
29
- </div>
30
- <div :else class="overflow-x-auto">
31
- <table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700">
32
- <thead class="bg-gray-50 dark:bg-neutral-900/50">
33
- <tr>
34
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Manufacturer</th>
35
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Description</th>
36
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Country</th>
37
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-right text-xs uppercase dark:text-neutral-400">Products</th>
38
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs uppercase dark:text-neutral-400">Created</th>
39
- <th scope="col" class="relative px-4 py-3"><span class="sr-only">Actions</span></th>
40
- </tr>
41
- </thead>
42
- <tbody class="divide-gray-100 divide-y dark:divide-neutral-700">
43
- <template :for="record in records()" :key="record.id">
44
- <tr class="hover:bg-gray-50/60 dark:hover:bg-neutral-700/40">
45
- <td class="px-4 py-4 min-w-48">
46
- <p class="font-medium text-gray-900 text-sm dark:text-white">{{ record.name || 'Unnamed manufacturer' }}</p>
47
- <span :if="record.featured" class="inline-flex items-center mt-1 font-medium text-blue-700 text-xs dark:text-blue-300">
48
- <span class="mr-1 h-3.5 w-3.5 i-hugeicons-star"></span>
49
- Featured
50
- </span>
51
- </td>
52
- <td class="px-4 py-4 max-w-md min-w-64 text-gray-500 text-sm dark:text-neutral-400">
53
- <p class="line-clamp-2">{{ record.description || 'No description recorded.' }}</p>
54
- </td>
55
- <td class="px-4 py-4 text-gray-700 text-sm whitespace-nowrap dark:text-neutral-200">{{ record.country || 'Not recorded' }}</td>
56
- <td class="px-4 py-4 font-mono text-gray-700 text-right text-sm dark:text-neutral-200">{{ record.productCount }}</td>
57
- <td class="px-4 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ formatDate(record.createdAt) }}</td>
58
- <td class="px-4 py-3 text-right whitespace-nowrap">
59
- <Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit ' + record.name" @click="emit('edit', record)">
60
- <span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
61
- </Button>
62
- <Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + record.name" @click="emit('delete', record)">
63
- <span class="block h-4 w-4 text-red-500 i-hugeicons-waste"></span>
64
- </Button>
65
- </td>
66
- </tr>
67
- </template>
68
- </tbody>
69
- </table>
70
- </div>
71
- </div>
52
+ {{--
53
+ No `searchable` or `filterable`: the parent dashboard already owns the
54
+ search box and the filter selects, and a second set inside the table would
55
+ duplicate them. Paging is what was missing, and Table brings it.
56
+ --}}
57
+ <Table
58
+ :columns="COLUMNS"
59
+ :data="rows()"
60
+ :loading="loading()"
61
+ :row-actions="ROW_ACTIONS"
62
+ action-label-key="name"
63
+ paginated
64
+ :page-size="25"
65
+ empty-title="No manufacturers found"
66
+ empty-description="Add a manufacturer or adjust the current filters."
67
+ @row-action="handleRowAction"
68
+ />
@@ -1,3 +1,5 @@
1
+ @import('../UI/Table')
2
+
1
3
  <script client>
2
4
  import type { ProductUnitRecord } from '../../../../app/Actions/Dashboard/Commerce/product-unit-records'
3
5
 
@@ -5,67 +7,63 @@ const records = useReactiveProp('records', [] as ProductUnitRecord[])
5
7
  const loading = useReactiveProp('loading', false)
6
8
  const emit = defineEmits()
7
9
 
8
- function formatDate(value: string): string {
9
- if (!value)
10
- return 'Unknown'
11
- const date = new Date(value)
12
- return Number.isNaN(date.getTime())
13
- ? value
14
- : new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(date)
15
- }
16
- </script>
10
+ /**
11
+ * No slots. `UI/Table`'s `cell` and `actions` slots cannot be used yet -
12
+ * slot content loses its `{{ … }}` (stacksjs/stx#2095) and `stx typecheck`
13
+ * rejects every scoped slot prop (stacksjs/stx#2096) - so the two custom
14
+ * cells here are expressed as column options instead: `subtitleKey` for the
15
+ * abbreviation under the name, and a flag `badge` column for the default
16
+ * marker.
17
+ */
18
+ const COLUMNS = [
19
+ { key: 'name', label: 'Unit', sortable: true, subtitleKey: 'abbreviation', class: 'font-medium text-gray-900 dark:text-white' },
20
+ {
21
+ key: 'defaultFlag',
22
+ label: '',
23
+ type: 'badge' as const,
24
+ // A flag rather than a status: only the default unit is marked, and the
25
+ // rest of the column stays blank.
26
+ emptyText: '',
27
+ badgeClasses: { default: 'text-emerald-700 dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40' },
28
+ badgeLabels: { default: 'Default' },
29
+ searchable: false,
30
+ },
31
+ { key: 'type', label: 'Type', sortable: true, class: 'whitespace-nowrap' },
32
+ { key: 'productName', label: 'Product', sortable: true, emptyText: 'Unassigned' },
33
+ { key: 'description', label: 'Description', class: 'max-w-sm', emptyText: 'No description' },
34
+ { key: 'createdAt', label: 'Created', type: 'datetime' as const, sortable: true, class: 'whitespace-nowrap' },
35
+ ]
17
36
 
18
- <div class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
19
- <div :if="loading() && records().length === 0" class="flex items-center justify-center py-16 text-gray-500 text-sm dark:text-neutral-400">
20
- <span class="mr-2 h-5 w-5 animate-spin i-hugeicons-loading-03"></span>
21
- Loading product units
22
- </div>
37
+ const ROW_ACTIONS = [
38
+ { action: 'edit', label: 'Edit', icon: 'i-hugeicons-edit-01' },
39
+ { action: 'delete', label: 'Delete', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
40
+ ]
23
41
 
24
- <div :else-if="records().length === 0" class="px-6 py-16 text-center">
25
- <span class="inline-block h-8 w-8 text-gray-400 i-hugeicons-package"></span>
26
- <p class="mt-3 font-medium text-gray-900 text-sm dark:text-white">No product units found</p>
27
- <p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Adjust the filters or add a ProductUnit record.</p>
28
- </div>
42
+ /** `defaultFlag` carries the badge value, since a badge column maps a row field. */
43
+ const rows = derived(() => records().map(record => ({
44
+ ...record,
45
+ defaultFlag: record.isDefault ? 'default' : '',
46
+ })))
47
+
48
+ function handleRowAction(payload: { action: string, row: ProductUnitRecord }): void {
49
+ emit(payload.action, payload.row)
50
+ }
51
+ </script>
29
52
 
30
- <div :else class="overflow-x-auto">
31
- <table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700">
32
- <thead class="bg-gray-50 dark:bg-neutral-900/50">
33
- <tr>
34
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Unit</th>
35
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Type</th>
36
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Product</th>
37
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Description</th>
38
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Created</th>
39
- <th scope="col" class="px-4 py-3 font-semibold text-gray-600 text-right text-xs dark:text-neutral-300"><span class="sr-only">Actions</span></th>
40
- </tr>
41
- </thead>
42
- <tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
43
- <template :for="record in records()" :key="record.id">
44
- <tr class="align-top">
45
- <td class="px-4 py-4">
46
- <div class="flex gap-2 items-center">
47
- <p class="font-medium text-gray-900 text-sm dark:text-white">{{ record.name }}</p>
48
- <span :if="record.isDefault" class="px-2 py-0.5 font-medium text-emerald-700 text-xs dark:text-emerald-300 bg-emerald-50 dark:bg-emerald-950/40 rounded-full">Default</span>
49
- </div>
50
- <p class="mt-1 text-gray-500 text-xs dark:text-neutral-400">{{ record.abbreviation }}</p>
51
- </td>
52
- <td class="px-4 py-4 text-gray-600 text-sm whitespace-nowrap dark:text-neutral-300">{{ record.type }}</td>
53
- <td class="px-4 py-4 text-gray-600 text-sm dark:text-neutral-300">{{ record.productName || 'Unassigned' }}</td>
54
- <td class="px-4 py-4 max-w-sm text-gray-600 text-sm dark:text-neutral-300">{{ record.description || 'No description' }}</td>
55
- <td class="px-4 py-4 text-gray-500 text-sm whitespace-nowrap dark:text-neutral-400">{{ formatDate(record.createdAt) }}</td>
56
- <td class="px-4 py-4">
57
- <div class="flex gap-1 justify-end">
58
- <Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit ' + record.name" @click="emit('edit', record)">
59
- <span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-01"></span>
60
- </Button>
61
- <Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + record.name" @click="emit('delete', record)">
62
- <span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span>
63
- </Button>
64
- </div>
65
- </td>
66
- </tr>
67
- </template>
68
- </tbody>
69
- </table>
70
- </div>
71
- </div>
53
+ {{--
54
+ No `searchable` or `filterable`: the parent dashboard already owns the
55
+ search box and the filter selects, and a second set inside the table would
56
+ duplicate them. Paging is what was missing, and Table brings it.
57
+ --}}
58
+ <Table
59
+ :columns="COLUMNS"
60
+ :data="rows()"
61
+ :loading="loading()"
62
+ :row-actions="ROW_ACTIONS"
63
+ action-label-key="name"
64
+ paginated
65
+ :page-size="25"
66
+ empty-title="No product units found"
67
+ empty-description="Adjust the filters or add a ProductUnit record."
68
+ @row-action="handleRowAction"
69
+ />