@stacksjs/defaults 0.75.243 → 0.75.245

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 (47) hide show
  1. package/ai/skills/stacks-dashboard/SKILL.md +2 -2
  2. package/ai/skills/stacks-native/CATALOG.md +9 -9
  3. package/ai/skills/stacks-path/SKILL.md +18 -1
  4. package/app/Actions/Dashboard/DashboardHomeAction.ts +7 -1
  5. package/app/Actions/Dashboard/Models/ModelsIndexAction.ts +2 -1
  6. package/app/Actions/Dashboard/Search/GlobalSearchAction.ts +2 -1
  7. package/app/Actions/Dashboard/Source/source-inventory.ts +2 -1
  8. package/app/Models/QueryLog.ts +10 -0
  9. package/functions/dashboard.ts +10 -1
  10. package/ide/vscode/package.json +1 -1
  11. package/package.json +2 -2
  12. package/project/storage/framework/tsconfig.app.json +8 -0
  13. package/resources/components/Dashboard/Analytics/WebAnalyticsDashboard.stx +5 -5
  14. package/resources/components/Dashboard/Ci/CiDashboard.stx +2 -2
  15. package/resources/components/Dashboard/Ci/CiRunHistoryDrawer.stx +1 -1
  16. package/resources/components/Dashboard/Content/AuthorsDashboard.stx +59 -20
  17. package/resources/components/Dashboard/Content/BlogDashboard.stx +2 -2
  18. package/resources/components/Dashboard/Content/ContentDashboard.stx +2 -2
  19. package/resources/components/Dashboard/Content/ContentTaxonomyDashboard.stx +50 -18
  20. package/resources/components/Dashboard/Content/PagesDashboard.stx +54 -10
  21. package/resources/components/Dashboard/Content/PostsDashboard.stx +91 -20
  22. package/resources/components/Dashboard/Content/SeoDashboard.stx +61 -59
  23. package/resources/components/Dashboard/Email/InboxSettingsDashboard.stx +3 -3
  24. package/resources/components/Dashboard/Environment/EnvironmentDashboard.stx +1 -1
  25. package/resources/components/Dashboard/Home/DashboardHome.stx +20 -2
  26. package/resources/components/Dashboard/Kanban/KanbanBoardDashboard.stx +3 -3
  27. package/resources/components/Dashboard/Kanban/KanbanBoardsDashboard.stx +2 -2
  28. package/resources/components/Dashboard/Kanban/KanbanCardDialog.stx +1 -1
  29. package/resources/components/Dashboard/Library/ComponentsDashboard.stx +47 -81
  30. package/resources/components/Dashboard/Library/FunctionsDashboard.stx +24 -15
  31. package/resources/components/Dashboard/Library/PackagesDashboard.stx +68 -13
  32. package/resources/components/Dashboard/Management/PermissionsDashboard.stx +12 -15
  33. package/resources/components/Dashboard/Models/ModelRecordsDashboard.stx +3 -3
  34. package/resources/components/Dashboard/Settings/AppearanceSettingsDashboard.stx +3 -3
  35. package/resources/components/Dashboard/Settings/SettingsDashboard.stx +1 -1
  36. package/resources/components/Dashboard/Teams/TeamInvitationDashboard.stx +2 -2
  37. package/resources/components/Dashboard/UI/Button.stx +29 -3
  38. package/resources/components/Dashboard/UI/SearchFilter.stx +46 -0
  39. package/resources/components/Dashboard/UI/Table.stx +43 -20
  40. package/resources/functions/dashboard/data.ts +27 -4
  41. package/resources/functions/dashboard/sidebar.ts +3 -2
  42. package/resources/functions/dashboard/toggles.ts +47 -11
  43. package/resources/plugins/preloader.ts +9 -1
  44. package/views/dashboard/layouts/default.stx +17 -13
  45. package/views/system-tray/index.stx +1 -1
  46. package/resources/components/Dashboard/Commerce/Delivery/SearchFilter.stx +0 -24
  47. package/resources/components/Dashboard/UI/FilterBar.stx +0 -62
@@ -77,6 +77,17 @@ interface DashboardTableColumn {
77
77
  * "On delivery".
78
78
  */
79
79
  badgeLabels?: Record<string, string>
80
+ /**
81
+ * Row field holding the pill's text, when it varies per row rather than per
82
+ * value.
83
+ *
84
+ * `badgeLabels` is a static map, which covers a status whose wording is
85
+ * fixed. It cannot cover a pill coloured by state but labelled with
86
+ * something row-specific - a publish pill that is green when published and
87
+ * grey when not, and reads as the date it happened. Wins over
88
+ * `badgeLabels` when both are given.
89
+ */
90
+ badgeLabelKey?: string
80
91
  /**
81
92
  * Row field rendered as a second, quieter line under the value.
82
93
  *
@@ -355,6 +366,12 @@ function badgeLabel(row: DashboardTableRow, column: DashboardTableColumn): strin
355
366
  if (value === null || value === undefined || value === '')
356
367
  return column.emptyText ?? '-'
357
368
 
369
+ if (column.badgeLabelKey) {
370
+ const label = rawValue(row, column.badgeLabelKey)
371
+ if (label !== null && label !== undefined && label !== '')
372
+ return String(label)
373
+ }
374
+
358
375
  const key = String(value)
359
376
  const mapped = column.badgeLabels?.[key]
360
377
  if (mapped)
@@ -696,16 +713,16 @@ function cellClass(column: DashboardTableColumn, index: number): string {
696
713
 
697
714
  <div class="flex gap-2 items-center">
698
715
  <div :if="columnToggle()" class="relative">
699
- <button
700
- type="button"
701
- aria-haspopup="true"
702
- :aria-expanded="columnMenuOpen() ? 'true' : 'false'"
716
+ <Button
717
+ variant="outline"
718
+ size="sm"
719
+ ariaHasPopup="true"
720
+ :ariaExpanded="columnMenuOpen() ? 'true' : 'false'"
703
721
  @click="columnMenuOpen.set(!columnMenuOpen())"
704
- class="inline-flex gap-1.5 items-center px-2.5 py-1.5 text-neutral-600 text-xs dark:text-neutral-300 hover:bg-stone-50 dark:hover:bg-neutral-800 border border-stone-300 rounded-lg dark:border-neutral-600"
705
722
  >
706
723
  <span aria-hidden="true" class="h-3.5 w-3.5 i-hugeicons-layout-table-01"></span>
707
724
  Columns
708
- </button>
725
+ </Button>
709
726
 
710
727
  <div
711
728
  :if="columnMenuOpen()"
@@ -774,10 +791,14 @@ function cellClass(column: DashboardTableColumn, index: number): string {
774
791
  :style="column.width ? 'width: ' + column.width : undefined"
775
792
  :aria-sort="ariaSort(column)"
776
793
  >
777
- <button
794
+ {{-- `bare`: a header button fills its `<th>` and carries no box
795
+ of its own, so none of the sizes fit. It still comes through
796
+ `UI/Button` for the shared focus ring and disabled handling.
797
+ See stacksjs/stacks#2926. --}}
798
+ <Button
778
799
  :if="column.sortable"
779
- type="button"
780
- :aria-label="sortLabel(column)"
800
+ variant="bare"
801
+ :ariaLabel="sortLabel(column)"
781
802
  class="inline-flex gap-1.5 items-center hover:text-stone-700 dark:hover:text-neutral-200"
782
803
  @click="sort(column)"
783
804
  >
@@ -786,7 +807,7 @@ function cellClass(column: DashboardTableColumn, index: number): string {
786
807
  aria-hidden="true"
787
808
  :class="'h-3.5 w-3.5 ' + (sortKey() === column.key ? (sortDirection() === 'asc' ? 'i-hugeicons-arrow-up-02' : 'i-hugeicons-arrow-down-02') : 'opacity-30 i-hugeicons-arrow-up-down')"
788
809
  ></span>
789
- </button>
810
+ </Button>
790
811
  <span :else>{{ column.label }}</span>
791
812
  </th>
792
813
  </template>
@@ -936,25 +957,27 @@ function cellClass(column: DashboardTableColumn, index: number): string {
936
957
  </div>
937
958
 
938
959
  <nav class="flex gap-1 items-center" aria-label="Pagination">
939
- <button
940
- type="button"
941
- aria-label="Previous page"
960
+ <Button
961
+ variant="outline"
962
+ size="sm"
963
+ iconOnly
964
+ ariaLabel="Previous page"
942
965
  :disabled="page() <= 1"
943
966
  @click="goToPage(page() - 1)"
944
- class="p-1.5 text-neutral-600 dark:text-neutral-300 hover:bg-white dark:hover:bg-neutral-800 border border-stone-300 rounded dark:border-neutral-600 disabled:opacity-40 disabled:cursor-not-allowed"
945
967
  >
946
968
  <span aria-hidden="true" class="h-4 w-4 i-hugeicons-arrow-left-01"></span>
947
- </button>
969
+ </Button>
948
970
  <span class="px-2 tabular-nums text-neutral-600 text-sm dark:text-neutral-300">{{ page() }} / {{ pageCount() }}</span>
949
- <button
950
- type="button"
951
- aria-label="Next page"
971
+ <Button
972
+ variant="outline"
973
+ size="sm"
974
+ iconOnly
975
+ ariaLabel="Next page"
952
976
  :disabled="page() >= pageCount()"
953
977
  @click="goToPage(page() + 1)"
954
- class="p-1.5 text-neutral-600 dark:text-neutral-300 hover:bg-white dark:hover:bg-neutral-800 border border-stone-300 rounded dark:border-neutral-600 disabled:opacity-40 disabled:cursor-not-allowed"
955
978
  >
956
979
  <span aria-hidden="true" class="h-4 w-4 i-hugeicons-arrow-right-01"></span>
957
- </button>
980
+ </Button>
958
981
  </nav>
959
982
  </div>
960
983
 
@@ -21,15 +21,36 @@ import { basename, dirname, resolve } from 'node:path'
21
21
 
22
22
  let projectRootCache: string | null = null
23
23
 
24
+ const VENDORED_DEFAULTS_PREFIX = 'storage/framework/defaults/'
25
+ let defaultsRootCache: string | null = null
26
+
27
+ /**
28
+ * The framework defaults tree: `storage/framework/defaults` in a project that
29
+ * vendors it, `node_modules/@stacksjs/defaults` in one that does not. Counted
30
+ * from this file first, which is inside that tree in both layouts; the project
31
+ * root is the fallback for a copy that was bundled somewhere else.
32
+ */
33
+ function defaultsRoot(): string {
34
+ if (defaultsRootCache) return defaultsRootCache
35
+ const own = resolve(dirname(new URL(import.meta.url).pathname), '../../..')
36
+ const candidates = [
37
+ own,
38
+ resolve(projectRoot(), 'storage/framework/defaults'),
39
+ resolve(projectRoot(), 'node_modules/@stacksjs/defaults'),
40
+ ]
41
+ defaultsRootCache = candidates.find(candidate => existsSync(resolve(candidate, 'app/Models'))) ?? candidates[1]!
42
+ return defaultsRootCache
43
+ }
44
+
24
45
  function projectRoot(): string {
25
46
  if (projectRootCache) return projectRootCache
26
47
  // Walk up from this file location to find the project root (the dir
27
- // that contains `storage/framework/defaults`). This mirrors what
48
+ // that contains `storage/framework`). This mirrors what
28
49
  // `@stacksjs/path` does, but without pulling in the path package
29
50
  // (which has its own evaluation graph and would slow page renders).
30
51
  let dir = dirname(new URL(import.meta.url).pathname)
31
52
  for (let i = 0; i < 12; i++) {
32
- if (existsSync(resolve(dir, 'storage/framework/defaults'))) {
53
+ if (existsSync(resolve(dir, 'storage/framework'))) {
33
54
  projectRootCache = dir
34
55
  return dir
35
56
  }
@@ -132,7 +153,7 @@ async function discoverModelPaths(): Promise<Map<string, string[]>> {
132
153
  const paths = new Map<string, string[]>()
133
154
  const modelRoots = [
134
155
  resolve(root, 'app/Models'),
135
- resolve(root, 'storage/framework/defaults/app/Models'),
156
+ resolve(defaultsRoot(), 'app/Models'),
136
157
  ]
137
158
 
138
159
  for (const modelsRoot of modelRoots) {
@@ -159,7 +180,9 @@ async function discoverModelPaths(): Promise<Map<string, string[]>> {
159
180
  async function modelCandidatePaths(name: string): Promise<string[]> {
160
181
  const root = projectRoot()
161
182
  const discovered = (await discoverModelPaths()).get(name) ?? []
162
- const explicit = (MODEL_PATHS[name] ?? []).map(path => resolve(root, path))
183
+ const explicit = (MODEL_PATHS[name] ?? []).map(path => path.startsWith(VENDORED_DEFAULTS_PREFIX)
184
+ ? resolve(defaultsRoot(), path.slice(VENDORED_DEFAULTS_PREFIX.length))
185
+ : resolve(root, path))
163
186
  return [...new Set([...discovered, ...explicit])]
164
187
  }
165
188
 
@@ -111,12 +111,13 @@ interface NavItem {
111
111
  }
112
112
 
113
113
  /**
114
- * Pull the discovered-models manifest from the framework defaults dir.
114
+ * Pull the discovered-models manifest `buddy dev:dashboard` writes into the
115
+ * project's runtime state.
115
116
  * Returns the default empty payload when the file is absent (fresh project
116
117
  * before the first dashboard run regenerates it).
117
118
  */
118
119
  export function loadDiscoveredManifest(
119
- manifestPath = resolve(process.cwd(), 'storage/framework/defaults/views/dashboard/.discovered-models.json'),
120
+ manifestPath = resolve(process.cwd(), 'storage/framework/runtime/dashboard/discovered-models.json'),
120
121
  ): DiscoveredManifest {
121
122
  if (!existsSync(manifestPath))
122
123
  return { models: [], sections: defaultToggles(), nav: [] }
@@ -45,12 +45,46 @@ export interface ResolvedDashboardToggles {
45
45
  data: DashboardDataRowToggles
46
46
  }
47
47
 
48
- export function defaultDashboardToggles(): ResolvedDashboardToggles {
48
+ /**
49
+ * Dashboard sections that mirror an installable feature.
50
+ *
51
+ * A section whose feature is off used to stay in the sidebar anyway, and every
52
+ * page under it failed: the models those pages query are never loaded, so the
53
+ * first call came back `Manufacturer.orderByDesc is not a function` and the
54
+ * endpoint 503'd. Switching commerce off took two edits in two files -
55
+ * `config/commerce.ts` and `config/dashboard.ts` - and missing the second left
56
+ * a Commerce section that could not load. See stacksjs/stacks#2928.
57
+ *
58
+ * `library`, `analytics`, `management` and `utilities` are deliberately absent:
59
+ * they have no feature of their own, so they stay on.
60
+ */
61
+ const FEATURE_BACKED_SECTIONS = {
62
+ commerce: 'commerce',
63
+ content: 'cms',
64
+ marketing: 'marketing',
65
+ } as const
66
+
67
+ /**
68
+ * Reads whether a feature is on. Supplied by the caller rather than imported,
69
+ * because this module is loaded on the dashboard's synchronous path and stays
70
+ * free of `@stacksjs/*` imports - its sibling `sidebar.ts` says why.
71
+ *
72
+ * Omitted, every section defaults on exactly as it did before.
73
+ */
74
+ export type FeatureResolver = (name: string) => boolean
75
+
76
+ function sectionFallback(section: keyof typeof FEATURE_BACKED_SECTIONS, isFeatureEnabled?: FeatureResolver): boolean {
77
+ if (!isFeatureEnabled)
78
+ return true
79
+ return isFeatureEnabled(FEATURE_BACKED_SECTIONS[section])
80
+ }
81
+
82
+ export function defaultDashboardToggles(isFeatureEnabled?: FeatureResolver): ResolvedDashboardToggles {
49
83
  return {
50
84
  library: true,
51
- content: true,
52
- commerce: true,
53
- marketing: true,
85
+ content: sectionFallback('content', isFeatureEnabled),
86
+ commerce: sectionFallback('commerce', isFeatureEnabled),
87
+ marketing: sectionFallback('marketing', isFeatureEnabled),
54
88
  analytics: true,
55
89
  management: true,
56
90
  utilities: true,
@@ -66,7 +100,7 @@ export function defaultDashboardToggles(): ResolvedDashboardToggles {
66
100
  }
67
101
  }
68
102
 
69
- export function resolveDashboardToggles(value: unknown): ResolvedDashboardToggles {
103
+ export function resolveDashboardToggles(value: unknown, isFeatureEnabled?: FeatureResolver): ResolvedDashboardToggles {
70
104
  const config = objectValue(value, 'dashboard config')
71
105
  const sections = config.sections === undefined
72
106
  ? {}
@@ -77,9 +111,11 @@ export function resolveDashboardToggles(value: unknown): ResolvedDashboardToggle
77
111
 
78
112
  return {
79
113
  library: sectionEnabled(sections.library, 'library', true),
80
- content: sectionEnabled(sections.content, 'content', true),
81
- commerce: sectionEnabled(sections.commerce, 'commerce', true),
82
- marketing: sectionEnabled(sections.marketing, 'marketing', true),
114
+ // An explicit `enabled` in config/dashboard.ts still wins, both ways: an
115
+ // app can show a section whose feature is off, or hide one that is on.
116
+ content: sectionEnabled(sections.content, 'content', sectionFallback('content', isFeatureEnabled)),
117
+ commerce: sectionEnabled(sections.commerce, 'commerce', sectionFallback('commerce', isFeatureEnabled)),
118
+ marketing: sectionEnabled(sections.marketing, 'marketing', sectionFallback('marketing', isFeatureEnabled)),
83
119
  analytics: sectionEnabled(sections.analytics, 'analytics', true),
84
120
  management: sectionEnabled(sections.management, 'management', true),
85
121
  utilities: sectionEnabled(sections.utilities, 'utilities', true),
@@ -157,16 +193,16 @@ export interface LoadedDashboardConfig {
157
193
  * One import rather than two: the config module is cache-busted on mtime, so a
158
194
  * second load of the same file would be a second evaluation of user code.
159
195
  */
160
- export async function loadDashboardConfig(configPath: string): Promise<LoadedDashboardConfig> {
196
+ export async function loadDashboardConfig(configPath: string, isFeatureEnabled?: FeatureResolver): Promise<LoadedDashboardConfig> {
161
197
  if (!existsSync(configPath))
162
- return { toggles: defaultDashboardToggles(), nav: [] }
198
+ return { toggles: defaultDashboardToggles(isFeatureEnabled), nav: [] }
163
199
 
164
200
  try {
165
201
  const moduleUrl = pathToFileURL(configPath)
166
202
  moduleUrl.searchParams.set('dashboard-config-mtime', String(statSync(configPath).mtimeMs))
167
203
  const configModule = await import(moduleUrl.href) as { default?: unknown }
168
204
  return {
169
- toggles: resolveDashboardToggles(configModule.default),
205
+ toggles: resolveDashboardToggles(configModule.default, isFeatureEnabled),
170
206
  nav: resolveDashboardNav(configModule.default),
171
207
  }
172
208
  }
@@ -166,13 +166,21 @@ if (!isRepl && !isPostinstall) {
166
166
  // `upgrade` that would fix it. Falling through leaves stx and ts-cloud on
167
167
  // their own defaults, which is degraded but recoverable.
168
168
  const pathPkg = '@stacksjs/' + 'path'
169
- const { applyRuntimeDirectoryEnv } = await import('../../../core/path/src/index.ts')
169
+ const { applyRuntimeDirectoryEnv, linkPackagedDefaults } = await import('../../../core/path/src/index.ts')
170
170
  .catch(() => import(pathPkg))
171
171
 
172
172
  if (typeof applyRuntimeDirectoryEnv === 'function')
173
173
  applyRuntimeDirectoryEnv()
174
174
  else
175
175
  console.warn('[stacks] installed @stacksjs/path has no applyRuntimeDirectoryEnv; stx and ts-cloud will use their default state directories. Run `buddy upgrade` to refresh the framework packages.')
176
+
177
+ // An app that does not vendor the framework defaults gets a gitignored
178
+ // `storage/framework/defaults` link to the installed package. A fresh clone
179
+ // has no link until something makes one, so boot does: one lstat when it is
180
+ // already there, and a no-op for a vendored tree. Guarded for the same
181
+ // published-version reason as the helper above.
182
+ if (typeof linkPackagedDefaults === 'function')
183
+ linkPackagedDefaults()
176
184
  }
177
185
 
178
186
  // stx template engine plugin
@@ -9,10 +9,14 @@
9
9
  // now publishes its .stx sources, so the real component resolves and the
10
10
  // duplicate markup is gone.
11
11
  //
12
- // Project-root-anchored (`~/`), not relative: the stx client bundler resolves
13
- // a relative import against the PAGE that extends this layout, not against
14
- // this file, so `../../..` pointed somewhere different for every page depth.
15
- import { buildWebSidebarSections } from '~/storage/framework/defaults/resources/functions/dashboard/sidebar'
12
+ // A package specifier, not relative: the stx client bundler resolves a
13
+ // relative import against the PAGE that extends this layout, not against this
14
+ // file, so `../../..` pointed somewhere different for every page depth. Nor
15
+ // a path from the project root (`~/`), where the tree exists only while it is
16
+ // vendored or linked. `@stacksjs/defaults/*` is mapped onto the vendored tree in
17
+ // `storage/framework/tsconfig.app.json` and falls back to the installed
18
+ // package when there is none, so it resolves in both layouts.
19
+ import { buildWebSidebarSections } from '@stacksjs/defaults/resources/functions/dashboard/sidebar.ts'
16
20
 
17
21
  export const sidebarSections = buildWebSidebarSections()
18
22
 
@@ -398,15 +402,15 @@ html[data-appearance="arc"] [data-sidebar-section] > div:first-child {
398
402
  // network requests fail or succeed; that signal is rendered in the
399
403
  // fixed-position container below the <main> slot. Mounted in the
400
404
  // layout so every dashboard page picks it up without opting in.
401
- // `~/`-anchored for the same reason as the sidebar import above.
402
- import type { ToastType } from '~/storage/framework/defaults/functions/toasts'
403
- import type { CraftSidebarSelectEvent } from '~/storage/framework/defaults/functions/craft'
404
- import { useToasts, dismissToast } from '~/storage/framework/defaults/functions/toasts'
405
- import { ensureCraftNativeSidebarMarker, useCraftSidebarSelection } from '~/storage/framework/defaults/functions/craft'
406
- import { readStoredAppearance, useAppearance } from '~/storage/framework/defaults/views/dashboard/composables/useAppearance'
407
- import { authStore } from '~/storage/framework/defaults/views/dashboard/stores/auth'
408
- import { kanbanStore } from '~/storage/framework/defaults/views/dashboard/stores/kanban'
409
- import { rbacStore } from '~/storage/framework/defaults/views/dashboard/stores/rbac'
405
+ // A package specifier for the same reason as the sidebar import above.
406
+ import type { ToastType } from '@stacksjs/defaults/functions/toasts.ts'
407
+ import type { CraftSidebarSelectEvent } from '@stacksjs/defaults/functions/craft.ts'
408
+ import { useToasts, dismissToast } from '@stacksjs/defaults/functions/toasts.ts'
409
+ import { ensureCraftNativeSidebarMarker, useCraftSidebarSelection } from '@stacksjs/defaults/functions/craft.ts'
410
+ import { readStoredAppearance, useAppearance } from '@stacksjs/defaults/views/dashboard/composables/useAppearance.ts'
411
+ import { authStore } from '@stacksjs/defaults/views/dashboard/stores/auth.ts'
412
+ import { kanbanStore } from '@stacksjs/defaults/views/dashboard/stores/kanban.ts'
413
+ import { rbacStore } from '@stacksjs/defaults/views/dashboard/stores/rbac.ts'
410
414
  const { toasts } = useToasts()
411
415
  registerStoresClient({ authStore, kanbanStore, rbacStore })
412
416
 
@@ -1,5 +1,5 @@
1
1
  <script client>
2
- import { installTrayMenu } from '~/storage/framework/defaults/resources/functions/system-tray'
2
+ import { installTrayMenu } from '@stacksjs/defaults/resources/functions/system-tray.ts'
3
3
 
4
4
  const projects = state<string[]>([])
5
5
  const selected = state('')
@@ -1,24 +0,0 @@
1
- <script client>
2
- const placeholder = useReactiveProp('placeholder', 'Search...')
3
- const query = state('')
4
- const emit = defineEmits()
5
-
6
- effect(() => emit('search', query()))
7
- </script>
8
-
9
- <div>
10
- <div class="relative rounded-md shadow-sm">
11
- <div class="flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none">
12
- <span class="h-5 w-5 text-gray-400 i-hugeicons-search-01"></span>
13
- </div>
14
- <input
15
- type="search"
16
- name="search"
17
- :placeholder="placeholder()"
18
- :aria-label="placeholder()"
19
- x-model="query"
20
- autocomplete="off"
21
- class="block pl-10 pr-3 py-2.5 w-full leading-6 text-gray-900 text-sm dark:text-white placeholder:text-gray-400 dark:placeholder:text-neutral-500 dark:bg-neutral-800 border-0 ring-1 ring-gray-300 ring-inset rounded-md dark:ring-neutral-700 focus:ring-2 focus:ring-blue-600 focus:ring-inset"
22
- >
23
- </div>
24
- </div>
@@ -1,62 +0,0 @@
1
- <script client>
2
- const searchPlaceholder = useReactiveProp('searchPlaceholder', 'Search...')
3
- const requestedSearch = useReactiveProp('searchValue', '')
4
- const requestedItemsPerPage = useReactiveProp('itemsPerPage', 10)
5
- const itemsPerPageOptions = useReactiveProp<number[]>('itemsPerPageOptions', [10, 25, 50, 100])
6
- const showItemsPerPage = useReactiveProp('showItemsPerPage', true)
7
- const searchValue = state(requestedSearch())
8
- const itemsPerPage = state(requestedItemsPerPage())
9
- const emit = defineEmits()
10
-
11
- effect(() => {
12
- searchValue.set(requestedSearch())
13
- })
14
-
15
- effect(() => {
16
- itemsPerPage.set(requestedItemsPerPage())
17
- })
18
-
19
- function emitSearch(event: Event): void {
20
- const value = (event.target as HTMLInputElement).value
21
- emit('update:searchValue', value)
22
- emit('search', value)
23
- }
24
-
25
- function emitItemsPerPage(event: Event): void {
26
- const value = Number((event.target as HTMLSelectElement).value)
27
- emit('update:itemsPerPage', value)
28
- emit('per-page', value)
29
- }
30
- </script>
31
-
32
- <div class="flex flex-col gap-3 sm:flex-row sm:items-center">
33
- <div class="relative flex-1 max-w-sm">
34
- <span aria-hidden="true" class="absolute inset-y-0 left-3 my-auto h-4 w-4 text-neutral-400 dark:text-neutral-500 i-hugeicons-search-01"></span>
35
- <input
36
- x-model="searchValue"
37
- type="search"
38
- :placeholder="searchPlaceholder()"
39
- :aria-label="searchPlaceholder()"
40
- class="block pl-9 pr-3 py-1.5 w-full text-[13px] text-neutral-900 dark:text-neutral-100 placeholder:text-neutral-400 dark:placeholder:text-neutral-500 bg-white dark:bg-white/10 border border-black/10 rounded-lg dark:border-white/15 focus:border-blue-400 focus:outline-none focus:ring-2 focus:ring-blue-500/20 hover:border-black/20 dark:hover:border-white/25 duration-150 transition-all"
41
- @input="emitSearch($event)"
42
- />
43
- </div>
44
-
45
- <div class="flex flex-wrap gap-2 items-center">
46
- <slot />
47
- </div>
48
-
49
- <label :if="showItemsPerPage()" class="flex gap-2 items-center ml-auto text-[12px] text-neutral-500 dark:text-neutral-400">
50
- <span>Show</span>
51
- <select
52
- x-model.number="itemsPerPage"
53
- aria-label="Items per page"
54
- class="px-2 h-7 text-[12px] text-neutral-700 dark:text-neutral-200 bg-white dark:bg-white/10 border border-black/10 rounded-md dark:border-white/15 focus:border-blue-500/50 focus:ring-blue-500/40"
55
- @change="emitItemsPerPage($event)"
56
- >
57
- <template :for="option in itemsPerPageOptions()">
58
- <option :value="option">{{ option }}</option>
59
- </template>
60
- </select>
61
- </label>
62
- </div>