@salsita-ai/sdk 0.50.0-develop.91d65bd507 → 0.50.0-develop.9239ced550

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 (212) hide show
  1. package/README.md +1 -1
  2. package/dist/application/index.d.ts +1 -0
  3. package/dist/application/index.js +1 -1
  4. package/dist/application/mount-application.d.ts +1 -1
  5. package/dist/application/mount-application.js +1 -1
  6. package/dist/application/mount-configurator.d.ts +19 -0
  7. package/dist/application/mount-configurator.js +1 -0
  8. package/dist/application/mounted-application-runtime.d.ts +4 -0
  9. package/dist/application/mounted-application-runtime.js +1 -0
  10. package/dist/application/register-web-component.d.ts +2 -0
  11. package/dist/application/register-web-component.js +1 -0
  12. package/dist/application-spec/core/enhanced-json-value.d.ts +2 -31
  13. package/dist/application-spec/core/enhanced-json-value.js +1 -1
  14. package/dist/application-spec/core/index.d.ts +1 -3
  15. package/dist/application-spec/core/index.js +1 -1
  16. package/dist/application-spec/core/markers/index.d.ts +1 -1
  17. package/dist/application-spec/core/markers/index.js +1 -1
  18. package/dist/application-spec/core/markers/msg-marker.d.ts +2 -28
  19. package/dist/application-spec/core/markers/msg-marker.js +1 -1
  20. package/dist/application-spec/core/markers/raw-eval-write-marker.d.ts +1 -1
  21. package/dist/application-spec/core/markers/raw-eval-write-marker.js +1 -1
  22. package/dist/application-spec/core/markers/raw-read-write-marker.js +1 -1
  23. package/dist/application-spec/generate-application/code/project-scaffolding.js +2 -2
  24. package/dist/application-spec/generate-application/code/templates/index.d.ts +2 -6
  25. package/dist/application-spec/generate-application/code/templates/index.js +1 -1
  26. package/dist/application-spec/generate-application/code/templates/skills/phoenix-skill-bundle.d.ts +10 -0
  27. package/dist/application-spec/generate-application/code/templates/skills/phoenix-skill-bundle.js +7 -0
  28. package/dist/application-spec/generate-application/code/templates/skills/phoenix-skill.js +1 -1
  29. package/dist/application-spec/generate-application/implementations/phx-application-types.d.ts +25 -0
  30. package/dist/application-spec/generate-application/implementations/phx-application-types.js +1 -0
  31. package/dist/application-spec/generate-application/implementations/phx-domain.d.ts +36 -0
  32. package/dist/application-spec/generate-application/implementations/phx-domain.js +1 -0
  33. package/dist/application-spec/generate-application/implementations/phx-model.d.ts +24 -0
  34. package/dist/application-spec/generate-application/implementations/phx-model.js +1 -0
  35. package/dist/application-spec/generate-application/implementations/phx.d.ts +22 -0
  36. package/dist/application-spec/generate-application/implementations/phx.js +1 -0
  37. package/dist/application-spec/generate-application/index.d.ts +2 -0
  38. package/dist/application-spec/generate-application/index.js +1 -1
  39. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-model-runtime.d.ts +3 -3
  40. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-runtime.d.ts +4 -2
  41. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-runtime.js +1 -1
  42. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-spec-runtime.d.ts +16 -0
  43. package/dist/application-spec/generate-application/runtime/admin-runtime/admin-spec-runtime.js +1 -0
  44. package/dist/application-spec/generate-application/runtime/application-runtime.d.ts +5 -0
  45. package/dist/application-spec/generate-application/runtime/application-runtime.js +1 -1
  46. package/dist/application-spec/generate-application/runtime/application-spec-slice.d.ts +71 -0
  47. package/dist/application-spec/generate-application/runtime/application-spec-slice.js +1 -0
  48. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-model-runtime.d.ts +3 -3
  49. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-runtime.d.ts +4 -2
  50. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-runtime.js +1 -1
  51. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-spec-runtime.d.ts +16 -0
  52. package/dist/application-spec/generate-application/runtime/configurator-runtime/configurator-spec-runtime.js +1 -0
  53. package/dist/application-spec/generate-application/runtime/create-application.d.ts +57 -0
  54. package/dist/application-spec/generate-application/runtime/create-application.js +1 -0
  55. package/dist/application-spec/generate-application/runtime/factory-data/recipe-defaults.js +1 -1
  56. package/dist/application-spec/generate-application/runtime/factory-data/static-violations.d.ts +1 -1
  57. package/dist/application-spec/generate-application/runtime/factory-data/static-violations.js +1 -1
  58. package/dist/application-spec/generate-application/runtime/generate-runtime-instance.d.ts +1 -1
  59. package/dist/application-spec/generate-application/runtime/generate-runtime-instance.js +1 -1
  60. package/dist/application-spec/generate-application/runtime/index.d.ts +8 -0
  61. package/dist/application-spec/generate-application/runtime/index.js +1 -0
  62. package/dist/application-spec/generate-application/runtime/sections/build-sdk-optic.js +1 -1
  63. package/dist/application-spec/generate-application/runtime/sections/expression-runtime.d.ts +1 -1
  64. package/dist/application-spec/generate-application/runtime/sections/expression-runtime.js +1 -1
  65. package/dist/application-spec/generate-application/runtime/sections/generate-memo-eval-plan-runtime.d.ts +1 -1
  66. package/dist/application-spec/generate-application/runtime/sections/generate-memo-eval-plan-runtime.js +1 -1
  67. package/dist/application-spec/generate-application/runtime/spec-code/resolve-spec-code.d.ts +1 -1
  68. package/dist/application-spec/generate-application/runtime/spec-runtime.d.ts +21 -0
  69. package/dist/application-spec/generate-application/runtime/spec-runtime.js +1 -0
  70. package/dist/application-spec/generate-application/runtime-spec/encoded-runtime-spec.d.ts +3 -2
  71. package/dist/application-spec/generate-application/runtime-spec/runtime-spec.d.ts +44 -9
  72. package/dist/application-spec/generate-application/runtime-spec/runtime-spec.js +1 -1
  73. package/dist/application-spec/parse-application-files/raw-spec/raw-expressions.d.ts +1 -1
  74. package/dist/application-spec/parse-application-files/raw-spec/raw-expressions.js +1 -1
  75. package/dist/client/authorized-client.d.ts +3 -1
  76. package/dist/client/authorized-client.js +1 -1
  77. package/dist/client/contract.d.ts +17 -0
  78. package/dist/client/index.d.ts +1 -1
  79. package/dist/client/local-authorized-client.d.ts +3 -1
  80. package/dist/client/local-authorized-client.js +1 -1
  81. package/dist/client/memory-authorized-client.d.ts +4 -1
  82. package/dist/client/memory-authorized-client.js +1 -1
  83. package/dist/client/phoenix-api.d.ts +228 -0
  84. package/dist/configurator/ConfiguratorApp.js +1 -1
  85. package/dist/configurator/application-runtime/extensions/selectors.js +1 -1
  86. package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.d.ts +13 -0
  87. package/dist/configurator/application-runtime/extensions/systems/assistant-system/assistant-system.js +1 -1
  88. package/dist/configurator/application-runtime/extensions/systems/assistant-system/graphql-tools.d.ts +2 -1
  89. package/dist/configurator/application-runtime/extensions/systems/assistant-system/graphql-tools.js +1 -1
  90. package/dist/configurator/application-runtime/extensions/systems/graphql-system/graphql-system.d.ts +15 -9
  91. package/dist/configurator/application-runtime/extensions/systems/graphql-system/graphql-system.js +1 -1
  92. package/dist/configurator/ui/assistant/screenshots/ScreenshotThumbnails.d.ts +1 -1
  93. package/dist/configurator/ui/assistant/screenshots/ScreenshotThumbnails.js +1 -1
  94. package/dist/configurator/ui/assistant/screenshots/tool-screenshots.d.ts +12 -4
  95. package/dist/configurator/ui/assistant/screenshots/tool-screenshots.js +1 -1
  96. package/dist/configurator/ui/assistant/screenshots/use-screenshot-src.d.ts +8 -0
  97. package/dist/configurator/ui/assistant/screenshots/use-screenshot-src.js +1 -0
  98. package/dist/docs/index.json +1 -1
  99. package/dist/docs/layouts/gallery/nightjar-guitar.mdx +31 -16
  100. package/dist/docs/layouts/gallery/tidepool-aquascape.mdx +205 -33
  101. package/dist/docs/phoenix/domain-model.mdx +4 -1
  102. package/dist/docs/phoenix/phoenix-cli.mdx +14 -6
  103. package/dist/docs/phoenix/project-structure.mdx +21 -17
  104. package/dist/docs/phoenix/sdk-builder-workflow.mdx +6 -4
  105. package/dist/domain/application/assistant/build-assistant-manifest.js +1 -1
  106. package/dist/domain/application/assistant/generate-assistant-prompt.js +1 -1
  107. package/dist/domain/application/configurator-builder.d.ts +7 -1
  108. package/dist/domain/application/configurator-builder.js +1 -1
  109. package/dist/domain/application/index.d.ts +1 -1
  110. package/dist/domain/application/index.js +1 -1
  111. package/dist/domain/application/standard-properties.js +1 -1
  112. package/dist/domain/domain-runtime/application.d.ts +39 -0
  113. package/dist/domain/domain-runtime/application.js +1 -0
  114. package/dist/domain/domain-runtime/domain-runtime.d.ts +36 -4
  115. package/dist/domain/domain-runtime/domain-runtime.js +1 -1
  116. package/dist/domain/domain-runtime/environment/initialize.d.ts +1 -1
  117. package/dist/domain/domain-runtime/index.d.ts +1 -0
  118. package/dist/domain/domain-runtime/index.js +1 -1
  119. package/dist/domain/domain-runtime/web-component/actions.d.ts +3 -0
  120. package/dist/domain/domain-runtime/web-component/actions.js +1 -1
  121. package/dist/domain/domain-runtime/web-component/initialize.d.ts +1 -0
  122. package/dist/domain/domain-runtime/web-component/initialize.js +1 -1
  123. package/dist/domain/domain-runtime/web-component/selectors.d.ts +2 -0
  124. package/dist/domain/domain-runtime/web-component/selectors.js +1 -1
  125. package/dist/domain/domain-runtime/web-component/types.d.ts +6 -3
  126. package/dist/domain/index.d.ts +1 -0
  127. package/dist/domain/index.js +1 -1
  128. package/dist/domain/mocks/create-mock-domain-runtime.d.ts +5 -0
  129. package/dist/domain/spec/application/index.d.ts +1 -0
  130. package/dist/domain/spec/application/index.js +1 -0
  131. package/dist/{application-spec/core → domain/spec}/components/component-filter.d.ts +3 -2
  132. package/dist/domain/spec/enhanced-json-value.d.ts +30 -0
  133. package/dist/domain/spec/enhanced-json-value.js +1 -0
  134. package/dist/domain/spec/index.d.ts +5 -0
  135. package/dist/domain/spec/index.js +1 -0
  136. package/dist/domain/spec/markers/index.d.ts +2 -0
  137. package/dist/domain/spec/markers/index.js +1 -0
  138. package/dist/domain/spec/markers/msg-marker.d.ts +28 -0
  139. package/dist/domain/spec/markers/msg-marker.js +1 -0
  140. package/dist/editor/domain-editors/DomainSpecContext.js +1 -1
  141. package/dist/editor/domain-editors/apply-spec-update.d.ts +2 -1
  142. package/dist/editor/domain-editors/apply-spec-update.js +1 -1
  143. package/dist/editor/domain-editors/use-implementation-files.d.ts +0 -3
  144. package/dist/editor/domain-editors/use-implementation-files.js +1 -1
  145. package/dist/phoenix/application-spec/generate-application-spec-project.js +2 -2
  146. package/dist/phoenix/application-spec/resolve-deployment-data.d.ts +11 -0
  147. package/dist/phoenix/application-spec/resolve-deployment-data.js +2 -2
  148. package/dist/phoenix/bin.js +1 -1
  149. package/dist/phoenix/cli.js +1 -1
  150. package/dist/phoenix/commands/deploy.js +1 -1
  151. package/dist/phoenix/commands/dev.js +5 -5
  152. package/dist/phoenix/commands/generated-env-markers.d.ts +1 -0
  153. package/dist/phoenix/commands/generated-env-markers.js +1 -1
  154. package/dist/phoenix/commands/index.d.ts +1 -1
  155. package/dist/phoenix/commands/index.js +1 -1
  156. package/dist/phoenix/lingui/index.d.ts +1 -0
  157. package/dist/phoenix/lingui/index.js +1 -0
  158. package/dist/phoenix/{services → lingui}/lingui.d.ts +5 -2
  159. package/dist/phoenix/lingui/lingui.js +1 -0
  160. package/dist/phoenix/services/application-spec-project-watcher.d.ts +3 -1
  161. package/dist/phoenix/services/application-spec-project-watcher.js +2 -2
  162. package/dist/phoenix/services/index.d.ts +1 -0
  163. package/dist/phoenix/services/index.js +1 -1
  164. package/dist/phoenix/services/lingui-watcher.js +1 -1
  165. package/dist/phoenix/services/prepare-development-environment.js +1 -1
  166. package/dist/phoenix/services/vite-config.js +1 -1
  167. package/dist/react-sdk/application-runtime/common-services/analytics/analytics-service.d.ts +1 -1
  168. package/dist/react-sdk/application-runtime/common-services/initialize.d.ts +2 -2
  169. package/dist/react-sdk/application-runtime/hooks/use-access-service.js +1 -1
  170. package/dist/react-sdk/application-runtime/hooks/use-web-component-readonly-sync.d.ts +4 -11
  171. package/dist/react-sdk/application-runtime/hooks/use-web-component-readonly-sync.js +1 -1
  172. package/dist/react-sdk/application-runtime/hooks/use-web-component.d.ts +5 -0
  173. package/dist/react-sdk/application-runtime/hooks/use-web-component.js +1 -1
  174. package/dist/react-sdk/application-runtime/host-properties/actions.d.ts +15 -0
  175. package/dist/react-sdk/application-runtime/host-properties/actions.js +1 -0
  176. package/dist/react-sdk/application-runtime/host-properties/connect-host-properties.d.ts +30 -0
  177. package/dist/react-sdk/application-runtime/host-properties/connect-host-properties.js +1 -0
  178. package/dist/react-sdk/application-runtime/host-properties/domain-host-properties.d.ts +7 -0
  179. package/dist/react-sdk/application-runtime/host-properties/domain-host-properties.js +1 -0
  180. package/dist/react-sdk/application-runtime/host-properties/host-property-descriptors.d.ts +21 -0
  181. package/dist/react-sdk/application-runtime/host-properties/host-property-descriptors.js +1 -0
  182. package/dist/react-sdk/application-runtime/host-properties/host-property-scopes.d.ts +42 -0
  183. package/dist/react-sdk/application-runtime/host-properties/host-property-scopes.js +1 -0
  184. package/dist/react-sdk/application-runtime/host-properties/index.d.ts +8 -0
  185. package/dist/react-sdk/application-runtime/host-properties/index.js +1 -0
  186. package/dist/react-sdk/application-runtime/host-properties/initialize.d.ts +7 -0
  187. package/dist/react-sdk/application-runtime/host-properties/initialize.js +1 -0
  188. package/dist/react-sdk/application-runtime/host-properties/selectors.d.ts +54 -0
  189. package/dist/react-sdk/application-runtime/host-properties/selectors.js +1 -0
  190. package/dist/react-sdk/application-runtime/host-properties/types.d.ts +18 -0
  191. package/dist/react-sdk/application-runtime/host-properties/types.js +1 -0
  192. package/dist/react-sdk/application-runtime/index.d.ts +1 -0
  193. package/dist/react-sdk/application-runtime/mocks/create-mock-application-runtime.js +1 -1
  194. package/dist/react-sdk/application-runtime/runtime/create-application-runtime.d.ts +151 -9
  195. package/dist/react-sdk/application-runtime/runtime/create-application-runtime.js +1 -1
  196. package/package.json +3 -3
  197. package/dist/phoenix/services/lingui.js +0 -1
  198. /package/dist/{application-spec/core → domain/spec/application}/application-instance.d.ts +0 -0
  199. /package/dist/{application-spec/core → domain/spec/application}/application-instance.js +0 -0
  200. /package/dist/{application-spec/core → domain/spec}/components/component-filter.js +0 -0
  201. /package/dist/{application-spec/core → domain/spec}/components/index.d.ts +0 -0
  202. /package/dist/{application-spec/core → domain/spec}/components/index.js +0 -0
  203. /package/dist/{application-spec/core → domain/spec}/components/sdk-code-ref.d.ts +0 -0
  204. /package/dist/{application-spec/core → domain/spec}/components/sdk-code-ref.js +0 -0
  205. /package/dist/{application-spec/core → domain/spec}/expressions/index.d.ts +0 -0
  206. /package/dist/{application-spec/core → domain/spec}/expressions/index.js +0 -0
  207. /package/dist/{application-spec/core → domain/spec}/expressions/relation-expression-params.d.ts +0 -0
  208. /package/dist/{application-spec/core → domain/spec}/expressions/relation-expression-params.js +0 -0
  209. /package/dist/{application-spec/core → domain/spec}/expressions/sdk-expression-implementation.d.ts +0 -0
  210. /package/dist/{application-spec/core → domain/spec}/expressions/sdk-expression-implementation.js +0 -0
  211. /package/dist/{application-spec/core → domain/spec}/markers/eval-marker.d.ts +0 -0
  212. /package/dist/{application-spec/core → domain/spec}/markers/eval-marker.js +0 -0
@@ -450,11 +450,13 @@ import { Box, ButtonBase } from '@mui/material'
450
450
  import { useState } from 'react'
451
451
 
452
452
  import { phx } from '../../.phoenix/phx.js'
453
+ import { announcePinTap } from '../_inspector/index.js'
453
454
  import { PieceIcon, TIDEPOOL, pieceLook } from '../_theme/index.js'
454
455
 
455
456
  // Rendered by the spec's `htmlHotspot` above every piece: a pin with the piece's icon, which opens into a label with
456
457
  // its name while the pointer is over the piece in the scene or over the pin, and stays open in coral while the piece
457
- // is selected. A click selects the piece; it stops there, so the scene does not take it as a click on empty water.
458
+ // is selected. A click selects the piece and announces the tap, so the phone page opens the inspector even for a piece
459
+ // that is selected already. The click goes no further, so the scene does not take it as a click on empty water.
458
460
  export default phx.frontend.createCustomPhoenixEntityElement('DecorLabel', ({ entityId }) => {
459
461
  const tank = phx.frontend.useRootEntityState()
460
462
  const selectedId = phx.frontend.useSelectedDomainEntityId()
@@ -480,6 +482,7 @@ export default phx.frontend.createCustomPhoenixEntityElement('DecorLabel', ({ en
480
482
  onClick={event => {
481
483
  event.stopPropagation()
482
484
  select(piece.id)
485
+ announcePinTap()
483
486
  }}
484
487
  onPointerEnter={() => setPointed(true)}
485
488
  onPointerLeave={() => setPointed(false)}
@@ -542,12 +545,45 @@ components:
542
545
 
543
546
  ```ts
544
547
  import { useEffect } from 'react'
548
+ import type { RefObject } from 'react'
545
549
 
546
550
  import { phx } from '../../.phoenix/phx.js'
547
551
 
548
- // Escape puts the selected piece down; Ctrl+Z or ⌘Z undoes, with Shift it redoes. The configurator sits in a shadow
549
- // root, so the key's real target is the first entry of the event's composed path.
550
- export const useWorkspaceKeys = () => {
552
+ // The input types that take text; in these Ctrl+Z belongs to the field
553
+ const TEXT_INPUT_TYPES: ReadonlySet<string> = new Set([
554
+ 'text',
555
+ 'search',
556
+ 'email',
557
+ 'url',
558
+ 'tel',
559
+ 'password',
560
+ 'number',
561
+ 'date',
562
+ 'datetime-local',
563
+ 'month',
564
+ 'time',
565
+ 'week',
566
+ ])
567
+
568
+ const isTextEntry = (target: EventTarget | null | undefined) => {
569
+ return (
570
+ target instanceof HTMLTextAreaElement ||
571
+ (target instanceof HTMLInputElement && TEXT_INPUT_TYPES.has(target.type)) ||
572
+ (target instanceof HTMLElement && target.isContentEditable)
573
+ )
574
+ }
575
+
576
+ // The configurator sits in a closed shadow root, so to the window a key pressed in it comes from the root's host; the
577
+ // root itself says which of its elements has focus
578
+ const getKeyTarget = (container: HTMLElement, event: KeyboardEvent) => {
579
+ const root = container.getRootNode()
580
+ const [first] = event.composedPath()
581
+
582
+ return root instanceof ShadowRoot && first === root.host ? root.activeElement : first
583
+ }
584
+
585
+ // Escape puts the selected piece down; Ctrl+Z or ⌘Z undoes, with Shift it redoes
586
+ export const useWorkspaceKeys = (containerRef: RefObject<HTMLElement>) => {
551
587
  const tankId = phx.frontend.useRootEntityId()
552
588
  const selectedId = phx.frontend.useSelectedDomainEntityId()
553
589
  const deselect = phx.frontend.useDeselect()
@@ -555,10 +591,20 @@ export const useWorkspaceKeys = () => {
555
591
 
556
592
  useEffect(() => {
557
593
  const onKeyDown = (event: KeyboardEvent) => {
558
- const target = event.composedPath()[0]
559
- const isTyping = target instanceof HTMLInputElement && target.type === 'text'
594
+ const container = containerRef.current
595
+
596
+ if (!container) {
597
+ return
598
+ }
599
+
600
+ const target = getKeyTarget(container, event)
601
+
602
+ // Another field's keys are not the layout's; the body's are, as focus falls there when the focused button goes
603
+ if (target !== document.body && !(target instanceof Node && container.contains(target))) {
604
+ return
605
+ }
560
606
 
561
- if (event.defaultPrevented || isTyping) {
607
+ if (event.defaultPrevented || isTextEntry(target)) {
562
608
  return
563
609
  }
564
610
 
@@ -578,10 +624,12 @@ export const useWorkspaceKeys = () => {
578
624
  window.addEventListener('keydown', onKeyDown)
579
625
 
580
626
  return () => window.removeEventListener('keydown', onKeyDown)
581
- }, [tankId, selectedId, deselect, undo, redo])
627
+ }, [containerRef, tankId, selectedId, deselect, undo, redo])
582
628
  }
583
629
  ```
584
630
 
631
+ The listener sits on `window`, but it acts only on keys pressed inside the layout's own container, which `ConfiguratorLayout` passes in as a ref, so another field on the page keeps its own Ctrl+Z, such as the multiline YAML field in the editor's Mocks panel. The configurator sits in a closed shadow root, so the window sees a key pressed in it as coming from the root's host; `getKeyTarget` asks the root which element has focus instead. A key pressed with nothing focused counts too, as its target is the page's `body`: focus falls there when the button just pressed leaves the page, as **Remove**, **Done** and a swap do, and the next Ctrl+Z must still work. Inside the container the hook still leaves a text field alone, whether a `textarea`, a content-editable element or an `input` whose type takes text. The container is focusable (`tabIndex={-1}`), so a click on the scene gives it focus and the shortcuts work after one.
632
+
585
633
  ## The camera frames the selection
586
634
 
587
635
  The tank has a `fitRootPerspectiveCamera` for each of the spec's two view states, **Front** and **Top**. Every piece has a `fitSelectionPerspectiveCamera` for each too, with a higher `priority`: a selection camera is active only while its entity is selected, so selecting a piece moves the camera to it, and putting it down moves the camera back to the tank. Both follow the view state, with no camera code in the layout.
@@ -995,7 +1043,8 @@ export const PieceActions = ({ pieceId }: PieceActionsProps) => {
995
1043
  <Button
996
1044
  variant="outlined"
997
1045
  aria-expanded={swapOpen}
998
- aria-controls="tidepool-swap"
1046
+ // The list unmounts while collapsed, so the button points at it only while it is open
1047
+ aria-controls={swapOpen ? 'tidepool-swap' : undefined}
999
1048
  onClick={() => setSwapOpen(open => !open)}
1000
1049
  startIcon={
1001
1050
  <svg viewBox="0 0 24 24" width={18} height={18} fill="none" stroke="currentColor" strokeWidth={1.8}>
@@ -1206,7 +1255,7 @@ export const Outline = () => {
1206
1255
 
1207
1256
  ## Undo and redo
1208
1257
 
1209
- Every change is one step of the undo history: adding, changing, swapping and removing a piece, and the tank's size and substrate. Selecting a piece is not a change, so undo passes over it; undoing a removal puts the piece back selected. The top bar's buttons read `canUndo` and `canRedo` from `phx.frontend.useUndoRedo()` and call `undo()` and `redo()`, which push the same analytics events as the built-in toolbar buttons; `use-workspace-keys` above binds them to Ctrl+Z and Ctrl+Shift+Z, or ⌘Z and ⇧⌘Z on a Mac. The top bar also shows the tank's name and size, how full it is and the total from `phx.frontend.useRootEntityPrice()`.
1258
+ Every change is one step of the undo history: adding, changing, swapping and removing a piece, and the tank's size and substrate. Selecting a piece is not a change, so undo passes over it; undoing a removal puts the piece back selected. The top bar's buttons read `canUndo` and `canRedo` from `phx.frontend.useUndoRedo()` and call `undo()` and `redo()`, which push the same analytics events as the built-in toolbar buttons; `use-workspace-keys` above binds them to Ctrl+Z and Ctrl+Shift+Z, or ⌘Z and ⇧⌘Z on a Mac, for keys pressed inside the layout or with nothing focused. The top bar also shows the tank's name and size, how full it is and the total from `phx.frontend.useRootEntityPrice()`.
1210
1259
 
1211
1260
  ```tsx
1212
1261
  import { Box, IconButton, Tooltip, Typography } from '@mui/material'
@@ -1387,7 +1436,7 @@ export type LatestMessageState = ReturnType<typeof useLatestMessage>
1387
1436
 
1388
1437
  ## On a phone
1389
1438
 
1390
- The same layout in a frame 390 pixels wide, as on a phone. The tank fills the screen, a bar at the bottom offers **Add**, **Inspect** and **Undo**, and the palette and the inspector rise as sheets above it. The safe area ends at the bar, or at the open sheet, so the camera keeps the tank, or the selected piece, in the free space. Selecting a piece, by tapping its pin or by adding it, opens it in the inspector. A pin shows only its icon until its piece is selected, as there is no pointer to open it. Opened on its own from the sidebar, the **Phone** story sets Storybook's viewport to 390 × 844.
1439
+ The same layout in a frame 390 pixels wide, as on a phone. The tank fills the screen, a bar at the bottom offers **Add**, **Inspect** and **Undo**, and the palette and the inspector rise as sheets above it. The safe area ends at the bar, or at the open sheet, so the camera keeps the tank, or the selected piece, in the free space. Selecting a piece, by tapping its pin or by adding it, opens it in the inspector, and tapping the pin of the piece that is still selected opens the inspector again after it was closed. A pin shows only its icon until its piece is selected, as there is no pointer to open it. Opened on its own from the sidebar, the **Phone** story sets Storybook's viewport to 390 × 844.
1391
1440
 
1392
1441
  ```tsx
1393
1442
  import { Box, ButtonBase, Typography } from '@mui/material'
@@ -1395,7 +1444,7 @@ import { useEffect, useState } from 'react'
1395
1444
  import type { ReactNode } from 'react'
1396
1445
 
1397
1446
  import { phx } from '../../.phoenix/phx.js'
1398
- import { Inspector, Outline } from '../_inspector/index.js'
1447
+ import { Inspector, Outline, onPinTap } from '../_inspector/index.js'
1399
1448
  import type { LatestMessageState } from '../_inspector/index.js'
1400
1449
  import { BrandMark, CARD_SX, PieceIcon, TIDEPOOL, pieceLook } from '../_theme/index.js'
1401
1450
  import { Palette } from './Palette.js'
@@ -1479,12 +1528,16 @@ export const PhonePage = ({ messages }: PhonePageProps) => {
1479
1528
  const selected = tank.$decor.find(piece => piece.id === selectedId)
1480
1529
  const selectedLook = selected ? pieceLook(selected) : null
1481
1530
 
1531
+ // Adding a piece selects it, and so does a click on it in the scene; the inspector follows the new selection
1482
1532
  useEffect(() => {
1483
1533
  if (selectedId !== tankId) {
1484
1534
  setSheet('inspect')
1485
1535
  }
1486
1536
  }, [selectedId, tankId])
1487
1537
 
1538
+ // A pin tap reopens the inspector for a piece that stayed selected, which the selection above does not show
1539
+ useEffect(() => onPinTap(() => setSheet('inspect')), [])
1540
+
1488
1541
  const toggle = (next: Sheet) => setSheet(current => (current === next ? null : next))
1489
1542
 
1490
1543
  return (
@@ -1600,10 +1653,35 @@ export const PhonePage = ({ messages }: PhonePageProps) => {
1600
1653
  }
1601
1654
  ```
1602
1655
 
1603
- `ConfiguratorLayout` keeps the scene mounted and lays the desktop or the phone page over it, by the width of the configurator's container from `phx.frontend.useActiveViewports()`. It also holds the messages and the keyboard shortcuts, so both work whichever page shows.
1656
+ A pin is drawn inside the scene, so it cannot hand the phone page a prop. `DecorLabel` announces each tap through a small channel, and the phone page opens the inspector on it. The channel is what reopens the sheet for a piece that is still selected, where the selection does not change; a piece that is added, or clicked in the scene, opens the inspector through the new selection.
1657
+
1658
+ ```ts
1659
+ type PinTapListener = () => void
1660
+
1661
+ const listeners = new Set<PinTapListener>()
1662
+
1663
+ // A pin is drawn inside the scene, so it cannot hand the phone page a prop. It announces each tap here, also the tap
1664
+ // on a piece that is already selected, which the selection alone does not show. The channel is shared by every copy of
1665
+ // the layout in the page; a tap there only opens a sheet, so that costs nothing.
1666
+ export const announcePinTap = () => {
1667
+ listeners.forEach(listener => listener())
1668
+ }
1669
+
1670
+ /** Calls `listener` on every pin tap until the returned function is called. */
1671
+ export const onPinTap = (listener: PinTapListener) => {
1672
+ listeners.add(listener)
1673
+
1674
+ return () => {
1675
+ listeners.delete(listener)
1676
+ }
1677
+ }
1678
+ ```
1679
+
1680
+ `ConfiguratorLayout` keeps the scene mounted and lays the desktop or the phone page over it, by the width of the configurator's container from `phx.frontend.useActiveViewports()`. It also holds the messages and the keyboard shortcuts, so both work whichever page shows; the shortcuts act on keys inside the container it renders, or with nothing focused.
1604
1681
 
1605
1682
  ```tsx
1606
1683
  import { Box } from '@mui/material'
1684
+ import { useRef } from 'react'
1607
1685
 
1608
1686
  import { LayoutSlots } from '@salsita-ai/sdk/configurator'
1609
1687
 
@@ -1615,17 +1693,25 @@ import { useWorkspaceKeys } from '../_shared/use-workspace-keys.js'
1615
1693
  import { TIDEPOOL } from '../_theme/index.js'
1616
1694
 
1617
1695
  // A workspace over the scene. The canvas stays mounted the whole time; the desktop or the phone page lies over it.
1618
- // The messages and the keyboard shortcuts live here, so they work whichever page shows and whatever is open.
1696
+ // The messages and the keyboard shortcuts live here, so they work whichever page shows and whatever is open. The
1697
+ // shortcuts act on keys pressed inside this container, or on the body, where focus falls when the focused control goes;
1698
+ // the container takes focus when the scene is clicked, so they work there too.
1619
1699
  const ConfiguratorLayout = () => {
1620
1700
  // `null` until the container has been measured; only the scene shows until then
1621
1701
  const viewports = phx.frontend.useActiveViewports()
1622
1702
  const messages = useLatestMessage()
1623
1703
  const isDesktop = viewports?.includes('desktop') ?? false
1704
+ const containerRef = useRef<HTMLDivElement>(null)
1624
1705
 
1625
- useWorkspaceKeys()
1706
+ useWorkspaceKeys(containerRef)
1626
1707
 
1627
1708
  return (
1628
- <Box component="div" sx={{ position: 'absolute', inset: 0, overflow: 'clip', bgcolor: TIDEPOOL.foam }}>
1709
+ <Box
1710
+ component="div"
1711
+ ref={containerRef}
1712
+ tabIndex={-1}
1713
+ sx={{ position: 'absolute', inset: 0, overflow: 'clip', bgcolor: TIDEPOOL.foam, outline: 'none' }}
1714
+ >
1629
1715
  <LayoutSlots.SharedCanvas />
1630
1716
  {viewports !== null && (isDesktop ? <DesktopPage messages={messages} /> : <PhonePage messages={messages} />)}
1631
1717
  </Box>
@@ -1718,16 +1804,18 @@ The aquarium is `public/models/gallery/aquarium.glb`, generated by `scripts/mode
1718
1804
  - On the neon tetras, the **School size** slider adds and takes away fish two at a time while it moves; at 12 the school costs $42.00.
1719
1805
  - **Swap for…** on a stone lists plants, wood and fish; **Amazon sword** puts a green Amazon sword in the stone's place, selected.
1720
1806
  - **Remove** takes the piece out, the count drops to "4 / 8" and the inspector shows the tank. **Undo**, or Ctrl+Z, puts the piece back, selected; **Redo**, or Ctrl+Shift+Z, removes it again.
1721
- - **Done**, Escape and a click on empty water put the selected piece down, and the camera returns to the whole tank.
1807
+ - Select a piece, press **Remove** and press Ctrl+Z, or ⌘Z on a Mac, at once, with no click in between: the piece comes back, selected. The button just pressed has left the page, so the shortcut must work with nothing focused. The same holds after **Done**, where Ctrl+Z takes back the last change, and after a swap from **Swap for…**, where it puts the first piece back.
1808
+ - **Done**, Escape and a click on empty water put the selected piece down, and the camera returns to the whole tank. Ctrl+Z and Escape work after a click on the tank too.
1809
+ - In the editor's **Mocks** panel, **Create Mock**, **Edit** and a click in a YAML field: typing there and pressing Ctrl+Z undoes the typing; the tank, its total and its undo history stay as they are.
1722
1810
  - A row of the palette adds its piece in the first free slot and selects it. With eight pieces in the tank every row is disabled, a banner says "The tank holds 8 pieces at most. Remove a piece to make room.", and "8 / 8" turns coral.
1723
1811
  - While more than four pieces stand in the tank, **60 cm** is faded with "A 60 cm tank holds 4 pieces at most." below it. Clicking it shows the same message at the top of the inspector, and the tank keeps its size.
1724
1812
  - With four pieces left, **60 cm** shrinks the tank, its pieces close up and the camera frames the smaller tank; the palette is disabled with "A 60 cm tank holds 4 pieces at most. Remove a piece, or choose a larger tank." **120 cm** spreads the pieces out, and the camera frames the larger tank.
1725
1813
  - With every piece removed, the outline says "Nothing yet." and the inspector suggests where to start.
1726
1814
  - **Top** moves the camera above the tank, also while a piece is selected, and **Front** moves it back; the third button undoes an orbit or a zoom.
1727
1815
  - **Aqua soil** and **River gravel** recolour the substrate and change the total.
1728
- - Applying **Full tank** in the editor's **Mocks** panel opens a full 90 cm tank on aqua soil.
1729
- - In the **Phone** story the tank fills the screen above a bar with **Add**, **Inspect** and **Undo**. **Add** raises the palette as a sheet, and the camera frames the tank above it; adding a piece opens it in the inspector sheet; tapping a pin opens that piece; closing the sheet gives the tank the screen again.
1730
- - The browser console shows no errors.
1816
+ - Applying **Full tank** in the editor's **Mocks** panel opens a full 90 cm tank on aqua soil. The canvas system logs three "Force flush timeout" errors to the browser console while it does; opening the story with that template does not.
1817
+ - In the **Phone** story the tank fills the screen above a bar with **Add**, **Inspect** and **Undo**. **Add** raises the palette as a sheet, and the camera frames the tank above it; adding a piece opens it in the inspector sheet; tapping a pin opens that piece; closing the sheet gives the tank the screen again, and tapping the same pin opens it once more.
1818
+ - The browser console shows no errors, apart from the three "Force flush timeout" errors that applying **Full tank** logs.
1731
1819
 
1732
1820
  ## What this layout leaves out
1733
1821
 
@@ -3360,7 +3448,8 @@ export const PieceActions = ({ pieceId }: PieceActionsProps) => {
3360
3448
  <Button
3361
3449
  variant="outlined"
3362
3450
  aria-expanded={swapOpen}
3363
- aria-controls="tidepool-swap"
3451
+ // The list unmounts while collapsed, so the button points at it only while it is open
3452
+ aria-controls={swapOpen ? 'tidepool-swap' : undefined}
3364
3453
  onClick={() => setSwapOpen(open => !open)}
3365
3454
  startIcon={
3366
3455
  <svg viewBox="0 0 24 24" width={18} height={18} fill="none" stroke="currentColor" strokeWidth={1.8}>
@@ -3588,6 +3677,30 @@ export const TankInspector = () => {
3588
3677
  }
3589
3678
  ```
3590
3679
 
3680
+ ### ./src/\_inspector/pin-taps.ts
3681
+
3682
+ ```ts
3683
+ type PinTapListener = () => void
3684
+
3685
+ const listeners = new Set<PinTapListener>()
3686
+
3687
+ // A pin is drawn inside the scene, so it cannot hand the phone page a prop. It announces each tap here, also the tap
3688
+ // on a piece that is already selected, which the selection alone does not show. The channel is shared by every copy of
3689
+ // the layout in the page; a tap there only opens a sheet, so that costs nothing.
3690
+ export const announcePinTap = () => {
3691
+ listeners.forEach(listener => listener())
3692
+ }
3693
+
3694
+ /** Calls `listener` on every pin tap until the returned function is called. */
3695
+ export const onPinTap = (listener: PinTapListener) => {
3696
+ listeners.add(listener)
3697
+
3698
+ return () => {
3699
+ listeners.delete(listener)
3700
+ }
3701
+ }
3702
+ ```
3703
+
3591
3704
  ### ./src/\_inspector/use-latest-message.ts
3592
3705
 
3593
3706
  ```ts
@@ -3861,7 +3974,7 @@ import { useEffect, useState } from 'react'
3861
3974
  import type { ReactNode } from 'react'
3862
3975
 
3863
3976
  import { phx } from '../../.phoenix/phx.js'
3864
- import { Inspector, Outline } from '../_inspector/index.js'
3977
+ import { Inspector, Outline, onPinTap } from '../_inspector/index.js'
3865
3978
  import type { LatestMessageState } from '../_inspector/index.js'
3866
3979
  import { BrandMark, CARD_SX, PieceIcon, TIDEPOOL, pieceLook } from '../_theme/index.js'
3867
3980
  import { Palette } from './Palette.js'
@@ -3945,12 +4058,16 @@ export const PhonePage = ({ messages }: PhonePageProps) => {
3945
4058
  const selected = tank.$decor.find(piece => piece.id === selectedId)
3946
4059
  const selectedLook = selected ? pieceLook(selected) : null
3947
4060
 
4061
+ // Adding a piece selects it, and so does a click on it in the scene; the inspector follows the new selection
3948
4062
  useEffect(() => {
3949
4063
  if (selectedId !== tankId) {
3950
4064
  setSheet('inspect')
3951
4065
  }
3952
4066
  }, [selectedId, tankId])
3953
4067
 
4068
+ // A pin tap reopens the inspector for a piece that stayed selected, which the selection above does not show
4069
+ useEffect(() => onPinTap(() => setSheet('inspect')), [])
4070
+
3954
4071
  const toggle = (next: Sheet) => setSheet(current => (current === next ? null : next))
3955
4072
 
3956
4073
  return (
@@ -4350,12 +4467,45 @@ export const ViewSwitch = () => {
4350
4467
 
4351
4468
  ```ts
4352
4469
  import { useEffect } from 'react'
4470
+ import type { RefObject } from 'react'
4353
4471
 
4354
4472
  import { phx } from '../../.phoenix/phx.js'
4355
4473
 
4356
- // Escape puts the selected piece down; Ctrl+Z or ⌘Z undoes, with Shift it redoes. The configurator sits in a shadow
4357
- // root, so the key's real target is the first entry of the event's composed path.
4358
- export const useWorkspaceKeys = () => {
4474
+ // The input types that take text; in these Ctrl+Z belongs to the field
4475
+ const TEXT_INPUT_TYPES: ReadonlySet<string> = new Set([
4476
+ 'text',
4477
+ 'search',
4478
+ 'email',
4479
+ 'url',
4480
+ 'tel',
4481
+ 'password',
4482
+ 'number',
4483
+ 'date',
4484
+ 'datetime-local',
4485
+ 'month',
4486
+ 'time',
4487
+ 'week',
4488
+ ])
4489
+
4490
+ const isTextEntry = (target: EventTarget | null | undefined) => {
4491
+ return (
4492
+ target instanceof HTMLTextAreaElement ||
4493
+ (target instanceof HTMLInputElement && TEXT_INPUT_TYPES.has(target.type)) ||
4494
+ (target instanceof HTMLElement && target.isContentEditable)
4495
+ )
4496
+ }
4497
+
4498
+ // The configurator sits in a closed shadow root, so to the window a key pressed in it comes from the root's host; the
4499
+ // root itself says which of its elements has focus
4500
+ const getKeyTarget = (container: HTMLElement, event: KeyboardEvent) => {
4501
+ const root = container.getRootNode()
4502
+ const [first] = event.composedPath()
4503
+
4504
+ return root instanceof ShadowRoot && first === root.host ? root.activeElement : first
4505
+ }
4506
+
4507
+ // Escape puts the selected piece down; Ctrl+Z or ⌘Z undoes, with Shift it redoes
4508
+ export const useWorkspaceKeys = (containerRef: RefObject<HTMLElement>) => {
4359
4509
  const tankId = phx.frontend.useRootEntityId()
4360
4510
  const selectedId = phx.frontend.useSelectedDomainEntityId()
4361
4511
  const deselect = phx.frontend.useDeselect()
@@ -4363,10 +4513,20 @@ export const useWorkspaceKeys = () => {
4363
4513
 
4364
4514
  useEffect(() => {
4365
4515
  const onKeyDown = (event: KeyboardEvent) => {
4366
- const target = event.composedPath()[0]
4367
- const isTyping = target instanceof HTMLInputElement && target.type === 'text'
4516
+ const container = containerRef.current
4517
+
4518
+ if (!container) {
4519
+ return
4520
+ }
4521
+
4522
+ const target = getKeyTarget(container, event)
4368
4523
 
4369
- if (event.defaultPrevented || isTyping) {
4524
+ // Another field's keys are not the layout's; the body's are, as focus falls there when the focused button goes
4525
+ if (target !== document.body && !(target instanceof Node && container.contains(target))) {
4526
+ return
4527
+ }
4528
+
4529
+ if (event.defaultPrevented || isTextEntry(target)) {
4370
4530
  return
4371
4531
  }
4372
4532
 
@@ -4386,7 +4546,7 @@ export const useWorkspaceKeys = () => {
4386
4546
  window.addEventListener('keydown', onKeyDown)
4387
4547
 
4388
4548
  return () => window.removeEventListener('keydown', onKeyDown)
4389
- }, [tankId, selectedId, deselect, undo, redo])
4549
+ }, [containerRef, tankId, selectedId, deselect, undo, redo])
4390
4550
  }
4391
4551
  ```
4392
4552
 
@@ -4609,6 +4769,7 @@ export const FADE_IN = keyframes`
4609
4769
 
4610
4770
  ```tsx
4611
4771
  import { Box } from '@mui/material'
4772
+ import { useRef } from 'react'
4612
4773
 
4613
4774
  import { LayoutSlots } from '@salsita-ai/sdk/configurator'
4614
4775
 
@@ -4620,17 +4781,25 @@ import { useWorkspaceKeys } from '../_shared/use-workspace-keys.js'
4620
4781
  import { TIDEPOOL } from '../_theme/index.js'
4621
4782
 
4622
4783
  // A workspace over the scene. The canvas stays mounted the whole time; the desktop or the phone page lies over it.
4623
- // The messages and the keyboard shortcuts live here, so they work whichever page shows and whatever is open.
4784
+ // The messages and the keyboard shortcuts live here, so they work whichever page shows and whatever is open. The
4785
+ // shortcuts act on keys pressed inside this container, or on the body, where focus falls when the focused control goes;
4786
+ // the container takes focus when the scene is clicked, so they work there too.
4624
4787
  const ConfiguratorLayout = () => {
4625
4788
  // `null` until the container has been measured; only the scene shows until then
4626
4789
  const viewports = phx.frontend.useActiveViewports()
4627
4790
  const messages = useLatestMessage()
4628
4791
  const isDesktop = viewports?.includes('desktop') ?? false
4792
+ const containerRef = useRef<HTMLDivElement>(null)
4629
4793
 
4630
- useWorkspaceKeys()
4794
+ useWorkspaceKeys(containerRef)
4631
4795
 
4632
4796
  return (
4633
- <Box component="div" sx={{ position: 'absolute', inset: 0, overflow: 'clip', bgcolor: TIDEPOOL.foam }}>
4797
+ <Box
4798
+ component="div"
4799
+ ref={containerRef}
4800
+ tabIndex={-1}
4801
+ sx={{ position: 'absolute', inset: 0, overflow: 'clip', bgcolor: TIDEPOOL.foam, outline: 'none' }}
4802
+ >
4634
4803
  <LayoutSlots.SharedCanvas />
4635
4804
  {viewports !== null && (isDesktop ? <DesktopPage messages={messages} /> : <PhonePage messages={messages} />)}
4636
4805
  </Box>
@@ -4647,11 +4816,13 @@ import { Box, ButtonBase } from '@mui/material'
4647
4816
  import { useState } from 'react'
4648
4817
 
4649
4818
  import { phx } from '../../.phoenix/phx.js'
4819
+ import { announcePinTap } from '../_inspector/index.js'
4650
4820
  import { PieceIcon, TIDEPOOL, pieceLook } from '../_theme/index.js'
4651
4821
 
4652
4822
  // Rendered by the spec's `htmlHotspot` above every piece: a pin with the piece's icon, which opens into a label with
4653
4823
  // its name while the pointer is over the piece in the scene or over the pin, and stays open in coral while the piece
4654
- // is selected. A click selects the piece; it stops there, so the scene does not take it as a click on empty water.
4824
+ // is selected. A click selects the piece and announces the tap, so the phone page opens the inspector even for a piece
4825
+ // that is selected already. The click goes no further, so the scene does not take it as a click on empty water.
4655
4826
  export default phx.frontend.createCustomPhoenixEntityElement('DecorLabel', ({ entityId }) => {
4656
4827
  const tank = phx.frontend.useRootEntityState()
4657
4828
  const selectedId = phx.frontend.useSelectedDomainEntityId()
@@ -4677,6 +4848,7 @@ export default phx.frontend.createCustomPhoenixEntityElement('DecorLabel', ({ en
4677
4848
  onClick={event => {
4678
4849
  event.stopPropagation()
4679
4850
  select(piece.id)
4851
+ announcePinTap()
4680
4852
  }}
4681
4853
  onPointerEnter={() => setPointed(true)}
4682
4854
  onPointerLeave={() => setPointed(false)}
@@ -842,7 +842,10 @@ Phoenix automatically:
842
842
  During `phoenix dev`, schema-compatible configurator changes can refresh the mounted application in
843
843
  place. Changes to the domain model itself are rejected from live application and require a restart;
844
844
  this preserves the last valid runtime and loaded configuration until migration and deployment can be
845
- coordinated atomically.
845
+ coordinated atomically. With server persistence, restart `phoenix dev` itself: it logs an error that
846
+ asks for the restart, and registers the changed model with its development deployment when it starts
847
+ again. Then open the preview address it prints rather than a tab whose address has `?cid=`, which
848
+ loads a configuration saved with the earlier model.
846
849
 
847
850
  ### The phx Global Namespace
848
851
 
@@ -164,6 +164,8 @@ A project has one generated env file. Phoenix keeps it in `.phoenix/`, as `.phoe
164
164
  - The deployment and environment commands (`deployment:prepare`, `deployment:prepare-dev`, `deployment:prepare-local`, `deployment:build-envs` and the others with an `--outFile` option) write `.env.generated` in the project root unless `--outFile` or `OUT_FILE` names another file. Deploy pipelines read it there. Once one of them has written it, `phoenix dev` and `phoenix build` use it.
165
165
  - Everything that reads environment variables follows the same rule: the root `.env.generated` when it exists, otherwise `.phoenix/.env.generated`. The two files are never merged.
166
166
  - `phoenix clean` removes `.phoenix/` and with it `.phoenix/.env.generated`; the next `phoenix dev` prepares the development deployment again. A root `.env.generated` stays.
167
+ - `phoenix dev` starts on the deployment the file names, and prepares a new one when the file was written for another project, environment or server, when the spec registers something on the server the deployment was not prepared with (the domain model, the published data schema, the auth hook, the roles that may create configurations, or the assistant), and on `--force`. A running `phoenix dev` does not prepare again after such a change: it logs an error that asks for a restart.
168
+ - Asset contents do not trigger preparation. After changing uploaded assets, restart with `phoenix dev --force` to prepare and upload them again.
167
169
 
168
170
  This holds for a project on the application-spec pipeline. In a project on the legacy pipeline (see [Current and legacy project tooling](#current-and-legacy-project-tooling)), `phoenix dev` always reads and writes `.env.generated` in the project root.
169
171
 
@@ -305,14 +307,16 @@ Starts the full development environment: Vite dev server, development webhook tu
305
307
  | `--environmentSlug` | `ENVIRONMENT_SLUG` | package.json name | Environment slug |
306
308
  | `--withLingui` | `WITH_LINGUI` | `false` | Enable i18n extraction/compilation and watch i18n sources during dev |
307
309
  | `--persistence` | `PERSISTENCE_MODE` | `server` | Persistence mode |
308
- | `--force` / `-f` | `FORCE_REBUILD` | `false` | Force full rebuild, skip cache |
310
+ | `--force` / `-f` | `FORCE_REBUILD` | `false` | Force full rebuild, skip cache, and prepare the development deployment again |
309
311
  | `--serverUrl` | `SERVER_URL` | the region's server | Phoenix server URL |
310
312
  | `--region` | `PHOENIX_REGION` | `eu` | Region of the signed-in workspace, used when no server URL is set |
311
313
  | `--skipUpload` / `--su` | `SKIP_UPLOAD` | `false` | Skip the S3 asset upload when the development deployment is prepared at startup |
312
314
 
313
315
  With the application-spec pipeline, `phoenix dev` creates an empty `src/` folder when the project has none, so that it rebuilds when the first source file, such as `src/webhooks/index.ts`, is added.
314
316
 
315
- With the application-spec pipeline, the SDK's own Lingui CLI runs with the configuration in `.phoenix/.linguirc`, both for `--withLingui` and when `phoenix sync` and `phoenix build` extract and compile the catalogs; see [Root Level Files](?path=/docs/phoenix-project-structure--docs#root-level-files). The dev server and `phoenix build` compile the Lingui macros (`msg`, `t`, `Trans` and the others, imported from `@lingui/core/macro`, `@lingui/react/macro` or `@lingui/macro`) with the SDK's own Lingui macro plugin and the same configuration, so the project needs neither `babel-plugin-macros` nor `@lingui/macro` for them. Babel macros of other packages are not compiled.
317
+ By default, with the application-spec pipeline, `phoenix dev` bootstraps missing compiled catalogs for every configured locale at startup, so a fresh project can start without a prior sync. It compiles existing `.po` translations as they are and extracts a locale only when its `.po` source is also missing. Existing compiled catalogs and `.po` sources stay unchanged. New or changed messages still need `phoenix sync` or continuous updates enabled by `--withLingui` or `WITH_LINGUI=true`.
318
+
319
+ The SDK's own Lingui CLI uses the configuration generated in `.phoenix/.linguirc`, both for `--withLingui` and when `phoenix sync` and `phoenix build` extract and compile the catalogs; see [Root Level Files](?path=/docs/phoenix-project-structure--docs#root-level-files). The dev server and `phoenix build` compile the Lingui macros (`msg`, `t`, `Trans` and the others, imported from `@lingui/core/macro`, `@lingui/react/macro` or `@lingui/macro`) with the SDK's own Lingui macro plugin and the same configuration, so the project needs neither `babel-plugin-macros` nor `@lingui/macro` for them. Babel macros of other packages are not compiled.
316
320
 
317
321
  In `server` persistence mode the project comes from `--projectSlug`, `PROJECT_SLUG` or `project` in the spec, and the credential from `DEVELOPMENT_KEY` or the `phoenix login` sign-in; see [Starting a New Project](#starting-a-new-project). When signed in, the environment is always your own development environment and `--environmentSlug` is ignored.
318
322
 
@@ -336,7 +340,11 @@ Restart the application after fixing either failure; a subscriber or generation
336
340
  observed the attempted update.
337
341
 
338
342
  Domain-model changes are not applied live yet. Phoenix rejects them with a restart-required
339
- diagnostic so the mounted application cannot observe a new model with old configuration data. Other
343
+ diagnostic so the mounted application cannot observe a new model with old configuration data. With
344
+ server persistence, `phoenix dev` logs an error that asks for its own restart, because the
345
+ development deployment keeps the schema it was prepared with until `phoenix dev` starts again and
346
+ prepares it. After the restart, open the preview address it prints: a tab whose address has `?cid=`
347
+ loads a configuration saved with the earlier model, which the new model rejects. Other
340
348
  generated bootstrap, admin, data, environment, and host-integration surfaces should also be treated
341
349
  as restart-required unless their feature documentation explicitly states live-update support.
342
350
 
@@ -609,7 +617,7 @@ Known limits:
609
617
  - Binary assets, CSS files, locale catalog changes, dependency changes and edits requiring a full application restart are unsupported.
610
618
  - Storybook `story-dev` has no variant previews. A project on the legacy pipeline has no variant previews; the command says so and exits.
611
619
 
612
- Generated projects carry the `phoenix-variants` agent skill, which tells coding agents when to offer options, how to write each kind and what to do with your answer.
620
+ Generated projects carry the `phoenix` agent skill, whose linked Variants guide tells coding agents when to offer options, how to write each kind and what to do with your answer.
613
621
 
614
622
  ## SDK Development
615
623
 
@@ -750,7 +758,7 @@ Revokes the token on the server and removes it from the credentials file. `--reg
750
758
 
751
759
  ### `phoenix deploy`
752
760
 
753
- Validates the spec, packs and uploads the project, starts its build on the server and follows it until the configurator is live. Before the upload the server compares the domain model with the deployed one: a model that changed under an already deployed model version is refused with the reason, and needs a new model version and a [migration](?path=/docs/phoenix-migrations--docs). Once the build starts it prints `Follow the build in Studio: <url>`, the build's page in the Studio you signed in through; a sign-in stored by an older CLI has no Studio URL, so sign in again to get the link. After `Your configurator is live at <url>` it prints one line for coding agents, which tells them to offer the run evaluation of the `phoenix-feedback` skill once you have nothing more to build; you can ignore it when you run the command yourself. `--region` or `--serverUrl` selects the server, as for `login`; `--no-wait` returns as soon as the build starts.
761
+ Validates the spec, packs and uploads the project, starts its build on the server and follows it until the configurator is live. Before the upload the server compares the domain model with the deployed one: a model that changed under an already deployed model version is refused with the reason, and needs a new model version and a [migration](?path=/docs/phoenix-migrations--docs). Once the build starts it prints `Follow the build in Studio: <url>`, the build's page in the Studio you signed in through; a sign-in stored by an older CLI has no Studio URL, so sign in again to get the link. After `Your configurator is live at <url>` it prints one line for coding agents, which tells them to offer the run evaluation in the Feedback guide linked from the `phoenix` skill once you have nothing more to build; you can ignore it when you run the command yourself. `--region` or `--serverUrl` selects the server, as for `login`; `--no-wait` returns as soon as the build starts.
754
762
 
755
763
  To keep the conversation that led to a build, place a reviewed UTF-8 transcript in `conversation.log` at the project root before deploying. Deploy automatically includes it in the private source archive. Download the build's sources from Studio to read it; access is the same as for the rest of that build's sources. The build extractor skips the log, so it is not an application build input or a public asset. Missing logs produce a warning and deployment continues. Directories, links and special files at that path are rejected.
756
764
 
@@ -779,7 +787,7 @@ Pass the message as the argument for a short note, or `--file` for a longer repo
779
787
 
780
788
  A message holds at most 100,000 characters, from the argument or from the file. Besides the message and the flags, the CLI sends only the SDK version, the operating system and the Node.js version; no file of the project is attached, and the Phoenix team reads a build's sources through the build's page in Studio. The project must belong to the signed-in workspace, otherwise the server refuses the feedback; without a project, the feedback is sent on its own.
781
789
 
782
- Generated projects carry the `phoenix-feedback` agent skill. It tells coding agents when to draft feedback, to show you the draft and send it only after you approve it, and how to write a report: the build id with its Studio page link, the live configurator link, what the agent was doing, what happened and what was expected, the exact error or output, the workaround and a condensed conversation log. When you say you are done, or a deploy went live and you confirm the run is finished, the agent asks you to evaluate the run, one question at a time: minutes spent, then a score from 0 to 10 for the agent's guidance, the configurator's functionality, its UX, the quality of the 3D, the UI visuals and the overall result, then your comments. It never picks a score for you and sends nothing when you decline. Secrets, credentials and personal data never go into a message; file paths, spec and code snippets and your own words may, once you approved the draft that contains them. The command needs a server with API 1.40.0 or newer; a message over 5000 characters needs API 1.43.0 or newer.
790
+ Generated projects carry the `phoenix` agent skill. Its linked Feedback guide tells coding agents when to draft feedback, to show you the draft and send it only after you approve it, and how to write a report: the build id with its Studio page link, the live configurator link, what the agent was doing, what happened and what was expected, the exact error or output, the workaround and a condensed conversation log. When you say you are done, or a deploy went live and you confirm the run is finished, the agent asks you to evaluate the run, one question at a time: minutes spent, then a score from 0 to 10 for the agent's guidance, the configurator's functionality, its UX, the quality of the 3D, the UI visuals and the overall result, then your comments. It never picks a score for you and sends nothing when you decline. Secrets, credentials and personal data never go into a message; file paths, spec and code snippets and your own words may, once you approved the draft that contains them. The command needs a server with API 1.40.0 or newer; a message over 5000 characters needs API 1.43.0 or newer.
783
791
 
784
792
  ## Next Steps
785
793
 
@@ -199,21 +199,21 @@ type CustomMaterial = Phx['AdminModel']['CustomMaterial']
199
199
 
200
200
  ### Root Level Files
201
201
 
202
- | File | Auto-Generated? | Notes |
203
- | ------------------------------------------------------ | --------------- | ------------------------------------- |
204
- | `package.json` | No | Yours; see Dependencies |
205
- | `tsconfig.json` | Once | Yours; see below |
206
- | `eslint.config.js` | Once | Yours; see below |
207
- | `prettier.config.js` | Once | Yours; see below |
208
- | `.prettierignore` | No | Optional and your own; see below |
209
- | `pnpm-workspace.yaml` | No | Only with pnpm; see Package manager |
210
- | `.gitignore` | Partially | Only in a git repository; see Markers |
211
- | `.env` | No | Git-ignored, local config |
212
- | `.env.example` | No | Template for env vars |
213
- | `.env.generated` | Yes | Optional, git-ignored; see below |
214
- | `README.md` | No | Project documentation |
215
- | `AGENTS.md` | No | Your own coding-agent instructions |
216
- | `.claude/skills/phoenix*/`, `.agents/skills/phoenix*/` | Yes | Phoenix agent skills |
202
+ | File | Auto-Generated? | Notes |
203
+ | ---------------------------------------------------- | --------------- | ------------------------------------- |
204
+ | `package.json` | No | Yours; see Dependencies |
205
+ | `tsconfig.json` | Once | Yours; see below |
206
+ | `eslint.config.js` | Once | Yours; see below |
207
+ | `prettier.config.js` | Once | Yours; see below |
208
+ | `.prettierignore` | No | Optional and your own; see below |
209
+ | `pnpm-workspace.yaml` | No | Only with pnpm; see Package manager |
210
+ | `.gitignore` | Partially | Only in a git repository; see Markers |
211
+ | `.env` | No | Git-ignored, local config |
212
+ | `.env.example` | No | Template for env vars |
213
+ | `.env.generated` | Yes | Optional, git-ignored; see below |
214
+ | `README.md` | No | Project documentation |
215
+ | `AGENTS.md` | No | Your own coding-agent instructions |
216
+ | `.claude/skills/phoenix/`, `.agents/skills/phoenix/` | Yes | Phoenix agent skills |
217
217
 
218
218
  Sync does not write `README.md`, `AGENTS.md`, `.env.example`, `.env.ci` or the `README.md` files in `assets/public/` and `assets/private/`. Earlier SDK versions wrote them once; a project that has them keeps them, and they are yours to edit.
219
219
 
@@ -242,9 +242,13 @@ With the application-spec pipeline, the Lingui configuration is `.phoenix/.lingu
242
242
 
243
243
  ### Agent Guidance
244
244
 
245
- Coding agents (Claude Code, Codex, Cursor) read the Phoenix agent skills. An `AGENTS.md` is yours to add for your own instructions; sync does not write one. The `phoenix`, `phoenix-cli`, `phoenix-feedback` and `phoenix-review` skills carry the SDK conventions, CLI tooling and review checklist, and the `phoenix-create-configurator` skill walks the agent from your product documents and assets to a reviewed configurator in five stages. The `phoenix-variants` skill has the agent show you several options over the dev preview when a decision is a matter of taste, and build the one you pick. Phoenix keeps them in step with the installed SDK, including the reference files inside a skill folder. During the internal beta, a project created from a canary build of the SDK gets one more section in the `phoenix` skill: once per conversation the agent checks for a newer canary and, after telling you, updates the project's SDK, runs `phoenix deps`, installs, then runs `phoenix sync`. Other skill folders are never touched.
245
+ Coding agents (Claude Code, Codex, Cursor) enter through the single `phoenix` skill under `.claude/skills/phoenix/` or `.agents/skills/phoenix/`. Its `SKILL.md` routes to the existing project, CLI, configurator workflow, code review, variants and feedback guides in `references/`. Agents read the guides for their task rather than the whole tree. The configurator workflow retains its five stages and supporting discovery, UX, asset-conversion and appearance-review references.
246
246
 
247
- Projects created by earlier SDK versions had this guidance in `CLAUDE.md`, `.cursor/rules/phoenix.mdc` and `commands/review.md`. Sync removes the SDK block from the first two, keeps your own text around it, deletes a file that held nothing else, and deletes the review commands; use the `phoenix-review` skill instead of `/review`.
247
+ Phoenix keeps this guidance in step with the installed SDK. During the internal beta, a project created from a canary build gets the canary-update instructions in the linked project guide: once per conversation the agent checks for a newer canary and, after telling you, updates the SDK, runs `phoenix deps`, installs, then runs `phoenix sync`.
248
+
249
+ An `AGENTS.md` is yours to add; sync does not write one. Sync removes retired `phoenix-*` skill folders but leaves unrelated skills alone. If your own instructions name an earlier separate skill such as `phoenix-review`, point them at `phoenix` and the same task instead.
250
+
251
+ Projects created by earlier SDK versions had this guidance in `CLAUDE.md`, `.cursor/rules/phoenix.mdc` and `commands/review.md`. Sync removes the SDK block from the first two, keeps your own text around it, deletes a file that held nothing else, and deletes the review commands; ask `phoenix` to review instead of using `/review`.
248
252
 
249
253
  ### Markers for Partial Auto-Generation
250
254