@demokit-ai/react 0.0.1

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/index.cjs ADDED
@@ -0,0 +1,231 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var core = require('@demokit-ai/core');
5
+ var jsxRuntime = require('react/jsx-runtime');
6
+
7
+ // src/provider.tsx
8
+ var DemoModeContext = react.createContext(void 0);
9
+ DemoModeContext.displayName = "DemoModeContext";
10
+ function DemoKitProvider({
11
+ children,
12
+ fixtures,
13
+ storageKey = "demokit-mode",
14
+ initialEnabled = false,
15
+ onDemoModeChange,
16
+ baseUrl
17
+ }) {
18
+ const [isDemoMode, setIsDemoMode] = react.useState(initialEnabled);
19
+ const [isHydrated, setIsHydrated] = react.useState(false);
20
+ const interceptorRef = react.useRef(null);
21
+ const initializedRef = react.useRef(false);
22
+ react.useEffect(() => {
23
+ if (initializedRef.current) {
24
+ return;
25
+ }
26
+ initializedRef.current = true;
27
+ interceptorRef.current = core.createDemoInterceptor({
28
+ fixtures,
29
+ storageKey,
30
+ initialEnabled,
31
+ baseUrl,
32
+ onEnable: () => {
33
+ setIsDemoMode(true);
34
+ onDemoModeChange?.(true);
35
+ },
36
+ onDisable: () => {
37
+ setIsDemoMode(false);
38
+ onDemoModeChange?.(false);
39
+ }
40
+ });
41
+ const storedState = interceptorRef.current.isEnabled();
42
+ setIsDemoMode(storedState);
43
+ setIsHydrated(true);
44
+ return () => {
45
+ interceptorRef.current?.destroy();
46
+ interceptorRef.current = null;
47
+ initializedRef.current = false;
48
+ };
49
+ }, []);
50
+ react.useEffect(() => {
51
+ if (interceptorRef.current && isHydrated) {
52
+ interceptorRef.current.setFixtures(fixtures);
53
+ }
54
+ }, [fixtures, isHydrated]);
55
+ const enable = react.useCallback(() => {
56
+ interceptorRef.current?.enable();
57
+ }, []);
58
+ const disable = react.useCallback(() => {
59
+ interceptorRef.current?.disable();
60
+ }, []);
61
+ const toggle = react.useCallback(() => {
62
+ interceptorRef.current?.toggle();
63
+ }, []);
64
+ const setDemoMode = react.useCallback((enabled) => {
65
+ if (enabled) {
66
+ interceptorRef.current?.enable();
67
+ } else {
68
+ interceptorRef.current?.disable();
69
+ }
70
+ }, []);
71
+ const resetSession = react.useCallback(() => {
72
+ interceptorRef.current?.resetSession();
73
+ }, []);
74
+ const getSession = react.useCallback(() => {
75
+ return interceptorRef.current?.getSession() ?? null;
76
+ }, []);
77
+ const value = react.useMemo(
78
+ () => ({
79
+ isDemoMode,
80
+ isHydrated,
81
+ enable,
82
+ disable,
83
+ toggle,
84
+ setDemoMode,
85
+ resetSession,
86
+ getSession
87
+ }),
88
+ [isDemoMode, isHydrated, enable, disable, toggle, setDemoMode, resetSession, getSession]
89
+ );
90
+ return /* @__PURE__ */ jsxRuntime.jsx(DemoModeContext.Provider, { value, children });
91
+ }
92
+ function useDemoMode() {
93
+ const context = react.useContext(DemoModeContext);
94
+ if (context === void 0) {
95
+ throw new Error(
96
+ "useDemoMode must be used within a DemoKitProvider. Make sure to wrap your app with <DemoKitProvider>."
97
+ );
98
+ }
99
+ return context;
100
+ }
101
+ function useIsDemoMode() {
102
+ return useDemoMode().isDemoMode;
103
+ }
104
+ function useIsHydrated() {
105
+ return useDemoMode().isHydrated;
106
+ }
107
+ function useDemoSession() {
108
+ return useDemoMode().getSession();
109
+ }
110
+ function EyeIcon() {
111
+ return /* @__PURE__ */ jsxRuntime.jsxs(
112
+ "svg",
113
+ {
114
+ width: "20",
115
+ height: "20",
116
+ viewBox: "0 0 24 24",
117
+ fill: "none",
118
+ stroke: "currentColor",
119
+ strokeWidth: "2",
120
+ strokeLinecap: "round",
121
+ strokeLinejoin: "round",
122
+ "aria-hidden": "true",
123
+ children: [
124
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }),
125
+ /* @__PURE__ */ jsxRuntime.jsx("circle", { cx: "12", cy: "12", r: "3" })
126
+ ]
127
+ }
128
+ );
129
+ }
130
+ var defaultStyles = {
131
+ container: {
132
+ display: "flex",
133
+ alignItems: "center",
134
+ justifyContent: "space-between",
135
+ padding: "8px 16px",
136
+ backgroundColor: "#fef3c7",
137
+ borderBottom: "1px solid rgba(217, 119, 6, 0.2)",
138
+ fontSize: "14px",
139
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
140
+ },
141
+ content: {
142
+ display: "flex",
143
+ alignItems: "center",
144
+ gap: "8px"
145
+ },
146
+ icon: {
147
+ color: "#d97706",
148
+ flexShrink: 0
149
+ },
150
+ label: {
151
+ fontWeight: 600,
152
+ color: "#78350f"
153
+ },
154
+ description: {
155
+ color: "rgba(120, 53, 15, 0.7)",
156
+ fontSize: "12px"
157
+ },
158
+ button: {
159
+ padding: "4px 12px",
160
+ fontSize: "14px",
161
+ backgroundColor: "transparent",
162
+ border: "1px solid rgba(217, 119, 6, 0.3)",
163
+ borderRadius: "4px",
164
+ cursor: "pointer",
165
+ color: "#78350f",
166
+ fontFamily: "inherit",
167
+ transition: "background-color 0.15s ease"
168
+ }
169
+ };
170
+ function DemoModeBanner({
171
+ className = "",
172
+ exitLabel = "Exit Demo Mode",
173
+ demoLabel = "Demo Mode Active",
174
+ description = "Changes are simulated and not saved",
175
+ showIcon = true,
176
+ style,
177
+ onExit
178
+ }) {
179
+ const { isDemoMode, isHydrated, disable } = useDemoMode();
180
+ if (!isHydrated || !isDemoMode) {
181
+ return null;
182
+ }
183
+ const handleExit = () => {
184
+ if (onExit) {
185
+ onExit();
186
+ } else {
187
+ disable();
188
+ }
189
+ };
190
+ return /* @__PURE__ */ jsxRuntime.jsxs(
191
+ "div",
192
+ {
193
+ className: `demokit-banner ${className}`.trim(),
194
+ style: { ...defaultStyles.container, ...style },
195
+ role: "status",
196
+ "aria-live": "polite",
197
+ children: [
198
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: defaultStyles.content, children: [
199
+ showIcon && /* @__PURE__ */ jsxRuntime.jsx("span", { style: defaultStyles.icon, children: /* @__PURE__ */ jsxRuntime.jsx(EyeIcon, {}) }),
200
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: defaultStyles.label, children: demoLabel }),
201
+ description && /* @__PURE__ */ jsxRuntime.jsx("span", { style: defaultStyles.description, children: description })
202
+ ] }),
203
+ /* @__PURE__ */ jsxRuntime.jsx(
204
+ "button",
205
+ {
206
+ onClick: handleExit,
207
+ style: defaultStyles.button,
208
+ onMouseOver: (e) => {
209
+ e.currentTarget.style.backgroundColor = "rgba(217, 119, 6, 0.1)";
210
+ },
211
+ onMouseOut: (e) => {
212
+ e.currentTarget.style.backgroundColor = "transparent";
213
+ },
214
+ type: "button",
215
+ children: exitLabel
216
+ }
217
+ )
218
+ ]
219
+ }
220
+ );
221
+ }
222
+
223
+ exports.DemoKitProvider = DemoKitProvider;
224
+ exports.DemoModeBanner = DemoModeBanner;
225
+ exports.DemoModeContext = DemoModeContext;
226
+ exports.useDemoMode = useDemoMode;
227
+ exports.useDemoSession = useDemoSession;
228
+ exports.useIsDemoMode = useIsDemoMode;
229
+ exports.useIsHydrated = useIsHydrated;
230
+ //# sourceMappingURL=index.cjs.map
231
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context.ts","../src/provider.tsx","../src/hooks.ts","../src/banner.tsx"],"names":["createContext","useState","useRef","useEffect","createDemoInterceptor","useCallback","useMemo","jsx","useContext","jsxs"],"mappings":"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","file":"index.cjs","sourcesContent":["import { createContext } from 'react'\nimport type { DemoModeContextValue } from './types'\n\n/**\n * React context for demo mode state\n * @internal\n */\nexport const DemoModeContext = createContext<DemoModeContextValue | undefined>(undefined)\n\nDemoModeContext.displayName = 'DemoModeContext'\n","'use client'\n\nimport { useState, useEffect, useCallback, useMemo, useRef } from 'react'\nimport { createDemoInterceptor, type DemoInterceptor, type SessionState } from '@demokit-ai/core'\nimport { DemoModeContext } from './context'\nimport type { DemoKitProviderProps, DemoModeContextValue } from './types'\n\n/**\n * Provider component that enables demo mode functionality\n *\n * Wraps your app to provide demo mode state and controls.\n * Handles SSR hydration safely and persists state to localStorage.\n *\n * @example\n * const fixtures = {\n * 'GET /api/users': () => [{ id: '1', name: 'Demo User' }],\n * 'GET /api/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * }\n *\n * function App() {\n * return (\n * <DemoKitProvider\n * fixtures={fixtures}\n * onDemoModeChange={(enabled) => console.log('Demo mode:', enabled)}\n * >\n * <YourApp />\n * </DemoKitProvider>\n * )\n * }\n */\nexport function DemoKitProvider({\n children,\n fixtures,\n storageKey = 'demokit-mode',\n initialEnabled = false,\n onDemoModeChange,\n baseUrl,\n}: DemoKitProviderProps) {\n // Start with initialEnabled for SSR to avoid hydration mismatch\n const [isDemoMode, setIsDemoMode] = useState(initialEnabled)\n const [isHydrated, setIsHydrated] = useState(false)\n\n // Keep a ref to the interceptor instance\n const interceptorRef = useRef<DemoInterceptor | null>(null)\n\n // Track if we've initialized\n const initializedRef = useRef(false)\n\n // Initialize interceptor on mount (client-side only)\n useEffect(() => {\n if (initializedRef.current) {\n return\n }\n initializedRef.current = true\n\n interceptorRef.current = createDemoInterceptor({\n fixtures,\n storageKey,\n initialEnabled,\n baseUrl,\n onEnable: () => {\n setIsDemoMode(true)\n onDemoModeChange?.(true)\n },\n onDisable: () => {\n setIsDemoMode(false)\n onDemoModeChange?.(false)\n },\n })\n\n // Sync state from storage after hydration\n const storedState = interceptorRef.current.isEnabled()\n setIsDemoMode(storedState)\n\n // Mark as hydrated\n setIsHydrated(true)\n\n return () => {\n interceptorRef.current?.destroy()\n interceptorRef.current = null\n initializedRef.current = false\n }\n }, []) // Empty deps - only run once on mount\n\n // Update fixtures if they change\n useEffect(() => {\n if (interceptorRef.current && isHydrated) {\n interceptorRef.current.setFixtures(fixtures)\n }\n }, [fixtures, isHydrated])\n\n const enable = useCallback(() => {\n interceptorRef.current?.enable()\n }, [])\n\n const disable = useCallback(() => {\n interceptorRef.current?.disable()\n }, [])\n\n const toggle = useCallback(() => {\n interceptorRef.current?.toggle()\n }, [])\n\n const setDemoMode = useCallback((enabled: boolean) => {\n if (enabled) {\n interceptorRef.current?.enable()\n } else {\n interceptorRef.current?.disable()\n }\n }, [])\n\n const resetSession = useCallback(() => {\n interceptorRef.current?.resetSession()\n }, [])\n\n const getSession = useCallback((): SessionState | null => {\n return interceptorRef.current?.getSession() ?? null\n }, [])\n\n const value = useMemo<DemoModeContextValue>(\n () => ({\n isDemoMode,\n isHydrated,\n enable,\n disable,\n toggle,\n setDemoMode,\n resetSession,\n getSession,\n }),\n [isDemoMode, isHydrated, enable, disable, toggle, setDemoMode, resetSession, getSession]\n )\n\n return (\n <DemoModeContext.Provider value={value}>\n {children}\n </DemoModeContext.Provider>\n )\n}\n","'use client'\n\nimport { useContext } from 'react'\nimport { DemoModeContext } from './context'\nimport type { DemoModeContextValue } from './types'\n\n/**\n * Hook to access demo mode state and controls\n *\n * @returns Demo mode context value with state and control methods\n * @throws Error if used outside of DemoKitProvider\n *\n * @example\n * function MyComponent() {\n * const { isDemoMode, isHydrated, toggle } = useDemoMode()\n *\n * // Wait for hydration before rendering demo-dependent UI\n * if (!isHydrated) {\n * return <Loading />\n * }\n *\n * return (\n * <div>\n * <p>Demo mode: {isDemoMode ? 'ON' : 'OFF'}</p>\n * <button onClick={toggle}>Toggle</button>\n * </div>\n * )\n * }\n */\nexport function useDemoMode(): DemoModeContextValue {\n const context = useContext(DemoModeContext)\n\n if (context === undefined) {\n throw new Error(\n 'useDemoMode must be used within a DemoKitProvider. ' +\n 'Make sure to wrap your app with <DemoKitProvider>.'\n )\n }\n\n return context\n}\n\n/**\n * Hook to check if demo mode is enabled\n * Shorthand for useDemoMode().isDemoMode\n *\n * @returns Whether demo mode is enabled\n */\nexport function useIsDemoMode(): boolean {\n return useDemoMode().isDemoMode\n}\n\n/**\n * Hook to check if the component has hydrated\n * Shorthand for useDemoMode().isHydrated\n *\n * @returns Whether the component has hydrated\n */\nexport function useIsHydrated(): boolean {\n return useDemoMode().isHydrated\n}\n\n/**\n * Hook to access the session state\n * Shorthand for useDemoMode().getSession()\n *\n * @returns The session state, or null if not yet initialized\n *\n * @example\n * function MyComponent() {\n * const session = useDemoSession()\n *\n * const cart = session?.get<CartItem[]>('cart') || []\n * const addToCart = (item: CartItem) => {\n * session?.set('cart', [...cart, item])\n * }\n *\n * return <CartView items={cart} onAdd={addToCart} />\n * }\n */\nexport function useDemoSession() {\n return useDemoMode().getSession()\n}\n","'use client'\n\nimport { useDemoMode } from './hooks'\nimport type { DemoModeBannerProps } from './types'\n\n/**\n * Eye icon SVG component\n */\nfunction EyeIcon() {\n return (\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\" />\n <circle cx=\"12\" cy=\"12\" r=\"3\" />\n </svg>\n )\n}\n\n/**\n * Default styles for the banner\n */\nconst defaultStyles = {\n container: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '8px 16px',\n backgroundColor: '#fef3c7',\n borderBottom: '1px solid rgba(217, 119, 6, 0.2)',\n fontSize: '14px',\n fontFamily:\n '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif',\n } as React.CSSProperties,\n content: {\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n } as React.CSSProperties,\n icon: {\n color: '#d97706',\n flexShrink: 0,\n } as React.CSSProperties,\n label: {\n fontWeight: 600,\n color: '#78350f',\n } as React.CSSProperties,\n description: {\n color: 'rgba(120, 53, 15, 0.7)',\n fontSize: '12px',\n } as React.CSSProperties,\n button: {\n padding: '4px 12px',\n fontSize: '14px',\n backgroundColor: 'transparent',\n border: '1px solid rgba(217, 119, 6, 0.3)',\n borderRadius: '4px',\n cursor: 'pointer',\n color: '#78350f',\n fontFamily: 'inherit',\n transition: 'background-color 0.15s ease',\n } as React.CSSProperties,\n}\n\n/**\n * A ready-to-use banner component that shows when demo mode is active\n *\n * Displays a prominent amber banner with a label, description, and exit button.\n * Automatically hides when demo mode is disabled or before hydration.\n *\n * @example\n * function App() {\n * return (\n * <DemoKitProvider fixtures={fixtures}>\n * <DemoModeBanner />\n * <YourApp />\n * </DemoKitProvider>\n * )\n * }\n *\n * @example Custom labels\n * <DemoModeBanner\n * demoLabel=\"Preview Mode\"\n * description=\"You're viewing sample data\"\n * exitLabel=\"Exit Preview\"\n * />\n */\nexport function DemoModeBanner({\n className = '',\n exitLabel = 'Exit Demo Mode',\n demoLabel = 'Demo Mode Active',\n description = 'Changes are simulated and not saved',\n showIcon = true,\n style,\n onExit,\n}: DemoModeBannerProps) {\n const { isDemoMode, isHydrated, disable } = useDemoMode()\n\n // Don't render until hydrated to avoid hydration mismatch\n if (!isHydrated || !isDemoMode) {\n return null\n }\n\n const handleExit = () => {\n if (onExit) {\n onExit()\n } else {\n disable()\n }\n }\n\n return (\n <div\n className={`demokit-banner ${className}`.trim()}\n style={{ ...defaultStyles.container, ...style }}\n role=\"status\"\n aria-live=\"polite\"\n >\n <div style={defaultStyles.content}>\n {showIcon && (\n <span style={defaultStyles.icon}>\n <EyeIcon />\n </span>\n )}\n <span style={defaultStyles.label}>{demoLabel}</span>\n {description && <span style={defaultStyles.description}>{description}</span>}\n </div>\n <button\n onClick={handleExit}\n style={defaultStyles.button}\n onMouseOver={(e) => {\n e.currentTarget.style.backgroundColor = 'rgba(217, 119, 6, 0.1)'\n }}\n onMouseOut={(e) => {\n e.currentTarget.style.backgroundColor = 'transparent'\n }}\n type=\"button\"\n >\n {exitLabel}\n </button>\n </div>\n )\n}\n"]}
@@ -0,0 +1,235 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as _demokit_ai_core from '@demokit-ai/core';
3
+ import { FixtureMap, SessionState } from '@demokit-ai/core';
4
+ export { FixtureHandler, FixtureMap, RequestContext, SessionState } from '@demokit-ai/core';
5
+ import * as react from 'react';
6
+ import { ReactNode } from 'react';
7
+
8
+ /**
9
+ * Props for the DemoKitProvider component
10
+ */
11
+ interface DemoKitProviderProps {
12
+ /**
13
+ * Child components to render
14
+ */
15
+ children: ReactNode;
16
+ /**
17
+ * Map of URL patterns to fixture handlers
18
+ */
19
+ fixtures: FixtureMap;
20
+ /**
21
+ * localStorage key for persisting demo mode state
22
+ * @default 'demokit-mode'
23
+ */
24
+ storageKey?: string;
25
+ /**
26
+ * Whether demo mode should be initially enabled
27
+ * If not provided, will read from localStorage
28
+ * @default false
29
+ */
30
+ initialEnabled?: boolean;
31
+ /**
32
+ * Callback invoked when demo mode state changes
33
+ */
34
+ onDemoModeChange?: (enabled: boolean) => void;
35
+ /**
36
+ * Base URL to use for relative URL parsing
37
+ * @default 'http://localhost'
38
+ */
39
+ baseUrl?: string;
40
+ }
41
+ /**
42
+ * Value provided by the DemoMode context
43
+ */
44
+ interface DemoModeContextValue {
45
+ /**
46
+ * Whether demo mode is currently enabled
47
+ */
48
+ isDemoMode: boolean;
49
+ /**
50
+ * Whether the component has hydrated (for SSR safety)
51
+ * Always check this before rendering demo-dependent UI
52
+ */
53
+ isHydrated: boolean;
54
+ /**
55
+ * Enable demo mode
56
+ */
57
+ enable(): void;
58
+ /**
59
+ * Disable demo mode
60
+ */
61
+ disable(): void;
62
+ /**
63
+ * Toggle demo mode and return the new state
64
+ */
65
+ toggle(): void;
66
+ /**
67
+ * Set demo mode to a specific state
68
+ */
69
+ setDemoMode(enabled: boolean): void;
70
+ /**
71
+ * Reset the session state, clearing all stored data
72
+ * Call this to manually reset the demo session without page refresh
73
+ */
74
+ resetSession(): void;
75
+ /**
76
+ * Get the current session state instance
77
+ * Useful for inspecting or manipulating session state directly
78
+ * Returns null if the interceptor hasn't been initialized yet
79
+ */
80
+ getSession(): SessionState | null;
81
+ }
82
+ /**
83
+ * Props for the DemoModeBanner component
84
+ */
85
+ interface DemoModeBannerProps {
86
+ /**
87
+ * Additional CSS class name
88
+ */
89
+ className?: string;
90
+ /**
91
+ * Label for the exit button
92
+ * @default 'Exit Demo Mode'
93
+ */
94
+ exitLabel?: string;
95
+ /**
96
+ * Label shown when demo mode is active
97
+ * @default 'Demo Mode Active'
98
+ */
99
+ demoLabel?: string;
100
+ /**
101
+ * Description shown in the banner
102
+ * @default 'Changes are simulated and not saved'
103
+ */
104
+ description?: string;
105
+ /**
106
+ * Whether to show the eye icon
107
+ * @default true
108
+ */
109
+ showIcon?: boolean;
110
+ /**
111
+ * Custom styles for the banner container
112
+ */
113
+ style?: React.CSSProperties;
114
+ /**
115
+ * Callback when exit button is clicked
116
+ * If not provided, will call disable() from context
117
+ */
118
+ onExit?: () => void;
119
+ }
120
+
121
+ /**
122
+ * Provider component that enables demo mode functionality
123
+ *
124
+ * Wraps your app to provide demo mode state and controls.
125
+ * Handles SSR hydration safely and persists state to localStorage.
126
+ *
127
+ * @example
128
+ * const fixtures = {
129
+ * 'GET /api/users': () => [{ id: '1', name: 'Demo User' }],
130
+ * 'GET /api/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),
131
+ * }
132
+ *
133
+ * function App() {
134
+ * return (
135
+ * <DemoKitProvider
136
+ * fixtures={fixtures}
137
+ * onDemoModeChange={(enabled) => console.log('Demo mode:', enabled)}
138
+ * >
139
+ * <YourApp />
140
+ * </DemoKitProvider>
141
+ * )
142
+ * }
143
+ */
144
+ declare function DemoKitProvider({ children, fixtures, storageKey, initialEnabled, onDemoModeChange, baseUrl, }: DemoKitProviderProps): react_jsx_runtime.JSX.Element;
145
+
146
+ /**
147
+ * Hook to access demo mode state and controls
148
+ *
149
+ * @returns Demo mode context value with state and control methods
150
+ * @throws Error if used outside of DemoKitProvider
151
+ *
152
+ * @example
153
+ * function MyComponent() {
154
+ * const { isDemoMode, isHydrated, toggle } = useDemoMode()
155
+ *
156
+ * // Wait for hydration before rendering demo-dependent UI
157
+ * if (!isHydrated) {
158
+ * return <Loading />
159
+ * }
160
+ *
161
+ * return (
162
+ * <div>
163
+ * <p>Demo mode: {isDemoMode ? 'ON' : 'OFF'}</p>
164
+ * <button onClick={toggle}>Toggle</button>
165
+ * </div>
166
+ * )
167
+ * }
168
+ */
169
+ declare function useDemoMode(): DemoModeContextValue;
170
+ /**
171
+ * Hook to check if demo mode is enabled
172
+ * Shorthand for useDemoMode().isDemoMode
173
+ *
174
+ * @returns Whether demo mode is enabled
175
+ */
176
+ declare function useIsDemoMode(): boolean;
177
+ /**
178
+ * Hook to check if the component has hydrated
179
+ * Shorthand for useDemoMode().isHydrated
180
+ *
181
+ * @returns Whether the component has hydrated
182
+ */
183
+ declare function useIsHydrated(): boolean;
184
+ /**
185
+ * Hook to access the session state
186
+ * Shorthand for useDemoMode().getSession()
187
+ *
188
+ * @returns The session state, or null if not yet initialized
189
+ *
190
+ * @example
191
+ * function MyComponent() {
192
+ * const session = useDemoSession()
193
+ *
194
+ * const cart = session?.get<CartItem[]>('cart') || []
195
+ * const addToCart = (item: CartItem) => {
196
+ * session?.set('cart', [...cart, item])
197
+ * }
198
+ *
199
+ * return <CartView items={cart} onAdd={addToCart} />
200
+ * }
201
+ */
202
+ declare function useDemoSession(): _demokit_ai_core.SessionState | null;
203
+
204
+ /**
205
+ * A ready-to-use banner component that shows when demo mode is active
206
+ *
207
+ * Displays a prominent amber banner with a label, description, and exit button.
208
+ * Automatically hides when demo mode is disabled or before hydration.
209
+ *
210
+ * @example
211
+ * function App() {
212
+ * return (
213
+ * <DemoKitProvider fixtures={fixtures}>
214
+ * <DemoModeBanner />
215
+ * <YourApp />
216
+ * </DemoKitProvider>
217
+ * )
218
+ * }
219
+ *
220
+ * @example Custom labels
221
+ * <DemoModeBanner
222
+ * demoLabel="Preview Mode"
223
+ * description="You're viewing sample data"
224
+ * exitLabel="Exit Preview"
225
+ * />
226
+ */
227
+ declare function DemoModeBanner({ className, exitLabel, demoLabel, description, showIcon, style, onExit, }: DemoModeBannerProps): react_jsx_runtime.JSX.Element | null;
228
+
229
+ /**
230
+ * React context for demo mode state
231
+ * @internal
232
+ */
233
+ declare const DemoModeContext: react.Context<DemoModeContextValue | undefined>;
234
+
235
+ export { DemoKitProvider, type DemoKitProviderProps, DemoModeBanner, type DemoModeBannerProps, DemoModeContext, type DemoModeContextValue, useDemoMode, useDemoSession, useIsDemoMode, useIsHydrated };
@@ -0,0 +1,235 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import * as _demokit_ai_core from '@demokit-ai/core';
3
+ import { FixtureMap, SessionState } from '@demokit-ai/core';
4
+ export { FixtureHandler, FixtureMap, RequestContext, SessionState } from '@demokit-ai/core';
5
+ import * as react from 'react';
6
+ import { ReactNode } from 'react';
7
+
8
+ /**
9
+ * Props for the DemoKitProvider component
10
+ */
11
+ interface DemoKitProviderProps {
12
+ /**
13
+ * Child components to render
14
+ */
15
+ children: ReactNode;
16
+ /**
17
+ * Map of URL patterns to fixture handlers
18
+ */
19
+ fixtures: FixtureMap;
20
+ /**
21
+ * localStorage key for persisting demo mode state
22
+ * @default 'demokit-mode'
23
+ */
24
+ storageKey?: string;
25
+ /**
26
+ * Whether demo mode should be initially enabled
27
+ * If not provided, will read from localStorage
28
+ * @default false
29
+ */
30
+ initialEnabled?: boolean;
31
+ /**
32
+ * Callback invoked when demo mode state changes
33
+ */
34
+ onDemoModeChange?: (enabled: boolean) => void;
35
+ /**
36
+ * Base URL to use for relative URL parsing
37
+ * @default 'http://localhost'
38
+ */
39
+ baseUrl?: string;
40
+ }
41
+ /**
42
+ * Value provided by the DemoMode context
43
+ */
44
+ interface DemoModeContextValue {
45
+ /**
46
+ * Whether demo mode is currently enabled
47
+ */
48
+ isDemoMode: boolean;
49
+ /**
50
+ * Whether the component has hydrated (for SSR safety)
51
+ * Always check this before rendering demo-dependent UI
52
+ */
53
+ isHydrated: boolean;
54
+ /**
55
+ * Enable demo mode
56
+ */
57
+ enable(): void;
58
+ /**
59
+ * Disable demo mode
60
+ */
61
+ disable(): void;
62
+ /**
63
+ * Toggle demo mode and return the new state
64
+ */
65
+ toggle(): void;
66
+ /**
67
+ * Set demo mode to a specific state
68
+ */
69
+ setDemoMode(enabled: boolean): void;
70
+ /**
71
+ * Reset the session state, clearing all stored data
72
+ * Call this to manually reset the demo session without page refresh
73
+ */
74
+ resetSession(): void;
75
+ /**
76
+ * Get the current session state instance
77
+ * Useful for inspecting or manipulating session state directly
78
+ * Returns null if the interceptor hasn't been initialized yet
79
+ */
80
+ getSession(): SessionState | null;
81
+ }
82
+ /**
83
+ * Props for the DemoModeBanner component
84
+ */
85
+ interface DemoModeBannerProps {
86
+ /**
87
+ * Additional CSS class name
88
+ */
89
+ className?: string;
90
+ /**
91
+ * Label for the exit button
92
+ * @default 'Exit Demo Mode'
93
+ */
94
+ exitLabel?: string;
95
+ /**
96
+ * Label shown when demo mode is active
97
+ * @default 'Demo Mode Active'
98
+ */
99
+ demoLabel?: string;
100
+ /**
101
+ * Description shown in the banner
102
+ * @default 'Changes are simulated and not saved'
103
+ */
104
+ description?: string;
105
+ /**
106
+ * Whether to show the eye icon
107
+ * @default true
108
+ */
109
+ showIcon?: boolean;
110
+ /**
111
+ * Custom styles for the banner container
112
+ */
113
+ style?: React.CSSProperties;
114
+ /**
115
+ * Callback when exit button is clicked
116
+ * If not provided, will call disable() from context
117
+ */
118
+ onExit?: () => void;
119
+ }
120
+
121
+ /**
122
+ * Provider component that enables demo mode functionality
123
+ *
124
+ * Wraps your app to provide demo mode state and controls.
125
+ * Handles SSR hydration safely and persists state to localStorage.
126
+ *
127
+ * @example
128
+ * const fixtures = {
129
+ * 'GET /api/users': () => [{ id: '1', name: 'Demo User' }],
130
+ * 'GET /api/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),
131
+ * }
132
+ *
133
+ * function App() {
134
+ * return (
135
+ * <DemoKitProvider
136
+ * fixtures={fixtures}
137
+ * onDemoModeChange={(enabled) => console.log('Demo mode:', enabled)}
138
+ * >
139
+ * <YourApp />
140
+ * </DemoKitProvider>
141
+ * )
142
+ * }
143
+ */
144
+ declare function DemoKitProvider({ children, fixtures, storageKey, initialEnabled, onDemoModeChange, baseUrl, }: DemoKitProviderProps): react_jsx_runtime.JSX.Element;
145
+
146
+ /**
147
+ * Hook to access demo mode state and controls
148
+ *
149
+ * @returns Demo mode context value with state and control methods
150
+ * @throws Error if used outside of DemoKitProvider
151
+ *
152
+ * @example
153
+ * function MyComponent() {
154
+ * const { isDemoMode, isHydrated, toggle } = useDemoMode()
155
+ *
156
+ * // Wait for hydration before rendering demo-dependent UI
157
+ * if (!isHydrated) {
158
+ * return <Loading />
159
+ * }
160
+ *
161
+ * return (
162
+ * <div>
163
+ * <p>Demo mode: {isDemoMode ? 'ON' : 'OFF'}</p>
164
+ * <button onClick={toggle}>Toggle</button>
165
+ * </div>
166
+ * )
167
+ * }
168
+ */
169
+ declare function useDemoMode(): DemoModeContextValue;
170
+ /**
171
+ * Hook to check if demo mode is enabled
172
+ * Shorthand for useDemoMode().isDemoMode
173
+ *
174
+ * @returns Whether demo mode is enabled
175
+ */
176
+ declare function useIsDemoMode(): boolean;
177
+ /**
178
+ * Hook to check if the component has hydrated
179
+ * Shorthand for useDemoMode().isHydrated
180
+ *
181
+ * @returns Whether the component has hydrated
182
+ */
183
+ declare function useIsHydrated(): boolean;
184
+ /**
185
+ * Hook to access the session state
186
+ * Shorthand for useDemoMode().getSession()
187
+ *
188
+ * @returns The session state, or null if not yet initialized
189
+ *
190
+ * @example
191
+ * function MyComponent() {
192
+ * const session = useDemoSession()
193
+ *
194
+ * const cart = session?.get<CartItem[]>('cart') || []
195
+ * const addToCart = (item: CartItem) => {
196
+ * session?.set('cart', [...cart, item])
197
+ * }
198
+ *
199
+ * return <CartView items={cart} onAdd={addToCart} />
200
+ * }
201
+ */
202
+ declare function useDemoSession(): _demokit_ai_core.SessionState | null;
203
+
204
+ /**
205
+ * A ready-to-use banner component that shows when demo mode is active
206
+ *
207
+ * Displays a prominent amber banner with a label, description, and exit button.
208
+ * Automatically hides when demo mode is disabled or before hydration.
209
+ *
210
+ * @example
211
+ * function App() {
212
+ * return (
213
+ * <DemoKitProvider fixtures={fixtures}>
214
+ * <DemoModeBanner />
215
+ * <YourApp />
216
+ * </DemoKitProvider>
217
+ * )
218
+ * }
219
+ *
220
+ * @example Custom labels
221
+ * <DemoModeBanner
222
+ * demoLabel="Preview Mode"
223
+ * description="You're viewing sample data"
224
+ * exitLabel="Exit Preview"
225
+ * />
226
+ */
227
+ declare function DemoModeBanner({ className, exitLabel, demoLabel, description, showIcon, style, onExit, }: DemoModeBannerProps): react_jsx_runtime.JSX.Element | null;
228
+
229
+ /**
230
+ * React context for demo mode state
231
+ * @internal
232
+ */
233
+ declare const DemoModeContext: react.Context<DemoModeContextValue | undefined>;
234
+
235
+ export { DemoKitProvider, type DemoKitProviderProps, DemoModeBanner, type DemoModeBannerProps, DemoModeContext, type DemoModeContextValue, useDemoMode, useDemoSession, useIsDemoMode, useIsHydrated };
package/dist/index.js ADDED
@@ -0,0 +1,223 @@
1
+ import { createContext, useState, useRef, useEffect, useCallback, useMemo, useContext } from 'react';
2
+ import { createDemoInterceptor } from '@demokit-ai/core';
3
+ import { jsx, jsxs } from 'react/jsx-runtime';
4
+
5
+ // src/provider.tsx
6
+ var DemoModeContext = createContext(void 0);
7
+ DemoModeContext.displayName = "DemoModeContext";
8
+ function DemoKitProvider({
9
+ children,
10
+ fixtures,
11
+ storageKey = "demokit-mode",
12
+ initialEnabled = false,
13
+ onDemoModeChange,
14
+ baseUrl
15
+ }) {
16
+ const [isDemoMode, setIsDemoMode] = useState(initialEnabled);
17
+ const [isHydrated, setIsHydrated] = useState(false);
18
+ const interceptorRef = useRef(null);
19
+ const initializedRef = useRef(false);
20
+ useEffect(() => {
21
+ if (initializedRef.current) {
22
+ return;
23
+ }
24
+ initializedRef.current = true;
25
+ interceptorRef.current = createDemoInterceptor({
26
+ fixtures,
27
+ storageKey,
28
+ initialEnabled,
29
+ baseUrl,
30
+ onEnable: () => {
31
+ setIsDemoMode(true);
32
+ onDemoModeChange?.(true);
33
+ },
34
+ onDisable: () => {
35
+ setIsDemoMode(false);
36
+ onDemoModeChange?.(false);
37
+ }
38
+ });
39
+ const storedState = interceptorRef.current.isEnabled();
40
+ setIsDemoMode(storedState);
41
+ setIsHydrated(true);
42
+ return () => {
43
+ interceptorRef.current?.destroy();
44
+ interceptorRef.current = null;
45
+ initializedRef.current = false;
46
+ };
47
+ }, []);
48
+ useEffect(() => {
49
+ if (interceptorRef.current && isHydrated) {
50
+ interceptorRef.current.setFixtures(fixtures);
51
+ }
52
+ }, [fixtures, isHydrated]);
53
+ const enable = useCallback(() => {
54
+ interceptorRef.current?.enable();
55
+ }, []);
56
+ const disable = useCallback(() => {
57
+ interceptorRef.current?.disable();
58
+ }, []);
59
+ const toggle = useCallback(() => {
60
+ interceptorRef.current?.toggle();
61
+ }, []);
62
+ const setDemoMode = useCallback((enabled) => {
63
+ if (enabled) {
64
+ interceptorRef.current?.enable();
65
+ } else {
66
+ interceptorRef.current?.disable();
67
+ }
68
+ }, []);
69
+ const resetSession = useCallback(() => {
70
+ interceptorRef.current?.resetSession();
71
+ }, []);
72
+ const getSession = useCallback(() => {
73
+ return interceptorRef.current?.getSession() ?? null;
74
+ }, []);
75
+ const value = useMemo(
76
+ () => ({
77
+ isDemoMode,
78
+ isHydrated,
79
+ enable,
80
+ disable,
81
+ toggle,
82
+ setDemoMode,
83
+ resetSession,
84
+ getSession
85
+ }),
86
+ [isDemoMode, isHydrated, enable, disable, toggle, setDemoMode, resetSession, getSession]
87
+ );
88
+ return /* @__PURE__ */ jsx(DemoModeContext.Provider, { value, children });
89
+ }
90
+ function useDemoMode() {
91
+ const context = useContext(DemoModeContext);
92
+ if (context === void 0) {
93
+ throw new Error(
94
+ "useDemoMode must be used within a DemoKitProvider. Make sure to wrap your app with <DemoKitProvider>."
95
+ );
96
+ }
97
+ return context;
98
+ }
99
+ function useIsDemoMode() {
100
+ return useDemoMode().isDemoMode;
101
+ }
102
+ function useIsHydrated() {
103
+ return useDemoMode().isHydrated;
104
+ }
105
+ function useDemoSession() {
106
+ return useDemoMode().getSession();
107
+ }
108
+ function EyeIcon() {
109
+ return /* @__PURE__ */ jsxs(
110
+ "svg",
111
+ {
112
+ width: "20",
113
+ height: "20",
114
+ viewBox: "0 0 24 24",
115
+ fill: "none",
116
+ stroke: "currentColor",
117
+ strokeWidth: "2",
118
+ strokeLinecap: "round",
119
+ strokeLinejoin: "round",
120
+ "aria-hidden": "true",
121
+ children: [
122
+ /* @__PURE__ */ jsx("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }),
123
+ /* @__PURE__ */ jsx("circle", { cx: "12", cy: "12", r: "3" })
124
+ ]
125
+ }
126
+ );
127
+ }
128
+ var defaultStyles = {
129
+ container: {
130
+ display: "flex",
131
+ alignItems: "center",
132
+ justifyContent: "space-between",
133
+ padding: "8px 16px",
134
+ backgroundColor: "#fef3c7",
135
+ borderBottom: "1px solid rgba(217, 119, 6, 0.2)",
136
+ fontSize: "14px",
137
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
138
+ },
139
+ content: {
140
+ display: "flex",
141
+ alignItems: "center",
142
+ gap: "8px"
143
+ },
144
+ icon: {
145
+ color: "#d97706",
146
+ flexShrink: 0
147
+ },
148
+ label: {
149
+ fontWeight: 600,
150
+ color: "#78350f"
151
+ },
152
+ description: {
153
+ color: "rgba(120, 53, 15, 0.7)",
154
+ fontSize: "12px"
155
+ },
156
+ button: {
157
+ padding: "4px 12px",
158
+ fontSize: "14px",
159
+ backgroundColor: "transparent",
160
+ border: "1px solid rgba(217, 119, 6, 0.3)",
161
+ borderRadius: "4px",
162
+ cursor: "pointer",
163
+ color: "#78350f",
164
+ fontFamily: "inherit",
165
+ transition: "background-color 0.15s ease"
166
+ }
167
+ };
168
+ function DemoModeBanner({
169
+ className = "",
170
+ exitLabel = "Exit Demo Mode",
171
+ demoLabel = "Demo Mode Active",
172
+ description = "Changes are simulated and not saved",
173
+ showIcon = true,
174
+ style,
175
+ onExit
176
+ }) {
177
+ const { isDemoMode, isHydrated, disable } = useDemoMode();
178
+ if (!isHydrated || !isDemoMode) {
179
+ return null;
180
+ }
181
+ const handleExit = () => {
182
+ if (onExit) {
183
+ onExit();
184
+ } else {
185
+ disable();
186
+ }
187
+ };
188
+ return /* @__PURE__ */ jsxs(
189
+ "div",
190
+ {
191
+ className: `demokit-banner ${className}`.trim(),
192
+ style: { ...defaultStyles.container, ...style },
193
+ role: "status",
194
+ "aria-live": "polite",
195
+ children: [
196
+ /* @__PURE__ */ jsxs("div", { style: defaultStyles.content, children: [
197
+ showIcon && /* @__PURE__ */ jsx("span", { style: defaultStyles.icon, children: /* @__PURE__ */ jsx(EyeIcon, {}) }),
198
+ /* @__PURE__ */ jsx("span", { style: defaultStyles.label, children: demoLabel }),
199
+ description && /* @__PURE__ */ jsx("span", { style: defaultStyles.description, children: description })
200
+ ] }),
201
+ /* @__PURE__ */ jsx(
202
+ "button",
203
+ {
204
+ onClick: handleExit,
205
+ style: defaultStyles.button,
206
+ onMouseOver: (e) => {
207
+ e.currentTarget.style.backgroundColor = "rgba(217, 119, 6, 0.1)";
208
+ },
209
+ onMouseOut: (e) => {
210
+ e.currentTarget.style.backgroundColor = "transparent";
211
+ },
212
+ type: "button",
213
+ children: exitLabel
214
+ }
215
+ )
216
+ ]
217
+ }
218
+ );
219
+ }
220
+
221
+ export { DemoKitProvider, DemoModeBanner, DemoModeContext, useDemoMode, useDemoSession, useIsDemoMode, useIsHydrated };
222
+ //# sourceMappingURL=index.js.map
223
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/context.ts","../src/provider.tsx","../src/hooks.ts","../src/banner.tsx"],"names":["jsx"],"mappings":"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","file":"index.js","sourcesContent":["import { createContext } from 'react'\nimport type { DemoModeContextValue } from './types'\n\n/**\n * React context for demo mode state\n * @internal\n */\nexport const DemoModeContext = createContext<DemoModeContextValue | undefined>(undefined)\n\nDemoModeContext.displayName = 'DemoModeContext'\n","'use client'\n\nimport { useState, useEffect, useCallback, useMemo, useRef } from 'react'\nimport { createDemoInterceptor, type DemoInterceptor, type SessionState } from '@demokit-ai/core'\nimport { DemoModeContext } from './context'\nimport type { DemoKitProviderProps, DemoModeContextValue } from './types'\n\n/**\n * Provider component that enables demo mode functionality\n *\n * Wraps your app to provide demo mode state and controls.\n * Handles SSR hydration safely and persists state to localStorage.\n *\n * @example\n * const fixtures = {\n * 'GET /api/users': () => [{ id: '1', name: 'Demo User' }],\n * 'GET /api/users/:id': ({ params }) => ({ id: params.id, name: 'Demo User' }),\n * }\n *\n * function App() {\n * return (\n * <DemoKitProvider\n * fixtures={fixtures}\n * onDemoModeChange={(enabled) => console.log('Demo mode:', enabled)}\n * >\n * <YourApp />\n * </DemoKitProvider>\n * )\n * }\n */\nexport function DemoKitProvider({\n children,\n fixtures,\n storageKey = 'demokit-mode',\n initialEnabled = false,\n onDemoModeChange,\n baseUrl,\n}: DemoKitProviderProps) {\n // Start with initialEnabled for SSR to avoid hydration mismatch\n const [isDemoMode, setIsDemoMode] = useState(initialEnabled)\n const [isHydrated, setIsHydrated] = useState(false)\n\n // Keep a ref to the interceptor instance\n const interceptorRef = useRef<DemoInterceptor | null>(null)\n\n // Track if we've initialized\n const initializedRef = useRef(false)\n\n // Initialize interceptor on mount (client-side only)\n useEffect(() => {\n if (initializedRef.current) {\n return\n }\n initializedRef.current = true\n\n interceptorRef.current = createDemoInterceptor({\n fixtures,\n storageKey,\n initialEnabled,\n baseUrl,\n onEnable: () => {\n setIsDemoMode(true)\n onDemoModeChange?.(true)\n },\n onDisable: () => {\n setIsDemoMode(false)\n onDemoModeChange?.(false)\n },\n })\n\n // Sync state from storage after hydration\n const storedState = interceptorRef.current.isEnabled()\n setIsDemoMode(storedState)\n\n // Mark as hydrated\n setIsHydrated(true)\n\n return () => {\n interceptorRef.current?.destroy()\n interceptorRef.current = null\n initializedRef.current = false\n }\n }, []) // Empty deps - only run once on mount\n\n // Update fixtures if they change\n useEffect(() => {\n if (interceptorRef.current && isHydrated) {\n interceptorRef.current.setFixtures(fixtures)\n }\n }, [fixtures, isHydrated])\n\n const enable = useCallback(() => {\n interceptorRef.current?.enable()\n }, [])\n\n const disable = useCallback(() => {\n interceptorRef.current?.disable()\n }, [])\n\n const toggle = useCallback(() => {\n interceptorRef.current?.toggle()\n }, [])\n\n const setDemoMode = useCallback((enabled: boolean) => {\n if (enabled) {\n interceptorRef.current?.enable()\n } else {\n interceptorRef.current?.disable()\n }\n }, [])\n\n const resetSession = useCallback(() => {\n interceptorRef.current?.resetSession()\n }, [])\n\n const getSession = useCallback((): SessionState | null => {\n return interceptorRef.current?.getSession() ?? null\n }, [])\n\n const value = useMemo<DemoModeContextValue>(\n () => ({\n isDemoMode,\n isHydrated,\n enable,\n disable,\n toggle,\n setDemoMode,\n resetSession,\n getSession,\n }),\n [isDemoMode, isHydrated, enable, disable, toggle, setDemoMode, resetSession, getSession]\n )\n\n return (\n <DemoModeContext.Provider value={value}>\n {children}\n </DemoModeContext.Provider>\n )\n}\n","'use client'\n\nimport { useContext } from 'react'\nimport { DemoModeContext } from './context'\nimport type { DemoModeContextValue } from './types'\n\n/**\n * Hook to access demo mode state and controls\n *\n * @returns Demo mode context value with state and control methods\n * @throws Error if used outside of DemoKitProvider\n *\n * @example\n * function MyComponent() {\n * const { isDemoMode, isHydrated, toggle } = useDemoMode()\n *\n * // Wait for hydration before rendering demo-dependent UI\n * if (!isHydrated) {\n * return <Loading />\n * }\n *\n * return (\n * <div>\n * <p>Demo mode: {isDemoMode ? 'ON' : 'OFF'}</p>\n * <button onClick={toggle}>Toggle</button>\n * </div>\n * )\n * }\n */\nexport function useDemoMode(): DemoModeContextValue {\n const context = useContext(DemoModeContext)\n\n if (context === undefined) {\n throw new Error(\n 'useDemoMode must be used within a DemoKitProvider. ' +\n 'Make sure to wrap your app with <DemoKitProvider>.'\n )\n }\n\n return context\n}\n\n/**\n * Hook to check if demo mode is enabled\n * Shorthand for useDemoMode().isDemoMode\n *\n * @returns Whether demo mode is enabled\n */\nexport function useIsDemoMode(): boolean {\n return useDemoMode().isDemoMode\n}\n\n/**\n * Hook to check if the component has hydrated\n * Shorthand for useDemoMode().isHydrated\n *\n * @returns Whether the component has hydrated\n */\nexport function useIsHydrated(): boolean {\n return useDemoMode().isHydrated\n}\n\n/**\n * Hook to access the session state\n * Shorthand for useDemoMode().getSession()\n *\n * @returns The session state, or null if not yet initialized\n *\n * @example\n * function MyComponent() {\n * const session = useDemoSession()\n *\n * const cart = session?.get<CartItem[]>('cart') || []\n * const addToCart = (item: CartItem) => {\n * session?.set('cart', [...cart, item])\n * }\n *\n * return <CartView items={cart} onAdd={addToCart} />\n * }\n */\nexport function useDemoSession() {\n return useDemoMode().getSession()\n}\n","'use client'\n\nimport { useDemoMode } from './hooks'\nimport type { DemoModeBannerProps } from './types'\n\n/**\n * Eye icon SVG component\n */\nfunction EyeIcon() {\n return (\n <svg\n width=\"20\"\n height=\"20\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\" />\n <circle cx=\"12\" cy=\"12\" r=\"3\" />\n </svg>\n )\n}\n\n/**\n * Default styles for the banner\n */\nconst defaultStyles = {\n container: {\n display: 'flex',\n alignItems: 'center',\n justifyContent: 'space-between',\n padding: '8px 16px',\n backgroundColor: '#fef3c7',\n borderBottom: '1px solid rgba(217, 119, 6, 0.2)',\n fontSize: '14px',\n fontFamily:\n '-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif',\n } as React.CSSProperties,\n content: {\n display: 'flex',\n alignItems: 'center',\n gap: '8px',\n } as React.CSSProperties,\n icon: {\n color: '#d97706',\n flexShrink: 0,\n } as React.CSSProperties,\n label: {\n fontWeight: 600,\n color: '#78350f',\n } as React.CSSProperties,\n description: {\n color: 'rgba(120, 53, 15, 0.7)',\n fontSize: '12px',\n } as React.CSSProperties,\n button: {\n padding: '4px 12px',\n fontSize: '14px',\n backgroundColor: 'transparent',\n border: '1px solid rgba(217, 119, 6, 0.3)',\n borderRadius: '4px',\n cursor: 'pointer',\n color: '#78350f',\n fontFamily: 'inherit',\n transition: 'background-color 0.15s ease',\n } as React.CSSProperties,\n}\n\n/**\n * A ready-to-use banner component that shows when demo mode is active\n *\n * Displays a prominent amber banner with a label, description, and exit button.\n * Automatically hides when demo mode is disabled or before hydration.\n *\n * @example\n * function App() {\n * return (\n * <DemoKitProvider fixtures={fixtures}>\n * <DemoModeBanner />\n * <YourApp />\n * </DemoKitProvider>\n * )\n * }\n *\n * @example Custom labels\n * <DemoModeBanner\n * demoLabel=\"Preview Mode\"\n * description=\"You're viewing sample data\"\n * exitLabel=\"Exit Preview\"\n * />\n */\nexport function DemoModeBanner({\n className = '',\n exitLabel = 'Exit Demo Mode',\n demoLabel = 'Demo Mode Active',\n description = 'Changes are simulated and not saved',\n showIcon = true,\n style,\n onExit,\n}: DemoModeBannerProps) {\n const { isDemoMode, isHydrated, disable } = useDemoMode()\n\n // Don't render until hydrated to avoid hydration mismatch\n if (!isHydrated || !isDemoMode) {\n return null\n }\n\n const handleExit = () => {\n if (onExit) {\n onExit()\n } else {\n disable()\n }\n }\n\n return (\n <div\n className={`demokit-banner ${className}`.trim()}\n style={{ ...defaultStyles.container, ...style }}\n role=\"status\"\n aria-live=\"polite\"\n >\n <div style={defaultStyles.content}>\n {showIcon && (\n <span style={defaultStyles.icon}>\n <EyeIcon />\n </span>\n )}\n <span style={defaultStyles.label}>{demoLabel}</span>\n {description && <span style={defaultStyles.description}>{description}</span>}\n </div>\n <button\n onClick={handleExit}\n style={defaultStyles.button}\n onMouseOver={(e) => {\n e.currentTarget.style.backgroundColor = 'rgba(217, 119, 6, 0.1)'\n }}\n onMouseOut={(e) => {\n e.currentTarget.style.backgroundColor = 'transparent'\n }}\n type=\"button\"\n >\n {exitLabel}\n </button>\n </div>\n )\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@demokit-ai/react",
3
+ "version": "0.0.1",
4
+ "description": "React bindings for DemoKit - provider, hooks, and components",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsup",
26
+ "dev": "tsup --watch",
27
+ "test": "vitest",
28
+ "typecheck": "tsc --noEmit"
29
+ },
30
+ "dependencies": {
31
+ "@demokit-ai/core": "*"
32
+ },
33
+ "devDependencies": {
34
+ "@types/react": "^19.0.0",
35
+ "@types/react-dom": "^19.0.0",
36
+ "react": "^19.0.0",
37
+ "react-dom": "^19.0.0",
38
+ "tsup": "^8.3.5",
39
+ "typescript": "^5.7.2",
40
+ "vitest": "^2.1.8"
41
+ },
42
+ "peerDependencies": {
43
+ "react": ">=17.0.0",
44
+ "react-dom": ">=17.0.0"
45
+ },
46
+ "keywords": [
47
+ "demo",
48
+ "mock",
49
+ "react",
50
+ "provider",
51
+ "hooks",
52
+ "saas"
53
+ ],
54
+ "license": "MIT",
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "https://github.com/your-org/demokit",
58
+ "directory": "packages/react"
59
+ },
60
+ "sideEffects": false
61
+ }