@moldable-ai/ui 0.2.8 → 0.2.10
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/components/app-error-boundary.d.ts +26 -0
- package/dist/components/app-error-boundary.d.ts.map +1 -0
- package/dist/components/app-error-boundary.js +87 -0
- package/dist/components/chat/chat-input.d.ts +8 -1
- package/dist/components/chat/chat-input.d.ts.map +1 -1
- package/dist/components/chat/chat-input.js +6 -3
- package/dist/components/chat/chat-message.d.ts +1 -0
- package/dist/components/chat/chat-message.d.ts.map +1 -1
- package/dist/components/chat/chat-message.js +100 -12
- package/dist/components/chat/chat-messages.d.ts +2 -1
- package/dist/components/chat/chat-messages.d.ts.map +1 -1
- package/dist/components/chat/chat-messages.js +3 -3
- package/dist/components/chat/chat-panel.d.ts +25 -3
- package/dist/components/chat/chat-panel.d.ts.map +1 -1
- package/dist/components/chat/chat-panel.js +158 -42
- package/dist/components/chat/index.d.ts +3 -3
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +1 -1
- package/dist/components/chat/model-selector.d.ts.map +1 -1
- package/dist/components/chat/model-selector.js +1 -1
- package/dist/components/chat/tool-handlers.d.ts.map +1 -1
- package/dist/components/chat/tool-handlers.js +89 -1
- package/dist/components/markdown.d.ts.map +1 -1
- package/dist/components/markdown.js +16 -2
- package/dist/components/rich-media-player.d.ts +8 -0
- package/dist/components/rich-media-player.d.ts.map +1 -0
- package/dist/components/rich-media-player.js +99 -0
- package/dist/components/ui/resizable.d.ts +4 -1
- package/dist/components/ui/resizable.d.ts.map +1 -1
- package/dist/components/ui/resizable.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/package.json +4 -1
- package/src/styles/index.css +25 -0
- package/dist/lib/commands.test.d.ts +0 -2
- package/dist/lib/commands.test.d.ts.map +0 -1
- package/dist/lib/commands.test.js +0 -111
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
|
+
import { FastForward, Pause, Play, Rewind, Volume2, VolumeX, } from 'lucide-react';
|
|
5
|
+
import { cn } from '../lib/utils';
|
|
6
|
+
const PLAYBACK_RATES = [0.75, 1, 1.25, 1.5, 2];
|
|
7
|
+
function formatTime(value) {
|
|
8
|
+
if (!Number.isFinite(value) || value < 0)
|
|
9
|
+
return '0:00';
|
|
10
|
+
const totalSeconds = Math.floor(value);
|
|
11
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
12
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
13
|
+
const seconds = totalSeconds % 60;
|
|
14
|
+
if (hours > 0) {
|
|
15
|
+
return `${hours}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
16
|
+
}
|
|
17
|
+
return `${minutes}:${String(seconds).padStart(2, '0')}`;
|
|
18
|
+
}
|
|
19
|
+
function safeDuration(duration) {
|
|
20
|
+
return Number.isFinite(duration) && duration > 0 ? duration : 0;
|
|
21
|
+
}
|
|
22
|
+
export function RichMediaPlayer({ className, kind, src, title, }) {
|
|
23
|
+
const mediaRef = useRef(null);
|
|
24
|
+
const containerRef = useRef(null);
|
|
25
|
+
const [currentTime, setCurrentTime] = useState(0);
|
|
26
|
+
const [duration, setDuration] = useState(0);
|
|
27
|
+
const [isPlaying, setIsPlaying] = useState(false);
|
|
28
|
+
const [isMuted, setIsMuted] = useState(false);
|
|
29
|
+
const [volume, setVolume] = useState(1);
|
|
30
|
+
const [playbackRate, setPlaybackRate] = useState(1);
|
|
31
|
+
const durationValue = safeDuration(duration);
|
|
32
|
+
const canSeek = durationValue > 0;
|
|
33
|
+
const isAudio = kind === 'audio';
|
|
34
|
+
const mediaTitle = useMemo(() => {
|
|
35
|
+
if (title)
|
|
36
|
+
return title;
|
|
37
|
+
try {
|
|
38
|
+
return decodeURIComponent(src.split('/').pop()?.split('?')[0] ?? src);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return src;
|
|
42
|
+
}
|
|
43
|
+
}, [src, title]);
|
|
44
|
+
const setMediaElement = useCallback((element) => {
|
|
45
|
+
mediaRef.current = element;
|
|
46
|
+
}, []);
|
|
47
|
+
const togglePlayback = () => {
|
|
48
|
+
const media = mediaRef.current;
|
|
49
|
+
if (!media)
|
|
50
|
+
return;
|
|
51
|
+
if (media.paused) {
|
|
52
|
+
void media.play();
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
media.pause();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const seekBy = (offset) => {
|
|
59
|
+
const media = mediaRef.current;
|
|
60
|
+
if (!media)
|
|
61
|
+
return;
|
|
62
|
+
media.currentTime = Math.max(0, Math.min(durationValue, media.currentTime + offset));
|
|
63
|
+
};
|
|
64
|
+
const seekTo = (value) => {
|
|
65
|
+
const media = mediaRef.current;
|
|
66
|
+
if (!media)
|
|
67
|
+
return;
|
|
68
|
+
media.currentTime = value;
|
|
69
|
+
setCurrentTime(value);
|
|
70
|
+
};
|
|
71
|
+
const changeVolume = (value) => {
|
|
72
|
+
const media = mediaRef.current;
|
|
73
|
+
if (!media)
|
|
74
|
+
return;
|
|
75
|
+
media.volume = value;
|
|
76
|
+
media.muted = value === 0;
|
|
77
|
+
setVolume(value);
|
|
78
|
+
setIsMuted(value === 0);
|
|
79
|
+
};
|
|
80
|
+
const toggleMuted = () => {
|
|
81
|
+
const media = mediaRef.current;
|
|
82
|
+
if (!media)
|
|
83
|
+
return;
|
|
84
|
+
const nextMuted = !media.muted;
|
|
85
|
+
media.muted = nextMuted;
|
|
86
|
+
setIsMuted(nextMuted);
|
|
87
|
+
};
|
|
88
|
+
const cyclePlaybackRate = () => {
|
|
89
|
+
const media = mediaRef.current;
|
|
90
|
+
if (!media)
|
|
91
|
+
return;
|
|
92
|
+
const currentIndex = PLAYBACK_RATES.findIndex((rate) => rate === playbackRate);
|
|
93
|
+
const nextRate = PLAYBACK_RATES[(currentIndex + 1) % PLAYBACK_RATES.length] ?? 1;
|
|
94
|
+
media.playbackRate = nextRate;
|
|
95
|
+
setPlaybackRate(nextRate);
|
|
96
|
+
};
|
|
97
|
+
const controls = (_jsxs("div", { className: cn('flex flex-col gap-2 text-white', isAudio ? 'p-3' : 'bg-gradient-to-b from-transparent via-black/65 to-neutral-950 px-3 pt-10 pb-3'), children: [_jsx("input", { "aria-label": "Seek", className: "h-2 w-full cursor-pointer accent-white disabled:cursor-default disabled:opacity-50", disabled: !canSeek, max: durationValue, min: 0, onChange: (event) => seekTo(Number(event.currentTarget.value)), step: 0.1, type: "range", value: Math.min(currentTime, durationValue) }), _jsxs("div", { className: "flex h-10 items-center gap-3 px-1", children: [_jsx("button", { "aria-label": isPlaying ? 'Pause' : 'Play', className: "cursor-pointer rounded-full p-1 text-white transition-colors hover:text-primary focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white", onClick: togglePlayback, type: "button", children: isPlaying ? _jsx(Pause, { className: "size-6" }) : _jsx(Play, { className: "size-6" }) }), _jsx("button", { "aria-label": "Rewind 10 seconds", className: "cursor-pointer rounded-full p-1 text-white/85 transition-colors hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white", onClick: () => seekBy(-10), type: "button", children: _jsx(Rewind, { className: "size-5" }) }), _jsx("button", { "aria-label": "Forward 10 seconds", className: "cursor-pointer rounded-full p-1 text-white/85 transition-colors hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white", onClick: () => seekBy(10), type: "button", children: _jsx(FastForward, { className: "size-5" }) }), _jsxs("div", { className: "group/volume flex items-center gap-2", children: [_jsx("button", { "aria-label": isMuted ? 'Unmute' : 'Mute', className: "cursor-pointer rounded-full p-1 text-white/85 transition-colors hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white", onClick: toggleMuted, type: "button", children: isMuted || volume === 0 ? (_jsx(VolumeX, { className: "size-5" })) : (_jsx(Volume2, { className: "size-5" })) }), _jsx("input", { "aria-label": "Volume", className: "h-2 w-0 cursor-pointer accent-white opacity-0 transition-all group-hover/volume:w-20 group-hover/volume:opacity-100 focus:w-20 focus:opacity-100", max: 1, min: 0, onChange: (event) => changeVolume(Number(event.currentTarget.value)), step: 0.05, type: "range", value: isMuted ? 0 : volume })] }), _jsxs("span", { className: "ml-auto min-w-28 text-right font-mono text-xs text-white/80", children: [formatTime(currentTime), " / ", formatTime(durationValue)] }), _jsxs("button", { "aria-label": "Change playback speed", className: "cursor-pointer rounded px-2 py-1 font-mono text-xs text-white/85 transition-colors hover:text-white focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-white", onClick: cyclePlaybackRate, type: "button", children: [playbackRate, "x"] })] })] }));
|
|
98
|
+
return (_jsx("div", { className: cn('not-prose overflow-hidden rounded-lg border border-black/5 bg-neutral-950 text-white shadow-sm dark:border-white/10', className), ref: containerRef, children: _jsxs("div", { className: cn('group/mediaplayer relative w-full overflow-hidden', !isAudio && 'bg-black'), children: [isAudio ? (_jsx("audio", { "aria-label": mediaTitle, onDurationChange: (event) => setDuration(event.currentTarget.duration), onEnded: () => setIsPlaying(false), onLoadedMetadata: (event) => setDuration(event.currentTarget.duration), onPause: () => setIsPlaying(false), onPlay: () => setIsPlaying(true), onTimeUpdate: (event) => setCurrentTime(event.currentTarget.currentTime), preload: "metadata", ref: setMediaElement, src: src })) : (_jsx("video", { className: "h-auto max-h-[600px] min-h-[225px] w-full bg-black object-contain", onClick: togglePlayback, onDurationChange: (event) => setDuration(event.currentTarget.duration), onEnded: () => setIsPlaying(false), onLoadedMetadata: (event) => setDuration(event.currentTarget.duration), onPause: () => setIsPlaying(false), onPlay: () => setIsPlaying(true), onTimeUpdate: (event) => setCurrentTime(event.currentTarget.currentTime), playsInline: true, preload: "metadata", ref: setMediaElement, src: src })), _jsx("div", { className: cn(isAudio ? 'bg-neutral-950' : 'absolute right-0 bottom-0 left-0 transition-transform duration-300', !isAudio && isPlaying && 'translate-y-[calc(100%-36px)] group-hover/mediaplayer:translate-y-0'), children: controls })] }) }));
|
|
99
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { type GroupProps, type PanelProps, type SeparatorProps } from 'react-resizable-panels';
|
|
2
2
|
declare function ResizablePanelGroup({ className, ...props }: GroupProps): import("react/jsx-runtime").JSX.Element;
|
|
3
|
-
|
|
3
|
+
type ResizablePanelProps = PanelProps & {
|
|
4
|
+
groupResizeBehavior?: string;
|
|
5
|
+
};
|
|
6
|
+
declare function ResizablePanel({ groupResizeBehavior: _groupResizeBehavior, ...props }: ResizablePanelProps): import("react/jsx-runtime").JSX.Element;
|
|
4
7
|
declare function ResizableHandle({ withHandle, className, ...props }: SeparatorProps & {
|
|
5
8
|
withHandle?: boolean;
|
|
6
9
|
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resizable.d.ts","sourceRoot":"","sources":["../../../src/components/ui/resizable.tsx"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,UAAU,EAEf,KAAK,UAAU,EAEf,KAAK,cAAc,EACpB,MAAM,wBAAwB,CAAA;AAG/B,iBAAS,mBAAmB,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,UAAU,2CAW/D;AAED,iBAAS,cAAc,CAAC,EAAE,GAAG,KAAK,
|
|
1
|
+
{"version":3,"file":"resizable.d.ts","sourceRoot":"","sources":["../../../src/components/ui/resizable.tsx"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,UAAU,EAEf,KAAK,UAAU,EAEf,KAAK,cAAc,EACpB,MAAM,wBAAwB,CAAA;AAG/B,iBAAS,mBAAmB,CAAC,EAAE,SAAS,EAAE,GAAG,KAAK,EAAE,EAAE,UAAU,2CAW/D;AAED,KAAK,mBAAmB,GAAG,UAAU,GAAG;IACtC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B,CAAA;AAED,iBAAS,cAAc,CAAC,EACtB,mBAAmB,EAAE,oBAAoB,EACzC,GAAG,KAAK,EACT,EAAE,mBAAmB,2CAErB;AAED,iBAAS,eAAe,CAAC,EACvB,UAAU,EACV,SAAS,EACT,GAAG,KAAK,EACT,EAAE,cAAc,GAAG;IAClB,UAAU,CAAC,EAAE,OAAO,CAAA;CACrB,2CAiBA;AAED,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,eAAe,EAAE,CAAA"}
|
|
@@ -6,7 +6,7 @@ import { cn } from '../../lib/utils';
|
|
|
6
6
|
function ResizablePanelGroup({ className, ...props }) {
|
|
7
7
|
return (_jsx(Group, { "data-slot": "resizable-panel-group", className: cn('flex h-full w-full data-[panel-group-direction=vertical]:flex-col', className), ...props }));
|
|
8
8
|
}
|
|
9
|
-
function ResizablePanel({ ...props }) {
|
|
9
|
+
function ResizablePanel({ groupResizeBehavior: _groupResizeBehavior, ...props }) {
|
|
10
10
|
return _jsx(Panel, { "data-slot": "resizable-panel", ...props });
|
|
11
11
|
}
|
|
12
12
|
function ResizableHandle({ withHandle, className, ...props }) {
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,9 @@ export { useMoldableCommands, useMoldableCommand, useMoldableAppCommands, isInMo
|
|
|
5
5
|
export * from './components/ui';
|
|
6
6
|
export { useIsMobile } from './hooks/use-mobile';
|
|
7
7
|
export { Markdown } from './components/markdown';
|
|
8
|
+
export { RichMediaPlayer, type RichMediaPlayerProps, } from './components/rich-media-player';
|
|
8
9
|
export { CodeBlock } from './components/code-block';
|
|
9
10
|
export { WidgetLayout } from './components/widget-layout';
|
|
11
|
+
export { AppErrorBoundary } from './components/app-error-boundary';
|
|
10
12
|
export * from './components/chat';
|
|
11
13
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAGhC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,aAAa,CAAA;AAG9E,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,GACjB,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAA;AAGvB,cAAc,iBAAiB,CAAA;AAG/B,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGhD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAGhC,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,MAAM,aAAa,CAAA;AAG9E,OAAO,EACL,iBAAiB,EACjB,YAAY,EACZ,gBAAgB,GACjB,MAAM,iBAAiB,CAAA;AAGxB,OAAO,EACL,mBAAmB,EACnB,kBAAkB,EAClB,sBAAsB,EACtB,YAAY,EACZ,cAAc,EACd,YAAY,EACZ,KAAK,UAAU,EACf,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,gBAAgB,CAAA;AAGvB,cAAc,iBAAiB,CAAA;AAG/B,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGhD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAChD,OAAO,EACL,eAAe,EACf,KAAK,oBAAoB,GAC1B,MAAM,gCAAgC,CAAA;AAGvC,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAA;AAGnD,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAA;AAGzD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AAGlE,cAAc,mBAAmB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -12,9 +12,12 @@ export * from './components/ui';
|
|
|
12
12
|
export { useIsMobile } from './hooks/use-mobile';
|
|
13
13
|
// Export Markdown
|
|
14
14
|
export { Markdown } from './components/markdown';
|
|
15
|
+
export { RichMediaPlayer, } from './components/rich-media-player';
|
|
15
16
|
// Export CodeBlock
|
|
16
17
|
export { CodeBlock } from './components/code-block';
|
|
17
18
|
// Export WidgetLayout
|
|
18
19
|
export { WidgetLayout } from './components/widget-layout';
|
|
20
|
+
// Export app error boundary
|
|
21
|
+
export { AppErrorBoundary } from './components/app-error-boundary';
|
|
19
22
|
// Export chat components
|
|
20
23
|
export * from './components/chat';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moldable-ai/ui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "Shared UI components for Moldable applications",
|
|
5
5
|
"author": "Desiderata LLC",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@base-ui/react": "^1.1.0",
|
|
46
|
+
"@dnd-kit/core": "^6.3.1",
|
|
47
|
+
"@dnd-kit/sortable": "^10.0.0",
|
|
48
|
+
"@dnd-kit/utilities": "^3.2.2",
|
|
46
49
|
"@hookform/resolvers": "^5.2.2",
|
|
47
50
|
"@radix-ui/react-accordion": "^1.2.12",
|
|
48
51
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
package/src/styles/index.css
CHANGED
|
@@ -68,6 +68,31 @@
|
|
|
68
68
|
--chat-safe-padding: var(--chat-safe-padding);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
@keyframes moldable-chat-shimmer {
|
|
72
|
+
0% {
|
|
73
|
+
background-position: 140% 50%;
|
|
74
|
+
}
|
|
75
|
+
100% {
|
|
76
|
+
background-position: -40% 50%;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.moldable-chat-shimmer {
|
|
81
|
+
color: transparent;
|
|
82
|
+
background: linear-gradient(
|
|
83
|
+
90deg,
|
|
84
|
+
color-mix(in oklch, var(--muted-foreground) 72%, transparent) 0%,
|
|
85
|
+
color-mix(in oklch, var(--foreground) 92%, white 8%) 42%,
|
|
86
|
+
color-mix(in oklch, var(--foreground) 100%, white 16%) 50%,
|
|
87
|
+
color-mix(in oklch, var(--foreground) 92%, white 8%) 58%,
|
|
88
|
+
color-mix(in oklch, var(--muted-foreground) 72%, transparent) 100%
|
|
89
|
+
);
|
|
90
|
+
background-size: 240% 100%;
|
|
91
|
+
-webkit-background-clip: text;
|
|
92
|
+
background-clip: text;
|
|
93
|
+
animation: moldable-chat-shimmer 1.8s ease-in-out infinite;
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
:root {
|
|
72
97
|
--radius: 0.65rem;
|
|
73
98
|
--background: oklch(1 0 0);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"commands.test.d.ts","sourceRoot":"","sources":["../../src/lib/commands.test.ts"],"names":[],"mappings":""}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
-
// Re-implement isInMoldable for testing (since it accesses window)
|
|
3
|
-
function isInMoldable() {
|
|
4
|
-
if (typeof window === 'undefined')
|
|
5
|
-
return false;
|
|
6
|
-
return window.parent !== window;
|
|
7
|
-
}
|
|
8
|
-
describe('commands utilities', () => {
|
|
9
|
-
describe('isInMoldable', () => {
|
|
10
|
-
const originalWindow = global.window;
|
|
11
|
-
afterEach(() => {
|
|
12
|
-
// Restore original window
|
|
13
|
-
if (originalWindow) {
|
|
14
|
-
global.window = originalWindow;
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
it('returns false when window is undefined (SSR)', () => {
|
|
18
|
-
// @ts-expect-error - intentionally setting to undefined
|
|
19
|
-
global.window = undefined;
|
|
20
|
-
// Re-evaluate with undefined window
|
|
21
|
-
const result = typeof window === 'undefined' ? false : window.parent !== window;
|
|
22
|
-
expect(result).toBe(false);
|
|
23
|
-
});
|
|
24
|
-
it('returns false when parent equals window (not in iframe)', () => {
|
|
25
|
-
// jsdom default: window.parent === window
|
|
26
|
-
expect(isInMoldable()).toBe(false);
|
|
27
|
-
});
|
|
28
|
-
it('returns true when parent differs from window (in iframe)', () => {
|
|
29
|
-
// Mock the iframe scenario
|
|
30
|
-
const mockParent = {};
|
|
31
|
-
const originalParent = window.parent;
|
|
32
|
-
Object.defineProperty(window, 'parent', {
|
|
33
|
-
value: mockParent,
|
|
34
|
-
writable: true,
|
|
35
|
-
configurable: true,
|
|
36
|
-
});
|
|
37
|
-
expect(isInMoldable()).toBe(true);
|
|
38
|
-
// Restore
|
|
39
|
-
Object.defineProperty(window, 'parent', {
|
|
40
|
-
value: originalParent,
|
|
41
|
-
writable: true,
|
|
42
|
-
configurable: true,
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
describe('CommandAction types', () => {
|
|
47
|
-
it('navigate action has path', () => {
|
|
48
|
-
const action = { type: 'navigate', path: '/settings' };
|
|
49
|
-
expect(action.type).toBe('navigate');
|
|
50
|
-
expect(action.path).toBe('/settings');
|
|
51
|
-
});
|
|
52
|
-
it('message action has payload', () => {
|
|
53
|
-
const action = { type: 'message', payload: { foo: 'bar' } };
|
|
54
|
-
expect(action.type).toBe('message');
|
|
55
|
-
expect(action.payload).toEqual({ foo: 'bar' });
|
|
56
|
-
});
|
|
57
|
-
it('focus action has target', () => {
|
|
58
|
-
const action = { type: 'focus', target: 'input-field' };
|
|
59
|
-
expect(action.type).toBe('focus');
|
|
60
|
-
expect(action.target).toBe('input-field');
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
describe('AppCommand structure', () => {
|
|
64
|
-
it('accepts valid command definition', () => {
|
|
65
|
-
const command = {
|
|
66
|
-
id: 'add-todo',
|
|
67
|
-
label: 'Add Todo',
|
|
68
|
-
shortcut: 'a',
|
|
69
|
-
icon: '➕',
|
|
70
|
-
group: 'Tasks',
|
|
71
|
-
action: { type: 'message', payload: {} },
|
|
72
|
-
};
|
|
73
|
-
expect(command.id).toBe('add-todo');
|
|
74
|
-
expect(command.label).toBe('Add Todo');
|
|
75
|
-
expect(command.shortcut).toBe('a');
|
|
76
|
-
expect(command.icon).toBe('➕');
|
|
77
|
-
expect(command.group).toBe('Tasks');
|
|
78
|
-
});
|
|
79
|
-
it('works with minimal required fields', () => {
|
|
80
|
-
const command = {
|
|
81
|
-
id: 'test',
|
|
82
|
-
label: 'Test Command',
|
|
83
|
-
action: { type: 'navigate', path: '/' },
|
|
84
|
-
};
|
|
85
|
-
expect(command.id).toBeDefined();
|
|
86
|
-
expect(command.label).toBeDefined();
|
|
87
|
-
expect(command.action).toBeDefined();
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
describe('CommandMessage structure', () => {
|
|
91
|
-
it('has correct type marker', () => {
|
|
92
|
-
const message = {
|
|
93
|
-
type: 'moldable:command',
|
|
94
|
-
command: 'add-todo',
|
|
95
|
-
payload: { text: 'Buy milk' },
|
|
96
|
-
};
|
|
97
|
-
expect(message.type).toBe('moldable:command');
|
|
98
|
-
expect(message.command).toBe('add-todo');
|
|
99
|
-
expect(message.payload).toEqual({ text: 'Buy milk' });
|
|
100
|
-
});
|
|
101
|
-
it('works without payload', () => {
|
|
102
|
-
const message = {
|
|
103
|
-
type: 'moldable:command',
|
|
104
|
-
command: 'refresh',
|
|
105
|
-
};
|
|
106
|
-
expect(message.type).toBe('moldable:command');
|
|
107
|
-
expect(message.command).toBe('refresh');
|
|
108
|
-
expect(message.payload).toBeUndefined();
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
});
|