diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..05934b4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,174 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +@design.estate/dees-catalog is a comprehensive web components library built with TypeScript and LitElement. It provides a large collection of UI components for building modern web applications with consistent design and behavior. + +## Build and Development Commands + +```bash +# Install dependencies +pnpm install + +# Build the project +pnpm run build +# This runs: tsbuild tsfolders --allowimplicitany && tsbundle element --production --bundler esbuild + +# Run development watch mode +pnpm run watch +# This runs: tswatch element + +# Run tests (browser tests) +pnpm test +# This runs: tstest test/ --web --verbose --timeout 30 --logfile + +# Run a specific test file +tsx test/test.wysiwyg-basic.browser.ts --verbose + +# Build documentation +pnpm run buildDocs +``` + +### Testing Notes +- Test files follow the pattern: `test.*.browser.ts`, `test.*.node.ts`, or `test.*.both.ts` +- Browser tests run in a headless browser environment +- Use `--logfile` option to store logs in `.nogit/testlogs/` +- For debugging, create files in `.nogit/debug/` and run with `tsx` + +## Architecture Overview + +### Component Structure +The library is organized into several categories: + +1. **Core UI Components** (`dees-button`, `dees-badge`, `dees-icon`, etc.) + - Basic building blocks with consistent theming + - All support light/dark themes via `cssManager.bdTheme()` + +2. **Form Components** (`dees-form`, `dees-input-*`) + - Complete form system with validation + - Base class `DeesInputBase` provides common functionality + - Form data collection via `DeesForm` container + +3. **Layout Components** (`dees-appui-*`) + - Application shell components + - `DeesAppuiBase` orchestrates the entire layout + - Grid-based responsive design + +4. **Data Display** (`dees-table`, `dees-dataview-*`, `dees-statsgrid`) + - Complex data visualization components + - Interactive tables with sorting/filtering + - Chart components using ApexCharts + +5. **Overlays** (`dees-modal`, `dees-contextmenu`, `dees-toast`) + - Managed by central z-index registry + - Window layer system for proper stacking + +### Key Architectural Patterns + +#### Z-Index Management +All overlay components use a centralized z-index registry system: +- Definition in `ts_web/elements/00zindex.ts` +- Dynamic z-index assignment via `ZIndexRegistry` class +- Components get z-index from registry when showing +- Ensures proper stacking order (dropdowns above modals, etc.) + +#### Theme System +- All components support light/dark themes +- Use `cssManager.bdTheme(lightValue, darkValue)` for theme-aware colors +- Consistent color palette defined in `00colors.ts` + +#### Component Demo System +- Each component has a static `demo` property +- Demo functions in separate `.demo.ts` files +- Showcase pages aggregate demos (e.g., `input-showcase.ts`) + +#### WYSIWYG Editor Architecture +The WYSIWYG editor uses a sophisticated architecture with separated concerns: +- **Main Component**: `dees-input-wysiwyg.ts` - Orchestrates the editor +- **Handler Classes**: + - `WysiwygInputHandler` - Handles text input and block transformations + - `WysiwygKeyboardHandler` - Manages keyboard shortcuts and navigation + - `WysiwygDragDropHandler` - Manages block reordering + - `WysiwygModalManager` - Shows configuration modals + - `WysiwygBlockOperations` - Core block manipulation logic +- **Global Menus**: + - `DeesSlashMenu` and `DeesFormattingMenu` render globally to avoid focus issues + - Singleton pattern ensures single instance +- **Programmatic Rendering**: Uses manual DOM manipulation to prevent focus loss + +### Component Communication +- Custom events for parent-child communication +- Form components emit standardized events (`change`, `blur`, etc.) +- Complex components like `DeesAppuiBase` re-emit child events + +### Build System +- TypeScript compilation with decorators support +- Web component bundling with esbuild +- Element exports in `ts_web/elements/index.ts` +- Distribution builds in `dist_ts_web/` + +## Important Implementation Details + +### When Creating New Components +1. Extend `DeesElement` from `@design.estate/dees-element` +2. Use `@customElement('dees-componentname')` decorator +3. Implement theme support with `cssManager.bdTheme()` +4. Create a demo function in a separate `.demo.ts` file +5. Export from `elements/index.ts` + +### Form Input Components +1. Extend `DeesInputBase` for form inputs +2. Implement `getValue()` and `setValue()` methods +3. Use `changeSubject.next(this)` to emit changes +4. Support `disabled` and `required` properties + +### Overlay Components +1. Import z-index from `00zindex.ts` +2. Get z-index from registry when showing: `zIndexRegistry.getNextZIndex()` +3. Register/unregister with the registry +4. Use `DeesWindowLayer` for backdrop if needed + +### Testing Components +1. Create test files in `test/` directory +2. Use `@git.zone/tstest` with tap-bundle +3. Test in browser environment for web components +4. Use proper async/await for component lifecycle + +## Common Patterns and Pitfalls + +### Focus Management +- WYSIWYG editor uses programmatic rendering to prevent focus loss +- Use `requestAnimationFrame` for timing-sensitive focus operations +- Avoid reactive re-renders during user input + +### Event Handling +- Prevent event bubbling in nested interactive components +- Use `pointer-events: none/auto` for click-through behavior +- Handle both mouse and keyboard events for accessibility + +### Performance Considerations +- Large components (editor, terminal) use lazy loading +- Charts use debounced resize observers +- Tables implement virtual scrolling for large datasets + +## File Organization +``` +ts_web/ +├── elements/ # All component files +│ ├── 00*.ts # Shared utilities (colors, z-index, plugins) +│ ├── dees-*.ts # Component implementations +│ ├── dees-*.demo.ts # Component demos +│ ├── interfaces/ # Shared TypeScript interfaces +│ ├── helperclasses/ # Utility classes (FormController) +│ └── wysiwyg/ # WYSIWYG editor subsystem +├── pages/ # Demo showcase pages +└── index.ts # Main export file +``` + +## Recent Major Changes +- Z-Index Registry System (2025-12-24): Dynamic stacking order management +- WYSIWYG Refactoring (2025-06-24): Complete architecture overhaul with separated concerns +- Form System Enhancement: Unified validation and data collection +- Theme System: Consistent light/dark theme support across all components \ No newline at end of file diff --git a/package.json b/package.json index 69b34a0..7137c5f 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "typings": "dist_ts_web/index.d.ts", "type": "module", "scripts": { - "test": "tstest test/ --web --verbose --timeout 30", + "test": "tstest test/ --web --verbose --timeout 30 --logfile", "build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production --bundler esbuild", "watch": "tswatch element", "buildDocs": "tsdoc" diff --git a/readme.hints.md b/readme.hints.md index f8faf34..9146783 100644 --- a/readme.hints.md +++ b/readme.hints.md @@ -1,5 +1,5 @@ !!! Please pay attention to the following points when writing the readme: !!! -* Give a short rundown of components and a few points abputspecific features on each. +* Give a short rundown of components and a few points abput specific features on each. * Try to list all components in a summary. * Then list all components with a short description. diff --git a/ts_web/elements/dees-button.demo.ts b/ts_web/elements/dees-button.demo.ts index 8887a1a..373161d 100644 --- a/ts_web/elements/dees-button.demo.ts +++ b/ts_web/elements/dees-button.demo.ts @@ -1,77 +1,301 @@ -import { html, css } from '@design.estate/dees-element'; +import { html, css, cssManager } from '@design.estate/dees-element'; +import '@design.estate/dees-wcctools/demotools'; +import './dees-panel.js'; +import './dees-form.js'; +import './dees-form-submit.js'; +import './dees-input-text.js'; +import './dees-icon.js'; export const demoFunc = () => html` - + +
+ +
+ Default + Secondary + Destructive + Outline + Ghost + Link Button +
+
- .button-group { - display: flex; - gap: 16px; - margin: 20px 0; - } - `} - - -

Button Types

- This is a slotted Text -

- Highlighted -

-

This is discreete button

-

This is a disabled button

-

This is a slotted Text

- -

Button States

-

Normal Status

-

Pending Status

-

Success Status

-

Error Status

- -

Buttons in Forms (Auto-spacing)

-
- - - - Save Draft - Save and Continue - Submit Form - -
- -

Buttons Outside Forms (No auto-spacing)

-
- Button 1 - Button 2 - Button 3 -
- -

Manual Form Spacing

-
- Manually spaced button 1 - Manually spaced button 2 -
+ +
+ Small Button + Default Size + Large Button + +
+ +
+ Small Secondary + Default Destructive + Large Outline +
+
+ + +
+ + + Add Item + + + + Delete + + + + Download + +
+ +
+ + + Settings + + + + Back + + + Next + + +
+ +
+ + + + + + + + + + + + + + + +
+
+ + +
+ Normal + Processing... + Success! + Error! + Disabled +
+ +
+ Small Loading + Default Loading + Large Loading +
+
+ + +
+ { + const output = document.querySelector('#click-output'); + if (output) { + output.textContent = `Clicked: Default button at ${new Date().toLocaleTimeString()}`; + } + }} + > + Click Me + + + { + const output = document.querySelector('#click-output'); + if (output) { + output.textContent = `Clicked: Secondary button with data: ${e.detail.data}`; + } + }} + > + Click with Data + + + { + const output = document.querySelector('#click-output'); + if (output) { + output.textContent = 'Processing...'; + await new Promise(resolve => setTimeout(resolve, 2000)); + output.textContent = 'Action completed!'; + } + }} + > + Async Action + +
+ +
+ Click a button to see the result... +
+
+ + + { + const output = document.querySelector('#form-output'); + if (output) { + output.innerHTML = 'Form submitted with data:
' + + JSON.stringify(e.detail.data, null, 2); + } + }}> + + + + + Save Draft + Cancel + Submit Form +
+ +
+ Submit the form to see the data... +
+
+ + +
+ Normal → Default + Highlighted → Destructive + Discreet → Outline + Big → Large Size +
+ +

+ These legacy type values are maintained for backward compatibility but we recommend using the new variant system. +

+
+ + +
+
+

Action Group

+ + + Save Changes + + + + Discard + + + + Help + +
+ +
+

Danger Zone

+ + + Delete Account + + + + Archive Data + + + + Not Available + +
+
+ +
+

Code Example:

+
+ <dees-button type="default" size="sm" @clicked="\${handleClick}">
+   <dees-icon iconFA="faSave"></dees-icon>
+   Save Changes
+ </dees-button> +
+
+
+
+ `; diff --git a/ts_web/elements/dees-button.ts b/ts_web/elements/dees-button.ts index e65ba1f..862c999 100644 --- a/ts_web/elements/dees-button.ts +++ b/ts_web/elements/dees-button.ts @@ -48,7 +48,12 @@ export class DeesButton extends DeesElement { @property({ type: String }) - public type: 'normal' | 'highlighted' | 'discreet' | 'big' = 'normal'; + public type: 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost' | 'link' | 'normal' | 'highlighted' | 'discreet' | 'big' = 'default'; + + @property({ + type: String + }) + public size: 'default' | 'sm' | 'lg' | 'icon' = 'default'; @property({ type: String @@ -77,25 +82,23 @@ export class DeesButton extends DeesElement { cssManager.defaultStyles, css` :host { - display: block; + display: inline-block; box-sizing: border-box; - font-family: 'Geist Sans', 'monospace'; + font-family: inherit; } :host([hidden]) { display: none; } /* Form spacing styles */ - /* Default vertical form layout */ :host([inside-form]) { - margin-bottom: 16px; /* Using standard 16px like inputs */ + margin-bottom: 16px; } :host([inside-form]:last-child) { margin-bottom: 0; } - /* Horizontal form layout - auto-detected via parent */ dees-form[horizontal-layout] :host([inside-form]) { display: inline-block; margin-right: 16px; @@ -107,114 +110,260 @@ export class DeesButton extends DeesElement { } .button { - transition: all 0.1s , color 0s; position: relative; - font-size: 14px; - font-weight: 400; - display: flex; - justify-content: center; + display: inline-flex; align-items: center; - background: ${cssManager.bdTheme('#fff', '#333')}; - box-shadow: ${cssManager.bdTheme('0px 1px 3px rgba(0,0,0,0.3)', 'none')}; - border: 1px solid ${cssManager.bdTheme('#eee', '#333')}; - border-top: ${cssManager.bdTheme('1px solid #eee', '1px solid #444')}; - border-radius: 4px; - height: 40px; - padding: 0px 8px; - min-width: 100px; + justify-content: center; + white-space: nowrap; + border-radius: 6px; + font-weight: 500; + transition: all 0.15s ease; + cursor: pointer; user-select: none; - color: ${cssManager.bdTheme('#333', ' #ccc')}; - max-width: 500px; + outline: none; + letter-spacing: -0.01em; + gap: 8px; } - .button:hover { - background: #0050b9; - color: #ffffff; - border: 1px solid #0050b9; - border-top: 1px solid #0050b9; + /* Size variants */ + .button.size-default { + height: 36px; + padding: 0 16px; + font-size: 14px; } - .button:active { - background: #0069f2; - border-top: 1px solid #0069f2; + .button.size-sm { + height: 32px; + padding: 0 12px; + font-size: 13px; } - .button.highlighted { - background: #e4002b; + .button.size-lg { + height: 44px; + padding: 0 24px; + font-size: 16px; + } + + .button.size-icon { + height: 36px; + width: 36px; + padding: 0; + } + + /* Default variant */ + .button.default { + background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20.2% 11.8%)')}; + color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; + border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 16.8%)')}; + box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + } + + .button.default:hover:not(.disabled) { + background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 10.2%)')}; + border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 20%)')}; + } + + .button.default:active:not(.disabled) { + background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 9%)')}; + } + + /* Destructive variant */ + .button.destructive { + background: hsl(0 84.2% 60.2%); + color: hsl(0 0% 98%); + border: 1px solid transparent; + } + + .button.destructive:hover:not(.disabled) { + background: hsl(0 84.2% 56.2%); + } + + .button.destructive:active:not(.disabled) { + background: hsl(0 84.2% 52.2%); + } + + /* Outline variant */ + .button.outline { + background: transparent; + color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; + border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')}; + } + + .button.outline:hover:not(.disabled) { + background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')}; + border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 26.8%)')}; + } + + .button.outline:active:not(.disabled) { + background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 13.8%)')}; + } + + /* Secondary variant */ + .button.secondary { + background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')}; + color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; + border: 1px solid transparent; + } + + .button.secondary:hover:not(.disabled) { + background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 13.8%)')}; + } + + .button.secondary:active:not(.disabled) { + background: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 11.8%)')}; + } + + /* Ghost variant */ + .button.ghost { + background: transparent; + color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; + border: 1px solid transparent; + } + + .button.ghost:hover:not(.disabled) { + background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')}; + } + + .button.ghost:active:not(.disabled) { + background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 13.8%)')}; + } + + /* Link variant */ + .button.link { + background: transparent; + color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(213.1 93.9% 67.8%)')}; border: none; - color: #fff; + text-decoration: underline; + text-decoration-color: transparent; + height: auto; + padding: 0; } - .button.highlighted:hover { - background: #b50021; - border: none; - color: #fff; + .button.link:hover:not(.disabled) { + text-decoration-color: currentColor; } - .button.discreet { - background: none; - border: 1px solid #9b9b9e; - color: ${cssManager.bdTheme('#000', '#fff')}; + /* Status states */ + .button.pending, + .button.success, + .button.error { + pointer-events: none; + padding-left: 36px; /* Space for spinner */ + } + + .button.size-sm.pending, + .button.size-sm.success, + .button.size-sm.error { + padding-left: 32px; + } + + .button.size-lg.pending, + .button.size-lg.success, + .button.size-lg.error { + padding-left: 44px; } - .button.discreet:hover { - background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; + .button.pending { + background: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(213.1 93.9% 67.8% / 0.2)')}; + color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(213.1 93.9% 67.8%)')}; + border: 1px solid transparent; } + .button.success { + background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3% / 0.2)')}; + color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(142.1 70.6% 45.3%)')}; + border: 1px solid transparent; + } + + .button.error { + background: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 62.8% 70.6% / 0.2)')}; + color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 62.8% 70.6%)')}; + border: 1px solid transparent; + } + + /* Disabled state */ .button.disabled { - background: ${cssManager.bdTheme('#ffffff00', '#11111100')}; - border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')}; - color: #9b9b9e; - cursor: default; + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; } + /* Hidden state */ .button.hidden { display: none; } - .button.big { - width: 300px; - line-height: 48px; - font-size: 16px; - padding: 0px 48px; - margin-top: 32px; - } - - .button.pending { - border: 1px dashed ${cssManager.bdTheme('#0069f2', '#0069f270')}; - background: ${cssManager.bdTheme('#0069f2', '#0069f270')}; - color: #fff; - } - - .button.success { - border: 1px dashed ${cssManager.bdTheme('#689F38', '#8BC34A70')}; - background: ${cssManager.bdTheme('#689F38', '#8BC34A70')}; - color: #fff; - } - - .button.error { - border: 1px dashed ${cssManager.bdTheme('#B71C1C', '#E64A1970')}; - background: ${cssManager.bdTheme('#B71C1C', '#E64A1970')}; - color: #fff; + /* Focus state */ + .button:focus-visible { + outline: 2px solid ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(213.1 93.9% 67.8%)')}; + outline-offset: 2px; } + /* Loading spinner */ dees-spinner { position: absolute; left: 10px; + width: 16px; + height: 16px; } + + .button.size-sm dees-spinner { + left: 8px; + width: 14px; + height: 14px; + } + + .button.size-lg dees-spinner { + left: 14px; + width: 18px; + height: 18px; + } + + /* Icon sizing within buttons */ + .button dees-icon { + width: 16px; + height: 16px; + flex-shrink: 0; + } + + .button.size-sm dees-icon { + width: 14px; + height: 14px; + } + + .button.size-lg dees-icon { + width: 18px; + height: 18px; + } + `, ]; public render(): TemplateResult { + // Map old types to new types for backward compatibility + const typeMap: {[key: string]: string} = { + 'normal': 'default', + 'highlighted': 'destructive', + 'discreet': 'outline', + 'big': 'default' // Will use size instead + }; + + const actualType = typeMap[this.type] || this.type; + const actualSize = this.type === 'big' ? 'lg' : this.size; + return html`
${this.status === 'normal' ? html``: html` - + `}
${this.text || html`Button`}
diff --git a/ts_web/elements/dees-panel.ts b/ts_web/elements/dees-panel.ts index 8979a56..4f0994a 100644 --- a/ts_web/elements/dees-panel.ts +++ b/ts_web/elements/dees-panel.ts @@ -30,28 +30,30 @@ export class DeesPanel extends DeesElement { css` :host { display: block; - background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')}; + background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20.2% 11.8%)')}; border-radius: 8px; padding: 24px; - box-shadow: 0 2px 4px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')}; - border: 1px solid ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')}; + border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 16.8%)')}; + box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); } .title { - margin: 0 0 16px 0; + margin: 0 0 8px 0; font-size: 18px; - font-weight: 500; - color: ${cssManager.bdTheme('#0069f2', '#0099ff')}; + font-weight: 600; + color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; + letter-spacing: -0.025em; } .subtitle { - margin: -12px 0 16px 0; + margin: 0 0 16px 0; font-size: 14px; - color: ${cssManager.bdTheme('#666', '#999')}; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; + letter-spacing: -0.01em; } .content { - color: ${cssManager.bdTheme('#333', '#ccc')}; + color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; } /* Remove margins from first and last children */ diff --git a/ts_web/elements/dees-statsgrid.demo.ts b/ts_web/elements/dees-statsgrid.demo.ts index 67ce39a..a452b25 100644 --- a/ts_web/elements/dees-statsgrid.demo.ts +++ b/ts_web/elements/dees-statsgrid.demo.ts @@ -1,389 +1,516 @@ -import { html, cssManager } from '@design.estate/dees-element'; +import { html, css, cssManager } from '@design.estate/dees-element'; +import '@design.estate/dees-wcctools/demotools'; +import './dees-panel.js'; import type { IStatsTile } from './dees-statsgrid.js'; export const demoFunc = () => { - // Demo data with different tile types - const demoTiles: IStatsTile[] = [ - { - id: 'revenue', - title: 'Total Revenue', - value: 125420, - unit: '$', - type: 'number', - icon: 'faDollarSign', - description: '+12.5% from last month', - color: '#22c55e', - actions: [ - { - name: 'View Details', - iconName: 'faChartLine', - action: async () => { - console.log('Viewing revenue details for tile:', 'revenue'); - console.log('Current value:', 125420); - alert(`Revenue Details: $125,420 (+12.5%)`); - } - }, - { - name: 'Export Data', - iconName: 'faFileExport', - action: async () => { - console.log('Exporting revenue data'); - alert('Revenue data exported to CSV'); - } - } - ] - }, - { - id: 'users', - title: 'Active Users', - value: 3847, - type: 'number', - icon: 'faUsers', - description: '324 new this week', - actions: [ - { - name: 'View User List', - iconName: 'faList', - action: async () => { - console.log('Viewing user list'); - } - } - ] - }, - { - id: 'cpu', - title: 'CPU Usage', - value: 73, - type: 'gauge', - icon: 'faMicrochip', - gaugeOptions: { - min: 0, - max: 100, - thresholds: [ - { value: 0, color: '#22c55e' }, - { value: 60, color: '#f59e0b' }, - { value: 80, color: '#ef4444' } - ] - } - }, - { - id: 'storage', - title: 'Storage Used', - value: 65, - type: 'percentage', - icon: 'faHardDrive', - description: '650 GB of 1 TB', - color: '#3b82f6' - }, - { - id: 'memory', - title: 'Memory Usage', - value: 45, - type: 'gauge', - icon: 'faMemory', - gaugeOptions: { - min: 0, - max: 100, - thresholds: [ - { value: 0, color: '#22c55e' }, - { value: 70, color: '#f59e0b' }, - { value: 90, color: '#ef4444' } - ] - } - }, - { - id: 'requests', - title: 'API Requests', - value: '1.2k', - unit: '/min', - type: 'trend', - icon: 'faServer', - trendData: [45, 52, 38, 65, 72, 68, 75, 82, 79, 85, 88, 92] - }, - { - id: 'uptime', - title: 'System Uptime', - value: '99.95%', - type: 'text', - icon: 'faCheckCircle', - color: '#22c55e', - description: 'Last 30 days' - }, - { - id: 'latency', - title: 'Response Time', - value: 142, - unit: 'ms', - type: 'trend', - icon: 'faClock', - trendData: [150, 145, 148, 142, 138, 140, 135, 145, 142], - description: 'P95 latency' - }, - { - id: 'errors', - title: 'Error Rate', - value: 0.03, - unit: '%', - type: 'number', - icon: 'faExclamationTriangle', - color: '#ef4444', - actions: [ - { - name: 'View Error Logs', - iconName: 'faFileAlt', - action: async () => { - console.log('Viewing error logs'); - } - } - ] - } - ]; - - // Grid actions for the demo - const gridActions = [ - { - name: 'Refresh', - iconName: 'faSync', - action: async () => { - console.log('Refreshing stats...'); - // Simulate refresh animation - const grid = document.querySelector('dees-statsgrid'); - if (grid) { - grid.style.opacity = '0.5'; - setTimeout(() => { - grid.style.opacity = '1'; - }, 500); - } - } - }, - { - name: 'Export Report', - iconName: 'faFileExport', - action: async () => { - console.log('Exporting stats report...'); - } - }, - { - name: 'Settings', - iconName: 'faCog', - action: async () => { - console.log('Opening settings...'); - } - } - ]; - return html` + - +
- - -
-

Full Featured Stats Grid

-

- A comprehensive dashboard with various tile types, actions, and real-time updates. -

- -
- -
-

Compact Grid (Smaller Tiles)

-

- Same data displayed with smaller minimum tile width for more compact layouts. -

- -
- -
-

Simple Metrics (No Actions)

-

- Clean display without interactive elements for pure visualization. -

+ { + const output = document.querySelector('#action-output'); + if (output) { + output.textContent = 'Viewing revenue details: $125,420 (+12.5%)'; + } + } + }, + { + name: 'Export Data', + iconName: 'lucide:download', + action: async () => { + const output = document.querySelector('#action-output'); + if (output) { + output.textContent = 'Exporting revenue data to CSV...'; + } + } + } + ] }, { - id: 'metric4', - title: 'Customer Satisfaction', - value: 92, - type: 'percentage', - icon: 'faSmile', - color: '#22c55e' - } - ]} - .minTileWidth=${220} - .gap=${16} - > -
- -
-

Performance Monitoring

-

- Real-time performance metrics with gauge visualizations and thresholds. -

- { + const output = document.querySelector('#action-output'); + if (output) { + output.textContent = 'Opening user list...'; + } + } + } + ] + }, { - id: 'perf1', - title: 'Database Load', - value: 42, + id: 'cpu', + title: 'CPU Usage', + value: 73, + unit: '%', type: 'gauge', - icon: 'faDatabase', + icon: 'lucide:cpu', gaugeOptions: { min: 0, max: 100, thresholds: [ - { value: 0, color: '#10b981' }, - { value: 50, color: '#f59e0b' }, - { value: 75, color: '#ef4444' } + { value: 0, color: 'hsl(142.1 76.2% 36.3%)' }, + { value: 60, color: 'hsl(45.4 93.4% 47.5%)' }, + { value: 80, color: 'hsl(0 84.2% 60.2%)' } ] } }, { - id: 'perf2', - title: 'Network I/O', - value: 856, - unit: 'MB/s', - type: 'trend', - icon: 'faNetworkWired', - trendData: [720, 780, 823, 845, 812, 876, 856] - }, - { - id: 'perf3', - title: 'Cache Hit Rate', - value: 94.2, + id: 'storage', + title: 'Storage Used', + value: 65, type: 'percentage', - icon: 'faBolt', - color: '#3b82f6' + icon: 'lucide:hard-drive', + description: '650 GB of 1 TB', }, { - id: 'perf4', - title: 'Active Connections', - value: 1428, - type: 'number', - icon: 'faLink', - description: 'Peak: 2,100' + id: 'latency', + title: 'Response Time', + value: 142, + unit: 'ms', + type: 'trend', + icon: 'lucide:activity', + trendData: [150, 145, 148, 142, 138, 140, 135, 145, 142], + description: 'P95' + }, + { + id: 'uptime', + title: 'System Uptime', + value: '99.95%', + type: 'text', + icon: 'lucide:check-circle', + color: 'hsl(142.1 76.2% 36.3%)', + description: 'Last 30 days' } ]} .gridActions=${[ { - name: 'Auto Refresh', - iconName: 'faPlay', + name: 'Refresh', + iconName: 'lucide:refresh-cw', action: async () => { - console.log('Starting auto refresh...'); + const grid = document.querySelector('dees-statsgrid'); + if (grid) { + grid.style.opacity = '0.5'; + setTimeout(() => { + grid.style.opacity = '1'; + }, 300); + } + } + }, + { + name: 'Export', + iconName: 'lucide:share', + action: async () => { + const output = document.querySelector('#action-output'); + if (output) { + output.textContent = 'Exporting dashboard report...'; + } + } + }, + { + name: 'Settings', + iconName: 'lucide:settings', + action: async () => { + const output = document.querySelector('#action-output'); + if (output) { + output.textContent = 'Opening dashboard settings...'; + } } } ]} + .minTileWidth=${250} + .gap=${16} + > + +
+ Click on tile actions or grid actions to see the result... +
+ + + + + +
+
+
Configuration Options
+
+ Each tile type supports different properties: +
    +
  • Number: value, unit, color, description
  • +
  • Gauge: value, unit, gaugeOptions (min, max, thresholds)
  • +
  • Percentage: value (0-100), color, description
  • +
  • Trend: value, unit, trendData array, description
  • +
  • Text: value (string), color, description
  • +
+
+
+
+
+ + +

Compact Layout (180px tiles)

+ + +

Spacious Layout (320px tiles)

+ -
- - + ]} + .gridActions=${[ + { + name: 'Start Live Updates', + iconName: 'lucide:play', + action: async function() { + // Toggle live updates + if (!window.liveUpdateInterval) { + window.liveUpdateInterval = setInterval(() => { + const grid = document.querySelector('#interactive-grid'); + if (grid) { + const tiles = [...grid.tiles]; + + // Update CPU gauge + const cpuTile = tiles.find(t => t.id === 'live-cpu'); + cpuTile.value = Math.max(0, Math.min(100, cpuTile.value + (Math.random() * 20 - 10))); + + // Update requests trend + const requestsTile = tiles.find(t => t.id === 'live-requests'); + const newValue = requestsTile.value + Math.round(Math.random() * 50 - 25); + requestsTile.value = Math.max(800, newValue); + requestsTile.trendData = [...requestsTile.trendData.slice(1), requestsTile.value]; + + // Update memory percentage + const memoryTile = tiles.find(t => t.id === 'live-memory'); + memoryTile.value = Math.max(0, Math.min(100, memoryTile.value + (Math.random() * 10 - 5))); + + grid.tiles = tiles; + } + }, 1000); + + this.name = 'Stop Live Updates'; + this.iconName = 'lucide:pause'; + } else { + clearInterval(window.liveUpdateInterval); + window.liveUpdateInterval = null; + this.name = 'Start Live Updates'; + this.iconName = 'lucide:play'; + } + } + } + ]} + .minTileWidth=${250} + .gap=${16} + > + + + +
${`const tiles: IStatsTile[] = [ + { + id: 'revenue', + title: 'Total Revenue', + value: 125420, + unit: '$', + type: 'number', + icon: 'lucide:dollar-sign', + description: '+12.5% from last month', + actions: [ + { + name: 'View Details', + iconName: 'lucide:trending-up', + action: async () => { + console.log('View revenue details'); + } + } + ] + }, + { + id: 'cpu', + title: 'CPU Usage', + value: 73, + unit: '%', + type: 'gauge', + icon: 'lucide:cpu', + gaugeOptions: { + min: 0, + max: 100, + thresholds: [ + { value: 0, color: 'hsl(142.1 76.2% 36.3%)' }, + { value: 60, color: 'hsl(45.4 93.4% 47.5%)' }, + { value: 80, color: 'hsl(0 84.2% 60.2%)' } + ] + } + } +]; + +// Render the stats grid +html\` + console.log('Refresh') + } + ]} + > +\`;`}
+
+ + +
`; }; \ No newline at end of file diff --git a/ts_web/elements/dees-statsgrid.ts b/ts_web/elements/dees-statsgrid.ts index 383453f..ad5ec33 100644 --- a/ts_web/elements/dees-statsgrid.ts +++ b/ts_web/elements/dees-statsgrid.ts @@ -81,28 +81,44 @@ export class DeesStatsGrid extends DeesElement { width: 100%; } + /* CSS Variables for consistent spacing and sizing */ + :host { + --grid-gap: 16px; + --tile-padding: 24px; + --header-spacing: 16px; + --content-min-height: 48px; + --value-font-size: 30px; + --unit-font-size: 16px; + --label-font-size: 13px; + --title-font-size: 14px; + --description-spacing: 12px; + --border-radius: 8px; + --transition-duration: 0.15s; + } + + /* Grid Layout */ .grid-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: ${unsafeCSS(16)}px; - min-height: 32px; + margin-bottom: calc(var(--grid-gap) * 1.5); + min-height: 40px; } .grid-title { - font-size: 18px; - font-weight: 600; - color: ${cssManager.bdTheme('#333', '#fff')}; + font-size: 16px; + font-weight: 500; + color: ${cssManager.bdTheme('#09090b', '#fafafa')}; + letter-spacing: -0.01em; } .grid-actions { display: flex; - gap: 8px; + gap: 6px; } .grid-actions dees-button { - font-size: 14px; - min-width: auto; + font-size: var(--label-font-size); } .stats-grid { @@ -112,86 +128,106 @@ export class DeesStatsGrid extends DeesElement { width: 100%; } + /* Tile Base Styles */ .stats-tile { - background: ${cssManager.bdTheme('#fff', '#1a1a1a')}; - border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; - border-radius: 12px; - padding: 20px; - transition: all 0.3s ease; - cursor: pointer; + background: ${cssManager.bdTheme('#ffffff', '#09090b')}; + border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 11.8%)')}; + border-radius: var(--border-radius); + padding: var(--tile-padding); + transition: all var(--transition-duration) ease; + cursor: default; position: relative; overflow: hidden; + display: flex; + flex-direction: column; } .stats-tile:hover { - transform: translateY(-2px); - box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')}; - border-color: ${cssManager.bdTheme('#d0d0d0', '#3a3a3a')}; + background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 10.2%)')}; + border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 16.8%)')}; } .stats-tile.clickable { cursor: pointer; } + .stats-tile.clickable:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px ${cssManager.bdTheme('rgba(0,0,0,0.04)', 'rgba(0,0,0,0.2)')}; + } + + /* Tile Header */ .tile-header { display: flex; justify-content: space-between; - align-items: center; - margin-bottom: 12px; - width: 100%; + align-items: flex-start; + margin-bottom: var(--header-spacing); + flex-shrink: 0; } .tile-title { - font-size: 14px; + font-size: var(--title-font-size); font-weight: 500; - color: ${cssManager.bdTheme('#666', '#aaa')}; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; margin: 0; + letter-spacing: -0.01em; + line-height: 1.2; } .tile-icon { - opacity: 0.6; + opacity: 0.7; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; + font-size: 16px; + flex-shrink: 0; } + /* Tile Content */ .tile-content { - height: 90px; + min-height: var(--content-min-height); display: flex; flex-direction: column; justify-content: center; - align-items: center; - position: relative; + flex: 1; } .tile-value { - font-size: 32px; + font-size: var(--value-font-size); font-weight: 600; - color: ${cssManager.bdTheme('#333', '#fff')}; - line-height: 1.2; + color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; + line-height: 1; display: flex; align-items: baseline; - justify-content: center; - gap: 6px; - width: 100%; + gap: 4px; + letter-spacing: -0.025em; } .tile-unit { - font-size: 18px; + font-size: var(--unit-font-size); font-weight: 400; - color: ${cssManager.bdTheme('#666', '#aaa')}; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; + letter-spacing: -0.01em; } .tile-description { - font-size: 12px; - color: ${cssManager.bdTheme('#888', '#777')}; - margin-top: 8px; + font-size: var(--label-font-size); + color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; + margin-top: var(--description-spacing); + letter-spacing: -0.01em; + flex-shrink: 0; + } + + /* Gauge Styles */ + .gauge-wrapper { + width: 100%; + display: flex; + justify-content: center; + align-items: center; } .gauge-container { - width: 100%; - height: 80px; + width: 140px; + height: 70px; position: relative; - display: flex; - align-items: center; - justify-content: center; } .gauge-svg { @@ -201,96 +237,130 @@ export class DeesStatsGrid extends DeesElement { .gauge-background { fill: none; - stroke: ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; - stroke-width: 6; + stroke: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')}; + stroke-width: 8; } .gauge-fill { fill: none; - stroke-width: 6; + stroke-width: 8; stroke-linecap: round; - transition: stroke-dashoffset 0.5s ease; + transition: stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1); } .gauge-text { - fill: ${cssManager.bdTheme('#333', '#fff')}; - font-size: 18px; + fill: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; + font-size: var(--value-font-size); font-weight: 600; text-anchor: middle; + dominant-baseline: alphabetic; + letter-spacing: -0.025em; + } + + .gauge-unit { + font-size: var(--unit-font-size); + fill: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; + font-weight: 400; } - .percentage-container { + /* Percentage Styles */ + .percentage-wrapper { width: 100%; - height: 24px; - background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')}; - border-radius: 12px; - overflow: hidden; position: relative; } + .percentage-value { + font-size: var(--value-font-size); + font-weight: 600; + color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; + letter-spacing: -0.025em; + margin-bottom: 8px; + } + + .percentage-bar { + width: 100%; + height: 8px; + background: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')}; + border-radius: 4px; + overflow: hidden; + } + .percentage-fill { height: 100%; - background: ${cssManager.bdTheme('#0084ff', '#0066cc')}; - transition: width 0.5s ease; - border-radius: 12px; - } - - .percentage-text { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - font-size: 12px; - font-weight: 600; - color: ${cssManager.bdTheme('#333', '#fff')}; + background: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; + transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1); + border-radius: 4px; } + /* Trend Styles */ .trend-container { width: 100%; - height: 100%; - position: relative; display: flex; flex-direction: column; - justify-content: center; - align-items: center; - gap: 4px; + gap: 8px; + } + + .trend-header { + display: flex; + align-items: baseline; + gap: 8px; + } + + .trend-value { + font-size: var(--value-font-size); + font-weight: 600; + color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; + letter-spacing: -0.025em; + } + + .trend-unit { + font-size: var(--unit-font-size); + font-weight: 400; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')}; + letter-spacing: -0.01em; + } + + .trend-label { + font-size: var(--label-font-size); + font-weight: 500; + color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')}; + letter-spacing: -0.01em; + margin-left: auto; + } + + .trend-graph { + width: 100%; + height: 32px; + position: relative; } .trend-svg { width: 100%; - height: 40px; - flex-shrink: 0; + height: 100%; + display: block; } .trend-line { fill: none; - stroke: ${cssManager.bdTheme('#0084ff', '#0066cc')}; + stroke: ${cssManager.bdTheme('hsl(215.4 16.3% 66.9%)', 'hsl(215 20.2% 55.1%)')}; stroke-width: 2; + stroke-linejoin: round; + stroke-linecap: round; } .trend-area { - fill: ${cssManager.bdTheme('rgba(0, 132, 255, 0.1)', 'rgba(0, 102, 204, 0.2)')}; + fill: ${cssManager.bdTheme('hsl(215.4 16.3% 66.9% / 0.1)', 'hsl(215 20.2% 55.1% / 0.08)')}; } + /* Text Value Styles */ .text-value { - font-size: 32px; + font-size: var(--value-font-size); font-weight: 600; - color: ${cssManager.bdTheme('#333', '#fff')}; - } - - .trend-value { - font-size: 32px; - font-weight: 600; - color: ${cssManager.bdTheme('#333', '#fff')}; - display: flex; - align-items: baseline; - gap: 6px; - } - - .trend-value .tile-unit { - font-size: 18px; + color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')}; + letter-spacing: -0.025em; } + /* Context Menu */ dees-contextmenu { position: fixed; z-index: 1000; @@ -306,10 +376,14 @@ export class DeesStatsGrid extends DeesElement { return html` ${this.gridActions.length > 0 ? html`
-
Statistics
+
${this.gridActions.map(action => html` - this.handleGridAction(action)}> + this.handleGridAction(action)} + type="outline" + size="sm" + > ${action.iconName ? html`` : ''} ${action.name} @@ -354,7 +428,7 @@ export class DeesStatsGrid extends DeesElement { ${this.renderTileContent(tile)}
- ${tile.description ? html` + ${tile.description && tile.type !== 'trend' ? html`
${tile.description}
` : ''}
@@ -396,12 +470,31 @@ export class DeesStatsGrid extends DeesElement { const value = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value); const options = tile.gaugeOptions || { min: 0, max: 100 }; const percentage = ((value - options.min) / (options.max - options.min)) * 100; - const strokeDasharray = 188.5; // Circumference of circle with r=30 - const strokeDashoffset = strokeDasharray - (strokeDasharray * percentage) / 100; + + // SVG dimensions and calculations + const width = 140; + const height = 70; + const strokeWidth = 8; + const padding = strokeWidth / 2 + 2; + const radius = 40; + const centerX = width / 2; + const centerY = height - padding; + + // Arc path + const startX = centerX - radius; + const startY = centerY; + const endX = centerX + radius; + const endY = centerY; + const arcPath = `M ${startX} ${startY} A ${radius} ${radius} 0 0 1 ${endX} ${endY}`; + + // Calculate stroke dasharray and dashoffset + const circumference = Math.PI * radius; + const strokeDashoffset = circumference - (circumference * percentage) / 100; - let strokeColor = tile.color || cssManager.bdTheme('#0084ff', '#0066cc'); + let strokeColor = tile.color || cssManager.bdTheme('hsl(215.3 25% 28.8%)', 'hsl(210 40% 78%)'); if (options.thresholds) { - for (const threshold of options.thresholds.reverse()) { + const sortedThresholds = [...options.thresholds].sort((a, b) => b.value - a.value); + for (const threshold of sortedThresholds) { if (value >= threshold.value) { strokeColor = threshold.color; break; @@ -410,29 +503,28 @@ export class DeesStatsGrid extends DeesElement { } return html` -
- - - - - ${value}${tile.unit || ''} - - +
+
+ + + + + + + + ${value}${tile.unit ? html`${tile.unit}` : ''} + + +
`; } @@ -442,12 +534,14 @@ export class DeesStatsGrid extends DeesElement { const percentage = Math.min(100, Math.max(0, value)); return html` -
-
-
${percentage}%
+
+
${percentage}%
+
+
+
`; } @@ -461,11 +555,14 @@ export class DeesStatsGrid extends DeesElement { const max = Math.max(...data); const min = Math.min(...data); const range = max - min || 1; - const width = 200; - const height = 40; + const width = 300; + const height = 32; + + // Add padding to prevent clipping + const padding = 2; const points = data.map((value, index) => { const x = (index / (data.length - 1)) * width; - const y = height - ((value - min) / range) * height; + const y = padding + (height - 2 * padding) - ((value - min) / range) * (height - 2 * padding); return `${x},${y}`; }).join(' '); @@ -473,13 +570,16 @@ export class DeesStatsGrid extends DeesElement { return html`
- - - - -
- ${tile.value} - ${tile.unit ? html`${tile.unit}` : ''} +
+ ${tile.value} + ${tile.unit ? html`${tile.unit}` : ''} + ${tile.description ? html`${tile.description}` : ''} +
+
+ + + +
`;