diff --git a/README.md b/README.md index 54c9096..6a24be4 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,17 @@ -

+

- React Native Canvas Kit + React Native Canvas Kit -

+  react-native-canvas-kit +

-# react-native-canvas-kit +

+ npm version + license + types included + runs with expo +

@@ -21,6 +27,21 @@ experience from scratch. > Full guides and API reference live in the > [documentation site](https://adithyavis.github.io/react-native-canvas-kit/intro). +## Showcase + +Popular app experiences rebuilt with Canvas Kit: + +

+ + + + + +

+

+ Canva  ·  Instagram  ·  Snapseed  ·  DocuSign  ·  Google Pay +

+ ## What you get - **Shapes**: `Rect`, `Circle`, `Ellipse`, `Line`, `RegularPolygon`, `Star`, diff --git a/assets/canva.mov b/assets/canva.mov new file mode 100644 index 0000000..e82d46e Binary files /dev/null and b/assets/canva.mov differ diff --git a/assets/docusign.mov b/assets/docusign.mov new file mode 100644 index 0000000..f26e359 Binary files /dev/null and b/assets/docusign.mov differ diff --git a/assets/gpay_scratch_card.mov b/assets/gpay_scratch_card.mov new file mode 100644 index 0000000..a40c659 Binary files /dev/null and b/assets/gpay_scratch_card.mov differ diff --git a/assets/instagram_crop.mov b/assets/instagram_crop.mov new file mode 100644 index 0000000..12f9c83 Binary files /dev/null and b/assets/instagram_crop.mov differ diff --git a/assets/snapseed.mov b/assets/snapseed.mov new file mode 100644 index 0000000..9843bbf Binary files /dev/null and b/assets/snapseed.mov differ diff --git a/docs/docs/core-concepts/infinite-canvas.md b/docs/docs/core-concepts/infinite-canvas.md new file mode 100644 index 0000000..5e3b2a0 --- /dev/null +++ b/docs/docs/core-concepts/infinite-canvas.md @@ -0,0 +1,193 @@ +--- +sidebar_position: 5 +title: Infinite Canvas +--- + +# Infinite Canvas + +Set `infinite` on the [`Stage`](./stage.md) to turn it into a pannable, +zoomable scene: one-finger drag pans the whole scene, two-finger pinch zooms it, +and content can live anywhere in scene coordinates. + +```tsx + + {/* shapes at any coordinates */} + +``` + +Infinite mode is **not** a giant surface. The canvas stays the size of the +viewport; a scene transform (a pan offset and a zoom factor) is applied at the +root of the scene graph. Because every node's on-screen position is already +resolved by walking up to that root, hit-testing, [Portals](../portal/overview.md), +the [Transformer](../interactivity/transformer.md), and snapping all pan and +zoom for free. + +## Gesture behavior + +With `infinite`, gestures route to a shape when one is under the fingers, and to +the scene otherwise: + +| Gesture | On a matching node | On empty space | +| ----------------- | --------------------------------- | --------------------------------- | +| One-finger drag | Drags the node (if `draggable`) | Pans the scene | +| Two-finger pinch | Scales the node (if `scalable`) | Zooms the scene | +| Two-finger rotate | Rotates the node (if `rotatable`) | Nothing — the scene never rotates | + +So a note you can move and resize still declares `draggable scalable`; dragging +it moves the note, while dragging the background pans the board. The scene itself +only ever pans and zooms — it does not rotate. + +## Zoom limits + +Clamp how far the scene can zoom with `minZoom` / `maxZoom` (defaults `0.1` and +`10`): + +```tsx + +``` + +## Controlling the zoom + +Attach a `ref` to drive the scene imperatively through the +[`StageHandle`](./stage.md): + +```tsx +const stageRef = useRef(null); + +stageRef.current?.zoomIn(); // zoom in around the viewport center +stageRef.current?.zoomOut(); +stageRef.current?.zoomTo(2, { x: 200, y: 300 }); // zoom to 2x around a focal point +stageRef.current?.panTo(x, y); // set the scene offset +stageRef.current?.centerOn(x, y, 0.7); // center a scene point at a given zoom +stageRef.current?.resetView(); // back to offset 0, zoom 1 +const scene = stageRef.current?.getScene(); // { x, y, scale } +``` + +| Method | Description | +| ---------------------------------- | ---------------------------------------------------------------- | +| `getScene()` | Current scene as `{ x, y, scale }`. | +| `zoomTo(scale, focal?)` | Zoom to `scale` (clamped) around `focal` or the viewport center. | +| `zoomIn(step?)` / `zoomOut(step?)` | Multiply / divide the zoom by `step` (default `1.2`). | +| `panTo(x, y)` | Set the scene offset directly. | +| `centerOn(x, y, scale?)` | Center the scene point `(x, y)` in the viewport at `scale`. | +| `resetView()` | Reset to offset `0`, zoom `1`. | + +Use `centerOn` on mount to open the view on your content instead of the +top-left origin: + +```tsx +useEffect(() => { + stageRef.current?.centerOn(boardCenterX, boardCenterY, 0.7); +}, []); +``` + +### Observing zoom changes + +Pass `onSceneChange` to react to zoom on the JS thread — for example, to render a +live zoom percentage. It fires on integer-percent zoom changes, so it does not +flood React during a pinch: + +```tsx +const [zoom, setZoom] = useState(1); + + setZoom(scene.scale)} +> + {/* ... */} +; + +// elsewhere: {Math.round(zoom * 100)}% +``` + +## Reacting to the changes from children + +`useSceneTransform()` exposes the live scene offset and zoom as shared values to +any component inside the `Stage`. It is the way to build scene-aware overlays — +a dot grid, rulers, a minimap — that stay crisp at every zoom without +re-rendering. Below, a grid keeps a constant on-screen dot spacing and size by +choosing them from the current zoom, entirely on the UI thread: + +```tsx +import { useSceneTransform } from 'react-native-canvas-kit'; +import { Path, Skia } from '@shopify/react-native-skia'; +import { useDerivedValue } from 'react-native-reanimated'; + +function DotGrid() { + const scene = useSceneTransform(); + + const path = useDerivedValue(() => { + const p = Skia.Path.Make(); + if (!scene) return p; + const scale = scene.sceneScaleSV.value.x || 1; + const offset = scene.sceneOffsetSV.value; + + const left = -offset.x / scale; + const top = -offset.y / scale; + const right = (scene.width - offset.x) / scale; + const bottom = (scene.height - offset.y) / scale; + + let spacing = 32; + while (spacing * scale < 24) spacing *= 2; + while (spacing * scale >= 48) spacing /= 2; + + const startX = Math.floor(left / spacing) * spacing; + const startY = Math.floor(top / spacing) * spacing; + for (let x = startX; x <= right; x += spacing) { + for (let y = startY; y <= bottom; y += spacing) { + p.addCircle(x, y, radius); + } + } + return p; + }, [scene]); + + return ; +} +``` + +`useSceneTransform()` returns `{ sceneOffsetSV, sceneScaleSV, width, height }`, +or `null` outside a `Stage`. The scene scale is a `Vector2d`; the zoom is +uniform, so read `.x`. + +## Rendering large scenes + +The scene transform itself is cheap — it is one group transform on the GPU. What +costs, at scale, is **the number of nodes**. Every canvas-kit node (a `Rect`, +`Circle`, `Group`, …) is registered for hit-testing and carries its own +transform, so a scene with thousands of them pays for all of them every frame, +even the off-screen ones (Skia quick-rejects their _fill_, but the node tree is +still walked). + +A few guidelines: + +- **Interactive objects** (things you tap, drag, or transform) are the right use + for canvas-kit nodes. Keep this to what the user actually manipulates. +- **Bulk decoration** (grids, backgrounds, particle fields) should be a single + Skia primitive, like the `DotGrid` above — one node whose drawing is computed + in a worklet, not one node per dot. +- **Large fields of static content** are cheapest drawn without per-item nodes: + render them as raw `@shopify/react-native-skia` components (`RoundedRect`, + `Text`, …) in a `React.memo`'d subtree so they mount once and are not + reconciled on pan/zoom, or record them into a single ``. Promote only + the item currently being edited to a live, interactive node. + +### Culling to the viewport + +For very large scenes, `useRenderBounds(margin)` gives you the visible scene +rectangle (expanded by `margin`) as a **shared value**, updated on the UI thread +with hysteresis so it does not fire every frame. Consume it in a worklet to draw +only what is on screen, and use `rectIntersectsBounds` to test each item: + +```tsx +import { useRenderBounds, rectIntersectsBounds } from 'react-native-canvas-kit'; + +const boundsSV = useRenderBounds(300); // SharedValue +``` + +Because it is a shared value, `useRenderBounds` is for **worklet-driven** +rendering (a `` or a Skia drawing). Mounting or unmounting canvas-kit +_nodes_ based on visibility is a React operation and must be driven from state, +so windowing a node list re-renders when the visible set changes — expected, and +the same model as `FlatList`. diff --git a/docs/docusaurus.config.ts b/docs/docusaurus.config.ts index c26209e..e213710 100644 --- a/docs/docusaurus.config.ts +++ b/docs/docusaurus.config.ts @@ -58,6 +58,12 @@ const config: Config = { colorMode: { respectPrefersColorScheme: true, }, + algolia: { + appId: 'U9PLQTMVZT', + apiKey: 'fb01f09f8f35cc449627ffcdfe315111', + indexName: 'React Native Canvas Kit', + contextualSearch: false, + }, navbar: { title: 'Canvas Kit', logo: { @@ -72,6 +78,10 @@ const config: Config = { position: 'left', label: 'Docs', }, + { + type: 'search', + position: 'right', + }, { type: 'docsVersionDropdown', position: 'right', diff --git a/docs/package.json b/docs/package.json index fa3bbe6..02263d8 100644 --- a/docs/package.json +++ b/docs/package.json @@ -27,7 +27,11 @@ "typescript": "~5.6.2" }, "browserslist": { - "production": [">0.5%", "not dead", "not op_mini all"], + "production": [ + ">0.5%", + "not dead", + "not op_mini all" + ], "development": [ "last 3 chrome version", "last 3 firefox version", diff --git a/docs/src/components/HomepageFeatures/styles.module.css b/docs/src/components/HomepageFeatures/styles.module.css index e99405e..eec05a2 100644 --- a/docs/src/components/HomepageFeatures/styles.module.css +++ b/docs/src/components/HomepageFeatures/styles.module.css @@ -1,13 +1,9 @@ -/** - * Homepage feature-grid styling. - */ - .features { - padding: 4.5rem 0 5rem; + padding: 5rem 0 5.5rem; } .sectionHead { - max-width: 640px; + max-width: 660px; margin: 0 auto 3rem; text-align: center; } @@ -18,8 +14,9 @@ } .sectionSubtitle { - font-size: 1.1rem; - color: var(--ifm-color-emphasis-700); + font-size: 1.12rem; + line-height: 1.6; + color: var(--ck-muted); margin: 0; } @@ -28,42 +25,67 @@ } .card { + position: relative; height: 100%; - padding: 1.75rem; - border-radius: 14px; + padding: 1.85rem; + border-radius: 1rem; background: var(--ifm-background-surface-color); - border: 1px solid var(--ifm-color-emphasis-200); + border: 1px solid var(--ck-line); + overflow: hidden; transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease; } +.card::before { + content: ''; + position: absolute; + inset: 0 0 auto 0; + height: 3px; + background: var(--ck-gradient); + opacity: 0; + transition: opacity 0.18s ease; +} + .card:hover { - transform: translateY(-4px); - border-color: var(--ifm-color-primary); - box-shadow: 0 16px 40px rgba(138, 43, 226, 0.14); + transform: translateY(-5px); + border-color: color-mix(in srgb, var(--ck-indigo) 40%, transparent); + box-shadow: 0 20px 44px var(--ck-shadow); +} + +.card:hover::before { + opacity: 1; } .cardIcon { - font-size: 2rem; + display: inline-flex; + align-items: center; + justify-content: center; + width: 3rem; + height: 3rem; + border-radius: 0.75rem; + font-size: 1.6rem; line-height: 1; - margin-bottom: 1rem; + margin-bottom: 1.1rem; + background: color-mix(in srgb, var(--ck-indigo) 12%, transparent); + border: 1px solid var(--ck-line); } .cardTitle { - font-size: 1.2rem; + font-size: 1.22rem; + letter-spacing: -0.015em; margin-bottom: 0.5rem; } .cardText { - color: var(--ifm-color-emphasis-700); + color: var(--ck-muted); margin: 0; - line-height: 1.6; + line-height: 1.62; } .ctaRow { display: flex; justify-content: center; - margin-top: 1.5rem; + margin-top: 2rem; } diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index bced9d7..2a8dd77 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -1,26 +1,266 @@ -/** - * Global styling for the documentation site. - */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap'); :root { - --ifm-color-primary: #8a2be2; - --ifm-color-primary-dark: #7d1fd1; - --ifm-color-primary-darker: #761dc6; - --ifm-color-primary-darkest: #6118a3; - --ifm-color-primary-light: #973ce6; - --ifm-color-primary-lighter: #9d47e8; - --ifm-color-primary-lightest: #b06ded; - --ifm-code-font-size: 95%; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); + --ck-indigo: #4f46e5; + --ck-indigo-bright: #635bea; + --ck-cyan: #06b6d4; + --ck-cyan-bright: #22d3ee; + --ck-ink: #14121f; + --ck-muted: #dfd5ff; + --ck-gradient: linear-gradient( + 115deg, + var(--ck-indigo) 0%, + var(--ck-cyan) 100% + ); + --ck-glow: rgba(79, 70, 229, 0.13); + --ck-dot: rgba(79, 70, 229, 0.13); + --ck-line: rgba(79, 70, 229, 0.14); + --ck-shadow: rgba(28, 22, 79, 0.13); + --ck-dot-size: 22px; + + --ifm-color-primary: #4f46e5; + --ifm-color-primary-dark: #4038d1; + --ifm-color-primary-darker: #3c34c5; + --ifm-color-primary-darkest: #3129a2; + --ifm-color-primary-light: #635bea; + --ifm-color-primary-lighter: #7069ec; + --ifm-color-primary-lightest: #948ef2; + + --ifm-background-color: #fbfaff; + --ifm-background-surface-color: #ffffff; + --ifm-heading-color: var(--ck-ink); + --ifm-font-color-base: #2a2638; + + --ifm-font-family-base: + 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + --ifm-font-family-monospace: + 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + --ifm-code-font-size: 90%; + --ifm-global-radius: 0.65rem; + --ifm-navbar-background-color: rgba(251, 250, 255, 0.7); + --ifm-navbar-shadow: none; + --ifm-toc-border-color: var(--ck-line); + --ifm-hr-border-color: var(--ck-line); + --ifm-color-emphasis-200: var(--ck-line); + + --docusaurus-highlighted-code-line-bg: rgba(79, 70, 229, 0.1); } [data-theme='dark'] { - --ifm-color-primary: #c79bff; - --ifm-color-primary-dark: #b078ff; - --ifm-color-primary-darker: #a566ff; - --ifm-color-primary-darkest: #8630ff; - --ifm-color-primary-light: #debeff; - --ifm-color-primary-lighter: #e9d0ff; - --ifm-color-primary-lightest: #ffffff; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); + --ck-indigo: #8b83ff; + --ck-indigo-bright: #a49dff; + --ck-cyan: #22d3ee; + --ck-cyan-bright: #67e8f9; + --ck-ink: #eceaf7; + --ck-muted: #9a93b0; + --ck-glow: rgba(120, 110, 255, 0.16); + --ck-dot: rgba(139, 131, 255, 0.15); + --ck-line: rgba(139, 131, 255, 0.14); + --ck-shadow: rgba(0, 0, 0, 0.5); + + --ifm-color-primary: #8b83ff; + --ifm-color-primary-dark: #6f66ff; + --ifm-color-primary-darker: #635aff; + --ifm-color-primary-darkest: #3f34ff; + --ifm-color-primary-light: #a49dff; + --ifm-color-primary-lighter: #b1abff; + --ifm-color-primary-lightest: #dedaff; + + --ifm-background-color: #0a0912; + --ifm-background-surface-color: #12111d; + --ifm-font-color-base: #d7d3e6; + --ifm-navbar-background-color: rgba(10, 9, 18, 0.7); + --ifm-color-emphasis-200: var(--ck-line); + + --docusaurus-highlighted-code-line-bg: rgba(139, 131, 255, 0.13); +} + +html { + font-feature-settings: + 'cv02' 1, + 'cv11' 1, + 'ss01' 1; + -webkit-font-smoothing: antialiased; +} + +body { + background-color: var(--ifm-background-color); + background-image: + radial-gradient(65% 40% at 50% -6%, var(--ck-glow), transparent 70%), + radial-gradient(var(--ck-dot) 1px, transparent 1.4px); + background-size: + 100% 100%, + var(--ck-dot-size) var(--ck-dot-size); + background-attachment: fixed, fixed; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + letter-spacing: -0.02em; + font-weight: 700; +} + +.markdown h1:first-child { + font-size: 2.6rem; + letter-spacing: -0.032em; +} + +.markdown h2 { + padding-bottom: 0.3rem; + border-bottom: 1px solid var(--ck-line); +} + +.navbar { + backdrop-filter: saturate(180%) blur(14px); + -webkit-backdrop-filter: saturate(180%) blur(14px); + border-bottom: 1px solid var(--ck-line); +} + +.navbar__title { + font-weight: 800; + letter-spacing: -0.02em; + background: var(--ck-gradient); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +.navbar__link { + font-weight: 500; +} + +.menu { + padding: 0.75rem 0.5rem; + font-weight: 500; +} + +.menu__link { + border-radius: 0.55rem; + transition: + background 0.15s ease, + color 0.15s ease; +} + +.menu__link:hover, +.menu__list-item-collapsible:hover { + background: color-mix(in srgb, var(--ck-indigo) 9%, transparent); + border-radius: 0.55rem; +} + +.menu__link--active:not(.menu__link--sublist) { + background: color-mix(in srgb, var(--ck-indigo) 15%, transparent); + color: var(--ck-indigo); + font-weight: 600; +} + +.table-of-contents__link--active { + color: var(--ck-indigo); + font-weight: 600; +} + +.table-of-contents { + border-left-color: var(--ck-line); +} + +a { + text-decoration-color: color-mix( + in srgb, + var(--ifm-color-primary) 45%, + transparent + ); + text-underline-offset: 0.15em; +} + +.button--primary { + --ifm-button-background-color: #4f46e5; + --ifm-button-border-color: #4f46e5; + color: #ffffff; + font-weight: 600; + box-shadow: 0 10px 24px color-mix(in srgb, var(--ck-indigo) 28%, transparent); + transition: + transform 0.16s ease, + box-shadow 0.16s ease, + filter 0.16s ease; +} + +.button--primary:hover { + --ifm-button-background-color: #4338ca; + --ifm-button-border-color: #4338ca; + color: #ffffff; + transform: translateY(-2px); + box-shadow: 0 16px 32px color-mix(in srgb, var(--ck-indigo) 36%, transparent); +} + +code { + background: color-mix(in srgb, var(--ck-indigo) 8%, transparent); + border: 1px solid var(--ck-line); + border-radius: 0.4rem; + font-weight: 500; +} + +.theme-code-block, +div[class*='codeBlockContainer'] { + border-radius: 0.75rem; + box-shadow: 0 14px 34px var(--ck-shadow); + border: 1px solid var(--ck-line); +} + +.theme-doc-sidebar-container { + border-right: 1px solid var(--ck-line); +} + +.pagination-nav__link { + border: 1px solid var(--ck-line); + border-radius: 0.75rem; + transition: + border-color 0.15s ease, + box-shadow 0.15s ease, + transform 0.15s ease; +} + +.pagination-nav__link:hover { + border-color: var(--ck-indigo); + box-shadow: 0 12px 28px var(--ck-shadow); + transform: translateY(-2px); +} + +.pagination-nav__label { + font-weight: 600; +} + +.alert { + border: 1px solid var(--ck-line); + border-left-width: 3px; + border-radius: 0.6rem; +} + +blockquote { + border-left: 3px solid var(--ck-indigo); + background: color-mix(in srgb, var(--ck-indigo) 7%, transparent); + border-radius: 0 0.6rem 0.6rem 0; +} + +table thead tr { + background: color-mix(in srgb, var(--ck-indigo) 8%, transparent); +} + +table th { + font-weight: 600; +} + +.footer { + background: linear-gradient(180deg, #100e20 0%, #08060f 100%); + border-top: 1px solid rgba(139, 131, 255, 0.14); +} + +.footer__title { + color: #dedaff; +} + +::selection { + background: color-mix(in srgb, var(--ck-indigo) 28%, transparent); } diff --git a/docs/src/pages/index.module.css b/docs/src/pages/index.module.css index 3fdd7bc..aa738fe 100644 --- a/docs/src/pages/index.module.css +++ b/docs/src/pages/index.module.css @@ -1,12 +1,53 @@ .hero { - padding: 5rem 1rem 4rem; - background: linear-gradient(150deg, #2a0a45 0%, #1b0030 45%, #10001f 100%); - color: #f4ecff; + position: relative; + padding: 6rem 1rem 5rem; + background: + radial-gradient( + 75% 60% at 50% -10%, + rgba(99, 91, 234, 0.4), + transparent 68% + ), + linear-gradient(160deg, #171338 0%, #0f0d28 50%, #080714 100%); + color: #eef0ff; overflow: hidden; } +.hero::before { + content: ''; + position: absolute; + inset: 0; + background-image: + linear-gradient(rgba(139, 131, 255, 0.09) 1px, transparent 1px), + linear-gradient(90deg, rgba(139, 131, 255, 0.09) 1px, transparent 1px); + background-size: 48px 48px; + mask-image: radial-gradient(72% 72% at 50% 28%, #000 0%, transparent 82%); + -webkit-mask-image: radial-gradient( + 72% 72% at 50% 28%, + #000 0%, + transparent 82% + ); + pointer-events: none; +} + +.hero::after { + content: ''; + position: absolute; + right: -140px; + bottom: -170px; + width: 440px; + height: 440px; + background: radial-gradient( + circle, + rgba(34, 211, 238, 0.22), + transparent 66% + ); + pointer-events: none; +} + .heroInner { - max-width: 820px; + position: relative; + z-index: 1; + max-width: 840px; margin: 0 auto; text-align: center; } @@ -20,14 +61,15 @@ align-items: center; gap: 0.5rem; padding: 0.35rem 0.9rem 0.35rem 0.5rem; - margin-bottom: 1.25rem; + margin-bottom: 1.5rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; letter-spacing: 0.02em; - color: #e9d0ff; - background: rgba(199, 155, 255, 0.14); - border: 1px solid rgba(199, 155, 255, 0.35); + color: #d7d4ff; + background: rgba(139, 131, 255, 0.12); + border: 1px solid rgba(139, 131, 255, 0.34); + backdrop-filter: blur(6px); } .badgeIcon { @@ -37,25 +79,27 @@ } .heroTitle { - font-size: 3.25rem; - line-height: 1.05; + font-size: 3.6rem; + line-height: 1.03; + letter-spacing: -0.035em; + font-weight: 800; margin-bottom: 1.25rem; - background: linear-gradient(90deg, #ffffff 0%, #c79bff 100%); + background: linear-gradient(105deg, #ffffff 0%, #b9b4ff 50%, #67e8f9 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; } .heroTagline { - font-size: 1.2rem; + font-size: 1.25rem; line-height: 1.6; - color: #d7c7ee; - margin-bottom: 2rem; + color: #c7c4e6; + margin-bottom: 2.25rem; } .heroTagline code { - background: rgba(199, 155, 255, 0.16); - color: #e9d0ff; + background: rgba(139, 131, 255, 0.16); + color: #d7d4ff; border: none; } @@ -66,17 +110,30 @@ gap: 0.85rem; } -.ghostButton { - color: #e9d0ff; - border-color: rgba(199, 155, 255, 0.5); +.docsButton { + --ifm-button-color: #ffffff; + --ifm-button-background-color: rgba(255, 255, 255, 0.06); + --ifm-button-border-color: rgba(139, 131, 255, 0.5); + backdrop-filter: blur(6px); + transition: + background 0.16s ease, + border-color 0.16s ease, + transform 0.16s ease; +} + +.docsButton:hover { + --ifm-button-color: #ffffff; + --ifm-button-background-color: rgba(255, 255, 255, 0.13); + --ifm-button-border-color: rgba(139, 131, 255, 0.85); + transform: translateY(-2px); } @media (max-width: 996px) { .hero { - padding: 3rem 1rem; + padding: 3.5rem 1rem; } .heroTitle { - font-size: 2.5rem; + font-size: 2.6rem; } } diff --git a/docs/src/pages/index.tsx b/docs/src/pages/index.tsx index bbb4232..6354983 100644 --- a/docs/src/pages/index.tsx +++ b/docs/src/pages/index.tsx @@ -37,7 +37,10 @@ function HomepageHero() { > Get Started - + Read the Docs