pagiera 0.2.0-alpha.43 → 0.2.0-alpha.45
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 +117 -16
- package/dist/full-editor.js.map +2 -2
- package/dist/internal/app/api/ai-design/route.d.ts +5 -0
- package/dist/internal/app/api/ai-design/route.d.ts.map +1 -1
- package/dist/internal/app/api/ai-design/route.js +1 -1
- package/dist/internal/app/api/ai-design/route.js.map +1 -1
- package/dist/internal/app/p/editor/editor.d.ts.map +1 -1
- package/dist/internal/app/p/editor/editor.js +23 -7
- package/dist/internal/app/p/editor/editor.js.map +1 -1
- package/dist/internal/app/p/editor/fields.d.ts +1 -1
- package/dist/internal/app/p/editor/fields.d.ts.map +1 -1
- package/dist/internal/app/p/editor/fields.js +5 -1
- package/dist/internal/app/p/editor/fields.js.map +1 -1
- package/dist/internal/app/p/editor/inspector.d.ts.map +1 -1
- package/dist/internal/app/p/editor/inspector.js +20 -4
- 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 +19 -3
- package/dist/internal/lib/editor/style.js.map +1 -1
- package/dist/internal/lib/editor/types.d.ts +9 -2
- 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 +13 -1
- package/dist/internal/lib/editor/validate.js.map +1 -1
- package/dist/internal/lib/pages.d.ts +1 -0
- package/dist/internal/lib/pages.d.ts.map +1 -1
- package/dist/internal/lib/render/css.d.ts.map +1 -1
- package/dist/internal/lib/render/css.js +10 -2
- package/dist/internal/lib/render/css.js.map +1 -1
- package/dist/runtime.js +24 -5
- package/dist/runtime.js.map +2 -2
- package/dist/server.js +20 -2
- 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\" | \"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\";\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 /** 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 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)` : "",
|
|
@@ -4096,6 +4112,11 @@ function applySize(css, axis, mode, value, isMainAxis, parentAlign = "start") {
|
|
|
4096
4112
|
if (isMainAxis) css.flexShrink = 0;
|
|
4097
4113
|
return;
|
|
4098
4114
|
}
|
|
4115
|
+
if (mode === "screen") {
|
|
4116
|
+
css[axis] = axis === "height" ? "100vh" : "100vw";
|
|
4117
|
+
if (isMainAxis) css.flexShrink = 0;
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4099
4120
|
if (mode === "auto") {
|
|
4100
4121
|
css[axis] = "auto";
|
|
4101
4122
|
if (isMainAxis) css.flexGrow = 0;
|
|
@@ -4121,6 +4142,8 @@ function splitBand(css, contentWidth) {
|
|
|
4121
4142
|
flexWrap: css.flexWrap,
|
|
4122
4143
|
gridTemplateColumns: css.gridTemplateColumns,
|
|
4123
4144
|
width: "100%",
|
|
4145
|
+
height: css.height,
|
|
4146
|
+
minHeight: css.minHeight,
|
|
4124
4147
|
maxWidth: contentWidth,
|
|
4125
4148
|
marginLeft: "auto",
|
|
4126
4149
|
marginRight: "auto"
|
|
@@ -4555,6 +4578,10 @@ function interactionDeclarations(element, byId, breakpoint, rootStyle, cascade,
|
|
|
4555
4578
|
}
|
|
4556
4579
|
return declarationsToCss(delta);
|
|
4557
4580
|
}
|
|
4581
|
+
function hoverSelector(element, byId) {
|
|
4582
|
+
const parent = element.hoverTrigger === "parent" && element.parentId ? byId.get(element.parentId) : void 0;
|
|
4583
|
+
return parent ? `.${classFor(parent.id)}:hover .${classFor(element.id)}` : `.${classFor(element.id)}:hover`;
|
|
4584
|
+
}
|
|
4558
4585
|
var ENTRANCE_KEYFRAMES = `
|
|
4559
4586
|
@keyframes pg-fade{from{opacity:0}to{opacity:1}}
|
|
4560
4587
|
@keyframes pg-up{from{opacity:0;translate:0 28px}to{opacity:1;translate:none}}
|
|
@@ -4668,7 +4695,7 @@ function stylesheetFor(elements, rootStyle) {
|
|
|
4668
4695
|
}
|
|
4669
4696
|
for (const element of elements) {
|
|
4670
4697
|
if (element.hover || element.press) parts.push(`.${classFor(element.id)}{transition:transform .42s cubic-bezier(.16,1,.3,1),scale .42s cubic-bezier(.16,1,.3,1),rotate .42s cubic-bezier(.16,1,.3,1),translate .42s cubic-bezier(.16,1,.3,1),background-color .32s ease,color .32s ease,border-color .32s ease,box-shadow .42s cubic-bezier(.16,1,.3,1),opacity .32s ease,filter .42s ease;will-change:transform}`);
|
|
4671
|
-
if (element.hover && Object.keys(element.hover).length) parts.push(
|
|
4698
|
+
if (element.hover && Object.keys(element.hover).length) parts.push(`${hoverSelector(element, byId)}{${interactionDeclarations(element, byId, baseId, rootStyle, cascade, element.hover)}}`);
|
|
4672
4699
|
if (element.press && Object.keys(element.press).length) parts.push(`.${classFor(element.id)}:active{${interactionDeclarations(element, byId, baseId, rootStyle, cascade, element.press)}}`);
|
|
4673
4700
|
if (element.loop) {
|
|
4674
4701
|
const name = element.loop.type;
|
|
@@ -4682,7 +4709,7 @@ function stylesheetFor(elements, rootStyle) {
|
|
|
4682
4709
|
if (!element.overrides?.[plan.id] && !parent?.overrides?.[plan.id]) return [];
|
|
4683
4710
|
const selector = `.${classFor(element.id)}`;
|
|
4684
4711
|
return [
|
|
4685
|
-
element.hover && Object.keys(element.hover).length ? `${
|
|
4712
|
+
element.hover && Object.keys(element.hover).length ? `${hoverSelector(element, byId)}{${interactionDeclarations(element, byId, plan.id, rootStyle, cascade, element.hover)}}` : "",
|
|
4686
4713
|
element.press && Object.keys(element.press).length ? `${selector}:active{${interactionDeclarations(element, byId, plan.id, rootStyle, cascade, element.press)}}` : ""
|
|
4687
4714
|
].filter(Boolean);
|
|
4688
4715
|
}).join("");
|
|
@@ -5115,6 +5142,10 @@ var SIZE_OPTIONS = [
|
|
|
5115
5142
|
{ label: "Fill", value: "fill" },
|
|
5116
5143
|
{ label: "Hug", value: "auto" }
|
|
5117
5144
|
];
|
|
5145
|
+
var HEIGHT_SIZE_OPTIONS = [
|
|
5146
|
+
...SIZE_OPTIONS,
|
|
5147
|
+
{ label: "Screen", value: "screen" }
|
|
5148
|
+
];
|
|
5118
5149
|
function SizeField({
|
|
5119
5150
|
axis,
|
|
5120
5151
|
mode,
|
|
@@ -5148,13 +5179,13 @@ function SizeField({
|
|
|
5148
5179
|
/* @__PURE__ */ jsx3("span", { className: "shrink-0 text-ed-faint", children: "px" })
|
|
5149
5180
|
] }) : (
|
|
5150
5181
|
// The layout owns this axis, so there is no number to type.
|
|
5151
|
-
/* @__PURE__ */ jsx3("span", { className: "flex-1 text-right text-ed-faint", children: mode === "fill" ? "Fill" : "Hug" })
|
|
5182
|
+
/* @__PURE__ */ jsx3("span", { className: "flex-1 text-right text-ed-faint", children: mode === "fill" ? "Fill" : mode === "screen" ? "100vh" : "Hug" })
|
|
5152
5183
|
) }),
|
|
5153
5184
|
/* @__PURE__ */ jsx3(
|
|
5154
5185
|
SelectShell,
|
|
5155
5186
|
{
|
|
5156
5187
|
value: mode,
|
|
5157
|
-
options: SIZE_OPTIONS,
|
|
5188
|
+
options: axis === "H" ? HEIGHT_SIZE_OPTIONS : SIZE_OPTIONS,
|
|
5158
5189
|
onChange: onMode,
|
|
5159
5190
|
ariaLabel: `${name} sizing mode`,
|
|
5160
5191
|
className: "w-[76px] shrink-0"
|
|
@@ -5450,6 +5481,7 @@ function DesignTab(ctx) {
|
|
|
5450
5481
|
/* @__PURE__ */ jsx4(SizeGroup, { ctx, ov }),
|
|
5451
5482
|
isContainer(element.type) && /* @__PURE__ */ jsx4(LayoutGroup, { ctx, ov }),
|
|
5452
5483
|
textual && /* @__PURE__ */ jsx4(TypographyGroup, { ctx, ov }),
|
|
5484
|
+
element.type === "Icon" && /* @__PURE__ */ jsx4(IconAppearanceGroup, { ctx, ov }),
|
|
5453
5485
|
/* @__PURE__ */ jsx4(FillGroup, { ctx, ov, defaultOpen: !textual }),
|
|
5454
5486
|
/* @__PURE__ */ jsx4(SpacingGroup, { ctx, ov }),
|
|
5455
5487
|
/* @__PURE__ */ jsx4(EffectsGroup, { ctx, ov }),
|
|
@@ -5457,6 +5489,20 @@ function DesignTab(ctx) {
|
|
|
5457
5489
|
/* @__PURE__ */ jsx4(MotionGroup, { ctx, ov })
|
|
5458
5490
|
] });
|
|
5459
5491
|
}
|
|
5492
|
+
function IconAppearanceGroup({ ctx, ov }) {
|
|
5493
|
+
const { style, onStyle } = ctx;
|
|
5494
|
+
return /* @__PURE__ */ jsx4(Group, { title: "Icon", children: ov(
|
|
5495
|
+
["color"],
|
|
5496
|
+
/* @__PURE__ */ jsx4(
|
|
5497
|
+
ColorInput,
|
|
5498
|
+
{
|
|
5499
|
+
label: "Colour",
|
|
5500
|
+
value: style.color,
|
|
5501
|
+
onChange: (color) => onStyle({ color })
|
|
5502
|
+
}
|
|
5503
|
+
)
|
|
5504
|
+
) });
|
|
5505
|
+
}
|
|
5460
5506
|
function SizeGroup({ ctx, ov }) {
|
|
5461
5507
|
const { style, parentLayout, onStyle, onCommitStart, onCommitEnd } = ctx;
|
|
5462
5508
|
return /* @__PURE__ */ jsxs3(Group, { title: "Size", children: [
|
|
@@ -5967,6 +6013,8 @@ function TypographyGroup({ ctx, ov }) {
|
|
|
5967
6013
|
function EffectsGroup({ ctx, ov }) {
|
|
5968
6014
|
const { style, onStyle, onCommitStart, onCommitEnd } = ctx;
|
|
5969
6015
|
const knownShadow = SHADOW_PRESETS.some((p) => p.value === style.shadow);
|
|
6016
|
+
const separateBorders = [style.borderT, style.borderR, style.borderB, style.borderL].some((value) => value !== null);
|
|
6017
|
+
const hasBorder = [style.borderT, style.borderR, style.borderB, style.borderL].map((value) => value ?? style.borderW).some((value) => value > 0);
|
|
5970
6018
|
return /* @__PURE__ */ jsxs3(Group, { title: "Effects", defaultOpen: false, children: [
|
|
5971
6019
|
ov(
|
|
5972
6020
|
["radius"],
|
|
@@ -6012,12 +6060,24 @@ function EffectsGroup({ ctx, ov }) {
|
|
|
6012
6060
|
)
|
|
6013
6061
|
),
|
|
6014
6062
|
/* @__PURE__ */ jsxs3(More, { label: "Border and rotation", children: [
|
|
6015
|
-
|
|
6063
|
+
/* @__PURE__ */ jsx4(
|
|
6064
|
+
Segmented,
|
|
6065
|
+
{
|
|
6066
|
+
label: "Border sides",
|
|
6067
|
+
value: separateBorders ? "separate" : "linked",
|
|
6068
|
+
options: [
|
|
6069
|
+
{ label: "Linked", value: "linked" },
|
|
6070
|
+
{ label: "Separate", value: "separate" }
|
|
6071
|
+
],
|
|
6072
|
+
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 })
|
|
6073
|
+
}
|
|
6074
|
+
),
|
|
6075
|
+
!separateBorders && ov(
|
|
6016
6076
|
["borderW"],
|
|
6017
6077
|
/* @__PURE__ */ jsx4(
|
|
6018
6078
|
NumberInput,
|
|
6019
6079
|
{
|
|
6020
|
-
label: "
|
|
6080
|
+
label: "All sides",
|
|
6021
6081
|
suffix: "px",
|
|
6022
6082
|
min: 0,
|
|
6023
6083
|
value: style.borderW,
|
|
@@ -6025,6 +6085,12 @@ function EffectsGroup({ ctx, ov }) {
|
|
|
6025
6085
|
}
|
|
6026
6086
|
)
|
|
6027
6087
|
),
|
|
6088
|
+
separateBorders && /* @__PURE__ */ jsxs3("div", { className: "grid grid-cols-2 gap-2", children: [
|
|
6089
|
+
ov(["borderT"], /* @__PURE__ */ jsx4(NumberInput, { label: "Top", suffix: "px", min: 0, value: style.borderT ?? style.borderW, onChange: (borderT) => onStyle({ borderT }) })),
|
|
6090
|
+
ov(["borderR"], /* @__PURE__ */ jsx4(NumberInput, { label: "Right", suffix: "px", min: 0, value: style.borderR ?? style.borderW, onChange: (borderR) => onStyle({ borderR }) })),
|
|
6091
|
+
ov(["borderB"], /* @__PURE__ */ jsx4(NumberInput, { label: "Bottom", suffix: "px", min: 0, value: style.borderB ?? style.borderW, onChange: (borderB) => onStyle({ borderB }) })),
|
|
6092
|
+
ov(["borderL"], /* @__PURE__ */ jsx4(NumberInput, { label: "Left", suffix: "px", min: 0, value: style.borderL ?? style.borderW, onChange: (borderL) => onStyle({ borderL }) }))
|
|
6093
|
+
] }),
|
|
6028
6094
|
ov(
|
|
6029
6095
|
["borderStyle"],
|
|
6030
6096
|
/* @__PURE__ */ jsx4(
|
|
@@ -6042,7 +6108,7 @@ function EffectsGroup({ ctx, ov }) {
|
|
|
6042
6108
|
)
|
|
6043
6109
|
),
|
|
6044
6110
|
" ",
|
|
6045
|
-
|
|
6111
|
+
hasBorder && ov(
|
|
6046
6112
|
["borderC"],
|
|
6047
6113
|
/* @__PURE__ */ jsx4(
|
|
6048
6114
|
ColorInput,
|
|
@@ -6755,6 +6821,18 @@ function HoverTab({ element, style, onProps, onStyle }) {
|
|
|
6755
6821
|
}
|
|
6756
6822
|
) : null,
|
|
6757
6823
|
children: [
|
|
6824
|
+
element.parentId && /* @__PURE__ */ jsx4(
|
|
6825
|
+
Segmented,
|
|
6826
|
+
{
|
|
6827
|
+
label: "Trigger",
|
|
6828
|
+
value: element.hoverTrigger ?? "self",
|
|
6829
|
+
options: [
|
|
6830
|
+
{ label: "Self", value: "self" },
|
|
6831
|
+
{ label: "Parent", value: "parent" }
|
|
6832
|
+
],
|
|
6833
|
+
onChange: (hoverTrigger) => onProps({ hoverTrigger })
|
|
6834
|
+
}
|
|
6835
|
+
),
|
|
6758
6836
|
/* @__PURE__ */ jsx4(
|
|
6759
6837
|
ColorInput,
|
|
6760
6838
|
{
|
|
@@ -11226,6 +11304,7 @@ function Editor({
|
|
|
11226
11304
|
const [hoveredEffectIds, setHoveredEffectIds] = useState14(() => /* @__PURE__ */ new Set());
|
|
11227
11305
|
const [pressedEffectId, setPressedEffectId] = useState14(null);
|
|
11228
11306
|
const [effectsPreview, setEffectsPreview] = useState14(false);
|
|
11307
|
+
const [stickyPreview, setStickyPreview] = useState14(true);
|
|
11229
11308
|
const [previewVisibility, setPreviewVisibility] = useState14({});
|
|
11230
11309
|
const [marquee, setMarquee] = useState14(null);
|
|
11231
11310
|
const [codeComposerOpen, setCodeComposerOpen] = useState14(false);
|
|
@@ -12484,6 +12563,13 @@ function Editor({
|
|
|
12484
12563
|
const band = isBand(el.type, style, rootStyle);
|
|
12485
12564
|
const css = styleToCss(style, contextFor(el, frame.bp), el);
|
|
12486
12565
|
if (css.position === "fixed") css.position = "absolute";
|
|
12566
|
+
if (!stickyPreview && style.position === "sticky") {
|
|
12567
|
+
css.position = "relative";
|
|
12568
|
+
css.top = void 0;
|
|
12569
|
+
css.right = void 0;
|
|
12570
|
+
css.bottom = void 0;
|
|
12571
|
+
css.left = void 0;
|
|
12572
|
+
}
|
|
12487
12573
|
if (el.hover || el.press) css.transition = "transform .42s cubic-bezier(.16,1,.3,1), scale .42s cubic-bezier(.16,1,.3,1), rotate .42s cubic-bezier(.16,1,.3,1), background-color .32s ease, color .32s ease, border-color .32s ease, box-shadow .42s cubic-bezier(.16,1,.3,1), opacity .32s ease, filter .42s ease";
|
|
12488
12574
|
if (el.loop) css.animation = `pg-loop-${el.loop.type} ${el.loop.duration}ms ease-in-out infinite`;
|
|
12489
12575
|
css.cursor = el.locked ? "default" : isEditing ? "text" : "move";
|
|
@@ -12506,6 +12592,10 @@ function Editor({
|
|
|
12506
12592
|
el.type === "Request" ? canvasData[el.sourceId ?? ""]?.[0] : row,
|
|
12507
12593
|
keyPrefix
|
|
12508
12594
|
));
|
|
12595
|
+
const hoverEffectIds = [
|
|
12596
|
+
...el.hover && el.hoverTrigger !== "parent" ? [el.id] : [],
|
|
12597
|
+
...children.filter((child) => child.hover && child.hoverTrigger === "parent").map((child) => child.id)
|
|
12598
|
+
];
|
|
12509
12599
|
return (
|
|
12510
12600
|
// A canvas node is manipulated by pointer; the Layers panel is its keyboard equivalent.
|
|
12511
12601
|
// biome-ignore lint/a11y/noStaticElementInteractions: pointer-driven canvas node
|
|
@@ -12515,18 +12605,18 @@ function Editor({
|
|
|
12515
12605
|
"data-canvas-element": el.id,
|
|
12516
12606
|
style: split ? split.shell : css,
|
|
12517
12607
|
onMouseDown: (event) => handleElementMouseDown(event, interactionElement, frame.bp),
|
|
12518
|
-
onMouseEnter: () => effectsPreview &&
|
|
12519
|
-
if (current.has(
|
|
12608
|
+
onMouseEnter: () => effectsPreview && hoverEffectIds.length > 0 && setHoveredEffectIds((current) => {
|
|
12609
|
+
if (hoverEffectIds.every((id) => current.has(id))) return current;
|
|
12520
12610
|
const next = new Set(current);
|
|
12521
|
-
next.add(
|
|
12611
|
+
for (const id of hoverEffectIds) next.add(id);
|
|
12522
12612
|
return next;
|
|
12523
12613
|
}),
|
|
12524
12614
|
onMouseUp: () => effectsPreview && setPressedEffectId((id) => id === el.id ? null : id),
|
|
12525
12615
|
onMouseLeave: () => {
|
|
12526
12616
|
setHoveredEffectIds((current) => {
|
|
12527
|
-
if (!current.has(
|
|
12617
|
+
if (!hoverEffectIds.some((id) => current.has(id))) return current;
|
|
12528
12618
|
const next = new Set(current);
|
|
12529
|
-
next.delete(
|
|
12619
|
+
for (const id of hoverEffectIds) next.delete(id);
|
|
12530
12620
|
return next;
|
|
12531
12621
|
});
|
|
12532
12622
|
setPressedEffectId((id) => id === el.id ? null : id);
|
|
@@ -12590,7 +12680,7 @@ function Editor({
|
|
|
12590
12680
|
Math.round(style.widthMode === "fixed" ? style.w : 0) || "auto",
|
|
12591
12681
|
" \xD7",
|
|
12592
12682
|
" ",
|
|
12593
|
-
Math.round(style.heightMode === "fixed" ? style.h : 0) || "auto"
|
|
12683
|
+
style.heightMode === "screen" ? "100vh" : Math.round(style.heightMode === "fixed" ? style.h : 0) || "auto"
|
|
12594
12684
|
]
|
|
12595
12685
|
}
|
|
12596
12686
|
),
|
|
@@ -13410,6 +13500,17 @@ function Editor({
|
|
|
13410
13500
|
setPressedEffectId(null);
|
|
13411
13501
|
setPreviewVisibility({});
|
|
13412
13502
|
}, className: `rounded-md p-1.5 transition-colors ${effectsPreview ? "bg-ed-accent text-white" : "text-ed-faint hover:bg-ed-field hover:text-ed-text"}`, children: /* @__PURE__ */ jsx15(IconPlayerPlay5, { size: 14, stroke: 1.5 }) }),
|
|
13503
|
+
/* @__PURE__ */ jsx15(
|
|
13504
|
+
"button",
|
|
13505
|
+
{
|
|
13506
|
+
type: "button",
|
|
13507
|
+
"aria-pressed": stickyPreview,
|
|
13508
|
+
title: stickyPreview ? "Disable sticky positioning on the canvas" : "Preview sticky positioning on the canvas",
|
|
13509
|
+
onClick: () => setStickyPreview((value) => !value),
|
|
13510
|
+
className: `rounded-md p-1.5 transition-colors ${stickyPreview ? "bg-ed-accent text-white" : "text-ed-faint hover:bg-ed-field hover:text-ed-text"}`,
|
|
13511
|
+
children: stickyPreview ? /* @__PURE__ */ jsx15(IconPinFilled, { size: 14, stroke: 1.5 }) : /* @__PURE__ */ jsx15(IconPin, { size: 14, stroke: 1.5 })
|
|
13512
|
+
}
|
|
13513
|
+
),
|
|
13413
13514
|
/* @__PURE__ */ jsx15(
|
|
13414
13515
|
"button",
|
|
13415
13516
|
{
|