@salsita-ai/sdk 0.50.0-develop.abef6d86a1 → 0.50.0-develop.ae52227c7e

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 (74) hide show
  1. package/README.md +2 -2
  2. package/dist/application-spec/errors/diagnostic-codes.d.ts +1 -0
  3. package/dist/application-spec/errors/diagnostic-codes.js +1 -1
  4. package/dist/application-spec/errors/diagnostic-messages.d.ts +1 -0
  5. package/dist/application-spec/errors/diagnostic-messages.js +1 -1
  6. package/dist/application-spec/generate-application/code/project-scaffolding.d.ts +7 -0
  7. package/dist/application-spec/generate-application/code/project-scaffolding.js +1 -1
  8. package/dist/application-spec/generate-application/code/templates/domain-spec/generate-domain-block.js +25 -23
  9. package/dist/application-spec/generate-application/code/templates/domain.js +3 -3
  10. package/dist/application-spec/generate-application/code/templates/package-json.js +1 -1
  11. package/dist/application-spec/generate-application/code/templates/skills/sdk-content.d.ts +1 -0
  12. package/dist/application-spec/generate-application/code/templates/skills/sdk-content.js +1 -1
  13. package/dist/application-spec/parse-application-files/spec/model/defaults.d.ts +5 -0
  14. package/dist/application-spec/parse-application-files/spec/model/defaults.js +1 -1
  15. package/dist/configurator/pages/DefaultConfiguratorLayout.d.ts +6 -0
  16. package/dist/configurator/pages/DefaultConfiguratorLayout.js +1 -0
  17. package/dist/configurator/pages/LoadedConfiguratorDetailPage.js +1 -1
  18. package/dist/configurator/ui/configurator-layout/universal/DesktopLayout.js +4 -4
  19. package/dist/configurator/ui/configurator-layout/universal/DesktopPreview.js +1 -1
  20. package/dist/configurator/ui/configurator-layout/universal/MobileLayout.js +4 -4
  21. package/dist/configurator/ui/configurator-layout/universal/MobilePreview.js +1 -1
  22. package/dist/configurator/ui/configurator-layout/universal/use-layout-scene-safe-area.d.ts +47 -0
  23. package/dist/configurator/ui/configurator-layout/universal/use-layout-scene-safe-area.js +1 -0
  24. package/dist/configurator/ui/configurator-layout/universal/use-scene-safe-area.d.ts +11 -26
  25. package/dist/configurator/ui/configurator-layout/universal/use-scene-safe-area.js +1 -1
  26. package/dist/docs/complex-use-cases/filtered-controls-with-fix-dialog.mdx +16 -19
  27. package/dist/docs/complex-use-cases/sketching.mdx +0 -38
  28. package/dist/docs/controls/select-attribute/cardselect.mdx +9 -13
  29. package/dist/docs/index.json +1 -1
  30. package/dist/docs/integrations/woocommerce/local-development.mdx +1 -1
  31. package/dist/docs/other-features/side-effects.mdx +8 -11
  32. package/dist/docs/other-features/violation-messages.mdx +52 -56
  33. package/dist/docs/phoenix/configurator-interface.mdx +100 -65
  34. package/dist/docs/phoenix/phoenix-cli.mdx +19 -26
  35. package/dist/docs/phoenix/project-structure.mdx +8 -6
  36. package/dist/docs/phoenix/quickstart.mdx +2 -2
  37. package/dist/docs/phoenix/sdk-builder-workflow.mdx +2 -2
  38. package/dist/docs/phoenix/webhooks.mdx +1 -1
  39. package/dist/domain/core/component/domain-component-optic.d.ts +10 -17
  40. package/dist/domain/core/component/domain-component-optic.js +2 -2
  41. package/dist/domain/core/component/legacy-filter.d.ts +2 -17
  42. package/dist/domain/core/component/system-component-filter.d.ts +21 -16
  43. package/dist/domain/core/invariant/custom-invariants.d.ts +6 -14
  44. package/dist/domain/core/invariant/invariants.d.ts +1 -4
  45. package/dist/domain/core/state/migrations.d.ts +14 -45
  46. package/dist/domain/core/state/migrations.js +1 -1
  47. package/dist/domain/domain-builder.js +1 -1
  48. package/dist/domain/domain-runtime/spec/create-violation-expressions.d.ts +24 -0
  49. package/dist/domain/domain-runtime/spec/create-violation-expressions.js +1 -0
  50. package/dist/domain/domain-runtime/spec/index.d.ts +1 -1
  51. package/dist/domain/domain-runtime/spec/index.js +1 -1
  52. package/dist/domain/domain-runtime/spec/module-checks/index.d.ts +1 -1
  53. package/dist/domain/domain-runtime/spec/module-checks/resolve-invariant-modules.d.ts +2 -2
  54. package/dist/legacy/domain-cli/gen/invariants.js +3 -4
  55. package/dist/legacy/domain-cli/utils/cache.d.ts +1 -1
  56. package/dist/legacy/domain-cli/utils/cache.js +1 -1
  57. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/claude-md.d.ts +1 -1
  58. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/cursor-rules.d.ts +1 -1
  59. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/shared-sdk-content.d.ts +1 -1
  60. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/agents/shared-sdk-content.js +1 -0
  61. package/dist/legacy/phoenix-cli/project-gen/generators/file-templates/domain-index.js +17 -15
  62. package/dist/phoenix/application-spec/application-spec-builder.js +1 -1
  63. package/dist/phoenix/commands/init.d.ts +4 -4
  64. package/dist/phoenix/commands/init.js +1 -1
  65. package/dist/phoenix/commands/prettier.js +1 -1
  66. package/dist/phoenix/utils/git/index.d.ts +1 -0
  67. package/dist/phoenix/utils/git/index.js +1 -0
  68. package/dist/phoenix/utils/git/is-inside-git-repository.d.ts +6 -0
  69. package/dist/phoenix/utils/git/is-inside-git-repository.js +1 -0
  70. package/dist/phoenix/utils/index.d.ts +1 -0
  71. package/dist/phoenix/utils/index.js +1 -1
  72. package/package.json +3 -3
  73. package/dist/domain/domain-runtime/spec/invariant-builder.d.ts +0 -26
  74. package/dist/domain/domain-runtime/spec/invariant-builder.js +0 -1
@@ -25,8 +25,9 @@ The configurator interface is generated in `.phoenix/configurator-interface.ts`:
25
25
 
26
26
  ```typescript
27
27
  // .phoenix/configurator-interface.ts (AUTO-GENERATED - DO NOT EDIT)
28
- import {'{'} createConfiguratorInterface {'}'} from '@salsita-ai/sdk/configurator'
29
- import {'{'} configuratorBuilder {'}'} from './domain.js'
28
+ import { createConfiguratorInterface } from '@salsita-ai/sdk/configurator'
29
+
30
+ import { configuratorBuilder } from './domain.js'
30
31
 
31
32
  export const configuratorInterface = createConfiguratorInterface(configuratorBuilder)
32
33
 
@@ -64,9 +65,9 @@ type CustomMaterial = Phx['AdminModel']['CustomMaterial']
64
65
  Access the configurator interface via `phx.frontend`:
65
66
 
66
67
  ```typescript
67
- export const CustomComponent = () => {'{'}
68
+ export const CustomComponent = () => {
68
69
  // Access domain systems
69
- const {'{'} navigationSystem, storeSystem {'}'} = phx.frontend.useDomain()
70
+ const { navigationSystem, storeSystem } = phx.frontend.useDomain()
70
71
 
71
72
  // Entity state hooks
72
73
  const rootEntity = phx.frontend.useRootEntityState()
@@ -76,11 +77,11 @@ export const CustomComponent = () => {'{'}
76
77
  const selectEntity = phx.frontend.useSelectEntity()
77
78
 
78
79
  // Type-safe entity access
79
- if (selectedEntity && Cabinet.is(selectedEntity)) {'{'}
80
+ if (selectedEntity && Cabinet.is(selectedEntity)) {
80
81
  // selectedEntity is now typed as $Cabinet
81
82
  console.log(selectedEntity.width, selectedEntity.$area)
82
- {'}'}
83
- {'}'}
83
+ }
84
+ }
84
85
  ```
85
86
 
86
87
  ## Core Hooks
@@ -90,16 +91,16 @@ The configurator interface provides 100+ hooks organized by category. Use TypeSc
90
91
  ### Domain & Application Context
91
92
 
92
93
  ```typescript
93
- const ExampleComponent = () => {'{'}
94
+ const ExampleComponent = () => {
94
95
  // Core domain systems
95
- const {'{'} navigationSystem, storeSystem, entitySystem {'}'} = phx.frontend.useDomain()
96
+ const { navigationSystem, storeSystem, entitySystem } = phx.frontend.useDomain()
96
97
 
97
98
  // Domain application context
98
- const {'{'} configurationPersistSystem {'}'} = phx.frontend.useDomainApplication()
99
+ const { configurationPersistSystem } = phx.frontend.useDomainApplication()
99
100
 
100
101
  // Full configurator context
101
- const {'{'} imageRenderingSystem {'}'} = phx.frontend.useConfigurator()
102
- {'}'}
102
+ const { imageRenderingSystem } = phx.frontend.useConfigurator()
103
+ }
103
104
  ```
104
105
 
105
106
  ### State Subscriptions
@@ -209,7 +210,7 @@ const validity = phx.frontend.useStateValidity()
209
210
  const controlViolations = phx.frontend.useControlViolations(entityId, controlId)
210
211
 
211
212
  // Finalize violations
212
- const {'{'} disabled, hints {'}'} = phx.frontend.useFinalizeViolations()
213
+ const { disabled, hints } = phx.frontend.useFinalizeViolations()
213
214
 
214
215
  // Map violations to hints
215
216
  const mapViolationHints = phx.frontend.useMapViolationHints()
@@ -284,89 +285,89 @@ The interface provides factory functions for creating handlers and providers:
284
285
 
285
286
  ```typescript
286
287
  // Runs early in app startup, before session auth
287
- const OnApplicationStart = phx.frontend.createOnApplicationStartProvider(({'{'} children {'}'}) => {'{'}
288
- return <YourProvider>{'{'}children{'}'}</YourProvider>
289
- {'}'})
288
+ const OnApplicationStart = phx.frontend.createOnApplicationStartProvider(({ children }) => {
289
+ return <YourProvider>{children}</YourProvider>
290
+ })
290
291
 
291
292
  // Runs after session auth, before domain data loading
292
- const OnSessionReady = phx.frontend.createOnSessionReadyProvider(({'{'} children {'}'}) => {'{'}
293
- return <YourProvider>{'{'}children{'}'}</YourProvider>
294
- {'}'})
293
+ const OnSessionReady = phx.frontend.createOnSessionReadyProvider(({ children }) => {
294
+ return <YourProvider>{children}</YourProvider>
295
+ })
295
296
 
296
297
  // Runs after published data is parsed, before frontend/backend loading
297
- const OnPublishedDataReady = phx.frontend.createOnPublishedDataReadyProvider(({'{'} children {'}'}) => {'{'}
298
- return <YourProvider>{'{'}children{'}'}</YourProvider>
299
- {'}'})
298
+ const OnPublishedDataReady = phx.frontend.createOnPublishedDataReadyProvider(({ children }) => {
299
+ return <YourProvider>{children}</YourProvider>
300
+ })
300
301
 
301
302
  // Runs after frontend collection and parsing, before backend loading
302
- const OnFrontendDataReady = phx.frontend.createOnFrontendDataReadyProvider(({'{'} children {'}'}) => {'{'}
303
- return <YourProvider>{'{'}children{'}'}</YourProvider>
304
- {'}'})
303
+ const OnFrontendDataReady = phx.frontend.createOnFrontendDataReadyProvider(({ children }) => {
304
+ return <YourProvider>{children}</YourProvider>
305
+ })
305
306
 
306
307
  // Runs after backend loading, before preset selection and configurator initialization
307
- const OnBackendDataReady = phx.frontend.createOnBackendDataReadyProvider(({'{'} children {'}'}) => {'{'}
308
- return <YourProvider>{'{'}children{'}'}</YourProvider>
309
- {'}'})
308
+ const OnBackendDataReady = phx.frontend.createOnBackendDataReadyProvider(({ children }) => {
309
+ return <YourProvider>{children}</YourProvider>
310
+ })
310
311
 
311
312
  // Runs after routing and preset selection are ready
312
- const OnRoutingReady = phx.frontend.createOnRoutingReadyProvider(({'{'} children {'}'}) => {'{'}
313
- return <YourProvider>{'{'}children{'}'}</YourProvider>
314
- {'}'})
313
+ const OnRoutingReady = phx.frontend.createOnRoutingReadyProvider(({ children }) => {
314
+ return <YourProvider>{children}</YourProvider>
315
+ })
315
316
 
316
317
  // Wraps OwnerUi and ReadUi pages
317
- const OnConfiguratorReady = phx.frontend.createOnConfiguratorReadyProvider(({'{'} children {'}'}) => {'{'}
318
- return <YourProvider>{'{'}children{'}'}</YourProvider>
319
- {'}'})
318
+ const OnConfiguratorReady = phx.frontend.createOnConfiguratorReadyProvider(({ children }) => {
319
+ return <YourProvider>{children}</YourProvider>
320
+ })
320
321
  ```
321
322
 
322
323
  ### Data Handlers
323
324
 
324
325
  ```typescript
325
326
  // Collect frontend data during initialization
326
- const collectFrontendData = phx.frontend.createCollectFrontendDataHandler(async (context) => {'{'}
327
- return {'{'} customData: await fetchCustomData() {'}'}
328
- {'}'})
327
+ const collectFrontendData = phx.frontend.createCollectFrontendDataHandler(async context => {
328
+ return { customData: await fetchCustomData() }
329
+ })
329
330
 
330
331
  // Override which published chunks are fetched by the frontend.
331
332
  // See Publishing page for full documentation.
332
- const filterPublishedChunks = phx.frontend.createFilterPublishedChunksHandler(() => {'{'}
333
+ const filterPublishedChunks = phx.frontend.createFilterPublishedChunksHandler(() => {
333
334
  const session = phx.frontend.useSession()
334
335
 
335
- return useMemo(() => {'{'}
336
+ return useMemo(() => {
336
337
  if (session.sessionPayload?.role === 'admin') return ['styling', 'catalog-na']
337
338
  return ['styling']
338
- {'}'}, [session.sessionPayload?.role])
339
- {'}'})
339
+ }, [session.sessionPayload?.role])
340
+ })
340
341
 
341
342
  // Runs after preset data is loaded
342
- const onPresetLoaded = phx.frontend.createOnPresetLoadedHandler((entities, presetData) => {'{'}
343
+ const onPresetLoaded = phx.frontend.createOnPresetLoadedHandler((entities, presetData) => {
343
344
  // Can modify preset entities before rendering
344
345
  return entities
345
- {'}'})
346
+ })
346
347
 
347
348
  // Intercept navigation to index/presets page
348
- const onGoToIndex = phx.frontend.createOnGoToIndexHandler(() => {'{'}
349
+ const onGoToIndex = phx.frontend.createOnGoToIndexHandler(() => {
349
350
  // Custom logic before navigating
350
- {'}'})
351
+ })
351
352
  ```
352
353
 
353
354
  ### UI Components
354
355
 
355
356
  ```typescript
356
357
  // Custom Phoenix layout element
357
- const MyLayoutElement = phx.frontend.createCustomPhoenixLayoutElement(() => {'{'}
358
+ const MyLayoutElement = phx.frontend.createCustomPhoenixLayoutElement('MyLayoutElement', () => {
358
359
  return <div>Custom Layout</div>
359
- {'}'})
360
+ })
360
361
 
361
362
  // Custom Phoenix entity element
362
- const MyEntityElement = phx.frontend.createCustomPhoenixEntityElement(() => {'{'}
363
+ const MyEntityElement = phx.frontend.createCustomPhoenixEntityElement('MyEntityElement', () => {
363
364
  return <mesh>...</mesh>
364
- {'}'})
365
+ })
365
366
 
366
367
  // Custom page when auth fails
367
- const NotAuthenticatedPage = phx.frontend.createNotAuthenticatedPage(() => {'{'}
368
+ const NotAuthenticatedPage = phx.frontend.createNotAuthenticatedPage(() => {
368
369
  return <div>Please log in</div>
369
- {'}'})
370
+ })
370
371
  ```
371
372
 
372
373
  ### Preset UI
@@ -383,21 +384,55 @@ const PresetAction = phx.frontend.createPresetPageAction(PresetHelpAction)
383
384
  const PresetOverlay = phx.frontend.createPresetDeviceNotSupported(SmallScreenNotice)
384
385
  ```
385
386
 
387
+ ### Loaded Configuration UI
388
+
389
+ A layout element registered under the name `ConfiguratorLayout` replaces the whole UI of a loaded configuration, in owner and in read mode, whatever `layout` the spec sets. Without it, Phoenix renders the layout the spec selects.
390
+
391
+ ```typescript
392
+ // src/elements/ConfiguratorLayout.tsx
393
+ import { LayoutSlots, useSceneSafeArea } from '@salsita-ai/sdk/configurator'
394
+
395
+ const SIDEBAR_WIDTH = 320
396
+
397
+ export default phx.frontend.createCustomPhoenixLayoutElement('ConfiguratorLayout', () => {
398
+ const { containerRef, safeAreaRef } = useSceneSafeArea()
399
+
400
+ return (
401
+ <div ref={containerRef} style={{ position: 'absolute', inset: 0 }}>
402
+ <LayoutSlots.Services />
403
+ <LayoutSlots.SharedCanvas />
404
+ <aside style={{ position: 'absolute', insetBlock: 0, insetInlineStart: 0, inlineSize: SIDEBAR_WIDTH }}>
405
+ Project sidebar
406
+ </aside>
407
+ <div
408
+ ref={safeAreaRef}
409
+ style={{ position: 'absolute', insetBlock: 0, insetInlineStart: SIDEBAR_WIDTH, insetInlineEnd: 0, pointerEvents: 'none' }}
410
+ />
411
+ </div>
412
+ )
413
+ })
414
+ ```
415
+
416
+ - `LayoutSlots.SharedCanvas` renders the scene. It is absolutely positioned and fills its nearest positioned ancestor, so the layout's root is positioned and covers the page. The layout positions its own UI above the canvas and after it in the markup, as the `aside` does; unpositioned content is painted under the canvas.
417
+ - `useSceneSafeArea()`, called without arguments, tells the camera which part of the canvas the UI leaves free. Attach `containerRef` to the element that covers the whole canvas and `safeAreaRef` to the region the scene should stay visible in: the hook measures the gap between the two and the camera frames the scene inside the safe area. Without it the scene is framed in the whole canvas. The safe-area element lies over the canvas, so it sets `pointerEvents: 'none'` to leave orbiting and clicking to the scene. A `canvasPadding` component in the spec scales the measured gaps, see [Canvas Padding](?path=/docs/layouts-canvas-padding--docs).
418
+ - The layout has to mount `LayoutSlots.SharedCanvas`, or the project sets `appLoader: legacy`. Otherwise the app loader stays on screen.
419
+ - The layout composes the rest from what `@salsita-ai/sdk/configurator` exports: `LayoutSlots.Services` and the other members of `LayoutSlots`.
420
+ - Nothing of the built-in layouts is rendered for it. The finalize page is `Legacy.CommonFinalizePage`, the dialog is `LayoutDialog`, and the preview start screen is `DesktopPreview` or `MobilePreview`, which the layout shows while `usePreview().isPreview` is set. Both previews frame the scene themselves, so the layout renders the component that calls `useSceneSafeArea()` only while `isPreview` is not set; it then measures when it mounts after the preview. The assistant panel is not available to a project layout.
421
+ - Any project layout element named `ConfiguratorLayout` takes over the page.
422
+
386
423
  ## TransformerBuilder
387
424
 
388
425
  Use `transformerBuilder` to create entity transformers:
389
426
 
390
427
  ```typescript
391
428
  // Create a transformer that runs when specific conditions are met
392
- const cabinetTransformer = phx.frontend.transformerBuilder
393
- .when(Cabinet.is)
394
- .do((cabinet, context) => {'{'}
395
- // Transform cabinet data
396
- return {'{'}
397
- ...cabinet,
398
- $computedField: calculateSomething(cabinet)
399
- {'}'}
400
- {'}'})
429
+ const cabinetTransformer = phx.frontend.transformerBuilder.when(Cabinet.is).do((cabinet, context) => {
430
+ // Transform cabinet data
431
+ return {
432
+ ...cabinet,
433
+ $computedField: calculateSomething(cabinet),
434
+ }
435
+ })
401
436
  ```
402
437
 
403
438
  **Note**: The deprecated `createTransformer` function should not be used for new implementations.
@@ -407,19 +442,19 @@ const cabinetTransformer = phx.frontend.transformerBuilder
407
442
  The `updates` factory creates type-safe entity updates:
408
443
 
409
444
  ```typescript
410
- const {'{'} storeSystem {'}'} = phx.frontend.useDomain()
445
+ const { storeSystem } = phx.frontend.useDomain()
411
446
 
412
447
  // Create new entity
413
- const createUpdates = phx.frontend.updates.create({'{'}
448
+ const createUpdates = phx.frontend.updates.create({
414
449
  type: 'Cabinet',
415
450
  width: 800,
416
451
  height: 2000,
417
- {'}'})
452
+ })
418
453
 
419
454
  // Modify existing entity
420
- const modifyUpdates = phx.frontend.updates.modify(cabinetId, {'{'}
455
+ const modifyUpdates = phx.frontend.updates.modify(cabinetId, {
421
456
  width: 1000,
422
- {'}'})
457
+ })
423
458
 
424
459
  // Delete entity
425
460
  const deleteUpdates = phx.frontend.updates.delete(cabinetId)
@@ -29,10 +29,11 @@ pnpm phoenix dev
29
29
 
30
30
  1. Refuses to run when a `spec/` folder already exists, so it never touches an existing project.
31
31
  2. Writes a `package.json` that depends on the CLI's SDK version when the folder has none. An existing `package.json` is kept, as long as it depends on the SDK.
32
- 3. Writes a starter spec (`spec/application.yaml`, `spec/configurator.yaml`) with one resizable, recolorable box, and switches `package.json` to the application-spec pipeline.
33
- 4. Writes `pnpm-workspace.yaml` with the pnpm settings the first install needs: the build scripts of the SDK's dependencies are allowed or denied, and `@salsita-ai/*` packages are exempt from pnpm's minimum release age. A project inside a pnpm monorepo gets no file; the workspace root holds these settings.
32
+ 3. Adds the `dev`, `build`, `deploy`, `check` and `fix` scripts to `package.json` (see [Project Scripts](#project-scripts)). A script the project already has under one of those names keeps its command.
33
+ 4. Writes a starter spec (`spec/application.yaml`, `spec/configurator.yaml`) with one resizable, recolorable box, and switches `package.json` to the application-spec pipeline.
34
+ 5. Writes `pnpm-workspace.yaml` with the pnpm settings the first install needs: the build scripts of the SDK's dependencies are allowed or denied, and `@salsita-ai/*` packages are exempt from pnpm's minimum release age. A project inside a pnpm monorepo gets no file; the workspace root holds these settings.
34
35
 
35
- It asks nothing, writes no `.env` and contacts no server. `phoenix sync` then generates every other project file, including the agent skills described in [Project Structure](?path=/docs/phoenix-project-structure--docs).
36
+ It asks nothing, writes no `.env` and contacts no server. `phoenix sync` then generates every other project file, including the agent skills described in [Project Structure](?path=/docs/phoenix-project-structure--docs). The `.gitignore` is among them only when the folder is inside a git repository, so run `git init` before the first sync, or sync again after it.
36
37
 
37
38
  ### Which project `phoenix dev` develops in
38
39
 
@@ -114,8 +115,6 @@ For quick local development without a server, use:
114
115
 
115
116
  ```bash
116
117
  phoenix dev --persistence local-storage
117
- # or use the shortcut script:
118
- pnpm dev:local
119
118
  ```
120
119
 
121
120
  **Important:** The server must be running before `deployment:prepare-local` when using `server` persistence mode.
@@ -158,27 +157,19 @@ spec/application.yaml → Server prepares environment → CLI writes .env.genera
158
157
 
159
158
  ### Project Scripts
160
159
 
161
- When you run `phoenix sync` or `phoenix clean`, the CLI auto-generates standardized scripts in your `package.json`. These scripts are enforced — they cannot be overridden by custom scripts with the same name.
162
-
163
- | Script | Command | Purpose |
164
- | -------------------------- | ------------------------------------------------------- | --------------------------------------------- |
165
- | `dev` | `phoenix dev` | Dev server with hot reload |
166
- | `dev:local` | `phoenix dev --persistence local-storage` | Dev without server dependency |
167
- | `build` | `phoenix build` | Production build |
168
- | `clean` | `phoenix clean` | Clean all artifacts and rebuild |
169
- | `sync` | `phoenix sync` | Sync generated code and dependencies |
170
- | `check` | `phoenix check` | Run all checks (TypeScript, ESLint, Prettier) |
171
- | `ts:check` | `phoenix check` | Alias for `check` |
172
- | `fix` | `phoenix fix` | Auto-fix lint and formatting issues |
173
- | `lint:fix` | `eslint src --fix` | ESLint fix only |
174
- | `prettify:fix` | `prettier --write .` | Prettier fix only |
175
- | `verify:fix` | `phoenix build && phoenix fix && phoenix check --types` | Build, fix, then type-check |
176
- | `test` | `vitest --passWithNoTests run` | Run tests |
177
- | `test:watch` | `vitest` | Tests in watch mode |
178
- | `prepare:ci` | `phoenix prepare:ci` | CI environment setup |
179
- | `deployment:prepare-local` | `phoenix deployment:prepare-local` | Prepare local environment |
180
-
181
- Custom scripts you add to `package.json` are preserved during sync. Deprecated scripts (like `environment:create`, `deployment:register`) are automatically removed.
160
+ `phoenix init` writes five scripts into `package.json`, each a shortcut for the CLI command of the same name:
161
+
162
+ | Script | Command | Purpose |
163
+ | -------- | ---------------- | --------------------------------------------- |
164
+ | `dev` | `phoenix dev` | Dev server with hot reload |
165
+ | `build` | `phoenix build` | Production build |
166
+ | `deploy` | `phoenix deploy` | Build and publish the project |
167
+ | `check` | `phoenix check` | Run all checks (TypeScript, ESLint, Prettier) |
168
+ | `fix` | `phoenix fix` | Auto-fix lint and formatting issues |
169
+
170
+ The scripts are yours: change, rename or delete them, and add your own. `phoenix sync` and `phoenix clean` leave the `scripts` section of `package.json` as it is, so they add no script, restore none you removed and remove none. Every CLI command also runs without a script, as `pnpm phoenix <command>`.
171
+
172
+ A project may have other scripts than these five, such as `sync`, `test` or `verify:fix`; they are its own and stay as they are. All of this holds for a project on the application-spec pipeline, which `phoenix init` selects. In a project on the legacy pipeline (see [Current and legacy project tooling](#current-and-legacy-project-tooling)), `phoenix sync` and `phoenix clean` write their own set of scripts into `package.json` on every run, over scripts of the same name.
182
173
 
183
174
  ### Server-Generated Variables
184
175
 
@@ -399,6 +390,8 @@ Removes all build artifacts (`.phoenix/`, `node_modules/`), reinstalls dependenc
399
390
 
400
391
  Syncs project dependencies and regenerates all project files without building Vite bundles or webhooks. Faster than `clean` — use this for routine updates after changing spec files or upgrading dependencies. Before installing, it adds the pnpm settings `phoenix init` writes to a standalone project's `pnpm-workspace.yaml` when they are missing; values you set there are kept.
401
392
 
393
+ With the application-spec pipeline (see [Current and legacy project tooling](#current-and-legacy-project-tooling)), `.gitignore` is written only when the project is inside a git repository: the project folder or one of its parent folders holds a `.git` entry. Outside a repository, sync neither creates the file nor changes one that is already there, and the same holds for `dev`, `build` and `clean`. Run `git init` before the first sync, or sync again after it, to get the file. The legacy pipeline writes `.gitignore` in every project.
394
+
402
395
  | Flag | Env Variable | Default | Description |
403
396
  | ---------------- | --------------- | ------- | ------------------------------ |
404
397
  | `--force` / `-f` | `FORCE_REBUILD` | `false` | Force full rebuild, skip cache |
@@ -178,14 +178,14 @@ type CustomMaterial = Phx['AdminModel']['CustomMaterial']
178
178
 
179
179
  | File | Auto-Generated? | Notes |
180
180
  | ------------------------------------------------------ | --------------- | -------------------------------------- |
181
- | `package.json` | Partially | Dependencies merged, scripts preserved |
181
+ | `package.json` | Partially | Dependencies merged, scripts untouched |
182
182
  | `tsconfig.json` | Yes | Managed by Phoenix |
183
183
  | `eslint.config.js` | Yes | Managed by Phoenix |
184
184
  | `prettier.config.js` | Yes | Managed by Phoenix |
185
185
  | `.prettierignore` | Yes | Managed by Phoenix |
186
186
  | `.linguirc` | Yes | Managed by Phoenix |
187
187
  | `.nvmrc` | Yes | Managed by Phoenix |
188
- | `.gitignore` | Partially | User entries between markers preserved |
188
+ | `.gitignore` | Partially | Only in a git repository; see Markers |
189
189
  | `.env` | No | Git-ignored, local config |
190
190
  | `.env.example` | No | Template for env vars |
191
191
  | `.env.generated` | Yes | Auto-generated IDs, git-ignored |
@@ -212,17 +212,19 @@ Projects created by earlier SDK versions had this guidance in `CLAUDE.md`, `.cur
212
212
 
213
213
  ### Markers for Partial Auto-Generation
214
214
 
215
- `.gitignore` preserves custom entries between markers:
215
+ `.gitignore` holds the Phoenix entries between two marker lines. Sync rewrites that block and keeps everything outside it:
216
216
 
217
217
  ```gitignore
218
- # phoenix-start
218
+ # ===== BEGIN PHOENIX AUTO-GENERATED =====
219
219
  # ... Phoenix-managed ignores ...
220
- # phoenix-end
220
+ # ===== END PHOENIX AUTO-GENERATED =====
221
221
 
222
- # Your custom ignores below (preserved)
222
+ # Your custom ignores (preserved)
223
223
  my-custom-folder/
224
224
  ```
225
225
 
226
+ With the application-spec pipeline (see [Current and legacy project tooling](?path=/docs/phoenix-phoenix-cli--docs#current-and-legacy-project-tooling)), sync writes `.gitignore` only when the project is inside a git repository: the project folder or one of its parent folders holds a `.git` entry. Outside a repository it neither creates the file nor changes one that is already there. Run `git init` before the first sync, or sync again after it, to get the file. The legacy pipeline writes the file in every project.
227
+
226
228
  ### Safe to Edit
227
229
 
228
230
  - `src/` (except compiled `src/locales/**/*.ts` files)
@@ -104,10 +104,10 @@ pnpm install
104
104
  pnpm phoenix sync
105
105
  ```
106
106
 
107
- `phoenix sync` keeps `spec/` and `src/` as your authored inputs and generates the application shell, scripts, types, and `.phoenix/` implementation. Start the generated development script without a server:
107
+ `phoenix sync` keeps `spec/` and `src/` as your authored inputs and generates the application shell, types, and `.phoenix/` implementation. Start development without a server:
108
108
 
109
109
  ```sh
110
- pnpm dev:local
110
+ pnpm phoenix dev --persistence local-storage
111
111
  ```
112
112
 
113
113
  Open the URL printed by the command. The application shell loads with the default `<salsita-configurator>` element and an empty 3D scene.
@@ -22,7 +22,7 @@ pnpm install
22
22
  pnpm phoenix sync
23
23
  ```
24
24
 
25
- `phoenix init [folder]` writes the starter project: `package.json`, `pnpm-workspace.yaml` and the starter spec, `spec/application.yaml` and `spec/configurator.yaml`, which describe one box you can resize and recolour. The starter depends on the same SDK version as the CLI that created it. `phoenix sync` then adds the rest of the project: package scripts, webhooks and lint config.
25
+ `phoenix init [folder]` writes the starter project: `package.json`, `pnpm-workspace.yaml` and the starter spec, `spec/application.yaml` and `spec/configurator.yaml`, which describe one box you can resize and recolour. The starter depends on the same SDK version as the CLI that created it, and its `package.json` has the `dev`, `build`, `deploy`, `check` and `fix` scripts, which are yours to change. `phoenix sync` then adds the rest of the project: webhooks and lint config. It writes a `.gitignore` only when the folder is inside a git repository; run `git init` before the first sync, or sync again after it, to get the file.
26
26
 
27
27
  `init` only writes these files: it asks nothing and contacts no server. It refuses a folder that already has a `spec/` folder. A `package.json` already in the folder is kept, as long as it depends on the SDK.
28
28
 
@@ -61,7 +61,7 @@ Signed in, `phoenix dev` needs no `.env` and nothing set up in Studio. When it s
61
61
 
62
62
  Every user who signs the CLI in gets their own development environment, so a teammate (a super admin of the workspace, like you) who clones the project and signs in doesn't overwrite your schema or take over your webhooks. The development environment is separate from `production`: nothing you do in `phoenix dev` changes the live configurator.
63
63
 
64
- `pnpm dev:local` still runs the project without any server, with state kept in the browser.
64
+ `pnpm phoenix dev --persistence local-storage` still runs the project without any server, with state kept in the browser.
65
65
 
66
66
  Coding agents follow the generated `phoenix` skill: before their first change they start `phoenix dev` and open the preview in your browser, and after `phoenix deploy` starts a build they open its page in Studio.
67
67
 
@@ -991,7 +991,7 @@ Save the same OAuth client ID, client secret, and base URL in the customer's **C
991
991
  JWT_SECRET_BASE64=<base64 of a long random key>
992
992
  ```
993
993
 
994
- For a deployment, run `pnpm phoenix deployment:prepare` followed by `pnpm phoenix deployment:finish`. For local development, run `pnpm deployment:prepare-local`. These current commands use the customer settings stored in Studio; the removed `environment:create` and `deployment:register` commands are not part of the setup flow.
994
+ For a deployment, run `pnpm phoenix deployment:prepare` followed by `pnpm phoenix deployment:finish`. For local development, run `pnpm phoenix deployment:prepare-local`. These current commands use the customer settings stored in Studio; the removed `environment:create` and `deployment:register` commands are not part of the setup flow.
995
995
 
996
996
  #### Clerk organization
997
997
 
@@ -1,10 +1,16 @@
1
- import type { SessionRole } from '@salsita-ai/contracts/api';
2
1
  import { type CustomExpression, type OpenExpressionContext } from '../expression/index.js';
3
2
  import type { $DomainEntity, AliasMap, DomainEntity, EntityByType, StateOfComponent, SystemComponentType, SystemComponentTypeConstraint, WritableStateOfComponent } from '../model/index.js';
4
3
  import type { DomainState } from '../state/index.js';
5
4
  import { type ComponentSystemFilter, type NavigationFilterOptic } from './legacy-filter.js';
6
- import type { GradualComponentSystemFilter, IdOrWithId } from './system-component-filter.js';
7
- import type { Viewport } from './viewport-state-component.js';
5
+ import type { ComponentSystemFilterProps, GradualComponentSystemFilter } from './system-component-filter.js';
6
+ /**
7
+ * The value a project states for one filter criterion: the value `ComponentSystemFilterProps`
8
+ * states for it, with an array read-only. `system-component-filter.ts` holds the same rule for the
9
+ * filter an optic carries; each file keeps its own copy so that the folder exports no new name.
10
+ */
11
+ type FilterCriterion<KEY extends keyof ComponentSystemFilterProps> = [
12
+ Required<ComponentSystemFilterProps>[KEY]
13
+ ] extends [ReadonlyArray<infer ITEM>] ? readonly ITEM[] : Required<ComponentSystemFilterProps>[KEY];
8
14
  type CustomOpticRead<ENTITIES extends DomainEntity<string>, $ENTITY extends $DomainEntity<ENTITIES>, COMPONENT_STATE extends Record<string, unknown>, ALLOWED_ROOT_ENTITIES extends ENTITIES, PUBLISHED_DATA extends Record<string, unknown>, SESSION_DATA extends Record<string, unknown>, FRONTEND_DATA extends Record<string, unknown>, CONTEXT_EXTENSION> = (self: $ENTITY, state: DomainState<ENTITIES>, ctx: OpenExpressionContext<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, CONTEXT_EXTENSION>) => COMPONENT_STATE;
9
15
  type CustomOpticAttach<ENTITIES extends DomainEntity<string>, ALLOWED_ROOT_ENTITIES extends ENTITIES, PUBLISHED_DATA extends Record<string, unknown>, SESSION_DATA extends Record<string, unknown>, FRONTEND_DATA extends Record<string, unknown>, ENTITY extends ENTITIES, $ENTITY extends $DomainEntity<ENTITIES>, COMPONENT_TYPE extends SystemComponentTypeConstraint, CONTEXT_EXTENSION> = (read: CustomOpticRead<ENTITIES, $ENTITY, StateOfComponent<COMPONENT_TYPE>, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, CONTEXT_EXTENSION>, write?: DomainComponentSimpleOptic<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, ENTITY, COMPONENT_TYPE, CONTEXT_EXTENSION>['write']) => DomainComponentSimpleOptic<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, ENTITY, COMPONENT_TYPE, CONTEXT_EXTENSION>;
10
16
  export interface DomainComponentSimpleOptic<ENTITIES extends DomainEntity<string>, ALLOWED_ROOT_ENTITIES extends ENTITIES, PUBLISHED_DATA extends Record<string, unknown>, SESSION_DATA extends Record<string, unknown>, FRONTEND_DATA extends Record<string, unknown>, ENTITY extends ENTITIES, COMPONENT_TYPE extends SystemComponentTypeConstraint, CONTEXT_EXTENSION = unknown> {
@@ -48,20 +54,7 @@ export declare class DomainComponentOpticBuilder<ENTITIES extends DomainEntity<s
48
54
  */
49
55
  withFilter(filter: ComponentSystemFilter | NavigationFilterOptic<ENTITIES, $ENTITY>): DomainComponentOpticBuilder<ENTITIES, ALLOWED_ROOT_ENTITIES, $MAP, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, ALIASES, ENTITY, $ENTITY, COMPONENT_TYPE, CONTEXT_EXTENSION>;
50
56
  withGradualFilter(filter: {
51
- readonly navigationStates?: readonly string[] | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, readonly string[] | undefined, CONTEXT_EXTENSION>;
52
- readonly selectedEntityIds?: readonly IdOrWithId[] | IdOrWithId | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, readonly IdOrWithId[] | IdOrWithId | undefined, CONTEXT_EXTENSION>;
53
- readonly hoveredEntityIds?: readonly IdOrWithId[] | IdOrWithId | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, readonly IdOrWithId[] | IdOrWithId | undefined, CONTEXT_EXTENSION>;
54
- readonly draggedEntityIds?: readonly IdOrWithId[] | IdOrWithId | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, readonly IdOrWithId[] | IdOrWithId | undefined, CONTEXT_EXTENSION>;
55
- readonly exportedEntityIds?: readonly IdOrWithId[] | IdOrWithId | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, readonly IdOrWithId[] | IdOrWithId | undefined, CONTEXT_EXTENSION>;
56
- readonly viewStates?: readonly string[] | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, readonly string[] | undefined, CONTEXT_EXTENSION>;
57
- readonly toolStates?: readonly string[] | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, readonly string[] | undefined, CONTEXT_EXTENSION>;
58
- readonly exportStates?: readonly string[] | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, readonly string[] | undefined, CONTEXT_EXTENSION>;
59
- readonly modes?: ReadonlyArray<'owner' | 'read' | 'finalize'> | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, ReadonlyArray<'owner' | 'read' | 'finalize'> | undefined, CONTEXT_EXTENSION>;
60
- readonly roles?: readonly SessionRole[] | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, readonly SessionRole[] | undefined, CONTEXT_EXTENSION>;
61
- readonly active?: boolean | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, boolean | undefined, CONTEXT_EXTENSION>;
62
- readonly fullscreen?: boolean | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, boolean | undefined, CONTEXT_EXTENSION>;
63
- readonly debug?: boolean | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, boolean | undefined, CONTEXT_EXTENSION>;
64
- readonly viewports?: readonly Viewport[] | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, readonly Viewport[] | undefined, CONTEXT_EXTENSION>;
57
+ readonly [KEY in keyof ComponentSystemFilterProps]?: FilterCriterion<KEY> | CustomExpression<ENTITIES, ALLOWED_ROOT_ENTITIES, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, $ENTITY, FilterCriterion<KEY> | undefined, CONTEXT_EXTENSION>;
65
58
  }): DomainComponentOpticBuilder<ENTITIES, ALLOWED_ROOT_ENTITIES, $MAP, PUBLISHED_DATA, SESSION_DATA, FRONTEND_DATA, ALIASES, ENTITY, $ENTITY, COMPONENT_TYPE, CONTEXT_EXTENSION>;
66
59
  private validate;
67
60
  private _getProfilerLabel;
@@ -1,4 +1,4 @@
1
- import{createCacheToken as t,getStickyCache as e}from"@salsita-ai/common/utils";import{withProfilerLabel as i}from"../../utils/profiler-label.js";import{createCustomExpression as n}from"../expression/index.js";export class DomainComponentOpticBuilder{aliasMap;entityTypes;componentType;instanceName;navigationFilters;generated;entityTypeOrAliasNames;constructor(t,e,i,n,s=[],a=!1,o=[]){this.aliasMap=t,this.entityTypes=e,this.componentType=i,this.instanceName=n,this.navigationFilters=s,this.generated=a,this.entityTypeOrAliasNames=o}static from(t){return new DomainComponentOpticBuilder(t,[])}withEntity(t){return new DomainComponentOpticBuilder(this.aliasMap,[...this.entityTypes,t],this.componentType,this.instanceName,this.navigationFilters,this.generated,[...this.entityTypeOrAliasNames,t])}withAlias(t){return new DomainComponentOpticBuilder(this.aliasMap,[...this.entityTypes,...this.aliasMap[t]],this.componentType,this.instanceName,this.navigationFilters,this.generated,[...this.entityTypeOrAliasNames,String(t)])}withComponent(t,e,i=!1){return new DomainComponentOpticBuilder(this.aliasMap,this.entityTypes,t,e,this.navigationFilters,i,this.entityTypeOrAliasNames)}withFilter(i){if(console.warn(`Component ${this.componentType?.name}#${this.instanceName} on entity ${this.entityTypes.join(", ")} uses a legacy filter with full filter evaluation. This is a suboptimal variant and is deprecated in favor of per-criterion evaluation.
1
+ import{createCacheToken as t,getStickyCache as e}from"@salsita-ai/common/utils";import{withProfilerLabel as i}from"../../utils/profiler-label.js";import{createCustomExpression as n}from"../expression/index.js";let s={navigationStates:void 0,selectedEntityIds:void 0,hoveredEntityIds:void 0,draggedEntityIds:void 0,viewStates:void 0,toolStates:void 0,modes:void 0,roles:void 0,active:void 0,fullscreen:void 0,debug:void 0,viewports:void 0,exportedEntityIds:void 0,exportStates:void 0};export class DomainComponentOpticBuilder{aliasMap;entityTypes;componentType;instanceName;navigationFilters;generated;entityTypeOrAliasNames;constructor(t,e,i,n,s=[],a=!1,o=[]){this.aliasMap=t,this.entityTypes=e,this.componentType=i,this.instanceName=n,this.navigationFilters=s,this.generated=a,this.entityTypeOrAliasNames=o}static from(t){return new DomainComponentOpticBuilder(t,[])}withEntity(t){return new DomainComponentOpticBuilder(this.aliasMap,[...this.entityTypes,t],this.componentType,this.instanceName,this.navigationFilters,this.generated,[...this.entityTypeOrAliasNames,t])}withAlias(t){return new DomainComponentOpticBuilder(this.aliasMap,[...this.entityTypes,...this.aliasMap[t]],this.componentType,this.instanceName,this.navigationFilters,this.generated,[...this.entityTypeOrAliasNames,String(t)])}withComponent(t,e,i=!1){return new DomainComponentOpticBuilder(this.aliasMap,this.entityTypes,t,e,this.navigationFilters,i,this.entityTypeOrAliasNames)}withFilter(i){if(console.warn(`Component ${this.componentType?.name}#${this.instanceName} on entity ${this.entityTypes.join(", ")} uses a legacy filter with full filter evaluation. This is a suboptimal variant and is deprecated in favor of per-criterion evaluation.
2
2
 
3
3
  In your TS files, replace
4
4
  withFilter()
@@ -13,4 +13,4 @@ import{createCacheToken as t,getStickyCache as e}from"@salsita-ai/common/utils";
13
13
  active:
14
14
  $read: $trueOrFalseExpression
15
15
  # ... other eventual criteria
16
- `),"function"==typeof i){let n=t(),s=t=>({evaluate:(s,a,o)=>{let r=e(a,n),d=r.get(s);if(!d){if(void 0===(d=i(o.domain.get$Entity(s,a),a)))throw Error(`Dynamic filter on component ${this.componentType?.name}.${this.instanceName} on entity ${s} returned 'undefined'. This is likely a bug in the component definition, check whether you are referencing a correct expression or field!`);r.put(s,d)}return"boolean"==typeof d?"active"===t?d:void 0:d[t]},meta:{}});return new DomainComponentOpticBuilder(this.aliasMap,this.entityTypes,this.componentType,this.instanceName,[...this.navigationFilters,{navigationStates:s("navigationStates"),selectedEntityIds:s("selectedEntityIds"),hoveredEntityIds:s("hoveredEntityIds"),draggedEntityIds:s("draggedEntityIds"),viewStates:s("viewStates"),toolStates:s("toolStates"),modes:s("modes"),roles:s("roles"),active:s("active"),fullscreen:s("fullscreen"),debug:s("debug"),viewports:s("viewports"),exportedEntityIds:void 0,exportStates:void 0}],this.generated,this.entityTypeOrAliasNames)}return"boolean"==typeof i?new DomainComponentOpticBuilder(this.aliasMap,this.entityTypes,this.componentType,this.instanceName,[...this.navigationFilters,{active:i,navigationStates:void 0,selectedEntityIds:void 0,hoveredEntityIds:void 0,draggedEntityIds:void 0,viewStates:void 0,toolStates:void 0,modes:void 0,roles:void 0,fullscreen:void 0,debug:void 0,viewports:void 0,exportedEntityIds:void 0,exportStates:void 0}],this.generated,this.entityTypeOrAliasNames):new DomainComponentOpticBuilder(this.aliasMap,this.entityTypes,this.componentType,this.instanceName,[...this.navigationFilters,{navigationStates:void 0,selectedEntityIds:void 0,hoveredEntityIds:void 0,draggedEntityIds:void 0,viewStates:void 0,toolStates:void 0,modes:void 0,roles:void 0,active:void 0,fullscreen:void 0,debug:void 0,viewports:void 0,exportedEntityIds:void 0,exportStates:void 0,...i}],this.generated,this.entityTypeOrAliasNames)}withGradualFilter(t){let e=t=>"function"==typeof t?n(t,{}):t,i={navigationStates:e(t.navigationStates),selectedEntityIds:e(t.selectedEntityIds),hoveredEntityIds:e(t.hoveredEntityIds),draggedEntityIds:e(t.draggedEntityIds),exportedEntityIds:e(t.exportedEntityIds),viewStates:e(t.viewStates),toolStates:e(t.toolStates),exportStates:e(t.exportStates),modes:e(t.modes),roles:e(t.roles),active:e(t.active),fullscreen:e(t.fullscreen),debug:e(t.debug),viewports:e(t.viewports)};return new DomainComponentOpticBuilder(this.aliasMap,this.entityTypes,this.componentType,this.instanceName,[...this.navigationFilters,i],this.generated,this.entityTypeOrAliasNames)}validate(){if(!this.componentType||!this.instanceName)throw Error("Use withComponent before attaching");if(!this.entityTypes.length)throw Error("Use withEntity and/or withAlias before attaching")}_getProfilerLabel(){return[...this.entityTypeOrAliasNames,this.componentType.name,this.instanceName].join("_")}attach=(n,s)=>{var a,o;let r,d;return this.validate(),a={kind:"simple",entityTypes:new Set(this.entityTypes),componentType:this.componentType,instanceName:this.instanceName,navigationFilters:this.navigationFilters,read:(t,e,i)=>n(i.domain.get$Entity(t,e),e,i),write:s??(()=>({})),generated:this.generated},o=this._getProfilerLabel(),r=t(),d=i(o,a.read),{...a,read:(t,i,n)=>{let s=e(i,r),a=s.get(t);if(a)return a;let o=d(t,i,n);return s.put(t,o),o}}};attachMany=n=>{var s,a;let o,r;return this.validate(),s={kind:"dynamic",entityTypes:new Set(this.entityTypes),componentType:this.componentType,instanceName:this.instanceName,navigationFilters:this.navigationFilters,read:(t,e,i)=>n(i.domain.get$Entity(t,e),e,i)},a=this._getProfilerLabel(),o=t(),r=i(a,s.read),{...s,read:(t,i,n)=>{let s=e(i,o),a=s.get(t);if(a)return a;let d=r(t,i,n);return s.put(t,d),d}}}}
16
+ `),"function"==typeof i){let n=t(),a=t=>({evaluate:(s,a,o)=>{let r=e(a,n),l=r.get(s);if(!l){if(void 0===(l=i(o.domain.get$Entity(s,a),a)))throw Error(`Dynamic filter on component ${this.componentType?.name}.${this.instanceName} on entity ${s} returned 'undefined'. This is likely a bug in the component definition, check whether you are referencing a correct expression or field!`);r.put(s,l)}return"boolean"==typeof l?"active"===t?l:void 0:l[t]},meta:{}});return new DomainComponentOpticBuilder(this.aliasMap,this.entityTypes,this.componentType,this.instanceName,[...this.navigationFilters,{...s,navigationStates:a("navigationStates"),selectedEntityIds:a("selectedEntityIds"),hoveredEntityIds:a("hoveredEntityIds"),draggedEntityIds:a("draggedEntityIds"),viewStates:a("viewStates"),toolStates:a("toolStates"),modes:a("modes"),roles:a("roles"),active:a("active"),fullscreen:a("fullscreen"),debug:a("debug"),viewports:a("viewports")}],this.generated,this.entityTypeOrAliasNames)}return"boolean"==typeof i?new DomainComponentOpticBuilder(this.aliasMap,this.entityTypes,this.componentType,this.instanceName,[...this.navigationFilters,{...s,active:i}],this.generated,this.entityTypeOrAliasNames):new DomainComponentOpticBuilder(this.aliasMap,this.entityTypes,this.componentType,this.instanceName,[...this.navigationFilters,{...s,...i}],this.generated,this.entityTypeOrAliasNames)}withGradualFilter(t){let e=t=>"function"==typeof t?n(t,{}):t,i={navigationStates:e(t.navigationStates),selectedEntityIds:e(t.selectedEntityIds),hoveredEntityIds:e(t.hoveredEntityIds),draggedEntityIds:e(t.draggedEntityIds),exportedEntityIds:e(t.exportedEntityIds),viewStates:e(t.viewStates),toolStates:e(t.toolStates),exportStates:e(t.exportStates),modes:e(t.modes),roles:e(t.roles),active:e(t.active),fullscreen:e(t.fullscreen),debug:e(t.debug),viewports:e(t.viewports)};return new DomainComponentOpticBuilder(this.aliasMap,this.entityTypes,this.componentType,this.instanceName,[...this.navigationFilters,i],this.generated,this.entityTypeOrAliasNames)}validate(){if(!this.componentType||!this.instanceName)throw Error("Use withComponent before attaching");if(!this.entityTypes.length)throw Error("Use withEntity and/or withAlias before attaching")}_getProfilerLabel(){return[...this.entityTypeOrAliasNames,this.componentType.name,this.instanceName].join("_")}attach=(n,s)=>{var a,o;let r,l;return this.validate(),a={kind:"simple",entityTypes:new Set(this.entityTypes),componentType:this.componentType,instanceName:this.instanceName,navigationFilters:this.navigationFilters,read:(t,e,i)=>n(i.domain.get$Entity(t,e),e,i),write:s??(()=>({})),generated:this.generated},o=this._getProfilerLabel(),r=t(),l=i(o,a.read),{...a,read:(t,i,n)=>{let s=e(i,r),a=s.get(t);if(a)return a;let o=l(t,i,n);return s.put(t,o),o}}};attachMany=n=>{var s,a;let o,r;return this.validate(),s={kind:"dynamic",entityTypes:new Set(this.entityTypes),componentType:this.componentType,instanceName:this.instanceName,navigationFilters:this.navigationFilters,read:(t,e,i)=>n(i.domain.get$Entity(t,e),e,i)},a=this._getProfilerLabel(),o=t(),r=i(a,s.read),{...s,read:(t,i,n)=>{let s=e(i,o),a=s.get(t);if(a)return a;let l=r(t,i,n);return s.put(t,l),l}}}}
@@ -1,22 +1,7 @@
1
- import type { SessionRole } from '@salsita-ai/contracts/api';
2
1
  import type { $DomainEntity, DomainEntity } from '../model/index.js';
3
2
  import type { DomainState } from '../state/index.js';
4
- import { type IdOrWithId } from './system-component-filter.js';
5
- import type { Viewport } from './viewport-state-component.js';
6
- export type DeclarativeComponentSystemFilter = {
7
- navigationStates?: string[];
8
- selectedEntityIds?: readonly IdOrWithId[] | IdOrWithId;
9
- hoveredEntityIds?: readonly IdOrWithId[] | IdOrWithId;
10
- draggedEntityIds?: readonly IdOrWithId[] | IdOrWithId;
11
- viewStates?: string[];
12
- toolStates?: string[];
13
- modes?: Array<'owner' | 'read' | 'finalize'>;
14
- roles?: SessionRole[];
15
- active?: boolean;
16
- fullscreen?: boolean;
17
- debug?: boolean;
18
- viewports?: readonly Viewport[];
19
- };
3
+ import { type ComponentSystemFilterProps } from './system-component-filter.js';
4
+ export type DeclarativeComponentSystemFilter = Omit<ComponentSystemFilterProps, 'exportedEntityIds' | 'exportStates'>;
20
5
  export type ComponentSystemFilter = DeclarativeComponentSystemFilter | boolean;
21
6
  export declare const ComponentSystemFilterSchema: {
22
7
  readonly anyOf: [{