@salsita-ai/sdk 0.50.0-develop.8f6deac646 → 0.50.0-develop.91d65bd507

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