@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.
- package/README.md +9 -3
- package/dist/runtime/built-in-block-artifact-documents.d.ts +3 -0
- package/dist/runtime/built-in-block-artifact-documents.js +2 -0
- package/dist/runtime/built-in-block-artifact-types.d.ts +51 -0
- package/dist/runtime/built-in-block-artifact-types.js +304 -0
- package/dist/runtime/built-in-block-artifacts.js +4 -803
- package/dist/runtime/built-in-block-attribute-emitters.d.ts +71 -0
- package/dist/runtime/built-in-block-attribute-emitters.js +176 -0
- package/dist/runtime/built-in-block-attribute-specs.d.ts +38 -0
- package/dist/runtime/built-in-block-attribute-specs.js +358 -0
- package/dist/runtime/built-in-block-code-templates/basic.d.ts +4 -0
- package/dist/runtime/built-in-block-code-templates/basic.js +249 -0
- package/dist/runtime/built-in-block-code-templates/compound-child.d.ts +4 -0
- package/dist/runtime/built-in-block-code-templates/compound-child.js +138 -0
- package/dist/runtime/built-in-block-code-templates/compound-parent.d.ts +6 -0
- package/dist/runtime/built-in-block-code-templates/compound-parent.js +227 -0
- package/dist/runtime/built-in-block-code-templates/compound-persistence.d.ts +4 -0
- package/dist/runtime/built-in-block-code-templates/compound-persistence.js +478 -0
- package/dist/runtime/built-in-block-code-templates/compound.d.ts +3 -0
- package/dist/runtime/built-in-block-code-templates/compound.js +3 -0
- package/dist/runtime/built-in-block-code-templates/interactivity.d.ts +5 -0
- package/dist/runtime/built-in-block-code-templates/interactivity.js +547 -0
- package/dist/runtime/built-in-block-code-templates/persistence.d.ts +5 -0
- package/dist/runtime/built-in-block-code-templates/persistence.js +550 -0
- package/dist/runtime/built-in-block-code-templates/shared.d.ts +16 -0
- package/dist/runtime/built-in-block-code-templates/shared.js +53 -0
- package/dist/runtime/built-in-block-code-templates.d.ts +5 -32
- package/dist/runtime/built-in-block-code-templates.js +5 -2230
- package/dist/runtime/cli-add-block-config.d.ts +6 -0
- package/dist/runtime/cli-add-block-config.js +143 -0
- package/dist/runtime/cli-add-block-legacy-validator.d.ts +4 -0
- package/dist/runtime/cli-add-block-legacy-validator.js +168 -0
- package/dist/runtime/cli-add-block.js +3 -301
- package/dist/runtime/cli-add-workspace-assets.d.ts +38 -0
- package/dist/runtime/cli-add-workspace-assets.js +399 -0
- package/dist/runtime/cli-add-workspace.d.ts +2 -38
- package/dist/runtime/cli-add-workspace.js +5 -396
- package/dist/runtime/cli-doctor-environment.d.ts +12 -0
- package/dist/runtime/cli-doctor-environment.js +123 -0
- package/dist/runtime/cli-doctor-workspace.d.ts +14 -0
- package/dist/runtime/cli-doctor-workspace.js +296 -0
- package/dist/runtime/cli-doctor.d.ts +4 -2
- package/dist/runtime/cli-doctor.js +10 -405
- package/dist/runtime/cli-help.js +1 -1
- package/dist/runtime/cli-scaffold.js +1 -1
- package/dist/runtime/migration-command-surface.d.ts +67 -0
- package/dist/runtime/migration-command-surface.js +189 -0
- package/dist/runtime/migration-diff-rename.d.ts +13 -0
- package/dist/runtime/migration-diff-rename.js +192 -0
- package/dist/runtime/migration-diff-transform.d.ts +14 -0
- package/dist/runtime/migration-diff-transform.js +105 -0
- package/dist/runtime/migration-diff.js +12 -297
- package/dist/runtime/migration-generated-artifacts.d.ts +3 -0
- package/dist/runtime/migration-generated-artifacts.js +41 -0
- package/dist/runtime/migration-maintenance.d.ts +51 -0
- package/dist/runtime/migration-maintenance.js +380 -0
- package/dist/runtime/migration-planning.d.ts +23 -0
- package/dist/runtime/migration-planning.js +131 -0
- package/dist/runtime/migration-project-config-source.d.ts +6 -0
- package/dist/runtime/migration-project-config-source.js +424 -0
- package/dist/runtime/migration-project-layout-discovery.d.ts +61 -0
- package/dist/runtime/migration-project-layout-discovery.js +337 -0
- package/dist/runtime/migration-project-layout-paths.d.ts +135 -0
- package/dist/runtime/migration-project-layout-paths.js +288 -0
- package/dist/runtime/migration-project-layout.d.ts +3 -0
- package/dist/runtime/migration-project-layout.js +2 -0
- package/dist/runtime/migration-project-workspace.d.ts +47 -0
- package/dist/runtime/migration-project-workspace.js +212 -0
- package/dist/runtime/migration-project.d.ts +4 -94
- package/dist/runtime/migration-project.js +3 -1101
- package/dist/runtime/migration-render-diff-rule.d.ts +5 -0
- package/dist/runtime/migration-render-diff-rule.js +120 -0
- package/dist/runtime/migration-render-execution.d.ts +3 -0
- package/dist/runtime/migration-render-execution.js +428 -0
- package/dist/runtime/migration-render-generated.d.ts +27 -0
- package/dist/runtime/migration-render-generated.js +230 -0
- package/dist/runtime/migration-render-support.d.ts +3 -0
- package/dist/runtime/migration-render-support.js +16 -0
- package/dist/runtime/migration-render.d.ts +3 -33
- package/dist/runtime/migration-render.js +3 -789
- package/dist/runtime/migration-ui-capability.js +1 -1
- package/dist/runtime/migrations.d.ts +24 -118
- package/dist/runtime/migrations.js +12 -700
- package/dist/runtime/scaffold-bootstrap.d.ts +45 -0
- package/dist/runtime/scaffold-bootstrap.js +185 -0
- package/dist/runtime/scaffold-package-manager-files.d.ts +35 -0
- package/dist/runtime/scaffold-package-manager-files.js +79 -0
- package/dist/runtime/scaffold.d.ts +1 -12
- package/dist/runtime/scaffold.js +10 -393
- package/dist/runtime/template-source-contracts.d.ts +81 -0
- package/dist/runtime/template-source-contracts.js +1 -0
- package/dist/runtime/template-source-external.d.ts +21 -0
- package/dist/runtime/template-source-external.js +184 -0
- package/dist/runtime/template-source-locators.d.ts +4 -0
- package/dist/runtime/template-source-locators.js +72 -0
- package/dist/runtime/template-source-normalization.d.ts +7 -0
- package/dist/runtime/template-source-normalization.js +53 -0
- package/dist/runtime/template-source-remote.d.ts +23 -0
- package/dist/runtime/template-source-remote.js +336 -0
- package/dist/runtime/template-source-seeds.d.ts +12 -0
- package/dist/runtime/template-source-seeds.js +243 -0
- package/dist/runtime/template-source.d.ts +4 -86
- package/dist/runtime/template-source.js +9 -828
- 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";
|