feat: add interfaces for secondary menu items with various types and functionalities
BIN
.playwright-mcp/amber-gold-headers.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.playwright-mcp/amber-gold-hover.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.playwright-mcp/filter-after-click.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.playwright-mcp/filter-after-fix.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.playwright-mcp/filter-before-click.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.playwright-mcp/muted-grey-headers.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.playwright-mcp/muted-grey-hover.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.playwright-mcp/orange-group-headers.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.playwright-mcp/rose-group-headers.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.playwright-mcp/rose-hover-effect.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.playwright-mcp/subtle-warm-grey.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.playwright-mcp/teal-group-headers.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
.playwright-mcp/teal-hover-effect.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
@@ -39,23 +39,28 @@ export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI
|
||||
themeDefaultStyles,
|
||||
cssManager.defaultStyles,
|
||||
css`
|
||||
/* TODO: Migrate hardcoded values to --dees-* CSS variables */
|
||||
:host {
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
/* CSS Variables aligned with secondary menu */
|
||||
--activitylog-bg: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
--activitylog-fg: ${cssManager.bdTheme('#525252', '#a3a3a3')};
|
||||
--activitylog-fg-muted: ${cssManager.bdTheme('#737373', '#737373')};
|
||||
--activitylog-fg-active: ${cssManager.bdTheme('#0a0a0a', '#fafafa')};
|
||||
--activitylog-border: ${cssManager.bdTheme('#e5e5e5', '#1a1a1a')};
|
||||
--activitylog-hover: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.06)')};
|
||||
--activitylog-accent: ${cssManager.bdTheme('#78716c', '#b5a99a')};
|
||||
|
||||
color: var(--activitylog-fg);
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
height: 100%;
|
||||
background: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
font-family: 'Geist Mono', monospace;
|
||||
border-left: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
background: var(--activitylog-bg);
|
||||
font-family: 'Geist Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
border-left: 1px solid var(--activitylog-border);
|
||||
cursor: default;
|
||||
box-shadow: ${cssManager.bdTheme(
|
||||
'-4px 0 12px rgba(0, 0, 0, 0.02)',
|
||||
'-4px 0 12px rgba(0, 0, 0, 0.2)'
|
||||
)};
|
||||
}
|
||||
|
||||
.maincontainer {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
@@ -64,35 +69,61 @@ export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Header with streaming indicator */
|
||||
.topbar {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
height: 48px;
|
||||
width: 100%;
|
||||
padding: 0px 16px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
padding: 0px 12px;
|
||||
background: var(--activitylog-bg);
|
||||
border-bottom: 1px solid var(--activitylog-border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.topbar .heading {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
color: var(--activitylog-fg-active);
|
||||
}
|
||||
|
||||
.live-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--activitylog-fg-muted);
|
||||
}
|
||||
|
||||
.live-indicator .dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: ${cssManager.bdTheme('#22c55e', '#22c55e')};
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.5; transform: scale(0.9); }
|
||||
50% { opacity: 1; transform: scale(1.1); }
|
||||
}
|
||||
|
||||
/* Activity container */
|
||||
.activityContainer {
|
||||
position: absolute;
|
||||
top: 48px;
|
||||
bottom: 48px;
|
||||
width: 100%;
|
||||
padding: 12px 0px;
|
||||
padding: 8px 0;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: ${cssManager.bdTheme('#e5e7eb', '#27272a')} transparent;
|
||||
scrollbar-color: ${cssManager.bdTheme('#d4d4d4', '#333333')} transparent;
|
||||
}
|
||||
|
||||
.activityContainer::-webkit-scrollbar {
|
||||
@@ -104,82 +135,53 @@ export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI
|
||||
}
|
||||
|
||||
.activityContainer::-webkit-scrollbar-thumb {
|
||||
background: ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
background: ${cssManager.bdTheme('#d4d4d4', '#333333')};
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.activityContainer::-webkit-scrollbar-thumb:hover {
|
||||
background: ${cssManager.bdTheme('#d4d4d8', '#3f3f46')};
|
||||
background: ${cssManager.bdTheme('#a3a3a3', '#525252')};
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
}
|
||||
|
||||
.streamingIndicator {
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.streamingIndicator::before {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
border-radius: 50%;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 0.4; transform: scale(0.8); }
|
||||
50% { opacity: 1; transform: scale(1.2); }
|
||||
padding: 40px 16px;
|
||||
color: var(--activitylog-fg-muted);
|
||||
}
|
||||
|
||||
/* Date separator - warm taupe styling */
|
||||
.date-separator {
|
||||
padding: 12px 16px 8px;
|
||||
font-size: 11px;
|
||||
padding: 12px 12px 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
background: ${cssManager.bdTheme('#f9fafb', '#09090b')};
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--activitylog-accent);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background: var(--activitylog-bg);
|
||||
}
|
||||
|
||||
/* Activity entry - modern stacked layout */
|
||||
.activityentry {
|
||||
min-height: 36px;
|
||||
font-size: 13px;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
||||
transition: all 0.15s ease;
|
||||
font-size: 12px;
|
||||
padding: 8px 12px;
|
||||
margin: 2px 4px;
|
||||
border-radius: 6px;
|
||||
transition: background 0.15s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
line-height: 1.4;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
animation: fadeIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-4px);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
@@ -187,88 +189,109 @@ export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI
|
||||
}
|
||||
}
|
||||
|
||||
.activityentry:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.activityentry:hover {
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
flex-shrink: 0;
|
||||
min-width: 45px;
|
||||
background: var(--activitylog-hover);
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 6px;
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.04)', 'rgba(255, 255, 255, 0.06)')};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
color: var(--activitylog-fg-muted);
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.activity-icon.login {
|
||||
background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.1)', 'rgba(34, 197, 94, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#16a34a', '#22c55e')};
|
||||
background: ${cssManager.bdTheme('rgba(34, 197, 94, 0.08)', 'rgba(34, 197, 94, 0.12)')};
|
||||
color: ${cssManager.bdTheme('#16a34a', '#4ade80')};
|
||||
}
|
||||
|
||||
.activity-icon.logout {
|
||||
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
||||
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.08)', 'rgba(239, 68, 68, 0.12)')};
|
||||
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
|
||||
}
|
||||
|
||||
.activity-icon.view {
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
|
||||
background: ${cssManager.bdTheme('rgba(59, 130, 246, 0.08)', 'rgba(59, 130, 246, 0.12)')};
|
||||
color: ${cssManager.bdTheme('#2563eb', '#60a5fa')};
|
||||
}
|
||||
|
||||
.activity-icon.create {
|
||||
background: ${cssManager.bdTheme('rgba(168, 85, 247, 0.1)', 'rgba(168, 85, 247, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#9333ea', '#a855f7')};
|
||||
background: ${cssManager.bdTheme('rgba(168, 85, 247, 0.08)', 'rgba(168, 85, 247, 0.12)')};
|
||||
color: ${cssManager.bdTheme('#9333ea', '#c084fc')};
|
||||
}
|
||||
|
||||
.activity-icon.update {
|
||||
background: ${cssManager.bdTheme('rgba(251, 146, 60, 0.1)', 'rgba(251, 146, 60, 0.1)')};
|
||||
background: ${cssManager.bdTheme('rgba(251, 146, 60, 0.08)', 'rgba(251, 146, 60, 0.12)')};
|
||||
color: ${cssManager.bdTheme('#ea580c', '#fb923c')};
|
||||
}
|
||||
|
||||
.activity-icon.delete {
|
||||
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.1)', 'rgba(239, 68, 68, 0.1)')};
|
||||
color: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
||||
background: ${cssManager.bdTheme('rgba(239, 68, 68, 0.08)', 'rgba(239, 68, 68, 0.12)')};
|
||||
color: ${cssManager.bdTheme('#dc2626', '#f87171')};
|
||||
}
|
||||
|
||||
.activity-icon.custom {
|
||||
background: ${cssManager.bdTheme('rgba(100, 116, 139, 0.1)', 'rgba(100, 116, 139, 0.1)')};
|
||||
background: ${cssManager.bdTheme('rgba(100, 116, 139, 0.08)', 'rgba(100, 116, 139, 0.12)')};
|
||||
color: ${cssManager.bdTheme('#475569', '#94a3b8')};
|
||||
}
|
||||
|
||||
.activity-text {
|
||||
.activity-content {
|
||||
flex: 1;
|
||||
color: ${cssManager.bdTheme('#18181b', '#e4e4e7')};
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.activity-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.activity-user {
|
||||
font-weight: 600;
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
font-size: 12px;
|
||||
color: var(--activitylog-fg-active);
|
||||
}
|
||||
|
||||
.activity-separator {
|
||||
color: var(--activitylog-fg-muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: var(--activitylog-fg-muted);
|
||||
font-weight: 400;
|
||||
font-size: 11px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: 'Geist Mono', monospace;
|
||||
}
|
||||
|
||||
.activity-message {
|
||||
color: var(--activitylog-fg);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Search box - refined styling */
|
||||
.searchbox {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: ${cssManager.bdTheme('#ffffff', '#09090b')};
|
||||
border-top: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
padding: 8px;
|
||||
background: var(--activitylog-bg);
|
||||
border-top: 1px solid var(--activitylog-border);
|
||||
padding: 8px 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
@@ -282,64 +305,37 @@ export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
font-size: 14px;
|
||||
color: var(--activitylog-fg-muted);
|
||||
font-size: 13px;
|
||||
pointer-events: none;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
.searchbox input {
|
||||
color: ${cssManager.bdTheme('#09090b', '#fafafa')};
|
||||
background: ${cssManager.bdTheme('#f4f4f5', '#18181b')};
|
||||
color: var(--activitylog-fg-active);
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.03)', 'rgba(255, 255, 255, 0.04)')};
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid ${cssManager.bdTheme('#e5e7eb', '#27272a')};
|
||||
border: 1px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.08)', 'rgba(255, 255, 255, 0.08)')};
|
||||
border-radius: 6px;
|
||||
padding: 0 12px 0 36px;
|
||||
padding: 0 12px 0 34px;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.searchbox input::placeholder {
|
||||
color: ${cssManager.bdTheme('#71717a', '#71717a')};
|
||||
color: var(--activitylog-fg-muted);
|
||||
}
|
||||
|
||||
.searchbox input:focus {
|
||||
outline: none;
|
||||
border-color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
box-shadow: 0 0 0 3px ${cssManager.bdTheme('rgba(59, 130, 246, 0.1)', 'rgba(59, 130, 246, 0.1)')};
|
||||
border-color: ${cssManager.bdTheme('rgba(0, 0, 0, 0.15)', 'rgba(255, 255, 255, 0.15)')};
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(255, 255, 255, 0.06)')};
|
||||
}
|
||||
|
||||
.searchbox input:focus ~ .search-icon,
|
||||
.search-wrapper:has(input:focus) .search-icon {
|
||||
color: ${cssManager.bdTheme('#3b82f6', '#3b82f6')};
|
||||
}
|
||||
|
||||
.bottomShadow {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
bottom: 48px;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(180deg, transparent 0%, #fafafa 100%)',
|
||||
'linear-gradient(180deg, transparent 0%, #0a0a0a 100%)'
|
||||
)};
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.topShadow {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 24px;
|
||||
top: 48px;
|
||||
background: ${cssManager.bdTheme(
|
||||
'linear-gradient(0deg, transparent 0%, #fafafa 100%)',
|
||||
'linear-gradient(0deg, transparent 0%, #0a0a0a 100%)'
|
||||
)};
|
||||
pointer-events: none;
|
||||
opacity: 0.8;
|
||||
color: var(--activitylog-fg);
|
||||
}
|
||||
`,
|
||||
];
|
||||
@@ -355,12 +351,11 @@ export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI
|
||||
<div class="maincontainer">
|
||||
<div class="topbar">
|
||||
<div class="heading">Activity Log</div>
|
||||
${filteredEntries.length > 0
|
||||
? html`<div class="live-indicator"><span class="dot"></span>Live</div>`
|
||||
: ''}
|
||||
</div>
|
||||
<div class="activityContainer">
|
||||
${filteredEntries.length > 0
|
||||
? html`<div class="streamingIndicator">Live Updates</div>`
|
||||
: ''}
|
||||
|
||||
${filteredEntries.length === 0
|
||||
? html`<div class="empty-state">No activity entries</div>`
|
||||
: groupedEntries.map(
|
||||
@@ -381,8 +376,6 @@ export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="topShadow"></div>
|
||||
<div class="bottomShadow"></div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -397,12 +390,16 @@ export class DeesAppuiActivitylog extends DeesElement implements IActivityLogAPI
|
||||
class="activityentry"
|
||||
@contextmenu=${(e: MouseEvent) => this.handleContextMenu(e, entry)}
|
||||
>
|
||||
<span class="timestamp">${timeStr}</span>
|
||||
<div class="activity-icon ${entry.type}">
|
||||
<dees-icon .icon=${iconName}></dees-icon>
|
||||
</div>
|
||||
<div class="activity-text">
|
||||
<span class="activity-user">${entry.user}</span> ${entry.message}
|
||||
<div class="activity-content">
|
||||
<div class="activity-header">
|
||||
<span class="activity-user">${entry.user}</span>
|
||||
<span class="activity-separator">·</span>
|
||||
<span class="timestamp">${timeStr}</span>
|
||||
</div>
|
||||
<div class="activity-message">${entry.message}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -57,6 +57,16 @@ export class DeesAppuiBar extends DeesElement {
|
||||
@property({ type: Boolean })
|
||||
accessor showSearch: boolean = false;
|
||||
|
||||
// Activity log toggle
|
||||
@property({ type: Boolean })
|
||||
accessor showActivityLogToggle: boolean = false;
|
||||
|
||||
@property({ type: Number })
|
||||
accessor activityLogCount: number = 0;
|
||||
|
||||
@property({ type: Boolean })
|
||||
accessor activityLogActive: boolean = false;
|
||||
|
||||
// STATE
|
||||
@state()
|
||||
accessor activeMenu: string | null = null;
|
||||
@@ -177,8 +187,8 @@ export class DeesAppuiBar extends DeesElement {
|
||||
public renderAccountSection(): TemplateResult {
|
||||
return html`
|
||||
${this.showSearch ? html`
|
||||
<dees-icon
|
||||
class="search-icon"
|
||||
<dees-icon
|
||||
class="search-icon"
|
||||
.icon=${'lucide:search'}
|
||||
@click=${this.handleSearchClick}
|
||||
></dees-icon>
|
||||
@@ -206,6 +216,18 @@ export class DeesAppuiBar extends DeesElement {
|
||||
></dees-appui-profiledropdown>
|
||||
</div>
|
||||
` : ''}
|
||||
${this.showActivityLogToggle ? html`
|
||||
<div
|
||||
class="activity-toggle ${this.activityLogActive ? 'active' : ''}"
|
||||
@click=${this.handleActivityToggle}
|
||||
title="Activity Log"
|
||||
>
|
||||
<dees-icon .icon=${'lucide:activity'}></dees-icon>
|
||||
${this.activityLogCount > 0 ? html`
|
||||
<span class="activity-badge">${this.activityLogCount > 99 ? '99+' : this.activityLogCount}</span>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -304,9 +326,16 @@ export class DeesAppuiBar extends DeesElement {
|
||||
}
|
||||
|
||||
private handleSearchClick() {
|
||||
this.dispatchEvent(new CustomEvent('search-click', {
|
||||
this.dispatchEvent(new CustomEvent('search-click', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleActivityToggle() {
|
||||
this.dispatchEvent(new CustomEvent('activity-toggle', {
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export const appuiAppbarStyles = [
|
||||
color: ${cssManager.bdTheme('#00000080', '#ffffff80')};
|
||||
font-size: var(--appbar-font-size);
|
||||
display: grid;
|
||||
grid-template-columns: ${cssManager.cssGridColumns(3, 20)};
|
||||
grid-template-columns: auto 1fr auto;
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
}
|
||||
@@ -233,6 +233,54 @@ export const appuiAppbarStyles = [
|
||||
.user-status.away {
|
||||
background: #ff9800;
|
||||
}
|
||||
|
||||
/* Activity log toggle button */
|
||||
.activity-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
border-radius: 6px;
|
||||
cursor: default;
|
||||
-webkit-app-region: no-drag;
|
||||
color: ${cssManager.bdTheme('#00000060', '#ffffff60')};
|
||||
border: 1px solid ${cssManager.bdTheme('rgba(0, 0, 0, 0.1)', 'rgba(255, 255, 255, 0.1)')};
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.activity-toggle:hover {
|
||||
background: ${cssManager.bdTheme('#00000010', '#ffffff15')};
|
||||
color: ${cssManager.bdTheme('#000000', '#ffffff')};
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.activity-toggle.active {
|
||||
background: ${cssManager.bdTheme('#00000015', '#ffffff20')};
|
||||
color: ${cssManager.bdTheme('#000000', '#ffffff')};
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.activity-toggle dees-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.activity-badge {
|
||||
position: relative;
|
||||
margin-left: 4px;
|
||||
min-width: 16px;
|
||||
height: 16px;
|
||||
padding: 0 4px;
|
||||
background: ${cssManager.bdTheme('#525252', '#525252')};
|
||||
color: #fafafa;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
|
||||
@@ -12,41 +12,102 @@ export const demoFunc = () => html`
|
||||
.demo-secondarymenu-container .spacer {
|
||||
flex: 1;
|
||||
background: #0f0f0f;
|
||||
padding: 20px;
|
||||
color: #a3a3a3;
|
||||
font-family: 'Geist Sans', sans-serif;
|
||||
}
|
||||
.demo-secondarymenu-container .spacer h3 {
|
||||
color: #fafafa;
|
||||
margin-top: 0;
|
||||
}
|
||||
.demo-secondarymenu-container .spacer code {
|
||||
background: #27272a;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.demo-secondarymenu-container .spacer ul {
|
||||
line-height: 1.8;
|
||||
}
|
||||
</style>
|
||||
<div class="demo-secondarymenu-container">
|
||||
<dees-appui-secondarymenu
|
||||
.heading=${'Projects'}
|
||||
.groups=${[
|
||||
// Group 1: Tab items (default behavior)
|
||||
{
|
||||
name: 'Active',
|
||||
iconName: 'lucide:folder',
|
||||
name: 'Navigation',
|
||||
iconName: 'lucide:compass',
|
||||
items: [
|
||||
{ key: 'Frontend App', iconName: 'code', action: () => console.log('Frontend'), badge: 3, badgeVariant: 'warning' },
|
||||
{ key: 'API Server', iconName: 'server', action: () => console.log('API'), badge: 'new', badgeVariant: 'success' },
|
||||
{ key: 'Database', iconName: 'database', action: () => console.log('Database') },
|
||||
]
|
||||
{ key: 'Dashboard', iconName: 'lucide:layoutDashboard', action: () => console.log('Dashboard clicked'), badge: 3, badgeVariant: 'warning' },
|
||||
{ key: 'Projects', iconName: 'lucide:folder', action: () => console.log('Projects clicked'), badge: 'new', badgeVariant: 'success' },
|
||||
{ key: 'Analytics', iconName: 'lucide:barChart2', action: () => console.log('Analytics clicked') },
|
||||
] as interfaces.ISecondaryMenuItemTab[]
|
||||
},
|
||||
// Group 2: Actions
|
||||
{
|
||||
name: 'Archived',
|
||||
iconName: 'lucide:archive',
|
||||
name: 'Actions',
|
||||
iconName: 'lucide:zap',
|
||||
items: [
|
||||
{ type: 'action', key: 'Create New', iconName: 'lucide:plus', action: () => alert('Create New clicked!') },
|
||||
{ type: 'action', key: 'Import Data', iconName: 'lucide:upload', action: () => alert('Import Data clicked!') },
|
||||
{ type: 'divider' },
|
||||
{ type: 'action', key: 'Delete All', iconName: 'lucide:trash2', variant: 'danger', confirmMessage: 'Are you sure you want to delete all items?', action: () => alert('Deleted!') },
|
||||
] as interfaces.ISecondaryMenuItem[]
|
||||
},
|
||||
// Group 3: Filters
|
||||
{
|
||||
name: 'Filters',
|
||||
iconName: 'lucide:filter',
|
||||
items: [
|
||||
{ type: 'header', label: 'Status' },
|
||||
{ type: 'filter', key: 'Show Active', iconName: 'lucide:checkCircle', active: true, onToggle: (active) => console.log('Show Active:', active) },
|
||||
{ type: 'filter', key: 'Show Archived', iconName: 'lucide:archive', active: false, onToggle: (active) => console.log('Show Archived:', active) },
|
||||
{ type: 'divider' },
|
||||
{ type: 'multiFilter', key: 'Categories', iconName: 'lucide:tag', collapsed: false, options: [
|
||||
{ key: 'frontend', label: 'Frontend', checked: true, iconName: 'lucide:monitor' },
|
||||
{ key: 'backend', label: 'Backend', checked: true, iconName: 'lucide:server' },
|
||||
{ key: 'devops', label: 'DevOps', checked: false, iconName: 'lucide:cloud' },
|
||||
{ key: 'design', label: 'Design', checked: false, iconName: 'lucide:palette' },
|
||||
], onChange: (keys) => console.log('Selected categories:', keys) },
|
||||
] as interfaces.ISecondaryMenuItem[]
|
||||
},
|
||||
// Group 4: Links and misc
|
||||
{
|
||||
name: 'Resources',
|
||||
iconName: 'lucide:bookOpen',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ key: 'Legacy System', iconName: 'box', action: () => console.log('Legacy') },
|
||||
{ key: 'Old API', iconName: 'server', action: () => console.log('Old API') },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Settings',
|
||||
iconName: 'lucide:settings',
|
||||
items: [
|
||||
{ key: 'Configuration', iconName: 'sliders', action: () => console.log('Config') },
|
||||
{ key: 'Integrations', iconName: 'plug', action: () => console.log('Integrations'), badge: 5, badgeVariant: 'error' },
|
||||
]
|
||||
{ type: 'header', label: 'Documentation' },
|
||||
{ type: 'link', key: 'API Reference', iconName: 'lucide:fileText', href: 'https://api.example.com/docs' },
|
||||
{ type: 'link', key: 'User Guide', iconName: 'lucide:book', href: 'https://docs.example.com/guide' },
|
||||
{ type: 'divider' },
|
||||
{ type: 'header', label: 'Support' },
|
||||
{ type: 'link', key: 'Help Center', iconName: 'lucide:helpCircle', href: '/help', external: false },
|
||||
{ type: 'link', key: 'GitHub Issues', iconName: 'lucide:github', href: 'https://github.com/example/issues' },
|
||||
] as interfaces.ISecondaryMenuItem[]
|
||||
}
|
||||
] as interfaces.IMenuGroup[]}
|
||||
@item-select=${(e: CustomEvent) => console.log('Selected:', e.detail)}
|
||||
] as interfaces.ISecondaryMenuGroup[]}
|
||||
@item-select=${(e: CustomEvent) => console.log('Tab selected:', e.detail)}
|
||||
@action-click=${(e: CustomEvent) => console.log('Action clicked:', e.detail)}
|
||||
@filter-toggle=${(e: CustomEvent) => console.log('Filter toggled:', e.detail)}
|
||||
@multifilter-change=${(e: CustomEvent) => console.log('Multi-filter changed:', e.detail)}
|
||||
@link-click=${(e: CustomEvent) => console.log('Link clicked:', e.detail)}
|
||||
></dees-appui-secondarymenu>
|
||||
<div class="spacer"></div>
|
||||
<div class="spacer">
|
||||
<h3>Secondary Menu Demo</h3>
|
||||
<p>This demo showcases all 8 item types:</p>
|
||||
<ul>
|
||||
<li><code>tab</code> - Selectable items (Navigation group)</li>
|
||||
<li><code>action</code> - Blue actions (Actions group)</li>
|
||||
<li><code>action</code> with <code>variant: 'danger'</code> - Red danger action</li>
|
||||
<li><code>filter</code> - Checkbox toggles (Filters group)</li>
|
||||
<li><code>multiFilter</code> - Collapsible multi-select (Categories)</li>
|
||||
<li><code>divider</code> - Visual separators</li>
|
||||
<li><code>header</code> - Section labels</li>
|
||||
<li><code>link</code> - External/internal links (Resources group)</li>
|
||||
</ul>
|
||||
<p>Try the collapse toggle on the left edge!</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -19,7 +19,16 @@ import { themeDefaultStyles } from '../../00theme.js';
|
||||
|
||||
/**
|
||||
* Secondary navigation menu for sub-navigation within MainMenu views
|
||||
* Supports collapsible groups, badges, and dynamic headings
|
||||
*
|
||||
* Supports 8 item types:
|
||||
* 1. Tab - selectable, stays highlighted (default)
|
||||
* 2. Action - executes without selection (blue)
|
||||
* 3. Danger Action - red styling with optional confirmation
|
||||
* 4. Filter - checkbox toggle
|
||||
* 5. Multi-Filter - collapsible box with multiple checkboxes
|
||||
* 6. Divider - visual separator
|
||||
* 7. Header - non-interactive label
|
||||
* 8. Link - opens URL
|
||||
*/
|
||||
@customElement('dees-appui-secondarymenu')
|
||||
export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
@@ -31,22 +40,30 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
@property({ type: String })
|
||||
accessor heading: string = 'Menu';
|
||||
|
||||
/** Grouped items with collapse support */
|
||||
/** Grouped items with collapse support - supports new ISecondaryMenuGroup */
|
||||
@property({ type: Array })
|
||||
accessor groups: interfaces.IMenuGroup[] = [];
|
||||
accessor groups: interfaces.ISecondaryMenuGroup[] = [];
|
||||
|
||||
/** Legacy flat list support for backward compatibility */
|
||||
@property({ type: Array })
|
||||
accessor selectionOptions: (interfaces.IMenuItem | { divider: true })[] = [];
|
||||
|
||||
/** Currently selected item */
|
||||
/** Currently selected tab item */
|
||||
@property({ type: Object })
|
||||
accessor selectedItem: interfaces.IMenuItem | null = null;
|
||||
accessor selectedItem: interfaces.ISecondaryMenuItemTab | null = null;
|
||||
|
||||
/** Internal state for collapsed groups */
|
||||
@state()
|
||||
accessor collapsedGroups: Set<string> = new Set();
|
||||
|
||||
/** Internal state for collapsed multi-filters */
|
||||
@state()
|
||||
accessor collapsedMultiFilters: Set<string> = new Set();
|
||||
|
||||
/** Render counter to force re-renders when items are mutated */
|
||||
@state()
|
||||
private accessor renderCounter: number = 0;
|
||||
|
||||
/** Horizontal collapse state */
|
||||
@property({ type: Boolean, reflect: true })
|
||||
accessor collapsed: boolean = false;
|
||||
@@ -80,6 +97,12 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
--badge-error-bg: ${cssManager.bdTheme('#fee2e2', '#450a0a')};
|
||||
--badge-error-fg: ${cssManager.bdTheme('#991b1b', '#f87171')};
|
||||
|
||||
/* Action colors */
|
||||
--action-primary: ${cssManager.bdTheme('#2563eb', '#3b82f6')};
|
||||
--action-primary-hover: ${cssManager.bdTheme('#1d4ed8', '#60a5fa')};
|
||||
--action-danger: ${cssManager.bdTheme('#dc2626', '#ef4444')};
|
||||
--action-danger-hover: ${cssManager.bdTheme('#b91c1c', '#f87171')};
|
||||
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 100%;
|
||||
@@ -220,7 +243,7 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
}
|
||||
|
||||
.groupHeader:hover {
|
||||
background: var(--sidebar-hover);
|
||||
background: ${cssManager.bdTheme('rgba(140, 120, 100, 0.06)', 'rgba(180, 160, 140, 0.08)')};
|
||||
}
|
||||
|
||||
.groupHeader .groupTitle {
|
||||
@@ -229,7 +252,7 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--sidebar-fg-muted);
|
||||
color: ${cssManager.bdTheme('#78716c', '#b5a99a')};
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
@@ -238,13 +261,13 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
|
||||
.groupHeader .groupTitle dees-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
color: ${cssManager.bdTheme('#78716c', '#b5a99a')};
|
||||
}
|
||||
|
||||
.groupHeader .chevron {
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
color: var(--sidebar-fg-muted);
|
||||
color: ${cssManager.bdTheme('#78716c', '#b5a99a')};
|
||||
}
|
||||
|
||||
.groupHeader.collapsed .chevron {
|
||||
@@ -264,7 +287,7 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
.groupItems {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.25s ease, opacity 0.2s ease;
|
||||
max-height: 500px;
|
||||
max-height: 1000px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -279,7 +302,7 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Menu Item */
|
||||
/* Menu Item Base */
|
||||
.menuItem {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -304,6 +327,12 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
background: var(--sidebar-active);
|
||||
}
|
||||
|
||||
.menuItem.disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.menuItem.selected {
|
||||
background: var(--sidebar-active);
|
||||
color: var(--sidebar-fg-active);
|
||||
@@ -340,6 +369,208 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
transition: opacity 0.2s ease, width 0.25s ease;
|
||||
}
|
||||
|
||||
/* Action Item Styles */
|
||||
.menuItem.action-primary {
|
||||
color: var(--action-primary);
|
||||
}
|
||||
|
||||
.menuItem.action-primary:hover {
|
||||
color: var(--action-primary-hover);
|
||||
background: ${cssManager.bdTheme('rgba(37, 99, 235, 0.08)', 'rgba(59, 130, 246, 0.12)')};
|
||||
}
|
||||
|
||||
.menuItem.action-primary dees-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menuItem.action-danger {
|
||||
color: var(--action-danger);
|
||||
}
|
||||
|
||||
.menuItem.action-danger:hover {
|
||||
color: var(--action-danger-hover);
|
||||
background: ${cssManager.bdTheme('rgba(220, 38, 38, 0.08)', 'rgba(239, 68, 68, 0.12)')};
|
||||
}
|
||||
|
||||
.menuItem.action-danger dees-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Filter Item Styles */
|
||||
.menuItem.filter {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.menuItem.filter .filter-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid ${cssManager.bdTheme('#d4d4d4', '#525252')};
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menuItem.filter .filter-checkbox.checked {
|
||||
background: var(--sidebar-accent);
|
||||
border-color: var(--sidebar-accent);
|
||||
}
|
||||
|
||||
.menuItem.filter .filter-checkbox dees-icon {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.menuItem.filter.active {
|
||||
color: var(--sidebar-fg-active);
|
||||
}
|
||||
|
||||
/* Multi-Filter Container */
|
||||
.multiFilter {
|
||||
margin: 4px 0;
|
||||
border: 1px solid var(--sidebar-border);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: ${cssManager.bdTheme('rgba(0, 0, 0, 0.02)', 'rgba(255, 255, 255, 0.02)')};
|
||||
}
|
||||
|
||||
.multiFilter-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.multiFilter-header:hover {
|
||||
background: var(--sidebar-hover);
|
||||
}
|
||||
|
||||
.multiFilter-header .multiFilter-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--sidebar-fg-active);
|
||||
}
|
||||
|
||||
.multiFilter-header .multiFilter-title dees-icon {
|
||||
font-size: 16px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.multiFilter-header .multiFilter-count {
|
||||
font-size: 11px;
|
||||
color: var(--sidebar-fg-muted);
|
||||
background: var(--badge-default-bg);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.multiFilter-header .chevron {
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
color: var(--sidebar-fg-muted);
|
||||
}
|
||||
|
||||
.multiFilter-header.collapsed .chevron {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.multiFilter-options {
|
||||
border-top: 1px solid var(--sidebar-border);
|
||||
overflow: hidden;
|
||||
transition: max-height 0.25s ease, opacity 0.2s ease;
|
||||
max-height: 500px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.multiFilter-options.collapsed {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.multiFilter-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
font-size: 13px;
|
||||
color: var(--sidebar-fg);
|
||||
}
|
||||
|
||||
.multiFilter-option:hover {
|
||||
background: var(--sidebar-hover);
|
||||
color: var(--sidebar-fg-active);
|
||||
}
|
||||
|
||||
.multiFilter-option .option-checkbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid ${cssManager.bdTheme('#d4d4d4', '#525252')};
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.multiFilter-option .option-checkbox.checked {
|
||||
background: var(--sidebar-accent);
|
||||
border-color: var(--sidebar-accent);
|
||||
}
|
||||
|
||||
.multiFilter-option .option-checkbox dees-icon {
|
||||
font-size: 12px;
|
||||
color: ${cssManager.bdTheme('#fafafa', '#0a0a0a')};
|
||||
}
|
||||
|
||||
.multiFilter-option dees-icon.option-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.menuDivider {
|
||||
height: 1px;
|
||||
background: var(--sidebar-border);
|
||||
margin: 8px 12px;
|
||||
}
|
||||
|
||||
:host([collapsed]) .menuDivider {
|
||||
margin: 8px 4px;
|
||||
}
|
||||
|
||||
/* Header/Label */
|
||||
.menuHeader {
|
||||
padding: 12px 12px 4px 12px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--sidebar-fg-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
:host([collapsed]) .menuHeader {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Link Item */
|
||||
.menuItem.link .external-icon {
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Collapsed menu item styles */
|
||||
:host([collapsed]) .menuItem {
|
||||
justify-content: center;
|
||||
@@ -357,6 +588,15 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
left: -4px;
|
||||
}
|
||||
|
||||
:host([collapsed]) .menuItem .filter-checkbox,
|
||||
:host([collapsed]) .menuItem .external-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:host([collapsed]) .multiFilter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Tooltip for collapsed state */
|
||||
.item-tooltip {
|
||||
position: absolute;
|
||||
@@ -431,17 +671,17 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
/* Legacy options container */
|
||||
.legacyOptions {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* Divider (legacy) */
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: var(--sidebar-border);
|
||||
margin: 8px 12px;
|
||||
}
|
||||
|
||||
/* Legacy options container */
|
||||
.legacyOptions {
|
||||
padding: 0 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -472,28 +712,58 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
@click="${() => this.toggleGroup(group.name)}"
|
||||
>
|
||||
<span class="groupTitle">
|
||||
${group.iconName ? html`<dees-icon .icon="${group.iconName.startsWith('lucide:') ? group.iconName : `lucide:${group.iconName}`}"></dees-icon>` : ''}
|
||||
${group.iconName ? html`<dees-icon .icon="${this.normalizeIcon(group.iconName)}"></dees-icon>` : ''}
|
||||
${group.name}
|
||||
</span>
|
||||
<dees-icon class="chevron" .icon="${'lucide:chevronDown'}"></dees-icon>
|
||||
</div>
|
||||
<div class="groupItems ${this.collapsedGroups.has(group.name) ? 'collapsed' : ''}">
|
||||
${group.items.map((item) => this.renderMenuItem(item, group))}
|
||||
${group.items.map((item) => this.renderItem(item, group))}
|
||||
</div>
|
||||
</div>
|
||||
`)}
|
||||
`;
|
||||
}
|
||||
|
||||
private renderMenuItem(item: interfaces.IMenuItem, group?: interfaces.IMenuGroup): TemplateResult {
|
||||
private renderItem(item: interfaces.ISecondaryMenuItem, group?: interfaces.ISecondaryMenuGroup): TemplateResult {
|
||||
// Check for hidden items
|
||||
if ('hidden' in item && item.hidden) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
// Determine item type
|
||||
const itemType = 'type' in item ? item.type : 'tab';
|
||||
|
||||
switch (itemType) {
|
||||
case 'action':
|
||||
return this.renderActionItem(item as interfaces.ISecondaryMenuItemAction);
|
||||
case 'filter':
|
||||
return this.renderFilterItem(item as interfaces.ISecondaryMenuItemFilter);
|
||||
case 'multiFilter':
|
||||
return this.renderMultiFilterItem(item as interfaces.ISecondaryMenuItemMultiFilter);
|
||||
case 'divider':
|
||||
return this.renderDivider();
|
||||
case 'header':
|
||||
return this.renderHeader(item as interfaces.ISecondaryMenuItemHeader);
|
||||
case 'link':
|
||||
return this.renderLinkItem(item as interfaces.ISecondaryMenuItemLink);
|
||||
case 'tab':
|
||||
default:
|
||||
return this.renderTabItem(item as interfaces.ISecondaryMenuItemTab, group);
|
||||
}
|
||||
}
|
||||
|
||||
private renderTabItem(item: interfaces.ISecondaryMenuItemTab, group?: interfaces.ISecondaryMenuGroup): TemplateResult {
|
||||
const isSelected = this.selectedItem?.key === item.key;
|
||||
const isDisabled = item.disabled === true;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="menuItem ${isSelected ? 'selected' : ''}"
|
||||
@click="${() => this.selectItem(item, group)}"
|
||||
class="menuItem ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''}"
|
||||
@click="${() => !isDisabled && this.selectTabItem(item, group)}"
|
||||
@contextmenu="${(e: MouseEvent) => this.handleContextMenu(e, item)}"
|
||||
>
|
||||
${item.iconName ? html`<dees-icon .icon="${item.iconName.startsWith('lucide:') ? item.iconName : `lucide:${item.iconName}`}"></dees-icon>` : ''}
|
||||
${item.iconName ? html`<dees-icon .icon="${this.normalizeIcon(item.iconName)}"></dees-icon>` : ''}
|
||||
<span class="itemLabel">${item.key}</span>
|
||||
${item.badge !== undefined ? html`
|
||||
<span class="badge ${item.badgeVariant || 'default'}">${item.badge}</span>
|
||||
@@ -503,6 +773,100 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderActionItem(item: interfaces.ISecondaryMenuItemAction): TemplateResult {
|
||||
const variant = item.variant || 'primary';
|
||||
const isDisabled = item.disabled === true;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="menuItem action-${variant} ${isDisabled ? 'disabled' : ''}"
|
||||
@click="${() => !isDisabled && this.handleActionClick(item)}"
|
||||
>
|
||||
${item.iconName ? html`<dees-icon .icon="${this.normalizeIcon(item.iconName)}"></dees-icon>` : ''}
|
||||
<span class="itemLabel">${item.key}</span>
|
||||
<span class="item-tooltip">${item.key}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderFilterItem(item: interfaces.ISecondaryMenuItemFilter): TemplateResult {
|
||||
const isDisabled = item.disabled === true;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="menuItem filter ${item.active ? 'active' : ''} ${isDisabled ? 'disabled' : ''}"
|
||||
@click="${() => !isDisabled && this.handleFilterToggle(item)}"
|
||||
>
|
||||
${item.iconName ? html`<dees-icon .icon="${this.normalizeIcon(item.iconName)}"></dees-icon>` : ''}
|
||||
<span class="itemLabel">${item.key}</span>
|
||||
<div class="filter-checkbox ${item.active ? 'checked' : ''}">
|
||||
${item.active ? html`<dees-icon .icon="${'lucide:check'}"></dees-icon>` : ''}
|
||||
</div>
|
||||
<span class="item-tooltip">${item.key}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderMultiFilterItem(item: interfaces.ISecondaryMenuItemMultiFilter): TemplateResult {
|
||||
const isCollapsed = this.collapsedMultiFilters.has(item.key);
|
||||
const checkedCount = item.options.filter(opt => opt.checked).length;
|
||||
|
||||
return html`
|
||||
<div class="multiFilter">
|
||||
<div
|
||||
class="multiFilter-header ${isCollapsed ? 'collapsed' : ''}"
|
||||
@click="${() => this.toggleMultiFilter(item.key)}"
|
||||
>
|
||||
<span class="multiFilter-title">
|
||||
${item.iconName ? html`<dees-icon .icon="${this.normalizeIcon(item.iconName)}"></dees-icon>` : ''}
|
||||
${item.key}
|
||||
</span>
|
||||
${checkedCount > 0 ? html`<span class="multiFilter-count">${checkedCount}</span>` : ''}
|
||||
<dees-icon class="chevron" .icon="${'lucide:chevronDown'}"></dees-icon>
|
||||
</div>
|
||||
<div class="multiFilter-options ${isCollapsed ? 'collapsed' : ''}">
|
||||
${item.options.map(option => html`
|
||||
<div
|
||||
class="multiFilter-option"
|
||||
@click="${() => this.handleMultiFilterOptionToggle(item, option.key)}"
|
||||
>
|
||||
<div class="option-checkbox ${option.checked ? 'checked' : ''}">
|
||||
${option.checked ? html`<dees-icon .icon="${'lucide:check'}"></dees-icon>` : ''}
|
||||
</div>
|
||||
${option.iconName ? html`<dees-icon class="option-icon" .icon="${this.normalizeIcon(option.iconName)}"></dees-icon>` : ''}
|
||||
<span>${option.label}</span>
|
||||
</div>
|
||||
`)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderDivider(): TemplateResult {
|
||||
return html`<div class="menuDivider"></div>`;
|
||||
}
|
||||
|
||||
private renderHeader(item: interfaces.ISecondaryMenuItemHeader): TemplateResult {
|
||||
return html`<div class="menuHeader">${item.label}</div>`;
|
||||
}
|
||||
|
||||
private renderLinkItem(item: interfaces.ISecondaryMenuItemLink): TemplateResult {
|
||||
const isExternal = item.external ?? item.href.startsWith('http');
|
||||
const isDisabled = item.disabled === true;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="menuItem link ${isDisabled ? 'disabled' : ''}"
|
||||
@click="${() => !isDisabled && this.handleLinkClick(item)}"
|
||||
>
|
||||
${item.iconName ? html`<dees-icon .icon="${this.normalizeIcon(item.iconName)}"></dees-icon>` : ''}
|
||||
<span class="itemLabel">${item.key}</span>
|
||||
${isExternal ? html`<dees-icon class="external-icon" .icon="${'lucide:externalLink'}"></dees-icon>` : ''}
|
||||
<span class="item-tooltip">${item.key}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderLegacyOptions(): TemplateResult {
|
||||
return html`
|
||||
<div class="legacyOptions">
|
||||
@@ -511,16 +875,25 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
return html`<div class="divider"></div>`;
|
||||
}
|
||||
const item = option as interfaces.IMenuItem;
|
||||
return this.renderMenuItem({
|
||||
// Convert legacy IMenuItem to ISecondaryMenuItemTab
|
||||
const tabItem: interfaces.ISecondaryMenuItemTab = {
|
||||
key: item.key,
|
||||
iconName: item.iconName,
|
||||
action: item.action,
|
||||
});
|
||||
badge: item.badge,
|
||||
badgeVariant: item.badgeVariant,
|
||||
};
|
||||
return this.renderTabItem(tabItem);
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// Helper to normalize icon names
|
||||
private normalizeIcon(iconName: string): string {
|
||||
return iconName.startsWith('lucide:') ? iconName : `lucide:${iconName}`;
|
||||
}
|
||||
|
||||
private toggleGroup(groupName: string): void {
|
||||
const newCollapsed = new Set(this.collapsedGroups);
|
||||
if (newCollapsed.has(groupName)) {
|
||||
@@ -531,6 +904,16 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
this.collapsedGroups = newCollapsed;
|
||||
}
|
||||
|
||||
private toggleMultiFilter(filterKey: string): void {
|
||||
const newCollapsed = new Set(this.collapsedMultiFilters);
|
||||
if (newCollapsed.has(filterKey)) {
|
||||
newCollapsed.delete(filterKey);
|
||||
} else {
|
||||
newCollapsed.add(filterKey);
|
||||
}
|
||||
this.collapsedMultiFilters = newCollapsed;
|
||||
}
|
||||
|
||||
public toggleCollapse(): void {
|
||||
this.collapsed = !this.collapsed;
|
||||
this.dispatchEvent(new CustomEvent('collapse-change', {
|
||||
@@ -540,7 +923,7 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
}));
|
||||
}
|
||||
|
||||
private selectItem(item: interfaces.IMenuItem, group?: interfaces.IMenuGroup): void {
|
||||
private selectTabItem(item: interfaces.ISecondaryMenuItemTab, group?: interfaces.ISecondaryMenuGroup): void {
|
||||
this.selectedItem = item;
|
||||
item.action();
|
||||
|
||||
@@ -551,7 +934,81 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
}));
|
||||
}
|
||||
|
||||
private handleContextMenu(event: MouseEvent, item: interfaces.IMenuItem): void {
|
||||
private async handleActionClick(item: interfaces.ISecondaryMenuItemAction): Promise<void> {
|
||||
// Handle confirmation if required
|
||||
if (item.confirmMessage) {
|
||||
const confirmed = window.confirm(item.confirmMessage);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await item.action();
|
||||
|
||||
this.dispatchEvent(new CustomEvent('action-click', {
|
||||
detail: { item },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleFilterToggle(item: interfaces.ISecondaryMenuItemFilter): void {
|
||||
const newActive = !item.active;
|
||||
// Update the item's active state
|
||||
item.active = newActive;
|
||||
item.onToggle(newActive);
|
||||
|
||||
// Force re-render by incrementing the render counter
|
||||
this.renderCounter++;
|
||||
|
||||
this.dispatchEvent(new CustomEvent('filter-toggle', {
|
||||
detail: { item, active: newActive },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleMultiFilterOptionToggle(item: interfaces.ISecondaryMenuItemMultiFilter, optionKey: string): void {
|
||||
// Update the option's checked state
|
||||
const option = item.options.find(opt => opt.key === optionKey);
|
||||
if (option) {
|
||||
option.checked = !option.checked;
|
||||
}
|
||||
|
||||
// Calculate the new selected keys
|
||||
const selectedKeys = item.options
|
||||
.filter(opt => opt.checked)
|
||||
.map(opt => opt.key);
|
||||
|
||||
item.onChange(selectedKeys);
|
||||
|
||||
// Force re-render by incrementing the render counter
|
||||
this.renderCounter++;
|
||||
|
||||
this.dispatchEvent(new CustomEvent('multifilter-change', {
|
||||
detail: { item, selectedKeys },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleLinkClick(item: interfaces.ISecondaryMenuItemLink): void {
|
||||
const isExternal = item.external ?? item.href.startsWith('http');
|
||||
|
||||
if (isExternal) {
|
||||
window.open(item.href, '_blank', 'noopener,noreferrer');
|
||||
} else {
|
||||
window.location.href = item.href;
|
||||
}
|
||||
|
||||
this.dispatchEvent(new CustomEvent('link-click', {
|
||||
detail: { item },
|
||||
bubbles: true,
|
||||
composed: true
|
||||
}));
|
||||
}
|
||||
|
||||
private handleContextMenu(event: MouseEvent, item: interfaces.ISecondaryMenuItemTab): void {
|
||||
DeesContextmenu.openContextMenuWithOptions(event, [
|
||||
{
|
||||
name: 'View details',
|
||||
@@ -572,26 +1029,52 @@ export class DeesAppuiSecondarymenu extends DeesElement {
|
||||
// Initialize collapsed state from group defaults
|
||||
if (this.groups.length > 0) {
|
||||
const initialCollapsed = new Set<string>();
|
||||
const initialMultiFilterCollapsed = new Set<string>();
|
||||
|
||||
this.groups.forEach(group => {
|
||||
if (group.collapsed) {
|
||||
initialCollapsed.add(group.name);
|
||||
}
|
||||
});
|
||||
this.collapsedGroups = initialCollapsed;
|
||||
|
||||
// Auto-select first item if none selected
|
||||
if (!this.selectedItem && this.groups[0]?.items.length > 0) {
|
||||
this.selectItem(this.groups[0].items[0], this.groups[0]);
|
||||
// Check for collapsed multi-filters
|
||||
group.items.forEach(item => {
|
||||
if ('type' in item && item.type === 'multiFilter') {
|
||||
const multiFilter = item as interfaces.ISecondaryMenuItemMultiFilter;
|
||||
if (multiFilter.collapsed) {
|
||||
initialMultiFilterCollapsed.add(multiFilter.key);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.collapsedGroups = initialCollapsed;
|
||||
this.collapsedMultiFilters = initialMultiFilterCollapsed;
|
||||
|
||||
// Auto-select first tab item if none selected
|
||||
if (!this.selectedItem) {
|
||||
for (const group of this.groups) {
|
||||
for (const item of group.items) {
|
||||
const itemType = 'type' in item ? item.type : 'tab';
|
||||
if (itemType === 'tab' || itemType === undefined) {
|
||||
const tabItem = item as interfaces.ISecondaryMenuItemTab;
|
||||
if (!tabItem.disabled) {
|
||||
this.selectTabItem(tabItem, group);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (this.selectionOptions.length > 0) {
|
||||
// Legacy mode: select first non-divider option
|
||||
const firstOption = this.selectionOptions.find(opt => !('divider' in opt)) as interfaces.IMenuItem;
|
||||
if (firstOption && !this.selectedItem) {
|
||||
this.selectItem({
|
||||
const tabItem: interfaces.ISecondaryMenuItemTab = {
|
||||
key: firstOption.key,
|
||||
iconName: firstOption.iconName,
|
||||
action: firstOption.action,
|
||||
});
|
||||
};
|
||||
this.selectTabItem(tabItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { html, css, DeesElement, customElement, state } from '@design.estate/dees-element';
|
||||
import type { DeesAppui } from './dees-appui.js';
|
||||
import type { IAppConfig, IViewActivationContext } from '../../interfaces/appconfig.js';
|
||||
import type * as interfaces from '../../interfaces/index.js';
|
||||
import '@design.estate/dees-wcctools/demotools';
|
||||
|
||||
// Demo view component with lifecycle hooks
|
||||
@@ -16,7 +17,7 @@ class DemoDashboardView extends DeesElement {
|
||||
this.activated = true;
|
||||
console.log('Dashboard activated with context:', context);
|
||||
|
||||
// Set view-specific secondary menu
|
||||
// Set view-specific secondary menu with new item types
|
||||
context.appui.setSecondaryMenu({
|
||||
heading: 'Dashboard',
|
||||
groups: [
|
||||
@@ -24,17 +25,36 @@ class DemoDashboardView extends DeesElement {
|
||||
name: 'Quick Access',
|
||||
iconName: 'lucide:zap',
|
||||
items: [
|
||||
{ key: 'overview', iconName: 'layoutDashboard', action: () => console.log('Overview') },
|
||||
{ key: 'recent', iconName: 'clock', badge: 5, action: () => console.log('Recent') },
|
||||
]
|
||||
{ key: 'Overview', iconName: 'layoutDashboard', action: () => console.log('Overview') },
|
||||
{ key: 'Recent', iconName: 'clock', badge: 5, action: () => console.log('Recent') },
|
||||
{ type: 'divider' },
|
||||
{ type: 'action', key: 'Refresh Data', iconName: 'lucide:refreshCw', action: () => alert('Refreshing dashboard data...') },
|
||||
] as interfaces.ISecondaryMenuItem[]
|
||||
},
|
||||
{
|
||||
name: 'Filters',
|
||||
iconName: 'lucide:filter',
|
||||
items: [
|
||||
{ type: 'header', label: 'Time Range' },
|
||||
{ type: 'filter', key: 'Live Updates', iconName: 'lucide:radio', active: true, onToggle: (active) => console.log('Live updates:', active) },
|
||||
{ type: 'filter', key: 'Show Archived', iconName: 'lucide:archive', active: false, onToggle: (active) => console.log('Show archived:', active) },
|
||||
{ type: 'divider' },
|
||||
{ type: 'multiFilter', key: 'Data Sources', iconName: 'lucide:database', options: [
|
||||
{ key: 'api', label: 'API Server', checked: true, iconName: 'lucide:server' },
|
||||
{ key: 'web', label: 'Web Traffic', checked: true, iconName: 'lucide:globe' },
|
||||
{ key: 'mobile', label: 'Mobile App', checked: false, iconName: 'lucide:smartphone' },
|
||||
], onChange: (keys) => console.log('Data sources:', keys) },
|
||||
] as interfaces.ISecondaryMenuItem[]
|
||||
},
|
||||
{
|
||||
name: 'Analytics',
|
||||
iconName: 'lucide:barChart3',
|
||||
items: [
|
||||
{ key: 'metrics', iconName: 'activity', action: () => console.log('Metrics') },
|
||||
{ key: 'reports', iconName: 'fileText', badge: 'new', badgeVariant: 'success', action: () => console.log('Reports') },
|
||||
]
|
||||
{ key: 'Metrics', iconName: 'activity', action: () => console.log('Metrics') },
|
||||
{ key: 'Reports', iconName: 'fileText', badge: 'new', badgeVariant: 'success', action: () => console.log('Reports') },
|
||||
{ type: 'divider' },
|
||||
{ type: 'link', key: 'Analytics Docs', iconName: 'lucide:externalLink', href: 'https://docs.example.com/analytics' },
|
||||
] as interfaces.ISecondaryMenuItem[]
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -322,11 +342,22 @@ class DemoProjectsView extends DeesElement {
|
||||
groups: [
|
||||
{
|
||||
name: 'My Projects',
|
||||
iconName: 'lucide:folder',
|
||||
items: [
|
||||
{ key: 'active', iconName: 'folder', badge: 3, action: () => console.log('Active') },
|
||||
{ key: 'archived', iconName: 'archive', action: () => console.log('Archived') },
|
||||
{ key: 'shared', iconName: 'users', badge: 2, badgeVariant: 'warning', action: () => console.log('Shared') },
|
||||
]
|
||||
{ key: 'Active', iconName: 'folder', badge: 3, action: () => console.log('Active') },
|
||||
{ key: 'Archived', iconName: 'archive', action: () => console.log('Archived') },
|
||||
{ key: 'Shared', iconName: 'users', badge: 2, badgeVariant: 'warning', action: () => console.log('Shared') },
|
||||
] as interfaces.ISecondaryMenuItem[]
|
||||
},
|
||||
{
|
||||
name: 'Quick Actions',
|
||||
iconName: 'lucide:zap',
|
||||
items: [
|
||||
{ type: 'action', key: 'New Project', iconName: 'lucide:folderPlus', action: () => alert('Create new project') },
|
||||
{ type: 'action', key: 'Import', iconName: 'lucide:download', action: () => alert('Import project') },
|
||||
{ type: 'divider' },
|
||||
{ type: 'link', key: 'Templates', iconName: 'lucide:layoutTemplate', href: 'https://templates.example.com' },
|
||||
] as interfaces.ISecondaryMenuItem[]
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -407,13 +438,40 @@ class DemoTasksView extends DeesElement {
|
||||
heading: 'Tasks',
|
||||
groups: [
|
||||
{
|
||||
name: 'Filters',
|
||||
name: 'Views',
|
||||
iconName: 'lucide:eye',
|
||||
items: [
|
||||
{ key: 'all', iconName: 'list', badge: 12, action: () => console.log('All') },
|
||||
{ key: 'today', iconName: 'calendar', badge: 3, action: () => console.log('Today') },
|
||||
{ key: 'upcoming', iconName: 'clock', action: () => console.log('Upcoming') },
|
||||
{ key: 'completed', iconName: 'checkCircle', action: () => console.log('Completed') },
|
||||
]
|
||||
{ key: 'All Tasks', iconName: 'list', badge: 12, action: () => console.log('All') },
|
||||
{ key: 'Today', iconName: 'calendar', badge: 3, action: () => console.log('Today') },
|
||||
{ key: 'Upcoming', iconName: 'clock', action: () => console.log('Upcoming') },
|
||||
{ key: 'Completed', iconName: 'checkCircle', action: () => console.log('Completed') },
|
||||
] as interfaces.ISecondaryMenuItem[]
|
||||
},
|
||||
{
|
||||
name: 'Filters',
|
||||
iconName: 'lucide:filter',
|
||||
items: [
|
||||
{ type: 'header', label: 'Priority' },
|
||||
{ type: 'multiFilter', key: 'Priority', iconName: 'lucide:flag', options: [
|
||||
{ key: 'high', label: 'High', checked: true, iconName: 'lucide:alertCircle' },
|
||||
{ key: 'medium', label: 'Medium', checked: true, iconName: 'lucide:minusCircle' },
|
||||
{ key: 'low', label: 'Low', checked: false, iconName: 'lucide:circle' },
|
||||
], onChange: (keys) => console.log('Priority filter:', keys) },
|
||||
{ type: 'divider' },
|
||||
{ type: 'header', label: 'Options' },
|
||||
{ type: 'filter', key: 'Show Subtasks', iconName: 'lucide:listTree', active: true, onToggle: (active) => console.log('Show subtasks:', active) },
|
||||
{ type: 'filter', key: 'Show Completed', iconName: 'lucide:checkSquare', active: false, onToggle: (active) => console.log('Show completed:', active) },
|
||||
] as interfaces.ISecondaryMenuItem[]
|
||||
},
|
||||
{
|
||||
name: 'Actions',
|
||||
iconName: 'lucide:zap',
|
||||
items: [
|
||||
{ type: 'action', key: 'Add Task', iconName: 'lucide:plus', action: () => alert('Add new task') },
|
||||
{ type: 'action', key: 'Import Tasks', iconName: 'lucide:upload', action: () => alert('Import tasks') },
|
||||
{ type: 'divider' },
|
||||
{ type: 'action', key: 'Clear Completed', iconName: 'lucide:trash2', variant: 'danger', confirmMessage: 'Delete all completed tasks?', action: () => alert('Cleared completed tasks') },
|
||||
] as interfaces.ISecondaryMenuItem[]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
@@ -98,10 +98,10 @@ export class DeesAppui extends DeesElement {
|
||||
accessor secondarymenuHeading: string = '';
|
||||
|
||||
@property({ type: Array })
|
||||
accessor secondarymenuGroups: interfaces.IMenuGroup[] = [];
|
||||
accessor secondarymenuGroups: interfaces.ISecondaryMenuGroup[] = [];
|
||||
|
||||
@property({ type: Object })
|
||||
accessor secondarymenuSelectedItem: interfaces.IMenuItem | undefined = undefined;
|
||||
accessor secondarymenuSelectedItem: interfaces.ISecondaryMenuItemTab | undefined = undefined;
|
||||
|
||||
// Collapse states
|
||||
@property({ type: Boolean })
|
||||
@@ -126,6 +126,13 @@ export class DeesAppui extends DeesElement {
|
||||
@property({ type: Number })
|
||||
accessor contentTabsAutoHideThreshold: number = 0;
|
||||
|
||||
// Activity log visibility and count
|
||||
@state()
|
||||
accessor activityLogVisible: boolean = false;
|
||||
|
||||
@state()
|
||||
accessor activityLogCount: number = 0;
|
||||
|
||||
// Properties for maincontent
|
||||
@property({ type: Array })
|
||||
accessor maincontentTabs: interfaces.IMenuItem[] = [];
|
||||
@@ -175,8 +182,9 @@ export class DeesAppui extends DeesElement {
|
||||
height: calc(100% - 40px);
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: auto auto 1fr 240px;
|
||||
/* grid-template-columns set dynamically in template */
|
||||
grid-template-rows: 1fr;
|
||||
transition: grid-template-columns 0.3s ease;
|
||||
}
|
||||
|
||||
/* Z-index layering for proper stacking */
|
||||
@@ -198,6 +206,19 @@ export class DeesAppui extends DeesElement {
|
||||
.maingrid > dees-appui-activitylog {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.maingrid > dees-appui-activitylog.hidden {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.maingrid > dees-appui-activitylog.visible {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* View container for dynamically loaded views */
|
||||
@@ -221,14 +242,18 @@ export class DeesAppui extends DeesElement {
|
||||
.user=${this.appbarUser}
|
||||
.profileMenuItems=${this.appbarProfileMenuItems}
|
||||
.showSearch=${this.appbarShowSearch}
|
||||
.showActivityLogToggle=${true}
|
||||
.activityLogCount=${this.activityLogCount}
|
||||
.activityLogActive=${this.activityLogVisible}
|
||||
@menu-select=${(e: CustomEvent) => this.handleAppbarMenuSelect(e)}
|
||||
@breadcrumb-navigate=${(e: CustomEvent) => this.handleAppbarBreadcrumbNavigate(e)}
|
||||
@search-click=${() => this.handleAppbarSearchClick()}
|
||||
@search-query=${(e: CustomEvent) => this.handleAppbarSearchQuery(e)}
|
||||
@user-menu-open=${() => this.handleAppbarUserMenuOpen()}
|
||||
@profile-menu-select=${(e: CustomEvent) => this.handleAppbarProfileMenuSelect(e)}
|
||||
@activity-toggle=${() => this.toggleActivityLog()}
|
||||
></dees-appui-appbar>
|
||||
<div class="maingrid">
|
||||
<div class="maingrid" style="grid-template-columns: auto auto 1fr ${this.activityLogVisible ? '280px' : '0px'};">
|
||||
${this.mainmenuVisible ? html`
|
||||
<dees-appui-mainmenu
|
||||
.logoIcon=${this.mainmenuLogoIcon}
|
||||
@@ -264,7 +289,9 @@ export class DeesAppui extends DeesElement {
|
||||
<div class="view-container"></div>
|
||||
<slot name="maincontent"></slot>
|
||||
</dees-appui-maincontent>
|
||||
<dees-appui-activitylog></dees-appui-activitylog>
|
||||
<dees-appui-activitylog
|
||||
class="${this.activityLogVisible ? 'visible' : 'hidden'}"
|
||||
></dees-appui-activitylog>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -277,6 +304,13 @@ export class DeesAppui extends DeesElement {
|
||||
this.maincontent = this.shadowRoot!.querySelector('dees-appui-maincontent') as DeesAppuiMaincontent;
|
||||
this.activitylogElement = this.shadowRoot!.querySelector('dees-appui-activitylog') as DeesAppuiActivitylog;
|
||||
|
||||
// Subscribe to activity log entry changes for badge count
|
||||
if (this.activitylogElement) {
|
||||
this.activitylogElement.entries$.subscribe((entries) => {
|
||||
this.activityLogCount = entries.length;
|
||||
});
|
||||
}
|
||||
|
||||
// Set appui reference in view registry for lifecycle context
|
||||
this.viewRegistry.setAppuiRef(this as unknown as interfaces.TDeesAppui);
|
||||
}
|
||||
@@ -534,7 +568,7 @@ export class DeesAppui extends DeesElement {
|
||||
/**
|
||||
* Set the secondary menu configuration
|
||||
*/
|
||||
public setSecondaryMenu(config: { heading?: string; groups: interfaces.IMenuGroup[] }): void {
|
||||
public setSecondaryMenu(config: { heading?: string; groups: interfaces.ISecondaryMenuGroup[] }): void {
|
||||
if (config.heading !== undefined) {
|
||||
this.secondarymenuHeading = config.heading;
|
||||
}
|
||||
@@ -544,7 +578,7 @@ export class DeesAppui extends DeesElement {
|
||||
/**
|
||||
* Update a specific secondary menu group
|
||||
*/
|
||||
public updateSecondaryMenuGroup(groupName: string, update: Partial<interfaces.IMenuGroup>): void {
|
||||
public updateSecondaryMenuGroup(groupName: string, update: Partial<interfaces.ISecondaryMenuGroup>): void {
|
||||
this.secondarymenuGroups = this.secondarymenuGroups.map(group =>
|
||||
group.name === groupName ? { ...group, ...update } : group
|
||||
);
|
||||
@@ -555,7 +589,7 @@ export class DeesAppui extends DeesElement {
|
||||
*/
|
||||
public addSecondaryMenuItem(
|
||||
groupName: string,
|
||||
item: interfaces.IMenuGroup['items'][0]
|
||||
item: interfaces.ISecondaryMenuItem
|
||||
): void {
|
||||
this.secondarymenuGroups = this.secondarymenuGroups.map(group => {
|
||||
if (group.name === groupName) {
|
||||
@@ -569,13 +603,13 @@ export class DeesAppui extends DeesElement {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected secondary menu item by key
|
||||
* Set the selected secondary menu item by key (for tab items only)
|
||||
*/
|
||||
public setSecondaryMenuSelection(itemKey: string): void {
|
||||
for (const group of this.secondarymenuGroups) {
|
||||
const item = group.items.find(i => i.key === itemKey);
|
||||
if (item) {
|
||||
this.secondarymenuSelectedItem = item;
|
||||
const item = group.items.find(i => 'key' in i && i.key === itemKey);
|
||||
if (item && (!('type' in item) || item.type === 'tab' || item.type === undefined)) {
|
||||
this.secondarymenuSelectedItem = item as interfaces.ISecondaryMenuItemTab;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -673,6 +707,27 @@ export class DeesAppui extends DeesElement {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set activity log visibility
|
||||
*/
|
||||
public setActivityLogVisible(visible: boolean): void {
|
||||
this.activityLogVisible = visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle activity log visibility
|
||||
*/
|
||||
public toggleActivityLog(): void {
|
||||
this.activityLogVisible = !this.activityLogVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get activity log visibility state
|
||||
*/
|
||||
public getActivityLogVisible(): boolean {
|
||||
return this.activityLogVisible;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// PROGRAMMATIC API: NAVIGATION
|
||||
// ==========================================
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { TemplateResult } from '@design.estate/dees-element';
|
||||
import type { IAppBarMenuItem } from './appbarmenuitem.js';
|
||||
import type { IMenuItem } from './tab.js';
|
||||
import type { IMenuGroup } from './menugroup.js';
|
||||
import type { ISecondaryMenuGroup, ISecondaryMenuItem } from './secondarymenu.js';
|
||||
|
||||
// Forward declaration for circular reference
|
||||
export type TDeesAppui = HTMLElement & {
|
||||
@@ -25,9 +26,9 @@ export type TDeesAppui = HTMLElement & {
|
||||
setContentTabsAutoHide: (enabled: boolean, threshold?: number) => void;
|
||||
setMainMenuBadge: (tabKey: string, badge: string | number) => void;
|
||||
clearMainMenuBadge: (tabKey: string) => void;
|
||||
setSecondaryMenu: (config: { heading?: string; groups: IMenuGroup[] }) => void;
|
||||
updateSecondaryMenuGroup: (groupName: string, update: Partial<IMenuGroup>) => void;
|
||||
addSecondaryMenuItem: (groupName: string, item: IMenuGroup['items'][0]) => void;
|
||||
setSecondaryMenu: (config: { heading?: string; groups: ISecondaryMenuGroup[] }) => void;
|
||||
updateSecondaryMenuGroup: (groupName: string, update: Partial<ISecondaryMenuGroup>) => void;
|
||||
addSecondaryMenuItem: (groupName: string, item: ISecondaryMenuItem) => void;
|
||||
setSecondaryMenuSelection: (itemKey: string) => void;
|
||||
clearSecondaryMenu: () => void;
|
||||
setContentTabs: (tabs: IMenuItem[]) => void;
|
||||
@@ -36,6 +37,9 @@ export type TDeesAppui = HTMLElement & {
|
||||
selectContentTab: (tabKey: string) => void;
|
||||
getSelectedContentTab: () => IMenuItem | undefined;
|
||||
activityLog: IActivityLogAPI;
|
||||
setActivityLogVisible: (visible: boolean) => void;
|
||||
toggleActivityLog: () => void;
|
||||
getActivityLogVisible: () => boolean;
|
||||
navigateToView: (viewId: string, params?: Record<string, string>) => Promise<boolean>;
|
||||
getCurrentView: () => IViewDefinition | undefined;
|
||||
};
|
||||
@@ -136,7 +140,7 @@ export interface IViewDefinition {
|
||||
| (() => TemplateResult)
|
||||
| (() => Promise<string | (new () => HTMLElement) | (() => TemplateResult)>);
|
||||
/** Secondary menu items specific to this view */
|
||||
secondaryMenu?: IMenuGroup[];
|
||||
secondaryMenu?: ISecondaryMenuGroup[];
|
||||
/** Content tabs specific to this view */
|
||||
contentTabs?: IMenuItem[];
|
||||
/** Optional route path (defaults to id). Supports params like 'settings/:section' */
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './tab.js';
|
||||
export * from './appbarmenuitem.js';
|
||||
export * from './menugroup.js';
|
||||
export * from './appconfig.js';
|
||||
export * from './secondarymenu.js';
|
||||
|
||||
93
ts_web/elements/interfaces/secondarymenu.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Secondary Menu Item Types
|
||||
*
|
||||
* Supports 8 item types:
|
||||
* 1. Tab - selectable, stays highlighted (existing behavior)
|
||||
* 2. Action - executes without selection (primary = blue)
|
||||
* 3. Danger Action - red styling with optional confirmation
|
||||
* 4. Filter - checkbox toggle, emits immediately
|
||||
* 5. Multi-Filter - collapsible box with multiple checkboxes
|
||||
* 6. Divider - visual separator
|
||||
* 7. Header - non-interactive label
|
||||
* 8. Link - opens URL
|
||||
*/
|
||||
|
||||
// Base properties shared by interactive items
|
||||
export interface ISecondaryMenuItemBase {
|
||||
key: string;
|
||||
iconName?: string;
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
// 1. Tab - existing behavior (selectable, stays highlighted)
|
||||
export interface ISecondaryMenuItemTab extends ISecondaryMenuItemBase {
|
||||
type?: 'tab'; // default if omitted for backward compatibility
|
||||
action: () => void;
|
||||
badge?: string | number;
|
||||
badgeVariant?: 'default' | 'success' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
// 2 & 3. Action - executes without selection
|
||||
export interface ISecondaryMenuItemAction extends ISecondaryMenuItemBase {
|
||||
type: 'action';
|
||||
action: () => void | Promise<void>;
|
||||
variant?: 'primary' | 'danger'; // primary = blue (default), danger = red
|
||||
confirmMessage?: string; // Shows confirmation dialog before executing
|
||||
}
|
||||
|
||||
// 4. Single filter toggle
|
||||
export interface ISecondaryMenuItemFilter extends ISecondaryMenuItemBase {
|
||||
type: 'filter';
|
||||
active: boolean;
|
||||
onToggle: (active: boolean) => void;
|
||||
}
|
||||
|
||||
// 5. Multi-select filter group (collapsible)
|
||||
export interface ISecondaryMenuItemMultiFilter extends ISecondaryMenuItemBase {
|
||||
type: 'multiFilter';
|
||||
collapsed?: boolean; // Accordion state
|
||||
options: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
iconName?: string;
|
||||
}>;
|
||||
onChange: (selectedKeys: string[]) => void;
|
||||
}
|
||||
|
||||
// 6. Divider
|
||||
export interface ISecondaryMenuItemDivider {
|
||||
type: 'divider';
|
||||
}
|
||||
|
||||
// 7. Header/Label
|
||||
export interface ISecondaryMenuItemHeader {
|
||||
type: 'header';
|
||||
label: string;
|
||||
}
|
||||
|
||||
// 8. External link
|
||||
export interface ISecondaryMenuItemLink extends ISecondaryMenuItemBase {
|
||||
type: 'link';
|
||||
href: string;
|
||||
external?: boolean; // Opens in new tab (default: true if href starts with http)
|
||||
}
|
||||
|
||||
// Union type for all secondary menu items
|
||||
export type ISecondaryMenuItem =
|
||||
| ISecondaryMenuItemTab
|
||||
| ISecondaryMenuItemAction
|
||||
| ISecondaryMenuItemFilter
|
||||
| ISecondaryMenuItemMultiFilter
|
||||
| ISecondaryMenuItemDivider
|
||||
| ISecondaryMenuItemHeader
|
||||
| ISecondaryMenuItemLink;
|
||||
|
||||
// Group interface for secondary menu
|
||||
export interface ISecondaryMenuGroup {
|
||||
name: string;
|
||||
iconName?: string;
|
||||
collapsed?: boolean;
|
||||
items: ISecondaryMenuItem[];
|
||||
}
|
||||