blocks-dusted 0.1.11 → 0.1.13
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/package.json +5 -2
- package/src/cli.js +39 -3
- package/src/commands/shop.js +59 -0
- package/src/commands/warehouse.js +73 -0
- package/src/installers/patchRenderBlocks.js +1 -1
- package/src/registry/validateTemplateManifest.js +2 -1
- package/templates/blocks/BannerDD790432/Component.tsx +63 -0
- package/templates/blocks/BannerDD790432/config.ts +54 -0
- package/templates/blocks/BannerDD790432/manifest.json +80 -0
- package/templates/blocks/Button47/Component.tsx +124 -0
- package/templates/blocks/Button47/config.ts +75 -0
- package/templates/blocks/Button47/link.ts +22 -0
- package/templates/blocks/Button47/manifest.json +105 -0
- package/templates/blocks/Button47/styles.module.scss +75 -0
- package/templates/blocks/DD-Accordion/Component.tsx +55 -36
- package/templates/blocks/DD-Accordion/config.ts +71 -59
- package/templates/blocks/DD-AnimText/Component.tsx +50 -32
- package/templates/blocks/DD-AnimText/config.ts +296 -295
- package/templates/blocks/DD-AsideNav/Component.tsx +323 -319
- package/templates/blocks/DD-Banner/Component.tsx +54 -51
- package/templates/blocks/DD-BookCall/Component.tsx +10 -9
- package/templates/blocks/DD-ButtonPop/Component.tsx +118 -117
- package/templates/blocks/DD-Card/Component.tsx +4 -4
- package/templates/blocks/DD-CardTemplate01/Component.tsx +5 -5
- package/templates/blocks/DD-CardsWithVariantV3/Component.tsx +472 -0
- package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/index.tsx +203 -0
- package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/styles.ts +87 -0
- package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/useInjectStyle.ts +39 -0
- package/templates/blocks/DD-CardsWithVariantV3/config.ts +326 -0
- package/templates/blocks/DD-CardsWithVariantV3/manifest.json +110 -0
- package/templates/blocks/DD-Carousel-A/Component.tsx +251 -249
- package/templates/blocks/DD-Carousel-B/Component.tsx +405 -403
- package/templates/blocks/DD-Chip/Component.tsx +14 -14
- package/templates/blocks/DD-ComparisonTable/Component.tsx +255 -256
- package/templates/blocks/DD-Contact/Component.tsx +433 -434
- package/templates/blocks/DD-Counter/Component.tsx +18 -13
- package/templates/blocks/DD-DeliCard/Component.tsx +303 -0
- package/templates/blocks/DD-DeliCard/config.ts +186 -0
- package/templates/blocks/DD-DeliCard/manifest.json +94 -0
- package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
- package/templates/blocks/DD-FeatProd/CircularGallery.tsx +1179 -0
- package/templates/blocks/DD-FeatProd/Component.tsx +484 -0
- package/templates/blocks/DD-FeatProd/config.ts +173 -0
- package/templates/blocks/DD-FeatProd/manifest.json +93 -0
- package/templates/blocks/DD-FeatServ/Component.tsx +607 -0
- package/templates/blocks/DD-FeatServ/config.ts +213 -0
- package/templates/blocks/DD-FeatServ/manifest.json +113 -0
- package/templates/blocks/DD-FeatStrip/Component.tsx +584 -585
- package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +627 -628
- package/templates/blocks/DD-FluidBtn/Component.tsx +46 -43
- package/templates/blocks/DD-GooeyInfo/Component.tsx +150 -149
- package/templates/blocks/DD-GooeyInfo/config.ts +136 -135
- package/templates/blocks/DD-Hero/Component.tsx +16 -40
- package/templates/blocks/DD-HeroPlas/Component.tsx +50 -0
- package/templates/blocks/DD-HeroPlas/ThreeScene.tsx +109 -0
- package/templates/blocks/DD-HeroPlas/config.ts +33 -0
- package/templates/blocks/DD-HeroPlas/manifest.json +88 -0
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +4 -5
- package/templates/blocks/DD-Icon/Component.tsx +46 -81
- package/templates/blocks/DD-IconButton/Component.tsx +13 -20
- package/templates/blocks/DD-IdTag/Component.tsx +22 -21
- package/templates/blocks/DD-IframeMedia/Component.tsx +14 -10
- package/templates/blocks/DD-LineAnim/Component.tsx +42 -36
- package/templates/blocks/DD-LogoMarq/Component.tsx +38 -48
- package/templates/blocks/DD-Lottie/Component.tsx +11 -3
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +544 -547
- package/templates/blocks/DD-MasonaryMedia/config.ts +389 -389
- package/templates/blocks/DD-Masonry/Component.tsx +654 -644
- package/templates/blocks/DD-Masonry/config.ts +475 -475
- package/templates/blocks/DD-MaterialSwatches/Component.tsx +85 -0
- package/templates/blocks/DD-MaterialSwatches/config.ts +232 -0
- package/templates/blocks/DD-MaterialSwatches/manifest.json +84 -0
- package/templates/blocks/DD-MaterialSwatches/material.ts +107 -0
- package/templates/blocks/DD-NineCanvas/Component.tsx +129 -0
- package/templates/blocks/DD-NineCanvas/RenderBlocks.tsx +244 -0
- package/templates/blocks/DD-NineCanvas/allowedCanvasBlocks.ts +6 -0
- package/templates/blocks/DD-NineCanvas/blockComponents.tsx +8 -0
- package/templates/blocks/DD-NineCanvas/canvas/CanvasGrid.tsx +316 -0
- package/templates/blocks/DD-NineCanvas/canvas/RadialNav.tsx +432 -0
- package/templates/blocks/DD-NineCanvas/canvas/canvas-panels.css +889 -0
- package/templates/blocks/DD-NineCanvas/canvas/icons/custom-icons.tsx +364 -0
- package/templates/blocks/DD-NineCanvas/config.ts +61 -0
- package/templates/blocks/DD-NineCanvas/manifest.json +165 -0
- package/templates/blocks/DD-NineCanvas/styles.scss +152 -0
- package/templates/blocks/DD-NineCanvas/tooltip/get-strict-context.tsx +26 -0
- package/templates/blocks/DD-NineCanvas/tooltip/primitives.tsx +178 -0
- package/templates/blocks/DD-NineCanvas/tooltip/tooltip.tsx +71 -0
- package/templates/blocks/DD-NineCanvas/tooltip/use-controlled-state.tsx +33 -0
- package/templates/blocks/DD-NineCanvas/utilities/FadeOnScroll.tsx +68 -0
- package/templates/blocks/DD-NineCanvas/utilities/ParallaxWrapper.tsx +157 -0
- package/templates/blocks/DD-NineCanvas/utilities/renderCustomCSS.ts +5 -0
- package/templates/blocks/DD-NineCanvas/utilities/webglCleanup.ts +27 -0
- package/templates/blocks/DD-PayloadAIAssistant/Component.tsx +78 -0
- package/templates/blocks/DD-PayloadAIAssistant/DustedBot.tsx +590 -0
- package/templates/blocks/DD-PayloadAIAssistant/config.ts +37 -0
- package/templates/blocks/DD-PayloadAIAssistant/demo.css +125 -0
- package/templates/blocks/DD-PayloadAIAssistant/manifest.json +93 -0
- package/templates/blocks/DD-PayloadAIAssistant/source-defaults.ts +17 -0
- package/templates/blocks/DD-PayloadAIAssistant/styles.ts +306 -0
- package/templates/blocks/DD-Pricing/Component.tsx +368 -372
- package/templates/blocks/DD-Pricing/config.ts +215 -215
- package/templates/blocks/DD-Process/Component.tsx +147 -149
- package/templates/blocks/DD-Progress/Component.tsx +156 -141
- package/templates/blocks/DD-ProjHeader/Component.tsx +93 -91
- package/templates/blocks/DD-ProjSlider/Component.tsx +1 -1
- package/templates/blocks/DD-ProjSlider/config.ts +2 -2
- package/templates/blocks/DD-Section/Component.tsx +265 -266
- package/templates/blocks/DD-Separator/Component.tsx +66 -66
- package/templates/blocks/DD-Services/Component.tsx +176 -176
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +306 -307
- package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
- package/templates/blocks/DD-StackCards/Component.tsx +474 -473
- package/templates/blocks/DD-Team/Component.tsx +247 -247
- package/templates/blocks/DD-TeamCards/Component.tsx +251 -251
- package/templates/blocks/DD-TechStack/Component.tsx +161 -168
- package/templates/blocks/DD-TestMarq/Component.tsx +158 -132
- package/templates/blocks/DD-Testim/Component.tsx +247 -247
- package/templates/blocks/DD-Testim2/Component.tsx +265 -256
- package/templates/blocks/DD-Testimonials/Component.tsx +139 -147
- package/templates/blocks/DD-TextMarq/Component.tsx +157 -157
- package/templates/blocks/DD-Thanks/Component.tsx +501 -500
- package/templates/blocks/DD-Work/Component.tsx +315 -315
- package/templates/blocks/DD-Work-B/Component.tsx +294 -294
- package/templates/blocks/DDHG-CTASection2/Component.tsx +183 -184
- package/templates/blocks/DDHG-HighlightArc/Component.tsx +246 -243
- package/templates/blocks/DDHG-LogoMarquee/Component.tsx +180 -93
- package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -66
- package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +225 -213
- package/templates/blocks/DDHG-ProcessSec/Component.tsx +38 -39
- package/templates/blocks/DDHG-ServiceDetails/Component.tsx +11 -11
- package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +30 -47
- package/templates/blocks/DDHG-ServiceHero/Component.tsx +10 -10
- package/templates/blocks/DDHG-ServiceOverview/Component.tsx +23 -23
- package/templates/blocks/DDHG-StatsFAQ/Component.tsx +40 -40
- package/templates/blocks/DDHG-TeamCards/Component.tsx +140 -131
- package/templates/blocks/Form/Checkbox/index.tsx +45 -0
- package/templates/blocks/Form/Component.tsx +163 -0
- package/templates/blocks/Form/Country/index.tsx +65 -0
- package/templates/blocks/Form/Country/options.ts +982 -0
- package/templates/blocks/Form/Email/index.tsx +38 -0
- package/templates/blocks/Form/Error/index.tsx +15 -0
- package/templates/blocks/Form/Message/index.tsx +13 -0
- package/templates/blocks/Form/Number/index.tsx +36 -0
- package/templates/blocks/Form/Select/index.tsx +63 -0
- package/templates/blocks/Form/State/index.tsx +64 -0
- package/templates/blocks/Form/State/options.ts +52 -0
- package/templates/blocks/Form/Text/index.tsx +32 -0
- package/templates/blocks/Form/Textarea/index.tsx +40 -0
- package/templates/blocks/Form/Width/index.tsx +13 -0
- package/templates/blocks/Form/config.ts +51 -0
- package/templates/blocks/Form/fields.tsx +21 -0
- package/templates/blocks/Form/manifest.json +168 -0
- package/templates/blocks/MediaBlock/Component.tsx +67 -0
- package/templates/blocks/MediaBlock/config.ts +14 -0
- package/templates/blocks/MediaBlock/manifest.json +92 -0
- package/templates/blocks/TransitionsDustedShowcase/Component.tsx +9 -0
- package/templates/blocks/TransitionsDustedShowcase/config.ts +13 -0
- package/templates/blocks/TransitionsDustedShowcase/manifest.json +80 -0
- package/templates/blocks/rawHtml/Component.tsx +25 -0
- package/templates/blocks/rawHtml/config.ts +56 -0
- package/templates/blocks/rawHtml/manifest.json +80 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blocks-dusted",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.13",
|
|
4
4
|
"description": "Manifest-driven CLI for installing reusable Payload CMS blocks.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,10 @@
|
|
|
23
23
|
"test:add": "node ./bin/index.js add DD-Hero --dry-run",
|
|
24
24
|
"test:list": "node ./bin/index.js list",
|
|
25
25
|
"test:doctor": "node ./bin/index.js doctor",
|
|
26
|
-
"convert:studio": "node ./scripts/convertStudioBlock.mjs"
|
|
26
|
+
"convert:studio": "node ./scripts/convertStudioBlock.mjs",
|
|
27
|
+
"warehouse:sync-lab": "node ./bin/index.js warehouse sync-lab",
|
|
28
|
+
"warehouse:sync-lab:dry-run": "node ./bin/index.js warehouse sync-lab --dry-run",
|
|
29
|
+
"shop:pull-sandbox:dry-run": "node ./bin/index.js shop pull-sandbox --dry-run"
|
|
27
30
|
},
|
|
28
31
|
"engines": {
|
|
29
32
|
"node": ">=20"
|
package/src/cli.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
import { addTemplate } from './commands/add.js'
|
|
4
4
|
import { doctor } from './commands/doctor.js'
|
|
5
5
|
import { addHeader } from './commands/header.js'
|
|
6
|
-
import { list } from './commands/list.js'
|
|
6
|
+
import { list } from './commands/list.js'
|
|
7
|
+
import { pullSandboxFromWarehouse } from './commands/shop.js'
|
|
8
|
+
import { syncLabToWarehouse } from './commands/warehouse.js'
|
|
7
9
|
import { error, info } from './utils/logger.js'
|
|
8
10
|
import { packageRoot } from './utils/paths.js'
|
|
9
11
|
|
|
@@ -14,7 +16,9 @@ function printHelp() {
|
|
|
14
16
|
info(' blocks-dusted list')
|
|
15
17
|
info(' blocks-dusted doctor [--cwd <path>]')
|
|
16
18
|
info(' blocks-dusted add <TemplateName> [--dry-run] [--no-register] [--cwd <path>]')
|
|
17
|
-
info(' blocks-dusted header add <HeaderName> [--dry-run] [--cwd <path>]')
|
|
19
|
+
info(' blocks-dusted header add <HeaderName> [--dry-run] [--cwd <path>]')
|
|
20
|
+
info(' blocks-dusted warehouse sync-lab [--dry-run] [--lab <path>]')
|
|
21
|
+
info(' blocks-dusted shop pull-sandbox [--dry-run] [--warehouse <path>] [--cwd <shop path>]')
|
|
18
22
|
info(' blocks-dusted --version')
|
|
19
23
|
}
|
|
20
24
|
|
|
@@ -57,11 +61,42 @@ export async function runCli(args, options = {}) {
|
|
|
57
61
|
})
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
if (command === 'warehouse') {
|
|
65
|
+
const [subcommand] = rest
|
|
66
|
+
if (subcommand !== 'sync-lab') throw new Error('Usage: blocks-dusted warehouse sync-lab [--dry-run] [--lab <path>]')
|
|
67
|
+
return syncLabToWarehouse({
|
|
68
|
+
labRoot: optionValue(rest, '--lab'),
|
|
69
|
+
dryRun: rest.includes('--dry-run'),
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (command === 'shop') {
|
|
74
|
+
const [subcommand] = rest
|
|
75
|
+
if (subcommand !== 'pull-sandbox') throw new Error('Usage: blocks-dusted shop pull-sandbox [--dry-run] [--warehouse <path>] [--cwd <shop path>]')
|
|
76
|
+
return pullSandboxFromWarehouse({
|
|
77
|
+
targetDirectory,
|
|
78
|
+
warehouseRoot: optionValue(rest, '--warehouse'),
|
|
79
|
+
dryRun: rest.includes('--dry-run'),
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
60
83
|
error(`Unknown command: ${command}`)
|
|
61
84
|
printHelp()
|
|
62
85
|
process.exitCode = 1
|
|
63
86
|
}
|
|
64
87
|
|
|
88
|
+
function optionValue(args, name) {
|
|
89
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
90
|
+
const argument = args[index]
|
|
91
|
+
if (argument === name) {
|
|
92
|
+
const value = args[index + 1]
|
|
93
|
+
if (!value) throw new Error(`${name} requires a path`)
|
|
94
|
+
return value
|
|
95
|
+
}
|
|
96
|
+
if (argument.startsWith(`${name}=`)) return argument.slice(name.length + 1)
|
|
97
|
+
}
|
|
98
|
+
return undefined
|
|
99
|
+
}
|
|
65
100
|
function parseGlobalOptions(args, originalDirectory) {
|
|
66
101
|
const commandArgs = []
|
|
67
102
|
let targetDirectory = originalDirectory
|
|
@@ -90,3 +125,4 @@ async function packageVersion() {
|
|
|
90
125
|
return packageJson.version
|
|
91
126
|
}
|
|
92
127
|
|
|
128
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat, copyFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
3
|
+
import { packageRoot } from '../utils/paths.js'
|
|
4
|
+
|
|
5
|
+
const defaultSandboxBlocks = join('src', 'sandbox', 'products', 'DDBlockProductSandboxes', 'blocks')
|
|
6
|
+
const excludedTemplateFiles = new Set(['config.ts', 'manifest.json', 'README.md'])
|
|
7
|
+
|
|
8
|
+
export async function pullSandboxFromWarehouse({ targetDirectory = process.cwd(), warehouseRoot = packageRoot, dryRun = false } = {}) {
|
|
9
|
+
const sourceRoot = join(resolve(warehouseRoot), 'templates', 'blocks')
|
|
10
|
+
const targetRoot = join(resolve(targetDirectory), defaultSandboxBlocks)
|
|
11
|
+
const blockEntries = await readdir(sourceRoot, { withFileTypes: true })
|
|
12
|
+
const results = []
|
|
13
|
+
const errors = []
|
|
14
|
+
|
|
15
|
+
for (const entry of blockEntries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
16
|
+
if (!entry.isDirectory()) continue
|
|
17
|
+
const blockName = entry.name
|
|
18
|
+
const manifestPath = join(sourceRoot, blockName, 'manifest.json')
|
|
19
|
+
let manifest
|
|
20
|
+
try {
|
|
21
|
+
manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
22
|
+
} catch (error) {
|
|
23
|
+
errors.push(`${blockName}: unable to read manifest: ${error.message}`)
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const file of manifest.files ?? []) {
|
|
28
|
+
const from = String(file.from ?? '')
|
|
29
|
+
if (!from || excludedTemplateFiles.has(from.replace(/\\/g, '/'))) continue
|
|
30
|
+
const source = join(sourceRoot, blockName, from)
|
|
31
|
+
const target = join(targetRoot, blockName, from)
|
|
32
|
+
try {
|
|
33
|
+
const sourceStat = await stat(source)
|
|
34
|
+
if (!sourceStat.isFile()) {
|
|
35
|
+
errors.push(`${blockName}: source is not a file: ${relative(resolve(warehouseRoot), source)}`)
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
results.push({ blockName, from: relative(resolve(warehouseRoot), source), to: relative(resolve(targetDirectory), target) })
|
|
39
|
+
if (!dryRun) {
|
|
40
|
+
await mkdir(dirname(target), { recursive: true })
|
|
41
|
+
await copyFile(source, target)
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
errors.push(`${blockName}: missing warehouse runtime file ${from}: ${error.message}`)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(`Warehouse to Shop sandbox pull${dryRun ? ' dry run' : ''}`)
|
|
50
|
+
console.log(`Target: ${targetRoot}`)
|
|
51
|
+
console.log(`Files ${dryRun ? 'planned' : 'copied'}: ${results.length}`)
|
|
52
|
+
for (const row of results) console.log(`- ${row.blockName}: ${row.from} -> ${row.to}`)
|
|
53
|
+
if (errors.length > 0) {
|
|
54
|
+
console.error('Errors:')
|
|
55
|
+
for (const error of errors) console.error(`- ${error}`)
|
|
56
|
+
throw new Error(`Warehouse to Shop pull failed with ${errors.length} error(s).`)
|
|
57
|
+
}
|
|
58
|
+
return { results, errors }
|
|
59
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { mkdir, readdir, readFile, stat, copyFile } from 'node:fs/promises'
|
|
2
|
+
import { dirname, join, relative, resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
|
|
5
|
+
const packageRoot = resolve(fileURLToPath(new URL('../..', import.meta.url)))
|
|
6
|
+
const defaultLabRoot = resolve(packageRoot, '..', 'CLI-TestGround', 'cli-testground')
|
|
7
|
+
const blockTemplateRoot = join(packageRoot, 'templates', 'blocks')
|
|
8
|
+
|
|
9
|
+
export async function syncLabToWarehouse({ labRoot = defaultLabRoot, dryRun = false } = {}) {
|
|
10
|
+
const labBlocksRoot = join(resolve(labRoot), 'src', 'blocks')
|
|
11
|
+
const manifests = await readBlockManifests(blockTemplateRoot)
|
|
12
|
+
const results = []
|
|
13
|
+
const errors = []
|
|
14
|
+
|
|
15
|
+
for (const { blockName, manifest, manifestDir } of manifests) {
|
|
16
|
+
for (const file of manifest.files ?? []) {
|
|
17
|
+
const from = String(file.from ?? '')
|
|
18
|
+
if (!from) continue
|
|
19
|
+
const source = join(labBlocksRoot, blockName, from)
|
|
20
|
+
const target = join(manifestDir, from)
|
|
21
|
+
try {
|
|
22
|
+
const sourceStat = await stat(source)
|
|
23
|
+
if (!sourceStat.isFile()) {
|
|
24
|
+
errors.push(`${blockName}: source is not a file: ${relative(resolve(labRoot), source)}`)
|
|
25
|
+
continue
|
|
26
|
+
}
|
|
27
|
+
results.push({ blockName, from: relative(resolve(labRoot), source), to: relative(packageRoot, target) })
|
|
28
|
+
if (!dryRun) {
|
|
29
|
+
await mkdir(dirname(target), { recursive: true })
|
|
30
|
+
await copyFile(source, target)
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
errors.push(`${blockName}: missing lab file for manifest entry ${from}: ${error.message}`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
printSyncResults({ title: 'Lab to Warehouse sync', dryRun, results, errors })
|
|
39
|
+
if (errors.length > 0) {
|
|
40
|
+
const error = new Error(`Lab to Warehouse sync failed with ${errors.length} error(s).`)
|
|
41
|
+
error.results = { results, errors }
|
|
42
|
+
throw error
|
|
43
|
+
}
|
|
44
|
+
return { results, errors }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function readBlockManifests(root) {
|
|
48
|
+
const entries = await readdir(root, { withFileTypes: true })
|
|
49
|
+
const manifests = []
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.isDirectory()) continue
|
|
52
|
+
const manifestDir = join(root, entry.name)
|
|
53
|
+
const manifestPath = join(manifestDir, 'manifest.json')
|
|
54
|
+
try {
|
|
55
|
+
const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
|
|
56
|
+
manifests.push({ blockName: entry.name, manifest, manifestDir })
|
|
57
|
+
} catch (error) {
|
|
58
|
+
throw new Error(`Unable to read ${relative(packageRoot, manifestPath)}: ${error.message}`)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
manifests.sort((a, b) => a.blockName.localeCompare(b.blockName))
|
|
62
|
+
return manifests
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function printSyncResults({ title, dryRun, results, errors }) {
|
|
66
|
+
console.log(`${title}${dryRun ? ' dry run' : ''}`)
|
|
67
|
+
console.log(`Files ${dryRun ? 'planned' : 'copied'}: ${results.length}`)
|
|
68
|
+
for (const row of results) console.log(`- ${row.blockName}: ${row.from} -> ${row.to}`)
|
|
69
|
+
if (errors.length > 0) {
|
|
70
|
+
console.error('Errors:')
|
|
71
|
+
for (const error of errors) console.error(`- ${error}`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -46,7 +46,7 @@ async function exists(path) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
function patchRenderBlocksContent(content, manifest) {
|
|
49
|
-
const exportName = manifest.exportName ?? manifest.name
|
|
49
|
+
const exportName = manifest.componentExportName ?? manifest.exportName ?? manifest.name
|
|
50
50
|
if (content.includes(manifest.slug) || content.includes(`@/blocks/${manifest.name}/Component`)) {
|
|
51
51
|
return { changed: false, reason: 'RenderBlocks already appears to contain this block' }
|
|
52
52
|
}
|
|
@@ -26,6 +26,7 @@ export function validateTemplateManifest(manifest) {
|
|
|
26
26
|
if (manifest.sourceName !== undefined && typeof manifest.sourceName !== 'string') errors.push('sourceName must be a string when present')
|
|
27
27
|
if (manifest.portableName !== undefined && typeof manifest.portableName !== 'string') errors.push('portableName must be a string when present')
|
|
28
28
|
if (manifest.exportName !== undefined && typeof manifest.exportName !== 'string') errors.push('exportName must be a string when present')
|
|
29
|
+
if (manifest.componentExportName !== undefined && typeof manifest.componentExportName !== 'string') errors.push('componentExportName must be a string when present')
|
|
29
30
|
if (manifest.interfaceName !== undefined && typeof manifest.interfaceName !== 'string') errors.push('interfaceName must be a string when present')
|
|
30
31
|
|
|
31
32
|
for (const key of OPTIONAL_ARRAYS) {
|
|
@@ -67,4 +68,4 @@ export function validateTemplateManifest(manifest) {
|
|
|
67
68
|
|
|
68
69
|
if (errors.length > 0) throw new Error(`Invalid template manifest:\n- ${errors.join('\n- ')}`)
|
|
69
70
|
return manifest
|
|
70
|
-
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { cn } from '@/utilities/ui'
|
|
2
|
+
import React, { useId } from 'react'
|
|
3
|
+
// import RichText from '@/components/RichText'
|
|
4
|
+
|
|
5
|
+
const BANNER_BEFORE_CLS =
|
|
6
|
+
'relative z-0 ' +
|
|
7
|
+
"before:content-[''] before:absolute before:inset-0 before:z-[-1] " +
|
|
8
|
+
'before:bg-lines before:bg-[length:10px_10px] ' +
|
|
9
|
+
'before:bg-repeat before:opacity-100 before:pointer-events-none'
|
|
10
|
+
|
|
11
|
+
export type BannerDD790432Props = {
|
|
12
|
+
className?: string
|
|
13
|
+
wrapperClassName?: string
|
|
14
|
+
content?: unknown
|
|
15
|
+
style?: 'info' | 'warning' | 'error' | 'success' | null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const BannerDD790432: React.FC<BannerDD790432Props> = ({
|
|
19
|
+
className,
|
|
20
|
+
wrapperClassName,
|
|
21
|
+
content,
|
|
22
|
+
style,
|
|
23
|
+
}) => {
|
|
24
|
+
const reactId = useId()
|
|
25
|
+
const richtextKey = `banner-richtext-${reactId.replace(/:/g, '')}`
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className={cn('mx-auto my-8 w-full banner_container', className)}>
|
|
29
|
+
<div
|
|
30
|
+
className={cn(
|
|
31
|
+
wrapperClassName,
|
|
32
|
+
'border py-3 pl-0 pr-3 flex items-center rounded shadow-2xl banner_content',
|
|
33
|
+
BANNER_BEFORE_CLS,
|
|
34
|
+
{
|
|
35
|
+
'banner-info border-border bg-card text-(--black) [box-shadow:inset_0_0_2px_0px_var(--border),var(--shadow-xl)] [&_blockquote]:border-s-border!':
|
|
36
|
+
style === 'info',
|
|
37
|
+
'banner-error border-(--error-border) bg-error text-(--white) [box-shadow:inset_0_0_2px_0px_var(--error),var(--shadow-xl)] [&_blockquote]:border-s-(--error-border)!':
|
|
38
|
+
style === 'error',
|
|
39
|
+
'banner-success border-(--success-border) bg-success text-(--white) [box-shadow:inset_0_0_2px_0px_var(--success),var(--shadow-xl)] [&_blockquote]:border-s-(--success-border)!':
|
|
40
|
+
style === 'success',
|
|
41
|
+
'banner-warning border-(--warning-border) bg-warning text-(--white) [box-shadow:inset_0_0_2px_0px_var(--warning),var(--shadow-xl)] [&_blockquote]:border-s-(--warning-border)!':
|
|
42
|
+
style === 'warning',
|
|
43
|
+
},
|
|
44
|
+
)}
|
|
45
|
+
>
|
|
46
|
+
{/* TODO: Reconnect this project's RichText renderer for `content`. */}
|
|
47
|
+
{content ? (
|
|
48
|
+
// Replace when RichText is configured.
|
|
49
|
+
<div className="banner_richtext">Rich text content is configured for this banner.</div>
|
|
50
|
+
) : // Adjust RichText props if required.
|
|
51
|
+
// <RichText
|
|
52
|
+
// data={content}
|
|
53
|
+
// data-richtext-key={richtextKey}
|
|
54
|
+
// className="banner_richtext"
|
|
55
|
+
// enableGutter={false}
|
|
56
|
+
// enableProse={true}
|
|
57
|
+
// />
|
|
58
|
+
null}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Block } from 'payload'
|
|
2
|
+
|
|
3
|
+
export const BannerDD790432: Block = {
|
|
4
|
+
slug: 'banner-dd-790432',
|
|
5
|
+
interfaceName: 'BannerDD790432',
|
|
6
|
+
fields: [
|
|
7
|
+
{
|
|
8
|
+
name: 'style',
|
|
9
|
+
type: 'select',
|
|
10
|
+
defaultValue: 'info',
|
|
11
|
+
options: [
|
|
12
|
+
{ label: 'Info', value: 'info' },
|
|
13
|
+
{ label: 'Warning', value: 'warning' },
|
|
14
|
+
{ label: 'Error', value: 'error' },
|
|
15
|
+
{ label: 'Success', value: 'success' },
|
|
16
|
+
],
|
|
17
|
+
required: true,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: 'content',
|
|
21
|
+
type: 'richText',
|
|
22
|
+
label: false,
|
|
23
|
+
required: true,
|
|
24
|
+
admin: {
|
|
25
|
+
description:
|
|
26
|
+
'TODO: Add this project\'s RichText renderer and custom Lexical features manually after installation.',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'useCustomClasses',
|
|
31
|
+
type: 'checkbox',
|
|
32
|
+
label: 'Override default classes',
|
|
33
|
+
defaultValue: false,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: 'className',
|
|
37
|
+
type: 'text',
|
|
38
|
+
label: 'Banner container class',
|
|
39
|
+
admin: {
|
|
40
|
+
description: 'banner_container class',
|
|
41
|
+
condition: (_, siblingData) => Boolean(siblingData.useCustomClasses),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'wrapperClassName',
|
|
46
|
+
type: 'text',
|
|
47
|
+
label: 'Banner content class',
|
|
48
|
+
admin: {
|
|
49
|
+
description: 'banner_content class',
|
|
50
|
+
condition: (_, siblingData) => Boolean(siblingData.useCustomClasses),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "block",
|
|
3
|
+
"name": "BannerDD790432",
|
|
4
|
+
"label": "Banner DD 790432",
|
|
5
|
+
"sourceName": "BannerDD790432",
|
|
6
|
+
"portableName": "BannerDD790432",
|
|
7
|
+
"slug": "banner-dd-790432",
|
|
8
|
+
"version": "0.0.1",
|
|
9
|
+
"source": {
|
|
10
|
+
"repo": "Hadizainal/Dusted-Payload-Blocks",
|
|
11
|
+
"originalPath": "CLI-TestGround/dusted-blocks-demo/src/blocks/BannerDD790432",
|
|
12
|
+
"originalSlug": "banner-dd-790432",
|
|
13
|
+
"originalConfigExport": "BannerDD790432",
|
|
14
|
+
"originalComponentExport": "BannerDD790432",
|
|
15
|
+
"originalInterfaceName": "BannerDD790432"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
{
|
|
19
|
+
"from": "Component.tsx",
|
|
20
|
+
"to": "src/blocks/BannerDD790432/Component.tsx"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"from": "config.ts",
|
|
24
|
+
"to": "src/blocks/BannerDD790432/config.ts"
|
|
25
|
+
}
|
|
26
|
+
],
|
|
27
|
+
"dependencies": [],
|
|
28
|
+
"sharedFiles": [],
|
|
29
|
+
"requiredProjectFiles": [
|
|
30
|
+
{
|
|
31
|
+
"path": "src/utilities/ui.ts",
|
|
32
|
+
"reason": "Component imports cn from @/utilities/ui."
|
|
33
|
+
}
|
|
34
|
+
],
|
|
35
|
+
"optionalProjectFiles": [],
|
|
36
|
+
"collectionRegistration": {
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"collections": [
|
|
39
|
+
"Pages",
|
|
40
|
+
"Posts"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"renderBlocksRegistration": {
|
|
44
|
+
"enabled": true
|
|
45
|
+
},
|
|
46
|
+
"renderBlocksBehavior": {
|
|
47
|
+
"wrapperless": false
|
|
48
|
+
},
|
|
49
|
+
"pageRouteIntegration": {
|
|
50
|
+
"required": false,
|
|
51
|
+
"manualSteps": []
|
|
52
|
+
},
|
|
53
|
+
"manual": [
|
|
54
|
+
"Register BannerDD790432 in Pages/Posts block arrays if automatic registration was skipped.",
|
|
55
|
+
"Register banner-dd-790432 in RenderBlocks if automatic registration was skipped.",
|
|
56
|
+
"Run payload generate:types after schema changes.",
|
|
57
|
+
"Run payload generate:importmap if admin/component imports changed."
|
|
58
|
+
],
|
|
59
|
+
"manualSteps": [
|
|
60
|
+
"Register BannerDD790432 in Pages/Posts block arrays if automatic registration was skipped.",
|
|
61
|
+
"Register banner-dd-790432 in RenderBlocks if automatic registration was skipped.",
|
|
62
|
+
"Run payload generate:types after schema changes.",
|
|
63
|
+
"Run payload generate:importmap if admin/component imports changed."
|
|
64
|
+
],
|
|
65
|
+
"patchTargets": {
|
|
66
|
+
"blockConfigRegistration": {
|
|
67
|
+
"description": "Installer can register BannerDD790432 from @/blocks/BannerDD790432/config."
|
|
68
|
+
},
|
|
69
|
+
"renderBlocksRegistration": {
|
|
70
|
+
"description": "Installer can register banner-dd-790432 with component export BannerDD790432."
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"notes": [
|
|
74
|
+
"Promoted from the verified Dusted Blocks demo implementation.",
|
|
75
|
+
"Installer preserves existing destination files and reports project requirements before writing."
|
|
76
|
+
],
|
|
77
|
+
"exportName": "BannerDD790432",
|
|
78
|
+
"componentExportName": "BannerDD790432",
|
|
79
|
+
"interfaceName": "BannerDD790432"
|
|
80
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import Button from '@mui/material/Button'
|
|
4
|
+
import { icons } from 'lucide-react'
|
|
5
|
+
import { motion, useReducedMotion } from 'motion/react'
|
|
6
|
+
import Link from 'next/link'
|
|
7
|
+
import type { Button47Block } from '@/payload-types'
|
|
8
|
+
import { cn } from '@/utilities/ui'
|
|
9
|
+
import { resolveButton47Href } from './link'
|
|
10
|
+
import styles from './styles.module.scss'
|
|
11
|
+
|
|
12
|
+
const colors = {
|
|
13
|
+
contained: {
|
|
14
|
+
background: '#4338ca',
|
|
15
|
+
hover: '#3730a3',
|
|
16
|
+
foreground: '#d9f99d',
|
|
17
|
+
border: '#4338ca',
|
|
18
|
+
frame: 'var(--button47-frame)',
|
|
19
|
+
},
|
|
20
|
+
outlined: {
|
|
21
|
+
background: '#a3e635',
|
|
22
|
+
hover: '#bef264',
|
|
23
|
+
foreground: '#1e1b4b',
|
|
24
|
+
border: '#3f6212',
|
|
25
|
+
frame: 'var(--button47-frame)',
|
|
26
|
+
},
|
|
27
|
+
text: {
|
|
28
|
+
background: 'var(--button47-ghost-bg)',
|
|
29
|
+
hover: 'var(--button47-ghost-hover)',
|
|
30
|
+
foreground: 'var(--button47-ghost-text)',
|
|
31
|
+
border: 'var(--button47-frame)',
|
|
32
|
+
frame: 'transparent',
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
export function Button47({ buttons, position = 'center', customClassName }: Button47Block) {
|
|
36
|
+
const reduceMotion = useReducedMotion()
|
|
37
|
+
if (!buttons?.length) return null
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div
|
|
41
|
+
className={cn(styles.root, customClassName)}
|
|
42
|
+
style={{
|
|
43
|
+
justifyContent:
|
|
44
|
+
position === 'left' ? 'flex-start' : position === 'right' ? 'flex-end' : 'center',
|
|
45
|
+
}}
|
|
46
|
+
>
|
|
47
|
+
{buttons.map((button, index) => {
|
|
48
|
+
const href = resolveButton47Href(button.link)
|
|
49
|
+
if (!href || !button.link.label?.trim()) return null
|
|
50
|
+
const variant = button.variant || 'contained'
|
|
51
|
+
const color = colors[variant]
|
|
52
|
+
|
|
53
|
+
const Icon = button.icon ? icons[button.icon as keyof typeof icons] : undefined
|
|
54
|
+
const icon = Icon ? <Icon size={18} aria-hidden="true" focusable="false" /> : undefined
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<motion.div
|
|
58
|
+
key={button.id || index}
|
|
59
|
+
className={styles.focusFrame}
|
|
60
|
+
initial={reduceMotion ? false : { opacity: 0, y: 16 }}
|
|
61
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
62
|
+
transition={{ duration: reduceMotion ? 0 : 0.3, delay: reduceMotion ? 0 : index * 0.1 }}
|
|
63
|
+
viewport={{ once: false, amount: 0.2 }}
|
|
64
|
+
>
|
|
65
|
+
<div
|
|
66
|
+
className={styles.bevel}
|
|
67
|
+
style={{ backgroundColor: '#ffffffe3', borderColor: '#cfcfcfa8' }}
|
|
68
|
+
>
|
|
69
|
+
<Button
|
|
70
|
+
component={Link}
|
|
71
|
+
href={href}
|
|
72
|
+
variant={variant}
|
|
73
|
+
target={button.link.newTab ? '_blank' : undefined}
|
|
74
|
+
rel={button.link.newTab ? 'noopener noreferrer' : undefined}
|
|
75
|
+
startIcon={button.iconPosition !== 'after' ? icon : undefined}
|
|
76
|
+
endIcon={button.iconPosition === 'after' ? icon : undefined}
|
|
77
|
+
className={cn(styles.button, 'group relative overflow-hidden')}
|
|
78
|
+
sx={{
|
|
79
|
+
minWidth: 160,
|
|
80
|
+
minHeight: 44,
|
|
81
|
+
maxWidth: '100%',
|
|
82
|
+
textTransform: 'none',
|
|
83
|
+
fontSize: 15,
|
|
84
|
+
fontFamily: 'inherit',
|
|
85
|
+
padding: '8px 16px',
|
|
86
|
+
border: '0px',
|
|
87
|
+
borderRadius: '4px',
|
|
88
|
+
lineHeight: 1.4,
|
|
89
|
+
color: color.foreground,
|
|
90
|
+
backgroundColor: color.background,
|
|
91
|
+
borderColor: color.border,
|
|
92
|
+
boxShadow: 'none',
|
|
93
|
+
'&:hover': {
|
|
94
|
+
backgroundColor: color.hover,
|
|
95
|
+
border: '0px',
|
|
96
|
+
borderColor: color.border,
|
|
97
|
+
boxShadow: '0 25px 50px -12px rgb(0 0 0 / 0.25)',
|
|
98
|
+
},
|
|
99
|
+
'&:active': { backgroundColor: color.background, border: '0px' },
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<motion.span
|
|
103
|
+
initial={reduceMotion ? false : { opacity: 0, y: -8 }}
|
|
104
|
+
whileInView={{ opacity: 1, y: 0 }}
|
|
105
|
+
transition={{ duration: reduceMotion ? 0 : 0.3 }}
|
|
106
|
+
viewport={{ once: false, amount: 0.2 }}
|
|
107
|
+
>
|
|
108
|
+
{button.link.label}
|
|
109
|
+
</motion.span>
|
|
110
|
+
<span
|
|
111
|
+
aria-hidden="true"
|
|
112
|
+
className="pointer-events-none absolute inset-0 flex size-full justify-center transition-transform [transform:skewX(-45deg)_translateX(-100%)] group-hover:duration-1000 group-hover:[transform:skewX(-45deg)_translateX(100%)] motion-reduce:hidden"
|
|
113
|
+
style={{ width: '200%' }}
|
|
114
|
+
>
|
|
115
|
+
<span className="relative h-full w-1/3 max-w-64 scale-200 bg-gradient-to-r from-transparent via-white/15 to-transparent" />
|
|
116
|
+
</span>
|
|
117
|
+
</Button>
|
|
118
|
+
</div>
|
|
119
|
+
</motion.div>
|
|
120
|
+
)
|
|
121
|
+
})}
|
|
122
|
+
</div>
|
|
123
|
+
)
|
|
124
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Block, Field, TextFieldSingleValidation } from 'payload'
|
|
2
|
+
import { link } from '@/fields/link'
|
|
3
|
+
import { iconPicker } from '@/fields/lucide-icon-picker/field'
|
|
4
|
+
import { isSupportedButtonURL } from './link'
|
|
5
|
+
|
|
6
|
+
const validateURL: TextFieldSingleValidation = (value, { siblingData }) =>
|
|
7
|
+
(siblingData as { type?: string })?.type !== 'custom' ||
|
|
8
|
+
isSupportedButtonURL(value) ||
|
|
9
|
+
'Enter an https:// or http:// URL, /path, #anchor, mailto: or tel: link.'
|
|
10
|
+
|
|
11
|
+
// Apply URL validation only to this block's copy of the shared link field.
|
|
12
|
+
const validateCustomURL = (field: Field): Field => {
|
|
13
|
+
if ('name' in field && field.name === 'url' && field.type === 'text' && !field.hasMany) {
|
|
14
|
+
return {
|
|
15
|
+
...field,
|
|
16
|
+
validate: validateURL,
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if ('fields' in field) return { ...field, fields: field.fields.map(validateCustomURL) }
|
|
20
|
+
return field
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const Button47: Block = {
|
|
24
|
+
slug: 'button47',
|
|
25
|
+
dbName: 'button47',
|
|
26
|
+
interfaceName: 'Button47Block',
|
|
27
|
+
labels: { singular: 'Button47', plural: 'Button47 blocks' },
|
|
28
|
+
fields: [
|
|
29
|
+
{
|
|
30
|
+
name: 'position',
|
|
31
|
+
label: 'Button Alignment',
|
|
32
|
+
type: 'select',
|
|
33
|
+
required: true,
|
|
34
|
+
defaultValue: 'center',
|
|
35
|
+
options: [
|
|
36
|
+
{ label: 'Left', value: 'left' },
|
|
37
|
+
{ label: 'Center', value: 'center' },
|
|
38
|
+
{ label: 'Right', value: 'right' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{ name: 'customClassName', type: 'text', label: 'Wrapper CSS Class' },
|
|
42
|
+
{
|
|
43
|
+
name: 'buttons',
|
|
44
|
+
type: 'array',
|
|
45
|
+
required: true,
|
|
46
|
+
minRows: 1,
|
|
47
|
+
fields: [
|
|
48
|
+
validateCustomURL(link({ appearances: false })),
|
|
49
|
+
{
|
|
50
|
+
name: 'variant',
|
|
51
|
+
label: 'Button Style',
|
|
52
|
+
type: 'select',
|
|
53
|
+
required: true,
|
|
54
|
+
defaultValue: 'contained',
|
|
55
|
+
options: [
|
|
56
|
+
{ label: 'Primary', value: 'contained' },
|
|
57
|
+
{ label: 'Secondary', value: 'outlined' },
|
|
58
|
+
{ label: 'Ghost', value: 'text' },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
iconPicker({ name: 'icon', description: 'Optional icon beside the button label.' }),
|
|
62
|
+
{
|
|
63
|
+
name: 'iconPosition',
|
|
64
|
+
type: 'select',
|
|
65
|
+
defaultValue: 'before',
|
|
66
|
+
options: [
|
|
67
|
+
{ label: 'Before Label', value: 'before' },
|
|
68
|
+
{ label: 'After Label', value: 'after' },
|
|
69
|
+
],
|
|
70
|
+
admin: { condition: (_, siblingData) => Boolean(siblingData?.icon) },
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Button47Block } from '@/payload-types'
|
|
2
|
+
|
|
3
|
+
export const isSupportedButtonURL = (value: unknown): value is string => {
|
|
4
|
+
if (typeof value !== 'string' || !value.trim()) return false
|
|
5
|
+
const url = value.trim()
|
|
6
|
+
if (/[\u0000-\u0020\u007f\\]/.test(url)) return false
|
|
7
|
+
if (url.startsWith('#') || (url.startsWith('/') && !url.startsWith('//'))) return true
|
|
8
|
+
try {
|
|
9
|
+
return ['https:', 'http:', 'mailto:', 'tel:'].includes(new URL(url).protocol)
|
|
10
|
+
} catch {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const resolveButton47Href = (link: Button47Block['buttons'][number]['link']) => {
|
|
16
|
+
if (link.type === 'reference') {
|
|
17
|
+
const page = link.reference?.value
|
|
18
|
+
if (!page || typeof page !== 'object' || !page.slug) return null
|
|
19
|
+
return page.slug === 'home' ? '/' : `/${page.slug}`
|
|
20
|
+
}
|
|
21
|
+
return link.type === 'custom' && isSupportedButtonURL(link.url) ? link.url.trim() : null
|
|
22
|
+
}
|