feat(components): add large set of new UI components and demos, reorganize groups, and bump a few dependencies
This commit is contained in:
@@ -15,7 +15,7 @@ import type { HLJSApi } from 'highlight.js';
|
||||
import * as smartstring from '@push.rocks/smartstring';
|
||||
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
|
||||
import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
|
||||
import { DeesServiceLibLoader } from '../../../services/index.js';
|
||||
|
||||
declare global {
|
||||
@@ -27,7 +27,7 @@ declare global {
|
||||
@customElement('dees-dataview-codebox')
|
||||
export class DeesDataviewCodebox extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroup = 'Data View';
|
||||
public static demoGroups = ['Data View', 'Workspace'];
|
||||
|
||||
@property()
|
||||
accessor progLang: string = 'typescript';
|
||||
@@ -206,9 +206,7 @@ export class DeesDataviewCodebox extends DeesElement {
|
||||
}}"
|
||||
>
|
||||
<div class="appbar">
|
||||
<dees-windowcontrols type="mac" position="left"></dees-windowcontrols>
|
||||
<div class="fileName">index.ts</div>
|
||||
<dees-windowcontrols type="mac" position="right"></dees-windowcontrols>
|
||||
</div>
|
||||
<div class="codegrid">
|
||||
<div class="lineNumbers">
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '@design.estate/dees-element';
|
||||
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
import { DeesContextmenu } from '../../dees-contextmenu/dees-contextmenu.js';
|
||||
import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
declare global {
|
||||
@@ -27,7 +27,7 @@ declare global {
|
||||
@customElement('dees-dataview-statusobject')
|
||||
export class DeesDataviewStatusobject extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroup = 'Data View';
|
||||
public static demoGroups = ['Data View'];
|
||||
|
||||
@property({ type: Object }) accessor statusObject: tsclass.code.IStatusObject;
|
||||
|
||||
|
||||
@@ -0,0 +1,744 @@
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
import '../../00group-layout/dees-panel/dees-panel.js';
|
||||
import type { IStatsTile, ICpuCore, IPartitionData, IDiskData } from '../dees-statsgrid/dees-statsgrid.js';
|
||||
|
||||
// Helper function to generate random CPU core data
|
||||
const generateCpuCores = (count: number): ICpuCore[] => {
|
||||
return Array.from({ length: count }, (_, i) => ({
|
||||
id: i,
|
||||
usage: Math.round(Math.random() * 100),
|
||||
label: `${i}`,
|
||||
}));
|
||||
};
|
||||
|
||||
export const demoFunc = () => {
|
||||
return html`
|
||||
<dees-demowrapper>
|
||||
<style>
|
||||
${css`
|
||||
.demo-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
dees-panel {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
dees-panel:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.tile-config {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.config-section {
|
||||
padding: 16px;
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(215 20.2% 16.8%)')};
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.config-title {
|
||||
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>
|
||||
|
||||
<div class="demo-container">
|
||||
<dees-panel .title=${'1. Comprehensive Dashboard'} .subtitle=${'Full-featured stats grid with various tile types, actions, and Lucide icons'}>
|
||||
<dees-statsgrid
|
||||
.tiles=${[
|
||||
{
|
||||
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 () => {
|
||||
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: 'users',
|
||||
title: 'Active Users',
|
||||
value: 3847,
|
||||
type: 'number',
|
||||
icon: 'lucide:users',
|
||||
description: '324 new this week',
|
||||
actions: [
|
||||
{
|
||||
name: 'View User List',
|
||||
iconName: 'lucide:list',
|
||||
action: async () => {
|
||||
const output = document.querySelector('#action-output');
|
||||
if (output) {
|
||||
output.textContent = 'Opening user list...';
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
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%)' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'storage',
|
||||
title: 'Storage Used',
|
||||
value: 65,
|
||||
type: 'percentage',
|
||||
icon: 'lucide:hard-drive',
|
||||
description: '650 GB of 1 TB',
|
||||
},
|
||||
{
|
||||
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: 'Refresh',
|
||||
iconName: 'lucide:refresh-cw',
|
||||
action: async () => {
|
||||
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}
|
||||
.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}
|
||||
></dees-statsgrid>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'4. CPU Cores Visualization'} .subtitle=${'Vertical bar visualization for multi-core CPU usage with column spanning'}>
|
||||
<dees-statsgrid
|
||||
id="cpu-cores-grid"
|
||||
.tiles=${[
|
||||
{
|
||||
id: 'cpu-cores-8',
|
||||
title: 'CPU Cores (8-core)',
|
||||
value: 0,
|
||||
type: 'cpuCores',
|
||||
icon: 'lucide:cpu',
|
||||
columnSpan: 2,
|
||||
coresData: generateCpuCores(8),
|
||||
description: 'Intel i7 - 8 cores'
|
||||
},
|
||||
{
|
||||
id: 'memory',
|
||||
title: 'Memory Usage',
|
||||
value: 68,
|
||||
type: 'percentage',
|
||||
icon: 'lucide:database',
|
||||
description: '13.6 GB of 20 GB'
|
||||
},
|
||||
{
|
||||
id: 'cpu-cores-16',
|
||||
title: 'CPU Cores (16-core)',
|
||||
value: 0,
|
||||
type: 'cpuCores',
|
||||
icon: 'lucide:cpu',
|
||||
columnSpan: 2,
|
||||
coresData: generateCpuCores(16),
|
||||
description: 'AMD Ryzen 9 - 16 cores'
|
||||
},
|
||||
{
|
||||
id: 'network',
|
||||
title: 'Network I/O',
|
||||
value: 245,
|
||||
unit: 'MB/s',
|
||||
type: 'trend',
|
||||
icon: 'lucide:network',
|
||||
trendData: [200, 220, 235, 240, 238, 245],
|
||||
description: 'throughput'
|
||||
},
|
||||
{
|
||||
id: 'cpu-cores-32',
|
||||
title: 'Server CPU (32-core)',
|
||||
value: 0,
|
||||
type: 'cpuCores',
|
||||
icon: 'lucide:server',
|
||||
columnSpan: 3,
|
||||
coresData: generateCpuCores(32),
|
||||
description: 'AMD EPYC - 32 cores'
|
||||
},
|
||||
{
|
||||
id: 'disk',
|
||||
title: 'Disk Usage',
|
||||
value: 42,
|
||||
type: 'percentage',
|
||||
icon: 'lucide:hard-drive',
|
||||
description: '420 GB of 1 TB'
|
||||
}
|
||||
]}
|
||||
.gridActions=${[
|
||||
{
|
||||
name: 'Randomize',
|
||||
iconName: 'lucide:shuffle',
|
||||
action: async () => {
|
||||
const grid = document.querySelector('#cpu-cores-grid') as any;
|
||||
if (!grid) return;
|
||||
const tiles = grid.tiles.map((tile: any) => {
|
||||
if (tile.type === 'cpuCores' && tile.coresData) {
|
||||
return {
|
||||
...tile,
|
||||
coresData: tile.coresData.map((core: any) => ({
|
||||
...core,
|
||||
usage: Math.round(Math.random() * 100)
|
||||
}))
|
||||
};
|
||||
}
|
||||
return tile;
|
||||
});
|
||||
grid.tiles = tiles;
|
||||
}
|
||||
}
|
||||
]}
|
||||
.minTileWidth=${250}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'5. Interactive Features'} .subtitle=${'Tiles with actions and real-time updates'}>
|
||||
<dees-statsgrid
|
||||
id="interactive-grid"
|
||||
.tiles=${[
|
||||
{
|
||||
id: 'live-cpu',
|
||||
title: 'Live CPU',
|
||||
value: 45,
|
||||
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%)' }
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
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') as any;
|
||||
if (!grid) return;
|
||||
const tile = grid.tiles.find((t: any) => 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') as any;
|
||||
if (!grid) return;
|
||||
const tile = grid.tiles.find((t: any) => t.id === 'counter');
|
||||
tile.value = 0;
|
||||
grid.tiles = [...grid.tiles];
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]}
|
||||
.gridActions=${[
|
||||
{
|
||||
name: 'Start Live Updates',
|
||||
iconName: 'lucide:play',
|
||||
action: async function() {
|
||||
// Toggle live updates
|
||||
if (!(window as any).liveUpdateInterval) {
|
||||
(window as any).liveUpdateInterval = setInterval(() => {
|
||||
const grid = document.querySelector('#interactive-grid') as any;
|
||||
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 as any).liveUpdateInterval);
|
||||
(window as any).liveUpdateInterval = null;
|
||||
this.name = 'Start Live Updates';
|
||||
this.iconName = 'lucide:play';
|
||||
}
|
||||
}
|
||||
}
|
||||
]}
|
||||
.minTileWidth=${250}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
</dees-panel>
|
||||
|
||||
<dees-panel .title=${'6. 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>
|
||||
|
||||
<dees-panel .title=${'7. Disk & Storage Tiles'} .subtitle=${'Partition and physical disk visualization tiles'}>
|
||||
<dees-statsgrid
|
||||
.tiles=${[
|
||||
{
|
||||
id: 'root-partition',
|
||||
title: 'Root Partition',
|
||||
value: 0,
|
||||
type: 'partition',
|
||||
icon: 'lucide:folder-root',
|
||||
partitionData: {
|
||||
used: 698_341_425_152, // ~650 GB
|
||||
total: 1_073_741_824_000, // ~1 TB
|
||||
filesystem: 'ext4',
|
||||
mountPoint: '/'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'home-partition',
|
||||
title: 'Home Partition',
|
||||
value: 0,
|
||||
type: 'partition',
|
||||
icon: 'lucide:home',
|
||||
partitionData: {
|
||||
used: 214_748_364_800, // ~200 GB
|
||||
total: 536_870_912_000, // ~500 GB
|
||||
filesystem: 'ext4',
|
||||
mountPoint: '/home'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'data-partition',
|
||||
title: 'Data Partition',
|
||||
value: 0,
|
||||
type: 'partition',
|
||||
icon: 'lucide:database',
|
||||
partitionData: {
|
||||
used: 1_932_735_283_200, // ~1.8 TB (90% - critical)
|
||||
total: 2_147_483_648_000, // ~2 TB
|
||||
filesystem: 'xfs',
|
||||
mountPoint: '/data'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'nvme-ssd',
|
||||
title: 'Primary NVMe',
|
||||
value: 0,
|
||||
type: 'disk',
|
||||
icon: 'lucide:hard-drive',
|
||||
columnSpan: 2,
|
||||
diskData: {
|
||||
capacity: 2_000_000_000_000, // 2 TB
|
||||
model: 'Samsung 990 Pro',
|
||||
type: 'nvme',
|
||||
iops: {
|
||||
read: 7450,
|
||||
write: 6900
|
||||
},
|
||||
health: 98
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'sata-ssd',
|
||||
title: 'Secondary SSD',
|
||||
value: 0,
|
||||
type: 'disk',
|
||||
icon: 'lucide:hard-drive',
|
||||
diskData: {
|
||||
capacity: 1_000_000_000_000, // 1 TB
|
||||
model: 'Crucial MX500',
|
||||
type: 'ssd',
|
||||
iops: {
|
||||
read: 560,
|
||||
write: 510
|
||||
},
|
||||
health: 85
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'hdd-storage',
|
||||
title: 'Backup HDD',
|
||||
value: 0,
|
||||
type: 'disk',
|
||||
icon: 'lucide:archive',
|
||||
diskData: {
|
||||
capacity: 8_000_000_000_000, // 8 TB
|
||||
model: 'Seagate IronWolf',
|
||||
type: 'hdd',
|
||||
iops: {
|
||||
read: 210,
|
||||
write: 195
|
||||
},
|
||||
health: 42
|
||||
}
|
||||
}
|
||||
]}
|
||||
.minTileWidth=${280}
|
||||
.gap=${16}
|
||||
></dees-statsgrid>
|
||||
|
||||
<div class="tile-config">
|
||||
<div class="config-section">
|
||||
<div class="config-title">Partition Tile Properties</div>
|
||||
<div class="config-description">
|
||||
<ul style="margin: 8px 0; padding-left: 20px;">
|
||||
<li><strong>partitionData.used:</strong> Used space in bytes (auto-formatted)</li>
|
||||
<li><strong>partitionData.total:</strong> Total capacity in bytes</li>
|
||||
<li><strong>partitionData.filesystem:</strong> Filesystem type (ext4, xfs, ntfs)</li>
|
||||
<li><strong>partitionData.mountPoint:</strong> Mount point path (optional)</li>
|
||||
</ul>
|
||||
Color thresholds: Normal (<75%), Warning (75-90%), Critical (>90%)
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<div class="config-title">Disk Tile Properties</div>
|
||||
<div class="config-description">
|
||||
<ul style="margin: 8px 0; padding-left: 20px;">
|
||||
<li><strong>diskData.capacity:</strong> Total capacity in bytes</li>
|
||||
<li><strong>diskData.model:</strong> Disk model name (optional)</li>
|
||||
<li><strong>diskData.type:</strong> Disk type: 'ssd', 'hdd', or 'nvme'</li>
|
||||
<li><strong>diskData.iops:</strong> Read/write IOPS (optional)</li>
|
||||
<li><strong>diskData.health:</strong> Health percentage 0-100 (optional)</li>
|
||||
</ul>
|
||||
Health thresholds: Good (70-100%), Warning (30-70%), Critical (<30%)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dees-panel>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Cleanup live updates on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if ((window as any).liveUpdateInterval) {
|
||||
clearInterval((window as any).liveUpdateInterval);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</dees-demowrapper>
|
||||
`;
|
||||
};
|
||||
1261
ts_web/elements/00group-dataview/dees-statsgrid/dees-statsgrid.ts
Normal file
1261
ts_web/elements/00group-dataview/dees-statsgrid/dees-statsgrid.ts
Normal file
File diff suppressed because it is too large
Load Diff
1
ts_web/elements/00group-dataview/dees-statsgrid/index.ts
Normal file
1
ts_web/elements/00group-dataview/dees-statsgrid/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-statsgrid.js';
|
||||
115
ts_web/elements/00group-dataview/dees-table/data.ts
Normal file
115
ts_web/elements/00group-dataview/dees-table/data.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
import type { Column, TDisplayFunction } from './types.js';
|
||||
|
||||
export function computeColumnsFromDisplayFunction<T>(
|
||||
displayFunction: TDisplayFunction<T>,
|
||||
data: T[]
|
||||
): Column<T>[] {
|
||||
if (!data || data.length === 0) return [];
|
||||
const firstTransformedItem = displayFunction(data[0]);
|
||||
const keys: string[] = Object.keys(firstTransformedItem);
|
||||
return keys.map((key) => ({
|
||||
key,
|
||||
header: key,
|
||||
value: (row: T) => displayFunction(row)[key],
|
||||
}));
|
||||
}
|
||||
|
||||
export function computeEffectiveColumns<T>(
|
||||
columns: Column<T>[] | undefined,
|
||||
augmentFromDisplayFunction: boolean,
|
||||
displayFunction: TDisplayFunction<T>,
|
||||
data: T[]
|
||||
): Column<T>[] {
|
||||
const base = (columns || []).slice();
|
||||
if (!augmentFromDisplayFunction) return base;
|
||||
const fromDisplay = computeColumnsFromDisplayFunction(displayFunction, data);
|
||||
const existingKeys = new Set(base.map((c) => String(c.key)));
|
||||
for (const col of fromDisplay) {
|
||||
if (!existingKeys.has(String(col.key))) {
|
||||
base.push(col);
|
||||
}
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
export function getCellValue<T>(row: T, col: Column<T>, displayFunction?: TDisplayFunction<T>): any {
|
||||
return col.value ? col.value(row) : (row as any)[col.key as any];
|
||||
}
|
||||
|
||||
export function getViewData<T>(
|
||||
data: T[],
|
||||
effectiveColumns: Column<T>[],
|
||||
sortKey?: string,
|
||||
sortDir?: 'asc' | 'desc' | null,
|
||||
filterText?: string,
|
||||
columnFilters?: Record<string, string>,
|
||||
filterMode: 'table' | 'data' = 'table',
|
||||
lucenePredicate?: (row: T) => boolean
|
||||
): T[] {
|
||||
let arr = data.slice();
|
||||
const ft = (filterText || '').trim().toLowerCase();
|
||||
const cf = columnFilters || {};
|
||||
const cfKeys = Object.keys(cf).filter((k) => (cf[k] ?? '').trim().length > 0);
|
||||
if (ft || cfKeys.length > 0) {
|
||||
arr = arr.filter((row) => {
|
||||
// column filters (AND across columns)
|
||||
for (const k of cfKeys) {
|
||||
if (filterMode === 'data') {
|
||||
// raw object check for that key
|
||||
const val = (row as any)[k];
|
||||
const s = String(val ?? '').toLowerCase();
|
||||
const needle = String(cf[k]).toLowerCase();
|
||||
if (!s.includes(needle)) return false;
|
||||
} else {
|
||||
const col = effectiveColumns.find((c) => String(c.key) === k);
|
||||
if (!col || col.hidden || col.filterable === false) continue;
|
||||
const val = getCellValue(row, col);
|
||||
const s = String(val ?? '').toLowerCase();
|
||||
const needle = String(cf[k]).toLowerCase();
|
||||
if (!s.includes(needle)) return false;
|
||||
}
|
||||
}
|
||||
// global filter (OR across visible columns) or lucene predicate
|
||||
if (ft) {
|
||||
if (lucenePredicate) {
|
||||
if (!lucenePredicate(row)) return false;
|
||||
return true;
|
||||
}
|
||||
let any = false;
|
||||
if (filterMode === 'data') {
|
||||
for (const val of Object.values(row as any)) {
|
||||
const s = String(val ?? '').toLowerCase();
|
||||
if (s.includes(ft)) { any = true; break; }
|
||||
}
|
||||
} else {
|
||||
for (const col of effectiveColumns) {
|
||||
if (col.hidden) continue;
|
||||
const val = getCellValue(row, col);
|
||||
const s = String(val ?? '').toLowerCase();
|
||||
if (s.includes(ft)) { any = true; break; }
|
||||
}
|
||||
}
|
||||
if (!any) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
if (!sortKey || !sortDir) return arr;
|
||||
const col = effectiveColumns.find((c) => String(c.key) === sortKey);
|
||||
if (!col) return arr;
|
||||
const dir = sortDir === 'asc' ? 1 : -1;
|
||||
arr.sort((a, b) => {
|
||||
const va = getCellValue(a, col);
|
||||
const vb = getCellValue(b, col);
|
||||
if (va == null && vb == null) return 0;
|
||||
if (va == null) return -1 * dir;
|
||||
if (vb == null) return 1 * dir;
|
||||
if (typeof va === 'number' && typeof vb === 'number') return (va - vb) * dir;
|
||||
const sa = String(va).toLowerCase();
|
||||
const sb = String(vb).toLowerCase();
|
||||
if (sa < sb) return -1 * dir;
|
||||
if (sa > sb) return 1 * dir;
|
||||
return 0;
|
||||
});
|
||||
return arr;
|
||||
}
|
||||
678
ts_web/elements/00group-dataview/dees-table/dees-table.demo.ts
Normal file
678
ts_web/elements/00group-dataview/dees-table/dees-table.demo.ts
Normal file
@@ -0,0 +1,678 @@
|
||||
import { type ITableAction } from './dees-table.js';
|
||||
import * as plugins from '../../00plugins.js';
|
||||
import { html, css, cssManager } from '@design.estate/dees-element';
|
||||
|
||||
interface ITableDemoData {
|
||||
date: string;
|
||||
amount: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const demoFunc = () => html`
|
||||
<style>
|
||||
${css`
|
||||
.demoWrapper {
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 32px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95%)', 'hsl(0 0% 5%)')};
|
||||
overflow-y: auto;
|
||||
}
|
||||
.demo-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.demo-section {
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
.demo-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
}
|
||||
.demo-description {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
z-index: 1000;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div class="demoWrapper">
|
||||
<dees-button class="theme-toggle" @click=${() => {
|
||||
document.body.classList.toggle('bright');
|
||||
document.body.classList.toggle('dark');
|
||||
}}>Toggle Theme</dees-button>
|
||||
|
||||
<div class="demo-container">
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Basic Table with Actions</h2>
|
||||
<p class="demo-description">A standard table with row actions, editable fields, and context menu support. Double-click on descriptions to edit. Grid lines are enabled by default.</p>
|
||||
<dees-table
|
||||
heading1="Current Account Statement"
|
||||
heading2="Bunq - Payment Account 2 - April 2021"
|
||||
.editableFields="${['description']}"
|
||||
.data=${[
|
||||
{
|
||||
date: '2021-04-01',
|
||||
amount: '2464.65 €',
|
||||
description: 'Printing Paper (Office Supplies) - STAPLES BREMEN',
|
||||
},
|
||||
{
|
||||
date: '2021-04-02',
|
||||
amount: '165.65 €',
|
||||
description: 'Logitech Mouse (Hardware) - logi.com OnlineShop',
|
||||
},
|
||||
{
|
||||
date: '2021-04-03',
|
||||
amount: '2999,00 €',
|
||||
description: 'Macbook Pro 16inch (Hardware) - Apple.de OnlineShop',
|
||||
},
|
||||
{
|
||||
date: '2021-04-01',
|
||||
amount: '2464.65 €',
|
||||
description: 'Office-Supplies - STAPLES BREMEN',
|
||||
},
|
||||
{
|
||||
date: '2021-04-01',
|
||||
amount: '2464.65 €',
|
||||
description: 'Office-Supplies - STAPLES BREMEN',
|
||||
},
|
||||
]}
|
||||
dataName="transactions"
|
||||
.dataActions="${[
|
||||
{
|
||||
name: 'upload',
|
||||
iconName: 'bell',
|
||||
useTableBehaviour: 'upload',
|
||||
type: ['inRow'],
|
||||
actionFunc: async (optionsArg) => {
|
||||
alert(optionsArg.item.amount);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'visibility',
|
||||
iconName: 'copy',
|
||||
type: ['inRow'],
|
||||
useTableBehaviour: 'preview',
|
||||
actionFunc: async (itemArg: any) => {},
|
||||
},
|
||||
{
|
||||
name: 'create new',
|
||||
iconName: 'instagram',
|
||||
type: ['header'],
|
||||
useTableBehaviour: 'preview',
|
||||
actionFunc: async (itemArg: any) => {},
|
||||
},
|
||||
{
|
||||
name: 'to gallery',
|
||||
iconName: 'message',
|
||||
type: ['footer'],
|
||||
useTableBehaviour: 'preview',
|
||||
actionFunc: async (itemArg: any) => {},
|
||||
},
|
||||
{
|
||||
name: 'copy',
|
||||
iconName: 'copySolid',
|
||||
type: ['contextmenu', 'inRow'],
|
||||
action: async () => {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'edit (from demo)',
|
||||
iconName: 'penToSquare',
|
||||
type: ['contextmenu'],
|
||||
action: async () => {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'paste',
|
||||
iconName: 'pasteSolid',
|
||||
type: ['contextmenu'],
|
||||
action: async () => {
|
||||
return null;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'preview',
|
||||
type: ['doubleClick', 'contextmenu'],
|
||||
iconName: 'eye',
|
||||
actionFunc: async (itemArg) => {
|
||||
alert(itemArg.item.amount);
|
||||
return null;
|
||||
},
|
||||
}
|
||||
] as ITableAction[]}"
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Table with Vertical Lines</h2>
|
||||
<p class="demo-description">Enhanced column separation for better data tracking.</p>
|
||||
<dees-table
|
||||
heading1="Product Inventory"
|
||||
heading2="Current stock levels across warehouses"
|
||||
.showVerticalLines=${true}
|
||||
.data=${[
|
||||
{
|
||||
product: 'MacBook Pro 16"',
|
||||
warehouse_a: '45',
|
||||
warehouse_b: '32',
|
||||
warehouse_c: '28',
|
||||
total: '105',
|
||||
status: '✓ In Stock'
|
||||
},
|
||||
{
|
||||
product: 'iPhone 15 Pro',
|
||||
warehouse_a: '120',
|
||||
warehouse_b: '89',
|
||||
warehouse_c: '156',
|
||||
total: '365',
|
||||
status: '✓ In Stock'
|
||||
},
|
||||
{
|
||||
product: 'AirPods Pro',
|
||||
warehouse_a: '0',
|
||||
warehouse_b: '12',
|
||||
warehouse_c: '5',
|
||||
total: '17',
|
||||
status: '⚠ Low Stock'
|
||||
},
|
||||
{
|
||||
product: 'iPad Air',
|
||||
warehouse_a: '23',
|
||||
warehouse_b: '45',
|
||||
warehouse_c: '67',
|
||||
total: '135',
|
||||
status: '✓ In Stock'
|
||||
}
|
||||
]}
|
||||
dataName="products"
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Table with Full Grid</h2>
|
||||
<p class="demo-description">Complete grid lines for maximum readability and structure.</p>
|
||||
<dees-table
|
||||
heading1="Server Monitoring Dashboard"
|
||||
heading2="Real-time metrics across regions"
|
||||
.showGrid=${true}
|
||||
.data=${[
|
||||
{
|
||||
server: 'API-1',
|
||||
region: 'US-East',
|
||||
cpu: '45%',
|
||||
memory: '62%',
|
||||
disk: '78%',
|
||||
latency: '12ms',
|
||||
uptime: '99.9%',
|
||||
status: '🟢 Healthy'
|
||||
},
|
||||
{
|
||||
server: 'API-2',
|
||||
region: 'EU-West',
|
||||
cpu: '38%',
|
||||
memory: '55%',
|
||||
disk: '45%',
|
||||
latency: '25ms',
|
||||
uptime: '99.8%',
|
||||
status: '🟢 Healthy'
|
||||
},
|
||||
{
|
||||
server: 'DB-Master',
|
||||
region: 'US-East',
|
||||
cpu: '72%',
|
||||
memory: '81%',
|
||||
disk: '92%',
|
||||
latency: '8ms',
|
||||
uptime: '100%',
|
||||
status: '🟡 Warning'
|
||||
},
|
||||
{
|
||||
server: 'DB-Replica',
|
||||
region: 'EU-West',
|
||||
cpu: '23%',
|
||||
memory: '34%',
|
||||
disk: '45%',
|
||||
latency: '15ms',
|
||||
uptime: '99.7%',
|
||||
status: '🟢 Healthy'
|
||||
},
|
||||
{
|
||||
server: 'Cache-1',
|
||||
region: 'AP-South',
|
||||
cpu: '89%',
|
||||
memory: '92%',
|
||||
disk: '12%',
|
||||
latency: '120ms',
|
||||
uptime: '98.5%',
|
||||
status: '🔴 Critical'
|
||||
}
|
||||
]}
|
||||
dataName="servers"
|
||||
.dataActions="${[
|
||||
{
|
||||
name: 'SSH Connect',
|
||||
iconName: 'lucide:terminal',
|
||||
type: ['inRow'],
|
||||
actionFunc: async (optionsArg) => {
|
||||
console.log('Connecting to:', optionsArg.item.server);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'View Logs',
|
||||
iconName: 'lucide:file-text',
|
||||
type: ['inRow', 'contextmenu'],
|
||||
actionFunc: async (optionsArg) => {
|
||||
console.log('Viewing logs for:', optionsArg.item.server);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Restart Server',
|
||||
iconName: 'lucide:refresh-cw',
|
||||
type: ['contextmenu'],
|
||||
actionFunc: async (optionsArg) => {
|
||||
console.log('Restarting:', optionsArg.item.server);
|
||||
},
|
||||
}
|
||||
] as ITableAction[]}"
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Table with Horizontal Lines Only</h2>
|
||||
<p class="demo-description">Emphasis on row separation without column dividers.</p>
|
||||
<dees-table
|
||||
heading1="Sales Performance"
|
||||
heading2="Top performers this quarter"
|
||||
.showHorizontalLines=${true}
|
||||
.showVerticalLines=${false}
|
||||
.data=${[
|
||||
{
|
||||
salesperson: 'Emily Johnson',
|
||||
region: 'North America',
|
||||
deals_closed: '42',
|
||||
revenue: '$1.2M',
|
||||
quota_achievement: '128%',
|
||||
rating: '⭐⭐⭐⭐⭐'
|
||||
},
|
||||
{
|
||||
salesperson: 'Michael Chen',
|
||||
region: 'Asia Pacific',
|
||||
deals_closed: '38',
|
||||
revenue: '$980K',
|
||||
quota_achievement: '115%',
|
||||
rating: '⭐⭐⭐⭐⭐'
|
||||
},
|
||||
{
|
||||
salesperson: 'Sarah Williams',
|
||||
region: 'Europe',
|
||||
deals_closed: '35',
|
||||
revenue: '$875K',
|
||||
quota_achievement: '108%',
|
||||
rating: '⭐⭐⭐⭐'
|
||||
},
|
||||
{
|
||||
salesperson: 'David Garcia',
|
||||
region: 'Latin America',
|
||||
deals_closed: '31',
|
||||
revenue: '$750K',
|
||||
quota_achievement: '95%',
|
||||
rating: '⭐⭐⭐⭐'
|
||||
}
|
||||
]}
|
||||
dataName="sales reps"
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Simple Table (No Grid)</h2>
|
||||
<p class="demo-description">Clean, minimal design without grid lines. Set showGrid to false to disable the default grid.</p>
|
||||
<dees-table
|
||||
heading1="Team Members"
|
||||
heading2="Engineering Department"
|
||||
.showGrid=${false}
|
||||
.data=${[
|
||||
{
|
||||
name: 'Alice Johnson',
|
||||
role: 'Lead Engineer',
|
||||
email: 'alice@company.com',
|
||||
location: 'San Francisco',
|
||||
joined: '2020-03-15'
|
||||
},
|
||||
{
|
||||
name: 'Bob Smith',
|
||||
role: 'Senior Developer',
|
||||
email: 'bob@company.com',
|
||||
location: 'New York',
|
||||
joined: '2019-07-22'
|
||||
},
|
||||
{
|
||||
name: 'Charlie Davis',
|
||||
role: 'DevOps Engineer',
|
||||
email: 'charlie@company.com',
|
||||
location: 'London',
|
||||
joined: '2021-01-10'
|
||||
},
|
||||
{
|
||||
name: 'Diana Martinez',
|
||||
role: 'Frontend Developer',
|
||||
email: 'diana@company.com',
|
||||
location: 'Barcelona',
|
||||
joined: '2022-05-18'
|
||||
}
|
||||
]}
|
||||
dataName="team members"
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Table with Custom Display Function</h2>
|
||||
<p class="demo-description">Transform data for display using custom formatting.</p>
|
||||
<dees-table
|
||||
heading1="Sales Report"
|
||||
heading2="Q4 2023 Performance"
|
||||
.data=${[
|
||||
{
|
||||
product: 'Enterprise License',
|
||||
units: 45,
|
||||
revenue: 225000,
|
||||
growth: 0.23,
|
||||
forecast: 280000
|
||||
},
|
||||
{
|
||||
product: 'Professional License',
|
||||
units: 128,
|
||||
revenue: 128000,
|
||||
growth: 0.15,
|
||||
forecast: 147000
|
||||
},
|
||||
{
|
||||
product: 'Starter License',
|
||||
units: 342,
|
||||
revenue: 68400,
|
||||
growth: 0.42,
|
||||
forecast: 97000
|
||||
}
|
||||
]}
|
||||
.displayFunction=${(item) => ({
|
||||
Product: item.product,
|
||||
'Units Sold': item.units.toLocaleString(),
|
||||
Revenue: '$' + item.revenue.toLocaleString(),
|
||||
Growth: (item.growth * 100).toFixed(1) + '%',
|
||||
'Q1 2024 Forecast': '$' + item.forecast.toLocaleString()
|
||||
})}
|
||||
dataName="products"
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Empty Table State</h2>
|
||||
<p class="demo-description">How the table looks when no data is available.</p>
|
||||
<dees-table
|
||||
heading1="No Data Available"
|
||||
heading2="This table is currently empty"
|
||||
.data=${[]}
|
||||
dataName="items"
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Schema-First Columns (New)</h2>
|
||||
<p class="demo-description">Defines columns explicitly and renders via schema. No displayFunction needed.</p>
|
||||
<dees-table
|
||||
heading1="Users (Schema-First)"
|
||||
heading2="Columns define rendering and order"
|
||||
.columns=${[
|
||||
{ key: 'name', header: 'Name', sortable: true },
|
||||
{ key: 'email', header: 'Email', renderer: (v: string) => html`<dees-badge>${v}</dees-badge>` },
|
||||
{ key: 'joinedAt', header: 'Joined', renderer: (v: string) => new Date(v).toLocaleDateString() },
|
||||
]}
|
||||
.data=${[
|
||||
{ name: 'Alice', email: 'alice@example.com', joinedAt: '2022-08-01' },
|
||||
{ name: 'Bob', email: 'bob@example.com', joinedAt: '2021-12-11' },
|
||||
{ name: 'Carol', email: 'carol@example.com', joinedAt: '2023-03-22' },
|
||||
]}
|
||||
dataName="users"
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Partial Schema + Augment (New)</h2>
|
||||
<p class="demo-description">Provides only the important columns; the rest are merged in from displayFunction.</p>
|
||||
<dees-table
|
||||
heading1="Users (Partial + Augment)"
|
||||
heading2="Missing columns are derived"
|
||||
.columns=${[
|
||||
{ key: 'name', header: 'Name', sortable: true },
|
||||
]}
|
||||
.displayFunction=${(u: any) => ({ name: u.name, email: u.email, role: u.role })}
|
||||
.augmentFromDisplayFunction=${true}
|
||||
.data=${[
|
||||
{ name: 'Erin', email: 'erin@example.com', role: 'Admin' },
|
||||
{ name: 'Finn', email: 'finn@example.com', role: 'User' },
|
||||
{ name: 'Gina', email: 'gina@example.com', role: 'User' },
|
||||
]}
|
||||
dataName="users"
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section"
|
||||
@selectionChange=${(e: CustomEvent) => { console.log('Selection changed', e.detail); }}
|
||||
@search-changed=${(e: CustomEvent) => {
|
||||
const tbl = document.getElementById('tableFilterSelectDemo') as any;
|
||||
if (tbl) tbl.setFilterText(e.detail.value);
|
||||
}}
|
||||
@search-submit=${(e: CustomEvent) => {
|
||||
const tbl = document.getElementById('tableFilterSelectDemo') as any;
|
||||
if (tbl) tbl.setFilterText(e.detail.value);
|
||||
}}
|
||||
>
|
||||
<h2 class="demo-title">Filtering + Multi-Selection (New)</h2>
|
||||
<p class="demo-description">Use the search bar to filter rows; toggle selection via checkboxes. Click headers to sort.</p>
|
||||
<dees-searchbar></dees-searchbar>
|
||||
<div style="height: 12px"></div>
|
||||
<dees-table
|
||||
id="tableFilterSelectDemo"
|
||||
heading1="Inventory (Filter + Select)"
|
||||
heading2="Try typing to filter and selecting multiple rows"
|
||||
.selectionMode=${'multi'}
|
||||
.rowKey=${'sku'}
|
||||
.columns=${[
|
||||
{ key: 'sku', header: 'SKU', sortable: true },
|
||||
{ key: 'name', header: 'Name', sortable: true },
|
||||
{ key: 'stock', header: 'Stock', sortable: true },
|
||||
]}
|
||||
.data=${[
|
||||
{ sku: 'A-100', name: 'USB-C Cable', stock: 120 },
|
||||
{ sku: 'A-101', name: 'Wireless Mouse', stock: 55 },
|
||||
{ sku: 'A-102', name: 'Laptop Stand', stock: 18 },
|
||||
{ sku: 'B-200', name: 'Keyboard (ISO)', stock: 89 },
|
||||
{ sku: 'B-201', name: 'HDMI Adapter', stock: 0 },
|
||||
{ sku: 'C-300', name: 'Webcam 1080p', stock: 42 },
|
||||
]}
|
||||
dataName="items"
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Column Filters + Sticky Header (New)</h2>
|
||||
<p class="demo-description">Per-column quick filters and sticky header with internal scroll. Try filtering the Name column. Uses --table-max-height var.</p>
|
||||
<style>
|
||||
dees-table[sticky-header] { --table-max-height: 220px; }
|
||||
</style>
|
||||
<dees-table
|
||||
heading1="Employees"
|
||||
heading2="Quick filter per column + sticky header"
|
||||
.showColumnFilters=${true}
|
||||
.stickyHeader=${true}
|
||||
.columns=${[
|
||||
{ key: 'name', header: 'Name', sortable: true },
|
||||
{ key: 'email', header: 'Email', sortable: true },
|
||||
{ key: 'department', header: 'Department', sortable: true },
|
||||
]}
|
||||
.data=${[
|
||||
{ name: 'Alice Johnson', email: 'alice@corp.com', department: 'Engineering' },
|
||||
{ name: 'Bob Smith', email: 'bob@corp.com', department: 'Sales' },
|
||||
{ name: 'Charlie Davis', email: 'charlie@corp.com', department: 'HR' },
|
||||
{ name: 'Diana Martinez', email: 'diana@corp.com', department: 'Engineering' },
|
||||
{ name: 'Ethan Brown', email: 'ethan@corp.com', department: 'Finance' },
|
||||
{ name: 'Fiona Clark', email: 'fiona@corp.com', department: 'Sales' },
|
||||
{ name: 'Grace Lee', email: 'grace@corp.com', department: 'Engineering' },
|
||||
{ name: 'Henry Wilson', email: 'henry@corp.com', department: 'Marketing' },
|
||||
{ name: 'Irene Walker', email: 'irene@corp.com', department: 'Finance' },
|
||||
{ name: 'Jack Turner', email: 'jack@corp.com', department: 'Support' },
|
||||
]}
|
||||
dataName="employees"
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section"
|
||||
@searchRequest=${async (e: CustomEvent) => {
|
||||
const { query } = e.detail || { query: '' };
|
||||
const table = document.getElementById('serverSearchDemo') as any;
|
||||
const baseData = [
|
||||
{ id: 1, name: 'Alice', city: 'Berlin', title: 'Engineer' },
|
||||
{ id: 2, name: 'Bob', city: 'Paris', title: 'Designer' },
|
||||
{ id: 3, name: 'Charlie', city: 'London', title: 'Manager' },
|
||||
{ id: 4, name: 'Diana', city: 'Madrid', title: 'Engineer' },
|
||||
{ id: 5, name: 'Ethan', city: 'Rome', title: 'Support' },
|
||||
];
|
||||
// Simulate async request
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
const q = String(query || '').toLowerCase();
|
||||
const filtered = q
|
||||
? baseData.filter((r) => Object.values(r).some((v) => String(v).toLowerCase().includes(q)))
|
||||
: baseData;
|
||||
table.data = filtered;
|
||||
}}
|
||||
>
|
||||
<h2 class="demo-title">Server Search (New)</h2>
|
||||
<p class="demo-description">Select Server mode, type a query, and watch the table fetch simulated results.</p>
|
||||
<dees-table
|
||||
id="serverSearchDemo"
|
||||
heading1="People (Server Search)"
|
||||
heading2="Click Search, choose Server mode, and type"
|
||||
.columns=${[
|
||||
{ key: 'name', header: 'Name' },
|
||||
{ key: 'city', header: 'City' },
|
||||
{ key: 'title', header: 'Title' },
|
||||
]}
|
||||
.data=${[
|
||||
{ id: 1, name: 'Alice', city: 'Berlin', title: 'Engineer' },
|
||||
{ id: 2, name: 'Bob', city: 'Paris', title: 'Designer' },
|
||||
{ id: 3, name: 'Charlie', city: 'London', title: 'Manager' },
|
||||
{ id: 4, name: 'Diana', city: 'Madrid', title: 'Engineer' },
|
||||
{ id: 5, name: 'Ethan', city: 'Rome', title: 'Support' },
|
||||
]}
|
||||
dataName="people"
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Wide Properties + Many Actions</h2>
|
||||
<p class="demo-description">A table with many columns and rich actions to stress test layout and sticky Actions.</p>
|
||||
<dees-table
|
||||
heading1="People Directory"
|
||||
heading2="Many properties and actions"
|
||||
.columns=${[
|
||||
{ key: 'id', header: 'ID', sortable: true },
|
||||
{ key: 'name', header: 'Name', sortable: true },
|
||||
{ key: 'role', header: 'Role', sortable: true },
|
||||
{ key: 'department', header: 'Department', sortable: true },
|
||||
{ key: 'email', header: 'Email' },
|
||||
{ key: 'phone', header: 'Phone' },
|
||||
{ key: 'location', header: 'Location', sortable: true },
|
||||
{ key: 'status', header: 'Status', sortable: true },
|
||||
{ key: 'createdAt', header: 'Created', sortable: true },
|
||||
{ key: 'updatedAt', header: 'Updated', sortable: true },
|
||||
{ key: 'lastLogin', header: 'Last Login', sortable: true },
|
||||
{ key: 'projects', header: 'Projects' },
|
||||
{ key: 'tags', header: 'Tags' },
|
||||
{ key: 'notes', header: 'Notes' },
|
||||
]}
|
||||
.data=${[
|
||||
{ id: 1, name: 'Alice Johnson', role: 'Engineer', department: 'R&D', email: 'alice@corp.com', phone: '+1 202 555 0111', location: 'Berlin', status: 'Active', createdAt: '2023-01-12', updatedAt: '2024-05-03', lastLogin: '2024-10-01', projects: 5, tags: 'typescript, ui', notes: 'Mentor' },
|
||||
{ id: 2, name: 'Bob Smith', role: 'Designer', department: 'Design', email: 'bob@corp.com', phone: '+1 202 555 0112', location: 'Paris', status: 'Active', createdAt: '2022-11-05', updatedAt: '2024-04-10', lastLogin: '2024-09-28', projects: 8, tags: 'figma, brand', notes: 'Part-time' },
|
||||
{ id: 3, name: 'Charlie Davis', role: 'Manager', department: 'Ops', email: 'charlie@corp.com', phone: '+1 202 555 0113', location: 'London', status: 'On Leave', createdAt: '2021-04-21', updatedAt: '2024-02-15', lastLogin: '2024-08-12', projects: 3, tags: 'sre, leadership', notes: '' },
|
||||
{ id: 4, name: 'Diana Martinez', role: 'Engineer', department: 'Platform', email: 'diana@corp.com', phone: '+1 202 555 0114', location: 'Madrid', status: 'Active', createdAt: '2020-06-30', updatedAt: '2024-06-25', lastLogin: '2024-10-02', projects: 6, tags: 'node, api', notes: 'On-call' },
|
||||
{ id: 5, name: 'Ethan Brown', role: 'Support', department: 'CS', email: 'ethan@corp.com', phone: '+1 202 555 0115', location: 'Rome', status: 'Inactive', createdAt: '2019-09-18', updatedAt: '2024-03-09', lastLogin: '2024-06-19', projects: 2, tags: 'zendesk', notes: 'Rehire' },
|
||||
{ id: 6, name: 'Fiona Clark', role: 'QA', department: 'QA', email: 'fiona@corp.com', phone: '+1 202 555 0116', location: 'Vienna', status: 'Active', createdAt: '2022-03-14', updatedAt: '2024-03-01', lastLogin: '2024-09-07', projects: 7, tags: 'playwright', notes: '' },
|
||||
]}
|
||||
.dataActions=${[
|
||||
{ name: 'View', iconName: 'lucide:eye', type: ['inRow', 'contextmenu'], actionFunc: async ({ item }) => { console.log('view', item); } },
|
||||
{ name: 'Edit', iconName: 'lucide:edit', type: ['inRow', 'contextmenu'], actionFunc: async ({ item }) => { console.log('edit', item); } },
|
||||
{ name: 'Delete', iconName: 'lucide:trash', type: ['inRow', 'contextmenu'], actionFunc: async ({ item }) => { console.log('delete', item); } },
|
||||
{ name: 'Message', iconName: 'lucide:message-square', type: ['inRow'], actionFunc: async ({ item }) => { console.log('message', item); } },
|
||||
{ name: 'History', iconName: 'lucide:clock', type: ['inRow'], actionFunc: async ({ item }) => { console.log('history', item); } },
|
||||
{ name: 'Add New', iconName: 'lucide:plus', type: ['header'], actionFunc: async ({ table }) => { console.log('add'); } },
|
||||
{ name: 'Export CSV', iconName: 'lucide:download', type: ['header'], actionFunc: async ({ table }) => { console.log('export'); } },
|
||||
{ name: 'Bulk Delete', iconName: 'lucide:trash-2', type: ['footer'], actionFunc: async ({ table }) => { console.log('bulk delete'); } },
|
||||
] as ITableAction[]}
|
||||
></dees-table>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h2 class="demo-title">Scrollable Small Height</h2>
|
||||
<p class="demo-description">Same as above, but with many items and a small fixed height to force vertical scrolling inside the table. Actions remain visible on the right; horizontal scroll appears if needed.</p>
|
||||
<style>
|
||||
#scrollSmallHeight { --table-max-height: 240px; }
|
||||
</style>
|
||||
<dees-table
|
||||
id="scrollSmallHeight"
|
||||
.stickyHeader=${true}
|
||||
heading1="People Directory (Scrollable)"
|
||||
heading2="Forced scrolling with many items"
|
||||
.columns=${[
|
||||
{ key: 'id', header: 'ID', sortable: true },
|
||||
{ key: 'name', header: 'Name', sortable: true },
|
||||
{ key: 'role', header: 'Role', sortable: true },
|
||||
{ key: 'department', header: 'Department', sortable: true },
|
||||
{ key: 'email', header: 'Email' },
|
||||
{ key: 'phone', header: 'Phone' },
|
||||
{ key: 'location', header: 'Location', sortable: true },
|
||||
{ key: 'status', header: 'Status', sortable: true },
|
||||
{ key: 'createdAt', header: 'Created', sortable: true },
|
||||
{ key: 'updatedAt', header: 'Updated', sortable: true },
|
||||
{ key: 'lastLogin', header: 'Last Login', sortable: true },
|
||||
{ key: 'projects', header: 'Projects' },
|
||||
{ key: 'tags', header: 'Tags' },
|
||||
{ key: 'notes', header: 'Notes' },
|
||||
]}
|
||||
.data=${Array.from({ length: 100 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: `User ${i + 1}`,
|
||||
role: ['Engineer','Designer','Manager','QA','Support'][i % 5],
|
||||
department: ['R&D','Design','Ops','QA','CS'][i % 5],
|
||||
email: `user${i+1}@corp.com`,
|
||||
phone: `+1 202 555 ${String(1000 + i).slice(-4)}`,
|
||||
location: ['Berlin','Paris','London','Madrid','Rome'][i % 5],
|
||||
status: ['Active','Inactive','On Leave'][i % 3],
|
||||
createdAt: `2023-${String((i%12)+1).padStart(2,'0')}-${String((i%28)+1).padStart(2,'0')}`,
|
||||
updatedAt: `2024-${String(((i+3)%12)+1).padStart(2,'0')}-${String(((i+7)%28)+1).padStart(2,'0')}`,
|
||||
lastLogin: `2024-${String(((i+6)%12)+1).padStart(2,'0')}-${String(((i+10)%28)+1).padStart(2,'0')}`,
|
||||
projects: (i % 12),
|
||||
tags: i % 2 ? 'typescript' : 'design',
|
||||
notes: i % 3 ? '' : 'Note',
|
||||
}))}
|
||||
.dataActions=${[
|
||||
{ name: 'View', iconName: 'lucide:eye', type: ['inRow'], actionFunc: async ({ item }) => {} },
|
||||
{ name: 'Edit', iconName: 'lucide:edit', type: ['inRow'], actionFunc: async ({ item }) => {} },
|
||||
{ name: 'Delete', iconName: 'lucide:trash', type: ['inRow'], actionFunc: async ({ item }) => {} },
|
||||
] as ITableAction[]}
|
||||
></dees-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
843
ts_web/elements/00group-dataview/dees-table/dees-table.ts
Normal file
843
ts_web/elements/00group-dataview/dees-table/dees-table.ts
Normal file
@@ -0,0 +1,843 @@
|
||||
import * as plugins from '../../00plugins.js';
|
||||
import { demoFunc } from './dees-table.demo.js';
|
||||
import { customElement, html, DeesElement, property, type TemplateResult, directives } from '@design.estate/dees-element';
|
||||
|
||||
import { DeesContextmenu } from '../../00group-overlay/dees-contextmenu/dees-contextmenu.js';
|
||||
import * as domtools from '@design.estate/dees-domtools';
|
||||
import { type TIconKey } from '../../00group-utility/dees-icon/dees-icon.js';
|
||||
import { tableStyles } from './styles.js';
|
||||
import type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
|
||||
import {
|
||||
computeColumnsFromDisplayFunction as computeColumnsFromDisplayFunctionFn,
|
||||
computeEffectiveColumns as computeEffectiveColumnsFn,
|
||||
getCellValue as getCellValueFn,
|
||||
getViewData as getViewDataFn,
|
||||
} from './data.js';
|
||||
import { compileLucenePredicate } from './lucene.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
export type { Column, ITableAction, ITableActionDataArg, TDisplayFunction } from './types.js';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'dees-table': DeesTable<any>;
|
||||
}
|
||||
}
|
||||
|
||||
// interfaces moved to ./types.ts and re-exported above
|
||||
|
||||
// the table implementation
|
||||
@customElement('dees-table')
|
||||
export class DeesTable<T> extends DeesElement {
|
||||
public static demo = demoFunc;
|
||||
public static demoGroups = ['Data View'];
|
||||
|
||||
// INSTANCE
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
accessor heading1: string = 'heading 1';
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
accessor heading2: string = 'heading 2';
|
||||
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
accessor data: T[] = [];
|
||||
|
||||
// dees-form compatibility -----------------------------------------
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
accessor key: string;
|
||||
|
||||
@property({
|
||||
type: String,
|
||||
})
|
||||
accessor label: string;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
accessor disabled: boolean = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
accessor required: boolean = false;
|
||||
|
||||
get value() {
|
||||
return this.data;
|
||||
}
|
||||
set value(_valueArg) {}
|
||||
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesTable<T>>();
|
||||
// end dees-form compatibility -----------------------------------------
|
||||
|
||||
/**
|
||||
* What does a row of data represent?
|
||||
*/
|
||||
@property({
|
||||
type: String,
|
||||
reflect: true,
|
||||
})
|
||||
accessor dataName: string;
|
||||
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
})
|
||||
accessor searchable: boolean = true;
|
||||
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
accessor dataActions: ITableAction<T>[] = [];
|
||||
|
||||
// schema-first columns API
|
||||
@property({ attribute: false })
|
||||
accessor columns: Column<T>[] = [];
|
||||
|
||||
/**
|
||||
* Stable row identity for selection and updates. If provided as a function,
|
||||
* it is only usable as a property (not via attribute).
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
accessor rowKey: keyof T | ((row: T) => string) | undefined = undefined;
|
||||
|
||||
/**
|
||||
* When true and columns are provided, merge any missing columns discovered
|
||||
* via displayFunction into the effective schema.
|
||||
*/
|
||||
@property({ type: Boolean })
|
||||
accessor augmentFromDisplayFunction: boolean = false;
|
||||
|
||||
@property({
|
||||
attribute: false,
|
||||
})
|
||||
accessor displayFunction: TDisplayFunction = (itemArg: T) => itemArg as any;
|
||||
|
||||
@property({
|
||||
attribute: false,
|
||||
})
|
||||
accessor reverseDisplayFunction: (itemArg: any) => T = (itemArg: any) => itemArg as T;
|
||||
|
||||
@property({
|
||||
type: Object,
|
||||
})
|
||||
accessor selectedDataRow: T;
|
||||
|
||||
@property({
|
||||
type: Array,
|
||||
})
|
||||
accessor editableFields: string[] = [];
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
attribute: 'show-vertical-lines'
|
||||
})
|
||||
accessor showVerticalLines: boolean = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
attribute: 'show-horizontal-lines'
|
||||
})
|
||||
accessor showHorizontalLines: boolean = false;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
attribute: 'show-grid'
|
||||
})
|
||||
accessor showGrid: boolean = true;
|
||||
|
||||
public files: File[] = [];
|
||||
public fileWeakMap = new WeakMap();
|
||||
|
||||
public dataChangeSubject = new domtools.plugins.smartrx.rxjs.Subject();
|
||||
|
||||
// simple client-side sorting (Phase 1)
|
||||
@property({ attribute: false })
|
||||
accessor sortKey: string | undefined = undefined;
|
||||
@property({ attribute: false })
|
||||
accessor sortDir: 'asc' | 'desc' | null = null;
|
||||
|
||||
// simple client-side filtering (Phase 1)
|
||||
@property({ type: String })
|
||||
accessor filterText: string = '';
|
||||
// per-column quick filters
|
||||
@property({ attribute: false })
|
||||
accessor columnFilters: Record<string, string> = {};
|
||||
@property({ type: Boolean, attribute: 'show-column-filters' })
|
||||
accessor showColumnFilters: boolean = false;
|
||||
@property({ type: Boolean, reflect: true, attribute: 'sticky-header' })
|
||||
accessor stickyHeader: boolean = false;
|
||||
|
||||
// search row state
|
||||
@property({ type: String })
|
||||
accessor searchMode: 'table' | 'data' | 'server' = 'table';
|
||||
private __searchTextSub?: { unsubscribe?: () => void };
|
||||
private __searchModeSub?: { unsubscribe?: () => void };
|
||||
|
||||
// selection (Phase 1)
|
||||
@property({ type: String })
|
||||
accessor selectionMode: 'none' | 'single' | 'multi' = 'none';
|
||||
@property({ attribute: false })
|
||||
accessor selectedIds: Set<string> = new Set();
|
||||
private _rowIdMap = new WeakMap<object, string>();
|
||||
private _rowIdCounter = 0;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public static styles = tableStyles;
|
||||
|
||||
public render(): TemplateResult {
|
||||
const usingColumns = Array.isArray(this.columns) && this.columns.length > 0;
|
||||
const effectiveColumns: Column<T>[] = usingColumns
|
||||
? computeEffectiveColumnsFn(this.columns, this.augmentFromDisplayFunction, this.displayFunction, this.data)
|
||||
: computeColumnsFromDisplayFunctionFn(this.displayFunction, this.data);
|
||||
|
||||
const lucenePred = compileLucenePredicate<T>(
|
||||
this.filterText,
|
||||
this.searchMode === 'data' ? 'data' : 'table',
|
||||
effectiveColumns
|
||||
);
|
||||
|
||||
const viewData = getViewDataFn(
|
||||
this.data,
|
||||
effectiveColumns,
|
||||
this.sortKey,
|
||||
this.sortDir,
|
||||
this.filterText,
|
||||
this.columnFilters,
|
||||
this.searchMode === 'data' ? 'data' : 'table',
|
||||
lucenePred || undefined
|
||||
);
|
||||
(this as any)._lastViewData = viewData;
|
||||
return html`
|
||||
<div class="mainbox">
|
||||
<!-- the heading part -->
|
||||
<div class="header">
|
||||
<div class="headingContainer">
|
||||
<div class="heading heading1">${this.label || this.heading1}</div>
|
||||
<div class="heading heading2">${this.heading2}</div>
|
||||
</div>
|
||||
<div class="headerActions">
|
||||
${directives.resolveExec(async () => {
|
||||
const resultArray: TemplateResult[] = [];
|
||||
for (const action of this.dataActions) {
|
||||
if (!action.type.includes('header')) continue;
|
||||
resultArray.push(
|
||||
html`<div
|
||||
class="headerAction"
|
||||
@click=${() => {
|
||||
action.actionFunc({
|
||||
item: this.selectedDataRow,
|
||||
table: this,
|
||||
});
|
||||
}}
|
||||
>
|
||||
${action.iconName
|
||||
? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
|
||||
${action.name}`
|
||||
: action.name}
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
return resultArray;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div class="headingSeparation"></div>
|
||||
<div class="searchGrid hidden">
|
||||
<dees-input-text
|
||||
.label=${'lucene syntax search'}
|
||||
.description=${`
|
||||
You can use the lucene syntax to search for data, e.g.:
|
||||
|
||||
\`\`\`
|
||||
name: "john" AND age: 18
|
||||
\`\`\`
|
||||
|
||||
`}
|
||||
></dees-input-text>
|
||||
<dees-input-multitoggle
|
||||
.label=${'search mode'}
|
||||
.options=${['table', 'data', 'server']}
|
||||
.selectedOption=${'table'}
|
||||
.description=${`
|
||||
There are three basic modes:
|
||||
|
||||
* table: only searches data already in the table
|
||||
* data: searches original data, ignoring table transforms
|
||||
* server: searches data on the server
|
||||
|
||||
`}
|
||||
></dees-input-multitoggle>
|
||||
</div>
|
||||
|
||||
<!-- the actual table -->
|
||||
<style></style>
|
||||
${this.data.length > 0
|
||||
? html`
|
||||
<div class="tableScroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
${this.selectionMode !== 'none'
|
||||
? html`
|
||||
<th style="width:42px; text-align:center;">
|
||||
${this.selectionMode === 'multi'
|
||||
? html`
|
||||
<dees-input-checkbox
|
||||
.value=${this.areAllVisibleSelected()}
|
||||
.indeterminate=${this.isVisibleSelectionIndeterminate()}
|
||||
@newValue=${(e: CustomEvent<boolean>) => {
|
||||
e.stopPropagation();
|
||||
this.setSelectVisible(e.detail === true);
|
||||
}}
|
||||
></dees-input-checkbox>
|
||||
`
|
||||
: html``}
|
||||
</th>
|
||||
`
|
||||
: html``}
|
||||
${effectiveColumns
|
||||
.filter((c) => !c.hidden)
|
||||
.map((col) => {
|
||||
const isSortable = !!col.sortable;
|
||||
const ariaSort = this.getAriaSort(col);
|
||||
return html`
|
||||
<th
|
||||
role="columnheader"
|
||||
aria-sort=${ariaSort}
|
||||
style="${isSortable ? 'cursor: pointer;' : ''}"
|
||||
@click=${() => (isSortable ? this.toggleSort(col) : null)}
|
||||
>
|
||||
${col.header ?? (col.key as any)}
|
||||
${this.renderSortIndicator(col)}
|
||||
</th>`;
|
||||
})}
|
||||
${(() => {
|
||||
if (this.dataActions && this.dataActions.length > 0) {
|
||||
return html` <th class="actionsCol">Actions</th> `;
|
||||
}
|
||||
})()}
|
||||
</tr>
|
||||
${this.showColumnFilters
|
||||
? html`<tr class="filtersRow">
|
||||
${this.selectionMode !== 'none'
|
||||
? html`<th style="width:42px;"></th>`
|
||||
: html``}
|
||||
${effectiveColumns
|
||||
.filter((c) => !c.hidden)
|
||||
.map((col) => {
|
||||
const key = String(col.key);
|
||||
if (col.filterable === false) return html`<th></th>`;
|
||||
return html`<th>
|
||||
<input type="text" placeholder="Filter..." .value=${this.columnFilters[key] || ''}
|
||||
@input=${(e: Event) => this.setColumnFilter(key, (e.target as HTMLInputElement).value)} />
|
||||
</th>`;
|
||||
})}
|
||||
${(() => {
|
||||
if (this.dataActions && this.dataActions.length > 0) {
|
||||
return html` <th></th> `;
|
||||
}
|
||||
})()}
|
||||
</tr>`
|
||||
: html``}
|
||||
</thead>
|
||||
<tbody>
|
||||
${viewData.map((itemArg, rowIndex) => {
|
||||
const getTr = (elementArg: HTMLElement): HTMLElement => {
|
||||
if (elementArg.tagName === 'TR') {
|
||||
return elementArg;
|
||||
} else {
|
||||
return getTr(elementArg.parentElement);
|
||||
}
|
||||
};
|
||||
return html`
|
||||
<tr
|
||||
@click=${() => {
|
||||
this.selectedDataRow = itemArg;
|
||||
if (this.selectionMode === 'single') {
|
||||
const id = this.getRowId(itemArg);
|
||||
this.selectedIds.clear();
|
||||
this.selectedIds.add(id);
|
||||
this.emitSelectionChange();
|
||||
this.requestUpdate();
|
||||
}
|
||||
}}
|
||||
@dragenter=${async (eventArg: DragEvent) => {
|
||||
eventArg.preventDefault();
|
||||
eventArg.stopPropagation();
|
||||
const realTarget = getTr(eventArg.target as HTMLElement);
|
||||
setTimeout(() => {
|
||||
realTarget.classList.add('hasAttachment');
|
||||
}, 0);
|
||||
}}
|
||||
@dragleave=${async (eventArg: DragEvent) => {
|
||||
eventArg.preventDefault();
|
||||
eventArg.stopPropagation();
|
||||
const realTarget = getTr(eventArg.target as HTMLElement);
|
||||
realTarget.classList.remove('hasAttachment');
|
||||
}}
|
||||
@dragover=${async (eventArg: DragEvent) => {
|
||||
eventArg.preventDefault();
|
||||
}}
|
||||
@drop=${async (eventArg: DragEvent) => {
|
||||
eventArg.preventDefault();
|
||||
const newFiles = [];
|
||||
for (const file of Array.from(eventArg.dataTransfer.files)) {
|
||||
this.files.push(file);
|
||||
newFiles.push(file);
|
||||
this.requestUpdate();
|
||||
}
|
||||
const result: File[] = this.fileWeakMap.get(itemArg as object);
|
||||
if (!result) {
|
||||
this.fileWeakMap.set(itemArg as object, newFiles);
|
||||
} else {
|
||||
result.push(...newFiles);
|
||||
}
|
||||
}}
|
||||
@contextmenu=${async (eventArg: MouseEvent) => {
|
||||
DeesContextmenu.openContextMenuWithOptions(
|
||||
eventArg,
|
||||
this.getActionsForType('contextmenu').map((action) => {
|
||||
const menuItem: plugins.tsclass.website.IMenuItem = {
|
||||
name: action.name,
|
||||
iconName: action.iconName as any,
|
||||
action: async () => {
|
||||
await action.actionFunc({
|
||||
item: itemArg,
|
||||
table: this,
|
||||
});
|
||||
return null;
|
||||
},
|
||||
};
|
||||
return menuItem;
|
||||
})
|
||||
);
|
||||
}}
|
||||
class="${itemArg === this.selectedDataRow ? 'selected' : ''}"
|
||||
>
|
||||
${this.selectionMode !== 'none'
|
||||
? html`<td style="width:42px; text-align:center;">
|
||||
<dees-input-checkbox
|
||||
.value=${this.isRowSelected(itemArg)}
|
||||
@newValue=${(e: CustomEvent<boolean>) => {
|
||||
e.stopPropagation();
|
||||
this.setRowSelected(itemArg, e.detail === true);
|
||||
}}
|
||||
></dees-input-checkbox>
|
||||
</td>`
|
||||
: html``}
|
||||
${effectiveColumns
|
||||
.filter((c) => !c.hidden)
|
||||
.map((col, colIndex) => {
|
||||
const value = getCellValueFn(itemArg, col, this.displayFunction);
|
||||
const content = col.renderer
|
||||
? col.renderer(value, itemArg, { rowIndex, colIndex, column: col })
|
||||
: value;
|
||||
const editKey = String(col.key);
|
||||
return html`
|
||||
<td
|
||||
@dblclick=${(e: Event) => {
|
||||
const dblAction = this.dataActions.find((actionArg) =>
|
||||
actionArg.type.includes('doubleClick')
|
||||
);
|
||||
if (this.editableFields.includes(editKey)) {
|
||||
this.handleCellEditing(e, itemArg, editKey);
|
||||
} else if (dblAction) {
|
||||
dblAction.actionFunc({ item: itemArg, table: this });
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="innerCellContainer">${content}</div>
|
||||
</td>
|
||||
`;
|
||||
})}
|
||||
${(() => {
|
||||
if (this.dataActions && this.dataActions.length > 0) {
|
||||
return html`
|
||||
<td class="actionsCol">
|
||||
<div class="actionsContainer">
|
||||
${this.getActionsForType('inRow').map(
|
||||
(actionArg) => html`
|
||||
<div
|
||||
class="action"
|
||||
@click=${() =>
|
||||
actionArg.actionFunc({
|
||||
item: itemArg,
|
||||
table: this,
|
||||
})}
|
||||
>
|
||||
${actionArg.iconName
|
||||
? html` <dees-icon .icon=${actionArg.iconName}></dees-icon> `
|
||||
: actionArg.name}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
}
|
||||
})()}
|
||||
</tr>`;
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`
|
||||
: html` <div class="noDataSet">No data set!</div> `}
|
||||
<div class="footer">
|
||||
<div class="tableStatistics">
|
||||
${this.data.length} ${this.dataName || 'data rows'} (total) |
|
||||
${this.selectedDataRow ? '# ' + `${this.data.indexOf(this.selectedDataRow) + 1}` : `No`}
|
||||
selected
|
||||
</div>
|
||||
<div class="footerActions">
|
||||
${directives.resolveExec(async () => {
|
||||
const resultArray: TemplateResult[] = [];
|
||||
for (const action of this.dataActions) {
|
||||
if (!action.type.includes('footer')) continue;
|
||||
resultArray.push(
|
||||
html`<div
|
||||
class="footerAction"
|
||||
@click=${() => {
|
||||
action.actionFunc({
|
||||
item: this.selectedDataRow,
|
||||
table: this,
|
||||
});
|
||||
}}
|
||||
>
|
||||
${action.iconName
|
||||
? html`<dees-icon .iconSize=${14} .icon=${action.iconName}></dees-icon>
|
||||
${action.name}`
|
||||
: action.name}
|
||||
</div>`
|
||||
);
|
||||
}
|
||||
return resultArray;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
|
||||
}
|
||||
|
||||
public async updated(changedProperties: Map<string | number | symbol, unknown>): Promise<void> {
|
||||
super.updated(changedProperties);
|
||||
this.determineColumnWidths();
|
||||
if (this.searchable) {
|
||||
const existing = this.dataActions.find((actionArg) => actionArg.type.includes('header') && actionArg.name === 'Search');
|
||||
if (!existing) {
|
||||
this.dataActions.unshift({
|
||||
name: 'Search',
|
||||
iconName: 'magnifyingGlass',
|
||||
type: ['header'],
|
||||
actionFunc: async () => {
|
||||
console.log('open search');
|
||||
const searchGrid = this.shadowRoot.querySelector('.searchGrid');
|
||||
searchGrid.classList.toggle('hidden');
|
||||
}
|
||||
});
|
||||
console.log(this.dataActions);
|
||||
this.requestUpdate();
|
||||
};
|
||||
// wire search inputs
|
||||
this.wireSearchInputs();
|
||||
}
|
||||
}
|
||||
|
||||
private __debounceTimer?: any;
|
||||
private debounceRun(fn: () => void, ms = 200) {
|
||||
if (this.__debounceTimer) clearTimeout(this.__debounceTimer);
|
||||
this.__debounceTimer = setTimeout(fn, ms);
|
||||
}
|
||||
|
||||
private wireSearchInputs() {
|
||||
const searchTextEl: any = this.shadowRoot?.querySelector('.searchGrid dees-input-text');
|
||||
const searchModeEl: any = this.shadowRoot?.querySelector('.searchGrid dees-input-multitoggle');
|
||||
if (searchTextEl && !this.__searchTextSub) {
|
||||
this.__searchTextSub = searchTextEl.changeSubject.subscribe((el: any) => {
|
||||
const val: string = el?.value ?? '';
|
||||
this.debounceRun(() => {
|
||||
if (this.searchMode === 'server') {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('searchRequest', {
|
||||
detail: { query: val, mode: 'server' },
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.setFilterText(val);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
if (searchModeEl && !this.__searchModeSub) {
|
||||
this.__searchModeSub = searchModeEl.changeSubject.subscribe((el: any) => {
|
||||
const mode: string = el?.selectedOption || el?.value || 'table';
|
||||
if (mode === 'table' || mode === 'data' || mode === 'server') {
|
||||
this.searchMode = mode as any;
|
||||
// When switching modes, re-apply current text input
|
||||
const val: string = searchTextEl?.value ?? '';
|
||||
this.debounceRun(() => {
|
||||
if (this.searchMode === 'server') {
|
||||
this.dispatchEvent(new CustomEvent('searchRequest', { detail: { query: val, mode: 'server' }, bubbles: true }));
|
||||
} else {
|
||||
this.setFilterText(val);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async determineColumnWidths() {
|
||||
const domtools = await this.domtoolsPromise;
|
||||
await domtools.convenience.smartdelay.delayFor(0);
|
||||
// Get the table element
|
||||
const table = this.shadowRoot.querySelector('table');
|
||||
if (!table) return;
|
||||
|
||||
// Get the first row's cells to measure the widths
|
||||
const cells = table.rows[0].cells;
|
||||
|
||||
const handleColumnByIndex = async (i: number, waitForRenderArg: boolean = false) => {
|
||||
const done = plugins.smartpromise.defer();
|
||||
const cell = cells[i];
|
||||
|
||||
// Get computed width
|
||||
const width = window.getComputedStyle(cell).width;
|
||||
if (cell.textContent.includes('Actions')) {
|
||||
const neededWidth =
|
||||
this.dataActions.filter((actionArg) => actionArg.type.includes('inRow')).length * 36;
|
||||
cell.style.width = `${Math.max(neededWidth, 68)}px`;
|
||||
} else {
|
||||
cell.style.width = width;
|
||||
}
|
||||
if (waitForRenderArg) {
|
||||
requestAnimationFrame(() => {
|
||||
done.resolve();
|
||||
});
|
||||
await done.promise;
|
||||
}
|
||||
};
|
||||
|
||||
if (cells[cells.length - 1].textContent.includes('Actions')) {
|
||||
await handleColumnByIndex(cells.length - 1, true);
|
||||
}
|
||||
|
||||
for (let i = 0; i < cells.length; i++) {
|
||||
if (cells[i].textContent.includes('Actions')) {
|
||||
continue;
|
||||
}
|
||||
await handleColumnByIndex(i);
|
||||
}
|
||||
table.style.tableLayout = 'fixed';
|
||||
}
|
||||
|
||||
// compute helpers moved to ./data.ts
|
||||
|
||||
private toggleSort(col: Column<T>) {
|
||||
const key = String(col.key);
|
||||
if (this.sortKey !== key) {
|
||||
this.sortKey = key;
|
||||
this.sortDir = 'asc';
|
||||
} else {
|
||||
if (this.sortDir === 'asc') this.sortDir = 'desc';
|
||||
else if (this.sortDir === 'desc') {
|
||||
this.sortDir = null;
|
||||
this.sortKey = undefined;
|
||||
} else this.sortDir = 'asc';
|
||||
}
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('sortChange', {
|
||||
detail: { key: this.sortKey, dir: this.sortDir },
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private getAriaSort(col: Column<T>): 'none' | 'ascending' | 'descending' {
|
||||
if (String(col.key) !== this.sortKey || !this.sortDir) return 'none';
|
||||
return this.sortDir === 'asc' ? 'ascending' : 'descending';
|
||||
}
|
||||
|
||||
private renderSortIndicator(col: Column<T>) {
|
||||
if (String(col.key) !== this.sortKey || !this.sortDir) return html``;
|
||||
return html`<span style="margin-left:6px; opacity:0.7;">${this.sortDir === 'asc' ? '▲' : '▼'}</span>`;
|
||||
}
|
||||
|
||||
// filtering helpers
|
||||
public setFilterText(value: string) {
|
||||
const prev = this.filterText;
|
||||
this.filterText = value ?? '';
|
||||
if (prev !== this.filterText) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('filterChange', {
|
||||
detail: { text: this.filterText, columns: { ...this.columnFilters } },
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public setColumnFilter(key: string, value: string) {
|
||||
this.columnFilters = { ...this.columnFilters, [key]: value };
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('filterChange', {
|
||||
detail: { text: this.filterText, columns: { ...this.columnFilters } },
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
// selection helpers
|
||||
private getRowId(row: T): string {
|
||||
if (this.rowKey) {
|
||||
if (typeof this.rowKey === 'function') return this.rowKey(row);
|
||||
return String((row as any)[this.rowKey]);
|
||||
}
|
||||
const key = row as any as object;
|
||||
if (!this._rowIdMap.has(key)) {
|
||||
this._rowIdMap.set(key, String(++this._rowIdCounter));
|
||||
}
|
||||
return this._rowIdMap.get(key);
|
||||
}
|
||||
|
||||
private isRowSelected(row: T): boolean {
|
||||
return this.selectedIds.has(this.getRowId(row));
|
||||
}
|
||||
|
||||
private toggleRowSelected(row: T) {
|
||||
const id = this.getRowId(row);
|
||||
if (this.selectionMode === 'single') {
|
||||
this.selectedIds.clear();
|
||||
this.selectedIds.add(id);
|
||||
} else if (this.selectionMode === 'multi') {
|
||||
if (this.selectedIds.has(id)) this.selectedIds.delete(id);
|
||||
else this.selectedIds.add(id);
|
||||
}
|
||||
this.emitSelectionChange();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private setRowSelected(row: T, checked: boolean) {
|
||||
const id = this.getRowId(row);
|
||||
if (this.selectionMode === 'single') {
|
||||
this.selectedIds.clear();
|
||||
if (checked) this.selectedIds.add(id);
|
||||
} else if (this.selectionMode === 'multi') {
|
||||
if (checked) this.selectedIds.add(id);
|
||||
else this.selectedIds.delete(id);
|
||||
}
|
||||
this.emitSelectionChange();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private areAllVisibleSelected(): boolean {
|
||||
const view: T[] = (this as any)._lastViewData || [];
|
||||
if (view.length === 0) return false;
|
||||
for (const r of view) {
|
||||
if (!this.selectedIds.has(this.getRowId(r))) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private isVisibleSelectionIndeterminate(): boolean {
|
||||
const view: T[] = (this as any)._lastViewData || [];
|
||||
if (view.length === 0) return false;
|
||||
let count = 0;
|
||||
for (const r of view) {
|
||||
if (this.selectedIds.has(this.getRowId(r))) count++;
|
||||
}
|
||||
return count > 0 && count < view.length;
|
||||
}
|
||||
|
||||
private setSelectVisible(checked: boolean) {
|
||||
const view: T[] = (this as any)._lastViewData || [];
|
||||
if (checked) {
|
||||
for (const r of view) this.selectedIds.add(this.getRowId(r));
|
||||
} else {
|
||||
for (const r of view) this.selectedIds.delete(this.getRowId(r));
|
||||
}
|
||||
this.emitSelectionChange();
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
private emitSelectionChange() {
|
||||
const selectedIds = Array.from(this.selectedIds);
|
||||
const selectedRows = this.data.filter((r) => this.selectedIds.has(this.getRowId(r)));
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('selectionChange', {
|
||||
detail: { selectedIds, selectedRows },
|
||||
bubbles: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getActionsForType(typeArg: ITableAction['type'][0]) {
|
||||
const actions: ITableAction[] = [];
|
||||
for (const action of this.dataActions) {
|
||||
if (!action.type.includes(typeArg)) continue;
|
||||
actions.push(action);
|
||||
}
|
||||
return actions;
|
||||
}
|
||||
|
||||
async handleCellEditing(event: Event, itemArg: T, key: string) {
|
||||
await this.domtoolsPromise;
|
||||
const target = event.target as HTMLElement;
|
||||
const originalColor = target.style.color;
|
||||
target.style.color = 'transparent';
|
||||
const transformedItem = this.displayFunction(itemArg);
|
||||
const initialValue = ((transformedItem as any)[key] ?? (itemArg as any)[key] ?? '') as string;
|
||||
// Create an input element
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.value = initialValue;
|
||||
|
||||
const blurInput = async (blurArg = true, saveArg = false) => {
|
||||
if (blurArg) {
|
||||
input.blur();
|
||||
}
|
||||
if (saveArg) {
|
||||
itemArg[key] = input.value as any; // Convert string to T (you might need better type casting depending on your data structure)
|
||||
this.changeSubject.next(this);
|
||||
}
|
||||
input.remove();
|
||||
target.style.color = originalColor;
|
||||
this.requestUpdate();
|
||||
};
|
||||
|
||||
// When the input loses focus or the Enter key is pressed, update the data
|
||||
input.addEventListener('blur', () => {
|
||||
blurInput(false, false);
|
||||
});
|
||||
input.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
blurInput(true, true); // This will trigger the blur event handler above
|
||||
}
|
||||
});
|
||||
|
||||
// Replace the cell's content with the input
|
||||
target.appendChild(input);
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
1
ts_web/elements/00group-dataview/dees-table/index.ts
Normal file
1
ts_web/elements/00group-dataview/dees-table/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './dees-table.js';
|
||||
170
ts_web/elements/00group-dataview/dees-table/lucene.ts
Normal file
170
ts_web/elements/00group-dataview/dees-table/lucene.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
import type { Column } from './types.js';
|
||||
|
||||
type FilterMode = 'table' | 'data';
|
||||
|
||||
export type RowPredicate<T> = (row: T) => boolean;
|
||||
|
||||
interface Term {
|
||||
field?: string; // if undefined, match across all fields
|
||||
value?: string; // lowercased string
|
||||
negate?: boolean;
|
||||
range?: { lower: string; upper: string; inclusive: boolean };
|
||||
}
|
||||
|
||||
interface Clause {
|
||||
terms: Term[]; // AND across terms
|
||||
}
|
||||
|
||||
interface LuceneQuery {
|
||||
clauses: Clause[]; // OR across clauses
|
||||
}
|
||||
|
||||
function stripQuotes(s: string): string {
|
||||
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
||||
return s.slice(1, -1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function splitByOr(input: string): string[] {
|
||||
return input.split(/\s+OR\s+/i).map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function splitByAnd(input: string): string[] {
|
||||
return input.split(/\s+AND\s+/i).map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
function parseTerm(raw: string): Term | null {
|
||||
if (!raw) return null;
|
||||
let negate = false;
|
||||
// handle NOT prefix or leading '-'
|
||||
const notMatch = raw.match(/^\s*(NOT\s+|-)\s*(.*)$/i);
|
||||
if (notMatch) {
|
||||
negate = true;
|
||||
raw = notMatch[2];
|
||||
}
|
||||
// range: field:[lower TO upper]
|
||||
const rangeMatch = raw.match(/^([^:\s]+)\s*:\s*\[(.*?)\s+TO\s+(.*?)\]$/i);
|
||||
if (rangeMatch) {
|
||||
return {
|
||||
field: rangeMatch[1],
|
||||
negate,
|
||||
range: { lower: stripQuotes(rangeMatch[2]).toLowerCase(), upper: stripQuotes(rangeMatch[3]).toLowerCase(), inclusive: true },
|
||||
};
|
||||
}
|
||||
// field:value (value may be quoted)
|
||||
const m = raw.match(/^([^:\s]+)\s*:\s*("[^"]*"|'[^']*'|[^"'\s]+)$/);
|
||||
if (m) {
|
||||
return { field: m[1], value: stripQuotes(m[2]).toLowerCase(), negate };
|
||||
}
|
||||
// plain term
|
||||
if (raw.length > 0) {
|
||||
return { value: stripQuotes(raw).toLowerCase(), negate };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseLucene(input: string): LuceneQuery | null {
|
||||
if (!input) return null;
|
||||
const clauses = splitByOr(input).map((clauseStr) => {
|
||||
const terms = splitByAnd(clauseStr)
|
||||
.map(parseTerm)
|
||||
.filter((t): t is Term => !!t && !!t.value);
|
||||
return { terms } as Clause;
|
||||
}).filter((c) => c.terms.length > 0);
|
||||
if (clauses.length === 0) return null;
|
||||
return { clauses };
|
||||
}
|
||||
|
||||
export function compileLucenePredicate<T>(
|
||||
input: string,
|
||||
mode: FilterMode,
|
||||
columns: Column<T>[]
|
||||
): RowPredicate<T> | null {
|
||||
const ast = parseLucene(input);
|
||||
if (!ast) return null;
|
||||
const colMap = new Map<string, Column<T>>(
|
||||
columns.map((c) => [String(c.key), c])
|
||||
);
|
||||
const cmp = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0);
|
||||
const coerce = (s: any) => {
|
||||
const str = String(s ?? '').toLowerCase();
|
||||
const num = Number(str);
|
||||
const date = Date.parse(str);
|
||||
if (!Number.isNaN(num) && str.trim() !== '') return { t: 'n' as const, v: num };
|
||||
if (!Number.isNaN(date)) return { t: 'd' as const, v: date };
|
||||
return { t: 's' as const, v: str };
|
||||
};
|
||||
const inRange = (val: any, lower: string, upper: string) => {
|
||||
const a = coerce(val);
|
||||
const lo = coerce(lower);
|
||||
const up = coerce(upper);
|
||||
// All strings: lexical compare
|
||||
if (a.t === 's' && lo.t === 's' && up.t === 's') {
|
||||
const av = a.v as string;
|
||||
return cmp(av, lo.v as string) >= 0 && cmp(av, up.v as string) <= 0;
|
||||
}
|
||||
// All numbers
|
||||
if (a.t === 'n' && lo.t === 'n' && up.t === 'n') {
|
||||
const av = a.v as number;
|
||||
return av >= (lo.v as number) && av <= (up.v as number);
|
||||
}
|
||||
// All dates (as numbers)
|
||||
if (a.t === 'd' && lo.t === 'd' && up.t === 'd') {
|
||||
const av = a.v as number;
|
||||
return av >= (lo.v as number) && av <= (up.v as number);
|
||||
}
|
||||
// Fallback: compare string forms
|
||||
const as = String(val ?? '').toLowerCase();
|
||||
return cmp(as, lower) >= 0 && cmp(as, upper) <= 0;
|
||||
};
|
||||
return (row: T) => {
|
||||
for (const clause of ast.clauses) {
|
||||
let clauseOk = true;
|
||||
for (const term of clause.terms) {
|
||||
let ok = false;
|
||||
if (term.range && term.field) {
|
||||
// range compare on field
|
||||
if (mode === 'data') {
|
||||
ok = inRange((row as any)[term.field], term.range.lower, term.range.upper);
|
||||
} else {
|
||||
const col = colMap.get(term.field);
|
||||
if (!col || col.hidden) { ok = false; } else {
|
||||
const val = col.value ? col.value(row) : (row as any)[col.key as any];
|
||||
ok = inRange(val, term.range.lower, term.range.upper);
|
||||
}
|
||||
}
|
||||
} else if (term.field && term.value != null) {
|
||||
if (mode === 'data') {
|
||||
const s = String((row as any)[term.field] ?? '').toLowerCase();
|
||||
ok = s.includes(term.value);
|
||||
} else {
|
||||
const col = colMap.get(term.field);
|
||||
if (!col || col.hidden === true) { ok = false; }
|
||||
else {
|
||||
const val = col.value ? col.value(row) : (row as any)[col.key as any];
|
||||
const s = String(val ?? '').toLowerCase();
|
||||
ok = s.includes(term.value);
|
||||
}
|
||||
}
|
||||
} else if (term.value != null) {
|
||||
// search across all visible/raw fields
|
||||
if (mode === 'data') {
|
||||
ok = Object.values(row as any).some((v) => String(v ?? '').toLowerCase().includes(term.value!));
|
||||
} else {
|
||||
ok = columns.some((col) => {
|
||||
if (col.hidden) return false;
|
||||
const val = col.value ? col.value(row) : (row as any)[col.key as any];
|
||||
const s = String(val ?? '').toLowerCase();
|
||||
return s.includes(term.value!);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (term.negate) ok = !ok;
|
||||
if (!ok) { clauseOk = false; break; }
|
||||
}
|
||||
if (clauseOk) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
432
ts_web/elements/00group-dataview/dees-table/styles.ts
Normal file
432
ts_web/elements/00group-dataview/dees-table/styles.ts
Normal file
@@ -0,0 +1,432 @@
|
||||
import { cssManager, css, type CSSResult } from '@design.estate/dees-element';
|
||||
import { cssGeistFontFamily } from '../../00fonts.js';
|
||||
import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
export const tableStyles: CSSResult[] = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mainbox {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
font-family: ${cssGeistFontFamily};
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
min-height: 64px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
.headingContainer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.heading {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.heading1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.heading2 {
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.headingSeparation {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.headerActions {
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.headerAction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
|
||||
background: transparent;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.headerAction:hover {
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
}
|
||||
|
||||
.headerAction dees-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.searchGrid {
|
||||
display: grid;
|
||||
grid-gap: 16px;
|
||||
grid-template-columns: 1fr max-content;
|
||||
padding: 16px 24px;
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 98%)', 'hsl(0 0% 3.9%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.searchGrid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* let search mode size to content (no forced width) */
|
||||
|
||||
.searchGrid.hidden {
|
||||
height: 0px;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
padding: 0px 24px;
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
|
||||
.tableScroll {
|
||||
/* enable horizontal scroll only when content exceeds width */
|
||||
overflow-x: auto;
|
||||
/* prevent vertical scroll inside the table container */
|
||||
overflow-y: hidden;
|
||||
/* avoid reserving extra space for classic scrollbars where possible */
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
/* Hide horizontal scrollbar entirely when not using sticky header */
|
||||
:host(:not([sticky-header])) .tableScroll {
|
||||
-ms-overflow-style: none; /* IE/Edge */
|
||||
scrollbar-width: none; /* Firefox (hides both axes) */
|
||||
}
|
||||
:host(:not([sticky-header])) .tableScroll::-webkit-scrollbar {
|
||||
display: none; /* Chrome/Safari */
|
||||
}
|
||||
/* In sticky-header mode, hide only the horizontal scrollbar in WebKit/Blink */
|
||||
:host([sticky-header]) .tableScroll::-webkit-scrollbar:horizontal {
|
||||
height: 0px;
|
||||
}
|
||||
:host([sticky-header]) .tableScroll {
|
||||
max-height: var(--table-max-height, 360px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
/* allow table to grow wider than container so actions column can stick */
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
caption-side: bottom;
|
||||
font-size: 14px;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.noDataSet {
|
||||
padding: 48px 24px;
|
||||
text-align: center;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
|
||||
}
|
||||
|
||||
thead {
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
|
||||
}
|
||||
:host([sticky-header]) thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
transition: background-color 0.15s ease;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Default horizontal lines (bottom border only) */
|
||||
tbody tr {
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
tbody tr:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Full horizontal lines when enabled */
|
||||
:host([show-horizontal-lines]) tbody tr {
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
:host([show-horizontal-lines]) tbody tr:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
:host([show-horizontal-lines]) tbody tr:last-child {
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1% / 0.5)', 'hsl(0 0% 14.9% / 0.5)')};
|
||||
}
|
||||
|
||||
/* Column hover effect for better traceability */
|
||||
td {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
td::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1% / 0.3)', 'hsl(0 0% 14.9% / 0.3)')};
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
td:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Grid mode - shows both vertical and horizontal lines */
|
||||
:host([show-grid]) th {
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
:host([show-grid]) td {
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
:host([show-grid]) th:first-child,
|
||||
:host([show-grid]) td:first-child {
|
||||
border-left: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
:host([show-grid]) tbody tr:first-child td {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* Sticky Actions column (right pinned) */
|
||||
thead th.actionsCol,
|
||||
tbody td.actionsCol {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
|
||||
}
|
||||
thead th.actionsCol { z-index: 3; }
|
||||
tbody td.actionsCol {
|
||||
z-index: 1;
|
||||
box-shadow: -1px 0 0 0 ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
tbody tr.selected {
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
tbody tr.hasAttachment {
|
||||
background: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 76.2% 36.3% / 0.1)')};
|
||||
}
|
||||
|
||||
th {
|
||||
height: 48px;
|
||||
padding: 12px 24px;
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
:host([show-vertical-lines]) th {
|
||||
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 12px 24px;
|
||||
vertical-align: middle;
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
}
|
||||
|
||||
:host([show-vertical-lines]) td {
|
||||
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 24px;
|
||||
}
|
||||
|
||||
:host([show-vertical-lines]) th:last-child,
|
||||
:host([show-vertical-lines]) td:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.innerCellContainer {
|
||||
position: relative;
|
||||
min-height: 24px;
|
||||
line-height: 24px;
|
||||
}
|
||||
td input {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
width: calc(100% - 40px);
|
||||
height: calc(100% - 8px);
|
||||
padding: 0 12px;
|
||||
outline: none;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
border-radius: 6px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
font-weight: inherit;
|
||||
transition: all 0.15s ease;
|
||||
box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||
}
|
||||
|
||||
td input:focus {
|
||||
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.2)', 'hsl(217.2 91.2% 59.8% / 0.2)')};
|
||||
}
|
||||
|
||||
/* filter row */
|
||||
thead tr.filtersRow th {
|
||||
padding: 8px 12px 12px 12px;
|
||||
}
|
||||
thead tr.filtersRow th input[type='text'] {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 6px 8px;
|
||||
font-size: 13px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
|
||||
}
|
||||
.actionsContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.action:hover {
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
}
|
||||
|
||||
.action:active {
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 11.8%)')};
|
||||
}
|
||||
|
||||
.action dees-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 52px;
|
||||
padding: 0 24px;
|
||||
font-size: 14px;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
|
||||
}
|
||||
|
||||
.tableStatistics {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.footerActions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footerActions .footerAction {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
font-weight: 500;
|
||||
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.footerActions .footerAction:hover {
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
|
||||
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
|
||||
}
|
||||
|
||||
.footerActions .footerAction dees-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
29
ts_web/elements/00group-dataview/dees-table/types.ts
Normal file
29
ts_web/elements/00group-dataview/dees-table/types.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { TemplateResult } from '@design.estate/dees-element';
|
||||
import type { TIconKey } from '../../00group-utility/dees-icon/dees-icon.js';
|
||||
|
||||
export interface ITableActionDataArg<T> {
|
||||
item: T;
|
||||
table: any; // avoid circular typing with DeesTable; consumers rely on shape only
|
||||
}
|
||||
|
||||
export interface ITableAction<T = any> {
|
||||
name: string;
|
||||
iconName: TIconKey;
|
||||
useTableBehaviour?: 'upload' | 'cancelUpload' | 'none';
|
||||
type: ('inRow' | 'contextmenu' | 'doubleClick' | 'footer' | 'header' | 'preview' | 'keyCombination')[];
|
||||
actionRelevancyCheckFunc?: (itemArg: T) => boolean;
|
||||
actionFunc: (actionDataArg: ITableActionDataArg<T>) => Promise<any>;
|
||||
}
|
||||
|
||||
export interface Column<T = any> {
|
||||
key: keyof T | string;
|
||||
header?: string | TemplateResult;
|
||||
value?: (row: T) => any;
|
||||
renderer?: (value: any, row: T, ctx: { rowIndex: number; colIndex: number; column: Column<T> }) => TemplateResult | string;
|
||||
sortable?: boolean;
|
||||
/** whether this column participates in per-column quick filtering (default: true) */
|
||||
filterable?: boolean;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export type TDisplayFunction<T = any> = (itemArg: T) => Record<string, any>;
|
||||
@@ -1,3 +1,5 @@
|
||||
// Data View Components
|
||||
export * from './dees-dataview-codebox/index.js';
|
||||
export * from './dees-dataview-statusobject/index.js';
|
||||
export * from './dees-table/index.js';
|
||||
export * from './dees-statsgrid/index.js';
|
||||
|
||||
Reference in New Issue
Block a user