Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c841c49e1e | |||
| 2595d822d0 | |||
| 3ae0541065 | |||
| 4b735b768a | |||
| 9422edbfa1 | |||
| 37c5e92d6d | |||
| c7503de11e | |||
| 408362f3be | |||
| b3f5ab3d31 | |||
| 8d954b17ad | |||
| ac9cc8cfed | |||
| a1e808345e | |||
| efea2d62d9 | |||
| 2f95979cc6 | |||
| b3f098b41e | |||
| a0d5462ff1 | |||
| f1c204f790 |
54
changelog.md
54
changelog.md
@@ -1,5 +1,59 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-04-08 - 3.69.0 - feat(dees-heading)
|
||||
add numeric aliases for horizontal rule heading levels and refine heading spacing styles
|
||||
|
||||
- Support level="7" as an alias for "hr" and level="8" as an alias for "hr-small".
|
||||
- Update heading and hr variant styles to use design tokens for spacing and colors, with per-level margin tuning.
|
||||
- Extend the demo to show both named and numeric hr heading level variants.
|
||||
|
||||
## 2026-04-08 - 3.68.0 - feat(dees-simple-appdash)
|
||||
add nested sidebar subviews and preserve submit labels from slotted text
|
||||
|
||||
- support grouped navigation items with expandable subviews and parent-to-first-subview fallback in the app dashboard
|
||||
- allow dees-form-submit to derive its button text from light DOM content when no explicit text property is set
|
||||
|
||||
## 2026-04-07 - 3.67.1 - fix(repo)
|
||||
no changes to commit
|
||||
|
||||
|
||||
## 2026-04-07 - 3.67.0 - feat(dees-table)
|
||||
improve inline cell editors with integrated input styling and auto-open dropdowns
|
||||
|
||||
- add a visually integrated mode to dees-input-text and dees-input-dropdown for table cell editing
|
||||
- auto-open dropdown editors when a table cell enters edit mode
|
||||
- refine table editing cell outline and dropdown value matching for inline editors
|
||||
|
||||
## 2026-04-07 - 3.66.0 - feat(dees-table)
|
||||
add virtualized row rendering for large tables and optimize table rendering performance
|
||||
|
||||
- add a virtualized mode with configurable overscan to render only visible rows while preserving scroll height
|
||||
- improve table render performance with memoized column and view-data computation plus deferred floating header rendering
|
||||
- update the dees-table demo to showcase virtualized scrolling in the fixed-height example
|
||||
|
||||
## 2026-04-07 - 3.65.0 - feat(dees-table)
|
||||
add schema-based in-cell editing with keyboard navigation and cell edit events
|
||||
|
||||
- replace editableFields with per-column editor configuration for text, number, checkbox, dropdown, date, and tags inputs
|
||||
- add focused/editing cell state with arrow key navigation plus Enter, Tab, Shift+Tab, F2, and Escape editing controls
|
||||
- dispatch cellEdit and cellEditError events with typed payloads and support column-level format, parse, validate, and editorOptions hooks
|
||||
- update table styles and demos to reflect editable cell behavior and rename sticky header usage to fixedHeight
|
||||
|
||||
## 2026-04-07 - 3.64.0 - feat(dees-table)
|
||||
add file-manager style row selection and JSON copy support
|
||||
|
||||
- adds optional selection checkbox rendering via the show-selection-checkbox property
|
||||
- supports plain, ctrl/cmd, and shift-click row selection with range selection behavior
|
||||
- adds Ctrl/Cmd+C and context menu actions to copy selected rows as formatted JSON
|
||||
- updates row selection styling to prevent native text selection during range selection
|
||||
|
||||
## 2026-04-07 - 3.63.0 - feat(dees-table)
|
||||
add floating header support with fixed-height table mode
|
||||
|
||||
- replace the sticky-header option with a fixed-height mode for internal scrolling
|
||||
- add a JS-managed floating header so column headers remain visible when tables scroll inside ancestor containers
|
||||
- sync floating header column widths and filter rows with the rendered table
|
||||
|
||||
## 2026-04-07 - 3.62.0 - feat(dees-table)
|
||||
add multi-column sorting with header menu controls and priority indicators
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@design.estate/dees-catalog",
|
||||
"version": "3.62.0",
|
||||
"version": "3.69.0",
|
||||
"private": false,
|
||||
"description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
|
||||
"main": "dist_ts_web/index.js",
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@design.estate/dees-catalog',
|
||||
version: '3.62.0',
|
||||
version: '3.69.0',
|
||||
description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
|
||||
}
|
||||
|
||||
@@ -55,36 +55,66 @@ export const demoFunc = () => html`
|
||||
<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>
|
||||
<p class="demo-description">A standard table with row actions, editable cells, and context menu support. Double-click any cell to edit. Tab moves to the next editable cell, Enter to the row below, Esc cancels.</p>
|
||||
<dees-table
|
||||
heading1="Current Account Statement"
|
||||
heading2="Bunq - Payment Account 2 - April 2021"
|
||||
.editableFields="${['description']}"
|
||||
.columns=${[
|
||||
{ key: 'date', header: 'Date', sortable: true, editable: true, editor: 'date' },
|
||||
{ key: 'amount', header: 'Amount', editable: true, editor: 'text' },
|
||||
{
|
||||
key: 'category',
|
||||
header: 'Category',
|
||||
editable: true,
|
||||
editor: 'dropdown',
|
||||
editorOptions: {
|
||||
options: [
|
||||
{ option: 'Office Supplies', key: 'office' },
|
||||
{ option: 'Hardware', key: 'hardware' },
|
||||
{ option: 'Software', key: 'software' },
|
||||
{ option: 'Travel', key: 'travel' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{ key: 'description', header: 'Description', editable: true },
|
||||
{ key: 'reconciled', header: 'OK', editable: true, editor: 'checkbox' },
|
||||
]}
|
||||
@cellEdit=${(e: CustomEvent) => console.log('cellEdit', e.detail)}
|
||||
.data=${[
|
||||
{
|
||||
date: '2021-04-01',
|
||||
amount: '2464.65 €',
|
||||
description: 'Printing Paper (Office Supplies) - STAPLES BREMEN',
|
||||
category: 'office',
|
||||
description: 'Printing Paper - STAPLES BREMEN',
|
||||
reconciled: true,
|
||||
},
|
||||
{
|
||||
date: '2021-04-02',
|
||||
amount: '165.65 €',
|
||||
description: 'Logitech Mouse (Hardware) - logi.com OnlineShop',
|
||||
category: 'hardware',
|
||||
description: 'Logitech Mouse - logi.com OnlineShop',
|
||||
reconciled: false,
|
||||
},
|
||||
{
|
||||
date: '2021-04-03',
|
||||
amount: '2999,00 €',
|
||||
description: 'Macbook Pro 16inch (Hardware) - Apple.de OnlineShop',
|
||||
category: 'hardware',
|
||||
description: 'Macbook Pro 16inch - Apple.de OnlineShop',
|
||||
reconciled: false,
|
||||
},
|
||||
{
|
||||
date: '2021-04-01',
|
||||
amount: '2464.65 €',
|
||||
category: 'office',
|
||||
description: 'Office-Supplies - STAPLES BREMEN',
|
||||
reconciled: true,
|
||||
},
|
||||
{
|
||||
date: '2021-04-01',
|
||||
amount: '2464.65 €',
|
||||
category: 'office',
|
||||
description: 'Office-Supplies - STAPLES BREMEN',
|
||||
reconciled: true,
|
||||
},
|
||||
]}
|
||||
dataName="transactions"
|
||||
@@ -510,13 +540,13 @@ export const demoFunc = () => html`
|
||||
<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; }
|
||||
dees-table[fixed-height] { --table-max-height: 220px; }
|
||||
</style>
|
||||
<dees-table
|
||||
heading1="Employees"
|
||||
heading2="Quick filter per column + sticky header"
|
||||
.showColumnFilters=${true}
|
||||
.stickyHeader=${true}
|
||||
.fixedHeight=${true}
|
||||
.columns=${[
|
||||
{ key: 'name', header: 'Name', sortable: true },
|
||||
{ key: 'email', header: 'Email', sortable: true },
|
||||
@@ -669,8 +699,9 @@ export const demoFunc = () => html`
|
||||
</style>
|
||||
<dees-table
|
||||
id="scrollSmallHeight"
|
||||
.stickyHeader=${true}
|
||||
heading1="People Directory (Scrollable)"
|
||||
.fixedHeight=${true}
|
||||
.virtualized=${true}
|
||||
heading1="People Directory (Scrollable, Virtualized)"
|
||||
heading2="Forced scrolling with many items"
|
||||
.columns=${[
|
||||
{ key: 'id', header: 'ID', sortable: true },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -114,29 +114,48 @@ export const tableStyles: CSSResult[] = [
|
||||
border-bottom-width: 0px;
|
||||
}
|
||||
|
||||
/* Default mode (Mode B, page sticky): horizontal scroll lives on
|
||||
.tableScroll (so wide tables don't get clipped by an ancestor
|
||||
overflow:hidden such as dees-tile). Vertical sticky is handled by
|
||||
a JS-managed floating header (.floatingHeader, position:fixed),
|
||||
which is unaffected by ancestor overflow. */
|
||||
.tableScroll {
|
||||
/* enable horizontal scroll only when content exceeds width */
|
||||
position: relative;
|
||||
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;
|
||||
overflow-y: visible;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
/* 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 {
|
||||
/* Mode A, internal scroll: opt-in via fixed-height attribute.
|
||||
The table scrolls inside its own box and the header sticks via plain CSS sticky. */
|
||||
:host([fixed-height]) .tableScroll {
|
||||
max-height: var(--table-max-height, 360px);
|
||||
overflow: auto;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
}
|
||||
:host([fixed-height]) .tableScroll::-webkit-scrollbar:horizontal {
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
/* Floating header overlay (Mode B). Position is managed by JS so it
|
||||
escapes any ancestor overflow:hidden (position:fixed is not clipped
|
||||
by overflow ancestors). */
|
||||
.floatingHeader {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 100;
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
pointer-events: none;
|
||||
}
|
||||
.floatingHeader.active {
|
||||
visibility: visible;
|
||||
}
|
||||
.floatingHeader table {
|
||||
margin: 0;
|
||||
}
|
||||
.floatingHeader th {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
@@ -159,15 +178,25 @@ export const tableStyles: CSSResult[] = [
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')};
|
||||
border-bottom: 1px solid var(--dees-color-border-strong);
|
||||
}
|
||||
:host([sticky-header]) thead th {
|
||||
/* th needs its own background so sticky cells paint over scrolled rows
|
||||
(browsers don't paint the <thead> box behind a sticky <th>). */
|
||||
th {
|
||||
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 9%)')};
|
||||
}
|
||||
/* Mode A — internal scroll sticky */
|
||||
:host([fixed-height]) thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
:host([fixed-height]) thead tr.filtersRow th {
|
||||
top: 36px; /* matches th { height: 36px } below */
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
transition: background-color 0.15s ease;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Default horizontal lines (bottom border only) */
|
||||
@@ -343,32 +372,37 @@ export const tableStyles: CSSResult[] = [
|
||||
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 var(--dees-color-border-default);
|
||||
border-radius: 6px;
|
||||
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
|
||||
color: var(--dees-color-text-primary);
|
||||
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);
|
||||
|
||||
/* Editable cell affordances */
|
||||
td.editable {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
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)')};
|
||||
td.focused {
|
||||
outline: 2px solid ${cssManager.bdTheme(
|
||||
'hsl(222.2 47.4% 51.2% / 0.6)',
|
||||
'hsl(217.2 91.2% 59.8% / 0.6)'
|
||||
)};
|
||||
outline-offset: -2px;
|
||||
}
|
||||
td.editingCell {
|
||||
padding: 0;
|
||||
outline: 2px solid ${cssManager.bdTheme(
|
||||
'hsl(222.2 47.4% 51.2% / 0.6)',
|
||||
'hsl(217.2 91.2% 59.8% / 0.6)'
|
||||
)};
|
||||
outline-offset: -2px;
|
||||
}
|
||||
td.editingCell .innerCellContainer {
|
||||
padding: 0;
|
||||
line-height: normal;
|
||||
}
|
||||
td.editingCell dees-input-text,
|
||||
td.editingCell dees-input-checkbox,
|
||||
td.editingCell dees-input-dropdown,
|
||||
td.editingCell dees-input-datepicker,
|
||||
td.editingCell dees-input-tags {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* filter row */
|
||||
|
||||
@@ -15,15 +15,56 @@ export interface ITableAction<T = any> {
|
||||
actionFunc: (actionDataArg: ITableActionDataArg<T>) => Promise<any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Available cell editor types. Each maps to a dees-input-* component.
|
||||
* Use `editor` on `Column<T>` to opt a column into in-cell editing.
|
||||
*/
|
||||
export type TCellEditorType =
|
||||
| 'text'
|
||||
| 'number'
|
||||
| 'checkbox'
|
||||
| 'dropdown'
|
||||
| 'date'
|
||||
| 'tags';
|
||||
|
||||
/** Detail payload for the `cellEdit` CustomEvent dispatched on commit. */
|
||||
export interface ICellEditDetail<T = any> {
|
||||
row: T;
|
||||
key: string;
|
||||
oldValue: any;
|
||||
newValue: any;
|
||||
}
|
||||
|
||||
/** Detail payload for the `cellEditError` CustomEvent dispatched on validation failure. */
|
||||
export interface ICellEditErrorDetail<T = any> {
|
||||
row: T;
|
||||
key: string;
|
||||
value: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
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;
|
||||
/** Whether this column can be sorted by clicking its header. Defaults to `true`; set to `false` to disable. */
|
||||
sortable?: boolean;
|
||||
/** whether this column participates in per-column quick filtering (default: true) */
|
||||
filterable?: boolean;
|
||||
hidden?: boolean;
|
||||
/** Marks the column as editable. Shorthand for `editor: 'text'` if no editor is specified. */
|
||||
editable?: boolean;
|
||||
/** Editor type — picks the dees-input-* component used for in-cell editing. */
|
||||
editor?: TCellEditorType;
|
||||
/** Editor-specific options forwarded to the editor (e.g. `{ options: [...] }` for dropdowns). */
|
||||
editorOptions?: Record<string, any>;
|
||||
/** Convert raw row value -> editor value. Defaults to identity. */
|
||||
format?: (raw: any, row: T) => any;
|
||||
/** Convert editor value -> raw row value. Defaults to identity. */
|
||||
parse?: (editorValue: any, row: T) => any;
|
||||
/** Validate the parsed value before commit. Return string for error, true/void for ok. */
|
||||
validate?: (value: any, row: T) => true | string | void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -75,12 +75,23 @@ export class DeesFormSubmit extends DeesElement {
|
||||
.text=${this.text}
|
||||
?disabled=${this.disabled}
|
||||
@clicked=${this.submit}
|
||||
>
|
||||
<slot></slot>
|
||||
</dees-button>
|
||||
></dees-button>
|
||||
`;
|
||||
}
|
||||
|
||||
public async firstUpdated() {
|
||||
// Capture light DOM text content as the button label. dees-button wipes
|
||||
// its own light DOM during extractLightDom(), so we cannot simply forward
|
||||
// a <slot> into it — we have to hoist the text onto the .text property
|
||||
// ourselves before handing it to dees-button.
|
||||
if (!this.text) {
|
||||
const slotText = this.textContent?.trim();
|
||||
if (slotText) {
|
||||
this.text = slotText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async submit() {
|
||||
if (this.disabled) {
|
||||
return;
|
||||
|
||||
@@ -46,6 +46,12 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
})
|
||||
accessor enableSearch: boolean = true;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
})
|
||||
accessor vintegrated: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor isOpened = false;
|
||||
|
||||
@@ -126,6 +132,36 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
|
||||
.selectedBox.open::after {
|
||||
transform: translateY(-50%) rotate(180deg);
|
||||
}
|
||||
|
||||
/* Visually integrated mode: shed chrome to blend into a host component
|
||||
(e.g. a dees-table cell in edit mode). */
|
||||
:host([vintegrated]) dees-label {
|
||||
display: none;
|
||||
}
|
||||
:host([vintegrated]) .maincontainer {
|
||||
height: 40px;
|
||||
}
|
||||
:host([vintegrated]) .selectedBox {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
padding: 0 32px 0 16px;
|
||||
font-size: 13px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
transition: none;
|
||||
}
|
||||
:host([vintegrated]) .selectedBox:hover:not(.disabled),
|
||||
:host([vintegrated]) .selectedBox:focus-visible {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
:host([vintegrated]) .selectedBox::after {
|
||||
right: 12px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
|
||||
@@ -57,6 +57,12 @@ export class DeesInputText extends DeesInputBase {
|
||||
@property({})
|
||||
accessor validationFunction!: (value: string) => boolean;
|
||||
|
||||
@property({
|
||||
type: Boolean,
|
||||
reflect: true,
|
||||
})
|
||||
accessor vintegrated: boolean = false;
|
||||
|
||||
public static styles = [
|
||||
themeDefaultStyles,
|
||||
...DeesInputBase.baseStyles,
|
||||
@@ -194,6 +200,36 @@ export class DeesInputText extends DeesInputBase {
|
||||
border-color: ${cssManager.bdTheme('hsl(142.1 76.2% 36.3%)', 'hsl(142.1 70.6% 45.3%)')};
|
||||
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.05)', 'hsl(142.1 70.6% 45.3% / 0.05)')};
|
||||
}
|
||||
|
||||
/* Visually integrated mode: shed chrome to blend into a host component
|
||||
(e.g. a dees-table cell in edit mode). */
|
||||
:host([vintegrated]) dees-label,
|
||||
:host([vintegrated]) .validationContainer {
|
||||
display: none;
|
||||
}
|
||||
:host([vintegrated]) .maincontainer {
|
||||
height: 40px;
|
||||
}
|
||||
:host([vintegrated]) input {
|
||||
height: 40px;
|
||||
line-height: 24px;
|
||||
padding: 0 16px;
|
||||
font-size: 13px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
transition: none;
|
||||
}
|
||||
:host([vintegrated]) input:hover:not(:disabled):not(:focus),
|
||||
:host([vintegrated]) input:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
:host([vintegrated]) .showPassword {
|
||||
display: none;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
|
||||
@@ -8,7 +8,9 @@ export function demoFunc() {
|
||||
<dees-heading level="4">This is a H4 heading</dees-heading>
|
||||
<dees-heading level="5">This is a H5 heading</dees-heading>
|
||||
<dees-heading level="6">This is a H6 heading</dees-heading>
|
||||
<dees-heading level="hr">This is an hr heading</dees-heading>
|
||||
<dees-heading level="hr-small">This is an hr small heading</dees-heading>
|
||||
<dees-heading level="hr">This is an hr heading (level="hr")</dees-heading>
|
||||
<dees-heading level="7">This is an hr heading (level="7")</dees-heading>
|
||||
<dees-heading level="hr-small">This is an hr-small heading (level="hr-small")</dees-heading>
|
||||
<dees-heading level="8">This is an hr-small heading (level="8")</dees-heading>
|
||||
`;
|
||||
}
|
||||
@@ -27,68 +27,91 @@ export class DeesHeading extends DeesElement {
|
||||
|
||||
// properties
|
||||
/**
|
||||
* Heading level: 1-6 for h1-h6, or 'hr' for horizontal rule style
|
||||
* Heading level:
|
||||
* '1'-'6' → <h1>..<h6>
|
||||
* '7'|'hr' → horizontal-rule style heading
|
||||
* '8'|'hr-small' → small horizontal-rule style heading
|
||||
*/
|
||||
@property({ type: String, reflect: true })
|
||||
accessor level: '1' | '2' | '3' | '4' | '5' | '6' | 'hr' | 'hr-small' = '1';
|
||||
accessor level: '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | 'hr' | 'hr-small' = '1';
|
||||
|
||||
// STATIC STYLES
|
||||
public static styles: CSSResult[] = [
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Heading styles */
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin: 16px 0 8px;
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#000', '#fff')};
|
||||
color: var(--dees-color-text-primary);
|
||||
}
|
||||
h1 { font-size: 32px; font-family: ${cssCalSansFontFamily}; letter-spacing: 0.025em;}
|
||||
h2 { font-size: 28px; }
|
||||
h3 { font-size: 24px; }
|
||||
h4 { font-size: 20px; }
|
||||
h5 { font-size: 16px; }
|
||||
h6 { font-size: 14px; }
|
||||
|
||||
/* Per-level typography + spacing.
|
||||
* Margin scales with importance: h1 gets the most breathing room,
|
||||
* h6 the least. Top margin > bottom margin so headings group with
|
||||
* the content that follows them. */
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-family: ${cssCalSansFontFamily};
|
||||
letter-spacing: 0.025em;
|
||||
margin: var(--dees-spacing-2xl) 0 var(--dees-spacing-lg);
|
||||
}
|
||||
h2 {
|
||||
font-size: 28px;
|
||||
margin: var(--dees-spacing-xl) 0 var(--dees-spacing-md);
|
||||
}
|
||||
h3 {
|
||||
font-size: 24px;
|
||||
margin: var(--dees-spacing-xl) 0 var(--dees-spacing-md);
|
||||
}
|
||||
h4 {
|
||||
font-size: 20px;
|
||||
margin: var(--dees-spacing-lg) 0 var(--dees-spacing-sm);
|
||||
}
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
margin: var(--dees-spacing-md) 0 var(--dees-spacing-sm);
|
||||
}
|
||||
h6 {
|
||||
font-size: 14px;
|
||||
margin: var(--dees-spacing-md) 0 var(--dees-spacing-xs);
|
||||
}
|
||||
|
||||
/* Horizontal rule style heading */
|
||||
.heading-hr {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 16px 0;
|
||||
color: ${cssManager.bdTheme('#999', '#555')};
|
||||
margin: var(--dees-spacing-lg) 0;
|
||||
color: var(--dees-color-text-muted);
|
||||
}
|
||||
/* Fade lines toward and away from text for hr style */
|
||||
.heading-hr::before {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
/* fade in toward center */
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(to right, transparent, #ccc)',
|
||||
'linear-gradient(to right, transparent, #333)'
|
||||
)};
|
||||
margin: 0 8px;
|
||||
background: linear-gradient(to right, transparent, var(--dees-color-border-strong));
|
||||
margin: 0 var(--dees-spacing-sm);
|
||||
}
|
||||
.heading-hr::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
/* fade out away from center */
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(to right, #ccc, transparent)',
|
||||
'linear-gradient(to right, #333, transparent)'
|
||||
)};
|
||||
margin: 0 8px;
|
||||
background: linear-gradient(to right, var(--dees-color-border-strong), transparent);
|
||||
margin: 0 var(--dees-spacing-sm);
|
||||
}
|
||||
/* Small hr variant with reduced margins */
|
||||
.heading-hr.heading-hr-small {
|
||||
margin: 8px 0;
|
||||
margin: var(--dees-spacing-sm) 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.heading-hr.heading-hr-small::before,
|
||||
.heading-hr.heading-hr-small::after {
|
||||
margin: 0 8px;
|
||||
margin: 0 var(--dees-spacing-sm);
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -109,8 +132,10 @@ export class DeesHeading extends DeesElement {
|
||||
return html`<h5><slot></slot></h5>`;
|
||||
case '6':
|
||||
return html`<h6><slot></slot></h6>`;
|
||||
case '7':
|
||||
case 'hr':
|
||||
return html`<div class="heading-hr"><slot></slot></div>`;
|
||||
case '8':
|
||||
case 'hr-small':
|
||||
return html`<div class="heading-hr heading-hr-small"><slot></slot></div>`;
|
||||
default:
|
||||
|
||||
@@ -353,12 +353,35 @@ export const demoFunc = () => html`
|
||||
name: 'Analytics',
|
||||
iconName: 'lucide:lineChart',
|
||||
element: DemoViewAnalytics,
|
||||
subViews: [
|
||||
{
|
||||
name: 'Overview',
|
||||
iconName: 'lucide:activity',
|
||||
element: DemoViewAnalytics,
|
||||
},
|
||||
{
|
||||
name: 'Reports',
|
||||
iconName: 'lucide:fileText',
|
||||
element: DemoViewDashboard,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Settings',
|
||||
iconName: 'lucide:settings',
|
||||
element: DemoViewSettings,
|
||||
}
|
||||
subViews: [
|
||||
{
|
||||
name: 'Profile',
|
||||
iconName: 'lucide:user',
|
||||
element: DemoViewSettings,
|
||||
},
|
||||
{
|
||||
name: 'Billing',
|
||||
iconName: 'lucide:creditCard',
|
||||
element: DemoViewSettings,
|
||||
},
|
||||
],
|
||||
},
|
||||
] as IView[]}
|
||||
@logout=${() => {
|
||||
console.log('Logout event triggered');
|
||||
|
||||
@@ -26,7 +26,8 @@ declare global {
|
||||
export interface IView {
|
||||
name: string;
|
||||
iconName?: string;
|
||||
element: DeesElement['constructor']['prototype'];
|
||||
element?: DeesElement['constructor']['prototype'];
|
||||
subViews?: IView[];
|
||||
}
|
||||
|
||||
export type TGlobalMessageType = 'info' | 'success' | 'warning' | 'error';
|
||||
@@ -250,6 +251,55 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.viewTab .chevron {
|
||||
flex: 0 0 auto;
|
||||
font-size: 14px;
|
||||
opacity: 0.5;
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.2s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.viewTab.hasSubs:hover .chevron {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.viewTab.hasSubs.groupActive .chevron {
|
||||
transform: rotate(0deg);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.subViews {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
margin: 2px 0 4px 12px;
|
||||
padding-left: 12px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.subViews::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 4px;
|
||||
bottom: 4px;
|
||||
width: 1px;
|
||||
background: var(--dees-color-border-default);
|
||||
}
|
||||
|
||||
.viewTab.sub {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.viewTab.sub dees-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.viewTab.sub.selected::before {
|
||||
left: -12px;
|
||||
}
|
||||
|
||||
.appActions {
|
||||
padding: 12px 8px;
|
||||
border-top: 1px solid var(--dees-color-border-default);
|
||||
@@ -563,10 +613,12 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
<div class="viewTabs-container">
|
||||
<div class="section-label">Navigation</div>
|
||||
<div class="viewTabs">
|
||||
${this.viewTabs.map(
|
||||
(view) => html`
|
||||
${this.viewTabs.map((view) => {
|
||||
const hasSubs = !!view.subViews?.length;
|
||||
const groupActive = hasSubs && this.isGroupActive(view);
|
||||
return html`
|
||||
<div
|
||||
class="viewTab ${this.selectedView === view ? 'selected' : ''}"
|
||||
class="viewTab ${this.selectedView === view ? 'selected' : ''} ${hasSubs ? 'hasSubs' : ''} ${groupActive ? 'groupActive' : ''}"
|
||||
@click=${() => this.loadView(view)}
|
||||
>
|
||||
${view.iconName ? html`
|
||||
@@ -575,9 +627,34 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
<dees-icon .icon="${'lucide:file'}"></dees-icon>
|
||||
`}
|
||||
<span>${view.name}</span>
|
||||
${hasSubs ? html`
|
||||
<dees-icon class="chevron" .icon="${'lucide:chevronDown'}"></dees-icon>
|
||||
` : ''}
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${hasSubs && groupActive ? html`
|
||||
<div class="subViews">
|
||||
${view.subViews!.map(
|
||||
(sub) => html`
|
||||
<div
|
||||
class="viewTab sub ${this.selectedView === sub ? 'selected' : ''}"
|
||||
@click=${(e: Event) => {
|
||||
e.stopPropagation();
|
||||
this.loadView(sub);
|
||||
}}
|
||||
>
|
||||
${sub.iconName ? html`
|
||||
<dees-icon .icon="${sub.iconName.includes(':') ? sub.iconName : `lucide:${sub.iconName}`}"></dees-icon>
|
||||
` : html`
|
||||
<dees-icon .icon="${'lucide:dot'}"></dees-icon>
|
||||
`}
|
||||
<span>${sub.name}</span>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div class="appActions">
|
||||
@@ -771,8 +848,23 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
}
|
||||
|
||||
|
||||
private isGroupActive(view: IView): boolean {
|
||||
if (this.selectedView === view) return true;
|
||||
return view.subViews?.some((sv) => sv === this.selectedView) ?? false;
|
||||
}
|
||||
|
||||
private currentView!: DeesElement;
|
||||
public async loadView(viewArg: IView) {
|
||||
// Group-only parent: resolve to first sub view with an element
|
||||
if (!viewArg.element && viewArg.subViews?.length) {
|
||||
const firstNavigable = viewArg.subViews.find((sv) => sv.element);
|
||||
if (firstNavigable) {
|
||||
return this.loadView(firstNavigable);
|
||||
}
|
||||
return; // nothing navigable — ignore click
|
||||
}
|
||||
if (!viewArg.element) return; // safety: no element and no subs → no-op
|
||||
|
||||
const appcontent = this.shadowRoot!.querySelector('.appcontent')!;
|
||||
const view = new viewArg.element();
|
||||
if (this.currentView) {
|
||||
@@ -781,7 +873,7 @@ export class DeesSimpleAppDash extends DeesElement {
|
||||
appcontent.appendChild(view);
|
||||
this.currentView = view;
|
||||
this.selectedView = viewArg;
|
||||
|
||||
|
||||
// Emit view-select event
|
||||
this.dispatchEvent(new CustomEvent('view-select', {
|
||||
detail: { view: viewArg },
|
||||
|
||||
Reference in New Issue
Block a user