prev-cli 0.24.19 → 0.25.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/dist/cli.js +2006 -1703
- package/dist/previews/components/cart-item/index.d.ts +5 -0
- package/dist/previews/components/price-tag/index.d.ts +6 -0
- package/dist/previews/screens/cart/empty.d.ts +1 -0
- package/dist/previews/screens/cart/index.d.ts +1 -0
- package/dist/previews/screens/payment/error.d.ts +1 -0
- package/dist/previews/screens/payment/index.d.ts +1 -0
- package/dist/previews/screens/payment/processing.d.ts +1 -0
- package/dist/previews/screens/receipt/index.d.ts +1 -0
- package/dist/previews/shared/data.d.ts +30 -0
- package/dist/src/content/config-parser.d.ts +30 -0
- package/dist/src/content/flow-verifier.d.ts +21 -0
- package/dist/src/content/preview-types.d.ts +288 -0
- package/dist/{vite → src/content}/previews.d.ts +3 -11
- package/dist/{preview-runtime → src/preview-runtime}/build-optimized.d.ts +2 -0
- package/dist/{preview-runtime → src/preview-runtime}/build.d.ts +1 -1
- package/dist/src/preview-runtime/region-bridge.d.ts +1 -0
- package/dist/{preview-runtime → src/preview-runtime}/types.d.ts +18 -0
- package/dist/src/preview-runtime/vendors.d.ts +11 -0
- package/dist/{renderers → src/renderers}/index.d.ts +1 -1
- package/dist/{renderers → src/renderers}/types.d.ts +3 -31
- package/dist/src/server/build.d.ts +6 -0
- package/dist/src/server/dev.d.ts +13 -0
- package/dist/src/server/plugins/aliases.d.ts +5 -0
- package/dist/src/server/plugins/mdx.d.ts +5 -0
- package/dist/src/server/plugins/virtual-modules.d.ts +8 -0
- package/dist/src/server/preview.d.ts +10 -0
- package/dist/src/server/routes/component-bundle.d.ts +1 -0
- package/dist/src/server/routes/jsx-bundle.d.ts +3 -0
- package/dist/src/server/routes/og-image.d.ts +15 -0
- package/dist/src/server/routes/preview-bundle.d.ts +1 -0
- package/dist/src/server/routes/preview-config.d.ts +1 -0
- package/dist/src/server/routes/tokens.d.ts +1 -0
- package/dist/{vite → src/server}/start.d.ts +5 -2
- package/dist/{ui → src/ui}/button.d.ts +1 -1
- package/dist/{validators → src/validators}/index.d.ts +0 -5
- package/dist/{validators → src/validators}/semantic-validator.d.ts +2 -3
- package/package.json +8 -11
- package/src/jsx/CLAUDE.md +18 -0
- package/src/jsx/jsx-runtime.ts +1 -1
- package/src/preview-runtime/CLAUDE.md +21 -0
- package/src/preview-runtime/build-optimized.ts +189 -73
- package/src/preview-runtime/build.ts +75 -79
- package/src/preview-runtime/fast-template.html +5 -1
- package/src/preview-runtime/region-bridge.test.ts +41 -0
- package/src/preview-runtime/region-bridge.ts +101 -0
- package/src/preview-runtime/types.ts +6 -0
- package/src/preview-runtime/vendors.ts +215 -22
- package/src/primitives/CLAUDE.md +17 -0
- package/src/theme/CLAUDE.md +20 -0
- package/src/theme/Preview.tsx +10 -4
- package/src/theme/Toolbar.tsx +2 -2
- package/src/theme/entry.tsx +247 -121
- package/src/theme/hooks/useAnnotations.ts +77 -0
- package/src/theme/hooks/useApprovalStatus.ts +50 -0
- package/src/theme/hooks/useSnapshots.ts +147 -0
- package/src/theme/hooks/useStorage.ts +26 -0
- package/src/theme/hooks/useTokenOverrides.ts +56 -0
- package/src/theme/hooks/useViewport.ts +23 -0
- package/src/theme/icons.tsx +39 -1
- package/src/theme/index.html +18 -0
- package/src/theme/mdx-components.tsx +1 -1
- package/src/theme/previews/AnnotationLayer.tsx +285 -0
- package/src/theme/previews/AnnotationPin.tsx +61 -0
- package/src/theme/previews/AnnotationThread.tsx +257 -0
- package/src/theme/previews/CLAUDE.md +18 -0
- package/src/theme/previews/ComponentPreview.tsx +487 -107
- package/src/theme/previews/FlowDiagram.tsx +111 -0
- package/src/theme/previews/FlowPreview.tsx +938 -174
- package/src/theme/previews/PreviewRouter.tsx +1 -4
- package/src/theme/previews/ScreenPreview.tsx +515 -175
- package/src/theme/previews/SnapshotButton.tsx +68 -0
- package/src/theme/previews/SnapshotCompare.tsx +216 -0
- package/src/theme/previews/SnapshotPanel.tsx +274 -0
- package/src/theme/previews/StatusBadge.tsx +66 -0
- package/src/theme/previews/StatusDropdown.tsx +158 -0
- package/src/theme/previews/TokenPlayground.tsx +438 -0
- package/src/theme/previews/ViewportControls.tsx +67 -0
- package/src/theme/previews/flow-diagram.test.ts +141 -0
- package/src/theme/previews/flow-diagram.ts +109 -0
- package/src/theme/previews/flow-navigation.test.ts +90 -0
- package/src/theme/previews/flow-navigation.ts +47 -0
- package/src/theme/previews/machines/derived.test.ts +225 -0
- package/src/theme/previews/machines/derived.ts +73 -0
- package/src/theme/previews/machines/flow-machine.test.ts +379 -0
- package/src/theme/previews/machines/flow-machine.ts +207 -0
- package/src/theme/previews/machines/screen-machine.test.ts +149 -0
- package/src/theme/previews/machines/screen-machine.ts +76 -0
- package/src/theme/previews/stores/flow-store.test.ts +157 -0
- package/src/theme/previews/stores/flow-store.ts +49 -0
- package/src/theme/previews/stores/screen-store.test.ts +68 -0
- package/src/theme/previews/stores/screen-store.ts +33 -0
- package/src/theme/storage.test.ts +97 -0
- package/src/theme/storage.ts +71 -0
- package/src/theme/styles.css +296 -25
- package/src/theme/types.ts +64 -0
- package/src/tokens/CLAUDE.md +16 -0
- package/src/tokens/resolver.ts +1 -1
- package/dist/preview-runtime/vendors.d.ts +0 -6
- package/dist/vite/config-parser.d.ts +0 -13
- package/dist/vite/config.d.ts +0 -12
- package/dist/vite/plugins/config-plugin.d.ts +0 -3
- package/dist/vite/plugins/debug-plugin.d.ts +0 -3
- package/dist/vite/plugins/entry-plugin.d.ts +0 -2
- package/dist/vite/plugins/fumadocs-plugin.d.ts +0 -9
- package/dist/vite/plugins/pages-plugin.d.ts +0 -5
- package/dist/vite/plugins/previews-plugin.d.ts +0 -2
- package/dist/vite/plugins/tokens-plugin.d.ts +0 -2
- package/dist/vite/preview-types.d.ts +0 -70
- package/src/theme/previews/AtlasPreview.tsx +0 -528
- package/dist/{cli.d.ts → src/cli.d.ts} +0 -0
- package/dist/{config → src/config}/index.d.ts +0 -0
- package/dist/{config → src/config}/loader.d.ts +0 -0
- package/dist/{config → src/config}/schema.d.ts +0 -0
- package/dist/{vite → src/content}/pages.d.ts +0 -0
- package/dist/{jsx → src/jsx}/adapters/html.d.ts +0 -0
- package/dist/{jsx → src/jsx}/adapters/react.d.ts +0 -0
- package/dist/{jsx → src/jsx}/define-component.d.ts +0 -0
- package/dist/{jsx → src/jsx}/index.d.ts +0 -0
- package/dist/{jsx → src/jsx}/jsx-runtime.d.ts +0 -0
- package/dist/{jsx → src/jsx}/migrate.d.ts +0 -0
- package/dist/{jsx → src/jsx}/schemas/index.d.ts +0 -0
- package/dist/{jsx → src/jsx}/schemas/primitives.d.ts +10 -10
- package/dist/{jsx → src/jsx}/schemas/tokens.d.ts +3 -3
- /package/dist/{jsx → src/jsx}/validation.d.ts +0 -0
- /package/dist/{jsx → src/jsx}/vnode.d.ts +0 -0
- /package/dist/{migrate.d.ts → src/migrate.d.ts} +0 -0
- /package/dist/{preview-runtime → src/preview-runtime}/tailwind.d.ts +0 -0
- /package/dist/{primitives → src/primitives}/index.d.ts +0 -0
- /package/dist/{primitives → src/primitives}/migrate.d.ts +0 -0
- /package/dist/{primitives → src/primitives}/parser.d.ts +0 -0
- /package/dist/{primitives → src/primitives}/template-parser.d.ts +0 -0
- /package/dist/{primitives → src/primitives}/template-renderer.d.ts +0 -0
- /package/dist/{primitives → src/primitives}/types.d.ts +0 -0
- /package/dist/{renderers → src/renderers}/html/index.d.ts +0 -0
- /package/dist/{renderers → src/renderers}/react/index.d.ts +0 -0
- /package/dist/{renderers → src/renderers}/registry.d.ts +0 -0
- /package/dist/{renderers → src/renderers}/render.d.ts +0 -0
- /package/dist/{tokens → src/tokens}/defaults.d.ts +0 -0
- /package/dist/{tokens → src/tokens}/resolver.d.ts +0 -0
- /package/dist/{tokens → src/tokens}/utils.d.ts +0 -0
- /package/dist/{tokens → src/tokens}/validation.d.ts +0 -0
- /package/dist/{typecheck → src/typecheck}/index.d.ts +0 -0
- /package/dist/{ui → src/ui}/card.d.ts +0 -0
- /package/dist/{ui → src/ui}/index.d.ts +0 -0
- /package/dist/{ui → src/ui}/utils.d.ts +0 -0
- /package/dist/{utils → src/utils}/cache.d.ts +0 -0
- /package/dist/{utils → src/utils}/debug.d.ts +0 -0
- /package/dist/{utils → src/utils}/port.d.ts +0 -0
- /package/dist/{validators → src/validators}/schema-validator.d.ts +0 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { test, expect, describe } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
screenReducer,
|
|
4
|
+
createScreenInitialState,
|
|
5
|
+
type ScreenMachineState,
|
|
6
|
+
type ScreenAction,
|
|
7
|
+
} from './screen-machine'
|
|
8
|
+
|
|
9
|
+
// --- Test fixtures ---
|
|
10
|
+
|
|
11
|
+
function readyState(overrides?: Partial<ScreenMachineState>): ScreenMachineState {
|
|
12
|
+
return {
|
|
13
|
+
...createScreenInitialState(['index', 'error', 'loading']),
|
|
14
|
+
status: 'ready',
|
|
15
|
+
...overrides,
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// --- Tests ---
|
|
20
|
+
|
|
21
|
+
describe('createScreenInitialState', () => {
|
|
22
|
+
test('returns loading state with provided states', () => {
|
|
23
|
+
const state = createScreenInitialState(['index', 'error', 'loading'])
|
|
24
|
+
expect(state.status).toBe('loading')
|
|
25
|
+
expect(state.activeState).toBe('index')
|
|
26
|
+
expect(state.availableStates).toEqual(['index', 'error', 'loading'])
|
|
27
|
+
expect(state.viewport).toBe('desktop')
|
|
28
|
+
expect(state.isFullscreen).toBe(false)
|
|
29
|
+
expect(state.error).toBeUndefined()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('defaults to index-only when no states provided', () => {
|
|
33
|
+
const state = createScreenInitialState([])
|
|
34
|
+
expect(state.activeState).toBe('index')
|
|
35
|
+
expect(state.availableStates).toEqual([])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('accepts initial state override', () => {
|
|
39
|
+
const state = createScreenInitialState(['index', 'error'], 'error')
|
|
40
|
+
expect(state.activeState).toBe('error')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('screenReducer — set_state', () => {
|
|
45
|
+
test('switches to a valid state', () => {
|
|
46
|
+
const state = readyState()
|
|
47
|
+
const next = screenReducer(state, { type: 'set_state', state: 'error' })
|
|
48
|
+
expect(next.activeState).toBe('error')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('ignores switch to non-existent state', () => {
|
|
52
|
+
const state = readyState()
|
|
53
|
+
const same = screenReducer(state, { type: 'set_state', state: 'nonexistent' })
|
|
54
|
+
expect(same.activeState).toBe('index')
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
describe('screenReducer — set_viewport', () => {
|
|
59
|
+
test('switches viewport to mobile', () => {
|
|
60
|
+
const state = readyState()
|
|
61
|
+
const next = screenReducer(state, { type: 'set_viewport', viewport: 'mobile' })
|
|
62
|
+
expect(next.viewport).toBe('mobile')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('switches viewport to tablet', () => {
|
|
66
|
+
const state = readyState()
|
|
67
|
+
const next = screenReducer(state, { type: 'set_viewport', viewport: 'tablet' })
|
|
68
|
+
expect(next.viewport).toBe('tablet')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('switches viewport to desktop', () => {
|
|
72
|
+
const state = readyState({ viewport: 'mobile' })
|
|
73
|
+
const next = screenReducer(state, { type: 'set_viewport', viewport: 'desktop' })
|
|
74
|
+
expect(next.viewport).toBe('desktop')
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe('screenReducer — toggle_fullscreen', () => {
|
|
79
|
+
test('toggles fullscreen on', () => {
|
|
80
|
+
const state = readyState()
|
|
81
|
+
const next = screenReducer(state, { type: 'toggle_fullscreen' })
|
|
82
|
+
expect(next.isFullscreen).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('toggles fullscreen off', () => {
|
|
86
|
+
const state = readyState({ isFullscreen: true })
|
|
87
|
+
const next = screenReducer(state, { type: 'toggle_fullscreen' })
|
|
88
|
+
expect(next.isFullscreen).toBe(false)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe('screenReducer — build status FSM', () => {
|
|
93
|
+
test('loading -> building via build_started', () => {
|
|
94
|
+
const state = createScreenInitialState(['index'])
|
|
95
|
+
expect(state.status).toBe('loading')
|
|
96
|
+
|
|
97
|
+
const next = screenReducer(state, { type: 'build_started' })
|
|
98
|
+
expect(next.status).toBe('building')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('building -> ready via build_ready', () => {
|
|
102
|
+
const state = readyState({ status: 'building' })
|
|
103
|
+
const next = screenReducer(state, { type: 'build_ready' })
|
|
104
|
+
expect(next.status).toBe('ready')
|
|
105
|
+
expect(next.error).toBeUndefined()
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test('build_ready clears previous error', () => {
|
|
109
|
+
const state = readyState({ status: 'building', error: 'previous error' })
|
|
110
|
+
const next = screenReducer(state, { type: 'build_ready' })
|
|
111
|
+
expect(next.error).toBeUndefined()
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test('building -> error via build_error', () => {
|
|
115
|
+
const state = readyState({ status: 'building' })
|
|
116
|
+
const next = screenReducer(state, { type: 'build_error', error: 'Syntax error in component' })
|
|
117
|
+
expect(next.status).toBe('error')
|
|
118
|
+
expect(next.error).toBe('Syntax error in component')
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
test('error -> building via build_started (retry)', () => {
|
|
122
|
+
const state = readyState({ status: 'error', error: 'previous' })
|
|
123
|
+
const next = screenReducer(state, { type: 'build_started' })
|
|
124
|
+
expect(next.status).toBe('building')
|
|
125
|
+
})
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
// --- Codex review fixes ---
|
|
129
|
+
|
|
130
|
+
describe('createScreenInitialState — invalid initial state clamping', () => {
|
|
131
|
+
test('clamps to index when initialState not in availableStates', () => {
|
|
132
|
+
const state = createScreenInitialState(['index', 'error'], 'nonexistent')
|
|
133
|
+
expect(state.activeState).toBe('index')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('clamps to first available when initialState invalid and no index', () => {
|
|
137
|
+
const state = createScreenInitialState(['error', 'loading'], 'nonexistent')
|
|
138
|
+
expect(state.activeState).toBe('error')
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('screenReducer — immutability', () => {
|
|
143
|
+
test('does not mutate previous state', () => {
|
|
144
|
+
const state = readyState()
|
|
145
|
+
const original = state.activeState
|
|
146
|
+
screenReducer(state, { type: 'set_state', state: 'error' })
|
|
147
|
+
expect(state.activeState).toBe(original)
|
|
148
|
+
})
|
|
149
|
+
})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Pure screen state machine — zero React imports
|
|
2
|
+
|
|
3
|
+
// --- State ---
|
|
4
|
+
|
|
5
|
+
export interface ScreenMachineState {
|
|
6
|
+
status: 'loading' | 'building' | 'ready' | 'error'
|
|
7
|
+
activeState: string
|
|
8
|
+
availableStates: string[]
|
|
9
|
+
viewport: 'mobile' | 'tablet' | 'desktop'
|
|
10
|
+
isFullscreen: boolean
|
|
11
|
+
error?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// --- Actions ---
|
|
15
|
+
|
|
16
|
+
export type ScreenAction =
|
|
17
|
+
| { type: 'set_state'; state: string }
|
|
18
|
+
| { type: 'set_viewport'; viewport: 'mobile' | 'tablet' | 'desktop' }
|
|
19
|
+
| { type: 'toggle_fullscreen' }
|
|
20
|
+
| { type: 'build_started' }
|
|
21
|
+
| { type: 'build_ready' }
|
|
22
|
+
| { type: 'build_error'; error: string }
|
|
23
|
+
|
|
24
|
+
// --- Initial state ---
|
|
25
|
+
|
|
26
|
+
export function createScreenInitialState(
|
|
27
|
+
availableStates: string[],
|
|
28
|
+
initialState?: string,
|
|
29
|
+
): ScreenMachineState {
|
|
30
|
+
// Clamp: use initialState if valid, else 'index' if available, else first available, else 'index'
|
|
31
|
+
let active = 'index'
|
|
32
|
+
if (initialState && availableStates.includes(initialState)) {
|
|
33
|
+
active = initialState
|
|
34
|
+
} else if (availableStates.includes('index')) {
|
|
35
|
+
active = 'index'
|
|
36
|
+
} else if (availableStates.length > 0) {
|
|
37
|
+
active = availableStates[0]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
status: 'loading',
|
|
42
|
+
activeState: active,
|
|
43
|
+
availableStates,
|
|
44
|
+
viewport: 'desktop',
|
|
45
|
+
isFullscreen: false,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// --- Reducer ---
|
|
50
|
+
|
|
51
|
+
export function screenReducer(state: ScreenMachineState, action: ScreenAction): ScreenMachineState {
|
|
52
|
+
switch (action.type) {
|
|
53
|
+
case 'set_state': {
|
|
54
|
+
if (!state.availableStates.includes(action.state)) return state
|
|
55
|
+
return { ...state, activeState: action.state }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
case 'set_viewport':
|
|
59
|
+
return { ...state, viewport: action.viewport }
|
|
60
|
+
|
|
61
|
+
case 'toggle_fullscreen':
|
|
62
|
+
return { ...state, isFullscreen: !state.isFullscreen }
|
|
63
|
+
|
|
64
|
+
case 'build_started':
|
|
65
|
+
return { ...state, status: 'building' }
|
|
66
|
+
|
|
67
|
+
case 'build_ready':
|
|
68
|
+
return { ...state, status: 'ready', error: undefined }
|
|
69
|
+
|
|
70
|
+
case 'build_error':
|
|
71
|
+
return { ...state, status: 'error', error: action.error }
|
|
72
|
+
|
|
73
|
+
default:
|
|
74
|
+
return state
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { test, expect, describe } from 'bun:test'
|
|
2
|
+
import { createFlowStore } from './flow-store'
|
|
3
|
+
import type { FlowStep } from '../../../content/preview-types'
|
|
4
|
+
|
|
5
|
+
const steps: FlowStep[] = [
|
|
6
|
+
{
|
|
7
|
+
id: 'step1',
|
|
8
|
+
title: 'Sign Up',
|
|
9
|
+
screen: 'signup',
|
|
10
|
+
regions: {
|
|
11
|
+
submit: { goto: 'step2' },
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
id: 'step2',
|
|
16
|
+
title: 'Dashboard',
|
|
17
|
+
screen: 'dashboard',
|
|
18
|
+
regions: {
|
|
19
|
+
'upgrade-btn': {
|
|
20
|
+
outcomes: {
|
|
21
|
+
success: { goto: 'step3', label: 'Paid' },
|
|
22
|
+
failure: { goto: 'step1' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'step3',
|
|
29
|
+
title: 'Done',
|
|
30
|
+
screen: 'settings',
|
|
31
|
+
terminal: true,
|
|
32
|
+
},
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
describe('createFlowStore', () => {
|
|
36
|
+
test('starts in loading state', () => {
|
|
37
|
+
const store = createFlowStore()
|
|
38
|
+
expect(store.getState().status).toBe('loading')
|
|
39
|
+
expect(store.getState().currentStepId).toBeNull()
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('loaded action transitions to ready with steps', () => {
|
|
43
|
+
const store = createFlowStore()
|
|
44
|
+
store.getState().loaded(steps, 'Test Flow', 'A flow')
|
|
45
|
+
const state = store.getState()
|
|
46
|
+
expect(state.status).toBe('ready')
|
|
47
|
+
expect(state.name).toBe('Test Flow')
|
|
48
|
+
expect(state.steps).toEqual(steps)
|
|
49
|
+
expect(state.currentStepId).toBe('step1')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('goto navigates and updates history', () => {
|
|
53
|
+
const store = createFlowStore()
|
|
54
|
+
store.getState().loaded(steps, 'Test')
|
|
55
|
+
store.getState().goto('step2')
|
|
56
|
+
expect(store.getState().currentStepId).toBe('step2')
|
|
57
|
+
expect(store.getState().history).toEqual(['step1', 'step2'])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('back pops history', () => {
|
|
61
|
+
const store = createFlowStore()
|
|
62
|
+
store.getState().loaded(steps, 'Test')
|
|
63
|
+
store.getState().goto('step2')
|
|
64
|
+
store.getState().back()
|
|
65
|
+
expect(store.getState().currentStepId).toBe('step1')
|
|
66
|
+
expect(store.getState().history).toEqual(['step1'])
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('regionClick with goto navigates', () => {
|
|
70
|
+
const store = createFlowStore()
|
|
71
|
+
store.getState().loaded(steps, 'Test')
|
|
72
|
+
store.getState().regionClick('submit')
|
|
73
|
+
expect(store.getState().currentStepId).toBe('step2')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('regionClick with outcomes opens picker', () => {
|
|
77
|
+
const store = createFlowStore()
|
|
78
|
+
store.getState().loaded(steps, 'Test')
|
|
79
|
+
store.getState().goto('step2')
|
|
80
|
+
store.getState().regionClick('upgrade-btn')
|
|
81
|
+
expect(store.getState().outcomePicker).not.toBeNull()
|
|
82
|
+
expect(store.getState().currentStepId).toBe('step2')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('pickOutcome navigates and clears picker', () => {
|
|
86
|
+
const store = createFlowStore()
|
|
87
|
+
store.getState().loaded(steps, 'Test')
|
|
88
|
+
store.getState().goto('step2')
|
|
89
|
+
store.getState().regionClick('upgrade-btn')
|
|
90
|
+
store.getState().pickOutcome('step3')
|
|
91
|
+
expect(store.getState().currentStepId).toBe('step3')
|
|
92
|
+
expect(store.getState().outcomePicker).toBeNull()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('cancelPicker clears picker without navigating', () => {
|
|
96
|
+
const store = createFlowStore()
|
|
97
|
+
store.getState().loaded(steps, 'Test')
|
|
98
|
+
store.getState().goto('step2')
|
|
99
|
+
store.getState().regionClick('upgrade-btn')
|
|
100
|
+
store.getState().cancelPicker()
|
|
101
|
+
expect(store.getState().outcomePicker).toBeNull()
|
|
102
|
+
expect(store.getState().currentStepId).toBe('step2')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('toggleOverlay toggles showOverlay', () => {
|
|
106
|
+
const store = createFlowStore()
|
|
107
|
+
expect(store.getState().showOverlay).toBe(true)
|
|
108
|
+
store.getState().toggleOverlay()
|
|
109
|
+
expect(store.getState().showOverlay).toBe(false)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('toggleFullscreen toggles isFullscreen', () => {
|
|
113
|
+
const store = createFlowStore()
|
|
114
|
+
expect(store.getState().isFullscreen).toBe(false)
|
|
115
|
+
store.getState().toggleFullscreen()
|
|
116
|
+
expect(store.getState().isFullscreen).toBe(true)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('linearNext advances for non-region steps', () => {
|
|
120
|
+
const linearSteps: FlowStep[] = [
|
|
121
|
+
{ id: 'a', screen: 'page-a' },
|
|
122
|
+
{ id: 'b', screen: 'page-b' },
|
|
123
|
+
]
|
|
124
|
+
const store = createFlowStore()
|
|
125
|
+
store.getState().loaded(linearSteps, 'Test')
|
|
126
|
+
store.getState().linearNext()
|
|
127
|
+
expect(store.getState().currentStepId).toBe('b')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('linearPrev goes back', () => {
|
|
131
|
+
const linearSteps: FlowStep[] = [
|
|
132
|
+
{ id: 'a', screen: 'page-a' },
|
|
133
|
+
{ id: 'b', screen: 'page-b' },
|
|
134
|
+
]
|
|
135
|
+
const store = createFlowStore()
|
|
136
|
+
store.getState().loaded(linearSteps, 'Test')
|
|
137
|
+
store.getState().linearNext()
|
|
138
|
+
store.getState().linearPrev()
|
|
139
|
+
expect(store.getState().currentStepId).toBe('a')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
// Codex review: missing store-level tests
|
|
143
|
+
test('loadError sets error state', () => {
|
|
144
|
+
const store = createFlowStore()
|
|
145
|
+
store.getState().loadError('Network failure')
|
|
146
|
+
expect(store.getState().status).toBe('error')
|
|
147
|
+
expect(store.getState().error).toBe('Network failure')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('load_error -> loaded clears stale error', () => {
|
|
151
|
+
const store = createFlowStore()
|
|
152
|
+
store.getState().loadError('oops')
|
|
153
|
+
store.getState().loaded(steps, 'Recovered')
|
|
154
|
+
expect(store.getState().status).toBe('ready')
|
|
155
|
+
expect(store.getState().error).toBeUndefined()
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Zustand store wrapping the flow state machine
|
|
2
|
+
|
|
3
|
+
import { createStore } from 'zustand/vanilla'
|
|
4
|
+
import { flowReducer, createFlowInitialState, type FlowMachineState } from '../machines/flow-machine'
|
|
5
|
+
import type { FlowStep } from '../../../content/preview-types'
|
|
6
|
+
|
|
7
|
+
export interface FlowStoreActions {
|
|
8
|
+
loaded: (steps: FlowStep[], name: string, description?: string) => void
|
|
9
|
+
loadError: (error?: string) => void
|
|
10
|
+
goto: (stepId: string) => void
|
|
11
|
+
back: () => void
|
|
12
|
+
linearNext: () => void
|
|
13
|
+
linearPrev: () => void
|
|
14
|
+
regionClick: (region: string) => void
|
|
15
|
+
pickOutcome: (stepId: string) => void
|
|
16
|
+
cancelPicker: () => void
|
|
17
|
+
toggleOverlay: () => void
|
|
18
|
+
toggleFullscreen: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type FlowStore = FlowMachineState & FlowStoreActions
|
|
22
|
+
|
|
23
|
+
export function createFlowStore() {
|
|
24
|
+
return createStore<FlowStore>((set) => ({
|
|
25
|
+
...createFlowInitialState(),
|
|
26
|
+
loaded: (steps, name, description) =>
|
|
27
|
+
set(s => flowReducer(s, { type: 'loaded', steps, name, description })),
|
|
28
|
+
loadError: (error) =>
|
|
29
|
+
set(s => flowReducer(s, { type: 'load_error', error })),
|
|
30
|
+
goto: (stepId) =>
|
|
31
|
+
set(s => flowReducer(s, { type: 'goto', stepId })),
|
|
32
|
+
back: () =>
|
|
33
|
+
set(s => flowReducer(s, { type: 'back' })),
|
|
34
|
+
linearNext: () =>
|
|
35
|
+
set(s => flowReducer(s, { type: 'linear_next' })),
|
|
36
|
+
linearPrev: () =>
|
|
37
|
+
set(s => flowReducer(s, { type: 'linear_prev' })),
|
|
38
|
+
regionClick: (region) =>
|
|
39
|
+
set(s => flowReducer(s, { type: 'region_click', region })),
|
|
40
|
+
pickOutcome: (stepId) =>
|
|
41
|
+
set(s => flowReducer(s, { type: 'pick_outcome', stepId })),
|
|
42
|
+
cancelPicker: () =>
|
|
43
|
+
set(s => flowReducer(s, { type: 'cancel_picker' })),
|
|
44
|
+
toggleOverlay: () =>
|
|
45
|
+
set(s => flowReducer(s, { type: 'toggle_overlay' })),
|
|
46
|
+
toggleFullscreen: () =>
|
|
47
|
+
set(s => flowReducer(s, { type: 'toggle_fullscreen' })),
|
|
48
|
+
}))
|
|
49
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { test, expect, describe } from 'bun:test'
|
|
2
|
+
import { createScreenStore } from './screen-store'
|
|
3
|
+
|
|
4
|
+
describe('createScreenStore', () => {
|
|
5
|
+
test('starts in loading state', () => {
|
|
6
|
+
const store = createScreenStore(['index', 'error'])
|
|
7
|
+
expect(store.getState().status).toBe('loading')
|
|
8
|
+
expect(store.getState().activeState).toBe('index')
|
|
9
|
+
expect(store.getState().availableStates).toEqual(['index', 'error'])
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('accepts initial state override', () => {
|
|
13
|
+
const store = createScreenStore(['index', 'error'], 'error')
|
|
14
|
+
expect(store.getState().activeState).toBe('error')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('setState switches to valid state', () => {
|
|
18
|
+
const store = createScreenStore(['index', 'error'])
|
|
19
|
+
store.getState().setState('error')
|
|
20
|
+
expect(store.getState().activeState).toBe('error')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('setState ignores invalid state', () => {
|
|
24
|
+
const store = createScreenStore(['index', 'error'])
|
|
25
|
+
store.getState().setState('nonexistent')
|
|
26
|
+
expect(store.getState().activeState).toBe('index')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('setViewport switches viewport', () => {
|
|
30
|
+
const store = createScreenStore(['index'])
|
|
31
|
+
store.getState().setViewport('mobile')
|
|
32
|
+
expect(store.getState().viewport).toBe('mobile')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('toggleFullscreen toggles', () => {
|
|
36
|
+
const store = createScreenStore(['index'])
|
|
37
|
+
expect(store.getState().isFullscreen).toBe(false)
|
|
38
|
+
store.getState().toggleFullscreen()
|
|
39
|
+
expect(store.getState().isFullscreen).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('build status FSM: loading -> building -> ready', () => {
|
|
43
|
+
const store = createScreenStore(['index'])
|
|
44
|
+
expect(store.getState().status).toBe('loading')
|
|
45
|
+
store.getState().buildStarted()
|
|
46
|
+
expect(store.getState().status).toBe('building')
|
|
47
|
+
store.getState().buildReady()
|
|
48
|
+
expect(store.getState().status).toBe('ready')
|
|
49
|
+
expect(store.getState().error).toBeUndefined()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('build status FSM: building -> error', () => {
|
|
53
|
+
const store = createScreenStore(['index'])
|
|
54
|
+
store.getState().buildStarted()
|
|
55
|
+
store.getState().buildError('Syntax error')
|
|
56
|
+
expect(store.getState().status).toBe('error')
|
|
57
|
+
expect(store.getState().error).toBe('Syntax error')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('build_ready clears previous error', () => {
|
|
61
|
+
const store = createScreenStore(['index'])
|
|
62
|
+
store.getState().buildStarted()
|
|
63
|
+
store.getState().buildError('oops')
|
|
64
|
+
store.getState().buildStarted()
|
|
65
|
+
store.getState().buildReady()
|
|
66
|
+
expect(store.getState().error).toBeUndefined()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Zustand store wrapping the screen state machine
|
|
2
|
+
|
|
3
|
+
import { createStore } from 'zustand/vanilla'
|
|
4
|
+
import { screenReducer, createScreenInitialState, type ScreenMachineState } from '../machines/screen-machine'
|
|
5
|
+
|
|
6
|
+
export interface ScreenStoreActions {
|
|
7
|
+
setState: (state: string) => void
|
|
8
|
+
setViewport: (viewport: 'mobile' | 'tablet' | 'desktop') => void
|
|
9
|
+
toggleFullscreen: () => void
|
|
10
|
+
buildStarted: () => void
|
|
11
|
+
buildReady: () => void
|
|
12
|
+
buildError: (error: string) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type ScreenStore = ScreenMachineState & ScreenStoreActions
|
|
16
|
+
|
|
17
|
+
export function createScreenStore(availableStates: string[], initialState?: string) {
|
|
18
|
+
return createStore<ScreenStore>((set) => ({
|
|
19
|
+
...createScreenInitialState(availableStates, initialState),
|
|
20
|
+
setState: (state) =>
|
|
21
|
+
set(s => screenReducer(s, { type: 'set_state', state })),
|
|
22
|
+
setViewport: (viewport) =>
|
|
23
|
+
set(s => screenReducer(s, { type: 'set_viewport', viewport })),
|
|
24
|
+
toggleFullscreen: () =>
|
|
25
|
+
set(s => screenReducer(s, { type: 'toggle_fullscreen' })),
|
|
26
|
+
buildStarted: () =>
|
|
27
|
+
set(s => screenReducer(s, { type: 'build_started' })),
|
|
28
|
+
buildReady: () =>
|
|
29
|
+
set(s => screenReducer(s, { type: 'build_ready' })),
|
|
30
|
+
buildError: (error) =>
|
|
31
|
+
set(s => screenReducer(s, { type: 'build_error', error })),
|
|
32
|
+
}))
|
|
33
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { test, expect, describe, beforeEach } from 'bun:test'
|
|
2
|
+
import { PrefixedStorageAdapter } from './storage'
|
|
3
|
+
|
|
4
|
+
function createMemoryBackend() {
|
|
5
|
+
const store = new Map<string, string>()
|
|
6
|
+
return {
|
|
7
|
+
getItem: (k: string) => store.get(k) ?? null,
|
|
8
|
+
setItem: (k: string, v: string) => store.set(k, v),
|
|
9
|
+
removeItem: (k: string) => store.delete(k),
|
|
10
|
+
key: (i: number) => [...store.keys()][i] ?? null,
|
|
11
|
+
get length() { return store.size },
|
|
12
|
+
_store: store,
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('PrefixedStorageAdapter', () => {
|
|
17
|
+
let backend: ReturnType<typeof createMemoryBackend>
|
|
18
|
+
let storage: PrefixedStorageAdapter
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
backend = createMemoryBackend()
|
|
22
|
+
storage = new PrefixedStorageAdapter(backend)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('get returns null for missing key', () => {
|
|
26
|
+
expect(storage.get('nonexistent')).toBeNull()
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('set and get round-trip', () => {
|
|
30
|
+
storage.set('test-key', { name: 'hello', count: 42 })
|
|
31
|
+
expect(storage.get('test-key')).toEqual({ name: 'hello', count: 42 })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('set overwrites existing value', () => {
|
|
35
|
+
storage.set('key', 'first')
|
|
36
|
+
storage.set('key', 'second')
|
|
37
|
+
expect(storage.get('key')).toBe('second')
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('remove deletes a key', () => {
|
|
41
|
+
storage.set('to-remove', 'value')
|
|
42
|
+
storage.remove('to-remove')
|
|
43
|
+
expect(storage.get('to-remove')).toBeNull()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('list returns keys matching prefix', () => {
|
|
47
|
+
storage.set('annotations:screen1:a', { id: 'a' })
|
|
48
|
+
storage.set('annotations:screen1:b', { id: 'b' })
|
|
49
|
+
storage.set('status:screen1', { status: 'draft' })
|
|
50
|
+
|
|
51
|
+
const keys = storage.list('annotations:')
|
|
52
|
+
expect(keys).toContain('annotations:screen1:a')
|
|
53
|
+
expect(keys).toContain('annotations:screen1:b')
|
|
54
|
+
expect(keys).not.toContain('status:screen1')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('list returns empty array when no matches', () => {
|
|
58
|
+
expect(storage.list('nothing:')).toEqual([])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('clear removes all keys matching prefix', () => {
|
|
62
|
+
storage.set('annotations:a', 1)
|
|
63
|
+
storage.set('annotations:b', 2)
|
|
64
|
+
storage.set('status:x', 3)
|
|
65
|
+
|
|
66
|
+
storage.clear('annotations:')
|
|
67
|
+
|
|
68
|
+
expect(storage.get('annotations:a')).toBeNull()
|
|
69
|
+
expect(storage.get('annotations:b')).toBeNull()
|
|
70
|
+
expect(storage.get('status:x')).toBe(3)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('get handles corrupt JSON gracefully', () => {
|
|
74
|
+
backend._store.set('prev:corrupt', '{bad json')
|
|
75
|
+
expect(storage.get('corrupt')).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('stores and retrieves arrays', () => {
|
|
79
|
+
storage.set('arr', [1, 2, 3])
|
|
80
|
+
expect(storage.get('arr')).toEqual([1, 2, 3])
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
test('stores and retrieves nested objects', () => {
|
|
84
|
+
const data = { a: { b: { c: 'deep' } } }
|
|
85
|
+
storage.set('nested', data)
|
|
86
|
+
expect(storage.get('nested')).toEqual(data)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('prefix isolation - different prefixes dont collide', () => {
|
|
90
|
+
const storage2 = new PrefixedStorageAdapter(backend, 'other:')
|
|
91
|
+
storage.set('key', 'from-prev')
|
|
92
|
+
storage2.set('key', 'from-other')
|
|
93
|
+
|
|
94
|
+
expect(storage.get('key')).toBe('from-prev')
|
|
95
|
+
expect(storage2.get('key')).toBe('from-other')
|
|
96
|
+
})
|
|
97
|
+
})
|