pagiera 0.2.0-alpha.46 → 0.2.0-alpha.48
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/data.js +16 -9
- package/dist/data.js.map +2 -2
- package/dist/full-editor.js +21 -3
- package/dist/full-editor.js.map +2 -2
- package/dist/internal/app/api/ai-design/route.d.ts +24 -0
- package/dist/internal/app/api/ai-design/route.d.ts.map +1 -1
- package/dist/internal/app/api/ai-design/route.js +697 -125
- package/dist/internal/app/api/ai-design/route.js.map +1 -1
- package/dist/internal/app/p/editor/ai-panel.d.ts.map +1 -1
- package/dist/internal/app/p/editor/ai-panel.js +6 -1
- package/dist/internal/app/p/editor/ai-panel.js.map +1 -1
- package/dist/internal/app/p/editor/editor.d.ts.map +1 -1
- package/dist/internal/app/p/editor/editor.js +8 -3
- package/dist/internal/app/p/editor/editor.js.map +1 -1
- package/dist/internal/lib/data/source.d.ts.map +1 -1
- package/dist/internal/lib/data/source.js +38 -9
- package/dist/internal/lib/data/source.js.map +1 -1
- package/dist/internal/lib/editor/ai/compose.d.ts +34 -0
- package/dist/internal/lib/editor/ai/compose.d.ts.map +1 -0
- package/dist/internal/lib/editor/ai/compose.js +569 -0
- package/dist/internal/lib/editor/ai/compose.js.map +1 -0
- package/dist/internal/lib/editor/ai/dsl.d.ts +162 -0
- package/dist/internal/lib/editor/ai/dsl.d.ts.map +1 -0
- package/dist/internal/lib/editor/ai/dsl.js +278 -0
- package/dist/internal/lib/editor/ai/dsl.js.map +1 -0
- package/dist/internal/lib/editor/ai/ops.d.ts +0 -2
- package/dist/internal/lib/editor/ai/ops.d.ts.map +1 -1
- package/dist/internal/lib/editor/ai/ops.js +0 -12
- package/dist/internal/lib/editor/ai/ops.js.map +1 -1
- package/dist/internal/lib/editor/ai/theme.d.ts +112 -1
- package/dist/internal/lib/editor/ai/theme.d.ts.map +1 -1
- package/dist/internal/lib/editor/ai/theme.js +296 -1
- package/dist/internal/lib/editor/ai/theme.js.map +1 -1
- package/dist/internal/lib/render/css.d.ts.map +1 -1
- package/dist/internal/lib/render/css.js +8 -0
- package/dist/internal/lib/render/css.js.map +1 -1
- package/dist/internal/lib/render/page-render.d.ts.map +1 -1
- package/dist/internal/lib/render/page-render.js +13 -0
- package/dist/internal/lib/render/page-render.js.map +1 -1
- package/dist/runtime.js +14 -1
- package/dist/runtime.js.map +2 -2
- package/dist/server.js +1409 -1360
- package/dist/server.js.map +4 -4
- package/package.json +1 -1
- package/dist/internal/lib/editor/ai/blocks.d.ts +0 -58
- package/dist/internal/lib/editor/ai/blocks.d.ts.map +0 -1
- package/dist/internal/lib/editor/ai/blocks.js +0 -815
- package/dist/internal/lib/editor/ai/blocks.js.map +0 -1
- package/dist/internal/lib/editor/ai/build.d.ts +0 -78
- package/dist/internal/lib/editor/ai/build.d.ts.map +0 -1
- package/dist/internal/lib/editor/ai/build.js +0 -337
- package/dist/internal/lib/editor/ai/build.js.map +0 -1
- package/dist/internal/lib/editor/ai/plan.d.ts +0 -44
- package/dist/internal/lib/editor/ai/plan.d.ts.map +0 -1
- package/dist/internal/lib/editor/ai/plan.js +0 -217
- package/dist/internal/lib/editor/ai/plan.js.map +0 -1
package/dist/data.js
CHANGED
|
@@ -15,6 +15,12 @@ var BLOCKED_HOSTS = /* @__PURE__ */ new Set([
|
|
|
15
15
|
]);
|
|
16
16
|
var PRIVATE_IPV4 = /^(10\.|127\.|0\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/;
|
|
17
17
|
var PRIVATE_SUFFIXES = [".local", ".internal", ".localdomain", ".home.arpa"];
|
|
18
|
+
function allowedPrivateHosts() {
|
|
19
|
+
const raw = typeof process === "undefined" ? "" : process.env?.PAGIERA_ALLOW_PRIVATE_HOSTS ?? "";
|
|
20
|
+
return new Set(
|
|
21
|
+
raw.split(",").map((host) => host.trim().toLowerCase()).map((host) => host.replace(/:\d+$/, "")).filter(Boolean)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
18
24
|
var MAX_ROWS = 200;
|
|
19
25
|
var MAX_BYTES = 2e6;
|
|
20
26
|
var TIMEOUT_MS = 8e3;
|
|
@@ -37,15 +43,16 @@ function assertFetchableUrl(raw) {
|
|
|
37
43
|
throw new DataSourceError("Only http and https URLs can be requested.");
|
|
38
44
|
}
|
|
39
45
|
const host = url.hostname.toLowerCase();
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
46
|
+
const allowed = allowedPrivateHosts();
|
|
47
|
+
if (allowed.has(host)) return url;
|
|
48
|
+
const refuse = (reason) => {
|
|
49
|
+
throw new DataSourceError(
|
|
50
|
+
`${reason} To use it, add it to PAGIERA_ALLOW_PRIVATE_HOSTS (for example PAGIERA_ALLOW_PRIVATE_HOSTS=localhost,127.0.0.1) and restart the server.`
|
|
51
|
+
);
|
|
52
|
+
};
|
|
53
|
+
if (BLOCKED_HOSTS.has(host) || PRIVATE_IPV4.test(host)) refuse(`Requests to ${host} are not allowed.`);
|
|
54
|
+
if (PRIVATE_SUFFIXES.some((suffix) => host.endsWith(suffix))) refuse(`Requests to ${host} are not allowed.`);
|
|
55
|
+
if (host.startsWith("[") && /^\[(fe80|fc|fd)/i.test(host)) refuse("Requests to private addresses are not allowed.");
|
|
49
56
|
return url;
|
|
50
57
|
}
|
|
51
58
|
function selectPath(payload, path) {
|
package/dist/data.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/internal/lib/editor/types.ts", "../src/internal/lib/data/source.ts", "../src/internal/lib/render/load-data.ts"],
|
|
4
|
-
"sourcesContent": ["export const ELEMENT_TYPES = [\n \"Frame\",\n \"Stack\",\n \"Section\",\n \"Container\",\n \"Grid\",\n \"Heading\",\n \"Text\",\n \"Image\",\n \"Button\",\n \"Video\",\n \"Icon\",\n \"Divider\",\n \"Spacer\",\n \"List\",\n \"ListItem\",\n \"Quote\",\n \"Embed\",\n \"Form\",\n \"Fieldset\",\n \"Label\",\n \"Input\",\n \"Textarea\",\n \"Select\",\n \"Checkbox\",\n \"Radio\",\n \"FileInput\",\n \"Request\",\n \"Repeat\",\n] as const;\n\nexport type ElementType = (typeof ELEMENT_TYPES)[number];\n\n/** One key/value pair on a request; values may carry `{{\u2026}}` tokens. */\nexport type RequestPair = { key: string; value: string };\n\nexport const HTTP_METHODS = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"] as const;\nexport type HttpMethod = (typeof HTTP_METHODS)[number];\n\nexport const DATA_SOURCE_NOT_FOUND_BEHAVIORS = [\"empty\", \"page-404\"] as const;\nexport type DataSourceNotFoundBehavior = (typeof DATA_SOURCE_NOT_FOUND_BEHAVIORS)[number];\n\n/** Methods that carry a request body. */\nexport function sendsBody(method: HttpMethod) {\n return method === \"POST\" || method === \"PUT\" || method === \"PATCH\";\n}\n\n/**\n * A JSON endpoint the page pulls content from. Sources live on the page so a\n * Repeat block can name one without carrying the URL on every element.\n */\nexport type DataSource = {\n id: string;\n name: string;\n url: string;\n /** Dotted path to the array inside the payload; \"\" when it is the root. */\n path: string;\n method?: HttpMethod;\n /** JSON body for POST/PUT/PATCH; tokens are resolved before sending. */\n body?: string;\n /** Appended to the URL as a query string. */\n params?: RequestPair[];\n /** Sent as request headers \u2014 for API keys and the like. */\n headers?: RequestPair[];\n /** Controls whether an upstream 404 empties this source or rejects the whole page. */\n onNotFound?: DataSourceNotFoundBehavior;\n};\n\n/**\n * Values a request token can read. Everything else resolves to an empty\n * string, so a typo cannot leak an unrelated value into the URL.\n */\nexport type RequestContext = {\n /** The visitor's query string, e.g. `{{query.id}}` on /post?id=5. */\n query: Record<string, string>;\n /** Values captured by a dynamic page path, e.g. `{{params.slug}}`. */\n params: Record<string, string>;\n /** The page being rendered, e.g. `{{page.slug}}`. */\n page: { slug: string };\n};\n\n/* ------------------------------------------------------------------ fields */\n\nexport const INPUT_TYPES = [\n \"text\",\n \"email\",\n \"password\",\n \"number\",\n \"tel\",\n \"url\",\n \"search\",\n \"date\",\n \"time\",\n \"datetime-local\",\n \"month\",\n \"week\",\n \"color\",\n \"range\",\n \"hidden\",\n] as const;\nexport type InputType = (typeof INPUT_TYPES)[number];\n\n/** One choice offered by a Select or a Radio group. */\nexport type FieldOption = { label: string; value: string };\n\n/** A raw HTML attribute the author put on an element. */\nexport type ElementAttribute = { name: string; value: string };\n\n/**\n * Tags an element may be rendered as instead of the one its type implies.\n *\n * Only non-void, inert tags are listed. A void tag (`img`, `br`, `input`)\n * cannot hold the children the canvas puts inside it, and a script-bearing one\n * would turn a semantic choice into a way to run code on the published page.\n */\nexport const CUSTOM_TAGS = [\n \"div\", \"span\", \"p\", \"section\", \"article\", \"aside\", \"header\", \"footer\", \"main\", \"nav\",\n \"figure\", \"figcaption\", \"blockquote\", \"address\",\n \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\",\n \"ul\", \"ol\", \"li\", \"dl\", \"dt\", \"dd\",\n \"details\", \"summary\", \"fieldset\", \"legend\", \"label\",\n \"strong\", \"em\", \"small\", \"mark\", \"code\", \"pre\", \"time\",\n \"table\", \"thead\", \"tbody\", \"tfoot\", \"tr\", \"th\", \"td\", \"caption\",\n] as const;\nexport type CustomTag = (typeof CUSTOM_TAGS)[number];\n\n/**\n * Attributes an author may never set by hand.\n *\n * An `on*` handler would run typed-in script in the visitor's page, and the\n * rest belong to the renderer: overwriting `class`, `style` or `id` would\n * detach the element from the stylesheet generated for it. The inspector\n * offers dedicated fields for those three instead.\n */\nexport const RESERVED_ATTRIBUTES: ReadonlySet<string> = new Set([\n \"class\",\n \"classname\",\n \"style\",\n \"id\",\n \"srcdoc\",\n \"is\",\n \"dangerouslysetinnerhtml\",\n]);\n\n/** Attributes whose value is a URL, so it has to pass the URL safety check. */\nexport const URL_ATTRIBUTES: ReadonlySet<string> = new Set([\n \"href\",\n \"src\",\n \"action\",\n \"formaction\",\n \"poster\",\n \"cite\",\n \"ping\",\n \"background\",\n]);\n\nexport type ResizeHandle = \"nw\" | \"ne\" | \"sw\" | \"se\" | \"n\" | \"s\" | \"w\" | \"e\";\n\n/* ------------------------------------------------------------- breakpoints */\n\nexport const BREAKPOINTS = [\"desktop\", \"tablet\", \"mobile\"] as const;\nexport type Breakpoint = string;\n\nexport type BreakpointDefinition = {\n id: string;\n name: string;\n width: number;\n};\n\nexport const DEFAULT_BREAKPOINTS: BreakpointDefinition[] = [\n { id: \"desktop\", name: \"Desktop\", width: 1280 },\n { id: \"tablet\", name: \"Tablet\", width: 768 },\n { id: \"mobile\", name: \"Mobile\", width: 375 },\n];\n\nexport const BREAKPOINT_WIDTHS: Record<string, number> = {\n desktop: 1280,\n tablet: 768,\n mobile: 375,\n};\n\n/**\n * Styles cascade from the widest breakpoint down, so a value set on desktop\n * holds everywhere until a narrower breakpoint overrides it.\n */\nexport const BREAKPOINT_CHAIN: Record<string, Breakpoint[]> = {\n desktop: [\"desktop\"],\n tablet: [\"desktop\", \"tablet\"],\n mobile: [\"desktop\", \"tablet\", \"mobile\"],\n};\n\n/* ------------------------------------------------------------------ styles */\n\n/** `fill` stretches to the parent, `auto` shrinks to the content. */\nexport type SizeMode = \"fixed\" | \"fill\" | \"auto\" | \"screen\";\nexport type Constraint = \"start\" | \"center\" | \"end\" | \"stretch\";\n/** `absolute` positions children by x/y; `stack` lays them out with flexbox. */\nexport type LayoutMode = \"absolute\" | \"stack\";\nexport type Direction = \"row\" | \"column\";\nexport type Justify = \"start\" | \"center\" | \"end\" | \"between\";\nexport type Align = \"start\" | \"center\" | \"end\" | \"stretch\";\n/** A child's own cross-axis alignment; `auto` defers to the parent's `align`. */\nexport type AlignSelf = \"auto\" | \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\";\n/** How wrapped lines are distributed along the cross axis. */\nexport type AlignContent = \"start\" | \"center\" | \"end\" | \"stretch\" | \"between\";\nexport type ListStyle = \"bullet\" | \"number\" | \"none\";\nexport type TextAlign = \"left\" | \"center\" | \"right\" | \"justify\";\nexport type TextTransform = \"none\" | \"uppercase\" | \"lowercase\" | \"capitalize\";\nexport type ObjectFit = \"cover\" | \"contain\" | \"fill\" | \"none\";\nexport type Overflow = \"visible\" | \"hidden\" | \"auto\" | \"scroll\";\nexport type Entrance = \"none\" | \"fade\" | \"up\" | \"down\" | \"left\" | \"right\" | \"zoom\";\nexport type MotionCurve = \"ease\" | \"spring\";\nexport type CursorStyle = \"auto\" | \"default\" | \"pointer\" | \"text\" | \"grab\" | \"zoom-in\" | \"none\";\n/**\n * How an element sits relative to its siblings.\n *\n * `absolute` is per element, not per container: it lifts this one out of the\n * flow and places it at x/y while everything around it keeps stacking. Making\n * the whole parent free instead would move every sibling to satisfy one of\n * them.\n */\nexport type PositionMode = \"static\" | \"sticky\" | \"fixed\" | \"absolute\";\n/** Which edge a pinned element holds to. */\nexport type PinSide = \"top\" | \"bottom\" | \"left\" | \"right\";\nexport type BgSize = \"cover\" | \"contain\" | \"auto\";\nexport type BlendMode =\n | \"normal\"\n | \"multiply\"\n | \"screen\"\n | \"overlay\"\n | \"darken\"\n | \"lighten\"\n | \"difference\"\n | \"luminosity\";\nexport type BorderStyle = \"solid\" | \"dashed\" | \"dotted\";\n\nexport type ElementStyle = {\n // Box \u2014 x/y only apply inside an `absolute` parent.\n x: number;\n y: number;\n constraintX: Constraint;\n constraintY: Constraint;\n w: number;\n h: number;\n widthMode: SizeMode;\n heightMode: SizeMode;\n\n // How this element arranges its own children.\n layout: LayoutMode;\n direction: Direction;\n gap: number;\n padT: number;\n padR: number;\n padB: number;\n padL: number;\n /**\n * Space held below the element, outside its own box.\n *\n * Separate from `padB` on purpose: padding is the breathing room the\n * author gave the content inside a section, while this is the distance to\n * whatever comes next. Sharing one value would make adjusting the rhythm\n * between sections quietly reflow their insides.\n */\n marginB: number;\n justify: Justify;\n align: Align;\n wrap: boolean;\n /**\n * Per-axis gaps. `gap` stays the value both axes fall back to, so an\n * author who only ever wanted one number never has to see these two; they\n * start at -1 meaning \"unset\" rather than 0, which is a real gap.\n */\n rowGap: number;\n columnGap: number;\n /** Distributes wrapped lines; only meaningful once `wrap` is on. */\n alignContent: AlignContent;\n /** Grid columns; only read when `layout` is `stack` on a Grid element. */\n columns: number;\n\n // How this element behaves as a child of its parent's layout.\n /** Overrides the parent's `align` for this one child. */\n alignSelf: AlignSelf;\n /**\n * Share of the leftover main-axis space this child takes.\n *\n * -1 means \"decide from the size mode\", which is what `fill` and `auto`\n * have always done. A number set here wins, so two siblings can split a row\n * 2:1 without either of them being a fixed width.\n */\n grow: number;\n /** Visual position among siblings, independent of document order. */\n order: number;\n /** Columns this child spans inside a Grid parent. */\n gridSpan: number;\n\n // Appearance\n bg: string;\n /**\n * Alpha applied to `bg` alone, as a percentage.\n *\n * Distinct from `opacity`, which fades the whole element \u2014 its text, its\n * border and the result of its own `backdropBlur` along with it. Glass is\n * a see-through *background* over a blurred backdrop, so it needs the fill\n * to be translucent while everything drawn on top stays solid.\n */\n bgOpacity: number;\n /** A full CSS gradient value, or \"\" for none. Painted over `bg`. */\n gradient: string;\n color: string;\n radius: number;\n opacity: number;\n borderW: number;\n /** Per-edge override; null inherits borderW. */\n borderT: number | null;\n borderR: number | null;\n borderB: number | null;\n borderL: number | null;\n borderC: string;\n borderStyle: BorderStyle;\n /** A full CSS box-shadow value, or \"\" for none. */\n shadow: string;\n rotate: number;\n\n // Typography\n fontFamily: string;\n fontSize: number;\n fontWeight: string;\n lineHeight: number;\n letterSpacing: number;\n textAlign: TextAlign;\n textTransform: TextTransform;\n\n // Composition \u2014 the pieces that make a layout feel designed rather than\n // stacked: clipping, sticky rails, imagery, glass and blend effects.\n overflow: Overflow;\n position: PositionMode;\n /** CSS stacking order, independent from the internal document order. */\n zIndex: number;\n /** Distance from `pinSide` while pinned; read when position is sticky or fixed. */\n stickyOffset: number;\n /** Edge a sticky or fixed element pins to. */\n pinSide: PinSide;\n /** Background image URL, or \"\" for none. Painted over `gradient`. */\n bgImage: string;\n bgSize: BgSize;\n bgPosition: string;\n /** Blurs the element's own content, in px. */\n blur: number;\n /** Blurs whatever sits behind the element, in px \u2014 the glass effect. */\n backdropBlur: number;\n blendMode: BlendMode;\n /** Percent; 100 leaves the element alone. */\n scale: number;\n /** A CSS ratio such as \"16/9\", or \"\" to leave height to the layout. */\n aspectRatio: string;\n\n /** Entrance effect, played once when the element scrolls into view. */\n entrance: Entrance;\n /** Milliseconds. */\n entranceDuration: number;\n entranceDelay: number;\n entranceCurve: MotionCurve;\n entranceBezier: string;\n springStiffness: number;\n springDamping: number;\n cursor: CursorStyle;\n\n /** Hidden at this breakpoint. */\n hidden: boolean;\n};\n\nexport const STYLE_KEYS = [\n \"x\",\n \"y\",\n \"constraintX\",\n \"constraintY\",\n \"w\",\n \"h\",\n \"widthMode\",\n \"heightMode\",\n \"layout\",\n \"direction\",\n \"gap\",\n \"padT\",\n \"padR\",\n \"padB\",\n \"padL\",\n \"marginB\",\n \"justify\",\n \"align\",\n \"wrap\",\n \"rowGap\",\n \"columnGap\",\n \"alignContent\",\n \"columns\",\n \"alignSelf\",\n \"grow\",\n \"order\",\n \"gridSpan\",\n \"bg\",\n \"bgOpacity\",\n \"gradient\",\n \"color\",\n \"radius\",\n \"opacity\",\n \"borderW\",\n \"borderT\",\n \"borderR\",\n \"borderB\",\n \"borderL\",\n \"borderC\",\n \"borderStyle\",\n \"shadow\",\n \"rotate\",\n \"fontFamily\",\n \"fontSize\",\n \"fontWeight\",\n \"lineHeight\",\n \"letterSpacing\",\n \"textAlign\",\n \"textTransform\",\n \"overflow\",\n \"position\",\n \"zIndex\",\n \"stickyOffset\",\n \"pinSide\",\n \"bgImage\",\n \"bgSize\",\n \"bgPosition\",\n \"blur\",\n \"backdropBlur\",\n \"blendMode\",\n \"scale\",\n \"aspectRatio\",\n \"entrance\",\n \"entranceDuration\",\n \"entranceDelay\",\n \"entranceCurve\",\n \"entranceBezier\",\n \"springStiffness\",\n \"springDamping\",\n \"cursor\",\n \"hidden\",\n] as const satisfies ReadonlyArray<keyof ElementStyle>;\n\nexport type StyleKey = (typeof STYLE_KEYS)[number];\n\nexport type CanvasElement = {\n id: string;\n type: ElementType;\n name?: string;\n parentId?: string;\n z: number;\n locked?: boolean;\n /** Page-local reusable component metadata. */\n componentRole?: \"master\" | \"instance\";\n componentId?: string;\n componentSourceId?: string;\n variant?: string;\n styleBindings?: Partial<Record<StyleKey, string>>;\n\n // Content is shared across breakpoints.\n content?: string;\n /** Sandboxed HTML/CSS used by code components. */\n code?: string;\n src?: string;\n alt?: string;\n objectFit?: ObjectFit;\n iconName?: PagieraIconName;\n /** Marker style for a List; also decides whether it publishes as ol or ul. */\n listStyle?: ListStyle;\n /**\n * An author-supplied SVG glyph, used instead of `iconName` when set.\n *\n * Stored as sanitised markup rather than a URL so the icon inherits\n * `currentColor` and scales with the element \u2014 an `<img>` would do\n * neither, and a remote URL would make every visitor fetch a third party.\n */\n svg?: string;\n placeholder?: string;\n fieldName?: string;\n inputType?: InputType;\n required?: boolean;\n disabled?: boolean;\n readOnly?: boolean;\n /** Pre-filled value for a field; the submitted value for Checkbox/Radio. */\n defaultValue?: string;\n /** Whether a Checkbox or Radio starts selected. */\n checked?: boolean;\n /** Choices offered by Select and Radio. */\n options?: FieldOption[];\n /** Select and FileInput may take more than one value. */\n multiple?: boolean;\n /** FileInput filter, e.g. \"image/*,.pdf\". */\n accept?: string;\n /** Native constraint validation; numeric and date fields read min/max/step. */\n minValue?: string;\n maxValue?: string;\n step?: string;\n pattern?: string;\n minLength?: number;\n maxLength?: number;\n autocomplete?: string;\n /** Id of the element a Label is bound to. */\n labelFor?: string;\n formAction?: string;\n formMethod?: HttpMethod;\n formSubmitMode?: \"request\" | \"native\";\n formContentType?: \"json\" | \"form-data\" | \"urlencoded\";\n /** Optional request body. `{{form.email}}` tokens read submitted fields. */\n formBody?: string;\n /** One `Header: value` pair per line. */\n formHeaders?: string;\n formSuccessMessage?: string;\n formErrorMessage?: string;\n formResetOnSuccess?: boolean;\n buttonType?: \"button\" | \"submit\" | \"reset\";\n href?: string;\n target?: \"_self\" | \"_blank\";\n\n /**\n * Renders this element as a different HTML tag than its type implies \u2014 the\n * escape hatch for semantics the type list does not model, such as a Stack\n * that should publish as a `<ul>`.\n */\n tag?: CustomTag;\n /** Extra class names, appended after the generated one. */\n customClass?: string;\n /** Inline CSS declarations, applied over every generated rule. */\n customStyle?: string;\n /** Raw HTML attributes; `RESERVED_ATTRIBUTES` lists what is refused. */\n attributes?: ElementAttribute[];\n interaction?: {\n trigger: \"click\";\n action: \"navigate\" | \"scroll-to\" | \"toggle-layer\" | \"show-layer\" | \"hide-layer\";\n value: string;\n target?: \"_self\" | \"_blank\";\n };\n\n /** Data source read by a Request/Repeat block or a directly-bound element. */\n sourceId?: string;\n /**\n * Inside Request/Repeat, pulls this field off the current object instead\n * of using the element's content. Dotted paths work: \"author.name\".\n */\n binding?: string;\n\n /** Desktop values; every breakpoint falls back to these. */\n base: ElementStyle;\n /** Narrower-breakpoint deltas, applied over `base` in cascade order. */\n overrides?: Record<string, Partial<ElementStyle>>;\n /** Applied on pointer hover, on top of the resolved breakpoint style. */\n hover?: Partial<ElementStyle>;\n /** Whether hover is activated by this node or its immediate parent. */\n hoverTrigger?: \"self\" | \"parent\";\n /** Applied while the pointer is pressed. */\n press?: Partial<ElementStyle>;\n loop?: { type: \"pulse\" | \"float\" | \"spin\"; duration: number };\n draggable?: boolean;\n};\n\n/* ---------------------------------------------------------------- defaults */\n\nexport const BASE_STYLE: ElementStyle = {\n x: 0,\n y: 0,\n constraintX: \"start\",\n constraintY: \"start\",\n w: 200,\n h: 100,\n widthMode: \"fixed\",\n heightMode: \"fixed\",\n\n layout: \"absolute\",\n direction: \"column\",\n gap: 0,\n padT: 0,\n padR: 0,\n padB: 0,\n padL: 0,\n marginB: 0,\n justify: \"start\",\n align: \"start\",\n wrap: false,\n rowGap: -1,\n columnGap: -1,\n alignContent: \"start\",\n columns: 3,\n\n alignSelf: \"auto\",\n grow: -1,\n order: 0,\n gridSpan: 1,\n\n bg: \"transparent\",\n bgOpacity: 100,\n gradient: \"\",\n color: \"#27272a\",\n radius: 0,\n opacity: 100,\n borderW: 0,\n borderT: null,\n borderR: null,\n borderB: null,\n borderL: null,\n borderC: \"transparent\",\n borderStyle: \"solid\",\n shadow: \"\",\n rotate: 0,\n\n fontFamily: \"inherit\",\n fontSize: 16,\n fontWeight: \"normal\",\n lineHeight: 1.5,\n letterSpacing: 0,\n textAlign: \"left\",\n textTransform: \"none\",\n\n overflow: \"visible\",\n position: \"static\",\n zIndex: 0,\n stickyOffset: 0,\n pinSide: \"top\",\n bgImage: \"\",\n bgSize: \"cover\",\n bgPosition: \"center\",\n blur: 0,\n backdropBlur: 0,\n blendMode: \"normal\",\n scale: 100,\n aspectRatio: \"\",\n\n entrance: \"none\",\n entranceDuration: 600,\n entranceDelay: 0,\n entranceCurve: \"ease\",\n entranceBezier: \"0.44, 0, 0.56, 1\",\n springStiffness: 300,\n springDamping: 30,\n cursor: \"auto\",\n\n hidden: false,\n};\n\nexport function makeStyle(overrides: Partial<ElementStyle>): ElementStyle {\n return { ...BASE_STYLE, ...overrides };\n}\n\n/** Every field a freshly dropped element starts with. */\nexport const ELEMENT_DEFAULTS: Record<\n ElementType,\n { style: Partial<ElementStyle>; props?: Partial<CanvasElement> }\n> = {\n Frame: {\n style: {\n w: 640,\n h: 420,\n widthMode: \"fixed\",\n heightMode: \"fixed\",\n layout: \"absolute\",\n direction: \"column\",\n overflow: \"hidden\",\n bg: \"#ffffff\",\n borderW: 1,\n borderC: \"#e4e4e7\",\n radius: 12,\n },\n },\n Stack: {\n style: {\n w: 600,\n h: 160,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 16,\n padT: 0,\n padR: 0,\n padB: 0,\n padL: 0,\n justify: \"start\",\n align: \"stretch\",\n bg: \"transparent\",\n },\n },\n Section: {\n style: {\n w: 1280,\n h: 480,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 24,\n padT: 64,\n padR: 48,\n padB: 64,\n padL: 48,\n justify: \"start\",\n align: \"stretch\",\n bg: \"#ffffff\",\n },\n },\n Container: {\n style: {\n w: 600,\n h: 240,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 16,\n padT: 24,\n padR: 24,\n padB: 24,\n padL: 24,\n align: \"stretch\",\n bg: \"transparent\",\n borderW: 1,\n borderC: \"#e4e4e7\",\n radius: 12,\n },\n },\n Grid: {\n style: {\n w: 900,\n h: 300,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"row\",\n columns: 3,\n gap: 24,\n align: \"stretch\",\n },\n },\n Heading: {\n style: {\n w: 480,\n h: 48,\n widthMode: \"fill\",\n heightMode: \"auto\",\n fontSize: 40,\n fontWeight: \"bold\",\n lineHeight: 1.2,\n letterSpacing: -0.5,\n color: \"#18181b\",\n },\n props: { content: \"Heading\" },\n },\n Text: {\n style: {\n w: 480,\n h: 24,\n widthMode: \"fill\",\n heightMode: \"auto\",\n fontSize: 16,\n lineHeight: 1.6,\n color: \"#52525b\",\n },\n props: { content: \"Text block content\" },\n },\n Image: {\n style: { w: 400, h: 260, widthMode: \"fill\", bg: \"#e4e4e7\", radius: 12 },\n props: { src: \"\", alt: \"\", objectFit: \"cover\" },\n },\n Button: {\n style: {\n w: 140,\n h: 44,\n widthMode: \"auto\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"row\",\n justify: \"center\",\n align: \"center\",\n padT: 12,\n padR: 22,\n padB: 12,\n padL: 22,\n bg: \"#2563eb\",\n color: \"#ffffff\",\n radius: 8,\n fontSize: 15,\n fontWeight: \"500\",\n textAlign: \"center\",\n },\n props: { content: \"Button\", href: \"\", target: \"_self\", buttonType: \"button\" },\n },\n Video: {\n style: { w: 640, h: 360, widthMode: \"fill\", bg: \"#18181b\", radius: 12 },\n props: { src: \"\" },\n },\n Icon: {\n style: { w: 24, h: 24, widthMode: \"fixed\", heightMode: \"fixed\", color: \"#5402E6\" },\n props: { iconName: \"star\" },\n },\n Form: {\n style: {\n w: 560,\n h: 240,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 14,\n align: \"stretch\",\n },\n props: {\n formAction: \"\",\n formMethod: \"POST\",\n formSubmitMode: \"request\",\n formContentType: \"json\",\n formSuccessMessage: \"Sent successfully.\",\n formErrorMessage: \"Something went wrong.\",\n },\n },\n Input: {\n style: {\n w: 320,\n h: 46,\n widthMode: \"fill\",\n heightMode: \"fixed\",\n padT: 0,\n padR: 14,\n padB: 0,\n padL: 14,\n bg: \"#ffffff\",\n color: \"#18181b\",\n borderW: 1,\n borderC: \"#d4d4d8\",\n radius: 10,\n fontSize: 15,\n },\n props: { placeholder: \"Enter a value\u2026\", fieldName: \"field\", inputType: \"text\" },\n },\n Textarea: {\n style: {\n w: 320,\n h: 120,\n widthMode: \"fill\",\n heightMode: \"fixed\",\n padT: 12,\n padR: 14,\n padB: 12,\n padL: 14,\n bg: \"#ffffff\",\n color: \"#18181b\",\n borderW: 1,\n borderC: \"#d4d4d8\",\n radius: 10,\n fontSize: 15,\n },\n props: { placeholder: \"Write your message\u2026\", fieldName: \"message\" },\n },\n Divider: {\n style: {\n w: 600,\n h: 1,\n widthMode: \"fill\",\n heightMode: \"fixed\",\n bg: \"#e4e4e7\",\n marginB: 0,\n },\n },\n Spacer: {\n // Height is the whole point of a spacer, so it starts fixed; setting\n // Grow in the inspector turns it into the flexible kind that pushes\n // siblings apart.\n style: {\n w: 600,\n h: 40,\n widthMode: \"fill\",\n heightMode: \"fixed\",\n bg: \"transparent\",\n },\n },\n List: {\n style: {\n w: 480,\n h: 120,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 8,\n align: \"stretch\",\n padL: 22,\n fontSize: 16,\n lineHeight: 1.6,\n color: \"#52525b\",\n },\n props: { listStyle: \"bullet\" },\n },\n ListItem: {\n style: {\n w: 480,\n h: 24,\n widthMode: \"fill\",\n heightMode: \"auto\",\n fontSize: 16,\n lineHeight: 1.6,\n color: \"#52525b\",\n },\n props: { content: \"List item\" },\n },\n Quote: {\n style: {\n w: 560,\n h: 96,\n widthMode: \"fill\",\n heightMode: \"auto\",\n padT: 4,\n padL: 20,\n borderL: 3,\n borderC: \"#d4d4d8\",\n fontSize: 20,\n lineHeight: 1.55,\n color: \"#3f3f46\",\n },\n props: { content: \"A quotation worth pulling out of the paragraph.\" },\n },\n Embed: {\n style: { w: 640, h: 400, widthMode: \"fill\", bg: \"#f4f4f5\", radius: 12 },\n props: { src: \"\" },\n },\n Fieldset: {\n style: {\n w: 560,\n h: 200,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 12,\n padT: 18,\n padR: 18,\n padB: 18,\n padL: 18,\n align: \"stretch\",\n borderW: 1,\n borderC: \"#e4e4e7\",\n radius: 12,\n },\n },\n Label: {\n style: {\n w: 200,\n h: 20,\n widthMode: \"fill\",\n heightMode: \"auto\",\n fontSize: 13,\n fontWeight: \"500\",\n lineHeight: 1.4,\n color: \"#3f3f46\",\n },\n props: { content: \"Label\", labelFor: \"\" },\n },\n Select: {\n style: {\n w: 320,\n h: 46,\n widthMode: \"fill\",\n heightMode: \"fixed\",\n padT: 0,\n padR: 14,\n padB: 0,\n padL: 14,\n bg: \"#ffffff\",\n color: \"#18181b\",\n borderW: 1,\n borderC: \"#d4d4d8\",\n radius: 10,\n fontSize: 15,\n },\n props: {\n fieldName: \"choice\",\n placeholder: \"Choose an option\u2026\",\n options: [\n { label: \"Option one\", value: \"one\" },\n { label: \"Option two\", value: \"two\" },\n ],\n },\n },\n Checkbox: {\n style: {\n w: 18,\n h: 18,\n widthMode: \"fixed\",\n heightMode: \"fixed\",\n color: \"#2563eb\",\n },\n props: { fieldName: \"accept\", defaultValue: \"yes\" },\n },\n Radio: {\n style: {\n w: 240,\n h: 96,\n widthMode: \"fill\",\n heightMode: \"auto\",\n color: \"#2563eb\",\n fontSize: 14,\n lineHeight: 1.6,\n },\n props: {\n fieldName: \"plan\",\n options: [\n { label: \"Monthly\", value: \"monthly\" },\n { label: \"Yearly\", value: \"yearly\" },\n ],\n },\n },\n FileInput: {\n style: {\n w: 320,\n h: 46,\n widthMode: \"fill\",\n heightMode: \"fixed\",\n padT: 0,\n padR: 14,\n padB: 0,\n padL: 14,\n bg: \"#ffffff\",\n color: \"#18181b\",\n borderW: 1,\n borderC: \"#d4d4d8\",\n radius: 10,\n fontSize: 15,\n },\n props: { fieldName: \"file\", accept: \"\" },\n },\n Request: {\n style: {\n w: 900,\n h: 200,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 16,\n align: \"stretch\",\n },\n props: { sourceId: \"\" },\n },\n Repeat: {\n style: {\n w: 900,\n h: 200,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"row\",\n gap: 24,\n align: \"stretch\",\n wrap: true,\n },\n props: { sourceId: \"\" },\n },\n};\n\n/* ----------------------------------------------------------------- helpers */\n\nconst CONTAINER_TYPES: ReadonlySet<ElementType> = new Set([\n \"Frame\",\n \"Stack\",\n \"Section\",\n \"Container\",\n \"Grid\",\n \"Button\",\n \"Form\",\n \"Fieldset\",\n \"List\",\n \"ListItem\",\n \"Request\",\n \"Repeat\",\n]);\n\n/** Whether an element can hold children. */\nexport function isContainer(type: ElementType) {\n return CONTAINER_TYPES.has(type);\n}\n\nconst TEXTUAL_TYPES: ReadonlySet<ElementType> = new Set([\n \"Heading\",\n \"Text\",\n \"Button\",\n \"Label\",\n \"Quote\",\n \"ListItem\",\n]);\n\n/** Whether an element renders editable text. */\nexport function isTextual(type: ElementType) {\n return TEXTUAL_TYPES.has(type);\n}\n\nconst FIELD_TYPES: ReadonlySet<ElementType> = new Set([\n \"Input\",\n \"Textarea\",\n \"Select\",\n \"Checkbox\",\n \"Radio\",\n \"FileInput\",\n]);\n\n/** Whether an element submits a value with the form around it. */\nexport function isField(type: ElementType) {\n return FIELD_TYPES.has(type);\n}\n\n/** Whether an element's value is chosen from a list the author writes. */\nexport function hasOptions(type: ElementType) {\n return type === \"Select\" || type === \"Radio\";\n}\n\n/**\n * Elements that draw nothing of their own \u2014 they exist to take up room.\n *\n * They are worth naming because the inspector has nothing to say about their\n * content, and the canvas has to hint at them or they look like a bug.\n */\nconst STRUCTURAL_TYPES: ReadonlySet<ElementType> = new Set([\"Divider\", \"Spacer\"]);\n\nexport function isStructural(type: ElementType) {\n return STRUCTURAL_TYPES.has(type);\n}\n\n/** Page-level settings; the canvas behaves as the root container. */\nexport type RootStyle = {\n documentMode: \"page\" | \"component\";\n /** Content is centred inside this width unless `fullWidth` is set. */\n maxWidth: number;\n /** Editable design-surface height; the public page can still grow with content. */\n canvasHeight: number;\n /** Let content run edge to edge instead of being capped at `maxWidth`. */\n fullWidth: boolean;\n bg: string;\n layout: LayoutMode;\n direction: Direction;\n gap: number;\n padT: number;\n padR: number;\n padB: number;\n padL: number;\n align: Align;\n fontFamily: string;\n /** Cross-document animation used by links on the published site. */\n pageTransition: \"smooth\" | \"fade\" | \"slide\" | \"none\";\n /** Duration of the incoming published-page animation in milliseconds. */\n pageTransitionDuration: number;\n /** User-defined viewport previews, ordered from widest to narrowest. */\n breakpoints?: BreakpointDefinition[];\n /** The breakpoint whose values live in every element's `base` style. */\n baseBreakpointId?: string;\n variables?: DesignVariable[];\n customFonts?: CustomFont[];\n /**\n * Raw CSS appended after the generated sheet, so it can override any rule\n * the builder produced. Escape hatch for what the inspector cannot express.\n */\n customCss?: string;\n /**\n * Raw JavaScript run on the published page, after the document parses.\n *\n * It is deliberately never executed inside the editor: the canvas and the\n * template preview both render without scripting, so a page cannot reach\n * the editor it is being built in.\n */\n customJs?: string;\n};\n\nexport type CustomFont = { id: string; name: string; url: string; weight: number; style: \"normal\" | \"italic\" };\n\nexport type DesignVariable = {\n id: string;\n name: string;\n type: \"color\" | \"number\";\n value: string | number;\n};\n\nexport const DEFAULT_ROOT_STYLE: RootStyle = {\n documentMode: \"page\",\n maxWidth: 1280,\n canvasHeight: 800,\n fullWidth: false,\n bg: \"#ffffff\",\n layout: \"stack\",\n direction: \"column\",\n gap: 0,\n padT: 0,\n padR: 0,\n padB: 0,\n padL: 0,\n align: \"stretch\",\n fontFamily: \"inherit\",\n pageTransition: \"smooth\",\n pageTransitionDuration: 380,\n};\n\nexport const FONT_STACKS: Array<{ label: string; value: string }> = [\n { label: \"Inherit\", value: \"inherit\" },\n { label: \"Sans\", value: \"ui-sans-serif, system-ui, sans-serif\" },\n { label: \"Serif\", value: \"ui-serif, Georgia, serif\" },\n { label: \"Mono\", value: \"ui-monospace, SFMono-Regular, monospace\" },\n];\n\nexport const SHADOW_PRESETS: Array<{ label: string; value: string }> = [\n { label: \"None\", value: \"\" },\n { label: \"Small\", value: \"0 1px 2px rgba(0,0,0,0.06)\" },\n { label: \"Medium\", value: \"0 4px 12px rgba(0,0,0,0.08)\" },\n { label: \"Large\", value: \"0 12px 32px rgba(0,0,0,0.12)\" },\n { label: \"Glow\", value: \"0 0 0 4px rgba(37,99,235,0.15)\" },\n];\n\nexport const ASPECT_RATIOS: Array<{ label: string; value: string }> = [\n { label: \"Free\", value: \"\" },\n { label: \"Square 1:1\", value: \"1/1\" },\n { label: \"Photo 4:3\", value: \"4/3\" },\n { label: \"Wide 16:9\", value: \"16/9\" },\n { label: \"Ultra 21:9\", value: \"21/9\" },\n { label: \"Portrait 3:4\", value: \"3/4\" },\n];\n\nexport const ENTRANCES: Array<{ label: string; value: Entrance }> = [\n { label: \"None\", value: \"none\" },\n { label: \"Fade in\", value: \"fade\" },\n { label: \"Rise up\", value: \"up\" },\n { label: \"Drop down\", value: \"down\" },\n { label: \"Slide from left\", value: \"left\" },\n { label: \"Slide from right\", value: \"right\" },\n { label: \"Zoom in\", value: \"zoom\" },\n];\n\nexport const DRAG_MIME = \"application/pagiera-element-type\";\n/** Set when dragging an element that already exists on the canvas. */\nexport const MOVE_MIME = \"application/pagiera-element-id\";\nimport type { PagieraIconName } from \"../../../icon-names\";\n", "import {\n type DataSource,\n type HttpMethod,\n type RequestContext,\n type RequestPair,\n sendsBody,\n} from \"@/lib/editor/types\";\n\n/**\n * Fetching an author-supplied URL from our server is a server-side request\n * forgery vector: without checks it can reach cloud metadata endpoints, admin\n * services on localhost, or anything else inside the network perimeter. Every\n * hop is validated against this list, and redirects are followed by hand so a\n * public URL cannot bounce into private space.\n */\nconst BLOCKED_HOSTS = new Set([\n \"localhost\",\n \"127.0.0.1\",\n \"0.0.0.0\",\n \"::1\",\n \"[::1]\",\n \"metadata.google.internal\",\n \"metadata.goog\",\n]);\n\nconst PRIVATE_IPV4 =\n /^(10\\.|127\\.|0\\.|169\\.254\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.)/;\n\n/** Hostnames that only resolve inside a private network. */\nconst PRIVATE_SUFFIXES = [\".local\", \".internal\", \".localdomain\", \".home.arpa\"];\n\nexport const MAX_ROWS = 200;\nconst MAX_BYTES = 2_000_000;\nconst TIMEOUT_MS = 8000;\nconst MAX_REDIRECTS = 3;\n\nexport class DataSourceError extends Error {\n constructor(message: string, public readonly status?: number) {\n super(message);\n this.name = \"DataSourceError\";\n }\n}\n\nexport function assertFetchableUrl(raw: string): URL {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw new DataSourceError(\"That is not a valid URL.\");\n }\n\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n throw new DataSourceError(\"Only http and https URLs can be requested.\");\n }\n\n const host = url.hostname.toLowerCase();\n if (BLOCKED_HOSTS.has(host) || PRIVATE_IPV4.test(host)) {\n throw new DataSourceError(`Requests to ${host} are not allowed.`);\n }\n if (PRIVATE_SUFFIXES.some((suffix) => host.endsWith(suffix))) {\n throw new DataSourceError(`Requests to ${host} are not allowed.`);\n }\n // Bracketed IPv6 literals: anything link-local or unique-local.\n if (host.startsWith(\"[\") && /^\\[(fe80|fc|fd)/i.test(host)) {\n throw new DataSourceError(\"Requests to private addresses are not allowed.\");\n }\n\n return url;\n}\n\n/**\n * Walks a dotted path such as `data.items` or `results.0.tags`. An empty path\n * returns the payload itself, which is what a bare JSON array needs.\n */\nexport function selectPath(payload: unknown, path: string): unknown {\n const steps = path.split(\".\").map((s) => s.trim()).filter(Boolean);\n let cursor: unknown = payload;\n for (const step of steps) {\n if (cursor === null || typeof cursor !== \"object\") return undefined;\n cursor = (cursor as Record<string, unknown>)[step];\n }\n return cursor;\n}\n\n/** Reads a body with a hard cap so a huge response cannot exhaust memory. */\nasync function readCapped(response: Response): Promise<string> {\n const reader = response.body?.getReader();\n if (!reader) return \"\";\n\n const chunks: Uint8Array[] = [];\n let total = 0;\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n total += value.byteLength;\n if (total > MAX_BYTES) {\n await reader.cancel();\n throw new DataSourceError(\"That response is too large (over 2 MB).\");\n }\n chunks.push(value);\n }\n return new TextDecoder().decode(\n chunks.reduce<Uint8Array>((acc, chunk) => {\n const merged = new Uint8Array(acc.length + chunk.length);\n merged.set(acc);\n merged.set(chunk, acc.length);\n return merged;\n }, new Uint8Array()),\n );\n}\n\nexport type SourceResult = { rows: Array<Record<string, unknown>>; keys: string[] };\n\n/**\n * Requests a source and normalizes it to rows. A root object becomes one row\n * for Request; an array stays multiple rows for Repeat.\n */\nexport type LoadOptions = {\n /**\n * Seconds to cache the response for. `false` bypasses the cache \u2014 only for\n * the editor's Test button; on a rendered route it would force an\n * otherwise-static page to become dynamic on every request.\n */\n revalidate?: number | false;\n /** Supplies `{{query.\u2026}}` / `{{page.\u2026}}` values for this request. */\n context?: RequestContext;\n};\n\nexport const DEFAULT_REVALIDATE = 60;\n\nexport async function loadSource(\n source: DataSource,\n { revalidate = DEFAULT_REVALIDATE, context = EMPTY_CONTEXT }: LoadOptions = {},\n): Promise<SourceResult> {\n const request = buildRequest(source, context);\n let target = request.url;\n\n let response: Response | undefined;\n for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {\n response = await fetch(target, {\n // Redirects are followed by hand so each hop is validated too.\n redirect: \"manual\",\n method: request.method,\n headers: request.headers,\n body: request.body,\n signal: AbortSignal.timeout(TIMEOUT_MS),\n // Only GET responses are cacheable; anything with side effects is\n // fetched fresh every time.\n ...(revalidate === false || request.method !== \"GET\"\n ? { cache: \"no-store\" as const }\n : { next: { revalidate } }),\n });\n\n if (response.status < 300 || response.status >= 400) break;\n\n const location = response.headers.get(\"location\");\n if (!location) break;\n target = assertFetchableUrl(new URL(location, target).toString());\n response = undefined;\n }\n\n if (!response) throw new DataSourceError(\"Too many redirects.\");\n if (!response.ok) {\n throw new DataSourceError(`The API replied ${response.status}.`, response.status);\n }\n\n const text = await readCapped(response);\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n throw new DataSourceError(\"The response was not JSON.\");\n }\n\n const selected = selectPath(payload, source.path);\n const selectedRows = Array.isArray(selected)\n ? selected\n : selected !== null && typeof selected === \"object\"\n ? [selected]\n : undefined;\n if (!selectedRows) {\n throw new DataSourceError(\n source.path\n ? `\"${source.path}\" is not a list in that response.`\n : \"That response is neither an object nor a list.\",\n );\n }\n\n const rows = selectedRows\n .slice(0, MAX_ROWS)\n .map((row) =>\n row !== null && typeof row === \"object\" && !Array.isArray(row)\n ? (row as Record<string, unknown>)\n : { value: row },\n );\n\n // Union of the keys across the sample, so binding menus list everything.\n const keys = [...new Set(rows.flatMap((row) => Object.keys(row)))].sort();\n return { rows, keys };\n}\n\n/** Reads one binding path off a row and flattens it to something renderable. */\nexport function readBinding(row: Record<string, unknown>, path: string): string {\n const value = selectPath(row, path);\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"object\") return \"\";\n return String(value);\n}\n\n/* ------------------------------------------------------------------ tokens */\n\n/**\n * Resolves `{{query.id}}`, `{{params.slug}}` and `{{page.slug}}`.\n *\n * Only these two namespaces exist: an unknown token becomes an empty string\n * rather than being left in the URL, so a typo cannot send a literal `{{\u2026}}`\n * to the API or accidentally expose something else.\n */\nexport function resolveTokens(value: string, context: RequestContext): string {\n return value.replace(/\\{\\{\\s*([a-zA-Z0-9_.]+)\\s*\\}\\}/g, (_, token: string) => {\n const [namespace, ...rest] = token.split(\".\");\n const key = rest.join(\".\");\n if (namespace === \"query\") return context.query[key] ?? \"\";\n if (namespace === \"params\") return context.params[key] ?? \"\";\n if (namespace === \"page\" && key === \"slug\") return context.page.slug;\n return \"\";\n });\n}\n\n/** Header names and values must not be able to inject extra headers. */\nfunction safeHeader(pair: RequestPair, context: RequestContext) {\n const name = pair.key.trim();\n if (!/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(name)) return null;\n // Strip anything that could terminate the header line.\n const value = resolveTokens(pair.value, context).replace(/[\\r\\n\\0]/g, \"\").trim();\n return value ? ([name, value] as const) : null;\n}\n\n/**\n * Builds the final URL: tokens resolved, params appended and percent-encoded\n * by `URLSearchParams` so a value can never break out into the query grammar.\n */\nexport function buildRequest(source: DataSource, context: RequestContext) {\n const url = assertFetchableUrl(resolveTokens(source.url, context));\n\n for (const pair of source.params ?? []) {\n const key = pair.key.trim();\n if (!key) continue;\n const value = resolveTokens(pair.value, context);\n // An empty optional filter is dropped rather than sent as `key=`.\n if (value === \"\") continue;\n url.searchParams.set(key, value);\n }\n\n const method: HttpMethod = source.method ?? \"GET\";\n\n const headers: Record<string, string> = {\n accept: \"application/json,text/plain;q=0.9,*/*;q=0.8\",\n };\n\n let body: string | undefined;\n if (sendsBody(method) && source.body?.trim()) {\n body = resolveTokens(source.body, context);\n headers[\"content-type\"] = \"application/json\";\n }\n\n // Author headers last, so they can override the defaults above.\n for (const pair of source.headers ?? []) {\n const safe = safeHeader(pair, context);\n if (safe) headers[safe[0]] = safe[1];\n }\n\n // Re-check after tokens: a token could have rewritten the host.\n return { url: assertFetchableUrl(url.toString()), headers, method, body };\n}\n\nexport const EMPTY_CONTEXT: RequestContext = { query: {}, params: {}, page: { slug: \"\" } };\n", "import { DataSourceError, type LoadOptions, loadSource } from \"@/lib/data/source\";\nimport type { CanvasElement, DataSource } from \"@/lib/editor/types\";\nimport type { PageData } from \"./bind\";\n\n/** Signals that a source elected to turn its upstream 404 into a page 404. */\nexport class PageDataNotFoundError extends Error {\n constructor(public readonly sourceId: string) {\n super(`Data source ${sourceId} returned 404.`);\n this.name = \"PageDataNotFoundError\";\n }\n}\n\n/**\n * Fetches every source a page actually uses. Sources nothing references are\n * skipped, and a source that fails yields no rows rather than failing the\n * whole page \u2014 a broken API should not take the site down.\n */\nexport async function loadPageData(\n elements: CanvasElement[],\n sources: DataSource[],\n options?: LoadOptions,\n): Promise<PageData> {\n const used = new Set(\n elements\n .filter((el) => el.sourceId)\n .map((el) => el.sourceId as string),\n );\n const wanted = sources.filter((source) => used.has(source.id) && source.url);\n if (wanted.length === 0) return {};\n\n const results = await Promise.all(\n wanted.map(async (source) => {\n try {\n const { rows } = await loadSource(source, options);\n return [source.id, rows] as const;\n } catch (error) {\n if (\n error instanceof DataSourceError &&\n error.status === 404 &&\n source.onNotFound === \"page-404\"\n ) {\n throw new PageDataNotFoundError(source.id);\n }\n console.warn(\n `Data source \"${source.name}\" failed:`,\n error instanceof DataSourceError ? error.message : error,\n );\n return [source.id, []] as const;\n }\n }),\n );\n\n return Object.fromEntries(results);\n}\n"],
|
|
5
|
-
"mappings": ";AA2CO,SAAS,UAAU,QAAoB;AAC1C,SAAO,WAAW,UAAU,WAAW,SAAS,WAAW;AAC/D;;;AC9BA,IAAM,gBAAgB,oBAAI,IAAI;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,CAAC;AAED,IAAM,eACF;AAGJ,IAAM,mBAAmB,CAAC,UAAU,aAAa,gBAAgB,YAAY;
|
|
4
|
+
"sourcesContent": ["export const ELEMENT_TYPES = [\n \"Frame\",\n \"Stack\",\n \"Section\",\n \"Container\",\n \"Grid\",\n \"Heading\",\n \"Text\",\n \"Image\",\n \"Button\",\n \"Video\",\n \"Icon\",\n \"Divider\",\n \"Spacer\",\n \"List\",\n \"ListItem\",\n \"Quote\",\n \"Embed\",\n \"Form\",\n \"Fieldset\",\n \"Label\",\n \"Input\",\n \"Textarea\",\n \"Select\",\n \"Checkbox\",\n \"Radio\",\n \"FileInput\",\n \"Request\",\n \"Repeat\",\n] as const;\n\nexport type ElementType = (typeof ELEMENT_TYPES)[number];\n\n/** One key/value pair on a request; values may carry `{{\u2026}}` tokens. */\nexport type RequestPair = { key: string; value: string };\n\nexport const HTTP_METHODS = [\"GET\", \"POST\", \"PUT\", \"PATCH\", \"DELETE\"] as const;\nexport type HttpMethod = (typeof HTTP_METHODS)[number];\n\nexport const DATA_SOURCE_NOT_FOUND_BEHAVIORS = [\"empty\", \"page-404\"] as const;\nexport type DataSourceNotFoundBehavior = (typeof DATA_SOURCE_NOT_FOUND_BEHAVIORS)[number];\n\n/** Methods that carry a request body. */\nexport function sendsBody(method: HttpMethod) {\n return method === \"POST\" || method === \"PUT\" || method === \"PATCH\";\n}\n\n/**\n * A JSON endpoint the page pulls content from. Sources live on the page so a\n * Repeat block can name one without carrying the URL on every element.\n */\nexport type DataSource = {\n id: string;\n name: string;\n url: string;\n /** Dotted path to the array inside the payload; \"\" when it is the root. */\n path: string;\n method?: HttpMethod;\n /** JSON body for POST/PUT/PATCH; tokens are resolved before sending. */\n body?: string;\n /** Appended to the URL as a query string. */\n params?: RequestPair[];\n /** Sent as request headers \u2014 for API keys and the like. */\n headers?: RequestPair[];\n /** Controls whether an upstream 404 empties this source or rejects the whole page. */\n onNotFound?: DataSourceNotFoundBehavior;\n};\n\n/**\n * Values a request token can read. Everything else resolves to an empty\n * string, so a typo cannot leak an unrelated value into the URL.\n */\nexport type RequestContext = {\n /** The visitor's query string, e.g. `{{query.id}}` on /post?id=5. */\n query: Record<string, string>;\n /** Values captured by a dynamic page path, e.g. `{{params.slug}}`. */\n params: Record<string, string>;\n /** The page being rendered, e.g. `{{page.slug}}`. */\n page: { slug: string };\n};\n\n/* ------------------------------------------------------------------ fields */\n\nexport const INPUT_TYPES = [\n \"text\",\n \"email\",\n \"password\",\n \"number\",\n \"tel\",\n \"url\",\n \"search\",\n \"date\",\n \"time\",\n \"datetime-local\",\n \"month\",\n \"week\",\n \"color\",\n \"range\",\n \"hidden\",\n] as const;\nexport type InputType = (typeof INPUT_TYPES)[number];\n\n/** One choice offered by a Select or a Radio group. */\nexport type FieldOption = { label: string; value: string };\n\n/** A raw HTML attribute the author put on an element. */\nexport type ElementAttribute = { name: string; value: string };\n\n/**\n * Tags an element may be rendered as instead of the one its type implies.\n *\n * Only non-void, inert tags are listed. A void tag (`img`, `br`, `input`)\n * cannot hold the children the canvas puts inside it, and a script-bearing one\n * would turn a semantic choice into a way to run code on the published page.\n */\nexport const CUSTOM_TAGS = [\n \"div\", \"span\", \"p\", \"section\", \"article\", \"aside\", \"header\", \"footer\", \"main\", \"nav\",\n \"figure\", \"figcaption\", \"blockquote\", \"address\",\n \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\",\n \"ul\", \"ol\", \"li\", \"dl\", \"dt\", \"dd\",\n \"details\", \"summary\", \"fieldset\", \"legend\", \"label\",\n \"strong\", \"em\", \"small\", \"mark\", \"code\", \"pre\", \"time\",\n \"table\", \"thead\", \"tbody\", \"tfoot\", \"tr\", \"th\", \"td\", \"caption\",\n] as const;\nexport type CustomTag = (typeof CUSTOM_TAGS)[number];\n\n/**\n * Attributes an author may never set by hand.\n *\n * An `on*` handler would run typed-in script in the visitor's page, and the\n * rest belong to the renderer: overwriting `class`, `style` or `id` would\n * detach the element from the stylesheet generated for it. The inspector\n * offers dedicated fields for those three instead.\n */\nexport const RESERVED_ATTRIBUTES: ReadonlySet<string> = new Set([\n \"class\",\n \"classname\",\n \"style\",\n \"id\",\n \"srcdoc\",\n \"is\",\n \"dangerouslysetinnerhtml\",\n]);\n\n/** Attributes whose value is a URL, so it has to pass the URL safety check. */\nexport const URL_ATTRIBUTES: ReadonlySet<string> = new Set([\n \"href\",\n \"src\",\n \"action\",\n \"formaction\",\n \"poster\",\n \"cite\",\n \"ping\",\n \"background\",\n]);\n\nexport type ResizeHandle = \"nw\" | \"ne\" | \"sw\" | \"se\" | \"n\" | \"s\" | \"w\" | \"e\";\n\n/* ------------------------------------------------------------- breakpoints */\n\nexport const BREAKPOINTS = [\"desktop\", \"tablet\", \"mobile\"] as const;\nexport type Breakpoint = string;\n\nexport type BreakpointDefinition = {\n id: string;\n name: string;\n width: number;\n};\n\nexport const DEFAULT_BREAKPOINTS: BreakpointDefinition[] = [\n { id: \"desktop\", name: \"Desktop\", width: 1280 },\n { id: \"tablet\", name: \"Tablet\", width: 768 },\n { id: \"mobile\", name: \"Mobile\", width: 375 },\n];\n\nexport const BREAKPOINT_WIDTHS: Record<string, number> = {\n desktop: 1280,\n tablet: 768,\n mobile: 375,\n};\n\n/**\n * Styles cascade from the widest breakpoint down, so a value set on desktop\n * holds everywhere until a narrower breakpoint overrides it.\n */\nexport const BREAKPOINT_CHAIN: Record<string, Breakpoint[]> = {\n desktop: [\"desktop\"],\n tablet: [\"desktop\", \"tablet\"],\n mobile: [\"desktop\", \"tablet\", \"mobile\"],\n};\n\n/* ------------------------------------------------------------------ styles */\n\n/** `fill` stretches to the parent, `auto` shrinks to the content. */\nexport type SizeMode = \"fixed\" | \"fill\" | \"auto\" | \"screen\";\nexport type Constraint = \"start\" | \"center\" | \"end\" | \"stretch\";\n/** `absolute` positions children by x/y; `stack` lays them out with flexbox. */\nexport type LayoutMode = \"absolute\" | \"stack\";\nexport type Direction = \"row\" | \"column\";\nexport type Justify = \"start\" | \"center\" | \"end\" | \"between\";\nexport type Align = \"start\" | \"center\" | \"end\" | \"stretch\";\n/** A child's own cross-axis alignment; `auto` defers to the parent's `align`. */\nexport type AlignSelf = \"auto\" | \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\";\n/** How wrapped lines are distributed along the cross axis. */\nexport type AlignContent = \"start\" | \"center\" | \"end\" | \"stretch\" | \"between\";\nexport type ListStyle = \"bullet\" | \"number\" | \"none\";\nexport type TextAlign = \"left\" | \"center\" | \"right\" | \"justify\";\nexport type TextTransform = \"none\" | \"uppercase\" | \"lowercase\" | \"capitalize\";\nexport type ObjectFit = \"cover\" | \"contain\" | \"fill\" | \"none\";\nexport type Overflow = \"visible\" | \"hidden\" | \"auto\" | \"scroll\";\nexport type Entrance = \"none\" | \"fade\" | \"up\" | \"down\" | \"left\" | \"right\" | \"zoom\";\nexport type MotionCurve = \"ease\" | \"spring\";\nexport type CursorStyle = \"auto\" | \"default\" | \"pointer\" | \"text\" | \"grab\" | \"zoom-in\" | \"none\";\n/**\n * How an element sits relative to its siblings.\n *\n * `absolute` is per element, not per container: it lifts this one out of the\n * flow and places it at x/y while everything around it keeps stacking. Making\n * the whole parent free instead would move every sibling to satisfy one of\n * them.\n */\nexport type PositionMode = \"static\" | \"sticky\" | \"fixed\" | \"absolute\";\n/** Which edge a pinned element holds to. */\nexport type PinSide = \"top\" | \"bottom\" | \"left\" | \"right\";\nexport type BgSize = \"cover\" | \"contain\" | \"auto\";\nexport type BlendMode =\n | \"normal\"\n | \"multiply\"\n | \"screen\"\n | \"overlay\"\n | \"darken\"\n | \"lighten\"\n | \"difference\"\n | \"luminosity\";\nexport type BorderStyle = \"solid\" | \"dashed\" | \"dotted\";\n\nexport type ElementStyle = {\n // Box \u2014 x/y only apply inside an `absolute` parent.\n x: number;\n y: number;\n constraintX: Constraint;\n constraintY: Constraint;\n w: number;\n h: number;\n widthMode: SizeMode;\n heightMode: SizeMode;\n\n // How this element arranges its own children.\n layout: LayoutMode;\n direction: Direction;\n gap: number;\n padT: number;\n padR: number;\n padB: number;\n padL: number;\n /**\n * Space held below the element, outside its own box.\n *\n * Separate from `padB` on purpose: padding is the breathing room the\n * author gave the content inside a section, while this is the distance to\n * whatever comes next. Sharing one value would make adjusting the rhythm\n * between sections quietly reflow their insides.\n */\n marginB: number;\n justify: Justify;\n align: Align;\n wrap: boolean;\n /**\n * Per-axis gaps. `gap` stays the value both axes fall back to, so an\n * author who only ever wanted one number never has to see these two; they\n * start at -1 meaning \"unset\" rather than 0, which is a real gap.\n */\n rowGap: number;\n columnGap: number;\n /** Distributes wrapped lines; only meaningful once `wrap` is on. */\n alignContent: AlignContent;\n /** Grid columns; only read when `layout` is `stack` on a Grid element. */\n columns: number;\n\n // How this element behaves as a child of its parent's layout.\n /** Overrides the parent's `align` for this one child. */\n alignSelf: AlignSelf;\n /**\n * Share of the leftover main-axis space this child takes.\n *\n * -1 means \"decide from the size mode\", which is what `fill` and `auto`\n * have always done. A number set here wins, so two siblings can split a row\n * 2:1 without either of them being a fixed width.\n */\n grow: number;\n /** Visual position among siblings, independent of document order. */\n order: number;\n /** Columns this child spans inside a Grid parent. */\n gridSpan: number;\n\n // Appearance\n bg: string;\n /**\n * Alpha applied to `bg` alone, as a percentage.\n *\n * Distinct from `opacity`, which fades the whole element \u2014 its text, its\n * border and the result of its own `backdropBlur` along with it. Glass is\n * a see-through *background* over a blurred backdrop, so it needs the fill\n * to be translucent while everything drawn on top stays solid.\n */\n bgOpacity: number;\n /** A full CSS gradient value, or \"\" for none. Painted over `bg`. */\n gradient: string;\n color: string;\n radius: number;\n opacity: number;\n borderW: number;\n /** Per-edge override; null inherits borderW. */\n borderT: number | null;\n borderR: number | null;\n borderB: number | null;\n borderL: number | null;\n borderC: string;\n borderStyle: BorderStyle;\n /** A full CSS box-shadow value, or \"\" for none. */\n shadow: string;\n rotate: number;\n\n // Typography\n fontFamily: string;\n fontSize: number;\n fontWeight: string;\n lineHeight: number;\n letterSpacing: number;\n textAlign: TextAlign;\n textTransform: TextTransform;\n\n // Composition \u2014 the pieces that make a layout feel designed rather than\n // stacked: clipping, sticky rails, imagery, glass and blend effects.\n overflow: Overflow;\n position: PositionMode;\n /** CSS stacking order, independent from the internal document order. */\n zIndex: number;\n /** Distance from `pinSide` while pinned; read when position is sticky or fixed. */\n stickyOffset: number;\n /** Edge a sticky or fixed element pins to. */\n pinSide: PinSide;\n /** Background image URL, or \"\" for none. Painted over `gradient`. */\n bgImage: string;\n bgSize: BgSize;\n bgPosition: string;\n /** Blurs the element's own content, in px. */\n blur: number;\n /** Blurs whatever sits behind the element, in px \u2014 the glass effect. */\n backdropBlur: number;\n blendMode: BlendMode;\n /** Percent; 100 leaves the element alone. */\n scale: number;\n /** A CSS ratio such as \"16/9\", or \"\" to leave height to the layout. */\n aspectRatio: string;\n\n /** Entrance effect, played once when the element scrolls into view. */\n entrance: Entrance;\n /** Milliseconds. */\n entranceDuration: number;\n entranceDelay: number;\n entranceCurve: MotionCurve;\n entranceBezier: string;\n springStiffness: number;\n springDamping: number;\n cursor: CursorStyle;\n\n /** Hidden at this breakpoint. */\n hidden: boolean;\n};\n\nexport const STYLE_KEYS = [\n \"x\",\n \"y\",\n \"constraintX\",\n \"constraintY\",\n \"w\",\n \"h\",\n \"widthMode\",\n \"heightMode\",\n \"layout\",\n \"direction\",\n \"gap\",\n \"padT\",\n \"padR\",\n \"padB\",\n \"padL\",\n \"marginB\",\n \"justify\",\n \"align\",\n \"wrap\",\n \"rowGap\",\n \"columnGap\",\n \"alignContent\",\n \"columns\",\n \"alignSelf\",\n \"grow\",\n \"order\",\n \"gridSpan\",\n \"bg\",\n \"bgOpacity\",\n \"gradient\",\n \"color\",\n \"radius\",\n \"opacity\",\n \"borderW\",\n \"borderT\",\n \"borderR\",\n \"borderB\",\n \"borderL\",\n \"borderC\",\n \"borderStyle\",\n \"shadow\",\n \"rotate\",\n \"fontFamily\",\n \"fontSize\",\n \"fontWeight\",\n \"lineHeight\",\n \"letterSpacing\",\n \"textAlign\",\n \"textTransform\",\n \"overflow\",\n \"position\",\n \"zIndex\",\n \"stickyOffset\",\n \"pinSide\",\n \"bgImage\",\n \"bgSize\",\n \"bgPosition\",\n \"blur\",\n \"backdropBlur\",\n \"blendMode\",\n \"scale\",\n \"aspectRatio\",\n \"entrance\",\n \"entranceDuration\",\n \"entranceDelay\",\n \"entranceCurve\",\n \"entranceBezier\",\n \"springStiffness\",\n \"springDamping\",\n \"cursor\",\n \"hidden\",\n] as const satisfies ReadonlyArray<keyof ElementStyle>;\n\nexport type StyleKey = (typeof STYLE_KEYS)[number];\n\nexport type CanvasElement = {\n id: string;\n type: ElementType;\n name?: string;\n parentId?: string;\n z: number;\n locked?: boolean;\n /** Page-local reusable component metadata. */\n componentRole?: \"master\" | \"instance\";\n componentId?: string;\n componentSourceId?: string;\n variant?: string;\n styleBindings?: Partial<Record<StyleKey, string>>;\n\n // Content is shared across breakpoints.\n content?: string;\n /** Sandboxed HTML/CSS used by code components. */\n code?: string;\n src?: string;\n alt?: string;\n objectFit?: ObjectFit;\n iconName?: PagieraIconName;\n /** Marker style for a List; also decides whether it publishes as ol or ul. */\n listStyle?: ListStyle;\n /**\n * An author-supplied SVG glyph, used instead of `iconName` when set.\n *\n * Stored as sanitised markup rather than a URL so the icon inherits\n * `currentColor` and scales with the element \u2014 an `<img>` would do\n * neither, and a remote URL would make every visitor fetch a third party.\n */\n svg?: string;\n placeholder?: string;\n fieldName?: string;\n inputType?: InputType;\n required?: boolean;\n disabled?: boolean;\n readOnly?: boolean;\n /** Pre-filled value for a field; the submitted value for Checkbox/Radio. */\n defaultValue?: string;\n /** Whether a Checkbox or Radio starts selected. */\n checked?: boolean;\n /** Choices offered by Select and Radio. */\n options?: FieldOption[];\n /** Select and FileInput may take more than one value. */\n multiple?: boolean;\n /** FileInput filter, e.g. \"image/*,.pdf\". */\n accept?: string;\n /** Native constraint validation; numeric and date fields read min/max/step. */\n minValue?: string;\n maxValue?: string;\n step?: string;\n pattern?: string;\n minLength?: number;\n maxLength?: number;\n autocomplete?: string;\n /** Id of the element a Label is bound to. */\n labelFor?: string;\n formAction?: string;\n formMethod?: HttpMethod;\n formSubmitMode?: \"request\" | \"native\";\n formContentType?: \"json\" | \"form-data\" | \"urlencoded\";\n /** Optional request body. `{{form.email}}` tokens read submitted fields. */\n formBody?: string;\n /** One `Header: value` pair per line. */\n formHeaders?: string;\n formSuccessMessage?: string;\n formErrorMessage?: string;\n formResetOnSuccess?: boolean;\n buttonType?: \"button\" | \"submit\" | \"reset\";\n href?: string;\n target?: \"_self\" | \"_blank\";\n\n /**\n * Renders this element as a different HTML tag than its type implies \u2014 the\n * escape hatch for semantics the type list does not model, such as a Stack\n * that should publish as a `<ul>`.\n */\n tag?: CustomTag;\n /** Extra class names, appended after the generated one. */\n customClass?: string;\n /** Inline CSS declarations, applied over every generated rule. */\n customStyle?: string;\n /** Raw HTML attributes; `RESERVED_ATTRIBUTES` lists what is refused. */\n attributes?: ElementAttribute[];\n interaction?: {\n trigger: \"click\";\n action: \"navigate\" | \"scroll-to\" | \"toggle-layer\" | \"show-layer\" | \"hide-layer\";\n value: string;\n target?: \"_self\" | \"_blank\";\n };\n\n /** Data source read by a Request/Repeat block or a directly-bound element. */\n sourceId?: string;\n /**\n * Inside Request/Repeat, pulls this field off the current object instead\n * of using the element's content. Dotted paths work: \"author.name\".\n */\n binding?: string;\n\n /** Desktop values; every breakpoint falls back to these. */\n base: ElementStyle;\n /** Narrower-breakpoint deltas, applied over `base` in cascade order. */\n overrides?: Record<string, Partial<ElementStyle>>;\n /** Applied on pointer hover, on top of the resolved breakpoint style. */\n hover?: Partial<ElementStyle>;\n /** Whether hover is activated by this node or its immediate parent. */\n hoverTrigger?: \"self\" | \"parent\";\n /** Applied while the pointer is pressed. */\n press?: Partial<ElementStyle>;\n loop?: { type: \"pulse\" | \"float\" | \"spin\"; duration: number };\n draggable?: boolean;\n};\n\n/* ---------------------------------------------------------------- defaults */\n\nexport const BASE_STYLE: ElementStyle = {\n x: 0,\n y: 0,\n constraintX: \"start\",\n constraintY: \"start\",\n w: 200,\n h: 100,\n widthMode: \"fixed\",\n heightMode: \"fixed\",\n\n layout: \"absolute\",\n direction: \"column\",\n gap: 0,\n padT: 0,\n padR: 0,\n padB: 0,\n padL: 0,\n marginB: 0,\n justify: \"start\",\n align: \"start\",\n wrap: false,\n rowGap: -1,\n columnGap: -1,\n alignContent: \"start\",\n columns: 3,\n\n alignSelf: \"auto\",\n grow: -1,\n order: 0,\n gridSpan: 1,\n\n bg: \"transparent\",\n bgOpacity: 100,\n gradient: \"\",\n color: \"#27272a\",\n radius: 0,\n opacity: 100,\n borderW: 0,\n borderT: null,\n borderR: null,\n borderB: null,\n borderL: null,\n borderC: \"transparent\",\n borderStyle: \"solid\",\n shadow: \"\",\n rotate: 0,\n\n fontFamily: \"inherit\",\n fontSize: 16,\n fontWeight: \"normal\",\n lineHeight: 1.5,\n letterSpacing: 0,\n textAlign: \"left\",\n textTransform: \"none\",\n\n overflow: \"visible\",\n position: \"static\",\n zIndex: 0,\n stickyOffset: 0,\n pinSide: \"top\",\n bgImage: \"\",\n bgSize: \"cover\",\n bgPosition: \"center\",\n blur: 0,\n backdropBlur: 0,\n blendMode: \"normal\",\n scale: 100,\n aspectRatio: \"\",\n\n entrance: \"none\",\n entranceDuration: 600,\n entranceDelay: 0,\n entranceCurve: \"ease\",\n entranceBezier: \"0.44, 0, 0.56, 1\",\n springStiffness: 300,\n springDamping: 30,\n cursor: \"auto\",\n\n hidden: false,\n};\n\nexport function makeStyle(overrides: Partial<ElementStyle>): ElementStyle {\n return { ...BASE_STYLE, ...overrides };\n}\n\n/** Every field a freshly dropped element starts with. */\nexport const ELEMENT_DEFAULTS: Record<\n ElementType,\n { style: Partial<ElementStyle>; props?: Partial<CanvasElement> }\n> = {\n Frame: {\n style: {\n w: 640,\n h: 420,\n widthMode: \"fixed\",\n heightMode: \"fixed\",\n layout: \"absolute\",\n direction: \"column\",\n overflow: \"hidden\",\n bg: \"#ffffff\",\n borderW: 1,\n borderC: \"#e4e4e7\",\n radius: 12,\n },\n },\n Stack: {\n style: {\n w: 600,\n h: 160,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 16,\n padT: 0,\n padR: 0,\n padB: 0,\n padL: 0,\n justify: \"start\",\n align: \"stretch\",\n bg: \"transparent\",\n },\n },\n Section: {\n style: {\n w: 1280,\n h: 480,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 24,\n padT: 64,\n padR: 48,\n padB: 64,\n padL: 48,\n justify: \"start\",\n align: \"stretch\",\n bg: \"#ffffff\",\n },\n },\n Container: {\n style: {\n w: 600,\n h: 240,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 16,\n padT: 24,\n padR: 24,\n padB: 24,\n padL: 24,\n align: \"stretch\",\n bg: \"transparent\",\n borderW: 1,\n borderC: \"#e4e4e7\",\n radius: 12,\n },\n },\n Grid: {\n style: {\n w: 900,\n h: 300,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"row\",\n columns: 3,\n gap: 24,\n align: \"stretch\",\n },\n },\n Heading: {\n style: {\n w: 480,\n h: 48,\n widthMode: \"fill\",\n heightMode: \"auto\",\n fontSize: 40,\n fontWeight: \"bold\",\n lineHeight: 1.2,\n letterSpacing: -0.5,\n color: \"#18181b\",\n },\n props: { content: \"Heading\" },\n },\n Text: {\n style: {\n w: 480,\n h: 24,\n widthMode: \"fill\",\n heightMode: \"auto\",\n fontSize: 16,\n lineHeight: 1.6,\n color: \"#52525b\",\n },\n props: { content: \"Text block content\" },\n },\n Image: {\n style: { w: 400, h: 260, widthMode: \"fill\", bg: \"#e4e4e7\", radius: 12 },\n props: { src: \"\", alt: \"\", objectFit: \"cover\" },\n },\n Button: {\n style: {\n w: 140,\n h: 44,\n widthMode: \"auto\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"row\",\n justify: \"center\",\n align: \"center\",\n padT: 12,\n padR: 22,\n padB: 12,\n padL: 22,\n bg: \"#2563eb\",\n color: \"#ffffff\",\n radius: 8,\n fontSize: 15,\n fontWeight: \"500\",\n textAlign: \"center\",\n },\n props: { content: \"Button\", href: \"\", target: \"_self\", buttonType: \"button\" },\n },\n Video: {\n style: { w: 640, h: 360, widthMode: \"fill\", bg: \"#18181b\", radius: 12 },\n props: { src: \"\" },\n },\n Icon: {\n style: { w: 24, h: 24, widthMode: \"fixed\", heightMode: \"fixed\", color: \"#5402E6\" },\n props: { iconName: \"star\" },\n },\n Form: {\n style: {\n w: 560,\n h: 240,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 14,\n align: \"stretch\",\n },\n props: {\n formAction: \"\",\n formMethod: \"POST\",\n formSubmitMode: \"request\",\n formContentType: \"json\",\n formSuccessMessage: \"Sent successfully.\",\n formErrorMessage: \"Something went wrong.\",\n },\n },\n Input: {\n style: {\n w: 320,\n h: 46,\n widthMode: \"fill\",\n heightMode: \"fixed\",\n padT: 0,\n padR: 14,\n padB: 0,\n padL: 14,\n bg: \"#ffffff\",\n color: \"#18181b\",\n borderW: 1,\n borderC: \"#d4d4d8\",\n radius: 10,\n fontSize: 15,\n },\n props: { placeholder: \"Enter a value\u2026\", fieldName: \"field\", inputType: \"text\" },\n },\n Textarea: {\n style: {\n w: 320,\n h: 120,\n widthMode: \"fill\",\n heightMode: \"fixed\",\n padT: 12,\n padR: 14,\n padB: 12,\n padL: 14,\n bg: \"#ffffff\",\n color: \"#18181b\",\n borderW: 1,\n borderC: \"#d4d4d8\",\n radius: 10,\n fontSize: 15,\n },\n props: { placeholder: \"Write your message\u2026\", fieldName: \"message\" },\n },\n Divider: {\n style: {\n w: 600,\n h: 1,\n widthMode: \"fill\",\n heightMode: \"fixed\",\n bg: \"#e4e4e7\",\n marginB: 0,\n },\n },\n Spacer: {\n // Height is the whole point of a spacer, so it starts fixed; setting\n // Grow in the inspector turns it into the flexible kind that pushes\n // siblings apart.\n style: {\n w: 600,\n h: 40,\n widthMode: \"fill\",\n heightMode: \"fixed\",\n bg: \"transparent\",\n },\n },\n List: {\n style: {\n w: 480,\n h: 120,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 8,\n align: \"stretch\",\n padL: 22,\n fontSize: 16,\n lineHeight: 1.6,\n color: \"#52525b\",\n },\n props: { listStyle: \"bullet\" },\n },\n ListItem: {\n style: {\n w: 480,\n h: 24,\n widthMode: \"fill\",\n heightMode: \"auto\",\n fontSize: 16,\n lineHeight: 1.6,\n color: \"#52525b\",\n },\n props: { content: \"List item\" },\n },\n Quote: {\n style: {\n w: 560,\n h: 96,\n widthMode: \"fill\",\n heightMode: \"auto\",\n padT: 4,\n padL: 20,\n borderL: 3,\n borderC: \"#d4d4d8\",\n fontSize: 20,\n lineHeight: 1.55,\n color: \"#3f3f46\",\n },\n props: { content: \"A quotation worth pulling out of the paragraph.\" },\n },\n Embed: {\n style: { w: 640, h: 400, widthMode: \"fill\", bg: \"#f4f4f5\", radius: 12 },\n props: { src: \"\" },\n },\n Fieldset: {\n style: {\n w: 560,\n h: 200,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 12,\n padT: 18,\n padR: 18,\n padB: 18,\n padL: 18,\n align: \"stretch\",\n borderW: 1,\n borderC: \"#e4e4e7\",\n radius: 12,\n },\n },\n Label: {\n style: {\n w: 200,\n h: 20,\n widthMode: \"fill\",\n heightMode: \"auto\",\n fontSize: 13,\n fontWeight: \"500\",\n lineHeight: 1.4,\n color: \"#3f3f46\",\n },\n props: { content: \"Label\", labelFor: \"\" },\n },\n Select: {\n style: {\n w: 320,\n h: 46,\n widthMode: \"fill\",\n heightMode: \"fixed\",\n padT: 0,\n padR: 14,\n padB: 0,\n padL: 14,\n bg: \"#ffffff\",\n color: \"#18181b\",\n borderW: 1,\n borderC: \"#d4d4d8\",\n radius: 10,\n fontSize: 15,\n },\n props: {\n fieldName: \"choice\",\n placeholder: \"Choose an option\u2026\",\n options: [\n { label: \"Option one\", value: \"one\" },\n { label: \"Option two\", value: \"two\" },\n ],\n },\n },\n Checkbox: {\n style: {\n w: 18,\n h: 18,\n widthMode: \"fixed\",\n heightMode: \"fixed\",\n color: \"#2563eb\",\n },\n props: { fieldName: \"accept\", defaultValue: \"yes\" },\n },\n Radio: {\n style: {\n w: 240,\n h: 96,\n widthMode: \"fill\",\n heightMode: \"auto\",\n color: \"#2563eb\",\n fontSize: 14,\n lineHeight: 1.6,\n },\n props: {\n fieldName: \"plan\",\n options: [\n { label: \"Monthly\", value: \"monthly\" },\n { label: \"Yearly\", value: \"yearly\" },\n ],\n },\n },\n FileInput: {\n style: {\n w: 320,\n h: 46,\n widthMode: \"fill\",\n heightMode: \"fixed\",\n padT: 0,\n padR: 14,\n padB: 0,\n padL: 14,\n bg: \"#ffffff\",\n color: \"#18181b\",\n borderW: 1,\n borderC: \"#d4d4d8\",\n radius: 10,\n fontSize: 15,\n },\n props: { fieldName: \"file\", accept: \"\" },\n },\n Request: {\n style: {\n w: 900,\n h: 200,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"column\",\n gap: 16,\n align: \"stretch\",\n },\n props: { sourceId: \"\" },\n },\n Repeat: {\n style: {\n w: 900,\n h: 200,\n widthMode: \"fill\",\n heightMode: \"auto\",\n layout: \"stack\",\n direction: \"row\",\n gap: 24,\n align: \"stretch\",\n wrap: true,\n },\n props: { sourceId: \"\" },\n },\n};\n\n/* ----------------------------------------------------------------- helpers */\n\nconst CONTAINER_TYPES: ReadonlySet<ElementType> = new Set([\n \"Frame\",\n \"Stack\",\n \"Section\",\n \"Container\",\n \"Grid\",\n \"Button\",\n \"Form\",\n \"Fieldset\",\n \"List\",\n \"ListItem\",\n \"Request\",\n \"Repeat\",\n]);\n\n/** Whether an element can hold children. */\nexport function isContainer(type: ElementType) {\n return CONTAINER_TYPES.has(type);\n}\n\nconst TEXTUAL_TYPES: ReadonlySet<ElementType> = new Set([\n \"Heading\",\n \"Text\",\n \"Button\",\n \"Label\",\n \"Quote\",\n \"ListItem\",\n]);\n\n/** Whether an element renders editable text. */\nexport function isTextual(type: ElementType) {\n return TEXTUAL_TYPES.has(type);\n}\n\nconst FIELD_TYPES: ReadonlySet<ElementType> = new Set([\n \"Input\",\n \"Textarea\",\n \"Select\",\n \"Checkbox\",\n \"Radio\",\n \"FileInput\",\n]);\n\n/** Whether an element submits a value with the form around it. */\nexport function isField(type: ElementType) {\n return FIELD_TYPES.has(type);\n}\n\n/** Whether an element's value is chosen from a list the author writes. */\nexport function hasOptions(type: ElementType) {\n return type === \"Select\" || type === \"Radio\";\n}\n\n/**\n * Elements that draw nothing of their own \u2014 they exist to take up room.\n *\n * They are worth naming because the inspector has nothing to say about their\n * content, and the canvas has to hint at them or they look like a bug.\n */\nconst STRUCTURAL_TYPES: ReadonlySet<ElementType> = new Set([\"Divider\", \"Spacer\"]);\n\nexport function isStructural(type: ElementType) {\n return STRUCTURAL_TYPES.has(type);\n}\n\n/** Page-level settings; the canvas behaves as the root container. */\nexport type RootStyle = {\n documentMode: \"page\" | \"component\";\n /** Content is centred inside this width unless `fullWidth` is set. */\n maxWidth: number;\n /** Editable design-surface height; the public page can still grow with content. */\n canvasHeight: number;\n /** Let content run edge to edge instead of being capped at `maxWidth`. */\n fullWidth: boolean;\n bg: string;\n layout: LayoutMode;\n direction: Direction;\n gap: number;\n padT: number;\n padR: number;\n padB: number;\n padL: number;\n align: Align;\n fontFamily: string;\n /** Cross-document animation used by links on the published site. */\n pageTransition: \"smooth\" | \"fade\" | \"slide\" | \"none\";\n /** Duration of the incoming published-page animation in milliseconds. */\n pageTransitionDuration: number;\n /** User-defined viewport previews, ordered from widest to narrowest. */\n breakpoints?: BreakpointDefinition[];\n /** The breakpoint whose values live in every element's `base` style. */\n baseBreakpointId?: string;\n variables?: DesignVariable[];\n customFonts?: CustomFont[];\n /**\n * Raw CSS appended after the generated sheet, so it can override any rule\n * the builder produced. Escape hatch for what the inspector cannot express.\n */\n customCss?: string;\n /**\n * Raw JavaScript run on the published page, after the document parses.\n *\n * It is deliberately never executed inside the editor: the canvas and the\n * template preview both render without scripting, so a page cannot reach\n * the editor it is being built in.\n */\n customJs?: string;\n};\n\nexport type CustomFont = { id: string; name: string; url: string; weight: number; style: \"normal\" | \"italic\" };\n\nexport type DesignVariable = {\n id: string;\n name: string;\n type: \"color\" | \"number\";\n value: string | number;\n};\n\nexport const DEFAULT_ROOT_STYLE: RootStyle = {\n documentMode: \"page\",\n maxWidth: 1280,\n canvasHeight: 800,\n fullWidth: false,\n bg: \"#ffffff\",\n layout: \"stack\",\n direction: \"column\",\n gap: 0,\n padT: 0,\n padR: 0,\n padB: 0,\n padL: 0,\n align: \"stretch\",\n fontFamily: \"inherit\",\n pageTransition: \"smooth\",\n pageTransitionDuration: 380,\n};\n\nexport const FONT_STACKS: Array<{ label: string; value: string }> = [\n { label: \"Inherit\", value: \"inherit\" },\n { label: \"Sans\", value: \"ui-sans-serif, system-ui, sans-serif\" },\n { label: \"Serif\", value: \"ui-serif, Georgia, serif\" },\n { label: \"Mono\", value: \"ui-monospace, SFMono-Regular, monospace\" },\n];\n\nexport const SHADOW_PRESETS: Array<{ label: string; value: string }> = [\n { label: \"None\", value: \"\" },\n { label: \"Small\", value: \"0 1px 2px rgba(0,0,0,0.06)\" },\n { label: \"Medium\", value: \"0 4px 12px rgba(0,0,0,0.08)\" },\n { label: \"Large\", value: \"0 12px 32px rgba(0,0,0,0.12)\" },\n { label: \"Glow\", value: \"0 0 0 4px rgba(37,99,235,0.15)\" },\n];\n\nexport const ASPECT_RATIOS: Array<{ label: string; value: string }> = [\n { label: \"Free\", value: \"\" },\n { label: \"Square 1:1\", value: \"1/1\" },\n { label: \"Photo 4:3\", value: \"4/3\" },\n { label: \"Wide 16:9\", value: \"16/9\" },\n { label: \"Ultra 21:9\", value: \"21/9\" },\n { label: \"Portrait 3:4\", value: \"3/4\" },\n];\n\nexport const ENTRANCES: Array<{ label: string; value: Entrance }> = [\n { label: \"None\", value: \"none\" },\n { label: \"Fade in\", value: \"fade\" },\n { label: \"Rise up\", value: \"up\" },\n { label: \"Drop down\", value: \"down\" },\n { label: \"Slide from left\", value: \"left\" },\n { label: \"Slide from right\", value: \"right\" },\n { label: \"Zoom in\", value: \"zoom\" },\n];\n\nexport const DRAG_MIME = \"application/pagiera-element-type\";\n/** Set when dragging an element that already exists on the canvas. */\nexport const MOVE_MIME = \"application/pagiera-element-id\";\nimport type { PagieraIconName } from \"../../../icon-names\";\n", "import {\n type DataSource,\n type HttpMethod,\n type RequestContext,\n type RequestPair,\n sendsBody,\n} from \"@/lib/editor/types\";\n\n/**\n * Fetching an author-supplied URL from our server is a server-side request\n * forgery vector: without checks it can reach cloud metadata endpoints, admin\n * services on localhost, or anything else inside the network perimeter. Every\n * hop is validated against this list, and redirects are followed by hand so a\n * public URL cannot bounce into private space.\n */\nconst BLOCKED_HOSTS = new Set([\n \"localhost\",\n \"127.0.0.1\",\n \"0.0.0.0\",\n \"::1\",\n \"[::1]\",\n \"metadata.google.internal\",\n \"metadata.goog\",\n]);\n\nconst PRIVATE_IPV4 =\n /^(10\\.|127\\.|0\\.|169\\.254\\.|192\\.168\\.|172\\.(1[6-9]|2\\d|3[01])\\.)/;\n\n/** Hostnames that only resolve inside a private network. */\nconst PRIVATE_SUFFIXES = [\".local\", \".internal\", \".localdomain\", \".home.arpa\"];\n\n/**\n * Private hosts the operator has deliberately opened.\n *\n * Blocking the whole private range is right by default \u2014 an author who can type\n * a URL should not be able to make the server read a cloud metadata endpoint \u2014\n * but it also blocks the ordinary case of a data source served by an API on the\n * same machine during development. Naming the hosts in\n * `PAGIERA_ALLOW_PRIVATE_HOSTS` opens exactly those and nothing else, so the\n * decision is explicit, visible in configuration, and does not silently differ\n * between a laptop and production.\n *\n * Read on every call rather than cached: a long-running dev server should pick\n * up an edited `.env.local` on restart of the request, not of the process.\n */\nfunction allowedPrivateHosts(): Set<string> {\n const raw = typeof process === \"undefined\" ? \"\" : (process.env?.PAGIERA_ALLOW_PRIVATE_HOSTS ?? \"\");\n return new Set(\n raw.split(\",\")\n .map((host) => host.trim().toLowerCase())\n // A port is not part of a hostname; allowing `localhost` allows it\n // on whatever port the author's service happens to use.\n .map((host) => host.replace(/:\\d+$/, \"\"))\n .filter(Boolean),\n );\n}\n\nexport const MAX_ROWS = 200;\nconst MAX_BYTES = 2_000_000;\nconst TIMEOUT_MS = 8000;\nconst MAX_REDIRECTS = 3;\n\nexport class DataSourceError extends Error {\n constructor(message: string, public readonly status?: number) {\n super(message);\n this.name = \"DataSourceError\";\n }\n}\n\nexport function assertFetchableUrl(raw: string): URL {\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n throw new DataSourceError(\"That is not a valid URL.\");\n }\n\n if (url.protocol !== \"https:\" && url.protocol !== \"http:\") {\n throw new DataSourceError(\"Only http and https URLs can be requested.\");\n }\n\n const host = url.hostname.toLowerCase();\n const allowed = allowedPrivateHosts();\n // An explicitly opened host skips the private-address rules entirely. The\n // protocol check above still applies, so this cannot open `file:` or\n // anything else that is not an ordinary web request.\n if (allowed.has(host)) return url;\n\n const refuse = (reason: string) => {\n throw new DataSourceError(\n `${reason} To use it, add it to PAGIERA_ALLOW_PRIVATE_HOSTS (for example PAGIERA_ALLOW_PRIVATE_HOSTS=localhost,127.0.0.1) and restart the server.`,\n );\n };\n\n if (BLOCKED_HOSTS.has(host) || PRIVATE_IPV4.test(host)) refuse(`Requests to ${host} are not allowed.`);\n if (PRIVATE_SUFFIXES.some((suffix) => host.endsWith(suffix))) refuse(`Requests to ${host} are not allowed.`);\n // Bracketed IPv6 literals: anything link-local or unique-local.\n if (host.startsWith(\"[\") && /^\\[(fe80|fc|fd)/i.test(host)) refuse(\"Requests to private addresses are not allowed.\");\n\n return url;\n}\n\n/**\n * Walks a dotted path such as `data.items` or `results.0.tags`. An empty path\n * returns the payload itself, which is what a bare JSON array needs.\n */\nexport function selectPath(payload: unknown, path: string): unknown {\n const steps = path.split(\".\").map((s) => s.trim()).filter(Boolean);\n let cursor: unknown = payload;\n for (const step of steps) {\n if (cursor === null || typeof cursor !== \"object\") return undefined;\n cursor = (cursor as Record<string, unknown>)[step];\n }\n return cursor;\n}\n\n/** Reads a body with a hard cap so a huge response cannot exhaust memory. */\nasync function readCapped(response: Response): Promise<string> {\n const reader = response.body?.getReader();\n if (!reader) return \"\";\n\n const chunks: Uint8Array[] = [];\n let total = 0;\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n total += value.byteLength;\n if (total > MAX_BYTES) {\n await reader.cancel();\n throw new DataSourceError(\"That response is too large (over 2 MB).\");\n }\n chunks.push(value);\n }\n return new TextDecoder().decode(\n chunks.reduce<Uint8Array>((acc, chunk) => {\n const merged = new Uint8Array(acc.length + chunk.length);\n merged.set(acc);\n merged.set(chunk, acc.length);\n return merged;\n }, new Uint8Array()),\n );\n}\n\nexport type SourceResult = { rows: Array<Record<string, unknown>>; keys: string[] };\n\n/**\n * Requests a source and normalizes it to rows. A root object becomes one row\n * for Request; an array stays multiple rows for Repeat.\n */\nexport type LoadOptions = {\n /**\n * Seconds to cache the response for. `false` bypasses the cache \u2014 only for\n * the editor's Test button; on a rendered route it would force an\n * otherwise-static page to become dynamic on every request.\n */\n revalidate?: number | false;\n /** Supplies `{{query.\u2026}}` / `{{page.\u2026}}` values for this request. */\n context?: RequestContext;\n};\n\nexport const DEFAULT_REVALIDATE = 60;\n\nexport async function loadSource(\n source: DataSource,\n { revalidate = DEFAULT_REVALIDATE, context = EMPTY_CONTEXT }: LoadOptions = {},\n): Promise<SourceResult> {\n const request = buildRequest(source, context);\n let target = request.url;\n\n let response: Response | undefined;\n for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {\n response = await fetch(target, {\n // Redirects are followed by hand so each hop is validated too.\n redirect: \"manual\",\n method: request.method,\n headers: request.headers,\n body: request.body,\n signal: AbortSignal.timeout(TIMEOUT_MS),\n // Only GET responses are cacheable; anything with side effects is\n // fetched fresh every time.\n ...(revalidate === false || request.method !== \"GET\"\n ? { cache: \"no-store\" as const }\n : { next: { revalidate } }),\n });\n\n if (response.status < 300 || response.status >= 400) break;\n\n const location = response.headers.get(\"location\");\n if (!location) break;\n target = assertFetchableUrl(new URL(location, target).toString());\n response = undefined;\n }\n\n if (!response) throw new DataSourceError(\"Too many redirects.\");\n if (!response.ok) {\n throw new DataSourceError(`The API replied ${response.status}.`, response.status);\n }\n\n const text = await readCapped(response);\n let payload: unknown;\n try {\n payload = JSON.parse(text);\n } catch {\n throw new DataSourceError(\"The response was not JSON.\");\n }\n\n const selected = selectPath(payload, source.path);\n const selectedRows = Array.isArray(selected)\n ? selected\n : selected !== null && typeof selected === \"object\"\n ? [selected]\n : undefined;\n if (!selectedRows) {\n throw new DataSourceError(\n source.path\n ? `\"${source.path}\" is not a list in that response.`\n : \"That response is neither an object nor a list.\",\n );\n }\n\n const rows = selectedRows\n .slice(0, MAX_ROWS)\n .map((row) =>\n row !== null && typeof row === \"object\" && !Array.isArray(row)\n ? (row as Record<string, unknown>)\n : { value: row },\n );\n\n // Union of the keys across the sample, so binding menus list everything.\n const keys = [...new Set(rows.flatMap((row) => Object.keys(row)))].sort();\n return { rows, keys };\n}\n\n/** Reads one binding path off a row and flattens it to something renderable. */\nexport function readBinding(row: Record<string, unknown>, path: string): string {\n const value = selectPath(row, path);\n if (value === null || value === undefined) return \"\";\n if (typeof value === \"object\") return \"\";\n return String(value);\n}\n\n/* ------------------------------------------------------------------ tokens */\n\n/**\n * Resolves `{{query.id}}`, `{{params.slug}}` and `{{page.slug}}`.\n *\n * Only these two namespaces exist: an unknown token becomes an empty string\n * rather than being left in the URL, so a typo cannot send a literal `{{\u2026}}`\n * to the API or accidentally expose something else.\n */\nexport function resolveTokens(value: string, context: RequestContext): string {\n return value.replace(/\\{\\{\\s*([a-zA-Z0-9_.]+)\\s*\\}\\}/g, (_, token: string) => {\n const [namespace, ...rest] = token.split(\".\");\n const key = rest.join(\".\");\n if (namespace === \"query\") return context.query[key] ?? \"\";\n if (namespace === \"params\") return context.params[key] ?? \"\";\n if (namespace === \"page\" && key === \"slug\") return context.page.slug;\n return \"\";\n });\n}\n\n/** Header names and values must not be able to inject extra headers. */\nfunction safeHeader(pair: RequestPair, context: RequestContext) {\n const name = pair.key.trim();\n if (!/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/.test(name)) return null;\n // Strip anything that could terminate the header line.\n const value = resolveTokens(pair.value, context).replace(/[\\r\\n\\0]/g, \"\").trim();\n return value ? ([name, value] as const) : null;\n}\n\n/**\n * Builds the final URL: tokens resolved, params appended and percent-encoded\n * by `URLSearchParams` so a value can never break out into the query grammar.\n */\nexport function buildRequest(source: DataSource, context: RequestContext) {\n const url = assertFetchableUrl(resolveTokens(source.url, context));\n\n for (const pair of source.params ?? []) {\n const key = pair.key.trim();\n if (!key) continue;\n const value = resolveTokens(pair.value, context);\n // An empty optional filter is dropped rather than sent as `key=`.\n if (value === \"\") continue;\n url.searchParams.set(key, value);\n }\n\n const method: HttpMethod = source.method ?? \"GET\";\n\n const headers: Record<string, string> = {\n accept: \"application/json,text/plain;q=0.9,*/*;q=0.8\",\n };\n\n let body: string | undefined;\n if (sendsBody(method) && source.body?.trim()) {\n body = resolveTokens(source.body, context);\n headers[\"content-type\"] = \"application/json\";\n }\n\n // Author headers last, so they can override the defaults above.\n for (const pair of source.headers ?? []) {\n const safe = safeHeader(pair, context);\n if (safe) headers[safe[0]] = safe[1];\n }\n\n // Re-check after tokens: a token could have rewritten the host.\n return { url: assertFetchableUrl(url.toString()), headers, method, body };\n}\n\nexport const EMPTY_CONTEXT: RequestContext = { query: {}, params: {}, page: { slug: \"\" } };\n", "import { DataSourceError, type LoadOptions, loadSource } from \"@/lib/data/source\";\nimport type { CanvasElement, DataSource } from \"@/lib/editor/types\";\nimport type { PageData } from \"./bind\";\n\n/** Signals that a source elected to turn its upstream 404 into a page 404. */\nexport class PageDataNotFoundError extends Error {\n constructor(public readonly sourceId: string) {\n super(`Data source ${sourceId} returned 404.`);\n this.name = \"PageDataNotFoundError\";\n }\n}\n\n/**\n * Fetches every source a page actually uses. Sources nothing references are\n * skipped, and a source that fails yields no rows rather than failing the\n * whole page \u2014 a broken API should not take the site down.\n */\nexport async function loadPageData(\n elements: CanvasElement[],\n sources: DataSource[],\n options?: LoadOptions,\n): Promise<PageData> {\n const used = new Set(\n elements\n .filter((el) => el.sourceId)\n .map((el) => el.sourceId as string),\n );\n const wanted = sources.filter((source) => used.has(source.id) && source.url);\n if (wanted.length === 0) return {};\n\n const results = await Promise.all(\n wanted.map(async (source) => {\n try {\n const { rows } = await loadSource(source, options);\n return [source.id, rows] as const;\n } catch (error) {\n if (\n error instanceof DataSourceError &&\n error.status === 404 &&\n source.onNotFound === \"page-404\"\n ) {\n throw new PageDataNotFoundError(source.id);\n }\n console.warn(\n `Data source \"${source.name}\" failed:`,\n error instanceof DataSourceError ? error.message : error,\n );\n return [source.id, []] as const;\n }\n }),\n );\n\n return Object.fromEntries(results);\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/full-editor.js
CHANGED
|
@@ -4980,7 +4980,7 @@ function stylesheetFor(elements, rootStyle) {
|
|
|
4980
4980
|
// element selector and leak the editor's typeface into the site.
|
|
4981
4981
|
// The shell spans the viewport; the content width is applied by each
|
|
4982
4982
|
// band's inner box, so section backgrounds reach both edges.
|
|
4983
|
-
`.pg-root{font-family:${resolveFont(rootStyle.fontFamily)}}.pg-node{box-sizing:border-box}.pg-node:is(input,textarea,button){font:inherit}.pg-node:is(input,textarea){outline:none}.pg-node:is(select){outline:none;appearance:none;background-image:url("data:image/svg+xml;utf8,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27 fill=%27none%27 stroke=%27%23888%27 stroke-width=%271.6%27><path d=%27M4 6.5 8 10.5 12 6.5%27/></svg>");background-repeat:no-repeat;background-position:right 12px center;padding-right:34px}.pg-node[role=radiogroup]{display:flex;flex-direction:column;gap:8px}.pg-choice{display:flex;align-items:center;gap:8px;cursor:pointer}.pg-choice input{accent-color:currentColor;margin:0}.pg-node:is(input[type=checkbox],input[type=radio]){accent-color:currentColor}.pg-node:is(textarea){resize:none}.pg-form-status:empty{display:none}.pg-form-status{font-size:12px;line-height:1.4}.pg-node[data-pg-state=success] .pg-form-status{color:#22c55e}.pg-node[data-pg-state=error] .pg-form-status{color:#ef4444}.pg-node img{display:block;width:100%;height:100%}.pg-node:is(ul,ol){list-style:none}.pg-list{counter-reset:pg-list}.pg-list>li{position:relative}.pg-list-bullet>li::before{content:'\\2022';position:absolute;left:-1em;opacity:.65}.pg-list-number>li::before{counter-increment:pg-list;content:counter(pg-list) '.';position:absolute;left:-1.6em;opacity:.65;font-variant-numeric:tabular-nums}.pg-node hr{border:0;width:100%;height:100%}.pg-node iframe{display:block;width:100%;height:100%;border:0}.pg-svg-glyph>svg{display:block;width:100%;height:100%}.pg-link{text-decoration:none;color:inherit}`
|
|
4983
|
+
`.pg-root{font-family:${resolveFont(rootStyle.fontFamily)}}.pg-node{box-sizing:border-box}.pg-node:is(input,textarea,button){font:inherit}.pg-node:where(button){appearance:none;background-color:transparent;border-width:0;color:inherit;text-align:inherit}.pg-node:is(input,textarea){outline:none}.pg-node:is(select){outline:none;appearance:none;background-image:url("data:image/svg+xml;utf8,<svg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 16 16%27 fill=%27none%27 stroke=%27%23888%27 stroke-width=%271.6%27><path d=%27M4 6.5 8 10.5 12 6.5%27/></svg>");background-repeat:no-repeat;background-position:right 12px center;padding-right:34px}.pg-node[role=radiogroup]{display:flex;flex-direction:column;gap:8px}.pg-choice{display:flex;align-items:center;gap:8px;cursor:pointer}.pg-choice input{accent-color:currentColor;margin:0}.pg-node:is(input[type=checkbox],input[type=radio]){accent-color:currentColor}.pg-node:is(textarea){resize:none}.pg-form-status:empty{display:none}.pg-form-status{font-size:12px;line-height:1.4}.pg-node[data-pg-state=success] .pg-form-status{color:#22c55e}.pg-node[data-pg-state=error] .pg-form-status{color:#ef4444}.pg-node img{display:block;width:100%;height:100%}.pg-node:is(ul,ol){list-style:none}.pg-list{counter-reset:pg-list}.pg-list>li{position:relative}.pg-list-bullet>li::before{content:'\\2022';position:absolute;left:-1em;opacity:.65}.pg-list-number>li::before{counter-increment:pg-list;content:counter(pg-list) '.';position:absolute;left:-1.6em;opacity:.65;font-variant-numeric:tabular-nums}.pg-node hr{border:0;width:100%;height:100%}.pg-node iframe{display:block;width:100%;height:100%;border:0}.pg-svg-glyph>svg{display:block;width:100%;height:100%}.pg-link{text-decoration:none;color:inherit}`
|
|
4984
4984
|
);
|
|
4985
4985
|
for (const element of elements) {
|
|
4986
4986
|
const emitted = rulesFor(element, byId, baseId, rootStyle, cascade).map(([suffix, decl]) => `.${classFor(element.id)}${suffix}{${decl}}`).join("");
|
|
@@ -9908,7 +9908,20 @@ function RenderedNode({
|
|
|
9908
9908
|
);
|
|
9909
9909
|
}
|
|
9910
9910
|
const tag = customTagOf(element) ?? semanticTag(element);
|
|
9911
|
+
const animates = style.entrance !== "none";
|
|
9911
9912
|
return React2.createElement(tag, withCustom(element, {
|
|
9913
|
+
/**
|
|
9914
|
+
* The entrance script runs while the document is still parsing, which
|
|
9915
|
+
* is before React hydrates, and it reveals whatever is already on
|
|
9916
|
+
* screen by adding `pg-in` to the element's class list. React then
|
|
9917
|
+
* finds a `class` it did not write and reports a hydration mismatch on
|
|
9918
|
+
* every visible animated node.
|
|
9919
|
+
*
|
|
9920
|
+
* The difference is intentional and client-only, so it is declared as
|
|
9921
|
+
* such. Only animated elements are excused: a mismatch anywhere else is
|
|
9922
|
+
* a real bug and must keep surfacing.
|
|
9923
|
+
*/
|
|
9924
|
+
suppressHydrationWarning: animates || void 0,
|
|
9912
9925
|
id: classFor(element.id),
|
|
9913
9926
|
htmlFor: tag === "label" && element.labelFor ? classFor(element.labelFor) : void 0,
|
|
9914
9927
|
type: tag === "button" ? element.buttonType ?? "button" : void 0,
|
|
@@ -12280,7 +12293,12 @@ function AiPanel({
|
|
|
12280
12293
|
id: event.id,
|
|
12281
12294
|
label: event.label,
|
|
12282
12295
|
status: event.status === "start" ? "running" : event.status,
|
|
12283
|
-
|
|
12296
|
+
// A second "start" for the same section is a retry,
|
|
12297
|
+
// and it carries the reason the last attempt was
|
|
12298
|
+
// discarded. Facts sent with a start are therefore
|
|
12299
|
+
// kept; only a start with nothing to say leaves the
|
|
12300
|
+
// section's existing facts alone.
|
|
12301
|
+
facts: event.facts ?? (event.status === "start" ? turn.sections[at]?.facts : void 0)
|
|
12284
12302
|
};
|
|
12285
12303
|
const sections = at >= 0 ? turn.sections.slice() : [...turn.sections, next];
|
|
12286
12304
|
if (at >= 0) sections[at] = next;
|
|
@@ -13800,7 +13818,7 @@ function Editor({
|
|
|
13800
13818
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null;
|
|
13801
13819
|
const selectedElement = selectedId ? byId.get(selectedId) : void 0;
|
|
13802
13820
|
const deviceWidth = frameWidthForBreakpoint;
|
|
13803
|
-
const frameWidth = rootStyle.fullWidth ?
|
|
13821
|
+
const frameWidth = rootStyle.layout === "absolute" && !rootStyle.fullWidth ? Math.min(deviceWidth, rootStyle.maxWidth) : deviceWidth;
|
|
13804
13822
|
useEffect12(() => setCanvasHeight(rootStyle.canvasHeight), [rootStyle.canvasHeight]);
|
|
13805
13823
|
useEffect12(() => {
|
|
13806
13824
|
if (!componentMode || !activeComponentMaster) return;
|