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; } }