@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.
- package/LICENSE +373 -0
- package/README.md +23 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +72 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +36 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/init.d.ts +16 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +76 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/context.d.ts +38 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +37 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/pg-url.d.ts +11 -0
- package/dist/lib/pg-url.d.ts.map +1 -0
- package/dist/lib/pg-url.js +22 -0
- package/dist/lib/pg-url.js.map +1 -0
- package/dist/manifest/deps.d.ts +29 -0
- package/dist/manifest/deps.d.ts.map +1 -0
- package/dist/manifest/deps.js +97 -0
- package/dist/manifest/deps.js.map +1 -0
- package/dist/manifest/env.d.ts +18 -0
- package/dist/manifest/env.d.ts.map +1 -0
- package/dist/manifest/env.js +38 -0
- package/dist/manifest/env.js.map +1 -0
- package/dist/phases/db-init.d.ts +3 -0
- package/dist/phases/db-init.d.ts.map +1 -0
- package/dist/phases/db-init.js +163 -0
- package/dist/phases/db-init.js.map +1 -0
- package/dist/phases/db.d.ts +11 -0
- package/dist/phases/db.d.ts.map +1 -0
- package/dist/phases/db.js +93 -0
- package/dist/phases/db.js.map +1 -0
- package/dist/phases/deps.d.ts +3 -0
- package/dist/phases/deps.d.ts.map +1 -0
- package/dist/phases/deps.js +115 -0
- package/dist/phases/deps.js.map +1 -0
- package/dist/phases/env.d.ts +3 -0
- package/dist/phases/env.d.ts.map +1 -0
- package/dist/phases/env.js +172 -0
- package/dist/phases/env.js.map +1 -0
- package/dist/phases/host.d.ts +3 -0
- package/dist/phases/host.d.ts.map +1 -0
- package/dist/phases/host.js +99 -0
- package/dist/phases/host.js.map +1 -0
- package/dist/phases/index.d.ts +7 -0
- package/dist/phases/index.d.ts.map +1 -0
- package/dist/phases/index.js +40 -0
- package/dist/phases/index.js.map +1 -0
- package/dist/phases/preflight.d.ts +4 -0
- package/dist/phases/preflight.d.ts.map +1 -0
- package/dist/phases/preflight.js +81 -0
- package/dist/phases/preflight.js.map +1 -0
- package/dist/phases/routes.d.ts +3 -0
- package/dist/phases/routes.d.ts.map +1 -0
- package/dist/phases/routes.js +145 -0
- package/dist/phases/routes.js.map +1 -0
- package/dist/phases/scaffold.d.ts +3 -0
- package/dist/phases/scaffold.d.ts.map +1 -0
- package/dist/phases/scaffold.js +113 -0
- package/dist/phases/scaffold.js.map +1 -0
- package/dist/phases/stub.d.ts +3 -0
- package/dist/phases/stub.d.ts.map +1 -0
- package/dist/phases/stub.js +25 -0
- package/dist/phases/stub.js.map +1 -0
- package/dist/phases/ui.d.ts +3 -0
- package/dist/phases/ui.d.ts.map +1 -0
- package/dist/phases/ui.js +93 -0
- package/dist/phases/ui.js.map +1 -0
- package/dist/phases/wire/index.d.ts +3 -0
- package/dist/phases/wire/index.d.ts.map +1 -0
- package/dist/phases/wire/index.js +67 -0
- package/dist/phases/wire/index.js.map +1 -0
- package/dist/phases/wire/root-tsx.d.ts +3 -0
- package/dist/phases/wire/root-tsx.d.ts.map +1 -0
- package/dist/phases/wire/root-tsx.js +57 -0
- package/dist/phases/wire/root-tsx.js.map +1 -0
- package/dist/phases/wire/server-ts.d.ts +3 -0
- package/dist/phases/wire/server-ts.d.ts.map +1 -0
- package/dist/phases/wire/server-ts.js +54 -0
- package/dist/phases/wire/server-ts.js.map +1 -0
- package/dist/phases/wire/shared.d.ts +34 -0
- package/dist/phases/wire/shared.d.ts.map +1 -0
- package/dist/phases/wire/shared.js +2 -0
- package/dist/phases/wire/shared.js.map +1 -0
- package/dist/phases/wire/start-ts.d.ts +3 -0
- package/dist/phases/wire/start-ts.d.ts.map +1 -0
- package/dist/phases/wire/start-ts.js +149 -0
- package/dist/phases/wire/start-ts.js.map +1 -0
- package/dist/phases/wire/tsconfig.d.ts +3 -0
- package/dist/phases/wire/tsconfig.d.ts.map +1 -0
- package/dist/phases/wire/tsconfig.js +105 -0
- package/dist/phases/wire/tsconfig.js.map +1 -0
- package/dist/phases/wire/vite-config.d.ts +3 -0
- package/dist/phases/wire/vite-config.d.ts.map +1 -0
- package/dist/phases/wire/vite-config.js +46 -0
- package/dist/phases/wire/vite-config.js.map +1 -0
- package/dist/prompts.d.ts +34 -0
- package/dist/prompts.d.ts.map +1 -0
- package/dist/prompts.js +49 -0
- package/dist/prompts.js.map +1 -0
- package/dist/runner.d.ts +5 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +91 -0
- package/dist/runner.js.map +1 -0
- package/dist/state.d.ts +18 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +68 -0
- package/dist/state.js.map +1 -0
- package/dist/templates/byline/admin.config.ts +41 -0
- package/dist/templates/byline/i18n.ts +47 -0
- package/dist/templates/byline/routes.ts +28 -0
- package/dist/templates/byline/seed.ts +19 -0
- package/dist/templates/byline/seeds/admin.ts +62 -0
- package/dist/templates/byline/server.config.ts +92 -0
- package/dist/templates/byline-examples/admin.config.ts +74 -0
- package/dist/templates/byline-examples/blocks/photo-block.ts +59 -0
- package/dist/templates/byline-examples/blocks/richtext-block.ts +35 -0
- package/dist/templates/byline-examples/collections/doc-example-flat-locale-all.ts +373 -0
- package/dist/templates/byline-examples/collections/doc-example-flat-locale-en.ts +283 -0
- package/dist/templates/byline-examples/collections/doc-example-tree-locale-all.ts +278 -0
- package/dist/templates/byline-examples/collections/doc-example-tree-locale-en.ts +205 -0
- package/dist/templates/byline-examples/collections/docs/admin.tsx +204 -0
- package/dist/templates/byline-examples/collections/docs/components/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/docs/components/feature-formatter.tsx +10 -0
- package/dist/templates/byline-examples/collections/docs/hooks/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/docs/index.ts +10 -0
- package/dist/templates/byline-examples/collections/docs/schema.ts +209 -0
- package/dist/templates/byline-examples/collections/docs-categories/admin.tsx +78 -0
- package/dist/templates/byline-examples/collections/docs-categories/components/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/docs-categories/hooks/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/docs-categories/index.ts +10 -0
- package/dist/templates/byline-examples/collections/docs-categories/schema.ts +33 -0
- package/dist/templates/byline-examples/collections/media/admin.tsx +188 -0
- package/dist/templates/byline-examples/collections/media/components/media-list-view.tsx +330 -0
- package/dist/templates/byline-examples/collections/media/components/media-thumbnail.tsx +63 -0
- package/dist/templates/byline-examples/collections/media/hooks/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/media/index.ts +10 -0
- package/dist/templates/byline-examples/collections/media/schema.ts +157 -0
- package/dist/templates/byline-examples/collections/news/admin.tsx +192 -0
- package/dist/templates/byline-examples/collections/news/components/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/news/hooks/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/news/index.ts +10 -0
- package/dist/templates/byline-examples/collections/news/schema.ts +91 -0
- package/dist/templates/byline-examples/collections/news-categories/admin.tsx +78 -0
- package/dist/templates/byline-examples/collections/news-categories/components/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/news-categories/hooks/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/news-categories/index.ts +10 -0
- package/dist/templates/byline-examples/collections/news-categories/schema.ts +33 -0
- package/dist/templates/byline-examples/collections/pages/admin.tsx +183 -0
- package/dist/templates/byline-examples/collections/pages/components/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/pages/hooks/.gitkeep +0 -0
- package/dist/templates/byline-examples/collections/pages/index.ts +10 -0
- package/dist/templates/byline-examples/collections/pages/schema.ts +96 -0
- package/dist/templates/byline-examples/components/length-indicator.tsx +138 -0
- package/dist/templates/byline-examples/components/pill.tsx +38 -0
- package/dist/templates/byline-examples/components/summary-length.tsx +39 -0
- package/dist/templates/byline-examples/fields/available-languages-field.ts +90 -0
- package/dist/templates/byline-examples/fields/lexical-richtext-compact.ts +88 -0
- package/dist/templates/byline-examples/i18n.ts +47 -0
- package/dist/templates/byline-examples/routes.ts +28 -0
- package/dist/templates/byline-examples/scripts/regenerate-media.ts +275 -0
- package/dist/templates/byline-examples/seed.ts +25 -0
- package/dist/templates/byline-examples/seeds/admin.ts +62 -0
- package/dist/templates/byline-examples/seeds/doc-categories.ts +71 -0
- package/dist/templates/byline-examples/seeds/docs.ts +293 -0
- package/dist/templates/byline-examples/seeds/news-categories.ts +71 -0
- package/dist/templates/byline-examples/server.config.ts +179 -0
- package/dist/templates/host/vite.config.ts +41 -0
- package/dist/templates/migrations/0000_condemned_kronos.sql +324 -0
- package/dist/templates/migrations/0001_sudden_phantom_reporter.sql +1 -0
- package/dist/templates/migrations/meta/0000_snapshot.json +2793 -0
- package/dist/templates/migrations/meta/0001_snapshot.json +2799 -0
- package/dist/templates/migrations/meta/_journal.json +20 -0
- package/dist/templates/routes/(byline)/admin/account/index.tsx +11 -0
- package/dist/templates/routes/(byline)/admin/collections/$collection/$id/api.tsx +16 -0
- package/dist/templates/routes/(byline)/admin/collections/$collection/$id/history.tsx +19 -0
- package/dist/templates/routes/(byline)/admin/collections/$collection/$id/index.tsx +16 -0
- package/dist/templates/routes/(byline)/admin/collections/$collection/create.tsx +11 -0
- package/dist/templates/routes/(byline)/admin/collections/$collection/index.tsx +11 -0
- package/dist/templates/routes/(byline)/admin/index.tsx +11 -0
- package/dist/templates/routes/(byline)/admin/permissions/index.tsx +11 -0
- package/dist/templates/routes/(byline)/admin/roles/$id/index.tsx +11 -0
- package/dist/templates/routes/(byline)/admin/roles/index.tsx +11 -0
- package/dist/templates/routes/(byline)/admin/route.tsx +11 -0
- package/dist/templates/routes/(byline)/admin/users/$id/index.tsx +11 -0
- package/dist/templates/routes/(byline)/admin/users/index.tsx +11 -0
- package/dist/templates/routes/(byline)/sign-in.tsx +11 -0
- package/dist/templates/ui-byline/blocks/photo-block/index.tsx +80 -0
- package/dist/templates/ui-byline/blocks/richtext-block/index.tsx +46 -0
- package/dist/templates/ui-byline/components/admonition/index.tsx +40 -0
- package/dist/templates/ui-byline/components/code/code-serializer.tsx +20 -0
- package/dist/templates/ui-byline/components/code/code.tsx +50 -0
- package/dist/templates/ui-byline/components/code/index.module.scss +137 -0
- package/dist/templates/ui-byline/components/code/index.ts +2 -0
- package/dist/templates/ui-byline/components/code/types.ts +5 -0
- package/dist/templates/ui-byline/components/code/utils.ts +20 -0
- package/dist/templates/ui-byline/components/heading-anchor/heading-anchor.tsx +69 -0
- package/dist/templates/ui-byline/components/heading-anchor/index.ts +1 -0
- package/dist/templates/ui-byline/components/heading-anchor/utils.ts +15 -0
- package/dist/templates/ui-byline/components/inline-image/index.tsx +109 -0
- package/dist/templates/ui-byline/components/layout/index.tsx +63 -0
- package/dist/templates/ui-byline/components/link/lang-link.tsx +70 -0
- package/dist/templates/ui-byline/components/link/link-field.tsx +298 -0
- package/dist/templates/ui-byline/components/link/link-lexical.tsx +191 -0
- package/dist/templates/ui-byline/components/list/index.ts +2 -0
- package/dist/templates/ui-byline/components/list/list-item.tsx +32 -0
- package/dist/templates/ui-byline/components/list/list.tsx +17 -0
- package/dist/templates/ui-byline/components/responsive-image/index.tsx +205 -0
- package/dist/templates/ui-byline/components/richtext-lexical/index.tsx +31 -0
- package/dist/templates/ui-byline/components/richtext-lexical/serialize/index.tsx +249 -0
- package/dist/templates/ui-byline/components/richtext-lexical/serialize/richtext-node-formats.ts +66 -0
- package/dist/templates/ui-byline/components/richtext-lexical/serialize/types.ts +48 -0
- package/dist/templates/ui-byline/components/richtext-lexical/serialize/utils.ts +15 -0
- package/dist/templates/ui-byline/components/table-cell/index.tsx +36 -0
- package/dist/templates/ui-byline/components/vimeo/index.tsx +21 -0
- package/dist/templates/ui-byline/components/youtube/index.tsx +22 -0
- package/dist/templates/ui-byline/render-blocks.tsx +71 -0
- package/dist/templates/ui-byline/types/i18n.ts +14 -0
- package/dist/templates/ui-byline/utils/image-sources.ts +102 -0
- package/dist/templates/ui-byline/utils/to-kebab-case.ts +5 -0
- package/dist/types.d.ts +54 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/diff.d.ts +4 -0
- package/dist/ui/diff.d.ts.map +1 -0
- package/dist/ui/diff.js +23 -0
- package/dist/ui/diff.js.map +1 -0
- package/dist/ui/grid.d.ts +7 -0
- package/dist/ui/grid.d.ts.map +1 -0
- package/dist/ui/grid.js +24 -0
- package/dist/ui/grid.js.map +1 -0
- package/dist/ui/logger.d.ts +14 -0
- package/dist/ui/logger.d.ts.map +1 -0
- package/dist/ui/logger.js +30 -0
- package/dist/ui/logger.js.map +1 -0
- package/dist/ui/snippet.d.ts +2 -0
- package/dist/ui/snippet.d.ts.map +1 -0
- package/dist/ui/snippet.js +7 -0
- package/dist/ui/snippet.js.map +1 -0
- 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
|
+
}
|
package/dist/templates/ui-byline/components/richtext-lexical/serialize/richtext-node-formats.ts
ADDED
|
@@ -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
|
+
}
|