This is an automated email from the ASF dual-hosted git repository.
PsiACE pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git
The following commit(s) were added to refs/heads/main by this push:
new 32a67d2bb feat(website): introduce a design system and rebuild the
homepage (#7638)
32a67d2bb is described below
commit 32a67d2bb30d007d5fc790dfa56f05b629625617
Author: Xuanwo <[email protected]>
AuthorDate: Sat May 30 06:13:54 2026 +0800
feat(website): introduce a design system and rebuild the homepage (#7638)
* feat(website): add design system and landing page
Add a token-based design system in src/css/custom.css (color,
typography, spacing, elevation, motion, and the OpenDAL bar motif),
projected onto Infima so docs and blog inherit it, and document it in
DESIGN_SYSTEM.md.
Add a developer-focused landing page at "/" (hero, adopters, value
props, the Operator contract, services, bindings, layers, community,
CTA). Move docs from "/" to "/docs/" with backward-compatible redirects
for legacy URLs, and enable the light/dark theme toggle.
* feat(website): add adopter logo wall
Add a "Used by" logo wall of real OpenDAL adopters taken from the core
and binding users lists, ordered by GitHub star count and rendered as
grayscale org logos (self-hosted under static/img/users/) that colorize
on hover. The wall shows a responsive count by viewport (4/6/8) and an
"+ add your logo" link to the users list.
Also show the Java quickstart dependency as a plain Maven coordinate
instead of a Gradle-specific implementation(...) line.
* feat(website): replace operator verb table with a capabilities section
Swap the abstract Operator verb table for a Capabilities section that
groups what OpenDAL can do — Read, Write, Manage — drawn from the core
features: byte-range and concurrent reads, adjacent-range merge, resumable
reads, multipart and concurrent uploads, append, conditional writes,
listing, batch delete, server-side copy/rename and presigned URLs.
Also drop the duplicate hero code window and the services caption.
* feat(website): add interactive capabilities explorer and used-by wall
* feat(website): make layers an interactive explorer and polish landing
sections
---
website/DESIGN_SYSTEM.md | 230 +++++++
website/docusaurus.config.js | 23 +-
website/src/components/landing/CodeTabs.jsx | 112 ++++
website/src/components/landing/data.js | 443 ++++++++++++++
website/src/components/landing/sections.jsx | 415 +++++++++++++
website/src/components/landing/styles.module.css | 744 +++++++++++++++++++++++
website/src/css/custom.css | 446 ++++++++++++--
website/src/pages/index.jsx | 51 ++
website/static/img/users/cratedb.png | Bin 0 -> 711 bytes
website/static/img/users/daft.png | Bin 0 -> 6745 bytes
website/static/img/users/databend.png | Bin 0 -> 3579 bytes
website/static/img/users/datafusion-comet.png | Bin 0 -> 7018 bytes
website/static/img/users/dify.png | Bin 0 -> 2218 bytes
website/static/img/users/gravitino.png | Bin 0 -> 7018 bytes
website/static/img/users/greptimedb.png | Bin 0 -> 6539 bytes
website/static/img/users/hasura.png | Bin 0 -> 5428 bytes
website/static/img/users/iceberg-rust.png | Bin 0 -> 7018 bytes
website/static/img/users/kubeflow-trainer.png | Bin 0 -> 9460 bytes
website/static/img/users/lance.png | Bin 0 -> 3328 bytes
website/static/img/users/llamaindex.png | Bin 0 -> 6890 bytes
website/static/img/users/lnx.png | Bin 0 -> 1895 bytes
website/static/img/users/loco.png | Bin 0 -> 5092 bytes
website/static/img/users/octobase.png | Bin 0 -> 11292 bytes
website/static/img/users/openraft.png | Bin 0 -> 3579 bytes
website/static/img/users/pants.png | Bin 0 -> 2676 bytes
website/static/img/users/pathway.png | Bin 0 -> 20187 bytes
website/static/img/users/questdb.png | Bin 0 -> 3767 bytes
website/static/img/users/quickwit.png | Bin 0 -> 3667 bytes
website/static/img/users/ragflow.png | Bin 0 -> 9257 bytes
website/static/img/users/risingwave.png | Bin 0 -> 5562 bytes
website/static/img/users/robustmq.png | Bin 0 -> 1540 bytes
website/static/img/users/rustic.png | Bin 0 -> 12204 bytes
website/static/img/users/sccache.png | Bin 0 -> 1356 bytes
website/static/img/users/seatunnel.png | Bin 0 -> 7018 bytes
website/static/img/users/slatedb.png | Bin 0 -> 4332 bytes
website/static/img/users/spiceai.png | Bin 0 -> 4518 bytes
website/static/img/users/vaultwarden.png | Bin 0 -> 18431 bytes
website/static/img/users/vector.png | Bin 0 -> 8211 bytes
website/static/img/users/walrus.png | Bin 0 -> 7880 bytes
website/static/img/users/wrenai.png | Bin 0 -> 2330 bytes
website/static/img/users/zino.png | Bin 0 -> 4517 bytes
41 files changed, 2403 insertions(+), 61 deletions(-)
diff --git a/website/DESIGN_SYSTEM.md b/website/DESIGN_SYSTEM.md
new file mode 100644
index 000000000..e4636087f
--- /dev/null
+++ b/website/DESIGN_SYSTEM.md
@@ -0,0 +1,230 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+# OpenDAL Design System
+
+> **One Layer, All Storage.** A precise, engineering-grade visual language for
+> Apache OpenDAL™ — built to feel as dependable, fast and composable as the
+> library itself.
+
+This document is the source of truth for OpenDAL's web visual language. The
+landing page (`src/pages/index.jsx`) is its first full application; docs and
+blog inherit it through Infima variable mapping.
+
+---
+
+## 1. Design principles
+
+The visual system is a direct translation of OpenDAL's engineering values.
+
+| Library value | Visual expression
|
+| ------------------------- |
---------------------------------------------------------------------------- |
+| **One Layer, All Storage**| One accent, one type voice, one grid —
convergence over decoration. |
+| **Solid Foundation** | Hairline structure, precise alignment,
restraint. Nothing is decorative-only.|
+| **Fast Access (zero-cost)**| System fonts, no web-font requests, CSS-only
motion. The page costs nothing it doesn't need. |
+| **Object Storage First** | Content-first layout; code and capabilities
lead, ornament follows. |
+| **Extensible Architecture**| Tokens compose; components are presentational
and reusable. |
+
+Operating posture: **engineering-minimal, light-first** (dark mode is a
+first-class, fully designed peer), near-monochrome **ink** with a single
+**precise-blue** accent, and the signature solid **bar** lifted from the
+OpenDAL wordmark.
+
+---
+
+## 2. Where the system lives
+
+| Concern | Location |
+| ------------------------------- | ----------------------------------------- |
+| Tokens + Infima mapping + motif | `src/css/custom.css` |
+| Landing sections | `src/components/landing/sections.jsx` |
+| Code window / language tabs | `src/components/landing/CodeTabs.jsx` |
+| Landing styles | `src/components/landing/styles.module.css`|
+| Content model (copy, catalogs) | `src/components/landing/data.js` |
+
+All design decisions are encoded as CSS custom properties prefixed `--odl-*`.
+Components never hardcode raw hex; they consume tokens.
+
+---
+
+## 3. Color
+
+### 3.1 Primitives
+
+**Ink** — a cool, neutral near-black → white ramp (the monochrome backbone):
+
+`--odl-ink-{0,50,100,150,200,300,400,500,600,700,800,850,900,950}`
+from `#ffffff` to `#0a0d11`.
+
+**Precise blue** — the single brand accent:
+
+`--odl-blue-{50…900}` from `#edf3ff` to `#163172`; `--odl-blue-600 #1e54e0` is
+the canonical light-mode accent.
+
+Functional signals (`--odl-green/amber/red-*`) exist mostly so inherited docs
+UI (admonitions, etc.) stay on-system. They are intentionally muted.
+
+### 3.2 Semantic roles (theme-aware)
+
+Components consume **semantic** tokens, not primitives:
+
+| Token | Light | Dark | Use
|
+| --------------------- | -------------------- | ----------------- |
---------------------------- |
+| `--odl-bg` | `ink-0` | `#0a0d11` | Page
background |
+| `--odl-bg-subtle` | `ink-50` | `#0e1218` |
Alternating section bands |
+| `--odl-surface` | `ink-0` | `#11161c` | Cards,
code window |
+| `--odl-fg` | `ink-900` | `#e7ebf0` | Body text
|
+| `--odl-fg-muted` | `ink-600` | `#9aa6b3` | Secondary
text |
+| `--odl-border` | `ink-200` | `#242c35` |
Hairlines, dividers |
+| `--odl-bar` | `ink-950` | `#f6f8fa` | The bar
motif |
+| `--odl-accent` | `blue-600` | `blue-300` | Links,
emphasis |
+| `--odl-action-bg` | `blue-600` | `blue-500` | Solid CTA
background |
+
+### 3.3 Rules
+
+- **Black/white leads, blue accents.** Blue is for interaction, emphasis and
+ code highlight — never large fills (except the solid CTA).
+- **Contrast:** body and accent text meet WCAG AA (≥ 4.5:1) in both themes;
+ white-on-`action-bg` passes for buttons. Dark mode lifts the accent to a
+ lighter blue rather than inverting it.
+- **Never rely on color alone** — pair with the bar, an icon, or a label.
+
+---
+
+## 4. Typography
+
+### 4.1 Stacks & rationale
+
+```
+--odl-font-sans: system-ui, -apple-system, "Segoe UI", Roboto, …
+--odl-font-mono: ui-monospace, "SF Mono", "Cascadia Code", "JetBrains Mono", …
+```
+
+System stacks are a **deliberate** choice, not a fallback: zero third-party
+requests (ASF privacy-friendly), zero layout shift, instant and globally
+consistent rendering (OpenDAL has a large global user base). The wordmark
+already carries the brand letterforms.
+
+**Monospace is promoted to a brand voice.** It marks anything structural or
+technical — eyebrows/kickers, section indices, statistics, service names, code.
+This gives the minimal system its engineering character without a custom font.
+
+> To adopt **IBM Plex** instead: self-host via `@fontsource/ibm-plex-sans` and
+> `@fontsource/ibm-plex-mono`, `@import` the weights in `custom.css`, and set
+> `--odl-font-sans`/`--odl-font-mono` accordingly. The rest of the system needs
+> no changes.
+
+### 4.2 Scale
+
+`--odl-text-2xs (11px)` · `xs (12)` · `sm (14)` · `base (16)` · `md (18)` ·
+`lg (22)` · `xl (28)` · `2xl (36)` · `3xl (48)` · `display clamp(44–80px)`.
+
+Headings: weight `650–700`, tracking `-0.022em`. Body: `400`, line-height
+`1.6`. Mono labels: uppercase, tracking `0.12em`.
+
+---
+
+## 5. Spacing & layout
+
+- **4px rhythm:** `--odl-space-1…10` (`0.25rem` → `8rem`).
+- **Container:** `--odl-container 1200px`, `--odl-container-narrow 880px`,
+ fluid `--odl-gutter clamp(1.25–2.5rem)`. Use the global `.odl-container`.
+- **Section rhythm:** vertical padding `clamp(3.5rem, 7rem)`, every section
+ separated by a single hairline (`border-top: 1px var(--odl-border)`),
+ alternating with `--odl-bg-subtle` bands.
+
+---
+
+## 6. Radius & elevation
+
+- **Radius:** `--odl-radius-xs 3 / sm 5 / md 8 / lg 12 / pill`. Restrained by
+ design. **The bar motif is always sharp (radius 0).**
+- **Elevation:** `--odl-shadow-sm/md/lg`. Prefer **border-based** separation;
+ reserve real shadow for floating surfaces (the code window).
+
+---
+
+## 7. Motion
+
+- **Tokens:** `--odl-dur-fast 120ms / dur 200ms / dur-slow 320ms`;
+ `--odl-ease-out cubic-bezier(0.16,1,0.3,1)`, `--odl-ease`.
+- **Micro-interactions** 120–200ms (hover, tab change, button press).
+- **Reveal-on-scroll** (`.reveal`) is **pure CSS** via `animation-timeline:
+ view()` — progressive enhancement: browsers without support show content
+ immediately, and `prefers-reduced-motion` disables it entirely.
+- Animate only `opacity`/`transform`. Never block input.
+
+---
+
+## 8. The bar motif
+
+The solid rectangle from the OpenDAL wordmark is the system's signature mark.
+
+- `.odl-bar` — a sharp solid rectangle for dividers/emphasis.
+- `.odl-eyebrow` — the canonical section kicker: monospace, uppercase, led by a
+ small **accent bar**. Used above every section title.
+- As a top-border accent on principle cards, and as the section-title marker.
+
+Used consistently, the bar makes the minimal system unmistakably OpenDAL.
+
+---
+
+## 9. Component patterns
+
+| Component | Class(es) | Notes
|
+| ---------------- | ------------------------------------------ |
------------------------------------------------ |
+| Primary button | `.btn .btnPrimary` | Solid
`action-bg`; one primary action per view. |
+| Secondary button | `.btn .btnSecondary` | Hairline
outline, ink text. |
+| On-dark buttons | `.btnOnDark` / `.btnOnDarkGhost` | For the dark
CTA panel. |
+| Eyebrow | `.odl-eyebrow` (global) | Mono +
accent bar; section kicker. |
+| Code window | `CodeTabs` | Window
chrome + language tabs + Prism highlight. |
+| Value card | `.valueCard` | Mono index,
title, body; 1px-gap grid. |
+| Service chip | `.serviceChip` | Mono name +
15px icon, grouped by family. |
+| Binding / layer | `.bindingCard` / `.layerItem` | Icon + mono
label in a hairline grid. |
+
+Layout grids use a **1px gap over a border-colored background** to render crisp
+hairline dividers between cells — a precise, technical signature.
+
+---
+
+## 10. Do / Don't
+
+**Do**
+- Lead with ink; use blue for interaction and emphasis.
+- Use monospace for labels, indices, figures and code.
+- Keep one primary CTA per view; separate secondary actions.
+- Reach for hairlines and spacing before shadows and fills.
+- Design and test light **and** dark together.
+
+**Don't**
+- Introduce a second accent hue or gradients-as-decoration.
+- Use emoji as icons (use the existing SVG sets in `static/img/*`).
+- Hardcode hex in components — consume `--odl-*` tokens.
+- Add motion that conveys nothing or ignores `prefers-reduced-motion`.
+
+---
+
+## 11. Accessibility checklist
+
+- [ ] Text/accent contrast ≥ 4.5:1 (AA) in light and dark.
+- [ ] Visible `:focus-visible` ring (accent, 2px) on every interactive element.
+- [ ] Decorative icons use empty `alt`; meaning carried by adjacent text.
+- [ ] Code tabs are a proper `tablist`/`tab`/`tabpanel` with arrow-key support.
+- [ ] `prefers-reduced-motion` respected globally.
+- [ ] Layout holds at 375 / 768 / 1024 / 1440px without horizontal scroll.
diff --git a/website/docusaurus.config.js b/website/docusaurus.config.js
index d32ff7f18..72e659482 100644
--- a/website/docusaurus.config.js
+++ b/website/docusaurus.config.js
@@ -96,7 +96,7 @@ const config = {
/** @type {import('@docusaurus/preset-classic').Options} */
({
docs: {
- routeBasePath: "/",
+ routeBasePath: "docs",
sidebarPath: require.resolve("./docs/sidebars.js"),
editUrl: "https://github.com/apache/opendal/tree/main/website/",
showLastUpdateAuthor: true,
@@ -136,10 +136,6 @@ const config = {
"@docusaurus/plugin-client-redirects",
{
redirects: [
- {
- from: "/docs/vision",
- to: "/vision",
- },
{
from: "/discord",
to: "https://discord.com/invite/XQy8yGR2dg",
@@ -149,6 +145,20 @@ const config = {
to: "https://lists.apache.org/[email protected]",
},
],
+ // The landing page now owns "/", so docs moved from "/" to "/docs/".
+ // Keep every legacy doc URL working by redirecting the old prefix-less
+ // path to its new /docs/ location. The docs home ("/docs/") is
excluded
+ // so it never collides with the landing page at "/".
+ createRedirects(existingPath) {
+ if (existingPath.startsWith("/docs/") && existingPath !== "/docs/") {
+ const legacy = existingPath.replace(/^\/docs\//, "/");
+ // Avoid colliding with root-level pages that are not docs.
+ if (legacy !== "/download/") {
+ return [legacy];
+ }
+ }
+ return undefined;
+ },
},
],
require.resolve("docusaurus-lunr-search"),
@@ -273,7 +283,8 @@ const config = {
// image: 'img/opendal-social-card.jpg',
colorMode: {
defaultMode: "light",
- disableSwitch: true,
+ disableSwitch: false,
+ respectPrefersColorScheme: true,
},
navbar: {
logo: {
diff --git a/website/src/components/landing/CodeTabs.jsx
b/website/src/components/landing/CodeTabs.jsx
new file mode 100644
index 000000000..cff47a506
--- /dev/null
+++ b/website/src/components/landing/CodeTabs.jsx
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useEffect, useId, useRef, useState } from "react";
+import CodeBlock from "@theme/CodeBlock";
+import styles from "./styles.module.css";
+
+// A framed "code window" with language tabs. Wraps Docusaurus' CodeBlock so
the
+// snippets get the same Prism highlighting as the docs.
+//
+// The header doubles as a terminal-style prompt: a sample's `install` command
+// is shown there (tracking the active tab), so install lives in the title bar
+// rather than a separate band. With `equalize`, the code area animates its
+// height to fit the active snippet — no dead space for short snippets, and the
+// left hero column never reflows (its width is locked and it is the taller
+// column, so only the code window resizes).
+export default function CodeTabs({ samples, title, equalize = false }) {
+ const [active, setActive] = useState(0);
+ const baseId = useId();
+ const current = samples[active];
+ const heading = current.install || title;
+
+ const innerRef = useRef(null);
+ const [bodyHeight, setBodyHeight] = useState(undefined);
+
+ useEffect(() => {
+ if (!equalize || !innerRef.current) return undefined;
+ const measure = () => setBodyHeight(innerRef.current?.offsetHeight);
+ measure();
+ const ro =
+ typeof ResizeObserver !== "undefined"
+ ? new ResizeObserver(measure)
+ : null;
+ if (ro && innerRef.current) ro.observe(innerRef.current);
+ return () => ro?.disconnect();
+ }, [active, equalize]);
+
+ function onKeyDown(event) {
+ if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
+ event.preventDefault();
+ const dir = event.key === "ArrowRight" ? 1 : -1;
+ setActive((i) => (i + dir + samples.length) % samples.length);
+ }
+
+ return (
+ <div className={styles.codeWindow}>
+ <div className={styles.windowBar}>
+ <div className={styles.windowDots} aria-hidden="true">
+ <span />
+ <span />
+ <span />
+ </div>
+ <span className={styles.windowTitle}>{heading}</span>
+ </div>
+
+ <div
+ className={styles.codeTabs}
+ role="tablist"
+ aria-label="Choose a language"
+ onKeyDown={onKeyDown}
+ >
+ {samples.map((sample, i) => (
+ <button
+ key={sample.id}
+ type="button"
+ role="tab"
+ id={`${baseId}-tab-${sample.id}`}
+ aria-selected={i === active}
+ aria-controls={`${baseId}-panel`}
+ tabIndex={i === active ? 0 : -1}
+ className={`${styles.codeTab} ${
+ i === active ? styles.codeTabActive : ""
+ }`}
+ onClick={() => setActive(i)}
+ >
+ {sample.label}
+ </button>
+ ))}
+ </div>
+
+ <div
+ className={`${styles.codeBody} ${equalize ? styles.codeBodyAnimated :
""}`}
+ style={
+ equalize && bodyHeight != null ? { height: bodyHeight } : undefined
+ }
+ role="tabpanel"
+ id={`${baseId}-panel`}
+ aria-labelledby={`${baseId}-tab-${current.id}`}
+ >
+ <div ref={innerRef}>
+ <CodeBlock language={current.language}>{current.code}</CodeBlock>
+ </div>
+ </div>
+ </div>
+ );
+}
diff --git a/website/src/components/landing/data.js
b/website/src/components/landing/data.js
new file mode 100644
index 000000000..3a7b786fd
--- /dev/null
+++ b/website/src/components/landing/data.js
@@ -0,0 +1,443 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+// Content model for the landing page. All copy and catalog data lives here so
+// the section components stay presentational. Code samples are taken from the
+// real binding APIs so the page never drifts from the libraries it advertises.
+
+export const REPO_URL = "https://github.com/apache/opendal";
+export const DOCS_URL = "/docs/";
+export const DISCORD_URL = "https://discord.gg/XQy8yGR2dg";
+
+export const heroStats = [
+ { value: "50+", label: "services" },
+ { value: "17", label: "languages" },
+ { value: "5k+", label: "stars" },
+ { value: "10M+", label: "downloads" },
+];
+
+// Same Operator contract, one mental model, every ecosystem.
+export const codeSamples = [
+ {
+ id: "rust",
+ label: "Rust",
+ language: "rust",
+ install: "$ cargo add opendal",
+ code: `use opendal::services::S3;
+use opendal::Operator;
+
+// Configure a backend once, then use one Operator.
+let builder = S3::default().bucket("data");
+let op = Operator::new(builder)?.finish();
+
+op.write("hello.txt", "Hello, World!").await?;
+let bytes = op.read("hello.txt").await?;`,
+ },
+ {
+ id: "python",
+ label: "Python",
+ language: "python",
+ install: "$ pip install opendal",
+ code: `import opendal
+
+# Configure a backend once, then use one Operator.
+op = opendal.Operator("s3", bucket="data")
+
+op.write("hello.txt", b"Hello, World!")
+data = op.read("hello.txt")`,
+ },
+ {
+ id: "go",
+ label: "Go",
+ language: "go",
+ install: "$ go get github.com/apache/opendal/bindings/go",
+ code: `import (
+ "github.com/apache/opendal-go-services/s3"
+ opendal "github.com/apache/opendal/bindings/go"
+)
+
+// Configure a backend once, then use one Operator.
+opts := opendal.OperatorOptions{"bucket": "data"}
+op, _ := opendal.NewOperator(s3.Scheme, opts)
+
+op.Write("hello.txt", []byte("Hello, World!"))
+data, _ := op.Read("hello.txt")`,
+ },
+ {
+ id: "java",
+ label: "Java",
+ language: "java",
+ install: "org.apache.opendal:opendal-java",
+ code: `import org.apache.opendal.AsyncOperator;
+import java.util.Map;
+
+// Configure a backend once, then use one Operator.
+var conf = Map.of("bucket", "data");
+try (var op = AsyncOperator.of("s3", conf)) {
+ op.write("hello.txt", "Hello, World!").join();
+ byte[] data = op.read("hello.txt").join();
+}`,
+ },
+ {
+ id: "node",
+ label: "Node.js",
+ language: "javascript",
+ install: "$ npm install opendal",
+ code: `import { Operator } from "opendal";
+
+// Configure a backend once, then use one Operator.
+const op = new Operator("s3", { bucket: "data" });
+
+await op.write("hello.txt", "Hello, World!");
+const data = await op.read("hello.txt");`,
+ },
+];
+
+export const valueProps = [
+ {
+ index: "01",
+ title: "One API, all storage",
+ body: "Object storage, file systems, cloud SaaS, databases, protocols and
key-value services reached through a single Operator and one mental model.",
+ },
+ {
+ index: "02",
+ title: "Zero-cost core",
+ body: "Built in Rust with composable services and layers. Compile in only
the backends and capabilities you use, and pay for nothing else.",
+ },
+ {
+ index: "03",
+ title: "Production-ready by composition",
+ body: "Stack retry, timeout, logging, tracing, metrics, throttling and
concurrency limits as reusable layers — no rewrites, no glue code.",
+ },
+ {
+ index: "04",
+ title: "Open and extensible",
+ body: "Add services, layers and language bindings without forking the
model. Developed in the open and governed the Apache Way.",
+ },
+];
+
+// What OpenDAL can do — grouped for the capabilities explorer. Each item pairs
+// a minimal, real snippet with its docs.rs reference. Grounded in the core
+// read / write / manage APIs (verified against types/operator +
types/options).
+const RS = "https://docs.rs/opendal/latest/opendal";
+const opDoc = (method) => `${RS}/struct.Operator.html#method.${method}`;
+const layerDoc = (name) => `${RS}/layers/struct.${name}.html`;
+
+export const capabilityThemes = [
+ {
+ title: "Read in parallel",
+ blurb: "Fetch a byte range, or a whole object in concurrent chunks.",
+ doc: opDoc("read_with"),
+ code: `let op = Operator::new(S3::default().bucket("data"))?.finish();
+
+// Read just the bytes you need.
+let head = op.read_with("logs/today").range(0..64 * 1024).await?;
+
+// Or pull a large object in parallel chunks.
+let full = op
+ .read_with("big.parquet")
+ .concurrent(8)
+ .chunk(8 * 1024 * 1024)
+ .await?;`,
+ },
+ {
+ title: "Upload in parts",
+ blurb: "Stream writes of any size as concurrent, multipart uploads.",
+ doc: opDoc("writer_with"),
+ code: `let op = Operator::new(S3::default().bucket("data"))?.finish();
+
+// Open a multipart writer with 8 concurrent parts.
+let mut w = op
+ .writer_with("big.bin")
+ .concurrent(8)
+ .chunk(8 * 1024 * 1024)
+ .await?;
+
+// Stream any number of buffers; close flushes the rest.
+w.write(part_one).await?;
+w.write(part_two).await?;
+w.close().await?;`,
+ },
+ {
+ title: "Recover from failure",
+ blurb: "Resume on retry, write atomically, and pin a version.",
+ doc: `${RS}/layers/struct.RetryLayer.html`,
+ code: `// Retries automatically resume interrupted transfers.
+let op = Operator::new(S3::default().bucket("data"))?
+ .layer(RetryLayer::new())
+ .finish();
+
+// Create only if absent.
+op.write_with("once.json", data).if_not_exists(true).await?;
+// Read only if unchanged.
+let doc = op.read_with("doc").if_match(etag).await?;
+// Pin an exact version.
+let pinned = op.read_with("doc").version(version_id).await?;`,
+ },
+ {
+ title: "Work with files",
+ blurb: "Inspect, list, move, and share — without moving bytes.",
+ doc: opDoc("list_with"),
+ code: `// Inspect a file without downloading it.
+let meta = op.stat("report.csv").await?;
+// List a prefix, recursing lazily through the tree.
+let mut entries = op.lister_with("logs/").recursive(true).await?;
+while let Some(entry) = entries.try_next().await? {
+ println!("{}", entry.path());
+}
+// Copy on the server — no download.
+op.copy("draft.md", "final.md").await?;
+// Recursively delete a subtree.
+op.delete_with("tmp/").recursive(true).await?;
+// Presign a temporary, shareable URL.
+let ttl = Duration::from_secs(3600);
+let url = op.presign_read("report.csv", ttl).await?;`,
+ },
+];
+
+// Real adopters from the project's users lists (core + bindings), ordered by
+// GitHub star count (highest first). Logos are the projects' GitHub org
+// avatars, self-hosted under static/img/users/. Refresh with scripts as the
+// ecosystem grows. The logo wall shows a responsive subset by viewport width.
+// Public projects with 1,000+ GitHub stars that depend on OpenDAL, sorted by
+// stars. Sourced via crates.io reverse deps + GitHub code search; logos are
the
+// owner avatars, mirrored locally under static/img/users to avoid runtime
calls.
+export const usedBy = [
+ { name: "Dify", icon: "/img/users/dify.png", href:
"https://github.com/langgenius/dify" },
+ { name: "RAGFlow", icon: "/img/users/ragflow.png", href:
"https://github.com/infiniflow/ragflow" },
+ { name: "Pathway", icon: "/img/users/pathway.png", href:
"https://github.com/pathwaycom/pathway" },
+ { name: "Vaultwarden", icon: "/img/users/vaultwarden.png", href:
"https://github.com/dani-garcia/vaultwarden" },
+ { name: "LlamaIndex", icon: "/img/users/llamaindex.png", href:
"https://github.com/run-llama/llama_index" },
+ { name: "Hasura", icon: "/img/users/hasura.png", href:
"https://github.com/hasura/graphql-engine" },
+ { name: "Vector", icon: "/img/users/vector.png", href:
"https://github.com/vectordotdev/vector" },
+ { name: "QuestDB", icon: "/img/users/questdb.png", href:
"https://github.com/questdb/questdb" },
+ { name: "WrenAI", icon: "/img/users/wrenai.png", href:
"https://github.com/Canner/WrenAI" },
+ { name: "Quickwit", icon: "/img/users/quickwit.png", href:
"https://github.com/quickwit-oss/quickwit" },
+ { name: "SeaTunnel", icon: "/img/users/seatunnel.png", href:
"https://github.com/apache/seatunnel" },
+ { name: "Databend", icon: "/img/users/databend.png", href:
"https://github.com/databendlabs/databend" },
+ { name: "RisingWave", icon: "/img/users/risingwave.png", href:
"https://github.com/risingwavelabs/risingwave" },
+ { name: "Loco", icon: "/img/users/loco.png", href:
"https://github.com/loco-rs/loco" },
+ { name: "sccache", icon: "/img/users/sccache.png", href:
"https://github.com/mozilla/sccache" },
+ { name: "Lance", icon: "/img/users/lance.png", href:
"https://github.com/lance-format/lance" },
+ { name: "GreptimeDB", icon: "/img/users/greptimedb.png", href:
"https://github.com/GreptimeTeam/greptimedb" },
+ { name: "Daft", icon: "/img/users/daft.png", href:
"https://github.com/Eventual-Inc/Daft" },
+ { name: "CrateDB", icon: "/img/users/cratedb.png", href:
"https://github.com/crate/crate" },
+ { name: "Pants", icon: "/img/users/pants.png", href:
"https://github.com/pantsbuild/pants" },
+ { name: "rustic", icon: "/img/users/rustic.png", href:
"https://github.com/rustic-rs/rustic" },
+ { name: "SlateDB", icon: "/img/users/slatedb.png", href:
"https://github.com/slatedb/slatedb" },
+ { name: "Gravitino", icon: "/img/users/gravitino.png", href:
"https://github.com/apache/gravitino" },
+ { name: "Spice.ai", icon: "/img/users/spiceai.png", href:
"https://github.com/spiceai/spiceai" },
+ { name: "Kubeflow Trainer", icon: "/img/users/kubeflow-trainer.png", href:
"https://github.com/kubeflow/trainer" },
+ { name: "OctoBase", icon: "/img/users/octobase.png", href:
"https://github.com/toeverything/OctoBase" },
+ { name: "Openraft", icon: "/img/users/openraft.png", href:
"https://github.com/databendlabs/openraft" },
+ { name: "Walrus", icon: "/img/users/walrus.png", href:
"https://github.com/nubskr/walrus" },
+ { name: "RobustMQ", icon: "/img/users/robustmq.png", href:
"https://github.com/robustmq/robustmq" },
+ { name: "lnx", icon: "/img/users/lnx.png", href:
"https://github.com/lnx-search/lnx" },
+ { name: "Iceberg Rust", icon: "/img/users/iceberg-rust.png", href:
"https://github.com/apache/iceberg-rust" },
+ { name: "DataFusion Comet", icon: "/img/users/datafusion-comet.png", href:
"https://github.com/apache/datafusion-comet" },
+ { name: "zino", icon: "/img/users/zino.png", href:
"https://github.com/zino-rs/zino" },
+];
+
+// Where people add their own project (PR to the users list).
+export const USERS_LIST_URL =
+ "https://github.com/apache/opendal/blob/main/core/users.md";
+
+// Curated, icon-backed slice of the 50+ supported services, grouped by family.
+export const serviceGroups = [
+ {
+ category: "Object Storage",
+ services: [
+ { name: "s3", icon: "/img/services/s3.svg" },
+ { name: "gcs", icon: "/img/services/gcs.png" },
+ { name: "azblob", icon: "/img/services/azure.svg" },
+ { name: "oss", icon: "/img/services/oss.svg" },
+ { name: "cos", icon: "/img/services/cos.svg" },
+ { name: "obs", icon: "/img/services/obs.png" },
+ { name: "b2", icon: "/img/services/backblaze.png" },
+ { name: "tos", icon: "/img/services/volcengine.png" },
+ ],
+ },
+ {
+ category: "File Storage",
+ services: [
+ { name: "fs", icon: "/img/services/opendal.svg" },
+ { name: "hdfs", icon: "/img/services/hadoop.ico" },
+ { name: "alluxio", icon: "/img/services/alluxio.svg" },
+ { name: "lakefs", icon: "/img/services/lakefs.ico" },
+ { name: "ipfs", icon: "/img/services/ipfs.ico" },
+ { name: "dbfs", icon: "/img/services/databricks.png" },
+ ],
+ },
+ {
+ category: "Cloud SaaS",
+ services: [
+ { name: "gdrive", icon: "/img/services/gdrive.png" },
+ { name: "dropbox", icon: "/img/services/dropbox.ico" },
+ { name: "onedrive", icon: "/img/services/onedrive.svg" },
+ { name: "hf", icon: "/img/services/huggingface.ico" },
+ { name: "github", icon: "/img/services/github.svg" },
+ { name: "koofr", icon: "/img/services/koofr.ico" },
+ ],
+ },
+ {
+ category: "Protocols",
+ services: [
+ { name: "http", icon: "/img/services/http.png" },
+ { name: "ftp", icon: "/img/services/ftp.png" },
+ { name: "webdav", icon: "/img/services/webdav.png" },
+ { name: "sftp", icon: "/img/services/sftp.png" },
+ ],
+ },
+ {
+ category: "Databases",
+ services: [
+ { name: "sqlite", icon: "/img/services/sqlite.ico" },
+ { name: "mysql", icon: "/img/services/mysql.ico" },
+ { name: "postgresql", icon: "/img/services/postgresql.ico" },
+ { name: "mongodb", icon: "/img/services/mongodb.ico" },
+ { name: "surrealdb", icon: "/img/services/surrealdb.svg" },
+ { name: "d1", icon: "/img/services/cloudflare.ico" },
+ ],
+ },
+ {
+ category: "Key-Value",
+ services: [
+ { name: "redis", icon: "/img/services/redis.png" },
+ { name: "etcd", icon: "/img/services/etcd.png" },
+ { name: "rocksdb", icon: "/img/services/rocksdb.png" },
+ { name: "memcached", icon: "/img/services/memcached.png" },
+ { name: "tikv", icon: "/img/services/tikv.png" },
+ { name: "foundationdb", icon: "/img/services/foundationdb.png" },
+ ],
+ },
+];
+
+export const bindings = [
+ { name: "Rust", icon: "/img/bindings/rust.svg" },
+ { name: "Python", icon: "/img/bindings/python.svg" },
+ { name: "Java", icon: "/img/bindings/java.svg" },
+ { name: "Go", icon: "/img/bindings/go.svg" },
+ { name: "Node.js", icon: "/img/bindings/nodejs.svg" },
+ { name: "C", icon: "/img/bindings/c.svg" },
+ { name: "C++", icon: "/img/bindings/cpp.svg" },
+ { name: ".NET", icon: "/img/bindings/dotnet.svg" },
+ { name: "Ruby", icon: "/img/bindings/ruby.svg" },
+ { name: "PHP", icon: "/img/bindings/php.svg" },
+ { name: "Swift", icon: "/img/bindings/swift.svg" },
+ { name: "Haskell", icon: "/img/bindings/haskell.svg" },
+ { name: "OCaml", icon: "/img/bindings/ocaml.svg" },
+ { name: "Lua", icon: "/img/bindings/lua.svg" },
+ { name: "Dart", icon: "/img/bindings/dart.svg" },
+ { name: "D", icon: "/img/bindings/d.svg" },
+ { name: "Zig", icon: "/img/bindings/zig.svg" },
+];
+
+export const layers = [
+ {
+ name: "RetryLayer",
+ desc: "Recover from transient failures automatically.",
+ doc: layerDoc("RetryLayer"),
+ code: `use opendal::layers::RetryLayer;
+
+// Exponential backoff with jitter; interrupted
+// reads and writes resume where they left off.
+let op = op.layer(
+ RetryLayer::new().with_max_times(5).with_jitter(),
+);`,
+ },
+ {
+ name: "TimeoutLayer",
+ desc: "Bound slow or hanging operations.",
+ doc: layerDoc("TimeoutLayer"),
+ code: `use opendal::layers::TimeoutLayer;
+use std::time::Duration;
+
+// Abort operations that stall past a deadline.
+let op = op.layer(
+ TimeoutLayer::new()
+ .with_timeout(Duration::from_secs(10)),
+);`,
+ },
+ {
+ name: "LoggingLayer",
+ desc: "Emit structured operation logs.",
+ doc: layerDoc("LoggingLayer"),
+ code: `use opendal::layers::LoggingLayer;
+
+// Structured logs for every operation, via the
+// standard log crate facade.
+let op = op.layer(LoggingLayer::default());`,
+ },
+ {
+ name: "TracingLayer",
+ desc: "Trace requests across systems.",
+ doc: layerDoc("TracingLayer"),
+ code: `use opendal::layers::TracingLayer;
+
+// One span per operation, into whatever
+// tracing subscriber your app installs.
+let op = op.layer(TracingLayer::new());`,
+ },
+ {
+ name: "MetricsLayer",
+ desc: "Export operation metrics.",
+ doc: layerDoc("MetricsLayer"),
+ code: `use opendal::layers::MetricsLayer;
+
+// Latency and throughput via the metrics crate
+// facade — plug in any exporter you like.
+let op = op.layer(MetricsLayer::default());`,
+ },
+ {
+ name: "PrometheusLayer",
+ desc: "Expose Prometheus metrics.",
+ doc: layerDoc("PrometheusLayer"),
+ code: `use opendal::layers::PrometheusLayer;
+
+// Register operation metrics into a Prometheus
+// registry you already expose.
+let registry = prometheus::default_registry();
+let op = op.layer(
+ PrometheusLayer::builder().register(registry)?,
+);`,
+ },
+ {
+ name: "ConcurrentLimitLayer",
+ desc: "Cap in-flight concurrency.",
+ doc: layerDoc("ConcurrentLimitLayer"),
+ code: `use opendal::layers::ConcurrentLimitLayer;
+
+// Cap how many operations hit the backend at once —
+// back-pressure for the whole Operator.
+let op = op.layer(ConcurrentLimitLayer::new(1024));`,
+ },
+ {
+ name: "ThrottleLayer",
+ desc: "Throttle I/O bandwidth.",
+ doc: layerDoc("ThrottleLayer"),
+ code: `use opendal::layers::ThrottleLayer;
+
+// Token-bucket bandwidth limit: ~10 MiB/s steady,
+// with headroom to burst for short spikes.
+let op = op.layer(ThrottleLayer::new(
+ 10 * 1024 * 1024,
+ 32 * 1024 * 1024,
+));`,
+ },
+];
diff --git a/website/src/components/landing/sections.jsx
b/website/src/components/landing/sections.jsx
new file mode 100644
index 000000000..f3f65506d
--- /dev/null
+++ b/website/src/components/landing/sections.jsx
@@ -0,0 +1,415 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React, { useState } from "react";
+import Link from "@docusaurus/Link";
+import { useBaseUrlUtils } from "@docusaurus/useBaseUrl";
+import CodeBlock from "@theme/CodeBlock";
+import CodeTabs from "./CodeTabs";
+import styles from "./styles.module.css";
+import {
+ REPO_URL,
+ DOCS_URL,
+ DISCORD_URL,
+ heroStats,
+ codeSamples,
+ valueProps,
+ capabilityThemes,
+ usedBy,
+ USERS_LIST_URL,
+ serviceGroups,
+ bindings,
+ layers,
+} from "./data";
+
+export function Hero() {
+ return (
+ <header className={styles.hero}>
+ <div className={`${styles.heroGrid} odl-grid-bg`} aria-hidden="true" />
+ <div className="odl-container">
+ <div className={styles.heroInner}>
+ <div>
+ <span className="odl-eyebrow">
+ Apache OpenDAL™ — Open Data Access Layer
+ </span>
+ <h1 className={styles.heroTitle}>
+ One Layer,
+ <br />
+ <span className={styles.heroTitleAccent}>All Storage.</span>
+ </h1>
+ <p className={styles.heroLede}>
+ One zero-cost Operator for object storage, file systems,
databases
+ and more — in the language you already ship.
+ </p>
+ <div className={styles.heroActions}>
+ <Link
+ className={`${styles.btn} ${styles.btnPrimary}`}
+ to={DOCS_URL}
+ >
+ Get started <span className={styles.btnArrow}>→</span>
+ </Link>
+ <Link
+ className={`${styles.btn} ${styles.btnSecondary}`}
+ to={REPO_URL}
+ >
+ View on GitHub
+ </Link>
+ </div>
+ <div className={styles.heroStats}>
+ {heroStats.map((stat) => (
+ <div className={styles.heroStat} key={stat.label}>
+ <span className={styles.heroStatValue}>{stat.value}</span>
+ <span className={styles.heroStatLabel}>{stat.label}</span>
+ </div>
+ ))}
+ </div>
+ </div>
+ <div className={styles.heroAside}>
+ <CodeTabs samples={codeSamples} title="quickstart" equalize />
+ </div>
+ </div>
+ </div>
+ </header>
+ );
+}
+
+export function UsedBy() {
+ const { withBaseUrl } = useBaseUrlUtils();
+ return (
+ <section className={`${styles.section} ${styles.sectionSubtle}`}>
+ <div className="odl-container">
+ <div className={styles.sectionHead}>
+ <span className="odl-eyebrow">Used by</span>
+ <h2 className={styles.sectionTitle}>
+ Powering AI, analytics, and real-time data
+ </h2>
+ <p className={styles.sectionLede}>
+ OpenDAL runs in production across the open-source ecosystem. These
+ are some of the projects that build on it.
+ </p>
+ </div>
+ <ul className={styles.logoWall}>
+ {usedBy.map((u) => (
+ <li key={u.name}>
+ <Link className={styles.logoItem} to={u.href} title={u.name}>
+ <img
+ className={styles.logoMark}
+ src={withBaseUrl(u.icon)}
+ alt=""
+ width="28"
+ height="28"
+ loading="lazy"
+ />
+ <span className={styles.logoName}>{u.name}</span>
+ </Link>
+ </li>
+ ))}
+ <li>
+ <Link className={styles.addLogo} to={USERS_LIST_URL}>
+ + add your logo
+ </Link>
+ </li>
+ </ul>
+ </div>
+ </section>
+ );
+}
+
+export function ValueProps() {
+ return (
+ <section className={styles.section}>
+ <div className="odl-container">
+ <div className={styles.sectionHead}>
+ <span className="odl-eyebrow">Why OpenDAL</span>
+ <h2 className={styles.sectionTitle}>
+ A storage layer you can build production on.
+ </h2>
+ <p className={styles.sectionLede}>
+ OpenDAL turns one vision — <em>One Layer, All Storage</em> — into a
+ practical foundation for applications, libraries and data systems.
+ </p>
+ </div>
+ <div className={`${styles.valueGrid} ${styles.reveal}`}>
+ {valueProps.map((v) => (
+ <article className={styles.valueCard} key={v.index}>
+ <span className={styles.valueIndex}>{v.index}</span>
+ <h3 className={styles.valueCardTitle}>{v.title}</h3>
+ <p className={styles.valueCardBody}>{v.body}</p>
+ </article>
+ ))}
+ </div>
+ </div>
+ </section>
+ );
+}
+
+export function Capabilities() {
+ const [active, setActive] = useState(capabilityThemes[0]);
+ return (
+ <section className={`${styles.section} ${styles.sectionSubtle}`}>
+ <div className="odl-container">
+ <div className={styles.sectionHead}>
+ <span className="odl-eyebrow">Capabilities</span>
+ <h2 className={styles.sectionTitle}>Configure once. Access
anything.</h2>
+ <p className={styles.sectionLede}>
+ One Operator is a full toolkit for real-world data — read and write
+ at scale, recover from failures, and work with files — the same way
+ on every backend.
+ </p>
+ </div>
+ <div className={styles.capabilityExplorer}>
+ <ul className={styles.capabilityNav}>
+ {capabilityThemes.map((theme) => {
+ const selected = active.title === theme.title;
+ return (
+ <li key={theme.title}>
+ <Link
+ className={`${styles.capabilityItem} ${
+ selected ? styles.capabilityItemActive : ""
+ }`}
+ to={theme.doc}
+ target="_blank"
+ rel="noreferrer"
+ aria-current={selected ? "true" : undefined}
+ onMouseEnter={() => setActive(theme)}
+ onFocus={() => setActive(theme)}
+ >
+ <span className={styles.capabilityText}>
+ <span className={styles.capabilityItemTitle}>
+ {theme.title}
+ </span>
+ <span className={styles.capabilityItemBlurb}>
+ {theme.blurb}
+ </span>
+ </span>
+ <span className={styles.capabilityArrow}
aria-hidden="true">
+ ↗
+ </span>
+ </Link>
+ </li>
+ );
+ })}
+ </ul>
+ <div className={styles.capabilityPreview}>
+ <div className={styles.codeWindow}>
+ <div className={styles.windowBar}>
+ <div className={styles.windowDots} aria-hidden="true">
+ <span />
+ <span />
+ <span />
+ </div>
+ <span className={styles.windowTitle}>{active.title}</span>
+ </div>
+ <div className={`${styles.codeBody}
${styles.capabilityCodeBody}`}>
+ <div className={styles.capabilityCodeFade} key={active.title}>
+ <CodeBlock language="rust">{active.code}</CodeBlock>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ );
+}
+
+export function Services() {
+ const { withBaseUrl } = useBaseUrlUtils();
+ return (
+ <section className={styles.section}>
+ <div className="odl-container">
+ <div className={styles.sectionHead}>
+ <span className="odl-eyebrow">Services</span>
+ <h2 className={styles.sectionTitle}>
+ 50+ storage services, one interface.
+ </h2>
+ <p className={styles.sectionLede}>
+ Enable only the backends your application needs. The Operator
+ contract stays identical across every one.
+ </p>
+ </div>
+ <div className={`${styles.serviceGroups} ${styles.reveal}`}>
+ {serviceGroups.map((group) => (
+ <div className={styles.serviceGroup} key={group.category}>
+ <h3 className={styles.serviceGroupTitle}>{group.category}</h3>
+ <div className={styles.serviceChips}>
+ {group.services.map((s) => (
+ <span className={styles.serviceChip} key={s.name}>
+ <img
+ src={withBaseUrl(s.icon)}
+ alt=""
+ width="15"
+ height="15"
+ loading="lazy"
+ />
+ {s.name}
+ </span>
+ ))}
+ </div>
+ </div>
+ ))}
+ </div>
+ <div className={styles.servicesFoot}>
+ <Link
+ className={`${styles.btn} ${styles.btnSecondary}`}
+ to={DOCS_URL}
+ >
+ Browse all services <span className={styles.btnArrow}>→</span>
+ </Link>
+ </div>
+ </div>
+ </section>
+ );
+}
+
+export function Bindings() {
+ const { withBaseUrl } = useBaseUrlUtils();
+ return (
+ <section className={`${styles.section} ${styles.sectionSubtle}`}>
+ <div className="odl-container">
+ <div className={styles.sectionHead}>
+ <span className="odl-eyebrow">Bindings</span>
+ <h2 className={styles.sectionTitle}>Ship it in your language.</h2>
+ <p className={styles.sectionLede}>
+ Each binding exposes the same OpenDAL service model while following
+ its own ecosystem's conventions.
+ </p>
+ </div>
+ <div className={`${styles.bindingGrid} ${styles.reveal}`}>
+ {bindings.map((b) => (
+ <div className={styles.bindingCard} key={b.name}>
+ <img
+ src={withBaseUrl(b.icon)}
+ alt=""
+ width="30"
+ height="30"
+ loading="lazy"
+ />
+ <span className={styles.bindingName}>{b.name}</span>
+ </div>
+ ))}
+ </div>
+ </div>
+ </section>
+ );
+}
+
+export function Layers() {
+ const [active, setActive] = useState(layers[0]);
+ return (
+ <section className={styles.section}>
+ <div className="odl-container">
+ <div className={styles.sectionHead}>
+ <span className="odl-eyebrow">Layers</span>
+ <h2 className={styles.sectionTitle}>
+ Production behavior, composed — not coded.
+ </h2>
+ <p className={styles.sectionLede}>
+ Stack cross-cutting concerns as reusable layers. The order is
+ explicit and the core stays zero-cost.
+ </p>
+ </div>
+ <div className={styles.layerExplorer}>
+ <div className={styles.layerGrid}>
+ {layers.map((l) => {
+ const selected = active.name === l.name;
+ return (
+ <Link
+ key={l.name}
+ className={`${styles.layerItem} ${
+ selected ? styles.layerItemActive : ""
+ }`}
+ to={l.doc}
+ target="_blank"
+ rel="noreferrer"
+ aria-current={selected ? "true" : undefined}
+ onMouseEnter={() => setActive(l)}
+ onFocus={() => setActive(l)}
+ >
+ <span className={styles.layerName}>{l.name}</span>
+ <span className={styles.layerDesc}>{l.desc}</span>
+ </Link>
+ );
+ })}
+ </div>
+ <div className={styles.capabilityPreview}>
+ <div className={styles.codeWindow}>
+ <div className={styles.windowBar}>
+ <div className={styles.windowDots} aria-hidden="true">
+ <span />
+ <span />
+ <span />
+ </div>
+ <span className={styles.windowTitle}>{active.name}</span>
+ </div>
+ <div className={`${styles.codeBody} ${styles.layerCodeBody}`}>
+ <div className={styles.capabilityCodeFade} key={active.name}>
+ <CodeBlock language="rust">{active.code}</CodeBlock>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ );
+}
+
+export function FinalCta() {
+ return (
+ <section className={`${styles.section} ${styles.sectionSubtle}`}>
+ <div className="odl-container">
+ <div className={styles.finalCta}>
+ <div className={`${styles.finalCtaInner} ${styles.finalCenter}`}>
+ <span className={`odl-eyebrow ${styles.finalEyebrow}`}>
+ Start building
+ </span>
+ <h2 className={styles.finalCtaTitle}>
+ One layer for all your storage.
+ </h2>
+ <p className={styles.finalCtaLede}>
+ Join the infrastructure builders, platform teams and application
+ developers accessing data freely, painlessly and efficiently.
+ </p>
+ <div className={styles.finalCtaActions}>
+ <Link
+ className={`${styles.btn} ${styles.btnPrimary}`}
+ to={DOCS_URL}
+ >
+ Get started <span className={styles.btnArrow}>→</span>
+ </Link>
+ <Link
+ className={`${styles.btn} ${styles.btnSecondary}`}
+ to={REPO_URL}
+ >
+ Star on GitHub
+ </Link>
+ <Link
+ className={`${styles.btn} ${styles.btnSecondary}`}
+ to={DISCORD_URL}
+ >
+ Join Discord
+ </Link>
+ </div>
+ </div>
+ </div>
+ </div>
+ </section>
+ );
+}
diff --git a/website/src/components/landing/styles.module.css
b/website/src/components/landing/styles.module.css
new file mode 100644
index 000000000..8b1559bbe
--- /dev/null
+++ b/website/src/components/landing/styles.module.css
@@ -0,0 +1,744 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* ===== Shared building blocks ============================================ */
+.section {
+ padding-block: clamp(3.5rem, 2rem + 7vw, 7rem);
+ border-top: 1px solid var(--odl-border);
+}
+.sectionSubtle {
+ background: var(--odl-bg-subtle);
+}
+
+.sectionHead {
+ max-width: 46rem;
+ margin-bottom: var(--odl-space-7);
+}
+.sectionTitle {
+ font-size: var(--odl-text-2xl);
+ line-height: var(--odl-leading-snug);
+ font-weight: 650;
+ color: var(--odl-fg-strong);
+ margin: var(--odl-space-4) 0 0;
+}
+.sectionLede {
+ font-size: var(--odl-text-md);
+ line-height: var(--odl-leading-normal);
+ color: var(--odl-fg-muted);
+ margin: var(--odl-space-3) 0 0;
+ max-width: 40rem;
+}
+
+/* Buttons --------------------------------------------------------------- */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--odl-space-2);
+ height: 2.875rem;
+ padding-inline: var(--odl-space-5);
+ border-radius: var(--odl-radius-md);
+ border: 1px solid transparent;
+ font-size: var(--odl-text-sm);
+ font-weight: 600;
+ letter-spacing: -0.005em;
+ text-decoration: none;
+ cursor: pointer;
+ transition: background-color var(--odl-dur) var(--odl-ease),
+ border-color var(--odl-dur) var(--odl-ease),
+ color var(--odl-dur) var(--odl-ease), transform var(--odl-dur-fast)
+ var(--odl-ease);
+}
+.btn:hover {
+ text-decoration: none;
+}
+.btn:active {
+ transform: translateY(1px);
+}
+.btnPrimary {
+ background: var(--odl-action-bg);
+ color: var(--odl-action-fg);
+}
+.btnPrimary:hover {
+ background: var(--odl-action-bg-hover);
+ color: var(--odl-action-fg);
+}
+.btnSecondary {
+ background: transparent;
+ color: var(--odl-fg-strong);
+ border-color: var(--odl-border-strong);
+}
+.btnSecondary:hover {
+ color: var(--odl-fg-strong);
+ border-color: var(--odl-fg-strong);
+ background: var(--odl-surface-2);
+}
+.btnArrow {
+ transition: transform var(--odl-dur) var(--odl-ease-out);
+}
+.btn:hover .btnArrow {
+ transform: translateX(3px);
+}
+
+/* ===== Hero ============================================================== */
+.hero {
+ position: relative;
+ overflow: hidden;
+ padding-top: clamp(1.75rem, 1rem + 2.4vw, 3rem);
+ padding-bottom: clamp(3rem, 2rem + 5vw, 5.5rem);
+}
+.heroGrid {
+ position: absolute;
+ inset: 0;
+ -webkit-mask-image: radial-gradient(
+ 120% 90% at 70% 0%,
+ #000 0%,
+ transparent 72%
+ );
+ mask-image: radial-gradient(120% 90% at 70% 0%, #000 0%, transparent 72%);
+ pointer-events: none;
+}
+.heroInner {
+ position: relative;
+ display: grid;
+ grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
+ gap: clamp(2rem, 1rem + 4vw, 4.5rem);
+ /* Column widths are locked by minmax(0, fr), so the left column never
+ reflows; center the columns to balance the fixed-height code window. */
+ align-items: center;
+}
+.heroTitle {
+ font-size: var(--odl-text-display);
+ line-height: var(--odl-leading-tight);
+ font-weight: 700;
+ letter-spacing: var(--odl-tracking-tight);
+ color: var(--odl-fg-strong);
+ margin: var(--odl-space-5) 0 0;
+}
+.heroTitleAccent {
+ color: var(--odl-accent);
+}
+.heroLede {
+ font-size: var(--odl-text-md);
+ line-height: var(--odl-leading-normal);
+ color: var(--odl-fg-muted);
+ margin: var(--odl-space-5) 0 0;
+ max-width: 34rem;
+}
+.heroActions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--odl-space-3);
+ margin-top: var(--odl-space-6);
+}
+.heroStats {
+ display: grid;
+ grid-template-columns: repeat(4, 1fr);
+ gap: var(--odl-space-4);
+ margin-top: var(--odl-space-8);
+ border-top: 1px solid var(--odl-border);
+ padding-top: var(--odl-space-5);
+}
+.heroStat {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+.heroStatValue {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-xl);
+ font-weight: 600;
+ color: var(--odl-fg-strong);
+ font-variant-numeric: tabular-nums;
+}
+.heroStatLabel {
+ font-size: var(--odl-text-xs);
+ color: var(--odl-fg-subtle);
+}
+
+/* ===== Code window (shared by Hero & Operator) ========================== */
+.codeWindow {
+ border: 1px solid var(--odl-border);
+ border-radius: var(--odl-radius-lg);
+ background: var(--odl-surface);
+ box-shadow: var(--odl-shadow-lg);
+ overflow: hidden;
+}
+.windowBar {
+ display: flex;
+ align-items: center;
+ gap: var(--odl-space-3);
+ padding: var(--odl-space-3) var(--odl-space-4);
+ border-bottom: 1px solid var(--odl-border);
+ background: var(--odl-surface-2);
+}
+.windowDots {
+ display: flex;
+ gap: 6px;
+}
+.windowDots span {
+ width: 11px;
+ height: 11px;
+ border-radius: var(--odl-radius-pill);
+ background: var(--odl-border-strong);
+}
+.windowTitle {
+ flex: 1 1 auto;
+ min-width: 0;
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-xs);
+ color: var(--odl-fg-muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+.codeTabs {
+ display: flex;
+ gap: 2px;
+ padding: var(--odl-space-2) var(--odl-space-3) 0;
+ border-bottom: 1px solid var(--odl-border);
+ background: var(--odl-surface-2);
+ overflow-x: auto;
+ scrollbar-width: none;
+}
+.codeTabs::-webkit-scrollbar {
+ display: none;
+}
+.codeTab {
+ appearance: none;
+ background: transparent;
+ border: 0;
+ border-bottom: 2px solid transparent;
+ padding: var(--odl-space-2) var(--odl-space-3) var(--odl-space-3);
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-xs);
+ color: var(--odl-fg-subtle);
+ cursor: pointer;
+ white-space: nowrap;
+ transition: color var(--odl-dur) var(--odl-ease),
+ border-color var(--odl-dur) var(--odl-ease);
+}
+.codeTab:hover {
+ color: var(--odl-fg);
+}
+.codeTabActive {
+ color: var(--odl-fg-strong);
+ border-bottom-color: var(--odl-accent);
+}
+/* The code area animates its height to fit the active snippet: no dead space
+ for short snippets, and switching language glides between sizes. The left
+ hero column never moves (its width is locked and it is the taller column),
+ so only this window resizes. */
+.codeBodyAnimated {
+ overflow: hidden;
+ background: var(--odl-code-bg);
+ transition: height var(--odl-dur-slow) var(--odl-ease);
+}
+@media (prefers-reduced-motion: reduce) {
+ .codeBodyAnimated {
+ transition: none;
+ }
+}
+
+/* Tighten the embedded Docusaurus CodeBlock to fit the window chrome */
+.codeBody :global(.theme-code-block),
+.codeBody :global(div[class*="codeBlockContainer"]) {
+ margin: 0;
+ border: 0;
+ border-radius: 0;
+ box-shadow: none;
+}
+.codeBody :global(pre) {
+ border: 0 !important;
+ border-radius: 0 !important;
+ margin: 0;
+ background: var(--odl-code-bg) !important;
+ font-size: 0.8125rem;
+ /* Wrap long lines so code windows never scroll horizontally. */
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+}
+.codeBody :global(pre code),
+.codeBody :global(.token-line) {
+ white-space: inherit;
+ overflow-wrap: inherit;
+}
+
+/* ===== Used-by wall ===================================================== */
+.logoWall {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(12.5rem, 1fr));
+ gap: var(--odl-space-3);
+}
+.logoWall > li {
+ display: flex;
+}
+.logoItem {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: var(--odl-space-3);
+ padding: var(--odl-space-4);
+ background: var(--odl-surface);
+ border: 1px solid var(--odl-border);
+ border-radius: var(--odl-radius-md);
+ text-decoration: none;
+ filter: grayscale(1);
+ opacity: 0.72;
+ transition: opacity var(--odl-dur) var(--odl-ease),
+ filter var(--odl-dur) var(--odl-ease),
+ background-color var(--odl-dur) var(--odl-ease);
+}
+.logoItem:hover {
+ background: var(--odl-surface-2);
+ filter: grayscale(0);
+ opacity: 1;
+ text-decoration: none;
+}
+.logoMark {
+ width: 28px;
+ height: 28px;
+ object-fit: contain;
+ border-radius: var(--odl-radius-sm);
+ flex: none;
+}
+.logoName {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-sm);
+ font-weight: 500;
+ color: var(--odl-fg-muted);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+/* "+ add your logo" rendered as the final, dashed tile in the wall. */
+.addLogo {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--odl-space-4);
+ border: 1px dashed var(--odl-border);
+ border-radius: var(--odl-radius-md);
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-xs);
+ letter-spacing: 0.04em;
+ color: var(--odl-fg-subtle);
+ text-decoration: none;
+ white-space: nowrap;
+ transition: color var(--odl-dur) var(--odl-ease),
+ border-color var(--odl-dur) var(--odl-ease);
+}
+.addLogo:hover {
+ color: var(--odl-accent);
+ border-color: var(--odl-accent);
+ text-decoration: none;
+}
+
+/* ===== Value props ====================================================== */
+.valueGrid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1px;
+ background: var(--odl-border);
+ border: 1px solid var(--odl-border);
+ border-radius: var(--odl-radius-lg);
+ overflow: hidden;
+}
+.valueCard {
+ background: var(--odl-surface);
+ padding: clamp(1.5rem, 1rem + 2vw, 2.5rem);
+ display: flex;
+ flex-direction: column;
+ gap: var(--odl-space-3);
+ transition: background-color var(--odl-dur) var(--odl-ease);
+}
+.valueCard:hover {
+ background: var(--odl-surface-2);
+}
+.valueIndex {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-xs);
+ font-weight: 500;
+ letter-spacing: var(--odl-tracking-label);
+ color: var(--odl-accent);
+}
+.valueCardTitle {
+ font-size: var(--odl-text-lg);
+ font-weight: 650;
+ color: var(--odl-fg-strong);
+ margin: 0;
+ letter-spacing: var(--odl-tracking-tight);
+}
+.valueCardBody {
+ font-size: var(--odl-text-base);
+ line-height: var(--odl-leading-normal);
+ color: var(--odl-fg-muted);
+ margin: 0;
+}
+
+/* ===== Capabilities (explorer) ========================================== */
+.capabilityExplorer {
+ display: grid;
+ grid-template-columns: minmax(0, 0.82fr) minmax(0, 1.18fr);
+ gap: clamp(2rem, 1rem + 3vw, 3.5rem);
+ align-items: start;
+}
+.capabilityNav {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: var(--odl-space-2);
+}
+.capabilityItem {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: var(--odl-space-3);
+ padding: var(--odl-space-4);
+ border: 1px solid transparent;
+ border-radius: var(--odl-radius-md);
+ text-decoration: none;
+ transition: background-color var(--odl-dur) var(--odl-ease),
+ border-color var(--odl-dur) var(--odl-ease);
+}
+.capabilityItem:hover {
+ background: var(--odl-surface);
+ text-decoration: none;
+}
+.capabilityItemActive {
+ background: var(--odl-surface);
+ border-color: var(--odl-border);
+ box-shadow: var(--odl-shadow-sm);
+}
+.capabilityText {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ min-width: 0;
+}
+.capabilityItemTitle {
+ font-size: var(--odl-text-md);
+ font-weight: 650;
+ letter-spacing: var(--odl-tracking-tight);
+ color: var(--odl-fg-strong);
+}
+.capabilityItemBlurb {
+ font-size: var(--odl-text-sm);
+ line-height: var(--odl-leading-snug);
+ color: var(--odl-fg-muted);
+}
+.capabilityArrow {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-sm);
+ color: var(--odl-accent);
+ opacity: 0;
+ transition: opacity var(--odl-dur) var(--odl-ease);
+}
+.capabilityItem:hover .capabilityArrow,
+.capabilityItemActive .capabilityArrow {
+ opacity: 1;
+}
+
+/* Sticky code preview that tracks the active capability */
+.capabilityPreview {
+ position: sticky;
+ top: 5rem;
+}
+.capabilityCodeBody {
+ height: 19rem;
+ overflow: auto;
+ background: var(--odl-code-bg);
+}
+/* Fade only the code itself; the panel background stays put, so switching
+ snippets never flashes the window surface behind it. */
+.capabilityCodeFade {
+ animation: odlFade var(--odl-dur) var(--odl-ease);
+}
+@keyframes odlFade {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+@media (prefers-reduced-motion: reduce) {
+ .capabilityCodeFade {
+ animation: none;
+ }
+}
+
+/* ===== Services ========================================================= */
+.serviceGroups {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 1px;
+ background: var(--odl-border);
+ border: 1px solid var(--odl-border);
+ border-radius: var(--odl-radius-lg);
+ overflow: hidden;
+}
+.serviceGroup {
+ background: var(--odl-surface);
+ padding: clamp(1.25rem, 1rem + 1vw, 1.75rem);
+}
+.serviceGroupTitle {
+ display: flex;
+ align-items: center;
+ gap: var(--odl-space-3);
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-xs);
+ font-weight: 500;
+ letter-spacing: var(--odl-tracking-label);
+ text-transform: uppercase;
+ color: var(--odl-fg-subtle);
+ margin-bottom: var(--odl-space-4);
+}
+.serviceGroupTitle::before {
+ content: "";
+ width: 0.875rem;
+ height: 0.375rem;
+ background: var(--odl-accent);
+ flex: none;
+}
+.serviceChips {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--odl-space-2);
+}
+.serviceChip {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--odl-space-2);
+ padding: 0.3rem 0.6rem;
+ border: 1px solid var(--odl-border);
+ border-radius: var(--odl-radius-sm);
+ background: var(--odl-bg);
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-xs);
+ color: var(--odl-fg);
+}
+.serviceChip img {
+ width: 15px;
+ height: 15px;
+ object-fit: contain;
+}
+.servicesFoot {
+ margin-top: var(--odl-space-6);
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+ gap: var(--odl-space-3);
+}
+
+/* ===== Bindings ========================================================= */
+.bindingGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(118px, 1fr));
+ gap: 1px;
+ background: var(--odl-border);
+ border: 1px solid var(--odl-border);
+ border-radius: var(--odl-radius-lg);
+ overflow: hidden;
+}
+.bindingCard {
+ background: var(--odl-surface);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: var(--odl-space-3);
+ padding: var(--odl-space-5) var(--odl-space-3);
+ transition: background-color var(--odl-dur) var(--odl-ease);
+}
+.bindingCard:hover {
+ background: var(--odl-surface-2);
+}
+.bindingCard img {
+ width: 30px;
+ height: 30px;
+ object-fit: contain;
+}
+.bindingName {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-xs);
+ color: var(--odl-fg-muted);
+}
+
+/* ===== Layers (tile grid left, live code preview right) ================= */
+.layerExplorer {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ gap: clamp(2rem, 1rem + 3vw, 3.5rem);
+ align-items: start;
+}
+.layerGrid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 1px;
+ background: var(--odl-border);
+ border: 1px solid var(--odl-border);
+ border-radius: var(--odl-radius-lg);
+ overflow: hidden;
+}
+.layerItem {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ padding: var(--odl-space-4);
+ background: var(--odl-surface);
+ text-decoration: none;
+ transition: background-color var(--odl-dur) var(--odl-ease);
+}
+.layerItem:hover {
+ background: var(--odl-surface-2);
+ text-decoration: none;
+}
+.layerItemActive {
+ background: var(--odl-surface-2);
+}
+.layerName {
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-sm);
+ font-weight: 600;
+ color: var(--odl-fg-strong);
+ transition: color var(--odl-dur) var(--odl-ease);
+}
+.layerItemActive .layerName {
+ color: var(--odl-accent);
+}
+.layerDesc {
+ font-size: var(--odl-text-xs);
+ line-height: var(--odl-leading-snug);
+ color: var(--odl-fg-muted);
+}
+.layerCodeBody {
+ height: 13rem;
+ overflow: auto;
+ background: var(--odl-code-bg);
+}
+
+/* ===== Final CTA ======================================================== */
+.finalCta {
+ background: var(--odl-surface);
+ border: 1px solid var(--odl-border);
+ border-radius: var(--odl-radius-lg);
+ box-shadow: var(--odl-shadow-sm);
+ padding: clamp(2.5rem, 1.5rem + 5vw, 5rem);
+ text-align: center;
+}
+.finalCtaInner {
+ position: relative;
+}
+.finalCtaTitle {
+ font-size: clamp(2rem, 1.3rem + 3vw, 3.25rem);
+ font-weight: 700;
+ letter-spacing: var(--odl-tracking-tight);
+ margin: var(--odl-space-4) 0 0;
+ color: var(--odl-fg-strong);
+}
+.finalCtaLede {
+ font-size: var(--odl-text-md);
+ color: var(--odl-fg-muted);
+ margin: var(--odl-space-4) auto 0;
+ max-width: 34rem;
+}
+.finalCtaActions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--odl-space-3);
+ justify-content: center;
+ margin-top: var(--odl-space-6);
+}
+.finalEyebrow {
+ justify-content: center;
+}
+.finalCenter {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+/* ===== Reveal-on-scroll — pure CSS, progressive enhancement ============= */
+/* Content is never hidden without support: browsers lacking scroll-driven
+ animations (or users who prefer reduced motion) simply see it immediately.
*/
+@media (prefers-reduced-motion: no-preference) {
+ @supports (animation-timeline: view()) {
+ .reveal {
+ animation: odlReveal linear both;
+ animation-timeline: view();
+ animation-range: entry 2% cover 18%;
+ }
+ }
+}
+@keyframes odlReveal {
+ from {
+ opacity: 0;
+ transform: translateY(18px);
+ }
+ to {
+ opacity: 1;
+ transform: none;
+ }
+}
+
+/* ===== Responsive ======================================================= */
+@media (max-width: 996px) {
+ .heroInner,
+ .capabilityExplorer,
+ .layerExplorer {
+ grid-template-columns: 1fr;
+ }
+ .capabilityPreview {
+ display: none;
+ }
+ .capabilityNav {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: var(--odl-space-3);
+ }
+ .serviceGroups {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ .heroAside {
+ order: 2;
+ }
+}
+
+@media (max-width: 640px) {
+ .valueGrid,
+ .serviceGroups,
+ .capabilityNav {
+ grid-template-columns: 1fr;
+ }
+ .heroStats {
+ grid-template-columns: repeat(2, 1fr);
+ gap: var(--odl-space-5) var(--odl-space-4);
+ }
+ .heroActions .btn {
+ flex: 1 1 auto;
+ }
+}
diff --git a/website/src/css/custom.css b/website/src/css/custom.css
index ff706266e..2ee0a5aa6 100644
--- a/website/src/css/custom.css
+++ b/website/src/css/custom.css
@@ -17,86 +17,422 @@
* under the License.
*/
-/**
- * Any CSS included here will be global. The classic template
- * bundles Infima by default. Infima is a CSS framework designed to
- * work well for content-centric websites.
- */
+/* ==========================================================================
+ OpenDAL Design System
+ --------------------------------------------------------------------------
+ "One Layer, All Storage." A precise, engineering-grade visual system built
+ on a near-monochrome ink scale, a single precise-blue accent, and the
+ signature solid "bar" lifted from the OpenDAL wordmark.
+
+ Token layers:
+ 1. Primitives — raw, theme-agnostic values (--odl-ink-*, --odl-blue-*)
+ 2. Semantic — meaning-bound, theme-aware roles (--odl-bg, --odl-fg ...)
+ 3. Infima map — semantic tokens projected onto Docusaurus/Infima vars so
+ docs, blog and community pages inherit the same system.
+
+ Typography uses native system stacks on purpose: zero third-party requests
+ (ASF privacy-friendly), zero layout shift, instant global rendering. The
+ monospace stack is promoted to a brand motif (labels, indices, figures).
+ To adopt IBM Plex instead, self-host it and swap --odl-font-* below.
+ ==========================================================================
*/
-/* You can override the default Infima variables here. */
+/* ---- 1. Primitive tokens -------------------------------------------------
*/
:root {
- --ifm-color-primary: #44A1F6;
- --ifm-color-primary-dark: #2692F5;
- --ifm-color-primary-darker: #178AF4;
- --ifm-color-primary-darkest: #0A72D2;
- --ifm-color-primary-light: #62B0F7;
- --ifm-color-primary-lighter: #71B8F8;
- --ifm-color-primary-lightest: #9ECEFA;
- --ifm-background-color: #ffffff;
- --ifm-code-font-size: 95%;
- --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1);
-
- --footer-font-color: black;
+ /* Ink — a cool, neutral near-black→white ramp */
+ --odl-ink-0: #ffffff;
+ --odl-ink-50: #f6f8fa;
+ --odl-ink-100: #edf1f5;
+ --odl-ink-150: #e4e9ef;
+ --odl-ink-200: #d8dee6;
+ --odl-ink-300: #c2cad4;
+ --odl-ink-400: #98a2b0;
+ --odl-ink-500: #6e7886;
+ --odl-ink-600: #4f5864;
+ --odl-ink-700: #39414b;
+ --odl-ink-800: #262c34;
+ --odl-ink-850: #1b2027;
+ --odl-ink-900: #12161b;
+ --odl-ink-950: #0a0d11;
+
+ /* Precise blue — the single brand accent */
+ --odl-blue-50: #edf3ff;
+ --odl-blue-100: #d8e5ff;
+ --odl-blue-200: #b4ccff;
+ --odl-blue-300: #85a9ff;
+ --odl-blue-400: #5384ff;
+ --odl-blue-500: #2c63f5;
+ --odl-blue-600: #1e54e0;
+ --odl-blue-700: #1846be;
+ --odl-blue-800: #163a97;
+ --odl-blue-900: #163172;
+
+ /* Functional signals (kept restrained, mostly for inherited docs UI) */
+ --odl-green-500: #1a8f5e;
+ --odl-green-400: #36c08a;
+ --odl-amber-500: #b7791f;
+ --odl-amber-400: #e0a93b;
+ --odl-red-500: #d23b3b;
+ --odl-red-400: #f06363;
+
+ /* Type families — system stacks with a clear engineering character */
+ --odl-font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
+ Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
+ "Apple Color Emoji", "Segoe UI Emoji";
+ --odl-font-mono: ui-monospace, "SF Mono", SFMono-Regular, "Cascadia Code",
+ "JetBrains Mono", "Roboto Mono", Menlo, Consolas, "Liberation Mono",
+ monospace;
+
+ /* Type scale */
+ --odl-text-2xs: 0.6875rem; /* 11px — mono micro-labels */
+ --odl-text-xs: 0.75rem; /* 12px */
+ --odl-text-sm: 0.875rem; /* 14px */
+ --odl-text-base: 1rem; /* 16px */
+ --odl-text-md: 1.125rem; /* 18px */
+ --odl-text-lg: 1.375rem; /* 22px */
+ --odl-text-xl: 1.75rem; /* 28px */
+ --odl-text-2xl: 2.25rem; /* 36px */
+ --odl-text-3xl: 3rem; /* 48px */
+ --odl-text-display: clamp(2.75rem, 1.6rem + 5.2vw, 5rem);
+ --odl-leading-tight: 1.08;
+ --odl-leading-snug: 1.28;
+ --odl-leading-normal: 1.6;
+ --odl-tracking-tight: -0.022em;
+ --odl-tracking-label: 0.12em;
+
+ /* Spacing — 4px rhythm */
+ --odl-space-1: 0.25rem;
+ --odl-space-2: 0.5rem;
+ --odl-space-3: 0.75rem;
+ --odl-space-4: 1rem;
+ --odl-space-5: 1.5rem;
+ --odl-space-6: 2rem;
+ --odl-space-7: 3rem;
+ --odl-space-8: 4rem;
+ --odl-space-9: 6rem;
+ --odl-space-10: 8rem;
+
+ /* Radius — restrained; the bar motif stays sharp (0) */
+ --odl-radius-xs: 3px;
+ --odl-radius-sm: 5px;
+ --odl-radius-md: 8px;
+ --odl-radius-lg: 12px;
+ --odl-radius-pill: 999px;
+
+ /* Layout */
+ --odl-container: 1200px;
+ --odl-container-narrow: 880px;
+ --odl-gutter: clamp(1.25rem, 1rem + 2vw, 2.5rem);
+
+ /* Motion */
+ --odl-ease-out: cubic-bezier(0.16, 1, 0.3, 1);
+ --odl-ease: cubic-bezier(0.4, 0, 0.2, 1);
+ --odl-dur-fast: 120ms;
+ --odl-dur: 200ms;
+ --odl-dur-slow: 320ms;
+}
+
+/* ---- 2. Semantic tokens — light (default) --------------------------------
*/
+:root {
+ --odl-bg: var(--odl-ink-0);
+ --odl-bg-subtle: var(--odl-ink-50);
+ --odl-bg-inset: var(--odl-ink-100);
+ --odl-surface: var(--odl-ink-0);
+ --odl-surface-2: var(--odl-ink-50);
+ --odl-fg: var(--odl-ink-900);
+ --odl-fg-strong: var(--odl-ink-950);
+ --odl-fg-muted: var(--odl-ink-600);
+ --odl-fg-subtle: var(--odl-ink-500);
+ --odl-border: var(--odl-ink-200);
+ --odl-border-strong: var(--odl-ink-300);
+ --odl-bar: var(--odl-ink-950);
+
+ --odl-accent: var(--odl-blue-600);
+ --odl-accent-hover: var(--odl-blue-700);
+ --odl-accent-subtle: var(--odl-blue-50);
+ --odl-accent-border: var(--odl-blue-200);
+ --odl-link: var(--odl-blue-600);
+
+ /* Solid action surface — vivid blue with white text in both themes */
+ --odl-action-bg: var(--odl-blue-600);
+ --odl-action-bg-hover: var(--odl-blue-700);
+ --odl-action-fg: #ffffff;
+
+ --odl-code-bg: var(--odl-ink-50);
+ --odl-code-border: var(--odl-ink-150);
+ --odl-selection: var(--odl-blue-100);
+
+ --odl-grid-line: rgba(18, 22, 27, 0.05);
+ --odl-shadow-sm: 0 1px 2px rgba(10, 13, 17, 0.06),
+ 0 1px 1px rgba(10, 13, 17, 0.04);
+ --odl-shadow-md: 0 4px 12px rgba(10, 13, 17, 0.07),
+ 0 1px 3px rgba(10, 13, 17, 0.05);
+ --odl-shadow-lg: 0 18px 40px rgba(10, 13, 17, 0.1),
+ 0 4px 12px rgba(10, 13, 17, 0.06);
+}
+/* ---- 2b. Semantic tokens — dark ------------------------------------------
*/
+[data-theme="dark"] {
+ --odl-bg: #0a0d11;
+ --odl-bg-subtle: #0e1218;
+ --odl-bg-inset: #12161d;
+ --odl-surface: #11161c;
+ --odl-surface-2: #161c24;
+ --odl-fg: #e7ebf0;
+ --odl-fg-strong: #f6f8fa;
+ --odl-fg-muted: #9aa6b3;
+ --odl-fg-subtle: #76828f;
+ --odl-border: #242c35;
+ --odl-border-strong: #313b46;
+ --odl-bar: #f6f8fa;
+
+ --odl-accent: var(--odl-blue-300);
+ --odl-accent-hover: var(--odl-blue-200);
+ --odl-accent-subtle: rgba(44, 99, 245, 0.14);
+ --odl-accent-border: rgba(133, 169, 255, 0.32);
+ --odl-link: var(--odl-blue-300);
+
+ --odl-action-bg: var(--odl-blue-500);
+ --odl-action-bg-hover: var(--odl-blue-400);
+ --odl-action-fg: #ffffff;
+
+ --odl-code-bg: #0e131a;
+ --odl-code-border: #232b35;
+ --odl-selection: rgba(44, 99, 245, 0.36);
+
+ --odl-grid-line: rgba(255, 255, 255, 0.045);
+ --odl-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
+ --odl-shadow-md: 0 6px 18px rgba(0, 0, 0, 0.5);
+ --odl-shadow-lg: 0 22px 48px rgba(0, 0, 0, 0.6);
+}
+
+/* ---- 3. Project onto Infima so docs/blog inherit the system --------------
*/
+:root {
+ --ifm-color-primary: #1e54e0;
+ --ifm-color-primary-dark: #1b4bca;
+ --ifm-color-primary-darker: #1846be;
+ --ifm-color-primary-darkest: #163a9c;
+ --ifm-color-primary-light: #3568ef;
+ --ifm-color-primary-lighter: #4c79f1;
+ --ifm-color-primary-lightest: #79a0f5;
+
+ --ifm-background-color: var(--odl-bg);
+ --ifm-background-surface-color: var(--odl-surface);
+ --ifm-font-color-base: var(--odl-fg);
+ --ifm-heading-color: var(--odl-fg-strong);
+ --ifm-font-family-base: var(--odl-font-sans);
+ --ifm-heading-font-family: var(--odl-font-sans);
+ --ifm-font-family-monospace: var(--odl-font-mono);
+ --ifm-font-size-base: 16px;
+ --ifm-line-height-base: 1.6;
+ --ifm-link-color: var(--odl-link);
+ --ifm-link-hover-color: var(--odl-accent-hover);
+
+ --ifm-heading-font-weight: 650;
+ --ifm-font-weight-semibold: 600;
+
+ --ifm-navbar-background-color: rgba(255, 255, 255, 0.82);
+ --ifm-navbar-shadow: inset 0 -1px 0 var(--odl-border);
+ --ifm-navbar-link-color: var(--odl-fg-muted);
+ --ifm-navbar-link-hover-color: var(--odl-fg-strong);
+
+ --ifm-toc-border-color: var(--odl-border);
+ --ifm-color-emphasis-200: var(--odl-border);
+ --ifm-color-emphasis-300: var(--odl-border-strong);
+
+ --ifm-code-font-size: 0.9em;
+ --ifm-code-background: var(--odl-code-bg);
+ --ifm-code-border-radius: var(--odl-radius-sm);
+ --ifm-pre-background: var(--odl-code-bg);
+
+ --ifm-button-border-radius: var(--odl-radius-md);
+ --ifm-global-radius: var(--odl-radius-md);
+
+ --docusaurus-highlighted-code-line-bg: rgba(30, 84, 224, 0.1);
+
+ --footer-font-color: var(--odl-fg-muted);
--footer-column-width: unset;
- --footer-row-max-width: 1000px;
+ --footer-row-max-width: 1100px;
}
-/* For readability concerns, you should choose a lighter palette in dark mode.
*/
-[data-theme='dark'] {
- --ifm-color-primary: #25c2a0;
- --ifm-color-primary-dark: #21af90;
- --ifm-color-primary-darker: #1fa588;
- --ifm-color-primary-darkest: #1a8870;
- --ifm-color-primary-light: #29d5b0;
- --ifm-color-primary-lighter: #32d8b4;
- --ifm-color-primary-lightest: #4fddbf;
- --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3);
+[data-theme="dark"] {
+ --ifm-color-primary: #5384ff;
+ --ifm-color-primary-dark: #3568ef;
+ --ifm-color-primary-darker: #2c63f5;
+ --ifm-color-primary-darkest: #1e54e0;
+ --ifm-color-primary-light: #79a0f5;
+ --ifm-color-primary-lighter: #95b5f8;
+ --ifm-color-primary-lightest: #b4ccff;
- --footer-font-color: white;
+ --ifm-navbar-background-color: rgba(10, 13, 17, 0.78);
+ --ifm-navbar-shadow: inset 0 -1px 0 var(--odl-border);
+ --docusaurus-highlighted-code-line-bg: rgba(83, 132, 255, 0.16);
}
-.header-github-link:hover {
- opacity: 0.6;
+/* ---- 4. Base element styling ---------------------------------------------
*/
+html {
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
}
-.header-github-link::before {
- content: '';
- width: 24px;
- height: 24px;
- display: flex;
- background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12
0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577
0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633
17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236
1.07 1.835 2.809 1.305
3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93
0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.52 [...]
- no-repeat;
+body {
+ background-color: var(--odl-bg);
+ color: var(--odl-fg);
+ font-feature-settings: "cv05" 1, "ss01" 1;
}
-[data-theme='dark'] .header-github-link::before {
- background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63
0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577
0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633
17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236
1.07 1.835 2.809 1.305
3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93
0-1.31.465-2.38 1.235-3.22-.135- [...]
- no-repeat;
+h1,
+h2,
+h3,
+h4 {
+ letter-spacing: var(--odl-tracking-tight);
}
+::selection {
+ background: var(--odl-selection);
+}
+
+:focus-visible {
+ outline: 2px solid var(--odl-accent);
+ outline-offset: 2px;
+ border-radius: var(--odl-radius-xs);
+}
+
+/* Translucent, blurred navbar for a precise, layered feel */
+.navbar {
+ backdrop-filter: saturate(180%) blur(12px);
+ -webkit-backdrop-filter: saturate(180%) blur(12px);
+}
+
+.navbar__logo {
+ height: 1.85rem;
+}
+.navbar__logo img {
+ height: 100%;
+}
+.navbar__title {
+ font-weight: 650;
+ letter-spacing: var(--odl-tracking-tight);
+}
+
+/* Tabs/Prism code surfaces align to the system */
+.theme-code-block,
+pre[class*="language-"] {
+ border: 1px solid var(--odl-code-border);
+ border-radius: var(--odl-radius-md);
+}
+
+/* ---- 5. Brand motif: the solid "bar" -------------------------------------
*/
+/* The signature OpenDAL bar — a sharp solid rectangle used as a structural
+ marker, divider and emphasis throughout the system. */
+.odl-bar {
+ display: inline-block;
+ width: 2.25rem;
+ height: 0.5rem;
+ background: var(--odl-bar);
+ border-radius: 0;
+}
+
+/* Eyebrow / kicker label: monospace, uppercase, led by a small accent bar */
+.odl-eyebrow {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--odl-space-3);
+ font-family: var(--odl-font-mono);
+ font-size: var(--odl-text-xs);
+ font-weight: 500;
+ letter-spacing: var(--odl-tracking-label);
+ text-transform: uppercase;
+ color: var(--odl-fg-muted);
+}
+.odl-eyebrow::before {
+ content: "";
+ width: 1.5rem;
+ height: 0.5rem;
+ background: var(--odl-accent);
+ flex: none;
+}
+
+/* Shared layout helpers used by landing sections */
+.odl-container {
+ width: 100%;
+ max-width: var(--odl-container);
+ margin-inline: auto;
+ padding-inline: var(--odl-gutter);
+}
+
+/* Faint blueprint grid — engineering texture for select surfaces */
+.odl-grid-bg {
+ background-image: linear-gradient(
+ var(--odl-grid-line) 1px,
+ transparent 1px
+ ),
+ linear-gradient(90deg, var(--odl-grid-line) 1px, transparent 1px);
+ background-size: 44px 44px;
+}
+
+/* Tabular figures for any numeric/metric display */
+.odl-tabular {
+ font-family: var(--odl-font-mono);
+ font-variant-numeric: tabular-nums;
+}
+
+/* ---- 6. Header icons (GitHub / Discord) ----------------------------------
*/
+.header-github-link:hover,
.header-discord-link:hover {
opacity: 0.6;
}
-/* Already suitable for dark mode */
-.header-discord-link::before {
- content: '';
- width: 24px;
- height: 24px;
+.header-github-link::before {
+ content: "";
+ width: 22px;
+ height: 22px;
display: flex;
- background: url("data:image/svg+xml,%3Csvg
xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor'
class='bi bi-discord' viewBox='0 0 16 16'%3E%3Cpath d='M13.545 2.907a13.227
13.227 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19
12.19 0 0 0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0
0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356
6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995
2.0 [...]
- no-repeat;
+ background-color: var(--odl-fg-muted);
+ -webkit-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12
0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577
0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633
17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236
1.07 1.835 2.809 1.305
3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93
0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1. [...]
+ no-repeat center / contain;
+ mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24'
xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12
0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577
0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633
17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236
1.07 1.835 2.809 1.305
3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93
0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105- [...]
+ no-repeat center / contain;
}
-/* set copyright font size */
-.footer__copyright {
- font-size: .7em;
+.header-discord-link::before {
+ content: "";
+ width: 22px;
+ height: 22px;
+ display: flex;
+ background-color: var(--odl-fg-muted);
+ -webkit-mask: url("data:image/svg+xml,%3Csvg
xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='currentColor'
viewBox='0 0 16 16'%3E%3Cpath d='M13.545 2.907a13.227 13.227 0 0
0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0
0-3.658 0 8.258 8.258 0 0 0-.412-.833.051.051 0 0
0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0 0 0-.021.018C.356
6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276 13.276 0 0 0 3.995
2.02.05.05 0 0 0 .056-. [...]
+ no-repeat center / contain;
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'
width='24' height='24' fill='currentColor' viewBox='0 0 16 16'%3E%3Cpath
d='M13.545 2.907a13.227 13.227 0 0 0-3.257-1.011.05.05 0 0
0-.052.025c-.141.25-.297.577-.406.833a12.19 12.19 0 0 0-3.658 0 8.258 8.258 0 0
0-.412-.833.051.051 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.041.041 0
0 0-.021.018C.356 6.024-.213 9.047.066 12.032c.001.014.01.028.021.037a13.276
13.276 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019c.308 [...]
+ no-repeat center / contain;
}
-.navbar__logo img {
- height: 120%;
+/* ---- 7. Footer -----------------------------------------------------------
*/
+.footer {
+ background-color: var(--odl-bg-subtle);
+ border-top: 1px solid var(--odl-border);
+}
+.footer__copyright {
+ font-size: 0.75em;
+ color: var(--odl-fg-subtle);
+ margin-top: var(--odl-space-4);
}
-
.footer__logo {
margin-top: unset;
+ max-width: 280px;
+}
+[data-theme="dark"] .footer__logo {
+ filter: brightness(0) invert(1) opacity(0.7);
+}
+
+/* ---- 8. Reduced motion ---------------------------------------------------
*/
+@media (prefers-reduced-motion: reduce) {
+ *,
+ *::before,
+ *::after {
+ animation-duration: 0.01ms !important;
+ animation-iteration-count: 1 !important;
+ transition-duration: 0.01ms !important;
+ scroll-behavior: auto !important;
+ }
}
diff --git a/website/src/pages/index.jsx b/website/src/pages/index.jsx
new file mode 100644
index 000000000..b6b6c7b88
--- /dev/null
+++ b/website/src/pages/index.jsx
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import React from "react";
+import Layout from "@theme/Layout";
+import {
+ Hero,
+ UsedBy,
+ ValueProps,
+ Capabilities,
+ Services,
+ Bindings,
+ Layers,
+ FinalCta,
+} from "../components/landing/sections";
+
+export default function Home() {
+ return (
+ <Layout
+ title="One Layer, All Storage"
+ description="Apache OpenDAL™ is an Open Data Access Layer that gives
every language a unified, zero-cost way to access object storage, file systems,
cloud SaaS, databases, protocols and key-value services."
+ >
+ <main>
+ <Hero />
+ <UsedBy />
+ <ValueProps />
+ <Capabilities />
+ <Services />
+ <Bindings />
+ <Layers />
+ <FinalCta />
+ </main>
+ </Layout>
+ );
+}
diff --git a/website/static/img/users/cratedb.png
b/website/static/img/users/cratedb.png
new file mode 100644
index 000000000..f65bfd245
Binary files /dev/null and b/website/static/img/users/cratedb.png differ
diff --git a/website/static/img/users/daft.png
b/website/static/img/users/daft.png
new file mode 100644
index 000000000..cd845656c
Binary files /dev/null and b/website/static/img/users/daft.png differ
diff --git a/website/static/img/users/databend.png
b/website/static/img/users/databend.png
new file mode 100644
index 000000000..908975ca6
Binary files /dev/null and b/website/static/img/users/databend.png differ
diff --git a/website/static/img/users/datafusion-comet.png
b/website/static/img/users/datafusion-comet.png
new file mode 100644
index 000000000..c8aaed47d
Binary files /dev/null and b/website/static/img/users/datafusion-comet.png
differ
diff --git a/website/static/img/users/dify.png
b/website/static/img/users/dify.png
new file mode 100644
index 000000000..d4c3195c1
Binary files /dev/null and b/website/static/img/users/dify.png differ
diff --git a/website/static/img/users/gravitino.png
b/website/static/img/users/gravitino.png
new file mode 100644
index 000000000..c8aaed47d
Binary files /dev/null and b/website/static/img/users/gravitino.png differ
diff --git a/website/static/img/users/greptimedb.png
b/website/static/img/users/greptimedb.png
new file mode 100644
index 000000000..de5d5eb61
Binary files /dev/null and b/website/static/img/users/greptimedb.png differ
diff --git a/website/static/img/users/hasura.png
b/website/static/img/users/hasura.png
new file mode 100644
index 000000000..bfd043810
Binary files /dev/null and b/website/static/img/users/hasura.png differ
diff --git a/website/static/img/users/iceberg-rust.png
b/website/static/img/users/iceberg-rust.png
new file mode 100644
index 000000000..c8aaed47d
Binary files /dev/null and b/website/static/img/users/iceberg-rust.png differ
diff --git a/website/static/img/users/kubeflow-trainer.png
b/website/static/img/users/kubeflow-trainer.png
new file mode 100644
index 000000000..8cc9ba859
Binary files /dev/null and b/website/static/img/users/kubeflow-trainer.png
differ
diff --git a/website/static/img/users/lance.png
b/website/static/img/users/lance.png
new file mode 100644
index 000000000..ed288e5b1
Binary files /dev/null and b/website/static/img/users/lance.png differ
diff --git a/website/static/img/users/llamaindex.png
b/website/static/img/users/llamaindex.png
new file mode 100644
index 000000000..731c902e4
Binary files /dev/null and b/website/static/img/users/llamaindex.png differ
diff --git a/website/static/img/users/lnx.png b/website/static/img/users/lnx.png
new file mode 100644
index 000000000..dfae753ef
Binary files /dev/null and b/website/static/img/users/lnx.png differ
diff --git a/website/static/img/users/loco.png
b/website/static/img/users/loco.png
new file mode 100644
index 000000000..e952fa04e
Binary files /dev/null and b/website/static/img/users/loco.png differ
diff --git a/website/static/img/users/octobase.png
b/website/static/img/users/octobase.png
new file mode 100644
index 000000000..aaf33dfe3
Binary files /dev/null and b/website/static/img/users/octobase.png differ
diff --git a/website/static/img/users/openraft.png
b/website/static/img/users/openraft.png
new file mode 100644
index 000000000..908975ca6
Binary files /dev/null and b/website/static/img/users/openraft.png differ
diff --git a/website/static/img/users/pants.png
b/website/static/img/users/pants.png
new file mode 100644
index 000000000..b9ebd5fbf
Binary files /dev/null and b/website/static/img/users/pants.png differ
diff --git a/website/static/img/users/pathway.png
b/website/static/img/users/pathway.png
new file mode 100644
index 000000000..8939308f2
Binary files /dev/null and b/website/static/img/users/pathway.png differ
diff --git a/website/static/img/users/questdb.png
b/website/static/img/users/questdb.png
new file mode 100644
index 000000000..663fb3c66
Binary files /dev/null and b/website/static/img/users/questdb.png differ
diff --git a/website/static/img/users/quickwit.png
b/website/static/img/users/quickwit.png
new file mode 100644
index 000000000..230241887
Binary files /dev/null and b/website/static/img/users/quickwit.png differ
diff --git a/website/static/img/users/ragflow.png
b/website/static/img/users/ragflow.png
new file mode 100644
index 000000000..5bc5c186f
Binary files /dev/null and b/website/static/img/users/ragflow.png differ
diff --git a/website/static/img/users/risingwave.png
b/website/static/img/users/risingwave.png
new file mode 100644
index 000000000..ece3bd7d5
Binary files /dev/null and b/website/static/img/users/risingwave.png differ
diff --git a/website/static/img/users/robustmq.png
b/website/static/img/users/robustmq.png
new file mode 100644
index 000000000..d13539b64
Binary files /dev/null and b/website/static/img/users/robustmq.png differ
diff --git a/website/static/img/users/rustic.png
b/website/static/img/users/rustic.png
new file mode 100644
index 000000000..53e2a2773
Binary files /dev/null and b/website/static/img/users/rustic.png differ
diff --git a/website/static/img/users/sccache.png
b/website/static/img/users/sccache.png
new file mode 100644
index 000000000..e5a7b2fc0
Binary files /dev/null and b/website/static/img/users/sccache.png differ
diff --git a/website/static/img/users/seatunnel.png
b/website/static/img/users/seatunnel.png
new file mode 100644
index 000000000..c8aaed47d
Binary files /dev/null and b/website/static/img/users/seatunnel.png differ
diff --git a/website/static/img/users/slatedb.png
b/website/static/img/users/slatedb.png
new file mode 100644
index 000000000..16b3d3e9c
Binary files /dev/null and b/website/static/img/users/slatedb.png differ
diff --git a/website/static/img/users/spiceai.png
b/website/static/img/users/spiceai.png
new file mode 100644
index 000000000..19e0eceef
Binary files /dev/null and b/website/static/img/users/spiceai.png differ
diff --git a/website/static/img/users/vaultwarden.png
b/website/static/img/users/vaultwarden.png
new file mode 100644
index 000000000..394dcae93
Binary files /dev/null and b/website/static/img/users/vaultwarden.png differ
diff --git a/website/static/img/users/vector.png
b/website/static/img/users/vector.png
new file mode 100644
index 000000000..b8a7bf4d9
Binary files /dev/null and b/website/static/img/users/vector.png differ
diff --git a/website/static/img/users/walrus.png
b/website/static/img/users/walrus.png
new file mode 100644
index 000000000..c87b21729
Binary files /dev/null and b/website/static/img/users/walrus.png differ
diff --git a/website/static/img/users/wrenai.png
b/website/static/img/users/wrenai.png
new file mode 100644
index 000000000..5de193a81
Binary files /dev/null and b/website/static/img/users/wrenai.png differ
diff --git a/website/static/img/users/zino.png
b/website/static/img/users/zino.png
new file mode 100644
index 000000000..df460333e
Binary files /dev/null and b/website/static/img/users/zino.png differ