@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,4 @@
1
+ export declare const COMPOUND_PERSISTENCE_PARENT_EDIT_TEMPLATE = "import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';\nimport { __ } from '@wordpress/i18n';\nimport {\n\tInspectorControls,\n\tInnerBlocks,\n\tRichText,\n\tuseBlockProps,\n} from '@wordpress/block-editor';\nimport {\n\tNotice,\n\tPanelBody,\n\tTextControl,\n\tToggleControl,\n} from '@wordpress/components';\n\nimport {\n\tALLOWED_CHILD_BLOCKS,\n\tDEFAULT_CHILD_TEMPLATE,\n} from './children';\nimport { useTypiaValidation } from './hooks';\nimport type { {{pascalCase}}Attributes } from './types';\nimport {\n\tcreateAttributeUpdater,\n\tvalidate{{pascalCase}}Attributes,\n} from './validators';\n\ntype EditProps = BlockEditProps< {{pascalCase}}Attributes >;\n\nexport default function Edit( {\n\tattributes,\n\tsetAttributes,\n}: EditProps ) {\n\tconst { errorMessages, isValid } = useTypiaValidation(\n\t\tattributes,\n\t\tvalidate{{pascalCase}}Attributes\n\t);\n\tconst updateAttribute = createAttributeUpdater( attributes, setAttributes );\n\tconst blockProps = useBlockProps( {\n\t\tclassName: '{{cssClassName}}',\n\t} );\n\n\treturn (\n\t\t<>\n\t\t\t<InspectorControls>\n\t\t\t\t<PanelBody title={ __( 'Compound Settings', '{{textDomain}}' ) }>\n\t\t\t\t\t<ToggleControl\n\t\t\t\t\t\tlabel={ __( 'Show dividers between items', '{{textDomain}}' ) }\n\t\t\t\t\t\tchecked={ attributes.showDividers ?? true }\n\t\t\t\t\t\tonChange={ ( value ) => updateAttribute( 'showDividers', value ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<ToggleControl\n\t\t\t\t\t\tlabel={ __( 'Show persisted count', '{{textDomain}}' ) }\n\t\t\t\t\t\tchecked={ attributes.showCount ?? true }\n\t\t\t\t\t\tonChange={ ( value ) => updateAttribute( 'showCount', value ) }\n\t\t\t\t\t/>\n\t\t\t\t\t<TextControl\n\t\t\t\t\t\tlabel={ __( 'Button label', '{{textDomain}}' ) }\n\t\t\t\t\t\tvalue={ attributes.buttonLabel ?? 'Persist Count' }\n\t\t\t\t\t\tonChange={ ( buttonLabel ) => updateAttribute( 'buttonLabel', buttonLabel ) }\n\t\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={ ( resourceKey ) => updateAttribute( 'resourceKey', resourceKey ) }\n\t\t\t\t\t\thelp={ __( 'Stable key used by the persisted 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</Notice>\n\t\t\t\t</PanelBody>\n\t\t\t\t{ ! isValid && (\n\t\t\t\t\t<PanelBody title={ __( 'Validation Errors', '{{textDomain}}' ) } initialOpen>\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 { ...blockProps }>\n\t\t\t\t<RichText\n\t\t\t\t\ttagName=\"h3\"\n\t\t\t\t\tclassName=\"{{cssClassName}}__heading\"\n\t\t\t\t\tvalue={ attributes.heading }\n\t\t\t\t\tonChange={ ( heading ) => updateAttribute( 'heading', heading ) }\n\t\t\t\t\tplaceholder={ __( {{titleJson}}, '{{textDomain}}' ) }\n\t\t\t\t/>\n\t\t\t\t<RichText\n\t\t\t\t\ttagName=\"p\"\n\t\t\t\t\tclassName=\"{{cssClassName}}__intro\"\n\t\t\t\t\tvalue={ attributes.intro ?? '' }\n\t\t\t\t\tonChange={ ( intro ) => updateAttribute( 'intro', intro ) }\n\t\t\t\t\tplaceholder={ __(\n\t\t\t\t\t\t'Add and reorder internal items inside this compound block.',\n\t\t\t\t\t\t'{{textDomain}}'\n\t\t\t\t\t) }\n\t\t\t\t/>\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\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<div className=\"{{cssClassName}}__items\">\n\t\t\t\t\t<InnerBlocks\n\t\t\t\t\t\tallowedBlocks={ ALLOWED_CHILD_BLOCKS }\n\t\t\t\t\t\trenderAppender={ InnerBlocks.ButtonBlockAppender }\n\t\t\t\t\t\ttemplate={ DEFAULT_CHILD_TEMPLATE }\n\t\t\t\t\t\ttemplateLock={ false }\n\t\t\t\t\t/>\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</>\n\t);\n}\n";
2
+ export declare const COMPOUND_PERSISTENCE_PARENT_SAVE_TEMPLATE = "export default function Save() {\n\treturn null;\n}\n";
3
+ export declare const COMPOUND_PERSISTENCE_PARENT_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";
4
+ export declare const COMPOUND_PERSISTENCE_PARENT_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';\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 result = await writeState( {\n\t\t\t\t\tdelta: 1,\n\t\t\t\t\tpostId: context.postId,\n\t\t\t\t\tpublicWriteRequestId:\n\t\t\t\t\t\tcontext.persistencePolicy === 'public'\n\t\t\t\t\t\t\t? generatePublicWriteRequestId()\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\tpublicWriteToken:\n\t\t\t\t\t\tcontext.persistencePolicy === 'public' &&\n\t\t\t\t\t\tclientState.writeToken.length > 0\n\t\t\t\t\t\t\t? clientState.writeToken\n\t\t\t\t\t\t\t: undefined,\n\t\t\t\t\tresourceKey: context.resourceKey,\n\t\t\t\t}, {\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";
@@ -0,0 +1,478 @@
1
+ export const COMPOUND_PERSISTENCE_PARENT_EDIT_TEMPLATE = `import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';
2
+ import { __ } from '@wordpress/i18n';
3
+ import {
4
+ \tInspectorControls,
5
+ \tInnerBlocks,
6
+ \tRichText,
7
+ \tuseBlockProps,
8
+ } from '@wordpress/block-editor';
9
+ import {
10
+ \tNotice,
11
+ \tPanelBody,
12
+ \tTextControl,
13
+ \tToggleControl,
14
+ } from '@wordpress/components';
15
+
16
+ import {
17
+ \tALLOWED_CHILD_BLOCKS,
18
+ \tDEFAULT_CHILD_TEMPLATE,
19
+ } from './children';
20
+ import { useTypiaValidation } from './hooks';
21
+ import type { {{pascalCase}}Attributes } from './types';
22
+ import {
23
+ \tcreateAttributeUpdater,
24
+ \tvalidate{{pascalCase}}Attributes,
25
+ } from './validators';
26
+
27
+ type EditProps = BlockEditProps< {{pascalCase}}Attributes >;
28
+
29
+ export default function Edit( {
30
+ \tattributes,
31
+ \tsetAttributes,
32
+ }: EditProps ) {
33
+ \tconst { errorMessages, isValid } = useTypiaValidation(
34
+ \t\tattributes,
35
+ \t\tvalidate{{pascalCase}}Attributes
36
+ \t);
37
+ \tconst updateAttribute = createAttributeUpdater( attributes, setAttributes );
38
+ \tconst blockProps = useBlockProps( {
39
+ \t\tclassName: '{{cssClassName}}',
40
+ \t} );
41
+
42
+ \treturn (
43
+ \t\t<>
44
+ \t\t\t<InspectorControls>
45
+ \t\t\t\t<PanelBody title={ __( 'Compound Settings', '{{textDomain}}' ) }>
46
+ \t\t\t\t\t<ToggleControl
47
+ \t\t\t\t\t\tlabel={ __( 'Show dividers between items', '{{textDomain}}' ) }
48
+ \t\t\t\t\t\tchecked={ attributes.showDividers ?? true }
49
+ \t\t\t\t\t\tonChange={ ( value ) => updateAttribute( 'showDividers', value ) }
50
+ \t\t\t\t\t/>
51
+ \t\t\t\t\t<ToggleControl
52
+ \t\t\t\t\t\tlabel={ __( 'Show persisted count', '{{textDomain}}' ) }
53
+ \t\t\t\t\t\tchecked={ attributes.showCount ?? true }
54
+ \t\t\t\t\t\tonChange={ ( value ) => updateAttribute( 'showCount', value ) }
55
+ \t\t\t\t\t/>
56
+ \t\t\t\t\t<TextControl
57
+ \t\t\t\t\t\tlabel={ __( 'Button label', '{{textDomain}}' ) }
58
+ \t\t\t\t\t\tvalue={ attributes.buttonLabel ?? 'Persist Count' }
59
+ \t\t\t\t\t\tonChange={ ( buttonLabel ) => updateAttribute( 'buttonLabel', buttonLabel ) }
60
+ \t\t\t\t\t/>
61
+ \t\t\t\t\t<TextControl
62
+ \t\t\t\t\t\tlabel={ __( 'Resource key', '{{textDomain}}' ) }
63
+ \t\t\t\t\t\tvalue={ attributes.resourceKey ?? '' }
64
+ \t\t\t\t\t\tonChange={ ( resourceKey ) => updateAttribute( 'resourceKey', resourceKey ) }
65
+ \t\t\t\t\t\thelp={ __( 'Stable key used by the persisted counter endpoint.', '{{textDomain}}' ) }
66
+ \t\t\t\t\t/>
67
+ \t\t\t\t\t<Notice status="info" isDismissible={ false }>
68
+ \t\t\t\t\t\t{ __( 'Storage mode: {{dataStorageMode}}', '{{textDomain}}' ) }
69
+ \t\t\t\t\t</Notice>
70
+ \t\t\t\t\t<Notice status="info" isDismissible={ false }>
71
+ \t\t\t\t\t\t{ __( 'Persistence policy: {{persistencePolicy}}', '{{textDomain}}' ) }
72
+ \t\t\t\t\t</Notice>
73
+ \t\t\t\t</PanelBody>
74
+ \t\t\t\t{ ! isValid && (
75
+ \t\t\t\t\t<PanelBody title={ __( 'Validation Errors', '{{textDomain}}' ) } initialOpen>
76
+ \t\t\t\t\t\t{ errorMessages.map( ( error, index ) => (
77
+ \t\t\t\t\t\t\t<Notice key={ index } status="error" isDismissible={ false }>
78
+ \t\t\t\t\t\t\t\t{ error }
79
+ \t\t\t\t\t\t\t</Notice>
80
+ \t\t\t\t\t\t) ) }
81
+ \t\t\t\t\t</PanelBody>
82
+ \t\t\t\t) }
83
+ \t\t\t</InspectorControls>
84
+ \t\t\t<div { ...blockProps }>
85
+ \t\t\t\t<RichText
86
+ \t\t\t\t\ttagName="h3"
87
+ \t\t\t\t\tclassName="{{cssClassName}}__heading"
88
+ \t\t\t\t\tvalue={ attributes.heading }
89
+ \t\t\t\t\tonChange={ ( heading ) => updateAttribute( 'heading', heading ) }
90
+ \t\t\t\t\tplaceholder={ __( {{titleJson}}, '{{textDomain}}' ) }
91
+ \t\t\t\t/>
92
+ \t\t\t\t<RichText
93
+ \t\t\t\t\ttagName="p"
94
+ \t\t\t\t\tclassName="{{cssClassName}}__intro"
95
+ \t\t\t\t\tvalue={ attributes.intro ?? '' }
96
+ \t\t\t\t\tonChange={ ( intro ) => updateAttribute( 'intro', intro ) }
97
+ \t\t\t\t\tplaceholder={ __(
98
+ \t\t\t\t\t\t'Add and reorder internal items inside this compound block.',
99
+ \t\t\t\t\t\t'{{textDomain}}'
100
+ \t\t\t\t\t) }
101
+ \t\t\t\t/>
102
+ \t\t\t\t{ ! isValid && (
103
+ \t\t\t\t\t<Notice status="error" isDismissible={ false }>
104
+ \t\t\t\t\t\t<ul>
105
+ \t\t\t\t\t\t\t{ errorMessages.map( ( error, index ) => <li key={ index }>{ error }</li> ) }
106
+ \t\t\t\t\t\t</ul>
107
+ \t\t\t\t\t</Notice>
108
+ \t\t\t\t) }
109
+ \t\t\t\t<p className="{{cssClassName}}__meta">
110
+ \t\t\t\t\t{ __( 'Resource key:', '{{textDomain}}' ) } { attributes.resourceKey || '—' }
111
+ \t\t\t\t</p>
112
+ \t\t\t\t<div className="{{cssClassName}}__items">
113
+ \t\t\t\t\t<InnerBlocks
114
+ \t\t\t\t\t\tallowedBlocks={ ALLOWED_CHILD_BLOCKS }
115
+ \t\t\t\t\t\trenderAppender={ InnerBlocks.ButtonBlockAppender }
116
+ \t\t\t\t\t\ttemplate={ DEFAULT_CHILD_TEMPLATE }
117
+ \t\t\t\t\t\ttemplateLock={ false }
118
+ \t\t\t\t\t/>
119
+ \t\t\t\t</div>
120
+ \t\t\t</div>
121
+ \t\t</>
122
+ \t);
123
+ }
124
+ `;
125
+ export const COMPOUND_PERSISTENCE_PARENT_SAVE_TEMPLATE = `export default function Save() {
126
+ \treturn null;
127
+ }
128
+ `;
129
+ export const COMPOUND_PERSISTENCE_PARENT_VALIDATORS_TEMPLATE = `import typia from 'typia';
130
+ import currentManifest from './manifest-defaults-document';
131
+ import type {
132
+ \t{{pascalCase}}Attributes,
133
+ \t{{pascalCase}}ValidationResult,
134
+ } from './types';
135
+ import { generateResourceKey } from '@wp-typia/block-runtime/identifiers';
136
+ import { createTemplateValidatorToolkit } from '../../validator-toolkit';
137
+
138
+ const scaffoldValidators = createTemplateValidatorToolkit< {{pascalCase}}Attributes >( {
139
+ \tassert: typia.createAssert< {{pascalCase}}Attributes >(),
140
+ \tclone: typia.misc.createClone< {{pascalCase}}Attributes >() as (
141
+ \t\tvalue: {{pascalCase}}Attributes,
142
+ \t) => {{pascalCase}}Attributes,
143
+ \tis: typia.createIs< {{pascalCase}}Attributes >(),
144
+ \tmanifest: currentManifest,
145
+ \tprune: typia.misc.createPrune< {{pascalCase}}Attributes >(),
146
+ \trandom: typia.createRandom< {{pascalCase}}Attributes >() as (
147
+ \t\t...args: unknown[]
148
+ \t) => {{pascalCase}}Attributes,
149
+ \tfinalize: ( normalized ) => ( {
150
+ \t\t...normalized,
151
+ \t\tresourceKey:
152
+ \t\t\tnormalized.resourceKey && normalized.resourceKey.length > 0
153
+ \t\t\t\t? normalized.resourceKey
154
+ \t\t\t\t: generateResourceKey( '{{slugKebabCase}}' ),
155
+ \t} ),
156
+ \tvalidate: typia.createValidate< {{pascalCase}}Attributes >(),
157
+ } );
158
+
159
+ export const validators = scaffoldValidators.validators;
160
+
161
+ export const validate{{pascalCase}}Attributes =
162
+ \tscaffoldValidators.validateAttributes as (
163
+ \t\tattributes: unknown
164
+ \t) => {{pascalCase}}ValidationResult;
165
+
166
+ export const sanitize{{pascalCase}}Attributes =
167
+ \tscaffoldValidators.sanitizeAttributes as (
168
+ \t\tattributes: Partial< {{pascalCase}}Attributes >
169
+ \t) => {{pascalCase}}Attributes;
170
+
171
+ export const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;
172
+ `;
173
+ export const COMPOUND_PERSISTENCE_PARENT_INTERACTIVITY_TEMPLATE = `import { getContext, store } from '@wordpress/interactivity';
174
+ import { generatePublicWriteRequestId } from '@wp-typia/block-runtime/identifiers';
175
+
176
+ import { fetchBootstrap, fetchState, writeState } from './api';
177
+ import type {
178
+ \t{{pascalCase}}ClientState,
179
+ \t{{pascalCase}}Context,
180
+ \t{{pascalCase}}State,
181
+ } from './types';
182
+
183
+ function hasExpiredPublicWriteToken(
184
+ \texpiresAt?: number
185
+ ): boolean {
186
+ \treturn (
187
+ \t\ttypeof expiresAt === 'number' &&
188
+ \t\texpiresAt > 0 &&
189
+ \t\tDate.now() >= expiresAt * 1000
190
+ \t);
191
+ }
192
+
193
+ function getWriteBlockedMessage(
194
+ \tcontext: {{pascalCase}}Context
195
+ ): string {
196
+ \treturn context.persistencePolicy === 'authenticated'
197
+ \t\t? 'Sign in to persist this counter.'
198
+ \t\t: 'Public writes are temporarily unavailable.';
199
+ }
200
+
201
+ const BOOTSTRAP_MAX_ATTEMPTS = 3;
202
+ const BOOTSTRAP_RETRY_DELAYS_MS = [ 250, 500 ];
203
+
204
+ async function waitForBootstrapRetry( delayMs: number ): Promise< void > {
205
+ \tawait new Promise( ( resolve ) => {
206
+ \t\tsetTimeout( resolve, delayMs );
207
+ \t} );
208
+ }
209
+
210
+ function getClientState(
211
+ \tcontext: {{pascalCase}}Context
212
+ ): {{pascalCase}}ClientState {
213
+ \tif ( context.client ) {
214
+ \t\treturn context.client;
215
+ \t}
216
+
217
+ \tcontext.client = {
218
+ \t\tbootstrapError: '',
219
+ \t\twriteExpiry: 0,
220
+ \t\twriteNonce: '',
221
+ \t\twriteToken: '',
222
+ \t};
223
+
224
+ \treturn context.client;
225
+ }
226
+
227
+ function clearBootstrapError(
228
+ \tcontext: {{pascalCase}}Context,
229
+ \tclientState: {{pascalCase}}ClientState
230
+ ): void {
231
+ \tif ( context.error === clientState.bootstrapError ) {
232
+ \t\tcontext.error = '';
233
+ \t}
234
+ \tclientState.bootstrapError = '';
235
+ }
236
+
237
+ function setBootstrapError(
238
+ \tcontext: {{pascalCase}}Context,
239
+ \tclientState: {{pascalCase}}ClientState,
240
+ \tmessage: string
241
+ ): void {
242
+ \tclientState.bootstrapError = message;
243
+ \tcontext.error = message;
244
+ }
245
+
246
+ const { actions, state } = store( '{{slugKebabCase}}', {
247
+ \tstate: {
248
+ \t\tisHydrated: false,
249
+ \t} as {{pascalCase}}State,
250
+
251
+ \tactions: {
252
+ \t\tasync loadState() {
253
+ \t\t\tconst context = getContext< {{pascalCase}}Context >();
254
+ \t\t\tif ( context.postId <= 0 || ! context.resourceKey ) {
255
+ \t\t\t\treturn;
256
+ \t\t\t}
257
+
258
+ \t\t\tcontext.isLoading = true;
259
+ \t\t\tcontext.error = '';
260
+
261
+ \t\t\ttry {
262
+ \t\t\t\tconst result = await fetchState( {
263
+ \t\t\t\t\tpostId: context.postId,
264
+ \t\t\t\t\tresourceKey: context.resourceKey,
265
+ \t\t\t\t}, {
266
+ \t\t\t\t\ttransportTarget: 'frontend',
267
+ \t\t\t\t} );
268
+ \t\t\t\tif ( ! result.isValid || ! result.data ) {
269
+ \t\t\t\t\tcontext.error = result.errors[ 0 ]?.expected ?? 'Unable to load counter';
270
+ \t\t\t\t\treturn;
271
+ \t\t\t\t}
272
+ \t\t\t\tcontext.count = result.data.count;
273
+ \t\t\t} catch ( error ) {
274
+ \t\t\t\tcontext.error =
275
+ \t\t\t\t\terror instanceof Error ? error.message : 'Unknown loading error';
276
+ \t\t\t} finally {
277
+ \t\t\t\tcontext.isLoading = false;
278
+ \t\t\t}
279
+ \t\t},
280
+ \t\tasync loadBootstrap() {
281
+ \t\t\tconst context = getContext< {{pascalCase}}Context >();
282
+ \t\t\tconst clientState = getClientState( context );
283
+ \t\t\tif ( context.postId <= 0 || ! context.resourceKey ) {
284
+ \t\t\t\tcontext.bootstrapReady = true;
285
+ \t\t\t\tcontext.canWrite = false;
286
+ \t\t\t\tclientState.bootstrapError = '';
287
+ \t\t\t\tclientState.writeExpiry = 0;
288
+ \t\t\t\tclientState.writeNonce = '';
289
+ \t\t\t\tclientState.writeToken = '';
290
+ \t\t\t\treturn;
291
+ \t\t\t}
292
+
293
+ \t\t\tcontext.isBootstrapping = true;
294
+
295
+ \t\t\tlet bootstrapSucceeded = false;
296
+ \t\t\tlet lastBootstrapError =
297
+ \t\t\t\t'Unable to initialize write access';
298
+ \t\t\tconst includePublicWriteCredentials = {{isPublicPersistencePolicy}};
299
+ \t\t\tconst includeRestNonce = {{isAuthenticatedPersistencePolicy}};
300
+
301
+ \t\t\tfor ( let attempt = 1; attempt <= BOOTSTRAP_MAX_ATTEMPTS; attempt += 1 ) {
302
+ \t\t\t\ttry {
303
+ \t\t\t\t\tconst result = await fetchBootstrap( {
304
+ \t\t\t\t\t\tpostId: context.postId,
305
+ \t\t\t\t\t\tresourceKey: context.resourceKey,
306
+ \t\t\t\t\t}, {
307
+ \t\t\t\t\t\ttransportTarget: 'frontend',
308
+ \t\t\t\t\t} );
309
+ \t\t\t\t\tif ( ! result.isValid || ! result.data ) {
310
+ \t\t\t\t\t\tlastBootstrapError =
311
+ \t\t\t\t\t\t\tresult.errors[ 0 ]?.expected ??
312
+ \t\t\t\t\t\t\t'Unable to initialize write access';
313
+ \t\t\t\t\t\tif ( attempt < BOOTSTRAP_MAX_ATTEMPTS ) {
314
+ \t\t\t\t\t\t\tawait waitForBootstrapRetry(
315
+ \t\t\t\t\t\t\t\tBOOTSTRAP_RETRY_DELAYS_MS[ attempt - 1 ] ?? 750
316
+ \t\t\t\t\t\t\t);
317
+ \t\t\t\t\t\t\tcontinue;
318
+ \t\t\t\t\t\t}
319
+ \t\t\t\t\t\tbreak;
320
+ \t\t\t\t\t}
321
+
322
+ \t\t\t\t\tclientState.writeExpiry =
323
+ \t\t\t\t\t\tincludePublicWriteCredentials &&
324
+ \t\t\t\t\t\t'publicWriteExpiresAt' in result.data &&
325
+ \t\t\t\t\t\ttypeof result.data.publicWriteExpiresAt === 'number' &&
326
+ \t\t\t\t\t\tresult.data.publicWriteExpiresAt > 0
327
+ \t\t\t\t\t\t\t? result.data.publicWriteExpiresAt
328
+ \t\t\t\t\t\t\t: 0;
329
+ \t\t\t\t\tclientState.writeToken =
330
+ \t\t\t\t\t\tincludePublicWriteCredentials &&
331
+ \t\t\t\t\t\t'publicWriteToken' in result.data &&
332
+ \t\t\t\t\t\ttypeof result.data.publicWriteToken === 'string' &&
333
+ \t\t\t\t\t\tresult.data.publicWriteToken.length > 0
334
+ \t\t\t\t\t\t\t? result.data.publicWriteToken
335
+ \t\t\t\t\t\t\t: '';
336
+ \t\t\t\t\tclientState.writeNonce =
337
+ \t\t\t\t\t\tincludeRestNonce &&
338
+ \t\t\t\t\t\t'restNonce' in result.data &&
339
+ \t\t\t\t\t\ttypeof result.data.restNonce === 'string' &&
340
+ \t\t\t\t\t\tresult.data.restNonce.length > 0
341
+ \t\t\t\t\t\t\t? result.data.restNonce
342
+ \t\t\t\t\t\t\t: '';
343
+ \t\t\t\t\tcontext.bootstrapReady = true;
344
+ \t\t\t\t\tcontext.canWrite =
345
+ \t\t\t\t\t\tresult.data.canWrite === true &&
346
+ \t\t\t\t\t\t(
347
+ \t\t\t\t\t\t\tcontext.persistencePolicy === 'authenticated'
348
+ \t\t\t\t\t\t\t\t? clientState.writeNonce.length > 0
349
+ \t\t\t\t\t\t\t\t: clientState.writeToken.length > 0 &&
350
+ \t\t\t\t\t\t\t\t\t! hasExpiredPublicWriteToken( clientState.writeExpiry )
351
+ \t\t\t\t\t\t);
352
+ \t\t\t\t\tclearBootstrapError( context, clientState );
353
+ \t\t\t\t\tbootstrapSucceeded = true;
354
+ \t\t\t\t\tbreak;
355
+ \t\t\t\t} catch ( error ) {
356
+ \t\t\t\t\tlastBootstrapError =
357
+ \t\t\t\t\t\terror instanceof Error ? error.message : 'Unknown bootstrap error';
358
+ \t\t\t\t\tif ( attempt < BOOTSTRAP_MAX_ATTEMPTS ) {
359
+ \t\t\t\t\t\tawait waitForBootstrapRetry(
360
+ \t\t\t\t\t\t\tBOOTSTRAP_RETRY_DELAYS_MS[ attempt - 1 ] ?? 750
361
+ \t\t\t\t\t\t);
362
+ \t\t\t\t\t\tcontinue;
363
+ \t\t\t\t\t}
364
+ \t\t\t\t\tbreak;
365
+ \t\t\t\t}
366
+ \t\t\t}
367
+
368
+ \t\t\tif ( ! bootstrapSucceeded ) {
369
+ \t\t\t\tcontext.bootstrapReady = false;
370
+ \t\t\t\tcontext.canWrite = false;
371
+ \t\t\t\tclientState.writeExpiry = 0;
372
+ \t\t\t\tclientState.writeNonce = '';
373
+ \t\t\t\tclientState.writeToken = '';
374
+ \t\t\t\tsetBootstrapError( context, clientState, lastBootstrapError );
375
+ \t\t\t}
376
+ \t\t\tcontext.isBootstrapping = false;
377
+ \t\t},
378
+ \t\tasync increment() {
379
+ \t\t\tconst context = getContext< {{pascalCase}}Context >();
380
+ \t\t\tconst clientState = getClientState( context );
381
+ \t\t\tif ( context.postId <= 0 || ! context.resourceKey ) {
382
+ \t\t\t\treturn;
383
+ \t\t\t}
384
+ \t\t\tif ( ! context.bootstrapReady ) {
385
+ \t\t\t\tawait actions.loadBootstrap();
386
+ \t\t\t}
387
+ \t\t\tif ( ! context.bootstrapReady ) {
388
+ \t\t\t\tcontext.error = 'Write access is still initializing.';
389
+ \t\t\t\treturn;
390
+ \t\t\t}
391
+ \t\t\tif (
392
+ \t\t\t\tcontext.persistencePolicy === 'public' &&
393
+ \t\t\t\thasExpiredPublicWriteToken( clientState.writeExpiry )
394
+ \t\t\t) {
395
+ \t\t\t\tawait actions.loadBootstrap();
396
+ \t\t\t}
397
+ \t\t\tif (
398
+ \t\t\t\tcontext.persistencePolicy === 'public' &&
399
+ \t\t\t\thasExpiredPublicWriteToken( clientState.writeExpiry )
400
+ \t\t\t) {
401
+ \t\t\t\tcontext.canWrite = false;
402
+ \t\t\t\tcontext.error = getWriteBlockedMessage( context );
403
+ \t\t\t\treturn;
404
+ \t\t\t}
405
+ \t\t\tif ( ! context.canWrite ) {
406
+ \t\t\t\tcontext.error = getWriteBlockedMessage( context );
407
+ \t\t\t\treturn;
408
+ \t\t\t}
409
+
410
+ \t\t\tcontext.isSaving = true;
411
+ \t\t\tcontext.error = '';
412
+
413
+ \t\t\ttry {
414
+ \t\t\t\tconst result = await writeState( {
415
+ \t\t\t\t\tdelta: 1,
416
+ \t\t\t\t\tpostId: context.postId,
417
+ \t\t\t\t\tpublicWriteRequestId:
418
+ \t\t\t\t\t\tcontext.persistencePolicy === 'public'
419
+ \t\t\t\t\t\t\t? generatePublicWriteRequestId()
420
+ \t\t\t\t\t\t\t: undefined,
421
+ \t\t\t\t\tpublicWriteToken:
422
+ \t\t\t\t\t\tcontext.persistencePolicy === 'public' &&
423
+ \t\t\t\t\t\tclientState.writeToken.length > 0
424
+ \t\t\t\t\t\t\t? clientState.writeToken
425
+ \t\t\t\t\t\t\t: undefined,
426
+ \t\t\t\t\tresourceKey: context.resourceKey,
427
+ \t\t\t\t}, {
428
+ \t\t\t\t\trestNonce:
429
+ \t\t\t\t\t\tclientState.writeNonce.length > 0
430
+ \t\t\t\t\t\t\t? clientState.writeNonce
431
+ \t\t\t\t\t\t\t: undefined,
432
+ \t\t\t\t\ttransportTarget: 'frontend',
433
+ \t\t\t\t} );
434
+ \t\t\t\tif ( ! result.isValid || ! result.data ) {
435
+ \t\t\t\t\tcontext.error = result.errors[ 0 ]?.expected ?? 'Unable to update counter';
436
+ \t\t\t\t\treturn;
437
+ \t\t\t\t}
438
+ \t\t\t\tcontext.count = result.data.count;
439
+ \t\t\t\tcontext.storage = result.data.storage;
440
+ \t\t\t} catch ( error ) {
441
+ \t\t\t\tcontext.error =
442
+ \t\t\t\t\terror instanceof Error ? error.message : 'Unknown update error';
443
+ \t\t\t} finally {
444
+ \t\t\t\tcontext.isSaving = false;
445
+ \t\t\t}
446
+ \t\t},
447
+ \t},
448
+
449
+ \tcallbacks: {
450
+ \t\tinit() {
451
+ \t\t\tconst context = getContext< {{pascalCase}}Context >();
452
+ \t\t\tcontext.client = {
453
+ \t\t\t\tbootstrapError: '',
454
+ \t\t\t\twriteExpiry: 0,
455
+ \t\t\t\twriteNonce: '',
456
+ \t\t\t\twriteToken: '',
457
+ \t\t\t};
458
+ \t\t\tcontext.bootstrapReady = false;
459
+ \t\t\tcontext.canWrite = false;
460
+ \t\t\tcontext.count = 0;
461
+ \t\t\tcontext.error = '';
462
+ \t\t\tcontext.isBootstrapping = false;
463
+ \t\t\tcontext.isLoading = false;
464
+ \t\t\tcontext.isSaving = false;
465
+ \t\t},
466
+ \t\tmounted() {
467
+ \t\t\tstate.isHydrated = true;
468
+ \t\t\tif ( typeof document !== 'undefined' ) {
469
+ \t\t\t\tdocument.documentElement.dataset[ '{{slugCamelCase}}Hydrated' ] = 'true';
470
+ \t\t\t}
471
+ \t\t\tvoid Promise.allSettled( [
472
+ \t\t\t\tactions.loadState(),
473
+ \t\t\t\tactions.loadBootstrap(),
474
+ \t\t\t] );
475
+ \t\t},
476
+ \t},
477
+ } );
478
+ `;
@@ -0,0 +1,3 @@
1
+ export { COMPOUND_CHILDREN_TEMPLATE, COMPOUND_LOCAL_HOOKS_TEMPLATE, COMPOUND_PARENT_EDIT_TEMPLATE, COMPOUND_PARENT_INDEX_TEMPLATE, COMPOUND_PARENT_SAVE_TEMPLATE, COMPOUND_PARENT_VALIDATORS_TEMPLATE, } from './compound-parent.js';
2
+ export { COMPOUND_CHILD_EDIT_TEMPLATE, COMPOUND_CHILD_INDEX_TEMPLATE, COMPOUND_CHILD_SAVE_TEMPLATE, COMPOUND_CHILD_VALIDATORS_TEMPLATE, } from './compound-child.js';
3
+ export { COMPOUND_PERSISTENCE_PARENT_EDIT_TEMPLATE, COMPOUND_PERSISTENCE_PARENT_INTERACTIVITY_TEMPLATE, COMPOUND_PERSISTENCE_PARENT_SAVE_TEMPLATE, COMPOUND_PERSISTENCE_PARENT_VALIDATORS_TEMPLATE, } from './compound-persistence.js';
@@ -0,0 +1,3 @@
1
+ export { COMPOUND_CHILDREN_TEMPLATE, COMPOUND_LOCAL_HOOKS_TEMPLATE, COMPOUND_PARENT_EDIT_TEMPLATE, COMPOUND_PARENT_INDEX_TEMPLATE, COMPOUND_PARENT_SAVE_TEMPLATE, COMPOUND_PARENT_VALIDATORS_TEMPLATE, } from './compound-parent.js';
2
+ export { COMPOUND_CHILD_EDIT_TEMPLATE, COMPOUND_CHILD_INDEX_TEMPLATE, COMPOUND_CHILD_SAVE_TEMPLATE, COMPOUND_CHILD_VALIDATORS_TEMPLATE, } from './compound-child.js';
3
+ export { COMPOUND_PERSISTENCE_PARENT_EDIT_TEMPLATE, COMPOUND_PERSISTENCE_PARENT_INTERACTIVITY_TEMPLATE, COMPOUND_PERSISTENCE_PARENT_SAVE_TEMPLATE, COMPOUND_PERSISTENCE_PARENT_VALIDATORS_TEMPLATE, } from './compound-persistence.js';
@@ -0,0 +1,5 @@
1
+ export declare const INTERACTIVITY_EDIT_TEMPLATE = "import type { BlockEditProps } from '@wp-typia/block-types/blocks/registration';\nimport { __ } from '@wordpress/i18n';\nimport { useBlockProps, InspectorControls, RichText, BlockControls, AlignmentToolbar } from '@wordpress/block-editor';\nimport { PanelBody, RangeControl, Button, Notice } from '@wordpress/components';\nimport { useState } from '@wordpress/element';\nimport currentManifest from './manifest-document';\nimport {\n InspectorFromManifest,\n useEditorFields,\n useTypedAttributeUpdater,\n} from '@wp-typia/block-runtime/inspector';\nimport type { {{pascalCase}}Attributes } from './types';\nimport {\n sanitize{{pascalCase}}Attributes,\n validate{{pascalCase}}Attributes,\n} from './validators';\nimport { useTypiaValidation } from './hooks';\n\ntype EditProps = BlockEditProps<{{pascalCase}}Attributes>;\n\nconst actionButtonRowStyle = { display: 'flex', gap: '8px', marginTop: '16px' };\nconst validationListStyle = { margin: 0, paddingLeft: '1em' };\n\nexport default function Edit({ attributes, setAttributes, isSelected }: EditProps) {\n const [isPreviewing, setIsPreviewing] = useState(false);\n const editorFields = useEditorFields(currentManifest, {\n manual: ['content', 'clickCount', 'maxClicks'],\n labels: {\n alignment: __('Alignment', '{{textDomain}}'),\n animation: __('Animation', '{{textDomain}}'),\n interactiveMode: __('Interactive Mode', '{{textDomain}}'),\n isVisible: __('Visible', '{{textDomain}}'),\n showCounter: __('Show Counter', '{{textDomain}}'),\n },\n });\n const { errorMessages, isValid } = useTypiaValidation(\n attributes,\n validate{{pascalCase}}Attributes,\n );\n const validateEditorUpdate = (nextAttributes: {{pascalCase}}Attributes) => {\n try {\n return {\n data: sanitize{{pascalCase}}Attributes(nextAttributes),\n errors: [],\n isValid: true as const,\n };\n } catch {\n return validate{{pascalCase}}Attributes(nextAttributes);\n }\n };\n const { updateField } = useTypedAttributeUpdater(\n attributes,\n setAttributes,\n validateEditorUpdate\n );\n const alignmentValue = editorFields.getStringValue(\n attributes,\n 'alignment',\n 'left'\n ) as NonNullable<{{pascalCase}}Attributes['alignment']>;\n const clickCount = attributes.clickCount ?? 0;\n const isVisible = editorFields.getBooleanValue(\n attributes,\n 'isVisible',\n true\n );\n const isAnimating = attributes.isAnimating ?? false;\n const maxClicks = attributes.maxClicks ?? 0;\n const showCounter = editorFields.getBooleanValue(\n attributes,\n 'showCounter',\n true\n );\n const interactiveMode = editorFields.getStringValue(\n attributes,\n 'interactiveMode',\n 'click'\n ) as NonNullable<{{pascalCase}}Attributes['interactiveMode']>;\n const animation = editorFields.getStringValue(\n attributes,\n 'animation',\n 'none'\n ) as NonNullable<{{pascalCase}}Attributes['animation']>;\n\n const blockProps = useBlockProps({\n className: `{{cssClassName}} {{cssClassName}}--${interactiveMode}`,\n 'data-wp-interactive': '{{slugKebabCase}}',\n 'data-wp-context': JSON.stringify({\n clicks: clickCount,\n isAnimating,\n isVisible,\n animation,\n maxClicks,\n })\n });\n const previewContentStyle = { textAlign: alignmentValue };\n const progressBarStyle = { width: `${(clickCount / maxClicks) * 100}%` };\n\n const resetCounter = () => {\n updateField('clickCount', 0);\n updateField('isAnimating', false);\n };\n\n const testAnimation = () => {\n updateField('isAnimating', true);\n setTimeout(() => {\n updateField('isAnimating', false);\n }, 1000);\n };\n\n return (\n <>\n <BlockControls>\n <AlignmentToolbar\n value={alignmentValue}\n onChange={(value) => updateField('alignment', (value || alignmentValue) as NonNullable<{{pascalCase}}Attributes['alignment']>)}\n />\n </BlockControls>\n\n <InspectorControls>\n <InspectorFromManifest\n attributes={attributes}\n fieldLookup={editorFields}\n onChange={updateField}\n paths={['alignment', 'interactiveMode', 'animation', 'showCounter', 'isVisible']}\n title={__('Interactive Settings', '{{textDomain}}')}\n />\n\n <PanelBody title={__('Counter Settings', '{{textDomain}}')}>\n <RangeControl\n label={__('Max Clicks', '{{textDomain}}')}\n value={maxClicks}\n onChange={(value) => updateField('maxClicks', value ?? maxClicks)}\n min={0}\n max={100}\n help={__('Set to 0 for unlimited clicks', '{{textDomain}}')}\n />\n\n <div style={actionButtonRowStyle}>\n <Button\n variant=\"secondary\"\n onClick={resetCounter}\n isSmall\n >\n {__('Reset Counter', '{{textDomain}}')}\n </Button>\n <Button\n variant=\"secondary\"\n onClick={testAnimation}\n isSmall\n >\n {__('Test Animation', '{{textDomain}}')}\n </Button>\n </div>\n </PanelBody>\n\n {!isValid && (\n <PanelBody title={__('Validation Errors', '{{textDomain}}')} initialOpen>\n {errorMessages.map((error, index) => (\n <Notice key={index} status=\"error\" isDismissible={false}>\n {error}\n </Notice>\n ))}\n </PanelBody>\n )}\n\n {isSelected && (\n <PanelBody title={__('Preview', '{{textDomain}}')}>\n <Button\n variant={isPreviewing ? 'primary' : 'secondary'}\n onClick={() => setIsPreviewing(!isPreviewing)}\n aria-pressed={isPreviewing}\n isSmall\n >\n {isPreviewing\n ? __('Disable Preview Mode', '{{textDomain}}')\n : __('Enable Preview Mode', '{{textDomain}}')}\n </Button>\n\n {clickCount > 0 && (\n <Notice status=\"info\" isDismissible={false}>\n {__('Current clicks:', '{{textDomain}}')} {clickCount}\n {maxClicks > 0 && ` / ${maxClicks}`}\n </Notice>\n )}\n </PanelBody>\n )}\n </InspectorControls>\n\n <div {...blockProps}>\n <div\n className={`{{cssClassName}}__content ${isAnimating ? 'is-animating' : ''}`}\n style={previewContentStyle}\n data-wp-on--click={isPreviewing ? 'actions.handleClick' : undefined}\n data-wp-on--mouseenter={isPreviewing && interactiveMode === 'hover' ? 'actions.handleMouseEnter' : undefined}\n data-wp-on--mouseleave={isPreviewing && interactiveMode === 'hover' ? 'actions.handleMouseLeave' : undefined}\n >\n <RichText\n tagName=\"p\"\n value={attributes.content}\n onChange={(value) => updateField('content', value)}\n placeholder={__( {{titleJson}} + ' \u2013 click me to interact!', '{{textDomain}}')}\n />\n\n {!isValid && (\n <Notice status=\"error\" isDismissible={false}>\n <p>\n <strong>{__('Validation Errors', '{{textDomain}}')}</strong>\n </p>\n <ul style={validationListStyle}>\n {errorMessages.map((error, index) => (\n <li key={index}>{error}</li>\n ))}\n </ul>\n </Notice>\n )}\n\n {showCounter && (\n <div className=\"{{cssClassName}}__counter\">\n <span className=\"{{cssClassName}}__counter-label\">\n {__('Clicks:', '{{textDomain}}')}\n </span>\n <span\n className=\"{{cssClassName}}__counter-value\"\n data-wp-text=\"state.clicks\"\n >\n {clickCount}\n </span>\n </div>\n )}\n\n {maxClicks > 0 && (\n <div className=\"{{cssClassName}}__progress\">\n <div\n className=\"{{cssClassName}}__progress-bar\"\n style={progressBarStyle}\n data-wp-style--width=\"state.progress + '%'\"\n />\n </div>\n )}\n\n {animation !== 'none' && (\n <div\n className={`{{cssClassName}}__animation ${isAnimating ? 'is-active' : ''}`}\n data-wp-class--is-active=\"state.isAnimating\"\n >\n {animation}\n </div>\n )}\n </div>\n </div>\n </>\n );\n}\n";
2
+ export declare const INTERACTIVITY_SAVE_TEMPLATE = "import { useBlockProps, RichText } from '@wordpress/block-editor';\nimport { __ } from '@wordpress/i18n';\nimport type { {{pascalCase}}Attributes } from './types';\n\nexport default function Save({ attributes }: { attributes: {{pascalCase}}Attributes }) {\n const clickCount = attributes.clickCount ?? 0;\n const interactiveMode = attributes.interactiveMode ?? 'click';\n const animation = attributes.animation ?? 'none';\n const isAnimating = attributes.isAnimating ?? false;\n const isVisible = attributes.isVisible ?? true;\n const maxClicks = attributes.maxClicks ?? 0;\n const showCounter = attributes.showCounter ?? true;\n const contentStyle = { textAlign: attributes.alignment };\n const blockProps = useBlockProps.save({\n className: `{{cssClassName}} {{cssClassName}}--${interactiveMode}`,\n 'data-wp-interactive': '{{slugKebabCase}}',\n 'data-wp-context': JSON.stringify({\n clicks: clickCount,\n isAnimating,\n isVisible,\n animation,\n maxClicks,\n })\n });\n\n return (\n <div {...blockProps}>\n <div\n className={`{{cssClassName}}__content ${isAnimating ? 'is-animating' : ''}`}\n style={contentStyle}\n data-wp-on--click=\"actions.handleClick\"\n data-wp-on--mouseenter={interactiveMode === 'hover' ? 'actions.handleMouseEnter' : undefined}\n data-wp-on--mouseleave={interactiveMode === 'hover' ? 'actions.handleMouseLeave' : undefined}\n data-wp-bind--hidden=\"!state.isVisible\"\n >\n <RichText.Content\n tagName=\"p\"\n value={attributes.content}\n className=\"{{cssClassName}}__text\"\n />\n\n {showCounter && (\n <div\n className=\"{{cssClassName}}__counter\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n >\n <span className=\"{{cssClassName}}__counter-label\">\n { __( 'Clicks:', '{{textDomain}}' ) }\n </span>\n <span\n className=\"{{cssClassName}}__counter-value\"\n data-wp-text=\"state.clicks\"\n >\n {clickCount}\n </span>\n </div>\n )}\n\n {maxClicks > 0 && (\n <div className=\"{{cssClassName}}__progress\">\n <div\n className=\"{{cssClassName}}__progress-bar\"\n role=\"progressbar\"\n aria-label={ __( 'Click progress', '{{textDomain}}' ) }\n aria-valuemin={0}\n aria-valuemax={maxClicks}\n aria-valuenow={Math.min(clickCount, maxClicks)}\n data-wp-bind--aria-valuenow=\"state.clampedClicks\"\n data-wp-style--width=\"state.progress + '%'\"\n />\n </div>\n )}\n\n <div\n className={`{{cssClassName}}__animation ${animation}`}\n aria-hidden=\"true\"\n data-wp-class--is-active=\"state.isAnimating\"\n />\n\n {maxClicks > 0 && (\n <div\n className=\"{{cssClassName}}__completion\"\n role=\"status\"\n aria-live=\"polite\"\n aria-atomic=\"true\"\n data-wp-bind--hidden=\"!state.isComplete\"\n >\n { __( '\uD83C\uDF89 Complete!', '{{textDomain}}' ) }\n </div>\n )}\n\n <button\n className=\"{{cssClassName}}__reset\"\n data-wp-on--click=\"actions.reset\"\n aria-label={ __( 'Reset counter', '{{textDomain}}' ) }\n >\n <span aria-hidden=\"true\">\u21BB</span>\n <span className=\"screen-reader-text\">\n { __( 'Reset counter', '{{textDomain}}' ) }\n </span>\n </button>\n </div>\n </div>\n );\n}\n";
3
+ export declare const INTERACTIVITY_INDEX_TEMPLATE = "import {\n registerScaffoldBlockType,\n type BlockConfiguration,\n} from '@wp-typia/block-types/blocks/registration';\nimport type { BlockSupports } from '@wp-typia/block-types/blocks/supports';\nimport {\n buildScaffoldBlockRegistration,\n parseScaffoldBlockMetadata,\n} from '@wp-typia/block-runtime/blocks';\n\nimport Edit from './edit';\nimport Save from './save';\nimport metadata from './block-metadata';\nimport './editor.scss';\nimport './style.scss';\n\nimport type { {{pascalCase}}Attributes } from './types';\n\nconst scaffoldSupports = {\n html: false,\n align: true,\n anchor: true,\n className: true,\n interactivity: true,\n} satisfies BlockSupports;\n\nconst registration = buildScaffoldBlockRegistration(\n parseScaffoldBlockMetadata<BlockConfiguration<{{pascalCase}}Attributes>>(metadata),\n {\n supports: scaffoldSupports,\n edit: Edit,\n save: Save,\n }\n);\n\nregisterScaffoldBlockType(registration.name, registration.settings);\n";
4
+ export declare const INTERACTIVITY_SCRIPT_TEMPLATE = "/**\n * WordPress Interactivity API implementation for {{title}} block\n */\nimport { store, getContext, getElement, withSyncEvent } from '@wordpress/interactivity';\nimport type { {{pascalCase}}Context } from './types';\n\nfunction getBlockContext() {\n return getContext<{{pascalCase}}Context>();\n}\n\n// Store configuration\nstore('{{slugKebabCase}}', {\n // State - reactive data that updates the UI\n state: {\n get clicks() {\n return getBlockContext().clicks;\n },\n get isAnimating() {\n return getBlockContext().isAnimating;\n },\n get isVisible() {\n return getBlockContext().isVisible;\n },\n get progress() {\n const context = getBlockContext();\n const clampedClicks =\n context.maxClicks > 0\n ? Math.min(context.clicks, context.maxClicks)\n : context.clicks;\n return context.maxClicks > 0 ? (clampedClicks / context.maxClicks) * 100 : 0;\n },\n get clampedClicks() {\n const context = getBlockContext();\n return context.maxClicks > 0\n ? Math.min(context.clicks, context.maxClicks)\n : context.clicks;\n },\n get isComplete() {\n const context = getBlockContext();\n return context.clicks >= context.maxClicks && context.maxClicks > 0;\n }\n },\n\n // Actions - user interactions\n actions: {\n // Handle block click\n handleClick: () => {\n const context = getBlockContext();\n const { ref } = getElement();\n\n if (!ref) {\n return;\n }\n\n if (context.maxClicks > 0 && context.clicks >= context.maxClicks) {\n return;\n }\n\n const previousClicks = context.clicks;\n\n // Increment click counter\n context.clicks += 1;\n\n // Trigger animation\n if (context.animation !== 'none') {\n context.isAnimating = true;\n setTimeout(() => {\n context.isAnimating = false;\n }, 1000);\n }\n\n // Emit custom event\n ref.dispatchEvent(new CustomEvent('{{slugKebabCase}}:click', {\n detail: { clicks: context.clicks }\n }));\n\n // Check if max clicks reached\n if (context.maxClicks > 0 && previousClicks < context.maxClicks && context.clicks === context.maxClicks) {\n ref.dispatchEvent(new CustomEvent('{{slugKebabCase}}:complete', {\n detail: { totalClicks: context.clicks }\n }));\n }\n },\n\n // Handle hover events\n handleMouseEnter: () => {\n const context = getBlockContext();\n if (context.animation === 'none') return;\n context.isAnimating = true;\n },\n\n handleMouseLeave: () => {\n const context = getBlockContext();\n if (context.animation === 'none') return;\n context.isAnimating = false;\n },\n\n // Reset counter\n reset: withSyncEvent((event: Event) => {\n event.stopPropagation();\n const context = getBlockContext();\n context.clicks = 0;\n context.isAnimating = false;\n })\n }\n});\n";
5
+ export declare const INTERACTIVITY_VALIDATORS_TEMPLATE = "import typia from 'typia';\nimport currentManifest from \"./manifest-defaults-document\";\nimport { {{pascalCase}}Attributes, {{pascalCase}}ValidationResult } from \"./types\";\nimport { createTemplateValidatorToolkit } from \"./validator-toolkit\";\n\nconst scaffoldValidators = createTemplateValidatorToolkit<{{pascalCase}}Attributes>({\n assert: typia.createAssert<{{pascalCase}}Attributes>(),\n clone: typia.misc.createClone<{{pascalCase}}Attributes>() as (\n value: {{pascalCase}}Attributes,\n ) => {{pascalCase}}Attributes,\n is: typia.createIs<{{pascalCase}}Attributes>(),\n manifest: currentManifest,\n prune: typia.misc.createPrune<{{pascalCase}}Attributes>(),\n random: typia.createRandom<{{pascalCase}}Attributes>() as (\n ...args: unknown[]\n ) => {{pascalCase}}Attributes,\n validate: typia.createValidate<{{pascalCase}}Attributes>(),\n});\n\nexport const validate{{pascalCase}}Attributes =\n scaffoldValidators.validateAttributes as (\n attributes: unknown,\n ) => {{pascalCase}}ValidationResult;\n\nexport const validators = scaffoldValidators.validators;\n\nexport const sanitize{{pascalCase}}Attributes =\n scaffoldValidators.sanitizeAttributes as (\n attributes: Partial<{{pascalCase}}Attributes>,\n ) => {{pascalCase}}Attributes;\n\n/**\n * Runtime type guard for checking if an object is {{pascalCase}}Attributes.\n */\nexport const is{{pascalCase}}Attributes = (obj: unknown): obj is {{pascalCase}}Attributes => {\n return validators.is(obj);\n};\n\nexport const createAttributeUpdater = scaffoldValidators.createAttributeUpdater;\n";