Compare commits

..

7 Commits

Author SHA1 Message Date
7817b4a440 1.10.7
Some checks failed
Default (tags) / security (push) Failing after 54s
Default (tags) / test (push) Failing after 12s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-06-27 19:33:37 +00:00
03f25b7f10 update wysiwyg 2025-06-27 19:29:26 +00:00
24957f02d4 update wysiwyg 2025-06-27 19:25:34 +00:00
fe3cd0591f update 2025-06-27 18:38:39 +00:00
56f5f5887b update 2025-06-27 18:03:42 +00:00
2e0bf26301 update table 2025-06-27 17:50:54 +00:00
3d7f5253e8 update fonts 2025-06-27 17:32:01 +00:00
27 changed files with 1885 additions and 627 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@design.estate/dees-catalog",
"version": "1.10.6",
"version": "1.10.7",
"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",

View File

@ -0,0 +1,35 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js';
import { demoFunc } from '../ts_web/elements/dees-contextmenu.demo.js';
tap.test('should render context menu demo', async () => {
// Create demo container
const demoContainer = document.createElement('div');
document.body.appendChild(demoContainer);
// Render the demo
const demoContent = demoFunc();
// Create a temporary element to hold the rendered template
const tempDiv = document.createElement('div');
tempDiv.innerHTML = demoContent.strings.join('');
// Check that panels are rendered
const panels = tempDiv.querySelectorAll('dees-panel');
expect(panels.length).toEqual(4);
// Check panel headings
expect(panels[0].getAttribute('heading')).toEqual('Basic Context Menu with Nested Submenus');
expect(panels[1].getAttribute('heading')).toEqual('Component-Specific Context Menu');
expect(panels[2].getAttribute('heading')).toEqual('Advanced Context Menu Example');
expect(panels[3].getAttribute('heading')).toEqual('Static Context Menu (Always Visible)');
// Check that static context menu exists
const staticMenu = tempDiv.querySelector('dees-contextmenu');
expect(staticMenu).toBeTruthy();
// Clean up
demoContainer.remove();
});
export default tap.start();

View File

@ -0,0 +1,93 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js';
tap.test('should close all parent menus when clicking action in nested submenu', async () => {
let actionCalled = false;
// Create a test element
const testDiv = document.createElement('div');
testDiv.style.width = '300px';
testDiv.style.height = '300px';
testDiv.style.background = '#f0f0f0';
testDiv.innerHTML = 'Right-click for nested menu test';
document.body.appendChild(testDiv);
// Simulate right-click to open context menu
const contextMenuEvent = new MouseEvent('contextmenu', {
clientX: 150,
clientY: 150,
bubbles: true,
cancelable: true
});
// Open context menu with nested structure
DeesContextmenu.openContextMenuWithOptions(contextMenuEvent, [
{
name: 'Parent Item',
iconName: 'folder',
action: async () => {}, // Parent items with submenus need an action
submenu: [
{
name: 'Child Item',
iconName: 'file',
action: async () => {
actionCalled = true;
console.log('Child action called');
}
},
{
name: 'Another Child',
iconName: 'fileText',
action: async () => console.log('Another child')
}
]
},
{
name: 'Regular Item',
iconName: 'box',
action: async () => console.log('Regular item')
}
]);
// Wait for main menu to appear
await new Promise(resolve => setTimeout(resolve, 150));
// Check main menu exists
const mainMenu = document.querySelector('dees-contextmenu');
expect(mainMenu).toBeInstanceOf(DeesContextmenu);
// Hover over "Parent Item" to trigger submenu
const parentItem = mainMenu!.shadowRoot!.querySelector('.menuitem');
expect(parentItem).toBeTruthy();
parentItem!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
// Wait for submenu to appear
await new Promise(resolve => setTimeout(resolve, 300));
// Check submenu exists
const allMenus = document.querySelectorAll('dees-contextmenu');
expect(allMenus.length).toEqual(2); // Main menu and submenu
const submenu = allMenus[1];
expect(submenu).toBeTruthy();
// Click on "Child Item" in submenu
const childItem = submenu.shadowRoot!.querySelector('.menuitem');
expect(childItem).toBeTruthy();
childItem!.click();
// Wait for menus to close
await new Promise(resolve => setTimeout(resolve, 200));
// Verify action was called
expect(actionCalled).toEqual(true);
// Verify all menus are closed
const remainingMenus = document.querySelectorAll('dees-contextmenu');
expect(remainingMenus.length).toEqual(0);
// Clean up
testDiv.remove();
});
export default tap.start();

View File

@ -0,0 +1,71 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js';
import { DeesElement, customElement, html } from '@design.estate/dees-element';
// Create a test element with shadow DOM
@customElement('test-shadow-element')
class TestShadowElement extends DeesElement {
public getContextMenuItems() {
return [
{ name: 'Shadow Item 1', iconName: 'box', action: async () => console.log('Shadow 1') },
{ name: 'Shadow Item 2', iconName: 'package', action: async () => console.log('Shadow 2') }
];
}
render() {
return html`
<div style="padding: 40px; background: #eee; border-radius: 8px;">
<h3>Shadow DOM Content</h3>
<p>Right-click anywhere inside this shadow DOM</p>
</div>
`;
}
}
tap.test('should show context menu when right-clicking inside shadow DOM', async () => {
// Create the shadow DOM element
const shadowElement = document.createElement('test-shadow-element');
document.body.appendChild(shadowElement);
// Wait for element to be ready
await shadowElement.updateComplete;
// Get the content inside shadow DOM
const shadowContent = shadowElement.shadowRoot!.querySelector('div');
expect(shadowContent).toBeTruthy();
// Simulate right-click on content inside shadow DOM
const contextMenuEvent = new MouseEvent('contextmenu', {
clientX: 100,
clientY: 100,
bubbles: true,
cancelable: true,
composed: true // Important for shadow DOM
});
shadowContent!.dispatchEvent(contextMenuEvent);
// Wait for context menu to appear
await new Promise(resolve => setTimeout(resolve, 100));
// Check if context menu is created
const contextMenu = document.querySelector('dees-contextmenu');
expect(contextMenu).toBeInstanceOf(DeesContextmenu);
// Check if menu items from shadow element are rendered
const menuItems = contextMenu!.shadowRoot!.querySelectorAll('.menuitem');
expect(menuItems.length).toBeGreaterThanOrEqual(2);
// Check menu item text
const menuTexts = Array.from(menuItems).map(item =>
item.querySelector('.menuitem-text')?.textContent
);
expect(menuTexts).toContain('Shadow Item 1');
expect(menuTexts).toContain('Shadow Item 2');
// Clean up
contextMenu!.remove();
shadowElement.remove();
});
export default tap.start();

View File

@ -0,0 +1,77 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js';
tap.test('should show context menu with nested submenu', async () => {
// Create a test element with context menu items
const testDiv = document.createElement('div');
testDiv.style.width = '200px';
testDiv.style.height = '200px';
testDiv.style.background = '#eee';
testDiv.innerHTML = 'Right-click me';
// Add getContextMenuItems method
(testDiv as any).getContextMenuItems = () => {
return [
{
name: 'Change Type',
iconName: 'type',
submenu: [
{ name: 'Paragraph', iconName: 'text', action: () => console.log('Paragraph') },
{ name: 'Heading 1', iconName: 'heading1', action: () => console.log('Heading 1') },
{ name: 'Heading 2', iconName: 'heading2', action: () => console.log('Heading 2') },
{ divider: true },
{ name: 'Code Block', iconName: 'fileCode', action: () => console.log('Code') },
{ name: 'Quote', iconName: 'quote', action: () => console.log('Quote') }
]
},
{ divider: true },
{
name: 'Delete',
iconName: 'trash2',
action: () => console.log('Delete')
}
];
};
document.body.appendChild(testDiv);
// Simulate right-click
const contextMenuEvent = new MouseEvent('contextmenu', {
clientX: 100,
clientY: 100,
bubbles: true,
cancelable: true
});
testDiv.dispatchEvent(contextMenuEvent);
// Wait for context menu to appear
await new Promise(resolve => setTimeout(resolve, 100));
// Check if context menu is created
const contextMenu = document.querySelector('dees-contextmenu');
expect(contextMenu).toBeInstanceOf(DeesContextmenu);
// Check if menu items are rendered
const menuItems = contextMenu!.shadowRoot!.querySelectorAll('.menuitem');
expect(menuItems.length).toEqual(2); // "Change Type" and "Delete"
// Hover over "Change Type" to trigger submenu
const changeTypeItem = menuItems[0] as HTMLElement;
changeTypeItem.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
// Wait for submenu to appear
await new Promise(resolve => setTimeout(resolve, 300));
// Check if submenu is created
const submenus = document.querySelectorAll('dees-contextmenu');
expect(submenus.length).toEqual(2); // Main menu and submenu
// Clean up
contextMenu!.remove();
const submenu = submenus[1];
if (submenu) submenu.remove();
testDiv.remove();
});
export default tap.start();

View File

@ -0,0 +1,98 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js';
tap.test('should change block type via context menu', async () => {
// Create WYSIWYG editor with a paragraph
const wysiwygEditor = new DeesInputWysiwyg();
wysiwygEditor.value = '<p>This is a test paragraph</p>';
document.body.appendChild(wysiwygEditor);
// Wait for editor to be ready
await wysiwygEditor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Get the first block
const firstBlock = wysiwygEditor.blocks[0];
expect(firstBlock.type).toEqual('paragraph');
// Get the block element
const firstBlockWrapper = wysiwygEditor.shadowRoot!.querySelector('.block-wrapper');
expect(firstBlockWrapper).toBeTruthy();
const blockComponent = firstBlockWrapper!.querySelector('dees-wysiwyg-block') as any;
expect(blockComponent).toBeTruthy();
await blockComponent.updateComplete;
// Get the editable content inside the block's shadow DOM
const editableBlock = blockComponent.shadowRoot!.querySelector('.block');
expect(editableBlock).toBeTruthy();
// Simulate right-click on the editable block
const contextMenuEvent = new MouseEvent('contextmenu', {
clientX: 200,
clientY: 200,
bubbles: true,
cancelable: true,
composed: true
});
editableBlock!.dispatchEvent(contextMenuEvent);
// Wait for context menu to appear
await new Promise(resolve => setTimeout(resolve, 100));
// Check if context menu is created
const contextMenu = document.querySelector('dees-contextmenu');
expect(contextMenu).toBeInstanceOf(DeesContextmenu);
// Find "Change Type" menu item
const menuItems = Array.from(contextMenu!.shadowRoot!.querySelectorAll('.menuitem'));
const changeTypeItem = menuItems.find(item =>
item.querySelector('.menuitem-text')?.textContent?.trim() === 'Change Type'
);
expect(changeTypeItem).toBeTruthy();
// Hover over "Change Type" to trigger submenu
changeTypeItem!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
// Wait for submenu to appear
await new Promise(resolve => setTimeout(resolve, 300));
// Check if submenu is created
const allMenus = document.querySelectorAll('dees-contextmenu');
expect(allMenus.length).toEqual(2);
const submenu = allMenus[1];
const submenuItems = Array.from(submenu.shadowRoot!.querySelectorAll('.menuitem'));
// Find "Heading 1" option
const heading1Item = submenuItems.find(item =>
item.querySelector('.menuitem-text')?.textContent?.trim() === 'Heading 1'
);
expect(heading1Item).toBeTruthy();
// Click on "Heading 1"
(heading1Item as HTMLElement).click();
// Wait for menu to close and block to update
await new Promise(resolve => setTimeout(resolve, 300));
// Verify block type has changed
expect(wysiwygEditor.blocks[0].type).toEqual('heading-1');
// Verify DOM has been updated
const updatedBlockComponent = wysiwygEditor.shadowRoot!
.querySelector('.block-wrapper')!
.querySelector('dees-wysiwyg-block') as any;
await updatedBlockComponent.updateComplete;
const updatedBlock = updatedBlockComponent.shadowRoot!.querySelector('.block');
expect(updatedBlock?.classList.contains('heading-1')).toEqual(true);
// Clean up
wysiwygEditor.remove();
});
export default tap.start();

View File

@ -0,0 +1,68 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { DeesInputWysiwyg } from '../ts_web/elements/wysiwyg/dees-input-wysiwyg.js';
import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu.js';
tap.test('should show context menu on WYSIWYG blocks', async () => {
// Create WYSIWYG editor
const wysiwygEditor = new DeesInputWysiwyg();
wysiwygEditor.value = '<p>Test paragraph</p><h1>Test heading</h1>';
document.body.appendChild(wysiwygEditor);
// Wait for editor to be ready
await wysiwygEditor.updateComplete;
await new Promise(resolve => setTimeout(resolve, 100));
// Get the first block element
const firstBlockWrapper = wysiwygEditor.shadowRoot!.querySelector('.block-wrapper');
expect(firstBlockWrapper).toBeTruthy();
const blockComponent = firstBlockWrapper!.querySelector('dees-wysiwyg-block') as any;
expect(blockComponent).toBeTruthy();
// Wait for block to be ready
await blockComponent.updateComplete;
// Get the editable content inside the block's shadow DOM
const editableBlock = blockComponent.shadowRoot!.querySelector('.block');
expect(editableBlock).toBeTruthy();
// Simulate right-click on the editable block
const contextMenuEvent = new MouseEvent('contextmenu', {
clientX: 200,
clientY: 200,
bubbles: true,
cancelable: true,
composed: true // Important for shadow DOM
});
editableBlock!.dispatchEvent(contextMenuEvent);
// Wait for context menu to appear
await new Promise(resolve => setTimeout(resolve, 100));
// Check if context menu is created
const contextMenu = document.querySelector('dees-contextmenu');
expect(contextMenu).toBeInstanceOf(DeesContextmenu);
// Check if menu items from WYSIWYG block are rendered
const menuItems = contextMenu!.shadowRoot!.querySelectorAll('.menuitem');
const menuTexts = Array.from(menuItems).map(item =>
item.querySelector('.menuitem-text')?.textContent?.trim()
);
// Should have "Change Type" and "Delete Block" items
expect(menuTexts).toContain('Change Type');
expect(menuTexts).toContain('Delete Block');
// Check if "Change Type" has submenu indicator
const changeTypeItem = Array.from(menuItems).find(item =>
item.querySelector('.menuitem-text')?.textContent?.trim() === 'Change Type'
);
expect(changeTypeItem?.classList.contains('has-submenu')).toEqual(true);
// Clean up
contextMenu!.remove();
wysiwygEditor.remove();
});
export default tap.start();

View File

@ -26,6 +26,22 @@ export const monoFontFamily = `'${intelOneMonoFont}', 'SF Mono', 'Monaco', 'Inco
export const cssGeistFontFamily = unsafeCSS(geistFontFamily);
export const cssMonoFontFamily = unsafeCSS(monoFontFamily);
/**
* Cal Sans font for headings - Display font
* May need to be loaded separately
*/
export const calSansFont = 'Cal Sans';
export const calSansFontFamily = `'${calSansFont}', ${geistFontFamily}`;
export const cssCalSansFontFamily = unsafeCSS(calSansFontFamily);
/**
* Roboto Slab font for special content - Serif font
* May need to be loaded separately
*/
export const robotoSlabFont = 'Roboto Slab';
export const robotoSlabFontFamily = `'${robotoSlabFont}', Georgia, serif`;
export const cssRobotoSlabFontFamily = unsafeCSS(robotoSlabFontFamily);
/**
* Base font styles that can be applied to components
*/

View File

@ -13,139 +13,203 @@ export const demoFunc = () => html`
display: flex;
flex-direction: column;
gap: 20px;
padding: 40px;
background: #f5f5f5;
padding: 20px;
min-height: 400px;
}
.demo-area {
background: white;
padding: 40px;
border-radius: 8px;
border: 1px solid #e0e0e0;
text-align: center;
cursor: context-menu;
transition: background 0.2s;
}
.demo-area:hover {
background: rgba(0, 0, 0, 0.02);
}
</style>
<div class="demo-container">
<div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'Cut',
iconName: 'scissors',
shortcut: 'Cmd+X',
action: async () => {
console.log('Cut action');
<dees-panel heading="Basic Context Menu with Nested Submenus">
<div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'File',
iconName: 'fileText',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'New', iconName: 'filePlus', shortcut: 'Cmd+N', action: async () => console.log('New file') },
{ name: 'Open', iconName: 'folderOpen', shortcut: 'Cmd+O', action: async () => console.log('Open file') },
{ name: 'Save', iconName: 'save', shortcut: 'Cmd+S', action: async () => console.log('Save') },
{ divider: true },
{ name: 'Export as PDF', iconName: 'download', action: async () => console.log('Export PDF') },
{ name: 'Export as HTML', iconName: 'code', action: async () => console.log('Export HTML') },
]
},
},
{
name: 'Copy',
iconName: 'copy',
shortcut: 'Cmd+C',
action: async () => {
console.log('Copy action');
{
name: 'Edit',
iconName: 'edit3',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'Cut', iconName: 'scissors', shortcut: 'Cmd+X', action: async () => console.log('Cut') },
{ name: 'Copy', iconName: 'copy', shortcut: 'Cmd+C', action: async () => console.log('Copy') },
{ name: 'Paste', iconName: 'clipboard', shortcut: 'Cmd+V', action: async () => console.log('Paste') },
{ divider: true },
{ name: 'Find', iconName: 'search', shortcut: 'Cmd+F', action: async () => console.log('Find') },
{ name: 'Replace', iconName: 'repeat', shortcut: 'Cmd+H', action: async () => console.log('Replace') },
]
},
},
{
name: 'Paste',
iconName: 'clipboard',
shortcut: 'Cmd+V',
action: async () => {
console.log('Paste action');
{
name: 'View',
iconName: 'eye',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'Zoom In', iconName: 'zoomIn', shortcut: 'Cmd++', action: async () => console.log('Zoom in') },
{ name: 'Zoom Out', iconName: 'zoomOut', shortcut: 'Cmd+-', action: async () => console.log('Zoom out') },
{ name: 'Reset Zoom', iconName: 'maximize2', shortcut: 'Cmd+0', action: async () => console.log('Reset zoom') },
{ divider: true },
{ name: 'Full Screen', iconName: 'maximize', shortcut: 'F11', action: async () => console.log('Full screen') },
]
},
},
{ divider: true },
{
name: 'Delete',
iconName: 'trash2',
action: async () => {
console.log('Delete action');
{ divider: true },
{
name: 'Settings',
iconName: 'settings',
action: async () => console.log('Settings')
},
},
{ divider: true },
{
name: 'Select All',
shortcut: 'Cmd+A',
action: async () => {
console.log('Select All action');
{
name: 'Help',
iconName: 'helpCircle',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'Documentation', iconName: 'book', action: async () => console.log('Documentation') },
{ name: 'Keyboard Shortcuts', iconName: 'keyboard', action: async () => console.log('Shortcuts') },
{ divider: true },
{ name: 'About', iconName: 'info', action: async () => console.log('About') },
]
}
]);
}}>
<h3>Right-click anywhere in this area</h3>
<p>A context menu with nested submenus will appear</p>
</div>
</dees-panel>
<dees-panel heading="Component-Specific Context Menu">
<dees-button style="margin: 20px;" @contextmenu=${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'Button Actions',
iconName: 'mousePointer',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'Click', iconName: 'mouse', action: async () => console.log('Click action') },
{ name: 'Double Click', iconName: 'zap', action: async () => console.log('Double click') },
{ name: 'Long Press', iconName: 'clock', action: async () => console.log('Long press') },
]
},
},
]);
}}>
<h3>Right-click anywhere in this area</h3>
<p>A context menu will appear with various options</p>
</div>
{
name: 'Button State',
iconName: 'toggleLeft',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'Enable', iconName: 'checkCircle', action: async () => console.log('Enable') },
{ name: 'Disable', iconName: 'xCircle', action: async () => console.log('Disable') },
{ divider: true },
{ name: 'Show', iconName: 'eye', action: async () => console.log('Show') },
{ name: 'Hide', iconName: 'eyeOff', action: async () => console.log('Hide') },
]
},
{ divider: true },
{
name: 'Disabled Action',
iconName: 'ban',
disabled: true,
action: async () => console.log('This should not run'),
},
{
name: 'Properties',
iconName: 'settings',
action: async () => console.log('Button properties'),
},
]);
}}>Right-click on this button</dees-button>
</dees-panel>
<dees-panel heading="Advanced Context Menu Example">
<div class="demo-area" @contextmenu=${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'Format',
iconName: 'type',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'Bold', iconName: 'bold', shortcut: 'Cmd+B', action: async () => console.log('Bold') },
{ name: 'Italic', iconName: 'italic', shortcut: 'Cmd+I', action: async () => console.log('Italic') },
{ name: 'Underline', iconName: 'underline', shortcut: 'Cmd+U', action: async () => console.log('Underline') },
{ divider: true },
{ name: 'Font Size', iconName: 'type', action: async () => console.log('Font size menu') },
{ name: 'Font Color', iconName: 'palette', action: async () => console.log('Font color menu') },
]
},
{
name: 'Transform',
iconName: 'shuffle',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'To Uppercase', iconName: 'arrowUp', action: async () => console.log('Uppercase') },
{ name: 'To Lowercase', iconName: 'arrowDown', action: async () => console.log('Lowercase') },
{ name: 'Capitalize', iconName: 'type', action: async () => console.log('Capitalize') },
]
},
{ divider: true },
{
name: 'Delete',
iconName: 'trash2',
action: async () => console.log('Delete')
}
]);
}}>
<h3>Advanced Nested Menu Example</h3>
<p>This shows deeply nested submenus and various formatting options</p>
</div>
</dees-panel>
<dees-button @contextmenu=${(eventArg: MouseEvent) => {
DeesContextmenu.openContextMenuWithOptions(eventArg, [
{
name: 'Button Action 1',
iconName: 'play',
action: async () => {
console.log('Button action 1');
},
},
{
name: 'Button Action 2',
iconName: 'pause',
action: async () => {
console.log('Button action 2');
},
},
{
name: 'Disabled Action',
iconName: 'ban',
disabled: true,
action: async () => {
console.log('This should not run');
},
},
{ divider: true },
{
name: 'Settings',
iconName: 'settings',
action: async () => {
console.log('Settings');
},
},
]);
}}>Right-click on this button for a different menu</dees-button>
<div style="margin-top: 20px;">
<h4>Static Context Menu (always visible):</h4>
<dees-panel heading="Static Context Menu (Always Visible)">
<dees-contextmenu
class="withMargin"
.menuItems=${[
{
name: 'New File',
iconName: 'filePlus',
shortcut: 'Cmd+N',
action: async () => console.log('New file'),
name: 'Project',
iconName: 'folder',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'New Project', iconName: 'folderPlus', shortcut: 'Cmd+Shift+N', action: async () => console.log('New project') },
{ name: 'Open Project', iconName: 'folderOpen', shortcut: 'Cmd+Shift+O', action: async () => console.log('Open project') },
{ divider: true },
{ name: 'Recent Projects', iconName: 'clock', action: async () => {}, submenu: [
{ name: 'Project Alpha', action: async () => console.log('Open Alpha') },
{ name: 'Project Beta', action: async () => console.log('Open Beta') },
{ name: 'Project Gamma', action: async () => console.log('Open Gamma') },
]},
]
},
{
name: 'Open File',
iconName: 'folderOpen',
shortcut: 'Cmd+O',
action: async () => console.log('Open file'),
},
{
name: 'Save',
iconName: 'save',
shortcut: 'Cmd+S',
action: async () => console.log('Save'),
name: 'Tools',
iconName: 'tool',
action: async () => {}, // Parent items with submenus still need an action
submenu: [
{ name: 'Terminal', iconName: 'terminal', shortcut: 'Cmd+T', action: async () => console.log('Terminal') },
{ name: 'Console', iconName: 'monitor', shortcut: 'Cmd+K', action: async () => console.log('Console') },
{ divider: true },
{ name: 'Extensions', iconName: 'package', action: async () => console.log('Extensions') },
]
},
{ divider: true },
{
name: 'Export',
iconName: 'download',
action: async () => console.log('Export'),
},
{
name: 'Import',
iconName: 'upload',
action: async () => console.log('Import'),
name: 'Preferences',
iconName: 'sliders',
action: async () => console.log('Preferences'),
},
]}
></dees-contextmenu>
</div>
</dees-panel>
</div>
`;

View File

@ -31,7 +31,7 @@ export class DeesContextmenu extends DeesElement {
// STATIC
// This will store all the accumulated menu items
public static contextMenuDeactivated = false;
public static accumulatedMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] = [];
public static accumulatedMenuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] } | { divider: true })[] = [];
// Add a global event listener for the right-click context menu
public static initializeGlobalListener() {
@ -41,16 +41,16 @@ export class DeesContextmenu extends DeesElement {
}
event.preventDefault();
// Get the target element of the right-click
let target: EventTarget | null = event.target;
// Clear previously accumulated items
DeesContextmenu.accumulatedMenuItems = [];
// Traverse up the DOM tree to accumulate menu items
while (target) {
if ((target as any).getContextMenuItems) {
const items = (target as any).getContextMenuItems();
// Use composedPath to properly traverse shadow DOM boundaries
const path = event.composedPath();
// Traverse the composed path to accumulate menu items
for (const element of path) {
if ((element as any).getContextMenuItems) {
const items = (element as any).getContextMenuItems();
if (items && items.length > 0) {
if (DeesContextmenu.accumulatedMenuItems.length > 0) {
DeesContextmenu.accumulatedMenuItems.push({ divider: true });
@ -58,7 +58,6 @@ export class DeesContextmenu extends DeesElement {
DeesContextmenu.accumulatedMenuItems.push(...items);
}
}
target = (target as Node).parentNode;
}
// Open the context menu with the accumulated items
@ -67,7 +66,7 @@ export class DeesContextmenu extends DeesElement {
}
// allows opening of a contextmenu with options
public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[]) {
public static async openContextMenuWithOptions(eventArg: MouseEvent, menuItemsArg: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[] } | { divider: true })[]) {
if (this.contextMenuDeactivated) {
return;
}
@ -80,8 +79,13 @@ export class DeesContextmenu extends DeesElement {
contextMenu.style.transform = 'scale(0.95) translateY(-10px)';
contextMenu.menuItems = menuItemsArg;
contextMenu.windowLayer = await DeesWindowLayer.createAndShow();
contextMenu.windowLayer.addEventListener('click', async () => {
await contextMenu.destroy();
contextMenu.windowLayer.addEventListener('click', async (event) => {
// Check if click is on the context menu or its submenus
const clickedElement = event.target as HTMLElement;
const isContextMenu = clickedElement.closest('dees-contextmenu');
if (!isContextMenu) {
await contextMenu.destroy();
}
})
document.body.append(contextMenu);
@ -123,8 +127,12 @@ export class DeesContextmenu extends DeesElement {
@property({
type: Array,
})
public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; divider?: never } | { divider: true })[] = [];
public menuItems: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: (plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean } | { divider: true })[]; divider?: never } | { divider: true })[] = [];
windowLayer: DeesWindowLayer;
private submenu: DeesContextmenu | null = null;
private submenuTimeout: any = null;
private parentMenu: DeesContextmenu | null = null;
constructor() {
super();
@ -167,13 +175,22 @@ export class DeesContextmenu extends DeesElement {
cursor: default;
transition: background 0.1s;
line-height: 1;
position: relative;
}
.menuitem:hover {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.08)')};
}
.menuitem.has-submenu::after {
content: '';
position: absolute;
right: 8px;
font-size: 16px;
opacity: 0.5;
}
.menuitem:active {
.menuitem:active:not(.has-submenu) {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.12)')};
}
@ -215,14 +232,20 @@ export class DeesContextmenu extends DeesElement {
return html`<div class="menu-divider"></div>`;
}
const menuItem = menuItemArg as plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean };
const menuItem = menuItemArg as plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean; submenu?: any };
const hasSubmenu = menuItem.submenu && menuItem.submenu.length > 0;
return html`
<div class="menuitem ${menuItem.disabled ? 'disabled' : ''}" @click=${() => !menuItem.disabled && this.handleClick(menuItem)}>
<div
class="menuitem ${menuItem.disabled ? 'disabled' : ''} ${hasSubmenu ? 'has-submenu' : ''}"
@click=${() => !menuItem.disabled && !hasSubmenu && this.handleClick(menuItem)}
@mouseenter=${() => this.handleMenuItemHover(menuItem, hasSubmenu)}
@mouseleave=${() => this.handleMenuItemLeave()}
>
${menuItem.iconName ? html`
<dees-icon .icon="${`lucide:${menuItem.iconName}`}"></dees-icon>
` : ''}
<span class="menuitem-text">${menuItem.name}</span>
${menuItem.shortcut ? html`
${menuItem.shortcut && !hasSubmenu ? html`
<span class="menuitem-shortcut">${menuItem.shortcut}</span>
` : ''}
</div>
@ -282,17 +305,151 @@ export class DeesContextmenu extends DeesElement {
public async handleClick(menuItem: plugins.tsclass.website.IMenuItem & { shortcut?: string; disabled?: boolean }) {
menuItem.action();
await this.destroy();
// Close all menus in the chain (this menu and all parent menus)
await this.destroyAll();
}
private async handleMenuItemHover(menuItem: plugins.tsclass.website.IMenuItem & { submenu?: any }, hasSubmenu: boolean) {
// Clear any existing timeout
if (this.submenuTimeout) {
clearTimeout(this.submenuTimeout);
this.submenuTimeout = null;
}
// Hide any existing submenu if hovering a different item
if (this.submenu) {
await this.hideSubmenu();
}
// Show submenu if this item has one
if (hasSubmenu && menuItem.submenu) {
this.submenuTimeout = setTimeout(() => {
this.showSubmenu(menuItem);
}, 200); // Small delay to prevent accidental triggers
}
}
private handleMenuItemLeave() {
// Add a delay before hiding to allow moving to submenu
if (this.submenuTimeout) {
clearTimeout(this.submenuTimeout);
}
this.submenuTimeout = setTimeout(() => {
if (this.submenu && !this.submenu.matches(':hover')) {
this.hideSubmenu();
}
}, 300);
}
private async showSubmenu(menuItem: plugins.tsclass.website.IMenuItem & { submenu?: any }) {
if (!menuItem.submenu || menuItem.submenu.length === 0) return;
// Find the menu item element
const menuItems = Array.from(this.shadowRoot.querySelectorAll('.menuitem'));
const menuItemElement = menuItems.find(el => el.querySelector('.menuitem-text')?.textContent === menuItem.name) as HTMLElement;
if (!menuItemElement) return;
// Create submenu
this.submenu = new DeesContextmenu();
this.submenu.menuItems = menuItem.submenu;
this.submenu.parentMenu = this;
this.submenu.style.position = 'fixed';
this.submenu.style.zIndex = String(parseInt(this.style.zIndex) + 1);
this.submenu.style.opacity = '0';
this.submenu.style.transform = 'scale(0.95)';
// Don't create a window layer for submenus
document.body.append(this.submenu);
// Position submenu
await domtools.plugins.smartdelay.delayFor(0);
const itemRect = menuItemElement.getBoundingClientRect();
const menuRect = this.getBoundingClientRect();
const submenuRect = this.submenu.getBoundingClientRect();
const windowWidth = window.innerWidth;
let left = menuRect.right - 4; // Slight overlap
let top = itemRect.top;
// Check if submenu would go off right edge
if (left + submenuRect.width > windowWidth - 10) {
// Show on left side instead
left = menuRect.left - submenuRect.width + 4;
}
// Adjust vertical position if needed
if (top + submenuRect.height > window.innerHeight - 10) {
top = window.innerHeight - submenuRect.height - 10;
}
this.submenu.style.left = `${left}px`;
this.submenu.style.top = `${top}px`;
// Animate in
await domtools.plugins.smartdelay.delayFor(0);
this.submenu.style.opacity = '1';
this.submenu.style.transform = 'scale(1)';
// Handle submenu hover
this.submenu.addEventListener('mouseenter', () => {
if (this.submenuTimeout) {
clearTimeout(this.submenuTimeout);
this.submenuTimeout = null;
}
});
this.submenu.addEventListener('mouseleave', () => {
this.handleMenuItemLeave();
});
}
private async hideSubmenu() {
if (!this.submenu) return;
await this.submenu.destroy();
this.submenu = null;
}
public async destroy() {
if (this.windowLayer) {
// Clear timeout
if (this.submenuTimeout) {
clearTimeout(this.submenuTimeout);
this.submenuTimeout = null;
}
// Destroy submenu first
if (this.submenu) {
await this.submenu.destroy();
this.submenu = null;
}
// Only destroy window layer if this is not a submenu
if (this.windowLayer && !this.parentMenu) {
this.windowLayer.destroy();
}
this.style.opacity = '0';
this.style.transform = 'scale(0.95) translateY(-10px)';
await domtools.plugins.smartdelay.delayFor(100);
this.parentElement.removeChild(this);
if (this.parentElement) {
this.parentElement.removeChild(this);
}
}
/**
* Destroys this menu and all parent menus in the chain
*/
public async destroyAll() {
// First destroy parent menus if they exist
if (this.parentMenu) {
await this.parentMenu.destroyAll();
} else {
// If we're at the top level, just destroy this menu
await this.destroy();
}
}
}

View File

@ -8,6 +8,7 @@ import {
state,
cssManager,
} from '@design.estate/dees-element';
import { cssGeistFontFamily, cssMonoFontFamily } from './00fonts.js';
import hlight from 'highlight.js';
@ -48,7 +49,7 @@ export class DeesDataviewCodebox extends DeesElement {
display: block;
text-align: left;
font-size: 16px;
font-family: 'Geist Sans', sans-serif;
font-family: ${cssGeistFontFamily};
}
.mainbox {
position: relative;
@ -142,7 +143,7 @@ export class DeesDataviewCodebox extends DeesElement {
.lineNumbers {
line-height: 1.4em;
font-weight: 200;
font-family: 'Intel One Mono', 'Geist Mono', 'monospace';
font-family: ${cssMonoFontFamily};
}
.hljs-string {

View File

@ -10,6 +10,7 @@ import {
} from '@design.estate/dees-element';
import { demoFunc } from './dees-heading.demo.js';
import { cssCalSansFontFamily } from './00fonts.js';
declare global {
interface HTMLElementTagNameMap {
@ -39,7 +40,7 @@ export class DeesHeading extends DeesElement {
font-weight: 600;
color: ${cssManager.bdTheme('#000', '#fff')};
}
h1 { font-size: 32px; font-family: 'Cal Sans'; letter-spacing: 0.025em;}
h1 { font-size: 32px; font-family: ${cssCalSansFontFamily}; letter-spacing: 0.025em;}
h2 { font-size: 28px; }
h3 { font-size: 24px; }
h4 { font-size: 20px; }

View File

@ -8,6 +8,7 @@ import {
} from '@design.estate/dees-element';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-checkbox.demo.js';
import { cssGeistFontFamily } from './00fonts.js';
declare global {
interface HTMLElementTagNameMap {
@ -44,7 +45,7 @@ export class DeesInputCheckbox extends DeesInputBase<DeesInputCheckbox> {
:host {
position: relative;
cursor: default;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-family: ${cssGeistFontFamily};
}
.maincontainer {

View File

@ -10,6 +10,7 @@ import {
import * as domtools from '@design.estate/dees-domtools';
import { demoFunc } from './dees-input-dropdown.demo.js';
import { DeesInputBase } from './dees-input-base.js';
import { cssGeistFontFamily } from './00fonts.js';
declare global {
interface HTMLElementTagNameMap {
@ -67,7 +68,7 @@ export class DeesInputDropdown extends DeesInputBase<DeesInputDropdown> {
}
:host {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-family: ${cssGeistFontFamily};
position: relative;
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 90%)')};
}

View File

@ -96,27 +96,27 @@ export class DeesInputRichtext extends DeesInputBase<string> {
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
}
.editor-container {
display: flex;
flex-direction: column;
min-height: ${cssManager.bdTheme('200px', '200px')};
border: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')};
border-radius: 8px;
background: ${cssManager.bdTheme('#ffffff', '#141414')};
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%)')};
overflow: hidden;
transition: all 0.2s ease;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.editor-container:hover {
border-color: ${cssManager.bdTheme('#d1d5db', '#404040')};
border-color: ${cssManager.bdTheme('hsl(0 0% 79.8%)', 'hsl(0 0% 20.9%)')};
}
.editor-container.focused {
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 80, 185, 0.1)', 'rgba(0, 105, 242, 0.1)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')};
}
.editor-toolbar {
@ -124,8 +124,8 @@ export class DeesInputRichtext extends DeesInputBase<string> {
flex-wrap: wrap;
gap: 4px;
padding: 8px 12px;
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
border-bottom: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')};
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
align-items: center;
position: relative;
}
@ -142,8 +142,8 @@ export class DeesInputRichtext extends DeesInputBase<string> {
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: ${cssManager.bdTheme('#374151', '#9ca3af')};
transition: all 0.2s;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
transition: all 0.15s ease;
user-select: none;
}
@ -153,13 +153,13 @@ export class DeesInputRichtext extends DeesInputBase<string> {
}
.toolbar-button:hover {
background: ${cssManager.bdTheme('#e5e7eb', '#2c2c2c')};
color: ${cssManager.bdTheme('#1f2937', '#e4e4e7')};
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
.toolbar-button.active {
background: ${cssManager.bdTheme('#0050b9', '#0069f2')};
color: white;
background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
}
.toolbar-button:disabled {
@ -170,7 +170,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
.toolbar-divider {
width: 1px;
height: 24px;
background: ${cssManager.bdTheme('#d1d5db', '#404040')};
background: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
margin: 0 4px;
}
@ -184,7 +184,7 @@ export class DeesInputRichtext extends DeesInputBase<string> {
.editor-content .ProseMirror {
outline: none;
line-height: 1.6;
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
min-height: 100%;
}
@ -232,25 +232,25 @@ export class DeesInputRichtext extends DeesInputBase<string> {
}
.editor-content .ProseMirror blockquote {
border-left: 4px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
border-left: 4px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
margin: 1em 0;
padding-left: 1em;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
font-style: italic;
}
.editor-content .ProseMirror code {
background: ${cssManager.bdTheme('#f3f4f6', '#2c2c2c')};
border-radius: 4px;
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-radius: 3px;
padding: 0.2em 0.4em;
font-family: 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-family: 'Intel One Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
font-size: 0.9em;
color: ${cssManager.bdTheme('#e11d48', '#f87171')};
color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
}
.editor-content .ProseMirror pre {
background: ${cssManager.bdTheme('#1f2937', '#0a0a0a')};
color: ${cssManager.bdTheme('#f9fafb', '#e4e4e7')};
background: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
color: ${cssManager.bdTheme('hsl(0 0% 98%)', 'hsl(0 0% 3.9%)')};
border-radius: 6px;
padding: 1em;
margin: 1em 0;
@ -265,21 +265,21 @@ export class DeesInputRichtext extends DeesInputBase<string> {
}
.editor-content .ProseMirror a {
color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
text-decoration: underline;
cursor: pointer;
}
.editor-content .ProseMirror a:hover {
color: ${cssManager.bdTheme('#0069f2', '#0084ff')};
color: ${cssManager.bdTheme('hsl(222.2 47.4% 41.2%)', 'hsl(217.2 91.2% 69.8%)')};
}
.editor-footer {
padding: 8px 12px;
background: ${cssManager.bdTheme('#f8f9fa', '#1a1a1a')};
border-top: 1px solid ${cssManager.bdTheme('#e1e5e9', '#2c2c2c')};
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
font-size: 12px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
display: flex;
justify-content: space-between;
align-items: center;
@ -295,8 +295,8 @@ export class DeesInputRichtext extends DeesInputBase<string> {
top: 100%;
left: 0;
right: 0;
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
padding: 12px;
@ -310,17 +310,18 @@ export class DeesInputRichtext extends DeesInputBase<string> {
.link-input input {
width: 100%;
padding: 8px 12px;
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
border-radius: 4px;
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
outline: none;
font-size: 14px;
background: ${cssManager.bdTheme('#ffffff', '#0a0a0a')};
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.link-input input:focus {
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('rgba(0, 80, 185, 0.1)', 'rgba(0, 105, 242, 0.1)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')};
}
.link-input-buttons {
@ -331,33 +332,36 @@ export class DeesInputRichtext extends DeesInputBase<string> {
.link-input-buttons button {
padding: 6px 12px;
border: 1px solid ${cssManager.bdTheme('#d1d5db', '#404040')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 4px;
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
cursor: pointer;
font-size: 12px;
color: ${cssManager.bdTheme('#374151', '#e4e4e7')};
transition: all 0.2s;
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
transition: all 0.15s ease;
font-weight: 500;
}
.link-input-buttons button:hover {
background: ${cssManager.bdTheme('#f3f4f6', '#2c2c2c')};
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
}
.link-input-buttons button.primary {
background: ${cssManager.bdTheme('#0050b9', '#0069f2')};
color: white;
border-color: ${cssManager.bdTheme('#0050b9', '#0069f2')};
background: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
color: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 3.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
}
.link-input-buttons button.primary:hover {
background: ${cssManager.bdTheme('#0069f2', '#0084ff')};
background: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 15%)', 'hsl(0 0% 93.9%)')};
}
.description {
margin-top: 8px;
font-size: 12px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
color: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
line-height: 1.4;
}

View File

@ -1,6 +1,7 @@
import * as colors from './00colors.js';
import { DeesInputBase } from './dees-input-base.js';
import { demoFunc } from './dees-input-text.demo.js';
import { cssGeistFontFamily, cssMonoFontFamily } from './00fonts.js';
import {
customElement,
@ -65,7 +66,7 @@ export class DeesInputText extends DeesInputBase {
:host {
position: relative;
z-index: auto;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
font-family: ${cssGeistFontFamily};
}
.maincontainer {
@ -80,18 +81,18 @@ export class DeesInputText extends DeesInputBase {
padding: 0 12px;
font-size: 14px;
line-height: 40px;
background: transparent;
background: ${cssManager.bdTheme('hsl(0 0% 100%)', 'hsl(0 0% 9%)')};
border: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
border-radius: 6px;
transition: all 0.15s ease;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
outline: none;
cursor: text;
font-family: inherit;
color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 95%)')};
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
}
input::placeholder {
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
}
input:hover:not(:disabled):not(:focus) {
@ -100,14 +101,14 @@ export class DeesInputText extends DeesInputBase {
input:focus {
outline: none;
border-color: ${cssManager.bdTheme('hsl(222.2 47.4% 51.2%)', 'hsl(217.2 91.2% 59.8%)')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(222.2 47.4% 51.2% / 0.1)', 'hsl(217.2 91.2% 59.8% / 0.1)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 9%)', 'hsl(0 0% 98%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 0% 9% / 0.05)', 'hsl(0 0% 98% / 0.05)')};
}
input:disabled {
background: ${cssManager.bdTheme('hsl(0 0% 95.1%)', 'hsl(0 0% 14.9%)')};
border-color: ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
color: ${cssManager.bdTheme('hsl(0 0% 63.9%)', 'hsl(0 0% 45.1%)')};
color: ${cssManager.bdTheme('hsl(0 0% 45.1%)', 'hsl(0 0% 63.9%)')};
cursor: not-allowed;
opacity: 0.5;
}
@ -166,7 +167,8 @@ export class DeesInputText extends DeesInputBase {
}
:host([validation-state="invalid"]) input:focus {
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.1)', 'hsl(0 72.2% 50.6% / 0.1)')};
border-color: ${cssManager.bdTheme('hsl(0 84.2% 60.2%)', 'hsl(0 72.2% 50.6%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(0 84.2% 60.2% / 0.05)', 'hsl(0 72.2% 50.6% / 0.05)')};
}
/* Warning state for input */
@ -175,7 +177,8 @@ export class DeesInputText extends DeesInputBase {
}
:host([validation-state="warn"]) input:focus {
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(25 95% 53% / 0.1)', 'hsl(25 95% 63% / 0.1)')};
border-color: ${cssManager.bdTheme('hsl(25 95% 53%)', 'hsl(25 95% 63%)')};
box-shadow: 0 0 0 2px ${cssManager.bdTheme('hsl(25 95% 53% / 0.05)', 'hsl(25 95% 63% / 0.05)')};
}
/* Valid state for input */
@ -184,7 +187,8 @@ export class DeesInputText extends DeesInputBase {
}
:host([validation-state="valid"]) input:focus {
box-shadow: 0 0 0 3px ${cssManager.bdTheme('hsl(142.1 76.2% 36.3% / 0.1)', 'hsl(142.1 70.6% 45.3% / 0.1)')};
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)')};
}
`,
];
@ -193,7 +197,7 @@ export class DeesInputText extends DeesInputBase {
return html`
<style>
input {
font-family: ${this.isPasswordBool ? 'SF Mono, Monaco, Consolas, Liberation Mono, Courier New, monospace' : 'inherit'};
font-family: ${this.isPasswordBool ? cssMonoFontFamily : 'inherit'};
letter-spacing: ${this.isPasswordBool ? '0.5px' : 'normal'};
padding-right: ${this.isPasswordBool ? '48px' : '12px'};
}

View File

@ -1,6 +1,7 @@
import * as colors from './00colors.js';
import * as plugins from './00plugins.js';
import { zIndexLayers, zIndexRegistry } from './00zindex.js';
import { cssGeistFontFamily } from './00fonts.js';
import { demoFunc } from './dees-modal.demo.js';
import {
@ -117,7 +118,7 @@ export class DeesModal extends DeesElement {
cssManager.defaultStyles,
css`
:host {
font-family: 'Geist Sans', sans-serif;
font-family: ${cssGeistFontFamily};
color: ${cssManager.bdTheme('#333', '#fff')};
will-change: transform;
}
@ -208,7 +209,7 @@ export class DeesModal extends DeesElement {
.modal .heading {
height: 40px;
min-height: 40px;
font-family: 'Geist Sans', sans-serif;
font-family: ${cssGeistFontFamily};
display: flex;
align-items: center;
justify-content: space-between;

View File

@ -1,5 +1,6 @@
import { demoFunc } from './dees-statsgrid.demo.js';
import * as plugins from './00plugins.js';
import { cssGeistFontFamily } from './00fonts.js';
import {
customElement,
html,
@ -79,6 +80,7 @@ export class DeesStatsGrid extends DeesElement {
:host {
display: block;
width: 100%;
font-family: ${cssGeistFontFamily};
}
/* CSS Variables for consistent spacing and sizing */
@ -194,7 +196,7 @@ export class DeesStatsGrid extends DeesElement {
font-size: var(--value-font-size);
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1;
line-height: 1.1;
display: flex;
align-items: baseline;
gap: 4px;
@ -251,10 +253,10 @@ export class DeesStatsGrid extends DeesElement {
.gauge-text {
fill: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
font-family: ${cssGeistFontFamily};
font-size: var(--value-font-size);
font-weight: 600;
text-anchor: middle;
dominant-baseline: alphabetic;
letter-spacing: -0.025em;
}
@ -262,6 +264,7 @@ export class DeesStatsGrid extends DeesElement {
font-size: var(--unit-font-size);
fill: ${cssManager.bdTheme('hsl(215.4 16.3% 46.9%)', 'hsl(215 20.2% 65.1%)')};
font-weight: 400;
font-family: ${cssGeistFontFamily};
}
/* Percentage Styles */
@ -274,6 +277,7 @@ export class DeesStatsGrid extends DeesElement {
font-size: var(--value-font-size);
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1.1;
letter-spacing: -0.025em;
margin-bottom: 8px;
}
@ -311,6 +315,7 @@ export class DeesStatsGrid extends DeesElement {
font-size: var(--value-font-size);
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1.1;
letter-spacing: -0.025em;
}
@ -358,6 +363,7 @@ export class DeesStatsGrid extends DeesElement {
font-size: var(--value-font-size);
font-weight: 600;
color: ${cssManager.bdTheme('hsl(215.3 25% 8.8%)', 'hsl(210 40% 98%)')};
line-height: 1.1;
letter-spacing: -0.025em;
}
@ -401,7 +407,7 @@ export class DeesStatsGrid extends DeesElement {
<dees-contextmenu
.x=${this.contextMenuPosition.x}
.y=${this.contextMenuPosition.y}
.menuItems=${this.contextMenuActions}
.menuItems=${this.contextMenuActions as any}
@clicked=${() => this.contextMenuVisible = false}
></dees-contextmenu>
` : ''}
@ -521,8 +527,8 @@ export class DeesStatsGrid extends DeesElement {
stroke-dashoffset="${strokeDashoffset}"
/>
<!-- Value text -->
<text class="gauge-text" x="${centerX}" y="${centerY}">
<tspan>${value}</tspan>${tile.unit ? html`<tspan class="gauge-unit" dx="4">${tile.unit}</tspan>` : ''}
<text class="gauge-text" x="${centerX}" y="${centerY - 8}" dominant-baseline="middle">
<tspan>${value}</tspan>${tile.unit ? html`<tspan class="gauge-unit" dx="2" dy="0">${tile.unit}</tspan>` : ''}
</text>
</svg>
</div>

View File

@ -1,6 +1,6 @@
import { type ITableAction } from './dees-table.js';
import * as plugins from './00plugins.js';
import { html } from '@design.estate/dees-element';
import { html, css, cssManager } from '@design.estate/dees-element';
interface ITableDemoData {
date: string;
@ -10,120 +10,423 @@ interface ITableDemoData {
export const demoFunc = () => html`
<style>
.demoWrapper {
box-sizing: border-box;
position: absolute;
width: 100%;
height: 100%;
padding: 20px;
background: #000000;
}
${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-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<ITableDemoData>)[] as any}"
.displayFunction=${(itemArg) => {
return {
...itemArg,
onlyDisplayProp: 'onlyDisplay',
};
}}
>This is a slotted Text</dees-table
>
<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>
</div>
`;
`;

View File

@ -1,6 +1,6 @@
import * as colors from './00colors.js';
import * as plugins from './00plugins.js';
import { demoFunc } from './dees-table.demo.js';
import { cssGeistFontFamily } from './00fonts.js';
import {
customElement,
html,
@ -9,9 +9,6 @@ import {
type TemplateResult,
cssManager,
css,
unsafeCSS,
type CSSResult,
state,
directives,
} from '@design.estate/dees-element';
@ -113,7 +110,7 @@ export class DeesTable<T> extends DeesElement {
get value() {
return this.data;
}
set value(valueArg) {}
set value(_valueArg) {}
public changeSubject = new domtools.plugins.smartrx.rxjs.Subject<DeesTable<T>>();
// end dees-form compatibility -----------------------------------------
@ -157,6 +154,27 @@ export class DeesTable<T> extends DeesElement {
})
public editableFields: string[] = [];
@property({
type: Boolean,
reflect: true,
attribute: 'show-vertical-lines'
})
public showVerticalLines: boolean = false;
@property({
type: Boolean,
reflect: true,
attribute: 'show-horizontal-lines'
})
public showHorizontalLines: boolean = false;
@property({
type: Boolean,
reflect: true,
attribute: 'show-grid'
})
public showGrid: boolean = true;
public files: File[] = [];
public fileWeakMap = new WeakMap();
@ -169,238 +187,358 @@ export class DeesTable<T> extends DeesElement {
public static styles = [
cssManager.defaultStyles,
css`
.mainbox {
color: ${cssManager.bdTheme('#333', '#fff')};
font-family: 'Geist Sans', sans-serif;
font-weight: 400;
font-size: 14px;
padding: 16px;
:host {
display: block;
width: 100%;
min-height: 50px;
background: ${cssManager.bdTheme('#ffffff', '#222222')};
border-radius: 3px;
border-top: 1px solid ${cssManager.bdTheme('#fff', '#ffffff10')};
box-shadow: 0px 1px 4px rgba(0, 0, 0, 0.3);
overflow-x: auto;
}
.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: flex-end;
justify-content: space-between;
align-items: center;
font-family: 'Geist Sans', sans-serif;
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 {
opacity: 0.6;
font-size: 14px;
color: ${cssManager.bdTheme('hsl(215.4 16.3% 56.9%)', 'hsl(215 20.2% 55.1%)')};
margin-top: 2px;
}
.headingSeparation {
margin-top: 7px;
border-bottom: 1px solid ${cssManager.bdTheme('#bcbcbc', '#444444')};
display: none;
}
.headerActions {
user-select: none;
display: flex;
flex-direction: row;
margin-left: auto;
gap: 8px;
}
.headerAction {
display: flex;
flex-direction: row;
color: ${cssManager.bdTheme('#333', '#ccc')};
margin-left: 16px;
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('#555', '#fff')};
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 {
margin-right: 8px;
width: 14px;
height: 14px;
}
.searchGrid {
background: ${cssManager.bdTheme('#fff', '#111111')};
display: grid;
grid-gap: 16px;
grid-template-columns: 1fr 200px;
margin-top: 16px;
padding: 0px 16px;
border-top: 1px solid ${cssManager.bdTheme('#fff', '#ffffff20')};
border-radius: 8px;
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;
}
.searchGrid.hidden {
height: 0px;
opacity: 0;
overflow: hidden;
margin-top: 0px;
padding: 0px 24px;
border-bottom-width: 0px;
}
table,
.noDataSet {
margin-top: 16px;
color: ${cssManager.bdTheme('#333', '#fff')};
border-collapse: collapse;
table {
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%)')};
}
tr {
border-bottom: 1px dashed ${cssManager.bdTheme('#999', '#808080')};
text-align: left;
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%)')};
}
tr:last-child {
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;
text-align: left;
}
tr:hover {
/* 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%)')};
}
tr:hover td {
background: ${cssManager.bdTheme('#22222210', '#ffffff10')};
:host([show-horizontal-lines]) tbody tr:first-child {
border-top: none;
}
tr:first-child:hover {
cursor: auto;
:host([show-horizontal-lines]) tbody tr:last-child {
border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
}
tr:first-child:hover .innerCellContainer {
background: none;
tbody tr:hover {
background: ${cssManager.bdTheme('hsl(210 40% 96.1% / 0.5)', 'hsl(0 0% 14.9% / 0.5)')};
}
tr.selected td {
background: ${cssManager.bdTheme('#22222220', '#ffffff20')};
/* Column hover effect for better traceability */
td {
position: relative;
}
td::after {
content: '';
position: absolute;
top: -1000px;
bottom: -1000px;
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;
}
tbody tr.selected {
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 14.9%)')};
}
tr.hasAttachment td {
background: ${cssManager.bdTheme('#0098847c', '#0098847c')};
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 {
text-transform: none;
font-family: 'Geist Sans', sans-serif;
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;
}
th,
:host([show-vertical-lines]) th {
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
}
td {
position: relative;
vertical-align: top;
padding: 0px;
border-right: 1px dashed ${cssManager.bdTheme('#999', '#808080')};
padding: 12px 24px;
vertical-align: middle;
color: ${cssManager.bdTheme('hsl(0 0% 3.9%)', 'hsl(0 0% 98%)')};
}
.innerCellContainer {
min-height: 36px;
position: relative;
height: 100%;
width: 100%;
padding: 6px 8px;
line-height: 24px;
:host([show-vertical-lines]) td {
border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 89.8%)', 'hsl(0 0% 14.9%)')};
}
th:first-child .innerCellContainer,
td:first-child .innerCellContainer {
padding-left: 0px;
}
th:last-child .innerCellContainer,
td:last-child .innerCellContainer {
padding-right: 0px;
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 {
width: 100%;
height: 100%;
outline: none;
border: 2px solid #fa6101;
top: 0px;
bottom: 0px;
right: 0px;
left: 0px;
position: absolute;
background: #fa610140;
color: ${cssManager.bdTheme('#333', '#fff')};
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;
padding: 0px 6px;
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)')};
}
.actionsContainer {
display: flex;
flex-direction: row;
height: 24px;
transform: translateY(-4px);
margin-left: -6px;
gap: 4px;
}
.action {
position: relative;
padding: 8px 10px;
line-height: 24px;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
size: 16px;
border-radius: 8px;
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(colors.bright.blue, colors.dark.blue)};
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(colors.bright.blue, colors.dark.blueActive)};
background: ${cssManager.bdTheme('hsl(210 40% 96.1%)', 'hsl(0 0% 11.8%)')};
}
.action:hover dees-icon {
filter: ${cssManager.bdTheme('invert(1) brightness(3)', '')};
.action dees-icon {
width: 16px;
height: 16px;
}
.footer {
font-family: 'Geist Sans', sans-serif;
font-size: 14px;
color: ${cssManager.bdTheme('#111', '#ffffff90')};
background: ${cssManager.bdTheme('#eeeeeb', '#00000050')};
margin: 16px -16px -16px -16px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
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 {
padding: 8px 16px;
font-weight: 500;
}
.footerActions {
margin-left: auto;
display: flex;
gap: 8px;
}
.footerActions .footerAction {
padding: 8px 16px;
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(colors.bright.blue, colors.dark.blue)};
color: #fff;
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 {
display: flex;
margin-right: 8px;
}
.footerActions .footerAction:hover dees-icon {
width: 14px;
height: 14px;
}
`,
];
@ -478,24 +616,23 @@ export class DeesTable<T> extends DeesElement {
const headings: string[] = Object.keys(firstTransformedItem);
return html`
<table>
<tr>
${headings.map(
(headingArg) => html`
<th>
<div class="innerCellContainer">${headingArg}</div>
</th>
`
)}
${(() => {
if (this.dataActions && this.dataActions.length > 0) {
return html`
<th>
<div class="innerCellContainer">Actions</div>
</th>
`;
}
})()}
</tr>
<thead>
<tr>
${headings.map(
(headingArg) => html`
<th>${headingArg}</th>
`
)}
${(() => {
if (this.dataActions && this.dataActions.length > 0) {
return html`
<th>Actions</th>
`;
}
})()}
</tr>
</thead>
<tbody>
${this.data.map((itemArg) => {
const transformedItem = this.displayFunction(itemArg);
const getTr = (elementArg: HTMLElement): HTMLElement => {
@ -592,10 +729,9 @@ export class DeesTable<T> extends DeesElement {
if (this.dataActions && this.dataActions.length > 0) {
return html`
<td>
<div class="innerCellContainer">
<div class="actionsContainer">
${this.getActionsForType('inRow').map(
(actionArg) => html`
<div class="actionsContainer">
${this.getActionsForType('inRow').map(
(actionArg) => html`
<div
class="action"
@click=${() =>
@ -614,7 +750,6 @@ export class DeesTable<T> extends DeesElement {
</div>
`
)}
</div>
</div>
</td>
`;
@ -623,6 +758,7 @@ export class DeesTable<T> extends DeesElement {
</tr>
`;
})}
</tbody>
</table>
`;
})()
@ -743,7 +879,7 @@ export class DeesTable<T> extends DeesElement {
}
async handleCellEditing(event: Event, itemArg: T, key: string) {
const domtools = await this.domtoolsPromise;
await this.domtoolsPromise;
const target = event.target as HTMLElement;
const originalColor = target.style.color;
target.style.color = 'transparent';

View File

@ -3,6 +3,7 @@ import { customElement, DeesElement, type TemplateResult, html, css, property, c
import * as domtools from '@design.estate/dees-domtools';
import { zIndexLayers } from './00zindex.js';
import { demoFunc } from './dees-toast.demo.js';
import { cssGeistFontFamily } from './00fonts.js';
declare global {
interface HTMLElementTagNameMap {
@ -152,7 +153,7 @@ export class DeesToast extends DeesElement {
:host {
display: block;
pointer-events: auto;
font-family: 'Geist Sans', sans-serif;
font-family: ${cssGeistFontFamily};
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);

View File

@ -3,6 +3,8 @@ import type { IBlock } from '../../wysiwyg.types.js';
import { cssManager } from '@design.estate/dees-element';
import { WysiwygSelection } from '../../wysiwyg.selection.js';
import hlight from 'highlight.js';
import { cssGeistFontFamily, cssMonoFontFamily } from '../../../00fonts.js';
import { PROGRAMMING_LANGUAGES } from '../../wysiwyg.constants.js';
/**
* CodeBlockHandler with improved architecture
@ -20,7 +22,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
private highlightTimer: any = null;
render(block: IBlock, isSelected: boolean): string {
const language = block.metadata?.language || 'javascript';
const language = block.metadata?.language || 'typescript';
const content = block.content || '';
const lineCount = content.split('\n').length;
@ -30,10 +32,18 @@ export class CodeBlockHandler extends BaseBlockHandler {
lineNumbersHtml += `<div class="line-number">${i}</div>`;
}
// Generate language options
const languageOptions = PROGRAMMING_LANGUAGES.map(lang => {
const value = lang.toLowerCase();
return `<option value="${value}" ${value === language ? 'selected' : ''}>${lang}</option>`;
}).join('');
return `
<div class="code-block-container${isSelected ? ' selected' : ''}" data-language="${language}">
<div class="code-header">
<span class="language-label">${language}</span>
<select class="language-selector" data-block-id="${block.id}">
${languageOptions}
</select>
<button class="copy-button" title="Copy code">
<svg class="copy-icon" width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 010 1.5h-1.5a.25.25 0 00-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 00.25-.25v-1.5a.75.75 0 011.5 0v1.5A1.75 1.75 0 019.25 16h-7.5A1.75 1.75 0 010 14.25v-7.5z"></path>
@ -60,9 +70,29 @@ export class CodeBlockHandler extends BaseBlockHandler {
const editor = element.querySelector('.code-editor') as HTMLElement;
const container = element.querySelector('.code-block-container') as HTMLElement;
const copyButton = element.querySelector('.copy-button') as HTMLButtonElement;
const languageSelector = element.querySelector('.language-selector') as HTMLSelectElement;
if (!editor || !container) return;
// Setup language selector
if (languageSelector) {
languageSelector.addEventListener('change', (e) => {
const newLanguage = (e.target as HTMLSelectElement).value;
block.metadata = { ...block.metadata, language: newLanguage };
container.setAttribute('data-language', newLanguage);
// Update the syntax highlighting if content exists and not focused
if (block.content && document.activeElement !== editor) {
this.applyHighlighting(element, block);
}
// Notify about the change
if (handlers.onInput) {
handlers.onInput(new InputEvent('input'));
}
});
}
// Setup copy button
if (copyButton) {
copyButton.addEventListener('click', async () => {
@ -285,7 +315,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
// Get plain text content
const content = editor.textContent || '';
const language = block.metadata?.language || 'javascript';
const language = block.metadata?.language || 'typescript';
// Apply highlighting
try {
@ -336,7 +366,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
type: 'code',
content: content,
metadata: {
language: element.querySelector('.code-block-container')?.getAttribute('data-language') || 'javascript'
language: element.querySelector('.code-block-container')?.getAttribute('data-language') || 'typescript'
}
};
this.applyHighlighting(element, block);
@ -440,13 +470,30 @@ export class CodeBlockHandler extends BaseBlockHandler {
align-items: center;
}
.language-label {
.language-selector {
font-size: 12px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-family: ${cssGeistFontFamily};
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
padding: 4px 8px;
cursor: pointer;
transition: all 0.15s ease;
outline: none;
}
.language-selector:hover {
background: ${cssManager.bdTheme('#f9fafb', '#1f2937')};
border-color: ${cssManager.bdTheme('#e5e7eb', '#374151')};
color: ${cssManager.bdTheme('#374151', '#e5e7eb')};
}
.language-selector:focus {
border-color: ${cssManager.bdTheme('#9ca3af', '#6b7280')};
}
/* Copy Button - Minimal */
@ -460,7 +507,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
border-radius: 4px;
color: ${cssManager.bdTheme('#6b7280', '#9ca3af')};
font-size: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
font-family: ${cssGeistFontFamily};
cursor: pointer;
transition: all 0.15s ease;
outline: none;
@ -515,7 +562,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
.line-number {
padding: 0 12px 0 8px;
color: ${cssManager.bdTheme('#9ca3af', '#4b5563')};
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-family: ${cssMonoFontFamily};
font-size: 13px;
line-height: 20px;
height: 20px;
@ -538,7 +585,7 @@ export class CodeBlockHandler extends BaseBlockHandler {
display: block;
padding: 12px 16px;
margin: 0;
font-family: 'SF Mono', Monaco, Consolas, monospace;
font-family: ${cssMonoFontFamily};
font-size: 13px;
line-height: 20px;
color: ${cssManager.bdTheme('#111827', '#f9fafb')};

View File

@ -235,6 +235,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
const blockComponent = document.createElement('dees-wysiwyg-block') as any;
blockComponent.block = block;
blockComponent.isSelected = this.selectedBlockId === block.id;
blockComponent.wysiwygComponent = this; // Pass parent reference
blockComponent.handlers = {
onInput: (e: InputEvent) => this.inputHandler.handleBlockInput(e, block),
onKeyDown: (e: KeyboardEvent) => this.keyboardHandler.handleBlockKeyDown(e, block),
@ -247,28 +248,7 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
};
wrapper.appendChild(blockComponent);
// Add settings button for non-divider blocks
if (block.type !== 'divider') {
const settings = document.createElement('div');
settings.className = 'block-settings';
settings.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor">
<circle cx="12" cy="5" r="2"></circle>
<circle cx="12" cy="12" r="2"></circle>
<circle cx="12" cy="19" r="2"></circle>
</svg>
`;
settings.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
WysiwygModalManager.showBlockSettingsModal(block, () => {
this.updateValue();
// Re-render only the updated block
this.updateBlockElement(block.id);
});
});
wrapper.appendChild(settings);
}
// Remove settings button - context menu will handle this
// Add drag event listeners
wrapper.addEventListener('dragover', (e) => this.dragDropHandler.handleDragOver(e, block));
@ -507,13 +487,9 @@ export class DeesInputWysiwyg extends DeesInputBase<string> {
// Close menu
this.closeSlashMenu(false);
// If it's a code block, ask for language
// If it's a code block, default to TypeScript
if (type === 'code') {
const language = await WysiwygModalManager.showLanguageSelectionModal();
if (!language) {
return; // User cancelled
}
currentBlock.metadata = { language };
currentBlock.metadata = { language: 'typescript' };
}
// Transform the current block

View File

@ -8,6 +8,7 @@ import {
state,
} from '@design.estate/dees-element';
import { zIndexRegistry } from '../00zindex.js';
import '../dees-icon.js';
import { type ISlashMenuItem } from './wysiwyg.types.js';
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
@ -61,10 +62,10 @@ export class DeesSlashMenu extends DeesElement {
.slash-menu {
position: fixed;
background: ${cssManager.bdTheme('#ffffff', '#262626')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')};
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
padding: 4px;
min-width: 220px;
max-height: 300px;
@ -77,7 +78,7 @@ export class DeesSlashMenu extends DeesElement {
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.95) translateY(-10px);
transform: scale(0.98) translateY(-2px);
}
to {
opacity: 1;
@ -86,37 +87,35 @@ export class DeesSlashMenu extends DeesElement {
}
.slash-menu-item {
padding: 10px 12px;
padding: 8px 10px;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
gap: 12px;
border-radius: 4px;
color: ${cssManager.bdTheme('#000000', '#e0e0e0')};
border-radius: 3px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
font-size: 14px;
}
.slash-menu-item:hover,
.slash-menu-item.selected {
background: ${cssManager.bdTheme('#f0f0f0', '#333333')};
color: ${cssManager.bdTheme('#000000', '#ffffff')};
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.slash-menu-item .icon {
width: 24px;
height: 24px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
color: ${cssManager.bdTheme('#666', '#999')};
font-weight: 600;
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
}
.slash-menu-item:hover .icon,
.slash-menu-item.selected .icon {
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
}
`,
];
@ -142,7 +141,7 @@ export class DeesSlashMenu extends DeesElement {
data-item-type="${item.type}"
data-item-index="${index}"
>
<span class="icon">${item.icon}</span>
<dees-icon class="icon" .icon="${item.icon}" iconSize="16"></dees-icon>
<span>${item.label}</span>
</div>
`)}

View File

@ -13,6 +13,8 @@ import { WysiwygBlocks } from './wysiwyg.blocks.js';
import { WysiwygSelection } from './wysiwyg.selection.js';
import { BlockRegistry, type IBlockEventHandlers } from './blocks/index.js';
import './wysiwyg.blockregistration.js';
import { WysiwygShortcuts } from './wysiwyg.shortcuts.js';
import '../dees-contextmenu.js';
declare global {
interface HTMLElementTagNameMap {
@ -37,6 +39,9 @@ export class DeesWysiwygBlock extends DeesElement {
@property({ type: Object })
public handlers: IBlockEventHandlers;
@property({ type: Object })
public wysiwygComponent: any; // Reference to parent dees-input-wysiwyg
// Reference to the editable block element
private blockElement: HTMLDivElement | null = null;
@ -685,6 +690,125 @@ export class DeesWysiwygBlock extends DeesElement {
/**
* Get context menu items for this block
*/
public getContextMenuItems(): any[] {
if (!this.block || this.block.type === 'divider') {
return [];
}
const blockTypes = WysiwygShortcuts.getSlashMenuItems();
const currentType = this.block.type;
// Use the parent reference passed from dees-input-wysiwyg
const wysiwygComponent = this.wysiwygComponent;
const blockId = this.block.id;
// Create submenu items for block type change
const blockTypeItems = blockTypes
.filter(item => item.type !== currentType && item.type !== 'divider')
.map(item => ({
name: item.label,
iconName: item.icon.replace('lucide:', ''),
action: async () => {
if (wysiwygComponent && wysiwygComponent.blockOperations) {
// Transform the block type
const blockToTransform = wysiwygComponent.blocks.find((b: IBlock) => b.id === blockId);
if (blockToTransform) {
blockToTransform.type = item.type;
blockToTransform.content = blockToTransform.content || '';
// Handle special metadata for different block types
if (item.type === 'code') {
blockToTransform.metadata = { language: 'typescript' };
} else if (item.type === 'list') {
blockToTransform.metadata = { listType: 'bullet' };
} else if (item.type === 'image') {
blockToTransform.content = '';
blockToTransform.metadata = { url: '', loading: false };
} else if (item.type === 'youtube') {
blockToTransform.content = '';
blockToTransform.metadata = { videoId: '', url: '' };
} else if (item.type === 'markdown') {
blockToTransform.metadata = { showPreview: false };
} else if (item.type === 'html') {
blockToTransform.metadata = { showPreview: false };
} else if (item.type === 'attachment') {
blockToTransform.content = '';
blockToTransform.metadata = { files: [] };
}
// Update the block element
wysiwygComponent.updateBlockElement(blockId);
wysiwygComponent.updateValue();
// Focus the block after transformation
requestAnimationFrame(() => {
wysiwygComponent.blockOperations.focusBlock(blockId);
});
}
}
}
}));
const menuItems: any[] = [
{
name: 'Change Type',
iconName: 'type',
submenu: blockTypeItems
}
];
// Add copy/cut/paste for editable blocks
if (!['image', 'divider', 'youtube', 'attachment'].includes(this.block.type)) {
menuItems.push(
{ divider: true },
{
name: 'Cut',
iconName: 'scissors',
shortcut: 'Cmd+X',
action: async () => {
document.execCommand('cut');
}
},
{
name: 'Copy',
iconName: 'copy',
shortcut: 'Cmd+C',
action: async () => {
document.execCommand('copy');
}
},
{
name: 'Paste',
iconName: 'clipboard',
shortcut: 'Cmd+V',
action: async () => {
document.execCommand('paste');
}
}
);
}
// Add delete option
menuItems.push(
{ divider: true },
{
name: 'Delete Block',
iconName: 'trash2',
action: async () => {
if (wysiwygComponent && wysiwygComponent.blockOperations) {
wysiwygComponent.blockOperations.deleteBlock(blockId);
}
}
}
);
return menuItems;
}
/**
* Gets content split at cursor position
*/

View File

@ -49,19 +49,19 @@ export class WysiwygShortcuts {
static getSlashMenuItems(): ISlashMenuItem[] {
return [
{ type: 'paragraph', label: 'Paragraph', icon: '' },
{ type: 'heading-1', label: 'Heading 1', icon: 'H₁' },
{ type: 'heading-2', label: 'Heading 2', icon: 'H₂' },
{ type: 'heading-3', label: 'Heading 3', icon: 'H₃' },
{ type: 'quote', label: 'Quote', icon: '"' },
{ type: 'code', label: 'Code', icon: '<>' },
{ type: 'list', label: 'List', icon: '' },
{ type: 'image', label: 'Image', icon: '🖼' },
{ type: 'divider', label: 'Divider', icon: '' },
{ type: 'youtube', label: 'YouTube', icon: '▶️' },
{ type: 'markdown', label: 'Markdown', icon: 'M↓' },
{ type: 'html', label: 'HTML', icon: '</>' },
{ type: 'attachment', label: 'File Attachment', icon: '📎' },
{ type: 'paragraph', label: 'Paragraph', icon: 'lucide:pilcrow' },
{ type: 'heading-1', label: 'Heading 1', icon: 'lucide:heading1' },
{ type: 'heading-2', label: 'Heading 2', icon: 'lucide:heading2' },
{ type: 'heading-3', label: 'Heading 3', icon: 'lucide:heading3' },
{ type: 'quote', label: 'Quote', icon: 'lucide:quote' },
{ type: 'code', label: 'Code Block', icon: 'lucide:fileCode' },
{ type: 'list', label: 'Bullet List', icon: 'lucide:list' },
{ type: 'image', label: 'Image', icon: 'lucide:image' },
{ type: 'divider', label: 'Divider', icon: 'lucide:minus' },
{ type: 'youtube', label: 'YouTube', icon: 'lucide:youtube' },
{ type: 'markdown', label: 'Markdown', icon: 'lucide:fileText' },
{ type: 'html', label: 'HTML', icon: 'lucide:code' },
{ type: 'attachment', label: 'File Attachment', icon: 'lucide:paperclip' },
];
}

View File

@ -7,23 +7,25 @@ export const wysiwygStyles = css`
}
.wysiwyg-container {
background: ${cssManager.bdTheme('#ffffff', '#1a1a1a')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
border-radius: 8px;
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 6px;
min-height: 200px;
padding: 32px 40px;
padding: 24px;
position: relative;
transition: all 0.2s ease;
color: ${cssManager.bdTheme('#000000', '#ffffff')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.wysiwyg-container:hover {
border-color: ${cssManager.bdTheme('#d0d0d0', '#444')};
border-color: ${cssManager.bdTheme('#d1d5db', '#3f3f46')};
}
.wysiwyg-container:focus-within {
border-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(0, 102, 204, 0.1)', 'rgba(77, 148, 255, 0.1)')};
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow: 0 0 0 2px ${cssManager.bdTheme('#f4f4f5', '#18181b')}, 0 0 0 4px ${cssManager.bdTheme('rgba(59, 130, 246, 0.5)', 'rgba(59, 130, 246, 0.5)')};
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
}
/* Visual hint for text selection */
@ -44,7 +46,7 @@ export const wysiwygStyles = css`
position: relative;
transition: all 0.15s ease;
min-height: 1.6em;
color: ${cssManager.bdTheme('#000000', '#e0e0e0')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
/* First and last blocks don't need extra spacing */
@ -57,8 +59,9 @@ export const wysiwygStyles = css`
}
.block.selected {
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.05)', 'rgba(77, 148, 255, 0.08)')};
box-shadow: inset 0 0 0 2px ${cssManager.bdTheme('rgba(0, 102, 204, 0.2)', 'rgba(77, 148, 255, 0.2)')};
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(59, 130, 246, 0.05)')};
outline: 2px solid ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(59, 130, 246, 0.2)')};
outline-offset: -2px;
border-radius: 4px;
margin-left: -8px;
margin-right: -8px;
@ -78,7 +81,7 @@ export const wysiwygStyles = css`
.block.paragraph:empty::before {
content: "Type '/' for commands...";
color: ${cssManager.bdTheme('#999', '#666')};
color: ${cssManager.bdTheme('#71717a', '#71717a')};
pointer-events: none;
font-size: 16px;
line-height: 1.6;
@ -89,12 +92,12 @@ export const wysiwygStyles = css`
font-size: 32px;
font-weight: 700;
line-height: 1.2;
color: ${cssManager.bdTheme('#000000', '#ffffff')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.block.heading-1:empty::before {
content: "Heading 1";
color: ${cssManager.bdTheme('#999', '#666')};
color: ${cssManager.bdTheme('#71717a', '#71717a')};
pointer-events: none;
font-size: 32px;
line-height: 1.2;
@ -105,12 +108,12 @@ export const wysiwygStyles = css`
font-size: 24px;
font-weight: 600;
line-height: 1.3;
color: ${cssManager.bdTheme('#000000', '#ffffff')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.block.heading-2:empty::before {
content: "Heading 2";
color: ${cssManager.bdTheme('#999', '#666')};
color: ${cssManager.bdTheme('#71717a', '#71717a')};
pointer-events: none;
font-size: 24px;
line-height: 1.3;
@ -121,12 +124,12 @@ export const wysiwygStyles = css`
font-size: 20px;
font-weight: 600;
line-height: 1.4;
color: ${cssManager.bdTheme('#000000', '#ffffff')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.block.heading-3:empty::before {
content: "Heading 3";
color: ${cssManager.bdTheme('#999', '#666')};
color: ${cssManager.bdTheme('#71717a', '#71717a')};
pointer-events: none;
font-size: 20px;
line-height: 1.4;
@ -134,10 +137,10 @@ export const wysiwygStyles = css`
}
.block.quote {
border-left: 3px solid ${cssManager.bdTheme('#0066cc', '#4d94ff')};
border-left: 2px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
padding-left: 20px;
font-style: italic;
color: ${cssManager.bdTheme('#555', '#b0b0b0')};
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
margin-left: 0;
margin-right: 0;
line-height: 1.6;
@ -145,7 +148,7 @@ export const wysiwygStyles = css`
.block.quote:empty::before {
content: "Quote";
color: ${cssManager.bdTheme('#999', '#666')};
color: ${cssManager.bdTheme('#71717a', '#71717a')};
pointer-events: none;
font-size: 16px;
line-height: 1.6;
@ -162,33 +165,33 @@ export const wysiwygStyles = css`
position: absolute;
top: 0;
right: 0;
background: ${cssManager.bdTheme('#e1e4e8', '#333333')};
color: ${cssManager.bdTheme('#586069', '#8b949e')};
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
padding: 4px 12px;
font-size: 12px;
border-radius: 0 6px 0 6px;
border-radius: 0 4px 0 4px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
text-transform: lowercase;
z-index: 1;
}
.block.code {
background: ${cssManager.bdTheme('#f8f8f8', '#0d0d0d')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#2a2a2a')};
border-radius: 6px;
padding: 16px 20px;
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 4px;
padding: 16px;
padding-top: 32px; /* Make room for language indicator */
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
font-size: 14px;
line-height: 1.5;
white-space: pre-wrap;
color: ${cssManager.bdTheme('#24292e', '#e1e4e8')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
overflow-x: auto;
}
.block.code:empty::before {
content: "// Code block";
color: ${cssManager.bdTheme('#999', '#666')};
color: ${cssManager.bdTheme('#71717a', '#71717a')};
pointer-events: none;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
font-size: 14px;
@ -233,16 +236,16 @@ export const wysiwygStyles = css`
.block.divider hr {
border: none;
border-top: 1px solid ${cssManager.bdTheme('#e0e0e0', '#333')};
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
margin: 0;
}
.slash-menu {
position: absolute;
background: ${cssManager.bdTheme('#ffffff', '#262626')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')};
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
padding: 4px;
z-index: 1000;
min-width: 220px;
@ -253,21 +256,21 @@ export const wysiwygStyles = css`
}
.slash-menu-item {
padding: 10px 12px;
padding: 8px 10px;
cursor: pointer;
transition: all 0.15s ease;
display: flex;
align-items: center;
gap: 12px;
border-radius: 4px;
color: ${cssManager.bdTheme('#000000', '#e0e0e0')};
border-radius: 3px;
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
font-size: 14px;
}
.slash-menu-item:hover,
.slash-menu-item.selected {
background: ${cssManager.bdTheme('#f0f0f0', '#333333')};
color: ${cssManager.bdTheme('#000000', '#ffffff')};
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.slash-menu-item .icon {
@ -277,23 +280,23 @@ export const wysiwygStyles = css`
align-items: center;
justify-content: center;
font-size: 16px;
color: ${cssManager.bdTheme('#666', '#999')};
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
font-weight: 600;
}
.slash-menu-item:hover .icon,
.slash-menu-item.selected .icon {
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
}
.toolbar {
position: absolute;
top: -40px;
left: 0;
background: ${cssManager.bdTheme('#ffffff', '#262626')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')};
border-radius: 6px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
padding: 4px;
display: none;
gap: 4px;
@ -310,17 +313,17 @@ export const wysiwygStyles = css`
border: none;
background: transparent;
cursor: pointer;
border-radius: 4px;
border-radius: 3px;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('#000000', '#e0e0e0')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.toolbar-button:hover {
background: ${cssManager.bdTheme('#f0f0f0', '#333333')};
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
}
/* Drag and Drop Styles */
@ -360,7 +363,7 @@ export const wysiwygStyles = css`
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('#999', '#666')};
color: ${cssManager.bdTheme('#71717a', '#71717a')};
border-radius: 4px;
}
@ -375,13 +378,13 @@ export const wysiwygStyles = css`
}
.drag-handle:hover {
color: ${cssManager.bdTheme('#666', '#999')};
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
color: ${cssManager.bdTheme('#71717a', '#a1a1aa')};
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
}
.drag-handle:active {
cursor: grabbing;
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
background: ${cssManager.bdTheme('#e5e7eb', '#3f3f46')};
}
.block-wrapper.dragging {
@ -407,9 +410,9 @@ export const wysiwygStyles = css`
position: absolute;
left: 0;
right: 0;
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.08)', 'rgba(77, 148, 255, 0.08)')};
border: 2px dashed ${cssManager.bdTheme('#0066cc', '#4d94ff')};
border-radius: 8px;
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.05)', 'rgba(59, 130, 246, 0.05)')};
border: 2px dashed ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
border-radius: 4px;
transition: top 0.2s ease, height 0.2s ease;
pointer-events: none;
z-index: 1999;
@ -426,50 +429,21 @@ export const wysiwygStyles = css`
user-select: none;
}
/* Block Settings Button */
.block-settings {
position: absolute;
right: -40px;
top: 50%;
transform: translateY(-50%);
width: 28px;
height: 28px;
cursor: pointer;
opacity: 0;
transition: opacity 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('#999', '#666')};
border-radius: 4px;
}
.block-wrapper:hover .block-settings {
opacity: 1;
}
.block-settings:hover {
color: ${cssManager.bdTheme('#666', '#999')};
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.05)', 'rgba(255, 255, 255, 0.05)')};
}
.block-settings:active {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
}
/* Block Settings Button - Removed in favor of context menu */
/* Text Selection Styles */
.block ::selection {
background: ${cssManager.bdTheme('rgba(0, 102, 204, 0.3)', 'rgba(77, 148, 255, 0.3)')};
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.2)', 'rgba(59, 130, 246, 0.2)')};
color: inherit;
}
/* Formatting Menu */
.formatting-menu {
position: absolute;
background: ${cssManager.bdTheme('#ffffff', '#262626')};
border: 1px solid ${cssManager.bdTheme('#e0e0e0', '#404040')};
border-radius: 6px;
box-shadow: 0 2px 16px rgba(0, 0, 0, 0.15);
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
border-radius: 4px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1), 0 1px 2px rgba(0, 0, 0, 0.06);
padding: 4px;
display: flex;
gap: 2px;
@ -480,7 +454,7 @@ export const wysiwygStyles = css`
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.95) translateY(5px);
transform: scale(0.98) translateY(2px);
}
to {
opacity: 1;
@ -494,20 +468,20 @@ export const wysiwygStyles = css`
border: none;
background: transparent;
cursor: pointer;
border-radius: 4px;
border-radius: 3px;
transition: all 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
color: ${cssManager.bdTheme('#000000', '#e0e0e0')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
font-weight: 600;
font-size: 14px;
position: relative;
}
.format-button:hover {
background: ${cssManager.bdTheme('#f0f0f0', '#333333')};
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
}
.format-button:active {
@ -535,7 +509,7 @@ export const wysiwygStyles = css`
.block strong,
.block b {
font-weight: 600;
color: ${cssManager.bdTheme('#000000', '#ffffff')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.block em,
@ -554,22 +528,22 @@ export const wysiwygStyles = css`
}
.block code {
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.06)', 'rgba(255, 255, 255, 0.1)')};
background: ${cssManager.bdTheme('#f4f4f5', '#27272a')};
padding: 2px 6px;
border-radius: 3px;
font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
font-size: 0.9em;
color: ${cssManager.bdTheme('#d14', '#ff6b6b')};
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
}
.block a {
color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.15s ease;
}
.block a:hover {
border-bottom-color: ${cssManager.bdTheme('#0066cc', '#4d94ff')};
border-bottom-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
}
`;