@byline/cli 0.1.1

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 (251) hide show
  1. package/LICENSE +373 -0
  2. package/README.md +23 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +72 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/commands/doctor.d.ts +2 -0
  8. package/dist/commands/doctor.d.ts.map +1 -0
  9. package/dist/commands/doctor.js +36 -0
  10. package/dist/commands/doctor.js.map +1 -0
  11. package/dist/commands/init.d.ts +16 -0
  12. package/dist/commands/init.d.ts.map +1 -0
  13. package/dist/commands/init.js +76 -0
  14. package/dist/commands/init.js.map +1 -0
  15. package/dist/context.d.ts +38 -0
  16. package/dist/context.d.ts.map +1 -0
  17. package/dist/context.js +37 -0
  18. package/dist/context.js.map +1 -0
  19. package/dist/index.d.ts +5 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +4 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/lib/pg-url.d.ts +11 -0
  24. package/dist/lib/pg-url.d.ts.map +1 -0
  25. package/dist/lib/pg-url.js +22 -0
  26. package/dist/lib/pg-url.js.map +1 -0
  27. package/dist/manifest/deps.d.ts +29 -0
  28. package/dist/manifest/deps.d.ts.map +1 -0
  29. package/dist/manifest/deps.js +97 -0
  30. package/dist/manifest/deps.js.map +1 -0
  31. package/dist/manifest/env.d.ts +18 -0
  32. package/dist/manifest/env.d.ts.map +1 -0
  33. package/dist/manifest/env.js +38 -0
  34. package/dist/manifest/env.js.map +1 -0
  35. package/dist/phases/db-init.d.ts +3 -0
  36. package/dist/phases/db-init.d.ts.map +1 -0
  37. package/dist/phases/db-init.js +163 -0
  38. package/dist/phases/db-init.js.map +1 -0
  39. package/dist/phases/db.d.ts +11 -0
  40. package/dist/phases/db.d.ts.map +1 -0
  41. package/dist/phases/db.js +93 -0
  42. package/dist/phases/db.js.map +1 -0
  43. package/dist/phases/deps.d.ts +3 -0
  44. package/dist/phases/deps.d.ts.map +1 -0
  45. package/dist/phases/deps.js +115 -0
  46. package/dist/phases/deps.js.map +1 -0
  47. package/dist/phases/env.d.ts +3 -0
  48. package/dist/phases/env.d.ts.map +1 -0
  49. package/dist/phases/env.js +172 -0
  50. package/dist/phases/env.js.map +1 -0
  51. package/dist/phases/host.d.ts +3 -0
  52. package/dist/phases/host.d.ts.map +1 -0
  53. package/dist/phases/host.js +99 -0
  54. package/dist/phases/host.js.map +1 -0
  55. package/dist/phases/index.d.ts +7 -0
  56. package/dist/phases/index.d.ts.map +1 -0
  57. package/dist/phases/index.js +40 -0
  58. package/dist/phases/index.js.map +1 -0
  59. package/dist/phases/preflight.d.ts +4 -0
  60. package/dist/phases/preflight.d.ts.map +1 -0
  61. package/dist/phases/preflight.js +81 -0
  62. package/dist/phases/preflight.js.map +1 -0
  63. package/dist/phases/routes.d.ts +3 -0
  64. package/dist/phases/routes.d.ts.map +1 -0
  65. package/dist/phases/routes.js +145 -0
  66. package/dist/phases/routes.js.map +1 -0
  67. package/dist/phases/scaffold.d.ts +3 -0
  68. package/dist/phases/scaffold.d.ts.map +1 -0
  69. package/dist/phases/scaffold.js +113 -0
  70. package/dist/phases/scaffold.js.map +1 -0
  71. package/dist/phases/stub.d.ts +3 -0
  72. package/dist/phases/stub.d.ts.map +1 -0
  73. package/dist/phases/stub.js +25 -0
  74. package/dist/phases/stub.js.map +1 -0
  75. package/dist/phases/ui.d.ts +3 -0
  76. package/dist/phases/ui.d.ts.map +1 -0
  77. package/dist/phases/ui.js +93 -0
  78. package/dist/phases/ui.js.map +1 -0
  79. package/dist/phases/wire/index.d.ts +3 -0
  80. package/dist/phases/wire/index.d.ts.map +1 -0
  81. package/dist/phases/wire/index.js +67 -0
  82. package/dist/phases/wire/index.js.map +1 -0
  83. package/dist/phases/wire/root-tsx.d.ts +3 -0
  84. package/dist/phases/wire/root-tsx.d.ts.map +1 -0
  85. package/dist/phases/wire/root-tsx.js +57 -0
  86. package/dist/phases/wire/root-tsx.js.map +1 -0
  87. package/dist/phases/wire/server-ts.d.ts +3 -0
  88. package/dist/phases/wire/server-ts.d.ts.map +1 -0
  89. package/dist/phases/wire/server-ts.js +54 -0
  90. package/dist/phases/wire/server-ts.js.map +1 -0
  91. package/dist/phases/wire/shared.d.ts +34 -0
  92. package/dist/phases/wire/shared.d.ts.map +1 -0
  93. package/dist/phases/wire/shared.js +2 -0
  94. package/dist/phases/wire/shared.js.map +1 -0
  95. package/dist/phases/wire/start-ts.d.ts +3 -0
  96. package/dist/phases/wire/start-ts.d.ts.map +1 -0
  97. package/dist/phases/wire/start-ts.js +149 -0
  98. package/dist/phases/wire/start-ts.js.map +1 -0
  99. package/dist/phases/wire/tsconfig.d.ts +3 -0
  100. package/dist/phases/wire/tsconfig.d.ts.map +1 -0
  101. package/dist/phases/wire/tsconfig.js +105 -0
  102. package/dist/phases/wire/tsconfig.js.map +1 -0
  103. package/dist/phases/wire/vite-config.d.ts +3 -0
  104. package/dist/phases/wire/vite-config.d.ts.map +1 -0
  105. package/dist/phases/wire/vite-config.js +46 -0
  106. package/dist/phases/wire/vite-config.js.map +1 -0
  107. package/dist/prompts.d.ts +34 -0
  108. package/dist/prompts.d.ts.map +1 -0
  109. package/dist/prompts.js +49 -0
  110. package/dist/prompts.js.map +1 -0
  111. package/dist/runner.d.ts +5 -0
  112. package/dist/runner.d.ts.map +1 -0
  113. package/dist/runner.js +91 -0
  114. package/dist/runner.js.map +1 -0
  115. package/dist/state.d.ts +18 -0
  116. package/dist/state.d.ts.map +1 -0
  117. package/dist/state.js +68 -0
  118. package/dist/state.js.map +1 -0
  119. package/dist/templates/byline/admin.config.ts +41 -0
  120. package/dist/templates/byline/i18n.ts +47 -0
  121. package/dist/templates/byline/routes.ts +28 -0
  122. package/dist/templates/byline/seed.ts +19 -0
  123. package/dist/templates/byline/seeds/admin.ts +62 -0
  124. package/dist/templates/byline/server.config.ts +92 -0
  125. package/dist/templates/byline-examples/admin.config.ts +74 -0
  126. package/dist/templates/byline-examples/blocks/photo-block.ts +59 -0
  127. package/dist/templates/byline-examples/blocks/richtext-block.ts +35 -0
  128. package/dist/templates/byline-examples/collections/doc-example-flat-locale-all.ts +373 -0
  129. package/dist/templates/byline-examples/collections/doc-example-flat-locale-en.ts +283 -0
  130. package/dist/templates/byline-examples/collections/doc-example-tree-locale-all.ts +278 -0
  131. package/dist/templates/byline-examples/collections/doc-example-tree-locale-en.ts +205 -0
  132. package/dist/templates/byline-examples/collections/docs/admin.tsx +204 -0
  133. package/dist/templates/byline-examples/collections/docs/components/.gitkeep +0 -0
  134. package/dist/templates/byline-examples/collections/docs/components/feature-formatter.tsx +10 -0
  135. package/dist/templates/byline-examples/collections/docs/hooks/.gitkeep +0 -0
  136. package/dist/templates/byline-examples/collections/docs/index.ts +10 -0
  137. package/dist/templates/byline-examples/collections/docs/schema.ts +209 -0
  138. package/dist/templates/byline-examples/collections/docs-categories/admin.tsx +78 -0
  139. package/dist/templates/byline-examples/collections/docs-categories/components/.gitkeep +0 -0
  140. package/dist/templates/byline-examples/collections/docs-categories/hooks/.gitkeep +0 -0
  141. package/dist/templates/byline-examples/collections/docs-categories/index.ts +10 -0
  142. package/dist/templates/byline-examples/collections/docs-categories/schema.ts +33 -0
  143. package/dist/templates/byline-examples/collections/media/admin.tsx +188 -0
  144. package/dist/templates/byline-examples/collections/media/components/media-list-view.tsx +330 -0
  145. package/dist/templates/byline-examples/collections/media/components/media-thumbnail.tsx +63 -0
  146. package/dist/templates/byline-examples/collections/media/hooks/.gitkeep +0 -0
  147. package/dist/templates/byline-examples/collections/media/index.ts +10 -0
  148. package/dist/templates/byline-examples/collections/media/schema.ts +157 -0
  149. package/dist/templates/byline-examples/collections/news/admin.tsx +192 -0
  150. package/dist/templates/byline-examples/collections/news/components/.gitkeep +0 -0
  151. package/dist/templates/byline-examples/collections/news/hooks/.gitkeep +0 -0
  152. package/dist/templates/byline-examples/collections/news/index.ts +10 -0
  153. package/dist/templates/byline-examples/collections/news/schema.ts +91 -0
  154. package/dist/templates/byline-examples/collections/news-categories/admin.tsx +78 -0
  155. package/dist/templates/byline-examples/collections/news-categories/components/.gitkeep +0 -0
  156. package/dist/templates/byline-examples/collections/news-categories/hooks/.gitkeep +0 -0
  157. package/dist/templates/byline-examples/collections/news-categories/index.ts +10 -0
  158. package/dist/templates/byline-examples/collections/news-categories/schema.ts +33 -0
  159. package/dist/templates/byline-examples/collections/pages/admin.tsx +183 -0
  160. package/dist/templates/byline-examples/collections/pages/components/.gitkeep +0 -0
  161. package/dist/templates/byline-examples/collections/pages/hooks/.gitkeep +0 -0
  162. package/dist/templates/byline-examples/collections/pages/index.ts +10 -0
  163. package/dist/templates/byline-examples/collections/pages/schema.ts +96 -0
  164. package/dist/templates/byline-examples/components/length-indicator.tsx +138 -0
  165. package/dist/templates/byline-examples/components/pill.tsx +38 -0
  166. package/dist/templates/byline-examples/components/summary-length.tsx +39 -0
  167. package/dist/templates/byline-examples/fields/available-languages-field.ts +90 -0
  168. package/dist/templates/byline-examples/fields/lexical-richtext-compact.ts +88 -0
  169. package/dist/templates/byline-examples/i18n.ts +47 -0
  170. package/dist/templates/byline-examples/routes.ts +28 -0
  171. package/dist/templates/byline-examples/scripts/regenerate-media.ts +275 -0
  172. package/dist/templates/byline-examples/seed.ts +25 -0
  173. package/dist/templates/byline-examples/seeds/admin.ts +62 -0
  174. package/dist/templates/byline-examples/seeds/doc-categories.ts +71 -0
  175. package/dist/templates/byline-examples/seeds/docs.ts +293 -0
  176. package/dist/templates/byline-examples/seeds/news-categories.ts +71 -0
  177. package/dist/templates/byline-examples/server.config.ts +179 -0
  178. package/dist/templates/host/vite.config.ts +41 -0
  179. package/dist/templates/migrations/0000_condemned_kronos.sql +324 -0
  180. package/dist/templates/migrations/0001_sudden_phantom_reporter.sql +1 -0
  181. package/dist/templates/migrations/meta/0000_snapshot.json +2793 -0
  182. package/dist/templates/migrations/meta/0001_snapshot.json +2799 -0
  183. package/dist/templates/migrations/meta/_journal.json +20 -0
  184. package/dist/templates/routes/(byline)/admin/account/index.tsx +11 -0
  185. package/dist/templates/routes/(byline)/admin/collections/$collection/$id/api.tsx +16 -0
  186. package/dist/templates/routes/(byline)/admin/collections/$collection/$id/history.tsx +19 -0
  187. package/dist/templates/routes/(byline)/admin/collections/$collection/$id/index.tsx +16 -0
  188. package/dist/templates/routes/(byline)/admin/collections/$collection/create.tsx +11 -0
  189. package/dist/templates/routes/(byline)/admin/collections/$collection/index.tsx +11 -0
  190. package/dist/templates/routes/(byline)/admin/index.tsx +11 -0
  191. package/dist/templates/routes/(byline)/admin/permissions/index.tsx +11 -0
  192. package/dist/templates/routes/(byline)/admin/roles/$id/index.tsx +11 -0
  193. package/dist/templates/routes/(byline)/admin/roles/index.tsx +11 -0
  194. package/dist/templates/routes/(byline)/admin/route.tsx +11 -0
  195. package/dist/templates/routes/(byline)/admin/users/$id/index.tsx +11 -0
  196. package/dist/templates/routes/(byline)/admin/users/index.tsx +11 -0
  197. package/dist/templates/routes/(byline)/sign-in.tsx +11 -0
  198. package/dist/templates/ui-byline/blocks/photo-block/index.tsx +80 -0
  199. package/dist/templates/ui-byline/blocks/richtext-block/index.tsx +46 -0
  200. package/dist/templates/ui-byline/components/admonition/index.tsx +40 -0
  201. package/dist/templates/ui-byline/components/code/code-serializer.tsx +20 -0
  202. package/dist/templates/ui-byline/components/code/code.tsx +50 -0
  203. package/dist/templates/ui-byline/components/code/index.module.scss +137 -0
  204. package/dist/templates/ui-byline/components/code/index.ts +2 -0
  205. package/dist/templates/ui-byline/components/code/types.ts +5 -0
  206. package/dist/templates/ui-byline/components/code/utils.ts +20 -0
  207. package/dist/templates/ui-byline/components/heading-anchor/heading-anchor.tsx +69 -0
  208. package/dist/templates/ui-byline/components/heading-anchor/index.ts +1 -0
  209. package/dist/templates/ui-byline/components/heading-anchor/utils.ts +15 -0
  210. package/dist/templates/ui-byline/components/inline-image/index.tsx +109 -0
  211. package/dist/templates/ui-byline/components/layout/index.tsx +63 -0
  212. package/dist/templates/ui-byline/components/link/lang-link.tsx +70 -0
  213. package/dist/templates/ui-byline/components/link/link-field.tsx +298 -0
  214. package/dist/templates/ui-byline/components/link/link-lexical.tsx +191 -0
  215. package/dist/templates/ui-byline/components/list/index.ts +2 -0
  216. package/dist/templates/ui-byline/components/list/list-item.tsx +32 -0
  217. package/dist/templates/ui-byline/components/list/list.tsx +17 -0
  218. package/dist/templates/ui-byline/components/responsive-image/index.tsx +205 -0
  219. package/dist/templates/ui-byline/components/richtext-lexical/index.tsx +31 -0
  220. package/dist/templates/ui-byline/components/richtext-lexical/serialize/index.tsx +249 -0
  221. package/dist/templates/ui-byline/components/richtext-lexical/serialize/richtext-node-formats.ts +66 -0
  222. package/dist/templates/ui-byline/components/richtext-lexical/serialize/types.ts +48 -0
  223. package/dist/templates/ui-byline/components/richtext-lexical/serialize/utils.ts +15 -0
  224. package/dist/templates/ui-byline/components/table-cell/index.tsx +36 -0
  225. package/dist/templates/ui-byline/components/vimeo/index.tsx +21 -0
  226. package/dist/templates/ui-byline/components/youtube/index.tsx +22 -0
  227. package/dist/templates/ui-byline/render-blocks.tsx +71 -0
  228. package/dist/templates/ui-byline/types/i18n.ts +14 -0
  229. package/dist/templates/ui-byline/utils/image-sources.ts +102 -0
  230. package/dist/templates/ui-byline/utils/to-kebab-case.ts +5 -0
  231. package/dist/types.d.ts +54 -0
  232. package/dist/types.d.ts.map +1 -0
  233. package/dist/types.js +2 -0
  234. package/dist/types.js.map +1 -0
  235. package/dist/ui/diff.d.ts +4 -0
  236. package/dist/ui/diff.d.ts.map +1 -0
  237. package/dist/ui/diff.js +23 -0
  238. package/dist/ui/diff.js.map +1 -0
  239. package/dist/ui/grid.d.ts +7 -0
  240. package/dist/ui/grid.d.ts.map +1 -0
  241. package/dist/ui/grid.js +24 -0
  242. package/dist/ui/grid.js.map +1 -0
  243. package/dist/ui/logger.d.ts +14 -0
  244. package/dist/ui/logger.d.ts.map +1 -0
  245. package/dist/ui/logger.js +30 -0
  246. package/dist/ui/logger.js.map +1 -0
  247. package/dist/ui/snippet.d.ts +2 -0
  248. package/dist/ui/snippet.d.ts.map +1 -0
  249. package/dist/ui/snippet.js +7 -0
  250. package/dist/ui/snippet.js.map +1 -0
  251. package/package.json +69 -0
@@ -0,0 +1,69 @@
1
+ 'use client'
2
+
3
+ import type * as React from 'react'
4
+
5
+ import { formatTextValue } from '@byline/core'
6
+ import cx from 'classnames'
7
+
8
+ import { extractHeadingText } from './utils.ts'
9
+ import type { SerializedLexicalNode } from '../richtext-lexical/serialize/types.ts'
10
+
11
+ type Heading = Extract<keyof React.JSX.IntrinsicElements, 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'>
12
+
13
+ export function HeadingWithAnchorSerializer({
14
+ node,
15
+ }: {
16
+ node: SerializedLexicalNode
17
+ }): React.JSX.Element {
18
+ const tag = node?.tag
19
+ const text = extractHeadingText(node?.children)
20
+ const headingId = formatTextValue(text)
21
+ const Element = tag as Heading
22
+ if (tag != null && (tag === 'h4' || tag === 'h5' || tag === 'h6')) {
23
+ return (
24
+ <Element
25
+ id={headingId}
26
+ className="clear-both relative font-normal text-gray-800 dark:text-gray-300"
27
+ >
28
+ {text}
29
+ </Element>
30
+ )
31
+ } else {
32
+ return (
33
+ <Element id={headingId} className="relative clear-both ">
34
+ <a href={`#${headingId}`} className="component--heading-anchor not-prose no-underline">
35
+ <span className="component--heading-anchor-text font-bold text-theme-900 dark:text-white">
36
+ {text}
37
+ </span>
38
+ <span
39
+ className={cx(
40
+ 'component--heading-anchor-icon absolute p-0 leading-4 sm:inline-block',
41
+ 'right-[-0.125em] top-[-0.4em] m-0 -translate-y-1 translate-x-1 sm:right-auto sm:top-auto',
42
+ 'transition-all duration-200 ease-in-out',
43
+ 'text-gray-300 hover:text-gray-400 dark:text-gray-700 dark:hover:text-gray-600',
44
+ 'aria-hidden="true"'
45
+ )}
46
+ >
47
+ <svg
48
+ aria-hidden="true"
49
+ tabIndex={-1}
50
+ className="inline"
51
+ width="0.85em"
52
+ height="0.85em"
53
+ viewBox="0 0 15 15"
54
+ fill="none"
55
+ xmlns="http://www.w3.org/2000/svg"
56
+ >
57
+ <path
58
+ d="M4.62471 4.00001L4.56402 4.00001C4.04134 3.99993 3.70687 3.99988 3.4182 4.055C2.2379 4.28039 1.29846 5.17053 1.05815 6.33035C0.999538 6.61321 0.999604 6.93998 0.999703 7.43689L0.999711 7.50001L0.999703 7.56312C0.999604 8.06003 0.999538 8.3868 1.05815 8.66966C1.29846 9.82948 2.2379 10.7196 3.4182 10.945C3.70688 11.0001 4.04135 11.0001 4.56403 11L4.62471 11H5.49971C5.7759 11 5.99971 10.7762 5.99971 10.5C5.99971 10.2239 5.7759 10 5.49971 10H4.62471C4.02084 10 3.78907 9.99777 3.60577 9.96277C2.80262 9.8094 2.18169 9.21108 2.02456 8.42678C1.98838 8.24809 1.98971 8.02242 1.98971 7.50001C1.98971 6.9776 1.98838 6.75192 2.02456 6.57323C2.18169 5.78893 2.80262 5.19061 3.60577 5.03724C3.78907 5.00225 4.02084 5.00001 4.62471 5.00001H5.49971C5.7759 5.00001 5.99971 4.77615 5.99971 4.50001C5.99971 4.22387 5.7759 4.00001 5.49971 4.00001H4.62471ZM10.3747 4.00001H9.49971C9.22357 4.00001 8.99971 4.22387 8.99971 4.50001C8.99971 4.77615 9.22357 5.00001 9.49971 5.00001H10.3747C10.9786 5.00001 11.2104 5.00225 11.3937 5.03724C12.1968 5.19061 12.8177 5.78893 12.9749 6.57323C13.0111 6.75192 13.0097 6.9776 13.0097 7.50001C13.0097 8.02242 13.0111 8.24809 12.9749 8.42678C12.8177 9.21108 12.1968 9.8094 11.3937 9.96277C11.2104 9.99777 10.9786 10 10.3747 10H9.49971C9.22357 10 8.99971 10.2239 8.99971 10.5C8.99971 10.7762 9.22357 11 9.49971 11H10.3747L10.4354 11C10.9581 11.0001 11.2925 11.0001 11.5812 10.945C12.7615 10.7196 13.701 9.82948 13.9413 8.66966C13.9999 8.3868 13.9998 8.06003 13.9997 7.56312L13.9997 7.50001L13.9997 7.43689C13.9998 6.93998 13.9999 6.61321 13.9413 6.33035C13.701 5.17053 12.7615 4.28039 11.5812 4.055C11.2925 3.99988 10.9581 3.99993 10.4354 4.00001L10.3747 4.00001ZM4.99971 7.50001C4.99971 7.22387 5.22357 7.00001 5.49971 7.00001H9.49971C9.7759 7.00001 9.99971 7.22387 9.99971 7.50001C9.99971 7.77615 9.7759 8.00001 9.49971 8.00001H5.49971C5.22357 8.00001 4.99971 7.77615 4.99971 7.50001Z"
59
+ fill="currentColor"
60
+ fillRule="evenodd"
61
+ clipRule="evenodd"
62
+ />
63
+ </svg>
64
+ </span>
65
+ </a>
66
+ </Element>
67
+ )
68
+ }
69
+ }
@@ -0,0 +1 @@
1
+ export * from './heading-anchor.tsx'
@@ -0,0 +1,15 @@
1
+ import type { SerializedLexicalNode } from '../richtext-lexical/serialize/types.ts'
2
+
3
+ export function extractHeadingText(nodes?: SerializedLexicalNode[], text: string = ''): string {
4
+ if (nodes != null) {
5
+ for (const node of nodes) {
6
+ if (node.type === 'text' && node.text != null) {
7
+ text = text + node.text
8
+ }
9
+ if (node.children != null) {
10
+ extractHeadingText(node.children, text)
11
+ }
12
+ }
13
+ }
14
+ return text
15
+ }
@@ -0,0 +1,109 @@
1
+ 'use client'
2
+
3
+ import type React from 'react'
4
+
5
+ import type { StoredFileValue } from '@byline/core'
6
+ import type { InlineImagePosition, SerializedInlineImageNode } from '@byline/richtext-lexical'
7
+ import { FadeInLift } from '@byline/ui'
8
+
9
+ import { ResponsiveImage } from '../responsive-image/index.tsx'
10
+ import type { Locale } from '@/ui/byline/types/i18n'
11
+ import type { SerializeOptions, SerializeProps } from '../richtext-lexical/serialize/index.tsx'
12
+
13
+ /**
14
+ * Map the inline-image `position` (a layout choice the editor exposes
15
+ * via radio buttons) onto the public renderer's `<ResponsiveImage>`
16
+ * size cap and constrained-layout flag. Mirrors the editor-side
17
+ * `variantFor()` heuristic in `@byline/richtext-lexical`'s
18
+ * `inline-image-plugin/utils.ts` so the public render and the editor
19
+ * preview agree on which variants matter.
20
+ *
21
+ * - `left` / `right` — floated 50% column on desktop. Cap at the tablet
22
+ * variant; flag `constrainedLayout` so the `sizes` hint halves the
23
+ * desktop viewport portion.
24
+ * - `full` / `default` — main article column (~920px). Tablet cap is
25
+ * plenty; no extra constraint.
26
+ * - `wide` — bleeds beyond the article. Full variant set; no constraint.
27
+ */
28
+ const POSITION_TO_SIZE: Record<
29
+ NonNullable<InlineImagePosition>,
30
+ { size: 'large' | 'medium' | 'small'; constrainedLayout: boolean }
31
+ > = {
32
+ left: { size: 'medium', constrainedLayout: true },
33
+ right: { size: 'medium', constrainedLayout: true },
34
+ full: { size: 'medium', constrainedLayout: false },
35
+ default: { size: 'medium', constrainedLayout: false },
36
+ wide: { size: 'large', constrainedLayout: false },
37
+ }
38
+
39
+ interface Props {
40
+ node: SerializedInlineImageNode
41
+ serialize: ({ nodes, options }: SerializeProps) => React.JSX.Element
42
+ lng: Locale
43
+ options: SerializeOptions
44
+ }
45
+
46
+ export function InlineImageSerializer({ node, serialize, lng, options }: Props): React.JSX.Element {
47
+ const { showCaption, position, altText, document: pickerDoc, caption } = node
48
+ const image = pickerDoc?.image as StoredFileValue | undefined
49
+
50
+ const floatLeft = position === 'left'
51
+ const floatRight = position === 'right'
52
+ const isFloat = floatLeft || floatRight
53
+
54
+ const { size, constrainedLayout } = POSITION_TO_SIZE[position ?? 'default']
55
+
56
+ let classes: string
57
+ if (floatLeft) {
58
+ classes =
59
+ 'inline-image-block w-full sm:w-[50%] float-left mt-5 mb-5 sm:mt-[0.5rem] sm:mr-[1.2rem] sm:mb-1 sm:ml-0'
60
+ } else if (floatRight) {
61
+ classes =
62
+ 'inline-image-block w-full sm:w-[50%] float-right mt-5 mb-5 sm:mt-[0.5rem] sm:ml-[1rem] sm:mb-1 sm:mr-[1rem]'
63
+ } else {
64
+ classes = 'inline-image-block block w-full mt-5 mb-5'
65
+ }
66
+
67
+ // For now, if animation is disabled we must be inside a table cell
68
+ // (or similar non-animatable host); also disable bleed-to-edge.
69
+ const animationDisabled = options?.disableAnimation === true
70
+
71
+ const Img = (
72
+ <ResponsiveImage
73
+ image={image}
74
+ size={size}
75
+ constrainedLayout={constrainedLayout}
76
+ bleedOnMobile={!animationDisabled && !isFloat}
77
+ alt={altText}
78
+ />
79
+ )
80
+
81
+ const ImgSlot = animationDisabled ? (
82
+ Img
83
+ ) : (
84
+ <FadeInLift as="span" delay={0.1} className="block">
85
+ {Img}
86
+ </FadeInLift>
87
+ )
88
+
89
+ if (showCaption) {
90
+ return (
91
+ <span className={classes}>
92
+ {ImgSlot}
93
+ <span className="block inline-image-block--caption">
94
+ {caption?.editorState?.root?.children != null ? (
95
+ serialize({
96
+ nodes: caption.editorState.root.children,
97
+ lng,
98
+ options: { renderParagraphInline: true },
99
+ })
100
+ ) : (
101
+ <span>Caption not found for inline image.</span>
102
+ )}
103
+ </span>
104
+ </span>
105
+ )
106
+ }
107
+
108
+ return <span className={classes}>{ImgSlot}</span>
109
+ }
@@ -0,0 +1,63 @@
1
+ 'use client'
2
+
3
+ import type React from 'react'
4
+
5
+ import cx from 'classnames'
6
+
7
+ import type { Locale } from '@/ui/byline/types/i18n'
8
+ import type { SerializeOptions, SerializeProps } from '../richtext-lexical/serialize/index.tsx'
9
+ import type { SerializedLexicalNode } from '../richtext-lexical/serialize/types.ts'
10
+
11
+ const layoutMap = {
12
+ '1fr 1fr': 'grid-cols-[1fr_1fr]',
13
+ '1fr 3fr': 'grid-cols-[1fr_3fr]',
14
+ '3fr 1fr': 'grid-cols-[3fr_1fr]',
15
+ '1fr 1fr 1fr': 'grid-cols-[1fr_1fr_1fr]',
16
+ '1fr 2fr 1fr': 'grid-cols-[1fr_2fr_1fr]',
17
+ '1fr 1fr 1fr 1fr': 'grid-cols-[1fr_1fr_1fr_1fr]',
18
+ }
19
+
20
+ export function LayoutContainerSerializer({
21
+ node,
22
+ serialize,
23
+ lng,
24
+ options,
25
+ }: {
26
+ node: SerializedLexicalNode
27
+ serialize: ({ nodes }: SerializeProps) => React.JSX.Element
28
+ lng: Locale
29
+ options: SerializeOptions
30
+ }): React.JSX.Element {
31
+ const gridColumns = layoutMap[node.templateColumns as keyof typeof layoutMap]
32
+ return (
33
+ <div className={cx('sm:grid gap-5 my-5', gridColumns)}>
34
+ {node?.children != null ? (
35
+ serialize({ nodes: node?.children, lng, options })
36
+ ) : (
37
+ <span>Content not found for layout container.</span>
38
+ )}
39
+ </div>
40
+ )
41
+ }
42
+
43
+ export function LayoutItemSerializer({
44
+ node,
45
+ serialize,
46
+ lng,
47
+ options,
48
+ }: {
49
+ node: SerializedLexicalNode
50
+ serialize: ({ nodes }: SerializeProps) => React.JSX.Element
51
+ lng: Locale
52
+ options: SerializeOptions
53
+ }): React.JSX.Element {
54
+ return (
55
+ <div className="mb-5 sm:mb-0">
56
+ {node?.children != null ? (
57
+ serialize({ nodes: node?.children, lng, options })
58
+ ) : (
59
+ <span>Content not found for layout item.</span>
60
+ )}
61
+ </div>
62
+ )
63
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Locale-aware Link STUB.
3
+ *
4
+ * The reference webapp ships a real `LangLink` that injects an optional
5
+ * `{-$lng}` path parameter onto TanStack Router's `<Link>` so that the
6
+ * current locale survives client-side navigation. This stub is a thin
7
+ * pass-through — it accepts the `lng` prop but does NOT prefix URLs.
8
+ *
9
+ * If your app is single-locale, this is fine as-is.
10
+ *
11
+ * If you have a multi-locale strategy, replace the body with your own
12
+ * Link wrapper. The reference implementation lives at
13
+ * apps/webapp/src/i18n/components/lang-link.tsx
14
+ * in https://github.com/Byline-CMS/bylinecms.dev.
15
+ */
16
+
17
+ import type React from 'react'
18
+ import { Link } from '@tanstack/react-router'
19
+
20
+ import type { Locale } from '../../types/i18n'
21
+
22
+ export interface LangLinkProps
23
+ extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> {
24
+ to: string
25
+ lng?: Locale
26
+ params?: Record<string, string | undefined>
27
+ search?:
28
+ | true
29
+ | Record<string, unknown>
30
+ | ((current: Record<string, unknown>) => Record<string, unknown>)
31
+ forceReload?: boolean
32
+ scroll?: boolean
33
+ replace?: boolean
34
+ ref?: React.Ref<HTMLAnchorElement>
35
+ children?: React.ReactNode
36
+ }
37
+
38
+ export function LangLink({
39
+ to,
40
+ lng: _lng,
41
+ params,
42
+ search,
43
+ forceReload,
44
+ scroll,
45
+ replace,
46
+ ref,
47
+ children,
48
+ ...rest
49
+ }: LangLinkProps): React.JSX.Element {
50
+ if (forceReload) {
51
+ return (
52
+ <a href={to} ref={ref} {...rest}>
53
+ {children}
54
+ </a>
55
+ )
56
+ }
57
+ return (
58
+ <Link
59
+ to={to}
60
+ params={params}
61
+ search={search}
62
+ resetScroll={scroll}
63
+ replace={replace}
64
+ ref={ref}
65
+ {...rest}
66
+ >
67
+ {children}
68
+ </Link>
69
+ )
70
+ }
@@ -0,0 +1,298 @@
1
+ 'use client'
2
+
3
+ import type React from 'react'
4
+
5
+ import { Button, type Intent } from '@byline/ui'
6
+ import cx from 'classnames'
7
+
8
+ import { LangLink } from '@/ui/byline/components/link/lang-link'
9
+ import type { Locale } from '@/ui/byline/types/i18n'
10
+
11
+ export interface LinkFieldAttributes {
12
+ id?: string | null
13
+ type?: ('reference' | 'custom') | null
14
+ newTab?: boolean | null
15
+ label: string
16
+ customId?: string | null
17
+ url?: string | null
18
+ appearance?: ('primary' | 'secondary') | null
19
+ reference?: {
20
+ value: string
21
+ relationTo: string
22
+ data: {
23
+ id: string
24
+ title: string
25
+ slug: string
26
+ collectionAlias: string
27
+ }
28
+ }
29
+ }
30
+
31
+ export interface LinkFieldProps extends React.ComponentPropsWithoutRef<'a'> {
32
+ link: LinkFieldAttributes
33
+ size?: 'sm' | 'md' | 'lg'
34
+ lng: Locale
35
+ className?: string
36
+ onMouseEnter?: () => void
37
+ onMouseLeave?: () => void
38
+ children?: React.ReactNode
39
+ }
40
+
41
+ export function manageRel(input: string, action: 'add' | 'remove', value: string): string {
42
+ let result: string
43
+ let mutableInput = `${input}`
44
+ if (action === 'add') {
45
+ // if we somehow got out of sync - clean up
46
+ if (mutableInput.includes(value)) {
47
+ const re = new RegExp(value, 'g')
48
+ mutableInput = mutableInput.replace(re, '').trim()
49
+ }
50
+ mutableInput = mutableInput.trim()
51
+ result = mutableInput.length === 0 ? `${value}` : `${mutableInput} ${value}`
52
+ } else {
53
+ const re = new RegExp(value, 'g')
54
+ result = mutableInput.replace(re, '').trim()
55
+ }
56
+ return result
57
+ }
58
+
59
+ function getHref(args: LinkFieldAttributes): string {
60
+ let href = ''
61
+ const publicWebsiteUrl = '/' // getPublicWebsiteUrl()
62
+ const { type, url } = args
63
+
64
+ if ((type === 'custom' || type === undefined) && url != null) {
65
+ href = url
66
+ } else if (
67
+ type === 'reference' &&
68
+ args.reference?.relationTo != null &&
69
+ args.reference?.data?.slug != null
70
+ ) {
71
+ const collection = args?.reference?.relationTo
72
+ // biome-ignore lint/correctness/noUnsafeOptionalChaining: To be refactored in the future
73
+ const { slug, collectionAlias } = args?.reference?.data
74
+ if (collectionAlias != null) {
75
+ // The alias might be for the root
76
+ if (collectionAlias.length === 0) {
77
+ href = `/${slug}`
78
+ } else {
79
+ href = `/${collectionAlias}/${slug}`
80
+ }
81
+ } else {
82
+ href = `/${collection}/${slug}`
83
+ }
84
+ }
85
+
86
+ const hrefIsLocal = ['tel:', 'mailto:', '/'].some((prefix) => href.startsWith(prefix))
87
+ if (!hrefIsLocal) {
88
+ try {
89
+ const objectURL = new URL(href)
90
+ if (objectURL.origin === publicWebsiteUrl) {
91
+ href = objectURL.href.replace(publicWebsiteUrl, '')
92
+ }
93
+ } catch (e) {
94
+ console.error(`Failed to format url: ${href}`, e) // eslint-disable-line no-console
95
+ }
96
+ }
97
+
98
+ return href
99
+ }
100
+
101
+ function getAdditionalProps(
102
+ args: LinkFieldAttributes,
103
+ href: string
104
+ ): {
105
+ rel: string | undefined
106
+ target: string | undefined
107
+ } {
108
+ const additionalProps: {
109
+ rel: string | undefined
110
+ target: string | undefined
111
+ } = {
112
+ rel: undefined,
113
+ target: undefined,
114
+ }
115
+
116
+ let rel = ''
117
+ if (args.newTab === true) rel = manageRel(rel, 'add', 'noopener')
118
+ additionalProps.rel = rel
119
+
120
+ if (args.newTab === true) {
121
+ additionalProps.target = '_blank'
122
+ }
123
+
124
+ if (!href.startsWith('/')) {
125
+ additionalProps.target = '_blank'
126
+ }
127
+
128
+ if (additionalProps.rel == null || additionalProps.rel.length === 0) delete additionalProps.rel
129
+ if (additionalProps.target == null) delete additionalProps.target
130
+
131
+ return additionalProps
132
+ }
133
+
134
+ /**
135
+ * DefaultLink
136
+ * @param param0
137
+ * @returns
138
+ */
139
+ export function DefaultLink({
140
+ link,
141
+ lng,
142
+ className,
143
+ onMouseEnter,
144
+ onMouseLeave,
145
+ ...rest
146
+ }: LinkFieldProps): React.JSX.Element {
147
+ const href = getHref(link)
148
+ const additionalProps = getAdditionalProps(link, href)
149
+
150
+ if (href.startsWith('/')) {
151
+ return (
152
+ <LangLink
153
+ lng={lng}
154
+ to={href}
155
+ {...additionalProps}
156
+ className={className}
157
+ onMouseEnter={onMouseEnter}
158
+ onMouseLeave={onMouseLeave}
159
+ {...rest}
160
+ >
161
+ {link.label}
162
+ </LangLink>
163
+ )
164
+ }
165
+ return (
166
+ <a
167
+ href={href}
168
+ {...additionalProps}
169
+ className={cx(className, '!no-underline')}
170
+ onMouseEnter={onMouseEnter}
171
+ onMouseLeave={onMouseLeave}
172
+ {...rest}
173
+ >
174
+ <span className="underline">{link.label}</span>
175
+ <span style={{ display: 'inline', whiteSpace: 'nowrap' }}>
176
+ &#x202F;
177
+ <svg
178
+ xmlns="http://www.w3.org/2000/svg"
179
+ className="fill-[#001827] dark:fill-gray-50"
180
+ style={{ display: 'inline' }}
181
+ focusable="false"
182
+ aria-hidden="true"
183
+ height="14px"
184
+ width="14px"
185
+ viewBox="0 0 24 24"
186
+ >
187
+ <path d="M0 0h24v24H0V0z" fill="none" />
188
+ <path d="M19 19H5V5h7V3H3v18h18v-9h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z" />
189
+ </svg>
190
+ </span>
191
+ </a>
192
+ )
193
+ }
194
+
195
+ /**
196
+ * ButtonLink
197
+ * @param param0
198
+ * @returns
199
+ */
200
+ export function ButtonLink({
201
+ link,
202
+ size = 'md',
203
+ lng,
204
+ className,
205
+ onMouseEnter,
206
+ onMouseLeave,
207
+ ...rest
208
+ }: LinkFieldProps): React.JSX.Element {
209
+ const href = getHref(link)
210
+ const additionalProps = getAdditionalProps(link, href)
211
+
212
+ if (href.startsWith('/')) {
213
+ return (
214
+ <Button
215
+ intent={link?.appearance as Intent}
216
+ size={size}
217
+ render={
218
+ <LangLink
219
+ lng={lng}
220
+ to={href}
221
+ {...additionalProps}
222
+ className={cx(className, '!no-underline')}
223
+ onMouseEnter={onMouseEnter}
224
+ onMouseLeave={onMouseLeave}
225
+ {...rest}
226
+ />
227
+ }
228
+ >
229
+ {link.label}
230
+ </Button>
231
+ )
232
+ }
233
+ return (
234
+ <Button
235
+ intent={link?.appearance as Intent}
236
+ size={size}
237
+ render={
238
+ <a
239
+ href={href}
240
+ {...additionalProps}
241
+ className={cx(className, '!no-underline')}
242
+ onMouseEnter={onMouseEnter}
243
+ onMouseLeave={onMouseLeave}
244
+ {...rest}
245
+ />
246
+ }
247
+ >
248
+ {link.label}
249
+ </Button>
250
+ )
251
+ }
252
+
253
+ /**
254
+ * We export LinkField which will attempt to automatically switch
255
+ * between ButtonLink and DefaultLink based on the appearance of the link.
256
+ *
257
+ * For more control including className and style overrides, a block may
258
+ * choose to use the above ButtonLink or DefaultLink directly.
259
+ *
260
+ * @param param0
261
+ * @returns
262
+ */
263
+ export function LinkField({
264
+ link,
265
+ lng,
266
+ size = 'md',
267
+ className,
268
+ onMouseEnter,
269
+ onMouseLeave,
270
+ ...rest
271
+ }: LinkFieldProps): React.JSX.Element {
272
+ if (
273
+ link.appearance != null &&
274
+ (link.appearance === 'primary' || link.appearance === 'secondary')
275
+ ) {
276
+ return (
277
+ <ButtonLink
278
+ size={size}
279
+ link={link}
280
+ lng={lng}
281
+ className={className}
282
+ onMouseEnter={onMouseEnter}
283
+ onMouseLeave={onMouseLeave}
284
+ {...rest}
285
+ />
286
+ )
287
+ }
288
+ return (
289
+ <DefaultLink
290
+ link={link}
291
+ lng={lng}
292
+ className={className}
293
+ onMouseEnter={onMouseEnter}
294
+ onMouseLeave={onMouseLeave}
295
+ {...rest}
296
+ />
297
+ )
298
+ }