@povio/ui 5.0.0-rc.13 → 5.0.0-rc.14

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/README.md CHANGED
@@ -322,3 +322,11 @@ Then open `/_canvas` directly. `enabled` accepts a boolean or a synchronous pred
322
322
  The canvas supports named workspaces, enabled pages per workspace, and independent positions per breakpoint. The selected page's HTML layers expand directly beneath its row in the sidebar.
323
323
 
324
324
  The existing `uiConfigurator` Vite plugin also saves canvas settings to `povio.canvas.json` in the app root. Configure an alternate file with `uiConfigurator({ canvasFile: "povio.canvas.json" })`, or disable this with `canvasFile: false`. No Git commands run: review and commit this file through the normal repository workflow. Conflicting writes from different sessions are rejected instead of silently overwriting settings. Without this development-server connection, settings stay in browser storage; export/import remains available. Theme versions from the Configure UI panel save separately to `povio.themes.json` (`themeVersionsFile` option).
325
+
326
+ ### Fontsource font families
327
+
328
+ In Configure UI, open Typography → Font families → Browse Fontsource. Search or filter the catalog, select a family to preview it, then choose Heading, Body, or Add to available fonts and click **Add font to app**. In development, this installs the verified Fontsource package with the application's existing package manager, updates its package.json and lockfile, and adds CSS imports. Variable families with a weight axis are preferred; static families load their supported weights and styles. Existing compatible packages and imports are reused.
329
+
330
+ The selected role is a live theme-version change. Close keeps it in that version; **Apply theme** writes the selected version's settings to the repo. Adding a font makes its assets available to every version without changing other versions' role assignments. Package installation scripts are disabled, and no Git commit or push is performed. Catalog browsing and temporary CDN previews also work without the development installer.
331
+
332
+ The Vite plugin uses `src/styles/globals.css` by default. For another CSS entrypoint, configure `uiConfigurator({ fontCssFile: "src/app.css" })`; it must exist inside the application's Vite root. Set `fontCssFile: false` to disable font installation. Restart Vite after adding or changing the plugin configuration.
@@ -1,5 +1,5 @@
1
1
  import { t as createLucideIcon } from "./createLucideIcon-LcezOE4z.js";
2
- import { _ as Moon, a as inspectElement, c as HighlightOverlay, d as useUIConfiguration, f as Trash, g as Pencil, h as Pin, l as InspectorDetails, m as Plus, p as Sun, r as useRepositoryStorage, s as getBoxHighlights, t as UIConfigurationModal } from "./UIConfigurator-DekPXjv9.js";
2
+ import { _ as Moon, a as inspectElement, c as HighlightOverlay, d as useUIConfiguration, f as Trash, g as Pencil, h as Pin, l as InspectorDetails, m as Plus, p as Sun, r as useRepositoryStorage, s as getBoxHighlights, t as UIConfigurationModal } from "./UIConfigurator-CjAW1SJY.js";
3
3
  import { t as ChevronDown } from "./chevron-down-B4Pr8bD3.js";
4
4
  import { t as ChevronRight } from "./chevron-right-3nKJR5Bb.js";
5
5
  import { i as Search } from "./selectDesktop.cva-CHAMxhfO.js";
@@ -3030,18 +3030,25 @@ function startCanvasDocument(doc, options) {
3030
3030
  doc.addEventListener("load", schedule, true);
3031
3031
  doc.fonts?.ready.then(schedule);
3032
3032
  const focusDescriptor = Object.getOwnPropertyDescriptor(win.HTMLElement.prototype, "focus");
3033
- const elementFocus = focusDescriptor.value;
3033
+ const elementFocus = win.HTMLElement.prototype.focus;
3034
3034
  const windowFocusDescriptor = Object.getOwnPropertyDescriptor(win, "focus");
3035
- const windowFocus = windowFocusDescriptor?.value;
3035
+ const windowFocus = win.focus;
3036
3036
  Object.defineProperty(win.HTMLElement.prototype, "focus", {
3037
- ...focusDescriptor,
3037
+ configurable: focusDescriptor.configurable,
3038
+ enumerable: focusDescriptor.enumerable,
3039
+ writable: true,
3038
3040
  value(focusOptions) {
3039
3041
  if (doc.hasFocus()) elementFocus.call(this, focusOptions);
3040
3042
  }
3041
3043
  });
3042
- win.focus = () => {
3043
- if (doc.hasFocus()) windowFocus?.call(win);
3044
- };
3044
+ Object.defineProperty(win, "focus", {
3045
+ configurable: windowFocusDescriptor?.configurable ?? true,
3046
+ enumerable: windowFocusDescriptor?.enumerable ?? true,
3047
+ writable: true,
3048
+ value() {
3049
+ if (doc.hasFocus()) windowFocus.call(win);
3050
+ }
3051
+ });
3045
3052
  refresh();
3046
3053
  return {
3047
3054
  refresh,
@@ -3140,7 +3147,7 @@ function AppCanvasFrameBridge({ href }) {
3140
3147
  const APP_CANVAS_PATH = "/_canvas";
3141
3148
  /** Must match `componentsFrameParam`; kept inline so the canvas page does not load the gallery eagerly. */
3142
3149
  const componentsParam = "povio-components";
3143
- const CanvasComponentsFrame = lazy(() => import("./CanvasComponentsFrame-CbgJJv-3.js"));
3150
+ const CanvasComponentsFrame = lazy(() => import("./CanvasComponentsFrame-D6Mh_G3q.js"));
3144
3151
  /** Ready-made route page. Mount beneath the app's UIConfigurationProvider. */
3145
3152
  function AppCanvasPage(t0) {
3146
3153
  const $ = c(24);
@@ -1,4 +1,4 @@
1
- import { ComponentArtboard } from "./ComponentGallery-COqEqqN6.js";
1
+ import { ComponentArtboard } from "./ComponentGallery-CzKU899_.js";
2
2
  import { c } from "react/compiler-runtime";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  //#region src/app-canvas/CanvasComponentsFrame.tsx
@@ -1,5 +1,5 @@
1
1
  import { t as createLucideIcon } from "./createLucideIcon-LcezOE4z.js";
2
- import { o as inspectTokens } from "./UIConfigurator-DekPXjv9.js";
2
+ import { o as inspectTokens } from "./UIConfigurator-CjAW1SJY.js";
3
3
  import { n as ArrowLeft } from "./DropdownMenu-5jmtgLXd.js";
4
4
  import { i as ArrowRight } from "./DropdownMenuDesktop-Cl9gZVq0.js";
5
5
  import { n as Italic, r as Bold, t as Underline } from "./underline-66hmELxa.js";
@@ -731,7 +731,7 @@ function UIConfigurationProvider(t0) {
731
731
  let t4;
732
732
  if ($[4] !== isOpen || $[5] !== open) {
733
733
  t4 = () => {
734
- if (isOpen) setRevision(_temp5$2);
734
+ if (isOpen) setRevision(_temp5$3);
735
735
  else open();
736
736
  setFloating(true);
737
737
  };
@@ -883,7 +883,7 @@ function UIConfigurationProvider(t0) {
883
883
  } else t17 = $[49];
884
884
  return t17;
885
885
  }
886
- function _temp5$2(value_1) {
886
+ function _temp5$3(value_1) {
887
887
  return value_1 + 1;
888
888
  }
889
889
  function _temp4$4(value_0) {
@@ -904,6 +904,360 @@ function useUIConfiguration() {
904
904
  return context;
905
905
  }
906
906
  //#endregion
907
+ //#region src/configurator/theme-writer-protocol.ts
908
+ /** Shared by the browser configurator and the isolated Node Vite entry. */
909
+ const themeWriterMetaName = "povio-ui-theme-writer";
910
+ const themeWriterTokenHeader = "x-povio-ui-theme-token";
911
+ //#endregion
912
+ //#region src/configurator/theme-writer-client.ts
913
+ function getThemeWriter() {
914
+ if (typeof document === "undefined") return null;
915
+ const content = document.querySelector(`meta[name="${themeWriterMetaName}"]`)?.content;
916
+ if (!content) return null;
917
+ try {
918
+ const connection = JSON.parse(content);
919
+ if (typeof connection.endpoint === "string" && connection.endpoint.startsWith("/") && !connection.endpoint.startsWith("//") && typeof connection.token === "string" && typeof connection.file === "string") return connection;
920
+ } catch {}
921
+ return null;
922
+ }
923
+ async function saveThemeCss(connection, css) {
924
+ const response = await fetch(connection.endpoint, {
925
+ method: "POST",
926
+ headers: {
927
+ "Content-Type": "application/json",
928
+ [themeWriterTokenHeader]: connection.token
929
+ },
930
+ body: JSON.stringify({ css })
931
+ });
932
+ if (!response.ok) {
933
+ const result = await response.json().catch(() => null);
934
+ throw new Error(result?.error ?? `Theme save failed (${response.status}).`);
935
+ }
936
+ }
937
+ //#endregion
938
+ //#region src/configurator/fontsource-client.ts
939
+ const validId = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
940
+ let catalog;
941
+ /** One metadata request per page, not one font download per search result. */
942
+ function loadFontsourceCatalog() {
943
+ catalog ??= fetch("https://api.fontsource.org/v1/fonts", { signal: AbortSignal.timeout(15e3) }).then(async (response) => {
944
+ if (!response.ok) throw new Error("Fontsource is unavailable. Try again.");
945
+ const data = await response.json();
946
+ if (!Array.isArray(data)) throw new Error("Fontsource returned an invalid catalog.");
947
+ return data.filter((font) => font && typeof font.id === "string" && font.id.length <= 100 && validId.test(font.id) && typeof font.family === "string" && typeof font.category === "string" && typeof font.variable === "boolean" && Array.isArray(font.subsets) && font.subsets.every((s) => typeof s === "string" && validId.test(s)) && typeof font.defSubset === "string" && font.subsets.includes(font.defSubset) && Array.isArray(font.weights) && font.weights.length > 0 && font.weights.every((w) => typeof w === "number" && w > 0 && w <= 1e3) && Array.isArray(font.styles) && (font.styles.includes("normal") || font.styles.includes("italic")) && font.type !== "icons").sort((a, b) => a.family.localeCompare(b.family));
948
+ }).catch((error) => {
949
+ catalog = void 0;
950
+ throw error;
951
+ });
952
+ return catalog;
953
+ }
954
+ function fontsourceWriter() {
955
+ const writer = getThemeWriter();
956
+ const fonts = writer?.fontsource;
957
+ return fonts && typeof fonts.endpoint === "string" && fonts.endpoint.startsWith("/") && !fonts.endpoint.startsWith("//") && typeof fonts.cssFile === "string" ? {
958
+ ...fonts,
959
+ token: writer.token
960
+ } : null;
961
+ }
962
+ async function addFontsourceFamily(id) {
963
+ const writer = fontsourceWriter();
964
+ if (!writer) throw new Error("Adding fonts requires the Povio UI Vite plugin in a development server.");
965
+ const response = await fetch(writer.endpoint, {
966
+ method: "POST",
967
+ headers: {
968
+ "Content-Type": "application/json",
969
+ [themeWriterTokenHeader]: writer.token
970
+ },
971
+ body: JSON.stringify({ id })
972
+ });
973
+ const result = await response.json();
974
+ if (!response.ok) throw new Error(result.error ?? "Could not add this font to the app.");
975
+ if (typeof result.family !== "string" || typeof result.packageName !== "string" || !Array.isArray(result.imports)) throw new Error("The font installer returned an invalid response.");
976
+ return result;
977
+ }
978
+ /** The static preview is temporary; installed variable families use their full range. */
979
+ function fontsourcePreview(font) {
980
+ const subset = font.subsets.includes("latin") ? "latin" : font.defSubset;
981
+ const weight = font.weights.includes(400) ? 400 : font.weights[0];
982
+ const style = font.styles.includes("normal") ? "normal" : "italic";
983
+ return {
984
+ alias: `Povio Fontsource preview ${font.id}`,
985
+ weight,
986
+ style,
987
+ url: `https://cdn.jsdelivr.net/fontsource/fonts/${font.id}@latest/${subset}-${weight}-${style}.woff2`
988
+ };
989
+ }
990
+ //#endregion
991
+ //#region src/configurator/FontsourcePicker.tsx
992
+ function FontsourcePicker({ roles, onAdded }) {
993
+ const trigger = useRef(null);
994
+ const mounted = useRef(true);
995
+ useEffect(() => {
996
+ mounted.current = true;
997
+ return () => {
998
+ mounted.current = false;
999
+ };
1000
+ }, []);
1001
+ const [open, setOpen] = useState(false);
1002
+ const [fonts, setFonts] = useState([]);
1003
+ const [loading, setLoading] = useState(false);
1004
+ const [query, setQuery] = useState("");
1005
+ const [limit, setLimit] = useState(30);
1006
+ const [category, setCategory] = useState("");
1007
+ const [variableOnly, setVariableOnly] = useState(false);
1008
+ useEffect(() => setLimit(30), [
1009
+ query,
1010
+ category,
1011
+ variableOnly
1012
+ ]);
1013
+ const [selected, setSelected] = useState(null);
1014
+ const [sample, setSample] = useState("The quick brown fox jumps over the lazy dog.");
1015
+ const [previewState, setPreviewState] = useState("loading");
1016
+ const [role, setRole] = useState(roles[0]?.token ?? "");
1017
+ const [adding, setAdding] = useState(false);
1018
+ const [error, setError] = useState("");
1019
+ const [notice, setNotice] = useState("");
1020
+ const [retry, setRetry] = useState(0);
1021
+ const writer = fontsourceWriter();
1022
+ useEffect(() => {
1023
+ if (!open) return;
1024
+ let active = true;
1025
+ setLoading(true);
1026
+ setError("");
1027
+ loadFontsourceCatalog().then((value) => {
1028
+ if (active) setFonts(value);
1029
+ }).catch((cause) => {
1030
+ if (active) setError(cause instanceof Error ? cause.message : "Could not load Fontsource.");
1031
+ }).finally(() => {
1032
+ if (active) setLoading(false);
1033
+ });
1034
+ return () => {
1035
+ active = false;
1036
+ };
1037
+ }, [open, retry]);
1038
+ useEffect(() => {
1039
+ if (!open || !selected) return;
1040
+ let active_0 = true;
1041
+ const doc = trigger.current?.ownerDocument ?? document;
1042
+ const preview = fontsourcePreview(selected);
1043
+ const face = new FontFace(preview.alias, `url("${preview.url}")`, {
1044
+ weight: String(preview.weight),
1045
+ style: preview.style
1046
+ });
1047
+ setPreviewState("loading");
1048
+ face.load().then((loaded) => {
1049
+ if (active_0) {
1050
+ doc.fonts.add(loaded);
1051
+ setPreviewState("ready");
1052
+ }
1053
+ }).catch(() => {
1054
+ if (active_0) setPreviewState("error");
1055
+ });
1056
+ return () => {
1057
+ active_0 = false;
1058
+ doc.fonts.delete(face);
1059
+ };
1060
+ }, [open, selected]);
1061
+ const filtered = useMemo(() => fonts.filter((font) => font.family.toLowerCase().includes(query.trim().toLowerCase()) && (!category || category === font.category) && (!variableOnly || font.variable)), [
1062
+ fonts,
1063
+ query,
1064
+ category,
1065
+ variableOnly
1066
+ ]);
1067
+ const preview_0 = selected ? fontsourcePreview(selected) : null;
1068
+ return /* @__PURE__ */ jsxs("div", {
1069
+ className: "studio-fontsource",
1070
+ children: [
1071
+ /* @__PURE__ */ jsx("button", {
1072
+ ref: trigger,
1073
+ type: "button",
1074
+ className: "studio-gallery-back",
1075
+ onClick: () => {
1076
+ setOpen(true);
1077
+ setError("");
1078
+ },
1079
+ children: "Browse Fontsource"
1080
+ }),
1081
+ notice && /* @__PURE__ */ jsx("span", {
1082
+ className: "studio-fontsource-notice",
1083
+ role: "status",
1084
+ children: notice
1085
+ }),
1086
+ /* @__PURE__ */ jsx($542a13ca2fa5b484$export$5b6b19405a83ff9d, {
1087
+ triggerRef: trigger,
1088
+ placement: "bottom start",
1089
+ offset: 6,
1090
+ isOpen: open,
1091
+ onOpenChange: (value_0) => {
1092
+ if (!adding) setOpen(value_0);
1093
+ },
1094
+ isKeyboardDismissDisabled: adding,
1095
+ className: "studio-fontsource-popover",
1096
+ children: /* @__PURE__ */ jsxs($f2ff30fde7b014be$export$3ddf2d174ce01153, {
1097
+ "aria-label": "Fontsource font picker",
1098
+ children: [
1099
+ /* @__PURE__ */ jsxs("header", { children: [/* @__PURE__ */ jsx($2ec61d1d4f780267$export$a8a3e93435678ff9, {
1100
+ slot: "title",
1101
+ children: "Fontsource fonts"
1102
+ }), /* @__PURE__ */ jsx("button", {
1103
+ type: "button",
1104
+ "aria-label": "Close font picker",
1105
+ disabled: adding,
1106
+ onClick: () => setOpen(false),
1107
+ children: /* @__PURE__ */ jsx(X, { size: 13 })
1108
+ })] }),
1109
+ /* @__PURE__ */ jsxs("label", {
1110
+ className: "studio-fontsource-search",
1111
+ children: [/* @__PURE__ */ jsx(Search, { size: 13 }), /* @__PURE__ */ jsx("input", {
1112
+ autoFocus: true,
1113
+ "aria-label": "Search Fontsource fonts",
1114
+ placeholder: "Find a font family…",
1115
+ value: query,
1116
+ onChange: (event) => setQuery(event.target.value)
1117
+ })]
1118
+ }),
1119
+ /* @__PURE__ */ jsxs("div", {
1120
+ className: "studio-fontsource-filters",
1121
+ children: [/* @__PURE__ */ jsxs("select", {
1122
+ "aria-label": "Font category",
1123
+ value: category,
1124
+ onChange: (event_0) => setCategory(event_0.target.value),
1125
+ children: [/* @__PURE__ */ jsx("option", {
1126
+ value: "",
1127
+ children: "All categories"
1128
+ }), [...new Set(fonts.map((font_0) => font_0.category))].sort().map((name) => /* @__PURE__ */ jsx("option", {
1129
+ value: name,
1130
+ children: name
1131
+ }, name))]
1132
+ }), /* @__PURE__ */ jsxs("label", { children: [
1133
+ /* @__PURE__ */ jsx("input", {
1134
+ type: "checkbox",
1135
+ checked: variableOnly,
1136
+ onChange: (event_1) => setVariableOnly(event_1.target.checked)
1137
+ }),
1138
+ " ",
1139
+ "Variable only"
1140
+ ] })]
1141
+ }),
1142
+ loading ? /* @__PURE__ */ jsx("p", {
1143
+ role: "status",
1144
+ children: "Loading font families…"
1145
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [
1146
+ /* @__PURE__ */ jsxs("span", {
1147
+ className: "studio-fontsource-count",
1148
+ children: [
1149
+ filtered.length,
1150
+ " families",
1151
+ filtered.length > limit ? ` · showing ${limit}` : ""
1152
+ ]
1153
+ }),
1154
+ /* @__PURE__ */ jsxs("div", {
1155
+ className: "studio-fontsource-list",
1156
+ role: "group",
1157
+ "aria-label": "Fontsource families",
1158
+ children: [filtered.slice(0, limit).map((font_1) => /* @__PURE__ */ jsxs("button", {
1159
+ type: "button",
1160
+ disabled: adding,
1161
+ "aria-pressed": selected?.id === font_1.id,
1162
+ onClick: () => {
1163
+ setSelected(font_1);
1164
+ setError("");
1165
+ },
1166
+ children: [
1167
+ /* @__PURE__ */ jsx("span", { children: font_1.family }),
1168
+ /* @__PURE__ */ jsxs("small", { children: [font_1.category, font_1.variable ? " · variable" : ""] }),
1169
+ selected?.id === font_1.id && /* @__PURE__ */ jsx(Check, { size: 12 })
1170
+ ]
1171
+ }, font_1.id)), !filtered.length && !error && /* @__PURE__ */ jsx("p", { children: "No matching font families." })]
1172
+ }),
1173
+ filtered.length > limit && /* @__PURE__ */ jsx("button", {
1174
+ type: "button",
1175
+ className: "studio-fontsource-more",
1176
+ onClick: () => setLimit((value_1) => value_1 + 30),
1177
+ children: "Show more fonts"
1178
+ })
1179
+ ] }),
1180
+ selected && /* @__PURE__ */ jsxs("section", {
1181
+ className: "studio-fontsource-preview",
1182
+ "aria-label": `Preview ${selected.family}`,
1183
+ children: [
1184
+ /* @__PURE__ */ jsx("strong", { children: selected.family }),
1185
+ /* @__PURE__ */ jsx("input", {
1186
+ "aria-label": "Font preview text",
1187
+ value: sample,
1188
+ onChange: (event_2) => setSample(event_2.target.value)
1189
+ }),
1190
+ /* @__PURE__ */ jsx("p", {
1191
+ className: "studio-fontsource-sample",
1192
+ style: {
1193
+ fontFamily: previewState === "ready" ? `"${preview_0.alias}", sans-serif` : "inherit",
1194
+ fontWeight: preview_0?.weight,
1195
+ fontStyle: preview_0?.style
1196
+ },
1197
+ children: sample || "Aa Bb Cc 0123456789"
1198
+ }),
1199
+ /* @__PURE__ */ jsx("span", {
1200
+ role: "status",
1201
+ children: previewState === "loading" ? "Loading preview…" : previewState === "error" ? "Preview could not load. You can still add this family." : ""
1202
+ }),
1203
+ /* @__PURE__ */ jsxs("label", {
1204
+ className: "studio-fontsource-role",
1205
+ children: ["Use for", /* @__PURE__ */ jsxs("select", {
1206
+ "aria-label": "Use font for",
1207
+ value: role,
1208
+ disabled: adding,
1209
+ onChange: (event_3) => setRole(event_3.target.value),
1210
+ children: [/* @__PURE__ */ jsx("option", {
1211
+ value: "",
1212
+ children: "Add to available fonts"
1213
+ }), roles.map((item) => /* @__PURE__ */ jsx("option", {
1214
+ value: item.token,
1215
+ children: item.label
1216
+ }, item.token))]
1217
+ })]
1218
+ }),
1219
+ /* @__PURE__ */ jsx("button", {
1220
+ type: "button",
1221
+ className: "studio-primary",
1222
+ disabled: adding || !writer,
1223
+ onClick: async () => {
1224
+ setAdding(true);
1225
+ setError("");
1226
+ try {
1227
+ const font_2 = await addFontsourceFamily(selected.id);
1228
+ if (!mounted.current) return;
1229
+ onAdded(font_2, role || void 0);
1230
+ setNotice(`${font_2.family} added to the app.`);
1231
+ setOpen(false);
1232
+ } catch (cause_0) {
1233
+ setError(cause_0 instanceof Error ? cause_0.message : "Could not add font.");
1234
+ } finally {
1235
+ setAdding(false);
1236
+ }
1237
+ },
1238
+ children: adding ? "Adding font…" : "Add font to app"
1239
+ })
1240
+ ]
1241
+ }),
1242
+ error && /* @__PURE__ */ jsxs("p", {
1243
+ role: "alert",
1244
+ children: [error, !fonts.length && /* @__PURE__ */ jsx("button", {
1245
+ type: "button",
1246
+ onClick: () => setRetry((value_2) => value_2 + 1),
1247
+ children: "Retry"
1248
+ })]
1249
+ }),
1250
+ /* @__PURE__ */ jsx("p", {
1251
+ className: "studio-fontsource-help",
1252
+ children: writer ? `Adding installs the font package and updates ${writer.cssFile}. Your theme version keeps the Heading/Body choice; Apply theme writes it to the repo.` : "Preview fonts here. Adding them to your app requires the Povio UI Vite plugin in development."
1253
+ })
1254
+ ]
1255
+ })
1256
+ })
1257
+ ]
1258
+ });
1259
+ }
1260
+ //#endregion
907
1261
  //#region src/configurator/inspector/InspectorDetails.tsx
908
1262
  /** A token as a design tool names a variable: `--text-large--line-height` → `text-large/line-height`. */
909
1263
  const variableName = (token) => token.replace(/^--/, "").replace(/-base$/, "").replaceAll("--", "/");
@@ -1025,7 +1379,7 @@ function BoxModel(t0) {
1025
1379
  let t3;
1026
1380
  let t4;
1027
1381
  if ($[0] !== box.border || $[1] !== box.content.height || $[2] !== box.content.width || $[3] !== box.gap.column || $[4] !== box.gap.row || $[5] !== box.margin || $[6] !== box.padding || $[7] !== box.position || $[8] !== box.radius || $[9] !== onHighlight || $[10] !== onSelect || $[11] !== snapshot.groups) {
1028
- const properties = snapshot.groups.flatMap(_temp5$1);
1382
+ const properties = snapshot.groups.flatMap(_temp5$2);
1029
1383
  const tokenFor = (label) => properties.find((property) => property.label === label)?.token;
1030
1384
  const nonzero = _temp7$1;
1031
1385
  let t5;
@@ -1204,7 +1558,7 @@ function _temp7$1(value_0) {
1204
1558
  function _temp6$1(part) {
1205
1559
  return parseFloat(part) !== 0 && Number.isFinite(parseFloat(part));
1206
1560
  }
1207
- function _temp5$1(group) {
1561
+ function _temp5$2(group) {
1208
1562
  return group.properties;
1209
1563
  }
1210
1564
  function _temp4$3(value) {
@@ -3784,7 +4138,7 @@ function CvaEditor(t0) {
3784
4138
  children: [/* @__PURE__ */ jsx("option", {
3785
4139
  value: "",
3786
4140
  children: "None"
3787
- }), Object.keys(options).map(_temp5)]
4141
+ }), Object.keys(options).map(_temp5$1)]
3788
4142
  })]
3789
4143
  }),
3790
4144
  Object.entries(options).map((t20) => {
@@ -3886,7 +4240,7 @@ function _temp6(t0) {
3886
4240
  const [key_3] = t0;
3887
4241
  return key_3 !== "className";
3888
4242
  }
3889
- function _temp5(value_1) {
4243
+ function _temp5$1(value_1) {
3890
4244
  return /* @__PURE__ */ jsx("option", {
3891
4245
  value: value_1,
3892
4246
  children: value_1
@@ -3908,37 +4262,6 @@ function _temp2$2(key) {
3908
4262
  return key.split(".")[0];
3909
4263
  }
3910
4264
  //#endregion
3911
- //#region src/configurator/theme-writer-protocol.ts
3912
- /** Shared by the browser configurator and the isolated Node Vite entry. */
3913
- const themeWriterMetaName = "povio-ui-theme-writer";
3914
- const themeWriterTokenHeader = "x-povio-ui-theme-token";
3915
- //#endregion
3916
- //#region src/configurator/theme-writer-client.ts
3917
- function getThemeWriter() {
3918
- if (typeof document === "undefined") return null;
3919
- const content = document.querySelector(`meta[name="${themeWriterMetaName}"]`)?.content;
3920
- if (!content) return null;
3921
- try {
3922
- const connection = JSON.parse(content);
3923
- if (typeof connection.endpoint === "string" && connection.endpoint.startsWith("/") && !connection.endpoint.startsWith("//") && typeof connection.token === "string" && typeof connection.file === "string") return connection;
3924
- } catch {}
3925
- return null;
3926
- }
3927
- async function saveThemeCss(connection, css) {
3928
- const response = await fetch(connection.endpoint, {
3929
- method: "POST",
3930
- headers: {
3931
- "Content-Type": "application/json",
3932
- [themeWriterTokenHeader]: connection.token
3933
- },
3934
- body: JSON.stringify({ css })
3935
- });
3936
- if (!response.ok) {
3937
- const result = await response.json().catch(() => null);
3938
- throw new Error(result?.error ?? `Theme save failed (${response.status}).`);
3939
- }
3940
- }
3941
- //#endregion
3942
4265
  //#region src/configurator/theme-versions.ts
3943
4266
  const themeVersionsMetaName = "povio-ui-theme-versions";
3944
4267
  function createThemeVersion(name, id, configuration) {
@@ -4893,13 +5216,13 @@ function useLoadedFonts() {
4893
5216
  document.fonts.ready.then(() => {
4894
5217
  if (!active) return;
4895
5218
  const root = getComputedStyle(document.documentElement);
4896
- const families = [...new Set([...document.fonts].map(_temp2))];
4897
- setFonts(families.map((family) => {
4898
- const base = `--font-${family.split(/\s+/)[0].toLowerCase()}-base`;
4899
- const value_0 = unquote(root.getPropertyValue(base)) === family ? `var(${base}), sans-serif` : `"${family}", sans-serif`;
4900
- const group = uploadedFamilies.has(family) ? "Uploaded fonts" : "Loaded fonts";
5219
+ const families = [...new Set([...document.fonts].map(_temp2))].filter(_temp3);
5220
+ setFonts(families.map((family_0) => {
5221
+ const base = `--font-${family_0.split(/\s+/)[0].toLowerCase()}-base`;
5222
+ const value_0 = unquote(root.getPropertyValue(base)) === family_0 ? `var(${base}), sans-serif` : `"${family_0}", sans-serif`;
5223
+ const group = uploadedFamilies.has(family_0) ? "Uploaded fonts" : "Loaded fonts";
4901
5224
  return {
4902
- name: `loaded:${family}`,
5225
+ name: `loaded:${family_0}`,
4903
5226
  label: familyName(value_0),
4904
5227
  value: value_0,
4905
5228
  group
@@ -4928,6 +5251,9 @@ function useLoadedFonts() {
4928
5251
  return t4;
4929
5252
  }
4930
5253
  /** Families added through the upload button in this page session. */
5254
+ function _temp3(family) {
5255
+ return !family.startsWith("Povio Fontsource preview ");
5256
+ }
4931
5257
  function _temp2(face) {
4932
5258
  return unquote(face.family);
4933
5259
  }
@@ -5106,7 +5432,7 @@ function FontRoleCard(t0) {
5106
5432
  } else t11 = $[21];
5107
5433
  let t12;
5108
5434
  if ($[22] !== options) {
5109
- t12 = new Set(options.map(_temp3));
5435
+ t12 = new Set(options.map(_temp4));
5110
5436
  $[22] = options;
5111
5437
  $[23] = t12;
5112
5438
  } else t12 = $[23];
@@ -5120,7 +5446,7 @@ function FontRoleCard(t0) {
5120
5446
  if ($[26] !== options || $[27] !== t13) {
5121
5447
  t14 = t13.map((group_0) => /* @__PURE__ */ jsx("optgroup", {
5122
5448
  label: group_0,
5123
- children: options.filter((option_1) => option_1.group === group_0).map(_temp4)
5449
+ children: options.filter((option_1) => option_1.group === group_0).map(_temp5)
5124
5450
  }, group_0));
5125
5451
  $[26] = options;
5126
5452
  $[27] = t13;
@@ -5182,13 +5508,13 @@ function FontRoleCard(t0) {
5182
5508
  } else t17 = $[45];
5183
5509
  return t17;
5184
5510
  }
5185
- function _temp4(option_2) {
5511
+ function _temp5(option_2) {
5186
5512
  return /* @__PURE__ */ jsx("option", {
5187
5513
  value: option_2.name,
5188
5514
  children: option_2.label
5189
5515
  }, option_2.name);
5190
5516
  }
5191
- function _temp3(t0) {
5517
+ function _temp4(t0) {
5192
5518
  const { group } = t0;
5193
5519
  return group;
5194
5520
  }
@@ -5740,7 +6066,7 @@ function _temp$1(t0) {
5740
6066
  }
5741
6067
  //#endregion
5742
6068
  //#region src/configurator/ComponentPreview.tsx
5743
- const ComponentGallery = lazy(() => import("./ComponentGallery-COqEqqN6.js"));
6069
+ const ComponentGallery = lazy(() => import("./ComponentGallery-CzKU899_.js"));
5744
6070
  /** Every component, styled by a configuration; the configurator's preview and the App Canvas Components view. */
5745
6071
  function ComponentPreview(t0) {
5746
6072
  const $ = c(14);
@@ -6671,6 +6997,7 @@ function ConfigurationModalContent({ memory, windowBounds, poppedOut, floating,
6671
6997
  const [mode, setMode] = useState(remembered.mode ?? "light");
6672
6998
  const [sizeUnit, setSizeUnit] = useState(remembered.sizeUnit ?? "px");
6673
6999
  const [loadedFonts, refreshLoadedFonts] = useLoadedFonts();
7000
+ const [fontsourceFamilies, setFontsourceFamilies] = useState([]);
6674
7001
  const [editorWidth, setEditorWidth] = useState(remembered.editorWidth);
6675
7002
  const [confirmingReset, setConfirmingReset] = useState(false);
6676
7003
  const [nothingToReset, setNothingToReset] = useState(false);
@@ -7037,6 +7364,7 @@ function ConfigurationModalContent({ memory, windowBounds, poppedOut, floating,
7037
7364
  const options = [
7038
7365
  ...themeFonts,
7039
7366
  ...loadedFonts.filter((font) => !themeFonts.some(({ value: value_11 }) => value_11 === font.value)),
7367
+ ...fontsourceFamilies.filter((font_0) => !loadedFonts.some((loaded) => loaded.label === font_0.label)),
7040
7368
  ...systemFonts
7041
7369
  ];
7042
7370
  const roleLabels = {
@@ -7059,6 +7387,23 @@ function ConfigurationModalContent({ memory, windowBounds, poppedOut, floating,
7059
7387
  highlighted: highlightedToken === name_24
7060
7388
  }, `${revision}-${resetRevision}-${name_24}`))
7061
7389
  }),
7390
+ /* @__PURE__ */ jsx(FontsourcePicker, {
7391
+ roles: roles.map(([token_0]) => ({
7392
+ token: token_0,
7393
+ label: roleLabels[token_0] ?? labelFor(token_0).replace(/^font /, "")
7394
+ })),
7395
+ onAdded: (font_1, role) => {
7396
+ const value_13 = `${JSON.stringify(font_1.family)}, sans-serif`;
7397
+ setFontsourceFamilies((current_12) => [...current_12.filter((item) => item.name !== `fontsource:${font_1.packageName}`), {
7398
+ name: `fontsource:${font_1.packageName}`,
7399
+ label: font_1.family,
7400
+ value: value_13,
7401
+ group: "Fontsource fonts"
7402
+ }]);
7403
+ refreshLoadedFonts();
7404
+ if (role) setToken(role, value_13);
7405
+ }
7406
+ }),
7062
7407
  /* @__PURE__ */ jsx(FontUpload, { onUpload: refreshLoadedFonts }),
7063
7408
  !!families.length && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("hr", { className: "studio-font-divider" }), families.map((entry_2) => renderToken(entry_2))] })
7064
7409
  ]
@@ -7242,8 +7587,8 @@ function ConfigurationModalContent({ memory, windowBounds, poppedOut, floating,
7242
7587
  role: "tab",
7243
7588
  "aria-selected": !searching && tab_0 === activeSectionTab,
7244
7589
  disabled: searching,
7245
- onClick: () => setSectionTabs((current_12) => ({
7246
- ...current_12,
7590
+ onClick: () => setSectionTabs((current_13) => ({
7591
+ ...current_13,
7247
7592
  [section]: tab_0
7248
7593
  })),
7249
7594
  children: [tab_0, groupChanged([...groupedEntries].filter(([key_14]) => tabFor(key_14) === tab_0).flatMap(([, group_6]) => group_6.entries)) && /* @__PURE__ */ jsx("span", {
@@ -7431,10 +7776,10 @@ function ConfigurationModalContent({ memory, windowBounds, poppedOut, floating,
7431
7776
  children: [/* @__PURE__ */ jsxs("span", { children: ["Use static inputs until interaction", /* @__PURE__ */ jsx("small", { children: "Reduce work when rendering large forms." })] }), /* @__PURE__ */ jsx("input", {
7432
7777
  type: "checkbox",
7433
7778
  checked: config.renderStaticInput,
7434
- onChange: (event_5) => setDraft((current_13) => ({
7435
- ...current_13,
7779
+ onChange: (event_5) => setDraft((current_14) => ({
7780
+ ...current_14,
7436
7781
  config: {
7437
- ...current_13.config,
7782
+ ...current_14.config,
7438
7783
  renderStaticInput: event_5.target.checked
7439
7784
  }
7440
7785
  }))
@@ -1,4 +1,4 @@
1
- import { n as AppCanvasPage, r as AppCanvasFrameBridge, t as APP_CANVAS_PATH } from "../AppCanvasPage-DtWEvs_Y.js";
1
+ import { n as AppCanvasPage, r as AppCanvasFrameBridge, t as APP_CANVAS_PATH } from "../AppCanvasPage-vta1j5YQ.js";
2
2
  import { c } from "react/compiler-runtime";
3
3
  import { jsx } from "react/jsx-runtime";
4
4
  import { createRoute, notFound, useRouter, useRouterState } from "@tanstack/react-router";
@@ -1,2 +1,2 @@
1
- import { a as defaultCanvasBreakpoints, c as layoutCanvasPages, i as AppCanvas, n as AppCanvasPage, o as isAppCanvasFrame, r as AppCanvasFrameBridge, s as discoverCanvasPages, t as APP_CANVAS_PATH } from "../AppCanvasPage-DtWEvs_Y.js";
1
+ import { a as defaultCanvasBreakpoints, c as layoutCanvasPages, i as AppCanvas, n as AppCanvasPage, o as isAppCanvasFrame, r as AppCanvasFrameBridge, s as discoverCanvasPages, t as APP_CANVAS_PATH } from "../AppCanvasPage-vta1j5YQ.js";
2
2
  export { APP_CANVAS_PATH, AppCanvas, AppCanvasFrameBridge, AppCanvasPage, defaultCanvasBreakpoints, discoverCanvasPages, isAppCanvasFrame, layoutCanvasPages };
@@ -12,6 +12,8 @@ interface UIConfiguratorPluginOptions {
12
12
  * `ui-configuration.json` beside the theme file. Import it with `resolveUIConfiguration`. false disables it.
13
13
  */
14
14
  configurationFile?: string | false;
15
+ /** Existing app CSS entrypoint for installed Fontsource imports. false disables font installation. */
16
+ fontCssFile?: string | false;
15
17
  }
16
18
  /** Adds the configurator's Save theme action to this app's development server. */
17
19
  declare function uiConfigurator(options?: UIConfiguratorPluginOptions): Plugin;
@@ -1,6 +1,9 @@
1
+ import { createRequire } from "node:module";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
- import { lstat, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
3
+ import { access, lstat, readFile, realpath, rename, unlink, writeFile } from "node:fs/promises";
3
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { execFile } from "node:child_process";
6
+ import { promisify } from "node:util";
4
7
  //#region src/configurator/repository-storage-protocol.ts
5
8
  const componentConfigurationMetaName = "povio-ui-component-configuration";
6
9
  /** Dev-server event sent when a repository storage file changes, so other tabs reload it. */
@@ -13,7 +16,7 @@ var StorageError = class extends Error {
13
16
  this.status = status;
14
17
  }
15
18
  };
16
- const inside = (root, path) => {
19
+ const inside$1 = (root, path) => {
17
20
  const rel = relative(root, path);
18
21
  return !!rel && !isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`);
19
22
  };
@@ -46,7 +49,7 @@ async function configureRepositoryStorage(server, { filename, route, field, labe
46
49
  const root = await realpath(server.config.root);
47
50
  const filePath = resolve(root, filename);
48
51
  const parent = dirname(filePath);
49
- if (!inside(root, filePath) || !filePath.endsWith(".json")) throw new Error(`Povio UI ${noun} file must be a JSON file inside this app's Vite root.`);
52
+ if (!inside$1(root, filePath) || !filePath.endsWith(".json")) throw new Error(`Povio UI ${noun} file must be a JSON file inside this app's Vite root.`);
50
53
  const guard = async () => {
51
54
  if (await realpath(parent) !== parent) throw new StorageError(`${label} file parent cannot be a symbolic link.`, 409);
52
55
  try {
@@ -391,6 +394,231 @@ function configureComponentConfigurationStorage(server, filename) {
391
394
  });
392
395
  }
393
396
  //#endregion
397
+ //#region src/configurator/theme-writer-protocol.ts
398
+ /** Shared by the browser configurator and the isolated Node Vite entry. */
399
+ const themeWriterMetaName = "povio-ui-theme-writer";
400
+ //#endregion
401
+ //#region src/configurator/fontsource-vite.ts
402
+ const execute = promisify(execFile);
403
+ const fontId = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
404
+ const versionPattern = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
405
+ var FontError = class extends Error {
406
+ constructor(message, status = 400) {
407
+ super(message);
408
+ this.status = status;
409
+ }
410
+ };
411
+ const dependencies = {
412
+ fetch: (...args) => globalThis.fetch(...args),
413
+ install: async (command, args, cwd) => {
414
+ await execute(command, args, {
415
+ cwd,
416
+ shell: false,
417
+ timeout: 18e4,
418
+ maxBuffer: 1048576,
419
+ env: {
420
+ ...process.env,
421
+ npm_config_ignore_scripts: "true",
422
+ YARN_ENABLE_SCRIPTS: "false"
423
+ }
424
+ });
425
+ }
426
+ };
427
+ const exists = async (file) => {
428
+ try {
429
+ await access(file);
430
+ return true;
431
+ } catch {
432
+ return false;
433
+ }
434
+ };
435
+ const inside = (root, file) => {
436
+ const path = relative(root, file);
437
+ return !!path && !isAbsolute(path) && path !== ".." && !path.startsWith(`..${sep}`);
438
+ };
439
+ async function manager(root) {
440
+ let directory = root;
441
+ while (true) {
442
+ let declared = "";
443
+ try {
444
+ declared = JSON.parse(await readFile(resolve(directory, "package.json"), "utf8")).packageManager ?? "";
445
+ } catch {}
446
+ if (/^(bun|npm|pnpm|yarn)@/.test(declared)) return {
447
+ name: declared.split("@")[0],
448
+ modernYarn: /^yarn@[2-9]/.test(declared)
449
+ };
450
+ for (const [file, name] of [
451
+ ["bun.lock", "bun"],
452
+ ["bun.lockb", "bun"],
453
+ ["pnpm-lock.yaml", "pnpm"],
454
+ ["package-lock.json", "npm"],
455
+ ["yarn.lock", "yarn"]
456
+ ]) if (await exists(resolve(directory, file))) return {
457
+ name,
458
+ modernYarn: name === "yarn" && (await readFile(resolve(directory, file), "utf8")).includes("__metadata:")
459
+ };
460
+ const parent = dirname(directory);
461
+ if (parent === directory) break;
462
+ directory = parent;
463
+ }
464
+ throw new FontError("No existing package manager was detected. Add a lockfile or packageManager field first.", 409);
465
+ }
466
+ async function metadata(fetcher, url) {
467
+ const response = await fetcher(url, {
468
+ redirect: "error",
469
+ signal: AbortSignal.timeout(2e4)
470
+ });
471
+ if (!response.ok) throw new FontError(response.status === 404 ? "Fontsource font/package was not found." : "Fontsource metadata could not be loaded.", response.status === 404 ? 400 : 502);
472
+ return response.json();
473
+ }
474
+ /** Internal service factory: injection is for server tests, never a plugin/client execution option. */
475
+ async function createFontsourceInstaller(rootInput, cssName, deps = dependencies) {
476
+ const root = await realpath(rootInput), css = resolve(root, cssName), packageFile = resolve(root, "package.json");
477
+ if (!inside(root, css) || !css.endsWith(".css")) throw new FontError("fontCssFile must be a CSS file inside the app root.");
478
+ if (!await exists(css) || !await exists(packageFile)) return void 0;
479
+ const guard = async () => {
480
+ if (await realpath(css) !== css || await realpath(packageFile) !== packageFile) throw new FontError("Font CSS and package.json must not use symbolic links.", 409);
481
+ };
482
+ await guard();
483
+ let pending = Promise.resolve();
484
+ const install = (id) => {
485
+ const task = pending.then(async () => {
486
+ if (typeof id !== "string" || id.length > 100 || !fontId.test(id)) throw new FontError("Expected a valid Fontsource font ID.");
487
+ await guard();
488
+ const font = await metadata(deps.fetch, `https://api.fontsource.org/v1/fonts/${id}`);
489
+ if (font.id !== id || typeof font.family !== "string" || !font.family.trim() || !Array.isArray(font.weights) || !Array.isArray(font.styles)) throw new FontError("Invalid Fontsource font metadata.", 502);
490
+ let variable = false;
491
+ if (font.variable) variable = !!(await metadata(deps.fetch, `https://api.fontsource.org/v1/variable/${id}`)).axes?.wght;
492
+ const packageName = `${variable ? "@fontsource-variable" : "@fontsource"}/${id}`;
493
+ const registry = await metadata(deps.fetch, `https://registry.npmjs.org/${encodeURIComponent(packageName)}/latest`);
494
+ if (registry.name !== packageName || typeof registry.version !== "string" || !versionPattern.test(registry.version)) throw new FontError("Invalid npm package version metadata.", 502);
495
+ const weights = font.weights.filter((weight) => typeof weight === "number" && Number.isInteger(weight) && weight >= 1 && weight <= 1e3);
496
+ const styles = ["normal", "italic"].filter((style) => font.styles.includes(style));
497
+ if (!weights.length || !styles.length) throw new FontError("This font has no supported styles.");
498
+ const imports = variable ? styles.map((style) => `${packageName}/wght${style === "italic" ? "-italic" : ""}.css`) : weights.flatMap((weight) => styles.map((style) => `${packageName}/${weight}${style === "italic" ? "-italic" : ""}.css`));
499
+ const require = createRequire(packageFile);
500
+ const installed = async (exact = false) => {
501
+ try {
502
+ const declared = JSON.parse(await readFile(packageFile, "utf8"));
503
+ if (!declared.dependencies?.[packageName] && !declared.devDependencies?.[packageName]) return false;
504
+ const pkg = JSON.parse(await readFile(require.resolve(`${packageName}/package.json`), "utf8"));
505
+ if (exact && pkg.version !== registry.version) return false;
506
+ if (!versionPattern.test(pkg.version) || pkg.version.split(".")[0] !== registry.version.split(".")[0]) return false;
507
+ await Promise.all(imports.map((item) => access(require.resolve(item))));
508
+ return true;
509
+ } catch {
510
+ return false;
511
+ }
512
+ };
513
+ const alreadyInstalled = await installed();
514
+ if (!alreadyInstalled) {
515
+ const pm = await manager(root);
516
+ const spec = `${packageName}@${registry.version}`;
517
+ const args = pm.name === "npm" ? [
518
+ "install",
519
+ "--save-exact",
520
+ "--ignore-scripts",
521
+ spec
522
+ ] : pm.name === "yarn" ? [
523
+ "add",
524
+ "--exact",
525
+ ...pm.modernYarn ? [] : ["--ignore-scripts"],
526
+ spec
527
+ ] : [
528
+ "add",
529
+ "--exact",
530
+ "--ignore-scripts",
531
+ spec
532
+ ];
533
+ if (pm.name === "pnpm") args[1] = "--save-exact";
534
+ try {
535
+ await deps.install(pm.name, args, root);
536
+ } catch {
537
+ throw new FontError("Font installation failed. Existing CSS imports were left unchanged.", 502);
538
+ }
539
+ if (!await installed(true)) throw new FontError("Installed font files could not be verified. CSS imports were left unchanged.", 502);
540
+ }
541
+ await guard();
542
+ const source = await readFile(css, "utf8");
543
+ const existing = new Set([...source.matchAll(/@import\s+(?:url\(\s*)?["']([^"']+)["']/g)].map((match) => match[1]));
544
+ const missing = imports.filter((item) => !existing.has(item));
545
+ if (missing.length) {
546
+ const additions = missing.map((item) => `@import "${item}";`).join("\n");
547
+ const charset = source.match(/^(?:\uFEFF)?@charset\s+["'][^"']+["'];\s*/i)?.[0] ?? "";
548
+ const next = `${charset}${additions}\n${source.slice(charset.length)}`;
549
+ const temporary = `${css}.fontsource-${randomUUID()}.tmp`;
550
+ try {
551
+ await writeFile(temporary, next, { flag: "wx" });
552
+ await guard();
553
+ await rename(temporary, css);
554
+ } finally {
555
+ await unlink(temporary).catch(() => {});
556
+ }
557
+ }
558
+ return {
559
+ family: `${font.family}${variable ? " Variable" : ""}`,
560
+ packageName,
561
+ imports,
562
+ alreadyInstalled
563
+ };
564
+ });
565
+ pending = task.then(() => {}, () => {});
566
+ return task;
567
+ };
568
+ return {
569
+ cssFile: relative(root, css).split(sep).join("/"),
570
+ install
571
+ };
572
+ }
573
+ async function configureFontsource(server, cssName, token, base) {
574
+ const installer = await createFontsourceInstaller(server.config.root, cssName);
575
+ if (!installer) return void 0;
576
+ const endpoint = `${base}__povio_ui/fontsource`;
577
+ server.middlewares.use((req, res, next) => {
578
+ if (req.url?.split("?")[0] !== endpoint) return next();
579
+ handle(req, res).catch((error) => respond(res, error instanceof FontError ? error.status : 500, { error: error instanceof Error ? error.message : "Could not install font." }));
580
+ });
581
+ function respond(res, status, body) {
582
+ res.writeHead(status, {
583
+ "Content-Type": "application/json",
584
+ "Cache-Control": "no-store"
585
+ });
586
+ res.end(JSON.stringify(body));
587
+ }
588
+ async function handle(req, res) {
589
+ if (req.method !== "POST") throw new FontError("Use POST to install a font.", 405);
590
+ if (req.headers["x-povio-ui-theme-token"] !== token) throw new FontError("Invalid theme token.", 403);
591
+ if (req.headers["sec-fetch-site"] === "cross-site") throw new FontError("Font installation requires the app origin.", 403);
592
+ if (req.headers.origin) {
593
+ let sameOrigin = false;
594
+ try {
595
+ sameOrigin = new URL(req.headers.origin).origin === `${server.config.server.https ? "https" : "http"}://${req.headers.host}`;
596
+ } catch {}
597
+ if (!sameOrigin) throw new FontError("Font installation requires the app origin.", 403);
598
+ }
599
+ if (!req.headers["content-type"]?.startsWith("application/json")) throw new FontError("Expected application/json.", 415);
600
+ let size = 0;
601
+ const chunks = [];
602
+ for await (const chunk of req) {
603
+ size += Buffer.byteLength(chunk);
604
+ if (size > 1024) throw new FontError("Font request is too large.", 413);
605
+ chunks.push(Buffer.from(chunk));
606
+ }
607
+ let data;
608
+ try {
609
+ data = JSON.parse(Buffer.concat(chunks).toString("utf8"));
610
+ } catch {
611
+ throw new FontError("Expected JSON containing a font ID.");
612
+ }
613
+ const id = data && typeof data === "object" && !Array.isArray(data) ? data.id : void 0;
614
+ respond(res, 200, await installer.install(id));
615
+ }
616
+ return {
617
+ endpoint,
618
+ cssFile: installer.cssFile
619
+ };
620
+ }
621
+ //#endregion
394
622
  //#region src/configurator/theme-model.ts
395
623
  const declarations = (source) => Object.fromEntries([...source.matchAll(/(--[\w-]+)\s*:\s*([^;]+);/g)].map(([, name, value]) => [name, value.trim()]));
396
624
  const selectorDeclarations = (source, selector) => Object.assign({}, ...[...source.matchAll(selector)].map((match) => declarations(match[1])));
@@ -460,10 +688,6 @@ function updateThemeSource(source, exportedCss) {
460
688
  return source;
461
689
  }
462
690
  //#endregion
463
- //#region src/configurator/theme-writer-protocol.ts
464
- /** Shared by the browser configurator and the isolated Node Vite entry. */
465
- const themeWriterMetaName = "povio-ui-theme-writer";
466
- //#endregion
467
691
  //#region src/configurator/vite.ts
468
692
  var ThemeWriteError = class extends Error {
469
693
  constructor(message, status) {
@@ -512,13 +736,15 @@ function uiConfigurator(options = {}) {
512
736
  if (!file || isAbsolute(file) || file === ".." || file.startsWith(`..${sep}`)) throw new Error("Povio UI themeFile must be inside this app's Vite root.");
513
737
  if (!themeFile.endsWith(".css")) throw new Error("Povio UI themeFile must be a CSS file.");
514
738
  if (options.configurationFile !== false) componentStorage = await configureComponentConfigurationStorage(server, options.configurationFile ?? join(dirname(file), "ui-configuration.json"));
515
- const endpoint = `${new URL(server.config.base, "http://localhost").pathname.replace(/\/?$/, "/")}__povio_ui/theme`;
739
+ const base = new URL(server.config.base, "http://localhost").pathname.replace(/\/?$/, "/");
740
+ const endpoint = `${base}__povio_ui/theme`;
516
741
  connection = {
517
742
  endpoint,
518
743
  token: randomUUID(),
519
744
  file: file.split(sep).join("/")
520
745
  };
521
746
  const token = connection.token;
747
+ if (options.fontCssFile !== false) connection.fontsource = await configureFontsource(server, options.fontCssFile ?? "src/styles/globals.css", token, base);
522
748
  const save = async (request, response) => {
523
749
  if (request.method !== "POST") {
524
750
  response.setHeader("Allow", "POST");
@@ -1,3 +1,3 @@
1
- import { d as useUIConfiguration, i as resolveUIConfiguration, n as ColorModeSwitch, t as UIConfigurationModal, u as UIConfigurationProvider } from "../UIConfigurator-DekPXjv9.js";
1
+ import { d as useUIConfiguration, i as resolveUIConfiguration, n as ColorModeSwitch, t as UIConfigurationModal, u as UIConfigurationProvider } from "../UIConfigurator-CjAW1SJY.js";
2
2
  import { i as exportCss, v as parseThemeCss } from "../theme-model-DtTsIxou.js";
3
3
  export { ColorModeSwitch, UIConfigurationModal, UIConfigurationProvider, exportCss, parseThemeCss, resolveUIConfiguration, useUIConfiguration };
@@ -3692,3 +3692,205 @@
3692
3692
  outline: 2px solid #555;
3693
3693
  outline-offset: 1px;
3694
3694
  }
3695
+ /* Fontsource browsing stays compact even when the preview theme changes. */
3696
+ .studio-fontsource {
3697
+ display: flex;
3698
+ align-items: center;
3699
+ gap: 8px;
3700
+ padding-top: 8px;
3701
+ }
3702
+ .studio-fontsource-notice {
3703
+ color: #777;
3704
+ font-size: 10px;
3705
+ }
3706
+ .studio-fontsource-popover {
3707
+ width: min(340px, calc(100vw - 24px));
3708
+ max-height: min(680px, calc(100dvh - 24px));
3709
+ overflow: auto;
3710
+ padding: 12px;
3711
+ border: 1px solid #e5e5e5;
3712
+ border-radius: 10px;
3713
+ background: #fff;
3714
+ color: #303036;
3715
+ box-shadow: 0 8px 32px #24202d26;
3716
+ font:
3717
+ 11px/1.4 Inter,
3718
+ system-ui,
3719
+ sans-serif;
3720
+ z-index: 10020;
3721
+ }
3722
+ .studio-fontsource-popover * {
3723
+ box-sizing: border-box;
3724
+ }
3725
+ .studio-fontsource-popover [role="dialog"] {
3726
+ outline: none;
3727
+ }
3728
+ .studio-fontsource-popover header {
3729
+ display: flex;
3730
+ align-items: center;
3731
+ justify-content: space-between;
3732
+ gap: 8px;
3733
+ margin-bottom: 10px;
3734
+ }
3735
+ .studio-fontsource-popover h2 {
3736
+ margin: 0;
3737
+ font-size: 12px;
3738
+ font-weight: 600;
3739
+ }
3740
+ .studio-fontsource-popover button,
3741
+ .studio-fontsource-popover input,
3742
+ .studio-fontsource-popover select {
3743
+ font: inherit;
3744
+ color: inherit;
3745
+ }
3746
+ .studio-fontsource-popover button {
3747
+ border: 1px solid #e5e5e5;
3748
+ border-radius: 5px;
3749
+ padding: 4px 7px;
3750
+ background: #fff;
3751
+ cursor: pointer;
3752
+ }
3753
+ .studio-fontsource-popover button:disabled {
3754
+ opacity: 0.5;
3755
+ cursor: default;
3756
+ }
3757
+ .studio-fontsource-popover header button {
3758
+ display: grid;
3759
+ place-items: center;
3760
+ padding: 3px;
3761
+ border: 0;
3762
+ }
3763
+ .studio-fontsource-popover input:not([type="checkbox"]),
3764
+ .studio-fontsource-popover select {
3765
+ min-width: 0;
3766
+ width: 100%;
3767
+ border: 1px solid #e5e5e5;
3768
+ border-radius: 5px;
3769
+ padding: 6px 7px;
3770
+ background: #fafafa;
3771
+ }
3772
+ .studio-fontsource-popover :focus-visible {
3773
+ outline: 2px solid #7655d7;
3774
+ outline-offset: 1px;
3775
+ }
3776
+ .studio-fontsource-search {
3777
+ position: relative;
3778
+ display: flex;
3779
+ align-items: center;
3780
+ }
3781
+ .studio-fontsource-search svg {
3782
+ position: absolute;
3783
+ left: 8px;
3784
+ color: #888;
3785
+ }
3786
+ .studio-fontsource-popover .studio-fontsource-search input {
3787
+ padding-left: 27px;
3788
+ }
3789
+ .studio-fontsource-filters {
3790
+ display: flex;
3791
+ align-items: center;
3792
+ gap: 8px;
3793
+ margin: 8px 0;
3794
+ }
3795
+ .studio-fontsource-filters select {
3796
+ flex: 1;
3797
+ }
3798
+ .studio-fontsource-filters label {
3799
+ display: flex;
3800
+ align-items: center;
3801
+ gap: 4px;
3802
+ white-space: nowrap;
3803
+ }
3804
+ .studio-fontsource-filters input {
3805
+ margin: 0;
3806
+ accent-color: #7655d7;
3807
+ }
3808
+ .studio-fontsource-count {
3809
+ display: block;
3810
+ margin: 8px 0 3px;
3811
+ font-size: 10px;
3812
+ color: #888;
3813
+ }
3814
+ .studio-fontsource-list {
3815
+ display: grid;
3816
+ gap: 2px;
3817
+ max-height: 180px;
3818
+ overflow: auto;
3819
+ padding: 3px;
3820
+ margin-inline: -3px;
3821
+ }
3822
+ .studio-fontsource-list button {
3823
+ display: grid;
3824
+ grid-template-columns: 1fr auto;
3825
+ align-items: center;
3826
+ text-align: left;
3827
+ border-color: transparent;
3828
+ padding: 5px 7px;
3829
+ }
3830
+ .studio-fontsource-list small {
3831
+ grid-column: 1;
3832
+ color: #888;
3833
+ font-size: 9px;
3834
+ }
3835
+ .studio-fontsource-list svg {
3836
+ grid-row: 1 / span 2;
3837
+ grid-column: 2;
3838
+ }
3839
+ .studio-fontsource-list button:hover {
3840
+ background: #f6f4fa;
3841
+ }
3842
+ .studio-fontsource-list button[aria-pressed="true"] {
3843
+ color: #6540c2;
3844
+ background: #eee8ff;
3845
+ }
3846
+ .studio-fontsource-preview {
3847
+ border-top: 1px solid #eee;
3848
+ margin-top: 10px;
3849
+ padding-top: 10px;
3850
+ }
3851
+ .studio-fontsource-preview > strong {
3852
+ display: block;
3853
+ margin-bottom: 6px;
3854
+ }
3855
+ .studio-fontsource-sample {
3856
+ margin: 10px 0;
3857
+ font-size: 26px;
3858
+ line-height: 1.4;
3859
+ overflow-wrap: anywhere;
3860
+ }
3861
+ .studio-fontsource-preview > [role="status"] {
3862
+ color: #888;
3863
+ font-size: 10px;
3864
+ }
3865
+ .studio-fontsource-role {
3866
+ display: flex;
3867
+ align-items: center;
3868
+ gap: 8px;
3869
+ margin: 8px 0;
3870
+ }
3871
+ .studio-fontsource-role select {
3872
+ flex: 1;
3873
+ }
3874
+ .studio-fontsource-popover .studio-primary {
3875
+ width: 100%;
3876
+ background: #7655d7;
3877
+ border-color: #7655d7;
3878
+ color: white;
3879
+ }
3880
+ .studio-fontsource-help {
3881
+ margin: 10px 0 0;
3882
+ color: #888;
3883
+ font-size: 10px;
3884
+ line-height: 1.5;
3885
+ }
3886
+ .studio-fontsource-popover [role="alert"] {
3887
+ color: #ad2937;
3888
+ }
3889
+ .studio-fontsource-popover [role="alert"] button {
3890
+ margin-left: 8px;
3891
+ }
3892
+ .studio-fontsource-popover .studio-fontsource-more {
3893
+ width: 100%;
3894
+ margin-top: 5px;
3895
+ font-size: 10px;
3896
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@povio/ui",
3
- "version": "5.0.0-rc.13",
3
+ "version": "5.0.0-rc.14",
4
4
  "type": "module",
5
5
  "module": "dist/index.js",
6
6
  "types": "dist/index.d.ts",