@wp-typia/project-tools 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +32 -0
  2. package/dist/runtime/cli-add.d.ts +38 -0
  3. package/dist/runtime/cli-add.js +561 -0
  4. package/dist/runtime/cli-core.d.ts +25 -0
  5. package/dist/runtime/cli-core.js +25 -0
  6. package/dist/runtime/cli-doctor.d.ts +34 -0
  7. package/dist/runtime/cli-doctor.js +131 -0
  8. package/dist/runtime/cli-help.d.ts +9 -0
  9. package/dist/runtime/cli-help.js +37 -0
  10. package/dist/runtime/cli-prompt.d.ts +21 -0
  11. package/dist/runtime/cli-prompt.js +53 -0
  12. package/dist/runtime/cli-scaffold.d.ts +79 -0
  13. package/dist/runtime/cli-scaffold.js +206 -0
  14. package/dist/runtime/cli-templates.d.ts +30 -0
  15. package/dist/runtime/cli-templates.js +61 -0
  16. package/dist/runtime/index.d.ts +9 -0
  17. package/dist/runtime/index.js +7 -0
  18. package/dist/runtime/json-utils.d.ts +10 -0
  19. package/dist/runtime/json-utils.js +12 -0
  20. package/dist/runtime/local-dev-presets.d.ts +26 -0
  21. package/dist/runtime/local-dev-presets.js +132 -0
  22. package/dist/runtime/metadata-analysis.d.ts +11 -0
  23. package/dist/runtime/metadata-analysis.js +285 -0
  24. package/dist/runtime/metadata-model.d.ts +84 -0
  25. package/dist/runtime/metadata-model.js +59 -0
  26. package/dist/runtime/metadata-parser.d.ts +53 -0
  27. package/dist/runtime/metadata-parser.js +794 -0
  28. package/dist/runtime/metadata-php-render.d.ts +29 -0
  29. package/dist/runtime/metadata-php-render.js +549 -0
  30. package/dist/runtime/metadata-projection.d.ts +7 -0
  31. package/dist/runtime/metadata-projection.js +233 -0
  32. package/dist/runtime/migration-constants.d.ts +15 -0
  33. package/dist/runtime/migration-constants.js +16 -0
  34. package/dist/runtime/migration-diff.d.ts +2 -0
  35. package/dist/runtime/migration-diff.js +537 -0
  36. package/dist/runtime/migration-fixtures.d.ts +8 -0
  37. package/dist/runtime/migration-fixtures.js +94 -0
  38. package/dist/runtime/migration-fuzz-plan.d.ts +2 -0
  39. package/dist/runtime/migration-fuzz-plan.js +50 -0
  40. package/dist/runtime/migration-manifest.d.ts +19 -0
  41. package/dist/runtime/migration-manifest.js +129 -0
  42. package/dist/runtime/migration-project.d.ts +94 -0
  43. package/dist/runtime/migration-project.js +1101 -0
  44. package/dist/runtime/migration-render.d.ts +11 -0
  45. package/dist/runtime/migration-render.js +741 -0
  46. package/dist/runtime/migration-risk.d.ts +4 -0
  47. package/dist/runtime/migration-risk.js +52 -0
  48. package/dist/runtime/migration-types.d.ts +249 -0
  49. package/dist/runtime/migration-types.js +1 -0
  50. package/dist/runtime/migration-ui-capability.d.ts +17 -0
  51. package/dist/runtime/migration-ui-capability.js +190 -0
  52. package/dist/runtime/migration-utils.d.ts +69 -0
  53. package/dist/runtime/migration-utils.js +246 -0
  54. package/dist/runtime/migrations.d.ts +249 -0
  55. package/dist/runtime/migrations.js +1061 -0
  56. package/dist/runtime/object-utils.d.ts +12 -0
  57. package/dist/runtime/object-utils.js +14 -0
  58. package/dist/runtime/package-managers.d.ts +28 -0
  59. package/dist/runtime/package-managers.js +156 -0
  60. package/dist/runtime/package-versions.d.ts +10 -0
  61. package/dist/runtime/package-versions.js +68 -0
  62. package/dist/runtime/scaffold-onboarding.d.ts +32 -0
  63. package/dist/runtime/scaffold-onboarding.js +99 -0
  64. package/dist/runtime/scaffold.d.ts +146 -0
  65. package/dist/runtime/scaffold.js +612 -0
  66. package/dist/runtime/schema-core.d.ts +267 -0
  67. package/dist/runtime/schema-core.js +597 -0
  68. package/dist/runtime/starter-manifests.d.ts +25 -0
  69. package/dist/runtime/starter-manifests.js +383 -0
  70. package/dist/runtime/string-case.d.ts +36 -0
  71. package/dist/runtime/string-case.js +69 -0
  72. package/dist/runtime/template-builtins.d.ts +38 -0
  73. package/dist/runtime/template-builtins.js +72 -0
  74. package/dist/runtime/template-defaults.d.ts +75 -0
  75. package/dist/runtime/template-defaults.js +65 -0
  76. package/dist/runtime/template-registry.d.ts +36 -0
  77. package/dist/runtime/template-registry.js +94 -0
  78. package/dist/runtime/template-render.d.ts +24 -0
  79. package/dist/runtime/template-render.js +113 -0
  80. package/dist/runtime/template-source.d.ts +71 -0
  81. package/dist/runtime/template-source.js +821 -0
  82. package/dist/runtime/typia-tags.d.ts +1 -0
  83. package/dist/runtime/typia-tags.js +1 -0
  84. package/package.json +79 -0
  85. package/templates/_shared/base/languages/.gitkeep +1 -0
  86. package/templates/_shared/base/package.json.mustache +41 -0
  87. package/templates/_shared/base/scripts/sync-types-to-block-json.ts.mustache +118 -0
  88. package/templates/_shared/base/src/hooks.ts.mustache +19 -0
  89. package/templates/_shared/base/src/validator-toolkit.ts.mustache +31 -0
  90. package/templates/_shared/base/tsconfig.json.mustache +21 -0
  91. package/templates/_shared/base/webpack.config.js.mustache +99 -0
  92. package/templates/_shared/base/{{slugKebabCase}}.php.mustache +53 -0
  93. package/templates/_shared/compound/core/package.json.mustache +45 -0
  94. package/templates/_shared/compound/core/scripts/add-compound-child.ts.mustache +559 -0
  95. package/templates/_shared/compound/core/scripts/block-config.ts.mustache +13 -0
  96. package/templates/_shared/compound/core/scripts/sync-types-to-block-json.ts.mustache +53 -0
  97. package/templates/_shared/compound/core/webpack.config.js.mustache +141 -0
  98. package/templates/_shared/compound/core/{{slugKebabCase}}.php.mustache +51 -0
  99. package/templates/_shared/compound/persistence/package.json.mustache +50 -0
  100. package/templates/_shared/compound/persistence/scripts/block-config.ts.mustache +59 -0
  101. package/templates/_shared/compound/persistence/scripts/sync-rest-contracts.ts.mustache +101 -0
  102. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-types.ts.mustache +21 -0
  103. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api-validators.ts.mustache +32 -0
  104. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/api.ts.mustache +68 -0
  105. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/block.json.mustache +52 -0
  106. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/data.ts.mustache +192 -0
  107. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +123 -0
  108. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  109. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/interactivity.ts.mustache +132 -0
  110. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/render.php.mustache +158 -0
  111. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/save.tsx.mustache +3 -0
  112. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/types.ts.mustache +56 -0
  113. package/templates/_shared/compound/persistence/src/blocks/{{slugKebabCase}}/validators.ts.mustache +32 -0
  114. package/templates/_shared/compound/persistence-auth/{{slugKebabCase}}.php.mustache +294 -0
  115. package/templates/_shared/compound/persistence-public/{{slugKebabCase}}.php.mustache +312 -0
  116. package/templates/_shared/migration-ui/common/src/admin/migration-dashboard.tsx +394 -0
  117. package/templates/_shared/migration-ui/common/src/migration-detector.ts +9 -0
  118. package/templates/_shared/migration-ui/common/src/migrations/helpers.ts +490 -0
  119. package/templates/_shared/migration-ui/common/src/migrations/index.ts +886 -0
  120. package/templates/_shared/persistence/auth/{{slugKebabCase}}.php.mustache +290 -0
  121. package/templates/_shared/persistence/core/package.json.mustache +46 -0
  122. package/templates/_shared/persistence/core/scripts/sync-rest-contracts.ts.mustache +113 -0
  123. package/templates/_shared/persistence/core/scripts/sync-types-to-block-json.ts.mustache +125 -0
  124. package/templates/_shared/persistence/core/src/api-types.ts.mustache +21 -0
  125. package/templates/_shared/persistence/core/src/api-validators.ts.mustache +32 -0
  126. package/templates/_shared/persistence/core/src/api.ts.mustache +68 -0
  127. package/templates/_shared/persistence/core/src/data.ts.mustache +192 -0
  128. package/templates/_shared/persistence/core/src/index.tsx.mustache +25 -0
  129. package/templates/_shared/persistence/core/src/interactivity.ts.mustache +134 -0
  130. package/templates/_shared/persistence/core/src/save.tsx.mustache +5 -0
  131. package/templates/_shared/persistence/core/src/validators.ts.mustache +32 -0
  132. package/templates/_shared/persistence/core/{{slugKebabCase}}.php.mustache +336 -0
  133. package/templates/_shared/persistence/public/{{slugKebabCase}}.php.mustache +308 -0
  134. package/templates/_shared/presets/test-preset/.wp-env.test.json.mustache +16 -0
  135. package/templates/_shared/presets/test-preset/playwright.config.ts.mustache +22 -0
  136. package/templates/_shared/presets/test-preset/scripts/wait-for-wp-env.mjs.mustache +102 -0
  137. package/templates/_shared/presets/test-preset/scripts/wp-env-utils.cjs.mustache +32 -0
  138. package/templates/_shared/presets/test-preset/tests/e2e/smoke.spec.ts.mustache +34 -0
  139. package/templates/_shared/presets/wp-env/.wp-env.json.mustache +16 -0
  140. package/templates/_shared/rest-helpers/auth/inc/rest-auth.php.mustache +37 -0
  141. package/templates/_shared/rest-helpers/public/inc/rest-public.php.mustache +314 -0
  142. package/templates/_shared/rest-helpers/shared/inc/rest-shared.php.mustache +58 -0
  143. package/templates/_shared/workspace/persistence-auth/inc/rest-auth.php.mustache +36 -0
  144. package/templates/_shared/workspace/persistence-auth/inc/rest-shared.php.mustache +55 -0
  145. package/templates/_shared/workspace/persistence-auth/server.php.mustache +237 -0
  146. package/templates/_shared/workspace/persistence-public/inc/rest-public.php.mustache +273 -0
  147. package/templates/_shared/workspace/persistence-public/inc/rest-shared.php.mustache +55 -0
  148. package/templates/_shared/workspace/persistence-public/server.php.mustache +252 -0
  149. package/templates/basic/src/block.json.mustache +51 -0
  150. package/templates/basic/src/edit.tsx.mustache +128 -0
  151. package/templates/basic/src/editor.scss.mustache +8 -0
  152. package/templates/basic/src/hooks.ts.mustache +18 -0
  153. package/templates/basic/src/index.tsx.mustache +45 -0
  154. package/templates/basic/src/save.tsx.mustache +30 -0
  155. package/templates/basic/src/style.scss.mustache +40 -0
  156. package/templates/basic/src/types.ts.mustache +56 -0
  157. package/templates/basic/src/validators.ts.mustache +26 -0
  158. package/templates/compound/src/blocks/{{slugKebabCase}}/block.json.mustache +37 -0
  159. package/templates/compound/src/blocks/{{slugKebabCase}}/children.ts.mustache +25 -0
  160. package/templates/compound/src/blocks/{{slugKebabCase}}/edit.tsx.mustache +93 -0
  161. package/templates/compound/src/blocks/{{slugKebabCase}}/hooks.ts.mustache +11 -0
  162. package/templates/compound/src/blocks/{{slugKebabCase}}/index.tsx.mustache +25 -0
  163. package/templates/compound/src/blocks/{{slugKebabCase}}/save.tsx.mustache +32 -0
  164. package/templates/compound/src/blocks/{{slugKebabCase}}/style.scss.mustache +31 -0
  165. package/templates/compound/src/blocks/{{slugKebabCase}}/types.ts.mustache +13 -0
  166. package/templates/compound/src/blocks/{{slugKebabCase}}/validators.ts.mustache +17 -0
  167. package/templates/compound/src/blocks/{{slugKebabCase}}-item/block.json.mustache +35 -0
  168. package/templates/compound/src/blocks/{{slugKebabCase}}-item/edit.tsx.mustache +50 -0
  169. package/templates/compound/src/blocks/{{slugKebabCase}}-item/hooks.ts.mustache +11 -0
  170. package/templates/compound/src/blocks/{{slugKebabCase}}-item/index.tsx.mustache +25 -0
  171. package/templates/compound/src/blocks/{{slugKebabCase}}-item/save.tsx.mustache +24 -0
  172. package/templates/compound/src/blocks/{{slugKebabCase}}-item/types.ts.mustache +12 -0
  173. package/templates/compound/src/blocks/{{slugKebabCase}}-item/validators.ts.mustache +17 -0
  174. package/templates/interactivity/package.json.mustache +42 -0
  175. package/templates/interactivity/src/block.json.mustache +73 -0
  176. package/templates/interactivity/src/edit.tsx.mustache +270 -0
  177. package/templates/interactivity/src/index.tsx.mustache +32 -0
  178. package/templates/interactivity/src/interactivity.ts.mustache +152 -0
  179. package/templates/interactivity/src/save.tsx.mustache +101 -0
  180. package/templates/interactivity/src/style.scss.mustache +60 -0
  181. package/templates/interactivity/src/types.ts.mustache +32 -0
  182. package/templates/interactivity/src/validators.ts.mustache +36 -0
  183. package/templates/persistence/src/block.json.mustache +52 -0
  184. package/templates/persistence/src/edit.tsx.mustache +165 -0
  185. package/templates/persistence/src/render.php.mustache +126 -0
  186. package/templates/persistence/src/style.scss.mustache +46 -0
  187. package/templates/persistence/src/types.ts.mustache +55 -0
@@ -0,0 +1,270 @@
1
+ import { __ } from '@wordpress/i18n';
2
+ import { useBlockProps, InspectorControls, RichText, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
3
+ import { PanelBody, RangeControl, TextControl, Button, Notice } from '@wordpress/components';
4
+ import { useState } from '@wordpress/element';
5
+ import currentManifest from './typia.manifest.json';
6
+ import {
7
+ InspectorFromManifest,
8
+ type ManifestDocument,
9
+ useEditorFields,
10
+ useTypedAttributeUpdater,
11
+ } from '@wp-typia/block-runtime/inspector';
12
+ import type { {{pascalCase}}Attributes } from './types';
13
+ import {
14
+ sanitize{{pascalCase}}Attributes,
15
+ validate{{pascalCase}}Attributes,
16
+ } from './validators';
17
+ import { useTypiaValidation } from './hooks';
18
+
19
+ export default function Edit({ attributes, setAttributes, isSelected }: {
20
+ attributes: {{pascalCase}}Attributes;
21
+ setAttributes: (attrs: Partial<{{pascalCase}}Attributes>) => void;
22
+ isSelected: boolean;
23
+ }) {
24
+ const [isPreviewing, setIsPreviewing] = useState(false);
25
+ const editorFields = useEditorFields(currentManifest as ManifestDocument, {
26
+ manual: ['content', 'clickCount', 'maxClicks', 'autoPlayInterval', 'uniqueId'],
27
+ labels: {
28
+ alignment: __('Alignment', '{{textDomain}}'),
29
+ animation: __('Animation', '{{textDomain}}'),
30
+ interactiveMode: __('Interactive Mode', '{{textDomain}}'),
31
+ isVisible: __('Visible', '{{textDomain}}'),
32
+ showCounter: __('Show Counter', '{{textDomain}}'),
33
+ },
34
+ });
35
+ const { errorMessages, isValid } = useTypiaValidation(
36
+ attributes,
37
+ validate{{pascalCase}}Attributes,
38
+ );
39
+ const validateEditorUpdate = (nextAttributes: {{pascalCase}}Attributes) => {
40
+ try {
41
+ return {
42
+ data: sanitize{{pascalCase}}Attributes(nextAttributes),
43
+ errors: [],
44
+ isValid: true as const,
45
+ };
46
+ } catch {
47
+ return validate{{pascalCase}}Attributes(nextAttributes);
48
+ }
49
+ };
50
+ const { updateField } = useTypedAttributeUpdater(
51
+ attributes,
52
+ setAttributes,
53
+ validateEditorUpdate
54
+ );
55
+ const alignmentValue = editorFields.getStringValue(
56
+ attributes,
57
+ 'alignment',
58
+ 'left'
59
+ );
60
+ const isVisible = editorFields.getBooleanValue(
61
+ attributes,
62
+ 'isVisible',
63
+ true
64
+ );
65
+ const showCounter = editorFields.getBooleanValue(
66
+ attributes,
67
+ 'showCounter',
68
+ true
69
+ );
70
+ const interactiveMode = editorFields.getStringValue(
71
+ attributes,
72
+ 'interactiveMode',
73
+ 'click'
74
+ );
75
+ const animation = editorFields.getStringValue(
76
+ attributes,
77
+ 'animation',
78
+ 'none'
79
+ );
80
+
81
+ const blockProps = useBlockProps({
82
+ className: `{{cssClassName}} {{cssClassName}}--${interactiveMode}`,
83
+ 'data-wp-interactive': '{{slugKebabCase}}',
84
+ 'data-wp-context': JSON.stringify({
85
+ clicks: attributes.clickCount,
86
+ isAnimating: attributes.isAnimating,
87
+ isVisible,
88
+ lastInteraction: '',
89
+ animation,
90
+ interactiveMode,
91
+ maxClicks: attributes.maxClicks,
92
+ autoPlayInterval: attributes.autoPlayInterval
93
+ })
94
+ });
95
+
96
+ const resetCounter = () => {
97
+ updateField('clickCount', 0);
98
+ updateField('isAnimating', false);
99
+ };
100
+
101
+ const testAnimation = () => {
102
+ updateField('isAnimating', true);
103
+ setTimeout(() => {
104
+ updateField('isAnimating', false);
105
+ }, 1000);
106
+ };
107
+
108
+ return (
109
+ <>
110
+ <BlockControls>
111
+ <AlignmentToolbar
112
+ value={alignmentValue}
113
+ onChange={(value) => updateField('alignment', (value || alignmentValue) as NonNullable<{{pascalCase}}Attributes['alignment']>)}
114
+ />
115
+ </BlockControls>
116
+
117
+ <InspectorControls>
118
+ <InspectorFromManifest
119
+ attributes={attributes}
120
+ fieldLookup={editorFields}
121
+ onChange={updateField}
122
+ paths={['alignment', 'interactiveMode', 'animation', 'showCounter', 'isVisible']}
123
+ title={__('Interactive Settings', '{{textDomain}}')}
124
+ />
125
+
126
+ <PanelBody title={__('Counter Settings', '{{textDomain}}')}>
127
+ <RangeControl
128
+ label={__('Max Clicks', '{{textDomain}}')}
129
+ value={attributes.maxClicks}
130
+ onChange={(value) => updateField('maxClicks', value ?? attributes.maxClicks ?? 0)}
131
+ min={0}
132
+ max={100}
133
+ help={__('Set to 0 for unlimited clicks', '{{textDomain}}')}
134
+ />
135
+
136
+ {interactiveMode === 'auto' && (
137
+ <RangeControl
138
+ label={__('Auto Play Interval (ms)', '{{textDomain}}')}
139
+ value={attributes.autoPlayInterval}
140
+ onChange={(value) => updateField('autoPlayInterval', value ?? attributes.autoPlayInterval ?? 0)}
141
+ min={100}
142
+ max={5000}
143
+ step={100}
144
+ help={__('Interval between automatic animations', '{{textDomain}}')}
145
+ />
146
+ )}
147
+
148
+ <div style={{ display: 'flex', gap: '8px', marginTop: '16px' }}>
149
+ <Button
150
+ variant="secondary"
151
+ onClick={resetCounter}
152
+ isSmall
153
+ >
154
+ {__('Reset Counter', '{{textDomain}}')}
155
+ </Button>
156
+ <Button
157
+ variant="secondary"
158
+ onClick={testAnimation}
159
+ isSmall
160
+ >
161
+ {__('Test Animation', '{{textDomain}}')}
162
+ </Button>
163
+ </div>
164
+
165
+ <TextControl
166
+ label={__('Unique ID', '{{textDomain}}')}
167
+ value={attributes.uniqueId}
168
+ onChange={(value) => updateField('uniqueId', value)}
169
+ help={__('Optional unique identifier for this block instance', '{{textDomain}}')}
170
+ />
171
+ </PanelBody>
172
+
173
+ {!isValid && (
174
+ <PanelBody title={__('Validation Errors', '{{textDomain}}')} initialOpen>
175
+ {errorMessages.map((error, index) => (
176
+ <Notice key={index} status="error" isDismissible={false}>
177
+ {error}
178
+ </Notice>
179
+ ))}
180
+ </PanelBody>
181
+ )}
182
+
183
+ {isSelected && (
184
+ <PanelBody title={__('Preview', '{{textDomain}}')}>
185
+ <Button
186
+ variant={isPreviewing ? 'primary' : 'secondary'}
187
+ onClick={() => setIsPreviewing(!isPreviewing)}
188
+ aria-pressed={isPreviewing}
189
+ isSmall
190
+ >
191
+ {isPreviewing
192
+ ? __('Disable Preview Mode', '{{textDomain}}')
193
+ : __('Enable Preview Mode', '{{textDomain}}')}
194
+ </Button>
195
+
196
+ {attributes.clickCount > 0 && (
197
+ <Notice status="info" isDismissible={false}>
198
+ {__('Current clicks:', '{{textDomain}}')} {attributes.clickCount}
199
+ {attributes.maxClicks > 0 && ` / ${attributes.maxClicks}`}
200
+ </Notice>
201
+ )}
202
+ </PanelBody>
203
+ )}
204
+ </InspectorControls>
205
+
206
+ <div {...blockProps}>
207
+ <div
208
+ className={`{{cssClassName}}__content ${attributes.isAnimating ? 'is-animating' : ''}`}
209
+ style={{ textAlign: alignmentValue }}
210
+ data-wp-on--click={isPreviewing ? 'actions.handleClick' : undefined}
211
+ data-wp-on--mouseenter={isPreviewing && interactiveMode === 'hover' ? 'actions.handleMouseEnter' : undefined}
212
+ data-wp-on--mouseleave={isPreviewing && interactiveMode === 'hover' ? 'actions.handleMouseLeave' : undefined}
213
+ >
214
+ <RichText
215
+ tagName="p"
216
+ value={attributes.content}
217
+ onChange={(value) => updateField('content', value)}
218
+ placeholder={__('{{title}} – click me to interact!', '{{textDomain}}')}
219
+ />
220
+
221
+ {!isValid && (
222
+ <Notice status="error" isDismissible={false}>
223
+ <p>
224
+ <strong>{__('Validation Errors', '{{textDomain}}')}</strong>
225
+ </p>
226
+ <ul style={{ margin: 0, paddingLeft: '1em' }}>
227
+ {errorMessages.map((error, index) => (
228
+ <li key={index}>{error}</li>
229
+ ))}
230
+ </ul>
231
+ </Notice>
232
+ )}
233
+
234
+ {showCounter && (
235
+ <div className="{{cssClassName}}__counter">
236
+ <span className="{{cssClassName}}__counter-label">
237
+ {__('Clicks:', '{{textDomain}}')}
238
+ </span>
239
+ <span
240
+ className="{{cssClassName}}__counter-value"
241
+ data-wp-text="state.clicks"
242
+ >
243
+ {attributes.clickCount}
244
+ </span>
245
+ </div>
246
+ )}
247
+
248
+ {attributes.maxClicks > 0 && (
249
+ <div className="{{cssClassName}}__progress">
250
+ <div
251
+ className="{{cssClassName}}__progress-bar"
252
+ style={{ width: `${(attributes.clickCount / attributes.maxClicks) * 100}%` }}
253
+ data-wp-style--width="state.progress + '%'"
254
+ />
255
+ </div>
256
+ )}
257
+
258
+ {animation !== 'none' && (
259
+ <div
260
+ className={`{{cssClassName}}__animation ${attributes.isAnimating ? 'is-active' : ''}`}
261
+ data-wp-class="is-active"
262
+ >
263
+ {animation}
264
+ </div>
265
+ )}
266
+ </div>
267
+ </div>
268
+ </>
269
+ );
270
+ }
@@ -0,0 +1,32 @@
1
+ import { registerBlockType } from '@wordpress/blocks';
2
+ import type { BlockConfiguration } from '@wordpress/blocks';
3
+ import type { BlockSupports } from '@wp-typia/block-types/blocks/supports';
4
+ import {
5
+ buildScaffoldBlockRegistration,
6
+ type ScaffoldBlockMetadata,
7
+ } from '@wp-typia/block-runtime/blocks';
8
+
9
+ import Edit from './edit';
10
+ import Save from './save';
11
+ import metadata from './block.json';
12
+ import './style.scss';
13
+
14
+ import type { {{pascalCase}}Attributes } from './types';
15
+
16
+ const scaffoldSupports = {
17
+ html: false,
18
+ align: true,
19
+ anchor: true,
20
+ className: true,
21
+ interactivity: true,
22
+ } satisfies BlockSupports;
23
+
24
+ const registration = buildScaffoldBlockRegistration<
25
+ BlockConfiguration<{{pascalCase}}Attributes>
26
+ >(metadata as ScaffoldBlockMetadata, {
27
+ supports: scaffoldSupports,
28
+ edit: Edit,
29
+ save: Save,
30
+ });
31
+
32
+ registerBlockType<{{pascalCase}}Attributes>(registration.name, registration.settings);
@@ -0,0 +1,152 @@
1
+ /**
2
+ * WordPress Interactivity API implementation for {{title}} block
3
+ */
4
+ import { store, getContext, getElement } from '@wordpress/interactivity';
5
+
6
+ // Store configuration
7
+ store('{{slugKebabCase}}', {
8
+ // State - reactive data that updates the UI
9
+ state: {
10
+ get clicks() {
11
+ return getContext().clicks;
12
+ },
13
+ get isAnimating() {
14
+ return getContext().isAnimating;
15
+ },
16
+ get isVisible() {
17
+ return getContext().isVisible;
18
+ },
19
+ get animationClass() {
20
+ const context = getContext();
21
+ if (!context.isAnimating) return '';
22
+
23
+ switch (context.animation) {
24
+ case 'bounce':
25
+ return 'animate-bounce';
26
+ case 'pulse':
27
+ return 'animate-pulse';
28
+ case 'shake':
29
+ return 'animate-shake';
30
+ case 'flip':
31
+ return 'animate-flip';
32
+ default:
33
+ return '';
34
+ }
35
+ },
36
+ get progress() {
37
+ const context = getContext();
38
+ return context.maxClicks > 0 ? (context.clicks / context.maxClicks) * 100 : 0;
39
+ },
40
+ get clampedClicks() {
41
+ const context = getContext();
42
+ return context.maxClicks > 0
43
+ ? Math.min(context.clicks, context.maxClicks)
44
+ : context.clicks;
45
+ },
46
+ get isComplete() {
47
+ const context = getContext();
48
+ return context.clicks >= context.maxClicks && context.maxClicks > 0;
49
+ }
50
+ },
51
+
52
+ // Actions - user interactions
53
+ actions: {
54
+ // Handle block click
55
+ handleClick: () => {
56
+ const context = getContext();
57
+ const { element } = getElement();
58
+
59
+ // Increment click counter
60
+ context.clicks += 1;
61
+ context.lastInteraction = 'click';
62
+
63
+ // Trigger animation
64
+ if (context.animation !== 'none') {
65
+ context.isAnimating = true;
66
+ setTimeout(() => {
67
+ context.isAnimating = false;
68
+ }, 1000);
69
+ }
70
+
71
+ // Emit custom event
72
+ element.dispatchEvent(new CustomEvent('{{slugKebabCase}}:click', {
73
+ detail: { clicks: context.clicks }
74
+ }));
75
+
76
+ // Check if max clicks reached
77
+ if (context.maxClicks > 0 && context.clicks >= context.maxClicks) {
78
+ element.dispatchEvent(new CustomEvent('{{slugKebabCase}}:complete', {
79
+ detail: { totalClicks: context.clicks }
80
+ }));
81
+ }
82
+ },
83
+
84
+ // Handle hover events
85
+ handleMouseEnter: () => {
86
+ const context = getContext();
87
+ if (context.interactiveMode === 'hover') {
88
+ context.isAnimating = true;
89
+ context.lastInteraction = 'hover';
90
+ }
91
+ },
92
+
93
+ handleMouseLeave: () => {
94
+ const context = getContext();
95
+ if (context.interactiveMode === 'hover') {
96
+ context.isAnimating = false;
97
+ }
98
+ },
99
+
100
+ // Toggle visibility
101
+ toggleVisibility: () => {
102
+ const context = getContext();
103
+ context.isVisible = !context.isVisible;
104
+ },
105
+
106
+ // Reset counter
107
+ reset: () => {
108
+ const context = getContext();
109
+ context.clicks = 0;
110
+ context.isAnimating = false;
111
+ context.lastInteraction = 'reset';
112
+ },
113
+
114
+ // Start/stop auto play
115
+ toggleAutoPlay: () => {
116
+ const context = getContext();
117
+ if (context.autoPlayInterval > 0) {
118
+ if (context.autoPlayTimer) {
119
+ clearInterval(context.autoPlayTimer);
120
+ context.autoPlayTimer = null;
121
+ } else {
122
+ context.autoPlayTimer = setInterval(() => {
123
+ context.clicks += 1;
124
+ context.isAnimating = true;
125
+ setTimeout(() => {
126
+ context.isAnimating = false;
127
+ }, 500);
128
+ }, context.autoPlayInterval);
129
+ }
130
+ }
131
+ }
132
+ }
133
+ });
134
+
135
+ // Type definitions for TypeScript
136
+ declare global {
137
+ namespace WordPress {
138
+ namespace Interactivity {
139
+ interface {{pascalCase}}Context {
140
+ clicks: number;
141
+ isAnimating: boolean;
142
+ isVisible: boolean;
143
+ lastInteraction: string;
144
+ animation: string;
145
+ interactiveMode: string;
146
+ maxClicks: number;
147
+ autoPlayInterval: number;
148
+ autoPlayTimer?: number;
149
+ }
150
+ }
151
+ }
152
+ }
@@ -0,0 +1,101 @@
1
+ import { useBlockProps, RichText } from '@wordpress/block-editor';
2
+ import type { {{pascalCase}}Attributes } from './types';
3
+
4
+ export default function Save({ attributes }: { attributes: {{pascalCase}}Attributes }) {
5
+ const blockProps = useBlockProps.save({
6
+ className: `{{cssClassName}} {{cssClassName}}--${attributes.interactiveMode}`,
7
+ 'data-wp-interactive': '{{slugKebabCase}}',
8
+ 'data-wp-context': JSON.stringify({
9
+ clicks: attributes.clickCount,
10
+ isAnimating: attributes.isAnimating,
11
+ isVisible: attributes.isVisible,
12
+ lastInteraction: '',
13
+ animation: attributes.animation,
14
+ interactiveMode: attributes.interactiveMode,
15
+ maxClicks: attributes.maxClicks,
16
+ autoPlayInterval: attributes.autoPlayInterval
17
+ })
18
+ });
19
+
20
+ return (
21
+ <div {...blockProps}>
22
+ <div
23
+ className={`{{cssClassName}}__content ${attributes.isAnimating ? 'is-animating' : ''}`}
24
+ style={{ textAlign: attributes.alignment }}
25
+ data-wp-on--click="actions.handleClick"
26
+ data-wp-on--mouseenter={attributes.interactiveMode === 'hover' ? 'actions.handleMouseEnter' : undefined}
27
+ data-wp-on--mouseleave={attributes.interactiveMode === 'hover' ? 'actions.handleMouseLeave' : undefined}
28
+ data-wp-bind--hidden="!state.isVisible"
29
+ >
30
+ <RichText.Content
31
+ tagName="p"
32
+ value={attributes.content}
33
+ className="{{cssClassName}}__text"
34
+ />
35
+
36
+ {attributes.showCounter && (
37
+ <div
38
+ className="{{cssClassName}}__counter"
39
+ role="status"
40
+ aria-live="polite"
41
+ aria-atomic="true"
42
+ >
43
+ <span className="{{cssClassName}}__counter-label">
44
+ Clicks:
45
+ </span>
46
+ <span
47
+ className="{{cssClassName}}__counter-value"
48
+ data-wp-text="state.clicks"
49
+ >
50
+ {attributes.clickCount}
51
+ </span>
52
+ </div>
53
+ )}
54
+
55
+ {attributes.maxClicks > 0 && (
56
+ <div className="{{cssClassName}}__progress">
57
+ <div
58
+ className="{{cssClassName}}__progress-bar"
59
+ role="progressbar"
60
+ aria-label="Click progress"
61
+ aria-valuemin={0}
62
+ aria-valuemax={attributes.maxClicks}
63
+ aria-valuenow={Math.min(attributes.clickCount, attributes.maxClicks)}
64
+ data-wp-bind--aria-valuenow="state.clampedClicks"
65
+ data-wp-style--width="state.progress + '%'"
66
+ />
67
+ </div>
68
+ )}
69
+
70
+ <div
71
+ className={`{{cssClassName}}__animation ${attributes.animation}`}
72
+ aria-hidden="true"
73
+ data-wp-class="is-active"
74
+ />
75
+
76
+ {attributes.maxClicks > 0 && (
77
+ <div
78
+ className="{{cssClassName}}__completion"
79
+ role="status"
80
+ aria-live="polite"
81
+ aria-atomic="true"
82
+ data-wp-bind--hidden="!state.isComplete"
83
+ >
84
+ 🎉 Complete!
85
+ </div>
86
+ )}
87
+
88
+ <button
89
+ className="{{cssClassName}}__reset"
90
+ data-wp-on--click="actions.reset"
91
+ aria-label="Reset counter"
92
+ >
93
+ <span aria-hidden="true">↻</span>
94
+ <span className="screen-reader-text">
95
+ Reset counter
96
+ </span>
97
+ </button>
98
+ </div>
99
+ </div>
100
+ );
101
+ }
@@ -0,0 +1,60 @@
1
+ .{{cssClassName}} {
2
+ position: relative;
3
+ padding: 1rem;
4
+ border: 1px solid #dcdcde;
5
+ border-radius: 0.75rem;
6
+ background: #fff;
7
+
8
+ &__content {
9
+ display: grid;
10
+ gap: 0.75rem;
11
+ }
12
+
13
+ &__counter {
14
+ display: inline-flex;
15
+ gap: 0.35rem;
16
+ align-items: center;
17
+ font-size: 0.9rem;
18
+ font-weight: 600;
19
+ }
20
+
21
+ &__progress {
22
+ width: 100%;
23
+ height: 0.5rem;
24
+ overflow: hidden;
25
+ background: #f0f0f1;
26
+ border-radius: 999px;
27
+ }
28
+
29
+ &__progress-bar {
30
+ height: 100%;
31
+ background: #3858e9;
32
+ transition: width 0.2s ease;
33
+ }
34
+
35
+ &__animation {
36
+ min-height: 1.5rem;
37
+ font-size: 0.85rem;
38
+ color: #3858e9;
39
+ opacity: 0;
40
+ transition: opacity 0.2s ease;
41
+
42
+ &.is-active {
43
+ opacity: 1;
44
+ }
45
+ }
46
+
47
+ &__completion {
48
+ font-weight: 700;
49
+ color: #06752d;
50
+ }
51
+
52
+ &__reset {
53
+ align-self: start;
54
+ padding: 0.4rem 0.7rem;
55
+ border: 1px solid #dcdcde;
56
+ border-radius: 999px;
57
+ background: transparent;
58
+ cursor: pointer;
59
+ }
60
+ }
@@ -0,0 +1,32 @@
1
+ import type { TextAlignment } from "@wp-typia/block-types/block-editor/alignment";
2
+ import type {
3
+ TypiaValidationError,
4
+ ValidationResult,
5
+ } from "@wp-typia/block-runtime/validation";
6
+ import { tags } from "typia";
7
+
8
+ export type { TypiaValidationError, ValidationResult } from "@wp-typia/block-runtime/validation";
9
+
10
+ export interface {{pascalCase}}Attributes {
11
+ content: string & tags.MinLength<1> & tags.MaxLength<1000> & tags.Default<"">;
12
+ alignment?: TextAlignment & tags.Default<"left">;
13
+ isVisible?: boolean & tags.Default<true>;
14
+ interactiveMode?: ('click' | 'hover' | 'auto') & tags.Default<"click">;
15
+ animation?: ('none' | 'bounce' | 'pulse' | 'shake' | 'flip') & tags.Default<"none">;
16
+ clickCount?: number & tags.Minimum<0> & tags.Type<"uint32"> & tags.Default<0>;
17
+ isAnimating?: boolean & tags.Default<false>;
18
+ showCounter?: boolean & tags.Default<true>;
19
+ maxClicks?: number & tags.Minimum<0> & tags.Type<"uint32"> & tags.Default<10>;
20
+ autoPlayInterval?: number & tags.Minimum<0> & tags.Type<"uint32"> & tags.Default<0>;
21
+ uniqueId?: string & tags.Default<"">;
22
+ }
23
+
24
+ export interface {{pascalCase}}Context {
25
+ clicks: number;
26
+ isAnimating: boolean;
27
+ isVisible: boolean;
28
+ lastInteraction: string;
29
+ animationClass: string;
30
+ }
31
+
32
+ export type {{pascalCase}}ValidationResult = ValidationResult<{{pascalCase}}Attributes>;
@@ -0,0 +1,36 @@
1
+ import currentManifest from "./typia.manifest.json";
2
+ import { {{pascalCase}}Attributes, {{pascalCase}}ValidationResult } from "./types";
3
+ import { generateScopedClientId } from "@wp-typia/block-runtime/identifiers";
4
+ import { createTemplateValidatorToolkit } from "./validator-toolkit";
5
+
6
+ const scaffoldValidators = createTemplateValidatorToolkit<{{pascalCase}}Attributes>({
7
+ manifest: currentManifest,
8
+ finalize: (normalized) => ({
9
+ ...normalized,
10
+ uniqueId:
11
+ normalized.uniqueId && normalized.uniqueId.length > 0
12
+ ? normalized.uniqueId
13
+ : generateScopedClientId("{{slugKebabCase}}"),
14
+ }),
15
+ });
16
+
17
+ export const validate{{pascalCase}}Attributes =
18
+ scaffoldValidators.validateAttributes as (
19
+ attributes: unknown,
20
+ ) => {{pascalCase}}ValidationResult;
21
+
22
+ export const validators = scaffoldValidators.validators;
23
+
24
+ export const sanitize{{pascalCase}}Attributes =
25
+ scaffoldValidators.sanitizeAttributes as (
26
+ attributes: Partial<{{pascalCase}}Attributes>,
27
+ ) => {{pascalCase}}Attributes;
28
+
29
+ /**
30
+ * Runtime type guard for checking if an object is {{pascalCase}}Attributes.
31
+ */
32
+ export const is{{pascalCase}}Attributes = (obj: unknown): obj is {{pascalCase}}Attributes => {
33
+ return validators.is(obj);
34
+ };
35
+
36
+ export const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;