@newtonedev/editor 0.1.5 → 0.1.7
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/Editor.d.ts +1 -1
- package/dist/Editor.d.ts.map +1 -1
- package/dist/components/CodeBlock.d.ts.map +1 -1
- package/dist/components/ConfiguratorPanel.d.ts +17 -0
- package/dist/components/ConfiguratorPanel.d.ts.map +1 -0
- package/dist/components/FontPicker.d.ts +4 -2
- package/dist/components/FontPicker.d.ts.map +1 -1
- package/dist/components/PresetSelector.d.ts.map +1 -1
- package/dist/components/PreviewWindow.d.ts +9 -3
- package/dist/components/PreviewWindow.d.ts.map +1 -1
- package/dist/components/PrimaryNav.d.ts +7 -0
- package/dist/components/PrimaryNav.d.ts.map +1 -0
- package/dist/components/RightSidebar.d.ts +4 -1
- package/dist/components/RightSidebar.d.ts.map +1 -1
- package/dist/components/Sidebar.d.ts +1 -10
- package/dist/components/Sidebar.d.ts.map +1 -1
- package/dist/components/TableOfContents.d.ts +2 -1
- package/dist/components/TableOfContents.d.ts.map +1 -1
- package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -1
- package/dist/components/sections/FontsSection.d.ts +3 -1
- package/dist/components/sections/FontsSection.d.ts.map +1 -1
- package/dist/hooks/useEditorState.d.ts +4 -1
- package/dist/hooks/useEditorState.d.ts.map +1 -1
- package/dist/index.cjs +2893 -2248
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2895 -2251
- package/dist/index.js.map +1 -1
- package/dist/preview/ComponentDetailView.d.ts +9 -2
- package/dist/preview/ComponentDetailView.d.ts.map +1 -1
- package/dist/preview/ComponentRenderer.d.ts +2 -1
- package/dist/preview/ComponentRenderer.d.ts.map +1 -1
- package/dist/preview/IconBrowserView.d.ts +7 -0
- package/dist/preview/IconBrowserView.d.ts.map +1 -0
- package/dist/types.d.ts +17 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/lookupFontMetrics.d.ts +19 -0
- package/dist/utils/lookupFontMetrics.d.ts.map +1 -0
- package/dist/utils/measureFonts.d.ts +18 -0
- package/dist/utils/measureFonts.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/Editor.tsx +57 -11
- package/src/components/CodeBlock.tsx +42 -14
- package/src/components/ConfiguratorPanel.tsx +77 -0
- package/src/components/FontPicker.tsx +38 -29
- package/src/components/PresetSelector.tsx +8 -33
- package/src/components/PreviewWindow.tsx +20 -4
- package/src/components/PrimaryNav.tsx +76 -0
- package/src/components/RightSidebar.tsx +103 -40
- package/src/components/Sidebar.tsx +4 -211
- package/src/components/TableOfContents.tsx +41 -78
- package/src/components/sections/DynamicRangeSection.tsx +2 -225
- package/src/components/sections/FontsSection.tsx +61 -93
- package/src/hooks/useEditorState.ts +68 -9
- package/src/index.ts +2 -0
- package/src/preview/ComponentDetailView.tsx +576 -73
- package/src/preview/ComponentRenderer.tsx +6 -4
- package/src/preview/IconBrowserView.tsx +187 -0
- package/src/types.ts +15 -0
- package/src/utils/lookupFontMetrics.ts +52 -0
- package/src/utils/measureFonts.ts +41 -0
|
@@ -18,6 +18,7 @@ import { srgbToHex } from "newtone";
|
|
|
18
18
|
interface ComponentRendererProps {
|
|
19
19
|
readonly componentId: string;
|
|
20
20
|
readonly props: Record<string, unknown>;
|
|
21
|
+
readonly previewText?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -93,7 +94,7 @@ function WrapperPreview(props: AnyProps) {
|
|
|
93
94
|
);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
|
-
export function ComponentRenderer({ componentId, props }: ComponentRendererProps) {
|
|
97
|
+
export function ComponentRenderer({ componentId, props, previewText }: ComponentRendererProps) {
|
|
97
98
|
const noop = useCallback(() => {}, []);
|
|
98
99
|
|
|
99
100
|
switch (componentId) {
|
|
@@ -128,12 +129,13 @@ export function ComponentRenderer({ componentId, props }: ComponentRendererProps
|
|
|
128
129
|
case "text":
|
|
129
130
|
return (
|
|
130
131
|
<Text
|
|
132
|
+
scope={props.scope as AnyProps}
|
|
133
|
+
role={props.role as AnyProps}
|
|
131
134
|
size={props.size as AnyProps}
|
|
132
|
-
weight={props.weight as AnyProps}
|
|
133
135
|
color={props.color as AnyProps}
|
|
134
|
-
|
|
136
|
+
responsive
|
|
135
137
|
>
|
|
136
|
-
The quick brown fox
|
|
138
|
+
{previewText || "The quick brown fox"}
|
|
137
139
|
</Text>
|
|
138
140
|
);
|
|
139
141
|
case "icon":
|
|
@@ -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
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type { ConfiguratorState } from "@newtonedev/configurator";
|
|
2
2
|
import type { NewtoneThemeConfig } from "@newtonedev/components";
|
|
3
|
+
import type { FontRuntimeMetrics, GoogleFontEntry } from "@newtonedev/fonts";
|
|
3
4
|
import type { ReactNode } from "react";
|
|
4
5
|
|
|
6
|
+
/** Font catalog entry enriched with weight metadata for the editor. */
|
|
7
|
+
export interface EditorFontEntry extends GoogleFontEntry {
|
|
8
|
+
readonly isVariable?: boolean;
|
|
9
|
+
readonly availableWeights?: readonly number[];
|
|
10
|
+
/** Weight axis range for variable fonts (from Google Fonts API wght axis). */
|
|
11
|
+
readonly weightAxisRange?: { readonly min: number; readonly max: number };
|
|
12
|
+
}
|
|
13
|
+
|
|
5
14
|
// --- Data types ---
|
|
6
15
|
|
|
7
16
|
export interface Preset {
|
|
@@ -43,6 +52,8 @@ export interface EditorPersistence {
|
|
|
43
52
|
readonly state: ConfiguratorState;
|
|
44
53
|
readonly presets: readonly Preset[];
|
|
45
54
|
readonly activePresetId: string;
|
|
55
|
+
readonly calibrations?: Record<string, number>;
|
|
56
|
+
readonly fontMetrics?: Record<string, FontRuntimeMetrics>;
|
|
46
57
|
}) => Promise<{ error?: unknown }>;
|
|
47
58
|
|
|
48
59
|
/** Persist preset metadata (used by preset CRUD operations). */
|
|
@@ -72,4 +83,8 @@ export interface EditorProps {
|
|
|
72
83
|
readonly headerSlots?: EditorHeaderSlots;
|
|
73
84
|
readonly onNavigate?: (view: PreviewView) => void;
|
|
74
85
|
readonly initialPreviewView?: PreviewView;
|
|
86
|
+
/** URL of the font manifest for metrics lookup at publish time. */
|
|
87
|
+
readonly manifestUrl?: string;
|
|
88
|
+
/** Curated fonts available in the font picker, enriched with weight metadata. */
|
|
89
|
+
readonly fontCatalog?: readonly EditorFontEntry[];
|
|
75
90
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { FontScope } from '@newtonedev/fonts';
|
|
2
|
+
import type { FontSlot } from '@newtonedev/components';
|
|
3
|
+
import type { FontRuntimeMetrics } from '@newtonedev/fonts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Look up FontRuntimeMetrics for all font scopes from the font manifest.
|
|
7
|
+
*
|
|
8
|
+
* Called at publish time alongside measureFontCalibrations. Fetches the
|
|
9
|
+
* manifest JSON from the given URL and extracts metrics for each unique
|
|
10
|
+
* font family in the current typography configuration.
|
|
11
|
+
*
|
|
12
|
+
* Deduplicates by family name so a font used in multiple scopes is
|
|
13
|
+
* looked up only once. Returns empty object if manifest is unavailable.
|
|
14
|
+
*
|
|
15
|
+
* @param fonts - The typography.fonts record from ConfiguratorState.
|
|
16
|
+
* @param manifestUrl - URL of the font manifest (e.g., Supabase Storage public URL).
|
|
17
|
+
* @returns Map of fontFamily → FontRuntimeMetrics.
|
|
18
|
+
*/
|
|
19
|
+
export async function lookupFontMetrics(
|
|
20
|
+
fonts: Record<FontScope, FontSlot> | undefined,
|
|
21
|
+
manifestUrl: string | undefined,
|
|
22
|
+
): Promise<Record<string, FontRuntimeMetrics>> {
|
|
23
|
+
if (!fonts || !manifestUrl) return {};
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const res = await fetch(manifestUrl);
|
|
27
|
+
if (!res.ok) return {};
|
|
28
|
+
const manifest = await res.json();
|
|
29
|
+
|
|
30
|
+
const result: Record<string, FontRuntimeMetrics> = {};
|
|
31
|
+
const seen = new Set<string>();
|
|
32
|
+
|
|
33
|
+
for (const slot of Object.values(fonts) as FontSlot[]) {
|
|
34
|
+
const family = slot.config.family;
|
|
35
|
+
if (seen.has(family)) continue;
|
|
36
|
+
seen.add(family);
|
|
37
|
+
|
|
38
|
+
const entry = manifest.families?.[family];
|
|
39
|
+
if (entry?.metrics) {
|
|
40
|
+
result[family] = {
|
|
41
|
+
naturalLineHeightRatio: entry.metrics.naturalLineHeightRatio,
|
|
42
|
+
verticalCenterOffset: entry.metrics.verticalCenterOffset,
|
|
43
|
+
features: entry.metrics.features ?? [],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return result;
|
|
49
|
+
} catch {
|
|
50
|
+
return {};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { FontScope } from '@newtonedev/fonts';
|
|
2
|
+
import type { FontSlot } from '@newtonedev/components';
|
|
3
|
+
import { measureAvgCharWidth } from '@newtonedev/components';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Measure avgCharWidth ratios for all font scopes at publish time.
|
|
7
|
+
*
|
|
8
|
+
* Deduplicates by font family name so a font used in multiple scopes is
|
|
9
|
+
* measured only once. Waits for fonts to load via `document.fonts.ready`
|
|
10
|
+
* before measuring, since the editor always preloads fonts for preview.
|
|
11
|
+
*
|
|
12
|
+
* Called in `handlePublish` before writing to persistence so that calibration
|
|
13
|
+
* data is included in the published config served to consumer sites.
|
|
14
|
+
*
|
|
15
|
+
* @param fonts - The typography.fonts record from ConfiguratorState.
|
|
16
|
+
* @returns Map of fontFamily → avgCharWidthRatio (e.g. `{ "Inter": 0.52 }`).
|
|
17
|
+
* Returns empty object if called outside a browser context.
|
|
18
|
+
*/
|
|
19
|
+
export async function measureFontCalibrations(
|
|
20
|
+
fonts: Record<FontScope, FontSlot> | undefined,
|
|
21
|
+
): Promise<Record<string, number>> {
|
|
22
|
+
if (!fonts || typeof document === 'undefined') return {};
|
|
23
|
+
|
|
24
|
+
const calibrations: Record<string, number> = {};
|
|
25
|
+
const seen = new Set<string>();
|
|
26
|
+
|
|
27
|
+
for (const slot of Object.values(fonts) as FontSlot[]) {
|
|
28
|
+
const { family, fallback } = slot.config;
|
|
29
|
+
if (seen.has(family)) continue;
|
|
30
|
+
seen.add(family);
|
|
31
|
+
|
|
32
|
+
const ratio = await measureAvgCharWidth(
|
|
33
|
+
family,
|
|
34
|
+
slot.weights.regular,
|
|
35
|
+
fallback,
|
|
36
|
+
);
|
|
37
|
+
calibrations[family] = ratio;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return calibrations;
|
|
41
|
+
}
|