@questpie/admin 3.5.2 → 3.5.4

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 (195) hide show
  1. package/README.md +8 -0
  2. package/dist/client/blocks/block-renderer.d.mts +2 -2
  3. package/dist/client/builder/index.d.mts +1 -1
  4. package/dist/client/builder/types/collection-types.d.mts +89 -5
  5. package/dist/client/builder/types/common.d.mts +5 -0
  6. package/dist/client/builder/types/field-types.d.mts +41 -1
  7. package/dist/client/builder/view/view.d.mts +3 -2
  8. package/dist/client/components/actions/action-dialog.mjs +5 -0
  9. package/dist/client/components/admin-link.d.mts +2 -2
  10. package/dist/client/components/fields/boolean-field.mjs +2 -1
  11. package/dist/client/components/fields/date-field.mjs +2 -1
  12. package/dist/client/components/fields/datetime-field.mjs +2 -1
  13. package/dist/client/components/fields/email-field.mjs +2 -1
  14. package/dist/client/components/fields/field-utils.d.mts +11 -0
  15. package/dist/client/components/fields/field-utils.mjs +3 -1
  16. package/dist/client/components/fields/field-wrapper.mjs +3 -3
  17. package/dist/client/components/fields/number-field.mjs +2 -1
  18. package/dist/client/components/fields/object-field.mjs +2 -1
  19. package/dist/client/components/fields/relation/displays/types.mjs +3 -3
  20. package/dist/client/components/fields/rich-text-editor/bubble-menu.mjs +7 -0
  21. package/dist/client/components/fields/rich-text-editor/extensions.mjs +19 -2
  22. package/dist/client/components/fields/rich-text-editor/image-popover.mjs +6 -2
  23. package/dist/client/components/fields/rich-text-editor/image-upload.mjs +2 -1
  24. package/dist/client/components/fields/rich-text-editor/index.d.mts +5 -3
  25. package/dist/client/components/fields/rich-text-editor/index.mjs +38 -76
  26. package/dist/client/components/fields/rich-text-editor/slash-commands.mjs +30 -7
  27. package/dist/client/components/fields/rich-text-editor/toolbar.mjs +1 -312
  28. package/dist/client/components/fields/rich-text-editor/types.d.mts +4 -0
  29. package/dist/client/components/fields/rich-text-editor/types.mjs +1 -1
  30. package/dist/client/components/fields/rich-text-editor/utils.mjs +6 -12
  31. package/dist/client/components/fields/select-field.mjs +2 -1
  32. package/dist/client/components/fields/text-field.mjs +2 -1
  33. package/dist/client/components/fields/textarea-field.mjs +2 -1
  34. package/dist/client/components/fields/time-field.mjs +2 -1
  35. package/dist/client/components/filter-builder/filter-builder-sheet.mjs +75 -22
  36. package/dist/client/components/layout/field-layout-renderer.mjs +4 -4
  37. package/dist/client/components/media/media-grid.mjs +2 -1
  38. package/dist/client/components/primitives/asset-preview.mjs +4 -2
  39. package/dist/client/components/primitives/dropzone.d.mts +100 -0
  40. package/dist/client/components/primitives/field-select-control.mjs +2 -1
  41. package/dist/client/components/ui/button.d.mts +23 -0
  42. package/dist/client/components/ui/button.mjs +2 -2
  43. package/dist/client/components/ui/dropdown-menu.d.mts +49 -0
  44. package/dist/client/components/ui/dropdown-menu.mjs +7 -19
  45. package/dist/client/components/ui/popover.mjs +1 -1
  46. package/dist/client/components/ui/search-input.d.mts +56 -0
  47. package/dist/client/components/ui/select.mjs +2 -2
  48. package/dist/client/components/ui/sheet.d.mts +40 -0
  49. package/dist/client/components/ui/table.d.mts +49 -0
  50. package/dist/client/components/ui/table.mjs +15 -1
  51. package/dist/client/components/ui/tooltip.d.mts +21 -0
  52. package/dist/client/contexts/focus-context.d.mts +2 -2
  53. package/dist/client/hooks/query-access.d.mts +9 -0
  54. package/dist/client/hooks/query-access.mjs +20 -0
  55. package/dist/client/hooks/typed-hooks.d.mts +4 -2
  56. package/dist/client/hooks/typed-hooks.mjs +30 -29
  57. package/dist/client/hooks/use-admin-config.mjs +20 -1
  58. package/dist/client/hooks/use-autosave.mjs +91 -0
  59. package/dist/client/hooks/use-collection.mjs +65 -23
  60. package/dist/client/hooks/use-reactive-fields.d.mts +1 -0
  61. package/dist/client/hooks/use-reactive-fields.mjs +16 -1
  62. package/dist/client/hooks/use-server-actions.mjs +12 -1
  63. package/dist/client/hooks/use-upload.d.mts +40 -0
  64. package/dist/client/hooks/use-upload.mjs +4 -2
  65. package/dist/client/hooks/use-view-state.mjs +15 -7
  66. package/dist/client/i18n/hooks.d.mts +20 -0
  67. package/dist/client/lib/utils.d.mts +6 -0
  68. package/dist/client/lib/view-filter-utils.mjs +30 -0
  69. package/dist/client/preview/block-scope-context.d.mts +2 -2
  70. package/dist/client/preview/preview-banner.d.mts +2 -2
  71. package/dist/client/preview/preview-field.d.mts +4 -4
  72. package/dist/client/runtime/provider.mjs +22 -3
  73. package/dist/client/scope/picker.d.mts +2 -2
  74. package/dist/client/scope/provider.d.mts +2 -2
  75. package/dist/client/styles/base.css +75 -79
  76. package/dist/client/utils/asset-url.mjs +27 -0
  77. package/dist/client/utils/build-field-definitions-from-schema.mjs +1 -0
  78. package/dist/client/views/auth/accept-invite-form.d.mts +2 -2
  79. package/dist/client/views/auth/auth-layout.d.mts +3 -3
  80. package/dist/client/views/auth/forgot-password-form.d.mts +2 -2
  81. package/dist/client/views/auth/login-form.d.mts +2 -2
  82. package/dist/client/views/auth/reset-password-form.d.mts +2 -2
  83. package/dist/client/views/auth/setup-form.d.mts +2 -2
  84. package/dist/client/views/collection/auto-form-fields.mjs +7 -6
  85. package/dist/client/views/collection/cells/primitive-cells.mjs +9 -6
  86. package/dist/client/views/collection/cells/shared/asset-thumbnail.d.mts +7 -0
  87. package/dist/client/views/collection/cells/shared/asset-thumbnail.mjs +3 -2
  88. package/dist/client/views/collection/cells/shared/cell-helpers.mjs +3 -2
  89. package/dist/client/views/collection/cells/upload-cells.mjs +2 -1
  90. package/dist/client/views/collection/columns/build-columns.mjs +3 -1
  91. package/dist/client/views/collection/document-view.d.mts +30 -0
  92. package/dist/client/views/collection/document-view.mjs +377 -0
  93. package/dist/client/views/collection/field-context.mjs +3 -2
  94. package/dist/client/views/collection/field-renderer.mjs +13 -5
  95. package/dist/client/views/collection/form-view.mjs +221 -282
  96. package/dist/client/views/collection/list-view.mjs +592 -190
  97. package/dist/client/views/collection/outline.mjs +44 -19
  98. package/dist/client/views/collection/quick-filter-bar.mjs +45 -0
  99. package/dist/client/views/collection/table-view.mjs +61 -17
  100. package/dist/client/views/globals/global-form-view.mjs +12 -9
  101. package/dist/client/views/layout/admin-layout-provider.mjs +4 -3
  102. package/dist/client/views/layout/admin-layout.mjs +108 -21
  103. package/dist/client/views/layout/admin-router.mjs +19 -3
  104. package/dist/client/views/layout/admin-sidebar.mjs +70 -20
  105. package/dist/client/views/layout/admin-theme.mjs +5 -4
  106. package/dist/client/views/layout/admin-view-layout.d.mts +36 -0
  107. package/dist/client/views/pages/accept-invite-page.d.mts +2 -2
  108. package/dist/client/views/pages/dashboard-page.d.mts +2 -2
  109. package/dist/client/views/pages/forgot-password-page.d.mts +2 -2
  110. package/dist/client/views/pages/invite-page.d.mts +2 -2
  111. package/dist/client/views/pages/login-page.d.mts +2 -2
  112. package/dist/client/views/pages/reset-password-page.d.mts +2 -2
  113. package/dist/client/views/pages/setup-page.d.mts +2 -2
  114. package/dist/client.d.mts +17 -2
  115. package/dist/client.mjs +17 -2
  116. package/dist/components/rich-text/rich-text-renderer.d.mts +5 -5
  117. package/dist/components/rich-text/rich-text-renderer.mjs +5 -2
  118. package/dist/factories.d.mts +4 -2
  119. package/dist/factories.mjs +2 -2
  120. package/dist/index.d.mts +17 -3
  121. package/dist/index.mjs +17 -2
  122. package/dist/modules/admin.d.mts +1 -1
  123. package/dist/server/adapters/index.d.mts +2 -0
  124. package/dist/server/adapters/nextjs.d.mts +1 -0
  125. package/dist/server/augmentation/actions.d.mts +9 -3
  126. package/dist/server/augmentation/dashboard.d.mts +11 -11
  127. package/dist/server/augmentation/form-layout.d.mts +16 -6
  128. package/dist/server/augmentation/index.d.mts +7 -0
  129. package/dist/server/augmentation/sidebar.d.mts +8 -8
  130. package/dist/server/augmentation/views.d.mts +4 -1
  131. package/dist/server/auth-helpers.d.mts +1 -0
  132. package/dist/server/codegen/admin-client-template.mjs +7 -6
  133. package/dist/server/fields/blocks.mjs +4 -1
  134. package/dist/server/fields/index.d.mts +1 -1
  135. package/dist/server/fields/reactive-runtime.mjs +3 -0
  136. package/dist/server/fields/rich-text.d.mts +16 -17
  137. package/dist/server/fields/rich-text.mjs +18 -7
  138. package/dist/server/i18n/messages/cs.mjs +2 -0
  139. package/dist/server/i18n/messages/de.mjs +2 -0
  140. package/dist/server/i18n/messages/en.mjs +4 -0
  141. package/dist/server/i18n/messages/es.mjs +2 -0
  142. package/dist/server/i18n/messages/fr.mjs +2 -0
  143. package/dist/server/i18n/messages/pl.mjs +2 -0
  144. package/dist/server/i18n/messages/pt.mjs +2 -0
  145. package/dist/server/i18n/messages/sk.mjs +2 -0
  146. package/dist/server/modules/admin/.generated/module.d.mts +1 -1
  147. package/dist/server/modules/admin/auth-helpers.mjs +7 -1
  148. package/dist/server/modules/admin/block/block-builder.d.mts +0 -8
  149. package/dist/server/modules/admin/block/introspection.d.mts +2 -2
  150. package/dist/server/modules/admin/block/introspection.mjs +28 -4
  151. package/dist/server/modules/admin/block/prefetch.d.mts +11 -0
  152. package/dist/server/modules/admin/block/prefetch.mjs +108 -27
  153. package/dist/server/modules/admin/client/.generated/module.d.mts +68 -67
  154. package/dist/server/modules/admin/client/.generated/module.mjs +2 -0
  155. package/dist/server/modules/admin/client/views/collection-document.d.mts +6 -0
  156. package/dist/server/modules/admin/client/views/collection-document.mjs +10 -0
  157. package/dist/server/modules/admin/collections/account.d.mts +53 -52
  158. package/dist/server/modules/admin/collections/admin-locks.d.mts +57 -56
  159. package/dist/server/modules/admin/collections/admin-preferences.d.mts +38 -37
  160. package/dist/server/modules/admin/collections/admin-saved-views.d.mts +50 -49
  161. package/dist/server/modules/admin/collections/apikey.d.mts +76 -67
  162. package/dist/server/modules/admin/collections/assets.d.mts +37 -36
  163. package/dist/server/modules/admin/collections/session.d.mts +42 -41
  164. package/dist/server/modules/admin/collections/user.d.mts +57 -56
  165. package/dist/server/modules/admin/collections/verification.d.mts +34 -33
  166. package/dist/server/modules/admin/dto/admin-config.dto.mjs +34 -4
  167. package/dist/server/modules/admin/factories.mjs +4 -34
  168. package/dist/server/modules/admin/index.d.mts +3 -3
  169. package/dist/server/modules/admin/routes/admin-config.d.mts +4 -2
  170. package/dist/server/modules/admin/routes/admin-config.mjs +56 -24
  171. package/dist/server/modules/admin/routes/execute-action.d.mts +9 -9
  172. package/dist/server/modules/admin/routes/execute-action.mjs +35 -9
  173. package/dist/server/modules/admin/routes/locales.mjs +1 -1
  174. package/dist/server/modules/admin/routes/preview.d.mts +11 -11
  175. package/dist/server/modules/admin/routes/preview.mjs +6 -5
  176. package/dist/server/modules/admin/routes/reactive.d.mts +9 -9
  177. package/dist/server/modules/admin/routes/reactive.mjs +2 -2
  178. package/dist/server/modules/admin/routes/route-helpers.d.mts +11 -7
  179. package/dist/server/modules/admin/routes/route-helpers.mjs +1 -1
  180. package/dist/server/modules/admin/routes/setup.d.mts +7 -7
  181. package/dist/server/modules/admin/routes/translations.d.mts +4 -4
  182. package/dist/server/modules/admin/routes/widget-data.d.mts +5 -5
  183. package/dist/server/modules/admin/routes/widget-data.mjs +12 -4
  184. package/dist/server/modules/admin-preferences/collections/saved-views.d.mts +45 -45
  185. package/dist/server/modules/audit/.generated/module.d.mts +6 -6
  186. package/dist/server/modules/audit/collections/audit-log.d.mts +81 -80
  187. package/dist/server/modules/audit/jobs/audit-cleanup.d.mts +2 -2
  188. package/dist/server/plugin.mjs +10 -5
  189. package/dist/server/proxy-factories.d.mts +8 -1
  190. package/dist/server/proxy-factories.mjs +33 -1
  191. package/dist/server.d.mts +3 -1
  192. package/dist/shared/types/index.d.mts +1 -0
  193. package/dist/shared/types/saved-views.types.d.mts +14 -7
  194. package/dist/shared.d.mts +3 -2
  195. package/package.json +5 -4
@@ -5,13 +5,16 @@ import { cn } from "../../lib/utils.mjs";
5
5
  import { selectRealtime, useAdminStore } from "../../runtime/provider.mjs";
6
6
  import { useSafeContentLocales } from "../../runtime/content-locales-provider.mjs";
7
7
  import { useScopedLocale } from "../../runtime/locale-scope.mjs";
8
+ import { resolveIconElement } from "../../components/component-renderer.mjs";
8
9
  import { flattenOptions } from "../../components/primitives/types.mjs";
9
10
  import { Button } from "../../components/ui/button.mjs";
10
11
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../../components/ui/select.mjs";
11
12
  import { LocaleSwitcher } from "../../components/locale-switcher.mjs";
13
+ import { Badge } from "../../components/ui/badge.mjs";
12
14
  import { Tooltip, TooltipContent, TooltipTrigger } from "../../components/ui/tooltip.mjs";
13
15
  import { Checkbox } from "../../components/ui/checkbox.mjs";
14
16
  import { ActionButton } from "../../components/actions/action-button.mjs";
17
+ import { adminCollectionKey, getCollectionQueryApi } from "../../hooks/query-access.mjs";
15
18
  import { useCollectionFields } from "../../hooks/use-collection-fields.mjs";
16
19
  import { useSuspenseCollectionMeta } from "../../hooks/use-collection-meta.mjs";
17
20
  import { ActionDialog } from "../../components/actions/action-dialog.mjs";
@@ -26,18 +29,19 @@ import { AdminViewHeader, AdminViewLayout } from "../layout/admin-view-layout.mj
26
29
  import { TableViewSkeleton } from "./view-skeletons.mjs";
27
30
  import { useUploadCollection } from "../../hooks/use-upload-collection.mjs";
28
31
  import { useDebouncedValue, useSearch } from "../../hooks/use-search.mjs";
32
+ import { SearchInput } from "../../components/ui/search-input.mjs";
29
33
  import { autoExpandFields, hasFieldsToExpand } from "../../utils/auto-expand-fields.mjs";
30
34
  import { computeDefaultColumns, getAllAvailableFields } from "./columns/column-defaults.mjs";
31
35
  import { buildColumns } from "./columns/build-columns.mjs";
32
36
  import { HeaderActions } from "../../components/actions/header-actions.mjs";
33
37
  import { FilterBuilderSheet } from "../../components/filter-builder/filter-builder-sheet.mjs";
34
- import { SearchInput } from "../../components/ui/search-input.mjs";
35
38
  import { useActions } from "../../hooks/use-action.mjs";
36
39
  import { useRealtimeHighlight } from "../../hooks/use-realtime-highlight.mjs";
37
40
  import { useDeleteSavedView, useSaveView, useSavedViews } from "../../hooks/use-saved-views.mjs";
38
41
  import { useViewState } from "../../hooks/use-view-state.mjs";
39
42
  import { BulkActionToolbar } from "./bulk-action-toolbar.mjs";
40
43
  import { buildOutlineRows } from "./outline.mjs";
44
+ import { QuickFilterBar } from "./quick-filter-bar.mjs";
41
45
  import { UploadCollectionButton, mapListSchemaToConfig, stringifyGroupValue } from "./table-view.mjs";
42
46
  import { Icon } from "@iconify/react";
43
47
  import * as React from "react";
@@ -194,16 +198,30 @@ function SimpleValue({ value }) {
194
198
  if (Array.isArray(value)) return /* @__PURE__ */ jsx(Fragment, { children: value.map((item) => stringifySimpleValue(item)).join(", ") });
195
199
  return /* @__PURE__ */ jsx(Fragment, { children: stringifySimpleValue(value) });
196
200
  }
201
+ function formatShortDate(date) {
202
+ const now = /* @__PURE__ */ new Date();
203
+ const sameYear = date.getFullYear() === now.getFullYear();
204
+ const month = date.toLocaleDateString(void 0, { month: "short" });
205
+ const day = date.getDate();
206
+ return sameYear ? `${month} ${day}` : `${month} ${day}, ${date.getFullYear()}`;
207
+ }
208
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
197
209
  function stringifySimpleValue(value) {
198
210
  if (value === null || value === void 0 || value === "") return "-";
199
211
  if (typeof value === "object") {
212
+ if (value instanceof Date && !Number.isNaN(value.getTime())) return formatShortDate(value);
200
213
  const record = value;
201
214
  return String(record.title ?? record.name ?? record.label ?? record.id ?? "-");
202
215
  }
216
+ if (typeof value === "string" && ISO_DATE_RE.test(value)) {
217
+ const date = new Date(value);
218
+ if (!Number.isNaN(date.getTime())) return formatShortDate(date);
219
+ }
203
220
  return String(value);
204
221
  }
205
222
  function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/admin", showSearch = true, showFilters = true, showToolbar = true, realtime, headerActions, actionsConfig }) {
206
223
  "use no memo";
224
+ const collectionKey = adminCollectionKey(collection);
207
225
  const globalRealtimeConfig = useAdminStore(selectRealtime);
208
226
  const { fields: resolvedFields, schema } = useCollectionFields(collection, { fallbackFields: config?.fields });
209
227
  const { collections: uploadCollections } = useUploadCollection();
@@ -248,7 +266,7 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
248
266
  const [isSheetOpen, setIsSheetOpen] = useSidebarSearchParam("view-options", { legacyKey: "viewOptions" });
249
267
  const [searchTerm, setSearchTerm] = React.useState("");
250
268
  const [isSearchPanelOpen, setIsSearchPanelOpen] = React.useState(false);
251
- const [collapsedOutlineKeys, setCollapsedOutlineKeys] = React.useState(() => /* @__PURE__ */ new Set());
269
+ const [toggledOutlineKeys, setToggledOutlineKeys] = React.useState(() => /* @__PURE__ */ new Set());
252
270
  const [rowSelection, setRowSelection] = React.useState({});
253
271
  const defaultColumns = React.useMemo(() => computeDefaultColumns(resolvedFields, {
254
272
  meta: collectionMeta,
@@ -260,14 +278,39 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
260
278
  ]);
261
279
  const groupingConfig = resolvedListConfig?.grouping;
262
280
  const defaultGroupBy = groupingConfig?.defaultField ?? null;
263
- const viewState = useViewState(defaultColumns, {
281
+ const defaultFilters = React.useMemo(() => resolvedListConfig?.defaultFilters ?? [], [resolvedListConfig?.defaultFilters]);
282
+ const viewState = useViewState(defaultColumns, React.useMemo(() => ({
264
283
  realtime: resolvedRealtime,
265
- groupBy: defaultGroupBy
266
- }, collection, user?.id);
284
+ groupBy: defaultGroupBy,
285
+ filters: defaultFilters
286
+ }), [
287
+ resolvedRealtime,
288
+ defaultGroupBy,
289
+ defaultFilters
290
+ ]), collection, user?.id);
267
291
  const effectiveRealtime = viewState.config.realtime ?? resolvedRealtime;
268
292
  const visibleColumns = viewState.config.visibleColumns.length > 0 ? viewState.config.visibleColumns : defaultColumns;
293
+ const layout = resolvedListConfig?.layout;
294
+ const titleField = layout?.titleField ?? (collectionMeta?.title?.type === "field" ? collectionMeta.title.fieldName : void 0);
295
+ const subtitleField = layout?.subtitleField;
296
+ const leadingFields = normalizeFieldList(layout?.leadingFields);
297
+ const badgeFields = normalizeFieldList(layout?.badgeFields);
298
+ const metaFields = normalizeFieldList(layout?.metaFields);
299
+ const density = layout?.density ?? "compact";
269
300
  const outlineRelationNames = React.useMemo(() => extractRelationNamesFromOutline(resolvedListConfig?.outline), [resolvedListConfig?.outline]);
270
- const visibleColumnsForExpansion = React.useMemo(() => Array.from(new Set([...visibleColumns, ...outlineRelationNames])), [visibleColumns, outlineRelationNames]);
301
+ const visibleColumnsForExpansion = React.useMemo(() => Array.from(new Set([
302
+ ...visibleColumns,
303
+ ...outlineRelationNames,
304
+ ...metaFields,
305
+ ...leadingFields,
306
+ ...badgeFields
307
+ ])), [
308
+ visibleColumns,
309
+ outlineRelationNames,
310
+ metaFields,
311
+ leadingFields,
312
+ badgeFields
313
+ ]);
271
314
  const expandedFields = React.useMemo(() => autoExpandFields({
272
315
  fields: resolvedFields,
273
316
  list: resolvedListConfig,
@@ -279,7 +322,7 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
279
322
  visibleColumnsForExpansion,
280
323
  collectionMeta?.relations
281
324
  ]);
282
- const isKnownSortField = React.useCallback((field) => !!field && (field === "_title" || !!resolvedFields?.[field]), [resolvedFields]);
325
+ const isKnownSortField = React.useCallback((field) => !!field && (field === "_title" || field === "createdAt" || field === "updatedAt" || !!resolvedFields?.[field]), [resolvedFields]);
283
326
  const effectiveSort = React.useMemo(() => {
284
327
  if (isKnownSortField(viewState.config.sortConfig?.field)) return viewState.config.sortConfig;
285
328
  if (isKnownSortField(resolvedListConfig?.defaultSort?.field)) return resolvedListConfig.defaultSort;
@@ -292,7 +335,79 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
292
335
  resolvedListConfig?.defaultSort,
293
336
  isKnownSortField
294
337
  ]);
295
- const queryOptions = React.useMemo(() => {
338
+ const availableFields = React.useMemo(() => getAllAvailableFields(resolvedFields, { meta: collectionMeta }), [resolvedFields, collectionMeta]);
339
+ const fieldByName = React.useMemo(() => new Map(availableFields.map((field) => [field.name, field])), [availableFields]);
340
+ const effectiveOutline = React.useMemo(() => {
341
+ const outline = resolvedListConfig?.outline;
342
+ const groupBy = viewState.config.groupBy;
343
+ if (!groupBy) return outline;
344
+ const levels = outline?.levels ?? [];
345
+ const first = levels[0];
346
+ if (first?.kind === "field" && first.field === groupBy) return outline;
347
+ return {
348
+ ...outline,
349
+ defaultExpanded: outline?.defaultExpanded ?? true,
350
+ levels: [{
351
+ kind: "field",
352
+ field: groupBy
353
+ }, ...levels]
354
+ };
355
+ }, [resolvedListConfig?.outline, viewState.config.groupBy]);
356
+ const hasOutline = (resolvedListConfig?.outline?.levels?.length ?? 0) > 0;
357
+ const firstFieldLevel = React.useMemo(() => {
358
+ const first = effectiveOutline?.levels?.[0];
359
+ return first?.kind === "field" ? first : null;
360
+ }, [effectiveOutline]);
361
+ const groupValues = React.useMemo(() => {
362
+ if (!firstFieldLevel) return null;
363
+ const fieldDef = fieldByName.get(firstFieldLevel.field);
364
+ if (fieldDef?.type !== "select" || !fieldDef.options?.options) return null;
365
+ const flat = flattenOptions(fieldDef.options.options);
366
+ const order = firstFieldLevel.order;
367
+ if (Array.isArray(order)) return order.map((v) => flat.find((o) => String(o.value) === String(v)) ?? {
368
+ value: v,
369
+ label: { en: String(v) }
370
+ });
371
+ return flat;
372
+ }, [firstFieldLevel, fieldByName]);
373
+ const filteredGroupValues = React.useMemo(() => {
374
+ if (!groupValues || !firstFieldLevel) return groupValues;
375
+ const groupField = firstFieldLevel.field;
376
+ const relevant = viewState.config.filters.filter((f) => f.field === groupField);
377
+ if (relevant.length === 0) return groupValues;
378
+ return groupValues.filter((gv) => {
379
+ const val = String(gv.value);
380
+ for (const filter of relevant) switch (filter.operator) {
381
+ case "equals":
382
+ if (String(filter.value) !== val) return false;
383
+ break;
384
+ case "not_equals":
385
+ if (String(filter.value) === val) return false;
386
+ break;
387
+ case "in":
388
+ if (!Array.isArray(filter.value) || !filter.value.map(String).includes(val)) return false;
389
+ break;
390
+ case "not_in":
391
+ if (Array.isArray(filter.value) && filter.value.map(String).includes(val)) return false;
392
+ break;
393
+ }
394
+ return true;
395
+ });
396
+ }, [
397
+ groupValues,
398
+ firstFieldLevel,
399
+ viewState.config.filters
400
+ ]);
401
+ const isGrouped = filteredGroupValues != null && filteredGroupValues.length > 0;
402
+ const [groupLimits, setGroupLimits] = React.useState({});
403
+ const DEFAULT_GROUP_LIMIT = 20;
404
+ const loadMoreInGroup = React.useCallback((groupKey) => {
405
+ setGroupLimits((prev) => ({
406
+ ...prev,
407
+ [groupKey]: (prev[groupKey] ?? DEFAULT_GROUP_LIMIT) + DEFAULT_GROUP_LIMIT
408
+ }));
409
+ }, []);
410
+ const baseQueryOptions = React.useMemo(() => {
296
411
  const options = {};
297
412
  if (collectionMeta?.softDelete) options.includeDeleted = !!viewState.config.includeDeleted;
298
413
  if (hasFieldsToExpand(expandedFields)) options.with = expandedFields;
@@ -302,28 +417,41 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
302
417
  relationNames: collectionMeta?.relations ?? []
303
418
  });
304
419
  if (where) options.where = where;
305
- const groupBy = viewState.config.groupBy;
306
420
  const sortConfig = effectiveSort;
307
- if (groupBy && sortConfig?.field && sortConfig.field !== groupBy) options.orderBy = [{ [groupBy]: "asc" }, { [sortConfig.field]: sortConfig.direction }];
308
- else if (groupBy) options.orderBy = { [groupBy]: sortConfig?.direction ?? "asc" };
309
- else if (sortConfig) options.orderBy = { [sortConfig.field]: sortConfig.direction };
310
- const pageSize = viewState.config.pagination?.pageSize ?? 25;
311
- const page = viewState.config.pagination?.page ?? 1;
312
- options.limit = pageSize;
313
- options.offset = (page - 1) * pageSize;
421
+ if (sortConfig) options.orderBy = { [sortConfig.field]: sortConfig.direction };
314
422
  return options;
315
423
  }, [
316
424
  collectionMeta?.softDelete,
317
425
  collectionMeta?.relations,
318
426
  viewState.config.includeDeleted,
319
427
  viewState.config.filters,
320
- viewState.config.groupBy,
321
- viewState.config.pagination?.page,
322
- viewState.config.pagination?.pageSize,
323
428
  expandedFields,
324
429
  effectiveSort,
325
430
  resolvedFields
326
431
  ]);
432
+ const flatQueryOptions = React.useMemo(() => {
433
+ if (isGrouped) return baseQueryOptions;
434
+ const options = { ...baseQueryOptions };
435
+ const groupBy = viewState.config.groupBy;
436
+ const sortConfig = effectiveSort;
437
+ if (groupBy && sortConfig?.field && sortConfig.field !== groupBy) options.orderBy = [{ [groupBy]: "asc" }, { [sortConfig.field]: sortConfig.direction }];
438
+ if (hasOutline && !isGrouped) options.limit = 500;
439
+ else {
440
+ const pageSize = viewState.config.pagination?.pageSize ?? 25;
441
+ const page = viewState.config.pagination?.page ?? 1;
442
+ options.limit = pageSize;
443
+ options.offset = (page - 1) * pageSize;
444
+ }
445
+ return options;
446
+ }, [
447
+ baseQueryOptions,
448
+ isGrouped,
449
+ hasOutline,
450
+ viewState.config.groupBy,
451
+ viewState.config.pagination?.page,
452
+ viewState.config.pagination?.pageSize,
453
+ effectiveSort
454
+ ]);
327
455
  const debouncedSearchTerm = useDebouncedValue(searchTerm, 300);
328
456
  const isSearching = debouncedSearchTerm.trim().length > 0;
329
457
  const { data: searchData, isLoading: searchLoading, isFetching: searchFetching } = useSearch({
@@ -332,13 +460,34 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
332
460
  limit: 100,
333
461
  highlights: true
334
462
  }, { enabled: isSearching });
335
- const { data: listData, isLoading: listLoading, error: listError } = useCollectionList(collection, queryOptions, { enabled: !isSearching }, { realtime: effectiveRealtime });
336
- const items = React.useMemo(() => isSearching ? searchData?.docs ?? [] : listData?.docs ?? [], [
463
+ const groupQueries = useQueries({ queries: isGrouped && !isSearching ? filteredGroupValues.map((gv) => {
464
+ const limit = groupLimits[String(gv.value)] ?? DEFAULT_GROUP_LIMIT;
465
+ const groupWhere = {
466
+ ...baseQueryOptions.where,
467
+ [firstFieldLevel.field]: gv.value
468
+ };
469
+ return getCollectionQueryApi(queryOpts, collectionKey).find({
470
+ ...baseQueryOptions,
471
+ where: groupWhere,
472
+ limit,
473
+ offset: 0,
474
+ locale
475
+ }, { realtime: false });
476
+ }) : [] });
477
+ const { data: listData, isLoading: listLoading, error: listError } = useCollectionList(collectionKey, flatQueryOptions, { enabled: !isSearching && !isGrouped }, { realtime: effectiveRealtime });
478
+ const groupDataVersion = isGrouped ? groupQueries.map((q) => q.dataUpdatedAt).join(",") : "";
479
+ const items = React.useMemo(() => {
480
+ if (isSearching) return searchData?.docs ?? [];
481
+ if (isGrouped) return groupQueries.flatMap((q) => q.data?.docs ?? []);
482
+ return listData?.docs ?? [];
483
+ }, [
337
484
  isSearching,
485
+ isGrouped,
338
486
  searchData?.docs,
487
+ groupDataVersion,
339
488
  listData?.docs
340
489
  ]);
341
- const isLoading = isSearching ? searchLoading : listLoading;
490
+ const isLoading = isSearching ? searchLoading : isGrouped ? groupQueries.some((q) => q.isLoading) : listLoading;
342
491
  const isSearchActive = isSearching && searchFetching;
343
492
  const { isHighlighted } = useRealtimeHighlight(items, { enabled: effectiveRealtime && !isSearching });
344
493
  const { getLock, isLocked: isDocLocked } = useLocks({
@@ -348,17 +497,7 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
348
497
  });
349
498
  const edgeLevels = React.useMemo(() => extractEdgeLevels(resolvedListConfig?.outline), [resolvedListConfig?.outline]);
350
499
  const edgeQueries = useQueries({ queries: edgeLevels.map((level) => {
351
- const collectionQueries = queryOpts.collections?.[level.collection];
352
- if (!collectionQueries?.find) return {
353
- queryKey: [
354
- "questpie",
355
- "outline-edge-missing",
356
- level.collection
357
- ],
358
- queryFn: async () => ({ docs: [] }),
359
- enabled: false
360
- };
361
- return collectionQueries.find({
500
+ return getCollectionQueryApi(queryOpts, adminCollectionKey(level.collection)).find({
362
501
  where: level.where,
363
502
  with: {
364
503
  [level.parentField]: true,
@@ -376,39 +515,86 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
376
515
  });
377
516
  return map;
378
517
  }, [edgeLevels, edgeQueries]);
379
- const effectiveOutline = React.useMemo(() => {
380
- const outline = resolvedListConfig?.outline;
381
- const groupBy = viewState.config.groupBy;
382
- if (!groupBy) return outline;
383
- const levels = outline?.levels ?? [];
384
- const first = levels[0];
385
- if (first?.kind === "field" && first.field === groupBy) return outline;
518
+ const metaForValue = React.useCallback((value, field) => {
519
+ if (!field) return void 0;
520
+ const fieldDef = fieldByName.get(field);
521
+ if (fieldDef?.type !== "select") return void 0;
522
+ const options = fieldDef?.options?.options;
523
+ if (!Array.isArray(options)) return void 0;
524
+ const option = flattenOptions(options).find((opt) => String(opt.value) === String(value));
525
+ if (!option) return void 0;
526
+ if (!option.icon && !option.className) return void 0;
386
527
  return {
387
- ...outline,
388
- defaultExpanded: outline?.defaultExpanded ?? true,
389
- levels: [{
390
- kind: "field",
391
- field: groupBy
392
- }, ...levels]
528
+ icon: option.icon,
529
+ className: option.className
393
530
  };
394
- }, [resolvedListConfig?.outline, viewState.config.groupBy]);
395
- const availableFields = React.useMemo(() => getAllAvailableFields(resolvedFields, { meta: collectionMeta }), [resolvedFields, collectionMeta]);
396
- const fieldByName = React.useMemo(() => new Map(availableFields.map((field) => [field.name, field])), [availableFields]);
397
- const outlineRows = React.useMemo(() => buildOutlineRows({
531
+ }, [fieldByName]);
532
+ const remainingOutline = React.useMemo(() => {
533
+ if (!isGrouped || !effectiveOutline) return effectiveOutline;
534
+ return {
535
+ ...effectiveOutline,
536
+ levels: effectiveOutline.levels.slice(1)
537
+ };
538
+ }, [isGrouped, effectiveOutline]);
539
+ const labelForValue = React.useCallback((value, field) => stringifyGroupValue(value, field ? fieldByName.get(field) : void 0, resolveText, t, uiLocale, t("common.noValue")), [
540
+ fieldByName,
541
+ resolveText,
542
+ t,
543
+ uiLocale
544
+ ]);
545
+ const outlineRows = React.useMemo(() => isGrouped ? [] : buildOutlineRows({
398
546
  docs: items,
399
547
  outline: effectiveOutline,
400
548
  edgesByCollection,
401
- collapsedKeys: collapsedOutlineKeys,
402
- labelForValue: (value, field) => stringifyGroupValue(value, field ? fieldByName.get(field) : void 0, resolveText, t, uiLocale, t("common.noValue"))
549
+ toggledKeys: toggledOutlineKeys,
550
+ labelForValue,
551
+ metaForValue
403
552
  }), [
553
+ isGrouped,
404
554
  items,
405
555
  effectiveOutline,
406
556
  edgesByCollection,
407
- collapsedOutlineKeys,
408
- fieldByName,
409
- resolveText,
410
- t,
411
- uiLocale
557
+ toggledOutlineKeys,
558
+ labelForValue,
559
+ metaForValue
560
+ ]);
561
+ const allGroupDocs = React.useMemo(() => {
562
+ if (!isGrouped) return void 0;
563
+ return groupQueries.flatMap((q) => q.data?.docs ?? []);
564
+ }, [isGrouped, groupDataVersion]);
565
+ const groupOutlineRows = React.useMemo(() => {
566
+ if (!isGrouped) return [];
567
+ return filteredGroupValues.map((gv, i) => {
568
+ const docs = (groupQueries[i]?.data)?.docs ?? [];
569
+ if (docs.length === 0) return [];
570
+ const outline = remainingOutline;
571
+ if (!outline || outline.levels.length === 0) return docs.map((doc) => ({
572
+ kind: "record",
573
+ key: `record:${doc.id}`,
574
+ id: String(doc.id),
575
+ doc,
576
+ depth: 0
577
+ }));
578
+ return buildOutlineRows({
579
+ docs,
580
+ outline,
581
+ edgesByCollection,
582
+ toggledKeys: toggledOutlineKeys,
583
+ labelForValue,
584
+ metaForValue,
585
+ allDocs: allGroupDocs
586
+ });
587
+ });
588
+ }, [
589
+ isGrouped,
590
+ filteredGroupValues,
591
+ groupDataVersion,
592
+ remainingOutline,
593
+ edgesByCollection,
594
+ toggledOutlineKeys,
595
+ labelForValue,
596
+ metaForValue,
597
+ allGroupDocs
412
598
  ]);
413
599
  const table = useReactTable({
414
600
  data: items,
@@ -419,33 +605,44 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
419
605
  getRowId: (row) => String(row.id),
420
606
  state: { rowSelection }
421
607
  });
422
- const rowsById = React.useMemo(() => new Map(table.getRowModel().rows.map((row) => [String(row.id), row])), [table, items]);
423
- const deleteMutation = useCollectionDelete(collection);
424
- const restoreMutation = useCollectionRestore(collection);
608
+ const rowsById = React.useMemo(() => {
609
+ const map = /* @__PURE__ */ new Map();
610
+ for (const row of table.getRowModel().rows) {
611
+ map.set(String(row.id), row);
612
+ const originalId = row.original?.id;
613
+ if (originalId !== void 0 && originalId !== null) map.set(String(originalId), row);
614
+ }
615
+ return map;
616
+ }, [table]);
617
+ const deleteMutation = useCollectionDelete(collectionKey);
618
+ const restoreMutation = useCollectionRestore(collectionKey);
425
619
  const { data: savedViewsData, isLoading: savedViewsLoading } = useSavedViews(collection, user?.id);
426
620
  const saveViewMutation = useSaveView(collection, user?.id);
427
621
  const deleteViewMutation = useDeleteSavedView(collection, user?.id);
428
622
  const hasActiveFilters = viewState.config.filters.length > 0;
429
- const hasViewOptionsState = hasActiveFilters || !!viewState.config.groupBy || viewState.config.visibleColumns.length !== defaultColumns.length || !!viewState.config.includeDeleted;
623
+ const hasViewOptionsState = hasActiveFilters || !!viewState.config.sortConfig || !!viewState.config.groupBy || !!viewState.config.includeDeleted;
430
624
  const groupableFields = React.useMemo(() => {
431
625
  const groupableNames = groupingConfig?.fields ?? [];
432
626
  if (groupableNames.length === 0) return [];
433
627
  const groupableSet = new Set(groupableNames);
434
628
  return availableFields.filter((field) => groupableSet.has(field.name));
435
629
  }, [availableFields, groupingConfig?.fields]);
436
- const layout = resolvedListConfig?.layout;
437
- const titleField = layout?.titleField ?? (collectionMeta?.title?.type === "field" ? collectionMeta.title.fieldName : void 0);
438
- const subtitleField = layout?.subtitleField;
439
- const leadingFields = normalizeFieldList(layout?.leadingFields);
440
- const badgeFields = normalizeFieldList(layout?.badgeFields);
441
- const metaFields = normalizeFieldList(layout?.metaFields);
442
- const density = layout?.density ?? "compact";
443
630
  const clearFilters = React.useCallback(() => {
444
631
  viewState.setConfig({
445
632
  ...viewState.config,
446
633
  filters: []
447
634
  });
448
635
  }, [viewState]);
636
+ const applyQuickFilters = React.useCallback((filters) => {
637
+ viewState.setConfig((current) => ({
638
+ ...current,
639
+ filters,
640
+ pagination: {
641
+ ...current.pagination ?? { pageSize: 25 },
642
+ page: 1
643
+ }
644
+ }));
645
+ }, [viewState]);
449
646
  const handleSaveView = React.useCallback((name, configuration) => {
450
647
  saveViewMutation.mutate({
451
648
  name,
@@ -469,7 +666,7 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
469
666
  collection
470
667
  ]);
471
668
  const toggleOutlineKey = React.useCallback((key) => {
472
- setCollapsedOutlineKeys((current) => {
669
+ setToggledOutlineKeys((current) => {
473
670
  const next = new Set(current);
474
671
  if (next.has(key)) next.delete(key);
475
672
  else next.add(key);
@@ -484,6 +681,37 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
484
681
  if (cell && column) return flexRender(cell.column.columnDef.cell, cell.getContext());
485
682
  return /* @__PURE__ */ jsx(SimpleValue, { value: fallback ?? getValueAtPath(row?.original, field) });
486
683
  }, [columnsByKey]);
684
+ const resolveFieldValue = React.useCallback((item, field) => {
685
+ const value = getValueAtPath(item, field);
686
+ if (value != null && typeof value === "object") return value;
687
+ const fieldDef = fieldByName.get(field);
688
+ if (fieldDef?.type === "relation") {
689
+ const expanded = getValueAtPath(item, fieldDef.options?.relationName ?? field);
690
+ if (expanded && typeof expanded === "object") return expanded;
691
+ }
692
+ return value;
693
+ }, [fieldByName]);
694
+ const renderMetaField = React.useCallback((field, value) => {
695
+ if (value === null || value === void 0 || value === "") return /* @__PURE__ */ jsx("span", {
696
+ className: "text-foreground-subtle",
697
+ children: "-"
698
+ });
699
+ const fieldDef = fieldByName.get(field);
700
+ if (fieldDef?.type === "datetime" || fieldDef?.type === "date" || field === "updatedAt" || field === "createdAt") return /* @__PURE__ */ jsx("span", { children: typeof value === "string" && ISO_DATE_RE.test(value) ? formatShortDate(new Date(value)) : value instanceof Date ? formatShortDate(value) : String(value) });
701
+ return /* @__PURE__ */ jsx(Badge, {
702
+ variant: "secondary",
703
+ className: "max-w-full truncate",
704
+ children: typeof value === "object" ? String(value.title ?? value.name ?? value.label ?? value.id ?? "-") : String(value)
705
+ });
706
+ }, [fieldByName]);
707
+ const renderLeadingIcon = React.useCallback((field, value) => {
708
+ const fieldDef = fieldByName.get(field);
709
+ if (fieldDef?.type === "select" && fieldDef.options?.options) {
710
+ const option = flattenOptions(fieldDef.options.options).find((opt) => String(opt.value) === String(value));
711
+ if (option?.icon) return resolveIconElement(option.icon);
712
+ }
713
+ return null;
714
+ }, [fieldByName]);
487
715
  const handleRowClick = React.useCallback((item) => navigate(`${basePath}/collections/${collection}/${item.id}`), [
488
716
  navigate,
489
717
  basePath,
@@ -540,7 +768,7 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
540
768
  showFilters && /* @__PURE__ */ jsxs(Tooltip, { children: [/* @__PURE__ */ jsx(TooltipTrigger, { render: /* @__PURE__ */ jsxs(Button, {
541
769
  variant: "outline",
542
770
  size: "icon-sm",
543
- className: cn("relative", hasViewOptionsState && "border-foreground"),
771
+ className: "relative",
544
772
  onClick: () => setIsSheetOpen(true),
545
773
  "aria-label": t("viewOptions.title"),
546
774
  children: [/* @__PURE__ */ jsx(Icon, { icon: "ph:sliders-horizontal" }), hasViewOptionsState && /* @__PURE__ */ jsx("span", { className: "bg-foreground absolute top-1 right-1 size-1.5 rounded-full" })]
@@ -549,30 +777,6 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
549
777
  align: "end",
550
778
  children: t("viewOptions.title")
551
779
  })] }),
552
- /* @__PURE__ */ jsxs(Select, {
553
- value: effectiveSort?.field ?? "",
554
- onValueChange: (field) => viewState.setSort(field ? {
555
- field,
556
- direction: effectiveSort?.direction ?? "asc"
557
- } : null),
558
- children: [/* @__PURE__ */ jsx(SelectTrigger, {
559
- className: "h-8 w-40",
560
- children: /* @__PURE__ */ jsx(SelectValue, { placeholder: "Sort" })
561
- }), /* @__PURE__ */ jsx(SelectContent, { children: availableFields.map((field) => /* @__PURE__ */ jsx(SelectItem, {
562
- value: field.name,
563
- children: resolveText(field.label, field.name)
564
- }, field.name)) })]
565
- }),
566
- /* @__PURE__ */ jsx(Button, {
567
- variant: "outline",
568
- size: "icon-sm",
569
- "aria-label": "Toggle sort direction",
570
- onClick: () => viewState.setSort({
571
- field: effectiveSort?.field ?? "createdAt",
572
- direction: effectiveSort?.direction === "asc" ? "desc" : "asc"
573
- }),
574
- children: /* @__PURE__ */ jsx(Icon, { icon: effectiveSort?.direction === "asc" ? "ph:sort-ascending" : "ph:sort-descending" })
575
- }),
576
780
  canUploadToCollection && showToolbar && /* @__PURE__ */ jsx(UploadCollectionButton, {
577
781
  collection,
578
782
  onUploaded: () => actionHelpers.invalidateCollection(collection)
@@ -587,120 +791,140 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
587
791
  ] })
588
792
  }),
589
793
  children: /* @__PURE__ */ jsxs("div", {
590
- className: "space-y-3",
794
+ className: "space-y-4",
591
795
  children: [
592
796
  showSearch && isSearchPanelOpen && /* @__PURE__ */ jsx("div", {
593
- className: "border-border-subtle bg-muted/20 rounded-md border p-3",
797
+ className: "max-w-xl",
594
798
  children: /* @__PURE__ */ jsx(SearchInput, {
595
799
  value: searchTerm,
596
800
  onChange: (event) => setSearchTerm(event.target.value),
597
801
  onClear: () => setSearchTerm(""),
598
802
  placeholder: t("collectionSearch.placeholder"),
599
- autoFocus: true,
600
- isLoading: isSearchActive
803
+ isLoading: isSearchActive,
804
+ containerClassName: "h-10"
601
805
  })
602
806
  }),
603
- outlineRows.length === 0 ? /* @__PURE__ */ jsx(EmptyState, {
604
- title: emptyStateTitle,
605
- description: emptyStateDescription,
606
- height: "h-64",
607
- action: isSearching || hasActiveFilters ? /* @__PURE__ */ jsxs("div", {
608
- className: "flex gap-2",
609
- children: [isSearching && /* @__PURE__ */ jsxs(Button, {
610
- variant: "outline",
611
- size: "sm",
612
- className: "gap-2",
613
- onClick: () => setSearchTerm(""),
614
- children: [/* @__PURE__ */ jsx(Icon, {
615
- icon: "ph:x",
616
- className: "size-3.5"
617
- }), t("common.clear")]
618
- }), hasActiveFilters && /* @__PURE__ */ jsxs(Button, {
619
- variant: "outline",
620
- size: "sm",
621
- className: "gap-2",
622
- onClick: clearFilters,
623
- children: [/* @__PURE__ */ jsx(Icon, {
624
- icon: "ph:funnel-x",
625
- className: "size-3.5"
626
- }), t("viewOptions.clearFilters")]
627
- })]
628
- }) : void 0
629
- }) : /* @__PURE__ */ jsx("div", {
630
- className: "border-border-subtle overflow-hidden rounded-md border",
631
- children: outlineRows.map((outlineRow) => {
632
- if (outlineRow.kind !== "record") return /* @__PURE__ */ jsx(OutlineHeaderRow, {
633
- row: outlineRow,
634
- showCounts: resolvedListConfig?.outline?.showCounts ?? true,
635
- onToggle: toggleOutlineKey
636
- }, outlineRow.key);
807
+ /* @__PURE__ */ jsx(QuickFilterBar, {
808
+ quickFilters: resolvedListConfig?.quickFilters,
809
+ currentFilters: viewState.config.filters,
810
+ onApply: applyQuickFilters
811
+ }),
812
+ (() => {
813
+ if (isGrouped ? groupOutlineRows.every((g) => g.length === 0) : outlineRows.length === 0) return /* @__PURE__ */ jsx(EmptyState, {
814
+ title: emptyStateTitle,
815
+ description: emptyStateDescription,
816
+ height: "h-64",
817
+ action: isSearching || hasActiveFilters ? /* @__PURE__ */ jsxs("div", {
818
+ className: "flex gap-2",
819
+ children: [isSearching && /* @__PURE__ */ jsxs(Button, {
820
+ variant: "outline",
821
+ size: "sm",
822
+ className: "gap-2",
823
+ onClick: () => setSearchTerm(""),
824
+ children: [/* @__PURE__ */ jsx(Icon, {
825
+ icon: "ph:x",
826
+ className: "size-3.5"
827
+ }), t("common.clear")]
828
+ }), hasActiveFilters && /* @__PURE__ */ jsxs(Button, {
829
+ variant: "outline",
830
+ size: "sm",
831
+ className: "gap-2",
832
+ onClick: clearFilters,
833
+ children: [/* @__PURE__ */ jsx(Icon, {
834
+ icon: "ph:funnel-x",
835
+ className: "size-3.5"
836
+ }), t("viewOptions.clearFilters")]
837
+ })]
838
+ }) : void 0
839
+ });
840
+ const renderRecordRow = (outlineRow, groupContext) => {
637
841
  const tableRow = rowsById.get(outlineRow.id);
638
- if (!tableRow) return null;
639
- const item = tableRow.original;
842
+ const item = tableRow?.original ?? outlineRow.doc;
640
843
  const lock = getLock(item.id);
641
844
  const locked = isDocLocked(item.id);
642
845
  const lockUser = lock ? getLockUser(lock) : null;
643
- const isSelected = tableRow.getIsSelected();
644
- const titleValue = (titleField ? getValueAtPath(item, titleField) : item._title) ?? item.title ?? item.name ?? item.id;
846
+ const isSelected = tableRow?.getIsSelected() ?? false;
847
+ const titleValue = (titleField ? getValueAtPath(item, titleField) : item["_title"]) ?? item.title ?? item.name ?? item.id;
645
848
  const subtitleValue = subtitleField ? getValueAtPath(item, subtitleField) : void 0;
849
+ const isGroupMismatch = groupContext != null && getValueAtPath(item, groupContext.field) !== groupContext.value;
646
850
  return /* @__PURE__ */ jsxs("div", {
647
- className: cn("group/list-row border-border-subtle hover:bg-muted/35 flex min-w-0 items-start gap-2 border-b px-3 text-sm transition-colors last:border-b-0", density === "compact" ? "py-2" : "py-3", isHighlighted(item.id) && "bg-info/10", isSelected && "bg-muted/50"),
648
- style: { paddingLeft: `${12 + outlineRow.depth * 18}px` },
851
+ "data-state": isSelected ? "selected" : void 0,
852
+ className: cn("group/list-row hover:bg-accent data-[state=selected]:bg-muted relative flex min-w-0 cursor-pointer items-center gap-2 rounded-md px-4 text-sm transition-colors", density === "compact" ? "min-h-9 py-1" : "min-h-11 py-2", isHighlighted(item.id) && "animate-realtime-pulse", isGroupMismatch && "opacity-45"),
649
853
  children: [
854
+ outlineRow.depth > 0 && /* @__PURE__ */ jsx("span", {
855
+ className: "bg-border-subtle absolute top-0 bottom-0 w-px",
856
+ style: { left: `${52 + (outlineRow.depth - 1) * 18}px` },
857
+ "aria-hidden": "true"
858
+ }),
650
859
  /* @__PURE__ */ jsx("div", {
651
860
  role: "presentation",
652
- className: "mt-0.5 shrink-0",
861
+ "data-state": isSelected ? "selected" : void 0,
862
+ className: "shrink-0 opacity-0 transition-opacity duration-[var(--motion-duration-fast)] group-hover/list-row:opacity-100 data-[state=selected]:opacity-100",
653
863
  onClick: (event) => event.stopPropagation(),
654
864
  onKeyDown: (event) => event.stopPropagation(),
655
865
  children: /* @__PURE__ */ jsx(Checkbox, {
656
866
  checked: isSelected,
657
- disabled: !tableRow.getCanSelect(),
658
- onCheckedChange: (checked) => tableRow.toggleSelected(!!checked),
659
- "aria-label": "Select row"
867
+ disabled: !tableRow || !tableRow.getCanSelect(),
868
+ onCheckedChange: (checked) => tableRow?.toggleSelected(!!checked),
869
+ "aria-label": t("table.selectRow")
660
870
  })
661
871
  }),
662
- /* @__PURE__ */ jsx("button", {
872
+ outlineRow.expandable ? /* @__PURE__ */ jsx("button", {
663
873
  type: "button",
664
- className: cn("text-muted-foreground mt-0.5 flex size-5 shrink-0 items-center justify-center rounded transition-colors", outlineRow.expandable && "hover:bg-muted hover:text-foreground"),
665
- disabled: !outlineRow.expandable,
874
+ className: "text-muted-foreground hover:bg-surface-high hover:text-foreground focus-visible:ring-ring/40 -ml-1 flex size-7 shrink-0 items-center justify-center rounded-[var(--control-radius-inner)] transition-[background-color,color,box-shadow] focus-visible:ring-2 focus-visible:outline-none",
666
875
  onClick: (event) => {
667
876
  event.stopPropagation();
668
- if (outlineRow.expandable) toggleOutlineKey(outlineRow.key);
877
+ toggleOutlineKey(outlineRow.key);
669
878
  },
670
- "aria-label": outlineRow.collapsed ? "Expand row" : "Collapse row",
671
- children: outlineRow.expandable ? /* @__PURE__ */ jsx(Icon, {
879
+ "aria-label": outlineRow.collapsed ? t("a11y.expand") : t("a11y.collapse"),
880
+ children: /* @__PURE__ */ jsx(Icon, {
672
881
  icon: "ph:caret-right-bold",
673
882
  className: cn("size-3 transition-transform", !outlineRow.collapsed && "rotate-90")
674
- }) : null
675
- }),
883
+ })
884
+ }) : null,
676
885
  /* @__PURE__ */ jsxs("button", {
677
886
  type: "button",
678
- className: "min-w-0 flex-1 text-left",
887
+ className: "focus-visible:ring-ring/40 -my-1 min-w-0 flex-1 rounded-[var(--control-radius-inner)] py-1 text-left focus-visible:ring-2 focus-visible:outline-none",
888
+ style: outlineRow.depth > 0 ? { paddingLeft: `${outlineRow.depth * 18}px` } : void 0,
679
889
  onClick: () => handleRowClick(item),
680
890
  children: [/* @__PURE__ */ jsxs("div", {
681
891
  className: "flex min-w-0 items-center gap-2",
682
892
  children: [
683
- leadingFields.map((field) => /* @__PURE__ */ jsx("span", {
684
- className: "shrink-0",
685
- children: renderField(tableRow, field, getValueAtPath(item, field))
686
- }, field)),
893
+ leadingFields.map((field) => {
894
+ const fieldValue = getValueAtPath(item, field);
895
+ return /* @__PURE__ */ jsx("span", {
896
+ className: "flex size-4 shrink-0 items-center justify-center",
897
+ children: renderLeadingIcon(field, fieldValue) || renderField(tableRow, field, fieldValue)
898
+ }, field);
899
+ }),
687
900
  /* @__PURE__ */ jsx("span", {
688
- className: "text-foreground truncate font-medium",
901
+ className: cn("truncate", outlineRow.depth > 0 ? "text-muted-foreground font-normal" : "text-foreground font-medium"),
689
902
  children: titleField ? renderField(tableRow, titleField, titleValue) : stringifySimpleValue(titleValue)
690
903
  }),
691
- badgeFields.map((field) => /* @__PURE__ */ jsx("span", {
692
- className: "bg-muted text-muted-foreground inline-flex h-5 max-w-36 shrink-0 items-center rounded px-1.5 font-mono text-[11px]",
693
- children: /* @__PURE__ */ jsx("span", {
694
- className: "truncate",
695
- children: renderField(tableRow, field, getValueAtPath(item, field))
696
- })
697
- }, field)),
904
+ badgeFields.map((field) => {
905
+ const fieldValue = getValueAtPath(item, field);
906
+ const icon = renderLeadingIcon(field, fieldValue);
907
+ if (icon) return /* @__PURE__ */ jsx("span", {
908
+ className: "flex size-4 shrink-0 items-center justify-center",
909
+ children: icon
910
+ }, field);
911
+ return /* @__PURE__ */ jsx("span", {
912
+ className: "text-muted-foreground inline-flex max-w-36 shrink-0 items-center text-xs",
913
+ children: /* @__PURE__ */ jsx("span", {
914
+ className: "truncate",
915
+ children: renderField(tableRow, field, fieldValue)
916
+ })
917
+ }, field);
918
+ }),
698
919
  locked && /* @__PURE__ */ jsxs("span", {
699
- className: "text-warning inline-flex items-center gap-1 text-xs",
920
+ className: "text-muted-foreground bg-muted inline-flex shrink-0 items-center gap-1 rounded-full px-1.5 py-0.5 text-xs",
700
921
  children: [/* @__PURE__ */ jsx(Icon, {
701
922
  icon: "ph:lock-key",
702
923
  className: "size-3"
703
- }), lockUser?.name ?? t("collection.locked")]
924
+ }), /* @__PURE__ */ jsx("span", {
925
+ className: "max-w-20 truncate",
926
+ children: lockUser?.name ?? t("collection.locked")
927
+ })]
704
928
  })
705
929
  ]
706
930
  }), subtitleValue !== void 0 && /* @__PURE__ */ jsx("div", {
@@ -709,10 +933,10 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
709
933
  })]
710
934
  }),
711
935
  metaFields.length > 0 && /* @__PURE__ */ jsx("div", {
712
- className: "text-muted-foreground hidden max-w-[42%] shrink-0 items-center justify-end gap-3 overflow-hidden text-right text-xs md:flex",
936
+ className: "text-muted-foreground hidden shrink-0 items-center gap-2 text-xs md:flex",
713
937
  children: metaFields.map((field) => /* @__PURE__ */ jsx("span", {
714
- className: "min-w-0 truncate",
715
- children: renderField(tableRow, field, getValueAtPath(item, field))
938
+ className: "flex w-28 min-w-0 items-center justify-end",
939
+ children: renderMetaField(field, resolveFieldValue(item, field))
716
940
  }, field))
717
941
  }),
718
942
  actions.row.length > 0 && /* @__PURE__ */ jsx("div", {
@@ -732,27 +956,198 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
732
956
  })
733
957
  ]
734
958
  }, outlineRow.key);
735
- })
959
+ };
960
+ if (isGrouped) return /* @__PURE__ */ jsx("div", {
961
+ className: "flex flex-col gap-1.5 overflow-hidden",
962
+ children: filteredGroupValues.map((gv, i) => {
963
+ const rows = groupOutlineRows[i] ?? [];
964
+ const queryData = groupQueries[i]?.data;
965
+ const totalDocs = queryData?.totalDocs ?? 0;
966
+ const loadedCount = (queryData?.docs ?? []).length;
967
+ const groupKey = String(gv.value);
968
+ const groupLabel = labelForValue(gv.value, firstFieldLevel?.field);
969
+ const meta = metaForValue(gv.value, firstFieldLevel?.field);
970
+ const groupIcon = meta?.icon ? resolveIconElement(meta.icon) : null;
971
+ const isGroupLoading = groupQueries[i]?.isLoading ?? false;
972
+ if (totalDocs === 0 && !isGroupLoading) return null;
973
+ const groupToggleKey = `group:${groupKey}`;
974
+ const isGroupCollapsed = toggledOutlineKeys.has(groupToggleKey);
975
+ return /* @__PURE__ */ jsxs("div", {
976
+ className: "flex flex-col",
977
+ children: [/* @__PURE__ */ jsxs("button", {
978
+ type: "button",
979
+ className: "hover:bg-surface-mid focus-visible:ring-ring/40 flex min-h-8 items-center gap-2 rounded-md px-3 py-1 text-left transition-colors focus-visible:ring-2 focus-visible:outline-none",
980
+ onClick: () => toggleOutlineKey(groupToggleKey),
981
+ "aria-expanded": !isGroupCollapsed,
982
+ children: [
983
+ /* @__PURE__ */ jsx(Icon, {
984
+ icon: "ph:caret-right-bold",
985
+ className: cn("text-muted-foreground size-3 shrink-0 transition-transform", !isGroupCollapsed && "rotate-90")
986
+ }),
987
+ groupIcon && /* @__PURE__ */ jsx("span", {
988
+ className: "size-4 shrink-0",
989
+ children: groupIcon
990
+ }),
991
+ /* @__PURE__ */ jsx("span", {
992
+ className: "text-muted-foreground text-xs font-medium",
993
+ children: groupLabel
994
+ }),
995
+ /* @__PURE__ */ jsx("span", {
996
+ className: "text-muted-foreground/60 text-xs tabular-nums",
997
+ children: totalDocs
998
+ })
999
+ ]
1000
+ }), !isGroupCollapsed && (isGroupLoading ? /* @__PURE__ */ jsx("div", {
1001
+ className: "flex items-center justify-center py-4",
1002
+ children: /* @__PURE__ */ jsx(Icon, {
1003
+ icon: "ph:spinner-gap",
1004
+ className: "text-muted-foreground size-4 animate-spin"
1005
+ })
1006
+ }) : /* @__PURE__ */ jsxs("div", {
1007
+ className: "flex flex-col gap-px",
1008
+ children: [rows.map((outlineRow) => {
1009
+ if (outlineRow.kind !== "record") return /* @__PURE__ */ jsx(OutlineHeaderRow, {
1010
+ row: outlineRow,
1011
+ showCounts: resolvedListConfig?.outline?.showCounts ?? true,
1012
+ onToggle: toggleOutlineKey
1013
+ }, outlineRow.key);
1014
+ return renderRecordRow(outlineRow, {
1015
+ field: firstFieldLevel.field,
1016
+ value: gv.value
1017
+ });
1018
+ }), totalDocs > loadedCount && /* @__PURE__ */ jsxs("button", {
1019
+ type: "button",
1020
+ className: "text-muted-foreground hover:text-foreground hover:bg-surface-mid focus-visible:ring-ring/40 flex w-full items-center justify-center gap-2 rounded-md py-2 text-xs transition-colors focus-visible:ring-2 focus-visible:outline-none",
1021
+ onClick: () => loadMoreInGroup(groupKey),
1022
+ children: [/* @__PURE__ */ jsx(Icon, {
1023
+ icon: "ph:caret-down",
1024
+ className: "size-3"
1025
+ }), /* @__PURE__ */ jsxs("span", { children: [
1026
+ t("table.loadMore"),
1027
+ " (",
1028
+ totalDocs - loadedCount,
1029
+ ")"
1030
+ ] })]
1031
+ })]
1032
+ }))]
1033
+ }, groupKey);
1034
+ })
1035
+ });
1036
+ return /* @__PURE__ */ jsx("div", {
1037
+ className: "flex flex-col gap-px overflow-hidden",
1038
+ children: outlineRows.map((outlineRow) => {
1039
+ if (outlineRow.kind !== "record") return /* @__PURE__ */ jsx(OutlineHeaderRow, {
1040
+ row: outlineRow,
1041
+ showCounts: resolvedListConfig?.outline?.showCounts ?? true,
1042
+ onToggle: toggleOutlineKey
1043
+ }, outlineRow.key);
1044
+ return renderRecordRow(outlineRow);
1045
+ })
1046
+ });
1047
+ })(),
1048
+ !isSearching && (hasOutline || isGrouped) && /* @__PURE__ */ jsx("div", {
1049
+ className: "text-muted-foreground flex items-center gap-2 py-2 text-sm tabular-nums",
1050
+ "aria-live": "polite",
1051
+ "aria-atomic": "true",
1052
+ children: /* @__PURE__ */ jsxs("span", { children: [
1053
+ isGrouped ? groupQueries.reduce((sum, q) => sum + (q.data?.totalDocs ?? 0), 0) : items.length,
1054
+ " ",
1055
+ t("table.items")
1056
+ ] })
736
1057
  }),
737
- /* @__PURE__ */ jsxs("div", {
738
- className: "text-muted-foreground flex flex-col gap-2 text-xs sm:flex-row sm:items-center sm:justify-between",
739
- children: [/* @__PURE__ */ jsx("span", { children: isSearching ? `${items.length} item${items.length === 1 ? "" : "s"}` : `${items.length > 0 ? ((viewState.config.pagination?.page ?? 1) - 1) * (viewState.config.pagination?.pageSize ?? 25) + 1 : 0}-${Math.min(((viewState.config.pagination?.page ?? 1) - 1) * (viewState.config.pagination?.pageSize ?? 25) + items.length, listData?.totalDocs ?? items.length)} ${t("table.of")} ${listData?.totalDocs ?? 0}` }), !isSearching && (listData?.totalPages ?? 1) > 1 && /* @__PURE__ */ jsxs("div", {
740
- className: "flex items-center gap-2",
741
- children: [/* @__PURE__ */ jsx(Button, {
742
- variant: "outline",
743
- size: "sm",
744
- disabled: (viewState.config.pagination?.page ?? 1) <= 1,
745
- onClick: () => viewState.setPage(Math.max(1, (viewState.config.pagination?.page ?? 1) - 1)),
746
- children: t("common.previous")
747
- }), /* @__PURE__ */ jsx(Button, {
748
- variant: "outline",
749
- size: "sm",
750
- disabled: (viewState.config.pagination?.page ?? 1) >= (listData?.totalPages ?? 1),
751
- onClick: () => viewState.setPage((viewState.config.pagination?.page ?? 1) + 1),
752
- children: t("common.next")
1058
+ !isSearching && !hasOutline && !isGrouped && /* @__PURE__ */ jsxs("div", {
1059
+ className: "qa-list-view__pagination flex items-center justify-between gap-4 py-2 tabular-nums",
1060
+ role: "navigation",
1061
+ "aria-label": t("table.pagination"),
1062
+ children: [/* @__PURE__ */ jsxs("div", {
1063
+ className: "text-muted-foreground flex items-center gap-4 text-sm",
1064
+ "aria-live": "polite",
1065
+ "aria-atomic": "true",
1066
+ children: [/* @__PURE__ */ jsxs("span", { children: [
1067
+ items.length > 0 ? `${((viewState.config.pagination?.page ?? 1) - 1) * (viewState.config.pagination?.pageSize ?? 25) + 1}-${Math.min(((viewState.config.pagination?.page ?? 1) - 1) * (viewState.config.pagination?.pageSize ?? 25) + items.length, listData?.totalDocs ?? items.length)}` : "0",
1068
+ " ",
1069
+ t("table.of"),
1070
+ " ",
1071
+ listData?.totalDocs ?? 0
1072
+ ] }), /* @__PURE__ */ jsxs("div", {
1073
+ className: "flex items-center gap-2",
1074
+ children: [/* @__PURE__ */ jsx("span", { children: t("table.show") }), /* @__PURE__ */ jsxs(Select, {
1075
+ value: String(viewState.config.pagination?.pageSize ?? 25),
1076
+ onValueChange: (value) => viewState.setPageSize(Number(value)),
1077
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
1078
+ className: "h-8 w-[70px]",
1079
+ children: /* @__PURE__ */ jsx(SelectValue, {})
1080
+ }), /* @__PURE__ */ jsx(SelectContent, {
1081
+ side: "top",
1082
+ children: [
1083
+ 10,
1084
+ 25,
1085
+ 50,
1086
+ 100
1087
+ ].map((size) => /* @__PURE__ */ jsx(SelectItem, {
1088
+ value: String(size),
1089
+ children: size
1090
+ }, size))
1091
+ })]
1092
+ })]
753
1093
  })]
1094
+ }), /* @__PURE__ */ jsxs("div", {
1095
+ className: "flex items-center gap-1",
1096
+ children: [
1097
+ /* @__PURE__ */ jsx(Button, {
1098
+ variant: "ghost",
1099
+ size: "sm",
1100
+ className: "size-8 p-0",
1101
+ disabled: (viewState.config.pagination?.page ?? 1) <= 1,
1102
+ onClick: () => viewState.setPage((viewState.config.pagination?.page ?? 1) - 1),
1103
+ "aria-label": t("table.previousPage"),
1104
+ children: /* @__PURE__ */ jsx(Icon, {
1105
+ icon: "ph:caret-left",
1106
+ className: "size-4"
1107
+ })
1108
+ }),
1109
+ Array.from({ length: Math.min(5, listData?.totalPages ?? 1) }, (_, i) => {
1110
+ const currentPage = viewState.config.pagination?.page ?? 1;
1111
+ const totalPages = listData?.totalPages ?? 1;
1112
+ let pageNum;
1113
+ if (totalPages <= 5) pageNum = i + 1;
1114
+ else if (currentPage <= 3) pageNum = i + 1;
1115
+ else if (currentPage >= totalPages - 2) pageNum = totalPages - 4 + i;
1116
+ else pageNum = currentPage - 2 + i;
1117
+ return /* @__PURE__ */ jsx(Button, {
1118
+ variant: currentPage === pageNum ? "secondary" : "ghost",
1119
+ size: "sm",
1120
+ className: "size-8 min-w-8 p-0 tabular-nums",
1121
+ onClick: () => viewState.setPage(pageNum),
1122
+ "aria-label": t("table.page", { page: pageNum }),
1123
+ "aria-current": currentPage === pageNum ? "page" : void 0,
1124
+ children: pageNum
1125
+ }, pageNum);
1126
+ }),
1127
+ /* @__PURE__ */ jsx(Button, {
1128
+ variant: "ghost",
1129
+ size: "sm",
1130
+ className: "size-8 p-0",
1131
+ disabled: (viewState.config.pagination?.page ?? 1) >= (listData?.totalPages ?? 1),
1132
+ onClick: () => viewState.setPage((viewState.config.pagination?.page ?? 1) + 1),
1133
+ "aria-label": t("table.nextPage"),
1134
+ children: /* @__PURE__ */ jsx(Icon, {
1135
+ icon: "ph:caret-right",
1136
+ className: "size-4"
1137
+ })
1138
+ })
1139
+ ]
754
1140
  })]
755
1141
  }),
1142
+ isSearching && /* @__PURE__ */ jsxs("div", {
1143
+ className: "text-muted-foreground flex items-center gap-2 py-2 text-sm tabular-nums",
1144
+ "aria-live": "polite",
1145
+ "aria-atomic": "true",
1146
+ children: [isSearchActive && /* @__PURE__ */ jsx(Icon, {
1147
+ icon: "ph:spinner-gap",
1148
+ className: "size-3 animate-spin"
1149
+ }), t("cell.item", { count: items.length })]
1150
+ }),
756
1151
  /* @__PURE__ */ jsx(BulkActionToolbar, {
757
1152
  table,
758
1153
  actions: actions.bulk,
@@ -781,7 +1176,9 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
781
1176
  onDeleteView: (viewId) => deleteViewMutation.mutate(viewId),
782
1177
  supportsSoftDelete: !!collectionMeta?.softDelete,
783
1178
  groupableFields,
784
- defaultGroupBy
1179
+ defaultGroupBy,
1180
+ defaultFilters,
1181
+ panels: { columns: false }
785
1182
  }),
786
1183
  dialogAction && /* @__PURE__ */ jsx(ActionDialog, {
787
1184
  open: !!dialogAction,
@@ -796,10 +1193,10 @@ function ListViewInner({ collection, config, viewConfig, navigate, basePath = "/
796
1193
  });
797
1194
  }
798
1195
  function OutlineHeaderRow({ row, showCounts, onToggle }) {
1196
+ const groupIcon = row.kind === "group" && "icon" in row ? resolveIconElement(row.icon) : null;
799
1197
  return /* @__PURE__ */ jsxs("button", {
800
1198
  type: "button",
801
- className: "bg-background/95 border-border-subtle sticky top-0 z-10 flex w-full items-center gap-2 border-b px-3 py-2 text-left backdrop-blur",
802
- style: { paddingLeft: `${12 + row.depth * 18}px` },
1199
+ className: "hover:bg-surface-mid focus-visible:ring-ring/40 flex min-h-8 w-full items-center gap-2 rounded-md px-3 py-1 text-left transition-[background-color,color,box-shadow] focus-visible:ring-2 focus-visible:outline-none",
803
1200
  onClick: () => onToggle(row.key),
804
1201
  "aria-expanded": !row.collapsed,
805
1202
  children: [
@@ -811,12 +1208,17 @@ function OutlineHeaderRow({ row, showCounts, onToggle }) {
811
1208
  icon: "ph:folder-simple",
812
1209
  className: "text-muted-foreground size-4"
813
1210
  }),
1211
+ groupIcon && /* @__PURE__ */ jsx("span", {
1212
+ className: "size-4 shrink-0",
1213
+ children: groupIcon
1214
+ }),
814
1215
  /* @__PURE__ */ jsx("span", {
815
- className: "font-chrome chrome-meta text-muted-foreground truncate text-[11px] font-semibold tracking-[0.12em] uppercase",
1216
+ className: "text-muted-foreground truncate text-xs font-medium",
1217
+ style: row.depth > 0 ? { paddingLeft: `${row.depth * 18}px` } : void 0,
816
1218
  children: row.label
817
1219
  }),
818
1220
  showCounts && /* @__PURE__ */ jsx("span", {
819
- className: "font-chrome chrome-meta text-muted-foreground text-[11px] tabular-nums",
1221
+ className: "text-muted-foreground/60 ml-1 text-xs tabular-nums",
820
1222
  children: row.count
821
1223
  })
822
1224
  ]