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.
Files changed (161) hide show
  1. package/package.json +5 -2
  2. package/src/cli.js +39 -3
  3. package/src/commands/shop.js +59 -0
  4. package/src/commands/warehouse.js +73 -0
  5. package/src/installers/patchRenderBlocks.js +1 -1
  6. package/src/registry/validateTemplateManifest.js +2 -1
  7. package/templates/blocks/BannerDD790432/Component.tsx +63 -0
  8. package/templates/blocks/BannerDD790432/config.ts +54 -0
  9. package/templates/blocks/BannerDD790432/manifest.json +80 -0
  10. package/templates/blocks/Button47/Component.tsx +124 -0
  11. package/templates/blocks/Button47/config.ts +75 -0
  12. package/templates/blocks/Button47/link.ts +22 -0
  13. package/templates/blocks/Button47/manifest.json +105 -0
  14. package/templates/blocks/Button47/styles.module.scss +75 -0
  15. package/templates/blocks/DD-Accordion/Component.tsx +55 -36
  16. package/templates/blocks/DD-Accordion/config.ts +71 -59
  17. package/templates/blocks/DD-AnimText/Component.tsx +50 -32
  18. package/templates/blocks/DD-AnimText/config.ts +296 -295
  19. package/templates/blocks/DD-AsideNav/Component.tsx +323 -319
  20. package/templates/blocks/DD-Banner/Component.tsx +54 -51
  21. package/templates/blocks/DD-BookCall/Component.tsx +10 -9
  22. package/templates/blocks/DD-ButtonPop/Component.tsx +118 -117
  23. package/templates/blocks/DD-Card/Component.tsx +4 -4
  24. package/templates/blocks/DD-CardTemplate01/Component.tsx +5 -5
  25. package/templates/blocks/DD-CardsWithVariantV3/Component.tsx +472 -0
  26. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/index.tsx +203 -0
  27. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/styles.ts +87 -0
  28. package/templates/blocks/DD-CardsWithVariantV3/ElectricBorder/useInjectStyle.ts +39 -0
  29. package/templates/blocks/DD-CardsWithVariantV3/config.ts +326 -0
  30. package/templates/blocks/DD-CardsWithVariantV3/manifest.json +110 -0
  31. package/templates/blocks/DD-Carousel-A/Component.tsx +251 -249
  32. package/templates/blocks/DD-Carousel-B/Component.tsx +405 -403
  33. package/templates/blocks/DD-Chip/Component.tsx +14 -14
  34. package/templates/blocks/DD-ComparisonTable/Component.tsx +255 -256
  35. package/templates/blocks/DD-Contact/Component.tsx +433 -434
  36. package/templates/blocks/DD-Counter/Component.tsx +18 -13
  37. package/templates/blocks/DD-DeliCard/Component.tsx +303 -0
  38. package/templates/blocks/DD-DeliCard/config.ts +186 -0
  39. package/templates/blocks/DD-DeliCard/manifest.json +94 -0
  40. package/templates/blocks/DD-FeatCat/Component.tsx +302 -302
  41. package/templates/blocks/DD-FeatProd/CircularGallery.tsx +1179 -0
  42. package/templates/blocks/DD-FeatProd/Component.tsx +484 -0
  43. package/templates/blocks/DD-FeatProd/config.ts +173 -0
  44. package/templates/blocks/DD-FeatProd/manifest.json +93 -0
  45. package/templates/blocks/DD-FeatServ/Component.tsx +607 -0
  46. package/templates/blocks/DD-FeatServ/config.ts +213 -0
  47. package/templates/blocks/DD-FeatServ/manifest.json +113 -0
  48. package/templates/blocks/DD-FeatStrip/Component.tsx +584 -585
  49. package/templates/blocks/DD-FluidBtn/ButtonsEyeCatching.tsx +627 -628
  50. package/templates/blocks/DD-FluidBtn/Component.tsx +46 -43
  51. package/templates/blocks/DD-GooeyInfo/Component.tsx +150 -149
  52. package/templates/blocks/DD-GooeyInfo/config.ts +136 -135
  53. package/templates/blocks/DD-Hero/Component.tsx +16 -40
  54. package/templates/blocks/DD-HeroPlas/Component.tsx +50 -0
  55. package/templates/blocks/DD-HeroPlas/ThreeScene.tsx +109 -0
  56. package/templates/blocks/DD-HeroPlas/config.ts +33 -0
  57. package/templates/blocks/DD-HeroPlas/manifest.json +88 -0
  58. package/templates/blocks/DD-HorizontalScroll/Component.tsx +4 -5
  59. package/templates/blocks/DD-Icon/Component.tsx +46 -81
  60. package/templates/blocks/DD-IconButton/Component.tsx +13 -20
  61. package/templates/blocks/DD-IdTag/Component.tsx +22 -21
  62. package/templates/blocks/DD-IframeMedia/Component.tsx +14 -10
  63. package/templates/blocks/DD-LineAnim/Component.tsx +42 -36
  64. package/templates/blocks/DD-LogoMarq/Component.tsx +38 -48
  65. package/templates/blocks/DD-Lottie/Component.tsx +11 -3
  66. package/templates/blocks/DD-MasonaryMedia/Component.tsx +544 -547
  67. package/templates/blocks/DD-MasonaryMedia/config.ts +389 -389
  68. package/templates/blocks/DD-Masonry/Component.tsx +654 -644
  69. package/templates/blocks/DD-Masonry/config.ts +475 -475
  70. package/templates/blocks/DD-MaterialSwatches/Component.tsx +85 -0
  71. package/templates/blocks/DD-MaterialSwatches/config.ts +232 -0
  72. package/templates/blocks/DD-MaterialSwatches/manifest.json +84 -0
  73. package/templates/blocks/DD-MaterialSwatches/material.ts +107 -0
  74. package/templates/blocks/DD-NineCanvas/Component.tsx +129 -0
  75. package/templates/blocks/DD-NineCanvas/RenderBlocks.tsx +244 -0
  76. package/templates/blocks/DD-NineCanvas/allowedCanvasBlocks.ts +6 -0
  77. package/templates/blocks/DD-NineCanvas/blockComponents.tsx +8 -0
  78. package/templates/blocks/DD-NineCanvas/canvas/CanvasGrid.tsx +316 -0
  79. package/templates/blocks/DD-NineCanvas/canvas/RadialNav.tsx +432 -0
  80. package/templates/blocks/DD-NineCanvas/canvas/canvas-panels.css +889 -0
  81. package/templates/blocks/DD-NineCanvas/canvas/icons/custom-icons.tsx +364 -0
  82. package/templates/blocks/DD-NineCanvas/config.ts +61 -0
  83. package/templates/blocks/DD-NineCanvas/manifest.json +165 -0
  84. package/templates/blocks/DD-NineCanvas/styles.scss +152 -0
  85. package/templates/blocks/DD-NineCanvas/tooltip/get-strict-context.tsx +26 -0
  86. package/templates/blocks/DD-NineCanvas/tooltip/primitives.tsx +178 -0
  87. package/templates/blocks/DD-NineCanvas/tooltip/tooltip.tsx +71 -0
  88. package/templates/blocks/DD-NineCanvas/tooltip/use-controlled-state.tsx +33 -0
  89. package/templates/blocks/DD-NineCanvas/utilities/FadeOnScroll.tsx +68 -0
  90. package/templates/blocks/DD-NineCanvas/utilities/ParallaxWrapper.tsx +157 -0
  91. package/templates/blocks/DD-NineCanvas/utilities/renderCustomCSS.ts +5 -0
  92. package/templates/blocks/DD-NineCanvas/utilities/webglCleanup.ts +27 -0
  93. package/templates/blocks/DD-PayloadAIAssistant/Component.tsx +78 -0
  94. package/templates/blocks/DD-PayloadAIAssistant/DustedBot.tsx +590 -0
  95. package/templates/blocks/DD-PayloadAIAssistant/config.ts +37 -0
  96. package/templates/blocks/DD-PayloadAIAssistant/demo.css +125 -0
  97. package/templates/blocks/DD-PayloadAIAssistant/manifest.json +93 -0
  98. package/templates/blocks/DD-PayloadAIAssistant/source-defaults.ts +17 -0
  99. package/templates/blocks/DD-PayloadAIAssistant/styles.ts +306 -0
  100. package/templates/blocks/DD-Pricing/Component.tsx +368 -372
  101. package/templates/blocks/DD-Pricing/config.ts +215 -215
  102. package/templates/blocks/DD-Process/Component.tsx +147 -149
  103. package/templates/blocks/DD-Progress/Component.tsx +156 -141
  104. package/templates/blocks/DD-ProjHeader/Component.tsx +93 -91
  105. package/templates/blocks/DD-ProjSlider/Component.tsx +1 -1
  106. package/templates/blocks/DD-ProjSlider/config.ts +2 -2
  107. package/templates/blocks/DD-Section/Component.tsx +265 -266
  108. package/templates/blocks/DD-Separator/Component.tsx +66 -66
  109. package/templates/blocks/DD-Services/Component.tsx +176 -176
  110. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +306 -307
  111. package/templates/blocks/DD-Slider-A/Component.tsx +237 -237
  112. package/templates/blocks/DD-StackCards/Component.tsx +474 -473
  113. package/templates/blocks/DD-Team/Component.tsx +247 -247
  114. package/templates/blocks/DD-TeamCards/Component.tsx +251 -251
  115. package/templates/blocks/DD-TechStack/Component.tsx +161 -168
  116. package/templates/blocks/DD-TestMarq/Component.tsx +158 -132
  117. package/templates/blocks/DD-Testim/Component.tsx +247 -247
  118. package/templates/blocks/DD-Testim2/Component.tsx +265 -256
  119. package/templates/blocks/DD-Testimonials/Component.tsx +139 -147
  120. package/templates/blocks/DD-TextMarq/Component.tsx +157 -157
  121. package/templates/blocks/DD-Thanks/Component.tsx +501 -500
  122. package/templates/blocks/DD-Work/Component.tsx +315 -315
  123. package/templates/blocks/DD-Work-B/Component.tsx +294 -294
  124. package/templates/blocks/DDHG-CTASection2/Component.tsx +183 -184
  125. package/templates/blocks/DDHG-HighlightArc/Component.tsx +246 -243
  126. package/templates/blocks/DDHG-LogoMarquee/Component.tsx +180 -93
  127. package/templates/blocks/DDHG-LogoMarquee/config.ts +154 -66
  128. package/templates/blocks/DDHG-NavCodeGrid/Component.tsx +225 -213
  129. package/templates/blocks/DDHG-ProcessSec/Component.tsx +38 -39
  130. package/templates/blocks/DDHG-ServiceDetails/Component.tsx +11 -11
  131. package/templates/blocks/DDHG-ServiceDetails/ServiceDetailCard.tsx +30 -47
  132. package/templates/blocks/DDHG-ServiceHero/Component.tsx +10 -10
  133. package/templates/blocks/DDHG-ServiceOverview/Component.tsx +23 -23
  134. package/templates/blocks/DDHG-StatsFAQ/Component.tsx +40 -40
  135. package/templates/blocks/DDHG-TeamCards/Component.tsx +140 -131
  136. package/templates/blocks/Form/Checkbox/index.tsx +45 -0
  137. package/templates/blocks/Form/Component.tsx +163 -0
  138. package/templates/blocks/Form/Country/index.tsx +65 -0
  139. package/templates/blocks/Form/Country/options.ts +982 -0
  140. package/templates/blocks/Form/Email/index.tsx +38 -0
  141. package/templates/blocks/Form/Error/index.tsx +15 -0
  142. package/templates/blocks/Form/Message/index.tsx +13 -0
  143. package/templates/blocks/Form/Number/index.tsx +36 -0
  144. package/templates/blocks/Form/Select/index.tsx +63 -0
  145. package/templates/blocks/Form/State/index.tsx +64 -0
  146. package/templates/blocks/Form/State/options.ts +52 -0
  147. package/templates/blocks/Form/Text/index.tsx +32 -0
  148. package/templates/blocks/Form/Textarea/index.tsx +40 -0
  149. package/templates/blocks/Form/Width/index.tsx +13 -0
  150. package/templates/blocks/Form/config.ts +51 -0
  151. package/templates/blocks/Form/fields.tsx +21 -0
  152. package/templates/blocks/Form/manifest.json +168 -0
  153. package/templates/blocks/MediaBlock/Component.tsx +67 -0
  154. package/templates/blocks/MediaBlock/config.ts +14 -0
  155. package/templates/blocks/MediaBlock/manifest.json +92 -0
  156. package/templates/blocks/TransitionsDustedShowcase/Component.tsx +9 -0
  157. package/templates/blocks/TransitionsDustedShowcase/config.ts +13 -0
  158. package/templates/blocks/TransitionsDustedShowcase/manifest.json +80 -0
  159. package/templates/blocks/rawHtml/Component.tsx +25 -0
  160. package/templates/blocks/rawHtml/config.ts +56 -0
  161. 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.11",
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
+ }