@wp-typia/project-tools 0.16.10 → 0.16.12

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 (104) hide show
  1. package/README.md +9 -3
  2. package/dist/runtime/built-in-block-artifact-documents.d.ts +3 -0
  3. package/dist/runtime/built-in-block-artifact-documents.js +2 -0
  4. package/dist/runtime/built-in-block-artifact-types.d.ts +51 -0
  5. package/dist/runtime/built-in-block-artifact-types.js +304 -0
  6. package/dist/runtime/built-in-block-artifacts.js +4 -803
  7. package/dist/runtime/built-in-block-attribute-emitters.d.ts +71 -0
  8. package/dist/runtime/built-in-block-attribute-emitters.js +176 -0
  9. package/dist/runtime/built-in-block-attribute-specs.d.ts +38 -0
  10. package/dist/runtime/built-in-block-attribute-specs.js +358 -0
  11. package/dist/runtime/built-in-block-code-templates/basic.d.ts +4 -0
  12. package/dist/runtime/built-in-block-code-templates/basic.js +249 -0
  13. package/dist/runtime/built-in-block-code-templates/compound-child.d.ts +4 -0
  14. package/dist/runtime/built-in-block-code-templates/compound-child.js +138 -0
  15. package/dist/runtime/built-in-block-code-templates/compound-parent.d.ts +6 -0
  16. package/dist/runtime/built-in-block-code-templates/compound-parent.js +227 -0
  17. package/dist/runtime/built-in-block-code-templates/compound-persistence.d.ts +4 -0
  18. package/dist/runtime/built-in-block-code-templates/compound-persistence.js +478 -0
  19. package/dist/runtime/built-in-block-code-templates/compound.d.ts +3 -0
  20. package/dist/runtime/built-in-block-code-templates/compound.js +3 -0
  21. package/dist/runtime/built-in-block-code-templates/interactivity.d.ts +5 -0
  22. package/dist/runtime/built-in-block-code-templates/interactivity.js +547 -0
  23. package/dist/runtime/built-in-block-code-templates/persistence.d.ts +5 -0
  24. package/dist/runtime/built-in-block-code-templates/persistence.js +550 -0
  25. package/dist/runtime/built-in-block-code-templates/shared.d.ts +16 -0
  26. package/dist/runtime/built-in-block-code-templates/shared.js +53 -0
  27. package/dist/runtime/built-in-block-code-templates.d.ts +5 -32
  28. package/dist/runtime/built-in-block-code-templates.js +5 -2230
  29. package/dist/runtime/cli-add-block-config.d.ts +6 -0
  30. package/dist/runtime/cli-add-block-config.js +143 -0
  31. package/dist/runtime/cli-add-block-legacy-validator.d.ts +4 -0
  32. package/dist/runtime/cli-add-block-legacy-validator.js +168 -0
  33. package/dist/runtime/cli-add-block.js +3 -301
  34. package/dist/runtime/cli-add-workspace-assets.d.ts +38 -0
  35. package/dist/runtime/cli-add-workspace-assets.js +399 -0
  36. package/dist/runtime/cli-add-workspace.d.ts +2 -38
  37. package/dist/runtime/cli-add-workspace.js +5 -396
  38. package/dist/runtime/cli-doctor-environment.d.ts +12 -0
  39. package/dist/runtime/cli-doctor-environment.js +123 -0
  40. package/dist/runtime/cli-doctor-workspace.d.ts +14 -0
  41. package/dist/runtime/cli-doctor-workspace.js +296 -0
  42. package/dist/runtime/cli-doctor.d.ts +4 -2
  43. package/dist/runtime/cli-doctor.js +10 -405
  44. package/dist/runtime/cli-help.js +1 -1
  45. package/dist/runtime/cli-scaffold.js +1 -1
  46. package/dist/runtime/migration-command-surface.d.ts +67 -0
  47. package/dist/runtime/migration-command-surface.js +189 -0
  48. package/dist/runtime/migration-diff-rename.d.ts +13 -0
  49. package/dist/runtime/migration-diff-rename.js +192 -0
  50. package/dist/runtime/migration-diff-transform.d.ts +14 -0
  51. package/dist/runtime/migration-diff-transform.js +105 -0
  52. package/dist/runtime/migration-diff.js +12 -297
  53. package/dist/runtime/migration-generated-artifacts.d.ts +3 -0
  54. package/dist/runtime/migration-generated-artifacts.js +41 -0
  55. package/dist/runtime/migration-maintenance.d.ts +51 -0
  56. package/dist/runtime/migration-maintenance.js +380 -0
  57. package/dist/runtime/migration-planning.d.ts +23 -0
  58. package/dist/runtime/migration-planning.js +131 -0
  59. package/dist/runtime/migration-project-config-source.d.ts +6 -0
  60. package/dist/runtime/migration-project-config-source.js +424 -0
  61. package/dist/runtime/migration-project-layout-discovery.d.ts +61 -0
  62. package/dist/runtime/migration-project-layout-discovery.js +337 -0
  63. package/dist/runtime/migration-project-layout-paths.d.ts +135 -0
  64. package/dist/runtime/migration-project-layout-paths.js +288 -0
  65. package/dist/runtime/migration-project-layout.d.ts +3 -0
  66. package/dist/runtime/migration-project-layout.js +2 -0
  67. package/dist/runtime/migration-project-workspace.d.ts +47 -0
  68. package/dist/runtime/migration-project-workspace.js +212 -0
  69. package/dist/runtime/migration-project.d.ts +4 -94
  70. package/dist/runtime/migration-project.js +3 -1101
  71. package/dist/runtime/migration-render-diff-rule.d.ts +5 -0
  72. package/dist/runtime/migration-render-diff-rule.js +120 -0
  73. package/dist/runtime/migration-render-execution.d.ts +3 -0
  74. package/dist/runtime/migration-render-execution.js +428 -0
  75. package/dist/runtime/migration-render-generated.d.ts +27 -0
  76. package/dist/runtime/migration-render-generated.js +230 -0
  77. package/dist/runtime/migration-render-support.d.ts +3 -0
  78. package/dist/runtime/migration-render-support.js +16 -0
  79. package/dist/runtime/migration-render.d.ts +3 -33
  80. package/dist/runtime/migration-render.js +3 -789
  81. package/dist/runtime/migration-ui-capability.js +1 -1
  82. package/dist/runtime/migrations.d.ts +24 -118
  83. package/dist/runtime/migrations.js +12 -700
  84. package/dist/runtime/scaffold-bootstrap.d.ts +45 -0
  85. package/dist/runtime/scaffold-bootstrap.js +185 -0
  86. package/dist/runtime/scaffold-package-manager-files.d.ts +35 -0
  87. package/dist/runtime/scaffold-package-manager-files.js +79 -0
  88. package/dist/runtime/scaffold.d.ts +1 -12
  89. package/dist/runtime/scaffold.js +10 -393
  90. package/dist/runtime/template-source-contracts.d.ts +81 -0
  91. package/dist/runtime/template-source-contracts.js +1 -0
  92. package/dist/runtime/template-source-external.d.ts +21 -0
  93. package/dist/runtime/template-source-external.js +184 -0
  94. package/dist/runtime/template-source-locators.d.ts +4 -0
  95. package/dist/runtime/template-source-locators.js +72 -0
  96. package/dist/runtime/template-source-normalization.d.ts +7 -0
  97. package/dist/runtime/template-source-normalization.js +53 -0
  98. package/dist/runtime/template-source-remote.d.ts +23 -0
  99. package/dist/runtime/template-source-remote.js +336 -0
  100. package/dist/runtime/template-source-seeds.d.ts +12 -0
  101. package/dist/runtime/template-source-seeds.js +243 -0
  102. package/dist/runtime/template-source.d.ts +4 -86
  103. package/dist/runtime/template-source.js +9 -828
  104. package/package.json +5 -5
@@ -0,0 +1,547 @@
1
+ export const INTERACTIVITY_EDIT_TEMPLATE = `import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';
2
+ import { __ } from '@wordpress/i18n';
3
+ import { useBlockProps, InspectorControls, RichText, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';
4
+ import { PanelBody, RangeControl, Button, Notice } from '@wordpress/components';
5
+ import { useState } from '@wordpress/element';
6
+ import currentManifest from './manifest-document';
7
+ import {
8
+ InspectorFromManifest,
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
+ type EditProps = BlockEditProps<{{pascalCase}}Attributes>;
20
+
21
+ const actionButtonRowStyle = { display: 'flex', gap: '8px', marginTop: '16px' };
22
+ const validationListStyle = { margin: 0, paddingLeft: '1em' };
23
+
24
+ export default function Edit({ attributes, setAttributes, isSelected }: EditProps) {
25
+ const [isPreviewing, setIsPreviewing] = useState(false);
26
+ const editorFields = useEditorFields(currentManifest, {
27
+ manual: ['content', 'clickCount', 'maxClicks'],
28
+ labels: {
29
+ alignment: __('Alignment', '{{textDomain}}'),
30
+ animation: __('Animation', '{{textDomain}}'),
31
+ interactiveMode: __('Interactive Mode', '{{textDomain}}'),
32
+ isVisible: __('Visible', '{{textDomain}}'),
33
+ showCounter: __('Show Counter', '{{textDomain}}'),
34
+ },
35
+ });
36
+ const { errorMessages, isValid } = useTypiaValidation(
37
+ attributes,
38
+ validate{{pascalCase}}Attributes,
39
+ );
40
+ const validateEditorUpdate = (nextAttributes: {{pascalCase}}Attributes) => {
41
+ try {
42
+ return {
43
+ data: sanitize{{pascalCase}}Attributes(nextAttributes),
44
+ errors: [],
45
+ isValid: true as const,
46
+ };
47
+ } catch {
48
+ return validate{{pascalCase}}Attributes(nextAttributes);
49
+ }
50
+ };
51
+ const { updateField } = useTypedAttributeUpdater(
52
+ attributes,
53
+ setAttributes,
54
+ validateEditorUpdate
55
+ );
56
+ const alignmentValue = editorFields.getStringValue(
57
+ attributes,
58
+ 'alignment',
59
+ 'left'
60
+ ) as NonNullable<{{pascalCase}}Attributes['alignment']>;
61
+ const clickCount = attributes.clickCount ?? 0;
62
+ const isVisible = editorFields.getBooleanValue(
63
+ attributes,
64
+ 'isVisible',
65
+ true
66
+ );
67
+ const isAnimating = attributes.isAnimating ?? false;
68
+ const maxClicks = attributes.maxClicks ?? 0;
69
+ const showCounter = editorFields.getBooleanValue(
70
+ attributes,
71
+ 'showCounter',
72
+ true
73
+ );
74
+ const interactiveMode = editorFields.getStringValue(
75
+ attributes,
76
+ 'interactiveMode',
77
+ 'click'
78
+ ) as NonNullable<{{pascalCase}}Attributes['interactiveMode']>;
79
+ const animation = editorFields.getStringValue(
80
+ attributes,
81
+ 'animation',
82
+ 'none'
83
+ ) as NonNullable<{{pascalCase}}Attributes['animation']>;
84
+
85
+ const blockProps = useBlockProps({
86
+ className: \`{{cssClassName}} {{cssClassName}}--\${interactiveMode}\`,
87
+ 'data-wp-interactive': '{{slugKebabCase}}',
88
+ 'data-wp-context': JSON.stringify({
89
+ clicks: clickCount,
90
+ isAnimating,
91
+ isVisible,
92
+ animation,
93
+ maxClicks,
94
+ })
95
+ });
96
+ const previewContentStyle = { textAlign: alignmentValue };
97
+ const progressBarStyle = { width: \`\${(clickCount / maxClicks) * 100}%\` };
98
+
99
+ const resetCounter = () => {
100
+ updateField('clickCount', 0);
101
+ updateField('isAnimating', false);
102
+ };
103
+
104
+ const testAnimation = () => {
105
+ updateField('isAnimating', true);
106
+ setTimeout(() => {
107
+ updateField('isAnimating', false);
108
+ }, 1000);
109
+ };
110
+
111
+ return (
112
+ <>
113
+ <BlockControls>
114
+ <AlignmentToolbar
115
+ value={alignmentValue}
116
+ onChange={(value) => updateField('alignment', (value || alignmentValue) as NonNullable<{{pascalCase}}Attributes['alignment']>)}
117
+ />
118
+ </BlockControls>
119
+
120
+ <InspectorControls>
121
+ <InspectorFromManifest
122
+ attributes={attributes}
123
+ fieldLookup={editorFields}
124
+ onChange={updateField}
125
+ paths={['alignment', 'interactiveMode', 'animation', 'showCounter', 'isVisible']}
126
+ title={__('Interactive Settings', '{{textDomain}}')}
127
+ />
128
+
129
+ <PanelBody title={__('Counter Settings', '{{textDomain}}')}>
130
+ <RangeControl
131
+ label={__('Max Clicks', '{{textDomain}}')}
132
+ value={maxClicks}
133
+ onChange={(value) => updateField('maxClicks', value ?? maxClicks)}
134
+ min={0}
135
+ max={100}
136
+ help={__('Set to 0 for unlimited clicks', '{{textDomain}}')}
137
+ />
138
+
139
+ <div style={actionButtonRowStyle}>
140
+ <Button
141
+ variant="secondary"
142
+ onClick={resetCounter}
143
+ isSmall
144
+ >
145
+ {__('Reset Counter', '{{textDomain}}')}
146
+ </Button>
147
+ <Button
148
+ variant="secondary"
149
+ onClick={testAnimation}
150
+ isSmall
151
+ >
152
+ {__('Test Animation', '{{textDomain}}')}
153
+ </Button>
154
+ </div>
155
+ </PanelBody>
156
+
157
+ {!isValid && (
158
+ <PanelBody title={__('Validation Errors', '{{textDomain}}')} initialOpen>
159
+ {errorMessages.map((error, index) => (
160
+ <Notice key={index} status="error" isDismissible={false}>
161
+ {error}
162
+ </Notice>
163
+ ))}
164
+ </PanelBody>
165
+ )}
166
+
167
+ {isSelected && (
168
+ <PanelBody title={__('Preview', '{{textDomain}}')}>
169
+ <Button
170
+ variant={isPreviewing ? 'primary' : 'secondary'}
171
+ onClick={() => setIsPreviewing(!isPreviewing)}
172
+ aria-pressed={isPreviewing}
173
+ isSmall
174
+ >
175
+ {isPreviewing
176
+ ? __('Disable Preview Mode', '{{textDomain}}')
177
+ : __('Enable Preview Mode', '{{textDomain}}')}
178
+ </Button>
179
+
180
+ {clickCount > 0 && (
181
+ <Notice status="info" isDismissible={false}>
182
+ {__('Current clicks:', '{{textDomain}}')} {clickCount}
183
+ {maxClicks > 0 && \` / \${maxClicks}\`}
184
+ </Notice>
185
+ )}
186
+ </PanelBody>
187
+ )}
188
+ </InspectorControls>
189
+
190
+ <div {...blockProps}>
191
+ <div
192
+ className={\`{{cssClassName}}__content \${isAnimating ? 'is-animating' : ''}\`}
193
+ style={previewContentStyle}
194
+ data-wp-on--click={isPreviewing ? 'actions.handleClick' : undefined}
195
+ data-wp-on--mouseenter={isPreviewing && interactiveMode === 'hover' ? 'actions.handleMouseEnter' : undefined}
196
+ data-wp-on--mouseleave={isPreviewing && interactiveMode === 'hover' ? 'actions.handleMouseLeave' : undefined}
197
+ >
198
+ <RichText
199
+ tagName="p"
200
+ value={attributes.content}
201
+ onChange={(value) => updateField('content', value)}
202
+ placeholder={__( {{titleJson}} + ' – click me to interact!', '{{textDomain}}')}
203
+ />
204
+
205
+ {!isValid && (
206
+ <Notice status="error" isDismissible={false}>
207
+ <p>
208
+ <strong>{__('Validation Errors', '{{textDomain}}')}</strong>
209
+ </p>
210
+ <ul style={validationListStyle}>
211
+ {errorMessages.map((error, index) => (
212
+ <li key={index}>{error}</li>
213
+ ))}
214
+ </ul>
215
+ </Notice>
216
+ )}
217
+
218
+ {showCounter && (
219
+ <div className="{{cssClassName}}__counter">
220
+ <span className="{{cssClassName}}__counter-label">
221
+ {__('Clicks:', '{{textDomain}}')}
222
+ </span>
223
+ <span
224
+ className="{{cssClassName}}__counter-value"
225
+ data-wp-text="state.clicks"
226
+ >
227
+ {clickCount}
228
+ </span>
229
+ </div>
230
+ )}
231
+
232
+ {maxClicks > 0 && (
233
+ <div className="{{cssClassName}}__progress">
234
+ <div
235
+ className="{{cssClassName}}__progress-bar"
236
+ style={progressBarStyle}
237
+ data-wp-style--width="state.progress + '%'"
238
+ />
239
+ </div>
240
+ )}
241
+
242
+ {animation !== 'none' && (
243
+ <div
244
+ className={\`{{cssClassName}}__animation \${isAnimating ? 'is-active' : ''}\`}
245
+ data-wp-class--is-active="state.isAnimating"
246
+ >
247
+ {animation}
248
+ </div>
249
+ )}
250
+ </div>
251
+ </div>
252
+ </>
253
+ );
254
+ }
255
+ `;
256
+ export const INTERACTIVITY_SAVE_TEMPLATE = `import { useBlockProps, RichText } from '@wordpress/block-editor';
257
+ import { __ } from '@wordpress/i18n';
258
+ import type { {{pascalCase}}Attributes } from './types';
259
+
260
+ export default function Save({ attributes }: { attributes: {{pascalCase}}Attributes }) {
261
+ const clickCount = attributes.clickCount ?? 0;
262
+ const interactiveMode = attributes.interactiveMode ?? 'click';
263
+ const animation = attributes.animation ?? 'none';
264
+ const isAnimating = attributes.isAnimating ?? false;
265
+ const isVisible = attributes.isVisible ?? true;
266
+ const maxClicks = attributes.maxClicks ?? 0;
267
+ const showCounter = attributes.showCounter ?? true;
268
+ const contentStyle = { textAlign: attributes.alignment };
269
+ const blockProps = useBlockProps.save({
270
+ className: \`{{cssClassName}} {{cssClassName}}--\${interactiveMode}\`,
271
+ 'data-wp-interactive': '{{slugKebabCase}}',
272
+ 'data-wp-context': JSON.stringify({
273
+ clicks: clickCount,
274
+ isAnimating,
275
+ isVisible,
276
+ animation,
277
+ maxClicks,
278
+ })
279
+ });
280
+
281
+ return (
282
+ <div {...blockProps}>
283
+ <div
284
+ className={\`{{cssClassName}}__content \${isAnimating ? 'is-animating' : ''}\`}
285
+ style={contentStyle}
286
+ data-wp-on--click="actions.handleClick"
287
+ data-wp-on--mouseenter={interactiveMode === 'hover' ? 'actions.handleMouseEnter' : undefined}
288
+ data-wp-on--mouseleave={interactiveMode === 'hover' ? 'actions.handleMouseLeave' : undefined}
289
+ data-wp-bind--hidden="!state.isVisible"
290
+ >
291
+ <RichText.Content
292
+ tagName="p"
293
+ value={attributes.content}
294
+ className="{{cssClassName}}__text"
295
+ />
296
+
297
+ {showCounter && (
298
+ <div
299
+ className="{{cssClassName}}__counter"
300
+ role="status"
301
+ aria-live="polite"
302
+ aria-atomic="true"
303
+ >
304
+ <span className="{{cssClassName}}__counter-label">
305
+ { __( 'Clicks:', '{{textDomain}}' ) }
306
+ </span>
307
+ <span
308
+ className="{{cssClassName}}__counter-value"
309
+ data-wp-text="state.clicks"
310
+ >
311
+ {clickCount}
312
+ </span>
313
+ </div>
314
+ )}
315
+
316
+ {maxClicks > 0 && (
317
+ <div className="{{cssClassName}}__progress">
318
+ <div
319
+ className="{{cssClassName}}__progress-bar"
320
+ role="progressbar"
321
+ aria-label={ __( 'Click progress', '{{textDomain}}' ) }
322
+ aria-valuemin={0}
323
+ aria-valuemax={maxClicks}
324
+ aria-valuenow={Math.min(clickCount, maxClicks)}
325
+ data-wp-bind--aria-valuenow="state.clampedClicks"
326
+ data-wp-style--width="state.progress + '%'"
327
+ />
328
+ </div>
329
+ )}
330
+
331
+ <div
332
+ className={\`{{cssClassName}}__animation \${animation}\`}
333
+ aria-hidden="true"
334
+ data-wp-class--is-active="state.isAnimating"
335
+ />
336
+
337
+ {maxClicks > 0 && (
338
+ <div
339
+ className="{{cssClassName}}__completion"
340
+ role="status"
341
+ aria-live="polite"
342
+ aria-atomic="true"
343
+ data-wp-bind--hidden="!state.isComplete"
344
+ >
345
+ { __( '🎉 Complete!', '{{textDomain}}' ) }
346
+ </div>
347
+ )}
348
+
349
+ <button
350
+ className="{{cssClassName}}__reset"
351
+ data-wp-on--click="actions.reset"
352
+ aria-label={ __( 'Reset counter', '{{textDomain}}' ) }
353
+ >
354
+ <span aria-hidden="true">↻</span>
355
+ <span className="screen-reader-text">
356
+ { __( 'Reset counter', '{{textDomain}}' ) }
357
+ </span>
358
+ </button>
359
+ </div>
360
+ </div>
361
+ );
362
+ }
363
+ `;
364
+ export const INTERACTIVITY_INDEX_TEMPLATE = `import {
365
+ registerScaffoldBlockType,
366
+ type BlockConfiguration,
367
+ } from '@wp-typia/block-types/blocks/registration';
368
+ import type { BlockSupports } from '@wp-typia/block-types/blocks/supports';
369
+ import {
370
+ buildScaffoldBlockRegistration,
371
+ parseScaffoldBlockMetadata,
372
+ } from '@wp-typia/block-runtime/blocks';
373
+
374
+ import Edit from './edit';
375
+ import Save from './save';
376
+ import metadata from './block-metadata';
377
+ import './editor.scss';
378
+ import './style.scss';
379
+
380
+ import type { {{pascalCase}}Attributes } from './types';
381
+
382
+ const scaffoldSupports = {
383
+ html: false,
384
+ align: true,
385
+ anchor: true,
386
+ className: true,
387
+ interactivity: true,
388
+ } satisfies BlockSupports;
389
+
390
+ const registration = buildScaffoldBlockRegistration(
391
+ parseScaffoldBlockMetadata<BlockConfiguration<{{pascalCase}}Attributes>>(metadata),
392
+ {
393
+ supports: scaffoldSupports,
394
+ edit: Edit,
395
+ save: Save,
396
+ }
397
+ );
398
+
399
+ registerScaffoldBlockType(registration.name, registration.settings);
400
+ `;
401
+ export const INTERACTIVITY_SCRIPT_TEMPLATE = `/**
402
+ * WordPress Interactivity API implementation for {{title}} block
403
+ */
404
+ import { store, getContext, getElement, withSyncEvent } from '@wordpress/interactivity';
405
+ import type { {{pascalCase}}Context } from './types';
406
+
407
+ function getBlockContext() {
408
+ return getContext<{{pascalCase}}Context>();
409
+ }
410
+
411
+ // Store configuration
412
+ store('{{slugKebabCase}}', {
413
+ // State - reactive data that updates the UI
414
+ state: {
415
+ get clicks() {
416
+ return getBlockContext().clicks;
417
+ },
418
+ get isAnimating() {
419
+ return getBlockContext().isAnimating;
420
+ },
421
+ get isVisible() {
422
+ return getBlockContext().isVisible;
423
+ },
424
+ get progress() {
425
+ const context = getBlockContext();
426
+ const clampedClicks =
427
+ context.maxClicks > 0
428
+ ? Math.min(context.clicks, context.maxClicks)
429
+ : context.clicks;
430
+ return context.maxClicks > 0 ? (clampedClicks / context.maxClicks) * 100 : 0;
431
+ },
432
+ get clampedClicks() {
433
+ const context = getBlockContext();
434
+ return context.maxClicks > 0
435
+ ? Math.min(context.clicks, context.maxClicks)
436
+ : context.clicks;
437
+ },
438
+ get isComplete() {
439
+ const context = getBlockContext();
440
+ return context.clicks >= context.maxClicks && context.maxClicks > 0;
441
+ }
442
+ },
443
+
444
+ // Actions - user interactions
445
+ actions: {
446
+ // Handle block click
447
+ handleClick: () => {
448
+ const context = getBlockContext();
449
+ const { ref } = getElement();
450
+
451
+ if (!ref) {
452
+ return;
453
+ }
454
+
455
+ if (context.maxClicks > 0 && context.clicks >= context.maxClicks) {
456
+ return;
457
+ }
458
+
459
+ const previousClicks = context.clicks;
460
+
461
+ // Increment click counter
462
+ context.clicks += 1;
463
+
464
+ // Trigger animation
465
+ if (context.animation !== 'none') {
466
+ context.isAnimating = true;
467
+ setTimeout(() => {
468
+ context.isAnimating = false;
469
+ }, 1000);
470
+ }
471
+
472
+ // Emit custom event
473
+ ref.dispatchEvent(new CustomEvent('{{slugKebabCase}}:click', {
474
+ detail: { clicks: context.clicks }
475
+ }));
476
+
477
+ // Check if max clicks reached
478
+ if (context.maxClicks > 0 && previousClicks < context.maxClicks && context.clicks === context.maxClicks) {
479
+ ref.dispatchEvent(new CustomEvent('{{slugKebabCase}}:complete', {
480
+ detail: { totalClicks: context.clicks }
481
+ }));
482
+ }
483
+ },
484
+
485
+ // Handle hover events
486
+ handleMouseEnter: () => {
487
+ const context = getBlockContext();
488
+ if (context.animation === 'none') return;
489
+ context.isAnimating = true;
490
+ },
491
+
492
+ handleMouseLeave: () => {
493
+ const context = getBlockContext();
494
+ if (context.animation === 'none') return;
495
+ context.isAnimating = false;
496
+ },
497
+
498
+ // Reset counter
499
+ reset: withSyncEvent((event: Event) => {
500
+ event.stopPropagation();
501
+ const context = getBlockContext();
502
+ context.clicks = 0;
503
+ context.isAnimating = false;
504
+ })
505
+ }
506
+ });
507
+ `;
508
+ export const INTERACTIVITY_VALIDATORS_TEMPLATE = `import typia from 'typia';
509
+ import currentManifest from "./manifest-defaults-document";
510
+ import { {{pascalCase}}Attributes, {{pascalCase}}ValidationResult } from "./types";
511
+ import { createTemplateValidatorToolkit } from "./validator-toolkit";
512
+
513
+ const scaffoldValidators = createTemplateValidatorToolkit<{{pascalCase}}Attributes>({
514
+ assert: typia.createAssert<{{pascalCase}}Attributes>(),
515
+ clone: typia.misc.createClone<{{pascalCase}}Attributes>() as (
516
+ value: {{pascalCase}}Attributes,
517
+ ) => {{pascalCase}}Attributes,
518
+ is: typia.createIs<{{pascalCase}}Attributes>(),
519
+ manifest: currentManifest,
520
+ prune: typia.misc.createPrune<{{pascalCase}}Attributes>(),
521
+ random: typia.createRandom<{{pascalCase}}Attributes>() as (
522
+ ...args: unknown[]
523
+ ) => {{pascalCase}}Attributes,
524
+ validate: typia.createValidate<{{pascalCase}}Attributes>(),
525
+ });
526
+
527
+ export const validate{{pascalCase}}Attributes =
528
+ scaffoldValidators.validateAttributes as (
529
+ attributes: unknown,
530
+ ) => {{pascalCase}}ValidationResult;
531
+
532
+ export const validators = scaffoldValidators.validators;
533
+
534
+ export const sanitize{{pascalCase}}Attributes =
535
+ scaffoldValidators.sanitizeAttributes as (
536
+ attributes: Partial<{{pascalCase}}Attributes>,
537
+ ) => {{pascalCase}}Attributes;
538
+
539
+ /**
540
+ * Runtime type guard for checking if an object is {{pascalCase}}Attributes.
541
+ */
542
+ export const is{{pascalCase}}Attributes = (obj: unknown): obj is {{pascalCase}}Attributes => {
543
+ return validators.is(obj);
544
+ };
545
+
546
+ export const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;
547
+ `;
@@ -0,0 +1,5 @@
1
+ export declare const PERSISTENCE_EDIT_TEMPLATE = "import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';\nimport { __ } from '@wordpress/i18n';\nimport {\n\tAlignmentToolbar,\n\tBlockControls,\n\tInspectorControls,\n\tRichText,\n\tuseBlockProps,\n} from '@wordpress/block-editor';\nimport {\n\tNotice,\n\tPanelBody,\n\tTextControl,\n} from '@wordpress/components';\nimport currentManifest from './manifest-document';\nimport {\n\tInspectorFromManifest,\n\tuseEditorFields,\n\tuseTypedAttributeUpdater,\n} from '@wp-typia/block-runtime/inspector';\nimport type { {{pascalCase}}Attributes } from './types';\nimport {\n\tsanitize{{pascalCase}}Attributes,\n\tvalidate{{pascalCase}}Attributes,\n} from './validators';\nimport { useTypiaValidation } from './hooks';\n\ntype EditProps = BlockEditProps< {{pascalCase}}Attributes >;\n\nexport default function Edit( {\n\tattributes,\n\tsetAttributes,\n}: EditProps ) {\n\tconst editorFields = useEditorFields(\n\t\tcurrentManifest,\n\t\t{\n\t\t\tmanual: [ 'content', 'resourceKey' ],\n\t\t\tlabels: {\n\t\t\t\tbuttonLabel: __( 'Button Label', '{{textDomain}}' ),\n\t\t\t\tresourceKey: __( 'Resource Key', '{{textDomain}}' ),\n\t\t\t\tshowCount: __( 'Show Count', '{{textDomain}}' ),\n\t\t\t},\n\t\t}\n\t);\n\tconst { errorMessages, isValid } = useTypiaValidation(\n\t\tattributes,\n\t\tvalidate{{pascalCase}}Attributes\n\t);\n\tconst validateEditorUpdate = (\n\t\tnextAttributes: {{pascalCase}}Attributes\n\t) => {\n\t\ttry {\n\t\t\treturn {\n\t\t\t\tdata: sanitize{{pascalCase}}Attributes( nextAttributes ),\n\t\t\t\terrors: [],\n\t\t\t\tisValid: true as const,\n\t\t\t};\n\t\t} catch {\n\t\t\treturn validate{{pascalCase}}Attributes( nextAttributes );\n\t\t}\n\t};\n\tconst { updateField } = useTypedAttributeUpdater(\n\t\tattributes,\n\t\tsetAttributes,\n\t\tvalidateEditorUpdate\n\t);\n\tconst alignmentValue = editorFields.getStringValue(\n\t\tattributes,\n\t\t'alignment',\n\t\t'left'\n\t);\n\tconst persistencePolicy = '{{persistencePolicy}}';\n\tconst persistencePolicyDescription = __(\n\t\t{{persistencePolicyDescriptionJson}},\n\t\t'{{textDomain}}'\n\t);\n\n\treturn (\n\t\t<>\n\t\t\t<BlockControls>\n\t\t\t\t<AlignmentToolbar\n\t\t\t\t\tvalue={ alignmentValue }\n\t\t\t\t\tonChange={ ( value ) =>\n\t\t\t\t\t\tupdateField(\n\t\t\t\t\t\t\t'alignment',\n\t\t\t\t\t\t\t( value || alignmentValue ) as NonNullable< {{pascalCase}}Attributes[ 'alignment' ] >\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t/>\n\t\t\t</BlockControls>\n\t\t\t<InspectorControls>\n\t\t\t\t<InspectorFromManifest\n\t\t\t\t\tattributes={ attributes }\n\t\t\t\t\tfieldLookup={ editorFields }\n\t\t\t\t\tonChange={ updateField }\n\t\t\t\t\tpaths={ [ 'alignment', 'isVisible', 'showCount', 'buttonLabel' ] }\n\t\t\t\t\ttitle={ __( 'Persistence Settings', '{{textDomain}}' ) }\n\t\t\t\t>\n\t\t\t\t\t<TextControl\n\t\t\t\t\t\tlabel={ __( 'Resource Key', '{{textDomain}}' ) }\n\t\t\t\t\t\tvalue={ attributes.resourceKey ?? '' }\n\t\t\t\t\t\tonChange={ ( value ) => updateField( 'resourceKey', value ) }\n\t\t\t\t\t\thelp={ __( 'Stable persisted identifier used by the storage-backed counter endpoint.', '{{textDomain}}' ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Storage mode: {{dataStorageMode}}', '{{textDomain}}' ) }\n\t\t\t\t\t</Notice>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Persistence policy: {{persistencePolicy}}', '{{textDomain}}' ) }\n\t\t\t\t\t\t<br />\n\t\t\t\t\t\t{ persistencePolicyDescription }\n\t\t\t\t\t</Notice>\n\t\t\t\t\t<Notice status=\"info\" isDismissible={ false }>\n\t\t\t\t\t\t{ __( 'Render mode: dynamic. `render.php` bootstraps durable post context, while fresh session-only write data is loaded from the dedicated `/bootstrap` endpoint after hydration.', '{{textDomain}}' ) }\n\t\t\t\t\t</Notice>\n\t\t\t\t</InspectorFromManifest>\n\t\t\t\t{ ! isValid && (\n\t\t\t\t\t<PanelBody\n\t\t\t\t\t\ttitle={ __( 'Validation Errors', '{{textDomain}}' ) }\n\t\t\t\t\t\tinitialOpen\n\t\t\t\t\t>\n\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => (\n\t\t\t\t\t\t\t<Notice key={ index } status=\"error\" isDismissible={ false }>\n\t\t\t\t\t\t\t\t{ error }\n\t\t\t\t\t\t\t</Notice>\n\t\t\t\t\t\t) ) }\n\t\t\t\t\t</PanelBody>\n\t\t\t\t) }\n\t\t\t</InspectorControls>\n\t\t\t<div\n\t\t\t\t{ ...useBlockProps( {\n\t\t\t\t\tclassName: '{{cssClassName}}',\n\t\t\t\t\tstyle: {\n\t\t\t\t\t\ttextAlign:\n\t\t\t\t\t\t\talignmentValue as NonNullable< {{pascalCase}}Attributes[ 'alignment' ] >,\n\t\t\t\t\t},\n\t\t\t\t} ) }\n\t\t\t>\n\t\t\t\t<RichText\n\t\t\t\t\ttagName=\"p\"\n\t\t\t\t\tvalue={ attributes.content }\n\t\t\t\t\tonChange={ ( value ) => updateField( 'content', value ) }\n\t\t\t\t\tplaceholder={ __( {{titleJson}} + ' persistence block', '{{textDomain}}' ) }\n\t\t\t\t/>\n\t\t\t\t<p className=\"{{cssClassName}}__meta\">\n\t\t\t\t\t{ __( 'Resource key:', '{{textDomain}}' ) } { attributes.resourceKey || '\u2014' }\n\t\t\t\t</p>\n\t\t\t\t<p className=\"{{cssClassName}}__meta\">\n\t\t\t\t\t{ __( 'Storage mode:', '{{textDomain}}' ) } {{dataStorageMode}}\n\t\t\t\t</p>\n\t\t\t\t<p className=\"{{cssClassName}}__meta\">\n\t\t\t\t\t{ __( 'Persistence policy:', '{{textDomain}}' ) } {{persistencePolicy}}\n\t\t\t\t</p>\n\t\t\t\t{ ! isValid && (\n\t\t\t\t\t<Notice status=\"error\" isDismissible={ false }>\n\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => <li key={ index }>{ error }</li> ) }\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t</Notice>\n\t\t\t\t) }\n\t\t\t</div>\n\t\t</>\n\t);\n}\n";
2
+ export declare const PERSISTENCE_INDEX_TEMPLATE = "import {\n\tregisterScaffoldBlockType,\n\ttype BlockConfiguration,\n} from '@wp-typia/block-types/blocks/registration';\nimport {\n\tbuildScaffoldBlockRegistration,\n\tparseScaffoldBlockMetadata,\n} from '@wp-typia/block-runtime/blocks';\n\nimport Edit from './edit';\nimport Save from './save';\nimport metadata from './block-metadata';\nimport './style.scss';\n\nimport type { {{pascalCase}}Attributes } from './types';\n\nconst registration = buildScaffoldBlockRegistration(\n\tparseScaffoldBlockMetadata<BlockConfiguration< {{pascalCase}}Attributes >>( metadata ),\n\t{\n\t\tedit: Edit,\n\t\tsave: Save,\n\t}\n);\n\nregisterScaffoldBlockType(registration.name, registration.settings);\n";
3
+ export declare const PERSISTENCE_SAVE_TEMPLATE = "export default function Save() {\n\t// This block is intentionally server-rendered. PHP bootstraps post context,\n\t// storage-backed state, and write-policy data before the frontend hydrates.\n\treturn null;\n}\n";
4
+ export declare const PERSISTENCE_VALIDATORS_TEMPLATE = "import typia from 'typia';\nimport currentManifest from './manifest-defaults-document';\nimport type {\n\t{{pascalCase}}Attributes,\n\t{{pascalCase}}ValidationResult,\n} from './types';\nimport { generateResourceKey } from '@wp-typia/block-runtime/identifiers';\nimport { createTemplateValidatorToolkit } from './validator-toolkit';\n\nconst scaffoldValidators = createTemplateValidatorToolkit< {{pascalCase}}Attributes >( {\n\tassert: typia.createAssert< {{pascalCase}}Attributes >(),\n\tclone: typia.misc.createClone< {{pascalCase}}Attributes >() as (\n\t\tvalue: {{pascalCase}}Attributes,\n\t) => {{pascalCase}}Attributes,\n\tis: typia.createIs< {{pascalCase}}Attributes >(),\n\tmanifest: currentManifest,\n\tprune: typia.misc.createPrune< {{pascalCase}}Attributes >(),\n\trandom: typia.createRandom< {{pascalCase}}Attributes >() as (\n\t\t...args: unknown[]\n\t) => {{pascalCase}}Attributes,\n\tfinalize: ( normalized ) => ( {\n\t\t...normalized,\n\t\tresourceKey:\n\t\t\tnormalized.resourceKey && normalized.resourceKey.length > 0\n\t\t\t\t? normalized.resourceKey\n\t\t\t\t: generateResourceKey( '{{slugKebabCase}}' ),\n\t} ),\n\tvalidate: typia.createValidate< {{pascalCase}}Attributes >(),\n} );\n\nexport const validators = scaffoldValidators.validators;\n\nexport const validate{{pascalCase}}Attributes =\n\tscaffoldValidators.validateAttributes as (\n\t\tattributes: unknown\n\t) => {{pascalCase}}ValidationResult;\n\nexport const sanitize{{pascalCase}}Attributes =\n\tscaffoldValidators.sanitizeAttributes as (\n\t\tattributes: Partial< {{pascalCase}}Attributes >\n\t) => {{pascalCase}}Attributes;\n\nexport const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;\n";
5
+ export declare const PERSISTENCE_INTERACTIVITY_TEMPLATE = "import { getContext, store } from '@wordpress/interactivity';\nimport { generatePublicWriteRequestId } from '@wp-typia/block-runtime/identifiers';\n\nimport { fetchBootstrap, fetchState, writeState } from './api';\nimport type {\n\t{{pascalCase}}ClientState,\n\t{{pascalCase}}Context,\n\t{{pascalCase}}State,\n} from './types';\nimport type {\n\t{{pascalCase}}WriteStateRequest,\n} from './api-types';\n\nfunction hasExpiredPublicWriteToken(\n\texpiresAt?: number\n): boolean {\n\treturn (\n\t\ttypeof expiresAt === 'number' &&\n\t\texpiresAt > 0 &&\n\t\tDate.now() >= expiresAt * 1000\n\t);\n}\n\nfunction getWriteBlockedMessage(\n\tcontext: {{pascalCase}}Context\n): string {\n\treturn context.persistencePolicy === 'authenticated'\n\t\t? 'Sign in to persist this counter.'\n\t\t: 'Public writes are temporarily unavailable.';\n}\n\nconst BOOTSTRAP_MAX_ATTEMPTS = 3;\nconst BOOTSTRAP_RETRY_DELAYS_MS = [ 250, 500 ];\n\nasync function waitForBootstrapRetry( delayMs: number ): Promise< void > {\n\tawait new Promise( ( resolve ) => {\n\t\tsetTimeout( resolve, delayMs );\n\t} );\n}\n\nfunction getClientState(\n\tcontext: {{pascalCase}}Context\n): {{pascalCase}}ClientState {\n\tif ( context.client ) {\n\t\treturn context.client;\n\t}\n\n\tcontext.client = {\n\t\tbootstrapError: '',\n\t\twriteExpiry: 0,\n\t\twriteNonce: '',\n\t\twriteToken: '',\n\t};\n\n\treturn context.client;\n}\n\nfunction clearBootstrapError(\n\tcontext: {{pascalCase}}Context,\n\tclientState: {{pascalCase}}ClientState\n): void {\n\tif ( context.error === clientState.bootstrapError ) {\n\t\tcontext.error = '';\n\t}\n\tclientState.bootstrapError = '';\n}\n\nfunction setBootstrapError(\n\tcontext: {{pascalCase}}Context,\n\tclientState: {{pascalCase}}ClientState,\n\tmessage: string\n): void {\n\tclientState.bootstrapError = message;\n\tcontext.error = message;\n}\n\nconst { actions, state } = store( '{{slugKebabCase}}', {\n\tstate: {\n\t\tisHydrated: false,\n\t} as {{pascalCase}}State,\n\n\tactions: {\n\t\tasync loadState() {\n\t\t\tconst context = getContext< {{pascalCase}}Context >();\n\t\t\tif ( context.postId <= 0 || ! context.resourceKey ) {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcontext.isLoading = true;\n\t\t\tcontext.error = '';\n\n\t\t\ttry {\n\t\t\t\tconst result = await fetchState( {\n\t\t\t\t\tpostId: context.postId,\n\t\t\t\t\tresourceKey: context.resourceKey,\n\t\t\t\t}, {\n\t\t\t\t\ttransportTarget: 'frontend',\n\t\t\t\t} );\n\t\t\t\tif ( ! result.isValid || ! result.data ) {\n\t\t\t\t\tcontext.error = result.errors[ 0 ]?.expected ?? 'Unable to load counter';\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcontext.count = result.data.count;\n\t\t\t} catch ( error ) {\n\t\t\t\tcontext.error =\n\t\t\t\t\terror instanceof Error ? error.message : 'Unknown loading error';\n\t\t\t} finally {\n\t\t\t\tcontext.isLoading = false;\n\t\t\t}\n\t\t},\n\t\tasync loadBootstrap() {\n\t\t\tconst context = getContext< {{pascalCase}}Context >();\n\t\t\tconst clientState = getClientState( context );\n\t\t\tif ( context.postId <= 0 || ! context.resourceKey ) {\n\t\t\t\tcontext.bootstrapReady = true;\n\t\t\t\tcontext.canWrite = false;\n\t\t\t\tclientState.bootstrapError = '';\n\t\t\t\tclientState.writeExpiry = 0;\n\t\t\t\tclientState.writeNonce = '';\n\t\t\t\tclientState.writeToken = '';\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcontext.isBootstrapping = true;\n\n\t\t\tlet bootstrapSucceeded = false;\n\t\t\tlet lastBootstrapError =\n\t\t\t\t'Unable to initialize write access';\n\t\t\tconst includePublicWriteCredentials = {{isPublicPersistencePolicy}};\n\t\t\tconst includeRestNonce = {{isAuthenticatedPersistencePolicy}};\n\n\t\t\tfor ( let attempt = 1; attempt <= BOOTSTRAP_MAX_ATTEMPTS; attempt += 1 ) {\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await fetchBootstrap( {\n\t\t\t\t\t\tpostId: context.postId,\n\t\t\t\t\t\tresourceKey: context.resourceKey,\n\t\t\t\t\t}, {\n\t\t\t\t\t\ttransportTarget: 'frontend',\n\t\t\t\t\t} );\n\t\t\t\t\tif ( ! result.isValid || ! result.data ) {\n\t\t\t\t\t\tlastBootstrapError =\n\t\t\t\t\t\t\tresult.errors[ 0 ]?.expected ??\n\t\t\t\t\t\t\t'Unable to initialize write access';\n\t\t\t\t\t\tif ( attempt < BOOTSTRAP_MAX_ATTEMPTS ) {\n\t\t\t\t\t\t\tawait waitForBootstrapRetry(\n\t\t\t\t\t\t\t\tBOOTSTRAP_RETRY_DELAYS_MS[ attempt - 1 ] ?? 750\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tclientState.writeExpiry =\n\t\t\t\t\t\tincludePublicWriteCredentials &&\n\t\t\t\t\t\t'publicWriteExpiresAt' in result.data &&\n\t\t\t\t\t\ttypeof result.data.publicWriteExpiresAt === 'number' &&\n\t\t\t\t\t\tresult.data.publicWriteExpiresAt > 0\n\t\t\t\t\t\t\t? result.data.publicWriteExpiresAt\n\t\t\t\t\t\t\t: 0;\n\t\t\t\t\tclientState.writeToken =\n\t\t\t\t\t\tincludePublicWriteCredentials &&\n\t\t\t\t\t\t'publicWriteToken' in result.data &&\n\t\t\t\t\t\ttypeof result.data.publicWriteToken === 'string' &&\n\t\t\t\t\t\tresult.data.publicWriteToken.length > 0\n\t\t\t\t\t\t\t? result.data.publicWriteToken\n\t\t\t\t\t\t\t: '';\n\t\t\t\t\tclientState.writeNonce =\n\t\t\t\t\t\tincludeRestNonce &&\n\t\t\t\t\t\t'restNonce' in result.data &&\n\t\t\t\t\t\ttypeof result.data.restNonce === 'string' &&\n\t\t\t\t\t\tresult.data.restNonce.length > 0\n\t\t\t\t\t\t\t? result.data.restNonce\n\t\t\t\t\t\t\t: '';\n\t\t\t\t\tcontext.bootstrapReady = true;\n\t\t\t\t\tcontext.canWrite =\n\t\t\t\t\t\tresult.data.canWrite === true &&\n\t\t\t\t\t\t(\n\t\t\t\t\t\t\tcontext.persistencePolicy === 'authenticated'\n\t\t\t\t\t\t\t\t? clientState.writeNonce.length > 0\n\t\t\t\t\t\t\t\t: clientState.writeToken.length > 0 &&\n\t\t\t\t\t\t\t\t\t! hasExpiredPublicWriteToken( clientState.writeExpiry )\n\t\t\t\t\t\t);\n\t\t\t\t\tclearBootstrapError( context, clientState );\n\t\t\t\t\tbootstrapSucceeded = true;\n\t\t\t\t\tbreak;\n\t\t\t\t} catch ( error ) {\n\t\t\t\t\tlastBootstrapError =\n\t\t\t\t\t\terror instanceof Error ? error.message : 'Unknown bootstrap error';\n\t\t\t\t\tif ( attempt < BOOTSTRAP_MAX_ATTEMPTS ) {\n\t\t\t\t\t\tawait waitForBootstrapRetry(\n\t\t\t\t\t\t\tBOOTSTRAP_RETRY_DELAYS_MS[ attempt - 1 ] ?? 750\n\t\t\t\t\t\t);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif ( ! bootstrapSucceeded ) {\n\t\t\t\tcontext.bootstrapReady = false;\n\t\t\t\tcontext.canWrite = false;\n\t\t\t\tclientState.writeExpiry = 0;\n\t\t\t\tclientState.writeNonce = '';\n\t\t\t\tclientState.writeToken = '';\n\t\t\t\tsetBootstrapError( context, clientState, lastBootstrapError );\n\t\t\t}\n\t\t\tcontext.isBootstrapping = false;\n\t\t},\n\t\tasync increment() {\n\t\t\tconst context = getContext< {{pascalCase}}Context >();\n\t\t\tconst clientState = getClientState( context );\n\t\t\tif ( context.postId <= 0 || ! context.resourceKey ) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( ! context.bootstrapReady ) {\n\t\t\t\tawait actions.loadBootstrap();\n\t\t\t}\n\t\t\tif ( ! context.bootstrapReady ) {\n\t\t\t\tcontext.error = 'Write access is still initializing.';\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (\n\t\t\t\tcontext.persistencePolicy === 'public' &&\n\t\t\t\thasExpiredPublicWriteToken( clientState.writeExpiry )\n\t\t\t) {\n\t\t\t\tawait actions.loadBootstrap();\n\t\t\t}\n\t\t\tif (\n\t\t\t\tcontext.persistencePolicy === 'public' &&\n\t\t\t\thasExpiredPublicWriteToken( clientState.writeExpiry )\n\t\t\t) {\n\t\t\t\tcontext.canWrite = false;\n\t\t\t\tcontext.error = getWriteBlockedMessage( context );\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif ( ! context.canWrite ) {\n\t\t\t\tcontext.error = getWriteBlockedMessage( context );\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tcontext.isSaving = true;\n\t\t\tcontext.error = '';\n\n\t\t\ttry {\n\t\t\t\tconst request = {\n\t\t\t\t\tdelta: 1,\n\t\t\t\t\tpostId: context.postId,\n\t\t\t\t\tresourceKey: context.resourceKey,\n\t\t\t\t} as {{pascalCase}}WriteStateRequest;\n\t\t\t\tif ( {{isPublicPersistencePolicy}} ) {\n\t\t\t\t\trequest.publicWriteRequestId =\n\t\t\t\t\t\tgeneratePublicWriteRequestId() as {{pascalCase}}WriteStateRequest[ 'publicWriteRequestId' ];\n\t\t\t\t\tif ( clientState.writeToken.length > 0 ) {\n\t\t\t\t\t\trequest.publicWriteToken =\n\t\t\t\t\t\t\tclientState.writeToken as {{pascalCase}}WriteStateRequest[ 'publicWriteToken' ];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tconst result = await writeState( request, {\n\t\t\t\t\trestNonce:\n\t\t\t\t\t\tclientState.writeNonce.length > 0\n\t\t\t\t\t\t\t? clientState.writeNonce\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\ttransportTarget: 'frontend',\n\t\t\t\t} );\n\t\t\t\tif ( ! result.isValid || ! result.data ) {\n\t\t\t\t\tcontext.error = result.errors[ 0 ]?.expected ?? 'Unable to update counter';\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t\tcontext.count = result.data.count;\n\t\t\t\tcontext.storage = result.data.storage;\n\t\t\t} catch ( error ) {\n\t\t\t\tcontext.error =\n\t\t\t\t\terror instanceof Error ? error.message : 'Unknown update error';\n\t\t\t} finally {\n\t\t\t\tcontext.isSaving = false;\n\t\t\t}\n\t\t},\n\t},\n\n\tcallbacks: {\n\t\tinit() {\n\t\t\tconst context = getContext< {{pascalCase}}Context >();\n\t\t\tcontext.client = {\n\t\t\t\tbootstrapError: '',\n\t\t\t\twriteExpiry: 0,\n\t\t\t\twriteNonce: '',\n\t\t\t\twriteToken: '',\n\t\t\t};\n\t\t\tcontext.bootstrapReady = false;\n\t\t\tcontext.canWrite = false;\n\t\t\tcontext.count = 0;\n\t\t\tcontext.error = '';\n\t\t\tcontext.isBootstrapping = false;\n\t\t\tcontext.isLoading = false;\n\t\t\tcontext.isSaving = false;\n\t\t},\n\t\tmounted() {\n\t\t\tstate.isHydrated = true;\n\t\t\tif ( typeof document !== 'undefined' ) {\n\t\t\t\tdocument.documentElement.dataset[ '{{slugCamelCase}}Hydrated' ] = 'true';\n\t\t\t}\n\t\t\tvoid Promise.allSettled( [\n\t\t\t\tactions.loadState(),\n\t\t\t\tactions.loadBootstrap(),\n\t\t\t] );\n\t\t},\n\t},\n} );\n";