@optilogic/core 1.0.0-beta.9 → 1.0.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.cjs +1115 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +326 -1
- package/dist/index.d.ts +326 -1
- package/dist/index.js +1097 -46
- package/dist/index.js.map +1 -1
- package/dist/styles.css +22 -0
- package/dist/tailwind-preset.cjs +17 -2
- package/dist/tailwind-preset.cjs.map +1 -1
- package/dist/tailwind-preset.js +17 -2
- package/dist/tailwind-preset.js.map +1 -1
- package/package.json +15 -1
- package/src/components/autocomplete.tsx +2 -1
- package/src/components/button.tsx +10 -8
- package/src/components/calendar.tsx +7 -7
- package/src/components/data-grid/DataGrid.tsx +6 -1
- package/src/components/data-grid/components/CellEditor.tsx +3 -3
- package/src/components/data-grid/hooks/useDataGridState.ts +18 -3
- package/src/components/data-grid/types.ts +4 -0
- package/src/components/data-grid/utils/dataProcessing.ts +40 -11
- package/src/components/date-picker.tsx +2 -1
- package/src/components/dropdown-menu.tsx +1 -1
- package/src/components/file-view/FileView.tsx +147 -0
- package/src/components/file-view/components/CodeRenderer.tsx +97 -0
- package/src/components/file-view/components/CsvRenderer.tsx +127 -0
- package/src/components/file-view/components/HtmlRenderer.tsx +24 -0
- package/src/components/file-view/components/ImageRenderer.tsx +67 -0
- package/src/components/file-view/components/MarkdownRenderer.tsx +304 -0
- package/src/components/file-view/components/PlainTextRenderer.tsx +27 -0
- package/src/components/file-view/components/index.ts +4 -0
- package/src/components/file-view/hooks/index.ts +5 -0
- package/src/components/file-view/hooks/useContentType.ts +34 -0
- package/src/components/file-view/hooks/useDarkMode.ts +62 -0
- package/src/components/file-view/hooks/useHighlightedTokens.ts +83 -0
- package/src/components/file-view/hooks/useShikiHighlighter.ts +69 -0
- package/src/components/file-view/index.ts +47 -0
- package/src/components/file-view/types.ts +180 -0
- package/src/components/file-view/utils/contentTypeDetection.ts +157 -0
- package/src/components/file-view/utils/index.ts +12 -0
- package/src/components/file-view/utils/languageMapping.ts +78 -0
- package/src/components/file-view/utils/rendererRegistry.ts +42 -0
- package/src/components/input.tsx +1 -1
- package/src/components/popover.tsx +1 -1
- package/src/components/select.tsx +1 -1
- package/src/components/switch.tsx +5 -3
- package/src/components/textarea.tsx +1 -1
- package/src/index.ts +39 -0
- package/src/styles.css +22 -0
- package/src/tailwind-preset.ts +17 -1
- package/src/theme/index.ts +5 -0
- package/src/theme/presets.ts +112 -2
- package/src/theme/types.ts +35 -0
- package/src/theme/utils.ts +231 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
|
|
3
|
+
// ============ CONTENT TYPES ============
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Built-in content types that FileView can render.
|
|
7
|
+
*/
|
|
8
|
+
export type BuiltInContentType =
|
|
9
|
+
| "code"
|
|
10
|
+
| "markdown"
|
|
11
|
+
| "image"
|
|
12
|
+
| "pdf"
|
|
13
|
+
| "csv"
|
|
14
|
+
| "html"
|
|
15
|
+
| "plaintext"
|
|
16
|
+
| "unknown";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Content type - either a built-in type or a custom string.
|
|
20
|
+
* The `(string & {})` pattern preserves autocomplete for built-in types
|
|
21
|
+
* while accepting arbitrary strings for custom renderers.
|
|
22
|
+
*/
|
|
23
|
+
export type ContentType = BuiltInContentType | (string & {});
|
|
24
|
+
|
|
25
|
+
// ============ IMAGE RESOLUTION ============
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Callback to resolve image `src` values to renderable URLs.
|
|
29
|
+
* Called for non-remote, non-data-URI image sources in markdown content.
|
|
30
|
+
* Can return a blob URL, data URL, or any string that works as an `<img>` src.
|
|
31
|
+
*/
|
|
32
|
+
export type ResolveImageUrl = (src: string) => Promise<string> | string;
|
|
33
|
+
|
|
34
|
+
// ============ RENDERER TYPES ============
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Props passed to every renderer component.
|
|
38
|
+
* All renderers receive a consistent contract regardless of content type.
|
|
39
|
+
*/
|
|
40
|
+
export interface FileRendererProps {
|
|
41
|
+
/** Text content of the file. Null when content is binary/URL-based. */
|
|
42
|
+
content: string | null;
|
|
43
|
+
/** URL for binary content (images, PDFs). Null for text-based content. */
|
|
44
|
+
url: string | null;
|
|
45
|
+
/** The file name, used for display and language detection. */
|
|
46
|
+
fileName: string;
|
|
47
|
+
/** The resolved content type. */
|
|
48
|
+
contentType: ContentType;
|
|
49
|
+
/** Optional className for the renderer container. */
|
|
50
|
+
className?: string;
|
|
51
|
+
/** Optional callback to resolve relative image URLs in markdown content. */
|
|
52
|
+
resolveImageUrl?: ResolveImageUrl;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A renderer component that can render file content.
|
|
57
|
+
*/
|
|
58
|
+
export type FileRenderer = React.ComponentType<FileRendererProps>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Registry mapping content types to renderer components.
|
|
62
|
+
* Users can override individual renderers by providing their own map.
|
|
63
|
+
*/
|
|
64
|
+
export type RendererRegistry = Partial<Record<ContentType, FileRenderer>>;
|
|
65
|
+
|
|
66
|
+
// ============ STATE TYPES ============
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Error information for the FileView.
|
|
70
|
+
*/
|
|
71
|
+
export interface FileViewError {
|
|
72
|
+
/** Error message to display. */
|
|
73
|
+
message: string;
|
|
74
|
+
/** Optional retry callback. If provided, a retry button is shown. */
|
|
75
|
+
onRetry?: () => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ============ COMPONENT PROPS ============
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Props for the FileView component.
|
|
82
|
+
*/
|
|
83
|
+
export interface FileViewProps {
|
|
84
|
+
// ============ CONTENT ============
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Text content of the file.
|
|
88
|
+
* For text-based files (code, markdown, plaintext, csv), pass the string content.
|
|
89
|
+
* For binary files (images, PDFs), pass null and use `url` instead.
|
|
90
|
+
*/
|
|
91
|
+
content: string | null;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* URL for binary content.
|
|
95
|
+
* Used for images, PDFs, and other non-text content.
|
|
96
|
+
* Can be an object URL, data URL, or HTTP URL.
|
|
97
|
+
*/
|
|
98
|
+
url?: string | null;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The file name including extension.
|
|
102
|
+
* Used for content type detection and display context.
|
|
103
|
+
* @example "README.md", "app.tsx", "photo.png"
|
|
104
|
+
*/
|
|
105
|
+
fileName: string;
|
|
106
|
+
|
|
107
|
+
// ============ CONTENT TYPE ============
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Explicit content type override.
|
|
111
|
+
* When provided, bypasses automatic detection from fileName.
|
|
112
|
+
*/
|
|
113
|
+
contentType?: ContentType;
|
|
114
|
+
|
|
115
|
+
// ============ RENDERERS ============
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Custom renderer overrides.
|
|
119
|
+
* Merges with (and overrides) the default renderer registry.
|
|
120
|
+
* Only the types you specify are overridden; others use defaults.
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* renderers={{ code: MyCustomCodeRenderer }}
|
|
124
|
+
*/
|
|
125
|
+
renderers?: RendererRegistry;
|
|
126
|
+
|
|
127
|
+
// ============ STATE ============
|
|
128
|
+
|
|
129
|
+
/** Loading state. When true, shows loading indicator. */
|
|
130
|
+
loading?: boolean;
|
|
131
|
+
|
|
132
|
+
/** Error state. When provided, shows error message with optional retry. */
|
|
133
|
+
error?: FileViewError | null;
|
|
134
|
+
|
|
135
|
+
// ============ CUSTOMIZATION ============
|
|
136
|
+
|
|
137
|
+
/** Custom loading component. Replaces the default spinner. */
|
|
138
|
+
loadingComponent?: React.ReactNode;
|
|
139
|
+
|
|
140
|
+
/** Custom empty state component. Shown when content is null/empty and not loading/error. */
|
|
141
|
+
emptyComponent?: React.ReactNode;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Empty state message. Used when no custom emptyComponent is provided.
|
|
145
|
+
* @default "No content to display"
|
|
146
|
+
*/
|
|
147
|
+
emptyMessage?: string;
|
|
148
|
+
|
|
149
|
+
/** Custom error component. Replaces the default error display. */
|
|
150
|
+
errorComponent?: React.ComponentType<{ error: FileViewError }>;
|
|
151
|
+
|
|
152
|
+
// ============ STYLING ============
|
|
153
|
+
|
|
154
|
+
/** Additional class name for the outermost container. */
|
|
155
|
+
className?: string;
|
|
156
|
+
|
|
157
|
+
/** Additional class name passed to the active renderer. */
|
|
158
|
+
rendererClassName?: string;
|
|
159
|
+
|
|
160
|
+
// ============ IMAGE RESOLUTION ============
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Optional callback to resolve image `src` values to renderable URLs.
|
|
164
|
+
* Used by MarkdownRenderer for relative/non-remote image paths.
|
|
165
|
+
* Remote URLs (http/https) and data URIs are passed through unchanged.
|
|
166
|
+
*/
|
|
167
|
+
resolveImageUrl?: ResolveImageUrl;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============ DETECTION TYPES ============
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Content type detection result.
|
|
174
|
+
*/
|
|
175
|
+
export interface ContentTypeDetectionResult {
|
|
176
|
+
/** The detected content type. */
|
|
177
|
+
type: ContentType;
|
|
178
|
+
/** The file extension that was matched, if any. */
|
|
179
|
+
extension: string | null;
|
|
180
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import type { ContentType, ContentTypeDetectionResult } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Extension-to-content-type mapping.
|
|
5
|
+
*/
|
|
6
|
+
const EXTENSION_MAP: Record<string, ContentType> = {
|
|
7
|
+
// Code / programming languages
|
|
8
|
+
ts: "code",
|
|
9
|
+
tsx: "code",
|
|
10
|
+
js: "code",
|
|
11
|
+
jsx: "code",
|
|
12
|
+
mjs: "code",
|
|
13
|
+
cjs: "code",
|
|
14
|
+
py: "code",
|
|
15
|
+
rb: "code",
|
|
16
|
+
go: "code",
|
|
17
|
+
rs: "code",
|
|
18
|
+
java: "code",
|
|
19
|
+
c: "code",
|
|
20
|
+
cpp: "code",
|
|
21
|
+
h: "code",
|
|
22
|
+
hpp: "code",
|
|
23
|
+
cs: "code",
|
|
24
|
+
php: "code",
|
|
25
|
+
swift: "code",
|
|
26
|
+
kt: "code",
|
|
27
|
+
scala: "code",
|
|
28
|
+
r: "code",
|
|
29
|
+
sql: "code",
|
|
30
|
+
sh: "code",
|
|
31
|
+
bash: "code",
|
|
32
|
+
zsh: "code",
|
|
33
|
+
ps1: "code",
|
|
34
|
+
bat: "code",
|
|
35
|
+
lua: "code",
|
|
36
|
+
perl: "code",
|
|
37
|
+
pl: "code",
|
|
38
|
+
// Config / data (code-rendered)
|
|
39
|
+
json: "code",
|
|
40
|
+
yaml: "code",
|
|
41
|
+
yml: "code",
|
|
42
|
+
toml: "code",
|
|
43
|
+
xml: "code",
|
|
44
|
+
html: "html",
|
|
45
|
+
htm: "html",
|
|
46
|
+
css: "code",
|
|
47
|
+
scss: "code",
|
|
48
|
+
sass: "code",
|
|
49
|
+
less: "code",
|
|
50
|
+
graphql: "code",
|
|
51
|
+
gql: "code",
|
|
52
|
+
// Optimization / domain-specific
|
|
53
|
+
lp: "code",
|
|
54
|
+
dat: "code",
|
|
55
|
+
// Markdown
|
|
56
|
+
md: "markdown",
|
|
57
|
+
mdx: "markdown",
|
|
58
|
+
// Images
|
|
59
|
+
png: "image",
|
|
60
|
+
jpg: "image",
|
|
61
|
+
jpeg: "image",
|
|
62
|
+
gif: "image",
|
|
63
|
+
svg: "image",
|
|
64
|
+
webp: "image",
|
|
65
|
+
ico: "image",
|
|
66
|
+
bmp: "image",
|
|
67
|
+
// PDF
|
|
68
|
+
pdf: "pdf",
|
|
69
|
+
// CSV
|
|
70
|
+
csv: "csv",
|
|
71
|
+
tsv: "csv",
|
|
72
|
+
// Plain text
|
|
73
|
+
txt: "plaintext",
|
|
74
|
+
log: "plaintext",
|
|
75
|
+
env: "plaintext",
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Special file names (without extension) that map to content types.
|
|
80
|
+
*/
|
|
81
|
+
const SPECIAL_FILES: Record<string, ContentType> = {
|
|
82
|
+
dockerfile: "code",
|
|
83
|
+
makefile: "code",
|
|
84
|
+
rakefile: "code",
|
|
85
|
+
gemfile: "code",
|
|
86
|
+
procfile: "code",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Extract the file extension from a file name.
|
|
91
|
+
* Handles dotfiles (e.g., ".gitignore" -> "gitignore") and
|
|
92
|
+
* compound extensions (e.g., "file.test.ts" -> "ts").
|
|
93
|
+
*/
|
|
94
|
+
export function getFileExtension(fileName: string): string | null {
|
|
95
|
+
if (!fileName) return null;
|
|
96
|
+
|
|
97
|
+
const baseName = fileName.split("/").pop() || fileName;
|
|
98
|
+
|
|
99
|
+
// Dotfile with no other extension (e.g., ".gitignore")
|
|
100
|
+
if (baseName.startsWith(".") && !baseName.slice(1).includes(".")) {
|
|
101
|
+
return baseName.slice(1).toLowerCase();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const lastDot = baseName.lastIndexOf(".");
|
|
105
|
+
if (lastDot === -1 || lastDot === baseName.length - 1) return null;
|
|
106
|
+
|
|
107
|
+
return baseName.slice(lastDot + 1).toLowerCase();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Detect content type from a file name.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* detectContentType("app.tsx") // { type: "code", extension: "tsx" }
|
|
115
|
+
* detectContentType("README.md") // { type: "markdown", extension: "md" }
|
|
116
|
+
* detectContentType("photo.png") // { type: "image", extension: "png" }
|
|
117
|
+
* detectContentType("unknown.xyz") // { type: "unknown", extension: "xyz" }
|
|
118
|
+
*/
|
|
119
|
+
export function detectContentType(
|
|
120
|
+
fileName: string,
|
|
121
|
+
): ContentTypeDetectionResult {
|
|
122
|
+
// Check special file names first
|
|
123
|
+
const baseName = (fileName.split("/").pop() || fileName).toLowerCase();
|
|
124
|
+
const specialType = SPECIAL_FILES[baseName];
|
|
125
|
+
if (specialType) {
|
|
126
|
+
return { type: specialType, extension: baseName };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const extension = getFileExtension(fileName);
|
|
130
|
+
if (!extension) {
|
|
131
|
+
return { type: "unknown", extension: null };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const type = EXTENSION_MAP[extension] ?? "unknown";
|
|
135
|
+
return { type, extension };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check if a content type is text-based (uses `content` string).
|
|
140
|
+
*/
|
|
141
|
+
export function isTextContentType(contentType: ContentType): boolean {
|
|
142
|
+
return (
|
|
143
|
+
contentType === "code" ||
|
|
144
|
+
contentType === "markdown" ||
|
|
145
|
+
contentType === "plaintext" ||
|
|
146
|
+
contentType === "csv" ||
|
|
147
|
+
contentType === "html" ||
|
|
148
|
+
contentType === "unknown"
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check if a content type is URL-based (uses `url` string).
|
|
154
|
+
*/
|
|
155
|
+
export function isUrlContentType(contentType: ContentType): boolean {
|
|
156
|
+
return contentType === "image" || contentType === "pdf";
|
|
157
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps file extensions to shiki language IDs.
|
|
3
|
+
*
|
|
4
|
+
* Most extensions work directly as shiki lang IDs. This map only
|
|
5
|
+
* contains edge-cases where the extension differs from the shiki ID.
|
|
6
|
+
*/
|
|
7
|
+
const EXTENSION_OVERRIDES: Record<string, string> = {
|
|
8
|
+
yml: "yaml",
|
|
9
|
+
htm: "html",
|
|
10
|
+
cjs: "javascript",
|
|
11
|
+
mjs: "javascript",
|
|
12
|
+
jsx: "jsx",
|
|
13
|
+
tsx: "tsx",
|
|
14
|
+
h: "c",
|
|
15
|
+
hpp: "cpp",
|
|
16
|
+
cs: "csharp",
|
|
17
|
+
rb: "ruby",
|
|
18
|
+
sh: "shellscript",
|
|
19
|
+
bash: "shellscript",
|
|
20
|
+
zsh: "shellscript",
|
|
21
|
+
ps1: "powershell",
|
|
22
|
+
bat: "bat",
|
|
23
|
+
kt: "kotlin",
|
|
24
|
+
rs: "rust",
|
|
25
|
+
gql: "graphql",
|
|
26
|
+
pl: "perl",
|
|
27
|
+
sass: "sass",
|
|
28
|
+
scss: "scss",
|
|
29
|
+
less: "less",
|
|
30
|
+
// Domain-specific / data formats without shiki support
|
|
31
|
+
lp: "text",
|
|
32
|
+
dat: "text",
|
|
33
|
+
env: "shellscript",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Special file names (without extension) mapped to shiki language IDs.
|
|
38
|
+
*/
|
|
39
|
+
const SPECIAL_FILE_LANGUAGES: Record<string, string> = {
|
|
40
|
+
dockerfile: "dockerfile",
|
|
41
|
+
makefile: "makefile",
|
|
42
|
+
rakefile: "ruby",
|
|
43
|
+
gemfile: "ruby",
|
|
44
|
+
procfile: "yaml",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the shiki language ID for a given file name.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* getLanguageFromFileName("app.tsx") // "tsx"
|
|
52
|
+
* getLanguageFromFileName("config.yml") // "yaml"
|
|
53
|
+
* getLanguageFromFileName("Dockerfile") // "dockerfile"
|
|
54
|
+
* getLanguageFromFileName("unknown.xyz") // "text"
|
|
55
|
+
*/
|
|
56
|
+
export function getLanguageFromFileName(fileName: string): string {
|
|
57
|
+
const baseName = (fileName.split("/").pop() || fileName).toLowerCase();
|
|
58
|
+
|
|
59
|
+
// Check special file names
|
|
60
|
+
const specialLang = SPECIAL_FILE_LANGUAGES[baseName];
|
|
61
|
+
if (specialLang) return specialLang;
|
|
62
|
+
|
|
63
|
+
// Extract extension
|
|
64
|
+
let ext: string | null = null;
|
|
65
|
+
if (baseName.startsWith(".") && !baseName.slice(1).includes(".")) {
|
|
66
|
+
ext = baseName.slice(1);
|
|
67
|
+
} else {
|
|
68
|
+
const lastDot = baseName.lastIndexOf(".");
|
|
69
|
+
if (lastDot !== -1 && lastDot !== baseName.length - 1) {
|
|
70
|
+
ext = baseName.slice(lastDot + 1);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!ext) return "text";
|
|
75
|
+
|
|
76
|
+
// Check overrides first, then use extension directly as shiki lang ID
|
|
77
|
+
return EXTENSION_OVERRIDES[ext] ?? ext;
|
|
78
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { RendererRegistry, FileRenderer } from "../types";
|
|
2
|
+
import { CodeRenderer } from "../components/CodeRenderer";
|
|
3
|
+
import { MarkdownRenderer } from "../components/MarkdownRenderer";
|
|
4
|
+
import { ImageRenderer } from "../components/ImageRenderer";
|
|
5
|
+
import { PlainTextRenderer } from "../components/PlainTextRenderer";
|
|
6
|
+
import { CsvRenderer } from "../components/CsvRenderer";
|
|
7
|
+
import { HtmlRenderer } from "../components/HtmlRenderer";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default renderer registry.
|
|
11
|
+
* Maps built-in content types to their default renderer components.
|
|
12
|
+
*/
|
|
13
|
+
export const DEFAULT_RENDERERS: RendererRegistry = {
|
|
14
|
+
code: CodeRenderer,
|
|
15
|
+
markdown: MarkdownRenderer,
|
|
16
|
+
image: ImageRenderer,
|
|
17
|
+
plaintext: PlainTextRenderer,
|
|
18
|
+
csv: CsvRenderer,
|
|
19
|
+
html: HtmlRenderer,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Merge user-provided renderers with defaults.
|
|
24
|
+
* User renderers override defaults for the same content type.
|
|
25
|
+
*/
|
|
26
|
+
export function mergeRenderers(
|
|
27
|
+
userRenderers?: RendererRegistry,
|
|
28
|
+
): RendererRegistry {
|
|
29
|
+
if (!userRenderers) return DEFAULT_RENDERERS;
|
|
30
|
+
return { ...DEFAULT_RENDERERS, ...userRenderers };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve a renderer for a given content type.
|
|
35
|
+
* Falls back to PlainTextRenderer if no match found.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveRenderer(
|
|
38
|
+
registry: RendererRegistry,
|
|
39
|
+
contentType: string,
|
|
40
|
+
): FileRenderer {
|
|
41
|
+
return registry[contentType] ?? registry["plaintext"] ?? PlainTextRenderer;
|
|
42
|
+
}
|
package/src/components/input.tsx
CHANGED
|
@@ -26,7 +26,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|
|
26
26
|
<input
|
|
27
27
|
type={type}
|
|
28
28
|
className={cn(
|
|
29
|
-
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
29
|
+
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground hover:border-input-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-input md:text-sm",
|
|
30
30
|
className
|
|
31
31
|
)}
|
|
32
32
|
ref={ref}
|
|
@@ -44,7 +44,7 @@ const PopoverContent = React.forwardRef<
|
|
|
44
44
|
align={align}
|
|
45
45
|
sideOffset={sideOffset}
|
|
46
46
|
className={cn(
|
|
47
|
-
"z-50 w-
|
|
47
|
+
"z-50 w-auto max-w-[90vw] rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none",
|
|
48
48
|
// Animation
|
|
49
49
|
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
|
50
50
|
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
@@ -20,7 +20,7 @@ const SelectTrigger = React.forwardRef<
|
|
|
20
20
|
<SelectPrimitive.Trigger
|
|
21
21
|
ref={ref}
|
|
22
22
|
className={cn(
|
|
23
|
-
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
|
23
|
+
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background data-[placeholder]:text-muted-foreground hover:border-input-hover focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-input [&>span]:line-clamp-1",
|
|
24
24
|
className
|
|
25
25
|
)}
|
|
26
26
|
{...props}
|
|
@@ -32,9 +32,11 @@ const Switch = React.forwardRef<
|
|
|
32
32
|
// Focus styles
|
|
33
33
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
|
34
34
|
// Disabled styles
|
|
35
|
-
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
35
|
+
"disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-muted",
|
|
36
36
|
// Unchecked state
|
|
37
|
-
"bg-
|
|
37
|
+
"bg-toggle-track",
|
|
38
|
+
// Hover
|
|
39
|
+
"hover:bg-toggle-track/80 data-[state=checked]:hover:bg-primary/80",
|
|
38
40
|
// Checked state
|
|
39
41
|
"data-[state=checked]:bg-primary",
|
|
40
42
|
className
|
|
@@ -46,7 +48,7 @@ const Switch = React.forwardRef<
|
|
|
46
48
|
className={cn(
|
|
47
49
|
// Base styles
|
|
48
50
|
"pointer-events-none block h-4 w-4 rounded-full",
|
|
49
|
-
"bg-
|
|
51
|
+
"bg-toggle-track-foreground shadow-lg ring-0",
|
|
50
52
|
"transition-transform",
|
|
51
53
|
// Position based on state
|
|
52
54
|
"data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
|
|
@@ -22,7 +22,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
22
22
|
return (
|
|
23
23
|
<textarea
|
|
24
24
|
className={cn(
|
|
25
|
-
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
25
|
+
"flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground hover:border-input-hover focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-input md:text-sm",
|
|
26
26
|
className
|
|
27
27
|
)}
|
|
28
28
|
ref={ref}
|
package/src/index.ts
CHANGED
|
@@ -321,6 +321,45 @@ export {
|
|
|
321
321
|
type UseContextMenuResult,
|
|
322
322
|
} from "./components/context-menu";
|
|
323
323
|
|
|
324
|
+
// FileView - Configurable file viewer with pluggable renderers
|
|
325
|
+
export {
|
|
326
|
+
// Main component
|
|
327
|
+
FileView,
|
|
328
|
+
|
|
329
|
+
// Types
|
|
330
|
+
type BuiltInContentType,
|
|
331
|
+
type ContentType,
|
|
332
|
+
type ContentTypeDetectionResult,
|
|
333
|
+
type FileRendererProps,
|
|
334
|
+
type FileRenderer,
|
|
335
|
+
type RendererRegistry,
|
|
336
|
+
type FileViewError,
|
|
337
|
+
type FileViewProps,
|
|
338
|
+
type ResolveImageUrl,
|
|
339
|
+
|
|
340
|
+
// Sub-components (for custom composition or standalone use)
|
|
341
|
+
CodeRenderer,
|
|
342
|
+
MarkdownRenderer,
|
|
343
|
+
ImageRenderer,
|
|
344
|
+
PlainTextRenderer,
|
|
345
|
+
CsvRenderer,
|
|
346
|
+
HtmlRenderer,
|
|
347
|
+
|
|
348
|
+
// Hooks
|
|
349
|
+
useContentType,
|
|
350
|
+
type UseContentTypeOptions,
|
|
351
|
+
type UseContentTypeReturn,
|
|
352
|
+
|
|
353
|
+
// Utilities
|
|
354
|
+
detectContentType,
|
|
355
|
+
getFileExtension,
|
|
356
|
+
isTextContentType,
|
|
357
|
+
isUrlContentType,
|
|
358
|
+
DEFAULT_RENDERERS,
|
|
359
|
+
mergeRenderers,
|
|
360
|
+
resolveRenderer,
|
|
361
|
+
} from "./components/file-view/index";
|
|
362
|
+
|
|
324
363
|
// Theme system
|
|
325
364
|
export {
|
|
326
365
|
// Types
|
package/src/styles.css
CHANGED
|
@@ -40,15 +40,26 @@
|
|
|
40
40
|
--border: 214.3 31.8% 91.4%;
|
|
41
41
|
--input: 214.3 31.8% 91.4%;
|
|
42
42
|
--ring: 222.2 84% 4.9%;
|
|
43
|
+
--toggle-track: var(--muted);
|
|
44
|
+
--toggle-track-foreground: var(--background);
|
|
45
|
+
--input-hover: var(--foreground);
|
|
43
46
|
--divider: 214.3 31.8% 91.4%;
|
|
44
47
|
--chip: 210 40% 96.1%;
|
|
45
48
|
--chip-foreground: 222.2 47.4% 11.2%;
|
|
49
|
+
--disabled-opacity: 0.5;
|
|
46
50
|
--radius: 0.5rem;
|
|
47
51
|
--chart-1: 12 76% 61%;
|
|
48
52
|
--chart-2: 173 58% 39%;
|
|
49
53
|
--chart-3: 197 37% 24%;
|
|
50
54
|
--chart-4: 43 74% 66%;
|
|
51
55
|
--chart-5: 27 87% 67%;
|
|
56
|
+
--chart-6: 262 60% 55%;
|
|
57
|
+
--chart-7: 142 60% 45%;
|
|
58
|
+
--chart-8: 350 65% 55%;
|
|
59
|
+
--chart-9: 200 70% 50%;
|
|
60
|
+
--chart-10: 60 65% 50%;
|
|
61
|
+
--chart-11: 310 55% 50%;
|
|
62
|
+
--chart-12: 180 50% 45%;
|
|
52
63
|
}
|
|
53
64
|
|
|
54
65
|
.dark {
|
|
@@ -75,14 +86,25 @@
|
|
|
75
86
|
--border: 217.2 32.6% 17.5%;
|
|
76
87
|
--input: 217.2 32.6% 17.5%;
|
|
77
88
|
--ring: 212.7 26.8% 83.9%;
|
|
89
|
+
--toggle-track: var(--muted);
|
|
90
|
+
--toggle-track-foreground: var(--background);
|
|
91
|
+
--input-hover: var(--foreground);
|
|
78
92
|
--divider: 217.2 32.6% 17.5%;
|
|
79
93
|
--chip: 217.2 32.6% 17.5%;
|
|
80
94
|
--chip-foreground: 210 40% 98%;
|
|
95
|
+
--disabled-opacity: 0.5;
|
|
81
96
|
--chart-1: 220 70% 50%;
|
|
82
97
|
--chart-2: 160 60% 45%;
|
|
83
98
|
--chart-3: 30 80% 55%;
|
|
84
99
|
--chart-4: 280 65% 60%;
|
|
85
100
|
--chart-5: 340 75% 55%;
|
|
101
|
+
--chart-6: 120 55% 50%;
|
|
102
|
+
--chart-7: 200 75% 55%;
|
|
103
|
+
--chart-8: 50 80% 55%;
|
|
104
|
+
--chart-9: 0 70% 55%;
|
|
105
|
+
--chart-10: 260 65% 60%;
|
|
106
|
+
--chart-11: 180 60% 50%;
|
|
107
|
+
--chart-12: 90 55% 55%;
|
|
86
108
|
}
|
|
87
109
|
}
|
|
88
110
|
|
package/src/tailwind-preset.ts
CHANGED
|
@@ -83,10 +83,19 @@ export const optiUiPreset: Partial<Config> = {
|
|
|
83
83
|
|
|
84
84
|
// Border, input, ring
|
|
85
85
|
border: "hsl(var(--border))",
|
|
86
|
-
input:
|
|
86
|
+
input: {
|
|
87
|
+
DEFAULT: "hsl(var(--input))",
|
|
88
|
+
hover: "hsl(var(--input-hover))",
|
|
89
|
+
},
|
|
87
90
|
ring: "hsl(var(--ring))",
|
|
88
91
|
divider: "hsl(var(--divider))",
|
|
89
92
|
|
|
93
|
+
// Toggle/switch track
|
|
94
|
+
"toggle-track": {
|
|
95
|
+
DEFAULT: "hsl(var(--toggle-track))",
|
|
96
|
+
foreground: "hsl(var(--toggle-track-foreground))",
|
|
97
|
+
},
|
|
98
|
+
|
|
90
99
|
// Chip
|
|
91
100
|
chip: {
|
|
92
101
|
DEFAULT: "hsl(var(--chip))",
|
|
@@ -100,6 +109,13 @@ export const optiUiPreset: Partial<Config> = {
|
|
|
100
109
|
3: "hsl(var(--chart-3))",
|
|
101
110
|
4: "hsl(var(--chart-4))",
|
|
102
111
|
5: "hsl(var(--chart-5))",
|
|
112
|
+
6: "hsl(var(--chart-6))",
|
|
113
|
+
7: "hsl(var(--chart-7))",
|
|
114
|
+
8: "hsl(var(--chart-8))",
|
|
115
|
+
9: "hsl(var(--chart-9))",
|
|
116
|
+
10: "hsl(var(--chart-10))",
|
|
117
|
+
11: "hsl(var(--chart-11))",
|
|
118
|
+
12: "hsl(var(--chart-12))",
|
|
103
119
|
},
|
|
104
120
|
},
|
|
105
121
|
borderRadius: {
|