mythik 0.0.1 → 0.1.0
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.
- package/LICENSE +201 -0
- package/NOTICE +4 -0
- package/README.md +97 -2
- package/dist/actions/dispatcher.d.ts +46 -0
- package/dist/actions/dispatcher.d.ts.map +1 -0
- package/dist/actions/dispatcher.js +366 -0
- package/dist/actions/dispatcher.js.map +1 -0
- package/dist/actions/middleware.d.ts +27 -0
- package/dist/actions/middleware.d.ts.map +1 -0
- package/dist/actions/middleware.js +37 -0
- package/dist/actions/middleware.js.map +1 -0
- package/dist/actions/transaction-engine.d.ts +13 -0
- package/dist/actions/transaction-engine.d.ts.map +1 -0
- package/dist/actions/transaction-engine.js +96 -0
- package/dist/actions/transaction-engine.js.map +1 -0
- package/dist/app/app-engine.d.ts +106 -0
- package/dist/app/app-engine.d.ts.map +1 -0
- package/dist/app/app-engine.js +413 -0
- package/dist/app/app-engine.js.map +1 -0
- package/dist/auth/cross-tab.d.ts +24 -0
- package/dist/auth/cross-tab.d.ts.map +1 -0
- package/dist/auth/cross-tab.js +117 -0
- package/dist/auth/cross-tab.js.map +1 -0
- package/dist/auth/engine.d.ts +16 -0
- package/dist/auth/engine.d.ts.map +1 -0
- package/dist/auth/engine.js +225 -0
- package/dist/auth/engine.js.map +1 -0
- package/dist/auth/persistence.d.ts +21 -0
- package/dist/auth/persistence.d.ts.map +1 -0
- package/dist/auth/persistence.js +97 -0
- package/dist/auth/persistence.js.map +1 -0
- package/dist/auth/providers/custom-jwt.d.ts +41 -0
- package/dist/auth/providers/custom-jwt.d.ts.map +1 -0
- package/dist/auth/providers/custom-jwt.js +165 -0
- package/dist/auth/providers/custom-jwt.js.map +1 -0
- package/dist/auth/providers/mock.d.ts +7 -0
- package/dist/auth/providers/mock.d.ts.map +1 -0
- package/dist/auth/providers/mock.js +52 -0
- package/dist/auth/providers/mock.js.map +1 -0
- package/dist/auth/providers/supabase.d.ts +90 -0
- package/dist/auth/providers/supabase.d.ts.map +1 -0
- package/dist/auth/providers/supabase.js +102 -0
- package/dist/auth/providers/supabase.js.map +1 -0
- package/dist/auth/rate-limiter.d.ts +29 -0
- package/dist/auth/rate-limiter.d.ts.map +1 -0
- package/dist/auth/rate-limiter.js +67 -0
- package/dist/auth/rate-limiter.js.map +1 -0
- package/dist/auth/refresh-engine.d.ts +25 -0
- package/dist/auth/refresh-engine.d.ts.map +1 -0
- package/dist/auth/refresh-engine.js +59 -0
- package/dist/auth/refresh-engine.js.map +1 -0
- package/dist/auth/types.d.ts +89 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/contract/endpoint-map.d.ts +20 -0
- package/dist/contract/endpoint-map.d.ts.map +1 -0
- package/dist/contract/endpoint-map.js +57 -0
- package/dist/contract/endpoint-map.js.map +1 -0
- package/dist/contract/engine.d.ts +12 -0
- package/dist/contract/engine.d.ts.map +1 -0
- package/dist/contract/engine.js +80 -0
- package/dist/contract/engine.js.map +1 -0
- package/dist/contract/extractor.d.ts +7 -0
- package/dist/contract/extractor.d.ts.map +1 -0
- package/dist/contract/extractor.js +133 -0
- package/dist/contract/extractor.js.map +1 -0
- package/dist/contract/index.d.ts +4 -0
- package/dist/contract/index.d.ts.map +1 -0
- package/dist/contract/index.js +2 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/contract/matcher.d.ts +13 -0
- package/dist/contract/matcher.d.ts.map +1 -0
- package/dist/contract/matcher.js +57 -0
- package/dist/contract/matcher.js.map +1 -0
- package/dist/contract/rules/endpoints-exist.d.ts +3 -0
- package/dist/contract/rules/endpoints-exist.d.ts.map +1 -0
- package/dist/contract/rules/endpoints-exist.js +29 -0
- package/dist/contract/rules/endpoints-exist.js.map +1 -0
- package/dist/contract/rules/fields-valid.d.ts +3 -0
- package/dist/contract/rules/fields-valid.d.ts.map +1 -0
- package/dist/contract/rules/fields-valid.js +39 -0
- package/dist/contract/rules/fields-valid.js.map +1 -0
- package/dist/contract/rules/params-match.d.ts +3 -0
- package/dist/contract/rules/params-match.d.ts.map +1 -0
- package/dist/contract/rules/params-match.js +40 -0
- package/dist/contract/rules/params-match.js.map +1 -0
- package/dist/contract/rules/permissions-consistent.d.ts +3 -0
- package/dist/contract/rules/permissions-consistent.d.ts.map +1 -0
- package/dist/contract/rules/permissions-consistent.js +52 -0
- package/dist/contract/rules/permissions-consistent.js.map +1 -0
- package/dist/contract/types.d.ts +66 -0
- package/dist/contract/types.d.ts.map +1 -0
- package/dist/contract/types.js +2 -0
- package/dist/contract/types.js.map +1 -0
- package/dist/data/data-sources.d.ts +26 -0
- package/dist/data/data-sources.d.ts.map +1 -0
- package/dist/data/data-sources.js +218 -0
- package/dist/data/data-sources.js.map +1 -0
- package/dist/data/fetch.d.ts +29 -0
- package/dist/data/fetch.d.ts.map +1 -0
- package/dist/data/fetch.js +63 -0
- package/dist/data/fetch.js.map +1 -0
- package/dist/data/offline.d.ts +29 -0
- package/dist/data/offline.d.ts.map +1 -0
- package/dist/data/offline.js +48 -0
- package/dist/data/offline.js.map +1 -0
- package/dist/data/realtime.d.ts +21 -0
- package/dist/data/realtime.d.ts.map +1 -0
- package/dist/data/realtime.js +40 -0
- package/dist/data/realtime.js.map +1 -0
- package/dist/derive/evaluator.d.ts +19 -0
- package/dist/derive/evaluator.d.ts.map +1 -0
- package/dist/derive/evaluator.js +59 -0
- package/dist/derive/evaluator.js.map +1 -0
- package/dist/derive/topo-sort.d.ts +10 -0
- package/dist/derive/topo-sort.d.ts.map +1 -0
- package/dist/derive/topo-sort.js +36 -0
- package/dist/derive/topo-sort.js.map +1 -0
- package/dist/design/animation/cascade-types.d.ts +23 -0
- package/dist/design/animation/cascade-types.d.ts.map +1 -0
- package/dist/design/animation/cascade-types.js +2 -0
- package/dist/design/animation/cascade-types.js.map +1 -0
- package/dist/design/animation/cascade.d.ts +4 -0
- package/dist/design/animation/cascade.d.ts.map +1 -0
- package/dist/design/animation/cascade.js +73 -0
- package/dist/design/animation/cascade.js.map +1 -0
- package/dist/design/animation/css-keyframes.d.ts +8 -0
- package/dist/design/animation/css-keyframes.d.ts.map +1 -0
- package/dist/design/animation/css-keyframes.js +72 -0
- package/dist/design/animation/css-keyframes.js.map +1 -0
- package/dist/design/animation/index.d.ts +14 -0
- package/dist/design/animation/index.d.ts.map +1 -0
- package/dist/design/animation/index.js +9 -0
- package/dist/design/animation/index.js.map +1 -0
- package/dist/design/animation/keyframes-builder.d.ts +40 -0
- package/dist/design/animation/keyframes-builder.d.ts.map +1 -0
- package/dist/design/animation/keyframes-builder.js +140 -0
- package/dist/design/animation/keyframes-builder.js.map +1 -0
- package/dist/design/animation/reanimated-spec.d.ts +16 -0
- package/dist/design/animation/reanimated-spec.d.ts.map +1 -0
- package/dist/design/animation/reanimated-spec.js +162 -0
- package/dist/design/animation/reanimated-spec.js.map +1 -0
- package/dist/design/animation/reduced-motion.d.ts +3 -0
- package/dist/design/animation/reduced-motion.d.ts.map +1 -0
- package/dist/design/animation/reduced-motion.js +54 -0
- package/dist/design/animation/reduced-motion.js.map +1 -0
- package/dist/design/animation/resolver.d.ts +3 -0
- package/dist/design/animation/resolver.d.ts.map +1 -0
- package/dist/design/animation/resolver.js +86 -0
- package/dist/design/animation/resolver.js.map +1 -0
- package/dist/design/animation/types.d.ts +109 -0
- package/dist/design/animation/types.d.ts.map +1 -0
- package/dist/design/animation/types.js +7 -0
- package/dist/design/animation/types.js.map +1 -0
- package/dist/design/animation/validator.d.ts +44 -0
- package/dist/design/animation/validator.d.ts.map +1 -0
- package/dist/design/animation/validator.js +169 -0
- package/dist/design/animation/validator.js.map +1 -0
- package/dist/design/background/blobs/catalog.d.ts +10 -0
- package/dist/design/background/blobs/catalog.d.ts.map +1 -0
- package/dist/design/background/blobs/catalog.js +63 -0
- package/dist/design/background/blobs/catalog.js.map +1 -0
- package/dist/design/background/blobs/index.d.ts +4 -0
- package/dist/design/background/blobs/index.d.ts.map +1 -0
- package/dist/design/background/blobs/index.js +5 -0
- package/dist/design/background/blobs/index.js.map +1 -0
- package/dist/design/background/blobs/resolver.d.ts +16 -0
- package/dist/design/background/blobs/resolver.d.ts.map +1 -0
- package/dist/design/background/blobs/resolver.js +301 -0
- package/dist/design/background/blobs/resolver.js.map +1 -0
- package/dist/design/background/blobs/types.d.ts +112 -0
- package/dist/design/background/blobs/types.d.ts.map +1 -0
- package/dist/design/background/blobs/types.js +27 -0
- package/dist/design/background/blobs/types.js.map +1 -0
- package/dist/design/background/grain.d.ts +3 -0
- package/dist/design/background/grain.d.ts.map +1 -0
- package/dist/design/background/grain.js +23 -0
- package/dist/design/background/grain.js.map +1 -0
- package/dist/design/background/index.d.ts +8 -0
- package/dist/design/background/index.d.ts.map +1 -0
- package/dist/design/background/index.js +8 -0
- package/dist/design/background/index.js.map +1 -0
- package/dist/design/background/layers.d.ts +3 -0
- package/dist/design/background/layers.d.ts.map +1 -0
- package/dist/design/background/layers.js +23 -0
- package/dist/design/background/layers.js.map +1 -0
- package/dist/design/background/patterns/chevron.d.ts +8 -0
- package/dist/design/background/patterns/chevron.d.ts.map +1 -0
- package/dist/design/background/patterns/chevron.js +10 -0
- package/dist/design/background/patterns/chevron.js.map +1 -0
- package/dist/design/background/patterns/crosshatch.d.ts +8 -0
- package/dist/design/background/patterns/crosshatch.d.ts.map +1 -0
- package/dist/design/background/patterns/crosshatch.js +12 -0
- package/dist/design/background/patterns/crosshatch.js.map +1 -0
- package/dist/design/background/patterns/diagonal.d.ts +8 -0
- package/dist/design/background/patterns/diagonal.d.ts.map +1 -0
- package/dist/design/background/patterns/diagonal.js +7 -0
- package/dist/design/background/patterns/diagonal.js.map +1 -0
- package/dist/design/background/patterns/dots.d.ts +7 -0
- package/dist/design/background/patterns/dots.d.ts.map +1 -0
- package/dist/design/background/patterns/dots.js +9 -0
- package/dist/design/background/patterns/dots.js.map +1 -0
- package/dist/design/background/patterns/grid.d.ts +7 -0
- package/dist/design/background/patterns/grid.d.ts.map +1 -0
- package/dist/design/background/patterns/grid.js +8 -0
- package/dist/design/background/patterns/grid.js.map +1 -0
- package/dist/design/background/patterns/index.d.ts +8 -0
- package/dist/design/background/patterns/index.d.ts.map +1 -0
- package/dist/design/background/patterns/index.js +8 -0
- package/dist/design/background/patterns/index.js.map +1 -0
- package/dist/design/background/patterns/iso.d.ts +7 -0
- package/dist/design/background/patterns/iso.d.ts.map +1 -0
- package/dist/design/background/patterns/iso.js +11 -0
- package/dist/design/background/patterns/iso.js.map +1 -0
- package/dist/design/background/patterns/resolver.d.ts +3 -0
- package/dist/design/background/patterns/resolver.d.ts.map +1 -0
- package/dist/design/background/patterns/resolver.js +56 -0
- package/dist/design/background/patterns/resolver.js.map +1 -0
- package/dist/design/background/primitives/gradient.d.ts +3 -0
- package/dist/design/background/primitives/gradient.d.ts.map +1 -0
- package/dist/design/background/primitives/gradient.js +68 -0
- package/dist/design/background/primitives/gradient.js.map +1 -0
- package/dist/design/background/primitives/image.d.ts +3 -0
- package/dist/design/background/primitives/image.d.ts.map +1 -0
- package/dist/design/background/primitives/image.js +12 -0
- package/dist/design/background/primitives/image.js.map +1 -0
- package/dist/design/background/primitives/index.d.ts +4 -0
- package/dist/design/background/primitives/index.d.ts.map +1 -0
- package/dist/design/background/primitives/index.js +4 -0
- package/dist/design/background/primitives/index.js.map +1 -0
- package/dist/design/background/primitives/solid.d.ts +8 -0
- package/dist/design/background/primitives/solid.d.ts.map +1 -0
- package/dist/design/background/primitives/solid.js +15 -0
- package/dist/design/background/primitives/solid.js.map +1 -0
- package/dist/design/background/resolver.d.ts +3 -0
- package/dist/design/background/resolver.d.ts.map +1 -0
- package/dist/design/background/resolver.js +20 -0
- package/dist/design/background/resolver.js.map +1 -0
- package/dist/design/background/sanitizer.d.ts +2 -0
- package/dist/design/background/sanitizer.d.ts.map +1 -0
- package/dist/design/background/sanitizer.js +54 -0
- package/dist/design/background/sanitizer.js.map +1 -0
- package/dist/design/deep-tokens.d.ts +125 -0
- package/dist/design/deep-tokens.d.ts.map +1 -0
- package/dist/design/deep-tokens.js +115 -0
- package/dist/design/deep-tokens.js.map +1 -0
- package/dist/design/dna.d.ts +7 -0
- package/dist/design/dna.d.ts.map +1 -0
- package/dist/design/dna.js +210 -0
- package/dist/design/dna.js.map +1 -0
- package/dist/design/identity/color.d.ts +8 -0
- package/dist/design/identity/color.d.ts.map +1 -0
- package/dist/design/identity/color.js +64 -0
- package/dist/design/identity/color.js.map +1 -0
- package/dist/design/identity/elevation.d.ts +8 -0
- package/dist/design/identity/elevation.d.ts.map +1 -0
- package/dist/design/identity/elevation.js +31 -0
- package/dist/design/identity/elevation.js.map +1 -0
- package/dist/design/identity/index.d.ts +12 -0
- package/dist/design/identity/index.d.ts.map +1 -0
- package/dist/design/identity/index.js +14 -0
- package/dist/design/identity/index.js.map +1 -0
- package/dist/design/identity/shape.d.ts +4 -0
- package/dist/design/identity/shape.d.ts.map +1 -0
- package/dist/design/identity/shape.js +23 -0
- package/dist/design/identity/shape.js.map +1 -0
- package/dist/design/identity/surface.d.ts +18 -0
- package/dist/design/identity/surface.d.ts.map +1 -0
- package/dist/design/identity/surface.js +125 -0
- package/dist/design/identity/surface.js.map +1 -0
- package/dist/design/identity/types.d.ts +280 -0
- package/dist/design/identity/types.d.ts.map +1 -0
- package/dist/design/identity/types.js +21 -0
- package/dist/design/identity/types.js.map +1 -0
- package/dist/design/identity/typography.d.ts +7 -0
- package/dist/design/identity/typography.d.ts.map +1 -0
- package/dist/design/identity/typography.js +42 -0
- package/dist/design/identity/typography.js.map +1 -0
- package/dist/design/identity-resolver.d.ts +15 -0
- package/dist/design/identity-resolver.d.ts.map +1 -0
- package/dist/design/identity-resolver.js +52 -0
- package/dist/design/identity-resolver.js.map +1 -0
- package/dist/design/oklch.d.ts +10 -0
- package/dist/design/oklch.d.ts.map +1 -0
- package/dist/design/oklch.js +61 -0
- package/dist/design/oklch.js.map +1 -0
- package/dist/design/palette.d.ts +26 -0
- package/dist/design/palette.d.ts.map +1 -0
- package/dist/design/palette.js +73 -0
- package/dist/design/palette.js.map +1 -0
- package/dist/design/presets.d.ts +18 -0
- package/dist/design/presets.d.ts.map +1 -0
- package/dist/design/presets.js +2 -0
- package/dist/design/presets.js.map +1 -0
- package/dist/design/recipes/animations.d.ts +11 -0
- package/dist/design/recipes/animations.d.ts.map +1 -0
- package/dist/design/recipes/animations.js +180 -0
- package/dist/design/recipes/animations.js.map +1 -0
- package/dist/design/recipes/backgrounds.d.ts +3 -0
- package/dist/design/recipes/backgrounds.d.ts.map +1 -0
- package/dist/design/recipes/backgrounds.js +152 -0
- package/dist/design/recipes/backgrounds.js.map +1 -0
- package/dist/design/recipes/index.d.ts +2 -0
- package/dist/design/recipes/index.d.ts.map +1 -0
- package/dist/design/recipes/index.js +2 -0
- package/dist/design/recipes/index.js.map +1 -0
- package/dist/design/surface-to-css.d.ts +57 -0
- package/dist/design/surface-to-css.d.ts.map +1 -0
- package/dist/design/surface-to-css.js +130 -0
- package/dist/design/surface-to-css.js.map +1 -0
- package/dist/design/surface-to-rn.d.ts +57 -0
- package/dist/design/surface-to-rn.d.ts.map +1 -0
- package/dist/design/surface-to-rn.js +133 -0
- package/dist/design/surface-to-rn.js.map +1 -0
- package/dist/design/tokens.d.ts +67 -0
- package/dist/design/tokens.d.ts.map +1 -0
- package/dist/design/tokens.js +36 -0
- package/dist/design/tokens.js.map +1 -0
- package/dist/device/context.d.ts +13 -0
- package/dist/device/context.d.ts.map +1 -0
- package/dist/device/context.js +2 -0
- package/dist/device/context.js.map +1 -0
- package/dist/editor-session/clone.d.ts +4 -0
- package/dist/editor-session/clone.d.ts.map +1 -0
- package/dist/editor-session/clone.js +55 -0
- package/dist/editor-session/clone.js.map +1 -0
- package/dist/editor-session/engine.d.ts +21 -0
- package/dist/editor-session/engine.d.ts.map +1 -0
- package/dist/editor-session/engine.js +375 -0
- package/dist/editor-session/engine.js.map +1 -0
- package/dist/editor-session/persistence.d.ts +8 -0
- package/dist/editor-session/persistence.d.ts.map +1 -0
- package/dist/editor-session/persistence.js +89 -0
- package/dist/editor-session/persistence.js.map +1 -0
- package/dist/editor-session/types.d.ts +90 -0
- package/dist/editor-session/types.d.ts.map +1 -0
- package/dist/editor-session/types.js +2 -0
- package/dist/editor-session/types.js.map +1 -0
- package/dist/editor-session/validators.d.ts +5 -0
- package/dist/editor-session/validators.d.ts.map +1 -0
- package/dist/editor-session/validators.js +106 -0
- package/dist/editor-session/validators.js.map +1 -0
- package/dist/elements/composer.d.ts +109 -0
- package/dist/elements/composer.d.ts.map +1 -0
- package/dist/elements/composer.js +129 -0
- package/dist/elements/composer.js.map +1 -0
- package/dist/export/csv.d.ts +3 -0
- package/dist/export/csv.d.ts.map +1 -0
- package/dist/export/csv.js +16 -0
- package/dist/export/csv.js.map +1 -0
- package/dist/export/download.d.ts +2 -0
- package/dist/export/download.d.ts.map +1 -0
- package/dist/export/download.js +13 -0
- package/dist/export/download.js.map +1 -0
- package/dist/export/format.d.ts +3 -0
- package/dist/export/format.d.ts.map +1 -0
- package/dist/export/format.js +43 -0
- package/dist/export/format.js.map +1 -0
- package/dist/export/types.d.ts +22 -0
- package/dist/export/types.d.ts.map +1 -0
- package/dist/export/types.js +2 -0
- package/dist/export/types.js.map +1 -0
- package/dist/expressions/deep-resolve.d.ts +4 -0
- package/dist/expressions/deep-resolve.d.ts.map +1 -0
- package/dist/expressions/deep-resolve.js +20 -0
- package/dist/expressions/deep-resolve.js.map +1 -0
- package/dist/expressions/handlers/array.d.ts +19 -0
- package/dist/expressions/handlers/array.d.ts.map +1 -0
- package/dist/expressions/handlers/array.js +161 -0
- package/dist/expressions/handlers/array.js.map +1 -0
- package/dist/expressions/handlers/auth.d.ts +13 -0
- package/dist/expressions/handlers/auth.d.ts.map +1 -0
- package/dist/expressions/handlers/auth.js +90 -0
- package/dist/expressions/handlers/auth.js.map +1 -0
- package/dist/expressions/handlers/bind.d.ts +22 -0
- package/dist/expressions/handlers/bind.d.ts.map +1 -0
- package/dist/expressions/handlers/bind.js +57 -0
- package/dist/expressions/handlers/bind.js.map +1 -0
- package/dist/expressions/handlers/breakpoint.d.ts +10 -0
- package/dist/expressions/handlers/breakpoint.d.ts.map +1 -0
- package/dist/expressions/handlers/breakpoint.js +39 -0
- package/dist/expressions/handlers/breakpoint.js.map +1 -0
- package/dist/expressions/handlers/computed.d.ts +3 -0
- package/dist/expressions/handlers/computed.d.ts.map +1 -0
- package/dist/expressions/handlers/computed.js +20 -0
- package/dist/expressions/handlers/computed.js.map +1 -0
- package/dist/expressions/handlers/cond.d.ts +3 -0
- package/dist/expressions/handlers/cond.d.ts.map +1 -0
- package/dist/expressions/handlers/cond.js +57 -0
- package/dist/expressions/handlers/cond.js.map +1 -0
- package/dist/expressions/handlers/date.d.ts +14 -0
- package/dist/expressions/handlers/date.d.ts.map +1 -0
- package/dist/expressions/handlers/date.js +95 -0
- package/dist/expressions/handlers/date.js.map +1 -0
- package/dist/expressions/handlers/format.d.ts +19 -0
- package/dist/expressions/handlers/format.d.ts.map +1 -0
- package/dist/expressions/handlers/format.js +93 -0
- package/dist/expressions/handlers/format.js.map +1 -0
- package/dist/expressions/handlers/group.d.ts +25 -0
- package/dist/expressions/handlers/group.d.ts.map +1 -0
- package/dist/expressions/handlers/group.js +85 -0
- package/dist/expressions/handlers/group.js.map +1 -0
- package/dist/expressions/handlers/i18n.d.ts +8 -0
- package/dist/expressions/handlers/i18n.d.ts.map +1 -0
- package/dist/expressions/handlers/i18n.js +35 -0
- package/dist/expressions/handlers/i18n.js.map +1 -0
- package/dist/expressions/handlers/index.d.ts +4 -0
- package/dist/expressions/handlers/index.d.ts.map +1 -0
- package/dist/expressions/handlers/index.js +52 -0
- package/dist/expressions/handlers/index.js.map +1 -0
- package/dist/expressions/handlers/let.d.ts +4 -0
- package/dist/expressions/handlers/let.d.ts.map +1 -0
- package/dist/expressions/handlers/let.js +36 -0
- package/dist/expressions/handlers/let.js.map +1 -0
- package/dist/expressions/handlers/logic.d.ts +27 -0
- package/dist/expressions/handlers/logic.d.ts.map +1 -0
- package/dist/expressions/handlers/logic.js +52 -0
- package/dist/expressions/handlers/logic.js.map +1 -0
- package/dist/expressions/handlers/math.d.ts +13 -0
- package/dist/expressions/handlers/math.d.ts.map +1 -0
- package/dist/expressions/handlers/math.js +58 -0
- package/dist/expressions/handlers/math.js.map +1 -0
- package/dist/expressions/handlers/platform.d.ts +13 -0
- package/dist/expressions/handlers/platform.d.ts.map +1 -0
- package/dist/expressions/handlers/platform.js +37 -0
- package/dist/expressions/handlers/platform.js.map +1 -0
- package/dist/expressions/handlers/prop.d.ts +10 -0
- package/dist/expressions/handlers/prop.d.ts.map +1 -0
- package/dist/expressions/handlers/prop.js +18 -0
- package/dist/expressions/handlers/prop.js.map +1 -0
- package/dist/expressions/handlers/selection.d.ts +10 -0
- package/dist/expressions/handlers/selection.d.ts.map +1 -0
- package/dist/expressions/handlers/selection.js +33 -0
- package/dist/expressions/handlers/selection.js.map +1 -0
- package/dist/expressions/handlers/state.d.ts +3 -0
- package/dist/expressions/handlers/state.d.ts.map +1 -0
- package/dist/expressions/handlers/state.js +8 -0
- package/dist/expressions/handlers/state.js.map +1 -0
- package/dist/expressions/handlers/switch.d.ts +15 -0
- package/dist/expressions/handlers/switch.d.ts.map +1 -0
- package/dist/expressions/handlers/switch.js +31 -0
- package/dist/expressions/handlers/switch.js.map +1 -0
- package/dist/expressions/handlers/template.d.ts +3 -0
- package/dist/expressions/handlers/template.d.ts.map +1 -0
- package/dist/expressions/handlers/template.js +23 -0
- package/dist/expressions/handlers/template.js.map +1 -0
- package/dist/expressions/handlers/token.d.ts +3 -0
- package/dist/expressions/handlers/token.d.ts.map +1 -0
- package/dist/expressions/handlers/token.js +40 -0
- package/dist/expressions/handlers/token.js.map +1 -0
- package/dist/expressions/handlers/unique-id.d.ts +11 -0
- package/dist/expressions/handlers/unique-id.d.ts.map +1 -0
- package/dist/expressions/handlers/unique-id.js +52 -0
- package/dist/expressions/handlers/unique-id.js.map +1 -0
- package/dist/expressions/registry.d.ts +8 -0
- package/dist/expressions/registry.d.ts.map +1 -0
- package/dist/expressions/registry.js +35 -0
- package/dist/expressions/registry.js.map +1 -0
- package/dist/expressions/resolver.d.ts +15 -0
- package/dist/expressions/resolver.d.ts.map +1 -0
- package/dist/expressions/resolver.js +56 -0
- package/dist/expressions/resolver.js.map +1 -0
- package/dist/factory.d.ts +56 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +265 -0
- package/dist/factory.js.map +1 -0
- package/dist/fetch/framework-fetch.d.ts +12 -0
- package/dist/fetch/framework-fetch.d.ts.map +1 -0
- package/dist/fetch/framework-fetch.js +36 -0
- package/dist/fetch/framework-fetch.js.map +1 -0
- package/dist/fetch/interceptors/auth.d.ts +19 -0
- package/dist/fetch/interceptors/auth.d.ts.map +1 -0
- package/dist/fetch/interceptors/auth.js +66 -0
- package/dist/fetch/interceptors/auth.js.map +1 -0
- package/dist/fetch/interceptors/logging.d.ts +8 -0
- package/dist/fetch/interceptors/logging.d.ts.map +1 -0
- package/dist/fetch/interceptors/logging.js +40 -0
- package/dist/fetch/interceptors/logging.js.map +1 -0
- package/dist/fetch/interceptors/retry.d.ts +20 -0
- package/dist/fetch/interceptors/retry.d.ts.map +1 -0
- package/dist/fetch/interceptors/retry.js +34 -0
- package/dist/fetch/interceptors/retry.js.map +1 -0
- package/dist/fetch/interceptors/timeout.d.ts +7 -0
- package/dist/fetch/interceptors/timeout.d.ts.map +1 -0
- package/dist/fetch/interceptors/timeout.js +20 -0
- package/dist/fetch/interceptors/timeout.js.map +1 -0
- package/dist/fetch/types.d.ts +30 -0
- package/dist/fetch/types.d.ts.map +1 -0
- package/dist/fetch/types.js +2 -0
- package/dist/fetch/types.js.map +1 -0
- package/dist/forms/engine.d.ts +3 -0
- package/dist/forms/engine.d.ts.map +1 -0
- package/dist/forms/engine.js +167 -0
- package/dist/forms/engine.js.map +1 -0
- package/dist/forms/types.d.ts +30 -0
- package/dist/forms/types.d.ts.map +1 -0
- package/dist/forms/types.js +2 -0
- package/dist/forms/types.js.map +1 -0
- package/dist/i18n/engine.d.ts +15 -0
- package/dist/i18n/engine.d.ts.map +1 -0
- package/dist/i18n/engine.js +61 -0
- package/dist/i18n/engine.js.map +1 -0
- package/dist/index.d.ts +161 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +106 -0
- package/dist/index.js.map +1 -0
- package/dist/interactions/data-operations.d.ts +51 -0
- package/dist/interactions/data-operations.d.ts.map +1 -0
- package/dist/interactions/data-operations.js +64 -0
- package/dist/interactions/data-operations.js.map +1 -0
- package/dist/interactions/drag-drop.d.ts +14 -0
- package/dist/interactions/drag-drop.d.ts.map +1 -0
- package/dist/interactions/drag-drop.js +35 -0
- package/dist/interactions/drag-drop.js.map +1 -0
- package/dist/interactions/export.d.ts +24 -0
- package/dist/interactions/export.d.ts.map +1 -0
- package/dist/interactions/export.js +44 -0
- package/dist/interactions/export.js.map +1 -0
- package/dist/interactions/history.d.ts +16 -0
- package/dist/interactions/history.d.ts.map +1 -0
- package/dist/interactions/history.js +52 -0
- package/dist/interactions/history.js.map +1 -0
- package/dist/motion/engine.d.ts +47 -0
- package/dist/motion/engine.d.ts.map +1 -0
- package/dist/motion/engine.js +81 -0
- package/dist/motion/engine.js.map +1 -0
- package/dist/navigation/router.d.ts +36 -0
- package/dist/navigation/router.d.ts.map +1 -0
- package/dist/navigation/router.js +51 -0
- package/dist/navigation/router.js.map +1 -0
- package/dist/permissions/evaluator.d.ts +15 -0
- package/dist/permissions/evaluator.d.ts.map +1 -0
- package/dist/permissions/evaluator.js +32 -0
- package/dist/permissions/evaluator.js.map +1 -0
- package/dist/plugins/loader.d.ts +30 -0
- package/dist/plugins/loader.d.ts.map +1 -0
- package/dist/plugins/loader.js +87 -0
- package/dist/plugins/loader.js.map +1 -0
- package/dist/reference/generator.d.ts +25 -0
- package/dist/reference/generator.d.ts.map +1 -0
- package/dist/reference/generator.js +808 -0
- package/dist/reference/generator.js.map +1 -0
- package/dist/renderer/deps.d.ts +24 -0
- package/dist/renderer/deps.d.ts.map +1 -0
- package/dist/renderer/deps.js +150 -0
- package/dist/renderer/deps.js.map +1 -0
- package/dist/renderer/engine.d.ts +15 -0
- package/dist/renderer/engine.d.ts.map +1 -0
- package/dist/renderer/engine.js +1097 -0
- package/dist/renderer/engine.js.map +1 -0
- package/dist/renderer/lazy-paths.d.ts +14 -0
- package/dist/renderer/lazy-paths.d.ts.map +1 -0
- package/dist/renderer/lazy-paths.js +63 -0
- package/dist/renderer/lazy-paths.js.map +1 -0
- package/dist/renderer/prop-schemas.d.ts +17 -0
- package/dist/renderer/prop-schemas.d.ts.map +1 -0
- package/dist/renderer/prop-schemas.js +81 -0
- package/dist/renderer/prop-schemas.js.map +1 -0
- package/dist/renderer/registry.d.ts +10 -0
- package/dist/renderer/registry.d.ts.map +1 -0
- package/dist/renderer/registry.js +22 -0
- package/dist/renderer/registry.js.map +1 -0
- package/dist/renderer/variants.d.ts +54 -0
- package/dist/renderer/variants.d.ts.map +1 -0
- package/dist/renderer/variants.js +105 -0
- package/dist/renderer/variants.js.map +1 -0
- package/dist/runtime/mount-spec-runtime.d.ts +59 -0
- package/dist/runtime/mount-spec-runtime.d.ts.map +1 -0
- package/dist/runtime/mount-spec-runtime.js +73 -0
- package/dist/runtime/mount-spec-runtime.js.map +1 -0
- package/dist/security/api-spec-validator.d.ts +13 -0
- package/dist/security/api-spec-validator.d.ts.map +1 -0
- package/dist/security/api-spec-validator.js +347 -0
- package/dist/security/api-spec-validator.js.map +1 -0
- package/dist/security/app-spec-validator.d.ts +4 -0
- package/dist/security/app-spec-validator.d.ts.map +1 -0
- package/dist/security/app-spec-validator.js +200 -0
- package/dist/security/app-spec-validator.js.map +1 -0
- package/dist/security/identifier-guard.d.ts +3 -0
- package/dist/security/identifier-guard.d.ts.map +1 -0
- package/dist/security/identifier-guard.js +15 -0
- package/dist/security/identifier-guard.js.map +1 -0
- package/dist/security/identity-token-validator.d.ts +3 -0
- package/dist/security/identity-token-validator.d.ts.map +1 -0
- package/dist/security/identity-token-validator.js +121 -0
- package/dist/security/identity-token-validator.js.map +1 -0
- package/dist/security/index.d.ts +14 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +8 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/protection-registry.d.ts +24 -0
- package/dist/security/protection-registry.d.ts.map +1 -0
- package/dist/security/protection-registry.js +35 -0
- package/dist/security/protection-registry.js.map +1 -0
- package/dist/security/rate-limiter.d.ts +23 -0
- package/dist/security/rate-limiter.d.ts.map +1 -0
- package/dist/security/rate-limiter.js +44 -0
- package/dist/security/rate-limiter.js.map +1 -0
- package/dist/security/spec-signing.d.ts +23 -0
- package/dist/security/spec-signing.d.ts.map +1 -0
- package/dist/security/spec-signing.js +62 -0
- package/dist/security/spec-signing.js.map +1 -0
- package/dist/security/spec-validator.d.ts +60 -0
- package/dist/security/spec-validator.d.ts.map +1 -0
- package/dist/security/spec-validator.js +903 -0
- package/dist/security/spec-validator.js.map +1 -0
- package/dist/security/state-protection.d.ts +23 -0
- package/dist/security/state-protection.d.ts.map +1 -0
- package/dist/security/state-protection.js +49 -0
- package/dist/security/state-protection.js.map +1 -0
- package/dist/security/url-whitelist.d.ts +14 -0
- package/dist/security/url-whitelist.d.ts.map +1 -0
- package/dist/security/url-whitelist.js +37 -0
- package/dist/security/url-whitelist.js.map +1 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +8 -0
- package/dist/server.js.map +1 -0
- package/dist/spec-engine/app-elements.d.ts +5 -0
- package/dist/spec-engine/app-elements.d.ts.map +1 -0
- package/dist/spec-engine/app-elements.js +36 -0
- package/dist/spec-engine/app-elements.js.map +1 -0
- package/dist/spec-engine/app-manifest.d.ts +3 -0
- package/dist/spec-engine/app-manifest.d.ts.map +1 -0
- package/dist/spec-engine/app-manifest.js +132 -0
- package/dist/spec-engine/app-manifest.js.map +1 -0
- package/dist/spec-engine/elements.d.ts +8 -0
- package/dist/spec-engine/elements.d.ts.map +1 -0
- package/dist/spec-engine/elements.js +18 -0
- package/dist/spec-engine/elements.js.map +1 -0
- package/dist/spec-engine/engine.d.ts +3 -0
- package/dist/spec-engine/engine.d.ts.map +1 -0
- package/dist/spec-engine/engine.js +76 -0
- package/dist/spec-engine/engine.js.map +1 -0
- package/dist/spec-engine/handlers/api-handler.d.ts +5 -0
- package/dist/spec-engine/handlers/api-handler.d.ts.map +1 -0
- package/dist/spec-engine/handlers/api-handler.js +111 -0
- package/dist/spec-engine/handlers/api-handler.js.map +1 -0
- package/dist/spec-engine/handlers/app-handler.d.ts +4 -0
- package/dist/spec-engine/handlers/app-handler.d.ts.map +1 -0
- package/dist/spec-engine/handlers/app-handler.js +41 -0
- package/dist/spec-engine/handlers/app-handler.js.map +1 -0
- package/dist/spec-engine/handlers/index.d.ts +8 -0
- package/dist/spec-engine/handlers/index.d.ts.map +1 -0
- package/dist/spec-engine/handlers/index.js +17 -0
- package/dist/spec-engine/handlers/index.js.map +1 -0
- package/dist/spec-engine/handlers/screen-handler.d.ts +4 -0
- package/dist/spec-engine/handlers/screen-handler.d.ts.map +1 -0
- package/dist/spec-engine/handlers/screen-handler.js +34 -0
- package/dist/spec-engine/handlers/screen-handler.js.map +1 -0
- package/dist/spec-engine/manifest.d.ts +11 -0
- package/dist/spec-engine/manifest.d.ts.map +1 -0
- package/dist/spec-engine/manifest.js +76 -0
- package/dist/spec-engine/manifest.js.map +1 -0
- package/dist/spec-engine/types.d.ts +61 -0
- package/dist/spec-engine/types.d.ts.map +1 -0
- package/dist/spec-engine/types.js +2 -0
- package/dist/spec-engine/types.js.map +1 -0
- package/dist/spec-stores/file.d.ts +12 -0
- package/dist/spec-stores/file.d.ts.map +1 -0
- package/dist/spec-stores/file.js +32 -0
- package/dist/spec-stores/file.js.map +1 -0
- package/dist/spec-stores/memory-versioned.d.ts +22 -0
- package/dist/spec-stores/memory-versioned.d.ts.map +1 -0
- package/dist/spec-stores/memory-versioned.js +125 -0
- package/dist/spec-stores/memory-versioned.js.map +1 -0
- package/dist/spec-stores/memory.d.ts +10 -0
- package/dist/spec-stores/memory.d.ts.map +1 -0
- package/dist/spec-stores/memory.js +31 -0
- package/dist/spec-stores/memory.js.map +1 -0
- package/dist/spec-stores/sqlserver-versioned.d.ts +44 -0
- package/dist/spec-stores/sqlserver-versioned.d.ts.map +1 -0
- package/dist/spec-stores/sqlserver-versioned.js +253 -0
- package/dist/spec-stores/sqlserver-versioned.js.map +1 -0
- package/dist/spec-stores/sqlserver.d.ts +27 -0
- package/dist/spec-stores/sqlserver.d.ts.map +1 -0
- package/dist/spec-stores/sqlserver.js +78 -0
- package/dist/spec-stores/sqlserver.js.map +1 -0
- package/dist/spec-stores/supabase-versioned.d.ts +38 -0
- package/dist/spec-stores/supabase-versioned.d.ts.map +1 -0
- package/dist/spec-stores/supabase-versioned.js +197 -0
- package/dist/spec-stores/supabase-versioned.js.map +1 -0
- package/dist/spec-stores/supabase.d.ts +18 -0
- package/dist/spec-stores/supabase.d.ts.map +1 -0
- package/dist/spec-stores/supabase.js +68 -0
- package/dist/spec-stores/supabase.js.map +1 -0
- package/dist/state/reserved-paths.d.ts +41 -0
- package/dist/state/reserved-paths.d.ts.map +1 -0
- package/dist/state/reserved-paths.js +40 -0
- package/dist/state/reserved-paths.js.map +1 -0
- package/dist/state/store.d.ts +18 -0
- package/dist/state/store.d.ts.map +1 -0
- package/dist/state/store.js +114 -0
- package/dist/state/store.js.map +1 -0
- package/dist/storage/supabase.d.ts +37 -0
- package/dist/storage/supabase.d.ts.map +1 -0
- package/dist/storage/supabase.js +22 -0
- package/dist/storage/supabase.js.map +1 -0
- package/dist/storage/types.d.ts +22 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +2 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/storage/upload-action.d.ts +22 -0
- package/dist/storage/upload-action.d.ts.map +1 -0
- package/dist/storage/upload-action.js +117 -0
- package/dist/storage/upload-action.js.map +1 -0
- package/dist/storage/url.d.ts +17 -0
- package/dist/storage/url.d.ts.map +1 -0
- package/dist/storage/url.js +48 -0
- package/dist/storage/url.js.map +1 -0
- package/dist/streaming/compiler.d.ts +26 -0
- package/dist/streaming/compiler.d.ts.map +1 -0
- package/dist/streaming/compiler.js +54 -0
- package/dist/streaming/compiler.js.map +1 -0
- package/dist/streaming/patch.d.ts +19 -0
- package/dist/streaming/patch.d.ts.map +1 -0
- package/dist/streaming/patch.js +139 -0
- package/dist/streaming/patch.js.map +1 -0
- package/dist/types.d.ts +443 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/util/to-array.d.ts +2 -0
- package/dist/util/to-array.d.ts.map +1 -0
- package/dist/util/to-array.js +14 -0
- package/dist/util/to-array.js.map +1 -0
- package/dist/utils/levenshtein.d.ts +3 -0
- package/dist/utils/levenshtein.d.ts.map +1 -0
- package/dist/utils/levenshtein.js +29 -0
- package/dist/utils/levenshtein.js.map +1 -0
- package/dist/validation/validators.d.ts +20 -0
- package/dist/validation/validators.d.ts.map +1 -0
- package/dist/validation/validators.js +138 -0
- package/dist/validation/validators.js.map +1 -0
- package/dist/versioning/compute-patches.d.ts +7 -0
- package/dist/versioning/compute-patches.d.ts.map +1 -0
- package/dist/versioning/compute-patches.js +62 -0
- package/dist/versioning/compute-patches.js.map +1 -0
- package/dist/versioning/index.d.ts +12 -0
- package/dist/versioning/index.d.ts.map +1 -0
- package/dist/versioning/index.js +7 -0
- package/dist/versioning/index.js.map +1 -0
- package/dist/versioning/promote-gate.d.ts +12 -0
- package/dist/versioning/promote-gate.d.ts.map +1 -0
- package/dist/versioning/promote-gate.js +166 -0
- package/dist/versioning/promote-gate.js.map +1 -0
- package/dist/versioning/rollback.d.ts +26 -0
- package/dist/versioning/rollback.d.ts.map +1 -0
- package/dist/versioning/rollback.js +67 -0
- package/dist/versioning/rollback.js.map +1 -0
- package/dist/versioning/structural-diff.d.ts +8 -0
- package/dist/versioning/structural-diff.d.ts.map +1 -0
- package/dist/versioning/structural-diff.js +287 -0
- package/dist/versioning/structural-diff.js.map +1 -0
- package/dist/versioning/types.d.ts +90 -0
- package/dist/versioning/types.d.ts.map +1 -0
- package/dist/versioning/types.js +2 -0
- package/dist/versioning/types.js.map +1 -0
- package/dist/visibility/evaluator.d.ts +12 -0
- package/dist/visibility/evaluator.d.ts.map +1 -0
- package/dist/visibility/evaluator.js +76 -0
- package/dist/visibility/evaluator.js.map +1 -0
- package/dist/watchers/engine.d.ts +19 -0
- package/dist/watchers/engine.d.ts.map +1 -0
- package/dist/watchers/engine.js +24 -0
- package/dist/watchers/engine.js.map +1 -0
- package/docs/consumer/README.md +44 -0
- package/docs/consumer/WHERE-TO-LOOK.md +94 -0
- package/docs/consumer/ai-context-api.md +195 -0
- package/docs/consumer/ai-context-custom-elements.md +249 -0
- package/docs/consumer/ai-context-patterns.md +254 -0
- package/docs/consumer/ai-context-primitives.md +839 -0
- package/docs/consumer/ai-context-runtime-semantics.md +1075 -0
- package/docs/consumer/ai-context.md +1374 -0
- package/docs/consumer/reference-doc.md +3201 -0
- package/docs/llms.txt +17 -0
- package/docs/wiki/compiled/README.md +33 -0
- package/docs/wiki/compiled/_gaps.md +52 -0
- package/docs/wiki/compiled/_index.md +354 -0
- package/docs/wiki/compiled/_inventory.md +2986 -0
- package/docs/wiki/compiled/_lint.md +66 -0
- package/docs/wiki/compiled/_unmapped.md +3 -0
- package/docs/wiki/compiled/action-apply-preset.md +63 -0
- package/docs/wiki/compiled/action-copy-clipboard.md +45 -0
- package/docs/wiki/compiled/action-drawer.md +50 -0
- package/docs/wiki/compiled/action-export.md +66 -0
- package/docs/wiki/compiled/action-fetch.md +87 -0
- package/docs/wiki/compiled/action-form-control.md +55 -0
- package/docs/wiki/compiled/action-login.md +71 -0
- package/docs/wiki/compiled/action-modal.md +61 -0
- package/docs/wiki/compiled/action-navigate.md +72 -0
- package/docs/wiki/compiled/action-open-url.md +38 -0
- package/docs/wiki/compiled/action-refresh-data-source.md +51 -0
- package/docs/wiki/compiled/action-selection.md +55 -0
- package/docs/wiki/compiled/action-set-locale.md +44 -0
- package/docs/wiki/compiled/action-set-state.md +61 -0
- package/docs/wiki/compiled/action-show-notification.md +65 -0
- package/docs/wiki/compiled/action-submit-form.md +69 -0
- package/docs/wiki/compiled/action-toggle-theme.md +50 -0
- package/docs/wiki/compiled/action-update-tokens.md +63 -0
- package/docs/wiki/compiled/action-upload-file.md +87 -0
- package/docs/wiki/compiled/antipattern-action-chain-no-stop.md +49 -0
- package/docs/wiki/compiled/antipattern-all-filter-wildcard.md +59 -0
- package/docs/wiki/compiled/antipattern-auth-domains-port.md +47 -0
- package/docs/wiki/compiled/antipattern-background-blobs-prop.md +62 -0
- package/docs/wiki/compiled/antipattern-background-css-token.md +53 -0
- package/docs/wiki/compiled/antipattern-checkbox-toggle-value.md +42 -0
- package/docs/wiki/compiled/antipattern-crud-id-collision.md +66 -0
- package/docs/wiki/compiled/antipattern-derive-server-pagination.md +58 -0
- package/docs/wiki/compiled/antipattern-element-variant-top-level.md +57 -0
- package/docs/wiki/compiled/antipattern-event-expression.md +55 -0
- package/docs/wiki/compiled/antipattern-input-type-name.md +43 -0
- package/docs/wiki/compiled/antipattern-mix-fetch-and-datasources.md +48 -0
- package/docs/wiki/compiled/antipattern-modal-drawer-visible.md +57 -0
- package/docs/wiki/compiled/antipattern-motion-entrance-token.md +53 -0
- package/docs/wiki/compiled/antipattern-overflow-hidden-with-shadow.md +50 -0
- package/docs/wiki/compiled/antipattern-required-if-equals.md +59 -0
- package/docs/wiki/compiled/antipattern-row-literal.md +70 -0
- package/docs/wiki/compiled/antipattern-row-template-capture.md +42 -0
- package/docs/wiki/compiled/antipattern-store-save-bypass.md +72 -0
- package/docs/wiki/compiled/antipattern-style-block-duplication.md +56 -0
- package/docs/wiki/compiled/antipattern-submit-form-in-tx-confirm.md +56 -0
- package/docs/wiki/compiled/cli-app-spec.md +53 -0
- package/docs/wiki/compiled/cli-config.md +46 -0
- package/docs/wiki/compiled/cli-contract.md +73 -0
- package/docs/wiki/compiled/cli-delete.md +37 -0
- package/docs/wiki/compiled/cli-docs.md +44 -0
- package/docs/wiki/compiled/cli-elements.md +39 -0
- package/docs/wiki/compiled/cli-existing-spec-edit-loop.md +24 -0
- package/docs/wiki/compiled/cli-history.md +38 -0
- package/docs/wiki/compiled/cli-lint.md +77 -0
- package/docs/wiki/compiled/cli-manifest.md +43 -0
- package/docs/wiki/compiled/cli-overview.md +77 -0
- package/docs/wiki/compiled/cli-patch.md +85 -0
- package/docs/wiki/compiled/cli-programmatic-api.md +73 -0
- package/docs/wiki/compiled/cli-pull.md +39 -0
- package/docs/wiki/compiled/cli-push.md +91 -0
- package/docs/wiki/compiled/cli-tokens.md +38 -0
- package/docs/wiki/compiled/cli-toon.md +44 -0
- package/docs/wiki/compiled/cli-validate.md +52 -0
- package/docs/wiki/compiled/cli-versioning-author.md +58 -0
- package/docs/wiki/compiled/concept-action-chains.md +65 -0
- package/docs/wiki/compiled/concept-action-middleware.md +55 -0
- package/docs/wiki/compiled/concept-animation-build-css.md +43 -0
- package/docs/wiki/compiled/concept-animation-build-rn.md +44 -0
- package/docs/wiki/compiled/concept-animation-caps.md +38 -0
- package/docs/wiki/compiled/concept-animation-cascade.md +89 -0
- package/docs/wiki/compiled/concept-animation-null-semantics.md +55 -0
- package/docs/wiki/compiled/concept-animation-recipes.md +67 -0
- package/docs/wiki/compiled/concept-animation-triggers.md +54 -0
- package/docs/wiki/compiled/concept-animations-engine.md +78 -0
- package/docs/wiki/compiled/concept-api-audit.md +67 -0
- package/docs/wiki/compiled/concept-api-auth.md +67 -0
- package/docs/wiki/compiled/concept-api-catalogs.md +44 -0
- package/docs/wiki/compiled/concept-api-crud-endpoint.md +90 -0
- package/docs/wiki/compiled/concept-api-endpoint-properties.md +40 -0
- package/docs/wiki/compiled/concept-api-endpoints-overview.md +57 -0
- package/docs/wiki/compiled/concept-api-handler-endpoint.md +56 -0
- package/docs/wiki/compiled/concept-api-login-body-contract.md +47 -0
- package/docs/wiki/compiled/concept-api-param-properties.md +44 -0
- package/docs/wiki/compiled/concept-api-public-endpoint.md +33 -0
- package/docs/wiki/compiled/concept-api-query-endpoint.md +53 -0
- package/docs/wiki/compiled/concept-api-scope-filter.md +59 -0
- package/docs/wiki/compiled/concept-api-spec.md +70 -0
- package/docs/wiki/compiled/concept-app-auto-state-paths.md +59 -0
- package/docs/wiki/compiled/concept-app-spec.md +89 -0
- package/docs/wiki/compiled/concept-auth-config.md +71 -0
- package/docs/wiki/compiled/concept-auth-domains.md +55 -0
- package/docs/wiki/compiled/concept-auth-security.md +58 -0
- package/docs/wiki/compiled/concept-auth-state-paths.md +48 -0
- package/docs/wiki/compiled/concept-auto-dark-mode.md +55 -0
- package/docs/wiki/compiled/concept-background-caps.md +42 -0
- package/docs/wiki/compiled/concept-background-layer-kinds.md +80 -0
- package/docs/wiki/compiled/concept-background-recipes.md +53 -0
- package/docs/wiki/compiled/concept-background-stack.md +55 -0
- package/docs/wiki/compiled/concept-blob-layer.md +92 -0
- package/docs/wiki/compiled/concept-blob-motion.md +58 -0
- package/docs/wiki/compiled/concept-blob-shapes-catalog.md +45 -0
- package/docs/wiki/compiled/concept-cli-tokens-inspect.md +38 -0
- package/docs/wiki/compiled/concept-component-variants.md +89 -0
- package/docs/wiki/compiled/concept-cross-tab-sync.md +35 -0
- package/docs/wiki/compiled/concept-css-vs-motion.md +46 -0
- package/docs/wiki/compiled/concept-custom-detection-pattern.md +49 -0
- package/docs/wiki/compiled/concept-custom-element-action-props.md +81 -0
- package/docs/wiki/compiled/concept-custom-element-black-box.md +56 -0
- package/docs/wiki/compiled/concept-custom-element-cache.md +39 -0
- package/docs/wiki/compiled/concept-custom-element-error-boundary.md +33 -0
- package/docs/wiki/compiled/concept-custom-element-prop-cascade.md +60 -0
- package/docs/wiki/compiled/concept-custom-element-repeat.md +53 -0
- package/docs/wiki/compiled/concept-custom-elements.md +71 -0
- package/docs/wiki/compiled/concept-custom-jwt-provider.md +63 -0
- package/docs/wiki/compiled/concept-customize-action.md +33 -0
- package/docs/wiki/compiled/concept-customize-cli-lint-rule.md +30 -0
- package/docs/wiki/compiled/concept-customize-expression-handler.md +30 -0
- package/docs/wiki/compiled/concept-customize-plugin.md +43 -0
- package/docs/wiki/compiled/concept-customize-server-middleware.md +36 -0
- package/docs/wiki/compiled/concept-customize-spec-store.md +42 -0
- package/docs/wiki/compiled/concept-customize-validator-rule.md +35 -0
- package/docs/wiki/compiled/concept-customize-versioned-store.md +37 -0
- package/docs/wiki/compiled/concept-data-sources-lifecycle.md +61 -0
- package/docs/wiki/compiled/concept-data-sources.md +96 -0
- package/docs/wiki/compiled/concept-debugging-runtime-pointers.md +45 -0
- package/docs/wiki/compiled/concept-derive-datasources-mount-order.md +72 -0
- package/docs/wiki/compiled/concept-derive.md +67 -0
- package/docs/wiki/compiled/concept-dna-seeds.md +79 -0
- package/docs/wiki/compiled/concept-editor-commit.md +37 -0
- package/docs/wiki/compiled/concept-editor-save.md +18 -0
- package/docs/wiki/compiled/concept-editor-sessions.md +23 -0
- package/docs/wiki/compiled/concept-element-children-slot.md +54 -0
- package/docs/wiki/compiled/concept-element-definition.md +60 -0
- package/docs/wiki/compiled/concept-element-key.md +56 -0
- package/docs/wiki/compiled/concept-element-prop-definition.md +54 -0
- package/docs/wiki/compiled/concept-element-properties.md +81 -0
- package/docs/wiki/compiled/concept-element-render-node.md +62 -0
- package/docs/wiki/compiled/concept-element-variants.md +67 -0
- package/docs/wiki/compiled/concept-environment-store.md +46 -0
- package/docs/wiki/compiled/concept-export-action.md +56 -0
- package/docs/wiki/compiled/concept-expression-contexts.md +56 -0
- package/docs/wiki/compiled/concept-expression-timing.md +56 -0
- package/docs/wiki/compiled/concept-fetch-interceptors.md +49 -0
- package/docs/wiki/compiled/concept-file-upload-overview.md +55 -0
- package/docs/wiki/compiled/concept-fire-and-forget.md +45 -0
- package/docs/wiki/compiled/concept-font-loading.md +52 -0
- package/docs/wiki/compiled/concept-form-state-paths.md +55 -0
- package/docs/wiki/compiled/concept-forms.md +89 -0
- package/docs/wiki/compiled/concept-identity-accent-application.md +48 -0
- package/docs/wiki/compiled/concept-identity-animations.md +48 -0
- package/docs/wiki/compiled/concept-identity-border-elevation-override.md +56 -0
- package/docs/wiki/compiled/concept-identity-color-scheme.md +74 -0
- package/docs/wiki/compiled/concept-identity-color-weight.md +50 -0
- package/docs/wiki/compiled/concept-identity-glass-rn.md +46 -0
- package/docs/wiki/compiled/concept-identity-gradients.md +56 -0
- package/docs/wiki/compiled/concept-identity-heading-color.md +40 -0
- package/docs/wiki/compiled/concept-identity-helpers.md +61 -0
- package/docs/wiki/compiled/concept-identity-icons.md +46 -0
- package/docs/wiki/compiled/concept-identity-images.md +36 -0
- package/docs/wiki/compiled/concept-identity-label-style.md +36 -0
- package/docs/wiki/compiled/concept-identity-overview.md +72 -0
- package/docs/wiki/compiled/concept-identity-radius-pattern.md +39 -0
- package/docs/wiki/compiled/concept-identity-surface.md +62 -0
- package/docs/wiki/compiled/concept-identity-text-decoration.md +48 -0
- package/docs/wiki/compiled/concept-identity-typography-hierarchy.md +40 -0
- package/docs/wiki/compiled/concept-initial-actions.md +82 -0
- package/docs/wiki/compiled/concept-interactive-states.md +72 -0
- package/docs/wiki/compiled/concept-keyframe-snapshot.md +52 -0
- package/docs/wiki/compiled/concept-layer-background.md +80 -0
- package/docs/wiki/compiled/concept-motion-field.md +77 -0
- package/docs/wiki/compiled/concept-mount-spec-runtime.md +47 -0
- package/docs/wiki/compiled/concept-mythik-renderer.md +76 -0
- package/docs/wiki/compiled/concept-navigation-dirty-guard.md +23 -0
- package/docs/wiki/compiled/concept-navigation.md +75 -0
- package/docs/wiki/compiled/concept-package-layout.md +35 -0
- package/docs/wiki/compiled/concept-path-references.md +53 -0
- package/docs/wiki/compiled/concept-pattern-primitives.md +54 -0
- package/docs/wiki/compiled/concept-preset-dropdown-pattern.md +52 -0
- package/docs/wiki/compiled/concept-presets.md +49 -0
- package/docs/wiki/compiled/concept-primitive-prop-schemas.md +53 -0
- package/docs/wiki/compiled/concept-primitives-overview.md +49 -0
- package/docs/wiki/compiled/concept-promote-gate.md +47 -0
- package/docs/wiki/compiled/concept-prop-cascade.md +76 -0
- package/docs/wiki/compiled/concept-public-package-names.md +33 -0
- package/docs/wiki/compiled/concept-query-envelope.md +55 -0
- package/docs/wiki/compiled/concept-reduced-motion.md +38 -0
- package/docs/wiki/compiled/concept-register-presets.md +44 -0
- package/docs/wiki/compiled/concept-render-error-visibility.md +57 -0
- package/docs/wiki/compiled/concept-repeat-grouped.md +72 -0
- package/docs/wiki/compiled/concept-repeat-selection.md +61 -0
- package/docs/wiki/compiled/concept-repeat.md +81 -0
- package/docs/wiki/compiled/concept-role-access.md +61 -0
- package/docs/wiki/compiled/concept-rollback.md +36 -0
- package/docs/wiki/compiled/concept-rules-catalog.md +49 -0
- package/docs/wiki/compiled/concept-screen-definition.md +51 -0
- package/docs/wiki/compiled/concept-screen-outlet.md +46 -0
- package/docs/wiki/compiled/concept-session-persistence.md +41 -0
- package/docs/wiki/compiled/concept-shape-animations.md +66 -0
- package/docs/wiki/compiled/concept-skeleton-auto.md +58 -0
- package/docs/wiki/compiled/concept-skeleton-manual.md +47 -0
- package/docs/wiki/compiled/concept-source-of-truth-references.md +37 -0
- package/docs/wiki/compiled/concept-source-reading-misleading.md +37 -0
- package/docs/wiki/compiled/concept-spatial-map-editor.md +22 -0
- package/docs/wiki/compiled/concept-spatial-map-zones.md +18 -0
- package/docs/wiki/compiled/concept-spec-engine.md +47 -0
- package/docs/wiki/compiled/concept-spec-store-interface.md +59 -0
- package/docs/wiki/compiled/concept-spec-store-layering.md +51 -0
- package/docs/wiki/compiled/concept-spec-stores-catalog.md +66 -0
- package/docs/wiki/compiled/concept-spec-structure.md +94 -0
- package/docs/wiki/compiled/concept-spec-types.md +76 -0
- package/docs/wiki/compiled/concept-state-change-animation.md +73 -0
- package/docs/wiki/compiled/concept-state-policies.md +44 -0
- package/docs/wiki/compiled/concept-state-protection.md +51 -0
- package/docs/wiki/compiled/concept-storage-adapter.md +69 -0
- package/docs/wiki/compiled/concept-storage-custom-names.md +69 -0
- package/docs/wiki/compiled/concept-storage-evolution.md +32 -0
- package/docs/wiki/compiled/concept-storage-idempotency.md +35 -0
- package/docs/wiki/compiled/concept-storage-overview.md +81 -0
- package/docs/wiki/compiled/concept-storage-postgres-jsonb.md +53 -0
- package/docs/wiki/compiled/concept-storage-postgres-triggers.md +60 -0
- package/docs/wiki/compiled/concept-storage-table-environments.md +40 -0
- package/docs/wiki/compiled/concept-storage-table-screens.md +44 -0
- package/docs/wiki/compiled/concept-storage-table-versions.md +50 -0
- package/docs/wiki/compiled/concept-storage-verification.md +57 -0
- package/docs/wiki/compiled/concept-template-children-marker.md +56 -0
- package/docs/wiki/compiled/concept-template-interpolation.md +60 -0
- package/docs/wiki/compiled/concept-templates-vs-variants.md +80 -0
- package/docs/wiki/compiled/concept-templates.md +93 -0
- package/docs/wiki/compiled/concept-token-categories.md +58 -0
- package/docs/wiki/compiled/concept-token-system.md +61 -0
- package/docs/wiki/compiled/concept-transaction-phase-timing.md +47 -0
- package/docs/wiki/compiled/concept-transaction-rollback.md +38 -0
- package/docs/wiki/compiled/concept-transaction-snapshot.md +37 -0
- package/docs/wiki/compiled/concept-transactions.md +72 -0
- package/docs/wiki/compiled/concept-validation-checks.md +63 -0
- package/docs/wiki/compiled/concept-validators-catalog.md +76 -0
- package/docs/wiki/compiled/concept-versioned-store.md +65 -0
- package/docs/wiki/compiled/concept-versioning-snapshots-patches.md +61 -0
- package/docs/wiki/compiled/concept-visibility.md +55 -0
- package/docs/wiki/compiled/concept-web-only-recipes.md +37 -0
- package/docs/wiki/compiled/concept-where-to-look.md +78 -0
- package/docs/wiki/compiled/expression-and-or-not.md +68 -0
- package/docs/wiki/compiled/expression-array.md +111 -0
- package/docs/wiki/compiled/expression-auth.md +83 -0
- package/docs/wiki/compiled/expression-binditem.md +52 -0
- package/docs/wiki/compiled/expression-bindstate.md +63 -0
- package/docs/wiki/compiled/expression-breakpoint.md +71 -0
- package/docs/wiki/compiled/expression-computed.md +52 -0
- package/docs/wiki/compiled/expression-cond.md +78 -0
- package/docs/wiki/compiled/expression-date.md +66 -0
- package/docs/wiki/compiled/expression-format.md +79 -0
- package/docs/wiki/compiled/expression-group.md +84 -0
- package/docs/wiki/compiled/expression-i18n.md +53 -0
- package/docs/wiki/compiled/expression-item-index.md +72 -0
- package/docs/wiki/compiled/expression-let-ref.md +87 -0
- package/docs/wiki/compiled/expression-math.md +67 -0
- package/docs/wiki/compiled/expression-platform.md +68 -0
- package/docs/wiki/compiled/expression-prop.md +89 -0
- package/docs/wiki/compiled/expression-selection.md +58 -0
- package/docs/wiki/compiled/expression-state.md +70 -0
- package/docs/wiki/compiled/expression-switch.md +68 -0
- package/docs/wiki/compiled/expression-template.md +71 -0
- package/docs/wiki/compiled/expression-token.md +67 -0
- package/docs/wiki/compiled/path-app-screens.md +55 -0
- package/docs/wiki/compiled/path-data-source-paths.md +44 -0
- package/docs/wiki/compiled/path-forms.md +38 -0
- package/docs/wiki/compiled/path-login.md +57 -0
- package/docs/wiki/compiled/path-navigation.md +62 -0
- package/docs/wiki/compiled/path-presets-available.md +44 -0
- package/docs/wiki/compiled/path-tokens.md +56 -0
- package/docs/wiki/compiled/path-tx-result-error.md +56 -0
- package/docs/wiki/compiled/path-ui-device.md +53 -0
- package/docs/wiki/compiled/path-ui-loading-error.md +40 -0
- package/docs/wiki/compiled/path-ui-modals-drawers.md +46 -0
- package/docs/wiki/compiled/path-ui-selected-row.md +92 -0
- package/docs/wiki/compiled/path-uploads.md +32 -0
- package/docs/wiki/compiled/pattern-cross-screen-data-flow.md +83 -0
- package/docs/wiki/compiled/pattern-fetch-vs-datasources.md +63 -0
- package/docs/wiki/compiled/pattern-file-upload-auto.md +61 -0
- package/docs/wiki/compiled/pattern-file-upload-manual.md +59 -0
- package/docs/wiki/compiled/pattern-form-validation-cross-field.md +58 -0
- package/docs/wiki/compiled/pattern-form-validation.md +58 -0
- package/docs/wiki/compiled/pattern-fullstack-coherence.md +49 -0
- package/docs/wiki/compiled/pattern-git-vs-db-versioning.md +55 -0
- package/docs/wiki/compiled/pattern-identity-aware-spec.md +70 -0
- package/docs/wiki/compiled/pattern-loading-content-empty.md +66 -0
- package/docs/wiki/compiled/pattern-login-body-template.md +47 -0
- package/docs/wiki/compiled/pattern-login-screen.md +83 -0
- package/docs/wiki/compiled/pattern-push-vs-patch.md +54 -0
- package/docs/wiki/compiled/pattern-reusable-components.md +67 -0
- package/docs/wiki/compiled/pattern-tx-create.md +76 -0
- package/docs/wiki/compiled/pattern-tx-delete.md +50 -0
- package/docs/wiki/compiled/pattern-tx-toggle.md +45 -0
- package/docs/wiki/compiled/pattern-tx-update.md +68 -0
- package/docs/wiki/compiled/primitive-accordion.md +46 -0
- package/docs/wiki/compiled/primitive-area-chart.md +35 -0
- package/docs/wiki/compiled/primitive-audio-player.md +27 -0
- package/docs/wiki/compiled/primitive-bar-chart.md +34 -0
- package/docs/wiki/compiled/primitive-box.md +58 -0
- package/docs/wiki/compiled/primitive-button.md +55 -0
- package/docs/wiki/compiled/primitive-camera.md +39 -0
- package/docs/wiki/compiled/primitive-checkbox.md +62 -0
- package/docs/wiki/compiled/primitive-divider.md +31 -0
- package/docs/wiki/compiled/primitive-drawer.md +44 -0
- package/docs/wiki/compiled/primitive-file-upload.md +65 -0
- package/docs/wiki/compiled/primitive-grid.md +48 -0
- package/docs/wiki/compiled/primitive-icon.md +106 -0
- package/docs/wiki/compiled/primitive-image.md +49 -0
- package/docs/wiki/compiled/primitive-input.md +81 -0
- package/docs/wiki/compiled/primitive-kanban-board.md +30 -0
- package/docs/wiki/compiled/primitive-line-chart.md +33 -0
- package/docs/wiki/compiled/primitive-list.md +34 -0
- package/docs/wiki/compiled/primitive-modal.md +51 -0
- package/docs/wiki/compiled/primitive-pie-chart.md +32 -0
- package/docs/wiki/compiled/primitive-screen-outlet.md +42 -0
- package/docs/wiki/compiled/primitive-screen.md +43 -0
- package/docs/wiki/compiled/primitive-scroll.md +33 -0
- package/docs/wiki/compiled/primitive-select.md +76 -0
- package/docs/wiki/compiled/primitive-signature.md +40 -0
- package/docs/wiki/compiled/primitive-skeleton.md +48 -0
- package/docs/wiki/compiled/primitive-slider.md +47 -0
- package/docs/wiki/compiled/primitive-spacer.md +33 -0
- package/docs/wiki/compiled/primitive-spatial-map.md +26 -0
- package/docs/wiki/compiled/primitive-stack.md +46 -0
- package/docs/wiki/compiled/primitive-table.md +127 -0
- package/docs/wiki/compiled/primitive-tabs.md +52 -0
- package/docs/wiki/compiled/primitive-text.md +59 -0
- package/docs/wiki/compiled/primitive-textarea.md +44 -0
- package/docs/wiki/compiled/primitive-toast-container.md +44 -0
- package/docs/wiki/compiled/primitive-toggle.md +46 -0
- package/docs/wiki/compiled/primitive-touchable.md +40 -0
- package/docs/wiki/compiled/primitive-wizard.md +43 -0
- package/package.json +58 -8
|
@@ -0,0 +1,3201 @@
|
|
|
1
|
+
# Mythik — Reference Document
|
|
2
|
+
|
|
3
|
+
> The AI reads this to produce correct JSON specs.
|
|
4
|
+
> Every element type, expression, action, and pattern available is documented here with examples.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## How to Use This Document
|
|
9
|
+
|
|
10
|
+
When asked to create or modify UI, produce a JSON **Spec** following the structure and rules below.
|
|
11
|
+
|
|
12
|
+
## CLI — Spec Modification via Mythik CLI
|
|
13
|
+
|
|
14
|
+
Use the `mythik` CLI to read and modify specs stored in Supabase (or other stores).
|
|
15
|
+
|
|
16
|
+
For AI agents, locate the bundled Mythik documentation before generating or changing specs:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
mythik docs path
|
|
20
|
+
mythik docs copy ./mythik-docs
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The `mythik` npm package ships `docs/llms.txt`, canonical `docs/consumer/*`, and the compiled `docs/wiki/compiled/*` wiki so the framework contract is available after install.
|
|
24
|
+
|
|
25
|
+
### Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Initialize configuration
|
|
29
|
+
mythik init --store supabase --url <url> --key $MYTHIK_API_KEY
|
|
30
|
+
|
|
31
|
+
# See the structural tree of a screen (colorized output)
|
|
32
|
+
mythik manifest <screen>
|
|
33
|
+
|
|
34
|
+
# Get specific element details
|
|
35
|
+
mythik elements <screen> <id1,id2,...>
|
|
36
|
+
|
|
37
|
+
# Apply RFC 6902 patches
|
|
38
|
+
mythik patch <screen> --from-file patch.json
|
|
39
|
+
|
|
40
|
+
# Export a full screen spec for backup/review
|
|
41
|
+
mythik pull <screen>
|
|
42
|
+
|
|
43
|
+
# Create or intentionally replace a full screen spec from stdin
|
|
44
|
+
mythik push <screen>
|
|
45
|
+
|
|
46
|
+
# Validate a screen spec
|
|
47
|
+
mythik validate <screen>
|
|
48
|
+
|
|
49
|
+
# Delete a screen (preview without --confirm)
|
|
50
|
+
mythik delete <screen> --confirm
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
All commands accept `--json` for machine-parseable output and `--store`, `--url`, `--key` flags (or read from `.mythikrc`).
|
|
54
|
+
|
|
55
|
+
### Configuration — Never pass keys inline
|
|
56
|
+
|
|
57
|
+
**Always** use `.mythikrc` + environment variables. Never pass API keys as CLI flags — they appear in terminal history, chat logs, and process lists.
|
|
58
|
+
|
|
59
|
+
```json
|
|
60
|
+
// .mythikrc
|
|
61
|
+
{
|
|
62
|
+
"store": "supabase",
|
|
63
|
+
"supabase": {
|
|
64
|
+
"url": "https://xxx.supabase.co",
|
|
65
|
+
"apiKey": "$MYTHIK_API_KEY"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Set the env var once per session: `export MYTHIK_API_KEY="your-key"`. The CLI resolves `$MYTHIK_API_KEY` at runtime. Add `.mythikrc` to `.gitignore`.
|
|
71
|
+
|
|
72
|
+
### Patch Input: File vs Stdin
|
|
73
|
+
|
|
74
|
+
**Preferred shell-safe path** - write patch JSON to a file:
|
|
75
|
+
```bash
|
|
76
|
+
mythik patch task-manager --from-file patch.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`--from-file <path>` wins over ambient non-TTY stdin. Use `--from-file -` or pipe without `--from-file` when stdin is intentional:
|
|
80
|
+
```bash
|
|
81
|
+
cat patch.json | mythik patch task-manager
|
|
82
|
+
mythik patch task-manager --from-file -
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Inline JSON still works for tiny patches, but `--from-file` is preferred for anything containing `$state`, `$template`, `$auth`, or shell-sensitive quoting.
|
|
86
|
+
|
|
87
|
+
**Array insertion:** `"op": "add"` with a numeric index (e.g., `"/elements/form/children/2"`) **inserts before** that index — it does NOT replace. Use `/-` to append at the end. Use `"op": "replace"` if you want to overwrite.
|
|
88
|
+
|
|
89
|
+
### TOON Format (Token-Efficient Output)
|
|
90
|
+
|
|
91
|
+
Use `--toon` for token-efficient output on `elements` and `patch` commands:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
mythik elements task-manager btn,nav --toon
|
|
95
|
+
mythik patch task-manager --from-file patch.toon --toon
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
TOON reduces output by ~40% vs JSON. Lossless roundtrip. Based on `@toon-format/toon` v2.
|
|
99
|
+
|
|
100
|
+
**Patch input autodetects format** — JSON (starts with `[` or `{`) or TOON (everything else). You can send patches in either format:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# JSON input (as before)
|
|
104
|
+
mythik patch task-manager --from-file patch.json
|
|
105
|
+
|
|
106
|
+
# TOON input (autodetected)
|
|
107
|
+
mythik patch task-manager --from-file patch.toon
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Workflow: Manifest → Elements → Patch
|
|
111
|
+
|
|
112
|
+
1. **`mythik manifest <screen>`** — see the structure, decide what to modify
|
|
113
|
+
2. **`mythik elements <screen> <ids>`** — inspect specific elements you need to change
|
|
114
|
+
3. **`mythik patch <screen> --from-file patch.json`** - apply surgical changes
|
|
115
|
+
4. **`mythik manifest <screen>`** — verify the result
|
|
116
|
+
|
|
117
|
+
This is the required default loop for AI agents editing an existing spec. Do not begin a small change by pulling the full spec and rewriting it. `manifest` gives the structural map, `elements` gives the exact JSON for the target nodes, and `patch --from-file` keeps the write validated, versionable, and reviewable. Use `pull` for backup/migration/review and `push` for new specs or deliberate full replacement only.
|
|
118
|
+
|
|
119
|
+
### Full Spec Lifecycle
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
Create: mythik push <id> < spec.json
|
|
123
|
+
Read: mythik manifest <id> / mythik elements <id> ids / mythik pull <id>
|
|
124
|
+
Modify: mythik patch <id> --from-file patch.json
|
|
125
|
+
Validate: mythik validate <id>
|
|
126
|
+
Delete: mythik delete <id> --confirm
|
|
127
|
+
Backup: mythik pull <id> > backup.json
|
|
128
|
+
Restore: mythik push <id> < backup.json
|
|
129
|
+
Migrate: mythik pull <id> --store A | mythik push <id> --store B
|
|
130
|
+
Tokens: mythik tokens --dna '{"primary":"#0D9488"}' --json
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Push — Create New Screens
|
|
134
|
+
|
|
135
|
+
Write a JSON spec and push via stdin. No seed scripts, no `.ts` files:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# From a file
|
|
139
|
+
mythik push login < login-spec.json
|
|
140
|
+
|
|
141
|
+
# Inline with heredoc
|
|
142
|
+
mythik push login <<'EOF'
|
|
143
|
+
{ "root": "page", "elements": { "page": { "type": "box", "children": ["title"] }, "title": { "type": "text", "props": { "content": "Login" } } } }
|
|
144
|
+
EOF
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**New screens** save even with validation errors (fix with `mythik patch`). **Existing screens** reject invalid specs unless `--force` is used. Validation errors include **suggested fix patches** for auto-fixable issues (typos, orphan children).
|
|
148
|
+
|
|
149
|
+
### Delete — With Safety Gate
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Preview (no deletion)
|
|
153
|
+
mythik delete task-manager
|
|
154
|
+
# → Screen "task-manager" (107 elements). Use --confirm to delete.
|
|
155
|
+
|
|
156
|
+
# Delete + backup in one command
|
|
157
|
+
mythik delete task-manager --confirm > backup.json
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### AppSpec — App-Level Configuration
|
|
161
|
+
|
|
162
|
+
The same CLI commands work on AppSpec documents (`type: "app"`). Detection is automatic.
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
# View app structure
|
|
166
|
+
mythik manifest app-demo
|
|
167
|
+
|
|
168
|
+
# Inspect specific sections (dot-notation)
|
|
169
|
+
mythik elements app-demo tokens.colors
|
|
170
|
+
mythik elements app-demo screens.task-manager
|
|
171
|
+
mythik elements app-demo navigation.auth
|
|
172
|
+
|
|
173
|
+
# Inspect layout elements (simple ID)
|
|
174
|
+
mythik elements app-demo sidebar,nav-item
|
|
175
|
+
|
|
176
|
+
# Mix both
|
|
177
|
+
mythik elements app-demo sidebar,tokens.colors,screens.task-manager
|
|
178
|
+
|
|
179
|
+
# Patch tokens
|
|
180
|
+
mythik patch app-demo '[{"op":"replace","path":"/tokens/colors/primary","value":"#2563EB"}]'
|
|
181
|
+
|
|
182
|
+
# Patch layout elements
|
|
183
|
+
mythik patch app-demo '[{"op":"replace","path":"/layout/elements/sidebar/props/style/width","value":280}]'
|
|
184
|
+
|
|
185
|
+
# Add a new screen
|
|
186
|
+
mythik patch app-demo '[{"op":"add","path":"/screens/reports","value":{"label":"Reports","icon":"chart-bar"}}]'
|
|
187
|
+
|
|
188
|
+
# Validate
|
|
189
|
+
mythik validate app-demo
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Key differences from screen specs:**
|
|
193
|
+
- Elements live under `/layout/elements/` (not `/elements/`)
|
|
194
|
+
- Dot-notation in `elements` command for inspecting tokens, screens, navigation, translations, sharedState
|
|
195
|
+
- Patch paths use full JSON Pointer: `/tokens/...`, `/screens/...`, `/layout/elements/...`
|
|
196
|
+
- Validation includes cross-reference checks (screen existence, roleAccess consistency)
|
|
197
|
+
- Translation key mismatches are warnings, not errors
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Spec Structure (Flat Tree)
|
|
202
|
+
|
|
203
|
+
Every screen is a JSON Spec with a `root` ID, a flat `elements` map, and optional `initialActions`:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"root": "main-layout",
|
|
208
|
+
"initialActions": [
|
|
209
|
+
{ "action": "fetch", "params": { "url": "https://api.example.com/data", "target": "/items" } }
|
|
210
|
+
],
|
|
211
|
+
"elements": {
|
|
212
|
+
"main-layout": {
|
|
213
|
+
"type": "stack",
|
|
214
|
+
"props": { "direction": "vertical" },
|
|
215
|
+
"children": ["header", "content"]
|
|
216
|
+
},
|
|
217
|
+
"header": {
|
|
218
|
+
"type": "text",
|
|
219
|
+
"props": { "content": { "$template": "${/patient/name}" }, "variant": "heading" }
|
|
220
|
+
},
|
|
221
|
+
"content": {
|
|
222
|
+
"type": "box",
|
|
223
|
+
"props": { "style": { "padding": { "$token": "spacing.unit", "multiply": 3 } } },
|
|
224
|
+
"children": ["field1", "field2"]
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Top-Level Spec Properties
|
|
231
|
+
|
|
232
|
+
| Property | Type | Purpose |
|
|
233
|
+
|----------|------|---------|
|
|
234
|
+
| `root` | string | ID of the root element |
|
|
235
|
+
| `elements` | object | Flat map of element ID → element definition |
|
|
236
|
+
| `initialActions` | ActionBinding[] | Actions to execute when the spec mounts (e.g., fetch data) |
|
|
237
|
+
|
|
238
|
+
### Element Properties
|
|
239
|
+
|
|
240
|
+
| Property | Type | Purpose |
|
|
241
|
+
|----------|------|---------|
|
|
242
|
+
| `type` | string | Primitive or custom element name |
|
|
243
|
+
| `props` | object | Component props (can contain expressions) |
|
|
244
|
+
| `children` | string[] | IDs of child elements |
|
|
245
|
+
| `style` | object | CSS styles (can contain expressions) |
|
|
246
|
+
| `visible` | boolean or condition | Show/hide condition |
|
|
247
|
+
| `permission` | object | Role-based access: `{ visible, editable, readonly }` |
|
|
248
|
+
| `repeat` | object | Iterate over array: `{ statePath, key }` or `{ source: expression, key }` |
|
|
249
|
+
| `on` | object | Event handlers: `{ press: action, change: action }` |
|
|
250
|
+
| `hover` | object | Style overrides when pointer enters (rendered via Motion) |
|
|
251
|
+
| `active` | object | Style overrides when element is pressed (rendered via Motion) |
|
|
252
|
+
| `focus` | object | Style overrides when element has keyboard focus (rendered via Motion) |
|
|
253
|
+
| `transition` | object | Animation config: `{ duration, ease, delay, type, stiffness, damping }` |
|
|
254
|
+
| `motion` | object | Mount/exit animations: `{ initial, animate, exit, transition }` |
|
|
255
|
+
| `key` | expression | Dynamic React key — forces remount when value changes. Supports `$template`, `$state`. Use to re-trigger mount animations on state change |
|
|
256
|
+
| `skeleton` | `false` | Opt out of auto-skeleton for this element |
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## Expression Types
|
|
261
|
+
|
|
262
|
+
Expressions are special JSON objects that resolve to dynamic values at runtime.
|
|
263
|
+
|
|
264
|
+
### `$state` — Read from state
|
|
265
|
+
```json
|
|
266
|
+
{ "$state": "/user/name" }
|
|
267
|
+
```
|
|
268
|
+
Reads the value at the given JSON Pointer path from application state.
|
|
269
|
+
|
|
270
|
+
### `$bindState` — Two-way binding
|
|
271
|
+
```json
|
|
272
|
+
{ "$bindState": "/form/email" }
|
|
273
|
+
```
|
|
274
|
+
Reads AND writes. When the user changes the input, the state updates automatically.
|
|
275
|
+
|
|
276
|
+
### `$token` — Design system token
|
|
277
|
+
```json
|
|
278
|
+
{ "$token": "colors.primary" }
|
|
279
|
+
{ "$token": "spacing.unit", "multiply": 3 }
|
|
280
|
+
```
|
|
281
|
+
Resolves a value from the design system. Use dot notation. `multiply` scales numeric tokens.
|
|
282
|
+
|
|
283
|
+
**Dynamic theme:** `$token` automatically reads `/preferences/theme` from state. When the theme is "dark" and dark mode tokens are configured, `$token` resolves with the dark mode overrides. No manual switching needed — use `toggleTheme` action and tokens resolve correctly.
|
|
284
|
+
|
|
285
|
+
### `$cond` / `$then` / `$else` — Conditional value
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"$cond": { "$state": "/form/isValid" },
|
|
289
|
+
"$then": { "$token": "colors.success" },
|
|
290
|
+
"$else": { "$token": "colors.muted" }
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
Evaluates a condition. Supports operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `not`.
|
|
294
|
+
|
|
295
|
+
Nested conditionals for multi-branch logic:
|
|
296
|
+
```json
|
|
297
|
+
{
|
|
298
|
+
"$cond": { "$item": "priority", "eq": "urgent" },
|
|
299
|
+
"$then": "#EF4444",
|
|
300
|
+
"$else": {
|
|
301
|
+
"$cond": { "$item": "priority", "eq": "high" },
|
|
302
|
+
"$then": "#F97316",
|
|
303
|
+
"$else": "#22C55E"
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### `$template` — String interpolation
|
|
309
|
+
```json
|
|
310
|
+
{ "$template": "Hello, ${/user/name}! You have ${/inbox/count} messages." }
|
|
311
|
+
```
|
|
312
|
+
Interpolates values into a string. Supports two reference types:
|
|
313
|
+
- State paths: `${/user/name}` — reads from application state
|
|
314
|
+
- `$let` bindings: `${age}` — reads from a named binding defined with `$let`
|
|
315
|
+
|
|
316
|
+
### `$computed` — Registered function
|
|
317
|
+
```json
|
|
318
|
+
{ "$computed": "calculateBMI", "args": { "weight": { "$state": "/patient/weight" }, "height": { "$state": "/patient/height" } } }
|
|
319
|
+
```
|
|
320
|
+
Calls a registered function with resolved arguments. **Prefer `$math`, `$array`, `$date`, `$format` over `$computed`** — built-in operations cover 90%+ of cases without code.
|
|
321
|
+
|
|
322
|
+
### `$let` / `$ref` — Named bindings
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"$let": { "bmi": { "$computed": "calculateBMI", "args": { ... } } },
|
|
326
|
+
"$in": { "$ref": "bmi" }
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
Define a named value once, reference it multiple times. Avoids repetition.
|
|
330
|
+
|
|
331
|
+
**Later bindings can reference earlier ones:**
|
|
332
|
+
```json
|
|
333
|
+
{
|
|
334
|
+
"$let": {
|
|
335
|
+
"total": { "$array": "count", "source": { "$state": "/items" } },
|
|
336
|
+
"label": { "$template": "${total} items" }
|
|
337
|
+
},
|
|
338
|
+
"$in": { "$ref": "label" }
|
|
339
|
+
}
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**IMPORTANT — JSONB array format:** When the spec is stored in a database (JSONB), key ordering is not preserved. If bindings reference each other via `$ref`, use array format to guarantee order:
|
|
343
|
+
```json
|
|
344
|
+
{
|
|
345
|
+
"$let": [
|
|
346
|
+
["filtered", { "$array": "filter", "source": { "$state": "/items" }, "where": { "field": "status", "eq": "active" } }],
|
|
347
|
+
["count", { "$array": "count", "source": { "$ref": "filtered" } }]
|
|
348
|
+
],
|
|
349
|
+
"$in": { "$template": "${count} active items" }
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
Object format is fine when bindings don't reference each other.
|
|
353
|
+
|
|
354
|
+
### `$i18n` — Translation
|
|
355
|
+
```json
|
|
356
|
+
{ "$i18n": "patient.name" }
|
|
357
|
+
{ "$i18n": "welcome", "args": { "name": { "$state": "/user/name" } } }
|
|
358
|
+
```
|
|
359
|
+
Resolves a translation key for the active locale.
|
|
360
|
+
|
|
361
|
+
### `$breakpoint` — Responsive value
|
|
362
|
+
```json
|
|
363
|
+
{ "$breakpoint": { "sm": 1, "md": 2, "lg": 3 } }
|
|
364
|
+
```
|
|
365
|
+
Resolves to different values based on viewport width. **Auto-tracked** — the renderer writes `/ui/device/viewportWidth` on mount and on resize. No manual setup needed.
|
|
366
|
+
|
|
367
|
+
Default breakpoints: `sm: 0`, `md: 768`, `lg: 1024`, `xl: 1280`. Override via design tokens.
|
|
368
|
+
|
|
369
|
+
### `$platform` — Cross-platform value
|
|
370
|
+
```json
|
|
371
|
+
{ "$platform": { "web": "blur(24px)", "native": null } }
|
|
372
|
+
```
|
|
373
|
+
Resolves to different values based on the current platform (`web`, `ios`, `android`). Reads `/ui/device/platform` from state — set automatically by `useDeviceContext` on both web and React Native.
|
|
374
|
+
|
|
375
|
+
**`native` alias** matches any non-web platform (ios, android). Specific platform keys take priority over `native`:
|
|
376
|
+
```json
|
|
377
|
+
{ "$platform": { "web": "layout-web", "native": "layout-native" } }
|
|
378
|
+
{ "$platform": { "web": "hover-card", "ios": "ios-card", "android": "android-card" } }
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Use with `$breakpoint`** — `$breakpoint` handles responsive layout (viewport width), `$platform` handles platform-specific techniques (CSS vs native components). They are complementary, not alternatives.
|
|
382
|
+
|
|
383
|
+
### `$item` / `$index` — Repeat context (read-only)
|
|
384
|
+
```json
|
|
385
|
+
{ "$item": "title" }
|
|
386
|
+
{ "$item": "" }
|
|
387
|
+
{ "$index": true }
|
|
388
|
+
```
|
|
389
|
+
Inside a `repeat` block: `$item` reads a field from the current item, `$index` returns the position. Can be used in `visible`, `style`, `props`, and `on` (event params are pre-resolved with item context).
|
|
390
|
+
|
|
391
|
+
### `$bindItem` — Repeat context (two-way)
|
|
392
|
+
```json
|
|
393
|
+
{ "$bindItem": "completed" }
|
|
394
|
+
```
|
|
395
|
+
Two-way binding to a field in the current repeat item.
|
|
396
|
+
|
|
397
|
+
### `$prop` — Element prop reference (Layer 3)
|
|
398
|
+
```json
|
|
399
|
+
{ "$prop": "label" }
|
|
400
|
+
```
|
|
401
|
+
References a prop passed to a custom element definition. Used inside element render trees.
|
|
402
|
+
|
|
403
|
+
### `$math` — Arithmetic operations
|
|
404
|
+
```json
|
|
405
|
+
{ "$math": "add", "args": [{ "$state": "/a" }, { "$state": "/b" }] }
|
|
406
|
+
{ "$math": "multiply", "args": [{ "$state": "/price" }, { "$state": "/qty" }] }
|
|
407
|
+
{ "$math": "round", "value": 3.14159, "decimals": 2 }
|
|
408
|
+
```
|
|
409
|
+
Operations: `add`, `subtract`, `multiply`, `divide`, `round`, `floor`, `ceil`, `abs`, `min`, `max`, `mod`
|
|
410
|
+
|
|
411
|
+
**Undefined args → 0:** If a `$math` argument resolves to `undefined` or `NaN` (e.g., reading a state path that doesn't exist yet), it is treated as `0`. This prevents NaN from propagating through the UI. For calculations that depend on data being loaded (e.g., `multiply(price, quantity)`), ensure the data exists before rendering — use `visible` conditions or `initialActions` to load data first. The spec author is responsible for data availability; `$math` is responsible for never producing NaN.
|
|
412
|
+
|
|
413
|
+
### `$array` — Array operations
|
|
414
|
+
```json
|
|
415
|
+
{ "$array": "count", "source": { "$state": "/products" } }
|
|
416
|
+
{ "$array": "count", "source": { "$state": "/products" }, "where": { "field": "stock", "lt": 10 } }
|
|
417
|
+
{ "$array": "sum", "source": { "$state": "/products" }, "field": "price" }
|
|
418
|
+
{ "$array": "sumProduct", "source": { "$state": "/products" }, "field1": "price", "field2": "stock" }
|
|
419
|
+
{ "$array": "filter", "source": { "$state": "/items" }, "where": { "field": "status", "eq": "active" } }
|
|
420
|
+
{ "$array": "remove", "source": { "$state": "/items" }, "where": { "field": "id", "eq": 3 } }
|
|
421
|
+
{ "$array": "append", "source": { "$state": "/items" }, "value": { "name": "New Item" } }
|
|
422
|
+
{ "$array": "replace", "source": { "$state": "/items" }, "where": { "field": "id", "eq": 1 }, "value": { "name": "Updated" } }
|
|
423
|
+
{ "$array": "toggle", "source": { "$state": "/selectedIds" }, "value": 5 }
|
|
424
|
+
{ "$array": "search", "source": { "$state": "/products" }, "query": { "$state": "/search" }, "fields": ["name", "category"] }
|
|
425
|
+
{ "$array": "slice", "source": { "$state": "/items" }, "from": 0, "to": 10 }
|
|
426
|
+
{ "$array": "sort", "source": { "$state": "/items" }, "field": "name", "direction": "asc" }
|
|
427
|
+
{ "$array": "find", "source": { "$state": "/items" }, "where": { "field": "id", "eq": 1 } }
|
|
428
|
+
{ "$array": "includes", "source": { "$state": "/tags" }, "value": "urgent" }
|
|
429
|
+
{ "$array": "map", "source": { "$state": "/items" }, "field": "name" }
|
|
430
|
+
{ "$array": "first", "source": { "$state": "/items" } }
|
|
431
|
+
{ "$array": "last", "source": { "$state": "/items" } }
|
|
432
|
+
```
|
|
433
|
+
Where operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `notIn`
|
|
434
|
+
|
|
435
|
+
### `$date` — Date operations
|
|
436
|
+
```json
|
|
437
|
+
{ "$date": "now" }
|
|
438
|
+
{ "$date": "today" }
|
|
439
|
+
{ "$date": "age", "from": { "$state": "/birthDate" } }
|
|
440
|
+
{ "$date": "diff", "from": { "$state": "/start" }, "to": { "$state": "/end" }, "unit": "days" }
|
|
441
|
+
{ "$date": "format", "value": { "$state": "/date" }, "pattern": "short" }
|
|
442
|
+
{ "$date": "add", "value": { "$state": "/date" }, "amount": 7, "unit": "days" }
|
|
443
|
+
```
|
|
444
|
+
Units: `days`, `months`, `years`, `hours`, `minutes`. Patterns: `short`, `long`, `numeric`, `time`, `datetime`
|
|
445
|
+
|
|
446
|
+
### `$format` — Value formatting
|
|
447
|
+
```json
|
|
448
|
+
{ "$format": "currency", "value": { "$state": "/price" }, "currency": "USD" }
|
|
449
|
+
{ "$format": "currency", "value": 1234, "currency": "HNL", "locale": "es-HN" }
|
|
450
|
+
{ "$format": "number", "value": 1234.5, "decimals": 2 }
|
|
451
|
+
{ "$format": "number", "value": 1234567, "notation": "compact" }
|
|
452
|
+
{ "$format": "number", "value": -500, "signDisplay": "always", "decimals": 2 }
|
|
453
|
+
{ "$format": "percent", "value": 0.75 }
|
|
454
|
+
{ "$format": "phone", "value": { "$state": "/phone" } }
|
|
455
|
+
{ "$format": "uppercase", "value": { "$state": "/name" } }
|
|
456
|
+
{ "$format": "lowercase", "value": "HELLO" }
|
|
457
|
+
{ "$format": "capitalize", "value": "hello world" }
|
|
458
|
+
{ "$format": "truncate", "value": "long text...", "length": 20 }
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
**Extended options for `currency` and `number`:**
|
|
462
|
+
|
|
463
|
+
| Parameter | Type | Default | Description |
|
|
464
|
+
|-----------|------|---------|-------------|
|
|
465
|
+
| `locale` | string or Expression | context locale | Override locale per-field (e.g., `"es-HN"`) |
|
|
466
|
+
| `notation` | `"standard"` \| `"compact"` \| `"scientific"` \| `"engineering"` | `"standard"` | Number notation style |
|
|
467
|
+
| `signDisplay` | `"auto"` \| `"always"` \| `"exceptZero"` \| `"negative"` | `"auto"` | Sign display mode |
|
|
468
|
+
| `useGrouping` | boolean | `true` | Thousands separator on/off |
|
|
469
|
+
|
|
470
|
+
All parameters are optional — existing specs work unchanged.
|
|
471
|
+
|
|
472
|
+
### `$and` / `$or` / `$not` — Boolean logic
|
|
473
|
+
```json
|
|
474
|
+
{ "$and": [{ "$state": "/form/name" }, { "$state": "/form/price" }] }
|
|
475
|
+
{ "$or": [{ "$state": "/user/isAdmin" }, { "$state": "/user/isModerator" }] }
|
|
476
|
+
{ "$not": { "$state": "/form/isValid" } }
|
|
477
|
+
```
|
|
478
|
+
Works everywhere — `visible`, `disabled`, `style`, any prop. Returns true/false.
|
|
479
|
+
|
|
480
|
+
### `$switch` — Multi-branch conditional
|
|
481
|
+
```json
|
|
482
|
+
{
|
|
483
|
+
"$switch": { "$state": "/filter/type" },
|
|
484
|
+
"cases": {
|
|
485
|
+
"1": "Current Expenses",
|
|
486
|
+
"2": "Capital Expenses",
|
|
487
|
+
"3": { "$token": "colors.warning" },
|
|
488
|
+
"4": { "$template": "Type ${/count}" }
|
|
489
|
+
},
|
|
490
|
+
"default": "Other"
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
Evaluates `$switch` value, converts to string, looks up in `cases`. Only the matching case is resolved (lazy). `default` is required — prevents silent `undefined`.
|
|
494
|
+
|
|
495
|
+
Replaces deeply nested `$cond` for 3+ branches. Cases can contain any expression.
|
|
496
|
+
|
|
497
|
+
### `$group` — Group context (inside repeat.groupBy)
|
|
498
|
+
```json
|
|
499
|
+
{ "$group": "key" }
|
|
500
|
+
{ "$group": "count" }
|
|
501
|
+
{ "$group": "index" }
|
|
502
|
+
{ "$group": "items" }
|
|
503
|
+
{ "$group": "sum", "field": "amount" }
|
|
504
|
+
{ "$group": "avg", "field": "price" }
|
|
505
|
+
{ "$group": "min", "field": "score" }
|
|
506
|
+
{ "$group": "max", "field": "score" }
|
|
507
|
+
{ "$group": "subtotal.total" }
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
| Operation | Description | Available in |
|
|
511
|
+
|-----------|-------------|-------------|
|
|
512
|
+
| `key` | Group key value (e.g., "Institution A") | groupHeader, groupFooter |
|
|
513
|
+
| `count` | Number of items in this group | groupHeader, groupFooter |
|
|
514
|
+
| `index` | Group index (0-based) | groupHeader, groupFooter |
|
|
515
|
+
| `items` | Array of items in this group | groupHeader, groupFooter |
|
|
516
|
+
| `sum` | Sum of `field` across group items | groupHeader, groupFooter |
|
|
517
|
+
| `avg` | Average of `field` | groupHeader, groupFooter |
|
|
518
|
+
| `min` | Minimum of `field` | groupHeader, groupFooter |
|
|
519
|
+
| `max` | Maximum of `field` | groupHeader, groupFooter |
|
|
520
|
+
| `dot.path` | Dot-notation access to raw group object (pre-grouped mode) | groupHeader, groupFooter |
|
|
521
|
+
|
|
522
|
+
**Only valid inside `repeat` with `groupBy` or `groupKey`.** Throws error if used outside.
|
|
523
|
+
|
|
524
|
+
### `$selection` — Selection state (inside repeat.selection)
|
|
525
|
+
```json
|
|
526
|
+
{ "$selection": "selected" }
|
|
527
|
+
{ "$selection": "count" }
|
|
528
|
+
```
|
|
529
|
+
| Operation | Returns | Description |
|
|
530
|
+
|-----------|---------|-------------|
|
|
531
|
+
| `selected` | boolean | Whether the current item's key is in the selection array |
|
|
532
|
+
| `count` | number | Total number of selected items |
|
|
533
|
+
|
|
534
|
+
**Only valid inside `repeat` with `selection` config.** Throws error if used outside.
|
|
535
|
+
|
|
536
|
+
### `$auth` — Authenticated user data
|
|
537
|
+
```json
|
|
538
|
+
{ "$auth": "email" }
|
|
539
|
+
{ "$auth": "role" }
|
|
540
|
+
{ "$auth": "isAuthenticated" }
|
|
541
|
+
{ "$auth": "metadata.department" }
|
|
542
|
+
```
|
|
543
|
+
Reads from the authenticated user's data. **Security whitelist** — only safe fields resolve. Sensitive fields (token, password, secret, session, and variants) always return `undefined`. See blocked fields list below.
|
|
544
|
+
|
|
545
|
+
| Field | Returns |
|
|
546
|
+
|-------|---------|
|
|
547
|
+
| `isAuthenticated` | boolean — false when no auth state |
|
|
548
|
+
| `id`, `email`, `name`, `avatar` | User profile fields |
|
|
549
|
+
| `role` | Primary role (string) |
|
|
550
|
+
| `roles` | All roles (string[]) |
|
|
551
|
+
| `metadata` | Full metadata object |
|
|
552
|
+
| `metadata.*` | Nested metadata field (dot notation) |
|
|
553
|
+
| `user` | Full user object |
|
|
554
|
+
| `loading` | Auth operation in progress |
|
|
555
|
+
| `error` | Last auth error message or null |
|
|
556
|
+
|
|
557
|
+
**Blocked fields:** `token`, `accessToken`, `access_token`, `refreshToken`, `refresh_token`, `password`, `secret`, `session`, `metadata.token`, `metadata.password` — always return `undefined`.
|
|
558
|
+
|
|
559
|
+
**Prefer `$auth` over `$state` for auth data** — `$auth` has security whitelist, clear semantics, and stable paths.
|
|
560
|
+
|
|
561
|
+
---
|
|
562
|
+
|
|
563
|
+
## Available Primitives
|
|
564
|
+
|
|
565
|
+
These are the built-in visual building blocks. Use them as the `type` field in elements.
|
|
566
|
+
|
|
567
|
+
**Layout:** `box`, `text`, `image`, `icon`, `stack`, `grid`, `scroll`, `divider`, `spacer`
|
|
568
|
+
**Form:** `input`, `textarea`, `select`, `checkbox`, `toggle`, `slider`
|
|
569
|
+
**Interaction:** `button`, `touchable`, `list`
|
|
570
|
+
**Overlays:** `modal`, `drawer`, `tabs`, `accordion`, `wizard`, `screen`
|
|
571
|
+
**Data visualization:** `bar-chart`, `line-chart`, `pie-chart`, `area-chart`, `table`, `kanban-board`
|
|
572
|
+
**Special:** `file-upload`, `camera`, `signature`, `audio-player`
|
|
573
|
+
|
|
574
|
+
Each primitive accepts `style` (CSS object), `visible` (condition), and `permission` (role-based access).
|
|
575
|
+
|
|
576
|
+
### Icon Primitive
|
|
577
|
+
|
|
578
|
+
The `icon` primitive renders icons from a connected icon library. By default it's a placeholder — apps connect a library via `plugins.overridePrimitive('icon', ...)`.
|
|
579
|
+
|
|
580
|
+
```json
|
|
581
|
+
{ "type": "icon", "props": { "name": "pencil-simple", "size": 16, "weight": "bold", "color": "#666" } }
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
| Prop | Type | Default | Purpose |
|
|
585
|
+
|------|------|---------|---------|
|
|
586
|
+
| `name` | string | `"circle"` | Icon name in kebab-case (e.g., `pencil-simple`, `trash`, `moon`) |
|
|
587
|
+
| `size` | number | `24` | Icon width/height in pixels |
|
|
588
|
+
| `weight` | string | `"regular"` | `thin`, `light`, `regular`, `bold`, `fill`, `duotone` |
|
|
589
|
+
| `color` | string | — | CSS color value. Supports `$token` expressions |
|
|
590
|
+
|
|
591
|
+
Icon names are library-agnostic (kebab-case). The connected library resolves them to the actual icon component.
|
|
592
|
+
|
|
593
|
+
**Icon inside a button:**
|
|
594
|
+
```json
|
|
595
|
+
"edit-btn": {
|
|
596
|
+
"type": "button",
|
|
597
|
+
"props": { "style": { "display": "inline-flex", "alignItems": "center" } },
|
|
598
|
+
"children": ["edit-icon"],
|
|
599
|
+
"on": { "press": { "action": "openModal", "params": { "id": "edit-modal" } } }
|
|
600
|
+
},
|
|
601
|
+
"edit-icon": {
|
|
602
|
+
"type": "icon",
|
|
603
|
+
"props": { "name": "pencil-simple", "size": 16, "weight": "bold", "color": { "$token": "colors.textMuted" } }
|
|
604
|
+
}
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
**Conditional icon name (e.g., theme toggle):**
|
|
608
|
+
```json
|
|
609
|
+
"theme-icon": {
|
|
610
|
+
"type": "icon",
|
|
611
|
+
"props": {
|
|
612
|
+
"name": { "$cond": { "$state": "/preferences/theme", "eq": "dark" }, "$then": "sun", "$else": "moon" },
|
|
613
|
+
"size": 16, "weight": "bold"
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
### Modal & Drawer Auto-Visibility
|
|
619
|
+
|
|
620
|
+
Modals and drawers **automatically manage their own visibility**. You do NOT need to add a `visible` condition. The engine binds:
|
|
621
|
+
- `type: "modal"` → visible when `/ui/modals/{elementId}` is truthy
|
|
622
|
+
- `type: "drawer"` → visible when `/ui/drawers/{elementId}` is truthy
|
|
623
|
+
|
|
624
|
+
Use `openModal`/`closeModal` actions to control them:
|
|
625
|
+
```json
|
|
626
|
+
"my-dialog": {
|
|
627
|
+
"type": "modal",
|
|
628
|
+
"props": { "title": "Confirm Action" },
|
|
629
|
+
"children": ["dialog-content"]
|
|
630
|
+
}
|
|
631
|
+
```
|
|
632
|
+
Open it with: `{ "action": "openModal", "params": { "id": "my-dialog" } }`
|
|
633
|
+
Close it with: `{ "action": "closeModal", "params": { "id": "my-dialog" } }`
|
|
634
|
+
|
|
635
|
+
The `modal` primitive renders a `title` prop as an h2 heading above the children.
|
|
636
|
+
|
|
637
|
+
**Drawer example:**
|
|
638
|
+
```json
|
|
639
|
+
"detail-drawer": {
|
|
640
|
+
"type": "drawer",
|
|
641
|
+
"props": { "side": "right", "width": 400, "style": { "padding": 24 } },
|
|
642
|
+
"children": ["drawer-content"]
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
Open it with: `{ "action": "openDrawer", "params": { "id": "detail-drawer" } }`
|
|
646
|
+
Close it with: `{ "action": "closeDrawer", "params": { "id": "detail-drawer" } }`
|
|
647
|
+
|
|
648
|
+
| Prop | Type | Default | Purpose |
|
|
649
|
+
|------|------|---------|---------|
|
|
650
|
+
| `side` | string | `"left"` | `"left"` or `"right"` — which edge the drawer slides from |
|
|
651
|
+
| `width` | number/string | `300` | Width of the drawer panel |
|
|
652
|
+
|
|
653
|
+
### Toast Notifications
|
|
654
|
+
|
|
655
|
+
Toasts render automatically — no configuration needed. The `showNotification` action writes to `/ui/notifications` and the renderer displays toasts with entry/exit animations, auto-dismiss, and type-based icons.
|
|
656
|
+
|
|
657
|
+
**Basic usage:**
|
|
658
|
+
```json
|
|
659
|
+
{ "action": "showNotification", "params": { "message": "Task created successfully", "type": "success" } }
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
**With title:**
|
|
663
|
+
```json
|
|
664
|
+
{ "action": "showNotification", "params": { "title": "Error", "message": "Could not save. Server returned 500.", "type": "error" } }
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**Persistent (no auto-dismiss):**
|
|
668
|
+
```json
|
|
669
|
+
{ "action": "showNotification", "params": { "message": "Unsaved changes", "type": "warning", "duration": null } }
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**Custom duration:**
|
|
673
|
+
```json
|
|
674
|
+
{ "action": "showNotification", "params": { "message": "Copied!", "type": "info", "duration": 2000 } }
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
**Dismiss programmatically:**
|
|
678
|
+
```json
|
|
679
|
+
{ "action": "dismissNotification", "params": { "id": "notification-id" } }
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
| Param | Type | Default | Description |
|
|
683
|
+
|-------|------|---------|-------------|
|
|
684
|
+
| `message` | string | — | Toast message text (required) |
|
|
685
|
+
| `type` | string | `"info"` | `success`, `error`, `warning`, `info` |
|
|
686
|
+
| `title` | string | — | Optional bold heading above message |
|
|
687
|
+
| `duration` | number or null | container default (4000) | Auto-dismiss time in ms. `null` = persistent |
|
|
688
|
+
|
|
689
|
+
**Override with toast-container element:**
|
|
690
|
+
|
|
691
|
+
By default, toasts appear at top-right with 4s duration. For custom positioning or behavior, add a `toast-container` element to your spec:
|
|
692
|
+
|
|
693
|
+
```json
|
|
694
|
+
"notifications": {
|
|
695
|
+
"type": "toast-container",
|
|
696
|
+
"props": {
|
|
697
|
+
"position": "bottom-center",
|
|
698
|
+
"duration": 6000,
|
|
699
|
+
"maxVisible": 3,
|
|
700
|
+
"offset": 16,
|
|
701
|
+
"gap": 12
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
| Prop | Type | Default | Description |
|
|
707
|
+
|------|------|---------|-------------|
|
|
708
|
+
| `position` | string | `"top-right"` | `top-right`, `top-left`, `top-center`, `bottom-right`, `bottom-left`, `bottom-center` |
|
|
709
|
+
| `duration` | number | `4000` | Default auto-dismiss time in ms |
|
|
710
|
+
| `maxVisible` | number | `5` | Maximum toasts shown at once |
|
|
711
|
+
| `offset` | number | `24` | Distance from viewport edge in px |
|
|
712
|
+
| `gap` | number | `8` | Space between toasts in px |
|
|
713
|
+
|
|
714
|
+
Without a `toast-container` in the spec, defaults are used automatically. Icons use the connected icon library (Phosphor, Lucide, etc.) or fallback SVGs.
|
|
715
|
+
|
|
716
|
+
### Device Context (Auto-Tracked)
|
|
717
|
+
|
|
718
|
+
The renderer automatically tracks device information and writes it to state. Specs can read these values for responsive behavior, platform detection, and OS preferences.
|
|
719
|
+
|
|
720
|
+
| State Path | Type | Description |
|
|
721
|
+
|-----------|------|-------------|
|
|
722
|
+
| `/ui/device/viewportWidth` | number | Current viewport width in px |
|
|
723
|
+
| `/ui/device/viewportHeight` | number | Current viewport height in px |
|
|
724
|
+
| `/ui/device/platform` | string | `"web"`, `"ios"`, or `"android"` |
|
|
725
|
+
| `/ui/device/orientation` | string | `"portrait"` or `"landscape"` (auto-computed) |
|
|
726
|
+
| `/ui/device/colorScheme` | string | OS preference: `"light"` or `"dark"` |
|
|
727
|
+
|
|
728
|
+
All values update in real-time (viewport on resize, colorScheme on OS change).
|
|
729
|
+
|
|
730
|
+
**Usage in specs:**
|
|
731
|
+
```json
|
|
732
|
+
{ "$breakpoint": { "sm": 1, "md": 2, "lg": 4 } }
|
|
733
|
+
{ "$platform": { "web": "blur(24px)", "native": null } }
|
|
734
|
+
{ "$platform": { "web": "layout-web", "native": "layout-native" } }
|
|
735
|
+
{ "$cond": { "$state": "/ui/device/orientation", "eq": "portrait" }, "$then": "vertical", "$else": "horizontal" }
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
**Opt-out:** If the host app manages device context manually, disable auto-tracking:
|
|
739
|
+
```tsx
|
|
740
|
+
<MythikRenderer spec={spec} autoDeviceContext={false} />
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
**Legacy compatibility:** `$breakpoint` reads `/ui/device/viewportWidth` first, falls back to `/ui/viewportWidth` for existing apps that write there manually.
|
|
744
|
+
|
|
745
|
+
### Render Error Visibility
|
|
746
|
+
|
|
747
|
+
`createMythik({ security: { exposeErrors } })` controls how much render error detail the UI exposes.
|
|
748
|
+
|
|
749
|
+
```tsx
|
|
750
|
+
const svc = createMythik({
|
|
751
|
+
security: { exposeErrors: false },
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
<MythikRenderer spec={spec} instance={svc} />
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
- Default is `true` (`secConfig.exposeErrors !== false`).
|
|
758
|
+
- `_error` render nodes still write diagnostics to `/ui/renderErrors` when error exposure is enabled.
|
|
759
|
+
- Primitive/component exceptions are caught by a renderer-level error boundary. In development with `exposeErrors !== false`, the boundary renders a visible overlay with the error message and component stack.
|
|
760
|
+
- In production or with `exposeErrors: false`, the boundary renders a neutral placeholder and does not leak error details.
|
|
761
|
+
- If a broken spec is replaced with a valid spec, the boundary resets on spec change and renders the recovered content.
|
|
762
|
+
|
|
763
|
+
### Table Primitive (Enhanced)
|
|
764
|
+
|
|
765
|
+
Feature-rich table with sorting, pagination, grouping, selection, and formatting. The AI enables/disables features with flags — no need to compose from primitives.
|
|
766
|
+
|
|
767
|
+
**Basic usage (backwards compatible):**
|
|
768
|
+
```json
|
|
769
|
+
"my-table": {
|
|
770
|
+
"type": "table",
|
|
771
|
+
"props": {
|
|
772
|
+
"columns": [
|
|
773
|
+
{ "key": "name", "label": "Name", "width": "2fr", "align": "left" },
|
|
774
|
+
{ "key": "amount", "label": "Amount", "width": "1fr", "align": "right", "format": "currency", "formatOptions": { "currency": "HNL", "locale": "es-HN" } },
|
|
775
|
+
{ "key": "status", "label": "Status", "width": "120px" }
|
|
776
|
+
],
|
|
777
|
+
"data": { "$state": "/items" }
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
Columns accept both `"key"` and `"field"` for the data field name.
|
|
783
|
+
|
|
784
|
+
**Full configuration:**
|
|
785
|
+
```json
|
|
786
|
+
{
|
|
787
|
+
"type": "table",
|
|
788
|
+
"props": {
|
|
789
|
+
"data": { "$state": "/response/data" },
|
|
790
|
+
"columns": [ "..." ],
|
|
791
|
+
"sorting": { "enabled": true, "default": { "field": "amount", "direction": "desc" }, "mode": "client" },
|
|
792
|
+
"pagination": { "enabled": true, "pageSize": 20, "mode": "client" },
|
|
793
|
+
"selection": { "enabled": true, "mode": "multiple", "state": "/selectedIds" },
|
|
794
|
+
"groupBy": { "field": "institution", "header": true, "footer": "subtotal", "collapsible": true },
|
|
795
|
+
"stickyHeader": true,
|
|
796
|
+
"emptyState": { "icon": "inbox", "message": "No data" },
|
|
797
|
+
"rowStyle": { "hover": { "backgroundColor": "#F1F5F9" } },
|
|
798
|
+
"onRowClick": { "action": "openDrawer", "params": { "id": "detail-drawer" } }
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
**Column properties:**
|
|
804
|
+
|
|
805
|
+
| Property | Type | Default | Description |
|
|
806
|
+
|----------|------|---------|-------------|
|
|
807
|
+
| `id` / `key` / `field` | string | — | Field name in data objects |
|
|
808
|
+
| `label` | string | — | Header text |
|
|
809
|
+
| `width` | string | `"1fr"` | CSS Grid width (`fr`, `px`, `%`) |
|
|
810
|
+
| `align` | `"left"` \| `"center"` \| `"right"` | `"left"` | Text alignment |
|
|
811
|
+
| `format` | `"currency"` \| `"number"` \| `"percent"` \| `"date"` | — | Auto-format cell values |
|
|
812
|
+
| `formatOptions` | object | — | `{ currency, locale, decimals }` |
|
|
813
|
+
| `sortable` | boolean | `true` | Column can be sorted |
|
|
814
|
+
| `visible` | boolean | `true` | Column visibility |
|
|
815
|
+
| `actions` | array | — | Action buttons in column: `[{ icon, color, onPress }]` |
|
|
816
|
+
|
|
817
|
+
**Sorting:** `{ enabled, mode, default, state }` — `"client"` sorts internally, `"server"` writes `{ field, direction }` to state (triggers dataSource re-fetch).
|
|
818
|
+
|
|
819
|
+
**Pagination:** `{ enabled, pageSize, mode, state, totalItems }` — `"client"` slices internally, `"server"` writes page number to state.
|
|
820
|
+
|
|
821
|
+
**Selection:** `{ enabled, mode, state }` — `"multiple"` adds checkbox column with select-all. `"single"` adds radio column.
|
|
822
|
+
|
|
823
|
+
**GroupBy:** `{ field, header, footer, expanded, collapsible }` — `footer: "subtotal"` auto-sums numeric columns.
|
|
824
|
+
|
|
825
|
+
### Design Token Auto-Inheritance
|
|
826
|
+
|
|
827
|
+
All primitives automatically inherit from deep design tokens — not just colors but also shape (border-radius), typography (font family, size, weight), spacing (padding, gaps), elevation (shadows), motion (transitions), and opacity (disabled/pressed states). No `$token` references needed for primitive defaults. Use explicit `style` props only for per-element overrides. Sub-elements (modal title, input label, error text) also read from tokens.
|
|
828
|
+
|
|
829
|
+
### Input Format Prop
|
|
830
|
+
|
|
831
|
+
The `input` primitive supports a `format` prop for display masking:
|
|
832
|
+
- `"phone"` — displays as (555) 123-4567, stores raw digits
|
|
833
|
+
- `"currency"` — displays as $1,234.56, stores raw number
|
|
834
|
+
|
|
835
|
+
### Input Type: Color
|
|
836
|
+
|
|
837
|
+
`type: "color"` renders a native color picker. Use with `$bindState` for interactive color selection:
|
|
838
|
+
```json
|
|
839
|
+
{ "type": "input", "props": { "type": "color", "label": "Pick Color", "value": { "$bindState": "/form/color" } } }
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
---
|
|
843
|
+
|
|
844
|
+
## Actions
|
|
845
|
+
|
|
846
|
+
Wire actions to events with the `on` property:
|
|
847
|
+
|
|
848
|
+
```json
|
|
849
|
+
{
|
|
850
|
+
"on": {
|
|
851
|
+
"press": { "action": "navigate", "params": { "screen": "detail" } }
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
### Multiple Actions (Sequential)
|
|
857
|
+
|
|
858
|
+
Actions in an array execute sequentially — each async action (like `fetch`) completes before the next one starts:
|
|
859
|
+
```json
|
|
860
|
+
{
|
|
861
|
+
"on": {
|
|
862
|
+
"press": [
|
|
863
|
+
{ "action": "fetch", "params": { "url": "...", "method": "POST", "body": { ... } } },
|
|
864
|
+
{ "action": "closeModal", "params": { "id": "my-modal" } },
|
|
865
|
+
{ "action": "fetch", "params": { "url": "...", "method": "GET", "target": "/items" } }
|
|
866
|
+
]
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
```
|
|
870
|
+
|
|
871
|
+
### fireAndForget Mode
|
|
872
|
+
|
|
873
|
+
Add `"fireAndForget": true` to dispatch an action without waiting for it to complete:
|
|
874
|
+
```json
|
|
875
|
+
{
|
|
876
|
+
"action": "fetch",
|
|
877
|
+
"fireAndForget": true,
|
|
878
|
+
"params": { "url": "...", "method": "GET", "target": "/items" }
|
|
879
|
+
}
|
|
880
|
+
```
|
|
881
|
+
The action executes in the background. The next action in the chain starts immediately.
|
|
882
|
+
|
|
883
|
+
**Use case:** Background re-fetches after closing a modal. The user sees the modal close instantly while the data refreshes behind the scenes.
|
|
884
|
+
|
|
885
|
+
### Action Reference
|
|
886
|
+
|
|
887
|
+
| Action | Params | Purpose |
|
|
888
|
+
|--------|--------|---------|
|
|
889
|
+
| `setState` | `{ statePath, value }` | Set a value in state. Value can contain expressions and nested objects |
|
|
890
|
+
| `navigateScreen` | `{ screen, ...params }` | Navigate to a screen (MythikApp context) |
|
|
891
|
+
| `goBackScreen` | — | Go to previous screen using navigation history (MythikApp context) |
|
|
892
|
+
| `navigate` | `{ screen, ...params }` | Set navigation intent in state (low-level, prefer `navigateScreen`) |
|
|
893
|
+
| `goBack` | — | Set goBack intent in state (low-level, prefer `goBackScreen`) |
|
|
894
|
+
| `openModal` | `{ id }` | Show a modal (sets `/ui/modals/{id}` to true) |
|
|
895
|
+
| `closeModal` | `{ id }` | Hide a modal (sets `/ui/modals/{id}` to false) |
|
|
896
|
+
| `openDrawer` | `{ id }` | Open a drawer |
|
|
897
|
+
| `closeDrawer` | `{ id }` | Close a drawer |
|
|
898
|
+
| `showNotification` | `{ message, type }` | Show a toast/snackbar |
|
|
899
|
+
| `toggleTheme` | — | Switch dark/light mode |
|
|
900
|
+
| `setLocale` | `{ locale }` | Change language |
|
|
901
|
+
| `copyToClipboard` | `{ value }` | Copy to clipboard. Objects auto-stringified as JSON |
|
|
902
|
+
| `openUrl` | `{ url }` | Open external URL |
|
|
903
|
+
| `fetch` | `{ url, method, body, target, headers }` | HTTP request, writes response to target state path |
|
|
904
|
+
| `submitForm` | `{ url, method, body, target, successMessage }` | Validate + HTTP request + success notification |
|
|
905
|
+
| `dismissNotification` | `{ id }` | Remove a toast notification by id |
|
|
906
|
+
| `login` | `{ email, password, ... }` | Authenticate via auth provider. Writes user to `/auth/*` state. Clears credentials after. |
|
|
907
|
+
| `logout` | — | Clear session, tokens, state. Navigate to loginScreen. |
|
|
908
|
+
| `refreshSession` | — | Manual token refresh. Normally automatic (proactive refresh). |
|
|
909
|
+
| `validateForm` | `{ formId }` | Mark all fields touched, validate all, write errors |
|
|
910
|
+
| `touchField` | `{ formId, field }` | Mark single field touched and validate it |
|
|
911
|
+
| `resetForm` | `{ formId }` | Reset form to initial values, clear errors |
|
|
912
|
+
| `uploadFile` | `{ bucket, target, path? }` | Upload file(s) to storage via StorageAdapter. Writes URL(s) to target path |
|
|
913
|
+
| `deleteFile` | `{ path, bucket }` | Delete file from storage |
|
|
914
|
+
| `export` | `{ source, columns, filename, format? }` | Export data as CSV (built-in) or via adapter (XLSX/PDF). Triggers download |
|
|
915
|
+
| `updateTokens` | `{ dna?: {...}, identity?: {...}, _replace?: true }` | Hot-swap design tokens at runtime. Deep-merges by default; `_replace: true` fully replaces. Persists raw config to `/tokens/raw`. Numeric DNA seeds > 1 auto-normalized (÷100) |
|
|
916
|
+
|
|
917
|
+
### fetch Action Details
|
|
918
|
+
|
|
919
|
+
The `fetch` action supports:
|
|
920
|
+
- **Custom headers:** `"headers": { "apikey": "...", "Authorization": "Bearer ..." }`
|
|
921
|
+
- **Body with expressions:** Nested expressions in `body` are deeply resolved before sending
|
|
922
|
+
- **Empty string sanitization:** Empty strings in body are automatically converted to `null` (prevents database errors for typed columns)
|
|
923
|
+
- **Loading state:** Sets `/ui/loading` to true while in flight, false when done
|
|
924
|
+
- **Error handling:** On failure, sets `/ui/lastError` with status and message
|
|
925
|
+
|
|
926
|
+
```json
|
|
927
|
+
{
|
|
928
|
+
"action": "fetch",
|
|
929
|
+
"params": {
|
|
930
|
+
"url": { "$template": "${/config/apiUrl}/rest/v1/tasks" },
|
|
931
|
+
"method": "POST",
|
|
932
|
+
"headers": { "$state": "/config/headers" },
|
|
933
|
+
"body": {
|
|
934
|
+
"title": { "$state": "/form/title" },
|
|
935
|
+
"description": { "$state": "/form/description" },
|
|
936
|
+
"due_date": { "$state": "/form/dueDate" }
|
|
937
|
+
},
|
|
938
|
+
"target": "/lastResult"
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
### Transactions (Optimistic Updates)
|
|
944
|
+
|
|
945
|
+
Use `transaction` for CRUD operations that need instant UI feedback. The framework takes a state snapshot, applies your changes instantly, then confirms with the server. On failure, it rolls back automatically.
|
|
946
|
+
|
|
947
|
+
```json
|
|
948
|
+
{
|
|
949
|
+
"on": {
|
|
950
|
+
"press": {
|
|
951
|
+
"transaction": {
|
|
952
|
+
"before": [/* UI transitions — NOT rolled back */],
|
|
953
|
+
"optimistic": [/* Data changes — ROLLED BACK on error */],
|
|
954
|
+
"confirm": [/* Network request — determines success/failure */],
|
|
955
|
+
"onSuccess": [/* Post-success sync (optional) */],
|
|
956
|
+
"onError": [/* User notification (optional, runs after rollback) */]
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
**Decision rule:**
|
|
964
|
+
- **"Would I undo this if the server says no?"** → `optimistic`
|
|
965
|
+
- **"Should this happen regardless?"** → `before`
|
|
966
|
+
|
|
967
|
+
| Action | Phase | Why |
|
|
968
|
+
|--------|-------|-----|
|
|
969
|
+
| `closeModal` | `before` | Don't reopen on error |
|
|
970
|
+
| `setState` (data) | `optimistic` | Rollback if server rejects |
|
|
971
|
+
| `setState` (form reset) | `optimistic` | Restore form so user can retry |
|
|
972
|
+
| `navigate` | `before` | Stay on page, show error there |
|
|
973
|
+
|
|
974
|
+
**Confirm result access:** The `fetch` in `confirm` can write to `/tx/result` via `target`. `onSuccess` reads it with `{ "$state": "/tx/result" }`. On error, `/tx/error` contains `{ message }`. Both are cleaned up automatically.
|
|
975
|
+
|
|
976
|
+
**Timeout:** Default 10 seconds. Override with `timeout` (ms):
|
|
977
|
+
```json
|
|
978
|
+
{ "transaction": { "timeout": 30000, "confirm": [...] } }
|
|
979
|
+
```
|
|
980
|
+
|
|
981
|
+
### Transaction Templates
|
|
982
|
+
|
|
983
|
+
Copy the template for your CRUD operation, replace the `{{PLACEHOLDERS}}`.
|
|
984
|
+
|
|
985
|
+
#### CREATE — Add item to list
|
|
986
|
+
|
|
987
|
+
```json
|
|
988
|
+
{
|
|
989
|
+
"transaction": {
|
|
990
|
+
"before": [
|
|
991
|
+
{ "action": "closeModal", "params": { "id": "{{MODAL_ID}}" } }
|
|
992
|
+
],
|
|
993
|
+
"optimistic": [
|
|
994
|
+
{ "action": "setState", "params": {
|
|
995
|
+
"statePath": "{{ARRAY_PATH}}",
|
|
996
|
+
"value": { "$array": "append", "source": { "$state": "{{ARRAY_PATH}}" }, "value": {
|
|
997
|
+
"id": { "$template": "temp-${/form/title}" },
|
|
998
|
+
"{{FIELD_1}}": { "$state": "/form/{{FIELD_1}}" },
|
|
999
|
+
"{{FIELD_2}}": { "$state": "/form/{{FIELD_2}}" }
|
|
1000
|
+
}}
|
|
1001
|
+
}}
|
|
1002
|
+
],
|
|
1003
|
+
"confirm": [
|
|
1004
|
+
{ "action": "fetch", "params": {
|
|
1005
|
+
"method": "POST",
|
|
1006
|
+
"url": "{{API_URL}}",
|
|
1007
|
+
"headers": { "Prefer": "return=representation" },
|
|
1008
|
+
"body": {
|
|
1009
|
+
"{{FIELD_1}}": { "$state": "/form/{{FIELD_1}}" },
|
|
1010
|
+
"{{FIELD_2}}": { "$state": "/form/{{FIELD_2}}" }
|
|
1011
|
+
},
|
|
1012
|
+
"target": "/tx/result"
|
|
1013
|
+
}}
|
|
1014
|
+
],
|
|
1015
|
+
"onSuccess": [
|
|
1016
|
+
{ "action": "fetch", "params": {
|
|
1017
|
+
"method": "GET",
|
|
1018
|
+
"url": "{{API_URL}}?select=*&order=created_at.desc",
|
|
1019
|
+
"target": "{{ARRAY_PATH}}"
|
|
1020
|
+
}}
|
|
1021
|
+
],
|
|
1022
|
+
"onError": [
|
|
1023
|
+
{ "action": "showNotification", "params": { "type": "error", "message": "Could not create {{ENTITY}}. Please try again." } }
|
|
1024
|
+
]
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
#### UPDATE — Edit item in list
|
|
1030
|
+
|
|
1031
|
+
```json
|
|
1032
|
+
{
|
|
1033
|
+
"transaction": {
|
|
1034
|
+
"before": [
|
|
1035
|
+
{ "action": "closeModal", "params": { "id": "{{MODAL_ID}}" } }
|
|
1036
|
+
],
|
|
1037
|
+
"optimistic": [
|
|
1038
|
+
{ "action": "setState", "params": {
|
|
1039
|
+
"statePath": "{{ARRAY_PATH}}",
|
|
1040
|
+
"value": { "$array": "replace", "source": { "$state": "{{ARRAY_PATH}}" },
|
|
1041
|
+
"where": { "field": "id", "eq": { "$state": "/form/editId" } },
|
|
1042
|
+
"value": {
|
|
1043
|
+
"id": { "$state": "/form/editId" },
|
|
1044
|
+
"{{FIELD_1}}": { "$state": "/form/{{FIELD_1}}" },
|
|
1045
|
+
"{{FIELD_2}}": { "$state": "/form/{{FIELD_2}}" }
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}}
|
|
1049
|
+
],
|
|
1050
|
+
"confirm": [
|
|
1051
|
+
{ "action": "fetch", "params": {
|
|
1052
|
+
"method": "PATCH",
|
|
1053
|
+
"url": { "$template": "{{API_URL}}?id=eq.${/form/editId}" },
|
|
1054
|
+
"headers": { "Prefer": "return=representation" },
|
|
1055
|
+
"body": {
|
|
1056
|
+
"{{FIELD_1}}": { "$state": "/form/{{FIELD_1}}" },
|
|
1057
|
+
"{{FIELD_2}}": { "$state": "/form/{{FIELD_2}}" }
|
|
1058
|
+
},
|
|
1059
|
+
"target": "/tx/result"
|
|
1060
|
+
}}
|
|
1061
|
+
],
|
|
1062
|
+
"onError": [
|
|
1063
|
+
{ "action": "showNotification", "params": { "type": "error", "message": "Could not update {{ENTITY}}. Please try again." } }
|
|
1064
|
+
]
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
Note: UPDATE has no `onSuccess` — the optimistic data is already correct. A re-fetch would cause a visible flash.
|
|
1070
|
+
|
|
1071
|
+
#### DELETE — Remove item from list
|
|
1072
|
+
|
|
1073
|
+
```json
|
|
1074
|
+
{
|
|
1075
|
+
"transaction": {
|
|
1076
|
+
"before": [
|
|
1077
|
+
{ "action": "closeModal", "params": { "id": "{{CONFIRM_MODAL_ID}}" } }
|
|
1078
|
+
],
|
|
1079
|
+
"optimistic": [
|
|
1080
|
+
{ "action": "setState", "params": {
|
|
1081
|
+
"statePath": "{{ARRAY_PATH}}",
|
|
1082
|
+
"value": { "$array": "remove", "source": { "$state": "{{ARRAY_PATH}}" },
|
|
1083
|
+
"where": { "field": "id", "eq": { "$state": "/ui/deleteId" } }
|
|
1084
|
+
}
|
|
1085
|
+
}}
|
|
1086
|
+
],
|
|
1087
|
+
"confirm": [
|
|
1088
|
+
{ "action": "fetch", "params": {
|
|
1089
|
+
"method": "DELETE",
|
|
1090
|
+
"url": { "$template": "{{API_URL}}?id=eq.${/ui/deleteId}" }
|
|
1091
|
+
}}
|
|
1092
|
+
],
|
|
1093
|
+
"onError": [
|
|
1094
|
+
{ "action": "showNotification", "params": { "type": "error", "message": "Could not delete {{ENTITY}}. Please try again." } }
|
|
1095
|
+
]
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
#### TOGGLE — Flip a boolean value
|
|
1101
|
+
|
|
1102
|
+
```json
|
|
1103
|
+
{
|
|
1104
|
+
"transaction": {
|
|
1105
|
+
"optimistic": [
|
|
1106
|
+
{ "action": "setState", "params": {
|
|
1107
|
+
"statePath": "{{BOOLEAN_PATH}}",
|
|
1108
|
+
"value": { "$not": { "$state": "{{BOOLEAN_PATH}}" } }
|
|
1109
|
+
}}
|
|
1110
|
+
],
|
|
1111
|
+
"confirm": [
|
|
1112
|
+
{ "action": "fetch", "params": {
|
|
1113
|
+
"method": "PATCH",
|
|
1114
|
+
"url": { "$template": "{{API_URL}}?id=eq.{{ITEM_ID}}" },
|
|
1115
|
+
"body": { "{{FIELD}}": { "$state": "{{BOOLEAN_PATH}}" } }
|
|
1116
|
+
}}
|
|
1117
|
+
],
|
|
1118
|
+
"onError": [
|
|
1119
|
+
{ "action": "showNotification", "params": { "type": "error", "message": "Could not update. Please try again." } }
|
|
1120
|
+
]
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
Note: TOGGLE has no `before` — there's no modal to close.
|
|
1126
|
+
|
|
1127
|
+
### Legacy CRUD Pattern (without transactions)
|
|
1128
|
+
|
|
1129
|
+
For simple cases that don't need optimistic updates, use sequential action arrays:
|
|
1130
|
+
|
|
1131
|
+
```json
|
|
1132
|
+
{
|
|
1133
|
+
"on": {
|
|
1134
|
+
"press": [
|
|
1135
|
+
{ "action": "closeModal", "params": { "id": "edit-modal" } },
|
|
1136
|
+
{ "action": "fetch", "params": { "url": "...", "method": "PATCH", "body": { ... } } },
|
|
1137
|
+
{ "action": "fetch", "params": { "url": "...", "method": "GET", "target": "/items" } }
|
|
1138
|
+
]
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
```
|
|
1142
|
+
|
|
1143
|
+
The modal closes instantly. The PATCH waits for completion, then GET fetches fresh data. The user sees the list update ~500ms later. **Prefer transactions for instant UX.**
|
|
1144
|
+
|
|
1145
|
+
---
|
|
1146
|
+
|
|
1147
|
+
## Validation
|
|
1148
|
+
|
|
1149
|
+
Add validation to input fields:
|
|
1150
|
+
|
|
1151
|
+
```json
|
|
1152
|
+
{
|
|
1153
|
+
"type": "input",
|
|
1154
|
+
"props": {
|
|
1155
|
+
"value": { "$bindState": "/form/email" },
|
|
1156
|
+
"checks": [
|
|
1157
|
+
{ "type": "required", "message": "Email is required" },
|
|
1158
|
+
{ "type": "email", "message": "Invalid email format" }
|
|
1159
|
+
],
|
|
1160
|
+
"validateOn": "blur"
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
| Validator | Args | Purpose |
|
|
1166
|
+
|-----------|------|---------|
|
|
1167
|
+
| `required` | — | Non-empty value |
|
|
1168
|
+
| `email` | — | Valid email format |
|
|
1169
|
+
| `minLength` | `{ min }` | Minimum string length |
|
|
1170
|
+
| `maxLength` | `{ max }` | Maximum string length |
|
|
1171
|
+
| `pattern` | `{ pattern }` | Regex match |
|
|
1172
|
+
| `min` | `{ min }` | Minimum number |
|
|
1173
|
+
| `max` | `{ max }` | Maximum number |
|
|
1174
|
+
| `numeric` | — | Must be a number |
|
|
1175
|
+
| `url` | — | Valid URL |
|
|
1176
|
+
| `matches` | `{ other }` | Must equal another field |
|
|
1177
|
+
| `equalTo` | `{ other }` | Must equal another field |
|
|
1178
|
+
| `greaterThan` | `{ other }` | Must be greater than another field |
|
|
1179
|
+
| `lessThan` | `{ other }` | Must be less than another field |
|
|
1180
|
+
| `requiredIf` | `{ field }` | Required when condition field is truthy |
|
|
1181
|
+
|
|
1182
|
+
---
|
|
1183
|
+
|
|
1184
|
+
## Repeat (Lists)
|
|
1185
|
+
|
|
1186
|
+
Render an element once per item in an array:
|
|
1187
|
+
|
|
1188
|
+
```json
|
|
1189
|
+
"task-list": {
|
|
1190
|
+
"type": "stack",
|
|
1191
|
+
"repeat": { "statePath": "/tasks", "key": "id" },
|
|
1192
|
+
"children": ["task-row"]
|
|
1193
|
+
}
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
Use `source` for filtered/paginated lists:
|
|
1197
|
+
```json
|
|
1198
|
+
"repeat": {
|
|
1199
|
+
"source": {
|
|
1200
|
+
"$array": "slice",
|
|
1201
|
+
"source": {
|
|
1202
|
+
"$array": "filter",
|
|
1203
|
+
"source": { "$state": "/tasks" },
|
|
1204
|
+
"where": { "field": "status", "eq": { "$state": "/filter/status" } }
|
|
1205
|
+
},
|
|
1206
|
+
"from": { "$math": "multiply", "args": [{ "$state": "/page" }, 10] },
|
|
1207
|
+
"to": { "$math": "multiply", "args": [{ "$math": "add", "args": [{ "$state": "/page" }, 1] }, 10] }
|
|
1208
|
+
},
|
|
1209
|
+
"key": "id"
|
|
1210
|
+
}
|
|
1211
|
+
```
|
|
1212
|
+
|
|
1213
|
+
Inside repeated elements, use `$item` and `$index` to access the current item's data. These work in `props`, `style`, `visible`, and `on` (event params).
|
|
1214
|
+
|
|
1215
|
+
### Grouped Repeat (`groupBy`)
|
|
1216
|
+
|
|
1217
|
+
Two modes — auto-detected:
|
|
1218
|
+
|
|
1219
|
+
**Mode 1: Client-Side Grouping** — engine groups a flat array by field value:
|
|
1220
|
+
|
|
1221
|
+
```json
|
|
1222
|
+
"task-list": {
|
|
1223
|
+
"type": "stack",
|
|
1224
|
+
"repeat": {
|
|
1225
|
+
"source": { "$state": "/tasks" },
|
|
1226
|
+
"key": "id",
|
|
1227
|
+
"groupBy": "category",
|
|
1228
|
+
"groupHeader": ["category-header"],
|
|
1229
|
+
"groupFooter": ["category-subtotal"]
|
|
1230
|
+
},
|
|
1231
|
+
"children": ["task-row"]
|
|
1232
|
+
}
|
|
1233
|
+
```
|
|
1234
|
+
|
|
1235
|
+
**Mode 2: Pre-Grouped Data** — server already grouped the data:
|
|
1236
|
+
|
|
1237
|
+
```json
|
|
1238
|
+
"budget-table": {
|
|
1239
|
+
"type": "stack",
|
|
1240
|
+
"repeat": {
|
|
1241
|
+
"source": { "$state": "/response/groups" },
|
|
1242
|
+
"key": "name",
|
|
1243
|
+
"groupKey": "name",
|
|
1244
|
+
"groupItems": "rows",
|
|
1245
|
+
"groupHeader": ["institution-header"],
|
|
1246
|
+
"groupFooter": ["subtotal-row"],
|
|
1247
|
+
"footer": ["grand-total-row"]
|
|
1248
|
+
},
|
|
1249
|
+
"children": ["data-row"]
|
|
1250
|
+
}
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
Pre-grouped data format: `[{ "name": "Group A", "rows": [...], "subtotal": { "total": 50000 } }, ...]`
|
|
1254
|
+
|
|
1255
|
+
**Detection:** `groupBy` (string) → client-side. `groupKey` + `groupItems` → pre-grouped.
|
|
1256
|
+
|
|
1257
|
+
| Property | Type | Description |
|
|
1258
|
+
|----------|------|-------------|
|
|
1259
|
+
| `groupBy` | string | Field name for client-side grouping |
|
|
1260
|
+
| `groupKey` | string | Field in group objects containing the key (pre-grouped) |
|
|
1261
|
+
| `groupItems` | string | Field in group objects containing the items array (pre-grouped) |
|
|
1262
|
+
| `groupHeader` | string[] | Element IDs to render as group header (receives `$group` context) |
|
|
1263
|
+
| `groupFooter` | string[] | Element IDs to render as group footer (receives `$group` context) |
|
|
1264
|
+
| `footer` | string[] | Element IDs to render once after all groups |
|
|
1265
|
+
|
|
1266
|
+
Inside `groupHeader` and `groupFooter`, use `$group` expressions. Inside `children`, use `$item`/`$index` as usual.
|
|
1267
|
+
|
|
1268
|
+
### Repeat Selection (`selection`)
|
|
1269
|
+
|
|
1270
|
+
Adds multi-select behavior to a repeat:
|
|
1271
|
+
|
|
1272
|
+
```json
|
|
1273
|
+
"task-list": {
|
|
1274
|
+
"type": "stack",
|
|
1275
|
+
"repeat": {
|
|
1276
|
+
"source": { "$state": "/tasks" },
|
|
1277
|
+
"key": "id",
|
|
1278
|
+
"selection": {
|
|
1279
|
+
"state": "/selectedIds",
|
|
1280
|
+
"mode": "multiple"
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
"children": ["task-row"]
|
|
1284
|
+
}
|
|
1285
|
+
```
|
|
1286
|
+
|
|
1287
|
+
| Property | Type | Description |
|
|
1288
|
+
|----------|------|-------------|
|
|
1289
|
+
| `state` | string | State path for selected IDs array |
|
|
1290
|
+
| `key` | string | Field from each item used as the selection identifier |
|
|
1291
|
+
| `mode` | `"single"` \| `"multiple"` | Single selects one at a time, multiple allows many |
|
|
1292
|
+
|
|
1293
|
+
Inside the repeat, use `$selection` expressions:
|
|
1294
|
+
```json
|
|
1295
|
+
"select-checkbox": {
|
|
1296
|
+
"type": "checkbox",
|
|
1297
|
+
"props": { "value": { "$selection": "selected" } },
|
|
1298
|
+
"on": { "change": { "action": "toggleSelection", "params": { "statePath": "/selectedIds", "value": { "$item": "id" } } } }
|
|
1299
|
+
}
|
|
1300
|
+
```
|
|
1301
|
+
|
|
1302
|
+
**Selection helper actions:**
|
|
1303
|
+
|
|
1304
|
+
| Action | Params | Description |
|
|
1305
|
+
|--------|--------|-------------|
|
|
1306
|
+
| `toggleSelection` | `{ statePath, value, mode? }` | Add/remove value from selection array. `mode: "single"` replaces |
|
|
1307
|
+
| `selectAll` | `{ statePath, values }` | Set selection to provided values array |
|
|
1308
|
+
| `selectNone` | `{ statePath }` | Clear selection array |
|
|
1309
|
+
|
|
1310
|
+
---
|
|
1311
|
+
|
|
1312
|
+
## Visibility Conditions
|
|
1313
|
+
|
|
1314
|
+
```json
|
|
1315
|
+
{ "$state": "/user/role", "eq": "admin" }
|
|
1316
|
+
[{ "$state": "/form/isValid" }, { "$state": "/form/hasChanges" }]
|
|
1317
|
+
{ "$or": [{ "$state": "/user/isVIP" }, { "$state": "/cart/total", "gt": 200 }] }
|
|
1318
|
+
{ "$and": [{ "$state": "/user/active" }, { "$state": "/user/verified" }] }
|
|
1319
|
+
```
|
|
1320
|
+
|
|
1321
|
+
### Loading/Content/Empty Pattern
|
|
1322
|
+
|
|
1323
|
+
For data-driven screens, use this visibility pattern:
|
|
1324
|
+
|
|
1325
|
+
```json
|
|
1326
|
+
"loading-section": {
|
|
1327
|
+
"visible": { "$and": [{ "$state": "/ui/loading" }, { "$not": { "$array": "count", "source": { "$state": "/items" } } }] }
|
|
1328
|
+
},
|
|
1329
|
+
"content-section": {
|
|
1330
|
+
"visible": { "$and": [{ "$not": { "$state": "/ui/lastError" } }, { "$array": "count", "source": { "$state": "/items" } }] }
|
|
1331
|
+
},
|
|
1332
|
+
"empty-section": {
|
|
1333
|
+
"visible": { "$and": [{ "$not": { "$state": "/ui/loading" } }, { "$not": { "$state": "/ui/lastError" } }, { "$not": { "$array": "count", "source": { "$state": "/items" } } }] }
|
|
1334
|
+
},
|
|
1335
|
+
"error-section": {
|
|
1336
|
+
"visible": { "$state": "/ui/lastError" }
|
|
1337
|
+
}
|
|
1338
|
+
```
|
|
1339
|
+
|
|
1340
|
+
**Key:** Loading skeleton shows only on initial load (loading AND no data). Content stays visible during background re-fetches (data exists, regardless of loading state).
|
|
1341
|
+
|
|
1342
|
+
---
|
|
1343
|
+
|
|
1344
|
+
## Derived State (`derive`)
|
|
1345
|
+
|
|
1346
|
+
Auto-computed state values that update reactively when their dependencies change. Declared at the spec level.
|
|
1347
|
+
|
|
1348
|
+
```json
|
|
1349
|
+
{
|
|
1350
|
+
"root": "...",
|
|
1351
|
+
"elements": { "..." },
|
|
1352
|
+
"derive": {
|
|
1353
|
+
"/stats/totalItems": {
|
|
1354
|
+
"$array": "count", "source": { "$state": "/items" }
|
|
1355
|
+
},
|
|
1356
|
+
"/stats/avgPrice": {
|
|
1357
|
+
"$math": "divide", "args": [
|
|
1358
|
+
{ "$array": "sum", "source": { "$state": "/items" }, "field": "price" },
|
|
1359
|
+
{ "$array": "count", "source": { "$state": "/items" } }
|
|
1360
|
+
]
|
|
1361
|
+
},
|
|
1362
|
+
"/stats/completionRate": {
|
|
1363
|
+
"$math": "multiply", "args": [
|
|
1364
|
+
{ "$math": "divide", "args": [
|
|
1365
|
+
{ "$array": "count", "source": { "$state": "/tasks" }, "where": { "field": "status", "eq": "completed" } },
|
|
1366
|
+
{ "$array": "count", "source": { "$state": "/tasks" } }
|
|
1367
|
+
]},
|
|
1368
|
+
100
|
|
1369
|
+
]
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
```
|
|
1374
|
+
|
|
1375
|
+
**How it works:**
|
|
1376
|
+
1. On spec mount: all derive expressions are evaluated in dependency order (topological sort)
|
|
1377
|
+
2. On state change: only derives whose dependencies changed re-evaluate
|
|
1378
|
+
3. Derive paths are **read-only** — `setState` to a derive path is blocked by StateGuard
|
|
1379
|
+
4. Circular dependencies are detected at mount time (throws error, not silent loop)
|
|
1380
|
+
|
|
1381
|
+
**Derive A can reference Derive B** — as long as there's no cycle. The engine evaluates them in topological order.
|
|
1382
|
+
|
|
1383
|
+
---
|
|
1384
|
+
|
|
1385
|
+
## Form Validation (`forms`)
|
|
1386
|
+
|
|
1387
|
+
Declarative form-level validation. Coordinates field validation as a unit, tracks form-level validity, supports cross-field rules, and gates `submitForm`.
|
|
1388
|
+
|
|
1389
|
+
```json
|
|
1390
|
+
{
|
|
1391
|
+
"forms": {
|
|
1392
|
+
"task-form": {
|
|
1393
|
+
"fields": {
|
|
1394
|
+
"title": {
|
|
1395
|
+
"statePath": "/form/title",
|
|
1396
|
+
"rules": [{ "type": "required", "message": "Title is required" }]
|
|
1397
|
+
},
|
|
1398
|
+
"email": {
|
|
1399
|
+
"statePath": "/form/email",
|
|
1400
|
+
"rules": [{ "type": "required" }, { "type": "email", "message": "Invalid email" }]
|
|
1401
|
+
},
|
|
1402
|
+
"maxPrice": {
|
|
1403
|
+
"statePath": "/form/maxPrice",
|
|
1404
|
+
"rules": [
|
|
1405
|
+
{ "type": "greaterThan", "args": { "other": { "$state": "/form/minPrice" } }, "message": "Max must exceed min" }
|
|
1406
|
+
]
|
|
1407
|
+
}
|
|
1408
|
+
},
|
|
1409
|
+
"validateOn": "blur"
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
```
|
|
1414
|
+
|
|
1415
|
+
### Form State (auto-generated)
|
|
1416
|
+
|
|
1417
|
+
| Path | Type | Description |
|
|
1418
|
+
|------|------|-------------|
|
|
1419
|
+
| `/ui/forms/{formId}/isValid` | boolean | True when ALL fields pass validation |
|
|
1420
|
+
| `/ui/forms/{formId}/errorCount` | number | Total displayed errors across all fields |
|
|
1421
|
+
| `/ui/forms/{formId}/isDirty` | boolean | True when any field value differs from initial |
|
|
1422
|
+
| `/ui/forms/{formId}/fields/{fieldId}/errors` | string[] | Error messages (empty when valid) |
|
|
1423
|
+
| `/ui/forms/{formId}/fields/{fieldId}/touched` | boolean | User has interacted with this field |
|
|
1424
|
+
| `/ui/forms/{formId}/fields/{fieldId}/dirty` | boolean | Value differs from initial |
|
|
1425
|
+
|
|
1426
|
+
**Key behavior:** `isValid` reflects actual validity even for untouched fields (for submit button state). But `errors` are only shown for touched fields (no error messages on fields the user hasn't interacted with yet).
|
|
1427
|
+
|
|
1428
|
+
### Form Actions
|
|
1429
|
+
|
|
1430
|
+
| Action | Params | Description |
|
|
1431
|
+
|--------|--------|-------------|
|
|
1432
|
+
| `validateForm` | `{ formId }` | Mark all fields as touched, validate all, write errors |
|
|
1433
|
+
| `touchField` | `{ formId, field }` | Mark one field as touched and validate it |
|
|
1434
|
+
| `resetForm` | `{ formId }` | Restore initial values, clear all errors/touched/dirty |
|
|
1435
|
+
| `submitForm` | `{ formId, url, ... }` | When `formId` present: validates first, blocks if invalid |
|
|
1436
|
+
|
|
1437
|
+
### Spec Author Usage
|
|
1438
|
+
|
|
1439
|
+
```json
|
|
1440
|
+
// Disable submit until form is valid
|
|
1441
|
+
"disabled": { "$not": { "$state": "/ui/forms/task-form/isValid" } }
|
|
1442
|
+
|
|
1443
|
+
// Show inline field error
|
|
1444
|
+
"error-text": {
|
|
1445
|
+
"type": "text",
|
|
1446
|
+
"visible": { "$array": "count", "source": { "$state": "/ui/forms/task-form/fields/title/errors" } },
|
|
1447
|
+
"props": {
|
|
1448
|
+
"content": { "$array": "first", "source": { "$state": "/ui/forms/task-form/fields/title/errors" } },
|
|
1449
|
+
"style": { "color": "#EF4444", "fontSize": 12 }
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// Submit with validation gate
|
|
1454
|
+
{ "action": "submitForm", "params": { "formId": "task-form", "url": "...", "method": "POST", "body": {...} } }
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
### Compatibility
|
|
1458
|
+
|
|
1459
|
+
Forms are 100% opt-in. Existing specs with inline `checks` on inputs continue working. `submitForm` without `formId` behaves exactly as before.
|
|
1460
|
+
|
|
1461
|
+
---
|
|
1462
|
+
|
|
1463
|
+
## Data Sources (`dataSources`)
|
|
1464
|
+
|
|
1465
|
+
Declarative reactive data fetching. Makes specs self-contained — no host-app code needed for GET requests.
|
|
1466
|
+
|
|
1467
|
+
```json
|
|
1468
|
+
{
|
|
1469
|
+
"root": "...",
|
|
1470
|
+
"elements": { "..." },
|
|
1471
|
+
"dataSources": {
|
|
1472
|
+
"tasks": {
|
|
1473
|
+
"url": { "$template": "${/config/apiUrl}/rest/v1/tasks?select=*&order=created_at.desc" },
|
|
1474
|
+
"method": "GET",
|
|
1475
|
+
"headers": { "$state": "/config/headers" },
|
|
1476
|
+
"params": {
|
|
1477
|
+
"status": { "$state": "/filter/status" },
|
|
1478
|
+
"page": { "$state": "/pagination/page" },
|
|
1479
|
+
"search": { "$state": "/filter/search" }
|
|
1480
|
+
},
|
|
1481
|
+
"target": "/tasks",
|
|
1482
|
+
"trigger": "auto",
|
|
1483
|
+
"debounce": 300,
|
|
1484
|
+
"initialFetch": true,
|
|
1485
|
+
"emptyWhileLoading": false
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
| Property | Type | Default | Description |
|
|
1492
|
+
|----------|------|---------|-------------|
|
|
1493
|
+
| `url` | Expression | — | Request URL (supports `$template`, `$state`) |
|
|
1494
|
+
| `method` | string | `"GET"` | HTTP method |
|
|
1495
|
+
| `headers` | Expression | — | Request headers (from state, NOT from spec — keys must not live in the database) |
|
|
1496
|
+
| `params` | `Record<string, Expression>` | — | Reactive parameters. Changes trigger re-fetch |
|
|
1497
|
+
| `target` | string | — | State path where response is written |
|
|
1498
|
+
| `trigger` | `"auto"` \| `"manual"` | `"auto"` | `auto`: re-fetch when params change. `manual`: only via `refreshDataSource` action |
|
|
1499
|
+
| `debounce` | number | `0` | Debounce time in ms for auto-trigger |
|
|
1500
|
+
| `initialFetch` | boolean | `true` | Fetch on spec mount |
|
|
1501
|
+
| `emptyWhileLoading` | boolean | `false` | If `true`, clear target while loading. If `false`, keep previous data |
|
|
1502
|
+
|
|
1503
|
+
**Auto-generated state paths** for a dataSource with `"target": "/tasks"`:
|
|
1504
|
+
- `/tasks` — response data
|
|
1505
|
+
- `/tasksLoading` — boolean
|
|
1506
|
+
- `/tasksError` — error object or null
|
|
1507
|
+
|
|
1508
|
+
**Param filtering:** Values `null`, `undefined`, `""`, or `"all"` are omitted from the query string (inactive filter).
|
|
1509
|
+
|
|
1510
|
+
**Force re-fetch action:**
|
|
1511
|
+
```json
|
|
1512
|
+
{ "action": "refreshDataSource", "params": { "id": "tasks" } }
|
|
1513
|
+
```
|
|
1514
|
+
|
|
1515
|
+
**Security:** Headers come from state, not from spec. API keys must NOT be in the spec (which lives in the database). The host app injects them via `initialState`:
|
|
1516
|
+
```json
|
|
1517
|
+
"sharedState": {
|
|
1518
|
+
"config": { "headers": { "apikey": "...", "Authorization": "Bearer ..." } }
|
|
1519
|
+
}
|
|
1520
|
+
```
|
|
1521
|
+
|
|
1522
|
+
**Interaction with `fetch` action:** `dataSources` handles reactive GET. `fetch` action handles one-shot operations (POST/PATCH/DELETE in transactions). They complement each other.
|
|
1523
|
+
|
|
1524
|
+
---
|
|
1525
|
+
|
|
1526
|
+
## Element Templates (`templates`)
|
|
1527
|
+
|
|
1528
|
+
Reusable element definitions within specs. Reduces repetition for patterns like monetary columns, stat cards, etc.
|
|
1529
|
+
|
|
1530
|
+
```json
|
|
1531
|
+
{
|
|
1532
|
+
"templates": {
|
|
1533
|
+
"monetary-col": {
|
|
1534
|
+
"type": "text",
|
|
1535
|
+
"defaults": { "color": "#0F172A", "bold": 400, "currency": "HNL" },
|
|
1536
|
+
"props": {
|
|
1537
|
+
"content": {
|
|
1538
|
+
"$format": "currency",
|
|
1539
|
+
"value": { "$prop": "value" },
|
|
1540
|
+
"currency": { "$prop": "currency" },
|
|
1541
|
+
"locale": "es-HN"
|
|
1542
|
+
}
|
|
1543
|
+
},
|
|
1544
|
+
"style": {
|
|
1545
|
+
"textAlign": "right",
|
|
1546
|
+
"fontFamily": "monospace",
|
|
1547
|
+
"fontSize": 13,
|
|
1548
|
+
"fontWeight": { "$prop": "bold" },
|
|
1549
|
+
"color": { "$prop": "color" }
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
},
|
|
1553
|
+
"elements": {
|
|
1554
|
+
"col-votado": {
|
|
1555
|
+
"type": "monetary-col",
|
|
1556
|
+
"props": { "value": { "$item": "allocatedAmount" } }
|
|
1557
|
+
},
|
|
1558
|
+
"col-mod": {
|
|
1559
|
+
"type": "monetary-col",
|
|
1560
|
+
"props": { "value": { "$item": "modificaciones" }, "color": "#D97706", "bold": 600 }
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1566
|
+
**How it works:**
|
|
1567
|
+
1. Engine encounters element with `type: "monetary-col"` (not a registered primitive)
|
|
1568
|
+
2. Checks `spec.templates` → found
|
|
1569
|
+
3. Merges element props with template `defaults` (element props win)
|
|
1570
|
+
4. Expands template: replaces `$prop` references with merged props
|
|
1571
|
+
5. Renders the resulting element as if defined inline
|
|
1572
|
+
|
|
1573
|
+
**Template properties:**
|
|
1574
|
+
|
|
1575
|
+
| Property | Type | Description |
|
|
1576
|
+
|----------|------|-------------|
|
|
1577
|
+
| `type` | string | Primitive type this template wraps |
|
|
1578
|
+
| `defaults` | `Record<string, unknown>` | Default prop values |
|
|
1579
|
+
| `props` | object | Props using `$prop` for parameterization |
|
|
1580
|
+
| `style` | object | Style using `$prop` |
|
|
1581
|
+
| `children` | `string[]` | Child element IDs. Use `"$children"` marker to insert the consuming element's children |
|
|
1582
|
+
|
|
1583
|
+
Templates support all Element fields: `visible`, `hover`, `active`, `motion`, etc.
|
|
1584
|
+
|
|
1585
|
+
**Templates with children:**
|
|
1586
|
+
```json
|
|
1587
|
+
{
|
|
1588
|
+
"templates": {
|
|
1589
|
+
"card": {
|
|
1590
|
+
"type": "box",
|
|
1591
|
+
"defaults": { "padding": 16 },
|
|
1592
|
+
"style": { "padding": { "$prop": "padding" }, "borderRadius": 12, "backgroundColor": { "$token": "colors.surface" } },
|
|
1593
|
+
"children": ["$children"]
|
|
1594
|
+
}
|
|
1595
|
+
},
|
|
1596
|
+
"elements": {
|
|
1597
|
+
"my-card": { "type": "card", "children": ["title", "body"] }
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
```
|
|
1601
|
+
|
|
1602
|
+
`"$children"` is replaced with the consuming element's actual children at expansion time.
|
|
1603
|
+
|
|
1604
|
+
**Sharing:** Templates defined in an AppSpec are available to all screens. Screen-level templates override app-level templates.
|
|
1605
|
+
|
|
1606
|
+
---
|
|
1607
|
+
|
|
1608
|
+
## App Spec & Screen Outlet
|
|
1609
|
+
|
|
1610
|
+
`AppSpec` defines the app-level layout, navigation, shared state, and screen registry. Screens render inside a `screen-outlet` pseudo-primitive. **100% opt-in** — without AppSpec, everything works as today.
|
|
1611
|
+
|
|
1612
|
+
```json
|
|
1613
|
+
{
|
|
1614
|
+
"type": "app",
|
|
1615
|
+
"name": "my-application",
|
|
1616
|
+
"navigation": {
|
|
1617
|
+
"type": "sidebar",
|
|
1618
|
+
"initialScreen": "task-manager",
|
|
1619
|
+
"breadcrumb": "history",
|
|
1620
|
+
"auth": {
|
|
1621
|
+
"loginScreen": "login",
|
|
1622
|
+
"protectedScreens": ["*"],
|
|
1623
|
+
"roleAccess": { "admin": ["*"], "user": ["task-manager", "profile"] }
|
|
1624
|
+
}
|
|
1625
|
+
},
|
|
1626
|
+
"screens": {
|
|
1627
|
+
"task-manager": {
|
|
1628
|
+
"label": "Task Manager",
|
|
1629
|
+
"icon": "clipboard-text",
|
|
1630
|
+
"roles": ["admin", "user"],
|
|
1631
|
+
"statePolicy": "preserve"
|
|
1632
|
+
},
|
|
1633
|
+
"new-task": {
|
|
1634
|
+
"label": "New Task",
|
|
1635
|
+
"icon": "plus",
|
|
1636
|
+
"roles": ["admin", "user"],
|
|
1637
|
+
"statePolicy": "reset",
|
|
1638
|
+
"parent": "task-manager"
|
|
1639
|
+
}
|
|
1640
|
+
},
|
|
1641
|
+
"tokens": { "colors": { "primary": "#0D9488" } },
|
|
1642
|
+
"translations": { "en": { "..." }, "es": { "..." } },
|
|
1643
|
+
"sharedState": { "preferences": { "theme": "light", "locale": "en" } },
|
|
1644
|
+
"templates": { "monetary-col": { "..." } },
|
|
1645
|
+
"layout": {
|
|
1646
|
+
"root": "app-shell",
|
|
1647
|
+
"elements": {
|
|
1648
|
+
"app-shell": { "type": "stack", "props": { "direction": "horizontal" }, "children": ["sidebar", "screen-outlet"] },
|
|
1649
|
+
"sidebar": { "type": "box", "children": ["nav-menu"] },
|
|
1650
|
+
"screen-outlet": { "type": "screen-outlet", "style": { "flex": 1 } }
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
```
|
|
1655
|
+
|
|
1656
|
+
### Screen Definition
|
|
1657
|
+
|
|
1658
|
+
| Property | Type | Default | Description |
|
|
1659
|
+
|----------|------|---------|-------------|
|
|
1660
|
+
| `label` | string or Expression | — | Display name (sidebar, breadcrumb) |
|
|
1661
|
+
| `icon` | string | — | Icon name (kebab-case) |
|
|
1662
|
+
| `roles` | `string[]` | `["*"]` | Which roles can access this screen (used only when `roleAccess` is NOT defined) |
|
|
1663
|
+
| `statePolicy` | `"preserve"` \| `"reset"` \| `"reload"` | `"preserve"` | State behavior on navigation |
|
|
1664
|
+
| `parent` | string | — | Parent screen ID (for hierarchy breadcrumb) |
|
|
1665
|
+
|
|
1666
|
+
### State Policies
|
|
1667
|
+
|
|
1668
|
+
| Policy | On navigate away | On navigate back | Use case |
|
|
1669
|
+
|--------|-----------------|-----------------|----------|
|
|
1670
|
+
| `preserve` | State kept | Restores previous state | Lists, dashboards |
|
|
1671
|
+
| `reset` | State cleared | Fresh initialState | Create forms |
|
|
1672
|
+
| `reload` | State cleared | initialActions re-execute | Always-fresh data |
|
|
1673
|
+
|
|
1674
|
+
### Inheritance
|
|
1675
|
+
|
|
1676
|
+
Screens inherit from AppSpec:
|
|
1677
|
+
- **Tokens**: deep merge (screen overrides app)
|
|
1678
|
+
- **Translations**: merged (screen extends app)
|
|
1679
|
+
- **Templates**: screen overrides app with same name
|
|
1680
|
+
|
|
1681
|
+
### Auto-Generated State
|
|
1682
|
+
|
|
1683
|
+
| Path | Content |
|
|
1684
|
+
|------|---------|
|
|
1685
|
+
| `/navigation/currentScreen` | Active screen ID |
|
|
1686
|
+
| `/navigation/history` | Array of visited screen IDs |
|
|
1687
|
+
| `/navigation/breadcrumb` | Enriched array with label, icon |
|
|
1688
|
+
| `/app/screens` | Array of accessible screen definitions (filtered by role, excludes loginScreen) |
|
|
1689
|
+
|
|
1690
|
+
**Sidebar auto-filtering:** `/app/screens` only includes screens the current user can access. The loginScreen is always excluded from the sidebar. When the user logs in or roles change, call `refreshScreenList()` on the AppEngine to re-compute the sidebar.
|
|
1691
|
+
|
|
1692
|
+
### Breadcrumb Modes
|
|
1693
|
+
|
|
1694
|
+
| Mode | Behavior |
|
|
1695
|
+
|------|----------|
|
|
1696
|
+
| `"history"` | Follows navigation stack |
|
|
1697
|
+
| `"hierarchy"` | Follows `parent` chain from current screen to root |
|
|
1698
|
+
| `"none"` | No auto-computed breadcrumb |
|
|
1699
|
+
|
|
1700
|
+
### Access Control — roleAccess vs Screen Roles
|
|
1701
|
+
|
|
1702
|
+
Two models exist for controlling screen access. **Only one is active per app** — they don't mix.
|
|
1703
|
+
|
|
1704
|
+
**Model 1: `roleAccess` map (recommended for multi-role apps)**
|
|
1705
|
+
|
|
1706
|
+
Define in `navigation.auth.roleAccess`. This is the **source of truth** when present — `ScreenDefinition.roles` is ignored.
|
|
1707
|
+
|
|
1708
|
+
```json
|
|
1709
|
+
"roleAccess": {
|
|
1710
|
+
"admin": ["*"],
|
|
1711
|
+
"user": ["task-manager", "dashboard", "profile"],
|
|
1712
|
+
"viewer": ["dashboard"]
|
|
1713
|
+
}
|
|
1714
|
+
```
|
|
1715
|
+
|
|
1716
|
+
- A role listed with `["*"]` can access every screen.
|
|
1717
|
+
- A role NOT listed in `roleAccess` has **zero access** (denied everywhere).
|
|
1718
|
+
- The loginScreen is always accessible regardless of roleAccess — prevents infinite redirect loops.
|
|
1719
|
+
|
|
1720
|
+
**Model 2: `ScreenDefinition.roles` (backward compatible)**
|
|
1721
|
+
|
|
1722
|
+
When `roleAccess` is NOT defined (or undefined), each screen's `roles` array controls access:
|
|
1723
|
+
|
|
1724
|
+
```json
|
|
1725
|
+
"screens": {
|
|
1726
|
+
"dashboard": { "label": "Dashboard", "roles": ["admin", "user", "viewer"] },
|
|
1727
|
+
"admin-panel": { "label": "Admin", "roles": ["admin"] }
|
|
1728
|
+
}
|
|
1729
|
+
```
|
|
1730
|
+
|
|
1731
|
+
- `roles: ["*"]` (default) means any role can access the screen.
|
|
1732
|
+
|
|
1733
|
+
**Navigation behavior on access denial:**
|
|
1734
|
+
|
|
1735
|
+
| User state | Behavior |
|
|
1736
|
+
|------------|----------|
|
|
1737
|
+
| **Unauthenticated** (no role, no `/auth/isAuthenticated`) | Redirect to `loginScreen` |
|
|
1738
|
+
| **Authenticated but unauthorized** (`/auth/isAuthenticated` is true, role doesn't match) | Stay on current screen, set `/auth/error` with denial message — **no redirect to login** |
|
|
1739
|
+
|
|
1740
|
+
**Sidebar auto-refresh:**
|
|
1741
|
+
|
|
1742
|
+
`MythikApp` automatically re-computes `/app/screens` when auth state changes (`/auth/user/*` or `/auth/isAuthenticated`). No manual call needed — login, logout, session restore, cross-tab sync, and role changes all trigger sidebar refresh automatically. If building a custom host app without `MythikApp`, call `appEngine.refreshScreenList()` after auth state changes.
|
|
1743
|
+
|
|
1744
|
+
---
|
|
1745
|
+
|
|
1746
|
+
## Interactive States
|
|
1747
|
+
|
|
1748
|
+
Add visual feedback to elements with `hover`, `active`, `focus`, and `transition`. Zero overhead for elements without interactions.
|
|
1749
|
+
|
|
1750
|
+
**CSS vs Motion — automatic detection:** If hover/active/focus contains any of these 6 transform properties → Motion wrapper: `scale`, `scaleX`, `scaleY`, `rotate`, `x`, `y`. Everything else → CSS `:hover`/`:active`/`:focus-visible` pseudo-classes (no wrapper, no extra DOM node).
|
|
1751
|
+
|
|
1752
|
+
### Hover — pointer enters
|
|
1753
|
+
|
|
1754
|
+
```json
|
|
1755
|
+
"hover": { "scale": 1.03, "backgroundColor": { "$token": "colors.primaryHover" } }
|
|
1756
|
+
```
|
|
1757
|
+
|
|
1758
|
+
### Active — element is pressed
|
|
1759
|
+
|
|
1760
|
+
```json
|
|
1761
|
+
"active": { "scale": 0.97 }
|
|
1762
|
+
```
|
|
1763
|
+
|
|
1764
|
+
### Focus — keyboard focus
|
|
1765
|
+
|
|
1766
|
+
```json
|
|
1767
|
+
"focus": { "boxShadow": "0 0 0 3px rgba(13,148,136,0.3)" }
|
|
1768
|
+
```
|
|
1769
|
+
|
|
1770
|
+
### Transition — how changes animate
|
|
1771
|
+
|
|
1772
|
+
```json
|
|
1773
|
+
"transition": { "duration": 200, "ease": "easeOut" }
|
|
1774
|
+
```
|
|
1775
|
+
|
|
1776
|
+
### Transform shorthand
|
|
1777
|
+
|
|
1778
|
+
Use `scale`, `x`, `y`, `rotate` directly — no CSS transform strings needed:
|
|
1779
|
+
|
|
1780
|
+
| Property | Effect |
|
|
1781
|
+
|----------|--------|
|
|
1782
|
+
| `scale` | Scale up/down |
|
|
1783
|
+
| `x` | Move horizontally |
|
|
1784
|
+
| `y` | Move vertically |
|
|
1785
|
+
| `rotate` | Rotate in degrees |
|
|
1786
|
+
| `scaleX`, `scaleY` | Axis-specific scale |
|
|
1787
|
+
|
|
1788
|
+
### Complete example
|
|
1789
|
+
|
|
1790
|
+
```json
|
|
1791
|
+
"save-btn": {
|
|
1792
|
+
"type": "button",
|
|
1793
|
+
"props": { "label": "Save Changes" },
|
|
1794
|
+
"style": { "backgroundColor": { "$token": "colors.primary" }, "color": "#fff", "padding": "10px 20px", "borderRadius": 10 },
|
|
1795
|
+
"hover": { "scale": 1.03, "y": -1 },
|
|
1796
|
+
"active": { "scale": 0.97 },
|
|
1797
|
+
"focus": { "boxShadow": "0 0 0 3px rgba(13,148,136,0.3)" },
|
|
1798
|
+
"transition": { "duration": 150, "ease": "easeOut" }
|
|
1799
|
+
}
|
|
1800
|
+
```
|
|
1801
|
+
|
|
1802
|
+
All fields support `$token`, `$state`, `$cond`, and other expressions.
|
|
1803
|
+
|
|
1804
|
+
---
|
|
1805
|
+
|
|
1806
|
+
## Animations
|
|
1807
|
+
|
|
1808
|
+
### Mount animation (fade in, slide up, etc.)
|
|
1809
|
+
|
|
1810
|
+
```json
|
|
1811
|
+
"card": {
|
|
1812
|
+
"type": "box",
|
|
1813
|
+
"props": {},
|
|
1814
|
+
"motion": {
|
|
1815
|
+
"initial": { "opacity": 0, "y": 20 },
|
|
1816
|
+
"animate": { "opacity": 1, "y": 0 },
|
|
1817
|
+
"transition": { "duration": 0.3, "ease": "easeOut" }
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
```
|
|
1821
|
+
|
|
1822
|
+
### Exit animation
|
|
1823
|
+
|
|
1824
|
+
```json
|
|
1825
|
+
"motion": {
|
|
1826
|
+
"initial": { "opacity": 0 },
|
|
1827
|
+
"animate": { "opacity": 1 },
|
|
1828
|
+
"exit": { "opacity": 0, "y": -10 }
|
|
1829
|
+
}
|
|
1830
|
+
```
|
|
1831
|
+
|
|
1832
|
+
### Staggered children
|
|
1833
|
+
|
|
1834
|
+
Parent defines stagger, children animate in sequence:
|
|
1835
|
+
|
|
1836
|
+
```json
|
|
1837
|
+
"list": {
|
|
1838
|
+
"type": "stack",
|
|
1839
|
+
"motion": {
|
|
1840
|
+
"animate": {},
|
|
1841
|
+
"transition": { "staggerChildren": 0.05 }
|
|
1842
|
+
},
|
|
1843
|
+
"children": ["card-1", "card-2", "card-3"]
|
|
1844
|
+
}
|
|
1845
|
+
```
|
|
1846
|
+
|
|
1847
|
+
Each child should have its own `motion.initial`/`motion.animate`.
|
|
1848
|
+
|
|
1849
|
+
### Motion design tokens
|
|
1850
|
+
|
|
1851
|
+
Define once at app level alongside `colors`, `spacing`, `radius`:
|
|
1852
|
+
|
|
1853
|
+
```json
|
|
1854
|
+
{
|
|
1855
|
+
"tokens": {
|
|
1856
|
+
"motion": {
|
|
1857
|
+
"duration": { "fast": 150, "normal": 250, "slow": 400 },
|
|
1858
|
+
"ease": {
|
|
1859
|
+
"default": "easeOut",
|
|
1860
|
+
"spring": { "type": "spring", "stiffness": 300, "damping": 20 },
|
|
1861
|
+
"smooth": [0.16, 1, 0.3, 1]
|
|
1862
|
+
},
|
|
1863
|
+
"presets": {
|
|
1864
|
+
"hoverLift": { "y": -2, "scale": 1.02 },
|
|
1865
|
+
"hoverGlow": { "boxShadow": "0 0 0 3px rgba(13,148,136,0.2)" },
|
|
1866
|
+
"press": { "scale": 0.97 },
|
|
1867
|
+
"fadeIn": { "initial": { "opacity": 0 }, "animate": { "opacity": 1 } },
|
|
1868
|
+
"slideUp": { "initial": { "opacity": 0, "y": 20 }, "animate": { "opacity": 1, "y": 0 } },
|
|
1869
|
+
"scaleIn": { "initial": { "scale": 0.9, "opacity": 0 }, "animate": { "scale": 1, "opacity": 1 } }
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
```
|
|
1875
|
+
|
|
1876
|
+
Reference tokens or write custom values:
|
|
1877
|
+
|
|
1878
|
+
```json
|
|
1879
|
+
"hover": { "$token": "motion.presets.hoverLift" }
|
|
1880
|
+
"transition": { "duration": { "$token": "motion.duration.fast" } }
|
|
1881
|
+
```
|
|
1882
|
+
|
|
1883
|
+
---
|
|
1884
|
+
|
|
1885
|
+
## Rules
|
|
1886
|
+
|
|
1887
|
+
1. **Always use the flat tree structure** — root + elements map with string IDs for children
|
|
1888
|
+
2. **Use `$token` for all visual values** — never hardcode colors, spacing, fonts
|
|
1889
|
+
3. **Use `$bindState` for form inputs** — enables two-way data binding
|
|
1890
|
+
4. **Use `$cond` for dynamic styles** — conditional colors, sizes, visibility
|
|
1891
|
+
5. **Use `$template` for computed strings** — interpolation over concatenation
|
|
1892
|
+
6. **Use `$math`, `$array`, `$date`, `$format` instead of `$computed`** — built-in operations cover 90%+ of cases without code
|
|
1893
|
+
7. **Use `$and`/`$or`/`$not` for boolean logic** — works in `disabled`, `style`, `visible`, anywhere
|
|
1894
|
+
8. **Use `$array: 'search'` for filtering lists** — case-insensitive multi-field search
|
|
1895
|
+
9. **Use `repeat.source` for paginated/filtered lists** — chain `$array: 'filter'` → `$array: 'search'` → `$array: 'slice'`
|
|
1896
|
+
10. **Don't add `visible` to modals/drawers** — the engine manages it automatically via `/ui/modals/{id}`
|
|
1897
|
+
11. **Use `$let` array format when stored in JSONB** — `[["a", expr], ["b", expr]]` preserves order
|
|
1898
|
+
12. **Use `transaction` for all CRUD operations** — instant UX with automatic rollback on failure
|
|
1899
|
+
13. **In transactions: `closeModal` goes in `before`, data changes in `optimistic`** — before is not rolled back, optimistic is
|
|
1900
|
+
14. **UPDATE transactions don't need `onSuccess`** — optimistic data is already correct, re-fetch causes visible flash
|
|
1901
|
+
15. **Use `type: 'icon'` with `name` prop for icons** — not emoji text. Names are kebab-case: `pencil-simple`, `trash`, `moon`
|
|
1902
|
+
16. **`$array: 'append'` and `$array: 'replace'` use `value`, not `item`** — `item` silently does nothing
|
|
1903
|
+
17. **Field IDs must be unique within the spec** — use descriptive names: `patient-name`, `submit-btn`
|
|
1904
|
+
18. **Arrays for ordered collections** — never objects
|
|
1905
|
+
19. **Use `hover`/`active`/`focus` for interactive feedback** — not inline styles or event handlers
|
|
1906
|
+
20. **Use `transition` to control animation speed** — default duration is fine for most cases
|
|
1907
|
+
21. **Use `motion.initial`/`motion.animate` for entrance animations** — elements animate when they first appear
|
|
1908
|
+
22. **Buttons and touchables have built-in CSS transitions** — only add explicit `hover`/`active` when you need custom behavior beyond the defaults
|
|
1909
|
+
23. **Use `$switch` for 3+ branches** — replaces deeply nested `$cond`. `default` is required
|
|
1910
|
+
24. **Use `$format` with `locale` for localized numbers** — `"locale": "es-HN"` for country-specific formatting
|
|
1911
|
+
25. **Use `derive` for computed stats** — auto-computed, dependency-tracked, read-only. Don't recompute in multiple elements
|
|
1912
|
+
26. **Use `dataSources` for reactive GET** — self-contained specs. `fetch` action for one-shot POST/PATCH/DELETE
|
|
1913
|
+
27. **Use `repeat.groupBy` for grouped data** — client-side (`groupBy` field) or pre-grouped (`groupKey` + `groupItems`)
|
|
1914
|
+
28. **Use templates for repetitive elements** — 7 identical columns become 7 one-liners with `$prop`
|
|
1915
|
+
29. **Use AppSpec for multi-screen apps** — sidebar, navigation, shared state, tokens, translations all in JSON
|
|
1916
|
+
30. **Table `mode: "server"` for large datasets** — writes sort/page to state, `dataSources` re-fetches
|
|
1917
|
+
31. **`$group` only inside groupBy context** — throws error if used in regular elements
|
|
1918
|
+
32. **`$selection` only inside repeat with selection** — throws error if used outside
|
|
1919
|
+
33. **CSS hover for color/opacity, Motion hover for transforms** — automatic detection, no manual choice needed
|
|
1920
|
+
34. **CSS hover only works on supported primitives** — `box`, `text`, `stack`, `grid`, `scroll`, `button`, `touchable`, `table`. Other types silently ignore className. `mythik validate` catches this
|
|
1921
|
+
35. **Don't use `overflow: hidden` with `box-shadow` hover** — overflow clips the shadow. Use `filter: brightness()` or remove overflow
|
|
1922
|
+
36. **Toasts render automatically** — `showNotification` writes to state, the renderer picks it up. No need to add a `toast-container` element unless you want custom positioning
|
|
1923
|
+
37. **Device context is auto-tracked** — viewport, platform, orientation, OS color scheme write to `/ui/device/*` automatically. Use `$breakpoint` for responsive values, `$platform` for cross-platform branching, `$state` for orientation detection
|
|
1924
|
+
38. **Use `forms` for coordinated validation** — form-level `isValid`, cross-field rules, submit gating. Use inline `checks` on inputs for standalone fields without form coordination
|
|
1925
|
+
39. **Use `$auth` for user data, not `$state`** — `{ "$auth": "email" }` is safer (whitelisted fields only) and more stable than `{ "$state": "/auth/user/email" }`. `$auth` blocks access to tokens/passwords even if somehow in state
|
|
1926
|
+
40. **Auth headers are auto-injected** — specs don't need to specify auth headers in `fetch`/`submitForm`. The framework injects Bearer tokens automatically for configured `authDomains`
|
|
1927
|
+
41. **Login screen is a normal spec** — build login forms with regular primitives (input, button). The `login` action connects to the auth provider. No special components needed. Use `$bindState: "/login/username"` and `"/login/password"` for form fields. The `login` action receives `{ email: "$state /login/username", password: "$state /login/password" }`
|
|
1928
|
+
42. **Never trust client auth as security** — `AppAuthConfig` is UX routing (hide screens by role), not server-side security. Always validate tokens on the backend
|
|
1929
|
+
43. **Use `roleAccess` for centralized access control** — when defined, it's the sole source of truth. Roles not listed get zero access. `ScreenDefinition.roles` is only used when `roleAccess` is absent
|
|
1930
|
+
44. **loginScreen is always accessible** — exempt from `protectedScreens`, even with `["*"]`. Prevents infinite redirect loops
|
|
1931
|
+
45. **Sidebar auto-refreshes on auth changes** — `MythikApp` subscribes to `/auth/user/*` and `/auth/isAuthenticated` state changes and calls `refreshScreenList()` automatically. No manual call needed after login/logout
|
|
1932
|
+
46. **Login screen renders fullscreen without layout** — `MythikApp` automatically renders the login screen WITHOUT the app layout (no sidebar, no header) when the user is not authenticated. After login, the full layout appears reactively. No `visible` conditions needed on the sidebar
|
|
1933
|
+
47. **Logout clears login form automatically** — the `logout` action clears `/login/*` state before navigating to the login screen. Credentials never persist after logout. No `statePolicy: "reset"` needed on the login screen for this (though it doesn't hurt)
|
|
1934
|
+
48. **Auth errors are silent in production** — auth action errors (login, logout, refreshSession) are logged to console only in development (`NODE_ENV !== 'production'`). In production, they're suppressed to prevent stack trace leakage to attackers. Errors still appear in the UI via `/auth/error` state
|
|
1935
|
+
49. **Use `createAuthSpecStore` for authenticated spec loading** — when specs are served by a Mythik server with auth, use `createAuthSpecStore(API_URL)` from `mythik-react` instead of a plain specStore. It auto-injects Bearer tokens for protected screen loading. MythikApp syncs the token from AuthEngine automatically
|
|
1936
|
+
50. **Input `type` not `inputType`** — the input primitive accepts `type: "password"`, not `inputType`. The CLI validates prop names and warns on unknown props with "did you mean?" suggestions (Levenshtein matching)
|
|
1937
|
+
51. **Use `mythik push` for spec creation** — no seed scripts, no .ts spec files. Write JSON, push via CLI, modify with patches. All spec operations go through the CLI
|
|
1938
|
+
52. **Use `uploadFile` for file uploads** — not custom `fetch` with FormData. The action handles validation, progress tracking, retry, and state management
|
|
1939
|
+
53. **Set `accept` and `maxSize` on every file-upload** — framework defaults (10MB, any type) protect, but explicit is better for UX and security
|
|
1940
|
+
54. **`autoUpload: true` (default) for simple cases, `false` for multi-field forms** — auto-upload sends files immediately on selection. Manual mode lets users select files first, then upload all together on form submit
|
|
1941
|
+
55. **File URLs go to `target` path — don't read from `/ui/uploads/*`** — `/ui/uploads/{elementId}/files` is internal state for the primitive (progress, preview). The spec author reads the `target` path for the final URL(s)
|
|
1942
|
+
56. **Auto-skeleton activates when spec has `fetch` in initialActions + loading + empty data** — zero config needed. Set `skeleton: false` on elements that show static content. Set `autoSkeleton={false}` on MythikRenderer to disable entirely
|
|
1943
|
+
57. **Use `export` action for data downloads, not custom fetch** — format `"csv"` is always available (built-in). For `"xlsx"` or `"pdf"`, register an ExportAdapter via `exportAdapters` prop on MythikRenderer
|
|
1944
|
+
58. **Export columns support formatting** — `format: "currency"` with `formatOptions: { currency: "USD", locale: "en-US", decimals: 2 }`. Same Intl formatters as `$format` expression handler
|
|
1945
|
+
59. **Use `variant` prop for consistent component styling** — don't copy style objects between elements. Define variants in `tokens.components.{type}.{variant}` and reference with `"variant": "primary"`
|
|
1946
|
+
60. **Element-level style/hover/active overrides variant** — variant is always the base. For one-off customizations, add explicit style/hover on the element. For reusable customizations, define a new variant
|
|
1947
|
+
61. **Use `$path` references in variant definitions** — `$colors.primary` resolves against active tokens so dark mode works automatically. No need for separate dark variant definitions unless the dark change isn't just a token swap
|
|
1948
|
+
62. **AppSpec patches use `/layout/elements/` not `/elements/`** — Screen spec elements are at `/elements/{id}`. AppSpec layout elements are at `/layout/elements/{id}`. Use dot-notation with `mythik elements` for inspecting non-element sections (tokens, screens, etc.)
|
|
1949
|
+
63. **AppSpec is filtered without Bearer token** — `GET /api/app/:id` strips `roleAccess` and `protectedScreens` from the response when no valid Bearer token is present. This prevents information disclosure of the authorization model. With a valid Bearer, the full AppSpec is returned. The endpoint never returns 401 — only the payload changes
|
|
1950
|
+
64. **CLI warns on unknown prop names** — `mythik push` and `mythik validate` check prop names against known schemas for all 38 primitives. Unknown props generate warnings (not errors) with Levenshtein suggestions: `⚠ unknown prop "inputType" for type "input" — did you mean "type"?`. Warnings don't block saves
|
|
1951
|
+
65. **Use `audit` config for automatic change tracking** — endpoint-level config that auto-injects username and timestamp into CRUD INSERT and UPDATE operations. Column names are configurable per table. All fields are optional — only configured fields are injected. Audit values override client-sent values (prevents spoofing)
|
|
1952
|
+
66. **Audit `timezone` for correct local timestamps** — set `"timezone": "America/El_Salvador"` (IANA string) in audit config. Without timezone, timestamps are UTC. Use timezone when the database stores local time. The framework converts using `Intl.DateTimeFormat` — works regardless of server location
|
|
1953
|
+
67. **CRUD queries are trigger-safe** — INSERT uses `SCOPE_IDENTITY()` + SELECT instead of `OUTPUT INSERTED.*`. UPDATE uses SELECT after the update. Both patterns are compatible with SQL Server tables that have triggers enabled
|
|
1954
|
+
68. **CustomJWTProvider maps response paths explicitly** — `tokenPath`, `refreshTokenPath`, and `userPath` are dot paths against the full login/refresh response. `rolePath` and `rolesPath` are compat dual: plain keys (`"role"`, `"roles"`) resolve inside the extracted user object, dotted paths (`"user.role"`, `"data.user.role"`) resolve against the full response. If no role/roles are found, `defaultRole` is used and development logs a warning. HTTP errors read `response.error.message` first, then `response.message`
|
|
1955
|
+
69. **ApiSpec is pure declarative — no connection, no secrets** — `createServer` receives database connection and JWT secret in `MythikServerConfig`, not in the ApiSpec. The ApiSpec only contains endpoints, catalogs, auth model (provider, policies, scopeFilter), and audit config. This allows the ApiSpec to be stored safely in the database
|
|
1956
|
+
70. **Server loads api-spec from SpecStore** — `createServer({ spec: { store: myStore, id: 'my-api' }, database: {...}, auth: {...} })`. Supports three modes: file path (string), ApiSpec object (testing), or `{ store, id }` (database). The server never owns a SpecStore — the caller provides one
|
|
1957
|
+
71. **SpecStore table is configurable** — `new SqlServerSpecStore({ ..., table: 'api_specs' })` (imported from `mythik/server` — Node-only) and `new SupabaseSpecStore({ ..., table: 'api_specs' })` (imported from `mythik` — browser-safe). Default: `'screens'`. Enables separate tables for frontend specs and backend specs with different DB permissions
|
|
1958
|
+
72. **Api-specs are never served to the browser** — `GET /api/screens/:id` and `GET /api/app/:id` return 404 for documents with `type: "api"`. Prevents information disclosure of table names, SQL queries, and auth config
|
|
1959
|
+
73. **CLI operates on api-specs automatically** — `mythik validate my-api`, `mythik pull my-api`, `mythik push my-api` detect `type: "api"` and use the apiHandler. Manifest shows catalogs, endpoints, auth config. Elements support dot-notation: `mythik elements my-api endpoints.records-crud`
|
|
1960
|
+
74. **Use `mythik contract` for frontend↔backend validation** — `mythik contract --app app-demo --api records-api`. Cross-validates screen fetch URLs against api-spec endpoints. Supports multiple api-specs (comma-separated). Use `--api-table` when api-specs are in a separate table. Use `--base-url` to strip host from URLs. Use `--json` for CI/CD integration
|
|
1961
|
+
75. **Contract validates 4 rules** — (1) endpoints-exist: fetch URLs must match api-spec endpoints/catalogs (error). (2) fields-valid: POST/PUT body fields must be in CRUD insertable/updatable (error). (3) params-match: query params must match endpoint param definitions (warning). (4) permissions-consistent: UI roleAccess must be consistent with API policies (warning). All rules include Levenshtein "did you mean?" suggestions
|
|
1962
|
+
76. **Contract permission check is unique** — no other framework can detect "Role X can access screen Y but API denies the action." This is only possible because both frontend (roleAccess) and backend (policies) are declarative JSON that can be cross-referenced statically. In code-based systems, UI visibility and API permissions live in separate layers
|
|
1963
|
+
77. **Input `on.submit` fires on Enter key** — add `on: { submit: { action: "login", params: {...} } }` to an input element. Pressing Enter triggers the action. Use for login forms and search fields
|
|
1964
|
+
78. **`_motion.whileTap` and `_motion.whileHover` work in specs** — Motion animations from `_motion` now forward `whileTap` and `whileHover` to Framer Motion. Use `"Infinity"` (string) for `repeat` in JSON — the renderer converts it to JS `Infinity` automatically. Enables infinite looping animations declared in specs
|
|
1965
|
+
79. **Contract deduplicates findings** — identical findings (same rule + screen + level + message) are collapsed into one with a `count` field. CLI shows `(×N)` when count > 1. Prevents noise from repeated fetch references to the same missing endpoint
|
|
1966
|
+
80. **Contract handles template domain URLs** — fetch URLs like `http://${authDomain}/api/items` are normalized to `/api/items` before matching. The extractor detects `http(s)://:param/path` patterns after template replacement and strips the domain portion. Works with any variable name and optional port
|
|
1967
|
+
81. **Contract --base-url is explicit only** — there is no auto-detection from authDomains. If screen specs use absolute URLs and no `--base-url` is provided, the engine emits a warning: "Absolute URLs detected... Use: mythik contract --base-url http://your-server:port". Template domain URLs (containing `${`) do not trigger this warning since they're already normalized
|
|
1968
|
+
82. **VersionedSpecStore extends SpecStore** — adds `saveVersion(id, doc, meta)`, `loadVersion(id, version)`, `listVersions(id, limit?)`, `currentVersion(id)`. Backward compatible — `load(id)` still returns the current spec. Versioning is opt-in; existing code works without changes
|
|
1969
|
+
83. **Version history uses patch-chain with snapshots** — v1 stores the full spec. Subsequent versions store RFC 6902 patches. Every N versions (configurable, default 10) a full snapshot is stored. Reconstructing version X = nearest snapshot + apply patches. `computePatches(before, after)` generates patches; `applyPatches` (existing) applies them
|
|
1970
|
+
84. **EnvironmentStore manages version pointers** — environments (dev, uat, prod, any custom name) are pointers to versions: `{ name, version, promotedAt, promotedBy }`. Created on-demand. `setEnvironment(specId, env, version, author)` creates or moves a pointer. Loading by environment = resolve pointer → loadVersion
|
|
1971
|
+
85. **Structural diff is semantic with before/after values** — `computeStructuralDiff(before, after)` produces `StructuralChange[]` with kinds: element-added, element-removed, prop-changed, action-changed, section-changed. Type-aware: screen specs diff elements/props, api specs diff endpoints/catalogs at item level, app specs diff navigation/tokens. Shows actual values: `prop content: "Send" → "Submit"`, `orderBy: added "year DESC"`, `orderBy: removed (was "year DESC")`. Summary: `+2 elements, ~3 changes, -1 element`
|
|
1972
|
+
86. **Promote gate validates across environments before moving pointers** — `runPromoteGate({ specIds, fromEnv, toEnv, store, envStore, apiIds? })` runs 3 validation layers: (1) spec validation — structural rules, always available; (2) cross-screen consistency — referenced screens exist in destination or batch, always available; (3) contract validation — against target env api-spec, only when `--api` provided. Supports individual, app-complete, and batch-selective modes. Atomic: all specs pass or none are promoted
|
|
1973
|
+
87. **Promote uses batch-aware contract validation** — when an api-spec is in the promote batch, contract validation runs against the batch version (source env), not the destination version. This prevents false alarms when promoting screens + updated api-spec together. When api-spec is NOT in batch, validates against destination env version
|
|
1974
|
+
88. **Rollback creates new version, never rewrites history** — `executeRollback(specId, toVersion)` creates vN+1 with the content of the target version. All intermediate versions are preserved. Impact analysis shows lost changes with author attribution and affected environments. Moving an environment pointer is a separate operation via `envs --set`
|
|
1975
|
+
89. **Lazy bootstrap for existing specs** — first versioned save on an existing spec with no history automatically creates v1 (snapshot of current spec), then saves the change as v2. No migration script needed. Specs without history continue working via base `SpecStore.load()`
|
|
1976
|
+
90. **Use `navigateScreen` and `goBackScreen` in specs** — `navigateScreen` and `goBackScreen` are registered by MythikApp and call the AppEngine directly. The built-in `navigate` and `goBack` only set state intents. Always use `navigateScreen`/`goBackScreen` in specs for navigation that works. Example: `{ "action": "goBackScreen" }` goes back to the previous screen in the navigation history, regardless of which screen navigated to the current one
|
|
1977
|
+
91. **CLI `--table` flag overrides store table** — all commands accept `--table <name>` to read/write from a different table. Use `--table api_specs` to operate on api-specs. The flag overrides `config.sqlserver.table` or `config.supabase.table`. Environment and version tables are NOT affected by `--table` — they always use their fixed names (`screen_environments`, `screen_versions`)
|
|
1978
|
+
92. **`push` and `patch` version automatically with `--author`** — when `--author` is provided, both commands use `VersionedSpecStore.saveVersion()` instead of `SpecStore.save()`. The version includes author, source type (`push`/`patch`), and optional `--description`. Without `--author`, commands work as before (no versioning). Example: `mythik patch screen-id --author alice --description "Fixed layout"`
|
|
1979
|
+
93. **`mythik history` shows inline diffs** — each version in the history output shows the actual changes (before/after values), not just a summary. Uses `computeStructuralDiff` between consecutive versions. Example output: `~ element "btn" prop content: "Send" → "Submit"`
|
|
1980
|
+
95. **`ai-context.md` is the AI-optimized spec reference** — compressed from this reference-doc (1145 lines vs 2658). Use ai-context.md for spec generation, reference-doc for full human reference. Validated via agent-based testing with progressive difficulty levels (L1-L4). Test scenarios in `../ai-context-test-scenarios.md`, results in `../ai-context-test-results.md` (framework-dev, not part of consumer publish surface)
|
|
1981
|
+
|
|
1982
|
+
94. **SqlServerVersionedSpecStore available** — `resolveVersionedStore` supports `sqlserver` store type. Creates `SqlServerVersionedSpecStore` (specs + version history) + `SqlServerEnvironmentStore` (environment pointers). Requires `screen_versions` and `screen_environments` tables — create with `setup-versioning.ts`
|
|
1983
|
+
96. **SupabaseVersionedSpecStore available** — `resolveVersionedStore` supports `supabase` store type. Uses PostgREST REST API (no `@supabase/supabase-js` dependency). Same snapshot+patches pattern as SqlServer. Environment upsert uses `on_conflict=screen_id,environment` for PostgREST compatibility. Requires `screen_versions` and `screen_environments` tables created in Supabase dashboard
|
|
1984
|
+
97. **`variant` is a universal prop** — any primitive can use `variant` when component variants are defined in `tokens.components.{type}.{variant}`. The validator accepts `variant` on all primitives (via `COMMON_PROPS`). The render engine resolves variants before passing props to the primitive
|
|
1985
|
+
98. **Use DNA seeds for app identity** — define `tokens.dna` in AppSpec with 1-8 seed values. The framework derives all visual tokens (colors via OKLCH tonal palette, shape, typography, spacing, elevation, motion, opacity) plus auto dark mode. No manual color palette or radius scale needed — DNA generates it from `{ "primary": "#0D9488" }`
|
|
1986
|
+
99. **Three-layer token resolution** — Framework DEFAULTS (always present) → DNA derivation (if `dna` seed exists) → Manual overrides (explicit `tokens.*` values always win). Specs without tokens use defaults. Specs with only DNA get full derived identity. Specs with DNA + overrides get derived base with surgical customization
|
|
1987
|
+
100. **`$token` auto-converts elevation to CSS** — `{ "$token": "elevation.md" }` returns a CSS `boxShadow` string on web (e.g., `"0px 4px 12px rgba(0,0,0,0.15)"`). On RN, primitives handle elevation internally via native shadow props. No manual conversion needed
|
|
1988
|
+
101. **`updateTokens` for runtime token changes** — `{ "action": "updateTokens", "params": { "dna": { "primary": "#F97316" } } }` hot-swaps all tokens without page reload. All primitives re-render with new values. Numeric DNA seeds > 1 are auto-normalized (divided by 100 for 0-100 slider compatibility)
|
|
1989
|
+
102. **Font loading is the project's responsibility** — DNA derives font family names (Inter, DM Sans, Playfair Display, Georgia, Source Sans) but does NOT load them. Web: include Google Fonts link or `@font-face`. RN: use `expo-font`. Without loading, browser falls back to system fonts
|
|
1990
|
+
103. **`useDesignTokens` replaces `useThemeColors` in primitives** — all 35 migrated primitives (18 web + 17 RN) use `useDesignTokens` internally. It extracts colors, shape, typography, spacing, elevation, motion, and opacity with framework defaults as fallback. `useThemeColors` is deprecated but still works as wrapper
|
|
1991
|
+
104. **`mythik tokens` inspects DNA derivation** — `mythik tokens --dna '{"primary":"#0D9488"}' --json` shows the full resolved token set. Use for debugging and verifying what DNA generates before applying to a spec
|
|
1992
|
+
105. **Formality drives 5 font tiers + continuous properties** — Body: Inter (0) → Space Grotesk (0.25) → Source Sans 3 (0.5) → Lora (0.75) → Merriweather (1.0). Heading: Inter → Space Grotesk → DM Sans → Lora → Playfair Display. Font names include CSS fallbacks (e.g., `'Inter', sans-serif`). Additionally, formality continuously varies `letterSpacing` (0 → 0.03em), `headingLetterSpacing` (0 → -0.02em), `lineHeight` ratio (1.45 → 1.6), and bold weight (700 → 800)
|
|
1993
|
+
106. **Neutral palette is visually distinct per mode** — `warm` uses amber hue (55°), `cool` uses blue (250°), `natural` uses primary hue. Chroma is 0.025 (natural) / 0.035 (warm/cool) — strong enough to be visible in backgrounds, borders, and surfaces
|
|
1994
|
+
107. **`Element.key` forces remount on value change** — Add `key` to any element with a dynamic expression (e.g., `{ "$template": "preview-${/internal/tokenVersion}" }`). When the resolved value changes, React unmounts and remounts the element, re-triggering mount animations. Used by DNA Playground to re-animate preview on Apply DNA
|
|
1995
|
+
108. **Input supports `type: "color"`** — Renders native OS color picker. Use with `$bindState` for interactive color selection in specs
|
|
1996
|
+
109. **Slider label is display-only** — The slider primitive renders the `label` prop as-is without appending the current value. Use `$template` in the label to include the value if desired (e.g., `"label": { "$template": "Roundness: ${/dna/roundness}%" }`)
|
|
1997
|
+
110. **`--author` activates CLI versioning** — `mythik push` and `mythik patch` without `--author` write directly to the `screens` table with NO version history. With `--author ai-agent`, they use `VersionedSpecStore` which writes to both `screens` AND `screen_versions`, enabling `history`, `diff`, and `rollback` commands. Always use `--author` during development
|
|
1998
|
+
111. **Identity System — `tokens.identity` controls visual identity beyond DNA** — DNA controls continuous/color values. Identity controls categorical dimensions: `surface` (how containers render), `radiusPattern` (corner shape), `typographyHierarchy` (heading scale), `labelStyle` (label formatting), `textDecoration` (heading decoration), `depth` (shadow intensity 0-1), `shadowAngle` (shadow direction 0-360°), `colorScheme` (light/dark/colored polarity), `colorWeight` (where color appears), `accentApplication` (where accent appears). Set via `tokens.identity` in AppSpec or screen Spec. Lint validates deep identity enum/range values; for example use `colorScheme: "light-surface"`, not `"light"`
|
|
1999
|
+
112. **Surface Treatment — 6 types transform all containers** — `identity.surface`: `elevated` (shadow+border, material), `flat` (color blocks only, no borders/shadows), `outlined` (borders only, transparent bg, Linear-like), `glass` (backdrop-filter blur, semi-transparent), `bold` (thick 2-3px borders, brutalist), `neo` (neumorphic inset+outset shadows). Every input, button, card, modal, select, textarea, checkbox, accordion, table adapts automatically
|
|
2000
|
+
113. **Surface styles are explicit — every property set, no implicit values** — All 6 surface types define `border` and `boxShadow` explicitly for every component category. Properties are `'none'` when not used, never omitted. This prevents browser defaults from showing (e.g., native button borders in flat mode)
|
|
2001
|
+
114. **`resolveSurfaceStyles()` returns structured platform-neutral objects** — Returns `StructuredSurfaceStyles` with 6 component categories: `card`, `input`, `inputFocus`, `buttonPrimary`, `buttonSecondary`, `modal`. Each is a `StructuredSurfaceStyleProps` with `backgroundColor?`, `color?`, `border?: BorderDef`, `borderTop/Right/Bottom/Left?: BorderDef`, `shadows: ShadowDef[]`, `blur?: BlurDef`, `backgroundOpacity?`. NO CSS strings — data only. Platform serializers convert: `surfaceToCSS()` for web (React/Vue/Angular/Svelte), `surfaceToRN()` for React Native. Core stays platform-neutral
|
|
2002
|
+
115. **Surface-aware focus per surface type** — `inputFocus` in SurfaceStyles. Elevated/outlined: 1px border primary + ring. Flat: no border, ring only. Bold: 2px border primary + ring (preserves thickness). Neo: primary-tinted inset shadow + ring. Glass: semi-transparent primary border + ring. Primitives use `t.surface.inputFocus` instead of hardcoded focus styles
|
|
2003
|
+
116. **Box primitive gains `surface` prop** — `{ "type": "box", "props": { "surface": "card" } }` applies identity surface styles (border, shadow, bg, borderRadius). Without `surface` prop, box renders as plain layout div. Use `surface="card"` for card-like containers, `surface="modal"` for overlay panels. AI should mark card-like boxes with this prop
|
|
2004
|
+
117. **Select dropdown uses `surface.modal` not `surface.card`** — Dropdowns are overlays that need opaque backgrounds for readability. In outlined mode, `card` has transparent bg which breaks dropdowns. Modal always has solid bg regardless of surface type
|
|
2005
|
+
118. **Flat inputs differentiate by background color, not borders** — Flat spec: "NO borders, differentiation through background color blocks only." Inputs and buttonSecondary use `colors.background` (#f8fafc off-white) instead of `colors.surface` (#ffffff) to remain visible without borders
|
|
2006
|
+
119. **Radius Pattern — 11 asymmetric corner options** — `identity.radiusPattern`: `all` (default), `top`, `bottom`, `left`, `right`, `diagonal` (TL+BR), `inverse-diagonal` (TR+BL), `single` (TL only), `single-tr`, `single-bl`, `single-br`. Applied via `t.radius(baseValue)` helper in all primitives. `resolveRadiusPattern()` converts to CSS border-radius string
|
|
2007
|
+
120. **Typography Hierarchy — 6 heading scales** — `identity.typographyHierarchy`: `dramatic` (3×, 800w), `uniform` (1.3×, 600w — Notion-like), `editorial` (2.2×, 700w, -0.02em tracking), `display` (4×, 900w, -0.03em — magazine hero), `mono` (1.5×, monospace font), `contrast` (2.5×, 300w ultra-thin — luxury/Apple). Applied to `<Text variant="heading">`
|
|
2008
|
+
121. **Label Style — 3 modes on all form labels** — `identity.labelStyle`: `normal`, `uppercase` (textTransform + letterSpacing 0.08em), `accent-colored` (label text in accent color). Applied to labels inside input, textarea, select, and checkbox primitives — not just `<Text variant="label">`
|
|
2009
|
+
122. **Text Decoration — 6 heading effects, multi-select** — `identity.textDecoration`: `stroke` (webkit-text-stroke), `underline-accent` (3px bottom border, accent color), `highlight` (accent bg tint behind text), `overline` (3px top border), `shadow` (3px hard offset, no alpha — solid accent). Supports array for combinations: `["underline-accent", "shadow"]`. `resolveTextDecorations()` merges styles from all selected decorations
|
|
2010
|
+
123. **Custom checkbox is surface-aware** — Replaced native `<input type="checkbox">` with custom div+SVG. Unchecked: uses `t.surface.input` styles. Checked: fills with primary, preserves surface border (bold keeps 2px, neo keeps shadow). Focus uses `t.surface.inputFocus`. Hidden input preserved for accessibility
|
|
2011
|
+
124. **Elevation and border resolvers ready but not yet consumed** — `resolveElevationStyle()` (diffuse/solid/color/none) and `resolveBorderStyle()` defined, tested, exported, values in `t.identity.*`. Primitives don't consume them yet — reserved for future phases when elevation identity overrides surface defaults
|
|
2012
|
+
125. **`colors.background` is the STATIC base page background** — Distinct from `colors.surface` (card/component bg). Returns `#f8fafc` light gray (or dark-mode equivalent via `/preferences/theme`), but does NOT respond to preset `identity.colorScheme` changes. Safe for light-only apps. For preset-aware layouts (apps that support dark-surface or colored-surface presets), use `backgroundCSS` instead — see rule 155. Visible failure mode: dark preset with layout using `colors.background` produces a light frame around dark content
|
|
2013
|
+
126. **`updateTokens` does deep merge — partial updates work** — Calling `updateTokens({ dna: { roundness: 50 } })` merges with previously-applied tokens instead of replacing them. Previous primary color, surface settings, etc. persist. Enables live playground controls where each slider updates only its dimension
|
|
2014
|
+
127. **`deepMergeTokens` exported from core** — `import { deepMergeTokens } from 'mythik'`. Recursively merges objects (arrays and non-objects replace, objects deep-merge). Used internally by `updateTokens`
|
|
2015
|
+
128. **Depth and angle are serializer concerns, not data concerns** — `identity.depth` (0-1) and `identity.shadowAngle` (0-360°) are NOT in `StructuredSurfaceStyleProps`. Shadows store raw `magnitude`/`blur`/`opacity`. The serializer (`surfaceToCSS`/`surfaceToRN`) applies `depthScale()` and sin/cos angle rotation when converting to platform-specific format. This keeps the structured data pure and reusable
|
|
2016
|
+
129. **`resolveIdentity<T>()` — unified orchestrator** — Single function in core orchestrates: resolveSchemeColors → generateTonalStep → resolveSurfaceStyles → serializer(T) → resolveColorWeight → resolveRadiusPattern. Accepts a `SurfaceSerializer<T>` function — each platform passes its own (surfaceToCSS, surfaceToRN). Returns `{ schemeColors, surface: T, colorWeight, radius }`. Web/RN hooks call this one function instead of 6+ manual steps
|
|
2017
|
+
130. **Color Scheme — surface polarity with configurable layers** — `identity.colorScheme`: `light-surface` (default, pass-through), `dark-surface` (uses `modes.dark` from DNA), `colored-surface` (3 configurable tonal layers from primary palette). `colored-surface` uses `identity.coloredSurfaceLayers: { background: 25, surface: 45, primitive: 65 }` — tonal steps from the primary OKLCH palette. `generateTonalStep()` produces colors at any step 0-100. Default 25/45/65 validated across 8 hues. Focus ring uses accent (not primary) in colored-surface to avoid invisible ring on primary background
|
|
2018
|
+
131. **`t.colors` stays original palette, scheme only affects surfaces** — `t.colors.text` always returns the original light palette text color. Surface styles (`t.surface.card`, etc.) use scheme-adjusted colors internally. This separation ensures UI controls (selects, labels) remain readable while the preview/app content adapts to the scheme. Preview elements use `$token: schemeColors.*` for scheme-aware text
|
|
2019
|
+
132. **Glass fix — derives rgba from actual surface color** — `hexToRgba(c.surface, opacity)` replaces hardcoded `rgba(255,255,255,...)`. Dark scheme + glass = semi-transparent dark, not semi-transparent white. All glass card, input, button, modal use `hexToRgba` for background and border
|
|
2020
|
+
133. **Color Weight — WHERE color appears in layout** — `identity.colorWeight`: `monochrome` (all neutral), `branded-nav` (primary navbar, white text), `gradient-hero` (primary→accent gradient hero section), `ambient` (subtle primary tint on sections via rgba), `dark-native` (dark nav using modes.dark). `resolveColorWeight()` returns `{ navBg, navText, sectionBg, heroBg, heroGradient }`. Layout tokens available via `$token: colorWeight.navBg`
|
|
2021
|
+
134. **Accent Application — where accent color appears** — `identity.accentApplication`: `buttons` (boolean — buttonPrimary uses accent), `navItems` (boolean — highlight active nav), `cardLine` (array of positions — accent border on cards), `links` (boolean — link color), `backgrounds` (boolean — section bg uses accentLight), `iconContainers` (boolean). `cardLine` supports multi-select: `["top", "left"]` for L-shape accent border. Applied inside `resolveSurfaceStyles` via `SurfaceOptions.accent/cardLine/accentButtons`
|
|
2022
|
+
135. **Surface styles include color: c.text for CSS inheritance** — All 6 surface types set `color: c.text` on card, input, and modal categories. Child primitives use `color: 'inherit'` to pick up the parent's scheme-adjusted color. This makes dark/colored surface text automatically correct via CSS inheritance
|
|
2023
|
+
136. **Text/Input/Select/Textarea/Checkbox primitives use `color: 'inherit'`** — These primitives no longer hardcode `t.colors.text`. They inherit text color from their parent container's surface style. Standalone usage inherits from body/page default color. Surface containers (Box, Accordion, Modal) set the correct color
|
|
2024
|
+
137. **`injectColorTokens` in factory.ts** — After `resolveDeepTokens`, the factory computes `colorWeight` and `schemeColors` and injects them into the resolved token tree. This makes `$token: "colorWeight.navBg"` and `$token: "schemeColors.text"` available in specs
|
|
2025
|
+
138. **colorWeight always returns heroBg + heroGradient** — Non-gradient modes return `'transparent'`/`'none'` instead of undefined. Prevents $token resolution errors when specs reference `colorWeight.heroGradient` regardless of active weight mode
|
|
2026
|
+
139. **`surfaceToCSS` emits `border: 'none'` explicitly** — When a surface has no border (flat, neo, glass buttons), the CSS serializer outputs `border: 'none'` instead of omitting the property. This resets browser defaults (native inputs have 2px inset border). Without explicit 'none', flat/neo inputs show unwanted browser borders
|
|
2027
|
+
140. **`inputFocus` has NO backgroundColor** — Focus styles only contain border and shadow changes, never backgroundColor. If inputFocus included backgroundColor, it would overwrite the checkbox's primary bg when checked+focused, making the white checkmark invisible on white background
|
|
2028
|
+
141. **BlurView support in React Native** — When `surface === 'glass'`, the RN serializer passes `blur: { radius }` through to surface props. RN primitives (accordion, input, modal, select, textarea) detect blur and wrap content in `<BlurView>` from `expo-blur`. `toViewStyle()` helper strips `blur` and `focusRing` from surface props before spreading into RN style objects
|
|
2029
|
+
142. **Glass border uses semi-transparent rgba** — Glass surface borders use `hexToRgba(c.surface, 0.2)` for cards/modal/secondary and `hexToRgba(c.surface, 0.15)` for inputs. NOT opaque surface color. Glass inputFocus border uses `c.primary + '80'` (50% opacity hex suffix)
|
|
2030
|
+
143. **Identity icons — weight default + container from tokens** — `identity.icons.weight` applies as default on all icon primitives. Spec `weight` prop overrides. `identity.icons.container` ('none'|'circle'|'square'|'rounded-square') auto-wraps icons with colored background. `container={false}` in spec suppresses. `containerColor`: 'primary'|'accent'|'muted'|'surface'
|
|
2031
|
+
144. **Identity images — corners, overlay, border from tokens** — `identity.images.corners` applies as default borderRadius ('match-card' uses card radius, 'circle'=50%, 'square'=0, 'rounded'=8px). `identity.images.overlay` adds gradient-bottom or color-tint overlay div. `identity.images.border` adds 1px border. Spec `style.borderRadius` or `overlay="none"` overrides
|
|
2032
|
+
145. **Gradient text on headings** — `identity.gradients.text = true` applies `background-clip: text` with primary→accent gradient on all `<Text variant="heading">`. Spec `style.color` overrides (gradient deactivates when explicit color set). Web-only (RN deferred — requires expo-linear-gradient)
|
|
2033
|
+
146. **Gradient buttons** — `identity.gradients.buttons = true` applies `linear-gradient(135deg, primary, accent)` on primary buttons. Spec `style.background` overrides. Web-only (RN deferred)
|
|
2034
|
+
147. **Background treatment — LayerBackground v2** — `identity.background` is `{ color?: string, layers?: LayerConfig[] }` consumed by `<BackgroundStack>` mounted at the root of `MythikRenderer` when present. Layer kinds: `'solid' | 'gradient' | 'pattern' | 'grain' | 'image' | 'blobs'`, each carrying `LayerCommonProps` (`opacity?`, `blendMode?`, `zIndex?`). `resolveBackgroundLayers(bg)` → `LayerSpec[]` → `<BackgroundLayer>` per kind. Blob layers route to `<BlobLayer>` when palette is available. Legacy `resolveBackgroundCSS`, `resolveBlobStyles`, `BackgroundConfig`, `BlobStyle`, and `tokens.backgroundCSS` are not part of the public contract.
|
|
2035
|
+
148. **Blob layer v2 — preset + explicit forms, motion dimensions** — `BlobV2Config` carries `preset: 'organic-duo'|'organic-trio'|'circle-pair'` + `palette: Array<'primary'|'accent'|hex>` + `motion: 'drift-gentle'|'drift-fluid'|'drift-snappy'|'static'` for framework-curated compositions, OR `blobs: BlobInstance[]` for explicit declarations. Each `BlobInstance` carries `shape` (6 curated + `'custom-svg'` with `path`+`viewBox`), `position`, `size`, `color`, optional `opacity`/`blur`/`rotation`/`motion`. Motion breaks into three independently composable dimensions (`drift`, `rotate`, `scale`) that `resolveBlobLayer` converts to `ElementAnimations.ambient` arrays. `useShapeAnimations` (web: CSSOM keyframes singleton; RN: Reanimated `useAnimatedProps` + `HARD_PER_TRIGGER` SV pool) drives them at render time
|
|
2036
|
+
149. **Coordinated page entrance** — do not emit legacy `identity.motionEntrance` or `identity.motionHover`. Entrance animations live in the animation engine: app-wide defaults use `tokens.identity.animations.mount`, while template-level and element-level overrides use `animations.mount` with a recipe reference (e.g., `{ mount: { recipe: 'fade-up' } }`). See rules 180-193 for the engine.
|
|
2037
|
+
150. **Gradient interpolation in OKLCH** — All gradient text and gradient buttons use `linear-gradient(in oklch, ...)` for vibrant transitions. sRGB interpolation crosses through desaturated midpoints (purple→gold becomes muddy brown); OKLCH maintains saturation through the hue arc. Graceful degradation on older browsers
|
|
2038
|
+
151. **Gradient modes: vibrant, soft, muted** — `identity.gradients.text`, `identity.gradients.buttons`, and `identity.gradients.cards` accept `boolean | 'vibrant' | 'soft' | 'muted'`. For text and buttons: Vibrant = OKLCH primary→accent. Soft = OKLCH primaryLight→accentLight (subtle). Muted = sRGB primary→accent (desaturated center, luxury/editorial aesthetic). For cards: Vibrant = 33% primary alpha (dramatic). Muted = 20% primary alpha (balanced default). Soft = 8% primary alpha (subtle kiss). `true` maps to vibrant for text/buttons, muted for cards
|
|
2039
|
+
152. **Explicit accent hex preserved as-is** — When `dna.accent` is set explicitly, `deriveDna` uses the exact hex for `colors.accent` instead of passing through `generateTonalPalette` step 60. The palette is still generated (for `accentLight`), but the primary accent color is the user's exact choice
|
|
2040
|
+
153. **`setIconRenderer` replaces `overridePrimitive` for icons** — `plugins.setIconRenderer(Component)` registers a custom icon renderer. The framework's `icon.tsx` handles identity wrapping (container, weight default) while the registered component renders the actual icon. `overridePrimitive('icon', ...)` still works but bypasses identity features
|
|
2041
|
+
154. **Do not emit Box `backgroundBlobs`** — Box `backgroundBlobs` and legacy per-element blob rendering are not part of the public primitive contract. App-level background lives exclusively at `tokens.identity.background` (rule 147) and mounts at MythikRenderer via `<BackgroundStack>`. Box is a pure surface-styled wrapper; primitives do not rely on `dangerouslySetInnerHTML` for keyframe injection.
|
|
2042
|
+
155. **Do not emit `$token: "backgroundCSS"`** — `$token: "backgroundCSS"` does not resolve. For preset-aware app backgrounds, set `tokens.identity.background` as a LayerBackground (rule 147) — presets update that field directly so the root `<BackgroundStack>` re-resolves on preset switch. No token alias indirection needed.
|
|
2043
|
+
156. **Accordion `badge` prop** — `accordion` accepts optional `badge` prop (`string | number | boolean`). Renders as: solid 8px primary-colored dot for `true`, pill with text/number for string/number values. Hidden when `false`, `0`, `undefined`, or `''`. Use with `$state` for reactive indicators: `"badge": { "$state": "/modified/mySection" }`
|
|
2044
|
+
157. **`updateTokens` `_replace` flag** — Pass `{ _replace: true, dna: {...}, identity: {...} }` to bypass `deepMergeTokens` and fully replace accumulated token state. Without `_replace`, each `updateTokens` call deep-merges onto previous values. Use `_replace` for reset-to-defaults scenarios where accumulated overrides must be cleared entirely
|
|
2045
|
+
158. **`/tokens/raw` state path** — After every `updateTokens` call, the framework persists `currentRawTokens` to `/tokens/raw` in the state store. This is the accumulated raw input (pre-normalization), useful for exporting the current DNA+Identity configuration. Read via `{ "$state": "/tokens/raw" }`
|
|
2046
|
+
159. **`copyToClipboard` stringifies objects** — The `copyToClipboard` action auto-stringifies object values with `JSON.stringify(value, null, 2)` before writing to `/ui/clipboard`. String and number values pass through as-is. The React renderer watches `/ui/clipboard` and calls `navigator.clipboard.writeText()` automatically
|
|
2047
|
+
160. **Identity module structure** — Identity system split into `packages/core/src/design/identity/` with 8 focused modules: `types.ts` (all types/interfaces), `surface.ts` (6 surface variants), `shape.ts` (radius/border), `elevation.ts` (depth/shadow), `typography.ts` (hierarchy/decoration/labels), `color.ts` (scheme/weight/hexToRgba), `background.ts` (CSS/blobs), `motion.ts` (entrance). Barrel `index.ts` re-exports all public API. Internal helpers (`sh`, `neoShadows`, `seededRandom`, etc.) are module-private
|
|
2048
|
+
161. **Border & elevation identity override** — `identity.borderWidth`, `borderStyle`, `borderColor`, `elevationStyle`, `elevationColor` override surface-produced borders/shadows via `applyBorderElevationCSS` (web) and `applyBorderElevationRN` (RN). Override is gated by two independent boolean flags: `overrideSurfaceBorders` (card+modal) and `overrideInputButtons` (input+buttonPrimary+buttonSecondary). Both default `false` — surface type controls everything until explicitly enabled. `inputFocus` is never overridden (accessibility). `borderColor` is semantic: `'neutral'|'primary'|'accent'|'text'` → resolved to hex via schemeColors. `elevationColor`: `'dark'|'primary'|'accent'` → resolved to hex (`dark`=#000000)
|
|
2049
|
+
162. **Elevation styles** — `elevationStyle` controls shadow rendering: `'none'` (no shadow), `'diffuse'` (soft blur shadow, rgba), `'solid'` (no-blur offset shadow, direct color — comic book style), `'color'` (blur shadow using elevationColor in rgba). Shadow angle decomposed via sin/cos matching the surface serializer's `shadowToCSS` pattern. `depthScale()` modulates intensity
|
|
2050
|
+
163. **deepResolve composes objects from multiple $state paths** — Action params and element props support nested objects where each value is a `$state` expression: `{ "dna": { "$state": "/dna" }, "identity": { "$state": "/identity" } }` → resolved to `{ dna: {...}, identity: {...} }`. Works in action params via `deepResolve` in dispatcher.ts and in element props via the unified deep resolver (`resolveDeep`) in engine.ts. No `$object` expression handler needed
|
|
2051
|
+
164. **Primary color preserved as-is** — `deriveDna` uses `seed.primary` directly as `colors.primary` (the exact hex the user chose). Variants derived via relative OKLCH lightness: `primaryLight` = L+15%, `primaryDark` = L-20%. Tonal palette still generated for dark mode (step 80) and neutrals. Previously, palette step 60 was used, which destroyed naturally light colors (amber L=77% → brown L=60%)
|
|
2052
|
+
165. **headingColor** — `identity.headingColor` (`'default'|'primary'|'accent'|'primary-dark'`) controls heading text color. `'default'` = inherit from container. Resolves to scheme colors. Gradient text (`identity.gradients.text`) takes precedence when active. Priority: `style.color > gradient > headingColor > inherit`. Applied in text.tsx for web and RN
|
|
2053
|
+
166. **Object composition in expressions already works** — Plain objects with `$state` children resolve recursively in both action params and element props. The framework walks the object tree: if a key starts with `$` and has a handler → resolve as expression; otherwise recurse into values. This means `{ "user": { "$state": "/user" }, "prefs": { "$state": "/prefs" } }` resolves without any `$object` handler
|
|
2054
|
+
167. **Presets system** — `PresetDefinition` interface in core defines the contract for curated DNA+Identity snapshots. `plugins.registerPresets(presets)` stores them, writes `/presets/available` to state (array of `{ value, label }` for dropdown consumption), and auto-registers `applyPreset` action. Framework provides tools (contract, registration, application); preset definitions live outside core. Architectural principle: framework provides tools, not opinions
|
|
2055
|
+
168. **applyPreset action** — Takes `{ preset: string }` param. Looks up registered preset by ID, calls `updateTokens({ _replace: true, dna, identity })`. `'custom'` is a silent no-op (not a real preset). Unknown IDs throw. `_replace: true` ensures zero stale flags between preset switches (addresses the override flag footgun where forgetting `overrideSurfaceBorders: true` would silently ignore border values)
|
|
2056
|
+
169. **Preset dropdown pattern** — Use `$bindState` on the select's `value` prop (not `$state`) for two-way binding. `on:change` reads the already-written value via `$state`. Do NOT use `$event` in action params — `$event` is not a supported expression handler. Pattern: `"value": { "$bindState": "/ui/currentPreset" }` + `"on": { "change": [{ "action": "applyPreset", "params": { "preset": { "$state": "/ui/currentPreset" } } }] }`
|
|
2057
|
+
170. **Custom detection in presets** — Every control that modifies tokens should include `{ "action": "setState", "params": { "statePath": "/ui/currentPreset", "value": "custom" } }` in its `on:change` array. This switches the preset dropdown back to "Custom" when any individual value is modified after applying a preset
|
|
2058
|
+
|
|
2059
|
+
---
|
|
2060
|
+
|
|
2061
|
+
## Authentication
|
|
2062
|
+
|
|
2063
|
+
### AppSpec Auth Config
|
|
2064
|
+
|
|
2065
|
+
```json
|
|
2066
|
+
{
|
|
2067
|
+
"navigation": {
|
|
2068
|
+
"auth": {
|
|
2069
|
+
"provider": "supabase",
|
|
2070
|
+
"loginScreen": "login",
|
|
2071
|
+
"protectedScreens": ["*"],
|
|
2072
|
+
"roleAccess": {
|
|
2073
|
+
"admin": ["*"],
|
|
2074
|
+
"viewer": ["dashboard", "reports"]
|
|
2075
|
+
},
|
|
2076
|
+
"persistence": "local",
|
|
2077
|
+
"tokenRefresh": true,
|
|
2078
|
+
"authDomains": ["myproject.supabase.co"],
|
|
2079
|
+
"sessionExpiredMessage": "Your session expired, please sign in again"
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
```
|
|
2084
|
+
|
|
2085
|
+
**`authDomains` matcher behavior:** the matcher compares the URL's hostname only — the port is stripped. `"myproject.supabase.co"` matches requests to that hostname on any port. In dev, use `["localhost"]` (NOT `["localhost:5173"]`) — the port in the entry never matches because URL hostnames never contain ports. Subdomain matching is also supported: `"example.com"` matches `api.example.com`. See `ai-context-runtime-semantics.md § 4.1` for the full contract including URL scheme filter and subdomain rules.
|
|
2086
|
+
|
|
2087
|
+
### `$auth` Expression
|
|
2088
|
+
|
|
2089
|
+
Access authenticated user data. Whitelisted fields only — tokens/passwords are BLOCKED.
|
|
2090
|
+
|
|
2091
|
+
```json
|
|
2092
|
+
{ "$auth": "email" }
|
|
2093
|
+
{ "$auth": "role" }
|
|
2094
|
+
{ "$auth": "roles" }
|
|
2095
|
+
{ "$auth": "isAuthenticated" }
|
|
2096
|
+
{ "$auth": "name" }
|
|
2097
|
+
{ "$auth": "avatar" }
|
|
2098
|
+
{ "$auth": "id" }
|
|
2099
|
+
{ "$auth": "metadata" }
|
|
2100
|
+
{ "$auth": "metadata.department" }
|
|
2101
|
+
{ "$auth": "user" }
|
|
2102
|
+
{ "$auth": "loading" }
|
|
2103
|
+
{ "$auth": "error" }
|
|
2104
|
+
```
|
|
2105
|
+
|
|
2106
|
+
**Blocked fields (return undefined):** `token`, `accessToken`, `access_token`, `refreshToken`, `refresh_token`, `password`, `secret`, `session`, `metadata.token`, `metadata.password`
|
|
2107
|
+
|
|
2108
|
+
### Auth Actions
|
|
2109
|
+
|
|
2110
|
+
**`login`** — authenticate via provider:
|
|
2111
|
+
```json
|
|
2112
|
+
{
|
|
2113
|
+
"action": "login",
|
|
2114
|
+
"params": {
|
|
2115
|
+
"email": { "$state": "/screens/login/email" },
|
|
2116
|
+
"password": { "$state": "/screens/login/password" }
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
```
|
|
2120
|
+
After login: tokens stored in closure (never state), user data written to `/auth/*`, credentials cleared from state, proactive refresh scheduled. Navigates to `initialScreen` (or first accessible screen if `initialScreen` is denied for the user's role).
|
|
2121
|
+
|
|
2122
|
+
**`logout`** — clear session:
|
|
2123
|
+
```json
|
|
2124
|
+
{ "action": "logout" }
|
|
2125
|
+
```
|
|
2126
|
+
Clears tokens, state, persistence. Broadcasts to other tabs. Navigates to loginScreen (with `statePolicy: "reset"`, form fields are cleared automatically).
|
|
2127
|
+
|
|
2128
|
+
### Auth State Paths (read-only, auto-managed)
|
|
2129
|
+
|
|
2130
|
+
```
|
|
2131
|
+
/auth/isAuthenticated → boolean
|
|
2132
|
+
/auth/loading → boolean
|
|
2133
|
+
/auth/error → string | null
|
|
2134
|
+
/auth/user/id → string
|
|
2135
|
+
/auth/user/email → string
|
|
2136
|
+
/auth/user/name → string | null
|
|
2137
|
+
/auth/user/avatar → string | null
|
|
2138
|
+
/auth/user/role → string (primary)
|
|
2139
|
+
/auth/user/roles → string[] (all roles)
|
|
2140
|
+
/auth/user/metadata → object | null
|
|
2141
|
+
```
|
|
2142
|
+
|
|
2143
|
+
These are protected by StateGuard — specs cannot write to `/auth/*`.
|
|
2144
|
+
|
|
2145
|
+
### Login Screen Example
|
|
2146
|
+
|
|
2147
|
+
**IMPORTANT:** Login form fields MUST use screen-namespaced state paths (`/screens/login/...`), not root-level paths (`/form/...`). Combined with `statePolicy: "reset"` on the login screen definition, this ensures credentials are automatically cleared on logout — the AppEngine clears `/screens/login` when navigating to the login screen with reset policy. Root-level paths like `/form/email` persist across logouts because nothing clears them.
|
|
2148
|
+
|
|
2149
|
+
```json
|
|
2150
|
+
{
|
|
2151
|
+
"root": "login-page",
|
|
2152
|
+
"elements": {
|
|
2153
|
+
"login-page": {
|
|
2154
|
+
"type": "box",
|
|
2155
|
+
"style": { "display": "flex", "alignItems": "center", "justifyContent": "center", "minHeight": "100vh", "backgroundColor": { "$token": "colors.surfaceMuted" } },
|
|
2156
|
+
"children": ["login-card"]
|
|
2157
|
+
},
|
|
2158
|
+
"login-card": {
|
|
2159
|
+
"type": "box",
|
|
2160
|
+
"style": { "width": 400, "padding": 32, "backgroundColor": { "$token": "colors.surface" }, "borderRadius": 16, "boxShadow": "0 4px 24px rgba(0,0,0,0.08)" },
|
|
2161
|
+
"children": ["title", "email-input", "password-input", "error-msg", "login-btn"]
|
|
2162
|
+
},
|
|
2163
|
+
"title": { "type": "text", "props": { "content": { "$i18n": "auth.login" } }, "style": { "fontSize": 24, "fontWeight": "bold", "marginBottom": 24 } },
|
|
2164
|
+
"email-input": {
|
|
2165
|
+
"type": "input",
|
|
2166
|
+
"props": { "placeholder": { "$i18n": "auth.email" }, "value": { "$bindState": "/screens/login/email" } },
|
|
2167
|
+
"style": { "marginBottom": 12 }
|
|
2168
|
+
},
|
|
2169
|
+
"password-input": {
|
|
2170
|
+
"type": "input",
|
|
2171
|
+
"props": { "placeholder": { "$i18n": "auth.password" }, "type": "password", "value": { "$bindState": "/screens/login/password" } },
|
|
2172
|
+
"style": { "marginBottom": 16 }
|
|
2173
|
+
},
|
|
2174
|
+
"error-msg": {
|
|
2175
|
+
"type": "text",
|
|
2176
|
+
"visible": { "$auth": "error" },
|
|
2177
|
+
"props": { "content": { "$auth": "error" } },
|
|
2178
|
+
"style": { "color": { "$token": "colors.error" }, "fontSize": 13, "marginBottom": 12, "padding": "8px 12px", "backgroundColor": "#FEF2F2", "borderRadius": 8 }
|
|
2179
|
+
},
|
|
2180
|
+
"login-btn": {
|
|
2181
|
+
"type": "button",
|
|
2182
|
+
"props": {
|
|
2183
|
+
"label": { "$cond": { "$auth": "loading" }, "$then": { "$i18n": "auth.signing_in" }, "$else": { "$i18n": "auth.login" } },
|
|
2184
|
+
"disabled": { "$auth": "loading" }
|
|
2185
|
+
},
|
|
2186
|
+
"style": { "width": "100%", "padding": "12px 0", "backgroundColor": { "$token": "colors.primary" }, "color": "#FFFFFF", "border": "none", "borderRadius": 10, "fontSize": 15, "fontWeight": 600, "cursor": "pointer" },
|
|
2187
|
+
"hover": { "backgroundColor": { "$token": "colors.primaryHover" } },
|
|
2188
|
+
"on": {
|
|
2189
|
+
"press": {
|
|
2190
|
+
"action": "login",
|
|
2191
|
+
"params": {
|
|
2192
|
+
"email": { "$state": "/screens/login/email" },
|
|
2193
|
+
"password": { "$state": "/screens/login/password" }
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
```
|
|
2201
|
+
|
|
2202
|
+
**AppSpec screen definition for login:**
|
|
2203
|
+
```json
|
|
2204
|
+
"login": { "label": "Login", "icon": "sign-in", "statePolicy": "reset" }
|
|
2205
|
+
```
|
|
2206
|
+
|
|
2207
|
+
The `statePolicy: "reset"` is what clears `/screens/login` on every navigation to the login screen. Without it, credentials persist after logout.
|
|
2208
|
+
|
|
2209
|
+
### Security Guarantees
|
|
2210
|
+
|
|
2211
|
+
- Tokens NEVER exist in the state store — only in engine closure
|
|
2212
|
+
- `$auth` blocks token/password fields via whitelist
|
|
2213
|
+
- Auth headers only injected for `authDomains` URLs
|
|
2214
|
+
- Credentials cleared from state after login (success AND failure)
|
|
2215
|
+
- Refresh mutex: max 1 concurrent refresh (anti-stampede)
|
|
2216
|
+
- Login rate limit: 5 attempts/min with exponential backoff
|
|
2217
|
+
- Cross-tab sync: logout in one tab = logout in all tabs
|
|
2218
|
+
- StateGuard blocks spec writes to `/auth/*`
|
|
2219
|
+
- XSS-resistant by architecture: JSON → primitives → React escaping
|
|
2220
|
+
|
|
2221
|
+
### Fetch Interceptors
|
|
2222
|
+
|
|
2223
|
+
The framework uses a scoped fetch wrapper (NOT `globalThis.fetch`). All `fetch` and `submitForm` spec actions go through this wrapper. Interceptors can be enabled declaratively in AppSpec:
|
|
2224
|
+
|
|
2225
|
+
```json
|
|
2226
|
+
{
|
|
2227
|
+
"navigation": {
|
|
2228
|
+
"auth": {
|
|
2229
|
+
"interceptors": {
|
|
2230
|
+
"logging": true,
|
|
2231
|
+
"timeout": { "ms": 15000 },
|
|
2232
|
+
"retryOnError": { "maxRetries": 2, "statuses": [502, 503] }
|
|
2233
|
+
}
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
```
|
|
2238
|
+
|
|
2239
|
+
| Interceptor | Config | Behavior |
|
|
2240
|
+
|-------------|--------|----------|
|
|
2241
|
+
| **Auth** | Auto when auth configured | Injects Bearer token for `authDomains` URLs. Triggers refresh+retry on 401. |
|
|
2242
|
+
| **Logging** | `"logging": true` | Logs request method/URL and response status. Redacts sensitive query params (token, password, key). |
|
|
2243
|
+
| **Timeout** | `"timeout": { "ms": 15000 }` | Aborts request after configured milliseconds. Uses AbortController. |
|
|
2244
|
+
| **Retry** | `"retryOnError": { ... }` | Retries on transient server errors (502, 503, 504) with exponential backoff. |
|
|
2245
|
+
|
|
2246
|
+
Auth interceptor is always first in the chain. Custom interceptors from the host app run after built-in ones.
|
|
2247
|
+
|
|
2248
|
+
### Action Middleware
|
|
2249
|
+
|
|
2250
|
+
Pre/post/onError hooks that run on EVERY dispatcher action (not just fetch). Configured at the host app level:
|
|
2251
|
+
|
|
2252
|
+
```tsx
|
|
2253
|
+
<MythikApp
|
|
2254
|
+
auth={{
|
|
2255
|
+
provider: createSupabaseAuthProvider(client),
|
|
2256
|
+
middleware: [myLoggingMiddleware, myAnalyticsMiddleware],
|
|
2257
|
+
}}
|
|
2258
|
+
/>
|
|
2259
|
+
```
|
|
2260
|
+
|
|
2261
|
+
Middleware API:
|
|
2262
|
+
|
|
2263
|
+
```typescript
|
|
2264
|
+
{
|
|
2265
|
+
name: "my-middleware",
|
|
2266
|
+
before: (ctx) => {
|
|
2267
|
+
// Runs before action. ctx.action, ctx.params, ctx.getState(), ctx.setParam()
|
|
2268
|
+
},
|
|
2269
|
+
after: (ctx, result) => {
|
|
2270
|
+
// Runs after successful action
|
|
2271
|
+
},
|
|
2272
|
+
onError: (ctx, error) => {
|
|
2273
|
+
// Runs on action failure. Individual errors are isolated.
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
```
|
|
2277
|
+
|
|
2278
|
+
Middleware executes in registration order. `before` can modify params via `ctx.setParam()`. `onError` handlers are isolated — one failing handler does not prevent others from running.
|
|
2279
|
+
|
|
2280
|
+
**Separation of concerns:** Fetch Interceptors = networking (headers, retry, timeout). Action Middleware = business logic (logging, analytics, auth lifecycle).
|
|
2281
|
+
|
|
2282
|
+
### Session Persistence
|
|
2283
|
+
|
|
2284
|
+
Auth session persistence is configurable in AppSpec:
|
|
2285
|
+
|
|
2286
|
+
| Mode | Config Value | Behavior |
|
|
2287
|
+
|------|-------------|----------|
|
|
2288
|
+
| **Local** | `"persistence": "local"` (default) | Survives browser close. Refresh token + safe user data in localStorage. |
|
|
2289
|
+
| **Session** | `"persistence": "session"` | Lost on browser/tab close. Uses sessionStorage. |
|
|
2290
|
+
| **Memory** | `"persistence": "memory"` | Lost on page refresh. Most secure — re-login required every time. |
|
|
2291
|
+
|
|
2292
|
+
**What's persisted:** Refresh token + user profile (id, email, name, avatar, role, roles, metadata).
|
|
2293
|
+
**What's NEVER persisted:** Access tokens. They live exclusively in the engine closure.
|
|
2294
|
+
|
|
2295
|
+
On page load, `mount()` restores the session from persistence and attempts a token refresh to validate the session is still alive. If refresh fails, the persisted session is cleared.
|
|
2296
|
+
|
|
2297
|
+
### Cross-Tab Synchronization
|
|
2298
|
+
|
|
2299
|
+
Auth events are automatically synchronized across browser tabs:
|
|
2300
|
+
|
|
2301
|
+
| Event | Trigger | Effect on other tabs |
|
|
2302
|
+
|-------|---------|---------------------|
|
|
2303
|
+
| `SIGNED_IN` | Login completes | Other tabs can restore session from persistence |
|
|
2304
|
+
| `SIGNED_OUT` | Logout | All tabs clear session and redirect to loginScreen |
|
|
2305
|
+
| `TOKEN_REFRESHED` | Token refresh completes | Other tabs pick up new refresh token from persistence |
|
|
2306
|
+
| `SESSION_EXPIRED` | Refresh fails | All tabs clear session |
|
|
2307
|
+
|
|
2308
|
+
**Primary mechanism:** BroadcastChannel API (97%+ browser support).
|
|
2309
|
+
**Fallback:** localStorage `storage` events for legacy browsers.
|
|
2310
|
+
|
|
2311
|
+
No configuration needed — cross-tab sync is automatic when auth is configured.
|
|
2312
|
+
|
|
2313
|
+
---
|
|
2314
|
+
|
|
2315
|
+
## Audit (SP3)
|
|
2316
|
+
|
|
2317
|
+
### Audit Config
|
|
2318
|
+
|
|
2319
|
+
Declarative audit config per endpoint — auto-injects username and timestamp into CRUD INSERT and UPDATE queries:
|
|
2320
|
+
|
|
2321
|
+
```json
|
|
2322
|
+
{
|
|
2323
|
+
"endpoints": {
|
|
2324
|
+
"items": {
|
|
2325
|
+
"path": "/api/items",
|
|
2326
|
+
"crud": { "table": "Items", "primaryKey": "id", "insertable": ["name"], "updatable": ["name"] },
|
|
2327
|
+
"audit": {
|
|
2328
|
+
"createdBy": "created_by",
|
|
2329
|
+
"createdAt": "created_at",
|
|
2330
|
+
"updatedBy": "updated_by",
|
|
2331
|
+
"updatedAt": "updated_at",
|
|
2332
|
+
"timezone": "UTC"
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
```
|
|
2338
|
+
|
|
2339
|
+
All fields are optional — only configured fields are injected.
|
|
2340
|
+
|
|
2341
|
+
### Behavior
|
|
2342
|
+
|
|
2343
|
+
| Operation | Fields injected | Source |
|
|
2344
|
+
|-----------|----------------|--------|
|
|
2345
|
+
| **INSERT** | `createdBy` + `createdAt` + `updatedBy` + `updatedAt` | `req.user.username` + timestamp |
|
|
2346
|
+
| **UPDATE** | `updatedBy` + `updatedAt` | `req.user.username` + timestamp |
|
|
2347
|
+
| **DELETE** | None | No change |
|
|
2348
|
+
|
|
2349
|
+
### Timezone
|
|
2350
|
+
|
|
2351
|
+
- Without `timezone` → UTC (default)
|
|
2352
|
+
- With `timezone` → IANA string (e.g., `"America/El_Salvador"`, `"America/Tegucigalpa"`, `"US/Eastern"`)
|
|
2353
|
+
- Use timezone when the database stores local time (most enterprise DBs)
|
|
2354
|
+
- Uses `Intl.DateTimeFormat` — works regardless of server physical location
|
|
2355
|
+
|
|
2356
|
+
### Security
|
|
2357
|
+
|
|
2358
|
+
- Audit values **override** any client-sent values — prevents spoofing
|
|
2359
|
+
- No username available (public endpoint) → only timestamp fields injected
|
|
2360
|
+
|
|
2361
|
+
---
|
|
2362
|
+
|
|
2363
|
+
## Typed Contract (SP4)
|
|
2364
|
+
|
|
2365
|
+
Cross-validate screen specs against api-specs before deploy. Only possible because both sides are declarative JSON.
|
|
2366
|
+
|
|
2367
|
+
### Usage
|
|
2368
|
+
|
|
2369
|
+
```bash
|
|
2370
|
+
# Single api-spec
|
|
2371
|
+
mythik contract --app app-demo --api records-api
|
|
2372
|
+
|
|
2373
|
+
# Multiple api-specs (modular backends)
|
|
2374
|
+
mythik contract --app app-demo --api inventory-api,users-api
|
|
2375
|
+
|
|
2376
|
+
# Api-specs in separate table
|
|
2377
|
+
mythik contract --app app-demo --api records-api --api-table api_specs
|
|
2378
|
+
|
|
2379
|
+
# Strip base URL from screen fetch URLs
|
|
2380
|
+
mythik contract --app app-demo --api records-api --base-url http://localhost:3010
|
|
2381
|
+
|
|
2382
|
+
# JSON output for CI/CD (exit code 1 on errors)
|
|
2383
|
+
mythik contract --app app-demo --api records-api --json
|
|
2384
|
+
```
|
|
2385
|
+
|
|
2386
|
+
### Rules
|
|
2387
|
+
|
|
2388
|
+
| Rule | Level | What it checks |
|
|
2389
|
+
|------|-------|----------------|
|
|
2390
|
+
| `endpoints-exist` | error | Every fetch URL in screens matches an endpoint, catalog, or builtin in the api-spec |
|
|
2391
|
+
| `fields-valid` | error | POST/PUT body fields exist in CRUD `insertable`/`updatable` definitions |
|
|
2392
|
+
| `params-match` | warning | Query params match endpoint `params` config. `page`/`pageSize` always valid |
|
|
2393
|
+
| `permissions-consistent` | warning | If AppSpec `roleAccess` grants role access to a screen, endpoints used by that screen allow that role via `policies` |
|
|
2394
|
+
|
|
2395
|
+
All rules include Levenshtein "did you mean?" suggestions when names are close matches.
|
|
2396
|
+
|
|
2397
|
+
Duplicate findings (same rule + screen + level + message) are automatically collapsed with a `count` field. Template domain URLs (`http://${authDomain}/api/items`) are normalized to relative paths before matching.
|
|
2398
|
+
|
|
2399
|
+
### Extensibility
|
|
2400
|
+
|
|
2401
|
+
Rules are pluggable. Adding a new rule = creating one file that implements `ContractRule` interface + adding to the rules array. No engine changes needed. When the backend evolves (data models, enums, response schemas), new rules plug in trivially.
|
|
2402
|
+
|
|
2403
|
+
---
|
|
2404
|
+
|
|
2405
|
+
## File Upload
|
|
2406
|
+
|
|
2407
|
+
### File Upload Primitive
|
|
2408
|
+
|
|
2409
|
+
The `file-upload` primitive is a god-primitive with preview, progress, validation, drop zone, and auto/manual upload modes.
|
|
2410
|
+
|
|
2411
|
+
**Props:**
|
|
2412
|
+
|
|
2413
|
+
| Prop | Type | Default | Description |
|
|
2414
|
+
|------|------|---------|-------------|
|
|
2415
|
+
| `accept` | string | `"*"` | Accepted MIME types or extensions. e.g., `"image/*"`, `".pdf,.docx"`, `"image/jpeg"` |
|
|
2416
|
+
| `multiple` | boolean | `false` | Allow multiple files |
|
|
2417
|
+
| `maxSize` | number | `10485760` (10MB) | Max bytes per file |
|
|
2418
|
+
| `maxFiles` | number | `10` | Max files (when multiple) |
|
|
2419
|
+
| `preview` | boolean | `true` | Show file previews (thumbnails for images, extension icons for others) |
|
|
2420
|
+
| `dropZone` | boolean | `false` | Drag-and-drop area instead of button |
|
|
2421
|
+
| `autoUpload` | boolean | `true` | Upload immediately on file selection |
|
|
2422
|
+
| `label` | string/expression | `"Choose file"` | Button/zone text. Supports `$i18n` |
|
|
2423
|
+
|
|
2424
|
+
**Example — avatar upload:**
|
|
2425
|
+
```json
|
|
2426
|
+
{
|
|
2427
|
+
"type": "file-upload",
|
|
2428
|
+
"props": {
|
|
2429
|
+
"accept": "image/*",
|
|
2430
|
+
"maxSize": 5242880,
|
|
2431
|
+
"dropZone": true,
|
|
2432
|
+
"label": { "$i18n": "upload.avatar" }
|
|
2433
|
+
},
|
|
2434
|
+
"on": {
|
|
2435
|
+
"upload": {
|
|
2436
|
+
"action": "uploadFile",
|
|
2437
|
+
"params": {
|
|
2438
|
+
"bucket": "avatars",
|
|
2439
|
+
"path": { "$template": "users/${/auth/user/id}/${filename}" },
|
|
2440
|
+
"target": "/form/avatarUrl"
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
```
|
|
2446
|
+
|
|
2447
|
+
**Example — document upload (manual):**
|
|
2448
|
+
```json
|
|
2449
|
+
{
|
|
2450
|
+
"type": "file-upload",
|
|
2451
|
+
"props": {
|
|
2452
|
+
"accept": ".pdf,.docx,.xlsx",
|
|
2453
|
+
"multiple": true,
|
|
2454
|
+
"maxFiles": 5,
|
|
2455
|
+
"autoUpload": false,
|
|
2456
|
+
"label": "Attach documents"
|
|
2457
|
+
},
|
|
2458
|
+
"on": {
|
|
2459
|
+
"upload": {
|
|
2460
|
+
"action": "uploadFile",
|
|
2461
|
+
"params": {
|
|
2462
|
+
"bucket": "documents",
|
|
2463
|
+
"target": "/form/attachments"
|
|
2464
|
+
}
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
}
|
|
2468
|
+
```
|
|
2469
|
+
With `autoUpload: false`, files are selected and previewed but not uploaded until triggered by a button with the `uploadFile` action.
|
|
2470
|
+
|
|
2471
|
+
### `uploadFile` Action
|
|
2472
|
+
|
|
2473
|
+
Uploads files to storage via the configured StorageAdapter. Writes the resulting URL(s) to the `target` state path.
|
|
2474
|
+
|
|
2475
|
+
| Param | Type | Required | Description |
|
|
2476
|
+
|-------|------|----------|-------------|
|
|
2477
|
+
| `bucket` | string | Yes | Storage bucket name |
|
|
2478
|
+
| `path` | string/expression | No | Path within bucket. `${filename}` replaced with original name. Default: original filename |
|
|
2479
|
+
| `target` | string | Yes | State path to write URL(s). String for single file, array for multiple |
|
|
2480
|
+
|
|
2481
|
+
**Behavior:**
|
|
2482
|
+
- Validates file type against `accept` and `maxSize`
|
|
2483
|
+
- Uploads via StorageAdapter with progress tracking
|
|
2484
|
+
- Writes progress to `/ui/uploads/{elementId}/files` (internal — don't read directly)
|
|
2485
|
+
- On success: URL written to `target`, status `done`
|
|
2486
|
+
- On failure: 1 automatic retry after 1s, then `error` status with retry button
|
|
2487
|
+
- Single file → `target` receives a URL string
|
|
2488
|
+
- Multiple files → `target` receives a URL array
|
|
2489
|
+
|
|
2490
|
+
### `deleteFile` Action
|
|
2491
|
+
|
|
2492
|
+
Deletes a file from storage.
|
|
2493
|
+
|
|
2494
|
+
| Param | Type | Required | Description |
|
|
2495
|
+
|-------|------|----------|-------------|
|
|
2496
|
+
| `path` | string | Yes | File path in storage |
|
|
2497
|
+
| `bucket` | string | Yes | Storage bucket name |
|
|
2498
|
+
|
|
2499
|
+
```json
|
|
2500
|
+
{ "action": "deleteFile", "params": { "path": { "$state": "/form/avatarPath" }, "bucket": "avatars" } }
|
|
2501
|
+
```
|
|
2502
|
+
|
|
2503
|
+
### StorageAdapter Setup (Host App)
|
|
2504
|
+
|
|
2505
|
+
The host app provides a StorageAdapter to enable file uploads:
|
|
2506
|
+
|
|
2507
|
+
**Supabase Storage:**
|
|
2508
|
+
```tsx
|
|
2509
|
+
import { createSupabaseStorageAdapter } from 'mythik';
|
|
2510
|
+
|
|
2511
|
+
<MythikRenderer
|
|
2512
|
+
spec={spec}
|
|
2513
|
+
instance={svc}
|
|
2514
|
+
storage={createSupabaseStorageAdapter({ client: supabaseClient })}
|
|
2515
|
+
/>
|
|
2516
|
+
```
|
|
2517
|
+
|
|
2518
|
+
**Generic URL (any backend):**
|
|
2519
|
+
```tsx
|
|
2520
|
+
import { createUrlStorageAdapter } from 'mythik';
|
|
2521
|
+
|
|
2522
|
+
<MythikRenderer
|
|
2523
|
+
spec={spec}
|
|
2524
|
+
instance={svc}
|
|
2525
|
+
storage={createUrlStorageAdapter({
|
|
2526
|
+
uploadUrl: 'https://api.example.com/upload',
|
|
2527
|
+
deleteUrl: 'https://api.example.com/files',
|
|
2528
|
+
responsePath: 'data.url',
|
|
2529
|
+
headers: { 'X-Api-Key': apiKey },
|
|
2530
|
+
})}
|
|
2531
|
+
/>
|
|
2532
|
+
```
|
|
2533
|
+
|
|
2534
|
+
**Global limits (optional security backstop):**
|
|
2535
|
+
```tsx
|
|
2536
|
+
<MythikRenderer
|
|
2537
|
+
spec={spec}
|
|
2538
|
+
instance={svc}
|
|
2539
|
+
storage={adapter}
|
|
2540
|
+
storageConfig={{
|
|
2541
|
+
allowedTypes: ['image/jpeg', 'image/png', 'application/pdf'],
|
|
2542
|
+
maxSize: 10_485_760,
|
|
2543
|
+
}}
|
|
2544
|
+
/>
|
|
2545
|
+
```
|
|
2546
|
+
|
|
2547
|
+
### Upload State Paths
|
|
2548
|
+
|
|
2549
|
+
| Path | Owner | Description |
|
|
2550
|
+
|------|-------|-------------|
|
|
2551
|
+
| `target` (e.g., `/form/avatarUrl`) | Spec author | URL string (single) or URL array (multiple). Use this for persistence |
|
|
2552
|
+
| `/ui/uploads/{elementId}/files` | Framework | Array of `{ name, size, type, progress, status, previewUrl, error }`. Internal — don't read directly |
|
|
2553
|
+
|
|
2554
|
+
The primitive reads `/ui/uploads/*` automatically. The spec author only needs to read the `target` path.
|
|
2555
|
+
|
|
2556
|
+
---
|
|
2557
|
+
|
|
2558
|
+
## Skeleton Primitive
|
|
2559
|
+
|
|
2560
|
+
Loading placeholders that replicate spec structure while data loads.
|
|
2561
|
+
|
|
2562
|
+
### Auto-Skeleton (Zero Config)
|
|
2563
|
+
|
|
2564
|
+
When a spec has `initialActions` with a `fetch` action, the renderer auto-detects loading state and replaces data-dependent primitives with shimmer skeletons. No configuration needed.
|
|
2565
|
+
|
|
2566
|
+
**Activation conditions (all must be true):**
|
|
2567
|
+
1. Spec has `initialActions` containing a `fetch` action
|
|
2568
|
+
2. `/ui/loading` is `true`
|
|
2569
|
+
3. The fetch target path is empty/undefined/empty array
|
|
2570
|
+
|
|
2571
|
+
**Shape mapping:** `text` → text line, `image` → rectangle, `button` → button shape, `input/textarea/select` → input rectangle, `icon` → circle, charts → large rectangle, `table` → table placeholder. Layout containers (`stack`, `grid`, `box`, `scroll`) pass through to children. Overlays (`modal`, `drawer`, `tabs`) are skipped.
|
|
2572
|
+
|
|
2573
|
+
### Element-Level Opt-Out
|
|
2574
|
+
|
|
2575
|
+
```json
|
|
2576
|
+
{
|
|
2577
|
+
"type": "text",
|
|
2578
|
+
"props": { "content": "Static Header" },
|
|
2579
|
+
"skeleton": false
|
|
2580
|
+
}
|
|
2581
|
+
```
|
|
2582
|
+
|
|
2583
|
+
Elements with `skeleton: false` render normally during loading. Use for static content that doesn't depend on fetched data.
|
|
2584
|
+
|
|
2585
|
+
### Manual Skeleton
|
|
2586
|
+
|
|
2587
|
+
For custom loading layouts, place `type: "skeleton"` elements directly:
|
|
2588
|
+
|
|
2589
|
+
```json
|
|
2590
|
+
{
|
|
2591
|
+
"type": "skeleton",
|
|
2592
|
+
"props": {
|
|
2593
|
+
"variant": "text",
|
|
2594
|
+
"width": "80%",
|
|
2595
|
+
"height": 16,
|
|
2596
|
+
"count": 3,
|
|
2597
|
+
"gap": 8
|
|
2598
|
+
},
|
|
2599
|
+
"visible": { "$state": "/ui/loading" }
|
|
2600
|
+
}
|
|
2601
|
+
```
|
|
2602
|
+
|
|
2603
|
+
| Prop | Type | Default | Description |
|
|
2604
|
+
|------|------|---------|-------------|
|
|
2605
|
+
| `variant` | `"text" \| "circle" \| "rect"` | `"rect"` | Shape of the skeleton |
|
|
2606
|
+
| `width` | `string \| number` | `"100%"` | Width (px or %) |
|
|
2607
|
+
| `height` | `number` | `16` | Height in px |
|
|
2608
|
+
| `count` | `number` | `1` | Number of shapes to render |
|
|
2609
|
+
| `gap` | `number` | `8` | Gap between shapes (when count > 1) |
|
|
2610
|
+
|
|
2611
|
+
### Disable Auto-Skeleton
|
|
2612
|
+
|
|
2613
|
+
Set `autoSkeleton={false}` on `MythikRenderer` to disable completely:
|
|
2614
|
+
|
|
2615
|
+
```tsx
|
|
2616
|
+
<MythikRenderer spec={spec} instance={svc} autoSkeleton={false} />
|
|
2617
|
+
```
|
|
2618
|
+
|
|
2619
|
+
---
|
|
2620
|
+
|
|
2621
|
+
## Export Action
|
|
2622
|
+
|
|
2623
|
+
Declarative data export with CSV built-in and adapter pattern for XLSX/PDF.
|
|
2624
|
+
|
|
2625
|
+
### Usage
|
|
2626
|
+
|
|
2627
|
+
```json
|
|
2628
|
+
{
|
|
2629
|
+
"action": "export",
|
|
2630
|
+
"params": {
|
|
2631
|
+
"source": "/tasks",
|
|
2632
|
+
"columns": [
|
|
2633
|
+
{ "field": "title", "label": "Title" },
|
|
2634
|
+
{ "field": "price", "label": "Price", "format": "currency", "formatOptions": { "currency": "USD" } },
|
|
2635
|
+
{ "field": "createdAt", "label": "Created", "format": "date" }
|
|
2636
|
+
],
|
|
2637
|
+
"filename": "tasks-report",
|
|
2638
|
+
"format": "csv"
|
|
2639
|
+
}
|
|
2640
|
+
}
|
|
2641
|
+
```
|
|
2642
|
+
|
|
2643
|
+
| Param | Type | Required | Description |
|
|
2644
|
+
|-------|------|----------|-------------|
|
|
2645
|
+
| `source` | string | Yes | State path to array of data |
|
|
2646
|
+
| `columns` | ExportColumn[] | Yes | Column definitions with optional formatting |
|
|
2647
|
+
| `filename` | string | Yes | Download filename (without extension) |
|
|
2648
|
+
| `format` | string | No | `"csv"` (default), or any custom format with registered adapter |
|
|
2649
|
+
|
|
2650
|
+
### Column Formatting
|
|
2651
|
+
|
|
2652
|
+
| Format | Example Output | Options |
|
|
2653
|
+
|--------|---------------|---------|
|
|
2654
|
+
| `currency` | `$1,234.50` | `currency` (code), `decimals`, `locale` |
|
|
2655
|
+
| `number` | `1,234.50` | `decimals`, `locale` |
|
|
2656
|
+
| `percent` | `75%` | `decimals`, `locale` |
|
|
2657
|
+
| `date` | `4/5/2026` | `locale` |
|
|
2658
|
+
|
|
2659
|
+
### CSV (Built-in, Zero Dependencies)
|
|
2660
|
+
|
|
2661
|
+
Format `"csv"` is always available. RFC 4180 escaping. Downloads as `.csv` file.
|
|
2662
|
+
|
|
2663
|
+
### XLSX / PDF (Via Adapter)
|
|
2664
|
+
|
|
2665
|
+
Register adapters via `exportAdapters` prop:
|
|
2666
|
+
|
|
2667
|
+
```tsx
|
|
2668
|
+
<MythikRenderer
|
|
2669
|
+
spec={spec}
|
|
2670
|
+
instance={svc}
|
|
2671
|
+
exportAdapters={{
|
|
2672
|
+
xlsx: myXlsxAdapter,
|
|
2673
|
+
pdf: myPdfAdapter,
|
|
2674
|
+
}}
|
|
2675
|
+
/>
|
|
2676
|
+
```
|
|
2677
|
+
|
|
2678
|
+
Adapters implement `ExportAdapter`:
|
|
2679
|
+
|
|
2680
|
+
```typescript
|
|
2681
|
+
interface ExportAdapter {
|
|
2682
|
+
generate(data: ExportData, format: string): Promise<Blob>;
|
|
2683
|
+
}
|
|
2684
|
+
```
|
|
2685
|
+
|
|
2686
|
+
`ExportData` includes both raw `rows` and pre-formatted `formattedRows`. XLSX adapters may use raw values with number formatting, PDF adapters may use formatted strings.
|
|
2687
|
+
|
|
2688
|
+
---
|
|
2689
|
+
|
|
2690
|
+
## Deep Design Token System
|
|
2691
|
+
|
|
2692
|
+
Three-layer token resolution that gives every app a unique visual identity from minimal configuration:
|
|
2693
|
+
|
|
2694
|
+
```
|
|
2695
|
+
Layer 1: Framework DEFAULTS (always present — app looks good out of the box)
|
|
2696
|
+
Layer 2: DNA derivation (OKLCH color science — 8 seeds generate full identity)
|
|
2697
|
+
Layer 3: Manual overrides (explicit values always win)
|
|
2698
|
+
```
|
|
2699
|
+
|
|
2700
|
+
### DNA Seeds
|
|
2701
|
+
|
|
2702
|
+
Define in `tokens.dna` at the AppSpec level. The framework derives ALL token categories automatically:
|
|
2703
|
+
|
|
2704
|
+
```json
|
|
2705
|
+
{
|
|
2706
|
+
"tokens": {
|
|
2707
|
+
"dna": {
|
|
2708
|
+
"primary": "#0D9488",
|
|
2709
|
+
"harmony": "analogous",
|
|
2710
|
+
"roundness": 0.7,
|
|
2711
|
+
"density": 0.5,
|
|
2712
|
+
"depth": 0.6,
|
|
2713
|
+
"motion": "fluid",
|
|
2714
|
+
"formality": 0.3
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
```
|
|
2719
|
+
|
|
2720
|
+
| Seed | Type | Default | Controls |
|
|
2721
|
+
|------|------|---------|----------|
|
|
2722
|
+
| `primary` | hex | (required) | Base color → OKLCH 13-stop tonal palette |
|
|
2723
|
+
| `harmony` | `complementary` \| `analogous` \| `triadic` \| `split-complementary` | `complementary` | Accent color hue rotation (analogous=+50°, triadic=+120°, split=+150°, complementary=+180°) |
|
|
2724
|
+
| `accent` | hex | derived | Explicit accent (skips harmony) |
|
|
2725
|
+
| `neutral` | `warm` \| `cool` \| `natural` | `natural` | Neutral palette tint. warm=amber hue (55°), cool=blue (250°), natural=primary hue. Chroma 0.025 (natural) / 0.035 (warm/cool) — visible in backgrounds and borders |
|
|
2726
|
+
| `roundness` | 0–1 | 0.5 | Border radii: 0=sharp, 1=pill |
|
|
2727
|
+
| `density` | 0–1 | 0.5 | Font size + spacing: 0=airy, 1=compact |
|
|
2728
|
+
| `depth` | 0–1 | 0.5 | Shadow intensity: 0=flat, 1=elevated |
|
|
2729
|
+
| `motion` | `fluid` \| `snappy` \| `gentle` \| `energetic` | `gentle` | Animation duration, spring, stagger |
|
|
2730
|
+
| `formality` | 0–1 | 0.5 | Typography: 5 font tiers. Body: Inter → Space Grotesk → Source Sans 3 → Lora → Merriweather. Heading: Inter → Space Grotesk → DM Sans → Lora → Playfair Display. Also drives continuous letterSpacing (0 → 0.03em), headingLetterSpacing (0 → -0.02em), lineHeight ratio (1.45 → 1.6), bold weight (700 → 800) |
|
|
2731
|
+
|
|
2732
|
+
### Three Control Levels
|
|
2733
|
+
|
|
2734
|
+
**Level 1 — Just a color:** `"dna": { "primary": "#0D9488" }` → full coherent identity.
|
|
2735
|
+
|
|
2736
|
+
**Level 2 — Personality:** `"dna": { "primary": "#0D9488", "roundness": 0.7, "motion": "fluid" }` → unique identity from 5 values.
|
|
2737
|
+
|
|
2738
|
+
**Level 3 — Override specific tokens:** DNA generates base, explicit values win:
|
|
2739
|
+
```json
|
|
2740
|
+
{ "tokens": { "dna": { "primary": "#0D9488" }, "colors": { "error": "#DC2626" }, "shape": { "radius": { "md": 24 } } } }
|
|
2741
|
+
```
|
|
2742
|
+
|
|
2743
|
+
### Token Categories & `$token` Paths
|
|
2744
|
+
|
|
2745
|
+
All primitives consume these via `useDesignTokens` hook internally. Specs reference them via `$token`:
|
|
2746
|
+
|
|
2747
|
+
| Category | Paths | Example |
|
|
2748
|
+
|----------|-------|---------|
|
|
2749
|
+
| Colors | `colors.primary`, `colors.surface`, `colors.text`, `colors.error`, `colors.accent`, etc. (13 semantic colors) | `{ "$token": "colors.primary" }` |
|
|
2750
|
+
| Shape | `shape.radius.none/sm/md/lg/xl/full` | `{ "$token": "shape.radius.lg" }` |
|
|
2751
|
+
| Typography | `typography.fontFamily.base/heading/mono`, `typography.scale.xs/sm/md/lg/xl/2xl` (each has `.fontSize`, `.lineHeight`), `typography.weight.normal/medium/semibold/bold`, `typography.letterSpacing`, `typography.headingLetterSpacing` | `{ "$token": "typography.scale.xl.fontSize" }` |
|
|
2752
|
+
| Spacing | `spacing.unit`, `spacing.scale.xs/sm/md/lg/xl/2xl` | `{ "$token": "spacing.scale.md" }` |
|
|
2753
|
+
| Elevation | `elevation.none/sm/md/lg/xl` — auto-converts to CSS `boxShadow` string when used in `$token` | `{ "$token": "elevation.lg" }` |
|
|
2754
|
+
| Motion | `motion.duration.fast/normal/slow`, `motion.easing.default/enter/exit`, `motion.spring.damping/stiffness/mass`, `motion.stagger` | `{ "$token": "motion.duration.fast" }` |
|
|
2755
|
+
| Opacity | `opacity.disabled/pressed/backdrop/muted` | `{ "$token": "opacity.backdrop" }` |
|
|
2756
|
+
|
|
2757
|
+
### Auto Dark Mode
|
|
2758
|
+
|
|
2759
|
+
DNA automatically generates `modes.dark` by inverting tonal palette stops. No manual dark theme needed. Override with explicit `modes.dark` values if desired. `toggleTheme` action works automatically.
|
|
2760
|
+
|
|
2761
|
+
### Runtime Token Update
|
|
2762
|
+
|
|
2763
|
+
Use `updateTokens` action to change tokens without page reload:
|
|
2764
|
+
```json
|
|
2765
|
+
{ "action": "updateTokens", "params": { "dna": { "primary": "#F97316", "roundness": 0.9 } } }
|
|
2766
|
+
```
|
|
2767
|
+
All primitives re-render with new tokens. Numeric seeds > 1 are auto-divided by 100 (slider 0-100 compatibility).
|
|
2768
|
+
|
|
2769
|
+
### Presets System
|
|
2770
|
+
|
|
2771
|
+
Complete DNA+Identity snapshots that transform the entire look of an app with a single action. The framework provides the **tools** (contract, registration, application); preset definitions live **outside core** (in the app, a file, or a future API like mythik.dev).
|
|
2772
|
+
|
|
2773
|
+
**PresetDefinition interface:**
|
|
2774
|
+
```typescript
|
|
2775
|
+
interface PresetDefinition {
|
|
2776
|
+
id: string; // 'startup-saas'
|
|
2777
|
+
name: string; // 'Startup SaaS'
|
|
2778
|
+
description: string; // 'Clean, professional, outlined'
|
|
2779
|
+
tags?: string[]; // ['professional', 'minimal']
|
|
2780
|
+
tokens: {
|
|
2781
|
+
dna: DnaSeed;
|
|
2782
|
+
identity: IdentityConfig;
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
```
|
|
2786
|
+
|
|
2787
|
+
**Registration:**
|
|
2788
|
+
```typescript
|
|
2789
|
+
const svc = createMythik({ tokens: { dna: { primary: '#0D9488' } } });
|
|
2790
|
+
svc.plugins.registerPresets(myPresets); // stores presets + writes /presets/available to state
|
|
2791
|
+
svc.applyPlugins();
|
|
2792
|
+
```
|
|
2793
|
+
|
|
2794
|
+
`registerPresets` automatically:
|
|
2795
|
+
1. Stores presets in internal Map
|
|
2796
|
+
2. Writes dropdown options to state at `/presets/available` (array of `{ value, label }`)
|
|
2797
|
+
3. Auto-registers `applyPreset` action
|
|
2798
|
+
|
|
2799
|
+
**Applying a preset from a spec:**
|
|
2800
|
+
```json
|
|
2801
|
+
{ "action": "applyPreset", "params": { "preset": "startup-saas" } }
|
|
2802
|
+
```
|
|
2803
|
+
|
|
2804
|
+
Uses `_replace: true` internally — fully replaces all tokens, no stale values from previous presets.
|
|
2805
|
+
|
|
2806
|
+
`applyPreset` with `preset: 'custom'` is a no-op (silently returns). Unknown preset IDs throw.
|
|
2807
|
+
|
|
2808
|
+
**Dynamic dropdown in specs:**
|
|
2809
|
+
```json
|
|
2810
|
+
{
|
|
2811
|
+
"type": "select",
|
|
2812
|
+
"props": {
|
|
2813
|
+
"options": { "$state": "/presets/available" },
|
|
2814
|
+
"value": { "$bindState": "/ui/currentPreset" }
|
|
2815
|
+
},
|
|
2816
|
+
"on": {
|
|
2817
|
+
"change": [{ "action": "applyPreset", "params": { "preset": { "$state": "/ui/currentPreset" } } }]
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
```
|
|
2821
|
+
|
|
2822
|
+
**Accessors:** `plugins.getPresets()` returns all registered presets. `plugins.getPreset(id)` returns one by ID.
|
|
2823
|
+
|
|
2824
|
+
### Font Loading
|
|
2825
|
+
|
|
2826
|
+
DNA generates font family names but does NOT load fonts. Web projects must include `@font-face` or Google Fonts. RN projects use `expo-font`. This is the project's responsibility, not the framework's.
|
|
2827
|
+
|
|
2828
|
+
### CLI Token Inspection
|
|
2829
|
+
|
|
2830
|
+
```bash
|
|
2831
|
+
mythik tokens --dna '{"primary":"#0D9488","roundness":0.7}' --json
|
|
2832
|
+
```
|
|
2833
|
+
Shows the full resolved token set for debugging and inspection.
|
|
2834
|
+
|
|
2835
|
+
---
|
|
2836
|
+
|
|
2837
|
+
## Component Variants
|
|
2838
|
+
|
|
2839
|
+
Token-driven component styling for any primitive. Define variant definitions in `tokens.components` — the engine applies style, hover, active, focus, and transition automatically.
|
|
2840
|
+
|
|
2841
|
+
### Defining Variants
|
|
2842
|
+
|
|
2843
|
+
Variants live in `tokens.components.{primitiveType}.{variantName}`:
|
|
2844
|
+
|
|
2845
|
+
```json
|
|
2846
|
+
{
|
|
2847
|
+
"tokens": {
|
|
2848
|
+
"colors": { "primary": "#0D9488", "surface": "#FFFFFF", "text": "#0F172A", "error": "#EF4444" },
|
|
2849
|
+
"radius": { "md": 12, "lg": 16 },
|
|
2850
|
+
"shadow": { "md": "0 4px 6px rgba(0,0,0,0.07)", "lg": "0 10px 25px rgba(0,0,0,0.1)" },
|
|
2851
|
+
|
|
2852
|
+
"components": {
|
|
2853
|
+
"button": {
|
|
2854
|
+
"primary": {
|
|
2855
|
+
"style": { "backgroundColor": "$colors.primary", "color": "#FFF", "borderRadius": "$radius.md", "padding": "10px 22px", "fontWeight": 700 },
|
|
2856
|
+
"hover": { "scale": 1.05, "y": -1 },
|
|
2857
|
+
"active": { "scale": 0.95 },
|
|
2858
|
+
"transition": { "duration": 0.15 }
|
|
2859
|
+
},
|
|
2860
|
+
"danger": {
|
|
2861
|
+
"style": { "backgroundColor": "$colors.error", "color": "#FFF", "borderRadius": "$radius.md" },
|
|
2862
|
+
"hover": { "scale": 1.03 },
|
|
2863
|
+
"active": { "scale": 0.95 }
|
|
2864
|
+
}
|
|
2865
|
+
},
|
|
2866
|
+
"box": {
|
|
2867
|
+
"card": {
|
|
2868
|
+
"style": { "backgroundColor": "$colors.surface", "borderRadius": "$radius.lg", "padding": 16, "boxShadow": "$shadow.md" },
|
|
2869
|
+
"hover": { "boxShadow": "$shadow.lg", "y": -2 },
|
|
2870
|
+
"transition": { "duration": 0.2 }
|
|
2871
|
+
}
|
|
2872
|
+
},
|
|
2873
|
+
"text": {
|
|
2874
|
+
"heading": {
|
|
2875
|
+
"style": { "fontSize": 24, "fontWeight": 700, "color": "$colors.text" }
|
|
2876
|
+
}
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
```
|
|
2882
|
+
|
|
2883
|
+
### Using Variants in Specs
|
|
2884
|
+
|
|
2885
|
+
Just set `variant` in props — no style copying needed:
|
|
2886
|
+
|
|
2887
|
+
```json
|
|
2888
|
+
{ "type": "button", "props": { "label": "Save", "variant": "primary" } }
|
|
2889
|
+
{ "type": "button", "props": { "label": "Delete", "variant": "danger" } }
|
|
2890
|
+
{ "type": "box", "props": { "variant": "card" }, "children": ["content"] }
|
|
2891
|
+
```
|
|
2892
|
+
|
|
2893
|
+
### Supported Variant Properties
|
|
2894
|
+
|
|
2895
|
+
| Property | Type | Purpose |
|
|
2896
|
+
|----------|------|---------|
|
|
2897
|
+
| `style` | object | CSS styles — applied as base |
|
|
2898
|
+
| `hover` | object | Hover state overrides |
|
|
2899
|
+
| `active` | object | Active/tap state overrides |
|
|
2900
|
+
| `focus` | object | Focus state overrides |
|
|
2901
|
+
| `transition` | object | Animation config |
|
|
2902
|
+
|
|
2903
|
+
### `$path` Token References
|
|
2904
|
+
|
|
2905
|
+
String values starting with `$` reference other tokens. Resolved against the active token tree (with dark mode already applied):
|
|
2906
|
+
|
|
2907
|
+
- `"$colors.primary"` → `"#0D9488"`
|
|
2908
|
+
- `"$shape.radius.md"` → `12`
|
|
2909
|
+
- `"$elevation.lg"` → CSS boxShadow string
|
|
2910
|
+
- `"$typography.weight.bold"` → `700`
|
|
2911
|
+
- `"$spacing.scale.md"` → `16`
|
|
2912
|
+
|
|
2913
|
+
Legacy paths (`$radius.md`, `$shadow.lg`) still work for backward compatibility.
|
|
2914
|
+
|
|
2915
|
+
**Dark mode works automatically.** When dark mode changes `colors.surface` to `#1E293B`, any variant using `$colors.surface` gets the dark value — no separate dark variant needed.
|
|
2916
|
+
|
|
2917
|
+
### Merge Precedence
|
|
2918
|
+
|
|
2919
|
+
Variant is base, element-level overrides. Same as CSS (class defaults + inline override):
|
|
2920
|
+
|
|
2921
|
+
```json
|
|
2922
|
+
{
|
|
2923
|
+
"type": "button",
|
|
2924
|
+
"props": { "variant": "primary" },
|
|
2925
|
+
"style": { "borderRadius": 20 },
|
|
2926
|
+
"hover": { "rotate": 5 }
|
|
2927
|
+
}
|
|
2928
|
+
```
|
|
2929
|
+
|
|
2930
|
+
Result: variant's `backgroundColor`, `color`, `padding` apply. Element's `borderRadius: 20` overrides variant's `borderRadius: 12`. Hover merges: variant's `scale: 1.05` + element's `rotate: 5`.
|
|
2931
|
+
|
|
2932
|
+
### Works for Any Primitive
|
|
2933
|
+
|
|
2934
|
+
Not just buttons — boxes, text, inputs, any primitive type can have variants defined in `tokens.components`.
|
|
2935
|
+
|
|
2936
|
+
---
|
|
2937
|
+
|
|
2938
|
+
## Background System v2 — Layered Backgrounds
|
|
2939
|
+
|
|
2940
|
+
Mythik backgrounds use a structured `LayerBackground` model. Apps declare `tokens.identity.background` with a base color and ordered layers; the renderer mounts a root `BackgroundStack` so backgrounds remain app-level composition, not per-card decoration.
|
|
2941
|
+
|
|
2942
|
+
### Layered background contract
|
|
2943
|
+
|
|
2944
|
+
171. **`LayerBackground` type** — New structured background type with `color?: string` and `layers?: LayerConfig[]`. Unions: `SolidLayerConfig`, `GradientLayerConfig`, `PatternLayerConfig`, `GrainLayerConfig`, `ImageLayerConfig`. See `packages/core/src/design/identity/types.ts`
|
|
2945
|
+
|
|
2946
|
+
172. **`resolveBackgroundLayers(config)` function** — Transforms `LayerBackground` into `LayerSpec[]` (structured renderer-ready specs). Base color always emitted first, then layers in array order. `zIndex` uses array index as fallback or explicit override. Entry point for any consumer rendering custom backgrounds
|
|
2947
|
+
|
|
2948
|
+
173. **8 curated background recipes shipped in `tokens.backgrounds.*`** — `linear-aura` (Linear-like), `stripe-ribbons` (Stripe-like), `vercel-center`, `arc-organic`, `grid-subtle`, `notion-warm`, `raycast-mono`, `comic-pop`. Exported as `BACKGROUND_RECIPES` constant. Consumers access via recipe name string or expand and modify
|
|
2949
|
+
|
|
2950
|
+
174. **Pattern primitives** — 6 curated kinds (`grid`, `dots`, `diagonal`, `iso`, `crosshatch`, `chevron`) + `custom-svg` escape hatch. Each emits `<pattern>` SVG fragment. Params: `spacing`, `thickness`, `color`, `angle`, `dotRadius` (dots only). Custom-svg passes through `sanitizeSVGShapes` (strict allowlist: shapes + geometric attrs only; rejects `<script>`, `<foreignObject>`, `<use href>`, `on*` events, `javascript:` URLs)
|
|
2951
|
+
|
|
2952
|
+
175. **Gradient primitive enhanced** — Linear/radial/conic with custom `stops[]` (each with `color`, `opacity`, `at` percentage), `position`, `size`, `shape`. Opacity→rgba conversion automatic. Previously was `{ angle }` only with hardcoded `30% 50%` position and fixed opacity values. New API: `{ type: 'gradient', kind: 'radial', shape: 'circle', size: '500px', position: '0% 20%', stops: [...] }`
|
|
2953
|
+
|
|
2954
|
+
176. **Grain primitive** — `{ type: 'grain', intensity, scale, monochrome }` emits SVG `feTurbulence` filter for noise overlay. Default `intensity: 0.05`, `scale: 0.9`, `monochrome: true` (premium desaturated grain). Cross-platform (web + react-native-svg)
|
|
2955
|
+
|
|
2956
|
+
177. **Layer composition via `layers[]` array** — Any mix of primitives (solid, gradient, pattern, grain, image) composed as ordered stack. Each layer supports `opacity`, `blendMode` (normal, multiply, screen, overlay, soft-light, hard-light, color-dodge, color-burn), `zIndex`
|
|
2957
|
+
|
|
2958
|
+
178. **`BackgroundLayer` + `BackgroundStack` components shipped for web and RN** — Web: `packages/react/src/background/` using CSS for solid/gradient/image + inline SVG for pattern/grain. RN: `packages/react-native/src/background/` using `react-native-svg` `SvgXml` for all layers + View for solid + Image for image. Accepts `LayerBackground` config OR recipe string name
|
|
2959
|
+
|
|
2960
|
+
179. **`custom-svg` sanitized escape hatch** — When built-in patterns aren't enough, consumers pass SVG shape fragments via `{ kind: 'custom-svg', shapes: '<circle.../>', tileSize: 20 }`. `sanitizeSVGShapes` uses regex pre-strip + DOMPurify allowlist. Only circle/rect/path/line/polygon/polyline/ellipse/g/defs/gradients + geometric attrs survive. Cross-platform safe (isomorphic-dompurify)
|
|
2961
|
+
|
|
2962
|
+
---
|
|
2963
|
+
|
|
2964
|
+
## Animation System
|
|
2965
|
+
|
|
2966
|
+
`Element.animations` is a first-class spec field. Animations can be declared directly on elements and can also cascade through identity, component variants, templates, and custom elements. The same core animation contract feeds web CSS keyframes and React Native Reanimated output where the platform supports the property.
|
|
2967
|
+
|
|
2968
|
+
### Animation contract
|
|
2969
|
+
|
|
2970
|
+
180. **`ElementAnimations` type with 7 triggers** — `mount`, `unmount`, `hover`, `focus`, `active`, `ambient`, `stateChange`. Each trigger accepts `AnimationRef | AnimationRef[] | null`; `null` explicitly disables inherited animation for that trigger in the cascade. See `packages/core/src/design/animation/types.ts`
|
|
2971
|
+
|
|
2972
|
+
181. **`AnimationRef` union: recipe reference or inline animation** — `{ recipe: string, duration?, easing?, delay?, iterations?, stagger?, essential? }` references a curated recipe with optional overrides. Alternative: `InlineAnimation` with explicit `keyframes: KeyframeSnapshot[]`. Resolver throws if a ref contains both `recipe` AND `keyframes` (malformed)
|
|
2973
|
+
|
|
2974
|
+
182. **`KeyframeSnapshot` schema** — `{ at: string, opacity?, transform?: TransformValue, backgroundColor?, borderColor?, borderRadius?, borderWidth?, color?, filter?: FilterValue }`. `at` must be percent format `'0%'` to `'100%'` (out-of-range throws); keyframes must be monotonically non-decreasing. `TransformValue` supports translateX/Y, scale (number or `{x,y}`), rotate, skewX/Y
|
|
2975
|
+
|
|
2976
|
+
183. **`resolveAnimation(ref, recipes)` function** — Transforms `AnimationRef` into fully-normalized `AnimationSpec` with parsed durations (handles `'200ms'`, `'1.5s'`, numeric), defaults applied (`iterations: 1`, `direction: 'normal'`, `fillMode: 'both'`, `easing: 'ease-out'`, `delay: 0`, `essential: false`). Throws on unknown recipe, empty keyframes, non-monotonic `at`, out-of-range `at`
|
|
2977
|
+
|
|
2978
|
+
184. **15 curated animation recipes shipped in `ANIMATION_RECIPES`** — `fade`, `fade-up`, `fade-down`, `scale-in`, `slide-left`, `slide-right`, `lift`, `glow`, `pulse-primary`, `breathe-subtle`, `shimmer`, `float`, `pop`, `shake`, `spin`. Recipe names are mechanical for AI consistency. Platform limitations are explicit: web-only recipes surface validator warnings on React Native instead of silently pretending to render parity.
|
|
2979
|
+
|
|
2980
|
+
185. **`buildCSSKeyframes(spec)` function — web output** — Returns `{ name, keyframesText, animationCSS }`. `name` is `svka-${djb2(fingerprint)}` — deterministic hash of content for Constructable StyleSheets dedup. `animationCSS` is CSS shorthand (`name duration easing delay iter direction fillMode`). `keyframesText` derives `at` from normalized fraction (not raw input) for whitespace-clean output
|
|
2981
|
+
|
|
2982
|
+
186. **`buildReanimatedSpec(spec)` function — RN output** — Returns `{ animatedProps, inputRange, outputRanges, animatedColorProps, timing }`. Emits plain data (no React, no Reanimated imports) — the RN runner wires shared values and `useAnimatedStyle` separately. Nearest-neighbor fill for missing property values at keyframe stops. Numeric rotate/skew stored as `rotateDeg`/`skewXDeg`/`skewYDeg` for correct `interpolate()` input. Non-deg rotate strings (`0.25turn`, `3.14rad`) are skipped on RN (web-only). Percentage `borderRadius` throws on RN (cannot animate natively)
|
|
2983
|
+
|
|
2984
|
+
187. **`applyReducedMotion(spec, trigger)` — moderate a11y policy** — `mount`/`unmount`/`active` strip transforms (keep opacity; returns null if all animated props disappear — I1 degenerate guard). `hover`/`ambient` return null (skip). `focus` preserved (a11y-critical). `stateChange` reduces duration 3x with floor at 50ms. `essential: true` on the spec bypasses entire policy — intended for spinners/loaders where motion is affordance
|
|
2985
|
+
|
|
2986
|
+
188. **`validateAnimationCaps(animations)` — performance gates** — Per-trigger: soft-cap 3 animations (warn), hard-cap 6 (error). Per-element: soft-cap 5 triggers with animations (warn), hard-cap 7 (error — all triggers filled). Returns `{ warnings: string[], errors: string[] }`. `null` triggers excluded from count
|
|
2987
|
+
|
|
2988
|
+
189. **`StateChangeAnimation` schema** — `{ watch: string, on?, recipe?, keyframes?, duration, easing?, debounce? }`. `on` patterns: `'change' | 'increase' | 'decrease' | 'truthy' | 'falsy' | { equals: unknown }`. Must supply either `recipe` or `keyframes` (both missing throws at resolution). Single-fire semantics: for repeating state-driven behavior, use `ambient` with conditional enablement via `$cond`. State-change animations should return to baseline or the transient clear can cause a visible snap.
|
|
2989
|
+
|
|
2990
|
+
190. **Constructable StyleSheets singleton (web) + `<style>.insertRule` fallback** — `packages/react/src/animation/stylesheet-singleton.ts`. `registerKeyframes(name, text)` idempotent dedup by name. Feature-detected Constructable support with iterability probe (jsdom defines `adoptedStyleSheets` non-iterably — fallback path activates). SSR-safe (`canUseDOM()` guards). Zero `dangerouslySetInnerHTML` in either path — CSSOM only
|
|
2991
|
+
|
|
2992
|
+
191. **`useElementAnimations` hook (web)** — `packages/react/src/animation/useElementAnimations.ts`. Signature: `(ref, animations, options) => { triggerUnmount }`. Composes active triggers into CSS `animation` shorthand (comma-separated). State flags (`isHovered`/`isFocused`/`isActive`) passed via options — hook does NOT attach DOM listeners. `stateChange` subscribes via `StateStore.subscribePath` with debounce + auto-clear. I1 mid-transient cleanup: effect re-run resets transient state. `triggerUnmount` imperative returns Promise resolving after max duration
|
|
2993
|
+
|
|
2994
|
+
192. **`useElementAnimations` hook (RN)** — `packages/react-native/src/animation/useElementAnimations.ts`. Signature: `(animations, options) => { animatedStyle, triggerUnmount }`. `composeRNStyle` (pure function in `compose-rn-style.ts`) merges contributions with CSS-parity semantics: scalars last-wins, transforms merge-by-key (not concat — prevents multiplicative composition). Supports arrays per trigger via fixed-pool of 6 `useSharedValue` per trigger (matches validator hard-cap). SVs snap to 0 when trigger transitions to empty to prevent `withRepeat` UI-thread leak. All 7 triggers supported; stateChange via `withSequence(withTiming up/2, withTiming down/2)`
|
|
2995
|
+
|
|
2996
|
+
193. **`react-native-reanimated` mock for jsdom test env** — `packages/react-native/tests/__mocks__/react-native-reanimated.ts`. End-state-synchronous semantics: `withTiming/withSpring/withDelay/withSequence/withRepeat` return target value immediately; `useAnimatedStyle` evaluates factory synchronously at each render. Covers 30+ Reanimated APIs (shared values, Easing, interpolate, interpolateColor, layout-animation builders FadeIn/FadeOutUp/etc, Animated.View/Text/ScrollView/Image passthrough with style-array flattening + testID/accessibilityRole/Label translation + RN-native event prop filtering). Closed 3 pre-existing RN test failures on integration
|
|
2997
|
+
|
|
2998
|
+
194. **`Element.animations` field — first-class spec field** — `Element` accepts `animations?: ElementAnimations | null`. Per-trigger `null` disables inherited cascade for that trigger; whole-field `null` disables all element animations. Accepts the 7 triggers `mount`/`unmount`/`hover`/`focus`/`active`/`ambient`/`stateChange`, each taking `AnimationRef | AnimationRef[] | null` (stateChange uses the distinct `StateChangeAnimation` shape)
|
|
2999
|
+
|
|
3000
|
+
195. **`<Box>` primitive consumes `useElementAnimations` (web)** — Box tracks `isHovered`/`isFocused`/`isActive` via internal state + composed event handlers (state-setter runs BEFORE user handler for throw-safety). Handlers memoized by user-prop identities. When `animations.focus` is set and `tabIndex` is omitted, tabIndex auto-defaults to `0` (prevents silent trigger no-op on non-focusable div); explicit consumer `tabIndex` (including `-1`) always wins. Accepts `animations?: ElementAnimations | null` — null-coalesces to undefined at the hook boundary
|
|
3001
|
+
|
|
3002
|
+
196. **`<Box>` primitive consumes `useElementAnimations` (RN)** — Root is ALWAYS `Animated.View` (never conditionally swapped with plain View — switching component type between renders would force unmount/remount). Event handlers use RN-native names (`onHoverIn`/`onHoverOut`/`onPressIn`/`onPressOut`/`onFocus`/`onBlur`) composed with internal state setters. Style-merge contract: `animatedStyle` is LAST in the style array so animation properties override user-supplied `style` during animation. `useDesignTokens` is hoisted above the early return to avoid Rules-of-Hooks violation if `surface` toggles.
|
|
3003
|
+
|
|
3004
|
+
197. **`boxShadow` keyframe field** — `KeyframeSnapshot.boxShadow?: string` accepts a literal CSS shorthand (`'none'` | `'0 0 20px 4px rgba(...)'`). CSS builder emits `box-shadow: ${value}`; RN builder explicitly omits (same policy as `filter` — no 1:1 platform mapping yet). Used by the `glow` recipe: `'none'` → `'0 0 20px 4px rgba(99,102,241,0.45)'` for a colored shadow halo. This models glow as brightness/shadow, not background color wash.
|
|
3005
|
+
|
|
3006
|
+
198. **`WEB_ONLY_RECIPES` constant + validator warning** — `WEB_ONLY_RECIPES: ReadonlySet<string>` exported from `mythik` (currently `{ 'glow' }`). `validateAnimationCaps` walks each trigger's AnimationRefs and emits a warning when a web-only recipe is referenced, calling out the RN no-op until per-platform shadow parsing lands. Accessible via the `AnimationValidationResult.warnings` array
|
|
3007
|
+
|
|
3008
|
+
199. **Recipe tuning** — `breathe-subtle` uses scale `1.025` and duration `2800ms` so the motion reads as alive without becoming nervous. `glow` uses a boxShadow halo (see rule 197). Other recipes keep conservative defaults for broad UI safety.
|
|
3009
|
+
|
|
3010
|
+
200. **Legacy motion system removed** — The legacy `identity.motionEntrance` / `identity.motionHover` path is no longer supported. The animation engine is the canonical path: use `element.animations` and cascade-level animation declarations instead. Also see rule 149 and `ai-context.md` rule 54.
|
|
3011
|
+
|
|
3012
|
+
### Current platform limitations
|
|
3013
|
+
|
|
3014
|
+
- RN parity for `boxShadow` and `filter` keyframes is intentionally limited because native shadows, Android elevation, and SVG/CSS filters do not share a 1:1 model. Validators warn for web-only animation recipes instead of claiming parity.
|
|
3015
|
+
|
|
3016
|
+
## Layered Backgrounds, Blobs, and Animation Cascade
|
|
3017
|
+
|
|
3018
|
+
The current background and motion stack combines app-level `LayerBackground`, animated blob layers, animation cascade, and safe CSSOM keyframe registration. Legacy background shorthands and per-element blob rendering are removed; `BackgroundStack` + `BlobLayer` (web + RN) mount at the renderer root from `tokens.identity.background`.
|
|
3019
|
+
|
|
3020
|
+
### Cascade and blob contracts
|
|
3021
|
+
|
|
3022
|
+
201. **`mergeElementAnimations(ctx: AnimationContext)` — pure cascade merge fn** — Exported from `mythik` via `packages/core/src/design/animation/cascade.ts`. Walks 4 levels (`identity → variant → template → element`) in order; later levels override earlier per-trigger. Returns `ElementAnimations | null` — null when every output trigger is absent or explicitly disabled. Null-stripping: triggers set to `null` are removed from the output object so consumers never null-check per-trigger. Compile-time exhaustiveness sentinel guards against future `AnimationTrigger` additions dropping silently.
|
|
3023
|
+
|
|
3024
|
+
202. **`AnimationContext` type** — `{ identity?, variant?, template?, element? }` where each field is `ElementAnimations | null | undefined`. `undefined` = inherit upward (pass-through), `null` = explicit disable at this level, per-trigger values override earlier levels. Disjoint by contract from `ElementAnimations` (triggers mount/unmount/hover/focus/active/ambient/stateChange) — runtime discriminant by key presence.
|
|
3025
|
+
|
|
3026
|
+
203. **Null semantics (decision table frozen)** — For any trigger across the cascade: (a) whole-field `null` at a level contributes nothing (inheritance-neutral, NOT a disable marker); (b) per-trigger `null` disables for that level AND blocks prior non-null values; (c) later non-null re-enables (later always trumps); (d) `undefined` = inheritance; (e) output strips nulls — callers see only defined triggers. Distinction is load-bearing: whole-field null vs per-trigger null have DIFFERENT semantics and must not conflate (tests in `packages/core/tests/design/animation/cascade.test.ts` pin the decision table).
|
|
3027
|
+
|
|
3028
|
+
204. **`useElementAnimations` accepts `AnimationContext` OR `ElementAnimations`** — Both web and RN hooks accept `animations` as `ElementAnimations | AnimationContext | null | undefined`. Runtime discriminant via `isAnimationContext()` checks for `'identity' | 'variant' | 'template' | 'element'` keys. Dev mode throws on mixed-key inputs (shape with both context keys AND trigger keys) because these keysets are disjoint by contract; silent classification would drop half the input. Production preserves permissive behavior. Resolution is memoized by param identity.
|
|
3029
|
+
|
|
3030
|
+
205. **Blob v2 catalog — `BLOB_CATALOG`** — Exported from `mythik`. `Record<CuratedBlobName, BlobShapeDef>` with 6 hand-curated entries: 5 organic (`organic-1..5`) + `circle`. Each entry has `name`, `viewBox: '0 0 W H'`, and a valid SVG `path` closed with `Z`. Custom user shapes use `shape: 'custom-svg'` (sentinel member of `BlobShapeName`, NOT in the catalog map). Aesthetic intent: balanced rounded, elongated diagonal, asymmetric multi-lobe (all-cubic to avoid T-reflection cusps), horizontal flowing ribbon, sharply asymmetric.
|
|
3031
|
+
|
|
3032
|
+
206. **Blob v2 type surface** — `BlobShapeName`, `CuratedBlobName`, `BlobShapeDef`, `BlobPreset`, `BlobMotionPreset`, `BlobMotion`, `BlobInstance`, `BlobV2Config`, `BlobRenderStyle`, `BlobSpec` all exported from `mythik`. `BlobV2Config` accepts TWO mutually-exclusive forms (preset vs explicit `blobs[]`) — runtime-enforced at resolver level, type-level relaxed for JSON ergonomics. `(string & {})` branded intersection preserves literal autocomplete for `'primary'`/`'accent'` color keywords while accepting arbitrary hex strings. `BlobRenderStyle` makes `opacity` + `blur` REQUIRED (post-resolution, defaults applied); `BlobInstance` makes them optional (pre-resolution input).
|
|
3033
|
+
|
|
3034
|
+
207. **`resolveBlobLayer(config, palette)` — pure fn, both forms** — Exported from `mythik` via `packages/core/src/design/background/blobs/resolver.ts`. Explicit form: for each `BlobInstance`, resolves palette keywords to hex, looks up catalog shapes, applies `opacity`/`blur` defaults using `??` (preserves explicit `0`), converts `BlobMotion` (drift/rotate/scale) to `ElementAnimations.ambient` inline animations. Preset form: hydrates curated `organic-duo` / `organic-trio` / `circle-pair` compositions from `PRESET_SEEDS`; `BlobMotionPreset` values (`drift-gentle`/`drift-fluid`/`drift-snappy`/`static`) map to `BlobMotion` via `PRESET_MOTION`. Ambiguous `{ preset, blobs }` prefers preset + dev-warn. Empty `palette: []` falls back to `['primary']` + dev-warn.
|
|
3035
|
+
|
|
3036
|
+
208. **Custom-svg error policy (fail-loud dev, degrade prod)** — `shape: 'custom-svg'` requires both `path` and `viewBox` as non-empty strings. Dev (`NODE_ENV !== 'production'`): throws with field-specific message (`"custom-svg requires a non-empty path. Got path=undefined."`). Prod: skips the malformed blob + `console.warn`, preserving app stability. Rationale: specs in production should have passed validation; crashing the whole app over one bad blob is disproportionate. Ambiguity policy (`preset` + `blobs` both set) is weaker — only dev-warn, no throw — because there is a defensible winner (preset) and no rendering is broken.
|
|
3037
|
+
|
|
3038
|
+
209. **Blob motion → `ElementAnimations.ambient` conversion** — Each `BlobMotion` dimension becomes one `InlineAnimation` on the `ambient` trigger: drift → 3-keyframe translate (`0%`→origin, `50%`→(range.x, range.y), `100%`→origin); rotate → 2-keyframe rotate (`from`→`to`); scale → 2-keyframe scale (`from`→`to`). All three use `direction: 'alternate'` + `iterations: 'infinite'`. Emission order: drift → rotate → scale. Single-dimension motion returns a scalar `AnimationRef`; multi-dimension returns an array — both valid per the `AnimationRef | AnimationRef[]` union. Perceived cycle duration: drift = 1× `duration` (symmetric trajectory), rotate/scale = 2× `duration` (forward + reverse iteration). Motion presets (`drift-gentle`: 28s/small/ease-in-out, `drift-fluid`: 20s/wide/ease-in-out, `drift-snappy`: 14s/medium/linear, `static`: no motion).
|
|
3039
|
+
|
|
3040
|
+
210. **`validateBackgroundCaps(input)` — performance caps validator** — Exported from `mythik` alongside `validateAnimationCaps`. Input shape `{ blobs?, layers? }`; output shape `{ warnings: string[], errors: string[] }` (shared `AnimationValidationResult` type). Caps: blobs per background (soft 8 / hard 16), layers per background (soft 6 / hard 10), custom-svg path length (soft 500 / hard 2000 chars, only applied when `shape === 'custom-svg'` AND `path` is a string), motion dimensions per blob (soft 2 / hard 3). Threshold semantics are strictly-over (`>`).
|
|
3041
|
+
|
|
3042
|
+
211. **`useShapeAnimations(ref, animations, options)` — Layer 3 web runner** — Exported from `mythik-react` via `packages/react/src/animation/useShapeAnimations.ts`. React hook for SVG-child animations (`<path>`/`<circle>`/`<rect>`/`<g>`…). Consumes the same `ElementAnimations` contract as `useElementAnimations` but narrowed to the `ambient` trigger ONLY; shape children have no hover/focus/active contract and no distinct mount ceremony. Attaches CSS animations via `el.style.animation` (surgical, preserves other inline styles); keyframes register once through the CSSOM singleton (zero `dangerouslySetInnerHTML`) and dedupe by hash so multiple shape instances with the same recipe share one CSS rule. Dev mode warns when non-ambient triggers are passed. Production silently ignores them. `options.recipes` SHOULD be stable for useMemo performance.
|
|
3043
|
+
|
|
3044
|
+
212. **`useShapeAnimations(animations, options)` — Layer 3 RN runner** — Repository-preview implementation in `packages/react-native/src/animation/useShapeAnimations.ts`; it is not part of the initial npm publish surface. Reanimated parity of rule 211: returns `{ animatedProps }` that the consumer spreads onto `Animated.createAnimatedComponent(Path)` from `react-native-svg`. Uses the `HARD_PER_TRIGGER` (=6) fixed-pool `useSharedValue` pattern (`useSharedValueArray` helper, also exported) so React Hook count stays stable. Reuses `composeRNStyle(contributions, interpolate, interpolateColor)` shared with `useElementAnimations` — single composition pipeline for View-style and animated SVG props. Relies on `react-native-svg` v13+ auto-translating the transform array into SVG `transform="..."` strings. Same dev-mode non-ambient-trigger warning as the web hook.
|
|
3045
|
+
|
|
3046
|
+
213. **Cross-platform Layer 3 parity pins** — `buildCSSKeyframes` (web) and `buildReanimatedSpec` (RN) interpret the same resolved `AnimationSpec` identically on load-bearing invariants: duration agreement (ms count matches regardless of input form `'28s'`/`'28000'`), keyframe stop count (web `%` markers match RN `inputRange` length), direction semantic (`'alternate'` token ⇔ `timing.reverse=true`), iterations `'infinite'`/numeric count, animated-prop enumeration (`translateX/Y`/`rotateDeg`/`scale`).
|
|
3047
|
+
|
|
3048
|
+
214. **`Template.animations` field + `$prop`/`$state`/`$cond` interpolation** — `TemplateDefinition` supports `animations?: Record<string, unknown> | null`. Type is pre-interpolation (JSON shape with `$`-expression placeholders); runtime shape is `ElementAnimations` after `resolveDeepWithProps` traversal in the renderer template branch. Three-branch semantics: field absent = inheritance-neutral (no emission), whole-field `null` = forwarded verbatim (cascade-neutral with explicit intent), object = `$prop.X` / `$state.X` / `$cond`/`$then`/`$else` all resolve against merged template-defaults + consumer-element-props.
|
|
3049
|
+
|
|
3050
|
+
215. **`ResolvedVariant.animations` passthrough with `$path` resolution** — `ResolvedVariant` supports `animations?: Record<string, unknown> | null`. `resolveVariant` routes the field through `resolveTokenRefs` (same mechanism as `style`/`hover`/`active`/`focus`) so `$path` references at the top level of each trigger value resolve (e.g. `animations: '$animations.interactive'`). Three-branch semantics mirror rule 214. `resolveTokenRefs` is one-level-deep: nested structures (`mount: { recipe: '$recipes.X' }`) pass through unchanged — downstream animation runtime layers are responsible for any deeper `$` resolution. Expression-system scope: only `$path` (token-tree) lookups are recognized here; `$prop`/`$state`/`$cond` are template-layer concepts.
|
|
3051
|
+
|
|
3052
|
+
216. **End-to-end animation cascade at renderer time** — The render engine merges identity + variant + template + element animations via `mergeElementAnimations` and emits the merged result as `resolvedProps.animations` on the primitive render node. Box (web + RN) consumes the single final prop via `useElementAnimations`; no cascade plumbing lives on Box itself. Regular-element branches merge `identity + variant + element`; template branches merge `identity + template + element`; custom elements add the element-definition level described in rules 226-234. Element-level `animations` resolves through unified `resolveDeep` before merging so `$state`/`$cond`/`$token`/`$prop` expressions inside animation configs become concrete values. `scanDeps(element.animations)` tracks state changes deep inside animation expressions so they dirty the render cache. Whole-field `null` at identity level is cascade-neutral. Cached `resolvedProps` omit `animations` when no cascade level contributes, preventing stale-animation leakage across theme/spec changes.
|
|
3053
|
+
|
|
3054
|
+
217. **`BlobsLayerConfig` + `resolveLayer` dispatch** — `LayerConfig` includes a blobs variant: `BlobsLayerConfig = LayerCommonProps & { type: 'blobs' } & Omit<BlobV2Config, 'opacity'> & { blobOpacity?: number }`. The `Omit<..., 'opacity'> + blobOpacity` rename resolves a semantic collision: `LayerCommonProps.opacity` is the layer-container wrapper alpha; `BlobV2Config.opacity` is the per-blob fill default. Keeping both as `opacity` at the same nesting level would compose multiplicatively. `LayerSpec` has a matching `{ kind: 'blobs'; config: BlobsLayerConfig; common: LayerCommon }` variant; unlike other `LayerSpec` kinds, the blobs variant carries unresolved config forward because palette/catalog/motion resolution needs tokens outside `resolveLayer` scope.
|
|
3055
|
+
|
|
3056
|
+
218. **`BackgroundStack` palette threading + dispatch** — `BackgroundStack` (web + RN) accepts an optional `palette?: { primary: string; accent: string }` prop. The internal `renderLayer` helper routes blob layers with palette to `BlobLayer`; without a palette, a dev-mode warning fires instead of silently rendering an empty blob layer. A `toBlobV2Config` helper strips layer-level fields (`type`/`opacity`/`blendMode`/`zIndex`) and promotes `blobOpacity` → `BlobV2Config.opacity` at the render boundary, keeping the two opacity axes cleanly separated.
|
|
3057
|
+
|
|
3058
|
+
219. **`BlobLayer` web — SVG blobs + `useShapeAnimations`** — `packages/react/src/background/BlobLayer.tsx`. Consumes `{ config: BlobV2Config, palette }` and emits one `<svg><path/></svg>` per resolved `BlobSpec`. The ref binds `useShapeAnimations` to the inner `<path>` (not the outer `<svg>`) so keyframe transforms apply to shape geometry; animating the wrapper would compose with the CSS position/size and produce visible positional drift under translate motion. JSX-native rendering throughout (no `dangerouslySetInnerHTML`). `resolveBlobLayer(config, palette)` is wrapped in `useMemo([config, palette])` so animation freshness does not force unnecessary resolve + keyframe-register work across all blobs.
|
|
3059
|
+
|
|
3060
|
+
220. **`BlobLayer` RN — react-native-svg + Reanimated `useAnimatedProps`** — `packages/react-native/src/background/BlobLayer.tsx`. Mirrors web prop surface; platform deviations: (a) animation flows through `animatedProps` spread onto `Animated.createAnimatedComponent(Path)` instead of imperative `el.style.animation`; (b) opacity is a style prop, not an SVG attribute; (c) static rotation uses RN's transform-array shape `[{ rotate: deg }]`. `blob.blur` fires a dev warning and no filter is applied because RN filter parity is limited. `BackgroundStack` (RN) warns on non-`normal` blendMode because RN has no `mixBlendMode` equivalent. `resolveBlobLayer` is memoized for parity with web.
|
|
3061
|
+
|
|
3062
|
+
221. **MythikRenderer root mount for `LayerBackground` v2** — When `tokens.identity.background` is a LayerBackground (`isLayerBackground` returns true — object carrying `color` or `layers`, no `style` field, rejects empty `{}` + arrays), the renderer wraps the rendered tree in a positioned container with `<BackgroundStack>` as a sibling inside a stacking context (`isolation: isolate`). The isolation context prevents per-layer `zIndex` values from leaking out to occlude the content wrapper. Palette threads from `tokens.colors.{primary, accent}`; missing/malformed colors fire a diagnostic dev warning at the renderer seam. Root wrappers carry `data-sv-renderer-root="v2"` (web) and `testID="sv-renderer-root-v2"` (RN) markers for test selection. `Spec.tokens?: Record<string, unknown>` is part of the public type graph so the renderer reads spec tokens without casts and the spec-validator accepts the field.
|
|
3063
|
+
|
|
3064
|
+
222. **Legacy background + per-element blobs removed** — `resolveBackgroundCSS`, `resolveBlobStyles`, `BackgroundConfig`, `BlobStyle`, `background.ts`, and `tokens.backgroundCSS` injection are removed from `packages/core`. `Box` no longer has a `backgroundBlobs` prop and is now a pure surface-styled wrapper. App-level background lives exclusively at `tokens.identity.background`. `IdentityConfig.background` is `LayerBackground`; `DEFAULTS.identity.background` is `{}` (empty LayerBackground, no root mount by default). `useDesignTokens` (web + RN) reads `identity.background.color` + `identity.background.layers`, omitting undefined keys so `isLayerBackground` does not fire on dead data. The `isLayerBackground` rejection for objects with `style` remains as malformed-spec defense against legacy-shape holdovers.
|
|
3065
|
+
|
|
3066
|
+
223. **`IdentityConfig.animations` on the type graph** — `IdentityConfig` includes `animations?: ElementAnimations | null`. TypeScript preset authors can declare `identity: { animations: {...} }` under `PresetDefinition.tokens.identity` without casts. Semantics: whole-field `null` is cascade-neutral, per-trigger `null` disables and blocks inheritance, absent = inheritance-neutral.
|
|
3067
|
+
|
|
3068
|
+
224. **`/tokens/resolved` store path — reactive fully-resolved tokens** — Factory persists `resolvedTokens` to the store at `/tokens/resolved` at `createMythik` initialization and after every `updateTokens` call. Companion path `/tokens/raw` holds only the raw input accumulated through `updateTokens` (useful for export/inspection). `/tokens/resolved` holds the fully-resolved token tree: DNA-derived colors, DEFAULT-merged identity, and downstream consumer values. `MythikRenderer`'s v2 `LayerBackground` root mount reads from `/tokens/resolved` so preset swaps and playground control changes trigger fresh reads and remount the `BackgroundStack` with the new layers.
|
|
3069
|
+
|
|
3070
|
+
225. **`Select` primitive listbox portals to `document.body`** — The Select dropdown's listbox panel (`packages/react/src/primitives/select.tsx`) renders via `React.createPortal(..., document.body)` instead of as a descendant of the select wrapper. Positioning uses `position: fixed` with coordinates tracked from the trigger button's `getBoundingClientRect()` and updated on open, scroll, and resize via `useLayoutEffect`. Outside-click detection checks both the wrapper ref and the listbox ref. Portaling to body escapes nested stacking contexts across the renderer tree, so the listbox competes with other top-level portals such as modals and toasts. `zIndex: 9999` is retained for safety above ambient overlay layers.
|
|
3071
|
+
|
|
3072
|
+
### Known limitations
|
|
3073
|
+
|
|
3074
|
+
- The `getActiveTokens()` helper resolves theme tokens on every call — within one element render it may be called 2-3 times. Memoize per render-cycle if profiling shows it matters.
|
|
3075
|
+
- `instancePropsCache` diff uses shallow reference equality — object/array props sourced from `$state` (which produce new references each render) over-invalidate every render even when contents are equal. Deep-equality diff deferred pending profiling (see rule 233 + engine.ts docstring).
|
|
3076
|
+
- Template branch inside custom element expansion doesn't currently run the `variant` or `elementDef` cascade slots — only `identity + template + element`. Extending to full 5-level is a follow-up if a real need emerges.
|
|
3077
|
+
|
|
3078
|
+
---
|
|
3079
|
+
|
|
3080
|
+
## Layer 3 Custom Elements
|
|
3081
|
+
|
|
3082
|
+
226. **Custom elements are first-class primitives** — Custom elements registered via `plugins.registerElement` participate in the full render contract. Consumers apply `animations`, `hover`, `active`, `focus`, `transition`, `motion`, `visible`, `key`, and `style` on the instance — identical syntax to built-in primitives. These apply to the OUTER primitive of the expansion (black-box boundary).
|
|
3083
|
+
|
|
3084
|
+
227. **ElementRenderNode supports all Element declarative fields** — Authors declare `animations`, `hover`, `active`, `focus`, `transition`, `motion`, `visible`, `key` inside their render tree. Each field behaves identically to `Element` semantics for the primitive rendered at that position. `ElementRenderNode.children` type is `Array<ElementRenderNode | '$children'>` — string literal `'$children'` marks the slot position for consumer children.
|
|
3085
|
+
|
|
3086
|
+
228. **ElementDefinition.variants + theme fallback** — Authors declare variants on the definition via `ElementDefinition.variants: Record<string, ElementVariantSpec>` where `ElementVariantSpec` has the same shape as primitive variant specs (`props`/`style`/`hover`/`active`/`focus`/`transition`/`animations`). Consumer's `{ variant: 'name' }` resolves: (1) `ElementDefinition.variants[name]` first, (2) `tokens.components[elementType].variants[name]` as theme-level fallback. Variant props merge into consumer props pre-expansion (consumer wins last, variant beats definition default).
|
|
3087
|
+
|
|
3088
|
+
229. **5-level animation cascade: identity → variant → elementDef → template → element** — Animation cascade includes an `elementDef` level between variant and template. `elementDef` is the author's declaration on a primitive inside a custom element expansion (sourced from the expanded element's own `animations` field when propContext is active). Defaults to `undefined` (inheritance-neutral) outside custom elements. `mergeElementAnimations` accepts all 5 levels via the `AnimationContext` interface.
|
|
3089
|
+
|
|
3090
|
+
230. **Custom element instance black-box — consumer affects outer only** — Consumer's instance-level declarations apply to the OUTER primitive of the expanded tree. Inner primitives are the author's domain. Inner primitives receive identity cascade, their own variant/elementDef declarations, and full expression resolution. Inner primitives do NOT receive consumer's instance-level `animations`/`hover`/`motion`/`style`/`visible`/`key`. A future `::part()`-style API can layer on top of this contract without breaking it.
|
|
3091
|
+
|
|
3092
|
+
231. **`$prop` is nearest-enclosing-custom-element-scoped** — Inside a custom element's expanded tree, `$prop` references resolve against THAT custom element's `mergedProps`. Nesting a custom element pushes a new `propContext`; the outer context is shadowed, not merged. Thread values via explicit prop passthrough when nested access is needed. Matches CSS custom-property scoping semantics.
|
|
3093
|
+
|
|
3094
|
+
232. **`$children` marker in custom element render trees** — Authors write `children: [..., '$children', ...]` in their render tree; consumer's children slot at each marker position during expansion via the `consumerChildrenIds` parameter of `expandElementToSpec`. Multiple markers each splice the full consumer children. Same slotting semantics as spec templates: consumer children retain their enclosing element IDs and render through unified dispatch.
|
|
3095
|
+
|
|
3096
|
+
233. **Custom element cache invalidates on consumer prop changes via `/__prop/<name>` sentinels** — Cache dep scanner (`scanDeps` in `deps.ts`) tracks `$prop` references as `/__prop/<name>` sentinel paths. At custom-element dispatch time, an `instancePropsCache` diffs current vs previous `mergedProps`; changed keys are injected into the active changed-paths set (scoped to the expansion render via `try/finally`) so inner primitives that declared those prop refs correctly invalidate. Shallow reference equality can over-invalidate object/array props sourced from `$state`; see `engine.ts` docstring.
|
|
3097
|
+
|
|
3098
|
+
234. **Custom element error boundary at instance level** — Errors anywhere inside a custom element's expansion (missing primitive, resolver throw, etc.) surface as an `_error` RenderNode caught by the `renderElement` `try/catch` wrapper. Consumer error handling does not need to know the author's internal structure; errors are isolated per instance so a broken custom element does not crash sibling renders.
|
|
3099
|
+
|
|
3100
|
+
235. **Custom elements under `repeat`** — Custom elements dispatched under `element.repeat` (`count`, `statePath`, or `source`) render N instances as expected, same as primitives. Repeat handling applies before custom-element dispatch so the repeat gate fires for all element types uniformly. `$index` / `$item` context threads through consumer props into the expansion's `propContext` so inner primitives see the correct iteration values.
|
|
3101
|
+
|
|
3102
|
+
236. **Custom element render trees can reference spec-level templates** — A custom element's render tree may use a type that resolves to a spec-level template (e.g. `render: { type: 'section-template' }`). The expansion (`expandElementToSpec`) propagates `spec.templates` and non-colliding `spec.elements` entries onto the expanded sub-spec so recursive template dispatch finds the definition. Template children references (e.g. `template.children: ['my-id']`) to outer-spec elements also resolve during recursion. Template's own `$prop` context is its own `mergedProps`, not the custom element's — standard template semantics are preserved.
|
|
3103
|
+
|
|
3104
|
+
237. **`plugins.registerElement` exposed on `PluginLoader`** — `PluginLoader` exposes `registerElement(def: ElementDefinition)` and `getElements(): ElementDefinition[]`. Definitions are staged internally; `factory.applyPlugins()` iterates `plugins.getElements()` and calls `elements.register(def)` for each, skipping entries already present (idempotent with `config.elements`). `count()` includes the element definitions.
|
|
3105
|
+
|
|
3106
|
+
238. **`validateSpec` accepts `elementRegistry` in `ValidationContext`** — An `el.type` not present in `primitiveRegistry` is no longer flagged as "unknown primitive type" when `elementRegistry` knows it. Suggestion candidates (for auto-fix Levenshtein matches) merge keys from both registries. `factory.ts` threads `elementRegistry: elements` through validation call sites so registered custom elements validate like built-ins.
|
|
3107
|
+
|
|
3108
|
+
239. **Event bindings may be `$prop` expressions** — Inside a custom-element render tree, `element.on.<event>` can be `{ "$prop": "<propName>" }`. The renderer detects the `$prop` shape at binding level and calls `resolveDeep` to substitute the consumer's supplied value (typically an action array). Any other binding shape (plain object, array of actions, `TransactionBinding`) is passed through verbatim; inner `$state` / `$template` inside binding params stay lazy so the action dispatcher resolves them at press time. Eager param resolution only activates inside a `repeat` where `$item` binding is required for correctness.
|
|
3109
|
+
|
|
3110
|
+
240. **Consumer `variant` lives under `props`** — The renderer reads the variant name from `resolvedProps.variant`. A top-level `variant` field on the element object is silently ignored. Applies uniformly to built-in primitives (button/box/etc. with `tokens.components` variants) and Layer 3 custom elements (with `ElementDefinition.variants`). Consumer specs: `{ "type": "stat-card", "props": { "variant": "primary", ... } }` — NOT `{ "type": "stat-card", "variant": "primary", "props": {...} }`.
|
|
3111
|
+
241. **`/ui/selectedRow` is framework-reserved** — framework writes the clicked row to this path before dispatching row interactions (column actions in `columns[].actions[].onPress` AND direct `onRowClick`). Single-selection scope per screen. Both row interaction types use the same lazy-resolution contract — `$state` / `$template` in action `params` resolves at click time against the freshly-written row. See `ai-context-runtime-semantics.md § 2.1` for the row context model, write-owner (the `createRowDispatcher` helper at `packages/react/src/runtime/row-dispatcher.ts`), lifetime, read patterns, and multi-table caveat.
|
|
3112
|
+
242. **Column action expressions resolve at press time via primitive `lazyActionPaths` declaration** — `$state`/`$template` inside `columns[].actions[].onPress` params resolve at click time, not render time. Any `$state` path returns its current value at press; framework writes the clicked row to `/ui/selectedRow` before dispatch (see rule 222 + runtime-semantics §5.1), so `$state: "/ui/selectedRow/*"` is a canonical pattern. Underlying mechanism: primitive declares `lazyActionPaths` in `PRIMITIVE_PROP_SCHEMAS` (table declares `['columns[].actions[].onPress']`); engine keeps subtree raw at render; dispatcher resolves at press. See `ai-context-runtime-semantics.md § 1.3`.
|
|
3113
|
+
243. **CRUD endpoint generates 3 routes from 1 declaration** — an endpoint with `crud: { table, primaryKey, insertable, updatable }` generates `POST <path>`, `PUT <path>/:id`, `DELETE <path>/:id`. Do not declare 3 endpoints. See `ai-context-runtime-semantics.md § 3.1`.
|
|
3114
|
+
244. **`/api/auth/login` expects `{ username, password }`** — not `email`. Use `loginBody` template to map consumer email convention → framework username. See `ai-context-runtime-semantics.md § 3.2`.
|
|
3115
|
+
245. **Query endpoints envelope responses in `{ data: [...] }`** — shape is `{ data, total?, page?, pageSize?, totals? }`. Consumer specs read `response.data` via state; envelope is NOT auto-unwrapped. See `ai-context-runtime-semantics.md § 3.3`.
|
|
3116
|
+
246. **Browser vs Server entries — `mythik` vs `mythik/server`** (v0.1.0). The default `mythik` entry is browser-safe by construction: zero transitive Node-only imports. `FileSpecStore`, `SqlServerSpecStore`, `SqlServerVersionedSpecStore`, `SqlServerEnvironmentStore` and their config types live in `mythik/server`.
|
|
3117
|
+
|
|
3118
|
+
247. **`derive` and `dataSources` are processed at runtime per spec mount** (v0.1.0). When `spec.derive` is present, the framework instantiates a `DeriveEngine`, evaluates all derive paths in topological order at mount, and re-evaluates dirty paths reactively on state changes. Derive paths are protected: setState targeting a derive path errors at validate time and runtime. When `spec.dataSources` is present, the framework instantiates a `DataSourcesEngine`, performs initial fetches (deferred to reactive resolution when URL template deps are undefined), and re-fetches reactively when dependencies change. The action `refreshDataSource` (params: `{ id: string }`) is automatically registered for any spec with dataSources. URL templating requires the explicit `{ $template: '...' }` form — plain strings with `${...}` are NOT substituted (validator catches at load). See `ai-context-runtime-semantics.md` for lifecycle, ordering, error degradation, and state protection details.
|
|
3119
|
+
|
|
3120
|
+
248. **CLI is the only approved path for spec writes** (v0.1.0). Three approved forms: (a) **Shell** - `mythik push <id> --from-file spec.json` / `mythik patch <id> --from-file patch.json` (or intentional stdin via `--from-file -` / pipe without `--from-file`); (b) **Bulk** - `mythik push --from-dir ./specs/` (sequential, continue-on-error, no rollback; partial state on failure recovers by fixing failures and re-running the same command); (c) **Programmatic** - `import { runPush, runPatch } from 'mythik-cli/api'` for IDE tooling, test harnesses, CI scripts. NEVER call `SpecStore.save()` directly from application code. It is an internal persistence primitive marked `@internal`; validation happens at the CLI / engine tier. Bypassing this path skips validation and can produce runtime errors visible only at render time. `mythik patch --from-file <path>` is preferred over inline JSON for any patch containing `$state`, `$template`, `$auth`, or `$row` references (PowerShell expands `$<word>` in double-quoted strings).
|
|
3121
|
+
|
|
3122
|
+
249. **`mythik lint` detects known anti-patterns in specs and consumer code** (v0.1.0).
|
|
3123
|
+
|
|
3124
|
+
A new CLI command + programmatic API:
|
|
3125
|
+
- `mythik lint [--from-file <path> | --from-dir <folder>]` — bulk or targeted lint
|
|
3126
|
+
- `--specs-only` / `--code-only` — filter by surface
|
|
3127
|
+
- `--code-dir <path>` — override default code scan root (default: `./src`)
|
|
3128
|
+
- `--json` — machine-readable output
|
|
3129
|
+
- `import { runLint } from 'mythik-cli/api'` — programmatic equivalent
|
|
3130
|
+
|
|
3131
|
+
**4 rules in v0.1**:
|
|
3132
|
+
- `spec-row-literal` (warning) — `$row` is not an expression handler; use `$state: '/ui/selectedRow/<key>'`. Validator-resident in `spec-validator.ts` (also surfaces during `mythik push` and `mythik validate`).
|
|
3133
|
+
- `spec-crud-id-collision` (error) — `endpoint.path` ending `/:id` combined with `crud: {}` produces `/path/:id/:id`. Strip trailing `/:id`. Walks the object-shaped `endpoints: Record<string, EndpointConfig>`; emits JSON Pointer paths `/endpoints/<name>/path`.
|
|
3134
|
+
- `spec-auth-domains-port` (warning) — `auth.authDomains[i]` containing `:port` strips the port silently (matcher uses hostname-only via `URL.hostname`). Walks the `string[]` shape; emits JSON Pointer paths `/auth/authDomains/<index>`.
|
|
3135
|
+
- `code-store-save-bypass` (error) — calling `*store*.save()` outside `packages/core/` and `packages/cli/` bypasses validation. Use `runPush` from `mythik-cli/api` or `mythik push --from-file`. **Known scope**: matches `<Identifier>.save()` callees only (e.g. `myStore.save(...)`). Does NOT detect `this.store.save()`, `<obj>.<store>.save()`, or `(await getStore()).save()` patterns — these slip through the AST scanner. Generation-level guidance in `ai-context.md` (anti-patterns section) covers all variants; the lint rule is one layer of defense-in-depth, not a complete catch. Widening the scanner to handle `PropertyAccessExpression` callees is tracked as a v0.2+ candidate if real consumer code shows these patterns slipping through generation guidance.
|
|
3136
|
+
|
|
3137
|
+
**Code rules require TypeScript ^5.0.0 as peerDependency** (optional). If not installed, code rules emit one warning finding `lint-meta-no-typescript` (severity matches `LintSeverity = 'error' | 'warning'`) and skip cleanly. Spec rules run independently.
|
|
3138
|
+
|
|
3139
|
+
Spec rules are added to `spec-validator.ts` (rule 1) and `api-spec-validator.ts` (rules 2+3) and run on EVERY validation (push, validate, lint) — defense-in-depth. Code rules run only via `mythik lint` (no push integration; push doesn't see consumer code). `ApiSpecValidationResult` gains optional `lintWarnings?: ValidationError[]` field for rules 2+3 (additive non-breaking; preserves existing `errors: string[]` shape). `SpecValidationResult` also gains optional `lintWarnings?: ValidationError[]` to propagate findings across the `DocumentHandler.validate` boundary (`api-handler.validate` forwards `validateApiSpec().lintWarnings` so `mythik validate` and `mythik push` actually surface rules 2+3).
|
|
3140
|
+
|
|
3141
|
+
Exit codes: 0 = no errors, 1 = errors found, 2 = runtime error (e.g., unreadable file).
|
|
3142
|
+
|
|
3143
|
+
See `ai-context.md` for spec-gen anti-patterns the AI must NOT generate.
|
|
3144
|
+
|
|
3145
|
+
250. **Storage tables are declarative, not auto-created** (v0.1.0). The framework's spec stores (`SqlServerSpecStore`, `SupabaseSpecStore`, and the versioned/environment subclasses) operate against three tables the consumer's database MUST already have: `screens` (base, REQUIRED for every adapter), `screen_versions` (opt-in, only for `*VersionedSpecStore`), and `screen_environments` (opt-in, only for `*EnvironmentStore`). The framework does NOT ship a database migration command in v0.1.0. Instead, the canonical schema is declared declaratively in `ai-context.md § Storage Setup`: per-table column specs (semantic types, nullability, defaults), constraints, indexes, Postgres-specific `jsonb` requirements, Postgres trigger requirements for `screens.updated_at` + `screens.version`, idempotency requirement, post-apply verification, custom table names, identifier-safety scope, and schema-evolution policy. The AI bootstrapping the consumer's environment reads the schema spec, translates it to the target SQL dialect (SQL Server, Postgres/Supabase, MySQL, etc.), applies idempotently (`IF NOT EXISTS`-style), and verifies via `INFORMATION_SCHEMA` queries. Future schema changes will be documented as additive ALTER instructions in the same ai-context section.
|
|
3146
|
+
251. **`security.exposeErrors` controls render error detail** (v0.1.0). Default is `true`; set `createMythik({ security: { exposeErrors: false } })` for production-like hosts that must avoid leaking error messages/stacks. `_error` render nodes write diagnostics to `/ui/renderErrors` only when exposure is enabled. Primitive/component exceptions are caught by `MythikRenderer`'s error boundary: development + exposed mode shows an overlay with message and component stack; production or `exposeErrors: false` shows a neutral placeholder. The overlay resets when the spec changes so a corrected spec can recover without remounting the host.
|
|
3147
|
+
252. **Icon packs register through `plugins.setIconRenderer`** (v0.1.0). Mythik does not bundle Phosphor/Lucide/etc. Register one host-level renderer from `MythikApp.onPlugins`; the built-in `icon` primitive keeps identity behavior and calls the renderer with `{ name, size, weight, color, style }`. If the placeholder circle renders, verify `onPlugins` registered the renderer and the consumer is not validating against stale tarballs or source aliases.
|
|
3148
|
+
253. **`overridePrimitive('icon')` returns a RenderNode, not JSX** (v0.1.0). Use it only for full primitive replacement. The renderer function must return `{ type, props, children }` with `_component` in props when targeting React. Returning `<Icon />` directly is not the primitive renderer contract and bypasses the built-in icon identity wrapper.
|
|
3149
|
+
254. **CLI explicit input wins over ambient stdin** (v0.1.0). For `mythik push`/`mythik patch`, `--from-file <path>` reads the file even if the host process exposes non-TTY stdin. Intentional stdin remains supported through `--from-file -` or by piping without an explicit file. `--from-file` plus a positional patch argument is still a conflict.
|
|
3150
|
+
255. **`runPatch` versions in every output mode when `author` is provided** (v0.1.0). `SpecEngine.patch` applies and validates patches. CLI/API `runPatch` persists through the normal `store.save` path without `author`; with a versioned store + `author`, it captures the patched document and writes once through `saveVersion`, preserving lazy-bootstrap pre-patch history. JSON/TOON success output includes additive `versioned` and `version` metadata.
|
|
3151
|
+
256. **DB-first specs require an explicit local sync step** (v0.1.0). In DB-first workflows, the database is source of truth and `specs/*.json` files are bootstrap/snapshot artifacts until refreshed. After `mythik patch`, run `mythik pull <id>` and write the stdout back to the local spec before committing or pushing from files; otherwise a later full `push` can overwrite DB-only patches.
|
|
3152
|
+
257. **`/ui/selectedSpatialItem` is framework-reserved** - `spatial-map` writes activated item context for JSON `onItemPress` actions before dispatch. The payload is domain-neutral (`itemId`, `zoneId`, `status`, `position`, `rotation`, `shape`, `metadata`, full `item`, optional `zone`). `selectedItemPath` can override the path for isolated maps. Action params under `onItemPress` resolve lazily at press time via the primitive `lazyActionPaths` declaration, so `$state: "/ui/selectedSpatialItem/itemId"` reads the item just pressed. Programmatic function callbacks pass through without framework state writes, matching table callback semantics. Menus, drawers, modals, and inspectors are composed outside the primitive.
|
|
3153
|
+
258. **`spatial-map` interaction hardening rules** - Canvas-level presses are handled at the SVG surface so empty zone areas can clear selection or dispatch `onCanvasPress`; item handlers stop propagation so item activation is not treated as a canvas press. Context dispatchers write context once, then pass ActionBinding arrays through unchanged so `MythikRenderer.dispatchAction` preserves serial async action-chain semantics. `interactionPolicy.selectItems` and `interactionPolicy.activateItems` are independent: selection can update `selectedItemPath` without dispatching `onItemPress`, including readonly maps explicitly opted into selection; activation controls action dispatch. In programmatic React mode, function callbacks pass through and consumers own selected visual state via `selectedItemId` or direct writes to `selectedItemPath`.
|
|
3154
|
+
|
|
3155
|
+
259. **`/ui/spatialItemChange` is framework-reserved** - `spatial-map` writes item edit-change context before dispatching JSON `onItemChange`. The payload is domain-neutral and includes `changeType`, `itemId`, `previousItem`, `nextItem`, `previousPosition`, `position`, and `delta`. `itemChangePath` overrides the path for isolated maps. Programmatic callbacks pass through and own their own state.
|
|
3156
|
+
|
|
3157
|
+
260. **`spatial-map` edit movement is controlled and data-first** - In `mode: "edit"`, drag preview is local and persistence occurs on drop through `onItemChange`/`onZoneChange`; keyboard arrows emit one change per valid move. Edit mode does not dispatch `onItemPress` by default; set `interactionPolicy.activateItems: true` only when edit-time activation is intentional. `editPolicy` controls movement, resize, bounds, keyboard steps, and coordinate precision. Current viewBox bounds clamp `item.position` to the `viewBox`; full collision and polygon/path point editing remain separate future primitive capabilities; item resize/rotate use `transform` plus `rotation`, and zone resize uses `transform.scaleX/scaleY` while preserving `shape`; snap/guides are configured through `editPolicy.snap` and `editPolicy.guides`. Create/duplicate/delete are composed generically today with `onCanvasPress`, `$uniqueId`, `$array`, and modal confirmation outside the primitive.
|
|
3158
|
+
|
|
3159
|
+
261. **`/ui/spatialCanvasPress` is framework-reserved** - `spatial-map` writes SVG canvas press context before dispatching JSON `onCanvasPress`. The payload is domain-neutral with `kind: "canvas"`, `mode`, effective `point`, `rawPoint`, optional `snap`, `viewBox`, optional `zoneId`, optional `zone`. `canvasPressPath` overrides the path for isolated editors. Item handlers stop propagation, so item activation is not treated as a canvas press. Use this path to compose click-to-place workflows externally; the primitive does not create domain objects itself.
|
|
3160
|
+
|
|
3161
|
+
262. **`$uniqueId` generates deterministic source-scoped ids** - Syntax: `{ "$uniqueId": true, "source": { "$state": "/items" }, "field": "id", "prefix": "item-", "padding": 2 }`. The handler scans the source array, stringifies existing field values, and returns the first unused prefixed number from `start` (default `1`). It is intended for JSON-composed local/editor objects and optimistic drafts; backend-owned database ids still come from the backend.
|
|
3162
|
+
|
|
3163
|
+
263. **Conditional branches deep-resolve selected structured values** - `$cond` and `$switch` resolve nested expressions inside the selected object/array branch, while non-selected branches remain lazy. This allows action params such as `{ "$cond": ..., "$then": { "id": { "$uniqueId": true, ... }, "position": { "x": { "$state": "/ui/spatialCanvasPress/point/x" } } } }` to materialize as plain data before `setState`. Keep `$state` values as string paths; `{ "$state": { "$state": "/path" } }` is invalid.
|
|
3164
|
+
|
|
3165
|
+
264. **`spatial-map` status is visual; `disabled` controls interactivity** - Item `status` values such as `available`, `blocked`, or `inactive` select semantic styling through `statusStyles` and remain selectable, activatable, draggable, keyboard-movable, duplicable, and deletable when the relevant policies allow those operations. Use `disabled: true` only for items that must be intentionally non-interactive. `canvasGuide` is the generic placement/inspection visual aid: drive `canvasGuide.visible` from consumer-owned state (for example `/ui/floorEditor/placeMode`) to render a dotted SVG crosshair at the current map pointer without mutating state on pointer move. Add/place still persists through `onCanvasPress` and `canvasPressPath`. When `editPolicy.snap` is enabled, the visual guide uses the same effective point that `onCanvasPress` writes to `/ui/spatialCanvasPress/point`.
|
|
3166
|
+
|
|
3167
|
+
265. **`spatial-map` snap/guides are generic editor aids** - `editPolicy.snap` computes effective coordinates for item drag, zone drag, keyboard movement, item/zone resize handles, and canvas placement. Supported snap sources are grid snap and item-center snap. Grid threshold is per-axis, so X and Y can snap independently; item-center wins equal-distance ties over grid on the same axis. Keyboard movement with grid snap advances by grid stops on the moved axis. Zone movement snaps by the center of transformed derived zone bounds, or by `position` when no bounds exist. `editPolicy.guides` renders contextual authoring overlays such as snap lines and coordinates. The framework preserves JSON-first persistence: `onItemChange` emits snapped `nextItem.position`, `onZoneChange` emits snapped `nextZone.position` or resized `nextZone.transform`, and `onCanvasPress` writes `point` as the effective coordinate plus `rawPoint` as the original pointer coordinate. Snap/guide visuals do not create, move, or persist domain objects by themselves.
|
|
3168
|
+
|
|
3169
|
+
266. **`spatial-map` resize uses item transform, not shape mutation** - Runtime resize writes `nextItem.transform.scaleX/scaleY` and keeps the base `shape` stable. Simple item shapes derive local edit bounds from geometry; arbitrary `path` items require `localBounds` for resize handles. This keeps one generic editing model across rect, circle, ellipse, polygon, and path shapes and avoids repeated lossy rewrites of polygon points or SVG path data.
|
|
3170
|
+
|
|
3171
|
+
267. **`spatial-map` rotate/resize emit lazy item-change contexts** - Pointer resize/rotate preview locally and commit once on pointer release through `onItemChange`; keyboard resize/rotate emit one change per command. Resize contexts include `previousTransform`, `transform`, `previousLocalBounds`, and `localBounds`; rotate contexts include `previousRotation` and `rotation`; all runtime edits include complete `previousItem` and `nextItem`. Context transforms are normalized with the active edit policy. JSON specs persist edits with `$array: "replace"` from `/ui/spatialItemChange/nextItem` or the configured `itemChangePath`; wrap that same value in `editorCommit` when the screen needs undo/redo/dirty tracking.
|
|
3172
|
+
|
|
3173
|
+
268. **`/ui/selectedSpatialZone` is framework-reserved** - `spatial-map` writes selected/activated zone context for JSON `onZonePress` actions before dispatch. The payload is domain-neutral (`kind: "zone"`, `mode`, `zoneId`, optional `label`, `position`, `rotation`, optional `transform`, optional `localBounds`, `shape`, `metadata`, full `zone`). `selectedZonePath` can override the path for isolated maps. Programmatic callbacks pass through and consumers own selected visual state with `selectedZoneId` or direct writes to `selectedZonePath`.
|
|
3174
|
+
|
|
3175
|
+
269. **`/ui/spatialZoneChange` is framework-reserved** - `spatial-map` writes zone edit-change context before dispatching JSON `onZoneChange`. Runtime zone movement emits `changeType: "move"` and writes `nextZone.position`; runtime zone resize emits `changeType: "resize"` and writes `nextZone.transform.scaleX/scaleY`. Both include complete `previousZone`/`nextZone` records and preserve `nextZone.shape`, which keeps polygon/path geometry stable and leaves vertex/path point editing as a separate capability. Drag, keyboard movement, and zone resize handles use the same snap/guides resolver pattern as items; the movement snap anchor is the center of transformed derived zone bounds, or `position` when no bounds exist. Persist with `$array: "replace"` from `/ui/spatialZoneChange/nextZone` or the configured `zoneChangePath`; wrap that same value in `editorCommit` when the screen needs undo/redo/dirty tracking.
|
|
3176
|
+
|
|
3177
|
+
270. **Gradient cards** — `identity.gradients.cards = true | 'vibrant' | 'soft' | 'muted'` applies a primary→surface diagonal gradient on box surfaces with `surface: "card"`. Implemented via `applyGradientCardsCSS` at the surface CSS layer (mirrors `applyBorderElevationCSS` architecture) so primitives consume `surface.card` uniformly without per-component branching. Uses `backgroundImage` longhand to compose with the surface treatment's `backgroundColor` cleanly. Spec `style.background`/`backgroundImage`/`backgroundColor` overrides
|
|
3178
|
+
|
|
3179
|
+
271. **Modal portals to document.body** — Modal primitive wraps its render in `createPortal(tree, document.body)` so `position: fixed` escapes containing block traps. Required because CSS mount animations with `fill-mode: both` leave a final transform on ancestor elements, and any non-`none` transform on an ancestor scopes `position: fixed` to that ancestor instead of the viewport. SSR-safe via `typeof document === 'undefined'` fallback that returns the inline tree
|
|
3180
|
+
|
|
3181
|
+
272. **Gradient styles use `backgroundImage` longhand** — Spec elements that compose with framework surface treatments must use `backgroundImage` (not `background` shorthand) for gradient values. React warns about mixing shorthand `background` with longhand `backgroundColor` from framework `surface.*` styles. `applyGradientCardsCSS`, `text.tsx` gradient text, and `button.tsx` primary gradient all use `backgroundImage` to compose cleanly. For solid colors, use `backgroundColor` directly
|
|
3182
|
+
|
|
3183
|
+
273. **`editorSessions` declares generic undoable document paths** - `Spec.editorSessions` mounts framework-managed editor history for explicitly tracked document paths. Example: `"editorSessions": { "floor-layout": { "paths": ["/layout/items", "/layout/zones"], "maxHistory": 50 } }`. Paths must be non-empty absolute state paths and must not live under `/ui`; keep transient drafts, selected objects, modals, and placement modes consumer-owned outside the tracked document set.
|
|
3184
|
+
|
|
3185
|
+
274. **`/ui/editorSessions/<id>` is framework-reserved metadata** - Mounted editor sessions write readable metadata under `/ui/editorSessions/<id>`: `dirty`, `canUndo`, `canRedo`, `undoDepth`, `redoDepth`, `revision`, `savedRevision`, `lastCommitLabel`, `lastSavedAt`, `status`, `saveStatus`, `saveError`, `lastSaveAttemptAt`, and `validation`. Consumers may read this metadata for toolbar state and save banners; consumers should not write it directly. `lastCommitLabel` reflects the current top undo entry and is cleared when no undo entry remains.
|
|
3186
|
+
|
|
3187
|
+
275. **Use `editorCommit` for undoable document edits** - `editorCommit` applies one atomic user edit to a configured session and records previous/next values for undo/redo. Use it instead of plain `setState` when an edit should be undoable. Spatial editors persist item/zone changes by wrapping the usual `$array: "replace"` value in an `editorCommit` change targeting `/layout/items` or `/layout/zones`. `editorCommit` with `validate: true` validates after applying the edit and does not roll back automatically.
|
|
3188
|
+
|
|
3189
|
+
276. **Save transport remains backend-owned; `editorSave` is the normal framework path** - The framework does not provide a database adapter, but editor sessions can declare generic persistence through `editorSessions.<id>.persistence` and save with `editorSave`. Use `editorMarkSaved` only as a low-level escape hatch after an advanced/custom persistence step has already succeeded. `editorDiscard` restores the last saved snapshot and clears undo/redo. Dirty state is computed from tracked path equality against the saved snapshot, not from undo-stack length. Commit, undo, redo, and discard clear stale validation metadata and stale save errors unless validation is explicitly requested again.
|
|
3190
|
+
|
|
3191
|
+
277. **`editorSessions.<id>.persistence` declares generic editor save transport** - Configure `url`, optional `method`, optional `headers`, optional `body`, optional `target`, and optional `timeout` on an editor session. Default method is `PUT`; default body is `trackedPaths`, which sends the tracked document paths as a nested object preserving state tree shape. Session headers and per-call headers are merged, with per-call keys winning. Use `body: "snapshot"` when the backend wants absolute-path keys under `paths`, or a custom object body when the app needs a specific envelope.
|
|
3192
|
+
|
|
3193
|
+
278. **`editorSave` is the recommended editor session save action** - Use `{ "action": "editorSave", "params": { "session": "<id>" } }` instead of hand-composing `transaction` + `editorMarkSaved`. The framework captures the in-flight snapshot, persists it through the host fetcher with URL-guard checks, marks only that snapshot as saved after success, and leaves later edits dirty if the user changed the document while save was in flight. Failed saves never mark clean; they expose `saveStatus`, `saveError`, and `lastSaveAttemptAt` under `/ui/editorSessions/<id>`.
|
|
3194
|
+
279. **`navigation.editorSessionGuard` protects dirty editor sessions during app navigation** - App specs can configure `navigation.editorSessionGuard` with `enabled`, optional `sessions`, optional `blockNavigation`, optional `blockGoBack`, optional `blockBrowserUnload`, and optional `pendingPath`. The guard reads framework metadata under `/ui/editorSessions/<id>` and writes pending state to `/ui/navigationGuard/pending` by default. If `sessions` is omitted, all mounted editor sessions are considered.
|
|
3195
|
+
|
|
3196
|
+
280. **Dirty-guard proceed is non-destructive; save/discard choices are explicit** - `navigationGuardCancel` clears the pending guard state and stays on the current screen. `navigationGuardSaveAndProceed` is the normal save-first guard action: it calls the mounted `EditorSessionEngine.save({ session })` for each pending dirty editor session, then resumes only if those sessions become clean. `navigationGuardProceed` is a low-level retry that resumes a pending navigation only after the pending editor sessions are already clean; it does not save or discard tracked-path changes. `navigationGuardDiscardAndProceed` delegates to `EditorSessionEngine.discard(sessionId)` for each pending dirty session, then resumes. Consumer JSON should render confirmation UI from `/ui/navigationGuard/pending` and keep custom `pendingPath` values under non-reserved `/ui/<segment>` paths.
|
|
3197
|
+
|
|
3198
|
+
281. **`MythikApp.fetcher` supplies editor persistence when auth fetch is absent** - React hosts can pass `fetcher={(url, options) => ...}` to `MythikApp` so `editorSave` and `navigationGuardSaveAndProceed` have a transport in non-auth apps. When auth is configured, the framework fetch with auth interceptors remains the active fetcher. Save actions do not read global `/ui/lastError`; editor save status, errors, and attempts are reported under `/ui/editorSessions/<id>`.
|
|
3199
|
+
|
|
3200
|
+
282. **Existing spec edits must use the CLI inspection-then-patch loop** - For an existing screen/app/api spec, AI agents should run `mythik manifest <id>` first, inspect only the target nodes with `mythik elements <id> <ids>`, apply a small RFC 6902 patch with `mythik patch <id> --from-file patch.json`, and verify with `manifest` or `elements`. This keeps edits surgical, validated, versionable, and reviewable. `pull` is for backup, migration, review, or full-document work; `push` is for new specs or intentional full replacement. Direct database edits and direct `SpecStore.save()` calls bypass validation and are not approved spec-write paths.
|
|
3201
|
+
283. **Bundled AI docs are part of the install surface** - The `mythik` package includes `docs/llms.txt`, `docs/consumer/*`, and `docs/wiki/compiled/*`. Agents should run `mythik docs path` before spec generation and read the bundled docs rather than guessing from package source. Use `mythik docs copy ./mythik-docs` to create a project-local copy when the host workflow needs explicit files.
|