alinea 2.0.0-preview.13 → 2.0.0-preview.14

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 (323) hide show
  1. package/dist/.init/content/pages/welcome.json +1 -1
  2. package/dist/.mcp/later/dev.json +1 -1
  3. package/dist/.mcp/online/dev.json +1 -1
  4. package/dist/adapter/next/cms.js +6 -2
  5. package/dist/adapter/next/context.js +1 -4
  6. package/dist/adapter/next/handler.js +12 -1
  7. package/dist/backend/CommitEntries.d.ts +17 -0
  8. package/dist/backend/CommitEntries.js +39 -0
  9. package/dist/backend/Handler.d.ts +9 -0
  10. package/dist/backend/Handler.js +80 -20
  11. package/dist/backend/api/BasicAuth.js +5 -1
  12. package/dist/backend/api/CreateBackend.js +2 -2
  13. package/dist/backend/api/DatabaseApi.js +18 -5
  14. package/dist/backend/api/GithubApi.d.ts +13 -0
  15. package/dist/backend/api/GithubApi.js +170 -30
  16. package/dist/backend/api/database.js +2 -2
  17. package/dist/backend/util/Syncable.d.ts +6 -0
  18. package/dist/backend/util/Syncable.js +3 -1
  19. package/dist/bundled.d.ts +911 -151
  20. package/dist/chunks/{chunk-5H4D5JQL.js → chunk-2PP7WJUJ.js} +1 -11
  21. package/dist/chunks/{chunk-QHHMKJUO.js → chunk-7DDPSWRY.js} +465 -2
  22. package/dist/chunks/{chunk-HOETF6HN.js → chunk-PMGIIYDD.js} +1 -0
  23. package/dist/chunks/{chunk-3QFSGTGF.js → chunk-SCV3TEXM.js} +1 -1
  24. package/dist/chunks/{chunk-UPY2RUZF.js → chunk-TWBREZWC.js} +30 -0
  25. package/dist/cli/Generate.js +20 -7
  26. package/dist/cli/Mcp.js +1 -1
  27. package/dist/cli/Serve.js +1 -1
  28. package/dist/cli/bin.js +1 -1
  29. package/dist/cli/build/BuildEmitter.d.ts +5 -1
  30. package/dist/cli/build/BuildEmitter.js +8 -3
  31. package/dist/cli/generate/CompileConfig.js +10 -6
  32. package/dist/cli/generate/DevDB.d.ts +4 -1
  33. package/dist/cli/generate/DevDB.js +15 -7
  34. package/dist/cli/generate/GenerateDashboard.js +1 -0
  35. package/dist/cli/generate/MediaLocations.d.ts +18 -0
  36. package/dist/cli/generate/MediaLocations.js +69 -0
  37. package/dist/cli/serve/CreateLocalServer.js +1 -0
  38. package/dist/cli/serve/mcp/DevMcp.js +1 -1
  39. package/dist/cloud/CloudRemote.js +21 -7
  40. package/dist/cms.d.ts +4 -2
  41. package/dist/components/Alert.js +2 -2
  42. package/dist/components/AppShell.js +2 -2
  43. package/dist/components/AsyncList.d.ts +52 -0
  44. package/dist/components/AsyncList.js +69 -0
  45. package/dist/components/Breadcrumbs.js +6 -6
  46. package/dist/components/Button.js +4 -4
  47. package/dist/components/Calendar.js +18 -18
  48. package/dist/components/Checkbox.js +10 -10
  49. package/dist/components/CheckboxGroup.js +2 -2
  50. package/dist/components/Collapsible.js +5 -5
  51. package/dist/components/ColorSwatch.js +2 -2
  52. package/dist/components/ColorSwatchPicker.js +4 -4
  53. package/dist/components/ComboBox.d.ts +27 -9
  54. package/dist/components/ComboBox.js +40 -9
  55. package/dist/components/Command.js +13 -13
  56. package/dist/components/ContentCard.js +28 -28
  57. package/dist/components/ContentGrid.js +11 -11
  58. package/dist/components/DataList.js +1 -1
  59. package/dist/components/DateField.d.ts +4 -0
  60. package/dist/components/DateField.js +2 -2
  61. package/dist/components/DatePicker.js +2 -2
  62. package/dist/components/DateRangePicker.js +2 -2
  63. package/dist/components/Dialog.d.ts +14 -3
  64. package/dist/components/Dialog.js +38 -11
  65. package/dist/components/DropZone.js +6 -6
  66. package/dist/components/DropdownMenu.js +8 -8
  67. package/dist/components/Empty.js +4 -4
  68. package/dist/components/Field.d.ts +1 -1
  69. package/dist/components/Field.js +18 -12
  70. package/dist/components/Form.d.ts +23 -0
  71. package/dist/components/Form.js +64 -0
  72. package/dist/components/Link.d.ts +8 -1
  73. package/dist/components/List.js +27 -27
  74. package/dist/components/MediaPreview.js +5 -5
  75. package/dist/components/MultipleSelect.d.ts +13 -8
  76. package/dist/components/MultipleSelect.js +25 -25
  77. package/dist/components/NavRail.js +7 -7
  78. package/dist/components/NumberField.d.ts +5 -1
  79. package/dist/components/NumberField.js +6 -4
  80. package/dist/components/Page.js +6 -6
  81. package/dist/components/Popover.js +2 -2
  82. package/dist/components/PreviewFrame.js +8 -8
  83. package/dist/components/RadioGroup.js +10 -10
  84. package/dist/components/Resizable.js +1 -1
  85. package/dist/components/SearchField.js +8 -8
  86. package/dist/components/Select.d.ts +13 -10
  87. package/dist/components/Select.js +18 -32
  88. package/dist/components/Sheet.js +6 -6
  89. package/dist/components/Sidebar.js +5 -5
  90. package/dist/components/SortableList.js +12 -12
  91. package/dist/components/Spinner.js +4 -4
  92. package/dist/components/Stack.d.ts +20 -0
  93. package/dist/components/Stack.js +45 -0
  94. package/dist/components/Surface.js +1 -1
  95. package/dist/components/Switch.js +5 -5
  96. package/dist/components/Table.d.ts +11 -1
  97. package/dist/components/Table.js +81 -53
  98. package/dist/components/Tabs.js +6 -6
  99. package/dist/components/TagGroup.js +10 -10
  100. package/dist/components/TextField.d.ts +7 -1
  101. package/dist/components/TextField.js +20 -11
  102. package/dist/components/TimeField.d.ts +6 -1
  103. package/dist/components/TimeField.js +51 -26
  104. package/dist/components/Timestamp.js +1 -1
  105. package/dist/components/ToggleGroup.js +2 -2
  106. package/dist/components/Toolbar.js +3 -3
  107. package/dist/components/Tooltip.js +5 -4
  108. package/dist/components/Tree.js +27 -27
  109. package/dist/components/internal/DateControl.js +11 -11
  110. package/dist/components/internal/FormScope.d.ts +2 -0
  111. package/dist/components/internal/FormScope.js +8 -0
  112. package/dist/components/internal/ListBoxGroup.d.ts +12 -0
  113. package/dist/components/internal/ListBoxGroup.js +46 -0
  114. package/dist/components/internal/ListBoxOption.d.ts +2 -2
  115. package/dist/components/internal/ListBoxOption.js +5 -5
  116. package/dist/components/internal/Locale.js +2 -2
  117. package/dist/components/internal/PopoverSurface.js +5 -4
  118. package/dist/components/internal/SelectionCheckbox.js +5 -5
  119. package/dist/components/internal/Slot.js +1 -1
  120. package/dist/components/internal/ToggleButton.js +4 -4
  121. package/dist/components/internal/Trigger.js +2 -2
  122. package/dist/components/internal/useDragDrop.js +2 -2
  123. package/dist/components/types.d.ts +4 -0
  124. package/dist/components.d.ts +3 -0
  125. package/dist/components.js +3 -0
  126. package/dist/core/Client.js +12 -1
  127. package/dist/core/Config.d.ts +6 -0
  128. package/dist/core/Config.js +12 -0
  129. package/dist/core/DashboardEntry.d.ts +19 -0
  130. package/dist/core/DashboardEntry.js +0 -0
  131. package/dist/core/Document.d.ts +2 -2
  132. package/dist/core/Document.js +10 -3
  133. package/dist/core/EntryFields.d.ts +8 -2
  134. package/dist/core/Field.d.ts +5 -0
  135. package/dist/core/Filter.d.ts +19 -2
  136. package/dist/core/Filter.js +5 -0
  137. package/dist/core/Graph.d.ts +10 -2
  138. package/dist/core/Overview.d.ts +74 -5
  139. package/dist/core/Overview.js +1 -0
  140. package/dist/core/Root.js +1 -1
  141. package/dist/core/Scope.d.ts +5 -0
  142. package/dist/core/Scope.js +22 -5
  143. package/dist/core/Type.d.ts +25 -6
  144. package/dist/core/Type.js +25 -7
  145. package/dist/core/db/LocalStore.d.ts +7 -0
  146. package/dist/core/field/CreateField.d.ts +1 -2
  147. package/dist/core/source/Source.d.ts +2 -0
  148. package/dist/core/source/Source.js +11 -0
  149. package/dist/core/util/Async.d.ts +2 -0
  150. package/dist/core/util/Async.js +12 -1
  151. package/dist/core/util/Checks.js +1 -1
  152. package/dist/core/util/Slugs.js +4 -3
  153. package/dist/dashboard/App.css +13 -11
  154. package/dist/dashboard/App.d.ts +1 -0
  155. package/dist/dashboard/App.js +22 -6
  156. package/dist/dashboard/Href.d.ts +37 -0
  157. package/dist/dashboard/Href.js +46 -0
  158. package/dist/dashboard/app/ActivityStatus.js +34 -12
  159. package/dist/dashboard/app/AuthView.js +2 -2
  160. package/dist/dashboard/app/BlockSettings.d.ts +9 -0
  161. package/dist/dashboard/app/BlockSettings.js +16 -0
  162. package/dist/dashboard/app/BlockSheet.js +3 -3
  163. package/dist/dashboard/app/BlockSheetStoryFrame.js +2 -2
  164. package/dist/dashboard/app/CompactField.js +15 -15
  165. package/dist/dashboard/app/CroppedImage.js +2 -2
  166. package/dist/dashboard/app/DashboardLayout.d.ts +1 -1
  167. package/dist/dashboard/app/DashboardLayout.js +12 -0
  168. package/dist/dashboard/app/EntryActions.d.ts +11 -0
  169. package/dist/dashboard/app/EntryActions.js +30 -0
  170. package/dist/dashboard/app/EntryFields.d.ts +15 -2
  171. package/dist/dashboard/app/EntryFields.js +26 -5
  172. package/dist/dashboard/app/EntryHeader.js +17 -7
  173. package/dist/dashboard/app/EntryReferences.js +2 -2
  174. package/dist/dashboard/app/EntrySidebar.js +25 -23
  175. package/dist/dashboard/app/EntrySidebarPreview.js +2 -2
  176. package/dist/dashboard/app/EntryValidationModal.js +3 -3
  177. package/dist/dashboard/app/Explorer.js +84 -45
  178. package/dist/dashboard/app/ExplorerBatchActions.js +4 -4
  179. package/dist/dashboard/app/ExplorerControls.js +5 -5
  180. package/dist/dashboard/app/ExplorerList.js +2 -2
  181. package/dist/dashboard/app/ExplorerModal.js +3 -3
  182. package/dist/dashboard/app/ExplorerOverviewHeader.d.ts +6 -0
  183. package/dist/dashboard/app/ExplorerOverviewHeader.js +28 -0
  184. package/dist/dashboard/app/ExplorerTable.js +7 -7
  185. package/dist/dashboard/app/ExplorerTabs.d.ts +12 -0
  186. package/dist/dashboard/app/ExplorerTabs.js +42 -0
  187. package/dist/dashboard/app/ImageEditor.js +31 -31
  188. package/dist/dashboard/app/LinkPicker.js +3 -3
  189. package/dist/dashboard/app/LocaleMenu.js +9 -9
  190. package/dist/dashboard/app/MediaPreviewCell.js +5 -5
  191. package/dist/dashboard/app/OverviewCell.js +12 -10
  192. package/dist/dashboard/app/RemoveDialog.js +5 -0
  193. package/dist/dashboard/app/SearchBar.js +4 -4
  194. package/dist/dashboard/app/SettingsTab.d.ts +6 -0
  195. package/dist/dashboard/app/SettingsTab.js +27 -0
  196. package/dist/dashboard/app/SidebarTree.js +19 -19
  197. package/dist/dashboard/app/WorkspaceMenu.js +6 -6
  198. package/dist/dashboard/app/WorkspaceRoots.js +102 -98
  199. package/dist/dashboard/app/editor/FileEditor.js +18 -18
  200. package/dist/dashboard/app/pages/EntryPage.js +42 -39
  201. package/dist/dashboard/app/pages/RootPage.js +3 -3
  202. package/dist/dashboard/app/pages/SplashPage.js +94 -79
  203. package/dist/dashboard/app/pages/UsersPage.js +9 -9
  204. package/dist/dashboard/app/ui/DashboardModal.js +14 -14
  205. package/dist/dashboard/atoms/activity.d.ts +1 -0
  206. package/dist/dashboard/atoms/activity.js +8 -5
  207. package/dist/dashboard/atoms/config.d.ts +2 -0
  208. package/dist/dashboard/atoms/dashboard.d.ts +11 -0
  209. package/dist/dashboard/atoms/dashboard.js +8 -0
  210. package/dist/dashboard/atoms/editor.d.ts +7 -1
  211. package/dist/dashboard/atoms/editor.js +14 -2
  212. package/dist/dashboard/atoms/explorer.d.ts +15 -0
  213. package/dist/dashboard/atoms/explorer.js +43 -14
  214. package/dist/dashboard/atoms/graph.d.ts +5 -0
  215. package/dist/dashboard/atoms/graph.js +26 -1
  216. package/dist/dashboard/atoms/nav.d.ts +16 -10
  217. package/dist/dashboard/atoms/nav.js +96 -36
  218. package/dist/dashboard/atoms/overview.d.ts +26 -4
  219. package/dist/dashboard/atoms/overview.js +41 -12
  220. package/dist/dashboard/atoms/root.js +7 -1
  221. package/dist/dashboard/boot/ActivityEvent.d.ts +1 -1
  222. package/dist/dashboard/boot/Boot.js +6 -0
  223. package/dist/dashboard/boot/DashboardWorker.d.ts +6 -0
  224. package/dist/dashboard/boot/DashboardWorker.js +89 -8
  225. package/dist/dashboard/boot/LoadWorker.js +3 -0
  226. package/dist/dashboard/boot/SyncEvent.d.ts +7 -0
  227. package/dist/dashboard/boot/SyncEvent.js +14 -0
  228. package/dist/dashboard/boot/WorkerDB.d.ts +1 -0
  229. package/dist/dashboard/boot/WorkerDB.js +3 -0
  230. package/dist/dashboard/cms/EntryTable.d.ts +4 -3
  231. package/dist/dashboard/cms/EntryTable.js +2 -2
  232. package/dist/dashboard/cms/ViewProps.d.ts +3 -2
  233. package/dist/dashboard/cms/hooks.d.ts +34 -18
  234. package/dist/dashboard/cms/hooks.js +43 -18
  235. package/dist/dashboard/contexts.d.ts +3 -2
  236. package/dist/dashboard/contexts.js +3 -2
  237. package/dist/dashboard/hook/UseEntryEditor.d.ts +1 -1
  238. package/dist/dashboard/hook/UseEntryEditor.js +7 -3
  239. package/dist/dashboard/icons.d.ts +1 -0
  240. package/dist/dashboard/icons.js +15 -0
  241. package/dist/database/BrowserEntryStore.js +35 -13
  242. package/dist/database/DatabaseTables.js +1 -1
  243. package/dist/database/EntryDatabase.d.ts +10 -1
  244. package/dist/database/EntryDatabase.js +31 -10
  245. package/dist/database/EntryStore.js +1 -11
  246. package/dist/database/entry/EntryTable.d.ts +5 -3
  247. package/dist/database/entry/EntryTable.js +25 -8
  248. package/dist/database/query/Condition.d.ts +4 -1
  249. package/dist/database/query/Condition.js +16 -5
  250. package/dist/database/query/EntryQuery.js +59 -7
  251. package/dist/database/query/Relation.d.ts +13 -1
  252. package/dist/database/query/Relation.js +19 -1
  253. package/dist/database/query/Search.d.ts +7 -1
  254. package/dist/database/query/Search.js +54 -9
  255. package/dist/database/sync/EntrySyncer.d.ts +2 -0
  256. package/dist/database/sync/EntrySyncer.js +4 -2
  257. package/dist/database/sync/Ingest.d.ts +2 -1
  258. package/dist/database/sync/Ingest.js +20 -6
  259. package/dist/field/code/CodeField.view.js +12 -12
  260. package/dist/field/link/EntryLink.d.ts +4 -5
  261. package/dist/field/link/FileLink.d.ts +2 -2
  262. package/dist/field/link/ImageLink.d.ts +2 -5
  263. package/dist/field/link/ImageLink.js +0 -1
  264. package/dist/field/link/LinkField.view.js +18 -15
  265. package/dist/field/list/ListField.view.js +50 -35
  266. package/dist/field/metadata/MetadataField.view.js +14 -14
  267. package/dist/field/richtext/RichTextBlock.js +39 -35
  268. package/dist/field/richtext/RichTextExtensions.js +23 -23
  269. package/dist/field/richtext/RichTextField.view.js +6 -6
  270. package/dist/field/richtext/RichTextInsertMenu.js +5 -5
  271. package/dist/field/richtext/RichTextToolbar.js +26 -1
  272. package/dist/field/richtext/Toolbar.d.ts +9 -1
  273. package/dist/field/richtext/Toolbar.js +12 -2
  274. package/dist/field/select/SelectField.view.js +1 -0
  275. package/dist/index.css +174 -23
  276. package/dist/index.d.ts +1 -0
  277. package/dist/index.js +3 -1
  278. package/dist/next.d.ts +2 -0
  279. package/dist/next.edge.d.ts +2 -0
  280. package/dist/picker/entry/EntryPicker.d.ts +3 -1
  281. package/dist/picker/entry/EntryPicker.js +15 -1
  282. package/dist/preview/react.js +3 -0
  283. package/dist/query.d.ts +26 -14
  284. package/dist/query.js +4 -0
  285. package/dist/types.d.ts +8 -5
  286. package/dist/ui/Stack.d.ts +4 -2
  287. package/docs/ai-setup.md +3 -3
  288. package/docs/cli.md +4 -2
  289. package/docs/components/combo-box.md +105 -16
  290. package/docs/components/date-field.md +1 -1
  291. package/docs/components/dialog.md +51 -0
  292. package/docs/components/form.md +67 -0
  293. package/docs/components/link.md +10 -0
  294. package/docs/components/multiple-select.md +9 -17
  295. package/docs/components/number-field.md +5 -1
  296. package/docs/components/select.md +9 -20
  297. package/docs/components/stack.md +42 -0
  298. package/docs/components/table.md +83 -0
  299. package/docs/components/text-field.md +9 -1
  300. package/docs/components/time-field.md +5 -1
  301. package/docs/components.md +39 -1
  302. package/docs/configuration.md +3 -1
  303. package/docs/custom-fields.md +135 -7
  304. package/docs/deploy/self-host.md +2 -0
  305. package/docs/deploy.md +7 -4
  306. package/docs/fields/image.md +0 -1
  307. package/docs/fields/list.md +19 -0
  308. package/docs/fields/object.md +5 -3
  309. package/docs/fields/rich-text.md +31 -0
  310. package/docs/index.md +8 -6
  311. package/docs/instant-publishing.md +23 -2
  312. package/docs/overviews.md +128 -4
  313. package/docs/query/filtering.md +2 -0
  314. package/docs/query/related.md +58 -9
  315. package/docs/query.md +52 -1
  316. package/docs/quickstart.md +5 -5
  317. package/docs/schema/document.md +2 -0
  318. package/docs/schema/type.md +3 -1
  319. package/docs/upgrading.md +100 -19
  320. package/llms-full.txt +1022 -125
  321. package/package.json +1 -1
  322. package/readme.md +2 -2
  323. package/dist/chunks/{chunk-4ZA6KZHS.js → chunk-6FQ5IMNA.js} +3 -3
package/docs/overviews.md CHANGED
@@ -64,16 +64,18 @@ export const products = Config.root('Products', {
64
64
  - `sort`: the default order of the children, in the overview and in the sidebar tree. See [Sorting](#sorting).
65
65
  - `sorts`: the orders editors can pick in the filter and sort menu, keyed by name. See [Sort options](#sort-options).
66
66
  - `filters`: filters editors can apply in the filter and sort menu, keyed by name. See [Filters](#filters).
67
+ - `tabs`: tabs above the listed entries, keyed by a name kept in the url. See [Tabs](#tabs).
67
68
  - `layout`: `'table'` or `'cards'`, the layout the overview opens in. Editors can still switch.
68
69
  - `thumbnail`: the image shown on cards: an image field, or one per type (`{BlogPost: BlogPost.cover, Event: Event.poster}`). Defaults to the first image found in the fields of the entry.
69
70
  - `actions`: components rendered in the toolbar of the overview, such as an export button. See [Actions](#actions).
71
+ - `header`: a component rendered between the toolbar and the listed entries, such as totals. See [Header](#header).
70
72
 
71
73
  ## Columns
72
74
 
73
75
  `Config.column(options)` takes:
74
76
 
75
77
  - `header` (required): the column header.
76
- - `select`: what the column shows. Anything a [query](query.md) can select: a field (`Product.price`), an expression (`Query.url`), a query of linked entries, or an object of those.
78
+ - `select`: what the column shows. Anything a [query](query.md) can select: a field (`Product.price`), an expression (`Query.url`), a query of linked entries, a relation such as `Query.referencedBy`, or an object of those.
77
79
  - `format`: a function that turns the selected value into text: `(value, {locale}) => string`.
78
80
  - `view`: a React component that renders the cell, see [Custom cells](#custom-cells).
79
81
  - `sortBy`, `sortable`: how the column sorts, see [Sorting](#sorting).
@@ -186,7 +188,34 @@ Force them on or off with `builtins`, for example `{type: true}` to always show
186
188
 
187
189
  ## Sorting
188
190
 
189
- Editors sort an overview by clicking a column header, or from the filter and sort menu. The title, the built-in columns, expressions of the entry (such as `Query.title`) and fields that hold a single value (text, number, date, select, check, path, ...) are sortable. Columns that select links, lists, multiple selects, rich text or JSON need `sortBy`: an expression, or a query of a linked entry that selects a single expression, such as `Product.brand.first({select: Query.title})`. Set `sortable: false` to turn sorting off for a column.
191
+ Editors sort an overview by clicking a column header, or from the filter and sort menu. The title, the built-in columns, expressions of the entry (such as `Query.title`) and fields that hold a single value (text, number, date, select, check, path, ...) are sortable, and so are columns that count related entries, such as `Query.referencedBy({type: Product, count: true})`. Columns that select links, lists, multiple selects, rich text or JSON need `sortBy`: an expression, or a query of a linked entry that selects a single expression, such as `Product.brand.first({select: Query.title})`. Set `sortable: false` to turn sorting off for a column.
192
+
193
+ A column that counts the products of each brand, which editors can sort by:
194
+
195
+ File: schema/Brands.ts
196
+ ```tsx
197
+ import {Config, Query} from 'alinea'
198
+ import {Product} from './Product'
199
+
200
+ export const Brands = Config.document('Brands', {
201
+ contains: ['Brand'],
202
+ fields: {},
203
+ overview: {
204
+ columns: {
205
+ products: Config.column({
206
+ header: 'Products',
207
+ width: 100,
208
+ align: 'end',
209
+ select: Query.referencedBy({
210
+ type: Product,
211
+ field: Product.brand,
212
+ count: true
213
+ })
214
+ })
215
+ }
216
+ }
217
+ })
218
+ ```
190
219
 
191
220
  Sorting runs in the database query, and entries without a value come last. It only changes what the editor sees, never the stored order of the entries. While a list is sorted, "Sorted by Price · Reset" shows in the toolbar and rows can't be reordered; Reset goes back to the default order. The sort is kept in the url (`?sort=price`, or `?sort=-price` for descending), so it survives a reload and can be shared, and it is restored when the editor comes back to the overview in the same session. Search results stay ordered by relevance.
192
221
 
@@ -257,6 +286,74 @@ export const Products = Config.document('Products', {
257
286
  })
258
287
  ```
259
288
 
289
+ ### Options from the content
290
+
291
+ `options` can also be a function that loads them, for example an option per category entry. It receives the `graph` to query and the `locale` of the overview, and runs when the overview opens. Filters can match on the entries that link to a listed entry too, with `_referenced` (see [Entries that link here](query/related.md#entries-that-link-here)):
292
+
293
+ File: schema/Products.ts
294
+ ```tsx
295
+ import {Config, Query} from 'alinea'
296
+ import {Category, Product} from './Product'
297
+
298
+ export const Products = Config.document('Products', {
299
+ contains: ['Product'],
300
+ overview: {
301
+ filters: {
302
+ category: {
303
+ label: 'Category',
304
+ multiple: true,
305
+ async options({graph, locale}) {
306
+ const categories = await graph.find({
307
+ type: Category,
308
+ locale,
309
+ select: {id: Query.id, title: Query.title},
310
+ orderBy: {asc: Query.title}
311
+ })
312
+ return Object.fromEntries(
313
+ categories.map(category => [
314
+ category.id,
315
+ {
316
+ label: category.title,
317
+ filter: {categories: {includes: {_entry: category.id}}}
318
+ }
319
+ ])
320
+ )
321
+ }
322
+ },
323
+ usage: {
324
+ label: 'Usage',
325
+ options: {
326
+ linked: {label: 'Linked from other entries', filter: {_referenced: true}},
327
+ unused: {label: 'Not linked anywhere', filter: {_referenced: false}}
328
+ }
329
+ }
330
+ }
331
+ },
332
+ fields: {}
333
+ })
334
+ ```
335
+
336
+ ## Tabs
337
+
338
+ `tabs` shows tabs above the listed entries, apart from the filter and sort menu. Each tab has a `label` and an optional `filter`, like the options of a filter. A tab without a filter lists every child. The first tab is selected when the overview opens. The key of the selected tab is kept in the url (`?tab=active`) and the overview remembers it when the editor returns to it. Entries match the filter of the tab, the applied filters and the search terms.
339
+
340
+ File: schema/Products.ts
341
+ ```tsx
342
+ import {Config} from 'alinea'
343
+
344
+ export const Products = Config.document('Products', {
345
+ contains: ['Product'],
346
+ overview: {
347
+ tabs: {
348
+ all: {label: 'All'},
349
+ active: {label: 'Active', filter: {status: 'active'}},
350
+ archived: {label: 'Archived', filter: {status: 'archived'}}
351
+ }
352
+ },
353
+ fields: {}
354
+ })
355
+ ```
356
+
260
357
  ## Mixed lists
261
358
 
262
359
  When a parent contains several types, a column can select a different value per type. Key `select` and `sortBy` by the type names of your schema:
@@ -342,6 +439,33 @@ export function ExportProducts({query}: OverviewActionProps) {
342
439
  }
343
440
  ```
344
441
 
442
+ ## Header
443
+
444
+ `header` renders a component between the toolbar (and the tabs) and the listed entries, for example totals or a chart of what is listed. It receives `OverviewHeaderProps`: the `workspace`, `root` and `parentId` of the list, the `locale`, the `search` terms, the `filter` of the selected tab and the applied filters (undefined when none applies) and a `query` for the listed entries, without paging. Like `actions`, it takes a component or a path to one: `header: '@/views/ProductTotals'`.
445
+
446
+ File: views/ProductTotals.tsx
447
+ ```tsx
448
+ import {type OverviewHeaderProps, useGraph} from 'alinea/cms'
449
+ import {Stack, Text} from 'alinea/components'
450
+ import {useEffect, useState} from 'react'
451
+ import {Product} from '@/schema/Product'
452
+
453
+ export function ProductTotals({query}: OverviewHeaderProps) {
454
+ const graph = useGraph()
455
+ const [prices, setPrices] = useState<Array<number | null>>([])
456
+ useEffect(() => {
457
+ graph.find({...query, select: Product.price}).then(setPrices)
458
+ }, [graph, query])
459
+ const total = prices.reduce<number>((sum, price) => sum + (price ?? 0), 0)
460
+ return (
461
+ <Stack direction="row" gap={24}>
462
+ <Text>{prices.length} products</Text>
463
+ <Text>€{total} in total</Text>
464
+ </Stack>
465
+ )
466
+ }
467
+ ```
468
+
345
469
  ## Entry tables in your own views
346
470
 
347
471
  `EntryTable` from `alinea/cms` renders a table of entries with the columns of an overview: the headers sort and a row opens its entry. Use it in a `Field.view` section, or any other custom view, to show related entries. Here a brand lists its products:
@@ -384,7 +508,7 @@ export function BrandProducts() {
384
508
  `EntryTable` takes:
385
509
 
386
510
  - `type`: list entries of this type, or of an array of types.
387
- - `filter`: a query [filter](query/filtering.md), such as `{brand: {has: {_entry: entry.id}}}`.
511
+ - `filter`: a query [filter](query/filtering.md), such as `{brand: {has: {_entry: entry.id}}}`. Conditions on related entries work too: `{_referenced: Query.referencedBy({type: Product, filter: {stock: {gt: 0}}})}` lists the entries that a product in stock links to.
388
512
  - `workspace`, `root`, `parentId`: only list entries in this workspace or root, or the children of this entry (`null` for the top level).
389
513
  - `overview`: the columns and default order to use: an overview object, or the type or root that configures one. Defaults to the overview of the root or type that contains `type`.
390
514
  - `columns`: show these columns instead of those of the overview.
@@ -445,7 +569,7 @@ Prefer this over a type with a hidden title and a single `Field.view`: that crea
445
569
 
446
570
  Two other options replace parts of the dashboard:
447
571
 
448
- - A type's `view` replaces the editor of entries of that type. It receives `TypeViewProps`: `{type}`.
572
+ - A type's `view` replaces the form of entries of that type, below the entry header. It receives `TypeViewProps`: `{type}`.
449
573
  - A type's `defaultView: 'overview'` opens the overview of an entry's children first, instead of its form.
450
574
 
451
575
  ## Upgrading from Alinea 1.x
@@ -21,6 +21,7 @@ A filter is an object with a condition per key. The keys are:
21
21
 
22
22
  - the fields of the queried `type`, such as `featured` or `publishDate`. TypeScript checks them when you pass a `type`.
23
23
  - the entry's own properties with an underscore: `_id`, `_type`, `_parentId`, `_path`, `_url`, `_status`, `_locale`, `_workspace`, `_root`, `_index`, `_createdAt` and `_updatedAt`.
24
+ - `_referenced`: whether another entry links to the entry. `true` or `false`, or a `Query.referencedBy(...)` query that the linking entry has to match, see [Entries that link here](related.md#entries-that-link-here).
24
25
 
25
26
  Every key has to match, so several keys in one filter mean "and".
26
27
 
@@ -152,6 +153,7 @@ export async function searchSite(terms: string, locale: string) {
152
153
  How it matches:
153
154
 
154
155
  - The input is split into words, and an entry must match every word.
156
+ - Hyphenated, spaced and joined spellings find each other: `multi-modal`, `multi modal` and `multimodal` all match "multi-modal", "multi modal" and "multimodal".
155
157
  - Words match as prefixes, so `alin` finds "Alinea".
156
158
  - Small typos are tolerated: one wrong letter for words of 5 to 14 characters, more for longer words. Case and accents don't matter.
157
159
  - Entry titles are always searched. Other fields only when you mark them `searchable: true`: [text](../fields/text.md) and [rich text](../fields/rich-text.md) fields support it, also inside lists, objects and rich text blocks.
@@ -1,6 +1,6 @@
1
1
  # Related
2
2
 
3
- A single query can also fetch entries related to the ones it finds: their parents, children and siblings, their translations, and the entries they link to. Put a relation in `select` (or in `include` when you don't select), and it's resolved in the same query.
3
+ A single query can also fetch entries related to the ones it finds: their parents, children and siblings, their translations, the entries they link to and the entries that link to them. Put a relation in `select` (or in `include` when you don't select), and it's resolved in the same query.
4
4
 
5
5
  ```tsx
6
6
  import {cms} from '@/cms'
@@ -100,9 +100,9 @@ const post = await cms.get({
100
100
 
101
101
  Linked entries are looked up in the locale of the entry you queried, falling back to entries without a locale, so a link to an untranslated author keeps working from every translation of a post.
102
102
 
103
- ## The other direction
103
+ ## Entries that link here
104
104
 
105
- Links are stored on the entry that links, so "all posts by this author" is a filter on the posts. Filter on the id of the target, stored as `_entry`:
105
+ Links are stored on the entry that links. `Query.referencedBy` finds the entries that link to the entry, through any of their fields, including links in rich text. Pass `type` to only find entries of that type, and `field` to only follow one link field of it. Like the other relations it takes the query options, `count: true` and `first: true`:
106
106
 
107
107
  ```tsx
108
108
  import {cms} from '@/cms'
@@ -110,19 +110,68 @@ import {Author, BlogPost} from '@/schema'
110
110
  import {Query} from 'alinea'
111
111
 
112
112
  export async function authorPage(path: string) {
113
- const author = await cms.get({
113
+ return cms.get({
114
114
  type: Author,
115
115
  path,
116
- select: {id: Query.id, name: Query.title, bio: Author.bio}
116
+ select: {
117
+ name: Query.title,
118
+ bio: Author.bio,
119
+ // The posts whose author field links to this author
120
+ posts: Query.referencedBy({
121
+ type: BlogPost,
122
+ field: BlogPost.author,
123
+ select: {title: Query.title, url: Query.url},
124
+ orderBy: {desc: BlogPost.publishDate}
125
+ }),
126
+ // Every entry linking to the author, in any field
127
+ mentions: Query.referencedBy({count: true})
128
+ }
117
129
  })
118
- const posts = await cms.find({
130
+ }
131
+ ```
132
+
133
+ The entries that link to a translated entry are looked up in its locale. For an entry without a locale, such as a shared author, the linking entries of every locale are returned: pass `locale` to only get those of one locale.
134
+
135
+ The same relation is a filter condition: `_referenced` holds for entries that an entry of the query links to. `{_referenced: true}` matches entries that anything links to and `{_referenced: false}` entries nothing links to, such as unused media files:
136
+
137
+ ```tsx
138
+ import {cms} from '@/cms'
139
+ import {Author, BlogPost} from '@/schema'
140
+ import {Query} from 'alinea'
141
+
142
+ // Authors of at least one featured post
143
+ const featuredAuthors = await cms.find({
144
+ type: Author,
145
+ filter: {
146
+ _referenced: Query.referencedBy({
147
+ type: BlogPost,
148
+ field: BlogPost.author,
149
+ filter: {featured: true}
150
+ })
151
+ },
152
+ select: Query.title
153
+ })
154
+ ```
155
+
156
+ ### Filtering on the link
157
+
158
+ The other way around, "the posts by this author" is a filter on the posts: links store the id of their target as `_entry`. This is the way to go when you start from the posts, for example to page through them:
159
+
160
+ ```tsx
161
+ import {cms} from '@/cms'
162
+ import {BlogPost} from '@/schema'
163
+ import {Query} from 'alinea'
164
+
165
+ export async function postsBy(authorId: string, page: number) {
166
+ return cms.find({
119
167
  type: BlogPost,
120
168
  // Single link: has, multiple links: includes
121
- filter: {author: {has: {_entry: author.id}}},
169
+ filter: {author: {has: {_entry: authorId}}},
122
170
  select: {title: Query.title, url: Query.url},
123
- orderBy: {desc: BlogPost.publishDate}
171
+ orderBy: {desc: BlogPost.publishDate},
172
+ skip: page * 10,
173
+ take: 10
124
174
  })
125
- return {author, posts}
126
175
  }
127
176
  ```
128
177
 
package/docs/query.md CHANGED
@@ -79,7 +79,58 @@ const urls = await cms.find({type: BlogPost, select: Query.url})
79
79
  // Array<string>
80
80
  ```
81
81
 
82
- `Query` has `id`, `title`, `type`, `index`, `workspace`, `root`, `status`, `parentId`, `locale`, `path`, `url`, `aliases`, `createdAt` and `updatedAt`, plus the relation helpers such as `Query.children` described in [Related content](query/related.md) and `Query.snippet` for [search results](query/filtering.md).
82
+ `Query` has `id`, `title`, `type`, `index`, `workspace`, `root`, `status`, `parentId`, `locale`, `path`, `url`, `aliases`, `createdAt` and `updatedAt`, plus the relation helpers such as `Query.children` and `Query.referencedBy` described in [Related content](query/related.md) and `Query.snippet` for [search results](query/filtering.md).
83
+
84
+ ### Query fields and type fields
85
+
86
+ There are two kinds of references you can select. `Query.*` are the built-in properties every entry has, whatever its type: `Query.id`, `Query.title`, `Query.url` and the others above. The fields you define are selected through their type: `BlogPost.intro` is the `intro` field of `BlogPost`, `BlogPost.cover` its cover image. Type fields are typed from their definition and resolved like they are without a select, so `BlogPost.cover` returns an `ImageLink`. Combine both in one `select`:
87
+
88
+ ```tsx
89
+ import {cms} from '@/cms'
90
+ import {BlogPost} from '@/schema'
91
+ import {Query} from 'alinea'
92
+
93
+ const posts = await cms.find({
94
+ type: BlogPost,
95
+ select: {
96
+ // Built-in properties of the entry
97
+ id: Query.id,
98
+ title: Query.title,
99
+ url: Query.url,
100
+ // Fields of the BlogPost type
101
+ intro: BlogPost.intro,
102
+ cover: BlogPost.cover
103
+ }
104
+ })
105
+ // Array<{id: string, title: string, url: string, intro: string, cover: ImageLink | null}>
106
+ ```
107
+
108
+ A type field only has a value on entries of that type, so select it in a query for that `type` (or one filtered on `_type`). `Query` fields work in any query, also one without a `type`, such as a search across all entries. Fields that `Config.document` adds, like `title` and `path`, are available both ways: `Query.title` and `BlogPost.title` return the same value.
109
+
110
+ Fields inside an [object field](fields/object.md) are selected through the object: `Page.seo.title` returns the `title` of the `seo` object, typed like the field. That works wherever a field can be selected, also inside relations such as `Query.children`, and in `orderBy`:
111
+
112
+ ```tsx
113
+ import {cms} from '@/cms'
114
+ import {Config, Field, Query} from 'alinea'
115
+
116
+ export const Page = Config.document('Page', {
117
+ fields: {
118
+ seo: Field.object('SEO', {
119
+ fields: {
120
+ title: Field.text('SEO title'),
121
+ description: Field.text('SEO description')
122
+ }
123
+ })
124
+ }
125
+ })
126
+
127
+ const pages = await cms.find({
128
+ type: Page,
129
+ select: {url: Query.url, title: Page.seo.title},
130
+ orderBy: {asc: Page.seo.title}
131
+ })
132
+ // Array<{url: string, title: string}>
133
+ ```
83
134
 
84
135
  Selecting only what a page needs keeps the data you pass to client components small, and TypeScript infers the result either way. To name the type of a result, use `Awaited<ReturnType<typeof yourQueryFunction>>`.
85
136
 
@@ -20,22 +20,22 @@ Navigate to the newly created project directory and install the package with you
20
20
 
21
21
  Variant: npm
22
22
  ```shellscript
23
- npm install alinea@preview
23
+ npm install alinea@rc
24
24
  ```
25
25
 
26
26
  Variant: yarn
27
27
  ```shellscript
28
- yarn add alinea@preview
28
+ yarn add alinea@rc
29
29
  ```
30
30
 
31
31
  Variant: pnpm
32
32
  ```shellscript
33
- pnpm add alinea@preview
33
+ pnpm add alinea@rc
34
34
  ```
35
35
 
36
36
  Variant: bun
37
37
  ```shellscript
38
- bun add alinea@preview
38
+ bun add alinea@rc
39
39
  ```
40
40
 
41
41
  ## 3. Initialize the project
@@ -44,7 +44,7 @@ Alinea requires a [config file](configuration.md) which can be auto-generated by
44
44
 
45
45
  Variant: npm
46
46
  ```shellscript
47
- npx alinea@preview init
47
+ npx alinea@rc init
48
48
  ```
49
49
 
50
50
  Variant: yarn
@@ -57,6 +57,8 @@ export const Home = Config.document('Home', {
57
57
  })
58
58
  ```
59
59
 
60
+ Next to `fields`, a document takes `seo` and `details`: fields added to the SEO and Details tabs. `settings` adds a Settings tab with its fields.
61
+
60
62
  ## Using the metadata
61
63
 
62
64
  Select the metadata field in the query for a page and map it to your framework's head tags. With the Next.js App Router:
@@ -19,6 +19,7 @@ The label ("Author") is what editors see. The name the type is stored under is i
19
19
  ## Options
20
20
 
21
21
  - `fields` (required): the [fields](../fields.md) of the type, keyed by name. Field names must start with a letter and contain only letters, digits and underscores. Spread [tabs](../fields/tabs.md) or `Field.view(...)` sections in between to lay out the form.
22
+ - `settings`: fields that editors set in the settings sheet of a block, below its label and anchor, instead of in the block's form. Entries of the type show them in a Settings tab. They are stored and queried like the other fields, so their names can't repeat a field name.
22
23
  - `contains`: the types that can be created as children of this entry, by schema name or by reference. Without it, entries of this type can't have children.
23
24
  - `overview`: how the dashboard lists the children: columns, the default order (`sort`), the layout and actions. See [Overviews](../overviews.md).
24
25
  - `insertOrder`: where new children are added: `'first'`, `'last'` or `'free'` (default), which lets the editor choose in the create dialog.
@@ -27,7 +28,8 @@ The label ("Author") is what editors see. The name the type is stored under is i
27
28
  - `hidden`: set to `true` to leave entries of this type out of the sidebar tree. They can still be linked to and queried.
28
29
  - `defaultView`: `'edit'` opens the form when an entry is selected, `'overview'` shows a table of its children first. The default is `'overview'` for entries that have children and `'edit'` otherwise.
29
30
  - `preview`: turn [live previews](../live-previews.md) on or off for this type (`true`/`false`), or pass a React component that renders the preview. It overrides the setting of the root, workspace and config.
30
- - `view`: a React component, or a path to one, that replaces the entry editor for this type. It receives `{type}`.
31
+ - `view`: a React component, or a path to one, that replaces the form of entries of this type. It renders below the entry header and edits the entry with the hooks of `alinea/cms`. It receives `{type}`. See [Replace the form of a type](../custom-fields.md#replace-the-form-of-a-type).
32
+ - `actions`: components rendered in the header of the entry editor, next to the publish button, such as an export button. Each is a component or a path to one and receives `EntryActionProps`: the `entry` and the `locale` it is edited in. See [Add actions to the entry header](../custom-fields.md#add-actions-to-the-entry-header).
31
33
  - `orderChildrenBy` (deprecated): sort children by one or more fields. Use `overview.sort`, which still accepts the same value.
32
34
  - `summaryRow`, `summaryThumb` (deprecated): not used by the dashboard. Configure the columns and card image in the `overview` of the parent instead.
33
35
 
package/docs/upgrading.md CHANGED
@@ -4,23 +4,25 @@ This guide walks through moving an existing Alinea 1.7 project to 2.0. Your cont
4
4
 
5
5
  ## Checklist
6
6
 
7
- 1. Run Node.js 24 or higher, locally, in CI and wherever your site runs, and use React 19.
8
- 2. Update the package: `npm install alinea@preview` (or `pnpm add`, `yarn add`, `bun add`).
9
- 3. Add `handlerUrl` to your config, it is now required. Replace `dashboardFile` with `adminPath`.
7
+ 1. Run Node.js 24 or higher, locally, in CI (scheduled jobs that run the alinea CLI too) and wherever your site runs, and use React 19.
8
+ 2. Update the package: `npm install alinea@rc` (or `pnpm add`, `yarn add`, `bun add`).
9
+ 3. Add `handlerUrl` to your config, it is now required, and set `baseUrl.production` (or make `handlerUrl` absolute). Replace `dashboardFile` with `adminPath`.
10
10
  4. Make sure your Next.js config is wrapped in `withAlinea` and your scripts run `alinea dev -- next dev` and `alinea build -- next build`: media URLs are now served through your site.
11
- 5. Move children of `Config.media(...)` into its `children` option.
12
- 6. Remove the options of `Field.metadata(...)` and the `step` option of `Field.time`.
13
- 7. Rich text: pass `extensions` as a function and upgrade custom extensions to Tiptap 3.
14
- 8. Handler: replace `remote` with `backend`, and move entry hooks to `beforeCommit` / `afterCommit`.
15
- 9. Self-hosted OAuth2: provide `validateClaims`.
16
- 10. Custom field views and dashboard code: import hooks from `alinea/cms` and components from `alinea/components` instead of `alinea/dashboard` and `alinea/ui`.
17
- 11. Run `alinea build`, open the dashboard and save an entry to review the diff.
11
+ 5. Media: replace `image.url` with `image.src` and use the URLs Alinea returns instead of building them from `location`. `alinea build` warns about media files that store a url as their location, `alinea build --fix` repairs them.
12
+ 6. Move children of `Config.media(...)` into its `children` option.
13
+ 7. Remove the options of `Field.metadata(...)` and the `step` option of `Field.time`.
14
+ 8. Rich text: pass `extensions` as a function, upgrade custom extensions to Tiptap 3 and render custom attributes as `data-*` attributes.
15
+ 9. Handler: replace `remote` with `backend`, and move entry hooks to `beforeCommit` / `afterCommit`.
16
+ 10. Self-hosted OAuth2: provide `validateClaims`.
17
+ 11. Custom field views and dashboard code: import hooks from `alinea/cms` and components from `alinea/components` instead of `alinea/dashboard` and `alinea/ui`, replace `useEntryEditor` with `useEntry` and rename the theme tokens your CSS reads.
18
+ 12. Run `alinea build`, open the dashboard and save an entry to review the diff.
19
+ 13. Upgrading from a 2.0 preview instead? See [From a 2.0 preview](#from-a-2-0-preview).
18
20
 
19
21
  ## Runtime requirements
20
22
 
21
- Node.js 24 or higher. The CLI exits with "Alinea requires Node version 24 or higher" on older versions. The bundled content database is opened with Node's built-in `node:sqlite` (or `bun:sqlite` under Bun), so the runtime that serves your site needs Node 24 too, not only the machine that builds it.
23
+ Node.js 24 or higher. The CLI exits with "Alinea requires Node version 24 or higher" on older versions. The bundled content database is opened with Node's built-in `node:sqlite` (or `bun:sqlite` under Bun), so the runtime that serves your site needs Node 24 too, not only the machine that builds it. The same goes for scheduled CI jobs that run the alinea CLI, such as a nightly `alinea build` or a script that imports content: update the Node version in those workflows as well.
22
24
 
23
- React 19. `react` and `react-dom` 19 are now peer dependencies and the CLI checks for them. In practice that means Next.js 15 or newer.
25
+ React 19. `react` and `react-dom` 19 are now peer dependencies and the CLI checks for them. In practice that means Next.js 15 or newer. Check third-party React renderers too: libraries built on React 18 internals, such as older versions of react-three-fiber or react-pixi, break under React 19. Upgrade them to a version that supports React 19.
24
26
 
25
27
  ## Configuration
26
28
 
@@ -50,6 +52,23 @@ export const cms = createCMS({
50
52
 
51
53
  The handler now matches its path exactly, so requests have to use the configured URL (a trailing slash no longer matches).
52
54
 
55
+ ### baseUrl is required for production
56
+
57
+ Your site reaches the handler at `handlerUrl` resolved against `baseUrl`, also in production, to check for new content. Without a production `baseUrl` (and with a relative `handlerUrl`), `alinea build` exits and queries throw `Missing baseUrl in Alinea config: set baseUrl.production to the url of your site`. Alinea doesn't fall back to the request origin or to hosting provider variables: set the production URL of your site yourself, for example from an environment variable you define:
58
+
59
+ File: cms.ts
60
+ ```tsx
61
+ export const cms = createCMS({
62
+ schema,
63
+ workspaces,
64
+ baseUrl: {
65
+ development: 'http://localhost:3000',
66
+ production: process.env.SITE_URL ?? 'https://example.com'
67
+ },
68
+ handlerUrl: '/api/cms'
69
+ })
70
+ ```
71
+
53
72
  ### dashboardFile is replaced by adminPath
54
73
 
55
74
  `adminPath` is the path the dashboard is served on, `/admin` by default. `alinea build` writes the dashboard to `public/admin.html` and `withAlinea` serves it at `/admin`. `dashboardFile` still works when `adminPath` isn't set, but it is deprecated. See [Configuration](configuration.md) for the new `maxUploadSize` and `tracer` options.
@@ -70,6 +89,28 @@ What to check:
70
89
  - Media requests now reach your handler route. A static export without the handler can't serve them.
71
90
  - If you defined `images.localPatterns` or `remotePatterns` yourself, check that the admin file route is still allowed. The `uploads.alinea.cloud` remote pattern is no longer added.
72
91
  - Don't change the `mediaDir` of an existing workspace: stored locations are relative to it. New projects default to `public/media`.
92
+ - A media file whose `location` is a url (some 1.x saves wrote the upload url there) can't be served. `alinea build` warns about these files; `alinea build --fix` points them at the file of the same name in the `mediaDir` when there is one.
93
+
94
+ Image links only have `src`: the `url` property, which held the same value, was removed so that spreading an image into `<img>` or `next/image` (`<Image {...image} />`) doesn't put a `url` attribute on the element. Replace `image.url` with `image.src`. File links keep `url` and `href`.
95
+
96
+ Don't derive media URLs from the stored file. Code that reads `location`, or parses the 1.x file names (`name.<id>.ext`) to build a URL or find an id, breaks on `/admin/file/...` URLs. Use the URL Alinea returns instead: `src` for images, `url` (or `href`) for files, and `_url` when you query media files directly:
97
+
98
+ ```tsx
99
+ import {cms} from '@/cms'
100
+ import {Query} from 'alinea'
101
+ import {MediaFile} from 'alinea/core'
102
+
103
+ // 1.7: built the url from the stored location
104
+ // const src = file.location
105
+
106
+ const files = await cms.find({
107
+ type: MediaFile,
108
+ select: {title: Query.title, url: Query.url}
109
+ })
110
+ // Without a select each result has its url as `_url`
111
+ ```
112
+
113
+ `ImageLink` no longer needs a type parameter when the image field has no extra fields: replace `ImageLink<undefined>` with `ImageLink`.
73
114
 
74
115
  A workspace can set `mediaUrl` to add a prefix to its media URLs, which helps when several workspaces have files with the same name.
75
116
 
@@ -123,6 +164,8 @@ Field.richText('Body', {
123
164
 
124
165
  The `RichText` renderer from `alinea/ui` always renders tables with a `<tbody>`, and the `tbody` view override is gone.
125
166
 
167
+ Attributes that a custom extension adds to a node only reach your views and tag overrides of `RichText` as `data-*` attributes: render them as `data-*` in the extension's `renderHTML`. Content saved by 1.7 gets these attributes when it is saved again, see [Custom attributes](fields/rich-text.md#custom-attributes).
168
+
126
169
  ### Edit.move
127
170
 
128
171
  ```tsx
@@ -157,9 +200,12 @@ const handler = createHandler({
157
200
  // Inspect or rewrite the changes, return the mutations to commit
158
201
  return mutations
159
202
  },
160
- afterCommit({sha}) {
161
- // Pages that query content refresh on their own, revalidate your own caches
162
- revalidateTag('products', 'max')
203
+ afterCommit({entries, removed}) {
204
+ // Pages that query content refresh on their own, revalidate your own
205
+ // caches, eg. a feed that lists products
206
+ const changed = [...entries, ...removed]
207
+ if (changed.some(entry => entry.type === 'Product'))
208
+ revalidateTag('products', 'max')
163
209
  }
164
210
  })
165
211
 
@@ -167,7 +213,7 @@ export const GET = handler
167
213
  export const POST = handler
168
214
  ```
169
215
 
170
- Errors thrown in `afterCommit` are logged and don't fail the commit. See [Instant publishing](instant-publishing.md).
216
+ `afterCommit` receives the `mutations`, the `sha` of the new commit, the `user`, the changed `entries` as they are after the commit and the `removed` entries with the url they had. Errors thrown in it are logged and don't fail the commit. See [Instant publishing](instant-publishing.md).
171
217
 
172
218
  ### remote is replaced by backend
173
219
 
@@ -203,7 +249,7 @@ The dashboard was rebuilt on React Aria Components. Nothing changes for editors
203
249
 
204
250
  ### Components
205
251
 
206
- `alinea/ui` now only exports the `RichText` renderer and the `imageBlurUrl` helper (and the deprecated `HStack`/`VStack`). Its `Button`, `Chip`, `Icon`, `Loader`, `Typo` and other components were removed, as were the files behind deep imports like `alinea/ui/Button`. Use the public component library instead, it is what the dashboard itself is built from:
252
+ `alinea/ui` now only exports the `RichText` renderer and the `imageBlurUrl` helper (and the deprecated `HStack`/`VStack`, use `Stack` from `alinea/components`). Its `Button`, `Chip`, `Icon`, `Loader`, `Typo` and other components were removed, as were the files behind deep imports like `alinea/ui/Button`. Use the public component library instead, it is what the dashboard itself is built from:
207
253
 
208
254
  ```tsx
209
255
  // 1.7
@@ -215,6 +261,23 @@ import {Button, Spinner} from 'alinea/components'
215
261
 
216
262
  See [Components](components.md). Dashboard icons are available from `alinea/dashboard/icons`.
217
263
 
264
+ ### Theme tokens
265
+
266
+ The CSS custom properties of 1.7 were renamed and many were removed. Styles that read an old token silently fall back to the property's default. The closest replacements:
267
+
268
+ - `--alinea-background` → `--alinea-bg`, `--alinea-content` → `--alinea-backdrop`, `--alinea-lift`, `--alinea-inner` and `--alinea-toolbar` → `--alinea-bg-muted`.
269
+ - `--alinea-foreground` → `--alinea-fg`, `--alinea-foreground-faded` → `--alinea-fg-muted`, `--alinea-title-foreground` → `--alinea-fg-strong`.
270
+ - `--alinea-outline` and `--alinea-inner-outline` → `--alinea-border`, `--alinea-highlight` → `--alinea-muted`.
271
+ - `--alinea-fields` → `--alinea-input`, `--alinea-fields-foreground` → `--alinea-field-label`, `--alinea-fields-focus` and `--alinea-focus-foreground` → `--alinea-ring`.
272
+ - `--alinea-selected` stays, `--alinea-selected-foreground` → `--alinea-selected-fg`, `--alinea-selected-faded` → `--alinea-primary-subtle`.
273
+ - `--alinea-button-background` → `--alinea-primary`, `--alinea-button-foreground` → `--alinea-primary-fg`, `--alinea-error` → `--alinea-danger`.
274
+ - `--alinea-accent` (the workspace color) → `--alinea-workspace-color`.
275
+ - `--alinea-variant-info-*` → `--alinea-primary-subtle` and `--alinea-primary-subtle-fg`, `--alinea-variant-success-*` → `--alinea-success-subtle(-fg)`, `--alinea-variant-progress-*` → `--alinea-warning-subtle(-fg)`.
276
+ - `--alinea-modal-background` → `--alinea-scrim`, `--alinea-shadow` → `--alinea-shadow-raised`, `--alinea-border-radius` → `--alinea-radius`.
277
+ - Removed without a replacement: `--alinea-hue`, `--alinea-saturation`, `--alinea-positive`, `--alinea-negative`, `--alinea-button-hover`, `--alinea-accent-foreground`, `--alinea-divider-top`, `--alinea-divider-bottom`, `--alinea-tabs-trigger-*`, `--alinea-fields-table`, `--alinea-fields-header`, `--alinea-fields-shadow`, `--alinea-fields-selected` and the `create` and `disabled` variants. Derive hover and disabled shades with `color-mix()` and `opacity`.
278
+
279
+ The tokens of `alinea/css`, and its resets, now apply inside elements with a `data-alinea-theme` attribute only. The dashboard sets it on `<html>`, so the CSS of your dashboard views reads the tokens as before. A page of your site that imports `alinea/css` to render `alinea/components` needs an element with `data-alinea-theme` around them. See [Theme tokens](components.md#theme-tokens) for the full list.
280
+
218
281
  ### Custom field views
219
282
 
220
283
  Field views are still referenced by path (`view: '@/fields/Range.view'`) or passed as a component. The hooks and helpers for custom views now come from one entry point, `alinea/cms`. From `alinea/dashboard`, `useField` and its related hooks still work but are deprecated: the new `useField` returns a `[value, setValue]` tuple, and the options and error have their own hooks:
@@ -231,11 +294,11 @@ const options = useFieldOptions(field)
231
294
  const error = useFieldError(field)
232
295
  ```
233
296
 
234
- `useFieldValue`, `useFieldOptions`, `useFieldError` and `useFieldKey` move to `alinea/cms` too and `useFieldMutator` becomes `useFieldSetter`. Deep imports of dashboard hooks move as well: `useLocale` and `useGraph` from `alinea/dashboard/hook/*` are exported from `alinea/cms`, and `useEntryEditor` is replaced by `useEntry`. `alinea/dashboard/hooks` is internal now. `InputLabel` is now an alias of the `Field` component: it takes `label`, `description`, `error`, `required`, `disabled`, `readOnly`, `icon` and `shared`. Its 1.7 props `help`, `width`, `inline`, `size` and the fold props are gone, so help text passed through `{...options}` is no longer shown: pass it as `description`, or replace it with `FieldChrome` from `alinea/cms`, which reads the label, help text and error from the field. The form atoms (`useForm`, `FormProvider`, `FormRow` and so on) were removed from `alinea/dashboard`. See [Custom fields](custom-fields.md) for a complete example.
297
+ `useFieldValue`, `useFieldOptions`, `useFieldError` and `useFieldKey` move to `alinea/cms` too and `useFieldMutator` becomes `useFieldSetter`. Deep imports of dashboard hooks move as well: `useLocale` and `useGraph` from `alinea/dashboard/hook/*` are exported from `alinea/cms`, and `useEntryEditor` is replaced by `useEntry`. `useEntryEditor` still works as a shim for views that only read the entry: its `activeVersion` has the `id`, `locale`, `path`, `status`, `title` and `type` of the entry, and `activeVersion.data` only has `path`. Reading another property throws, read field values with `useFieldValue(field)` instead. `alinea/dashboard/hooks` is internal now. `InputLabel` is now an alias of the `Field` component: it takes `label`, `description`, `error`, `required`, `disabled`, `readOnly`, `icon` and `shared`. Its 1.7 props `help`, `width`, `inline`, `size` and the fold props are gone, so help text passed through `{...options}` is no longer shown: pass it as `description`, or replace it with `FieldChrome` from `alinea/cms`, which reads the label, help text and error from the field. The form atoms (`useForm`, `FormProvider`, `FormRow` and so on) were removed from `alinea/dashboard`. See [Custom fields](custom-fields.md) for a complete example.
235
298
 
236
299
  ### Custom type views
237
300
 
238
- A type's `view` now only receives `{type}`, the `editor` prop of 1.7 is gone. A custom `auth` view in the config is not used: the dashboard always renders its own sign-in.
301
+ A type's `view` now only receives `{type}`, the `editor` prop of 1.7 is gone. The view no longer replaces the whole editor: it renders below the entry header, which publishes and saves drafts, and reads and writes the fields of the entry with the hooks of `alinea/cms`. A custom `auth` view in the config is not used: the dashboard always renders its own sign-in.
239
302
 
240
303
  ### List overviews
241
304
 
@@ -282,6 +345,24 @@ See [Overviews](overviews.md) for sorting, custom cells and toolbar actions.
282
345
 
283
346
  `alinea/yjs` and `alinea/cloud/view/CloudAuth` no longer exist. `alinea/picker/entry/EntryPicker` and `alinea/picker/url/UrlPicker` no longer ship the picker UI: `EntryPickerModal`, `UrlPickerForm` and `UrlPickerModal` were removed without a public replacement. Import the picker option types (`EditorInfo`, `EditorLocation`, `EditorLimitLocation` and `EntryPickerConditions`) from `alinea/field/link`. Code that subclassed fields relied on the removed shape system (`alinea/core/Shape`, `Field.shape`, `Type.shape`): the field constructors no longer take a shape, and `postProcess` became `queryValue`.
284
347
 
348
+ ## From a 2.0 preview
349
+
350
+ Projects on a 2.0 preview have most of the above already. These changes landed in the release candidate:
351
+
352
+ - Image links no longer have `url`, use `src`. File links keep `url` and `href`.
353
+ - `useEntry()` from `alinea/cms` returns a `DashboardEntry`: the `id`, `type`, `title`, `path`, `url`, `locale`, `status`, `workspace`, `root` and `parentId` of the entry and its saved field values in `data`. The other properties of the internal entry row are gone.
354
+ - `useEntryEditor()`: the publish status moved from `activeVersion.data.status` to `activeVersion.status`. Reading another property of `activeVersion.data` than `path` throws.
355
+ - `useSiblingFieldValue(key)` is deprecated: pass the field to `useFieldValue(field)`, which is typed and also reads fields of a parent row or the entry, or its key: `useFieldValue('path')`. `EditFields` takes an object field to render its fields in its scope.
356
+ - Theme tokens apply inside `data-alinea-theme` only, see [Theme tokens](#theme-tokens) above. The workspace color is `--alinea-workspace-color`.
357
+ - `Select` from `alinea/components` only shows its clear button with `clearable`. `Field.select` still lets editors clear fields that aren't required.
358
+ - `Select`, `ComboBox` and `MultipleSelect` values are strings or numbers, and `onValueChange` is typed like the `value` you pass. Without a `value`, it receives `string | number`.
359
+ - `TimeField` with a `name` submits `14:30` (or `14:30:15` with `granularity="second"`) like a native time input, it used to submit seconds always.
360
+ - `NumberField` no longer groups digits: pass `formatOptions={{useGrouping: true}}` to show `1,250`.
361
+ - `afterCommit` also receives the changed `entries` and the `removed` entries, so hooks no longer have to look them up from the mutations.
362
+ - Every field accepts `width`, custom fields don't need to declare it in their options.
363
+ - `alinea build` accepts an absolute `handlerUrl` without a production `baseUrl`, and its error names the missing `baseUrl` setting.
364
+ - A type's `view` renders below the entry header, in the editor of the entry: the field hooks of `alinea/cms` and `EditField` edit its fields and the header publishes them. It used to replace the whole editor without access to the fields.
365
+
285
366
  ## After upgrading
286
367
 
287
368
  Run your build and open the dashboard. Things to try: an image renders on your site through `/admin/file/...`, publishing an entry updates the site, and custom fields still render. Then have a look at what's new in 2.0: [Field.localiser](internationalization.md) for per-field translations, URL aliases and backlinks, [instant publishing](instant-publishing.md) and the [MCP server](mcp.md) for coding agents.