feat: first take at UI app
This commit is contained in:
701
priv/ui/app.css
Normal file
701
priv/ui/app.css
Normal file
@@ -0,0 +1,701 @@
|
||||
:root {
|
||||
--bg: #f2efe8;
|
||||
--paper: #f8f5ef;
|
||||
--ink: #1e1b18;
|
||||
--muted: #6f665d;
|
||||
--line: rgba(34, 28, 23, 0.12);
|
||||
--line-strong: rgba(34, 28, 23, 0.22);
|
||||
--accent: #b4472f;
|
||||
--accent-soft: rgba(180, 71, 47, 0.12);
|
||||
--activity: #171411;
|
||||
--activity-ink: #f4efe7;
|
||||
--shadow: 0 20px 60px rgba(41, 30, 20, 0.12);
|
||||
--radius: 18px;
|
||||
--sidebar-width: 280px;
|
||||
--assistant-width: 360px;
|
||||
--panel-height: 168px;
|
||||
--title-height: 56px;
|
||||
--status-height: 38px;
|
||||
--activity-width: 60px;
|
||||
color-scheme: light;
|
||||
font-family: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Georgia, serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(202, 156, 86, 0.18), transparent 28%),
|
||||
radial-gradient(circle at top right, rgba(180, 71, 47, 0.12), transparent 26%),
|
||||
linear-gradient(180deg, #f7f2e8 0%, #efe8dc 100%);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--vscode-foreground: #cccccc;
|
||||
--vscode-descriptionForeground: #8c8c8c;
|
||||
--vscode-editor-background: #1e1e1e;
|
||||
--vscode-editorGroupHeader-tabsBackground: #252526;
|
||||
--vscode-editorGroupHeader-tabsBorder: #1f1f1f;
|
||||
--vscode-panel-border: #2b2b2b;
|
||||
--vscode-sideBar-background: #181818;
|
||||
--vscode-sideBar-border: #2b2b2b;
|
||||
--vscode-sideBar-foreground: #cccccc;
|
||||
--vscode-statusBar-background: #007acc;
|
||||
--vscode-statusBar-foreground: #ffffff;
|
||||
--vscode-activityBar-background: #181818;
|
||||
--vscode-activityBar-foreground: #cccccc;
|
||||
--vscode-titleBar-activeForeground: #cccccc;
|
||||
--vscode-list-hoverBackground: #2a2d2e;
|
||||
--vscode-list-activeSelectionBackground: #37373d;
|
||||
--vscode-tab-inactiveBackground: #2d2d2d;
|
||||
--vscode-tab-activeBackground: #1e1e1e;
|
||||
--vscode-tab-activeBorderTop: #0078d4;
|
||||
--vscode-tab-border: #252526;
|
||||
--vscode-menu-background: #252526;
|
||||
--vscode-menu-selectionBackground: #094771;
|
||||
--vscode-menu-border: #454545;
|
||||
--vscode-widget-shadow: 0 10px 24px rgba(0, 0, 0, 0.35);
|
||||
--sidebar-width: 320px;
|
||||
--assistant-width: 336px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: var(--vscode-editor-background);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.window-titlebar {
|
||||
position: relative;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--vscode-editorGroupHeader-tabsBackground);
|
||||
border-bottom: 1px solid var(--vscode-editorGroupHeader-tabsBorder);
|
||||
flex-shrink: 0;
|
||||
-webkit-app-region: drag;
|
||||
app-region: drag;
|
||||
}
|
||||
|
||||
.window-titlebar-menu-bar,
|
||||
.window-titlebar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
gap: 2px;
|
||||
padding: 0 6px;
|
||||
-webkit-app-region: no-drag;
|
||||
app-region: no-drag;
|
||||
}
|
||||
|
||||
.window-titlebar-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 12px;
|
||||
color: var(--vscode-titleBar-activeForeground);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.window-titlebar-menu-button,
|
||||
.window-titlebar-action-button {
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--vscode-titleBar-activeForeground);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.window-titlebar-menu-button:hover,
|
||||
.window-titlebar-action-button:hover {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.window-titlebar-sidebar-icon,
|
||||
.window-titlebar-panel-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 1.5px solid currentColor;
|
||||
border-radius: 2px;
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.window-titlebar-sidebar-icon::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 33%;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1.5px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.window-titlebar-sidebar-pane {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 33%;
|
||||
height: 100%;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.window-titlebar-panel-icon::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 66%;
|
||||
height: 1.5px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.activity-bar {
|
||||
width: 48px;
|
||||
height: 100%;
|
||||
background-color: var(--vscode-activityBar-background);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border-right: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.activity-bar-top,
|
||||
.activity-bar-bottom {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.activity-bar-item {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--vscode-activityBar-foreground);
|
||||
opacity: 0.6;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.activity-bar-item.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.activity-bar-item.active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 2px;
|
||||
background-color: var(--vscode-activityBar-foreground);
|
||||
}
|
||||
|
||||
.sidebar-shell,
|
||||
.assistant-sidebar-shell {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sidebar-shell {
|
||||
width: var(--sidebar-width);
|
||||
}
|
||||
|
||||
.assistant-sidebar-shell {
|
||||
width: var(--assistant-width);
|
||||
}
|
||||
|
||||
.sidebar,
|
||||
.assistant-sidebar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--vscode-sideBar-border);
|
||||
}
|
||||
|
||||
.assistant-sidebar {
|
||||
border-left: 1px solid var(--vscode-sideBar-border);
|
||||
}
|
||||
|
||||
.sidebar-header,
|
||||
.assistant-header,
|
||||
.panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--vscode-sideBar-foreground);
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.sidebar-content,
|
||||
.assistant-content,
|
||||
.panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.sidebar-item,
|
||||
.assistant-card,
|
||||
.panel-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.sidebar-item:hover,
|
||||
.assistant-card:hover,
|
||||
.panel-entry:hover {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.sidebar-item.active {
|
||||
background-color: var(--vscode-list-activeSelectionBackground);
|
||||
border-left: 2px solid var(--vscode-tab-activeBorderTop);
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.sidebar-item strong,
|
||||
.assistant-card strong,
|
||||
.panel-entry strong {
|
||||
font-size: 13px;
|
||||
color: var(--vscode-sideBar-foreground);
|
||||
}
|
||||
|
||||
.sidebar-item span,
|
||||
.assistant-card span,
|
||||
.panel-entry span,
|
||||
.editor-subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
}
|
||||
|
||||
.app-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--vscode-editorGroupHeader-tabsBackground);
|
||||
border-bottom: 1px solid var(--vscode-editorGroupHeader-tabsBorder);
|
||||
height: 35px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-bar-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
overflow-x: auto;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.tab-bar-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 10px;
|
||||
height: 100%;
|
||||
min-width: 110px;
|
||||
max-width: 180px;
|
||||
cursor: pointer;
|
||||
background-color: var(--vscode-tab-inactiveBackground);
|
||||
border-right: 1px solid var(--vscode-tab-border);
|
||||
color: #969696;
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
background-color: var(--vscode-tab-activeBackground);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: var(--vscode-tab-activeBorderTop);
|
||||
}
|
||||
|
||||
.tab.transient .tab-title {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.tab-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-close,
|
||||
.tab-dirty-indicator {
|
||||
width: 18px;
|
||||
text-align: center;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.editor-shell {
|
||||
flex: 1;
|
||||
background: var(--vscode-editor-background);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.editor-frame {
|
||||
min-height: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(260px, 0.75fr);
|
||||
}
|
||||
|
||||
.editor-main {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.editor-meta {
|
||||
border-left: 1px solid var(--vscode-panel-border);
|
||||
background: #181818;
|
||||
padding: 18px 16px;
|
||||
}
|
||||
|
||||
.editor-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.editor-toolbar {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin: 16px 0 20px;
|
||||
}
|
||||
|
||||
.editor-toolbar button {
|
||||
border: 1px solid var(--vscode-panel-border);
|
||||
background: #252526;
|
||||
color: var(--vscode-foreground);
|
||||
padding: 6px 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.editor-paragraph {
|
||||
max-width: 72ch;
|
||||
line-height: 1.6;
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.panel-shell {
|
||||
height: 220px;
|
||||
background: #181818;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.panel-tab {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
padding: 8px 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.panel-tab.active {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
height: 22px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--vscode-statusBar-background);
|
||||
color: var(--vscode-statusBar-foreground);
|
||||
font-size: 12px;
|
||||
padding: 0 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-bar-left,
|
||||
.status-bar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.status-bar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.resizable-panel-divider {
|
||||
width: 4px;
|
||||
background: transparent;
|
||||
cursor: col-resize;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.resizable-panel-divider:hover,
|
||||
.resizable-panel-divider.is-dragging {
|
||||
background: rgba(0, 120, 212, 0.35);
|
||||
}
|
||||
|
||||
.is-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.assistant-sidebar-shell {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.sidebar-shell {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.editor-frame {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.editor-meta {
|
||||
border-left: none;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
}
|
||||
[data-role="resize-handle"] {
|
||||
background: linear-gradient(180deg, transparent, rgba(120, 93, 71, 0.3), transparent);
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
[data-role="resize-handle"][data-target="sidebar"] {
|
||||
grid-area: sidebar-handle;
|
||||
}
|
||||
|
||||
[data-role="resize-handle"][data-target="assistant"] {
|
||||
grid-area: assistant-handle;
|
||||
}
|
||||
|
||||
#bds-shell-app[data-sidebar-visible="false"] [data-role="resize-handle"][data-target="sidebar"],
|
||||
#bds-shell-app[data-assistant-visible="false"] [data-role="resize-handle"][data-target="assistant"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-header,
|
||||
.assistant-header {
|
||||
padding: 18px 18px 10px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.sidebar-header strong,
|
||||
.assistant-header strong {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.sidebar-placeholder,
|
||||
.assistant-placeholder {
|
||||
padding: 18px;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.placeholder-note {
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
[data-region="status-bar"] {
|
||||
grid-area: status;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 0 14px;
|
||||
border-top: 1px solid var(--line);
|
||||
background: rgba(24, 19, 15, 0.92);
|
||||
color: rgba(255, 248, 240, 0.88);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.status-pill,
|
||||
.status-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.status-post[data-status="draft"]::before,
|
||||
.status-post[data-status="published"]::before,
|
||||
.status-post[data-status="archived"]::before {
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 999px;
|
||||
background: #d6a347;
|
||||
}
|
||||
|
||||
.status-post[data-status="published"]::before {
|
||||
background: #5aa86b;
|
||||
}
|
||||
|
||||
.status-post[data-status="archived"]::before {
|
||||
background: #8b7c72;
|
||||
}
|
||||
|
||||
.status-right select,
|
||||
.status-left select {
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
color: inherit;
|
||||
padding: 6px 10px;
|
||||
}
|
||||
|
||||
.offline-toggle[data-active="true"] {
|
||||
background: rgba(180, 71, 47, 0.26);
|
||||
border-color: rgba(255, 255, 255, 0.08);
|
||||
color: #fff5ec;
|
||||
}
|
||||
|
||||
kbd {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 7px;
|
||||
background: rgba(22, 18, 14, 0.08);
|
||||
border: 1px solid rgba(22, 18, 14, 0.12);
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
#bds-shell-app,
|
||||
#bds-shell-app[data-sidebar-visible="false"],
|
||||
#bds-shell-app[data-assistant-visible="false"],
|
||||
#bds-shell-app[data-sidebar-visible="false"][data-assistant-visible="false"] {
|
||||
grid-template-columns: var(--activity-width) minmax(0, 1fr);
|
||||
grid-template-rows: var(--title-height) auto minmax(180px, auto) minmax(320px, 1fr) auto var(--status-height);
|
||||
grid-template-areas:
|
||||
"title title"
|
||||
"activity activity"
|
||||
"sidebar sidebar"
|
||||
"content content"
|
||||
"assistant assistant"
|
||||
"status status";
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[data-role="resize-handle"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
[data-region="activity-bar"] {
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.activity-stack {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
[data-region="status-bar"] {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
}
|
||||
688
priv/ui/app.js
Normal file
688
priv/ui/app.js
Normal file
@@ -0,0 +1,688 @@
|
||||
const shell = document.getElementById("bds-shell-app");
|
||||
const bootstrap = JSON.parse(document.getElementById("bds-shell-bootstrap").textContent);
|
||||
const bootstrap = JSON.parse(document.getElementById('bds-bootstrap').textContent);
|
||||
|
||||
const state = {
|
||||
...bootstrap,
|
||||
};
|
||||
|
||||
const root = document.getElementById('app');
|
||||
|
||||
function render() {
|
||||
root.style.setProperty('--sidebar-width', state.sidebarVisible ? `${state.sidebarWidth}px` : '0px');
|
||||
root.style.setProperty('--assistant-width', state.assistantVisible ? `${state.assistantWidth}px` : '0px');
|
||||
|
||||
renderMenuBar();
|
||||
renderActivityBar();
|
||||
renderSidebar();
|
||||
renderTabs();
|
||||
renderEditor();
|
||||
renderPanel();
|
||||
renderAssistant();
|
||||
renderStatusBar();
|
||||
applyVisibility();
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
function renderMenuBar() {
|
||||
const menuBar = root.querySelector('.window-titlebar-menu-bar');
|
||||
menuBar.innerHTML = state.menuGroups
|
||||
.map((group) => `<button class="window-titlebar-menu-button">${group.label}</button>`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function renderActivityBar() {
|
||||
const node = root.querySelector('.activity-bar');
|
||||
const top = state.sidebarViews.filter((view) => view.group === 'top');
|
||||
const bottom = state.sidebarViews.filter((view) => view.group === 'bottom');
|
||||
|
||||
node.innerHTML = `
|
||||
<div class="activity-bar-top">${top.map(renderActivityButton).join('')}</div>
|
||||
<div class="activity-bar-bottom">${bottom.map(renderActivityButton).join('')}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderActivityButton(view) {
|
||||
const active = state.sidebarVisible && state.activeView === view.id;
|
||||
return `<button class="activity-bar-item ${active ? 'active' : ''}" data-activity="${view.id}" title="${view.label}">${view.label.slice(0, 1)}</button>`;
|
||||
}
|
||||
|
||||
function renderSidebar() {
|
||||
const view = state.sidebarViews.find((entry) => entry.id === state.activeView) || state.sidebarViews[0];
|
||||
const node = root.querySelector('.sidebar');
|
||||
|
||||
node.innerHTML = `
|
||||
<div class="sidebar-header">
|
||||
<span>${view.label}</span>
|
||||
<span>${view.items.length}</span>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
${view.items
|
||||
.map((item, index) => {
|
||||
const itemId = `${view.id}:${index}`;
|
||||
const active = state.activeTabId === itemId;
|
||||
return `
|
||||
<button class="sidebar-item ${active ? 'active' : ''}" data-open-tab="${itemId}">
|
||||
<strong>${item}</strong>
|
||||
<span>${view.label} entry</span>
|
||||
</button>
|
||||
`;
|
||||
})
|
||||
.join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
const node = root.querySelector('.tab-bar');
|
||||
node.innerHTML = `<div class="tab-bar-tabs">${state.tabs.map(renderTab).join('')}</div>`;
|
||||
}
|
||||
|
||||
function renderTab(tab) {
|
||||
const active = tab.id === state.activeTabId;
|
||||
const dirtyMarker = tab.dirty ? '<span class="tab-dirty-indicator">●</span>' : '<span class="tab-close">×</span>';
|
||||
return `
|
||||
<div class="tab ${active ? 'active' : ''} ${tab.pinned ? '' : 'transient'}" data-tab="${tab.id}">
|
||||
<span class="tab-title">${tab.title}</span>
|
||||
${dirtyMarker}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderEditor() {
|
||||
const node = root.querySelector('.editor-shell');
|
||||
const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId) || state.tabs[0];
|
||||
|
||||
node.innerHTML = `
|
||||
<div class="editor-frame">
|
||||
<section class="editor-main">
|
||||
<h1 class="editor-title">${activeTab.title}</h1>
|
||||
<div class="editor-subtitle">${activeTab.kind} editor surface routed through the desktop shell</div>
|
||||
<div class="editor-toolbar">
|
||||
<button type="button">Publish</button>
|
||||
<button type="button">Preview</button>
|
||||
<button type="button">Metadata</button>
|
||||
</div>
|
||||
<p class="editor-paragraph">This desktop shell now runs inside an Elixir Desktop window instead of a standalone browser page. The frame, activity bar, tab strip, status bar and resizable side areas match the old application structure so the next slice can replace placeholders with the real editors and lists.</p>
|
||||
<p class="editor-paragraph">Preview tabs, dirty state and shell routing still come from the shared Elixir workbench modules. The runtime boundary is now a desktop window backed by a local shell server.</p>
|
||||
</section>
|
||||
<aside class="editor-meta">
|
||||
<div class="assistant-card">
|
||||
<strong>Document State</strong>
|
||||
<span>${activeTab.dirty ? 'Unsaved changes' : 'Clean'}</span>
|
||||
</div>
|
||||
<div class="assistant-card">
|
||||
<strong>Publishing</strong>
|
||||
<span>Main language: en</span>
|
||||
</div>
|
||||
<div class="assistant-card">
|
||||
<strong>Filesystem</strong>
|
||||
<span>Metadata flush pending wiring</span>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderPanel() {
|
||||
const node = root.querySelector('.panel-shell');
|
||||
const tabs = ['problems', 'search', 'tasks'];
|
||||
|
||||
node.innerHTML = `
|
||||
<div class="panel-header">
|
||||
<div class="panel-tabs">
|
||||
${tabs
|
||||
.map((tab) => `<button class="panel-tab ${state.panelTab === tab ? 'active' : ''}" data-panel-tab="${tab}">${tab}</button>`)
|
||||
.join('')}
|
||||
</div>
|
||||
<span>${state.panelVisible ? 'Visible' : 'Hidden'}</span>
|
||||
</div>
|
||||
<div class="panel-content">
|
||||
<div class="panel-entry">
|
||||
<strong>${state.panelTab}</strong>
|
||||
<span>Shared bottom panel host for problems, search, tasks and later runtime details.</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderAssistant() {
|
||||
const node = root.querySelector('.assistant-sidebar');
|
||||
node.innerHTML = `
|
||||
<div class="assistant-header">
|
||||
<span>Assistant</span>
|
||||
<span>Project context</span>
|
||||
</div>
|
||||
<div class="assistant-content">
|
||||
<div class="assistant-card">
|
||||
<strong>Next shell work</strong>
|
||||
<span>Swap sidebar placeholders with real project data views.</span>
|
||||
</div>
|
||||
<div class="assistant-card">
|
||||
<strong>Offline gate</strong>
|
||||
<span>Automatic AI remains gated by airplane mode.</span>
|
||||
</div>
|
||||
<div class="assistant-card">
|
||||
<strong>Desktop runtime</strong>
|
||||
<span>Window, menu bar and launch path now come from Elixir Desktop.</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderStatusBar() {
|
||||
const node = root.querySelector('.status-bar');
|
||||
node.innerHTML = `
|
||||
<div class="status-bar-left">${state.statusBar.left.map(renderStatusItem).join('')}</div>
|
||||
<div class="status-bar-right">${state.statusBar.right.map(renderStatusItem).join('')}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderStatusItem(item) {
|
||||
return `<div class="status-bar-item">${item.label}</div>`;
|
||||
}
|
||||
|
||||
function applyVisibility() {
|
||||
root.querySelector('.sidebar-shell').classList.toggle('is-hidden', !state.sidebarVisible);
|
||||
root.querySelector('.assistant-sidebar-shell').classList.toggle('is-hidden', !state.assistantVisible);
|
||||
root.querySelector('.panel-shell').classList.toggle('is-hidden', !state.panelVisible);
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
root.querySelectorAll('[data-activity]').forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const next = button.getAttribute('data-activity');
|
||||
if (state.activeView === next && state.sidebarVisible) {
|
||||
state.sidebarVisible = false;
|
||||
} else {
|
||||
state.activeView = next;
|
||||
state.sidebarVisible = true;
|
||||
}
|
||||
render();
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll('[data-open-tab]').forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const tabId = button.getAttribute('data-open-tab');
|
||||
let tab = state.tabs.find((entry) => entry.id === tabId);
|
||||
if (!tab) {
|
||||
tab = {
|
||||
id: tabId,
|
||||
title: tabId.split(':').pop(),
|
||||
kind: state.activeView,
|
||||
pinned: false,
|
||||
dirty: false,
|
||||
};
|
||||
state.tabs.push(tab);
|
||||
}
|
||||
state.activeTabId = tab.id;
|
||||
render();
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll('[data-tab]').forEach((tab) => {
|
||||
tab.onclick = () => {
|
||||
state.activeTabId = tab.getAttribute('data-tab');
|
||||
render();
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll('[data-command]').forEach((button) => {
|
||||
button.onclick = () => {
|
||||
const command = button.getAttribute('data-command');
|
||||
if (command === 'toggle-sidebar') {
|
||||
state.sidebarVisible = !state.sidebarVisible;
|
||||
}
|
||||
if (command === 'toggle-panel') {
|
||||
state.panelVisible = !state.panelVisible;
|
||||
}
|
||||
render();
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll('[data-panel-tab]').forEach((button) => {
|
||||
button.onclick = () => {
|
||||
state.panelTab = button.getAttribute('data-panel-tab');
|
||||
state.panelVisible = true;
|
||||
render();
|
||||
};
|
||||
});
|
||||
|
||||
root.querySelectorAll('[data-resize]').forEach((handle) => {
|
||||
handle.onpointerdown = (event) => {
|
||||
const target = handle.getAttribute('data-resize');
|
||||
const startX = event.clientX;
|
||||
const startWidth = target === 'sidebar' ? state.sidebarWidth : state.assistantWidth;
|
||||
handle.classList.add('is-dragging');
|
||||
|
||||
const onMove = (moveEvent) => {
|
||||
const delta = moveEvent.clientX - startX;
|
||||
if (target === 'sidebar') {
|
||||
state.sidebarWidth = clamp(startWidth + delta, 220, 520);
|
||||
} else {
|
||||
state.assistantWidth = clamp(startWidth - delta, 280, 520);
|
||||
}
|
||||
render();
|
||||
};
|
||||
|
||||
const onUp = () => {
|
||||
handle.classList.remove('is-dragging');
|
||||
window.removeEventListener('pointermove', onMove);
|
||||
window.removeEventListener('pointerup', onUp);
|
||||
};
|
||||
|
||||
window.addEventListener('pointermove', onMove);
|
||||
window.addEventListener('pointerup', onUp);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
render();
|
||||
function renderStatusBar() {
|
||||
const active = activeTab();
|
||||
const dirty = active && rootState.dirty_tabs.some(entry => sameTab(entry, active));
|
||||
const postStatus = active && active.type === "post" ? (dirty ? "draft" : "published") : null;
|
||||
const tokenUsage = active && active.type === "chat" ? "I 120 · O 42 · C 8" : null;
|
||||
|
||||
statusBar.innerHTML = `
|
||||
<div class="status-left">
|
||||
<select data-action="project-select">
|
||||
<option selected>${rootState.project}</option>
|
||||
</select>
|
||||
<span class="status-pill">${rootState.running_task_message}${rootState.running_task_overflow ? ` +${rootState.running_task_overflow}` : ""}</span>
|
||||
</div>
|
||||
<div class="status-right">
|
||||
${postStatus ? `<span class="status-chip status-post" data-status="${postStatus}">${postStatus}</span>` : ""}
|
||||
<span class="status-chip">${rootState.post_count} posts</span>
|
||||
<span class="status-chip">${rootState.media_count} media</span>
|
||||
${tokenUsage ? `<span class="status-chip">${tokenUsage}</span>` : ""}
|
||||
<select data-action="theme-select">
|
||||
${["zinc", "amber", "jade", "sand"].map(theme => `<option value="${theme}" ${theme === rootState.theme_badge ? "selected" : ""}>${theme}</option>`).join("")}
|
||||
</select>
|
||||
<button class="offline-toggle" data-action="toggle-offline" data-active="${String(rootState.offline_mode)}">${rootState.offline_mode ? "Offline" : "Online"}</button>
|
||||
<select data-action="language-select">
|
||||
${["en", "de", "fr", "it", "es"].map(language => `<option value="${language}" ${language === rootState.ui_language ? "selected" : ""}>${language.toUpperCase()}</option>`).join("")}
|
||||
</select>
|
||||
<span class="status-pill">bDS</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function wireGlobalEvents() {
|
||||
document.addEventListener("click", handleClick);
|
||||
document.addEventListener("dblclick", handleDoubleClick);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
document.addEventListener("change", handleChange);
|
||||
document.querySelectorAll('[data-role="resize-handle"]').forEach(handle => {
|
||||
handle.addEventListener("pointerdown", startResize);
|
||||
});
|
||||
}
|
||||
|
||||
function handleClick(event) {
|
||||
const commandButton = event.target.closest("[data-command]");
|
||||
if (commandButton) {
|
||||
event.preventDefault();
|
||||
executeCommand(commandButton.dataset.command);
|
||||
return;
|
||||
}
|
||||
|
||||
const menuTrigger = event.target.closest("[data-menu-trigger]");
|
||||
if (menuTrigger) {
|
||||
const id = menuTrigger.dataset.menuTrigger;
|
||||
openMenu = openMenu === id ? null : id;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!event.target.closest(".menu-group")) {
|
||||
if (openMenu !== null) {
|
||||
openMenu = null;
|
||||
renderTitleBar();
|
||||
}
|
||||
}
|
||||
|
||||
const activity = event.target.closest("[data-activity]");
|
||||
if (activity) {
|
||||
clickActivity(activity.dataset.activity);
|
||||
return;
|
||||
}
|
||||
|
||||
const activate = event.target.closest("[data-tab-activate]");
|
||||
if (activate) {
|
||||
const [type, id] = activate.dataset.tabActivate.split(":");
|
||||
rootState.active_tab = { type, id };
|
||||
normalizePanel();
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
const closeButton = event.target.closest("[data-tab-close]");
|
||||
if (closeButton) {
|
||||
const [type, id] = closeButton.dataset.tabClose.split(":");
|
||||
closeTab(type, id);
|
||||
return;
|
||||
}
|
||||
|
||||
const demoButton = event.target.closest("[data-open-demo]");
|
||||
if (demoButton) {
|
||||
openDemoFromView(demoButton.dataset.openDemo);
|
||||
return;
|
||||
}
|
||||
|
||||
const action = event.target.closest("[data-action]");
|
||||
if (action) {
|
||||
handleAction(action.dataset.action);
|
||||
return;
|
||||
}
|
||||
|
||||
const panelTab = event.target.closest("[data-panel-tab]");
|
||||
if (panelTab && !panelTab.disabled) {
|
||||
rootState.panel.active_tab = panelTab.dataset.panelTab;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function handleDoubleClick(event) {
|
||||
const tab = event.target.closest("[data-tab-type][data-tab-id]");
|
||||
if (!tab) return;
|
||||
|
||||
const type = tab.dataset.tabType;
|
||||
const id = tab.dataset.tabId;
|
||||
const found = rootState.tabs.find(entry => entry.type === type && entry.id === id);
|
||||
|
||||
if (found && found.is_transient) {
|
||||
found.is_transient = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
const primary = event.metaKey || event.ctrlKey;
|
||||
if (!primary) return;
|
||||
|
||||
const key = event.key.toLowerCase();
|
||||
|
||||
if (key === "b") {
|
||||
event.preventDefault();
|
||||
executeCommand("toggle_sidebar");
|
||||
}
|
||||
|
||||
if (key === "w") {
|
||||
event.preventDefault();
|
||||
executeCommand("close_tab");
|
||||
}
|
||||
}
|
||||
|
||||
function handleChange(event) {
|
||||
const action = event.target.dataset.action;
|
||||
if (!action) return;
|
||||
|
||||
if (action === "theme-select") {
|
||||
rootState.theme_badge = event.target.value;
|
||||
render();
|
||||
}
|
||||
|
||||
if (action === "language-select") {
|
||||
rootState.ui_language = event.target.value;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function startResize(event) {
|
||||
const target = event.currentTarget.dataset.target;
|
||||
const initialX = event.clientX;
|
||||
const initialWidth = target === "sidebar" ? rootState.sidebar_width : rootState.assistant_sidebar_width;
|
||||
|
||||
const move = moveEvent => {
|
||||
const delta = moveEvent.clientX - initialX;
|
||||
|
||||
if (target === "sidebar") {
|
||||
rootState.sidebar_width = clamp(initialWidth + delta, 200, 500);
|
||||
} else {
|
||||
rootState.assistant_sidebar_width = clamp(initialWidth - delta, 280, 640);
|
||||
}
|
||||
|
||||
render();
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
window.removeEventListener("pointermove", move);
|
||||
window.removeEventListener("pointerup", stop);
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", move);
|
||||
window.addEventListener("pointerup", stop);
|
||||
}
|
||||
|
||||
function executeCommand(command) {
|
||||
if (command === "toggle_sidebar") {
|
||||
rootState.sidebar_visible = !rootState.sidebar_visible;
|
||||
}
|
||||
|
||||
if (command === "toggle_panel") {
|
||||
rootState.panel.visible = !rootState.panel.visible;
|
||||
}
|
||||
|
||||
if (command === "toggle_assistant_sidebar") {
|
||||
rootState.assistant_sidebar_visible = !rootState.assistant_sidebar_visible;
|
||||
}
|
||||
|
||||
if (command === "close_tab" && rootState.active_tab) {
|
||||
closeTab(rootState.active_tab.type, rootState.active_tab.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === "settings") {
|
||||
openTab("settings", "settings", "pin");
|
||||
}
|
||||
|
||||
if (command === "documentation") {
|
||||
openTab("documentation", "documentation", "pin");
|
||||
}
|
||||
|
||||
if (command === "api_documentation") {
|
||||
openTab("api_documentation", "api_documentation", "pin");
|
||||
}
|
||||
|
||||
normalizePanel();
|
||||
render();
|
||||
}
|
||||
|
||||
function handleAction(action) {
|
||||
if (action === "toggle-dirty") {
|
||||
const active = activeTab();
|
||||
if (!active || active.type !== "post") return;
|
||||
|
||||
const index = rootState.dirty_tabs.findIndex(entry => sameTab(entry, active));
|
||||
if (index >= 0) {
|
||||
rootState.dirty_tabs.splice(index, 1);
|
||||
} else {
|
||||
rootState.dirty_tabs.push({ type: active.type, id: active.id });
|
||||
}
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "pin-active") {
|
||||
const active = activeTab();
|
||||
if (!active) return;
|
||||
const found = rootState.tabs.find(tab => sameTab(tab, active));
|
||||
if (found) found.is_transient = false;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "toggle-offline") {
|
||||
rootState.offline_mode = !rootState.offline_mode;
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "reset-session") {
|
||||
localStorage.removeItem(sessionKey);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action === "open-chat") {
|
||||
openTab("chat", "conversation-demo", "pin");
|
||||
}
|
||||
}
|
||||
|
||||
function clickActivity(id) {
|
||||
if (rootState.active_view === id) {
|
||||
rootState.sidebar_visible = !rootState.sidebar_visible;
|
||||
} else {
|
||||
rootState.active_view = id;
|
||||
rootState.sidebar_visible = true;
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function openDemoFromView(mode) {
|
||||
const view = registry.sidebar_views.find(entry => entry.id === rootState.active_view) || registry.sidebar_views[0];
|
||||
const type = view.editor_route;
|
||||
const isSingleton = !!view.singleton;
|
||||
const id = isSingleton ? type : `${rootState.active_view}-demo-${view.activity_group === "bottom" ? "tool" : "item"}`;
|
||||
const intent = mode === "pin" ? "pin" : "preview";
|
||||
|
||||
if (mode === "background") {
|
||||
openTab(type, id, intent, true);
|
||||
} else {
|
||||
openTab(type, id, intent, false);
|
||||
}
|
||||
}
|
||||
|
||||
function openTab(type, id, intent, background) {
|
||||
const existing = rootState.tabs.find(tab => tab.type === type && tab.id === id);
|
||||
const singleton = !!routeMeta(type)?.singleton;
|
||||
const sticky = type === "chat" || type === "import" || singleton;
|
||||
const transient = !sticky && intent === "preview";
|
||||
|
||||
if (existing) {
|
||||
if (intent === "pin") existing.is_transient = false;
|
||||
if (!background) rootState.active_tab = { type, id };
|
||||
normalizePanel();
|
||||
render();
|
||||
return;
|
||||
}
|
||||
|
||||
if (transient) {
|
||||
const replacementIndex = rootState.tabs.findIndex(tab => tab.type === type && tab.is_transient);
|
||||
const newTab = { type, id, is_transient: true };
|
||||
if (replacementIndex >= 0) {
|
||||
rootState.tabs.splice(replacementIndex, 1, newTab);
|
||||
} else {
|
||||
rootState.tabs.push(newTab);
|
||||
}
|
||||
} else {
|
||||
rootState.tabs.push({ type, id, is_transient: false });
|
||||
}
|
||||
|
||||
if (!background) {
|
||||
rootState.active_tab = { type, id };
|
||||
}
|
||||
|
||||
normalizePanel();
|
||||
render();
|
||||
}
|
||||
|
||||
function closeTab(type, id) {
|
||||
const index = rootState.tabs.findIndex(tab => tab.type === type && tab.id === id);
|
||||
if (index < 0) return;
|
||||
|
||||
rootState.tabs.splice(index, 1);
|
||||
rootState.dirty_tabs = rootState.dirty_tabs.filter(tab => !(tab.type === type && tab.id === id));
|
||||
|
||||
if (rootState.active_tab && rootState.active_tab.type === type && rootState.active_tab.id === id) {
|
||||
if (rootState.tabs[index]) {
|
||||
rootState.active_tab = { type: rootState.tabs[index].type, id: rootState.tabs[index].id };
|
||||
} else if (rootState.tabs[index - 1]) {
|
||||
rootState.active_tab = { type: rootState.tabs[index - 1].type, id: rootState.tabs[index - 1].id };
|
||||
} else {
|
||||
rootState.active_tab = null;
|
||||
}
|
||||
}
|
||||
|
||||
normalizePanel();
|
||||
render();
|
||||
}
|
||||
|
||||
function normalizePanel() {
|
||||
const route = activeTab() ? activeTab().type : "dashboard";
|
||||
if (!panelAvailable(route, rootState.panel.active_tab)) {
|
||||
rootState.panel.active_tab = "tasks";
|
||||
}
|
||||
}
|
||||
|
||||
function panelAvailable(route, tab) {
|
||||
if (tab === "tasks" || tab === "output") return true;
|
||||
if (tab === "post_links") return route === "post";
|
||||
if (tab === "git_log") return route === "post" || route === "media";
|
||||
return false;
|
||||
}
|
||||
|
||||
function activeTab() {
|
||||
if (!rootState.active_tab) return null;
|
||||
return rootState.tabs.find(tab => sameTab(tab, rootState.active_tab)) || null;
|
||||
}
|
||||
|
||||
function routeMeta(id) {
|
||||
return registry.editor_routes.find(route => route.id === id) || null;
|
||||
}
|
||||
|
||||
function sameTab(left, right) {
|
||||
return !!left && !!right && left.type === right.type && left.id === right.id;
|
||||
}
|
||||
|
||||
function tabTitle(tab) {
|
||||
const meta = routeMeta(tab.type);
|
||||
const prefix = meta ? meta.title : titleCase(tab.type);
|
||||
if (meta && meta.singleton) return prefix;
|
||||
return `${prefix} ${tab.id}`;
|
||||
}
|
||||
|
||||
function labelForPanel(tab) {
|
||||
return {
|
||||
tasks: "Tasks",
|
||||
output: "Output",
|
||||
post_links: "Post Links",
|
||||
git_log: "Git Log"
|
||||
}[tab] || titleCase(tab);
|
||||
}
|
||||
|
||||
function labelForCommand(id) {
|
||||
return id
|
||||
.split("_")
|
||||
.map(titleCase)
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function compactLabel(label) {
|
||||
return label.split(" ").map(part => part[0]).join("").slice(0, 3).toUpperCase();
|
||||
}
|
||||
|
||||
function titleCase(value) {
|
||||
return String(value)
|
||||
.split(/[_\s-]+/)
|
||||
.filter(Boolean)
|
||||
.map(part => part[0].toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(max, Math.max(min, Number(value) || min));
|
||||
}
|
||||
|
||||
function safeParse(value) {
|
||||
try {
|
||||
return value ? JSON.parse(value) : null;
|
||||
} catch (_error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
104
priv/ui/index.html
Normal file
104
priv/ui/index.html
Normal file
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>bDS Shell</title>
|
||||
<link rel="stylesheet" href="./app.css">
|
||||
</head>
|
||||
<body>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Blogging Desktop Server</title>
|
||||
<link rel="stylesheet" href="/assets/app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app" id="app">
|
||||
<div class="window-titlebar">
|
||||
<div class="window-titlebar-menu-bar"></div>
|
||||
<div class="window-titlebar-title">Blogging Desktop Server</div>
|
||||
<div class="window-titlebar-actions">
|
||||
<button class="window-titlebar-action-button" data-command="toggle-sidebar" aria-label="Toggle sidebar">
|
||||
<span class="window-titlebar-sidebar-icon"><span class="window-titlebar-sidebar-pane"></span></span>
|
||||
</button>
|
||||
<button class="window-titlebar-action-button" data-command="toggle-panel" aria-label="Toggle panel">
|
||||
<span class="window-titlebar-panel-icon"></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-main">
|
||||
<aside class="activity-bar"></aside>
|
||||
|
||||
<section class="sidebar-shell">
|
||||
<div class="sidebar"></div>
|
||||
<div class="resizable-panel-divider sidebar-divider" data-resize="sidebar"></div>
|
||||
</section>
|
||||
|
||||
<main class="app-content">
|
||||
<div class="tab-bar"></div>
|
||||
<div class="editor-shell"></div>
|
||||
<div class="panel-shell"></div>
|
||||
</main>
|
||||
|
||||
<section class="assistant-sidebar-shell">
|
||||
<div class="resizable-panel-divider assistant-divider" data-resize="assistant"></div>
|
||||
<div class="assistant-sidebar"></div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="status-bar"></div>
|
||||
</div>
|
||||
|
||||
<script id="bds-bootstrap" type="application/json">
|
||||
{
|
||||
"menuGroups": [
|
||||
{"id":"app","label":"App","items":[{"id":"about","label":"About"}]},
|
||||
{"id":"file","label":"File","items":[{"id":"new_post","label":"New Post"},{"id":"close_tab","label":"Close Tab"}]},
|
||||
{"id":"edit","label":"Edit","items":[{"id":"undo","label":"Undo"},{"id":"redo","label":"Redo"}]},
|
||||
{"id":"view","label":"View","items":[{"id":"toggle_sidebar","label":"Toggle Sidebar"},{"id":"toggle_panel","label":"Toggle Panel"},{"id":"toggle_assistant_sidebar","label":"Toggle Assistant Sidebar"}]},
|
||||
{"id":"window","label":"Window","items":[{"id":"minimize","label":"Minimize"}]},
|
||||
{"id":"help","label":"Help","items":[{"id":"documentation","label":"Documentation"}]}
|
||||
],
|
||||
"sidebarViews": [
|
||||
{"id":"posts","label":"Posts","group":"top","items":["welcome.md","launch-plan.md","publishing-notes.md"]},
|
||||
{"id":"pages","label":"Pages","group":"top","items":["about.md","contact.md"]},
|
||||
{"id":"media","label":"Media","group":"top","items":["cover.jpg","launch-banner.png"]},
|
||||
{"id":"scripts","label":"Scripts","group":"top","items":["import_posts.exs","sync_tags.exs"]},
|
||||
{"id":"templates","label":"Templates","group":"top","items":["post.liquid","listing.liquid"]},
|
||||
{"id":"git","label":"Git","group":"bottom","items":["Working tree clean"]},
|
||||
{"id":"settings","label":"Settings","group":"bottom","items":["Project", "Publishing", "AI"]}
|
||||
],
|
||||
"tabs": [
|
||||
{"id":"dashboard","title":"Dashboard","kind":"dashboard","pinned":true,"dirty":false},
|
||||
{"id":"post:welcome","title":"welcome.md","kind":"post","pinned":true,"dirty":true},
|
||||
{"id":"post:launch","title":"launch-plan.md","kind":"post","pinned":false,"dirty":false}
|
||||
],
|
||||
"activeTabId": "post:welcome",
|
||||
"activeView": "posts",
|
||||
"sidebarVisible": true,
|
||||
"sidebarWidth": 320,
|
||||
"assistantVisible": true,
|
||||
"assistantWidth": 336,
|
||||
"panelVisible": true,
|
||||
"panelTab": "problems",
|
||||
"statusBar": {
|
||||
"left": [
|
||||
{"id":"branch","label":"main"},
|
||||
{"id":"sync","label":"Filesystem synced"},
|
||||
{"id":"language","label":"EN"}
|
||||
],
|
||||
"right": [
|
||||
{"id":"project","label":"Starter project"},
|
||||
{"id":"mode","label":"Airplane off"},
|
||||
{"id":"theme","label":"Desktop shell"}
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script src="/assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user