@stacksjs/defaults 0.75.244 → 0.75.246

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 (42) hide show
  1. package/ai/skills/stacks-dashboard/SKILL.md +2 -2
  2. package/ai/skills/stacks-native/CATALOG.md +7 -7
  3. package/ai/skills/stacks-path/SKILL.md +19 -1
  4. package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +2 -1
  5. package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +2 -1
  6. package/app/Actions/Dashboard/Source/source-inventory.ts +2 -1
  7. package/ide/vscode/package.json +1 -1
  8. package/package.json +2 -2
  9. package/project/storage/framework/tsconfig.app.json +8 -0
  10. package/resources/components/Dashboard/Analytics/WebAnalyticsDashboard.stx +5 -5
  11. package/resources/components/Dashboard/Ci/CiDashboard.stx +2 -2
  12. package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +1 -1
  13. package/resources/components/Dashboard/Content/AuthorsDashboard.stx +59 -20
  14. package/resources/components/Dashboard/Content/BlogDashboard.stx +2 -2
  15. package/resources/components/Dashboard/Content/ContentDashboard.stx +2 -2
  16. package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +50 -18
  17. package/resources/components/Dashboard/Content/PagesDashboard.stx +54 -10
  18. package/resources/components/Dashboard/Content/PostsDashboard.stx +91 -20
  19. package/resources/components/Dashboard/Content/SeoDashboard.stx +61 -59
  20. package/resources/components/Dashboard/Email/InboxSettingsDashboard.stx +3 -3
  21. package/resources/components/Dashboard/Environment/EnvironmentDashboard.stx +1 -1
  22. package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +3 -3
  23. package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +2 -2
  24. package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +1 -1
  25. package/resources/components/Dashboard/Library/ComponentsDashboard.stx +47 -81
  26. package/resources/components/Dashboard/Library/FunctionsDashboard.stx +24 -15
  27. package/resources/components/Dashboard/Library/PackagesDashboard.stx +68 -13
  28. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +12 -15
  29. package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +3 -3
  30. package/resources/components/Dashboard/Settings/AppearanceSettingsDashboard.stx +3 -3
  31. package/resources/components/Dashboard/Settings/SettingsDashboard.stx +1 -1
  32. package/resources/components/Dashboard/Teams/TeamInvitationDashboard.stx +2 -2
  33. package/resources/components/Dashboard/UI/Button.stx +29 -3
  34. package/resources/components/Dashboard/UI/SearchFilter.stx +46 -0
  35. package/resources/components/Dashboard/UI/Table.stx +43 -20
  36. package/resources/functions/dashboard/data.ts +27 -4
  37. package/resources/functions/dashboard/sidebar.ts +3 -2
  38. package/resources/plugins/preloader.ts +9 -1
  39. package/views/dashboard/layouts/default.stx +17 -13
  40. package/views/system-tray/index.stx +1 -1
  41. package/resources/components/Dashboard/Commerce/Delivery/SearchFilter.stx +0 -24
  42. package/resources/components/Dashboard/UI/FilterBar.stx +0 -62
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: stacks-dashboard
3
- description: Use when building or customizing the Stacks admin dashboard, including dashboard pages, model management views, analytics widgets, commerce dashboards, content management, settings panels, deployment monitoring, job/queue management, or the 414 built-in dashboard components. Covers the dashboard system at storage/framework/defaults/.
3
+ description: Use when building or customizing the Stacks admin dashboard, including dashboard pages, model management views, analytics widgets, commerce dashboards, content management, settings panels, deployment monitoring, job/queue management, or the 413 built-in dashboard components. Covers the dashboard system at storage/framework/defaults/.
4
4
  license: MIT
5
5
  compatibility: Bun >= 1.3.0, TypeScript
6
6
  allowed-tools: Read Edit Write Bash Grep Glob
@@ -8,7 +8,7 @@ allowed-tools: Read Edit Write Bash Grep Glob
8
8
 
9
9
  # Stacks Dashboard
10
10
 
11
- The Stacks admin dashboard provides a full-featured admin panel with 100+ route views, 414 components, and a multi-section layout.
11
+ The Stacks admin dashboard provides a full-featured admin panel with 100+ route views, 413 components, and a multi-section layout.
12
12
 
13
13
  ## Key Paths
14
14
  - Dashboard components: `storage/framework/defaults/resources/components/Dashboard/`
@@ -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) | 152 |
24
+ | `@stacksjs/buddy` | [`stacks-buddy`](../stacks-buddy/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/buddy/src/index.ts) | 153 |
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 |
@@ -67,7 +67,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
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
68
  | `@stacksjs/orm` | [`stacks-orm`](../stacks-orm/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/orm/src/index.ts) | 64 |
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
- | `@stacksjs/path` | [`stacks-path`](../stacks-path/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/src/index.ts) | 11 |
70
+ | `@stacksjs/path` | [`stacks-path`](../stacks-path/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/src/index.ts) | 12 |
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 |
72
72
  | `@stacksjs/push` | [`stacks-push`](../stacks-push/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/push/src/index.ts) | 6 |
73
73
  | `@stacksjs/query-builder` | [`stacks-query-builder`](../stacks-query-builder/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/query-builder/src/index.ts) | 6 |
@@ -88,7 +88,7 @@ Browser runtime bindings are documented separately in `stacks-auto-imports`.
88
88
  | `@stacksjs/socials` | [`stacks-socials`](../stacks-socials/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/socials/src/index.ts) | 17 |
89
89
  | `@stacksjs/storage` | [`stacks-storage`](../stacks-storage/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/storage/src/index.ts) | 42 |
90
90
  | `@stacksjs/strings` | [`stacks-strings`](../stacks-strings/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/strings/src/index.ts) | 7 |
91
- | `@stacksjs/testing` | [`stacks-testing`](../stacks-testing/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/testing/src/index.ts) | 17 |
91
+ | `@stacksjs/testing` | [`stacks-testing`](../stacks-testing/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/testing/src/index.ts) | 18 |
92
92
  | `@stacksjs/tinker` | [`stacks-repl`](../stacks-repl/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/tinker/src/index.ts) | 1 |
93
93
  | `@stacksjs/tunnel` | [`stacks-tunnel`](../stacks-tunnel/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/tunnel/src/index.ts) | 1 |
94
94
  | `@stacksjs/types` | [`stacks-types`](../stacks-types/SKILL.md) | [entrypoint](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/types/src/index.ts) | 2 |
@@ -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: [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.
218
+ Evidence directory: [153 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
 
@@ -645,11 +645,11 @@ Declared runtime exports: `isCursorPaginator`, `isPaginator`, `isSimplePaginator
645
645
 
646
646
  Guidance: [`stacks-path`](../stacks-path/SKILL.md). Source: [storage/framework/core/path/src/index.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/src/index.ts).
647
647
 
648
- Declared runtime exports: `DEFAULTS_SYNC_MARKER`, `actionRunnerPath`, `actionsPath`, `aiPath`, `aliasPath`, `analyticsPath`, `appPath`, `applyRuntimeDirectoryEnv`, `arraysPath`, `assetsPath`, `authPath`, `basename`, `browserPath`, `buddyPath`, `buildEnginePath`, `buildPath`, `builtUserActionsPath`, `cachePath`, `chartsPath`, `chatPath`, `cliPath`, `cloudPath`, `cloudStatePath`, `collectionsPath`, `commandsPath`, `componentsPath`, `configPath`, `coreApiPath`, `coreEnvPath`, `corePath`, `coreStoragePath`, `customElementsDataPath`, `databasePath`, `datetimePath`, `defaultsAppPath`, `defaultsPackagePath`, `defaultsResourcesPath`, `delimiter`, `desktopPath`, `developmentPath`, `dirname`, `dnsPath`, `docsPath`, `emailPath`, `ensureRuntimeDirectories`, `enumsPath`, `errorHandlingPath`, `eslintPluginPath`, `eventsPath`, `examplesPath`, `extname`, `fakerPath`, `featureFlagsPath`, `featuresPath`, `findProjectPath`, `frameworkCloudPath`, `frameworkPath`, `frameworkRuntimePath`, `functionsPath`, `gitPath`, `healthPath`, `homeDir`, `inspectDefaultsProvenance`, `installedDefaultsVersion`, `isAbsolute`, `isCompiledBinary`, `jobsPath`, `join`, `langPath`, `layoutsPath`, `libraryPackagePath`, `libsEntriesPath`, `libsPath`, `lintPath`, `listenersPath`, `loggingPath`, `logsPath`, `mergeDirectoryInto`, `modelMetaPath`, `modelsPath`, `modulesPath`, `newsletterPath`, `normalize`, `notificationsPath`, `objectsPath`, `onboardingPath`, `ormPath`, `paginationPath`, `path`, `pathPath`, `paymentsPath`, `projectConfigPath`, `projectPath`, `publicPath`, `pushPath`, `queryBuilderPath`, `queuePath`, `realtimePath`, `relative`, `relativeActionsPath`, `replPath`, `resolve`, `resolveModulePath`, `resolveUserLibBase`, `resourcesPath`, `routerPath`, `routesPath`, `runtimeDirectoryEnv`, `runtimePath`, `schedulerPath`, `scriptsPath`, `searchEnginePath`, `securityPath`, `sep`, `serverPath`, `serverlessPath`, `settingsPath`, `shellPath`, `siteConfigPath`, `skillsPath`, `slugPath`, `smsPath`, `socialsPath`, `stacksBackupPath`, `stacksLockPath`, `stacksPath`, `storagePath`, `storesPath`, `stringsPath`, `stxPath`, `testingPath`, `testsPath`, `tinkerPath`, `toNamespacedPath`, `typesPath`, `uiPath`, `userActionsPath`, `userComponentsPath`, `userControllersPath`, `userDatabasePath`, `userEmailsPath`, `userEventsPath`, `userFunctionsPath`, `userJobsPath`, `userLibsPath`, `userListenersPath`, `userMailPath`, `userMiddlewarePath`, `userMigrationsPath`, `userModelsPath`, `userNotificationsPath`, `userServerPath`, `userViewsPath`, `utilsPath`, `validationPath`, `viewsPath`.
648
+ Declared runtime exports: `DEFAULTS_SYNC_MARKER`, `actionRunnerPath`, `actionsPath`, `aiPath`, `aliasPath`, `analyticsPath`, `appPath`, `applyRuntimeDirectoryEnv`, `arraysPath`, `assetsPath`, `authPath`, `basename`, `browserPath`, `buddyPath`, `buildEnginePath`, `buildPath`, `builtUserActionsPath`, `cachePath`, `chartsPath`, `chatPath`, `cliPath`, `cloudPath`, `cloudStatePath`, `collectionsPath`, `commandsPath`, `componentsPath`, `configPath`, `coreApiPath`, `coreEnvPath`, `corePath`, `coreStoragePath`, `customElementsDataPath`, `databasePath`, `datetimePath`, `defaultsAppPath`, `defaultsPackagePath`, `defaultsResourcesPath`, `delimiter`, `desktopPath`, `developmentPath`, `dirname`, `dnsPath`, `docsPath`, `emailPath`, `ensureRuntimeDirectories`, `enumsPath`, `errorHandlingPath`, `eslintPluginPath`, `eventsPath`, `examplesPath`, `extname`, `fakerPath`, `featureFlagsPath`, `featuresPath`, `findProjectPath`, `frameworkCloudPath`, `frameworkDefaultsRoot`, `frameworkPath`, `frameworkRuntimePath`, `functionsPath`, `gitPath`, `healthPath`, `homeDir`, `inspectDefaultsProvenance`, `installedDefaultsVersion`, `isAbsolute`, `isCompiledBinary`, `isInstalledDefaultsRoot`, `jobsPath`, `join`, `langPath`, `layoutsPath`, `libraryPackagePath`, `libsEntriesPath`, `libsPath`, `linkPackagedDefaults`, `lintPath`, `listenersPath`, `loggingPath`, `logsPath`, `mergeDirectoryInto`, `modelMetaPath`, `modelsPath`, `modulesPath`, `newsletterPath`, `normalize`, `notificationsPath`, `objectsPath`, `onboardingPath`, `ormPath`, `packagedDefaultsRoot`, `paginationPath`, `path`, `pathPath`, `paymentsPath`, `projectConfigPath`, `projectPath`, `publicPath`, `pushPath`, `queryBuilderPath`, `queuePath`, `realtimePath`, `relative`, `relativeActionsPath`, `replPath`, `resolve`, `resolveModulePath`, `resolveUserLibBase`, `resourcesPath`, `routerPath`, `routesPath`, `runtimeDirectoryEnv`, `runtimePath`, `schedulerPath`, `scriptsPath`, `searchEnginePath`, `securityPath`, `sep`, `serverPath`, `serverlessPath`, `settingsPath`, `shellPath`, `siteConfigPath`, `skillsPath`, `slugPath`, `smsPath`, `socialsPath`, `stacksBackupPath`, `stacksLockPath`, `stacksPath`, `storagePath`, `storesPath`, `stringsPath`, `stxPath`, `testingPath`, `testsPath`, `tinkerPath`, `toNamespacedPath`, `typesPath`, `uiPath`, `userActionsPath`, `userComponentsPath`, `userControllersPath`, `userDatabasePath`, `userEmailsPath`, `userEventsPath`, `userFunctionsPath`, `userJobsPath`, `userLibsPath`, `userListenersPath`, `userMailPath`, `userMiddlewarePath`, `userMigrationsPath`, `userModelsPath`, `userNotificationsPath`, `userServerPath`, `userViewsPath`, `usesVendoredDefaults`, `utilsPath`, `validationPath`, `viewsPath`.
649
649
 
650
650
  Local source modules: [src/module-resolution.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/src/module-resolution.ts), [src/project.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/src/project.ts).
651
651
 
652
- Evidence directory: [11 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
652
+ Evidence directory: [12 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/path/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
653
653
 
654
654
  ### @stacksjs/payments
655
655
 
@@ -851,7 +851,7 @@ Read the entrypoint and package export map for re-exported or type-only surfaces
851
851
 
852
852
  Local source modules: [src/console.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/testing/src/console.ts), [src/disposable-database.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/testing/src/disposable-database.ts), [src/events.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/testing/src/events.ts), [src/feature.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/testing/src/feature.ts), [src/isolated.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/testing/src/isolated.ts), [src/mail.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/testing/src/mail.ts), [src/time.ts](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/testing/src/time.ts).
853
853
 
854
- Evidence directory: [17 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/testing/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
854
+ Evidence directory: [18 retained tests](https://github.com/stacksjs/stacks/blob/main/storage/framework/core/testing/tests). These files are evidence pointers, not a claim that all tests were run for this catalog.
855
855
 
856
856
  ### @stacksjs/tinker
857
857
 
@@ -91,11 +91,29 @@ buildEnginePath()
91
91
  libsEntriesPath()
92
92
  frameworkPath() // ~/storage/framework
93
93
  corePath() // ~/storage/framework/core
94
- frameworkPath('defaults') // ~/storage/framework/defaults
94
+ frameworkPath('defaults') // the defaults tree (see below)
95
95
  defaultsAppPath()
96
96
  defaultsResourcesPath()
97
+ frameworkDefaultsRoot() // where that tree actually is
98
+ usesVendoredDefaults() // true when it is a real directory
99
+ linkPackagedDefaults() // (re)create the link; boot calls it
97
100
  ```
98
101
 
102
+ `storagePath('framework/defaults/...')` and `frameworkPath('defaults/...')`
103
+ resolve to `storage/framework/defaults` only when that is a real, vendored
104
+ directory (the framework checkout, an app made with `--vendor-defaults` or
105
+ `buddy publish:defaults`). Otherwise they resolve to the installed
106
+ `node_modules/@stacksjs/defaults`. A new app keeps a gitignored
107
+ `storage/framework/defaults` link to the package for people and tools, but
108
+ framework code resolves the package itself: a module loaded through a link
109
+ created after the process started fails in Bun.
110
+
111
+ In code and templates, import a default by package name:
112
+ `@stacksjs/defaults/<path>.ts` in TypeScript, `@stacksjs/defaults/<path>` with
113
+ no extension in a `.stx` script (`stx typecheck` rejects `.ts` there). `storage/framework/tsconfig.app.json` maps that
114
+ onto a vendored tree when there is one and falls back to the package, so it
115
+ resolves in every layout; `~/storage/framework/defaults/...` does not.
116
+
99
117
  ### Relative Path Variants
100
118
  Only some helpers have relative modes. Inspect the selected signature rather
101
119
  than deriving a function name from another path helper:
@@ -2,6 +2,7 @@ import { Action } from '@stacksjs/actions/runtime'
2
2
  import { existsSync, readdirSync } from 'node:fs'
3
3
  import { join } from 'node:path'
4
4
  import process from 'node:process'
5
+ import { frameworkDefaultsRoot } from '@stacksjs/path'
5
6
  import { countRows } from '../../../../resources/functions/dashboard/data'
6
7
  import { dashboardOperationalError, dashboardOperationalIssue } from '../dashboard-response'
7
8
  import { resolveDashboardModel } from '../../../../resources/functions/dashboard/model-access'
@@ -123,7 +124,7 @@ export default new Action({
123
124
  async handle() {
124
125
  const cwd = process.cwd()
125
126
  const userlandDir = join(cwd, 'app', 'Models')
126
- const defaultsDir = join(cwd, 'storage/framework/defaults/app/Models')
127
+ const defaultsDir = join(frameworkDefaultsRoot(), 'app/Models')
127
128
 
128
129
  let userland: Array<Omit<DiscoveredModel, 'source'>>
129
130
  let defaults: Array<Omit<DiscoveredModel, 'source'>>
@@ -4,6 +4,7 @@ import { env } from '@stacksjs/env'
4
4
  import { readdirSync } from 'node:fs'
5
5
  import { basename, extname, join } from 'node:path'
6
6
  import process from 'node:process'
7
+ import { frameworkDefaultsRoot } from '@stacksjs/path'
7
8
  import { resolveDashboardModel } from '../../../../resources/functions/dashboard/model-access'
8
9
  import { dashboardOperationalError } from '../dashboard-response'
9
10
 
@@ -114,7 +115,7 @@ function iconForModel(name: string): string {
114
115
  }
115
116
 
116
117
  async function loadSearchableModels(): Promise<SearchableModel[]> {
117
- const defaultsRoot = join(process.cwd(), 'storage/framework/defaults/app/Models')
118
+ const defaultsRoot = join(frameworkDefaultsRoot(), 'app/Models')
118
119
  const userRoot = join(process.cwd(), 'app/Models')
119
120
  const merged = new Map<string, string>()
120
121
 
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync, statSync } from 'node:fs'
2
2
  import { basename, join, relative } from 'node:path'
3
+ import { frameworkDefaultsRoot } from '@stacksjs/path'
3
4
 
4
5
  export interface SourceInventoryItem {
5
6
  name: string
@@ -71,7 +72,7 @@ export function parseCommandSource(
71
72
  export async function discoverActionSources(projectRoot: string): Promise<SourceInventoryItem[]> {
72
73
  const roots: SourceRoot[] = [
73
74
  {
74
- directory: join(projectRoot, 'storage/framework/defaults/app/Actions'),
75
+ directory: join(frameworkDefaultsRoot(projectRoot), 'app/Actions'),
75
76
  origin: 'Framework',
76
77
  },
77
78
  {
@@ -2,7 +2,7 @@
2
2
  "publisher": "Stacks",
3
3
  "name": "vscode-stacks",
4
4
  "displayName": "Stacks",
5
- "version": "0.75.244",
5
+ "version": "0.75.246",
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.244",
5
+ "version": "0.75.246",
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.244",
47
+ "@stacksjs/mobile": "0.75.246",
48
48
  "@stacksjs/sanitizer": "^0.2.534",
49
49
  "@xterm/addon-fit": "^0.11.0",
50
50
  "@xterm/xterm": "^6.0.0",
@@ -17,6 +17,14 @@
17
17
  "Middleware/*": ["../../app/Middleware/*"],
18
18
  "Models/*": ["../../app/Models/*"],
19
19
  "Notifications/*": ["../../app/Notifications/*"],
20
+ // The framework defaults by package name. Lands on this project's
21
+ // `storage/framework/defaults` when there is one (a framework checkout, a
22
+ // vendored app, or the link to node_modules), and falls back to the
23
+ // installed `@stacksjs/defaults` when the mapped file does not exist -
24
+ // Bun and tsc both try the package after a `paths` miss. So one
25
+ // specifier resolves in every layout, which `~/storage/framework/
26
+ // defaults/...` does not.
27
+ "@stacksjs/defaults/*": ["./defaults/*"],
20
28
  "~/*": ["../../*"],
21
29
  "@/*": ["../../resources/*"]
22
30
  }
@@ -208,11 +208,11 @@ import type {
208
208
  PageData,
209
209
  ReferrerData,
210
210
  TrafficDataPoint,
211
- } from '~/storage/framework/defaults/functions/analytics'
212
- import { fetchWebAnalytics } from '~/storage/framework/defaults/functions/analytics'
213
- import { pushToast } from '~/storage/framework/defaults/functions/toasts'
214
- import type { ChartCtor, ChartLike } from '~/storage/framework/defaults/views/dashboard/composables/useChart'
215
- import { useChart } from '~/storage/framework/defaults/views/dashboard/composables/useChart'
211
+ } from '@stacksjs/defaults/functions/analytics'
212
+ import { fetchWebAnalytics } from '@stacksjs/defaults/functions/analytics'
213
+ import { pushToast } from '@stacksjs/defaults/functions/toasts'
214
+ import type { ChartCtor, ChartLike } from '@stacksjs/defaults/views/dashboard/composables/useChart'
215
+ import { useChart } from '@stacksjs/defaults/views/dashboard/composables/useChart'
216
216
 
217
217
  const analyticsOverview = state<AnalyticsOverview>({
218
218
  realtimeVisitors: 0,
@@ -7,8 +7,8 @@
7
7
  //
8
8
  // No <script server>, no window./document. reads - per #1838 the page
9
9
  // is pure presentation against the store.
10
- import { useRole } from '~/storage/framework/defaults/views/dashboard/composables/useRole'
11
- import { ciStore } from '~/storage/framework/defaults/views/dashboard/stores/ci'
10
+ import { useRole } from '@stacksjs/defaults/views/dashboard/composables/useRole'
11
+ import { ciStore } from '@stacksjs/defaults/views/dashboard/stores/ci'
12
12
 
13
13
  // Import the store definition directly so its typed setup and persistence
14
14
  // are part of this page's client bundle. A name-only useStore('ci') lookup
@@ -1,5 +1,5 @@
1
1
  <script client>
2
- import { ciStore } from '~/storage/framework/defaults/views/dashboard/stores/ci'
2
+ import { ciStore } from '@stacksjs/defaults/views/dashboard/stores/ci'
3
3
 
4
4
  const ci = ciStore
5
5
  const target = derived(() => ci.drilldown())
@@ -1,3 +1,5 @@
1
+ @import('../UI/Table')
2
+
1
3
  <script client>
2
4
  import { dashboardApi } from '../../../../functions/dashboard-api'
3
5
  import { pushToast } from '../../../../functions/toasts'
@@ -23,7 +25,6 @@ const authors = state<AuthorRecord[]>([])
23
25
  const assignedPostCount = state(0)
24
26
  const loading = state(true)
25
27
  const loadError = state('')
26
- const search = state('')
27
28
  const formOpen = state(false)
28
29
  const editingAuthor = state<AuthorRecord | null>(null)
29
30
  const saving = state(false)
@@ -44,10 +45,33 @@ function normalize(record: any): AuthorRecord {
44
45
  }
45
46
  }
46
47
 
47
- function visibleAuthors(): AuthorRecord[] {
48
- const query = search().trim().toLowerCase()
49
- return authors().filter(author => !query || [author.name, author.email, author.bio]
50
- .some(value => value.toLowerCase().includes(query)))
48
+ const COLUMNS = [
49
+ // Slotted: the cell is an avatar, or the author's initials when there is no
50
+ // image, with the name and email beside it.
51
+ { key: 'name', label: 'Author', sortable: true, slotted: true },
52
+ { key: 'postCount', label: 'Posts', type: 'number' as const, sortable: true, class: 'text-gray-500 dark:text-neutral-400' },
53
+ { key: 'createdAt', label: 'Created', type: 'date' as const, sortable: true, emptyText: 'Unknown', class: 'text-gray-500 dark:text-neutral-400' },
54
+ ]
55
+
56
+ const ROW_ACTIONS = [
57
+ { action: 'edit', label: 'Edit author', icon: 'i-hugeicons-edit-02' },
58
+ { action: 'delete', label: 'Delete author', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
59
+ ]
60
+
61
+ /**
62
+ * `UI/Table` owns the search. `initialsLabel` is computed per row rather than
63
+ * called from the slot, because a slot body reads the row through attribute
64
+ * bindings and keeping the derivation here is one less thing to depend on.
65
+ */
66
+ function visibleAuthors(): Array<AuthorRecord & { initialsLabel: string }> {
67
+ return authors().map(author => ({ ...author, initialsLabel: initials(author.name) }))
68
+ }
69
+
70
+ function handleRowAction(payload: { action: string, row: AuthorRecord }): void {
71
+ if (payload.action === 'edit')
72
+ openEdit(payload.row)
73
+ else
74
+ openDelete(payload.row)
51
75
  }
52
76
 
53
77
  function initials(value: string): string {
@@ -55,15 +79,6 @@ function initials(value: string): string {
55
79
  .map(part => part[0]?.toUpperCase() || '').join('') || '?'
56
80
  }
57
81
 
58
- function formatDate(value: string): string {
59
- if (!value)
60
- return 'Unknown'
61
- const date = new Date(value)
62
- return Number.isNaN(date.getTime())
63
- ? value
64
- : new Intl.DateTimeFormat('en-US', { month: 'short', day: 'numeric', year: 'numeric' }).format(date)
65
- }
66
-
67
82
  async function loadAuthors(): Promise<void> {
68
83
  loading.set(true)
69
84
  loadError.set('')
@@ -185,12 +200,36 @@ onMount(() => {
185
200
  </section>
186
201
 
187
202
  <section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
188
- <div class="p-4 border-b border-gray-200 dark:border-neutral-700"><label><span class="sr-only">Search authors</span><input x-model="search" type="search" placeholder="Search authors" class="px-3 py-2 max-w-md w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label></div>
189
- <div class="overflow-x-auto"><table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700"><thead class="bg-gray-50 dark:bg-neutral-900/50"><tr><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Author</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Posts</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Created</th><th class="px-4 py-3"><span class="sr-only">Actions</span></th></tr></thead><tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
190
- <template :if="loading()"><tr><td colspan="4" class="px-4 py-12 text-center text-gray-500 text-sm dark:text-neutral-400"><span class="inline-block mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>Loading authors</td></tr></template>
191
- <template :else-if="visibleAuthors().length === 0"><tr><td colspan="4" class="px-4 py-14 text-center"><span class="inline-block h-6 w-6 text-gray-400 i-hugeicons-user-group"></span><p class="mt-2 font-medium text-gray-900 text-sm dark:text-white">No authors found</p><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Adjust the search or create an author.</p></td></tr></template>
192
- <template :else><template :for="author in visibleAuthors()"><tr><td class="px-4 py-3"><div class="flex gap-3 items-center"><div class="flex overflow-hidden flex-shrink-0 items-center justify-center h-9 w-9 font-semibold text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-950/50 rounded-full"><img :if="author.avatar" :src="author.avatar" :alt="author.name" class="object-cover h-full w-full" /><span :else>{{ initials(author.name) }}</span></div><div class="min-w-0"><p class="font-medium text-gray-900 text-sm truncate dark:text-white">{{ author.name }}</p><p class="text-gray-500 text-xs truncate dark:text-neutral-400">{{ author.email }}</p></div></div></td><td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-400">{{ author.postCount }}</td><td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-400">{{ formatDate(author.createdAt) }}</td><td class="px-4 py-3"><div class="flex justify-end"><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit author ' + author.name" @click="openEdit(author)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-02"></span></Button><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete author ' + author.name" @click="openDelete(author)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button></div></td></tr></template></template>
193
- </tbody></table></div>
203
+ {{-- Slot bodies read the row through `x-text` and `:attr`; an
204
+ interpolation in slot content is preserved but not evaluated on the
205
+ installed stx. See stacksjs/stx#2095. --}}
206
+ <Table
207
+ :columns="COLUMNS"
208
+ :data="visibleAuthors()"
209
+ :loading="loading()"
210
+ :row-actions="ROW_ACTIONS"
211
+ action-label-key="name"
212
+ searchable
213
+ search-placeholder="Search authors"
214
+ paginated
215
+ :page-size="25"
216
+ empty-title="No authors found"
217
+ empty-description="Adjust the search or create an author."
218
+ @row-action="handleRowAction"
219
+ >
220
+ <template #cell="{ row }">
221
+ <div class="flex gap-3 items-center">
222
+ <div class="flex overflow-hidden flex-shrink-0 items-center justify-center h-9 w-9 font-semibold text-blue-700 text-xs dark:text-blue-300 bg-blue-50 dark:bg-blue-950/50 rounded-full">
223
+ <img :if="row.avatar" :src="row.avatar" :alt="row.name" class="object-cover h-full w-full" />
224
+ <span :else x-text="row.initialsLabel"></span>
225
+ </div>
226
+ <div class="min-w-0">
227
+ <p class="font-medium text-gray-900 text-sm truncate dark:text-white" x-text="row.name"></p>
228
+ <p class="text-gray-500 text-xs truncate dark:text-neutral-400" x-text="row.email"></p>
229
+ </div>
230
+ </div>
231
+ </template>
232
+ </Table>
194
233
  </section>
195
234
  </div>
196
235
 
@@ -202,8 +202,8 @@ defineClientPayload({ blogUrl })
202
202
  <script client>
203
203
  // Plain async transport for /api/dashboard/blog, which reads and writes the
204
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'
206
- import { deleteBlogPost, fetchBlogPost, fetchBlogPosts, saveBlogPost } from '~/storage/framework/defaults/functions/blog'
205
+ import type { BlogPost, BlogPostDraft } from '@stacksjs/defaults/functions/blog'
206
+ import { deleteBlogPost, fetchBlogPost, fetchBlogPosts, saveBlogPost } from '@stacksjs/defaults/functions/blog'
207
207
 
208
208
  const publicBlogBaseUrl = {{ blogUrl }}
209
209
 
@@ -1,8 +1,8 @@
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'
5
- import { useChart } from '~/storage/framework/defaults/views/dashboard/composables/useChart'
4
+ import type { ChartCtor, ChartDataset, ChartLike, ChartOptions } from '@stacksjs/defaults/views/dashboard/composables/useChart'
5
+ import { useChart } from '@stacksjs/defaults/views/dashboard/composables/useChart'
6
6
 
7
7
  interface ContentPostSummary {
8
8
  id: number
@@ -1,3 +1,5 @@
1
+ @import('../UI/Table')
2
+
1
3
  <script client>
2
4
  import { dashboardApi } from '../../../../functions/dashboard-api'
3
5
  import { pushToast } from '../../../../functions/toasts'
@@ -23,7 +25,6 @@ const description = useReactiveProp('description', '')
23
25
  const records = state<TaxonomyRecord[]>([])
24
26
  const loading = state(true)
25
27
  const loadError = state('')
26
- const search = state('')
27
28
  const dialogOpen = state(false)
28
29
  const saving = state(false)
29
30
  const saveError = state('')
@@ -46,13 +47,41 @@ function normalize(record: any): TaxonomyRecord {
46
47
  }
47
48
  }
48
49
 
50
+ /**
51
+ * Derived rather than constant: this component serves both categories and
52
+ * tags, and the fourth column is the post count for tags and the active state
53
+ * for categories - a different heading and a different value.
54
+ */
55
+ function columns() {
56
+ return [
57
+ { key: 'name', label: 'Name', sortable: true, class: 'font-medium text-gray-900 dark:text-white' },
58
+ { key: 'slug', label: 'Slug', sortable: true, class: 'font-mono text-gray-500 dark:text-neutral-400' },
59
+ { key: 'description', label: 'Description', emptyText: 'No description', class: 'max-w-md truncate text-gray-500 dark:text-neutral-400' },
60
+ {
61
+ key: 'stateLabel',
62
+ label: resource() === 'tags' ? 'Posts' : 'Status',
63
+ sortable: true,
64
+ class: 'text-gray-500 dark:text-neutral-400',
65
+ },
66
+ ]
67
+ }
68
+
69
+ const ROW_ACTIONS = [
70
+ { action: 'delete', label: 'Delete', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
71
+ ]
72
+
73
+ /** `UI/Table` owns the search; the fourth cell is derived onto the row. */
49
74
  function visibleRecords(): TaxonomyRecord[] {
50
- const query = search().trim().toLowerCase()
51
- return records().filter(record => !query || [
52
- record.name,
53
- record.slug,
54
- record.description,
55
- ].some(value => value.toLowerCase().includes(query)))
75
+ return records().map(record => ({
76
+ ...record,
77
+ stateLabel: resource() === 'tags'
78
+ ? String(record.postCount)
79
+ : record.active ? 'Active' : 'Inactive',
80
+ })) as TaxonomyRecord[]
81
+ }
82
+
83
+ function handleRowAction(payload: { action: string, row: TaxonomyRecord }): void {
84
+ openDelete(payload.row)
56
85
  }
57
86
 
58
87
  function secondaryMetricLabel(): string {
@@ -184,17 +213,20 @@ onMount(() => {
184
213
  </section>
185
214
 
186
215
  <section class="overflow-hidden max-w-full min-w-0 bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
187
- <div class="p-4 border-b border-gray-200 dark:border-neutral-700"><label><span class="sr-only">Search {{ resource() }}</span><input x-model="search" type="search" :placeholder="'Search ' + resource()" class="px-3 py-2 max-w-md w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label></div>
188
- <div class="overflow-x-auto max-w-full w-full">
189
- <table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700">
190
- <thead class="bg-gray-50 dark:bg-neutral-900/50"><tr><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Name</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Slug</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Description</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">{{ resource() === 'tags' ? 'Posts' : 'Status' }}</th><th class="px-4 py-3"><span class="sr-only">Actions</span></th></tr></thead>
191
- <tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
192
- <template :if="loading()"><tr><td colspan="5" class="px-4 py-12 text-center text-gray-500 text-sm dark:text-neutral-400"><span class="inline-block mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>Loading {{ resource() }}</td></tr></template>
193
- <template :else-if="visibleRecords().length === 0"><tr><td colspan="5" class="px-4 py-14 text-center"><span class="inline-block h-6 w-6 text-gray-400 i-hugeicons-tags"></span><p class="mt-2 font-medium text-gray-900 text-sm dark:text-white">No {{ resource() }} found</p><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Adjust the search or create a {{ singular() }}.</p></td></tr></template>
194
- <template :else><template :for="record in visibleRecords()"><tr><td class="px-4 py-3 font-medium text-gray-900 text-sm dark:text-white">{{ record.name }}</td><td class="px-4 py-3 font-mono text-gray-500 text-sm dark:text-neutral-400">{{ record.slug }}</td><td class="px-4 py-3 max-w-md text-gray-500 text-sm truncate dark:text-neutral-400">{{ record.description || 'No description' }}</td><td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-400">{{ resource() === 'tags' ? record.postCount : record.active ? 'Active' : 'Inactive' }}</td><td class="px-4 py-3 text-right"><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete ' + singular() + ' ' + record.name" @click="openDelete(record)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button></td></tr></template></template>
195
- </tbody>
196
- </table>
197
- </div>
216
+ <Table
217
+ :columns="columns()"
218
+ :data="visibleRecords()"
219
+ :loading="loading()"
220
+ :row-actions="ROW_ACTIONS"
221
+ action-label-key="name"
222
+ searchable
223
+ :search-placeholder="'Search ' + resource()"
224
+ paginated
225
+ :page-size="25"
226
+ :empty-title="'No ' + resource() + ' found'"
227
+ :empty-description="'Adjust the search or create a ' + singular() + '.'"
228
+ @row-action="handleRowAction"
229
+ />
198
230
  </section>
199
231
  </div>
200
232
 
@@ -1,3 +1,5 @@
1
+ @import('../UI/Table')
2
+
1
3
  <script client>
2
4
  import { dashboardApi } from '../../../../functions/dashboard-api'
3
5
  import { pushToast } from '../../../../functions/toasts'
@@ -20,7 +22,6 @@ interface PageDraft {
20
22
  const pages = state<PageRecord[]>([])
21
23
  const loading = state(true)
22
24
  const loadError = state('')
23
- const search = state('')
24
25
  const formOpen = state(false)
25
26
  const editingPage = state<PageRecord | null>(null)
26
27
  const saving = state(false)
@@ -40,10 +41,45 @@ function normalize(record: any): PageRecord {
40
41
  }
41
42
  }
42
43
 
44
+ const COLUMNS = [
45
+ { key: 'title', label: 'Page', sortable: true, subtitleKey: 'template', class: 'font-medium text-gray-900 dark:text-white' },
46
+ { key: 'viewsLabel', label: 'Traffic', subtitleKey: 'conversionsLabel', class: 'text-gray-500 dark:text-neutral-400' },
47
+ {
48
+ key: 'publishState',
49
+ label: 'Publishing',
50
+ type: 'badge' as const,
51
+ sortable: true,
52
+ // Coloured by state, labelled with the date it happened - which is why
53
+ // `badgeLabels` cannot express it.
54
+ badgeLabelKey: 'publishedLabel',
55
+ badgeClasses: {
56
+ published: 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/40',
57
+ draft: 'text-gray-600 dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700',
58
+ },
59
+ },
60
+ ]
61
+
62
+ const ROW_ACTIONS = [
63
+ { action: 'edit', label: 'Edit page', icon: 'i-hugeicons-edit-02' },
64
+ { action: 'delete', label: 'Delete page', icon: 'i-hugeicons-delete-02', class: 'text-red-500' },
65
+ ]
66
+
67
+ /** `UI/Table` owns the search; the cells each read a field, so derive them. */
43
68
  function visiblePages(): PageRecord[] {
44
- const query = search().trim().toLowerCase()
45
- return pages().filter(page => !query || [page.title, page.template]
46
- .some(value => value.toLowerCase().includes(query)))
69
+ return pages().map(page => ({
70
+ ...page,
71
+ viewsLabel: `${formatNumber(page.views)} views`,
72
+ conversionsLabel: `${formatNumber(page.conversions)} conversions`,
73
+ publishState: page.publishedAt ? 'published' : 'draft',
74
+ publishedLabel: formatDate(page.publishedAt),
75
+ })) as PageRecord[]
76
+ }
77
+
78
+ function handleRowAction(payload: { action: string, row: PageRecord }): void {
79
+ if (payload.action === 'edit')
80
+ openEdit(payload.row)
81
+ else
82
+ openDelete(payload.row)
47
83
  }
48
84
 
49
85
  function totalViews(): number {
@@ -179,12 +215,20 @@ onMount(() => {
179
215
  </section>
180
216
 
181
217
  <section class="overflow-hidden bg-white dark:bg-neutral-800 border border-gray-200 rounded-lg dark:border-neutral-700">
182
- <div class="p-4 border-b border-gray-200 dark:border-neutral-700"><label><span class="sr-only">Search pages</span><input x-model="search" type="search" placeholder="Search pages" class="px-3 py-2 max-w-md w-full text-gray-900 text-sm dark:text-white bg-white dark:bg-neutral-900 border border-gray-300 rounded-md dark:border-neutral-700" /></label></div>
183
- <div class="overflow-x-auto"><table class="min-w-full divide-gray-200 divide-y dark:divide-neutral-700"><thead class="bg-gray-50 dark:bg-neutral-900/50"><tr><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Page</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Traffic</th><th class="px-4 py-3 font-semibold text-gray-600 text-left text-xs dark:text-neutral-300">Publishing</th><th class="px-4 py-3"><span class="sr-only">Actions</span></th></tr></thead><tbody class="divide-gray-200 divide-y dark:divide-neutral-700">
184
- <template :if="loading()"><tr><td colspan="4" class="px-4 py-12 text-center text-gray-500 text-sm dark:text-neutral-400"><span class="inline-block mr-2 h-4 w-4 animate-spin i-hugeicons-loading-03"></span>Loading pages</td></tr></template>
185
- <template :else-if="visiblePages().length === 0"><tr><td colspan="4" class="px-4 py-14 text-center"><span class="inline-block h-6 w-6 text-gray-400 i-hugeicons-file-02"></span><p class="mt-2 font-medium text-gray-900 text-sm dark:text-white">No pages found</p><p class="mt-1 text-gray-500 text-sm dark:text-neutral-400">Adjust the search or create a page.</p></td></tr></template>
186
- <template :else><template :for="page in visiblePages()"><tr><td class="px-4 py-3"><p class="font-medium text-gray-900 text-sm dark:text-white">{{ page.title }}</p><p class="mt-1 font-mono text-gray-500 text-xs dark:text-neutral-400">{{ page.template }}</p></td><td class="px-4 py-3 text-gray-500 text-sm dark:text-neutral-400"><p>{{ formatNumber(page.views) }} views</p><p class="mt-1 text-xs">{{ formatNumber(page.conversions) }} conversions</p></td><td class="px-4 py-3"><span :class="'inline-flex px-2 py-1 font-medium text-xs rounded-full ' + (page.publishedAt ? 'text-green-700 dark:text-green-300 bg-green-50 dark:bg-green-950/40' : 'text-gray-600 dark:text-neutral-300 bg-gray-100 dark:bg-neutral-700')">{{ formatDate(page.publishedAt) }}</span></td><td class="px-4 py-3"><div class="flex justify-end"><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Edit page ' + page.title" @click="openEdit(page)"><span class="block h-4 w-4 text-blue-500 i-hugeicons-edit-02"></span></Button><Button variant="ghost" size="sm" iconOnly :ariaLabel="'Delete page ' + page.title" @click="openDelete(page)"><span class="block h-4 w-4 text-red-500 i-hugeicons-delete-02"></span></Button></div></td></tr></template></template>
187
- </tbody></table></div>
218
+ <Table
219
+ :columns="COLUMNS"
220
+ :data="visiblePages()"
221
+ :loading="loading()"
222
+ :row-actions="ROW_ACTIONS"
223
+ action-label-key="title"
224
+ searchable
225
+ search-placeholder="Search pages"
226
+ paginated
227
+ :page-size="25"
228
+ empty-title="No pages found"
229
+ empty-description="Adjust the search or create a page."
230
+ @row-action="handleRowAction"
231
+ />
188
232
  </section>
189
233
  </div>
190
234