@newtonedev/editor 0.1.5 → 0.1.6

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.
@@ -3,19 +3,22 @@ import { useTokens } from "@newtonedev/components";
3
3
  import { srgbToHex } from "newtone";
4
4
  import { getComponent } from "@newtonedev/components";
5
5
  import { ComponentRenderer } from "./ComponentRenderer";
6
+ import { IconBrowserView } from "./IconBrowserView";
6
7
 
7
8
  interface ComponentDetailViewProps {
8
9
  readonly componentId: string;
9
10
  readonly selectedVariantId: string | null;
10
- readonly propOverrides?: Record<string, unknown>;
11
11
  readonly onSelectVariant: (variantId: string) => void;
12
+ readonly propOverrides?: Record<string, unknown>;
13
+ readonly onPropOverride?: (name: string, value: unknown) => void;
12
14
  }
13
15
 
14
16
  export function ComponentDetailView({
15
17
  componentId,
16
18
  selectedVariantId,
17
- propOverrides,
18
19
  onSelectVariant,
20
+ propOverrides,
21
+ onPropOverride,
19
22
  }: ComponentDetailViewProps) {
20
23
  const tokens = useTokens();
21
24
  const component = getComponent(componentId);
@@ -23,6 +26,46 @@ export function ComponentDetailView({
23
26
 
24
27
  if (!component) return null;
25
28
 
29
+ if (componentId === "icon" && propOverrides && onPropOverride) {
30
+ return (
31
+ <div
32
+ style={{
33
+ padding: "32px 0 0",
34
+ height: "100%",
35
+ display: "flex",
36
+ flexDirection: "column",
37
+ }}
38
+ >
39
+ <div style={{ padding: "0 32px", marginBottom: 24 }}>
40
+ <h2
41
+ style={{
42
+ fontSize: 22,
43
+ fontWeight: 700,
44
+ color: srgbToHex(tokens.textPrimary.srgb),
45
+ margin: 0,
46
+ marginBottom: 4,
47
+ }}
48
+ >
49
+ {component.name}
50
+ </h2>
51
+ <p
52
+ style={{
53
+ fontSize: 14,
54
+ color: srgbToHex(tokens.textSecondary.srgb),
55
+ margin: 0,
56
+ }}
57
+ >
58
+ {component.description}
59
+ </p>
60
+ </div>
61
+ <IconBrowserView
62
+ selectedIconName={(propOverrides.name as string) ?? "add"}
63
+ onIconSelect={(name) => onPropOverride("name", name)}
64
+ />
65
+ </div>
66
+ );
67
+ }
68
+
26
69
  const interactiveColor = srgbToHex(tokens.accent.fill.srgb);
27
70
 
28
71
  return (
@@ -99,11 +142,7 @@ export function ComponentDetailView({
99
142
  >
100
143
  <ComponentRenderer
101
144
  componentId={componentId}
102
- props={
103
- isSelected && propOverrides
104
- ? { ...variant.props, ...propOverrides }
105
- : variant.props
106
- }
145
+ props={variant.props}
107
146
  />
108
147
  </div>
109
148
  <span
@@ -0,0 +1,187 @@
1
+ import { useState, useRef, useEffect, useMemo } from "react";
2
+ import { Icon, useTokens, ICON_CATALOG } from "@newtonedev/components";
3
+ import { srgbToHex } from "newtone";
4
+
5
+ interface IconBrowserViewProps {
6
+ readonly selectedIconName: string;
7
+ readonly onIconSelect: (name: string) => void;
8
+ }
9
+
10
+ export function IconBrowserView({
11
+ selectedIconName,
12
+ onIconSelect,
13
+ }: IconBrowserViewProps) {
14
+ const tokens = useTokens();
15
+ const [search, setSearch] = useState("");
16
+ const [hoveredIcon, setHoveredIcon] = useState<string | null>(null);
17
+ const scrollRef = useRef<HTMLDivElement>(null);
18
+
19
+ const filteredCategories = useMemo(() => {
20
+ const q = search.toLowerCase().trim();
21
+ if (!q) return ICON_CATALOG;
22
+ return ICON_CATALOG
23
+ .map((cat) => ({
24
+ ...cat,
25
+ icons: cat.icons.filter((name) => name.includes(q)),
26
+ }))
27
+ .filter((cat) => cat.icons.length > 0);
28
+ }, [search]);
29
+
30
+ // Scroll to selected icon when it changes externally (user types in sidebar)
31
+ useEffect(() => {
32
+ if (!selectedIconName || !scrollRef.current) return;
33
+ const el = scrollRef.current.querySelector(
34
+ `[data-icon="${selectedIconName}"]`,
35
+ );
36
+ if (el) {
37
+ el.scrollIntoView({ behavior: "smooth", block: "nearest" });
38
+ }
39
+ }, [selectedIconName]);
40
+
41
+ const accentColor = srgbToHex(tokens.accent.fill.srgb);
42
+
43
+ return (
44
+ <div
45
+ style={{
46
+ display: "flex",
47
+ flexDirection: "column",
48
+ height: "100%",
49
+ minHeight: 0,
50
+ }}
51
+ >
52
+ {/* Search */}
53
+ <div style={{ padding: "0 32px", flexShrink: 0 }}>
54
+ <div style={{ position: "relative" }}>
55
+ <Icon
56
+ name="search"
57
+ size={18}
58
+ color={srgbToHex(tokens.textTertiary.srgb)}
59
+ style={{
60
+ position: "absolute",
61
+ left: 10,
62
+ top: 9,
63
+ pointerEvents: "none",
64
+ }}
65
+ />
66
+ <input
67
+ type="text"
68
+ placeholder="Search icons..."
69
+ value={search}
70
+ onChange={(e) => setSearch(e.target.value)}
71
+ style={{
72
+ width: "100%",
73
+ padding: "8px 12px 8px 34px",
74
+ borderRadius: 8,
75
+ border: `1px solid ${srgbToHex(tokens.border.srgb)}`,
76
+ backgroundColor: srgbToHex(tokens.backgroundSunken.srgb),
77
+ color: srgbToHex(tokens.textPrimary.srgb),
78
+ fontSize: 13,
79
+ boxSizing: "border-box",
80
+ outline: "none",
81
+ }}
82
+ />
83
+ </div>
84
+ </div>
85
+
86
+ {/* Icon grid */}
87
+ <div
88
+ ref={scrollRef}
89
+ style={{
90
+ flex: 1,
91
+ overflowY: "auto",
92
+ padding: "16px 32px 32px",
93
+ }}
94
+ >
95
+ {filteredCategories.length === 0 && (
96
+ <p
97
+ style={{
98
+ fontSize: 13,
99
+ color: srgbToHex(tokens.textTertiary.srgb),
100
+ textAlign: "center",
101
+ marginTop: 32,
102
+ }}
103
+ >
104
+ No icons found
105
+ </p>
106
+ )}
107
+
108
+ {filteredCategories.map((category) => (
109
+ <div key={category.id} style={{ marginBottom: 24 }}>
110
+ <h3
111
+ style={{
112
+ fontSize: 12,
113
+ fontWeight: 600,
114
+ color: srgbToHex(tokens.textSecondary.srgb),
115
+ textTransform: "uppercase",
116
+ letterSpacing: 0.5,
117
+ margin: "0 0 8px",
118
+ }}
119
+ >
120
+ {category.label}
121
+ </h3>
122
+ <div
123
+ style={{
124
+ display: "grid",
125
+ gridTemplateColumns: "repeat(auto-fill, minmax(80px, 1fr))",
126
+ gap: 6,
127
+ }}
128
+ >
129
+ {category.icons.map((name) => {
130
+ const isSelected = selectedIconName === name;
131
+ const isHovered = hoveredIcon === name;
132
+
133
+ const borderColor = isSelected
134
+ ? accentColor
135
+ : isHovered
136
+ ? `${accentColor}66`
137
+ : "transparent";
138
+
139
+ return (
140
+ <button
141
+ key={name}
142
+ data-icon={name}
143
+ onClick={() => onIconSelect(name)}
144
+ onMouseEnter={() => setHoveredIcon(name)}
145
+ onMouseLeave={() => setHoveredIcon(null)}
146
+ style={{
147
+ display: "flex",
148
+ flexDirection: "column",
149
+ alignItems: "center",
150
+ justifyContent: "center",
151
+ gap: 4,
152
+ padding: "8px 4px 6px",
153
+ borderRadius: 8,
154
+ border: `2px solid ${borderColor}`,
155
+ backgroundColor: isSelected
156
+ ? srgbToHex(tokens.backgroundElevated.srgb)
157
+ : "transparent",
158
+ cursor: "pointer",
159
+ transition: "border-color 150ms ease",
160
+ }}
161
+ >
162
+ <Icon name={name} size={40} />
163
+ <span
164
+ style={{
165
+ fontSize: 10,
166
+ color: isSelected
167
+ ? accentColor
168
+ : srgbToHex(tokens.textTertiary.srgb),
169
+ fontWeight: isSelected ? 600 : 400,
170
+ maxWidth: "100%",
171
+ overflow: "hidden",
172
+ textOverflow: "ellipsis",
173
+ whiteSpace: "nowrap",
174
+ }}
175
+ >
176
+ {name}
177
+ </span>
178
+ </button>
179
+ );
180
+ })}
181
+ </div>
182
+ </div>
183
+ ))}
184
+ </div>
185
+ </div>
186
+ );
187
+ }