@prenta/admin 1.17.1 → 1.17.3

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 (293) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/__tests__/lib/posts-service.test.js +35 -0
  3. package/dist/__tests__/lib/posts-service.test.js.map +1 -1
  4. package/dist/__tests__/lib/public-path.test.d.ts +2 -0
  5. package/dist/__tests__/lib/public-path.test.d.ts.map +1 -0
  6. package/dist/__tests__/lib/public-path.test.js +25 -0
  7. package/dist/__tests__/lib/public-path.test.js.map +1 -0
  8. package/dist/__tests__/views/ai-settings.render.test.js +67 -1
  9. package/dist/__tests__/views/ai-settings.render.test.js.map +1 -1
  10. package/dist/__tests__/views/posts-list-view.render.test.js +11 -0
  11. package/dist/__tests__/views/posts-list-view.render.test.js.map +1 -1
  12. package/dist/__tests__/views/seo-autopilot-card.render.test.js +4 -2
  13. package/dist/__tests__/views/seo-autopilot-card.render.test.js.map +1 -1
  14. package/dist/components/CoauthorResultPopover.d.ts.map +1 -1
  15. package/dist/components/CoauthorResultPopover.js +4 -1
  16. package/dist/components/CoauthorResultPopover.js.map +1 -1
  17. package/dist/components/CommentSidePanel.d.ts +0 -7
  18. package/dist/components/CommentSidePanel.d.ts.map +1 -1
  19. package/dist/components/CommentSidePanel.js +0 -2
  20. package/dist/components/CommentSidePanel.js.map +1 -1
  21. package/dist/components/EditorRail.d.ts +2 -1
  22. package/dist/components/EditorRail.d.ts.map +1 -1
  23. package/dist/components/MediaPickerModal.d.ts +2 -1
  24. package/dist/components/MediaPickerModal.d.ts.map +1 -1
  25. package/dist/components/TipTapEditor.d.ts +3 -2
  26. package/dist/components/TipTapEditor.d.ts.map +1 -1
  27. package/dist/components/forms/EntriesTable.d.ts +0 -1
  28. package/dist/components/forms/EntriesTable.d.ts.map +1 -1
  29. package/dist/components/forms/EntriesTable.js +1 -1
  30. package/dist/components/forms/EntriesTable.js.map +1 -1
  31. package/dist/components/pages/tag-badges.d.ts +2 -2
  32. package/dist/components/pages/tag-badges.d.ts.map +1 -1
  33. package/dist/components/pages/tag-badges.js.map +1 -1
  34. package/dist/components/seo/FaqProposalPreview.d.ts +2 -3
  35. package/dist/components/seo/FaqProposalPreview.d.ts.map +1 -1
  36. package/dist/components/seo/FaqProposalPreview.js +1 -1
  37. package/dist/components/seo/FaqProposalPreview.js.map +1 -1
  38. package/dist/components/seo/SeoCopilotDrawer.js +1 -1
  39. package/dist/components/seo/SeoCopilotDrawer.js.map +1 -1
  40. package/dist/components/seo/SeoEditorPane.d.ts.map +1 -1
  41. package/dist/components/seo/SeoEditorPane.js +4 -1
  42. package/dist/components/seo/SeoEditorPane.js.map +1 -1
  43. package/dist/components/ui/Badge.d.ts +3 -9
  44. package/dist/components/ui/Badge.d.ts.map +1 -1
  45. package/dist/components/ui/Badge.js +2 -3
  46. package/dist/components/ui/Badge.js.map +1 -1
  47. package/dist/components/ui/Button.d.ts +2 -2
  48. package/dist/components/ui/Button.d.ts.map +1 -1
  49. package/dist/components/ui/Button.js +0 -1
  50. package/dist/components/ui/Button.js.map +1 -1
  51. package/dist/components/ui/Card.d.ts +2 -3
  52. package/dist/components/ui/Card.d.ts.map +1 -1
  53. package/dist/components/ui/Card.js +0 -4
  54. package/dist/components/ui/Card.js.map +1 -1
  55. package/dist/components/ui/Input.d.ts +6 -8
  56. package/dist/components/ui/Input.d.ts.map +1 -1
  57. package/dist/components/ui/Input.js +6 -6
  58. package/dist/components/ui/Input.js.map +1 -1
  59. package/dist/components/ui/Modal.d.ts +1 -1
  60. package/dist/components/ui/Modal.d.ts.map +1 -1
  61. package/dist/components/ui/ResizableAside.d.ts +0 -13
  62. package/dist/components/ui/ResizableAside.d.ts.map +1 -1
  63. package/dist/components/ui/ResizableAside.js +5 -5
  64. package/dist/components/ui/ResizableAside.js.map +1 -1
  65. package/dist/components/ui/StatusBadge.d.ts +2 -2
  66. package/dist/components/ui/StatusBadge.d.ts.map +1 -1
  67. package/dist/components/ui/StatusBadge.js +0 -1
  68. package/dist/components/ui/StatusBadge.js.map +1 -1
  69. package/dist/components/ui/Toggle.d.ts +2 -8
  70. package/dist/components/ui/Toggle.d.ts.map +1 -1
  71. package/dist/components/ui/Toggle.js +0 -1
  72. package/dist/components/ui/Toggle.js.map +1 -1
  73. package/dist/fields/FieldRenderer.d.ts +2 -1
  74. package/dist/fields/FieldRenderer.d.ts.map +1 -1
  75. package/dist/fields/index.d.ts +0 -17
  76. package/dist/fields/index.d.ts.map +1 -1
  77. package/dist/fields/index.js +0 -15
  78. package/dist/fields/index.js.map +1 -1
  79. package/dist/layout/primitives/tokens.d.ts +0 -2
  80. package/dist/layout/primitives/tokens.d.ts.map +1 -1
  81. package/dist/layout/primitives/tokens.js +0 -6
  82. package/dist/layout/primitives/tokens.js.map +1 -1
  83. package/dist/lib/brand-voice-client.d.ts +14 -1
  84. package/dist/lib/brand-voice-client.d.ts.map +1 -1
  85. package/dist/lib/brand-voice-client.js +7 -0
  86. package/dist/lib/brand-voice-client.js.map +1 -1
  87. package/dist/lib/coauthor-client.d.ts +2 -1
  88. package/dist/lib/coauthor-client.d.ts.map +1 -1
  89. package/dist/lib/collection-schema.d.ts +0 -9
  90. package/dist/lib/collection-schema.d.ts.map +1 -1
  91. package/dist/lib/collection-schema.js +2 -2
  92. package/dist/lib/collection-schema.js.map +1 -1
  93. package/dist/lib/editor-seo-content.d.ts +0 -10
  94. package/dist/lib/editor-seo-content.d.ts.map +1 -1
  95. package/dist/lib/editor-seo-content.js +0 -13
  96. package/dist/lib/editor-seo-content.js.map +1 -1
  97. package/dist/lib/forms-service.d.ts +3 -21
  98. package/dist/lib/forms-service.d.ts.map +1 -1
  99. package/dist/lib/forms-service.js +0 -25
  100. package/dist/lib/forms-service.js.map +1 -1
  101. package/dist/lib/notifications-client.d.ts +4 -3
  102. package/dist/lib/notifications-client.d.ts.map +1 -1
  103. package/dist/lib/page-audit-input.d.ts +0 -10
  104. package/dist/lib/page-audit-input.d.ts.map +1 -1
  105. package/dist/lib/page-audit-input.js +1 -1
  106. package/dist/lib/page-audit-input.js.map +1 -1
  107. package/dist/lib/page-editor-service.d.ts +2 -4
  108. package/dist/lib/page-editor-service.d.ts.map +1 -1
  109. package/dist/lib/page-editor-service.js +3 -3
  110. package/dist/lib/page-editor-service.js.map +1 -1
  111. package/dist/lib/pages-service.d.ts +2 -13
  112. package/dist/lib/pages-service.d.ts.map +1 -1
  113. package/dist/lib/pages-service.js +3 -24
  114. package/dist/lib/pages-service.js.map +1 -1
  115. package/dist/lib/plan.d.ts +2 -1
  116. package/dist/lib/plan.d.ts.map +1 -1
  117. package/dist/lib/post-editor-service.d.ts +3 -13
  118. package/dist/lib/post-editor-service.d.ts.map +1 -1
  119. package/dist/lib/post-editor-service.js +5 -6
  120. package/dist/lib/post-editor-service.js.map +1 -1
  121. package/dist/lib/post-types-service.d.ts +2 -3
  122. package/dist/lib/post-types-service.d.ts.map +1 -1
  123. package/dist/lib/post-types-service.js +1 -1
  124. package/dist/lib/post-types-service.js.map +1 -1
  125. package/dist/lib/posts-service.d.ts +8 -1
  126. package/dist/lib/posts-service.d.ts.map +1 -1
  127. package/dist/lib/posts-service.js +6 -1
  128. package/dist/lib/posts-service.js.map +1 -1
  129. package/dist/lib/preview-link.d.ts +0 -9
  130. package/dist/lib/preview-link.d.ts.map +1 -1
  131. package/dist/lib/preview-link.js +0 -15
  132. package/dist/lib/preview-link.js.map +1 -1
  133. package/dist/lib/public-path.d.ts +21 -0
  134. package/dist/lib/public-path.d.ts.map +1 -0
  135. package/dist/lib/public-path.js +28 -0
  136. package/dist/lib/public-path.js.map +1 -0
  137. package/dist/lib/rail-width.d.ts +0 -1
  138. package/dist/lib/rail-width.d.ts.map +1 -1
  139. package/dist/lib/rail-width.js +1 -1
  140. package/dist/lib/rail-width.js.map +1 -1
  141. package/dist/lib/reflow-probe.d.ts +0 -67
  142. package/dist/lib/reflow-probe.d.ts.map +1 -1
  143. package/dist/lib/reflow-probe.js +4 -4
  144. package/dist/lib/reflow-probe.js.map +1 -1
  145. package/dist/lib/seo-service.d.ts +14 -32
  146. package/dist/lib/seo-service.d.ts.map +1 -1
  147. package/dist/lib/seo-service.js +7 -26
  148. package/dist/lib/seo-service.js.map +1 -1
  149. package/dist/lib/theme-chrome.d.ts +0 -1
  150. package/dist/lib/theme-chrome.d.ts.map +1 -1
  151. package/dist/lib/theme-chrome.js +1 -1
  152. package/dist/lib/theme-chrome.js.map +1 -1
  153. package/dist/lib/version-history.d.ts +2 -2
  154. package/dist/lib/version-history.d.ts.map +1 -1
  155. package/dist/lib/workflow-chrome.d.ts +0 -3
  156. package/dist/lib/workflow-chrome.d.ts.map +1 -1
  157. package/dist/lib/workflow-chrome.js +3 -3
  158. package/dist/lib/workflow-chrome.js.map +1 -1
  159. package/dist/views/Login.d.ts +2 -1
  160. package/dist/views/Login.d.ts.map +1 -1
  161. package/dist/views/MediaBrowser.d.ts.map +1 -1
  162. package/dist/views/MediaBrowser.js +3 -2
  163. package/dist/views/MediaBrowser.js.map +1 -1
  164. package/dist/views/PageEditor.d.ts +2 -1
  165. package/dist/views/PageEditor.d.ts.map +1 -1
  166. package/dist/views/Pages/PagesListView.d.ts +0 -1
  167. package/dist/views/Pages/PagesListView.d.ts.map +1 -1
  168. package/dist/views/Pages/PagesListView.js +1 -1
  169. package/dist/views/Pages/PagesListView.js.map +1 -1
  170. package/dist/views/Pages.d.ts +2 -1
  171. package/dist/views/Pages.d.ts.map +1 -1
  172. package/dist/views/Posts/PostsListView.d.ts.map +1 -1
  173. package/dist/views/Posts/PostsListView.js +1 -1
  174. package/dist/views/Posts/PostsListView.js.map +1 -1
  175. package/dist/views/Redirects.d.ts +2 -1
  176. package/dist/views/Redirects.d.ts.map +1 -1
  177. package/dist/views/page-editor/section-types.d.ts +2 -2
  178. package/dist/views/page-editor/section-types.d.ts.map +1 -1
  179. package/dist/views/page-editor/section-types.js +1 -1
  180. package/dist/views/page-editor/section-types.js.map +1 -1
  181. package/dist/views/page-editor/sections/admin-renderers.d.ts +2 -1
  182. package/dist/views/page-editor/sections/admin-renderers.d.ts.map +1 -1
  183. package/dist/views/page-editor/theme-chrome/index.d.ts +1 -6
  184. package/dist/views/page-editor/theme-chrome/index.d.ts.map +1 -1
  185. package/dist/views/page-editor/theme-chrome/index.js +1 -3
  186. package/dist/views/page-editor/theme-chrome/index.js.map +1 -1
  187. package/dist/views/page-editor/viewports.d.ts +1 -1
  188. package/dist/views/page-editor/viewports.d.ts.map +1 -1
  189. package/dist/views/page-editor/viewports.js +1 -1
  190. package/dist/views/page-editor/viewports.js.map +1 -1
  191. package/dist/views/settings/AISettingsTab.d.ts.map +1 -1
  192. package/dist/views/settings/AISettingsTab.js +13 -3
  193. package/dist/views/settings/AISettingsTab.js.map +1 -1
  194. package/dist/views/settings/BrandVoiceCard.d.ts +3 -0
  195. package/dist/views/settings/BrandVoiceCard.d.ts.map +1 -1
  196. package/dist/views/settings/BrandVoiceCard.js +2 -2
  197. package/dist/views/settings/BrandVoiceCard.js.map +1 -1
  198. package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.d.ts.map +1 -1
  199. package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.js +4 -1
  200. package/dist/views/settings/brand-voice/BrandVoiceAlignmentPanel.js.map +1 -1
  201. package/dist/views/settings/featureModel.d.ts +0 -3
  202. package/dist/views/settings/featureModel.d.ts.map +1 -1
  203. package/dist/views/settings/featureModel.js +2 -2
  204. package/dist/views/settings/featureModel.js.map +1 -1
  205. package/dist/views/settings/useAiSettings.d.ts +2 -2
  206. package/dist/views/settings/useAiSettings.d.ts.map +1 -1
  207. package/dist/views/settings/useAiSettings.js +6 -2
  208. package/dist/views/settings/useAiSettings.js.map +1 -1
  209. package/dist/views/settings/useAppearanceSettings.d.ts +5 -6
  210. package/dist/views/settings/useAppearanceSettings.d.ts.map +1 -1
  211. package/dist/views/settings/useAppearanceSettings.js +1 -1
  212. package/dist/views/settings/useAppearanceSettings.js.map +1 -1
  213. package/dist/views/settings/useEmailSettings.d.ts +3 -2
  214. package/dist/views/settings/useEmailSettings.d.ts.map +1 -1
  215. package/dist/views/settings/useGeneralSettings.d.ts +2 -11
  216. package/dist/views/settings/useGeneralSettings.d.ts.map +1 -1
  217. package/dist/views/settings/useGeneralSettings.js +3 -3
  218. package/dist/views/settings/useGeneralSettings.js.map +1 -1
  219. package/dist/views/settings/useSeoSettings.d.ts +1 -1
  220. package/dist/views/settings/useSeoSettings.d.ts.map +1 -1
  221. package/dist/views/settings/useUpdates.d.ts +1 -3
  222. package/dist/views/settings/useUpdates.d.ts.map +1 -1
  223. package/dist/views/settings/useUpdates.js +1 -1
  224. package/dist/views/settings/useUpdates.js.map +1 -1
  225. package/package.json +4 -4
  226. package/src/__tests__/lib/posts-service.test.ts +35 -0
  227. package/src/__tests__/lib/public-path.test.ts +37 -0
  228. package/src/__tests__/views/ai-settings.render.test.tsx +86 -2
  229. package/src/__tests__/views/posts-list-view.render.test.tsx +12 -0
  230. package/src/__tests__/views/seo-autopilot-card.render.test.tsx +4 -4
  231. package/src/components/CoauthorResultPopover.tsx +3 -1
  232. package/src/components/CommentSidePanel.tsx +0 -1
  233. package/src/components/EditorRail.tsx +1 -1
  234. package/src/components/MediaPickerModal.tsx +1 -1
  235. package/src/components/TipTapEditor.tsx +2 -2
  236. package/src/components/forms/EntriesTable.tsx +1 -1
  237. package/src/components/pages/tag-badges.tsx +1 -1
  238. package/src/components/seo/FaqProposalPreview.tsx +2 -2
  239. package/src/components/seo/SeoCopilotDrawer.tsx +1 -1
  240. package/src/components/seo/SeoEditorPane.tsx +3 -1
  241. package/src/components/ui/Badge.tsx +4 -6
  242. package/src/components/ui/Button.tsx +1 -3
  243. package/src/components/ui/Card.tsx +1 -14
  244. package/src/components/ui/Input.tsx +7 -15
  245. package/src/components/ui/Modal.tsx +1 -1
  246. package/src/components/ui/ResizableAside.tsx +5 -5
  247. package/src/components/ui/StatusBadge.tsx +1 -3
  248. package/src/components/ui/Toggle.tsx +1 -3
  249. package/src/fields/FieldRenderer.tsx +1 -1
  250. package/src/fields/index.ts +0 -17
  251. package/src/layout/primitives/tokens.ts +0 -7
  252. package/src/lib/brand-voice-client.ts +18 -1
  253. package/src/lib/coauthor-client.ts +1 -1
  254. package/src/lib/collection-schema.ts +2 -2
  255. package/src/lib/editor-seo-content.ts +0 -22
  256. package/src/lib/forms-service.ts +2 -42
  257. package/src/lib/notifications-client.ts +3 -3
  258. package/src/lib/page-audit-input.ts +1 -1
  259. package/src/lib/page-editor-service.ts +1 -3
  260. package/src/lib/pages-service.ts +4 -29
  261. package/src/lib/plan.ts +1 -1
  262. package/src/lib/post-editor-service.ts +4 -10
  263. package/src/lib/post-types-service.ts +2 -2
  264. package/src/lib/posts-service.ts +13 -2
  265. package/src/lib/preview-link.ts +0 -14
  266. package/src/lib/public-path.ts +33 -0
  267. package/src/lib/rail-width.ts +1 -1
  268. package/src/lib/reflow-probe.ts +4 -4
  269. package/src/lib/seo-service.ts +20 -60
  270. package/src/lib/theme-chrome.ts +1 -1
  271. package/src/lib/version-history.ts +2 -2
  272. package/src/lib/workflow-chrome.ts +3 -3
  273. package/src/views/Login.tsx +1 -1
  274. package/src/views/MediaBrowser.tsx +3 -2
  275. package/src/views/PageEditor.tsx +1 -1
  276. package/src/views/Pages/PagesListView.tsx +1 -1
  277. package/src/views/Pages.tsx +1 -1
  278. package/src/views/Posts/PostsListView.tsx +2 -1
  279. package/src/views/Redirects.tsx +1 -1
  280. package/src/views/page-editor/section-types.ts +0 -9
  281. package/src/views/page-editor/sections/admin-renderers.tsx +1 -1
  282. package/src/views/page-editor/theme-chrome/index.ts +2 -10
  283. package/src/views/page-editor/viewports.ts +0 -3
  284. package/src/views/settings/AISettingsTab.tsx +15 -3
  285. package/src/views/settings/BrandVoiceCard.tsx +11 -1
  286. package/src/views/settings/brand-voice/BrandVoiceAlignmentPanel.tsx +3 -1
  287. package/src/views/settings/featureModel.ts +2 -2
  288. package/src/views/settings/useAiSettings.ts +8 -3
  289. package/src/views/settings/useAppearanceSettings.ts +4 -6
  290. package/src/views/settings/useEmailSettings.ts +2 -2
  291. package/src/views/settings/useGeneralSettings.ts +5 -10
  292. package/src/views/settings/useSeoSettings.ts +1 -1
  293. package/src/views/settings/useUpdates.ts +2 -2
@@ -1,7 +1,11 @@
1
- import { forwardRef, type InputHTMLAttributes } from 'react'
2
- import { cv, type VariantProps } from '../../lib/cv.js'
1
+ import { cv } from '../../lib/cv.js'
3
2
  import { formControlBase, formControlState } from './form-control.js'
4
3
 
4
+ /**
5
+ * Input class-variance recipe shared by the header search and `SearchInput`.
6
+ * Consumers render their own `<input>` with `inputVariants({ size, state })`;
7
+ * there is no wrapper component.
8
+ */
5
9
  const input = cv(formControlBase, {
6
10
  variants: {
7
11
  size: {
@@ -18,16 +22,4 @@ const input = cv(formControlBase, {
18
22
  defaultVariants: { size: 'md', state: 'default' },
19
23
  })
20
24
 
21
- export type InputVariants = VariantProps<typeof input>
22
-
23
- export interface InputProps
24
- extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>, InputVariants {}
25
-
26
- export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
27
- { size, state, className, ...rest },
28
- ref,
29
- ) {
30
- return <input ref={ref} className={input({ size, state, class: className })} {...rest} />
31
- })
32
-
33
- export { input as inputVariants, formControlBase, formControlState }
25
+ export { input as inputVariants }
@@ -12,7 +12,7 @@ const MODAL_SIZE_CLASS = {
12
12
  xl: 'flex max-h-[min(92vh,880px)] max-w-5xl flex-col',
13
13
  } as const
14
14
 
15
- export type ModalSize = keyof typeof MODAL_SIZE_CLASS
15
+ type ModalSize = keyof typeof MODAL_SIZE_CLASS
16
16
 
17
17
  export interface ModalProps {
18
18
  open: boolean
@@ -17,11 +17,11 @@ import { Maximize2, Minimize2 } from 'lucide-react'
17
17
  export const SECTION_INSPECTOR_WIDTH_KEY = 'prenta:section-inspector-width'
18
18
  export const SECTION_INSPECTOR_DEFAULT_WIDTH = 320
19
19
  export const SECTION_INSPECTOR_MIN_WIDTH = 280
20
- export const SECTION_INSPECTOR_MAX_WIDTH = 640
21
- export const SECTION_INSPECTOR_MIN_CANVAS_WIDTH = 96
20
+ const SECTION_INSPECTOR_MAX_WIDTH = 640
21
+ const SECTION_INSPECTOR_MIN_CANVAS_WIDTH = 96
22
22
  /** Below this editor-row width the expand action uses an overlay panel. */
23
- export const SECTION_INSPECTOR_OVERLAY_BREAKPOINT = 768
24
- export const SECTION_INSPECTOR_OVERLAY_MAX_WIDTH = 480
23
+ const SECTION_INSPECTOR_OVERLAY_BREAKPOINT = 768
24
+ const SECTION_INSPECTOR_OVERLAY_MAX_WIDTH = 480
25
25
 
26
26
  function clampWidth(width: number, max = SECTION_INSPECTOR_MAX_WIDTH): number {
27
27
  return Math.min(max, Math.max(SECTION_INSPECTOR_MIN_WIDTH, width))
@@ -81,7 +81,7 @@ interface InspectorResizeContextValue {
81
81
 
82
82
  const InspectorResizeContext = createContext<InspectorResizeContextValue | null>(null)
83
83
 
84
- export function useInspectorResize(): InspectorResizeContextValue | null {
84
+ function useInspectorResize(): InspectorResizeContextValue | null {
85
85
  return useContext(InspectorResizeContext)
86
86
  }
87
87
 
@@ -40,7 +40,7 @@ const DOT_CLASS: Record<NonNullable<StatusBadgeVariants['tone']>, string> = {
40
40
  muted: 'bg-[var(--muted)]',
41
41
  }
42
42
 
43
- export type StatusBadgeVariants = VariantProps<typeof statusBadge>
43
+ type StatusBadgeVariants = VariantProps<typeof statusBadge>
44
44
  export type StatusBadgeTone = NonNullable<StatusBadgeVariants['tone']>
45
45
 
46
46
  export interface StatusBadgeProps extends HTMLAttributes<HTMLSpanElement>, StatusBadgeVariants {
@@ -62,5 +62,3 @@ export function StatusBadge({ tone, dot = true, className, children, ...rest }:
62
62
  </span>
63
63
  )
64
64
  }
65
-
66
- export { statusBadge as statusBadgeVariants }
@@ -40,7 +40,7 @@ const toggleThumb = cv(
40
40
  },
41
41
  )
42
42
 
43
- export type ToggleVariants = VariantProps<typeof toggleTrack>
43
+ type ToggleVariants = VariantProps<typeof toggleTrack>
44
44
 
45
45
  export interface ToggleProps extends ToggleVariants {
46
46
  checked: boolean
@@ -82,5 +82,3 @@ export function Toggle({
82
82
  </Switch.Root>
83
83
  )
84
84
  }
85
-
86
- export { toggleTrack as toggleTrackVariants, toggleThumb as toggleThumbVariants }
@@ -15,7 +15,7 @@ import { NavBuilderField } from './NavBuilderField.js'
15
15
  import { NumberField } from './NumberField.js'
16
16
  import { ComponentBlockField } from './ComponentBlockField.js'
17
17
 
18
- export interface FieldDefinition {
18
+ interface FieldDefinition {
19
19
  name: string
20
20
  type: string
21
21
  label: string
@@ -1,20 +1,3 @@
1
- export { FieldRenderer } from './FieldRenderer.js'
2
- export type { FieldRendererProps, FieldDefinition } from './FieldRenderer.js'
3
- export { TextField } from './TextField.js'
4
- export { RichTextField } from './RichTextField.js'
5
- export { SlugField } from './SlugField.js'
6
- export { SelectField } from './SelectField.js'
7
- export { MediaField } from './MediaField.js'
8
- export { RelationshipField } from './RelationshipField.js'
9
- export { DateField } from './DateField.js'
10
- export { ToggleField } from './ToggleField.js'
11
- export { ArrayField } from './ArrayField.js'
12
- export { BlockBuilderField } from './BlockBuilderField.js'
13
- export { PRESET_BLOCKS } from './block-types.js'
14
- export type { BlockTypeDefinition } from './block-types.js'
15
- export { GroupField } from './GroupField.js'
16
- export { NavBuilderField } from './NavBuilderField.js'
17
- export { NumberField } from './NumberField.js'
18
1
  export { ComponentBlockField } from './ComponentBlockField.js'
19
2
  export type { ComponentBlockFieldProps, ComponentBlockValue } from './ComponentBlockField.js'
20
3
  export { PropInput, defaultForType } from './PropInput.js'
@@ -74,10 +74,3 @@ export const tokens = {
74
74
  '2xl': 1536,
75
75
  } as const,
76
76
  }
77
-
78
- /** Map a space token to the corresponding Tailwind `space-y-*` / `gap-*` value. */
79
- export function spaceToTw(t: SpaceToken): string {
80
- // Tailwind's spacing utility already accepts the same scale, so we map
81
- // 1:1. Centralizing here means callers don't repeat the string literal.
82
- return t
83
- }
@@ -1,7 +1,16 @@
1
1
  import type { BrandAlignmentScoreResult } from '@prenta/core/brand-voice'
2
2
  import { cmsApi } from './api.js'
3
3
 
4
- export type { BrandAlignmentScoreResult }
4
+ /**
5
+ * Present only when an AI rewrite was wanted (`useAi`, remaining issues) but
6
+ * the governed runtime refused — `code` is the runtime refusal code and `hint`
7
+ * the operator-facing next step for the 503 "not configured" cases.
8
+ */
9
+ interface BrandVoiceFixAiUnavailable {
10
+ code: string
11
+ message: string
12
+ hint?: string
13
+ }
5
14
 
6
15
  export interface BrandVoiceFixResult {
7
16
  text: string
@@ -9,6 +18,14 @@ export interface BrandVoiceFixResult {
9
18
  beforeScore: BrandAlignmentScoreResult
10
19
  afterScore: BrandAlignmentScoreResult
11
20
  usedAi: boolean
21
+ aiUnavailable?: BrandVoiceFixAiUnavailable
22
+ }
23
+
24
+ /** Toast copy for a fix that fell back to rule-only edits because AI refused. */
25
+ export function brandVoiceFixAiNotice(result: BrandVoiceFixResult): string | null {
26
+ if (result.usedAi || !result.aiUnavailable) return null
27
+ const { message, hint } = result.aiUnavailable
28
+ return hint ? `${message} ${hint}` : message
12
29
  }
13
30
 
14
31
  export async function fixBrandVoiceContent(input: {
@@ -64,7 +64,7 @@ export interface CoauthorOptions {
64
64
  targetLength?: number
65
65
  }
66
66
 
67
- export interface CoauthorResult {
67
+ interface CoauthorResult {
68
68
  action: CoauthorAction
69
69
  text: string
70
70
  /** Populated only when `action === 'proofread'`. */
@@ -19,7 +19,7 @@ interface CollectionLike {
19
19
  }
20
20
 
21
21
  /** Look up a collection by slug; `collections` may be a record or an array. */
22
- export function findCollection(config: unknown, slug: string): CollectionLike | null {
22
+ function findCollection(config: unknown, slug: string): CollectionLike | null {
23
23
  const collections = (config as { collections?: unknown } | null | undefined)?.collections
24
24
  if (!collections) return null
25
25
  if (Array.isArray(collections)) {
@@ -32,7 +32,7 @@ export function findCollection(config: unknown, slug: string): CollectionLike |
32
32
  }
33
33
 
34
34
  /** Field definitions may be a record (keyed by name) or an array of `{ name }`. */
35
- export function declaredFieldNames(fields: unknown): Set<string> | null {
35
+ function declaredFieldNames(fields: unknown): Set<string> | null {
36
36
  if (!fields) return null
37
37
  if (Array.isArray(fields)) {
38
38
  return new Set(
@@ -3,7 +3,6 @@ import {
3
3
  isSectionEffectivelyVisible,
4
4
  type PageSection,
5
5
  } from '@prenta/core/page-sections'
6
- import { analyzeContent } from '@prenta/core/seo/analysis'
7
6
 
8
7
  const HTML_LIKE = /<\/?[a-z][\s\S]*>/i
9
8
 
@@ -19,27 +18,6 @@ export function buildEditorContentHtml(sections: PageSection[], body = ''): stri
19
18
  return parts.join('\n')
20
19
  }
21
20
 
22
- /** Live 0–100 SEO score from the current in-editor document state. */
23
- export function computeLiveSeoScore(input: {
24
- title: string
25
- slug: string
26
- seoTitle: string
27
- seoDescription: string
28
- focusKeyword?: string
29
- sections: PageSection[]
30
- bodyHtml?: string
31
- }): number {
32
- const content = buildEditorContentHtml(input.sections, input.bodyHtml ?? '')
33
- return analyzeContent({
34
- title: input.title,
35
- slug: input.slug,
36
- content,
37
- metaTitle: input.seoTitle,
38
- metaDescription: input.seoDescription,
39
- focusKeyphrase: input.focusKeyword,
40
- }).score
41
- }
42
-
43
21
  function extractSectionContentHtml(section: PageSection): string {
44
22
  const content = section.content
45
23
  if (!content || typeof content !== 'object') return ''
@@ -34,21 +34,16 @@ export type {
34
34
  FormSchemaVersion,
35
35
  FormField,
36
36
  FormFieldType,
37
- FormFieldOption,
38
- FieldValidationRules,
39
37
  FormAnalyticsConfig,
40
38
  FormNotificationSettings,
41
39
  FormEmbedSettings,
42
40
  FormWebhookConfig,
43
- FormWebhookEvent,
44
41
  FormStatus,
45
42
  FormStats,
46
43
  FormsSidebarCount,
47
44
  FormSubmission,
48
- FormSubmissionFile,
49
45
  FetchFormEntriesParams,
50
46
  EntryCounts,
51
- SpamStatus,
52
47
  } from '@prenta/core'
53
48
 
54
49
  /**
@@ -170,34 +165,6 @@ export async function updateForm(
170
165
  return { data: res.data, error: res.error }
171
166
  }
172
167
 
173
- export async function duplicateForm(
174
- id: string,
175
- ): Promise<{ data?: FormDefinition; error?: string }> {
176
- const res = await cmsApi<FormDefinition>(`/forms/${encodeURIComponent(id)}/duplicate`, {
177
- method: 'POST',
178
- })
179
- return { data: res.data, error: res.error }
180
- }
181
-
182
- /** Move a form to the archived state (hidden from embed pickers, no new submissions). */
183
- export async function archiveForm(id: string): Promise<{ error?: string }> {
184
- return updateForm(id, { status: 'archived' })
185
- }
186
-
187
- /** Restore an archived form back to draft. */
188
- export async function restoreForm(id: string): Promise<{ error?: string }> {
189
- return updateForm(id, { status: 'draft' })
190
- }
191
-
192
- export async function deleteForm(
193
- id: string,
194
- opts: { force?: boolean } = {},
195
- ): Promise<{ error?: string }> {
196
- const qs = opts.force ? '?force=true' : ''
197
- const res = await cmsApi(`/forms/${encodeURIComponent(id)}${qs}`, { method: 'DELETE' })
198
- return { error: res.error }
199
- }
200
-
201
168
  // ─── Schema ──────────────────────────────────────────────────────────────
202
169
 
203
170
  export async function fetchFormSchema(id: string): Promise<FormSchemaResult> {
@@ -496,8 +463,8 @@ export async function downloadEntriesExport(
496
463
 
497
464
  // ─── Delivery attempts (#675) ─────────────────────────────────────────────
498
465
 
499
- export type FormDeliveryChannel = 'email_notify' | 'email_autoresponder' | 'form_webhook'
500
- export type FormDeliveryStatus = 'pending' | 'retrying' | 'success' | 'failed' | 'skipped'
466
+ type FormDeliveryChannel = 'email_notify' | 'email_autoresponder' | 'form_webhook'
467
+ type FormDeliveryStatus = 'pending' | 'retrying' | 'success' | 'failed' | 'skipped'
501
468
 
502
469
  export interface FormDeliveryAttempt {
503
470
  id: string
@@ -560,10 +527,3 @@ export async function retryFormDelivery(
560
527
  )
561
528
  return { data: res.data, error: res.error }
562
529
  }
563
-
564
- export async function fetchFailedDeliveryCount(formId?: string): Promise<number> {
565
- const qs = formId ? `?formId=${encodeURIComponent(formId)}` : ''
566
- const res = await cmsApi<{ count: number }>(`/forms/deliveries/failed-count${qs}`)
567
- throwIfError(res)
568
- return res.data?.count ?? 0
569
- }
@@ -15,21 +15,21 @@ import { cmsApi, getApiBase } from './api.js'
15
15
 
16
16
  export type NotificationKind = 'comment_reply' | 'comment_resolved' | 'comment_mention'
17
17
 
18
- export interface CommentReplyPayload {
18
+ interface CommentReplyPayload {
19
19
  kind: 'comment_reply'
20
20
  actorId: string | null
21
21
  actorName: string | null
22
22
  excerpt: string
23
23
  }
24
24
 
25
- export interface CommentResolvedPayload {
25
+ interface CommentResolvedPayload {
26
26
  kind: 'comment_resolved'
27
27
  actorId: string | null
28
28
  actorName: string | null
29
29
  rootExcerpt: string
30
30
  }
31
31
 
32
- export interface CommentMentionPayload {
32
+ interface CommentMentionPayload {
33
33
  kind: 'comment_mention'
34
34
  actorId: string | null
35
35
  actorName: string | null
@@ -78,7 +78,7 @@ function stripHtml(html: string): string {
78
78
  * when one is set — otherwise `imageSlots` would count an empty media field
79
79
  * and fail alt-text for an image that is not on the page.
80
80
  */
81
- export function withPostAuditSlots(sections: PageSection[], post?: PostAuditExtras): PageSection[] {
81
+ function withPostAuditSlots(sections: PageSection[], post?: PostAuditExtras): PageSection[] {
82
82
  if (!post) return sections
83
83
 
84
84
  const extras: PageSection[] = []
@@ -27,7 +27,6 @@ import {
27
27
  moveSection,
28
28
  reorderSections,
29
29
  updateSectionContent,
30
- updateSectionContentAtPath,
31
30
  updateSectionSettings,
32
31
  updateSectionMeta,
33
32
  coerceSections as coerceSectionsCore,
@@ -55,7 +54,6 @@ export {
55
54
  moveSection,
56
55
  reorderSections,
57
56
  updateSectionContent,
58
- updateSectionContentAtPath,
59
57
  updateSectionSettings,
60
58
  updateSectionMeta,
61
59
  }
@@ -278,7 +276,7 @@ function toWriteBody(page: EditorPage, extra?: Record<string, unknown>): string
278
276
  }
279
277
 
280
278
  /** Create a new page document, returning the new id. */
281
- export async function createPage(page: EditorPage): Promise<EditorPage> {
279
+ async function createPage(page: EditorPage): Promise<EditorPage> {
282
280
  const res = await cmsApi<DocumentApiRow>(`/collections/${PAGES_COLLECTION}`, {
283
281
  method: 'POST',
284
282
  body: toWriteBody(page),
@@ -30,8 +30,8 @@ import { cmsApi } from './api.js'
30
30
 
31
31
  export type PageStatus = 'DRAFT' | 'PUBLISHED' | 'SCHEDULED' | 'ARCHIVED'
32
32
 
33
- export const PAGES_COLLECTION = 'pages'
34
- export const PAGE_TAGS_COLLECTION = 'page-tags'
33
+ const PAGES_COLLECTION = 'pages'
34
+ const PAGE_TAGS_COLLECTION = 'page-tags'
35
35
 
36
36
  export interface PageTag {
37
37
  id: string // == slug (stable join key; pages store slugs in data.tags)
@@ -47,7 +47,7 @@ export interface PageTag {
47
47
  updatedAt: string | null
48
48
  }
49
49
 
50
- export interface PageAuthor {
50
+ interface PageAuthor {
51
51
  id: string
52
52
  name: string
53
53
  initials: string
@@ -222,22 +222,6 @@ async function fetchTagDocs(): Promise<DocumentApiRow[]> {
222
222
  return (res.data?.docs ?? []) as DocumentApiRow[]
223
223
  }
224
224
 
225
- /**
226
- * Fetch all tags. `pageCounts` (slug → count) is optional so the tag
227
- * manager / pickers can render before page data is loaded.
228
- */
229
- export async function fetchPageTags(pageCounts?: Map<string, number>): Promise<PageTag[]> {
230
- const docs = await fetchTagDocs()
231
- const tags = docs.map((d) => docToTag(d, pageCounts?.get(d.slug ?? d.id) ?? 0))
232
- tags.sort((a, b) => a.sortOrder - b.sortOrder || a.name.localeCompare(b.name))
233
- return tags
234
- }
235
-
236
- export async function fetchAuthors(): Promise<PageAuthor[]> {
237
- const map = await fetchAuthorsMap()
238
- return [...map.values()].sort((a, b) => a.name.localeCompare(b.name))
239
- }
240
-
241
225
  // ─── Pages ───────────────────────────────────────────────────────────
242
226
 
243
227
  function docToPage(d: DocumentApiRow, authors: Map<string, PageAuthor>): Page {
@@ -439,15 +423,6 @@ export async function fetchPages(query: PagesQuery = {}): Promise<PagesResult> {
439
423
  }
440
424
  }
441
425
 
442
- export async function fetchPageById(pageId: string): Promise<Page | null> {
443
- const res = await cmsApi<DocumentApiRow>(
444
- `/collections/${PAGES_COLLECTION}/${encodeURIComponent(pageId)}`,
445
- )
446
- if (res.error || !res.data) return null
447
- const authors = await fetchAuthorsMap()
448
- return docToPage(res.data, authors)
449
- }
450
-
451
426
  // ─── Bulk page mutations ─────────────────────────────────────────────
452
427
 
453
428
  export interface BulkResult {
@@ -621,7 +596,7 @@ export async function updatePageTag(
621
596
  return res.error ? { error: res.error } : {}
622
597
  }
623
598
 
624
- export async function deletePageTag(tagDocId: string): Promise<{ error?: string }> {
599
+ async function deletePageTag(tagDocId: string): Promise<{ error?: string }> {
625
600
  const res = await cmsApi(`/collections/${PAGE_TAGS_COLLECTION}/${encodeURIComponent(tagDocId)}`, {
626
601
  method: 'DELETE',
627
602
  })
package/src/lib/plan.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { cmsApi } from './api.js'
2
2
 
3
- export type PlanTier = 'starter' | 'pro' | 'enterprise'
3
+ type PlanTier = 'starter' | 'pro' | 'enterprise'
4
4
 
5
5
  export type PlanFeature =
6
6
  | 'seo.inlineApply'
@@ -30,10 +30,8 @@ import {
30
30
  removeSection,
31
31
  duplicateSection,
32
32
  toggleSectionVisibility,
33
- moveSection,
34
33
  reorderSections,
35
34
  updateSectionContent,
36
- updateSectionContentAtPath,
37
35
  updateSectionSettings,
38
36
  updateSectionMeta,
39
37
  coerceSections,
@@ -56,17 +54,13 @@ export {
56
54
  removeSection,
57
55
  duplicateSection,
58
56
  toggleSectionVisibility,
59
- moveSection,
60
57
  reorderSections,
61
58
  updateSectionContent,
62
- updateSectionContentAtPath,
63
59
  updateSectionSettings,
64
60
  updateSectionMeta,
65
61
  }
66
62
 
67
- export type PostStatus = 'DRAFT' | 'PUBLISHED' | 'SCHEDULED' | 'ARCHIVED'
68
-
69
- export const TEMPLATES_COLLECTION = 'post-templates'
63
+ type PostStatus = 'DRAFT' | 'PUBLISHED' | 'SCHEDULED' | 'ARCHIVED'
70
64
 
71
65
  export interface EditorPost {
72
66
  id: string | null
@@ -230,7 +224,7 @@ export interface FeaturedImageRef {
230
224
  }
231
225
 
232
226
  /** True for paths and http(s) URLs — never a bare media cuid. */
233
- export function isDisplayableImageSrc(value: string): boolean {
227
+ function isDisplayableImageSrc(value: string): boolean {
234
228
  const v = value.trim()
235
229
  return v.startsWith('/') || /^https?:\/\//i.test(v)
236
230
  }
@@ -272,7 +266,7 @@ export function parseFeaturedImage(raw: unknown): string {
272
266
  * `[{ tag: 'AI' }]` rows; older/hand-rolled configs may store `['AI']`
273
267
  * directly — accept both so no site loses tags in the editor.
274
268
  */
275
- export function parseTags(raw: unknown): string[] {
269
+ function parseTags(raw: unknown): string[] {
276
270
  if (!Array.isArray(raw)) return []
277
271
  const out: string[] = []
278
272
  for (const item of raw) {
@@ -435,7 +429,7 @@ function toWriteBody(post: EditorPost, extra?: Record<string, unknown>): string
435
429
  return JSON.stringify(payload)
436
430
  }
437
431
 
438
- export async function createPost(post: EditorPost): Promise<EditorPost> {
432
+ async function createPost(post: EditorPost): Promise<EditorPost> {
439
433
  const res = await cmsApi<DocumentApiRow>(`/collections/${encodeURIComponent(post.postType)}`, {
440
434
  method: 'POST',
441
435
  body: toWriteBody(post),
@@ -6,7 +6,7 @@ import { cmsApi } from './api.js'
6
6
  * schemas are file-defined in `prenta.config.ts`, so the Post Type editor
7
7
  * inspects the live schema and generates config snippets for changes.
8
8
  */
9
- export interface PostTypeFieldSummary {
9
+ interface PostTypeFieldSummary {
10
10
  name: string
11
11
  type: string
12
12
  required: boolean
@@ -57,7 +57,7 @@ export async function fetchPostTypeDetail(slug: string): Promise<PostTypeDetail
57
57
  }
58
58
 
59
59
  /** Friendly display names for cms-core field types (design's type badges). */
60
- export const FIELD_TYPE_LABELS: Record<string, string> = {
60
+ const FIELD_TYPE_LABELS: Record<string, string> = {
61
61
  text: 'Short Text',
62
62
  richText: 'Rich Text',
63
63
  number: 'Number',
@@ -13,6 +13,7 @@
13
13
  * collections the caller's scope can read.
14
14
  */
15
15
  import { cmsApi } from './api.js'
16
+ import { publicPathFor } from './public-path.js'
16
17
 
17
18
  export type PostStatus = 'DRAFT' | 'PUBLISHED' | 'SCHEDULED' | 'ARCHIVED'
18
19
 
@@ -30,7 +31,7 @@ export interface PostType {
30
31
  active: boolean
31
32
  }
32
33
 
33
- export interface PostAuthor {
34
+ interface PostAuthor {
34
35
  id: string
35
36
  name: string
36
37
  initials: string
@@ -40,6 +41,12 @@ export interface Post {
40
41
  id: string
41
42
  title: string
42
43
  slug: string
44
+ /**
45
+ * Public path (`/{urlPrefix}/{slug}`) built from the collection's
46
+ * `urlPrefix`, falling back to the collection slug when no prefix is known.
47
+ * `postTypeId` stays the API identity — never render that as the URL.
48
+ */
49
+ publicPath: string
43
50
  postTypeId: string
44
51
  postTypeName: string
45
52
  postTypeAccentColor: string | null
@@ -229,10 +236,14 @@ export async function fetchPosts(query: PostsQuery = {}): Promise<PostsResult> {
229
236
  const posts: Post[] = r.posts.map((d) => {
230
237
  const type = typeBySlug.get(d.collection)
231
238
  const authorName = d.author?.name?.trim() || 'Unknown'
239
+ const slug = d.slug ?? ''
232
240
  return {
233
241
  id: d.id,
234
242
  title: d.title ?? '(untitled)',
235
- slug: d.slug ?? '',
243
+ slug,
244
+ // The server resolves `urlPrefix` from the running config (`''` for a
245
+ // root-mounted collection); a missing type row falls back to the slug.
246
+ publicPath: publicPathFor(type?.urlPrefix, d.collection, slug),
236
247
  postTypeId: d.collection,
237
248
  postTypeName: type?.singular ?? d.collection,
238
249
  postTypeAccentColor: type?.color ?? null,
@@ -101,20 +101,6 @@ export function buildPublicUrl({
101
101
  return resolveDocumentUrl({ siteUrl, urlPrefix, slug, path, data, trailingSlash })
102
102
  }
103
103
 
104
- /**
105
- * Resolve the public site origin for link building.
106
- *
107
- * Prefer an explicit URL (effective public apex from `/site/public-url` or
108
- * `config.seo.siteUrl`). Falling back to `window.location.origin` is a last
109
- * resort — on Astro-split installs the admin tab is the Next CMS host, so that
110
- * fallback opens the wrong site. Callers should prefer {@link usePublicSiteUrl}.
111
- */
112
- export function resolveSiteUrl(configSiteUrl?: string | null): string {
113
- if (configSiteUrl && configSiteUrl.trim()) return configSiteUrl.trim().replace(/\/+$/, '')
114
- if (typeof window !== 'undefined') return window.location.origin
115
- return ''
116
- }
117
-
118
104
  /** True when `siteUrl` shares an origin with the admin tab (split-deploy smell). */
119
105
  export function siteUrlMatchesAdminOrigin(
120
106
  siteUrl: string | null | undefined,
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Public path helpers for list-row subtitles.
3
+ *
4
+ * A collection's slug (`posts`) is its API / cache-tag / MCP identity; its
5
+ * `urlPrefix` (`blog`) is where the public site serves it. Editors read the
6
+ * subtitle as the live URL, so it must be built from the prefix (#921).
7
+ */
8
+
9
+ function trimSlashes(value: string): string {
10
+ return value.replace(/^\/+|\/+$/g, '')
11
+ }
12
+
13
+ /**
14
+ * Build the public path for a document.
15
+ *
16
+ * - `urlPrefix` non-empty → `/{urlPrefix}/{slug}` (nested prefixes such as
17
+ * `help/articles` are kept; stray slashes are trimmed).
18
+ * - `urlPrefix === ''` → `/{slug}` (collection mounted at the site root).
19
+ * - `urlPrefix === undefined` → `/{collectionSlug}/{slug}` (no prefix known;
20
+ * the collection slug is the best available guess).
21
+ *
22
+ * An empty `slug` yields a trailing slash (`/blog/`) so callers can append a
23
+ * placeholder.
24
+ */
25
+ export function publicPathFor(
26
+ urlPrefix: string | undefined,
27
+ collectionSlug: string,
28
+ slug: string,
29
+ ): string {
30
+ const prefix = trimSlashes(urlPrefix ?? collectionSlug)
31
+ const base = prefix ? `/${prefix}` : ''
32
+ return `${base}/${trimSlashes(slug)}`
33
+ }
@@ -10,7 +10,7 @@ export const RAIL_DEFAULT_WIDTH = 360
10
10
  /** Width of the strip that replaces the rail when it is collapsed. */
11
11
  export const RAIL_COLLAPSED_WIDTH = 38
12
12
 
13
- export const RAIL_WIDTH_STORAGE_KEY = 'pb.panelW'
13
+ const RAIL_WIDTH_STORAGE_KEY = 'pb.panelW'
14
14
 
15
15
  /** Constrain a candidate width to the rail's range, rejecting junk. */
16
16
  export function clampRailWidth(width: number): number {