feat: Zoo Design System — tokens, components, brand assets, guidelines

Monochrome, dark by default. One hue rendered through an opacity ladder.
Tokens (color, type, spacing, radius, elevation, motion) + core/forms/navigation/
overlays components + brand guidelines + ui kit. The single source of truth for
every Zoo surface — loadable and editable.
This commit is contained in:
Zoo Labs
2026-07-24 17:21:13 -07:00
commit d9aebcdfd4
146 changed files with 2805 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
.DS_Store
*.log
dist/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Hanzo AI, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+57
View File
@@ -0,0 +1,57 @@
<div align="center">
# Zoo Design System
**Monochrome. Dark by default. One hue rendered through an opacity ladder.**
The single source of truth for how every Zoo surface looks — tokens, components, brand assets, and the guidelines that hold them together.
`@zooai/design`
</div>
## Use it
One import pulls in the whole token layer (fonts, color, type, spacing, radius, elevation, motion):
```css
@import "@zooai/design/styles.css";
```
Everything below is expressed as CSS custom properties, so code copies over 1:1 — the semantic names match `zoo.ngo`'s variables exactly.
```jsx
import { Button } from "@zooai/design/components/core/Button.jsx";
import { ZooLogo } from "@zooai/design/components/core/ZooLogo.jsx";
<Button pill>Try Zoo</Button>
```
## What's inside
| Path | What |
|------|------|
| `styles.css` | The one entry point — imports every token file below. |
| `tokens/` | The palette. `colors` (monochrome opacity ladder, dark-default), `typography`, `spacing`, `radius`, `elevation`, `motion`, `z`, `fonts`, `base`. |
| `components/` | `core` (Button, Card, Badge, Icon, ZooLogo, Avatar, ChromeText…), `forms`, `overlays`, `navigation` — each as `.jsx` + `.d.ts` + a `.prompt.md` usage guide. |
| `prompts/` | System-level generation guidance — the "make it look Zoo" system prompt, do/don't rules, page prompts. |
| `content/` | The words — brand voice and taglines. |
| `docs/` | How to use the system — integrate the tokens, theme, extend. |
| `guidelines/` | Specimen cards — color, type, spacing, brand, iconography — the visual reference. |
| `assets/` | The mark, wordmark, favicon, provider + partner logos, brand imagery. |
| `ui_kits/` | Composed surfaces (e.g. `SiteChrome`) assembled from the components. |
## Principles
- **Monochrome by construction** — one neutral ladder plus an opacity ladder is the entire palette. Color appears only as genuine semantics (live/error/warning).
- **Dark is the default theme** — surfaces mount dark-first; light is the override.
- **Self-contained components** — inline styles, no CSS-framework coupling, so a component drops into any host (Next, Vite, Tamagui, none) and renders identically.
- **Every component ships its own `.prompt.md`** — a one-screen usage guide for humans and AI alike.
## Source of truth
Tokens track `zoo.ngo` (`app/globals.css`, `tailwind.config.ts`, `DESIGN.md`) and the press kit. When the brand moves, it moves here first.
## License
MIT. Brand marks (the Zoo logo, partner and provider logos) are the property of their respective owners and are provided for identification.
Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="6" fill="black"/>
<path d="M10 10H14L16 16L18 10H22" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 22H22" stroke="white" stroke-width="2" stroke-linecap="round"/>
<circle cx="16" cy="16" r="12" stroke="white" stroke-width="1" stroke-opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

+6
View File
@@ -0,0 +1,6 @@
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="6" fill="black"/>
<path d="M10 10H14L16 16L18 10H22" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 22H22" stroke="white" stroke-width="2" stroke-linecap="round"/>
<circle cx="16" cy="16" r="12" stroke="white" stroke-width="1" stroke-opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

+6
View File
@@ -0,0 +1,6 @@
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="6" fill="black"/>
<path d="M10 10H14L16 16L18 10H22" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 22H22" stroke="white" stroke-width="2" stroke-linecap="round"/>
<circle cx="16" cy="16" r="12" stroke="white" stroke-width="1" stroke-opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

+6
View File
@@ -0,0 +1,6 @@
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="6" fill="black"/>
<path d="M10 10H14L16 16L18 10H22" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 22H22" stroke="white" stroke-width="2" stroke-linecap="round"/>
<circle cx="16" cy="16" r="12" stroke="white" stroke-width="1" stroke-opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 418 B

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M17.304 3.541h-3.672l6.696 16.918h3.672l-6.696-16.918zM6.696 3.541 0 20.459h3.744l1.32-3.456h6.624l1.296 3.456h3.744L10.08 3.541H6.696zm.576 10.632 2.136-5.616 2.112 5.616H7.272z"></path>
</svg>

After

Width:  |  Height:  |  Size: 286 B

+4
View File
@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M20.5 9.5C19.5 6.5 16.5 4 12 3.5 7 3 3 6.5 2 10.5c-.8 4 .8 7.5 4 9.5 1 .5 2 .2 2-1 0-1.5-1-3-1-5 0-2.8 2.2-5 5-5s5 2.2 5 5c0 2-1 3.5-1 5 0 1.2 1 1.5 2 1 3.2-2 4.8-5.5 2.5-10.5z"></path>
<circle cx="12" cy="11.5" r="1.5"></circle>
</svg>

After

Width:  |  Height:  |  Size: 330 B

+6
View File
@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"></path>
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"></path>
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"></path>
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"></path>
</svg>

After

Width:  |  Height:  |  Size: 679 B

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M13.5 2.5A9.5 9.5 0 0 0 4 12a9.5 9.5 0 0 0 9.5 9.5A9.5 9.5 0 0 0 23 12h-4a5.5 5.5 0 0 1-5.5 5.5A5.5 5.5 0 0 1 8 12a5.5 5.5 0 0 1 5.5-5.5V2.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 249 B

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-3.5 7c.83 0 1.5.67 1.5 1.5S9.33 12 8.5 12 7 11.33 7 10.5 7.67 9 8.5 9zm7 0c.83 0 1.5.67 1.5 1.5s-.67 1.5-1.5 1.5-1.5-.67-1.5-1.5.67-1.5 1.5-1.5zM8 14h8c.55 0 1 .45 1 1s-.45 1-1 1H8c-.55 0-1-.45-1-1s.45-1 1-1zm1.5 3c.28 0 .5.22.5.5v.5h4v-.5c0-.28.22-.5.5-.5s.5.22.5.5v1c0 .28-.22.5-.5.5h-5c-.28 0-.5-.22-.5-.5v-1c0-.28.22-.5.5-.5z"></path>
</svg>

After

Width:  |  Height:  |  Size: 505 B

+22
View File
@@ -0,0 +1,22 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<rect x="2" y="2" width="4" height="4"></rect>
<rect x="10" y="2" width="4" height="4"></rect>
<rect x="18" y="2" width="4" height="4"></rect>
<rect x="2" y="6" width="4" height="4"></rect>
<rect x="6" y="6" width="4" height="4"></rect>
<rect x="10" y="6" width="4" height="4"></rect>
<rect x="14" y="6" width="4" height="4"></rect>
<rect x="18" y="6" width="4" height="4"></rect>
<rect x="2" y="10" width="4" height="4"></rect>
<rect x="6" y="10" width="4" height="4"></rect>
<rect x="10" y="10" width="4" height="4"></rect>
<rect x="14" y="10" width="4" height="4"></rect>
<rect x="18" y="10" width="4" height="4"></rect>
<rect x="2" y="14" width="4" height="4"></rect>
<rect x="10" y="14" width="4" height="4"></rect>
<rect x="14" y="14" width="4" height="4"></rect>
<rect x="18" y="14" width="4" height="4"></rect>
<rect x="2" y="18" width="4" height="4"></rect>
<rect x="10" y="18" width="4" height="4"></rect>
<rect x="18" y="18" width="4" height="4"></rect>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.8956zm16.0993 3.8558L12.6 8.3829l2.02-1.1638a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M16.778 1.844v1.919q-.569-.026-1.138-.032-.708-.008-1.415.037c-1.93.126-4.023.728-6.149 2.237-2.911 2.066-2.731 1.95-4.14 2.75-.396.223-1.342.574-2.185.798-.841.225-1.753.333-1.751.333v4.229s.768.108 1.61.333c.842.224 1.789.575 2.185.799 1.41.798 1.228.683 4.14 2.75 2.126 1.509 4.22 2.11 6.148 2.236.88.058 1.716.041 2.555.005v1.918l7.222-4.168-7.222-4.17v2.176c-.86.038-1.611.065-2.278.021-1.364-.09-2.417-.357-3.979-1.465-2.244-1.593-2.866-2.027-3.68-2.508.889-.518 1.449-.906 3.822-2.59 1.56-1.109 2.614-1.377 3.978-1.466.667-.044 1.418-.017 2.278.02v2.176L24 6.014Z"></path></svg>

After

Width:  |  Height:  |  Size: 674 B

+12
View File
@@ -0,0 +1,12 @@
<svg width="800px" height="800px" viewBox="0 -30 150 150" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#000">
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
<g id="SVGRepo_iconCarrier"> <path d="M122.714 62.703c5.28-.643 16.924-2.027 18.997.643 2.072 2.621-2.319 13.649-4.293 18.544-.592 1.484.691 2.077 2.023.94 8.684-7.319 10.954-22.6 9.178-24.825-1.777-2.175-17.023-4.055-26.3 2.473-1.431 1.038-1.184 2.423.395 2.225z" fill="#000"></path> <path d="M74.852 89.456c20.28 0 43.865-6.38 60.099-18.396 2.664-1.978.345-4.994-2.369-3.758-18.207 7.714-37.993 11.473-56.003 11.473-26.694 0-52.5-7.368-73.42-19.533-1.827-1.088-3.208.791-1.679 2.176 19.343 17.505 44.951 28.038 73.372 28.038z" fill="#000"></path> <path d="M42.632 32.835c0 1.83.197 3.313.542 4.401a26.505 26.505 0 001.58 3.56c.246.396.345.792.345 1.138 0 .495-.296.99-.938 1.484l-3.108 2.077c-.444.296-.889.445-1.283.445-.494 0-.987-.248-1.48-.693a15.29 15.29 0 01-1.777-2.324 38.28 38.28 0 01-1.53-2.918c-3.848 4.55-8.684 6.825-14.506 6.825-4.145 0-7.45-1.187-9.868-3.56-2.418-2.374-3.652-5.54-3.652-9.495 0-4.204 1.48-7.616 4.49-10.187 3.01-2.572 7.007-3.857 12.09-3.857 1.677 0 3.404.148 5.23.395 1.825.248 3.7.643 5.674 1.088v-3.61c0-3.758-.79-6.379-2.32-7.912-1.578-1.533-4.243-2.274-8.042-2.274-1.727 0-3.503.197-5.329.642a39.247 39.247 0 00-5.329 1.682 14.14 14.14 0 01-1.727.643c-.345.098-.592.148-.79.148-.69 0-1.036-.494-1.036-1.533V6.577c0-.791.1-1.385.346-1.73.246-.347.69-.693 1.381-1.04 1.727-.89 3.8-1.631 6.217-2.225C20.23.94 22.797.642 25.51.642c5.872 0 10.164 1.336 12.927 4.006 2.714 2.67 4.096 6.726 4.096 12.165v16.022h.099zm-20.033 7.517c1.628 0 3.306-.297 5.082-.89 1.776-.594 3.355-1.682 4.687-3.165.79-.94 1.382-1.978 1.678-3.165.296-1.187.493-2.621.493-4.302v-2.077a41.135 41.135 0 00-4.539-.84 37.099 37.099 0 00-4.638-.298c-3.306 0-5.724.643-7.352 1.978-1.628 1.336-2.418 3.215-2.418 5.687 0 2.324.592 4.055 1.826 5.242 1.184 1.236 2.911 1.83 5.18 1.83zm39.621 5.34c-.888 0-1.48-.148-1.875-.494-.394-.297-.74-.99-1.036-1.929L47.714 5.044c-.296-.99-.444-1.632-.444-1.978 0-.791.394-1.236 1.184-1.236h4.835c.938 0 1.58.148 1.925.494.395.297.69.99.987 1.929l8.29 32.736 7.697-32.736c.246-.99.542-1.632.937-1.929.395-.297 1.085-.494 1.974-.494h3.947c.938 0 1.579.148 1.974.494.395.297.74.99.937 1.929l7.796 33.132L98.29 4.253c.295-.99.64-1.632.986-1.929.395-.297 1.036-.494 1.925-.494h4.588c.79 0 1.234.395 1.234 1.236 0 .247-.049.494-.099.791a7.041 7.041 0 01-.345 1.236L94.688 43.32c-.297.989-.642 1.631-1.037 1.928-.394.297-1.036.495-1.875.495h-4.243c-.938 0-1.58-.149-1.974-.495-.394-.346-.74-.989-.937-1.978l-7.648-31.895-7.599 31.846c-.247.989-.543 1.632-.938 1.978-.394.346-1.085.494-1.973.494H62.22zm63.405 1.336c-2.566 0-5.132-.297-7.599-.89-2.467-.594-4.391-1.237-5.674-1.979-.79-.445-1.332-.94-1.53-1.384a3.503 3.503 0 01-.296-1.385v-2.522c0-1.038.395-1.533 1.135-1.533.296 0 .592.05.888.148.296.1.74.297 1.234.495a26.791 26.791 0 005.428 1.73c1.973.396 3.898.594 5.871.594 3.109 0 5.527-.544 7.204-1.632 1.678-1.088 2.566-2.67 2.566-4.698 0-1.384-.444-2.521-1.332-3.461-.888-.94-2.566-1.78-4.984-2.571l-7.154-2.226c-3.602-1.137-6.267-2.818-7.895-5.044-1.628-2.176-2.467-4.599-2.467-7.17 0-2.077.444-3.907 1.332-5.489.888-1.582 2.072-2.967 3.553-4.055 1.48-1.137 3.157-1.978 5.131-2.571 1.974-.594 4.046-.841 6.217-.841 1.086 0 2.221.05 3.306.198 1.135.148 2.171.346 3.207.544.987.247 1.925.494 2.813.79.888.298 1.579.594 2.072.891.691.396 1.185.791 1.481 1.236.296.396.444.94.444 1.632V8.16c0 1.039-.395 1.583-1.135 1.583-.395 0-1.036-.198-1.875-.594-2.813-1.285-5.971-1.928-9.474-1.928-2.812 0-5.033.445-6.562 1.384-1.53.94-2.319 2.374-2.319 4.402 0 1.384.493 2.57 1.48 3.51s2.812 1.88 5.427 2.72l7.007 2.226c3.553 1.137 6.118 2.72 7.648 4.747 1.53 2.027 2.27 4.351 2.27 6.923 0 2.126-.444 4.055-1.283 5.736-.888 1.681-2.072 3.165-3.602 4.352-1.53 1.236-3.355 2.126-5.477 2.769-2.22.692-4.54 1.039-7.056 1.039z" fill="#000"></path> </g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 530 60">
<text x="0" y="48" font-family="Arial,Helvetica,sans-serif" font-weight="400" font-size="48" letter-spacing="0" fill="#000000">Google Cloud</text>
</svg>

After

Width:  |  Height:  |  Size: 217 B

+6
View File
@@ -0,0 +1,6 @@
<svg width="126" height="34" viewBox="0 0 63 17" xmlns="http://www.w3.org/2000/svg">
<g fill="#000000" fill-rule="nonzero">
<polygon points="18 12.485 18 17 0 17 0 0 5.061 0 5.061 12.485"></polygon>
<path d="M62.991,0 L56.069,0 L50.841,5.265 L45.64,0 L33.537,0 L33.537,8.355 C33.537,10.39 32.904,12.547 28.355,12.547 C23.805,12.547 23.173,10.418 23.173,8.355 L23.173,0 L18,0 L18,8.355 C18,14.199 21.107,17 28.364,17 C35.593,17 38.728,14.171 38.728,8.355 L38.728,0.327 C38.728,0.215 38.858,0.159 38.942,0.233 L47.25,8.374 L38.7,16.748 L45.649,16.748 L50.85,11.483 L56.078,16.748 L63,16.748 L54.478,8.374 L62.991,0 Z"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 647 B

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 108.472 20">
&#x9;
&#x9; <desc>NVIDIA Home</desc>
&#x9; <path id="nvidia-logo-reg" d="M1072.628,253.918v-.3h.192c.105,0,.248.008.248.136s-.073.163-.2.163h-.243m0,.211h.129l.3.524h.327l-.33-.545a.3.3,0,0,0,.311-.323c0-.285-.2-.377-.53-.377h-.482v1.245h.276v-.524m1.4-.1a1.2,1.2,0,1,0-1.2,1.157,1.14,1.14,0,0,0,1.2-1.157m-.347,0a.854.854,0,0,1-.855.891v0a.889.889,0,1,1,.855-.887Z" transform="translate(-965.557 -237.878)"></path>
&#x9; <path id="nvidia-logo-NVIDIA" d="M463.9,151.934v13.127h3.707V151.934Zm-29.164-.018v13.145h3.74v-10.2l2.918.01a2.674,2.674,0,0,1,2.086.724c.586.625.826,1.632.826,3.476v5.995h3.624V157.8c0-5.183-3.3-5.882-6.536-5.882Zm35.134.018v13.127h6.013c3.2,0,4.249-.533,5.38-1.727a7.352,7.352,0,0,0,1.316-4.692,7.789,7.789,0,0,0-1.2-4.516c-1.373-1.833-3.352-2.191-6.306-2.191Zm3.677,2.858h1.594c2.312,0,3.808,1.039,3.808,3.733s-1.5,3.734-3.808,3.734h-1.594Zm-14.992-2.858-3.094,10.4-2.965-10.4h-4l4.234,13.127h5.343l4.267-13.127Zm25.749,13.127h3.708V151.935h-3.709ZM494.7,151.939l-5.177,13.117h3.656l.819-2.318h6.126l.775,2.318h3.969l-5.216-13.118Zm2.407,2.393,2.246,6.145h-4.562Z" transform="translate(-399.551 -148.155)"></path>
&#x9; <path id="nvidia-logo-eye-mark" data-name="Eye Mark" d="M129.832,124.085v-1.807c.175-.013.353-.022.533-.028,4.941-.155,8.183,4.246,8.183,4.246s-3.5,4.863-7.255,4.863a4.553,4.553,0,0,1-1.461-.234v-5.478c1.924.232,2.31,1.082,3.467,3.01l2.572-2.169a6.81,6.81,0,0,0-5.042-2.462,9.328,9.328,0,0,0-1,.059m0-5.968v2.7c.177-.014.355-.025.533-.032,6.871-.232,11.348,5.635,11.348,5.635s-5.142,6.253-10.5,6.253a7.906,7.906,0,0,1-1.383-.122v1.668a9.1,9.1,0,0,0,1.151.075c4.985,0,8.59-2.546,12.081-5.559.578.463,2.948,1.591,3.435,2.085-3.319,2.778-11.055,5.018-15.44,5.018-.423,0-.829-.026-1.228-.064v2.344h18.947v-20Zm0,13.009v1.424c-4.611-.822-5.89-5.615-5.89-5.615a9.967,9.967,0,0,1,5.89-2.85v1.563h-.007a4.424,4.424,0,0,0-3.437,1.571s.845,3.035,3.444,3.908m-8.189-4.4a11.419,11.419,0,0,1,8.189-4.449v-1.463c-6.043.485-11.277,5.6-11.277,5.6s2.964,8.569,11.277,9.354v-1.555C123.731,133.451,121.643,126.728,121.643,126.728Z" transform="translate(-118.555 -118.117)" fill="#000"></path>
&#x9; </svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 60" fill="#000000"><path d="M19.63 38.552l2.097-6.73-4.377-3.358L6.026 39.56h12.16l1.43-.982zm14.028-10.087l-4.377 3.358 2.1 6.753 1.42.982H44.98z"></path><path d="M31.62 24.634L27 24.622l-1.49-4.425-1.503 4.425-4.62.012 3.686 2.85-1.346 4.353L25.5 29.24l3.77 2.595-1.358-4.353zm-6.1 9.942l-.012-.012-7.323 4.995h14.622z"></path><path d="M46.812 30.15v-.982h1.66v-2.134l1.212-.533v2.667h2.097v.982h-2.097v6.668c0 1.382.145 1.7 1.128 1.7.324.005.65-.015.97-.06v1.103a10.84 10.84 0 0 1-1.212.061c-1.66 0-2.158-.74-2.158-2.013V30.2h-1.66zm7.602 4.462v.8c0 1.443.388 3.346 2.34 3.346a2.12 2.12 0 0 0 2.279-2.425h1.297a3.14 3.14 0 0 1-3.637 3.371c-1.794 0-3.637-.715-3.637-3.868V33.3c0-3.007 1.212-4.377 3.637-4.377 3.637 0 3.637 2.57 3.637 5.65h-5.917zm4.692-.982c0-2.304-.376-3.637-2.34-3.637s-2.34 1.382-2.34 3.637h4.692zm8.1-1.48c.06-1.297-.412-2.194-1.83-2.194a1.71 1.71 0 0 0-1.661 1.212 14.15 14.15 0 0 0-.291 3.274c0 1.867 0 4.365 1.916 4.365 1.625 0 1.83-1.03 1.89-2.28h1.212c.156.83-.075 1.685-.628 2.322s-1.366.988-2.2.95c-3.516 0-3.516-2.776-3.516-5.347a13.75 13.75 0 0 1 .303-3.431c.45-1.316 1.745-2.15 3.128-2.013a2.69 2.69 0 0 1 2.898 3.189h-1.212zm3.383 7.42V24.706H71.8v5.735c.526-.892 1.474-1.45 2.5-1.48 2.57 0 2.837 1.916 2.837 3.274V39.5h-1.212v-7.275c0-1.212-.473-2.34-1.916-2.34-1.115 0-2.243.764-2.243 3.08v6.596h-1.212zm11.882.182c-2.243 0-3.128-1.09-3.104-3.286h1.212c0 1.42.327 2.425 1.916 2.425.495.054.987-.124 1.333-.482s.507-.856.437-1.35c0-2.716-4.668-2.013-4.668-5.444 0-1.916 1.322-2.595 3.128-2.595 2.06 0 2.716 1.42 2.716 3.007h-1.212c-.06-1.322-.412-2.013-1.807-2.013a1.48 1.48 0 0 0-1.588 1.503c0 2.595 4.668 1.89 4.668 5.37.053.805-.256 1.59-.842 2.144s-1.39.816-2.19.717zm4.316-9.602v-.982h1.66v-2.134l1.212-.533v2.667h2.097v.982H89.66v6.668c0 1.382.145 1.7 1.128 1.7.324.005.65-.015.97-.06v1.103a10.91 10.91 0 0 1-1.212.061c-1.66 0-2.158-.74-2.158-2.013V30.2h-1.66zM98.8 38.274c-.653.978-1.772 1.54-2.946 1.48a2.75 2.75 0 0 1-2.861-3.067c0-3.637 3.637-3.274 5.76-3.43v-.86c0-1.528-.45-2.425-2.097-2.425a1.78 1.78 0 0 0-2.037 1.794h-1.322c.097-2 1.65-2.776 3.455-2.776 1.043 0 3.225.097 3.225 2.752v5.226c-.01.87.03 1.74.12 2.607H98.8v-1.273zm-.085-4.025c-1.77.06-4.437-.145-4.437 2.425a1.88 1.88 0 0 0 1.867 2.134 2.67 2.67 0 0 0 2.57-2.631v-1.89zm5.165-3.686a3.15 3.15 0 0 1 2.861-1.6v1.212c-.762-.157-1.554.06-2.127.587s-.86 1.295-.77 2.068v6.74h-1.212v-10.4h1.212v1.394zm7.056 9.2c-2.243 0-3.128-1.09-3.104-3.286h1.212c0 1.42.327 2.425 1.916 2.425.495.054.987-.124 1.333-.482s.507-.856.437-1.35c0-2.716-4.668-2.013-4.668-5.444 0-1.916 1.322-2.595 3.128-2.595 2.06 0 2.716 1.42 2.716 3.007h-1.212c-.06-1.322-.412-2.013-1.807-2.013a1.48 1.48 0 0 0-1.588 1.503c0 2.595 4.668 1.89 4.668 5.37.053.805-.256 1.59-.842 2.144s-1.39.816-2.19.717z"></path></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="2 10.5 27 10">
<g>
&#x9;<path fill="#000000" d="M3.5,20.2v-1.1l4.4-5.9H3.5v-1.5h6.7v1.1l-4.4,5.9h4.4v1.5H3.5z"></path>
&#x9;<path fill="#000000" d="M18.9,14.2c0.2,0.5,0.3,1.2,0.3,1.8c0,0.7-0.1,1.3-0.3,1.8c-0.2,0.5-0.5,1-0.9,1.4s-0.8,0.6-1.3,0.8&#xA;&#x9;&#x9;c-0.5,0.2-1,0.3-1.6,0.3S14,20.2,13.5,20s-0.9-0.5-1.3-0.8c-0.4-0.4-0.7-0.8-0.9-1.4C11.1,17.3,11,16.7,11,16&#xA;&#x9;&#x9;c0-0.7,0.1-1.3,0.3-1.8s0.5-1,0.9-1.4s0.8-0.6,1.3-0.8s1-0.3,1.6-0.3s1.1,0.1,1.6,0.3s0.9,0.5,1.3,0.8S18.6,13.6,18.9,14.2z&#xA;&#x9;&#x9; M16.7,18c0.4-0.5,0.6-1.2,0.6-2c0-0.9-0.2-1.6-0.6-2c-0.4-0.5-0.9-0.7-1.6-0.7c-0.7,0-1.2,0.2-1.6,0.7c-0.4,0.5-0.6,1.2-0.6,2&#xA;&#x9;&#x9;c0,0.9,0.2,1.6,0.6,2c0.4,0.5,0.9,0.7,1.6,0.7C15.8,18.8,16.3,18.5,16.7,18z"></path>
&#x9;<path fill="#000000" d="M28.2,14.2c0.2,0.5,0.3,1.2,0.3,1.8c0,0.7-0.1,1.3-0.3,1.8c-0.2,0.5-0.5,1-0.9,1.4S26.5,19.8,26,20&#xA;&#x9;&#x9;c-0.5,0.2-1,0.3-1.6,0.3s-1.1-0.1-1.6-0.3s-0.9-0.5-1.3-0.8c-0.4-0.4-0.7-0.8-0.9-1.4c-0.2-0.5-0.3-1.2-0.3-1.8&#xA;&#x9;&#x9;c0-0.7,0.1-1.3,0.3-1.8c0.2-0.5,0.5-1,0.9-1.4s0.8-0.6,1.3-0.8s1-0.3,1.6-0.3s1.1,0.1,1.6,0.3s0.9,0.5,1.3,0.8&#xA;&#x9;&#x9;C27.7,13.2,28,13.6,28.2,14.2z M26.1,18c0.4-0.5,0.6-1.2,0.6-2c0-0.9-0.2-1.6-0.6-2c-0.4-0.5-0.9-0.7-1.6-0.7s-1.2,0.2-1.6,0.7&#xA;&#x9;&#x9;s-0.6,1.2-0.6,2c0,0.9,0.2,1.6,0.6,2c0.4,0.5,0.9,0.7,1.6,0.7S25.7,18.5,26.1,18z"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"></path><path d="M12 6c-3.31 0-6 2.69-6 6s2.69 6 6 6c1.66 0 3.16-.67 4.24-1.76l-1.42-1.42A3.97 3.97 0 0112 16c-2.21 0-4-1.79-4-4s1.79-4 4-4c1.04 0 1.99.4 2.7 1.05l1.42-1.42A5.96 5.96 0 0012 6z"></path></svg>

After

Width:  |  Height:  |  Size: 421 B

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M5 3h8a5 5 0 0 1 0 10H9l5 8h-4l-5-8V3zm3 3v4h5a2 2 0 0 0 0-4H8z"></path>
</svg>

After

Width:  |  Height:  |  Size: 171 B

+6
View File
@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="3.5" r="2.5" fill="currentColor"></circle>
<circle cx="3.5" cy="18.5" r="2.5" fill="currentColor"></circle>
<circle cx="20.5" cy="18.5" r="2.5" fill="currentColor"></circle>
<path d="M12 6L4.5 16.5M12 6L19.5 16.5M6 18.5L18 18.5" stroke="currentColor" stroke-width="1.5" fill="none"></path>
</svg>

After

Width:  |  Height:  |  Size: 385 B

+3
View File
@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.746l7.73-8.835L1.254 2.25H8.08l4.253 5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path>
</svg>

After

Width:  |  Height:  |  Size: 265 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M25.7146 63.2153L41.4393 54.3917L41.7025 53.6226L41.4393 53.1976H40.6705L38.0394 53.0359L29.054 52.7929L21.2624 52.4691L13.7134 52.0644L11.8111 51.6594L10.0303 49.3118L10.2123 48.138L11.8111 47.0657L14.0981 47.2681L19.1574 47.6119L26.7467 48.138L32.2516 48.4618L40.4073 49.3118H41.7025L41.8846 48.7857L41.4393 48.4618L41.0955 48.138L33.243 42.8155L24.7432 37.1894L20.2909 33.9513L17.8824 32.3119L16.6684 30.774L16.1422 27.4147L18.328 25.0062L21.2624 25.2088L22.0112 25.4112L24.9861 27.6979L31.3407 32.616L39.6381 38.7273L40.8525 39.7391L41.3381 39.395L41.399 39.1523L40.8525 38.2415L36.3394 30.0858L31.5227 21.7883L29.3775 18.3478L28.811 16.2837C28.6087 15.4334 28.4669 14.7252 28.4669 13.8549L30.9563 10.4753L32.3321 10.0303L35.6515 10.4756L37.0479 11.6897L39.112 16.4052L42.4513 23.8327L47.6321 33.9313L49.15 36.9265L49.9594 39.6991L50.2632 40.5491H50.7894V40.0632L51.2141 34.3766L52.0035 27.3944L52.7726 18.4087L53.0358 15.8793L54.2905 12.8435L56.7795 11.2041L58.7224 12.135L60.3212 14.422L60.0986 15.899L59.1474 22.0718L57.2857 31.7458L56.0713 38.2218H56.7795L57.5892 37.4121L60.8677 33.061L66.3723 26.18L68.801 23.448L71.6342 20.4325L73.4556 18.9957H76.8962L79.4255 22.7601L78.2926 26.6456L74.7509 31.1384L71.8163 34.943L67.607 40.6097L64.9758 45.1431L65.2188 45.5072L65.8464 45.4466L75.358 43.4228L80.4984 42.4917L86.6304 41.4393L89.4033 42.7346L89.7065 44.0502L88.6135 46.7419L82.0566 48.3607L74.3662 49.8989L62.9118 52.6109L62.77 52.7121L62.9321 52.9144L68.0925 53.4L70.2987 53.5214H75.7021L85.7601 54.2702L88.3912 56.0108L89.9697 58.1358L89.7065 59.7545L85.6589 61.8189L80.1949 60.5236L67.4452 57.4881L63.0735 56.3952H62.4665V56.7596L66.1093 60.3213L72.7877 66.3523L81.1461 74.1236L81.5707 76.0462L80.4984 77.5638L79.3649 77.4021L72.0186 71.8772L69.1854 69.3879L62.77 63.9844H62.3453V64.5509L63.8223 66.7164L71.6342 78.4544L72.0389 82.0567L71.4725 83.2308L69.4487 83.939L67.2222 83.534L62.6485 77.1189L57.9333 69.8937L54.1284 63.4177L53.6631 63.6809L51.4167 87.8651L50.3644 89.0995L47.9356 90.0303L45.9121 88.4924L44.8392 86.0031L45.9118 81.0852L47.2071 74.6701L48.2594 69.5699L49.2106 63.2356L49.7773 61.131L49.7367 60.9892L49.2715 61.0498L44.4954 67.607L37.23 77.4224L31.4825 83.5746L30.1063 84.1211L27.7181 82.8864L27.9408 80.6805L29.2763 78.7177L37.2297 68.5988L42.026 62.3248L45.1227 58.7025L45.1024 58.176H44.9204L23.7917 71.8975L20.0274 72.3831L18.4083 70.8655L18.6106 68.3761L19.3798 67.5664L25.7343 63.195L25.7146 63.2153Z" fill="white"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M83.7733 42.8087C84.6678 40.1149 84.9771 37.2613 84.6807 34.4385C84.3843 31.6156 83.489 28.8885 82.0544 26.4394C77.6908 18.8436 68.9203 14.9365 60.3548 16.7725C57.9831 14.1344 54.9591 12.1668 51.5864 11.0673C48.2137 9.96772 44.611 9.77498 41.1402 10.5084C37.6694 11.2418 34.4527 12.8755 31.8132 15.2455C29.1736 17.6155 27.204 20.6383 26.1024 24.0103C23.3212 24.5806 20.6938 25.738 18.3958 27.405C16.0977 29.0721 14.1819 31.2104 12.7765 33.6772C8.36538 41.2609 9.3669 50.8267 15.2527 57.3327C14.3549 60.0251 14.0424 62.8782 14.3361 65.7012C14.6298 68.5241 15.523 71.2518 16.9558 73.7017C21.325 81.3002 30.1011 85.207 38.6712 83.3686C40.5554 85.4904 42.8707 87.1858 45.4623 88.3416C48.0539 89.4975 50.8622 90.0871 53.6999 90.0713C62.4793 90.079 70.2575 84.4114 72.9393 76.0515C75.7201 75.4802 78.347 74.3225 80.6449 72.6555C82.9427 70.9886 84.8587 68.8507 86.2649 66.3846C90.6227 58.8145 89.6172 49.3005 83.7733 42.8087ZM53.6999 84.8356C50.1955 84.8411 46.801 83.6129 44.1116 81.3661L44.5848 81.098L60.5123 71.9043C60.9087 71.6718 61.2379 71.3402 61.4674 70.942C61.6969 70.5439 61.8189 70.0929 61.8215 69.6333V47.1769L68.5553 51.072C68.6225 51.1063 68.6694 51.1707 68.6814 51.2456V69.854C68.6641 78.1208 61.9667 84.8183 53.6999 84.8356ZM21.4977 71.0843C19.7402 68.0497 19.1092 64.4925 19.7156 61.0386L20.1885 61.3225L36.1321 70.5165C36.5266 70.748 36.9757 70.87 37.4331 70.87C37.8905 70.87 38.3396 70.748 38.7341 70.5165L58.21 59.2883V67.0628C58.2081 67.1031 58.1973 67.1424 58.1782 67.1779C58.1591 67.2134 58.1322 67.2441 58.0996 67.2678L41.9671 76.5722C34.798 80.7022 25.6388 78.2463 21.4977 71.0843ZM17.3026 36.3898C19.0723 33.3357 21.8655 31.0062 25.1878 29.8138V48.7376C25.1818 49.1949 25.2986 49.6453 25.5261 50.042C25.7535 50.4387 26.0833 50.7671 26.4809 50.9928L45.8622 62.1739L39.1283 66.069C39.0919 66.0883 39.0513 66.0984 39.0101 66.0984C38.9689 66.0984 38.9283 66.0883 38.8919 66.069L22.7908 56.7809C15.6359 52.6337 13.1822 43.4816 17.3026 36.3112V36.3898ZM72.624 49.2426L53.1792 37.9512L59.8976 34.0718C59.9341 34.0524 59.9747 34.0423 60.016 34.0423C60.0573 34.0423 60.0979 34.0524 60.1344 34.0718L76.2355 43.3761C78.6973 44.7966 80.7043 46.8882 82.0221 49.4065C83.3398 51.9249 83.914 54.7661 83.6775 57.5985C83.4411 60.431 82.4038 63.1377 80.6867 65.4027C78.9696 67.6677 76.6436 69.3975 73.9803 70.3901V51.466C73.9663 51.0096 73.834 50.5647 73.5962 50.1749C73.3584 49.7851 73.0234 49.4638 72.624 49.2426ZM79.3261 39.1657L78.8529 38.8815L62.9411 29.6089C62.5442 29.376 62.0924 29.2532 61.6322 29.2532C61.172 29.2532 60.7202 29.376 60.3233 29.6089L40.8629 40.8374V33.0628C40.8587 33.0233 40.8654 32.9834 40.882 32.9473C40.8987 32.9113 40.9248 32.8803 40.9575 32.8579L57.0586 23.5692C59.5263 22.1476 62.3478 21.458 65.193 21.5811C68.0382 21.7042 70.7896 22.6348 73.1253 24.2642C75.461 25.8936 77.2845 28.1543 78.3825 30.782C79.4806 33.4097 79.8077 36.2957 79.3257 39.1025V39.1657H79.3261ZM37.1888 52.9484L30.455 49.069C30.4213 49.0487 30.3925 49.0212 30.3707 48.9884C30.3488 48.9557 30.3345 48.9186 30.3286 48.8797V30.3188C30.3323 27.4714 31.1466 24.6839 32.6761 22.2822C34.2057 19.8805 36.3874 17.9639 38.9661 16.7564C41.5448 15.549 44.4139 15.1005 47.2381 15.4636C50.0622 15.8267 52.7247 16.9862 54.9141 18.8067L54.4409 19.0748L38.5134 28.2686C38.117 28.5011 37.7879 28.8327 37.5584 29.2308C37.329 29.629 37.207 30.0799 37.2045 30.5395L37.1888 52.9487V52.9484ZM40.8472 45.0632L49.5209 40.0643L58.21 45.0635V55.0615L49.5523 60.0608L40.8632 55.0615L40.8472 45.0632Z" fill="white"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M79.7057 21.6044C85.0857 27.2967 87.3915 35.1429 88.3522 46.0659C90.8885 46.0659 93.2711 46.6428 94.8851 48.8352L97.8826 52.9121C98.728 54.0659 99.1892 55.4505 99.1892 56.9121V67.9505C99.1833 68.6605 99.0137 69.3596 98.6938 69.9934C98.3739 70.6271 97.9122 71.1784 97.3446 71.6044C83.7791 81.5275 67.0241 89.5275 50 89.5275C31.1698 89.5275 12.2627 78.6428 2.65542 71.6044C2.08785 71.1784 1.62614 70.6271 1.30621 69.9934C0.986278 69.3596 0.816752 68.6605 0.810822 67.9505L0.810822 56.9121C0.810822 55.4505 1.27197 54.0659 2.11741 52.8736L5.11488 48.8352C6.7289 46.6428 9.1115 46.0659 11.6478 46.0659C12.6085 35.1429 14.8759 27.2967 20.2944 21.6044C30.5165 10.7582 44.0819 9.52747 49.8463 9.52747H50C55.6491 9.52747 69.3683 10.6429 79.7057 21.6044ZM50 39.7967C48.8471 39.7967 47.5021 39.8736 46.0418 40.0275C45.678 41.7486 44.8848 43.3496 43.7361 44.6813C42.1163 46.2842 40.1962 47.5511 38.0858 48.4092C35.9755 49.2672 33.7165 49.6995 31.4388 49.6813C28.8256 49.6813 26.0971 49.1044 23.8683 47.6813C21.7547 48.4121 19.7179 49.4121 19.5642 51.9121C19.3721 56.6044 19.3336 61.3352 19.3336 66.0659C19.3336 68.4121 19.3336 70.7967 19.2568 73.1813C19.2568 74.5659 20.1022 75.8352 21.3704 76.4121C31.5156 81.0275 41.1613 83.3736 50 83.3736C58.8387 83.3736 68.446 81.0659 78.6297 76.4121C79.2511 76.1286 79.7794 75.6746 80.1535 75.1028C80.5276 74.531 80.7321 73.8647 80.7432 73.1813C80.8585 66.1044 80.7432 58.9505 80.4358 51.9121C80.2821 49.3736 78.2454 48.4121 76.1318 47.6813C73.9029 49.1044 71.136 49.6429 68.5612 49.6429C66.2861 49.6662 64.0288 49.2399 61.9186 48.3885C59.8084 47.5371 57.8868 46.2773 56.2639 44.6813C55.1153 43.3496 54.322 41.7486 53.9582 40.0275C52.6516 39.8736 51.3066 39.8352 50 39.7967ZM39.6242 56.7198C41.853 56.7198 43.6592 58.489 43.6592 60.7198V68.1044C43.6592 69.1653 43.2381 70.1827 42.4886 70.9328C41.7391 71.683 40.7226 72.1044 39.6626 72.1044C38.6026 72.1044 37.5861 71.683 36.8366 70.9328C36.087 70.1827 35.666 69.1653 35.666 68.1044V60.6813C35.666 58.4505 37.4337 56.6813 39.6626 56.6813L39.6242 56.7198ZM60.1453 56.7198C62.3742 56.7198 64.1419 58.489 64.1419 60.7198V68.1044C64.1419 69.1653 63.7208 70.1827 62.9713 70.9328C62.2218 71.683 61.2052 72.1044 60.1453 72.1044C59.0853 72.1044 58.0688 71.683 57.3192 70.9328C56.5697 70.1827 56.1487 69.1653 56.1487 68.1044V60.6813C56.1487 58.4505 57.9548 56.6813 60.1453 56.6813V56.7198ZM32.0921 20.4121C27.788 20.8352 24.1757 22.2582 22.3311 24.2582C18.3345 28.6044 19.1799 39.6813 21.4857 42.0275C23.7655 43.9275 26.6682 44.9142 29.6326 44.7967C32.2458 44.7967 37.1647 44.2198 41.1998 40.1044C43.0059 38.3736 44.0819 34.0659 43.9666 29.7198C43.8514 26.2198 42.8522 23.2967 41.3919 22.0659C39.7779 20.6813 36.1655 20.0659 32.0921 20.4121ZM58.6081 22.0659C57.1478 23.2967 56.1487 26.2198 56.0334 29.7198C55.9181 34.0659 56.9941 38.3736 58.8003 40.1044C60.3121 41.6165 62.111 42.8106 64.0909 43.6164C66.0708 44.4223 68.1918 44.8236 70.329 44.7967C73.7492 44.7967 76.8619 43.6813 78.5144 42.0275C80.8201 39.6813 81.6655 28.6044 77.6689 24.2198C75.8243 22.2967 72.212 20.8352 67.9079 20.4121C63.8345 20.0275 60.2221 20.6813 58.6081 22.0659ZM50 31.0659C49.0009 31.0659 47.848 31.1429 46.5414 31.2582C46.6951 31.9121 46.7335 32.6813 46.8104 33.4505C46.8104 34.0275 46.8104 34.6044 46.7335 35.1813C47.9633 35.0659 49.0393 35.0659 50 35.0659C50.9992 35.0659 52.0367 35.0659 53.2665 35.1813C53.1896 34.5659 53.1896 34.0275 53.1896 33.4505C53.2665 32.6813 53.3049 31.9121 53.4586 31.2582C52.152 31.1429 50.9992 31.0659 50 31.0659Z" fill="white"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M84.0704 28.9353L51.9066 10.4454C50.8738 9.85153 49.5994 9.85153 48.5666 10.4454L16.4043 28.9353C15.536 29.4345 15 30.3576 15 31.3575V68.6425C15 69.6424 15.536 70.5655 16.4043 71.0647L48.5681 89.5546C49.6009 90.1485 50.8753 90.1485 51.9081 89.5546L84.0719 71.0647C84.9402 70.5655 85.4762 69.6424 85.4762 68.6425V31.3575C85.4762 30.3576 84.9402 29.4345 84.0719 28.9353H84.0704ZM82.0501 32.8519L51.0006 86.4003C50.7907 86.7611 50.2366 86.6138 50.2366 86.1958V51.1329C50.2366 50.4322 49.8606 49.7842 49.2506 49.4324L18.7553 31.9017C18.3929 31.6927 18.5409 31.141 18.9606 31.141H81.0595C81.9414 31.141 82.4925 32.0927 82.0516 32.8534H82.0501V32.8519Z" fill="white"></path>
</svg>

After

Width:  |  Height:  |  Size: 784 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M48.217 10.7381C48.8274 9.07596 51.1705 9.10263 51.7434 10.7779L53.3442 15.4571C55.7354 22.447 59.6489 28.8096 64.7979 34.079C69.947 39.3483 76.2012 43.3909 83.1024 45.9106L88.2657 47.7954C89.9088 48.3953 89.9123 50.7344 88.2711 51.3392L83.0955 53.2469C76.3818 55.7214 70.2839 59.6393 65.2262 64.728C60.1684 69.8168 56.2722 75.9542 53.8085 82.7134L51.783 88.2704C51.1836 89.915 48.8758 89.9193 48.2703 88.2772L46.1551 82.5404C43.6813 75.8303 39.7939 69.7395 34.7602 64.6867C29.7265 59.6339 23.6659 55.739 16.9952 53.2698L11.7265 51.3198C10.0944 50.7155 10.0901 48.3924 11.7198 47.7822L17.0915 45.7707C23.7343 43.2834 29.7654 39.3804 34.7722 34.3288C39.7791 29.2771 43.6435 23.196 46.1013 16.5012L48.217 10.7381Z" fill="white"></path>
</svg>

After

Width:  |  Height:  |  Size: 849 B

+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<svg width="100" height="100" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="none">
<path fill="#FFFFFF" fill-rule="evenodd" d="M9.27 15.29l7.978-5.897c.391-.29.95-.177 1.137.272.98 2.369.542 5.215-1.41 7.169-1.951 1.954-4.667 2.382-7.149 1.406l-2.711 1.257c3.889 2.661 8.611 2.003 11.562-.953 2.341-2.344 3.066-5.539 2.388-8.42l.006.007c-.983-4.232.242-5.924 2.75-9.383.06-.082.12-.164.179-.248l-3.301 3.305v-.01L9.267 15.292M7.623 16.723c-2.792-2.67-2.31-6.801.071-9.184 1.761-1.763 4.647-2.483 7.166-1.425l2.705-1.25a7.808 7.808 0 00-1.829-1A8.975 8.975 0 005.984 5.83c-2.533 2.536-3.33 6.436-1.962 9.764 1.022 2.487-.653 4.246-2.34 6.022-.599.63-1.199 1.259-1.682 1.925l7.62-6.815"></path>
</svg>

After

Width:  |  Height:  |  Size: 740 B

+7
View File
@@ -0,0 +1,7 @@
<svg width="240" height="240" viewBox="0 0 240 240" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M178.46 4.35561C184.336 17.4008 185.708 34.691 181.282 48.9317C173.371 46.2895 166.112 44.2644 157.689 43.6987C157.683 31.339 159.916 19.3337 165.062 8.06984C167.606 2.50202 174.409 -4.63787 178.46 4.35561Z" fill="white"></path>
<path d="M71.0775 3.03524C78.6788 14.9096 81.1013 29.9224 81.5422 43.7049C76.2668 44.1323 73.3195 44.3426 68.1429 45.475C64.2552 46.5137 61.5822 47.5774 57.9607 49.1793C53.4513 36.0203 54.6527 19.0144 59.7519 6.05038C61.7817 0.89087 67.2534 -2.93785 71.0775 3.03524Z" fill="white"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M118.468 34.9089C133.047 34.4971 143.909 40.9545 152.475 52.3242C155.36 52.3174 158.114 52.3188 161.007 52.5342C192.594 54.8898 214.339 87.9185 202.9 117.705C201.118 122.346 199.259 125.395 196.558 129.482C199.777 134.285 202.183 139.586 203.679 145.17C207.471 158.991 206.629 172.355 201.064 185.655C200.363 187.326 198.819 189.956 197.538 192.138C197.083 192.912 196.662 193.629 196.323 194.228C196.657 194.972 197.06 195.734 197.461 196.493L197.461 196.494C197.746 197.034 198.031 197.572 198.29 198.103C204.271 210.316 207.346 226.522 204.209 240L34.8319 239.986C31.6644 223.366 34.9109 209.011 42.7579 194.305C41.608 192.023 40.6474 190.29 39.815 188.789C37.4308 184.489 36.0988 182.087 34.3839 174.119C30.992 158.767 33.8942 142.702 42.4428 129.502C36.7922 120.354 33.0087 112.387 33.0115 101.22C33.1229 88.2256 38.3544 75.798 47.5731 66.6286C58.8619 55.3546 71.1566 52.3046 86.5175 52.3527C95.0381 41.0683 104.069 35.6943 118.468 34.9089ZM178.913 103.272C180.049 108.491 176.742 113.643 171.523 114.795C166.275 115.956 161.084 112.633 159.942 107.387C158.802 102.142 162.146 96.9687 167.4 95.8482C172.631 94.7331 177.777 98.0525 178.913 103.272ZM78.993 103.057C80.2302 108.169 77.1438 113.332 72.0506 114.668C68.6649 115.557 65.0631 114.546 62.6329 112.031C60.2028 109.514 59.324 105.883 60.3354 102.535C61.3467 99.188 64.09 96.6478 67.5082 95.8941C72.6505 94.7597 77.7561 97.9438 78.993 103.057ZM147.427 104.182C140.194 98.1187 130.52 94.6173 120.041 94.6173C109.572 94.6173 99.6973 98.1075 92.2183 104.115C84.8512 110.032 80.3951 117.975 80.3951 125.884C80.3951 133.882 84.269 141.265 91.177 146.523C98.1411 151.836 107.792 154.734 119.089 154.734C129.748 154.734 139.052 152.698 145.993 148.503C154.133 143.558 158.679 135.817 158.679 125.884C158.679 117.997 154.48 110.077 147.427 104.182Z" fill="white"></path>
<path fill-rule="evenodd" clip-rule="evenodd" d="M120.041 104.64C112.273 104.573 104.712 107.136 98.5889 111.912C93.4275 116.051 90.5053 121.252 90.5053 125.895C90.5053 130.683 92.8565 135.168 97.3349 138.58C102.429 142.462 109.919 144.711 119.089 144.711C128.035 144.711 135.581 143.066 140.72 139.945C145.904 136.813 148.557 132.271 148.557 125.884C148.557 121.152 145.803 115.928 140.91 111.833C135.491 107.303 128.147 104.64 120.041 104.64ZM127.497 118.221L127.453 118.176C126.793 117.385 125.85 116.883 124.826 116.777C123.801 116.67 122.775 116.967 121.966 117.606L119.503 119.552L117.096 117.628C116.285 116.976 115.249 116.67 114.213 116.777C113.178 116.884 112.226 117.395 111.565 118.198C111.241 118.596 110.999 119.053 110.853 119.544C110.707 120.036 110.659 120.551 110.714 121.06C110.768 121.57 110.922 122.064 111.168 122.513C111.414 122.963 111.747 123.359 112.147 123.68L115.182 126.119V131.264C115.188 132.375 115.634 133.438 116.423 134.22C117.212 135.002 118.28 135.44 119.391 135.437C120.503 135.44 121.57 135.002 122.36 134.22C123.149 133.438 123.595 132.375 123.601 131.264V126.275L126.87 123.702C128.561 122.36 128.841 119.91 127.497 118.221Z" fill="white"></path>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

+5
View File
@@ -0,0 +1,5 @@
/** Circular user chip. Empty state is the neutral-700→900 gradient disc used in the app header. */
export interface AvatarProps extends React.HTMLAttributes<HTMLSpanElement> { size?: number }
export function Avatar(props: AvatarProps): JSX.Element;
export function AvatarImage(props: React.ImgHTMLAttributes<HTMLImageElement>): JSX.Element;
export function AvatarFallback(props: React.HTMLAttributes<HTMLSpanElement>): JSX.Element;
+15
View File
@@ -0,0 +1,15 @@
import React from 'react'
export function Avatar({ size = 32, style, children, ...rest }) {
return (
<span style={{position:'relative',display:'inline-flex',alignItems:'center',justifyContent:'center',width:size,height:size,flexShrink:0,borderRadius:'var(--radius-full)',overflow:'hidden',border:'1px solid var(--border-hairline)',background:'linear-gradient(to bottom right,var(--neutral-700),var(--neutral-900))',...style}} {...rest}>
{children}
</span>
)
}
export function AvatarImage({ src, alt = '', style, ...rest }) {
return <img src={src} alt={alt} style={{width:'100%',height:'100%',objectFit:'cover',...style}} {...rest} />
}
export function AvatarFallback({ style, children, ...rest }) {
return <span style={{fontSize:'var(--text-xs)',fontWeight:'var(--weight-medium)',color:'var(--text-secondary)',...style}} {...rest}>{children}</span>
}
+8
View File
@@ -0,0 +1,8 @@
Circular avatar with image or initials fallback.
```jsx
<Avatar size={32}><AvatarImage src="/me.png" alt="Zach" /></Avatar>
<Avatar size={32}><AvatarFallback>ZK</AvatarFallback></Avatar>
```
With no children it renders the neutral gradient disc used in the console header. Sizes: 24 in dense rows, 32 in headers, 40+ on profile screens.
+10
View File
@@ -0,0 +1,10 @@
/** Pill label. Tier/rank is expressed with the white opacity ladder, never hue. */
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
/** tier1/2/3 map to white/5, white/15, white/20 — the rank ladder. error is the only hued variant. */
variant?: 'default' | 'secondary' | 'outline' | 'tier1' | 'tier2' | 'tier3' | 'error';
pill?: boolean;
}
export function Badge(props: BadgeProps): JSX.Element;
export function EyebrowBadge(props: React.HTMLAttributes<HTMLSpanElement>): JSX.Element;
export interface StatusBadgeProps { status?: 'online' | 'idle' | 'error'; label?: string }
export function StatusBadge(props: StatusBadgeProps): JSX.Element;
+47
View File
@@ -0,0 +1,47 @@
import React from 'react'
/** Rank is carried by opacity, never by hue (DESIGN.md §2.3). */
const V = {
default:{background:'var(--white-10)',color:'var(--text-primary)',border:'1px solid var(--border)'},
secondary:{background:'var(--secondary)',color:'var(--secondary-foreground)',border:'1px solid transparent'},
outline:{background:'transparent',color:'var(--text-secondary)',border:'1px solid var(--border-card)'},
tier1:{background:'var(--white-05)',color:'var(--text-tertiary)',border:'1px solid var(--border-card)'},
tier2:{background:'var(--white-15)',color:'var(--text-secondary)',border:'1px solid var(--border-card)'},
tier3:{background:'var(--white-20)',color:'var(--text-primary)',border:'1px solid var(--white-20)'},
error:{background:'var(--state-error-bg)',color:'var(--state-error-text)',border:'1px solid transparent'},
}
export function Badge({ variant = 'default', pill = true, style, children, ...rest }) {
return (
<span
style={{
display:'inline-flex',alignItems:'center',gap:6,
padding:'3px 10px',fontSize:'var(--text-xs)',lineHeight:1.35,
fontFamily:'var(--font-sans)',fontWeight:'var(--weight-medium)',whiteSpace:'nowrap',
borderRadius: pill ? 'var(--radius-full)' : 'var(--radius-sm)',
...(V[variant] || V.default), ...style,
}}
{...rest}
>
{children}
</span>
)
}
/** The eyebrow pill above a hero headline. */
export function EyebrowBadge({ children, style, ...rest }) {
return (
<span style={{display:'inline-flex',alignItems:'center',gap:8,padding:'8px 16px',borderRadius:'var(--radius-full)',border:'1px solid var(--border-hairline)',background:'var(--white-05)',fontSize:'var(--text-sm)',color:'var(--neutral-200)',...style}} {...rest}>{children}</span>
)
}
/** Live-status dot + label. The green is one of the few permitted hues. */
export function StatusBadge({ status = 'online', label, style }) {
const dot = status === 'online' ? 'var(--state-online)' : status === 'error' ? 'var(--state-error)' : 'var(--neutral-500)'
return (
<span style={{display:'inline-flex',alignItems:'center',gap:8,fontSize:'var(--text-sm)',color:'var(--text-secondary)',...style}}>
<span style={{width:8,height:8,borderRadius:'var(--radius-full)',background:dot,animation: status==='online' ? 'zoo-pulse-dot 2s var(--ease-in-out) infinite' : 'none'}} />
{label || status}
</span>
)
}
+9
View File
@@ -0,0 +1,9 @@
Pill labels. `Badge` for tags and tiers, `EyebrowBadge` for the pill above a hero headline, `StatusBadge` for live status.
```jsx
<Badge variant="tier3">Ultra</Badge>
<EyebrowBadge><Icon name="Sparkles" /> Introducing Zoo · Zoo Cloud</EyebrowBadge>
<StatusBadge status="online" label="All systems normal" />
```
Rank ladder: tier1 = white/5 (entry), tier2 = white/15 (pro), tier3 = white/20 (ultra). Never colour a tier badge with hue.
+9
View File
@@ -0,0 +1,9 @@
/** The Zoo button. Monochrome: primary is white-on-black, everything else is a hairline or nothing.
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/** primary = white fill (the one CTA per view). destructive is the only hued variant. */
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'link' | 'destructive';
size?: 'sm' | 'default' | 'lg' | 'icon';
/** Fully rounded. Marketing CTAs and nav actions are pills; app-surface buttons are not. */
pill?: boolean;
}
export function Button(props: ButtonProps): JSX.Element;
+48
View File
@@ -0,0 +1,48 @@
import React from 'react'
const V = {
primary:{background:'var(--primary)',color:'var(--primary-foreground)',border:'1px solid transparent'},
secondary:{background:'var(--secondary)',color:'var(--secondary-foreground)',border:'1px solid transparent'},
outline:{background:'transparent',color:'var(--text-primary)',border:'1px solid var(--border)'},
ghost:{background:'transparent',color:'var(--text-secondary)',border:'1px solid transparent'},
link:{background:'transparent',color:'var(--text-primary)',border:'1px solid transparent',textDecoration:'underline',textUnderlineOffset:4,padding:0,height:'auto'},
destructive:{background:'var(--destructive)',color:'var(--destructive-foreground)',border:'1px solid transparent'},
}
const SZ = {
sm:{height:32,padding:'0 12px',fontSize:'var(--text-xs)'},
default:{height:36,padding:'0 16px',fontSize:'var(--text-sm)'},
lg:{height:44,padding:'0 24px',fontSize:'var(--text-sm)'},
icon:{height:36,width:36,padding:0},
}
const HOVER = { primary:'primary', secondary:'secondary', outline:'accent', ghost:'accent', destructive:'destructive', link:'link' }
export function Button({ variant = 'primary', size = 'default', pill = false, disabled, style, children, ...rest }) {
const [hover, setHover] = React.useState(false)
const v = V[variant] || V.primary
const s = SZ[size] || SZ.default
const h = HOVER[variant]
const hoverStyle = !hover || disabled ? null
: h === 'primary' || h === 'destructive' ? { opacity: 0.9 }
: h === 'secondary' ? { background: 'var(--neutral-800)' }
: h === 'accent' ? { background: 'var(--accent)', color: 'var(--text-primary)' }
: { textDecoration: 'underline' }
return (
<button
disabled={disabled}
onMouseEnter={() => setHover(true)}
onMouseLeave={() => setHover(false)}
style={{
display:'inline-flex',alignItems:'center',justifyContent:'center',gap:8,whiteSpace:'nowrap',
fontFamily:'var(--font-sans)',fontWeight:'var(--weight-medium)',lineHeight:1,
borderRadius: pill ? 'var(--radius-full)' : 'var(--radius-sm)',
cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.5 : 1,
transition:'background-color var(--duration-fast) var(--ease-out), color var(--duration-fast) var(--ease-out), opacity var(--duration-fast) var(--ease-out)',
...v, ...s, ...hoverStyle, ...style,
}}
{...rest}
>
{children}
</button>
)
}
+9
View File
@@ -0,0 +1,9 @@
The Zoo button — one primary (white) action per view, everything else outline or ghost.
```jsx
<Button pill>Try Zoo <Icon name="ChevronDown" /></Button>
<Button variant="outline" size="sm"><Icon name="Filter" /> Filter</Button>
<Button variant="ghost" size="icon"><Icon name="Settings" /></Button>
```
Variants: primary · secondary · outline · ghost · link · destructive. Sizes: sm · default · lg · icon. Use `pill` on marketing/nav CTAs (rounded-full), plain radius inside app surfaces. Hover is a pure CSS opacity/background shift — never a transform.
+15
View File
@@ -0,0 +1,15 @@
/** Hairline-bordered panel on a near-black fill. The default surface for any grouped content.
export interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
/** default = neutral-900/50 grid tile · emphasis = /80 · quiet = /40 story card · plain = no fill. */
variant?: 'default' | 'emphasis' | 'quiet' | 'plain';
/** Adds the top-right radial sheen used on landing story cards. */
sheen?: boolean;
/** Border brightens to neutral-700 on hover — use for clickable cards. */
interactive?: boolean;
}
export function Card(props: CardProps): JSX.Element;
export function CardHeader(props: React.HTMLAttributes<HTMLDivElement>): JSX.Element;
export function CardTitle(props: React.HTMLAttributes<HTMLHeadingElement>): JSX.Element;
export function CardDescription(props: React.HTMLAttributes<HTMLParagraphElement>): JSX.Element;
export function CardContent(props: React.HTMLAttributes<HTMLDivElement>): JSX.Element;
export function CardFooter(props: React.HTMLAttributes<HTMLDivElement>): JSX.Element;
+46
View File
@@ -0,0 +1,46 @@
import React from 'react'
const FILL = {
default:'var(--surface-card)',
emphasis:'var(--surface-card-emphasis)',
quiet:'var(--surface-card-quiet)',
plain:'transparent',
}
export function Card({ variant = 'default', sheen = false, interactive = false, style, children, ...rest }) {
const [hover, setHover] = React.useState(false)
return (
<div
onMouseEnter={interactive ? () => setHover(true) : undefined}
onMouseLeave={interactive ? () => setHover(false) : undefined}
style={{
position:'relative',overflow:'hidden',
borderRadius:'var(--radius-lg)',
border:'1px solid ' + (hover ? 'var(--border-strong)' : 'var(--border-hairline)'),
background:FILL[variant] || FILL.default,
transition:'border-color var(--duration-fast) var(--ease-out)',
...style,
}}
{...rest}
>
{sheen && <div aria-hidden style={{position:'absolute',inset:0,pointerEvents:'none',background:'var(--sheen-card)',opacity:0.7}} />}
{children}
</div>
)
}
export function CardHeader({ style, children, ...rest }) {
return <div style={{position:'relative',padding:'24px 24px 0',display:'flex',flexDirection:'column',gap:6,...style}} {...rest}>{children}</div>
}
export function CardTitle({ style, children, ...rest }) {
return <h3 style={{fontSize:'var(--text-base)',fontWeight:'var(--weight-semibold)',color:'var(--text-primary)',...style}} {...rest}>{children}</h3>
}
export function CardDescription({ style, children, ...rest }) {
return <p style={{fontSize:'var(--text-sm)',lineHeight:'var(--leading-relaxed)',color:'var(--text-helper)',...style}} {...rest}>{children}</p>
}
export function CardContent({ style, children, ...rest }) {
return <div style={{position:'relative',padding:24,...style}} {...rest}>{children}</div>
}
export function CardFooter({ style, children, ...rest }) {
return <div style={{position:'relative',padding:'0 24px 24px',display:'flex',alignItems:'center',gap:12,...style}} {...rest}>{children}</div>
}
+13
View File
@@ -0,0 +1,13 @@
The default grouping surface: 1px hairline border, near-black fill, 12px radius.
```jsx
<Card interactive sheen>
<CardContent>
<Icon name="Shield" size={24} />
<CardTitle style={{marginTop:12}}>Private by default</CardTitle>
<CardDescription>Every request is scoped to your org.</CardDescription>
</CardContent>
</Card>
```
Fills: default (grid tiles) · emphasis (featured) · quiet (landing story cards) · plain. Hover only ever brightens the border — cards never lift, scale, or glow.
+10
View File
@@ -0,0 +1,10 @@
/** Chrome-gradient headline text — the one gradient Zoo permits. */
export interface ChromeTextProps extends React.HTMLAttributes<HTMLElement> {
/** Element to render, e.g. "h1" | "h2". */
as?: keyof JSX.IntrinsicElements;
/** chrome = white→white/60 (default). deep = white→neutral-500, for very large display type. */
tone?: 'chrome' | 'deep';
/** Optional uppercase eyebrow rendered above the headline. */
preHeading?: string;
}
export function ChromeText(props: ChromeTextProps): JSX.Element;
+29
View File
@@ -0,0 +1,29 @@
import React from 'react'
/** The canonical Zoo headline treatment: white → white/60 chrome gradient,
* clipped to the text. Never a saturated or rainbow gradient. */
export function ChromeText({ as = 'span', tone = 'chrome', preHeading, preHeadingStyle, style, children, ...rest }) {
const Tag = as
const gradient = tone === 'deep' ? 'var(--gradient-chrome-2)' : 'var(--gradient-chrome)'
const node = (
<Tag
style={{
backgroundImage:gradient,
WebkitBackgroundClip:'text',backgroundClip:'text',
WebkitTextFillColor:'transparent',color:'transparent',
display:'inline-block',
...style,
}}
{...rest}
>
{children}
</Tag>
)
if (!preHeading) return node
return (
<span style={{display:'flex',flexDirection:'column',gap:12,alignItems:'inherit'}}>
<span style={{fontSize:'var(--text-xs)',fontWeight:'var(--weight-semibold)',textTransform:'uppercase',letterSpacing:'var(--tracking-widest)',color:'var(--text-helper)',...preHeadingStyle}}>{preHeading}</span>
{node}
</span>
)
}
+8
View File
@@ -0,0 +1,8 @@
Wraps hero and section headlines in the chrome gradient (white → white/60).
```jsx
<ChromeText as="h1" style={{fontSize:'var(--text-6xl)',fontWeight:700}}>Zoo, our frontier model</ChromeText>
<ChromeText as="h2" preHeading="AI cloud" style={{fontSize:'var(--text-4xl)',fontWeight:700}}>Built for agents</ChromeText>
```
Every product-page h1 uses this. `tone="deep"` (white → neutral-500) is for the very largest display type. Never substitute a hued gradient.
+10
View File
@@ -0,0 +1,10 @@
/** Lucide icon glyph, the only icon set Zoo uses. */
export interface IconProps extends React.SVGAttributes<SVGSVGElement> {
/** Lucide icon name, PascalCase or kebab-case: "ArrowRight" | "arrow-right". */
name: string;
/** Pixel size. Zoo sizes: 12 inline, 16 in CTAs, 24 card eyebrow, 40 section accent. */
size?: number;
strokeWidth?: number;
color?: string;
}
export function Icon(props: IconProps): JSX.Element;
+43
View File
@@ -0,0 +1,43 @@
import React from 'react'
/** Lucide glyph renderer. Zoo uses lucide-react 0.462.0 and no other icon set;
* in a plain browser page the same set arrives as the lucide UMD bundle, which
* this component reads. Load it once per page:
* <script src="https://unpkg.com/lucide@0.462.0/dist/umd/lucide.js"></script> */
const pascal = (n) => n.replace(/(^|[-_ ])(\w)/g, (_, __, c) => c.toUpperCase())
function lookup(name) {
const g = typeof window !== 'undefined' ? window.lucide : null
if (!g) return null
const set = g.icons || g
return set[name] || set[pascal(name)] || null
}
export function Icon({ name, size = 16, strokeWidth = 2, color = 'currentColor', style, ...rest }) {
const node = lookup(name)
const kids = []
if (Array.isArray(node)) {
node.forEach((child, i) => {
if (Array.isArray(child)) kids.push(React.createElement(child[0], { key: i, ...child[1] }))
else if (child && child.tag) kids.push(React.createElement(child.tag, { key: i, ...child.attrs }))
})
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
style={{ flexShrink: 0, display: 'block', ...style }}
{...rest}
>
{kids}
</svg>
)
}
+9
View File
@@ -0,0 +1,9 @@
Renders a lucide glyph — the only icon set on Zoo surfaces.
```jsx
<Icon name="ArrowRight" size={16} />
<Icon name="Shield" size={24} color="var(--text-secondary)" />
```
Requires the lucide UMD bundle on the page: `<script src="https://unpkg.com/lucide@0.462.0/dist/umd/lucide.js"></script>`.
Sizes in use: 12 (inline with body text), 16 (button/CTA), 24 (card eyebrow), 40 (section accent). Stroke stays at the default 2. Colour inherits — never tint an icon with hue.
+8
View File
@@ -0,0 +1,8 @@
/** Thin monochrome meter — white fill on a neutral-800 track. */
export interface ProgressProps extends React.HTMLAttributes<HTMLDivElement> {
value?: number;
max?: number;
/** Track height in px. 6 in dashboards, 1.52 for inline meters. */
height?: number;
}
export function Progress(props: ProgressProps): JSX.Element;
+11
View File
@@ -0,0 +1,11 @@
import React from 'react'
export function Progress({ value = 0, max = 100, height = 6, style, ...rest }) {
const pct = Math.max(0, Math.min(100, (value / max) * 100))
return (
<div role="progressbar" aria-valuenow={value} aria-valuemin={0} aria-valuemax={max}
style={{position:'relative',width:'100%',height,borderRadius:'var(--radius-full)',background:'var(--neutral-800)',overflow:'hidden',...style}} {...rest}>
<div style={{height:'100%',width:pct+'%',background:'var(--primary)',borderRadius:'var(--radius-full)',transition:'width var(--duration-base) var(--ease-out)'}} />
</div>
)
}
+8
View File
@@ -0,0 +1,8 @@
Usage/quota meter. White fill on a neutral-800 track — no colour thresholds.
```jsx
<Progress value={68} height={6} />
<Progress value={92} height={2} style={{width:64}} />
```
Used for agent memory, credit balance, and resource usage. Pair with a numeric label; the bar alone never carries the value.
+11
View File
@@ -0,0 +1,11 @@
/** Dense data table. Rows separated by neutral-800 hairlines; header sits on pure black. */
export interface TableRowProps extends React.HTMLAttributes<HTMLTableRowElement> {
/** Adds the neutral-900/60 hover fill — use when the row opens something. */
interactive?: boolean;
}
export function Table(props: React.TableHTMLAttributes<HTMLTableElement>): JSX.Element;
export function TableHeader(props: React.HTMLAttributes<HTMLTableSectionElement>): JSX.Element;
export function TableBody(props: React.HTMLAttributes<HTMLTableSectionElement>): JSX.Element;
export function TableRow(props: TableRowProps): JSX.Element;
export function TableHead(props: React.ThHTMLAttributes<HTMLTableCellElement> & { align?: 'left' | 'right' | 'center' }): JSX.Element;
export function TableCell(props: React.TdHTMLAttributes<HTMLTableCellElement> & { align?: 'left' | 'right' | 'center' }): JSX.Element;
+30
View File
@@ -0,0 +1,30 @@
import React from 'react'
export function Table({ style, children, ...rest }) {
return <table style={{width:'100%',borderCollapse:'collapse',fontSize:'var(--text-sm)',...style}} {...rest}>{children}</table>
}
export function TableHeader({ style, children, ...rest }) {
return <thead style={{background:'var(--black)',...style}} {...rest}>{children}</thead>
}
export function TableBody({ style, children, ...rest }) {
return <tbody style={style} {...rest}>{children}</tbody>
}
export function TableRow({ interactive = false, style, children, ...rest }) {
const [hover, setHover] = React.useState(false)
return (
<tr
onMouseEnter={interactive ? () => setHover(true) : undefined}
onMouseLeave={interactive ? () => setHover(false) : undefined}
style={{borderTop:'1px solid var(--border-hairline)',background: hover ? 'rgb(23 23 23 / .6)' : 'transparent',cursor: interactive ? 'pointer' : 'default',transition:'background-color var(--duration-fast) var(--ease-out)',...style}}
{...rest}
>
{children}
</tr>
)
}
export function TableHead({ align = 'left', style, children, ...rest }) {
return <th style={{padding:'12px 24px',textAlign:align,fontWeight:'var(--weight-medium)',color:'var(--text-helper)',...style}} {...rest}>{children}</th>
}
export function TableCell({ align = 'left', style, children, ...rest }) {
return <td style={{padding:'16px 24px',textAlign:align,color:'var(--foreground)',...style}} {...rest}>{children}</td>
}
+10
View File
@@ -0,0 +1,10 @@
Invoice / agent / usage tables in the console.
```jsx
<Table>
<TableHeader><TableRow><TableHead>Invoice</TableHead><TableHead align="right">Amount</TableHead></TableRow></TableHeader>
<TableBody><TableRow interactive><TableCell>INV-20230501</TableCell><TableCell align="right">$25.00</TableCell></TableRow></TableBody>
</Table>
```
Wrap in a `Card` for the rounded hairline frame. Status cells use `Badge` with the opacity ladder, not coloured chips.
+11
View File
@@ -0,0 +1,11 @@
/** The Zoo mark from the press kit.
export interface ZooLogoProps {
/** Rendered square size in px. Nav/footer use 22; hero lockups 80. */
size?: number;
/** white on dark surfaces (default), black on light, inherit to take currentColor. */
variant?: 'white' | 'black' | 'inherit';
title?: string;
}
export function ZooLogo(props: ZooLogoProps): JSX.Element;
export interface ZooWordmarkProps extends ZooLogoProps { label?: string }
export function ZooWordmark(props: ZooWordmarkProps): JSX.Element;
+21
View File
@@ -0,0 +1,21 @@
import React from 'react'
/** The Zoo mark (assets/logo.svg), monochrome — never re-hued. */
export function ZooLogo({ size = 22, variant = 'white', title = 'Zoo', style, ...rest }) {
const fill = variant === 'white' ? '#ffffff' : variant === 'black' ? 'var(--zoo-black)' : 'currentColor'
return (
<svg viewBox="0 0 32 32" width={size} height={size} xmlns="http://www.w3.org/2000/svg" role="img" aria-label={title} style={{ display: 'block', flexShrink: 0, ...style }} {...rest}>
<path d="M10 10H14L16 16L18 10H22" stroke={fill} stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"></path><path d="M10 22H22" stroke={fill} stroke-width="2" stroke-linecap="round" fill="none"></path><circle cx="16" cy="16" r="12" stroke={fill} stroke-width="1" stroke-opacity="0.2" fill="none"></circle>
</svg>
)
}
/** Mark + wordmark lockup, as used in the nav and footer. */
export function ZooWordmark({ size = 22, label = 'Zoo Labs Foundation', variant = 'white', style }) {
return (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8, ...style }}>
<ZooLogo size={size} variant={variant} />
<span style={{ fontSize: 15, fontWeight: 600, letterSpacing: 'var(--tracking-tight)', color: 'var(--text-primary)' }}>{label}</span>
</span>
)
}
+8
View File
@@ -0,0 +1,8 @@
The Zoo mark (and mark+wordmark lockup) — use instead of typing "Zoo" where a logo belongs.
```jsx
<ZooLogo size={22} variant="white" />
<ZooWordmark label="Zoo Labs Foundation" />
```
Nav and footer use size 22; the wordmark collapses to the bare mark once the page scrolls. Never recolour with hue, never stretch, keep clear space around it.
+56
View File
@@ -0,0 +1,56 @@
<!-- @dsCard group="Components" viewport="700x340" name="Core" subtitle="Button, Badge, Card, Progress, Avatar, ChromeText, Icon" -->
<!doctype html><html class="dark"><head><meta charset="utf-8"><title>Core components</title>
<link rel="stylesheet" href="../../styles.css">
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script src="https://unpkg.com/lucide@0.462.0/dist/umd/lucide.js"></script>
<script src="../../_ds_bundle.js"></script>
<style>body{margin:0;padding:20px;background:#000;font-family:var(--font-sans)}.row{display:flex;flex-wrap:wrap;align-items:center;gap:10px}.col{display:flex;flex-direction:column;gap:14px}.lbl{font-size:10px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--neutral-600)}</style></head><body><div id="root"></div>
<script type="text/babel">
const { Button, Badge, StatusBadge, Card, CardContent, CardTitle, CardDescription, ChromeText, Progress, Avatar, AvatarFallback, Icon, ZooLogo } = window.ZooDesignSystem_0a9445;
function Demo(){
return (<div className="col">
<div className="row">
<Button pill>Try Zoo <Icon name="ChevronDown" /></Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline"><Icon name="Filter" /> Filter</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
<Button variant="link">Read the docs</Button>
<Button variant="ghost" size="icon"><Icon name="Settings" /></Button>
<Button disabled>Disabled</Button>
<Button size="sm" variant="outline">Small</Button>
<Button size="lg" pill>Get started <Icon name="ArrowRight" /></Button>
</div>
<div className="row">
<Badge>Default</Badge><Badge variant="tier1">Edge</Badge><Badge variant="tier2">Pro</Badge><Badge variant="tier3">Ultra</Badge>
<Badge variant="outline">Open source</Badge><Badge variant="error">Failed</Badge>
<StatusBadge status="online" label="All systems normal" />
<Avatar size={28}><AvatarFallback>ZK</AvatarFallback></Avatar>
<Avatar size={28} />
<ZooLogo size={22} />
</div>
<div className="row" style={{alignItems:'stretch'}}>
<Card interactive sheen style={{flex:1,minWidth:200}}>
<CardContent>
<Icon name="Shield" size={24} color="var(--text-secondary)" />
<CardTitle style={{marginTop:12}}>Private by default</CardTitle>
<CardDescription>Every request scoped to your org.</CardDescription>
</CardContent>
</Card>
<Card variant="emphasis" style={{flex:1,minWidth:200}}>
<CardContent>
<div className="lbl">Credits</div>
<div style={{fontSize:'var(--text-2xl)',fontWeight:600,color:'#fff',margin:'6px 0 10px'}}>$412.80</div>
<Progress value={68} />
</CardContent>
</Card>
<Card variant="plain" style={{flex:1,minWidth:200,display:'flex',alignItems:'center',justifyContent:'center',padding:16}}>
<ChromeText as="div" style={{fontSize:'var(--text-3xl)',fontWeight:700,letterSpacing:'-.025em'}}>Chrome text</ChromeText>
</Card>
</div>
</div>)
}
ReactDOM.createRoot(document.getElementById('root')).render(<Demo />);
</script></body></html>
+10
View File
@@ -0,0 +1,10 @@
/** Square 16px checkbox; checked state is a white fill with a black tick. */
export interface CheckboxProps {
checked?: boolean;
defaultChecked?: boolean;
onChange?: (checked: boolean) => void;
disabled?: boolean;
/** Optional inline label rendered to the right. */
label?: React.ReactNode;
}
export function Checkbox(props: CheckboxProps): JSX.Element;
+33
View File
@@ -0,0 +1,33 @@
import React from 'react'
export function Checkbox({ checked, defaultChecked, onChange, disabled, label, style, ...rest }) {
const [inner, setInner] = React.useState(!!defaultChecked)
const on = checked === undefined ? inner : checked
const toggle = () => {
if (disabled) return
if (checked === undefined) setInner(!on)
onChange && onChange(!on)
}
const box = (
<span
role="checkbox" aria-checked={on} tabIndex={disabled ? -1 : 0}
onClick={toggle}
onKeyDown={(e)=>{if(e.key===' '||e.key==='Enter'){e.preventDefault();toggle()}}}
style={{
display:'inline-flex',alignItems:'center',justifyContent:'center',width:16,height:16,flexShrink:0,
borderRadius:4,border:'1px solid ' + (on ? 'var(--primary)' : 'var(--border-strong)'),
background: on ? 'var(--primary)' : 'transparent',
cursor: disabled ? 'not-allowed' : 'pointer', opacity: disabled ? 0.5 : 1,
transition:'background-color var(--duration-fast) var(--ease-out), border-color var(--duration-fast) var(--ease-out)',
...style,
}}
{...rest}
>
{on && (
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="var(--primary-foreground)" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12" /></svg>
)}
</span>
)
if (!label) return box
return <span style={{display:'inline-flex',alignItems:'center',gap:8,fontSize:'var(--text-sm)',color:'var(--text-secondary)'}}>{box}{label}</span>
}
+8
View File
@@ -0,0 +1,8 @@
Checkbox for opt-ins and multi-select filters.
```jsx
<Checkbox label="Auto-recharge credits" defaultChecked />
<Checkbox checked={agree} onChange={setAgree} label="I agree to the terms" />
```
Checked = white fill + black tick. There is no indeterminate state in the Zoo set.
+3
View File
@@ -0,0 +1,3 @@
/** Single-line text field. Black fill, neutral-800 hairline, ring-coloured focus border. */
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> { invalid?: boolean }
export function Input(props: InputProps): JSX.Element;
+21
View File
@@ -0,0 +1,21 @@
import React from 'react'
export function Input({ invalid = false, style, ...rest }) {
const [focus, setFocus] = React.useState(false)
return (
<input
onFocus={(e)=>{setFocus(true);rest.onFocus&&rest.onFocus(e)}}
onBlur={(e)=>{setFocus(false);rest.onBlur&&rest.onBlur(e)}}
style={{
width:'100%',height:36,padding:'0 12px',
fontFamily:'var(--font-sans)',fontSize:'var(--text-sm)',color:'var(--foreground)',
background:'var(--black)',
border:'1px solid ' + (invalid ? 'var(--state-error)' : focus ? 'var(--ring)' : 'var(--border)'),
borderRadius:'var(--radius-sm)',outline:'none',
transition:'border-color var(--duration-fast) var(--ease-out)',
...style,
}}
{...rest}
/>
)
}
+8
View File
@@ -0,0 +1,8 @@
Text field for search, forms, and settings.
```jsx
<Input placeholder="Search agents…" />
<Input type="email" invalid aria-describedby="err" />
```
For search fields, place a 16px lucide `Search` icon absolutely at 12px from the left and pad the input to 36px. Focus shows a `--ring` border, never a removed outline.
+3
View File
@@ -0,0 +1,3 @@
/** Field label — 14px medium at 80% white. */
export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> { required?: boolean }
export function Label(props: LabelProps): JSX.Element;
+9
View File
@@ -0,0 +1,9 @@
import React from 'react'
export function Label({ required = false, style, children, ...rest }) {
return (
<label style={{display:'inline-block',fontSize:'var(--text-sm)',fontWeight:'var(--weight-medium)',color:'var(--text-secondary)',...style}} {...rest}>
{children}{required && <span aria-hidden style={{color:'var(--text-helper)'}}> *</span>}
</label>
)
}
+8
View File
@@ -0,0 +1,8 @@
Field label. Always `htmlFor` the control it names.
```jsx
<Label htmlFor="org" required>Organization name</Label>
<Input id="org" />
```
Stack label → control with 8px gap; helper text goes below the control in `--text-xs` / `--text-helper`.
+9
View File
@@ -0,0 +1,9 @@
/** Dropdown select. Same chrome as Input plus a chevron. */
export interface SelectOption { value: string; label: string }
export interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, 'onChange'> {
options?: (SelectOption | string)[];
value?: string;
onChange?: (value: string) => void;
placeholder?: string;
}
export function Select(props: SelectProps): JSX.Element;
+33
View File
@@ -0,0 +1,33 @@
import React from 'react'
/** Native-select build of the @zoo/ui Select — same chrome, no portal. */
export function Select({ options = [], value, onChange, placeholder, style, children, ...rest }) {
const [focus, setFocus] = React.useState(false)
return (
<span style={{position:'relative',display:'inline-flex',alignItems:'center',width:'100%'}}>
<select
value={value}
onChange={(e)=>onChange && onChange(e.target.value)}
onFocus={()=>setFocus(true)} onBlur={()=>setFocus(false)}
style={{
appearance:'none',width:'100%',height:36,padding:'0 32px 0 12px',
fontFamily:'var(--font-sans)',fontSize:'var(--text-sm)',color:'var(--foreground)',
background:'var(--black)',border:'1px solid ' + (focus ? 'var(--ring)' : 'var(--border)'),
borderRadius:'var(--radius-sm)',outline:'none',cursor:'pointer',
transition:'border-color var(--duration-fast) var(--ease-out)',
...style,
}}
{...rest}
>
{placeholder && <option value="">{placeholder}</option>}
{options.map((o) => {
const v = typeof o === 'string' ? o : o.value
const l = typeof o === 'string' ? o : o.label
return <option key={v} value={v}>{l}</option>
})}
{children}
</select>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--text-helper)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" style={{position:'absolute',right:10,pointerEvents:'none'}}><polyline points="6 9 12 15 18 9" /></svg>
</span>
)
}
+7
View File
@@ -0,0 +1,7 @@
Single-choice dropdown for settings and filters.
```jsx
<Select value={org} onChange={setOrg} options={[{value:'zoo',label:'Zoo Labs Foundation'},{value:'zoo',label:'Zoo Labs'}]} />
```
For 23 short options prefer `Tabs` or a segmented row of `Button variant="outline"`; reach for Select at 4+.
+7
View File
@@ -0,0 +1,7 @@
/** Range control — white filled track on neutral-800, used on the pricing/team-size calculators. */
export interface SliderProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value'> {
min?: number; max?: number; step?: number;
value?: number; defaultValue?: number;
onChange?: (value: number) => void;
}
export function Slider(props: SliderProps): JSX.Element;
+22
View File
@@ -0,0 +1,22 @@
import React from 'react'
export function Slider({ min = 0, max = 100, step = 1, value, defaultValue = 0, onChange, style, ...rest }) {
const [inner, setInner] = React.useState(defaultValue)
const v = value === undefined ? inner : value
const pct = ((v - min) / (max - min)) * 100
return (
<input
type="range" min={min} max={max} step={step} value={v}
onChange={(e) => { const n = Number(e.target.value); if (value === undefined) setInner(n); onChange && onChange(n) }}
style={{
appearance:'none',WebkitAppearance:'none',width:'100%',height:16,background:'transparent',cursor:'pointer',
backgroundImage:'linear-gradient(var(--primary),var(--primary)),linear-gradient(var(--neutral-800),var(--neutral-800))',
backgroundSize: pct + '% 4px, 100% 4px',
backgroundPosition:'left center, left center',
backgroundRepeat:'no-repeat',
...style,
}}
{...rest}
/>
)
}
+7
View File
@@ -0,0 +1,7 @@
Numeric range — seats, credits, spend caps.
```jsx
<Slider min={1} max={200} value={seats} onChange={setSeats} />
```
Always render the current value as text beside or above the slider; the track alone never communicates the number.
+3
View File
@@ -0,0 +1,3 @@
/** Multi-line field. Same chrome as Input, vertical resize only. */
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
export function Textarea(props: TextareaProps): JSX.Element;
+21
View File
@@ -0,0 +1,21 @@
import React from 'react'
export function Textarea({ rows = 4, style, ...rest }) {
const [focus, setFocus] = React.useState(false)
return (
<textarea
rows={rows}
onFocus={(e)=>{setFocus(true);rest.onFocus&&rest.onFocus(e)}}
onBlur={(e)=>{setFocus(false);rest.onBlur&&rest.onBlur(e)}}
style={{
width:'100%',padding:'8px 12px',resize:'vertical',
fontFamily:'var(--font-sans)',fontSize:'var(--text-sm)',lineHeight:'var(--leading-relaxed)',color:'var(--foreground)',
background:'var(--black)',border:'1px solid ' + (focus ? 'var(--ring)' : 'var(--border)'),
borderRadius:'var(--radius-sm)',outline:'none',
transition:'border-color var(--duration-fast) var(--ease-out)',
...style,
}}
{...rest}
/>
)
}
+7
View File
@@ -0,0 +1,7 @@
Multi-line input for descriptions, prompts, and notes.
```jsx
<Textarea rows={5} placeholder="System prompt…" />
```
The chat composer is NOT this component — it is a borderless textarea inside a 28px-radius pill (see the site UI kit).
+39
View File
@@ -0,0 +1,39 @@
<!-- @dsCard group="Components" viewport="700x300" name="Forms" subtitle="Input, Label, Textarea, Checkbox, Select, Slider" -->
<!doctype html><html class="dark"><head><meta charset="utf-8"><title>Form components</title>
<link rel="stylesheet" href="../../styles.css">
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script src="https://unpkg.com/lucide@0.462.0/dist/umd/lucide.js"></script>
<script src="../../_ds_bundle.js"></script>
<style>body{margin:0;padding:20px;background:#000;font-family:var(--font-sans)}.row{display:flex;flex-wrap:wrap;align-items:center;gap:10px}.col{display:flex;flex-direction:column;gap:14px}.lbl{font-size:10px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--neutral-600)}</style></head><body><div id="root"></div>
<script type="text/babel">
const { Input, Label, Textarea, Checkbox, Select, Slider, Icon } = window.ZooDesignSystem_0a9445;
function Demo(){
const [seats,setSeats] = React.useState(24);
return (<div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:20}}>
<div className="col">
<div><Label htmlFor="a" required>Organization name</Label><div style={{height:8}} /><Input id="a" defaultValue="Zoo Labs Foundation" /></div>
<div style={{position:'relative'}}>
<Icon name="Search" size={16} color="var(--text-helper)" style={{position:'absolute',left:12,top:10}} />
<Input placeholder="Search agents…" style={{paddingLeft:36}} />
</div>
<Input placeholder="you@company.com" invalid defaultValue="not-an-email" />
<div className="row" style={{gap:20}}>
<Checkbox label="Auto-recharge" defaultChecked />
<Checkbox label="Email receipts" />
</div>
</div>
<div className="col">
<div><Label htmlFor="m">Default model</Label><div style={{height:8}} /><Select id="m" options={[{value:'flash',label:'Zoo Flash'},{value:'pro',label:'Zoo Pro'},{value:'ultra',label:'Zoo Ultra'}]} /></div>
<Textarea rows={3} placeholder="System prompt…" />
<div>
<div style={{display:'flex',justifyContent:'space-between',fontSize:'var(--text-sm)',color:'var(--text-secondary)'}}><span>Seats</span><span style={{color:'#fff'}}>{seats}</span></div>
<div style={{height:8}} />
<Slider min={1} max={200} value={seats} onChange={setSeats} />
</div>
</div>
</div>)
}
ReactDOM.createRoot(document.getElementById('root')).render(<Demo />);
</script></body></html>
+11
View File
@@ -0,0 +1,11 @@
/** Hairline-divided disclosure list. The FAQ pattern on pricing and download pages. */
export interface AccordionProps extends React.HTMLAttributes<HTMLDivElement> {
type?: 'single' | 'multiple';
defaultValue?: string | string[] | null;
}
export function Accordion(props: AccordionProps): JSX.Element;
export interface AccordionItemProps extends React.HTMLAttributes<HTMLDivElement> {
value: string;
title: React.ReactNode;
}
export function AccordionItem(props: AccordionItemProps): JSX.Element;
+37
View File
@@ -0,0 +1,37 @@
import React from 'react'
const Ctx = React.createContext({ open: null, toggle: () => {} })
export function Accordion({ type = 'single', defaultValue = null, style, children, ...rest }) {
const [open, setOpen] = React.useState(type === 'multiple' ? [].concat(defaultValue || []) : defaultValue)
const toggle = (v) => {
if (type === 'multiple') setOpen((cur) => (cur.includes(v) ? cur.filter((x) => x !== v) : [...cur, v]))
else setOpen((cur) => (cur === v ? null : v))
}
const isOpen = (v) => (type === 'multiple' ? open.includes(v) : open === v)
return (
<Ctx.Provider value={{ isOpen, toggle }}>
<div style={{borderTop:'1px solid var(--border-hairline)',...style}} {...rest}>{children}</div>
</Ctx.Provider>
)
}
export function AccordionItem({ value, title, style, children, ...rest }) {
const { isOpen, toggle } = React.useContext(Ctx)
const on = isOpen(value)
return (
<div style={{borderBottom:'1px solid var(--border-hairline)',...style}} {...rest}>
<button
onClick={() => toggle(value)} aria-expanded={on}
style={{display:'flex',width:'100%',alignItems:'center',justifyContent:'space-between',gap:16,padding:'18px 0',background:'transparent',border:'none',cursor:'pointer',textAlign:'left',fontFamily:'var(--font-sans)',fontSize:'var(--text-base)',fontWeight:'var(--weight-medium)',color:'var(--text-primary)'}}
>
{title}
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="var(--text-helper)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" style={{flexShrink:0,transform: on ? 'rotate(180deg)' : 'none',transition:'transform var(--duration-fast) var(--ease-out)'}}><polyline points="6 9 12 15 18 9" /></svg>
</button>
{on && <div style={{paddingBottom:20,fontSize:'var(--text-sm)',lineHeight:'var(--leading-relaxed)',color:'var(--text-helper)',maxWidth:'62ch'}}>{children}</div>}
</div>
)
}
export const AccordionTrigger = ({ children }) => children
export const AccordionContent = ({ children }) => children
+10
View File
@@ -0,0 +1,10 @@
FAQ / disclosure list — hairline dividers, chevron rotates on open.
```jsx
<Accordion defaultValue="q1">
<AccordionItem value="q1" title="How is usage billed?">Per token, metered hourly.</AccordionItem>
<AccordionItem value="q2" title="Can I self-host?">Yes every service is open source.</AccordionItem>
</Accordion>
```
Answers cap at ~62ch. Use `type="multiple"` only for long reference lists.
+10
View File
@@ -0,0 +1,10 @@
/** Segmented tab bar above a panel. Active tab is a neutral-1a chip with a hairline.
export interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {
defaultValue?: string;
value?: string;
onValueChange?: (value: string) => void;
}
export function Tabs(props: TabsProps): JSX.Element;
export function TabsList(props: React.HTMLAttributes<HTMLDivElement>): JSX.Element;
export function TabsTrigger(props: React.ButtonHTMLAttributes<HTMLButtonElement> & { value: string }): JSX.Element;
export function TabsContent(props: React.HTMLAttributes<HTMLDivElement> & { value: string }): JSX.Element;
+47
View File
@@ -0,0 +1,47 @@
import React from 'react'
const Ctx = React.createContext({ value: null, setValue: () => {} })
export function Tabs({ defaultValue, value, onValueChange, style, children, ...rest }) {
const [inner, setInner] = React.useState(defaultValue)
const v = value === undefined ? inner : value
const setValue = (n) => { if (value === undefined) setInner(n); onValueChange && onValueChange(n) }
return (
<Ctx.Provider value={{ value: v, setValue }}>
<div style={{display:'flex',flexDirection:'column',gap:16,...style}} {...rest}>{children}</div>
</Ctx.Provider>
)
}
export function TabsList({ style, children, ...rest }) {
return (
<div role="tablist" style={{display:'inline-flex',alignItems:'center',gap:4,padding:4,borderRadius:'var(--radius-sm)',background:'var(--muted)',border:'1px solid var(--border)',...style}} {...rest}>{children}</div>
)
}
export function TabsTrigger({ value, style, children, ...rest }) {
const { value: active, setValue } = React.useContext(Ctx)
const on = active === value
return (
<button
role="tab" aria-selected={on} onClick={() => setValue(value)}
style={{
display:'inline-flex',alignItems:'center',gap:8,height:30,padding:'0 12px',
fontFamily:'var(--font-sans)',fontSize:'var(--text-sm)',fontWeight:'var(--weight-medium)',
color: on ? 'var(--text-primary)' : 'var(--text-helper)',
background: on ? 'var(--secondary)' : 'transparent',
border:'1px solid ' + (on ? 'var(--border)' : 'transparent'),
borderRadius:'calc(var(--radius-sm) - 2px)',cursor:'pointer',whiteSpace:'nowrap',
transition:'color var(--duration-fast) var(--ease-out), background-color var(--duration-fast) var(--ease-out)',
...style,
}}
{...rest}
>{children}</button>
)
}
export function TabsContent({ value, style, children, ...rest }) {
const { value: active } = React.useContext(Ctx)
if (active !== value) return null
return <div role="tabpanel" style={style} {...rest}>{children}</div>
}
+11
View File
@@ -0,0 +1,11 @@
Switches between sibling panels — used for language tabs on code samples and for pricing categories.
```jsx
<Tabs defaultValue="ts">
<TabsList><TabsTrigger value="ts">TypeScript</TabsTrigger><TabsTrigger value="py">Python</TabsTrigger></TabsList>
<TabsContent value="ts"><pre></pre></TabsContent>
<TabsContent value="py"><pre></pre></TabsContent>
</Tabs>
```
The tab bar always stacks above the panel. Keep labels to one or two words.
@@ -0,0 +1,31 @@
<!-- @dsCard group="Components" viewport="700x300" name="Navigation" subtitle="Tabs and Accordion" -->
<!doctype html><html class="dark"><head><meta charset="utf-8"><title>Navigation components</title>
<link rel="stylesheet" href="../../styles.css">
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
<script src="../../_ds_bundle.js"></script>
<style>body{margin:0;padding:20px;background:#000;font-family:var(--font-sans)}pre{margin:0;padding:16px;background:var(--card);border:1px solid var(--border-hairline);border-radius:var(--radius-lg);font-size:12px;line-height:1.7;color:var(--neutral-300);overflow:auto}</style></head>
<body><div id="root"></div>
<script type="text/babel">
const { Tabs, TabsList, TabsTrigger, TabsContent, Accordion, AccordionItem } = window.ZooDesignSystem_0a9445;
const TS = "import Zoo from '@zoo/cloud'\n\nconst zoo = new Zoo()\nawait zoo.chat.completions.create({\n model: 'zoo-pro',\n messages: [{ role: 'user', content: 'Hi' }],\n})";
const PY = "from zoo import Zoo\n\nzoo = Zoo()\nzoo.chat.completions.create(\n model=\"zoo-pro\",\n messages=[{\"role\": \"user\", \"content\": \"Hi\"}],\n)";
const SH = "curl https://api.zoo.ngo/v1/chat/completions \\\n -H \"Authorization: Bearer $ZOO_API_KEY\" \\\n -d '{\"model\":\"zoo-pro\"}'";
function Demo(){
return (<div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:24}}>
<Tabs defaultValue="ts">
<TabsList><TabsTrigger value="ts">TypeScript</TabsTrigger><TabsTrigger value="py">Python</TabsTrigger><TabsTrigger value="sh">cURL</TabsTrigger></TabsList>
<TabsContent value="ts"><pre>{TS}</pre></TabsContent>
<TabsContent value="py"><pre>{PY}</pre></TabsContent>
<TabsContent value="sh"><pre>{SH}</pre></TabsContent>
</Tabs>
<Accordion defaultValue="q1">
<AccordionItem value="q1" title="How is usage billed?">Per token, metered hourly. You pay only for what you use.</AccordionItem>
<AccordionItem value="q2" title="Can I self-host?">Yes. Every service in the Zoo cloud is open source.</AccordionItem>
<AccordionItem value="q3" title="Which models are available?">Zen open weights, plus Zoo Flash, Pro, and Ultra.</AccordionItem>
</Accordion>
</div>)
}
ReactDOM.createRoot(document.getElementById('root')).render(<Demo />);
</script></body></html>
+12
View File
@@ -0,0 +1,12 @@
/** Centred modal on an 80% black scrim with a 4px backdrop blur. Escape and scrim click close it.
export interface DialogProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
children?: React.ReactNode;
}
export function Dialog(props: DialogProps): JSX.Element | null;
export function DialogContent(props: React.HTMLAttributes<HTMLDivElement> & { width?: number }): JSX.Element;
export function DialogHeader(props: React.HTMLAttributes<HTMLDivElement>): JSX.Element;
export function DialogTitle(props: React.HTMLAttributes<HTMLHeadingElement>): JSX.Element;
export function DialogDescription(props: React.HTMLAttributes<HTMLParagraphElement>): JSX.Element;
export function DialogFooter(props: React.HTMLAttributes<HTMLDivElement>): JSX.Element;
+41
View File
@@ -0,0 +1,41 @@
import React from 'react'
export function Dialog({ open, onOpenChange, children }) {
React.useEffect(() => {
if (!open) return
const onKey = (e) => { if (e.key === 'Escape') onOpenChange && onOpenChange(false) }
document.addEventListener('keydown', onKey)
return () => document.removeEventListener('keydown', onKey)
}, [open, onOpenChange])
if (!open) return null
return (
<div
onClick={() => onOpenChange && onOpenChange(false)}
style={{position:'fixed',inset:0,zIndex:80,display:'flex',alignItems:'center',justifyContent:'center',padding:24,background:'rgb(0 0 0 / .8)',backdropFilter:'blur(4px)',animation:'zoo-fade-up var(--duration-fast) var(--ease-out)'}}
>
{children}
</div>
)
}
export function DialogContent({ width = 480, style, children, ...rest }) {
return (
<div
role="dialog" aria-modal="true" onClick={(e) => e.stopPropagation()}
style={{width:'100%',maxWidth:width,maxHeight:'85vh',overflowY:'auto',padding:24,display:'flex',flexDirection:'column',gap:16,background:'var(--popover)',color:'var(--popover-foreground)',border:'1px solid var(--border-hairline)',borderRadius:'var(--radius-xl)',boxShadow:'var(--shadow-floating)',...style}}
{...rest}
>{children}</div>
)
}
export function DialogHeader({ style, children, ...rest }) {
return <div style={{display:'flex',flexDirection:'column',gap:6,...style}} {...rest}>{children}</div>
}
export function DialogTitle({ style, children, ...rest }) {
return <h2 style={{fontSize:'var(--text-lg)',fontWeight:'var(--weight-semibold)',color:'var(--text-primary)',...style}} {...rest}>{children}</h2>
}
export function DialogDescription({ style, children, ...rest }) {
return <p style={{fontSize:'var(--text-sm)',lineHeight:'var(--leading-relaxed)',color:'var(--text-helper)',...style}} {...rest}>{children}</p>
}
export function DialogFooter({ style, children, ...rest }) {
return <div style={{display:'flex',justifyContent:'flex-end',gap:8,...style}} {...rest}>{children}</div>
}
+13
View File
@@ -0,0 +1,13 @@
Modal for focused tasks — add credits, agent detail, confirm destructive action.
```jsx
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent width={480}>
<DialogHeader><DialogTitle>Add credits</DialogTitle><DialogDescription>Credits never expire.</DialogDescription></DialogHeader>
<Input placeholder="$50" />
<DialogFooter><Button variant="ghost" onClick={()=>setOpen(false)}>Cancel</Button><Button>Add credits</Button></DialogFooter>
</DialogContent>
</Dialog>
```
Escape and scrim click both close. 16px radius (rounded-2xl), hairline border, shadow-2xl.
+13
View File
@@ -0,0 +1,13 @@
/** Floating menu panel — 16px radius, blurred near-black fill, 8px padding. */
export interface DropdownMenuProps {
/** Which edge the panel aligns to. */
align?: 'left' | 'right';
/** Open on hover (the nav behaviour) instead of click. */
hover?: boolean;
trigger: React.ReactNode;
children?: React.ReactNode;
}
export function DropdownMenu(props: DropdownMenuProps): JSX.Element;
export function DropdownMenuItem(props: React.AnchorHTMLAttributes<HTMLAnchorElement> & { desc?: string }): JSX.Element;
export function DropdownMenuLabel(props: React.HTMLAttributes<HTMLSpanElement>): JSX.Element;
export function DropdownMenuSeparator(props: { style?: React.CSSProperties }): JSX.Element;
+49
View File
@@ -0,0 +1,49 @@
import React from 'react'
/** Hover-and-click dropdown, matching the nav's Log in / Try Zoo menus. */
export function DropdownMenu({ align = 'right', hover = false, trigger, style, children }) {
const [open, setOpen] = React.useState(false)
const ref = React.useRef(null)
React.useEffect(() => {
const onDoc = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false) }
document.addEventListener('mousedown', onDoc)
return () => document.removeEventListener('mousedown', onDoc)
}, [])
return (
<span
ref={ref}
onMouseEnter={hover ? () => setOpen(true) : undefined}
onMouseLeave={hover ? () => setOpen(false) : undefined}
style={{position:'relative',display:'inline-flex',...style}}
>
<span onClick={() => setOpen((v) => !v)} style={{display:'inline-flex'}}>{trigger}</span>
{open && (
<div style={{position:'absolute',top:'100%',[align]:0,paddingTop:12,zIndex:70,minWidth:208}}>
<div style={{display:'flex',flexDirection:'column',padding:8,background:'var(--surface-overlay)',border:'1px solid var(--border-hairline)',borderRadius:'var(--radius-xl)',boxShadow:'var(--shadow-floating)',backdropFilter:'blur(16px)',animation:'zoo-fade-down var(--duration-fast) var(--ease-out)'}}>
{children}
</div>
</div>
)}
</span>
)
}
export function DropdownMenuItem({ desc, style, children, ...rest }) {
const [hover, setHover] = React.useState(false)
return (
<a
onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
style={{display:'block',padding: desc ? '10px 12px' : '8px 12px',borderRadius:'var(--radius-sm)',background: hover ? 'var(--neutral-900)' : 'transparent',cursor:'pointer',textDecoration:'none',transition:'background-color var(--duration-fast) var(--ease-out)',...style}}
{...rest}
>
<span style={{display:'block',fontSize:'var(--text-sm)',fontWeight:'var(--weight-medium)',color:'var(--neutral-100)'}}>{children}</span>
{desc && <span style={{display:'block',marginTop:2,fontSize:'var(--text-xs)',color:'var(--neutral-500)'}}>{desc}</span>}
</a>
)
}
export function DropdownMenuLabel({ style, children, ...rest }) {
return <span style={{padding:'6px 12px',fontSize:'var(--text-xs)',fontWeight:'var(--weight-medium)',textTransform:'uppercase',letterSpacing:'var(--tracking-wide)',color:'var(--neutral-500)',...style}} {...rest}>{children}</span>
}
export function DropdownMenuSeparator({ style }) {
return <span style={{height:1,margin:'6px 4px',background:'var(--border-hairline)',...style}} />
}
@@ -0,0 +1,11 @@
Menu panel for nav actions, account switchers, and row actions.
```jsx
<DropdownMenu hover align="right" trigger={<Button pill>Try Zoo <Icon name="ChevronDown" /></Button>}>
<DropdownMenuItem desc="Chat with frontier models" href="https://zoo.chat">Zoo Chat</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem href="https://zoo.app">Zoo App</DropdownMenuItem>
</DropdownMenu>
```
Nav menus open on hover (`hover`); in-app menus open on click. Items support a small `desc` second line.
+12
View File
@@ -0,0 +1,12 @@
/** Edge-anchored panel — filter drawers on catalog pages, mobile navigation. */
export interface SheetProps {
open?: boolean;
onOpenChange?: (open: boolean) => void;
side?: 'left' | 'right';
width?: number;
children?: React.ReactNode;
}
export function Sheet(props: SheetProps): JSX.Element | null;
export function SheetHeader(props: React.HTMLAttributes<HTMLDivElement>): JSX.Element;
export function SheetTitle(props: React.HTMLAttributes<HTMLHeadingElement>): JSX.Element;
export function SheetContent(props: React.HTMLAttributes<HTMLDivElement>): JSX.Element;
+31
View File
@@ -0,0 +1,31 @@
import React from 'react'
export function Sheet({ open, onOpenChange, side = 'right', width = 380, children }) {
React.useEffect(() => {
if (!open) return
const onKey = (e) => { if (e.key === 'Escape') onOpenChange && onOpenChange(false) }
document.addEventListener('keydown', onKey)
return () => document.removeEventListener('keydown', onKey)
}, [open, onOpenChange])
if (!open) return null
const pos = side === 'left'
? { left: 0, top: 0, bottom: 0, width, borderRight: '1px solid var(--border-hairline)' }
: { right: 0, top: 0, bottom: 0, width, borderLeft: '1px solid var(--border-hairline)' }
return (
<div onClick={() => onOpenChange && onOpenChange(false)} style={{position:'fixed',inset:0,zIndex:80,background:'rgb(0 0 0 / .7)'}}>
<div
onClick={(e) => e.stopPropagation()}
style={{position:'absolute',display:'flex',flexDirection:'column',background:'var(--popover)',boxShadow:'var(--shadow-floating)',animation:'zoo-fade-up var(--duration-base) var(--ease-out)',...pos}}
>{children}</div>
</div>
)
}
export function SheetHeader({ style, children, ...rest }) {
return <div style={{padding:'20px 24px',borderBottom:'1px solid var(--border-hairline)',display:'flex',flexDirection:'column',gap:4,...style}} {...rest}>{children}</div>
}
export function SheetTitle({ style, children, ...rest }) {
return <h2 style={{fontSize:'var(--text-base)',fontWeight:'var(--weight-semibold)',color:'var(--text-primary)',...style}} {...rest}>{children}</h2>
}
export function SheetContent({ style, children, ...rest }) {
return <div style={{flex:1,overflowY:'auto',padding:24,...style}} {...rest}>{children}</div>
}
+10
View File
@@ -0,0 +1,10 @@
Side drawer — used for the OSS-catalog filters and the mobile menu.
```jsx
<Sheet open={open} onOpenChange={setOpen} side="right" width={380}>
<SheetHeader><SheetTitle>Filters</SheetTitle></SheetHeader>
<SheetContent><Checkbox label="Open source" /></SheetContent>
</Sheet>
```
Square corners against the viewport edge; no radius. Full-height only.
+12
View File
@@ -0,0 +1,12 @@
/** Transient confirmation. Mount <Toaster /> once, then call toast() from anywhere. */
export interface ToasterProps {
/** Auto-dismiss in ms. */
duration?: number;
position?: 'bottom-right' | 'top-right';
}
export function Toaster(props: ToasterProps): JSX.Element;
export interface ToastOptions { description?: string; tone?: 'default' | 'success' | 'error' }
export const toast: ((message: string, opts?: ToastOptions) => number) & {
success(message: string, opts?: ToastOptions): number;
error(message: string, opts?: ToastOptions): number;
};

Some files were not shown because too many files have changed in this diff Show More