@ryndesign/preview 0.1.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/LICENSE +21 -0
- package/client/index.html +12 -0
- package/client/src/App.tsx +261 -0
- package/client/src/components/CodeViewer.tsx +88 -0
- package/client/src/components/ComponentBrowser.tsx +54 -0
- package/client/src/components/PreviewPanel.tsx +188 -0
- package/client/src/components/ThemeSwitcher.tsx +27 -0
- package/client/src/components/TokenEditor.tsx +113 -0
- package/client/src/hooks/useTokens.ts +14 -0
- package/client/src/hooks/useWebSocket.ts +116 -0
- package/client/src/index.tsx +6 -0
- package/dist/index.cjs +691 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +660 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
tokens: any[];
|
|
5
|
+
onTokenUpdate: (path: string, value: unknown) => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function formatValue(value: any): string {
|
|
9
|
+
if (!value) return '';
|
|
10
|
+
switch (value.type) {
|
|
11
|
+
case 'color': return value.hex;
|
|
12
|
+
case 'dimension': return `${value.value}${value.unit}`;
|
|
13
|
+
case 'fontWeight': return String(value.value);
|
|
14
|
+
case 'duration': return `${value.value}${value.unit}`;
|
|
15
|
+
case 'number': return String(value.value);
|
|
16
|
+
default: return JSON.stringify(value);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function TokenEditor({ tokens, onTokenUpdate }: Props) {
|
|
21
|
+
const [search, setSearch] = useState('');
|
|
22
|
+
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
|
23
|
+
|
|
24
|
+
if (tokens.length === 0) {
|
|
25
|
+
return <p style={{ color: 'var(--text-secondary)', fontSize: 13 }}>No tokens loaded</p>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Group tokens by first path segment
|
|
29
|
+
const groups: Record<string, any[]> = {};
|
|
30
|
+
for (const token of tokens) {
|
|
31
|
+
const group = token.path[0];
|
|
32
|
+
if (!groups[group]) groups[group] = [];
|
|
33
|
+
groups[group].push(token);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const filteredGroups: Record<string, any[]> = {};
|
|
37
|
+
for (const [group, groupTokens] of Object.entries(groups)) {
|
|
38
|
+
const filtered = search
|
|
39
|
+
? groupTokens.filter(t => t.path.join('.').toLowerCase().includes(search.toLowerCase()))
|
|
40
|
+
: groupTokens;
|
|
41
|
+
if (filtered.length > 0) {
|
|
42
|
+
filteredGroups[group] = filtered;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<div>
|
|
48
|
+
<input
|
|
49
|
+
className="search-input"
|
|
50
|
+
type="text"
|
|
51
|
+
placeholder="Filter tokens..."
|
|
52
|
+
value={search}
|
|
53
|
+
onChange={e => setSearch(e.target.value)}
|
|
54
|
+
/>
|
|
55
|
+
{Object.entries(filteredGroups).map(([group, groupTokens]) => (
|
|
56
|
+
<div className="token-group" key={group}>
|
|
57
|
+
<h3 onClick={() => setCollapsed(prev => ({ ...prev, [group]: !prev[group] }))}>
|
|
58
|
+
{collapsed[group] ? '▸' : '▾'} {group} ({groupTokens.length})
|
|
59
|
+
</h3>
|
|
60
|
+
{!collapsed[group] && groupTokens.map((token: any) => {
|
|
61
|
+
const path = token.path.join('.');
|
|
62
|
+
const value = token.$value;
|
|
63
|
+
return (
|
|
64
|
+
<div className="token-item" key={path}>
|
|
65
|
+
{value.type === 'color' && (
|
|
66
|
+
<div className="color-swatch" style={{ background: value.hex }}>
|
|
67
|
+
<input
|
|
68
|
+
type="color"
|
|
69
|
+
value={value.hex}
|
|
70
|
+
onChange={e => onTokenUpdate(path, e.target.value)}
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
{value.type === 'dimension' && (
|
|
75
|
+
<input
|
|
76
|
+
className="token-input"
|
|
77
|
+
type="number"
|
|
78
|
+
value={value.value}
|
|
79
|
+
onChange={e => onTokenUpdate(path, `${e.target.value}${value.unit}`)}
|
|
80
|
+
style={{ width: 60 }}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
{value.type === 'fontWeight' && (
|
|
84
|
+
<select
|
|
85
|
+
className="token-input"
|
|
86
|
+
value={value.value}
|
|
87
|
+
onChange={e => onTokenUpdate(path, Number(e.target.value))}
|
|
88
|
+
style={{ width: 80 }}
|
|
89
|
+
>
|
|
90
|
+
{[100, 200, 300, 400, 500, 600, 700, 800, 900].map(w => (
|
|
91
|
+
<option key={w} value={w}>{w}</option>
|
|
92
|
+
))}
|
|
93
|
+
</select>
|
|
94
|
+
)}
|
|
95
|
+
{value.type === 'number' && (
|
|
96
|
+
<input
|
|
97
|
+
className="token-input"
|
|
98
|
+
type="number"
|
|
99
|
+
value={value.value}
|
|
100
|
+
onChange={e => onTokenUpdate(path, Number(e.target.value))}
|
|
101
|
+
style={{ width: 60 }}
|
|
102
|
+
/>
|
|
103
|
+
)}
|
|
104
|
+
<span className="token-name">{token.path.slice(1).join('.')}</span>
|
|
105
|
+
<span className="token-value">{formatValue(value)}</span>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
|
|
3
|
+
export function useTokens(send: (msg: Record<string, unknown>) => void) {
|
|
4
|
+
const updateToken = useCallback((path: string, value: unknown, theme?: string) => {
|
|
5
|
+
send({
|
|
6
|
+
type: 'token-update',
|
|
7
|
+
theme,
|
|
8
|
+
path,
|
|
9
|
+
value,
|
|
10
|
+
});
|
|
11
|
+
}, [send]);
|
|
12
|
+
|
|
13
|
+
return { updateToken };
|
|
14
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
interface TokenSet {
|
|
4
|
+
metadata: { name?: string; description?: string };
|
|
5
|
+
tokens: any[];
|
|
6
|
+
groups: any[];
|
|
7
|
+
themes: { default: string; themes: Record<string, any> };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface WsState {
|
|
11
|
+
connected: boolean;
|
|
12
|
+
tokenSet: TokenSet | null;
|
|
13
|
+
components: any[];
|
|
14
|
+
snippets: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function useWebSocket() {
|
|
18
|
+
const [state, setState] = useState<WsState>({
|
|
19
|
+
connected: false,
|
|
20
|
+
tokenSet: null,
|
|
21
|
+
components: [],
|
|
22
|
+
snippets: {},
|
|
23
|
+
});
|
|
24
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
25
|
+
|
|
26
|
+
const send = useCallback((msg: Record<string, unknown>) => {
|
|
27
|
+
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
28
|
+
wsRef.current.send(JSON.stringify(msg));
|
|
29
|
+
}
|
|
30
|
+
}, []);
|
|
31
|
+
|
|
32
|
+
const requestSnippets = useCallback((platform: string, component: string) => {
|
|
33
|
+
fetch(`/api/snippets?platform=${platform}&component=${component}`)
|
|
34
|
+
.then(r => r.json())
|
|
35
|
+
.then(data => {
|
|
36
|
+
setState(prev => ({ ...prev, snippets: { ...prev.snippets, [`${platform}:${component}`]: data.code ?? '' } }));
|
|
37
|
+
})
|
|
38
|
+
.catch(() => {});
|
|
39
|
+
|
|
40
|
+
fetch(`/api/snippets?platform=${platform}&type=tokens`)
|
|
41
|
+
.then(r => r.json())
|
|
42
|
+
.then(data => {
|
|
43
|
+
setState(prev => ({ ...prev, snippets: { ...prev.snippets, [`${platform}:tokens`]: data.code ?? '' } }));
|
|
44
|
+
})
|
|
45
|
+
.catch(() => {});
|
|
46
|
+
}, []);
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
function connect() {
|
|
50
|
+
const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
51
|
+
const ws = new WebSocket(`${protocol}//${location.host}`);
|
|
52
|
+
wsRef.current = ws;
|
|
53
|
+
|
|
54
|
+
ws.onopen = () => {
|
|
55
|
+
setState(prev => ({ ...prev, connected: true }));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
ws.onmessage = (event) => {
|
|
59
|
+
const msg = JSON.parse(event.data);
|
|
60
|
+
switch (msg.type) {
|
|
61
|
+
case 'init':
|
|
62
|
+
case 'full-state':
|
|
63
|
+
setState(prev => ({
|
|
64
|
+
...prev,
|
|
65
|
+
tokenSet: msg.tokenSet,
|
|
66
|
+
components: msg.components ?? prev.components,
|
|
67
|
+
}));
|
|
68
|
+
break;
|
|
69
|
+
case 'rebuild-complete':
|
|
70
|
+
if (msg.tokenSet) {
|
|
71
|
+
setState(prev => ({
|
|
72
|
+
...prev,
|
|
73
|
+
tokenSet: msg.tokenSet,
|
|
74
|
+
components: msg.components ?? prev.components,
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
ws.onclose = () => {
|
|
82
|
+
setState(prev => ({ ...prev, connected: false }));
|
|
83
|
+
setTimeout(connect, 2000);
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
connect();
|
|
88
|
+
|
|
89
|
+
// Fallback: fetch initial data via REST
|
|
90
|
+
fetch('/api/tokens').then(r => r.json()).then(data => {
|
|
91
|
+
setState(prev => {
|
|
92
|
+
if (prev.tokenSet) return prev;
|
|
93
|
+
return { ...prev, tokenSet: data };
|
|
94
|
+
});
|
|
95
|
+
}).catch(() => {});
|
|
96
|
+
|
|
97
|
+
fetch('/api/components').then(r => r.json()).then(data => {
|
|
98
|
+
if (Array.isArray(data)) {
|
|
99
|
+
setState(prev => ({ ...prev, components: data }));
|
|
100
|
+
}
|
|
101
|
+
}).catch(() => {});
|
|
102
|
+
|
|
103
|
+
return () => {
|
|
104
|
+
wsRef.current?.close();
|
|
105
|
+
};
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
connected: state.connected,
|
|
110
|
+
tokenSet: state.tokenSet,
|
|
111
|
+
components: state.components,
|
|
112
|
+
snippets: state.snippets,
|
|
113
|
+
send,
|
|
114
|
+
requestSnippets,
|
|
115
|
+
};
|
|
116
|
+
}
|