Last active
November 6, 2025 18:08
-
-
Save kristoferjoseph/04ae16317568c7d3f240b91cbd3273c0 to your computer and use it in GitHub Desktop.
layout components prototyping
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en" data-theme="auto"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Design System 808</title> | |
| <style> | |
| /* ============================================ | |
| PARAMETRIC DESIGN TOKENS | |
| ============================================ */ | |
| :root { | |
| /* Enable native light/dark mode */ | |
| color-scheme: light dark; | |
| /* Viewport Configuration */ | |
| --fluid-min-width: 320; | |
| --fluid-max-width: 1500; | |
| --fluid-screen: 100vw; | |
| --fluid-bp: calc( | |
| (var(--fluid-screen) - ((var(--fluid-min-width) / 16) * 1rem)) / | |
| ((var(--fluid-max-width) / 16) - (var(--fluid-min-width) / 16)) | |
| ); | |
| /* Fluid Type Scale (Minor Third → Perfect Fourth) */ | |
| --fluid--2: clamp(0.79rem, calc(0.77rem + 0.08vw), 0.84rem); | |
| --fluid--1: clamp(0.89rem, calc(0.87rem + 0.11vw), 0.97rem); | |
| --fluid-0: clamp(1.00rem, calc(0.98rem + 0.13vw), 1.13rem); | |
| --fluid-1: clamp(1.20rem, calc(1.15rem + 0.25vw), 1.42rem); | |
| --fluid-2: clamp(1.44rem, calc(1.35rem + 0.43vw), 1.78rem); | |
| --fluid-3: clamp(1.73rem, calc(1.59rem + 0.67vw), 2.37rem); | |
| --fluid-4: clamp(2.07rem, calc(1.88rem + 0.98vw), 3.16rem); | |
| --fluid-5: clamp(2.49rem, calc(2.20rem + 1.43vw), 4.21rem); | |
| /* Fluid Space Scale (Minor Third → Perfect Fourth) */ | |
| --space--2: clamp(0.50rem, calc(0.49rem + 0.07vw), 0.56rem); | |
| --space--1: clamp(0.75rem, calc(0.73rem + 0.10vw), 0.84rem); | |
| --space-0: clamp(1.00rem, calc(0.98rem + 0.13vw), 1.13rem); | |
| --space-1: clamp(1.50rem, calc(1.46rem + 0.20vw), 1.69rem); | |
| --space-2: clamp(2.00rem, calc(1.95rem + 0.27vw), 2.25rem); | |
| --space-3: clamp(3.00rem, calc(2.93rem + 0.40vw), 3.38rem); | |
| --space-4: clamp(4.00rem, calc(3.90rem + 0.54vw), 4.50rem); | |
| --space-5: clamp(6.00rem, calc(5.85rem + 0.81vw), 6.75rem); | |
| /* Border Radii */ | |
| --radius-1: 2px; | |
| --radius-2: 4px; | |
| --radius-3: 8px; | |
| --radius-full: 9999px; | |
| /* Border Widths */ | |
| --border-1: 1px; | |
| --border-2: 2px; | |
| /* Component-specific tokens */ | |
| --nav-width: 0px; | |
| --rail-width: 0px; | |
| --appbar-height: clamp(3.5rem, calc(3.41rem + 0.47vw), 3.94rem); | |
| --footer-height: clamp(2.5rem, calc(2.44rem + 0.34vw), 2.81rem); | |
| } | |
| /* ============================================ | |
| LIGHT MODE (Default) | |
| WCAG AA Compliant - 4.5:1 minimum contrast | |
| ============================================ */ | |
| :root, | |
| :root[data-theme="light"] { | |
| color-scheme: light; | |
| /* Colorblind-safe blue (safe for protanopia/deuteranopia) */ | |
| accent-color: #0066CC; | |
| --accent: #0066CC; | |
| --canvas-base: #ffffff; | |
| --canvas-elevated: #f5f5f5; | |
| --canvas-inset: #e8e8e8; | |
| /* WCAG AA compliant surfaces */ | |
| --surface-default: rgba(0, 0, 0, 0.08); | |
| --surface-hover: rgba(0, 0, 0, 0.12); | |
| --surface-active: rgba(0, 0, 0, 0.16); | |
| /* WCAG AA compliant text - 4.5:1+ contrast on white */ | |
| --text-primary: #1a1a1a; /* 16.1:1 contrast */ | |
| --text-secondary: #4d4d4d; /* 8.6:1 contrast */ | |
| --text-tertiary: #737373; /* 4.6:1 contrast */ | |
| --border-default: rgba(0, 0, 0, 0.15); | |
| /* Three-layer focus indicator */ | |
| --focus-ring-outer: #000000; /* Black outer ring */ | |
| --focus-ring-middle: #0066CC; /* Accent middle ring */ | |
| --focus-ring-inner: #ffffff; /* White inner ring */ | |
| } | |
| /* ============================================ | |
| DARK MODE | |
| WCAG AA Compliant - 4.5:1 minimum contrast | |
| ============================================ */ | |
| :root[data-theme="dark"] { | |
| color-scheme: dark; | |
| /* Lighter colorblind-safe blue for dark mode */ | |
| accent-color: #4D94FF; | |
| --accent: #4D94FF; | |
| --canvas-base: #0f0f0f; | |
| --canvas-elevated: #1a1a1a; | |
| --canvas-inset: #000000; | |
| /* WCAG AA compliant surfaces */ | |
| --surface-default: rgba(255, 255, 255, 0.10); | |
| --surface-hover: rgba(255, 255, 255, 0.15); | |
| --surface-active: rgba(255, 255, 255, 0.20); | |
| /* WCAG AA compliant text - 4.5:1+ contrast on #0f0f0f */ | |
| --text-primary: #f0f0f0; /* 15.3:1 contrast */ | |
| --text-secondary: #b3b3b3; /* 8.7:1 contrast */ | |
| --text-tertiary: #8c8c8c; /* 4.8:1 contrast */ | |
| --border-default: rgba(255, 255, 255, 0.20); | |
| /* Three-layer focus indicator (reversed for dark mode) */ | |
| --focus-ring-outer: #ffffff; /* White outer ring */ | |
| --focus-ring-middle: #4D94FF; /* Accent middle ring */ | |
| --focus-ring-inner: #000000; /* Black inner ring */ | |
| } | |
| /* Auto mode - follows system preference */ | |
| @media (prefers-color-scheme: dark) { | |
| :root[data-theme="auto"] { | |
| color-scheme: dark; | |
| accent-color: #4D94FF; | |
| --accent: #4D94FF; | |
| --canvas-base: #0f0f0f; | |
| --canvas-elevated: #1a1a1a; | |
| --canvas-inset: #000000; | |
| --surface-default: rgba(255, 255, 255, 0.10); | |
| --surface-hover: rgba(255, 255, 255, 0.15); | |
| --surface-active: rgba(255, 255, 255, 0.20); | |
| --text-primary: #f0f0f0; | |
| --text-secondary: #b3b3b3; | |
| --text-tertiary: #8c8c8c; | |
| --border-default: rgba(255, 255, 255, 0.20); | |
| /* Three-layer focus indicator (reversed for dark mode) */ | |
| --focus-ring-outer: #ffffff; | |
| --focus-ring-middle: #4D94FF; | |
| --focus-ring-inner: #000000; | |
| } | |
| } | |
| @media (min-width: 768px) { | |
| :root { | |
| --nav-width: 200px; | |
| --rail-width: 280px; | |
| } | |
| } | |
| /* Cap fluid scaling at max viewport */ | |
| @media screen and (min-width: 1500px) { | |
| :root { | |
| --fluid-screen: calc(var(--fluid-max-width) * 1px); | |
| } | |
| } | |
| /* ============================================ | |
| BASE STYLES | |
| ============================================ */ | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, | |
| Oxygen, Ubuntu, Cantarell, sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| background: var(--canvas-inset); | |
| color: var(--text-primary); | |
| font-size: var(--fluid-0); | |
| line-height: 1.5; | |
| height: 100vh; | |
| transition: background 0.2s, color 0.2s; | |
| } | |
| h1, h2, h3, h4, h5, h6 { | |
| line-height: 1.2; | |
| font-weight: 600; | |
| color: currentColor; | |
| } | |
| h1 { font-size: var(--fluid-4); } | |
| h2 { font-size: var(--fluid-3); } | |
| h3 { font-size: var(--fluid-2); } | |
| h4 { font-size: var(--fluid-1); } | |
| h5 { font-size: var(--fluid-0); } | |
| h6 { font-size: var(--fluid--1); } | |
| .demo-container { | |
| height: 100vh; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| button { | |
| background-color: AccentColor; | |
| color: AccentColorText; | |
| border-radius: var(--radius-2); | |
| padding: var(--space--1) var(--space-1); | |
| border: 0; | |
| cursor: pointer; | |
| font-size: var(--fluid--1); | |
| font-family: inherit; | |
| line-height: 1; | |
| transition: opacity 0.15s; | |
| position: relative; | |
| } | |
| button:hover { | |
| opacity: 0.9; | |
| } | |
| button:active { | |
| opacity: 0.8; | |
| } | |
| button:focus-visible { | |
| outline: 1px solid var(--focus-ring-outer); | |
| outline-offset: 2px; | |
| box-shadow: | |
| 0 0 0 1px var(--focus-ring-inner), | |
| 0 0 0 2px var(--focus-ring-middle); | |
| } | |
| /* Ensure minimum touch target size (44x44px) */ | |
| button { | |
| min-height: 44px; | |
| min-width: 44px; | |
| } | |
| code { | |
| font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace; | |
| background: var(--surface-default); | |
| padding: 0.125em 0.375em; | |
| border-radius: var(--radius-1); | |
| font-size: 0.9em; | |
| } | |
| /* Input base styles */ | |
| input[type="text"], | |
| input[type="email"], | |
| input[type="date"], | |
| input[type="color"], | |
| textarea, | |
| select { | |
| font-family: inherit; | |
| font-size: var(--fluid--1); | |
| color: var(--text-primary); | |
| background: var(--canvas-base); | |
| border: var(--border-1) solid var(--border-default); | |
| border-radius: var(--radius-2); | |
| padding: var(--space--1); | |
| transition: border-color 0.15s, background 0.2s; | |
| } | |
| /* Native date picker styling */ | |
| input[type="date"] { | |
| min-height: 44px; | |
| cursor: pointer; | |
| } | |
| input[type="date"]::-webkit-calendar-picker-indicator { | |
| cursor: pointer; | |
| filter: var(--date-picker-filter, none); | |
| } | |
| /* Dark mode filter for calendar icon */ | |
| @media (prefers-color-scheme: dark) { | |
| :root[data-theme="auto"] { | |
| --date-picker-filter: invert(1); | |
| } | |
| } | |
| :root[data-theme="dark"] { | |
| --date-picker-filter: invert(1); | |
| } | |
| input[type="color"] { | |
| width: 100px; | |
| height: 40px; | |
| padding: var(--space--2); | |
| cursor: pointer; | |
| } | |
| input[type="text"]:focus, | |
| input[type="email"]:focus, | |
| input[type="date"]:focus, | |
| textarea:focus, | |
| select:focus { | |
| outline: 1px solid var(--focus-ring-outer); | |
| outline-offset: 2px; | |
| box-shadow: | |
| 0 0 0 1px var(--focus-ring-inner), | |
| 0 0 0 2px var(--focus-ring-middle); | |
| border-color: var(--focus-ring-middle); | |
| } | |
| /* Three-layer focus ring for all interactive elements */ | |
| *:focus-visible { | |
| outline: 1px solid var(--focus-ring-outer); | |
| outline-offset: 2px; | |
| box-shadow: | |
| 0 0 0 1px var(--focus-ring-inner), | |
| 0 0 0 2px var(--focus-ring-middle); | |
| } | |
| /* Remove default outline, rely on focus-visible */ | |
| *:focus:not(:focus-visible) { | |
| outline: none; | |
| } | |
| /* Accent color elements */ | |
| input[type="checkbox"], | |
| input[type="radio"], | |
| input[type="range"], | |
| progress { | |
| accent-color: inherit; /* Inherits from root */ | |
| } | |
| input[type="checkbox"], | |
| input[type="radio"] { | |
| width: 18px; | |
| height: 18px; | |
| cursor: pointer; | |
| } | |
| input[type="range"] { | |
| width: 100%; | |
| cursor: pointer; | |
| } | |
| progress { | |
| width: 100%; | |
| height: 12px; | |
| border: none; | |
| border-radius: var(--radius-1); | |
| background: var(--surface-default); | |
| } | |
| progress::-webkit-progress-bar { | |
| background: var(--surface-default); | |
| border-radius: var(--radius-1); | |
| } | |
| progress::-webkit-progress-value { | |
| background: var(--accent); | |
| border-radius: var(--radius-1); | |
| } | |
| progress::-moz-progress-bar { | |
| background: var(--accent); | |
| border-radius: var(--radius-1); | |
| } | |
| label { | |
| font-size: var(--fluid--1); | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| } | |
| .checkbox-group, | |
| .radio-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space--2); | |
| } | |
| .checkbox-item, | |
| .radio-item { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--space--1); | |
| } | |
| .checkbox-item label, | |
| .radio-item label { | |
| font-weight: normal; | |
| cursor: pointer; | |
| } | |
| /* Table base styles */ | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: var(--fluid--1); | |
| } | |
| th, td { | |
| text-align: left; | |
| padding: var(--space--1) var(--space-0); | |
| } | |
| th { | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| td { | |
| color: var(--text-secondary); | |
| } | |
| th button { | |
| background: none; | |
| border: none; | |
| padding: 0; | |
| font: inherit; | |
| color: inherit; | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| gap: var(--space--2); | |
| width: 100%; | |
| text-align: left; | |
| min-height: 44px; | |
| } | |
| th button:hover { | |
| color: var(--text-primary); | |
| } | |
| th button:focus-visible { | |
| outline: 1px solid var(--focus-ring-outer); | |
| outline-offset: 2px; | |
| box-shadow: | |
| 0 0 0 1px var(--focus-ring-inner), | |
| 0 0 0 2px var(--focus-ring-middle); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="demo-container"> | |
| <ui-layout> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: flex; | |
| flex-direction: column; | |
| width: 100%; | |
| height: 100%; | |
| overflow: hidden; | |
| } | |
| ::slotted([slot="header"]) { order: 1; } | |
| ::slotted([slot="leading"]) { order: 2; } | |
| ::slotted(:not([slot])) { order: 3; flex: 1; overflow-y: auto; } | |
| ::slotted([slot="trailing"]) { display: none; } | |
| ::slotted([slot="footer"]) { order: 4; } | |
| @media (min-width: 768px) { | |
| :host { | |
| display: grid; | |
| grid-template-columns: var(--nav-width) 1fr var(--rail-width); | |
| grid-template-rows: auto 1fr auto; | |
| grid-template-areas: | |
| "header header header" | |
| "nav content sidebar" | |
| "footer footer footer"; | |
| } | |
| ::slotted([slot="header"]) { grid-area: header; order: unset; } | |
| ::slotted([slot="leading"]) { grid-area: nav; } | |
| ::slotted([slot="trailing"]) { display: flex; grid-area: sidebar; } | |
| ::slotted([slot="footer"]) { grid-area: footer; order: unset; } | |
| ::slotted(:not([slot])) { grid-area: content; order: unset; } | |
| } | |
| </style> | |
| <slot name="header"></slot> | |
| <slot name="leading"></slot> | |
| <slot></slot> | |
| <slot name="trailing"></slot> | |
| <slot name="footer"></slot> | |
| </template> | |
| <ui-appbar slot="header" title="Design System"> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| background: var(--canvas-base); | |
| border-bottom: var(--border-1) solid var(--border-default); | |
| transition: background 0.2s; | |
| } | |
| .appbar { | |
| display: flex; | |
| align-items: center; | |
| min-height: var(--appbar-height); | |
| padding: 0 var(--space-1); | |
| gap: var(--space-1); | |
| } | |
| .title { | |
| font-size: var(--fluid-1); | |
| font-weight: 600; | |
| flex: 1; | |
| } | |
| .actions { | |
| display: flex; | |
| gap: var(--space--1); | |
| align-items: center; | |
| } | |
| ::slotted(div) { | |
| display: flex; | |
| gap: var(--space--1); | |
| align-items: center; | |
| } | |
| @media (min-width: 768px) { | |
| .appbar { padding: 0 var(--space-2); } | |
| } | |
| </style> | |
| <div class="appbar"> | |
| <div class="title"></div> | |
| <div class="actions"> | |
| <slot name="actions"></slot> | |
| </div> | |
| </div> | |
| </template> | |
| <div slot="actions"> | |
| <ui-theme-switcher> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: inline-flex; | |
| gap: 2px; | |
| background: var(--surface-default); | |
| padding: 2px; | |
| border-radius: var(--radius-2); | |
| } | |
| button { | |
| background: transparent; | |
| border: none; | |
| padding: var(--space--2) var(--space--1); | |
| border-radius: var(--radius-1); | |
| cursor: pointer; | |
| font-size: var(--fluid--2); | |
| color: var(--text-secondary); | |
| transition: all 0.15s; | |
| font-family: inherit; | |
| min-width: 44px; | |
| min-height: 44px; | |
| } | |
| button:hover { | |
| background: var(--surface-hover); | |
| color: var(--text-primary); | |
| } | |
| button.active { | |
| background: AccentColor; | |
| color: AccentColorText; | |
| } | |
| button:focus-visible { | |
| outline: 1px solid var(--focus-ring-outer); | |
| outline-offset: 2px; | |
| box-shadow: | |
| 0 0 0 1px var(--focus-ring-inner), | |
| 0 0 0 2px var(--focus-ring-middle); | |
| } | |
| </style> | |
| <button data-theme="light" aria-label="Switch to light theme" title="Light theme">+</button> | |
| <button data-theme="auto" class="active" aria-label="Use system theme preference" title="Auto theme">=</button> | |
| <button data-theme="dark" aria-label="Switch to dark theme" title="Dark theme">-</button> | |
| </template> | |
| </ui-theme-switcher> | |
| <ui-divider orientation="vertical"> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: inline-block; | |
| width: var(--border-1); | |
| height: 24px; | |
| background: var(--border-default); | |
| margin: 0 var(--space--1); | |
| } | |
| </style> | |
| </template> | |
| </ui-divider> | |
| <button>Save</button> | |
| <button>Publish</button> | |
| </div> | |
| </ui-appbar> | |
| <ui-navigation slot="leading"> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| background: var(--canvas-elevated); | |
| border-bottom: var(--border-1) solid var(--border-default); | |
| transition: background 0.2s; | |
| } | |
| @media (min-width: 768px) { | |
| :host { | |
| width: var(--nav-width); | |
| height: 100%; | |
| border-bottom: none; | |
| border-right: var(--border-1) solid var(--border-default); | |
| overflow-y: auto; | |
| } | |
| } | |
| </style> | |
| <nav aria-label="Main navigation"> | |
| <slot></slot> | |
| </nav> | |
| </template> | |
| <ui-nav-menu> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: flex; | |
| gap: var(--space--1); | |
| padding: var(--space--1) var(--space-1); | |
| overflow-x: auto; | |
| overflow-y: hidden; | |
| } | |
| @media (min-width: 768px) { | |
| :host { | |
| display: block; | |
| padding: var(--space--1); | |
| overflow-x: visible; | |
| } | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <ui-nav-item> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| flex-shrink: 0; | |
| } | |
| .nav-item { | |
| display: block; | |
| padding: var(--space--1) var(--space-0); | |
| border-radius: var(--radius-2); | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| transition: all 0.15s; | |
| white-space: nowrap; | |
| font-size: var(--fluid--1); | |
| background: transparent; | |
| text-decoration: none; | |
| min-height: 44px; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .nav-item:hover { | |
| background: var(--surface-hover); | |
| color: var(--text-primary); | |
| } | |
| .nav-item.active { | |
| background: AccentColor; | |
| color: AccentColorText; | |
| } | |
| .nav-item:focus-visible { | |
| outline: 1px solid var(--focus-ring-outer); | |
| outline-offset: 2px; | |
| box-shadow: | |
| 0 0 0 1px var(--focus-ring-inner), | |
| 0 0 0 2px var(--focus-ring-middle); | |
| } | |
| @media (min-width: 768px) { | |
| .nav-item { | |
| margin-bottom: 2px; | |
| } | |
| } | |
| </style> | |
| <a href="#dashboard" class="nav-item" role="button" tabindex="0"> | |
| <slot></slot> | |
| </a> | |
| </template> | |
| Dashboard | |
| </ui-nav-item> | |
| <ui-nav-item active> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| flex-shrink: 0; | |
| } | |
| .nav-item { | |
| display: block; | |
| padding: var(--space--1) var(--space-0); | |
| border-radius: var(--radius-2); | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| transition: all 0.15s; | |
| white-space: nowrap; | |
| font-size: var(--fluid--1); | |
| background: transparent; | |
| text-decoration: none; | |
| min-height: 44px; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .nav-item:hover { | |
| background: var(--surface-hover); | |
| color: var(--text-primary); | |
| } | |
| .nav-item.active { | |
| background: AccentColor; | |
| color: AccentColorText; | |
| } | |
| .nav-item:focus-visible { | |
| outline: 3px solid var(--focus-ring); | |
| outline-offset: 2px; | |
| } | |
| @media (min-width: 768px) { | |
| .nav-item { | |
| margin-bottom: 2px; | |
| } | |
| } | |
| </style> | |
| <a href="#documents" class="nav-item active" role="button" tabindex="0" aria-current="page"> | |
| <slot></slot> | |
| </a> | |
| </template> | |
| Documents | |
| </ui-nav-item> | |
| <ui-nav-item> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| flex-shrink: 0; | |
| } | |
| .nav-item { | |
| display: block; | |
| padding: var(--space--1) var(--space-0); | |
| border-radius: var(--radius-2); | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| transition: all 0.15s; | |
| white-space: nowrap; | |
| font-size: var(--fluid--1); | |
| background: transparent; | |
| text-decoration: none; | |
| min-height: 44px; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .nav-item:hover { | |
| background: var(--surface-hover); | |
| color: var(--text-primary); | |
| } | |
| .nav-item.active { | |
| background: AccentColor; | |
| color: AccentColorText; | |
| } | |
| .nav-item:focus-visible { | |
| outline: 3px solid var(--focus-ring); | |
| outline-offset: 2px; | |
| } | |
| @media (min-width: 768px) { | |
| .nav-item { | |
| margin-bottom: 2px; | |
| } | |
| } | |
| </style> | |
| <a href="#media" class="nav-item" role="button" tabindex="0"> | |
| <slot></slot> | |
| </a> | |
| </template> | |
| Media | |
| </ui-nav-item> | |
| <ui-nav-item> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| flex-shrink: 0; | |
| } | |
| .nav-item { | |
| display: block; | |
| padding: var(--space--1) var(--space-0); | |
| border-radius: var(--radius-2); | |
| cursor: pointer; | |
| color: var(--text-secondary); | |
| transition: all 0.15s; | |
| white-space: nowrap; | |
| font-size: var(--fluid--1); | |
| background: transparent; | |
| text-decoration: none; | |
| min-height: 44px; | |
| display: flex; | |
| align-items: center; | |
| } | |
| .nav-item:hover { | |
| background: var(--surface-hover); | |
| color: var(--text-primary); | |
| } | |
| .nav-item.active { | |
| background: AccentColor; | |
| color: AccentColorText; | |
| } | |
| .nav-item:focus-visible { | |
| outline: 3px solid var(--focus-ring); | |
| outline-offset: 2px; | |
| } | |
| @media (min-width: 768px) { | |
| .nav-item { | |
| margin-bottom: 2px; | |
| } | |
| } | |
| </style> | |
| <a href="#settings" class="nav-item" role="button" tabindex="0"> | |
| <slot></slot> | |
| </a> | |
| </template> | |
| Settings | |
| </ui-nav-item> | |
| </ui-nav-menu> | |
| </ui-navigation> | |
| <ui-content> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| width: 100%; | |
| height: 100%; | |
| overflow-y: auto; | |
| background: var(--canvas-base); | |
| transition: background 0.2s; | |
| } | |
| </style> | |
| <main role="main"> | |
| <slot></slot> | |
| </main> | |
| </template> | |
| <ui-section> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| padding: var(--space-1); | |
| border-bottom: var(--border-1) solid var(--border-default); | |
| } | |
| :host(:last-child) { | |
| border-bottom: none; | |
| } | |
| ::slotted(h1), ::slotted(h2), ::slotted(h3), | |
| ::slotted(h4), ::slotted(h5), ::slotted(h6) { | |
| margin-top: 0; | |
| margin-bottom: var(--space-0); | |
| } | |
| ::slotted(p) { | |
| margin: 0 0 var(--space-0) 0; | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| } | |
| ::slotted(p:last-child) { | |
| margin-bottom: 0; | |
| } | |
| @media (min-width: 768px) { | |
| :host { | |
| padding: var(--space-2); | |
| } | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <h2>Theme Controls</h2> | |
| <p>Customize the appearance of the design system using the controls below.</p> | |
| <ui-input-group> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space--2); | |
| margin-bottom: var(--space-1); | |
| } | |
| ::slotted(label) { | |
| font-size: var(--fluid--1); | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| } | |
| ::slotted(input), | |
| ::slotted(textarea), | |
| ::slotted(select) { | |
| width: 100%; | |
| } | |
| ::slotted(span) { | |
| font-size: var(--fluid--2); | |
| color: var(--text-secondary); | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <label for="accent-picker">Accent Color</label> | |
| <input type="color" id="accent-picker" value="#2563eb"> | |
| <span>Changes all interactive elements instantly</span> | |
| </ui-input-group> | |
| </ui-section> | |
| <ui-section> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| padding: var(--space-1); | |
| border-bottom: var(--border-1) solid var(--border-default); | |
| } | |
| :host(:last-child) { | |
| border-bottom: none; | |
| } | |
| ::slotted(h1), ::slotted(h2), ::slotted(h3), | |
| ::slotted(h4), ::slotted(h5), ::slotted(h6) { | |
| margin-top: 0; | |
| margin-bottom: var(--space-0); | |
| } | |
| ::slotted(p) { | |
| margin: 0 0 var(--space-0) 0; | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| } | |
| ::slotted(p:last-child) { | |
| margin-bottom: 0; | |
| } | |
| @media (min-width: 768px) { | |
| :host { | |
| padding: var(--space-2); | |
| } | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <h3>Form Components</h3> | |
| <p>All form elements automatically inherit the accent color from the theme.</p> | |
| <ui-input-group> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space--2); | |
| margin-bottom: var(--space-1); | |
| } | |
| ::slotted(label) { | |
| font-size: var(--fluid--1); | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| } | |
| ::slotted(input), | |
| ::slotted(textarea), | |
| ::slotted(select) { | |
| width: 100%; | |
| } | |
| ::slotted(span) { | |
| font-size: var(--fluid--2); | |
| color: var(--text-secondary); | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <label for="name-input">Name</label> | |
| <input type="text" id="name-input" placeholder="Enter your name" aria-describedby="name-input-help"> | |
| <span id="name-input-help">Text inputs use accent color for focus outline</span> | |
| </ui-input-group> | |
| <ui-input-group> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space--2); | |
| margin-bottom: var(--space-1); | |
| } | |
| ::slotted(label) { | |
| font-size: var(--fluid--1); | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| } | |
| ::slotted(input), | |
| ::slotted(textarea), | |
| ::slotted(select) { | |
| width: 100%; | |
| } | |
| ::slotted(span) { | |
| font-size: var(--fluid--2); | |
| color: var(--text-secondary); | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <label for="category-select">Category</label> | |
| <select id="category-select" aria-describedby="category-select-help"> | |
| <option>Design</option> | |
| <option>Development</option> | |
| <option>Marketing</option> | |
| <option>Sales</option> | |
| </select> | |
| <span id="category-select-help">Select dropdowns use accent color</span> | |
| </ui-input-group> | |
| <ui-input-group> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space--2); | |
| margin-bottom: var(--space-1); | |
| } | |
| ::slotted(label) { | |
| font-size: var(--fluid--1); | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| } | |
| ::slotted(input), | |
| ::slotted(textarea), | |
| ::slotted(select) { | |
| width: 100%; | |
| } | |
| ::slotted(span) { | |
| font-size: var(--fluid--2); | |
| color: var(--text-secondary); | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <label for="volume-range">Volume</label> | |
| <input type="range" id="volume-range" min="0" max="100" value="75" aria-describedby="volume-range-help" aria-valuemin="0" aria-valuemax="100" aria-valuenow="75"> | |
| <span id="volume-range-help">Range sliders use accent color for the track fill</span> | |
| </ui-input-group> | |
| <ui-input-group> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space--2); | |
| margin-bottom: var(--space-1); | |
| } | |
| ::slotted(label) { | |
| font-size: var(--fluid--1); | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| } | |
| ::slotted(input), | |
| ::slotted(textarea), | |
| ::slotted(select) { | |
| width: 100%; | |
| } | |
| ::slotted(span) { | |
| font-size: var(--fluid--2); | |
| color: var(--text-secondary); | |
| } | |
| ::slotted(.checkbox-group), | |
| ::slotted(.radio-group) { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space--2); | |
| } | |
| ::slotted(.checkbox-item), | |
| ::slotted(.radio-item) { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--space--1); | |
| } | |
| ::slotted(input[type="checkbox"]), | |
| ::slotted(input[type="radio"]) { | |
| width: auto; | |
| cursor: pointer; | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <label id="preferences-label">Preferences</label> | |
| <div class="checkbox-group" role="group" aria-labelledby="preferences-label"> | |
| <div class="checkbox-item"> | |
| <input type="checkbox" id="newsletter" checked> | |
| <label for="newsletter" style="font-weight: normal; cursor: pointer;">Subscribe to newsletter</label> | |
| </div> | |
| <div class="checkbox-item"> | |
| <input type="checkbox" id="updates" checked> | |
| <label for="updates" style="font-weight: normal; cursor: pointer;">Receive product updates</label> | |
| </div> | |
| <div class="checkbox-item"> | |
| <input type="checkbox" id="offers"> | |
| <label for="offers" style="font-weight: normal; cursor: pointer;">Special offers</label> | |
| </div> | |
| </div> | |
| <span id="preferences-help">Checkboxes use accent color when checked</span> | |
| </ui-input-group> | |
| <ui-input-group> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space--2); | |
| margin-bottom: var(--space-1); | |
| } | |
| ::slotted(label) { | |
| font-size: var(--fluid--1); | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| } | |
| ::slotted(input), | |
| ::slotted(textarea), | |
| ::slotted(select) { | |
| width: 100%; | |
| } | |
| ::slotted(span) { | |
| font-size: var(--fluid--2); | |
| color: var(--text-secondary); | |
| } | |
| ::slotted(.checkbox-group), | |
| ::slotted(.radio-group) { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space--2); | |
| } | |
| ::slotted(.checkbox-item), | |
| ::slotted(.radio-item) { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--space--1); | |
| } | |
| ::slotted(input[type="checkbox"]), | |
| ::slotted(input[type="radio"]) { | |
| width: auto; | |
| cursor: pointer; | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <label id="plan-label">Plan Type</label> | |
| <div class="radio-group" role="radiogroup" aria-labelledby="plan-label" aria-describedby="plan-help"> | |
| <div class="radio-item"> | |
| <input type="radio" id="plan-basic" name="plan" value="basic"> | |
| <label for="plan-basic" style="font-weight: normal; cursor: pointer;">Basic</label> | |
| </div> | |
| <div class="radio-item"> | |
| <input type="radio" id="plan-pro" name="plan" value="pro" checked> | |
| <label for="plan-pro" style="font-weight: normal; cursor: pointer;">Pro</label> | |
| </div> | |
| <div class="radio-item"> | |
| <input type="radio" id="plan-enterprise" name="plan" value="enterprise"> | |
| <label for="plan-enterprise" style="font-weight: normal; cursor: pointer;">Enterprise</label> | |
| </div> | |
| </div> | |
| <span id="plan-help">Radio buttons use accent color when selected</span> | |
| </ui-input-group> | |
| <ui-input-group> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space--2); | |
| margin-bottom: var(--space-1); | |
| } | |
| ::slotted(label) { | |
| font-size: var(--fluid--1); | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| } | |
| ::slotted(input), | |
| ::slotted(textarea), | |
| ::slotted(select) { | |
| width: 100%; | |
| } | |
| ::slotted(span) { | |
| font-size: var(--fluid--2); | |
| color: var(--text-secondary); | |
| } | |
| ::slotted(progress) { | |
| width: 100%; | |
| height: 12px; | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <label for="upload-progress">Upload Progress</label> | |
| <progress id="upload-progress" value="65" max="100" aria-describedby="upload-progress-help">65%</progress> | |
| <span id="upload-progress-help">Progress bars use accent color for completion</span> | |
| </ui-input-group> | |
| <ui-button-stack> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: flex; | |
| gap: var(--space--1); | |
| align-items: center; | |
| flex-wrap: wrap; | |
| margin-top: var(--space-1); | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <button>Submit</button> | |
| <button style="background: var(--surface-default); color: var(--text-primary);">Cancel</button> | |
| </ui-button-stack> | |
| </ui-section> | |
| <ui-section> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| padding: var(--space-1); | |
| border-bottom: var(--border-1) solid var(--border-default); | |
| } | |
| :host(:last-child) { | |
| border-bottom: none; | |
| } | |
| ::slotted(h1), ::slotted(h2), ::slotted(h3), | |
| ::slotted(h4), ::slotted(h5), ::slotted(h6) { | |
| margin-top: 0; | |
| margin-bottom: var(--space-0); | |
| } | |
| ::slotted(p) { | |
| margin: 0 0 var(--space-0) 0; | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| } | |
| ::slotted(p:last-child) { | |
| margin-bottom: 0; | |
| } | |
| @media (min-width: 768px) { | |
| :host { | |
| padding: var(--space-2); | |
| } | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <h3>Date Picker Components</h3> | |
| <p>Native browser date picker styled with theme tokens, and a custom calendar component using the Temporal API for range selection.</p> | |
| <ui-input-group> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space--2); | |
| margin-bottom: var(--space-1); | |
| } | |
| ::slotted(label) { | |
| font-size: var(--fluid--1); | |
| font-weight: 500; | |
| color: var(--text-primary); | |
| } | |
| ::slotted(input), | |
| ::slotted(textarea), | |
| ::slotted(select) { | |
| width: 100%; | |
| } | |
| ::slotted(span) { | |
| font-size: var(--fluid--2); | |
| color: var(--text-secondary); | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <label for="native-date">Native Date Picker</label> | |
| <input type="date" id="native-date" aria-describedby="native-date-help"> | |
| <span id="native-date-help">Uses browser's native date picker with theme styling</span> | |
| </ui-input-group> | |
| <ui-divider> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| width: 100%; | |
| height: var(--border-1); | |
| background: var(--border-default); | |
| margin: var(--space-1) 0; | |
| } | |
| </style> | |
| </template> | |
| </ui-divider> | |
| <h4>Custom Range Calendar (Temporal API)</h4> | |
| <p style="font-size: var(--fluid--1); color: var(--text-secondary); margin-bottom: var(--space-1);"> | |
| Select a date range like Airbnb. Click start date, then end date. | |
| </p> | |
| <ui-date-range-picker> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| margin-top: var(--space-1); | |
| } | |
| .calendar-container { | |
| border: var(--border-1) solid var(--border-default); | |
| border-radius: var(--radius-2); | |
| background: var(--canvas-base); | |
| padding: var(--space-1); | |
| max-width: 320px; | |
| } | |
| .calendar-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: var(--space-0); | |
| padding-bottom: var(--space-0); | |
| border-bottom: var(--border-1) solid var(--border-default); | |
| } | |
| .month-year { | |
| font-weight: 600; | |
| font-size: var(--fluid-0); | |
| color: var(--text-primary); | |
| } | |
| .nav-button { | |
| background: var(--surface-default); | |
| border: none; | |
| border-radius: var(--radius-2); | |
| width: 32px; | |
| height: 32px; | |
| cursor: pointer; | |
| color: var(--text-primary); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: var(--fluid-0); | |
| transition: background 0.15s; | |
| } | |
| .nav-button:hover { | |
| background: var(--surface-hover); | |
| } | |
| .nav-button:focus-visible { | |
| outline: 1px solid var(--focus-ring-outer); | |
| outline-offset: 2px; | |
| box-shadow: | |
| 0 0 0 1px var(--focus-ring-inner), | |
| 0 0 0 2px var(--focus-ring-middle); | |
| } | |
| .calendar-grid { | |
| display: grid; | |
| grid-template-columns: repeat(7, 1fr); | |
| gap: 2px; | |
| margin-top: var(--space-0); | |
| } | |
| .day-label { | |
| text-align: center; | |
| font-size: var(--fluid--2); | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| padding: var(--space--2); | |
| } | |
| .day { | |
| aspect-ratio: 1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: var(--fluid--1); | |
| color: var(--text-secondary); | |
| border-radius: var(--radius-2); | |
| cursor: pointer; | |
| background: transparent; | |
| border: var(--border-1) solid transparent; | |
| transition: all 0.15s; | |
| min-width: 36px; | |
| min-height: 36px; | |
| } | |
| .day:hover:not(.disabled):not(.selected-start):not(.selected-end) { | |
| background: var(--surface-hover); | |
| color: var(--text-primary); | |
| } | |
| .day.disabled { | |
| color: var(--text-tertiary); | |
| cursor: not-allowed; | |
| opacity: 0.4; | |
| } | |
| .day.other-month { | |
| color: var(--text-tertiary); | |
| } | |
| .day.today { | |
| border-color: var(--border-default); | |
| font-weight: 600; | |
| } | |
| .day.in-range { | |
| background: var(--surface-default); | |
| } | |
| .day.selected-start, | |
| .day.selected-end { | |
| background: AccentColor; | |
| color: AccentColorText; | |
| font-weight: 600; | |
| } | |
| .day:focus-visible { | |
| outline: 1px solid var(--focus-ring-outer); | |
| outline-offset: 2px; | |
| box-shadow: | |
| 0 0 0 1px var(--focus-ring-inner), | |
| 0 0 0 2px var(--focus-ring-middle); | |
| } | |
| .selected-range { | |
| margin-top: var(--space-1); | |
| padding: var(--space-0); | |
| background: var(--surface-default); | |
| border-radius: var(--radius-2); | |
| font-size: var(--fluid--1); | |
| color: var(--text-secondary); | |
| } | |
| .selected-range strong { | |
| color: var(--text-primary); | |
| font-weight: 600; | |
| } | |
| .clear-button { | |
| background: var(--surface-default); | |
| color: var(--text-primary); | |
| border: none; | |
| padding: var(--space--2) var(--space-0); | |
| border-radius: var(--radius-2); | |
| cursor: pointer; | |
| font-size: var(--fluid--2); | |
| margin-top: var(--space--1); | |
| width: 100%; | |
| transition: background 0.15s; | |
| } | |
| .clear-button:hover { | |
| background: var(--surface-hover); | |
| } | |
| </style> | |
| <div class="calendar-container" role="application" aria-label="Date range picker calendar"> | |
| <div class="calendar-header"> | |
| <button class="nav-button" type="button" aria-label="Previous month">‹</button> | |
| <div class="month-year" aria-live="polite"></div> | |
| <button class="nav-button" type="button" aria-label="Next month">›</button> | |
| </div> | |
| <div class="calendar-grid" role="grid" aria-label="Calendar dates"> | |
| <!-- Day labels --> | |
| <div class="day-label" role="columnheader" aria-label="Sunday">Su</div> | |
| <div class="day-label" role="columnheader" aria-label="Monday">Mo</div> | |
| <div class="day-label" role="columnheader" aria-label="Tuesday">Tu</div> | |
| <div class="day-label" role="columnheader" aria-label="Wednesday">We</div> | |
| <div class="day-label" role="columnheader" aria-label="Thursday">Th</div> | |
| <div class="day-label" role="columnheader" aria-label="Friday">Fr</div> | |
| <div class="day-label" role="columnheader" aria-label="Saturday">Sa</div> | |
| <!-- Days will be inserted here --> | |
| </div> | |
| <div class="selected-range" role="status" aria-live="polite"></div> | |
| <button class="clear-button" type="button" style="display: none;">Clear Selection</button> | |
| </div> | |
| </template> | |
| </ui-date-range-picker> | |
| <p style="margin-top: var(--space-1); font-size: var(--fluid--2); color: var(--text-secondary);"> | |
| <strong>Temporal API Features:</strong> Accurate date calculations, timezone-aware, range selection, keyboard navigation (Arrow keys), proper month/year handling, and accessible date announcements. | |
| </p> | |
| </ui-section> | |
| <ui-section> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| padding: var(--space-1); | |
| border-bottom: var(--border-1) solid var(--border-default); | |
| } | |
| :host(:last-child) { | |
| border-bottom: none; | |
| } | |
| ::slotted(h1), ::slotted(h2), ::slotted(h3), | |
| ::slotted(h4), ::slotted(h5), ::slotted(h6) { | |
| margin-top: 0; | |
| margin-bottom: var(--space-0); | |
| } | |
| ::slotted(p) { | |
| margin: 0 0 var(--space-0) 0; | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| } | |
| ::slotted(p:last-child) { | |
| margin-bottom: 0; | |
| } | |
| @media (min-width: 768px) { | |
| :host { | |
| padding: var(--space-2); | |
| } | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <h3>Grid-Based Data Table</h3> | |
| <p>Inspired by TanStack Table, built with CSS Grid for maximum flexibility and control. Features text truncation with tooltips, sortable columns, and maintains grid integrity.</p> | |
| <ui-grid-table columns="40px 1fr 2fr 1fr 100px"> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| width: 100%; | |
| overflow-x: auto; | |
| margin-top: var(--space-1); | |
| } | |
| .grid-table { | |
| display: grid; | |
| grid-template-columns: var(--columns, 1fr 1fr 1fr); | |
| border: var(--border-1) solid var(--border-default); | |
| border-radius: var(--radius-2); | |
| overflow: hidden; | |
| background: var(--canvas-base); | |
| } | |
| /* All cells get base styling */ | |
| ::slotted(.header-cell), | |
| ::slotted(.cell) { | |
| padding: var(--space-0); | |
| border-right: var(--border-1) solid var(--border-default); | |
| border-bottom: var(--border-1) solid var(--border-default); | |
| display: flex; | |
| align-items: center; | |
| min-height: 44px; | |
| overflow: hidden; | |
| } | |
| /* Header cells */ | |
| ::slotted(.header-cell) { | |
| background: var(--canvas-elevated); | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| font-size: var(--fluid--1); | |
| border-bottom: var(--border-2) solid var(--border-default); | |
| position: sticky; | |
| top: 0; | |
| z-index: 10; | |
| } | |
| /* Body cells */ | |
| ::slotted(.cell) { | |
| color: var(--text-secondary); | |
| font-size: var(--fluid--1); | |
| background: var(--canvas-base); | |
| } | |
| /* Remove right border from last column */ | |
| ::slotted(.header-cell:nth-child(5n)), | |
| ::slotted(.cell:nth-child(5n)) { | |
| border-right: none; | |
| } | |
| /* Remove bottom border from last row */ | |
| ::slotted(.cell:nth-last-child(-n+5)) { | |
| border-bottom: none; | |
| } | |
| /* Row hover - target every 5th element starting from position based on column count */ | |
| ::slotted(.cell:hover), | |
| ::slotted(.cell:nth-child(5n+6):hover) ~ ::slotted(.cell:nth-child(-n+10)), | |
| ::slotted(.cell:nth-child(5n+7):hover) ~ ::slotted(.cell:nth-child(-n+10)), | |
| ::slotted(.cell:nth-child(5n+8):hover) ~ ::slotted(.cell:nth-child(-n+10)), | |
| ::slotted(.cell:nth-child(5n+9):hover) ~ ::slotted(.cell:nth-child(-n+10)), | |
| ::slotted(.cell:nth-child(5n+10):hover) ~ ::slotted(.cell:nth-child(-n+10)) { | |
| background: var(--surface-hover); | |
| } | |
| /* Alternating row backgrounds */ | |
| ::slotted(.cell:nth-child(10n+6)), | |
| ::slotted(.cell:nth-child(10n+7)), | |
| ::slotted(.cell:nth-child(10n+8)), | |
| ::slotted(.cell:nth-child(10n+9)), | |
| ::slotted(.cell:nth-child(10n+10)) { | |
| background: var(--surface-default); | |
| } | |
| </style> | |
| <div class="grid-table"> | |
| <slot></slot> | |
| </div> | |
| </template> | |
| <!-- Header Row --> | |
| <div class="header-cell"> | |
| <input type="checkbox" id="grid-select-all" aria-label="Select all rows"> | |
| </div> | |
| <div class="header-cell"> | |
| <button class="sort-button" type="button" aria-sort="ascending" aria-label="Sort by name" title="Click to sort by Name" style="background: none; border: none; padding: 0; font: inherit; color: inherit; cursor: pointer; display: flex; align-items: center; gap: var(--space--2); width: 100%; min-height: 44px;"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;">Name</span> | |
| <span aria-hidden="true" style="flex-shrink: 0;">↑</span> | |
| </button> | |
| </div> | |
| <div class="header-cell"> | |
| <button class="sort-button" type="button" aria-sort="none" aria-label="Sort by email" title="Click to sort by Email" style="background: none; border: none; padding: 0; font: inherit; color: inherit; cursor: pointer; display: flex; align-items: center; gap: var(--space--2); width: 100%; min-height: 44px;"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;">Email Address</span> | |
| <span aria-hidden="true" style="flex-shrink: 0; opacity: 0.3;">↕</span> | |
| </button> | |
| </div> | |
| <div class="header-cell"> | |
| <button class="sort-button" type="button" aria-sort="none" aria-label="Sort by role" title="Click to sort by Role" style="background: none; border: none; padding: 0; font: inherit; color: inherit; cursor: pointer; display: flex; align-items: center; gap: var(--space--2); width: 100%; min-height: 44px;"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;">Role</span> | |
| <span aria-hidden="true" style="flex-shrink: 0; opacity: 0.3;">↕</span> | |
| </button> | |
| </div> | |
| <div class="header-cell"> | |
| <button class="sort-button" type="button" aria-sort="none" aria-label="Sort by status" title="Click to sort by Status" style="background: none; border: none; padding: 0; font: inherit; color: inherit; cursor: pointer; display: flex; align-items: center; gap: var(--space--2); width: 100%; min-height: 44px;"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1;">Status</span> | |
| <span aria-hidden="true" style="flex-shrink: 0; opacity: 0.3;">↕</span> | |
| </button> | |
| </div> | |
| <!-- Row 1 --> | |
| <div class="cell"> | |
| <input type="checkbox" aria-label="Select Alice Johnson"> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Alice Johnson">Alice Johnson</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="[email protected]">[email protected]</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Senior Product Designer">Senior Product Designer</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="display: inline-flex; align-items: center; padding: 2px 8px; background: var(--surface-active); border-radius: var(--radius-1); font-size: var(--fluid--2);" title="Active user">Active</span> | |
| </div> | |
| <!-- Row 2 --> | |
| <div class="cell"> | |
| <input type="checkbox" aria-label="Select Bob Smith"> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Bob Smith">Bob Smith</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="[email protected]">[email protected]</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Full Stack Developer">Full Stack Developer</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="display: inline-flex; align-items: center; padding: 2px 8px; background: var(--surface-active); border-radius: var(--radius-1); font-size: var(--fluid--2);" title="Active user">Active</span> | |
| </div> | |
| <!-- Row 3 --> | |
| <div class="cell"> | |
| <input type="checkbox" aria-label="Select Carol White"> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Carol White">Carol White</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="[email protected]">[email protected]</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Engineering Manager">Engineering Manager</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="display: inline-flex; align-items: center; padding: 2px 8px; background: var(--surface-default); border-radius: var(--radius-1); font-size: var(--fluid--2); color: var(--text-tertiary);" title="Inactive user">Inactive</span> | |
| </div> | |
| <!-- Row 4 --> | |
| <div class="cell"> | |
| <input type="checkbox" aria-label="Select David Brown"> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="David Brown">David Brown</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="[email protected]">[email protected]</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Backend Developer">Backend Developer</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="display: inline-flex; align-items: center; padding: 2px 8px; background: var(--surface-active); border-radius: var(--radius-1); font-size: var(--fluid--2);" title="Active user">Active</span> | |
| </div> | |
| <!-- Row 5 --> | |
| <div class="cell"> | |
| <input type="checkbox" aria-label="Select Emma Davis"> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="Emma Davis">Emma Davis</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="[email protected]">[email protected]</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; width: 100%;" title="UX/UI Designer with specialization in accessibility">UX/UI Designer with specialization in accessibility</span> | |
| </div> | |
| <div class="cell"> | |
| <span style="display: inline-flex; align-items: center; padding: 2px 8px; background: var(--surface-active); border-radius: var(--radius-1); font-size: var(--fluid--2);" title="Active user">Active</span> | |
| </div> | |
| </ui-grid-table> | |
| <p style="margin-top: var(--space-1); font-size: var(--fluid--2); color: var(--text-secondary);"> | |
| <strong>CSS Grid Features:</strong> Custom column sizing (40px 1fr 2fr 1fr 100px), text truncation with tooltips, sticky headers, keyboard navigation, and dividers that maintain grid integrity. Hover over truncated text to see full content. | |
| </p> | |
| </ui-section> | |
| <ui-section> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| padding: var(--space-1); | |
| border-bottom: var(--border-1) solid var(--border-default); | |
| } | |
| :host(:last-child) { | |
| border-bottom: none; | |
| } | |
| ::slotted(h1), ::slotted(h2), ::slotted(h3), | |
| ::slotted(h4), ::slotted(h5), ::slotted(h6) { | |
| margin-top: 0; | |
| margin-bottom: var(--space-0); | |
| } | |
| ::slotted(p) { | |
| margin: 0 0 var(--space-0) 0; | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| } | |
| ::slotted(p:last-child) { | |
| margin-bottom: 0; | |
| } | |
| @media (min-width: 768px) { | |
| :host { | |
| padding: var(--space-2); | |
| } | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <h3>Layout Utilities</h3> | |
| <p>Dividers and spacing components for organizing content.</p> | |
| <p>Horizontal divider below:</p> | |
| <ui-divider> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| width: 100%; | |
| height: var(--border-1); | |
| background: var(--border-default); | |
| margin: var(--space-1) 0; | |
| } | |
| </style> | |
| </template> | |
| </ui-divider> | |
| <p>Content continues after divider</p> | |
| </ui-section> | |
| </ui-content> | |
| <ui-rail slot="trailing"> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: flex; | |
| flex-direction: column; | |
| width: var(--rail-width); | |
| height: 100%; | |
| border-left: var(--border-1) solid var(--border-default); | |
| overflow-y: auto; | |
| background: var(--canvas-elevated); | |
| transition: background 0.2s; | |
| } | |
| </style> | |
| <aside role="complementary" aria-label="Sidebar information"> | |
| <slot></slot> | |
| </aside> | |
| </template> | |
| <ui-section> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| padding: var(--space-1); | |
| border-bottom: var(--border-1) solid var(--border-default); | |
| } | |
| :host(:last-child) { | |
| border-bottom: none; | |
| } | |
| ::slotted(h1), ::slotted(h2), ::slotted(h3), | |
| ::slotted(h4), ::slotted(h5), ::slotted(h6) { | |
| margin-top: 0; | |
| margin-bottom: var(--space-0); | |
| } | |
| ::slotted(p) { | |
| margin: 0 0 var(--space-0) 0; | |
| color: var(--text-secondary); | |
| line-height: 1.6; | |
| } | |
| ::slotted(p:last-child) { | |
| margin-bottom: 0; | |
| } | |
| @media (min-width: 768px) { | |
| :host { | |
| padding: var(--space-2); | |
| } | |
| } | |
| </style> | |
| <slot></slot> | |
| </template> | |
| <h4>Accessibility</h4> | |
| <p>WCAG AA compliant design:</p> | |
| <ul style="margin: var(--space-0) 0 0 0; padding-left: var(--space-1); color: var(--text-secondary); font-size: var(--fluid--2);"> | |
| <li style="margin-bottom: var(--space--2);">4.5:1+ contrast ratios</li> | |
| <li style="margin-bottom: var(--space--2);">Colorblind-safe palette</li> | |
| <li style="margin-bottom: var(--space--2);">Proper ARIA labels</li> | |
| <li style="margin-bottom: var(--space--2);">Keyboard navigation</li> | |
| <li style="margin-bottom: var(--space--2);">44px touch targets</li> | |
| <li style="margin-bottom: var(--space--2);">3-layer focus ring</li> | |
| <li style="margin-bottom: var(--space--2);">High contrast indicators</li> | |
| <li style="margin-bottom: var(--space--2);">CSS Grid data tables</li> | |
| <li style="margin-bottom: var(--space--2);">Date range selection</li> | |
| <li style="margin-bottom: var(--space--2);">Text truncation tooltips</li> | |
| <li style="margin-bottom: var(--space--2);">Screen reader support</li> | |
| </ul> | |
| <ui-divider> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| width: 100%; | |
| height: var(--border-1); | |
| background: var(--border-default); | |
| margin: var(--space-1) 0; | |
| } | |
| </style> | |
| </template> | |
| </ui-divider> | |
| <h4>Theme System</h4> | |
| <p>Built on CSS system colors:</p> | |
| <ul style="margin: var(--space-0) 0 0 0; padding-left: var(--space-1); color: var(--text-secondary);"> | |
| <li style="margin-bottom: var(--space--2);"><code>accent-color</code></li> | |
| <li style="margin-bottom: var(--space--2);"><code>AccentColor</code></li> | |
| <li style="margin-bottom: var(--space--2);"><code>AccentColorText</code></li> | |
| <li style="margin-bottom: var(--space--2);"><code>currentColor</code></li> | |
| </ul> | |
| <ui-divider> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| width: 100%; | |
| height: var(--border-1); | |
| background: var(--border-default); | |
| margin: var(--space-1) 0; | |
| } | |
| </style> | |
| </template> | |
| </ui-divider> | |
| <h5 style="font-size: var(--fluid-0); margin-bottom: var(--space--1);">Accent Color Applies To:</h5> | |
| <ul style="margin: 0; padding-left: var(--space-1); color: var(--text-secondary); font-size: var(--fluid--2);"> | |
| <li style="margin-bottom: var(--space--2);">Checkboxes ✓</li> | |
| <li style="margin-bottom: var(--space--2);">Radio buttons ✓</li> | |
| <li style="margin-bottom: var(--space--2);">Range sliders ✓</li> | |
| <li style="margin-bottom: var(--space--2);">Progress bars ✓</li> | |
| <li style="margin-bottom: var(--space--2);">Select dropdowns ✓</li> | |
| <li style="margin-bottom: var(--space--2);">Date pickers ✓</li> | |
| <li style="margin-bottom: var(--space--2);">Buttons (custom) ✓</li> | |
| <li style="margin-bottom: var(--space--2);">Active states ✓</li> | |
| <li style="margin-bottom: var(--space--2);">Focus outlines ✓</li> | |
| <li style="margin-bottom: var(--space--2);">Grid table sorting ✓</li> | |
| </ul> | |
| </ui-section> | |
| </ui-rail> | |
| <ui-footer slot="footer" status="WCAG AA Compliant • Colorblind-Safe • Keyboard Accessible"> | |
| <template shadowrootmode="open"> | |
| <style> | |
| :host { | |
| display: block; | |
| background: var(--canvas-elevated); | |
| border-top: var(--border-1) solid var(--border-default); | |
| transition: background 0.2s; | |
| } | |
| .footer { | |
| display: flex; | |
| align-items: center; | |
| min-height: var(--footer-height); | |
| padding: 0 var(--space-1); | |
| font-size: var(--fluid--2); | |
| color: var(--text-tertiary); | |
| } | |
| @media (min-width: 768px) { | |
| .footer { padding: 0 var(--space-2); } | |
| } | |
| </style> | |
| <footer role="contentinfo" class="footer"></footer> | |
| </template> | |
| </ui-footer> | |
| </ui-layout> | |
| </div> | |
| <script> | |
| // ============================================ | |
| // THEME MANAGEMENT | |
| // ============================================ | |
| function initTheme() { | |
| const saved = localStorage.getItem('theme') || 'auto'; | |
| document.documentElement.setAttribute('data-theme', saved); | |
| } | |
| function setTheme(theme) { | |
| document.documentElement.setAttribute('data-theme', theme); | |
| localStorage.setItem('theme', theme); | |
| // Update all theme switcher buttons | |
| document.querySelectorAll('ui-theme-switcher').forEach(switcher => { | |
| const buttons = switcher.shadowRoot.querySelectorAll('button'); | |
| buttons.forEach(btn => { | |
| const btnTheme = btn.getAttribute('data-theme'); | |
| btn.classList.toggle('active', btnTheme === theme); | |
| }); | |
| }); | |
| } | |
| // Initialize theme on load | |
| initTheme(); | |
| // Set up theme switcher and accent color picker after DOM loads | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Accent color picker | |
| const picker = document.getElementById('accent-picker'); | |
| if (picker) { | |
| picker.addEventListener('input', (e) => { | |
| const color = e.target.value; | |
| document.documentElement.style.setProperty('accent-color', color); | |
| document.documentElement.style.setProperty('--accent', color); | |
| document.documentElement.style.setProperty('--focus-ring-middle', color); | |
| }); | |
| } | |
| }); | |
| // ============================================ | |
| // COMPONENT DEFINITIONS | |
| // All use Declarative Shadow DOM | |
| // JavaScript only for attribute observation | |
| // ============================================ | |
| class UiLayout extends HTMLElement { | |
| constructor() { super(); } | |
| } | |
| class UiAppBar extends HTMLElement { | |
| static observedAttributes = ["title"]; | |
| constructor() { super(); } | |
| connectedCallback() { this.updateTitle(); } | |
| attributeChangedCallback(name, oldValue, newValue) { | |
| if (name === "title" && oldValue !== newValue) this.updateTitle(); | |
| } | |
| updateTitle() { | |
| const title = this.getAttribute("title") || ""; | |
| const titleEl = this.shadowRoot?.querySelector(".title"); | |
| if (titleEl) titleEl.textContent = title; | |
| } | |
| } | |
| class UiNavigation extends HTMLElement { | |
| constructor() { super(); } | |
| } | |
| class UiNavMenu extends HTMLElement { | |
| constructor() { super(); } | |
| } | |
| class UiNavItem extends HTMLElement { | |
| static observedAttributes = ["active"]; | |
| constructor() { super(); } | |
| connectedCallback() { | |
| this.updateActive(); | |
| // Add click handler to update aria-current | |
| const link = this.shadowRoot?.querySelector('.nav-item'); | |
| if (link) { | |
| link.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| // Remove aria-current from all nav items | |
| document.querySelectorAll('ui-nav-item').forEach(item => { | |
| item.removeAttribute('active'); | |
| const itemLink = item.shadowRoot?.querySelector('.nav-item'); | |
| if (itemLink) { | |
| itemLink.classList.remove('active'); | |
| itemLink.removeAttribute('aria-current'); | |
| } | |
| }); | |
| // Add to clicked item | |
| this.setAttribute('active', ''); | |
| link.classList.add('active'); | |
| link.setAttribute('aria-current', 'page'); | |
| }); | |
| } | |
| } | |
| attributeChangedCallback(name) { | |
| if (name === "active") this.updateActive(); | |
| } | |
| updateActive() { | |
| const item = this.shadowRoot?.querySelector(".nav-item"); | |
| if (item) { | |
| const isActive = this.hasAttribute("active"); | |
| item.classList.toggle("active", isActive); | |
| if (isActive) { | |
| item.setAttribute('aria-current', 'page'); | |
| } else { | |
| item.removeAttribute('aria-current'); | |
| } | |
| } | |
| } | |
| } | |
| class UiContent extends HTMLElement { | |
| constructor() { super(); } | |
| } | |
| class UiSection extends HTMLElement { | |
| constructor() { super(); } | |
| } | |
| class UiRail extends HTMLElement { | |
| constructor() { super(); } | |
| } | |
| class UiFooter extends HTMLElement { | |
| static observedAttributes = ["status"]; | |
| constructor() { super(); } | |
| connectedCallback() { this.updateStatus(); } | |
| attributeChangedCallback(name, oldValue, newValue) { | |
| if (name === "status" && oldValue !== newValue) this.updateStatus(); | |
| } | |
| updateStatus() { | |
| const status = this.getAttribute("status") || ""; | |
| const footerEl = this.shadowRoot?.querySelector(".footer"); | |
| if (footerEl) footerEl.textContent = status; | |
| } | |
| } | |
| // Utility components - pure declarative | |
| class UiDivider extends HTMLElement { | |
| constructor() { super(); } | |
| } | |
| class UiButtonStack extends HTMLElement { | |
| constructor() { super(); } | |
| } | |
| class UiInputGroup extends HTMLElement { | |
| constructor() { super(); } | |
| } | |
| class UiThemeSwitcher extends HTMLElement { | |
| constructor() { | |
| super(); | |
| } | |
| connectedCallback() { | |
| // Attach event listeners to buttons in shadow DOM | |
| const buttons = this.shadowRoot.querySelectorAll('button'); | |
| buttons.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const theme = btn.getAttribute('data-theme'); | |
| setTheme(theme); | |
| }); | |
| }); | |
| // Update initial active state | |
| const currentTheme = document.documentElement.getAttribute('data-theme') || 'auto'; | |
| buttons.forEach(btn => { | |
| const theme = btn.getAttribute('data-theme'); | |
| btn.classList.toggle('active', theme === currentTheme); | |
| }); | |
| } | |
| } | |
| class UiGridTable extends HTMLElement { | |
| static observedAttributes = ['columns']; | |
| constructor() { | |
| super(); | |
| } | |
| connectedCallback() { | |
| // Set grid columns from attribute | |
| const columns = this.getAttribute('columns') || '1fr'; | |
| const gridTable = this.shadowRoot?.querySelector('.grid-table'); | |
| if (gridTable) { | |
| gridTable.style.setProperty('--columns', columns); | |
| } | |
| this.setupSorting(); | |
| this.setupRowSelection(); | |
| this.setupSelectAll(); | |
| this.setupKeyboardNavigation(); | |
| } | |
| attributeChangedCallback(name, oldValue, newValue) { | |
| if (name === 'columns' && oldValue !== newValue) { | |
| const gridTable = this.shadowRoot?.querySelector('.grid-table'); | |
| if (gridTable) { | |
| gridTable.style.setProperty('--columns', newValue); | |
| } | |
| } | |
| } | |
| setupSorting() { | |
| const sortButtons = this.querySelectorAll('.sort-button'); | |
| sortButtons.forEach((button, columnIndex) => { | |
| button.addEventListener('click', () => { | |
| const currentSort = button.getAttribute('aria-sort'); | |
| // Reset all other columns | |
| sortButtons.forEach(btn => { | |
| if (btn !== button) { | |
| btn.setAttribute('aria-sort', 'none'); | |
| const icon = btn.querySelector('span[aria-hidden]'); | |
| if (icon) { | |
| icon.textContent = '↕'; | |
| icon.style.opacity = '0.3'; | |
| } | |
| } | |
| }); | |
| // Toggle current column | |
| const newSort = currentSort === 'ascending' ? 'descending' : 'ascending'; | |
| button.setAttribute('aria-sort', newSort); | |
| const icon = button.querySelector('span[aria-hidden]'); | |
| if (icon) { | |
| icon.textContent = newSort === 'ascending' ? '↑' : '↓'; | |
| icon.style.opacity = '1'; | |
| } | |
| // Sort the grid | |
| this.sortGrid(columnIndex, newSort === 'ascending'); | |
| }); | |
| }); | |
| } | |
| sortGrid(columnIndex, ascending) { | |
| const allCells = Array.from(this.querySelectorAll('.cell')); | |
| const numColumns = this.getAttribute('columns').split(' ').length; | |
| // Group cells into rows | |
| const rows = []; | |
| for (let i = 0; i < allCells.length; i += numColumns) { | |
| rows.push(allCells.slice(i, i + numColumns)); | |
| } | |
| // Sort rows based on column | |
| rows.sort((a, b) => { | |
| const aText = a[columnIndex]?.textContent.trim() || ''; | |
| const bText = b[columnIndex]?.textContent.trim() || ''; | |
| return ascending ? | |
| aText.localeCompare(bText) : | |
| bText.localeCompare(aText); | |
| }); | |
| // Re-append sorted cells | |
| const headerCells = this.querySelectorAll('.header-cell'); | |
| rows.forEach(row => { | |
| row.forEach(cell => this.appendChild(cell)); | |
| }); | |
| } | |
| setupRowSelection() { | |
| const checkboxes = this.querySelectorAll('.cell input[type="checkbox"]'); | |
| checkboxes.forEach(checkbox => { | |
| checkbox.addEventListener('change', () => { | |
| this.updateSelectAllState(); | |
| }); | |
| }); | |
| } | |
| setupSelectAll() { | |
| const selectAll = this.querySelector('#grid-select-all'); | |
| if (selectAll) { | |
| selectAll.addEventListener('change', () => { | |
| const checkboxes = this.querySelectorAll('.cell input[type="checkbox"]'); | |
| checkboxes.forEach(checkbox => { | |
| checkbox.checked = selectAll.checked; | |
| }); | |
| }); | |
| } | |
| } | |
| updateSelectAllState() { | |
| const selectAll = this.querySelector('#grid-select-all'); | |
| const checkboxes = Array.from(this.querySelectorAll('.cell input[type="checkbox"]')); | |
| const checkedCount = checkboxes.filter(cb => cb.checked).length; | |
| if (selectAll) { | |
| selectAll.checked = checkedCount === checkboxes.length; | |
| selectAll.indeterminate = checkedCount > 0 && checkedCount < checkboxes.length; | |
| } | |
| } | |
| setupKeyboardNavigation() { | |
| // Grid keyboard navigation would go here | |
| } | |
| } | |
| class UiDateRangePicker extends HTMLElement { | |
| constructor() { | |
| super(); | |
| this.startDate = null; | |
| this.endDate = null; | |
| this.currentMonth = null; | |
| this.currentYear = null; | |
| } | |
| connectedCallback() { | |
| // Check for Temporal API support | |
| if (typeof Temporal === 'undefined') { | |
| console.warn('Temporal API not available. Using Date fallback.'); | |
| this.useFallback = true; | |
| } | |
| this.initializeCalendar(); | |
| this.render(); | |
| this.setupEventListeners(); | |
| } | |
| initializeCalendar() { | |
| if (this.useFallback) { | |
| const now = new Date(); | |
| this.currentMonth = now.getMonth(); | |
| this.currentYear = now.getFullYear(); | |
| } else { | |
| const now = Temporal.Now.plainDateISO(); | |
| this.currentMonth = now.month; | |
| this.currentYear = now.year; | |
| } | |
| } | |
| render() { | |
| const monthYear = this.shadowRoot.querySelector('.month-year'); | |
| const grid = this.shadowRoot.querySelector('.calendar-grid'); | |
| const selectedRange = this.shadowRoot.querySelector('.selected-range'); | |
| if (!grid) return; | |
| // Update month/year display | |
| const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', | |
| 'July', 'August', 'September', 'October', 'November', 'December']; | |
| if (this.useFallback) { | |
| monthYear.textContent = `${monthNames[this.currentMonth]} ${this.currentYear}`; | |
| } else { | |
| monthYear.textContent = `${monthNames[this.currentMonth - 1]} ${this.currentYear}`; | |
| } | |
| // Clear existing days (keep day labels) | |
| const dayLabels = grid.querySelectorAll('.day-label'); | |
| grid.innerHTML = ''; | |
| dayLabels.forEach(label => grid.appendChild(label)); | |
| // Calculate calendar | |
| const firstDay = this.useFallback ? | |
| new Date(this.currentYear, this.currentMonth, 1).getDay() : | |
| Temporal.PlainDate.from({ year: this.currentYear, month: this.currentMonth, day: 1 }).dayOfWeek % 7; | |
| const daysInMonth = this.useFallback ? | |
| new Date(this.currentYear, this.currentMonth + 1, 0).getDate() : | |
| Temporal.PlainDate.from({ year: this.currentYear, month: this.currentMonth, day: 1 }).daysInMonth; | |
| const today = this.useFallback ? | |
| { year: new Date().getFullYear(), month: new Date().getMonth(), day: new Date().getDate() } : | |
| Temporal.Now.plainDateISO(); | |
| // Previous month days | |
| const prevMonthDays = this.useFallback ? | |
| new Date(this.currentYear, this.currentMonth, 0).getDate() : | |
| Temporal.PlainDate.from({ | |
| year: this.currentMonth === 1 ? this.currentYear - 1 : this.currentYear, | |
| month: this.currentMonth === 1 ? 12 : this.currentMonth - 1, | |
| day: 1 | |
| }).daysInMonth; | |
| for (let i = firstDay - 1; i >= 0; i--) { | |
| const day = prevMonthDays - i; | |
| this.createDayButton(day, true, grid); | |
| } | |
| // Current month days | |
| for (let day = 1; day <= daysInMonth; day++) { | |
| const isToday = this.useFallback ? | |
| (this.currentYear === today.year && this.currentMonth === today.month && day === today.day) : | |
| (this.currentYear === today.year && this.currentMonth === today.month && day === today.day); | |
| this.createDayButton(day, false, grid, isToday); | |
| } | |
| // Next month days | |
| const totalCells = firstDay + daysInMonth; | |
| const remainingCells = totalCells % 7 === 0 ? 0 : 7 - (totalCells % 7); | |
| for (let day = 1; day <= remainingCells; day++) { | |
| this.createDayButton(day, true, grid); | |
| } | |
| // Update selected range display | |
| this.updateRangeDisplay(); | |
| } | |
| createDayButton(day, isOtherMonth, grid, isToday = false) { | |
| const button = document.createElement('button'); | |
| button.className = `day ${isOtherMonth ? 'other-month' : ''} ${isToday ? 'today' : ''}`; | |
| button.textContent = day; | |
| button.type = 'button'; | |
| button.setAttribute('role', 'gridcell'); | |
| if (!isOtherMonth) { | |
| const dateStr = this.useFallback ? | |
| `${this.currentYear}-${String(this.currentMonth + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}` : | |
| `${this.currentYear}-${String(this.currentMonth).padStart(2, '0')}-${String(day).padStart(2, '0')}`; | |
| button.dataset.date = dateStr; | |
| button.setAttribute('aria-label', this.formatDateForAria(this.currentYear, this.currentMonth, day)); | |
| // Check if in range | |
| if (this.startDate && this.endDate) { | |
| const currentDate = this.useFallback ? new Date(dateStr) : Temporal.PlainDate.from(dateStr); | |
| const start = this.useFallback ? new Date(this.startDate) : Temporal.PlainDate.from(this.startDate); | |
| const end = this.useFallback ? new Date(this.endDate) : Temporal.PlainDate.from(this.endDate); | |
| if (this.useFallback) { | |
| if (currentDate >= start && currentDate <= end) { | |
| button.classList.add('in-range'); | |
| } | |
| if (currentDate.getTime() === start.getTime()) { | |
| button.classList.add('selected-start'); | |
| } | |
| if (currentDate.getTime() === end.getTime()) { | |
| button.classList.add('selected-end'); | |
| } | |
| } else { | |
| const comp1 = Temporal.PlainDate.compare(currentDate, start); | |
| const comp2 = Temporal.PlainDate.compare(currentDate, end); | |
| if (comp1 >= 0 && comp2 <= 0) { | |
| button.classList.add('in-range'); | |
| } | |
| if (comp1 === 0) { | |
| button.classList.add('selected-start'); | |
| } | |
| if (comp2 === 0) { | |
| button.classList.add('selected-end'); | |
| } | |
| } | |
| } else if (this.startDate) { | |
| const currentDate = this.useFallback ? new Date(dateStr) : Temporal.PlainDate.from(dateStr); | |
| const start = this.useFallback ? new Date(this.startDate) : Temporal.PlainDate.from(this.startDate); | |
| if (this.useFallback) { | |
| if (currentDate.getTime() === start.getTime()) { | |
| button.classList.add('selected-start'); | |
| } | |
| } else { | |
| if (Temporal.PlainDate.compare(currentDate, start) === 0) { | |
| button.classList.add('selected-start'); | |
| } | |
| } | |
| } | |
| button.addEventListener('click', () => this.selectDate(dateStr)); | |
| } else { | |
| button.disabled = true; | |
| button.classList.add('disabled'); | |
| } | |
| grid.appendChild(button); | |
| } | |
| formatDateForAria(year, month, day) { | |
| const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', | |
| 'July', 'August', 'September', 'October', 'November', 'December']; | |
| const monthIndex = this.useFallback ? month : month - 1; | |
| return `${monthNames[monthIndex]} ${day}, ${year}`; | |
| } | |
| selectDate(dateStr) { | |
| if (!this.startDate || (this.startDate && this.endDate)) { | |
| // Start new selection | |
| this.startDate = dateStr; | |
| this.endDate = null; | |
| } else { | |
| // Complete the range | |
| const start = this.useFallback ? new Date(this.startDate) : Temporal.PlainDate.from(this.startDate); | |
| const end = this.useFallback ? new Date(dateStr) : Temporal.PlainDate.from(dateStr); | |
| // Ensure start is before end | |
| if (this.useFallback) { | |
| if (end < start) { | |
| this.endDate = this.startDate; | |
| this.startDate = dateStr; | |
| } else { | |
| this.endDate = dateStr; | |
| } | |
| } else { | |
| if (Temporal.PlainDate.compare(end, start) < 0) { | |
| this.endDate = this.startDate; | |
| this.startDate = dateStr; | |
| } else { | |
| this.endDate = dateStr; | |
| } | |
| } | |
| } | |
| this.render(); | |
| } | |
| updateRangeDisplay() { | |
| const rangeDisplay = this.shadowRoot.querySelector('.selected-range'); | |
| const clearButton = this.shadowRoot.querySelector('.clear-button'); | |
| if (!rangeDisplay) return; | |
| if (this.startDate && this.endDate) { | |
| const start = new Date(this.startDate).toLocaleDateString('en-US', { | |
| month: 'short', day: 'numeric', year: 'numeric' | |
| }); | |
| const end = new Date(this.endDate).toLocaleDateString('en-US', { | |
| month: 'short', day: 'numeric', year: 'numeric' | |
| }); | |
| rangeDisplay.innerHTML = `<strong>Selected:</strong> ${start} → ${end}`; | |
| clearButton.style.display = 'block'; | |
| } else if (this.startDate) { | |
| const start = new Date(this.startDate).toLocaleDateString('en-US', { | |
| month: 'short', day: 'numeric', year: 'numeric' | |
| }); | |
| rangeDisplay.innerHTML = `<strong>Start:</strong> ${start} <em>(select end date)</em>`; | |
| clearButton.style.display = 'block'; | |
| } else { | |
| rangeDisplay.textContent = 'Select a date range'; | |
| clearButton.style.display = 'none'; | |
| } | |
| } | |
| setupEventListeners() { | |
| const prevButton = this.shadowRoot.querySelectorAll('.nav-button')[0]; | |
| const nextButton = this.shadowRoot.querySelectorAll('.nav-button')[1]; | |
| const clearButton = this.shadowRoot.querySelector('.clear-button'); | |
| prevButton?.addEventListener('click', () => { | |
| if (this.useFallback) { | |
| this.currentMonth--; | |
| if (this.currentMonth < 0) { | |
| this.currentMonth = 11; | |
| this.currentYear--; | |
| } | |
| } else { | |
| if (this.currentMonth === 1) { | |
| this.currentMonth = 12; | |
| this.currentYear--; | |
| } else { | |
| this.currentMonth--; | |
| } | |
| } | |
| this.render(); | |
| }); | |
| nextButton?.addEventListener('click', () => { | |
| if (this.useFallback) { | |
| this.currentMonth++; | |
| if (this.currentMonth > 11) { | |
| this.currentMonth = 0; | |
| this.currentYear++; | |
| } | |
| } else { | |
| if (this.currentMonth === 12) { | |
| this.currentMonth = 1; | |
| this.currentYear++; | |
| } else { | |
| this.currentMonth++; | |
| } | |
| } | |
| this.render(); | |
| }); | |
| clearButton?.addEventListener('click', () => { | |
| this.startDate = null; | |
| this.endDate = null; | |
| this.render(); | |
| }); | |
| } | |
| } | |
| // Register all components | |
| customElements.define("ui-layout", UiLayout); | |
| customElements.define("ui-appbar", UiAppBar); | |
| customElements.define("ui-navigation", UiNavigation); | |
| customElements.define("ui-nav-menu", UiNavMenu); | |
| customElements.define("ui-nav-item", UiNavItem); | |
| customElements.define("ui-content", UiContent); | |
| customElements.define("ui-section", UiSection); | |
| customElements.define("ui-rail", UiRail); | |
| customElements.define("ui-footer", UiFooter); | |
| customElements.define("ui-divider", UiDivider); | |
| customElements.define("ui-button-stack", UiButtonStack); | |
| customElements.define("ui-input-group", UiInputGroup); | |
| customElements.define("ui-theme-switcher", UiThemeSwitcher); | |
| customElements.define("ui-grid-table", UiGridTable); | |
| customElements.define("ui-date-range-picker", UiDateRangePicker); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment