@salsita-ai/sdk 0.50.0-develop.9ed52379e9 → 0.50.0-develop.9fe72d003e
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 +20 -15
- package/dist/admin/default-admin-elements.d.ts +9 -0
- package/dist/admin/default-admin-elements.js +1 -0
- package/dist/admin/default-elements/built-in-entity-element.d.ts +10 -0
- package/dist/admin/default-elements/built-in-entity-element.js +1 -0
- package/dist/admin/default-elements/built-in-layout-element.d.ts +10 -0
- package/dist/admin/default-elements/built-in-layout-element.js +1 -0
- package/dist/admin/default-elements/index.d.ts +2 -0
- package/dist/admin/default-elements/index.js +1 -0
- package/dist/admin/index.d.ts +1 -0
- package/dist/admin/index.js +1 -1
- package/dist/application/index.d.ts +1 -0
- package/dist/application/index.js +1 -1
- package/dist/application/mount-application.d.ts +1 -1
- package/dist/application/mount-application.js +1 -1
- package/dist/application/mount-configurator.d.ts +19 -0
- package/dist/application/mount-configurator.js +1 -0
- package/dist/application/mounted-application-runtime.d.ts +4 -0
- package/dist/application/mounted-application-runtime.js +1 -0
- package/dist/application/register-web-component.d.ts +2 -0
- package/dist/application/register-web-component.js +1 -0
- package/dist/application-spec/caching/content-diff.js +1 -1
- package/dist/application-spec/core/application-file.d.ts +0 -2
- package/dist/application-spec/core/application-file.js +1 -1
- package/dist/application-spec/core/enhanced-json-value.d.ts +2 -99
- package/dist/application-spec/core/enhanced-json-value.js +1 -1
- package/dist/application-spec/core/index.d.ts +1 -1
- package/dist/application-spec/core/index.js +1 -1
- package/dist/application-spec/core/json-schema.d.ts +2 -52
- package/dist/application-spec/core/json-value.d.ts +1 -1
- package/dist/application-spec/core/markers/index.d.ts +1 -1
- package/dist/application-spec/core/markers/index.js +1 -1
- package/dist/application-spec/core/markers/msg-marker.d.ts +2 -28
- package/dist/application-spec/core/markers/msg-marker.js +1 -1
- package/dist/application-spec/core/markers/raw-eval-write-marker.d.ts +1 -1
- package/dist/application-spec/core/markers/raw-eval-write-marker.js +1 -1
- package/dist/application-spec/core/markers/raw-read-write-marker.js +1 -1
- package/dist/application-spec/{core → enhanced-json}/eval-to-constant.d.ts +1 -1
- package/dist/application-spec/enhanced-json/index.d.ts +4 -0
- package/dist/application-spec/enhanced-json/index.js +1 -0
- package/dist/application-spec/enhanced-json/is-enhanced-json-array.d.ts +2 -0
- package/dist/application-spec/enhanced-json/is-enhanced-json-array.js +1 -0
- package/dist/application-spec/enhanced-json/resolve-enhanced-json-value.d.ts +18 -0
- package/dist/application-spec/enhanced-json/resolve-enhanced-json-value.js +1 -0
- package/dist/application-spec/enhanced-json/schema-from-value.d.ts +24 -0
- package/dist/application-spec/enhanced-json/schema-from-value.js +1 -0
- package/dist/application-spec/enhanced-json/stringify-enhanced-json-value.d.ts +14 -0
- package/dist/application-spec/enhanced-json/stringify-enhanced-json-value.js +1 -0
- package/dist/application-spec/enhanced-json/validate-enhanced-json-value-caps.d.ts +12 -0
- package/dist/application-spec/enhanced-json/validate-enhanced-json-value-caps.js +1 -0
- package/dist/application-spec/enhanced-json/validate-no-legacy-code-ref.d.ts +13 -0
- package/dist/application-spec/enhanced-json/validate-no-legacy-code-ref.js +1 -0
- package/dist/application-spec/errors/diagnostic-codes.d.ts +31 -1
- package/dist/application-spec/errors/diagnostic-codes.js +1 -1
- package/dist/application-spec/errors/diagnostic-messages.d.ts +42 -2
- package/dist/application-spec/errors/diagnostic-messages.js +1 -1
- package/dist/application-spec/generate-application/code/common/code-imports.js +1 -1
- package/dist/application-spec/generate-application/code/common/env-reader.js +2 -2
- package/dist/application-spec/generate-application/code/common/index.d.ts +1 -0
- package/dist/application-spec/generate-application/code/common/index.js +1 -1
- package/dist/application-spec/generate-application/code/common/single-quoted-string.d.ts +6 -0
- package/dist/application-spec/generate-application/code/common/single-quoted-string.js +1 -0
- package/dist/application-spec/generate-application/code/domain-runtime-files.js +1 -1
- package/dist/application-spec/generate-application/code/generate-application-code.d.ts +3 -2
- package/dist/application-spec/generate-application/code/generate-application-code.js +1 -1
- package/dist/application-spec/generate-application/code/project-scaffolding.d.ts +22 -14
- package/dist/application-spec/generate-application/code/project-scaffolding.js +2 -2
- package/dist/application-spec/generate-application/code/standalone-app-files.d.ts +2 -2
- package/dist/application-spec/generate-application/code/standalone-app-files.js +1 -1
- package/dist/application-spec/generate-application/code/story-files.js +1 -1
- package/dist/application-spec/generate-application/code/templates/admin-config.js +3 -0
- package/dist/application-spec/generate-application/code/templates/application-types.js +6 -6
- package/dist/application-spec/generate-application/code/templates/common/doc-comment.d.ts +2 -0
- package/dist/application-spec/generate-application/code/templates/common/doc-comment.js +1 -0
- package/dist/application-spec/generate-application/code/templates/common/format-property-key.d.ts +7 -0
- package/dist/application-spec/generate-application/code/templates/common/format-property-key.js +1 -0
- package/dist/application-spec/generate-application/code/templates/common/schema-to-type-string.js +3 -3
- package/dist/application-spec/generate-application/code/templates/configurator-config.js +6 -4
- package/dist/application-spec/generate-application/code/templates/configurator-globals.d.ts +2 -1
- package/dist/application-spec/generate-application/code/templates/configurator-globals.js +19 -6
- package/dist/application-spec/generate-application/code/templates/domain-spec/factory-types/domain-factory-types.d.ts +35 -0
- package/dist/application-spec/generate-application/code/templates/domain-spec/factory-types/domain-factory-types.js +5 -0
- package/dist/application-spec/generate-application/code/templates/domain-spec/factory-types/index.d.ts +1 -0
- package/dist/application-spec/generate-application/code/templates/domain-spec/factory-types/index.js +1 -0
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-component-optics.d.ts +1 -1
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-component-optics.js +7 -7
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-defaults.d.ts +4 -2
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-defaults.js +1 -1
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-domain-block.js +69 -73
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-invariants.d.ts +8 -0
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-invariants.js +1 -1
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-phx-globals.js +3 -0
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-presets.js +1 -1
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-relation-expressions.js +1 -1
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-resources.d.ts +6 -8
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-resources.js +21 -21
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-states.d.ts +1 -1
- package/dist/application-spec/generate-application/code/templates/domain-spec/generate-states.js +2 -2
- package/dist/application-spec/generate-application/code/templates/domain-spec/index.d.ts +3 -2
- package/dist/application-spec/generate-application/code/templates/domain-spec/index.js +1 -1
- package/dist/application-spec/generate-application/code/templates/domain-spec.js +12 -12
- package/dist/application-spec/generate-application/code/templates/domain.js +3 -3
- package/dist/application-spec/generate-application/code/templates/eslint-config.d.ts +5 -0
- package/dist/application-spec/generate-application/code/templates/eslint-config.js +4 -5
- package/dist/application-spec/generate-application/code/templates/frontend-config.js +3 -5
- package/dist/application-spec/generate-application/code/templates/gitignore.js +0 -3
- package/dist/application-spec/generate-application/code/templates/global-definition/generate-action-types.js +7 -7
- package/dist/application-spec/generate-application/code/templates/global-definition/generate-assistant-form-element-types.d.ts +12 -0
- package/dist/application-spec/generate-application/code/templates/global-definition/generate-assistant-form-element-types.js +7 -0
- package/dist/application-spec/generate-application/code/templates/global-definition/generate-phx-types.d.ts +43 -0
- package/dist/application-spec/generate-application/code/templates/global-definition/generate-phx-types.js +33 -0
- package/dist/application-spec/generate-application/code/templates/global-definition/index.d.ts +2 -0
- package/dist/application-spec/generate-application/code/templates/global-definition/index.js +1 -1
- package/dist/application-spec/generate-application/code/templates/globals.js +24 -43
- package/dist/application-spec/generate-application/code/templates/index.d.ts +5 -10
- package/dist/application-spec/generate-application/code/templates/index.js +1 -1
- package/dist/application-spec/generate-application/code/templates/lingui-rc.d.ts +5 -0
- package/dist/application-spec/generate-application/code/templates/lingui-rc.js +4 -4
- package/dist/application-spec/generate-application/code/templates/prettier-config.d.ts +5 -0
- package/dist/application-spec/generate-application/code/templates/prettier-config.js +4 -5
- package/dist/application-spec/generate-application/code/templates/root-tsconfig.d.ts +6 -0
- package/dist/application-spec/generate-application/code/templates/root-tsconfig.js +2 -0
- package/dist/application-spec/generate-application/code/templates/skills/phoenix-cli-skill.js +1 -1
- package/dist/application-spec/generate-application/code/templates/skills/phoenix-create-configurator-skill.js +1 -1
- package/dist/application-spec/generate-application/code/templates/skills/phoenix-feedback-skill.js +5 -5
- package/dist/application-spec/generate-application/code/templates/skills/phoenix-review-skill.js +9 -3
- package/dist/application-spec/generate-application/code/templates/skills/phoenix-skill-bundle.d.ts +10 -0
- package/dist/application-spec/generate-application/code/templates/skills/phoenix-skill-bundle.js +7 -0
- package/dist/application-spec/generate-application/code/templates/skills/phoenix-skill.js +1 -1
- package/dist/application-spec/generate-application/code/templates/skills/phoenix-variants-skill.js +2 -2
- package/dist/application-spec/generate-application/code/templates/skills/sdk-content.d.ts +33 -7
- package/dist/application-spec/generate-application/code/templates/skills/sdk-content.js +2 -2
- package/dist/application-spec/generate-application/code/templates/spec-code/collect-spec-code.d.ts +58 -0
- package/dist/application-spec/generate-application/code/templates/spec-code/collect-spec-code.js +1 -0
- package/dist/application-spec/generate-application/code/templates/spec-code/index.d.ts +2 -0
- package/dist/application-spec/generate-application/code/templates/spec-code/index.js +1 -0
- package/dist/application-spec/generate-application/code/templates/spec-code/spec-code.d.ts +18 -0
- package/dist/application-spec/generate-application/code/templates/spec-code/spec-code.js +41 -0
- package/dist/application-spec/generate-application/code/templates/spec-messages/index.d.ts +1 -0
- package/dist/application-spec/generate-application/code/templates/spec-messages/index.js +1 -0
- package/dist/application-spec/generate-application/code/templates/spec-messages/spec-messages.d.ts +20 -0
- package/dist/application-spec/generate-application/code/templates/spec-messages/spec-messages.js +6 -0
- package/dist/application-spec/generate-application/code/templates/story/phx-module.d.ts +8 -2
- package/dist/application-spec/generate-application/code/templates/story/phx-module.js +15 -3
- package/dist/application-spec/generate-application/code/templates/story/wrapper.d.ts +3 -0
- package/dist/application-spec/generate-application/code/templates/story/wrapper.js +2 -1
- package/dist/application-spec/generate-application/code/templates/tsconfig.d.ts +5 -0
- package/dist/application-spec/generate-application/code/templates/tsconfig.js +1 -1
- package/dist/application-spec/generate-application/code/templates/webhook-index.d.ts +6 -1
- package/dist/application-spec/generate-application/code/templates/webhook-index.js +10 -1
- package/dist/application-spec/generate-application/code/templates/webhooks-config.js +2 -3
- package/dist/application-spec/generate-application/implementations/phx-application-types.d.ts +25 -0
- package/dist/application-spec/generate-application/implementations/phx-application-types.js +1 -0
- package/dist/application-spec/generate-application/implementations/phx-domain.d.ts +36 -0
- package/dist/application-spec/generate-application/implementations/phx-domain.js +1 -0
- package/dist/application-spec/generate-application/implementations/phx-model.d.ts +24 -0
- package/dist/application-spec/generate-application/implementations/phx-model.js +1 -0
- package/dist/application-spec/generate-application/implementations/phx.d.ts +22 -0
- package/dist/application-spec/generate-application/implementations/phx.js +1 -0
- package/dist/application-spec/generate-application/index.d.ts +4 -0
- package/dist/application-spec/generate-application/index.js +1 -1
- package/dist/application-spec/generate-application/runtime/admin-runtime/admin-model-runtime.d.ts +6 -5
- package/dist/application-spec/generate-application/runtime/admin-runtime/admin-model-runtime.js +1 -1
- package/dist/application-spec/generate-application/runtime/admin-runtime/admin-runtime.d.ts +12 -4
- package/dist/application-spec/generate-application/runtime/admin-runtime/admin-runtime.js +1 -1
- package/dist/application-spec/generate-application/runtime/admin-runtime/admin-spec-runtime.d.ts +16 -0
- package/dist/application-spec/generate-application/runtime/admin-runtime/admin-spec-runtime.js +1 -0
- package/dist/application-spec/generate-application/runtime/admin-runtime/states-runtime.d.ts +2 -2
- package/dist/application-spec/generate-application/runtime/application-runtime.d.ts +35 -10
- package/dist/application-spec/generate-application/runtime/application-runtime.js +1 -1
- package/dist/application-spec/generate-application/runtime/application-spec-slice.d.ts +71 -0
- package/dist/application-spec/generate-application/runtime/application-spec-slice.js +1 -0
- package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-model-runtime.d.ts +7 -6
- package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-model-runtime.js +1 -1
- package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-runtime.d.ts +12 -7
- package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-runtime.js +1 -1
- package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-spec-runtime.d.ts +16 -0
- package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-spec-runtime.js +1 -0
- package/dist/application-spec/generate-application/runtime/configurator-runtime/states-runtime.d.ts +2 -2
- package/dist/application-spec/generate-application/runtime/create-application.d.ts +57 -0
- package/dist/application-spec/generate-application/runtime/create-application.js +1 -0
- package/dist/application-spec/generate-application/runtime/factory-data/factory-data-runtime.d.ts +42 -0
- package/dist/application-spec/generate-application/runtime/factory-data/factory-data-runtime.js +1 -0
- package/dist/application-spec/generate-application/runtime/factory-data/index.d.ts +3 -0
- package/dist/application-spec/generate-application/runtime/factory-data/index.js +1 -0
- package/dist/application-spec/generate-application/runtime/factory-data/recipe-defaults.d.ts +13 -0
- package/dist/application-spec/generate-application/runtime/factory-data/recipe-defaults.js +1 -0
- package/dist/application-spec/generate-application/runtime/factory-data/static-violations.d.ts +15 -0
- package/dist/application-spec/generate-application/runtime/factory-data/static-violations.js +1 -0
- package/dist/application-spec/generate-application/runtime/generate-runtime-instance.d.ts +1 -1
- package/dist/application-spec/generate-application/runtime/generate-runtime-instance.js +1 -1
- package/dist/application-spec/generate-application/runtime/index.d.ts +8 -0
- package/dist/application-spec/generate-application/runtime/index.js +1 -0
- package/dist/application-spec/generate-application/runtime/migrations/index.d.ts +1 -0
- package/dist/application-spec/generate-application/runtime/migrations/index.js +1 -0
- package/dist/application-spec/generate-application/runtime/migrations/migration-runtime.d.ts +53 -0
- package/dist/application-spec/generate-application/runtime/migrations/migration-runtime.js +1 -0
- package/dist/application-spec/generate-application/runtime/sections/actions-runtime.d.ts +1 -2
- package/dist/application-spec/generate-application/runtime/sections/assistant-runtime.d.ts +2 -2
- package/dist/application-spec/generate-application/runtime/sections/build-sdk-optic.d.ts +3 -2
- package/dist/application-spec/generate-application/runtime/sections/build-sdk-optic.js +1 -1
- package/dist/application-spec/generate-application/runtime/sections/build-user-bound-optic.d.ts +3 -2
- package/dist/application-spec/generate-application/runtime/sections/build-user-bound-optic.js +1 -1
- package/dist/application-spec/generate-application/runtime/sections/component-optic.d.ts +6 -3
- package/dist/application-spec/generate-application/runtime/sections/component-optic.js +1 -1
- package/dist/application-spec/generate-application/runtime/sections/data-runtime.d.ts +4 -4
- package/dist/application-spec/generate-application/runtime/sections/expression-runtime.d.ts +14 -6
- package/dist/application-spec/generate-application/runtime/sections/expression-runtime.js +1 -1
- package/dist/application-spec/generate-application/runtime/sections/generate-memo-eval-plan-runtime.d.ts +10 -7
- package/dist/application-spec/generate-application/runtime/sections/generate-memo-eval-plan-runtime.js +1 -1
- package/dist/application-spec/generate-application/runtime/sections/preset-runtime.d.ts +2 -3
- package/dist/application-spec/generate-application/runtime/sections/reflection-constants-runtime.d.ts +4 -2
- package/dist/application-spec/generate-application/runtime/sections/reflection-constants-runtime.js +1 -1
- package/dist/application-spec/generate-application/runtime/sections/theme-runtime.d.ts +2 -2
- package/dist/application-spec/generate-application/runtime/sections/transformers-runtime.d.ts +4 -6
- package/dist/application-spec/generate-application/runtime/spec-code/index.d.ts +2 -0
- package/dist/application-spec/generate-application/runtime/spec-code/index.js +1 -0
- package/dist/application-spec/generate-application/runtime/spec-code/resolve-spec-code.d.ts +18 -0
- package/dist/application-spec/generate-application/runtime/spec-code/resolve-spec-code.js +1 -0
- package/dist/application-spec/generate-application/runtime/spec-code/spec-code-module.d.ts +17 -0
- package/dist/application-spec/generate-application/runtime/spec-runtime.d.ts +21 -0
- package/dist/application-spec/generate-application/runtime/spec-runtime.js +1 -0
- package/dist/application-spec/generate-application/runtime-spec/decode-runtime-spec.d.ts +9 -0
- package/dist/application-spec/generate-application/runtime-spec/decode-runtime-spec.js +1 -0
- package/dist/application-spec/generate-application/runtime-spec/encode-runtime-spec.d.ts +8 -0
- package/dist/application-spec/generate-application/runtime-spec/encode-runtime-spec.js +1 -0
- package/dist/application-spec/generate-application/runtime-spec/encoded-runtime-spec.d.ts +28 -0
- package/dist/application-spec/generate-application/runtime-spec/encoded-runtime-spec.js +1 -0
- package/dist/application-spec/generate-application/runtime-spec/index.d.ts +5 -0
- package/dist/application-spec/generate-application/runtime-spec/index.js +1 -0
- package/dist/application-spec/generate-application/runtime-spec/runtime-spec.d.ts +168 -0
- package/dist/application-spec/generate-application/runtime-spec/runtime-spec.js +1 -0
- package/dist/application-spec/generate-application/runtime-spec/spec-code-id.d.ts +40 -0
- package/dist/application-spec/generate-application/runtime-spec/spec-code-id.js +1 -0
- package/dist/application-spec/index.d.ts +1 -0
- package/dist/application-spec/index.js +1 -1
- package/dist/application-spec/parse-application-files/categorize/categorize-files.d.ts +34 -0
- package/dist/application-spec/parse-application-files/categorize/categorize-files.js +1 -1
- package/dist/application-spec/parse-application-files/categorize/earlier-generated-editor-configs.d.ts +16 -0
- package/dist/application-spec/parse-application-files/categorize/earlier-generated-editor-configs.js +9 -0
- package/dist/application-spec/parse-application-files/categorize/earlier-generated-tsconfig.d.ts +9 -0
- package/dist/application-spec/parse-application-files/categorize/earlier-generated-tsconfig.js +1 -0
- package/dist/application-spec/parse-application-files/categorize/module-analysis.d.ts +1 -1
- package/dist/application-spec/parse-application-files/categorize/module-analysis.js +1 -1
- package/dist/application-spec/parse-application-files/merge/merged-assistant.d.ts +13 -4
- package/dist/application-spec/parse-application-files/merge/merged-assistant.js +1 -1
- package/dist/application-spec/parse-application-files/merge/merged-infra.d.ts +4 -11
- package/dist/application-spec/parse-application-files/merge/merged-transformers.d.ts +2 -4
- package/dist/application-spec/parse-application-files/parse-application-files.js +1 -1
- package/dist/application-spec/parse-application-files/raw-spec/parse-yaml-spec-file.js +1 -1
- package/dist/application-spec/parse-application-files/raw-spec/raw-acl.d.ts +2 -2
- package/dist/application-spec/parse-application-files/raw-spec/raw-acl.js +1 -1
- package/dist/application-spec/parse-application-files/raw-spec/raw-admin-pages.d.ts +3 -24
- package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.d.ts +72 -24
- package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.js +1 -1
- package/dist/application-spec/parse-application-files/raw-spec/raw-expressions.d.ts +1 -1
- package/dist/application-spec/parse-application-files/raw-spec/raw-expressions.js +1 -1
- package/dist/application-spec/parse-application-files/raw-spec/raw-resources.d.ts +19 -64
- package/dist/application-spec/parse-application-files/raw-spec/raw-spec-file.js +1 -1
- package/dist/application-spec/parse-application-files/spec/application/admin-spec/admin-pages.d.ts +3 -20
- package/dist/application-spec/parse-application-files/spec/application/admin-spec/admin-spec.js +1 -1
- package/dist/application-spec/parse-application-files/spec/application/admin-spec/states.d.ts +3 -5
- package/dist/application-spec/parse-application-files/spec/application/configurator-spec/configurator-spec.js +1 -1
- package/dist/application-spec/parse-application-files/spec/application/configurator-spec/states.d.ts +3 -8
- package/dist/application-spec/parse-application-files/spec/common/index.d.ts +1 -0
- package/dist/application-spec/parse-application-files/spec/common/index.js +1 -1
- package/dist/application-spec/parse-application-files/spec/common/value-type/format-value-type.d.ts +3 -0
- package/dist/application-spec/parse-application-files/spec/common/value-type/format-value-type.js +1 -0
- package/dist/application-spec/parse-application-files/spec/common/value-type/index.d.ts +5 -0
- package/dist/application-spec/parse-application-files/spec/common/value-type/index.js +1 -0
- package/dist/application-spec/parse-application-files/spec/common/value-type/intersect-value-types.d.ts +6 -0
- package/dist/application-spec/parse-application-files/spec/common/value-type/intersect-value-types.js +1 -0
- package/dist/application-spec/parse-application-files/spec/common/value-type/is-assignable-value-type.d.ts +8 -0
- package/dist/application-spec/parse-application-files/spec/common/value-type/is-assignable-value-type.js +1 -0
- package/dist/application-spec/parse-application-files/spec/common/value-type/value-type-at.d.ts +10 -0
- package/dist/application-spec/parse-application-files/spec/common/value-type/value-type-at.js +1 -0
- package/dist/application-spec/parse-application-files/spec/common/value-type/value-type.d.ts +98 -0
- package/dist/application-spec/parse-application-files/spec/common/value-type/value-type.js +1 -0
- package/dist/application-spec/parse-application-files/spec/components/bindings/component-path.d.ts +13 -0
- package/dist/application-spec/parse-application-files/spec/components/bindings/component-path.js +1 -0
- package/dist/application-spec/parse-application-files/spec/components/bindings/index.d.ts +2 -0
- package/dist/application-spec/parse-application-files/spec/components/bindings/index.js +1 -1
- package/dist/application-spec/parse-application-files/spec/components/bindings/read-binding-type.d.ts +24 -0
- package/dist/application-spec/parse-application-files/spec/components/bindings/read-binding-type.js +1 -0
- package/dist/application-spec/parse-application-files/spec/components/bindings/read-binding.d.ts +3 -13
- package/dist/application-spec/parse-application-files/spec/components/bindings/write-binding.d.ts +4 -32
- package/dist/application-spec/parse-application-files/spec/components/component-filter.d.ts +3 -62
- package/dist/application-spec/parse-application-files/spec/components/component-filter.js +1 -1
- package/dist/application-spec/parse-application-files/spec/components/component-state-type.d.ts +8 -0
- package/dist/application-spec/parse-application-files/spec/components/component-state-type.js +1 -0
- package/dist/application-spec/parse-application-files/spec/components/components.d.ts +3 -4
- package/dist/application-spec/parse-application-files/spec/components/filter-criterion-types.d.ts +13 -0
- package/dist/application-spec/parse-application-files/spec/components/filter-criterion-types.js +1 -0
- package/dist/application-spec/parse-application-files/spec/components/index.d.ts +1 -2
- package/dist/application-spec/parse-application-files/spec/components/index.js +1 -1
- package/dist/application-spec/parse-application-files/spec/components/sdk-component-entry.d.ts +1 -1
- package/dist/application-spec/parse-application-files/spec/components/static-entity-id.d.ts +14 -0
- package/dist/application-spec/parse-application-files/spec/components/static-entity-id.js +1 -0
- package/dist/application-spec/parse-application-files/spec/components/user-component-entry.d.ts +8 -3
- package/dist/application-spec/parse-application-files/spec/components/user-component-entry.js +1 -1
- package/dist/application-spec/parse-application-files/spec/components/user-components.d.ts +9 -7
- package/dist/application-spec/parse-application-files/spec/components/user-components.js +1 -1
- package/dist/application-spec/parse-application-files/spec/expressions/expression-read-types.d.ts +24 -0
- package/dist/application-spec/parse-application-files/spec/expressions/expression-read-types.js +1 -0
- package/dist/application-spec/parse-application-files/spec/expressions/expression-read.d.ts +13 -0
- package/dist/application-spec/parse-application-files/spec/expressions/expression-read.js +1 -0
- package/dist/application-spec/parse-application-files/spec/expressions/expression-value-type.d.ts +9 -0
- package/dist/application-spec/parse-application-files/spec/expressions/expression-value-type.js +1 -0
- package/dist/application-spec/parse-application-files/spec/expressions/expressions.d.ts +12 -30
- package/dist/application-spec/parse-application-files/spec/expressions/expressions.js +1 -1
- package/dist/application-spec/parse-application-files/spec/expressions/index.d.ts +0 -2
- package/dist/application-spec/parse-application-files/spec/expressions/index.js +1 -1
- package/dist/application-spec/parse-application-files/spec/expressions/memo-eval-plan.d.ts +4 -71
- package/dist/application-spec/parse-application-files/spec/expressions/memo-eval-plan.js +1 -1
- package/dist/application-spec/parse-application-files/spec/expressions/user-expressions.d.ts +5 -3
- package/dist/application-spec/parse-application-files/spec/expressions/user-expressions.js +1 -1
- package/dist/application-spec/parse-application-files/spec/invariants/invariant.d.ts +5 -21
- package/dist/application-spec/parse-application-files/spec/invariants/invariants.d.ts +6 -16
- package/dist/application-spec/parse-application-files/spec/invariants/invariants.js +1 -1
- package/dist/application-spec/parse-application-files/spec/model/aliases.d.ts +3 -9
- package/dist/application-spec/parse-application-files/spec/model/common-types.d.ts +6 -14
- package/dist/application-spec/parse-application-files/spec/model/defaults.d.ts +6 -26
- package/dist/application-spec/parse-application-files/spec/model/defaults.js +1 -1
- package/dist/application-spec/parse-application-files/spec/model/entity-types.d.ts +3 -8
- package/dist/application-spec/parse-application-files/spec/model/index.d.ts +1 -0
- package/dist/application-spec/parse-application-files/spec/model/index.js +1 -1
- package/dist/application-spec/parse-application-files/spec/model/model-type-resolution.d.ts +10 -0
- package/dist/application-spec/parse-application-files/spec/model/model-type-resolution.js +1 -0
- package/dist/application-spec/parse-application-files/spec/model/model-version-key.d.ts +3 -3
- package/dist/application-spec/parse-application-files/spec/model/names/action-name.d.ts +3 -7
- package/dist/application-spec/parse-application-files/spec/model/names/alias-name.d.ts +3 -7
- package/dist/application-spec/parse-application-files/spec/model/names/entity-or-alias-name.d.ts +3 -8
- package/dist/application-spec/parse-application-files/spec/model/names/entity-type-name.d.ts +3 -8
- package/dist/application-spec/parse-application-files/spec/model/names/expression-name.d.ts +3 -6
- package/dist/application-spec/parse-application-files/spec/model/names/instance-name.d.ts +3 -7
- package/dist/application-spec/parse-application-files/spec/model/names/invariant-name.d.ts +3 -3
- package/dist/application-spec/parse-application-files/spec/model/names/shape-property-name.d.ts +3 -11
- package/dist/application-spec/parse-application-files/spec/model/names/violation-type-name.d.ts +3 -3
- package/dist/application-spec/parse-application-files/spec/model/presets.d.ts +6 -29
- package/dist/application-spec/parse-application-files/spec/relations/reference-field-type.d.ts +25 -0
- package/dist/application-spec/parse-application-files/spec/relations/reference-field-type.js +1 -0
- package/dist/application-spec/parse-application-files/spec/relations/relation-field-mismatch.d.ts +46 -0
- package/dist/application-spec/parse-application-files/spec/relations/relation-field-mismatch.js +1 -0
- package/dist/application-spec/parse-application-files/spec/relations/relations.d.ts +4 -3
- package/dist/application-spec/parse-application-files/spec/relations/relations.js +1 -1
- package/dist/application-spec/parse-application-files/spec/sections/actions.d.ts +6 -23
- package/dist/application-spec/parse-application-files/spec/sections/app-metadata.d.ts +3 -18
- package/dist/application-spec/parse-application-files/spec/sections/application-version.d.ts +3 -7
- package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-form-elements.d.ts +9 -0
- package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-form-elements.js +1 -0
- package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-schema.d.ts +4 -1
- package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-schema.js +1 -1
- package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant.js +1 -1
- package/dist/application-spec/parse-application-files/spec/sections/data-spec/chunks.d.ts +6 -23
- package/dist/application-spec/parse-application-files/spec/sections/data-spec/custom-data-spec.d.ts +1 -16
- package/dist/application-spec/parse-application-files/spec/sections/data-spec/data-entry-key.d.ts +3 -9
- package/dist/application-spec/parse-application-files/spec/sections/data-spec/data-spec.d.ts +1 -10
- package/dist/application-spec/parse-application-files/spec/sections/data-spec/swatch-data-spec.d.ts +1 -14
- package/dist/application-spec/parse-application-files/spec/sections/infra.d.ts +2 -0
- package/dist/application-spec/parse-application-files/spec/sections/infra.js +1 -1
- package/dist/application-spec/parse-application-files/spec/sections/state-value.d.ts +3 -7
- package/dist/application-spec/parse-application-files/spec/sections/theme.d.ts +3 -10
- package/dist/application-spec/parse-application-files/spec/sections/transformers.d.ts +4 -11
- package/dist/application-spec/parse-application-files/spec/sections/ui.d.ts +3 -12
- package/dist/client/authorized-client.d.ts +3 -1
- package/dist/client/authorized-client.js +1 -1
- package/dist/client/contract.d.ts +17 -0
- package/dist/client/index.d.ts +1 -1
- package/dist/client/local-authorized-client.d.ts +3 -1
- package/dist/client/local-authorized-client.js +1 -1
- package/dist/client/memory-authorized-client.d.ts +4 -1
- package/dist/client/memory-authorized-client.js +1 -1
- package/dist/client/phoenix-api.d.ts +809 -118
- package/dist/configurator/ConfiguratorApp.js +1 -1
- package/dist/configurator/app-loader-content/DefaultAppLoaderContent.d.ts +2 -0
- package/dist/configurator/app-loader-content/DefaultAppLoaderContent.js +5 -0
- package/dist/configurator/app-loader-content/index.d.ts +1 -0
- package/dist/configurator/app-loader-content/index.js +1 -0
- package/dist/configurator/application-runtime/extensions/create-configurator-extensions.js +1 -1
- package/dist/configurator/application-runtime/extensions/selectors.d.ts +3 -1
- package/dist/configurator/application-runtime/extensions/selectors.js +1 -1
- package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.d.ts +54 -3
- package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.js +1 -1
- package/dist/configurator/application-runtime/extensions/systems/assistant-system/graphql-tools.d.ts +58 -9
- package/dist/configurator/application-runtime/extensions/systems/assistant-system/graphql-tools.js +1 -1
- package/dist/configurator/application-runtime/extensions/systems/camera-control-system/camera-control-system.d.ts +1 -1
- package/dist/configurator/application-runtime/extensions/systems/canvas-system.d.ts +8 -0
- package/dist/configurator/application-runtime/extensions/systems/canvas-system.js +1 -1
- package/dist/configurator/application-runtime/extensions/systems/drag-and-drop-system/drag-and-drop-system.d.ts +3 -3
- package/dist/configurator/application-runtime/extensions/systems/export-system/export-busy-error.d.ts +8 -0
- package/dist/configurator/application-runtime/extensions/systems/export-system/export-busy-error.js +1 -0
- package/dist/configurator/application-runtime/extensions/systems/export-system/export-system.d.ts +23 -1
- package/dist/configurator/application-runtime/extensions/systems/export-system/export-system.js +1 -1
- package/dist/configurator/application-runtime/extensions/systems/graphql-system/close-unclosed-braces.d.ts +12 -0
- package/dist/configurator/application-runtime/extensions/systems/graphql-system/close-unclosed-braces.js +1 -0
- package/dist/configurator/application-runtime/extensions/systems/graphql-system/entity-meta.d.ts +82 -55
- package/dist/configurator/application-runtime/extensions/systems/graphql-system/entity-meta.js +1 -1
- package/dist/configurator/application-runtime/extensions/systems/graphql-system/graphql-system.d.ts +85 -7
- package/dist/configurator/application-runtime/extensions/systems/graphql-system/graphql-system.js +1 -1
- package/dist/configurator/application-runtime/extensions/systems/graphql-system/inject-assistant-lenses.d.ts +11 -0
- package/dist/configurator/application-runtime/extensions/systems/graphql-system/inject-assistant-lenses.js +1 -0
- package/dist/configurator/application-runtime/extensions/systems/graphql-system/violations.d.ts +40 -0
- package/dist/configurator/application-runtime/extensions/systems/graphql-system/violations.js +1 -0
- package/dist/configurator/application-runtime/extensions/systems/image-rendering-system/image-rendering-system.d.ts +14 -5
- package/dist/configurator/application-runtime/extensions/systems/image-rendering-system/image-rendering-system.js +1 -1
- package/dist/configurator/application-runtime/extensions/systems/index.d.ts +1 -1
- package/dist/configurator/application-runtime/extensions/systems/layout-system/layout-system.d.ts +2 -2
- package/dist/configurator/application-runtime/extensions/systems/layout-system/layout-system.js +1 -1
- package/dist/configurator/common/configurator-interface.d.ts +145 -5
- package/dist/configurator/common/configurator-interface.js +1 -1
- package/dist/configurator/common/index.d.ts +3 -1
- package/dist/configurator/common/index.js +1 -1
- package/dist/configurator/common/use-reset-camera.d.ts +6 -0
- package/dist/configurator/common/use-reset-camera.js +1 -0
- package/dist/configurator/configuration-not-found-page/BuiltInConfigurationNotFoundPage.d.ts +5 -0
- package/dist/configurator/configuration-not-found-page/BuiltInConfigurationNotFoundPage.js +1 -0
- package/dist/configurator/configuration-not-found-page/ConfigurationNotFoundPageElement.d.ts +6 -0
- package/dist/configurator/configuration-not-found-page/ConfigurationNotFoundPageElement.js +1 -0
- package/dist/configurator/configuration-not-found-page/index.d.ts +2 -0
- package/dist/configurator/configuration-not-found-page/index.js +1 -0
- package/dist/configurator/configurator-context/ConfiguratorResponsiveProvider.d.ts +12 -0
- package/dist/configurator/configurator-context/ConfiguratorResponsiveProvider.js +1 -0
- package/dist/configurator/configurator-context/PresetSelectionProvider.js +1 -1
- package/dist/configurator/configurator-context/PreviewContextProvider.js +1 -1
- package/dist/configurator/configurator-context/ViewportStateSync.d.ts +2 -2
- package/dist/configurator/configurator-context/ViewportStateSync.js +1 -1
- package/dist/configurator/configurator-context/configurator-hooks/index.d.ts +0 -1
- package/dist/configurator/configurator-context/configurator-hooks/index.js +1 -1
- package/dist/configurator/configurator-context/configurator-hooks/layout-hooks.d.ts +11 -1
- package/dist/configurator/configurator-context/index.d.ts +0 -4
- package/dist/configurator/configurator-context/index.js +1 -1
- package/dist/configurator/configurator-error-page/BuiltInConfiguratorErrorPage.d.ts +6 -0
- package/dist/configurator/configurator-error-page/BuiltInConfiguratorErrorPage.js +1 -0
- package/dist/configurator/configurator-error-page/ConfiguratorErrorPageElement.d.ts +7 -0
- package/dist/configurator/configurator-error-page/ConfiguratorErrorPageElement.js +1 -0
- package/dist/configurator/configurator-error-page/index.d.ts +2 -0
- package/dist/configurator/configurator-error-page/index.js +1 -0
- package/dist/configurator/index.d.ts +10 -0
- package/dist/configurator/index.js +1 -1
- package/dist/configurator/pages/ConfiguratorErrorPage.d.ts +0 -1
- package/dist/configurator/pages/ConfiguratorErrorPage.js +1 -1
- package/dist/configurator/pages/ConfiguratorNotFoundPage.js +1 -1
- package/dist/configurator/pages/DefaultConfiguratorLayout.d.ts +3 -2
- package/dist/configurator/pages/DefaultNotAuthenticatedPage.d.ts +8 -0
- package/dist/configurator/pages/DefaultNotAuthenticatedPage.js +1 -0
- package/dist/configurator/pages/DefaultPresetUi.d.ts +7 -0
- package/dist/configurator/pages/DefaultPresetUi.js +1 -0
- package/dist/configurator/pages/LoadedConfiguratorDetailPage.js +1 -1
- package/dist/configurator/pages/NewConfiguratorPage.js +1 -1
- package/dist/configurator/pages/PresetCreationErrorPage.d.ts +1 -1
- package/dist/configurator/pages/PresetCreationErrorPage.js +1 -1
- package/dist/configurator/pages/RootPage.js +1 -1
- package/dist/configurator/pages/configurator-layout-name.d.ts +2 -0
- package/dist/configurator/pages/configurator-layout-name.js +1 -0
- package/dist/configurator/pages/default-configurator-elements.d.ts +13 -0
- package/dist/configurator/pages/default-configurator-elements.js +1 -0
- package/dist/configurator/pages/default-elements/built-in-entity-element.d.ts +10 -0
- package/dist/configurator/pages/default-elements/built-in-entity-element.js +1 -0
- package/dist/configurator/pages/default-elements/built-in-layout-element.d.ts +10 -0
- package/dist/configurator/pages/default-elements/built-in-layout-element.js +1 -0
- package/dist/configurator/pages/default-elements/domain-control-elements.d.ts +6 -0
- package/dist/configurator/pages/default-elements/domain-control-elements.js +1 -0
- package/dist/configurator/pages/default-elements/index.d.ts +5 -0
- package/dist/configurator/pages/default-elements/index.js +1 -0
- package/dist/configurator/pages/default-elements/recipe-panel-elements.d.ts +3 -0
- package/dist/configurator/pages/default-elements/recipe-panel-elements.js +1 -0
- package/dist/configurator/pages/default-elements/scene-controls/BuiltInAddEntityFab.d.ts +3 -0
- package/dist/configurator/pages/default-elements/scene-controls/BuiltInAddEntityFab.js +1 -0
- package/dist/configurator/pages/default-elements/scene-controls/BuiltInDeleteEntityFab.d.ts +3 -0
- package/dist/configurator/pages/default-elements/scene-controls/BuiltInDeleteEntityFab.js +1 -0
- package/dist/configurator/pages/default-elements/scene-controls/BuiltInNavigationHotspot.d.ts +3 -0
- package/dist/configurator/pages/default-elements/scene-controls/BuiltInNavigationHotspot.js +1 -0
- package/dist/configurator/pages/default-elements/scene-controls/BuiltInSelectionHotspot.d.ts +3 -0
- package/dist/configurator/pages/default-elements/scene-controls/BuiltInSelectionHotspot.js +1 -0
- package/dist/configurator/pages/default-elements/scene-controls/BuiltInToolMenuFab.d.ts +3 -0
- package/dist/configurator/pages/default-elements/scene-controls/BuiltInToolMenuFab.js +1 -0
- package/dist/configurator/pages/default-elements/scene-controls/index.d.ts +6 -0
- package/dist/configurator/pages/default-elements/scene-controls/index.js +1 -0
- package/dist/configurator/pages/default-elements/scene-controls/scene-control-elements.d.ts +6 -0
- package/dist/configurator/pages/default-elements/scene-controls/scene-control-elements.js +1 -0
- package/dist/configurator/pages/default-elements/sidebar-footer-elements.d.ts +7 -0
- package/dist/configurator/pages/default-elements/sidebar-footer-elements.js +1 -0
- package/dist/configurator/pages/default-elements/toolbar-elements.d.ts +7 -0
- package/dist/configurator/pages/default-elements/toolbar-elements.js +1 -0
- package/dist/configurator/pages/error-boundaries/DevContextFallback.js +1 -1
- package/dist/configurator/pages/error-boundaries/DevLoadingFallback.js +1 -1
- package/dist/configurator/pages/error-boundaries/GeneralErrorBoundary.js +1 -1
- package/dist/configurator/pages/error-boundaries/LoadingErrorBoundary.js +1 -1
- package/dist/configurator/pages/index.d.ts +1 -0
- package/dist/configurator/pages/index.js +1 -1
- package/dist/configurator/preset-screen-elements/PresetDeviceNotSupportedElement.d.ts +4 -0
- package/dist/configurator/preset-screen-elements/PresetDeviceNotSupportedElement.js +1 -0
- package/dist/configurator/preset-screen-elements/PresetPageActionElement.d.ts +4 -0
- package/dist/configurator/preset-screen-elements/PresetPageActionElement.js +1 -0
- package/dist/configurator/preset-screen-elements/index.d.ts +2 -0
- package/dist/configurator/preset-screen-elements/index.js +1 -0
- package/dist/configurator/scene/index.d.ts +1 -0
- package/dist/configurator/scene/index.js +1 -1
- package/dist/configurator/scene/scene-controls/DeleteEntityFab.js +1 -1
- package/dist/configurator/scene/scene-controls/SelectionHotspot.js +1 -1
- package/dist/configurator/scene/scene-controls/ToolFab.d.ts +4 -0
- package/dist/configurator/scene/scene-controls/ToolFab.js +1 -1
- package/dist/configurator/scene/scene-controls/hotspot-drag-handle/HotspotDragHandle.d.ts +1 -1
- package/dist/configurator/scene/scene-controls/hotspot-drag-handle/HotspotDragSceneHandle.d.ts +1 -1
- package/dist/configurator/scene/scene-controls/use-scene-control-tooltip.d.ts +2 -1
- package/dist/configurator/scene/scene-entity/dimension-indicators/DimensionIndicator.d.ts +1 -1
- package/dist/configurator/scene/scene-entity/dimension-indicators/DimensionIndicatorInternal.d.ts +1 -1
- package/dist/configurator/scene/scene-entity/drag-and-drop/shared.d.ts +2 -2
- package/dist/configurator/scene/scene-entity/drag-and-drop/use-drag-object.d.ts +1 -1
- package/dist/configurator/scene/scene-entity/materials/RemoteReplacedMaterial.js +1 -1
- package/dist/configurator/scene/scene-entity/objects/SceneEntityObjectTextDecal.d.ts +1 -1
- package/dist/configurator/scene/scene-entity/scene-controls/SceneEntityHtmlHotspots.js +1 -1
- package/dist/configurator/toolbar-actions/index.d.ts +3 -0
- package/dist/configurator/toolbar-actions/index.js +1 -0
- package/dist/configurator/toolbar-actions/use-assistant-panel.d.ts +18 -0
- package/dist/configurator/toolbar-actions/use-assistant-panel.js +1 -0
- package/dist/configurator/toolbar-actions/use-measurements.d.ts +35 -0
- package/dist/configurator/toolbar-actions/use-measurements.js +1 -0
- package/dist/configurator/toolbar-actions/use-toolbar-actions.d.ts +100 -0
- package/dist/configurator/toolbar-actions/use-toolbar-actions.js +1 -0
- package/dist/configurator/toolbar-actions/use-toolbar-help.d.ts +7 -0
- package/dist/configurator/toolbar-actions/use-toolbar-help.js +1 -0
- package/dist/configurator/ui/assistant/AssistantChips.d.ts +10 -3
- package/dist/configurator/ui/assistant/AssistantChips.js +1 -1
- package/dist/configurator/ui/assistant/AssistantMarkdown.d.ts +7 -2
- package/dist/configurator/ui/assistant/AssistantMarkdown.js +1 -1
- package/dist/configurator/ui/assistant/AssistantPanel.js +1 -1
- package/dist/configurator/ui/assistant/AssistantThread.js +1 -1
- package/dist/configurator/ui/assistant/alert/AssistantAlert.d.ts +8 -0
- package/dist/configurator/ui/assistant/alert/AssistantAlert.js +1 -0
- package/dist/configurator/ui/assistant/alert/index.d.ts +2 -0
- package/dist/configurator/ui/assistant/alert/index.js +1 -0
- package/dist/configurator/ui/assistant/alert/remark-alerts.d.ts +10 -0
- package/dist/configurator/ui/assistant/alert/remark-alerts.js +1 -0
- package/dist/configurator/ui/assistant/entity-link/AssistantEntityLink.d.ts +13 -0
- package/dist/configurator/ui/assistant/entity-link/AssistantEntityLink.js +1 -0
- package/dist/configurator/ui/assistant/entity-link/entity-link-selection.d.ts +22 -0
- package/dist/configurator/ui/assistant/entity-link/entity-link-selection.js +1 -0
- package/dist/configurator/ui/assistant/entity-link/entity-link-state.d.ts +27 -0
- package/dist/configurator/ui/assistant/entity-link/entity-link-state.js +1 -0
- package/dist/configurator/ui/assistant/entity-link/index.d.ts +2 -0
- package/dist/configurator/ui/assistant/entity-link/index.js +1 -0
- package/dist/configurator/ui/assistant/form/AssistantForm.d.ts +19 -0
- package/dist/configurator/ui/assistant/form/AssistantForm.js +1 -0
- package/dist/configurator/ui/assistant/form/compose-answer.d.ts +11 -0
- package/dist/configurator/ui/assistant/form/compose-answer.js +1 -0
- package/dist/configurator/ui/assistant/form/describe-build-errors.d.ts +6 -0
- package/dist/configurator/ui/assistant/form/describe-build-errors.js +1 -0
- package/dist/configurator/ui/assistant/form/form-model.d.ts +37 -0
- package/dist/configurator/ui/assistant/form/form-model.js +1 -0
- package/dist/configurator/ui/assistant/form/form-state.d.ts +4 -0
- package/dist/configurator/ui/assistant/form/form-state.js +1 -0
- package/dist/configurator/ui/assistant/form/index.d.ts +4 -0
- package/dist/configurator/ui/assistant/form/index.js +1 -0
- package/dist/configurator/ui/assistant/form/parse-form-block.d.ts +10 -0
- package/dist/configurator/ui/assistant/form/parse-form-block.js +1 -0
- package/dist/configurator/ui/assistant/form/read-form-attributes.d.ts +13 -0
- package/dist/configurator/ui/assistant/form/read-form-attributes.js +1 -0
- package/dist/configurator/ui/assistant/form/registry/built-in-form-elements.d.ts +3 -0
- package/dist/configurator/ui/assistant/form/registry/built-in-form-elements.js +1 -0
- package/dist/configurator/ui/assistant/form/registry/create-assistant-form-element.d.ts +31 -0
- package/dist/configurator/ui/assistant/form/registry/create-assistant-form-element.js +1 -0
- package/dist/configurator/ui/assistant/form/registry/form-element-definition.d.ts +16 -0
- package/dist/configurator/ui/assistant/form/registry/form-element-definition.js +1 -0
- package/dist/configurator/ui/assistant/form/registry/form-registry.d.ts +31 -0
- package/dist/configurator/ui/assistant/form/registry/form-registry.js +1 -0
- package/dist/configurator/ui/assistant/form/registry/index.d.ts +6 -0
- package/dist/configurator/ui/assistant/form/registry/index.js +1 -0
- package/dist/configurator/ui/assistant/form/registry/use-form-registry.d.ts +8 -0
- package/dist/configurator/ui/assistant/form/registry/use-form-registry.js +1 -0
- package/dist/configurator/ui/assistant/form-elements/FormCard.d.ts +9 -0
- package/dist/configurator/ui/assistant/form-elements/FormCard.js +1 -0
- package/dist/configurator/ui/assistant/form-elements/FormConfirm.d.ts +9 -0
- package/dist/configurator/ui/assistant/form-elements/FormConfirm.js +1 -0
- package/dist/configurator/ui/assistant/form-elements/FormGroup.d.ts +6 -0
- package/dist/configurator/ui/assistant/form-elements/FormGroup.js +1 -0
- package/dist/configurator/ui/assistant/form-elements/FormNumber.d.ts +9 -0
- package/dist/configurator/ui/assistant/form-elements/FormNumber.js +1 -0
- package/dist/configurator/ui/assistant/form-elements/FormOptions.d.ts +6 -0
- package/dist/configurator/ui/assistant/form-elements/FormOptions.js +1 -0
- package/dist/configurator/ui/assistant/form-elements/FormReply.d.ts +6 -0
- package/dist/configurator/ui/assistant/form-elements/FormReply.js +1 -0
- package/dist/configurator/ui/assistant/form-elements/FormSwatch.d.ts +10 -0
- package/dist/configurator/ui/assistant/form-elements/FormSwatch.js +1 -0
- package/dist/configurator/ui/assistant/form-elements/FormText.d.ts +6 -0
- package/dist/configurator/ui/assistant/form-elements/FormText.js +1 -0
- package/dist/configurator/ui/assistant/form-elements/clamp-form-number.d.ts +10 -0
- package/dist/configurator/ui/assistant/form-elements/clamp-form-number.js +1 -0
- package/dist/configurator/ui/assistant/form-elements/form-element-props.d.ts +38 -0
- package/dist/configurator/ui/assistant/form-elements/form-element-props.js +1 -0
- package/dist/configurator/ui/assistant/form-elements/index.d.ts +9 -0
- package/dist/configurator/ui/assistant/form-elements/index.js +1 -0
- package/dist/configurator/ui/assistant/index.d.ts +3 -0
- package/dist/configurator/ui/assistant/index.js +1 -1
- package/dist/configurator/ui/assistant/reference-link/AssistantReferenceLink.d.ts +16 -0
- package/dist/configurator/ui/assistant/reference-link/AssistantReferenceLink.js +1 -0
- package/dist/configurator/ui/assistant/reference-link/assistant-link-target.d.ts +34 -0
- package/dist/configurator/ui/assistant/reference-link/assistant-link-target.js +1 -0
- package/dist/configurator/ui/assistant/reference-link/assistant-reference-details.d.ts +25 -0
- package/dist/configurator/ui/assistant/reference-link/assistant-reference-details.js +1 -0
- package/dist/configurator/ui/assistant/reference-link/index.d.ts +3 -0
- package/dist/configurator/ui/assistant/reference-link/index.js +1 -0
- package/dist/configurator/ui/assistant/reference-link/reference-styles.d.ts +12 -0
- package/dist/configurator/ui/assistant/reference-link/reference-styles.js +1 -0
- package/dist/configurator/ui/assistant/reference-link/use-assistant-reference-details.d.ts +8 -0
- package/dist/configurator/ui/assistant/reference-link/use-assistant-reference-details.js +1 -0
- package/dist/configurator/ui/assistant/resource-image/AssistantImage.d.ts +9 -0
- package/dist/configurator/ui/assistant/resource-image/AssistantImage.js +1 -0
- package/dist/configurator/ui/assistant/resource-image/index.d.ts +3 -0
- package/dist/configurator/ui/assistant/resource-image/index.js +1 -0
- package/dist/configurator/ui/assistant/resource-image/resource-path.d.ts +2 -0
- package/dist/configurator/ui/assistant/resource-image/resource-path.js +1 -0
- package/dist/configurator/ui/assistant/resource-image/use-resource-image-url.d.ts +5 -0
- package/dist/configurator/ui/assistant/resource-image/use-resource-image-url.js +1 -0
- package/dist/configurator/ui/assistant/screenshots/ScreenshotThumbnails.d.ts +9 -0
- package/dist/configurator/ui/assistant/screenshots/ScreenshotThumbnails.js +1 -0
- package/dist/configurator/ui/assistant/screenshots/index.d.ts +2 -0
- package/dist/configurator/ui/assistant/screenshots/index.js +1 -0
- package/dist/configurator/ui/assistant/screenshots/tool-screenshots.d.ts +22 -0
- package/dist/configurator/ui/assistant/screenshots/tool-screenshots.js +1 -0
- package/dist/configurator/ui/assistant/screenshots/use-screenshot-src.d.ts +8 -0
- package/dist/configurator/ui/assistant/screenshots/use-screenshot-src.js +1 -0
- package/dist/configurator/ui/assistant/streamed-markdown.d.ts +4 -2
- package/dist/configurator/ui/assistant/streamed-markdown.js +2 -2
- package/dist/configurator/ui/assistant/thread-items.d.ts +8 -1
- package/dist/configurator/ui/assistant/thread-items.js +1 -1
- package/dist/configurator/ui/assistant/tool-chip.d.ts +19 -9
- package/dist/configurator/ui/assistant/tool-chip.js +1 -1
- package/dist/configurator/ui/assistant/use-follow-stream.js +1 -1
- package/dist/configurator/ui/configurator-layout/sidebar-keys/index.d.ts +1 -0
- package/dist/configurator/ui/configurator-layout/sidebar-keys/index.js +1 -0
- package/dist/configurator/ui/configurator-layout/sidebar-keys/sidebar-keys.d.ts +11 -0
- package/dist/configurator/ui/configurator-layout/sidebar-keys/sidebar-keys.js +1 -0
- package/dist/configurator/ui/configurator-layout/slots/canvas/GPUAccelerationDisabled.js +1 -1
- package/dist/configurator/ui/configurator-layout/slots/canvas/LayoutCanvas.js +1 -1
- package/dist/configurator/ui/configurator-layout/slots/canvas/SharedCanvas.js +1 -1
- package/dist/configurator/ui/configurator-layout/slots/canvas/WebGLNotSupported.js +1 -1
- package/dist/configurator/ui/configurator-layout/slots/desktop-sidebar/DesktopSidebar.d.ts +101 -0
- package/dist/configurator/ui/configurator-layout/slots/desktop-sidebar/DesktopSidebar.js +16 -0
- package/dist/configurator/ui/configurator-layout/slots/desktop-sidebar/index.d.ts +1 -0
- package/dist/configurator/ui/configurator-layout/slots/desktop-sidebar/index.js +1 -0
- package/dist/configurator/ui/configurator-layout/slots/mobile-tabs/use-mobile-tabs.d.ts +1 -1
- package/dist/configurator/ui/configurator-layout/slots/mobile-tabs/use-mobile-tabs.js +1 -1
- package/dist/configurator/ui/configurator-layout/slots/readonly-banner/CloneToEditDialog.js +1 -1
- package/dist/configurator/ui/configurator-layout/slots/services/InvariantViolations.d.ts +4 -4
- package/dist/configurator/ui/configurator-layout/slots/services/InvariantViolations.js +1 -1
- package/dist/configurator/ui/configurator-layout/slots/services/Services.js +1 -1
- package/dist/configurator/ui/configurator-layout/slots/services/SideEffects.d.ts +3 -6
- package/dist/configurator/ui/configurator-layout/slots/services/SideEffects.js +1 -1
- package/dist/configurator/ui/configurator-layout/slots/sidebar-tabs/use-sidebar-tabs.d.ts +1 -1
- package/dist/configurator/ui/configurator-layout/slots/sidebar-tabs/use-sidebar-tabs.js +1 -1
- package/dist/configurator/ui/configurator-layout/slots/slots.d.ts +1 -0
- package/dist/configurator/ui/configurator-layout/slots/slots.js +1 -1
- package/dist/configurator/ui/configurator-layout/universal/DesktopLayout.d.ts +0 -74
- package/dist/configurator/ui/configurator-layout/universal/DesktopLayout.js +5 -20
- package/dist/configurator/ui/configurator-layout/universal/DesktopPreview.js +1 -1
- package/dist/configurator/ui/configurator-layout/universal/ExportingOverlay.d.ts +5 -0
- package/dist/configurator/ui/configurator-layout/universal/ExportingOverlay.js +1 -0
- package/dist/configurator/ui/configurator-layout/universal/Layout.js +1 -1
- package/dist/configurator/ui/configurator-layout/universal/MobileLayout.js +4 -4
- package/dist/configurator/ui/configurator-layout/universal/MobilePreview.js +1 -1
- package/dist/configurator/ui/configurator-layout/universal/use-layout-scene-safe-area.d.ts +47 -0
- package/dist/configurator/ui/configurator-layout/universal/use-layout-scene-safe-area.js +1 -0
- package/dist/configurator/ui/configurator-layout/universal/use-scene-safe-area.d.ts +11 -26
- package/dist/configurator/ui/configurator-layout/universal/use-scene-safe-area.js +1 -1
- package/dist/configurator/ui/index.d.ts +1 -0
- package/dist/configurator/ui/index.js +1 -1
- package/dist/configurator/ui/legacy/chat/MobileChat.js +1 -1
- package/dist/configurator/ui/legacy/layout/common/CommonFinalizeButton.js +1 -1
- package/dist/configurator/ui/legacy/layout/common/CommonSummaryButton.js +1 -1
- package/dist/configurator/ui/legacy/layout/common/CommonTopNavigation.js +1 -1
- package/dist/configurator/ui/legacy/layout/common/ReactiveInterfaceLayout.js +1 -1
- package/dist/configurator/ui/legacy/layout/common/ResponsiveFinalizeButton.js +1 -1
- package/dist/configurator/ui/legacy/layout/common/SummaryReadLayout.js +1 -1
- package/dist/configurator/ui/legacy/layout/simple/SimpleOwnerLayout.js +1 -1
- package/dist/configurator/ui/legacy/layout/simple/SimpleSidebar.js +1 -1
- package/dist/configurator/ui/legacy/layout/simple/SimpleTopNavigation.js +1 -1
- package/dist/configurator/ui/legacy/layout/steps/StepsNavigation.d.ts +2 -2
- package/dist/configurator/ui/legacy/layout/steps/StepsNavigation.js +1 -1
- package/dist/configurator/ui/legacy/layout/steps/StepsOwnerLayout.js +1 -1
- package/dist/configurator/ui/legacy/layout/steps/StepsSideBar.js +1 -1
- package/dist/configurator/ui/legacy/layout/steps/StepsSidebarFooter.js +1 -1
- package/dist/configurator/ui/legacy/layout/tabs/TabsOwnerLayout.js +1 -1
- package/dist/configurator/ui/legacy/layout/tabs/TabsSidebar.js +1 -1
- package/dist/configurator/ui/legacy/toolbar/common/DimensionUnitSelector.js +1 -1
- package/dist/configurator/ui/legacy/toolbar/common/MeasurementsIndicatorsSwitch.js +1 -1
- package/dist/configurator/ui/legacy/toolbar/desktop-toolbar/FullscreenToolbarAction.js +1 -1
- package/dist/configurator/ui/legacy/toolbar/desktop-toolbar/HelpToolbarAction.js +1 -1
- package/dist/configurator/ui/legacy/toolbar/desktop-toolbar/MeasurementsToolbarAction.js +1 -1
- package/dist/configurator/ui/legacy/toolbar/desktop-toolbar/UndoRedoToolbarGroup.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/AppLoader.d.ts +0 -7
- package/dist/configurator/ui/phoenix-layout-elements/AppLoader.js +2 -6
- package/dist/configurator/ui/phoenix-layout-elements/AssistantToolbarButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/EnterFullscreenToolbarButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/ExitFullscreenToolbarButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/HelpToolbarButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/LanguageMenuAction.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/LanguageToolbarButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/LayoutCustomOwnerUi.d.ts +1 -0
- package/dist/configurator/ui/phoenix-layout-elements/LayoutCustomOwnerUi.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/LayoutCustomReadUi.d.ts +1 -0
- package/dist/configurator/ui/phoenix-layout-elements/LayoutCustomReadUi.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/MeasurementsMenuAction.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/MeasurementsToolbarButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/RedoToolbarButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/ShareToolbarButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/UndoToolbarButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/header/common/back-button/use-go-back-action.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/header/common/horizontal-tabs/DesktopHorizontalTabs.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/header/common/step-navigation/DesktopNavigationSteps.d.ts +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/header/common/step-navigation/DesktopNavigationSteps.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/header/common/step-navigation/StepsNavigation.d.ts +2 -8
- package/dist/configurator/ui/phoenix-layout-elements/header/common/step-navigation/StepsNavigation.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/index.d.ts +8 -1
- package/dist/configurator/ui/phoenix-layout-elements/index.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/measurements/DimensionUnitSelector.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/measurements/MeasurementsIndicatorsSwitch.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/share/ShareModal.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/share/use-share.d.ts +7 -8
- package/dist/configurator/ui/phoenix-layout-elements/share/use-share.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/basic/BasicFinalizeButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/CustomFinalizeButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/EnterSummaryButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/FinalizeActions.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/NextStepButton.js +1 -1
- package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/PreviousStepButton.js +1 -1
- package/dist/configurator/ui/preset-layout/CategoryPresetUi.js +1 -1
- package/dist/configurator/ui/preset-layout/PresetUi.d.ts +4 -2
- package/dist/configurator/ui/preset-layout/PresetUi.js +1 -1
- package/dist/configurator/ui/preset-layout/index.d.ts +6 -1
- package/dist/configurator/ui/preset-layout/index.js +1 -1
- package/dist/configurator/ui/save-message-snackbars/SaveMessageSnackbars.d.ts +10 -0
- package/dist/configurator/ui/save-message-snackbars/SaveMessageSnackbars.js +1 -0
- package/dist/configurator/ui/save-message-snackbars/index.d.ts +1 -0
- package/dist/configurator/ui/save-message-snackbars/index.js +1 -0
- package/dist/docs/3d-scene/cameras/custom.mdx +4 -2
- package/dist/docs/complex-use-cases/filtered-controls-with-fix-dialog.mdx +16 -19
- package/dist/docs/complex-use-cases/focus-mode.mdx +2 -0
- package/dist/docs/complex-use-cases/sketching.mdx +0 -38
- package/dist/docs/controls/select-attribute/cardselect.mdx +9 -13
- package/dist/docs/controls/select-child-entity/cardselect.mdx +1 -0
- package/dist/docs/dimensions/3d-scene-integration/dimension-indicators.mdx +2 -2
- package/dist/docs/dimensions/3d-scene-integration/editable-angle-indicator.mdx +1 -1
- package/dist/docs/dimensions/3d-scene-integration/editable-distance-dimension-indicator.mdx +1 -1
- package/dist/docs/dimensions/3d-scene-integration/list-selector-dimension-indicator.mdx +1 -1
- package/dist/docs/drag-and-drop/drag-handle.mdx +1 -1
- package/dist/docs/drag-and-drop/hotspot-handle.mdx +1 -1
- package/dist/docs/drag-and-drop/planes.mdx +1 -1
- package/dist/docs/drag-and-drop/room-planner.mdx +1 -1
- package/dist/docs/drag-and-drop/rotatable.mdx +2 -2
- package/dist/docs/fabs/basic.mdx +6 -2
- package/dist/docs/index.json +136 -1
- package/dist/docs/integrations/webcomponent-api/basics.mdx +2 -0
- package/dist/docs/integrations/woocommerce/local-development.mdx +1 -1
- package/dist/docs/layouts/custom-layout.mdx +2597 -0
- package/dist/docs/layouts/gallery/ardent-compressor.mdx +3516 -0
- package/dist/docs/layouts/gallery/corvid-drone.mdx +3428 -0
- package/dist/docs/layouts/gallery/fenwick-window.mdx +2803 -0
- package/dist/docs/layouts/gallery/ferrule-bar-station.mdx +6567 -0
- package/dist/docs/layouts/gallery/kestrel-kettle.mdx +1858 -0
- package/dist/docs/layouts/gallery/nightjar-guitar.mdx +3698 -0
- package/dist/docs/layouts/gallery/sunfold-solar.mdx +2683 -0
- package/dist/docs/layouts/gallery/tidepool-aquascape.mdx +5203 -0
- package/dist/docs/layouts/header/fixed.mdx +2 -2
- package/dist/docs/layouts/header/floating.mdx +2 -2
- package/dist/docs/layouts/sidebar/in-a-custom-layout.mdx +380 -0
- package/dist/docs/layouts/sidebar-footer/basic.mdx +32 -77
- package/dist/docs/other-features/client-side-rendering/declarative.mdx +2 -2
- package/dist/docs/other-features/error-5xx.mdx +1 -1
- package/dist/docs/other-features/navigation-hotspot.mdx +2 -0
- package/dist/docs/other-features/not-found-404.mdx +1 -1
- package/dist/docs/other-features/side-effects.mdx +8 -11
- package/dist/docs/other-features/transformers.mdx +4 -0
- package/dist/docs/other-features/violation-messages.mdx +52 -56
- package/dist/docs/phoenix/application-states.mdx +9 -7
- package/dist/docs/phoenix/clerk-sign-in.mdx +1 -1
- package/dist/docs/phoenix/components.mdx +6 -0
- package/dist/docs/phoenix/configurator-interface.mdx +377 -83
- package/dist/docs/phoenix/deployments.mdx +2 -6
- package/dist/docs/phoenix/domain-model.mdx +10 -1
- package/dist/docs/phoenix/finalization.mdx +1 -1
- package/dist/docs/phoenix/getting-started.mdx +6 -3
- package/dist/docs/phoenix/layout-slots.mdx +137 -0
- package/dist/docs/phoenix/localization.mdx +10 -6
- package/dist/docs/phoenix/migration-guide.mdx +24 -11
- package/dist/docs/phoenix/phoenix-cli.mdx +101 -48
- package/dist/docs/phoenix/project-structure.mdx +83 -37
- package/dist/docs/phoenix/quickstart.mdx +9 -9
- package/dist/docs/phoenix/sdk-builder-workflow.mdx +15 -13
- package/dist/docs/phoenix/sessions-and-access.mdx +2 -0
- package/dist/docs/phoenix/testing.mdx +148 -0
- package/dist/docs/phoenix/webhook-membership.mdx +10 -9
- package/dist/docs/phoenix/webhooks.mdx +27 -6
- package/dist/docs/selection-and-highlight/hotspots.mdx +2 -0
- package/dist/domain/admin-domain/implementation/common-admin-expressions.d.ts +26 -11
- package/dist/domain/admin-domain/implementation/common-admin-publish.d.ts +1 -2
- package/dist/domain/admin-domain/index.js +1 -1
- package/dist/domain/application/GetApplicationWrapperHandler.d.ts +1 -1
- package/dist/domain/application/application-config.d.ts +13 -27
- package/dist/domain/application/assistant/assistant-fields.d.ts +12 -0
- package/dist/domain/application/assistant/assistant-fields.js +1 -0
- package/dist/domain/application/assistant/assistant-manifest.d.ts +38 -5
- package/dist/domain/application/assistant/assistant-manifest.js +1 -1
- package/dist/domain/application/assistant/assistant-resolved-manifest.d.ts +48 -9
- package/dist/domain/application/assistant/build-assistant-lenses.d.ts +14 -0
- package/dist/domain/application/assistant/build-assistant-lenses.js +5 -0
- package/dist/domain/application/assistant/build-assistant-manifest.js +1 -1
- package/dist/domain/application/assistant/create-assistant-execution-schema.d.ts +32 -0
- package/dist/domain/application/assistant/create-assistant-execution-schema.js +1 -0
- package/dist/domain/application/assistant/form/assistant-form-element-name.d.ts +11 -0
- package/dist/domain/application/assistant/form/assistant-form-element-name.js +8 -0
- package/dist/domain/application/assistant/form/assistant-form-elements.d.ts +174 -0
- package/dist/domain/application/assistant/form/assistant-form-elements.js +1 -0
- package/dist/domain/application/assistant/form/assistant-resource-path.d.ts +10 -0
- package/dist/domain/application/assistant/form/assistant-resource-path.js +1 -0
- package/dist/domain/application/assistant/form/index.d.ts +4 -0
- package/dist/domain/application/assistant/form/index.js +1 -0
- package/dist/domain/application/assistant/generate-assistant-prompt.d.ts +3 -0
- package/dist/domain/application/assistant/generate-assistant-prompt.js +1 -1
- package/dist/domain/application/assistant/index.d.ts +8 -2
- package/dist/domain/application/assistant/index.js +1 -1
- package/dist/domain/application/assistant/session-setup/AssistantSetupHandler.d.ts +20 -0
- package/dist/domain/application/assistant/session-setup/AssistantSetupHandler.js +1 -0
- package/dist/domain/application/assistant/session-setup/apply-assistant-setup.d.ts +10 -0
- package/dist/domain/application/assistant/session-setup/apply-assistant-setup.js +1 -0
- package/dist/domain/application/assistant/session-setup/assistant-setup-manifest.d.ts +48 -0
- package/dist/domain/application/assistant/session-setup/assistant-setup-manifest.js +1 -0
- package/dist/domain/application/assistant/session-setup/create-session-assistant.d.ts +15 -0
- package/dist/domain/application/assistant/session-setup/create-session-assistant.js +1 -0
- package/dist/domain/application/assistant/session-setup/index.d.ts +4 -0
- package/dist/domain/application/assistant/session-setup/index.js +1 -0
- package/dist/domain/application/assistant-mutations.js +1 -1
- package/dist/domain/application/assistant-recipe-groups.d.ts +3 -1
- package/dist/domain/application/configurator-builder.d.ts +11 -2
- package/dist/domain/application/configurator-builder.js +1 -1
- package/dist/domain/application/create-initial-published-data.d.ts +5 -5
- package/dist/domain/application/index.d.ts +1 -2
- package/dist/domain/application/index.js +1 -1
- package/dist/domain/application/published-data/map-published-chunked-data.d.ts +1 -1
- package/dist/domain/application/published-data/map-published-data.d.ts +7 -3
- package/dist/domain/application/standard-properties.js +1 -1
- package/dist/domain/application/web-component-property.d.ts +3 -1
- package/dist/domain/application/web-component-property.js +1 -1
- package/dist/domain/available-system-components.d.ts +1 -1
- package/dist/domain/available-system-components.js +1 -1
- package/dist/domain/core/component/domain-component-optic.d.ts +1 -2
- package/dist/domain/core/component/domain-component-optic.js +2 -2
- package/dist/domain/core/component/system-component-filter.d.ts +15 -2
- package/dist/domain/core/expression/constant-expressions.d.ts +16 -8
- package/dist/domain/core/expression/custom-expression.d.ts +16 -3
- package/dist/domain/core/invariant/custom-invariants.d.ts +6 -14
- package/dist/domain/core/invariant/invariants.d.ts +1 -4
- package/dist/domain/core/invariant/violation.d.ts +50 -2
- package/dist/domain/core/invariant/violation.js +1 -1
- package/dist/domain/core/model/model.d.ts +1 -1
- package/dist/domain/core/recipe/recipes.d.ts +18 -50
- package/dist/domain/core/recipe/recipes.js +1 -1
- package/dist/domain/core/relation/all-of-type-relation.d.ts +8 -21
- package/dist/domain/core/relation/all-of-type-relation.js +1 -1
- package/dist/domain/core/relation/direct-relation.d.ts +13 -24
- package/dist/domain/core/relation/index.d.ts +0 -1
- package/dist/domain/core/relation/indirect-relation.d.ts +12 -67
- package/dist/domain/core/relation/indirect-relation.js +1 -1
- package/dist/domain/core/relation/relation-factory.d.ts +19 -15
- package/dist/domain/core/relation/relation-factory.js +1 -1
- package/dist/domain/core/schema/extended-schema-validator.js +1 -1
- package/dist/domain/core/state/state.d.ts +1 -1
- package/dist/domain/domain-builder.d.ts +9 -3
- package/dist/domain/domain-builder.js +1 -1
- package/dist/domain/domain-context/index.d.ts +1 -1
- package/dist/domain/domain-context/store-system/store-system.d.ts +5 -0
- package/dist/domain/domain-context/store-system/store-system.js +1 -1
- package/dist/domain/domain-context/undo-system.d.ts +28 -1
- package/dist/domain/domain-context/undo-system.js +1 -1
- package/dist/domain/domain-runtime/access/selectors.d.ts +9 -0
- package/dist/domain/domain-runtime/access/selectors.js +1 -1
- package/dist/domain/domain-runtime/access/types.d.ts +2 -0
- package/dist/domain/domain-runtime/access/types.js +1 -1
- package/dist/domain/domain-runtime/application.d.ts +39 -0
- package/dist/domain/domain-runtime/application.js +1 -0
- package/dist/domain/domain-runtime/domain-context/selectors.d.ts +15 -13
- package/dist/domain/domain-runtime/domain-context/selectors.js +1 -1
- package/dist/domain/domain-runtime/domain-context/types.d.ts +4 -1
- package/dist/domain/domain-runtime/domain-runtime.d.ts +76 -20
- package/dist/domain/domain-runtime/domain-runtime.js +1 -1
- package/dist/domain/domain-runtime/environment/initialize.d.ts +1 -1
- package/dist/domain/domain-runtime/environment/types.d.ts +1 -1
- package/dist/domain/domain-runtime/errors/implementation-error-collector.d.ts +21 -0
- package/dist/domain/domain-runtime/errors/implementation-error-collector.js +1 -0
- package/dist/domain/domain-runtime/errors/implementation-error.d.ts +46 -0
- package/dist/domain/domain-runtime/errors/implementation-error.js +1 -0
- package/dist/domain/domain-runtime/errors/implementation-errors.d.ts +13 -0
- package/dist/domain/domain-runtime/errors/implementation-errors.js +1 -0
- package/dist/domain/domain-runtime/errors/index.d.ts +4 -0
- package/dist/domain/domain-runtime/errors/index.js +1 -0
- package/dist/domain/domain-runtime/errors/to-implementation-errors.d.ts +10 -0
- package/dist/domain/domain-runtime/errors/to-implementation-errors.js +1 -0
- package/dist/domain/domain-runtime/index.d.ts +2 -0
- package/dist/domain/domain-runtime/index.js +1 -1
- package/dist/domain/domain-runtime/spec/create-violation-expressions.d.ts +24 -0
- package/dist/domain/domain-runtime/spec/create-violation-expressions.js +1 -0
- package/dist/domain/domain-runtime/spec/index.d.ts +2 -2
- package/dist/domain/domain-runtime/spec/index.js +1 -1
- package/dist/domain/domain-runtime/spec/initialize.d.ts +11 -0
- package/dist/domain/domain-runtime/spec/initialize.js +1 -1
- package/dist/domain/domain-runtime/spec/module-checks/element-registration.d.ts +27 -12
- package/dist/domain/domain-runtime/spec/module-checks/element-registration.js +1 -1
- package/dist/domain/domain-runtime/spec/module-checks/find-assistant-form-element-errors.d.ts +12 -0
- package/dist/domain/domain-runtime/spec/module-checks/find-assistant-form-element-errors.js +1 -0
- package/dist/domain/domain-runtime/spec/module-checks/index.d.ts +4 -2
- package/dist/domain/domain-runtime/spec/module-checks/index.js +1 -1
- package/dist/domain/domain-runtime/spec/module-checks/merge-expression-modules.d.ts +23 -0
- package/dist/domain/domain-runtime/spec/module-checks/merge-expression-modules.js +1 -0
- package/dist/domain/domain-runtime/spec/module-checks/project-modules.d.ts +8 -1
- package/dist/domain/domain-runtime/spec/module-checks/project-modules.js +1 -1
- package/dist/domain/domain-runtime/spec/module-checks/resolve-action-modules.js +1 -1
- package/dist/domain/domain-runtime/spec/module-checks/resolve-expression-modules.d.ts +3 -4
- package/dist/domain/domain-runtime/spec/module-checks/resolve-expression-modules.js +1 -1
- package/dist/domain/domain-runtime/spec/module-checks/resolve-imported-transformers.d.ts +7 -7
- package/dist/domain/domain-runtime/spec/module-checks/resolve-imported-transformers.js +1 -1
- package/dist/domain/domain-runtime/spec/module-checks/resolve-invariant-modules.d.ts +2 -2
- package/dist/domain/domain-runtime/spec/module-checks/resolve-invariant-modules.js +1 -1
- package/dist/domain/domain-runtime/spec/selectors.d.ts +3 -1
- package/dist/domain/domain-runtime/spec/selectors.js +1 -1
- package/dist/domain/domain-runtime/spec/types.d.ts +2 -0
- package/dist/domain/domain-runtime/web-component/actions.d.ts +3 -0
- package/dist/domain/domain-runtime/web-component/actions.js +1 -1
- package/dist/domain/domain-runtime/web-component/initialize.d.ts +1 -0
- package/dist/domain/domain-runtime/web-component/initialize.js +1 -1
- package/dist/domain/domain-runtime/web-component/selectors.d.ts +2 -0
- package/dist/domain/domain-runtime/web-component/selectors.js +1 -1
- package/dist/domain/domain-runtime/web-component/types.d.ts +6 -3
- package/dist/domain/extensions/attribute-switch.d.ts +1 -1
- package/dist/domain/extensions/attribute-switch.js +1 -1
- package/dist/domain/extensions/canvas-config.d.ts +1 -1
- package/dist/domain/extensions/common/angle-indicator-schema.d.ts +2 -2
- package/dist/domain/extensions/common/dimension-indicator-schema.d.ts +2 -2
- package/dist/domain/extensions/editable-angle-indicator.d.ts +1 -1
- package/dist/domain/extensions/editable-distance-dimension-indicator.d.ts +1 -1
- package/dist/domain/extensions/editable-offset-indicator.d.ts +1 -1
- package/dist/domain/extensions/editable-offset-indicator.js +1 -1
- package/dist/domain/extensions/export-images/agent-screenshot.d.ts +24 -0
- package/dist/domain/extensions/export-images/agent-screenshot.js +1 -0
- package/dist/domain/extensions/export-images/client-side-image.d.ts +14 -0
- package/dist/domain/extensions/export-images/client-side-image.js +1 -0
- package/dist/domain/extensions/export-images/index.d.ts +3 -0
- package/dist/domain/extensions/export-images/index.js +1 -0
- package/dist/domain/extensions/{save-template-thumbnail.d.ts → export-images/save-template-thumbnail.d.ts} +4 -4
- package/dist/domain/extensions/export-images/save-template-thumbnail.js +1 -0
- package/dist/domain/extensions/fit-selection-orthographic-camera-component.js +1 -1
- package/dist/domain/extensions/fit-selection-perspective-camera-component.js +1 -1
- package/dist/domain/extensions/horizontal-tabs.d.ts +2 -2
- package/dist/domain/extensions/index.d.ts +1 -2
- package/dist/domain/extensions/index.js +1 -1
- package/dist/domain/extensions/list-selector-dimension-indicator.d.ts +1 -1
- package/dist/domain/extensions/navigation-steps.d.ts +2 -2
- package/dist/domain/extensions/offset-indicator.js +1 -1
- package/dist/domain/extensions/rotation-handle.d.ts +2 -2
- package/dist/domain/extensions/scene-entity-animation.js +1 -1
- package/dist/domain/index.d.ts +1 -0
- package/dist/domain/index.js +1 -1
- package/dist/domain/mocks/create-mock-domain-context.d.ts +1 -1
- package/dist/domain/mocks/create-mock-domain-runtime.d.ts +27 -9
- package/dist/domain/mocks/create-mock-domain-runtime.js +1 -1
- package/dist/domain/mocks/create-mock-domain.d.ts +1 -1
- package/dist/domain/mocks/create-mock-domain.js +1 -1
- package/dist/domain/mocks/create-mock-state.d.ts +2 -2
- package/dist/domain/mocks/create-test-application-config.d.ts +59 -0
- package/dist/domain/mocks/create-test-application-config.js +1 -0
- package/dist/domain/mocks/index.d.ts +1 -0
- package/dist/domain/mocks/index.js +1 -1
- package/dist/domain/projections/blender-projections/camera.d.ts +1 -1
- package/dist/domain/spec/application/admin-pages-section.d.ts +29 -0
- package/dist/domain/spec/application/admin-pages-section.js +1 -0
- package/dist/domain/spec/application/admin-pages.d.ts +26 -0
- package/dist/domain/spec/application/admin-pages.js +1 -0
- package/dist/domain/spec/application/admin-states.d.ts +5 -0
- package/dist/domain/spec/application/admin-states.js +1 -0
- package/dist/domain/spec/application/configurator-states.d.ts +8 -0
- package/dist/domain/spec/application/configurator-states.js +1 -0
- package/dist/domain/spec/application/index.d.ts +5 -0
- package/dist/domain/spec/application/index.js +1 -0
- package/dist/domain/spec/components/bindings/index.d.ts +2 -0
- package/dist/domain/spec/components/bindings/index.js +1 -0
- package/dist/domain/spec/components/bindings/read-binding.d.ts +13 -0
- package/dist/domain/spec/components/bindings/read-binding.js +1 -0
- package/dist/domain/spec/components/bindings/write-binding.d.ts +32 -0
- package/dist/domain/spec/components/bindings/write-binding.js +1 -0
- package/dist/{application-spec/parse-application-files → domain}/spec/components/component-entry.d.ts +3 -11
- package/dist/domain/spec/components/component-entry.js +1 -0
- package/dist/domain/spec/components/component-filter.d.ts +54 -0
- package/dist/domain/spec/components/component-filter.js +1 -0
- package/dist/domain/spec/components/component-filters.d.ts +10 -0
- package/dist/domain/spec/components/component-filters.js +1 -0
- package/dist/domain/spec/components/components.d.ts +4 -0
- package/dist/domain/spec/components/components.js +1 -0
- package/dist/domain/spec/components/index.d.ts +6 -0
- package/dist/domain/spec/components/index.js +1 -0
- package/dist/domain/spec/components/sdk-code-ref.d.ts +9 -0
- package/dist/domain/spec/enhanced-json-value.d.ts +30 -0
- package/dist/domain/spec/enhanced-json-value.js +1 -0
- package/dist/{application-spec/parse-application-files → domain}/spec/expressions/expression.d.ts +9 -30
- package/dist/domain/spec/expressions/expression.js +1 -0
- package/dist/domain/spec/expressions/expressions.d.ts +27 -0
- package/dist/domain/spec/expressions/expressions.js +1 -0
- package/dist/domain/spec/expressions/index.d.ts +5 -0
- package/dist/domain/spec/expressions/index.js +1 -0
- package/dist/domain/spec/expressions/memo-eval-plan.d.ts +70 -0
- package/dist/domain/spec/expressions/memo-eval-plan.js +1 -0
- package/dist/domain/spec/expressions/relation-expression-params.d.ts +11 -0
- package/dist/domain/spec/expressions/relation-expression-params.js +1 -0
- package/dist/domain/spec/expressions/sdk-expression-implementation.d.ts +24 -0
- package/dist/domain/spec/index.d.ts +9 -0
- package/dist/domain/spec/index.js +1 -0
- package/dist/domain/spec/invariants/index.d.ts +2 -0
- package/dist/domain/spec/invariants/index.js +1 -0
- package/dist/domain/spec/invariants/invariant.d.ts +24 -0
- package/dist/domain/spec/invariants/invariant.js +1 -0
- package/dist/domain/spec/invariants/invariants.d.ts +14 -0
- package/dist/domain/spec/invariants/invariants.js +1 -0
- package/dist/domain/spec/json-schema.d.ts +52 -0
- package/dist/domain/spec/json-schema.js +1 -0
- package/dist/domain/spec/markers/index.d.ts +3 -0
- package/dist/domain/spec/markers/index.js +1 -0
- package/dist/domain/spec/markers/msg-marker.d.ts +28 -0
- package/dist/domain/spec/markers/msg-marker.js +1 -0
- package/dist/domain/spec/model/aliases.d.ts +9 -0
- package/dist/domain/spec/model/aliases.js +1 -0
- package/dist/domain/spec/model/common-types.d.ts +15 -0
- package/dist/domain/spec/model/common-types.js +1 -0
- package/dist/domain/spec/model/defaults.d.ts +33 -0
- package/dist/domain/spec/model/defaults.js +1 -0
- package/dist/domain/spec/model/entity-types.d.ts +8 -0
- package/dist/domain/spec/model/entity-types.js +1 -0
- package/dist/domain/spec/model/index.d.ts +7 -0
- package/dist/domain/spec/model/index.js +1 -0
- package/dist/domain/spec/model/model-version-key.d.ts +3 -0
- package/dist/domain/spec/model/model-version-key.js +1 -0
- package/dist/domain/spec/model/names/action-name.d.ts +7 -0
- package/dist/domain/spec/model/names/action-name.js +1 -0
- package/dist/domain/spec/model/names/alias-name.d.ts +7 -0
- package/dist/domain/spec/model/names/alias-name.js +1 -0
- package/dist/domain/spec/model/names/entity-or-alias-name.d.ts +8 -0
- package/dist/domain/spec/model/names/entity-or-alias-name.js +1 -0
- package/dist/domain/spec/model/names/entity-type-name.d.ts +8 -0
- package/dist/domain/spec/model/names/entity-type-name.js +1 -0
- package/dist/domain/spec/model/names/expression-name.d.ts +6 -0
- package/dist/domain/spec/model/names/expression-name.js +1 -0
- package/dist/domain/spec/model/names/index.d.ts +9 -0
- package/dist/domain/spec/model/names/index.js +1 -0
- package/dist/domain/spec/model/names/instance-name.d.ts +7 -0
- package/dist/domain/spec/model/names/instance-name.js +1 -0
- package/dist/domain/spec/model/names/invariant-name.d.ts +3 -0
- package/dist/domain/spec/model/names/invariant-name.js +1 -0
- package/dist/domain/spec/model/names/shape-property-name.d.ts +11 -0
- package/dist/domain/spec/model/names/shape-property-name.js +1 -0
- package/dist/domain/spec/model/names/violation-type-name.d.ts +3 -0
- package/dist/domain/spec/model/names/violation-type-name.js +1 -0
- package/dist/domain/spec/model/presets.d.ts +30 -0
- package/dist/domain/spec/model/presets.js +1 -0
- package/dist/domain/spec/sections/actions.d.ts +23 -0
- package/dist/domain/spec/sections/actions.js +1 -0
- package/dist/domain/spec/sections/app-metadata.d.ts +18 -0
- package/dist/domain/spec/sections/app-metadata.js +1 -0
- package/dist/domain/spec/sections/application-version.d.ts +7 -0
- package/dist/domain/spec/sections/application-version.js +1 -0
- package/dist/domain/spec/sections/data-spec/chunks.d.ts +23 -0
- package/dist/domain/spec/sections/data-spec/chunks.js +1 -0
- package/dist/domain/spec/sections/data-spec/custom-data-spec-entry.d.ts +16 -0
- package/dist/domain/spec/sections/data-spec/custom-data-spec-entry.js +1 -0
- package/dist/domain/spec/sections/data-spec/data-entry-key.d.ts +9 -0
- package/dist/domain/spec/sections/data-spec/data-entry-key.js +1 -0
- package/dist/domain/spec/sections/data-spec/index.d.ts +5 -0
- package/dist/domain/spec/sections/data-spec/index.js +1 -0
- package/dist/domain/spec/sections/data-spec/parsed-data-spec-entries.d.ts +10 -0
- package/dist/domain/spec/sections/data-spec/parsed-data-spec-entries.js +1 -0
- package/dist/domain/spec/sections/data-spec/swatch-data-spec-entry.d.ts +14 -0
- package/dist/domain/spec/sections/data-spec/swatch-data-spec-entry.js +1 -0
- package/dist/domain/spec/sections/index.d.ts +9 -0
- package/dist/domain/spec/sections/index.js +1 -0
- package/dist/domain/spec/sections/infra/acl.d.ts +12 -0
- package/dist/domain/spec/sections/infra/acl.js +1 -0
- package/dist/domain/spec/sections/infra/index.d.ts +3 -0
- package/dist/domain/spec/sections/infra/index.js +1 -0
- package/dist/domain/spec/sections/infra/resource-entry.d.ts +46 -0
- package/dist/domain/spec/sections/infra/resource-entry.js +1 -0
- package/dist/domain/spec/sections/infra/resources-config.d.ts +10 -0
- package/dist/domain/spec/sections/infra/resources-config.js +1 -0
- package/dist/domain/spec/sections/state-value.d.ts +7 -0
- package/dist/domain/spec/sections/state-value.js +1 -0
- package/dist/domain/spec/sections/theme.d.ts +10 -0
- package/dist/domain/spec/sections/theme.js +1 -0
- package/dist/domain/spec/sections/transformers.d.ts +15 -0
- package/dist/domain/spec/sections/transformers.js +1 -0
- package/dist/domain/spec/sections/ui.d.ts +12 -0
- package/dist/domain/spec/sections/ui.js +1 -0
- package/dist/domain/system-components/application-config.d.ts +2 -2
- package/dist/domain/system-components/child-entity-selection.js +1 -1
- package/dist/domain/system-components/collision-box-geometry.d.ts +4 -2
- package/dist/domain/system-components/collision-geometry.d.ts +4 -2
- package/dist/domain/system-components/collision-point-geometry.d.ts +4 -2
- package/dist/domain/system-components/common/material-override-schema.d.ts +2 -2
- package/dist/domain/system-components/common/recipes.d.ts +21 -3
- package/dist/domain/system-components/common/recipes.js +1 -1
- package/dist/domain/system-components/common/scene-entity-control-schema.d.ts +3 -0
- package/dist/domain/system-components/common/scene-entity-control-schema.js +1 -1
- package/dist/domain/system-components/constraint.d.ts +10 -23
- package/dist/domain/system-components/drag-handle.d.ts +1 -1
- package/dist/domain/system-components/drag-handle.js +1 -1
- package/dist/domain/system-components/fit-entity-orthographic-camera-component.js +1 -1
- package/dist/domain/system-components/fit-entity-perspective-camera-component.js +1 -1
- package/dist/domain/system-components/fit-root-orthographic-camera-component.js +1 -1
- package/dist/domain/system-components/fit-root-perspective-camera-component.js +1 -1
- package/dist/domain/system-components/hotspot-drag-handle.d.ts +1 -1
- package/dist/domain/system-components/hotspot-drag-handle.js +1 -1
- package/dist/domain/system-components/material-swatch.js +1 -1
- package/dist/domain/system-components/scene-entity-material-override.d.ts +1 -1
- package/dist/domain/system-components/scene-entity-object-material-override.d.ts +1 -1
- package/dist/domain/system-components/scene-entity-object-text-decal.d.ts +1 -1
- package/dist/domain/system-components/scene-entity-object-transimissive-material-override.d.ts +1 -1
- package/dist/domain/system-components/scene-entity-transimissive-material-override.d.ts +1 -1
- package/dist/domain/system-components/static-orthographic-camera-component.js +1 -1
- package/dist/domain/system-components/static-perspective-camera-component.js +1 -1
- package/dist/domain/utils/eval-cache.d.ts +1 -1
- package/dist/domain/utils/map-nested-object.d.ts +1 -1
- package/dist/editor/Editor.d.ts +7 -0
- package/dist/editor/Editor.js +1 -1
- package/dist/editor/domain-editors/DomainSpecContext.js +1 -1
- package/dist/editor/domain-editors/apply-spec-update.d.ts +10 -2
- package/dist/editor/domain-editors/apply-spec-update.js +1 -1
- package/dist/editor/domain-editors/use-implementation-files.d.ts +7 -16
- package/dist/editor/domain-editors/use-implementation-files.js +1 -1
- package/dist/editor/graphql/GraphQLPanel.js +1 -1
- package/dist/editor/mocks/CreateMockYaml.js +2 -2
- package/dist/editor/mocks/EditorMockContext.d.ts +2 -0
- package/dist/editor/mocks/EditorMockContext.js +1 -1
- package/dist/editor/screenshot/ScreenshotHandler.js +1 -1
- package/dist/globals/index.d.ts +2 -0
- package/dist/i18n/locales/cs/messages.js +1 -1
- package/dist/i18n/locales/da/messages.js +1 -1
- package/dist/i18n/locales/de/messages.js +1 -1
- package/dist/i18n/locales/de-AT/messages.js +1 -1
- package/dist/i18n/locales/en/messages.js +1 -1
- package/dist/i18n/locales/en-AU/messages.js +1 -1
- package/dist/i18n/locales/en-GB/messages.js +1 -1
- package/dist/i18n/locales/es/messages.js +1 -1
- package/dist/i18n/locales/fr/messages.js +1 -1
- package/dist/i18n/locales/hu/messages.js +1 -1
- package/dist/i18n/locales/it/messages.js +1 -1
- package/dist/i18n/locales/nl/messages.js +1 -1
- package/dist/i18n/locales/pl/messages.js +1 -1
- package/dist/i18n/locales/pt/messages.js +1 -1
- package/dist/i18n/locales/ro/messages.js +1 -1
- package/dist/i18n/locales/sk/messages.js +1 -1
- package/dist/i18n/locales/tr/messages.js +1 -1
- package/dist/legacy/domain-cli/gen/configurator-builder.js +13 -12
- package/dist/legacy/domain-cli/gen/constant-expressions.js +1 -1
- package/dist/legacy/domain-cli/gen/defaults.js +1 -7
- package/dist/legacy/domain-cli/gen/invariants.js +3 -4
- package/dist/legacy/domain-cli/gen/relations.js +1 -1
- package/dist/legacy/domain-cli/utils/cache.d.ts +1 -1
- package/dist/legacy/domain-cli/utils/cache.js +1 -1
- package/dist/legacy/domain-spec/__generated/domain-schema.d.ts +176 -148
- package/dist/legacy/domain-spec/spec/domain-spec.js +1 -1
- package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/claude-md.d.ts +1 -1
- package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/cursor-rules.d.ts +1 -1
- package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/shared-sdk-content.d.ts +1 -1
- package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/shared-sdk-content.js +13 -1
- package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/domain-index.js +15 -13
- package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/frontend-admin.js +15 -14
- package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/frontend-configurator.js +14 -12
- package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/frontend-embedded-admin.js +14 -13
- package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/frontend-embedded-configurator.js +13 -11
- package/dist/phoenix/application-spec/application-spec-builder.d.ts +0 -2
- package/dist/phoenix/application-spec/application-spec-builder.js +1 -1
- package/dist/phoenix/application-spec/generate-application-spec-project.d.ts +1 -2
- package/dist/phoenix/application-spec/generate-application-spec-project.js +2 -2
- package/dist/phoenix/application-spec/generate-application-spec-story.js +2 -2
- package/dist/phoenix/application-spec/generate-project-files.d.ts +1 -2
- package/dist/phoenix/application-spec/index.d.ts +1 -0
- package/dist/phoenix/application-spec/index.js +1 -1
- package/dist/phoenix/application-spec/project-files.js +1 -1
- package/dist/phoenix/application-spec/resolve-deployment-data.d.ts +11 -0
- package/dist/phoenix/application-spec/resolve-deployment-data.js +2 -2
- package/dist/phoenix/bin.d.ts +2 -0
- package/dist/phoenix/bin.js +2 -0
- package/dist/phoenix/check-configs/built-in-eslint-config.d.ts +9 -0
- package/dist/phoenix/check-configs/built-in-eslint-config.js +1 -0
- package/dist/phoenix/check-configs/eslint-config.d.ts +10 -0
- package/dist/phoenix/check-configs/eslint-config.js +1 -0
- package/dist/phoenix/check-configs/index.d.ts +8 -0
- package/dist/phoenix/check-configs/index.js +1 -0
- package/dist/phoenix/check-configs/typescript-project.d.ts +5 -0
- package/dist/phoenix/check-configs/typescript-project.js +1 -0
- package/dist/phoenix/cli.d.ts +0 -1
- package/dist/phoenix/cli.js +1 -2
- package/dist/phoenix/commands/build.js +1 -1
- package/dist/phoenix/commands/check.js +1 -1
- package/dist/phoenix/commands/clean.js +1 -1
- package/dist/phoenix/commands/deploy.js +2 -2
- package/dist/phoenix/commands/deps/deps.d.ts +73 -0
- package/dist/phoenix/commands/deps/deps.js +3 -0
- package/dist/phoenix/commands/deps/index.d.ts +1 -0
- package/dist/phoenix/commands/deps/index.js +1 -0
- package/dist/phoenix/commands/deps/sdk-manifest.d.ts +12 -0
- package/dist/phoenix/commands/deps/sdk-manifest.js +1 -0
- package/dist/phoenix/commands/dev.js +5 -5
- package/dist/phoenix/commands/fix.js +1 -1
- package/dist/phoenix/commands/generated-env-markers.d.ts +1 -0
- package/dist/phoenix/commands/generated-env-markers.js +1 -1
- package/dist/phoenix/commands/index.d.ts +1 -0
- package/dist/phoenix/commands/index.js +1 -1
- package/dist/phoenix/commands/init.d.ts +17 -5
- package/dist/phoenix/commands/init.js +1 -1
- package/dist/phoenix/commands/link.js +1 -1
- package/dist/phoenix/commands/prettier.js +1 -1
- package/dist/phoenix/commands/sync.js +1 -1
- package/dist/phoenix/commands/unlink.js +1 -1
- package/dist/phoenix/lingui/index.d.ts +1 -0
- package/dist/phoenix/lingui/index.js +1 -0
- package/dist/phoenix/lingui/lingui.d.ts +18 -0
- package/dist/phoenix/lingui/lingui.js +1 -0
- package/dist/phoenix/services/application-spec-project-watcher.d.ts +5 -3
- package/dist/phoenix/services/application-spec-project-watcher.js +3 -3
- package/dist/phoenix/services/index.d.ts +1 -0
- package/dist/phoenix/services/index.js +1 -1
- package/dist/phoenix/services/lingui-watcher.js +1 -1
- package/dist/phoenix/services/prepare-development-environment.js +1 -1
- package/dist/phoenix/services/source-archive.d.ts +1 -0
- package/dist/phoenix/services/source-archive.js +1 -1
- package/dist/phoenix/services/swc.js +1 -1
- package/dist/phoenix/services/vite-config.js +1 -1
- package/dist/phoenix/utils/git/index.d.ts +1 -0
- package/dist/phoenix/utils/git/index.js +1 -0
- package/dist/phoenix/utils/git/is-inside-git-repository.d.ts +6 -0
- package/dist/phoenix/utils/git/is-inside-git-repository.js +1 -0
- package/dist/phoenix/utils/index.d.ts +5 -2
- package/dist/phoenix/utils/index.js +1 -1
- package/dist/phoenix/utils/node-version/index.d.ts +1 -0
- package/dist/phoenix/utils/node-version/index.js +1 -0
- package/dist/phoenix/utils/node-version/supported-node-version.d.ts +12 -0
- package/dist/phoenix/utils/node-version/supported-node-version.js +4 -0
- package/dist/phoenix/utils/package-info.d.ts +0 -9
- package/dist/phoenix/utils/package-info.js +1 -1
- package/dist/phoenix/utils/package-manager/index.d.ts +2 -0
- package/dist/phoenix/utils/package-manager/index.js +1 -0
- package/dist/phoenix/utils/package-manager/package-manager-commands.d.ts +19 -0
- package/dist/phoenix/utils/package-manager/package-manager-commands.js +1 -0
- package/dist/phoenix/utils/package-manager/pnpm-project.d.ts +9 -0
- package/dist/phoenix/utils/package-manager/pnpm-project.js +1 -0
- package/dist/phoenix/utils/run-command.d.ts +11 -0
- package/dist/phoenix/utils/run-command.js +1 -1
- package/dist/phoenix/utils/sdk-tool/index.d.ts +1 -0
- package/dist/phoenix/utils/sdk-tool/index.js +1 -0
- package/dist/phoenix/utils/sdk-tool/resolve-sdk-tool-bin.d.ts +15 -0
- package/dist/phoenix/utils/sdk-tool/resolve-sdk-tool-bin.js +1 -0
- package/dist/react-sdk/application-runtime/common-services/analytics/analytics-service.d.ts +1 -1
- package/dist/{configurator/common → react-sdk/application-runtime/common-services/analytics}/frontend-analytics.d.ts +6 -0
- package/dist/react-sdk/application-runtime/common-services/analytics/frontend-analytics.js +1 -0
- package/dist/react-sdk/application-runtime/common-services/analytics/index.d.ts +1 -0
- package/dist/react-sdk/application-runtime/common-services/analytics/index.js +1 -1
- package/dist/react-sdk/application-runtime/common-services/initialize.d.ts +2 -2
- package/dist/react-sdk/application-runtime/domain-application-context/chat-system.d.ts +1 -1
- package/dist/react-sdk/application-runtime/hooks/index.d.ts +2 -0
- package/dist/react-sdk/application-runtime/hooks/index.js +1 -1
- package/dist/react-sdk/application-runtime/hooks/loading/index.d.ts +5 -0
- package/dist/react-sdk/application-runtime/hooks/loading/index.js +1 -0
- package/dist/react-sdk/application-runtime/hooks/loading/loader-host-context.d.ts +11 -0
- package/dist/react-sdk/application-runtime/hooks/loading/loader-host-context.js +1 -0
- package/dist/react-sdk/application-runtime/hooks/loading/use-is-awaiting-loading-phase.d.ts +8 -0
- package/dist/react-sdk/application-runtime/hooks/loading/use-is-awaiting-loading-phase.js +1 -0
- package/dist/react-sdk/application-runtime/hooks/loading/use-is-loader-hosted.d.ts +7 -0
- package/dist/react-sdk/application-runtime/hooks/loading/use-is-loader-hosted.js +1 -0
- package/dist/react-sdk/application-runtime/hooks/loading/use-loading-actions.d.ts +9 -0
- package/dist/react-sdk/application-runtime/hooks/loading/use-loading-actions.js +1 -0
- package/dist/react-sdk/application-runtime/hooks/loading/use-loading-application.d.ts +24 -0
- package/dist/react-sdk/application-runtime/hooks/loading/use-loading-application.js +1 -0
- package/dist/react-sdk/application-runtime/hooks/responsive/index.d.ts +3 -0
- package/dist/react-sdk/application-runtime/hooks/responsive/index.js +1 -0
- package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-actions.d.ts +9 -0
- package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-actions.js +1 -0
- package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-application.d.ts +26 -0
- package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-application.js +1 -0
- package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-state.d.ts +10 -0
- package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-state.js +1 -0
- package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-store.d.ts +24 -0
- package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-store.js +1 -0
- package/dist/react-sdk/application-runtime/hooks/use-access-service.d.ts +7 -0
- package/dist/react-sdk/application-runtime/hooks/use-access-service.js +1 -1
- package/dist/react-sdk/application-runtime/hooks/use-phoenix-element-slots.d.ts +2 -1
- package/dist/react-sdk/application-runtime/hooks/use-web-component-readonly-sync.d.ts +4 -11
- package/dist/react-sdk/application-runtime/hooks/use-web-component-readonly-sync.js +1 -1
- package/dist/react-sdk/application-runtime/hooks/use-web-component.d.ts +5 -0
- package/dist/react-sdk/application-runtime/hooks/use-web-component.js +1 -1
- package/dist/react-sdk/application-runtime/host-properties/actions.d.ts +15 -0
- package/dist/react-sdk/application-runtime/host-properties/actions.js +1 -0
- package/dist/react-sdk/application-runtime/host-properties/connect-host-properties.d.ts +30 -0
- package/dist/react-sdk/application-runtime/host-properties/connect-host-properties.js +1 -0
- package/dist/react-sdk/application-runtime/host-properties/domain-host-properties.d.ts +7 -0
- package/dist/react-sdk/application-runtime/host-properties/domain-host-properties.js +1 -0
- package/dist/react-sdk/application-runtime/host-properties/host-property-descriptors.d.ts +21 -0
- package/dist/react-sdk/application-runtime/host-properties/host-property-descriptors.js +1 -0
- package/dist/react-sdk/application-runtime/host-properties/host-property-scopes.d.ts +42 -0
- package/dist/react-sdk/application-runtime/host-properties/host-property-scopes.js +1 -0
- package/dist/react-sdk/application-runtime/host-properties/index.d.ts +8 -0
- package/dist/react-sdk/application-runtime/host-properties/index.js +1 -0
- package/dist/react-sdk/application-runtime/host-properties/initialize.d.ts +7 -0
- package/dist/react-sdk/application-runtime/host-properties/initialize.js +1 -0
- package/dist/react-sdk/application-runtime/host-properties/selectors.d.ts +54 -0
- package/dist/react-sdk/application-runtime/host-properties/selectors.js +1 -0
- package/dist/react-sdk/application-runtime/host-properties/types.d.ts +18 -0
- package/dist/react-sdk/application-runtime/host-properties/types.js +1 -0
- package/dist/react-sdk/application-runtime/index.d.ts +3 -0
- package/dist/react-sdk/application-runtime/integration/initialize.d.ts +7 -6
- package/dist/react-sdk/application-runtime/integration/initialize.js +1 -1
- package/dist/react-sdk/application-runtime/integration/selectors.d.ts +11 -2
- package/dist/react-sdk/application-runtime/integration/selectors.js +1 -1
- package/dist/react-sdk/application-runtime/integration/types.d.ts +5 -2
- package/dist/react-sdk/application-runtime/loading/actions.d.ts +45 -0
- package/dist/react-sdk/application-runtime/loading/actions.js +1 -0
- package/dist/react-sdk/application-runtime/loading/index.d.ts +4 -0
- package/dist/react-sdk/application-runtime/loading/index.js +1 -0
- package/dist/react-sdk/application-runtime/loading/initialize.d.ts +3 -0
- package/dist/react-sdk/application-runtime/loading/initialize.js +1 -0
- package/dist/react-sdk/application-runtime/loading/selectors.d.ts +18 -0
- package/dist/react-sdk/application-runtime/loading/selectors.js +1 -0
- package/dist/react-sdk/application-runtime/loading/types.d.ts +59 -0
- package/dist/react-sdk/application-runtime/loading/types.js +1 -0
- package/dist/react-sdk/application-runtime/mocks/create-mock-application-runtime.js +1 -1
- package/dist/react-sdk/application-runtime/provider/ApplicationRuntime.d.ts +6 -0
- package/dist/react-sdk/application-runtime/provider/ApplicationRuntime.js +1 -1
- package/dist/react-sdk/application-runtime/provider/ApplicationType.d.ts +7 -0
- package/dist/react-sdk/application-runtime/provider/ApplicationType.js +1 -1
- package/dist/react-sdk/application-runtime/responsive/actions.d.ts +47 -0
- package/dist/react-sdk/application-runtime/responsive/actions.js +1 -0
- package/dist/react-sdk/application-runtime/responsive/index.d.ts +4 -0
- package/dist/react-sdk/application-runtime/responsive/index.js +1 -0
- package/dist/react-sdk/application-runtime/responsive/initialize.d.ts +3 -0
- package/dist/react-sdk/application-runtime/responsive/initialize.js +1 -0
- package/dist/react-sdk/application-runtime/responsive/selectors.d.ts +20 -0
- package/dist/react-sdk/application-runtime/responsive/selectors.js +1 -0
- package/dist/react-sdk/application-runtime/responsive/types.d.ts +104 -0
- package/dist/react-sdk/application-runtime/responsive/types.js +1 -0
- package/dist/react-sdk/application-runtime/routing/access-service.d.ts +1 -0
- package/dist/react-sdk/application-runtime/routing/routing-service.d.ts +34 -2
- package/dist/react-sdk/application-runtime/routing/routing-service.js +1 -1
- package/dist/react-sdk/application-runtime/runtime/create-application-runtime.d.ts +346 -26
- package/dist/react-sdk/application-runtime/runtime/create-application-runtime.js +1 -1
- package/dist/{configurator/configurator-context → react-sdk}/configurator-routing/ConfiguratorRoutingProvider.d.ts +1 -1
- package/dist/react-sdk/configurator-routing/ConfiguratorRoutingProvider.js +1 -0
- package/dist/{configurator/configurator-context → react-sdk}/configurator-routing/OnGoToIndexHandler.d.ts +1 -2
- package/dist/react-sdk/configurator-routing/OnGoToIndexHandler.js +1 -0
- package/dist/react-sdk/configurator-routing/index.d.ts +3 -0
- package/dist/{configurator/configurator-context → react-sdk}/configurator-routing/index.js +1 -1
- package/dist/{configurator/configurator-context → react-sdk}/configurator-routing/use-power-routing-lifecycle.d.ts +2 -0
- package/dist/react-sdk/configurator-routing/use-power-routing-lifecycle.js +1 -0
- package/dist/react-sdk/domain-application-context/BackendDataProvider.js +1 -1
- package/dist/react-sdk/domain-application-context/ConfigurationPersistSystemFailureHandler.js +1 -1
- package/dist/react-sdk/domain-application-context/DomainLifecycleBoundary.js +1 -1
- package/dist/react-sdk/domain-application-context/FrontendDataProvider.js +1 -1
- package/dist/react-sdk/domain-application-context/OnPresetLoadedHandler.js +1 -0
- package/dist/react-sdk/domain-application-context/PublishedDataProvider.js +1 -1
- package/dist/react-sdk/domain-application-context/chat-hooks.d.ts +4 -0
- package/dist/react-sdk/domain-application-context/chat-hooks.js +1 -0
- package/dist/react-sdk/domain-application-context/index.d.ts +2 -0
- package/dist/react-sdk/domain-application-context/index.js +1 -1
- package/dist/react-sdk/domain-context/common-hooks/index.d.ts +9 -0
- package/dist/react-sdk/domain-context/common-hooks/index.js +1 -0
- package/dist/react-sdk/domain-context/common-hooks/use-configuration-save-state.d.ts +23 -0
- package/dist/react-sdk/domain-context/common-hooks/use-configuration-save-state.js +1 -0
- package/dist/react-sdk/domain-context/common-hooks/use-configurator-messages.d.ts +41 -0
- package/dist/react-sdk/domain-context/common-hooks/use-configurator-messages.js +1 -0
- package/dist/react-sdk/domain-context/common-hooks/use-domain-context.d.ts +5 -4
- package/dist/react-sdk/domain-context/component-hooks/index.d.ts +1 -0
- package/dist/react-sdk/domain-context/component-hooks/index.js +1 -0
- package/dist/react-sdk/domain-context/component-hooks/use-component.js +1 -1
- package/dist/react-sdk/domain-context/entity-hooks/use-domain-entity-state.js +1 -1
- package/dist/react-sdk/domain-context/entity-hooks/use-entity-price.js +1 -1
- package/dist/react-sdk/domain-context/entity-hooks/use-root-entity-id.js +1 -1
- package/dist/react-sdk/domain-context/entity-hooks/use-root-entity-state.js +1 -1
- package/dist/react-sdk/domain-context/index.d.ts +9 -1
- package/dist/react-sdk/domain-context/index.js +1 -1
- package/dist/react-sdk/domain-context/system-hooks/composition-hooks.js +1 -1
- package/dist/react-sdk/domain-context/system-hooks/dimension-hooks.js +1 -1
- package/dist/react-sdk/domain-context/system-hooks/generate-share-link-hooks.d.ts +8 -0
- package/dist/react-sdk/domain-context/system-hooks/generate-share-link-hooks.js +1 -0
- package/dist/react-sdk/domain-context/system-hooks/hover-hooks.js +1 -1
- package/dist/react-sdk/domain-context/system-hooks/locale-switch-hooks.d.ts +24 -0
- package/dist/react-sdk/domain-context/system-hooks/locale-switch-hooks.js +1 -0
- package/dist/react-sdk/domain-context/system-hooks/navigation-hooks.d.ts +11 -0
- package/dist/react-sdk/domain-context/system-hooks/navigation-hooks.js +1 -1
- package/dist/react-sdk/domain-context/system-hooks/selection-hooks.js +1 -1
- package/dist/react-sdk/domain-context/system-hooks/share-links-hooks.d.ts +32 -0
- package/dist/react-sdk/domain-context/system-hooks/share-links-hooks.js +1 -0
- package/dist/react-sdk/domain-context/system-hooks/step-navigation-hooks.d.ts +57 -0
- package/dist/react-sdk/domain-context/system-hooks/step-navigation-hooks.js +1 -0
- package/dist/react-sdk/domain-context/system-hooks/tab-navigation-hooks.d.ts +61 -0
- package/dist/react-sdk/domain-context/system-hooks/tab-navigation-hooks.js +1 -0
- package/dist/react-sdk/domain-context/system-hooks/undo-redo-hooks.d.ts +20 -0
- package/dist/react-sdk/domain-context/system-hooks/undo-redo-hooks.js +1 -0
- package/dist/react-sdk/domain-controls/hooks/use-angle-dimension.d.ts +1 -1
- package/dist/react-sdk/handlers/FinalizeActionRunner.d.ts +13 -0
- package/dist/react-sdk/handlers/FinalizeActionRunner.js +1 -0
- package/dist/react-sdk/handlers/FinalizeActionsHost.d.ts +10 -0
- package/dist/react-sdk/handlers/FinalizeActionsHost.js +1 -0
- package/dist/react-sdk/handlers/FinalizeActionsProvider.d.ts +11 -0
- package/dist/react-sdk/handlers/FinalizeActionsProvider.js +1 -0
- package/dist/react-sdk/handlers/finalize-actions-context.d.ts +6 -0
- package/dist/react-sdk/handlers/finalize-actions-context.js +1 -0
- package/dist/react-sdk/handlers/finalize-actions-store.d.ts +23 -0
- package/dist/react-sdk/handlers/finalize-actions-store.js +1 -0
- package/dist/react-sdk/handlers/index.d.ts +6 -0
- package/dist/react-sdk/handlers/index.js +1 -1
- package/dist/react-sdk/handlers/use-finalize-action.d.ts +32 -0
- package/dist/react-sdk/handlers/use-finalize-action.js +1 -0
- package/dist/react-sdk/handlers/use-finalize-actions.d.ts +53 -0
- package/dist/react-sdk/handlers/use-finalize-actions.js +1 -0
- package/dist/react-sdk/index.d.ts +6 -0
- package/dist/react-sdk/index.js +1 -1
- package/dist/react-sdk/integration/IntegrationSubscription.js +1 -0
- package/dist/react-sdk/integration/index.d.ts +1 -0
- package/dist/react-sdk/integration/index.js +1 -0
- package/dist/react-sdk/lifecycle-providers/ConfiguratorDataProvider.d.ts +8 -0
- package/dist/react-sdk/lifecycle-providers/ConfiguratorDataProvider.js +1 -0
- package/dist/react-sdk/lifecycle-providers/LifecycleProviders.js +1 -0
- package/dist/react-sdk/lifecycle-providers/index.d.ts +2 -0
- package/dist/react-sdk/lifecycle-providers/index.js +1 -0
- package/dist/react-sdk/loading-state/LoaderHost.d.ts +20 -0
- package/dist/react-sdk/loading-state/LoaderHost.js +1 -0
- package/dist/react-sdk/loading-state/built-in-loading-phase.d.ts +9 -0
- package/dist/react-sdk/loading-state/built-in-loading-phase.js +1 -0
- package/dist/react-sdk/loading-state/create-app-loader.d.ts +18 -0
- package/dist/react-sdk/loading-state/create-app-loader.js +1 -0
- package/dist/react-sdk/loading-state/index.d.ts +4 -2
- package/dist/react-sdk/loading-state/index.js +1 -1
- package/dist/react-sdk/loading-state/{LoadingStateProvider.d.ts → loading-state-hooks.d.ts} +3 -12
- package/dist/react-sdk/loading-state/loading-state-hooks.js +1 -0
- package/dist/react-sdk/notifications/index.d.ts +1 -0
- package/dist/react-sdk/notifications/index.js +1 -0
- package/dist/react-sdk/notifications/snackbar-action-button.d.ts +30 -0
- package/dist/react-sdk/notifications/snackbar-action-button.js +1 -0
- package/dist/react-sdk/page-elements/ConfigurationNotFoundPagePropsProvider.d.ts +17 -0
- package/dist/react-sdk/page-elements/ConfigurationNotFoundPagePropsProvider.js +1 -0
- package/dist/react-sdk/page-elements/ConfiguratorErrorPagePropsProvider.d.ts +17 -0
- package/dist/react-sdk/page-elements/ConfiguratorErrorPagePropsProvider.js +1 -0
- package/dist/react-sdk/page-elements/configurator-error-page-props.d.ts +20 -0
- package/dist/react-sdk/page-elements/configurator-error-page-props.js +1 -0
- package/dist/react-sdk/page-elements/create-configuration-not-found-page.d.ts +15 -0
- package/dist/react-sdk/page-elements/create-configuration-not-found-page.js +1 -0
- package/dist/react-sdk/page-elements/create-configurator-error-page.d.ts +14 -0
- package/dist/react-sdk/page-elements/create-configurator-error-page.js +1 -0
- package/dist/react-sdk/page-elements/create-preset-ui.d.ts +9 -0
- package/dist/react-sdk/page-elements/create-preset-ui.js +1 -0
- package/dist/react-sdk/page-elements/index.d.ts +6 -0
- package/dist/react-sdk/page-elements/index.js +1 -0
- package/dist/react-sdk/scene-control-elements/AddEntityFabElement.d.ts +36 -0
- package/dist/react-sdk/scene-control-elements/AddEntityFabElement.js +1 -0
- package/dist/react-sdk/scene-control-elements/DeleteEntityFabElement.d.ts +44 -0
- package/dist/react-sdk/scene-control-elements/DeleteEntityFabElement.js +1 -0
- package/dist/react-sdk/scene-control-elements/NavigationHotspotElement.d.ts +38 -0
- package/dist/react-sdk/scene-control-elements/NavigationHotspotElement.js +1 -0
- package/dist/react-sdk/scene-control-elements/SelectionHotspotElement.d.ts +30 -0
- package/dist/react-sdk/scene-control-elements/SelectionHotspotElement.js +1 -0
- package/dist/react-sdk/scene-control-elements/ToolMenuFabElement.d.ts +30 -0
- package/dist/react-sdk/scene-control-elements/ToolMenuFabElement.js +1 -0
- package/dist/react-sdk/scene-control-elements/create-add-entity-fab.d.ts +9 -0
- package/dist/react-sdk/scene-control-elements/create-add-entity-fab.js +1 -0
- package/dist/react-sdk/scene-control-elements/create-delete-entity-fab.d.ts +9 -0
- package/dist/react-sdk/scene-control-elements/create-delete-entity-fab.js +1 -0
- package/dist/react-sdk/scene-control-elements/create-navigation-hotspot.d.ts +10 -0
- package/dist/react-sdk/scene-control-elements/create-navigation-hotspot.js +1 -0
- package/dist/react-sdk/scene-control-elements/create-selection-hotspot.d.ts +9 -0
- package/dist/react-sdk/scene-control-elements/create-selection-hotspot.js +1 -0
- package/dist/react-sdk/scene-control-elements/create-tool-menu-fab.d.ts +9 -0
- package/dist/react-sdk/scene-control-elements/create-tool-menu-fab.js +1 -0
- package/dist/react-sdk/scene-control-elements/index.d.ts +11 -0
- package/dist/react-sdk/scene-control-elements/index.js +1 -0
- package/dist/react-sdk/scene-control-elements/scene-control-tooltip-visibility.d.ts +5 -0
- package/dist/react-sdk/scene-control-elements/scene-control-tooltip-visibility.js +1 -0
- package/dist/react-sdk/session/NotAuthenticatedPage.d.ts +4 -0
- package/dist/react-sdk/session/NotAuthenticatedPage.js +1 -1
- package/dist/react-sdk/session/SessionServiceProvider.js +1 -1
- package/dist/react-sdk/web-component/create-web-component.js +1 -1
- package/dist/react-sdk/web-component/god-mode/god-mode-registry.d.ts +30 -0
- package/dist/react-sdk/web-component/god-mode/god-mode-registry.js +1 -0
- package/dist/react-sdk/web-component/god-mode/index.d.ts +1 -0
- package/dist/react-sdk/web-component/god-mode/index.js +1 -0
- package/dist/ui-sdk/components/snackbar/Snackbar.d.ts +24 -3
- package/dist/ui-sdk/components/snackbar/Snackbar.js +1 -1
- package/dist/ui-sdk/icons/__generated/Lightbulb.d.ts +3 -0
- package/dist/ui-sdk/icons/__generated/Lightbulb.js +1 -0
- package/dist/ui-sdk/icons/__generated/XCircle.d.ts +3 -0
- package/dist/ui-sdk/icons/__generated/XCircle.js +1 -0
- package/dist/ui-sdk/icons/__generated/index.d.ts +4 -0
- package/dist/ui-sdk/icons/__generated/index.js +1 -1
- package/dist/ui-sdk/icons/icons-registry.js +1 -1
- package/dist/ui-sdk/responsive-application/ResponsiveApplicationProvider.d.ts +9 -0
- package/dist/ui-sdk/responsive-application/ResponsiveApplicationProvider.js +1 -1
- package/dist/ui-sdk/responsive-application/index.d.ts +1 -0
- package/dist/ui-sdk/responsive-application/responsive-state-store.d.ts +59 -0
- package/dist/ui-sdk/responsive-application/responsive-state-store.js +1 -0
- package/dist/ui-sdk/responsive-application/use-store-responsive-info.d.ts +7 -0
- package/dist/ui-sdk/responsive-application/use-store-responsive-info.js +1 -0
- package/dist/ui-sdk/theme/types.d.ts +5 -1
- package/dist/watch/glob-matcher.d.ts +4 -4
- package/dist/watch/glob-matcher.js +1 -1
- package/dist/watch/watch.d.ts +4 -4
- package/dist/watch/watch.js +1 -1
- package/dist/webhook/builders/webhook-builder.js +1 -1
- package/package.json +58 -36
- package/schema/domain-spec.json +1341 -217
- package/dist/application-spec/generate-application/code/templates/nvmrc.d.ts +0 -1
- package/dist/application-spec/generate-application/code/templates/nvmrc.js +0 -2
- package/dist/application-spec/generate-application/code/templates/package-json.d.ts +0 -10
- package/dist/application-spec/generate-application/code/templates/package-json.js +0 -1
- package/dist/application-spec/generate-application/code/templates/prettier-ignore.d.ts +0 -1
- package/dist/application-spec/generate-application/code/templates/prettier-ignore.js +0 -6
- package/dist/application-spec/generate-application/code/templates/webhook-implementation.d.ts +0 -1
- package/dist/application-spec/generate-application/code/templates/webhook-implementation.js +0 -7
- package/dist/configurator/common/frontend-analytics.js +0 -1
- package/dist/configurator/configurator-context/ConfiguratorDataProvider.d.ts +0 -2
- package/dist/configurator/configurator-context/ConfiguratorDataProvider.js +0 -1
- package/dist/configurator/configurator-context/IntegrationSubscription.js +0 -1
- package/dist/configurator/configurator-context/LifecycleProviders.js +0 -1
- package/dist/configurator/configurator-context/OnPresetLoadedHandler.js +0 -1
- package/dist/configurator/configurator-context/configurator-hooks/chat-hooks.d.ts +0 -1
- package/dist/configurator/configurator-context/configurator-hooks/chat-hooks.js +0 -1
- package/dist/configurator/configurator-context/configurator-routing/ConfiguratorRoutingProvider.js +0 -1
- package/dist/configurator/configurator-context/configurator-routing/OnGoToIndexHandler.js +0 -1
- package/dist/configurator/configurator-context/configurator-routing/index.d.ts +0 -2
- package/dist/configurator/configurator-context/configurator-routing/use-power-routing-lifecycle.js +0 -1
- package/dist/configurator/ui/phoenix-layout-elements/language/use-locale-options.d.ts +0 -4
- package/dist/configurator/ui/phoenix-layout-elements/language/use-locale-options.js +0 -1
- package/dist/configurator/ui/phoenix-layout-elements/measurements/use-measurements-visibility.d.ts +0 -4
- package/dist/configurator/ui/phoenix-layout-elements/measurements/use-measurements-visibility.js +0 -1
- package/dist/domain/application/resource-builder.d.ts +0 -13
- package/dist/domain/application/resource-builder.js +0 -1
- package/dist/domain/core/relation/relation-implementation.d.ts +0 -7
- package/dist/domain/domain-runtime/spec/invariant-builder.d.ts +0 -26
- package/dist/domain/domain-runtime/spec/invariant-builder.js +0 -1
- package/dist/domain/extensions/client-side-image.d.ts +0 -14
- package/dist/domain/extensions/client-side-image.js +0 -1
- package/dist/domain/extensions/save-template-thumbnail.js +0 -1
- package/dist/phoenix/commands/generate-project-files.d.ts +0 -6
- package/dist/phoenix/commands/generate-project-files.js +0 -1
- package/dist/phoenix/services/lingui.d.ts +0 -7
- package/dist/phoenix/services/lingui.js +0 -1
- package/dist/react-sdk/application-runtime/runtime/god-mode.d.ts +0 -19
- package/dist/react-sdk/application-runtime/runtime/god-mode.js +0 -1
- package/dist/react-sdk/loading-state/LoadingStateProvider.js +0 -1
- package/dist/react-sdk/loading-state/loading-state-manager.d.ts +0 -32
- package/dist/react-sdk/loading-state/loading-state-manager.js +0 -1
- /package/dist/application-spec/{core → enhanced-json}/eval-code-uses-self.d.ts +0 -0
- /package/dist/application-spec/{core → enhanced-json}/eval-code-uses-self.js +0 -0
- /package/dist/application-spec/{core → enhanced-json}/eval-to-constant.js +0 -0
- /package/dist/{domain/core/relation/relation-implementation.js → application-spec/generate-application/runtime/spec-code/spec-code-module.js} +0 -0
- /package/dist/{application-spec/core → domain/spec/application}/application-instance.d.ts +0 -0
- /package/dist/{application-spec/core → domain/spec/application}/application-instance.js +0 -0
- /package/dist/{application-spec/parse-application-files/spec/components/component-entry.js → domain/spec/components/sdk-code-ref.js} +0 -0
- /package/dist/{application-spec/parse-application-files/spec/expressions/expression.js → domain/spec/expressions/sdk-expression-implementation.js} +0 -0
- /package/dist/{application-spec/parse-application-files/spec/components → domain/spec/markers}/component-prop-target.d.ts +0 -0
- /package/dist/{application-spec/parse-application-files/spec/components → domain/spec/markers}/component-prop-target.js +0 -0
- /package/dist/{application-spec/core → domain/spec}/markers/eval-marker.d.ts +0 -0
- /package/dist/{application-spec/core → domain/spec}/markers/eval-marker.js +0 -0
- /package/dist/{configurator/configurator-context → react-sdk/domain-application-context}/OnPresetLoadedHandler.d.ts +0 -0
- /package/dist/{configurator/configurator-context → react-sdk/integration}/IntegrationSubscription.d.ts +0 -0
- /package/dist/{configurator/configurator-context → react-sdk/lifecycle-providers}/LifecycleProviders.d.ts +0 -0
|
@@ -0,0 +1,3698 @@
|
|
|
1
|
+
# Nightjar Guitar
|
|
2
|
+
|
|
3
|
+
Walk a customer through a product one step at a time and end in an order. A custom guitar shop, Northfret, sells its Nightjar electric guitar through a five-step wizard: the body, the neck, the electronics, a signature on the headstock and a summary with **Place order**. A stepper across the top shows where the customer is, each step's choices slide in beside the guitar, the camera moves to what the step changes, and a bar at the bottom keeps the price, **Back** and **Next** in reach. The project writes the wizard as one layout element; the spec holds the steps, which step asks each choice, the cameras, the rules and the order action.
|
|
4
|
+
|
|
5
|
+
- [Try it](#try-it)
|
|
6
|
+
- [What the example demonstrates](#what-the-example-demonstrates)
|
|
7
|
+
- [Steps from the spec](#steps-from-the-spec)
|
|
8
|
+
- [Each step shows the choices the spec gives it](#each-step-shows-the-choices-the-spec-gives-it)
|
|
9
|
+
- [The camera follows the wizard](#the-camera-follows-the-wizard)
|
|
10
|
+
- [Finish swatches](#finish-swatches)
|
|
11
|
+
- [A signature on the headstock](#a-signature-on-the-headstock)
|
|
12
|
+
- [Summary and conflicts](#summary-and-conflicts)
|
|
13
|
+
- [Place the order](#place-the-order)
|
|
14
|
+
- [Pages that slide in](#pages-that-slide-in)
|
|
15
|
+
- [On a phone](#on-a-phone)
|
|
16
|
+
- [The stage and the model](#the-stage-and-the-model)
|
|
17
|
+
- [Test cases](#test-cases)
|
|
18
|
+
- [What this layout leaves out](#what-this-layout-leaves-out)
|
|
19
|
+
- [Related pages](#related-pages)
|
|
20
|
+
- [Complete Implementation](#complete-implementation)
|
|
21
|
+
|
|
22
|
+
## Try it
|
|
23
|
+
|
|
24
|
+
The phone version is under [On a phone](#on-a-phone).
|
|
25
|
+
|
|
26
|
+
## What the example demonstrates
|
|
27
|
+
|
|
28
|
+
[Custom Layout](?path=/docs/layouts-custom-layout--docs) explains how a project layout works: the element name, the scene, the safe area and the hooks. This page is a finished design built the same way, for a product sold in ordered steps, and it shows the pieces Custom Layout does not use:
|
|
29
|
+
|
|
30
|
+
| Piece | Where |
|
|
31
|
+
| ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- |
|
|
32
|
+
| `phx.frontend.useStepNavigation()` for a stepper and **Back** and **Next** from the spec's `navigationSteps` | `StepHeader`, `OrderBar`, [Steps from the spec](#steps-from-the-spec) |
|
|
33
|
+
| A step's fields from the components a `$filter` leaves active, read with `phx.frontend.useComponents()` | `use-active-choices`, [Each step shows…](#each-step-shows-the-choices-the-spec-gives-it) |
|
|
34
|
+
| Cameras filtered by navigation state, so the camera follows the wizard with no camera code | the spec, [The camera follows the wizard](#the-camera-follows-the-wizard) |
|
|
35
|
+
| `phx.frontend.useSwatchSelection()` for a `materialSwatch` | `FinishField`, [Finish swatches](#finish-swatches) |
|
|
36
|
+
| `phx.frontend.useTextValue()` with a `sceneEntityObjectTextDecal`, and a `fixable` rule on the text | `SignatureField`, [A signature on the headstock](#a-signature-on-the-headstock) |
|
|
37
|
+
| A summary read with the control hooks, and warnings from `phx.frontend.useFinalizeViolations()` | `SummaryPanel`, `SummaryRows`, [Summary and conflicts](#summary-and-conflicts) |
|
|
38
|
+
| `phx.frontend.useFinalizeActions()` with its pending state and error, and an `ordered` confirmation | `OrderBar`, `SummaryPanel`, [Place the order](#place-the-order) |
|
|
39
|
+
| `phx.frontend.useSceneSafeArea().update()` once a page has slid in | `DesktopPage`, `PhonePage`, [Pages that slide in](#pages-that-slide-in) |
|
|
40
|
+
| `phx.frontend.useAnalytics()` with `FrontendAnalyticsFactory.SummaryButtonClicked()` | `OrderBar`, [Steps from the spec](#steps-from-the-spec) |
|
|
41
|
+
| `sceneEntityObjectLocalMaterialOverride` with a clearcoat, and a `canvasCustomEnvironment` stage | the spec, `NightjarStage`, [The stage and the model](#the-stage-and-the-model) |
|
|
42
|
+
|
|
43
|
+
## Steps from the spec
|
|
44
|
+
|
|
45
|
+
The steps are product structure, so they live in the spec. Each navigation state is a page of the wizard; `navigationSteps` groups them into the steps the stepper shows, with their labels and their order. The summary step holds two states: `summary`, and `ordered` for the confirmation after the order.
|
|
46
|
+
|
|
47
|
+
```yaml
|
|
48
|
+
navigationStates:
|
|
49
|
+
- body
|
|
50
|
+
- neck
|
|
51
|
+
- electronics
|
|
52
|
+
- details
|
|
53
|
+
- summary
|
|
54
|
+
- ordered
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```yaml
|
|
58
|
+
components:
|
|
59
|
+
Guitar:
|
|
60
|
+
navigationSteps:
|
|
61
|
+
main:
|
|
62
|
+
steps:
|
|
63
|
+
- label: Body
|
|
64
|
+
navigationStates:
|
|
65
|
+
- body
|
|
66
|
+
- label: Neck
|
|
67
|
+
navigationStates:
|
|
68
|
+
- neck
|
|
69
|
+
- label: Electronics
|
|
70
|
+
navigationStates:
|
|
71
|
+
- electronics
|
|
72
|
+
- label: Details
|
|
73
|
+
navigationStates:
|
|
74
|
+
- details
|
|
75
|
+
- label: Summary
|
|
76
|
+
navigationStates:
|
|
77
|
+
- summary
|
|
78
|
+
- ordered
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`phx.frontend.useStepNavigation()` reads them and returns every step with its label resolved and its `status`, `'complete'`, `'active'` or `'upcoming'`, with `next()`, `previous()` and `goTo(index)` to move. `StepHeader` draws the stepper from `steps`, and `OrderBar` the buttons from `canGoPrevious`, `isLast` and the label of the step after the active one:
|
|
82
|
+
|
|
83
|
+
- **A wizard that remembers.** The hook enforces no order: `goTo` enters any step. `StepHeader` lets the customer go back to any step they have reached and never ahead of it, so after changing the body from the summary they return to the summary in one click. The furthest step is the layout's own state, kept in `ConfiguratorLayout` with `useReachedStep()` so it survives the switch between the desktop and the phone page.
|
|
84
|
+
- **Analytics come with the moves.** `next()` and `previous()` record the built-in step buttons' events and `goTo()` the header stepper's, so the stepper and the bar need no analytics code. The button that opens the summary is the exception: like the built-in **Summary** button, it records `FrontendAnalyticsFactory.SummaryButtonClicked()` from `@salsita-ai/sdk/configurator` through `phx.frontend.useAnalytics().push()` and enters the state with `phx.frontend.useNavigate()`, because `next()` would record the next-step button's event instead.
|
|
85
|
+
- **Locked once ordered.** The stepper takes no clicks while the order is being placed or after it was placed; **Design another** in the bar returns to the first step.
|
|
86
|
+
|
|
87
|
+
## Each step shows the choices the spec gives it
|
|
88
|
+
|
|
89
|
+
The layout does not list which choice belongs to which step. A `$filter` with `navigationStates` makes a spec component active only in those states, so each choice names its step in the spec, plus the summary and the confirmation, which list every choice:
|
|
90
|
+
|
|
91
|
+
```yaml
|
|
92
|
+
components:
|
|
93
|
+
Guitar:
|
|
94
|
+
attributeSelection:
|
|
95
|
+
bodyShape:
|
|
96
|
+
value:
|
|
97
|
+
$bind: bodyShape
|
|
98
|
+
options:
|
|
99
|
+
- label: Offset
|
|
100
|
+
description: Contoured waist, 3.4 kg
|
|
101
|
+
value: Offset
|
|
102
|
+
- label: Single-cut
|
|
103
|
+
description: Thicker body, long sustain
|
|
104
|
+
value: SingleCut
|
|
105
|
+
$filter:
|
|
106
|
+
navigationStates:
|
|
107
|
+
- body
|
|
108
|
+
- summary
|
|
109
|
+
- ordered
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`useActiveChoices` reads the `attributeSelection`, `materialSwatch` and `textValue` components active now with `phx.frontend.useComponents()` and orders them by the layout's field list, which also names each choice: the spec's control components carry no label.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import { useMemo } from 'react'
|
|
116
|
+
|
|
117
|
+
import { attributeSelectionComponent, materialSwatchComponent, textValueComponent } from '@salsita-ai/sdk/domain'
|
|
118
|
+
|
|
119
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
120
|
+
import { byFieldOrder } from './fields.js'
|
|
121
|
+
|
|
122
|
+
export type ChoiceKind = 'selection' | 'swatch' | 'text'
|
|
123
|
+
|
|
124
|
+
/** A choice of the spec that is active now. */
|
|
125
|
+
export interface ActiveChoice {
|
|
126
|
+
readonly instanceName: string
|
|
127
|
+
readonly kind: ChoiceKind
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The guitar's choices whose spec component is active in the current navigation state, in the layout's order. A
|
|
131
|
+
// `$filter` with `navigationStates` makes a component active only in those states, so on each step this is the step's
|
|
132
|
+
// choices, and on the summary every choice. The control hooks throw for an inactive component; a field mounts only
|
|
133
|
+
// for a choice listed here.
|
|
134
|
+
export const useActiveChoices = (): readonly ActiveChoice[] => {
|
|
135
|
+
const guitarId = phx.frontend.useRootEntityId()
|
|
136
|
+
const selections = phx.frontend.useComponents({
|
|
137
|
+
componentTypes: [attributeSelectionComponent],
|
|
138
|
+
entityIds: [guitarId],
|
|
139
|
+
})
|
|
140
|
+
const swatches = phx.frontend.useComponents({ componentTypes: [materialSwatchComponent], entityIds: [guitarId] })
|
|
141
|
+
const texts = phx.frontend.useComponents({ componentTypes: [textValueComponent], entityIds: [guitarId] })
|
|
142
|
+
|
|
143
|
+
return useMemo(() => {
|
|
144
|
+
const choices = [
|
|
145
|
+
...(selections ?? []).map(
|
|
146
|
+
({ componentInstance }): ActiveChoice => ({ instanceName: componentInstance.instanceName, kind: 'selection' })
|
|
147
|
+
),
|
|
148
|
+
...(swatches ?? []).map(
|
|
149
|
+
({ componentInstance }): ActiveChoice => ({ instanceName: componentInstance.instanceName, kind: 'swatch' })
|
|
150
|
+
),
|
|
151
|
+
...(texts ?? []).map(
|
|
152
|
+
({ componentInstance }): ActiveChoice => ({ instanceName: componentInstance.instanceName, kind: 'text' })
|
|
153
|
+
),
|
|
154
|
+
]
|
|
155
|
+
|
|
156
|
+
return choices.sort((a, b) => byFieldOrder(a.instanceName, b.instanceName))
|
|
157
|
+
}, [selections, swatches, texts])
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`StepFields` then mounts one field per active choice, by its kind. Moving a choice to another step is a change of its `$filter` in the spec, and the step follows:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import { Box } from '@mui/material'
|
|
165
|
+
|
|
166
|
+
import { ChoiceField } from './ChoiceField.js'
|
|
167
|
+
import { FinishField } from './FinishField.js'
|
|
168
|
+
import { SignatureField } from './SignatureField.js'
|
|
169
|
+
import { fieldOf } from './fields.js'
|
|
170
|
+
import { useActiveChoices } from './use-active-choices.js'
|
|
171
|
+
|
|
172
|
+
interface StepFieldsProps {
|
|
173
|
+
readonly compact: boolean
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The fields of the active step. The layout does not list which choice belongs to which step: it shows the choices
|
|
177
|
+
// the spec leaves active, so moving a choice to another step is a change of its `$filter` in the spec.
|
|
178
|
+
export const StepFields = ({ compact }: StepFieldsProps) => {
|
|
179
|
+
const choices = useActiveChoices()
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<Box component="div" sx={{ display: 'grid', gap: compact ? 2.5 : 4 }}>
|
|
183
|
+
{choices.map(({ instanceName, kind }) => {
|
|
184
|
+
const { label, warningTypes } = fieldOf(instanceName)
|
|
185
|
+
const props = { instanceName, label, warningTypes, compact }
|
|
186
|
+
|
|
187
|
+
switch (kind) {
|
|
188
|
+
case 'selection':
|
|
189
|
+
return <ChoiceField key={instanceName} {...props} />
|
|
190
|
+
case 'swatch':
|
|
191
|
+
return <FinishField key={instanceName} {...props} />
|
|
192
|
+
case 'text':
|
|
193
|
+
return <SignatureField key={instanceName} {...props} />
|
|
194
|
+
}
|
|
195
|
+
})}
|
|
196
|
+
</Box>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Mount a field only for a choice that is active. The control hooks, such as `useAttributeSelection`, throw for a component that a `$filter` makes inactive, so a layout that renders its fields some other way checks first with `phx.frontend.useComponent({ entityId, componentType, instanceName })`, which returns `null` for an inactive component.
|
|
202
|
+
|
|
203
|
+
## The camera follows the wizard
|
|
204
|
+
|
|
205
|
+
The cameras are spec components too, and the same `$filter` switches them: the whole guitar for the body, the summary and the confirmation, the upper neck and the headstock for the neck, the pickups for the electronics, and the headstock close up for the signature. Entering a step activates its camera and the camera moves there; the layout has no camera code.
|
|
206
|
+
|
|
207
|
+
```yaml
|
|
208
|
+
components:
|
|
209
|
+
Guitar:
|
|
210
|
+
fitRootPerspectiveCamera:
|
|
211
|
+
overview:
|
|
212
|
+
fov: 30
|
|
213
|
+
fitToBox: true
|
|
214
|
+
$filter:
|
|
215
|
+
navigationStates:
|
|
216
|
+
- body
|
|
217
|
+
- summary
|
|
218
|
+
- ordered
|
|
219
|
+
staticPerspectiveCamera:
|
|
220
|
+
neck:
|
|
221
|
+
fov: 30
|
|
222
|
+
lookAtX: 0
|
|
223
|
+
lookAtY: 625
|
|
224
|
+
lookAtZ: 0
|
|
225
|
+
distance: 1350
|
|
226
|
+
azimuthAngle: 0.55
|
|
227
|
+
polarAngle: 1.36
|
|
228
|
+
$filter:
|
|
229
|
+
navigationStates:
|
|
230
|
+
- neck
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The overview fits the guitar into the safe area the page leaves free. A `staticPerspectiveCamera` looks at a point of the model from a fixed distance, in millimetres, which suits a close-up of one part; the camera padding of the safe area still applies, so the part stays beside the panel. [Cameras / Transitions](?path=/docs/3d-scene-cameras-transitions--docs) describes when the camera moves between cameras.
|
|
234
|
+
|
|
235
|
+
## Finish swatches
|
|
236
|
+
|
|
237
|
+
The finish is a `materialSwatch`: the spec lists each swatch with its name and the colour of its chip.
|
|
238
|
+
|
|
239
|
+
```yaml
|
|
240
|
+
components:
|
|
241
|
+
Guitar:
|
|
242
|
+
materialSwatch:
|
|
243
|
+
finish:
|
|
244
|
+
selectedSwatchKey:
|
|
245
|
+
$bind: finish
|
|
246
|
+
swatches:
|
|
247
|
+
Seafoam:
|
|
248
|
+
swatchName: Seafoam green
|
|
249
|
+
color: '#8fcab6'
|
|
250
|
+
CandyRed:
|
|
251
|
+
swatchName: Candy apple red
|
|
252
|
+
color: '#b0141f'
|
|
253
|
+
Midnight:
|
|
254
|
+
swatchName: Midnight blue metallic
|
|
255
|
+
color: '#1d2b4f'
|
|
256
|
+
NaturalAsh:
|
|
257
|
+
swatchName: Natural ash
|
|
258
|
+
color: '#d8b98a'
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
`FinishField` reads it with `phx.frontend.useSwatchSelection(guitarId, 'finish', [])`. Each option has `swatchKey`, `swatchName`, `color`, `selected`, `disabled`, `hint` and `onClick()`, which writes the swatch; the third argument lists the `fixable` violation types the options ignore. The field draws the swatches as round chips in the spec's colours with the chosen one's name beside the label. How the finish renders on the guitar is the material override's business, see [The stage and the model](#the-stage-and-the-model).
|
|
262
|
+
|
|
263
|
+
## A signature on the headstock
|
|
264
|
+
|
|
265
|
+
The customer can sign the headstock. The signature is a `textValue`, and a `sceneEntityObjectTextDecal` on the `headstock` object prints it in script along the headstock's face:
|
|
266
|
+
|
|
267
|
+
```yaml
|
|
268
|
+
components:
|
|
269
|
+
Guitar:
|
|
270
|
+
textValue:
|
|
271
|
+
signature:
|
|
272
|
+
value:
|
|
273
|
+
$bind: signature
|
|
274
|
+
sceneEntityObjectTextDecal:
|
|
275
|
+
headstock:
|
|
276
|
+
text:
|
|
277
|
+
$read: signature
|
|
278
|
+
font: SignPainter-HouseScript
|
|
279
|
+
preloadFonts:
|
|
280
|
+
- SignPainter-HouseScript
|
|
281
|
+
fontSize: auto
|
|
282
|
+
textColor:
|
|
283
|
+
$read: $signatureColor
|
|
284
|
+
positionX: 0.002
|
|
285
|
+
positionY: 0.76
|
|
286
|
+
positionZ: 0.004
|
|
287
|
+
rotationZ: 1.5708
|
|
288
|
+
sizeX: 0.13
|
|
289
|
+
sizeY: 0.03
|
|
290
|
+
sizeZ: 0.02
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The decal's instance name is the object's: `headstock` in `sceneEntityRemoteObject`. Its position, rotation and size are in the object's own units, metres for this model, and `fontSize: auto` fits the text into the decal's width. The font must be loaded by the page; `preloadFonts` redraws the decal once it is.
|
|
294
|
+
|
|
295
|
+
The headstock takes 16 characters. The limit is the expression `$signatureLimit`, declared in the spec so the layout can read it, and a `fixable` rule rejects a longer signature with a message that names it:
|
|
296
|
+
|
|
297
|
+
```ts
|
|
298
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
299
|
+
|
|
300
|
+
export default phx.domain.implementCustomInvariants({
|
|
301
|
+
Guitar: {
|
|
302
|
+
// `fixable`: a change that makes the signature too long is rejected, so the headstock never shows more than fits.
|
|
303
|
+
// The message names the limit, so it is built here rather than in the spec.
|
|
304
|
+
signatureLength: s => {
|
|
305
|
+
if (s.signature.length <= s.$signatureLimit) {
|
|
306
|
+
return []
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return [
|
|
310
|
+
{
|
|
311
|
+
type: 'signatureTooLong',
|
|
312
|
+
severity: 'fixable',
|
|
313
|
+
message: `The headstock fits ${s.$signatureLimit} characters. Shorten the signature.`,
|
|
314
|
+
},
|
|
315
|
+
]
|
|
316
|
+
},
|
|
317
|
+
// `warning` in the spec: the change is kept, so a body chosen after the pickups does not undo them, and ordering
|
|
318
|
+
// waits until the customer resolves it
|
|
319
|
+
offsetHumbuckers: s => {
|
|
320
|
+
return s.bodyShape !== 'Offset' || s.pickups !== 'Humbuckers'
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
})
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
`SignatureField` writes with `phx.frontend.useTextValue(guitarId, 'signature')`. `update(event, value)` stages each value as the customer types, so the headstock follows every key, and `resolve()` commits it when they leave the field or press Enter. While the staged text breaks the rule, `hasErrors` is set and `hints` holds the rule's message, which the field shows in red with the counter; `resolve()` then rolls back to the committed signature instead, so the guitar never keeps a signature that does not fit.
|
|
327
|
+
|
|
328
|
+
```tsx
|
|
329
|
+
import { Box, InputBase, Typography } from '@mui/material'
|
|
330
|
+
|
|
331
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
332
|
+
import { FieldShell } from './FieldShell.js'
|
|
333
|
+
import { NIGHTJAR } from './tokens.js'
|
|
334
|
+
|
|
335
|
+
interface SignatureFieldProps {
|
|
336
|
+
readonly instanceName: string
|
|
337
|
+
readonly label: string
|
|
338
|
+
readonly warningTypes: readonly string[]
|
|
339
|
+
readonly compact: boolean
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// The spec's `textValue`, which the spec prints on the headstock with a text decal. Typing stages each value, so the
|
|
343
|
+
// headstock follows every key; leaving the field or pressing Enter commits it. A value the `fixable` length rule
|
|
344
|
+
// rejects shows the rule's message as the hook's `hints`, and leaving the field rolls back to the last value that
|
|
345
|
+
// fitted. The counter reads the limit from the spec's `$signatureLimit` expression, which the rule checks too.
|
|
346
|
+
export const SignatureField = ({ instanceName, label, warningTypes, compact }: SignatureFieldProps) => {
|
|
347
|
+
const guitarId = phx.frontend.useRootEntityId()
|
|
348
|
+
const guitar = phx.frontend.useRootEntityState()
|
|
349
|
+
const signature = phx.frontend.useTextValue(guitarId, instanceName)
|
|
350
|
+
const limit = guitar.$signatureLimit
|
|
351
|
+
|
|
352
|
+
return (
|
|
353
|
+
<FieldShell
|
|
354
|
+
id={`nightjar-${instanceName}`}
|
|
355
|
+
label={label}
|
|
356
|
+
aside={
|
|
357
|
+
<Typography
|
|
358
|
+
aria-live="polite"
|
|
359
|
+
sx={{
|
|
360
|
+
fontFamily: NIGHTJAR.font,
|
|
361
|
+
fontSize: 13,
|
|
362
|
+
fontVariantNumeric: 'tabular-nums',
|
|
363
|
+
color: signature.hasErrors ? NIGHTJAR.alert : NIGHTJAR.muted,
|
|
364
|
+
}}
|
|
365
|
+
>
|
|
366
|
+
{signature.value.length} / {limit}
|
|
367
|
+
</Typography>
|
|
368
|
+
}
|
|
369
|
+
hint={
|
|
370
|
+
compact ? undefined : 'In gold leaf on dark necks, in black on maple. Leave it empty for a plain headstock.'
|
|
371
|
+
}
|
|
372
|
+
warningTypes={warningTypes}
|
|
373
|
+
compact={compact}
|
|
374
|
+
>
|
|
375
|
+
<Box
|
|
376
|
+
component="div"
|
|
377
|
+
sx={{
|
|
378
|
+
display: 'grid',
|
|
379
|
+
gap: 1,
|
|
380
|
+
paddingInline: 2,
|
|
381
|
+
paddingBlock: compact ? 1 : 1.5,
|
|
382
|
+
borderRadius: 1.5,
|
|
383
|
+
border: `1px solid ${signature.hasErrors ? NIGHTJAR.alert : NIGHTJAR.line}`,
|
|
384
|
+
bgcolor: NIGHTJAR.raised,
|
|
385
|
+
'&:focus-within': { borderColor: signature.hasErrors ? NIGHTJAR.alert : NIGHTJAR.amber },
|
|
386
|
+
}}
|
|
387
|
+
>
|
|
388
|
+
<InputBase
|
|
389
|
+
value={signature.value}
|
|
390
|
+
placeholder="Your name"
|
|
391
|
+
onChange={event => signature.update(event, event.target.value)}
|
|
392
|
+
onBlur={signature.resolve}
|
|
393
|
+
onKeyDown={event => {
|
|
394
|
+
if (event.key === 'Enter') {
|
|
395
|
+
signature.resolve()
|
|
396
|
+
}
|
|
397
|
+
}}
|
|
398
|
+
inputProps={{
|
|
399
|
+
'aria-labelledby': `nightjar-${instanceName}-label`,
|
|
400
|
+
'aria-invalid': signature.hasErrors,
|
|
401
|
+
'aria-describedby': `nightjar-${instanceName}-error`,
|
|
402
|
+
spellCheck: false,
|
|
403
|
+
}}
|
|
404
|
+
sx={{
|
|
405
|
+
fontFamily: NIGHTJAR.script,
|
|
406
|
+
fontSize: compact ? 30 : 38,
|
|
407
|
+
lineHeight: 1.2,
|
|
408
|
+
color: NIGHTJAR.amber,
|
|
409
|
+
'& input::placeholder': { color: NIGHTJAR.faint, opacity: 1 },
|
|
410
|
+
}}
|
|
411
|
+
/>
|
|
412
|
+
</Box>
|
|
413
|
+
<Typography
|
|
414
|
+
id={`nightjar-${instanceName}-error`}
|
|
415
|
+
role="alert"
|
|
416
|
+
sx={{ fontFamily: NIGHTJAR.font, fontSize: 14, color: NIGHTJAR.alert, '&:empty': { display: 'none' } }}
|
|
417
|
+
>
|
|
418
|
+
{signature.hasErrors ? signature.hints : ''}
|
|
419
|
+
</Typography>
|
|
420
|
+
</FieldShell>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Summary and conflicts
|
|
426
|
+
|
|
427
|
+
The last step reviews the guitar. `SummaryRows` lists every choice active in the `summary` state with the control hook of its kind: the option's `title` from `useAttributeSelection`, the swatch's name and colour from `useSwatchSelection`, the text from `useTextValue`. The summary only reviews; a choice is changed on its step.
|
|
428
|
+
|
|
429
|
+
One rule spans two steps: the Offset body is routed for single-coil-sized pickups. It is a `warning`, so a change that breaks it is kept, and a body chosen after the pickups does not undo them:
|
|
430
|
+
|
|
431
|
+
```yaml
|
|
432
|
+
invariants:
|
|
433
|
+
Guitar:
|
|
434
|
+
offsetHumbuckers:
|
|
435
|
+
type: offsetHumbuckers
|
|
436
|
+
severity: warning
|
|
437
|
+
message: The Offset body is routed for single-coil-sized pickups. Choose single coils or P-90s, or the Single-cut body for humbuckers.
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
While it holds, the layout shows it three times, each read from `phx.frontend.useFinalizeViolations().violations`: under the **Pickups** and **Body shape** fields, as a mark on the **Electronics** step, and on the summary with **Go to Electronics**. The layout's `STEP_WARNINGS` names the step that resolves each warning. `phx.frontend.useStateValidity()` holds the same violations, but on `phx.frontend` it is typed as the validity value instead of a hook, so calling it does not type-check; `useWarnings` uses `useFinalizeViolations` instead.
|
|
441
|
+
|
|
442
|
+
## Place the order
|
|
443
|
+
|
|
444
|
+
The spec declares the order action, and the handler, `src/handlers/place-order.ts`, stands in for the shop's order service:
|
|
445
|
+
|
|
446
|
+
```yaml
|
|
447
|
+
components:
|
|
448
|
+
Guitar:
|
|
449
|
+
finalizeAction:
|
|
450
|
+
placeOrder:
|
|
451
|
+
variant: primary
|
|
452
|
+
label: Place order
|
|
453
|
+
title: Send the guitar as configured to the Northfret workshop
|
|
454
|
+
icon: ShoppingBag
|
|
455
|
+
handler: placeOrder
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
```ts
|
|
459
|
+
import { useCallback } from 'react'
|
|
460
|
+
|
|
461
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
462
|
+
|
|
463
|
+
// Stands in for Northfret's order service. Placing an order takes a moment. The service checks stock when the order
|
|
464
|
+
// arrives, which the configurator cannot know beforehand: gold hardware is back-ordered, so it refuses that order with
|
|
465
|
+
// its reason; any other order is accepted and the page shows the confirmation.
|
|
466
|
+
export default phx.frontend.createPhoenixHandler('placeOrder', () => {
|
|
467
|
+
const navigate = phx.frontend.useNavigate()
|
|
468
|
+
const guitar = phx.frontend.useRootEntityState()
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
handler: useCallback(async () => {
|
|
472
|
+
await new Promise(resolve => setTimeout(resolve, 1600))
|
|
473
|
+
|
|
474
|
+
if (guitar.hardware === 'Gold') {
|
|
475
|
+
throw new Error(
|
|
476
|
+
'Gold hardware is back-ordered until next month. Choose chrome or black hardware, or order later.'
|
|
477
|
+
)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
navigate('ordered')
|
|
481
|
+
}, [guitar.hardware, navigate]),
|
|
482
|
+
}
|
|
483
|
+
})
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**Place order** sits in the bar on the summary step. It runs `phx.frontend.useFinalizeActions().primary`:
|
|
487
|
+
|
|
488
|
+
- **Blocked.** While a warning holds, the action is `disabled` and `hints` holds the warnings' text: the bar shows "Settle the choices above first" beside the button, and the summary lists the conflicts. The name and the e-mail are the layout's own state. The button stays clickable; while either blocks the order, a click reveals what is missing from the form instead of ordering.
|
|
489
|
+
- **Pending.** `run()` starts the handler beside the layout; `isPending` turns the button into "Placing order…" with a spinner and locks the stepper, **Back** and the form.
|
|
490
|
+
- **Failed.** The service refuses gold hardware, which the configurator cannot know beforehand. The handler throws, and the action's `error` is shown at the top of the summary; no snackbar is raised. **Place order** stays available to try again.
|
|
491
|
+
- **Placed.** The handler enters `ordered`, where the panel thanks the customer by name with an order number made from the configuration's id, and lists the build.
|
|
492
|
+
|
|
493
|
+
A viewer of a shared configuration sees no **Place order**: neither hook checks read mode, so the bar hides it while `phx.frontend.useAccessService().accessInfo.viewMode` is set.
|
|
494
|
+
|
|
495
|
+
## Pages that slide in
|
|
496
|
+
|
|
497
|
+
`ConfiguratorLayout` keeps the scene mounted and lays the desktop or the phone page over it, by the width of the configurator's container from `phx.frontend.useActiveViewports()`. It also holds the contact form's state and the furthest step reached, so both survive the switch between the two, and the form survives the move to the confirmation.
|
|
498
|
+
|
|
499
|
+
```tsx
|
|
500
|
+
import { Box } from '@mui/material'
|
|
501
|
+
|
|
502
|
+
import { LayoutSlots } from '@salsita-ai/sdk/configurator'
|
|
503
|
+
|
|
504
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
505
|
+
import { NIGHTJAR } from '../_fields/index.js'
|
|
506
|
+
import { DesktopPage } from '../_shared/DesktopPage.js'
|
|
507
|
+
import { PhonePage } from '../_shared/PhonePage.js'
|
|
508
|
+
import { useOrderForm } from '../_shared/order-form.js'
|
|
509
|
+
import { useReachedStep } from '../_shared/use-reached-step.js'
|
|
510
|
+
|
|
511
|
+
// A wizard over the scene. The canvas stays mounted the whole time; the desktop or the phone page lies over it. The
|
|
512
|
+
// contact form's state and the furthest step reached live here, so they survive the switch between the two; the form
|
|
513
|
+
// also survives the move to the confirmation.
|
|
514
|
+
const ConfiguratorLayout = () => {
|
|
515
|
+
// `null` until the container has been measured; only the scene shows until then
|
|
516
|
+
const viewports = phx.frontend.useActiveViewports()
|
|
517
|
+
const { activeIndex } = phx.frontend.useStepNavigation()
|
|
518
|
+
const form = useOrderForm()
|
|
519
|
+
const reached = useReachedStep(activeIndex)
|
|
520
|
+
const isDesktop = viewports?.includes('desktop') ?? false
|
|
521
|
+
|
|
522
|
+
return (
|
|
523
|
+
<Box component="div" sx={{ position: 'absolute', inset: 0, overflow: 'clip', bgcolor: NIGHTJAR.ink }}>
|
|
524
|
+
<LayoutSlots.SharedCanvas />
|
|
525
|
+
{viewports !== null &&
|
|
526
|
+
(isDesktop ? <DesktopPage form={form} reached={reached} /> : <PhonePage form={form} reached={reached} />)}
|
|
527
|
+
</Box>
|
|
528
|
+
)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
export default phx.frontend.createCustomPhoenixLayoutElement('ConfiguratorLayout', ConfiguratorLayout)
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Each page measures its safe area with `phx.frontend.useSceneSafeArea()`. The summary's panel is wider than a step's, so the steps, the summary and the confirmation are three pages, each with its own safe area, and each slides in when it opens. The safe area slides with its page: the measurement taken when the page mounts sees it shifted, and a `transform` that ends is not a resize, so nothing measures again. The page calls `update()` from the same hook when its own slide ends, not when the step content's slide inside it does. Without it the guitar stays shifted sideways by the slide's 48 pixels.
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
import { Box, Typography } from '@mui/material'
|
|
538
|
+
import type { AnimationEvent } from 'react'
|
|
539
|
+
|
|
540
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
541
|
+
import { ENTER_FROM_END, HEADING_SX, NIGHTJAR } from '../_fields/index.js'
|
|
542
|
+
import { OrderBar } from './OrderBar.js'
|
|
543
|
+
import { OrderConfirmation } from './OrderConfirmation.js'
|
|
544
|
+
import { StepHeader } from './StepHeader.js'
|
|
545
|
+
import { StepPanel } from './StepPanel.js'
|
|
546
|
+
import { SummaryPanel } from './SummaryPanel.js'
|
|
547
|
+
import type { OrderForm } from './order-form.js'
|
|
548
|
+
|
|
549
|
+
type Page = 'steps' | 'summary' | 'ordered'
|
|
550
|
+
|
|
551
|
+
// The summary needs room for the contact form, so its panel is wider
|
|
552
|
+
const PANEL_WIDTHS: Readonly<Record<Page, number>> = { steps: 420, summary: 520, ordered: 420 }
|
|
553
|
+
|
|
554
|
+
const pageOf = (navigationState: string): Page => {
|
|
555
|
+
return navigationState === 'ordered' ? 'ordered' : navigationState === 'summary' ? 'summary' : 'steps'
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// The brand's mark: a fret of amber over the name of the shop
|
|
559
|
+
const Brand = () => {
|
|
560
|
+
return (
|
|
561
|
+
<Box component="div" sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 'none' }}>
|
|
562
|
+
<Box component="span" aria-hidden sx={{ inlineSize: 4, blockSize: 28, bgcolor: NIGHTJAR.amber }} />
|
|
563
|
+
<Box component="div" sx={{ display: 'grid' }}>
|
|
564
|
+
<Typography sx={{ ...HEADING_SX, fontSize: 16, letterSpacing: '0.32em', lineHeight: 1.1 }}>
|
|
565
|
+
Northfret
|
|
566
|
+
</Typography>
|
|
567
|
+
<Typography sx={{ ...HEADING_SX, fontSize: 10, letterSpacing: '0.36em', color: NIGHTJAR.muted }}>
|
|
568
|
+
Custom shop
|
|
569
|
+
</Typography>
|
|
570
|
+
</Box>
|
|
571
|
+
</Box>
|
|
572
|
+
)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
interface DesktopPageProps {
|
|
576
|
+
readonly form: OrderForm
|
|
577
|
+
readonly reached: number
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// The desktop wizard: the stepper on top, the guitar in the free area on the left, the panel of the step on the
|
|
581
|
+
// right and the bar with the price at the bottom.
|
|
582
|
+
//
|
|
583
|
+
// The summary's panel is wider than a step's, so the steps, the summary and the confirmation are three pages, each
|
|
584
|
+
// with its own safe area, and each slides in when it opens. The safe area slides with its page: the first
|
|
585
|
+
// measurement, taken when the page mounts, sees it shifted, and no resize follows when the slide ends. `update()`
|
|
586
|
+
// from `useSceneSafeArea` measures again once the slide has settled, so the camera frames the guitar in the place the
|
|
587
|
+
// page leaves for it.
|
|
588
|
+
export const DesktopPage = ({ form, reached }: DesktopPageProps) => {
|
|
589
|
+
const { containerRef, safeAreaRef, update } = phx.frontend.useSceneSafeArea()
|
|
590
|
+
const navigationState = phx.frontend.useActiveNavigationState()
|
|
591
|
+
const page = pageOf(navigationState)
|
|
592
|
+
|
|
593
|
+
// The step's own content slides too; only the page's slide moves the safe area
|
|
594
|
+
const handleAnimationEnd = (event: AnimationEvent<HTMLDivElement>) => {
|
|
595
|
+
if (event.target === event.currentTarget) {
|
|
596
|
+
update()
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return (
|
|
601
|
+
<Box
|
|
602
|
+
component="div"
|
|
603
|
+
ref={containerRef}
|
|
604
|
+
sx={{
|
|
605
|
+
position: 'absolute',
|
|
606
|
+
inset: 0,
|
|
607
|
+
display: 'grid',
|
|
608
|
+
gridTemplateRows: 'auto minmax(0, 1fr) auto',
|
|
609
|
+
fontFamily: NIGHTJAR.font,
|
|
610
|
+
color: NIGHTJAR.cream,
|
|
611
|
+
// The free area lies over the canvas; only the bars and the panel take the pointer
|
|
612
|
+
pointerEvents: 'none',
|
|
613
|
+
}}
|
|
614
|
+
>
|
|
615
|
+
<Box
|
|
616
|
+
component="header"
|
|
617
|
+
sx={{
|
|
618
|
+
display: 'flex',
|
|
619
|
+
alignItems: 'center',
|
|
620
|
+
gap: 5,
|
|
621
|
+
minBlockSize: 72,
|
|
622
|
+
paddingInline: 4,
|
|
623
|
+
bgcolor: NIGHTJAR.ink,
|
|
624
|
+
borderBlockEnd: `1px solid ${NIGHTJAR.line}`,
|
|
625
|
+
pointerEvents: 'auto',
|
|
626
|
+
}}
|
|
627
|
+
>
|
|
628
|
+
<Brand />
|
|
629
|
+
<StepHeader compact={false} reached={reached} />
|
|
630
|
+
</Box>
|
|
631
|
+
|
|
632
|
+
<Box
|
|
633
|
+
component="div"
|
|
634
|
+
key={page}
|
|
635
|
+
onAnimationEnd={handleAnimationEnd}
|
|
636
|
+
sx={{
|
|
637
|
+
display: 'grid',
|
|
638
|
+
gridTemplateColumns: `minmax(0, 1fr) ${PANEL_WIDTHS[page]}px`,
|
|
639
|
+
minBlockSize: 0,
|
|
640
|
+
animation: `${ENTER_FROM_END} 480ms cubic-bezier(0.2, 0.7, 0.2, 1)`,
|
|
641
|
+
'@media (prefers-reduced-motion: reduce)': { animation: 'none' },
|
|
642
|
+
}}
|
|
643
|
+
>
|
|
644
|
+
<Box component="div" sx={{ position: 'relative', minInlineSize: 0 }}>
|
|
645
|
+
{/* The camera fits the guitar into this box, so its inset is the room around the guitar */}
|
|
646
|
+
<Box component="div" ref={safeAreaRef} sx={{ position: 'absolute', inset: '40px 72px 56px' }} />
|
|
647
|
+
<Typography
|
|
648
|
+
sx={{
|
|
649
|
+
position: 'absolute',
|
|
650
|
+
insetBlockEnd: 20,
|
|
651
|
+
insetInline: 0,
|
|
652
|
+
textAlign: 'center',
|
|
653
|
+
fontSize: 12,
|
|
654
|
+
letterSpacing: '0.14em',
|
|
655
|
+
textTransform: 'uppercase',
|
|
656
|
+
color: NIGHTJAR.faint,
|
|
657
|
+
}}
|
|
658
|
+
>
|
|
659
|
+
Drag to turn the guitar
|
|
660
|
+
</Typography>
|
|
661
|
+
</Box>
|
|
662
|
+
<Box
|
|
663
|
+
component="aside"
|
|
664
|
+
aria-label={page === 'steps' ? 'Step' : page === 'summary' ? 'Summary' : 'Order'}
|
|
665
|
+
sx={{
|
|
666
|
+
overflowY: 'auto',
|
|
667
|
+
paddingInline: 4,
|
|
668
|
+
paddingBlock: 4,
|
|
669
|
+
bgcolor: NIGHTJAR.panel,
|
|
670
|
+
borderInlineStart: `1px solid ${NIGHTJAR.line}`,
|
|
671
|
+
pointerEvents: 'auto',
|
|
672
|
+
}}
|
|
673
|
+
>
|
|
674
|
+
{page === 'steps' && <StepPanel compact={false} />}
|
|
675
|
+
{page === 'summary' && <SummaryPanel compact={false} form={form} />}
|
|
676
|
+
{page === 'ordered' && <OrderConfirmation compact={false} contact={form.contact} />}
|
|
677
|
+
</Box>
|
|
678
|
+
</Box>
|
|
679
|
+
|
|
680
|
+
<OrderBar compact={false} form={form} />
|
|
681
|
+
</Box>
|
|
682
|
+
)
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
## On a phone
|
|
687
|
+
|
|
688
|
+
The same layout in a frame 390 pixels wide, as on a phone. A compact "2 / 5 · Neck" line with a thin progress bar replaces the stepper, the choices become chips in a row that scrolls sideways, the guitar keeps the top of the screen and the bar stays fixed at the bottom. Opened on its own from the sidebar, the **Phone** story sets Storybook's viewport to 390 × 844.
|
|
689
|
+
|
|
690
|
+
## The stage and the model
|
|
691
|
+
|
|
692
|
+
The spec's `canvasCustomEnvironment` names `NightjarStage`, a layout element rendered inside the scene: a dark room with a warm glow on the wall behind the guitar, a warm key light and a cool rim. drei's `Lightformer` panels exist only in the reflections, where they draw the long highlights on the clearcoat and the hardware, and no image has to be downloaded.
|
|
693
|
+
|
|
694
|
+
```tsx
|
|
695
|
+
import { Environment, Lightformer } from '@react-three/drei'
|
|
696
|
+
import { useMemo } from 'react'
|
|
697
|
+
import { CanvasTexture, SRGBColorSpace } from 'three'
|
|
698
|
+
|
|
699
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
700
|
+
import { NIGHTJAR } from '../_fields/index.js'
|
|
701
|
+
|
|
702
|
+
/** A warm pool of light that fades into the dark, drawn once on a canvas. */
|
|
703
|
+
const createGlow = (): CanvasTexture => {
|
|
704
|
+
const size = 512
|
|
705
|
+
const canvas = document.createElement('canvas')
|
|
706
|
+
canvas.width = size
|
|
707
|
+
canvas.height = size
|
|
708
|
+
const context = canvas.getContext('2d')
|
|
709
|
+
|
|
710
|
+
if (context) {
|
|
711
|
+
const gradient = context.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2)
|
|
712
|
+
gradient.addColorStop(0, 'rgba(242, 163, 58, 0.42)')
|
|
713
|
+
gradient.addColorStop(0.22, 'rgba(214, 112, 46, 0.2)')
|
|
714
|
+
gradient.addColorStop(0.5, 'rgba(122, 30, 43, 0.07)')
|
|
715
|
+
gradient.addColorStop(0.75, 'rgba(14, 15, 18, 0)')
|
|
716
|
+
context.fillStyle = gradient
|
|
717
|
+
context.fillRect(0, 0, size, size)
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const texture = new CanvasTexture(canvas)
|
|
721
|
+
texture.colorSpace = SRGBColorSpace
|
|
722
|
+
return texture
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// The stage named by the spec's `canvasCustomEnvironment`: a dark room with a warm glow on the wall behind the guitar,
|
|
726
|
+
// a warm key light and a cool rim. The light panels exist only in the reflections, where they draw the long highlights
|
|
727
|
+
// on the clearcoat and the hardware. The scene is in millimetres.
|
|
728
|
+
export default phx.frontend.createCustomPhoenixLayoutElement('NightjarStage', () => {
|
|
729
|
+
const glow = useMemo(createGlow, [])
|
|
730
|
+
|
|
731
|
+
return (
|
|
732
|
+
<>
|
|
733
|
+
<color attach="background" args={[NIGHTJAR.ink]} />
|
|
734
|
+
<ambientLight intensity={0.4} />
|
|
735
|
+
<directionalLight position={[-900, 1400, 1600]} intensity={2.4} color="#ffe6c7" />
|
|
736
|
+
<directionalLight position={[1400, 700, -900]} intensity={1.2} color="#a9c1ff" />
|
|
737
|
+
|
|
738
|
+
{/* The glow on the wall, behind the guitar and out of the camera's fit */}
|
|
739
|
+
<mesh position={[-250, 300, -900]}>
|
|
740
|
+
<planeGeometry args={[3600, 3600]} />
|
|
741
|
+
<meshBasicMaterial map={glow} transparent depthWrite={false} toneMapped={false} />
|
|
742
|
+
</mesh>
|
|
743
|
+
|
|
744
|
+
<Environment resolution={256} frames={1}>
|
|
745
|
+
<color attach="background" args={['#15161b']} />
|
|
746
|
+
{/* Tall strips at both sides for the long highlights, a softbox above, an amber wash behind */}
|
|
747
|
+
<Lightformer form="rect" intensity={7} position={[-5, 1, 3]} rotation-y={Math.PI / 3} scale={[1.2, 9, 1]} />
|
|
748
|
+
<Lightformer form="rect" intensity={4} position={[5, 1, 2]} rotation-y={-Math.PI / 3} scale={[1, 9, 1]} />
|
|
749
|
+
<Lightformer form="rect" intensity={2.5} position={[0, 6, 1]} rotation-x={Math.PI / 2} scale={[8, 3, 1]} />
|
|
750
|
+
<Lightformer form="rect" intensity={1.2} position={[0, 1, 7]} rotation-y={Math.PI} scale={[8, 4, 1]} />
|
|
751
|
+
<Lightformer form="rect" color={NIGHTJAR.amber} intensity={0.8} position={[0, -2, -7]} scale={[10, 3, 1]} />
|
|
752
|
+
</Environment>
|
|
753
|
+
</>
|
|
754
|
+
)
|
|
755
|
+
})
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
The finish is a physical material with a clearcoat, set per body object with `sceneEntityObjectLocalMaterialOverride`: the override names the object, and only that object's `Finish` material changes. Natural ash shows a wood texture through the clearcoat; the paints have none. The materials many parts share, such as `Hardware` on the tuners, the bridges, the knobs and the switches, use `sceneEntityLocalMaterialOverride`, which changes every object of the entity that carries the material. The values come from expressions in `src/expressions/guitar.ts`, which also price the guitar.
|
|
759
|
+
|
|
760
|
+
```yaml
|
|
761
|
+
components:
|
|
762
|
+
Guitar:
|
|
763
|
+
sceneEntityObjectLocalMaterialOverride:
|
|
764
|
+
bodyOffset:
|
|
765
|
+
materialsToOverride:
|
|
766
|
+
- Finish
|
|
767
|
+
materialType: physical
|
|
768
|
+
color:
|
|
769
|
+
$read: $finishColor
|
|
770
|
+
textureUrl:
|
|
771
|
+
$read: $finishTexture
|
|
772
|
+
metalness:
|
|
773
|
+
$read: $finishMetalness
|
|
774
|
+
roughness:
|
|
775
|
+
$read: $finishRoughness
|
|
776
|
+
clearcoat: 1
|
|
777
|
+
clearcoatRoughness: 0.04
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
The guitar is `public/models/gallery/guitar.glb`, generated by `scripts/models/guitar.ts` in the Storybook package from three.js geometry: two bodies extruded from their outlines with a rounded bevel, a pickguard for each, the neck, fretboard, frets, inlays, headstock, tuners and strings, three pickup sets and the bridges, knobs and switches of each body. Each part is a named node the spec loads with `sceneEntityRemoteObject.<name>.objectName`; `sceneEntityObjectProperties` shows the parts of the chosen body and pickups. The script lives in `scripts/models/`, outside the example's `src/`, because every file under `src/` is part of this page.
|
|
781
|
+
|
|
782
|
+
## Test cases
|
|
783
|
+
|
|
784
|
+
- The page opens on **Body**: the Offset in Seafoam green with a parchment pickguard, maple neck, rosewood board, single coils and chrome, **$1,650.00**, and **Next: Neck**. The stepper shows step 1 active and the rest upcoming, which cannot be clicked yet.
|
|
785
|
+
- **Candy apple red** turns the body red under a glossy clearcoat at $1,830.00; **Natural ash** shows the wood grain. The finish's name appears beside the label.
|
|
786
|
+
- **Single-cut** swaps the body, its small pickguard, the bar bridge with a tailpiece, four knobs and the toggle switch, and adds $150. **None** removes the pickguard.
|
|
787
|
+
- **Next: Neck** slides the neck's choices in from the right and moves the camera to the upper neck and the headstock. **Roasted maple** and **Ebony** change the neck and the board; on a maple board the dots turn black.
|
|
788
|
+
- **Back** slides the body's choices in from the left and returns the camera to the whole guitar.
|
|
789
|
+
- On **Electronics** the camera moves to the pickups. **Humbuckers** on the Offset shows "The Offset body is routed for single-coil-sized pickups. …" under **Pickups**, and the stepper marks **Electronics**. **P-90s** clears both.
|
|
790
|
+
- **Gold** turns the tuners, the bridge, the knobs and the switch gold.
|
|
791
|
+
- On **Details** the camera shows the headstock. Typing a name prints it on the headstock as you type, in gold on a dark neck and in black on maple; the counter shows the characters used of 16, and the price rises by $45.
|
|
792
|
+
- Typing past 16 characters turns the counter and the field red and shows "The headstock fits 16 characters. Shorten the signature."; leaving the field puts back the signature committed before, and the headstock with it.
|
|
793
|
+
- **Review order** opens the summary: a wider panel lists every choice, with the signature in script. With the humbucker conflict, the summary lists it with **Go to Electronics**, the bar shows "Settle the choices above first", and **Place order** reveals "Enter the name the order is for." and "Enter an e-mail address for the build updates." under the empty fields.
|
|
794
|
+
- **Go to Electronics** opens that step; after **P-90s**, the stepper's **Summary** returns to the summary in one click.
|
|
795
|
+
- After going back from the summary to an earlier step, narrowing the window until the phone page shows and widening it again leaves the stepper's **Summary** enabled.
|
|
796
|
+
- With a name and an e-mail, **Place order** shows "Placing order…" with a spinner for under two seconds. With gold hardware it then shows "The order did not go through" and the service's reason at the top of the summary; with chrome or black it opens the confirmation: "Thank you, …", the order number, the e-mail and the build. The stepper no longer takes clicks, and **Design another** returns to **Body**.
|
|
797
|
+
- Each step, the summary and the confirmation leave the guitar centred in the area beside the panel once their slide has settled.
|
|
798
|
+
- With the browser console's Verbose level, **Next** and **Back** log the step buttons' `nextStepButton` and `previousStepButton` events, the stepper the `topNavigation` `step` event, **Review order** the `summaryButton` event and **Place order** the `finalizeButton` event: without a Google Tag Manager id, Storybook's analytics log their events with `console.debug`.
|
|
799
|
+
- In the **Phone** story the guitar keeps the top of the screen, "1 / 5 · Body" and the progress line sit above it, the choices are chips in rows that scroll sideways, and **Next**, **Review order** and **Place order** work as on the desktop.
|
|
800
|
+
- The browser console shows no errors, apart from the "Failed to finalize" line that the refused order with gold hardware logs.
|
|
801
|
+
|
|
802
|
+
## What this layout leaves out
|
|
803
|
+
|
|
804
|
+
- **Messages for rejected changes.** The only rule that rejects a change is the signature's, which the field explains itself, so the layout mounts neither `LayoutSlots.Services` nor `phx.frontend.useConfiguratorMessages()`. A project whose other rules reject changes, or that saves the configuration, mounts one of the two so a rejected change or a failed save is not silent; see [Custom Layout → Show messages](?path=/docs/layouts-custom-layout--docs#show-messages).
|
|
805
|
+
- **Read mode.** **Place order** is hidden while `phx.frontend.useAccessService().accessInfo.viewMode` is set, but the fields stay editable and the example ships no read template; [Custom Layout → Serve read mode](?path=/docs/layouts-custom-layout--docs#serve-read-mode) shows one.
|
|
806
|
+
- **An order that outlives the page.** The order number is made from the configuration's id and the contact details are the layout's state, so a configuration reopened in the `ordered` state thanks no one by name. A deployed project sends the details with the order and shows what its service answers.
|
|
807
|
+
- **The built-in arrangement.** Header, sidebar, sidebar footer, tabs, tool and view controls and the toolbar are not mounted; [Layout Slots](?path=/docs/phoenix-layout-slots--docs) lists every slot a project layout can mount.
|
|
808
|
+
|
|
809
|
+
## Related pages
|
|
810
|
+
|
|
811
|
+
- [Kestrel Kettle](?path=/docs/layouts-gallery-kestrel-kettle--docs) sells a small product from one page, and [Ardent Compressor](?path=/docs/layouts-gallery-ardent-compressor--docs) is a stepped quote builder without a scene.
|
|
812
|
+
- [Custom Layout](?path=/docs/layouts-custom-layout--docs) explains a project layout step by step.
|
|
813
|
+
- [Configurator Interface → Hooks for a Project Layout](?path=/docs/phoenix-configurator-interface--docs#hooks-for-a-project-layout) lists the hooks used here, steps and tabs included.
|
|
814
|
+
- [Floating Header → Adding step navigation](?path=/docs/layouts-header-floating--docs#adding-step-navigation) shows the built-in stepper over the same `navigationSteps`.
|
|
815
|
+
- [Domain Model → Invariants](?path=/docs/phoenix-domain-model--docs#invariants) describes the rules.
|
|
816
|
+
- [Finalization](?path=/docs/phoenix-finalization--docs) describes the server side of a finalize action.
|
|
817
|
+
|
|
818
|
+
## Complete Implementation
|
|
819
|
+
|
|
820
|
+
{/* AUTO-GENERATED SECTION: This section is automatically updated by 'phoenix story build' */}
|
|
821
|
+
|
|
822
|
+
<details>
|
|
823
|
+
<summary>Click to view the complete implementation files</summary>
|
|
824
|
+
|
|
825
|
+
### ./spec/domain.yaml
|
|
826
|
+
|
|
827
|
+
```yaml
|
|
828
|
+
model:
|
|
829
|
+
latest:
|
|
830
|
+
entityTypes:
|
|
831
|
+
- Guitar
|
|
832
|
+
aliases:
|
|
833
|
+
RootEntity:
|
|
834
|
+
- Guitar
|
|
835
|
+
commonTypes:
|
|
836
|
+
BodyShape:
|
|
837
|
+
enum:
|
|
838
|
+
- Offset
|
|
839
|
+
- SingleCut
|
|
840
|
+
Finish:
|
|
841
|
+
enum:
|
|
842
|
+
- Seafoam
|
|
843
|
+
- CandyRed
|
|
844
|
+
- Midnight
|
|
845
|
+
- NaturalAsh
|
|
846
|
+
Pickguard:
|
|
847
|
+
enum:
|
|
848
|
+
- Parchment
|
|
849
|
+
- Black
|
|
850
|
+
- Pearloid
|
|
851
|
+
- None
|
|
852
|
+
NeckWood:
|
|
853
|
+
enum:
|
|
854
|
+
- Maple
|
|
855
|
+
- RoastedMaple
|
|
856
|
+
- Mahogany
|
|
857
|
+
Fretboard:
|
|
858
|
+
enum:
|
|
859
|
+
- Maple
|
|
860
|
+
- Rosewood
|
|
861
|
+
- Ebony
|
|
862
|
+
Pickups:
|
|
863
|
+
enum:
|
|
864
|
+
- SingleCoils
|
|
865
|
+
- Humbuckers
|
|
866
|
+
- P90s
|
|
867
|
+
Hardware:
|
|
868
|
+
enum:
|
|
869
|
+
- Chrome
|
|
870
|
+
- Gold
|
|
871
|
+
- Black
|
|
872
|
+
shapes:
|
|
873
|
+
Guitar:
|
|
874
|
+
bodyShape:
|
|
875
|
+
$ref: '#/model/latest/commonTypes/BodyShape'
|
|
876
|
+
finish:
|
|
877
|
+
$ref: '#/model/latest/commonTypes/Finish'
|
|
878
|
+
pickguard:
|
|
879
|
+
$ref: '#/model/latest/commonTypes/Pickguard'
|
|
880
|
+
neckWood:
|
|
881
|
+
$ref: '#/model/latest/commonTypes/NeckWood'
|
|
882
|
+
fretboard:
|
|
883
|
+
$ref: '#/model/latest/commonTypes/Fretboard'
|
|
884
|
+
pickups:
|
|
885
|
+
$ref: '#/model/latest/commonTypes/Pickups'
|
|
886
|
+
hardware:
|
|
887
|
+
$ref: '#/model/latest/commonTypes/Hardware'
|
|
888
|
+
signature:
|
|
889
|
+
type: string
|
|
890
|
+
|
|
891
|
+
presets:
|
|
892
|
+
- rootEntityType: Guitar
|
|
893
|
+
initialDomainEntities:
|
|
894
|
+
- id: Guitar-1
|
|
895
|
+
type: Guitar
|
|
896
|
+
bodyShape: Offset
|
|
897
|
+
finish: Seafoam
|
|
898
|
+
pickguard: Parchment
|
|
899
|
+
neckWood: Maple
|
|
900
|
+
fretboard: Rosewood
|
|
901
|
+
pickups: SingleCoils
|
|
902
|
+
hardware: Chrome
|
|
903
|
+
signature: ''
|
|
904
|
+
|
|
905
|
+
# One state per wizard step, and `ordered` for the confirmation after the order
|
|
906
|
+
navigationStates:
|
|
907
|
+
- body
|
|
908
|
+
- neck
|
|
909
|
+
- electronics
|
|
910
|
+
- details
|
|
911
|
+
- summary
|
|
912
|
+
- ordered
|
|
913
|
+
|
|
914
|
+
# The longest signature the headstock takes; the invariant and the layout's counter both read it
|
|
915
|
+
expressions:
|
|
916
|
+
Guitar:
|
|
917
|
+
$signatureLimit:
|
|
918
|
+
type: number
|
|
919
|
+
|
|
920
|
+
invariants:
|
|
921
|
+
Guitar:
|
|
922
|
+
signatureLength:
|
|
923
|
+
types:
|
|
924
|
+
- signatureTooLong
|
|
925
|
+
offsetHumbuckers:
|
|
926
|
+
type: offsetHumbuckers
|
|
927
|
+
severity: warning
|
|
928
|
+
message: The Offset body is routed for single-coil-sized pickups. Choose single coils or P-90s, or the Single-cut body for humbuckers.
|
|
929
|
+
|
|
930
|
+
components:
|
|
931
|
+
Guitar:
|
|
932
|
+
detail:
|
|
933
|
+
main:
|
|
934
|
+
title: Nightjar
|
|
935
|
+
subtitle: Northfret Custom Shop
|
|
936
|
+
description: A hand-built electric guitar. Choose the body, the neck, the electronics and the details, then order it from the summary.
|
|
937
|
+
price:
|
|
938
|
+
main:
|
|
939
|
+
price:
|
|
940
|
+
$read: $price
|
|
941
|
+
navigationSteps:
|
|
942
|
+
main:
|
|
943
|
+
steps:
|
|
944
|
+
- label: Body
|
|
945
|
+
navigationStates:
|
|
946
|
+
- body
|
|
947
|
+
- label: Neck
|
|
948
|
+
navigationStates:
|
|
949
|
+
- neck
|
|
950
|
+
- label: Electronics
|
|
951
|
+
navigationStates:
|
|
952
|
+
- electronics
|
|
953
|
+
- label: Details
|
|
954
|
+
navigationStates:
|
|
955
|
+
- details
|
|
956
|
+
- label: Summary
|
|
957
|
+
navigationStates:
|
|
958
|
+
- summary
|
|
959
|
+
- ordered
|
|
960
|
+
finalizeAction:
|
|
961
|
+
placeOrder:
|
|
962
|
+
variant: primary
|
|
963
|
+
label: Place order
|
|
964
|
+
title: Send the guitar as configured to the Northfret workshop
|
|
965
|
+
icon: ShoppingBag
|
|
966
|
+
handler: placeOrder
|
|
967
|
+
|
|
968
|
+
# Each choice is active on its own step, and on the summary and the confirmation, which list every choice
|
|
969
|
+
attributeSelection:
|
|
970
|
+
bodyShape:
|
|
971
|
+
value:
|
|
972
|
+
$bind: bodyShape
|
|
973
|
+
options:
|
|
974
|
+
- label: Offset
|
|
975
|
+
description: Contoured waist, 3.4 kg
|
|
976
|
+
value: Offset
|
|
977
|
+
- label: Single-cut
|
|
978
|
+
description: Thicker body, long sustain
|
|
979
|
+
value: SingleCut
|
|
980
|
+
$filter:
|
|
981
|
+
navigationStates:
|
|
982
|
+
- body
|
|
983
|
+
- summary
|
|
984
|
+
- ordered
|
|
985
|
+
pickguard:
|
|
986
|
+
value:
|
|
987
|
+
$bind: pickguard
|
|
988
|
+
options:
|
|
989
|
+
- label: Parchment
|
|
990
|
+
value: Parchment
|
|
991
|
+
- label: Black
|
|
992
|
+
value: Black
|
|
993
|
+
- label: Pearloid
|
|
994
|
+
description: Shimmers in the light
|
|
995
|
+
value: Pearloid
|
|
996
|
+
- label: None
|
|
997
|
+
value: None
|
|
998
|
+
$filter:
|
|
999
|
+
navigationStates:
|
|
1000
|
+
- body
|
|
1001
|
+
- summary
|
|
1002
|
+
- ordered
|
|
1003
|
+
neckWood:
|
|
1004
|
+
value:
|
|
1005
|
+
$bind: neckWood
|
|
1006
|
+
options:
|
|
1007
|
+
- label: Maple
|
|
1008
|
+
description: Bright and snappy
|
|
1009
|
+
value: Maple
|
|
1010
|
+
- label: Roasted maple
|
|
1011
|
+
description: Baked for stability, caramel colour
|
|
1012
|
+
value: RoastedMaple
|
|
1013
|
+
- label: Mahogany
|
|
1014
|
+
description: Warm and round
|
|
1015
|
+
value: Mahogany
|
|
1016
|
+
$filter:
|
|
1017
|
+
navigationStates:
|
|
1018
|
+
- neck
|
|
1019
|
+
- summary
|
|
1020
|
+
- ordered
|
|
1021
|
+
fretboard:
|
|
1022
|
+
value:
|
|
1023
|
+
$bind: fretboard
|
|
1024
|
+
options:
|
|
1025
|
+
- label: Maple
|
|
1026
|
+
value: Maple
|
|
1027
|
+
- label: Rosewood
|
|
1028
|
+
value: Rosewood
|
|
1029
|
+
- label: Ebony
|
|
1030
|
+
description: Dense and smooth
|
|
1031
|
+
value: Ebony
|
|
1032
|
+
$filter:
|
|
1033
|
+
navigationStates:
|
|
1034
|
+
- neck
|
|
1035
|
+
- summary
|
|
1036
|
+
- ordered
|
|
1037
|
+
pickups:
|
|
1038
|
+
value:
|
|
1039
|
+
$bind: pickups
|
|
1040
|
+
options:
|
|
1041
|
+
- label: Single coils
|
|
1042
|
+
description: Glassy and articulate
|
|
1043
|
+
value: SingleCoils
|
|
1044
|
+
- label: Humbuckers
|
|
1045
|
+
description: Thick and hum-free
|
|
1046
|
+
value: Humbuckers
|
|
1047
|
+
- label: P-90s
|
|
1048
|
+
description: Gritty, with bite
|
|
1049
|
+
value: P90s
|
|
1050
|
+
$filter:
|
|
1051
|
+
navigationStates:
|
|
1052
|
+
- electronics
|
|
1053
|
+
- summary
|
|
1054
|
+
- ordered
|
|
1055
|
+
hardware:
|
|
1056
|
+
value:
|
|
1057
|
+
$bind: hardware
|
|
1058
|
+
options:
|
|
1059
|
+
- label: Chrome
|
|
1060
|
+
value: Chrome
|
|
1061
|
+
- label: Gold
|
|
1062
|
+
value: Gold
|
|
1063
|
+
- label: Black
|
|
1064
|
+
value: Black
|
|
1065
|
+
$filter:
|
|
1066
|
+
navigationStates:
|
|
1067
|
+
- electronics
|
|
1068
|
+
- summary
|
|
1069
|
+
- ordered
|
|
1070
|
+
materialSwatch:
|
|
1071
|
+
finish:
|
|
1072
|
+
selectedSwatchKey:
|
|
1073
|
+
$bind: finish
|
|
1074
|
+
swatches:
|
|
1075
|
+
Seafoam:
|
|
1076
|
+
swatchName: Seafoam green
|
|
1077
|
+
color: '#8fcab6'
|
|
1078
|
+
CandyRed:
|
|
1079
|
+
swatchName: Candy apple red
|
|
1080
|
+
color: '#b0141f'
|
|
1081
|
+
Midnight:
|
|
1082
|
+
swatchName: Midnight blue metallic
|
|
1083
|
+
color: '#1d2b4f'
|
|
1084
|
+
NaturalAsh:
|
|
1085
|
+
swatchName: Natural ash
|
|
1086
|
+
color: '#d8b98a'
|
|
1087
|
+
$filter:
|
|
1088
|
+
navigationStates:
|
|
1089
|
+
- body
|
|
1090
|
+
- summary
|
|
1091
|
+
- ordered
|
|
1092
|
+
textValue:
|
|
1093
|
+
signature:
|
|
1094
|
+
value:
|
|
1095
|
+
$bind: signature
|
|
1096
|
+
$filter:
|
|
1097
|
+
navigationStates:
|
|
1098
|
+
- details
|
|
1099
|
+
- summary
|
|
1100
|
+
- ordered
|
|
1101
|
+
|
|
1102
|
+
canvasConfig:
|
|
1103
|
+
main:
|
|
1104
|
+
highlightingDisabled: true
|
|
1105
|
+
canvasCustomEnvironment:
|
|
1106
|
+
main:
|
|
1107
|
+
name: NightjarStage
|
|
1108
|
+
|
|
1109
|
+
# The camera follows the wizard: the whole guitar for the body and the summary, then a closer view of what each
|
|
1110
|
+
# step changes. Each camera is active only in its step's navigation state; the scene is in millimetres.
|
|
1111
|
+
fitRootPerspectiveCamera:
|
|
1112
|
+
overview:
|
|
1113
|
+
fov: 30
|
|
1114
|
+
near: 10
|
|
1115
|
+
far: 20000
|
|
1116
|
+
minDistance: 600
|
|
1117
|
+
maxDistance: 9000
|
|
1118
|
+
azimuthAngle: 0.28
|
|
1119
|
+
polarAngle: 1.48
|
|
1120
|
+
minPolarAngle: 0.9
|
|
1121
|
+
maxPolarAngle: 2
|
|
1122
|
+
fitToBox: true
|
|
1123
|
+
fitMeshChanges: false
|
|
1124
|
+
$filter:
|
|
1125
|
+
navigationStates:
|
|
1126
|
+
- body
|
|
1127
|
+
- summary
|
|
1128
|
+
- ordered
|
|
1129
|
+
staticPerspectiveCamera:
|
|
1130
|
+
neck:
|
|
1131
|
+
fov: 30
|
|
1132
|
+
near: 10
|
|
1133
|
+
far: 20000
|
|
1134
|
+
lookAtX: 0
|
|
1135
|
+
lookAtY: 625
|
|
1136
|
+
lookAtZ: 0
|
|
1137
|
+
distance: 1350
|
|
1138
|
+
minDistance: 300
|
|
1139
|
+
maxDistance: 3000
|
|
1140
|
+
azimuthAngle: 0.55
|
|
1141
|
+
polarAngle: 1.36
|
|
1142
|
+
$filter:
|
|
1143
|
+
navigationStates:
|
|
1144
|
+
- neck
|
|
1145
|
+
electronics:
|
|
1146
|
+
fov: 30
|
|
1147
|
+
near: 10
|
|
1148
|
+
far: 20000
|
|
1149
|
+
lookAtX: 0
|
|
1150
|
+
lookAtY: 40
|
|
1151
|
+
lookAtZ: 0
|
|
1152
|
+
distance: 1150
|
|
1153
|
+
minDistance: 300
|
|
1154
|
+
maxDistance: 3000
|
|
1155
|
+
azimuthAngle: -0.32
|
|
1156
|
+
polarAngle: 1.22
|
|
1157
|
+
$filter:
|
|
1158
|
+
navigationStates:
|
|
1159
|
+
- electronics
|
|
1160
|
+
details:
|
|
1161
|
+
fov: 30
|
|
1162
|
+
near: 10
|
|
1163
|
+
far: 20000
|
|
1164
|
+
lookAtX: -10
|
|
1165
|
+
lookAtY: 750
|
|
1166
|
+
lookAtZ: 0
|
|
1167
|
+
distance: 520
|
|
1168
|
+
minDistance: 200
|
|
1169
|
+
maxDistance: 3000
|
|
1170
|
+
azimuthAngle: 0.12
|
|
1171
|
+
polarAngle: 1.5
|
|
1172
|
+
$filter:
|
|
1173
|
+
navigationStates:
|
|
1174
|
+
- details
|
|
1175
|
+
|
|
1176
|
+
sceneEntityRemoteObject:
|
|
1177
|
+
bodyOffset:
|
|
1178
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1179
|
+
objectName: BodyOffset
|
|
1180
|
+
dimensionUnit: m
|
|
1181
|
+
bodySingleCut:
|
|
1182
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1183
|
+
objectName: BodySingleCut
|
|
1184
|
+
dimensionUnit: m
|
|
1185
|
+
pickguardOffset:
|
|
1186
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1187
|
+
objectName: PickguardOffset
|
|
1188
|
+
dimensionUnit: m
|
|
1189
|
+
pickguardSingleCut:
|
|
1190
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1191
|
+
objectName: PickguardSingleCut
|
|
1192
|
+
dimensionUnit: m
|
|
1193
|
+
neck:
|
|
1194
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1195
|
+
objectName: Neck
|
|
1196
|
+
dimensionUnit: m
|
|
1197
|
+
headstock:
|
|
1198
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1199
|
+
objectName: Headstock
|
|
1200
|
+
dimensionUnit: m
|
|
1201
|
+
fretboard:
|
|
1202
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1203
|
+
objectName: Fretboard
|
|
1204
|
+
dimensionUnit: m
|
|
1205
|
+
frets:
|
|
1206
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1207
|
+
objectName: Frets
|
|
1208
|
+
dimensionUnit: m
|
|
1209
|
+
inlays:
|
|
1210
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1211
|
+
objectName: Inlays
|
|
1212
|
+
dimensionUnit: m
|
|
1213
|
+
nut:
|
|
1214
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1215
|
+
objectName: Nut
|
|
1216
|
+
dimensionUnit: m
|
|
1217
|
+
strings:
|
|
1218
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1219
|
+
objectName: Strings
|
|
1220
|
+
dimensionUnit: m
|
|
1221
|
+
tuners:
|
|
1222
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1223
|
+
objectName: Tuners
|
|
1224
|
+
dimensionUnit: m
|
|
1225
|
+
pickupsSingleCoil:
|
|
1226
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1227
|
+
objectName: PickupsSingleCoil
|
|
1228
|
+
dimensionUnit: m
|
|
1229
|
+
polesSingleCoil:
|
|
1230
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1231
|
+
objectName: PolesSingleCoil
|
|
1232
|
+
dimensionUnit: m
|
|
1233
|
+
pickupsHumbucker:
|
|
1234
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1235
|
+
objectName: PickupsHumbucker
|
|
1236
|
+
dimensionUnit: m
|
|
1237
|
+
polesHumbucker:
|
|
1238
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1239
|
+
objectName: PolesHumbucker
|
|
1240
|
+
dimensionUnit: m
|
|
1241
|
+
pickupsP90:
|
|
1242
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1243
|
+
objectName: PickupsP90
|
|
1244
|
+
dimensionUnit: m
|
|
1245
|
+
polesP90:
|
|
1246
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1247
|
+
objectName: PolesP90
|
|
1248
|
+
dimensionUnit: m
|
|
1249
|
+
bridgeTremolo:
|
|
1250
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1251
|
+
objectName: BridgeTremolo
|
|
1252
|
+
dimensionUnit: m
|
|
1253
|
+
bridgeTuneOMatic:
|
|
1254
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1255
|
+
objectName: BridgeTuneOMatic
|
|
1256
|
+
dimensionUnit: m
|
|
1257
|
+
knobsOffset:
|
|
1258
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1259
|
+
objectName: KnobsOffset
|
|
1260
|
+
dimensionUnit: m
|
|
1261
|
+
knobsSingleCut:
|
|
1262
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1263
|
+
objectName: KnobsSingleCut
|
|
1264
|
+
dimensionUnit: m
|
|
1265
|
+
switchOffset:
|
|
1266
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1267
|
+
objectName: SwitchOffset
|
|
1268
|
+
dimensionUnit: m
|
|
1269
|
+
switchSingleCut:
|
|
1270
|
+
fileUrl: /models/gallery/guitar.glb
|
|
1271
|
+
objectName: SwitchSingleCut
|
|
1272
|
+
dimensionUnit: m
|
|
1273
|
+
|
|
1274
|
+
# Each body brings its own pickguard, bridge, knobs and switch; the pickups show the chosen set
|
|
1275
|
+
sceneEntityObjectProperties:
|
|
1276
|
+
bodyOffset:
|
|
1277
|
+
visible:
|
|
1278
|
+
$eval: self.bodyShape === 'Offset'
|
|
1279
|
+
bodySingleCut:
|
|
1280
|
+
visible:
|
|
1281
|
+
$eval: self.bodyShape === 'SingleCut'
|
|
1282
|
+
pickguardOffset:
|
|
1283
|
+
visible:
|
|
1284
|
+
$eval: self.bodyShape === 'Offset' && self.pickguard !== 'None'
|
|
1285
|
+
pickguardSingleCut:
|
|
1286
|
+
visible:
|
|
1287
|
+
$eval: self.bodyShape === 'SingleCut' && self.pickguard !== 'None'
|
|
1288
|
+
bridgeTremolo:
|
|
1289
|
+
visible:
|
|
1290
|
+
$eval: self.bodyShape === 'Offset'
|
|
1291
|
+
bridgeTuneOMatic:
|
|
1292
|
+
visible:
|
|
1293
|
+
$eval: self.bodyShape === 'SingleCut'
|
|
1294
|
+
knobsOffset:
|
|
1295
|
+
visible:
|
|
1296
|
+
$eval: self.bodyShape === 'Offset'
|
|
1297
|
+
knobsSingleCut:
|
|
1298
|
+
visible:
|
|
1299
|
+
$eval: self.bodyShape === 'SingleCut'
|
|
1300
|
+
switchOffset:
|
|
1301
|
+
visible:
|
|
1302
|
+
$eval: self.bodyShape === 'Offset'
|
|
1303
|
+
switchSingleCut:
|
|
1304
|
+
visible:
|
|
1305
|
+
$eval: self.bodyShape === 'SingleCut'
|
|
1306
|
+
pickupsSingleCoil:
|
|
1307
|
+
visible:
|
|
1308
|
+
$eval: self.pickups === 'SingleCoils'
|
|
1309
|
+
polesSingleCoil:
|
|
1310
|
+
visible:
|
|
1311
|
+
$eval: self.pickups === 'SingleCoils'
|
|
1312
|
+
pickupsHumbucker:
|
|
1313
|
+
visible:
|
|
1314
|
+
$eval: self.pickups === 'Humbuckers'
|
|
1315
|
+
polesHumbucker:
|
|
1316
|
+
visible:
|
|
1317
|
+
$eval: self.pickups === 'Humbuckers'
|
|
1318
|
+
pickupsP90:
|
|
1319
|
+
visible:
|
|
1320
|
+
$eval: self.pickups === 'P90s'
|
|
1321
|
+
polesP90:
|
|
1322
|
+
visible:
|
|
1323
|
+
$eval: self.pickups === 'P90s'
|
|
1324
|
+
|
|
1325
|
+
# The nitro finish: a physical material with a clearcoat on each body object; natural ash shows the grain
|
|
1326
|
+
sceneEntityObjectLocalMaterialOverride:
|
|
1327
|
+
bodyOffset:
|
|
1328
|
+
materialsToOverride:
|
|
1329
|
+
- Finish
|
|
1330
|
+
materialType: physical
|
|
1331
|
+
color:
|
|
1332
|
+
$read: $finishColor
|
|
1333
|
+
textureUrl:
|
|
1334
|
+
$read: $finishTexture
|
|
1335
|
+
rotation: 1.5708
|
|
1336
|
+
repeatX: 2.2
|
|
1337
|
+
repeatY: 2.2
|
|
1338
|
+
metalness:
|
|
1339
|
+
$read: $finishMetalness
|
|
1340
|
+
roughness:
|
|
1341
|
+
$read: $finishRoughness
|
|
1342
|
+
clearcoat: 1
|
|
1343
|
+
clearcoatRoughness: 0.04
|
|
1344
|
+
bodySingleCut:
|
|
1345
|
+
materialsToOverride:
|
|
1346
|
+
- Finish
|
|
1347
|
+
materialType: physical
|
|
1348
|
+
color:
|
|
1349
|
+
$read: $finishColor
|
|
1350
|
+
textureUrl:
|
|
1351
|
+
$read: $finishTexture
|
|
1352
|
+
rotation: 1.5708
|
|
1353
|
+
repeatX: 2.2
|
|
1354
|
+
repeatY: 2.2
|
|
1355
|
+
metalness:
|
|
1356
|
+
$read: $finishMetalness
|
|
1357
|
+
roughness:
|
|
1358
|
+
$read: $finishRoughness
|
|
1359
|
+
clearcoat: 1
|
|
1360
|
+
clearcoatRoughness: 0.04
|
|
1361
|
+
|
|
1362
|
+
# The materials many parts share change for every object that carries them
|
|
1363
|
+
sceneEntityLocalMaterialOverride:
|
|
1364
|
+
pickguard:
|
|
1365
|
+
materialsToOverride:
|
|
1366
|
+
- Pickguard
|
|
1367
|
+
materialType: physical
|
|
1368
|
+
color:
|
|
1369
|
+
$read: $pickguardColor
|
|
1370
|
+
roughness:
|
|
1371
|
+
$read: $pickguardRoughness
|
|
1372
|
+
metalness: 0
|
|
1373
|
+
iridescence:
|
|
1374
|
+
$read: $pickguardIridescence
|
|
1375
|
+
iridescenceIor: 1.4
|
|
1376
|
+
clearcoat:
|
|
1377
|
+
$read: $pickguardIridescence
|
|
1378
|
+
neck:
|
|
1379
|
+
materialsToOverride:
|
|
1380
|
+
- Neck
|
|
1381
|
+
materialType: physical
|
|
1382
|
+
color:
|
|
1383
|
+
$read: $neckColor
|
|
1384
|
+
textureUrl:
|
|
1385
|
+
$read: $neckTexture
|
|
1386
|
+
repeatX: 5
|
|
1387
|
+
repeatY: 1
|
|
1388
|
+
offsetX: 0.5
|
|
1389
|
+
metalness: 0
|
|
1390
|
+
roughness: 0.42
|
|
1391
|
+
clearcoat: 0.6
|
|
1392
|
+
clearcoatRoughness: 0.2
|
|
1393
|
+
fretboard:
|
|
1394
|
+
materialsToOverride:
|
|
1395
|
+
- Fretboard
|
|
1396
|
+
color:
|
|
1397
|
+
$read: $fretboardColor
|
|
1398
|
+
textureUrl:
|
|
1399
|
+
$read: $fretboardTexture
|
|
1400
|
+
repeatX: 5
|
|
1401
|
+
repeatY: 1
|
|
1402
|
+
offsetX: 0.5
|
|
1403
|
+
metalness: 0
|
|
1404
|
+
roughness: 0.62
|
|
1405
|
+
inlays:
|
|
1406
|
+
materialsToOverride:
|
|
1407
|
+
- Inlay
|
|
1408
|
+
color:
|
|
1409
|
+
$read: $inlayColor
|
|
1410
|
+
metalness: 0
|
|
1411
|
+
roughness: 0.3
|
|
1412
|
+
hardware:
|
|
1413
|
+
materialsToOverride:
|
|
1414
|
+
- Hardware
|
|
1415
|
+
color:
|
|
1416
|
+
$read: $hardwareColor
|
|
1417
|
+
metalness:
|
|
1418
|
+
$read: $hardwareMetalness
|
|
1419
|
+
roughness:
|
|
1420
|
+
$read: $hardwareRoughness
|
|
1421
|
+
pickupCover:
|
|
1422
|
+
materialsToOverride:
|
|
1423
|
+
- PickupCover
|
|
1424
|
+
color:
|
|
1425
|
+
$read: $pickupCoverColor
|
|
1426
|
+
metalness: 0
|
|
1427
|
+
roughness: 0.45
|
|
1428
|
+
|
|
1429
|
+
# The signature in script on the headstock's face, along its length; the text is the customer's own
|
|
1430
|
+
sceneEntityObjectTextDecal:
|
|
1431
|
+
headstock:
|
|
1432
|
+
text:
|
|
1433
|
+
$read: signature
|
|
1434
|
+
font: SignPainter-HouseScript
|
|
1435
|
+
preloadFonts:
|
|
1436
|
+
- SignPainter-HouseScript
|
|
1437
|
+
fontSize: auto
|
|
1438
|
+
textColor:
|
|
1439
|
+
$read: $signatureColor
|
|
1440
|
+
textCanvasHeight: 256
|
|
1441
|
+
positionX: 0.002
|
|
1442
|
+
positionY: 0.76
|
|
1443
|
+
positionZ: 0.004
|
|
1444
|
+
rotationX: 0
|
|
1445
|
+
rotationY: 0
|
|
1446
|
+
rotationZ: 1.5708
|
|
1447
|
+
sizeX: 0.13
|
|
1448
|
+
sizeY: 0.03
|
|
1449
|
+
sizeZ: 0.02
|
|
1450
|
+
roughness: 0.4
|
|
1451
|
+
metalness: 0.2
|
|
1452
|
+
bumpScale: 0.4
|
|
1453
|
+
invertedBump: true
|
|
1454
|
+
```
|
|
1455
|
+
|
|
1456
|
+
### ./src/\_fields/ChoiceField.tsx
|
|
1457
|
+
|
|
1458
|
+
```tsx
|
|
1459
|
+
import { Box, ButtonBase, Typography } from '@mui/material'
|
|
1460
|
+
import type { KeyboardEvent } from 'react'
|
|
1461
|
+
|
|
1462
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
1463
|
+
import { FieldShell } from './FieldShell.js'
|
|
1464
|
+
import { NIGHTJAR } from './tokens.js'
|
|
1465
|
+
|
|
1466
|
+
// The body outlines of the model, drawn small, with a stub of the neck
|
|
1467
|
+
const BODY_PATHS: Readonly<Record<string, string>> = {
|
|
1468
|
+
Offset:
|
|
1469
|
+
'M20 52L23.2 51.8L26.4 51.6L29.6 50.8L32.5 49.5L35.1 47.6L36.9 45L38.1 41.9L38.6 38.8L38.5 35.6L37.8 32.4L36.8 29.4L35.5 26.4L34.6 23.3L34.3 20.1L34.8 17L35 13.7L34.4 10.6L32.8 7.8L30.1 6.2L27.4 7.7L25.5 10.3L22.6 10.7L19.4 10.1L16.4 8.9L15.2 6L14.1 3L11.7 1L8.7 2L7 4.7L6.1 7.8L5.4 10.9L5.3 14.1L5.8 17.3L5.5 20.5L4.8 23.6L3.8 26.7L2.8 29.8L2.2 33L1.8 36.2L1.8 39.4L2.1 42.6L3.1 45.7L4.9 48.3L7.3 50.4L10.4 51.5L13.6 52L16.8 52.1Z',
|
|
1470
|
+
SingleCut:
|
|
1471
|
+
'M20 51L22.9 50.9L25.7 50.7L28.5 50.2L31.2 49.1L33.6 47.5L35.4 45.3L36.5 42.7L37.1 39.9L37.3 37L36.9 34.2L36 31.5L34.5 29L33.2 26.5L32.2 23.8L32.2 21L31.9 18.1L31 15.4L29.6 12.9L27.7 10.8L25.1 11.6L23.9 14.2L23 12.2L23.1 9.3L21.6 7.1L18.9 6.2L16 6L13.2 6.5L10.6 7.7L8.5 9.6L7.3 12.2L7 15L7.6 17.8L7.3 20.7L6.5 23.4L5.6 26.2L4.6 28.8L3.8 31.6L3.3 34.4L3.1 37.3L3.2 40.1L3.7 43L4.7 45.6L6.4 47.9L8.8 49.5L11.4 50.6L14.3 51L17.1 51.1Z',
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// Two pickups as seen from the front: thin single coils, double humbuckers, wide P-90s
|
|
1475
|
+
const PICKUP_SHAPES: Readonly<Record<string, readonly { x: number; y: number; width: number; height: number }[]>> = {
|
|
1476
|
+
SingleCoils: [
|
|
1477
|
+
{ x: 8, y: 13, width: 24, height: 5 },
|
|
1478
|
+
{ x: 8, y: 30, width: 24, height: 5 },
|
|
1479
|
+
],
|
|
1480
|
+
Humbuckers: [
|
|
1481
|
+
{ x: 9, y: 9, width: 22, height: 5 },
|
|
1482
|
+
{ x: 9, y: 15, width: 22, height: 5 },
|
|
1483
|
+
{ x: 9, y: 28, width: 22, height: 5 },
|
|
1484
|
+
{ x: 9, y: 34, width: 22, height: 5 },
|
|
1485
|
+
],
|
|
1486
|
+
P90s: [
|
|
1487
|
+
{ x: 6, y: 10, width: 28, height: 10 },
|
|
1488
|
+
{ x: 6, y: 28, width: 28, height: 10 },
|
|
1489
|
+
],
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
// Swatches as CSS backgrounds: wood with a hint of grain, metal with a highlight, the pearloid's shimmer
|
|
1493
|
+
const WOOD_GRAIN = 'repeating-linear-gradient(100deg, rgba(0, 0, 0, 0.08) 0 2px, transparent 2px 6px)'
|
|
1494
|
+
|
|
1495
|
+
const SWATCHES: Readonly<Record<string, Readonly<Record<string, string>>>> = {
|
|
1496
|
+
pickguard: {
|
|
1497
|
+
Parchment: '#ece2cb',
|
|
1498
|
+
Black: 'radial-gradient(circle at 30% 30%, #3a3a40, #0c0c0e 70%)',
|
|
1499
|
+
Pearloid: 'conic-gradient(from 40deg, #f6f1ea, #e6eef6, #f4e6f0, #eef6ea, #f6f1ea)',
|
|
1500
|
+
None: 'transparent',
|
|
1501
|
+
},
|
|
1502
|
+
neckWood: {
|
|
1503
|
+
Maple: `${WOOD_GRAIN}, #ecd3a3`,
|
|
1504
|
+
RoastedMaple: `${WOOD_GRAIN}, #b9804a`,
|
|
1505
|
+
Mahogany: `${WOOD_GRAIN}, #7b3f28`,
|
|
1506
|
+
},
|
|
1507
|
+
fretboard: {
|
|
1508
|
+
Maple: `${WOOD_GRAIN}, #ecd3a3`,
|
|
1509
|
+
Rosewood: `${WOOD_GRAIN}, #4a2a1c`,
|
|
1510
|
+
Ebony: `${WOOD_GRAIN}, #1d1916`,
|
|
1511
|
+
},
|
|
1512
|
+
hardware: {
|
|
1513
|
+
Chrome: 'linear-gradient(135deg, #ffffff, #9aa1a8 55%, #e4e7ea)',
|
|
1514
|
+
Gold: 'linear-gradient(135deg, #fff1c4, #b8862e 55%, #f3d38a)',
|
|
1515
|
+
Black: 'linear-gradient(135deg, #6a6a70, #121214 55%, #3a3a40)',
|
|
1516
|
+
},
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
interface OptionArtProps {
|
|
1520
|
+
readonly instanceName: string
|
|
1521
|
+
readonly value: string
|
|
1522
|
+
readonly size: number
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// The small picture beside an option: a silhouette for the body, the pickup layout, or a swatch of the material
|
|
1526
|
+
const OptionArt = ({ instanceName, value, size }: OptionArtProps) => {
|
|
1527
|
+
const bodyPath = instanceName === 'bodyShape' ? BODY_PATHS[value] : undefined
|
|
1528
|
+
const pickups = instanceName === 'pickups' ? PICKUP_SHAPES[value] : undefined
|
|
1529
|
+
const swatch = SWATCHES[instanceName]?.[value]
|
|
1530
|
+
|
|
1531
|
+
if (bodyPath) {
|
|
1532
|
+
return (
|
|
1533
|
+
<Box component="svg" viewBox="0 0 40 54" aria-hidden sx={{ inlineSize: size, blockSize: size, flex: 'none' }}>
|
|
1534
|
+
<rect x={17.8} y={0} width={4.4} height={12} rx={1} fill={NIGHTJAR.faint} />
|
|
1535
|
+
<path d={bodyPath} fill={NIGHTJAR.cream} />
|
|
1536
|
+
</Box>
|
|
1537
|
+
)
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
if (pickups) {
|
|
1541
|
+
return (
|
|
1542
|
+
<Box component="svg" viewBox="0 0 40 48" aria-hidden sx={{ inlineSize: size, blockSize: size, flex: 'none' }}>
|
|
1543
|
+
{pickups.map(shape => (
|
|
1544
|
+
<rect key={`${shape.x}-${shape.y}`} {...shape} rx={2.5} fill={NIGHTJAR.cream} />
|
|
1545
|
+
))}
|
|
1546
|
+
</Box>
|
|
1547
|
+
)
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
if (swatch === undefined) {
|
|
1551
|
+
return null
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
return (
|
|
1555
|
+
<Box
|
|
1556
|
+
component="span"
|
|
1557
|
+
aria-hidden
|
|
1558
|
+
sx={{
|
|
1559
|
+
inlineSize: size,
|
|
1560
|
+
blockSize: size,
|
|
1561
|
+
flex: 'none',
|
|
1562
|
+
borderRadius: '50%',
|
|
1563
|
+
background: swatch,
|
|
1564
|
+
boxShadow: `inset 0 0 0 1px ${NIGHTJAR.line}`,
|
|
1565
|
+
// "None" is an empty, dashed ring
|
|
1566
|
+
...(value === 'None' && {
|
|
1567
|
+
border: `1.5px dashed ${NIGHTJAR.faint}`,
|
|
1568
|
+
boxShadow: 'none',
|
|
1569
|
+
}),
|
|
1570
|
+
}}
|
|
1571
|
+
/>
|
|
1572
|
+
)
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// Arrow keys move the selection between the options, as in a native radio group
|
|
1576
|
+
const ARROW_STEPS: Readonly<Record<string, number | undefined>> = {
|
|
1577
|
+
ArrowRight: 1,
|
|
1578
|
+
ArrowDown: 1,
|
|
1579
|
+
ArrowLeft: -1,
|
|
1580
|
+
ArrowUp: -1,
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
interface ChoiceFieldProps {
|
|
1584
|
+
readonly instanceName: string
|
|
1585
|
+
readonly label: string
|
|
1586
|
+
readonly warningTypes: readonly string[]
|
|
1587
|
+
/** Chips in one scrolling row, for a phone; cards in a grid otherwise */
|
|
1588
|
+
readonly compact: boolean
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// One `attributeSelection` of the spec: cards with a picture, a title and a description, or chips on a phone. An
|
|
1592
|
+
// option a `fixable` invariant rules out is faded with the reason below; it stays clickable, so the change is tried
|
|
1593
|
+
// and rejected.
|
|
1594
|
+
export const ChoiceField = ({ instanceName, label, warningTypes, compact }: ChoiceFieldProps) => {
|
|
1595
|
+
const resolve = phx.frontend.useResolveLocalizable()
|
|
1596
|
+
const guitarId = phx.frontend.useRootEntityId()
|
|
1597
|
+
const { value, options } = phx.frontend.useAttributeSelection(guitarId, instanceName)
|
|
1598
|
+
const selected = options.find(option => option.value === value)
|
|
1599
|
+
const ruledOut = options.find(option => option.disabled && option.hint)?.hint
|
|
1600
|
+
|
|
1601
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLElement>, index: number) => {
|
|
1602
|
+
const step = ARROW_STEPS[event.key]
|
|
1603
|
+
|
|
1604
|
+
if (step === undefined) {
|
|
1605
|
+
return
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
event.preventDefault()
|
|
1609
|
+
const next = (index + step + options.length) % options.length
|
|
1610
|
+
options[next]?.onSelect()
|
|
1611
|
+
event.currentTarget.parentElement?.querySelectorAll<HTMLElement>('[role="radio"]')[next]?.focus()
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
return (
|
|
1615
|
+
<FieldShell
|
|
1616
|
+
id={`nightjar-${instanceName}`}
|
|
1617
|
+
label={label}
|
|
1618
|
+
aside={
|
|
1619
|
+
compact && selected ? (
|
|
1620
|
+
<Typography sx={{ fontFamily: NIGHTJAR.font, fontSize: 14, color: NIGHTJAR.cream }}>
|
|
1621
|
+
{resolve(selected.title)}
|
|
1622
|
+
</Typography>
|
|
1623
|
+
) : undefined
|
|
1624
|
+
}
|
|
1625
|
+
hint={ruledOut}
|
|
1626
|
+
warningTypes={warningTypes}
|
|
1627
|
+
compact={compact}
|
|
1628
|
+
>
|
|
1629
|
+
<Box
|
|
1630
|
+
component="div"
|
|
1631
|
+
role="radiogroup"
|
|
1632
|
+
aria-labelledby={`nightjar-${instanceName}-label`}
|
|
1633
|
+
sx={
|
|
1634
|
+
compact
|
|
1635
|
+
? {
|
|
1636
|
+
display: 'flex',
|
|
1637
|
+
gap: 1,
|
|
1638
|
+
marginInline: -2,
|
|
1639
|
+
paddingInline: 2,
|
|
1640
|
+
paddingBlockEnd: 0.5,
|
|
1641
|
+
overflowX: 'auto',
|
|
1642
|
+
scrollbarWidth: 'none',
|
|
1643
|
+
}
|
|
1644
|
+
: { display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 1 }
|
|
1645
|
+
}
|
|
1646
|
+
>
|
|
1647
|
+
{options.map((option, index) => {
|
|
1648
|
+
const isSelected = option.value === value
|
|
1649
|
+
const description = resolve(option.description)
|
|
1650
|
+
|
|
1651
|
+
return (
|
|
1652
|
+
<ButtonBase
|
|
1653
|
+
key={String(option.value)}
|
|
1654
|
+
role="radio"
|
|
1655
|
+
aria-checked={isSelected}
|
|
1656
|
+
aria-disabled={option.disabled}
|
|
1657
|
+
tabIndex={isSelected ? 0 : -1}
|
|
1658
|
+
onClick={option.onSelect}
|
|
1659
|
+
onKeyDown={event => handleKeyDown(event, index)}
|
|
1660
|
+
sx={{
|
|
1661
|
+
display: 'flex',
|
|
1662
|
+
alignItems: 'center',
|
|
1663
|
+
justifyContent: 'flex-start',
|
|
1664
|
+
gap: compact ? 1 : 1.5,
|
|
1665
|
+
flex: compact ? 'none' : undefined,
|
|
1666
|
+
minBlockSize: compact ? 44 : 64,
|
|
1667
|
+
paddingInline: compact ? 1.5 : 1.5,
|
|
1668
|
+
paddingBlock: compact ? 0.75 : 1.25,
|
|
1669
|
+
textAlign: 'start',
|
|
1670
|
+
borderRadius: compact ? 999 : 1.5,
|
|
1671
|
+
border: `1px solid ${isSelected ? NIGHTJAR.amber : NIGHTJAR.line}`,
|
|
1672
|
+
bgcolor: isSelected ? NIGHTJAR.amberTint : NIGHTJAR.raised,
|
|
1673
|
+
color: NIGHTJAR.cream,
|
|
1674
|
+
opacity: option.disabled ? 0.45 : 1,
|
|
1675
|
+
transition: 'border-color 160ms, background-color 160ms',
|
|
1676
|
+
'&:hover': { bgcolor: isSelected ? NIGHTJAR.amberTint : NIGHTJAR.raisedHover },
|
|
1677
|
+
'&.Mui-focusVisible': { outline: `2px solid ${NIGHTJAR.amber}`, outlineOffset: 2 },
|
|
1678
|
+
}}
|
|
1679
|
+
>
|
|
1680
|
+
<OptionArt instanceName={instanceName} value={String(option.value)} size={compact ? 22 : 36} />
|
|
1681
|
+
<Box component="span" sx={{ display: 'grid', gap: 0.25 }}>
|
|
1682
|
+
<Typography
|
|
1683
|
+
component="span"
|
|
1684
|
+
sx={{
|
|
1685
|
+
fontFamily: NIGHTJAR.font,
|
|
1686
|
+
fontSize: compact ? 14 : 15,
|
|
1687
|
+
fontWeight: 500,
|
|
1688
|
+
whiteSpace: 'nowrap',
|
|
1689
|
+
color: NIGHTJAR.cream,
|
|
1690
|
+
}}
|
|
1691
|
+
>
|
|
1692
|
+
{resolve(option.title)}
|
|
1693
|
+
</Typography>
|
|
1694
|
+
{!compact && description && (
|
|
1695
|
+
<Typography
|
|
1696
|
+
component="span"
|
|
1697
|
+
sx={{ fontFamily: NIGHTJAR.font, fontSize: 13, lineHeight: 1.3, color: NIGHTJAR.muted }}
|
|
1698
|
+
>
|
|
1699
|
+
{description}
|
|
1700
|
+
</Typography>
|
|
1701
|
+
)}
|
|
1702
|
+
</Box>
|
|
1703
|
+
</ButtonBase>
|
|
1704
|
+
)
|
|
1705
|
+
})}
|
|
1706
|
+
</Box>
|
|
1707
|
+
</FieldShell>
|
|
1708
|
+
)
|
|
1709
|
+
}
|
|
1710
|
+
```
|
|
1711
|
+
|
|
1712
|
+
### ./src/\_fields/FieldShell.tsx
|
|
1713
|
+
|
|
1714
|
+
```tsx
|
|
1715
|
+
import { Box, Typography } from '@mui/material'
|
|
1716
|
+
import type { ReactNode } from 'react'
|
|
1717
|
+
|
|
1718
|
+
import { LABEL_SX, NIGHTJAR } from './tokens.js'
|
|
1719
|
+
import { useWarnings } from './use-warnings.js'
|
|
1720
|
+
|
|
1721
|
+
interface FieldShellProps {
|
|
1722
|
+
readonly id: string
|
|
1723
|
+
readonly label: string
|
|
1724
|
+
/** Beside the label: the chosen option's name, or a counter */
|
|
1725
|
+
readonly aside?: ReactNode
|
|
1726
|
+
/** Below the field: why an option is ruled out, or what the customer should know */
|
|
1727
|
+
readonly hint?: string | undefined
|
|
1728
|
+
readonly warningTypes: readonly string[]
|
|
1729
|
+
readonly compact: boolean
|
|
1730
|
+
readonly children: ReactNode
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// A labelled field. A `warning` violation the field explains is shown below it while it holds; it is the same text
|
|
1734
|
+
// the summary lists, and the stepper marks the step until it is resolved.
|
|
1735
|
+
export const FieldShell = ({ id, label, aside, hint, warningTypes, compact, children }: FieldShellProps) => {
|
|
1736
|
+
const warning = useWarnings().find(candidate => warningTypes.includes(candidate.type))
|
|
1737
|
+
|
|
1738
|
+
return (
|
|
1739
|
+
<Box component="section" aria-labelledby={`${id}-label`} sx={{ display: 'grid', gap: compact ? 1 : 1.5 }}>
|
|
1740
|
+
<Box component="div" sx={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 2 }}>
|
|
1741
|
+
<Typography component="h3" id={`${id}-label`} sx={LABEL_SX}>
|
|
1742
|
+
{label}
|
|
1743
|
+
</Typography>
|
|
1744
|
+
{aside}
|
|
1745
|
+
</Box>
|
|
1746
|
+
{children}
|
|
1747
|
+
{hint && <Typography sx={{ fontFamily: NIGHTJAR.font, fontSize: 13, color: NIGHTJAR.muted }}>{hint}</Typography>}
|
|
1748
|
+
{warning && (
|
|
1749
|
+
<Typography
|
|
1750
|
+
role="status"
|
|
1751
|
+
sx={{
|
|
1752
|
+
paddingInline: 1.5,
|
|
1753
|
+
paddingBlock: 1,
|
|
1754
|
+
borderInlineStart: `3px solid ${NIGHTJAR.amber}`,
|
|
1755
|
+
bgcolor: NIGHTJAR.amberTint,
|
|
1756
|
+
fontFamily: NIGHTJAR.font,
|
|
1757
|
+
fontSize: 14,
|
|
1758
|
+
lineHeight: 1.4,
|
|
1759
|
+
color: NIGHTJAR.cream,
|
|
1760
|
+
}}
|
|
1761
|
+
>
|
|
1762
|
+
{warning.text}
|
|
1763
|
+
</Typography>
|
|
1764
|
+
)}
|
|
1765
|
+
</Box>
|
|
1766
|
+
)
|
|
1767
|
+
}
|
|
1768
|
+
```
|
|
1769
|
+
|
|
1770
|
+
### ./src/\_fields/FinishField.tsx
|
|
1771
|
+
|
|
1772
|
+
```tsx
|
|
1773
|
+
import { Box, ButtonBase, Tooltip, Typography } from '@mui/material'
|
|
1774
|
+
import type { KeyboardEvent } from 'react'
|
|
1775
|
+
|
|
1776
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
1777
|
+
import { FieldShell } from './FieldShell.js'
|
|
1778
|
+
import { NIGHTJAR } from './tokens.js'
|
|
1779
|
+
|
|
1780
|
+
const ARROW_STEPS: Readonly<Record<string, number | undefined>> = {
|
|
1781
|
+
ArrowRight: 1,
|
|
1782
|
+
ArrowDown: 1,
|
|
1783
|
+
ArrowLeft: -1,
|
|
1784
|
+
ArrowUp: -1,
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
// A highlight over each swatch, so the paint reads as a glossy finish
|
|
1788
|
+
const GLOSS = 'radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0) 42%)'
|
|
1789
|
+
|
|
1790
|
+
interface FinishFieldProps {
|
|
1791
|
+
readonly instanceName: string
|
|
1792
|
+
readonly label: string
|
|
1793
|
+
readonly warningTypes: readonly string[]
|
|
1794
|
+
readonly compact: boolean
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
// The spec's `materialSwatch`: round swatches in the colours the spec gives each finish, the chosen one's name
|
|
1798
|
+
// beside the label. `useSwatchSelection` takes the `fixable` violation types to ignore as its third argument.
|
|
1799
|
+
export const FinishField = ({ instanceName, label, warningTypes, compact }: FinishFieldProps) => {
|
|
1800
|
+
const resolve = phx.frontend.useResolveLocalizable()
|
|
1801
|
+
const guitarId = phx.frontend.useRootEntityId()
|
|
1802
|
+
const { options } = phx.frontend.useSwatchSelection(guitarId, instanceName, [])
|
|
1803
|
+
const selected = options.find(option => option.selected)
|
|
1804
|
+
const ruledOut = options.find(option => option.disabled && option.hint)?.hint
|
|
1805
|
+
const size = compact ? 40 : 48
|
|
1806
|
+
|
|
1807
|
+
const handleKeyDown = (event: KeyboardEvent<HTMLElement>, index: number) => {
|
|
1808
|
+
const step = ARROW_STEPS[event.key]
|
|
1809
|
+
|
|
1810
|
+
if (step === undefined) {
|
|
1811
|
+
return
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
event.preventDefault()
|
|
1815
|
+
const next = (index + step + options.length) % options.length
|
|
1816
|
+
options[next]?.onClick()
|
|
1817
|
+
event.currentTarget.parentElement?.querySelectorAll<HTMLElement>('[role="radio"]')[next]?.focus()
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
return (
|
|
1821
|
+
<FieldShell
|
|
1822
|
+
id={`nightjar-${instanceName}`}
|
|
1823
|
+
label={label}
|
|
1824
|
+
aside={
|
|
1825
|
+
selected && (
|
|
1826
|
+
<Typography sx={{ fontFamily: NIGHTJAR.font, fontSize: 14, color: NIGHTJAR.cream }}>
|
|
1827
|
+
{resolve(selected.swatchName)}
|
|
1828
|
+
</Typography>
|
|
1829
|
+
)
|
|
1830
|
+
}
|
|
1831
|
+
hint={ruledOut ?? undefined}
|
|
1832
|
+
warningTypes={warningTypes}
|
|
1833
|
+
compact={compact}
|
|
1834
|
+
>
|
|
1835
|
+
<Box
|
|
1836
|
+
component="div"
|
|
1837
|
+
role="radiogroup"
|
|
1838
|
+
aria-labelledby={`nightjar-${instanceName}-label`}
|
|
1839
|
+
sx={{ display: 'flex', flexWrap: 'wrap', gap: compact ? 1.5 : 2 }}
|
|
1840
|
+
>
|
|
1841
|
+
{options.map((option, index) => {
|
|
1842
|
+
const name = resolve(option.swatchName)
|
|
1843
|
+
|
|
1844
|
+
return (
|
|
1845
|
+
<Tooltip key={option.swatchKey} title={name}>
|
|
1846
|
+
<ButtonBase
|
|
1847
|
+
role="radio"
|
|
1848
|
+
aria-label={name}
|
|
1849
|
+
aria-checked={option.selected}
|
|
1850
|
+
aria-disabled={option.disabled}
|
|
1851
|
+
tabIndex={option.selected ? 0 : -1}
|
|
1852
|
+
onClick={option.onClick}
|
|
1853
|
+
onKeyDown={event => handleKeyDown(event, index)}
|
|
1854
|
+
sx={{
|
|
1855
|
+
inlineSize: size,
|
|
1856
|
+
blockSize: size,
|
|
1857
|
+
borderRadius: '50%',
|
|
1858
|
+
background: `${GLOSS}, ${option.color}`,
|
|
1859
|
+
// The ring sits outside the swatch, in the panel's colour, so the swatch keeps its full size
|
|
1860
|
+
boxShadow: option.selected
|
|
1861
|
+
? `0 0 0 3px ${NIGHTJAR.ink}, 0 0 0 5px ${NIGHTJAR.amber}`
|
|
1862
|
+
: `inset 0 0 0 1px ${NIGHTJAR.line}`,
|
|
1863
|
+
opacity: option.disabled ? 0.4 : 1,
|
|
1864
|
+
transition: 'box-shadow 160ms, transform 160ms',
|
|
1865
|
+
'&:hover': { transform: 'scale(1.06)' },
|
|
1866
|
+
'&.Mui-focusVisible': { outline: `2px solid ${NIGHTJAR.cream}`, outlineOffset: 6 },
|
|
1867
|
+
}}
|
|
1868
|
+
/>
|
|
1869
|
+
</Tooltip>
|
|
1870
|
+
)
|
|
1871
|
+
})}
|
|
1872
|
+
</Box>
|
|
1873
|
+
</FieldShell>
|
|
1874
|
+
)
|
|
1875
|
+
}
|
|
1876
|
+
```
|
|
1877
|
+
|
|
1878
|
+
### ./src/\_fields/SignatureField.tsx
|
|
1879
|
+
|
|
1880
|
+
```tsx
|
|
1881
|
+
import { Box, InputBase, Typography } from '@mui/material'
|
|
1882
|
+
|
|
1883
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
1884
|
+
import { FieldShell } from './FieldShell.js'
|
|
1885
|
+
import { NIGHTJAR } from './tokens.js'
|
|
1886
|
+
|
|
1887
|
+
interface SignatureFieldProps {
|
|
1888
|
+
readonly instanceName: string
|
|
1889
|
+
readonly label: string
|
|
1890
|
+
readonly warningTypes: readonly string[]
|
|
1891
|
+
readonly compact: boolean
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
// The spec's `textValue`, which the spec prints on the headstock with a text decal. Typing stages each value, so the
|
|
1895
|
+
// headstock follows every key; leaving the field or pressing Enter commits it. A value the `fixable` length rule
|
|
1896
|
+
// rejects shows the rule's message as the hook's `hints`, and leaving the field rolls back to the last value that
|
|
1897
|
+
// fitted. The counter reads the limit from the spec's `$signatureLimit` expression, which the rule checks too.
|
|
1898
|
+
export const SignatureField = ({ instanceName, label, warningTypes, compact }: SignatureFieldProps) => {
|
|
1899
|
+
const guitarId = phx.frontend.useRootEntityId()
|
|
1900
|
+
const guitar = phx.frontend.useRootEntityState()
|
|
1901
|
+
const signature = phx.frontend.useTextValue(guitarId, instanceName)
|
|
1902
|
+
const limit = guitar.$signatureLimit
|
|
1903
|
+
|
|
1904
|
+
return (
|
|
1905
|
+
<FieldShell
|
|
1906
|
+
id={`nightjar-${instanceName}`}
|
|
1907
|
+
label={label}
|
|
1908
|
+
aside={
|
|
1909
|
+
<Typography
|
|
1910
|
+
aria-live="polite"
|
|
1911
|
+
sx={{
|
|
1912
|
+
fontFamily: NIGHTJAR.font,
|
|
1913
|
+
fontSize: 13,
|
|
1914
|
+
fontVariantNumeric: 'tabular-nums',
|
|
1915
|
+
color: signature.hasErrors ? NIGHTJAR.alert : NIGHTJAR.muted,
|
|
1916
|
+
}}
|
|
1917
|
+
>
|
|
1918
|
+
{signature.value.length} / {limit}
|
|
1919
|
+
</Typography>
|
|
1920
|
+
}
|
|
1921
|
+
hint={
|
|
1922
|
+
compact ? undefined : 'In gold leaf on dark necks, in black on maple. Leave it empty for a plain headstock.'
|
|
1923
|
+
}
|
|
1924
|
+
warningTypes={warningTypes}
|
|
1925
|
+
compact={compact}
|
|
1926
|
+
>
|
|
1927
|
+
<Box
|
|
1928
|
+
component="div"
|
|
1929
|
+
sx={{
|
|
1930
|
+
display: 'grid',
|
|
1931
|
+
gap: 1,
|
|
1932
|
+
paddingInline: 2,
|
|
1933
|
+
paddingBlock: compact ? 1 : 1.5,
|
|
1934
|
+
borderRadius: 1.5,
|
|
1935
|
+
border: `1px solid ${signature.hasErrors ? NIGHTJAR.alert : NIGHTJAR.line}`,
|
|
1936
|
+
bgcolor: NIGHTJAR.raised,
|
|
1937
|
+
'&:focus-within': { borderColor: signature.hasErrors ? NIGHTJAR.alert : NIGHTJAR.amber },
|
|
1938
|
+
}}
|
|
1939
|
+
>
|
|
1940
|
+
<InputBase
|
|
1941
|
+
value={signature.value}
|
|
1942
|
+
placeholder="Your name"
|
|
1943
|
+
onChange={event => signature.update(event, event.target.value)}
|
|
1944
|
+
onBlur={signature.resolve}
|
|
1945
|
+
onKeyDown={event => {
|
|
1946
|
+
if (event.key === 'Enter') {
|
|
1947
|
+
signature.resolve()
|
|
1948
|
+
}
|
|
1949
|
+
}}
|
|
1950
|
+
inputProps={{
|
|
1951
|
+
'aria-labelledby': `nightjar-${instanceName}-label`,
|
|
1952
|
+
'aria-invalid': signature.hasErrors,
|
|
1953
|
+
'aria-describedby': `nightjar-${instanceName}-error`,
|
|
1954
|
+
spellCheck: false,
|
|
1955
|
+
}}
|
|
1956
|
+
sx={{
|
|
1957
|
+
fontFamily: NIGHTJAR.script,
|
|
1958
|
+
fontSize: compact ? 30 : 38,
|
|
1959
|
+
lineHeight: 1.2,
|
|
1960
|
+
color: NIGHTJAR.amber,
|
|
1961
|
+
'& input::placeholder': { color: NIGHTJAR.faint, opacity: 1 },
|
|
1962
|
+
}}
|
|
1963
|
+
/>
|
|
1964
|
+
</Box>
|
|
1965
|
+
<Typography
|
|
1966
|
+
id={`nightjar-${instanceName}-error`}
|
|
1967
|
+
role="alert"
|
|
1968
|
+
sx={{ fontFamily: NIGHTJAR.font, fontSize: 14, color: NIGHTJAR.alert, '&:empty': { display: 'none' } }}
|
|
1969
|
+
>
|
|
1970
|
+
{signature.hasErrors ? signature.hints : ''}
|
|
1971
|
+
</Typography>
|
|
1972
|
+
</FieldShell>
|
|
1973
|
+
)
|
|
1974
|
+
}
|
|
1975
|
+
```
|
|
1976
|
+
|
|
1977
|
+
### ./src/\_fields/StepFields.tsx
|
|
1978
|
+
|
|
1979
|
+
```tsx
|
|
1980
|
+
import { Box } from '@mui/material'
|
|
1981
|
+
|
|
1982
|
+
import { ChoiceField } from './ChoiceField.js'
|
|
1983
|
+
import { FinishField } from './FinishField.js'
|
|
1984
|
+
import { SignatureField } from './SignatureField.js'
|
|
1985
|
+
import { fieldOf } from './fields.js'
|
|
1986
|
+
import { useActiveChoices } from './use-active-choices.js'
|
|
1987
|
+
|
|
1988
|
+
interface StepFieldsProps {
|
|
1989
|
+
readonly compact: boolean
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// The fields of the active step. The layout does not list which choice belongs to which step: it shows the choices
|
|
1993
|
+
// the spec leaves active, so moving a choice to another step is a change of its `$filter` in the spec.
|
|
1994
|
+
export const StepFields = ({ compact }: StepFieldsProps) => {
|
|
1995
|
+
const choices = useActiveChoices()
|
|
1996
|
+
|
|
1997
|
+
return (
|
|
1998
|
+
<Box component="div" sx={{ display: 'grid', gap: compact ? 2.5 : 4 }}>
|
|
1999
|
+
{choices.map(({ instanceName, kind }) => {
|
|
2000
|
+
const { label, warningTypes } = fieldOf(instanceName)
|
|
2001
|
+
const props = { instanceName, label, warningTypes, compact }
|
|
2002
|
+
|
|
2003
|
+
switch (kind) {
|
|
2004
|
+
case 'selection':
|
|
2005
|
+
return <ChoiceField key={instanceName} {...props} />
|
|
2006
|
+
case 'swatch':
|
|
2007
|
+
return <FinishField key={instanceName} {...props} />
|
|
2008
|
+
case 'text':
|
|
2009
|
+
return <SignatureField key={instanceName} {...props} />
|
|
2010
|
+
}
|
|
2011
|
+
})}
|
|
2012
|
+
</Box>
|
|
2013
|
+
)
|
|
2014
|
+
}
|
|
2015
|
+
```
|
|
2016
|
+
|
|
2017
|
+
### ./src/\_fields/SummaryRows.tsx
|
|
2018
|
+
|
|
2019
|
+
```tsx
|
|
2020
|
+
import { Box, Typography } from '@mui/material'
|
|
2021
|
+
import type { ReactNode } from 'react'
|
|
2022
|
+
|
|
2023
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
2024
|
+
import { fieldOf } from './fields.js'
|
|
2025
|
+
import { NIGHTJAR } from './tokens.js'
|
|
2026
|
+
import type { ActiveChoice } from './use-active-choices.js'
|
|
2027
|
+
import { useActiveChoices } from './use-active-choices.js'
|
|
2028
|
+
|
|
2029
|
+
interface RowProps {
|
|
2030
|
+
readonly label: string
|
|
2031
|
+
readonly children: ReactNode
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
const Row = ({ label, children }: RowProps) => {
|
|
2035
|
+
return (
|
|
2036
|
+
<Box
|
|
2037
|
+
component="div"
|
|
2038
|
+
sx={{
|
|
2039
|
+
display: 'flex',
|
|
2040
|
+
alignItems: 'baseline',
|
|
2041
|
+
justifyContent: 'space-between',
|
|
2042
|
+
gap: 2,
|
|
2043
|
+
paddingBlock: 1.1,
|
|
2044
|
+
borderBlockEnd: `1px solid ${NIGHTJAR.line}`,
|
|
2045
|
+
}}
|
|
2046
|
+
>
|
|
2047
|
+
<Typography component="dt" sx={{ fontFamily: NIGHTJAR.font, fontSize: 14, color: NIGHTJAR.muted }}>
|
|
2048
|
+
{label}
|
|
2049
|
+
</Typography>
|
|
2050
|
+
<Typography
|
|
2051
|
+
component="dd"
|
|
2052
|
+
sx={{
|
|
2053
|
+
display: 'flex',
|
|
2054
|
+
alignItems: 'center',
|
|
2055
|
+
gap: 1,
|
|
2056
|
+
margin: 0,
|
|
2057
|
+
fontFamily: NIGHTJAR.font,
|
|
2058
|
+
fontSize: 15,
|
|
2059
|
+
fontWeight: 500,
|
|
2060
|
+
textAlign: 'end',
|
|
2061
|
+
color: NIGHTJAR.cream,
|
|
2062
|
+
}}
|
|
2063
|
+
>
|
|
2064
|
+
{children}
|
|
2065
|
+
</Typography>
|
|
2066
|
+
</Box>
|
|
2067
|
+
)
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
interface ChoiceRowProps {
|
|
2071
|
+
readonly instanceName: string
|
|
2072
|
+
readonly label: string
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// Each row reads its choice with the same control hook the step uses: the option's title, the swatch's name and
|
|
2076
|
+
// colour, the text as written
|
|
2077
|
+
const SelectionRow = ({ instanceName, label }: ChoiceRowProps) => {
|
|
2078
|
+
const resolve = phx.frontend.useResolveLocalizable()
|
|
2079
|
+
const guitarId = phx.frontend.useRootEntityId()
|
|
2080
|
+
const { value, options } = phx.frontend.useAttributeSelection(guitarId, instanceName)
|
|
2081
|
+
const selected = options.find(option => option.value === value)
|
|
2082
|
+
|
|
2083
|
+
return <Row label={label}>{selected ? resolve(selected.title) : '—'}</Row>
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
const SwatchRow = ({ instanceName, label }: ChoiceRowProps) => {
|
|
2087
|
+
const resolve = phx.frontend.useResolveLocalizable()
|
|
2088
|
+
const guitarId = phx.frontend.useRootEntityId()
|
|
2089
|
+
const { options } = phx.frontend.useSwatchSelection(guitarId, instanceName, [])
|
|
2090
|
+
const selected = options.find(option => option.selected)
|
|
2091
|
+
|
|
2092
|
+
return (
|
|
2093
|
+
<Row label={label}>
|
|
2094
|
+
{selected && (
|
|
2095
|
+
<Box
|
|
2096
|
+
component="span"
|
|
2097
|
+
aria-hidden
|
|
2098
|
+
sx={{ inlineSize: 12, blockSize: 12, borderRadius: '50%', bgcolor: selected.color }}
|
|
2099
|
+
/>
|
|
2100
|
+
)}
|
|
2101
|
+
{selected ? resolve(selected.swatchName) : '—'}
|
|
2102
|
+
</Row>
|
|
2103
|
+
)
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
const TextRow = ({ instanceName, label }: ChoiceRowProps) => {
|
|
2107
|
+
const guitarId = phx.frontend.useRootEntityId()
|
|
2108
|
+
const { value } = phx.frontend.useTextValue(guitarId, instanceName)
|
|
2109
|
+
|
|
2110
|
+
return (
|
|
2111
|
+
<Row label={label}>
|
|
2112
|
+
{value.trim() === '' ? (
|
|
2113
|
+
'None'
|
|
2114
|
+
) : (
|
|
2115
|
+
<Box component="span" sx={{ fontFamily: NIGHTJAR.script, fontSize: 24, lineHeight: 1, color: NIGHTJAR.amber }}>
|
|
2116
|
+
{value}
|
|
2117
|
+
</Box>
|
|
2118
|
+
)}
|
|
2119
|
+
</Row>
|
|
2120
|
+
)
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
const ChoiceRow = ({ instanceName, kind }: ActiveChoice) => {
|
|
2124
|
+
const { label } = fieldOf(instanceName)
|
|
2125
|
+
|
|
2126
|
+
switch (kind) {
|
|
2127
|
+
case 'selection':
|
|
2128
|
+
return <SelectionRow instanceName={instanceName} label={label} />
|
|
2129
|
+
case 'swatch':
|
|
2130
|
+
return <SwatchRow instanceName={instanceName} label={label} />
|
|
2131
|
+
case 'text':
|
|
2132
|
+
return <TextRow instanceName={instanceName} label={label} />
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// Every choice, read-only: the summary and the confirmation list the choices the spec leaves active in their states
|
|
2137
|
+
export const SummaryRows = () => {
|
|
2138
|
+
const choices = useActiveChoices()
|
|
2139
|
+
|
|
2140
|
+
return (
|
|
2141
|
+
<Box component="dl" sx={{ margin: 0 }}>
|
|
2142
|
+
{choices.map(choice => (
|
|
2143
|
+
<ChoiceRow key={choice.instanceName} {...choice} />
|
|
2144
|
+
))}
|
|
2145
|
+
</Box>
|
|
2146
|
+
)
|
|
2147
|
+
}
|
|
2148
|
+
```
|
|
2149
|
+
|
|
2150
|
+
### ./src/\_fields/fields.ts
|
|
2151
|
+
|
|
2152
|
+
```ts
|
|
2153
|
+
/** A choice of the spec as the layout presents it. */
|
|
2154
|
+
export interface Field {
|
|
2155
|
+
/** The spec component's instance name */
|
|
2156
|
+
readonly instanceName: string
|
|
2157
|
+
/** The spec's `attributeSelection`, `materialSwatch` and `textValue` carry no label; the layout names the choice */
|
|
2158
|
+
readonly label: string
|
|
2159
|
+
/** The `warning` violations the field explains when they hold */
|
|
2160
|
+
readonly warningTypes: readonly string[]
|
|
2161
|
+
}
|
|
2162
|
+
|
|
2163
|
+
// Every choice the layout knows, in the order it lists them. Which ones a step shows is the spec's business: a step
|
|
2164
|
+
// lists the choices whose `$filter` leaves them active in its navigation state.
|
|
2165
|
+
export const FIELDS: readonly Field[] = [
|
|
2166
|
+
{ instanceName: 'bodyShape', label: 'Body shape', warningTypes: ['offsetHumbuckers'] },
|
|
2167
|
+
{ instanceName: 'finish', label: 'Finish', warningTypes: [] },
|
|
2168
|
+
{ instanceName: 'pickguard', label: 'Pickguard', warningTypes: [] },
|
|
2169
|
+
{ instanceName: 'neckWood', label: 'Neck wood', warningTypes: [] },
|
|
2170
|
+
{ instanceName: 'fretboard', label: 'Fretboard', warningTypes: [] },
|
|
2171
|
+
{ instanceName: 'pickups', label: 'Pickups', warningTypes: ['offsetHumbuckers'] },
|
|
2172
|
+
{ instanceName: 'hardware', label: 'Hardware', warningTypes: [] },
|
|
2173
|
+
{ instanceName: 'signature', label: 'Headstock signature', warningTypes: [] },
|
|
2174
|
+
]
|
|
2175
|
+
|
|
2176
|
+
/** The layout's field for an instance name, or a plain one for a choice the layout does not know. */
|
|
2177
|
+
export const fieldOf = (instanceName: string): Field => {
|
|
2178
|
+
return (
|
|
2179
|
+
FIELDS.find(field => field.instanceName === instanceName) ?? { instanceName, label: instanceName, warningTypes: [] }
|
|
2180
|
+
)
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
/** Orders instance names as the layout lists them; unknown ones go last. */
|
|
2184
|
+
export const byFieldOrder = (a: string, b: string): number => {
|
|
2185
|
+
const rank = (instanceName: string) => {
|
|
2186
|
+
const index = FIELDS.findIndex(field => field.instanceName === instanceName)
|
|
2187
|
+
|
|
2188
|
+
return index === -1 ? FIELDS.length : index
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
return rank(a) - rank(b)
|
|
2192
|
+
}
|
|
2193
|
+
```
|
|
2194
|
+
|
|
2195
|
+
### ./src/\_fields/tokens.ts
|
|
2196
|
+
|
|
2197
|
+
```ts
|
|
2198
|
+
import { keyframes } from '@mui/material'
|
|
2199
|
+
|
|
2200
|
+
// Backstage at night: ink panels, amber light and cream type. Barlow is the brand's typeface, loaded by the page;
|
|
2201
|
+
// SignPainter only previews the headstock signature. The stacks fall back to system fonts where they are not loaded.
|
|
2202
|
+
export const NIGHTJAR = {
|
|
2203
|
+
ink: '#0e0f12',
|
|
2204
|
+
panel: 'rgba(14, 15, 18, 0.9)',
|
|
2205
|
+
raised: '#181a20',
|
|
2206
|
+
raisedHover: '#20232a',
|
|
2207
|
+
line: 'rgba(237, 230, 214, 0.14)',
|
|
2208
|
+
amber: '#f2a33a',
|
|
2209
|
+
amberHover: '#ffb54f',
|
|
2210
|
+
amberTint: 'rgba(242, 163, 58, 0.12)',
|
|
2211
|
+
cream: '#ede6d6',
|
|
2212
|
+
muted: 'rgba(237, 230, 214, 0.64)',
|
|
2213
|
+
faint: 'rgba(237, 230, 214, 0.38)',
|
|
2214
|
+
oxblood: '#7a1e2b',
|
|
2215
|
+
alert: '#f07a84',
|
|
2216
|
+
alertTint: 'rgba(122, 30, 43, 0.32)',
|
|
2217
|
+
font: "'Barlow', 'Helvetica Neue', Arial, sans-serif",
|
|
2218
|
+
script: "'SignPainter-HouseScript', 'Brush Script MT', cursive",
|
|
2219
|
+
} as const
|
|
2220
|
+
|
|
2221
|
+
// Bold capitals with wide tracking, for headings and the stepper
|
|
2222
|
+
export const HEADING_SX = {
|
|
2223
|
+
fontFamily: NIGHTJAR.font,
|
|
2224
|
+
fontWeight: 700,
|
|
2225
|
+
letterSpacing: '0.16em',
|
|
2226
|
+
textTransform: 'uppercase',
|
|
2227
|
+
color: NIGHTJAR.cream,
|
|
2228
|
+
} as const
|
|
2229
|
+
|
|
2230
|
+
// Small capitals, for field labels and overlines
|
|
2231
|
+
export const LABEL_SX = {
|
|
2232
|
+
fontFamily: NIGHTJAR.font,
|
|
2233
|
+
fontSize: 12,
|
|
2234
|
+
fontWeight: 500,
|
|
2235
|
+
letterSpacing: '0.2em',
|
|
2236
|
+
textTransform: 'uppercase',
|
|
2237
|
+
color: NIGHTJAR.muted,
|
|
2238
|
+
} as const
|
|
2239
|
+
|
|
2240
|
+
// A page or a step's content enters from the side it comes from, or a phone's sheet from below, fading in
|
|
2241
|
+
export const ENTER_FROM_END = keyframes`
|
|
2242
|
+
from { opacity: 0; transform: translateX(48px); }
|
|
2243
|
+
to { opacity: 1; transform: none; }
|
|
2244
|
+
`
|
|
2245
|
+
|
|
2246
|
+
export const ENTER_FROM_START = keyframes`
|
|
2247
|
+
from { opacity: 0; transform: translateX(-48px); }
|
|
2248
|
+
to { opacity: 1; transform: none; }
|
|
2249
|
+
`
|
|
2250
|
+
|
|
2251
|
+
export const ENTER_FROM_BELOW = keyframes`
|
|
2252
|
+
from { opacity: 0; transform: translateY(56px); }
|
|
2253
|
+
to { opacity: 1; transform: none; }
|
|
2254
|
+
`
|
|
2255
|
+
```
|
|
2256
|
+
|
|
2257
|
+
### ./src/\_fields/use-active-choices.ts
|
|
2258
|
+
|
|
2259
|
+
```ts
|
|
2260
|
+
import { useMemo } from 'react'
|
|
2261
|
+
|
|
2262
|
+
import { attributeSelectionComponent, materialSwatchComponent, textValueComponent } from '@salsita-ai/sdk/domain'
|
|
2263
|
+
|
|
2264
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
2265
|
+
import { byFieldOrder } from './fields.js'
|
|
2266
|
+
|
|
2267
|
+
export type ChoiceKind = 'selection' | 'swatch' | 'text'
|
|
2268
|
+
|
|
2269
|
+
/** A choice of the spec that is active now. */
|
|
2270
|
+
export interface ActiveChoice {
|
|
2271
|
+
readonly instanceName: string
|
|
2272
|
+
readonly kind: ChoiceKind
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// The guitar's choices whose spec component is active in the current navigation state, in the layout's order. A
|
|
2276
|
+
// `$filter` with `navigationStates` makes a component active only in those states, so on each step this is the step's
|
|
2277
|
+
// choices, and on the summary every choice. The control hooks throw for an inactive component; a field mounts only
|
|
2278
|
+
// for a choice listed here.
|
|
2279
|
+
export const useActiveChoices = (): readonly ActiveChoice[] => {
|
|
2280
|
+
const guitarId = phx.frontend.useRootEntityId()
|
|
2281
|
+
const selections = phx.frontend.useComponents({
|
|
2282
|
+
componentTypes: [attributeSelectionComponent],
|
|
2283
|
+
entityIds: [guitarId],
|
|
2284
|
+
})
|
|
2285
|
+
const swatches = phx.frontend.useComponents({ componentTypes: [materialSwatchComponent], entityIds: [guitarId] })
|
|
2286
|
+
const texts = phx.frontend.useComponents({ componentTypes: [textValueComponent], entityIds: [guitarId] })
|
|
2287
|
+
|
|
2288
|
+
return useMemo(() => {
|
|
2289
|
+
const choices = [
|
|
2290
|
+
...(selections ?? []).map(
|
|
2291
|
+
({ componentInstance }): ActiveChoice => ({ instanceName: componentInstance.instanceName, kind: 'selection' })
|
|
2292
|
+
),
|
|
2293
|
+
...(swatches ?? []).map(
|
|
2294
|
+
({ componentInstance }): ActiveChoice => ({ instanceName: componentInstance.instanceName, kind: 'swatch' })
|
|
2295
|
+
),
|
|
2296
|
+
...(texts ?? []).map(
|
|
2297
|
+
({ componentInstance }): ActiveChoice => ({ instanceName: componentInstance.instanceName, kind: 'text' })
|
|
2298
|
+
),
|
|
2299
|
+
]
|
|
2300
|
+
|
|
2301
|
+
return choices.sort((a, b) => byFieldOrder(a.instanceName, b.instanceName))
|
|
2302
|
+
}, [selections, swatches, texts])
|
|
2303
|
+
}
|
|
2304
|
+
```
|
|
2305
|
+
|
|
2306
|
+
### ./src/\_fields/use-warnings.ts
|
|
2307
|
+
|
|
2308
|
+
```ts
|
|
2309
|
+
import { useMemo } from 'react'
|
|
2310
|
+
|
|
2311
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
2312
|
+
|
|
2313
|
+
export interface Warning {
|
|
2314
|
+
readonly type: string
|
|
2315
|
+
readonly text: string
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
// The warnings of the committed configuration, with their text resolved. A `warning` invariant keeps the change, so
|
|
2319
|
+
// its violation stays until the customer resolves it, and ordering waits for that. `useFinalizeViolations` lists them;
|
|
2320
|
+
// `useStateValidity` holds the same list, but on `phx.frontend` it is typed as the validity value instead of a hook,
|
|
2321
|
+
// so calling it does not type-check.
|
|
2322
|
+
export const useWarnings = (): readonly Warning[] => {
|
|
2323
|
+
const { violations } = phx.frontend.useFinalizeViolations()
|
|
2324
|
+
const resolve = phx.frontend.useResolveLocalizable()
|
|
2325
|
+
|
|
2326
|
+
return useMemo(() => {
|
|
2327
|
+
return (violations ?? [])
|
|
2328
|
+
.filter(({ violation }) => violation.severity === 'warning')
|
|
2329
|
+
.map(({ violation }) => ({ type: violation.type, text: resolve(violation.message) ?? violation.type }))
|
|
2330
|
+
}, [violations, resolve])
|
|
2331
|
+
}
|
|
2332
|
+
```
|
|
2333
|
+
|
|
2334
|
+
### ./src/\_shared/DesktopPage.tsx
|
|
2335
|
+
|
|
2336
|
+
```tsx
|
|
2337
|
+
import { Box, Typography } from '@mui/material'
|
|
2338
|
+
import type { AnimationEvent } from 'react'
|
|
2339
|
+
|
|
2340
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
2341
|
+
import { ENTER_FROM_END, HEADING_SX, NIGHTJAR } from '../_fields/index.js'
|
|
2342
|
+
import { OrderBar } from './OrderBar.js'
|
|
2343
|
+
import { OrderConfirmation } from './OrderConfirmation.js'
|
|
2344
|
+
import { StepHeader } from './StepHeader.js'
|
|
2345
|
+
import { StepPanel } from './StepPanel.js'
|
|
2346
|
+
import { SummaryPanel } from './SummaryPanel.js'
|
|
2347
|
+
import type { OrderForm } from './order-form.js'
|
|
2348
|
+
|
|
2349
|
+
type Page = 'steps' | 'summary' | 'ordered'
|
|
2350
|
+
|
|
2351
|
+
// The summary needs room for the contact form, so its panel is wider
|
|
2352
|
+
const PANEL_WIDTHS: Readonly<Record<Page, number>> = { steps: 420, summary: 520, ordered: 420 }
|
|
2353
|
+
|
|
2354
|
+
const pageOf = (navigationState: string): Page => {
|
|
2355
|
+
return navigationState === 'ordered' ? 'ordered' : navigationState === 'summary' ? 'summary' : 'steps'
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// The brand's mark: a fret of amber over the name of the shop
|
|
2359
|
+
const Brand = () => {
|
|
2360
|
+
return (
|
|
2361
|
+
<Box component="div" sx={{ display: 'flex', alignItems: 'center', gap: 1.5, flex: 'none' }}>
|
|
2362
|
+
<Box component="span" aria-hidden sx={{ inlineSize: 4, blockSize: 28, bgcolor: NIGHTJAR.amber }} />
|
|
2363
|
+
<Box component="div" sx={{ display: 'grid' }}>
|
|
2364
|
+
<Typography sx={{ ...HEADING_SX, fontSize: 16, letterSpacing: '0.32em', lineHeight: 1.1 }}>
|
|
2365
|
+
Northfret
|
|
2366
|
+
</Typography>
|
|
2367
|
+
<Typography sx={{ ...HEADING_SX, fontSize: 10, letterSpacing: '0.36em', color: NIGHTJAR.muted }}>
|
|
2368
|
+
Custom shop
|
|
2369
|
+
</Typography>
|
|
2370
|
+
</Box>
|
|
2371
|
+
</Box>
|
|
2372
|
+
)
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2375
|
+
interface DesktopPageProps {
|
|
2376
|
+
readonly form: OrderForm
|
|
2377
|
+
readonly reached: number
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
// The desktop wizard: the stepper on top, the guitar in the free area on the left, the panel of the step on the
|
|
2381
|
+
// right and the bar with the price at the bottom.
|
|
2382
|
+
//
|
|
2383
|
+
// The summary's panel is wider than a step's, so the steps, the summary and the confirmation are three pages, each
|
|
2384
|
+
// with its own safe area, and each slides in when it opens. The safe area slides with its page: the first
|
|
2385
|
+
// measurement, taken when the page mounts, sees it shifted, and no resize follows when the slide ends. `update()`
|
|
2386
|
+
// from `useSceneSafeArea` measures again once the slide has settled, so the camera frames the guitar in the place the
|
|
2387
|
+
// page leaves for it.
|
|
2388
|
+
export const DesktopPage = ({ form, reached }: DesktopPageProps) => {
|
|
2389
|
+
const { containerRef, safeAreaRef, update } = phx.frontend.useSceneSafeArea()
|
|
2390
|
+
const navigationState = phx.frontend.useActiveNavigationState()
|
|
2391
|
+
const page = pageOf(navigationState)
|
|
2392
|
+
|
|
2393
|
+
// The step's own content slides too; only the page's slide moves the safe area
|
|
2394
|
+
const handleAnimationEnd = (event: AnimationEvent<HTMLDivElement>) => {
|
|
2395
|
+
if (event.target === event.currentTarget) {
|
|
2396
|
+
update()
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
return (
|
|
2401
|
+
<Box
|
|
2402
|
+
component="div"
|
|
2403
|
+
ref={containerRef}
|
|
2404
|
+
sx={{
|
|
2405
|
+
position: 'absolute',
|
|
2406
|
+
inset: 0,
|
|
2407
|
+
display: 'grid',
|
|
2408
|
+
gridTemplateRows: 'auto minmax(0, 1fr) auto',
|
|
2409
|
+
fontFamily: NIGHTJAR.font,
|
|
2410
|
+
color: NIGHTJAR.cream,
|
|
2411
|
+
// The free area lies over the canvas; only the bars and the panel take the pointer
|
|
2412
|
+
pointerEvents: 'none',
|
|
2413
|
+
}}
|
|
2414
|
+
>
|
|
2415
|
+
<Box
|
|
2416
|
+
component="header"
|
|
2417
|
+
sx={{
|
|
2418
|
+
display: 'flex',
|
|
2419
|
+
alignItems: 'center',
|
|
2420
|
+
gap: 5,
|
|
2421
|
+
minBlockSize: 72,
|
|
2422
|
+
paddingInline: 4,
|
|
2423
|
+
bgcolor: NIGHTJAR.ink,
|
|
2424
|
+
borderBlockEnd: `1px solid ${NIGHTJAR.line}`,
|
|
2425
|
+
pointerEvents: 'auto',
|
|
2426
|
+
}}
|
|
2427
|
+
>
|
|
2428
|
+
<Brand />
|
|
2429
|
+
<StepHeader compact={false} reached={reached} />
|
|
2430
|
+
</Box>
|
|
2431
|
+
|
|
2432
|
+
<Box
|
|
2433
|
+
component="div"
|
|
2434
|
+
key={page}
|
|
2435
|
+
onAnimationEnd={handleAnimationEnd}
|
|
2436
|
+
sx={{
|
|
2437
|
+
display: 'grid',
|
|
2438
|
+
gridTemplateColumns: `minmax(0, 1fr) ${PANEL_WIDTHS[page]}px`,
|
|
2439
|
+
minBlockSize: 0,
|
|
2440
|
+
animation: `${ENTER_FROM_END} 480ms cubic-bezier(0.2, 0.7, 0.2, 1)`,
|
|
2441
|
+
'@media (prefers-reduced-motion: reduce)': { animation: 'none' },
|
|
2442
|
+
}}
|
|
2443
|
+
>
|
|
2444
|
+
<Box component="div" sx={{ position: 'relative', minInlineSize: 0 }}>
|
|
2445
|
+
{/* The camera fits the guitar into this box, so its inset is the room around the guitar */}
|
|
2446
|
+
<Box component="div" ref={safeAreaRef} sx={{ position: 'absolute', inset: '40px 72px 56px' }} />
|
|
2447
|
+
<Typography
|
|
2448
|
+
sx={{
|
|
2449
|
+
position: 'absolute',
|
|
2450
|
+
insetBlockEnd: 20,
|
|
2451
|
+
insetInline: 0,
|
|
2452
|
+
textAlign: 'center',
|
|
2453
|
+
fontSize: 12,
|
|
2454
|
+
letterSpacing: '0.14em',
|
|
2455
|
+
textTransform: 'uppercase',
|
|
2456
|
+
color: NIGHTJAR.faint,
|
|
2457
|
+
}}
|
|
2458
|
+
>
|
|
2459
|
+
Drag to turn the guitar
|
|
2460
|
+
</Typography>
|
|
2461
|
+
</Box>
|
|
2462
|
+
<Box
|
|
2463
|
+
component="aside"
|
|
2464
|
+
aria-label={page === 'steps' ? 'Step' : page === 'summary' ? 'Summary' : 'Order'}
|
|
2465
|
+
sx={{
|
|
2466
|
+
overflowY: 'auto',
|
|
2467
|
+
paddingInline: 4,
|
|
2468
|
+
paddingBlock: 4,
|
|
2469
|
+
bgcolor: NIGHTJAR.panel,
|
|
2470
|
+
borderInlineStart: `1px solid ${NIGHTJAR.line}`,
|
|
2471
|
+
pointerEvents: 'auto',
|
|
2472
|
+
}}
|
|
2473
|
+
>
|
|
2474
|
+
{page === 'steps' && <StepPanel compact={false} />}
|
|
2475
|
+
{page === 'summary' && <SummaryPanel compact={false} form={form} />}
|
|
2476
|
+
{page === 'ordered' && <OrderConfirmation compact={false} contact={form.contact} />}
|
|
2477
|
+
</Box>
|
|
2478
|
+
</Box>
|
|
2479
|
+
|
|
2480
|
+
<OrderBar compact={false} form={form} />
|
|
2481
|
+
</Box>
|
|
2482
|
+
)
|
|
2483
|
+
}
|
|
2484
|
+
```
|
|
2485
|
+
|
|
2486
|
+
### ./src/\_shared/OrderBar.tsx
|
|
2487
|
+
|
|
2488
|
+
```tsx
|
|
2489
|
+
import { Box, Button, CircularProgress, IconButton, Tooltip, Typography } from '@mui/material'
|
|
2490
|
+
|
|
2491
|
+
import { FrontendAnalyticsFactory } from '@salsita-ai/sdk/configurator'
|
|
2492
|
+
import { detailComponent } from '@salsita-ai/sdk/domain'
|
|
2493
|
+
|
|
2494
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
2495
|
+
import { HEADING_SX, LABEL_SX, NIGHTJAR } from '../_fields/index.js'
|
|
2496
|
+
import type { OrderForm } from './order-form.js'
|
|
2497
|
+
|
|
2498
|
+
const BACK_ICON = 'M15 5l-7 7 7 7'
|
|
2499
|
+
|
|
2500
|
+
interface OrderBarProps {
|
|
2501
|
+
/** The phone's bar: price and two buttons */
|
|
2502
|
+
readonly compact: boolean
|
|
2503
|
+
readonly form: OrderForm
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// The bar at the bottom: the guitar's name and price, Back, and the step's main action. On a step that is Next, on
|
|
2507
|
+
// the step before the summary **Review order**, on the summary **Place order**, and after the order **Design another**.
|
|
2508
|
+
export const OrderBar = ({ compact, form }: OrderBarProps) => {
|
|
2509
|
+
const resolve = phx.frontend.useResolveLocalizable()
|
|
2510
|
+
const detail = phx.frontend.useFirstRootComponent(detailComponent)
|
|
2511
|
+
const price = phx.frontend.useRootEntityPrice()
|
|
2512
|
+
const { steps, activeIndex, canGoPrevious, isLast, next, previous } = phx.frontend.useStepNavigation()
|
|
2513
|
+
const navigationState = phx.frontend.useActiveNavigationState()
|
|
2514
|
+
const navigate = phx.frontend.useNavigate()
|
|
2515
|
+
const analytics = phx.frontend.useAnalytics()
|
|
2516
|
+
const { primary, hints } = phx.frontend.useFinalizeActions()
|
|
2517
|
+
const { accessInfo } = phx.frontend.useAccessService()
|
|
2518
|
+
|
|
2519
|
+
const isOrdered = navigationState === 'ordered'
|
|
2520
|
+
const isPending = primary?.isPending ?? false
|
|
2521
|
+
const nextStep = steps[activeIndex + 1]
|
|
2522
|
+
const firstStep = steps[0]
|
|
2523
|
+
|
|
2524
|
+
// The step before the last one leads to the summary. Like the built-in summary button, it records
|
|
2525
|
+
// `SummaryButtonClicked`, which `next()` would not: it records the next-step button's event.
|
|
2526
|
+
const enterSummary = () => {
|
|
2527
|
+
if (nextStep === undefined) {
|
|
2528
|
+
return
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
analytics.push(FrontendAnalyticsFactory.SummaryButtonClicked())
|
|
2532
|
+
navigate(nextStep.state)
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// A conflict or a missing detail blocks the order; the button stays clickable and shows what is missing instead
|
|
2536
|
+
const placeOrder = () => {
|
|
2537
|
+
if (primary === null) {
|
|
2538
|
+
return
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
if (primary.disabled || !form.isComplete) {
|
|
2542
|
+
form.revealErrors()
|
|
2543
|
+
return
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
void primary.run()
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2549
|
+
const designAnother = () => {
|
|
2550
|
+
if (firstStep) {
|
|
2551
|
+
navigate(firstStep.state)
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
const action = isOrdered
|
|
2556
|
+
? { label: 'Design another', onClick: designAnother, blocked: false }
|
|
2557
|
+
: isLast
|
|
2558
|
+
? { label: primary?.label ?? 'Place order', onClick: placeOrder, blocked: primary?.disabled ?? true }
|
|
2559
|
+
: nextStep?.index === steps.length - 1
|
|
2560
|
+
? { label: 'Review order', onClick: enterSummary, blocked: false }
|
|
2561
|
+
: { label: compact ? 'Next' : `Next: ${nextStep?.label ?? ''}`, onClick: next, blocked: false }
|
|
2562
|
+
const showAction = !(isLast && !isOrdered && (primary === null || accessInfo.viewMode))
|
|
2563
|
+
const blockedNote = isLast && !isOrdered && primary?.disabled && hints ? 'Settle the choices above first' : null
|
|
2564
|
+
|
|
2565
|
+
return (
|
|
2566
|
+
<Box
|
|
2567
|
+
component="footer"
|
|
2568
|
+
sx={{
|
|
2569
|
+
display: 'flex',
|
|
2570
|
+
alignItems: 'center',
|
|
2571
|
+
gap: compact ? 1.5 : 3,
|
|
2572
|
+
minBlockSize: compact ? 72 : 84,
|
|
2573
|
+
paddingInline: compact ? 2 : 4,
|
|
2574
|
+
paddingBlock: 1.5,
|
|
2575
|
+
bgcolor: NIGHTJAR.ink,
|
|
2576
|
+
borderBlockStart: `1px solid ${NIGHTJAR.line}`,
|
|
2577
|
+
pointerEvents: 'auto',
|
|
2578
|
+
}}
|
|
2579
|
+
>
|
|
2580
|
+
{!compact && (
|
|
2581
|
+
<Box component="div" sx={{ display: 'grid', gap: 0.25, minInlineSize: 0 }}>
|
|
2582
|
+
<Typography sx={{ ...HEADING_SX, fontSize: 18, letterSpacing: '0.24em' }}>
|
|
2583
|
+
{detail ? resolve(detail.state.title) : 'Nightjar'}
|
|
2584
|
+
</Typography>
|
|
2585
|
+
<Typography sx={{ fontFamily: NIGHTJAR.font, fontSize: 13, color: NIGHTJAR.muted, whiteSpace: 'nowrap' }}>
|
|
2586
|
+
{detail ? resolve(detail.state.subtitle) : ''}
|
|
2587
|
+
</Typography>
|
|
2588
|
+
</Box>
|
|
2589
|
+
)}
|
|
2590
|
+
|
|
2591
|
+
<Box component="div" sx={{ display: 'grid', marginInlineStart: compact ? 0 : 'auto', minInlineSize: 0 }}>
|
|
2592
|
+
<Typography sx={{ ...LABEL_SX, fontSize: 11 }}>{isOrdered ? 'Ordered' : 'Your price'}</Typography>
|
|
2593
|
+
<Typography
|
|
2594
|
+
aria-live="polite"
|
|
2595
|
+
sx={{
|
|
2596
|
+
fontFamily: NIGHTJAR.font,
|
|
2597
|
+
fontSize: compact ? 22 : 28,
|
|
2598
|
+
fontWeight: 700,
|
|
2599
|
+
lineHeight: 1.1,
|
|
2600
|
+
fontVariantNumeric: 'tabular-nums',
|
|
2601
|
+
color: NIGHTJAR.cream,
|
|
2602
|
+
}}
|
|
2603
|
+
>
|
|
2604
|
+
{price}
|
|
2605
|
+
</Typography>
|
|
2606
|
+
</Box>
|
|
2607
|
+
|
|
2608
|
+
<Box
|
|
2609
|
+
component="div"
|
|
2610
|
+
sx={{ display: 'flex', alignItems: 'center', gap: 1.5, marginInlineStart: compact ? 'auto' : 2 }}
|
|
2611
|
+
>
|
|
2612
|
+
{blockedNote && !compact && (
|
|
2613
|
+
<Typography sx={{ fontFamily: NIGHTJAR.font, fontSize: 14, color: NIGHTJAR.amber }}>{blockedNote}</Typography>
|
|
2614
|
+
)}
|
|
2615
|
+
{canGoPrevious && !isOrdered && (
|
|
2616
|
+
<Tooltip title="Back">
|
|
2617
|
+
<span>
|
|
2618
|
+
<IconButton
|
|
2619
|
+
aria-label="Back"
|
|
2620
|
+
onClick={previous}
|
|
2621
|
+
disabled={isPending}
|
|
2622
|
+
sx={{
|
|
2623
|
+
inlineSize: 48,
|
|
2624
|
+
blockSize: 48,
|
|
2625
|
+
border: `1px solid ${NIGHTJAR.line}`,
|
|
2626
|
+
color: NIGHTJAR.cream,
|
|
2627
|
+
'&:hover': { bgcolor: NIGHTJAR.raisedHover },
|
|
2628
|
+
'&.Mui-disabled': { color: NIGHTJAR.faint },
|
|
2629
|
+
}}
|
|
2630
|
+
>
|
|
2631
|
+
<svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
|
|
2632
|
+
<path d={BACK_ICON} strokeLinecap="round" strokeLinejoin="round" />
|
|
2633
|
+
</svg>
|
|
2634
|
+
</IconButton>
|
|
2635
|
+
</span>
|
|
2636
|
+
</Tooltip>
|
|
2637
|
+
)}
|
|
2638
|
+
{showAction && (
|
|
2639
|
+
<Button
|
|
2640
|
+
onClick={action.onClick}
|
|
2641
|
+
disabled={isPending}
|
|
2642
|
+
aria-disabled={action.blocked || undefined}
|
|
2643
|
+
title={isLast && !isOrdered ? (hints ?? primary?.title) : undefined}
|
|
2644
|
+
startIcon={isPending ? <CircularProgress size={16} sx={{ color: NIGHTJAR.ink }} /> : undefined}
|
|
2645
|
+
sx={{
|
|
2646
|
+
...HEADING_SX,
|
|
2647
|
+
minBlockSize: 48,
|
|
2648
|
+
paddingInline: compact ? 2.5 : 3.5,
|
|
2649
|
+
borderRadius: 1,
|
|
2650
|
+
fontSize: 14,
|
|
2651
|
+
whiteSpace: 'nowrap',
|
|
2652
|
+
color: NIGHTJAR.ink,
|
|
2653
|
+
bgcolor: action.blocked ? NIGHTJAR.muted : NIGHTJAR.amber,
|
|
2654
|
+
'&:hover': { bgcolor: action.blocked ? NIGHTJAR.muted : NIGHTJAR.amberHover },
|
|
2655
|
+
'&.Mui-disabled': { color: NIGHTJAR.ink, bgcolor: NIGHTJAR.amber, opacity: 0.75 },
|
|
2656
|
+
'&.Mui-focusVisible': { outline: `2px solid ${NIGHTJAR.cream}`, outlineOffset: 2 },
|
|
2657
|
+
}}
|
|
2658
|
+
>
|
|
2659
|
+
{isPending ? 'Placing order…' : action.label}
|
|
2660
|
+
</Button>
|
|
2661
|
+
)}
|
|
2662
|
+
</Box>
|
|
2663
|
+
</Box>
|
|
2664
|
+
)
|
|
2665
|
+
}
|
|
2666
|
+
```
|
|
2667
|
+
|
|
2668
|
+
### ./src/\_shared/OrderConfirmation.tsx
|
|
2669
|
+
|
|
2670
|
+
```tsx
|
|
2671
|
+
import { Box, Typography } from '@mui/material'
|
|
2672
|
+
|
|
2673
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
2674
|
+
import { HEADING_SX, LABEL_SX, NIGHTJAR, SummaryRows } from '../_fields/index.js'
|
|
2675
|
+
import type { Contact } from './order-form.js'
|
|
2676
|
+
|
|
2677
|
+
const CHECK_ICON = 'M5 12.5 10 17.5 19 7'
|
|
2678
|
+
|
|
2679
|
+
interface OrderConfirmationProps {
|
|
2680
|
+
readonly compact: boolean
|
|
2681
|
+
readonly contact: Contact
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
// The `ordered` navigation state, which the order handler enters once the order is accepted. The order number is
|
|
2685
|
+
// made from the configuration's id, so it stays the same when the confirmation is shown again.
|
|
2686
|
+
export const OrderConfirmation = ({ compact, contact }: OrderConfirmationProps) => {
|
|
2687
|
+
const configurationId = phx.frontend.useConfigurationId()
|
|
2688
|
+
const price = phx.frontend.useRootEntityPrice()
|
|
2689
|
+
const orderNumber = `NJ-${String(configurationId)
|
|
2690
|
+
.replace(/[^a-z0-9]/gi, '')
|
|
2691
|
+
.slice(-6)
|
|
2692
|
+
.toUpperCase()}`
|
|
2693
|
+
const name = contact.name.trim()
|
|
2694
|
+
|
|
2695
|
+
return (
|
|
2696
|
+
<Box component="div" role="status" sx={{ display: 'grid', gap: compact ? 2.5 : 3 }}>
|
|
2697
|
+
<Box component="header" sx={{ display: 'grid', gap: 1.5, justifyItems: 'start' }}>
|
|
2698
|
+
<Box
|
|
2699
|
+
component="svg"
|
|
2700
|
+
viewBox="0 0 24 24"
|
|
2701
|
+
aria-hidden
|
|
2702
|
+
sx={{ inlineSize: 44, blockSize: 44, padding: 1.25, borderRadius: '50%', bgcolor: NIGHTJAR.amber }}
|
|
2703
|
+
>
|
|
2704
|
+
<path d={CHECK_ICON} fill="none" stroke={NIGHTJAR.ink} strokeWidth={2.6} strokeLinecap="round" />
|
|
2705
|
+
</Box>
|
|
2706
|
+
<Typography sx={LABEL_SX}>Order {orderNumber}</Typography>
|
|
2707
|
+
<Typography component="h2" sx={{ ...HEADING_SX, fontSize: compact ? 24 : 30, lineHeight: 1.05 }}>
|
|
2708
|
+
{name ? `Thank you, ${name}` : 'Thank you'}
|
|
2709
|
+
</Typography>
|
|
2710
|
+
<Typography sx={{ fontFamily: NIGHTJAR.font, fontSize: 15, lineHeight: 1.5, color: NIGHTJAR.muted }}>
|
|
2711
|
+
Your Nightjar goes into the build queue at {price}. We will write to {contact.email.trim() || 'you'} when the
|
|
2712
|
+
shop confirms it; a build takes about eight weeks.
|
|
2713
|
+
</Typography>
|
|
2714
|
+
</Box>
|
|
2715
|
+
<Box component="section" sx={{ display: 'grid', gap: 1.5 }}>
|
|
2716
|
+
<Typography component="h3" sx={LABEL_SX}>
|
|
2717
|
+
What we are building
|
|
2718
|
+
</Typography>
|
|
2719
|
+
<SummaryRows />
|
|
2720
|
+
</Box>
|
|
2721
|
+
</Box>
|
|
2722
|
+
)
|
|
2723
|
+
}
|
|
2724
|
+
```
|
|
2725
|
+
|
|
2726
|
+
### ./src/\_shared/PhonePage.tsx
|
|
2727
|
+
|
|
2728
|
+
```tsx
|
|
2729
|
+
import { Box, Typography } from '@mui/material'
|
|
2730
|
+
import type { AnimationEvent } from 'react'
|
|
2731
|
+
|
|
2732
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
2733
|
+
import { ENTER_FROM_BELOW, HEADING_SX, NIGHTJAR } from '../_fields/index.js'
|
|
2734
|
+
import { OrderBar } from './OrderBar.js'
|
|
2735
|
+
import { OrderConfirmation } from './OrderConfirmation.js'
|
|
2736
|
+
import { StepHeader } from './StepHeader.js'
|
|
2737
|
+
import { StepPanel } from './StepPanel.js'
|
|
2738
|
+
import { SummaryPanel } from './SummaryPanel.js'
|
|
2739
|
+
import type { OrderForm } from './order-form.js'
|
|
2740
|
+
|
|
2741
|
+
type Page = 'steps' | 'summary' | 'ordered'
|
|
2742
|
+
|
|
2743
|
+
// How much of the screen below the header the guitar keeps: the summary's form needs more of it
|
|
2744
|
+
const SCENE_SHARES: Readonly<Record<Page, string>> = { steps: '46%', summary: '30%', ordered: '40%' }
|
|
2745
|
+
|
|
2746
|
+
const pageOf = (navigationState: string): Page => {
|
|
2747
|
+
return navigationState === 'ordered' ? 'ordered' : navigationState === 'summary' ? 'summary' : 'steps'
|
|
2748
|
+
}
|
|
2749
|
+
|
|
2750
|
+
interface PhonePageProps {
|
|
2751
|
+
readonly form: OrderForm
|
|
2752
|
+
readonly reached: number
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
// The phone wizard: a compact progress line, the guitar at the top, the step's choices in a sheet below it and the
|
|
2756
|
+
// bar fixed at the bottom. Like the desktop pages, each page rises in with its own safe area and measures it again
|
|
2757
|
+
// with `update()` once the slide has settled.
|
|
2758
|
+
export const PhonePage = ({ form, reached }: PhonePageProps) => {
|
|
2759
|
+
const { containerRef, safeAreaRef, update } = phx.frontend.useSceneSafeArea()
|
|
2760
|
+
const navigationState = phx.frontend.useActiveNavigationState()
|
|
2761
|
+
const page = pageOf(navigationState)
|
|
2762
|
+
|
|
2763
|
+
const handleAnimationEnd = (event: AnimationEvent<HTMLDivElement>) => {
|
|
2764
|
+
if (event.target === event.currentTarget) {
|
|
2765
|
+
update()
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
return (
|
|
2770
|
+
<Box
|
|
2771
|
+
component="div"
|
|
2772
|
+
ref={containerRef}
|
|
2773
|
+
sx={{
|
|
2774
|
+
position: 'absolute',
|
|
2775
|
+
inset: 0,
|
|
2776
|
+
display: 'flex',
|
|
2777
|
+
flexDirection: 'column',
|
|
2778
|
+
fontFamily: NIGHTJAR.font,
|
|
2779
|
+
color: NIGHTJAR.cream,
|
|
2780
|
+
pointerEvents: 'none',
|
|
2781
|
+
}}
|
|
2782
|
+
>
|
|
2783
|
+
<Box
|
|
2784
|
+
component="header"
|
|
2785
|
+
sx={{
|
|
2786
|
+
display: 'grid',
|
|
2787
|
+
gap: 1.25,
|
|
2788
|
+
paddingInline: 2,
|
|
2789
|
+
paddingBlock: 1.5,
|
|
2790
|
+
bgcolor: NIGHTJAR.ink,
|
|
2791
|
+
borderBlockEnd: `1px solid ${NIGHTJAR.line}`,
|
|
2792
|
+
pointerEvents: 'auto',
|
|
2793
|
+
}}
|
|
2794
|
+
>
|
|
2795
|
+
<Box component="div" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
2796
|
+
<Box component="span" aria-hidden sx={{ inlineSize: 3, blockSize: 16, bgcolor: NIGHTJAR.amber }} />
|
|
2797
|
+
<Typography sx={{ ...HEADING_SX, fontSize: 13, letterSpacing: '0.32em' }}>Northfret</Typography>
|
|
2798
|
+
</Box>
|
|
2799
|
+
<StepHeader compact reached={reached} />
|
|
2800
|
+
</Box>
|
|
2801
|
+
|
|
2802
|
+
<Box
|
|
2803
|
+
component="div"
|
|
2804
|
+
key={page}
|
|
2805
|
+
onAnimationEnd={handleAnimationEnd}
|
|
2806
|
+
sx={{
|
|
2807
|
+
display: 'flex',
|
|
2808
|
+
flexDirection: 'column',
|
|
2809
|
+
flex: '1 1 0',
|
|
2810
|
+
minBlockSize: 0,
|
|
2811
|
+
animation: `${ENTER_FROM_BELOW} 460ms cubic-bezier(0.2, 0.7, 0.2, 1)`,
|
|
2812
|
+
'@media (prefers-reduced-motion: reduce)': { animation: 'none' },
|
|
2813
|
+
}}
|
|
2814
|
+
>
|
|
2815
|
+
<Box component="div" sx={{ position: 'relative', flex: `0 0 ${SCENE_SHARES[page]}`, minBlockSize: 0 }}>
|
|
2816
|
+
<Box component="div" ref={safeAreaRef} sx={{ position: 'absolute', inset: '12px 16px 8px' }} />
|
|
2817
|
+
</Box>
|
|
2818
|
+
<Box
|
|
2819
|
+
component="main"
|
|
2820
|
+
sx={{
|
|
2821
|
+
flex: '1 1 0',
|
|
2822
|
+
minBlockSize: 0,
|
|
2823
|
+
overflowY: 'auto',
|
|
2824
|
+
overscrollBehavior: 'contain',
|
|
2825
|
+
paddingInline: 2,
|
|
2826
|
+
paddingBlock: 2.5,
|
|
2827
|
+
bgcolor: NIGHTJAR.panel,
|
|
2828
|
+
borderBlockStart: `1px solid ${NIGHTJAR.line}`,
|
|
2829
|
+
borderStartStartRadius: 16,
|
|
2830
|
+
borderStartEndRadius: 16,
|
|
2831
|
+
pointerEvents: 'auto',
|
|
2832
|
+
}}
|
|
2833
|
+
>
|
|
2834
|
+
{page === 'steps' && <StepPanel compact />}
|
|
2835
|
+
{page === 'summary' && <SummaryPanel compact form={form} />}
|
|
2836
|
+
{page === 'ordered' && <OrderConfirmation compact contact={form.contact} />}
|
|
2837
|
+
</Box>
|
|
2838
|
+
</Box>
|
|
2839
|
+
|
|
2840
|
+
<OrderBar compact form={form} />
|
|
2841
|
+
</Box>
|
|
2842
|
+
)
|
|
2843
|
+
}
|
|
2844
|
+
```
|
|
2845
|
+
|
|
2846
|
+
### ./src/\_shared/StepHeader.tsx
|
|
2847
|
+
|
|
2848
|
+
```tsx
|
|
2849
|
+
import { Box, ButtonBase, Typography } from '@mui/material'
|
|
2850
|
+
|
|
2851
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
2852
|
+
import { HEADING_SX, NIGHTJAR, useWarnings } from '../_fields/index.js'
|
|
2853
|
+
import { STEP_WARNINGS } from './step-warnings.js'
|
|
2854
|
+
|
|
2855
|
+
const CHECK_ICON = 'M5 12.5 10 17.5 19 7'
|
|
2856
|
+
|
|
2857
|
+
interface StepHeaderProps {
|
|
2858
|
+
/** One line with a progress bar, for a phone; the full stepper otherwise */
|
|
2859
|
+
readonly compact: boolean
|
|
2860
|
+
/** The furthest step the customer has reached; the layout keeps it, so it survives the switch between the pages */
|
|
2861
|
+
readonly reached: number
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
// The spec's `navigationSteps` as a stepper. Labels and order come from the spec; `useStepNavigation` gives each step
|
|
2865
|
+
// its status, and `goTo` enters a step and records the stepper's analytics event. The customer can go back to any step
|
|
2866
|
+
// they have reached, never ahead of it, and nowhere while the order is being placed or after it was placed.
|
|
2867
|
+
export const StepHeader = ({ compact, reached }: StepHeaderProps) => {
|
|
2868
|
+
const { steps, activeIndex, activeStep, goTo } = phx.frontend.useStepNavigation()
|
|
2869
|
+
const navigationState = phx.frontend.useActiveNavigationState()
|
|
2870
|
+
const { primary } = phx.frontend.useFinalizeActions()
|
|
2871
|
+
const warnings = useWarnings()
|
|
2872
|
+
const isLocked = navigationState === 'ordered' || (primary?.isPending ?? false)
|
|
2873
|
+
|
|
2874
|
+
const hasWarning = (state: string) => {
|
|
2875
|
+
return warnings.some(warning => STEP_WARNINGS[state]?.includes(warning.type))
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
if (compact) {
|
|
2879
|
+
const progress = steps.length === 0 ? 0 : (activeIndex + 1) / steps.length
|
|
2880
|
+
|
|
2881
|
+
return (
|
|
2882
|
+
<Box component="nav" aria-label="Build steps" sx={{ display: 'grid', gap: 1 }}>
|
|
2883
|
+
<Typography
|
|
2884
|
+
sx={{ ...HEADING_SX, fontSize: 13, letterSpacing: '0.14em', display: 'flex', alignItems: 'center', gap: 1 }}
|
|
2885
|
+
>
|
|
2886
|
+
<Box component="span" sx={{ color: NIGHTJAR.amber, fontVariantNumeric: 'tabular-nums' }}>
|
|
2887
|
+
{activeIndex + 1} / {steps.length}
|
|
2888
|
+
</Box>
|
|
2889
|
+
<Box component="span" aria-hidden sx={{ color: NIGHTJAR.faint }}>
|
|
2890
|
+
·
|
|
2891
|
+
</Box>
|
|
2892
|
+
<Box component="span" aria-current="step">
|
|
2893
|
+
{activeStep?.label}
|
|
2894
|
+
</Box>
|
|
2895
|
+
</Typography>
|
|
2896
|
+
<Box
|
|
2897
|
+
component="div"
|
|
2898
|
+
role="progressbar"
|
|
2899
|
+
aria-label="Build progress"
|
|
2900
|
+
aria-valuemin={0}
|
|
2901
|
+
aria-valuemax={steps.length}
|
|
2902
|
+
aria-valuenow={activeIndex + 1}
|
|
2903
|
+
sx={{ blockSize: 2, borderRadius: 1, bgcolor: NIGHTJAR.line, overflow: 'hidden' }}
|
|
2904
|
+
>
|
|
2905
|
+
<Box
|
|
2906
|
+
component="div"
|
|
2907
|
+
sx={{
|
|
2908
|
+
blockSize: '100%',
|
|
2909
|
+
inlineSize: `${progress * 100}%`,
|
|
2910
|
+
bgcolor: NIGHTJAR.amber,
|
|
2911
|
+
transition: 'inline-size 400ms ease-out',
|
|
2912
|
+
}}
|
|
2913
|
+
/>
|
|
2914
|
+
</Box>
|
|
2915
|
+
</Box>
|
|
2916
|
+
)
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
return (
|
|
2920
|
+
<Box component="nav" aria-label="Build steps">
|
|
2921
|
+
<Box
|
|
2922
|
+
component="ol"
|
|
2923
|
+
sx={{ display: 'flex', alignItems: 'center', gap: 1, margin: 0, padding: 0, listStyle: 'none' }}
|
|
2924
|
+
>
|
|
2925
|
+
{steps.map(step => {
|
|
2926
|
+
const isActive = step.status === 'active'
|
|
2927
|
+
const isComplete = step.status === 'complete'
|
|
2928
|
+
const canEnter = !isLocked && !isActive && step.index <= reached
|
|
2929
|
+
const warn = hasWarning(step.state)
|
|
2930
|
+
|
|
2931
|
+
return (
|
|
2932
|
+
<Box component="li" key={step.state} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
2933
|
+
{step.index > 0 && (
|
|
2934
|
+
<Box
|
|
2935
|
+
component="span"
|
|
2936
|
+
aria-hidden
|
|
2937
|
+
sx={{
|
|
2938
|
+
inlineSize: 28,
|
|
2939
|
+
blockSize: '1px',
|
|
2940
|
+
bgcolor: step.index <= reached ? NIGHTJAR.muted : NIGHTJAR.line,
|
|
2941
|
+
}}
|
|
2942
|
+
/>
|
|
2943
|
+
)}
|
|
2944
|
+
<ButtonBase
|
|
2945
|
+
aria-label={warn ? `${step.label}, needs attention` : step.label}
|
|
2946
|
+
aria-current={isActive ? 'step' : undefined}
|
|
2947
|
+
disabled={!canEnter}
|
|
2948
|
+
onClick={() => goTo(step.index)}
|
|
2949
|
+
sx={{
|
|
2950
|
+
display: 'flex',
|
|
2951
|
+
alignItems: 'center',
|
|
2952
|
+
gap: 1,
|
|
2953
|
+
paddingInline: 1,
|
|
2954
|
+
paddingBlock: 0.75,
|
|
2955
|
+
borderRadius: 1,
|
|
2956
|
+
color: isActive ? NIGHTJAR.cream : step.index <= reached ? NIGHTJAR.muted : NIGHTJAR.faint,
|
|
2957
|
+
'&:hover': { color: NIGHTJAR.cream },
|
|
2958
|
+
'&.Mui-focusVisible': { outline: `2px solid ${NIGHTJAR.amber}` },
|
|
2959
|
+
}}
|
|
2960
|
+
>
|
|
2961
|
+
<Box
|
|
2962
|
+
component="span"
|
|
2963
|
+
sx={{
|
|
2964
|
+
display: 'grid',
|
|
2965
|
+
placeItems: 'center',
|
|
2966
|
+
inlineSize: 24,
|
|
2967
|
+
blockSize: 24,
|
|
2968
|
+
borderRadius: '50%',
|
|
2969
|
+
fontFamily: NIGHTJAR.font,
|
|
2970
|
+
fontSize: 12,
|
|
2971
|
+
fontWeight: 700,
|
|
2972
|
+
color: isActive ? NIGHTJAR.ink : 'inherit',
|
|
2973
|
+
bgcolor: isActive ? NIGHTJAR.amber : 'transparent',
|
|
2974
|
+
border: isActive ? 'none' : `1px solid ${isComplete ? NIGHTJAR.muted : NIGHTJAR.line}`,
|
|
2975
|
+
}}
|
|
2976
|
+
>
|
|
2977
|
+
{isComplete ? (
|
|
2978
|
+
<Box component="svg" viewBox="0 0 24 24" aria-hidden sx={{ inlineSize: 14, blockSize: 14 }}>
|
|
2979
|
+
<path d={CHECK_ICON} fill="none" stroke="currentColor" strokeWidth={2.6} strokeLinecap="round" />
|
|
2980
|
+
</Box>
|
|
2981
|
+
) : (
|
|
2982
|
+
step.index + 1
|
|
2983
|
+
)}
|
|
2984
|
+
</Box>
|
|
2985
|
+
{/* In a narrow window only the active step keeps its label; the others keep their number */}
|
|
2986
|
+
<Typography
|
|
2987
|
+
component="span"
|
|
2988
|
+
sx={{
|
|
2989
|
+
...HEADING_SX,
|
|
2990
|
+
display: { xs: isActive ? 'inline' : 'none', lg: 'inline' },
|
|
2991
|
+
fontSize: 13,
|
|
2992
|
+
color: 'inherit',
|
|
2993
|
+
}}
|
|
2994
|
+
>
|
|
2995
|
+
{step.label}
|
|
2996
|
+
</Typography>
|
|
2997
|
+
{warn && (
|
|
2998
|
+
<Box
|
|
2999
|
+
component="span"
|
|
3000
|
+
aria-hidden
|
|
3001
|
+
sx={{ inlineSize: 7, blockSize: 7, borderRadius: '50%', bgcolor: NIGHTJAR.amber }}
|
|
3002
|
+
/>
|
|
3003
|
+
)}
|
|
3004
|
+
</ButtonBase>
|
|
3005
|
+
</Box>
|
|
3006
|
+
)
|
|
3007
|
+
})}
|
|
3008
|
+
</Box>
|
|
3009
|
+
</Box>
|
|
3010
|
+
)
|
|
3011
|
+
}
|
|
3012
|
+
```
|
|
3013
|
+
|
|
3014
|
+
### ./src/\_shared/StepPanel.tsx
|
|
3015
|
+
|
|
3016
|
+
```tsx
|
|
3017
|
+
import { Box, Typography } from '@mui/material'
|
|
3018
|
+
import { useState } from 'react'
|
|
3019
|
+
|
|
3020
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
3021
|
+
import { ENTER_FROM_END, ENTER_FROM_START, HEADING_SX, LABEL_SX, NIGHTJAR, StepFields } from '../_fields/index.js'
|
|
3022
|
+
|
|
3023
|
+
// A line under each step's title; the layout's copy, keyed by the step's navigation state
|
|
3024
|
+
const STEP_INTROS: Readonly<Record<string, string>> = {
|
|
3025
|
+
body: 'The shape sets the feel, the finish and the pickguard the look.',
|
|
3026
|
+
neck: 'What your hand holds: the neck and the board under the strings.',
|
|
3027
|
+
electronics: 'The sound: two pickups, and the metal parts around them.',
|
|
3028
|
+
details: 'Sign the headstock. The shop paints it on by hand.',
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
/** 1 when the active step moved forward, -1 when it moved back; the step's content slides in from that side. */
|
|
3032
|
+
const useStepDirection = (activeIndex: number): 1 | -1 => {
|
|
3033
|
+
const [shown, setShown] = useState<{ readonly index: number; readonly direction: 1 | -1 }>({
|
|
3034
|
+
index: activeIndex,
|
|
3035
|
+
direction: 1,
|
|
3036
|
+
})
|
|
3037
|
+
|
|
3038
|
+
if (activeIndex !== shown.index) {
|
|
3039
|
+
setShown({ index: activeIndex, direction: activeIndex > shown.index ? 1 : -1 })
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
return shown.direction
|
|
3043
|
+
}
|
|
3044
|
+
|
|
3045
|
+
interface StepPanelProps {
|
|
3046
|
+
readonly compact: boolean
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
// The active step: its number, its label from the spec, and its fields. The content is keyed by the step, so each
|
|
3050
|
+
// step slides in from the side the customer moves towards.
|
|
3051
|
+
export const StepPanel = ({ compact }: StepPanelProps) => {
|
|
3052
|
+
const { steps, activeStep, activeIndex } = phx.frontend.useStepNavigation()
|
|
3053
|
+
const direction = useStepDirection(activeIndex)
|
|
3054
|
+
|
|
3055
|
+
if (activeStep === null) {
|
|
3056
|
+
return null
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
return (
|
|
3060
|
+
<Box
|
|
3061
|
+
component="div"
|
|
3062
|
+
key={activeStep.state}
|
|
3063
|
+
sx={{
|
|
3064
|
+
display: 'grid',
|
|
3065
|
+
gap: compact ? 2.5 : 3.5,
|
|
3066
|
+
animation: `${direction === 1 ? ENTER_FROM_END : ENTER_FROM_START} 420ms cubic-bezier(0.2, 0.7, 0.2, 1)`,
|
|
3067
|
+
'@media (prefers-reduced-motion: reduce)': { animation: 'none' },
|
|
3068
|
+
}}
|
|
3069
|
+
>
|
|
3070
|
+
{!compact && (
|
|
3071
|
+
<Box component="header" sx={{ display: 'grid', gap: 1 }}>
|
|
3072
|
+
<Typography sx={LABEL_SX}>
|
|
3073
|
+
Step {activeIndex + 1} of {steps.length}
|
|
3074
|
+
</Typography>
|
|
3075
|
+
<Typography component="h2" sx={{ ...HEADING_SX, fontSize: 30, lineHeight: 1.05 }}>
|
|
3076
|
+
{activeStep.label}
|
|
3077
|
+
</Typography>
|
|
3078
|
+
{STEP_INTROS[activeStep.state] && (
|
|
3079
|
+
<Typography sx={{ fontFamily: NIGHTJAR.font, fontSize: 15, lineHeight: 1.45, color: NIGHTJAR.muted }}>
|
|
3080
|
+
{STEP_INTROS[activeStep.state]}
|
|
3081
|
+
</Typography>
|
|
3082
|
+
)}
|
|
3083
|
+
</Box>
|
|
3084
|
+
)}
|
|
3085
|
+
<StepFields compact={compact} />
|
|
3086
|
+
</Box>
|
|
3087
|
+
)
|
|
3088
|
+
}
|
|
3089
|
+
```
|
|
3090
|
+
|
|
3091
|
+
### ./src/\_shared/SummaryPanel.tsx
|
|
3092
|
+
|
|
3093
|
+
```tsx
|
|
3094
|
+
import { Box, Button, InputBase, Typography } from '@mui/material'
|
|
3095
|
+
import type { ReactNode } from 'react'
|
|
3096
|
+
import { useEffect, useRef } from 'react'
|
|
3097
|
+
|
|
3098
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
3099
|
+
import { HEADING_SX, LABEL_SX, NIGHTJAR, SummaryRows, useWarnings } from '../_fields/index.js'
|
|
3100
|
+
import type { OrderForm } from './order-form.js'
|
|
3101
|
+
import { STEP_WARNINGS } from './step-warnings.js'
|
|
3102
|
+
|
|
3103
|
+
const SECTION_SX = { display: 'grid', gap: 1.5 } as const
|
|
3104
|
+
|
|
3105
|
+
interface ContactInputProps {
|
|
3106
|
+
readonly id: string
|
|
3107
|
+
readonly label: string
|
|
3108
|
+
readonly value: string
|
|
3109
|
+
readonly onChange: (value: string) => void
|
|
3110
|
+
readonly error: string | undefined
|
|
3111
|
+
readonly type: 'text' | 'email'
|
|
3112
|
+
readonly autoComplete: string
|
|
3113
|
+
readonly disabled: boolean
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
const ContactInput = ({ id, label, value, onChange, error, type, autoComplete, disabled }: ContactInputProps) => {
|
|
3117
|
+
return (
|
|
3118
|
+
<Box component="div" sx={{ display: 'grid', gap: 0.75 }}>
|
|
3119
|
+
<Typography
|
|
3120
|
+
component="label"
|
|
3121
|
+
htmlFor={id}
|
|
3122
|
+
sx={{ fontFamily: NIGHTJAR.font, fontSize: 14, color: NIGHTJAR.muted }}
|
|
3123
|
+
>
|
|
3124
|
+
{label}
|
|
3125
|
+
</Typography>
|
|
3126
|
+
<InputBase
|
|
3127
|
+
id={id}
|
|
3128
|
+
type={type}
|
|
3129
|
+
value={value}
|
|
3130
|
+
disabled={disabled}
|
|
3131
|
+
autoComplete={autoComplete}
|
|
3132
|
+
onChange={event => onChange(event.target.value)}
|
|
3133
|
+
inputProps={{ 'aria-invalid': error !== undefined, 'aria-describedby': error ? `${id}-error` : undefined }}
|
|
3134
|
+
sx={{
|
|
3135
|
+
paddingInline: 1.5,
|
|
3136
|
+
paddingBlock: 0.75,
|
|
3137
|
+
borderRadius: 1.5,
|
|
3138
|
+
border: `1px solid ${error ? NIGHTJAR.alert : NIGHTJAR.line}`,
|
|
3139
|
+
bgcolor: NIGHTJAR.raised,
|
|
3140
|
+
fontFamily: NIGHTJAR.font,
|
|
3141
|
+
fontSize: 16,
|
|
3142
|
+
color: NIGHTJAR.cream,
|
|
3143
|
+
'&.Mui-focused': { borderColor: error ? NIGHTJAR.alert : NIGHTJAR.amber },
|
|
3144
|
+
}}
|
|
3145
|
+
/>
|
|
3146
|
+
{error && (
|
|
3147
|
+
<Typography id={`${id}-error`} sx={{ fontFamily: NIGHTJAR.font, fontSize: 13, color: NIGHTJAR.alert }}>
|
|
3148
|
+
{error}
|
|
3149
|
+
</Typography>
|
|
3150
|
+
)}
|
|
3151
|
+
</Box>
|
|
3152
|
+
)
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
const Card = ({ children, tone }: { readonly children: ReactNode; readonly tone: 'plain' | 'warning' | 'alert' }) => {
|
|
3156
|
+
const borders = { plain: NIGHTJAR.line, warning: NIGHTJAR.amber, alert: NIGHTJAR.alert }
|
|
3157
|
+
const backgrounds = { plain: NIGHTJAR.raised, warning: NIGHTJAR.amberTint, alert: NIGHTJAR.alertTint }
|
|
3158
|
+
|
|
3159
|
+
return (
|
|
3160
|
+
<Box
|
|
3161
|
+
component="section"
|
|
3162
|
+
sx={{
|
|
3163
|
+
display: 'grid',
|
|
3164
|
+
gap: 1.5,
|
|
3165
|
+
padding: 2,
|
|
3166
|
+
borderRadius: 2,
|
|
3167
|
+
border: `1px solid ${borders[tone]}`,
|
|
3168
|
+
bgcolor: backgrounds[tone],
|
|
3169
|
+
}}
|
|
3170
|
+
>
|
|
3171
|
+
{children}
|
|
3172
|
+
</Box>
|
|
3173
|
+
)
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
// The warnings left between choices, each with the way to the step that resolves it. Ordering waits until none is
|
|
3177
|
+
// left: `useFinalizeActions` reports the action `disabled` with the same texts as `hints`.
|
|
3178
|
+
const Conflicts = () => {
|
|
3179
|
+
const warnings = useWarnings()
|
|
3180
|
+
const { steps, goTo } = phx.frontend.useStepNavigation()
|
|
3181
|
+
const conflicts = warnings.flatMap(warning => {
|
|
3182
|
+
const step = steps.find(candidate => STEP_WARNINGS[candidate.state]?.includes(warning.type))
|
|
3183
|
+
|
|
3184
|
+
return step ? [{ ...warning, step }] : []
|
|
3185
|
+
})
|
|
3186
|
+
|
|
3187
|
+
if (conflicts.length === 0) {
|
|
3188
|
+
return null
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
return (
|
|
3192
|
+
<Card tone="warning">
|
|
3193
|
+
<Typography component="h3" sx={{ ...LABEL_SX, color: NIGHTJAR.amber }}>
|
|
3194
|
+
{conflicts.length === 1 ? 'One choice to settle before ordering' : `${conflicts.length} choices to settle`}
|
|
3195
|
+
</Typography>
|
|
3196
|
+
{conflicts.map(conflict => (
|
|
3197
|
+
<Box component="div" key={conflict.type} sx={{ display: 'grid', gap: 1, justifyItems: 'start' }}>
|
|
3198
|
+
<Typography sx={{ fontFamily: NIGHTJAR.font, fontSize: 15, lineHeight: 1.45, color: NIGHTJAR.cream }}>
|
|
3199
|
+
{conflict.text}
|
|
3200
|
+
</Typography>
|
|
3201
|
+
<Button
|
|
3202
|
+
onClick={() => goTo(conflict.step.index)}
|
|
3203
|
+
sx={{
|
|
3204
|
+
...LABEL_SX,
|
|
3205
|
+
minBlockSize: 36,
|
|
3206
|
+
paddingInline: 1.5,
|
|
3207
|
+
color: NIGHTJAR.ink,
|
|
3208
|
+
bgcolor: NIGHTJAR.amber,
|
|
3209
|
+
'&:hover': { bgcolor: NIGHTJAR.amberHover },
|
|
3210
|
+
}}
|
|
3211
|
+
>
|
|
3212
|
+
Go to {conflict.step.label}
|
|
3213
|
+
</Button>
|
|
3214
|
+
</Box>
|
|
3215
|
+
))}
|
|
3216
|
+
</Card>
|
|
3217
|
+
)
|
|
3218
|
+
}
|
|
3219
|
+
|
|
3220
|
+
interface SummaryPanelProps {
|
|
3221
|
+
readonly compact: boolean
|
|
3222
|
+
readonly form: OrderForm
|
|
3223
|
+
}
|
|
3224
|
+
|
|
3225
|
+
// The last step: every choice as the spec's control hooks read it, the conflicts left, the contact form and the
|
|
3226
|
+
// order's error. The summary only reviews; a choice is changed on its step, which the stepper and the conflicts link
|
|
3227
|
+
// to. **Place order** is in the bar at the bottom.
|
|
3228
|
+
export const SummaryPanel = ({ compact, form }: SummaryPanelProps) => {
|
|
3229
|
+
const { primary } = phx.frontend.useFinalizeActions()
|
|
3230
|
+
const { contact, setContact, errors, showErrors, blockedAttempts, isComplete } = form
|
|
3231
|
+
const isPending = primary?.isPending ?? false
|
|
3232
|
+
const topRef = useRef<HTMLDivElement>(null)
|
|
3233
|
+
const detailsRef = useRef<HTMLElement>(null)
|
|
3234
|
+
// The attempts already handled; the summary opens again after a blocked attempt without scrolling
|
|
3235
|
+
const scrolledFor = useRef(blockedAttempts)
|
|
3236
|
+
|
|
3237
|
+
// After each blocked attempt to order, bring what blocks it into view: the missing details, or the conflicts on top.
|
|
3238
|
+
// Typing into the form changes `isComplete` but is no new attempt, so it does not scroll.
|
|
3239
|
+
useEffect(() => {
|
|
3240
|
+
if (blockedAttempts === scrolledFor.current) {
|
|
3241
|
+
return
|
|
3242
|
+
}
|
|
3243
|
+
|
|
3244
|
+
scrolledFor.current = blockedAttempts
|
|
3245
|
+
const target = isComplete ? topRef.current : detailsRef.current
|
|
3246
|
+
target?.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
|
3247
|
+
}, [blockedAttempts, isComplete])
|
|
3248
|
+
|
|
3249
|
+
return (
|
|
3250
|
+
<Box component="div" ref={topRef} sx={{ display: 'grid', gap: compact ? 2.5 : 3, scrollMarginBlockStart: 32 }}>
|
|
3251
|
+
{!compact && (
|
|
3252
|
+
<Box component="header" sx={{ display: 'grid', gap: 1 }}>
|
|
3253
|
+
<Typography sx={LABEL_SX}>Last step</Typography>
|
|
3254
|
+
<Typography component="h2" sx={{ ...HEADING_SX, fontSize: 30, lineHeight: 1.05 }}>
|
|
3255
|
+
Your Nightjar
|
|
3256
|
+
</Typography>
|
|
3257
|
+
<Typography sx={{ fontFamily: NIGHTJAR.font, fontSize: 15, lineHeight: 1.45, color: NIGHTJAR.muted }}>
|
|
3258
|
+
Check the build, tell us who it is for, and place the order. Nothing is charged until the shop confirms.
|
|
3259
|
+
</Typography>
|
|
3260
|
+
</Box>
|
|
3261
|
+
)}
|
|
3262
|
+
|
|
3263
|
+
{primary?.error && (
|
|
3264
|
+
<Card tone="alert">
|
|
3265
|
+
<Typography component="h3" sx={{ ...LABEL_SX, color: NIGHTJAR.alert }}>
|
|
3266
|
+
The order did not go through
|
|
3267
|
+
</Typography>
|
|
3268
|
+
<Typography
|
|
3269
|
+
role="alert"
|
|
3270
|
+
sx={{ fontFamily: NIGHTJAR.font, fontSize: 15, lineHeight: 1.45, color: NIGHTJAR.cream }}
|
|
3271
|
+
>
|
|
3272
|
+
{primary.error.message}
|
|
3273
|
+
</Typography>
|
|
3274
|
+
</Card>
|
|
3275
|
+
)}
|
|
3276
|
+
|
|
3277
|
+
<Conflicts />
|
|
3278
|
+
|
|
3279
|
+
<Box component="section" sx={SECTION_SX}>
|
|
3280
|
+
<Typography component="h3" sx={LABEL_SX}>
|
|
3281
|
+
The build
|
|
3282
|
+
</Typography>
|
|
3283
|
+
<SummaryRows />
|
|
3284
|
+
</Box>
|
|
3285
|
+
|
|
3286
|
+
<Box component="section" ref={detailsRef} sx={{ ...SECTION_SX, scrollMarginBlockEnd: 32 }}>
|
|
3287
|
+
<Typography component="h3" sx={LABEL_SX}>
|
|
3288
|
+
Your details
|
|
3289
|
+
</Typography>
|
|
3290
|
+
<Box component="div" sx={{ display: 'grid', gridTemplateColumns: compact ? '1fr' : '1fr 1fr', gap: 1.5 }}>
|
|
3291
|
+
<ContactInput
|
|
3292
|
+
id="nightjar-name"
|
|
3293
|
+
label="Name"
|
|
3294
|
+
type="text"
|
|
3295
|
+
autoComplete="name"
|
|
3296
|
+
value={contact.name}
|
|
3297
|
+
onChange={name => setContact({ ...contact, name })}
|
|
3298
|
+
error={showErrors ? errors.name : undefined}
|
|
3299
|
+
disabled={isPending}
|
|
3300
|
+
/>
|
|
3301
|
+
<ContactInput
|
|
3302
|
+
id="nightjar-email"
|
|
3303
|
+
label="E-mail"
|
|
3304
|
+
type="email"
|
|
3305
|
+
autoComplete="email"
|
|
3306
|
+
value={contact.email}
|
|
3307
|
+
onChange={email => setContact({ ...contact, email })}
|
|
3308
|
+
error={showErrors ? errors.email : undefined}
|
|
3309
|
+
disabled={isPending}
|
|
3310
|
+
/>
|
|
3311
|
+
</Box>
|
|
3312
|
+
</Box>
|
|
3313
|
+
</Box>
|
|
3314
|
+
)
|
|
3315
|
+
}
|
|
3316
|
+
```
|
|
3317
|
+
|
|
3318
|
+
### ./src/\_shared/order-form.ts
|
|
3319
|
+
|
|
3320
|
+
```ts
|
|
3321
|
+
import { useState } from 'react'
|
|
3322
|
+
|
|
3323
|
+
/** The customer's name and e-mail, which the summary asks for. */
|
|
3324
|
+
export interface Contact {
|
|
3325
|
+
readonly name: string
|
|
3326
|
+
readonly email: string
|
|
3327
|
+
}
|
|
3328
|
+
|
|
3329
|
+
/** What is missing from the contact details, per field. */
|
|
3330
|
+
export interface ContactErrors {
|
|
3331
|
+
readonly name?: string
|
|
3332
|
+
readonly email?: string
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
/**
|
|
3336
|
+
* The summary's contact form. It is the layout's own state, not the configuration's: the details are needed only to
|
|
3337
|
+
* place the order, and the confirmation thanks the customer by name.
|
|
3338
|
+
*/
|
|
3339
|
+
export interface OrderForm {
|
|
3340
|
+
readonly contact: Contact
|
|
3341
|
+
readonly setContact: (contact: Contact) => void
|
|
3342
|
+
readonly errors: ContactErrors
|
|
3343
|
+
readonly isComplete: boolean
|
|
3344
|
+
/** Whether the form shows what is missing: from the first attempt to order on */
|
|
3345
|
+
readonly showErrors: boolean
|
|
3346
|
+
/** How many attempts to order were blocked; the summary scrolls to what blocks the order after each */
|
|
3347
|
+
readonly blockedAttempts: number
|
|
3348
|
+
readonly revealErrors: () => void
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/
|
|
3352
|
+
|
|
3353
|
+
export const useOrderForm = (): OrderForm => {
|
|
3354
|
+
const [contact, setContact] = useState<Contact>({ name: '', email: '' })
|
|
3355
|
+
const [blockedAttempts, setBlockedAttempts] = useState(0)
|
|
3356
|
+
const errors: ContactErrors = {
|
|
3357
|
+
...(contact.name.trim() === '' && { name: 'Enter the name the order is for.' }),
|
|
3358
|
+
...(!EMAIL.test(contact.email.trim()) && { email: 'Enter an e-mail address for the build updates.' }),
|
|
3359
|
+
}
|
|
3360
|
+
|
|
3361
|
+
return {
|
|
3362
|
+
contact,
|
|
3363
|
+
setContact,
|
|
3364
|
+
errors,
|
|
3365
|
+
isComplete: errors.name === undefined && errors.email === undefined,
|
|
3366
|
+
showErrors: blockedAttempts > 0,
|
|
3367
|
+
blockedAttempts,
|
|
3368
|
+
revealErrors: () => setBlockedAttempts(attempts => attempts + 1),
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
```
|
|
3372
|
+
|
|
3373
|
+
### ./src/\_shared/step-warnings.ts
|
|
3374
|
+
|
|
3375
|
+
```ts
|
|
3376
|
+
// Which step resolves each warning: the stepper marks the step while the warning holds, and the summary sends the
|
|
3377
|
+
// customer there. A warning can involve several steps; this names the one the layout sends the customer to.
|
|
3378
|
+
export const STEP_WARNINGS: Readonly<Record<string, readonly string[]>> = {
|
|
3379
|
+
electronics: ['offsetHumbuckers'],
|
|
3380
|
+
}
|
|
3381
|
+
```
|
|
3382
|
+
|
|
3383
|
+
### ./src/\_shared/use-reached-step.ts
|
|
3384
|
+
|
|
3385
|
+
```ts
|
|
3386
|
+
import { useState } from 'react'
|
|
3387
|
+
|
|
3388
|
+
// The furthest step the customer has reached. The stepper offers every step up to it, so after changing an earlier
|
|
3389
|
+
// step the customer returns to where they were without stepping through the ones between. The layout calls this, where
|
|
3390
|
+
// it stays mounted, so the furthest step survives the switch between the desktop and the phone page.
|
|
3391
|
+
export const useReachedStep = (activeIndex: number): number => {
|
|
3392
|
+
const [reached, setReached] = useState(activeIndex)
|
|
3393
|
+
|
|
3394
|
+
if (activeIndex > reached) {
|
|
3395
|
+
setReached(activeIndex)
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
return Math.max(reached, activeIndex)
|
|
3399
|
+
}
|
|
3400
|
+
```
|
|
3401
|
+
|
|
3402
|
+
### ./src/elements/ConfiguratorLayout.tsx
|
|
3403
|
+
|
|
3404
|
+
```tsx
|
|
3405
|
+
import { Box } from '@mui/material'
|
|
3406
|
+
|
|
3407
|
+
import { LayoutSlots } from '@salsita-ai/sdk/configurator'
|
|
3408
|
+
|
|
3409
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
3410
|
+
import { NIGHTJAR } from '../_fields/index.js'
|
|
3411
|
+
import { DesktopPage } from '../_shared/DesktopPage.js'
|
|
3412
|
+
import { PhonePage } from '../_shared/PhonePage.js'
|
|
3413
|
+
import { useOrderForm } from '../_shared/order-form.js'
|
|
3414
|
+
import { useReachedStep } from '../_shared/use-reached-step.js'
|
|
3415
|
+
|
|
3416
|
+
// A wizard over the scene. The canvas stays mounted the whole time; the desktop or the phone page lies over it. The
|
|
3417
|
+
// contact form's state and the furthest step reached live here, so they survive the switch between the two; the form
|
|
3418
|
+
// also survives the move to the confirmation.
|
|
3419
|
+
const ConfiguratorLayout = () => {
|
|
3420
|
+
// `null` until the container has been measured; only the scene shows until then
|
|
3421
|
+
const viewports = phx.frontend.useActiveViewports()
|
|
3422
|
+
const { activeIndex } = phx.frontend.useStepNavigation()
|
|
3423
|
+
const form = useOrderForm()
|
|
3424
|
+
const reached = useReachedStep(activeIndex)
|
|
3425
|
+
const isDesktop = viewports?.includes('desktop') ?? false
|
|
3426
|
+
|
|
3427
|
+
return (
|
|
3428
|
+
<Box component="div" sx={{ position: 'absolute', inset: 0, overflow: 'clip', bgcolor: NIGHTJAR.ink }}>
|
|
3429
|
+
<LayoutSlots.SharedCanvas />
|
|
3430
|
+
{viewports !== null &&
|
|
3431
|
+
(isDesktop ? <DesktopPage form={form} reached={reached} /> : <PhonePage form={form} reached={reached} />)}
|
|
3432
|
+
</Box>
|
|
3433
|
+
)
|
|
3434
|
+
}
|
|
3435
|
+
|
|
3436
|
+
export default phx.frontend.createCustomPhoenixLayoutElement('ConfiguratorLayout', ConfiguratorLayout)
|
|
3437
|
+
```
|
|
3438
|
+
|
|
3439
|
+
### ./src/elements/NightjarStage.tsx
|
|
3440
|
+
|
|
3441
|
+
```tsx
|
|
3442
|
+
import { Environment, Lightformer } from '@react-three/drei'
|
|
3443
|
+
import { useMemo } from 'react'
|
|
3444
|
+
import { CanvasTexture, SRGBColorSpace } from 'three'
|
|
3445
|
+
|
|
3446
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
3447
|
+
import { NIGHTJAR } from '../_fields/index.js'
|
|
3448
|
+
|
|
3449
|
+
/** A warm pool of light that fades into the dark, drawn once on a canvas. */
|
|
3450
|
+
const createGlow = (): CanvasTexture => {
|
|
3451
|
+
const size = 512
|
|
3452
|
+
const canvas = document.createElement('canvas')
|
|
3453
|
+
canvas.width = size
|
|
3454
|
+
canvas.height = size
|
|
3455
|
+
const context = canvas.getContext('2d')
|
|
3456
|
+
|
|
3457
|
+
if (context) {
|
|
3458
|
+
const gradient = context.createRadialGradient(size / 2, size / 2, 0, size / 2, size / 2, size / 2)
|
|
3459
|
+
gradient.addColorStop(0, 'rgba(242, 163, 58, 0.42)')
|
|
3460
|
+
gradient.addColorStop(0.22, 'rgba(214, 112, 46, 0.2)')
|
|
3461
|
+
gradient.addColorStop(0.5, 'rgba(122, 30, 43, 0.07)')
|
|
3462
|
+
gradient.addColorStop(0.75, 'rgba(14, 15, 18, 0)')
|
|
3463
|
+
context.fillStyle = gradient
|
|
3464
|
+
context.fillRect(0, 0, size, size)
|
|
3465
|
+
}
|
|
3466
|
+
|
|
3467
|
+
const texture = new CanvasTexture(canvas)
|
|
3468
|
+
texture.colorSpace = SRGBColorSpace
|
|
3469
|
+
return texture
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
// The stage named by the spec's `canvasCustomEnvironment`: a dark room with a warm glow on the wall behind the guitar,
|
|
3473
|
+
// a warm key light and a cool rim. The light panels exist only in the reflections, where they draw the long highlights
|
|
3474
|
+
// on the clearcoat and the hardware. The scene is in millimetres.
|
|
3475
|
+
export default phx.frontend.createCustomPhoenixLayoutElement('NightjarStage', () => {
|
|
3476
|
+
const glow = useMemo(createGlow, [])
|
|
3477
|
+
|
|
3478
|
+
return (
|
|
3479
|
+
<>
|
|
3480
|
+
<color attach="background" args={[NIGHTJAR.ink]} />
|
|
3481
|
+
<ambientLight intensity={0.4} />
|
|
3482
|
+
<directionalLight position={[-900, 1400, 1600]} intensity={2.4} color="#ffe6c7" />
|
|
3483
|
+
<directionalLight position={[1400, 700, -900]} intensity={1.2} color="#a9c1ff" />
|
|
3484
|
+
|
|
3485
|
+
{/* The glow on the wall, behind the guitar and out of the camera's fit */}
|
|
3486
|
+
<mesh position={[-250, 300, -900]}>
|
|
3487
|
+
<planeGeometry args={[3600, 3600]} />
|
|
3488
|
+
<meshBasicMaterial map={glow} transparent depthWrite={false} toneMapped={false} />
|
|
3489
|
+
</mesh>
|
|
3490
|
+
|
|
3491
|
+
<Environment resolution={256} frames={1}>
|
|
3492
|
+
<color attach="background" args={['#15161b']} />
|
|
3493
|
+
{/* Tall strips at both sides for the long highlights, a softbox above, an amber wash behind */}
|
|
3494
|
+
<Lightformer form="rect" intensity={7} position={[-5, 1, 3]} rotation-y={Math.PI / 3} scale={[1.2, 9, 1]} />
|
|
3495
|
+
<Lightformer form="rect" intensity={4} position={[5, 1, 2]} rotation-y={-Math.PI / 3} scale={[1, 9, 1]} />
|
|
3496
|
+
<Lightformer form="rect" intensity={2.5} position={[0, 6, 1]} rotation-x={Math.PI / 2} scale={[8, 3, 1]} />
|
|
3497
|
+
<Lightformer form="rect" intensity={1.2} position={[0, 1, 7]} rotation-y={Math.PI} scale={[8, 4, 1]} />
|
|
3498
|
+
<Lightformer form="rect" color={NIGHTJAR.amber} intensity={0.8} position={[0, -2, -7]} scale={[10, 3, 1]} />
|
|
3499
|
+
</Environment>
|
|
3500
|
+
</>
|
|
3501
|
+
)
|
|
3502
|
+
})
|
|
3503
|
+
```
|
|
3504
|
+
|
|
3505
|
+
### ./src/expressions/guitar.ts
|
|
3506
|
+
|
|
3507
|
+
```ts
|
|
3508
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
3509
|
+
|
|
3510
|
+
const BASE_PRICE = 1650
|
|
3511
|
+
const SIGNATURE_PRICE = 45
|
|
3512
|
+
const SIGNATURE_LIMIT = 16
|
|
3513
|
+
|
|
3514
|
+
const SURCHARGES = {
|
|
3515
|
+
bodyShape: { Offset: 0, SingleCut: 150 },
|
|
3516
|
+
finish: { Seafoam: 0, CandyRed: 180, Midnight: 120, NaturalAsh: 90 },
|
|
3517
|
+
pickguard: { Parchment: 0, Black: 0, Pearloid: 40, None: 0 },
|
|
3518
|
+
neckWood: { Maple: 0, RoastedMaple: 110, Mahogany: 80 },
|
|
3519
|
+
fretboard: { Maple: 0, Rosewood: 0, Ebony: 140 },
|
|
3520
|
+
pickups: { SingleCoils: 0, Humbuckers: 120, P90s: 90 },
|
|
3521
|
+
hardware: { Chrome: 0, Gold: 160, Black: 60 },
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
// The wood textures ship with Storybook; a texture is multiplied by the material's colour
|
|
3525
|
+
const LIGHT_WOOD = '/textures/Material_Lightwood.png'
|
|
3526
|
+
const DARK_WOOD = '/textures/Material_Darkwood.png'
|
|
3527
|
+
const ASH = '/textures/oak_varnished_BaseColor.png'
|
|
3528
|
+
|
|
3529
|
+
// How each finish renders under the clearcoat: the paints are solid, natural ash shows the grain through a tint
|
|
3530
|
+
const FINISHES = {
|
|
3531
|
+
Seafoam: { color: '#9fd8c4', metalness: 0, roughness: 0.38, texture: null },
|
|
3532
|
+
CandyRed: { color: '#a3101c', metalness: 0.35, roughness: 0.3, texture: null },
|
|
3533
|
+
Midnight: { color: '#1b2a52', metalness: 0.55, roughness: 0.34, texture: null },
|
|
3534
|
+
NaturalAsh: { color: '#f4e2c4', metalness: 0, roughness: 0.5, texture: ASH },
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
const PICKGUARDS = {
|
|
3538
|
+
Parchment: { color: '#ece2cb', roughness: 0.5, iridescence: 0 },
|
|
3539
|
+
Black: { color: '#121214', roughness: 0.3, iridescence: 0 },
|
|
3540
|
+
Pearloid: { color: '#f2ede6', roughness: 0.22, iridescence: 1 },
|
|
3541
|
+
None: { color: '#ece2cb', roughness: 0.5, iridescence: 0 },
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
const NECKS = {
|
|
3545
|
+
Maple: { color: '#f6dcae', texture: LIGHT_WOOD },
|
|
3546
|
+
RoastedMaple: { color: '#c58a4e', texture: LIGHT_WOOD },
|
|
3547
|
+
Mahogany: { color: '#c47a5a', texture: DARK_WOOD },
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
const FRETBOARDS = {
|
|
3551
|
+
Maple: { color: '#f6dcae', texture: LIGHT_WOOD, inlay: '#1b1714' },
|
|
3552
|
+
Rosewood: { color: '#ffffff', texture: DARK_WOOD, inlay: '#efe9df' },
|
|
3553
|
+
Ebony: { color: '#3a3330', texture: DARK_WOOD, inlay: '#efe9df' },
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
const HARDWARE = {
|
|
3557
|
+
Chrome: { color: '#eceef0', metalness: 1, roughness: 0.12 },
|
|
3558
|
+
Gold: { color: '#e6b964', metalness: 1, roughness: 0.2 },
|
|
3559
|
+
Black: { color: '#26262a', metalness: 0.7, roughness: 0.32 },
|
|
3560
|
+
}
|
|
3561
|
+
|
|
3562
|
+
export default phx.domain.implementCustomExpressions({
|
|
3563
|
+
Guitar: {
|
|
3564
|
+
$price: s => {
|
|
3565
|
+
return (
|
|
3566
|
+
BASE_PRICE +
|
|
3567
|
+
SURCHARGES.bodyShape[s.bodyShape] +
|
|
3568
|
+
SURCHARGES.finish[s.finish] +
|
|
3569
|
+
SURCHARGES.pickguard[s.pickguard] +
|
|
3570
|
+
SURCHARGES.neckWood[s.neckWood] +
|
|
3571
|
+
SURCHARGES.fretboard[s.fretboard] +
|
|
3572
|
+
SURCHARGES.pickups[s.pickups] +
|
|
3573
|
+
SURCHARGES.hardware[s.hardware] +
|
|
3574
|
+
(s.signature.trim() === '' ? 0 : SIGNATURE_PRICE)
|
|
3575
|
+
)
|
|
3576
|
+
},
|
|
3577
|
+
$signatureLimit: () => {
|
|
3578
|
+
return SIGNATURE_LIMIT
|
|
3579
|
+
},
|
|
3580
|
+
$finishColor: s => {
|
|
3581
|
+
return FINISHES[s.finish].color
|
|
3582
|
+
},
|
|
3583
|
+
$finishMetalness: s => {
|
|
3584
|
+
return FINISHES[s.finish].metalness
|
|
3585
|
+
},
|
|
3586
|
+
$finishRoughness: s => {
|
|
3587
|
+
return FINISHES[s.finish].roughness
|
|
3588
|
+
},
|
|
3589
|
+
$finishTexture: s => {
|
|
3590
|
+
return FINISHES[s.finish].texture
|
|
3591
|
+
},
|
|
3592
|
+
$pickguardColor: s => {
|
|
3593
|
+
return PICKGUARDS[s.pickguard].color
|
|
3594
|
+
},
|
|
3595
|
+
$pickguardRoughness: s => {
|
|
3596
|
+
return PICKGUARDS[s.pickguard].roughness
|
|
3597
|
+
},
|
|
3598
|
+
$pickguardIridescence: s => {
|
|
3599
|
+
return PICKGUARDS[s.pickguard].iridescence
|
|
3600
|
+
},
|
|
3601
|
+
$neckColor: s => {
|
|
3602
|
+
return NECKS[s.neckWood].color
|
|
3603
|
+
},
|
|
3604
|
+
$neckTexture: s => {
|
|
3605
|
+
return NECKS[s.neckWood].texture
|
|
3606
|
+
},
|
|
3607
|
+
$fretboardColor: s => {
|
|
3608
|
+
return FRETBOARDS[s.fretboard].color
|
|
3609
|
+
},
|
|
3610
|
+
$fretboardTexture: s => {
|
|
3611
|
+
return FRETBOARDS[s.fretboard].texture
|
|
3612
|
+
},
|
|
3613
|
+
$inlayColor: s => {
|
|
3614
|
+
return FRETBOARDS[s.fretboard].inlay
|
|
3615
|
+
},
|
|
3616
|
+
$hardwareColor: s => {
|
|
3617
|
+
return HARDWARE[s.hardware].color
|
|
3618
|
+
},
|
|
3619
|
+
$hardwareMetalness: s => {
|
|
3620
|
+
return HARDWARE[s.hardware].metalness
|
|
3621
|
+
},
|
|
3622
|
+
$hardwareRoughness: s => {
|
|
3623
|
+
return HARDWARE[s.hardware].roughness
|
|
3624
|
+
},
|
|
3625
|
+
// Single coils match a black pickguard and are cream otherwise; humbuckers and P-90s are black
|
|
3626
|
+
$pickupCoverColor: s => {
|
|
3627
|
+
return s.pickups === 'SingleCoils' && s.pickguard !== 'Black' ? '#efe7d4' : '#141416'
|
|
3628
|
+
},
|
|
3629
|
+
// Gold leaf on the dark woods, black ink on maple
|
|
3630
|
+
$signatureColor: s => {
|
|
3631
|
+
return s.neckWood === 'Maple' ? '#1c1714' : '#e8c27a'
|
|
3632
|
+
},
|
|
3633
|
+
},
|
|
3634
|
+
})
|
|
3635
|
+
```
|
|
3636
|
+
|
|
3637
|
+
### ./src/handlers/place-order.ts
|
|
3638
|
+
|
|
3639
|
+
```ts
|
|
3640
|
+
import { useCallback } from 'react'
|
|
3641
|
+
|
|
3642
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
3643
|
+
|
|
3644
|
+
// Stands in for Northfret's order service. Placing an order takes a moment. The service checks stock when the order
|
|
3645
|
+
// arrives, which the configurator cannot know beforehand: gold hardware is back-ordered, so it refuses that order with
|
|
3646
|
+
// its reason; any other order is accepted and the page shows the confirmation.
|
|
3647
|
+
export default phx.frontend.createPhoenixHandler('placeOrder', () => {
|
|
3648
|
+
const navigate = phx.frontend.useNavigate()
|
|
3649
|
+
const guitar = phx.frontend.useRootEntityState()
|
|
3650
|
+
|
|
3651
|
+
return {
|
|
3652
|
+
handler: useCallback(async () => {
|
|
3653
|
+
await new Promise(resolve => setTimeout(resolve, 1600))
|
|
3654
|
+
|
|
3655
|
+
if (guitar.hardware === 'Gold') {
|
|
3656
|
+
throw new Error(
|
|
3657
|
+
'Gold hardware is back-ordered until next month. Choose chrome or black hardware, or order later.'
|
|
3658
|
+
)
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
navigate('ordered')
|
|
3662
|
+
}, [guitar.hardware, navigate]),
|
|
3663
|
+
}
|
|
3664
|
+
})
|
|
3665
|
+
```
|
|
3666
|
+
|
|
3667
|
+
### ./src/invariants/guitar.ts
|
|
3668
|
+
|
|
3669
|
+
```ts
|
|
3670
|
+
import { phx } from '../../.phoenix/phx.js'
|
|
3671
|
+
|
|
3672
|
+
export default phx.domain.implementCustomInvariants({
|
|
3673
|
+
Guitar: {
|
|
3674
|
+
// `fixable`: a change that makes the signature too long is rejected, so the headstock never shows more than fits.
|
|
3675
|
+
// The message names the limit, so it is built here rather than in the spec.
|
|
3676
|
+
signatureLength: s => {
|
|
3677
|
+
if (s.signature.length <= s.$signatureLimit) {
|
|
3678
|
+
return []
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
return [
|
|
3682
|
+
{
|
|
3683
|
+
type: 'signatureTooLong',
|
|
3684
|
+
severity: 'fixable',
|
|
3685
|
+
message: `The headstock fits ${s.$signatureLimit} characters. Shorten the signature.`,
|
|
3686
|
+
},
|
|
3687
|
+
]
|
|
3688
|
+
},
|
|
3689
|
+
// `warning` in the spec: the change is kept, so a body chosen after the pickups does not undo them, and ordering
|
|
3690
|
+
// waits until the customer resolves it
|
|
3691
|
+
offsetHumbuckers: s => {
|
|
3692
|
+
return s.bodyShape !== 'Offset' || s.pickups !== 'Humbuckers'
|
|
3693
|
+
},
|
|
3694
|
+
},
|
|
3695
|
+
})
|
|
3696
|
+
```
|
|
3697
|
+
|
|
3698
|
+
</details>
|