pagiera 0.2.0-alpha.43 → 0.2.0-alpha.44
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.map +1 -1
- package/dist/full-editor.js +42 -6
- package/dist/full-editor.js.map +2 -2
- package/dist/internal/app/p/editor/inspector.d.ts.map +1 -1
- package/dist/internal/app/p/editor/inspector.js +11 -2
- package/dist/internal/app/p/editor/inspector.js.map +1 -1
- package/dist/internal/lib/editor/style.d.ts.map +1 -1
- package/dist/internal/lib/editor/style.js +11 -3
- package/dist/internal/lib/editor/style.js.map +1 -1
- package/dist/internal/lib/editor/types.d.ts +6 -1
- package/dist/internal/lib/editor/types.d.ts.map +1 -1
- package/dist/internal/lib/editor/types.js +8 -0
- package/dist/internal/lib/editor/types.js.map +1 -1
- package/dist/internal/lib/editor/validate.d.ts.map +1 -1
- package/dist/internal/lib/editor/validate.js +11 -0
- package/dist/internal/lib/editor/validate.js.map +1 -1
- package/dist/internal/lib/pages.d.ts.map +1 -1
- package/dist/runtime.js +11 -3
- package/dist/runtime.js.map +2 -2
- package/dist/server.js +17 -0
- package/dist/server.js.map +2 -2
- package/package.json +1 -1
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 \"Form\",\n \"Input\",\n \"Textarea\",\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\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\";\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\";\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 /** Grid columns; only read when `layout` is `stack` on a Grid element. */\n columns: number;\n\n // Appearance\n bg: string;\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 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 \"columns\",\n \"bg\",\n \"gradient\",\n \"color\",\n \"radius\",\n \"opacity\",\n \"borderW\",\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 placeholder?: string;\n fieldName?: string;\n inputType?: \"text\" | \"email\" | \"password\" | \"number\" | \"tel\" | \"url\" | \"search\";\n required?: boolean;\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 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 /** 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 columns: 3,\n\n bg: \"transparent\",\n gradient: \"\",\n color: \"#27272a\",\n radius: 0,\n opacity: 100,\n borderW: 0,\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 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 \"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]);\n\n/** Whether an element renders editable text. */\nexport function isTextual(type: ElementType) {\n return TEXTUAL_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"],
|
|
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 \"Form\",\n \"Input\",\n \"Textarea\",\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\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\";\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\";\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 /** Grid columns; only read when `layout` is `stack` on a Grid element. */\n columns: number;\n\n // Appearance\n bg: string;\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 \"columns\",\n \"bg\",\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 placeholder?: string;\n fieldName?: string;\n inputType?: \"text\" | \"email\" | \"password\" | \"number\" | \"tel\" | \"url\" | \"search\";\n required?: boolean;\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 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 /** 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 columns: 3,\n\n bg: \"transparent\",\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 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 \"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]);\n\n/** Whether an element renders editable text. */\nexport function isTextual(type: ElementType) {\n return TEXTUAL_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
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/full-editor.js
CHANGED
|
@@ -3451,6 +3451,10 @@ var STYLE_KEYS = [
|
|
|
3451
3451
|
"radius",
|
|
3452
3452
|
"opacity",
|
|
3453
3453
|
"borderW",
|
|
3454
|
+
"borderT",
|
|
3455
|
+
"borderR",
|
|
3456
|
+
"borderB",
|
|
3457
|
+
"borderL",
|
|
3454
3458
|
"borderC",
|
|
3455
3459
|
"borderStyle",
|
|
3456
3460
|
"shadow",
|
|
@@ -3512,6 +3516,10 @@ var BASE_STYLE = {
|
|
|
3512
3516
|
radius: 0,
|
|
3513
3517
|
opacity: 100,
|
|
3514
3518
|
borderW: 0,
|
|
3519
|
+
borderT: null,
|
|
3520
|
+
borderR: null,
|
|
3521
|
+
borderB: null,
|
|
3522
|
+
borderL: null,
|
|
3515
3523
|
borderC: "transparent",
|
|
3516
3524
|
borderStyle: "solid",
|
|
3517
3525
|
shadow: "",
|
|
@@ -3964,6 +3972,11 @@ var ALIGN_MAP = {
|
|
|
3964
3972
|
stretch: "stretch"
|
|
3965
3973
|
};
|
|
3966
3974
|
function styleToCss(style, context, element) {
|
|
3975
|
+
const borderT = style.borderT ?? style.borderW;
|
|
3976
|
+
const borderR = style.borderR ?? style.borderW;
|
|
3977
|
+
const borderB = style.borderB ?? style.borderW;
|
|
3978
|
+
const borderL = style.borderL ?? style.borderW;
|
|
3979
|
+
const hasBorder = borderT > 0 || borderR > 0 || borderB > 0 || borderL > 0;
|
|
3967
3980
|
const css = {
|
|
3968
3981
|
boxSizing: "border-box",
|
|
3969
3982
|
display: "flex",
|
|
@@ -3994,9 +4007,12 @@ function styleToCss(style, context, element) {
|
|
|
3994
4007
|
color: style.color,
|
|
3995
4008
|
borderRadius: style.radius || void 0,
|
|
3996
4009
|
opacity: style.opacity === 100 ? void 0 : style.opacity / 100,
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4010
|
+
borderTopWidth: borderT || void 0,
|
|
4011
|
+
borderRightWidth: borderR || void 0,
|
|
4012
|
+
borderBottomWidth: borderB || void 0,
|
|
4013
|
+
borderLeftWidth: borderL || void 0,
|
|
4014
|
+
borderColor: hasBorder ? style.borderC : void 0,
|
|
4015
|
+
borderStyle: hasBorder ? style.borderStyle : void 0,
|
|
4000
4016
|
boxShadow: style.shadow || void 0,
|
|
4001
4017
|
transform: [
|
|
4002
4018
|
style.rotate ? `rotate(${style.rotate}deg)` : "",
|
|
@@ -5967,6 +5983,8 @@ function TypographyGroup({ ctx, ov }) {
|
|
|
5967
5983
|
function EffectsGroup({ ctx, ov }) {
|
|
5968
5984
|
const { style, onStyle, onCommitStart, onCommitEnd } = ctx;
|
|
5969
5985
|
const knownShadow = SHADOW_PRESETS.some((p) => p.value === style.shadow);
|
|
5986
|
+
const separateBorders = [style.borderT, style.borderR, style.borderB, style.borderL].some((value) => value !== null);
|
|
5987
|
+
const hasBorder = [style.borderT, style.borderR, style.borderB, style.borderL].map((value) => value ?? style.borderW).some((value) => value > 0);
|
|
5970
5988
|
return /* @__PURE__ */ jsxs3(Group, { title: "Effects", defaultOpen: false, children: [
|
|
5971
5989
|
ov(
|
|
5972
5990
|
["radius"],
|
|
@@ -6012,12 +6030,24 @@ function EffectsGroup({ ctx, ov }) {
|
|
|
6012
6030
|
)
|
|
6013
6031
|
),
|
|
6014
6032
|
/* @__PURE__ */ jsxs3(More, { label: "Border and rotation", children: [
|
|
6015
|
-
|
|
6033
|
+
/* @__PURE__ */ jsx4(
|
|
6034
|
+
Segmented,
|
|
6035
|
+
{
|
|
6036
|
+
label: "Border sides",
|
|
6037
|
+
value: separateBorders ? "separate" : "linked",
|
|
6038
|
+
options: [
|
|
6039
|
+
{ label: "Linked", value: "linked" },
|
|
6040
|
+
{ label: "Separate", value: "separate" }
|
|
6041
|
+
],
|
|
6042
|
+
onChange: (value) => value === "linked" ? onStyle({ borderT: null, borderR: null, borderB: null, borderL: null }) : onStyle({ borderT: style.borderW, borderR: style.borderW, borderB: style.borderW, borderL: style.borderW })
|
|
6043
|
+
}
|
|
6044
|
+
),
|
|
6045
|
+
!separateBorders && ov(
|
|
6016
6046
|
["borderW"],
|
|
6017
6047
|
/* @__PURE__ */ jsx4(
|
|
6018
6048
|
NumberInput,
|
|
6019
6049
|
{
|
|
6020
|
-
label: "
|
|
6050
|
+
label: "All sides",
|
|
6021
6051
|
suffix: "px",
|
|
6022
6052
|
min: 0,
|
|
6023
6053
|
value: style.borderW,
|
|
@@ -6025,6 +6055,12 @@ function EffectsGroup({ ctx, ov }) {
|
|
|
6025
6055
|
}
|
|
6026
6056
|
)
|
|
6027
6057
|
),
|
|
6058
|
+
separateBorders && /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-2 gap-2", children: [
|
|
6059
|
+
ov(["borderT"], /* @__PURE__ */ jsx4(NumberInput, { label: "Top", suffix: "px", min: 0, value: style.borderT ?? style.borderW, onChange: (borderT) => onStyle({ borderT }) })),
|
|
6060
|
+
ov(["borderR"], /* @__PURE__ */ jsx4(NumberInput, { label: "Right", suffix: "px", min: 0, value: style.borderR ?? style.borderW, onChange: (borderR) => onStyle({ borderR }) })),
|
|
6061
|
+
ov(["borderB"], /* @__PURE__ */ jsx4(NumberInput, { label: "Bottom", suffix: "px", min: 0, value: style.borderB ?? style.borderW, onChange: (borderB) => onStyle({ borderB }) })),
|
|
6062
|
+
ov(["borderL"], /* @__PURE__ */ jsx4(NumberInput, { label: "Left", suffix: "px", min: 0, value: style.borderL ?? style.borderW, onChange: (borderL) => onStyle({ borderL }) }))
|
|
6063
|
+
] }),
|
|
6028
6064
|
ov(
|
|
6029
6065
|
["borderStyle"],
|
|
6030
6066
|
/* @__PURE__ */ jsx4(
|
|
@@ -6042,7 +6078,7 @@ function EffectsGroup({ ctx, ov }) {
|
|
|
6042
6078
|
)
|
|
6043
6079
|
),
|
|
6044
6080
|
" ",
|
|
6045
|
-
|
|
6081
|
+
hasBorder && ov(
|
|
6046
6082
|
["borderC"],
|
|
6047
6083
|
/* @__PURE__ */ jsx4(
|
|
6048
6084
|
ColorInput,
|