@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.
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import App from './App';
4
+
5
+ const root = createRoot(document.getElementById('root')!);
6
+ root.render(<App />);