Right-click anywhere inside this shadow DOM
-This is a test paragraph
'; - 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(); \ No newline at end of file diff --git a/test/test.wysiwyg-contextmenu.chromium.ts b/test/test.wysiwyg-contextmenu.chromium.ts deleted file mode 100644 index 5a71a5a..0000000 --- a/test/test.wysiwyg-contextmenu.chromium.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js'; -import { DeesContextmenu } from '../ts_web/elements/dees-contextmenu/dees-contextmenu.js'; - -tap.test('should show context menu on WYSIWYG blocks', async () => { - // Create WYSIWYG editor - const wysiwygEditor = new DeesInputWysiwyg(); - wysiwygEditor.value = 'Test paragraph
element
- const computedStyle = window.getComputedStyle(codeElement);
- // Font family may vary by platform, so just check it contains something
- expect(computedStyle.fontFamily).toBeTruthy();
-});
-
-tap.test('Phase 3: List block should render correctly', async () => {
- const editor: DeesInputWysiwyg = await webhelpers.fixture(
- webhelpers.html` `
- );
-
- // Import a list block
- editor.importBlocks([
- { id: 'list-1', type: 'list', content: 'First item\nSecond item\nThird item' }
- ]);
-
- await editor.updateComplete;
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Check if list block was rendered
- const listBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="list-1"]');
- const listBlockComponent = listBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
- const listContainer = listBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
- const listElement = listContainer?.querySelector('.block.list') as HTMLElement;
-
- expect(listElement).toBeTruthy();
-
- // Check if list items were created
- const listItems = listElement?.querySelectorAll('li');
- expect(listItems?.length).toEqual(3);
- expect(listItems?.[0].textContent).toEqual('First item');
- expect(listItems?.[1].textContent).toEqual('Second item');
- expect(listItems?.[2].textContent).toEqual('Third item');
-
- // Check if it's an unordered list by default
- const ulElement = listElement?.querySelector('ul');
- expect(ulElement).toBeTruthy();
-});
-
-tap.test('Phase 3: Quote block split should work', async () => {
- const editor: DeesInputWysiwyg = await webhelpers.fixture(
- webhelpers.html` `
- );
-
- // Import a quote block
- editor.importBlocks([
- { id: 'quote-split', type: 'quote', content: 'To be or not to be' }
- ]);
-
- await editor.updateComplete;
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Get the quote block
- const quoteBlockWrapper = editor.shadowRoot?.querySelector('[data-block-id="quote-split"]');
- const quoteBlockComponent = quoteBlockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
- const quoteContainer = quoteBlockComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
- const quoteElement = quoteContainer?.querySelector('.block.quote') as HTMLElement;
-
- // Focus and set cursor after "To be"
- quoteElement.focus();
- const textNode = quoteElement.firstChild;
- if (textNode && textNode.nodeType === Node.TEXT_NODE) {
- const range = document.createRange();
- const selection = window.getSelection();
- range.setStart(textNode, 5); // After "To be"
- range.setEnd(textNode, 5);
- selection?.removeAllRanges();
- selection?.addRange(range);
-
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Press Enter to split
- const enterEvent = new KeyboardEvent('keydown', {
- key: 'Enter',
- code: 'Enter',
- bubbles: true,
- cancelable: true,
- composed: true
- });
-
- quoteElement.dispatchEvent(enterEvent);
- await new Promise(resolve => setTimeout(resolve, 200));
-
- // Check if split happened correctly
- expect(editor.blocks.length).toEqual(2);
- expect(editor.blocks[0].content).toEqual('To be');
- expect(editor.blocks[1].content).toEqual(' or not to be');
- expect(editor.blocks[1].type).toEqual('paragraph'); // New block should be paragraph
- }
-});
-
-export default tap.start();
\ No newline at end of file
diff --git a/test/test.wysiwyg-registry.both.ts b/test/test.wysiwyg-registry.both.ts
deleted file mode 100644
index 8937f8f..0000000
--- a/test/test.wysiwyg-registry.both.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
-
-import { BlockRegistry } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/block.registry.js';
-import { DividerBlockHandler } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/content/divider.block.js';
-import { ParagraphBlockHandler } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/text/paragraph.block.js';
-import { HeadingBlockHandler } from '../ts_web/elements/00group-input/dees-input-wysiwyg/blocks/text/heading.block.js';
-
-// Import block registration to ensure handlers are registered
-import '../ts_web/elements/00group-input/dees-input-wysiwyg/wysiwyg.blockregistration.js';
-
-tap.test('BlockRegistry should register and retrieve handlers', async () => {
- // Test divider handler
- const dividerHandler = BlockRegistry.getHandler('divider');
- expect(dividerHandler).toBeDefined();
- expect(dividerHandler).toBeInstanceOf(DividerBlockHandler);
- expect(dividerHandler?.type).toEqual('divider');
-
- // Test paragraph handler
- const paragraphHandler = BlockRegistry.getHandler('paragraph');
- expect(paragraphHandler).toBeDefined();
- expect(paragraphHandler).toBeInstanceOf(ParagraphBlockHandler);
- expect(paragraphHandler?.type).toEqual('paragraph');
-
- // Test heading handlers
- const heading1Handler = BlockRegistry.getHandler('heading-1');
- expect(heading1Handler).toBeDefined();
- expect(heading1Handler).toBeInstanceOf(HeadingBlockHandler);
- expect(heading1Handler?.type).toEqual('heading-1');
-
- const heading2Handler = BlockRegistry.getHandler('heading-2');
- expect(heading2Handler).toBeDefined();
- expect(heading2Handler).toBeInstanceOf(HeadingBlockHandler);
- expect(heading2Handler?.type).toEqual('heading-2');
-
- const heading3Handler = BlockRegistry.getHandler('heading-3');
- expect(heading3Handler).toBeDefined();
- expect(heading3Handler).toBeInstanceOf(HeadingBlockHandler);
- expect(heading3Handler?.type).toEqual('heading-3');
-});
-
-tap.test('Block handlers should render content correctly', async () => {
- const testBlock = {
- id: 'test-1',
- type: 'paragraph' as const,
- content: 'Test paragraph content'
- };
-
- const handler = BlockRegistry.getHandler('paragraph');
- expect(handler).toBeDefined();
-
- if (handler) {
- const rendered = handler.render(testBlock, false);
- // The render() method returns the HTML template structure
- // Content is set later in setup()
- expect(rendered).toContain('contenteditable="true"');
- expect(rendered).toContain('data-block-type="paragraph"');
- expect(rendered).toContain('data-block-id="test-1"');
- expect(rendered).toContain('class="block paragraph"');
- }
-});
-
-tap.test('Divider handler should render correctly', async () => {
- const dividerBlock = {
- id: 'test-divider',
- type: 'divider' as const,
- content: ' '
- };
-
- const handler = BlockRegistry.getHandler('divider');
- expect(handler).toBeDefined();
-
- if (handler) {
- const rendered = handler.render(dividerBlock, false);
- expect(rendered).toContain('class="block divider"');
- expect(rendered).toContain('tabindex="0"');
- expect(rendered).toContain('
');
- expect(rendered).toContain('data-block-id="test-divider"');
- }
-});
-
-tap.test('Heading handlers should render with correct levels', async () => {
- const headingBlock = {
- id: 'test-h1',
- type: 'heading-1' as const,
- content: 'Test Heading'
- };
-
- const handler = BlockRegistry.getHandler('heading-1');
- expect(handler).toBeDefined();
-
- if (handler) {
- const rendered = handler.render(headingBlock, false);
- // The render() method returns the HTML template structure
- // Content is set later in setup()
- expect(rendered).toContain('class="block heading-1"');
- expect(rendered).toContain('contenteditable="true"');
- expect(rendered).toContain('data-block-id="test-h1"');
- expect(rendered).toContain('data-block-type="heading-1"');
- }
-});
-
-tap.test('getAllTypes should return all registered types', async () => {
- const allTypes = BlockRegistry.getAllTypes();
- expect(allTypes).toContain('divider');
- expect(allTypes).toContain('paragraph');
- expect(allTypes).toContain('heading-1');
- expect(allTypes).toContain('heading-2');
- expect(allTypes).toContain('heading-3');
- expect(allTypes.length).toBeGreaterThanOrEqual(5);
-});
-
-export default tap.start();
\ No newline at end of file
diff --git a/test/test.wysiwyg-selection-highlight.chromium.ts b/test/test.wysiwyg-selection-highlight.chromium.ts
deleted file mode 100644
index f212251..0000000
--- a/test/test.wysiwyg-selection-highlight.chromium.ts
+++ /dev/null
@@ -1,158 +0,0 @@
-import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
-import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
-import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
-
-tap.test('Selection highlighting should work consistently for all block types', async () => {
- const editor: DeesInputWysiwyg = await webhelpers.fixture(
- webhelpers.html` `
- );
-
- // Import various block types
- editor.importBlocks([
- { id: 'para-1', type: 'paragraph', content: 'This is a paragraph' },
- { id: 'heading-1', type: 'heading-1', content: 'This is a heading' },
- { id: 'quote-1', type: 'quote', content: 'This is a quote' },
- { id: 'code-1', type: 'code', content: 'const x = 42;' },
- { id: 'list-1', type: 'list', content: 'Item 1\nItem 2' }
- ]);
-
- await editor.updateComplete;
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Test paragraph highlighting
- console.log('Testing paragraph highlighting...');
- const paraWrapper = editor.shadowRoot?.querySelector('[data-block-id="para-1"]');
- const paraComponent = paraWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
- const paraContainer = paraComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
- const paraElement = paraContainer?.querySelector('.block.paragraph') as HTMLElement;
-
- // Focus paragraph to select it
- paraElement.focus();
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Check if paragraph has selected class
- const paraHasSelected = paraElement.classList.contains('selected');
- console.log('Paragraph has selected class:', paraHasSelected);
-
- // Check computed styles
- const paraStyle = window.getComputedStyle(paraElement);
- console.log('Paragraph background:', paraStyle.background);
- console.log('Paragraph box-shadow:', paraStyle.boxShadow);
-
- // Test heading highlighting
- console.log('\nTesting heading highlighting...');
- const headingWrapper = editor.shadowRoot?.querySelector('[data-block-id="heading-1"]');
- const headingComponent = headingWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
- const headingContainer = headingComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
- const headingElement = headingContainer?.querySelector('.block.heading-1') as HTMLElement;
-
- // Focus heading to select it
- headingElement.focus();
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Check if heading has selected class
- const headingHasSelected = headingElement.classList.contains('selected');
- console.log('Heading has selected class:', headingHasSelected);
-
- // Check computed styles
- const headingStyle = window.getComputedStyle(headingElement);
- console.log('Heading background:', headingStyle.background);
- console.log('Heading box-shadow:', headingStyle.boxShadow);
-
- // Test quote highlighting
- console.log('\nTesting quote highlighting...');
- const quoteWrapper = editor.shadowRoot?.querySelector('[data-block-id="quote-1"]');
- const quoteComponent = quoteWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
- const quoteContainer = quoteComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
- const quoteElement = quoteContainer?.querySelector('.block.quote') as HTMLElement;
-
- // Focus quote to select it
- quoteElement.focus();
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Check if quote has selected class
- const quoteHasSelected = quoteElement.classList.contains('selected');
- console.log('Quote has selected class:', quoteHasSelected);
-
- // Test code highlighting - code blocks use .code-editor instead of .block.code
- console.log('\nTesting code highlighting...');
- const codeWrapper = editor.shadowRoot?.querySelector('[data-block-id="code-1"]');
- const codeComponent = codeWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
- const codeContainer = codeComponent?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
- const codeElement = codeContainer?.querySelector('.code-editor') as HTMLElement;
- const codeBlockContainer = codeContainer?.querySelector('.code-block-container') as HTMLElement;
-
- // Focus code to select it
- codeElement.focus();
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // For code blocks, the selection is on the container, not the editor
- const codeHasSelected = codeBlockContainer?.classList.contains('selected');
- console.log('Code container has selected class:', codeHasSelected);
-
- // Focus back on paragraph and check if others are deselected
- console.log('\nFocusing back on paragraph...');
- paraElement.focus();
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Check that only paragraph is selected
- expect(paraElement.classList.contains('selected')).toBeTrue();
- expect(headingElement.classList.contains('selected')).toBeFalse();
- expect(quoteElement.classList.contains('selected')).toBeFalse();
- // Code blocks use different selection structure
- expect(codeBlockContainer?.classList.contains('selected') || false).toBeFalse();
-
- console.log('Selection highlighting test complete');
-});
-
-tap.test('Selected class should toggle correctly when clicking between blocks', async () => {
- const editor: DeesInputWysiwyg = await webhelpers.fixture(
- webhelpers.html` `
- );
-
- // Import two blocks
- editor.importBlocks([
- { id: 'block-1', type: 'paragraph', content: 'First block' },
- { id: 'block-2', type: 'paragraph', content: 'Second block' }
- ]);
-
- await editor.updateComplete;
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Get both blocks
- const block1Wrapper = editor.shadowRoot?.querySelector('[data-block-id="block-1"]');
- const block1Component = block1Wrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
- const block1Container = block1Component?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
- const block1Element = block1Container?.querySelector('.block.paragraph') as HTMLElement;
-
- const block2Wrapper = editor.shadowRoot?.querySelector('[data-block-id="block-2"]');
- const block2Component = block2Wrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
- const block2Container = block2Component?.shadowRoot?.querySelector('.wysiwyg-block-container') as HTMLElement;
- const block2Element = block2Container?.querySelector('.block.paragraph') as HTMLElement;
-
- // Initially neither should be selected
- expect(block1Element.classList.contains('selected')).toBeFalse();
- expect(block2Element.classList.contains('selected')).toBeFalse();
-
- // Click on first block
- block1Element.click();
- block1Element.focus();
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // First block should be selected
- expect(block1Element.classList.contains('selected')).toBeTrue();
- expect(block2Element.classList.contains('selected')).toBeFalse();
-
- // Click on second block
- block2Element.click();
- block2Element.focus();
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Second block should be selected, first should not
- expect(block1Element.classList.contains('selected')).toBeFalse();
- expect(block2Element.classList.contains('selected')).toBeTrue();
-
- console.log('Toggle test complete');
-});
-
-export default tap.start();
\ No newline at end of file
diff --git a/test/test.wysiwyg-selection-simple.chromium.ts b/test/test.wysiwyg-selection-simple.chromium.ts
deleted file mode 100644
index 84dcfe3..0000000
--- a/test/test.wysiwyg-selection-simple.chromium.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { expect, tap, webhelpers } from '@git.zone/tstest/tapbundle';
-import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
-import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
-
-tap.test('Selection highlighting basic test', async () => {
- const editor: DeesInputWysiwyg = await webhelpers.fixture(
- webhelpers.html` `
- );
-
- // Import two blocks
- editor.importBlocks([
- { id: 'para-1', type: 'paragraph', content: 'First paragraph' },
- { id: 'head-1', type: 'heading-1', content: 'First heading' }
- ]);
-
- await editor.updateComplete;
- await new Promise(resolve => setTimeout(resolve, 500));
-
- // Get paragraph element
- const paraWrapper = editor.shadowRoot?.querySelector('[data-block-id="para-1"]');
- const paraComponent = paraWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
- const paraBlock = paraComponent?.shadowRoot?.querySelector('.block.paragraph') as HTMLElement;
-
- // Get heading element
- const headWrapper = editor.shadowRoot?.querySelector('[data-block-id="head-1"]');
- const headComponent = headWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
- const headBlock = headComponent?.shadowRoot?.querySelector('.block.heading-1') as HTMLElement;
-
- console.log('Found elements:', {
- paraBlock: !!paraBlock,
- headBlock: !!headBlock
- });
-
- // Focus paragraph
- paraBlock.focus();
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Check classes
- console.log('Paragraph classes:', paraBlock.className);
- console.log('Heading classes:', headBlock.className);
-
- // Check isSelected property
- console.log('Paragraph component isSelected:', paraComponent.isSelected);
- console.log('Heading component isSelected:', headComponent.isSelected);
-
- // Focus heading
- headBlock.focus();
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Check classes again
- console.log('\nAfter focusing heading:');
- console.log('Paragraph classes:', paraBlock.className);
- console.log('Heading classes:', headBlock.className);
- console.log('Paragraph component isSelected:', paraComponent.isSelected);
- console.log('Heading component isSelected:', headComponent.isSelected);
-
- // Check that heading is selected
- expect(headBlock.classList.contains('selected')).toBeTrue();
- expect(paraBlock.classList.contains('selected')).toBeFalse();
-});
-
-export default tap.start();
\ No newline at end of file
diff --git a/test/test.wysiwyg-split.chromium.ts b/test/test.wysiwyg-split.chromium.ts
deleted file mode 100644
index 2247347..0000000
--- a/test/test.wysiwyg-split.chromium.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import { tap, expect, webhelpers } from '@git.zone/tstest/tapbundle';
-
-import { DeesInputWysiwyg } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-input-wysiwyg.js';
-import { DeesWysiwygBlock } from '../ts_web/elements/00group-input/dees-input-wysiwyg/dees-wysiwyg-block.js';
-
-tap.test('should split paragraph content on Enter key', async () => {
- // Create the wysiwyg editor
- const editor: DeesInputWysiwyg = await webhelpers.fixture(
- webhelpers.html` `
- );
-
- // Import a test paragraph
- editor.importBlocks([{
- id: 'test-para-1',
- type: 'paragraph',
- content: 'Hello World'
- }]);
-
- await editor.updateComplete;
-
- // Wait for blocks to render
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Get the block wrapper and component
- const blockWrapper = editor.shadowRoot?.querySelector('[data-block-id="test-para-1"]');
- expect(blockWrapper).toBeDefined();
-
- const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
- expect(blockComponent).toBeDefined();
- expect(blockComponent.block.type).toEqual('paragraph');
-
- // Wait for block to render
- await blockComponent.updateComplete;
-
- // Test getSplitContent
- console.log('Testing getSplitContent...');
- const splitResult = blockComponent.getSplitContent();
- console.log('Split result:', splitResult);
-
- // Since we haven't set cursor position, it might return null or split at start
- // This is just to test if the method is callable
- expect(typeof blockComponent.getSplitContent).toEqual('function');
-});
-
-tap.test('should handle Enter key press in paragraph', async () => {
- // Create the wysiwyg editor
- const editor: DeesInputWysiwyg = await webhelpers.fixture(
- webhelpers.html` `
- );
-
- // Import a test paragraph
- editor.importBlocks([{
- id: 'test-enter-1',
- type: 'paragraph',
- content: 'First part|Second part' // | marks where we'll simulate cursor
- }]);
-
- await editor.updateComplete;
- await new Promise(resolve => setTimeout(resolve, 100));
-
- // Check initial state
- expect(editor.blocks.length).toEqual(1);
- expect(editor.blocks[0].content).toEqual('First part|Second part');
-
- // Get the block element
- const blockWrapper = editor.shadowRoot?.querySelector('[data-block-id="test-enter-1"]');
- const blockComponent = blockWrapper?.querySelector('dees-wysiwyg-block') as DeesWysiwygBlock;
- const blockElement = blockComponent.shadowRoot?.querySelector('.block.paragraph') as HTMLDivElement;
-
- expect(blockElement).toBeDefined();
-
- // Set content without the | marker
- blockElement.textContent = 'First partSecond part';
-
- // Focus the block
- blockElement.focus();
-
- // Create and dispatch Enter key event
- const enterEvent = new KeyboardEvent('keydown', {
- key: 'Enter',
- code: 'Enter',
- bubbles: true,
- cancelable: true,
- composed: true
- });
-
- // Dispatch the event
- blockElement.dispatchEvent(enterEvent);
-
- // Wait for processing
- await new Promise(resolve => setTimeout(resolve, 200));
-
- // Check if block was split (this might not work perfectly in test environment)
- console.log('Blocks after Enter:', editor.blocks.length);
- console.log('Block contents:', editor.blocks.map(b => b.content));
-});
-
-export default tap.start();
\ No newline at end of file
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/eco-applauncher-powermenu.demo.ts b/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/eco-applauncher-powermenu.demo.ts
new file mode 100644
index 0000000..8471c80
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/eco-applauncher-powermenu.demo.ts
@@ -0,0 +1,20 @@
+import { html } from '@design.estate/dees-element';
+
+export const demo = () => html`
+
+
+ console.log('Power action:', e.detail.action)}
+ @menu-close=${() => console.log('Menu closed')}
+ >
+
+`;
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/eco-applauncher-powermenu.ts b/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/eco-applauncher-powermenu.ts
new file mode 100644
index 0000000..84388f8
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/eco-applauncher-powermenu.ts
@@ -0,0 +1,261 @@
+import {
+ customElement,
+ DeesElement,
+ type TemplateResult,
+ html,
+ property,
+ css,
+ cssManager,
+} from '@design.estate/dees-element';
+import { DeesIcon } from '@design.estate/dees-catalog';
+import { demo } from './eco-applauncher-powermenu.demo.js';
+
+// Ensure dees-icon is registered
+DeesIcon;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'eco-applauncher-powermenu': EcoApplauncherPowermenu;
+ }
+}
+
+export type TPowerAction = 'lock' | 'lock-sleep' | 'reboot';
+
+@customElement('eco-applauncher-powermenu')
+export class EcoApplauncherPowermenu extends DeesElement {
+ public static demo = demo;
+ public static demoGroup = 'App Launcher';
+
+ public static styles = [
+ cssManager.defaultStyles,
+ css`
+ :host {
+ display: block;
+ position: relative;
+ pointer-events: none;
+ }
+
+ :host([open]) {
+ pointer-events: auto;
+ }
+
+ .menu-container {
+ background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 10%)')};
+ border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
+ border-radius: 12px;
+ box-shadow: ${cssManager.bdTheme(
+ '0 8px 32px rgba(0, 0, 0, 0.15)',
+ '0 8px 32px rgba(0, 0, 0, 0.4)'
+ )};
+ min-width: 200px;
+ overflow: hidden;
+ opacity: 0;
+ transform: scale(0.95) translateY(-8px);
+ transition: all 0.2s ease-out;
+ pointer-events: none;
+ }
+
+ :host([open]) .menu-container {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ pointer-events: auto;
+ }
+
+ .menu-header {
+ padding: 12px 16px;
+ border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 15%)')};
+ font-size: 12px;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
+ }
+
+ .menu-options {
+ padding: 8px 0;
+ }
+
+ .menu-option {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ cursor: pointer;
+ transition: background 0.15s ease;
+ color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
+ }
+
+ .menu-option:hover {
+ background: ${cssManager.bdTheme('hsl(0 0% 96%)', 'hsl(240 5% 15%)')};
+ }
+
+ .menu-option:active {
+ background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 18%)')};
+ }
+
+ .menu-option.danger {
+ color: ${cssManager.bdTheme('hsl(0 72% 45%)', 'hsl(0 72% 60%)')};
+ }
+
+ .menu-option.danger:hover {
+ background: ${cssManager.bdTheme('hsl(0 72% 97%)', 'hsl(0 50% 15%)')};
+ }
+
+ .option-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ border-radius: 6px;
+ background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')};
+ }
+
+ .menu-option.danger .option-icon {
+ background: ${cssManager.bdTheme('hsl(0 72% 94%)', 'hsl(0 50% 18%)')};
+ }
+
+ .option-text {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .option-label {
+ font-size: 14px;
+ font-weight: 500;
+ }
+
+ .option-description {
+ font-size: 11px;
+ color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 50%)')};
+ }
+
+ .menu-divider {
+ height: 1px;
+ background: ${cssManager.bdTheme('hsl(0 0% 92%)', 'hsl(240 5% 15%)')};
+ margin: 4px 0;
+ }
+ `,
+ ];
+
+ @property({ type: Boolean, reflect: true })
+ accessor open = false;
+
+ private boundHandleClickOutside = this.handleClickOutside.bind(this);
+ private inactivityTimeout: ReturnType | null = null;
+ private readonly INACTIVITY_TIMEOUT = 60000; // 1 minute
+ private lastActivityTime = 0;
+
+ public render(): TemplateResult {
+ return html`
+
+ `;
+ }
+
+ private handleAction(action: TPowerAction): void {
+ this.dispatchEvent(new CustomEvent('power-action', {
+ detail: { action },
+ bubbles: true,
+ composed: true,
+ }));
+ this.closeMenu();
+ }
+
+ private handleClickOutside(e: MouseEvent): void {
+ if (this.open && !this.contains(e.target as Node)) {
+ this.closeMenu();
+ }
+ }
+
+ private resetInactivityTimer(): void {
+ const now = Date.now();
+ // Throttle: only reset if 5+ seconds since last reset
+ if (now - this.lastActivityTime < 5000) {
+ return;
+ }
+ this.lastActivityTime = now;
+ this.clearInactivityTimer();
+ if (this.open) {
+ this.inactivityTimeout = setTimeout(() => {
+ this.closeMenu();
+ }, this.INACTIVITY_TIMEOUT);
+ }
+ }
+
+ private clearInactivityTimer(): void {
+ if (this.inactivityTimeout) {
+ clearTimeout(this.inactivityTimeout);
+ this.inactivityTimeout = null;
+ }
+ }
+
+ private closeMenu(): void {
+ this.open = false;
+ this.dispatchEvent(new CustomEvent('menu-close', {
+ bubbles: true,
+ composed: true,
+ }));
+ }
+
+ protected updated(changedProperties: Map): void {
+ if (changedProperties.has('open')) {
+ if (this.open) {
+ this.resetInactivityTimer();
+ } else {
+ this.clearInactivityTimer();
+ }
+ }
+ }
+
+ async connectedCallback(): Promise {
+ await super.connectedCallback();
+ setTimeout(() => {
+ document.addEventListener('click', this.boundHandleClickOutside);
+ }, 0);
+ }
+
+ async disconnectedCallback(): Promise {
+ await super.disconnectedCallback();
+ document.removeEventListener('click', this.boundHandleClickOutside);
+ this.clearInactivityTimer();
+ }
+}
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/index.ts b/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/index.ts
new file mode 100644
index 0000000..8b36cfc
--- /dev/null
+++ b/ts_web/elements/00group-applauncher/eco-applauncher-powermenu/index.ts
@@ -0,0 +1 @@
+export * from './eco-applauncher-powermenu.js';
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.demo.ts b/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.demo.ts
index 9ccd9b5..6eb40ea 100644
--- a/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.demo.ts
+++ b/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.demo.ts
@@ -1,21 +1,18 @@
import { html } from '@design.estate/dees-element';
-import type { IAppIcon } from './eco-applauncher.js';
+import type { IAppIcon, ILoginConfig, ILoginCredentials, TApplauncherMode } from './eco-applauncher.js';
import type { IWifiNetwork } from '../eco-applauncher-wifimenu/index.js';
import type { IAudioDevice } from '../eco-applauncher-soundmenu/index.js';
+import '../../../views/eco-view-settings/eco-view-settings.js';
+import '../../../views/eco-view-peripherals/eco-view-peripherals.js';
+import '../../../views/eco-view-saasshare/eco-view-saasshare.js';
+import '../../../views/eco-view-system/eco-view-system.js';
+import type { EcoApplauncher } from './eco-applauncher.js';
const mockApps: IAppIcon[] = [
- { name: 'Settings', icon: 'lucide:settings', action: () => console.log('Settings clicked') },
- { name: 'Browser', icon: 'lucide:globe', action: () => console.log('Browser clicked') },
- { name: 'Terminal', icon: 'lucide:terminal', action: () => console.log('Terminal clicked') },
- { name: 'Files', icon: 'lucide:folder', action: () => console.log('Files clicked') },
- { name: 'Calendar', icon: 'lucide:calendar', action: () => console.log('Calendar clicked') },
- { name: 'Mail', icon: 'lucide:mail', action: () => console.log('Mail clicked') },
- { name: 'Music', icon: 'lucide:music', action: () => console.log('Music clicked') },
- { name: 'Photos', icon: 'lucide:image', action: () => console.log('Photos clicked') },
- { name: 'Notes', icon: 'lucide:fileText', action: () => console.log('Notes clicked') },
- { name: 'Calculator', icon: 'lucide:calculator', action: () => console.log('Calculator clicked') },
- { name: 'Weather', icon: 'lucide:cloudSun', action: () => console.log('Weather clicked') },
- { name: 'Maps', icon: 'lucide:map', action: () => console.log('Maps clicked') },
+ { name: 'SaaS Share', icon: 'lucide:share2', view: html` ` },
+ { name: 'System', icon: 'lucide:activity', view: html` ` },
+ { name: 'Peripherals', icon: 'lucide:monitor', view: html` ` },
+ { name: 'Settings', icon: 'lucide:settings', view: html` ` },
];
const mockNetworks: IWifiNetwork[] = [
@@ -32,7 +29,33 @@ const mockAudioDevices: IAudioDevice[] = [
{ id: 'hdmi', name: 'LG Monitor', type: 'hdmi' },
];
-export const demo = () => html`
+const loginConfig: ILoginConfig = {
+ allowedMethods: ['pin', 'password', 'qr'],
+ pinLength: 4,
+ welcomeMessage: 'Welcome to EcoBridge',
+};
+
+const handleLoginAttempt = (e: CustomEvent) => {
+ const credentials = e.detail as ILoginCredentials;
+ const applauncher = e.target as EcoApplauncher;
+
+ console.log('Login attempt:', credentials);
+
+ // Demo validation: PIN "1234" or password "demo"
+ if (
+ (credentials.method === 'pin' && credentials.value === '1234') ||
+ (credentials.method === 'password' && credentials.value === 'demo')
+ ) {
+ console.log('Login successful!');
+ applauncher.setLoginResult(true);
+ } else {
+ console.log('Login failed');
+ applauncher.setLoginResult(false, 'Invalid credentials. Try PIN: 1234 or Password: demo');
+ }
+};
+
+// Home mode demo
+const demoHome = () => html`
html`
.muted=${false}
.userName=${'John Doe'}
.notificationCount=${3}
+ @login-attempt=${handleLoginAttempt}
+ @login-success=${() => console.log('Login success event received')}
+ @login-failure=${(e: CustomEvent) => console.log('Login failure:', e.detail)}
@wifi-toggle=${(e: CustomEvent) => console.log('WiFi toggle:', e.detail)}
@network-select=${(e: CustomEvent) => console.log('Network selected:', e.detail)}
@wifi-settings-click=${() => console.log('WiFi settings clicked')}
@@ -69,3 +97,37 @@ export const demo = () => html`
>
`;
+demoHome.demoTitle = 'Home Mode';
+
+// Login mode demo
+const demoLogin = () => html`
+
+
+ console.log('Login success event received')}
+ @login-failure=${(e: CustomEvent) => console.log('Login failure:', e.detail)}
+ >
+
+`;
+demoLogin.demoTitle = 'Login Mode';
+
+// Export array of demo functions
+export const demo = [demoHome, demoLogin];
diff --git a/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.ts b/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.ts
index 37e2cf8..e7c8ada 100644
--- a/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.ts
+++ b/ts_web/elements/00group-applauncher/eco-applauncher/eco-applauncher.ts
@@ -14,6 +14,9 @@ import { EcoApplauncherWifimenu, type IWifiNetwork } from '../eco-applauncher-wi
import { EcoApplauncherBatterymenu } from '../eco-applauncher-batterymenu/index.js';
import { EcoApplauncherSoundmenu, type IAudioDevice } from '../eco-applauncher-soundmenu/index.js';
import { EcoApplauncherKeyboard } from '../eco-applauncher-keyboard/index.js';
+import { EcoApplauncherPowermenu, type TPowerAction } from '../eco-applauncher-powermenu/index.js';
+import { EcoViewHome } from '../../../views/eco-view-home/index.js';
+import { EcoViewLogin, type ILoginConfig, type ILoginCredentials } from '../../../views/eco-view-login/index.js';
// Ensure components are registered
DeesIcon;
@@ -21,6 +24,9 @@ EcoApplauncherWifimenu;
EcoApplauncherBatterymenu;
EcoApplauncherSoundmenu;
EcoApplauncherKeyboard;
+EcoApplauncherPowermenu;
+EcoViewHome;
+EcoViewLogin;
declare global {
interface HTMLElementTagNameMap {
@@ -28,12 +34,18 @@ declare global {
}
}
+export type TApplauncherMode = 'login' | 'home';
+
export interface IAppIcon {
name: string;
icon: string;
action?: () => void;
+ view?: TemplateResult;
}
+export type { ILoginConfig, ILoginCredentials } from '../../../views/eco-view-login/index.js';
+export type { TPowerAction } from '../eco-applauncher-powermenu/index.js';
+
export interface IStatusBarConfig {
showTime?: boolean;
showNetwork?: boolean;
@@ -153,6 +165,10 @@ export class EcoApplauncher extends DeesElement {
background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')};
}
+ .top-icon-button.active {
+ background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 18%)')};
+ }
+
.user-avatar {
width: 32px;
height: 32px;
@@ -195,9 +211,6 @@ export class EcoApplauncher extends DeesElement {
.apps-area {
flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
padding: 48px;
overflow-y: auto;
}
@@ -206,7 +219,6 @@ export class EcoApplauncher extends DeesElement {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
gap: 32px;
- max-width: 800px;
width: 100%;
}
@@ -415,6 +427,18 @@ export class EcoApplauncher extends DeesElement {
pointer-events: none;
}
+ .topbar-menu-wrapper {
+ position: relative;
+ }
+
+ .topbar-menu-popup {
+ position: absolute;
+ top: calc(100% + 8px);
+ right: 0;
+ z-index: 100;
+ pointer-events: none;
+ }
+
.keyboard-area {
flex-shrink: 0;
height: 0;
@@ -430,6 +454,53 @@ export class EcoApplauncher extends DeesElement {
overflow: visible;
}
+ .view-area {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ }
+
+ .view-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 24px;
+ background: ${cssManager.bdTheme('hsl(220 15% 94%)', 'hsl(240 6% 8%)')};
+ border-bottom: 1px solid ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 15%)')};
+ flex-shrink: 0;
+ }
+
+ .back-button {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 6px;
+ padding: 8px 14px;
+ background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 14%)')};
+ border-radius: 8px;
+ cursor: pointer;
+ transition: background 0.15s ease;
+ color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
+ font-size: 13px;
+ font-weight: 500;
+ }
+
+ .back-button:hover {
+ background: ${cssManager.bdTheme('hsl(220 15% 82%)', 'hsl(240 5% 18%)')};
+ }
+
+ .view-title {
+ font-size: 16px;
+ font-weight: 600;
+ color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
+ }
+
+ .view-content {
+ flex: 1;
+ overflow: auto;
+ }
+
@media (max-width: 600px) {
.apps-area {
padding: 24px;
@@ -462,6 +533,16 @@ export class EcoApplauncher extends DeesElement {
`,
];
+ @property({ type: String })
+ accessor mode: TApplauncherMode = 'home';
+
+ @property({ type: Object })
+ accessor loginConfig: ILoginConfig = {
+ allowedMethods: ['pin', 'password', 'qr'],
+ pinLength: 4,
+ welcomeMessage: 'Welcome',
+ };
+
@property({ type: Array })
accessor apps: IAppIcon[] = [];
@@ -512,9 +593,18 @@ export class EcoApplauncher extends DeesElement {
@state()
accessor soundMenuOpen = false;
+ @state()
+ accessor powerMenuOpen = false;
+
@state()
accessor keyboardVisible = false;
+ @state()
+ accessor activeView: TemplateResult | null = null;
+
+ @state()
+ accessor activeViewName: string | null = null;
+
@property({ type: Array })
accessor networks: IWifiNetwork[] = [];
@@ -552,12 +642,8 @@ export class EcoApplauncher extends DeesElement {
public render(): TemplateResult {
return html`
- ${this.renderTopBar()}
-
-
- ${this.apps.map((app) => this.renderAppIcon(app))}
-
-
+ ${this.mode === 'login' ? '' : this.renderTopBar()}
+ ${this.renderMainContent()}
+ `;
+ }
+
+ private renderHomeView(): TemplateResult {
+ return html`
+
+ `;
+ }
+
+ private renderAppsArea(): TemplateResult {
+ return html`
+
+
+ ${this.apps.map((app) => this.renderAppIcon(app))}
+
+
+ `;
+ }
+
+ private renderActiveView(): TemplateResult {
+ return html`
+
+
+ ${this.activeView}
+
+
+ `;
+ }
+
private renderAppIcon(app: IAppIcon): TemplateResult {
return html`
`;
@@ -831,15 +990,91 @@ export class EcoApplauncher extends DeesElement {
composed: true,
})
);
+
+ // If app has a view, open it inside the applauncher
+ if (app.view) {
+ this.activeView = app.view;
+ this.activeViewName = app.name;
+ return;
+ }
+
+ // Otherwise execute the action
if (app.action) {
app.action();
}
}
+ private handleHomeAppClick(e: CustomEvent): void {
+ const app = e.detail.app as IAppIcon;
+ this.handleAppClick(app);
+ }
+
+ private handleLoginAttempt(e: CustomEvent): void {
+ const credentials = e.detail as ILoginCredentials;
+ // Dispatch event for parent to handle validation
+ this.dispatchEvent(new CustomEvent('login-attempt', {
+ detail: credentials,
+ bubbles: true,
+ composed: true,
+ }));
+ }
+
+ /**
+ * Set the login result after validation
+ * @param success Whether login was successful
+ * @param errorMessage Optional error message to display
+ */
+ public setLoginResult(success: boolean, errorMessage?: string): void {
+ const loginView = this.shadowRoot?.querySelector('eco-view-login') as EcoViewLogin | null;
+
+ if (success) {
+ this.mode = 'home';
+ this.dispatchEvent(new CustomEvent('login-success', {
+ bubbles: true,
+ composed: true,
+ }));
+ } else {
+ if (loginView && errorMessage) {
+ loginView.showErrorMessage(errorMessage);
+ }
+ this.dispatchEvent(new CustomEvent('login-failure', {
+ detail: { error: errorMessage },
+ bubbles: true,
+ composed: true,
+ }));
+ }
+ }
+
+ /**
+ * Switch to login mode
+ */
+ public showLogin(): void {
+ this.mode = 'login';
+ this.activeView = null;
+ this.activeViewName = null;
+ }
+
+ /**
+ * Switch to home mode
+ */
+ public showHome(): void {
+ this.mode = 'home';
+ }
+
+ private handleBackClick(): void {
+ this.activeView = null;
+ this.activeViewName = null;
+ this.dispatchEvent(new CustomEvent('view-close', {
+ bubbles: true,
+ composed: true,
+ }));
+ }
+
private handleNetworkClick(e: MouseEvent): void {
e.stopPropagation();
this.batteryMenuOpen = false;
this.soundMenuOpen = false;
+ this.powerMenuOpen = false;
this.wifiMenuOpen = !this.wifiMenuOpen;
}
@@ -847,6 +1082,7 @@ export class EcoApplauncher extends DeesElement {
e.stopPropagation();
this.wifiMenuOpen = false;
this.soundMenuOpen = false;
+ this.powerMenuOpen = false;
this.batteryMenuOpen = !this.batteryMenuOpen;
}
@@ -854,9 +1090,18 @@ export class EcoApplauncher extends DeesElement {
e.stopPropagation();
this.wifiMenuOpen = false;
this.batteryMenuOpen = false;
+ this.powerMenuOpen = false;
this.soundMenuOpen = !this.soundMenuOpen;
}
+ private handlePowerClick(e: MouseEvent): void {
+ e.stopPropagation();
+ this.wifiMenuOpen = false;
+ this.batteryMenuOpen = false;
+ this.soundMenuOpen = false;
+ this.powerMenuOpen = !this.powerMenuOpen;
+ }
+
private handleWifiMenuClose(): void {
this.wifiMenuOpen = false;
}
@@ -869,6 +1114,19 @@ export class EcoApplauncher extends DeesElement {
this.soundMenuOpen = false;
}
+ private handlePowerMenuClose(): void {
+ this.powerMenuOpen = false;
+ }
+
+ private handlePowerAction(e: CustomEvent): void {
+ const action = e.detail.action as TPowerAction;
+ this.dispatchEvent(new CustomEvent('power-action', {
+ detail: { action },
+ bubbles: true,
+ composed: true,
+ }));
+ }
+
private handleVolumeChange(e: CustomEvent): void {
this.soundLevel = e.detail.volume;
this.dispatchEvent(new CustomEvent('volume-change', {
@@ -953,6 +1211,7 @@ export class EcoApplauncher extends DeesElement {
this.wifiMenuOpen = false;
this.batteryMenuOpen = false;
this.soundMenuOpen = false;
+ this.powerMenuOpen = false;
}
this.dispatchEvent(new CustomEvent('keyboard-toggle', {
detail: { visible: this.keyboardVisible },
diff --git a/ts_web/elements/00group-applauncher/index.ts b/ts_web/elements/00group-applauncher/index.ts
index 1ce25b4..958b416 100644
--- a/ts_web/elements/00group-applauncher/index.ts
+++ b/ts_web/elements/00group-applauncher/index.ts
@@ -4,5 +4,4 @@ export * from './eco-applauncher-wifimenu/index.js';
export * from './eco-applauncher-batterymenu/index.js';
export * from './eco-applauncher-soundmenu/index.js';
export * from './eco-applauncher-keyboard/index.js';
-export * from './eco-settings/index.js';
-export * from './eco-peripherals/index.js';
+export * from './eco-applauncher-powermenu/index.js';
diff --git a/ts_web/views/eco-view-home/eco-view-home.demo.ts b/ts_web/views/eco-view-home/eco-view-home.demo.ts
new file mode 100644
index 0000000..1295645
--- /dev/null
+++ b/ts_web/views/eco-view-home/eco-view-home.demo.ts
@@ -0,0 +1,31 @@
+import { html } from '@design.estate/dees-element';
+import type { IAppIcon } from './eco-view-home.js';
+
+const mockApps: IAppIcon[] = [
+ { name: 'SaaS Share', icon: 'lucide:share2' },
+ { name: 'System', icon: 'lucide:activity' },
+ { name: 'Peripherals', icon: 'lucide:monitor' },
+ { name: 'Settings', icon: 'lucide:settings' },
+ { name: 'Files', icon: 'lucide:folder' },
+ { name: 'Terminal', icon: 'lucide:terminal' },
+ { name: 'Browser', icon: 'lucide:globe' },
+ { name: 'Camera', icon: 'lucide:camera' },
+];
+
+export const demo = () => html`
+
+
+ console.log('App clicked:', e.detail.app)}
+ >
+
+`;
diff --git a/ts_web/views/eco-view-home/eco-view-home.ts b/ts_web/views/eco-view-home/eco-view-home.ts
new file mode 100644
index 0000000..c03b44d
--- /dev/null
+++ b/ts_web/views/eco-view-home/eco-view-home.ts
@@ -0,0 +1,157 @@
+import {
+ customElement,
+ DeesElement,
+ type TemplateResult,
+ html,
+ property,
+ css,
+ cssManager,
+} from '@design.estate/dees-element';
+import { DeesIcon } from '@design.estate/dees-catalog';
+
+// Ensure icon component is registered
+DeesIcon;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'eco-view-home': EcoViewHome;
+ }
+}
+
+export interface IAppIcon {
+ name: string;
+ icon: string;
+ action?: () => void;
+ view?: TemplateResult;
+}
+
+@customElement('eco-view-home')
+export class EcoViewHome extends DeesElement {
+ public static styles = [
+ cssManager.defaultStyles,
+ css`
+ :host {
+ display: block;
+ width: 100%;
+ height: 100%;
+ overflow-y: auto;
+ }
+
+ .apps-area {
+ padding: 48px;
+ min-height: 100%;
+ box-sizing: border-box;
+ }
+
+ .apps-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
+ gap: 32px;
+ width: 100%;
+ }
+
+ .app-icon {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+ padding: 16px;
+ border-radius: 16px;
+ cursor: pointer;
+ transition: background 0.2s ease, transform 0.15s ease;
+ user-select: none;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ .app-icon:hover {
+ background: ${cssManager.bdTheme('hsl(220 15% 92%)', 'hsl(240 5% 12%)')};
+ }
+
+ .app-icon:active {
+ transform: scale(0.95);
+ background: ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 16%)')};
+ }
+
+ .app-icon-circle {
+ width: 64px;
+ height: 64px;
+ border-radius: 16px;
+ background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 15%)')};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 80%)')};
+ }
+
+ .app-icon-circle dees-icon {
+ --dees-icon-size: 28px;
+ }
+
+ .app-icon-name {
+ font-size: 13px;
+ font-weight: 500;
+ color: ${cssManager.bdTheme('hsl(0 0% 25%)', 'hsl(0 0% 85%)')};
+ text-align: center;
+ max-width: 90px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+
+ @media (max-width: 600px) {
+ .apps-area {
+ padding: 24px;
+ }
+
+ .apps-grid {
+ grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
+ gap: 16px;
+ }
+
+ .app-icon-circle {
+ width: 56px;
+ height: 56px;
+ font-size: 24px;
+ }
+
+ .app-icon-name {
+ font-size: 12px;
+ }
+ }
+ `,
+ ];
+
+ @property({ type: Array })
+ accessor apps: IAppIcon[] = [];
+
+ public render(): TemplateResult {
+ return html`
+
+
+ ${this.apps.map((app) => this.renderAppIcon(app))}
+
+
+ `;
+ }
+
+ private renderAppIcon(app: IAppIcon): TemplateResult {
+ return html`
+
+ `;
+ }
+
+ private handleAppClick(app: IAppIcon): void {
+ this.dispatchEvent(
+ new CustomEvent('app-click', {
+ detail: { app },
+ bubbles: true,
+ composed: true,
+ })
+ );
+ }
+}
diff --git a/ts_web/views/eco-view-home/index.ts b/ts_web/views/eco-view-home/index.ts
new file mode 100644
index 0000000..453039d
--- /dev/null
+++ b/ts_web/views/eco-view-home/index.ts
@@ -0,0 +1 @@
+export * from './eco-view-home.js';
diff --git a/ts_web/views/eco-view-login/eco-view-login.demo.ts b/ts_web/views/eco-view-login/eco-view-login.demo.ts
new file mode 100644
index 0000000..28d4149
--- /dev/null
+++ b/ts_web/views/eco-view-login/eco-view-login.demo.ts
@@ -0,0 +1,48 @@
+import { html } from '@design.estate/dees-element';
+import type { ILoginConfig, ILoginCredentials } from './eco-view-login.js';
+
+const handleLoginAttempt = (e: CustomEvent) => {
+ const { method, value } = e.detail;
+ console.log(`Login attempt via ${method}:`, value);
+
+ // Demo: Show success for PIN "1234" or password "demo"
+ const loginView = e.target as HTMLElement & { showErrorMessage: (msg: string) => void; clearInput: () => void };
+
+ if ((method === 'pin' && value === '1234') || (method === 'password' && value === 'demo')) {
+ console.log('Login successful!');
+ alert('Login successful! (Demo)');
+ loginView.clearInput();
+ } else {
+ loginView.showErrorMessage('Invalid credentials. Try PIN: 1234 or Password: demo');
+ }
+};
+
+const pinOnlyConfig: ILoginConfig = {
+ allowedMethods: ['pin'],
+ pinLength: 4,
+ welcomeMessage: 'Enter PIN',
+};
+
+const allMethodsConfig: ILoginConfig = {
+ allowedMethods: ['pin', 'password', 'qr'],
+ pinLength: 6,
+ welcomeMessage: 'Sign In',
+};
+
+export const demo = () => html`
+
+
+
+
+`;
diff --git a/ts_web/views/eco-view-login/eco-view-login.ts b/ts_web/views/eco-view-login/eco-view-login.ts
new file mode 100644
index 0000000..140ccc4
--- /dev/null
+++ b/ts_web/views/eco-view-login/eco-view-login.ts
@@ -0,0 +1,749 @@
+import {
+ customElement,
+ DeesElement,
+ type TemplateResult,
+ html,
+ property,
+ css,
+ cssManager,
+ state,
+} from '@design.estate/dees-element';
+import { DeesIcon } from '@design.estate/dees-catalog';
+
+// Ensure icon component is registered
+DeesIcon;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'eco-view-login': EcoViewLogin;
+ }
+}
+
+export type TAuthMethod = 'pin' | 'password' | 'qr';
+
+export interface ILoginConfig {
+ allowedMethods: TAuthMethod[];
+ pinLength?: number;
+ qrCodeData?: string;
+ logoUrl?: string;
+ welcomeMessage?: string;
+ subtitle?: string;
+}
+
+export interface ILoginCredentials {
+ method: TAuthMethod;
+ value: string;
+}
+
+@customElement('eco-view-login')
+export class EcoViewLogin extends DeesElement {
+ public static styles = [
+ cssManager.defaultStyles,
+ css`
+ :host {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ }
+
+ .login-container {
+ display: flex;
+ width: 100%;
+ height: 100%;
+ }
+
+ /* Left Panel - Branding & Method Selection */
+ .left-panel {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 64px;
+ background: ${cssManager.bdTheme('hsl(220 15% 96%)', 'hsl(240 6% 10%)')};
+ border-right: 1px solid ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 16%)')};
+ }
+
+ .branding {
+ margin-bottom: 48px;
+ }
+
+ .logo {
+ width: 72px;
+ height: 72px;
+ border-radius: 18px;
+ background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ margin-bottom: 24px;
+ }
+
+ .logo img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ border-radius: 18px;
+ }
+
+ .welcome-message {
+ font-size: 32px;
+ font-weight: 700;
+ color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 95%)')};
+ margin-bottom: 8px;
+ line-height: 1.2;
+ }
+
+ .subtitle {
+ font-size: 16px;
+ color: ${cssManager.bdTheme('hsl(0 0% 45%)', 'hsl(0 0% 55%)')};
+ line-height: 1.5;
+ }
+
+ .method-selector {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .method-selector-label {
+ font-size: 13px;
+ font-weight: 600;
+ color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 60%)')};
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 4px;
+ }
+
+ .method-option {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ padding: 16px 20px;
+ background: ${cssManager.bdTheme('white', 'hsl(240 5% 14%)')};
+ border: 2px solid ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 20%)')};
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ }
+
+ .method-option:hover {
+ border-color: ${cssManager.bdTheme('hsl(220 15% 80%)', 'hsl(240 5% 28%)')};
+ background: ${cssManager.bdTheme('hsl(220 15% 98%)', 'hsl(240 5% 16%)')};
+ }
+
+ .method-option.active {
+ border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
+ background: ${cssManager.bdTheme('hsl(217 91% 97%)', 'hsl(217 91% 15%)')};
+ }
+
+ .method-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 10px;
+ background: ${cssManager.bdTheme('hsl(220 15% 94%)', 'hsl(240 5% 20%)')};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
+ transition: all 0.2s ease;
+ }
+
+ .method-option.active .method-icon {
+ background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
+ color: white;
+ }
+
+ .method-info {
+ flex: 1;
+ }
+
+ .method-name {
+ font-size: 15px;
+ font-weight: 600;
+ color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
+ margin-bottom: 2px;
+ }
+
+ .method-description {
+ font-size: 13px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
+ }
+
+ .method-check {
+ width: 24px;
+ height: 24px;
+ border-radius: 50%;
+ border: 2px solid ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 25%)')};
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+ }
+
+ .method-option.active .method-check {
+ background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
+ border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
+ color: white;
+ }
+
+ /* Right Panel - Auth Input */
+ .right-panel {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 64px;
+ background: ${cssManager.bdTheme('white', 'hsl(240 6% 6%)')};
+ }
+
+ .auth-content {
+ width: 100%;
+ max-width: 320px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 32px;
+ }
+
+ .auth-title {
+ font-size: 20px;
+ font-weight: 600;
+ color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
+ text-align: center;
+ }
+
+ /* Error message */
+ .error-message {
+ color: hsl(0 72% 51%);
+ font-size: 14px;
+ text-align: center;
+ padding: 12px 16px;
+ background: hsla(0, 72%, 51%, 0.1);
+ border-radius: 8px;
+ width: 100%;
+ box-sizing: border-box;
+ }
+
+ /* PIN Input */
+ .pin-display {
+ display: flex;
+ gap: 16px;
+ }
+
+ .pin-dot {
+ width: 18px;
+ height: 18px;
+ border-radius: 50%;
+ background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 20%)')};
+ transition: all 0.15s ease;
+ }
+
+ .pin-dot.filled {
+ background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
+ transform: scale(1.15);
+ }
+
+ .pin-dot.error {
+ background: hsl(0 72% 51%);
+ animation: shake 0.3s ease;
+ }
+
+ @keyframes shake {
+ 0%, 100% { transform: translateX(0); }
+ 25% { transform: translateX(-4px); }
+ 75% { transform: translateX(4px); }
+ }
+
+ .numpad {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 16px;
+ }
+
+ .numpad-button {
+ width: 76px;
+ height: 76px;
+ border-radius: 50%;
+ background: ${cssManager.bdTheme('hsl(220 15% 95%)', 'hsl(240 5% 14%)')};
+ border: none;
+ font-size: 28px;
+ font-weight: 500;
+ color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 85%)')};
+ cursor: pointer;
+ transition: all 0.15s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+
+ .numpad-button:hover {
+ background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 20%)')};
+ }
+
+ .numpad-button:active {
+ transform: scale(0.95);
+ background: ${cssManager.bdTheme('hsl(220 15% 85%)', 'hsl(240 5% 24%)')};
+ }
+
+ .numpad-button.action {
+ background: transparent;
+ font-size: 18px;
+ }
+
+ .numpad-button.action:hover {
+ background: ${cssManager.bdTheme('hsl(220 15% 95%)', 'hsl(240 5% 14%)')};
+ }
+
+ .numpad-button.submit {
+ background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
+ color: white;
+ }
+
+ .numpad-button.submit:hover {
+ background: ${cssManager.bdTheme('hsl(217 91% 55%)', 'hsl(217 91% 45%)')};
+ }
+
+ /* Password Input */
+ .password-form {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+ }
+
+ .password-input-wrapper {
+ width: 100%;
+ position: relative;
+ }
+
+ .password-input {
+ width: 100%;
+ padding: 18px 52px 18px 18px;
+ font-size: 16px;
+ background: ${cssManager.bdTheme('hsl(220 15% 96%)', 'hsl(240 5% 12%)')};
+ border: 2px solid ${cssManager.bdTheme('hsl(220 15% 88%)', 'hsl(240 5% 20%)')};
+ border-radius: 12px;
+ color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
+ outline: none;
+ box-sizing: border-box;
+ transition: border-color 0.2s ease;
+ }
+
+ .password-input:focus {
+ border-color: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
+ }
+
+ .password-input.error {
+ border-color: hsl(0 72% 51%);
+ animation: shake 0.3s ease;
+ }
+
+ .password-toggle {
+ position: absolute;
+ right: 14px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
+ padding: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 6px;
+ transition: all 0.15s ease;
+ }
+
+ .password-toggle:hover {
+ background: ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 18%)')};
+ color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
+ }
+
+ .submit-button {
+ width: 100%;
+ padding: 18px;
+ font-size: 16px;
+ font-weight: 600;
+ background: ${cssManager.bdTheme('hsl(217 91% 60%)', 'hsl(217 91% 50%)')};
+ color: white;
+ border: none;
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ }
+
+ .submit-button:hover {
+ background: ${cssManager.bdTheme('hsl(217 91% 55%)', 'hsl(217 91% 45%)')};
+ }
+
+ .submit-button:active {
+ transform: scale(0.98);
+ }
+
+ /* QR Code */
+ .qr-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 24px;
+ }
+
+ .qr-code {
+ width: 220px;
+ height: 220px;
+ background: white;
+ border-radius: 16px;
+ padding: 20px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
+ }
+
+ .qr-code img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+
+ .qr-placeholder {
+ width: 100%;
+ height: 100%;
+ background: repeating-linear-gradient(
+ 45deg,
+ hsl(0 0% 92%),
+ hsl(0 0% 92%) 10px,
+ hsl(0 0% 88%) 10px,
+ hsl(0 0% 88%) 20px
+ );
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: hsl(0 0% 50%);
+ font-size: 14px;
+ }
+
+ .qr-instruction {
+ font-size: 14px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
+ text-align: center;
+ line-height: 1.6;
+ max-width: 280px;
+ }
+
+ /* Responsive */
+ @media (max-width: 800px) {
+ .login-container {
+ flex-direction: column;
+ }
+
+ .left-panel {
+ padding: 32px;
+ border-right: none;
+ border-bottom: 1px solid ${cssManager.bdTheme('hsl(220 15% 90%)', 'hsl(240 5% 16%)')};
+ }
+
+ .right-panel {
+ padding: 32px;
+ }
+
+ .branding {
+ margin-bottom: 32px;
+ }
+
+ .welcome-message {
+ font-size: 24px;
+ }
+
+ .method-selector {
+ flex-direction: row;
+ flex-wrap: wrap;
+ }
+
+ .method-option {
+ flex: 1;
+ min-width: 140px;
+ flex-direction: column;
+ text-align: center;
+ padding: 12px;
+ }
+
+ .method-info {
+ text-align: center;
+ }
+
+ .method-check {
+ display: none;
+ }
+ }
+ `,
+ ];
+
+ @property({ type: Object })
+ accessor config: ILoginConfig = {
+ allowedMethods: ['pin', 'password', 'qr'],
+ pinLength: 4,
+ welcomeMessage: 'Welcome',
+ subtitle: 'Sign in to continue',
+ };
+
+ @state()
+ accessor selectedMethod: TAuthMethod = 'pin';
+
+ @state()
+ accessor pinValue = '';
+
+ @state()
+ accessor passwordValue = '';
+
+ @state()
+ accessor showPassword = false;
+
+ @state()
+ accessor error = '';
+
+ @state()
+ accessor showError = false;
+
+ public render(): TemplateResult {
+ const effectivePinLength = this.config.pinLength || 4;
+
+ return html`
+
+
+
+ ${this.config.logoUrl
+ ? html`
`
+ : html` `
+ }
+ ${this.config.welcomeMessage || 'Welcome'}
+ ${this.config.subtitle || 'Sign in to continue'}
+
+
+ ${this.config.allowedMethods.length > 1 ? this.renderMethodSelector() : ''}
+
+
+
+
+ ${this.getAuthTitle()}
+ ${this.showError ? html`` : ''}
+ ${this.selectedMethod === 'pin' ? this.renderPinInput(effectivePinLength) : ''}
+ ${this.selectedMethod === 'password' ? this.renderPasswordInput() : ''}
+ ${this.selectedMethod === 'qr' ? this.renderQrCode() : ''}
+
+
+
+ `;
+ }
+
+ private getAuthTitle(): string {
+ switch (this.selectedMethod) {
+ case 'pin':
+ return 'Enter your PIN';
+ case 'password':
+ return 'Enter your password';
+ case 'qr':
+ return 'Scan to sign in';
+ default:
+ return 'Sign in';
+ }
+ }
+
+ private renderMethodSelector(): TemplateResult {
+ const methods: Array<{ id: TAuthMethod; icon: string; name: string; description: string }> = [
+ { id: 'pin', icon: 'lucide:keySquare', name: 'PIN Code', description: 'Quick numeric access' },
+ { id: 'password', icon: 'lucide:key', name: 'Password', description: 'Traditional password' },
+ { id: 'qr', icon: 'lucide:qrCode', name: 'QR Code', description: 'Scan with mobile app' },
+ ];
+
+ const availableMethods = methods.filter((m) => this.config.allowedMethods.includes(m.id));
+
+ return html`
+
+ Sign in method
+ ${availableMethods.map((method) => html`
+ this.selectMethod(method.id)}
+ >
+
+
+ ${method.name}
+ ${method.description}
+
+
+ ${this.selectedMethod === method.id
+ ? html` `
+ : ''
+ }
+
+
+ `)}
+
+ `;
+ }
+
+ private renderPinInput(length: number): TemplateResult {
+ return html`
+
+ ${Array.from({ length }, (_, i) => html`
+
+ `)}
+
+
+
+ ${[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => html`
+
+ `)}
+
+
+
+
+ `;
+ }
+
+ private renderPasswordInput(): TemplateResult {
+ return html`
+
+
+
+
+
+
+
+ `;
+ }
+
+ private renderQrCode(): TemplateResult {
+ return html`
+
+
+ ${this.config.qrCodeData
+ ? html`
`
+ : html`QR Code`
+ }
+
+
+ Open your authenticator app and scan this code to sign in securely without typing a password.
+
+
+ `;
+ }
+
+ private selectMethod(method: TAuthMethod): void {
+ this.selectedMethod = method;
+ this.clearError();
+ this.pinValue = '';
+ this.passwordValue = '';
+ }
+
+ private handlePinInput(digit: string): void {
+ this.clearError();
+ const maxLength = this.config.pinLength || 4;
+ if (this.pinValue.length < maxLength) {
+ this.pinValue += digit;
+ this.dispatchKeyPress(digit);
+ }
+ }
+
+ private handleBackspace(): void {
+ this.clearError();
+ if (this.pinValue.length > 0) {
+ this.pinValue = this.pinValue.slice(0, -1);
+ this.dispatchEvent(new CustomEvent('backspace', {
+ bubbles: true,
+ composed: true,
+ }));
+ }
+ }
+
+ private handlePinSubmit(): void {
+ if (this.pinValue.length === 0) {
+ this.showErrorMessage('Please enter your PIN');
+ return;
+ }
+
+ this.dispatchLoginAttempt('pin', this.pinValue);
+ }
+
+ private handlePasswordInput(e: InputEvent): void {
+ this.clearError();
+ const input = e.target as HTMLInputElement;
+ this.passwordValue = input.value;
+ }
+
+ private handlePasswordKeydown(e: KeyboardEvent): void {
+ if (e.key === 'Enter') {
+ this.handlePasswordSubmit();
+ }
+ }
+
+ private handlePasswordSubmit(): void {
+ if (this.passwordValue.length === 0) {
+ this.showErrorMessage('Please enter your password');
+ return;
+ }
+
+ this.dispatchLoginAttempt('password', this.passwordValue);
+ }
+
+ private togglePasswordVisibility(): void {
+ this.showPassword = !this.showPassword;
+ }
+
+ private dispatchKeyPress(key: string): void {
+ this.dispatchEvent(new CustomEvent('key-press', {
+ detail: { key },
+ bubbles: true,
+ composed: true,
+ }));
+ }
+
+ private dispatchLoginAttempt(method: TAuthMethod, value: string): void {
+ this.dispatchEvent(new CustomEvent('login-attempt', {
+ detail: { method, value } as ILoginCredentials,
+ bubbles: true,
+ composed: true,
+ }));
+ }
+
+ public showErrorMessage(message: string): void {
+ this.error = message;
+ this.showError = true;
+ }
+
+ public clearError(): void {
+ this.error = '';
+ this.showError = false;
+ }
+
+ public clearInput(): void {
+ this.pinValue = '';
+ this.passwordValue = '';
+ }
+}
diff --git a/ts_web/views/eco-view-login/index.ts b/ts_web/views/eco-view-login/index.ts
new file mode 100644
index 0000000..e078d7a
--- /dev/null
+++ b/ts_web/views/eco-view-login/index.ts
@@ -0,0 +1 @@
+export * from './eco-view-login.js';
diff --git a/ts_web/views/eco-view-peripherals/eco-view-peripherals.demo.ts b/ts_web/views/eco-view-peripherals/eco-view-peripherals.demo.ts
index 26ae86d..9bd18ed 100644
--- a/ts_web/views/eco-view-peripherals/eco-view-peripherals.demo.ts
+++ b/ts_web/views/eco-view-peripherals/eco-view-peripherals.demo.ts
@@ -4,18 +4,18 @@ export const demo = () => html`
- console.log('Device selected:', e.detail)}
@scan-start=${() => console.log('Scanning started')}
@scan-complete=${() => console.log('Scanning complete')}
- >
+ >
`;
diff --git a/ts_web/views/eco-view-peripherals/eco-view-peripherals.ts b/ts_web/views/eco-view-peripherals/eco-view-peripherals.ts
index f70df0c..c5e2884 100644
--- a/ts_web/views/eco-view-peripherals/eco-view-peripherals.ts
+++ b/ts_web/views/eco-view-peripherals/eco-view-peripherals.ts
@@ -9,8 +9,8 @@ import {
state,
} from '@design.estate/dees-element';
import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog';
-import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../interfaces/secondarymenu.js';
-import { demo } from './eco-peripherals.demo.js';
+import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js';
+import { demo } from './eco-view-peripherals.demo.js';
// Ensure components are registered
DeesAppuiSecondarymenu;
@@ -18,7 +18,7 @@ DeesIcon;
declare global {
interface HTMLElementTagNameMap {
- 'eco-peripherals': EcoPeripherals;
+ 'eco-view-peripherals': EcoViewPeripherals;
}
}
@@ -47,10 +47,10 @@ export interface IPeripheralDevice {
isDefault?: boolean;
}
-@customElement('eco-peripherals')
-export class EcoPeripherals extends DeesElement {
+@customElement('eco-view-peripherals')
+export class EcoViewPeripherals extends DeesElement {
public static demo = demo;
- public static demoGroup = 'App Launcher';
+ public static demoGroup = 'Views';
public static styles = [
cssManager.defaultStyles,
diff --git a/ts_web/views/eco-view-peripherals/index.ts b/ts_web/views/eco-view-peripherals/index.ts
index 030b4cb..3c6d074 100644
--- a/ts_web/views/eco-view-peripherals/index.ts
+++ b/ts_web/views/eco-view-peripherals/index.ts
@@ -1 +1 @@
-export * from './eco-peripherals.js';
+export * from './eco-view-peripherals.js';
diff --git a/ts_web/views/eco-view-saasshare/eco-view-saasshare.demo.ts b/ts_web/views/eco-view-saasshare/eco-view-saasshare.demo.ts
new file mode 100644
index 0000000..2f7e663
--- /dev/null
+++ b/ts_web/views/eco-view-saasshare/eco-view-saasshare.demo.ts
@@ -0,0 +1,20 @@
+import { html } from '@design.estate/dees-element';
+
+export const demo = () => html`
+
+
+ console.log('Request approved:', e.detail)}
+ @request-denied=${(e: CustomEvent) => console.log('Request denied:', e.detail)}
+ >
+
+`;
diff --git a/ts_web/views/eco-view-saasshare/eco-view-saasshare.ts b/ts_web/views/eco-view-saasshare/eco-view-saasshare.ts
new file mode 100644
index 0000000..5b289d3
--- /dev/null
+++ b/ts_web/views/eco-view-saasshare/eco-view-saasshare.ts
@@ -0,0 +1,1288 @@
+import {
+ customElement,
+ DeesElement,
+ type TemplateResult,
+ html,
+ property,
+ css,
+ cssManager,
+ state,
+} from '@design.estate/dees-element';
+import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog';
+import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js';
+import { demo } from './eco-view-saasshare.demo.js';
+
+// Ensure components are registered
+DeesAppuiSecondarymenu;
+DeesIcon;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'eco-view-saasshare': EcoViewSaasshare;
+ }
+}
+
+export type TSharePanel =
+ | 'apps'
+ | 'devices'
+ | 'permissions'
+ | 'requests'
+ | 'activity'
+ | 'security';
+
+export type TPermissionType =
+ | 'print'
+ | 'scan'
+ | 'storage'
+ | 'camera'
+ | 'audio'
+ | 'display'
+ | 'network';
+
+export interface ISaasApp {
+ id: string;
+ name: string;
+ domain: string;
+ icon?: string;
+ color?: string;
+ verified: boolean;
+ lastAccess?: Date;
+ permissions: ISaasPermission[];
+}
+
+export interface ISaasPermission {
+ type: TPermissionType;
+ deviceId?: string;
+ deviceName?: string;
+ granted: boolean;
+ grantedAt?: Date;
+ expiresAt?: Date;
+}
+
+export interface IAccessRequest {
+ id: string;
+ appId: string;
+ appName: string;
+ appDomain: string;
+ permissionType: TPermissionType;
+ deviceId?: string;
+ deviceName?: string;
+ requestedAt: Date;
+ status: 'pending' | 'approved' | 'denied';
+}
+
+export interface IAccessActivity {
+ id: string;
+ appId: string;
+ appName: string;
+ permissionType: TPermissionType;
+ deviceName?: string;
+ action: string;
+ timestamp: Date;
+}
+
+@customElement('eco-view-saasshare')
+export class EcoViewSaasshare extends DeesElement {
+ public static demo = demo;
+ public static demoGroup = 'Views';
+
+ public static styles = [
+ cssManager.defaultStyles,
+ css`
+ :host {
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: ${cssManager.bdTheme('#f5f5f7', 'hsl(240 6% 10%)')};
+ color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ }
+
+ .share-container {
+ display: flex;
+ height: 100%;
+ }
+
+ dees-appui-secondarymenu {
+ flex-shrink: 0;
+ background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')};
+ border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')};
+ }
+
+ .content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 32px 48px;
+ }
+
+ .panel-header {
+ margin-bottom: 32px;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ }
+
+ .panel-header-left {
+ flex: 1;
+ }
+
+ .panel-title {
+ font-size: 28px;
+ font-weight: 600;
+ color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
+ margin-bottom: 8px;
+ }
+
+ .panel-description {
+ font-size: 14px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
+ }
+
+ .header-action {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 10px 20px;
+ background: hsl(217 91% 60%);
+ color: white;
+ border: none;
+ border-radius: 10px;
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: background 0.15s ease;
+ }
+
+ .header-action:hover {
+ background: hsl(217 91% 55%);
+ }
+
+ .section {
+ background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
+ border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
+ border-radius: 12px;
+ margin-bottom: 24px;
+ overflow: hidden;
+ }
+
+ .section-title {
+ padding: 16px 20px 12px;
+ font-size: 13px;
+ font-weight: 600;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .section-count {
+ background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
+ padding: 2px 8px;
+ border-radius: 10px;
+ font-size: 12px;
+ font-weight: 600;
+ }
+
+ .app-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 20px;
+ border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
+ transition: background 0.15s ease;
+ cursor: pointer;
+ }
+
+ .app-item:first-child {
+ border-top: none;
+ }
+
+ .app-item:hover {
+ background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')};
+ }
+
+ .app-left {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ }
+
+ .app-icon {
+ width: 44px;
+ height: 44px;
+ border-radius: 10px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ font-size: 20px;
+ font-weight: 600;
+ }
+
+ .app-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .app-name {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 15px;
+ font-weight: 500;
+ color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
+ }
+
+ .verified-badge {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 8px;
+ background: hsl(142 71% 45% / 0.15);
+ color: hsl(142 71% 40%);
+ border-radius: 4px;
+ font-size: 11px;
+ font-weight: 600;
+ }
+
+ .app-domain {
+ font-size: 13px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
+ }
+
+ .app-right {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ }
+
+ .permission-badges {
+ display: flex;
+ gap: 6px;
+ }
+
+ .permission-badge {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+ border-radius: 6px;
+ background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')};
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
+ }
+
+ .permission-badge.active {
+ background: hsl(217 91% 60% / 0.15);
+ color: hsl(217 91% 55%);
+ }
+
+ .last-access {
+ font-size: 12px;
+ color: ${cssManager.bdTheme('hsl(0 0% 60%)', 'hsl(0 0% 50%)')};
+ min-width: 100px;
+ text-align: right;
+ }
+
+ .request-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 16px 20px;
+ border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
+ background: ${cssManager.bdTheme('hsl(45 100% 97%)', 'hsl(45 30% 12%)')};
+ }
+
+ .request-item:first-child {
+ border-top: none;
+ }
+
+ .request-info {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ }
+
+ .request-title {
+ font-size: 14px;
+ font-weight: 500;
+ color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
+ }
+
+ .request-detail {
+ font-size: 13px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
+ }
+
+ .request-actions {
+ display: flex;
+ gap: 8px;
+ }
+
+ .btn {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 16px;
+ border: none;
+ border-radius: 8px;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.15s ease;
+ }
+
+ .btn-approve {
+ background: hsl(142 71% 45%);
+ color: white;
+ }
+
+ .btn-approve:hover {
+ background: hsl(142 71% 40%);
+ }
+
+ .btn-deny {
+ background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
+ color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
+ }
+
+ .btn-deny:hover {
+ background: hsl(0 72% 51%);
+ color: white;
+ }
+
+ .device-group {
+ margin-bottom: 24px;
+ }
+
+ .device-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px 20px;
+ background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')};
+ border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
+ }
+
+ .device-icon {
+ width: 36px;
+ height: 36px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 20%)')};
+ color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 70%)')};
+ }
+
+ .device-info h4 {
+ margin: 0;
+ font-size: 15px;
+ font-weight: 500;
+ color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
+ }
+
+ .device-info span {
+ font-size: 13px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
+ }
+
+ .device-apps {
+ padding: 0;
+ }
+
+ .device-app {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 20px 12px 68px;
+ border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
+ }
+
+ .device-app:first-child {
+ border-top: none;
+ }
+
+ .device-app-name {
+ font-size: 14px;
+ color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 80%)')};
+ }
+
+ .toggle-switch {
+ position: relative;
+ width: 44px;
+ height: 24px;
+ background: ${cssManager.bdTheme('hsl(0 0% 85%)', 'hsl(240 5% 25%)')};
+ border-radius: 12px;
+ cursor: pointer;
+ transition: background 0.2s ease;
+ }
+
+ .toggle-switch.active {
+ background: hsl(217 91% 60%);
+ }
+
+ .toggle-switch::after {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 20px;
+ height: 20px;
+ background: white;
+ border-radius: 50%;
+ transition: transform 0.2s ease;
+ box-shadow: ${cssManager.bdTheme('0 1px 3px rgba(0,0,0,0.2)', 'none')};
+ }
+
+ .toggle-switch.active::after {
+ transform: translateX(20px);
+ }
+
+ .activity-item {
+ display: flex;
+ align-items: flex-start;
+ gap: 14px;
+ padding: 14px 20px;
+ border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
+ }
+
+ .activity-item:first-child {
+ border-top: none;
+ }
+
+ .activity-icon {
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')};
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
+ flex-shrink: 0;
+ }
+
+ .activity-content {
+ flex: 1;
+ }
+
+ .activity-title {
+ font-size: 14px;
+ color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 90%)')};
+ margin-bottom: 2px;
+ }
+
+ .activity-title strong {
+ font-weight: 600;
+ }
+
+ .activity-time {
+ font-size: 12px;
+ color: ${cssManager.bdTheme('hsl(0 0% 55%)', 'hsl(0 0% 50%)')};
+ }
+
+ .settings-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 20px;
+ border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
+ }
+
+ .settings-item:first-child {
+ border-top: none;
+ }
+
+ .item-left {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ }
+
+ .item-icon {
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: white;
+ }
+
+ .item-icon.blue { background: hsl(217 91% 60%); }
+ .item-icon.green { background: hsl(142 71% 45%); }
+ .item-icon.orange { background: hsl(25 95% 53%); }
+ .item-icon.red { background: hsl(0 72% 51%); }
+ .item-icon.purple { background: hsl(262 83% 58%); }
+ .item-icon.gray { background: hsl(220 9% 46%); }
+
+ .item-info {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ }
+
+ .item-label {
+ font-size: 15px;
+ font-weight: 500;
+ color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
+ }
+
+ .item-sublabel {
+ font-size: 13px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
+ }
+
+ .empty-state {
+ text-align: center;
+ padding: 48px 20px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
+ }
+
+ .empty-state dees-icon {
+ margin-bottom: 16px;
+ opacity: 0.5;
+ }
+
+ .empty-state h3 {
+ margin: 0 0 8px;
+ font-size: 16px;
+ font-weight: 500;
+ color: ${cssManager.bdTheme('hsl(0 0% 30%)', 'hsl(0 0% 70%)')};
+ }
+
+ .empty-state p {
+ margin: 0;
+ font-size: 14px;
+ }
+
+ .permission-type-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 20px;
+ border-top: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
+ }
+
+ .permission-type-row:first-child {
+ border-top: none;
+ }
+
+ .permission-type-info {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ }
+
+ .permission-type-details h4 {
+ margin: 0;
+ font-size: 15px;
+ font-weight: 500;
+ color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
+ }
+
+ .permission-type-details span {
+ font-size: 13px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
+ }
+
+ .permission-apps-count {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 12px;
+ background: ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 18%)')};
+ border-radius: 6px;
+ font-size: 13px;
+ color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 65%)')};
+ cursor: pointer;
+ transition: background 0.15s ease;
+ }
+
+ .permission-apps-count:hover {
+ background: ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 22%)')};
+ }
+ `,
+ ];
+
+ @property({ type: String })
+ accessor activePanel: TSharePanel = 'apps';
+
+ @state()
+ accessor saasApps: ISaasApp[] = [
+ {
+ id: 'google-docs',
+ name: 'Google Docs',
+ domain: 'docs.google.com',
+ color: '#4285F4',
+ verified: true,
+ lastAccess: new Date(Date.now() - 1000 * 60 * 5),
+ permissions: [
+ { type: 'print', deviceName: 'HP LaserJet Pro', granted: true },
+ { type: 'storage', deviceName: 'Synology NAS', granted: true },
+ ],
+ },
+ {
+ id: 'figma',
+ name: 'Figma',
+ domain: 'figma.com',
+ color: '#F24E1E',
+ verified: true,
+ lastAccess: new Date(Date.now() - 1000 * 60 * 30),
+ permissions: [
+ { type: 'display', granted: true },
+ ],
+ },
+ {
+ id: 'zoom',
+ name: 'Zoom',
+ domain: 'zoom.us',
+ color: '#2D8CFF',
+ verified: true,
+ lastAccess: new Date(Date.now() - 1000 * 60 * 60 * 2),
+ permissions: [
+ { type: 'camera', deviceName: 'Logitech C920', granted: true },
+ { type: 'audio', deviceName: 'Built-in Microphone', granted: true },
+ { type: 'display', granted: true },
+ ],
+ },
+ {
+ id: 'notion',
+ name: 'Notion',
+ domain: 'notion.so',
+ color: '#000000',
+ verified: true,
+ lastAccess: new Date(Date.now() - 1000 * 60 * 60 * 24),
+ permissions: [
+ { type: 'print', deviceName: 'HP LaserJet Pro', granted: true },
+ ],
+ },
+ {
+ id: 'dropbox',
+ name: 'Dropbox',
+ domain: 'dropbox.com',
+ color: '#0061FF',
+ verified: true,
+ lastAccess: new Date(Date.now() - 1000 * 60 * 60 * 48),
+ permissions: [
+ { type: 'storage', deviceName: 'Synology NAS', granted: true },
+ { type: 'scan', deviceName: 'Epson Scanner', granted: true },
+ ],
+ },
+ ];
+
+ @state()
+ accessor accessRequests: IAccessRequest[] = [
+ {
+ id: 'req-1',
+ appId: 'slack',
+ appName: 'Slack',
+ appDomain: 'slack.com',
+ permissionType: 'camera',
+ deviceName: 'Logitech C920',
+ requestedAt: new Date(Date.now() - 1000 * 60 * 10),
+ status: 'pending',
+ },
+ {
+ id: 'req-2',
+ appId: 'canva',
+ appName: 'Canva',
+ appDomain: 'canva.com',
+ permissionType: 'print',
+ deviceName: 'HP LaserJet Pro',
+ requestedAt: new Date(Date.now() - 1000 * 60 * 25),
+ status: 'pending',
+ },
+ ];
+
+ @state()
+ accessor activities: IAccessActivity[] = [
+ {
+ id: 'act-1',
+ appId: 'google-docs',
+ appName: 'Google Docs',
+ permissionType: 'print',
+ deviceName: 'HP LaserJet Pro',
+ action: 'printed document "Q4 Report.pdf"',
+ timestamp: new Date(Date.now() - 1000 * 60 * 5),
+ },
+ {
+ id: 'act-2',
+ appId: 'zoom',
+ appName: 'Zoom',
+ permissionType: 'camera',
+ deviceName: 'Logitech C920',
+ action: 'accessed camera for video call',
+ timestamp: new Date(Date.now() - 1000 * 60 * 60 * 2),
+ },
+ {
+ id: 'act-3',
+ appId: 'dropbox',
+ appName: 'Dropbox',
+ permissionType: 'scan',
+ deviceName: 'Epson Scanner',
+ action: 'scanned 3 pages',
+ timestamp: new Date(Date.now() - 1000 * 60 * 60 * 5),
+ },
+ {
+ id: 'act-4',
+ appId: 'figma',
+ appName: 'Figma',
+ permissionType: 'display',
+ action: 'shared screen to external display',
+ timestamp: new Date(Date.now() - 1000 * 60 * 60 * 24),
+ },
+ ];
+
+ @state()
+ accessor requireApproval = true;
+
+ @state()
+ accessor autoRevokeInactive = true;
+
+ @state()
+ accessor activityLogging = true;
+
+ private getMenuGroups(): ISecondaryMenuGroup[] {
+ const pendingCount = this.accessRequests.filter(r => r.status === 'pending').length;
+
+ return [
+ {
+ name: 'Overview',
+ iconName: 'lucide:share2',
+ items: [
+ {
+ key: 'apps',
+ iconName: 'lucide:layoutGrid',
+ action: () => this.activePanel = 'apps',
+ badge: this.saasApps.length.toString(),
+ },
+ {
+ key: 'requests',
+ iconName: 'lucide:inbox',
+ action: () => this.activePanel = 'requests',
+ badge: pendingCount > 0 ? pendingCount.toString() : undefined,
+ },
+ ],
+ },
+ {
+ name: 'Browse',
+ iconName: 'lucide:folder',
+ items: [
+ {
+ key: 'devices',
+ iconName: 'lucide:hardDrive',
+ action: () => this.activePanel = 'devices',
+ },
+ {
+ key: 'permissions',
+ iconName: 'lucide:key',
+ action: () => this.activePanel = 'permissions',
+ },
+ ],
+ },
+ {
+ name: 'Monitor',
+ iconName: 'lucide:activity',
+ items: [
+ {
+ key: 'activity',
+ iconName: 'lucide:clock',
+ action: () => this.activePanel = 'activity',
+ },
+ {
+ key: 'security',
+ iconName: 'lucide:shield',
+ action: () => this.activePanel = 'security',
+ },
+ ],
+ },
+ ];
+ }
+
+ private getSelectedItem(): ISecondaryMenuItem | null {
+ for (const group of this.getMenuGroups()) {
+ for (const item of group.items) {
+ if ('key' in item && item.key === this.activePanel) {
+ return item;
+ }
+ }
+ }
+ return null;
+ }
+
+ public render(): TemplateResult {
+ return html`
+
+ `;
+ }
+
+ private renderActivePanel(): TemplateResult {
+ switch (this.activePanel) {
+ case 'apps':
+ return this.renderAppsPanel();
+ case 'devices':
+ return this.renderDevicesPanel();
+ case 'permissions':
+ return this.renderPermissionsPanel();
+ case 'requests':
+ return this.renderRequestsPanel();
+ case 'activity':
+ return this.renderActivityPanel();
+ case 'security':
+ return this.renderSecurityPanel();
+ default:
+ return this.renderAppsPanel();
+ }
+ }
+
+ private renderAppsPanel(): TemplateResult {
+ return html`
+
+
+ Connected Apps
+ Manage SaaS applications with access to your peripherals
+
+
+
+
+ ${this.accessRequests.filter(r => r.status === 'pending').length > 0 ? html`
+
+
+ Pending Requests
+ ${this.accessRequests.filter(r => r.status === 'pending').length}
+
+ ${this.accessRequests.filter(r => r.status === 'pending').map(request => html`
+
+
+
+ ${request.appName} wants access to ${this.getPermissionLabel(request.permissionType)}
+
+
+ ${request.deviceName ? `Device: ${request.deviceName} • ` : ''}
+ Requested ${this.formatTimeAgo(request.requestedAt)}
+
+
+
+
+
+
+
+ `)}
+
+ ` : ''}
+
+
+
+ All Apps
+ ${this.saasApps.length}
+
+ ${this.saasApps.map(app => html`
+
+
+
+
+
+ ${app.name}
+ ${app.verified ? html`
+
+
+ Verified
+
+ ` : ''}
+
+ ${app.domain}
+
+
+
+
+
+ ${app.lastAccess ? this.formatTimeAgo(app.lastAccess) : 'Never'}
+
+
+
+
+ `)}
+
+ `;
+ }
+
+ private renderDevicesPanel(): TemplateResult {
+ const devices = [
+ {
+ name: 'HP LaserJet Pro',
+ type: 'print',
+ icon: 'lucide:printer',
+ apps: ['Google Docs', 'Notion'],
+ },
+ {
+ name: 'Epson Scanner',
+ type: 'scan',
+ icon: 'lucide:scan',
+ apps: ['Dropbox'],
+ },
+ {
+ name: 'Logitech C920',
+ type: 'camera',
+ icon: 'lucide:camera',
+ apps: ['Zoom'],
+ },
+ {
+ name: 'Built-in Microphone',
+ type: 'audio',
+ icon: 'lucide:mic',
+ apps: ['Zoom'],
+ },
+ {
+ name: 'Synology NAS',
+ type: 'storage',
+ icon: 'lucide:hardDrive',
+ apps: ['Google Docs', 'Dropbox'],
+ },
+ {
+ name: 'External Display',
+ type: 'display',
+ icon: 'lucide:monitor',
+ apps: ['Zoom', 'Figma'],
+ },
+ ];
+
+ return html`
+
+
+ Devices
+ View which apps have access to each peripheral
+
+
+
+ ${devices.map(device => html`
+
+
+
+
+ ${device.name}
+ ${device.apps.length} app${device.apps.length !== 1 ? 's' : ''} with access
+
+
+
+ ${device.apps.map(appName => html`
+
+ ${appName}
+
+
+ `)}
+
+
+ `)}
+ `;
+ }
+
+ private renderPermissionsPanel(): TemplateResult {
+ const permissionTypes: { type: TPermissionType; icon: string; label: string; color: string; count: number }[] = [
+ { type: 'print', icon: 'lucide:printer', label: 'Printing', color: 'blue', count: 2 },
+ { type: 'scan', icon: 'lucide:scan', label: 'Scanning', color: 'purple', count: 1 },
+ { type: 'camera', icon: 'lucide:camera', label: 'Camera', color: 'green', count: 1 },
+ { type: 'audio', icon: 'lucide:mic', label: 'Microphone', color: 'red', count: 1 },
+ { type: 'storage', icon: 'lucide:hardDrive', label: 'Network Storage', color: 'orange', count: 2 },
+ { type: 'display', icon: 'lucide:monitor', label: 'Screen Sharing', color: 'gray', count: 2 },
+ ];
+
+ return html`
+
+
+ Permissions
+ Manage access by permission type
+
+
+
+
+ ${permissionTypes.map(perm => html`
+
+ `)}
+
+ `;
+ }
+
+ private renderRequestsPanel(): TemplateResult {
+ const pendingRequests = this.accessRequests.filter(r => r.status === 'pending');
+
+ return html`
+
+
+ Access Requests
+ Review and manage permission requests from SaaS applications
+
+
+
+ ${pendingRequests.length > 0 ? html`
+
+
+ Pending
+ ${pendingRequests.length}
+
+ ${pendingRequests.map(request => html`
+
+
+
+ ${request.appName} wants access to ${this.getPermissionLabel(request.permissionType)}
+
+
+ ${request.deviceName ? `Device: ${request.deviceName} • ` : ''}
+ ${request.appDomain} • Requested ${this.formatTimeAgo(request.requestedAt)}
+
+
+
+
+
+
+
+ `)}
+
+ ` : html`
+
+
+
+ No pending requests
+ When SaaS apps request access to your peripherals, they'll appear here
+
+
+ `}
+ `;
+ }
+
+ private renderActivityPanel(): TemplateResult {
+ return html`
+
+
+ Activity Log
+ Recent peripheral access by SaaS applications
+
+
+
+
+ Recent Activity
+ ${this.activities.map(activity => html`
+
+
+
+
+ ${activity.appName} ${activity.action}
+
+ ${this.formatTimeAgo(activity.timestamp)}
+
+
+ `)}
+
+ `;
+ }
+
+ private renderSecurityPanel(): TemplateResult {
+ return html`
+
+
+ Security Settings
+ Configure how SaaS apps can access your peripherals
+
+
+
+
+ Access Control
+
+
+
+
+ Require Approval
+ Ask before granting new app access
+
+
+ this.requireApproval = !this.requireApproval}
+ >
+
+
+
+
+
+ Auto-revoke Inactive Apps
+ Remove access after 30 days of inactivity
+
+
+ this.autoRevokeInactive = !this.autoRevokeInactive}
+ >
+
+
+
+
+ Logging
+
+
+
+
+ Activity Logging
+ Record all peripheral access events
+
+
+ this.activityLogging = !this.activityLogging}
+ >
+
+
+
+
+ Danger Zone
+
+
+
+
+ Revoke All Access
+ Remove all SaaS app permissions
+
+
+
+
+
+ `;
+ }
+
+ private renderPermissionBadges(permissions: ISaasPermission[]): TemplateResult[] {
+ const permissionIcons: Record = {
+ print: 'lucide:printer',
+ scan: 'lucide:scan',
+ storage: 'lucide:hardDrive',
+ camera: 'lucide:camera',
+ audio: 'lucide:mic',
+ display: 'lucide:monitor',
+ network: 'lucide:wifi',
+ };
+
+ return permissions.map(perm => html`
+
+ `);
+ }
+
+ private getPermissionLabel(type: TPermissionType): string {
+ const labels: Record = {
+ print: 'Printing',
+ scan: 'Scanning',
+ storage: 'Storage',
+ camera: 'Camera',
+ audio: 'Microphone',
+ display: 'Display',
+ network: 'Network',
+ };
+ return labels[type];
+ }
+
+ private getPermissionIcon(type: TPermissionType): string {
+ const icons: Record = {
+ print: 'lucide:printer',
+ scan: 'lucide:scan',
+ storage: 'lucide:hardDrive',
+ camera: 'lucide:camera',
+ audio: 'lucide:mic',
+ display: 'lucide:monitor',
+ network: 'lucide:wifi',
+ };
+ return icons[type];
+ }
+
+ private formatTimeAgo(date: Date): string {
+ const now = new Date();
+ const diff = now.getTime() - date.getTime();
+ const minutes = Math.floor(diff / 60000);
+ const hours = Math.floor(diff / 3600000);
+ const days = Math.floor(diff / 86400000);
+
+ if (minutes < 1) return 'Just now';
+ if (minutes < 60) return `${minutes}m ago`;
+ if (hours < 24) return `${hours}h ago`;
+ if (days < 7) return `${days}d ago`;
+ return date.toLocaleDateString();
+ }
+
+ private approveRequest(requestId: string): void {
+ this.accessRequests = this.accessRequests.map(r =>
+ r.id === requestId ? { ...r, status: 'approved' as const } : r
+ );
+ this.dispatchEvent(new CustomEvent('request-approved', {
+ detail: { requestId },
+ bubbles: true,
+ composed: true,
+ }));
+ }
+
+ private denyRequest(requestId: string): void {
+ this.accessRequests = this.accessRequests.map(r =>
+ r.id === requestId ? { ...r, status: 'denied' as const } : r
+ );
+ this.dispatchEvent(new CustomEvent('request-denied', {
+ detail: { requestId },
+ bubbles: true,
+ composed: true,
+ }));
+ }
+}
diff --git a/ts_web/views/eco-view-saasshare/index.ts b/ts_web/views/eco-view-saasshare/index.ts
new file mode 100644
index 0000000..421a95e
--- /dev/null
+++ b/ts_web/views/eco-view-saasshare/index.ts
@@ -0,0 +1 @@
+export * from './eco-view-saasshare.js';
diff --git a/ts_web/views/eco-view-settings/eco-view-settings.demo.ts b/ts_web/views/eco-view-settings/eco-view-settings.demo.ts
index 5acdb55..66f1a11 100644
--- a/ts_web/views/eco-view-settings/eco-view-settings.demo.ts
+++ b/ts_web/views/eco-view-settings/eco-view-settings.demo.ts
@@ -4,15 +4,15 @@ export const demo = () => html`
-
+ >
`;
diff --git a/ts_web/views/eco-view-settings/eco-view-settings.ts b/ts_web/views/eco-view-settings/eco-view-settings.ts
index 4cbf7ae..b55bf3f 100644
--- a/ts_web/views/eco-view-settings/eco-view-settings.ts
+++ b/ts_web/views/eco-view-settings/eco-view-settings.ts
@@ -9,8 +9,8 @@ import {
state,
} from '@design.estate/dees-element';
import { DeesAppuiSecondarymenu, DeesIcon } from '@design.estate/dees-catalog';
-import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../interfaces/secondarymenu.js';
-import { demo } from './eco-settings.demo.js';
+import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js';
+import { demo } from './eco-view-settings.demo.js';
// Ensure components are registered
DeesAppuiSecondarymenu;
@@ -18,7 +18,7 @@ DeesIcon;
declare global {
interface HTMLElementTagNameMap {
- 'eco-settings': EcoSettings;
+ 'eco-view-settings': EcoViewSettings;
}
}
@@ -35,10 +35,10 @@ export type TSettingsPanel =
| 'updates'
| 'about';
-@customElement('eco-settings')
-export class EcoSettings extends DeesElement {
+@customElement('eco-view-settings')
+export class EcoViewSettings extends DeesElement {
public static demo = demo;
- public static demoGroup = 'App Launcher';
+ public static demoGroup = 'Views';
public static styles = [
cssManager.defaultStyles,
diff --git a/ts_web/views/eco-view-settings/index.ts b/ts_web/views/eco-view-settings/index.ts
index 1b6599f..68b38de 100644
--- a/ts_web/views/eco-view-settings/index.ts
+++ b/ts_web/views/eco-view-settings/index.ts
@@ -1 +1 @@
-export * from './eco-settings.js';
+export * from './eco-view-settings.js';
diff --git a/ts_web/views/eco-view-system/eco-view-system.demo.ts b/ts_web/views/eco-view-system/eco-view-system.demo.ts
new file mode 100644
index 0000000..f93ae36
--- /dev/null
+++ b/ts_web/views/eco-view-system/eco-view-system.demo.ts
@@ -0,0 +1,18 @@
+import { html } from '@design.estate/dees-element';
+
+export const demo = () => html`
+
+
+
+
+`;
diff --git a/ts_web/views/eco-view-system/eco-view-system.ts b/ts_web/views/eco-view-system/eco-view-system.ts
new file mode 100644
index 0000000..ec826f0
--- /dev/null
+++ b/ts_web/views/eco-view-system/eco-view-system.ts
@@ -0,0 +1,877 @@
+import {
+ customElement,
+ DeesElement,
+ type TemplateResult,
+ html,
+ property,
+ css,
+ cssManager,
+ state,
+} from '@design.estate/dees-element';
+import { DeesAppuiSecondarymenu, DeesIcon, DeesStatsGrid } from '@design.estate/dees-catalog';
+import type { ISecondaryMenuGroup, ISecondaryMenuItem } from '../../elements/interfaces/secondarymenu.js';
+import { demo } from './eco-view-system.demo.js';
+
+// Ensure components are registered
+DeesAppuiSecondarymenu;
+DeesIcon;
+DeesStatsGrid;
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'eco-view-system': EcoViewSystem;
+ }
+}
+
+export type TSystemPanel =
+ | 'overview'
+ | 'cpu'
+ | 'memory'
+ | 'storage'
+ | 'network'
+ | 'processes';
+
+@customElement('eco-view-system')
+export class EcoViewSystem extends DeesElement {
+ public static demo = demo;
+ public static demoGroup = 'Views';
+
+ public static styles = [
+ cssManager.defaultStyles,
+ css`
+ :host {
+ display: block;
+ width: 100%;
+ height: 100%;
+ background: ${cssManager.bdTheme('#f5f5f7', 'hsl(240 6% 10%)')};
+ color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ }
+
+ .system-container {
+ display: flex;
+ height: 100%;
+ }
+
+ dees-appui-secondarymenu {
+ flex-shrink: 0;
+ background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 8%)')};
+ border-right: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 15%)')};
+ }
+
+ .content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 32px 48px;
+ }
+
+ .panel-header {
+ margin-bottom: 32px;
+ }
+
+ .panel-title {
+ font-size: 28px;
+ font-weight: 600;
+ color: ${cssManager.bdTheme('hsl(0 0% 10%)', 'hsl(0 0% 98%)')};
+ margin-bottom: 8px;
+ }
+
+ .panel-description {
+ font-size: 14px;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 60%)')};
+ }
+
+ .stats-section {
+ margin-bottom: 32px;
+ }
+
+ .section-title {
+ font-size: 13px;
+ font-weight: 600;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 50%)')};
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ margin-bottom: 16px;
+ }
+
+ dees-statsgrid {
+ --dees-statsgrid-gap: 16px;
+ }
+
+ .process-list {
+ background: ${cssManager.bdTheme('#ffffff', 'hsl(240 6% 12%)')};
+ border: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
+ border-radius: 12px;
+ overflow: hidden;
+ }
+
+ .process-header {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1fr 1fr;
+ padding: 12px 16px;
+ background: ${cssManager.bdTheme('hsl(0 0% 97%)', 'hsl(240 5% 14%)')};
+ border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 90%)', 'hsl(240 5% 18%)')};
+ font-size: 12px;
+ font-weight: 600;
+ color: ${cssManager.bdTheme('hsl(0 0% 50%)', 'hsl(0 0% 55%)')};
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ }
+
+ .process-row {
+ display: grid;
+ grid-template-columns: 2fr 1fr 1fr 1fr;
+ padding: 12px 16px;
+ border-bottom: 1px solid ${cssManager.bdTheme('hsl(0 0% 94%)', 'hsl(240 5% 15%)')};
+ font-size: 14px;
+ color: ${cssManager.bdTheme('hsl(0 0% 20%)', 'hsl(0 0% 85%)')};
+ }
+
+ .process-row:last-child {
+ border-bottom: none;
+ }
+
+ .process-name {
+ font-weight: 500;
+ }
+
+ .process-value {
+ color: ${cssManager.bdTheme('hsl(0 0% 40%)', 'hsl(0 0% 65%)')};
+ }
+
+ .process-value.high {
+ color: hsl(0 84% 60%);
+ font-weight: 500;
+ }
+ `,
+ ];
+
+ @property({ type: String })
+ accessor activePanel: TSystemPanel = 'overview';
+
+ // Mock system data
+ @state()
+ accessor cpuUsage = 42;
+
+ @state()
+ accessor memoryUsage = 67;
+
+ @state()
+ accessor diskUsage = 54;
+
+ @state()
+ accessor cpuTemp = 58;
+
+ @state()
+ accessor uptime = '14d 7h 32m';
+
+ @state()
+ accessor networkIn = [45, 52, 38, 65, 72, 68, 75, 82, 79, 85, 88, 72];
+
+ @state()
+ accessor networkOut = [32, 28, 35, 42, 38, 45, 52, 48, 55, 62, 58, 65];
+
+ private getMenuGroups(): ISecondaryMenuGroup[] {
+ return [
+ {
+ name: 'Monitor',
+ iconName: 'lucide:activity',
+ items: [
+ {
+ key: 'overview',
+ iconName: 'lucide:layoutDashboard',
+ action: () => this.activePanel = 'overview',
+ },
+ ],
+ },
+ {
+ name: 'Hardware',
+ iconName: 'lucide:cpu',
+ items: [
+ {
+ key: 'cpu',
+ iconName: 'lucide:cpu',
+ action: () => this.activePanel = 'cpu',
+ },
+ {
+ key: 'memory',
+ iconName: 'lucide:memoryStick',
+ action: () => this.activePanel = 'memory',
+ },
+ {
+ key: 'storage',
+ iconName: 'lucide:hardDrive',
+ action: () => this.activePanel = 'storage',
+ },
+ ],
+ },
+ {
+ name: 'Network',
+ iconName: 'lucide:network',
+ items: [
+ {
+ key: 'network',
+ iconName: 'lucide:wifi',
+ action: () => this.activePanel = 'network',
+ },
+ ],
+ },
+ {
+ name: 'Software',
+ iconName: 'lucide:layers',
+ items: [
+ {
+ key: 'processes',
+ iconName: 'lucide:listTree',
+ action: () => this.activePanel = 'processes',
+ },
+ ],
+ },
+ ];
+ }
+
+ private getSelectedItem(): ISecondaryMenuItem | null {
+ for (const group of this.getMenuGroups()) {
+ for (const item of group.items) {
+ if ('key' in item && item.key === this.activePanel) {
+ return item;
+ }
+ }
+ }
+ return null;
+ }
+
+ public render(): TemplateResult {
+ return html`
+
+
+
+ ${this.renderActivePanel()}
+
+
+ `;
+ }
+
+ private renderActivePanel(): TemplateResult {
+ switch (this.activePanel) {
+ case 'overview':
+ return this.renderOverviewPanel();
+ case 'cpu':
+ return this.renderCpuPanel();
+ case 'memory':
+ return this.renderMemoryPanel();
+ case 'storage':
+ return this.renderStoragePanel();
+ case 'network':
+ return this.renderNetworkPanel();
+ case 'processes':
+ return this.renderProcessesPanel();
+ default:
+ return this.renderOverviewPanel();
+ }
+ }
+
+ private renderOverviewPanel(): TemplateResult {
+ const overviewTiles = [
+ {
+ id: 'cpu',
+ title: 'CPU Usage',
+ value: this.cpuUsage,
+ type: 'gauge' as const,
+ icon: 'lucide:cpu',
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(142 71% 45%)' },
+ { value: 60, color: 'hsl(45 93% 47%)' },
+ { value: 80, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ },
+ {
+ id: 'memory',
+ title: 'Memory Usage',
+ value: this.memoryUsage,
+ type: 'gauge' as const,
+ icon: 'lucide:memoryStick',
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(142 71% 45%)' },
+ { value: 70, color: 'hsl(45 93% 47%)' },
+ { value: 85, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ },
+ {
+ id: 'disk',
+ title: 'Disk Usage',
+ value: this.diskUsage,
+ type: 'gauge' as const,
+ icon: 'lucide:hardDrive',
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(142 71% 45%)' },
+ { value: 75, color: 'hsl(45 93% 47%)' },
+ { value: 90, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ },
+ {
+ id: 'temp',
+ title: 'CPU Temp',
+ value: this.cpuTemp,
+ unit: '°C',
+ type: 'gauge' as const,
+ icon: 'lucide:thermometer',
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(217 91% 60%)' },
+ { value: 50, color: 'hsl(142 71% 45%)' },
+ { value: 70, color: 'hsl(45 93% 47%)' },
+ { value: 85, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ },
+ {
+ id: 'network-in',
+ title: 'Network In',
+ value: '85',
+ unit: 'Mbps',
+ type: 'trend' as const,
+ icon: 'lucide:download',
+ trendData: this.networkIn,
+ color: 'hsl(142 71% 45%)',
+ },
+ {
+ id: 'network-out',
+ title: 'Network Out',
+ value: '65',
+ unit: 'Mbps',
+ type: 'trend' as const,
+ icon: 'lucide:upload',
+ trendData: this.networkOut,
+ color: 'hsl(217 91% 60%)',
+ },
+ {
+ id: 'uptime',
+ title: 'System Uptime',
+ value: this.uptime,
+ type: 'text' as const,
+ icon: 'lucide:clock',
+ color: 'hsl(142 71% 45%)',
+ description: 'Since last reboot',
+ },
+ {
+ id: 'processes',
+ title: 'Processes',
+ value: 247,
+ type: 'number' as const,
+ icon: 'lucide:layers',
+ description: '12 running, 235 sleeping',
+ },
+ ];
+
+ return html`
+
+ System Overview
+ Real-time system performance metrics
+
+
+
+
+
+ `;
+ }
+
+ private renderCpuPanel(): TemplateResult {
+ const cpuTiles = [
+ {
+ id: 'cpu-total',
+ title: 'Total CPU Usage',
+ value: this.cpuUsage,
+ type: 'gauge' as const,
+ icon: 'lucide:cpu',
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(142 71% 45%)' },
+ { value: 60, color: 'hsl(45 93% 47%)' },
+ { value: 80, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ },
+ {
+ id: 'core-0',
+ title: 'Core 0',
+ value: 38,
+ type: 'gauge' as const,
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(142 71% 45%)' },
+ { value: 60, color: 'hsl(45 93% 47%)' },
+ { value: 80, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ },
+ {
+ id: 'core-1',
+ title: 'Core 1',
+ value: 52,
+ type: 'gauge' as const,
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(142 71% 45%)' },
+ { value: 60, color: 'hsl(45 93% 47%)' },
+ { value: 80, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ },
+ {
+ id: 'core-2',
+ title: 'Core 2',
+ value: 45,
+ type: 'gauge' as const,
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(142 71% 45%)' },
+ { value: 60, color: 'hsl(45 93% 47%)' },
+ { value: 80, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ },
+ {
+ id: 'core-3',
+ title: 'Core 3',
+ value: 33,
+ type: 'gauge' as const,
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(142 71% 45%)' },
+ { value: 60, color: 'hsl(45 93% 47%)' },
+ { value: 80, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ },
+ {
+ id: 'load-avg',
+ title: 'Load Average',
+ value: '2.45',
+ type: 'trend' as const,
+ icon: 'lucide:activity',
+ trendData: [1.8, 2.1, 2.4, 2.2, 2.5, 2.3, 2.6, 2.4, 2.45],
+ description: '1m: 2.45, 5m: 2.32, 15m: 2.18',
+ },
+ {
+ id: 'cpu-temp',
+ title: 'Temperature',
+ value: this.cpuTemp,
+ unit: '°C',
+ type: 'gauge' as const,
+ icon: 'lucide:thermometer',
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(217 91% 60%)' },
+ { value: 50, color: 'hsl(142 71% 45%)' },
+ { value: 70, color: 'hsl(45 93% 47%)' },
+ { value: 85, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ },
+ {
+ id: 'freq',
+ title: 'Clock Speed',
+ value: '3.2',
+ unit: 'GHz',
+ type: 'number' as const,
+ icon: 'lucide:gauge',
+ description: 'Max: 4.2 GHz',
+ },
+ ];
+
+ return html`
+
+ CPU
+ Processor usage and performance
+
+
+
+
+
+ `;
+ }
+
+ private renderMemoryPanel(): TemplateResult {
+ const memoryTiles = [
+ {
+ id: 'ram-usage',
+ title: 'RAM Usage',
+ value: this.memoryUsage,
+ type: 'gauge' as const,
+ icon: 'lucide:memoryStick',
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(142 71% 45%)' },
+ { value: 70, color: 'hsl(45 93% 47%)' },
+ { value: 85, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ description: '10.7 GB of 16 GB',
+ },
+ {
+ id: 'swap-usage',
+ title: 'Swap Usage',
+ value: 12,
+ type: 'gauge' as const,
+ icon: 'lucide:hardDrive',
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(142 71% 45%)' },
+ { value: 50, color: 'hsl(45 93% 47%)' },
+ { value: 75, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ description: '0.5 GB of 4 GB',
+ },
+ {
+ id: 'mem-trend',
+ title: 'Memory History',
+ value: '67%',
+ type: 'trend' as const,
+ icon: 'lucide:trendingUp',
+ trendData: [58, 62, 65, 63, 68, 72, 70, 65, 67],
+ description: 'Last hour',
+ },
+ {
+ id: 'cached',
+ title: 'Cached',
+ value: '3.2',
+ unit: 'GB',
+ type: 'number' as const,
+ icon: 'lucide:database',
+ color: 'hsl(217 91% 60%)',
+ },
+ {
+ id: 'buffers',
+ title: 'Buffers',
+ value: '512',
+ unit: 'MB',
+ type: 'number' as const,
+ icon: 'lucide:layers',
+ color: 'hsl(262 83% 58%)',
+ },
+ {
+ id: 'available',
+ title: 'Available',
+ value: '5.3',
+ unit: 'GB',
+ type: 'number' as const,
+ icon: 'lucide:checkCircle',
+ color: 'hsl(142 71% 45%)',
+ },
+ ];
+
+ return html`
+
+ Memory
+ RAM and swap usage details
+
+
+
+
+
+ `;
+ }
+
+ private renderStoragePanel(): TemplateResult {
+ const storageTiles = [
+ {
+ id: 'disk-main',
+ title: 'System Drive',
+ value: this.diskUsage,
+ type: 'percentage' as const,
+ icon: 'lucide:hardDrive',
+ description: '275 GB of 512 GB used',
+ color: 'hsl(217 91% 60%)',
+ },
+ {
+ id: 'disk-data',
+ title: 'Data Drive',
+ value: 38,
+ type: 'percentage' as const,
+ icon: 'lucide:hardDrive',
+ description: '380 GB of 1 TB used',
+ color: 'hsl(142 71% 45%)',
+ },
+ {
+ id: 'read-speed',
+ title: 'Read Speed',
+ value: '245',
+ unit: 'MB/s',
+ type: 'trend' as const,
+ icon: 'lucide:download',
+ trendData: [180, 220, 195, 280, 245, 210, 265, 230, 245],
+ color: 'hsl(142 71% 45%)',
+ },
+ {
+ id: 'write-speed',
+ title: 'Write Speed',
+ value: '128',
+ unit: 'MB/s',
+ type: 'trend' as const,
+ icon: 'lucide:upload',
+ trendData: [95, 110, 85, 145, 120, 105, 138, 115, 128],
+ color: 'hsl(217 91% 60%)',
+ },
+ {
+ id: 'iops-read',
+ title: 'Read IOPS',
+ value: '12.4k',
+ type: 'number' as const,
+ icon: 'lucide:gauge',
+ description: 'Operations/sec',
+ },
+ {
+ id: 'iops-write',
+ title: 'Write IOPS',
+ value: '8.2k',
+ type: 'number' as const,
+ icon: 'lucide:gauge',
+ description: 'Operations/sec',
+ },
+ ];
+
+ return html`
+
+ Storage
+ Disk usage and I/O performance
+
+
+
+
+
+ `;
+ }
+
+ private renderNetworkPanel(): TemplateResult {
+ const networkTiles = [
+ {
+ id: 'download',
+ title: 'Download',
+ value: '85.2',
+ unit: 'Mbps',
+ type: 'trend' as const,
+ icon: 'lucide:download',
+ trendData: this.networkIn,
+ color: 'hsl(142 71% 45%)',
+ },
+ {
+ id: 'upload',
+ title: 'Upload',
+ value: '64.8',
+ unit: 'Mbps',
+ type: 'trend' as const,
+ icon: 'lucide:upload',
+ trendData: this.networkOut,
+ color: 'hsl(217 91% 60%)',
+ },
+ {
+ id: 'latency',
+ title: 'Latency',
+ value: 12,
+ unit: 'ms',
+ type: 'gauge' as const,
+ icon: 'lucide:activity',
+ gaugeOptions: {
+ min: 0,
+ max: 100,
+ thresholds: [
+ { value: 0, color: 'hsl(142 71% 45%)' },
+ { value: 30, color: 'hsl(45 93% 47%)' },
+ { value: 60, color: 'hsl(0 84% 60%)' },
+ ],
+ },
+ },
+ {
+ id: 'packets-in',
+ title: 'Packets In',
+ value: '1.2M',
+ type: 'number' as const,
+ icon: 'lucide:arrowDownCircle',
+ description: 'Per second',
+ },
+ {
+ id: 'packets-out',
+ title: 'Packets Out',
+ value: '892k',
+ type: 'number' as const,
+ icon: 'lucide:arrowUpCircle',
+ description: 'Per second',
+ },
+ {
+ id: 'connections',
+ title: 'Active Connections',
+ value: 48,
+ type: 'number' as const,
+ icon: 'lucide:link',
+ description: '12 established, 36 waiting',
+ },
+ {
+ id: 'total-down',
+ title: 'Total Downloaded',
+ value: '24.5',
+ unit: 'GB',
+ type: 'number' as const,
+ icon: 'lucide:database',
+ description: 'This session',
+ color: 'hsl(142 71% 45%)',
+ },
+ {
+ id: 'total-up',
+ title: 'Total Uploaded',
+ value: '8.2',
+ unit: 'GB',
+ type: 'number' as const,
+ icon: 'lucide:database',
+ description: 'This session',
+ color: 'hsl(217 91% 60%)',
+ },
+ ];
+
+ return html`
+
+ Network
+ Network traffic and connectivity
+
+
+
+
+
+ `;
+ }
+
+ private renderProcessesPanel(): TemplateResult {
+ const processTiles = [
+ {
+ id: 'total-processes',
+ title: 'Total Processes',
+ value: 247,
+ type: 'number' as const,
+ icon: 'lucide:layers',
+ },
+ {
+ id: 'running',
+ title: 'Running',
+ value: 12,
+ type: 'number' as const,
+ icon: 'lucide:play',
+ color: 'hsl(142 71% 45%)',
+ },
+ {
+ id: 'sleeping',
+ title: 'Sleeping',
+ value: 235,
+ type: 'number' as const,
+ icon: 'lucide:moon',
+ color: 'hsl(217 91% 60%)',
+ },
+ {
+ id: 'threads',
+ title: 'Threads',
+ value: 1842,
+ type: 'number' as const,
+ icon: 'lucide:gitBranch',
+ },
+ ];
+
+ const topProcesses = [
+ { name: 'node', pid: 1234, cpu: 12.5, memory: 8.2 },
+ { name: 'chrome', pid: 2345, cpu: 8.3, memory: 15.4 },
+ { name: 'code', pid: 3456, cpu: 5.2, memory: 12.1 },
+ { name: 'docker', pid: 4567, cpu: 4.8, memory: 6.8 },
+ { name: 'postgres', pid: 5678, cpu: 3.2, memory: 4.5 },
+ { name: 'nginx', pid: 6789, cpu: 1.5, memory: 2.1 },
+ { name: 'redis', pid: 7890, cpu: 0.8, memory: 1.8 },
+ ];
+
+ return html`
+
+ Processes
+ Running processes and resource usage
+
+
+
+
+
+
+
+ Top Processes by CPU
+
+
+ Process
+ PID
+ CPU %
+ Memory %
+
+ ${topProcesses.map(proc => html`
+
+ ${proc.name}
+ ${proc.pid}
+ ${proc.cpu}%
+ ${proc.memory}%
+
+ `)}
+
+
+ `;
+ }
+}
diff --git a/ts_web/views/eco-view-system/index.ts b/ts_web/views/eco-view-system/index.ts
new file mode 100644
index 0000000..99a6c25
--- /dev/null
+++ b/ts_web/views/eco-view-system/index.ts
@@ -0,0 +1 @@
+export * from './eco-view-system.js';
diff --git a/ts_web/views/index.ts b/ts_web/views/index.ts
new file mode 100644
index 0000000..ddbb266
--- /dev/null
+++ b/ts_web/views/index.ts
@@ -0,0 +1,6 @@
+export * from './eco-view-settings/index.js';
+export * from './eco-view-peripherals/index.js';
+export * from './eco-view-saasshare/index.js';
+export * from './eco-view-system/index.js';
+export * from './eco-view-home/index.js';
+export * from './eco-view-login/index.js';