experiments in a post-browser web
10
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(components): add Phase 1 UI component library with Lit.js

+1564 -1
+353
app/components/README.md
··· 1 + # Peek UI Components 2 + 3 + A lightweight, themeable web component library built on [Lit.js](https://lit.dev/). Components use Shadow DOM for style encapsulation and CSS custom properties for theming. 4 + 5 + ## Quick Start 6 + 7 + ```html 8 + <!-- Import all components --> 9 + <script type="module" src="peek://app/components/index.js"></script> 10 + 11 + <!-- Or import individually --> 12 + <script type="module" src="peek://app/components/peek-button.js"></script> 13 + ``` 14 + 15 + ```html 16 + <!-- Use components --> 17 + <peek-button variant="primary">Save</peek-button> 18 + <peek-card> 19 + <span slot="header">Card Title</span> 20 + <p>Card content here</p> 21 + </peek-card> 22 + ``` 23 + 24 + ## Theming 25 + 26 + Components inherit from the Peek theme system (`peek://theme/variables.css`). Override component tokens via CSS custom properties: 27 + 28 + ```css 29 + /* Global theming */ 30 + :root { 31 + --theme-accent: #ff6b35; 32 + --peek-radius-md: 8px; 33 + } 34 + 35 + /* Component-specific overrides */ 36 + peek-button { 37 + --peek-btn-bg: #333; 38 + --peek-btn-text: #fff; 39 + } 40 + ``` 41 + 42 + ### Design Tokens 43 + 44 + All components share these tokens: 45 + 46 + | Token | Default | Description | 47 + |-------|---------|-------------| 48 + | `--peek-space-xs` | 4px | Extra small spacing | 49 + | `--peek-space-sm` | 8px | Small spacing | 50 + | `--peek-space-md` | 12px | Medium spacing | 51 + | `--peek-space-lg` | 16px | Large spacing | 52 + | `--peek-space-xl` | 24px | Extra large spacing | 53 + | `--peek-radius-sm` | 4px | Small border radius | 54 + | `--peek-radius-md` | 6px | Medium border radius | 55 + | `--peek-radius-lg` | 8px | Large border radius | 56 + | `--peek-font-sm` | 13px | Small font size | 57 + | `--peek-font-md` | 14px | Medium font size | 58 + | `--peek-font-lg` | 16px | Large font size | 59 + | `--peek-shadow-sm` | ... | Small shadow | 60 + | `--peek-shadow-md` | ... | Medium shadow | 61 + | `--peek-shadow-lg` | ... | Large shadow | 62 + | `--peek-transition-fast` | 100ms ease | Fast transitions | 63 + | `--peek-transition-normal` | 150ms ease | Normal transitions | 64 + 65 + --- 66 + 67 + ## Components 68 + 69 + ### `<peek-button>` 70 + 71 + A themeable button built on native `<button>` for accessibility. 72 + 73 + #### Properties 74 + 75 + | Property | Type | Default | Description | 76 + |----------|------|---------|-------------| 77 + | `variant` | `'primary' \| 'secondary' \| 'ghost' \| 'danger'` | `'secondary'` | Button style variant | 78 + | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Button size | 79 + | `disabled` | `boolean` | `false` | Disable the button | 80 + | `loading` | `boolean` | `false` | Show loading spinner | 81 + | `type` | `'button' \| 'submit' \| 'reset'` | `'button'` | Button type | 82 + 83 + #### Slots 84 + 85 + | Slot | Description | 86 + |------|-------------| 87 + | (default) | Button label content | 88 + | `prefix` | Content before label (e.g., icon) | 89 + | `suffix` | Content after label (e.g., icon) | 90 + 91 + #### CSS Parts 92 + 93 + | Part | Description | 94 + |------|-------------| 95 + | `button` | The native button element | 96 + 97 + #### CSS Custom Properties 98 + 99 + | Property | Description | 100 + |----------|-------------| 101 + | `--peek-btn-bg` | Button background | 102 + | `--peek-btn-text` | Button text color | 103 + | `--peek-btn-border` | Button border color | 104 + | `--peek-btn-hover-bg` | Hover background | 105 + | `--peek-btn-active-bg` | Active/pressed background | 106 + 107 + #### Examples 108 + 109 + ```html 110 + <!-- Variants --> 111 + <peek-button variant="primary">Primary</peek-button> 112 + <peek-button variant="secondary">Secondary</peek-button> 113 + <peek-button variant="ghost">Ghost</peek-button> 114 + <peek-button variant="danger">Delete</peek-button> 115 + 116 + <!-- Sizes --> 117 + <peek-button size="sm">Small</peek-button> 118 + <peek-button size="md">Medium</peek-button> 119 + <peek-button size="lg">Large</peek-button> 120 + 121 + <!-- States --> 122 + <peek-button disabled>Disabled</peek-button> 123 + <peek-button loading>Loading</peek-button> 124 + 125 + <!-- With icons --> 126 + <peek-button> 127 + <svg slot="prefix">...</svg> 128 + Save 129 + </peek-button> 130 + ``` 131 + 132 + --- 133 + 134 + ### `<peek-card>` 135 + 136 + A flexible card container with header, body, and footer slots. 137 + 138 + #### Properties 139 + 140 + | Property | Type | Default | Description | 141 + |----------|------|---------|-------------| 142 + | `interactive` | `boolean` | `false` | Make card clickable/focusable | 143 + | `selected` | `boolean` | `false` | Visual selected state | 144 + | `elevated` | `boolean` | `false` | Add elevation shadow | 145 + | `bordered` | `boolean` | `true` | Show border | 146 + 147 + #### Slots 148 + 149 + | Slot | Description | 150 + |------|-------------| 151 + | (default) | Card body content | 152 + | `header` | Card header content | 153 + | `footer` | Card footer content | 154 + | `media` | Media content (images), displayed edge-to-edge | 155 + 156 + #### CSS Parts 157 + 158 + | Part | Description | 159 + |------|-------------| 160 + | `card` | The card container | 161 + | `header` | Header section | 162 + | `body` | Body section | 163 + | `footer` | Footer section | 164 + | `media` | Media section | 165 + 166 + #### CSS Custom Properties 167 + 168 + | Property | Description | 169 + |----------|-------------| 170 + | `--peek-card-bg` | Card background | 171 + | `--peek-card-border` | Card border color | 172 + | `--peek-card-radius` | Card border radius | 173 + | `--peek-card-padding` | Content padding | 174 + | `--peek-card-gap` | Gap between sections | 175 + 176 + #### Events 177 + 178 + | Event | Detail | Description | 179 + |-------|--------|-------------| 180 + | `card-click` | `{ originalEvent }` | Fired when interactive card is clicked | 181 + 182 + #### Examples 183 + 184 + ```html 185 + <!-- Basic card --> 186 + <peek-card> 187 + <span slot="header">Card Title</span> 188 + <p>Card content goes here.</p> 189 + <span slot="footer">Updated 2 hours ago</span> 190 + </peek-card> 191 + 192 + <!-- Interactive card --> 193 + <peek-card interactive elevated> 194 + <img slot="media" src="image.jpg" alt="Preview"> 195 + <h3 slot="header">Clickable Card</h3> 196 + <p>Click anywhere on this card.</p> 197 + </peek-card> 198 + 199 + <!-- Selected card --> 200 + <peek-card selected> 201 + <span slot="header">Selected Item</span> 202 + <p>This card is in selected state.</p> 203 + </peek-card> 204 + ``` 205 + 206 + --- 207 + 208 + ### `<peek-list>` and `<peek-list-item>` 209 + 210 + A keyboard-navigable list with selection support. 211 + 212 + #### `<peek-list>` Properties 213 + 214 + | Property | Type | Default | Description | 215 + |----------|------|---------|-------------| 216 + | `selection` | `'none' \| 'single' \| 'multiple'` | `'none'` | Selection mode | 217 + | `selected-index` | `number` | `-1` | Selected index (single mode) | 218 + | `selectedIndices` | `number[]` | `[]` | Selected indices (multiple mode) | 219 + | `wrap` | `boolean` | `false` | Wrap navigation at ends | 220 + 221 + #### `<peek-list-item>` Properties 222 + 223 + | Property | Type | Default | Description | 224 + |----------|------|---------|-------------| 225 + | `selected` | `boolean` | `false` | Whether item is selected | 226 + | `disabled` | `boolean` | `false` | Whether item is disabled | 227 + | `value` | `any` | `null` | Optional value for this item | 228 + 229 + #### Slots 230 + 231 + **`<peek-list>`:** 232 + | Slot | Description | 233 + |------|-------------| 234 + | (default) | `<peek-list-item>` elements | 235 + 236 + **`<peek-list-item>`:** 237 + | Slot | Description | 238 + |------|-------------| 239 + | (default) | Item content | 240 + | `prefix` | Content before item (e.g., icon) | 241 + | `suffix` | Content after item (e.g., badge) | 242 + 243 + #### CSS Parts 244 + 245 + | Part | Element | Description | 246 + |------|---------|-------------| 247 + | `list` | `<peek-list>` | The list container | 248 + | `item` | `<peek-list-item>` | Individual item | 249 + 250 + #### CSS Custom Properties 251 + 252 + | Property | Description | 253 + |----------|-------------| 254 + | `--peek-list-gap` | Gap between items | 255 + | `--peek-list-padding` | List container padding | 256 + | `--peek-list-item-bg` | Item background | 257 + | `--peek-list-item-hover-bg` | Item hover background | 258 + | `--peek-list-item-selected-bg` | Selected item background | 259 + | `--peek-list-item-padding-x` | Item horizontal padding | 260 + | `--peek-list-item-padding-y` | Item vertical padding | 261 + 262 + #### Events 263 + 264 + | Event | Detail | Description | 265 + |-------|--------|-------------| 266 + | `selection-change` | `{ selectedIndex, selectedIndices, item }` | Selection changed | 267 + | `item-activate` | `{ index, item }` | Item activated (Enter/click) | 268 + 269 + #### Keyboard Navigation 270 + 271 + | Key | Action | 272 + |-----|--------| 273 + | `ArrowDown` / `j` | Move to next item | 274 + | `ArrowUp` / `k` | Move to previous item | 275 + | `Home` / `gg` | Move to first item | 276 + | `End` / `G` | Move to last item | 277 + | `Enter` / `Space` | Activate/select focused item | 278 + | `Escape` | Clear focus | 279 + 280 + #### Examples 281 + 282 + ```html 283 + <!-- Single selection --> 284 + <peek-list selection="single" @selection-change=${handleChange}> 285 + <peek-list-item value="1">Option 1</peek-list-item> 286 + <peek-list-item value="2">Option 2</peek-list-item> 287 + <peek-list-item value="3">Option 3</peek-list-item> 288 + </peek-list> 289 + 290 + <!-- Multiple selection --> 291 + <peek-list selection="multiple" wrap> 292 + <peek-list-item> 293 + <svg slot="prefix">...</svg> 294 + Item with icon 295 + <span slot="suffix">Badge</span> 296 + </peek-list-item> 297 + <peek-list-item disabled>Disabled item</peek-list-item> 298 + </peek-list> 299 + 300 + <!-- Navigation list (no selection) --> 301 + <peek-list @item-activate=${navigate}> 302 + <peek-list-item value="/home">Home</peek-list-item> 303 + <peek-list-item value="/settings">Settings</peek-list-item> 304 + </peek-list> 305 + ``` 306 + 307 + --- 308 + 309 + ## Extending Components 310 + 311 + Create custom components by extending `PeekElement`: 312 + 313 + ```javascript 314 + import { html, css } from 'lit'; 315 + import { PeekElement, sharedStyles } from 'peek://app/components/base.js'; 316 + 317 + class MyComponent extends PeekElement { 318 + static styles = [ 319 + sharedStyles, 320 + css` 321 + :host { 322 + display: block; 323 + } 324 + /* Component styles */ 325 + ` 326 + ]; 327 + 328 + render() { 329 + return html`<div>My component</div>`; 330 + } 331 + } 332 + 333 + customElements.define('my-component', MyComponent); 334 + ``` 335 + 336 + ### PeekElement Utilities 337 + 338 + | Method | Description | 339 + |--------|-------------| 340 + | `emit(name, detail, options)` | Dispatch a composed custom event | 341 + | `classMap(classes)` | Generate class string from condition map | 342 + 343 + --- 344 + 345 + ## Browser Support 346 + 347 + Components use modern CSS features: 348 + - CSS custom properties 349 + - `color-mix()` for color adjustments 350 + - `:focus-visible` for keyboard focus styles 351 + - CSS Grid and Flexbox 352 + 353 + Supported in all modern browsers (Chrome, Firefox, Safari, Edge).
+139
app/components/base.js
··· 1 + /** 2 + * Base class for Peek UI components 3 + * 4 + * Provides: 5 + * - Shared styles (design tokens, theme integration) 6 + * - Common utilities 7 + * - Event helpers for composed custom events 8 + */ 9 + 10 + import { LitElement, css } from 'lit'; 11 + 12 + /** 13 + * Shared styles that all Peek components inherit. 14 + * Includes design tokens and theme variable integration. 15 + */ 16 + export const sharedStyles = css` 17 + :host { 18 + /* Spacing Scale */ 19 + --peek-space-xs: 4px; 20 + --peek-space-sm: 8px; 21 + --peek-space-md: 12px; 22 + --peek-space-lg: 16px; 23 + --peek-space-xl: 24px; 24 + --peek-space-2xl: 32px; 25 + 26 + /* Border Radius */ 27 + --peek-radius-sm: 4px; 28 + --peek-radius-md: 6px; 29 + --peek-radius-lg: 8px; 30 + --peek-radius-xl: 12px; 31 + --peek-radius-full: 9999px; 32 + 33 + /* Font Sizes */ 34 + --peek-font-xs: 11px; 35 + --peek-font-sm: 13px; 36 + --peek-font-md: 14px; 37 + --peek-font-lg: 16px; 38 + --peek-font-xl: 18px; 39 + --peek-font-2xl: 24px; 40 + 41 + /* Font Weights */ 42 + --peek-font-normal: 400; 43 + --peek-font-medium: 500; 44 + --peek-font-semibold: 600; 45 + --peek-font-bold: 700; 46 + 47 + /* Line Heights */ 48 + --peek-leading-tight: 1.25; 49 + --peek-leading-normal: 1.5; 50 + --peek-leading-relaxed: 1.75; 51 + 52 + /* Shadows */ 53 + --peek-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1); 54 + --peek-shadow-md: 0 2px 4px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.08); 55 + --peek-shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1); 56 + --peek-shadow-xl: 0 8px 24px rgba(0, 0, 0, 0.2), 0 4px 8px rgba(0, 0, 0, 0.12); 57 + 58 + /* Transitions */ 59 + --peek-transition-fast: 100ms ease; 60 + --peek-transition-normal: 150ms ease; 61 + --peek-transition-slow: 250ms ease; 62 + 63 + /* Focus Ring */ 64 + --peek-focus-ring: 0 0 0 2px var(--theme-accent, #007aff); 65 + 66 + /* Button */ 67 + --peek-btn-height-sm: 28px; 68 + --peek-btn-height-md: 36px; 69 + --peek-btn-height-lg: 44px; 70 + --peek-btn-padding-x: var(--peek-space-md); 71 + --peek-btn-padding-x-sm: var(--peek-space-sm); 72 + --peek-btn-padding-x-lg: var(--peek-space-lg); 73 + 74 + /* Card */ 75 + --peek-card-padding: var(--peek-space-lg); 76 + --peek-card-gap: var(--peek-space-md); 77 + --peek-card-radius: var(--peek-radius-lg); 78 + 79 + /* List */ 80 + --peek-list-item-padding-x: var(--peek-space-md); 81 + --peek-list-item-padding-y: var(--peek-space-sm); 82 + --peek-list-item-gap: var(--peek-space-xs); 83 + 84 + /* Theme Integration */ 85 + font-family: var(--theme-font-sans, system-ui, -apple-system, BlinkMacSystemFont, sans-serif); 86 + color: var(--theme-text, #333); 87 + box-sizing: border-box; 88 + } 89 + 90 + :host *, 91 + :host *::before, 92 + :host *::after { 93 + box-sizing: inherit; 94 + } 95 + 96 + :host([hidden]) { 97 + display: none !important; 98 + } 99 + `; 100 + 101 + /** 102 + * Base class for all Peek UI components. 103 + * Extends LitElement with shared styles and utilities. 104 + */ 105 + export class PeekElement extends LitElement { 106 + static styles = [sharedStyles]; 107 + 108 + /** 109 + * Emit a custom event that crosses shadow DOM boundaries. 110 + * @param {string} name - Event name 111 + * @param {*} detail - Event detail payload 112 + * @param {CustomEventInit} [options] - Additional event options 113 + * @returns {boolean} - Whether the event was cancelled 114 + */ 115 + emit(name, detail, options = {}) { 116 + const event = new CustomEvent(name, { 117 + bubbles: true, 118 + composed: true, 119 + cancelable: true, 120 + detail, 121 + ...options 122 + }); 123 + return this.dispatchEvent(event); 124 + } 125 + 126 + /** 127 + * Helper to conditionally join class names. 128 + * @param {Object<string, boolean>} classes - Map of class names to conditions 129 + * @returns {string} - Space-separated class string 130 + */ 131 + classMap(classes) { 132 + return Object.entries(classes) 133 + .filter(([_, condition]) => condition) 134 + .map(([name]) => name) 135 + .join(' '); 136 + } 137 + } 138 + 139 + export default PeekElement;
+34
app/components/index.js
··· 1 + /** 2 + * Peek UI Components 3 + * 4 + * A lightweight, themeable web component library built on Lit.js. 5 + * Components use Shadow DOM for style encapsulation and CSS custom 6 + * properties for theming integration. 7 + * 8 + * Usage: 9 + * import 'peek://app/components/index.js'; 10 + * 11 + * Or import individual components: 12 + * import 'peek://app/components/peek-button.js'; 13 + * import 'peek://app/components/peek-card.js'; 14 + * import 'peek://app/components/peek-list.js'; 15 + * 16 + * Components automatically register with the custom elements registry. 17 + * 18 + * Theming: 19 + * Components inherit theme variables from peek://theme/variables.css 20 + * Override component tokens via CSS custom properties on :root or component. 21 + */ 22 + 23 + // Base utilities 24 + export { PeekElement, sharedStyles } from './base.js'; 25 + 26 + // Components 27 + export { PeekButton } from './peek-button.js'; 28 + export { PeekCard } from './peek-card.js'; 29 + export { PeekList, PeekListItem } from './peek-list.js'; 30 + 31 + // Side-effect imports to register all components 32 + import './peek-button.js'; 33 + import './peek-card.js'; 34 + import './peek-list.js';
+244
app/components/peek-button.js
··· 1 + /** 2 + * Peek Button Component 3 + * 4 + * A themeable button component built on native <button> for accessibility. 5 + * 6 + * @element peek-button 7 + * 8 + * @prop {string} variant - Button style: 'primary' | 'secondary' | 'ghost' | 'danger' 9 + * @prop {string} size - Button size: 'sm' | 'md' | 'lg' 10 + * @prop {boolean} disabled - Whether the button is disabled 11 + * @prop {boolean} loading - Show loading state 12 + * @prop {string} type - Button type: 'button' | 'submit' | 'reset' 13 + * 14 + * @slot - Default slot for button content 15 + * @slot prefix - Content before the main label (e.g., icon) 16 + * @slot suffix - Content after the main label (e.g., icon) 17 + * 18 + * @csspart button - The native button element 19 + * 20 + * @cssprop --peek-btn-bg - Button background color 21 + * @cssprop --peek-btn-text - Button text color 22 + * @cssprop --peek-btn-border - Button border color 23 + * @cssprop --peek-btn-hover-bg - Background on hover 24 + * @cssprop --peek-btn-active-bg - Background on active/press 25 + * 26 + * @fires click - When button is clicked (native event) 27 + * 28 + * @example 29 + * <peek-button variant="primary">Save</peek-button> 30 + * <peek-button variant="secondary" size="sm">Cancel</peek-button> 31 + * <peek-button variant="ghost" disabled>Disabled</peek-button> 32 + */ 33 + 34 + import { html, css } from 'lit'; 35 + import { PeekElement, sharedStyles } from './base.js'; 36 + 37 + export class PeekButton extends PeekElement { 38 + static properties = { 39 + variant: { type: String, reflect: true }, 40 + size: { type: String, reflect: true }, 41 + disabled: { type: Boolean, reflect: true }, 42 + loading: { type: Boolean, reflect: true }, 43 + type: { type: String } 44 + }; 45 + 46 + static styles = [ 47 + sharedStyles, 48 + css` 49 + :host { 50 + display: inline-block; 51 + } 52 + 53 + .button { 54 + display: inline-flex; 55 + align-items: center; 56 + justify-content: center; 57 + gap: var(--peek-space-sm); 58 + border: 1px solid transparent; 59 + border-radius: var(--peek-radius-md); 60 + font-family: inherit; 61 + font-size: var(--peek-font-md); 62 + font-weight: var(--peek-font-medium); 63 + line-height: var(--peek-leading-tight); 64 + cursor: pointer; 65 + transition: background var(--peek-transition-fast), 66 + border-color var(--peek-transition-fast), 67 + color var(--peek-transition-fast), 68 + box-shadow var(--peek-transition-fast); 69 + user-select: none; 70 + white-space: nowrap; 71 + 72 + /* Default size (md) */ 73 + height: var(--peek-btn-height-md); 74 + padding: 0 var(--peek-btn-padding-x); 75 + 76 + /* Default variant colors (secondary) */ 77 + --_btn-bg: var(--peek-btn-bg, var(--theme-bg-secondary, #fff)); 78 + --_btn-text: var(--peek-btn-text, var(--theme-text, #333)); 79 + --_btn-border: var(--peek-btn-border, var(--theme-border, #e0e0e0)); 80 + --_btn-hover-bg: var(--peek-btn-hover-bg, var(--theme-bg-tertiary, #f0f0f0)); 81 + --_btn-active-bg: var(--peek-btn-active-bg, var(--theme-border, #e0e0e0)); 82 + 83 + background: var(--_btn-bg); 84 + color: var(--_btn-text); 85 + border-color: var(--_btn-border); 86 + } 87 + 88 + .button:hover:not(:disabled) { 89 + background: var(--_btn-hover-bg); 90 + } 91 + 92 + .button:active:not(:disabled) { 93 + background: var(--_btn-active-bg); 94 + } 95 + 96 + .button:focus-visible { 97 + outline: none; 98 + box-shadow: var(--peek-focus-ring); 99 + } 100 + 101 + .button:disabled { 102 + cursor: not-allowed; 103 + opacity: 0.5; 104 + } 105 + 106 + /* Size variants */ 107 + :host([size="sm"]) .button { 108 + height: var(--peek-btn-height-sm); 109 + padding: 0 var(--peek-btn-padding-x-sm); 110 + font-size: var(--peek-font-sm); 111 + border-radius: var(--peek-radius-sm); 112 + } 113 + 114 + :host([size="lg"]) .button { 115 + height: var(--peek-btn-height-lg); 116 + padding: 0 var(--peek-btn-padding-x-lg); 117 + font-size: var(--peek-font-lg); 118 + border-radius: var(--peek-radius-lg); 119 + } 120 + 121 + /* Primary variant */ 122 + :host([variant="primary"]) .button { 123 + --_btn-bg: var(--peek-btn-bg, var(--theme-accent, #007aff)); 124 + --_btn-text: var(--peek-btn-text, #fff); 125 + --_btn-border: var(--peek-btn-border, var(--theme-accent, #007aff)); 126 + --_btn-hover-bg: var(--peek-btn-hover-bg, color-mix(in srgb, var(--theme-accent, #007aff) 85%, #000)); 127 + --_btn-active-bg: var(--peek-btn-active-bg, color-mix(in srgb, var(--theme-accent, #007aff) 75%, #000)); 128 + } 129 + 130 + /* Ghost variant */ 131 + :host([variant="ghost"]) .button { 132 + --_btn-bg: transparent; 133 + --_btn-text: var(--peek-btn-text, var(--theme-text, #333)); 134 + --_btn-border: transparent; 135 + --_btn-hover-bg: var(--peek-btn-hover-bg, var(--theme-bg-tertiary, rgba(0, 0, 0, 0.05))); 136 + --_btn-active-bg: var(--peek-btn-active-bg, var(--theme-border, rgba(0, 0, 0, 0.1))); 137 + } 138 + 139 + /* Danger variant */ 140 + :host([variant="danger"]) .button { 141 + --_btn-bg: var(--peek-btn-bg, var(--theme-danger, #ff3b30)); 142 + --_btn-text: var(--peek-btn-text, #fff); 143 + --_btn-border: var(--peek-btn-border, var(--theme-danger, #ff3b30)); 144 + --_btn-hover-bg: var(--peek-btn-hover-bg, color-mix(in srgb, var(--theme-danger, #ff3b30) 85%, #000)); 145 + --_btn-active-bg: var(--peek-btn-active-bg, color-mix(in srgb, var(--theme-danger, #ff3b30) 75%, #000)); 146 + } 147 + 148 + /* Loading state */ 149 + :host([loading]) .button { 150 + pointer-events: none; 151 + position: relative; 152 + } 153 + 154 + :host([loading]) .button-content { 155 + visibility: hidden; 156 + } 157 + 158 + .spinner { 159 + display: none; 160 + position: absolute; 161 + width: 16px; 162 + height: 16px; 163 + border: 2px solid currentColor; 164 + border-right-color: transparent; 165 + border-radius: 50%; 166 + animation: spin 0.6s linear infinite; 167 + } 168 + 169 + :host([loading]) .spinner { 170 + display: block; 171 + } 172 + 173 + @keyframes spin { 174 + to { transform: rotate(360deg); } 175 + } 176 + 177 + /* Slot styling */ 178 + .button-content { 179 + display: inline-flex; 180 + align-items: center; 181 + gap: var(--peek-space-sm); 182 + } 183 + 184 + ::slotted([slot="prefix"]), 185 + ::slotted([slot="suffix"]) { 186 + display: flex; 187 + align-items: center; 188 + } 189 + ` 190 + ]; 191 + 192 + constructor() { 193 + super(); 194 + this.variant = 'secondary'; 195 + this.size = 'md'; 196 + this.disabled = false; 197 + this.loading = false; 198 + this.type = 'button'; 199 + } 200 + 201 + render() { 202 + return html` 203 + <button 204 + part="button" 205 + class="button" 206 + type=${this.type} 207 + ?disabled=${this.disabled || this.loading} 208 + aria-busy=${this.loading ? 'true' : 'false'} 209 + > 210 + <span class="spinner" aria-hidden="true"></span> 211 + <span class="button-content"> 212 + <slot name="prefix"></slot> 213 + <slot></slot> 214 + <slot name="suffix"></slot> 215 + </span> 216 + </button> 217 + `; 218 + } 219 + 220 + /** 221 + * Focus the button element 222 + */ 223 + focus() { 224 + this.shadowRoot?.querySelector('button')?.focus(); 225 + } 226 + 227 + /** 228 + * Blur the button element 229 + */ 230 + blur() { 231 + this.shadowRoot?.querySelector('button')?.blur(); 232 + } 233 + 234 + /** 235 + * Simulate a click on the button 236 + */ 237 + click() { 238 + this.shadowRoot?.querySelector('button')?.click(); 239 + } 240 + } 241 + 242 + customElements.define('peek-button', PeekButton); 243 + 244 + export default PeekButton;
+254
app/components/peek-card.js
··· 1 + /** 2 + * Peek Card Component 3 + * 4 + * A flexible card container with header, body, and footer slots. 5 + * Supports interactive (clickable) and static modes. 6 + * 7 + * @element peek-card 8 + * 9 + * @prop {boolean} interactive - Make card clickable/focusable 10 + * @prop {boolean} selected - Visual selected state 11 + * @prop {boolean} elevated - Add elevation shadow 12 + * @prop {boolean} bordered - Show border (default true) 13 + * 14 + * @slot - Default slot for card body content 15 + * @slot header - Card header content 16 + * @slot footer - Card footer content 17 + * @slot media - Media content (images, etc.) displayed edge-to-edge 18 + * 19 + * @csspart card - The card container 20 + * @csspart header - The header section 21 + * @csspart body - The body section 22 + * @csspart footer - The footer section 23 + * @csspart media - The media section 24 + * 25 + * @cssprop --peek-card-bg - Card background color 26 + * @cssprop --peek-card-border - Card border color 27 + * @cssprop --peek-card-radius - Card border radius 28 + * @cssprop --peek-card-padding - Card content padding 29 + * @cssprop --peek-card-gap - Gap between card sections 30 + * 31 + * @fires card-click - When interactive card is clicked 32 + * 33 + * @example 34 + * <peek-card> 35 + * <span slot="header">Title</span> 36 + * <p>Card content goes here</p> 37 + * <span slot="footer">Footer text</span> 38 + * </peek-card> 39 + * 40 + * @example 41 + * <peek-card interactive elevated> 42 + * <img slot="media" src="image.jpg" alt="Card image"> 43 + * <h3 slot="header">Clickable Card</h3> 44 + * <p>Click anywhere on this card</p> 45 + * </peek-card> 46 + */ 47 + 48 + import { html, css, nothing } from 'lit'; 49 + import { PeekElement, sharedStyles } from './base.js'; 50 + 51 + export class PeekCard extends PeekElement { 52 + static properties = { 53 + interactive: { type: Boolean, reflect: true }, 54 + selected: { type: Boolean, reflect: true }, 55 + elevated: { type: Boolean, reflect: true }, 56 + bordered: { type: Boolean, reflect: true } 57 + }; 58 + 59 + static styles = [ 60 + sharedStyles, 61 + css` 62 + :host { 63 + display: block; 64 + } 65 + 66 + .card { 67 + display: flex; 68 + flex-direction: column; 69 + background: var(--peek-card-bg, var(--theme-bg-secondary, #fff)); 70 + border-radius: var(--peek-card-radius, var(--peek-radius-lg)); 71 + overflow: hidden; 72 + transition: box-shadow var(--peek-transition-normal), 73 + border-color var(--peek-transition-normal), 74 + transform var(--peek-transition-fast); 75 + } 76 + 77 + /* Bordered (default) */ 78 + :host([bordered]) .card, 79 + :host(:not([bordered])) .card { 80 + border: 1px solid var(--peek-card-border, var(--theme-border, #e0e0e0)); 81 + } 82 + 83 + :host([bordered="false"]) .card { 84 + border: none; 85 + } 86 + 87 + /* Elevated */ 88 + :host([elevated]) .card { 89 + box-shadow: var(--peek-shadow-md); 90 + border-color: transparent; 91 + } 92 + 93 + /* Selected */ 94 + :host([selected]) .card { 95 + border-color: var(--theme-accent, #007aff); 96 + box-shadow: 0 0 0 1px var(--theme-accent, #007aff); 97 + } 98 + 99 + /* Interactive */ 100 + :host([interactive]) .card { 101 + cursor: pointer; 102 + } 103 + 104 + :host([interactive]) .card:hover { 105 + border-color: var(--theme-accent, #007aff); 106 + } 107 + 108 + :host([interactive][elevated]) .card:hover { 109 + box-shadow: var(--peek-shadow-lg); 110 + transform: translateY(-1px); 111 + } 112 + 113 + :host([interactive]) .card:active { 114 + transform: scale(0.99); 115 + } 116 + 117 + :host([interactive]) .card:focus-visible { 118 + outline: none; 119 + box-shadow: var(--peek-focus-ring); 120 + } 121 + 122 + /* Sections */ 123 + .header { 124 + padding: var(--peek-card-padding, var(--peek-space-lg)); 125 + padding-bottom: 0; 126 + font-weight: var(--peek-font-semibold); 127 + font-size: var(--peek-font-lg); 128 + color: var(--theme-text, #333); 129 + } 130 + 131 + .header:empty { 132 + display: none; 133 + } 134 + 135 + .body { 136 + padding: var(--peek-card-padding, var(--peek-space-lg)); 137 + flex: 1; 138 + color: var(--theme-text-secondary, #666); 139 + font-size: var(--peek-font-md); 140 + line-height: var(--peek-leading-normal); 141 + } 142 + 143 + .body:empty { 144 + display: none; 145 + } 146 + 147 + /* Adjust body padding when header exists */ 148 + .header + .body { 149 + padding-top: var(--peek-card-gap, var(--peek-space-md)); 150 + } 151 + 152 + .footer { 153 + padding: var(--peek-card-padding, var(--peek-space-lg)); 154 + padding-top: 0; 155 + border-top: 1px solid var(--peek-card-border, var(--theme-border, #e0e0e0)); 156 + margin-top: auto; 157 + padding-top: var(--peek-card-gap, var(--peek-space-md)); 158 + font-size: var(--peek-font-sm); 159 + color: var(--theme-text-muted, #999); 160 + } 161 + 162 + .footer:empty { 163 + display: none; 164 + } 165 + 166 + /* Remove border-top when footer directly follows header (no body) */ 167 + .header + .footer { 168 + border-top: none; 169 + } 170 + 171 + .media { 172 + margin: 0; 173 + line-height: 0; 174 + } 175 + 176 + .media:empty { 177 + display: none; 178 + } 179 + 180 + ::slotted([slot="media"]) { 181 + width: 100%; 182 + height: auto; 183 + display: block; 184 + } 185 + 186 + /* Media at top - no top radius padding needed */ 187 + .media:first-child ::slotted([slot="media"]) { 188 + border-radius: var(--peek-card-radius, var(--peek-radius-lg)) 189 + var(--peek-card-radius, var(--peek-radius-lg)) 190 + 0 0; 191 + } 192 + ` 193 + ]; 194 + 195 + constructor() { 196 + super(); 197 + this.interactive = false; 198 + this.selected = false; 199 + this.elevated = false; 200 + this.bordered = true; 201 + } 202 + 203 + render() { 204 + return html` 205 + <div 206 + part="card" 207 + class="card" 208 + role=${this.interactive ? 'button' : nothing} 209 + tabindex=${this.interactive ? '0' : nothing} 210 + @click=${this._handleClick} 211 + @keydown=${this._handleKeydown} 212 + > 213 + <div part="media" class="media"> 214 + <slot name="media"></slot> 215 + </div> 216 + <div part="header" class="header"> 217 + <slot name="header"></slot> 218 + </div> 219 + <div part="body" class="body"> 220 + <slot></slot> 221 + </div> 222 + <div part="footer" class="footer"> 223 + <slot name="footer"></slot> 224 + </div> 225 + </div> 226 + `; 227 + } 228 + 229 + _handleClick(e) { 230 + if (this.interactive) { 231 + this.emit('card-click', { originalEvent: e }); 232 + } 233 + } 234 + 235 + _handleKeydown(e) { 236 + if (this.interactive && (e.key === 'Enter' || e.key === ' ')) { 237 + e.preventDefault(); 238 + this.emit('card-click', { originalEvent: e }); 239 + } 240 + } 241 + 242 + /** 243 + * Focus the card (only works when interactive) 244 + */ 245 + focus() { 246 + if (this.interactive) { 247 + this.shadowRoot?.querySelector('.card')?.focus(); 248 + } 249 + } 250 + } 251 + 252 + customElements.define('peek-card', PeekCard); 253 + 254 + export default PeekCard;
+402
app/components/peek-list.js
··· 1 + /** 2 + * Peek List Component 3 + * 4 + * A keyboard-navigable list supporting selection and custom item rendering. 5 + * Uses native listbox semantics for accessibility. 6 + * 7 + * @element peek-list 8 + * 9 + * @prop {string} selection - Selection mode: 'none' | 'single' | 'multiple' 10 + * @prop {number} selectedIndex - Currently selected index (single selection) 11 + * @prop {number[]} selectedIndices - Selected indices (multiple selection) 12 + * @prop {boolean} wrap - Wrap navigation at list ends 13 + * 14 + * @slot - Default slot for peek-list-item elements 15 + * 16 + * @csspart list - The list container 17 + * 18 + * @cssprop --peek-list-gap - Gap between items 19 + * @cssprop --peek-list-padding - List container padding 20 + * 21 + * @fires selection-change - When selection changes. Detail: { selectedIndex, selectedIndices, item } 22 + * @fires item-activate - When item is activated (Enter/click). Detail: { index, item } 23 + * 24 + * @example 25 + * <peek-list selection="single"> 26 + * <peek-list-item>Item 1</peek-list-item> 27 + * <peek-list-item>Item 2</peek-list-item> 28 + * <peek-list-item>Item 3</peek-list-item> 29 + * </peek-list> 30 + */ 31 + 32 + import { html, css } from 'lit'; 33 + import { PeekElement, sharedStyles } from './base.js'; 34 + 35 + export class PeekList extends PeekElement { 36 + static properties = { 37 + selection: { type: String, reflect: true }, 38 + selectedIndex: { type: Number, attribute: 'selected-index' }, 39 + selectedIndices: { type: Array }, 40 + wrap: { type: Boolean }, 41 + _focusedIndex: { type: Number, state: true } 42 + }; 43 + 44 + static styles = [ 45 + sharedStyles, 46 + css` 47 + :host { 48 + display: block; 49 + } 50 + 51 + .list { 52 + display: flex; 53 + flex-direction: column; 54 + gap: var(--peek-list-gap, var(--peek-list-item-gap, 2px)); 55 + padding: var(--peek-list-padding, 0); 56 + margin: 0; 57 + list-style: none; 58 + outline: none; 59 + } 60 + 61 + .list:focus-visible { 62 + box-shadow: var(--peek-focus-ring); 63 + border-radius: var(--peek-radius-md); 64 + } 65 + ` 66 + ]; 67 + 68 + constructor() { 69 + super(); 70 + this.selection = 'none'; 71 + this.selectedIndex = -1; 72 + this.selectedIndices = []; 73 + this.wrap = false; 74 + this._focusedIndex = -1; 75 + } 76 + 77 + get items() { 78 + const slot = this.shadowRoot?.querySelector('slot'); 79 + if (!slot) return []; 80 + return slot.assignedElements().filter(el => el.tagName === 'PEEK-LIST-ITEM'); 81 + } 82 + 83 + firstUpdated() { 84 + this._updateItemStates(); 85 + } 86 + 87 + updated(changedProperties) { 88 + if (changedProperties.has('selectedIndex') || changedProperties.has('selectedIndices')) { 89 + this._updateItemStates(); 90 + } 91 + } 92 + 93 + _updateItemStates() { 94 + const items = this.items; 95 + items.forEach((item, index) => { 96 + const isSelected = this.selection === 'multiple' 97 + ? this.selectedIndices.includes(index) 98 + : this.selectedIndex === index; 99 + item.selected = isSelected; 100 + item.focused = this._focusedIndex === index; 101 + }); 102 + } 103 + 104 + render() { 105 + return html` 106 + <div 107 + part="list" 108 + class="list" 109 + role="listbox" 110 + tabindex="0" 111 + aria-multiselectable=${this.selection === 'multiple' ? 'true' : 'false'} 112 + @keydown=${this._handleKeydown} 113 + @click=${this._handleClick} 114 + > 115 + <slot @slotchange=${this._handleSlotChange}></slot> 116 + </div> 117 + `; 118 + } 119 + 120 + _handleSlotChange() { 121 + this._updateItemStates(); 122 + } 123 + 124 + _handleKeydown(e) { 125 + const items = this.items; 126 + if (items.length === 0) return; 127 + 128 + let handled = false; 129 + let newIndex = this._focusedIndex; 130 + 131 + switch (e.key) { 132 + case 'ArrowDown': 133 + case 'j': 134 + newIndex = this._getNextIndex(1); 135 + handled = true; 136 + break; 137 + 138 + case 'ArrowUp': 139 + case 'k': 140 + newIndex = this._getNextIndex(-1); 141 + handled = true; 142 + break; 143 + 144 + case 'Home': 145 + case 'g': 146 + if (e.key === 'g' && !e.shiftKey) break; 147 + newIndex = 0; 148 + handled = true; 149 + break; 150 + 151 + case 'End': 152 + case 'G': 153 + newIndex = items.length - 1; 154 + handled = true; 155 + break; 156 + 157 + case 'Enter': 158 + case ' ': 159 + if (this._focusedIndex >= 0) { 160 + this._activateItem(this._focusedIndex); 161 + handled = true; 162 + } 163 + break; 164 + 165 + case 'Escape': 166 + this._focusedIndex = -1; 167 + this._updateItemStates(); 168 + handled = true; 169 + break; 170 + } 171 + 172 + if (handled) { 173 + e.preventDefault(); 174 + e.stopPropagation(); 175 + } 176 + 177 + if (newIndex !== this._focusedIndex && newIndex >= 0) { 178 + this._focusedIndex = newIndex; 179 + this._updateItemStates(); 180 + 181 + // Scroll item into view 182 + const item = items[newIndex]; 183 + item?.scrollIntoView({ block: 'nearest' }); 184 + } 185 + } 186 + 187 + _getNextIndex(delta) { 188 + const items = this.items; 189 + const len = items.length; 190 + if (len === 0) return -1; 191 + 192 + let current = this._focusedIndex; 193 + if (current < 0) { 194 + return delta > 0 ? 0 : len - 1; 195 + } 196 + 197 + let next = current + delta; 198 + 199 + if (this.wrap) { 200 + next = ((next % len) + len) % len; 201 + } else { 202 + next = Math.max(0, Math.min(len - 1, next)); 203 + } 204 + 205 + return next; 206 + } 207 + 208 + _handleClick(e) { 209 + const items = this.items; 210 + const item = e.target.closest('peek-list-item'); 211 + if (!item) return; 212 + 213 + const index = items.indexOf(item); 214 + if (index < 0) return; 215 + 216 + this._focusedIndex = index; 217 + this._activateItem(index); 218 + } 219 + 220 + _activateItem(index) { 221 + const items = this.items; 222 + const item = items[index]; 223 + 224 + if (this.selection === 'single') { 225 + this.selectedIndex = index; 226 + this.emit('selection-change', { 227 + selectedIndex: index, 228 + selectedIndices: [index], 229 + item 230 + }); 231 + } else if (this.selection === 'multiple') { 232 + const newIndices = this.selectedIndices.includes(index) 233 + ? this.selectedIndices.filter(i => i !== index) 234 + : [...this.selectedIndices, index]; 235 + this.selectedIndices = newIndices; 236 + this.emit('selection-change', { 237 + selectedIndex: index, 238 + selectedIndices: newIndices, 239 + item 240 + }); 241 + } 242 + 243 + this._updateItemStates(); 244 + this.emit('item-activate', { index, item }); 245 + } 246 + 247 + /** 248 + * Focus the list container 249 + */ 250 + focus() { 251 + this.shadowRoot?.querySelector('.list')?.focus(); 252 + } 253 + 254 + /** 255 + * Select an item by index 256 + * @param {number} index - Index to select 257 + */ 258 + select(index) { 259 + if (this.selection === 'single') { 260 + this.selectedIndex = index; 261 + } else if (this.selection === 'multiple') { 262 + if (!this.selectedIndices.includes(index)) { 263 + this.selectedIndices = [...this.selectedIndices, index]; 264 + } 265 + } 266 + this._updateItemStates(); 267 + } 268 + 269 + /** 270 + * Clear all selections 271 + */ 272 + clearSelection() { 273 + this.selectedIndex = -1; 274 + this.selectedIndices = []; 275 + this._updateItemStates(); 276 + } 277 + } 278 + 279 + /** 280 + * Peek List Item Component 281 + * 282 + * An item within a peek-list. Supports selection states and custom content. 283 + * 284 + * @element peek-list-item 285 + * 286 + * @prop {boolean} selected - Whether item is selected 287 + * @prop {boolean} disabled - Whether item is disabled 288 + * @prop {*} value - Optional value associated with this item 289 + * 290 + * @slot - Default slot for item content 291 + * @slot prefix - Content before main content (e.g., icon) 292 + * @slot suffix - Content after main content (e.g., badge) 293 + * 294 + * @csspart item - The item container 295 + * 296 + * @cssprop --peek-list-item-bg - Item background 297 + * @cssprop --peek-list-item-hover-bg - Item hover background 298 + * @cssprop --peek-list-item-selected-bg - Selected item background 299 + */ 300 + export class PeekListItem extends PeekElement { 301 + static properties = { 302 + selected: { type: Boolean, reflect: true }, 303 + disabled: { type: Boolean, reflect: true }, 304 + focused: { type: Boolean, reflect: true }, 305 + value: { type: Object } 306 + }; 307 + 308 + static styles = [ 309 + sharedStyles, 310 + css` 311 + :host { 312 + display: block; 313 + } 314 + 315 + .item { 316 + display: flex; 317 + align-items: center; 318 + gap: var(--peek-space-sm); 319 + padding: var(--peek-list-item-padding-y, var(--peek-space-sm)) 320 + var(--peek-list-item-padding-x, var(--peek-space-md)); 321 + background: var(--peek-list-item-bg, transparent); 322 + border-radius: var(--peek-radius-md); 323 + cursor: pointer; 324 + user-select: none; 325 + transition: background var(--peek-transition-fast); 326 + font-size: var(--peek-font-md); 327 + color: var(--theme-text, #333); 328 + } 329 + 330 + .item:hover:not([aria-disabled="true"]) { 331 + background: var(--peek-list-item-hover-bg, var(--theme-bg-tertiary, rgba(0, 0, 0, 0.05))); 332 + } 333 + 334 + :host([selected]) .item { 335 + background: var(--peek-list-item-selected-bg, var(--theme-accent, #007aff)); 336 + color: #fff; 337 + } 338 + 339 + :host([selected]) .item:hover { 340 + background: var(--peek-list-item-selected-bg, color-mix(in srgb, var(--theme-accent, #007aff) 90%, #000)); 341 + } 342 + 343 + :host([focused]:not([selected])) .item { 344 + background: var(--peek-list-item-hover-bg, var(--theme-bg-tertiary, rgba(0, 0, 0, 0.05))); 345 + outline: 1px solid var(--theme-accent, #007aff); 346 + outline-offset: -1px; 347 + } 348 + 349 + :host([disabled]) .item { 350 + opacity: 0.5; 351 + cursor: not-allowed; 352 + pointer-events: none; 353 + } 354 + 355 + .content { 356 + flex: 1; 357 + min-width: 0; 358 + overflow: hidden; 359 + text-overflow: ellipsis; 360 + white-space: nowrap; 361 + } 362 + 363 + ::slotted([slot="prefix"]), 364 + ::slotted([slot="suffix"]) { 365 + display: flex; 366 + align-items: center; 367 + flex-shrink: 0; 368 + } 369 + ` 370 + ]; 371 + 372 + constructor() { 373 + super(); 374 + this.selected = false; 375 + this.disabled = false; 376 + this.focused = false; 377 + this.value = null; 378 + } 379 + 380 + render() { 381 + return html` 382 + <div 383 + part="item" 384 + class="item" 385 + role="option" 386 + aria-selected=${this.selected ? 'true' : 'false'} 387 + aria-disabled=${this.disabled ? 'true' : 'false'} 388 + > 389 + <slot name="prefix"></slot> 390 + <span class="content"> 391 + <slot></slot> 392 + </span> 393 + <slot name="suffix"></slot> 394 + </div> 395 + `; 396 + } 397 + } 398 + 399 + customElements.define('peek-list', PeekList); 400 + customElements.define('peek-list-item', PeekListItem); 401 + 402 + export default PeekList;
+81
app/components/tokens.css
··· 1 + /** 2 + * Component Design Tokens 3 + * 4 + * These tokens extend the base theme variables from peek://theme/variables.css 5 + * Components use these for consistent spacing, sizing, and effects. 6 + * 7 + * Usage in components: 8 + * import { css } from 'lit'; 9 + * import componentTokens from './tokens.css' with { type: 'css' }; 10 + */ 11 + 12 + :host { 13 + /* Spacing Scale */ 14 + --peek-space-xs: 4px; 15 + --peek-space-sm: 8px; 16 + --peek-space-md: 12px; 17 + --peek-space-lg: 16px; 18 + --peek-space-xl: 24px; 19 + --peek-space-2xl: 32px; 20 + 21 + /* Border Radius */ 22 + --peek-radius-sm: 4px; 23 + --peek-radius-md: 6px; 24 + --peek-radius-lg: 8px; 25 + --peek-radius-xl: 12px; 26 + --peek-radius-full: 9999px; 27 + 28 + /* Font Sizes */ 29 + --peek-font-xs: 11px; 30 + --peek-font-sm: 13px; 31 + --peek-font-md: 14px; 32 + --peek-font-lg: 16px; 33 + --peek-font-xl: 18px; 34 + --peek-font-2xl: 24px; 35 + 36 + /* Font Weights */ 37 + --peek-font-normal: 400; 38 + --peek-font-medium: 500; 39 + --peek-font-semibold: 600; 40 + --peek-font-bold: 700; 41 + 42 + /* Line Heights */ 43 + --peek-leading-tight: 1.25; 44 + --peek-leading-normal: 1.5; 45 + --peek-leading-relaxed: 1.75; 46 + 47 + /* Shadows */ 48 + --peek-shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.1); 49 + --peek-shadow-md: 0 2px 4px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.08); 50 + --peek-shadow-lg: 0 4px 12px rgba(0, 0, 0, 0.15), 0 2px 4px rgba(0, 0, 0, 0.1); 51 + --peek-shadow-xl: 0 8px 24px rgba(0, 0, 0, 0.2), 0 4px 8px rgba(0, 0, 0, 0.12); 52 + 53 + /* Transitions */ 54 + --peek-transition-fast: 100ms ease; 55 + --peek-transition-normal: 150ms ease; 56 + --peek-transition-slow: 250ms ease; 57 + 58 + /* Focus Ring */ 59 + --peek-focus-ring: 0 0 0 2px var(--theme-accent, #007aff); 60 + --peek-focus-ring-offset: 0 0 0 3px var(--theme-bg, #fff); 61 + 62 + /* Component-specific tokens */ 63 + 64 + /* Button */ 65 + --peek-btn-height-sm: 28px; 66 + --peek-btn-height-md: 36px; 67 + --peek-btn-height-lg: 44px; 68 + --peek-btn-padding-x: var(--peek-space-md); 69 + --peek-btn-padding-x-sm: var(--peek-space-sm); 70 + --peek-btn-padding-x-lg: var(--peek-space-lg); 71 + 72 + /* Card */ 73 + --peek-card-padding: var(--peek-space-lg); 74 + --peek-card-gap: var(--peek-space-md); 75 + --peek-card-radius: var(--peek-radius-lg); 76 + 77 + /* List */ 78 + --peek-list-item-padding-x: var(--peek-space-md); 79 + --peek-list-item-padding-y: var(--peek-space-sm); 80 + --peek-list-item-gap: var(--peek-space-xs); 81 + }
+2 -1
package.json
··· 145 145 "dependencies": { 146 146 "archiver": "^7.0.0", 147 147 "better-sqlite3": "^12.5.0", 148 - "electron-unhandled": "^5.0.0" 148 + "electron-unhandled": "^5.0.0", 149 + "lit": "^3.3.2" 149 150 }, 150 151 "devDependencies": { 151 152 "@electron/rebuild": "^4.0.2",
+55
yarn.lock
··· 429 429 languageName: node 430 430 linkType: hard 431 431 432 + "@lit-labs/ssr-dom-shim@npm:^1.5.0": 433 + version: 1.5.1 434 + resolution: "@lit-labs/ssr-dom-shim@npm:1.5.1" 435 + checksum: 10c0/2b10a42db0af33a4db32b3aa34db0f546aaa6794acdfc173499e999b4423102a1c9d15687679c674f07fa799cf740b5f5641c2ca6eee5d4af30c762a1e3b8c4f 436 + languageName: node 437 + linkType: hard 438 + 439 + "@lit/reactive-element@npm:^2.1.0": 440 + version: 2.1.2 441 + resolution: "@lit/reactive-element@npm:2.1.2" 442 + dependencies: 443 + "@lit-labs/ssr-dom-shim": "npm:^1.5.0" 444 + checksum: 10c0/557069ce6ebbbafb1140e1e0a25ce73d3501bf455cda231d42bb131baa9065c54b6b7ca1655507eede397decd7ddde16c84192cb72a07d4edf41d54e07725933 445 + languageName: node 446 + linkType: hard 447 + 432 448 "@malept/cross-spawn-promise@npm:^2.0.0": 433 449 version: 2.0.0 434 450 resolution: "@malept/cross-spawn-promise@npm:2.0.0" ··· 734 750 languageName: node 735 751 linkType: hard 736 752 753 + "@types/trusted-types@npm:^2.0.2": 754 + version: 2.0.7 755 + resolution: "@types/trusted-types@npm:2.0.7" 756 + checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c 757 + languageName: node 758 + linkType: hard 759 + 737 760 "@types/verror@npm:^1.10.3": 738 761 version: 1.10.11 739 762 resolution: "@types/verror@npm:1.10.11" ··· 772 795 electron-builder: "npm:26.0.12" 773 796 electron-unhandled: "npm:^5.0.0" 774 797 fake-indexeddb: "npm:^6.2.5" 798 + lit: "npm:^3.3.2" 775 799 playwright: "npm:^1.57.0" 776 800 typescript: "npm:^5.9.3" 777 801 web-ext: "npm:^9.2.0" ··· 3825 3849 debug: "npm:^4.4.1" 3826 3850 marky: "npm:^1.2.2" 3827 3851 checksum: 10c0/bbce3939a0359d5f1f84b7cc623f1ee3daf5a28e55b7b9bf7d461d906121e64fa6de290c53bd6bdd6068a67442fa39a7deb6f61da2e0e1721c39ec4cc80876b8 3852 + languageName: node 3853 + linkType: hard 3854 + 3855 + "lit-element@npm:^4.2.0": 3856 + version: 4.2.2 3857 + resolution: "lit-element@npm:4.2.2" 3858 + dependencies: 3859 + "@lit-labs/ssr-dom-shim": "npm:^1.5.0" 3860 + "@lit/reactive-element": "npm:^2.1.0" 3861 + lit-html: "npm:^3.3.0" 3862 + checksum: 10c0/114ab5837f1f9e03a30b1ed1c055fa0e31f01e444464e5ab0453ef88be12d778508235533267c42614d323e3048ee58f865b5c612948a53bd6219abca404c710 3863 + languageName: node 3864 + linkType: hard 3865 + 3866 + "lit-html@npm:^3.3.0": 3867 + version: 3.3.2 3868 + resolution: "lit-html@npm:3.3.2" 3869 + dependencies: 3870 + "@types/trusted-types": "npm:^2.0.2" 3871 + checksum: 10c0/0a6763875acd03dfc5d4483ea74ca4bfe5d71a90b05bfc484e8201721c8603db982760fd27566a69a834f21d34437f7c390e21cd4c6bff149ca7e3a46d3b217a 3872 + languageName: node 3873 + linkType: hard 3874 + 3875 + "lit@npm:^3.3.2": 3876 + version: 3.3.2 3877 + resolution: "lit@npm:3.3.2" 3878 + dependencies: 3879 + "@lit/reactive-element": "npm:^2.1.0" 3880 + lit-element: "npm:^4.2.0" 3881 + lit-html: "npm:^3.3.0" 3882 + checksum: 10c0/50563fd9c7bf546f8fdc6a936321b5be581ce440a359b06048ae5d44c1ecf6c38c2ded708e97d36a1ce70da1a7ad569890e40e0fb5ed040ec42d5ed2365f468d 3828 3883 languageName: node 3829 3884 linkType: hard 3830 3885