UNPKG

@ryanhelsing/ry-ui

Version:

Framework-agnostic, Light DOM web components. CSS is the source of truth.

552 lines (428 loc) 18.1 kB
# ry-ui Framework-agnostic, Light DOM web components. Zero dependencies. CSS is the source of truth. ## Setup (2 lines) ```html <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ryanhelsing/ry-ui/dist/css/ry-ui.css"> <script type="module" src="https://cdn.jsdelivr.net/npm/@ryanhelsing/ry-ui/dist/ry-ui.js"></script> ``` ```bash # or npm npm install @ryanhelsing/ry-ui ``` Set theme: `<html data-ry-theme="light">` — `light` | `dark` | omit for OS preference Set body: `<body data-ry-reset style="background: var(--ry-color-bg); color: var(--ry-color-text);">` ## DON'T / DO DON'T write flexbox/grid CSS for page layout. DO: `<ry-page><ry-header>H</ry-header><ry-main>M</ry-main><ry-footer>F</ry-footer></ry-page>` DON'T write a custom modal with backdrop, focus trap, escape handling. DO: `<ry-button modal="m">Open</ry-button><ry-modal id="m" title="T">Content</ry-modal>` DON'T write a slide-out drawer with CSS transforms. DO: `<ry-button drawer="d">Open</ry-button><ry-drawer id="d" side="left">Content</ry-drawer>` DON'T write tab switching logic or CSS. DO: `<ry-tabs><ry-tab title="A" active>A</ry-tab><ry-tab title="B">B</ry-tab></ry-tabs>` DON'T write a custom select dropdown with keyboard navigation. DO: `<ry-select placeholder="Pick"><ry-option value="a">A</ry-option></ry-select>` DON'T write CSS variables for colors, spacing, shadows. DO: Use `--ry-color-*`, `--ry-space-*`, `--ry-radius-*`, `--ry-shadow-*` tokens. DON'T write button styles with hover/active/focus states. DO: `<ry-button variant="primary">Click</ry-button>` DON'T write toast/notification CSS and JS. DO: `RyToast.success('Saved!')` / `RyToast.error('Failed')` DON'T write accordion expand/collapse logic. DO: `<ry-accordion><ry-accordion-item title="Q" open>A</ry-accordion-item></ry-accordion>` DON'T write a toggle switch from scratch. DO: `<ry-switch name="notify" checked></ry-switch>` ## Full Page Template ```html <!DOCTYPE html> <html lang="en" data-ry-theme="light"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My App</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ryanhelsing/ry-ui/dist/css/ry-ui.css"> </head> <body data-ry-reset style="background: var(--ry-color-bg); color: var(--ry-color-text);"> <ry-page> <ry-header sticky> <ry-cluster> <strong>My App</strong> <ry-nav> <a href="/" aria-current="page">Home</a> <a href="/about">About</a> </ry-nav> </ry-cluster> <ry-actions> <ry-theme-toggle themes="light,dark"></ry-theme-toggle> </ry-actions> </ry-header> <ry-main> <ry-section> <h1>Hello World</h1> <p>Your content here.</p> </ry-section> </ry-main> <ry-footer>Built with ry-ui</ry-footer> </ry-page> <script type="module" src="https://cdn.jsdelivr.net/npm/@ryanhelsing/ry-ui/dist/ry-ui.js"></script> </body> </html> ``` ## Clean Syntax Wrap markup in `<ry>` to use unprefixed tags: ```html <ry> <accordion> <accordion-item title="FAQ" open>No ry- prefix needed.</accordion-item> </accordion> </ry> ``` --- ## Component Catalog ### Layout (CSS-only, no JS) | Component | Attributes | Description | |-----------|-----------|-------------| | `<ry-page>` || Root page container, flex column, min-height 100dvh | | `<ry-header>` | `sticky` | Flex row, space-between. `sticky` pins to top | | `<ry-main>` || Content area, max-width 1200px, centered | | `<ry-footer>` || Footer with border-top | | `<ry-section>` || Block section with bottom margin | | `<ry-grid>` | `cols="1-6\|auto-fit\|auto-fill"`, `cols-sm`, `cols-md`, `cols-lg` | CSS grid. [Details](docs/components/layout.md) | | `<ry-stack>` | `gap="sm\|md\|lg\|xl"` | Vertical flex column | | `<ry-cluster>` | `gap="sm\|md\|lg"` | Horizontal flex row, wraps | | `<ry-split>` | `resizable`, `persist="key"` | Two-column with drag resize. [Details](docs/components/layout.md) | | `<ry-center>` || Flex center (both axes) | | `<ry-nav>` || Horizontal nav links. Active: `a[aria-current="page"]` | | `<ry-logo>` || Inline-flex, bold text | | `<ry-actions>` || Flex row for action buttons | | `<ry-divider>` | `vertical` | Horizontal line; `vertical` for inline separator | | `<ry-aside>` || Sidebar content area | ### Interactive Components | Component | Key Attributes | Events | |-----------|---------------|--------| | `<ry-button>` | `variant="primary\|secondary\|outline\|ghost\|danger\|accent"`, `size="sm\|lg"`, `disabled`, `pressed`, `modal="id"`, `drawer="id"` | `ry:click` | | `<ry-toggle-button>` | `pressed`, `name`, `value`, `size`, `icon`, `disabled` | `ry:change` `{pressed, value}` | | `<ry-modal>` | `id`, `title` | Trigger: `<ry-button modal="id">`. [Details](docs/components/modal.md) | | `<ry-drawer>` | `id`, `position="left\|right"`, `size` | Trigger: `<ry-button drawer="id">`. [Details](docs/components/drawer.md) | | `<ry-accordion>` || Container for accordion-items. [Details](docs/components/accordion.md) | | `<ry-accordion-item>` | `title`, `open` | Collapsible section | | `<ry-tabs>` || Children: `<ry-tab title="..." active>`. [Details](docs/components/tabs.md) | | `<ry-dropdown>` || [Details](docs/components/dropdown.md) | | `<ry-select>` | `placeholder`, `name`, `value`, `disabled` | `ry:change` `{value}`. Children: `<ry-option>` | | `<ry-combobox>` | `placeholder`, `name`, `value`, `disabled` | `ry:change` `{value, label}`, `ry:input` — searchable dropdown | | `<ry-switch>` | `checked`, `disabled`, `name` | `ry:change` `{value, label}` — value is `"true"`/`"false"` string | | `<ry-tooltip>` | `content`, `position` | [Details](docs/components/tooltip.md) | | `<ry-toast>` || `RyToast.success()`, `.error()`, `.warning()`, `.info()`. [Details](docs/components/toast.md) | | `<ry-slider>` | `min`, `max`, `step`, `value`, `color`, `disabled` | `ry:change` `{value}`. [Details](docs/components/slider.md) | | `<ry-knob>` | `min`, `max`, `step`, `value`, `color`, `size` | `ry:change` `{value}`. [Details](docs/components/knob.md) | | `<ry-number-select>` | `min`, `max`, `step`, `value`, `arrows`, `prefix`, `suffix` | `ry:change` `{value}`. [Details](docs/components/number-select.md) | | `<ry-color-picker>` | `value`, `format` | `ry:change` `{value}`. [Details](docs/components/color.md) | | `<ry-color-input>` | `value`, `format` | `ry:change` `{value}` | | `<ry-gradient-picker>` | `value` | `ry:change` `{value}` | | `<ry-tree>` | `data` (JSON) | `ry:select`, `ry:move`. [Details](docs/components/tree.md) | | `<ry-tag>` | `removable` | `ry:remove` | | `<ry-tag-input>` | `name`, `value`, `placeholder` | `ry:change` `{tags}` | | `<ry-carousel>` | `autoplay`, `interval` | `ry:change` `{index}` | | `<ry-theme-toggle>` | `themes="light,dark"` | Cycles through themes | | `<ry-theme-panel>` | `theme`, `mode` | Floating theme/mode selector. Persists to localStorage | | `<ry-testimonial>` | `stars`, `avatar`, `name`, `role` | Quote card. Plain text children = quote | ### Display Components | Component | Key Attributes | Description | |-----------|---------------|-------------| | `<ry-card>` | `interactive`, `href` | Card container. `interactive` adds click/keyboard. `href` navigates | | `<ry-badge>` | `variant="primary\|success\|warning\|danger\|accent"` | Pill badge. Custom: `style="--ry-badge-color: #8B5CF6"` | | `<ry-alert>` | `type="info\|success\|warning\|danger"` | Alert box | | `<ry-field>` | `label`, `error`, `hint` | Form field wrapper. [Details](docs/components/forms.md) | | `<ry-icon>` | `name` | SVG icon from registry | | `<ry-code>` | `language`, `title` | Syntax-highlighted code block | | `<ry-hero>` | `size="sm\|lg"`, `full-bleed`, `align="left"` | Marketing hero section | | `<ry-stat>` | `value`, `label`, `size="sm\|lg"` | Stat card | | `<ry-feature>` | `icon` | Feature card with icon | | `<ry-feature-grid>` | `cols="2\|3\|4"` | Responsive grid for feature cards | | `<ry-pricing>` || Container for pricing cards | | `<ry-pricing-card>` | `title`, `price`, `featured` | Pricing tier. `featured` scales up with bold border | | `<ry-heading>` | `size="sm\|lg"`, `align="center\|right"`, `divider`, `sub` | Section heading with optional subtitle | --- ## Patterns ### Grid ```html <!-- Fixed columns (auto-responsive: 3-6 → 2 at ≤1024px → 1 at ≤640px) --> <ry-grid cols="3">...</ry-grid> <!-- Explicit per-breakpoint --> <ry-grid cols="5" cols-md="3" cols-sm="1">...</ry-grid> <!-- Fluid auto-fit --> <ry-grid cols="auto-fit">...</ry-grid> <ry-grid cols="auto-fit" style="--ry-grid-min: 240px">...</ry-grid> ``` ### Split Layout ```html <ry-split resizable persist="sidebar" style="--ry-split-width: 400px"> <div>Main content</div> <div>Resizable sidebar — drag, arrow keys, double-click to reset</div> </ry-split> ``` CSS vars: `--ry-split-width`, `--ry-split-min-width`, `--ry-split-max-width` Keyboard: Arrow (±10px), Shift+Arrow (±50px), Home/End, double-click reset Event: `ry:resize` `{ width }` ### Forms ```html <ry-field label="Email" hint="We'll never share your email"> <input type="email" placeholder="you@example.com"> </ry-field> <ry-field label="Password" error="Must be at least 8 characters"> <input type="password"> </ry-field> ``` Error hides hint automatically. Set `error=""` to clear. ### Button Variants ```html <ry-button>Default</ry-button> <ry-button variant="primary">Primary</ry-button> <ry-button variant="secondary">Secondary</ry-button> <ry-button variant="outline">Outline</ry-button> <ry-button variant="ghost">Ghost</ry-button> <ry-button variant="danger">Danger</ry-button> <ry-button variant="accent">Accent</ry-button> <ry-button size="sm">Small</ry-button> <ry-button size="lg">Large</ry-button> ``` ### Modal & Drawer ```html <ry-button modal="confirm">Open Modal</ry-button> <ry-modal id="confirm" title="Confirm Action"> <p>Are you sure?</p> <ry-cluster> <ry-button variant="danger">Delete</ry-button> <ry-button variant="ghost">Cancel</ry-button> </ry-cluster> </ry-modal> <ry-button drawer="settings">Settings</ry-button> <ry-drawer id="settings" position="right" size="400px"> <h3>Settings</h3> </ry-drawer> ``` ### Nav Bar ```html <ry-header sticky> <ry-cluster> <ry-logo>MyApp</ry-logo> <ry-divider vertical></ry-divider> <ry-nav> <a href="/" aria-current="page">Home</a> <a href="/docs">Docs</a> </ry-nav> </ry-cluster> <ry-actions> <ry-button variant="ghost" size="sm">Login</ry-button> <ry-button size="sm">Sign Up</ry-button> </ry-actions> </ry-header> ``` ### Hero ```html <ry-hero> <h1>Build faster with ry-ui</h1> <p>Framework-agnostic components for any app.</p> <ry-cluster> <ry-button size="lg">Get Started</ry-button> <ry-button variant="outline" size="lg">View Docs</ry-button> </ry-cluster> </ry-hero> ``` ### Pricing ```html <ry-pricing> <ry-pricing-card title="Free" price="$0/mo"> <ul class="ry-check-list"> <li>3 projects</li> <li>Basic support</li> </ul> <ry-button variant="outline">Get Started</ry-button> </ry-pricing-card> <ry-pricing-card featured title="Pro" price="$19/mo"> <ul class="ry-check-list"> <li>Unlimited projects</li> <li>Priority support</li> </ul> <ry-button>Upgrade</ry-button> </ry-pricing-card> </ry-pricing> ``` ### Interactive Card Grid ```html <ry-grid cols="3"> <ry-card interactive href="/feature-a"> <h3>Feature A</h3> <p>Description</p> </ry-card> </ry-grid> ``` ### Dropdown Menu ```html <ry-dropdown> <ry-button>Actions</ry-button> <ry-menu> <ry-menu-item>Edit</ry-menu-item> <ry-menu-item>Duplicate</ry-menu-item> <ry-menu-item>Delete</ry-menu-item> </ry-menu> </ry-dropdown> ``` --- ## Events All events prefixed with `ry:`: ```javascript element.addEventListener('ry:change', (e) => console.log(e.detail)); element.addEventListener('ry:open', () => {}); element.addEventListener('ry:close', () => {}); element.addEventListener('ry:click', () => {}); ``` ### Programmatic Control ```javascript document.querySelector('ry-modal').open(); document.querySelector('ry-modal').close(); document.querySelector('ry-drawer').toggle(); document.querySelector('ry-select').value = 'new-value'; ``` --- ## CSS Token System All visual properties use CSS custom properties. Override in your own CSS to customize. ### Colors | Token | Purpose | |-------|---------| | `--ry-color-primary` / `-hover` / `-active` | Primary action color | | `--ry-color-secondary` / `-hover` / `-active` | Secondary muted color | | `--ry-color-accent` / `-hover` / `-active` | Accent/highlight color | | `--ry-color-success` | Green for positive states | | `--ry-color-warning` | Yellow/orange for caution | | `--ry-color-danger` / `-hover` | Red for destructive actions | | `--ry-color-info` | Blue for informational | | `--ry-color-text` / `-muted` / `-inverse` | Text colors | | `--ry-color-bg` / `-subtle` / `-muted` | Background colors | | `--ry-color-border` / `-strong` | Border colors | | `--ry-color-overlay` | Modal/drawer backdrop | Each color also has `-bg` and `-text` variants for alert/badge backgrounds. ### Spacing `--ry-space-{0,1,2,3,4,5,6,8,10,12,16,20}` — 0 to 5rem ### Typography | Token | Value | |-------|-------| | `--ry-font-sans` | system-ui stack | | `--ry-font-mono` | ui-monospace stack | | `--ry-text-{xs,sm,base,lg,xl,2xl,3xl,4xl}` | 0.75rem to 2.25rem | | `--ry-font-{normal,medium,semibold,bold}` | 400 to 700 | ### Borders & Shadows | Token | Value | |-------|-------| | `--ry-radius-{none,sm,md,lg,xl,2xl,full}` | 0 to 9999px | | `--ry-shadow-{sm,md,lg,xl}` | Elevation shadows | | `--ry-border-width` | 1px | ### Transitions | Token | Value | |-------|-------| | `--ry-duration-{fast,normal,slow}` | 100ms, 200ms, 300ms | | `--ry-ease` / `-in` / `-out` | Cubic bezier easing | ### Z-Index | Token | Value | |-------|-------| | `--ry-z-dropdown` | 1000 | | `--ry-z-sticky` | 1020 | | `--ry-z-modal-backdrop` | 1040 | | `--ry-z-modal` | 1050 | | `--ry-z-tooltip` | 1070 | | `--ry-z-toast` | 1080 | --- ## Theming Three CSS layers, loaded in order: 1. **ry-tokens.css** — CSS custom properties (colors, spacing, etc.) 2. **ry-structure.css** — Pure layout (no colors) + Preflight reset via `data-ry-reset` 3. **ry-theme.css** — All visual styling (colors, shadows, borders) ### Preflight Reset Add `data-ry-reset` to `<body>` to normalize all elements (Tailwind Preflight-equivalent): ```html <body data-ry-reset> ``` Resets box-sizing, margins, padding, form element inheritance, media elements, lists, tables. Without it, only ry-* components are reset. ### Custom Theme Load structure-only and bring your own: ```html <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ryanhelsing/ry-ui/dist/css/ry-structure.css"> <link rel="stylesheet" href="your-tokens.css"> <link rel="stylesheet" href="your-theme.css"> ``` ### Override Tokens No build step needed: ```css :root { --ry-color-primary: oklch(0.541 0.218 293); --ry-color-primary-hover: oklch(0.491 0.234 292); --ry-radius-md: 0; } ``` ### Themes & Modes Theme and mode are independent: ```html <html data-ry-theme="ocean" data-ry-mode="dark"> ``` Themes: `default`, `ocean`, `none` (structure only) Modes: `auto` (OS preference), `light`, `dark` Use `<ry-theme-panel>` for an interactive floating selector. --- ## TypeScript ```ts import { RyElement, RyButton, RyToast } from '@ryanhelsing/ry-ui'; RyToast.success('Saved!'); document.querySelector('ry-select')?.addEventListener('ry:change', (e: CustomEvent) => { console.log(e.detail.value); }); // Extend components class MyWidget extends RyElement { setup() { this.on(this, 'click', () => this.emit('activate')); } } ``` ## Icon Registry Built-in: `settings`, `heart`, `star`, `chevron-up`, `chevron-down`, `chevron-left`, `chevron-right`, `check`, `x`, `plus`, `minus`, `search`, `sun`, `moon`, `copy`, `trash`, `edit`, `eye`, `folder`, `file`, `drag` ```ts import { registerIcon, registerIcons } from '@ryanhelsing/ry-ui'; registerIcon('custom', '<svg>...</svg>'); registerIcons({ 'app-logo': '<svg>...</svg>' }); ``` ## Vendoring Copy into your project instead of using CDN: ```bash npm pack @ryanhelsing/ry-ui && tar -xf ryanhelsing-ry-ui-*.tgz cp -r package/dist ./vendor/ry-ui && rm -rf package ryanhelsing-ry-ui-*.tgz ``` ```html <link rel="stylesheet" href="/vendor/ry-ui/css/ry-ui.css"> <script type="module" src="/vendor/ry-ui/ry-ui.js"></script> ``` --- ## Detailed Docs Per-component docs with full attributes, events, and examples: | Doc | Components | |-----|-----------| | [layout](docs/components/layout.md) | page, header, main, footer, section, grid, stack, cluster, split, center, card, nav, divider | | [button](docs/components/button.md) | button, toggle-button | | [accordion](docs/components/accordion.md) | accordion, accordion-item | | [tabs](docs/components/tabs.md) | tabs, tab | | [modal](docs/components/modal.md) | modal | | [drawer](docs/components/drawer.md) | drawer | | [dropdown](docs/components/dropdown.md) | dropdown, menu, menu-item | | [tooltip](docs/components/tooltip.md) | tooltip | | [toast](docs/components/toast.md) | toast | | [forms](docs/components/forms.md) | field, select, switch | | [slider](docs/components/slider.md) | slider | | [knob](docs/components/knob.md) | knob | | [number-select](docs/components/number-select.md) | number-select | | [color](docs/components/color.md) | color-picker, color-input, gradient-picker | | [tree](docs/components/tree.md) | tree | | [display](docs/components/display.md) | badge, alert, icon, code | | [theme-toggle](docs/components/theme-toggle.md) | theme-toggle | | [theming](docs/theming.md) | tokens, custom themes, structure-only loading | ## AI-Friendly This package includes a `.claude/skills/ry-ui-builder` skill so Claude Code can build with these components automatically. The detailed docs above serve as the complete agent reference. ## License MIT