@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.
Files changed (62) hide show
  1. package/dist/Editor.d.ts +1 -1
  2. package/dist/Editor.d.ts.map +1 -1
  3. package/dist/components/CodeBlock.d.ts.map +1 -1
  4. package/dist/components/ConfiguratorPanel.d.ts +17 -0
  5. package/dist/components/ConfiguratorPanel.d.ts.map +1 -0
  6. package/dist/components/FontPicker.d.ts +4 -2
  7. package/dist/components/FontPicker.d.ts.map +1 -1
  8. package/dist/components/PresetSelector.d.ts.map +1 -1
  9. package/dist/components/PreviewWindow.d.ts +9 -3
  10. package/dist/components/PreviewWindow.d.ts.map +1 -1
  11. package/dist/components/PrimaryNav.d.ts +7 -0
  12. package/dist/components/PrimaryNav.d.ts.map +1 -0
  13. package/dist/components/RightSidebar.d.ts +4 -1
  14. package/dist/components/RightSidebar.d.ts.map +1 -1
  15. package/dist/components/Sidebar.d.ts +1 -10
  16. package/dist/components/Sidebar.d.ts.map +1 -1
  17. package/dist/components/TableOfContents.d.ts +2 -1
  18. package/dist/components/TableOfContents.d.ts.map +1 -1
  19. package/dist/components/sections/DynamicRangeSection.d.ts.map +1 -1
  20. package/dist/components/sections/FontsSection.d.ts +3 -1
  21. package/dist/components/sections/FontsSection.d.ts.map +1 -1
  22. package/dist/hooks/useEditorState.d.ts +4 -1
  23. package/dist/hooks/useEditorState.d.ts.map +1 -1
  24. package/dist/index.cjs +2893 -2248
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.d.ts +2 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +2895 -2251
  29. package/dist/index.js.map +1 -1
  30. package/dist/preview/ComponentDetailView.d.ts +9 -2
  31. package/dist/preview/ComponentDetailView.d.ts.map +1 -1
  32. package/dist/preview/ComponentRenderer.d.ts +2 -1
  33. package/dist/preview/ComponentRenderer.d.ts.map +1 -1
  34. package/dist/preview/IconBrowserView.d.ts +7 -0
  35. package/dist/preview/IconBrowserView.d.ts.map +1 -0
  36. package/dist/types.d.ts +17 -0
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/utils/lookupFontMetrics.d.ts +19 -0
  39. package/dist/utils/lookupFontMetrics.d.ts.map +1 -0
  40. package/dist/utils/measureFonts.d.ts +18 -0
  41. package/dist/utils/measureFonts.d.ts.map +1 -0
  42. package/package.json +1 -1
  43. package/src/Editor.tsx +57 -11
  44. package/src/components/CodeBlock.tsx +42 -14
  45. package/src/components/ConfiguratorPanel.tsx +77 -0
  46. package/src/components/FontPicker.tsx +38 -29
  47. package/src/components/PresetSelector.tsx +8 -33
  48. package/src/components/PreviewWindow.tsx +20 -4
  49. package/src/components/PrimaryNav.tsx +76 -0
  50. package/src/components/RightSidebar.tsx +103 -40
  51. package/src/components/Sidebar.tsx +4 -211
  52. package/src/components/TableOfContents.tsx +41 -78
  53. package/src/components/sections/DynamicRangeSection.tsx +2 -225
  54. package/src/components/sections/FontsSection.tsx +61 -93
  55. package/src/hooks/useEditorState.ts +68 -9
  56. package/src/index.ts +2 -0
  57. package/src/preview/ComponentDetailView.tsx +576 -73
  58. package/src/preview/ComponentRenderer.tsx +6 -4
  59. package/src/preview/IconBrowserView.tsx +187 -0
  60. package/src/types.ts +15 -0
  61. package/src/utils/lookupFontMetrics.ts +52 -0
  62. 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
- font={props.font as AnyProps}
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
+ }