@salsita-ai/sdk 0.50.0-develop.08837efd9c → 0.50.0-develop.0b28d3ba99

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 (1069) 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/index.d.ts +1 -0
  13. package/dist/application/index.js +1 -1
  14. package/dist/application/mount-application.d.ts +1 -1
  15. package/dist/application/mount-application.js +1 -1
  16. package/dist/application/mount-configurator.d.ts +19 -0
  17. package/dist/application/mount-configurator.js +1 -0
  18. package/dist/application/mounted-application-runtime.d.ts +4 -0
  19. package/dist/application/mounted-application-runtime.js +1 -0
  20. package/dist/application/register-web-component.d.ts +2 -0
  21. package/dist/application/register-web-component.js +1 -0
  22. package/dist/application-spec/caching/content-diff.js +1 -1
  23. package/dist/application-spec/core/application-file.d.ts +0 -2
  24. package/dist/application-spec/core/application-file.js +1 -1
  25. package/dist/application-spec/core/expressions/index.d.ts +2 -0
  26. package/dist/application-spec/core/expressions/index.js +1 -0
  27. package/dist/application-spec/core/expressions/relation-expression-params.d.ts +11 -0
  28. package/dist/application-spec/core/expressions/relation-expression-params.js +1 -0
  29. package/dist/application-spec/core/expressions/sdk-expression-implementation.d.ts +24 -0
  30. package/dist/application-spec/core/expressions/sdk-expression-implementation.js +1 -0
  31. package/dist/application-spec/core/index.d.ts +1 -0
  32. package/dist/application-spec/core/index.js +1 -1
  33. package/dist/application-spec/errors/diagnostic-codes.d.ts +14 -0
  34. package/dist/application-spec/errors/diagnostic-codes.js +1 -1
  35. package/dist/application-spec/errors/diagnostic-messages.d.ts +15 -1
  36. package/dist/application-spec/errors/diagnostic-messages.js +1 -1
  37. package/dist/application-spec/generate-application/code/common/code-imports.js +1 -1
  38. package/dist/application-spec/generate-application/code/common/env-reader.js +2 -2
  39. package/dist/application-spec/generate-application/code/common/index.d.ts +1 -0
  40. package/dist/application-spec/generate-application/code/common/index.js +1 -1
  41. package/dist/application-spec/generate-application/code/common/single-quoted-string.d.ts +6 -0
  42. package/dist/application-spec/generate-application/code/common/single-quoted-string.js +1 -0
  43. package/dist/application-spec/generate-application/code/domain-runtime-files.js +1 -1
  44. package/dist/application-spec/generate-application/code/generate-application-code.d.ts +3 -2
  45. package/dist/application-spec/generate-application/code/generate-application-code.js +1 -1
  46. package/dist/application-spec/generate-application/code/project-scaffolding.d.ts +15 -14
  47. package/dist/application-spec/generate-application/code/project-scaffolding.js +2 -2
  48. package/dist/application-spec/generate-application/code/standalone-app-files.d.ts +2 -2
  49. package/dist/application-spec/generate-application/code/standalone-app-files.js +1 -1
  50. package/dist/application-spec/generate-application/code/story-files.js +1 -1
  51. package/dist/application-spec/generate-application/code/templates/admin-config.js +3 -0
  52. package/dist/application-spec/generate-application/code/templates/application-types.js +6 -6
  53. package/dist/application-spec/generate-application/code/templates/common/doc-comment.d.ts +2 -0
  54. package/dist/application-spec/generate-application/code/templates/common/doc-comment.js +1 -0
  55. package/dist/application-spec/generate-application/code/templates/common/format-property-key.d.ts +7 -0
  56. package/dist/application-spec/generate-application/code/templates/common/format-property-key.js +1 -0
  57. package/dist/application-spec/generate-application/code/templates/common/schema-to-type-string.js +3 -3
  58. package/dist/application-spec/generate-application/code/templates/configurator-config.js +6 -4
  59. package/dist/application-spec/generate-application/code/templates/configurator-globals.d.ts +2 -1
  60. package/dist/application-spec/generate-application/code/templates/configurator-globals.js +19 -6
  61. package/dist/application-spec/generate-application/code/templates/domain-spec/factory-types/domain-factory-types.d.ts +35 -0
  62. package/dist/application-spec/generate-application/code/templates/domain-spec/factory-types/domain-factory-types.js +5 -0
  63. package/dist/application-spec/generate-application/code/templates/domain-spec/factory-types/index.d.ts +1 -0
  64. package/dist/application-spec/generate-application/code/templates/domain-spec/factory-types/index.js +1 -0
  65. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-component-optics.d.ts +1 -1
  66. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-component-optics.js +6 -6
  67. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-domain-block.js +59 -63
  68. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-invariants.d.ts +8 -0
  69. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-invariants.js +1 -1
  70. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-presets.js +1 -1
  71. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-resources.js +13 -13
  72. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-states.d.ts +1 -1
  73. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-states.js +2 -2
  74. package/dist/application-spec/generate-application/code/templates/domain-spec/index.d.ts +2 -1
  75. package/dist/application-spec/generate-application/code/templates/domain-spec/index.js +1 -1
  76. package/dist/application-spec/generate-application/code/templates/eslint-config.d.ts +5 -0
  77. package/dist/application-spec/generate-application/code/templates/eslint-config.js +4 -5
  78. package/dist/application-spec/generate-application/code/templates/frontend-config.js +3 -5
  79. package/dist/application-spec/generate-application/code/templates/gitignore.js +0 -3
  80. package/dist/application-spec/generate-application/code/templates/global-definition/generate-action-types.js +7 -7
  81. package/dist/application-spec/generate-application/code/templates/global-definition/generate-assistant-form-element-types.d.ts +12 -0
  82. package/dist/application-spec/generate-application/code/templates/global-definition/generate-assistant-form-element-types.js +7 -0
  83. package/dist/application-spec/generate-application/code/templates/global-definition/generate-phx-types.d.ts +43 -0
  84. package/dist/application-spec/generate-application/code/templates/global-definition/generate-phx-types.js +33 -0
  85. package/dist/application-spec/generate-application/code/templates/global-definition/index.d.ts +2 -0
  86. package/dist/application-spec/generate-application/code/templates/global-definition/index.js +1 -1
  87. package/dist/application-spec/generate-application/code/templates/globals.js +24 -44
  88. package/dist/application-spec/generate-application/code/templates/index.d.ts +3 -3
  89. package/dist/application-spec/generate-application/code/templates/index.js +1 -1
  90. package/dist/application-spec/generate-application/code/templates/lingui-rc.d.ts +5 -0
  91. package/dist/application-spec/generate-application/code/templates/lingui-rc.js +4 -4
  92. package/dist/application-spec/generate-application/code/templates/prettier-config.d.ts +5 -0
  93. package/dist/application-spec/generate-application/code/templates/prettier-config.js +4 -5
  94. package/dist/application-spec/generate-application/code/templates/root-tsconfig.d.ts +6 -0
  95. package/dist/application-spec/generate-application/code/templates/root-tsconfig.js +2 -0
  96. package/dist/application-spec/generate-application/code/templates/skills/phoenix-cli-skill.js +1 -1
  97. package/dist/application-spec/generate-application/code/templates/skills/phoenix-create-configurator-skill.js +1 -1
  98. package/dist/application-spec/generate-application/code/templates/skills/phoenix-feedback-skill.js +5 -5
  99. package/dist/application-spec/generate-application/code/templates/skills/phoenix-review-skill.js +3 -3
  100. package/dist/application-spec/generate-application/code/templates/skills/phoenix-skill.js +1 -1
  101. package/dist/application-spec/generate-application/code/templates/skills/phoenix-variants-skill.js +2 -2
  102. package/dist/application-spec/generate-application/code/templates/skills/sdk-content.d.ts +32 -7
  103. package/dist/application-spec/generate-application/code/templates/skills/sdk-content.js +2 -2
  104. package/dist/application-spec/generate-application/code/templates/spec-code/collect-spec-code.d.ts +58 -0
  105. package/dist/application-spec/generate-application/code/templates/spec-code/collect-spec-code.js +1 -0
  106. package/dist/application-spec/generate-application/code/templates/spec-code/index.d.ts +2 -0
  107. package/dist/application-spec/generate-application/code/templates/spec-code/index.js +1 -0
  108. package/dist/application-spec/generate-application/code/templates/spec-code/spec-code.d.ts +18 -0
  109. package/dist/application-spec/generate-application/code/templates/spec-code/spec-code.js +41 -0
  110. package/dist/application-spec/generate-application/code/templates/spec-messages/index.d.ts +1 -0
  111. package/dist/application-spec/generate-application/code/templates/spec-messages/index.js +1 -0
  112. package/dist/application-spec/generate-application/code/templates/spec-messages/spec-messages.d.ts +20 -0
  113. package/dist/application-spec/generate-application/code/templates/spec-messages/spec-messages.js +6 -0
  114. package/dist/application-spec/generate-application/code/templates/story/phx-module.d.ts +8 -2
  115. package/dist/application-spec/generate-application/code/templates/story/phx-module.js +15 -3
  116. package/dist/application-spec/generate-application/code/templates/story/wrapper.d.ts +3 -0
  117. package/dist/application-spec/generate-application/code/templates/story/wrapper.js +2 -1
  118. package/dist/application-spec/generate-application/code/templates/tsconfig.d.ts +5 -0
  119. package/dist/application-spec/generate-application/code/templates/tsconfig.js +1 -1
  120. package/dist/application-spec/generate-application/code/templates/webhook-index.d.ts +6 -1
  121. package/dist/application-spec/generate-application/code/templates/webhook-index.js +10 -1
  122. package/dist/application-spec/generate-application/code/templates/webhooks-config.js +2 -3
  123. package/dist/application-spec/generate-application/implementations/phx-application-types.d.ts +25 -0
  124. package/dist/application-spec/generate-application/implementations/phx-application-types.js +1 -0
  125. package/dist/application-spec/generate-application/implementations/phx-domain.d.ts +36 -0
  126. package/dist/application-spec/generate-application/implementations/phx-domain.js +1 -0
  127. package/dist/application-spec/generate-application/implementations/phx-model.d.ts +24 -0
  128. package/dist/application-spec/generate-application/implementations/phx-model.js +1 -0
  129. package/dist/application-spec/generate-application/implementations/phx.d.ts +22 -0
  130. package/dist/application-spec/generate-application/implementations/phx.js +1 -0
  131. package/dist/application-spec/generate-application/index.d.ts +4 -0
  132. package/dist/application-spec/generate-application/index.js +1 -1
  133. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-model-runtime.d.ts +6 -4
  134. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-model-runtime.js +1 -1
  135. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-runtime.d.ts +12 -4
  136. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-runtime.js +1 -1
  137. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-spec-runtime.d.ts +16 -0
  138. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-spec-runtime.js +1 -0
  139. package/dist/application-spec/generate-application/runtime/admin-runtime/states-runtime.d.ts +2 -2
  140. package/dist/application-spec/generate-application/runtime/application-runtime.d.ts +35 -8
  141. package/dist/application-spec/generate-application/runtime/application-runtime.js +1 -1
  142. package/dist/application-spec/generate-application/runtime/application-spec-slice.d.ts +71 -0
  143. package/dist/application-spec/generate-application/runtime/application-spec-slice.js +1 -0
  144. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-model-runtime.d.ts +7 -5
  145. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-model-runtime.js +1 -1
  146. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-runtime.d.ts +12 -5
  147. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-runtime.js +1 -1
  148. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-spec-runtime.d.ts +16 -0
  149. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-spec-runtime.js +1 -0
  150. package/dist/application-spec/generate-application/runtime/configurator-runtime/states-runtime.d.ts +2 -2
  151. package/dist/application-spec/generate-application/runtime/create-application.d.ts +57 -0
  152. package/dist/application-spec/generate-application/runtime/create-application.js +1 -0
  153. package/dist/application-spec/generate-application/runtime/factory-data/factory-data-runtime.d.ts +43 -0
  154. package/dist/application-spec/generate-application/runtime/factory-data/factory-data-runtime.js +1 -0
  155. package/dist/application-spec/generate-application/runtime/factory-data/index.d.ts +3 -0
  156. package/dist/application-spec/generate-application/runtime/factory-data/index.js +1 -0
  157. package/dist/application-spec/generate-application/runtime/factory-data/recipe-defaults.d.ts +13 -0
  158. package/dist/application-spec/generate-application/runtime/factory-data/recipe-defaults.js +1 -0
  159. package/dist/application-spec/generate-application/runtime/factory-data/static-violations.d.ts +16 -0
  160. package/dist/application-spec/generate-application/runtime/factory-data/static-violations.js +1 -0
  161. package/dist/application-spec/generate-application/runtime/index.d.ts +8 -0
  162. package/dist/application-spec/generate-application/runtime/index.js +1 -0
  163. package/dist/application-spec/generate-application/runtime/migrations/index.d.ts +1 -0
  164. package/dist/application-spec/generate-application/runtime/migrations/index.js +1 -0
  165. package/dist/application-spec/generate-application/runtime/migrations/migration-runtime.d.ts +53 -0
  166. package/dist/application-spec/generate-application/runtime/migrations/migration-runtime.js +1 -0
  167. package/dist/application-spec/generate-application/runtime/sections/assistant-runtime.d.ts +2 -2
  168. package/dist/application-spec/generate-application/runtime/sections/build-sdk-optic.d.ts +2 -1
  169. package/dist/application-spec/generate-application/runtime/sections/build-sdk-optic.js +1 -1
  170. package/dist/application-spec/generate-application/runtime/sections/build-user-bound-optic.d.ts +2 -1
  171. package/dist/application-spec/generate-application/runtime/sections/build-user-bound-optic.js +1 -1
  172. package/dist/application-spec/generate-application/runtime/sections/component-optic.d.ts +3 -0
  173. package/dist/application-spec/generate-application/runtime/sections/component-optic.js +1 -1
  174. package/dist/application-spec/generate-application/runtime/sections/data-runtime.d.ts +4 -4
  175. package/dist/application-spec/generate-application/runtime/sections/expression-runtime.d.ts +15 -6
  176. package/dist/application-spec/generate-application/runtime/sections/expression-runtime.js +1 -1
  177. package/dist/application-spec/generate-application/runtime/sections/generate-memo-eval-plan-runtime.d.ts +9 -5
  178. package/dist/application-spec/generate-application/runtime/sections/generate-memo-eval-plan-runtime.js +1 -1
  179. package/dist/application-spec/generate-application/runtime/sections/reflection-constants-runtime.d.ts +3 -1
  180. package/dist/application-spec/generate-application/runtime/sections/reflection-constants-runtime.js +1 -1
  181. package/dist/application-spec/generate-application/runtime/sections/theme-runtime.d.ts +2 -2
  182. package/dist/application-spec/generate-application/runtime/sections/transformers-runtime.d.ts +3 -3
  183. package/dist/application-spec/generate-application/runtime/spec-code/index.d.ts +2 -0
  184. package/dist/application-spec/generate-application/runtime/spec-code/index.js +1 -0
  185. package/dist/application-spec/generate-application/runtime/spec-code/resolve-spec-code.d.ts +18 -0
  186. package/dist/application-spec/generate-application/runtime/spec-code/resolve-spec-code.js +1 -0
  187. package/dist/application-spec/generate-application/runtime/spec-code/spec-code-module.d.ts +17 -0
  188. package/dist/application-spec/generate-application/runtime/spec-code/spec-code-module.js +1 -0
  189. package/dist/application-spec/generate-application/runtime/spec-runtime.d.ts +21 -0
  190. package/dist/application-spec/generate-application/runtime/spec-runtime.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 +28 -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 +103 -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/parse-application-files/categorize/categorize-files.d.ts +34 -0
  204. package/dist/application-spec/parse-application-files/categorize/categorize-files.js +1 -1
  205. package/dist/application-spec/parse-application-files/categorize/earlier-generated-editor-configs.d.ts +16 -0
  206. package/dist/application-spec/parse-application-files/categorize/earlier-generated-editor-configs.js +9 -0
  207. package/dist/application-spec/parse-application-files/categorize/earlier-generated-tsconfig.d.ts +9 -0
  208. package/dist/application-spec/parse-application-files/categorize/earlier-generated-tsconfig.js +1 -0
  209. package/dist/application-spec/parse-application-files/raw-spec/parse-yaml-spec-file.js +1 -1
  210. package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.d.ts +33 -3
  211. package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.js +1 -1
  212. package/dist/application-spec/parse-application-files/spec/application/admin-spec/admin-spec.js +1 -1
  213. package/dist/application-spec/parse-application-files/spec/application/configurator-spec/configurator-spec.js +1 -1
  214. package/dist/application-spec/parse-application-files/spec/common/index.d.ts +1 -0
  215. package/dist/application-spec/parse-application-files/spec/common/index.js +1 -1
  216. package/dist/application-spec/parse-application-files/spec/common/value-type/format-value-type.d.ts +3 -0
  217. package/dist/application-spec/parse-application-files/spec/common/value-type/format-value-type.js +1 -0
  218. package/dist/application-spec/parse-application-files/spec/common/value-type/index.d.ts +5 -0
  219. package/dist/application-spec/parse-application-files/spec/common/value-type/index.js +1 -0
  220. package/dist/application-spec/parse-application-files/spec/common/value-type/intersect-value-types.d.ts +6 -0
  221. package/dist/application-spec/parse-application-files/spec/common/value-type/intersect-value-types.js +1 -0
  222. package/dist/application-spec/parse-application-files/spec/common/value-type/is-assignable-value-type.d.ts +8 -0
  223. package/dist/application-spec/parse-application-files/spec/common/value-type/is-assignable-value-type.js +1 -0
  224. package/dist/application-spec/parse-application-files/spec/common/value-type/value-type-at.d.ts +10 -0
  225. package/dist/application-spec/parse-application-files/spec/common/value-type/value-type-at.js +1 -0
  226. package/dist/application-spec/parse-application-files/spec/common/value-type/value-type.d.ts +98 -0
  227. package/dist/application-spec/parse-application-files/spec/common/value-type/value-type.js +1 -0
  228. package/dist/application-spec/parse-application-files/spec/components/bindings/component-path.d.ts +13 -0
  229. package/dist/application-spec/parse-application-files/spec/components/bindings/component-path.js +1 -0
  230. package/dist/application-spec/parse-application-files/spec/components/bindings/index.d.ts +2 -0
  231. package/dist/application-spec/parse-application-files/spec/components/bindings/index.js +1 -1
  232. package/dist/application-spec/parse-application-files/spec/components/bindings/read-binding-type.d.ts +24 -0
  233. package/dist/application-spec/parse-application-files/spec/components/bindings/read-binding-type.js +1 -0
  234. package/dist/application-spec/parse-application-files/spec/components/component-state-type.d.ts +8 -0
  235. package/dist/application-spec/parse-application-files/spec/components/component-state-type.js +1 -0
  236. package/dist/application-spec/parse-application-files/spec/components/filter-criterion-types.d.ts +13 -0
  237. package/dist/application-spec/parse-application-files/spec/components/filter-criterion-types.js +1 -0
  238. package/dist/application-spec/parse-application-files/spec/components/static-entity-id.d.ts +14 -0
  239. package/dist/application-spec/parse-application-files/spec/components/static-entity-id.js +1 -0
  240. package/dist/application-spec/parse-application-files/spec/components/user-component-entry.d.ts +6 -0
  241. package/dist/application-spec/parse-application-files/spec/components/user-component-entry.js +1 -1
  242. package/dist/application-spec/parse-application-files/spec/components/user-components.d.ts +7 -4
  243. package/dist/application-spec/parse-application-files/spec/components/user-components.js +1 -1
  244. package/dist/application-spec/parse-application-files/spec/expressions/expression-read-types.d.ts +24 -0
  245. package/dist/application-spec/parse-application-files/spec/expressions/expression-read-types.js +1 -0
  246. package/dist/application-spec/parse-application-files/spec/expressions/expression-read.d.ts +13 -0
  247. package/dist/application-spec/parse-application-files/spec/expressions/expression-read.js +1 -0
  248. package/dist/application-spec/parse-application-files/spec/expressions/expression-value-type.d.ts +9 -0
  249. package/dist/application-spec/parse-application-files/spec/expressions/expression-value-type.js +1 -0
  250. package/dist/application-spec/parse-application-files/spec/expressions/expression.d.ts +1 -28
  251. package/dist/application-spec/parse-application-files/spec/expressions/expression.js +1 -1
  252. package/dist/application-spec/parse-application-files/spec/expressions/expressions.d.ts +7 -3
  253. package/dist/application-spec/parse-application-files/spec/expressions/expressions.js +1 -1
  254. package/dist/application-spec/parse-application-files/spec/expressions/index.d.ts +0 -1
  255. package/dist/application-spec/parse-application-files/spec/expressions/index.js +1 -1
  256. package/dist/application-spec/parse-application-files/spec/expressions/user-expressions.d.ts +3 -1
  257. package/dist/application-spec/parse-application-files/spec/expressions/user-expressions.js +1 -1
  258. package/dist/application-spec/parse-application-files/spec/invariants/invariants.d.ts +2 -2
  259. package/dist/application-spec/parse-application-files/spec/invariants/invariants.js +1 -1
  260. package/dist/application-spec/parse-application-files/spec/model/index.d.ts +1 -0
  261. package/dist/application-spec/parse-application-files/spec/model/index.js +1 -1
  262. package/dist/application-spec/parse-application-files/spec/model/model-type-resolution.d.ts +10 -0
  263. package/dist/application-spec/parse-application-files/spec/model/model-type-resolution.js +1 -0
  264. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-form-elements.d.ts +9 -0
  265. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-form-elements.js +1 -0
  266. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-schema.d.ts +4 -1
  267. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-schema.js +1 -1
  268. package/dist/client/phoenix-api.d.ts +367 -0
  269. package/dist/configurator/ConfiguratorApp.js +1 -1
  270. package/dist/configurator/app-loader-content/DefaultAppLoaderContent.d.ts +2 -0
  271. package/dist/configurator/app-loader-content/DefaultAppLoaderContent.js +5 -0
  272. package/dist/configurator/app-loader-content/index.d.ts +1 -0
  273. package/dist/configurator/app-loader-content/index.js +1 -0
  274. package/dist/configurator/application-runtime/extensions/selectors.js +1 -1
  275. package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.d.ts +39 -3
  276. package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.js +1 -1
  277. package/dist/configurator/application-runtime/extensions/systems/assistant-system/graphql-tools.d.ts +39 -7
  278. package/dist/configurator/application-runtime/extensions/systems/assistant-system/graphql-tools.js +1 -1
  279. package/dist/configurator/application-runtime/extensions/systems/camera-control-system/camera-control-system.d.ts +1 -1
  280. package/dist/configurator/application-runtime/extensions/systems/canvas-system.d.ts +8 -0
  281. package/dist/configurator/application-runtime/extensions/systems/canvas-system.js +1 -1
  282. package/dist/configurator/application-runtime/extensions/systems/export-system/export-busy-error.d.ts +8 -0
  283. package/dist/configurator/application-runtime/extensions/systems/export-system/export-busy-error.js +1 -0
  284. package/dist/configurator/application-runtime/extensions/systems/export-system/export-system.d.ts +23 -1
  285. package/dist/configurator/application-runtime/extensions/systems/export-system/export-system.js +1 -1
  286. package/dist/configurator/application-runtime/extensions/systems/graphql-system/close-unclosed-braces.d.ts +12 -0
  287. package/dist/configurator/application-runtime/extensions/systems/graphql-system/close-unclosed-braces.js +1 -0
  288. package/dist/configurator/application-runtime/extensions/systems/graphql-system/entity-meta.d.ts +14 -1
  289. package/dist/configurator/application-runtime/extensions/systems/graphql-system/entity-meta.js +1 -1
  290. package/dist/configurator/application-runtime/extensions/systems/graphql-system/graphql-system.d.ts +52 -5
  291. package/dist/configurator/application-runtime/extensions/systems/graphql-system/graphql-system.js +1 -1
  292. package/dist/configurator/application-runtime/extensions/systems/image-rendering-system/image-rendering-system.d.ts +14 -5
  293. package/dist/configurator/application-runtime/extensions/systems/image-rendering-system/image-rendering-system.js +1 -1
  294. package/dist/configurator/application-runtime/extensions/systems/index.d.ts +1 -1
  295. package/dist/configurator/common/configurator-interface.d.ts +141 -5
  296. package/dist/configurator/common/configurator-interface.js +1 -1
  297. package/dist/configurator/common/index.d.ts +3 -1
  298. package/dist/configurator/common/index.js +1 -1
  299. package/dist/configurator/common/use-reset-camera.d.ts +6 -0
  300. package/dist/configurator/common/use-reset-camera.js +1 -0
  301. package/dist/configurator/configuration-not-found-page/BuiltInConfigurationNotFoundPage.d.ts +5 -0
  302. package/dist/configurator/configuration-not-found-page/BuiltInConfigurationNotFoundPage.js +1 -0
  303. package/dist/configurator/configuration-not-found-page/ConfigurationNotFoundPageElement.d.ts +6 -0
  304. package/dist/configurator/configuration-not-found-page/ConfigurationNotFoundPageElement.js +1 -0
  305. package/dist/configurator/configuration-not-found-page/index.d.ts +2 -0
  306. package/dist/configurator/configuration-not-found-page/index.js +1 -0
  307. package/dist/configurator/configurator-context/ConfiguratorResponsiveProvider.d.ts +12 -0
  308. package/dist/configurator/configurator-context/ConfiguratorResponsiveProvider.js +1 -0
  309. package/dist/configurator/configurator-context/PresetSelectionProvider.js +1 -1
  310. package/dist/configurator/configurator-context/PreviewContextProvider.js +1 -1
  311. package/dist/configurator/configurator-context/ViewportStateSync.d.ts +2 -2
  312. package/dist/configurator/configurator-context/ViewportStateSync.js +1 -1
  313. package/dist/configurator/configurator-context/configurator-hooks/layout-hooks.d.ts +10 -0
  314. package/dist/configurator/configurator-context/configurator-routing/ConfiguratorRoutingProvider.js +1 -1
  315. package/dist/configurator/configurator-error-page/BuiltInConfiguratorErrorPage.d.ts +6 -0
  316. package/dist/configurator/configurator-error-page/BuiltInConfiguratorErrorPage.js +1 -0
  317. package/dist/configurator/configurator-error-page/ConfiguratorErrorPageElement.d.ts +7 -0
  318. package/dist/configurator/configurator-error-page/ConfiguratorErrorPageElement.js +1 -0
  319. package/dist/configurator/configurator-error-page/index.d.ts +2 -0
  320. package/dist/configurator/configurator-error-page/index.js +1 -0
  321. package/dist/configurator/index.d.ts +10 -0
  322. package/dist/configurator/index.js +1 -1
  323. package/dist/configurator/pages/ConfiguratorErrorPage.d.ts +0 -1
  324. package/dist/configurator/pages/ConfiguratorErrorPage.js +1 -1
  325. package/dist/configurator/pages/ConfiguratorNotFoundPage.js +1 -1
  326. package/dist/configurator/pages/DefaultConfiguratorLayout.d.ts +3 -2
  327. package/dist/configurator/pages/DefaultNotAuthenticatedPage.d.ts +8 -0
  328. package/dist/configurator/pages/DefaultNotAuthenticatedPage.js +1 -0
  329. package/dist/configurator/pages/DefaultPresetUi.d.ts +7 -0
  330. package/dist/configurator/pages/DefaultPresetUi.js +1 -0
  331. package/dist/configurator/pages/LoadedConfiguratorDetailPage.js +1 -1
  332. package/dist/configurator/pages/NewConfiguratorPage.js +1 -1
  333. package/dist/configurator/pages/PresetCreationErrorPage.d.ts +1 -1
  334. package/dist/configurator/pages/PresetCreationErrorPage.js +1 -1
  335. package/dist/configurator/pages/RootPage.js +1 -1
  336. package/dist/configurator/pages/configurator-layout-name.d.ts +2 -0
  337. package/dist/configurator/pages/configurator-layout-name.js +1 -0
  338. package/dist/configurator/pages/default-configurator-elements.d.ts +13 -0
  339. package/dist/configurator/pages/default-configurator-elements.js +1 -0
  340. package/dist/configurator/pages/default-elements/built-in-entity-element.d.ts +10 -0
  341. package/dist/configurator/pages/default-elements/built-in-entity-element.js +1 -0
  342. package/dist/configurator/pages/default-elements/built-in-layout-element.d.ts +10 -0
  343. package/dist/configurator/pages/default-elements/built-in-layout-element.js +1 -0
  344. package/dist/configurator/pages/default-elements/domain-control-elements.d.ts +6 -0
  345. package/dist/configurator/pages/default-elements/domain-control-elements.js +1 -0
  346. package/dist/configurator/pages/default-elements/index.d.ts +5 -0
  347. package/dist/configurator/pages/default-elements/index.js +1 -0
  348. package/dist/configurator/pages/default-elements/recipe-panel-elements.d.ts +3 -0
  349. package/dist/configurator/pages/default-elements/recipe-panel-elements.js +1 -0
  350. package/dist/configurator/pages/default-elements/scene-controls/BuiltInAddEntityFab.d.ts +3 -0
  351. package/dist/configurator/pages/default-elements/scene-controls/BuiltInAddEntityFab.js +1 -0
  352. package/dist/configurator/pages/default-elements/scene-controls/BuiltInDeleteEntityFab.d.ts +3 -0
  353. package/dist/configurator/pages/default-elements/scene-controls/BuiltInDeleteEntityFab.js +1 -0
  354. package/dist/configurator/pages/default-elements/scene-controls/BuiltInNavigationHotspot.d.ts +3 -0
  355. package/dist/configurator/pages/default-elements/scene-controls/BuiltInNavigationHotspot.js +1 -0
  356. package/dist/configurator/pages/default-elements/scene-controls/BuiltInSelectionHotspot.d.ts +3 -0
  357. package/dist/configurator/pages/default-elements/scene-controls/BuiltInSelectionHotspot.js +1 -0
  358. package/dist/configurator/pages/default-elements/scene-controls/BuiltInToolMenuFab.d.ts +3 -0
  359. package/dist/configurator/pages/default-elements/scene-controls/BuiltInToolMenuFab.js +1 -0
  360. package/dist/configurator/pages/default-elements/scene-controls/index.d.ts +6 -0
  361. package/dist/configurator/pages/default-elements/scene-controls/index.js +1 -0
  362. package/dist/configurator/pages/default-elements/scene-controls/scene-control-elements.d.ts +6 -0
  363. package/dist/configurator/pages/default-elements/scene-controls/scene-control-elements.js +1 -0
  364. package/dist/configurator/pages/default-elements/sidebar-footer-elements.d.ts +7 -0
  365. package/dist/configurator/pages/default-elements/sidebar-footer-elements.js +1 -0
  366. package/dist/configurator/pages/default-elements/toolbar-elements.d.ts +7 -0
  367. package/dist/configurator/pages/default-elements/toolbar-elements.js +1 -0
  368. package/dist/configurator/pages/error-boundaries/GeneralErrorBoundary.js +1 -1
  369. package/dist/configurator/pages/error-boundaries/LoadingErrorBoundary.js +1 -1
  370. package/dist/configurator/pages/index.d.ts +1 -0
  371. package/dist/configurator/pages/index.js +1 -1
  372. package/dist/configurator/preset-screen-elements/PresetDeviceNotSupportedElement.d.ts +4 -0
  373. package/dist/configurator/preset-screen-elements/PresetDeviceNotSupportedElement.js +1 -0
  374. package/dist/configurator/preset-screen-elements/PresetPageActionElement.d.ts +4 -0
  375. package/dist/configurator/preset-screen-elements/PresetPageActionElement.js +1 -0
  376. package/dist/configurator/preset-screen-elements/index.d.ts +2 -0
  377. package/dist/configurator/preset-screen-elements/index.js +1 -0
  378. package/dist/configurator/scene/index.d.ts +1 -0
  379. package/dist/configurator/scene/index.js +1 -1
  380. package/dist/configurator/scene/scene-controls/DeleteEntityFab.js +1 -1
  381. package/dist/configurator/scene/scene-controls/SelectionHotspot.js +1 -1
  382. package/dist/configurator/scene/scene-controls/ToolFab.d.ts +4 -0
  383. package/dist/configurator/scene/scene-controls/ToolFab.js +1 -1
  384. package/dist/configurator/scene/scene-controls/use-scene-control-tooltip.d.ts +2 -1
  385. package/dist/configurator/scene/scene-entity/scene-controls/SceneEntityHtmlHotspots.js +1 -1
  386. package/dist/configurator/toolbar-actions/index.d.ts +3 -0
  387. package/dist/configurator/toolbar-actions/index.js +1 -0
  388. package/dist/configurator/toolbar-actions/use-assistant-panel.d.ts +18 -0
  389. package/dist/configurator/toolbar-actions/use-assistant-panel.js +1 -0
  390. package/dist/configurator/toolbar-actions/use-measurements.d.ts +35 -0
  391. package/dist/configurator/toolbar-actions/use-measurements.js +1 -0
  392. package/dist/configurator/toolbar-actions/use-toolbar-actions.d.ts +100 -0
  393. package/dist/configurator/toolbar-actions/use-toolbar-actions.js +1 -0
  394. package/dist/configurator/toolbar-actions/use-toolbar-help.d.ts +7 -0
  395. package/dist/configurator/toolbar-actions/use-toolbar-help.js +1 -0
  396. package/dist/configurator/ui/assistant/AssistantChips.d.ts +10 -3
  397. package/dist/configurator/ui/assistant/AssistantChips.js +1 -1
  398. package/dist/configurator/ui/assistant/AssistantMarkdown.d.ts +7 -2
  399. package/dist/configurator/ui/assistant/AssistantMarkdown.js +1 -1
  400. package/dist/configurator/ui/assistant/AssistantPanel.js +1 -1
  401. package/dist/configurator/ui/assistant/AssistantThread.js +1 -1
  402. package/dist/configurator/ui/assistant/alert/AssistantAlert.d.ts +8 -0
  403. package/dist/configurator/ui/assistant/alert/AssistantAlert.js +1 -0
  404. package/dist/configurator/ui/assistant/alert/index.d.ts +2 -0
  405. package/dist/configurator/ui/assistant/alert/index.js +1 -0
  406. package/dist/configurator/ui/assistant/alert/remark-alerts.d.ts +10 -0
  407. package/dist/configurator/ui/assistant/alert/remark-alerts.js +1 -0
  408. package/dist/configurator/ui/assistant/entity-link/AssistantEntityLink.d.ts +1 -1
  409. package/dist/configurator/ui/assistant/entity-link/AssistantEntityLink.js +1 -1
  410. package/dist/configurator/ui/assistant/entity-link/entity-link-state.d.ts +1 -1
  411. package/dist/configurator/ui/assistant/entity-link/index.d.ts +0 -1
  412. package/dist/configurator/ui/assistant/entity-link/index.js +1 -1
  413. package/dist/configurator/ui/assistant/form/AssistantForm.d.ts +19 -0
  414. package/dist/configurator/ui/assistant/form/AssistantForm.js +1 -0
  415. package/dist/configurator/ui/assistant/form/compose-answer.d.ts +11 -0
  416. package/dist/configurator/ui/assistant/form/compose-answer.js +1 -0
  417. package/dist/configurator/ui/assistant/form/describe-build-errors.d.ts +6 -0
  418. package/dist/configurator/ui/assistant/form/describe-build-errors.js +1 -0
  419. package/dist/configurator/ui/assistant/form/form-model.d.ts +37 -0
  420. package/dist/configurator/ui/assistant/form/form-model.js +1 -0
  421. package/dist/configurator/ui/assistant/form/form-state.d.ts +4 -0
  422. package/dist/configurator/ui/assistant/form/form-state.js +1 -0
  423. package/dist/configurator/ui/assistant/form/index.d.ts +4 -0
  424. package/dist/configurator/ui/assistant/form/index.js +1 -0
  425. package/dist/configurator/ui/assistant/form/parse-form-block.d.ts +10 -0
  426. package/dist/configurator/ui/assistant/form/parse-form-block.js +1 -0
  427. package/dist/configurator/ui/assistant/form/read-form-attributes.d.ts +13 -0
  428. package/dist/configurator/ui/assistant/form/read-form-attributes.js +1 -0
  429. package/dist/configurator/ui/assistant/form/registry/built-in-form-elements.d.ts +3 -0
  430. package/dist/configurator/ui/assistant/form/registry/built-in-form-elements.js +1 -0
  431. package/dist/configurator/ui/assistant/form/registry/create-assistant-form-element.d.ts +31 -0
  432. package/dist/configurator/ui/assistant/form/registry/create-assistant-form-element.js +1 -0
  433. package/dist/configurator/ui/assistant/form/registry/form-element-definition.d.ts +16 -0
  434. package/dist/configurator/ui/assistant/form/registry/form-element-definition.js +1 -0
  435. package/dist/configurator/ui/assistant/form/registry/form-registry.d.ts +31 -0
  436. package/dist/configurator/ui/assistant/form/registry/form-registry.js +1 -0
  437. package/dist/configurator/ui/assistant/form/registry/index.d.ts +6 -0
  438. package/dist/configurator/ui/assistant/form/registry/index.js +1 -0
  439. package/dist/configurator/ui/assistant/form/registry/use-form-registry.d.ts +8 -0
  440. package/dist/configurator/ui/assistant/form/registry/use-form-registry.js +1 -0
  441. package/dist/configurator/ui/assistant/form-elements/FormCard.d.ts +9 -0
  442. package/dist/configurator/ui/assistant/form-elements/FormCard.js +1 -0
  443. package/dist/configurator/ui/assistant/form-elements/FormConfirm.d.ts +9 -0
  444. package/dist/configurator/ui/assistant/form-elements/FormConfirm.js +1 -0
  445. package/dist/configurator/ui/assistant/form-elements/FormGroup.d.ts +6 -0
  446. package/dist/configurator/ui/assistant/form-elements/FormGroup.js +1 -0
  447. package/dist/configurator/ui/assistant/form-elements/FormNumber.d.ts +9 -0
  448. package/dist/configurator/ui/assistant/form-elements/FormNumber.js +1 -0
  449. package/dist/configurator/ui/assistant/form-elements/FormOptions.d.ts +6 -0
  450. package/dist/configurator/ui/assistant/form-elements/FormOptions.js +1 -0
  451. package/dist/configurator/ui/assistant/form-elements/FormReply.d.ts +6 -0
  452. package/dist/configurator/ui/assistant/form-elements/FormReply.js +1 -0
  453. package/dist/configurator/ui/assistant/form-elements/FormSwatch.d.ts +10 -0
  454. package/dist/configurator/ui/assistant/form-elements/FormSwatch.js +1 -0
  455. package/dist/configurator/ui/assistant/form-elements/FormText.d.ts +6 -0
  456. package/dist/configurator/ui/assistant/form-elements/FormText.js +1 -0
  457. package/dist/configurator/ui/assistant/form-elements/clamp-form-number.d.ts +10 -0
  458. package/dist/configurator/ui/assistant/form-elements/clamp-form-number.js +1 -0
  459. package/dist/configurator/ui/assistant/form-elements/form-element-props.d.ts +38 -0
  460. package/dist/configurator/ui/assistant/form-elements/form-element-props.js +1 -0
  461. package/dist/configurator/ui/assistant/form-elements/index.d.ts +9 -0
  462. package/dist/configurator/ui/assistant/form-elements/index.js +1 -0
  463. package/dist/configurator/ui/assistant/index.d.ts +3 -0
  464. package/dist/configurator/ui/assistant/index.js +1 -1
  465. package/dist/configurator/ui/assistant/reference-link/AssistantReferenceLink.d.ts +16 -0
  466. package/dist/configurator/ui/assistant/reference-link/AssistantReferenceLink.js +1 -0
  467. package/dist/configurator/ui/assistant/reference-link/assistant-link-target.d.ts +34 -0
  468. package/dist/configurator/ui/assistant/reference-link/assistant-link-target.js +1 -0
  469. package/dist/configurator/ui/assistant/reference-link/assistant-reference-details.d.ts +25 -0
  470. package/dist/configurator/ui/assistant/reference-link/assistant-reference-details.js +1 -0
  471. package/dist/configurator/ui/assistant/reference-link/index.d.ts +3 -0
  472. package/dist/configurator/ui/assistant/reference-link/index.js +1 -0
  473. package/dist/configurator/ui/assistant/reference-link/reference-styles.d.ts +12 -0
  474. package/dist/configurator/ui/assistant/reference-link/reference-styles.js +1 -0
  475. package/dist/configurator/ui/assistant/reference-link/use-assistant-reference-details.d.ts +8 -0
  476. package/dist/configurator/ui/assistant/reference-link/use-assistant-reference-details.js +1 -0
  477. package/dist/configurator/ui/assistant/resource-image/AssistantImage.d.ts +9 -0
  478. package/dist/configurator/ui/assistant/resource-image/AssistantImage.js +1 -0
  479. package/dist/configurator/ui/assistant/resource-image/index.d.ts +3 -0
  480. package/dist/configurator/ui/assistant/resource-image/index.js +1 -0
  481. package/dist/configurator/ui/assistant/resource-image/resource-path.d.ts +2 -0
  482. package/dist/configurator/ui/assistant/resource-image/resource-path.js +1 -0
  483. package/dist/configurator/ui/assistant/resource-image/use-resource-image-url.d.ts +5 -0
  484. package/dist/configurator/ui/assistant/resource-image/use-resource-image-url.js +1 -0
  485. package/dist/configurator/ui/assistant/screenshots/ScreenshotThumbnails.d.ts +9 -0
  486. package/dist/configurator/ui/assistant/screenshots/ScreenshotThumbnails.js +1 -0
  487. package/dist/configurator/ui/assistant/screenshots/index.d.ts +2 -0
  488. package/dist/configurator/ui/assistant/screenshots/index.js +1 -0
  489. package/dist/configurator/ui/assistant/screenshots/tool-screenshots.d.ts +14 -0
  490. package/dist/configurator/ui/assistant/screenshots/tool-screenshots.js +1 -0
  491. package/dist/configurator/ui/assistant/streamed-markdown.d.ts +4 -2
  492. package/dist/configurator/ui/assistant/streamed-markdown.js +2 -2
  493. package/dist/configurator/ui/assistant/thread-items.d.ts +8 -1
  494. package/dist/configurator/ui/assistant/thread-items.js +1 -1
  495. package/dist/configurator/ui/assistant/tool-chip.js +1 -1
  496. package/dist/configurator/ui/assistant/use-follow-stream.js +1 -1
  497. package/dist/configurator/ui/configurator-layout/slots/canvas/LayoutCanvas.js +1 -1
  498. package/dist/configurator/ui/configurator-layout/slots/canvas/SharedCanvas.js +1 -1
  499. package/dist/configurator/ui/configurator-layout/slots/mobile-tabs/use-mobile-tabs.d.ts +1 -1
  500. package/dist/configurator/ui/configurator-layout/slots/mobile-tabs/use-mobile-tabs.js +1 -1
  501. package/dist/configurator/ui/configurator-layout/slots/services/InvariantViolations.d.ts +4 -4
  502. package/dist/configurator/ui/configurator-layout/slots/services/InvariantViolations.js +1 -1
  503. package/dist/configurator/ui/configurator-layout/slots/services/Services.js +1 -1
  504. package/dist/configurator/ui/configurator-layout/slots/services/SideEffects.d.ts +3 -6
  505. package/dist/configurator/ui/configurator-layout/slots/services/SideEffects.js +1 -1
  506. package/dist/configurator/ui/configurator-layout/slots/sidebar-tabs/use-sidebar-tabs.d.ts +1 -1
  507. package/dist/configurator/ui/configurator-layout/slots/sidebar-tabs/use-sidebar-tabs.js +1 -1
  508. package/dist/configurator/ui/configurator-layout/universal/ExportingOverlay.d.ts +5 -0
  509. package/dist/configurator/ui/configurator-layout/universal/ExportingOverlay.js +1 -0
  510. package/dist/configurator/ui/configurator-layout/universal/Layout.js +1 -1
  511. package/dist/configurator/ui/configurator-layout/universal/use-layout-scene-safe-area.d.ts +1 -1
  512. package/dist/configurator/ui/configurator-layout/universal/use-layout-scene-safe-area.js +1 -1
  513. package/dist/configurator/ui/configurator-layout/universal/use-scene-safe-area.d.ts +1 -1
  514. package/dist/configurator/ui/index.d.ts +1 -0
  515. package/dist/configurator/ui/index.js +1 -1
  516. package/dist/configurator/ui/legacy/layout/common/CommonFinalizeButton.js +1 -1
  517. package/dist/configurator/ui/legacy/layout/common/CommonSummaryButton.js +1 -1
  518. package/dist/configurator/ui/legacy/layout/common/CommonTopNavigation.js +1 -1
  519. package/dist/configurator/ui/legacy/layout/common/ReactiveInterfaceLayout.js +1 -1
  520. package/dist/configurator/ui/legacy/layout/common/ResponsiveFinalizeButton.js +1 -1
  521. package/dist/configurator/ui/legacy/layout/common/SummaryReadLayout.js +1 -1
  522. package/dist/configurator/ui/legacy/layout/simple/SimpleOwnerLayout.js +1 -1
  523. package/dist/configurator/ui/legacy/layout/simple/SimpleTopNavigation.js +1 -1
  524. package/dist/configurator/ui/legacy/layout/steps/StepsNavigation.js +1 -1
  525. package/dist/configurator/ui/legacy/layout/steps/StepsOwnerLayout.js +1 -1
  526. package/dist/configurator/ui/legacy/layout/steps/StepsSidebarFooter.js +1 -1
  527. package/dist/configurator/ui/legacy/layout/tabs/TabsOwnerLayout.js +1 -1
  528. package/dist/configurator/ui/legacy/toolbar/common/DimensionUnitSelector.js +1 -1
  529. package/dist/configurator/ui/legacy/toolbar/common/MeasurementsIndicatorsSwitch.js +1 -1
  530. package/dist/configurator/ui/legacy/toolbar/desktop-toolbar/FullscreenToolbarAction.js +1 -1
  531. package/dist/configurator/ui/legacy/toolbar/desktop-toolbar/HelpToolbarAction.js +1 -1
  532. package/dist/configurator/ui/legacy/toolbar/desktop-toolbar/MeasurementsToolbarAction.js +1 -1
  533. package/dist/configurator/ui/legacy/toolbar/desktop-toolbar/UndoRedoToolbarGroup.js +1 -1
  534. package/dist/configurator/ui/phoenix-layout-elements/AppLoader.d.ts +0 -7
  535. package/dist/configurator/ui/phoenix-layout-elements/AppLoader.js +2 -6
  536. package/dist/configurator/ui/phoenix-layout-elements/AssistantToolbarButton.js +1 -1
  537. package/dist/configurator/ui/phoenix-layout-elements/EnterFullscreenToolbarButton.js +1 -1
  538. package/dist/configurator/ui/phoenix-layout-elements/ExitFullscreenToolbarButton.js +1 -1
  539. package/dist/configurator/ui/phoenix-layout-elements/HelpToolbarButton.js +1 -1
  540. package/dist/configurator/ui/phoenix-layout-elements/LanguageMenuAction.js +1 -1
  541. package/dist/configurator/ui/phoenix-layout-elements/LanguageToolbarButton.js +1 -1
  542. package/dist/configurator/ui/phoenix-layout-elements/LayoutCustomOwnerUi.d.ts +1 -0
  543. package/dist/configurator/ui/phoenix-layout-elements/LayoutCustomOwnerUi.js +1 -1
  544. package/dist/configurator/ui/phoenix-layout-elements/LayoutCustomReadUi.d.ts +1 -0
  545. package/dist/configurator/ui/phoenix-layout-elements/LayoutCustomReadUi.js +1 -1
  546. package/dist/configurator/ui/phoenix-layout-elements/MeasurementsMenuAction.js +1 -1
  547. package/dist/configurator/ui/phoenix-layout-elements/MeasurementsToolbarButton.js +1 -1
  548. package/dist/configurator/ui/phoenix-layout-elements/RedoToolbarButton.js +1 -1
  549. package/dist/configurator/ui/phoenix-layout-elements/ShareToolbarButton.js +1 -1
  550. package/dist/configurator/ui/phoenix-layout-elements/UndoToolbarButton.js +1 -1
  551. package/dist/configurator/ui/phoenix-layout-elements/header/common/back-button/use-go-back-action.js +1 -1
  552. package/dist/configurator/ui/phoenix-layout-elements/header/common/horizontal-tabs/DesktopHorizontalTabs.js +1 -1
  553. package/dist/configurator/ui/phoenix-layout-elements/header/common/step-navigation/DesktopNavigationSteps.d.ts +1 -1
  554. package/dist/configurator/ui/phoenix-layout-elements/header/common/step-navigation/DesktopNavigationSteps.js +1 -1
  555. package/dist/configurator/ui/phoenix-layout-elements/header/common/step-navigation/StepsNavigation.d.ts +2 -8
  556. package/dist/configurator/ui/phoenix-layout-elements/header/common/step-navigation/StepsNavigation.js +1 -1
  557. package/dist/configurator/ui/phoenix-layout-elements/index.d.ts +8 -1
  558. package/dist/configurator/ui/phoenix-layout-elements/index.js +1 -1
  559. package/dist/configurator/ui/phoenix-layout-elements/measurements/DimensionUnitSelector.js +1 -1
  560. package/dist/configurator/ui/phoenix-layout-elements/measurements/MeasurementsIndicatorsSwitch.js +1 -1
  561. package/dist/configurator/ui/phoenix-layout-elements/share/ShareModal.js +1 -1
  562. package/dist/configurator/ui/phoenix-layout-elements/share/use-share.d.ts +7 -8
  563. package/dist/configurator/ui/phoenix-layout-elements/share/use-share.js +1 -1
  564. package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/basic/BasicFinalizeButton.js +1 -1
  565. package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/CustomFinalizeButton.js +1 -1
  566. package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/EnterSummaryButton.js +1 -1
  567. package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/FinalizeActions.js +1 -1
  568. package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/NextStepButton.js +1 -1
  569. package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/PreviousStepButton.js +1 -1
  570. package/dist/configurator/ui/preset-layout/CategoryPresetUi.js +1 -1
  571. package/dist/configurator/ui/preset-layout/PresetUi.d.ts +4 -2
  572. package/dist/configurator/ui/preset-layout/PresetUi.js +1 -1
  573. package/dist/configurator/ui/preset-layout/index.d.ts +6 -1
  574. package/dist/configurator/ui/preset-layout/index.js +1 -1
  575. package/dist/configurator/ui/save-message-snackbars/SaveMessageSnackbars.d.ts +10 -0
  576. package/dist/configurator/ui/save-message-snackbars/SaveMessageSnackbars.js +1 -0
  577. package/dist/configurator/ui/save-message-snackbars/index.d.ts +1 -0
  578. package/dist/configurator/ui/save-message-snackbars/index.js +1 -0
  579. package/dist/docs/complex-use-cases/focus-mode.mdx +2 -0
  580. package/dist/docs/controls/select-child-entity/cardselect.mdx +1 -0
  581. package/dist/docs/fabs/basic.mdx +6 -2
  582. package/dist/docs/index.json +105 -1
  583. package/dist/docs/integrations/webcomponent-api/basics.mdx +2 -0
  584. package/dist/docs/layouts/custom-layout.mdx +1906 -290
  585. package/dist/docs/layouts/gallery/ardent-compressor.mdx +3516 -0
  586. package/dist/docs/layouts/gallery/corvid-drone.mdx +3428 -0
  587. package/dist/docs/layouts/gallery/fenwick-window.mdx +2803 -0
  588. package/dist/docs/layouts/gallery/ferrule-bar-station.mdx +6567 -0
  589. package/dist/docs/layouts/gallery/kestrel-kettle.mdx +1858 -0
  590. package/dist/docs/layouts/gallery/nightjar-guitar.mdx +3698 -0
  591. package/dist/docs/layouts/gallery/sunfold-solar.mdx +2683 -0
  592. package/dist/docs/layouts/gallery/tidepool-aquascape.mdx +5203 -0
  593. package/dist/docs/layouts/sidebar/in-a-custom-layout.mdx +2 -2
  594. package/dist/docs/layouts/sidebar-footer/basic.mdx +32 -77
  595. package/dist/docs/other-features/client-side-rendering/declarative.mdx +1 -1
  596. package/dist/docs/other-features/error-5xx.mdx +1 -1
  597. package/dist/docs/other-features/navigation-hotspot.mdx +2 -0
  598. package/dist/docs/other-features/not-found-404.mdx +1 -1
  599. package/dist/docs/phoenix/application-states.mdx +9 -7
  600. package/dist/docs/phoenix/clerk-sign-in.mdx +1 -1
  601. package/dist/docs/phoenix/components.mdx +6 -0
  602. package/dist/docs/phoenix/configurator-interface.mdx +303 -23
  603. package/dist/docs/phoenix/deployments.mdx +2 -6
  604. package/dist/docs/phoenix/domain-model.mdx +2 -0
  605. package/dist/docs/phoenix/finalization.mdx +1 -1
  606. package/dist/docs/phoenix/getting-started.mdx +6 -3
  607. package/dist/docs/phoenix/layout-slots.mdx +116 -0
  608. package/dist/docs/phoenix/localization.mdx +10 -6
  609. package/dist/docs/phoenix/migration-guide.mdx +24 -11
  610. package/dist/docs/phoenix/phoenix-cli.mdx +72 -21
  611. package/dist/docs/phoenix/project-structure.mdx +70 -33
  612. package/dist/docs/phoenix/quickstart.mdx +7 -7
  613. package/dist/docs/phoenix/sdk-builder-workflow.mdx +8 -8
  614. package/dist/docs/phoenix/sessions-and-access.mdx +2 -0
  615. package/dist/docs/phoenix/testing.mdx +8 -0
  616. package/dist/docs/phoenix/webhooks.mdx +21 -4
  617. package/dist/docs/selection-and-highlight/hotspots.mdx +2 -0
  618. package/dist/domain/application/application-config.d.ts +3 -1
  619. package/dist/domain/application/assistant/assistant-manifest.d.ts +16 -1
  620. package/dist/domain/application/assistant/assistant-resolved-manifest.d.ts +8 -1
  621. package/dist/domain/application/assistant/build-assistant-manifest.js +1 -1
  622. package/dist/domain/application/assistant/form/assistant-form-element-name.d.ts +11 -0
  623. package/dist/domain/application/assistant/form/assistant-form-element-name.js +8 -0
  624. package/dist/domain/application/assistant/form/assistant-form-elements.d.ts +174 -0
  625. package/dist/domain/application/assistant/form/assistant-form-elements.js +1 -0
  626. package/dist/domain/application/assistant/form/assistant-resource-path.d.ts +10 -0
  627. package/dist/domain/application/assistant/form/assistant-resource-path.js +1 -0
  628. package/dist/domain/application/assistant/form/index.d.ts +4 -0
  629. package/dist/domain/application/assistant/form/index.js +1 -0
  630. package/dist/domain/application/assistant/generate-assistant-prompt.d.ts +3 -0
  631. package/dist/domain/application/assistant/generate-assistant-prompt.js +1 -1
  632. package/dist/domain/application/assistant/index.d.ts +2 -1
  633. package/dist/domain/application/assistant/index.js +1 -1
  634. package/dist/domain/application/assistant/session-setup/apply-assistant-setup.js +1 -1
  635. package/dist/domain/application/assistant/session-setup/assistant-setup-manifest.d.ts +2 -0
  636. package/dist/domain/application/assistant/session-setup/assistant-setup-manifest.js +1 -1
  637. package/dist/domain/application/assistant-recipe-groups.d.ts +3 -1
  638. package/dist/domain/application/configurator-builder.d.ts +10 -1
  639. package/dist/domain/application/configurator-builder.js +1 -1
  640. package/dist/domain/application/index.d.ts +1 -1
  641. package/dist/domain/application/index.js +1 -1
  642. package/dist/domain/application/standard-properties.js +1 -1
  643. package/dist/domain/application/web-component-property.d.ts +3 -1
  644. package/dist/domain/application/web-component-property.js +1 -1
  645. package/dist/domain/available-system-components.d.ts +1 -1
  646. package/dist/domain/available-system-components.js +1 -1
  647. package/dist/domain/core/model/model.d.ts +1 -1
  648. package/dist/domain/core/schema/extended-schema-validator.js +1 -1
  649. package/dist/domain/domain-builder.d.ts +9 -3
  650. package/dist/domain/domain-builder.js +1 -1
  651. package/dist/domain/domain-context/index.d.ts +1 -1
  652. package/dist/domain/domain-context/store-system/store-system.d.ts +5 -0
  653. package/dist/domain/domain-context/store-system/store-system.js +1 -1
  654. package/dist/domain/domain-context/undo-system.d.ts +28 -1
  655. package/dist/domain/domain-context/undo-system.js +1 -1
  656. package/dist/domain/domain-runtime/application.d.ts +39 -0
  657. package/dist/domain/domain-runtime/application.js +1 -0
  658. package/dist/domain/domain-runtime/domain-context/selectors.d.ts +1 -0
  659. package/dist/domain/domain-runtime/domain-context/selectors.js +1 -1
  660. package/dist/domain/domain-runtime/domain-runtime.d.ts +44 -4
  661. package/dist/domain/domain-runtime/domain-runtime.js +1 -1
  662. package/dist/domain/domain-runtime/environment/initialize.d.ts +1 -1
  663. package/dist/domain/domain-runtime/environment/types.d.ts +1 -1
  664. package/dist/domain/domain-runtime/errors/implementation-error-collector.d.ts +2 -0
  665. package/dist/domain/domain-runtime/errors/implementation-error-collector.js +1 -1
  666. package/dist/domain/domain-runtime/index.d.ts +2 -1
  667. package/dist/domain/domain-runtime/index.js +1 -1
  668. package/dist/domain/domain-runtime/spec/index.d.ts +1 -1
  669. package/dist/domain/domain-runtime/spec/index.js +1 -1
  670. package/dist/domain/domain-runtime/spec/initialize.js +1 -1
  671. package/dist/domain/domain-runtime/spec/module-checks/element-registration.d.ts +27 -12
  672. package/dist/domain/domain-runtime/spec/module-checks/element-registration.js +1 -1
  673. package/dist/domain/domain-runtime/spec/module-checks/find-assistant-form-element-errors.d.ts +12 -0
  674. package/dist/domain/domain-runtime/spec/module-checks/find-assistant-form-element-errors.js +1 -0
  675. package/dist/domain/domain-runtime/spec/module-checks/index.d.ts +3 -1
  676. package/dist/domain/domain-runtime/spec/module-checks/index.js +1 -1
  677. package/dist/domain/domain-runtime/spec/module-checks/merge-expression-modules.d.ts +23 -0
  678. package/dist/domain/domain-runtime/spec/module-checks/merge-expression-modules.js +1 -0
  679. package/dist/domain/domain-runtime/spec/module-checks/resolve-expression-modules.d.ts +3 -4
  680. package/dist/domain/domain-runtime/spec/module-checks/resolve-expression-modules.js +1 -1
  681. package/dist/domain/domain-runtime/spec/selectors.d.ts +3 -1
  682. package/dist/domain/domain-runtime/spec/selectors.js +1 -1
  683. package/dist/domain/domain-runtime/spec/types.d.ts +2 -0
  684. package/dist/domain/domain-runtime/web-component/actions.d.ts +3 -0
  685. package/dist/domain/domain-runtime/web-component/actions.js +1 -1
  686. package/dist/domain/domain-runtime/web-component/initialize.d.ts +1 -0
  687. package/dist/domain/domain-runtime/web-component/initialize.js +1 -1
  688. package/dist/domain/domain-runtime/web-component/selectors.d.ts +2 -0
  689. package/dist/domain/domain-runtime/web-component/selectors.js +1 -1
  690. package/dist/domain/domain-runtime/web-component/types.d.ts +6 -3
  691. package/dist/domain/extensions/attribute-switch.d.ts +1 -1
  692. package/dist/domain/extensions/attribute-switch.js +1 -1
  693. package/dist/domain/extensions/editable-offset-indicator.js +1 -1
  694. package/dist/domain/extensions/export-images/agent-screenshot.d.ts +24 -0
  695. package/dist/domain/extensions/export-images/agent-screenshot.js +1 -0
  696. package/dist/domain/extensions/{client-side-image.d.ts → export-images/client-side-image.d.ts} +2 -2
  697. package/dist/domain/extensions/export-images/client-side-image.js +1 -0
  698. package/dist/domain/extensions/export-images/index.d.ts +3 -0
  699. package/dist/domain/extensions/export-images/index.js +1 -0
  700. package/dist/domain/extensions/{save-template-thumbnail.d.ts → export-images/save-template-thumbnail.d.ts} +2 -2
  701. package/dist/domain/extensions/export-images/save-template-thumbnail.js +1 -0
  702. package/dist/domain/extensions/fit-selection-orthographic-camera-component.js +1 -1
  703. package/dist/domain/extensions/fit-selection-perspective-camera-component.js +1 -1
  704. package/dist/domain/extensions/index.d.ts +1 -2
  705. package/dist/domain/extensions/index.js +1 -1
  706. package/dist/domain/extensions/offset-indicator.js +1 -1
  707. package/dist/domain/extensions/scene-entity-animation.js +1 -1
  708. package/dist/domain/mocks/create-mock-domain-runtime.d.ts +9 -0
  709. package/dist/domain/system-components/child-entity-selection.js +1 -1
  710. package/dist/domain/system-components/common/recipes.d.ts +21 -3
  711. package/dist/domain/system-components/common/recipes.js +1 -1
  712. package/dist/domain/system-components/common/scene-entity-control-schema.d.ts +3 -0
  713. package/dist/domain/system-components/common/scene-entity-control-schema.js +1 -1
  714. package/dist/domain/system-components/drag-handle.js +1 -1
  715. package/dist/domain/system-components/fit-entity-orthographic-camera-component.js +1 -1
  716. package/dist/domain/system-components/fit-entity-perspective-camera-component.js +1 -1
  717. package/dist/domain/system-components/fit-root-orthographic-camera-component.js +1 -1
  718. package/dist/domain/system-components/fit-root-perspective-camera-component.js +1 -1
  719. package/dist/domain/system-components/hotspot-drag-handle.js +1 -1
  720. package/dist/domain/system-components/static-orthographic-camera-component.js +1 -1
  721. package/dist/domain/system-components/static-perspective-camera-component.js +1 -1
  722. package/dist/editor/Editor.d.ts +7 -0
  723. package/dist/editor/Editor.js +1 -1
  724. package/dist/editor/domain-editors/DomainSpecContext.js +1 -1
  725. package/dist/editor/domain-editors/apply-spec-update.d.ts +10 -2
  726. package/dist/editor/domain-editors/apply-spec-update.js +1 -1
  727. package/dist/editor/domain-editors/use-implementation-files.d.ts +6 -14
  728. package/dist/editor/domain-editors/use-implementation-files.js +1 -1
  729. package/dist/editor/mocks/CreateMockYaml.js +2 -2
  730. package/dist/editor/mocks/EditorMockContext.d.ts +2 -0
  731. package/dist/editor/mocks/EditorMockContext.js +1 -1
  732. package/dist/editor/screenshot/ScreenshotHandler.js +1 -1
  733. package/dist/globals/index.d.ts +2 -0
  734. package/dist/i18n/locales/cs/messages.js +1 -1
  735. package/dist/i18n/locales/da/messages.js +1 -1
  736. package/dist/i18n/locales/de/messages.js +1 -1
  737. package/dist/i18n/locales/de-AT/messages.js +1 -1
  738. package/dist/i18n/locales/en/messages.js +1 -1
  739. package/dist/i18n/locales/en-AU/messages.js +1 -1
  740. package/dist/i18n/locales/en-GB/messages.js +1 -1
  741. package/dist/i18n/locales/es/messages.js +1 -1
  742. package/dist/i18n/locales/fr/messages.js +1 -1
  743. package/dist/i18n/locales/hu/messages.js +1 -1
  744. package/dist/i18n/locales/it/messages.js +1 -1
  745. package/dist/i18n/locales/nl/messages.js +1 -1
  746. package/dist/i18n/locales/pl/messages.js +1 -1
  747. package/dist/i18n/locales/pt/messages.js +1 -1
  748. package/dist/i18n/locales/ro/messages.js +1 -1
  749. package/dist/i18n/locales/sk/messages.js +1 -1
  750. package/dist/i18n/locales/tr/messages.js +1 -1
  751. package/dist/legacy/domain-cli/utils/cache.d.ts +1 -1
  752. package/dist/legacy/domain-cli/utils/cache.js +1 -1
  753. package/dist/legacy/domain-spec/__generated/domain-schema.d.ts +162 -145
  754. package/dist/legacy/domain-spec/spec/domain-spec.js +1 -1
  755. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/claude-md.d.ts +1 -1
  756. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/cursor-rules.d.ts +1 -1
  757. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/shared-sdk-content.d.ts +1 -1
  758. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/shared-sdk-content.js +13 -1
  759. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/frontend-admin.js +15 -14
  760. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/frontend-configurator.js +14 -12
  761. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/frontend-embedded-admin.js +14 -13
  762. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/frontend-embedded-configurator.js +13 -11
  763. package/dist/phoenix/application-spec/application-spec-builder.d.ts +0 -2
  764. package/dist/phoenix/application-spec/application-spec-builder.js +1 -1
  765. package/dist/phoenix/application-spec/generate-application-spec-project.d.ts +0 -2
  766. package/dist/phoenix/application-spec/generate-application-spec-project.js +2 -2
  767. package/dist/phoenix/application-spec/generate-application-spec-story.js +2 -2
  768. package/dist/phoenix/application-spec/generate-project-files.d.ts +0 -2
  769. package/dist/phoenix/application-spec/project-files.js +1 -1
  770. package/dist/phoenix/bin.d.ts +2 -0
  771. package/dist/phoenix/bin.js +2 -0
  772. package/dist/phoenix/check-configs/built-in-eslint-config.d.ts +9 -0
  773. package/dist/phoenix/check-configs/built-in-eslint-config.js +1 -0
  774. package/dist/phoenix/check-configs/eslint-config.d.ts +10 -0
  775. package/dist/phoenix/check-configs/eslint-config.js +1 -0
  776. package/dist/phoenix/check-configs/index.d.ts +8 -0
  777. package/dist/phoenix/check-configs/index.js +1 -0
  778. package/dist/phoenix/check-configs/typescript-project.d.ts +5 -0
  779. package/dist/phoenix/check-configs/typescript-project.js +1 -0
  780. package/dist/phoenix/cli.d.ts +0 -1
  781. package/dist/phoenix/cli.js +1 -2
  782. package/dist/phoenix/commands/build.js +1 -1
  783. package/dist/phoenix/commands/check.js +1 -1
  784. package/dist/phoenix/commands/clean.js +1 -1
  785. package/dist/phoenix/commands/deploy.js +2 -2
  786. package/dist/phoenix/commands/deps/deps.d.ts +73 -0
  787. package/dist/phoenix/commands/deps/deps.js +3 -0
  788. package/dist/phoenix/commands/deps/index.d.ts +1 -0
  789. package/dist/phoenix/commands/deps/index.js +1 -0
  790. package/dist/phoenix/commands/deps/sdk-manifest.d.ts +12 -0
  791. package/dist/phoenix/commands/deps/sdk-manifest.js +1 -0
  792. package/dist/phoenix/commands/dev.js +5 -5
  793. package/dist/phoenix/commands/fix.js +1 -1
  794. package/dist/phoenix/commands/index.d.ts +1 -0
  795. package/dist/phoenix/commands/index.js +1 -1
  796. package/dist/phoenix/commands/init.d.ts +17 -5
  797. package/dist/phoenix/commands/init.js +1 -1
  798. package/dist/phoenix/commands/link.js +1 -1
  799. package/dist/phoenix/commands/prettier.js +1 -1
  800. package/dist/phoenix/commands/sync.js +1 -1
  801. package/dist/phoenix/commands/unlink.js +1 -1
  802. package/dist/phoenix/services/application-spec-project-watcher.d.ts +2 -2
  803. package/dist/phoenix/services/application-spec-project-watcher.js +3 -3
  804. package/dist/phoenix/services/lingui.d.ts +8 -0
  805. package/dist/phoenix/services/lingui.js +1 -1
  806. package/dist/phoenix/services/source-archive.d.ts +1 -0
  807. package/dist/phoenix/services/source-archive.js +1 -1
  808. package/dist/phoenix/services/swc.js +1 -1
  809. package/dist/phoenix/services/vite-config.js +1 -1
  810. package/dist/phoenix/utils/index.d.ts +4 -2
  811. package/dist/phoenix/utils/index.js +1 -1
  812. package/dist/phoenix/utils/node-version/index.d.ts +1 -0
  813. package/dist/phoenix/utils/node-version/index.js +1 -0
  814. package/dist/phoenix/utils/node-version/supported-node-version.d.ts +12 -0
  815. package/dist/phoenix/utils/node-version/supported-node-version.js +4 -0
  816. package/dist/phoenix/utils/package-info.d.ts +0 -9
  817. package/dist/phoenix/utils/package-info.js +1 -1
  818. package/dist/phoenix/utils/package-manager/index.d.ts +2 -0
  819. package/dist/phoenix/utils/package-manager/index.js +1 -0
  820. package/dist/phoenix/utils/package-manager/package-manager-commands.d.ts +19 -0
  821. package/dist/phoenix/utils/package-manager/package-manager-commands.js +1 -0
  822. package/dist/phoenix/utils/package-manager/pnpm-project.d.ts +9 -0
  823. package/dist/phoenix/utils/package-manager/pnpm-project.js +1 -0
  824. package/dist/phoenix/utils/run-command.d.ts +11 -0
  825. package/dist/phoenix/utils/run-command.js +1 -1
  826. package/dist/phoenix/utils/sdk-tool/index.d.ts +1 -0
  827. package/dist/phoenix/utils/sdk-tool/index.js +1 -0
  828. package/dist/phoenix/utils/sdk-tool/resolve-sdk-tool-bin.d.ts +15 -0
  829. package/dist/phoenix/utils/sdk-tool/resolve-sdk-tool-bin.js +1 -0
  830. package/dist/react-sdk/application-runtime/common-services/analytics/analytics-service.d.ts +1 -1
  831. package/dist/{configurator/common → react-sdk/application-runtime/common-services/analytics}/frontend-analytics.d.ts +4 -0
  832. package/dist/react-sdk/application-runtime/common-services/analytics/index.d.ts +1 -0
  833. package/dist/react-sdk/application-runtime/common-services/analytics/index.js +1 -1
  834. package/dist/react-sdk/application-runtime/common-services/initialize.d.ts +2 -2
  835. package/dist/react-sdk/application-runtime/hooks/index.d.ts +2 -0
  836. package/dist/react-sdk/application-runtime/hooks/index.js +1 -1
  837. package/dist/react-sdk/application-runtime/hooks/loading/index.d.ts +5 -0
  838. package/dist/react-sdk/application-runtime/hooks/loading/index.js +1 -0
  839. package/dist/react-sdk/application-runtime/hooks/loading/loader-host-context.d.ts +11 -0
  840. package/dist/react-sdk/application-runtime/hooks/loading/loader-host-context.js +1 -0
  841. package/dist/react-sdk/application-runtime/hooks/loading/use-is-awaiting-loading-phase.d.ts +8 -0
  842. package/dist/react-sdk/application-runtime/hooks/loading/use-is-awaiting-loading-phase.js +1 -0
  843. package/dist/react-sdk/application-runtime/hooks/loading/use-is-loader-hosted.d.ts +7 -0
  844. package/dist/react-sdk/application-runtime/hooks/loading/use-is-loader-hosted.js +1 -0
  845. package/dist/react-sdk/application-runtime/hooks/loading/use-loading-actions.d.ts +9 -0
  846. package/dist/react-sdk/application-runtime/hooks/loading/use-loading-actions.js +1 -0
  847. package/dist/react-sdk/application-runtime/hooks/loading/use-loading-application.d.ts +24 -0
  848. package/dist/react-sdk/application-runtime/hooks/loading/use-loading-application.js +1 -0
  849. package/dist/react-sdk/application-runtime/hooks/responsive/index.d.ts +3 -0
  850. package/dist/react-sdk/application-runtime/hooks/responsive/index.js +1 -0
  851. package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-actions.d.ts +9 -0
  852. package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-actions.js +1 -0
  853. package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-application.d.ts +26 -0
  854. package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-application.js +1 -0
  855. package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-state.d.ts +10 -0
  856. package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-state.js +1 -0
  857. package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-store.d.ts +24 -0
  858. package/dist/react-sdk/application-runtime/hooks/responsive/use-responsive-store.js +1 -0
  859. package/dist/react-sdk/application-runtime/hooks/use-access-service.d.ts +5 -0
  860. package/dist/react-sdk/application-runtime/hooks/use-access-service.js +1 -1
  861. package/dist/react-sdk/application-runtime/hooks/use-phoenix-element-slots.d.ts +2 -1
  862. package/dist/react-sdk/application-runtime/hooks/use-web-component-readonly-sync.d.ts +4 -11
  863. package/dist/react-sdk/application-runtime/hooks/use-web-component-readonly-sync.js +1 -1
  864. package/dist/react-sdk/application-runtime/hooks/use-web-component.d.ts +5 -0
  865. package/dist/react-sdk/application-runtime/hooks/use-web-component.js +1 -1
  866. package/dist/react-sdk/application-runtime/host-properties/actions.d.ts +15 -0
  867. package/dist/react-sdk/application-runtime/host-properties/actions.js +1 -0
  868. package/dist/react-sdk/application-runtime/host-properties/connect-host-properties.d.ts +30 -0
  869. package/dist/react-sdk/application-runtime/host-properties/connect-host-properties.js +1 -0
  870. package/dist/react-sdk/application-runtime/host-properties/domain-host-properties.d.ts +7 -0
  871. package/dist/react-sdk/application-runtime/host-properties/domain-host-properties.js +1 -0
  872. package/dist/react-sdk/application-runtime/host-properties/host-property-descriptors.d.ts +21 -0
  873. package/dist/react-sdk/application-runtime/host-properties/host-property-descriptors.js +1 -0
  874. package/dist/react-sdk/application-runtime/host-properties/host-property-scopes.d.ts +42 -0
  875. package/dist/react-sdk/application-runtime/host-properties/host-property-scopes.js +1 -0
  876. package/dist/react-sdk/application-runtime/host-properties/index.d.ts +8 -0
  877. package/dist/react-sdk/application-runtime/host-properties/index.js +1 -0
  878. package/dist/react-sdk/application-runtime/host-properties/initialize.d.ts +7 -0
  879. package/dist/react-sdk/application-runtime/host-properties/initialize.js +1 -0
  880. package/dist/react-sdk/application-runtime/host-properties/selectors.d.ts +54 -0
  881. package/dist/react-sdk/application-runtime/host-properties/selectors.js +1 -0
  882. package/dist/react-sdk/application-runtime/host-properties/types.d.ts +18 -0
  883. package/dist/react-sdk/application-runtime/host-properties/types.js +1 -0
  884. package/dist/react-sdk/application-runtime/index.d.ts +3 -0
  885. package/dist/react-sdk/application-runtime/integration/initialize.d.ts +7 -6
  886. package/dist/react-sdk/application-runtime/integration/initialize.js +1 -1
  887. package/dist/react-sdk/application-runtime/integration/selectors.d.ts +11 -2
  888. package/dist/react-sdk/application-runtime/integration/selectors.js +1 -1
  889. package/dist/react-sdk/application-runtime/integration/types.d.ts +5 -2
  890. package/dist/react-sdk/application-runtime/loading/actions.d.ts +45 -0
  891. package/dist/react-sdk/application-runtime/loading/actions.js +1 -0
  892. package/dist/react-sdk/application-runtime/loading/index.d.ts +4 -0
  893. package/dist/react-sdk/application-runtime/loading/index.js +1 -0
  894. package/dist/react-sdk/application-runtime/loading/initialize.d.ts +3 -0
  895. package/dist/react-sdk/application-runtime/loading/initialize.js +1 -0
  896. package/dist/react-sdk/application-runtime/loading/selectors.d.ts +18 -0
  897. package/dist/react-sdk/application-runtime/loading/selectors.js +1 -0
  898. package/dist/react-sdk/application-runtime/loading/types.d.ts +59 -0
  899. package/dist/react-sdk/application-runtime/loading/types.js +1 -0
  900. package/dist/react-sdk/application-runtime/mocks/create-mock-application-runtime.js +1 -1
  901. package/dist/react-sdk/application-runtime/provider/ApplicationRuntime.d.ts +6 -0
  902. package/dist/react-sdk/application-runtime/provider/ApplicationRuntime.js +1 -1
  903. package/dist/react-sdk/application-runtime/provider/ApplicationType.d.ts +7 -0
  904. package/dist/react-sdk/application-runtime/provider/ApplicationType.js +1 -1
  905. package/dist/react-sdk/application-runtime/responsive/actions.d.ts +47 -0
  906. package/dist/react-sdk/application-runtime/responsive/actions.js +1 -0
  907. package/dist/react-sdk/application-runtime/responsive/index.d.ts +4 -0
  908. package/dist/react-sdk/application-runtime/responsive/index.js +1 -0
  909. package/dist/react-sdk/application-runtime/responsive/initialize.d.ts +3 -0
  910. package/dist/react-sdk/application-runtime/responsive/initialize.js +1 -0
  911. package/dist/react-sdk/application-runtime/responsive/selectors.d.ts +20 -0
  912. package/dist/react-sdk/application-runtime/responsive/selectors.js +1 -0
  913. package/dist/react-sdk/application-runtime/responsive/types.d.ts +104 -0
  914. package/dist/react-sdk/application-runtime/responsive/types.js +1 -0
  915. package/dist/react-sdk/application-runtime/routing/routing-service.d.ts +34 -2
  916. package/dist/react-sdk/application-runtime/routing/routing-service.js +1 -1
  917. package/dist/react-sdk/application-runtime/runtime/create-application-runtime.d.ts +313 -10
  918. package/dist/react-sdk/application-runtime/runtime/create-application-runtime.js +1 -1
  919. package/dist/react-sdk/domain-application-context/BackendDataProvider.js +1 -1
  920. package/dist/react-sdk/domain-application-context/DomainLifecycleBoundary.js +1 -1
  921. package/dist/react-sdk/domain-application-context/FrontendDataProvider.js +1 -1
  922. package/dist/react-sdk/domain-application-context/PublishedDataProvider.js +1 -1
  923. package/dist/react-sdk/domain-context/common-hooks/index.d.ts +9 -0
  924. package/dist/react-sdk/domain-context/common-hooks/index.js +1 -0
  925. package/dist/react-sdk/domain-context/common-hooks/use-configuration-save-state.d.ts +23 -0
  926. package/dist/react-sdk/domain-context/common-hooks/use-configuration-save-state.js +1 -0
  927. package/dist/react-sdk/domain-context/common-hooks/use-configurator-messages.d.ts +41 -0
  928. package/dist/react-sdk/domain-context/common-hooks/use-configurator-messages.js +1 -0
  929. package/dist/react-sdk/domain-context/common-hooks/use-domain-context.d.ts +1 -0
  930. package/dist/react-sdk/domain-context/component-hooks/index.d.ts +1 -0
  931. package/dist/react-sdk/domain-context/component-hooks/index.js +1 -0
  932. package/dist/react-sdk/domain-context/component-hooks/use-component.js +1 -1
  933. package/dist/react-sdk/domain-context/entity-hooks/use-domain-entity-state.js +1 -1
  934. package/dist/react-sdk/domain-context/entity-hooks/use-entity-price.js +1 -1
  935. package/dist/react-sdk/domain-context/entity-hooks/use-root-entity-id.js +1 -1
  936. package/dist/react-sdk/domain-context/entity-hooks/use-root-entity-state.js +1 -1
  937. package/dist/react-sdk/domain-context/index.d.ts +9 -1
  938. package/dist/react-sdk/domain-context/index.js +1 -1
  939. package/dist/react-sdk/domain-context/system-hooks/composition-hooks.js +1 -1
  940. package/dist/react-sdk/domain-context/system-hooks/dimension-hooks.js +1 -1
  941. package/dist/react-sdk/domain-context/system-hooks/generate-share-link-hooks.d.ts +8 -0
  942. package/dist/react-sdk/domain-context/system-hooks/generate-share-link-hooks.js +1 -0
  943. package/dist/react-sdk/domain-context/system-hooks/hover-hooks.js +1 -1
  944. package/dist/react-sdk/domain-context/system-hooks/locale-switch-hooks.d.ts +24 -0
  945. package/dist/react-sdk/domain-context/system-hooks/locale-switch-hooks.js +1 -0
  946. package/dist/react-sdk/domain-context/system-hooks/navigation-hooks.d.ts +5 -0
  947. package/dist/react-sdk/domain-context/system-hooks/navigation-hooks.js +1 -1
  948. package/dist/react-sdk/domain-context/system-hooks/selection-hooks.js +1 -1
  949. package/dist/react-sdk/domain-context/system-hooks/share-links-hooks.d.ts +32 -0
  950. package/dist/react-sdk/domain-context/system-hooks/share-links-hooks.js +1 -0
  951. package/dist/react-sdk/domain-context/system-hooks/step-navigation-hooks.d.ts +57 -0
  952. package/dist/react-sdk/domain-context/system-hooks/step-navigation-hooks.js +1 -0
  953. package/dist/react-sdk/domain-context/system-hooks/tab-navigation-hooks.d.ts +61 -0
  954. package/dist/react-sdk/domain-context/system-hooks/tab-navigation-hooks.js +1 -0
  955. package/dist/react-sdk/domain-context/system-hooks/undo-redo-hooks.d.ts +20 -0
  956. package/dist/react-sdk/domain-context/system-hooks/undo-redo-hooks.js +1 -0
  957. package/dist/react-sdk/handlers/FinalizeActionRunner.d.ts +13 -0
  958. package/dist/react-sdk/handlers/FinalizeActionRunner.js +1 -0
  959. package/dist/react-sdk/handlers/FinalizeActionsHost.d.ts +10 -0
  960. package/dist/react-sdk/handlers/FinalizeActionsHost.js +1 -0
  961. package/dist/react-sdk/handlers/FinalizeActionsProvider.d.ts +11 -0
  962. package/dist/react-sdk/handlers/FinalizeActionsProvider.js +1 -0
  963. package/dist/react-sdk/handlers/finalize-actions-context.d.ts +6 -0
  964. package/dist/react-sdk/handlers/finalize-actions-context.js +1 -0
  965. package/dist/react-sdk/handlers/finalize-actions-store.d.ts +23 -0
  966. package/dist/react-sdk/handlers/finalize-actions-store.js +1 -0
  967. package/dist/react-sdk/handlers/index.d.ts +6 -0
  968. package/dist/react-sdk/handlers/index.js +1 -1
  969. package/dist/react-sdk/handlers/use-finalize-action.d.ts +32 -0
  970. package/dist/react-sdk/handlers/use-finalize-action.js +1 -0
  971. package/dist/react-sdk/handlers/use-finalize-actions.d.ts +53 -0
  972. package/dist/react-sdk/handlers/use-finalize-actions.js +1 -0
  973. package/dist/react-sdk/index.d.ts +2 -0
  974. package/dist/react-sdk/index.js +1 -1
  975. package/dist/react-sdk/loading-state/LoaderHost.d.ts +20 -0
  976. package/dist/react-sdk/loading-state/LoaderHost.js +1 -0
  977. package/dist/react-sdk/loading-state/built-in-loading-phase.d.ts +9 -0
  978. package/dist/react-sdk/loading-state/built-in-loading-phase.js +1 -0
  979. package/dist/react-sdk/loading-state/create-app-loader.d.ts +18 -0
  980. package/dist/react-sdk/loading-state/create-app-loader.js +1 -0
  981. package/dist/react-sdk/loading-state/index.d.ts +4 -2
  982. package/dist/react-sdk/loading-state/index.js +1 -1
  983. package/dist/react-sdk/loading-state/{LoadingStateProvider.d.ts → loading-state-hooks.d.ts} +3 -12
  984. package/dist/react-sdk/loading-state/loading-state-hooks.js +1 -0
  985. package/dist/react-sdk/page-elements/ConfigurationNotFoundPagePropsProvider.d.ts +17 -0
  986. package/dist/react-sdk/page-elements/ConfigurationNotFoundPagePropsProvider.js +1 -0
  987. package/dist/react-sdk/page-elements/ConfiguratorErrorPagePropsProvider.d.ts +17 -0
  988. package/dist/react-sdk/page-elements/ConfiguratorErrorPagePropsProvider.js +1 -0
  989. package/dist/react-sdk/page-elements/configurator-error-page-props.d.ts +20 -0
  990. package/dist/react-sdk/page-elements/configurator-error-page-props.js +1 -0
  991. package/dist/react-sdk/page-elements/create-configuration-not-found-page.d.ts +15 -0
  992. package/dist/react-sdk/page-elements/create-configuration-not-found-page.js +1 -0
  993. package/dist/react-sdk/page-elements/create-configurator-error-page.d.ts +14 -0
  994. package/dist/react-sdk/page-elements/create-configurator-error-page.js +1 -0
  995. package/dist/react-sdk/page-elements/create-preset-ui.d.ts +9 -0
  996. package/dist/react-sdk/page-elements/create-preset-ui.js +1 -0
  997. package/dist/react-sdk/page-elements/index.d.ts +6 -0
  998. package/dist/react-sdk/page-elements/index.js +1 -0
  999. package/dist/react-sdk/scene-control-elements/AddEntityFabElement.d.ts +36 -0
  1000. package/dist/react-sdk/scene-control-elements/AddEntityFabElement.js +1 -0
  1001. package/dist/react-sdk/scene-control-elements/DeleteEntityFabElement.d.ts +44 -0
  1002. package/dist/react-sdk/scene-control-elements/DeleteEntityFabElement.js +1 -0
  1003. package/dist/react-sdk/scene-control-elements/NavigationHotspotElement.d.ts +38 -0
  1004. package/dist/react-sdk/scene-control-elements/NavigationHotspotElement.js +1 -0
  1005. package/dist/react-sdk/scene-control-elements/SelectionHotspotElement.d.ts +30 -0
  1006. package/dist/react-sdk/scene-control-elements/SelectionHotspotElement.js +1 -0
  1007. package/dist/react-sdk/scene-control-elements/ToolMenuFabElement.d.ts +30 -0
  1008. package/dist/react-sdk/scene-control-elements/ToolMenuFabElement.js +1 -0
  1009. package/dist/react-sdk/scene-control-elements/create-add-entity-fab.d.ts +9 -0
  1010. package/dist/react-sdk/scene-control-elements/create-add-entity-fab.js +1 -0
  1011. package/dist/react-sdk/scene-control-elements/create-delete-entity-fab.d.ts +9 -0
  1012. package/dist/react-sdk/scene-control-elements/create-delete-entity-fab.js +1 -0
  1013. package/dist/react-sdk/scene-control-elements/create-navigation-hotspot.d.ts +10 -0
  1014. package/dist/react-sdk/scene-control-elements/create-navigation-hotspot.js +1 -0
  1015. package/dist/react-sdk/scene-control-elements/create-selection-hotspot.d.ts +9 -0
  1016. package/dist/react-sdk/scene-control-elements/create-selection-hotspot.js +1 -0
  1017. package/dist/react-sdk/scene-control-elements/create-tool-menu-fab.d.ts +9 -0
  1018. package/dist/react-sdk/scene-control-elements/create-tool-menu-fab.js +1 -0
  1019. package/dist/react-sdk/scene-control-elements/index.d.ts +11 -0
  1020. package/dist/react-sdk/scene-control-elements/index.js +1 -0
  1021. package/dist/react-sdk/scene-control-elements/scene-control-tooltip-visibility.d.ts +5 -0
  1022. package/dist/react-sdk/scene-control-elements/scene-control-tooltip-visibility.js +1 -0
  1023. package/dist/react-sdk/session/NotAuthenticatedPage.d.ts +4 -0
  1024. package/dist/react-sdk/session/NotAuthenticatedPage.js +1 -1
  1025. package/dist/react-sdk/session/SessionServiceProvider.js +1 -1
  1026. package/dist/react-sdk/web-component/create-web-component.js +1 -1
  1027. package/dist/react-sdk/web-component/god-mode/god-mode-registry.d.ts +30 -0
  1028. package/dist/react-sdk/web-component/god-mode/god-mode-registry.js +1 -0
  1029. package/dist/react-sdk/web-component/god-mode/index.d.ts +1 -0
  1030. package/dist/react-sdk/web-component/god-mode/index.js +1 -0
  1031. package/dist/ui-sdk/icons/__generated/Lightbulb.d.ts +3 -0
  1032. package/dist/ui-sdk/icons/__generated/Lightbulb.js +1 -0
  1033. package/dist/ui-sdk/icons/__generated/XCircle.d.ts +3 -0
  1034. package/dist/ui-sdk/icons/__generated/XCircle.js +1 -0
  1035. package/dist/ui-sdk/icons/__generated/index.d.ts +4 -0
  1036. package/dist/ui-sdk/icons/__generated/index.js +1 -1
  1037. package/dist/ui-sdk/icons/icons-registry.js +1 -1
  1038. package/dist/ui-sdk/responsive-application/ResponsiveApplicationProvider.d.ts +9 -0
  1039. package/dist/ui-sdk/responsive-application/ResponsiveApplicationProvider.js +1 -1
  1040. package/dist/ui-sdk/responsive-application/index.d.ts +1 -0
  1041. package/dist/ui-sdk/responsive-application/responsive-state-store.d.ts +59 -0
  1042. package/dist/ui-sdk/responsive-application/responsive-state-store.js +1 -0
  1043. package/dist/ui-sdk/responsive-application/use-store-responsive-info.d.ts +7 -0
  1044. package/dist/ui-sdk/responsive-application/use-store-responsive-info.js +1 -0
  1045. package/dist/ui-sdk/theme/types.d.ts +5 -1
  1046. package/package.json +58 -36
  1047. package/schema/domain-spec.json +1136 -205
  1048. package/dist/application-spec/generate-application/code/templates/nvmrc.d.ts +0 -1
  1049. package/dist/application-spec/generate-application/code/templates/nvmrc.js +0 -2
  1050. package/dist/application-spec/generate-application/code/templates/package-json.d.ts +0 -10
  1051. package/dist/application-spec/generate-application/code/templates/package-json.js +0 -1
  1052. package/dist/application-spec/generate-application/code/templates/webhook-implementation.d.ts +0 -1
  1053. package/dist/application-spec/generate-application/code/templates/webhook-implementation.js +0 -7
  1054. package/dist/configurator/ui/assistant/entity-link/entity-link-target.d.ts +0 -5
  1055. package/dist/configurator/ui/assistant/entity-link/entity-link-target.js +0 -1
  1056. package/dist/configurator/ui/phoenix-layout-elements/language/use-locale-options.d.ts +0 -4
  1057. package/dist/configurator/ui/phoenix-layout-elements/language/use-locale-options.js +0 -1
  1058. package/dist/configurator/ui/phoenix-layout-elements/measurements/use-measurements-visibility.d.ts +0 -4
  1059. package/dist/configurator/ui/phoenix-layout-elements/measurements/use-measurements-visibility.js +0 -1
  1060. package/dist/domain/extensions/client-side-image.js +0 -1
  1061. package/dist/domain/extensions/save-template-thumbnail.js +0 -1
  1062. package/dist/phoenix/commands/generate-project-files.d.ts +0 -6
  1063. package/dist/phoenix/commands/generate-project-files.js +0 -1
  1064. package/dist/react-sdk/application-runtime/runtime/god-mode.d.ts +0 -19
  1065. package/dist/react-sdk/application-runtime/runtime/god-mode.js +0 -1
  1066. package/dist/react-sdk/loading-state/LoadingStateProvider.js +0 -1
  1067. package/dist/react-sdk/loading-state/loading-state-manager.d.ts +0 -32
  1068. package/dist/react-sdk/loading-state/loading-state-manager.js +0 -1
  1069. /package/dist/{configurator/common → react-sdk/application-runtime/common-services/analytics}/frontend-analytics.js +0 -0
@@ -1,6 +1,6 @@
1
1
  # Custom Layout
2
2
 
3
- Make the configurator look like the brand's own product page. A furniture shop wants its table configurator to read like the rest of its site: a floating panel on the left with the product's name, the edge finishes as picture cards, the price in large type and one clear call to action, and the table in the space beside it. The project writes that page as one React component. Phoenix supplies the 3D scene, the product's state and a few hooks; every pixel around the scene is the project's own.
3
+ Make the configurator look like the brand's own product page. A furniture shop wants its table configurator to read like the rest of its site: a floating panel on the left with the product's name, the edge finishes as picture cards, the price in large type and one clear call to action, and the table in the space beside it. On a phone the panel becomes a sheet at the bottom, and a button on the table's edge offers the finishes right where they apply, and the layout's own buttons switch between the whole table and a close-up of its edge. The project writes that page as one React component. Phoenix supplies the 3D scene, the product's state and a few hooks; every pixel around the scene is the project's own.
4
4
 
5
5
  - [Choose this approach](#choose-this-approach)
6
6
  - [Register the layout](#register-the-layout)
@@ -8,7 +8,13 @@ Make the configurator look like the brand's own product page. A furniture shop w
8
8
  - [Read and change the product](#read-and-change-the-product)
9
9
  - [Draw the choices](#draw-the-choices)
10
10
  - [Offer fullscreen](#offer-fullscreen)
11
+ - [Undo and redo](#undo-and-redo)
12
+ - [Show messages](#show-messages)
13
+ - [Request a quote](#request-a-quote)
11
14
  - [Serve read mode](#serve-read-mode)
15
+ - [Fit a phone](#fit-a-phone)
16
+ - [Put a control on the product](#put-a-control-on-the-product)
17
+ - [Control the scene](#control-the-scene)
12
18
  - [The spec the layout reads](#the-spec-the-layout-reads)
13
19
  - [Try the example](#try-the-example)
14
20
  - [Test cases](#test-cases)
@@ -22,7 +28,7 @@ Write your own layout when the configurator has to follow a design the built-in
22
28
 
23
29
  Stay with the built-in layout when its structure fits and only its parts change. [Universal](?path=/docs/layouts-universal--docs) demonstrates that layout, and the pages of its parts describe what the spec configures: [Header](?path=/docs/layouts-header-floating--docs), [Sidebar](?path=/docs/layouts-sidebar-general--docs), [Sidebar Footer](?path=/docs/layouts-sidebar-footer-basic--docs), [Toolbar](?path=/docs/layouts-toolbar--docs) and [View Controls](?path=/docs/layouts-view-controls--docs).
24
30
 
25
- A project layout replaces all of that. None of the built-in sidebar, footer, toolbar or property pages is rendered for it, so the layout builds every control the user needs. This example uses no SDK component that draws UI: only the scene, the safe-area hook and hooks that read and change the product's state. A layout may also mount the SDK's own pieces where it wants them, see [Configurator Interface → Loaded Configuration UI](?path=/docs/phoenix-configurator-interface--docs#loaded-configuration-ui).
31
+ A project layout replaces all of that. None of the built-in sidebar, footer, toolbar or property pages is rendered for it, so the layout builds every control the user needs. This example uses no SDK component that draws UI: only the scene and the `phx.frontend` hooks that measure the free area, read and change the product's state and record analytics. A layout may also mount the SDK's own pieces where it wants them, see [Configurator Interface → Loaded Configuration UI](?path=/docs/phoenix-configurator-interface--docs#loaded-configuration-ui).
26
32
 
27
33
  ## Register the layout
28
34
 
@@ -30,25 +36,44 @@ Create `src/elements/ConfiguratorLayout.tsx` with a default export made by `phx.
30
36
 
31
37
  ```tsx
32
38
  import { Box } from '@mui/material'
39
+ import { useState } from 'react'
33
40
 
34
- import { LayoutSlots, useSceneSafeArea } from '@salsita-ai/sdk/configurator'
41
+ import { LayoutSlots } from '@salsita-ai/sdk/configurator'
35
42
 
36
43
  import { phx } from '../../.phoenix/phx.js'
44
+ import { BottomSheet } from '../_shared/BottomSheet.js'
37
45
  import { FullscreenToggle } from '../_shared/FullscreenToggle.js'
46
+ import { MessageToast } from '../_shared/MessageToast.js'
38
47
  import { ProductPanel } from '../_shared/ProductPanel.js'
48
+ import { SceneControls } from '../_shared/SceneControls.js'
49
+ import { UndoRedo } from '../_shared/UndoRedo.js'
39
50
 
40
51
  const GUTTER = 24
41
52
  const PANEL_WIDTH = 380
53
+ const PHONE_GUTTER = 16
54
+ const TOUCH_TARGET = 44
42
55
 
43
56
  const ConfiguratorLayout = () => {
44
- const { containerRef, safeAreaRef } = useSceneSafeArea()
57
+ const { containerRef, safeAreaRef } = phx.frontend.useSceneSafeArea()
45
58
  const isFullscreen = phx.frontend.useIsFullscreenActive()
59
+ // `null` until the container has been measured; the layout shows only the scene until then
60
+ const viewports = phx.frontend.useActiveViewports()
61
+ const [isSheetOpen, setSheetOpen] = useState(false)
62
+
63
+ const isDesktop = viewports?.includes('desktop') ?? false
64
+ const gutter = isDesktop ? GUTTER : PHONE_GUTTER
46
65
 
47
66
  return (
48
- <Box component="div" ref={containerRef} sx={{ position: 'absolute', inset: 0 }}>
67
+ <Box
68
+ component="div"
69
+ ref={containerRef}
70
+ // `clip`, not `hidden`: what overflows, such as the sheet's fill below its edge, can never be scrolled into view,
71
+ // not even by the browser when the focus moves
72
+ sx={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', overflow: 'clip' }}
73
+ >
49
74
  <LayoutSlots.SharedCanvas />
50
75
 
51
- {!isFullscreen && (
76
+ {viewports !== null && isDesktop && !isFullscreen && (
52
77
  <Box
53
78
  component="aside"
54
79
  sx={{
@@ -62,31 +87,80 @@ const ConfiguratorLayout = () => {
62
87
  borderRadius: 5,
63
88
  bgcolor: 'background.paper',
64
89
  boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)',
65
- overflow: 'hidden',
90
+ // A very short container, such as a phone held sideways, scrolls the panel instead of clipping its footer
91
+ overflowX: 'hidden',
92
+ overflowY: 'auto',
93
+ overscrollBehavior: 'contain',
66
94
  }}
67
95
  >
68
96
  <ProductPanel />
69
97
  </Box>
70
98
  )}
71
99
 
72
- <Box
73
- component="div"
74
- ref={safeAreaRef}
75
- sx={{
76
- position: 'absolute',
77
- insetBlock: 0,
78
- insetInlineStart: isFullscreen ? 0 : PANEL_WIDTH + 2 * GUTTER,
79
- insetInlineEnd: 0,
80
- pointerEvents: 'none',
81
- }}
82
- >
100
+ {viewports !== null && (
83
101
  <Box
84
102
  component="div"
85
- sx={{ position: 'absolute', insetBlockStart: GUTTER, insetInlineEnd: GUTTER, pointerEvents: 'auto' }}
103
+ ref={safeAreaRef}
104
+ sx={{
105
+ // Desktop: right of the panel. Phone: above the sheet, whose height the column gives the rest.
106
+ ...(isDesktop
107
+ ? {
108
+ position: 'absolute',
109
+ insetBlock: 0,
110
+ insetInlineStart: isFullscreen ? 0 : PANEL_WIDTH + 2 * GUTTER,
111
+ insetInlineEnd: 0,
112
+ }
113
+ : { position: 'relative', flex: '1 1 0', minBlockSize: 0 }),
114
+ pointerEvents: 'none',
115
+ }}
86
116
  >
87
- <FullscreenToggle />
117
+ <Box
118
+ component="div"
119
+ sx={{
120
+ position: 'absolute',
121
+ insetBlockStart: gutter,
122
+ insetInlineEnd: gutter,
123
+ display: 'flex',
124
+ gap: 1,
125
+ pointerEvents: 'auto',
126
+ }}
127
+ >
128
+ {!isFullscreen && <UndoRedo />}
129
+ <FullscreenToggle compact={!isDesktop} />
130
+ </Box>
131
+ <Box
132
+ component="div"
133
+ sx={{
134
+ position: 'absolute',
135
+ pointerEvents: 'auto',
136
+ // Desktop: bottom left, above the row of view controls. Phone: at the top below the row of undo, redo and
137
+ // fullscreen, clear of the sheet's handle and the view controls.
138
+ ...(isDesktop
139
+ ? { insetBlockEnd: 2 * GUTTER + TOUCH_TARGET, insetInlineStart: GUTTER }
140
+ : { insetBlockStart: gutter + TOUCH_TARGET + gutter / 2, insetInline: gutter }),
141
+ }}
142
+ >
143
+ <MessageToast />
144
+ </Box>
145
+ {/* A phone in fullscreen keeps only the reset, as the built-in layout does. Below 1024 pixels (`tablet`) the
146
+ free area beside the panel is narrow, so the reset shows only its icon there too. */}
147
+ <Box
148
+ component="div"
149
+ sx={{ position: 'absolute', insetBlockEnd: gutter, insetInlineEnd: gutter, pointerEvents: 'auto' }}
150
+ >
151
+ <SceneControls
152
+ compact={!isDesktop || (viewports?.includes('tablet') ?? false)}
153
+ resetOnly={!isDesktop && isFullscreen}
154
+ />
155
+ </Box>
88
156
  </Box>
89
- </Box>
157
+ )}
158
+
159
+ {viewports !== null && !isDesktop && !isFullscreen && (
160
+ <BottomSheet open={isSheetOpen} onOpenChange={setSheetOpen} label="Product details">
161
+ <ProductPanel collapsed={!isSheetOpen} inSheet />
162
+ </BottomSheet>
163
+ )}
90
164
  </Box>
91
165
  )
92
166
  }
@@ -99,12 +173,13 @@ The components it uses live in `src/_shared/`, which Phoenix does not import by
99
173
  ## What the layout has to do
100
174
 
101
175
  - **Position the root and cover the page.** `LayoutSlots.SharedCanvas` is absolutely positioned and fills its nearest positioned ancestor. The root here is `position: absolute; inset: 0`.
102
- - **Mount the shared canvas.** The app loader stays on screen until the scene has loaded. A layout without `LayoutSlots.SharedCanvas` never gets past the loader unless the project sets `appLoader: legacy`.
176
+ - **Mount the shared canvas.** It renders the scene, and the app loader stays on screen until the scene has loaded. A layout without a canvas is not held by the loader: it leaves once the configuration is up.
103
177
  - **Put the UI above the canvas.** The canvas is opaque. Content that is positioned and comes after it in the markup is painted above it; unpositioned content is painted under it and cannot be seen.
104
- - **Tell the camera where the free area is.** `useSceneSafeArea()` returns two refs. `containerRef` goes on the element that covers the whole canvas, `safeAreaRef` on the region the product should stay visible in, here everything right of the panel. The camera then frames the product in that region. The hook measures again whenever either of the two elements resizes; for a change no resize reports, such as a `transform` on an ancestor settling at the end of an animation, it also returns `update` to call. Without the two refs the product is centred in the whole canvas and sits against the panel.
178
+ - **Tell the camera where the free area is.** `phx.frontend.useSceneSafeArea()` returns two refs. `containerRef` goes on the element that covers the whole canvas, `safeAreaRef` on the region the product should stay visible in, here everything right of the panel, or above the sheet on a phone. The camera then frames the product in that region. The hook measures again whenever either of the two elements resizes; for a change no resize reports, such as a `transform` on an ancestor settling at the end of an animation, it also returns `update` to call. Without the two refs the product is centred in the whole canvas and sits against the panel.
105
179
  - **Let the pointer through.** The safe-area element lies over the canvas, so it sets `pointerEvents: 'none'`; otherwise the scene cannot be orbited. The fullscreen button inside it switches pointer events back on.
106
- - **Honour fullscreen.** Fullscreen is a flag in the product's state, not a browser feature. The layout reads it with `phx.frontend.useIsFullscreenActive()`, removes the panel and lets the safe area start at the left edge, so the camera frames the product in the whole canvas.
107
- - **Serve read mode.** The same element renders a configuration shared for viewing, and the layout decides what a viewer sees.
180
+ - **Honour fullscreen.** Fullscreen is a flag in the product's state, not a browser feature. The layout reads it with `phx.frontend.useIsFullscreenActive()`, removes the panel or the sheet and lets the safe area cover the canvas, so the camera frames the product in the whole canvas.
181
+ - **Fit the width it is given.** The configurator can be embedded in a narrow column or opened on a phone. The layout reads the container's width class with `phx.frontend.useActiveViewports()` and renders a desktop or a phone arrangement, see [Fit a phone](#fit-a-phone).
182
+ - **Serve read mode.** The same element renders a configuration shared for viewing, or one the host page opens read-only, and the layout decides what a viewer sees; see [Serve read mode](#serve-read-mode).
108
183
 
109
184
  ## Read and change the product
110
185
 
@@ -114,13 +189,22 @@ The panel reads everything it shows from hooks and writes the customer's choice
114
189
  import { Box, Button, Typography } from '@mui/material'
115
190
 
116
191
  import { detailComponent } from '@salsita-ai/sdk/domain'
117
- import { useResolveLocalizable } from '@salsita-ai/sdk/react'
118
192
 
119
193
  import { phx } from '../../.phoenix/phx.js'
120
194
  import { EdgeFinishCards } from './EdgeFinishCards.js'
195
+ import { ReviewActions } from './ReviewActions.js'
196
+
197
+ const BUTTON_SX = { minBlockSize: 48, borderRadius: 999, textTransform: 'none', fontWeight: 600 } as const
198
+
199
+ interface ProductPanelProps {
200
+ // Only the name, the price and the action, for a collapsed bottom sheet
201
+ readonly collapsed?: boolean
202
+ // In a bottom sheet the cards region keeps the top of the cards in view, and the sheet scrolls when that does not fit
203
+ readonly inSheet?: boolean
204
+ }
121
205
 
122
- export const ProductPanel = () => {
123
- const resolve = useResolveLocalizable()
206
+ export const ProductPanel = ({ collapsed = false, inSheet = false }: ProductPanelProps) => {
207
+ const resolve = phx.frontend.useResolveLocalizable()
124
208
  const detail = phx.frontend.useFirstRootComponent(detailComponent)
125
209
  const price = phx.frontend.useRootEntityPrice()
126
210
  const tableId = phx.frontend.useRootEntityId()
@@ -129,9 +213,10 @@ export const ProductPanel = () => {
129
213
  const navigationState = phx.frontend.useActiveNavigationState()
130
214
  const pushNavigate = phx.frontend.usePushNavigate()
131
215
  const popNavigate = phx.frontend.usePopNavigate()
216
+ const navigate = phx.frontend.useNavigate()
132
217
 
133
218
  const isReadMode = accessInfo.viewMode
134
- const isReview = isReadMode || navigationState === 'checkout'
219
+ const isReview = isReadMode || navigationState !== 'configure'
135
220
  const chosenEdge = edgeFinish.options.find(option => option.value === edgeFinish.value)
136
221
 
137
222
  return (
@@ -152,29 +237,44 @@ export const ProductPanel = () => {
152
237
  <Typography variant="h5" component="h2" fontWeight={700}>
153
238
  {resolve(detail?.state.title)}
154
239
  </Typography>
155
- <Typography variant="body2" color="text.secondary">
156
- {resolve(detail?.state.description)}
157
- </Typography>
158
- </Box>
159
-
160
- <Box component="div" sx={{ display: 'grid', gap: 1.5, paddingInline: 3, paddingBlockEnd: 3, overflowY: 'auto' }}>
161
- <Typography variant="subtitle2" component="h3">
162
- Edge finish
163
- </Typography>
164
- {isReview ? (
165
- <Box component="div" sx={{ padding: 2, borderRadius: 3, bgcolor: 'action.hover' }}>
166
- <Typography variant="body2" fontWeight={600}>
167
- {resolve(chosenEdge?.title)}
168
- </Typography>
169
- <Typography variant="caption" color="text.secondary">
170
- {resolve(chosenEdge?.description)}
171
- </Typography>
172
- </Box>
173
- ) : (
174
- <EdgeFinishCards selection={edgeFinish} />
240
+ {!collapsed && (
241
+ <Typography variant="body2" color="text.secondary">
242
+ {resolve(detail?.state.description)}
243
+ </Typography>
175
244
  )}
176
245
  </Box>
177
246
 
247
+ {!collapsed && (
248
+ <Box
249
+ component="div"
250
+ sx={{
251
+ display: 'grid',
252
+ gap: 1.5,
253
+ paddingInline: 3,
254
+ paddingBlockEnd: 3,
255
+ // In a short container the region shrinks and scrolls; a scroll past its end moves its container on
256
+ minBlockSize: inSheet ? 120 : 0,
257
+ overflowY: 'auto',
258
+ }}
259
+ >
260
+ <Typography variant="subtitle2" component="h3">
261
+ Edge finish
262
+ </Typography>
263
+ {isReview ? (
264
+ <Box component="div" sx={{ padding: 2, borderRadius: 3, bgcolor: 'action.hover' }}>
265
+ <Typography variant="body2" fontWeight={600}>
266
+ {resolve(chosenEdge?.title)}
267
+ </Typography>
268
+ <Typography variant="caption" color="text.secondary">
269
+ {resolve(chosenEdge?.description)}
270
+ </Typography>
271
+ </Box>
272
+ ) : (
273
+ <EdgeFinishCards selection={edgeFinish} />
274
+ )}
275
+ </Box>
276
+ )}
277
+
178
278
  <Box component="footer" sx={{ display: 'grid', gap: 2, padding: 3, borderTop: 1, borderColor: 'divider' }}>
179
279
  <Box component="div" sx={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
180
280
  <Typography variant="body2" color="text.secondary">
@@ -184,42 +284,59 @@ export const ProductPanel = () => {
184
284
  {price}
185
285
  </Typography>
186
286
  </Box>
187
- {isReview && !isReadMode && (
188
- <Typography variant="body2" color="text.secondary">
189
- Your table is ready for a quote. A real shop collects the contact details here.
190
- </Typography>
191
- )}
192
- {!isReadMode && (
193
- <Button
194
- size="large"
195
- variant={isReview ? 'outlined' : 'contained'}
196
- onClick={isReview ? popNavigate : () => pushNavigate('checkout')}
197
- sx={{ minBlockSize: 48, borderRadius: 999, textTransform: 'none', fontWeight: 600 }}
198
- >
199
- {isReview ? 'Back to the configuration' : 'Review and request a quote'}
287
+ {!isReadMode && navigationState === 'configure' && (
288
+ <Button size="large" variant="contained" onClick={() => pushNavigate('checkout')} sx={BUTTON_SX}>
289
+ Review and request a quote
200
290
  </Button>
201
291
  )}
292
+ {!isReadMode && navigationState === 'checkout' && (
293
+ <>
294
+ <Typography variant="body2" color="text.secondary">
295
+ Your table is ready for a quote. A real shop collects the contact details here.
296
+ </Typography>
297
+ <ReviewActions onBack={popNavigate} />
298
+ </>
299
+ )}
300
+ {!isReadMode && navigationState === 'requested' && (
301
+ <>
302
+ <Typography variant="body2" role="status">
303
+ Thank you. Your quote request is on its way, and the shop replies within two working days.
304
+ </Typography>
305
+ <Button size="large" variant="outlined" onClick={() => navigate('configure')} sx={BUTTON_SX}>
306
+ Back to the configuration
307
+ </Button>
308
+ </>
309
+ )}
202
310
  </Box>
203
311
  </>
204
312
  )
205
313
  }
206
314
  ```
207
315
 
208
- | Hook | What the layout uses it for |
209
- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
210
- | `phx.frontend.useFirstRootComponent(detailComponent)` | The `detail` component of the root entity: name, subtitle and description. `detailComponent` comes from `@salsita-ai/sdk/domain`; the hook returns `null` without one. |
211
- | `phx.frontend.useRootEntityId()` | The id of the table, which the control hooks take. |
212
- | `phx.frontend.useAttributeSelection(entityId, instanceName)` | The `attributeSelection` component named `edgeFinish`: the current `value` and the `options`, each with `title`, `description`, `value`, `disabled` and `onSelect`. |
213
- | `phx.frontend.useRootEntityPrice()` | The root entity's `price` component, formatted for the current locale and currency, or `null` without one. |
214
- | `phx.frontend.useAccessService()` | `accessInfo.viewMode` is `true` while a configuration shared for viewing is open, and also while the host sets the `readonly` property. |
215
- | `phx.frontend.useActiveNavigationState()` | The active navigation state; the panel shows its review step while it is `checkout`. |
216
- | `phx.frontend.usePushNavigate()` and `phx.frontend.usePopNavigate()` | Enter the review step and leave it again. |
217
- | `phx.frontend.useIsFullscreenActive()` and `phx.frontend.useSetFullscreen()` | Read the fullscreen flag, and set it from the layout's own button. |
218
- | `useResolveLocalizable()` from `@salsita-ai/sdk/react` | Turns the localizable texts of the spec into strings in the current language. [Localization](?path=/docs/phoenix-localization--docs) describes it and `LocalizableMessage`. |
316
+ | Hook | What the layout uses it for |
317
+ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
318
+ | `phx.frontend.useFirstRootComponent(detailComponent)` | The `detail` component of the root entity: name, subtitle and description. `detailComponent` comes from `@salsita-ai/sdk/domain`; the hook returns `null` without one. |
319
+ | `phx.frontend.useRootEntityId()` | The id of the table, which the control hooks take. |
320
+ | `phx.frontend.useAttributeSelection(entityId, instanceName)` | The `attributeSelection` component named `edgeFinish`: the current `value` and the `options`, each with `title`, `description`, `value`, `disabled` and `onSelect`. |
321
+ | `phx.frontend.useRootEntityPrice()` | The root entity's `price` component, formatted for the current locale and currency, or `null` without one. |
322
+ | `phx.frontend.useResolveLocalizable()` | Turns the localizable texts of the spec into strings in the current language. [Localization](?path=/docs/phoenix-localization--docs) describes it, `phx.frontend.useLocalizable(text)` and `LocalizableMessage`. |
323
+ | `phx.frontend.useAccessService()` | `accessInfo.viewMode` is `true` while a configuration shared for viewing is open, and also while the host sets the `readonly` property; see [Serve read mode](#serve-read-mode). |
324
+ | `phx.frontend.useActiveNavigationState()` | The active navigation state; the panel shows its review step while it is `checkout`. |
325
+ | `phx.frontend.usePushNavigate()` and `phx.frontend.usePopNavigate()` | Enter the review step and leave it again. |
326
+ | `phx.frontend.useNavigate()` | Return from the confirmation to the configuration, forgetting the review step. |
327
+ | `phx.frontend.useFinalizeActions()` | The spec's `finalizeAction` components with what a button needs; the review step offers the `primary` one and runs it, with its pending state, its error and whether finalizing is possible; see [Request a quote](#request-a-quote). |
328
+ | `phx.frontend.useUndoRedo()` | Whether there is a change to undo or redo, and `undo` and `redo`, for the layout's own buttons; see [Undo and redo](#undo-and-redo). |
329
+ | `phx.frontend.useIsFullscreenActive()` and `phx.frontend.useSetFullscreen()` | Read the fullscreen flag, and set it from the layout's own button. |
330
+ | `phx.frontend.useAnalytics()` | Record the fullscreen button's click as the built-in button does; see [Offer fullscreen](#offer-fullscreen). |
331
+ | `phx.frontend.useSceneSafeArea()` | The two refs that tell the camera which part of the canvas the panel or the sheet leaves free; see [What the layout has to do](#what-the-layout-has-to-do). |
332
+ | `phx.frontend.useActiveViewports()` | The width classes of the configurator's container, or `null` before it is measured; the layout renders its phone arrangement unless it includes `desktop`, see [Fit a phone](#fit-a-phone). |
333
+ | `phx.frontend.useActiveViewState()` and `phx.frontend.useSetViewState()` | Read the active view state, one of the spec's `viewStates`, to mark the active view button, and make another one active; see [Control the scene](#control-the-scene). |
334
+ | `phx.frontend.useResetCamera()` | Move the camera back to the framing of the active camera, undoing the customer's orbit, pan and zoom. |
335
+ | `phx.frontend.useConfiguratorMessages(onMessage)` | Receive violations, side-effect hints and errors one by one, with their text resolved, for the layout's own toast; see [Show messages](#show-messages). |
219
336
 
220
337
  Calling an option's `onSelect` is the whole write: it changes `edgeFinish` on the table, the scene morphs the edge and the price expression recalculates. The option fields are named for display: the spec's `label` arrives as `title`, its `img` as `imageSrc`.
221
338
 
222
- The review step is the layout's own: the button pushes the `checkout` navigation state, and the panel shows a summary instead of the cards. A real shop collects the contact details there and sends the request.
339
+ The review step is the layout's own: the button pushes the `checkout` navigation state, and the panel shows a summary instead of the cards. There **Request a quote** runs the spec's finalize action, whose handler moves the panel on to the `requested` state and its confirmation; see [Request a quote](#request-a-quote). A real shop collects the contact details in the review step before it sends the request.
223
340
 
224
341
  ## Draw the choices
225
342
 
@@ -229,7 +346,9 @@ The edge finish is a visual choice, so the panel shows it as cards with a drawin
229
346
  import { Box, ButtonBase, type SxProps, type Theme, Typography } from '@mui/material'
230
347
  import type { KeyboardEvent } from 'react'
231
348
 
232
- import { type UseAttributeSelection, useResolveLocalizable } from '@salsita-ai/sdk/react'
349
+ import type { UseAttributeSelection } from '@salsita-ai/sdk/react'
350
+
351
+ import { phx } from '../../.phoenix/phx.js'
233
352
 
234
353
  // Cross-sections of the tabletop's edge, keyed by the option values of the spec.
235
354
  const EDGE_PROFILES: Readonly<Record<string, string>> = {
@@ -266,7 +385,7 @@ interface EdgeFinishCardsProps {
266
385
  }
267
386
 
268
387
  export const EdgeFinishCards = ({ selection }: EdgeFinishCardsProps) => {
269
- const resolve = useResolveLocalizable()
388
+ const resolve = phx.frontend.useResolveLocalizable()
270
389
  const { options } = selection
271
390
  // The card in the tab order: the selected one, or the first enabled one. Disabled cards are never in it.
272
391
  const selectedIndex = options.findIndex(option => option.value === selection.value && !option.disabled)
@@ -362,191 +481,821 @@ The drawings are keyed by the option values of the spec. A spec can carry a pict
362
481
 
363
482
  ## Offer fullscreen
364
483
 
365
- Fullscreen is a flag in the configurator's state, not the browser's fullscreen mode. `phx.frontend.useIsFullscreenActive()` reads it and `phx.frontend.useSetFullscreen()` returns a function that sets it: `setFullscreen(true)` turns it on, `setFullscreen(false)` off. The layout's own button flips it, and the layout decides what to hide.
484
+ Fullscreen is a flag in the configurator's state, not the browser's fullscreen mode. `phx.frontend.useIsFullscreenActive()` reads it and `phx.frontend.useSetFullscreen()` returns a function that sets it: `setFullscreen(true)` turns it on, `setFullscreen(false)` off. The layout's own button flips it, and the layout decides what to hide. Before it flips the flag, the button records the click with `phx.frontend.useAnalytics().push(FrontendAnalyticsFactory.FullScreenModeToggled())`, the event the built-in fullscreen button records, so the shop's analytics count fullscreen the same way with either layout. `FrontendAnalyticsFactory` comes from `@salsita-ai/sdk/configurator`.
366
485
 
367
486
  ```tsx
368
- import { Button } from '@mui/material'
487
+ import { Button, Tooltip } from '@mui/material'
488
+
489
+ import { FrontendAnalyticsFactory } from '@salsita-ai/sdk/configurator'
369
490
 
370
491
  import { phx } from '../../.phoenix/phx.js'
371
492
 
372
493
  const ENTER_ICON = 'M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5'
373
494
  const EXIT_ICON = 'M9 4v5H4M15 4v5h5M15 20v-5h5M9 20v-5H4'
374
495
 
375
- export const FullscreenToggle = () => {
496
+ interface FullscreenToggleProps {
497
+ // Only the icon, with the label as its accessible name and tooltip, where a label would cover the product
498
+ readonly compact?: boolean
499
+ }
500
+
501
+ export const FullscreenToggle = ({ compact = false }: FullscreenToggleProps) => {
376
502
  const isFullscreen = phx.frontend.useIsFullscreenActive()
377
503
  const setFullscreen = phx.frontend.useSetFullscreen()
504
+ const analytics = phx.frontend.useAnalytics()
505
+ const label = isFullscreen ? 'Exit fullscreen' : 'Fullscreen'
506
+ const icon = (
507
+ <svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
508
+ <path d={isFullscreen ? EXIT_ICON : ENTER_ICON} />
509
+ </svg>
510
+ )
511
+
512
+ // The same event the built-in fullscreen button records, for entering and for leaving
513
+ const toggle = () => {
514
+ analytics.push(FrontendAnalyticsFactory.FullScreenModeToggled())
515
+ setFullscreen(!isFullscreen)
516
+ }
378
517
 
379
518
  return (
380
- <Button
381
- onClick={() => setFullscreen(!isFullscreen)}
382
- startIcon={
519
+ <Tooltip title={compact ? label : ''}>
520
+ <Button
521
+ onClick={toggle}
522
+ aria-label={compact ? label : undefined}
523
+ startIcon={compact ? undefined : icon}
524
+ sx={{
525
+ minBlockSize: 44,
526
+ minInlineSize: 44,
527
+ paddingInline: compact ? 0 : 2.5,
528
+ borderRadius: 999,
529
+ bgcolor: 'background.paper',
530
+ color: 'text.primary',
531
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
532
+ textTransform: 'none',
533
+ fontWeight: 600,
534
+ '&:hover': { bgcolor: 'background.paper', boxShadow: '0 6px 20px rgba(0, 0, 0, 0.18)' },
535
+ }}
536
+ >
537
+ {compact ? icon : label}
538
+ </Button>
539
+ </Tooltip>
540
+ )
541
+ }
542
+ ```
543
+
544
+ The button stays in the free area in both states, so the way back is always where the user left it. [Interaction Modes](?path=/docs/ux-guidelines-interaction-modes--docs#full-screen-preview) describes what fullscreen hides.
545
+
546
+ ## Undo and redo
547
+
548
+ The customer tries the four finishes against each other, so the layout offers **Undo** and **Redo** at the top right of the free area, beside the fullscreen button. `phx.frontend.useUndoRedo()` returns `canUndo` and `canRedo`, whether the history has a change to undo or to redo, and `undo` and `redo`, which change the configuration back or forward as the built-in toolbar's buttons do and push the same analytics events:
549
+
550
+ ```tsx
551
+ import { Box, IconButton, Tooltip } from '@mui/material'
552
+
553
+ import { phx } from '../../.phoenix/phx.js'
554
+
555
+ const UNDO_ICON = 'M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11'
556
+ const REDO_ICON = 'M15 14l5-5-5-5M20 9H9.5a5.5 5.5 0 0 0 0 11H13'
557
+
558
+ interface HistoryButtonProps {
559
+ readonly label: string
560
+ readonly icon: string
561
+ readonly enabled: boolean
562
+ readonly onClick: () => void
563
+ }
564
+
565
+ const HistoryButton = ({ label, icon, enabled, onClick }: HistoryButtonProps) => (
566
+ <Tooltip title={label}>
567
+ {/* A disabled button fires no pointer events, so the span keeps the tooltip working */}
568
+ <span>
569
+ <IconButton
570
+ aria-label={label}
571
+ disabled={!enabled}
572
+ onClick={onClick}
573
+ sx={{ inlineSize: 44, blockSize: 44, color: 'text.primary' }}
574
+ >
383
575
  <svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
384
- <path d={isFullscreen ? EXIT_ICON : ENTER_ICON} />
576
+ <path d={icon} />
385
577
  </svg>
386
- }
578
+ </IconButton>
579
+ </span>
580
+ </Tooltip>
581
+ )
582
+
583
+ // Undo and redo for the customer's choices. They change the configuration, so they are offered only where the cards
584
+ // are: not to a viewer, and not in the review step, where the summary and a running request must not change under the
585
+ // customer. `useUndoRedo` pushes the same analytics events as the built-in toolbar's buttons.
586
+ export const UndoRedo = () => {
587
+ const { canUndo, canRedo, undo, redo } = phx.frontend.useUndoRedo()
588
+ const { accessInfo } = phx.frontend.useAccessService()
589
+ const navigationState = phx.frontend.useActiveNavigationState()
590
+
591
+ if (accessInfo.viewMode || navigationState !== 'configure') {
592
+ return null
593
+ }
594
+
595
+ return (
596
+ <Box
597
+ component="div"
598
+ role="group"
599
+ aria-label="History"
387
600
  sx={{
388
- minBlockSize: 44,
389
- paddingInline: 2.5,
601
+ display: 'flex',
390
602
  borderRadius: 999,
391
603
  bgcolor: 'background.paper',
392
- color: 'text.primary',
393
604
  boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
394
- textTransform: 'none',
395
- fontWeight: 600,
396
- '&:hover': { bgcolor: 'background.paper', boxShadow: '0 6px 20px rgba(0, 0, 0, 0.18)' },
397
605
  }}
398
606
  >
399
- {isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
400
- </Button>
607
+ <HistoryButton label="Undo" icon={UNDO_ICON} enabled={canUndo} onClick={undo} />
608
+ <HistoryButton label="Redo" icon={REDO_ICON} enabled={canRedo} onClick={redo} />
609
+ </Box>
401
610
  )
402
611
  }
403
612
  ```
404
613
 
405
- The button stays in the free area in both states, so the way back is always where the user left it. [Interaction Modes](?path=/docs/ux-guidelines-interaction-modes--docs#full-screen-preview) describes what fullscreen hides.
614
+ - Each choice of a finish is one step of the history, whether it is made on the cards or on the button on the table's edge; undoing it moves the check mark back, changes the edge back and restores the price.
615
+ - Each button is disabled while there is nothing to undo or redo, and keeps its tooltip and accessible name. A disabled MUI button receives no pointer events, so a `span` around it carries the tooltip.
616
+ - The hook does not look at read mode or at the navigation state. The buttons change the configuration, so the component renders them only while the cards are shown: not to a viewer, not in the review step, where the summary and a running request must not change under the customer, and not in the confirmation. The layout also leaves them out in fullscreen, which gives the canvas to the product and keeps only the controls placed on it or for looking at it. The button on the table's edge stays there and still changes the finish; the history keeps that change, so **Undo** undoes it after fullscreen is left.
406
617
 
407
- ## Serve read mode
618
+ ## Show messages
408
619
 
409
- While `accessInfo.viewMode` is `true` the panel marks the view as view only, replaces the cards with the chosen finish and shows the price without an action. The hooks do not hide or disable anything in read mode: what a viewer can change is decided by what the layout renders. Rotating and zooming the scene, and fullscreen, stay available, as [Interaction Modes](?path=/docs/ux-guidelines-interaction-modes--docs#view-only-mode) recommends.
620
+ When a change is rejected because it breaks an invariant, when a committed change carries a hint, when an error is thrown while a change is validated, or when saving the configuration fails and later succeeds again, the configurator has a message for the customer. `phx.frontend.useConfiguratorMessages(onMessage)` calls `onMessage` once for each, with the text already in the customer's language, and the layout shows it its own way. Here a toast shows a message for six seconds, colored by its severity, at the bottom left of the free area above the view controls on a desktop, and at its top, below the row of buttons, on a phone:
410
621
 
411
- ## The spec the layout reads
622
+ ```tsx
623
+ import { Alert } from '@mui/material'
624
+ import { useEffect, useState } from 'react'
412
625
 
413
- The layout reads three components of the root entity and one navigation state; the scene components are in the complete implementation below.
626
+ import type { ConfiguratorMessage } from '@salsita-ai/sdk/react'
414
627
 
415
- ```yaml
416
- navigationStates:
417
- - configure
418
- - checkout
628
+ import { phx } from '../../.phoenix/phx.js'
629
+
630
+ const VIOLATION_SEVERITY = { fatal: 'error', fixable: 'error', warning: 'warning', notice: 'info' } as const
631
+ const RANK = { info: 0, warning: 1, error: 2 } as const
632
+ const VISIBLE_MS = 6000
633
+
634
+ const severityOf = (message: ConfiguratorMessage) => {
635
+ if (message.kind === 'violation') {
636
+ return VIOLATION_SEVERITY[message.violation.severity]
637
+ }
638
+
639
+ return message.kind === 'error' || message.kind === 'saveError' ? 'error' : 'info'
640
+ }
641
+
642
+ export const MessageToast = () => {
643
+ const [message, setMessage] = useState<ConfiguratorMessage | null>(null)
644
+
645
+ // A rejected change also reports the notices already present; keep the worst message while it is shown
646
+ phx.frontend.useConfiguratorMessages(next => {
647
+ setMessage(shown => (shown !== null && RANK[severityOf(next)] < RANK[severityOf(shown)] ? shown : next))
648
+ })
649
+
650
+ useEffect(() => {
651
+ if (message === null) {
652
+ return
653
+ }
654
+
655
+ const timeout = setTimeout(() => setMessage(null), VISIBLE_MS)
656
+
657
+ return () => clearTimeout(timeout)
658
+ }, [message])
659
+
660
+ if (message === null) {
661
+ return null
662
+ }
663
+
664
+ return (
665
+ <Alert
666
+ severity={severityOf(message)}
667
+ onClose={() => setMessage(null)}
668
+ sx={{ maxInlineSize: 420, borderRadius: 3, boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)' }}
669
+ >
670
+ {message.text}
671
+ </Alert>
672
+ )
673
+ }
674
+ ```
675
+
676
+ The hook takes a new function on every render without missing a message, so passing the state setter, or an inline function, is fine. `message.kind` is `'violation'`, `'sideEffect'`, `'error'`, `'saveError'` or `'saveRecovery'`; the toast shows an error and a failed save as errors, a hint and the save that succeeded again as information. A violation also carries `entityId` and the `violation` itself, with its `severity`. A rejected change brings one message for each violation of the rejected state, including notices and warnings that were already present, so the toast keeps the most severe message while it is shown instead of letting a later notice replace the reason for the rejection. The layout mounts the toast in the safe-area element, which sets `pointerEvents: 'none'`, so the toast's wrapper switches pointer events back on for the close button.
419
677
 
678
+ To give the toast something to show, the spec registers one transformer. Choosing **Tree trunk** keeps the change and adds a hint to it, which arrives as a `sideEffect` message:
679
+
680
+ ```ts
681
+ import { msg } from '@lingui/core/macro'
682
+
683
+ import { phx } from '../../.phoenix/phx.js'
684
+
685
+ // A tree-trunk edge is cut by hand; choosing it carries a hint, which reaches the layout as a side-effect message
686
+ export default phx.frontend.transformerBuilder.when
687
+ .entity(['Table'])
688
+ .modified({ atFields: ['edgeFinish'] })
689
+ .then.overrideMatchingUpdate(({ update }) => {
690
+ if (update.stateDelta.edgeFinish !== 'TreeTrunk') {
691
+ return [update]
692
+ }
693
+
694
+ return [{ ...update, hint: msg`Tree trunk edges are cut by hand, so delivery takes three weeks longer.` }]
695
+ })
696
+ .build('tree-trunk-delivery')
697
+ ```
698
+
699
+ [Configurator Interface → Loaded Configuration UI](?path=/docs/phoenix-configurator-interface--docs#loaded-configuration-ui) lists the messages the hook does not deliver: errors of the built-in finalize and other actions stay snackbars, and a failure to start from a preset is raised on the preset screen.
700
+
701
+ ## Request a quote
702
+
703
+ The spec declares what finalizing means for this product as a `finalizeAction` component, and the project registers the handler it names:
704
+
705
+ ```yaml
420
706
  components:
421
707
  Table:
422
- detail:
423
- main:
424
- title: Linden dining table
425
- subtitle: Solid oak, seats six
426
- description: A solid oak top on slender steel legs, finished with the edge profile of your choice.
427
- price:
428
- main:
429
- price:
430
- $read: $price
431
- attributeSelection:
432
- edgeFinish:
433
- value:
434
- $bind: edgeFinish
435
- options:
436
- - label: Square
437
- description: Crisp and modern
438
- value: None
439
- - label: Facet
440
- description: A light bevel
441
- value: Facet
442
- - label: Round
443
- description: Soft to the touch
444
- value: Round
445
- - label: Tree trunk
446
- description: The natural edge
447
- value: TreeTrunk
708
+ finalizeAction:
709
+ quote:
710
+ variant: primary
711
+ label: Request a quote
712
+ title: Send the configuration to the shop for a quote
713
+ icon: PaperPlaneTilt
714
+ handler: requestQuote
448
715
  ```
449
716
 
450
- `$price` is a custom expression in `src/expressions/table.ts` that adds a surcharge per finish. The example sets no `applicationConfig` and no layout components: `layout`, the header, the sidebar footer, the toolbar, `viewControls` and property pages configure the built-in layout and have no effect on this one.
717
+ ```ts
718
+ import { useCallback } from 'react'
451
719
 
452
- ## Try the example
720
+ import { phx } from '../../.phoenix/phx.js'
453
721
 
454
- To see read mode, open **Mocks** in the editor rail on the left and apply the **Read User** template.
722
+ // Stands in for the shop's quote service: the request takes a moment, then the panel shows the confirmation
723
+ export default phx.frontend.createPhoenixHandler('requestQuote', () => {
724
+ const navigate = phx.frontend.useNavigate()
455
725
 
456
- ## Test cases
726
+ return {
727
+ handler: useCallback(async () => {
728
+ await new Promise(resolve => setTimeout(resolve, 1500))
729
+ navigate('requested')
730
+ }, [navigate]),
731
+ }
732
+ })
733
+ ```
457
734
 
458
- - The panel is on the left and the table is centred in the area to the right of it, not in the whole canvas.
459
- - Choosing **Tree trunk** moves the check mark to that card, changes the edge of the tabletop and raises the price from $1,290.00 to $1,630.00.
460
- - Tab reaches the selected card, and the arrow keys move the selection to the next or previous card.
461
- - Dragging in the area to the right of the panel orbits the camera.
462
- - **Review and request a quote** replaces the cards with the chosen finish; **Back to the configuration** brings them back.
463
- - **Fullscreen** removes the panel and the table moves to the centre of the canvas; **Exit fullscreen** brings both back.
464
- - With the **Read User** mock the panel says **View only**, shows the chosen finish and the price, and has no button.
735
+ The handler stands in for the shop's quote service: it waits a moment, as a request would, and then enters the `requested` navigation state, in which the panel thanks the customer. A handler is a hook factory, so it can use any hook, here `phx.frontend.useNavigate()`.
465
736
 
466
- ## Known gaps
737
+ The review step offers the spec's primary finalize action, or the first one when none is primary, with `phx.frontend.useFinalizeActions()`. The hook lists the active `finalizeAction` components with what a button needs, the label and the title resolved to the customer's language, and runs their handlers itself, beside the layout, so the buttons need no handler boundary and the component may be called anywhere and as often as the layout likes. **Back to the configuration** lives in the same component, so it can be disabled while the request is pending.
467
738
 
468
- A project layout has only what it renders. This one leaves out the following, each of which a product may need.
739
+ ```tsx
740
+ import { Alert, Box, Button, CircularProgress, Typography } from '@mui/material'
469
741
 
470
- - **Phone width.** The panel is written for desktop: on a narrow screen it covers the scene. A phone layout puts the panel in a bottom sheet and moves the safe area above it.
471
- - **Error, violation and side-effect messages.** The layout mounts nothing that shows them. `LayoutSlots.Services` raises invariant violations and side effects as snackbars, and shows "Oops, something went wrong" for an error thrown while the state is validated, for example by a custom expression, which any spec with custom expressions can hit, this one included; `LayoutSlots.DesktopSnackbarsRoot` is the element the snackbars are rendered into. A layout mounts both, or reads `phx.frontend.useStateValidity()` and shows them its own way.
472
- - **A finalize page from the spec.** The review step here is the layout's own and submits nothing. It does not show a `finalizePage` component and does not check `phx.frontend.useFinalizeViolations()`. [Finalization](?path=/docs/phoenix-finalization--docs) covers what a real flow does.
473
- - **Toolbar actions.** Undo and redo, camera reset, measurements, sharing and language switching are not offered. A layout can mount `LayoutSlots.DesktopToolbar` for the toolbar the spec configures, or build its own buttons.
474
- - **View controls, tool controls and leaving a tool mode.** A spec can configure view controls and tool controls, and a tool mode has a button that leaves it. The built-in layout places them with `LayoutSlots.DesktopViewControls`, `LayoutSlots.DesktopToolControls` and `LayoutSlots.DesktopCallToAction`; this layout mounts none of them, so they do not appear.
475
- - **A project's layout dialog.** A dialog the project registers with `phx.frontend.createLayoutDialog` is shown by `LayoutDialog`, which the built-in layout mounts. This layout does not; a layout that needs the dialog mounts `LayoutDialog` from `@salsita-ai/sdk/configurator`.
476
- - **Selection.** The table has no parts to select. A product with selectable parts reads `phx.frontend.useSelectedDomainEntityId()` and shows the controls of the selected part.
477
- - **Clone to edit in read mode.** With `shareCloneToEdit` set, the built-in layout's `LayoutSlots.DesktopReadonlyBanner` offers **Edit configuration** to a viewer; this layout's "View only" label offers nothing. A layout mounts the banner, or builds its own action with `LayoutSlots.useIsCloningAllowed()` and `LayoutSlots.CloneToEditDialog`.
478
- - **Back and close.** The built-in header's back button returns to the product choice, runs the host's close action or leaves the expanded view, whichever applies. This layout has none of them. `useCloseAction()` from `@salsita-ai/sdk/configurator` returns the host's close action, or `null` when there is none; the hook that chooses among the three is not exported.
479
- - **Analytics.** The built-in fullscreen and finalize buttons record `FullScreenModeToggled` and `FinalizeButtonClicked`; this layout's buttons record nothing. A layout that wants the events pushes `FrontendAnalyticsFactory.FullScreenModeToggled()` and the like, from `@salsita-ai/sdk/configurator`, with `phx.frontend.useAnalytics().push(...)`.
480
- - **Preview start screen.** The built-in layout shows a preview while `usePreview().isPreview` is set, which `preview: auto` does on phones without the project opting in. This layout ignores it and always renders the interface.
481
- - **Assistant panel.** A project layout gets it only with `LayoutSlots.DesktopSidebar`, the built-in sidebar as one piece, see [Sidebar in a Custom Layout](?path=/docs/layouts-sidebar-in-a-custom-layout--docs); this layout does not use it.
482
- - **"Powered by Salsita" mark.** The built-in layout shows it; a project layout decides whether to mount `LayoutSlots.DesktopPoweredBy`, and this one does not.
742
+ import { phx } from '../../.phoenix/phx.js'
483
743
 
484
- ## Related pages
744
+ const BUTTON_SX = { minBlockSize: 48, borderRadius: 999, textTransform: 'none', fontWeight: 600 } as const
485
745
 
486
- - [Configurator Interface → Loaded Configuration UI](?path=/docs/phoenix-configurator-interface--docs#loaded-configuration-ui) is the reference for the element name, the minimal layout and the SDK pieces a layout can mount.
487
- - [Universal](?path=/docs/layouts-universal--docs) is the built-in layout this one replaces.
488
- - [Header / Custom](?path=/docs/layouts-header-custom--docs) replaces only the header, with the price from `useRootEntityPrice()`.
489
- - [Canvas Padding](?path=/docs/layouts-canvas-padding--docs) scales the gaps `useSceneSafeArea()` measures.
490
- - [Presets / Custom](?path=/docs/layouts-presets-custom--docs) replaces the preset screen the same way.
746
+ interface ReviewActionsProps {
747
+ readonly onBack: () => void
748
+ }
491
749
 
492
- ## Complete Implementation
750
+ // The review step's buttons: the spec's primary finalize action and the way back. `useFinalizeActions` runs the
751
+ // action's handler beside the layout, so its pending state and error outlive this component: when the panel unmounts,
752
+ // as in fullscreen, the request goes on and the remounted buttons still show it. Back is disabled while the request
753
+ // is pending, so the customer cannot change the configuration while it is sent.
754
+ export const ReviewActions = ({ onBack }: ReviewActionsProps) => {
755
+ const { primary, hints } = phx.frontend.useFinalizeActions()
756
+ const isPending = primary?.isPending ?? false
493
757
 
494
- {/* AUTO-GENERATED SECTION: This section is automatically updated by 'phoenix story build' */}
758
+ return (
759
+ <Box component="div" sx={{ display: 'grid', gap: 2 }}>
760
+ {primary && (
761
+ <Box component="div" sx={{ display: 'grid', gap: 1 }}>
762
+ <Button
763
+ size="large"
764
+ variant="contained"
765
+ disabled={primary.disabled || isPending}
766
+ onClick={() => void primary.run()}
767
+ title={primary.title}
768
+ startIcon={isPending ? <CircularProgress size={18} color="inherit" /> : undefined}
769
+ sx={BUTTON_SX}
770
+ >
771
+ {isPending ? 'Sending the request…' : primary.label}
772
+ </Button>
773
+ {primary.disabled && hints && (
774
+ <Typography variant="caption" color="text.secondary">
775
+ {hints}
776
+ </Typography>
777
+ )}
778
+ {primary.error && <Alert severity="error">The request could not be sent: {primary.error.message}</Alert>}
779
+ </Box>
780
+ )}
781
+ <Button size="large" variant="outlined" disabled={isPending} onClick={onBack} sx={BUTTON_SX}>
782
+ Back to the configuration
783
+ </Button>
784
+ </Box>
785
+ )
786
+ }
787
+ ```
495
788
 
496
- <details>
497
- <summary>Click to view the complete implementation files</summary>
789
+ - `primary` is the first `'primary'` action, or else the first action, or `null` when the spec has none; the component then shows only **Back to the configuration**. `secondary` holds the others, which this product does not have.
790
+ - `run()` records the finalize-button click for analytics and runs the handler; it does nothing while `disabled` or `isPending`, and returns a promise that never rejects.
791
+ - While `isPending` the button shows a spinner and cannot be clicked again, and **Back to the configuration** is disabled, so the customer cannot change the configuration while it is sent. The configurator keeps `isPending` and `error` while the layout's components unmount: entering fullscreen during the request removes the panel, and the panel shown after fullscreen still shows the spinner until the handler settles.
792
+ - `error` is what the handler rejected with. The layout shows it under the button; no snackbar is raised. It is cleared when the customer tries again.
793
+ - `disabled` and `hints` come from the configuration's violations: while a `warning` or worse holds, or the configuration failed to save, finalizing is not possible and `hints` says why.
498
794
 
499
- ### ./spec/domain.yaml
795
+ The review step stays: a quote request is a commitment, so the customer sees the summary first, and a real shop collects the contact details there. A product that finalizes without a review offers the button in place of **Review and request a quote**. [Configurator Interface → Hooks for a Project Layout](?path=/docs/phoenix-configurator-interface--docs#hooks-for-a-project-layout) describes the hook, and `phx.frontend.useFinalizeAction(handler)`, which runs one known action inside the layout instead, and [Finalization](?path=/docs/phoenix-finalization--docs) what a real flow does on the server.
500
796
 
501
- ```yaml
502
- model:
503
- latest:
504
- entityTypes:
505
- - Table
506
- aliases:
507
- RootEntity:
508
- - Table
509
- commonTypes:
510
- EdgeFinish:
511
- enum:
512
- - None
513
- - Facet
514
- - Round
515
- - TreeTrunk
516
- shapes:
517
- Table:
518
- edgeFinish:
519
- $ref: '#/model/latest/commonTypes/EdgeFinish'
797
+ ## Serve read mode
520
798
 
521
- presets:
522
- - rootEntityType: Table
523
- initialDomainEntities:
524
- - id: Table-1
525
- type: Table
526
- edgeFinish: None
799
+ A configuration is in read mode by one of two routes, and the layout does not need to know which:
527
800
 
528
- navigationStates:
529
- - configure
530
- - checkout
801
+ - **A configuration shared for viewing.** The configuration is opened with `read-revision` access, as a viewer of a shared link opens it. The `read` mock template sets that access, and the **Read Mode** story below opens it.
802
+ - **The host page sets `readonly`.** The page that embeds the configurator sets the web component's `readonly` property, here on a configuration the user owns. The mock template `mocks/readonly-host.yaml`, **Host sets readonly** in the editor's **Mocks** panel, keeps `access: owner` and sets the property with `webComponentProperties: { readonly: true }`.
531
803
 
532
- components:
533
- Table:
534
- detail:
535
- main:
536
- title: Linden dining table
537
- subtitle: Solid oak, seats six
538
- description: A solid oak top on slender steel legs, finished with the edge profile of your choice.
539
- price:
540
- main:
541
- price:
542
- $read: $price
543
- attributeSelection:
544
- edgeFinish:
545
- value:
546
- $bind: edgeFinish
547
- options:
548
- - label: Square
549
- description: Crisp and modern
804
+ Both routes set `accessInfo.viewMode` to `true`, and both switch the spec's filter mode to read, so a component filtered with `$filter.modes: [owner]` is left out either way. While `viewMode` is `true` the panel marks the view as view only, replaces the cards with the chosen finish and shows the price without an action; neither the finalize action nor undo and redo are offered, since `useFinalizeActions` and `useUndoRedo` do not check read mode themselves. The hooks do not hide or disable anything in read mode: what a viewer can change is decided by what the layout renders. Rotating and zooming the scene, and fullscreen, stay available, as [Interaction Modes](?path=/docs/ux-guidelines-interaction-modes--docs#view-only-mode) recommends. The button on the table's edge is hidden for viewers by the spec, see [Put a control on the product](#put-a-control-on-the-product).
805
+
806
+ A viewer of a shared configuration has no way here to edit a copy of it; see **Clone to edit in read mode** in [Known gaps](#known-gaps).
807
+
808
+ ## Fit a phone
809
+
810
+ A panel 380 pixels wide beside the scene leaves nothing of the table on a phone. Below a desktop width the layout puts the panel into a sheet at the bottom and lets the camera frame the table in the area above it. The layout above shows both arrangements.
811
+
812
+ **The switch.** `phx.frontend.useActiveViewports()` returns the width classes the configurator's container satisfies. The container decides, not the window, so a configurator embedded in a narrow column of a wide page gets the phone arrangement too. The built-in layout switches on `desktop` as well.
813
+
814
+ | Value | When it is active |
815
+ | ----------- | ---------------------------------------------------------------------------------- |
816
+ | `phone` | The container is narrower than 480 pixels. |
817
+ | `mobile` | The container is at most 768 pixels wide. |
818
+ | `tablet` | The container is wider than 480 and narrower than 1024 pixels. |
819
+ | `desktop` | The container is wider than 768 pixels; `mobile` and `desktop` exclude each other. |
820
+ | `landscape` | The browser window, not the container, is wider than it is high. |
821
+ | `portrait` | The browser window, not the container, is higher than it is wide. |
822
+
823
+ The hook returns `null` until the container has been measured, and the layout renders only the scene until then, so a phone never shows the desktop panel for a frame.
824
+
825
+ **The sheet.** On a phone the layout is a column: the safe-area element takes the height the sheet leaves, and the sheet sits below it. The sheet has two positions. Collapsed, it shows the name, the price and the action; open, it shows the description and the cards too. The `collapsed` prop of the panel hides the description and the cards, and nothing else, and `inSheet` keeps the top of the cards in view. The sheet scrolls what does not fit, so the price, the review step, **Request a quote** and the thank-you message are reachable in both positions.
826
+
827
+ ```tsx
828
+ import { Box, ButtonBase } from '@mui/material'
829
+ import { type MouseEvent, type PointerEvent, type ReactNode, useRef } from 'react'
830
+
831
+ // A drag shorter than this is a tap, and the handle's click toggles the sheet
832
+ const TAP_DISTANCE = 6
833
+ // A drag that ends this far from where it started snaps to the other position
834
+ const SNAP_DISTANCE = 48
835
+ // A drag faster than this, in pixels per millisecond, snaps in its direction even when it is short
836
+ const SNAP_VELOCITY = 0.5
837
+ // How far the sheet follows the finger towards the position it is already in
838
+ const OVERDRAG = 0.25
839
+
840
+ interface BottomSheetProps {
841
+ readonly open: boolean
842
+ readonly onOpenChange: (open: boolean) => void
843
+ readonly label: string
844
+ readonly children: ReactNode
845
+ }
846
+
847
+ interface Drag {
848
+ readonly startY: number
849
+ readonly startTime: number
850
+ delta: number
851
+ }
852
+
853
+ // A persistent sheet at the bottom of the layout with two positions, open and collapsed. It is not a modal: the scene
854
+ // above it stays interactive. Its height comes from its content, so the layout's safe area ends where the sheet
855
+ // starts and changes only when the sheet snaps. While the handle is dragged the sheet follows the finger with a
856
+ // transform, which the safe area does not see, so the camera refits once per snap and not on every move.
857
+ export const BottomSheet = ({ open, onOpenChange, label, children }: BottomSheetProps) => {
858
+ const sheetRef = useRef<HTMLElement>(null)
859
+ const dragRef = useRef<Drag | null>(null)
860
+ const draggedRef = useRef(false)
861
+
862
+ const moveSheet = (offset: number, animate: boolean) => {
863
+ if (sheetRef.current) {
864
+ sheetRef.current.style.transition = animate ? 'transform 200ms ease-out' : 'none'
865
+ sheetRef.current.style.transform = offset === 0 ? '' : `translateY(${offset}px)`
866
+ }
867
+ }
868
+
869
+ const handlePointerDown = (event: PointerEvent<HTMLButtonElement>) => {
870
+ event.currentTarget.setPointerCapture(event.pointerId)
871
+ dragRef.current = { startY: event.clientY, startTime: event.timeStamp, delta: 0 }
872
+ draggedRef.current = false
873
+ }
874
+
875
+ const handlePointerMove = (event: PointerEvent<HTMLButtonElement>) => {
876
+ const drag = dragRef.current
877
+
878
+ if (drag === null) {
879
+ return
880
+ }
881
+
882
+ drag.delta = event.clientY - drag.startY
883
+ // Down closes an open sheet and up opens a collapsed one; the other direction only gives a little
884
+ const towardsOther = open ? drag.delta > 0 : drag.delta < 0
885
+ moveSheet(towardsOther ? drag.delta : drag.delta * OVERDRAG, false)
886
+ }
887
+
888
+ const handlePointerEnd = (event: PointerEvent<HTMLButtonElement>) => {
889
+ const drag = dragRef.current
890
+ dragRef.current = null
891
+
892
+ if (drag === null) {
893
+ return
894
+ }
895
+
896
+ moveSheet(0, true)
897
+
898
+ if (Math.abs(drag.delta) < TAP_DISTANCE) {
899
+ return
900
+ }
901
+
902
+ draggedRef.current = true
903
+ const velocity = drag.delta / Math.max(event.timeStamp - drag.startTime, 1)
904
+ const flicked = Math.abs(drag.delta) >= SNAP_DISTANCE || Math.abs(velocity) >= SNAP_VELOCITY
905
+
906
+ if (flicked && (open ? drag.delta > 0 : drag.delta < 0)) {
907
+ onOpenChange(!open)
908
+ }
909
+ }
910
+
911
+ // A tap, Enter or Space toggles the sheet. The pointer click that ends a drag has already been handled; a keyboard
912
+ // click (`detail` 0) always toggles, also after a touch drag that was followed by no click.
913
+ const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
914
+ const dragged = draggedRef.current
915
+ draggedRef.current = false
916
+
917
+ if (dragged && event.detail !== 0) {
918
+ return
919
+ }
920
+
921
+ onOpenChange(!open)
922
+ }
923
+
924
+ return (
925
+ <Box
926
+ component="section"
927
+ ref={sheetRef}
928
+ aria-label={label}
929
+ sx={{
930
+ position: 'relative',
931
+ flexShrink: 0,
932
+ display: 'flex',
933
+ flexDirection: 'column',
934
+ maxBlockSize: '55%',
935
+ paddingBlockEnd: 'env(safe-area-inset-bottom)',
936
+ borderStartStartRadius: 20,
937
+ borderStartEndRadius: 20,
938
+ bgcolor: 'background.paper',
939
+ boxShadow: '0 -8px 32px rgba(0, 0, 0, 0.12)',
940
+ // Fills the gap below the sheet while it is dragged up
941
+ '&::after': {
942
+ content: '""',
943
+ position: 'absolute',
944
+ insetInline: 0,
945
+ insetBlockStart: '100%',
946
+ blockSize: '100vh',
947
+ bgcolor: 'background.paper',
948
+ },
949
+ }}
950
+ >
951
+ <ButtonBase
952
+ aria-expanded={open}
953
+ disableRipple
954
+ aria-label={open ? `Collapse ${label.toLowerCase()}` : `Expand ${label.toLowerCase()}`}
955
+ onClick={handleClick}
956
+ onPointerDown={handlePointerDown}
957
+ onPointerMove={handlePointerMove}
958
+ onPointerUp={handlePointerEnd}
959
+ onPointerCancel={handlePointerEnd}
960
+ sx={{
961
+ flexShrink: 0,
962
+ blockSize: 36,
963
+ touchAction: 'none',
964
+ '&.Mui-focusVisible': { outline: 2, outlineColor: 'primary.main', outlineOffset: -4, borderRadius: 3 },
965
+ }}
966
+ >
967
+ <Box component="span" sx={{ inlineSize: 40, blockSize: 4, borderRadius: 2, bgcolor: 'text.disabled' }} />
968
+ </ButtonBase>
969
+ {/* A column, so the panel's cards region shrinks first and the price and action stay in view; below the cards
970
+ region's minimum height the content scrolls as a whole, as the review step does on a short phone */}
971
+ <Box
972
+ component="div"
973
+ sx={{
974
+ display: 'flex',
975
+ flexDirection: 'column',
976
+ flex: '1 1 auto',
977
+ minBlockSize: 0,
978
+ overflowY: 'auto',
979
+ overscrollBehavior: 'contain',
980
+ }}
981
+ >
982
+ {children}
983
+ </Box>
984
+ </Box>
985
+ )
986
+ }
987
+ ```
988
+
989
+ - The handle is a real button with `aria-expanded`. A tap, Enter or Space toggles the sheet, and dragging it up or down snaps the sheet to the other position when the drag is long or fast enough. `touchAction: 'none'` is set on the handle only, so the cards still scroll by touch.
990
+ - The sheet's height comes from its content and is at most 55% of the configurator; when the content does not fit, the cards region shrinks until only the top of the cards shows and scrolls, and below that the sheet's content scrolls as a whole. On a phone with room for the header, the top of the cards and the footer, the price and the action stay in view with the sheet open; on a shorter one the customer scrolls the sheet to them, and the review step's buttons are reached the same way in both positions. A scroll on the cards that reaches their end moves the sheet on. Because the safe-area element is its neighbour in the column, the free area ends where the sheet starts without anything being measured by the layout.
991
+ - While the handle is dragged the sheet follows the finger with a `transform`. `phx.frontend.useSceneSafeArea()` does not see a transform, so the free area changes once, when the sheet snaps, and the camera refits once. A safe area that followed the finger would refit the camera on every move and reset an orbit the customer made.
992
+ - The sheet is not a modal: no backdrop covers the scene, so the table can be orbited with one finger and zoomed with two while the sheet is open. MUI's temporary `Drawer` and `SwipeableDrawer` are modals and would block the scene; their persistent and permanent variants have no backdrop, but no drag handle with two positions either.
993
+
994
+ **What else moves.** The toast sits at the top of the free area, below the row of undo, redo and fullscreen buttons, so it does not cover the sheet's handle, and the fullscreen button shows only its icon, with its label as tooltip and accessible name. Fullscreen removes the sheet as it removes the panel on a desktop. The sheet keeps its position while fullscreen is on.
995
+
996
+ **A camera for the phone.** The desktop camera is close enough to fill a wide free area, and on a narrow one it cuts off the ends of the table. So each camera has a filter on the viewport: the desktop camera `main` applies while `desktop` is active, and a second camera, `phone`, with a larger distance frames the whole table while `mobile` is. The filters exclude each other once the container is measured. Before that, for example when an image of the configuration is rendered, no viewport is known and a viewport filter matches, so both cameras apply and the higher priority of `main` decides:
997
+
998
+ ```yaml
999
+ components:
1000
+ Table:
1001
+ fitRootPerspectiveCamera:
1002
+ main:
1003
+ priority: 2
1004
+ fov: 35
1005
+ near: 100
1006
+ far: 10000
1007
+ minDistance: 1000
1008
+ maxDistance: 9000
1009
+ distance: 5000
1010
+ azimuthAngle: 1.5
1011
+ polarAngle: 1.1
1012
+ fitMeshChanges: true
1013
+ $filter:
1014
+ viewports: [desktop]
1015
+ phone:
1016
+ priority: 1
1017
+ fov: 35
1018
+ near: 100
1019
+ far: 10000
1020
+ minDistance: 1000
1021
+ maxDistance: 12000
1022
+ distance: 8000
1023
+ azimuthAngle: 1.5
1024
+ polarAngle: 1.1
1025
+ fitMeshChanges: true
1026
+ $filter:
1027
+ viewports: [mobile]
1028
+ ```
1029
+
1030
+ ## Put a control on the product
1031
+
1032
+ The edge finish is a detail of the tabletop, so the layout also offers it on the table itself: a round button on the middle of the long edge opens the four finishes next to the edge. On a phone it is a way to change the edge without opening the sheet.
1033
+
1034
+ A control in the scene is a spec component that names an element of the project. `htmlHotspot` places an HTML element at an offset from the entity's origin, in the model's units, here millimetres: half the table's depth towards the camera and just below the top's surface. Its `$filter` shows it only to the owner and only while the `configure` navigation state is active, so a viewer and the review step do not get it, and the element does not check either itself:
1035
+
1036
+ ```yaml
1037
+ components:
1038
+ Table:
1039
+ htmlHotspot:
1040
+ edgeFinish:
1041
+ name: EdgeFinishHotspot
1042
+ offsetX: 500
1043
+ offsetY: 800
1044
+ offsetZ: 0
1045
+ $filter:
1046
+ modes: [owner]
1047
+ navigationStates: [configure]
1048
+ ```
1049
+
1050
+ The element is registered with `phx.frontend.createCustomPhoenixEntityElement` under the name the spec gives. It receives the id of the entity the hotspot belongs to, and reads the same `attributeSelection` the panel reads:
1051
+
1052
+ ```tsx
1053
+ import { Box, ButtonBase, Popover, Tooltip } from '@mui/material'
1054
+ import { useState } from 'react'
1055
+
1056
+ import { phx } from '../../.phoenix/phx.js'
1057
+ import { EdgeFinishCards } from '../_shared/EdgeFinishCards.js'
1058
+
1059
+ // The edge profile the button shows, the square edge of the cards' drawings
1060
+ const EDGE_ICON = 'M3 7H17L21 11V13L17 17H3Z'
1061
+
1062
+ // Rendered by the spec's `htmlHotspot` on the table's edge: a button on the product that opens the edge finishes
1063
+ // next to the edge, with the same cards as the panel. The spec's `$filter` decides when it is shown.
1064
+ export default phx.frontend.createCustomPhoenixEntityElement('EdgeFinishHotspot', ({ entityId }) => {
1065
+ const edgeFinish = phx.frontend.useAttributeSelection(entityId, 'edgeFinish')
1066
+ const [anchor, setAnchor] = useState<HTMLElement | null>(null)
1067
+
1068
+ return (
1069
+ <>
1070
+ <Tooltip title="Edge finish">
1071
+ <ButtonBase
1072
+ aria-label="Edge finish"
1073
+ aria-haspopup="dialog"
1074
+ aria-expanded={anchor !== null}
1075
+ onClick={event => setAnchor(event.currentTarget)}
1076
+ sx={{
1077
+ display: 'grid',
1078
+ placeItems: 'center',
1079
+ inlineSize: 44,
1080
+ blockSize: 44,
1081
+ borderRadius: '50%',
1082
+ bgcolor: 'background.paper',
1083
+ color: 'primary.main',
1084
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.2)',
1085
+ '&.Mui-focusVisible': { outline: 2, outlineColor: 'primary.main', outlineOffset: 2 },
1086
+ }}
1087
+ >
1088
+ <svg viewBox="0 0 24 24" width={22} height={22} aria-hidden>
1089
+ <path d={EDGE_ICON} fill="currentColor" />
1090
+ </svg>
1091
+ </ButtonBase>
1092
+ </Tooltip>
1093
+ <Popover
1094
+ open={anchor !== null}
1095
+ anchorEl={anchor}
1096
+ onClose={() => setAnchor(null)}
1097
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
1098
+ transformOrigin={{ vertical: 'bottom', horizontal: 'center' }}
1099
+ slotProps={{
1100
+ paper: { role: 'dialog', 'aria-label': 'Edge finish', sx: { borderRadius: 4, marginBlockStart: -1 } },
1101
+ }}
1102
+ >
1103
+ <Box component="div" sx={{ inlineSize: 340, maxInlineSize: 'calc(100vw - 32px)', padding: 2 }}>
1104
+ <EdgeFinishCards selection={edgeFinish} />
1105
+ </Box>
1106
+ </Popover>
1107
+ </>
1108
+ )
1109
+ })
1110
+ ```
1111
+
1112
+ - The hotspot is rendered inside the scene, but it has the theme and every `phx.frontend` hook, like the layout. MUI's `Popover` and `Tooltip` render into the configurator's own modal root, so they are styled and work inside its shadow root.
1113
+ - The cards in the popover are the panel's `EdgeFinishCards`, used a second time. A choice changes the edge, the price and the panel's cards at once, and the popover stays open so the customer can compare the finishes on the table.
1114
+ - Keyboard: the button is in the tab order, Enter and Space open the popover and move the focus into it, the arrow keys change the finish, and Escape closes it and returns the focus to the button.
1115
+ - `controlHotspot: true` would hide the hotspot while a change is being applied, which would also unmount the open popover on every choice, so this hotspot does not set it.
1116
+ - The scene's HTML is painted inside the canvas's stacking context, under the layout's panel and sheet. A hotspot that a panel covers cannot be clicked there.
1117
+
1118
+ [Html Hotspots](?path=/docs/controls-scene-html-hotspots--docs) describes the component and its other properties.
1119
+
1120
+ The SDK's own controls on the product, such as `sceneEntityAddFab` and `sceneEntitySelectHotspot`, keep the SDK's look in a project layout too. Each is rendered through an entity element of its own name, so a project gives it the brand's look by registering that name, for example with `phx.frontend.createAddEntityFab`; see [Configurator Interface → In-Scene Controls](?path=/docs/phoenix-configurator-interface--docs#in-scene-controls). `htmlHotspot` is for a control the SDK has none of, like this one.
1121
+
1122
+ ## Control the scene
1123
+
1124
+ The four finishes differ by a few millimetres on a table two metres long, so from the default distance the difference is hard to judge. The layout offers two views, **Whole table** and **Edge close-up**, a close and low look at a corner of the top, and a **Reset view** button that brings back the framing after the customer has orbited, panned or zoomed.
1125
+
1126
+ **Views are view states.** The spec lists them, and each camera says by its `$filter` in which view states and at which widths it applies. The two close-up cameras have the highest priorities and apply only in the `edge` view state, the desktop one with `desktop` and the phone one with `mobile`, a little farther away. In the `overview` view state, the first in the list and so the initial one, only the two whole-table cameras of [Fit a phone](#fit-a-phone) apply. A camera always looks at the middle of the table, so the close-up shows the near corner, where the edge profile is seen on both sides:
1127
+
1128
+ ```yaml
1129
+ viewStates:
1130
+ - overview
1131
+ - edge
1132
+
1133
+ components:
1134
+ Table:
1135
+ fitRootPerspectiveCamera:
1136
+ edge:
1137
+ priority: 4
1138
+ fov: 35
1139
+ near: 100
1140
+ far: 10000
1141
+ minDistance: 800
1142
+ maxDistance: 9000
1143
+ distance: 2000
1144
+ azimuthAngle: 0.5
1145
+ polarAngle: 1.15
1146
+ fitMeshChanges: true
1147
+ $filter:
1148
+ viewports: [desktop]
1149
+ viewStates: [edge]
1150
+ edgePhone:
1151
+ priority: 3
1152
+ fov: 35
1153
+ near: 100
1154
+ far: 10000
1155
+ minDistance: 800
1156
+ maxDistance: 12000
1157
+ distance: 3000
1158
+ azimuthAngle: 0.5
1159
+ polarAngle: 1.15
1160
+ fitMeshChanges: true
1161
+ $filter:
1162
+ viewports: [mobile]
1163
+ viewStates: [edge]
1164
+ ```
1165
+
1166
+ **The buttons.** `phx.frontend.useActiveViewState()` returns the active view state and `phx.frontend.useSetViewState()` a function that makes another one active; the camera then moves to the camera of that view state. `phx.frontend.useResetCamera()` returns a function that moves the camera back to the framing of the active camera, the whole table or the close-up:
1167
+
1168
+ ```tsx
1169
+ import { Box, Button, Tooltip } from '@mui/material'
1170
+
1171
+ import { phx } from '../../.phoenix/phx.js'
1172
+
1173
+ // The spec's view states, each with the label of the button that shows it
1174
+ const VIEWS = [
1175
+ { state: 'overview', label: 'Whole table' },
1176
+ { state: 'edge', label: 'Edge close-up' },
1177
+ ] as const
1178
+
1179
+ const RESET_ICON = 'M4 12a8 8 0 1 0 2.4-5.7M4 4v4h4'
1180
+
1181
+ const SURFACE_SX = {
1182
+ borderRadius: 999,
1183
+ bgcolor: 'background.paper',
1184
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
1185
+ } as const
1186
+
1187
+ const BUTTON_SX = {
1188
+ minBlockSize: 44,
1189
+ minInlineSize: 44,
1190
+ borderRadius: 999,
1191
+ textTransform: 'none',
1192
+ fontWeight: 600,
1193
+ whiteSpace: 'nowrap',
1194
+ } as const
1195
+
1196
+ interface SceneControlsProps {
1197
+ // The reset button shows only its icon, with its label as tooltip and accessible name
1198
+ readonly compact?: boolean
1199
+ // Only the reset button, without the view buttons
1200
+ readonly resetOnly?: boolean
1201
+ }
1202
+
1203
+ // The layout's own camera controls: one button per view state of the spec, and a reset that undoes an orbit
1204
+ export const SceneControls = ({ compact = false, resetOnly = false }: SceneControlsProps) => {
1205
+ const activeView = phx.frontend.useActiveViewState()
1206
+ const setViewState = phx.frontend.useSetViewState()
1207
+ const resetCamera = phx.frontend.useResetCamera()
1208
+ const resetIcon = (
1209
+ <svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
1210
+ <path d={RESET_ICON} />
1211
+ </svg>
1212
+ )
1213
+
1214
+ return (
1215
+ <Box component="div" sx={{ display: 'flex', gap: 1 }}>
1216
+ {!resetOnly && (
1217
+ <Box component="div" role="group" aria-label="View" sx={{ ...SURFACE_SX, display: 'flex' }}>
1218
+ {VIEWS.map(view => {
1219
+ const isActive = activeView === view.state
1220
+
1221
+ return (
1222
+ <Button
1223
+ key={view.state}
1224
+ aria-pressed={isActive}
1225
+ onClick={() => setViewState(view.state)}
1226
+ sx={{
1227
+ ...BUTTON_SX,
1228
+ paddingInline: 2,
1229
+ bgcolor: isActive ? 'text.primary' : 'transparent',
1230
+ color: isActive ? 'background.paper' : 'text.primary',
1231
+ '&:hover': { bgcolor: isActive ? 'text.primary' : 'action.hover' },
1232
+ }}
1233
+ >
1234
+ {view.label}
1235
+ </Button>
1236
+ )
1237
+ })}
1238
+ </Box>
1239
+ )}
1240
+ <Tooltip title={compact ? 'Reset view' : ''}>
1241
+ <Button
1242
+ onClick={resetCamera}
1243
+ aria-label={compact ? 'Reset view' : undefined}
1244
+ startIcon={compact ? undefined : resetIcon}
1245
+ sx={{
1246
+ ...SURFACE_SX,
1247
+ ...BUTTON_SX,
1248
+ paddingInline: compact ? 0 : 2,
1249
+ color: 'text.primary',
1250
+ '&:hover': { bgcolor: 'background.paper', boxShadow: '0 6px 20px rgba(0, 0, 0, 0.18)' },
1251
+ }}
1252
+ >
1253
+ {compact ? resetIcon : 'Reset view'}
1254
+ </Button>
1255
+ </Tooltip>
1256
+ </Box>
1257
+ )
1258
+ }
1259
+ ```
1260
+
1261
+ - The view buttons are a group with `aria-pressed`, so a screen reader announces which view is active, and the active one is drawn dark, not only in another colour.
1262
+ - On a desktop the controls sit at the bottom right of the free area and the toast above its bottom left; on a phone they sit at the bottom of the free area, just above the sheet. On a phone, and on a desktop arrangement below 1024 pixels, where `tablet` is also active and the free area beside the panel is narrow, **Reset view** shows only its icon, with its label as tooltip and accessible name.
1263
+ - In fullscreen the controls stay on a desktop. On a phone the view buttons are hidden and **Reset view** stays, as [Interaction Modes](?path=/docs/ux-guidelines-interaction-modes--docs#full-screen-preview) describes for the built-in layout: on a phone the canvas is small and the product comes first, and the way back to the framed product is still needed after an orbit.
1264
+ - The controls stay in the review step and in read mode: changing the view changes nothing in the configuration, and a viewer may look at the product as closely as the customer.
1265
+ - The hooks need no `viewControls` component. That component configures the built-in layout's view controls and has no effect here.
1266
+
1267
+ ## The spec the layout reads
1268
+
1269
+ The layout reads four components of the root entity and its navigation and view states; the scene components are in the complete implementation below. `SceneControls` names the view states `overview` and `edge` in its code, so they must match the spec's `viewStates`.
1270
+
1271
+ ```yaml
1272
+ navigationStates:
1273
+ - configure
1274
+ - checkout
1275
+ - requested
1276
+
1277
+ viewStates:
1278
+ - overview
1279
+ - edge
1280
+
1281
+ components:
1282
+ Table:
1283
+ detail:
1284
+ main:
1285
+ title: Linden dining table
1286
+ subtitle: Solid oak, seats six
1287
+ description: A solid oak top on slender steel legs, finished with the edge profile of your choice.
1288
+ price:
1289
+ main:
1290
+ price:
1291
+ $read: $price
1292
+ attributeSelection:
1293
+ edgeFinish:
1294
+ value:
1295
+ $bind: edgeFinish
1296
+ options:
1297
+ - label: Square
1298
+ description: Crisp and modern
550
1299
  value: None
551
1300
  - label: Facet
552
1301
  description: A light bevel
@@ -557,9 +1306,171 @@ components:
557
1306
  - label: Tree trunk
558
1307
  description: The natural edge
559
1308
  value: TreeTrunk
1309
+ finalizeAction:
1310
+ quote:
1311
+ variant: primary
1312
+ label: Request a quote
1313
+ title: Send the configuration to the shop for a quote
1314
+ icon: PaperPlaneTilt
1315
+ handler: requestQuote
1316
+
1317
+ transformers:
1318
+ - name: tree-trunk-delivery
1319
+ ```
1320
+
1321
+ `$price` is a custom expression in `src/expressions/table.ts` that adds a surcharge per finish, and `tree-trunk-delivery` is the transformer in `src/transformers/tree-trunk-delivery.ts` that adds the hint shown in [Show messages](#show-messages), and `requestQuote` is the handler in `src/handlers/request-quote.ts`. The example sets no `applicationConfig` and no layout components: `layout`, the header, the sidebar footer, the toolbar, `viewControls` and property pages configure the built-in layout and have no effect on this one. The `finalizeAction` is not one of them: it describes the product's own finalizing, which this layout offers itself.
1322
+
1323
+ ## Try the example
1324
+
1325
+ Each story names the mock template it opens with `args: { mockTemplate }`: **Example** and **Phone** open `default-user`, and **Read Mode** opens `read`. A template applied from the **Mocks** panel lasts until the story is opened again.
1326
+
1327
+ The same layout in a frame 390 pixels wide, as on a phone. Opened on its own from the sidebar, the **Phone** story sets Storybook's viewport to 390 × 844.
1328
+
1329
+ ## Test cases
1330
+
1331
+ - The panel is on the left and the table is centred in the area to the right of it, not in the whole canvas.
1332
+ - Choosing **Tree trunk** moves the check mark to that card, changes the edge of the tabletop and raises the price from $1,290.00 to $1,630.00.
1333
+ - Choosing **Tree trunk** also shows "Tree trunk edges are cut by hand, so delivery takes three weeks longer." at the bottom left of the free area, above the view controls, until its close button is clicked or six seconds pass. Choosing another finish shows nothing.
1334
+ - Tab reaches the selected card, and the arrow keys move the selection to the next or previous card.
1335
+ - Dragging in the area to the right of the panel orbits the camera.
1336
+ - **Review and request a quote** replaces the cards with the chosen finish and offers **Request a quote**; **Back to the configuration** brings the cards back.
1337
+ - **Request a quote** shows a spinner and "Sending the request…" for about a second and a half, with **Back to the configuration** disabled, then "Thank you. Your quote request is on its way, and the shop replies within two working days."; **Back to the configuration** then returns to the cards.
1338
+ - **Fullscreen** removes the panel and the table moves to the centre of the canvas; **Exit fullscreen** brings both back.
1339
+ - In the **Read Mode** story the panel says **View only**, shows the chosen finish and the price, and has no button, so no quote can be requested; **Undo**, **Redo** and the button on the table's edge are not shown, and **Fullscreen**, the view buttons and **Reset view** work.
1340
+ - Applying **Host sets readonly** from the **Mocks** panel in the **Example** story shows the same read mode: **View only**, the chosen finish, no button in the panel, no **Undo**, **Redo** or button on the table's edge. Applying **Default User** brings the cards back.
1341
+ - **Undo** and **Redo** are disabled when the story opens. After choosing **Tree trunk**, **Undo** restores **Square** and $1,290.00 and enables **Redo**; **Redo** chooses **Tree trunk** again at $1,630.00.
1342
+ - **Undo** and **Redo** are not shown in the review step, in the confirmation or in fullscreen.
1343
+ - Clicking **Fullscreen** and **Exit fullscreen** each pushes the `fullscreenButton` user-action event, as an `AnalyticsService.push` line in the browser console shows: without a Google Tag Manager id, Storybook's analytics only log their events with `console.debug`, which the console shows only with its Verbose level enabled.
1344
+ - Entering fullscreen while **Request a quote** is pending and leaving it again shows the panel with the spinner still running, until the thank-you message appears.
1345
+ - A round button sits on the middle of the table's long edge. Clicking it opens the four finishes next to it; choosing **Round** there moves the check mark in the popover and in the panel, rounds the edge and raises the price to $1,410.00. Escape closes the popover.
1346
+ - Tab reaches the button on the edge, and Enter or Space opens the popover.
1347
+ - The button on the edge is gone in the review step, in the confirmation and in both read routes, and stays in fullscreen.
1348
+ - In the **Phone** story the panel is a sheet at the bottom showing the name, the price and **Review and request a quote**, and the whole table is visible above it.
1349
+ - Tapping the sheet's handle, or dragging it up, opens the sheet with the cards; the table moves up once the sheet has snapped and stays fully visible. Dragging the handle down, or tapping it again, collapses the sheet.
1350
+ - On the phone, the review step, **Request a quote** and the thank-you message work with the sheet collapsed and open.
1351
+ - On the phone, choosing **Tree trunk** shows the hint at the top of the free area, below the undo, redo and fullscreen buttons.
1352
+ - On the phone, the fullscreen button removes the sheet and the table moves to the centre of the canvas; pressing it again brings the sheet back in the position it had.
1353
+ - **Edge close-up** moves the camera close to the near corner of the top, where the edge profile shows; the button is drawn dark and the other light. **Whole table** returns to the whole table.
1354
+ - After dragging in the free area to orbit the table, **Reset view** returns to the framing of the active view, the whole table or the close-up.
1355
+ - The view buttons and **Reset view** sit at the bottom right of the free area on a desktop and just above the sheet on a phone, and overlap neither the panel, the sheet, the button on the edge nor the toast.
1356
+ - In fullscreen the view controls stay on a desktop. On a phone the view buttons are hidden and **Reset view** stays; after an orbit it restores the framing.
1357
+
1358
+ ## Known gaps
1359
+
1360
+ A project layout has only what it renders. This one leaves out the following, each of which a product may need. [Layout Slots](?path=/docs/phoenix-layout-slots--docs) describes every `LayoutSlots` member named here.
1361
+
1362
+ - **Messages that stay snackbars.** The toast shows violations, side effects, errors and failed saves, and the finalize button shows its own error. Two kinds of message stay snackbars. An error of a built-in action, such as one from `LayoutSlots.DesktopToolbar`, renders into `LayoutSlots.DesktopSnackbarsRoot`; this layout mounts neither, and a layout that mounts such an action mounts the root too, or the snackbar appears outside the configurator. A failure to start from a preset appears on the preset screen, before this layout mounts; a project replaces that screen with `phx.frontend.createPresetUi`, see [Configurator Interface → Preset UI](?path=/docs/phoenix-configurator-interface--docs#preset-ui).
1363
+ - **Toolbar actions.** Measurements, sharing and language switching are not offered; undo and redo are the layout's own buttons, see [Undo and redo](#undo-and-redo). A layout can mount `LayoutSlots.DesktopToolbar` for the toolbar the spec configures, or build its own buttons: `phx.frontend.useToolbarActions()` returns the spec's toolbar as runnable entries, and `phx.frontend.useUndoRedo()`, `useMeasurements()`, `useShareLinks()` and `useLocaleSwitch()` serve single actions, pushing the same analytics events as the built-in buttons; see [Configurator Interface → Hooks for a Project Layout](?path=/docs/phoenix-configurator-interface--docs#hooks-for-a-project-layout).
1364
+ - **The spec's view controls, tool controls and leaving a tool mode.** A spec can configure view controls and tool controls, and a tool mode has a button that leaves it. The built-in layout places them with `LayoutSlots.DesktopViewControls`, `LayoutSlots.DesktopToolControls` and `LayoutSlots.DesktopCallToAction`; this layout mounts none of them, so the spec's `viewControls` and `toolControls` do not appear. Its views come from its own buttons instead, see [Control the scene](#control-the-scene), and it has no tool modes.
1365
+ - **A project's layout dialog.** A dialog the project registers with `phx.frontend.createLayoutDialog` is shown by `LayoutDialog`, which the built-in layout mounts. This layout does not; a layout that needs the dialog mounts `LayoutDialog` from `@salsita-ai/sdk/configurator`.
1366
+ - **Selection.** The table has no parts to select. A product with selectable parts reads `phx.frontend.useSelectedDomainEntityId()` and shows the controls of the selected part.
1367
+ - **Clone to edit in read mode.** With `shareCloneToEdit` set, the built-in layout's `LayoutSlots.DesktopReadonlyBanner` offers **Edit configuration** to a viewer; this layout's "View only" label offers nothing. A layout mounts the banner, or builds its own action with `phx.frontend.useIsCloningAllowed()` and `LayoutSlots.CloneToEditDialog`. The example leaves it out because Storybook cannot show the result: the dialog creates and opens the copy, but the mock template keeps its `read-revision` access on every configuration it opens, so the copy opens in read mode again.
1368
+ - **Back and close.** The built-in header's back button returns to the product choice, runs the host's close action or leaves the expanded view, whichever applies. This layout has none of them. `phx.frontend.useCloseAction()` returns the host's close action, or `null` when there is none, as in this Storybook example; the hook that chooses among the three is not exported.
1369
+ - **Analytics.** The finalize button records `FinalizeButtonClicked` through `useFinalizeActions`, the cards record the attribute choice through `useAttributeSelection`, undo and redo record their events through `useUndoRedo`, and the fullscreen button records `FullScreenModeToggled` itself. The review step's navigation buttons, the view buttons, **Reset view**, the sheet's handle and the button on the table's edge record nothing. A layout that wants more events pushes them from `FrontendAnalyticsFactory`, from `@salsita-ai/sdk/configurator`, with `phx.frontend.useAnalytics().push(...)`, as [Offer fullscreen](#offer-fullscreen) does.
1370
+ - **Preview start screen.** The built-in layout shows a preview while `phx.frontend.usePreview().isPreview` is set, which `preview: auto` does on phones without the project opting in. This layout ignores it and always renders the interface.
1371
+ - **Assistant panel.** A project layout gets it only with `LayoutSlots.DesktopSidebar`, the built-in sidebar as one piece, see [Sidebar in a Custom Layout](?path=/docs/layouts-sidebar-in-a-custom-layout--docs); this layout does not use it.
1372
+ - **"Powered by Salsita" mark.** The built-in layout shows it; a project layout decides whether to mount `LayoutSlots.DesktopPoweredBy`, and this one does not.
1373
+
1374
+ ## Related pages
1375
+
1376
+ - [Configurator Interface → Loaded Configuration UI](?path=/docs/phoenix-configurator-interface--docs#loaded-configuration-ui) is the reference for the element name, the minimal layout, the SDK pieces a layout can mount and the hooks it reads.
1377
+ - [Universal](?path=/docs/layouts-universal--docs) is the built-in layout this one replaces.
1378
+ - [Header / Custom](?path=/docs/layouts-header-custom--docs) replaces only the header, with the price from `useRootEntityPrice()`.
1379
+ - [Canvas Padding](?path=/docs/layouts-canvas-padding--docs) scales the gaps `phx.frontend.useSceneSafeArea()` measures.
1380
+ - [Presets / Custom](?path=/docs/layouts-presets-custom--docs) replaces the preset screen the same way.
1381
+
1382
+ ## Complete Implementation
1383
+
1384
+ {/* AUTO-GENERATED SECTION: This section is automatically updated by 'phoenix story build' */}
1385
+
1386
+ <details>
1387
+ <summary>Click to view the complete implementation files</summary>
1388
+
1389
+ ### ./spec/domain.yaml
1390
+
1391
+ ```yaml
1392
+ model:
1393
+ latest:
1394
+ entityTypes:
1395
+ - Table
1396
+ aliases:
1397
+ RootEntity:
1398
+ - Table
1399
+ commonTypes:
1400
+ EdgeFinish:
1401
+ enum:
1402
+ - None
1403
+ - Facet
1404
+ - Round
1405
+ - TreeTrunk
1406
+ shapes:
1407
+ Table:
1408
+ edgeFinish:
1409
+ $ref: '#/model/latest/commonTypes/EdgeFinish'
1410
+
1411
+ presets:
1412
+ - rootEntityType: Table
1413
+ initialDomainEntities:
1414
+ - id: Table-1
1415
+ type: Table
1416
+ edgeFinish: None
1417
+
1418
+ navigationStates:
1419
+ - configure
1420
+ - checkout
1421
+ - requested
1422
+
1423
+ viewStates:
1424
+ - overview
1425
+ - edge
1426
+
1427
+ components:
1428
+ Table:
1429
+ detail:
1430
+ main:
1431
+ title: Linden dining table
1432
+ subtitle: Solid oak, seats six
1433
+ description: A solid oak top on slender steel legs, finished with the edge profile of your choice.
1434
+ price:
1435
+ main:
1436
+ price:
1437
+ $read: $price
1438
+ attributeSelection:
1439
+ edgeFinish:
1440
+ value:
1441
+ $bind: edgeFinish
1442
+ options:
1443
+ - label: Square
1444
+ description: Crisp and modern
1445
+ value: None
1446
+ - label: Facet
1447
+ description: A light bevel
1448
+ value: Facet
1449
+ - label: Round
1450
+ description: Soft to the touch
1451
+ value: Round
1452
+ - label: Tree trunk
1453
+ description: The natural edge
1454
+ value: TreeTrunk
1455
+ finalizeAction:
1456
+ quote:
1457
+ variant: primary
1458
+ label: Request a quote
1459
+ title: Send the configuration to the shop for a quote
1460
+ icon: PaperPlaneTilt
1461
+ handler: requestQuote
1462
+ htmlHotspot:
1463
+ edgeFinish:
1464
+ name: EdgeFinishHotspot
1465
+ offsetX: 500
1466
+ offsetY: 800
1467
+ offsetZ: 0
1468
+ $filter:
1469
+ modes: [owner]
1470
+ navigationStates: [configure]
560
1471
  fitRootPerspectiveCamera:
561
1472
  main:
562
- priority: 1
1473
+ priority: 2
563
1474
  fov: 35
564
1475
  near: 100
565
1476
  far: 10000
@@ -569,6 +1480,49 @@ components:
569
1480
  azimuthAngle: 1.5
570
1481
  polarAngle: 1.1
571
1482
  fitMeshChanges: true
1483
+ $filter:
1484
+ viewports: [desktop]
1485
+ phone:
1486
+ priority: 1
1487
+ fov: 35
1488
+ near: 100
1489
+ far: 10000
1490
+ minDistance: 1000
1491
+ maxDistance: 12000
1492
+ distance: 8000
1493
+ azimuthAngle: 1.5
1494
+ polarAngle: 1.1
1495
+ fitMeshChanges: true
1496
+ $filter:
1497
+ viewports: [mobile]
1498
+ edge:
1499
+ priority: 4
1500
+ fov: 35
1501
+ near: 100
1502
+ far: 10000
1503
+ minDistance: 800
1504
+ maxDistance: 9000
1505
+ distance: 2000
1506
+ azimuthAngle: 0.5
1507
+ polarAngle: 1.15
1508
+ fitMeshChanges: true
1509
+ $filter:
1510
+ viewports: [desktop]
1511
+ viewStates: [edge]
1512
+ edgePhone:
1513
+ priority: 3
1514
+ fov: 35
1515
+ near: 100
1516
+ far: 10000
1517
+ minDistance: 800
1518
+ maxDistance: 12000
1519
+ distance: 3000
1520
+ azimuthAngle: 0.5
1521
+ polarAngle: 1.15
1522
+ fitMeshChanges: true
1523
+ $filter:
1524
+ viewports: [mobile]
1525
+ viewStates: [edge]
572
1526
  sceneEntityRemoteObject:
573
1527
  top:
574
1528
  fileUrl: /models/tables.gltf
@@ -611,6 +1565,173 @@ components:
611
1565
  value: 780
612
1566
  scaleOne: 780
613
1567
  scaleZero: 740
1568
+
1569
+ transformers:
1570
+ - name: tree-trunk-delivery
1571
+ ```
1572
+
1573
+ ### ./src/\_shared/BottomSheet.tsx
1574
+
1575
+ ```tsx
1576
+ import { Box, ButtonBase } from '@mui/material'
1577
+ import { type MouseEvent, type PointerEvent, type ReactNode, useRef } from 'react'
1578
+
1579
+ // A drag shorter than this is a tap, and the handle's click toggles the sheet
1580
+ const TAP_DISTANCE = 6
1581
+ // A drag that ends this far from where it started snaps to the other position
1582
+ const SNAP_DISTANCE = 48
1583
+ // A drag faster than this, in pixels per millisecond, snaps in its direction even when it is short
1584
+ const SNAP_VELOCITY = 0.5
1585
+ // How far the sheet follows the finger towards the position it is already in
1586
+ const OVERDRAG = 0.25
1587
+
1588
+ interface BottomSheetProps {
1589
+ readonly open: boolean
1590
+ readonly onOpenChange: (open: boolean) => void
1591
+ readonly label: string
1592
+ readonly children: ReactNode
1593
+ }
1594
+
1595
+ interface Drag {
1596
+ readonly startY: number
1597
+ readonly startTime: number
1598
+ delta: number
1599
+ }
1600
+
1601
+ // A persistent sheet at the bottom of the layout with two positions, open and collapsed. It is not a modal: the scene
1602
+ // above it stays interactive. Its height comes from its content, so the layout's safe area ends where the sheet
1603
+ // starts and changes only when the sheet snaps. While the handle is dragged the sheet follows the finger with a
1604
+ // transform, which the safe area does not see, so the camera refits once per snap and not on every move.
1605
+ export const BottomSheet = ({ open, onOpenChange, label, children }: BottomSheetProps) => {
1606
+ const sheetRef = useRef<HTMLElement>(null)
1607
+ const dragRef = useRef<Drag | null>(null)
1608
+ const draggedRef = useRef(false)
1609
+
1610
+ const moveSheet = (offset: number, animate: boolean) => {
1611
+ if (sheetRef.current) {
1612
+ sheetRef.current.style.transition = animate ? 'transform 200ms ease-out' : 'none'
1613
+ sheetRef.current.style.transform = offset === 0 ? '' : `translateY(${offset}px)`
1614
+ }
1615
+ }
1616
+
1617
+ const handlePointerDown = (event: PointerEvent<HTMLButtonElement>) => {
1618
+ event.currentTarget.setPointerCapture(event.pointerId)
1619
+ dragRef.current = { startY: event.clientY, startTime: event.timeStamp, delta: 0 }
1620
+ draggedRef.current = false
1621
+ }
1622
+
1623
+ const handlePointerMove = (event: PointerEvent<HTMLButtonElement>) => {
1624
+ const drag = dragRef.current
1625
+
1626
+ if (drag === null) {
1627
+ return
1628
+ }
1629
+
1630
+ drag.delta = event.clientY - drag.startY
1631
+ // Down closes an open sheet and up opens a collapsed one; the other direction only gives a little
1632
+ const towardsOther = open ? drag.delta > 0 : drag.delta < 0
1633
+ moveSheet(towardsOther ? drag.delta : drag.delta * OVERDRAG, false)
1634
+ }
1635
+
1636
+ const handlePointerEnd = (event: PointerEvent<HTMLButtonElement>) => {
1637
+ const drag = dragRef.current
1638
+ dragRef.current = null
1639
+
1640
+ if (drag === null) {
1641
+ return
1642
+ }
1643
+
1644
+ moveSheet(0, true)
1645
+
1646
+ if (Math.abs(drag.delta) < TAP_DISTANCE) {
1647
+ return
1648
+ }
1649
+
1650
+ draggedRef.current = true
1651
+ const velocity = drag.delta / Math.max(event.timeStamp - drag.startTime, 1)
1652
+ const flicked = Math.abs(drag.delta) >= SNAP_DISTANCE || Math.abs(velocity) >= SNAP_VELOCITY
1653
+
1654
+ if (flicked && (open ? drag.delta > 0 : drag.delta < 0)) {
1655
+ onOpenChange(!open)
1656
+ }
1657
+ }
1658
+
1659
+ // A tap, Enter or Space toggles the sheet. The pointer click that ends a drag has already been handled; a keyboard
1660
+ // click (`detail` 0) always toggles, also after a touch drag that was followed by no click.
1661
+ const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
1662
+ const dragged = draggedRef.current
1663
+ draggedRef.current = false
1664
+
1665
+ if (dragged && event.detail !== 0) {
1666
+ return
1667
+ }
1668
+
1669
+ onOpenChange(!open)
1670
+ }
1671
+
1672
+ return (
1673
+ <Box
1674
+ component="section"
1675
+ ref={sheetRef}
1676
+ aria-label={label}
1677
+ sx={{
1678
+ position: 'relative',
1679
+ flexShrink: 0,
1680
+ display: 'flex',
1681
+ flexDirection: 'column',
1682
+ maxBlockSize: '55%',
1683
+ paddingBlockEnd: 'env(safe-area-inset-bottom)',
1684
+ borderStartStartRadius: 20,
1685
+ borderStartEndRadius: 20,
1686
+ bgcolor: 'background.paper',
1687
+ boxShadow: '0 -8px 32px rgba(0, 0, 0, 0.12)',
1688
+ // Fills the gap below the sheet while it is dragged up
1689
+ '&::after': {
1690
+ content: '""',
1691
+ position: 'absolute',
1692
+ insetInline: 0,
1693
+ insetBlockStart: '100%',
1694
+ blockSize: '100vh',
1695
+ bgcolor: 'background.paper',
1696
+ },
1697
+ }}
1698
+ >
1699
+ <ButtonBase
1700
+ aria-expanded={open}
1701
+ disableRipple
1702
+ aria-label={open ? `Collapse ${label.toLowerCase()}` : `Expand ${label.toLowerCase()}`}
1703
+ onClick={handleClick}
1704
+ onPointerDown={handlePointerDown}
1705
+ onPointerMove={handlePointerMove}
1706
+ onPointerUp={handlePointerEnd}
1707
+ onPointerCancel={handlePointerEnd}
1708
+ sx={{
1709
+ flexShrink: 0,
1710
+ blockSize: 36,
1711
+ touchAction: 'none',
1712
+ '&.Mui-focusVisible': { outline: 2, outlineColor: 'primary.main', outlineOffset: -4, borderRadius: 3 },
1713
+ }}
1714
+ >
1715
+ <Box component="span" sx={{ inlineSize: 40, blockSize: 4, borderRadius: 2, bgcolor: 'text.disabled' }} />
1716
+ </ButtonBase>
1717
+ {/* A column, so the panel's cards region shrinks first and the price and action stay in view; below the cards
1718
+ region's minimum height the content scrolls as a whole, as the review step does on a short phone */}
1719
+ <Box
1720
+ component="div"
1721
+ sx={{
1722
+ display: 'flex',
1723
+ flexDirection: 'column',
1724
+ flex: '1 1 auto',
1725
+ minBlockSize: 0,
1726
+ overflowY: 'auto',
1727
+ overscrollBehavior: 'contain',
1728
+ }}
1729
+ >
1730
+ {children}
1731
+ </Box>
1732
+ </Box>
1733
+ )
1734
+ }
614
1735
  ```
615
1736
 
616
1737
  ### ./src/\_shared/EdgeFinishCards.tsx
@@ -619,7 +1740,9 @@ components:
619
1740
  import { Box, ButtonBase, type SxProps, type Theme, Typography } from '@mui/material'
620
1741
  import type { KeyboardEvent } from 'react'
621
1742
 
622
- import { type UseAttributeSelection, useResolveLocalizable } from '@salsita-ai/sdk/react'
1743
+ import type { UseAttributeSelection } from '@salsita-ai/sdk/react'
1744
+
1745
+ import { phx } from '../../.phoenix/phx.js'
623
1746
 
624
1747
  // Cross-sections of the tabletop's edge, keyed by the option values of the spec.
625
1748
  const EDGE_PROFILES: Readonly<Record<string, string>> = {
@@ -656,7 +1779,7 @@ interface EdgeFinishCardsProps {
656
1779
  }
657
1780
 
658
1781
  export const EdgeFinishCards = ({ selection }: EdgeFinishCardsProps) => {
659
- const resolve = useResolveLocalizable()
1782
+ const resolve = phx.frontend.useResolveLocalizable()
660
1783
  const { options } = selection
661
1784
  // The card in the tab order: the selected one, or the first enabled one. Disabled cards are never in it.
662
1785
  const selectedIndex = options.findIndex(option => option.value === selection.value && !option.disabled)
@@ -751,39 +1874,115 @@ export const EdgeFinishCards = ({ selection }: EdgeFinishCardsProps) => {
751
1874
  ### ./src/\_shared/FullscreenToggle.tsx
752
1875
 
753
1876
  ```tsx
754
- import { Button } from '@mui/material'
1877
+ import { Button, Tooltip } from '@mui/material'
1878
+
1879
+ import { FrontendAnalyticsFactory } from '@salsita-ai/sdk/configurator'
755
1880
 
756
1881
  import { phx } from '../../.phoenix/phx.js'
757
1882
 
758
1883
  const ENTER_ICON = 'M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5'
759
1884
  const EXIT_ICON = 'M9 4v5H4M15 4v5h5M15 20v-5h5M9 20v-5H4'
760
1885
 
761
- export const FullscreenToggle = () => {
1886
+ interface FullscreenToggleProps {
1887
+ // Only the icon, with the label as its accessible name and tooltip, where a label would cover the product
1888
+ readonly compact?: boolean
1889
+ }
1890
+
1891
+ export const FullscreenToggle = ({ compact = false }: FullscreenToggleProps) => {
762
1892
  const isFullscreen = phx.frontend.useIsFullscreenActive()
763
1893
  const setFullscreen = phx.frontend.useSetFullscreen()
1894
+ const analytics = phx.frontend.useAnalytics()
1895
+ const label = isFullscreen ? 'Exit fullscreen' : 'Fullscreen'
1896
+ const icon = (
1897
+ <svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
1898
+ <path d={isFullscreen ? EXIT_ICON : ENTER_ICON} />
1899
+ </svg>
1900
+ )
1901
+
1902
+ // The same event the built-in fullscreen button records, for entering and for leaving
1903
+ const toggle = () => {
1904
+ analytics.push(FrontendAnalyticsFactory.FullScreenModeToggled())
1905
+ setFullscreen(!isFullscreen)
1906
+ }
764
1907
 
765
1908
  return (
766
- <Button
767
- onClick={() => setFullscreen(!isFullscreen)}
768
- startIcon={
769
- <svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
770
- <path d={isFullscreen ? EXIT_ICON : ENTER_ICON} />
771
- </svg>
772
- }
773
- sx={{
774
- minBlockSize: 44,
775
- paddingInline: 2.5,
776
- borderRadius: 999,
777
- bgcolor: 'background.paper',
778
- color: 'text.primary',
779
- boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
780
- textTransform: 'none',
781
- fontWeight: 600,
782
- '&:hover': { bgcolor: 'background.paper', boxShadow: '0 6px 20px rgba(0, 0, 0, 0.18)' },
783
- }}
1909
+ <Tooltip title={compact ? label : ''}>
1910
+ <Button
1911
+ onClick={toggle}
1912
+ aria-label={compact ? label : undefined}
1913
+ startIcon={compact ? undefined : icon}
1914
+ sx={{
1915
+ minBlockSize: 44,
1916
+ minInlineSize: 44,
1917
+ paddingInline: compact ? 0 : 2.5,
1918
+ borderRadius: 999,
1919
+ bgcolor: 'background.paper',
1920
+ color: 'text.primary',
1921
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
1922
+ textTransform: 'none',
1923
+ fontWeight: 600,
1924
+ '&:hover': { bgcolor: 'background.paper', boxShadow: '0 6px 20px rgba(0, 0, 0, 0.18)' },
1925
+ }}
1926
+ >
1927
+ {compact ? icon : label}
1928
+ </Button>
1929
+ </Tooltip>
1930
+ )
1931
+ }
1932
+ ```
1933
+
1934
+ ### ./src/\_shared/MessageToast.tsx
1935
+
1936
+ ```tsx
1937
+ import { Alert } from '@mui/material'
1938
+ import { useEffect, useState } from 'react'
1939
+
1940
+ import type { ConfiguratorMessage } from '@salsita-ai/sdk/react'
1941
+
1942
+ import { phx } from '../../.phoenix/phx.js'
1943
+
1944
+ const VIOLATION_SEVERITY = { fatal: 'error', fixable: 'error', warning: 'warning', notice: 'info' } as const
1945
+ const RANK = { info: 0, warning: 1, error: 2 } as const
1946
+ const VISIBLE_MS = 6000
1947
+
1948
+ const severityOf = (message: ConfiguratorMessage) => {
1949
+ if (message.kind === 'violation') {
1950
+ return VIOLATION_SEVERITY[message.violation.severity]
1951
+ }
1952
+
1953
+ return message.kind === 'error' || message.kind === 'saveError' ? 'error' : 'info'
1954
+ }
1955
+
1956
+ export const MessageToast = () => {
1957
+ const [message, setMessage] = useState<ConfiguratorMessage | null>(null)
1958
+
1959
+ // A rejected change also reports the notices already present; keep the worst message while it is shown
1960
+ phx.frontend.useConfiguratorMessages(next => {
1961
+ setMessage(shown => (shown !== null && RANK[severityOf(next)] < RANK[severityOf(shown)] ? shown : next))
1962
+ })
1963
+
1964
+ useEffect(() => {
1965
+ if (message === null) {
1966
+ return
1967
+ }
1968
+
1969
+ const timeout = setTimeout(() => setMessage(null), VISIBLE_MS)
1970
+
1971
+ return () => clearTimeout(timeout)
1972
+ }, [message])
1973
+
1974
+ if (message === null) {
1975
+ return null
1976
+ }
1977
+
1978
+ return (
1979
+ <Alert
1980
+ severity={severityOf(message)}
1981
+ onClose={() => setMessage(null)}
1982
+ sx={{ maxInlineSize: 420, borderRadius: 3, boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)' }}
784
1983
  >
785
- {isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
786
- </Button>
1984
+ {message.text}
1985
+ </Alert>
787
1986
  )
788
1987
  }
789
1988
  ```
@@ -794,13 +1993,22 @@ export const FullscreenToggle = () => {
794
1993
  import { Box, Button, Typography } from '@mui/material'
795
1994
 
796
1995
  import { detailComponent } from '@salsita-ai/sdk/domain'
797
- import { useResolveLocalizable } from '@salsita-ai/sdk/react'
798
1996
 
799
1997
  import { phx } from '../../.phoenix/phx.js'
800
1998
  import { EdgeFinishCards } from './EdgeFinishCards.js'
1999
+ import { ReviewActions } from './ReviewActions.js'
2000
+
2001
+ const BUTTON_SX = { minBlockSize: 48, borderRadius: 999, textTransform: 'none', fontWeight: 600 } as const
2002
+
2003
+ interface ProductPanelProps {
2004
+ // Only the name, the price and the action, for a collapsed bottom sheet
2005
+ readonly collapsed?: boolean
2006
+ // In a bottom sheet the cards region keeps the top of the cards in view, and the sheet scrolls when that does not fit
2007
+ readonly inSheet?: boolean
2008
+ }
801
2009
 
802
- export const ProductPanel = () => {
803
- const resolve = useResolveLocalizable()
2010
+ export const ProductPanel = ({ collapsed = false, inSheet = false }: ProductPanelProps) => {
2011
+ const resolve = phx.frontend.useResolveLocalizable()
804
2012
  const detail = phx.frontend.useFirstRootComponent(detailComponent)
805
2013
  const price = phx.frontend.useRootEntityPrice()
806
2014
  const tableId = phx.frontend.useRootEntityId()
@@ -809,9 +2017,10 @@ export const ProductPanel = () => {
809
2017
  const navigationState = phx.frontend.useActiveNavigationState()
810
2018
  const pushNavigate = phx.frontend.usePushNavigate()
811
2019
  const popNavigate = phx.frontend.usePopNavigate()
2020
+ const navigate = phx.frontend.useNavigate()
812
2021
 
813
2022
  const isReadMode = accessInfo.viewMode
814
- const isReview = isReadMode || navigationState === 'checkout'
2023
+ const isReview = isReadMode || navigationState !== 'configure'
815
2024
  const chosenEdge = edgeFinish.options.find(option => option.value === edgeFinish.value)
816
2025
 
817
2026
  return (
@@ -832,29 +2041,44 @@ export const ProductPanel = () => {
832
2041
  <Typography variant="h5" component="h2" fontWeight={700}>
833
2042
  {resolve(detail?.state.title)}
834
2043
  </Typography>
835
- <Typography variant="body2" color="text.secondary">
836
- {resolve(detail?.state.description)}
837
- </Typography>
838
- </Box>
839
-
840
- <Box component="div" sx={{ display: 'grid', gap: 1.5, paddingInline: 3, paddingBlockEnd: 3, overflowY: 'auto' }}>
841
- <Typography variant="subtitle2" component="h3">
842
- Edge finish
843
- </Typography>
844
- {isReview ? (
845
- <Box component="div" sx={{ padding: 2, borderRadius: 3, bgcolor: 'action.hover' }}>
846
- <Typography variant="body2" fontWeight={600}>
847
- {resolve(chosenEdge?.title)}
848
- </Typography>
849
- <Typography variant="caption" color="text.secondary">
850
- {resolve(chosenEdge?.description)}
851
- </Typography>
852
- </Box>
853
- ) : (
854
- <EdgeFinishCards selection={edgeFinish} />
2044
+ {!collapsed && (
2045
+ <Typography variant="body2" color="text.secondary">
2046
+ {resolve(detail?.state.description)}
2047
+ </Typography>
855
2048
  )}
856
2049
  </Box>
857
2050
 
2051
+ {!collapsed && (
2052
+ <Box
2053
+ component="div"
2054
+ sx={{
2055
+ display: 'grid',
2056
+ gap: 1.5,
2057
+ paddingInline: 3,
2058
+ paddingBlockEnd: 3,
2059
+ // In a short container the region shrinks and scrolls; a scroll past its end moves its container on
2060
+ minBlockSize: inSheet ? 120 : 0,
2061
+ overflowY: 'auto',
2062
+ }}
2063
+ >
2064
+ <Typography variant="subtitle2" component="h3">
2065
+ Edge finish
2066
+ </Typography>
2067
+ {isReview ? (
2068
+ <Box component="div" sx={{ padding: 2, borderRadius: 3, bgcolor: 'action.hover' }}>
2069
+ <Typography variant="body2" fontWeight={600}>
2070
+ {resolve(chosenEdge?.title)}
2071
+ </Typography>
2072
+ <Typography variant="caption" color="text.secondary">
2073
+ {resolve(chosenEdge?.description)}
2074
+ </Typography>
2075
+ </Box>
2076
+ ) : (
2077
+ <EdgeFinishCards selection={edgeFinish} />
2078
+ )}
2079
+ </Box>
2080
+ )}
2081
+
858
2082
  <Box component="footer" sx={{ display: 'grid', gap: 2, padding: 3, borderTop: 1, borderColor: 'divider' }}>
859
2083
  <Box component="div" sx={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
860
2084
  <Typography variant="body2" color="text.secondary">
@@ -864,23 +2088,244 @@ export const ProductPanel = () => {
864
2088
  {price}
865
2089
  </Typography>
866
2090
  </Box>
867
- {isReview && !isReadMode && (
868
- <Typography variant="body2" color="text.secondary">
869
- Your table is ready for a quote. A real shop collects the contact details here.
870
- </Typography>
2091
+ {!isReadMode && navigationState === 'configure' && (
2092
+ <Button size="large" variant="contained" onClick={() => pushNavigate('checkout')} sx={BUTTON_SX}>
2093
+ Review and request a quote
2094
+ </Button>
2095
+ )}
2096
+ {!isReadMode && navigationState === 'checkout' && (
2097
+ <>
2098
+ <Typography variant="body2" color="text.secondary">
2099
+ Your table is ready for a quote. A real shop collects the contact details here.
2100
+ </Typography>
2101
+ <ReviewActions onBack={popNavigate} />
2102
+ </>
2103
+ )}
2104
+ {!isReadMode && navigationState === 'requested' && (
2105
+ <>
2106
+ <Typography variant="body2" role="status">
2107
+ Thank you. Your quote request is on its way, and the shop replies within two working days.
2108
+ </Typography>
2109
+ <Button size="large" variant="outlined" onClick={() => navigate('configure')} sx={BUTTON_SX}>
2110
+ Back to the configuration
2111
+ </Button>
2112
+ </>
871
2113
  )}
872
- {!isReadMode && (
2114
+ </Box>
2115
+ </>
2116
+ )
2117
+ }
2118
+ ```
2119
+
2120
+ ### ./src/\_shared/ReviewActions.tsx
2121
+
2122
+ ```tsx
2123
+ import { Alert, Box, Button, CircularProgress, Typography } from '@mui/material'
2124
+
2125
+ import { phx } from '../../.phoenix/phx.js'
2126
+
2127
+ const BUTTON_SX = { minBlockSize: 48, borderRadius: 999, textTransform: 'none', fontWeight: 600 } as const
2128
+
2129
+ interface ReviewActionsProps {
2130
+ readonly onBack: () => void
2131
+ }
2132
+
2133
+ // The review step's buttons: the spec's primary finalize action and the way back. `useFinalizeActions` runs the
2134
+ // action's handler beside the layout, so its pending state and error outlive this component: when the panel unmounts,
2135
+ // as in fullscreen, the request goes on and the remounted buttons still show it. Back is disabled while the request
2136
+ // is pending, so the customer cannot change the configuration while it is sent.
2137
+ export const ReviewActions = ({ onBack }: ReviewActionsProps) => {
2138
+ const { primary, hints } = phx.frontend.useFinalizeActions()
2139
+ const isPending = primary?.isPending ?? false
2140
+
2141
+ return (
2142
+ <Box component="div" sx={{ display: 'grid', gap: 2 }}>
2143
+ {primary && (
2144
+ <Box component="div" sx={{ display: 'grid', gap: 1 }}>
873
2145
  <Button
874
2146
  size="large"
875
- variant={isReview ? 'outlined' : 'contained'}
876
- onClick={isReview ? popNavigate : () => pushNavigate('checkout')}
877
- sx={{ minBlockSize: 48, borderRadius: 999, textTransform: 'none', fontWeight: 600 }}
2147
+ variant="contained"
2148
+ disabled={primary.disabled || isPending}
2149
+ onClick={() => void primary.run()}
2150
+ title={primary.title}
2151
+ startIcon={isPending ? <CircularProgress size={18} color="inherit" /> : undefined}
2152
+ sx={BUTTON_SX}
878
2153
  >
879
- {isReview ? 'Back to the configuration' : 'Review and request a quote'}
2154
+ {isPending ? 'Sending the request…' : primary.label}
880
2155
  </Button>
881
- )}
882
- </Box>
883
- </>
2156
+ {primary.disabled && hints && (
2157
+ <Typography variant="caption" color="text.secondary">
2158
+ {hints}
2159
+ </Typography>
2160
+ )}
2161
+ {primary.error && <Alert severity="error">The request could not be sent: {primary.error.message}</Alert>}
2162
+ </Box>
2163
+ )}
2164
+ <Button size="large" variant="outlined" disabled={isPending} onClick={onBack} sx={BUTTON_SX}>
2165
+ Back to the configuration
2166
+ </Button>
2167
+ </Box>
2168
+ )
2169
+ }
2170
+ ```
2171
+
2172
+ ### ./src/\_shared/SceneControls.tsx
2173
+
2174
+ ```tsx
2175
+ import { Box, Button, Tooltip } from '@mui/material'
2176
+
2177
+ import { phx } from '../../.phoenix/phx.js'
2178
+
2179
+ // The spec's view states, each with the label of the button that shows it
2180
+ const VIEWS = [
2181
+ { state: 'overview', label: 'Whole table' },
2182
+ { state: 'edge', label: 'Edge close-up' },
2183
+ ] as const
2184
+
2185
+ const RESET_ICON = 'M4 12a8 8 0 1 0 2.4-5.7M4 4v4h4'
2186
+
2187
+ const SURFACE_SX = {
2188
+ borderRadius: 999,
2189
+ bgcolor: 'background.paper',
2190
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
2191
+ } as const
2192
+
2193
+ const BUTTON_SX = {
2194
+ minBlockSize: 44,
2195
+ minInlineSize: 44,
2196
+ borderRadius: 999,
2197
+ textTransform: 'none',
2198
+ fontWeight: 600,
2199
+ whiteSpace: 'nowrap',
2200
+ } as const
2201
+
2202
+ interface SceneControlsProps {
2203
+ // The reset button shows only its icon, with its label as tooltip and accessible name
2204
+ readonly compact?: boolean
2205
+ // Only the reset button, without the view buttons
2206
+ readonly resetOnly?: boolean
2207
+ }
2208
+
2209
+ // The layout's own camera controls: one button per view state of the spec, and a reset that undoes an orbit
2210
+ export const SceneControls = ({ compact = false, resetOnly = false }: SceneControlsProps) => {
2211
+ const activeView = phx.frontend.useActiveViewState()
2212
+ const setViewState = phx.frontend.useSetViewState()
2213
+ const resetCamera = phx.frontend.useResetCamera()
2214
+ const resetIcon = (
2215
+ <svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
2216
+ <path d={RESET_ICON} />
2217
+ </svg>
2218
+ )
2219
+
2220
+ return (
2221
+ <Box component="div" sx={{ display: 'flex', gap: 1 }}>
2222
+ {!resetOnly && (
2223
+ <Box component="div" role="group" aria-label="View" sx={{ ...SURFACE_SX, display: 'flex' }}>
2224
+ {VIEWS.map(view => {
2225
+ const isActive = activeView === view.state
2226
+
2227
+ return (
2228
+ <Button
2229
+ key={view.state}
2230
+ aria-pressed={isActive}
2231
+ onClick={() => setViewState(view.state)}
2232
+ sx={{
2233
+ ...BUTTON_SX,
2234
+ paddingInline: 2,
2235
+ bgcolor: isActive ? 'text.primary' : 'transparent',
2236
+ color: isActive ? 'background.paper' : 'text.primary',
2237
+ '&:hover': { bgcolor: isActive ? 'text.primary' : 'action.hover' },
2238
+ }}
2239
+ >
2240
+ {view.label}
2241
+ </Button>
2242
+ )
2243
+ })}
2244
+ </Box>
2245
+ )}
2246
+ <Tooltip title={compact ? 'Reset view' : ''}>
2247
+ <Button
2248
+ onClick={resetCamera}
2249
+ aria-label={compact ? 'Reset view' : undefined}
2250
+ startIcon={compact ? undefined : resetIcon}
2251
+ sx={{
2252
+ ...SURFACE_SX,
2253
+ ...BUTTON_SX,
2254
+ paddingInline: compact ? 0 : 2,
2255
+ color: 'text.primary',
2256
+ '&:hover': { bgcolor: 'background.paper', boxShadow: '0 6px 20px rgba(0, 0, 0, 0.18)' },
2257
+ }}
2258
+ >
2259
+ {compact ? resetIcon : 'Reset view'}
2260
+ </Button>
2261
+ </Tooltip>
2262
+ </Box>
2263
+ )
2264
+ }
2265
+ ```
2266
+
2267
+ ### ./src/\_shared/UndoRedo.tsx
2268
+
2269
+ ```tsx
2270
+ import { Box, IconButton, Tooltip } from '@mui/material'
2271
+
2272
+ import { phx } from '../../.phoenix/phx.js'
2273
+
2274
+ const UNDO_ICON = 'M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11'
2275
+ const REDO_ICON = 'M15 14l5-5-5-5M20 9H9.5a5.5 5.5 0 0 0 0 11H13'
2276
+
2277
+ interface HistoryButtonProps {
2278
+ readonly label: string
2279
+ readonly icon: string
2280
+ readonly enabled: boolean
2281
+ readonly onClick: () => void
2282
+ }
2283
+
2284
+ const HistoryButton = ({ label, icon, enabled, onClick }: HistoryButtonProps) => (
2285
+ <Tooltip title={label}>
2286
+ {/* A disabled button fires no pointer events, so the span keeps the tooltip working */}
2287
+ <span>
2288
+ <IconButton
2289
+ aria-label={label}
2290
+ disabled={!enabled}
2291
+ onClick={onClick}
2292
+ sx={{ inlineSize: 44, blockSize: 44, color: 'text.primary' }}
2293
+ >
2294
+ <svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
2295
+ <path d={icon} />
2296
+ </svg>
2297
+ </IconButton>
2298
+ </span>
2299
+ </Tooltip>
2300
+ )
2301
+
2302
+ // Undo and redo for the customer's choices. They change the configuration, so they are offered only where the cards
2303
+ // are: not to a viewer, and not in the review step, where the summary and a running request must not change under the
2304
+ // customer. `useUndoRedo` pushes the same analytics events as the built-in toolbar's buttons.
2305
+ export const UndoRedo = () => {
2306
+ const { canUndo, canRedo, undo, redo } = phx.frontend.useUndoRedo()
2307
+ const { accessInfo } = phx.frontend.useAccessService()
2308
+ const navigationState = phx.frontend.useActiveNavigationState()
2309
+
2310
+ if (accessInfo.viewMode || navigationState !== 'configure') {
2311
+ return null
2312
+ }
2313
+
2314
+ return (
2315
+ <Box
2316
+ component="div"
2317
+ role="group"
2318
+ aria-label="History"
2319
+ sx={{
2320
+ display: 'flex',
2321
+ borderRadius: 999,
2322
+ bgcolor: 'background.paper',
2323
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
2324
+ }}
2325
+ >
2326
+ <HistoryButton label="Undo" icon={UNDO_ICON} enabled={canUndo} onClick={undo} />
2327
+ <HistoryButton label="Redo" icon={REDO_ICON} enabled={canRedo} onClick={redo} />
2328
+ </Box>
884
2329
  )
885
2330
  }
886
2331
  ```
@@ -889,25 +2334,44 @@ export const ProductPanel = () => {
889
2334
 
890
2335
  ```tsx
891
2336
  import { Box } from '@mui/material'
2337
+ import { useState } from 'react'
892
2338
 
893
- import { LayoutSlots, useSceneSafeArea } from '@salsita-ai/sdk/configurator'
2339
+ import { LayoutSlots } from '@salsita-ai/sdk/configurator'
894
2340
 
895
2341
  import { phx } from '../../.phoenix/phx.js'
2342
+ import { BottomSheet } from '../_shared/BottomSheet.js'
896
2343
  import { FullscreenToggle } from '../_shared/FullscreenToggle.js'
2344
+ import { MessageToast } from '../_shared/MessageToast.js'
897
2345
  import { ProductPanel } from '../_shared/ProductPanel.js'
2346
+ import { SceneControls } from '../_shared/SceneControls.js'
2347
+ import { UndoRedo } from '../_shared/UndoRedo.js'
898
2348
 
899
2349
  const GUTTER = 24
900
2350
  const PANEL_WIDTH = 380
2351
+ const PHONE_GUTTER = 16
2352
+ const TOUCH_TARGET = 44
901
2353
 
902
2354
  const ConfiguratorLayout = () => {
903
- const { containerRef, safeAreaRef } = useSceneSafeArea()
2355
+ const { containerRef, safeAreaRef } = phx.frontend.useSceneSafeArea()
904
2356
  const isFullscreen = phx.frontend.useIsFullscreenActive()
2357
+ // `null` until the container has been measured; the layout shows only the scene until then
2358
+ const viewports = phx.frontend.useActiveViewports()
2359
+ const [isSheetOpen, setSheetOpen] = useState(false)
2360
+
2361
+ const isDesktop = viewports?.includes('desktop') ?? false
2362
+ const gutter = isDesktop ? GUTTER : PHONE_GUTTER
905
2363
 
906
2364
  return (
907
- <Box component="div" ref={containerRef} sx={{ position: 'absolute', inset: 0 }}>
2365
+ <Box
2366
+ component="div"
2367
+ ref={containerRef}
2368
+ // `clip`, not `hidden`: what overflows, such as the sheet's fill below its edge, can never be scrolled into view,
2369
+ // not even by the browser when the focus moves
2370
+ sx={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', overflow: 'clip' }}
2371
+ >
908
2372
  <LayoutSlots.SharedCanvas />
909
2373
 
910
- {!isFullscreen && (
2374
+ {viewports !== null && isDesktop && !isFullscreen && (
911
2375
  <Box
912
2376
  component="aside"
913
2377
  sx={{
@@ -921,31 +2385,80 @@ const ConfiguratorLayout = () => {
921
2385
  borderRadius: 5,
922
2386
  bgcolor: 'background.paper',
923
2387
  boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)',
924
- overflow: 'hidden',
2388
+ // A very short container, such as a phone held sideways, scrolls the panel instead of clipping its footer
2389
+ overflowX: 'hidden',
2390
+ overflowY: 'auto',
2391
+ overscrollBehavior: 'contain',
925
2392
  }}
926
2393
  >
927
2394
  <ProductPanel />
928
2395
  </Box>
929
2396
  )}
930
2397
 
931
- <Box
932
- component="div"
933
- ref={safeAreaRef}
934
- sx={{
935
- position: 'absolute',
936
- insetBlock: 0,
937
- insetInlineStart: isFullscreen ? 0 : PANEL_WIDTH + 2 * GUTTER,
938
- insetInlineEnd: 0,
939
- pointerEvents: 'none',
940
- }}
941
- >
2398
+ {viewports !== null && (
942
2399
  <Box
943
2400
  component="div"
944
- sx={{ position: 'absolute', insetBlockStart: GUTTER, insetInlineEnd: GUTTER, pointerEvents: 'auto' }}
2401
+ ref={safeAreaRef}
2402
+ sx={{
2403
+ // Desktop: right of the panel. Phone: above the sheet, whose height the column gives the rest.
2404
+ ...(isDesktop
2405
+ ? {
2406
+ position: 'absolute',
2407
+ insetBlock: 0,
2408
+ insetInlineStart: isFullscreen ? 0 : PANEL_WIDTH + 2 * GUTTER,
2409
+ insetInlineEnd: 0,
2410
+ }
2411
+ : { position: 'relative', flex: '1 1 0', minBlockSize: 0 }),
2412
+ pointerEvents: 'none',
2413
+ }}
945
2414
  >
946
- <FullscreenToggle />
2415
+ <Box
2416
+ component="div"
2417
+ sx={{
2418
+ position: 'absolute',
2419
+ insetBlockStart: gutter,
2420
+ insetInlineEnd: gutter,
2421
+ display: 'flex',
2422
+ gap: 1,
2423
+ pointerEvents: 'auto',
2424
+ }}
2425
+ >
2426
+ {!isFullscreen && <UndoRedo />}
2427
+ <FullscreenToggle compact={!isDesktop} />
2428
+ </Box>
2429
+ <Box
2430
+ component="div"
2431
+ sx={{
2432
+ position: 'absolute',
2433
+ pointerEvents: 'auto',
2434
+ // Desktop: bottom left, above the row of view controls. Phone: at the top below the row of undo, redo and
2435
+ // fullscreen, clear of the sheet's handle and the view controls.
2436
+ ...(isDesktop
2437
+ ? { insetBlockEnd: 2 * GUTTER + TOUCH_TARGET, insetInlineStart: GUTTER }
2438
+ : { insetBlockStart: gutter + TOUCH_TARGET + gutter / 2, insetInline: gutter }),
2439
+ }}
2440
+ >
2441
+ <MessageToast />
2442
+ </Box>
2443
+ {/* A phone in fullscreen keeps only the reset, as the built-in layout does. Below 1024 pixels (`tablet`) the
2444
+ free area beside the panel is narrow, so the reset shows only its icon there too. */}
2445
+ <Box
2446
+ component="div"
2447
+ sx={{ position: 'absolute', insetBlockEnd: gutter, insetInlineEnd: gutter, pointerEvents: 'auto' }}
2448
+ >
2449
+ <SceneControls
2450
+ compact={!isDesktop || (viewports?.includes('tablet') ?? false)}
2451
+ resetOnly={!isDesktop && isFullscreen}
2452
+ />
2453
+ </Box>
947
2454
  </Box>
948
- </Box>
2455
+ )}
2456
+
2457
+ {viewports !== null && !isDesktop && !isFullscreen && (
2458
+ <BottomSheet open={isSheetOpen} onOpenChange={setSheetOpen} label="Product details">
2459
+ <ProductPanel collapsed={!isSheetOpen} inSheet />
2460
+ </BottomSheet>
2461
+ )}
949
2462
  </Box>
950
2463
  )
951
2464
  }
@@ -953,6 +2466,68 @@ const ConfiguratorLayout = () => {
953
2466
  export default phx.frontend.createCustomPhoenixLayoutElement('ConfiguratorLayout', ConfiguratorLayout)
954
2467
  ```
955
2468
 
2469
+ ### ./src/elements/EdgeFinishHotspot.tsx
2470
+
2471
+ ```tsx
2472
+ import { Box, ButtonBase, Popover, Tooltip } from '@mui/material'
2473
+ import { useState } from 'react'
2474
+
2475
+ import { phx } from '../../.phoenix/phx.js'
2476
+ import { EdgeFinishCards } from '../_shared/EdgeFinishCards.js'
2477
+
2478
+ // The edge profile the button shows, the square edge of the cards' drawings
2479
+ const EDGE_ICON = 'M3 7H17L21 11V13L17 17H3Z'
2480
+
2481
+ // Rendered by the spec's `htmlHotspot` on the table's edge: a button on the product that opens the edge finishes
2482
+ // next to the edge, with the same cards as the panel. The spec's `$filter` decides when it is shown.
2483
+ export default phx.frontend.createCustomPhoenixEntityElement('EdgeFinishHotspot', ({ entityId }) => {
2484
+ const edgeFinish = phx.frontend.useAttributeSelection(entityId, 'edgeFinish')
2485
+ const [anchor, setAnchor] = useState<HTMLElement | null>(null)
2486
+
2487
+ return (
2488
+ <>
2489
+ <Tooltip title="Edge finish">
2490
+ <ButtonBase
2491
+ aria-label="Edge finish"
2492
+ aria-haspopup="dialog"
2493
+ aria-expanded={anchor !== null}
2494
+ onClick={event => setAnchor(event.currentTarget)}
2495
+ sx={{
2496
+ display: 'grid',
2497
+ placeItems: 'center',
2498
+ inlineSize: 44,
2499
+ blockSize: 44,
2500
+ borderRadius: '50%',
2501
+ bgcolor: 'background.paper',
2502
+ color: 'primary.main',
2503
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.2)',
2504
+ '&.Mui-focusVisible': { outline: 2, outlineColor: 'primary.main', outlineOffset: 2 },
2505
+ }}
2506
+ >
2507
+ <svg viewBox="0 0 24 24" width={22} height={22} aria-hidden>
2508
+ <path d={EDGE_ICON} fill="currentColor" />
2509
+ </svg>
2510
+ </ButtonBase>
2511
+ </Tooltip>
2512
+ <Popover
2513
+ open={anchor !== null}
2514
+ anchorEl={anchor}
2515
+ onClose={() => setAnchor(null)}
2516
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
2517
+ transformOrigin={{ vertical: 'bottom', horizontal: 'center' }}
2518
+ slotProps={{
2519
+ paper: { role: 'dialog', 'aria-label': 'Edge finish', sx: { borderRadius: 4, marginBlockStart: -1 } },
2520
+ }}
2521
+ >
2522
+ <Box component="div" sx={{ inlineSize: 340, maxInlineSize: 'calc(100vw - 32px)', padding: 2 }}>
2523
+ <EdgeFinishCards selection={edgeFinish} />
2524
+ </Box>
2525
+ </Popover>
2526
+ </>
2527
+ )
2528
+ })
2529
+ ```
2530
+
956
2531
  ### ./src/expressions/table.ts
957
2532
 
958
2533
  ```ts
@@ -978,4 +2553,45 @@ export default phx.domain.implementCustomExpressions({
978
2553
  })
979
2554
  ```
980
2555
 
2556
+ ### ./src/handlers/request-quote.ts
2557
+
2558
+ ```ts
2559
+ import { useCallback } from 'react'
2560
+
2561
+ import { phx } from '../../.phoenix/phx.js'
2562
+
2563
+ // Stands in for the shop's quote service: the request takes a moment, then the panel shows the confirmation
2564
+ export default phx.frontend.createPhoenixHandler('requestQuote', () => {
2565
+ const navigate = phx.frontend.useNavigate()
2566
+
2567
+ return {
2568
+ handler: useCallback(async () => {
2569
+ await new Promise(resolve => setTimeout(resolve, 1500))
2570
+ navigate('requested')
2571
+ }, [navigate]),
2572
+ }
2573
+ })
2574
+ ```
2575
+
2576
+ ### ./src/transformers/tree-trunk-delivery.ts
2577
+
2578
+ ```ts
2579
+ import { msg } from '@lingui/core/macro'
2580
+
2581
+ import { phx } from '../../.phoenix/phx.js'
2582
+
2583
+ // A tree-trunk edge is cut by hand; choosing it carries a hint, which reaches the layout as a side-effect message
2584
+ export default phx.frontend.transformerBuilder.when
2585
+ .entity(['Table'])
2586
+ .modified({ atFields: ['edgeFinish'] })
2587
+ .then.overrideMatchingUpdate(({ update }) => {
2588
+ if (update.stateDelta.edgeFinish !== 'TreeTrunk') {
2589
+ return [update]
2590
+ }
2591
+
2592
+ return [{ ...update, hint: msg`Tree trunk edges are cut by hand, so delivery takes three weeks longer.` }]
2593
+ })
2594
+ .build('tree-trunk-delivery')
2595
+ ```
2596
+
981
2597
  </details>