@questpie/admin 3.5.3 → 3.5.5

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 (141) 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 +80 -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/admin-link.d.mts +2 -2
  9. package/dist/client/components/fields/boolean-field.mjs +2 -1
  10. package/dist/client/components/fields/date-field.mjs +2 -1
  11. package/dist/client/components/fields/datetime-field.mjs +2 -1
  12. package/dist/client/components/fields/email-field.mjs +2 -1
  13. package/dist/client/components/fields/field-utils.d.mts +11 -0
  14. package/dist/client/components/fields/field-utils.mjs +3 -1
  15. package/dist/client/components/fields/field-wrapper.mjs +3 -3
  16. package/dist/client/components/fields/number-field.mjs +2 -1
  17. package/dist/client/components/fields/object-field.mjs +2 -1
  18. package/dist/client/components/fields/relation/displays/types.mjs +3 -3
  19. package/dist/client/components/fields/rich-text-editor/extensions.mjs +2 -1
  20. package/dist/client/components/fields/rich-text-editor/image-popover.mjs +6 -2
  21. package/dist/client/components/fields/rich-text-editor/image-upload.mjs +2 -1
  22. package/dist/client/components/fields/rich-text-editor/index.d.mts +3 -2
  23. package/dist/client/components/fields/rich-text-editor/index.mjs +4 -3
  24. package/dist/client/components/fields/select-field.mjs +2 -1
  25. package/dist/client/components/fields/text-field.mjs +2 -1
  26. package/dist/client/components/fields/textarea-field.mjs +2 -1
  27. package/dist/client/components/fields/time-field.mjs +2 -1
  28. package/dist/client/components/layout/field-layout-renderer.mjs +4 -4
  29. package/dist/client/components/media/media-grid.mjs +2 -1
  30. package/dist/client/components/primitives/asset-preview.mjs +4 -2
  31. package/dist/client/components/primitives/dropzone.d.mts +100 -0
  32. package/dist/client/components/primitives/field-select-control.mjs +2 -1
  33. package/dist/client/components/ui/button.d.mts +23 -0
  34. package/dist/client/components/ui/button.mjs +2 -2
  35. package/dist/client/components/ui/dropdown-menu.d.mts +49 -0
  36. package/dist/client/components/ui/dropdown-menu.mjs +22 -1
  37. package/dist/client/components/ui/popover.mjs +1 -1
  38. package/dist/client/components/ui/search-input.d.mts +56 -0
  39. package/dist/client/components/ui/select.mjs +2 -2
  40. package/dist/client/components/ui/sheet.d.mts +40 -0
  41. package/dist/client/components/ui/table.d.mts +49 -0
  42. package/dist/client/components/ui/table.mjs +15 -1
  43. package/dist/client/components/ui/tooltip.d.mts +21 -0
  44. package/dist/client/contexts/focus-context.d.mts +2 -2
  45. package/dist/client/hooks/use-admin-config.mjs +20 -1
  46. package/dist/client/hooks/use-autosave.mjs +91 -0
  47. package/dist/client/hooks/use-collection.mjs +65 -23
  48. package/dist/client/hooks/use-upload.d.mts +40 -0
  49. package/dist/client/hooks/use-upload.mjs +4 -2
  50. package/dist/client/i18n/hooks.d.mts +20 -0
  51. package/dist/client/lib/utils.d.mts +6 -0
  52. package/dist/client/preview/block-scope-context.d.mts +2 -2
  53. package/dist/client/preview/preview-banner.d.mts +2 -2
  54. package/dist/client/preview/preview-field.d.mts +4 -4
  55. package/dist/client/runtime/provider.mjs +22 -3
  56. package/dist/client/scope/picker.d.mts +2 -2
  57. package/dist/client/scope/provider.d.mts +2 -2
  58. package/dist/client/styles/base.css +22 -18
  59. package/dist/client/utils/asset-url.mjs +27 -0
  60. package/dist/client/views/auth/accept-invite-form.d.mts +2 -2
  61. package/dist/client/views/auth/auth-layout.d.mts +3 -3
  62. package/dist/client/views/auth/forgot-password-form.d.mts +2 -2
  63. package/dist/client/views/auth/login-form.d.mts +2 -2
  64. package/dist/client/views/auth/reset-password-form.d.mts +2 -2
  65. package/dist/client/views/auth/setup-form.d.mts +2 -2
  66. package/dist/client/views/collection/auto-form-fields.mjs +4 -4
  67. package/dist/client/views/collection/cells/shared/asset-thumbnail.d.mts +7 -0
  68. package/dist/client/views/collection/cells/shared/asset-thumbnail.mjs +3 -2
  69. package/dist/client/views/collection/cells/shared/cell-helpers.mjs +3 -2
  70. package/dist/client/views/collection/cells/upload-cells.mjs +2 -1
  71. package/dist/client/views/collection/document-view.d.mts +30 -0
  72. package/dist/client/views/collection/document-view.mjs +377 -0
  73. package/dist/client/views/collection/field-context.mjs +3 -2
  74. package/dist/client/views/collection/field-renderer.mjs +2 -2
  75. package/dist/client/views/collection/form-view.mjs +14 -80
  76. package/dist/client/views/collection/list-view.mjs +19 -15
  77. package/dist/client/views/collection/table-view.mjs +1 -1
  78. package/dist/client/views/layout/admin-layout-provider.mjs +4 -3
  79. package/dist/client/views/layout/admin-layout.mjs +107 -20
  80. package/dist/client/views/layout/admin-router.mjs +19 -3
  81. package/dist/client/views/layout/admin-sidebar.mjs +50 -6
  82. package/dist/client/views/layout/admin-view-layout.d.mts +36 -0
  83. package/dist/client/views/pages/accept-invite-page.d.mts +2 -2
  84. package/dist/client/views/pages/dashboard-page.d.mts +2 -2
  85. package/dist/client/views/pages/forgot-password-page.d.mts +2 -2
  86. package/dist/client/views/pages/invite-page.d.mts +2 -2
  87. package/dist/client/views/pages/login-page.d.mts +2 -2
  88. package/dist/client/views/pages/reset-password-page.d.mts +2 -2
  89. package/dist/client/views/pages/setup-page.d.mts +2 -2
  90. package/dist/client.d.mts +17 -2
  91. package/dist/client.mjs +16 -1
  92. package/dist/components/rich-text/rich-text-renderer.d.mts +2 -2
  93. package/dist/factories.d.mts +2 -2
  94. package/dist/factories.mjs +2 -2
  95. package/dist/index.d.mts +17 -3
  96. package/dist/index.mjs +16 -1
  97. package/dist/server/augmentation/actions.d.mts +5 -0
  98. package/dist/server/augmentation/form-layout.d.mts +5 -0
  99. package/dist/server/augmentation/views.d.mts +4 -1
  100. package/dist/server/fields/blocks.mjs +4 -1
  101. package/dist/server/fields/reactive-runtime.mjs +3 -0
  102. package/dist/server/modules/admin/.generated/module.d.mts +1 -1
  103. package/dist/server/modules/admin/auth-helpers.mjs +7 -1
  104. package/dist/server/modules/admin/block/introspection.mjs +28 -4
  105. package/dist/server/modules/admin/block/prefetch.d.mts +11 -0
  106. package/dist/server/modules/admin/block/prefetch.mjs +108 -27
  107. package/dist/server/modules/admin/client/.generated/module.d.mts +68 -67
  108. package/dist/server/modules/admin/client/.generated/module.mjs +2 -0
  109. package/dist/server/modules/admin/client/views/collection-document.d.mts +6 -0
  110. package/dist/server/modules/admin/client/views/collection-document.mjs +10 -0
  111. package/dist/server/modules/admin/collections/account.d.mts +46 -46
  112. package/dist/server/modules/admin/collections/admin-locks.d.mts +57 -57
  113. package/dist/server/modules/admin/collections/admin-preferences.d.mts +42 -42
  114. package/dist/server/modules/admin/collections/admin-saved-views.d.mts +50 -50
  115. package/dist/server/modules/admin/collections/apikey.d.mts +79 -71
  116. package/dist/server/modules/admin/collections/assets.d.mts +42 -42
  117. package/dist/server/modules/admin/collections/session.d.mts +45 -45
  118. package/dist/server/modules/admin/collections/user.d.mts +66 -66
  119. package/dist/server/modules/admin/collections/verification.d.mts +39 -39
  120. package/dist/server/modules/admin/dto/admin-config.dto.mjs +34 -4
  121. package/dist/server/modules/admin/factories.mjs +4 -34
  122. package/dist/server/modules/admin/routes/admin-config.d.mts +3 -2
  123. package/dist/server/modules/admin/routes/admin-config.mjs +18 -2
  124. package/dist/server/modules/admin/routes/execute-action.d.mts +9 -9
  125. package/dist/server/modules/admin/routes/execute-action.mjs +10 -4
  126. package/dist/server/modules/admin/routes/locales.d.mts +2 -2
  127. package/dist/server/modules/admin/routes/locales.mjs +1 -1
  128. package/dist/server/modules/admin/routes/preview.d.mts +11 -11
  129. package/dist/server/modules/admin/routes/preview.mjs +6 -5
  130. package/dist/server/modules/admin/routes/reactive.d.mts +9 -9
  131. package/dist/server/modules/admin/routes/reactive.mjs +2 -2
  132. package/dist/server/modules/admin/routes/route-helpers.mjs +1 -1
  133. package/dist/server/modules/admin/routes/setup.d.mts +7 -7
  134. package/dist/server/modules/admin/routes/translations.d.mts +4 -4
  135. package/dist/server/modules/admin/routes/widget-data.d.mts +5 -5
  136. package/dist/server/modules/admin/routes/widget-data.mjs +1 -1
  137. package/dist/server/modules/admin-preferences/collections/saved-views.d.mts +27 -27
  138. package/dist/server/plugin.mjs +8 -3
  139. package/dist/server/proxy-factories.d.mts +8 -1
  140. package/dist/server/proxy-factories.mjs +33 -1
  141. package/package.json +4 -4
package/README.md CHANGED
@@ -65,6 +65,14 @@ export default adminConfig({
65
65
 
66
66
  Collections, globals, routes, and jobs are auto-discovered via file convention. Codegen produces a `.generated/index.ts` with the fully-typed `App` and runtime `app` instance.
67
67
 
68
+ ### Admin Auth Contract
69
+
70
+ `@questpie/admin` expects the app to use `adminModule`'s starter Better Auth model, or an equivalent auth setup that exposes `session.user.role` on the active session.
71
+
72
+ Access to the admin panel and admin RPC routes is granted only when `session.user.role === "admin"`. This includes admin config, content locale callbacks, preview URL/token generation, actions, widgets, and reactive field handlers. Authenticated users with `role: "user"` or a missing role are not admin users. The built-in setup route uses the same contract when checking for and creating the first admin user with `role = "admin"`.
73
+
74
+ If you extend the Better Auth `user` collection, merge the starter user collection instead of replacing it from scratch. Custom user collections must preserve the role field and session role propagation used by the admin guard.
75
+
68
76
  ### Collection Admin Config
69
77
 
70
78
  Admin metadata, list views, and form views are defined on the collection itself:
@@ -1,6 +1,6 @@
1
1
  import { BlockContent } from "./types.mjs";
2
2
  import * as React from "react";
3
- import * as react_jsx_runtime19 from "react/jsx-runtime";
3
+ import * as react_jsx_runtime53 from "react/jsx-runtime";
4
4
 
5
5
  //#region src/client/blocks/block-renderer.d.ts
6
6
 
@@ -50,6 +50,6 @@ declare function BlockRenderer({
50
50
  onBlockClick,
51
51
  onBlockInsert,
52
52
  className
53
- }: BlockRendererProps): react_jsx_runtime19.JSX.Element | null;
53
+ }: BlockRendererProps): react_jsx_runtime53.JSX.Element | null;
54
54
  //#endregion
55
55
  export { BlockRenderer, BlockRendererProps };
@@ -4,7 +4,7 @@ import { AnyWidgetConfig, WidgetAction, WidgetCardVariant, WidgetComponentProps,
4
4
  import { DashboardAction, DashboardConfig, DashboardLayoutItem, DashboardSection, DashboardTabConfig, DashboardTabs } from "./types/ui-config.mjs";
5
5
  import { AdminState } from "./admin-types.mjs";
6
6
  import { Admin, AppAdmin, InferAdminCMS } from "./admin.mjs";
7
- import { ComponentRegistry, FieldComponentProps, FieldLayoutItem, FormSidebarConfig, FormViewConfig, SectionLayout, TabConfig, TabsLayout } from "./types/field-types.mjs";
7
+ import { ComponentRegistry, DocumentViewConfig, FieldComponentProps, FieldLayoutItem, FormSidebarConfig, FormViewConfig, SectionLayout, TabConfig, TabsLayout } from "./types/field-types.mjs";
8
8
  import { QuestpieApp, QuestpieClient } from "questpie/client";
9
9
  import { CollectionInfer } from "questpie";
10
10
 
@@ -1,9 +1,10 @@
1
- import "../../i18n/types.mjs";
2
- import { MaybeLazyComponent } from "./common.mjs";
1
+ import { I18nText } from "../../i18n/types.mjs";
2
+ import { IconComponent, MaybeLazyComponent } from "./common.mjs";
3
+ import { ComponentReference } from "../../../server/augmentation/common.mjs";
3
4
  import { FilterRule, QuickFilterConfig } from "../../../shared/types/saved-views.types.mjs";
4
5
  import "../../../server/augmentation.mjs";
5
- import "../field/field.mjs";
6
- import "../admin.mjs";
6
+ import { FieldDefinition } from "../field/field.mjs";
7
+ import { Admin } from "../admin.mjs";
7
8
  import { ActionsConfig } from "./action-types.mjs";
8
9
 
9
10
  //#region src/client/builder/types/collection-types.d.ts
@@ -202,5 +203,79 @@ interface ListViewConfig<TFieldNames extends string = string> {
202
203
  */
203
204
  actions?: ActionsConfig;
204
205
  }
206
+ /**
207
+ * Preview configuration for live preview in form view
208
+ */
209
+ interface PreviewConfig {
210
+ /**
211
+ * URL builder function that returns preview URL for current form values
212
+ * @param values - Current form values
213
+ * @param locale - Current content locale
214
+ * @returns Preview URL string
215
+ */
216
+ url: (values: Record<string, unknown>, locale: string) => string;
217
+ /**
218
+ * Whether preview is enabled (default: true)
219
+ */
220
+ enabled?: boolean;
221
+ /**
222
+ * Position of the preview panel (default: "right")
223
+ */
224
+ position?: "right" | "bottom";
225
+ /**
226
+ * Default width/height percentage of preview panel (default: 50)
227
+ */
228
+ defaultWidth?: number;
229
+ /**
230
+ * Minimum width/height percentage (default: 30)
231
+ */
232
+ minWidth?: number;
233
+ /**
234
+ * Maximum width/height percentage (default: 70)
235
+ */
236
+ maxWidth?: number;
237
+ }
238
+ /**
239
+ * Autosave configuration for form view
240
+ */
241
+ interface AutoSaveConfig {
242
+ /**
243
+ * Whether autosave is enabled
244
+ * @default false
245
+ */
246
+ enabled?: boolean;
247
+ /**
248
+ * Debounce delay in milliseconds before autosave triggers
249
+ * @default 500 (0.5s as specified)
250
+ */
251
+ debounce?: number;
252
+ /**
253
+ * Show autosave status indicator in form header
254
+ * @default true
255
+ */
256
+ indicator?: boolean;
257
+ /**
258
+ * Warn user before navigating away with unsaved changes
259
+ * @default true
260
+ */
261
+ preventNavigation?: boolean;
262
+ }
263
+ /**
264
+ * Collection builder state - internal state during building
265
+ */
266
+ interface CollectionBuilderState<TAdminApp extends Admin<any> = Admin<any>> {
267
+ readonly name: string;
268
+ readonly "~adminApp": TAdminApp;
269
+ /** Display label - supports inline translations */
270
+ readonly label?: I18nText;
271
+ /** Description - supports inline translations */
272
+ readonly description?: I18nText;
273
+ readonly icon?: IconComponent | ComponentReference;
274
+ readonly fields?: Record<string, FieldDefinition>;
275
+ readonly list?: any;
276
+ readonly form?: any;
277
+ readonly preview?: PreviewConfig;
278
+ readonly autoSave?: AutoSaveConfig;
279
+ }
205
280
  //#endregion
206
- export { ListViewConfig };
281
+ export { CollectionBuilderState, ListViewConfig };
@@ -39,6 +39,11 @@ type BaseFieldProps = {
39
39
  required?: boolean;
40
40
  localized?: boolean;
41
41
  locale?: string;
42
+ /**
43
+ * Render the control WITHOUT its own label/description. Used by compact
44
+ * layouts (e.g. Notion-style property rows) that supply the label themselves.
45
+ */
46
+ hideLabel?: boolean;
42
47
  control?: any;
43
48
  className?: string;
44
49
  };
@@ -578,6 +578,46 @@ interface FormViewActionsConfig<TItem = any> {
578
578
  /** Actions shown in dropdown menu (...) */
579
579
  secondary?: ActionDefinition<TItem>[];
580
580
  }
581
+ /**
582
+ * Document view configuration — a Notion-style page with a dominant rich-text
583
+ * body and inline-editable property rows.
584
+ *
585
+ * No hardcoded field names: the consumer declares which field is the body, the
586
+ * (optional) title field, and which fields appear as properties. Defaults are
587
+ * data-driven (`properties: "auto"` derives from the schema).
588
+ *
589
+ * @example
590
+ * ```ts
591
+ * .form(({ v, f }) => v.collectionDocument({
592
+ * document: {
593
+ * body: f.body,
594
+ * title: f.name,
595
+ * properties: [f.scopeType, f.path, f.createdAt],
596
+ * save: "autosave",
597
+ * },
598
+ * }))
599
+ * ```
600
+ */
601
+ interface DocumentViewConfig {
602
+ document: {
603
+ /** The dominant long-form field, rendered with the rich-text editor. */
604
+ body: string;
605
+ /**
606
+ * Property fields shown as inline-editable rows.
607
+ * - `"auto"` (default): all schema fields except body/title, in schema order.
608
+ * - `string[]`: an explicit, ordered list.
609
+ */
610
+ properties?: "auto" | string[];
611
+ /**
612
+ * Save behavior.
613
+ * - `"autosave"` (default): debounced autosave with a "Saved"/"Saving" indicator.
614
+ * - `"manual"`: a Save affordance with dirty tracking.
615
+ */
616
+ save?: "autosave" | "manual";
617
+ /** Optional page-title field; falls back to the record id/path. */
618
+ title?: string;
619
+ };
620
+ }
581
621
  /**
582
622
  * Registry for custom field and widget components
583
623
  */
@@ -596,4 +636,4 @@ interface ComponentRegistry {
596
636
  custom?: Record<string, MaybeLazyComponent<any>>;
597
637
  }
598
638
  //#endregion
599
- export { ComponentRegistry, FieldComponentProps, FieldLayoutItem, FormSidebarConfig, FormViewConfig, SectionLayout, TabConfig, TabsLayout };
639
+ export { ComponentRegistry, DocumentViewConfig, FieldComponentProps, FieldLayoutItem, FormSidebarConfig, FormViewConfig, SectionLayout, TabConfig, TabsLayout };
@@ -3,9 +3,10 @@ import { MaybeLazyComponent } from "../types/common.mjs";
3
3
  //#region src/client/builder/view/view.d.ts
4
4
 
5
5
  /**
6
- * View kind discriminant — "list" for collection list pages, "form" for edit/create pages.
6
+ * View kind discriminant — "list" for collection list pages, "form" for edit/create pages,
7
+ * "document" for Notion-style document pages (dominant rich-text body + property rows).
7
8
  */
8
- type ViewKind = "list" | "form";
9
+ type ViewKind = "list" | "form" | "document";
9
10
  /**
10
11
  * View definition — a registry entry mapping a view name to its component.
11
12
  *
@@ -1,6 +1,6 @@
1
1
  import { CollectionNames, GlobalNames } from "../builder/index.mjs";
2
2
  import * as React from "react";
3
- import * as react_jsx_runtime14 from "react/jsx-runtime";
3
+ import * as react_jsx_runtime48 from "react/jsx-runtime";
4
4
  import { QuestpieApp } from "questpie/client";
5
5
 
6
6
  //#region src/client/components/admin-link.d.ts
@@ -61,6 +61,6 @@ declare function AdminLink<TApp extends QuestpieApp>({
61
61
  children,
62
62
  onClick,
63
63
  ...rest
64
- }: AdminLinkProps<TApp>): react_jsx_runtime14.JSX.Element;
64
+ }: AdminLinkProps<TApp>): react_jsx_runtime48.JSX.Element;
65
65
  //#endregion
66
66
  export { AdminLink };
@@ -11,7 +11,7 @@ import { Controller } from "react-hook-form";
11
11
  * Unified boolean field component.
12
12
  * Renders as checkbox (default) or switch based on `displayAs` prop.
13
13
  */
14
- function BooleanField({ name, label, description, required, disabled, localized, locale, control, className, displayAs = "checkbox" }) {
14
+ function BooleanField({ name, label, description, required, disabled, localized, locale, hideLabel, control, className, displayAs = "checkbox" }) {
15
15
  return /* @__PURE__ */ jsx(Controller, {
16
16
  name,
17
17
  control: useResolvedControl(control),
@@ -23,6 +23,7 @@ function BooleanField({ name, label, description, required, disabled, localized,
23
23
  disabled,
24
24
  localized,
25
25
  locale,
26
+ hideLabel,
26
27
  error: fieldState.error?.message,
27
28
  children: displayAs === "switch" ? /* @__PURE__ */ jsx(ToggleInput, {
28
29
  id: name,
@@ -11,7 +11,7 @@ function parseDateFieldValue(value) {
11
11
  const date = value instanceof Date ? value : new Date(String(value));
12
12
  return Number.isNaN(date.getTime()) ? null : date;
13
13
  }
14
- function DateField({ name, label, description, placeholder, required, disabled, localized, locale, control, className, minDate, maxDate, format }) {
14
+ function DateField({ name, label, description, placeholder, required, disabled, localized, locale, hideLabel, control, className, minDate, maxDate, format }) {
15
15
  return /* @__PURE__ */ jsx(Controller, {
16
16
  name,
17
17
  control: useResolvedControl(control),
@@ -25,6 +25,7 @@ function DateField({ name, label, description, placeholder, required, disabled,
25
25
  disabled,
26
26
  localized,
27
27
  locale,
28
+ hideLabel,
28
29
  error: fieldState.error?.message,
29
30
  children: /* @__PURE__ */ jsx(DateInput, {
30
31
  id: name,
@@ -11,7 +11,7 @@ function parseDateTimeFieldValue(value) {
11
11
  const date = value instanceof Date ? value : new Date(String(value));
12
12
  return Number.isNaN(date.getTime()) ? null : date;
13
13
  }
14
- function DatetimeField({ name, label, description, placeholder, required, disabled, localized, locale, control, className, minDate, maxDate, format, precision }) {
14
+ function DatetimeField({ name, label, description, placeholder, required, disabled, localized, locale, hideLabel, control, className, minDate, maxDate, format, precision }) {
15
15
  return /* @__PURE__ */ jsx(Controller, {
16
16
  name,
17
17
  control: useResolvedControl(control),
@@ -25,6 +25,7 @@ function DatetimeField({ name, label, description, placeholder, required, disabl
25
25
  disabled,
26
26
  localized,
27
27
  locale,
28
+ hideLabel,
28
29
  error: fieldState.error?.message,
29
30
  children: /* @__PURE__ */ jsx(DateTimeInput, {
30
31
  id: name,
@@ -6,7 +6,7 @@ import { jsx } from "react/jsx-runtime";
6
6
  import { Controller } from "react-hook-form";
7
7
 
8
8
  //#region src/client/components/fields/email-field.tsx
9
- function EmailField({ name, label, description, placeholder, required, disabled, localized, locale, control, className }) {
9
+ function EmailField({ name, label, description, placeholder, required, disabled, localized, locale, hideLabel, control, className }) {
10
10
  return /* @__PURE__ */ jsx(Controller, {
11
11
  name,
12
12
  control: useResolvedControl(control),
@@ -18,6 +18,7 @@ function EmailField({ name, label, description, placeholder, required, disabled,
18
18
  disabled,
19
19
  localized,
20
20
  locale,
21
+ hideLabel,
21
22
  error: fieldState.error?.message,
22
23
  children: /* @__PURE__ */ jsx(TextInput, {
23
24
  id: name,
@@ -0,0 +1,11 @@
1
+ import "react-hook-form";
2
+
3
+ //#region src/client/components/fields/field-utils.d.ts
4
+
5
+ /**
6
+ * Sanitize filename for safe storage.
7
+ * Removes special characters, replaces spaces with hyphens.
8
+ */
9
+ declare function sanitizeFilename(filename: string): string;
10
+ //#endregion
11
+ export { sanitizeFilename };
@@ -12,7 +12,9 @@ function useResolvedControl(control) {
12
12
  function sanitizeFilename(filename) {
13
13
  const lastDot = filename.lastIndexOf(".");
14
14
  const ext = lastDot > 0 ? filename.slice(lastDot) : "";
15
- return ((lastDot > 0 ? filename.slice(0, lastDot) : filename).normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\s+/g, "-").replace(/[^a-zA-Z0-9._-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase() || "file") + ext.toLowerCase();
15
+ const sanitized = (lastDot > 0 ? filename.slice(0, lastDot) : filename).normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/\s+/g, "-").replace(/[^a-zA-Z0-9._-]/g, "").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
16
+ const safeExt = ext.toLowerCase().replace(/[^a-z0-9.]/g, "");
17
+ return (sanitized || "file") + safeExt;
16
18
  }
17
19
  /**
18
20
  * Extract columns from collection list config.
@@ -36,10 +36,10 @@ function FieldLocaleIndicator({ localized, locale }) {
36
36
  showFlag: false
37
37
  });
38
38
  }
39
- function FieldWrapper({ name, label, description, labelAccessory, required, disabled, readOnly, error, localized, locale, children, fieldPath }) {
39
+ function FieldWrapper({ name, label, description, labelAccessory, required, disabled, readOnly, error, localized, locale, hideLabel, children, fieldPath }) {
40
40
  const resolveText = useResolveText();
41
- const resolvedLabel = label ? resolveText(label) : void 0;
42
- const resolvedDescription = description ? resolveText(description) : void 0;
41
+ const resolvedLabel = hideLabel || !label ? void 0 : resolveText(label);
42
+ const resolvedDescription = hideLabel || !description ? void 0 : resolveText(description);
43
43
  return /* @__PURE__ */ jsx(Field, {
44
44
  "data-disabled": disabled,
45
45
  "data-readonly": !disabled && readOnly,
@@ -6,7 +6,7 @@ import { jsx } from "react/jsx-runtime";
6
6
  import { Controller } from "react-hook-form";
7
7
 
8
8
  //#region src/client/components/fields/number-field.tsx
9
- function NumberField({ name, label, description, placeholder, required, disabled, localized, locale, control, className, min, max, step, showButtons }) {
9
+ function NumberField({ name, label, description, placeholder, required, disabled, localized, locale, hideLabel, control, className, min, max, step, showButtons }) {
10
10
  return /* @__PURE__ */ jsx(Controller, {
11
11
  name,
12
12
  control: useResolvedControl(control),
@@ -18,6 +18,7 @@ function NumberField({ name, label, description, placeholder, required, disabled
18
18
  disabled,
19
19
  localized,
20
20
  locale,
21
+ hideLabel,
21
22
  error: fieldState.error?.message,
22
23
  children: /* @__PURE__ */ jsx(NumberInput, {
23
24
  id: name,
@@ -26,7 +26,8 @@ function ObjectFieldPanel({ name, label, description, required, disabled, locali
26
26
  type: "button",
27
27
  variant: "ghost",
28
28
  onClick: onToggle,
29
- className: "h-auto min-h-10 w-full justify-between rounded-none px-3 py-2 text-left",
29
+ className: "hover:bg-surface-low aria-expanded:bg-surface-low h-auto min-h-12 w-full justify-between rounded-none px-4 py-3 text-left",
30
+ "aria-expanded": !isCollapsed,
30
31
  disabled,
31
32
  children: [/* @__PURE__ */ jsxs("span", {
32
33
  className: "flex min-w-0 items-center gap-2",
@@ -1,4 +1,5 @@
1
1
  import { formatLabel } from "../../../../lib/utils.mjs";
2
+ import { resolveAssetUrl } from "../../../../utils/asset-url.mjs";
2
3
  import { DefaultCell } from "../../../../views/collection/cells/primitive-cells.mjs";
3
4
 
4
5
  //#region src/client/components/fields/relation/displays/types.ts
@@ -31,9 +32,8 @@ function formatCellValue(value) {
31
32
  function getImageUrl(item, imageField) {
32
33
  if (!imageField) return null;
33
34
  const imageValue = item[imageField];
34
- if (typeof imageValue === "string") return imageValue;
35
- if (imageValue?.url) return imageValue.url;
36
- if (imageValue?.key) return imageValue.key;
35
+ if (typeof imageValue === "string") return resolveAssetUrl(imageValue) ?? null;
36
+ if (imageValue?.url) return resolveAssetUrl(imageValue.url) ?? null;
37
37
  return null;
38
38
  }
39
39
  /**
@@ -55,12 +55,13 @@ const defaultLabels = {
55
55
  * (i.e. codeBlock disabled, or lowlight already cached). Only returns a
56
56
  * `Promise` on the first build that includes codeBlock.
57
57
  */
58
- function buildExtensions({ features, labels, placeholder, maxCharacters, customExtensions }) {
58
+ function buildExtensions({ features, labels, placeholder, maxCharacters, outputMode, customExtensions }) {
59
59
  const base = buildBaseExtensions({
60
60
  features,
61
61
  labels,
62
62
  placeholder,
63
63
  maxCharacters,
64
+ outputMode,
64
65
  customExtensions
65
66
  });
66
67
  if (!features.codeBlock) return base;
@@ -1,6 +1,7 @@
1
1
  import { useTranslation } from "../../../i18n/hooks.mjs";
2
2
  import { Button } from "../../ui/button.mjs";
3
3
  import { Input } from "../../ui/input.mjs";
4
+ import { resolveAssetUrl } from "../../../utils/asset-url.mjs";
4
5
  import { Popover, PopoverContent, PopoverHeader, PopoverTitle, PopoverTrigger } from "../../ui/popover.mjs";
5
6
  import { MediaPickerDialog } from "../../media/media-picker-dialog.mjs";
6
7
  import { useRichTextImageUpload } from "./image-upload.mjs";
@@ -76,7 +77,7 @@ function ImagePopover({ editor, open, onOpenChange, disabled, onImageUpload, ima
76
77
  return;
77
78
  }
78
79
  editor.chain().focus().setImage({
79
- src: asset.url,
80
+ src: resolveAssetUrl(asset.url) ?? asset.url,
80
81
  alt: imageAlt || asset.alt || void 0
81
82
  }).run();
82
83
  setImageUrl("");
@@ -95,7 +96,10 @@ function ImagePopover({ editor, open, onOpenChange, disabled, onImageUpload, ima
95
96
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Popover, {
96
97
  open,
97
98
  onOpenChange,
98
- children: [/* @__PURE__ */ jsx(PopoverTrigger, { render: /* @__PURE__ */ jsx("div", { className: "sr-only" }) }), /* @__PURE__ */ jsxs(PopoverContent, {
99
+ children: [/* @__PURE__ */ jsx(PopoverTrigger, {
100
+ nativeButton: false,
101
+ render: /* @__PURE__ */ jsx("div", { className: "sr-only" })
102
+ }), /* @__PURE__ */ jsxs(PopoverContent, {
99
103
  className: "w-80",
100
104
  children: [/* @__PURE__ */ jsx(PopoverHeader, { children: /* @__PURE__ */ jsx(PopoverTitle, { children: t("editor.image") }) }), /* @__PURE__ */ jsxs("div", {
101
105
  className: "space-y-3",
@@ -1,5 +1,6 @@
1
1
  import { useTranslation } from "../../../i18n/hooks.mjs";
2
2
  import { sanitizeFilename } from "../field-utils.mjs";
3
+ import { resolveAssetUrl } from "../../../utils/asset-url.mjs";
3
4
  import { useUploadCollection } from "../../../hooks/use-upload-collection.mjs";
4
5
  import { useUpload } from "../../../hooks/use-upload.mjs";
5
6
  import * as React from "react";
@@ -35,7 +36,7 @@ function useRichTextImageUpload({ imageCollection, onImageUpload }) {
35
36
  const sanitizedName = sanitizeFilename(file.name);
36
37
  const uploadedAsset = await upload(sanitizedName === file.name ? file : new File([file], sanitizedName, { type: file.type }), { to: collection });
37
38
  if (!uploadedAsset?.url) throw new Error(t("upload.error"));
38
- return uploadedAsset.url;
39
+ return resolveAssetUrl(uploadedAsset.url) ?? uploadedAsset.url;
39
40
  }, [
40
41
  collection,
41
42
  collections,
@@ -1,5 +1,5 @@
1
1
  import { RichTextEditorProps } from "./types.mjs";
2
- import * as react_jsx_runtime0 from "react/jsx-runtime";
2
+ import * as react_jsx_runtime33 from "react/jsx-runtime";
3
3
 
4
4
  //#region src/client/components/fields/rich-text-editor/index.d.ts
5
5
 
@@ -24,6 +24,7 @@ declare function RichTextEditor({
24
24
  error,
25
25
  localized,
26
26
  locale,
27
+ hideLabel,
27
28
  extensions,
28
29
  preset,
29
30
  features,
@@ -34,6 +35,6 @@ declare function RichTextEditor({
34
35
  imageCollection,
35
36
  enableMediaLibrary,
36
37
  outputMode
37
- }: RichTextEditorProps): react_jsx_runtime0.JSX.Element;
38
+ }: RichTextEditorProps): react_jsx_runtime33.JSX.Element;
38
39
  //#endregion
39
40
  export { RichTextEditor };
@@ -66,7 +66,7 @@ function RichTextEditorLoadingSkeleton({ disabled, readOnly }) {
66
66
  * `useEditor` with an empty extension list which causes the ProseMirror
67
67
  * "Schema is missing its top node type" error.
68
68
  */
69
- function RichTextEditor({ name, value, onChange, disabled, readOnly, label, description, placeholder, required, error, localized, locale, extensions, preset, features, showCharacterCount, maxCharacters, enableImages, onImageUpload, imageCollection, enableMediaLibrary, outputMode }) {
69
+ function RichTextEditor({ name, value, onChange, disabled, readOnly, label, description, placeholder, required, error, localized, locale, hideLabel, extensions, preset, features, showCharacterCount, maxCharacters, enableImages, onImageUpload, imageCollection, enableMediaLibrary, outputMode }) {
70
70
  const { t } = useTranslation();
71
71
  const resolveText = useResolveText();
72
72
  const resolvedLabel = label ? resolveText(label) : void 0;
@@ -137,7 +137,7 @@ function RichTextEditor({ name, value, onChange, disabled, readOnly, label, desc
137
137
  className: "space-y-2",
138
138
  "data-disabled": disabled || readOnly,
139
139
  children: [
140
- resolvedLabel && /* @__PURE__ */ jsxs("div", {
140
+ !hideLabel && resolvedLabel && /* @__PURE__ */ jsxs("div", {
141
141
  className: "flex items-center gap-2",
142
142
  children: [/* @__PURE__ */ jsxs(Label, {
143
143
  htmlFor: name,
@@ -193,7 +193,7 @@ function RichTextEditorCore({ name, ariaLabel, ariaDescribedBy, value, onChange,
193
193
  const [linkOpen, setLinkOpen] = React.useState(false);
194
194
  const [imageOpen, setImageOpen] = React.useState(false);
195
195
  const [uploadingInlineImage, setUploadingInlineImage] = React.useState(false);
196
- const lastEmittedValueRef = React.useRef(void 0);
196
+ const lastEmittedValueRef = React.useRef(value);
197
197
  const editorRef = React.useRef(null);
198
198
  const allowImages = features.image && (enableImages ?? true);
199
199
  const allowLinks = features.link;
@@ -312,6 +312,7 @@ function RichTextEditorCore({ name, ariaLabel, ariaDescribedBy, value, onChange,
312
312
  onUpdate: ({ editor: currentEditor }) => {
313
313
  if (disabled || readOnly) return;
314
314
  const nextValue = getOutput(currentEditor, outputMode);
315
+ if (isSameValue(nextValue, lastEmittedValueRef.current)) return;
315
316
  lastEmittedValueRef.current = nextValue;
316
317
  onChange?.(nextValue);
317
318
  }
@@ -23,7 +23,7 @@ function resolveOptionIcons(options) {
23
23
  icon: resolveIconElement(option.icon)
24
24
  } : option);
25
25
  }
26
- function SelectField({ name, label, description, placeholder, required, disabled, localized, locale, control, className, options, loadOptions, multiple, clearable, maxSelections, emptyMessage }) {
26
+ function SelectField({ name, label, description, placeholder, required, disabled, localized, locale, hideLabel, control, className, options, loadOptions, multiple, clearable, maxSelections, emptyMessage }) {
27
27
  const resolvedControl = useResolvedControl(control);
28
28
  const resolvedOptions = useMemo(() => resolveOptionIcons(options), [options]);
29
29
  const resolvedLoadOptions = useMemo(() => {
@@ -43,6 +43,7 @@ function SelectField({ name, label, description, placeholder, required, disabled
43
43
  disabled,
44
44
  localized,
45
45
  locale,
46
+ hideLabel,
46
47
  error: fieldState.error?.message,
47
48
  children: multiple ? /* @__PURE__ */ jsx(SelectMulti, {
48
49
  id: name,
@@ -6,7 +6,7 @@ import { jsx } from "react/jsx-runtime";
6
6
  import { Controller } from "react-hook-form";
7
7
 
8
8
  //#region src/client/components/fields/text-field.tsx
9
- function TextField({ name, label, description, placeholder, required, disabled, localized, locale, control, className, type = "text", maxLength, autoComplete }) {
9
+ function TextField({ name, label, description, placeholder, required, disabled, localized, locale, hideLabel, control, className, type = "text", maxLength, autoComplete }) {
10
10
  return /* @__PURE__ */ jsx(Controller, {
11
11
  name,
12
12
  control: useResolvedControl(control),
@@ -18,6 +18,7 @@ function TextField({ name, label, description, placeholder, required, disabled,
18
18
  disabled,
19
19
  localized,
20
20
  locale,
21
+ hideLabel,
21
22
  error: fieldState.error?.message,
22
23
  children: /* @__PURE__ */ jsx(TextInput, {
23
24
  id: name,
@@ -6,7 +6,7 @@ import { jsx } from "react/jsx-runtime";
6
6
  import { Controller } from "react-hook-form";
7
7
 
8
8
  //#region src/client/components/fields/textarea-field.tsx
9
- function TextareaField({ name, label, description, placeholder, required, disabled, localized, locale, control, className, rows, maxLength, autoResize }) {
9
+ function TextareaField({ name, label, description, placeholder, required, disabled, localized, locale, hideLabel, control, className, rows, maxLength, autoResize }) {
10
10
  return /* @__PURE__ */ jsx(Controller, {
11
11
  name,
12
12
  control: useResolvedControl(control),
@@ -18,6 +18,7 @@ function TextareaField({ name, label, description, placeholder, required, disabl
18
18
  disabled,
19
19
  localized,
20
20
  locale,
21
+ hideLabel,
21
22
  error: fieldState.error?.message,
22
23
  children: /* @__PURE__ */ jsx(TextareaInput, {
23
24
  id: name,
@@ -6,7 +6,7 @@ import { jsx } from "react/jsx-runtime";
6
6
  import { Controller } from "react-hook-form";
7
7
 
8
8
  //#region src/client/components/fields/time-field.tsx
9
- function TimeField({ name, label, description, placeholder, required, disabled, localized, locale, control, className, precision }) {
9
+ function TimeField({ name, label, description, placeholder, required, disabled, localized, locale, hideLabel, control, className, precision }) {
10
10
  return /* @__PURE__ */ jsx(Controller, {
11
11
  name,
12
12
  control: useResolvedControl(control),
@@ -19,6 +19,7 @@ function TimeField({ name, label, description, placeholder, required, disabled,
19
19
  disabled,
20
20
  localized,
21
21
  locale,
22
+ hideLabel,
22
23
  error: fieldState.error?.message,
23
24
  children: /* @__PURE__ */ jsx(TimeInput, {
24
25
  id: name,
@@ -96,18 +96,18 @@ function SectionRenderer({ section, index, ctx }) {
96
96
  const value = `section-${index}`;
97
97
  return /* @__PURE__ */ jsx(Accordion, {
98
98
  defaultValue: section.defaultCollapsed !== true ? [value] : [],
99
- className: "w-full",
99
+ className: "qa-field-layout__section qa-field-layout__section--collapsible panel-surface bg-card overflow-hidden",
100
100
  children: /* @__PURE__ */ jsxs(AccordionItem, {
101
101
  value,
102
- className: "border-transparent px-4",
102
+ className: "border-none px-0 data-open:bg-transparent",
103
103
  children: [/* @__PURE__ */ jsx(AccordionTrigger, {
104
- className: "hover:no-underline",
104
+ className: "hover:bg-surface-low aria-expanded:bg-surface-low min-h-12 px-4 py-3 hover:no-underline",
105
105
  children: /* @__PURE__ */ jsx("span", {
106
106
  className: "font-semibold",
107
107
  children: ctx.resolveText(section.label, "Section")
108
108
  })
109
109
  }), /* @__PURE__ */ jsxs(AccordionContent, {
110
- className: "pt-2 pb-4",
110
+ className: "border-border-subtle border-t px-4 pt-3 pb-4",
111
111
  children: [section.description && /* @__PURE__ */ jsx("p", {
112
112
  className: "text-muted-foreground mb-4 text-sm text-pretty",
113
113
  children: ctx.resolveText(section.description, "")
@@ -1,5 +1,6 @@
1
1
  import { useSafeI18n } from "../../i18n/hooks.mjs";
2
2
  import { cn } from "../../lib/utils.mjs";
3
+ import { resolveAssetUrl } from "../../utils/asset-url.mjs";
3
4
  import { Skeleton } from "../ui/skeleton.mjs";
4
5
  import { Icon } from "@iconify/react";
5
6
  import * as React from "react";
@@ -62,7 +63,7 @@ function MediaGridSkeleton({ columns = 4 }) {
62
63
  }
63
64
  function AssetItem({ asset, selected, selectionMode, onToggle, onClick }) {
64
65
  const [imageError, setImageError] = React.useState(false);
65
- const thumbnailUrl = asset.url;
66
+ const thumbnailUrl = resolveAssetUrl(asset.url);
66
67
  const isImageType = isImage(asset.mimeType);
67
68
  const showCheckbox = selectionMode !== "none";
68
69
  const handleClick = () => {