@run0/jiki-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,133 @@
1
+ import { useRef, useEffect, useState } from "react";
2
+ import type { AccentColor } from "./types";
3
+
4
+ export interface TabItem {
5
+ id: string;
6
+ label: string;
7
+ icon?: React.ReactNode;
8
+ }
9
+
10
+ export interface MobileTabBarProps {
11
+ tabs: TabItem[];
12
+ activeTab: string;
13
+ onTabChange: (id: string) => void;
14
+ accentColor?: AccentColor;
15
+ }
16
+
17
+ const ACTIVE_BG: Record<AccentColor, string> = {
18
+ emerald: "bg-emerald-500/15",
19
+ violet: "bg-violet-500/15",
20
+ orange: "bg-orange-500/15",
21
+ blue: "bg-blue-500/15",
22
+ pink: "bg-pink-500/15",
23
+ green: "bg-green-500/15",
24
+ amber: "bg-amber-500/15",
25
+ };
26
+
27
+ const ACTIVE_TEXT: Record<AccentColor, string> = {
28
+ emerald: "text-emerald-300",
29
+ violet: "text-violet-300",
30
+ orange: "text-orange-300",
31
+ blue: "text-blue-300",
32
+ pink: "text-pink-300",
33
+ green: "text-green-300",
34
+ amber: "text-amber-300",
35
+ };
36
+
37
+ export function MobileTabBar({
38
+ tabs,
39
+ activeTab,
40
+ onTabChange,
41
+ accentColor = "emerald",
42
+ }: MobileTabBarProps) {
43
+ const containerRef = useRef<HTMLDivElement>(null);
44
+ const [indicator, setIndicator] = useState({ left: 0, width: 0 });
45
+
46
+ useEffect(() => {
47
+ if (!containerRef.current) return;
48
+ const activeIndex = tabs.findIndex(t => t.id === activeTab);
49
+ if (activeIndex < 0) return;
50
+ const buttons = containerRef.current.querySelectorAll<HTMLButtonElement>(
51
+ "[data-tab-id]",
52
+ );
53
+ const btn = buttons[activeIndex];
54
+ if (!btn) return;
55
+ setIndicator({
56
+ left: btn.offsetLeft,
57
+ width: btn.offsetWidth,
58
+ });
59
+ }, [activeTab, tabs]);
60
+
61
+ return (
62
+ <div className="flex-shrink-0 px-3 py-1.5 border-b border-zinc-800 bg-zinc-950">
63
+ <div
64
+ ref={containerRef}
65
+ className="relative flex rounded-lg bg-zinc-900 border border-zinc-800 p-0.5">
66
+ {/* Sliding indicator */}
67
+ <div
68
+ className={`absolute top-0.5 bottom-0.5 rounded-md ${ACTIVE_BG[accentColor]} transition-all duration-200 ease-out`}
69
+ style={{ left: indicator.left, width: indicator.width }}
70
+ />
71
+ {tabs.map(tab => {
72
+ const isActive = tab.id === activeTab;
73
+ return (
74
+ <button
75
+ key={tab.id}
76
+ data-tab-id={tab.id}
77
+ onClick={() => onTabChange(tab.id)}
78
+ className={`relative z-10 flex-1 flex items-center justify-center gap-1.5
79
+ rounded-md py-1.5 px-3 font-mono text-[11px] tracking-wide
80
+ transition-colors duration-150
81
+ ${isActive ? ACTIVE_TEXT[accentColor] : "text-zinc-500 hover:text-zinc-400"}`}>
82
+ {tab.icon}
83
+ {tab.label}
84
+ </button>
85
+ );
86
+ })}
87
+ </div>
88
+ </div>
89
+ );
90
+ }
91
+
92
+ /* ── Preset icon SVGs ────────────────────────────────────────── */
93
+
94
+ export function ChatIcon({ className = "w-3.5 h-3.5" }: { className?: string }) {
95
+ return (
96
+ <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
97
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
98
+ </svg>
99
+ );
100
+ }
101
+
102
+ export function PreviewIcon({ className = "w-3.5 h-3.5" }: { className?: string }) {
103
+ return (
104
+ <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
105
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
106
+ <path strokeLinecap="round" strokeLinejoin="round" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
107
+ </svg>
108
+ );
109
+ }
110
+
111
+ export function CodeIcon({ className = "w-3.5 h-3.5" }: { className?: string }) {
112
+ return (
113
+ <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
114
+ <path strokeLinecap="round" strokeLinejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
115
+ </svg>
116
+ );
117
+ }
118
+
119
+ export function TerminalIcon({ className = "w-3.5 h-3.5" }: { className?: string }) {
120
+ return (
121
+ <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
122
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
123
+ </svg>
124
+ );
125
+ }
126
+
127
+ export function FilesIcon({ className = "w-3.5 h-3.5" }: { className?: string }) {
128
+ return (
129
+ <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
130
+ <path strokeLinecap="round" strokeLinejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
131
+ </svg>
132
+ );
133
+ }
@@ -0,0 +1,101 @@
1
+ import type { AccentColor } from "./types";
2
+
3
+ export interface PanelToggleProps {
4
+ collapsed: boolean;
5
+ onClick: () => void;
6
+ label: string;
7
+ side?: "left" | "right";
8
+ accentColor?: AccentColor;
9
+ }
10
+
11
+ const ACCENT_LABELS: Record<AccentColor, string> = {
12
+ emerald: "text-emerald-500/70",
13
+ violet: "text-violet-500/70",
14
+ orange: "text-orange-500/70",
15
+ blue: "text-blue-500/70",
16
+ pink: "text-pink-500/70",
17
+ green: "text-green-500/70",
18
+ amber: "text-amber-500/70",
19
+ };
20
+
21
+ const ACCENT_CHEVRONS: Record<AccentColor, string> = {
22
+ emerald: "text-emerald-400",
23
+ violet: "text-violet-400",
24
+ orange: "text-orange-400",
25
+ blue: "text-blue-400",
26
+ pink: "text-pink-400",
27
+ green: "text-green-400",
28
+ amber: "text-amber-400",
29
+ };
30
+
31
+ const ACCENT_HOVER_BG: Record<AccentColor, string> = {
32
+ emerald: "hover:bg-emerald-500/5",
33
+ violet: "hover:bg-violet-500/5",
34
+ orange: "hover:bg-orange-500/5",
35
+ blue: "hover:bg-blue-500/5",
36
+ pink: "hover:bg-pink-500/5",
37
+ green: "hover:bg-green-500/5",
38
+ amber: "hover:bg-amber-500/5",
39
+ };
40
+
41
+ export function PanelToggle({
42
+ collapsed,
43
+ onClick,
44
+ label,
45
+ side = "left",
46
+ accentColor = "emerald",
47
+ }: PanelToggleProps) {
48
+ const chevronRight = (
49
+ <>
50
+ <path stroke="none" d="M0 0h24v24H0z" fill="none" />
51
+ <path d="M9 6l6 6l-6 6" />
52
+ </>
53
+ );
54
+ const chevronLeft = (
55
+ <>
56
+ <path stroke="none" d="M0 0h24v24H0z" />
57
+ <path d="m15 6-6 6 6 6" />
58
+ </>
59
+ );
60
+
61
+ if (collapsed) {
62
+ return (
63
+ <button
64
+ onClick={onClick}
65
+ className="flex-shrink-0 w-7 flex flex-col items-center gap-2 pt-3 pb-3
66
+ border-r border-zinc-800 bg-zinc-900/40
67
+ hover:bg-zinc-800/60 transition-colors cursor-pointer"
68
+ title={`Show ${label}`}>
69
+ <svg
70
+ className={`w-3 h-3 ${ACCENT_CHEVRONS[accentColor]}`}
71
+ viewBox="0 0 24 24"
72
+ stroke="currentColor"
73
+ fill="none">
74
+ {side === "left" ? chevronRight : chevronLeft}
75
+ </svg>
76
+ <span
77
+ className={`text-[10px] font-mono tracking-wider uppercase select-none ${ACCENT_LABELS[accentColor]}`}
78
+ style={{ writingMode: "vertical-rl", transform: "rotate(180deg)" }}>
79
+ {label}
80
+ </span>
81
+ </button>
82
+ );
83
+ }
84
+
85
+ return (
86
+ <button
87
+ onClick={onClick}
88
+ className={`flex-shrink-0 w-[9px] flex items-center justify-center
89
+ border-r border-zinc-800 bg-transparent
90
+ ${ACCENT_HOVER_BG[accentColor]} transition-colors cursor-pointer group`}
91
+ title={`Hide ${label}`}>
92
+ <svg
93
+ className={`w-2.5 h-2.5 text-zinc-600 opacity-0 group-hover:opacity-100
94
+ transition-opacity ${ACCENT_CHEVRONS[accentColor]}`}
95
+ viewBox="0 0 16 16"
96
+ fill="currentColor">
97
+ {side === "left" ? chevronLeft : chevronRight}
98
+ </svg>
99
+ </button>
100
+ );
101
+ }
@@ -0,0 +1,178 @@
1
+ import { useState, useRef, useEffect, useCallback } from "react";
2
+ import type { AccentColor, TerminalLine } from "./types";
3
+ import { getTerminalTheme, getLineStyles } from "./theme";
4
+
5
+ export interface TerminalProps {
6
+ lines: TerminalLine[];
7
+ onCommand?: (cmd: string) => Promise<void>;
8
+ onClear: () => void;
9
+ accentColor?: AccentColor;
10
+ variant?: "default" | "compact";
11
+ title?: string;
12
+ lineStyles?: Partial<Record<TerminalLine["type"], string>>;
13
+ }
14
+
15
+ export function Terminal({
16
+ lines,
17
+ onCommand,
18
+ onClear,
19
+ accentColor = "emerald",
20
+ variant = "default",
21
+ title = "Terminal",
22
+ lineStyles: lineStyleOverrides,
23
+ }: TerminalProps) {
24
+ const [input, setInput] = useState("");
25
+ const [history, setHistory] = useState<string[]>([]);
26
+ const [historyIdx, setHistoryIdx] = useState(-1);
27
+ const [isRunning, setIsRunning] = useState(false);
28
+ const scrollRef = useRef<HTMLDivElement>(null);
29
+ const inputRef = useRef<HTMLInputElement>(null);
30
+
31
+ const termTheme = getTerminalTheme(accentColor);
32
+ const styles = getLineStyles(accentColor, lineStyleOverrides);
33
+
34
+ const isCompact = variant === "compact";
35
+ const hasInput = !!onCommand;
36
+
37
+ useEffect(() => {
38
+ const el = scrollRef.current;
39
+ if (el) el.scrollTop = el.scrollHeight;
40
+ }, [lines]);
41
+
42
+ const executeCommand = useCallback(
43
+ async (cmd: string) => {
44
+ if (cmd === "clear") {
45
+ onClear();
46
+ return;
47
+ }
48
+ if (!onCommand) return;
49
+ setIsRunning(true);
50
+ await onCommand(cmd);
51
+ setIsRunning(false);
52
+ inputRef.current?.focus();
53
+ },
54
+ [onCommand, onClear],
55
+ );
56
+
57
+ const handleKeyDown = useCallback(
58
+ (e: React.KeyboardEvent<HTMLInputElement>) => {
59
+ if (e.key === "Enter") {
60
+ e.preventDefault();
61
+ const cmd = e.currentTarget.value.trim();
62
+ if (!cmd || isRunning) return;
63
+ setInput("");
64
+ setHistory(prev => [...prev, cmd]);
65
+ setHistoryIdx(-1);
66
+ executeCommand(cmd);
67
+ } else if (e.key === "ArrowUp") {
68
+ e.preventDefault();
69
+ if (history.length === 0) return;
70
+ const newIdx =
71
+ historyIdx === -1 ? history.length - 1 : Math.max(0, historyIdx - 1);
72
+ setHistoryIdx(newIdx);
73
+ setInput(history[newIdx]);
74
+ } else if (e.key === "ArrowDown") {
75
+ e.preventDefault();
76
+ if (historyIdx === -1) return;
77
+ const newIdx = historyIdx + 1;
78
+ if (newIdx >= history.length) {
79
+ setHistoryIdx(-1);
80
+ setInput("");
81
+ } else {
82
+ setHistoryIdx(newIdx);
83
+ setInput(history[newIdx]);
84
+ }
85
+ }
86
+ },
87
+ [history, historyIdx, isRunning, executeCommand],
88
+ );
89
+
90
+ // Size tokens based on variant
91
+ const headerPy = isCompact ? "py-1" : "py-1.5";
92
+ const dotSize = isCompact ? "h-2 w-2" : "h-2.5 w-2.5";
93
+ const dotOpacity = isCompact ? "bg-red-500/60" : "bg-red-500/70";
94
+ const dotYellow = isCompact ? "bg-yellow-500/60" : "bg-yellow-500/70";
95
+ const dotGreen = isCompact ? "bg-green-500/60" : "bg-green-500/70";
96
+ const titleSize = isCompact ? "text-[10px]" : "text-[11px]";
97
+ const titleMl = isCompact ? "ml-1" : "";
98
+ const clearSize = isCompact ? "text-[10px]" : "text-[11px]";
99
+ const outputClass = isCompact
100
+ ? "flex-1 overflow-y-auto px-3 py-1.5 font-mono text-[11px] leading-4"
101
+ : "flex-1 overflow-y-auto p-3 font-mono text-[13px] leading-5";
102
+ const inputPy = isCompact ? "py-1.5" : "py-2";
103
+ const inputGap = isCompact ? "gap-1.5" : "gap-2";
104
+ const promptSize = isCompact ? "text-[11px]" : "text-sm";
105
+ const inputFontSize = isCompact ? "text-[11px]" : "text-[13px]";
106
+ const spinnerSize = isCompact ? "h-2.5 w-2.5" : "h-3 w-3";
107
+
108
+ return (
109
+ <div className="h-full flex flex-col bg-zinc-950">
110
+ {/* Header */}
111
+ <div
112
+ className={`flex items-center justify-between px-3 ${headerPy} bg-zinc-900/50 border-b border-zinc-800`}>
113
+ <div className="flex items-center gap-1.5">
114
+ <div className="flex gap-1">
115
+ <div className={`${dotSize} rounded-full ${dotOpacity}`} />
116
+ <div className={`${dotSize} rounded-full ${dotYellow}`} />
117
+ <div className={`${dotSize} rounded-full ${dotGreen}`} />
118
+ </div>
119
+ <span
120
+ className={`${titleSize} font-semibold uppercase tracking-wider text-zinc-500 ${titleMl}`}>
121
+ {title}
122
+ </span>
123
+ </div>
124
+ <button
125
+ onClick={onClear}
126
+ className={`${clearSize} text-zinc-600 hover:text-zinc-400 transition-colors`}>
127
+ Clear
128
+ </button>
129
+ </div>
130
+
131
+ {/* Output area */}
132
+ <div
133
+ ref={scrollRef}
134
+ data-testid="terminal-output"
135
+ className={outputClass}>
136
+ {lines.map(line => (
137
+ <div key={line.id} className={styles[line.type]}>
138
+ {line.text.split("\n").map((segment, i) => (
139
+ <div key={i}>{segment || "\u00A0"}</div>
140
+ ))}
141
+ </div>
142
+ ))}
143
+ </div>
144
+
145
+ {/* Input (only when onCommand is provided) */}
146
+ {hasInput && (
147
+ <div className="flex-shrink-0 border-t border-zinc-800">
148
+ <div className={`flex items-center px-3 ${inputPy} ${inputGap}`}>
149
+ <span
150
+ className={`${termTheme.promptColor} font-mono ${promptSize} font-bold select-none`}>
151
+ $
152
+ </span>
153
+ <input
154
+ ref={inputRef}
155
+ type="text"
156
+ value={input}
157
+ onChange={e => setInput(e.target.value)}
158
+ onKeyDown={handleKeyDown}
159
+ disabled={isRunning}
160
+ placeholder={isRunning ? "Running..." : "Type a command..."}
161
+ className={`
162
+ flex-1 bg-transparent text-zinc-200 ${inputFontSize} font-mono
163
+ outline-none placeholder:text-zinc-700
164
+ disabled:opacity-50
165
+ ${termTheme.inputCaret}
166
+ `}
167
+ />
168
+ {isRunning && (
169
+ <div
170
+ className={`${spinnerSize} animate-spin rounded-full border border-zinc-600 ${termTheme.spinnerBorder}`}
171
+ />
172
+ )}
173
+ </div>
174
+ </div>
175
+ )}
176
+ </div>
177
+ );
178
+ }
@@ -0,0 +1,81 @@
1
+ import type { AccentColor } from "./types";
2
+
3
+ export interface ChatTheme {
4
+ userBubble: string;
5
+ assistantBubble: string;
6
+ sendButton: string;
7
+ sendButtonDisabled: string;
8
+ streamingDot: string;
9
+ inputCaret: string;
10
+ modelBadge: string;
11
+ }
12
+
13
+ const CHAT_THEMES: Record<AccentColor, ChatTheme> = {
14
+ emerald: {
15
+ userBubble: "bg-emerald-500/15 border border-emerald-500/20",
16
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
17
+ sendButton: "bg-emerald-500 hover:bg-emerald-600 text-white",
18
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
19
+ streamingDot: "bg-emerald-400",
20
+ inputCaret: "caret-emerald-400",
21
+ modelBadge: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20",
22
+ },
23
+ violet: {
24
+ userBubble: "bg-violet-500/15 border border-violet-500/20",
25
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
26
+ sendButton: "bg-violet-500 hover:bg-violet-600 text-white",
27
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
28
+ streamingDot: "bg-violet-400",
29
+ inputCaret: "caret-violet-400",
30
+ modelBadge: "bg-violet-500/10 text-violet-400 border-violet-500/20",
31
+ },
32
+ orange: {
33
+ userBubble: "bg-orange-500/15 border border-orange-500/20",
34
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
35
+ sendButton: "bg-orange-500 hover:bg-orange-600 text-white",
36
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
37
+ streamingDot: "bg-orange-400",
38
+ inputCaret: "caret-orange-400",
39
+ modelBadge: "bg-orange-500/10 text-orange-400 border-orange-500/20",
40
+ },
41
+ blue: {
42
+ userBubble: "bg-blue-500/15 border border-blue-500/20",
43
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
44
+ sendButton: "bg-blue-500 hover:bg-blue-600 text-white",
45
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
46
+ streamingDot: "bg-blue-400",
47
+ inputCaret: "caret-blue-400",
48
+ modelBadge: "bg-blue-500/10 text-blue-400 border-blue-500/20",
49
+ },
50
+ pink: {
51
+ userBubble: "bg-pink-500/15 border border-pink-500/20",
52
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
53
+ sendButton: "bg-pink-500 hover:bg-pink-600 text-white",
54
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
55
+ streamingDot: "bg-pink-400",
56
+ inputCaret: "caret-pink-400",
57
+ modelBadge: "bg-pink-500/10 text-pink-400 border-pink-500/20",
58
+ },
59
+ green: {
60
+ userBubble: "bg-green-500/15 border border-green-500/20",
61
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
62
+ sendButton: "bg-green-500 hover:bg-green-600 text-white",
63
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
64
+ streamingDot: "bg-green-400",
65
+ inputCaret: "caret-green-400",
66
+ modelBadge: "bg-green-500/10 text-green-400 border-green-500/20",
67
+ },
68
+ amber: {
69
+ userBubble: "bg-amber-500/15 border border-amber-500/20",
70
+ assistantBubble: "bg-zinc-800/50 border border-zinc-700/50",
71
+ sendButton: "bg-amber-500 hover:bg-amber-600 text-white",
72
+ sendButtonDisabled: "bg-zinc-700 text-zinc-500",
73
+ streamingDot: "bg-amber-400",
74
+ inputCaret: "caret-amber-400",
75
+ modelBadge: "bg-amber-500/10 text-amber-400 border-amber-500/20",
76
+ },
77
+ };
78
+
79
+ export function getChatTheme(color: AccentColor): ChatTheme {
80
+ return CHAT_THEMES[color];
81
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ export { CodeEditor } from "./CodeEditor";
2
+ export type { CodeEditorProps } from "./CodeEditor";
3
+ export { Terminal } from "./Terminal";
4
+ export type { TerminalProps } from "./Terminal";
5
+ export { BrowserWindow } from "./BrowserWindow";
6
+ export type { BrowserWindowProps, InspectedElement } from "./BrowserWindow";
7
+ export { FileExplorer } from "./FileExplorer";
8
+ export type { FileExplorerProps } from "./FileExplorer";
9
+ export { PanelToggle } from "./PanelToggle";
10
+ export type { PanelToggleProps } from "./PanelToggle";
11
+ export {
12
+ MobileTabBar,
13
+ ChatIcon,
14
+ PreviewIcon,
15
+ CodeIcon,
16
+ TerminalIcon,
17
+ FilesIcon,
18
+ } from "./MobileTabBar";
19
+ export type { MobileTabBarProps, TabItem } from "./MobileTabBar";
20
+ export { useMediaQuery } from "./useMediaQuery";
21
+ export type { TerminalLine, AccentColor, FileEntry } from "./types";
22
+ export { AIChatPanel } from "./AIChatPanel";
23
+ export type { AIChatPanelProps, ChatMessage } from "./AIChatPanel";
24
+ export { getLanguageLabel } from "./language-labels";
25
+ export {
26
+ getEditorTheme,
27
+ getTerminalTheme,
28
+ getLineStyles,
29
+ getFileExplorerTheme,
30
+ } from "./theme";
31
+ export type { EditorTheme, TerminalTheme, FileExplorerTheme } from "./theme";
32
+ export { getChatTheme } from "./chat-theme";
33
+ export type { ChatTheme } from "./chat-theme";
@@ -0,0 +1,26 @@
1
+ const LANGUAGE_MAP: Record<string, string> = {
2
+ js: "JavaScript",
3
+ jsx: "JSX",
4
+ mjs: "JavaScript",
5
+ cjs: "JavaScript",
6
+ ts: "TypeScript",
7
+ tsx: "TSX",
8
+ mts: "TypeScript",
9
+ cts: "TypeScript",
10
+ json: "JSON",
11
+ md: "Markdown",
12
+ html: "HTML",
13
+ css: "CSS",
14
+ vue: "Vue SFC",
15
+ svelte: "Svelte",
16
+ astro: "Astro",
17
+ yaml: "YAML",
18
+ yml: "YAML",
19
+ toml: "TOML",
20
+ sh: "Shell",
21
+ };
22
+
23
+ export function getLanguageLabel(filename: string): string {
24
+ const ext = filename.split(".").pop()?.toLowerCase();
25
+ return LANGUAGE_MAP[ext || ""] || "Plain Text";
26
+ }
@@ -0,0 +1,27 @@
1
+ const SHIKI_LANG_MAP: Record<string, string> = {
2
+ js: "javascript",
3
+ mjs: "javascript",
4
+ cjs: "javascript",
5
+ ts: "typescript",
6
+ mts: "typescript",
7
+ cts: "typescript",
8
+ jsx: "jsx",
9
+ tsx: "tsx",
10
+ json: "json",
11
+ html: "html",
12
+ css: "css",
13
+ md: "markdown",
14
+ vue: "vue",
15
+ svelte: "svelte",
16
+ astro: "astro",
17
+ yaml: "yaml",
18
+ yml: "yaml",
19
+ toml: "toml",
20
+ sh: "shellscript",
21
+ };
22
+
23
+ export function getShikiLang(filename: string | null): string {
24
+ if (!filename) return "plaintext";
25
+ const ext = filename.split(".").pop()?.toLowerCase();
26
+ return SHIKI_LANG_MAP[ext || ""] || "plaintext";
27
+ }