@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,249 @@
1
+ 'use client'
2
+
3
+ import { Fragment } from 'react'
4
+
5
+ import type { SerializedInlineImageNode, SerializedTextNode } from '@byline/richtext-lexical'
6
+ import { Table } from '@byline/ui'
7
+
8
+ import { AdmonitionSerializer } from '../../admonition/index.tsx'
9
+ import { CodeSerializer } from '../../code/code-serializer.tsx'
10
+ import { HeadingWithAnchorSerializer } from '../../heading-anchor/index.ts'
11
+ import { InlineImageSerializer } from '../../inline-image/index.tsx'
12
+ import { LayoutContainerSerializer, LayoutItemSerializer } from '../../layout/index.tsx'
13
+ import { LinkLexicalSerializer } from '../../link/link-lexical.tsx'
14
+ import { ListItemSerializer, ListSerializer } from '../../list/index.ts'
15
+ import { TableCellSerializer } from '../../table-cell/index.tsx'
16
+ import { VimeoSerializer } from '../../vimeo/index.tsx'
17
+ import { YouTubeSerializer } from '../../youtube/index.tsx'
18
+ import {
19
+ IS_BOLD,
20
+ IS_CODE,
21
+ IS_ITALIC,
22
+ IS_STRIKETHROUGH,
23
+ IS_SUBSCRIPT,
24
+ IS_SUPERSCRIPT,
25
+ IS_UNDERLINE,
26
+ } from './richtext-node-formats.ts'
27
+ import type { Locale } from '@/ui/byline/types/i18n'
28
+ import type { SerializedLexicalNode } from './types.ts'
29
+
30
+ export interface SerializeOptions {
31
+ renderParagraphInline: boolean
32
+ disableAnimation?: boolean
33
+ }
34
+
35
+ export interface SerializeProps {
36
+ nodes: SerializedLexicalNode[]
37
+ lng: Locale
38
+ options?: SerializeOptions
39
+ }
40
+
41
+ export function Serialize({
42
+ nodes,
43
+ lng,
44
+ options = { renderParagraphInline: false },
45
+ }: SerializeProps): React.JSX.Element {
46
+ return (
47
+ <Fragment>
48
+ {nodes?.map((node, index): React.JSX.Element | null => {
49
+ if (node.type === 'text') {
50
+ const textNode = node as SerializedTextNode
51
+ const { text, format } = textNode
52
+ if (format & IS_BOLD) {
53
+ return <strong key={index}>{text}</strong>
54
+ }
55
+ if (format & IS_ITALIC) {
56
+ return <em key={index}>{text}</em>
57
+ }
58
+ if (format & IS_STRIKETHROUGH) {
59
+ return (
60
+ <span key={index} className="line-through">
61
+ {text}
62
+ </span>
63
+ )
64
+ }
65
+ if (format & IS_UNDERLINE) {
66
+ return (
67
+ <span key={index} className="underline">
68
+ {text}
69
+ </span>
70
+ )
71
+ }
72
+ if (format & IS_CODE) {
73
+ return <code key={index}>{text}</code>
74
+ }
75
+ if (format & IS_SUBSCRIPT) {
76
+ return <sub key={index}>{text}</sub>
77
+ }
78
+ if (format & IS_SUPERSCRIPT) {
79
+ return <sup key={index}> {text} </sup>
80
+ }
81
+ return <Fragment key={index}>{text}</Fragment>
82
+ }
83
+
84
+ if (node == null) {
85
+ return null
86
+ }
87
+
88
+ // NOTE: Hacky fix for
89
+ // https://github.com/facebook/lexical/blob/d10c4e6e55261b2fdd7d1845aed46151d0f06a8c/packages/lexical-list/src/LexicalListItemNode.ts#L133
90
+ // which does not return checked: false (only true - i.e. there is no prop for false)
91
+ // NOTE: also 'look ahead' for table rows that should be a header.
92
+ const serializedChildrenFn = (node: SerializedLexicalNode): React.JSX.Element | null => {
93
+ if (node.children == null) {
94
+ return null
95
+ } else {
96
+ if (node?.type === 'list' && node?.listType === 'check') {
97
+ for (const item of node.children) {
98
+ if (!item?.checked) {
99
+ item.checked = false
100
+ }
101
+ }
102
+ return Serialize({ nodes: node.children, lng, options })
103
+ } else {
104
+ return Serialize({ nodes: node.children, lng, options })
105
+ }
106
+ }
107
+ }
108
+
109
+ const serializedChildren = serializedChildrenFn(node)
110
+
111
+ switch (node.type) {
112
+ case 'linebreak': {
113
+ return <br key={index} />
114
+ }
115
+ case 'quote': {
116
+ return <blockquote key={index}>{serializedChildren}</blockquote>
117
+ }
118
+ case 'horizontalrule': {
119
+ return (
120
+ <hr
121
+ className="not-prose clear-both my-6 border-gray-300 dark:border-gray-600"
122
+ key={index}
123
+ />
124
+ )
125
+ }
126
+ case 'paragraph': {
127
+ if (options.renderParagraphInline) {
128
+ return <span key={index}>{serializedChildren}</span>
129
+ } else {
130
+ return <p key={index}>{serializedChildren}</p>
131
+ }
132
+ }
133
+ case 'heading': {
134
+ return <HeadingWithAnchorSerializer key={index} node={node} />
135
+ }
136
+ case 'list': {
137
+ return (
138
+ <ListSerializer key={index} node={node}>
139
+ {serializedChildren}
140
+ </ListSerializer>
141
+ )
142
+ }
143
+ case 'listitem': {
144
+ return (
145
+ <ListItemSerializer key={index} node={node}>
146
+ {serializedChildren}
147
+ </ListItemSerializer>
148
+ )
149
+ }
150
+ case 'link': {
151
+ return (
152
+ <LinkLexicalSerializer key={index} attributes={node.attributes} lng={lng}>
153
+ {serializedChildren}
154
+ </LinkLexicalSerializer>
155
+ )
156
+ }
157
+ case 'admonition': {
158
+ return (
159
+ <AdmonitionSerializer
160
+ key={index}
161
+ node={node}
162
+ serialize={Serialize}
163
+ lng={lng}
164
+ options={options}
165
+ />
166
+ )
167
+ }
168
+ case 'inline-image': {
169
+ return (
170
+ <InlineImageSerializer
171
+ key={index}
172
+ node={node as SerializedInlineImageNode}
173
+ serialize={Serialize}
174
+ lng={lng}
175
+ options={options}
176
+ />
177
+ )
178
+ }
179
+ case 'code': {
180
+ return <CodeSerializer key={index} node={node} />
181
+ }
182
+ case 'table': {
183
+ return (
184
+ <Table.Container key={index}>
185
+ <Table>
186
+ <Table.Body>{serializedChildren}</Table.Body>
187
+ </Table>
188
+ </Table.Container>
189
+ )
190
+ }
191
+ case 'tablerow': {
192
+ return <Table.Row key={index}>{serializedChildren}</Table.Row>
193
+ }
194
+ case 'tablecell': {
195
+ // TODO: revisit - we special case inline images if they appear inside a table
196
+ // cell - and so that's why we're using the TableCellSerializer now
197
+ return (
198
+ <TableCellSerializer
199
+ key={index}
200
+ node={node}
201
+ serialize={Serialize}
202
+ lng={lng}
203
+ options={options}
204
+ />
205
+ )
206
+ // if (node?.headerState === 1 || node?.headerState === 2 || node?.headerState === 3) {
207
+ // return <TableHeadingCell key={index}>{serializedChildren}</TableHeadingCell>
208
+ // } else {
209
+ // return <TableCell key={index}>{serializedChildren}</TableCell>
210
+ // }
211
+ }
212
+ case 'layout-container': {
213
+ return (
214
+ <LayoutContainerSerializer
215
+ key={index}
216
+ node={node}
217
+ serialize={Serialize}
218
+ lng={lng}
219
+ options={options}
220
+ />
221
+ )
222
+ }
223
+ case 'layout-item': {
224
+ return (
225
+ <LayoutItemSerializer
226
+ key={index}
227
+ node={node}
228
+ serialize={Serialize}
229
+ lng={lng}
230
+ options={options}
231
+ />
232
+ )
233
+ }
234
+ case 'youtube': {
235
+ return <YouTubeSerializer key={index} node={node} />
236
+ }
237
+ case 'vimeo': {
238
+ return <VimeoSerializer key={index} node={node} />
239
+ }
240
+ // case 'code-highlight': {
241
+ // return <Fragment key={index}>{`${node.text}\r\n`}</Fragment>
242
+ // }
243
+ default:
244
+ return null
245
+ }
246
+ })}
247
+ </Fragment>
248
+ )
249
+ }
@@ -0,0 +1,66 @@
1
+ // This copy-and-pasted from somewhere in lexical here: https://github.com/facebook/lexical/blob/c2ceee223f46543d12c574e62155e619f9a18a5d/packages/lexical/src/LexicalConstants.ts
2
+
3
+ // DOM
4
+ export const DOM_ELEMENT_TYPE = 1
5
+ export const DOM_TEXT_TYPE = 3
6
+
7
+ // Reconciling
8
+ export const NO_DIRTY_NODES = 0
9
+ export const HAS_DIRTY_NODES = 1
10
+ export const FULL_RECONCILE = 2
11
+
12
+ // Text node modes
13
+ export const IS_NORMAL = 0
14
+ export const IS_TOKEN = 1
15
+ export const IS_SEGMENTED = 2
16
+ // IS_INERT = 3
17
+
18
+ // Text node formatting
19
+ export const IS_BOLD = 1
20
+ export const IS_ITALIC = 1 << 1
21
+ export const IS_STRIKETHROUGH = 1 << 2
22
+ export const IS_UNDERLINE = 1 << 3
23
+ export const IS_CODE = 1 << 4
24
+ export const IS_SUBSCRIPT = 1 << 5
25
+ export const IS_SUPERSCRIPT = 1 << 6
26
+ export const IS_HIGHLIGHT = 1 << 7
27
+
28
+ export const IS_ALL_FORMATTING =
29
+ IS_BOLD |
30
+ IS_ITALIC |
31
+ IS_STRIKETHROUGH |
32
+ IS_UNDERLINE |
33
+ IS_CODE |
34
+ IS_SUBSCRIPT |
35
+ IS_SUPERSCRIPT |
36
+ IS_HIGHLIGHT
37
+
38
+ export const IS_DIRECTIONLESS = 1
39
+ export const IS_UNMERGEABLE = 1 << 1
40
+
41
+ // Element node formatting
42
+ export const IS_ALIGN_LEFT = 1
43
+ export const IS_ALIGN_CENTER = 2
44
+ export const IS_ALIGN_RIGHT = 3
45
+ export const IS_ALIGN_JUSTIFY = 4
46
+ export const IS_ALIGN_START = 5
47
+ export const IS_ALIGN_END = 6
48
+
49
+ export const TEXT_TYPE_TO_FORMAT: Record<TextFormatType | string, number> = {
50
+ bold: IS_BOLD,
51
+ code: IS_CODE,
52
+ italic: IS_ITALIC,
53
+ strikethrough: IS_STRIKETHROUGH,
54
+ subscript: IS_SUBSCRIPT,
55
+ superscript: IS_SUPERSCRIPT,
56
+ underline: IS_UNDERLINE,
57
+ }
58
+
59
+ export type TextFormatType =
60
+ | 'bold'
61
+ | 'underline'
62
+ | 'strikethrough'
63
+ | 'italic'
64
+ | 'code'
65
+ | 'subscript'
66
+ | 'superscript'
@@ -0,0 +1,48 @@
1
+ /**
2
+ * This Source Code is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
5
+ *
6
+ * Copyright (c) Infonomic Company Limited
7
+ */
8
+
9
+ import type {
10
+ SerializedLexicalNode as LexicalSerializedLexicalNode,
11
+ SerializedRootNode as LexicalSerializedRootNode,
12
+ } from '@byline/richtext-lexical'
13
+
14
+ /**
15
+ * Root of a Lexical editor state — `editorState.root` from a persisted
16
+ * document. Matches Lexical's package shape directly so callers can
17
+ * pass `editorState.root.children` straight in without casting.
18
+ */
19
+ export interface SerializedLexicalEditorState {
20
+ root: LexicalSerializedRootNode
21
+ }
22
+
23
+ /**
24
+ * Serializer-side node shape for the dispatch in `serialize/index.tsx`.
25
+ *
26
+ * Lexical's package type only guarantees `{ type, version }` on the
27
+ * base; concrete node types (text, element, decorator, custom Byline
28
+ * nodes) carry many more fields. The dispatcher narrows by
29
+ * `node.type` and each `case` reaches into branch-specific fields —
30
+ * we keep the most common ones declared as optional (so reads are
31
+ * shape-aware where they can be) and an `[other: string]: any` escape
32
+ * hatch for fields specific to custom nodes (`tag`, `listType`,
33
+ * `checked`, `attributes`, `headerState`, `kind`, etc.) without
34
+ * enumerating every node type.
35
+ *
36
+ * Structurally a superset of Lexical's `SerializedLexicalNode`, so
37
+ * values of either flavor flow through the dispatcher without casts.
38
+ */
39
+ export type SerializedLexicalNode = LexicalSerializedLexicalNode & {
40
+ format?: number | string
41
+ text?: string
42
+ mode?: string
43
+ style?: string
44
+ indent?: string | number
45
+ direction?: 'ltr' | 'rtl' | null
46
+ children?: SerializedLexicalNode[]
47
+ [other: string]: any
48
+ }
@@ -0,0 +1,15 @@
1
+ import type { SerializedLexicalNode } from './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,36 @@
1
+ import { Table } from '@byline/ui'
2
+
3
+ import type { Locale } from '@/ui/byline/types/i18n'
4
+ import type { SerializeOptions, SerializeProps } from '../richtext-lexical/serialize/index.tsx'
5
+ import type { SerializedLexicalNode } from '../richtext-lexical/serialize/types.ts'
6
+
7
+ export function TableCellSerializer({
8
+ node,
9
+ serialize,
10
+ lng,
11
+ options,
12
+ }: {
13
+ node: SerializedLexicalNode
14
+ serialize: ({ nodes, options }: SerializeProps) => React.JSX.Element
15
+ lng: Locale
16
+ options: SerializeOptions
17
+ }): React.JSX.Element {
18
+ // Disable any animations for any child nodes that appear inside a table cell
19
+ const tableCellOptions: SerializeOptions = { ...options, disableAnimation: true }
20
+
21
+ if (node?.headerState === 1 || node?.headerState === 2 || node?.headerState === 3) {
22
+ return (
23
+ <Table.Cell>
24
+ {node?.children != null &&
25
+ serialize({ nodes: node?.children, lng, options: tableCellOptions })}
26
+ </Table.Cell>
27
+ )
28
+ } else {
29
+ return (
30
+ <Table.Cell>
31
+ {node?.children != null &&
32
+ serialize({ nodes: node?.children, lng, options: tableCellOptions })}
33
+ </Table.Cell>
34
+ )
35
+ }
36
+ }
@@ -0,0 +1,21 @@
1
+ 'use client'
2
+
3
+ import type React from 'react'
4
+
5
+ import type { SerializedLexicalNode } from '../richtext-lexical/serialize/types.ts'
6
+
7
+ export function VimeoSerializer({ node }: { node: SerializedLexicalNode }): React.JSX.Element {
8
+ const videoID = node.videoID as string
9
+ return (
10
+ <iframe
11
+ style={{
12
+ aspectRatio: '16 / 9',
13
+ width: '100%',
14
+ }}
15
+ src={`https://player.vimeo.com/video/${videoID}`}
16
+ allow="autoplay fullscreen picture-in-picture"
17
+ allowFullScreen={true}
18
+ title="Vimeo Video"
19
+ />
20
+ )
21
+ }
@@ -0,0 +1,22 @@
1
+ 'use client'
2
+
3
+ import type React from 'react'
4
+
5
+ import type { SerializedLexicalNode } from '../richtext-lexical/serialize/types.ts'
6
+
7
+ export function YouTubeSerializer({ node }: { node: SerializedLexicalNode }): React.JSX.Element {
8
+ const videoID = node.videoID as string
9
+ return (
10
+ <iframe
11
+ style={{
12
+ aspectRatio: '16 / 9',
13
+ width: '100%',
14
+ }}
15
+ src={`https://www.youtube-nocookie.com/embed/${videoID}`}
16
+ frameBorder="0"
17
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
18
+ allowFullScreen={true}
19
+ title="YouTube Video"
20
+ />
21
+ )
22
+ }
@@ -0,0 +1,71 @@
1
+ import type { BlocksUnion } from '@byline/core'
2
+ import { Section } from '@byline/ui'
3
+
4
+ import { PhotoBlock as PhotoBlockDef } from '~/blocks/photo-block'
5
+ import { RichTextBlock as RichTextBlockDef } from '~/blocks/richtext-block'
6
+
7
+ import { PhotoBlock } from '@/ui/byline/blocks/photo-block'
8
+ import { RichTextBlock } from '@/ui/byline/blocks/richtext-block'
9
+ import { toKebabCase } from '@/ui/byline/utils/to-kebab-case'
10
+ import type { Locale } from '@/ui/byline/types/i18n'
11
+
12
+ /**
13
+ * Registered block schemas. Add a new block here (and a matching `case`
14
+ * in the switch below) to wire it into the front-end renderer.
15
+ */
16
+ const Blocks = [PhotoBlockDef, RichTextBlockDef] as const
17
+
18
+ /**
19
+ * Discriminated union of every registered block's instance shape. Use
20
+ * this as the `blocks` prop type when calling `RenderBlocks`.
21
+ */
22
+ export type AnyBlock = BlocksUnion<typeof Blocks>
23
+
24
+ interface Props {
25
+ blocks: AnyBlock[] | undefined | null
26
+ lng: Locale
27
+ constrainedLayout?: boolean
28
+ }
29
+
30
+ export function RenderBlocks({
31
+ blocks,
32
+ constrainedLayout = false,
33
+ lng,
34
+ }: Props): React.JSX.Element | null {
35
+ if (!Array.isArray(blocks) || blocks.length === 0) return null
36
+
37
+ return (
38
+ <>
39
+ {blocks.map((block) => {
40
+ switch (block._type) {
41
+ case 'photoBlock':
42
+ return (
43
+ <Section className={toKebabCase(block._type)} key={block._id}>
44
+ <PhotoBlock
45
+ id={block._id}
46
+ block={block}
47
+ lng={lng}
48
+ constrainedLayout={constrainedLayout}
49
+ />
50
+ </Section>
51
+ )
52
+ case 'richTextBlock':
53
+ return (
54
+ <Section className={toKebabCase(block._type)} key={block._id}>
55
+ <RichTextBlock
56
+ id={block._id}
57
+ block={block}
58
+ lng={lng}
59
+ constrainedLayout={constrainedLayout}
60
+ />
61
+ </Section>
62
+ )
63
+ default: {
64
+ const _exhaustive: never = block
65
+ return null
66
+ }
67
+ }
68
+ })}
69
+ </>
70
+ )
71
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * `Locale` type used throughout the ui/byline tree as a `lng` prop type.
3
+ *
4
+ * The reference webapp ships its own `i18n-config.ts` that narrows this
5
+ * to a literal union of supported locales (`'en' | 'fr' | ...`). This
6
+ * stub keeps the type as `string` so the install builds without
7
+ * requiring you to wire up an i18n config.
8
+ *
9
+ * If your app has its own i18n setup, replace this file with one that
10
+ * re-exports your narrower `Locale` type, e.g.
11
+ *
12
+ * export type { Locale } from '@/i18n/i18n-config'
13
+ */
14
+ export type Locale = string
@@ -0,0 +1,102 @@
1
+ import type { PersistedVariant, StoredFileValue } from '@byline/core'
2
+
3
+ /**
4
+ * Maximum variant width (px) included in a srcSet for each `size` cap.
5
+ *
6
+ * - `auto` — every variant.
7
+ * - `medium` — drops the largest variant (`desktop` ≥ 2100w).
8
+ * - `small` — keeps only sub-tablet variants (≤ 768w).
9
+ *
10
+ * Tuned for the variant set declared in the reference Media collection
11
+ * (`Media.image.upload.sizes`); safe to widen as the variant set grows.
12
+ */
13
+ const SIZE_CAPS: Record<'auto' | 'medium' | 'small', number> = {
14
+ auto: Number.POSITIVE_INFINITY,
15
+ medium: 1280,
16
+ small: 768,
17
+ }
18
+
19
+ /**
20
+ * Image-format names the renderer pipeline knows about. Aligns with the
21
+ * `format` values Sharp emits via `UploadConfig.sizes[].format`. AVIF is
22
+ * preferred when present (smaller files at comparable quality); webp is
23
+ * the standard fallback for older browsers.
24
+ */
25
+ export type VariantFormat = 'avif' | 'webp'
26
+
27
+ /** MIME type emitted on the matching `<source type="…">` element. */
28
+ export const VARIANT_MIME: Record<VariantFormat, string> = {
29
+ avif: 'image/avif',
30
+ webp: 'image/webp',
31
+ }
32
+
33
+ type ResolvedVariant = PersistedVariant & { storageUrl: string; width: number }
34
+
35
+ function isResolvedVariant(v: PersistedVariant): v is ResolvedVariant {
36
+ return v.storageUrl != null && v.width != null
37
+ }
38
+
39
+ /**
40
+ * Build a srcSet (`<url> <width>w`) for variants of the given `format`,
41
+ * sorted ascending by width and filtered by an optional size cap.
42
+ * Variants without a `storageUrl` or `width` are skipped.
43
+ */
44
+ export function getVariantSrcSet(
45
+ image: StoredFileValue | undefined | null,
46
+ format: VariantFormat,
47
+ maxSize: 'auto' | 'medium' | 'small' = 'auto'
48
+ ): string[] {
49
+ if (image == null) return []
50
+ const cap = SIZE_CAPS[maxSize]
51
+ return (image.variants ?? [])
52
+ .filter(isResolvedVariant)
53
+ .filter((v) => v.format === format && v.width <= cap)
54
+ .sort((a, b) => a.width - b.width)
55
+ .map((v) => `${v.storageUrl} ${v.width}w`)
56
+ }
57
+
58
+ /**
59
+ * `true` when the upload value carries at least one variant in the
60
+ * given format. Used to decide whether to emit a `<source>` element
61
+ * for that format on a `<picture>`.
62
+ */
63
+ export function hasVariantFormat(
64
+ image: StoredFileValue | undefined | null,
65
+ format: VariantFormat
66
+ ): boolean {
67
+ if (image == null) return false
68
+ return (image.variants ?? []).some((v) => v.format === format)
69
+ }
70
+
71
+ /**
72
+ * Find a named variant (matching `image.upload.sizes[].name`) on an
73
+ * upload value. Returns `undefined` if the variant has not been
74
+ * generated yet or has no storage URL.
75
+ */
76
+ export function getVariant(
77
+ image: StoredFileValue | undefined | null,
78
+ name: string
79
+ ): ResolvedVariant | undefined {
80
+ const v = image?.variants?.find((variant) => variant.name === name)
81
+ return v != null && isResolvedVariant(v) ? v : undefined
82
+ }
83
+
84
+ /**
85
+ * Resolve a single URL from an upload value, walking a preference list
86
+ * of variant names and finally falling back to the original
87
+ * `storageUrl`. Useful for non-responsive contexts that need a single
88
+ * `<img src>` (avatars, list thumbnails before they were swapped to
89
+ * `<picture>`, social-share preview URLs, etc.).
90
+ */
91
+ export function pickVariantUrl(
92
+ image: StoredFileValue | undefined | null,
93
+ ...preferred: string[]
94
+ ): string | undefined {
95
+ if (image == null) return undefined
96
+ const variants = image.variants ?? []
97
+ for (const name of preferred) {
98
+ const hit = variants.find((v) => v.name === name)
99
+ if (hit?.storageUrl != null) return hit.storageUrl
100
+ }
101
+ return image.storageUrl
102
+ }
@@ -0,0 +1,5 @@
1
+ export const toKebabCase = (string?: string): string =>
2
+ (string ?? '')
3
+ ?.replace(/([a-z])([A-Z])/g, '$1-$2')
4
+ .replace(/\s+/g, '-')
5
+ .toLowerCase()