update button and statsgrid with better styles.

This commit is contained in:
Juergen Kunz
2025-06-27 11:50:07 +00:00
parent d7b690621e
commit 243ecddd42
8 changed files with 1413 additions and 637 deletions

174
CLAUDE.md Normal file
View File

@ -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

View File

@ -7,7 +7,7 @@
"typings": "dist_ts_web/index.d.ts", "typings": "dist_ts_web/index.d.ts",
"type": "module", "type": "module",
"scripts": { "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", "build": "tsbuild tsfolders --allowimplicitany && tsbundle element --production --bundler esbuild",
"watch": "tswatch element", "watch": "tswatch element",
"buildDocs": "tsdoc" "buildDocs": "tsdoc"

View File

@ -1,5 +1,5 @@
!!! Please pay attention to the following points when writing the readme: !!! !!! 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. * Try to list all components in a summary.
* Then list all components with a short description. * Then list all components with a short description.

View File

@ -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` export const demoFunc = () => html`
<style> <dees-demowrapper>
${css` <style>
h3 { ${css`
margin-top: 32px; .demo-container {
margin-bottom: 16px; display: flex;
color: #333; flex-direction: column;
} gap: 24px;
padding: 24px;
@media (prefers-color-scheme: dark) { max-width: 1200px;
h3 { margin: 0 auto;
color: #ccc;
} }
}
dees-panel {
.form-demo { margin-bottom: 24px;
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
@media (prefers-color-scheme: dark) {
.form-demo {
background: #1a1a1a;
} }
}
dees-panel:last-child {
margin-bottom: 0;
}
.button-group {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.vertical-group {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 300px;
}
.horizontal-group {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
}
.demo-output {
margin-top: 16px;
padding: 12px;
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
border-radius: 6px;
font-size: 14px;
font-family: monospace;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
}
.icon-row {
display: flex;
align-items: center;
gap: 12px;
margin: 8px 0;
}
.code-snippet {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 11.8%)')};
padding: 8px 12px;
border-radius: 4px;
font-family: monospace;
font-size: 13px;
display: inline-block;
margin: 4px 0;
}
`}
</style>
<div class="demo-container">
<dees-panel .title=${'1. Button Variants'} .subtitle=${'Different visual styles for various use cases'}>
<div class="button-group">
<dees-button type="default">Default</dees-button>
<dees-button type="secondary">Secondary</dees-button>
<dees-button type="destructive">Destructive</dees-button>
<dees-button type="outline">Outline</dees-button>
<dees-button type="ghost">Ghost</dees-button>
<dees-button type="link">Link Button</dees-button>
</div>
</dees-panel>
.button-group { <dees-panel .title=${'2. Button Sizes'} .subtitle=${'Multiple sizes for different contexts and use cases'}>
display: flex; <div class="button-group">
gap: 16px; <dees-button size="sm">Small Button</dees-button>
margin: 20px 0; <dees-button size="default">Default Size</dees-button>
} <dees-button size="lg">Large Button</dees-button>
`} <dees-button size="icon" type="outline" .text=${'🚀'}></dees-button>
</style> </div>
<h3>Button Types</h3> <div class="button-group" style="margin-top: 16px;">
<dees-button>This is a slotted Text</dees-button> <dees-button size="sm" type="secondary">Small Secondary</dees-button>
<p> <dees-button size="default" type="destructive">Default Destructive</dees-button>
<dees-button text="Highlighted: This text shows" type="highlighted">Highlighted</dees-button> <dees-button size="lg" type="outline">Large Outline</dees-button>
</p> </div>
<p><dees-button type="discreet">This is discreete button</dees-button></p> </dees-panel>
<p><dees-button disabled>This is a disabled button</dees-button></p>
<p><dees-button type="big">This is a slotted Text</dees-button></p> <dees-panel .title=${'3. Buttons with Icons'} .subtitle=${'Combining icons with text for enhanced visual communication'}>
<div class="icon-row">
<h3>Button States</h3> <dees-button>
<p><dees-button status="normal">Normal Status</dees-button></p> <dees-icon iconFA="faPlus"></dees-icon>
<p><dees-button disabled status="pending">Pending Status</dees-button></p> Add Item
<p><dees-button disabled status="success">Success Status</dees-button></p> </dees-button>
<p><dees-button disabled status="error">Error Status</dees-button></p> <dees-button type="destructive">
<dees-icon iconFA="faTrash"></dees-icon>
<h3>Buttons in Forms (Auto-spacing)</h3> Delete
<div class="form-demo"> </dees-button>
<dees-form> <dees-button type="outline">
<dees-input-text label="Name" key="name"></dees-input-text> <dees-icon iconFA="faDownload"></dees-icon>
<dees-input-text label="Email" key="email"></dees-input-text> Download
<dees-button>Save Draft</dees-button> </dees-button>
<dees-button type="highlighted">Save and Continue</dees-button> </div>
<dees-form-submit>Submit Form</dees-form-submit>
</dees-form> <div class="icon-row">
</div> <dees-button type="secondary" size="sm">
<dees-icon iconFA="faCog"></dees-icon>
<h3>Buttons Outside Forms (No auto-spacing)</h3> Settings
<div class="button-group"> </dees-button>
<dees-button>Button 1</dees-button> <dees-button type="ghost">
<dees-button>Button 2</dees-button> <dees-icon iconFA="faChevronLeft"></dees-icon>
<dees-button>Button 3</dees-button> Back
</div> </dees-button>
<dees-button type="ghost">
<h3>Manual Form Spacing</h3> Next
<div> <dees-icon iconFA="faChevronRight"></dees-icon>
<dees-button inside-form="true">Manually spaced button 1</dees-button> </dees-button>
<dees-button inside-form="true">Manually spaced button 2</dees-button> </div>
</div>
<div class="icon-row">
<dees-button size="icon" type="default">
<dees-icon iconFA="faPlus"></dees-icon>
</dees-button>
<dees-button size="icon" type="secondary">
<dees-icon iconFA="faCog"></dees-icon>
</dees-button>
<dees-button size="icon" type="outline">
<dees-icon iconFA="faSearch"></dees-icon>
</dees-button>
<dees-button size="icon" type="ghost">
<dees-icon iconFA="faEllipsisV"></dees-icon>
</dees-button>
<dees-button size="icon" type="destructive">
<dees-icon iconFA="faTrash"></dees-icon>
</dees-button>
</div>
</dees-panel>
<dees-panel .title=${'4. Button States'} .subtitle=${'Different states to indicate button status and loading conditions'}>
<div class="button-group">
<dees-button status="normal">Normal</dees-button>
<dees-button status="pending">Processing...</dees-button>
<dees-button status="success">Success!</dees-button>
<dees-button status="error">Error!</dees-button>
<dees-button disabled>Disabled</dees-button>
</div>
<div class="button-group" style="margin-top: 16px;">
<dees-button type="secondary" status="pending" size="sm">Small Loading</dees-button>
<dees-button type="outline" status="pending">Default Loading</dees-button>
<dees-button type="destructive" status="pending" size="lg">Large Loading</dees-button>
</div>
</dees-panel>
<dees-panel .title=${'5. Event Handling'} .subtitle=${'Interactive examples with click event handling'}>
<div class="button-group">
<dees-button
@clicked=${() => {
const output = document.querySelector('#click-output');
if (output) {
output.textContent = `Clicked: Default button at ${new Date().toLocaleTimeString()}`;
}
}}
>
Click Me
</dees-button>
<dees-button
type="secondary"
.eventDetailData=${'custom-data-123'}
@clicked=${(e: CustomEvent) => {
const output = document.querySelector('#click-output');
if (output) {
output.textContent = `Clicked: Secondary button with data: ${e.detail.data}`;
}
}}
>
Click with Data
</dees-button>
<dees-button
type="destructive"
@clicked=${async () => {
const output = document.querySelector('#click-output');
if (output) {
output.textContent = 'Processing...';
await new Promise(resolve => setTimeout(resolve, 2000));
output.textContent = 'Action completed!';
}
}}
>
Async Action
</dees-button>
</div>
<div id="click-output" class="demo-output">
<em>Click a button to see the result...</em>
</div>
</dees-panel>
<dees-panel .title=${'6. Form Integration'} .subtitle=${'Buttons working within forms with automatic spacing'}>
<dees-form @formData=${(e: CustomEvent) => {
const output = document.querySelector('#form-output');
if (output) {
output.innerHTML = '<strong>Form submitted with data:</strong><br>' +
JSON.stringify(e.detail.data, null, 2);
}
}}>
<dees-input-text label="Name" key="name" required></dees-input-text>
<dees-input-text label="Email" key="email" type="email" required></dees-input-text>
<dees-input-text label="Message" key="message" isMultiline></dees-input-text>
<dees-button type="secondary">Save Draft</dees-button>
<dees-button type="ghost">Cancel</dees-button>
<dees-form-submit>Submit Form</dees-form-submit>
</dees-form>
<div id="form-output" class="demo-output" style="white-space: pre-wrap;">
<em>Submit the form to see the data...</em>
</div>
</dees-panel>
<dees-panel .title=${'7. Backward Compatibility'} .subtitle=${'Old button types are automatically mapped to new variants'}>
<div class="button-group">
<dees-button type="normal">Normal → Default</dees-button>
<dees-button type="highlighted">Highlighted → Destructive</dees-button>
<dees-button type="discreet">Discreet → Outline</dees-button>
<dees-button type="big">Big → Large Size</dees-button>
</div>
<p style="margin-top: 16px; font-size: 14px; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};">
These legacy type values are maintained for backward compatibility but we recommend using the new variant system.
</p>
</dees-panel>
<dees-panel .title=${'8. Advanced Examples'} .subtitle=${'Complex button configurations and real-world use cases'}>
<div class="horizontal-group">
<div class="vertical-group">
<h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Action Group</h4>
<dees-button type="default" size="sm">
<dees-icon iconFA="faSave"></dees-icon>
Save Changes
</dees-button>
<dees-button type="secondary" size="sm">
<dees-icon iconFA="faUndo"></dees-icon>
Discard
</dees-button>
<dees-button type="ghost" size="sm">
<dees-icon iconFA="faQuestionCircle"></dees-icon>
Help
</dees-button>
</div>
<div class="vertical-group">
<h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Danger Zone</h4>
<dees-button type="destructive" size="sm">
<dees-icon iconFA="faTrash"></dees-icon>
Delete Account
</dees-button>
<dees-button type="outline" size="sm">
<dees-icon iconFA="faArchive"></dees-icon>
Archive Data
</dees-button>
<dees-button type="ghost" size="sm" disabled>
<dees-icon iconFA="faBan"></dees-icon>
Not Available
</dees-button>
</div>
</div>
<div style="margin-top: 24px;">
<h4 style="margin: 0 0 8px 0; font-size: 14px; font-weight: 500;">Code Example:</h4>
<div class="code-snippet">
&lt;dees-button type="default" size="sm" @clicked="\${handleClick}"&gt;<br>
&nbsp;&nbsp;&lt;dees-icon iconFA="faSave"&gt;&lt;/dees-icon&gt;<br>
&nbsp;&nbsp;Save Changes<br>
&lt;/dees-button&gt;
</div>
</div>
</dees-panel>
</div>
</dees-demowrapper>
`; `;

View File

@ -48,7 +48,12 @@ export class DeesButton extends DeesElement {
@property({ @property({
type: String 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({ @property({
type: String type: String
@ -77,25 +82,23 @@ export class DeesButton extends DeesElement {
cssManager.defaultStyles, cssManager.defaultStyles,
css` css`
:host { :host {
display: block; display: inline-block;
box-sizing: border-box; box-sizing: border-box;
font-family: 'Geist Sans', 'monospace'; font-family: inherit;
} }
:host([hidden]) { :host([hidden]) {
display: none; display: none;
} }
/* Form spacing styles */ /* Form spacing styles */
/* Default vertical form layout */
:host([inside-form]) { :host([inside-form]) {
margin-bottom: 16px; /* Using standard 16px like inputs */ margin-bottom: 16px;
} }
:host([inside-form]:last-child) { :host([inside-form]:last-child) {
margin-bottom: 0; margin-bottom: 0;
} }
/* Horizontal form layout - auto-detected via parent */
dees-form[horizontal-layout] :host([inside-form]) { dees-form[horizontal-layout] :host([inside-form]) {
display: inline-block; display: inline-block;
margin-right: 16px; margin-right: 16px;
@ -107,114 +110,260 @@ export class DeesButton extends DeesElement {
} }
.button { .button {
transition: all 0.1s , color 0s;
position: relative; position: relative;
font-size: 14px; display: inline-flex;
font-weight: 400;
display: flex;
justify-content: center;
align-items: center; align-items: center;
background: ${cssManager.bdTheme('#fff', '#333')}; justify-content: center;
box-shadow: ${cssManager.bdTheme('0px 1px 3px rgba(0,0,0,0.3)', 'none')}; white-space: nowrap;
border: 1px solid ${cssManager.bdTheme('#eee', '#333')}; border-radius: 6px;
border-top: ${cssManager.bdTheme('1px solid #eee', '1px solid #444')}; font-weight: 500;
border-radius: 4px; transition: all 0.15s ease;
height: 40px; cursor: pointer;
padding: 0px 8px;
min-width: 100px;
user-select: none; user-select: none;
color: ${cssManager.bdTheme('#333', ' #ccc')}; outline: none;
max-width: 500px; letter-spacing: -0.01em;
gap: 8px;
} }
.button:hover { /* Size variants */
background: #0050b9; .button.size-default {
color: #ffffff; height: 36px;
border: 1px solid #0050b9; padding: 0 16px;
border-top: 1px solid #0050b9; font-size: 14px;
} }
.button:active { .button.size-sm {
background: #0069f2; height: 32px;
border-top: 1px solid #0069f2; padding: 0 12px;
font-size: 13px;
} }
.button.highlighted { .button.size-lg {
background: #e4002b; 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; border: none;
color: #fff; text-decoration: underline;
text-decoration-color: transparent;
height: auto;
padding: 0;
} }
.button.highlighted:hover { .button.link:hover:not(.disabled) {
background: #b50021; text-decoration-color: currentColor;
border: none;
color: #fff;
} }
.button.discreet { /* Status states */
background: none; .button.pending,
border: 1px solid #9b9b9e; .button.success,
color: ${cssManager.bdTheme('#000', '#fff')}; .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 { .button.pending {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')}; 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 { .button.disabled {
background: ${cssManager.bdTheme('#ffffff00', '#11111100')}; opacity: 0.5;
border: 1px dashed ${cssManager.bdTheme('#666666', '#666666')}; cursor: not-allowed;
color: #9b9b9e; pointer-events: none;
cursor: default;
} }
/* Hidden state */
.button.hidden { .button.hidden {
display: none; display: none;
} }
.button.big { /* Focus state */
width: 300px; .button:focus-visible {
line-height: 48px; outline: 2px solid ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(213.1 93.9% 67.8%)')};
font-size: 16px; outline-offset: 2px;
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;
} }
/* Loading spinner */
dees-spinner { dees-spinner {
position: absolute; position: absolute;
left: 10px; 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 { 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` return html`
<div <div
class="button ${this.isHidden ? 'hidden' : 'block'} ${this.type} ${this.status} ${this.disabled class="button ${this.isHidden ? 'hidden' : ''} ${actualType} size-${actualSize} ${this.status} ${this.disabled
? 'disabled' ? 'disabled'
: null}" : ''}"
@click="${this.dispatchClick}" @click="${this.dispatchClick}"
> >
${this.status === 'normal' ? html``: html` ${this.status === 'normal' ? html``: html`
<dees-spinner .bnw=${true} status="${this.status}"></dees-spinner> <dees-spinner
.bnw=${true}
status="${this.status}"
size="${actualSize === 'sm' ? 14 : actualSize === 'lg' ? 18 : 16}"
></dees-spinner>
`} `}
<div class="textbox">${this.text || html`<slot>Button</slot>`}</div> <div class="textbox">${this.text || html`<slot>Button</slot>`}</div>
</div> </div>

View File

@ -30,28 +30,30 @@ export class DeesPanel extends DeesElement {
css` css`
:host { :host {
display: block; display: block;
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')}; background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(215 20.2% 11.8%)')};
border-radius: 8px; border-radius: 8px;
padding: 24px; 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('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 16.8%)')};
border: 1px solid ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(255,255,255,0.1)')}; box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
} }
.title { .title {
margin: 0 0 16px 0; margin: 0 0 8px 0;
font-size: 18px; font-size: 18px;
font-weight: 500; font-weight: 600;
color: ${cssManager.bdTheme('#0069f2', '#0099ff')}; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
letter-spacing: -0.025em;
} }
.subtitle { .subtitle {
margin: -12px 0 16px 0; margin: 0 0 16px 0;
font-size: 14px; 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 { .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 */ /* Remove margins from first and last children */

View File

@ -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'; import type { IStatsTile } from './dees-statsgrid.js';
export const demoFunc = () => { 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` return html`
<dees-demowrapper>
<style> <style>
.demo-container { ${css`
padding: 32px; .demo-container {
background: ${cssManager.bdTheme('#f8f9fa', '#0a0a0a')}; display: flex;
min-height: 100vh; flex-direction: column;
} gap: 24px;
padding: 24px;
.demo-section { max-width: 1400px;
margin-bottom: 48px; margin: 0 auto;
} }
.demo-title { dees-panel {
font-size: 24px; margin-bottom: 24px;
font-weight: 600; }
margin-bottom: 16px;
color: ${cssManager.bdTheme('#333', '#fff')}; dees-panel:last-child {
} margin-bottom: 0;
}
.demo-description {
font-size: 14px; .tile-config {
color: ${cssManager.bdTheme('#666', '#aaa')}; display: grid;
margin-bottom: 24px; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
} gap: 16px;
margin-top: 16px;
.theme-toggle { }
position: fixed;
top: 16px; .config-section {
right: 16px; padding: 16px;
padding: 8px 16px; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
background: ${cssManager.bdTheme('#fff', '#1a1a1a')}; border-radius: 6px;
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; }
border-radius: 8px;
cursor: pointer; .config-title {
z-index: 100; font-size: 14px;
} font-weight: 600;
margin-bottom: 8px;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
}
.config-description {
font-size: 13px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
}
.code-block {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 11.8%)')};
border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 16.8%)')};
border-radius: 6px;
padding: 16px;
font-family: monospace;
font-size: 13px;
overflow-x: auto;
white-space: pre;
}
`}
</style> </style>
<div class="demo-container"> <div class="demo-container">
<button class="theme-toggle" @click=${() => { <dees-panel .title=${'1. Comprehensive Dashboard'} .subtitle=${'Full-featured stats grid with various tile types, actions, and Lucide icons'}>
document.body.classList.toggle('bright');
}}>Toggle Theme</button>
<div class="demo-section">
<h2 class="demo-title">Full Featured Stats Grid</h2>
<p class="demo-description">
A comprehensive dashboard with various tile types, actions, and real-time updates.
</p>
<dees-statsgrid
.tiles=${demoTiles}
.gridActions=${gridActions}
.minTileWidth=${250}
.gap=${16}
></dees-statsgrid>
</div>
<div class="demo-section">
<h2 class="demo-title">Compact Grid (Smaller Tiles)</h2>
<p class="demo-description">
Same data displayed with smaller minimum tile width for more compact layouts.
</p>
<dees-statsgrid
.tiles=${demoTiles.slice(0, 6)}
.minTileWidth=${180}
.gap=${12}
></dees-statsgrid>
</div>
<div class="demo-section">
<h2 class="demo-title">Simple Metrics (No Actions)</h2>
<p class="demo-description">
Clean display without interactive elements for pure visualization.
</p>
<dees-statsgrid <dees-statsgrid
.tiles=${[ .tiles=${[
{ {
id: 'metric1', id: 'revenue',
title: 'Total Sales', title: 'Total Revenue',
value: 48293, value: 125420,
type: 'number',
icon: 'faShoppingCart'
},
{
id: 'metric2',
title: 'Conversion Rate',
value: 3.4,
unit: '%',
type: 'number',
icon: 'faChartLine'
},
{
id: 'metric3',
title: 'Avg Order Value',
value: 127.50,
unit: '$', unit: '$',
type: 'number', type: 'number',
icon: 'faReceipt' icon: 'lucide:dollar-sign',
description: '+12.5% from last month',
actions: [
{
name: 'View Details',
iconName: 'lucide:trending-up',
action: async () => {
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', id: 'users',
title: 'Customer Satisfaction', title: 'Active Users',
value: 92, value: 3847,
type: 'percentage', type: 'number',
icon: 'faSmile', icon: 'lucide:users',
color: '#22c55e' description: '324 new this week',
} actions: [
]} {
.minTileWidth=${220} name: 'View User List',
.gap=${16} iconName: 'lucide:list',
></dees-statsgrid> action: async () => {
</div> const output = document.querySelector('#action-output');
if (output) {
<div class="demo-section"> output.textContent = 'Opening user list...';
<h2 class="demo-title">Performance Monitoring</h2> }
<p class="demo-description"> }
Real-time performance metrics with gauge visualizations and thresholds. }
</p> ]
<dees-statsgrid },
.tiles=${[
{ {
id: 'perf1', id: 'cpu',
title: 'Database Load', title: 'CPU Usage',
value: 42, value: 73,
unit: '%',
type: 'gauge', type: 'gauge',
icon: 'faDatabase', icon: 'lucide:cpu',
gaugeOptions: { gaugeOptions: {
min: 0, min: 0,
max: 100, max: 100,
thresholds: [ thresholds: [
{ value: 0, color: '#10b981' }, { value: 0, color: 'hsl(142.1 76.2% 36.3%)' },
{ value: 50, color: '#f59e0b' }, { value: 60, color: 'hsl(45.4 93.4% 47.5%)' },
{ value: 75, color: '#ef4444' } { value: 80, color: 'hsl(0 84.2% 60.2%)' }
] ]
} }
}, },
{ {
id: 'perf2', id: 'storage',
title: 'Network I/O', title: 'Storage Used',
value: 856, value: 65,
unit: 'MB/s',
type: 'trend',
icon: 'faNetworkWired',
trendData: [720, 780, 823, 845, 812, 876, 856]
},
{
id: 'perf3',
title: 'Cache Hit Rate',
value: 94.2,
type: 'percentage', type: 'percentage',
icon: 'faBolt', icon: 'lucide:hard-drive',
color: '#3b82f6' description: '650 GB of 1 TB',
}, },
{ {
id: 'perf4', id: 'latency',
title: 'Active Connections', title: 'Response Time',
value: 1428, value: 142,
type: 'number', unit: 'ms',
icon: 'faLink', type: 'trend',
description: 'Peak: 2,100' 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=${[ .gridActions=${[
{ {
name: 'Auto Refresh', name: 'Refresh',
iconName: 'faPlay', iconName: 'lucide:refresh-cw',
action: async () => { 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}
></dees-statsgrid>
<div id="action-output" style="margin-top: 16px; padding: 12px; background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')}; border-radius: 6px; font-size: 14px; font-family: monospace; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};">
<em>Click on tile actions or grid actions to see the result...</em>
</div>
</dees-panel>
<dees-panel .title=${'2. Tile Types'} .subtitle=${'Different visualization types available in the stats grid'}>
<dees-statsgrid
.tiles=${[
{
id: 'number-example',
title: 'Number Tile',
value: 42195,
unit: '$',
type: 'number',
icon: 'lucide:hash',
description: 'Simple numeric display'
},
{
id: 'gauge-example',
title: 'Gauge Tile',
value: 68,
unit: '%',
type: 'gauge',
icon: 'lucide:gauge',
gaugeOptions: {
min: 0,
max: 100,
thresholds: [
{ value: 0, color: 'hsl(142.1 76.2% 36.3%)' },
{ value: 50, color: 'hsl(45.4 93.4% 47.5%)' },
{ value: 80, color: 'hsl(0 84.2% 60.2%)' }
]
}
},
{
id: 'percentage-example',
title: 'Percentage Tile',
value: 78,
type: 'percentage',
icon: 'lucide:percent',
description: 'Progress bar visualization'
},
{
id: 'trend-example',
title: 'Trend Tile',
value: 892,
unit: 'ops/s',
type: 'trend',
icon: 'lucide:trending-up',
trendData: [720, 750, 780, 795, 810, 835, 850, 865, 880, 892],
description: 'avg'
},
{
id: 'text-example',
title: 'Text Tile',
value: 'Operational',
type: 'text',
icon: 'lucide:info',
color: 'hsl(142.1 76.2% 36.3%)',
description: 'Status display'
}
]}
.minTileWidth=${280} .minTileWidth=${280}
.gap=${16}
></dees-statsgrid>
<div class="tile-config">
<div class="config-section">
<div class="config-title">Configuration Options</div>
<div class="config-description">
Each tile type supports different properties:
<ul style="margin: 8px 0; padding-left: 20px;">
<li><strong>Number:</strong> value, unit, color, description</li>
<li><strong>Gauge:</strong> value, unit, gaugeOptions (min, max, thresholds)</li>
<li><strong>Percentage:</strong> value (0-100), color, description</li>
<li><strong>Trend:</strong> value, unit, trendData array, description</li>
<li><strong>Text:</strong> value (string), color, description</li>
</ul>
</div>
</div>
</div>
</dees-panel>
<dees-panel .title=${'3. Grid Configurations'} .subtitle=${'Different layout options and responsive behavior'}>
<h4 style="margin: 0 0 16px 0; font-size: 16px; font-weight: 600;">Compact Layout (180px tiles)</h4>
<dees-statsgrid
.tiles=${[
{ id: '1', title: 'Orders', value: 156, type: 'number', icon: 'lucide:shopping-cart' },
{ id: '2', title: 'Revenue', value: 8420, unit: '$', type: 'number', icon: 'lucide:dollar-sign' },
{ id: '3', title: 'Users', value: 423, type: 'number', icon: 'lucide:users' },
{ id: '4', title: 'Growth', value: 12.5, unit: '%', type: 'number', icon: 'lucide:trending-up', color: 'hsl(142.1 76.2% 36.3%)' }
]}
.minTileWidth=${180}
.gap=${12}
></dees-statsgrid>
<h4 style="margin: 24px 0 16px 0; font-size: 16px; font-weight: 600;">Spacious Layout (320px tiles)</h4>
<dees-statsgrid
.tiles=${[
{
id: 'spacious1',
title: 'Monthly Revenue',
value: 184500,
unit: '$',
type: 'number',
icon: 'lucide:credit-card',
description: 'Total revenue this month'
},
{
id: 'spacious2',
title: 'Customer Satisfaction',
value: 94,
type: 'percentage',
icon: 'lucide:smile',
description: 'Based on 1,234 reviews'
},
{
id: 'spacious3',
title: 'Server Response',
value: 98,
unit: 'ms',
type: 'trend',
icon: 'lucide:server',
trendData: [105, 102, 100, 99, 98, 98, 97, 98],
description: 'avg response time'
}
]}
.minTileWidth=${320}
.gap=${20} .gap=${20}
></dees-statsgrid> ></dees-statsgrid>
</div> </dees-panel>
<script> <dees-panel .title=${'4. Interactive Features'} .subtitle=${'Tiles with actions and real-time updates'}>
// Simulate real-time updates <dees-statsgrid
setInterval(() => { id="interactive-grid"
const grids = document.querySelectorAll('dees-statsgrid'); .tiles=${[
grids.forEach(grid => { {
if (grid.tiles && grid.tiles.length > 0) { id: 'live-cpu',
// Update some random values title: 'Live CPU',
const updatedTiles = [...grid.tiles]; value: 45,
unit: '%',
// Update trends with new data point type: 'gauge',
updatedTiles.forEach(tile => { icon: 'lucide:cpu',
if (tile.type === 'trend' && tile.trendData) { gaugeOptions: {
tile.trendData = [...tile.trendData.slice(1), min: 0,
tile.trendData[tile.trendData.length - 1] + Math.random() * 10 - 5 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%)' }
]
}
},
{
id: 'live-requests',
title: 'Requests/sec',
value: 892,
type: 'trend',
icon: 'lucide:activity',
trendData: [850, 860, 870, 880, 885, 890, 892]
},
{
id: 'live-memory',
title: 'Memory Usage',
value: 62,
type: 'percentage',
icon: 'lucide:database'
},
{
id: 'counter',
title: 'Event Counter',
value: 0,
type: 'number',
icon: 'lucide:zap',
actions: [
{
name: 'Increment',
iconName: 'lucide:plus',
action: async () => {
const grid = document.querySelector('#interactive-grid');
const tile = grid.tiles.find(t => t.id === 'counter');
tile.value = typeof tile.value === 'number' ? tile.value + 1 : 1;
grid.tiles = [...grid.tiles];
}
},
{
name: 'Reset',
iconName: 'lucide:rotate-ccw',
action: async () => {
const grid = document.querySelector('#interactive-grid');
const tile = grid.tiles.find(t => t.id === 'counter');
tile.value = 0;
grid.tiles = [...grid.tiles];
}
} }
]
// Randomly update some numeric values
if (tile.type === 'number' && Math.random() > 0.7) {
const currentValue = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
tile.value = Math.round(currentValue + (Math.random() * 10 - 5));
}
// Update gauge values
if (tile.type === 'gauge' && Math.random() > 0.5) {
const currentValue = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
const newValue = currentValue + (Math.random() * 10 - 5);
tile.value = Math.max(tile.gaugeOptions?.min || 0,
Math.min(tile.gaugeOptions?.max || 100, Math.round(newValue)));
}
});
grid.tiles = updatedTiles;
} }
}); ]}
}, 3000); .gridActions=${[
</script> {
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}
></dees-statsgrid>
</dees-panel>
<dees-panel .title=${'5. Code Example'} .subtitle=${'How to implement a stats grid with TypeScript'}>
<div class="code-block">${`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\`
<dees-statsgrid
.tiles=\${tiles}
.minTileWidth=\${250}
.gap=\${16}
.gridActions=\${[
{
name: 'Refresh',
iconName: 'lucide:refresh-cw',
action: async () => console.log('Refresh')
}
]}
></dees-statsgrid>
\`;`}</div>
</dees-panel>
</div> </div>
<script>
// Cleanup live updates on page unload
window.addEventListener('beforeunload', () => {
if (window.liveUpdateInterval) {
clearInterval(window.liveUpdateInterval);
}
});
</script>
</dees-demowrapper>
`; `;
}; };

View File

@ -81,28 +81,44 @@ export class DeesStatsGrid extends DeesElement {
width: 100%; 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 { .grid-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: ${unsafeCSS(16)}px; margin-bottom: calc(var(--grid-gap) * 1.5);
min-height: 32px; min-height: 40px;
} }
.grid-title { .grid-title {
font-size: 18px; font-size: 16px;
font-weight: 600; font-weight: 500;
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('#09090b', '#fafafa')};
letter-spacing: -0.01em;
} }
.grid-actions { .grid-actions {
display: flex; display: flex;
gap: 8px; gap: 6px;
} }
.grid-actions dees-button { .grid-actions dees-button {
font-size: 14px; font-size: var(--label-font-size);
min-width: auto;
} }
.stats-grid { .stats-grid {
@ -112,86 +128,106 @@ export class DeesStatsGrid extends DeesElement {
width: 100%; width: 100%;
} }
/* Tile Base Styles */
.stats-tile { .stats-tile {
background: ${cssManager.bdTheme('#fff', '#1a1a1a')}; background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; border: 1px solid ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 11.8%)')};
border-radius: 12px; border-radius: var(--border-radius);
padding: 20px; padding: var(--tile-padding);
transition: all 0.3s ease; transition: all var(--transition-duration) ease;
cursor: pointer; cursor: default;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: flex;
flex-direction: column;
} }
.stats-tile:hover { .stats-tile:hover {
transform: translateY(-2px); background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(215 20.2% 10.2%)')};
box-shadow: 0 4px 12px ${cssManager.bdTheme('rgba(0,0,0,0.1)', 'rgba(0,0,0,0.3)')}; border-color: ${cssManager.bdTheme('hsl(214.3 31.8% 85%)', 'hsl(215 20.2% 16.8%)')};
border-color: ${cssManager.bdTheme('#d0d0d0', '#3a3a3a')};
} }
.stats-tile.clickable { .stats-tile.clickable {
cursor: pointer; 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 { .tile-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: flex-start;
margin-bottom: 12px; margin-bottom: var(--header-spacing);
width: 100%; flex-shrink: 0;
} }
.tile-title { .tile-title {
font-size: 14px; font-size: var(--title-font-size);
font-weight: 500; 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; margin: 0;
letter-spacing: -0.01em;
line-height: 1.2;
} }
.tile-icon { .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 { .tile-content {
height: 90px; min-height: var(--content-min-height);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; flex: 1;
position: relative;
} }
.tile-value { .tile-value {
font-size: 32px; font-size: var(--value-font-size);
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1.2; line-height: 1;
display: flex; display: flex;
align-items: baseline; align-items: baseline;
justify-content: center; gap: 4px;
gap: 6px; letter-spacing: -0.025em;
width: 100%;
} }
.tile-unit { .tile-unit {
font-size: 18px; font-size: var(--unit-font-size);
font-weight: 400; 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 { .tile-description {
font-size: 12px; font-size: var(--label-font-size);
color: ${cssManager.bdTheme('#888', '#777')}; color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
margin-top: 8px; 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 { .gauge-container {
width: 100%; width: 140px;
height: 80px; height: 70px;
position: relative; position: relative;
display: flex;
align-items: center;
justify-content: center;
} }
.gauge-svg { .gauge-svg {
@ -201,96 +237,130 @@ export class DeesStatsGrid extends DeesElement {
.gauge-background { .gauge-background {
fill: none; fill: none;
stroke: ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')}; stroke: ${cssManager.bdTheme('hsl(214.3 31.8% 91.4%)', 'hsl(215 20.2% 21.8%)')};
stroke-width: 6; stroke-width: 8;
} }
.gauge-fill { .gauge-fill {
fill: none; fill: none;
stroke-width: 6; stroke-width: 8;
stroke-linecap: round; stroke-linecap: round;
transition: stroke-dashoffset 0.5s ease; transition: stroke-dashoffset 0.6s cubic-bezier(0.4, 0, 0.2, 1);
} }
.gauge-text { .gauge-text {
fill: ${cssManager.bdTheme('#333', '#fff')}; fill: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
font-size: 18px; font-size: var(--value-font-size);
font-weight: 600; font-weight: 600;
text-anchor: middle; 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%; width: 100%;
height: 24px;
background: ${cssManager.bdTheme('#f0f0f0', '#2a2a2a')};
border-radius: 12px;
overflow: hidden;
position: relative; 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 { .percentage-fill {
height: 100%; height: 100%;
background: ${cssManager.bdTheme('#0084ff', '#0066cc')}; background: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
transition: width 0.5s ease; transition: width 0.6s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 12px; border-radius: 4px;
}
.percentage-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 12px;
font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')};
} }
/* Trend Styles */
.trend-container { .trend-container {
width: 100%; width: 100%;
height: 100%;
position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: center; gap: 8px;
align-items: center; }
gap: 4px;
.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 { .trend-svg {
width: 100%; width: 100%;
height: 40px; height: 100%;
flex-shrink: 0; display: block;
} }
.trend-line { .trend-line {
fill: none; 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-width: 2;
stroke-linejoin: round;
stroke-linecap: round;
} }
.trend-area { .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 { .text-value {
font-size: 32px; font-size: var(--value-font-size);
font-weight: 600; font-weight: 600;
color: ${cssManager.bdTheme('#333', '#fff')}; color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
} letter-spacing: -0.025em;
.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;
} }
/* Context Menu */
dees-contextmenu { dees-contextmenu {
position: fixed; position: fixed;
z-index: 1000; z-index: 1000;
@ -306,10 +376,14 @@ export class DeesStatsGrid extends DeesElement {
return html` return html`
${this.gridActions.length > 0 ? html` ${this.gridActions.length > 0 ? html`
<div class="grid-header"> <div class="grid-header">
<div class="grid-title">Statistics</div> <div class="grid-title"></div>
<div class="grid-actions"> <div class="grid-actions">
${this.gridActions.map(action => html` ${this.gridActions.map(action => html`
<dees-button @clicked=${() => this.handleGridAction(action)}> <dees-button
@clicked=${() => this.handleGridAction(action)}
type="outline"
size="sm"
>
${action.iconName ? html`<dees-icon .iconFA=${action.iconName} size="small"></dees-icon>` : ''} ${action.iconName ? html`<dees-icon .iconFA=${action.iconName} size="small"></dees-icon>` : ''}
${action.name} ${action.name}
</dees-button> </dees-button>
@ -354,7 +428,7 @@ export class DeesStatsGrid extends DeesElement {
${this.renderTileContent(tile)} ${this.renderTileContent(tile)}
</div> </div>
${tile.description ? html` ${tile.description && tile.type !== 'trend' ? html`
<div class="tile-description">${tile.description}</div> <div class="tile-description">${tile.description}</div>
` : ''} ` : ''}
</div> </div>
@ -396,12 +470,31 @@ export class DeesStatsGrid extends DeesElement {
const value = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value); const value = typeof tile.value === 'number' ? tile.value : parseFloat(tile.value);
const options = tile.gaugeOptions || { min: 0, max: 100 }; const options = tile.gaugeOptions || { min: 0, max: 100 };
const percentage = ((value - options.min) / (options.max - options.min)) * 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) { 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) { if (value >= threshold.value) {
strokeColor = threshold.color; strokeColor = threshold.color;
break; break;
@ -410,29 +503,28 @@ export class DeesStatsGrid extends DeesElement {
} }
return html` return html`
<div class="gauge-container"> <div class="gauge-wrapper">
<svg class="gauge-svg" viewBox="0 0 80 80"> <div class="gauge-container">
<circle <svg class="gauge-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="xMidYMid meet">
class="gauge-background" <!-- Background arc -->
cx="40" <path
cy="40" class="gauge-background"
r="30" d="${arcPath}"
transform="rotate(-90 40 40)" />
/> <!-- Filled arc -->
<circle <path
class="gauge-fill" class="gauge-fill"
cx="40" d="${arcPath}"
cy="40" stroke="${strokeColor}"
r="30" stroke-dasharray="${circumference}"
transform="rotate(-90 40 40)" stroke-dashoffset="${strokeDashoffset}"
stroke="${strokeColor}" />
stroke-dasharray="${strokeDasharray}" <!-- Value text -->
stroke-dashoffset="${strokeDashoffset}" <text class="gauge-text" x="${centerX}" y="${centerY}">
/> <tspan>${value}</tspan>${tile.unit ? html`<tspan class="gauge-unit" dx="4">${tile.unit}</tspan>` : ''}
<text class="gauge-text" x="40" y="40" dy="0.35em"> </text>
${value}${tile.unit || ''} </svg>
</text> </div>
</svg>
</div> </div>
`; `;
} }
@ -442,12 +534,14 @@ export class DeesStatsGrid extends DeesElement {
const percentage = Math.min(100, Math.max(0, value)); const percentage = Math.min(100, Math.max(0, value));
return html` return html`
<div class="percentage-container"> <div class="percentage-wrapper">
<div <div class="percentage-value">${percentage}%</div>
class="percentage-fill" <div class="percentage-bar">
style="width: ${percentage}%; ${tile.color ? `background: ${tile.color}` : ''}" <div
></div> class="percentage-fill"
<div class="percentage-text">${percentage}%</div> style="width: ${percentage}%; ${tile.color ? `background: ${tile.color}` : ''}"
></div>
</div>
</div> </div>
`; `;
} }
@ -461,11 +555,14 @@ export class DeesStatsGrid extends DeesElement {
const max = Math.max(...data); const max = Math.max(...data);
const min = Math.min(...data); const min = Math.min(...data);
const range = max - min || 1; const range = max - min || 1;
const width = 200; const width = 300;
const height = 40; const height = 32;
// Add padding to prevent clipping
const padding = 2;
const points = data.map((value, index) => { const points = data.map((value, index) => {
const x = (index / (data.length - 1)) * width; 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}`; return `${x},${y}`;
}).join(' '); }).join(' ');
@ -473,13 +570,16 @@ export class DeesStatsGrid extends DeesElement {
return html` return html`
<div class="trend-container"> <div class="trend-container">
<svg class="trend-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none"> <div class="trend-header">
<polygon class="trend-area" points="${areaPoints}" /> <span class="trend-value">${tile.value}</span>
<polyline class="trend-line" points="${points}" /> ${tile.unit ? html`<span class="trend-unit">${tile.unit}</span>` : ''}
</svg> ${tile.description ? html`<span class="trend-label">${tile.description}</span>` : ''}
<div class="trend-value"> </div>
<span>${tile.value}</span> <div class="trend-graph">
${tile.unit ? html`<span class="tile-unit">${tile.unit}</span>` : ''} <svg class="trend-svg" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none">
<polygon class="trend-area" points="${areaPoints}" />
<polyline class="trend-line" points="${points}" />
</svg>
</div> </div>
</div> </div>
`; `;