From ea19497833600a43f433b4c8b5ae9eb855798bb0 Mon Sep 17 00:00:00 2001 From: Minseok Kim Date: Thu, 23 Jul 2026 00:29:27 +0900 Subject: [PATCH 01/27] feat(website): redesign landing and reskin docs (Iris/Cloud) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recreate the Iris/Cloud design handoff in the live Astro + Starlight site. Design system only — all copy follows the current repo. - Landing: standalone src/pages/index.astro with a guided, keyboard-free live console and write/read architecture paths converging on a shared storage backend. Live GitHub star count; theme synced via the starlight-theme key. Replaces the old Starlight splash index.mdx. - Docs reskin (structure unchanged): Starlight CSS custom-property overrides for Iris/Cloud, Space Grotesk / Manrope / JetBrains Mono fonts, a dark Expressive Code theme, and consistent header/footer/theme-toggle chrome. - HANDOFF.md (temporary) with decisions and remaining work. Co-Authored-By: Claude Opus 4.8 (1M context) --- HANDOFF.md | 62 ++ website/astro.config.mjs | 37 +- website/src/assets/landing.css | 176 +++- website/src/components/Footer.astro | 34 + website/src/components/Head.astro | 35 +- website/src/components/ThemeSelect.astro | 51 + website/src/content/docs/index.mdx | 43 - website/src/pages/index.astro | 1193 ++++++++++++++++++++++ 8 files changed, 1544 insertions(+), 87 deletions(-) create mode 100644 HANDOFF.md create mode 100644 website/src/components/Footer.astro create mode 100644 website/src/components/ThemeSelect.astro delete mode 100644 website/src/content/docs/index.mdx create mode 100644 website/src/pages/index.astro diff --git a/HANDOFF.md b/HANDOFF.md new file mode 100644 index 000000000..142ccc6df --- /dev/null +++ b/HANDOFF.md @@ -0,0 +1,62 @@ +# Handoff — Site redesign (landing + docs reskin) + +> **Temporary.** This file exists to bootstrap a fresh working session. **Remove it before merge** — it is not site content. + +## Goal + +Recreate the "Actionbase landing page redesign" design handoff (an Iris/Cloud design system delivered as HTML prototypes + a README, in a zip) inside the real Astro + Starlight site. Take the **design system only**; all textual content follows the **current repo**, never the prototype's copy. + +## Locked decisions + +- **Palette:** Iris `#5D5FEF` accent + Cloud background gradient (light) / dark gradient (dark). The prototype's palette/personalization bar is dropped. +- **Landing** is a standalone `website/src/pages/index.astro` with its own chrome, replacing the old Starlight splash `website/src/content/docs/index.mdx` (removed). Its theme syncs with Starlight through the shared `starlight-theme` localStorage key + ``. +- **Docs** are reskinned, not rewritten: Starlight structure/nav/search/TOC kept; `website/src/assets/landing.css` overrides Starlight CSS custom properties (Iris/Cloud), fonts are added via `astro.config.mjs` `head`, code blocks are themed via Expressive Code (single dark theme in both site themes). +- **Content is current-repo:** hero = follows / likes / related items (U2U/U2I/I2I); no Blog links; live GitHub star badge (not a hardcoded count); storage = HBase today + SlateDB planned (per `design/storage-backends` and the SlateDB tracking issue); the architecture write/read paths mirror `design/mutation` (WAL → storage → CDC, Kafka-backed) and `design/query` (direct lookups), both converging on one shared Storage Backend. + +## Done + +**Landing** (`website/src/pages/index.astro`): + +- Sections: nav (brand SVG logo, live GitHub star badge, theme toggle) · hero (copy + live console) · trust band · The engine (6 toned-down cards) · Architecture (write path left / read path right → one wide shared Storage Backend) · Quick start (3 dark code blocks) · CTA · footer. +- Console is a guided **sequential** demo: the 3 preset buttons (`get` → `scan` → `count`) unlock one at a time; a Reset button restores step one. Keyboard input is disabled (static prompt + blinking cursor + "run a step below" hint). No scrollbar — content is bottom-anchored (the banner clips off the top as lines accumulate). +- Removed during review: marginalia strip, "DB" chip, hero eyebrow, "Live console / Precomputed reads" labels, feature chips, the "HBase-backed" card (→ "Pluggable Storage"), "Pangyo" and "Docs" from the footer, the architecture footnote. + +**Docs reskin** (`website/src/assets/landing.css`, `website/astro.config.mjs`): + +- Starlight token overrides for Iris/Cloud in both themes; fonts (Space Grotesk headings, Manrope body, JetBrains Mono mono) via head links; page background gradient; translucent blurred header. +- Signature components: sidebar mono-uppercase group labels + accent active pill with a left bar; minimal callouts (mono label + bottom hairline, no icon/box); code blocks forced onto the dark terminal surface (`#1b1c2b`) in both themes; TOC accent-active; prev/next cards; light-surface search button. + +## Verified + +`cd website && npm run build` passes (41 pages, internal links valid). Screenshots captured for the landing (light/dark + console sequence) and docs quick-start (light/dark). + +## Preview / verify + +```bash +cd website +npm run build && npm run preview # serves dist (first free port from 4321) +``` + +Visual capture during dev used Playwright: put a small script **inside `website/`** so node resolves `website/node_modules`, seed `localStorage['starlight-theme']` before `goto`, then `screenshot({ fullPage: true })`. Two contexts (light/dark). The script was throwaway and is not committed. + +## Remaining / review before merge + +- **Delete this HANDOFF.md.** +- Fonts load from the Google Fonts CDN — consider self-hosting (the design README suggested it). +- Expressive Code uses a single dark theme (`github-dark`) for both site themes to match the always-dark mock code; syntax colors are github-dark's, not the mock's exact palette. +- Sweep the remaining docs surfaces under the new theme: asides of every type (note/tip/caution/danger), tables, `
`, images, and API-reference pages; mermaid is still `theme: 'forest'` + autoTheme — consider aligning to Iris. +- Mobile/responsive: Starlight drawer nav on docs; landing narrow breakpoints. +- Accessibility: muted-text contrast over the gradient, focus-visible states. +- Confirm the retained claims ("a million requests per minute", "in production at Kakao") are still acceptable marketing copy. + +## Key files + +- `website/src/pages/index.astro` — standalone landing (markup + scoped styles + console/theme scripts). +- `website/src/assets/landing.css` — docs theme (Starlight token overrides + component rules + retained utilities). +- `website/astro.config.mjs` — fonts in `head`, Expressive Code dark-code config. +- `website/src/content/docs/index.mdx` — **removed** (old splash; `/` is now the standalone page). +- Design source of truth: the handoff zip (`Actionbase Pangyo.dc.html` = landing, `Actionbase Docs.dc.html` = docs, `README.md` = tokens/spec, `screenshots/`). + +## Language + +`kakao/actionbase` is non-fork: all artifacts (code, comments, commits, PR, this file) are **English**; only live conversation is Korean. diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 4ebca1324..c13ee8ce0 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -87,6 +87,21 @@ export default defineConfig({ }, social: [], head: [ + { + tag: 'link', + attrs: { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, + }, + { + tag: 'link', + attrs: { rel: 'preconnect', href: 'https://fonts.gstatic.com', crossorigin: true }, + }, + { + tag: 'link', + attrs: { + rel: 'stylesheet', + href: 'https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Manrope:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap', + }, + }, { tag: 'script', attrs: { async: true, src: `https://www.googletagmanager.com/gtag/js?id=${GA_ID}` }, @@ -115,7 +130,6 @@ export default defineConfig({ { label: 'Nav', items: [ - { label: 'Docs', slug: 'introduction' }, { label: 'GitHub', link: 'https://github.com/kakao/actionbase', @@ -175,8 +189,27 @@ export default defineConfig({ components: { Head: './src/components/Head.astro', PageSidebar: './src/components/PageSidebar.astro', + ThemeSelect: './src/components/ThemeSelect.astro', + Footer: './src/components/Footer.astro', + }, + expressiveCode: { + // A single dark theme keeps code blocks on the terminal surface in both + // light and dark site themes, matching the design mocks. + themes: ['github-dark'], + styleOverrides: { + borderRadius: '10px', + borderColor: 'transparent', + codeBackground: '#1b1c2b', + frames: { + editorBackground: '#1b1c2b', + terminalBackground: '#1b1c2b', + editorActiveTabBackground: '#1b1c2b', + terminalTitlebarBackground: '#1b1c2b', + shadowColor: 'transparent', + }, + }, + shiki: { langs: [markdocGrammar] }, }, - expressiveCode: { shiki: { langs: [markdocGrammar] } }, plugins: [ starlightUtils({ multiSidebar: { diff --git a/website/src/assets/landing.css b/website/src/assets/landing.css index 22619e285..0f66ac359 100644 --- a/website/src/assets/landing.css +++ b/website/src/assets/landing.css @@ -1,39 +1,154 @@ +/* + * Docs theme — Iris accent + Cloud backgrounds, matched to the marketing + * landing (src/pages/index.astro, which ships its own styles). This file only + * reskins the Starlight-powered docs by overriding its CSS custom properties, + * plus a few targeted component rules. Page structure is left to Starlight. + */ + +/* ---------- Palette + type tokens ---------- */ + :root { - --purple-hsl: 255, 60%, 60%; - --overlay-blurple: hsla(var(--purple-hsl), 0.2); + /* dark (default) */ + --sl-color-white: #ecedf6; + --sl-color-gray-1: #d7d9e6; + --sl-color-gray-2: #b4b7cc; /* body text */ + --sl-color-gray-3: #8a8da6; /* muted */ + --sl-color-gray-4: #6a6d84; + --sl-color-gray-5: #3a3c4d; + --sl-color-gray-6: #23252f; + --sl-color-gray-7: #1a1b24; + --sl-color-black: #0e0f17; + + --sl-color-accent-low: rgba(93, 95, 239, 0.24); + --sl-color-accent: #5d5fef; + --sl-color-accent-high: #a9aaf5; + --sl-color-text-accent: #a9aaf5; + + --sl-color-hairline: rgba(255, 255, 255, 0.1); + --sl-color-hairline-light: rgba(255, 255, 255, 0.16); + --sl-color-hairline-shade: rgba(255, 255, 255, 0.06); + --sl-color-bg-inline-code: rgba(255, 255, 255, 0.08); + --sl-color-bg-nav: rgba(18, 19, 28, 0.82); + --sl-color-bg-sidebar: transparent; + + --ab-page-bg: linear-gradient(180deg, #141520 0%, #0e0f17 100%); + --ab-accent-fill: rgba(93, 95, 239, 0.18); + + --sl-font: 'Manrope', system-ui, -apple-system, sans-serif; + --sl-font-mono: 'JetBrains Mono', ui-monospace, monospace; + --ab-font-head: 'Space Grotesk', var(--sl-font); } :root[data-theme='light'] { - --purple-hsl: 255, 85%, 65%; + --sl-color-white: #20223a; /* headings */ + --sl-color-black: #ffffff; /* light surface (search button, overlays) */ + --sl-color-gray-1: #2c2f45; + --sl-color-gray-2: #4e5168; /* body */ + --sl-color-gray-3: #82859b; /* muted */ + --sl-color-gray-4: #9aa0b3; + --sl-color-gray-5: #d6dae1; + --sl-color-gray-6: #e9ecf1; + --sl-color-gray-7: #f3f5f8; + + --sl-color-accent-low: rgba(93, 95, 239, 0.12); + --sl-color-accent: #5d5fef; + --sl-color-accent-high: #4749d0; + --sl-color-text-accent: #5d5fef; + + --sl-color-hairline: rgba(32, 34, 58, 0.12); + --sl-color-hairline-light: rgba(32, 34, 58, 0.16); + --sl-color-hairline-shade: rgba(32, 34, 58, 0.08); + --sl-color-bg-inline-code: rgba(32, 34, 58, 0.07); + --sl-color-bg-nav: rgba(255, 255, 255, 0.82); + --sl-color-bg-sidebar: transparent; + + --ab-page-bg: linear-gradient(180deg, #f3f5f8 0%, #e9ecf1 100%); + --ab-accent-fill: rgba(93, 95, 239, 0.12); } -[data-has-hero] .page { - background: - linear-gradient(215deg, var(--overlay-blurple), transparent 40%), - radial-gradient(var(--overlay-blurple), transparent 40%) no-repeat -60vw -40vh / 105vw 200vh, - radial-gradient(var(--overlay-blurple), transparent 65%) no-repeat 50% calc(100% + 20rem) / - 60rem 30rem; +/* ---------- Base ---------- */ + +body { + background: var(--ab-page-bg); + background-attachment: fixed; +} + +:is(h1, h2, h3, h4, h5, h6), +.sl-markdown-content :is(h1, h2, h3, h4, h5, h6) { + font-family: var(--ab-font-head); + letter-spacing: -0.02em; +} + +/* ---------- Header (sticky, translucent, blurred) ---------- */ + +.header { + background: var(--sl-color-bg-nav); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); + border-bottom: 1px solid var(--sl-color-hairline); } -[data-has-hero] header { - border-bottom: 1px solid transparent; - background-color: transparent; - -webkit-backdrop-filter: blur(16px); - backdrop-filter: blur(16px); +/* ---------- Sidebar ---------- */ + +.sidebar-content .group-label .large { + font-family: var(--sl-font-mono); + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--sl-color-gray-3); } -[data-has-hero] .hero > img { - filter: drop-shadow(0 0 3rem var(--overlay-blurple)); +.sidebar-content a { + border-radius: 7px; } -/* Smaller hero image on mobile */ -@media (max-width: 768px) { - [data-has-hero] .hero > img { - max-width: 150px; - } +.sidebar-content a[aria-current='page'] { + color: var(--sl-color-text-accent); + background: var(--ab-accent-fill); + font-weight: 700; + box-shadow: inset 2px 0 0 var(--sl-color-accent); } -/* Icon row component */ +/* ---------- Right rail (on this page) ---------- */ + +.right-sidebar a[aria-current='true'] { + color: var(--sl-color-text-accent); + font-weight: 700; +} + +/* ---------- Callouts (minimal: mono label + bottom hairline) ---------- */ + +.starlight-aside { + background: transparent; + border: 0; + border-bottom: 1px solid var(--sl-color-hairline); + border-radius: 0; + padding: 0.25rem 0 1rem; +} + +.starlight-aside__title { + font-family: var(--sl-font-mono); + font-size: 0.6875rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.starlight-aside__icon { + display: none; +} + +/* ---------- Prev / next cards ---------- */ + +.pagination-links a { + border-radius: 10px; + border: 1px solid var(--sl-color-hairline); + background: var(--sl-color-bg-nav); +} + +/* ---------- Retained utilities ---------- */ + +/* Icon row component (quick-start guide) */ .icon-row { display: inline-flex; align-items: center; @@ -64,20 +179,21 @@ display: none; } -/* Align nav links vertically when shields.io badge is present */ +/* Align nav links vertically when the shields.io badge is present */ ul:has(.nav-external) { align-items: center; } -.gh-badge { - height: 1.35rem; - display: block; - position: relative; - top: 1px; +.nav-external { + display: inline-flex; + align-items: center; + gap: 6px; } -:root[data-theme='light'] .gh-badge { - filter: invert(0.88); +.gh-stars { + font-family: var(--sl-font-mono); + font-size: 0.75rem; + color: var(--sl-color-gray-3); } /* WIP items in sidebar */ diff --git a/website/src/components/Footer.astro b/website/src/components/Footer.astro new file mode 100644 index 000000000..b82eb0eaf --- /dev/null +++ b/website/src/components/Footer.astro @@ -0,0 +1,34 @@ +--- +// Keeps Starlight's footer (pagination, last-updated, edit link) and appends a +// mono footer bar matching the landing, for a consistent close on every page. +import StarlightFooter from '@astrojs/starlight/components/Footer.astro'; +--- + + + + + + diff --git a/website/src/components/Head.astro b/website/src/components/Head.astro index ac0e64211..269e5ab50 100644 --- a/website/src/components/Head.astro +++ b/website/src/components/Head.astro @@ -28,18 +28,29 @@ import StarlightHead from '@astrojs/starlight/components/Head.astro'; diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx deleted file mode 100644 index 020b03c30..000000000 --- a/website/src/content/docs/index.mdx +++ /dev/null @@ -1,43 +0,0 @@ ---- -title: Actionbase -head: - - tag: title - content: Actionbase -description: A database for serving user interactions at scale. -template: splash -editUrl: false -lastUpdated: false -hero: - title: Actionbase - tagline: One database for user-user (follows), user-item (likes), and item-item (related items) interactions — precomputed at write time, served as simple lookups - image: - file: ~/assets/hero.webp - actions: - - text: Get started - icon: right-arrow - link: introduction/ - - text: View on GitHub - icon: external - variant: minimal - link: https://github.com/kakao/actionbase ---- - -import { CardGrid, Card } from '@astrojs/starlight/components'; - - - - Writes do the work. Reads return what's already computed. - - - One model for user–user, user–item, and item–item interactions. - - - Simple writes, fast reads — all via HTTP. - - - Stream changes for downstream pipelines. - - - Serving over a million requests per minute at Kakao. - - diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro new file mode 100644 index 000000000..0593727b0 --- /dev/null +++ b/website/src/pages/index.astro @@ -0,0 +1,1193 @@ +--- +import logoLight from '../assets/logo-light.svg?url'; +import logoDark from '../assets/logo-dark.svg?url'; + +// Standalone marketing landing. Kept outside Starlight's content collection so +// the bespoke chrome (marginalia, hero console, architecture flow) has full +// layout control; theme state still syncs with Starlight via the shared +// `starlight-theme` localStorage key and `` attribute. + +const GA_ID = 'G-2PY5JXRJ5J'; +const site = 'https://actionbase.io/'; +const ogUrl = new URL('og.png?v=1', site).href; +const ogImageAlt = + 'Actionbase is a production-proven OLTP database serving tens of millions of users across Kakao.'; +const repo = 'https://github.com/kakao/actionbase'; + +// ASCII otter banner, boxed at build time (from the Actionbase CLI welcome art). +const BANNER_WIDTH = 98; +const bannerArt = [ + '', + ' _____________', + ' __/ ___ \\ Welcome to Actionbase! v0.2.1', + ' _/\\ ____/ \\____\\_', + ' / \\/ _/ @ \\_♥/\\ Actionbase standalone:8080', + ' / / | |', + ' _/ / ___/ \\/ proxy off', + ' / | ______/ debug off', + ' __/ | /', + ' ___/ \\ _/', + ' | /\\__', + " | / \\__ _ _ _ _", + " | /______ \\ / \\ ___| |_(_) ___ _ __ | |__ __ _ ___ ___", + " \\ \\_ \\ \\ / _ \\ / __| __| |/ _ \\| '_ \\| '_ \\ / _` / __|/ _ \\", + " \\__ \\__ |_| / ___ \\ (__| |_| | (_) | | | | |_) | (_| \\__ \\ __/", + " \\____\\ /_/ \\_\\___|\\__|_|\\___/|_| |_|_.__/ \\__,_|___/\\___|", + '', +]; +const banner = [ + '╭' + '─'.repeat(BANNER_WIDTH) + '╮', + ...bannerArt.map((l) => '│' + (l + ' '.repeat(BANNER_WIDTH)).slice(0, BANNER_WIDTH) + '│'), + '╰' + '─'.repeat(BANNER_WIDTH) + '╯', +].join('\n'); + +const features = [ + { n: 'I', title: 'Pre-computed Reads', desc: 'Writes do the work. Reads return what’s already computed.' }, + { n: 'II', title: 'U2U · U2I · I2I', desc: 'One model for user–user, user–item, and item–item interactions.' }, + { n: 'III', title: 'REST API', desc: 'Simple writes, fast reads — all via HTTP.' }, + { n: 'IV', title: 'CDC Built-in', desc: 'Stream changes for downstream pipelines.' }, + { n: 'V', title: 'Pluggable Storage', desc: 'A minimal storage abstraction — HBase today, SlateDB planned.' }, + { n: 'VI', title: 'Battle-tested', desc: 'Over a million requests per minute at Kakao.' }, +]; + +// Both paths converge on one wide Storage Backend. Kafka sits on the write side +// only (WAL in, CDC out); the read path (Query doc) is a direct lookup. +const writeNode = { name: 'Kafka', sub: 'WAL in · CDC out' }; +const readNode = { name: 'GET · COUNT · SCAN', sub: 'direct REST lookups' }; +const storageNode = { name: 'Storage Backend', sub: 'state · index · count — HBase / SlateDB' }; + +const steps = [ + { n: '1', label: 'Run', code: 'docker run -it \\\n ghcr.io/kakao/actionbase:standalone' }, + { n: '2', label: 'Load sample data', code: 'load preset likes' }, + { + n: '3', + label: 'Query — just read', + code: 'get --source Alice --target Phone\nscan --index recent --start Bob --direction OUT\ncount --start Phone --direction IN', + }, +]; + +const presets = [ + 'get --source Alice --target Phone', + 'scan --index recent --start Bob --direction OUT', + 'count --start Phone --direction IN', +]; +--- + + + + + + + Actionbase + + + + + + + + + + + + + + + + + + + +
+ + + + +
+
+

One database for follows, likes, and related items

+

+ Pre-computed at write time, served in real-time. Reads become simple lookups — no read-time + aggregation. +

+ +
+ + +
+
+
+
+ + actionbase — standalone +
+
+ +
+
actionbase standalone · server :8080 · CLI ready
+
preset 'likes' loaded — 3 edges
+
Alice → Phone Bob → Laptop Bob → Phone
+
run the steps below, in order ↓
+
+
+ actionbase> + + run a step below ↓ +
+
+
+
+
+ {presets.map((c, i) => ( + + ))} + +
+
+
+ + +
+
+

+ Serving over a million requests per minute, in production at Kakao. +

+
+ #real-time + #low-latency + #high-throughput +
+
+
+ + +
+
+

The engine

+ Writes do the work +
+
+ { + features.map((f) => ( +
+
{f.n}
+

{f.title}

+

{f.desc}

+
+ )) + } +
+
+ + +
+
+ ArchitectureClient → Actionbase → storage & stream +
+
+
Client
+
+
REST API · HTTP
+
+
Actionbase
+
+
+
+
Write path · writes do the work
+
+
+
+ {writeNode.name} + {writeNode.sub} +
+
+
+
+
+
Read path · simple lookups
+
+
+
+ {readNode.name} + {readNode.sub} +
+
+
+
+
+
+ {storageNode.name} + {storageNode.sub} +
+
+
+ + +
+
+
+

Quick start

+ Three steps +
+
+ { + steps.map((s) => ( +
+
+ {s.n} + {s.label} +
+
{s.code}
+
+ )) + } +
+

+ See the Quick Start, or + Build Your Social Media App to go deeper. +

+
+
+ + +
+
+

Ready to build?

+

+ The reference covers concepts, architecture, and hands-on guides — from a single write to a + million reads per minute. +

+ +
+
+ + +
+ +
+
+ + + + + + From dbc18e00cd53c8a20f51182e4489fa7528fa95c0 Mon Sep 17 00:00:00 2001 From: Minseok Kim Date: Thu, 23 Jul 2026 14:00:54 +0900 Subject: [PATCH 02/27] feat(website): refine landing console and engine cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLI console: title now "actionbase cli"; colour the banner to match the real CLI (white otter, gold frame/heart/labels/wordmark) instead of a flat gold; fix scroll so the whole stack (banner included) scrolls up as commands run — the term body is a plain block scroll container pinned to the bottom (a flex column was squashing the banner instead of overflowing). - Terminal frame: thinner, muted so it no longer overpowers the console. - Engine cards: vertical stripes only (no grid crosslines, no gradient wash). - "The engine" section retitled "Under the hood"; drop the roman-numeral card ordinals. Co-Authored-By: Claude Opus 4.8 (1M context) --- website/src/pages/index.astro | 139 +++++++++++++++++++++------------- 1 file changed, 86 insertions(+), 53 deletions(-) diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 0593727b0..2f747c30d 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -14,40 +14,53 @@ const ogImageAlt = 'Actionbase is a production-proven OLTP database serving tens of millions of users across Kakao.'; const repo = 'https://github.com/kakao/actionbase'; -// ASCII otter banner, boxed at build time (from the Actionbase CLI welcome art). +// ASCII otter banner, coloured to mirror the real CLI welcome art (ANSI 33): +// the otter body stays the default terminal foreground; the frame, the heart, +// the right-hand info labels and the "Actionbase" wordmark are gold. Each line +// is a list of [text, gold?] segments — concatenated they reproduce the raw +// art, and gold runs are wrapped in a at build time. const BANNER_WIDTH = 98; -const bannerArt = [ - '', - ' _____________', - ' __/ ___ \\ Welcome to Actionbase! v0.2.1', - ' _/\\ ____/ \\____\\_', - ' / \\/ _/ @ \\_♥/\\ Actionbase standalone:8080', - ' / / | |', - ' _/ / ___/ \\/ proxy off', - ' / | ______/ debug off', - ' __/ | /', - ' ___/ \\ _/', - ' | /\\__', - " | / \\__ _ _ _ _", - " | /______ \\ / \\ ___| |_(_) ___ _ __ | |__ __ _ ___ ___", - " \\ \\_ \\ \\ / _ \\ / __| __| |/ _ \\| '_ \\| '_ \\ / _` / __|/ _ \\", - " \\__ \\__ |_| / ___ \\ (__| |_| | (_) | | | | |_) | (_| \\__ \\ __/", - " \\____\\ /_/ \\_\\___|\\__|_|\\___/|_| |_|_.__/ \\__,_|___/\\___|", - '', +const G = true; +const bannerSegs = [ + [['', false]], + [[' _____________', false]], + [[' __/ ___ \\ Welcome to Actionbase! v0.2.1', false]], + [[' _/\\ ____/ \\____\\_', false]], + [[' / \\/ _/ @ \\_', false], ['♥/', G], ['\\ ', false], ['Actionbase standalone:8080', G]], + [[' / / | |', false]], + [[' _/ / ___/ \\/ ', false], ['proxy', G], [' off', false]], + [[' / | ______/ ', false], ['debug', G], [' off', false]], + [[' __/ | /', false]], + [[' ___/ \\ _/', false]], + [[' | /\\__', false]], + [[' | / \\__', false], [' _ _ _ _', G]], + [[' | /______ \\', false], [' / \\ ___| |_(_) ___ _ __ | |__ __ _ ___ ___', G]], + [[" \\ \\_ \\ \\", false], [" / _ \\ / __| __| |/ _ \\| '_ \\| '_ \\ / _` / __|/ _ \\", G]], + [[" \\__ \\__ |_|", false], [" / ___ \\ (__| |_| | (_) | | | | |_) | (_| \\__ \\ __/", G]], + [[' \\____\\', false], [' /_/ \\_\\___|\\__|_|\\___/|_| |_|_.__/ \\__,_|___/\\___|', G]], + [['', false]], ]; +const escHtml = (s) => s.replace(/&/g, '&').replace(//g, '>'); +const goldSpan = (s) => `${escHtml(s)}`; +const bannerLine = (segs) => { + const raw = segs.map(([t]) => t).join(''); + const pad = ' '.repeat(Math.max(0, BANNER_WIDTH - raw.length)); + const inner = segs.map(([t, g]) => (g ? goldSpan(t) : escHtml(t))).join('') + pad; + return goldSpan('│') + inner + goldSpan('│'); +}; const banner = [ - '╭' + '─'.repeat(BANNER_WIDTH) + '╮', - ...bannerArt.map((l) => '│' + (l + ' '.repeat(BANNER_WIDTH)).slice(0, BANNER_WIDTH) + '│'), - '╰' + '─'.repeat(BANNER_WIDTH) + '╯', + goldSpan('╭' + '─'.repeat(BANNER_WIDTH) + '╮'), + ...bannerSegs.map(bannerLine), + goldSpan('╰' + '─'.repeat(BANNER_WIDTH) + '╯'), ].join('\n'); const features = [ - { n: 'I', title: 'Pre-computed Reads', desc: 'Writes do the work. Reads return what’s already computed.' }, - { n: 'II', title: 'U2U · U2I · I2I', desc: 'One model for user–user, user–item, and item–item interactions.' }, - { n: 'III', title: 'REST API', desc: 'Simple writes, fast reads — all via HTTP.' }, - { n: 'IV', title: 'CDC Built-in', desc: 'Stream changes for downstream pipelines.' }, - { n: 'V', title: 'Pluggable Storage', desc: 'A minimal storage abstraction — HBase today, SlateDB planned.' }, - { n: 'VI', title: 'Battle-tested', desc: 'Over a million requests per minute at Kakao.' }, + { title: 'Pre-computed Reads', desc: 'Writes do the work. Reads return what’s already computed.' }, + { title: 'U2U · U2I · I2I', desc: 'One model for user–user, user–item, and item–item interactions.' }, + { title: 'REST API', desc: 'Simple writes, fast reads — all via HTTP.' }, + { title: 'CDC Built-in', desc: 'Stream changes for downstream pipelines.' }, + { title: 'Pluggable Storage', desc: 'A minimal storage abstraction — HBase today, SlateDB planned.' }, + { title: 'Battle-tested', desc: 'Over a million requests per minute at Kakao.' }, ]; // Both paths converge on one wide Storage Backend. Kafka sits on the write side @@ -164,10 +177,10 @@ const presets = [
- actionbase — standalone + actionbase cli
- +
actionbase standalone · server :8080 · CLI ready
preset 'likes' loaded — 3 edges
@@ -207,17 +220,16 @@ const presets = [
- +
-

The engine

+

Under the hood

Writes do the work
{ features.map((f) => (
-
{f.n}

{f.title}

{f.desc}

@@ -362,6 +374,7 @@ const presets = [ --surf-soft: rgba(255, 255, 255, 0.62); --surf-panel: rgba(255, 255, 255, 0.5); --surf-foot: rgba(230, 232, 240, 0.7); + --grid-line: rgba(32, 34, 58, 0.05); --page-bg: linear-gradient(180deg, #f3f5f8 0%, #e9ecf1 100%); } @@ -377,6 +390,7 @@ const presets = [ --surf-soft: rgba(255, 255, 255, 0.04); --surf-panel: rgba(255, 255, 255, 0.03); --surf-foot: rgba(0, 0, 0, 0.28); + --grid-line: rgba(255, 255, 255, 0.05); --page-bg: linear-gradient(180deg, #141520 0%, #0e0f17 100%); } @@ -543,18 +557,20 @@ const presets = [ margin: 0; } .glass { - padding: 11px; + /* Thinner, muted frame so it frames the terminal without shouting: + slimmer padding, a desaturated indigo wash, fainter grid + shadow. */ + padding: 6px; background: - linear-gradient(155deg, #7c7ee8, #3e40a8), - linear-gradient(rgba(255, 255, 255, 0.22) 1px, transparent 1px), - linear-gradient(90deg, rgba(255, 255, 255, 0.22) 1px, transparent 1px); + linear-gradient(155deg, #4c4e86, #2e3068), + linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px), + linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px); background-size: 100% 100%, 26px 26px, 26px 26px; box-shadow: - 0 30px 60px -30px rgba(35, 37, 75, 0.5), - inset 0 1px 2px rgba(255, 255, 255, 0.4); + 0 22px 46px -32px rgba(35, 37, 75, 0.4), + inset 0 1px 2px rgba(255, 255, 255, 0.22); } .term { background: var(--term-bg); @@ -582,25 +598,35 @@ const presets = [ .term-body { padding: 16px; height: 294px; - overflow: hidden; - display: flex; - flex-direction: column; - justify-content: flex-end; + /* Real-terminal scroll: a plain block scroll container so content keeps + its natural height and overflows (a flex column would instead shrink + the banner to fit). JS pins the view to the bottom so the prompt stays + visible and the banner scrolls up as commands accumulate; scrollbar + hidden to keep the chrome clean. */ + overflow-y: auto; + scrollbar-width: none; font-family: var(--font-mono); font-size: 12.5px; line-height: 1.65; color: var(--term-text); } + .term-body::-webkit-scrollbar { + display: none; + } .banner { margin: 0 0 10px; padding: 0; - color: var(--term-banner); + /* Otter body = default terminal foreground; gold runs opt in via .g. */ + color: var(--term-text); font-family: var(--font-mono); font-size: 6.5px; line-height: 1.28; white-space: pre; overflow: hidden; } + .banner .g { + color: var(--term-banner); + } .line { display: flex; gap: 8px; @@ -748,18 +774,14 @@ const presets = [ gap: 20px; } .card { - background: var(--surf-soft); + /* Vertical stripes only — no grid crosslines, no gradient wash. */ + background-color: var(--surf-soft); + background-image: linear-gradient(90deg, var(--grid-line) 1px, transparent 1px); + background-size: 28px 100%; border: 1px solid var(--line-soft); padding: 24px 22px; min-height: 148px; - } - .card-num { - font-family: var(--font-head); - font-weight: 700; - font-size: 15px; - color: var(--accent); - margin-bottom: 14px; - letter-spacing: 0.04em; + box-shadow: 0 16px 34px -26px rgba(35, 37, 75, 0.4); } .card h3 { margin: 0 0 8px; @@ -1069,6 +1091,11 @@ const presets = [ const consoleEl = document.querySelector('[data-console]'); if (consoleEl) { const linesEl = consoleEl.querySelector('[data-lines]'); + const bodyEl = consoleEl.querySelector('.term-body'); + // Keep the newest output (and the banner scrolling off the top) in view. + const scrollToEnd = () => { + if (bodyEl) bodyEl.scrollTop = bodyEl.scrollHeight; + }; const preset = [ { source: 'Alice', target: 'Phone', ts: 5 }, { source: 'Bob', target: 'Laptop', ts: 4 }, @@ -1090,6 +1117,7 @@ const presets = [ span.textContent = text; row.appendChild(span); linesEl.appendChild(row); + scrollToEnd(); }; const parseFlags = (tokens) => { @@ -1187,6 +1215,11 @@ const presets = [ }); syncButtons(); + // Pin to the bottom now, on the next frame, and again once the tiny + // 6.5px banner webfont swaps in (which changes the scroll height). + scrollToEnd(); + requestAnimationFrame(scrollToEnd); + if (document.fonts && document.fonts.ready) document.fonts.ready.then(scrollToEnd); } From 7d5dcc144aee87b8e2fccc41563c7380d6d3caa4 Mon Sep 17 00:00:00 2001 From: Minseok Kim Date: Thu, 23 Jul 2026 14:16:30 +0900 Subject: [PATCH 03/27] feat(website): lock the landing console from user scroll/drag Make the term body overflow:hidden (was auto) so the wheel and drag no longer move it, and user-select:none so text can't be drag-selected. The JS still scrolls it programmatically after each command, pinning the view to the bottom so a click jumps there and stays put. Co-Authored-By: Claude Opus 4.8 (1M context) --- website/src/pages/index.astro | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 2f747c30d..d6d46638c 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -598,21 +598,19 @@ const presets = [ .term-body { padding: 16px; height: 294px; - /* Real-terminal scroll: a plain block scroll container so content keeps - its natural height and overflows (a flex column would instead shrink - the banner to fit). JS pins the view to the bottom so the prompt stays - visible and the banner scrolls up as commands accumulate; scrollbar - hidden to keep the chrome clean. */ - overflow-y: auto; - scrollbar-width: none; + /* Block scroll container so content keeps its natural height and + overflows (a flex column would instead shrink the banner to fit). + overflow:hidden means the user can't scroll or drag it — only the JS + below moves it, pinning the view to the bottom after each command so + the prompt stays put and the banner scrolls off. user-select:none + stops drag-selection inside the console. */ + overflow: hidden; + user-select: none; font-family: var(--font-mono); font-size: 12.5px; line-height: 1.65; color: var(--term-text); } - .term-body::-webkit-scrollbar { - display: none; - } .banner { margin: 0 0 10px; padding: 0; From 81515929905c69c80e3fafb71233e8846d87ab2a Mon Sep 17 00:00:00 2001 From: Minseok Kim Date: Thu, 23 Jul 2026 16:13:20 +0900 Subject: [PATCH 04/27] =?UTF-8?q?feat(website):=20rework=20hero=20into=20a?= =?UTF-8?q?=202=C3=972=20demo=20and=20use=20the=20wordmark=20logo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Hero is now a 2×2 grid — copy | console over graph | run controls — so the space reads full instead of a lone column. The mascot is dropped from the hero; the interactive edge-state graph reacts to each console step (get/scan/ count highlight the relevant edges/nodes, count shows a badge). - Copy: value-prop headline "Precomputed at write time, served as simple lookups" over the GitHub-description subline. - The three demo cells (console, graph, controls) share one .panel frame (surf-soft fill, line-soft border, equal padding) so they read as a set; drop the loud indigo console frame and the unused --glass token. - Logo: crop the shared SVG viewBox to the "actionbase" wordmark (otter off), and size the docs header logo to match the landing nav (26px). - Section retitled "Under the hood" → "Features"; drop its subtitle. Co-Authored-By: Claude Opus 4.8 (1M context) --- website/src/assets/landing.css | 7 + website/src/assets/logo-dark.svg | 2 +- website/src/assets/logo-light.svg | 2 +- website/src/pages/index.astro | 380 ++++++++++++++++++++++++------ 4 files changed, 314 insertions(+), 77 deletions(-) diff --git a/website/src/assets/landing.css b/website/src/assets/landing.css index 0f66ac359..0fb9a9789 100644 --- a/website/src/assets/landing.css +++ b/website/src/assets/landing.css @@ -200,3 +200,10 @@ ul:has(.nav-external) { .sidebar-wip { opacity: 0.5; } + +/* Docs header logo: the shared wordmark SVG is wide/short, and Starlight renders + it at 40px by default — size it down to match the landing nav (26px). */ +.site-title img { + height: 26px !important; + width: auto !important; +} diff --git a/website/src/assets/logo-dark.svg b/website/src/assets/logo-dark.svg index fc5352992..bba0860ec 100644 --- a/website/src/assets/logo-dark.svg +++ b/website/src/assets/logo-dark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/website/src/assets/logo-light.svg b/website/src/assets/logo-light.svg index 4e2fd9055..ac6885e90 100644 --- a/website/src/assets/logo-light.svg +++ b/website/src/assets/logo-light.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index d6d46638c..2158bcc89 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -146,8 +146,8 @@ const presets = [ - +
-
-

One database for follows, likes, and related items

-

- Pre-computed at write time, served in real-time. Reads become simple lookups — no read-time - aggregation. -

-
- Get started - View on GitHub +
+
+

+ Precomputed at write time, served as simple lookups +

+

+ One database for user–user (follows), user–item (likes), and item–item (related + items) interactions. +

+
-
- -
-
-
-
- - actionbase cli -
-
- -
-
actionbase standalone · server :8080 · CLI ready
-
preset 'likes' loaded — 3 edges
-
Alice → Phone Bob → Laptop Bob → Phone
-
run the steps below, in order ↓
+ +
+
+
+
+ + actionbase cli
-
- actionbase> - - run a step below ↓ +
+ +
+
actionbase standalone · server :8080 · CLI ready
+
preset 'likes' loaded — 3 edges
+
Alice → Phone Bob → Laptop Bob → Phone
+
run a step → watch the graph
+
+
+ actionbase> + + run a step → +
+
+ + +
+
Edge state · arrows denote likes
+ + + + + + + + + + + + + + + + + + + + Alice + user + + + + Bob + user + + + + Phone + item + + + + Laptop + item + + + + +
+ + +
+ Run a step — watch the edges +
+ {presets.map((c, i) => ( + + ))} + +
-
- {presets.map((c, i) => ( - - ))} - -
-
+
@@ -220,11 +285,10 @@ const presets = [
- +
-

Under the hood

- Writes do the work +

Features

{ @@ -349,7 +413,6 @@ const presets = [ --term-prompt: #a9aaf5; --term-banner: #d9a94e; --term-dot: #33354a; - --glass: linear-gradient(155deg, #7c7ee8, #3e40a8); --out-ok: #7fd1b0; --out-dim: #9092ab; --out-err: #e0a06a; @@ -500,11 +563,42 @@ const presets = [ .hero { max-width: var(--maxw); margin: 0 auto; - padding: 56px var(--gutter) 30px; + padding: 48px var(--gutter) 40px; + } + /* 2×2 hero: copy | console / graph | run buttons */ + .hero-grid { display: grid; grid-template-columns: 1.05fr 1fr; - gap: 60px; - align-items: center; + grid-template-areas: + 'copy term' + 'graph btns'; + column-gap: 44px; + row-gap: 26px; + align-items: start; + } + .cell-copy { + grid-area: copy; + align-self: center; + } + .cell-term { + grid-area: term; + } + .cell-graph { + grid-area: graph; + } + .cell-btns { + grid-area: btns; + align-self: stretch; + display: flex; + flex-direction: column; + gap: 12px; + } + .run-label { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--muted); } .title { font-family: var(--font-head); @@ -552,25 +646,103 @@ const presets = [ color: var(--ink); border-color: var(--ink); } + /* edge state graph */ + .graph { + margin: 0; + display: flex; + flex-direction: column; + } + .graph-cap { + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--muted); + margin-bottom: 14px; + } + .graph-svg { + width: 100%; + height: auto; + margin: auto 0; + max-height: 320px; + } + .gnode rect { + fill: var(--surf-card); + stroke: var(--line-mid); + stroke-width: 1.2; + transition: + fill 0.25s, + stroke 0.25s, + opacity 0.25s; + } + .gnode-user rect { + stroke: var(--line-24); + } + .gn-name { + fill: var(--ink); + font-family: var(--font-head); + font-weight: 700; + font-size: 14px; + text-anchor: middle; + transition: fill 0.25s; + } + .gn-type { + fill: var(--muted); + font-family: var(--font-mono); + font-size: 7.5px; + letter-spacing: 0.14em; + text-transform: uppercase; + text-anchor: middle; + } + .gedge line { + stroke: var(--line-strong); + stroke-width: 1.6; + transition: + stroke 0.25s, + stroke-width 0.25s, + opacity 0.25s; + } + /* reactive states, toggled from the console */ + .gnode.dim, + .gedge.dim { + opacity: 0.26; + } + .gedge.on line { + stroke: var(--accent); + stroke-width: 2.6; + } + .gnode.on rect { + stroke: var(--accent); + stroke-width: 2; + fill: var(--accent-fill); + } + .gnode.on .gn-name { + fill: var(--accent); + } + .gbadge[hidden] { + display: none; + } + .gbadge circle { + fill: var(--accent); + } + .gbadge text { + fill: #fff; + font-family: var(--font-mono); + font-size: 12px; + font-weight: 700; + text-anchor: middle; + dominant-baseline: central; + } /* console */ .console { margin: 0; } - .glass { - /* Thinner, muted frame so it frames the terminal without shouting: - slimmer padding, a desaturated indigo wash, fainter grid + shadow. */ - padding: 6px; - background: - linear-gradient(155deg, #4c4e86, #2e3068), - linear-gradient(rgba(255, 255, 255, 0.1) 1px, transparent 1px), - linear-gradient(90deg, rgba(255, 255, 255, 0.1) 1px, transparent 1px); - background-size: - 100% 100%, - 26px 26px, - 26px 26px; - box-shadow: - 0 22px 46px -32px rgba(35, 37, 75, 0.4), - inset 0 1px 2px rgba(255, 255, 255, 0.22); + /* Shared frame for the three demo cells (console, graph, controls) so + they read as one set; the dark console just sits inside its panel. */ + .panel { + padding: 20px; + background: var(--surf-soft); + border: 1px solid var(--line-soft); } .term { background: var(--term-bg); @@ -678,15 +850,15 @@ const presets = [ } .presets { display: flex; - flex-wrap: wrap; - gap: 7px; - margin-top: 14px; + flex-direction: column; + align-items: stretch; + gap: 8px; } .preset { text-align: left; - background: var(--surf-soft); + background: var(--surf-card); border: 1px solid var(--line-mid); - padding: 6px 10px; + padding: 8px 11px; font-family: var(--font-mono); font-size: 11px; color: var(--body); @@ -1017,13 +1189,14 @@ const presets = [ /* responsive */ @media (max-width: 900px) { - .hero { - grid-template-columns: 1fr; - gap: 40px; - } .title { font-size: 44px; } + .hero-grid { + grid-template-columns: 1fr; + grid-template-areas: 'copy' 'term' 'graph' 'btns'; + row-gap: 22px; + } .features, .steps { grid-template-columns: 1fr; @@ -1101,6 +1274,49 @@ const presets = [ ]; let edges = preset.slice(); + // Edge-state graph — the console drives it so a command's meaning is + // visible: get highlights one edge, scan/count highlight a fan of them. + const graphEl = document.querySelector('[data-graph]'); + const NAME = { Alice: 'alice', Bob: 'bob', Phone: 'phone', Laptop: 'laptop' }; + const NODE_CORNER = { alice: [118, 52], bob: [118, 156], phone: [316, 46], laptop: [316, 164] }; + const gEdges = graphEl ? [...graphEl.querySelectorAll('.gedge')] : []; + const gNodes = graphEl ? [...graphEl.querySelectorAll('.gnode')] : []; + const gBadge = graphEl ? graphEl.querySelector('[data-badge]') : null; + const gBadgeC = graphEl ? graphEl.querySelector('[data-badge-c]') : null; + const gBadgeT = graphEl ? graphEl.querySelector('[data-badge-t]') : null; + const edgeId = (e) => NAME[e.source] + '-' + NAME[e.target]; + + const clearGraph = () => { + gEdges.forEach((e) => e.classList.remove('on', 'dim')); + gNodes.forEach((n) => n.classList.remove('on', 'dim')); + if (gBadge) gBadge.setAttribute('hidden', ''); + }; + const focusGraph = (edgeIds, nodeIds) => { + const es = new Set(edgeIds); + const ns = new Set(nodeIds); + gEdges.forEach((e) => { + const on = es.has(e.dataset.edge); + e.classList.toggle('on', on); + e.classList.toggle('dim', !on); + }); + gNodes.forEach((n) => { + const on = ns.has(n.dataset.node); + n.classList.toggle('on', on); + n.classList.toggle('dim', !on); + }); + if (gBadge) gBadge.setAttribute('hidden', ''); + }; + const showBadge = (nodeId, n) => { + if (!gBadge || !NODE_CORNER[nodeId]) return; + const [x, y] = NODE_CORNER[nodeId]; + gBadgeC.setAttribute('cx', x); + gBadgeC.setAttribute('cy', y); + gBadgeT.setAttribute('x', x); + gBadgeT.setAttribute('y', y); + gBadgeT.textContent = String(n); + gBadge.removeAttribute('hidden'); + }; + const addLine = (kind, text, withPrompt) => { const row = document.createElement('div'); row.className = 'line'; @@ -1149,13 +1365,21 @@ const presets = [ if (hit) { addLine('ok', '→ 1 edge'); addLine('out', ' ' + hit.source + ' --likes--> ' + hit.target + ' (exists)'); + focusGraph([edgeId(hit)], [NAME[f.source], NAME[f.target]]); } else { addLine('dim', '→ 0 edges · not found'); + focusGraph([], []); } } else if (cmd === 'count') { const dir = String(f.direction || 'OUT').toUpperCase(); const m = edges.filter((e) => (dir === 'IN' ? e.target === f.start : e.source === f.start)); addLine('ok', '→ ' + m.length); + const startId = NAME[f.start]; + focusGraph( + m.map(edgeId), + [startId, ...m.map((e) => (dir === 'IN' ? NAME[e.source] : NAME[e.target]))], + ); + showBadge(startId, m.length); } else if (cmd === 'scan') { const dir = String(f.direction || 'OUT').toUpperCase(); const m = edges @@ -1164,9 +1388,14 @@ const presets = [ addLine('ok', '→ ' + m.length + ' edge' + (m.length === 1 ? '' : 's') + ' · index recent'); m.forEach((e) => addLine('out', ' ' + e.source + ' --likes--> ' + e.target)); if (!m.length) addLine('dim', ' (none)'); + focusGraph( + m.map(edgeId), + [NAME[f.start], ...m.map((e) => (dir === 'IN' ? NAME[e.source] : NAME[e.target]))], + ); } else if (cmd === 'load') { edges = preset.slice(); addLine('ok', "→ preset 'likes' loaded · 3 edges"); + clearGraph(); } else if (cmd === 'help') { addLine('dim', 'get --source --target '); addLine('dim', 'scan --index recent --start --direction OUT|IN'); @@ -1178,13 +1407,13 @@ const presets = [ // Guided tour: the three presets unlock one at a time, in order; a reset // appears once the tour has started and returns the console to step one. - const stepButtons = [...consoleEl.parentElement.querySelectorAll('[data-step]')]; - const resetButton = consoleEl.parentElement.querySelector('[data-reset]'); + const stepButtons = [...document.querySelectorAll('[data-step]')]; + const resetButton = document.querySelector('[data-reset]'); const initialLines = [ ['sys', 'actionbase standalone · server :8080 · CLI ready'], ['sys', "preset 'likes' loaded — 3 edges"], ['sys', 'Alice → Phone Bob → Laptop Bob → Phone'], - ['dim', 'run the steps below, in order ↓'], + ['dim', 'run a step → watch the graph'], ]; let step = 0; @@ -1208,6 +1437,7 @@ const presets = [ edges = preset.slice(); linesEl.replaceChildren(); initialLines.forEach(([k, t]) => addLine(k, t, false)); + clearGraph(); step = 0; syncButtons(); }); From 7955ecedac02d6b3b6200dd1fac81735ea9ae9a9 Mon Sep 17 00:00:00 2001 From: Minseok Kim Date: Thu, 23 Jul 2026 17:05:22 +0900 Subject: [PATCH 05/27] feat(website): restructure the landing into a demo-led flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move the interactive console/graph out of the hero into a dedicated "Interactive demo" section (renamed from "Try it" to signal it's a simulation), below the trust band and Features. - Section order is now Features → Interactive demo → Architecture → Quick start → CTA. Architecture is promoted to a top-level section (h2), and the CTA reads "Start building". - Hero is copy-only: a two-line value-prop headline over the one-line GitHub description. - Demo controls become two tabs — Supported (get/scan/count, runnable in any order, clicked step stays active) and Coming soon (agg + two topk shapes from #369 + multihop, disabled with a TBD tag). Each command shows a short description; on mobile the tabs collapse and the upcoming queries render as a plain list. - Console and graph share the panel row height; the console content is absolutely positioned so running commands never grows the cell. Graph is modernized (thin edges, open-chevron arrows, "likes" labels, count numbers the matched edges 1..n). - Nav/docs logo is the otter-less "actionbase" wordmark. Co-Authored-By: Claude Opus 4.8 (1M context) --- website/src/pages/index.astro | 523 +++++++++++++++++++++------------- 1 file changed, 330 insertions(+), 193 deletions(-) diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 2158bcc89..05fe053b5 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -80,9 +80,19 @@ const steps = [ ]; const presets = [ - 'get --source Alice --target Phone', - 'scan --index recent --start Bob --direction OUT', - 'count --start Phone --direction IN', + { cmd: 'get --source Alice --target Phone', desc: 'Fetch a single edge' }, + { cmd: 'scan --index recent --start Bob --direction OUT', desc: 'List edges, newest first' }, + { cmd: 'count --start Phone --direction IN', desc: 'Count edges on a node' }, +]; + +// Teaser for query operations that exist internally but aren't shipped yet — +// shown disabled ("TBD") to hint at what's coming. topk covers issue #369 +// shapes 2 (Target|Window) and 4 (Target|Filter,Window). +const soon = [ + { cmd: 'agg --start Phone --direction IN --by hour --last 1d', desc: 'Aggregate edges over time' }, + { cmd: 'topk --target product --edge purchase --last 1w', desc: 'Trending items this week' }, + { cmd: 'topk --start Bob --target product --edge purchase --last 1y', desc: "A user's recent purchases" }, + { cmd: 'multihop --start Alice --path follow,wish', desc: "Friends' wishes, across hops" }, ]; --- @@ -157,26 +167,62 @@ const presets = [
- +
-
-
-

- Precomputed at write time, served as simple lookups -

-

- One database for user–user (follows), user–item (likes), and item–item (related - items) interactions. -

- +
+

+ Precomputed at write time, served as simple lookups +

+

+ One database for user–user (follows), user–item (likes), and item–item (related + items) interactions. +

+ +
+
- -
-
+ +
+
+

+ Serving over a million requests per minute, in production at Kakao. +

+
+ #real-time + #low-latency + #high-throughput +
+
+
+ + +
+
+

Features

+
+
+ { + features.map((f) => ( +
+

{f.title}

+

{f.desc}

+
+ )) + } +
+
+ + +
+
+

Interactive demo

+
+
+
+
@@ -200,34 +246,26 @@ const presets = [
- -
-
Edge state · arrows denote likes
+
+
Graph
- - + + - + likes + likes + likes - Alice @@ -248,64 +286,49 @@ const presets = [ Laptop item - -
+
- -
- Run a step — watch the edges +
+
+ + +
+
- {presets.map((c, i) => ( - ))} -
-
- - - -
-
-

- Serving over a million requests per minute, in production at Kakao. -

-
- #real-time - #low-latency - #high-throughput +
- -
-
-

Features

-
-
- { - features.map((f) => ( -
-

{f.title}

-

{f.desc}

-
- )) - } -
-
-
-
- ArchitectureClient → Actionbase → storage & stream +
+

Architecture

+ Client → Actionbase → storage & stream
Client
@@ -375,7 +398,7 @@ const presets = [
-

Ready to build?

+

Start building

The reference covers concepts, architecture, and hands-on guides — from a single write to a million reads per minute. @@ -565,40 +588,69 @@ const presets = [ margin: 0 auto; padding: 48px var(--gutter) 40px; } - /* 2×2 hero: copy | console / graph | run buttons */ - .hero-grid { - display: grid; - grid-template-columns: 1.05fr 1fr; - grid-template-areas: - 'copy term' - 'graph btns'; - column-gap: 44px; - row-gap: 26px; - align-items: start; + .hero-copy { + max-width: 1000px; + } + + /* "Try it" section: [console | graph] over a horizontal control bar */ + .demo { + max-width: var(--maxw); + margin: 0 auto; + padding: 60px var(--gutter) 20px; } - .cell-copy { - grid-area: copy; - align-self: center; + .demo-head { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + margin-bottom: 22px; } - .cell-term { - grid-area: term; + .demo-head h2 { + font-family: var(--font-head); + font-weight: 700; + font-size: 34px; + letter-spacing: -0.02em; + margin: 0; } - .cell-graph { - grid-area: graph; + .demo-panels { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + align-items: stretch; + margin-bottom: 20px; } - .cell-btns { - grid-area: btns; - align-self: stretch; + .demo-controls { display: flex; flex-direction: column; - gap: 12px; + gap: 14px; } - .run-label { + .tabs { + display: flex; + gap: 4px; + border-bottom: 1px solid var(--line-soft); + } + .tab { + margin-bottom: -1px; + padding: 8px 14px; + background: none; + border: none; + border-bottom: 2px solid transparent; font-family: var(--font-mono); - font-size: 11px; - letter-spacing: 0.12em; + font-size: 12px; + letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); + cursor: pointer; + } + .tab.active { + color: var(--ink); + border-bottom-color: var(--accent); + } + .tab-panel[hidden] { + display: none; + } + .soon-heading { + display: none; } .title { font-family: var(--font-head); @@ -613,7 +665,6 @@ const presets = [ font-size: 19px; line-height: 1.58; color: var(--body); - max-width: 36ch; margin: 0 0 34px; text-wrap: pretty; } @@ -695,13 +746,31 @@ const presets = [ text-anchor: middle; } .gedge line { - stroke: var(--line-strong); - stroke-width: 1.6; + stroke: var(--line-mid); + stroke-width: 1.25; + stroke-linecap: round; transition: stroke 0.25s, stroke-width 0.25s, opacity 0.25s; } + .gedge-label { + fill: var(--muted); + font-family: var(--font-mono); + font-size: 7px; + letter-spacing: 0.04em; + text-anchor: middle; + transition: fill 0.25s; + } + .gedge.on .gedge-label { + fill: var(--accent); + } + /* count numbering: the label becomes the ordinal (1, 2, …) on each edge */ + .gedge-label.num { + fill: var(--accent); + font-size: 10px; + font-weight: 700; + } /* reactive states, toggled from the console */ .gnode.dim, .gedge.dim { @@ -709,7 +778,7 @@ const presets = [ } .gedge.on line { stroke: var(--accent); - stroke-width: 2.6; + stroke-width: 2; } .gnode.on rect { stroke: var(--accent); @@ -719,20 +788,6 @@ const presets = [ .gnode.on .gn-name { fill: var(--accent); } - .gbadge[hidden] { - display: none; - } - .gbadge circle { - fill: var(--accent); - } - .gbadge text { - fill: #fff; - font-family: var(--font-mono); - font-size: 12px; - font-weight: 700; - text-anchor: middle; - dominant-baseline: central; - } /* console */ .console { margin: 0; @@ -851,18 +906,28 @@ const presets = [ .presets { display: flex; flex-direction: column; - align-items: stretch; gap: 8px; } .preset { + position: relative; + display: flex; + flex-direction: column; + gap: 3px; text-align: left; background: var(--surf-card); border: 1px solid var(--line-mid); - padding: 8px 11px; + padding: 9px 11px; + cursor: pointer; + } + .preset-cmd { font-family: var(--font-mono); font-size: 11px; color: var(--body); - cursor: pointer; + } + .preset-desc { + font-family: var(--font-body); + font-size: 11px; + color: var(--muted); } .preset:hover:not(:disabled) { border-color: var(--line-strong); @@ -871,9 +936,31 @@ const presets = [ opacity: 0.4; cursor: not-allowed; } - .preset-reset { + .preset.active { + border-color: var(--accent); + background: var(--accent-fill); + } + .preset.active .preset-cmd { color: var(--accent); } + /* teaser: query ops that aren't shipped yet, shown disabled with a TBD tag */ + .preset-soon { + padding-right: 46px; + } + .preset-soon:disabled { + opacity: 0.6; + } + .tbd { + position: absolute; + top: 8px; + right: 10px; + font-size: 9px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--muted); + border: 1px solid var(--line-mid); + padding: 1px 5px; + } /* trust band */ .band { @@ -972,17 +1059,6 @@ const presets = [ padding-top: 26px; padding-bottom: 74px; } - .arch-kicker { - display: flex; - justify-content: space-between; - align-items: baseline; - margin-bottom: 28px; - font-family: var(--font-mono); - font-size: 11px; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--muted); - } .arch-panel { border: 1px solid var(--line-mid); background: var(--surf-soft); @@ -1187,15 +1263,79 @@ const presets = [ gap: 22px; } + /* On desktop the console fills its panel row to match the graph height. + Its content is taken out of flow (absolute) so running commands never + grows the cell — the body just clips (overflow:hidden) and auto-scrolls. + Mobile keeps the fixed body height. */ + @media (min-width: 901px) { + .term-fill { + position: relative; + } + .term-fill [data-console] { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + } + .term-fill .term { + flex: 1 1 0; + min-height: 0; + display: flex; + flex-direction: column; + } + .term-fill .term-body { + flex: 1 1 0; + min-height: 0; + height: auto; + } + /* full-width rows: command on the left, dimmed description on the right */ + .preset { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 16px; + } + .preset-desc { + text-align: right; + } + } + /* responsive */ @media (max-width: 900px) { .title { font-size: 44px; } - .hero-grid { + .demo-panels { grid-template-columns: 1fr; - grid-template-areas: 'copy' 'term' 'graph' 'btns'; - row-gap: 22px; + } + /* No tabs on mobile: show supported buttons as-is, then list the + upcoming queries as plain descriptions (no buttons). */ + .tabs { + display: none; + } + .tab-panel[hidden] { + display: block; + } + .tab-panel[data-panel='soon'] { + margin-top: 22px; + } + .soon-heading { + display: block; + font-family: var(--font-mono); + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--muted); + margin-bottom: 10px; + } + .tab-panel[data-panel='soon'] .preset-soon { + border: none; + background: none; + padding: 3px 0; + opacity: 0.85; + } + .tab-panel[data-panel='soon'] .tbd { + display: none; } .features, .steps { @@ -1278,18 +1418,24 @@ const presets = [ // visible: get highlights one edge, scan/count highlight a fan of them. const graphEl = document.querySelector('[data-graph]'); const NAME = { Alice: 'alice', Bob: 'bob', Phone: 'phone', Laptop: 'laptop' }; - const NODE_CORNER = { alice: [118, 52], bob: [118, 156], phone: [316, 46], laptop: [316, 164] }; const gEdges = graphEl ? [...graphEl.querySelectorAll('.gedge')] : []; const gNodes = graphEl ? [...graphEl.querySelectorAll('.gnode')] : []; - const gBadge = graphEl ? graphEl.querySelector('[data-badge]') : null; - const gBadgeC = graphEl ? graphEl.querySelector('[data-badge-c]') : null; - const gBadgeT = graphEl ? graphEl.querySelector('[data-badge-t]') : null; const edgeId = (e) => NAME[e.source] + '-' + NAME[e.target]; + // every edge label reads "likes" until count numbers the matched ones 1, 2, … + const resetLabels = () => { + gEdges.forEach((e) => { + const t = e.querySelector('.gedge-label'); + if (t) { + t.textContent = 'likes'; + t.classList.remove('num'); + } + }); + }; const clearGraph = () => { gEdges.forEach((e) => e.classList.remove('on', 'dim')); gNodes.forEach((n) => n.classList.remove('on', 'dim')); - if (gBadge) gBadge.setAttribute('hidden', ''); + resetLabels(); }; const focusGraph = (edgeIds, nodeIds) => { const es = new Set(edgeIds); @@ -1304,17 +1450,18 @@ const presets = [ n.classList.toggle('on', on); n.classList.toggle('dim', !on); }); - if (gBadge) gBadge.setAttribute('hidden', ''); + resetLabels(); }; - const showBadge = (nodeId, n) => { - if (!gBadge || !NODE_CORNER[nodeId]) return; - const [x, y] = NODE_CORNER[nodeId]; - gBadgeC.setAttribute('cx', x); - gBadgeC.setAttribute('cy', y); - gBadgeT.setAttribute('x', x); - gBadgeT.setAttribute('y', y); - gBadgeT.textContent = String(n); - gBadge.removeAttribute('hidden'); + // count: label the matched edges 1, 2, … so the total reads off the graph + const numberEdges = (edgeIds) => { + edgeIds.forEach((id, i) => { + const el = gEdges.find((e) => e.dataset.edge === id); + const t = el && el.querySelector('.gedge-label'); + if (t) { + t.textContent = String(i + 1); + t.classList.add('num'); + } + }); }; const addLine = (kind, text, withPrompt) => { @@ -1375,11 +1522,9 @@ const presets = [ const m = edges.filter((e) => (dir === 'IN' ? e.target === f.start : e.source === f.start)); addLine('ok', '→ ' + m.length); const startId = NAME[f.start]; - focusGraph( - m.map(edgeId), - [startId, ...m.map((e) => (dir === 'IN' ? NAME[e.source] : NAME[e.target]))], - ); - showBadge(startId, m.length); + const ids = m.map(edgeId); + focusGraph(ids, [startId, ...m.map((e) => (dir === 'IN' ? NAME[e.source] : NAME[e.target]))]); + numberEdges(ids); } else if (cmd === 'scan') { const dir = String(f.direction || 'OUT').toUpperCase(); const m = edges @@ -1405,50 +1550,42 @@ const presets = [ } }; - // Guided tour: the three presets unlock one at a time, in order; a reset - // appears once the tour has started and returns the console to step one. + // Any step runs on click — no forced order, no reset. The clicked + // button takes an active state; TBD teasers stay disabled via markup. const stepButtons = [...document.querySelectorAll('[data-step]')]; - const resetButton = document.querySelector('[data-reset]'); - const initialLines = [ - ['sys', 'actionbase standalone · server :8080 · CLI ready'], - ['sys', "preset 'likes' loaded — 3 edges"], - ['sys', 'Alice → Phone Bob → Laptop Bob → Phone'], - ['dim', 'run a step → watch the graph'], - ]; - let step = 0; - - const syncButtons = () => { - stepButtons.forEach((b, i) => { - b.disabled = i !== step; - }); - if (resetButton) resetButton.hidden = step === 0; - }; - - stepButtons.forEach((btn, i) => { + stepButtons.forEach((btn) => { btn.addEventListener('click', () => { - if (i !== step) return; + stepButtons.forEach((b) => b.classList.remove('active')); + btn.classList.add('active'); run(btn.getAttribute('data-cmd')); - step = Math.min(step + 1, stepButtons.length); - syncButtons(); }); }); - - resetButton?.addEventListener('click', () => { - edges = preset.slice(); - linesEl.replaceChildren(); - initialLines.forEach(([k, t]) => addLine(k, t, false)); - clearGraph(); - step = 0; - syncButtons(); - }); - - syncButtons(); // Pin to the bottom now, on the next frame, and again once the tiny // 6.5px banner webfont swaps in (which changes the scroll height). scrollToEnd(); requestAnimationFrame(scrollToEnd); if (document.fonts && document.fonts.ready) document.fonts.ready.then(scrollToEnd); } + + // Try-it tabs: switch between the supported and coming-soon panels. + const tabList = document.querySelector('[data-tabs]'); + if (tabList) { + const tabs = [...tabList.querySelectorAll('.tab')]; + const panels = [...document.querySelectorAll('.tab-panel')]; + tabs.forEach((tab) => { + tab.addEventListener('click', () => { + const name = tab.dataset.tab; + tabs.forEach((t) => { + const on = t === tab; + t.classList.toggle('active', on); + t.setAttribute('aria-selected', String(on)); + }); + panels.forEach((p) => { + p.hidden = p.dataset.panel !== name; + }); + }); + }); + } From a0f6f2a4dcc7990ad3efc50ac204db90b3af5eb0 Mon Sep 17 00:00:00 2001 From: Minseok Kim Date: Thu, 23 Jul 2026 18:50:49 +0900 Subject: [PATCH 06/27] feat(website): realistic demo console, graph redesign, unified chrome MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Landing: - Console output now mirrors the real CLI for get/scan/count — request line, 200 response, aligned result table and timing, with the actionbase(likes:likes)> prompt; the banner leading is fixed to a 2:1 cell so the otter art connects; a Reset control sits by the section title. - Graph reskinned to the handoff: dotted board, card nodes that glow when active, marching-ants dashed active edges (no arrowheads), "likes" labels; count still numbers the matched edges. - Architecture is now a connected SVG (Client → Actionbase → write Kafka / read request → one storage backend → CDC Kafka), no broken connectors. - Features cards regain 01–06 ordinals; header GitHub is a round icon button sized/placed to match the theme toggle. Docs (Starlight): - GitHub moves from a text nav link to a round social icon next to the toggle (divider removed, icon centered); header background matches the page. - Footer bar spans content + on-this-page rail (not the nav) as one band; the content↔rail vertical divider is dropped. - Active sidebar item and the Main/Stories switcher drop the rounded pill for a flat accent underline / left bar. Co-Authored-By: Claude Opus 4.8 (1M context) --- website/astro.config.mjs | 17 +- website/src/assets/landing.css | 55 ++-- website/src/components/Footer.astro | 10 +- website/src/pages/index.astro | 466 ++++++++++++++-------------- 4 files changed, 277 insertions(+), 271 deletions(-) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index c13ee8ce0..2b235bfe8 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -85,7 +85,7 @@ export default defineConfig({ editLink: { baseUrl: 'https://github.com/kakao/actionbase/edit/main/website/', }, - social: [], + social: [{ icon: 'github', label: 'GitHub', href: 'https://github.com/kakao/actionbase' }], head: [ { tag: 'link', @@ -127,16 +127,6 @@ export default defineConfig({ customCss: ['./src/assets/landing.css'], locales, sidebar: [ - { - label: 'Nav', - items: [ - { - label: 'GitHub', - link: 'https://github.com/kakao/actionbase', - attrs: { class: 'nav-external', target: '_blank', rel: 'noopener' }, - }, - ], - }, { label: 'Main', items: [ @@ -215,11 +205,6 @@ export default defineConfig({ multiSidebar: { switcherStyle: 'horizontalList', }, - navLinks: { - leading: { - useSidebarLabelled: 'Nav', - }, - }, }), starlightLinksValidator({ errorOnFallbackPages: false, diff --git a/website/src/assets/landing.css b/website/src/assets/landing.css index 0fb9a9789..34867259f 100644 --- a/website/src/assets/landing.css +++ b/website/src/assets/landing.css @@ -82,10 +82,8 @@ body { /* ---------- Header (sticky, translucent, blurred) ---------- */ .header { - background: var(--sl-color-bg-nav); - -webkit-backdrop-filter: blur(12px); - backdrop-filter: blur(12px); - border-bottom: 1px solid var(--sl-color-hairline); + /* match the page background so the header and body read as one surface */ + background: var(--sl-color-bg); } /* ---------- Sidebar ---------- */ @@ -105,9 +103,15 @@ body { .sidebar-content a[aria-current='page'] { color: var(--sl-color-text-accent); - background: var(--ab-accent-fill); + background: transparent; font-weight: 700; - box-shadow: inset 2px 0 0 var(--sl-color-accent); + border-radius: 0; + box-shadow: inset 3px 0 0 var(--sl-color-accent); +} + +/* Sidebar switcher (Main / Stories): flat accent underline, no rounded pill */ +li.tab { + border-radius: 0 !important; } /* ---------- Right rail (on this page) ---------- */ @@ -179,21 +183,30 @@ body { display: none; } -/* Align nav links vertically when the shields.io badge is present */ -ul:has(.nav-external) { - align-items: center; +/* GitHub social icon as a round bordered button, matching the theme toggle + (and the landing nav) so the header controls read as one set. */ +/* no divider bar before the theme toggle */ +.social-icons::after { + display: none !important; } - -.nav-external { - display: inline-flex; +.social-icons a { + display: flex; align-items: center; - gap: 6px; + justify-content: center; + padding: 0; + width: 32px; + height: 32px; + border-radius: 999px; + border: 1px solid var(--sl-color-hairline-light); + color: var(--sl-color-gray-2); } - -.gh-stars { - font-family: var(--sl-font-mono); - font-size: 0.75rem; - color: var(--sl-color-gray-3); +.social-icons a:hover { + border-color: var(--sl-color-gray-4); + color: var(--sl-color-gray-2); +} +.social-icons a svg { + width: 18px !important; + height: 18px !important; } /* WIP items in sidebar */ @@ -201,6 +214,12 @@ ul:has(.nav-external) { opacity: 0.5; } +/* Drop the vertical divider between content and the on-this-page rail (keep + the menu↔content one) so the footer band reads across both. */ +.right-sidebar { + border-inline-start: 0 !important; +} + /* Docs header logo: the shared wordmark SVG is wide/short, and Starlight renders it at 40px by default — size it down to match the landing nav (26px). */ .site-title img { diff --git a/website/src/components/Footer.astro b/website/src/components/Footer.astro index b82eb0eaf..2c191a899 100644 --- a/website/src/components/Footer.astro +++ b/website/src/components/Footer.astro @@ -13,13 +13,19 @@ import StarlightFooter from '@astrojs/starlight/components/Footer.astro'; diff --git a/website/src/components/ThemeSelect.astro b/website/src/components/ThemeSelect.astro index 75fa2891b..d56472685 100644 --- a/website/src/components/ThemeSelect.astro +++ b/website/src/components/ThemeSelect.astro @@ -13,8 +13,8 @@ display: inline-flex; align-items: center; justify-content: center; - width: 32px; - height: 32px; + width: 44px; + height: 44px; border-radius: 999px; border: 1px solid var(--sl-color-hairline-light); background: var(--sl-color-bg-nav); diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index 1983ef17b..d24a3b531 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -1,6 +1,7 @@ --- import logoLight from '../assets/logo-light.svg?url'; import logoDark from '../assets/logo-dark.svg?url'; +import { Check, Copy, RotateCcw } from '@lucide/astro'; // Standalone marketing landing. Kept outside Starlight's content collection so // the bespoke chrome (marginalia, hero console, architecture flow) has full @@ -54,13 +55,16 @@ const banner = [ goldSpan('╰' + '─'.repeat(BANNER_WIDTH) + '╯'), ].join('\n'); -const features = [ +const primaryFeatures = [ { title: 'Pre-computed Reads', desc: 'Writes do the work. Reads return what’s already computed.' }, { title: 'U2U · U2I · I2I', desc: 'One model for user–user, user–item, and item–item interactions.' }, + { title: 'Battle-tested', desc: 'Over a million requests per minute in stable production at Kakao.' }, +]; + +const platformFeatures = [ { title: 'REST API', desc: 'Simple writes, fast reads — all via HTTP.' }, { title: 'CDC Built-in', desc: 'Stream changes for downstream pipelines.' }, { title: 'Pluggable Storage', desc: 'A minimal storage abstraction — HBase today, SlateDB planned.' }, - { title: 'Battle-tested', desc: 'Over a million requests per minute at Kakao.' }, ]; // Both paths converge on one wide Storage Backend. Kafka sits on the write side @@ -160,6 +164,8 @@ const soon = [

@@ -202,29 +208,14 @@ const soon = [
- -
-
-

Features

-
-
- { - features.map((f, i) => ( -
- {String(i + 1).padStart(2, '0')} -

{f.title}

-

{f.desc}

-
- )) - } -
-
-

Interactive demo

- +
@@ -325,6 +316,42 @@ const soon = [
+ +
+
+

Why Actionbase

+ Built for interaction data +
+
+ Core advantages +
+ { + primaryFeatures.map((f, i) => ( +
+ {String(i + 1).padStart(2, '0')} +

{f.title}

+

{f.desc}

+
+ )) + } +
+
+
+ Platform capabilities +
+ { + platformFeatures.map((f, i) => ( +
+ {String(i + 4).padStart(2, '0')} +

{f.title}

+

{f.desc}

+
+ )) + } +
+
+
+
@@ -344,7 +371,7 @@ const soon = [ d="M310 52 V80 M310 124 V152 M155 152 H465 M155 152 V180 M465 152 V196 M465 228 V286 M155 226 V286 M155 334 V390" /> - REST · HTTP + REST · HTTP WRITE PATH READ PATH @@ -386,8 +413,20 @@ const soon = [
{s.n} {s.label} + +
+
+
{s.code}
-
{s.code}
)) } @@ -408,8 +447,8 @@ const soon = [ million reads per minute.

@@ -419,6 +458,8 @@ const soon = [