From ca85587cac3cab918428f4fc129cb2b3795da224 Mon Sep 17 00:00:00 2001 From: Invalid8 Date: Mon, 7 Sep 2026 19:28:13 +0100 Subject: [PATCH] Rebuild the playground as the better-content intro site Drop the "playground" framing. The page now introduces better-content and happens to be editable, rather than presenting itself as a demo annexe. Palette is derived from logo.svg instead of the inherited warm paper: white ground, #20242b ink, #71767f muted, and #ffe24a as the marker. Yellow is only ever a background with dark text on it, never a text colour. Header is a three column grid so the nav sits on the true centre regardless of what the actions weigh. Actions swap between reading and editing off html.editing-on, with the keyboard shortcuts wired. Edit affordances are scoped to hover and focus. Entering edit mode no longer tints, outlines or reflows anything. Structure: - Toolbar.astro becomes SiteHeader.astro, Base.astro gains the real footer - PreviewFrame splits into HeroIsland, RowsIsland and NoteIsland - Vue and Svelte render NoteCard directly, so all three bindings show the same row - index.astro becomes hero plus five numbered sections - the adapter switch moves onto the database panel, where its effect shows Performance: PGlite was constructed at module scope in db.ts, so 16 MB of Postgres wasm downloaded on every visit including the adapters that never touch it. Schema, DDL and client move to lib/pgliteAdapter.ts behind a dynamic import in loadAdapter(). Initial JS is now 68 KB gzipped. Loading states carry no copy. The hero and database rows render shimmering skeletons instead of hardcoded seed text, which was flashing stale content for anyone whose rows had been edited. The page table gains a tagline column, with ALTER TABLE in the DDL. --- design/index.pen | 6200 +++++++++++++++++ .../src/components/AdapterCalls.astro | 4 - .../src/components/DatabaseRows.astro | 58 +- .../src/components/SiteHeader.astro | 107 + .../playground/src/components/Toolbar.astro | 89 - .../src/components/react/HeroIsland.tsx | 63 + .../src/components/react/NoteCard.tsx | 32 +- .../src/components/react/NoteIsland.tsx | 20 + .../src/components/react/PreviewFrame.tsx | 175 - .../src/components/react/RowsIsland.tsx | 157 + .../src/components/react/useLiveEngine.ts | 16 + .../src/components/svelte/NoteCard.svelte | 39 +- ...PreviewIsland.svelte => NoteIsland.svelte} | 6 +- .../src/components/svelte/PreviewFrame.svelte | 161 - .../src/components/vue/NoteCard.vue | 39 +- .../vue/{PreviewIsland.vue => NoteIsland.vue} | 6 +- .../src/components/vue/PreviewFrame.vue | 157 - examples/playground/src/layouts/Base.astro | 107 +- examples/playground/src/lib/db.ts | 141 +- examples/playground/src/lib/editMode.ts | 2 +- examples/playground/src/lib/engine.ts | 4 +- examples/playground/src/lib/pgliteAdapter.ts | 79 + examples/playground/src/lib/seed.ts | 39 + examples/playground/src/pages/index.astro | 500 +- examples/playground/src/styles.css | 1607 ++--- 25 files changed, 7955 insertions(+), 1853 deletions(-) create mode 100644 design/index.pen create mode 100644 examples/playground/src/components/SiteHeader.astro delete mode 100644 examples/playground/src/components/Toolbar.astro create mode 100644 examples/playground/src/components/react/HeroIsland.tsx create mode 100644 examples/playground/src/components/react/NoteIsland.tsx delete mode 100644 examples/playground/src/components/react/PreviewFrame.tsx create mode 100644 examples/playground/src/components/react/RowsIsland.tsx create mode 100644 examples/playground/src/components/react/useLiveEngine.ts rename examples/playground/src/components/svelte/{PreviewIsland.svelte => NoteIsland.svelte} (50%) delete mode 100644 examples/playground/src/components/svelte/PreviewFrame.svelte rename examples/playground/src/components/vue/{PreviewIsland.vue => NoteIsland.vue} (74%) delete mode 100644 examples/playground/src/components/vue/PreviewFrame.vue create mode 100644 examples/playground/src/lib/pgliteAdapter.ts create mode 100644 examples/playground/src/lib/seed.ts diff --git a/design/index.pen b/design/index.pen new file mode 100644 index 0000000..960da8f --- /dev/null +++ b/design/index.pen @@ -0,0 +1,6200 @@ +{ + "version": "2.17", + "children": [ + { + "type": "frame", + "id": "zEA7F", + "x": 0, + "y": 0, + "name": "better-content — Reading mode", + "clip": true, + "width": 1440, + "fill": "$paper", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "iPpIc", + "name": "Header", + "width": "fill_container", + "fill": "$paper", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "padding": [ + 20, + 48 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "riHpY", + "name": "Brand", + "width": 184, + "gap": 10, + "alignItems": "center", + "children": [ + { + "id": "kLRTB", + "type": "ref", + "ref": "z7lkD9", + "name": "Logo" + }, + { + "type": "text", + "id": "z3nGe", + "name": "Wordmark", + "fill": "$ink", + "content": "better-content", + "fontFamily": "$mono", + "fontSize": 15, + "fontWeight": "500", + "letterSpacing": -0.2 + } + ] + }, + { + "type": "frame", + "id": "f2ctY4", + "name": "Nav", + "gap": 28, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "V6Sut", + "name": "Docs", + "fill": "$muted", + "content": "Docs", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Gzj2n", + "name": "Adapters", + "fill": "$muted", + "content": "Adapters", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "oJcK5", + "name": "GitHub", + "fill": "$muted", + "content": "GitHub", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "aw9eA", + "name": "Header actions", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "EP1HE", + "name": "Theme toggle", + "cornerRadius": 8, + "stroke": "$line", + "strokeWidth": 1, + "padding": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "cbesD", + "name": "Sun", + "width": 15, + "height": 15, + "icon": "sun", + "library": "lucide", + "fill": "$muted" + } + ] + }, + { + "type": "frame", + "id": "sPVz6", + "name": "Edit mode button", + "fill": "$ink", + "cornerRadius": 8, + "gap": 8, + "padding": [ + 9, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "aSSER", + "name": "Pen", + "width": 14, + "height": 14, + "icon": "pencil", + "library": "lucide", + "fill": "$paper" + }, + { + "type": "text", + "id": "zKJwH", + "name": "Label", + "fill": "$paper", + "content": "Edit mode", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "T8ER7", + "name": "Shortcut", + "fill": "#FFFFFF80", + "content": "⌘E", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "dNoxx", + "name": "Hero", + "width": "fill_container", + "fill": "$paper", + "layout": "vertical", + "padding": [ + 88, + 48, + 72, + 48 + ], + "children": [ + { + "type": "frame", + "id": "Fp0Ee", + "name": "Hero inner", + "width": 1040, + "layout": "vertical", + "gap": 28, + "children": [ + { + "type": "frame", + "id": "SWFgO", + "name": "Eyebrow chip", + "fill": "$accent", + "cornerRadius": 4, + "padding": [ + 3, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "nEGz4", + "name": "Eyebrow", + "fill": "$ink", + "content": "INLINE-EDIT CMS ENGINE", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal", + "letterSpacing": 1.2 + } + ] + }, + { + "type": "frame", + "id": "hRedk", + "name": "Headline", + "width": "fill_container", + "layout": "vertical", + "gap": 2, + "children": [ + { + "type": "text", + "id": "NCM4b", + "name": "Line 1", + "fill": "$ink", + "content": "Edit this page.", + "lineHeight": 1.08, + "fontFamily": "$serif", + "fontSize": 76, + "fontWeight": "normal", + "letterSpacing": -1.6 + }, + { + "type": "frame", + "id": "eBxEN", + "name": "Line 2", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YsvtM", + "name": "Watch your", + "fill": "$ink", + "content": "Watch your ", + "lineHeight": 1.08, + "fontFamily": "$serif", + "fontSize": 76, + "fontWeight": "normal", + "letterSpacing": -1.6 + }, + { + "type": "frame", + "id": "Xj7ZV", + "name": "Marker", + "fill": "$accent", + "cornerRadius": 6, + "padding": [ + 2, + 10 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "w7SAz", + "name": "database", + "fill": "$ink", + "content": "database", + "lineHeight": 1.08, + "fontFamily": "$serif", + "fontSize": 76, + "fontWeight": "normal", + "letterSpacing": -1.6 + } + ] + }, + { + "type": "text", + "id": "y4qten", + "name": "change", + "fill": "$ink", + "content": " change.", + "lineHeight": 1.08, + "fontFamily": "$serif", + "fontSize": 76, + "fontWeight": "normal", + "letterSpacing": -1.6 + } + ] + } + ] + }, + { + "type": "text", + "id": "qeuoe", + "name": "Deck", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 700, + "content": "better-content puts editing on the page itself, and writes every change to a database you own — through a seven-method adapter, from React, Vue or Svelte. The Postgres below is real, and it is running in this browser tab.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 20, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "aLX2x", + "name": "CTA row", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "IZKCM", + "name": "Primary CTA", + "fill": "$accent", + "cornerRadius": 10, + "gap": 10, + "padding": [ + 13, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "F0Rt9g", + "name": "Pen", + "width": 16, + "height": 16, + "icon": "pencil", + "library": "lucide", + "fill": "$ink" + }, + { + "type": "text", + "id": "nFPDa", + "name": "Label", + "fill": "$ink", + "content": "Turn on edit mode", + "fontFamily": "$sans", + "fontSize": 16, + "fontWeight": "500" + }, + { + "type": "text", + "id": "nAFRK", + "name": "Shortcut", + "fill": "#20242BA6", + "content": "⌘E", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "YI41H", + "name": "Copy command", + "fill": "$panel", + "cornerRadius": 10, + "stroke": "$line", + "strokeWidth": 1, + "gap": 12, + "padding": [ + 13, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "q6tLAb", + "name": "Prompt", + "fill": "$faint", + "content": "$", + "fontFamily": "$mono", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "eNIhZ", + "name": "Command", + "fill": "$ink", + "content": "npm create better-content", + "fontFamily": "$mono", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "BZgTl", + "name": "Copy", + "width": 15, + "height": 15, + "icon": "copy", + "library": "lucide", + "fill": "$faint" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "N4ia2", + "name": "Try it", + "width": "fill_container", + "fill": "$paper", + "stroke": "$line", + "strokeWidth": { + "top": 1 + }, + "layout": "vertical", + "gap": 36, + "padding": [ + 64, + 48, + 72, + 48 + ], + "children": [ + { + "type": "frame", + "id": "nAR4e", + "name": "Section head", + "width": 1040, + "layout": "vertical", + "gap": 14, + "children": [ + { + "type": "frame", + "id": "x8ZHbP", + "name": "Eyebrow chip", + "fill": "$accent", + "cornerRadius": 4, + "padding": [ + 3, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "IlRgy", + "name": "Label", + "fill": "$ink", + "content": "01 — TRY IT", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal", + "letterSpacing": 1.2 + } + ] + }, + { + "type": "text", + "id": "N00pI", + "name": "Title", + "fill": "$ink", + "content": "There is no demo mode. This is the app.", + "lineHeight": 1.2, + "fontFamily": "$serif", + "fontSize": 40, + "fontWeight": "normal", + "letterSpacing": -0.8 + }, + { + "type": "text", + "id": "ZGneR", + "name": "Sub", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 640, + "content": "Every keystroke you make on this page runs the same code path a production app runs: engine → transport → adapter → your database. There is no sandbox build.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "mu8lh", + "name": "Steps", + "width": 1040, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "sGpz8", + "name": "Step", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 28, + "padding": [ + 18, + 0 + ], + "children": [ + { + "type": "frame", + "id": "F8RR3X", + "name": "Gutter", + "width": 104, + "gap": 6, + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "Z6JFF", + "type": "ref", + "ref": "x7ZEh2", + "name": "Key ⌘", + "descendants": { + "lLGR9": { + "content": "⌘" + } + } + }, + { + "id": "SYdQP", + "type": "ref", + "ref": "x7ZEh2", + "name": "Key E", + "descendants": { + "lLGR9": { + "content": "E" + } + } + } + ] + }, + { + "type": "text", + "id": "Malmg", + "name": "Copy", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Turn on edit mode. Every field on this page becomes editable in place — headings, body copy, the cards below.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "WEHXx", + "name": "Step", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 28, + "padding": [ + 18, + 0 + ], + "children": [ + { + "type": "frame", + "id": "T2Qjg", + "name": "Gutter", + "width": 104, + "gap": 6, + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "TMtge", + "name": "Dot", + "fill": "$line-strong", + "content": "·", + "fontFamily": "$mono", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "MISuG", + "name": "Copy", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Click anything and type. No admin panel, no modal, no second URL to remember.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "neCfz", + "name": "Step", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 28, + "padding": [ + 18, + 0 + ], + "children": [ + { + "type": "frame", + "id": "t0QTFX", + "name": "Gutter", + "width": 104, + "gap": 6, + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "eCDNy", + "type": "ref", + "ref": "x7ZEh2", + "name": "Key ⌘", + "descendants": { + "lLGR9": { + "content": "⌘" + } + } + }, + { + "id": "EW4ig", + "type": "ref", + "ref": "x7ZEh2", + "name": "Key S", + "descendants": { + "lLGR9": { + "content": "S" + } + } + } + ] + }, + { + "type": "text", + "id": "ChzwD", + "name": "Copy", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Save every dirty field in one round trip. Volatile adapters skip this and save on blur.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MccfZ", + "name": "Step", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 28, + "padding": [ + 18, + 0 + ], + "children": [ + { + "type": "frame", + "id": "amrxB", + "name": "Gutter", + "width": 104, + "gap": 6, + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "id": "Ipglc", + "type": "ref", + "ref": "x7ZEh2", + "name": "Key ⌘", + "descendants": { + "lLGR9": { + "content": "⌘" + } + } + }, + { + "id": "aXELt", + "type": "ref", + "ref": "x7ZEh2", + "name": "Key Z", + "descendants": { + "lLGR9": { + "content": "Z" + } + } + } + ] + }, + { + "type": "text", + "id": "AMlcu", + "name": "Copy", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Undo. The engine holds the original snapshot until a save is acknowledged.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "XvQq4", + "name": "Step", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 28, + "padding": [ + 18, + 0 + ], + "children": [ + { + "type": "frame", + "id": "Ps7jO", + "name": "Gutter", + "width": 104, + "gap": 6, + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mc6iD", + "name": "Dot", + "fill": "$line-strong", + "content": "·", + "fontFamily": "$mono", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "O7QsKa", + "name": "Copy", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Open the drawer at the bottom. The row you just changed is sitting in Postgres.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "VShs5", + "name": "Step", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 28, + "padding": [ + 18, + 0 + ], + "children": [ + { + "type": "frame", + "id": "SgNkn", + "name": "Gutter", + "width": 104, + "gap": 6, + "justifyContent": "end", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zdYoc", + "name": "Dot", + "fill": "$line-strong", + "content": "·", + "fontFamily": "$mono", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "nUV3p", + "name": "Copy", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Reload. It is still there — PGlite persists to IndexedDB, in your tab, no server.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "z9nYJ", + "name": "Where it went", + "width": "fill_container", + "fill": "$paper", + "stroke": "$line", + "strokeWidth": { + "top": 1 + }, + "layout": "vertical", + "gap": 36, + "padding": [ + 64, + 48, + 72, + 48 + ], + "children": [ + { + "type": "frame", + "id": "PL800", + "name": "Section head", + "width": 1344, + "gap": 48, + "justifyContent": "space_between", + "alignItems": "end", + "children": [ + { + "type": "frame", + "id": "ErQw5", + "name": "Head copy", + "width": "fill_container", + "layout": "vertical", + "gap": 14, + "children": [ + { + "type": "frame", + "id": "VcMuG", + "name": "Eyebrow chip", + "fill": "$accent", + "cornerRadius": 4, + "padding": [ + 3, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qV2bt", + "name": "Label", + "fill": "$ink", + "content": "02 — WHERE IT WENT", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal", + "letterSpacing": 1.2 + } + ] + }, + { + "type": "text", + "id": "NF5nd", + "name": "Title", + "fill": "$ink", + "content": "Your edit is a row. Here it is.", + "lineHeight": 1.2, + "fontFamily": "$serif", + "fontSize": 40, + "fontWeight": "normal", + "letterSpacing": -0.8 + }, + { + "type": "text", + "id": "HjT3o", + "name": "Sub", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 640, + "content": "Content is rows, not commits. That is the difference between this and a git-backed CMS — it works for apps, not only static sites.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "sR886", + "name": "Reset", + "cornerRadius": 9, + "stroke": "$line-strong", + "strokeWidth": 1, + "gap": 8, + "padding": [ + 10, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "qFRzh", + "name": "Icon", + "width": 14, + "height": 14, + "icon": "rotate-ccw", + "library": "lucide", + "fill": "$muted" + }, + { + "type": "text", + "id": "gdO4t", + "name": "Label", + "fill": "$muted", + "content": "Reset data", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "jsF1X", + "name": "DB panel", + "clip": true, + "width": 1344, + "fill": "$code-bg", + "cornerRadius": 16, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "mkqq6", + "name": "Panel bar", + "width": "fill_container", + "stroke": "#FFFFFF14", + "strokeWidth": { + "bottom": 1 + }, + "padding": [ + 12, + 16 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "i8ztPQ", + "name": "Tabs", + "gap": 4, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "Y4duS", + "name": "Rows", + "fill": "$code-soft", + "cornerRadius": 7, + "padding": [ + 7, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "qOCrt", + "name": "Label", + "fill": "$code-ink", + "content": "Rows", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "B1MFp", + "name": "Adapter calls", + "fill": "#00000000", + "cornerRadius": 7, + "padding": [ + 7, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "C0CtLS", + "name": "Label", + "fill": "$code-muted", + "content": "Adapter calls", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "fqhau", + "name": "Schema", + "fill": "#00000000", + "cornerRadius": 7, + "padding": [ + 7, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "COnRv", + "name": "Label", + "fill": "$code-muted", + "content": "Schema", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "x74TE", + "name": "Adapter switch", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Vdstx", + "name": "Label", + "fill": "$code-muted", + "content": "adapter", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "I2l5Vb", + "name": "Segmented", + "fill": "#00000059", + "cornerRadius": 9, + "stroke": "#FFFFFF14", + "strokeWidth": 1, + "gap": 2, + "padding": 3, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "moXCp", + "name": "PGlite", + "fill": "$accent", + "cornerRadius": 7, + "padding": [ + 6, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "gCib7", + "name": "Label", + "fill": "$ink", + "content": "PGlite", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "ltRcW", + "name": "In-memory", + "fill": "#00000000", + "cornerRadius": 7, + "padding": [ + 6, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "yzXJ5", + "name": "Label", + "fill": "$code-muted", + "content": "In-memory", + "fontFamily": "$mono", + "fontSize": 12 + } + ] + }, + { + "type": "frame", + "id": "leJoh", + "name": "REST", + "fill": "#00000000", + "cornerRadius": 7, + "padding": [ + 6, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "UJOdj", + "name": "Label", + "fill": "$code-muted", + "content": "REST", + "fontFamily": "$mono", + "fontSize": 12 + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "eObSf", + "name": "Panel body", + "width": "fill_container", + "children": [ + { + "type": "frame", + "id": "q8Edf", + "name": "Rows table", + "width": "fill_container", + "fill": "$code-bg", + "layout": "vertical", + "padding": [ + 8, + 0, + 16, + 0 + ], + "children": [ + { + "type": "frame", + "id": "M9sxKA", + "name": "Header row", + "width": "fill_container", + "gap": 16, + "padding": [ + 10, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "uBflA", + "name": "id", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": 96, + "content": "id", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + }, + { + "type": "text", + "id": "xjS3A", + "name": "collection", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": 132, + "content": "collection", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + }, + { + "type": "text", + "id": "bYX78", + "name": "field", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": 176, + "content": "field", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + }, + { + "type": "text", + "id": "w9V2Wd", + "name": "value", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "value", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + }, + { + "type": "text", + "id": "AnQK1", + "name": "saved", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": 112, + "content": "saved", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + } + ] + }, + { + "type": "frame", + "id": "a6R2b", + "name": "Row page hero.title", + "width": "fill_container", + "fill": "#FFE24A26", + "gap": 16, + "padding": [ + 11, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "k3ZAd", + "name": "id", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": 96, + "content": "page", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "pbPxS", + "name": "collection", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": 132, + "content": "page", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "D6duNo", + "name": "field", + "fill": "$accent", + "textGrowth": "fixed-width", + "width": 176, + "content": "hero.title", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ARRym", + "name": "value", + "fill": "$code-ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Edit this page.", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ANmB1", + "name": "saved", + "width": 112, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "GPN6F", + "name": "Dot", + "fill": "$green-bright", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "M4ZYj", + "name": "When", + "fill": "$green-bright", + "content": "2s ago", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "K9pDXK", + "name": "Row page hero.deck", + "width": "fill_container", + "fill": "#00000000", + "gap": 16, + "padding": [ + 11, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "m5X9G", + "name": "id", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": 96, + "content": "page", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "jvpJC", + "name": "collection", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": 132, + "content": "page", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "RO8ED", + "name": "field", + "fill": "$code-ink", + "textGrowth": "fixed-width", + "width": 176, + "content": "hero.deck", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "eQu6K", + "name": "value", + "fill": "$code-ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "better-content is an inline-edit CMS…", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "l022nw", + "name": "saved", + "width": 112, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "G5lHH6", + "name": "Dot", + "fill": "#FFFFFF33", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "nswct", + "name": "When", + "fill": "$code-muted", + "content": "4m ago", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "U8sXxV", + "name": "Row c_01 body", + "width": "fill_container", + "fill": "#00000000", + "gap": 16, + "padding": [ + 11, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Juhfv", + "name": "id", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": 96, + "content": "c_01", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "s1J6sa", + "name": "collection", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": 132, + "content": "cards", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Y2vhV", + "name": "field", + "fill": "$code-ink", + "textGrowth": "fixed-width", + "width": 176, + "content": "body", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "KrYxa", + "name": "value", + "fill": "$code-ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "One engine, three bindings.", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "mXmgj", + "name": "saved", + "width": 112, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "U2r03s", + "name": "Dot", + "fill": "#FFFFFF33", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "od1Zo", + "name": "When", + "fill": "$code-muted", + "content": "4m ago", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "j3qD5", + "name": "Row c_02 body", + "width": "fill_container", + "fill": "#00000000", + "gap": 16, + "padding": [ + 11, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Rr3Cd", + "name": "id", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": 96, + "content": "c_02", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FCza1", + "name": "collection", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": 132, + "content": "cards", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Q7Xk0Y", + "name": "field", + "fill": "$code-ink", + "textGrowth": "fixed-width", + "width": 176, + "content": "body", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "r4g6iP", + "name": "value", + "fill": "$code-ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Bring your own Postgres.", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "qwrfp", + "name": "saved", + "width": 112, + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "AXkOZ", + "name": "Dot", + "fill": "#FFFFFF33", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "aICPS", + "name": "When", + "fill": "$code-muted", + "content": "4m ago", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "nXfMy", + "name": "Adapter calls", + "width": 460, + "fill": "$code-bg", + "stroke": "#FFFFFF14", + "strokeWidth": { + "left": 1 + }, + "layout": "vertical", + "padding": [ + 0, + 0, + 16, + 0 + ], + "children": [ + { + "type": "frame", + "id": "C3epK", + "name": "Log head", + "width": "fill_container", + "padding": [ + 14, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "BHgFR", + "name": "Title", + "fill": "$code-muted", + "content": "adapter calls", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + }, + { + "type": "frame", + "id": "tummL", + "name": "Live", + "gap": 6, + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "VK6C5", + "name": "Dot", + "fill": "$green-bright", + "width": 6, + "height": 6 + }, + { + "type": "text", + "id": "OL6sn", + "name": "Label", + "fill": "$green-bright", + "content": "live", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "V3Rfi", + "name": "Call upsert", + "width": "fill_container", + "fill": "#5FCB8E1F", + "gap": 12, + "padding": [ + 9, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "X3QeaP", + "name": "Time", + "fill": "#FFFFFF40", + "textGrowth": "fixed-width", + "width": 58, + "content": "14:02:11", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "tfSoG", + "name": "Method", + "fill": "$green-bright", + "textGrowth": "fixed-width", + "width": 120, + "content": "upsert", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "iFjaD", + "name": "Detail", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "\"page\", \"page\", { title }", + "fontFamily": "$mono", + "fontSize": 11.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "zGnSZ", + "name": "Ms", + "fill": "#FFFFFF40", + "content": "6ms", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "t4djwy", + "name": "Call subscribe", + "width": "fill_container", + "fill": "#00000000", + "gap": 12, + "padding": [ + 9, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ISxJF", + "name": "Time", + "fill": "#FFFFFF40", + "textGrowth": "fixed-width", + "width": 58, + "content": "14:02:11", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "z088F", + "name": "Method", + "fill": "$code-ink", + "textGrowth": "fixed-width", + "width": 120, + "content": "notify", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "z0yTx", + "name": "Detail", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "3 components notified", + "fontFamily": "$mono", + "fontSize": 11.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "m7l0ca", + "name": "Ms", + "fill": "#FFFFFF40", + "content": "—", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "VpvRv", + "name": "Call list", + "width": "fill_container", + "fill": "#00000000", + "gap": 12, + "padding": [ + 9, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "oAYRN", + "name": "Time", + "fill": "#FFFFFF40", + "textGrowth": "fixed-width", + "width": 58, + "content": "13:58:04", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "F9zuA", + "name": "Method", + "fill": "$code-ink", + "textGrowth": "fixed-width", + "width": 120, + "content": "fetchCollection", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "thcvg", + "name": "Detail", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "\"cards\" → 4 items", + "fontFamily": "$mono", + "fontSize": 11.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "abvfp", + "name": "Ms", + "fill": "#FFFFFF40", + "content": "2ms", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "g4L6H", + "name": "Call list", + "width": "fill_container", + "fill": "#00000000", + "gap": 12, + "padding": [ + 9, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "se9lI", + "name": "Time", + "fill": "#FFFFFF40", + "textGrowth": "fixed-width", + "width": 58, + "content": "13:58:04", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "w1MZM", + "name": "Method", + "fill": "$code-ink", + "textGrowth": "fixed-width", + "width": 120, + "content": "fetchCollection", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "zkn7M", + "name": "Detail", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "\"page\" → 1 item", + "fontFamily": "$mono", + "fontSize": 11.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "qHTz7", + "name": "Ms", + "fill": "#FFFFFF40", + "content": "3ms", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "N2sGF", + "name": "Call init", + "width": "fill_container", + "fill": "#00000000", + "gap": 12, + "padding": [ + 9, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Hd8tS", + "name": "Time", + "fill": "#FFFFFF40", + "textGrowth": "fixed-width", + "width": 58, + "content": "13:58:03", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Dhtfe", + "name": "Method", + "fill": "$code-ink", + "textGrowth": "fixed-width", + "width": 120, + "content": "connect", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "500" + }, + { + "type": "text", + "id": "TZ0zI", + "name": "Detail", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "PGlite · IndexedDB", + "fontFamily": "$mono", + "fontSize": 11.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Iriu3", + "name": "Ms", + "fill": "#FFFFFF40", + "content": "118ms", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Obyim", + "name": "Log foot", + "width": "fill_container", + "gap": 8, + "padding": [ + 14, + 20, + 4, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zKW6c", + "name": "Note", + "fill": "$code-muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "7 methods total. That is the whole DataAdapter contract.", + "lineHeight": 1.5, + "fontFamily": "$sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "uv4QQ", + "name": "Bindings", + "width": "fill_container", + "fill": "$paper", + "stroke": "$line", + "strokeWidth": { + "top": 1 + }, + "layout": "vertical", + "gap": 36, + "padding": [ + 64, + 48, + 72, + 48 + ], + "children": [ + { + "type": "frame", + "id": "ceVKS", + "name": "Section head", + "width": 1040, + "layout": "vertical", + "gap": 14, + "children": [ + { + "type": "frame", + "id": "nN9Nx", + "name": "Eyebrow chip", + "fill": "$accent", + "cornerRadius": 4, + "padding": [ + 3, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "I0hNY", + "name": "Label", + "fill": "$ink", + "content": "03 — BINDINGS", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal", + "letterSpacing": 1.2 + } + ] + }, + { + "type": "text", + "id": "C3M6o", + "name": "Title", + "fill": "$ink", + "content": "Edit in React. Watch Vue and Svelte.", + "lineHeight": 1.2, + "fontFamily": "$serif", + "fontSize": 40, + "fontWeight": "normal", + "letterSpacing": -0.8 + }, + { + "type": "text", + "id": "C3NSg", + "name": "Sub", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 680, + "content": "These are three real islands on one page, each a different framework, all subscribed to the same engine. Type into any of them — the other two re-render before the save even lands.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "R9lxd", + "name": "Islands", + "width": 1344, + "gap": 20, + "children": [ + { + "type": "frame", + "id": "H9158", + "name": "React island", + "clip": true, + "width": "fill_container", + "fill": "$panel", + "cornerRadius": 14, + "stroke": "$mark-line", + "strokeWidth": 2, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "AAssb", + "name": "Bar", + "width": "fill_container", + "fill": "$panel-soft", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 9, + "padding": [ + 11, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "R4UxVJ", + "name": "Dot", + "fill": "$react", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "dKJbW", + "name": "Name", + "fill": "$ink", + "content": "React", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "ivO6F", + "name": "Hook", + "fill": "$faint", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "useSyncExternalStore", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "pTxcE", + "name": "Editing badge", + "fill": "$accent", + "cornerRadius": 20, + "gap": 5, + "padding": [ + 3, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "r3XDeS", + "name": "Pen", + "width": 10, + "height": 10, + "icon": "pencil", + "library": "lucide", + "fill": "$ink" + }, + { + "type": "text", + "id": "mXE67", + "name": "Label", + "fill": "$ink", + "content": "editing", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "lMG4Q", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": 20, + "children": [ + { + "type": "text", + "id": "bUhC8", + "name": "Eyebrow", + "fill": "$faint", + "content": "CARDS / C_01", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + }, + { + "type": "frame", + "id": "j633B", + "name": "Title row", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "rU8g1", + "name": "Title", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "One engine, three bindings", + "lineHeight": 1.25, + "fontFamily": "$serif", + "fontSize": 23, + "fontWeight": "normal", + "letterSpacing": -0.3 + }, + { + "type": "rectangle", + "id": "DnH0j", + "name": "Caret", + "fill": "$ink", + "width": 2, + "height": 26 + } + ] + }, + { + "type": "text", + "id": "OYxmi", + "name": "Body", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "The core is getSnapshot() and subscribe(). Everything else is a thin adapter over it.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 15, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ejhuf", + "name": "Vue island", + "clip": true, + "width": "fill_container", + "fill": "$panel", + "cornerRadius": 14, + "stroke": "$line", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "ojyU9", + "name": "Bar", + "width": "fill_container", + "fill": "$panel-soft", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 9, + "padding": [ + 11, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "afrPV", + "name": "Dot", + "fill": "$vue", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "qw4lZ", + "name": "Name", + "fill": "$ink", + "content": "Vue", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "QKs9W", + "name": "Hook", + "fill": "$faint", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "useCmsSnapshot", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "rx6Xn", + "name": "Sync", + "fill": "$green", + "content": "synced", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "EVvV3", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": 20, + "children": [ + { + "type": "text", + "id": "sD6Bv", + "name": "Eyebrow", + "fill": "$faint", + "content": "CARDS / C_01", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + }, + { + "type": "frame", + "id": "ZDoIq", + "name": "Title row", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Q758N", + "name": "Title", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "One engine, three bindings", + "lineHeight": 1.25, + "fontFamily": "$serif", + "fontSize": 23, + "fontWeight": "normal", + "letterSpacing": -0.3 + } + ] + }, + { + "type": "text", + "id": "LjHRM", + "name": "Body", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "The core is getSnapshot() and subscribe(). Everything else is a thin adapter over it.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 15, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "TRrOg", + "name": "Svelte island", + "clip": true, + "width": "fill_container", + "fill": "$panel", + "cornerRadius": 14, + "stroke": "$line", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "uFObq", + "name": "Bar", + "width": "fill_container", + "fill": "$panel-soft", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 9, + "padding": [ + 11, + 16 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "WqOft", + "name": "Dot", + "fill": "$svelte", + "width": 9, + "height": 9 + }, + { + "type": "text", + "id": "GwVVA", + "name": "Name", + "fill": "$ink", + "content": "Svelte", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "lSyd3", + "name": "Hook", + "fill": "$faint", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "$engine store", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DIi1q", + "name": "Sync", + "fill": "$green", + "content": "synced", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "e5Pez", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 10, + "padding": 20, + "children": [ + { + "type": "text", + "id": "pZUUd", + "name": "Eyebrow", + "fill": "$faint", + "content": "CARDS / C_01", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + }, + { + "type": "frame", + "id": "y4FqE", + "name": "Title row", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "w0coLq", + "name": "Title", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "One engine, three bindings", + "lineHeight": 1.25, + "fontFamily": "$serif", + "fontSize": 23, + "fontWeight": "normal", + "letterSpacing": -0.3 + } + ] + }, + { + "type": "text", + "id": "H1QFg", + "name": "Body", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "The core is getSnapshot() and subscribe(). Everything else is a thin adapter over it.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 15, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "MbmwN", + "name": "Stems", + "width": 1344, + "gap": 20, + "children": [ + { + "type": "frame", + "id": "BS12b", + "name": "Stem React", + "width": "fill_container", + "justifyContent": "center", + "children": [ + { + "type": "rectangle", + "id": "Cr0rZ", + "name": "Line", + "fill": "$line-strong", + "width": 1, + "height": 28 + } + ] + }, + { + "type": "frame", + "id": "q2SPM", + "name": "Stem Vue", + "width": "fill_container", + "justifyContent": "center", + "children": [ + { + "type": "rectangle", + "id": "CZ2yC", + "name": "Line", + "fill": "$line-strong", + "width": 1, + "height": 28 + } + ] + }, + { + "type": "frame", + "id": "JSKmd", + "name": "Stem Svelte", + "width": "fill_container", + "justifyContent": "center", + "children": [ + { + "type": "rectangle", + "id": "i7qpL", + "name": "Line", + "fill": "$line-strong", + "width": 1, + "height": 28 + } + ] + } + ] + }, + { + "type": "frame", + "id": "NKHJt", + "name": "Engine bar", + "width": 1344, + "fill": "$panel-soft", + "cornerRadius": 12, + "stroke": "$line-strong", + "strokeWidth": 1, + "gap": 14, + "padding": [ + 16, + 20 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "GCTXT", + "name": "Icon", + "width": 16, + "height": 16, + "icon": "box", + "library": "lucide", + "fill": "$muted" + }, + { + "type": "text", + "id": "IHZn4", + "name": "Code", + "fill": "$ink", + "content": "const engine = createCmsEngine({ transport: adapterTransport(adapter) })", + "fontFamily": "$mono", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "VxtUX", + "name": "Note", + "fill": "$faint", + "content": "— framework-free, zero framework imports", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Rrofw", + "name": "Adapters", + "width": "fill_container", + "fill": "$paper", + "stroke": "$line", + "strokeWidth": { + "top": 1 + }, + "layout": "vertical", + "gap": 36, + "padding": [ + 64, + 48, + 72, + 48 + ], + "children": [ + { + "type": "frame", + "id": "pZmLx", + "name": "Section head", + "width": 1040, + "layout": "vertical", + "gap": 14, + "children": [ + { + "type": "frame", + "id": "AcMtV", + "name": "Eyebrow chip", + "fill": "$accent", + "cornerRadius": 4, + "padding": [ + 3, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ox3fy", + "name": "Label", + "fill": "$ink", + "content": "04 — ADAPTERS", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal", + "letterSpacing": 1.2 + } + ] + }, + { + "type": "text", + "id": "NMrmL", + "name": "Title", + "fill": "$ink", + "content": "Seven methods stand between us and your database.", + "lineHeight": 1.2, + "fontFamily": "$serif", + "fontSize": 40, + "fontWeight": "normal", + "letterSpacing": -0.8 + }, + { + "type": "text", + "id": "HgPCu", + "name": "Sub", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 680, + "content": "There is no hosted service to sign up for and no proprietary backend to migrate off. Implement these seven and better-content talks to whatever you already run.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "HApUw", + "name": "Split", + "width": 1344, + "gap": 28, + "children": [ + { + "type": "frame", + "id": "Nmgey", + "name": "Contract", + "clip": true, + "width": 496, + "fill": "$panel", + "cornerRadius": 14, + "stroke": "$line", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "dPott", + "name": "Contract head", + "width": "fill_container", + "fill": "$panel-soft", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "padding": [ + 13, + 18 + ], + "children": [ + { + "type": "text", + "id": "rftLf", + "name": "Title", + "fill": "$ink", + "content": "interface DataAdapter", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "E4aTd", + "name": "fetchCollection", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 10, + "padding": [ + 12, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "NT2KT", + "name": "Name", + "fill": "$ink", + "content": "fetchCollection", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "X8M2fp", + "name": "Sig", + "fill": "$faint", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "(collection, q?)", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "YiyBf", + "name": "Kind", + "fill": "$green-soft", + "cornerRadius": 20, + "padding": [ + 3, + 8 + ], + "children": [ + { + "type": "text", + "id": "f2ZlW", + "name": "Label", + "fill": "$green", + "content": "read", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "n9eRrL", + "name": "fetchById", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 10, + "padding": [ + 12, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LDUMd", + "name": "Name", + "fill": "$ink", + "content": "fetchById", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "i6ofS", + "name": "Sig", + "fill": "$faint", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "(collection, id)", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "omBm0", + "name": "Kind", + "fill": "$green-soft", + "cornerRadius": 20, + "padding": [ + 3, + 8 + ], + "children": [ + { + "type": "text", + "id": "QEFx1", + "name": "Label", + "fill": "$green", + "content": "read", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "SeHeo", + "name": "create", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 10, + "padding": [ + 12, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "iaqz5", + "name": "Name", + "fill": "$ink", + "content": "create", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "Mjk6m", + "name": "Sig", + "fill": "$faint", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "(collection, data)", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Al1K0", + "name": "Kind", + "fill": "$accent-soft", + "cornerRadius": 20, + "padding": [ + 3, + 8 + ], + "children": [ + { + "type": "text", + "id": "K8YOE", + "name": "Label", + "fill": "$ink", + "content": "write", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "xMIV2", + "name": "createWithId", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 10, + "padding": [ + 12, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "b58mz", + "name": "Name", + "fill": "$ink", + "content": "createWithId", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "X8AWmn", + "name": "Sig", + "fill": "$faint", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "(collection, id, data)", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "Kqxgf", + "name": "Kind", + "fill": "$accent-soft", + "cornerRadius": 20, + "padding": [ + 3, + 8 + ], + "children": [ + { + "type": "text", + "id": "loi35", + "name": "Label", + "fill": "$ink", + "content": "write", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ZJr0C", + "name": "update", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 10, + "padding": [ + 12, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "zAOfB", + "name": "Name", + "fill": "$ink", + "content": "update", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "wm4ES", + "name": "Sig", + "fill": "$faint", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "(collection, id, patch)", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "K5riX7", + "name": "Kind", + "fill": "$accent-soft", + "cornerRadius": 20, + "padding": [ + 3, + 8 + ], + "children": [ + { + "type": "text", + "id": "C4wMdb", + "name": "Label", + "fill": "$ink", + "content": "write", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "hcliT", + "name": "upsert", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 10, + "padding": [ + 12, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "wVTmG", + "name": "Name", + "fill": "$ink", + "content": "upsert", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "Miq2M", + "name": "Sig", + "fill": "$faint", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "(collection, id, patch)", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "ozBEC", + "name": "Kind", + "fill": "$accent-soft", + "cornerRadius": 20, + "padding": [ + 3, + 8 + ], + "children": [ + { + "type": "text", + "id": "NXm56", + "name": "Label", + "fill": "$ink", + "content": "write", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "TtlUS", + "name": "delete", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 10, + "padding": [ + 12, + 18 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "ZcxYA", + "name": "Name", + "fill": "$ink", + "content": "delete", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "E8jzIA", + "name": "Sig", + "fill": "$faint", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "(collection, id)", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "TKc4X", + "name": "Kind", + "fill": "$accent-soft", + "cornerRadius": 20, + "padding": [ + 3, + 8 + ], + "children": [ + { + "type": "text", + "id": "BBNpW", + "name": "Label", + "fill": "$ink", + "content": "write", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "h1RvN", + "name": "Contract foot", + "width": "fill_container", + "padding": [ + 14, + 18 + ], + "children": [ + { + "type": "text", + "id": "Cz6OV", + "name": "Note", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "An afternoon of work for a database you already know.", + "lineHeight": 1.5, + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "QkSxV", + "name": "Code panel", + "clip": true, + "width": "fill_container", + "fill": "$code-bg", + "cornerRadius": 14, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "m6ENJk", + "name": "File tabs", + "width": "fill_container", + "stroke": "#FFFFFF14", + "strokeWidth": { + "bottom": 1 + }, + "gap": 4, + "padding": [ + 10, + 12 + ], + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "HbCj5", + "name": "postgres.ts", + "fill": "$code-soft", + "cornerRadius": 7, + "padding": [ + 6, + 11 + ], + "children": [ + { + "type": "text", + "id": "IpCWw", + "name": "Label", + "fill": "$code-ink", + "content": "postgres.ts", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MahIY", + "name": "firestore.ts", + "fill": "#00000000", + "cornerRadius": 7, + "padding": [ + 6, + 11 + ], + "children": [ + { + "type": "text", + "id": "MeMdC", + "name": "Label", + "fill": "$code-muted", + "content": "firestore.ts", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "sPJ13", + "name": "memory.ts", + "fill": "#00000000", + "cornerRadius": 7, + "padding": [ + 6, + 11 + ], + "children": [ + { + "type": "text", + "id": "uXkyc", + "name": "Label", + "fill": "$code-muted", + "content": "memory.ts", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "OQwOD", + "name": "rest.ts", + "fill": "#00000000", + "cornerRadius": 7, + "padding": [ + 6, + 11 + ], + "children": [ + { + "type": "text", + "id": "rqSZS", + "name": "Label", + "fill": "$code-muted", + "content": "rest.ts", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "HvyNR", + "name": "Code", + "width": "fill_container", + "layout": "vertical", + "padding": [ + 18, + 20, + 22, + 20 + ], + "children": [ + { + "type": "frame", + "id": "h8A6h", + "name": "line", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "M0Jtl4", + "name": "t", + "fill": "#E0A56A", + "content": "import", + "lineHeight": 1.75, + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "tEzu2", + "name": "t", + "fill": "$code-ink", + "content": " { drizzleAdapter } ", + "lineHeight": 1.75, + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "GuAbX", + "name": "t", + "fill": "#E0A56A", + "content": "from", + "lineHeight": 1.75, + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "hYUxQ", + "name": "t", + "fill": "#9CC5A1", + "content": " \"better-content/adapters/postgres\";", + "lineHeight": 1.75, + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "s12db", + "name": "blank", + "width": "fill_container", + "height": 20 + }, + { + "type": "frame", + "id": "vSdVN", + "name": "line", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "YgIPM", + "name": "t", + "fill": "#E0A56A", + "content": "export const", + "lineHeight": 1.75, + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Zm6Kt", + "name": "t", + "fill": "$code-ink", + "content": " adapter ", + "lineHeight": 1.75, + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "DqdKZ", + "name": "t", + "fill": "$code-ink", + "content": " = ", + "lineHeight": 1.75, + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "n7Tj1i", + "name": "t", + "fill": "$code-ink", + "content": "drizzleAdapter({", + "lineHeight": 1.75, + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Fcokd", + "name": "line", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "R7ioj", + "name": "i", + "fill": "$code-ink", + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "U5YV1", + "name": "t", + "fill": "$code-ink", + "content": " db,", + "lineHeight": 1.75, + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "AgUUX", + "name": "line", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Ec3w4", + "name": "i", + "fill": "$code-ink", + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Fcjnc", + "name": "t", + "fill": "$code-ink", + "content": " tables: { page, cards },", + "lineHeight": 1.75, + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "GIy1r", + "name": "line", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Z7jgIm", + "name": "t", + "fill": "$code-ink", + "content": "});", + "lineHeight": 1.75, + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "pa7mh", + "name": "blank", + "width": "fill_container", + "height": 20 + }, + { + "type": "frame", + "id": "LQgy6", + "name": "line", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "H7PoJT", + "name": "t", + "fill": "#6E675E", + "content": "// that is the whole integration.", + "lineHeight": 1.75, + "fontFamily": "$mono", + "fontSize": 13.5, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "E7k9Is", + "name": "Closing CTA", + "width": "fill_container", + "fill": "$panel-soft", + "stroke": "$line", + "strokeWidth": { + "top": 1 + }, + "layout": "vertical", + "gap": 28, + "padding": [ + 96, + 48 + ], + "children": [ + { + "type": "frame", + "id": "NjvrH", + "name": "Inner", + "width": 1040, + "layout": "vertical", + "gap": 24, + "children": [ + { + "type": "text", + "id": "uIx9y", + "name": "Title", + "fill": "$ink", + "content": "Take it home.", + "lineHeight": 1.1, + "fontFamily": "$serif", + "fontSize": 64, + "fontWeight": "normal", + "letterSpacing": -1.4 + }, + { + "type": "text", + "id": "fPqAq", + "name": "Sub", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 660, + "content": "Scaffold a Next.js, Nuxt, SvelteKit or Astro app wired to Postgres or Firestore. MIT licensed, pre-1.0, no account required.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 19, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "MxvWs", + "name": "Buttons", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "VpCt5", + "name": "Scaffold", + "fill": "$ink", + "cornerRadius": 10, + "gap": 12, + "padding": [ + 15, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mvMFU", + "name": "Prompt", + "fill": "#FFFFFF66", + "content": "$", + "fontFamily": "$mono", + "fontSize": 15, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "ejBG8", + "name": "Command", + "fill": "$paper", + "content": "npm create better-content", + "fontFamily": "$mono", + "fontSize": 15, + "fontWeight": "normal" + }, + { + "type": "icon", + "id": "HcF4X", + "name": "Copy", + "width": 15, + "height": 15, + "icon": "copy", + "library": "lucide", + "fill": "#FFFFFF66" + } + ] + }, + { + "type": "frame", + "id": "yMsmj", + "name": "Docs", + "cornerRadius": 10, + "stroke": "$line-strong", + "strokeWidth": 1, + "gap": 8, + "padding": [ + 15, + 20 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Ym2jb", + "name": "Label", + "fill": "$ink", + "content": "Read the docs", + "fontFamily": "$sans", + "fontSize": 15, + "fontWeight": "500" + }, + { + "type": "icon", + "id": "jLnW1", + "name": "Arrow", + "width": 15, + "height": 15, + "icon": "arrow-right", + "library": "lucide", + "fill": "$ink" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "L2gaT7", + "name": "Footer", + "width": "fill_container", + "fill": "$paper", + "stroke": "$line", + "strokeWidth": { + "top": 1 + }, + "layout": "vertical", + "gap": 40, + "padding": [ + 48, + 48, + 40, + 48 + ], + "children": [ + { + "type": "frame", + "id": "FO2jz", + "name": "Columns", + "width": 1344, + "gap": 32, + "children": [ + { + "type": "frame", + "id": "J18vj", + "name": "Brand col", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "frame", + "id": "R06Vm", + "name": "Brand", + "gap": 9, + "alignItems": "center", + "children": [ + { + "id": "C8CzCO", + "type": "ref", + "ref": "z7lkD9", + "name": "Logo" + }, + { + "type": "text", + "id": "s8r5Gs", + "name": "Wordmark", + "fill": "$ink", + "content": "better-content", + "fontFamily": "$mono", + "fontSize": 14, + "fontWeight": "500" + } + ] + }, + { + "type": "text", + "id": "jWcnu", + "name": "Tag", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 260, + "content": "Own-your-data inline editing. MIT.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "bwSnU", + "name": "Learn", + "width": 180, + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "text", + "id": "of7xr", + "name": "Head", + "fill": "$faint", + "content": "Learn", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + }, + { + "type": "text", + "id": "pJAA0", + "name": "Quickstart", + "fill": "$muted", + "content": "Quickstart", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "MpwIR", + "name": "Guides", + "fill": "$muted", + "content": "Guides", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "gOtZ4", + "name": "API reference", + "fill": "$muted", + "content": "API reference", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EYic2", + "name": "Migration", + "fill": "$muted", + "content": "Migration", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "t1dSsx", + "name": "Adapters", + "width": 180, + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "text", + "id": "AMpR7", + "name": "Head", + "fill": "$faint", + "content": "Adapters", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + }, + { + "type": "text", + "id": "lT0gJ", + "name": "Postgres", + "fill": "$muted", + "content": "Postgres", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vAiVT", + "name": "Firestore", + "fill": "$muted", + "content": "Firestore", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Keyyb", + "name": "In-memory", + "fill": "$muted", + "content": "In-memory", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "M973A5", + "name": "Write your own", + "fill": "$muted", + "content": "Write your own", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "MWi0C", + "name": "Bindings", + "width": 180, + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "text", + "id": "at2dx", + "name": "Head", + "fill": "$faint", + "content": "Bindings", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + }, + { + "type": "text", + "id": "gD7f8", + "name": "React", + "fill": "$muted", + "content": "React", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "iWpLK", + "name": "Vue", + "fill": "$muted", + "content": "Vue", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "b9uoSJ", + "name": "Svelte", + "fill": "$muted", + "content": "Svelte", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "Ffh2k", + "name": "Core", + "fill": "$muted", + "content": "Core", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "zUMYy", + "name": "Project", + "width": 180, + "layout": "vertical", + "gap": 12, + "children": [ + { + "type": "text", + "id": "CAbM6", + "name": "Head", + "fill": "$faint", + "content": "Project", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal", + "letterSpacing": 0.8 + }, + { + "type": "text", + "id": "rTzVS", + "name": "GitHub", + "fill": "$muted", + "content": "GitHub", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "aouTC", + "name": "npm", + "fill": "$muted", + "content": "npm", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "vWduO", + "name": "Changelog", + "fill": "$muted", + "content": "Changelog", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "u2qRW", + "name": "License", + "fill": "$muted", + "content": "License", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "iF0zY", + "name": "Colophon", + "width": 1344, + "stroke": "$line", + "strokeWidth": { + "top": 1 + }, + "padding": [ + 20, + 0, + 0, + 0 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "vXLVN", + "name": "Left", + "fill": "$faint", + "content": "Pre-1.0 — minor versions may break until 1.0.", + "fontFamily": "$sans", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "EtoiO", + "name": "Right", + "fill": "$faint", + "content": "Every word on this page is a row you can edit.", + "fontFamily": "$sans", + "fontSize": 13, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "x7ZEh2", + "x": 0, + "y": -240, + "name": "Keycap", + "reusable": true, + "fill": "$panel", + "cornerRadius": 6, + "stroke": "$line-strong", + "strokeWidth": 1, + "padding": [ + 3, + 9 + ], + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "lLGR9", + "name": "Key", + "fill": "$ink", + "content": "⌘", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "500" + } + ] + }, + { + "type": "frame", + "id": "d64N92", + "x": 1560, + "y": 0, + "name": "better-content — Edit mode", + "clip": true, + "width": 1440, + "height": 1000, + "fill": "$paper", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "fO5Hc", + "name": "Header", + "width": "fill_container", + "fill": "$paper", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "padding": [ + 20, + 48 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "y45CtF", + "name": "Brand", + "width": 255, + "gap": 10, + "alignItems": "center", + "children": [ + { + "id": "dMmSI", + "type": "ref", + "ref": "z7lkD9", + "name": "Logo" + }, + { + "type": "text", + "id": "ZvnxK", + "name": "Wordmark", + "fill": "$ink", + "content": "better-content", + "fontFamily": "$mono", + "fontSize": 15, + "fontWeight": "500", + "letterSpacing": -0.2 + } + ] + }, + { + "type": "frame", + "id": "L6ydi", + "name": "Nav", + "gap": 28, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "HWqim", + "name": "Docs", + "fill": "$muted", + "content": "Docs", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "q0eUZ", + "name": "Adapters", + "fill": "$muted", + "content": "Adapters", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "y2KZTZ", + "name": "GitHub", + "fill": "$muted", + "content": "GitHub", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "p3Es3T", + "name": "Actions", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "N3Vma", + "name": "Theme toggle", + "cornerRadius": 8, + "stroke": "$line", + "strokeWidth": 1, + "padding": 8, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "bCf0h", + "name": "Sun", + "width": 15, + "height": 15, + "icon": "sun", + "library": "lucide", + "fill": "$muted" + } + ] + }, + { + "type": "frame", + "id": "SpJkw", + "name": "Discard", + "cornerRadius": 8, + "stroke": "$line-strong", + "strokeWidth": 1, + "padding": [ + 9, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "l2zRE6", + "name": "Label", + "fill": "$muted", + "content": "Discard", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "oyVZz", + "name": "Edit mode on", + "fill": "$accent", + "cornerRadius": 8, + "gap": 8, + "padding": [ + 9, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "rJhvZ", + "name": "Pen", + "width": 14, + "height": 14, + "icon": "check", + "library": "lucide", + "fill": "$ink" + }, + { + "type": "text", + "id": "Y4S0L", + "name": "Label", + "fill": "$ink", + "content": "Save all", + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "500" + }, + { + "type": "text", + "id": "BtI4o", + "name": "Shortcut", + "fill": "#20242BA6", + "content": "⌘S", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "LUjtz", + "name": "Canvas", + "width": "fill_container", + "height": "fill_container", + "layout": "vertical", + "padding": [ + 64, + 48, + 0, + 48 + ], + "children": [ + { + "type": "frame", + "id": "g0u9l", + "name": "Column", + "width": 1040, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "iIotg", + "name": "Field hero.title", + "width": "fill_container", + "fill": "#00000000", + "cornerRadius": 8, + "stroke": "$mark-line", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "glZzp", + "name": "Line", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "R5MqM", + "name": "Part 1", + "fill": "$ink", + "content": "Edit ", + "lineHeight": 1.08, + "fontFamily": "$serif", + "fontSize": 76, + "fontWeight": "normal", + "letterSpacing": -1.6 + }, + { + "type": "frame", + "id": "X8hE3", + "name": "Selection", + "fill": "$accent", + "cornerRadius": 2, + "padding": [ + 0, + 1 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Zse9Y", + "name": "Selected", + "fill": "$ink", + "content": "this page", + "lineHeight": 1.08, + "fontFamily": "$serif", + "fontSize": 76, + "fontWeight": "normal", + "letterSpacing": -1.6 + } + ] + }, + { + "type": "text", + "id": "vOn1z", + "name": "Part 2", + "fill": "$ink", + "content": ".", + "lineHeight": 1.08, + "fontFamily": "$serif", + "fontSize": 76, + "fontWeight": "normal", + "letterSpacing": -1.6 + }, + { + "type": "rectangle", + "id": "gJPno", + "name": "Caret", + "fill": "$ink", + "width": 2, + "height": 72 + } + ] + } + ] + }, + { + "type": "frame", + "id": "k8JNUp", + "name": "Gap", + "width": "fill_container", + "height": 60 + }, + { + "type": "frame", + "id": "KmJ3O", + "name": "Field hero.deck", + "width": "fill_container", + "cornerRadius": 8, + "stroke": "$line-strong", + "layout": "vertical", + "children": [ + { + "type": "text", + "id": "gN92O", + "name": "Text", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "better-content is an inline-edit CMS engine that writes to a database you own — through a seven-method adapter, from React, Vue or Svelte.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 20, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "axBK3", + "layoutPosition": "absolute", + "x": -30, + "y": 6, + "name": "Block handle", + "cornerRadius": 5, + "gap": 3, + "padding": [ + 4, + 3 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "c9Ye1a", + "name": "Grip", + "width": 14, + "height": 14, + "icon": "grip-vertical", + "library": "lucide", + "fill": "$line-strong" + } + ] + } + ] + }, + { + "type": "frame", + "id": "j97Rc", + "name": "Spacer", + "width": "fill_container", + "height": 44 + }, + { + "type": "frame", + "id": "jCjUa", + "name": "Islands", + "width": "fill_container", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "iZVN7", + "name": "React", + "clip": true, + "width": "fill_container", + "fill": "$panel", + "cornerRadius": 12, + "stroke": "$line", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "CNkFw", + "name": "Bar", + "width": "fill_container", + "fill": "$panel-soft", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 8, + "padding": [ + 9, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "e1leQ", + "name": "Dot", + "fill": "$react", + "width": 8, + "height": 8 + }, + { + "type": "text", + "id": "V68fm", + "name": "Name", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "React", + "fontFamily": "$sans", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "fKGOl", + "name": "State", + "fill": "$muted", + "content": "typing", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "B4mfK", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "padding": 16, + "children": [ + { + "type": "frame", + "id": "CUPFp", + "name": "Title row", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "g6exz", + "name": "Title", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "One engine, three bindings", + "lineHeight": 1.3, + "fontFamily": "$serif", + "fontSize": 19, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "id": "ir9qm", + "name": "Caret", + "fill": "$ink", + "width": 2, + "height": 21 + } + ] + }, + { + "type": "text", + "id": "Qrso4", + "name": "Body", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "getSnapshot() and subscribe(). Everything else is a thin adapter.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "Huwgy", + "name": "Vue", + "clip": true, + "width": "fill_container", + "fill": "$panel", + "cornerRadius": 12, + "stroke": "$line", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "o69xc", + "name": "Bar", + "width": "fill_container", + "fill": "$panel-soft", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 8, + "padding": [ + 9, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "p46cS", + "name": "Dot", + "fill": "$vue", + "width": 8, + "height": 8 + }, + { + "type": "text", + "id": "oUb2P", + "name": "Name", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Vue", + "fontFamily": "$sans", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "erWdS", + "name": "State", + "fill": "$green", + "content": "re-rendered", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "pqSEn", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "padding": 16, + "children": [ + { + "type": "frame", + "id": "Y5unXS", + "name": "Title row", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "LpFaA", + "name": "Title", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "One engine, three bindings", + "lineHeight": 1.3, + "fontFamily": "$serif", + "fontSize": 19, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "WBJmj", + "name": "Body", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "getSnapshot() and subscribe(). Everything else is a thin adapter.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "kWp4j", + "name": "Svelte", + "clip": true, + "width": "fill_container", + "fill": "$panel", + "cornerRadius": 12, + "stroke": "$line", + "strokeWidth": 1, + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "yNBFQ", + "name": "Bar", + "width": "fill_container", + "fill": "$panel-soft", + "stroke": "$line", + "strokeWidth": { + "bottom": 1 + }, + "gap": 8, + "padding": [ + 9, + 14 + ], + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "E2iOGO", + "name": "Dot", + "fill": "$svelte", + "width": 8, + "height": 8 + }, + { + "type": "text", + "id": "c1LNC", + "name": "Name", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Svelte", + "fontFamily": "$sans", + "fontSize": 13, + "fontWeight": "500" + }, + { + "type": "text", + "id": "ys7aN", + "name": "State", + "fill": "$green", + "content": "re-rendered", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "I3Tnq", + "name": "Body", + "width": "fill_container", + "layout": "vertical", + "gap": 8, + "padding": 16, + "children": [ + { + "type": "frame", + "id": "rvLJV", + "name": "Title row", + "width": "fill_container", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "eUYdq", + "name": "Title", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "One engine, three bindings", + "lineHeight": 1.3, + "fontFamily": "$serif", + "fontSize": 19, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "I8qiE1", + "name": "Body", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "getSnapshot() and subscribe(). Everything else is a thin adapter.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "lrd4K", + "name": "Gap 2", + "width": "fill_container", + "height": 64 + }, + { + "type": "frame", + "id": "GY9om", + "name": "Next section", + "width": "fill_container", + "layout": "vertical", + "gap": 16, + "children": [ + { + "type": "frame", + "id": "Kjhop", + "name": "Eyebrow chip", + "fill": "$accent", + "cornerRadius": 4, + "padding": [ + 3, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "jIufF", + "name": "Label", + "fill": "$ink", + "content": "04 — ADAPTERS", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal", + "letterSpacing": 1.2 + } + ] + }, + { + "type": "text", + "id": "jLW1Y", + "name": "Title", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Seven methods stand between us and your database.", + "lineHeight": 1.2, + "fontFamily": "$serif", + "fontSize": 40, + "fontWeight": "normal", + "letterSpacing": -0.8 + }, + { + "type": "text", + "id": "L3dlgj", + "name": "Sub", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 680, + "content": "There is no hosted service to sign up for and no proprietary backend to migrate off. Implement these seven and better-content talks to whatever you already run.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "GPW0q", + "name": "Database drawer", + "clip": true, + "width": "fill_container", + "fill": "$code-bg", + "layout": "vertical", + "children": [ + { + "type": "frame", + "id": "NsULs", + "name": "Drawer bar", + "width": "fill_container", + "stroke": "#FFFFFF14", + "padding": [ + 12, + 20 + ], + "justifyContent": "space_between", + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "t7ZUL", + "name": "Left", + "gap": 12, + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "uyXyT", + "name": "Chevron", + "width": 15, + "height": 15, + "icon": "chevron-up", + "library": "lucide", + "fill": "$code-muted" + }, + { + "type": "text", + "id": "Va0FF", + "name": "Title", + "fill": "$code-ink", + "content": "database", + "fontFamily": "$mono", + "fontSize": 13, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "fqe0Z", + "name": "Pending", + "fill": "#FFE24A29", + "cornerRadius": 20, + "gap": 6, + "padding": [ + 3, + 9 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "mvWrx", + "name": "Label", + "fill": "$accent", + "content": "1 write in flight", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "s5CVG", + "name": "Right", + "gap": 10, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "K0zEeP", + "name": "Label", + "fill": "$code-muted", + "content": "adapter", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "g6LByT", + "name": "Segmented", + "fill": "#00000059", + "cornerRadius": 9, + "stroke": "#FFFFFF14", + "strokeWidth": 1, + "gap": 2, + "padding": 3, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "M43Be", + "name": "PGlite", + "fill": "$accent", + "cornerRadius": 7, + "padding": [ + 5, + 11 + ], + "children": [ + { + "type": "text", + "id": "JjgVu", + "name": "Label", + "fill": "$ink", + "content": "PGlite", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "ND6HT", + "name": "In-memory", + "fill": "#00000000", + "cornerRadius": 7, + "padding": [ + 5, + 11 + ], + "children": [ + { + "type": "text", + "id": "ArnP1", + "name": "Label", + "fill": "$code-muted", + "content": "In-memory", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "OqF1H", + "name": "REST", + "fill": "#00000000", + "cornerRadius": 7, + "padding": [ + 5, + 11 + ], + "children": [ + { + "type": "text", + "id": "SQuBZ", + "name": "Label", + "fill": "$code-muted", + "content": "REST", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "vpLyf", + "layoutPosition": "absolute", + "x": 203, + "y": 230, + "name": "Format toolbar", + "fill": "$ink", + "cornerRadius": 12, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#1F1F1C4D", + "offset": { + "x": 0, + "y": 8 + }, + "blur": 28, + "spread": -4 + }, + "gap": 4, + "padding": 5, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "o2jiZN", + "name": "bold", + "fill": "#FFFFFF1F", + "cornerRadius": 8, + "padding": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "k4HD3", + "name": "i", + "width": 16, + "height": 16, + "icon": "bold", + "library": "lucide", + "fill": "#FFFFFF" + } + ] + }, + { + "type": "frame", + "id": "WTUzn", + "name": "italic", + "fill": "#00000000", + "cornerRadius": 8, + "padding": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "g7Hgx", + "name": "i", + "width": 16, + "height": 16, + "icon": "italic", + "library": "lucide", + "fill": "#FFFFFFB3" + } + ] + }, + { + "type": "frame", + "id": "q91t4", + "name": "link", + "fill": "#00000000", + "cornerRadius": 8, + "padding": 8, + "justifyContent": "center", + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "VvfFS", + "name": "i", + "width": 16, + "height": 16, + "icon": "link", + "library": "lucide", + "fill": "#FFFFFFB3" + } + ] + }, + { + "type": "rectangle", + "id": "StJc6", + "name": "Divider", + "fill": "#FFFFFF26", + "width": 1, + "height": 22 + }, + { + "type": "frame", + "id": "f8wzCU", + "name": "Field path", + "gap": 7, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "aXiXj", + "name": "Coll", + "fill": "#FFFFFF80", + "content": "page", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "LZEGO", + "name": "Sep", + "fill": "#FFFFFF4D", + "content": "/", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "FlVhW", + "name": "Field", + "fill": "$paper", + "content": "hero.title", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "z7lkD9", + "x": 0, + "y": -440, + "name": "Logo", + "reusable": true, + "clip": true, + "width": 24, + "height": 24, + "fill": "$ink", + "cornerRadius": 5.25, + "layout": "none", + "children": [ + { + "type": "rectangle", + "cornerRadius": 0.75, + "id": "QahdK", + "x": 4.5, + "y": 5.25, + "name": "Line 1", + "fill": "#FAF9F6", + "width": 15, + "height": 2.25 + }, + { + "type": "rectangle", + "cornerRadius": 0.75, + "id": "TUWXC", + "x": 4.5, + "y": 9.75, + "name": "Line 2", + "fill": "#71767F", + "width": 10.5, + "height": 1.875 + }, + { + "type": "rectangle", + "cornerRadius": 1.125, + "id": "rtRi4", + "x": 4.5, + "y": 14.25, + "name": "Highlight", + "fill": "$accent", + "width": 15, + "height": 4.5 + } + ] + }, + { + "type": "frame", + "id": "c85Ko", + "x": 1560, + "y": 1120, + "name": "Edit affordances — states", + "clip": true, + "width": 1100, + "fill": "$paper", + "layout": "vertical", + "padding": [ + 56, + 56, + 64, + 56 + ], + "children": [ + { + "type": "frame", + "id": "EjUdn", + "name": "Head", + "width": "fill_container", + "layout": "vertical", + "gap": 14, + "padding": [ + 0, + 0, + 36, + 0 + ], + "children": [ + { + "type": "frame", + "id": "yjEKX", + "name": "Eyebrow chip", + "fill": "$accent", + "cornerRadius": 4, + "padding": [ + 3, + 8 + ], + "children": [ + { + "type": "text", + "id": "iD3bH", + "name": "Label", + "fill": "$ink", + "content": "EDIT MODE — STATES", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal", + "letterSpacing": 1.2 + } + ] + }, + { + "type": "text", + "id": "I5xGtF", + "name": "Title", + "fill": "$ink", + "content": "The page never changes. Only what you touch does.", + "lineHeight": 1.25, + "fontFamily": "$serif", + "fontSize": 34, + "fontWeight": "normal", + "letterSpacing": -0.6 + }, + { + "type": "text", + "id": "d9qmq1", + "name": "Sub", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": 700, + "content": "Every affordance below is transient and scoped to one block. Nothing outlines, tints or reflows the page on entering edit mode.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 16, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "yMxZy", + "name": "IDLE", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "top": 1 + }, + "gap": 36, + "padding": [ + 26, + 0 + ], + "children": [ + { + "type": "frame", + "id": "lHspB", + "name": "Meta", + "width": 250, + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "t9Rnw", + "name": "Name", + "fill": "$ink", + "content": "IDLE", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal", + "letterSpacing": 0.9 + }, + { + "type": "text", + "id": "H8u8tU", + "name": "Caption", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Edit mode is on and nothing is focused. Pixel-identical to reading mode.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "Dn6oK", + "name": "Demo", + "width": "fill_container", + "layout": "vertical", + "padding": [ + 0, + 0, + 0, + 34 + ], + "children": [ + { + "type": "text", + "id": "Z3rIW8", + "name": "Body", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Content is rows, not commits. That is the difference between this and a git-backed CMS — it works for apps, not only static sites.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "MFEV1", + "name": "HOVER", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "top": 1 + }, + "gap": 36, + "padding": [ + 26, + 0 + ], + "children": [ + { + "type": "frame", + "id": "vVpCS", + "name": "Meta", + "width": 250, + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "fVkfv", + "name": "Name", + "fill": "$ink", + "content": "HOVER", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal", + "letterSpacing": 0.9 + }, + { + "type": "text", + "id": "DpKqh", + "name": "Caption", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Pointer over a block. A grip fades in outside the measure — the text never moves.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "aTSmX", + "name": "Demo", + "width": "fill_container", + "layout": "vertical", + "padding": [ + 0, + 0, + 0, + 34 + ], + "children": [ + { + "type": "text", + "id": "cRpQg", + "name": "Body", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Content is rows, not commits. That is the difference between this and a git-backed CMS — it works for apps, not only static sites.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "iBTZe", + "layoutPosition": "absolute", + "x": -34, + "y": 3, + "name": "Grip", + "cornerRadius": 5, + "padding": [ + 4, + 3 + ], + "alignItems": "center", + "children": [ + { + "type": "icon", + "id": "Y59u0k", + "name": "Icon", + "width": 14, + "height": 14, + "icon": "grip-vertical", + "library": "lucide", + "fill": "$line-strong" + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "uBVoa", + "name": "SELECTION", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "top": 1 + }, + "gap": 36, + "padding": [ + 26, + 0 + ], + "children": [ + { + "type": "frame", + "id": "jMeyj", + "name": "Meta", + "width": 250, + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "tbyE3", + "name": "Name", + "fill": "$ink", + "content": "SELECTION", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal", + "letterSpacing": 0.9 + }, + { + "type": "text", + "id": "tWFZD", + "name": "Caption", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "The marker highlights the range. The toolbar floats at the selection and names the field.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "B7it4", + "name": "Demo", + "width": "fill_container", + "layout": "vertical", + "gap": 12, + "padding": [ + 0, + 0, + 0, + 34 + ], + "children": [ + { + "type": "frame", + "id": "VHBJo", + "name": "Line", + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Ifcmv", + "name": "Pre", + "fill": "$ink", + "content": "Content is ", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "uHI6a", + "name": "Selection", + "fill": "$accent", + "cornerRadius": 2, + "padding": 1, + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "Y7G5pt", + "name": "Selected", + "fill": "$ink", + "content": "rows, not commits", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "zkKEB", + "name": "Post", + "fill": "$ink", + "content": ".", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + }, + { + "type": "rectangle", + "id": "M2Zhh", + "name": "Caret", + "fill": "$ink", + "width": 1.5, + "height": 22 + } + ] + }, + { + "type": "frame", + "id": "x8aD4N", + "name": "Toolbar", + "fill": "$ink", + "cornerRadius": 10, + "effect": { + "type": "shadow", + "shadowType": "outer", + "color": "#20242B4D", + "offset": { + "x": 0, + "y": 6 + }, + "blur": 20, + "spread": -4 + }, + "gap": 3, + "padding": 5, + "alignItems": "center", + "children": [ + { + "type": "frame", + "id": "KlaWM", + "name": "bold", + "fill": "#FFFFFF1F", + "cornerRadius": 7, + "padding": 7, + "children": [ + { + "type": "icon", + "id": "HLvcb", + "name": "i", + "width": 14, + "height": 14, + "icon": "bold", + "library": "lucide", + "fill": "#FFFFFF" + } + ] + }, + { + "type": "frame", + "id": "C0OJRo", + "name": "italic", + "fill": "#00000000", + "cornerRadius": 7, + "padding": 7, + "children": [ + { + "type": "icon", + "id": "OQjVC", + "name": "i", + "width": 14, + "height": 14, + "icon": "italic", + "library": "lucide", + "fill": "#FFFFFFB3" + } + ] + }, + { + "type": "frame", + "id": "pZltV", + "name": "link", + "fill": "#00000000", + "cornerRadius": 7, + "padding": 7, + "children": [ + { + "type": "icon", + "id": "MfsBN", + "name": "i", + "width": 14, + "height": 14, + "icon": "link", + "library": "lucide", + "fill": "#FFFFFFB3" + } + ] + }, + { + "type": "rectangle", + "id": "eokY1", + "name": "Divider", + "fill": "#FFFFFF26", + "width": 1, + "height": 18 + }, + { + "type": "frame", + "id": "mtZUP", + "name": "Path", + "gap": 6, + "padding": [ + 0, + 8 + ], + "alignItems": "center", + "children": [ + { + "type": "text", + "id": "KWz0S", + "name": "Coll", + "fill": "#FFFFFF80", + "content": "cards", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "WMgof", + "name": "Sep", + "fill": "#FFFFFF4D", + "content": "/", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + }, + { + "type": "text", + "id": "fk5Qu", + "name": "Field", + "fill": "$code-ink", + "content": "body", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + } + ] + } + ] + } + ] + }, + { + "type": "frame", + "id": "sX03Q", + "name": "CARET BETWEEN BLOCKS", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "top": 1 + }, + "gap": 36, + "padding": [ + 26, + 0 + ], + "children": [ + { + "type": "frame", + "id": "mbVjf", + "name": "Meta", + "width": 250, + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "y9Jn81", + "name": "Name", + "fill": "$ink", + "content": "CARET BETWEEN BLOCKS", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal", + "letterSpacing": 0.9 + }, + { + "type": "text", + "id": "em1tn", + "name": "Caption", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Only now does the insert rule appear, at the one gap the caret is in.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "dMitG", + "name": "Demo", + "width": "fill_container", + "layout": "vertical", + "padding": [ + 0, + 0, + 0, + 34 + ], + "children": [ + { + "type": "text", + "id": "EXdLQ", + "name": "Body", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Content is rows, not commits.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "mdrnC", + "name": "Insert rule", + "width": "fill_container", + "gap": 10, + "padding": [ + 13, + 0 + ], + "alignItems": "center", + "children": [ + { + "type": "rectangle", + "id": "d547z", + "name": "Caret", + "fill": "$ink", + "width": 1.5, + "height": 14 + }, + { + "type": "rectangle", + "id": "AxNnR", + "name": "Rule", + "fill": "$mark-line", + "width": "fill_container", + "height": 1 + }, + { + "type": "text", + "id": "g0SLi", + "name": "Hint", + "fill": "$muted", + "content": "⏎ insert block", + "fontFamily": "$mono", + "fontSize": 11, + "fontWeight": "normal" + } + ] + }, + { + "type": "text", + "id": "QvI7Y", + "name": "Body", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "That is the difference between this and a git-backed CMS.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + } + ] + } + ] + }, + { + "type": "frame", + "id": "ZLGGu", + "name": "UNSAVED", + "width": "fill_container", + "stroke": "$line", + "strokeWidth": { + "top": 1 + }, + "gap": 36, + "padding": [ + 26, + 0 + ], + "children": [ + { + "type": "frame", + "id": "I45oh", + "name": "Meta", + "width": 250, + "layout": "vertical", + "gap": 8, + "children": [ + { + "type": "text", + "id": "JAqvM", + "name": "Name", + "fill": "$ink", + "content": "UNSAVED", + "fontFamily": "$mono", + "fontSize": 12, + "fontWeight": "normal", + "letterSpacing": 0.9 + }, + { + "type": "text", + "id": "ocIIO", + "name": "Caption", + "fill": "$muted", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "A marker dot in the margin until the adapter acknowledges the write.", + "lineHeight": 1.55, + "fontFamily": "$sans", + "fontSize": 14, + "fontWeight": "normal" + } + ] + }, + { + "type": "frame", + "id": "DgPDt", + "name": "Demo", + "width": "fill_container", + "layout": "vertical", + "padding": [ + 0, + 0, + 0, + 34 + ], + "children": [ + { + "type": "text", + "id": "aQAu4", + "name": "Body", + "fill": "$ink", + "textGrowth": "fixed-width", + "width": "fill_container", + "content": "Content is rows, not commits. That is the difference between this and a git-backed CMS — it works for apps, not only static sites.", + "lineHeight": 1.6, + "fontFamily": "$sans", + "fontSize": 17, + "fontWeight": "normal" + }, + { + "type": "frame", + "id": "gvMeE", + "layoutPosition": "absolute", + "x": -30, + "y": 9, + "name": "Dirty marker", + "alignItems": "center", + "children": [ + { + "type": "ellipse", + "id": "eD5Y8", + "name": "Dot", + "fill": "$accent", + "width": 8, + "height": 8, + "stroke": "$mark-line", + "strokeWidth": 1 + } + ] + } + ] + } + ] + } + ] + } + ], + "variables": { + "paper": { + "type": "color", + "value": "#FFFFFF" + }, + "panel": { + "type": "color", + "value": "#FAF9F6" + }, + "panel-soft": { + "type": "color", + "value": "#F4F4F1" + }, + "ink": { + "type": "color", + "value": "#20242B" + }, + "muted": { + "type": "color", + "value": "#71767F" + }, + "faint": { + "type": "color", + "value": "#A8ACB3" + }, + "line": { + "type": "color", + "value": "#E7E8EB" + }, + "line-strong": { + "type": "color", + "value": "#CDD0D6" + }, + "accent": { + "type": "color", + "value": "#FFE24A" + }, + "accent-soft": { + "type": "color", + "value": "#FFF4BF" + }, + "green": { + "type": "color", + "value": "#2F8F5B" + }, + "green-soft": { + "type": "color", + "value": "#E3F1E9" + }, + "code-bg": { + "type": "color", + "value": "#20242B" + }, + "code-ink": { + "type": "color", + "value": "#FAF9F6" + }, + "code-muted": { + "type": "color", + "value": "#8A9099" + }, + "code-soft": { + "type": "color", + "value": "#2A2F37" + }, + "react": { + "type": "color", + "value": "#087EA4" + }, + "vue": { + "type": "color", + "value": "#42B883" + }, + "svelte": { + "type": "color", + "value": "#FF3E00" + }, + "serif": { + "type": "string", + "value": "IBM Plex Serif" + }, + "sans": { + "type": "string", + "value": "IBM Plex Sans" + }, + "mono": { + "type": "string", + "value": "IBM Plex Mono" + }, + "mark-line": { + "type": "color", + "value": "#E0C22A" + }, + "green-bright": { + "type": "color", + "value": "#5FCB8E" + } + }, + "fileToken": "c4a931a4-08fe-42bc-a91a-eee47c989399" +} \ No newline at end of file diff --git a/examples/playground/src/components/AdapterCalls.astro b/examples/playground/src/components/AdapterCalls.astro index 2a2ce01..24db18a 100644 --- a/examples/playground/src/components/AdapterCalls.astro +++ b/examples/playground/src/components/AdapterCalls.astro @@ -1,10 +1,6 @@
adapter calls - live
    diff --git a/examples/playground/src/components/DatabaseRows.astro b/examples/playground/src/components/DatabaseRows.astro index d557338..2da9c74 100644 --- a/examples/playground/src/components/DatabaseRows.astro +++ b/examples/playground/src/components/DatabaseRows.astro @@ -1,24 +1,40 @@ -
    +--- +const fields = [ + ["hero.headline", "page", "hero", "headline"], + ["hero.tagline", "page", "hero", "tagline"], + ["hero.intro", "page", "hero", "intro"], + ["shared.message", "page", "shared", "message"], + ["cards[0].title", "cards", "cards-are-rows", "title"], + ["cards[1].title", "cards", "ops-are-optimistic", "title"], +]; +--- + +
    your database - 0 rows + {fields.length} rows +
    +
    + { + fields.map(([key, collection, id, field]) => ( +
    + {key} + +
    + )) + }
    -
    diff --git a/examples/playground/src/components/Toolbar.astro b/examples/playground/src/components/Toolbar.astro deleted file mode 100644 index 7daa6cf..0000000 --- a/examples/playground/src/components/Toolbar.astro +++ /dev/null @@ -1,89 +0,0 @@ ---- -import { BookOpen } from "@lucide/astro"; -import { siGithub } from "simple-icons"; ---- - -
    - -
    - - diff --git a/examples/playground/src/components/react/HeroIsland.tsx b/examples/playground/src/components/react/HeroIsland.tsx new file mode 100644 index 0000000..d23c1ad --- /dev/null +++ b/examples/playground/src/components/react/HeroIsland.tsx @@ -0,0 +1,63 @@ +import { + CmsAuthProvider, + ContentEditSpan, + PageProvider, +} from "better-content/react"; +import { editMode } from "../../lib/editMode"; +import { useLiveEngine } from "./useLiveEngine"; + +function HeroContent() { + return ( + <> +

    + + +

    + + + ); +} + +export default function HeroIsland() { + const { engine, isEditing } = useLiveEngine(); + + if (!engine) { + return ( + + ); + } + + return ( + + + + + + ); +} diff --git a/examples/playground/src/components/react/NoteCard.tsx b/examples/playground/src/components/react/NoteCard.tsx index a5bb550..bae36ff 100644 --- a/examples/playground/src/components/react/NoteCard.tsx +++ b/examples/playground/src/components/react/NoteCard.tsx @@ -4,21 +4,25 @@ export default function NoteCard() { const { hasUnsavedChanges } = usePageContext(); return ( -
    -
    -

    React

    - <ContentEditSpan /> +
    +
    + + React + <ContentEditSpan /> + + {hasUnsavedChanges ? "unsaved" : "synced"} +
    - -
    - unsaved changes: {hasUnsavedChanges ? "yes" : "no"} -
    +
    +

    page / shared.message

    + +
    ); } diff --git a/examples/playground/src/components/react/NoteIsland.tsx b/examples/playground/src/components/react/NoteIsland.tsx new file mode 100644 index 0000000..9ebded8 --- /dev/null +++ b/examples/playground/src/components/react/NoteIsland.tsx @@ -0,0 +1,20 @@ +import { CmsAuthProvider, PageProvider } from "better-content/react"; +import { editMode } from "../../lib/editMode"; +import { useLiveEngine } from "./useLiveEngine"; +import NoteCard from "./NoteCard"; + +export default function NoteIsland() { + const { engine, isEditing } = useLiveEngine(); + + if (!engine) return

    starting React...

    ; + + return ( + + + + + + ); +} diff --git a/examples/playground/src/components/react/PreviewFrame.tsx b/examples/playground/src/components/react/PreviewFrame.tsx deleted file mode 100644 index 94e698d..0000000 --- a/examples/playground/src/components/react/PreviewFrame.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import { useEffect, useState } from "react"; -import type { CmsEngine, Item } from "better-content/core"; -import { - CmsAuthProvider, - ContentEditSpan, - EditableImage, - PageProvider, - useCmsAuth, - usePageContext, -} from "better-content/react"; -import { ready } from "../../lib/engine"; -import { editMode } from "../../lib/editMode"; - -function PreviewContent() { - const { items, engine, getItem } = usePageContext(); - const { isEditing } = useCmsAuth(); - const [draggingId, setDraggingId] = useState(null); - const cover = (getItem("page", "hero")?.cover as string) ?? ""; - - const cards = [...(items.cards ?? [])].sort( - (a, b) => ((a.order as number) ?? 0) - ((b.order as number) ?? 0), - ); - - const reorder = (sourceId: string, targetId: string) => { - if (!sourceId || sourceId === targetId) return; - const ids = cards.map((card: Item) => card.id); - const from = ids.indexOf(sourceId); - const to = ids.indexOf(targetId); - if (from < 0 || to < 0) return; - ids.splice(from, 1); - ids.splice(to, 0, sourceId); - void engine.reorderItems("cards", ids); - }; - - const add = () => - void engine.createItem("cards", { - title: "New row", - body: "I was just INSERTed. Click me in edit mode, then check the inspector.", - order: cards.length, - }); - - return ( -
    -
    - - -
    - - {({ src, isEditing, saving, openFilePicker, imgProps }) => ( -
    - {src ? ( - Editable content cover - ) : ( -
    cover image
    - )} - {isEditing &&
    click to replace the image
    } -
    - )} -
    - -
    -
    - {cards.map((card) => ( -
    { - if (!isEditing || !draggingId || draggingId === card.id) return; - event.preventDefault(); - event.dataTransfer.dropEffect = "move"; - }} - onDrop={(event) => { - if (!isEditing) return; - event.preventDefault(); - reorder( - event.dataTransfer.getData("text/plain") || draggingId || "", - card.id, - ); - setDraggingId(null); - }} - > - - - {isEditing && ( -
    - { - event.dataTransfer.effectAllowed = "move"; - event.dataTransfer.setData("text/plain", card.id); - setDraggingId(card.id); - }} - onDragEnd={() => setDraggingId(null)} - > - drag - - -
    - )} -
    - ))} - {isEditing && ( - - )} -
    - {!isEditing && ( -

    - Toggle Edit in the preview header to insert, drag, and delete rows. -

    - )} -
    -
    - ); -} - -export default function PreviewFrame() { - const [engine, setEngine] = useState(null); - const [isEditing, setIsEditing] = useState(editMode.get()); - - useEffect(() => { - void ready.then(setEngine); - return editMode.subscribe(() => setIsEditing(editMode.get())); - }, []); - - if (!engine) { - return

    starting React preview...

    ; - } - - return ( - - - - - - ); -} diff --git a/examples/playground/src/components/react/RowsIsland.tsx b/examples/playground/src/components/react/RowsIsland.tsx new file mode 100644 index 0000000..c20665d --- /dev/null +++ b/examples/playground/src/components/react/RowsIsland.tsx @@ -0,0 +1,157 @@ +import { useState } from "react"; +import type { Item } from "better-content/core"; +import { + CmsAuthProvider, + ContentEditSpan, + EditableImage, + PageProvider, + useCmsAuth, + usePageContext, +} from "better-content/react"; +import { editMode } from "../../lib/editMode"; +import { useLiveEngine } from "./useLiveEngine"; + +function RowsContent() { + const { items, engine, getItem } = usePageContext(); + const { isEditing } = useCmsAuth(); + const [draggingId, setDraggingId] = useState(null); + const cover = (getItem("page", "hero")?.cover as string) ?? ""; + + const cards = [...(items.cards ?? [])].sort( + (a, b) => ((a.order as number) ?? 0) - ((b.order as number) ?? 0), + ); + + const reorder = (sourceId: string, targetId: string) => { + if (!sourceId || sourceId === targetId) return; + const ids = cards.map((card: Item) => card.id); + const from = ids.indexOf(sourceId); + const to = ids.indexOf(targetId); + if (from < 0 || to < 0) return; + ids.splice(from, 1); + ids.splice(to, 0, sourceId); + void engine.reorderItems("cards", ids); + }; + + const add = () => { + const used = new Set(cards.map((card: Item) => card.id)); + let n = cards.length + 1; + while (used.has(`new-row-${n}`)) n += 1; + void engine.createItem( + "cards", + { + title: "New row", + body: "I was just INSERTed. Edit me, then watch the call log below.", + order: cards.length, + }, + { id: `new-row-${n}` }, + ); + }; + + return ( +
    +
    + {cards.map((card) => ( +
    { + if (!isEditing || !draggingId || draggingId === card.id) return; + event.preventDefault(); + event.dataTransfer.dropEffect = "move"; + }} + onDrop={(event) => { + if (!isEditing) return; + event.preventDefault(); + reorder( + event.dataTransfer.getData("text/plain") || draggingId || "", + card.id, + ); + setDraggingId(null); + }} + > +

    cards / {card.id}

    + + + {isEditing && ( +
    + { + event.dataTransfer.effectAllowed = "move"; + event.dataTransfer.setData("text/plain", card.id); + setDraggingId(card.id); + }} + onDragEnd={() => setDraggingId(null)} + > + drag + + +
    + )} +
    + ))} + {isEditing && ( + + )} +
    + + + {({ src, isEditing: editingImage, saving, openFilePicker, imgProps }) => ( +
    + {src ? ( + Editable cover + ) : ( +
    page / hero.cover
    + )} +
    + {editingImage + ? "click to replace, the file becomes a data URL through the storage adapter" + : "page / hero.cover, written through the storage adapter"} +
    +
    + )} +
    +
    + ); +} + +export default function RowsIsland() { + const { engine, isEditing } = useLiveEngine(); + + if (!engine) return

    starting the engine...

    ; + + return ( + + + + + + ); +} diff --git a/examples/playground/src/components/react/useLiveEngine.ts b/examples/playground/src/components/react/useLiveEngine.ts new file mode 100644 index 0000000..6f8a7de --- /dev/null +++ b/examples/playground/src/components/react/useLiveEngine.ts @@ -0,0 +1,16 @@ +import { useEffect, useState } from "react"; +import type { CmsEngine } from "better-content/core"; +import { ready } from "../../lib/engine"; +import { editMode } from "../../lib/editMode"; + +export function useLiveEngine() { + const [engine, setEngine] = useState(null); + const [isEditing, setIsEditing] = useState(editMode.get()); + + useEffect(() => { + void ready.then(setEngine); + return editMode.subscribe(() => setIsEditing(editMode.get())); + }, []); + + return { engine, isEditing }; +} diff --git a/examples/playground/src/components/svelte/NoteCard.svelte b/examples/playground/src/components/svelte/NoteCard.svelte index 526c13d..5a170ed 100644 --- a/examples/playground/src/components/svelte/NoteCard.svelte +++ b/examples/playground/src/components/svelte/NoteCard.svelte @@ -8,23 +8,26 @@ const snapshot = engineStore(engine); -
    -
    -

    Svelte

    - use:contentEdit +
    +
    + + Svelte + use:contentEdit + + {$snapshot.hasUnsavedChanges ? "unsaved" : "synced"} +
    -

    -
    - unsaved changes: - {$snapshot.hasUnsavedChanges ? "yes" : "no"} -
    +
    +

    page / shared.message

    +

    +
    diff --git a/examples/playground/src/components/svelte/PreviewIsland.svelte b/examples/playground/src/components/svelte/NoteIsland.svelte similarity index 50% rename from examples/playground/src/components/svelte/PreviewIsland.svelte rename to examples/playground/src/components/svelte/NoteIsland.svelte index 436b914..7295fcb 100644 --- a/examples/playground/src/components/svelte/PreviewIsland.svelte +++ b/examples/playground/src/components/svelte/NoteIsland.svelte @@ -1,11 +1,11 @@ {#await ready} -

    starting Svelte preview...

    +

    starting Svelte...

    {:then engine} - + {/await} diff --git a/examples/playground/src/components/svelte/PreviewFrame.svelte b/examples/playground/src/components/svelte/PreviewFrame.svelte deleted file mode 100644 index 01d20ec..0000000 --- a/examples/playground/src/components/svelte/PreviewFrame.svelte +++ /dev/null @@ -1,161 +0,0 @@ - - -
    -
    -

    -

    -
    - -
    - {#if $cover.src} - Editable content cover - {:else} -
    cover image
    - {/if} - {#if editing} -
    click to replace the image
    - {/if} -
    - -
    -
    - {#each cards as card (card.id)} -
    { - if (!editing || !draggingId || draggingId === card.id) return; - event.preventDefault(); - event.dataTransfer!.dropEffect = "move"; - }} - on:drop={(event) => { - if (!editing) return; - event.preventDefault(); - reorder( - event.dataTransfer!.getData("text/plain") || draggingId || "", - card.id, - ); - draggingId = null; - }} - > -

    -

    - {#if editing} -
    - { - event.dataTransfer!.effectAllowed = "move"; - event.dataTransfer!.setData("text/plain", card.id); - draggingId = card.id; - }} - on:dragend={() => (draggingId = null)} - > - drag - - -
    - {/if} -
    - {/each} - {#if editing} - - {/if} -
    - {#if !editing} -

    - Toggle Edit in the preview header to insert, drag, and delete rows. -

    - {/if} -
    -
    diff --git a/examples/playground/src/components/vue/NoteCard.vue b/examples/playground/src/components/vue/NoteCard.vue index 606f59d..504f2a8 100644 --- a/examples/playground/src/components/vue/NoteCard.vue +++ b/examples/playground/src/components/vue/NoteCard.vue @@ -7,24 +7,27 @@ const snapshot = useCmsSnapshot(props.engine); diff --git a/examples/playground/src/components/vue/PreviewIsland.vue b/examples/playground/src/components/vue/NoteIsland.vue similarity index 74% rename from examples/playground/src/components/vue/PreviewIsland.vue rename to examples/playground/src/components/vue/NoteIsland.vue index 08a8a98..b54a135 100644 --- a/examples/playground/src/components/vue/PreviewIsland.vue +++ b/examples/playground/src/components/vue/NoteIsland.vue @@ -3,7 +3,7 @@ import { onMounted, onUnmounted, shallowRef } from "vue"; import type { CmsEngine } from "better-content/core"; import { ready } from "../../lib/engine"; import { editMode } from "../../lib/editMode"; -import PreviewFrame from "./PreviewFrame.vue"; +import NoteCard from "./NoteCard.vue"; const engine = shallowRef(null); const editing = shallowRef(editMode.get()); @@ -17,6 +17,6 @@ onUnmounted(() => stop()); diff --git a/examples/playground/src/components/vue/PreviewFrame.vue b/examples/playground/src/components/vue/PreviewFrame.vue deleted file mode 100644 index 1cdf512..0000000 --- a/examples/playground/src/components/vue/PreviewFrame.vue +++ /dev/null @@ -1,157 +0,0 @@ - - - diff --git a/examples/playground/src/layouts/Base.astro b/examples/playground/src/layouts/Base.astro index 24c58a8..c6b34ae 100644 --- a/examples/playground/src/layouts/Base.astro +++ b/examples/playground/src/layouts/Base.astro @@ -5,12 +5,54 @@ import "@fontsource/ibm-plex-sans/500.css"; import "@fontsource/ibm-plex-mono/400.css"; import "@fontsource/ibm-plex-mono/500.css"; import "../styles.css"; -import Toolbar from "../components/Toolbar.astro"; +import SiteHeader from "../components/SiteHeader.astro"; const { title, description } = Astro.props as { title: string; description: string; }; + +const DOCS = "https://better-content-docs.vercel.app"; +const REPO = "https://github.com/Invalid8/better-content"; + +const footerColumns = [ + { + heading: "Learn", + links: [ + ["Quickstart", `${DOCS}/quickstart`], + ["Guides", `${DOCS}/guides`], + ["API reference", `${DOCS}/api`], + ["Migration", `${DOCS}/migration`], + ], + }, + { + heading: "Adapters", + links: [ + ["Postgres", `${DOCS}/adapters/postgres`], + ["Firestore", `${DOCS}/adapters/firestore`], + ["In-memory", `${DOCS}/adapters/memory`], + ["Write your own", `${DOCS}/adapters`], + ], + }, + { + heading: "Bindings", + links: [ + ["React", `${DOCS}/react`], + ["Vue", `${DOCS}/vue`], + ["Svelte", `${DOCS}/svelte`], + ["Core", `${DOCS}/core`], + ], + }, + { + heading: "Project", + links: [ + ["GitHub", REPO], + ["npm", "https://www.npmjs.com/package/better-content"], + ["Changelog", `${REPO}/blob/main/CHANGELOG.md`], + ["License", `${REPO}/blob/main/LICENSE`], + ], + }, +]; --- @@ -25,37 +67,42 @@ const { title, description } = Astro.props as { /> {title} - -
    - - -
    + + + +
    + + + +
    diff --git a/examples/playground/src/lib/db.ts b/examples/playground/src/lib/db.ts index 4cfd4c1..c6ba7cc 100644 --- a/examples/playground/src/lib/db.ts +++ b/examples/playground/src/lib/db.ts @@ -1,8 +1,7 @@ -import { PGlite } from "@electric-sql/pglite"; -import { drizzle } from "drizzle-orm/pglite"; -import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; -import { PostgresDataAdapter } from "better-content/adapters/postgres"; -import type { DataAdapter, Item, ItemMap, Query } from "better-content/core"; +import type { DataAdapter, Item, Query } from "better-content/core"; +import { seedItems } from "./seed"; + +export { seedItems }; export type AdapterId = "pglite" | "memory" | "rest"; @@ -31,92 +30,6 @@ export const adapterOptions: Array<{ }, ]; -// The playground schema: two tables, owned by the app, not by the library. -const page = pgTable("page", { - id: text("id").primaryKey(), - headline: text("headline"), - intro: text("intro"), - message: text("message"), - cover: text("cover"), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(), -}); - -const cards = pgTable("cards", { - id: text("id").primaryKey(), - title: text("title"), - body: text("body"), - order: integer("order"), - createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(), - updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(), -}); - -const schema = { page, cards }; - -const DDL = ` - CREATE TABLE IF NOT EXISTS page ( - id text PRIMARY KEY, - headline text, - intro text, - message text, - cover text, - created_at timestamptz DEFAULT now(), - updated_at timestamptz DEFAULT now() - ); - CREATE TABLE IF NOT EXISTS cards ( - id text PRIMARY KEY, - title text, - body text, - "order" integer, - created_at timestamptz DEFAULT now(), - updated_at timestamptz DEFAULT now() - ); -`; - -export const seedItems: ItemMap = { - page: [ - { - id: "hero", - headline: "This page is a database.", - intro: - "Every block of text you can read here is a row. Toggle Edit in the toolbar, click any highlighted text, change it, press Save, then swap adapters to see the same UI talk to another backend.", - cover: "/images/content-playground-hero.png", - }, - { - id: "shared", - message: - "One engine holds this sentence. React, Vue, and Svelte are three ways of looking at it. Edit me in any island, click away, and watch the other two follow.", - }, - ], - cards: [ - { - id: "cards-are-rows", - title: "Cards are rows", - body: "This card is a row in the cards table. Its title and body are columns you are reading through the adapter.", - order: 0, - }, - { - id: "ops-are-optimistic", - title: "Ops are optimistic", - body: "Create, reorder, and delete update the snapshot immediately and roll back if the adapter rejects the write.", - order: 1, - }, - { - id: "order-is-an-integer", - title: "Order is an integer", - body: "Reordering patches each row with a new order value through the transport. No hidden magic.", - order: 2, - }, - ], -}; - -const client = new PGlite("idb://better-content-playground"); - -const pgliteAdapter = new PostgresDataAdapter({ - db: drizzle(client, { schema }) as never, - schema: schema as never, -}); - function applyQuery(rows: Item[], query?: Query): Item[] { let next = [...rows]; for (const order of query?.orderBy ?? []) { @@ -283,7 +196,7 @@ function readAdapterId(): AdapterId { const hash = new URLSearchParams(window.location.hash.slice(1)); const candidate = (hash.get("adapter") as AdapterId | null) ?? - (localStorage.getItem("bc-playground-adapter") as AdapterId | null) ?? + (localStorage.getItem("bc-adapter") as AdapterId | null) ?? "memory"; return adapterOptions.some((option) => option.id === candidate) ? candidate @@ -292,47 +205,35 @@ function readAdapterId(): AdapterId { export const activeAdapterId = readAdapterId(); -const memoryAdapter = new MapDataAdapter(); -const restAdapter = new MockRestAdapter(); +let loaded: DataAdapter | null = null; -export const adapter: DataAdapter = - activeAdapterId === "memory" - ? memoryAdapter - : activeAdapterId === "rest" - ? restAdapter - : pgliteAdapter; +export async function loadAdapter(): Promise { + if (loaded) return loaded; -async function seedThrough(target: DataAdapter): Promise { - for (const [collection, rows] of Object.entries(seedItems)) { - for (const { id, ...fields } of rows) { - await target.createWithId(collection, id, fields); - } + if (activeAdapterId === "pglite") { + const pglite = await import("./pgliteAdapter"); + await pglite.init(); + loaded = pglite.adapter; + return loaded; } -} -export async function initDb(): Promise { - if (activeAdapterId !== "pglite") return; - await client.exec(DDL); - const existing = await client.query<{ n: number }>( - "SELECT count(*)::int AS n FROM page", - ); - if ((existing.rows[0]?.n ?? 0) === 0) { - await seedThrough(pgliteAdapter); - } + loaded = + activeAdapterId === "rest" ? new MockRestAdapter() : new MapDataAdapter(); + return loaded; } export async function resetDemo(): Promise { if (activeAdapterId === "pglite") { - await client.exec('DELETE FROM page; DELETE FROM cards;'); - await seedThrough(pgliteAdapter); - } else { - (adapter as MapDataAdapter).replace(seedItems); + const pglite = await import("./pgliteAdapter"); + await pglite.reset(); + } else if (loaded) { + (loaded as MapDataAdapter).replace(seedItems); } location.reload(); } export function setAdapter(id: AdapterId): void { - localStorage.setItem("bc-playground-adapter", id); + localStorage.setItem("bc-adapter", id); const params = new URLSearchParams(window.location.hash.slice(1)); params.set("adapter", id); window.location.hash = params.toString(); diff --git a/examples/playground/src/lib/editMode.ts b/examples/playground/src/lib/editMode.ts index d4a2f0c..afe13ba 100644 --- a/examples/playground/src/lib/editMode.ts +++ b/examples/playground/src/lib/editMode.ts @@ -1,4 +1,4 @@ -const KEY = "bc-playground-editing"; +const KEY = "bc-editing"; let editing = typeof sessionStorage !== "undefined" && sessionStorage.getItem(KEY) === "1"; diff --git a/examples/playground/src/lib/engine.ts b/examples/playground/src/lib/engine.ts index ee72623..5df81e9 100644 --- a/examples/playground/src/lib/engine.ts +++ b/examples/playground/src/lib/engine.ts @@ -5,7 +5,7 @@ import { type CmsEngine, } from "better-content/core"; import { loadItemMap } from "better-content/server"; -import { adapter, initDb } from "./db"; +import { loadAdapter } from "./db"; import { statusStore } from "./status"; // Uploaded images become data URLs so the whole demo stays inside your tab. @@ -24,7 +24,7 @@ const dataUrlStorage: ClientStorageAdapter = { // framework, binds to this exact object. The engine itself never // imports React, Vue, or Svelte. export const ready: Promise = (async () => { - await initDb(); + const adapter = await loadAdapter(); const initialItems = await loadItemMap(adapter, { page: {}, diff --git a/examples/playground/src/lib/pgliteAdapter.ts b/examples/playground/src/lib/pgliteAdapter.ts new file mode 100644 index 0000000..90e42e5 --- /dev/null +++ b/examples/playground/src/lib/pgliteAdapter.ts @@ -0,0 +1,79 @@ +import { PGlite } from "@electric-sql/pglite"; +import { drizzle } from "drizzle-orm/pglite"; +import { integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"; +import { PostgresDataAdapter } from "better-content/adapters/postgres"; +import type { DataAdapter, Item } from "better-content/core"; +import { seedItems } from "./seed"; + +// The playground schema: two tables, owned by the app, not by the library. +const page = pgTable("page", { + id: text("id").primaryKey(), + headline: text("headline"), + tagline: text("tagline"), + intro: text("intro"), + message: text("message"), + cover: text("cover"), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(), +}); + +const cards = pgTable("cards", { + id: text("id").primaryKey(), + title: text("title"), + body: text("body"), + order: integer("order"), + createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(), +}); + +const schema = { page, cards }; + +const DDL = ` + CREATE TABLE IF NOT EXISTS page ( + id text PRIMARY KEY, + headline text, + tagline text, + intro text, + message text, + cover text, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() + ); + CREATE TABLE IF NOT EXISTS cards ( + id text PRIMARY KEY, + title text, + body text, + "order" integer, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() + ); + ALTER TABLE page ADD COLUMN IF NOT EXISTS tagline text; +`; + +const client = new PGlite("idb://better-content"); + +export const adapter: DataAdapter = new PostgresDataAdapter({ + db: drizzle(client, { schema }) as never, + schema: schema as never, +}); + +async function seed(): Promise { + for (const [collection, rows] of Object.entries(seedItems)) { + for (const { id, ...fields } of rows as Item[]) { + await adapter.createWithId(collection, id, fields); + } + } +} + +export async function init(): Promise { + await client.exec(DDL); + const existing = await client.query<{ n: number }>( + "SELECT count(*)::int AS n FROM page", + ); + if ((existing.rows[0]?.n ?? 0) === 0) await seed(); +} + +export async function reset(): Promise { + await client.exec("DELETE FROM page; DELETE FROM cards;"); + await seed(); +} diff --git a/examples/playground/src/lib/seed.ts b/examples/playground/src/lib/seed.ts new file mode 100644 index 0000000..ecdbaf8 --- /dev/null +++ b/examples/playground/src/lib/seed.ts @@ -0,0 +1,39 @@ +import type { ItemMap } from "better-content/core"; + +export const seedItems: ItemMap = { + page: [ + { + id: "hero", + headline: "Edit this page.", + tagline: "Watch your database change.", + intro: + "better-content puts editing on the page itself, and writes every change to a database you own, through a seven-method adapter, from React, Vue or Svelte. The Postgres below is real, and it is running in this browser tab.", + cover: "/images/content-playground-hero.png", + }, + { + id: "shared", + message: + "One engine holds this sentence. React, Vue and Svelte are three ways of looking at it. Edit it in any island, click away, and watch the other two follow.", + }, + ], + cards: [ + { + id: "cards-are-rows", + title: "Cards are rows", + body: "This card is a row in the cards table. Its title and body are columns you are reading through the adapter.", + order: 0, + }, + { + id: "ops-are-optimistic", + title: "Ops are optimistic", + body: "Create, reorder, and delete update the snapshot immediately and roll back if the adapter rejects the write.", + order: 1, + }, + { + id: "order-is-an-integer", + title: "Order is an integer", + body: "Reordering patches each row with a new order value through the transport. No hidden magic.", + order: 2, + }, + ], +}; diff --git a/examples/playground/src/pages/index.astro b/examples/playground/src/pages/index.astro index fe22b9c..d7cd888 100644 --- a/examples/playground/src/pages/index.astro +++ b/examples/playground/src/pages/index.astro @@ -3,262 +3,285 @@ import Base from "../layouts/Base.astro"; import CodeTabs from "../components/CodeTabs.astro"; import DatabaseRows from "../components/DatabaseRows.astro"; import AdapterCalls from "../components/AdapterCalls.astro"; -import ReactPreviewFrame from "../components/react/PreviewFrame"; -import VuePreviewIsland from "../components/vue/PreviewIsland.vue"; -import SveltePreviewIsland from "../components/svelte/PreviewIsland.svelte"; -import { Check, Pencil } from "@lucide/astro"; -import { siReact, siSvelte, siVuedotjs } from "simple-icons"; +import HeroIsland from "../components/react/HeroIsland"; +import RowsIsland from "../components/react/RowsIsland"; +import ReactNoteIsland from "../components/react/NoteIsland"; +import VueNoteIsland from "../components/vue/NoteIsland.vue"; +import SvelteNoteIsland from "../components/svelte/NoteIsland.svelte"; +import { ArrowRight, Copy, Pencil } from "@lucide/astro"; import engineSource from "../lib/engine.ts?raw"; import pgliteAdapterSource from "../snippets/adapters/pglite.ts?raw"; import memoryAdapterSource from "../snippets/adapters/memory.ts?raw"; import restAdapterSource from "../snippets/adapters/rest.ts?raw"; -import reactPreviewSource from "../components/react/PreviewFrame.tsx?raw"; import reactNoteSource from "../components/react/NoteCard.tsx?raw"; -import vuePreviewSource from "../components/vue/PreviewFrame.vue?raw"; -import sveltePreviewSource from "../components/svelte/PreviewFrame.svelte?raw"; import vueNoteSource from "../components/vue/NoteCard.vue?raw"; import svelteNoteSource from "../components/svelte/NoteCard.svelte?raw"; + +const steps = [ + { + keys: ["⌘", "E"], + copy: "Turn on edit mode. Every field on this page becomes editable in place, headings and body copy included.", + }, + { + keys: [], + copy: "Click anything and type. No admin panel, no modal, no second URL to remember.", + }, + { + keys: ["⌘", "S"], + copy: "Save every dirty field in one round trip. Volatile adapters skip this and save on blur.", + }, + { + keys: [], + copy: "Open the database panel below. The row you just changed is sitting in Postgres.", + }, + { + keys: [], + copy: "Reload. It is still there, because PGlite persists to IndexedDB, in your tab, with no server.", + }, +]; + +const contract = [ + ["fetchCollection", "(collection, q?)", "read"], + ["fetchById", "(collection, id)", "read"], + ["create", "(collection, data)", "write"], + ["createWithId", "(collection, id, data)", "write"], + ["update", "(collection, id, patch)", "write"], + ["upsert", "(collection, id, patch)", "write"], + ["delete", "(collection, id)", "write"], +]; --- -
    -
    - - - -
    - -
    -
    - -
    - Adapter here is PGlite. - +
    +
    -
    -
    - preview - React · useSyncExternalStore -
    - - -
    -
    -
    -
    - +
    +
    +
    +

    01 — Try it

    +

    There is no demo mode. This is the app.

    +

    + Every keystroke you make on this page runs the same code path a + production app runs: engine, transport, adapter, your database. +

    +
    + +
      + { + steps.map((step) => ( +
    1. + {step.copy} + {step.keys.length > 0 && ( + + {step.keys.map((key) => ( + {key} + ))} + + )} +
    2. + )) + } +
    +
    +
    + +
    +
    +
    +
    +

    02 — Where it went

    +

    Your edit is a row. Here it is.

    +

    + Content is rows, not commits. That is the difference between this + and a git-backed CMS: it works for apps, not only static sites. +

    -
    - + +
    + +
    +
    + database +
    -
    - +
    + +
    -

    Click any text while Edit is on. The edit runs through the adapter for real.

    -
    +
    -
    - - +
    +
    +
    +

    03 — Beyond text

    +

    Insert a row. Drag it. Delete it. Drop in an image.

    +

    + The same engine that edits a field also creates, reorders and + deletes records. Every op updates the snapshot immediately and rolls + back if the adapter rejects the write. Turn on edit mode to get the + controls. +

    +
    + +
    -
    -
    -
    - -

    One engine, component systems stay optional.

    -

    - React, Vue, and Svelte are bindings around the same exposed core: - a snapshot store, a subscription method, and a seven-method adapter - contract. Any UI layer can wrap those primitives. +

    +
    +
    +

    04 — Bindings

    +

    Edit in React. Watch Vue and Svelte.

    +

    + These are three real islands on one page, each a different + framework, all subscribed to the same engine. Type into any of them + and the other two re-render before the save even lands.

    +
    + +
    + + +
    -
    -
    - - React - Inline editing through useSyncExternalStore. -
    -
    - - Vue - Composition-friendly access to the live snapshot. -
    -
    - - Svelte - Store-style subscription without a new backend. -
    -
    - - More - Wrap getSnapshot and subscribe for any framework. -
    + +
    + const engine = createCmsEngine({ transport: + adapterTransport(adapter) }) + framework-free, zero framework imports
    + +
    -
    -
    - +
    +
    +

    Take it home.

    +

    + Scaffold a Next.js, Nuxt, SvelteKit or Astro app wired to Postgres or + Firestore. MIT licensed, pre-1.0, no account required. +

    +
    + + + Read the docs + +
    +
    +
    + diff --git a/examples/playground/src/styles.css b/examples/playground/src/styles.css index 7d489a4..f50f255 100644 --- a/examples/playground/src/styles.css +++ b/examples/playground/src/styles.css @@ -1,63 +1,71 @@ :root { - --paper: #f3f0e8; - --panel: #fbfaf6; - --panel-soft: #f7f4ec; - --ink: #1f1f1c; - --muted: #68645d; - --faint: #a49d91; - --line: #ded8ca; - --line-strong: #cbc2b1; - --accent: #d4653c; - --accent-soft: #f2ddcf; - --green: #4a8a68; - --code-bg: #211f1c; - --code-bg-soft: #2b2824; - --code-ink: #eee9df; - --code-muted: #9f988d; - --error: #ad3d35; - --react: #d1653b; - --vue: #4a8a68; - --svelte: #a75a42; + --paper: #ffffff; + --panel: #faf9f6; + --panel-soft: #f4f4f1; + --ink: #20242b; + --muted: #71767f; + --faint: #a8acb3; + --line: #e7e8eb; + --line-strong: #cdd0d6; + --accent: #ffe24a; + --accent-soft: #fff4bf; + --mark-line: #e0c22a; + --green: #2f8f5b; + --green-soft: #e3f1e9; + --code-bg: #20242b; + --code-bg-soft: #2a2f37; + --code-ink: #faf9f6; + --code-muted: #8a9099; + --error: #b3261e; + --react: #087ea4; + --vue: #42b883; + --svelte: #ff3e00; --serif: "IBM Plex Serif", georgia, serif; --sans: "IBM Plex Sans", system-ui, sans-serif; --mono: "IBM Plex Mono", ui-monospace, monospace; + --measure: 65rem; + --gutter: 3rem; } [data-theme="dark"] { - --paper: #070707; - --panel: #111111; - --panel-soft: #171717; - --ink: #f4f1ea; - --muted: #beb7aa; - --faint: #80796f; - --line: #2a2a2a; - --line-strong: #424242; - --accent: #e2734f; - --accent-soft: #39241d; - --green: #75d39d; - --code-bg: #050505; - --code-bg-soft: #101010; - --code-ink: #f4f1ea; - --code-muted: #827b72; + --paper: #16181d; + --panel: #1c1f26; + --panel-soft: #232730; + --ink: #f2f3f5; + --muted: #9ba1aa; + --faint: #6c727c; + --line: #2b2f38; + --line-strong: #3c414c; + --accent-soft: #40391a; + --mark-line: #b99c1f; + --green: #5fcb8e; + --green-soft: #1e3328; + --code-bg: #0f1115; + --code-bg-soft: #171a20; + --code-ink: #f2f3f5; + --code-muted: #8a9099; + --error: #f2837b; } @media (prefers-color-scheme: dark) { [data-theme="system"] { - --paper: #070707; - --panel: #111111; - --panel-soft: #171717; - --ink: #f4f1ea; - --muted: #beb7aa; - --faint: #80796f; - --line: #2a2a2a; - --line-strong: #424242; - --accent: #e2734f; - --accent-soft: #39241d; - --green: #75d39d; - --code-bg: #050505; - --code-bg-soft: #101010; - --code-ink: #f4f1ea; - --code-muted: #827b72; + --paper: #16181d; + --panel: #1c1f26; + --panel-soft: #232730; + --ink: #f2f3f5; + --muted: #9ba1aa; + --faint: #6c727c; + --line: #2b2f38; + --line-strong: #3c414c; + --accent-soft: #40391a; + --mark-line: #b99c1f; + --green: #5fcb8e; + --green-soft: #1e3328; + --code-bg: #0f1115; + --code-bg-soft: #171a20; + --code-ink: #f2f3f5; + --code-muted: #8a9099; + --error: #f2837b; } } @@ -79,32 +87,42 @@ body { background: var(--paper); color: var(--ink); font-family: var(--sans); - font-size: 14px; - line-height: 1.48; + font-size: 16px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; } button, select, +input, textarea { font: inherit; + color: inherit; } img { display: block; max-width: 100%; + height: auto; } h1, h2, h3, -p { +p, +ol, +ul, +figure { margin: 0; } h1, h2, h3 { - line-height: 1.12; + font-family: var(--serif); + font-weight: 600; + letter-spacing: -0.02em; + line-height: 1.15; } a { @@ -113,787 +131,882 @@ a { code { font-family: var(--mono); - font-size: 0.9em; } -pre code { - font-size: inherit; +.sr-only { + position: absolute; + width: 1px; + height: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; + border: 0; } -.bar-label, -.section-label, -.crumb, -.toolbar-status, -.toolbar-note, -.panel-meta, -.hint, -.panel-note { - font-family: var(--mono); +.site-header { + position: sticky; + top: 0; + z-index: 30; + border-bottom: 1px solid var(--line); + background: color-mix(in srgb, var(--paper) 88%, transparent); + backdrop-filter: blur(10px); } -.bar-label, -.section-label { - color: var(--faint); - font-size: 0.7rem; - letter-spacing: 0.14em; - text-transform: uppercase; +.site-header__inner { + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + gap: 1.5rem; + max-width: 90rem; + margin: 0 auto; + padding: 1.15rem var(--gutter); } -.btn { +.site-brand { + justify-self: start; display: inline-flex; align-items: center; - justify-content: center; - min-height: 2rem; - gap: 0.4rem; - border: 1px solid var(--line-strong); - border-radius: 8px; - background: var(--panel); + gap: 0.6rem; color: var(--ink); - cursor: pointer; font-family: var(--mono); - font-size: 0.78rem; - padding: 0.42rem 0.78rem; + font-size: 0.94rem; + font-weight: 500; + letter-spacing: -0.01em; text-decoration: none; } -.btn:hover:not([disabled]) { - border-color: var(--accent); - color: var(--accent); +.site-brand span { + white-space: nowrap; } -.btn.primary, -.btn.editing { - background: var(--accent); - border-color: var(--accent); - color: #fffaf3; +.site-brand img { + width: 24px; + height: 24px; + border-radius: 5px; } -.btn.ghost { - background: transparent; +.site-nav { + justify-self: center; + display: flex; + align-items: center; + gap: 1.75rem; } -.btn[disabled] { - cursor: not-allowed; - opacity: 0.45; +.site-nav a { + color: var(--muted); + font-size: 0.875rem; + text-decoration: none; } -.dirty-dot { - width: 7px; - height: 7px; - border-radius: 999px; - background: currentColor; +.site-nav a:hover { + color: var(--ink); } -.toolbar { - position: sticky; - top: 0; - z-index: 50; - padding: 0; - border-bottom: 1px solid var(--line); - background: color-mix(in srgb, var(--paper) 92%, transparent); - backdrop-filter: blur(10px); +.site-actions { + justify-self: end; + display: inline-flex; + align-items: center; + gap: 0.6rem; } -.toolbar-inner { - width: 100%; - max-width: 88rem; - margin: 0 auto; - display: flex; +.button { + display: inline-flex; align-items: center; - gap: 1rem; - padding: 0.85rem clamp(1rem, 4vw, 2rem); + gap: 0.5rem; + border: 1px solid transparent; + border-radius: 8px; + padding: 0.55rem 0.85rem; + background: transparent; + color: var(--ink); + cursor: pointer; + font-family: var(--sans); + font-size: 0.875rem; + font-weight: 500; + line-height: 1; + text-decoration: none; } -.brand { - display: flex; +.button kbd { + display: inline-flex; align-items: center; - gap: 0.45rem; + gap: 0.06em; font-family: var(--mono); - font-weight: 500; - text-decoration: none; - white-space: nowrap; + font-size: 0.9em; + font-weight: 400; + opacity: 0.72; } -.brand img { - width: 18px; - height: 18px; +.kbd-mod { + font-size: 1.2em; + line-height: 1; } -.brand em { - color: var(--muted); - font-style: normal; - font-weight: 400; +.button--ink { + background: var(--ink); + color: var(--paper); } -.toolbar-note { - color: var(--faint); - font-size: 0.72rem; +.button--mark { + background: var(--accent); + border-color: var(--mark-line); + color: #20242b; } -.toolbar-status { - margin-left: auto; - max-width: 18rem; - overflow: hidden; +.button--quiet { + border-color: var(--line-strong); color: var(--muted); - font-size: 0.73rem; - text-align: right; - text-overflow: ellipsis; - white-space: nowrap; } -.toolbar-actions { - display: flex; - flex-shrink: 0; - gap: 0.45rem; +.button--quiet:hover:not([disabled]) { + color: var(--ink); } -.icon-btn { - position: relative; - width: 2.15rem; - min-width: 2.15rem; - padding: 0; +.button[disabled] { + cursor: not-allowed; + opacity: 0.45; } -.icon-btn svg { - width: 1rem; - height: 1rem; - fill: none; - stroke: currentColor; - stroke-linecap: round; - stroke-linejoin: round; - stroke-width: 1.8; +.icon-button { + display: inline-flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border: 1px solid var(--line); + border-radius: 8px; + background: transparent; + color: var(--muted); + cursor: pointer; } -.icon-btn svg.brand-icon { - fill: currentColor; - stroke: none; +.icon-button:hover { + color: var(--ink); + border-color: var(--line-strong); } -.icon-btn .dirty-dot { - position: absolute; - margin: -1.05rem 0 0 1.15rem; +.theme-icon--dark { + display: none; } -.toolbar-actions .icon-btn { - width: 2.35rem; - min-width: 2.35rem; - min-height: 2.35rem; +[data-theme="dark"] .theme-icon--light { + display: none; } -.toolbar-actions .icon-btn svg { - width: 1.12rem; - height: 1.12rem; +[data-theme="dark"] .theme-icon--dark { + display: inline; } -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - white-space: nowrap; - border: 0; +.when-editing { + display: none; } -.site-shell { - max-width: 88rem; +.editing-on .when-editing { + display: inline-flex; +} + +.editing-on .when-reading { + display: none; +} + +.shell { + max-width: 90rem; margin: 0 auto; - padding: 0 clamp(1rem, 4vw, 2rem); + padding-inline: var(--gutter); } -.lab { - display: flex; - flex-direction: column; - gap: 1.25rem; - padding: 1.45rem 0 1.5rem; +.chip { + display: inline-block; + margin: 0; + padding: 0.2rem 0.5rem; + border-radius: 4px; + background: var(--accent); + color: #20242b; + font-family: var(--mono); + font-size: 0.72rem; + letter-spacing: 0.09em; + text-transform: uppercase; } -.lab-header { - display: flex; +.command { + display: inline-flex; align-items: center; - justify-content: space-between; - gap: 2rem; - padding-bottom: 0.85rem; - border-bottom: 1px solid var(--line); + gap: 0.7rem; + border: 1px solid var(--line-strong); + border-radius: 10px; + padding: 0.7rem 1rem; + background: var(--panel); + color: var(--ink); + cursor: pointer; + font-family: var(--mono); + font-size: 0.9rem; } -.crumb { - color: var(--muted); - font-size: 0.88rem; +.command__prompt { + color: var(--faint); } -.crumb strong { - color: var(--ink); +.command--ink { + border-color: var(--ink); + background: var(--ink); + color: var(--paper); } -.lab-controls { - display: flex; - align-items: flex-end; - gap: 0.7rem; - align-self: flex-start; +.command--ink .command__prompt { + color: color-mix(in srgb, var(--paper) 45%, transparent); +} + +.hero { + padding: 5.5rem 0 4.5rem; +} + +.hero .chip { + margin-bottom: 1.75rem; +} + +.hero-title { + max-width: 54rem; + font-size: clamp(2.6rem, 5.6vw, 4.75rem); + line-height: 1.1; } -.lab-controls label { +.hero-line { + display: block; +} + +.hero-skeleton { display: flex; flex-direction: column; - gap: 0.3rem; - color: var(--faint); - font-family: var(--mono); - font-size: 0.68rem; - letter-spacing: 0.12em; - text-transform: uppercase; } -.lab-controls select, -.query-row select, -.query-row textarea { - appearance: none; - min-height: 2rem; - border: 1px solid var(--line-strong); - border-radius: 8px; - background: var(--panel); - background-image: - linear-gradient(45deg, transparent 50%, var(--muted) 50%), - linear-gradient(135deg, var(--muted) 50%, transparent 50%); - background-position: - calc(100% - 1rem) 52%, - calc(100% - 0.68rem) 52%; +.hero-skeleton span { + display: block; + border-radius: 6px; + background-color: color-mix(in srgb, var(--ink) 7%, transparent); + background-image: linear-gradient( + 90deg, + transparent 20%, + color-mix(in srgb, var(--ink) 7%, transparent) 50%, + transparent 80% + ); background-repeat: no-repeat; - background-size: 0.32rem 0.32rem; - color: var(--ink); - font-family: var(--mono); - font-size: 0.78rem; - letter-spacing: 0; - padding: 0.35rem 2.2rem 0.35rem 0.7rem; - text-transform: none; + background-size: 200% 100%; + animation: bc-shimmer 1.5s ease-in-out infinite; } -.reset-action { - min-height: 2rem; +.hero-skeleton__title { + max-width: 54rem; + height: 0.78em; + margin: 0.16em 0; + font-size: clamp(2.6rem, 5.6vw, 4.75rem); } -.flow { +.hero-skeleton__title:nth-of-type(2) { + max-width: 36rem; +} + +.hero-skeleton__deck { + max-width: 44rem; + height: 0.8rem; + margin: 0.53rem 0; +} + +.hero-skeleton__deck:nth-of-type(3) { + margin-top: 2.28rem; +} + +.hero-skeleton__deck:last-child { + max-width: 30rem; +} + +.hero-deck { + max-width: 44rem; + margin-top: 1.75rem; + color: var(--muted); + font-size: 1.2rem; + line-height: 1.55; +} + +.hero__actions { display: flex; - flex-direction: column; + flex-wrap: wrap; gap: 0.75rem; + margin-top: 2.25rem; } -.flow-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 1rem; +.hero__actions .button { + padding: 0.8rem 1.15rem; + font-size: 1rem; } -.flow-card { - position: relative; - display: flex; - min-height: 4.8rem; - flex-direction: column; - justify-content: center; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); - padding: 0.9rem; - text-decoration: none; +.section { + padding: 4rem 0 4.5rem; + border-top: 1px solid var(--line); } -.flow-card span { - color: var(--accent); - font-family: var(--mono); - font-size: 0.68rem; - font-weight: 500; +.section__head { + max-width: 65rem; } -.flow-card strong { - margin-top: 0.15rem; - font-weight: 600; +.section__head--split { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 3rem; + max-width: none; } -.flow-card small { - margin-top: 0.12rem; - color: var(--muted); - font-size: 0.82rem; +.section__head h2 { + margin-top: 0.9rem; + font-size: clamp(1.75rem, 3vw, 2.5rem); } -.hint { +.section__deck { + max-width: 42rem; + margin-top: 1rem; color: var(--muted); - font-size: 0.82rem; + font-size: 1.05rem; } -.lab-grid, -.data-grid { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 1.25rem; - align-items: stretch; +.steps { + max-width: 54rem; + margin-top: 2.5rem; + padding: 0; + list-style: none; +} + +.step { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 1.75rem; + padding: 1.1rem 0; + border-bottom: 1px solid var(--line); } -.lab-grid { - height: min(80vh, 46rem); - min-height: 34rem; +.step:first-child { + border-top: 1px solid var(--line); } -.source-stack { +.step__keys { display: flex; - min-width: 0; - min-height: 0; - flex-direction: column; - overflow: hidden; - border: 1px solid var(--code-bg); - border-radius: 8px; - background: var(--code-bg); + flex-shrink: 0; + gap: 0.35rem; } -.source-stack .code-tabs { - min-height: 0; - flex: 1; - border: 0; - border-radius: 0; -} - -.source-stack[data-active-framework="react"] - [data-code-framework]:not([data-code-framework="all"]):not( - [data-code-framework="react"] - ), -.source-stack[data-active-framework="vue"] - [data-code-framework]:not([data-code-framework="all"]):not( - [data-code-framework="vue"] - ), -.source-stack[data-active-framework="svelte"] - [data-code-framework]:not([data-code-framework="all"]):not( - [data-code-framework="svelte"] - ) { - display: none !important; +.step__keys kbd { + border: 1px solid var(--line-strong); + border-radius: 6px; + padding: 0.15rem 0.5rem; + background: var(--panel); + font-family: var(--mono); + font-size: 0.78rem; } -.source-stack[data-active-adapter="pglite"] - [data-code-adapter]:not([data-code-adapter="all"]):not( - [data-code-adapter="pglite"] - ), -.source-stack[data-active-adapter="memory"] - [data-code-adapter]:not([data-code-adapter="all"]):not( - [data-code-adapter="memory"] - ), -.source-stack[data-active-adapter="rest"] - [data-code-adapter]:not([data-code-adapter="all"]):not( - [data-code-adapter="rest"] - ) { - display: none !important; +.step__copy { + font-size: 1.05rem; +} + +.db-panel { + margin-top: 2.5rem; + border-radius: 14px; + background: var(--code-bg); + color: var(--code-ink); + overflow: hidden; } -.source-foot { +.db-panel__bar { display: flex; align-items: center; justify-content: space-between; gap: 1rem; - border-top: 1px solid color-mix(in srgb, var(--code-bg) 70%, #000 30%); - background: var(--code-bg-soft); - color: var(--code-muted); + padding: 0.8rem 1.1rem; + border-bottom: 1px solid rgb(255 255 255 / 0.08); +} + +.db-panel__title { font-family: var(--mono); - font-size: 0.76rem; - padding: 0.7rem 1rem; + font-size: 0.85rem; } -.source-foot strong { - color: #86caa6; +.adapter-switch { + display: inline-flex; + align-items: center; + gap: 0.6rem; + color: var(--code-muted); + font-family: var(--mono); + font-size: 0.78rem; } -.compact-copy { - border-color: rgba(255, 255, 255, 0.16); +.adapter-switch select { + border: 1px solid rgb(255 255 255 / 0.14); + appearance: none; + -webkit-appearance: none; + border-radius: 8px; + padding: 0.35rem 2.1rem 0.35rem 0.7rem; + background-color: rgb(0 0 0 / 0.35); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238a9099' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.75rem center; + background-size: 12px; color: var(--code-ink); + font-family: var(--mono); + font-size: 0.78rem; + cursor: pointer; +} + +.db-panel__body { + display: grid; + grid-template-columns: minmax(0, 1fr) 26rem; + min-height: 20rem; +} + +.database-panel, +.calls-panel { + min-width: 0; + padding-bottom: 1rem; +} + +.calls-panel { + border-left: 1px solid rgb(255 255 255 / 0.08); } .panel-bar { display: flex; align-items: center; - gap: 0.65rem; - min-height: 2.6rem; - border-bottom: 1px solid var(--line); - padding: 0.55rem 0.9rem; + gap: 0.75rem; + padding: 0.85rem 1.25rem 0.6rem; +} + +.bar-label { + color: var(--code-muted); + font-family: var(--mono); + font-size: 0.7rem; + letter-spacing: 0.08em; + text-transform: uppercase; } -.panel-meta { +.row-pill { margin-left: auto; - color: var(--muted); - font-size: 0.74rem; + color: var(--code-muted); + font-family: var(--mono); + font-size: 0.72rem; } -.preview-actions { - display: flex; - gap: 0.4rem; - margin-left: 0.25rem; +.database-row { + display: grid; + grid-template-columns: 12rem minmax(0, 1fr); + gap: 1rem; + padding: 0.6rem 1.25rem; + font-family: var(--mono); + font-size: 0.8rem; } -.preview-actions .icon-btn { - width: 2rem; - min-width: 2rem; - min-height: 2rem; +.database-row code { + color: var(--accent); +} + +.database-row span { + color: var(--code-ink); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.database-row__value[data-placeholder] { + display: block; + width: 68%; + height: 0.85em; + border-radius: 3px; + background-color: rgb(255 255 255 / 0.07); + background-image: linear-gradient( + 90deg, + transparent 20%, + rgb(255 255 255 / 0.09) 50%, + transparent 80% + ); + background-repeat: no-repeat; + background-size: 200% 100%; + animation: bc-shimmer 1.5s ease-in-out infinite; +} + +.database-row:nth-child(even) .database-row__value[data-placeholder] { + width: 44%; +} + + +.live-dot { + margin-left: auto; + color: var(--green); + font-family: var(--mono); + font-size: 0.72rem; } -.panel-meta i, .live-dot::before { display: inline-block; width: 7px; height: 7px; margin-right: 0.38rem; border-radius: 999px; - background: var(--accent); + background: var(--green); content: ""; } -.preview-panel, -.database-panel, -.calls-panel { +.call-log { + margin: 0; + padding: 0; + list-style: none; +} + +.call-log li { + display: flex; + gap: 0.85rem; + padding: 0.5rem 1.25rem; + font-family: var(--mono); + font-size: 0.76rem; +} + +.call-log time { + color: rgb(255 255 255 / 0.32); +} + +.call-log code { + color: var(--code-ink); overflow: hidden; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); + text-overflow: ellipsis; + white-space: nowrap; +} + +.rows-board { + min-height: 26rem; + margin-top: 2.5rem; +} + +.rows-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1.25rem; + align-items: start; } -.preview-panel { +.row-card { display: flex; - min-height: 0; flex-direction: column; + gap: 0.55rem; + border: 1px solid var(--line); + border-radius: 12px; + padding: 1.15rem; + background: var(--panel); + text-align: left; } -.preview-body { - flex: 1; - min-height: 0; - overflow: auto; - padding: 1.45rem 1.55rem 1rem; +.row-card[data-dragging="true"] { + opacity: 0.45; } -.preview-island { - display: none; +.row-card__id { + overflow-wrap: anywhere; + color: var(--faint); + font-family: var(--mono); + font-size: 0.7rem; + letter-spacing: 0.05em; } -.preview-panel[data-active-framework="react"] - [data-preview-framework="react"], -.preview-panel[data-active-framework="vue"] - [data-preview-framework="vue"], -.preview-panel[data-active-framework="svelte"] - [data-preview-framework="svelte"] { - display: block; +.row-card h3 { + font-size: 1.2rem; } -.panel-note { - border-top: 1px solid var(--line); +.row-card p { color: var(--muted); - font-size: 0.76rem; - padding: 0.75rem 1rem; + font-size: 0.95rem; } -.hero-live { - display: block; +.row-card__ops { + display: flex; + gap: 0.5rem; + margin-top: 0.35rem; } -.hero-copy { - display: flex; - flex-direction: column; - gap: 0.55rem; - max-width: 32rem; +.drag-handle, +.row-card__remove { + border: 1px solid var(--line-strong); + border-radius: 6px; + padding: 0.2rem 0.55rem; + background: transparent; + color: var(--muted); + cursor: grab; + font-family: var(--mono); + font-size: 0.72rem; } -.hero-headline { - display: block; - font-family: var(--serif); - font-size: clamp(2rem, 4vw, 2.8rem); - font-weight: 600; - letter-spacing: 0; +.row-card__remove { + cursor: pointer; } -.hero-intro { - display: block; +.row-card__remove:hover { + border-color: var(--error); + color: var(--error); +} + +.row-card--add { + align-items: center; + justify-content: center; + min-height: 8rem; + border-style: dashed; + border-color: var(--mark-line); + background: transparent; color: var(--muted); - font-size: 0.98rem; + cursor: pointer; + font-family: var(--mono); + font-size: 0.82rem; } -.hero-cover { - position: relative; - margin: 1rem 0 0; - overflow: hidden; +.rows-cover { + margin-top: 1.75rem; border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel-soft); + border-radius: 12px; + overflow: hidden; + background: var(--panel); +} + +.rows-cover[data-editing] { + border-color: var(--mark-line); + cursor: pointer; } -.hero-cover img, -.cover-empty { - aspect-ratio: 16 / 5; +.rows-cover img { width: 100%; + max-height: 22rem; object-fit: cover; } -.cover-empty { +.rows-cover__empty { display: grid; place-items: center; - color: var(--muted); + min-height: 12rem; + color: var(--faint); font-family: var(--mono); + font-size: 0.8rem; } -.hero-cover[data-editing] { - cursor: pointer; - outline: 1px dashed var(--accent); - outline-offset: -0.35rem; -} - -.hero-cover figcaption { - position: absolute; - right: 0.7rem; - bottom: 0.65rem; - border: 1px solid color-mix(in srgb, var(--line-strong) 80%, transparent); - border-radius: 999px; - background: color-mix(in srgb, var(--panel) 92%, transparent); +.rows-cover figcaption { + padding: 0.7rem 1rem; + border-top: 1px solid var(--line); color: var(--muted); font-family: var(--mono); - font-size: 0.68rem; - padding: 0.24rem 0.5rem; + font-size: 0.75rem; } -.cards-board { - margin-top: 1rem; +.islands { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1.25rem; + min-height: 12rem; + margin-top: 2.5rem; + align-items: start; } -.cards-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.8rem; +.island { + border: 1px solid var(--line); + border-radius: 12px; + background: var(--panel); + overflow: hidden; } -.board-card { +.island__bar { display: flex; - min-height: 7.4rem; - flex-direction: column; - gap: 0.45rem; - border: 1px solid var(--line); - border-radius: 8px; + align-items: center; + gap: 0.55rem; + padding: 0.65rem 0.9rem; + border-bottom: 1px solid var(--line); background: var(--panel-soft); - padding: 0.95rem; } -.board-card[data-dragging="true"] { - opacity: 0.58; +.island__dot { + width: 9px; + height: 9px; + border-radius: 999px; + background: var(--muted); } -.board-card h3 { - font-family: var(--serif); - font-size: 1rem; +[data-framework="react"] .island__dot { + background: var(--react); } -.board-card p { - color: var(--muted); - font-size: 0.84rem; +[data-framework="vue"] .island__dot { + background: var(--vue); } -.preview-panel .board-hint { - display: none; +[data-framework="svelte"] .island__dot { + background: var(--svelte); } -.card-ops { - display: flex; - align-items: center; - gap: 0.35rem; - margin-top: auto; - padding-top: 0.55rem; - border-top: 1px solid var(--line); +.island__name { + font-size: 0.88rem; + font-weight: 500; } -.card-ops button, -.add-card { - border: 1px solid var(--line-strong); - border-radius: 7px; - background: transparent; - color: var(--ink); - cursor: pointer; - font-family: var(--mono); +.island__hook { + flex: 1; + color: var(--faint); font-size: 0.72rem; - padding: 0.24rem 0.5rem; -} - -.card-ops button:hover:not([disabled]), -.add-card:hover { - border-color: var(--accent); - color: var(--accent); } -.card-ops button[disabled] { - cursor: not-allowed; - opacity: 0.38; -} - -.drag-handle { - display: inline-flex; - align-items: center; - min-height: 1.7rem; - border: 1px solid var(--line-strong); - border-radius: 7px; - color: var(--muted); - cursor: grab; +.island__state { + color: var(--green); font-family: var(--mono); - font-size: 0.72rem; - padding: 0.22rem 0.5rem; - user-select: none; + font-size: 0.7rem; } -.drag-handle:hover { - border-color: var(--accent); - color: var(--accent); +.island__state[data-dirty] { + color: var(--ink); } -.drag-handle:active { - cursor: grabbing; +.island__body { + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 1.1rem; } -.card-ops .remove { - margin-left: auto; - color: var(--error); +.island__path { + color: var(--faint); + font-family: var(--mono); + font-size: 0.7rem; + letter-spacing: 0.05em; } -.add-card { - min-height: 7.4rem; - border-style: dashed; +.island__text { + color: var(--ink); + font-size: 0.98rem; } -.board-hint { - margin-top: 0.7rem; +.island-loading { color: var(--muted); font-family: var(--mono); - font-size: 0.75rem; + font-size: 0.8rem; } -.database-panel, -.calls-panel { - min-height: 14.5rem; +.engine-bar { display: flex; - flex-direction: column; + flex-wrap: wrap; + align-items: center; + justify-content: center; + gap: 0.9rem; + margin-top: 1.5rem; + border: 1px solid var(--line-strong); + border-radius: 12px; + padding: 1rem 1.25rem; + background: var(--panel-soft); } -.database-table { - display: grid; +.engine-bar code { + font-size: 0.88rem; } -.database-row { - display: grid; - grid-template-columns: 9rem minmax(0, 1fr); - gap: 1.1rem; - border-top: 1px solid var(--line); - padding: 0.72rem 0.95rem; +.engine-bar span { + color: var(--faint); + font-size: 0.88rem; } -.database-row:first-child { - border-top: 0; +.adapters { + display: grid; + grid-template-columns: 31rem minmax(0, 1fr); + gap: 1.75rem; + margin-top: 2.5rem; + align-items: start; } -.database-row code { - color: var(--accent); - font-size: 0.76rem; +.contract { + border: 1px solid var(--line); + border-radius: 12px; + background: var(--panel); + overflow: hidden; } -.database-row span { - min-width: 0; - color: var(--ink); +.contract__head { + padding: 0.8rem 1.1rem; + border-bottom: 1px solid var(--line); + background: var(--panel-soft); font-family: var(--mono); - font-size: 0.78rem; - overflow-wrap: anywhere; + font-size: 0.85rem; } -.row-pill { - margin-left: auto; - color: var(--muted); - font-family: var(--mono); - font-size: 0.72rem; +.contract__row { + display: flex; + align-items: center; + gap: 0.65rem; + padding: 0.7rem 1.1rem; + border-bottom: 1px solid var(--line); } -.offline-toggle { - margin-left: auto; - color: var(--muted); - font-family: var(--mono); - font-size: 0.72rem; +.contract__row code { + font-size: 0.85rem; + font-weight: 500; } -.offline-toggle input { - accent-color: var(--accent); - vertical-align: middle; +.contract__sig { + flex: 1; + color: var(--faint); + font-family: var(--mono); + font-size: 0.76rem; } -.live-dot { - color: var(--green); +.contract__kind { + border-radius: 999px; + padding: 0.15rem 0.5rem; font-family: var(--mono); - font-size: 0.72rem; + font-size: 0.68rem; } -.live-dot::before { - background: var(--green); +.contract__kind[data-kind="read"] { + background: var(--green-soft); + color: var(--green); } -.call-log { - display: flex; - min-height: 0; - flex: 1; - flex-direction: column; - gap: 0.35rem; - margin: 0; - background: var(--code-bg); - color: var(--code-ink); - font-family: var(--mono); - list-style: none; - padding: 0.9rem; +.contract__kind[data-kind="write"] { + background: var(--accent-soft); + color: var(--ink); } -.call-log li { - display: flex; - gap: 0.75rem; +.contract__foot { + padding: 0.9rem 1.1rem; + color: var(--muted); + font-size: 0.88rem; } -.call-log time { - color: var(--code-muted); - flex: 0 0 4.4rem; - font-size: 0.75rem; +.code-tabs { + border-radius: 12px; + background: var(--code-bg); + overflow: hidden; } -.call-log code { - color: #87c79e; - font-size: 0.76rem; +.section .code-tabs { + margin-top: 1.5rem; } -.code-tabs { - min-width: 0; - overflow: hidden; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--code-bg); - display: flex; - flex-direction: column; +.adapters .code-tabs { + margin-top: 0; } .code-tabs-bar { display: flex; - align-items: stretch; - overflow-x: auto; - border-bottom: 1px solid color-mix(in srgb, var(--code-bg) 70%, #000 30%); - background: var(--code-bg); + align-items: center; + gap: 0.25rem; + padding: 0.6rem 0.7rem; + border-bottom: 1px solid rgb(255 255 255 / 0.08); } .code-tab { - display: flex; - align-items: center; - gap: 0.45rem; border: 0; - border-right: 1px solid color-mix(in srgb, var(--code-bg) 70%, #000 30%); + border-radius: 7px; + padding: 0.4rem 0.7rem; background: transparent; color: var(--code-muted); cursor: pointer; font-family: var(--mono); - font-size: 0.74rem; - padding: 0.68rem 0.9rem; - white-space: nowrap; -} - -.code-tab span { - color: var(--code-muted); - font-size: 0.65rem; + font-size: 0.76rem; } .code-tab[aria-selected="true"] { @@ -901,333 +1014,241 @@ pre code { color: var(--code-ink); } +.code-tab span { + margin-left: 0.4rem; + opacity: 0.6; +} + .code-copy { margin-left: auto; - border: 0; + border: 1px solid rgb(255 255 255 / 0.14); + border-radius: 7px; + padding: 0.32rem 0.6rem; background: transparent; color: var(--code-muted); cursor: pointer; font-family: var(--mono); font-size: 0.72rem; - padding: 0.68rem 0.9rem; -} - -.code-copy:hover { - color: var(--code-ink); } .code-view { - flex: 1; - min-height: 0; - max-height: none; + max-height: 27rem; margin: 0; + padding: 1.1rem 1.25rem; overflow: auto; color: var(--code-ink); font-family: var(--mono); - font-size: 0.75rem; - line-height: 1.55; - padding: 1rem 1.15rem; - tab-size: 2; - white-space: pre; - word-break: normal; + font-size: 0.8rem; + line-height: 1.75; } -.tok-keyword { - color: #ff9b72; +.tok-comment { + color: #6e757f; } .tok-string { - color: #9bdcfe; + color: #9cc5a1; } -.tok-comment { - color: #7f8c9d; +.tok-keyword { + color: #e0c22a; } .tok-type { - color: #c4b5fd; + color: #8fb8d8; } .tok-number { - color: #f8d776; + color: #d9a05b; } -.bindings { - display: flex; - flex-direction: column; - gap: 1rem; - margin-top: 2.35rem; - padding-top: 1.45rem; +.closing { + padding: 6rem 0; border-top: 1px solid var(--line); + background: var(--panel-soft); } -.bindings-head { - display: grid; - grid-template-columns: minmax(15rem, 0.8fr) minmax(0, 1.2fr); - gap: 1.25rem; - align-items: start; -} - -.bindings h2 { - margin-top: 0.45rem; - font-family: var(--serif); - font-size: 1.45rem; +.closing h2 { + font-size: clamp(2.25rem, 4.5vw, 3.75rem); } -.bindings-copy { - max-width: 32rem; - margin-top: 0.7rem; +.closing p { + max-width: 40rem; + margin-top: 1.25rem; color: var(--muted); + font-size: 1.15rem; } -.bindings-head .code-tabs { - min-height: 18rem; - max-height: none; +.closing__actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.75rem; + margin-top: 2rem; } -.bindings-head .code-view { - min-height: 13rem; - max-height: none; +.closing__actions .button { + padding: 0.8rem 1.15rem; + font-size: 1rem; } -.framework-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.9rem; +.site-footer { + border-top: 1px solid var(--line); + background: var(--paper); } -.framework-card { +.site-footer__inner { display: grid; - grid-template-columns: 2.6rem minmax(0, 1fr); - gap: 0.25rem 0.8rem; - align-content: center; - align-items: start; - min-height: 7.25rem; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); - padding: 0.9rem; -} - -.framework-card svg { - grid-row: 1 / span 2; - align-self: center; - width: 2.35rem; - height: 2.35rem; - fill: none; - stroke: currentColor; - stroke-linecap: round; - stroke-linejoin: round; - stroke-width: 1.5; -} - -.framework-card svg.brand-icon { - fill: currentColor; - stroke: none; -} - -.framework-card strong { - align-self: end; - font-family: var(--mono); - font-size: 0.84rem; + grid-template-columns: 1.4fr repeat(4, minmax(0, 1fr)); + gap: 2rem; + max-width: 90rem; + margin: 0 auto; + padding: 3rem var(--gutter) 2.5rem; } -.framework-card span { - align-self: start; +.site-footer__brand p { + margin: 0.8rem 0 0; + max-width: 16rem; color: var(--muted); - font-size: 0.82rem; -} - -.react-card { - color: #61dafb; -} - -.vue-card { - color: #42b883; -} - -.svelte-card { - color: #ff7a45; -} - -.more-card { - color: var(--accent); -} - -.note-grid { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 1rem; + font-size: 0.875rem; + line-height: 1.6; } -.note-card { +.site-footer__column { display: flex; - min-height: 11rem; flex-direction: column; - overflow: hidden; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); -} - -.note-card[data-framework="react"] { - border-top: 3px solid var(--react); -} - -.note-card[data-framework="vue"] { - border-top: 3px solid var(--vue); -} - -.note-card[data-framework="svelte"] { - border-top: 3px solid var(--svelte); -} - -.note-card header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.6rem; - border-bottom: 1px solid var(--line); - padding: 0.7rem 0.85rem; + gap: 0.75rem; } -.note-card h3 { +.site-footer__heading { + margin: 0; + color: var(--faint); font-family: var(--mono); - font-size: 0.82rem; - font-weight: 500; -} - -.note-card header code { - color: var(--muted); - font-size: 0.66rem; -} - -.note-text { - display: block; - flex: 1; - color: var(--muted); - font-size: 0.9rem; - padding: 0.9rem; + font-size: 0.69rem; + letter-spacing: 0.08em; + text-transform: uppercase; } -.note-card footer { - border-top: 1px solid var(--line); +.site-footer__column a { color: var(--muted); - font-family: var(--mono); - font-size: 0.7rem; - padding: 0.55rem 0.85rem; + font-size: 0.875rem; + text-decoration: none; } -.note-card footer strong { +.site-footer__column a:hover { color: var(--ink); } -.footer { +.site-footer__colophon { display: flex; flex-wrap: wrap; - gap: 1.1rem; - padding: 1.5rem 0 2rem; + justify-content: space-between; + gap: 1rem; + max-width: 90rem; + margin: 0 auto; + padding: 1.25rem var(--gutter) 2.75rem; border-top: 1px solid var(--line); - color: var(--muted); - font-family: var(--mono); - font-size: 0.86rem; + color: var(--faint); + font-size: 0.82rem; } -.footer a:hover { - color: var(--accent); +@keyframes bc-shimmer { + from { + background-position: 150% 0; + } + + to { + background-position: -50% 0; + } +} + +@media (prefers-reduced-motion: reduce) { + .hero-skeleton span, + .database-row__value[data-placeholder] { + animation: none; + } } [data-cms-editing] { border-radius: 3px; - background: color-mix(in srgb, var(--accent-soft) 72%, transparent); cursor: text; outline: 1px solid transparent; outline-offset: 2px; } [data-cms-editing]:hover { - background: var(--accent-soft); - outline-color: color-mix(in srgb, var(--accent) 42%, transparent); + background: color-mix(in srgb, var(--accent-soft) 60%, transparent); } -[data-cms-focused] { - background: var(--panel); - outline: 2px solid var(--accent); - outline-offset: 3px; +[data-cms-focused], +[data-cms-focused]:hover { + background: var(--accent-soft); + outline: 1px solid var(--mark-line); + outline-offset: 2px; } -.island-loading { - color: var(--muted); - font-family: var(--mono); - font-size: 0.8rem; +[data-cms-editing]::selection, +[data-cms-editing] ::selection { + background: var(--accent); + color: #20242b; } -.state-panel, -.adapter-lab, -.query-console, -.adapter-picker { - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); -} +@media (max-width: 48rem) { + .site-actions .button { + padding: 0.55rem 0.62rem; + } -@media (max-width: 68rem) { - .lab-header, - .bindings-head, - .lab-grid, - .data-grid { - grid-template-columns: 1fr; + .site-actions .button__label, + .site-actions .button kbd { + display: none; } +} - .lab-header { - display: grid; - align-items: start; +@media (max-width: 68rem) { + :root { + --gutter: 1.5rem; } - .lab-grid { - height: auto; - min-height: 0; + .site-nav { + display: none; } - .flow-grid, - .note-grid, - .framework-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); + .site-header__inner { + grid-template-columns: 1fr auto; } - .flow-card::after { - display: none; + .db-panel__body, + .adapters { + grid-template-columns: minmax(0, 1fr); } -} -@media (max-width: 48rem) { - .toolbar { - flex-wrap: wrap; + .calls-panel { + border-left: 0; + border-top: 1px solid rgb(255 255 255 / 0.08); } - .toolbar-inner { - flex-wrap: wrap; + .islands, + .rows-grid { + grid-template-columns: minmax(0, 1fr); } - .toolbar-note, - .toolbar-status { - display: none; + .site-footer__inner { + grid-template-columns: repeat(2, minmax(0, 1fr)); } - .toolbar-actions, - .lab-controls { - flex-wrap: wrap; + .site-footer__brand { + grid-column: 1 / -1; + margin-bottom: 1rem; } - .flow-grid, - .note-grid, - .framework-grid, - .cards-grid { - grid-template-columns: 1fr; + .section__head--split { + flex-direction: column; + align-items: flex-start; + gap: 1.5rem; } - .database-row { - grid-template-columns: 1fr; - gap: 0.25rem; + .step { + flex-direction: column; + gap: 0.75rem; } }