@demokit-ai/react 0.2.0 → 0.3.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/index.js CHANGED
@@ -1,13 +1,32 @@
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
1
  // src/provider.tsx
2
+ import { useState, useEffect, useCallback, useMemo, useRef } from "react";
3
+ import {
4
+ createDemoInterceptor,
5
+ fetchCloudFixtures,
6
+ createRemoteFixtures
7
+ } from "@demokit-ai/core";
8
+
9
+ // src/context.ts
10
+ import { createContext } from "react";
6
11
  var DemoModeContext = createContext(void 0);
7
12
  DemoModeContext.displayName = "DemoModeContext";
13
+
14
+ // src/provider.tsx
15
+ import { jsx } from "react/jsx-runtime";
8
16
  function DemoKitProvider({
9
17
  children,
10
18
  fixtures,
19
+ // Remote config
20
+ apiKey,
21
+ cloudUrl,
22
+ timeout,
23
+ retry,
24
+ maxRetries,
25
+ onRemoteLoad,
26
+ onRemoteError,
27
+ loadingFallback = null,
28
+ errorFallback,
29
+ // Standard props
11
30
  storageKey = "demokit-mode",
12
31
  initialEnabled = false,
13
32
  onDemoModeChange,
@@ -15,30 +34,91 @@ function DemoKitProvider({
15
34
  }) {
16
35
  const [isDemoMode, setIsDemoMode] = useState(initialEnabled);
17
36
  const [isHydrated, setIsHydrated] = useState(false);
37
+ const [isLoading, setIsLoading] = useState(!!apiKey);
38
+ const [remoteError, setRemoteError] = useState(null);
39
+ const [remoteVersion, setRemoteVersion] = useState(null);
18
40
  const interceptorRef = useRef(null);
19
41
  const initializedRef = useRef(false);
42
+ const remoteFixturesRef = useRef(null);
43
+ const refetchFnRef = useRef(null);
44
+ const setupInterceptor = useCallback(
45
+ (mergedFixtures) => {
46
+ interceptorRef.current?.destroy();
47
+ interceptorRef.current = createDemoInterceptor({
48
+ fixtures: mergedFixtures,
49
+ storageKey,
50
+ initialEnabled,
51
+ baseUrl,
52
+ onEnable: () => {
53
+ setIsDemoMode(true);
54
+ onDemoModeChange?.(true);
55
+ },
56
+ onDisable: () => {
57
+ setIsDemoMode(false);
58
+ onDemoModeChange?.(false);
59
+ }
60
+ });
61
+ const storedState = interceptorRef.current.isEnabled();
62
+ setIsDemoMode(storedState);
63
+ setIsHydrated(true);
64
+ },
65
+ [storageKey, initialEnabled, baseUrl, onDemoModeChange]
66
+ );
67
+ const fetchAndSetup = useCallback(async () => {
68
+ if (!apiKey) return;
69
+ setIsLoading(true);
70
+ setRemoteError(null);
71
+ try {
72
+ const response = await fetchCloudFixtures({
73
+ apiKey,
74
+ cloudUrl,
75
+ timeout,
76
+ retry,
77
+ maxRetries,
78
+ onLoad: onRemoteLoad,
79
+ onError: onRemoteError
80
+ });
81
+ const remoteFixtures = createRemoteFixtures(response, fixtures);
82
+ remoteFixturesRef.current = remoteFixtures;
83
+ setRemoteVersion(response.version);
84
+ setupInterceptor(remoteFixtures);
85
+ } catch (error) {
86
+ const err = error instanceof Error ? error : new Error(String(error));
87
+ setRemoteError(err);
88
+ onRemoteError?.(err);
89
+ if (fixtures && Object.keys(fixtures).length > 0) {
90
+ setupInterceptor(fixtures);
91
+ } else {
92
+ setIsHydrated(true);
93
+ }
94
+ } finally {
95
+ setIsLoading(false);
96
+ }
97
+ }, [
98
+ apiKey,
99
+ cloudUrl,
100
+ timeout,
101
+ retry,
102
+ maxRetries,
103
+ fixtures,
104
+ onRemoteLoad,
105
+ onRemoteError,
106
+ setupInterceptor
107
+ ]);
108
+ refetchFnRef.current = fetchAndSetup;
20
109
  useEffect(() => {
21
110
  if (initializedRef.current) {
22
111
  return;
23
112
  }
24
113
  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);
114
+ if (apiKey) {
115
+ fetchAndSetup();
116
+ } else if (fixtures) {
117
+ setupInterceptor(fixtures);
118
+ } else {
119
+ setIsHydrated(true);
120
+ setIsLoading(false);
121
+ }
42
122
  return () => {
43
123
  interceptorRef.current?.destroy();
44
124
  interceptorRef.current = null;
@@ -46,10 +126,14 @@ function DemoKitProvider({
46
126
  };
47
127
  }, []);
48
128
  useEffect(() => {
49
- if (interceptorRef.current && isHydrated) {
50
- interceptorRef.current.setFixtures(fixtures);
129
+ if (!isHydrated || isLoading) return;
130
+ if (apiKey && remoteFixturesRef.current) {
131
+ const merged = { ...remoteFixturesRef.current, ...fixtures };
132
+ interceptorRef.current?.setFixtures(merged);
133
+ } else if (fixtures) {
134
+ interceptorRef.current?.setFixtures(fixtures);
51
135
  }
52
- }, [fixtures, isHydrated]);
136
+ }, [fixtures, isHydrated, isLoading, apiKey]);
53
137
  const enable = useCallback(() => {
54
138
  interceptorRef.current?.enable();
55
139
  }, []);
@@ -72,21 +156,55 @@ function DemoKitProvider({
72
156
  const getSession = useCallback(() => {
73
157
  return interceptorRef.current?.getSession() ?? null;
74
158
  }, []);
159
+ const refetch = useCallback(async () => {
160
+ if (!apiKey) {
161
+ console.warn("[DemoKit] refetch() called but no apiKey provided");
162
+ return;
163
+ }
164
+ await refetchFnRef.current?.();
165
+ }, [apiKey]);
75
166
  const value = useMemo(
76
167
  () => ({
77
168
  isDemoMode,
78
169
  isHydrated,
170
+ isLoading,
171
+ remoteError,
172
+ remoteVersion,
79
173
  enable,
80
174
  disable,
81
175
  toggle,
82
176
  setDemoMode,
83
177
  resetSession,
84
- getSession
178
+ getSession,
179
+ refetch
85
180
  }),
86
- [isDemoMode, isHydrated, enable, disable, toggle, setDemoMode, resetSession, getSession]
181
+ [
182
+ isDemoMode,
183
+ isHydrated,
184
+ isLoading,
185
+ remoteError,
186
+ remoteVersion,
187
+ enable,
188
+ disable,
189
+ toggle,
190
+ setDemoMode,
191
+ resetSession,
192
+ getSession,
193
+ refetch
194
+ ]
87
195
  );
196
+ if (isLoading && apiKey) {
197
+ return /* @__PURE__ */ jsx(DemoModeContext.Provider, { value, children: loadingFallback });
198
+ }
199
+ if (remoteError && errorFallback) {
200
+ const errorContent = typeof errorFallback === "function" ? errorFallback(remoteError) : errorFallback;
201
+ return /* @__PURE__ */ jsx(DemoModeContext.Provider, { value, children: errorContent });
202
+ }
88
203
  return /* @__PURE__ */ jsx(DemoModeContext.Provider, { value, children });
89
204
  }
205
+
206
+ // src/hooks.ts
207
+ import { useContext } from "react";
90
208
  function useDemoMode() {
91
209
  const context = useContext(DemoModeContext);
92
210
  if (context === void 0) {
@@ -105,8 +223,116 @@ function useIsHydrated() {
105
223
  function useDemoSession() {
106
224
  return useDemoMode().getSession();
107
225
  }
108
- function EyeIcon() {
226
+
227
+ // src/powered-by.tsx
228
+ import { jsx as jsx2, jsxs } from "react/jsx-runtime";
229
+ function ExternalLinkIcon({ size }) {
109
230
  return /* @__PURE__ */ jsxs(
231
+ "svg",
232
+ {
233
+ width: size,
234
+ height: size,
235
+ viewBox: "0 0 24 24",
236
+ fill: "none",
237
+ stroke: "currentColor",
238
+ strokeWidth: "2",
239
+ strokeLinecap: "round",
240
+ strokeLinejoin: "round",
241
+ "aria-hidden": "true",
242
+ children: [
243
+ /* @__PURE__ */ jsx2("path", { d: "M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" }),
244
+ /* @__PURE__ */ jsx2("polyline", { points: "15 3 21 3 21 9" }),
245
+ /* @__PURE__ */ jsx2("line", { x1: "10", y1: "14", x2: "21", y2: "3" })
246
+ ]
247
+ }
248
+ );
249
+ }
250
+ var sizeConfig = {
251
+ xs: {
252
+ fontSize: "10px",
253
+ padding: "2px 6px",
254
+ gap: "3px",
255
+ iconSize: 8
256
+ },
257
+ sm: {
258
+ fontSize: "11px",
259
+ padding: "3px 8px",
260
+ gap: "4px",
261
+ iconSize: 10
262
+ },
263
+ md: {
264
+ fontSize: "12px",
265
+ padding: "4px 10px",
266
+ gap: "5px",
267
+ iconSize: 12
268
+ }
269
+ };
270
+ var variantStyles = {
271
+ light: {
272
+ color: "rgba(120, 53, 15, 0.7)",
273
+ hoverColor: "rgba(120, 53, 15, 0.9)",
274
+ backgroundColor: "transparent",
275
+ hoverBackgroundColor: "rgba(217, 119, 6, 0.08)"
276
+ },
277
+ dark: {
278
+ color: "rgba(255, 255, 255, 0.6)",
279
+ hoverColor: "rgba(255, 255, 255, 0.9)",
280
+ backgroundColor: "transparent",
281
+ hoverBackgroundColor: "rgba(255, 255, 255, 0.1)"
282
+ }
283
+ };
284
+ function PoweredByBadge({
285
+ url = "https://demokit.ai",
286
+ variant = "light",
287
+ size = "sm",
288
+ className = "",
289
+ style
290
+ }) {
291
+ const config = sizeConfig[size];
292
+ const colors = variantStyles[variant === "auto" ? "light" : variant];
293
+ const baseStyles = {
294
+ display: "inline-flex",
295
+ alignItems: "center",
296
+ gap: config.gap,
297
+ fontSize: config.fontSize,
298
+ padding: config.padding,
299
+ color: colors.color,
300
+ textDecoration: "none",
301
+ borderRadius: "4px",
302
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
303
+ fontWeight: 500,
304
+ transition: "color 0.15s ease, background-color 0.15s ease",
305
+ whiteSpace: "nowrap",
306
+ ...style
307
+ };
308
+ return /* @__PURE__ */ jsxs(
309
+ "a",
310
+ {
311
+ href: url,
312
+ target: "_blank",
313
+ rel: "noopener noreferrer",
314
+ className: `demokit-powered-by ${className}`.trim(),
315
+ style: baseStyles,
316
+ onMouseOver: (e) => {
317
+ e.currentTarget.style.color = colors.hoverColor;
318
+ e.currentTarget.style.backgroundColor = colors.hoverBackgroundColor;
319
+ },
320
+ onMouseOut: (e) => {
321
+ e.currentTarget.style.color = colors.color;
322
+ e.currentTarget.style.backgroundColor = "transparent";
323
+ },
324
+ children: [
325
+ /* @__PURE__ */ jsx2("span", { children: "Powered by DemoKit" }),
326
+ /* @__PURE__ */ jsx2(ExternalLinkIcon, { size: config.iconSize })
327
+ ]
328
+ }
329
+ );
330
+ }
331
+
332
+ // src/banner.tsx
333
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
334
+ function EyeIcon() {
335
+ return /* @__PURE__ */ jsxs2(
110
336
  "svg",
111
337
  {
112
338
  width: "20",
@@ -119,8 +345,8 @@ function EyeIcon() {
119
345
  strokeLinejoin: "round",
120
346
  "aria-hidden": "true",
121
347
  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" })
348
+ /* @__PURE__ */ jsx3("path", { d: "M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" }),
349
+ /* @__PURE__ */ jsx3("circle", { cx: "12", cy: "12", r: "3" })
124
350
  ]
125
351
  }
126
352
  );
@@ -171,6 +397,8 @@ function DemoModeBanner({
171
397
  demoLabel = "Demo Mode Active",
172
398
  description = "Changes are simulated and not saved",
173
399
  showIcon = true,
400
+ showPoweredBy = true,
401
+ poweredByUrl = "https://demokit.ai",
174
402
  style,
175
403
  onExit
176
404
  }) {
@@ -185,39 +413,221 @@ function DemoModeBanner({
185
413
  disable();
186
414
  }
187
415
  };
188
- return /* @__PURE__ */ jsxs(
416
+ const effectiveShowPoweredBy = showPoweredBy;
417
+ return /* @__PURE__ */ jsxs2(
189
418
  "div",
190
419
  {
191
420
  className: `demokit-banner ${className}`.trim(),
192
- style: { ...defaultStyles.container, ...style },
421
+ style: { ...defaultStyles.container, flexDirection: "column", gap: "4px", ...style },
193
422
  role: "status",
194
423
  "aria-live": "polite",
195
424
  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 })
425
+ /* @__PURE__ */ jsxs2("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }, children: [
426
+ /* @__PURE__ */ jsxs2("div", { style: defaultStyles.content, children: [
427
+ showIcon && /* @__PURE__ */ jsx3("span", { style: defaultStyles.icon, children: /* @__PURE__ */ jsx3(EyeIcon, {}) }),
428
+ /* @__PURE__ */ jsx3("span", { style: defaultStyles.label, children: demoLabel }),
429
+ description && /* @__PURE__ */ jsx3("span", { style: defaultStyles.description, children: description })
430
+ ] }),
431
+ /* @__PURE__ */ jsx3(
432
+ "button",
433
+ {
434
+ onClick: handleExit,
435
+ style: defaultStyles.button,
436
+ onMouseOver: (e) => {
437
+ e.currentTarget.style.backgroundColor = "rgba(217, 119, 6, 0.1)";
438
+ },
439
+ onMouseOut: (e) => {
440
+ e.currentTarget.style.backgroundColor = "transparent";
441
+ },
442
+ type: "button",
443
+ children: exitLabel
444
+ }
445
+ )
200
446
  ] }),
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
- )
447
+ effectiveShowPoweredBy && /* @__PURE__ */ jsx3("div", { style: { display: "flex", justifyContent: "flex-end", width: "100%" }, children: /* @__PURE__ */ jsx3(PoweredByBadge, { url: poweredByUrl, size: "xs" }) })
216
448
  ]
217
449
  }
218
450
  );
219
451
  }
220
452
 
221
- export { DemoKitProvider, DemoModeBanner, DemoModeContext, useDemoMode, useDemoSession, useIsDemoMode, useIsHydrated };
222
- //# sourceMappingURL=index.js.map
453
+ // src/toggle.tsx
454
+ import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
455
+ var sizeConfig2 = {
456
+ sm: {
457
+ trackWidth: 36,
458
+ trackHeight: 20,
459
+ thumbSize: 16,
460
+ thumbOffset: 2,
461
+ fontSize: "12px",
462
+ padding: "8px 12px",
463
+ gap: "8px"
464
+ },
465
+ md: {
466
+ trackWidth: 44,
467
+ trackHeight: 24,
468
+ thumbSize: 20,
469
+ thumbOffset: 2,
470
+ fontSize: "14px",
471
+ padding: "12px 16px",
472
+ gap: "10px"
473
+ },
474
+ lg: {
475
+ trackWidth: 52,
476
+ trackHeight: 28,
477
+ thumbSize: 24,
478
+ thumbOffset: 2,
479
+ fontSize: "16px",
480
+ padding: "16px 20px",
481
+ gap: "12px"
482
+ }
483
+ };
484
+ var positionStyles = {
485
+ inline: {},
486
+ floating: {
487
+ position: "fixed",
488
+ zIndex: 9999,
489
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
490
+ borderRadius: "8px"
491
+ },
492
+ corner: {
493
+ position: "fixed",
494
+ bottom: "20px",
495
+ right: "20px",
496
+ zIndex: 9999,
497
+ boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
498
+ borderRadius: "8px"
499
+ }
500
+ };
501
+ var defaultStyles2 = {
502
+ container: {
503
+ display: "inline-flex",
504
+ flexDirection: "column",
505
+ alignItems: "flex-start",
506
+ gap: "4px",
507
+ backgroundColor: "#ffffff",
508
+ border: "1px solid rgba(0, 0, 0, 0.1)",
509
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
510
+ },
511
+ row: {
512
+ display: "flex",
513
+ alignItems: "center",
514
+ width: "100%"
515
+ },
516
+ label: {
517
+ fontWeight: 500,
518
+ color: "#1f2937",
519
+ userSelect: "none"
520
+ },
521
+ track: {
522
+ position: "relative",
523
+ borderRadius: "9999px",
524
+ cursor: "pointer",
525
+ transition: "background-color 0.2s ease",
526
+ flexShrink: 0
527
+ },
528
+ trackOff: {
529
+ backgroundColor: "#d1d5db"
530
+ },
531
+ trackOn: {
532
+ backgroundColor: "#d97706"
533
+ },
534
+ thumb: {
535
+ position: "absolute",
536
+ top: "50%",
537
+ transform: "translateY(-50%)",
538
+ backgroundColor: "#ffffff",
539
+ borderRadius: "50%",
540
+ boxShadow: "0 1px 3px rgba(0, 0, 0, 0.2)",
541
+ transition: "left 0.2s ease"
542
+ },
543
+ poweredByContainer: {
544
+ display: "flex",
545
+ justifyContent: "flex-end",
546
+ width: "100%"
547
+ }
548
+ };
549
+ function DemoModeToggle({
550
+ showLabel = true,
551
+ label = "Demo Mode",
552
+ showPoweredBy = true,
553
+ poweredByUrl = "https://demokit.ai",
554
+ position = "inline",
555
+ size = "md",
556
+ className = "",
557
+ style,
558
+ onChange
559
+ }) {
560
+ const { isDemoMode, isHydrated, toggle } = useDemoMode();
561
+ if (!isHydrated) {
562
+ return null;
563
+ }
564
+ const config = sizeConfig2[size];
565
+ const posStyle = positionStyles[position];
566
+ const handleToggle = () => {
567
+ toggle();
568
+ onChange?.(!isDemoMode);
569
+ };
570
+ const thumbLeft = isDemoMode ? config.trackWidth - config.thumbSize - config.thumbOffset : config.thumbOffset;
571
+ const effectiveShowPoweredBy = showPoweredBy;
572
+ return /* @__PURE__ */ jsxs3(
573
+ "div",
574
+ {
575
+ className: `demokit-toggle ${className}`.trim(),
576
+ style: {
577
+ ...defaultStyles2.container,
578
+ padding: config.padding,
579
+ ...posStyle,
580
+ ...style
581
+ },
582
+ role: "group",
583
+ "aria-label": "Demo mode toggle",
584
+ children: [
585
+ /* @__PURE__ */ jsxs3("div", { style: { ...defaultStyles2.row, gap: config.gap }, children: [
586
+ showLabel && /* @__PURE__ */ jsx4("span", { style: { ...defaultStyles2.label, fontSize: config.fontSize }, children: label }),
587
+ /* @__PURE__ */ jsx4(
588
+ "button",
589
+ {
590
+ type: "button",
591
+ role: "switch",
592
+ "aria-checked": isDemoMode,
593
+ "aria-label": `${label}: ${isDemoMode ? "On" : "Off"}`,
594
+ onClick: handleToggle,
595
+ style: {
596
+ ...defaultStyles2.track,
597
+ ...isDemoMode ? defaultStyles2.trackOn : defaultStyles2.trackOff,
598
+ width: config.trackWidth,
599
+ height: config.trackHeight,
600
+ border: "none",
601
+ padding: 0
602
+ },
603
+ children: /* @__PURE__ */ jsx4(
604
+ "span",
605
+ {
606
+ style: {
607
+ ...defaultStyles2.thumb,
608
+ width: config.thumbSize,
609
+ height: config.thumbSize,
610
+ left: thumbLeft
611
+ }
612
+ }
613
+ )
614
+ }
615
+ )
616
+ ] }),
617
+ effectiveShowPoweredBy && /* @__PURE__ */ jsx4("div", { style: defaultStyles2.poweredByContainer, children: /* @__PURE__ */ jsx4(PoweredByBadge, { url: poweredByUrl, size: "xs" }) })
618
+ ]
619
+ }
620
+ );
621
+ }
622
+ export {
623
+ DemoKitProvider,
624
+ DemoModeBanner,
625
+ DemoModeContext,
626
+ DemoModeToggle,
627
+ PoweredByBadge,
628
+ useDemoMode,
629
+ useDemoSession,
630
+ useIsDemoMode,
631
+ useIsHydrated
632
+ };
223
633
  //# sourceMappingURL=index.js.map