@salsita-ai/sdk 0.50.0-develop.70dc8d05b6 → 0.50.0-develop.780177b115

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 (1009) hide show
  1. package/README.md +19 -14
  2. package/dist/admin/default-admin-elements.d.ts +9 -0
  3. package/dist/admin/default-admin-elements.js +1 -0
  4. package/dist/admin/default-elements/built-in-entity-element.d.ts +10 -0
  5. package/dist/admin/default-elements/built-in-entity-element.js +1 -0
  6. package/dist/admin/default-elements/built-in-layout-element.d.ts +10 -0
  7. package/dist/admin/default-elements/built-in-layout-element.js +1 -0
  8. package/dist/admin/default-elements/index.d.ts +2 -0
  9. package/dist/admin/default-elements/index.js +1 -0
  10. package/dist/admin/index.d.ts +1 -0
  11. package/dist/admin/index.js +1 -1
  12. package/dist/application-spec/caching/content-diff.js +1 -1
  13. package/dist/application-spec/core/application-file.d.ts +0 -2
  14. package/dist/application-spec/core/application-file.js +1 -1
  15. package/dist/application-spec/core/components/component-filter.d.ts +53 -0
  16. package/dist/application-spec/core/components/component-filter.js +1 -0
  17. package/dist/application-spec/core/components/index.d.ts +2 -0
  18. package/dist/application-spec/core/components/index.js +1 -0
  19. package/dist/application-spec/core/components/sdk-code-ref.d.ts +9 -0
  20. package/dist/application-spec/core/components/sdk-code-ref.js +1 -0
  21. package/dist/application-spec/core/enhanced-json-value.d.ts +0 -68
  22. package/dist/application-spec/core/enhanced-json-value.js +1 -1
  23. package/dist/application-spec/core/expressions/index.d.ts +2 -0
  24. package/dist/application-spec/core/expressions/index.js +1 -0
  25. package/dist/application-spec/core/expressions/relation-expression-params.d.ts +11 -0
  26. package/dist/application-spec/core/expressions/relation-expression-params.js +1 -0
  27. package/dist/application-spec/core/expressions/sdk-expression-implementation.d.ts +24 -0
  28. package/dist/application-spec/core/expressions/sdk-expression-implementation.js +1 -0
  29. package/dist/application-spec/core/index.d.ts +2 -0
  30. package/dist/application-spec/core/index.js +1 -1
  31. package/dist/application-spec/core/json-value.d.ts +1 -1
  32. package/dist/application-spec/{core → enhanced-json}/eval-to-constant.d.ts +1 -1
  33. package/dist/application-spec/enhanced-json/index.d.ts +4 -0
  34. package/dist/application-spec/enhanced-json/index.js +1 -0
  35. package/dist/application-spec/enhanced-json/is-enhanced-json-array.d.ts +2 -0
  36. package/dist/application-spec/enhanced-json/is-enhanced-json-array.js +1 -0
  37. package/dist/application-spec/enhanced-json/resolve-enhanced-json-value.d.ts +18 -0
  38. package/dist/application-spec/enhanced-json/resolve-enhanced-json-value.js +1 -0
  39. package/dist/application-spec/enhanced-json/schema-from-value.d.ts +24 -0
  40. package/dist/application-spec/enhanced-json/schema-from-value.js +1 -0
  41. package/dist/application-spec/enhanced-json/stringify-enhanced-json-value.d.ts +14 -0
  42. package/dist/application-spec/enhanced-json/stringify-enhanced-json-value.js +1 -0
  43. package/dist/application-spec/enhanced-json/validate-enhanced-json-value-caps.d.ts +12 -0
  44. package/dist/application-spec/enhanced-json/validate-enhanced-json-value-caps.js +1 -0
  45. package/dist/application-spec/enhanced-json/validate-no-legacy-code-ref.d.ts +13 -0
  46. package/dist/application-spec/enhanced-json/validate-no-legacy-code-ref.js +1 -0
  47. package/dist/application-spec/errors/diagnostic-codes.d.ts +14 -0
  48. package/dist/application-spec/errors/diagnostic-codes.js +1 -1
  49. package/dist/application-spec/errors/diagnostic-messages.d.ts +15 -1
  50. package/dist/application-spec/errors/diagnostic-messages.js +1 -1
  51. package/dist/application-spec/generate-application/code/common/code-imports.js +1 -1
  52. package/dist/application-spec/generate-application/code/common/env-reader.js +2 -2
  53. package/dist/application-spec/generate-application/code/common/index.d.ts +1 -0
  54. package/dist/application-spec/generate-application/code/common/index.js +1 -1
  55. package/dist/application-spec/generate-application/code/common/single-quoted-string.d.ts +6 -0
  56. package/dist/application-spec/generate-application/code/common/single-quoted-string.js +1 -0
  57. package/dist/application-spec/generate-application/code/domain-runtime-files.js +1 -1
  58. package/dist/application-spec/generate-application/code/generate-application-code.d.ts +3 -2
  59. package/dist/application-spec/generate-application/code/generate-application-code.js +1 -1
  60. package/dist/application-spec/generate-application/code/project-scaffolding.d.ts +15 -14
  61. package/dist/application-spec/generate-application/code/project-scaffolding.js +2 -2
  62. package/dist/application-spec/generate-application/code/standalone-app-files.d.ts +2 -2
  63. package/dist/application-spec/generate-application/code/standalone-app-files.js +1 -1
  64. package/dist/application-spec/generate-application/code/story-files.js +1 -1
  65. package/dist/application-spec/generate-application/code/templates/admin-config.js +3 -0
  66. package/dist/application-spec/generate-application/code/templates/application-types.js +6 -6
  67. package/dist/application-spec/generate-application/code/templates/common/doc-comment.d.ts +2 -0
  68. package/dist/application-spec/generate-application/code/templates/common/doc-comment.js +1 -0
  69. package/dist/application-spec/generate-application/code/templates/common/format-property-key.d.ts +7 -0
  70. package/dist/application-spec/generate-application/code/templates/common/format-property-key.js +1 -0
  71. package/dist/application-spec/generate-application/code/templates/common/schema-to-type-string.js +3 -3
  72. package/dist/application-spec/generate-application/code/templates/configurator-config.js +6 -4
  73. package/dist/application-spec/generate-application/code/templates/configurator-globals.d.ts +2 -1
  74. package/dist/application-spec/generate-application/code/templates/configurator-globals.js +19 -6
  75. package/dist/application-spec/generate-application/code/templates/domain-spec/factory-types/domain-factory-types.d.ts +35 -0
  76. package/dist/application-spec/generate-application/code/templates/domain-spec/factory-types/domain-factory-types.js +5 -0
  77. package/dist/application-spec/generate-application/code/templates/domain-spec/factory-types/index.d.ts +1 -0
  78. package/dist/application-spec/generate-application/code/templates/domain-spec/factory-types/index.js +1 -0
  79. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-component-optics.d.ts +1 -1
  80. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-component-optics.js +7 -7
  81. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-defaults.js +1 -1
  82. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-domain-block.js +59 -63
  83. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-invariants.d.ts +8 -0
  84. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-invariants.js +1 -1
  85. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-presets.js +1 -1
  86. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-resources.js +13 -13
  87. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-states.d.ts +1 -1
  88. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-states.js +2 -2
  89. package/dist/application-spec/generate-application/code/templates/domain-spec/index.d.ts +2 -1
  90. package/dist/application-spec/generate-application/code/templates/domain-spec/index.js +1 -1
  91. package/dist/application-spec/generate-application/code/templates/eslint-config.d.ts +5 -0
  92. package/dist/application-spec/generate-application/code/templates/eslint-config.js +4 -5
  93. package/dist/application-spec/generate-application/code/templates/frontend-config.js +3 -5
  94. package/dist/application-spec/generate-application/code/templates/gitignore.js +0 -3
  95. package/dist/application-spec/generate-application/code/templates/global-definition/generate-action-types.js +7 -7
  96. package/dist/application-spec/generate-application/code/templates/global-definition/generate-assistant-form-element-types.d.ts +12 -0
  97. package/dist/application-spec/generate-application/code/templates/global-definition/generate-assistant-form-element-types.js +7 -0
  98. package/dist/application-spec/generate-application/code/templates/global-definition/generate-phx-types.d.ts +43 -0
  99. package/dist/application-spec/generate-application/code/templates/global-definition/generate-phx-types.js +33 -0
  100. package/dist/application-spec/generate-application/code/templates/global-definition/index.d.ts +2 -0
  101. package/dist/application-spec/generate-application/code/templates/global-definition/index.js +1 -1
  102. package/dist/application-spec/generate-application/code/templates/globals.js +24 -44
  103. package/dist/application-spec/generate-application/code/templates/index.d.ts +3 -3
  104. package/dist/application-spec/generate-application/code/templates/index.js +1 -1
  105. package/dist/application-spec/generate-application/code/templates/lingui-rc.d.ts +5 -0
  106. package/dist/application-spec/generate-application/code/templates/lingui-rc.js +4 -4
  107. package/dist/application-spec/generate-application/code/templates/prettier-config.d.ts +5 -0
  108. package/dist/application-spec/generate-application/code/templates/prettier-config.js +4 -5
  109. package/dist/application-spec/generate-application/code/templates/root-tsconfig.d.ts +6 -0
  110. package/dist/application-spec/generate-application/code/templates/root-tsconfig.js +2 -0
  111. package/dist/application-spec/generate-application/code/templates/skills/phoenix-cli-skill.js +1 -1
  112. package/dist/application-spec/generate-application/code/templates/skills/phoenix-create-configurator-skill.js +1 -1
  113. package/dist/application-spec/generate-application/code/templates/skills/phoenix-feedback-skill.js +5 -5
  114. package/dist/application-spec/generate-application/code/templates/skills/phoenix-review-skill.js +3 -3
  115. package/dist/application-spec/generate-application/code/templates/skills/phoenix-skill.js +1 -1
  116. package/dist/application-spec/generate-application/code/templates/skills/phoenix-variants-skill.js +2 -2
  117. package/dist/application-spec/generate-application/code/templates/skills/sdk-content.d.ts +26 -7
  118. package/dist/application-spec/generate-application/code/templates/skills/sdk-content.js +2 -2
  119. package/dist/application-spec/generate-application/code/templates/spec-code/collect-spec-code.d.ts +58 -0
  120. package/dist/application-spec/generate-application/code/templates/spec-code/collect-spec-code.js +1 -0
  121. package/dist/application-spec/generate-application/code/templates/spec-code/index.d.ts +2 -0
  122. package/dist/application-spec/generate-application/code/templates/spec-code/index.js +1 -0
  123. package/dist/application-spec/generate-application/code/templates/spec-code/spec-code.d.ts +18 -0
  124. package/dist/application-spec/generate-application/code/templates/spec-code/spec-code.js +41 -0
  125. package/dist/application-spec/generate-application/code/templates/spec-messages/index.d.ts +1 -0
  126. package/dist/application-spec/generate-application/code/templates/spec-messages/index.js +1 -0
  127. package/dist/application-spec/generate-application/code/templates/spec-messages/spec-messages.d.ts +20 -0
  128. package/dist/application-spec/generate-application/code/templates/spec-messages/spec-messages.js +6 -0
  129. package/dist/application-spec/generate-application/code/templates/story/phx-module.d.ts +8 -2
  130. package/dist/application-spec/generate-application/code/templates/story/phx-module.js +15 -3
  131. package/dist/application-spec/generate-application/code/templates/story/wrapper.d.ts +3 -0
  132. package/dist/application-spec/generate-application/code/templates/story/wrapper.js +2 -1
  133. package/dist/application-spec/generate-application/code/templates/tsconfig.d.ts +5 -0
  134. package/dist/application-spec/generate-application/code/templates/tsconfig.js +1 -1
  135. package/dist/application-spec/generate-application/code/templates/webhook-index.d.ts +6 -1
  136. package/dist/application-spec/generate-application/code/templates/webhook-index.js +10 -1
  137. package/dist/application-spec/generate-application/code/templates/webhooks-config.js +2 -3
  138. package/dist/application-spec/generate-application/index.d.ts +2 -0
  139. package/dist/application-spec/generate-application/index.js +1 -1
  140. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-model-runtime.d.ts +6 -4
  141. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-model-runtime.js +1 -1
  142. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-runtime.d.ts +8 -3
  143. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-runtime.js +1 -1
  144. package/dist/application-spec/generate-application/runtime/admin-runtime/states-runtime.d.ts +2 -2
  145. package/dist/application-spec/generate-application/runtime/application-runtime.d.ts +19 -5
  146. package/dist/application-spec/generate-application/runtime/application-runtime.js +1 -1
  147. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-model-runtime.d.ts +6 -4
  148. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-model-runtime.js +1 -1
  149. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-runtime.d.ts +7 -2
  150. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-runtime.js +1 -1
  151. package/dist/application-spec/generate-application/runtime/configurator-runtime/states-runtime.d.ts +2 -2
  152. package/dist/application-spec/generate-application/runtime/factory-data/factory-data-runtime.d.ts +43 -0
  153. package/dist/application-spec/generate-application/runtime/factory-data/factory-data-runtime.js +1 -0
  154. package/dist/application-spec/generate-application/runtime/factory-data/index.d.ts +3 -0
  155. package/dist/application-spec/generate-application/runtime/factory-data/index.js +1 -0
  156. package/dist/application-spec/generate-application/runtime/factory-data/recipe-defaults.d.ts +13 -0
  157. package/dist/application-spec/generate-application/runtime/factory-data/recipe-defaults.js +1 -0
  158. package/dist/application-spec/generate-application/runtime/factory-data/static-violations.d.ts +16 -0
  159. package/dist/application-spec/generate-application/runtime/factory-data/static-violations.js +1 -0
  160. package/dist/application-spec/generate-application/runtime/migrations/index.d.ts +1 -0
  161. package/dist/application-spec/generate-application/runtime/migrations/index.js +1 -0
  162. package/dist/application-spec/generate-application/runtime/migrations/migration-runtime.d.ts +53 -0
  163. package/dist/application-spec/generate-application/runtime/migrations/migration-runtime.js +1 -0
  164. package/dist/application-spec/generate-application/runtime/sections/assistant-runtime.d.ts +2 -2
  165. package/dist/application-spec/generate-application/runtime/sections/build-sdk-optic.d.ts +3 -2
  166. package/dist/application-spec/generate-application/runtime/sections/build-sdk-optic.js +1 -1
  167. package/dist/application-spec/generate-application/runtime/sections/build-user-bound-optic.d.ts +2 -1
  168. package/dist/application-spec/generate-application/runtime/sections/build-user-bound-optic.js +1 -1
  169. package/dist/application-spec/generate-application/runtime/sections/component-optic.d.ts +3 -0
  170. package/dist/application-spec/generate-application/runtime/sections/component-optic.js +1 -1
  171. package/dist/application-spec/generate-application/runtime/sections/data-runtime.d.ts +4 -4
  172. package/dist/application-spec/generate-application/runtime/sections/expression-runtime.d.ts +7 -4
  173. package/dist/application-spec/generate-application/runtime/sections/expression-runtime.js +1 -1
  174. package/dist/application-spec/generate-application/runtime/sections/generate-memo-eval-plan-runtime.d.ts +9 -5
  175. package/dist/application-spec/generate-application/runtime/sections/generate-memo-eval-plan-runtime.js +1 -1
  176. package/dist/application-spec/generate-application/runtime/sections/reflection-constants-runtime.d.ts +3 -1
  177. package/dist/application-spec/generate-application/runtime/sections/reflection-constants-runtime.js +1 -1
  178. package/dist/application-spec/generate-application/runtime/sections/theme-runtime.d.ts +2 -2
  179. package/dist/application-spec/generate-application/runtime/sections/transformers-runtime.d.ts +3 -3
  180. package/dist/application-spec/generate-application/runtime/spec-code/index.d.ts +2 -0
  181. package/dist/application-spec/generate-application/runtime/spec-code/index.js +1 -0
  182. package/dist/application-spec/generate-application/runtime/spec-code/resolve-spec-code.d.ts +18 -0
  183. package/dist/application-spec/generate-application/runtime/spec-code/resolve-spec-code.js +1 -0
  184. package/dist/application-spec/generate-application/runtime/spec-code/spec-code-module.d.ts +17 -0
  185. package/dist/application-spec/generate-application/runtime/spec-code/spec-code-module.js +1 -0
  186. package/dist/application-spec/generate-application/runtime-spec/decode-runtime-spec.d.ts +9 -0
  187. package/dist/application-spec/generate-application/runtime-spec/decode-runtime-spec.js +1 -0
  188. package/dist/application-spec/generate-application/runtime-spec/encode-runtime-spec.d.ts +8 -0
  189. package/dist/application-spec/generate-application/runtime-spec/encode-runtime-spec.js +1 -0
  190. package/dist/application-spec/generate-application/runtime-spec/encoded-runtime-spec.d.ts +27 -0
  191. package/dist/application-spec/generate-application/runtime-spec/encoded-runtime-spec.js +1 -0
  192. package/dist/application-spec/generate-application/runtime-spec/index.d.ts +5 -0
  193. package/dist/application-spec/generate-application/runtime-spec/index.js +1 -0
  194. package/dist/application-spec/generate-application/runtime-spec/runtime-spec.d.ts +68 -0
  195. package/dist/application-spec/generate-application/runtime-spec/runtime-spec.js +1 -0
  196. package/dist/application-spec/generate-application/runtime-spec/spec-code-id.d.ts +40 -0
  197. package/dist/application-spec/generate-application/runtime-spec/spec-code-id.js +1 -0
  198. package/dist/application-spec/index.d.ts +1 -0
  199. package/dist/application-spec/index.js +1 -1
  200. package/dist/application-spec/parse-application-files/categorize/categorize-files.d.ts +34 -0
  201. package/dist/application-spec/parse-application-files/categorize/categorize-files.js +1 -1
  202. package/dist/application-spec/parse-application-files/categorize/earlier-generated-editor-configs.d.ts +16 -0
  203. package/dist/application-spec/parse-application-files/categorize/earlier-generated-editor-configs.js +9 -0
  204. package/dist/application-spec/parse-application-files/categorize/earlier-generated-tsconfig.d.ts +9 -0
  205. package/dist/application-spec/parse-application-files/categorize/earlier-generated-tsconfig.js +1 -0
  206. package/dist/application-spec/parse-application-files/raw-spec/parse-yaml-spec-file.js +1 -1
  207. package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.d.ts +33 -3
  208. package/dist/application-spec/parse-application-files/raw-spec/raw-assistant.js +1 -1
  209. package/dist/application-spec/parse-application-files/raw-spec/raw-spec-file.js +1 -1
  210. package/dist/application-spec/parse-application-files/spec/application/admin-spec/admin-spec.js +1 -1
  211. package/dist/application-spec/parse-application-files/spec/application/configurator-spec/configurator-spec.js +1 -1
  212. package/dist/application-spec/parse-application-files/spec/common/index.d.ts +1 -0
  213. package/dist/application-spec/parse-application-files/spec/common/index.js +1 -1
  214. package/dist/application-spec/parse-application-files/spec/common/value-type/format-value-type.d.ts +3 -0
  215. package/dist/application-spec/parse-application-files/spec/common/value-type/format-value-type.js +1 -0
  216. package/dist/application-spec/parse-application-files/spec/common/value-type/index.d.ts +5 -0
  217. package/dist/application-spec/parse-application-files/spec/common/value-type/index.js +1 -0
  218. package/dist/application-spec/parse-application-files/spec/common/value-type/intersect-value-types.d.ts +6 -0
  219. package/dist/application-spec/parse-application-files/spec/common/value-type/intersect-value-types.js +1 -0
  220. package/dist/application-spec/parse-application-files/spec/common/value-type/is-assignable-value-type.d.ts +8 -0
  221. package/dist/application-spec/parse-application-files/spec/common/value-type/is-assignable-value-type.js +1 -0
  222. package/dist/application-spec/parse-application-files/spec/common/value-type/value-type-at.d.ts +10 -0
  223. package/dist/application-spec/parse-application-files/spec/common/value-type/value-type-at.js +1 -0
  224. package/dist/application-spec/parse-application-files/spec/common/value-type/value-type.d.ts +98 -0
  225. package/dist/application-spec/parse-application-files/spec/common/value-type/value-type.js +1 -0
  226. package/dist/application-spec/parse-application-files/spec/components/bindings/component-path.d.ts +13 -0
  227. package/dist/application-spec/parse-application-files/spec/components/bindings/component-path.js +1 -0
  228. package/dist/application-spec/parse-application-files/spec/components/bindings/index.d.ts +2 -0
  229. package/dist/application-spec/parse-application-files/spec/components/bindings/index.js +1 -1
  230. package/dist/application-spec/parse-application-files/spec/components/bindings/read-binding-type.d.ts +24 -0
  231. package/dist/application-spec/parse-application-files/spec/components/bindings/read-binding-type.js +1 -0
  232. package/dist/application-spec/parse-application-files/spec/components/component-entry.d.ts +1 -9
  233. package/dist/application-spec/parse-application-files/spec/components/component-entry.js +1 -1
  234. package/dist/application-spec/parse-application-files/spec/components/component-filter.d.ts +1 -54
  235. package/dist/application-spec/parse-application-files/spec/components/component-filter.js +1 -1
  236. package/dist/application-spec/parse-application-files/spec/components/component-state-type.d.ts +8 -0
  237. package/dist/application-spec/parse-application-files/spec/components/component-state-type.js +1 -0
  238. package/dist/application-spec/parse-application-files/spec/components/filter-criterion-types.d.ts +13 -0
  239. package/dist/application-spec/parse-application-files/spec/components/filter-criterion-types.js +1 -0
  240. package/dist/application-spec/parse-application-files/spec/components/index.d.ts +1 -1
  241. package/dist/application-spec/parse-application-files/spec/components/index.js +1 -1
  242. package/dist/application-spec/parse-application-files/spec/components/static-entity-id.d.ts +14 -0
  243. package/dist/application-spec/parse-application-files/spec/components/static-entity-id.js +1 -0
  244. package/dist/application-spec/parse-application-files/spec/components/user-component-entry.d.ts +6 -0
  245. package/dist/application-spec/parse-application-files/spec/components/user-component-entry.js +1 -1
  246. package/dist/application-spec/parse-application-files/spec/components/user-components.d.ts +7 -4
  247. package/dist/application-spec/parse-application-files/spec/components/user-components.js +1 -1
  248. package/dist/application-spec/parse-application-files/spec/expressions/expression-read-types.d.ts +24 -0
  249. package/dist/application-spec/parse-application-files/spec/expressions/expression-read-types.js +1 -0
  250. package/dist/application-spec/parse-application-files/spec/expressions/expression-read.d.ts +13 -0
  251. package/dist/application-spec/parse-application-files/spec/expressions/expression-read.js +1 -0
  252. package/dist/application-spec/parse-application-files/spec/expressions/expression-value-type.d.ts +9 -0
  253. package/dist/application-spec/parse-application-files/spec/expressions/expression-value-type.js +1 -0
  254. package/dist/application-spec/parse-application-files/spec/expressions/expression.d.ts +1 -28
  255. package/dist/application-spec/parse-application-files/spec/expressions/expression.js +1 -1
  256. package/dist/application-spec/parse-application-files/spec/expressions/expressions.d.ts +7 -3
  257. package/dist/application-spec/parse-application-files/spec/expressions/expressions.js +1 -1
  258. package/dist/application-spec/parse-application-files/spec/expressions/index.d.ts +0 -1
  259. package/dist/application-spec/parse-application-files/spec/expressions/index.js +1 -1
  260. package/dist/application-spec/parse-application-files/spec/expressions/memo-eval-plan.js +1 -1
  261. package/dist/application-spec/parse-application-files/spec/expressions/user-expressions.d.ts +3 -1
  262. package/dist/application-spec/parse-application-files/spec/expressions/user-expressions.js +1 -1
  263. package/dist/application-spec/parse-application-files/spec/invariants/invariants.d.ts +2 -2
  264. package/dist/application-spec/parse-application-files/spec/invariants/invariants.js +1 -1
  265. package/dist/application-spec/parse-application-files/spec/model/defaults.js +1 -1
  266. package/dist/application-spec/parse-application-files/spec/model/index.d.ts +1 -0
  267. package/dist/application-spec/parse-application-files/spec/model/index.js +1 -1
  268. package/dist/application-spec/parse-application-files/spec/model/model-type-resolution.d.ts +10 -0
  269. package/dist/application-spec/parse-application-files/spec/model/model-type-resolution.js +1 -0
  270. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-form-elements.d.ts +9 -0
  271. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-form-elements.js +1 -0
  272. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-schema.d.ts +4 -1
  273. package/dist/application-spec/parse-application-files/spec/sections/assistant/assistant-schema.js +1 -1
  274. package/dist/client/phoenix-api.d.ts +463 -0
  275. package/dist/configurator/ConfiguratorApp.js +1 -1
  276. package/dist/configurator/app-loader-content/DefaultAppLoaderContent.d.ts +2 -0
  277. package/dist/configurator/app-loader-content/DefaultAppLoaderContent.js +5 -0
  278. package/dist/configurator/app-loader-content/app-loader-content-name.d.ts +5 -0
  279. package/dist/configurator/app-loader-content/app-loader-content-name.js +1 -0
  280. package/dist/configurator/app-loader-content/index.d.ts +2 -0
  281. package/dist/configurator/app-loader-content/index.js +1 -0
  282. package/dist/configurator/application-runtime/extensions/selectors.js +1 -1
  283. package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.d.ts +36 -1
  284. package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.js +1 -1
  285. package/dist/configurator/application-runtime/extensions/systems/assistant-system/graphql-tools.d.ts +19 -3
  286. package/dist/configurator/application-runtime/extensions/systems/assistant-system/graphql-tools.js +1 -1
  287. package/dist/configurator/application-runtime/extensions/systems/camera-control-system/camera-control-system.d.ts +1 -1
  288. package/dist/configurator/application-runtime/extensions/systems/canvas-system.d.ts +8 -0
  289. package/dist/configurator/application-runtime/extensions/systems/canvas-system.js +1 -1
  290. package/dist/configurator/application-runtime/extensions/systems/drag-and-drop-system/drag-and-drop-system.d.ts +3 -3
  291. package/dist/configurator/application-runtime/extensions/systems/export-system/export-busy-error.d.ts +8 -0
  292. package/dist/configurator/application-runtime/extensions/systems/export-system/export-busy-error.js +1 -0
  293. package/dist/configurator/application-runtime/extensions/systems/export-system/export-system.d.ts +23 -1
  294. package/dist/configurator/application-runtime/extensions/systems/export-system/export-system.js +1 -1
  295. package/dist/configurator/application-runtime/extensions/systems/graphql-system/close-unclosed-braces.d.ts +12 -0
  296. package/dist/configurator/application-runtime/extensions/systems/graphql-system/close-unclosed-braces.js +1 -0
  297. package/dist/configurator/application-runtime/extensions/systems/graphql-system/entity-meta.d.ts +14 -1
  298. package/dist/configurator/application-runtime/extensions/systems/graphql-system/entity-meta.js +1 -1
  299. package/dist/configurator/application-runtime/extensions/systems/graphql-system/graphql-system.d.ts +52 -5
  300. package/dist/configurator/application-runtime/extensions/systems/graphql-system/graphql-system.js +1 -1
  301. package/dist/configurator/application-runtime/extensions/systems/image-rendering-system/image-rendering-system.d.ts +14 -5
  302. package/dist/configurator/application-runtime/extensions/systems/image-rendering-system/image-rendering-system.js +1 -1
  303. package/dist/configurator/application-runtime/extensions/systems/index.d.ts +1 -1
  304. package/dist/configurator/application-runtime/extensions/systems/layout-system/layout-system.d.ts +2 -2
  305. package/dist/configurator/common/configurator-interface.d.ts +137 -4
  306. package/dist/configurator/common/configurator-interface.js +1 -1
  307. package/dist/configurator/common/index.d.ts +3 -1
  308. package/dist/configurator/common/index.js +1 -1
  309. package/dist/configurator/common/use-reset-camera.d.ts +6 -0
  310. package/dist/configurator/common/use-reset-camera.js +1 -0
  311. package/dist/configurator/configuration-not-found-page/ConfigurationNotFoundPageElement.d.ts +12 -0
  312. package/dist/configurator/configuration-not-found-page/ConfigurationNotFoundPageElement.js +1 -0
  313. package/dist/configurator/configuration-not-found-page/create-configuration-not-found-page.d.ts +9 -0
  314. package/dist/configurator/configuration-not-found-page/create-configuration-not-found-page.js +1 -0
  315. package/dist/configurator/configuration-not-found-page/index.d.ts +2 -0
  316. package/dist/configurator/configuration-not-found-page/index.js +1 -0
  317. package/dist/configurator/configurator-context/ConfiguratorResponsiveProvider.d.ts +12 -0
  318. package/dist/configurator/configurator-context/ConfiguratorResponsiveProvider.js +1 -0
  319. package/dist/configurator/configurator-context/PresetSelectionProvider.js +1 -1
  320. package/dist/configurator/configurator-context/PreviewContextProvider.js +1 -1
  321. package/dist/configurator/configurator-context/ViewportStateSync.d.ts +2 -2
  322. package/dist/configurator/configurator-context/ViewportStateSync.js +1 -1
  323. package/dist/configurator/configurator-context/configurator-hooks/layout-hooks.d.ts +11 -1
  324. package/dist/configurator/configurator-context/configurator-routing/ConfiguratorRoutingProvider.js +1 -1
  325. package/dist/configurator/configurator-error-page/BuiltInConfiguratorErrorPage.d.ts +6 -0
  326. package/dist/configurator/configurator-error-page/BuiltInConfiguratorErrorPage.js +1 -0
  327. package/dist/configurator/configurator-error-page/ConfiguratorErrorPageElement.d.ts +12 -0
  328. package/dist/configurator/configurator-error-page/ConfiguratorErrorPageElement.js +1 -0
  329. package/dist/configurator/configurator-error-page/create-configurator-error-page.d.ts +9 -0
  330. package/dist/configurator/configurator-error-page/create-configurator-error-page.js +1 -0
  331. package/dist/configurator/configurator-error-page/index.d.ts +3 -0
  332. package/dist/configurator/configurator-error-page/index.js +1 -0
  333. package/dist/configurator/index.d.ts +3 -0
  334. package/dist/configurator/index.js +1 -1
  335. package/dist/configurator/pages/ConfiguratorErrorPage.d.ts +0 -1
  336. package/dist/configurator/pages/ConfiguratorErrorPage.js +1 -1
  337. package/dist/configurator/pages/ConfiguratorNotFoundPage.js +1 -1
  338. package/dist/configurator/pages/DefaultConfiguratorLayout.d.ts +3 -2
  339. package/dist/configurator/pages/DefaultNotAuthenticatedPage.d.ts +8 -0
  340. package/dist/configurator/pages/DefaultNotAuthenticatedPage.js +1 -0
  341. package/dist/configurator/pages/DefaultPresetUi.d.ts +7 -0
  342. package/dist/configurator/pages/DefaultPresetUi.js +1 -0
  343. package/dist/configurator/pages/LoadedConfiguratorDetailPage.js +1 -1
  344. package/dist/configurator/pages/NewConfiguratorPage.js +1 -1
  345. package/dist/configurator/pages/PresetCreationErrorPage.d.ts +1 -1
  346. package/dist/configurator/pages/PresetCreationErrorPage.js +1 -1
  347. package/dist/configurator/pages/RootPage.js +1 -1
  348. package/dist/configurator/pages/configurator-layout-name.d.ts +2 -0
  349. package/dist/configurator/pages/configurator-layout-name.js +1 -0
  350. package/dist/configurator/pages/default-configurator-elements.d.ts +13 -0
  351. package/dist/configurator/pages/default-configurator-elements.js +1 -0
  352. package/dist/configurator/pages/default-elements/built-in-entity-element.d.ts +10 -0
  353. package/dist/configurator/pages/default-elements/built-in-entity-element.js +1 -0
  354. package/dist/configurator/pages/default-elements/built-in-layout-element.d.ts +10 -0
  355. package/dist/configurator/pages/default-elements/built-in-layout-element.js +1 -0
  356. package/dist/configurator/pages/default-elements/domain-control-elements.d.ts +6 -0
  357. package/dist/configurator/pages/default-elements/domain-control-elements.js +1 -0
  358. package/dist/configurator/pages/default-elements/index.d.ts +5 -0
  359. package/dist/configurator/pages/default-elements/index.js +1 -0
  360. package/dist/configurator/pages/default-elements/recipe-panel-elements.d.ts +3 -0
  361. package/dist/configurator/pages/default-elements/recipe-panel-elements.js +1 -0
  362. package/dist/configurator/pages/default-elements/scene-controls/BuiltInAddEntityFab.d.ts +3 -0
  363. package/dist/configurator/pages/default-elements/scene-controls/BuiltInAddEntityFab.js +1 -0
  364. package/dist/configurator/pages/default-elements/scene-controls/BuiltInDeleteEntityFab.d.ts +3 -0
  365. package/dist/configurator/pages/default-elements/scene-controls/BuiltInDeleteEntityFab.js +1 -0
  366. package/dist/configurator/pages/default-elements/scene-controls/BuiltInNavigationHotspot.d.ts +3 -0
  367. package/dist/configurator/pages/default-elements/scene-controls/BuiltInNavigationHotspot.js +1 -0
  368. package/dist/configurator/pages/default-elements/scene-controls/BuiltInSelectionHotspot.d.ts +3 -0
  369. package/dist/configurator/pages/default-elements/scene-controls/BuiltInSelectionHotspot.js +1 -0
  370. package/dist/configurator/pages/default-elements/scene-controls/BuiltInToolMenuFab.d.ts +3 -0
  371. package/dist/configurator/pages/default-elements/scene-controls/BuiltInToolMenuFab.js +1 -0
  372. package/dist/configurator/pages/default-elements/scene-controls/index.d.ts +6 -0
  373. package/dist/configurator/pages/default-elements/scene-controls/index.js +1 -0
  374. package/dist/configurator/pages/default-elements/scene-controls/scene-control-elements.d.ts +6 -0
  375. package/dist/configurator/pages/default-elements/scene-controls/scene-control-elements.js +1 -0
  376. package/dist/configurator/pages/default-elements/sidebar-footer-elements.d.ts +7 -0
  377. package/dist/configurator/pages/default-elements/sidebar-footer-elements.js +1 -0
  378. package/dist/configurator/pages/default-elements/toolbar-elements.d.ts +7 -0
  379. package/dist/configurator/pages/default-elements/toolbar-elements.js +1 -0
  380. package/dist/configurator/pages/error-boundaries/GeneralErrorBoundary.js +1 -1
  381. package/dist/configurator/pages/index.d.ts +1 -0
  382. package/dist/configurator/pages/index.js +1 -1
  383. package/dist/configurator/preset-screen-elements/PresetDeviceNotSupportedElement.d.ts +4 -0
  384. package/dist/configurator/preset-screen-elements/PresetDeviceNotSupportedElement.js +1 -0
  385. package/dist/configurator/preset-screen-elements/PresetPageActionElement.d.ts +4 -0
  386. package/dist/configurator/preset-screen-elements/PresetPageActionElement.js +1 -0
  387. package/dist/configurator/preset-screen-elements/index.d.ts +2 -0
  388. package/dist/configurator/preset-screen-elements/index.js +1 -0
  389. package/dist/configurator/scene/index.d.ts +1 -0
  390. package/dist/configurator/scene/index.js +1 -1
  391. package/dist/configurator/scene/scene-controls/DeleteEntityFab.js +1 -1
  392. package/dist/configurator/scene/scene-controls/SelectionHotspot.js +1 -1
  393. package/dist/configurator/scene/scene-controls/ToolFab.d.ts +4 -0
  394. package/dist/configurator/scene/scene-controls/ToolFab.js +1 -1
  395. package/dist/configurator/scene/scene-controls/hotspot-drag-handle/HotspotDragHandle.d.ts +1 -1
  396. package/dist/configurator/scene/scene-controls/hotspot-drag-handle/HotspotDragSceneHandle.d.ts +1 -1
  397. package/dist/configurator/scene/scene-controls/use-scene-control-tooltip.d.ts +2 -1
  398. package/dist/configurator/scene/scene-entity/dimension-indicators/DimensionIndicator.d.ts +1 -1
  399. package/dist/configurator/scene/scene-entity/dimension-indicators/DimensionIndicatorInternal.d.ts +1 -1
  400. package/dist/configurator/scene/scene-entity/drag-and-drop/shared.d.ts +2 -2
  401. package/dist/configurator/scene/scene-entity/drag-and-drop/use-drag-object.d.ts +1 -1
  402. package/dist/configurator/scene/scene-entity/materials/RemoteReplacedMaterial.js +1 -1
  403. package/dist/configurator/scene/scene-entity/objects/SceneEntityObjectTextDecal.d.ts +1 -1
  404. package/dist/configurator/scene/scene-entity/scene-controls/SceneEntityHtmlHotspots.js +1 -1
  405. package/dist/configurator/toolbar-actions/index.d.ts +3 -0
  406. package/dist/configurator/toolbar-actions/index.js +1 -0
  407. package/dist/configurator/toolbar-actions/use-assistant-panel.d.ts +18 -0
  408. package/dist/configurator/toolbar-actions/use-assistant-panel.js +1 -0
  409. package/dist/configurator/toolbar-actions/use-measurements.d.ts +35 -0
  410. package/dist/configurator/toolbar-actions/use-measurements.js +1 -0
  411. package/dist/configurator/toolbar-actions/use-toolbar-actions.d.ts +100 -0
  412. package/dist/configurator/toolbar-actions/use-toolbar-actions.js +1 -0
  413. package/dist/configurator/toolbar-actions/use-toolbar-help.d.ts +7 -0
  414. package/dist/configurator/toolbar-actions/use-toolbar-help.js +1 -0
  415. package/dist/configurator/ui/assistant/AssistantChips.d.ts +10 -3
  416. package/dist/configurator/ui/assistant/AssistantChips.js +1 -1
  417. package/dist/configurator/ui/assistant/AssistantMarkdown.d.ts +7 -2
  418. package/dist/configurator/ui/assistant/AssistantMarkdown.js +1 -1
  419. package/dist/configurator/ui/assistant/AssistantPanel.js +1 -1
  420. package/dist/configurator/ui/assistant/AssistantThread.js +1 -1
  421. package/dist/configurator/ui/assistant/alert/AssistantAlert.d.ts +8 -0
  422. package/dist/configurator/ui/assistant/alert/AssistantAlert.js +1 -0
  423. package/dist/configurator/ui/assistant/alert/index.d.ts +2 -0
  424. package/dist/configurator/ui/assistant/alert/index.js +1 -0
  425. package/dist/configurator/ui/assistant/alert/remark-alerts.d.ts +10 -0
  426. package/dist/configurator/ui/assistant/alert/remark-alerts.js +1 -0
  427. package/dist/configurator/ui/assistant/entity-link/AssistantEntityLink.d.ts +13 -0
  428. package/dist/configurator/ui/assistant/entity-link/AssistantEntityLink.js +1 -0
  429. package/dist/configurator/ui/assistant/entity-link/entity-link-selection.d.ts +22 -0
  430. package/dist/configurator/ui/assistant/entity-link/entity-link-selection.js +1 -0
  431. package/dist/configurator/ui/assistant/entity-link/entity-link-state.d.ts +27 -0
  432. package/dist/configurator/ui/assistant/entity-link/entity-link-state.js +1 -0
  433. package/dist/configurator/ui/assistant/entity-link/index.d.ts +2 -0
  434. package/dist/configurator/ui/assistant/entity-link/index.js +1 -0
  435. package/dist/configurator/ui/assistant/form/AssistantForm.d.ts +19 -0
  436. package/dist/configurator/ui/assistant/form/AssistantForm.js +1 -0
  437. package/dist/configurator/ui/assistant/form/compose-answer.d.ts +11 -0
  438. package/dist/configurator/ui/assistant/form/compose-answer.js +1 -0
  439. package/dist/configurator/ui/assistant/form/describe-build-errors.d.ts +6 -0
  440. package/dist/configurator/ui/assistant/form/describe-build-errors.js +1 -0
  441. package/dist/configurator/ui/assistant/form/form-model.d.ts +37 -0
  442. package/dist/configurator/ui/assistant/form/form-model.js +1 -0
  443. package/dist/configurator/ui/assistant/form/form-state.d.ts +4 -0
  444. package/dist/configurator/ui/assistant/form/form-state.js +1 -0
  445. package/dist/configurator/ui/assistant/form/index.d.ts +4 -0
  446. package/dist/configurator/ui/assistant/form/index.js +1 -0
  447. package/dist/configurator/ui/assistant/form/parse-form-block.d.ts +10 -0
  448. package/dist/configurator/ui/assistant/form/parse-form-block.js +1 -0
  449. package/dist/configurator/ui/assistant/form/read-form-attributes.d.ts +13 -0
  450. package/dist/configurator/ui/assistant/form/read-form-attributes.js +1 -0
  451. package/dist/configurator/ui/assistant/form/registry/built-in-form-elements.d.ts +3 -0
  452. package/dist/configurator/ui/assistant/form/registry/built-in-form-elements.js +1 -0
  453. package/dist/configurator/ui/assistant/form/registry/create-assistant-form-element.d.ts +31 -0
  454. package/dist/configurator/ui/assistant/form/registry/create-assistant-form-element.js +1 -0
  455. package/dist/configurator/ui/assistant/form/registry/form-element-definition.d.ts +16 -0
  456. package/dist/configurator/ui/assistant/form/registry/form-element-definition.js +1 -0
  457. package/dist/configurator/ui/assistant/form/registry/form-registry.d.ts +31 -0
  458. package/dist/configurator/ui/assistant/form/registry/form-registry.js +1 -0
  459. package/dist/configurator/ui/assistant/form/registry/index.d.ts +6 -0
  460. package/dist/configurator/ui/assistant/form/registry/index.js +1 -0
  461. package/dist/configurator/ui/assistant/form/registry/use-form-registry.d.ts +8 -0
  462. package/dist/configurator/ui/assistant/form/registry/use-form-registry.js +1 -0
  463. package/dist/configurator/ui/assistant/form-elements/FormCard.d.ts +9 -0
  464. package/dist/configurator/ui/assistant/form-elements/FormCard.js +1 -0
  465. package/dist/configurator/ui/assistant/form-elements/FormConfirm.d.ts +9 -0
  466. package/dist/configurator/ui/assistant/form-elements/FormConfirm.js +1 -0
  467. package/dist/configurator/ui/assistant/form-elements/FormGroup.d.ts +6 -0
  468. package/dist/configurator/ui/assistant/form-elements/FormGroup.js +1 -0
  469. package/dist/configurator/ui/assistant/form-elements/FormNumber.d.ts +9 -0
  470. package/dist/configurator/ui/assistant/form-elements/FormNumber.js +1 -0
  471. package/dist/configurator/ui/assistant/form-elements/FormOptions.d.ts +6 -0
  472. package/dist/configurator/ui/assistant/form-elements/FormOptions.js +1 -0
  473. package/dist/configurator/ui/assistant/form-elements/FormReply.d.ts +6 -0
  474. package/dist/configurator/ui/assistant/form-elements/FormReply.js +1 -0
  475. package/dist/configurator/ui/assistant/form-elements/FormSwatch.d.ts +10 -0
  476. package/dist/configurator/ui/assistant/form-elements/FormSwatch.js +1 -0
  477. package/dist/configurator/ui/assistant/form-elements/FormText.d.ts +6 -0
  478. package/dist/configurator/ui/assistant/form-elements/FormText.js +1 -0
  479. package/dist/configurator/ui/assistant/form-elements/clamp-form-number.d.ts +10 -0
  480. package/dist/configurator/ui/assistant/form-elements/clamp-form-number.js +1 -0
  481. package/dist/configurator/ui/assistant/form-elements/form-element-props.d.ts +38 -0
  482. package/dist/configurator/ui/assistant/form-elements/form-element-props.js +1 -0
  483. package/dist/configurator/ui/assistant/form-elements/index.d.ts +9 -0
  484. package/dist/configurator/ui/assistant/form-elements/index.js +1 -0
  485. package/dist/configurator/ui/assistant/index.d.ts +3 -0
  486. package/dist/configurator/ui/assistant/index.js +1 -1
  487. package/dist/configurator/ui/assistant/reference-link/AssistantReferenceLink.d.ts +16 -0
  488. package/dist/configurator/ui/assistant/reference-link/AssistantReferenceLink.js +1 -0
  489. package/dist/configurator/ui/assistant/reference-link/assistant-link-target.d.ts +34 -0
  490. package/dist/configurator/ui/assistant/reference-link/assistant-link-target.js +1 -0
  491. package/dist/configurator/ui/assistant/reference-link/assistant-reference-details.d.ts +25 -0
  492. package/dist/configurator/ui/assistant/reference-link/assistant-reference-details.js +1 -0
  493. package/dist/configurator/ui/assistant/reference-link/index.d.ts +3 -0
  494. package/dist/configurator/ui/assistant/reference-link/index.js +1 -0
  495. package/dist/configurator/ui/assistant/reference-link/reference-styles.d.ts +12 -0
  496. package/dist/configurator/ui/assistant/reference-link/reference-styles.js +1 -0
  497. package/dist/configurator/ui/assistant/reference-link/use-assistant-reference-details.d.ts +8 -0
  498. package/dist/configurator/ui/assistant/reference-link/use-assistant-reference-details.js +1 -0
  499. package/dist/configurator/ui/assistant/resource-image/AssistantImage.d.ts +9 -0
  500. package/dist/configurator/ui/assistant/resource-image/AssistantImage.js +1 -0
  501. package/dist/configurator/ui/assistant/resource-image/index.d.ts +3 -0
  502. package/dist/configurator/ui/assistant/resource-image/index.js +1 -0
  503. package/dist/configurator/ui/assistant/resource-image/resource-path.d.ts +2 -0
  504. package/dist/configurator/ui/assistant/resource-image/resource-path.js +1 -0
  505. package/dist/configurator/ui/assistant/resource-image/use-resource-image-url.d.ts +5 -0
  506. package/dist/configurator/ui/assistant/resource-image/use-resource-image-url.js +1 -0
  507. package/dist/configurator/ui/assistant/screenshots/ScreenshotThumbnails.d.ts +9 -0
  508. package/dist/configurator/ui/assistant/screenshots/ScreenshotThumbnails.js +1 -0
  509. package/dist/configurator/ui/assistant/screenshots/index.d.ts +2 -0
  510. package/dist/configurator/ui/assistant/screenshots/index.js +1 -0
  511. package/dist/configurator/ui/assistant/screenshots/tool-screenshots.d.ts +14 -0
  512. package/dist/configurator/ui/assistant/screenshots/tool-screenshots.js +1 -0
  513. package/dist/configurator/ui/assistant/streamed-markdown.d.ts +4 -2
  514. package/dist/configurator/ui/assistant/streamed-markdown.js +2 -2
  515. package/dist/configurator/ui/assistant/thread-items.d.ts +8 -1
  516. package/dist/configurator/ui/assistant/thread-items.js +1 -1
  517. package/dist/configurator/ui/assistant/tool-chip.js +1 -1
  518. package/dist/configurator/ui/assistant/use-follow-stream.js +1 -1
  519. package/dist/configurator/ui/configurator-layout/sidebar-keys/index.d.ts +1 -0
  520. package/dist/configurator/ui/configurator-layout/sidebar-keys/index.js +1 -0
  521. package/dist/configurator/ui/configurator-layout/sidebar-keys/sidebar-keys.d.ts +11 -0
  522. package/dist/configurator/ui/configurator-layout/sidebar-keys/sidebar-keys.js +1 -0
  523. package/dist/configurator/ui/configurator-layout/slots/canvas/LayoutCanvas.js +1 -1
  524. package/dist/configurator/ui/configurator-layout/slots/canvas/SharedCanvas.js +1 -1
  525. package/dist/configurator/ui/configurator-layout/{universal → slots/desktop-sidebar}/DesktopSidebar.d.ts +23 -14
  526. package/dist/configurator/ui/configurator-layout/slots/desktop-sidebar/DesktopSidebar.js +16 -0
  527. package/dist/configurator/ui/configurator-layout/slots/desktop-sidebar/index.d.ts +1 -0
  528. package/dist/configurator/ui/configurator-layout/slots/desktop-sidebar/index.js +1 -0
  529. package/dist/configurator/ui/configurator-layout/slots/mobile-tabs/use-mobile-tabs.d.ts +1 -1
  530. package/dist/configurator/ui/configurator-layout/slots/mobile-tabs/use-mobile-tabs.js +1 -1
  531. package/dist/configurator/ui/configurator-layout/slots/services/InvariantViolations.d.ts +4 -4
  532. package/dist/configurator/ui/configurator-layout/slots/services/InvariantViolations.js +1 -1
  533. package/dist/configurator/ui/configurator-layout/slots/services/Services.js +1 -1
  534. package/dist/configurator/ui/configurator-layout/slots/services/SideEffects.d.ts +3 -6
  535. package/dist/configurator/ui/configurator-layout/slots/services/SideEffects.js +1 -1
  536. package/dist/configurator/ui/configurator-layout/slots/sidebar-tabs/use-sidebar-tabs.d.ts +1 -1
  537. package/dist/configurator/ui/configurator-layout/slots/sidebar-tabs/use-sidebar-tabs.js +1 -1
  538. package/dist/configurator/ui/configurator-layout/slots/slots.d.ts +1 -0
  539. package/dist/configurator/ui/configurator-layout/slots/slots.js +1 -1
  540. package/dist/configurator/ui/configurator-layout/universal/DesktopLayout.d.ts +1 -1
  541. package/dist/configurator/ui/configurator-layout/universal/DesktopLayout.js +2 -2
  542. package/dist/configurator/ui/configurator-layout/universal/ExportingOverlay.d.ts +5 -0
  543. package/dist/configurator/ui/configurator-layout/universal/ExportingOverlay.js +1 -0
  544. package/dist/configurator/ui/configurator-layout/universal/Layout.js +1 -1
  545. package/dist/configurator/ui/configurator-layout/universal/use-layout-scene-safe-area.d.ts +1 -1
  546. package/dist/configurator/ui/configurator-layout/universal/use-layout-scene-safe-area.js +1 -1
  547. package/dist/configurator/ui/configurator-layout/universal/use-scene-safe-area.d.ts +1 -1
  548. package/dist/configurator/ui/index.d.ts +1 -0
  549. package/dist/configurator/ui/index.js +1 -1
  550. package/dist/configurator/ui/legacy/layout/common/CommonFinalizeButton.js +1 -1
  551. package/dist/configurator/ui/legacy/layout/common/CommonSummaryButton.js +1 -1
  552. package/dist/configurator/ui/legacy/layout/common/CommonTopNavigation.js +1 -1
  553. package/dist/configurator/ui/legacy/layout/common/ReactiveInterfaceLayout.js +1 -1
  554. package/dist/configurator/ui/legacy/layout/common/ResponsiveFinalizeButton.js +1 -1
  555. package/dist/configurator/ui/legacy/layout/common/SummaryReadLayout.js +1 -1
  556. package/dist/configurator/ui/legacy/layout/simple/SimpleOwnerLayout.js +1 -1
  557. package/dist/configurator/ui/legacy/layout/simple/SimpleTopNavigation.js +1 -1
  558. package/dist/configurator/ui/legacy/layout/steps/StepsNavigation.d.ts +2 -2
  559. package/dist/configurator/ui/legacy/layout/steps/StepsNavigation.js +1 -1
  560. package/dist/configurator/ui/legacy/layout/steps/StepsOwnerLayout.js +1 -1
  561. package/dist/configurator/ui/legacy/layout/steps/StepsSidebarFooter.js +1 -1
  562. package/dist/configurator/ui/legacy/layout/tabs/TabsOwnerLayout.js +1 -1
  563. package/dist/configurator/ui/legacy/toolbar/common/DimensionUnitSelector.js +1 -1
  564. package/dist/configurator/ui/legacy/toolbar/common/MeasurementsIndicatorsSwitch.js +1 -1
  565. package/dist/configurator/ui/legacy/toolbar/desktop-toolbar/FullscreenToolbarAction.js +1 -1
  566. package/dist/configurator/ui/legacy/toolbar/desktop-toolbar/HelpToolbarAction.js +1 -1
  567. package/dist/configurator/ui/legacy/toolbar/desktop-toolbar/MeasurementsToolbarAction.js +1 -1
  568. package/dist/configurator/ui/legacy/toolbar/desktop-toolbar/UndoRedoToolbarGroup.js +1 -1
  569. package/dist/configurator/ui/phoenix-layout-elements/AppLoader.js +2 -6
  570. package/dist/configurator/ui/phoenix-layout-elements/AssistantToolbarButton.js +1 -1
  571. package/dist/configurator/ui/phoenix-layout-elements/EnterFullscreenToolbarButton.js +1 -1
  572. package/dist/configurator/ui/phoenix-layout-elements/ExitFullscreenToolbarButton.js +1 -1
  573. package/dist/configurator/ui/phoenix-layout-elements/HelpToolbarButton.js +1 -1
  574. package/dist/configurator/ui/phoenix-layout-elements/LanguageMenuAction.js +1 -1
  575. package/dist/configurator/ui/phoenix-layout-elements/LanguageToolbarButton.js +1 -1
  576. package/dist/configurator/ui/phoenix-layout-elements/LayoutCustomOwnerUi.d.ts +1 -0
  577. package/dist/configurator/ui/phoenix-layout-elements/LayoutCustomOwnerUi.js +1 -1
  578. package/dist/configurator/ui/phoenix-layout-elements/LayoutCustomReadUi.d.ts +1 -0
  579. package/dist/configurator/ui/phoenix-layout-elements/LayoutCustomReadUi.js +1 -1
  580. package/dist/configurator/ui/phoenix-layout-elements/MeasurementsMenuAction.js +1 -1
  581. package/dist/configurator/ui/phoenix-layout-elements/MeasurementsToolbarButton.js +1 -1
  582. package/dist/configurator/ui/phoenix-layout-elements/RedoToolbarButton.js +1 -1
  583. package/dist/configurator/ui/phoenix-layout-elements/ShareToolbarButton.js +1 -1
  584. package/dist/configurator/ui/phoenix-layout-elements/UndoToolbarButton.js +1 -1
  585. package/dist/configurator/ui/phoenix-layout-elements/header/common/back-button/use-go-back-action.js +1 -1
  586. package/dist/configurator/ui/phoenix-layout-elements/header/common/horizontal-tabs/DesktopHorizontalTabs.js +1 -1
  587. package/dist/configurator/ui/phoenix-layout-elements/header/common/step-navigation/DesktopNavigationSteps.d.ts +1 -1
  588. package/dist/configurator/ui/phoenix-layout-elements/header/common/step-navigation/DesktopNavigationSteps.js +1 -1
  589. package/dist/configurator/ui/phoenix-layout-elements/header/common/step-navigation/StepsNavigation.d.ts +2 -8
  590. package/dist/configurator/ui/phoenix-layout-elements/header/common/step-navigation/StepsNavigation.js +1 -1
  591. package/dist/configurator/ui/phoenix-layout-elements/measurements/DimensionUnitSelector.js +1 -1
  592. package/dist/configurator/ui/phoenix-layout-elements/measurements/MeasurementsIndicatorsSwitch.js +1 -1
  593. package/dist/configurator/ui/phoenix-layout-elements/share/ShareModal.js +1 -1
  594. package/dist/configurator/ui/phoenix-layout-elements/share/use-share.d.ts +7 -8
  595. package/dist/configurator/ui/phoenix-layout-elements/share/use-share.js +1 -1
  596. package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/basic/BasicFinalizeButton.js +1 -1
  597. package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/CustomFinalizeButton.js +1 -1
  598. package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/EnterSummaryButton.js +1 -1
  599. package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/FinalizeActions.js +1 -1
  600. package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/NextStepButton.js +1 -1
  601. package/dist/configurator/ui/phoenix-layout-elements/sidebar-footer/common/PreviousStepButton.js +1 -1
  602. package/dist/configurator/ui/preset-layout/CategoryPresetUi.js +1 -1
  603. package/dist/configurator/ui/preset-layout/PresetUi.d.ts +4 -0
  604. package/dist/configurator/ui/preset-layout/PresetUi.js +1 -1
  605. package/dist/configurator/ui/save-message-snackbars/SaveMessageSnackbars.d.ts +10 -0
  606. package/dist/configurator/ui/save-message-snackbars/SaveMessageSnackbars.js +1 -0
  607. package/dist/configurator/ui/save-message-snackbars/index.d.ts +1 -0
  608. package/dist/configurator/ui/save-message-snackbars/index.js +1 -0
  609. package/dist/docs/complex-use-cases/focus-mode.mdx +2 -0
  610. package/dist/docs/controls/select-child-entity/cardselect.mdx +1 -0
  611. package/dist/docs/dimensions/3d-scene-integration/dimension-indicators.mdx +2 -2
  612. package/dist/docs/dimensions/3d-scene-integration/editable-angle-indicator.mdx +1 -1
  613. package/dist/docs/dimensions/3d-scene-integration/editable-distance-dimension-indicator.mdx +1 -1
  614. package/dist/docs/dimensions/3d-scene-integration/list-selector-dimension-indicator.mdx +1 -1
  615. package/dist/docs/drag-and-drop/drag-handle.mdx +1 -1
  616. package/dist/docs/drag-and-drop/hotspot-handle.mdx +1 -1
  617. package/dist/docs/drag-and-drop/planes.mdx +1 -1
  618. package/dist/docs/drag-and-drop/rotatable.mdx +2 -2
  619. package/dist/docs/fabs/basic.mdx +6 -2
  620. package/dist/docs/index.json +49 -1
  621. package/dist/docs/layouts/custom-layout.mdx +1640 -184
  622. package/dist/docs/layouts/gallery/kestrel-kettle.mdx +1858 -0
  623. package/dist/docs/layouts/header/fixed.mdx +2 -2
  624. package/dist/docs/layouts/header/floating.mdx +2 -2
  625. package/dist/docs/layouts/sidebar/in-a-custom-layout.mdx +380 -0
  626. package/dist/docs/layouts/sidebar-footer/basic.mdx +32 -77
  627. package/dist/docs/other-features/client-side-rendering/declarative.mdx +2 -2
  628. package/dist/docs/other-features/error-5xx.mdx +1 -1
  629. package/dist/docs/other-features/navigation-hotspot.mdx +2 -0
  630. package/dist/docs/other-features/not-found-404.mdx +1 -1
  631. package/dist/docs/other-features/transformers.mdx +4 -0
  632. package/dist/docs/phoenix/application-states.mdx +9 -7
  633. package/dist/docs/phoenix/clerk-sign-in.mdx +1 -1
  634. package/dist/docs/phoenix/components.mdx +6 -0
  635. package/dist/docs/phoenix/configurator-interface.mdx +302 -23
  636. package/dist/docs/phoenix/deployments.mdx +2 -6
  637. package/dist/docs/phoenix/domain-model.mdx +2 -0
  638. package/dist/docs/phoenix/finalization.mdx +1 -1
  639. package/dist/docs/phoenix/getting-started.mdx +6 -3
  640. package/dist/docs/phoenix/layout-slots.mdx +116 -0
  641. package/dist/docs/phoenix/localization.mdx +10 -6
  642. package/dist/docs/phoenix/migration-guide.mdx +24 -11
  643. package/dist/docs/phoenix/phoenix-cli.mdx +72 -21
  644. package/dist/docs/phoenix/project-structure.mdx +72 -34
  645. package/dist/docs/phoenix/quickstart.mdx +7 -7
  646. package/dist/docs/phoenix/sdk-builder-workflow.mdx +8 -8
  647. package/dist/docs/phoenix/testing.mdx +148 -0
  648. package/dist/docs/phoenix/webhooks.mdx +21 -4
  649. package/dist/docs/selection-and-highlight/hotspots.mdx +2 -0
  650. package/dist/domain/application/application-config.d.ts +3 -1
  651. package/dist/domain/application/assistant/assistant-manifest.d.ts +16 -1
  652. package/dist/domain/application/assistant/assistant-resolved-manifest.d.ts +8 -1
  653. package/dist/domain/application/assistant/build-assistant-manifest.js +1 -1
  654. package/dist/domain/application/assistant/form/assistant-form-element-name.d.ts +11 -0
  655. package/dist/domain/application/assistant/form/assistant-form-element-name.js +8 -0
  656. package/dist/domain/application/assistant/form/assistant-form-elements.d.ts +174 -0
  657. package/dist/domain/application/assistant/form/assistant-form-elements.js +1 -0
  658. package/dist/domain/application/assistant/form/assistant-resource-path.d.ts +10 -0
  659. package/dist/domain/application/assistant/form/assistant-resource-path.js +1 -0
  660. package/dist/domain/application/assistant/form/index.d.ts +4 -0
  661. package/dist/domain/application/assistant/form/index.js +1 -0
  662. package/dist/domain/application/assistant/generate-assistant-prompt.d.ts +3 -0
  663. package/dist/domain/application/assistant/generate-assistant-prompt.js +1 -1
  664. package/dist/domain/application/assistant/index.d.ts +2 -1
  665. package/dist/domain/application/assistant/index.js +1 -1
  666. package/dist/domain/application/assistant/session-setup/apply-assistant-setup.js +1 -1
  667. package/dist/domain/application/assistant/session-setup/assistant-setup-manifest.d.ts +2 -0
  668. package/dist/domain/application/assistant/session-setup/assistant-setup-manifest.js +1 -1
  669. package/dist/domain/application/assistant-recipe-groups.d.ts +3 -1
  670. package/dist/domain/application/configurator-builder.d.ts +4 -1
  671. package/dist/domain/application/configurator-builder.js +1 -1
  672. package/dist/domain/available-system-components.d.ts +1 -1
  673. package/dist/domain/available-system-components.js +1 -1
  674. package/dist/domain/core/invariant/violation.d.ts +50 -2
  675. package/dist/domain/core/invariant/violation.js +1 -1
  676. package/dist/domain/core/model/model.d.ts +1 -1
  677. package/dist/domain/core/schema/extended-schema-validator.js +1 -1
  678. package/dist/domain/domain-context/index.d.ts +1 -1
  679. package/dist/domain/domain-context/store-system/store-system.d.ts +5 -0
  680. package/dist/domain/domain-context/store-system/store-system.js +1 -1
  681. package/dist/domain/domain-context/undo-system.d.ts +28 -1
  682. package/dist/domain/domain-context/undo-system.js +1 -1
  683. package/dist/domain/domain-runtime/domain-runtime.d.ts +4 -0
  684. package/dist/domain/domain-runtime/environment/types.d.ts +1 -1
  685. package/dist/domain/domain-runtime/spec/index.d.ts +1 -1
  686. package/dist/domain/domain-runtime/spec/index.js +1 -1
  687. package/dist/domain/domain-runtime/spec/initialize.d.ts +11 -0
  688. package/dist/domain/domain-runtime/spec/initialize.js +1 -1
  689. package/dist/domain/domain-runtime/spec/module-checks/element-registration.d.ts +27 -12
  690. package/dist/domain/domain-runtime/spec/module-checks/element-registration.js +1 -1
  691. package/dist/domain/domain-runtime/spec/module-checks/find-assistant-form-element-errors.d.ts +12 -0
  692. package/dist/domain/domain-runtime/spec/module-checks/find-assistant-form-element-errors.js +1 -0
  693. package/dist/domain/domain-runtime/spec/module-checks/index.d.ts +2 -1
  694. package/dist/domain/domain-runtime/spec/module-checks/index.js +1 -1
  695. package/dist/domain/domain-runtime/spec/selectors.d.ts +3 -1
  696. package/dist/domain/domain-runtime/spec/selectors.js +1 -1
  697. package/dist/domain/domain-runtime/spec/types.d.ts +2 -0
  698. package/dist/domain/extensions/attribute-switch.d.ts +1 -1
  699. package/dist/domain/extensions/attribute-switch.js +1 -1
  700. package/dist/domain/extensions/canvas-config.d.ts +1 -1
  701. package/dist/domain/extensions/common/angle-indicator-schema.d.ts +2 -2
  702. package/dist/domain/extensions/common/dimension-indicator-schema.d.ts +2 -2
  703. package/dist/domain/extensions/editable-angle-indicator.d.ts +1 -1
  704. package/dist/domain/extensions/editable-distance-dimension-indicator.d.ts +1 -1
  705. package/dist/domain/extensions/editable-offset-indicator.d.ts +1 -1
  706. package/dist/domain/extensions/editable-offset-indicator.js +1 -1
  707. package/dist/domain/extensions/export-images/agent-screenshot.d.ts +24 -0
  708. package/dist/domain/extensions/export-images/agent-screenshot.js +1 -0
  709. package/dist/domain/extensions/export-images/client-side-image.d.ts +14 -0
  710. package/dist/domain/extensions/export-images/client-side-image.js +1 -0
  711. package/dist/domain/extensions/export-images/index.d.ts +3 -0
  712. package/dist/domain/extensions/export-images/index.js +1 -0
  713. package/dist/domain/extensions/{save-template-thumbnail.d.ts → export-images/save-template-thumbnail.d.ts} +4 -4
  714. package/dist/domain/extensions/export-images/save-template-thumbnail.js +1 -0
  715. package/dist/domain/extensions/fit-selection-orthographic-camera-component.js +1 -1
  716. package/dist/domain/extensions/fit-selection-perspective-camera-component.js +1 -1
  717. package/dist/domain/extensions/horizontal-tabs.d.ts +2 -2
  718. package/dist/domain/extensions/index.d.ts +1 -2
  719. package/dist/domain/extensions/index.js +1 -1
  720. package/dist/domain/extensions/list-selector-dimension-indicator.d.ts +1 -1
  721. package/dist/domain/extensions/navigation-steps.d.ts +2 -2
  722. package/dist/domain/extensions/offset-indicator.js +1 -1
  723. package/dist/domain/extensions/rotation-handle.d.ts +2 -2
  724. package/dist/domain/extensions/scene-entity-animation.js +1 -1
  725. package/dist/domain/mocks/create-mock-domain-runtime.d.ts +2 -0
  726. package/dist/domain/mocks/create-test-application-config.d.ts +59 -0
  727. package/dist/domain/mocks/create-test-application-config.js +1 -0
  728. package/dist/domain/mocks/index.d.ts +1 -0
  729. package/dist/domain/mocks/index.js +1 -1
  730. package/dist/domain/system-components/application-config.d.ts +2 -2
  731. package/dist/domain/system-components/child-entity-selection.js +1 -1
  732. package/dist/domain/system-components/collision-box-geometry.d.ts +4 -2
  733. package/dist/domain/system-components/collision-geometry.d.ts +4 -2
  734. package/dist/domain/system-components/collision-point-geometry.d.ts +4 -2
  735. package/dist/domain/system-components/common/material-override-schema.d.ts +2 -2
  736. package/dist/domain/system-components/common/recipes.d.ts +21 -3
  737. package/dist/domain/system-components/common/recipes.js +1 -1
  738. package/dist/domain/system-components/common/scene-entity-control-schema.d.ts +3 -0
  739. package/dist/domain/system-components/common/scene-entity-control-schema.js +1 -1
  740. package/dist/domain/system-components/constraint.d.ts +10 -23
  741. package/dist/domain/system-components/drag-handle.d.ts +1 -1
  742. package/dist/domain/system-components/drag-handle.js +1 -1
  743. package/dist/domain/system-components/fit-entity-orthographic-camera-component.js +1 -1
  744. package/dist/domain/system-components/fit-entity-perspective-camera-component.js +1 -1
  745. package/dist/domain/system-components/fit-root-orthographic-camera-component.js +1 -1
  746. package/dist/domain/system-components/fit-root-perspective-camera-component.js +1 -1
  747. package/dist/domain/system-components/hotspot-drag-handle.d.ts +1 -1
  748. package/dist/domain/system-components/hotspot-drag-handle.js +1 -1
  749. package/dist/domain/system-components/material-swatch.js +1 -1
  750. package/dist/domain/system-components/scene-entity-material-override.d.ts +1 -1
  751. package/dist/domain/system-components/scene-entity-object-material-override.d.ts +1 -1
  752. package/dist/domain/system-components/scene-entity-object-text-decal.d.ts +1 -1
  753. package/dist/domain/system-components/scene-entity-object-transimissive-material-override.d.ts +1 -1
  754. package/dist/domain/system-components/scene-entity-transimissive-material-override.d.ts +1 -1
  755. package/dist/domain/system-components/static-orthographic-camera-component.js +1 -1
  756. package/dist/domain/system-components/static-perspective-camera-component.js +1 -1
  757. package/dist/editor/Editor.d.ts +7 -0
  758. package/dist/editor/Editor.js +1 -1
  759. package/dist/editor/domain-editors/DomainSpecContext.js +1 -1
  760. package/dist/editor/domain-editors/apply-spec-update.d.ts +8 -1
  761. package/dist/editor/domain-editors/apply-spec-update.js +1 -1
  762. package/dist/editor/domain-editors/use-implementation-files.js +1 -1
  763. package/dist/editor/mocks/CreateMockYaml.js +2 -2
  764. package/dist/editor/mocks/EditorMockContext.d.ts +2 -0
  765. package/dist/editor/mocks/EditorMockContext.js +1 -1
  766. package/dist/editor/screenshot/ScreenshotHandler.js +1 -1
  767. package/dist/globals/index.d.ts +2 -0
  768. package/dist/i18n/locales/cs/messages.js +1 -1
  769. package/dist/i18n/locales/da/messages.js +1 -1
  770. package/dist/i18n/locales/de/messages.js +1 -1
  771. package/dist/i18n/locales/de-AT/messages.js +1 -1
  772. package/dist/i18n/locales/en/messages.js +1 -1
  773. package/dist/i18n/locales/en-AU/messages.js +1 -1
  774. package/dist/i18n/locales/en-GB/messages.js +1 -1
  775. package/dist/i18n/locales/es/messages.js +1 -1
  776. package/dist/i18n/locales/fr/messages.js +1 -1
  777. package/dist/i18n/locales/hu/messages.js +1 -1
  778. package/dist/i18n/locales/it/messages.js +1 -1
  779. package/dist/i18n/locales/nl/messages.js +1 -1
  780. package/dist/i18n/locales/pl/messages.js +1 -1
  781. package/dist/i18n/locales/pt/messages.js +1 -1
  782. package/dist/i18n/locales/ro/messages.js +1 -1
  783. package/dist/i18n/locales/sk/messages.js +1 -1
  784. package/dist/i18n/locales/tr/messages.js +1 -1
  785. package/dist/legacy/domain-cli/utils/cache.d.ts +1 -1
  786. package/dist/legacy/domain-cli/utils/cache.js +1 -1
  787. package/dist/legacy/domain-spec/__generated/domain-schema.d.ts +176 -148
  788. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/claude-md.d.ts +1 -1
  789. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/cursor-rules.d.ts +1 -1
  790. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/shared-sdk-content.d.ts +1 -1
  791. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/shared-sdk-content.js +7 -1
  792. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/frontend-admin.js +15 -14
  793. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/frontend-configurator.js +14 -12
  794. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/frontend-embedded-admin.js +14 -13
  795. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/frontend-embedded-configurator.js +13 -11
  796. package/dist/phoenix/application-spec/application-spec-builder.d.ts +0 -2
  797. package/dist/phoenix/application-spec/application-spec-builder.js +1 -1
  798. package/dist/phoenix/application-spec/generate-application-spec-project.d.ts +0 -2
  799. package/dist/phoenix/application-spec/generate-application-spec-project.js +2 -2
  800. package/dist/phoenix/application-spec/generate-application-spec-story.js +2 -2
  801. package/dist/phoenix/application-spec/generate-project-files.d.ts +0 -2
  802. package/dist/phoenix/application-spec/project-files.js +1 -1
  803. package/dist/phoenix/bin.d.ts +2 -0
  804. package/dist/phoenix/bin.js +2 -0
  805. package/dist/phoenix/check-configs/built-in-eslint-config.d.ts +9 -0
  806. package/dist/phoenix/check-configs/built-in-eslint-config.js +1 -0
  807. package/dist/phoenix/check-configs/eslint-config.d.ts +10 -0
  808. package/dist/phoenix/check-configs/eslint-config.js +1 -0
  809. package/dist/phoenix/check-configs/index.d.ts +8 -0
  810. package/dist/phoenix/check-configs/index.js +1 -0
  811. package/dist/phoenix/check-configs/typescript-project.d.ts +5 -0
  812. package/dist/phoenix/check-configs/typescript-project.js +1 -0
  813. package/dist/phoenix/cli.d.ts +0 -1
  814. package/dist/phoenix/cli.js +1 -2
  815. package/dist/phoenix/commands/build.js +1 -1
  816. package/dist/phoenix/commands/check.js +1 -1
  817. package/dist/phoenix/commands/clean.js +1 -1
  818. package/dist/phoenix/commands/deploy.js +2 -2
  819. package/dist/phoenix/commands/deps/deps.d.ts +73 -0
  820. package/dist/phoenix/commands/deps/deps.js +3 -0
  821. package/dist/phoenix/commands/deps/index.d.ts +1 -0
  822. package/dist/phoenix/commands/deps/index.js +1 -0
  823. package/dist/phoenix/commands/deps/sdk-manifest.d.ts +12 -0
  824. package/dist/phoenix/commands/deps/sdk-manifest.js +1 -0
  825. package/dist/phoenix/commands/dev.js +5 -5
  826. package/dist/phoenix/commands/fix.js +1 -1
  827. package/dist/phoenix/commands/index.d.ts +1 -0
  828. package/dist/phoenix/commands/index.js +1 -1
  829. package/dist/phoenix/commands/init.d.ts +17 -5
  830. package/dist/phoenix/commands/init.js +1 -1
  831. package/dist/phoenix/commands/link.js +1 -1
  832. package/dist/phoenix/commands/prettier.js +1 -1
  833. package/dist/phoenix/commands/sync.js +1 -1
  834. package/dist/phoenix/commands/unlink.js +1 -1
  835. package/dist/phoenix/services/application-spec-project-watcher.d.ts +2 -2
  836. package/dist/phoenix/services/application-spec-project-watcher.js +3 -3
  837. package/dist/phoenix/services/lingui.d.ts +8 -0
  838. package/dist/phoenix/services/lingui.js +1 -1
  839. package/dist/phoenix/services/source-archive.d.ts +1 -0
  840. package/dist/phoenix/services/source-archive.js +1 -1
  841. package/dist/phoenix/services/swc.js +1 -1
  842. package/dist/phoenix/services/vite-config.js +1 -1
  843. package/dist/phoenix/utils/index.d.ts +4 -2
  844. package/dist/phoenix/utils/index.js +1 -1
  845. package/dist/phoenix/utils/node-version/index.d.ts +1 -0
  846. package/dist/phoenix/utils/node-version/index.js +1 -0
  847. package/dist/phoenix/utils/node-version/supported-node-version.d.ts +12 -0
  848. package/dist/phoenix/utils/node-version/supported-node-version.js +4 -0
  849. package/dist/phoenix/utils/package-info.d.ts +0 -9
  850. package/dist/phoenix/utils/package-info.js +1 -1
  851. package/dist/phoenix/utils/package-manager/index.d.ts +2 -0
  852. package/dist/phoenix/utils/package-manager/index.js +1 -0
  853. package/dist/phoenix/utils/package-manager/package-manager-commands.d.ts +19 -0
  854. package/dist/phoenix/utils/package-manager/package-manager-commands.js +1 -0
  855. package/dist/phoenix/utils/package-manager/pnpm-project.d.ts +9 -0
  856. package/dist/phoenix/utils/package-manager/pnpm-project.js +1 -0
  857. package/dist/phoenix/utils/run-command.d.ts +11 -0
  858. package/dist/phoenix/utils/run-command.js +1 -1
  859. package/dist/phoenix/utils/sdk-tool/index.d.ts +1 -0
  860. package/dist/phoenix/utils/sdk-tool/index.js +1 -0
  861. package/dist/phoenix/utils/sdk-tool/resolve-sdk-tool-bin.d.ts +15 -0
  862. package/dist/phoenix/utils/sdk-tool/resolve-sdk-tool-bin.js +1 -0
  863. package/dist/{configurator/common → react-sdk/application-runtime/common-services/analytics}/frontend-analytics.d.ts +6 -0
  864. package/dist/react-sdk/application-runtime/common-services/analytics/frontend-analytics.js +1 -0
  865. package/dist/react-sdk/application-runtime/common-services/analytics/index.d.ts +1 -0
  866. package/dist/react-sdk/application-runtime/common-services/analytics/index.js +1 -1
  867. package/dist/react-sdk/application-runtime/hooks/use-phoenix-element-slots.d.ts +2 -1
  868. package/dist/react-sdk/application-runtime/routing/routing-service.d.ts +34 -2
  869. package/dist/react-sdk/application-runtime/routing/routing-service.js +1 -1
  870. package/dist/react-sdk/application-runtime/runtime/create-application-runtime.d.ts +4 -0
  871. package/dist/react-sdk/application-runtime/runtime/create-application-runtime.js +1 -1
  872. package/dist/react-sdk/domain-application-context/DomainLifecycleBoundary.js +1 -1
  873. package/dist/react-sdk/domain-context/common-hooks/index.d.ts +8 -0
  874. package/dist/react-sdk/domain-context/common-hooks/index.js +1 -0
  875. package/dist/react-sdk/domain-context/common-hooks/use-configurator-messages.d.ts +41 -0
  876. package/dist/react-sdk/domain-context/common-hooks/use-configurator-messages.js +1 -0
  877. package/dist/react-sdk/domain-context/component-hooks/index.d.ts +1 -0
  878. package/dist/react-sdk/domain-context/component-hooks/index.js +1 -0
  879. package/dist/react-sdk/domain-context/component-hooks/use-component.js +1 -1
  880. package/dist/react-sdk/domain-context/entity-hooks/use-domain-entity-state.js +1 -1
  881. package/dist/react-sdk/domain-context/entity-hooks/use-entity-price.js +1 -1
  882. package/dist/react-sdk/domain-context/entity-hooks/use-root-entity-id.js +1 -1
  883. package/dist/react-sdk/domain-context/entity-hooks/use-root-entity-state.js +1 -1
  884. package/dist/react-sdk/domain-context/index.d.ts +8 -1
  885. package/dist/react-sdk/domain-context/index.js +1 -1
  886. package/dist/react-sdk/domain-context/system-hooks/composition-hooks.js +1 -1
  887. package/dist/react-sdk/domain-context/system-hooks/dimension-hooks.js +1 -1
  888. package/dist/react-sdk/domain-context/system-hooks/generate-share-link-hooks.d.ts +8 -0
  889. package/dist/react-sdk/domain-context/system-hooks/generate-share-link-hooks.js +1 -0
  890. package/dist/react-sdk/domain-context/system-hooks/hover-hooks.js +1 -1
  891. package/dist/react-sdk/domain-context/system-hooks/locale-switch-hooks.d.ts +24 -0
  892. package/dist/react-sdk/domain-context/system-hooks/locale-switch-hooks.js +1 -0
  893. package/dist/react-sdk/domain-context/system-hooks/navigation-hooks.d.ts +5 -0
  894. package/dist/react-sdk/domain-context/system-hooks/navigation-hooks.js +1 -1
  895. package/dist/react-sdk/domain-context/system-hooks/selection-hooks.js +1 -1
  896. package/dist/react-sdk/domain-context/system-hooks/share-links-hooks.d.ts +32 -0
  897. package/dist/react-sdk/domain-context/system-hooks/share-links-hooks.js +1 -0
  898. package/dist/react-sdk/domain-context/system-hooks/step-navigation-hooks.d.ts +57 -0
  899. package/dist/react-sdk/domain-context/system-hooks/step-navigation-hooks.js +1 -0
  900. package/dist/react-sdk/domain-context/system-hooks/tab-navigation-hooks.d.ts +61 -0
  901. package/dist/react-sdk/domain-context/system-hooks/tab-navigation-hooks.js +1 -0
  902. package/dist/react-sdk/domain-context/system-hooks/undo-redo-hooks.d.ts +20 -0
  903. package/dist/react-sdk/domain-context/system-hooks/undo-redo-hooks.js +1 -0
  904. package/dist/react-sdk/domain-controls/hooks/use-angle-dimension.d.ts +1 -1
  905. package/dist/react-sdk/elements/configurator-error-page-props.d.ts +20 -0
  906. package/dist/react-sdk/elements/configurator-error-page-props.js +1 -0
  907. package/dist/react-sdk/elements/index.d.ts +1 -0
  908. package/dist/react-sdk/handlers/FinalizeActionRunner.d.ts +13 -0
  909. package/dist/react-sdk/handlers/FinalizeActionRunner.js +1 -0
  910. package/dist/react-sdk/handlers/FinalizeActionsHost.d.ts +10 -0
  911. package/dist/react-sdk/handlers/FinalizeActionsHost.js +1 -0
  912. package/dist/react-sdk/handlers/FinalizeActionsProvider.d.ts +11 -0
  913. package/dist/react-sdk/handlers/FinalizeActionsProvider.js +1 -0
  914. package/dist/react-sdk/handlers/finalize-actions-context.d.ts +6 -0
  915. package/dist/react-sdk/handlers/finalize-actions-context.js +1 -0
  916. package/dist/react-sdk/handlers/finalize-actions-store.d.ts +23 -0
  917. package/dist/react-sdk/handlers/finalize-actions-store.js +1 -0
  918. package/dist/react-sdk/handlers/index.d.ts +6 -0
  919. package/dist/react-sdk/handlers/index.js +1 -1
  920. package/dist/react-sdk/handlers/use-finalize-action.d.ts +32 -0
  921. package/dist/react-sdk/handlers/use-finalize-action.js +1 -0
  922. package/dist/react-sdk/handlers/use-finalize-actions.d.ts +53 -0
  923. package/dist/react-sdk/handlers/use-finalize-actions.js +1 -0
  924. package/dist/react-sdk/index.d.ts +2 -0
  925. package/dist/react-sdk/index.js +1 -1
  926. package/dist/react-sdk/loading-state/loading-state-manager.d.ts +5 -0
  927. package/dist/react-sdk/loading-state/loading-state-manager.js +1 -1
  928. package/dist/react-sdk/responsive-state/ResponsiveStateProvider.d.ts +25 -0
  929. package/dist/react-sdk/responsive-state/ResponsiveStateProvider.js +1 -0
  930. package/dist/react-sdk/responsive-state/canvas-padding.d.ts +14 -0
  931. package/dist/react-sdk/responsive-state/canvas-padding.js +1 -0
  932. package/dist/react-sdk/responsive-state/index.d.ts +5 -0
  933. package/dist/react-sdk/responsive-state/index.js +1 -0
  934. package/dist/react-sdk/responsive-state/responsive-state-manager.d.ts +111 -0
  935. package/dist/react-sdk/responsive-state/responsive-state-manager.js +1 -0
  936. package/dist/react-sdk/responsive-state/use-responsive-state-manager.d.ts +10 -0
  937. package/dist/react-sdk/responsive-state/use-responsive-state-manager.js +1 -0
  938. package/dist/react-sdk/responsive-state/use-responsive-state.d.ts +11 -0
  939. package/dist/react-sdk/responsive-state/use-responsive-state.js +1 -0
  940. package/dist/react-sdk/scene-control-elements/AddEntityFabElement.d.ts +36 -0
  941. package/dist/react-sdk/scene-control-elements/AddEntityFabElement.js +1 -0
  942. package/dist/react-sdk/scene-control-elements/DeleteEntityFabElement.d.ts +44 -0
  943. package/dist/react-sdk/scene-control-elements/DeleteEntityFabElement.js +1 -0
  944. package/dist/react-sdk/scene-control-elements/NavigationHotspotElement.d.ts +38 -0
  945. package/dist/react-sdk/scene-control-elements/NavigationHotspotElement.js +1 -0
  946. package/dist/react-sdk/scene-control-elements/SelectionHotspotElement.d.ts +30 -0
  947. package/dist/react-sdk/scene-control-elements/SelectionHotspotElement.js +1 -0
  948. package/dist/react-sdk/scene-control-elements/ToolMenuFabElement.d.ts +30 -0
  949. package/dist/react-sdk/scene-control-elements/ToolMenuFabElement.js +1 -0
  950. package/dist/react-sdk/scene-control-elements/create-add-entity-fab.d.ts +9 -0
  951. package/dist/react-sdk/scene-control-elements/create-add-entity-fab.js +1 -0
  952. package/dist/react-sdk/scene-control-elements/create-delete-entity-fab.d.ts +9 -0
  953. package/dist/react-sdk/scene-control-elements/create-delete-entity-fab.js +1 -0
  954. package/dist/react-sdk/scene-control-elements/create-navigation-hotspot.d.ts +10 -0
  955. package/dist/react-sdk/scene-control-elements/create-navigation-hotspot.js +1 -0
  956. package/dist/react-sdk/scene-control-elements/create-selection-hotspot.d.ts +9 -0
  957. package/dist/react-sdk/scene-control-elements/create-selection-hotspot.js +1 -0
  958. package/dist/react-sdk/scene-control-elements/create-tool-menu-fab.d.ts +9 -0
  959. package/dist/react-sdk/scene-control-elements/create-tool-menu-fab.js +1 -0
  960. package/dist/react-sdk/scene-control-elements/index.d.ts +11 -0
  961. package/dist/react-sdk/scene-control-elements/index.js +1 -0
  962. package/dist/react-sdk/scene-control-elements/scene-control-tooltip-visibility.d.ts +5 -0
  963. package/dist/react-sdk/scene-control-elements/scene-control-tooltip-visibility.js +1 -0
  964. package/dist/react-sdk/session/NotAuthenticatedPage.d.ts +4 -0
  965. package/dist/react-sdk/session/NotAuthenticatedPage.js +1 -1
  966. package/dist/react-sdk/web-component/create-web-component.js +1 -1
  967. package/dist/react-sdk/web-component/god-mode/god-mode-registry.d.ts +30 -0
  968. package/dist/react-sdk/web-component/god-mode/god-mode-registry.js +1 -0
  969. package/dist/react-sdk/web-component/god-mode/index.d.ts +1 -0
  970. package/dist/react-sdk/web-component/god-mode/index.js +1 -0
  971. package/dist/ui-sdk/icons/__generated/Lightbulb.d.ts +3 -0
  972. package/dist/ui-sdk/icons/__generated/Lightbulb.js +1 -0
  973. package/dist/ui-sdk/icons/__generated/XCircle.d.ts +3 -0
  974. package/dist/ui-sdk/icons/__generated/XCircle.js +1 -0
  975. package/dist/ui-sdk/icons/__generated/index.d.ts +4 -0
  976. package/dist/ui-sdk/icons/__generated/index.js +1 -1
  977. package/dist/ui-sdk/icons/icons-registry.js +1 -1
  978. package/dist/ui-sdk/responsive-application/ResponsiveApplicationProvider.d.ts +9 -0
  979. package/dist/ui-sdk/responsive-application/ResponsiveApplicationProvider.js +1 -1
  980. package/dist/ui-sdk/responsive-application/index.d.ts +1 -0
  981. package/dist/ui-sdk/responsive-application/responsive-state-store.d.ts +57 -0
  982. package/dist/ui-sdk/responsive-application/responsive-state-store.js +1 -0
  983. package/dist/ui-sdk/responsive-application/use-store-responsive-info.d.ts +7 -0
  984. package/dist/ui-sdk/responsive-application/use-store-responsive-info.js +1 -0
  985. package/dist/ui-sdk/theme/types.d.ts +5 -1
  986. package/package.json +48 -36
  987. package/schema/domain-spec.json +1341 -217
  988. package/dist/application-spec/generate-application/code/templates/nvmrc.d.ts +0 -1
  989. package/dist/application-spec/generate-application/code/templates/nvmrc.js +0 -2
  990. package/dist/application-spec/generate-application/code/templates/package-json.d.ts +0 -10
  991. package/dist/application-spec/generate-application/code/templates/package-json.js +0 -1
  992. package/dist/application-spec/generate-application/code/templates/webhook-implementation.d.ts +0 -1
  993. package/dist/application-spec/generate-application/code/templates/webhook-implementation.js +0 -7
  994. package/dist/configurator/common/frontend-analytics.js +0 -1
  995. package/dist/configurator/ui/configurator-layout/universal/DesktopSidebar.js +0 -16
  996. package/dist/configurator/ui/phoenix-layout-elements/language/use-locale-options.d.ts +0 -4
  997. package/dist/configurator/ui/phoenix-layout-elements/language/use-locale-options.js +0 -1
  998. package/dist/configurator/ui/phoenix-layout-elements/measurements/use-measurements-visibility.d.ts +0 -4
  999. package/dist/configurator/ui/phoenix-layout-elements/measurements/use-measurements-visibility.js +0 -1
  1000. package/dist/domain/extensions/client-side-image.d.ts +0 -14
  1001. package/dist/domain/extensions/client-side-image.js +0 -1
  1002. package/dist/domain/extensions/save-template-thumbnail.js +0 -1
  1003. package/dist/phoenix/commands/generate-project-files.d.ts +0 -6
  1004. package/dist/phoenix/commands/generate-project-files.js +0 -1
  1005. package/dist/react-sdk/application-runtime/runtime/god-mode.d.ts +0 -19
  1006. package/dist/react-sdk/application-runtime/runtime/god-mode.js +0 -1
  1007. /package/dist/application-spec/{core → enhanced-json}/eval-code-uses-self.d.ts +0 -0
  1008. /package/dist/application-spec/{core → enhanced-json}/eval-code-uses-self.js +0 -0
  1009. /package/dist/application-spec/{core → enhanced-json}/eval-to-constant.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,12 @@ 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
+ - [Show messages](#show-messages)
12
+ - [Request a quote](#request-a-quote)
11
13
  - [Serve read mode](#serve-read-mode)
14
+ - [Fit a phone](#fit-a-phone)
15
+ - [Put a control on the product](#put-a-control-on-the-product)
16
+ - [Control the scene](#control-the-scene)
12
17
  - [The spec the layout reads](#the-spec-the-layout-reads)
13
18
  - [Try the example](#try-the-example)
14
19
  - [Test cases](#test-cases)
@@ -30,25 +35,44 @@ Create `src/elements/ConfiguratorLayout.tsx` with a default export made by `phx.
30
35
 
31
36
  ```tsx
32
37
  import { Box } from '@mui/material'
38
+ import { useState } from 'react'
33
39
 
34
40
  import { LayoutSlots, useSceneSafeArea } from '@salsita-ai/sdk/configurator'
41
+ import { useActiveViewports } from '@salsita-ai/sdk/react'
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'
39
49
 
40
50
  const GUTTER = 24
41
51
  const PANEL_WIDTH = 380
52
+ const PHONE_GUTTER = 16
53
+ const TOUCH_TARGET = 44
42
54
 
43
55
  const ConfiguratorLayout = () => {
44
56
  const { containerRef, safeAreaRef } = useSceneSafeArea()
45
57
  const isFullscreen = phx.frontend.useIsFullscreenActive()
58
+ // `null` until the container has been measured; the layout shows only the scene until then
59
+ const viewports = useActiveViewports()
60
+ const [isSheetOpen, setSheetOpen] = useState(false)
61
+
62
+ const isDesktop = viewports?.includes('desktop') ?? false
63
+ const gutter = isDesktop ? GUTTER : PHONE_GUTTER
46
64
 
47
65
  return (
48
- <Box component="div" ref={containerRef} sx={{ position: 'absolute', inset: 0 }}>
66
+ <Box
67
+ component="div"
68
+ ref={containerRef}
69
+ // `clip`, not `hidden`: what overflows, such as the sheet's fill below its edge, can never be scrolled into view,
70
+ // not even by the browser when the focus moves
71
+ sx={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', overflow: 'clip' }}
72
+ >
49
73
  <LayoutSlots.SharedCanvas />
50
74
 
51
- {!isFullscreen && (
75
+ {viewports !== null && isDesktop && !isFullscreen && (
52
76
  <Box
53
77
  component="aside"
54
78
  sx={{
@@ -62,31 +86,72 @@ const ConfiguratorLayout = () => {
62
86
  borderRadius: 5,
63
87
  bgcolor: 'background.paper',
64
88
  boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)',
65
- overflow: 'hidden',
89
+ // A very short container, such as a phone held sideways, scrolls the panel instead of clipping its footer
90
+ overflowX: 'hidden',
91
+ overflowY: 'auto',
92
+ overscrollBehavior: 'contain',
66
93
  }}
67
94
  >
68
95
  <ProductPanel />
69
96
  </Box>
70
97
  )}
71
98
 
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
- >
99
+ {viewports !== null && (
83
100
  <Box
84
101
  component="div"
85
- sx={{ position: 'absolute', insetBlockStart: GUTTER, insetInlineEnd: GUTTER, pointerEvents: 'auto' }}
102
+ ref={safeAreaRef}
103
+ sx={{
104
+ // Desktop: right of the panel. Phone: above the sheet, whose height the column gives the rest.
105
+ ...(isDesktop
106
+ ? {
107
+ position: 'absolute',
108
+ insetBlock: 0,
109
+ insetInlineStart: isFullscreen ? 0 : PANEL_WIDTH + 2 * GUTTER,
110
+ insetInlineEnd: 0,
111
+ }
112
+ : { position: 'relative', flex: '1 1 0', minBlockSize: 0 }),
113
+ pointerEvents: 'none',
114
+ }}
86
115
  >
87
- <FullscreenToggle />
116
+ <Box
117
+ component="div"
118
+ sx={{ position: 'absolute', insetBlockStart: gutter, insetInlineEnd: gutter, pointerEvents: 'auto' }}
119
+ >
120
+ <FullscreenToggle compact={!isDesktop} />
121
+ </Box>
122
+ <Box
123
+ component="div"
124
+ sx={{
125
+ position: 'absolute',
126
+ pointerEvents: 'auto',
127
+ // Desktop: bottom left, above the row of view controls. Phone: at the top beside the fullscreen button,
128
+ // clear of the sheet's handle and the view controls.
129
+ ...(isDesktop
130
+ ? { insetBlockEnd: 2 * GUTTER + TOUCH_TARGET, insetInlineStart: GUTTER }
131
+ : { insetBlockStart: gutter, insetInline: `${gutter}px ${2 * gutter + TOUCH_TARGET}px` }),
132
+ }}
133
+ >
134
+ <MessageToast />
135
+ </Box>
136
+ {/* A phone in fullscreen keeps only the reset, as the built-in layout does. Below 1024 pixels (`tablet`) the
137
+ free area beside the panel is narrow, so the reset shows only its icon there too. */}
138
+ <Box
139
+ component="div"
140
+ sx={{ position: 'absolute', insetBlockEnd: gutter, insetInlineEnd: gutter, pointerEvents: 'auto' }}
141
+ >
142
+ <SceneControls
143
+ compact={!isDesktop || (viewports?.includes('tablet') ?? false)}
144
+ resetOnly={!isDesktop && isFullscreen}
145
+ />
146
+ </Box>
88
147
  </Box>
89
- </Box>
148
+ )}
149
+
150
+ {viewports !== null && !isDesktop && !isFullscreen && (
151
+ <BottomSheet open={isSheetOpen} onOpenChange={setSheetOpen} label="Product details">
152
+ <ProductPanel collapsed={!isSheetOpen} inSheet />
153
+ </BottomSheet>
154
+ )}
90
155
  </Box>
91
156
  )
92
157
  }
@@ -99,11 +164,12 @@ The components it uses live in `src/_shared/`, which Phoenix does not import by
99
164
  ## What the layout has to do
100
165
 
101
166
  - **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`.
167
+ - **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
168
  - **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.
169
+ - **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, 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
170
  - **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.
171
+ - **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.
172
+ - **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 `useActiveViewports()` and renders a desktop or a phone arrangement, see [Fit a phone](#fit-a-phone).
107
173
  - **Serve read mode.** The same element renders a configuration shared for viewing, and the layout decides what a viewer sees.
108
174
 
109
175
  ## Read and change the product
@@ -113,13 +179,23 @@ The panel reads everything it shows from hooks and writes the customer's choice
113
179
  ```tsx
114
180
  import { Box, Button, Typography } from '@mui/material'
115
181
 
116
- import { detailComponent } from '@salsita-ai/sdk/domain'
117
- import { useResolveLocalizable } from '@salsita-ai/sdk/react'
182
+ import { detailComponent, finalizeActionComponent } from '@salsita-ai/sdk/domain'
183
+ import { PhoenixHandlerBoundary, useResolveLocalizable } from '@salsita-ai/sdk/react'
118
184
 
119
185
  import { phx } from '../../.phoenix/phx.js'
120
186
  import { EdgeFinishCards } from './EdgeFinishCards.js'
187
+ import { ReviewActions } from './ReviewActions.js'
121
188
 
122
- export const ProductPanel = () => {
189
+ const BUTTON_SX = { minBlockSize: 48, borderRadius: 999, textTransform: 'none', fontWeight: 600 } as const
190
+
191
+ interface ProductPanelProps {
192
+ // Only the name, the price and the action, for a collapsed bottom sheet
193
+ readonly collapsed?: boolean
194
+ // In a bottom sheet the cards region keeps the top of the cards in view, and the sheet scrolls when that does not fit
195
+ readonly inSheet?: boolean
196
+ }
197
+
198
+ export const ProductPanel = ({ collapsed = false, inSheet = false }: ProductPanelProps) => {
123
199
  const resolve = useResolveLocalizable()
124
200
  const detail = phx.frontend.useFirstRootComponent(detailComponent)
125
201
  const price = phx.frontend.useRootEntityPrice()
@@ -129,9 +205,13 @@ export const ProductPanel = () => {
129
205
  const navigationState = phx.frontend.useActiveNavigationState()
130
206
  const pushNavigate = phx.frontend.usePushNavigate()
131
207
  const popNavigate = phx.frontend.usePopNavigate()
208
+ const navigate = phx.frontend.useNavigate()
209
+ const finalizeActions = phx.frontend.useComponents({ componentTypes: [finalizeActionComponent] }) ?? []
132
210
 
133
211
  const isReadMode = accessInfo.viewMode
134
- const isReview = isReadMode || navigationState === 'checkout'
212
+ const isReview = isReadMode || navigationState !== 'configure'
213
+ // The spec's primary finalize action, or its first one
214
+ const quoteAction = finalizeActions.find(action => action.state.variant === 'primary') ?? finalizeActions[0]
135
215
  const chosenEdge = edgeFinish.options.find(option => option.value === edgeFinish.value)
136
216
 
137
217
  return (
@@ -152,29 +232,44 @@ export const ProductPanel = () => {
152
232
  <Typography variant="h5" component="h2" fontWeight={700}>
153
233
  {resolve(detail?.state.title)}
154
234
  </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} />
235
+ {!collapsed && (
236
+ <Typography variant="body2" color="text.secondary">
237
+ {resolve(detail?.state.description)}
238
+ </Typography>
175
239
  )}
176
240
  </Box>
177
241
 
242
+ {!collapsed && (
243
+ <Box
244
+ component="div"
245
+ sx={{
246
+ display: 'grid',
247
+ gap: 1.5,
248
+ paddingInline: 3,
249
+ paddingBlockEnd: 3,
250
+ // In a short container the region shrinks and scrolls; a scroll past its end moves its container on
251
+ minBlockSize: inSheet ? 120 : 0,
252
+ overflowY: 'auto',
253
+ }}
254
+ >
255
+ <Typography variant="subtitle2" component="h3">
256
+ Edge finish
257
+ </Typography>
258
+ {isReview ? (
259
+ <Box component="div" sx={{ padding: 2, borderRadius: 3, bgcolor: 'action.hover' }}>
260
+ <Typography variant="body2" fontWeight={600}>
261
+ {resolve(chosenEdge?.title)}
262
+ </Typography>
263
+ <Typography variant="caption" color="text.secondary">
264
+ {resolve(chosenEdge?.description)}
265
+ </Typography>
266
+ </Box>
267
+ ) : (
268
+ <EdgeFinishCards selection={edgeFinish} />
269
+ )}
270
+ </Box>
271
+ )}
272
+
178
273
  <Box component="footer" sx={{ display: 'grid', gap: 2, padding: 3, borderTop: 1, borderColor: 'divider' }}>
179
274
  <Box component="div" sx={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
180
275
  <Typography variant="body2" color="text.secondary">
@@ -184,42 +279,71 @@ export const ProductPanel = () => {
184
279
  {price}
185
280
  </Typography>
186
281
  </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'}
282
+ {!isReadMode && navigationState === 'configure' && (
283
+ <Button size="large" variant="contained" onClick={() => pushNavigate('checkout')} sx={BUTTON_SX}>
284
+ Review and request a quote
200
285
  </Button>
201
286
  )}
287
+ {!isReadMode && navigationState === 'checkout' && (
288
+ <>
289
+ <Typography variant="body2" color="text.secondary">
290
+ Your table is ready for a quote. A real shop collects the contact details here.
291
+ </Typography>
292
+ {quoteAction ? (
293
+ <PhoenixHandlerBoundary name={quoteAction.state.handler}>
294
+ <ReviewActions
295
+ handler={quoteAction.state.handler}
296
+ label={resolve(quoteAction.state.label)}
297
+ onBack={popNavigate}
298
+ />
299
+ </PhoenixHandlerBoundary>
300
+ ) : (
301
+ <Button size="large" variant="outlined" onClick={popNavigate} sx={BUTTON_SX}>
302
+ Back to the configuration
303
+ </Button>
304
+ )}
305
+ </>
306
+ )}
307
+ {!isReadMode && navigationState === 'requested' && (
308
+ <>
309
+ <Typography variant="body2" role="status">
310
+ Thank you. Your quote request is on its way, and the shop replies within two working days.
311
+ </Typography>
312
+ <Button size="large" variant="outlined" onClick={() => navigate('configure')} sx={BUTTON_SX}>
313
+ Back to the configuration
314
+ </Button>
315
+ </>
316
+ )}
202
317
  </Box>
203
318
  </>
204
319
  )
205
320
  }
206
321
  ```
207
322
 
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`. |
323
+ | Hook | What the layout uses it for |
324
+ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
325
+ | `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. |
326
+ | `phx.frontend.useRootEntityId()` | The id of the table, which the control hooks take. |
327
+ | `phx.frontend.useAttributeSelection(entityId, instanceName)` | The `attributeSelection` component named `edgeFinish`: the current `value` and the `options`, each with `title`, `description`, `value`, `disabled` and `onSelect`. |
328
+ | `phx.frontend.useRootEntityPrice()` | The root entity's `price` component, formatted for the current locale and currency, or `null` without one. |
329
+ | `phx.frontend.useAccessService()` | `accessInfo.viewMode` is `true` while a configuration shared for viewing is open, and also while the host sets the `readonly` property. |
330
+ | `phx.frontend.useActiveNavigationState()` | The active navigation state; the panel shows its review step while it is `checkout`. |
331
+ | `phx.frontend.usePushNavigate()` and `phx.frontend.usePopNavigate()` | Enter the review step and leave it again. |
332
+ | `phx.frontend.useNavigate()` | Return from the confirmation to the configuration, forgetting the review step. |
333
+ | `phx.frontend.useComponents({ componentTypes: [finalizeActionComponent] })` | The spec's `finalizeAction` components; the panel offers the primary one in its review step. `finalizeActionComponent` comes from `@salsita-ai/sdk/domain`. |
334
+ | `phx.frontend.useFinalizeAction(handler)` | Run the finalize action's handler, with its pending state, its error and whether finalizing is possible; see [Request a quote](#request-a-quote). |
335
+ | `phx.frontend.useIsFullscreenActive()` and `phx.frontend.useSetFullscreen()` | Read the fullscreen flag, and set it from the layout's own button. |
336
+ | `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). |
337
+ | `phx.frontend.useResetCamera()` | Move the camera back to the framing of the active camera, undoing the customer's orbit, pan and zoom. |
338
+ | `phx.frontend.useUndoRedo()` | Not used here: whether there is a change to undo or redo, and `undo` and `redo`, for a layout's own undo and redo buttons; see [Known gaps](#known-gaps). |
339
+ | `useActiveViewports()` from `@salsita-ai/sdk/react` | 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). |
340
+ | `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). |
341
+ | `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`. |
342
+ | `PhoenixHandlerBoundary` from `@salsita-ai/sdk/react` | Wraps the review step's buttons, so an edited handler takes effect without a reload. |
219
343
 
220
344
  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
345
 
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.
346
+ 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
347
 
224
348
  ## Draw the choices
225
349
 
@@ -365,57 +489,709 @@ The drawings are keyed by the option values of the spec. A spec can carry a pict
365
489
  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.
366
490
 
367
491
  ```tsx
368
- import { Button } from '@mui/material'
492
+ import { Button, Tooltip } from '@mui/material'
369
493
 
370
494
  import { phx } from '../../.phoenix/phx.js'
371
495
 
372
496
  const ENTER_ICON = 'M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5'
373
497
  const EXIT_ICON = 'M9 4v5H4M15 4v5h5M15 20v-5h5M9 20v-5H4'
374
498
 
375
- export const FullscreenToggle = () => {
499
+ interface FullscreenToggleProps {
500
+ // Only the icon, with the label as its accessible name and tooltip, where a label would cover the product
501
+ readonly compact?: boolean
502
+ }
503
+
504
+ export const FullscreenToggle = ({ compact = false }: FullscreenToggleProps) => {
376
505
  const isFullscreen = phx.frontend.useIsFullscreenActive()
377
506
  const setFullscreen = phx.frontend.useSetFullscreen()
507
+ const label = isFullscreen ? 'Exit fullscreen' : 'Fullscreen'
508
+ const icon = (
509
+ <svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
510
+ <path d={isFullscreen ? EXIT_ICON : ENTER_ICON} />
511
+ </svg>
512
+ )
513
+
514
+ return (
515
+ <Tooltip title={compact ? label : ''}>
516
+ <Button
517
+ onClick={() => setFullscreen(!isFullscreen)}
518
+ aria-label={compact ? label : undefined}
519
+ startIcon={compact ? undefined : icon}
520
+ sx={{
521
+ minBlockSize: 44,
522
+ minInlineSize: 44,
523
+ paddingInline: compact ? 0 : 2.5,
524
+ borderRadius: 999,
525
+ bgcolor: 'background.paper',
526
+ color: 'text.primary',
527
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
528
+ textTransform: 'none',
529
+ fontWeight: 600,
530
+ '&:hover': { bgcolor: 'background.paper', boxShadow: '0 6px 20px rgba(0, 0, 0, 0.18)' },
531
+ }}
532
+ >
533
+ {compact ? icon : label}
534
+ </Button>
535
+ </Tooltip>
536
+ )
537
+ }
538
+ ```
539
+
540
+ 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.
541
+
542
+ ## Show messages
543
+
544
+ 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 on a phone:
545
+
546
+ ```tsx
547
+ import { Alert } from '@mui/material'
548
+ import { useEffect, useState } from 'react'
549
+
550
+ import type { ConfiguratorMessage } from '@salsita-ai/sdk/react'
551
+
552
+ import { phx } from '../../.phoenix/phx.js'
553
+
554
+ const VIOLATION_SEVERITY = { fatal: 'error', fixable: 'error', warning: 'warning', notice: 'info' } as const
555
+ const RANK = { info: 0, warning: 1, error: 2 } as const
556
+ const VISIBLE_MS = 6000
557
+
558
+ const severityOf = (message: ConfiguratorMessage) => {
559
+ if (message.kind === 'violation') {
560
+ return VIOLATION_SEVERITY[message.violation.severity]
561
+ }
562
+
563
+ return message.kind === 'error' || message.kind === 'saveError' ? 'error' : 'info'
564
+ }
565
+
566
+ export const MessageToast = () => {
567
+ const [message, setMessage] = useState<ConfiguratorMessage | null>(null)
568
+
569
+ // A rejected change also reports the notices already present; keep the worst message while it is shown
570
+ phx.frontend.useConfiguratorMessages(next => {
571
+ setMessage(shown => (shown !== null && RANK[severityOf(next)] < RANK[severityOf(shown)] ? shown : next))
572
+ })
573
+
574
+ useEffect(() => {
575
+ if (message === null) {
576
+ return
577
+ }
578
+
579
+ const timeout = setTimeout(() => setMessage(null), VISIBLE_MS)
580
+
581
+ return () => clearTimeout(timeout)
582
+ }, [message])
583
+
584
+ if (message === null) {
585
+ return null
586
+ }
587
+
588
+ return (
589
+ <Alert
590
+ severity={severityOf(message)}
591
+ onClose={() => setMessage(null)}
592
+ sx={{ maxInlineSize: 420, borderRadius: 3, boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)' }}
593
+ >
594
+ {message.text}
595
+ </Alert>
596
+ )
597
+ }
598
+ ```
599
+
600
+ 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.
601
+
602
+ 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:
603
+
604
+ ```ts
605
+ import { msg } from '@lingui/core/macro'
606
+
607
+ import { phx } from '../../.phoenix/phx.js'
608
+
609
+ // A tree-trunk edge is cut by hand; choosing it carries a hint, which reaches the layout as a side-effect message
610
+ export default phx.frontend.transformerBuilder.when
611
+ .entity(['Table'])
612
+ .modified({ atFields: ['edgeFinish'] })
613
+ .then.overrideMatchingUpdate(({ update }) => {
614
+ if (update.stateDelta.edgeFinish !== 'TreeTrunk') {
615
+ return [update]
616
+ }
617
+
618
+ return [{ ...update, hint: msg`Tree trunk edges are cut by hand, so delivery takes three weeks longer.` }]
619
+ })
620
+ .build('tree-trunk-delivery')
621
+ ```
622
+
623
+ [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.
624
+
625
+ ## Request a quote
626
+
627
+ The spec declares what finalizing means for this product as a `finalizeAction` component, and the project registers the handler it names:
628
+
629
+ ```yaml
630
+ components:
631
+ Table:
632
+ finalizeAction:
633
+ quote:
634
+ variant: primary
635
+ label: Request a quote
636
+ title: Send the configuration to the shop for a quote
637
+ icon: PaperPlaneTilt
638
+ handler: requestQuote
639
+ ```
640
+
641
+ ```ts
642
+ import { useCallback } from 'react'
643
+
644
+ import { phx } from '../../.phoenix/phx.js'
645
+
646
+ // Stands in for the shop's quote service: the request takes a moment, then the panel shows the confirmation
647
+ export default phx.frontend.createPhoenixHandler('requestQuote', () => {
648
+ const navigate = phx.frontend.useNavigate()
649
+
650
+ return {
651
+ handler: useCallback(async () => {
652
+ await new Promise(resolve => setTimeout(resolve, 1500))
653
+ navigate('requested')
654
+ }, [navigate]),
655
+ }
656
+ })
657
+ ```
658
+
659
+ 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()`.
660
+
661
+ The panel lists the finalize actions with `phx.frontend.useComponents({ componentTypes: [finalizeActionComponent] })` and offers the primary one, or the first when none is primary, in its review step. It renders the review step's buttons in a component of their own inside `<PhoenixHandlerBoundary name={handler}>`, because `phx.frontend.useFinalizeAction(handler)` runs the project's handler factory: it is called once per component, never in a loop, and the boundary remounts the buttons when the handler is edited. **Back to the configuration** lives in the same component, so it can be disabled while the request is pending.
662
+
663
+ ```tsx
664
+ import { Alert, Box, Button, CircularProgress, Typography } from '@mui/material'
665
+
666
+ import { phx } from '../../.phoenix/phx.js'
667
+
668
+ const BUTTON_SX = { minBlockSize: 48, borderRadius: 999, textTransform: 'none', fontWeight: 600 } as const
669
+
670
+ interface ReviewActionsProps {
671
+ readonly handler: string
672
+ readonly label: string
673
+ readonly onBack: () => void
674
+ }
675
+
676
+ // The review step's buttons: one finalize action and the way back. `useFinalizeAction` runs the action's handler,
677
+ // so this component calls it exactly once and is rendered inside a `PhoenixHandlerBoundary` for that handler.
678
+ // Back is disabled while the request is pending, so the customer cannot change the configuration while it is sent.
679
+ // The pending state belongs to this component: when the panel unmounts, as in fullscreen, the request goes on and
680
+ // the remounted buttons are enabled again.
681
+ export const ReviewActions = ({ handler, label, onBack }: ReviewActionsProps) => {
682
+ const { run, isPending, error, disabled, hints } = phx.frontend.useFinalizeAction(handler)
378
683
 
379
684
  return (
380
- <Button
381
- onClick={() => setFullscreen(!isFullscreen)}
382
- startIcon={
383
- <svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
384
- <path d={isFullscreen ? EXIT_ICON : ENTER_ICON} />
385
- </svg>
386
- }
685
+ <Box component="div" sx={{ display: 'grid', gap: 2 }}>
686
+ <Box component="div" sx={{ display: 'grid', gap: 1 }}>
687
+ <Button
688
+ size="large"
689
+ variant="contained"
690
+ disabled={disabled || isPending}
691
+ onClick={run}
692
+ startIcon={isPending ? <CircularProgress size={18} color="inherit" /> : undefined}
693
+ sx={BUTTON_SX}
694
+ >
695
+ {isPending ? 'Sending the request…' : label}
696
+ </Button>
697
+ {disabled && hints && (
698
+ <Typography variant="caption" color="text.secondary">
699
+ {hints}
700
+ </Typography>
701
+ )}
702
+ {error && <Alert severity="error">The request could not be sent: {error.message}</Alert>}
703
+ </Box>
704
+ <Button size="large" variant="outlined" disabled={isPending} onClick={onBack} sx={BUTTON_SX}>
705
+ Back to the configuration
706
+ </Button>
707
+ </Box>
708
+ )
709
+ }
710
+ ```
711
+
712
+ - `run()` records the finalize-button click for analytics and runs the handler; it does nothing while `disabled` or `isPending`.
713
+ - 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. `isPending` and `error` belong to the mounted component: entering fullscreen unmounts the panel, the request goes on, and the panel shown after fullscreen starts with both buttons enabled. A layout that must rule that out keeps the review step mounted, or hides its fullscreen button while a request runs.
714
+ - `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.
715
+ - `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.
716
+
717
+ 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 [Finalization](?path=/docs/phoenix-finalization--docs) what a real flow does on the server.
718
+
719
+ ## Serve read mode
720
+
721
+ 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 finalize action is not offered, since `useFinalizeAction` does not check read mode itself. 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).
722
+
723
+ ## Fit a phone
724
+
725
+ 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.
726
+
727
+ **The switch.** `useActiveViewports()` from `@salsita-ai/sdk/react` 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.
728
+
729
+ | Value | When it is active |
730
+ | ----------- | ---------------------------------------------------------------------------------- |
731
+ | `phone` | The container is narrower than 480 pixels. |
732
+ | `mobile` | The container is at most 768 pixels wide. |
733
+ | `tablet` | The container is wider than 480 and narrower than 1024 pixels. |
734
+ | `desktop` | The container is wider than 768 pixels; `mobile` and `desktop` exclude each other. |
735
+ | `landscape` | The browser window, not the container, is wider than it is high. |
736
+ | `portrait` | The browser window, not the container, is higher than it is wide. |
737
+
738
+ 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.
739
+
740
+ **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.
741
+
742
+ ```tsx
743
+ import { Box, ButtonBase } from '@mui/material'
744
+ import { type MouseEvent, type PointerEvent, type ReactNode, useRef } from 'react'
745
+
746
+ // A drag shorter than this is a tap, and the handle's click toggles the sheet
747
+ const TAP_DISTANCE = 6
748
+ // A drag that ends this far from where it started snaps to the other position
749
+ const SNAP_DISTANCE = 48
750
+ // A drag faster than this, in pixels per millisecond, snaps in its direction even when it is short
751
+ const SNAP_VELOCITY = 0.5
752
+ // How far the sheet follows the finger towards the position it is already in
753
+ const OVERDRAG = 0.25
754
+
755
+ interface BottomSheetProps {
756
+ readonly open: boolean
757
+ readonly onOpenChange: (open: boolean) => void
758
+ readonly label: string
759
+ readonly children: ReactNode
760
+ }
761
+
762
+ interface Drag {
763
+ readonly startY: number
764
+ readonly startTime: number
765
+ delta: number
766
+ }
767
+
768
+ // A persistent sheet at the bottom of the layout with two positions, open and collapsed. It is not a modal: the scene
769
+ // above it stays interactive. Its height comes from its content, so the layout's safe area ends where the sheet
770
+ // starts and changes only when the sheet snaps. While the handle is dragged the sheet follows the finger with a
771
+ // transform, which the safe area does not see, so the camera refits once per snap and not on every move.
772
+ export const BottomSheet = ({ open, onOpenChange, label, children }: BottomSheetProps) => {
773
+ const sheetRef = useRef<HTMLElement>(null)
774
+ const dragRef = useRef<Drag | null>(null)
775
+ const draggedRef = useRef(false)
776
+
777
+ const moveSheet = (offset: number, animate: boolean) => {
778
+ if (sheetRef.current) {
779
+ sheetRef.current.style.transition = animate ? 'transform 200ms ease-out' : 'none'
780
+ sheetRef.current.style.transform = offset === 0 ? '' : `translateY(${offset}px)`
781
+ }
782
+ }
783
+
784
+ const handlePointerDown = (event: PointerEvent<HTMLButtonElement>) => {
785
+ event.currentTarget.setPointerCapture(event.pointerId)
786
+ dragRef.current = { startY: event.clientY, startTime: event.timeStamp, delta: 0 }
787
+ draggedRef.current = false
788
+ }
789
+
790
+ const handlePointerMove = (event: PointerEvent<HTMLButtonElement>) => {
791
+ const drag = dragRef.current
792
+
793
+ if (drag === null) {
794
+ return
795
+ }
796
+
797
+ drag.delta = event.clientY - drag.startY
798
+ // Down closes an open sheet and up opens a collapsed one; the other direction only gives a little
799
+ const towardsOther = open ? drag.delta > 0 : drag.delta < 0
800
+ moveSheet(towardsOther ? drag.delta : drag.delta * OVERDRAG, false)
801
+ }
802
+
803
+ const handlePointerEnd = (event: PointerEvent<HTMLButtonElement>) => {
804
+ const drag = dragRef.current
805
+ dragRef.current = null
806
+
807
+ if (drag === null) {
808
+ return
809
+ }
810
+
811
+ moveSheet(0, true)
812
+
813
+ if (Math.abs(drag.delta) < TAP_DISTANCE) {
814
+ return
815
+ }
816
+
817
+ draggedRef.current = true
818
+ const velocity = drag.delta / Math.max(event.timeStamp - drag.startTime, 1)
819
+ const flicked = Math.abs(drag.delta) >= SNAP_DISTANCE || Math.abs(velocity) >= SNAP_VELOCITY
820
+
821
+ if (flicked && (open ? drag.delta > 0 : drag.delta < 0)) {
822
+ onOpenChange(!open)
823
+ }
824
+ }
825
+
826
+ // A tap, Enter or Space toggles the sheet. The pointer click that ends a drag has already been handled; a keyboard
827
+ // click (`detail` 0) always toggles, also after a touch drag that was followed by no click.
828
+ const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
829
+ const dragged = draggedRef.current
830
+ draggedRef.current = false
831
+
832
+ if (dragged && event.detail !== 0) {
833
+ return
834
+ }
835
+
836
+ onOpenChange(!open)
837
+ }
838
+
839
+ return (
840
+ <Box
841
+ component="section"
842
+ ref={sheetRef}
843
+ aria-label={label}
387
844
  sx={{
388
- minBlockSize: 44,
389
- paddingInline: 2.5,
390
- borderRadius: 999,
845
+ position: 'relative',
846
+ flexShrink: 0,
847
+ display: 'flex',
848
+ flexDirection: 'column',
849
+ maxBlockSize: '55%',
850
+ paddingBlockEnd: 'env(safe-area-inset-bottom)',
851
+ borderStartStartRadius: 20,
852
+ borderStartEndRadius: 20,
391
853
  bgcolor: 'background.paper',
392
- color: 'text.primary',
393
- 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)' },
854
+ boxShadow: '0 -8px 32px rgba(0, 0, 0, 0.12)',
855
+ // Fills the gap below the sheet while it is dragged up
856
+ '&::after': {
857
+ content: '""',
858
+ position: 'absolute',
859
+ insetInline: 0,
860
+ insetBlockStart: '100%',
861
+ blockSize: '100vh',
862
+ bgcolor: 'background.paper',
863
+ },
397
864
  }}
398
865
  >
399
- {isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
400
- </Button>
866
+ <ButtonBase
867
+ aria-expanded={open}
868
+ disableRipple
869
+ aria-label={open ? `Collapse ${label.toLowerCase()}` : `Expand ${label.toLowerCase()}`}
870
+ onClick={handleClick}
871
+ onPointerDown={handlePointerDown}
872
+ onPointerMove={handlePointerMove}
873
+ onPointerUp={handlePointerEnd}
874
+ onPointerCancel={handlePointerEnd}
875
+ sx={{
876
+ flexShrink: 0,
877
+ blockSize: 36,
878
+ touchAction: 'none',
879
+ '&.Mui-focusVisible': { outline: 2, outlineColor: 'primary.main', outlineOffset: -4, borderRadius: 3 },
880
+ }}
881
+ >
882
+ <Box component="span" sx={{ inlineSize: 40, blockSize: 4, borderRadius: 2, bgcolor: 'text.disabled' }} />
883
+ </ButtonBase>
884
+ {/* A column, so the panel's cards region shrinks first and the price and action stay in view; below the cards
885
+ region's minimum height the content scrolls as a whole, as the review step does on a short phone */}
886
+ <Box
887
+ component="div"
888
+ sx={{
889
+ display: 'flex',
890
+ flexDirection: 'column',
891
+ flex: '1 1 auto',
892
+ minBlockSize: 0,
893
+ overflowY: 'auto',
894
+ overscrollBehavior: 'contain',
895
+ }}
896
+ >
897
+ {children}
898
+ </Box>
899
+ </Box>
900
+ )
901
+ }
902
+ ```
903
+
904
+ - 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.
905
+ - 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.
906
+ - While the handle is dragged the sheet follows the finger with a `transform`. `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.
907
+ - 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.
908
+
909
+ **What else moves.** The toast sits at the top of the free area, 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, so the toast has room beside it. Fullscreen removes the sheet as it removes the panel on a desktop. The sheet keeps its position while fullscreen is on.
910
+
911
+ **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:
912
+
913
+ ```yaml
914
+ components:
915
+ Table:
916
+ fitRootPerspectiveCamera:
917
+ main:
918
+ priority: 2
919
+ fov: 35
920
+ near: 100
921
+ far: 10000
922
+ minDistance: 1000
923
+ maxDistance: 9000
924
+ distance: 5000
925
+ azimuthAngle: 1.5
926
+ polarAngle: 1.1
927
+ fitMeshChanges: true
928
+ $filter:
929
+ viewports: [desktop]
930
+ phone:
931
+ priority: 1
932
+ fov: 35
933
+ near: 100
934
+ far: 10000
935
+ minDistance: 1000
936
+ maxDistance: 12000
937
+ distance: 8000
938
+ azimuthAngle: 1.5
939
+ polarAngle: 1.1
940
+ fitMeshChanges: true
941
+ $filter:
942
+ viewports: [mobile]
943
+ ```
944
+
945
+ ## Put a control on the product
946
+
947
+ 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.
948
+
949
+ 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:
950
+
951
+ ```yaml
952
+ components:
953
+ Table:
954
+ htmlHotspot:
955
+ edgeFinish:
956
+ name: EdgeFinishHotspot
957
+ offsetX: 500
958
+ offsetY: 800
959
+ offsetZ: 0
960
+ $filter:
961
+ modes: [owner]
962
+ navigationStates: [configure]
963
+ ```
964
+
965
+ 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:
966
+
967
+ ```tsx
968
+ import { Box, ButtonBase, Popover, Tooltip } from '@mui/material'
969
+ import { useState } from 'react'
970
+
971
+ import { phx } from '../../.phoenix/phx.js'
972
+ import { EdgeFinishCards } from '../_shared/EdgeFinishCards.js'
973
+
974
+ // The edge profile the button shows, the square edge of the cards' drawings
975
+ const EDGE_ICON = 'M3 7H17L21 11V13L17 17H3Z'
976
+
977
+ // Rendered by the spec's `htmlHotspot` on the table's edge: a button on the product that opens the edge finishes
978
+ // next to the edge, with the same cards as the panel. The spec's `$filter` decides when it is shown.
979
+ export default phx.frontend.createCustomPhoenixEntityElement('EdgeFinishHotspot', ({ entityId }) => {
980
+ const edgeFinish = phx.frontend.useAttributeSelection(entityId, 'edgeFinish')
981
+ const [anchor, setAnchor] = useState<HTMLElement | null>(null)
982
+
983
+ return (
984
+ <>
985
+ <Tooltip title="Edge finish">
986
+ <ButtonBase
987
+ aria-label="Edge finish"
988
+ aria-haspopup="dialog"
989
+ aria-expanded={anchor !== null}
990
+ onClick={event => setAnchor(event.currentTarget)}
991
+ sx={{
992
+ display: 'grid',
993
+ placeItems: 'center',
994
+ inlineSize: 44,
995
+ blockSize: 44,
996
+ borderRadius: '50%',
997
+ bgcolor: 'background.paper',
998
+ color: 'primary.main',
999
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.2)',
1000
+ '&.Mui-focusVisible': { outline: 2, outlineColor: 'primary.main', outlineOffset: 2 },
1001
+ }}
1002
+ >
1003
+ <svg viewBox="0 0 24 24" width={22} height={22} aria-hidden>
1004
+ <path d={EDGE_ICON} fill="currentColor" />
1005
+ </svg>
1006
+ </ButtonBase>
1007
+ </Tooltip>
1008
+ <Popover
1009
+ open={anchor !== null}
1010
+ anchorEl={anchor}
1011
+ onClose={() => setAnchor(null)}
1012
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
1013
+ transformOrigin={{ vertical: 'bottom', horizontal: 'center' }}
1014
+ slotProps={{
1015
+ paper: { role: 'dialog', 'aria-label': 'Edge finish', sx: { borderRadius: 4, marginBlockStart: -1 } },
1016
+ }}
1017
+ >
1018
+ <Box component="div" sx={{ inlineSize: 340, maxInlineSize: 'calc(100vw - 32px)', padding: 2 }}>
1019
+ <EdgeFinishCards selection={edgeFinish} />
1020
+ </Box>
1021
+ </Popover>
1022
+ </>
1023
+ )
1024
+ })
1025
+ ```
1026
+
1027
+ - 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.
1028
+ - 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.
1029
+ - 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.
1030
+ - `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.
1031
+ - 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.
1032
+
1033
+ [Html Hotspots](?path=/docs/controls-scene-html-hotspots--docs) describes the component and its other properties.
1034
+
1035
+ 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.
1036
+
1037
+ ## Control the scene
1038
+
1039
+ 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.
1040
+
1041
+ **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:
1042
+
1043
+ ```yaml
1044
+ viewStates:
1045
+ - overview
1046
+ - edge
1047
+
1048
+ components:
1049
+ Table:
1050
+ fitRootPerspectiveCamera:
1051
+ edge:
1052
+ priority: 4
1053
+ fov: 35
1054
+ near: 100
1055
+ far: 10000
1056
+ minDistance: 800
1057
+ maxDistance: 9000
1058
+ distance: 2000
1059
+ azimuthAngle: 0.5
1060
+ polarAngle: 1.15
1061
+ fitMeshChanges: true
1062
+ $filter:
1063
+ viewports: [desktop]
1064
+ viewStates: [edge]
1065
+ edgePhone:
1066
+ priority: 3
1067
+ fov: 35
1068
+ near: 100
1069
+ far: 10000
1070
+ minDistance: 800
1071
+ maxDistance: 12000
1072
+ distance: 3000
1073
+ azimuthAngle: 0.5
1074
+ polarAngle: 1.15
1075
+ fitMeshChanges: true
1076
+ $filter:
1077
+ viewports: [mobile]
1078
+ viewStates: [edge]
1079
+ ```
1080
+
1081
+ **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:
1082
+
1083
+ ```tsx
1084
+ import { Box, Button, Tooltip } from '@mui/material'
1085
+
1086
+ import { phx } from '../../.phoenix/phx.js'
1087
+
1088
+ // The spec's view states, each with the label of the button that shows it
1089
+ const VIEWS = [
1090
+ { state: 'overview', label: 'Whole table' },
1091
+ { state: 'edge', label: 'Edge close-up' },
1092
+ ] as const
1093
+
1094
+ const RESET_ICON = 'M4 12a8 8 0 1 0 2.4-5.7M4 4v4h4'
1095
+
1096
+ const SURFACE_SX = {
1097
+ borderRadius: 999,
1098
+ bgcolor: 'background.paper',
1099
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
1100
+ } as const
1101
+
1102
+ const BUTTON_SX = {
1103
+ minBlockSize: 44,
1104
+ minInlineSize: 44,
1105
+ borderRadius: 999,
1106
+ textTransform: 'none',
1107
+ fontWeight: 600,
1108
+ whiteSpace: 'nowrap',
1109
+ } as const
1110
+
1111
+ interface SceneControlsProps {
1112
+ // The reset button shows only its icon, with its label as tooltip and accessible name
1113
+ readonly compact?: boolean
1114
+ // Only the reset button, without the view buttons
1115
+ readonly resetOnly?: boolean
1116
+ }
1117
+
1118
+ // The layout's own camera controls: one button per view state of the spec, and a reset that undoes an orbit
1119
+ export const SceneControls = ({ compact = false, resetOnly = false }: SceneControlsProps) => {
1120
+ const activeView = phx.frontend.useActiveViewState()
1121
+ const setViewState = phx.frontend.useSetViewState()
1122
+ const resetCamera = phx.frontend.useResetCamera()
1123
+ const resetIcon = (
1124
+ <svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
1125
+ <path d={RESET_ICON} />
1126
+ </svg>
1127
+ )
1128
+
1129
+ return (
1130
+ <Box component="div" sx={{ display: 'flex', gap: 1 }}>
1131
+ {!resetOnly && (
1132
+ <Box component="div" role="group" aria-label="View" sx={{ ...SURFACE_SX, display: 'flex' }}>
1133
+ {VIEWS.map(view => {
1134
+ const isActive = activeView === view.state
1135
+
1136
+ return (
1137
+ <Button
1138
+ key={view.state}
1139
+ aria-pressed={isActive}
1140
+ onClick={() => setViewState(view.state)}
1141
+ sx={{
1142
+ ...BUTTON_SX,
1143
+ paddingInline: 2,
1144
+ bgcolor: isActive ? 'text.primary' : 'transparent',
1145
+ color: isActive ? 'background.paper' : 'text.primary',
1146
+ '&:hover': { bgcolor: isActive ? 'text.primary' : 'action.hover' },
1147
+ }}
1148
+ >
1149
+ {view.label}
1150
+ </Button>
1151
+ )
1152
+ })}
1153
+ </Box>
1154
+ )}
1155
+ <Tooltip title={compact ? 'Reset view' : ''}>
1156
+ <Button
1157
+ onClick={resetCamera}
1158
+ aria-label={compact ? 'Reset view' : undefined}
1159
+ startIcon={compact ? undefined : resetIcon}
1160
+ sx={{
1161
+ ...SURFACE_SX,
1162
+ ...BUTTON_SX,
1163
+ paddingInline: compact ? 0 : 2,
1164
+ color: 'text.primary',
1165
+ '&:hover': { bgcolor: 'background.paper', boxShadow: '0 6px 20px rgba(0, 0, 0, 0.18)' },
1166
+ }}
1167
+ >
1168
+ {compact ? resetIcon : 'Reset view'}
1169
+ </Button>
1170
+ </Tooltip>
1171
+ </Box>
401
1172
  )
402
1173
  }
403
1174
  ```
404
1175
 
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.
406
-
407
- ## Serve read mode
408
-
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.
1176
+ - 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.
1177
+ - 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.
1178
+ - 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.
1179
+ - 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.
1180
+ - The hooks need no `viewControls` component. That component configures the built-in layout's view controls and has no effect here.
410
1181
 
411
1182
  ## The spec the layout reads
412
1183
 
413
- The layout reads three components of the root entity and one navigation state; the scene components are in the complete implementation below.
1184
+ 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`.
414
1185
 
415
1186
  ```yaml
416
1187
  navigationStates:
417
1188
  - configure
418
1189
  - checkout
1190
+ - requested
1191
+
1192
+ viewStates:
1193
+ - overview
1194
+ - edge
419
1195
 
420
1196
  components:
421
1197
  Table:
@@ -445,45 +1221,69 @@ components:
445
1221
  - label: Tree trunk
446
1222
  description: The natural edge
447
1223
  value: TreeTrunk
1224
+ finalizeAction:
1225
+ quote:
1226
+ variant: primary
1227
+ label: Request a quote
1228
+ title: Send the configuration to the shop for a quote
1229
+ icon: PaperPlaneTilt
1230
+ handler: requestQuote
1231
+
1232
+ transformers:
1233
+ - name: tree-trunk-delivery
448
1234
  ```
449
1235
 
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.
1236
+ `$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.
451
1237
 
452
1238
  ## Try the example
453
1239
 
454
1240
  To see read mode, open **Mocks** in the editor rail on the left and apply the **Read User** template.
455
1241
 
1242
+ 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.
1243
+
456
1244
  ## Test cases
457
1245
 
458
1246
  - 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
1247
  - 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.
1248
+ - 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.
460
1249
  - Tab reaches the selected card, and the arrow keys move the selection to the next or previous card.
461
1250
  - 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.
1251
+ - **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.
1252
+ - **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.
463
1253
  - **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.
1254
+ - With the **Read User** mock the panel says **View only**, shows the chosen finish and the price, and has no button, so no quote can be requested.
1255
+ - 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.
1256
+ - Tab reaches the button on the edge, and Enter or Space opens the popover.
1257
+ - The button on the edge is gone in the review step, in the confirmation and with the **Read User** mock, and stays in fullscreen.
1258
+ - 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.
1259
+ - 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.
1260
+ - On the phone, the review step, **Request a quote** and the thank-you message work with the sheet collapsed and open.
1261
+ - On the phone, choosing **Tree trunk** shows the hint at the top of the free area, beside the fullscreen button.
1262
+ - 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.
1263
+ - **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.
1264
+ - 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.
1265
+ - 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.
1266
+ - 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.
465
1267
 
466
1268
  ## Known gaps
467
1269
 
468
- A project layout has only what it renders. This one leaves out the following, each of which a product may need.
1270
+ 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.
469
1271
 
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.
1272
+ - **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).
1273
+ - **Toolbar actions.** Undo and redo, 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: `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).
1274
+ - **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.
475
1275
  - **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
1276
  - **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
1277
  - **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
1278
  - **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(...)`.
1279
+ - **Analytics.** The finalize button records `FinalizeButtonClicked` through `useFinalizeAction`. The built-in fullscreen button records `FullScreenModeToggled`; this layout's fullscreen and other 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
1280
  - **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.** It is not exported, so a project layout cannot show it.
1281
+ - **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
1282
  - **"Powered by Salsita" mark.** The built-in layout shows it; a project layout decides whether to mount `LayoutSlots.DesktopPoweredBy`, and this one does not.
483
1283
 
484
1284
  ## Related pages
485
1285
 
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.
1286
+ - [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.
487
1287
  - [Universal](?path=/docs/layouts-universal--docs) is the built-in layout this one replaces.
488
1288
  - [Header / Custom](?path=/docs/layouts-header-custom--docs) replaces only the header, with the price from `useRootEntityPrice()`.
489
1289
  - [Canvas Padding](?path=/docs/layouts-canvas-padding--docs) scales the gaps `useSceneSafeArea()` measures.
@@ -528,6 +1328,11 @@ presets:
528
1328
  navigationStates:
529
1329
  - configure
530
1330
  - checkout
1331
+ - requested
1332
+
1333
+ viewStates:
1334
+ - overview
1335
+ - edge
531
1336
 
532
1337
  components:
533
1338
  Table:
@@ -557,9 +1362,25 @@ components:
557
1362
  - label: Tree trunk
558
1363
  description: The natural edge
559
1364
  value: TreeTrunk
1365
+ finalizeAction:
1366
+ quote:
1367
+ variant: primary
1368
+ label: Request a quote
1369
+ title: Send the configuration to the shop for a quote
1370
+ icon: PaperPlaneTilt
1371
+ handler: requestQuote
1372
+ htmlHotspot:
1373
+ edgeFinish:
1374
+ name: EdgeFinishHotspot
1375
+ offsetX: 500
1376
+ offsetY: 800
1377
+ offsetZ: 0
1378
+ $filter:
1379
+ modes: [owner]
1380
+ navigationStates: [configure]
560
1381
  fitRootPerspectiveCamera:
561
1382
  main:
562
- priority: 1
1383
+ priority: 2
563
1384
  fov: 35
564
1385
  near: 100
565
1386
  far: 10000
@@ -569,6 +1390,49 @@ components:
569
1390
  azimuthAngle: 1.5
570
1391
  polarAngle: 1.1
571
1392
  fitMeshChanges: true
1393
+ $filter:
1394
+ viewports: [desktop]
1395
+ phone:
1396
+ priority: 1
1397
+ fov: 35
1398
+ near: 100
1399
+ far: 10000
1400
+ minDistance: 1000
1401
+ maxDistance: 12000
1402
+ distance: 8000
1403
+ azimuthAngle: 1.5
1404
+ polarAngle: 1.1
1405
+ fitMeshChanges: true
1406
+ $filter:
1407
+ viewports: [mobile]
1408
+ edge:
1409
+ priority: 4
1410
+ fov: 35
1411
+ near: 100
1412
+ far: 10000
1413
+ minDistance: 800
1414
+ maxDistance: 9000
1415
+ distance: 2000
1416
+ azimuthAngle: 0.5
1417
+ polarAngle: 1.15
1418
+ fitMeshChanges: true
1419
+ $filter:
1420
+ viewports: [desktop]
1421
+ viewStates: [edge]
1422
+ edgePhone:
1423
+ priority: 3
1424
+ fov: 35
1425
+ near: 100
1426
+ far: 10000
1427
+ minDistance: 800
1428
+ maxDistance: 12000
1429
+ distance: 3000
1430
+ azimuthAngle: 0.5
1431
+ polarAngle: 1.15
1432
+ fitMeshChanges: true
1433
+ $filter:
1434
+ viewports: [mobile]
1435
+ viewStates: [edge]
572
1436
  sceneEntityRemoteObject:
573
1437
  top:
574
1438
  fileUrl: /models/tables.gltf
@@ -611,6 +1475,173 @@ components:
611
1475
  value: 780
612
1476
  scaleOne: 780
613
1477
  scaleZero: 740
1478
+
1479
+ transformers:
1480
+ - name: tree-trunk-delivery
1481
+ ```
1482
+
1483
+ ### ./src/\_shared/BottomSheet.tsx
1484
+
1485
+ ```tsx
1486
+ import { Box, ButtonBase } from '@mui/material'
1487
+ import { type MouseEvent, type PointerEvent, type ReactNode, useRef } from 'react'
1488
+
1489
+ // A drag shorter than this is a tap, and the handle's click toggles the sheet
1490
+ const TAP_DISTANCE = 6
1491
+ // A drag that ends this far from where it started snaps to the other position
1492
+ const SNAP_DISTANCE = 48
1493
+ // A drag faster than this, in pixels per millisecond, snaps in its direction even when it is short
1494
+ const SNAP_VELOCITY = 0.5
1495
+ // How far the sheet follows the finger towards the position it is already in
1496
+ const OVERDRAG = 0.25
1497
+
1498
+ interface BottomSheetProps {
1499
+ readonly open: boolean
1500
+ readonly onOpenChange: (open: boolean) => void
1501
+ readonly label: string
1502
+ readonly children: ReactNode
1503
+ }
1504
+
1505
+ interface Drag {
1506
+ readonly startY: number
1507
+ readonly startTime: number
1508
+ delta: number
1509
+ }
1510
+
1511
+ // A persistent sheet at the bottom of the layout with two positions, open and collapsed. It is not a modal: the scene
1512
+ // above it stays interactive. Its height comes from its content, so the layout's safe area ends where the sheet
1513
+ // starts and changes only when the sheet snaps. While the handle is dragged the sheet follows the finger with a
1514
+ // transform, which the safe area does not see, so the camera refits once per snap and not on every move.
1515
+ export const BottomSheet = ({ open, onOpenChange, label, children }: BottomSheetProps) => {
1516
+ const sheetRef = useRef<HTMLElement>(null)
1517
+ const dragRef = useRef<Drag | null>(null)
1518
+ const draggedRef = useRef(false)
1519
+
1520
+ const moveSheet = (offset: number, animate: boolean) => {
1521
+ if (sheetRef.current) {
1522
+ sheetRef.current.style.transition = animate ? 'transform 200ms ease-out' : 'none'
1523
+ sheetRef.current.style.transform = offset === 0 ? '' : `translateY(${offset}px)`
1524
+ }
1525
+ }
1526
+
1527
+ const handlePointerDown = (event: PointerEvent<HTMLButtonElement>) => {
1528
+ event.currentTarget.setPointerCapture(event.pointerId)
1529
+ dragRef.current = { startY: event.clientY, startTime: event.timeStamp, delta: 0 }
1530
+ draggedRef.current = false
1531
+ }
1532
+
1533
+ const handlePointerMove = (event: PointerEvent<HTMLButtonElement>) => {
1534
+ const drag = dragRef.current
1535
+
1536
+ if (drag === null) {
1537
+ return
1538
+ }
1539
+
1540
+ drag.delta = event.clientY - drag.startY
1541
+ // Down closes an open sheet and up opens a collapsed one; the other direction only gives a little
1542
+ const towardsOther = open ? drag.delta > 0 : drag.delta < 0
1543
+ moveSheet(towardsOther ? drag.delta : drag.delta * OVERDRAG, false)
1544
+ }
1545
+
1546
+ const handlePointerEnd = (event: PointerEvent<HTMLButtonElement>) => {
1547
+ const drag = dragRef.current
1548
+ dragRef.current = null
1549
+
1550
+ if (drag === null) {
1551
+ return
1552
+ }
1553
+
1554
+ moveSheet(0, true)
1555
+
1556
+ if (Math.abs(drag.delta) < TAP_DISTANCE) {
1557
+ return
1558
+ }
1559
+
1560
+ draggedRef.current = true
1561
+ const velocity = drag.delta / Math.max(event.timeStamp - drag.startTime, 1)
1562
+ const flicked = Math.abs(drag.delta) >= SNAP_DISTANCE || Math.abs(velocity) >= SNAP_VELOCITY
1563
+
1564
+ if (flicked && (open ? drag.delta > 0 : drag.delta < 0)) {
1565
+ onOpenChange(!open)
1566
+ }
1567
+ }
1568
+
1569
+ // A tap, Enter or Space toggles the sheet. The pointer click that ends a drag has already been handled; a keyboard
1570
+ // click (`detail` 0) always toggles, also after a touch drag that was followed by no click.
1571
+ const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
1572
+ const dragged = draggedRef.current
1573
+ draggedRef.current = false
1574
+
1575
+ if (dragged && event.detail !== 0) {
1576
+ return
1577
+ }
1578
+
1579
+ onOpenChange(!open)
1580
+ }
1581
+
1582
+ return (
1583
+ <Box
1584
+ component="section"
1585
+ ref={sheetRef}
1586
+ aria-label={label}
1587
+ sx={{
1588
+ position: 'relative',
1589
+ flexShrink: 0,
1590
+ display: 'flex',
1591
+ flexDirection: 'column',
1592
+ maxBlockSize: '55%',
1593
+ paddingBlockEnd: 'env(safe-area-inset-bottom)',
1594
+ borderStartStartRadius: 20,
1595
+ borderStartEndRadius: 20,
1596
+ bgcolor: 'background.paper',
1597
+ boxShadow: '0 -8px 32px rgba(0, 0, 0, 0.12)',
1598
+ // Fills the gap below the sheet while it is dragged up
1599
+ '&::after': {
1600
+ content: '""',
1601
+ position: 'absolute',
1602
+ insetInline: 0,
1603
+ insetBlockStart: '100%',
1604
+ blockSize: '100vh',
1605
+ bgcolor: 'background.paper',
1606
+ },
1607
+ }}
1608
+ >
1609
+ <ButtonBase
1610
+ aria-expanded={open}
1611
+ disableRipple
1612
+ aria-label={open ? `Collapse ${label.toLowerCase()}` : `Expand ${label.toLowerCase()}`}
1613
+ onClick={handleClick}
1614
+ onPointerDown={handlePointerDown}
1615
+ onPointerMove={handlePointerMove}
1616
+ onPointerUp={handlePointerEnd}
1617
+ onPointerCancel={handlePointerEnd}
1618
+ sx={{
1619
+ flexShrink: 0,
1620
+ blockSize: 36,
1621
+ touchAction: 'none',
1622
+ '&.Mui-focusVisible': { outline: 2, outlineColor: 'primary.main', outlineOffset: -4, borderRadius: 3 },
1623
+ }}
1624
+ >
1625
+ <Box component="span" sx={{ inlineSize: 40, blockSize: 4, borderRadius: 2, bgcolor: 'text.disabled' }} />
1626
+ </ButtonBase>
1627
+ {/* A column, so the panel's cards region shrinks first and the price and action stay in view; below the cards
1628
+ region's minimum height the content scrolls as a whole, as the review step does on a short phone */}
1629
+ <Box
1630
+ component="div"
1631
+ sx={{
1632
+ display: 'flex',
1633
+ flexDirection: 'column',
1634
+ flex: '1 1 auto',
1635
+ minBlockSize: 0,
1636
+ overflowY: 'auto',
1637
+ overscrollBehavior: 'contain',
1638
+ }}
1639
+ >
1640
+ {children}
1641
+ </Box>
1642
+ </Box>
1643
+ )
1644
+ }
614
1645
  ```
615
1646
 
616
1647
  ### ./src/\_shared/EdgeFinishCards.tsx
@@ -751,39 +1782,106 @@ export const EdgeFinishCards = ({ selection }: EdgeFinishCardsProps) => {
751
1782
  ### ./src/\_shared/FullscreenToggle.tsx
752
1783
 
753
1784
  ```tsx
754
- import { Button } from '@mui/material'
1785
+ import { Button, Tooltip } from '@mui/material'
755
1786
 
756
1787
  import { phx } from '../../.phoenix/phx.js'
757
1788
 
758
1789
  const ENTER_ICON = 'M4 9V4h5M15 4h5v5M20 15v5h-5M9 20H4v-5'
759
1790
  const EXIT_ICON = 'M9 4v5H4M15 4v5h5M15 20v-5h5M9 20v-5H4'
760
1791
 
761
- export const FullscreenToggle = () => {
1792
+ interface FullscreenToggleProps {
1793
+ // Only the icon, with the label as its accessible name and tooltip, where a label would cover the product
1794
+ readonly compact?: boolean
1795
+ }
1796
+
1797
+ export const FullscreenToggle = ({ compact = false }: FullscreenToggleProps) => {
762
1798
  const isFullscreen = phx.frontend.useIsFullscreenActive()
763
1799
  const setFullscreen = phx.frontend.useSetFullscreen()
1800
+ const label = isFullscreen ? 'Exit fullscreen' : 'Fullscreen'
1801
+ const icon = (
1802
+ <svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
1803
+ <path d={isFullscreen ? EXIT_ICON : ENTER_ICON} />
1804
+ </svg>
1805
+ )
764
1806
 
765
1807
  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
- }}
1808
+ <Tooltip title={compact ? label : ''}>
1809
+ <Button
1810
+ onClick={() => setFullscreen(!isFullscreen)}
1811
+ aria-label={compact ? label : undefined}
1812
+ startIcon={compact ? undefined : icon}
1813
+ sx={{
1814
+ minBlockSize: 44,
1815
+ minInlineSize: 44,
1816
+ paddingInline: compact ? 0 : 2.5,
1817
+ borderRadius: 999,
1818
+ bgcolor: 'background.paper',
1819
+ color: 'text.primary',
1820
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
1821
+ textTransform: 'none',
1822
+ fontWeight: 600,
1823
+ '&:hover': { bgcolor: 'background.paper', boxShadow: '0 6px 20px rgba(0, 0, 0, 0.18)' },
1824
+ }}
1825
+ >
1826
+ {compact ? icon : label}
1827
+ </Button>
1828
+ </Tooltip>
1829
+ )
1830
+ }
1831
+ ```
1832
+
1833
+ ### ./src/\_shared/MessageToast.tsx
1834
+
1835
+ ```tsx
1836
+ import { Alert } from '@mui/material'
1837
+ import { useEffect, useState } from 'react'
1838
+
1839
+ import type { ConfiguratorMessage } from '@salsita-ai/sdk/react'
1840
+
1841
+ import { phx } from '../../.phoenix/phx.js'
1842
+
1843
+ const VIOLATION_SEVERITY = { fatal: 'error', fixable: 'error', warning: 'warning', notice: 'info' } as const
1844
+ const RANK = { info: 0, warning: 1, error: 2 } as const
1845
+ const VISIBLE_MS = 6000
1846
+
1847
+ const severityOf = (message: ConfiguratorMessage) => {
1848
+ if (message.kind === 'violation') {
1849
+ return VIOLATION_SEVERITY[message.violation.severity]
1850
+ }
1851
+
1852
+ return message.kind === 'error' || message.kind === 'saveError' ? 'error' : 'info'
1853
+ }
1854
+
1855
+ export const MessageToast = () => {
1856
+ const [message, setMessage] = useState<ConfiguratorMessage | null>(null)
1857
+
1858
+ // A rejected change also reports the notices already present; keep the worst message while it is shown
1859
+ phx.frontend.useConfiguratorMessages(next => {
1860
+ setMessage(shown => (shown !== null && RANK[severityOf(next)] < RANK[severityOf(shown)] ? shown : next))
1861
+ })
1862
+
1863
+ useEffect(() => {
1864
+ if (message === null) {
1865
+ return
1866
+ }
1867
+
1868
+ const timeout = setTimeout(() => setMessage(null), VISIBLE_MS)
1869
+
1870
+ return () => clearTimeout(timeout)
1871
+ }, [message])
1872
+
1873
+ if (message === null) {
1874
+ return null
1875
+ }
1876
+
1877
+ return (
1878
+ <Alert
1879
+ severity={severityOf(message)}
1880
+ onClose={() => setMessage(null)}
1881
+ sx={{ maxInlineSize: 420, borderRadius: 3, boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)' }}
784
1882
  >
785
- {isFullscreen ? 'Exit fullscreen' : 'Fullscreen'}
786
- </Button>
1883
+ {message.text}
1884
+ </Alert>
787
1885
  )
788
1886
  }
789
1887
  ```
@@ -793,13 +1891,23 @@ export const FullscreenToggle = () => {
793
1891
  ```tsx
794
1892
  import { Box, Button, Typography } from '@mui/material'
795
1893
 
796
- import { detailComponent } from '@salsita-ai/sdk/domain'
797
- import { useResolveLocalizable } from '@salsita-ai/sdk/react'
1894
+ import { detailComponent, finalizeActionComponent } from '@salsita-ai/sdk/domain'
1895
+ import { PhoenixHandlerBoundary, useResolveLocalizable } from '@salsita-ai/sdk/react'
798
1896
 
799
1897
  import { phx } from '../../.phoenix/phx.js'
800
1898
  import { EdgeFinishCards } from './EdgeFinishCards.js'
1899
+ import { ReviewActions } from './ReviewActions.js'
1900
+
1901
+ const BUTTON_SX = { minBlockSize: 48, borderRadius: 999, textTransform: 'none', fontWeight: 600 } as const
1902
+
1903
+ interface ProductPanelProps {
1904
+ // Only the name, the price and the action, for a collapsed bottom sheet
1905
+ readonly collapsed?: boolean
1906
+ // In a bottom sheet the cards region keeps the top of the cards in view, and the sheet scrolls when that does not fit
1907
+ readonly inSheet?: boolean
1908
+ }
801
1909
 
802
- export const ProductPanel = () => {
1910
+ export const ProductPanel = ({ collapsed = false, inSheet = false }: ProductPanelProps) => {
803
1911
  const resolve = useResolveLocalizable()
804
1912
  const detail = phx.frontend.useFirstRootComponent(detailComponent)
805
1913
  const price = phx.frontend.useRootEntityPrice()
@@ -809,9 +1917,13 @@ export const ProductPanel = () => {
809
1917
  const navigationState = phx.frontend.useActiveNavigationState()
810
1918
  const pushNavigate = phx.frontend.usePushNavigate()
811
1919
  const popNavigate = phx.frontend.usePopNavigate()
1920
+ const navigate = phx.frontend.useNavigate()
1921
+ const finalizeActions = phx.frontend.useComponents({ componentTypes: [finalizeActionComponent] }) ?? []
812
1922
 
813
1923
  const isReadMode = accessInfo.viewMode
814
- const isReview = isReadMode || navigationState === 'checkout'
1924
+ const isReview = isReadMode || navigationState !== 'configure'
1925
+ // The spec's primary finalize action, or its first one
1926
+ const quoteAction = finalizeActions.find(action => action.state.variant === 'primary') ?? finalizeActions[0]
815
1927
  const chosenEdge = edgeFinish.options.find(option => option.value === edgeFinish.value)
816
1928
 
817
1929
  return (
@@ -832,29 +1944,44 @@ export const ProductPanel = () => {
832
1944
  <Typography variant="h5" component="h2" fontWeight={700}>
833
1945
  {resolve(detail?.state.title)}
834
1946
  </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} />
1947
+ {!collapsed && (
1948
+ <Typography variant="body2" color="text.secondary">
1949
+ {resolve(detail?.state.description)}
1950
+ </Typography>
855
1951
  )}
856
1952
  </Box>
857
1953
 
1954
+ {!collapsed && (
1955
+ <Box
1956
+ component="div"
1957
+ sx={{
1958
+ display: 'grid',
1959
+ gap: 1.5,
1960
+ paddingInline: 3,
1961
+ paddingBlockEnd: 3,
1962
+ // In a short container the region shrinks and scrolls; a scroll past its end moves its container on
1963
+ minBlockSize: inSheet ? 120 : 0,
1964
+ overflowY: 'auto',
1965
+ }}
1966
+ >
1967
+ <Typography variant="subtitle2" component="h3">
1968
+ Edge finish
1969
+ </Typography>
1970
+ {isReview ? (
1971
+ <Box component="div" sx={{ padding: 2, borderRadius: 3, bgcolor: 'action.hover' }}>
1972
+ <Typography variant="body2" fontWeight={600}>
1973
+ {resolve(chosenEdge?.title)}
1974
+ </Typography>
1975
+ <Typography variant="caption" color="text.secondary">
1976
+ {resolve(chosenEdge?.description)}
1977
+ </Typography>
1978
+ </Box>
1979
+ ) : (
1980
+ <EdgeFinishCards selection={edgeFinish} />
1981
+ )}
1982
+ </Box>
1983
+ )}
1984
+
858
1985
  <Box component="footer" sx={{ display: 'grid', gap: 2, padding: 3, borderTop: 1, borderColor: 'divider' }}>
859
1986
  <Box component="div" sx={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
860
1987
  <Typography variant="body2" color="text.secondary">
@@ -864,50 +1991,235 @@ export const ProductPanel = () => {
864
1991
  {price}
865
1992
  </Typography>
866
1993
  </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>
871
- )}
872
- {!isReadMode && (
873
- <Button
874
- size="large"
875
- variant={isReview ? 'outlined' : 'contained'}
876
- onClick={isReview ? popNavigate : () => pushNavigate('checkout')}
877
- sx={{ minBlockSize: 48, borderRadius: 999, textTransform: 'none', fontWeight: 600 }}
878
- >
879
- {isReview ? 'Back to the configuration' : 'Review and request a quote'}
1994
+ {!isReadMode && navigationState === 'configure' && (
1995
+ <Button size="large" variant="contained" onClick={() => pushNavigate('checkout')} sx={BUTTON_SX}>
1996
+ Review and request a quote
880
1997
  </Button>
881
1998
  )}
1999
+ {!isReadMode && navigationState === 'checkout' && (
2000
+ <>
2001
+ <Typography variant="body2" color="text.secondary">
2002
+ Your table is ready for a quote. A real shop collects the contact details here.
2003
+ </Typography>
2004
+ {quoteAction ? (
2005
+ <PhoenixHandlerBoundary name={quoteAction.state.handler}>
2006
+ <ReviewActions
2007
+ handler={quoteAction.state.handler}
2008
+ label={resolve(quoteAction.state.label)}
2009
+ onBack={popNavigate}
2010
+ />
2011
+ </PhoenixHandlerBoundary>
2012
+ ) : (
2013
+ <Button size="large" variant="outlined" onClick={popNavigate} sx={BUTTON_SX}>
2014
+ Back to the configuration
2015
+ </Button>
2016
+ )}
2017
+ </>
2018
+ )}
2019
+ {!isReadMode && navigationState === 'requested' && (
2020
+ <>
2021
+ <Typography variant="body2" role="status">
2022
+ Thank you. Your quote request is on its way, and the shop replies within two working days.
2023
+ </Typography>
2024
+ <Button size="large" variant="outlined" onClick={() => navigate('configure')} sx={BUTTON_SX}>
2025
+ Back to the configuration
2026
+ </Button>
2027
+ </>
2028
+ )}
882
2029
  </Box>
883
2030
  </>
884
2031
  )
885
2032
  }
886
2033
  ```
887
2034
 
2035
+ ### ./src/\_shared/ReviewActions.tsx
2036
+
2037
+ ```tsx
2038
+ import { Alert, Box, Button, CircularProgress, Typography } from '@mui/material'
2039
+
2040
+ import { phx } from '../../.phoenix/phx.js'
2041
+
2042
+ const BUTTON_SX = { minBlockSize: 48, borderRadius: 999, textTransform: 'none', fontWeight: 600 } as const
2043
+
2044
+ interface ReviewActionsProps {
2045
+ readonly handler: string
2046
+ readonly label: string
2047
+ readonly onBack: () => void
2048
+ }
2049
+
2050
+ // The review step's buttons: one finalize action and the way back. `useFinalizeAction` runs the action's handler,
2051
+ // so this component calls it exactly once and is rendered inside a `PhoenixHandlerBoundary` for that handler.
2052
+ // Back is disabled while the request is pending, so the customer cannot change the configuration while it is sent.
2053
+ // The pending state belongs to this component: when the panel unmounts, as in fullscreen, the request goes on and
2054
+ // the remounted buttons are enabled again.
2055
+ export const ReviewActions = ({ handler, label, onBack }: ReviewActionsProps) => {
2056
+ const { run, isPending, error, disabled, hints } = phx.frontend.useFinalizeAction(handler)
2057
+
2058
+ return (
2059
+ <Box component="div" sx={{ display: 'grid', gap: 2 }}>
2060
+ <Box component="div" sx={{ display: 'grid', gap: 1 }}>
2061
+ <Button
2062
+ size="large"
2063
+ variant="contained"
2064
+ disabled={disabled || isPending}
2065
+ onClick={run}
2066
+ startIcon={isPending ? <CircularProgress size={18} color="inherit" /> : undefined}
2067
+ sx={BUTTON_SX}
2068
+ >
2069
+ {isPending ? 'Sending the request…' : label}
2070
+ </Button>
2071
+ {disabled && hints && (
2072
+ <Typography variant="caption" color="text.secondary">
2073
+ {hints}
2074
+ </Typography>
2075
+ )}
2076
+ {error && <Alert severity="error">The request could not be sent: {error.message}</Alert>}
2077
+ </Box>
2078
+ <Button size="large" variant="outlined" disabled={isPending} onClick={onBack} sx={BUTTON_SX}>
2079
+ Back to the configuration
2080
+ </Button>
2081
+ </Box>
2082
+ )
2083
+ }
2084
+ ```
2085
+
2086
+ ### ./src/\_shared/SceneControls.tsx
2087
+
2088
+ ```tsx
2089
+ import { Box, Button, Tooltip } from '@mui/material'
2090
+
2091
+ import { phx } from '../../.phoenix/phx.js'
2092
+
2093
+ // The spec's view states, each with the label of the button that shows it
2094
+ const VIEWS = [
2095
+ { state: 'overview', label: 'Whole table' },
2096
+ { state: 'edge', label: 'Edge close-up' },
2097
+ ] as const
2098
+
2099
+ const RESET_ICON = 'M4 12a8 8 0 1 0 2.4-5.7M4 4v4h4'
2100
+
2101
+ const SURFACE_SX = {
2102
+ borderRadius: 999,
2103
+ bgcolor: 'background.paper',
2104
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.12)',
2105
+ } as const
2106
+
2107
+ const BUTTON_SX = {
2108
+ minBlockSize: 44,
2109
+ minInlineSize: 44,
2110
+ borderRadius: 999,
2111
+ textTransform: 'none',
2112
+ fontWeight: 600,
2113
+ whiteSpace: 'nowrap',
2114
+ } as const
2115
+
2116
+ interface SceneControlsProps {
2117
+ // The reset button shows only its icon, with its label as tooltip and accessible name
2118
+ readonly compact?: boolean
2119
+ // Only the reset button, without the view buttons
2120
+ readonly resetOnly?: boolean
2121
+ }
2122
+
2123
+ // The layout's own camera controls: one button per view state of the spec, and a reset that undoes an orbit
2124
+ export const SceneControls = ({ compact = false, resetOnly = false }: SceneControlsProps) => {
2125
+ const activeView = phx.frontend.useActiveViewState()
2126
+ const setViewState = phx.frontend.useSetViewState()
2127
+ const resetCamera = phx.frontend.useResetCamera()
2128
+ const resetIcon = (
2129
+ <svg viewBox="0 0 24 24" width={20} height={20} fill="none" stroke="currentColor" strokeWidth={2}>
2130
+ <path d={RESET_ICON} />
2131
+ </svg>
2132
+ )
2133
+
2134
+ return (
2135
+ <Box component="div" sx={{ display: 'flex', gap: 1 }}>
2136
+ {!resetOnly && (
2137
+ <Box component="div" role="group" aria-label="View" sx={{ ...SURFACE_SX, display: 'flex' }}>
2138
+ {VIEWS.map(view => {
2139
+ const isActive = activeView === view.state
2140
+
2141
+ return (
2142
+ <Button
2143
+ key={view.state}
2144
+ aria-pressed={isActive}
2145
+ onClick={() => setViewState(view.state)}
2146
+ sx={{
2147
+ ...BUTTON_SX,
2148
+ paddingInline: 2,
2149
+ bgcolor: isActive ? 'text.primary' : 'transparent',
2150
+ color: isActive ? 'background.paper' : 'text.primary',
2151
+ '&:hover': { bgcolor: isActive ? 'text.primary' : 'action.hover' },
2152
+ }}
2153
+ >
2154
+ {view.label}
2155
+ </Button>
2156
+ )
2157
+ })}
2158
+ </Box>
2159
+ )}
2160
+ <Tooltip title={compact ? 'Reset view' : ''}>
2161
+ <Button
2162
+ onClick={resetCamera}
2163
+ aria-label={compact ? 'Reset view' : undefined}
2164
+ startIcon={compact ? undefined : resetIcon}
2165
+ sx={{
2166
+ ...SURFACE_SX,
2167
+ ...BUTTON_SX,
2168
+ paddingInline: compact ? 0 : 2,
2169
+ color: 'text.primary',
2170
+ '&:hover': { bgcolor: 'background.paper', boxShadow: '0 6px 20px rgba(0, 0, 0, 0.18)' },
2171
+ }}
2172
+ >
2173
+ {compact ? resetIcon : 'Reset view'}
2174
+ </Button>
2175
+ </Tooltip>
2176
+ </Box>
2177
+ )
2178
+ }
2179
+ ```
2180
+
888
2181
  ### ./src/elements/ConfiguratorLayout.tsx
889
2182
 
890
2183
  ```tsx
891
2184
  import { Box } from '@mui/material'
2185
+ import { useState } from 'react'
892
2186
 
893
2187
  import { LayoutSlots, useSceneSafeArea } from '@salsita-ai/sdk/configurator'
2188
+ import { useActiveViewports } from '@salsita-ai/sdk/react'
894
2189
 
895
2190
  import { phx } from '../../.phoenix/phx.js'
2191
+ import { BottomSheet } from '../_shared/BottomSheet.js'
896
2192
  import { FullscreenToggle } from '../_shared/FullscreenToggle.js'
2193
+ import { MessageToast } from '../_shared/MessageToast.js'
897
2194
  import { ProductPanel } from '../_shared/ProductPanel.js'
2195
+ import { SceneControls } from '../_shared/SceneControls.js'
898
2196
 
899
2197
  const GUTTER = 24
900
2198
  const PANEL_WIDTH = 380
2199
+ const PHONE_GUTTER = 16
2200
+ const TOUCH_TARGET = 44
901
2201
 
902
2202
  const ConfiguratorLayout = () => {
903
2203
  const { containerRef, safeAreaRef } = useSceneSafeArea()
904
2204
  const isFullscreen = phx.frontend.useIsFullscreenActive()
2205
+ // `null` until the container has been measured; the layout shows only the scene until then
2206
+ const viewports = useActiveViewports()
2207
+ const [isSheetOpen, setSheetOpen] = useState(false)
2208
+
2209
+ const isDesktop = viewports?.includes('desktop') ?? false
2210
+ const gutter = isDesktop ? GUTTER : PHONE_GUTTER
905
2211
 
906
2212
  return (
907
- <Box component="div" ref={containerRef} sx={{ position: 'absolute', inset: 0 }}>
2213
+ <Box
2214
+ component="div"
2215
+ ref={containerRef}
2216
+ // `clip`, not `hidden`: what overflows, such as the sheet's fill below its edge, can never be scrolled into view,
2217
+ // not even by the browser when the focus moves
2218
+ sx={{ position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', overflow: 'clip' }}
2219
+ >
908
2220
  <LayoutSlots.SharedCanvas />
909
2221
 
910
- {!isFullscreen && (
2222
+ {viewports !== null && isDesktop && !isFullscreen && (
911
2223
  <Box
912
2224
  component="aside"
913
2225
  sx={{
@@ -921,31 +2233,72 @@ const ConfiguratorLayout = () => {
921
2233
  borderRadius: 5,
922
2234
  bgcolor: 'background.paper',
923
2235
  boxShadow: '0 12px 40px rgba(0, 0, 0, 0.12)',
924
- overflow: 'hidden',
2236
+ // A very short container, such as a phone held sideways, scrolls the panel instead of clipping its footer
2237
+ overflowX: 'hidden',
2238
+ overflowY: 'auto',
2239
+ overscrollBehavior: 'contain',
925
2240
  }}
926
2241
  >
927
2242
  <ProductPanel />
928
2243
  </Box>
929
2244
  )}
930
2245
 
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
- >
2246
+ {viewports !== null && (
942
2247
  <Box
943
2248
  component="div"
944
- sx={{ position: 'absolute', insetBlockStart: GUTTER, insetInlineEnd: GUTTER, pointerEvents: 'auto' }}
2249
+ ref={safeAreaRef}
2250
+ sx={{
2251
+ // Desktop: right of the panel. Phone: above the sheet, whose height the column gives the rest.
2252
+ ...(isDesktop
2253
+ ? {
2254
+ position: 'absolute',
2255
+ insetBlock: 0,
2256
+ insetInlineStart: isFullscreen ? 0 : PANEL_WIDTH + 2 * GUTTER,
2257
+ insetInlineEnd: 0,
2258
+ }
2259
+ : { position: 'relative', flex: '1 1 0', minBlockSize: 0 }),
2260
+ pointerEvents: 'none',
2261
+ }}
945
2262
  >
946
- <FullscreenToggle />
2263
+ <Box
2264
+ component="div"
2265
+ sx={{ position: 'absolute', insetBlockStart: gutter, insetInlineEnd: gutter, pointerEvents: 'auto' }}
2266
+ >
2267
+ <FullscreenToggle compact={!isDesktop} />
2268
+ </Box>
2269
+ <Box
2270
+ component="div"
2271
+ sx={{
2272
+ position: 'absolute',
2273
+ pointerEvents: 'auto',
2274
+ // Desktop: bottom left, above the row of view controls. Phone: at the top beside the fullscreen button,
2275
+ // clear of the sheet's handle and the view controls.
2276
+ ...(isDesktop
2277
+ ? { insetBlockEnd: 2 * GUTTER + TOUCH_TARGET, insetInlineStart: GUTTER }
2278
+ : { insetBlockStart: gutter, insetInline: `${gutter}px ${2 * gutter + TOUCH_TARGET}px` }),
2279
+ }}
2280
+ >
2281
+ <MessageToast />
2282
+ </Box>
2283
+ {/* A phone in fullscreen keeps only the reset, as the built-in layout does. Below 1024 pixels (`tablet`) the
2284
+ free area beside the panel is narrow, so the reset shows only its icon there too. */}
2285
+ <Box
2286
+ component="div"
2287
+ sx={{ position: 'absolute', insetBlockEnd: gutter, insetInlineEnd: gutter, pointerEvents: 'auto' }}
2288
+ >
2289
+ <SceneControls
2290
+ compact={!isDesktop || (viewports?.includes('tablet') ?? false)}
2291
+ resetOnly={!isDesktop && isFullscreen}
2292
+ />
2293
+ </Box>
947
2294
  </Box>
948
- </Box>
2295
+ )}
2296
+
2297
+ {viewports !== null && !isDesktop && !isFullscreen && (
2298
+ <BottomSheet open={isSheetOpen} onOpenChange={setSheetOpen} label="Product details">
2299
+ <ProductPanel collapsed={!isSheetOpen} inSheet />
2300
+ </BottomSheet>
2301
+ )}
949
2302
  </Box>
950
2303
  )
951
2304
  }
@@ -953,6 +2306,68 @@ const ConfiguratorLayout = () => {
953
2306
  export default phx.frontend.createCustomPhoenixLayoutElement('ConfiguratorLayout', ConfiguratorLayout)
954
2307
  ```
955
2308
 
2309
+ ### ./src/elements/EdgeFinishHotspot.tsx
2310
+
2311
+ ```tsx
2312
+ import { Box, ButtonBase, Popover, Tooltip } from '@mui/material'
2313
+ import { useState } from 'react'
2314
+
2315
+ import { phx } from '../../.phoenix/phx.js'
2316
+ import { EdgeFinishCards } from '../_shared/EdgeFinishCards.js'
2317
+
2318
+ // The edge profile the button shows, the square edge of the cards' drawings
2319
+ const EDGE_ICON = 'M3 7H17L21 11V13L17 17H3Z'
2320
+
2321
+ // Rendered by the spec's `htmlHotspot` on the table's edge: a button on the product that opens the edge finishes
2322
+ // next to the edge, with the same cards as the panel. The spec's `$filter` decides when it is shown.
2323
+ export default phx.frontend.createCustomPhoenixEntityElement('EdgeFinishHotspot', ({ entityId }) => {
2324
+ const edgeFinish = phx.frontend.useAttributeSelection(entityId, 'edgeFinish')
2325
+ const [anchor, setAnchor] = useState<HTMLElement | null>(null)
2326
+
2327
+ return (
2328
+ <>
2329
+ <Tooltip title="Edge finish">
2330
+ <ButtonBase
2331
+ aria-label="Edge finish"
2332
+ aria-haspopup="dialog"
2333
+ aria-expanded={anchor !== null}
2334
+ onClick={event => setAnchor(event.currentTarget)}
2335
+ sx={{
2336
+ display: 'grid',
2337
+ placeItems: 'center',
2338
+ inlineSize: 44,
2339
+ blockSize: 44,
2340
+ borderRadius: '50%',
2341
+ bgcolor: 'background.paper',
2342
+ color: 'primary.main',
2343
+ boxShadow: '0 4px 16px rgba(0, 0, 0, 0.2)',
2344
+ '&.Mui-focusVisible': { outline: 2, outlineColor: 'primary.main', outlineOffset: 2 },
2345
+ }}
2346
+ >
2347
+ <svg viewBox="0 0 24 24" width={22} height={22} aria-hidden>
2348
+ <path d={EDGE_ICON} fill="currentColor" />
2349
+ </svg>
2350
+ </ButtonBase>
2351
+ </Tooltip>
2352
+ <Popover
2353
+ open={anchor !== null}
2354
+ anchorEl={anchor}
2355
+ onClose={() => setAnchor(null)}
2356
+ anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
2357
+ transformOrigin={{ vertical: 'bottom', horizontal: 'center' }}
2358
+ slotProps={{
2359
+ paper: { role: 'dialog', 'aria-label': 'Edge finish', sx: { borderRadius: 4, marginBlockStart: -1 } },
2360
+ }}
2361
+ >
2362
+ <Box component="div" sx={{ inlineSize: 340, maxInlineSize: 'calc(100vw - 32px)', padding: 2 }}>
2363
+ <EdgeFinishCards selection={edgeFinish} />
2364
+ </Box>
2365
+ </Popover>
2366
+ </>
2367
+ )
2368
+ })
2369
+ ```
2370
+
956
2371
  ### ./src/expressions/table.ts
957
2372
 
958
2373
  ```ts
@@ -978,4 +2393,45 @@ export default phx.domain.implementCustomExpressions({
978
2393
  })
979
2394
  ```
980
2395
 
2396
+ ### ./src/handlers/request-quote.ts
2397
+
2398
+ ```ts
2399
+ import { useCallback } from 'react'
2400
+
2401
+ import { phx } from '../../.phoenix/phx.js'
2402
+
2403
+ // Stands in for the shop's quote service: the request takes a moment, then the panel shows the confirmation
2404
+ export default phx.frontend.createPhoenixHandler('requestQuote', () => {
2405
+ const navigate = phx.frontend.useNavigate()
2406
+
2407
+ return {
2408
+ handler: useCallback(async () => {
2409
+ await new Promise(resolve => setTimeout(resolve, 1500))
2410
+ navigate('requested')
2411
+ }, [navigate]),
2412
+ }
2413
+ })
2414
+ ```
2415
+
2416
+ ### ./src/transformers/tree-trunk-delivery.ts
2417
+
2418
+ ```ts
2419
+ import { msg } from '@lingui/core/macro'
2420
+
2421
+ import { phx } from '../../.phoenix/phx.js'
2422
+
2423
+ // A tree-trunk edge is cut by hand; choosing it carries a hint, which reaches the layout as a side-effect message
2424
+ export default phx.frontend.transformerBuilder.when
2425
+ .entity(['Table'])
2426
+ .modified({ atFields: ['edgeFinish'] })
2427
+ .then.overrideMatchingUpdate(({ update }) => {
2428
+ if (update.stateDelta.edgeFinish !== 'TreeTrunk') {
2429
+ return [update]
2430
+ }
2431
+
2432
+ return [{ ...update, hint: msg`Tree trunk edges are cut by hand, so delivery takes three weeks longer.` }]
2433
+ })
2434
+ .build('tree-trunk-delivery')
2435
+ ```
2436
+
981
2437
  </details>