Redesign toolbar, palette, and dock UI
Toolbar: - Compact project/MCM selectors always visible at top - Action bar (SVG, JSON, Import, Clear) as compact button strip - Cleaner section toggles with minimal chrome - Inline settings rows (W/H and Grid/Gap on one line each) - Narrower width (220px vs 240px) Palette: - Group headers are collapsible (start collapsed to save space) - Small symbols (Controls, Sensors, I/O, Other) use 2-column grid - Conveyance keeps list layout (wider items) - Lighter, less cluttered item styling - Custom scrollbar Color scheme: - Shifted from navy/red (#16213e/#e94560) to slate/blue (#111827/#3b82f6) - Better contrast and more professional feel - DeviceDock updated to match Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1e67c3de47
commit
e3a0e422e6
@ -143,8 +143,8 @@
|
|||||||
.device-dock {
|
.device-dock {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
background: #16213e;
|
background: #111827;
|
||||||
border-left: 2px solid #0f3460;
|
border-left: 1px solid #1f2937;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -163,8 +163,8 @@
|
|||||||
left: 4px;
|
left: 4px;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
background: #0f3460;
|
background: #1f2937;
|
||||||
border: 1px solid #1a1a5e;
|
border: 1px solid #374151;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -177,7 +177,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.collapse-btn:hover {
|
.collapse-btn:hover {
|
||||||
background: #e94560;
|
background: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dock-content {
|
.dock-content {
|
||||||
@ -230,8 +230,8 @@
|
|||||||
.search-box input {
|
.search-box input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 5px 22px 5px 8px;
|
padding: 5px 22px 5px 8px;
|
||||||
background: #0f3460;
|
background: #1f2937;
|
||||||
border: 1px solid #1a1a5e;
|
border: 1px solid #374151;
|
||||||
color: #e0e0e0;
|
color: #e0e0e0;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
@ -241,7 +241,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-box input:focus {
|
.search-box input:focus {
|
||||||
border-color: #e94560;
|
border-color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-box input::placeholder {
|
.search-box input::placeholder {
|
||||||
@ -268,7 +268,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.clear-search:hover {
|
.clear-search:hover {
|
||||||
color: #e94560;
|
color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dock-empty {
|
.dock-empty {
|
||||||
@ -291,8 +291,8 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 4px 6px;
|
padding: 4px 6px;
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
background: #0d2847;
|
background: #1f2937;
|
||||||
border: 1px solid #1a1a5e;
|
border: 1px solid #374151;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: #8899aa;
|
color: #8899aa;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
@ -305,7 +305,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.zone-toggle:hover {
|
.zone-toggle:hover {
|
||||||
color: #e94560;
|
color: #3b82f6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.zone-count {
|
.zone-count {
|
||||||
@ -342,8 +342,8 @@
|
|||||||
gap: 5px;
|
gap: 5px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 3px 6px;
|
padding: 3px 6px;
|
||||||
background: #0f3460;
|
background: #1f2937;
|
||||||
border: 1px solid #1a1a5e;
|
border: 1px solid #374151;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@ -353,8 +353,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.device-item:hover {
|
.device-item:hover {
|
||||||
background: #1a1a5e;
|
background: #374151;
|
||||||
border-color: #e94560;
|
border-color: #3b82f6;
|
||||||
transform: translateX(-2px);
|
transform: translateX(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,55 +32,64 @@
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
let expandedSubgroups = $state<Set<string>>(new Set());
|
let expandedGroups = $state<Set<string>>(new Set());
|
||||||
|
|
||||||
function toggleSubgroup(key: string) {
|
function toggleGroup(key: string) {
|
||||||
const next = new Set(expandedSubgroups);
|
const next = new Set(expandedGroups);
|
||||||
if (next.has(key)) next.delete(key);
|
if (next.has(key)) next.delete(key);
|
||||||
else next.add(key);
|
else next.add(key);
|
||||||
expandedSubgroups = next;
|
expandedGroups = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Small symbols use grid layout (2 columns)
|
||||||
|
const GRID_GROUPS = new Set(['I/O Modules', 'Sensors', 'Controls', 'Other']);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="palette">
|
<div class="palette">
|
||||||
{#each grouped as group (group.name)}
|
{#each grouped as group (group.name)}
|
||||||
<div class="group">
|
{@const isGrid = GRID_GROUPS.has(group.name)}
|
||||||
<div class="group-header">{group.name}</div>
|
{@const groupKey = group.name}
|
||||||
|
|
||||||
|
<!-- Group header (collapsible) -->
|
||||||
|
<button class="group-header" onclick={() => toggleGroup(groupKey)}>
|
||||||
|
<span class="chevron" class:open={expandedGroups.has(groupKey)}></span>
|
||||||
|
{group.name}
|
||||||
|
<span class="count">{group.subgroups.reduce((n, s) => n + s.symbols.length, 0) + group.symbols.length}</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if expandedGroups.has(groupKey)}
|
||||||
|
<!-- Subgroups -->
|
||||||
{#if group.subgroups.length > 0}
|
{#if group.subgroups.length > 0}
|
||||||
{#each group.subgroups as sub (sub.name)}
|
{#each group.subgroups as sub (sub.name)}
|
||||||
{@const key = `${group.name}/${sub.name}`}
|
<div class="sub-label">{sub.name}</div>
|
||||||
<button class="subgroup-header" onclick={() => toggleSubgroup(key)}>
|
<div class="items" class:grid={isGrid}>
|
||||||
<span class="chevron" class:open={expandedSubgroups.has(key)}></span>
|
{#each sub.symbols as sym (sym.id)}
|
||||||
{sub.name}
|
<button
|
||||||
</button>
|
class="item"
|
||||||
{#if expandedSubgroups.has(key)}
|
class:grid-item={isGrid}
|
||||||
<div class="group-items subgroup-items">
|
title={sym.name}
|
||||||
{#each sub.symbols as sym (sym.id)}
|
onmousedown={(e) => onMousedown(e, sym)}
|
||||||
<button
|
>
|
||||||
class="palette-item"
|
<img
|
||||||
title={sym.name}
|
src={sym.file}
|
||||||
onmousedown={(e) => onMousedown(e, sym)}
|
alt={sym.name}
|
||||||
>
|
draggable="false"
|
||||||
<img
|
style={sym.defaultRotation ? `transform: rotate(${sym.defaultRotation}deg)` : ''}
|
||||||
src={sym.file}
|
/>
|
||||||
alt={sym.name}
|
<span class="item-name">{sym.name}</span>
|
||||||
draggable="false"
|
</button>
|
||||||
style={sym.defaultRotation ? `transform: rotate(${sym.defaultRotation}deg)` : ''}
|
{/each}
|
||||||
/>
|
</div>
|
||||||
<span>{sym.name}</span>
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Ungrouped symbols -->
|
||||||
{#if group.symbols.length > 0}
|
{#if group.symbols.length > 0}
|
||||||
<div class="group-items">
|
<div class="items" class:grid={isGrid}>
|
||||||
{#each group.symbols as sym (sym.id)}
|
{#each group.symbols as sym (sym.id)}
|
||||||
<button
|
<button
|
||||||
class="palette-item"
|
class="item"
|
||||||
|
class:grid-item={isGrid}
|
||||||
title={sym.name}
|
title={sym.name}
|
||||||
onmousedown={(e) => onMousedown(e, sym)}
|
onmousedown={(e) => onMousedown(e, sym)}
|
||||||
>
|
>
|
||||||
@ -90,12 +99,12 @@
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
style={sym.defaultRotation ? `transform: rotate(${sym.defaultRotation}deg)` : ''}
|
style={sym.defaultRotation ? `transform: rotate(${sym.defaultRotation}deg)` : ''}
|
||||||
/>
|
/>
|
||||||
<span>{sym.name}</span>
|
<span class="item-name">{sym.name}</span>
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -103,113 +112,156 @@
|
|||||||
.palette {
|
.palette {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0 2px;
|
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
padding-bottom: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group {
|
/* Scrollbar */
|
||||||
margin-bottom: 6px;
|
.palette::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
.palette::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.palette::-webkit-scrollbar-thumb {
|
||||||
|
background: #374151;
|
||||||
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-header {
|
.group-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 4px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid #1f2937;
|
||||||
|
color: #e5e7eb;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #e94560;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 0.8px;
|
||||||
font-weight: 600;
|
cursor: pointer;
|
||||||
padding: 4px 6px 2px;
|
transition: color 0.15s;
|
||||||
|
text-align: left;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
background: #16213e;
|
background: #111827;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subgroup-header {
|
.group-header:hover {
|
||||||
display: flex;
|
color: #3b82f6;
|
||||||
align-items: center;
|
|
||||||
gap: 4px;
|
|
||||||
width: 100%;
|
|
||||||
padding: 3px 6px 3px 12px;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: #8899aa;
|
|
||||||
font-size: 9px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.subgroup-header:hover {
|
.count {
|
||||||
color: #e94560;
|
margin-left: auto;
|
||||||
|
font-size: 9px;
|
||||||
|
color: #4b5563;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chevron {
|
.chevron {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
border-top: 4px solid transparent;
|
border-top: 3.5px solid transparent;
|
||||||
border-bottom: 4px solid transparent;
|
border-bottom: 3.5px solid transparent;
|
||||||
border-left: 5px solid currentColor;
|
border-left: 4.5px solid currentColor;
|
||||||
transition: transform 0.15s ease;
|
transition: transform 0.15s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chevron.open {
|
.chevron.open {
|
||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.subgroup-items {
|
.sub-label {
|
||||||
padding-left: 8px;
|
font-size: 9px;
|
||||||
|
color: #6b7280;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 4px 2px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-items {
|
/* List layout (conveyance) */
|
||||||
|
.items {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
padding: 0 2px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grid layout (small symbols) */
|
||||||
|
.items.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.palette-item {
|
.item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 5px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 4px 6px;
|
padding: 3px 5px;
|
||||||
background: #0f3460;
|
background: transparent;
|
||||||
border: 1px solid #1a1a5e;
|
border: 1px solid transparent;
|
||||||
border-radius: 4px;
|
border-radius: 3px;
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
transition: all 0.15s ease;
|
transition: all 0.12s ease;
|
||||||
color: #ccc;
|
color: #9ca3af;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.palette-item:hover {
|
.item:hover {
|
||||||
background: #1a1a5e;
|
background: #1f2937;
|
||||||
border-color: #e94560;
|
border-color: #374151;
|
||||||
transform: translateX(2px);
|
color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.palette-item:active {
|
.item:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
transform: scale(0.98);
|
background: #374151;
|
||||||
}
|
}
|
||||||
|
|
||||||
.palette-item img {
|
/* Grid items are more compact */
|
||||||
width: 36px;
|
.item.grid-item {
|
||||||
height: 24px;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px 2px 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item.grid-item .item-name {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item img {
|
||||||
|
width: 28px;
|
||||||
|
height: 18px;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
background: transparent;
|
|
||||||
border-radius: 3px;
|
|
||||||
padding: 2px;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.palette-item span {
|
.item:hover img {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item.grid-item img {
|
||||||
|
width: 24px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-name {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 1.2;
|
line-height: 1.1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|||||||
@ -10,7 +10,6 @@
|
|||||||
let pdfFileEl = $state<HTMLInputElement>(null!);
|
let pdfFileEl = $state<HTMLInputElement>(null!);
|
||||||
let toolbarCollapsed = $state(false);
|
let toolbarCollapsed = $state(false);
|
||||||
|
|
||||||
let projectOpen = $state(false);
|
|
||||||
let pdfOpen = $state(false);
|
let pdfOpen = $state(false);
|
||||||
let settingsOpen = $state(false);
|
let settingsOpen = $state(false);
|
||||||
|
|
||||||
@ -66,23 +65,10 @@
|
|||||||
await loadPdfFromPath(mcm.pdfPath);
|
await loadPdfFromPath(mcm.pdfPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCanvasSizeChange() {
|
function onCanvasSizeChange() { layout.markDirty(); layout.saveMcmState(); }
|
||||||
layout.markDirty();
|
function onGridSizeChange() { layout.markDirty(); layout.saveMcmState(); }
|
||||||
layout.saveMcmState();
|
function onShowGridChange() { layout.markDirty(); }
|
||||||
}
|
function onMinSpacingChange() { layout.saveMcmState(); }
|
||||||
|
|
||||||
function onGridSizeChange() {
|
|
||||||
layout.markDirty();
|
|
||||||
layout.saveMcmState();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onShowGridChange() {
|
|
||||||
layout.markDirty();
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMinSpacingChange() {
|
|
||||||
layout.saveMcmState();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearCanvas() {
|
function clearCanvas() {
|
||||||
if (!confirm('Clear all placed symbols?')) return;
|
if (!confirm('Clear all placed symbols?')) return;
|
||||||
@ -93,11 +79,8 @@
|
|||||||
const file = (e.target as HTMLInputElement).files?.[0];
|
const file = (e.target as HTMLInputElement).files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
try {
|
try {
|
||||||
if (file.name.endsWith('.svg')) {
|
if (file.name.endsWith('.svg')) await loadLayoutSVG(file);
|
||||||
await loadLayoutSVG(file);
|
else await loadLayoutJSON(file);
|
||||||
} else {
|
|
||||||
await loadLayoutJSON(file);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Invalid layout file: ' + (err instanceof Error ? err.message : String(err)));
|
alert('Invalid layout file: ' + (err instanceof Error ? err.message : String(err)));
|
||||||
}
|
}
|
||||||
@ -118,141 +101,104 @@
|
|||||||
|
|
||||||
<div class="toolbar" class:collapsed={toolbarCollapsed}>
|
<div class="toolbar" class:collapsed={toolbarCollapsed}>
|
||||||
<button class="collapse-btn" onclick={() => toolbarCollapsed = !toolbarCollapsed}>
|
<button class="collapse-btn" onclick={() => toolbarCollapsed = !toolbarCollapsed}>
|
||||||
{toolbarCollapsed ? '>' : '<'}
|
{toolbarCollapsed ? '\u276F' : '\u276E'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{#if !toolbarCollapsed}
|
{#if !toolbarCollapsed}
|
||||||
<div class="toolbar-content">
|
<div class="toolbar-content">
|
||||||
<!-- Project section (collapsible) -->
|
<!-- Project & MCM selectors — always visible, compact -->
|
||||||
<button class="section-toggle" onclick={() => projectOpen = !projectOpen}>
|
<div class="project-bar">
|
||||||
<span class="toggle-arrow">{projectOpen ? '\u25BC' : '\u25B6'}</span>
|
{#if layout.projects.length > 0}
|
||||||
Project
|
<select class="proj-select" value={layout.currentProject} onchange={onProjectChange} title="Project">
|
||||||
<span class="section-summary">{layout.currentProject || '...'} / {layout.currentMcm || '...'}</span>
|
{#each layout.projects as proj (proj.name)}
|
||||||
</button>
|
<option value={proj.name}>{proj.name}</option>
|
||||||
{#if projectOpen}
|
{/each}
|
||||||
<div class="section-body">
|
</select>
|
||||||
{#if layout.projects.length > 0}
|
{/if}
|
||||||
<div class="setting">
|
<select class="mcm-select" value={layout.currentMcm} onchange={onMcmChange} title="MCM">
|
||||||
<label for="projectSelect">Project:</label>
|
{#each currentMcms as mcm (mcm.name)}
|
||||||
<select id="projectSelect" value={layout.currentProject} onchange={onProjectChange}>
|
<option value={mcm.name}>{mcm.name}</option>
|
||||||
{#each layout.projects as proj (proj.name)}
|
{/each}
|
||||||
<option value={proj.name}>{proj.name}</option>
|
</select>
|
||||||
{/each}
|
</div>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="setting">
|
|
||||||
<label for="mcmSelect">MCM:</label>
|
|
||||||
<select id="mcmSelect" value={layout.currentMcm} onchange={onMcmChange}>
|
|
||||||
{#each currentMcms as mcm (mcm.name)}
|
|
||||||
<option value={mcm.name}>{mcm.name}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="setting">
|
|
||||||
<label for="mcmSelect">MCM:</label>
|
|
||||||
<select id="mcmSelect" value={layout.currentMcm} onchange={onMcmChange}>
|
|
||||||
<option value="MCM08">MCM08</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Background PDF section (collapsible) -->
|
<!-- Action bar — always visible -->
|
||||||
|
<div class="action-bar">
|
||||||
|
<button onclick={exportSVG} title="Export SVG">SVG</button>
|
||||||
|
<button onclick={exportJSON} title="Export JSON">JSON</button>
|
||||||
|
<button onclick={() => importFileEl.click()} title="Import layout file">Import</button>
|
||||||
|
<button class="btn-danger" onclick={clearCanvas} title="Clear canvas">Clear</button>
|
||||||
|
<input bind:this={importFileEl} type="file" accept=".json,.svg" style="display:none" onchange={onImportFile}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="divider"></div>
|
||||||
|
|
||||||
|
<!-- Background PDF -->
|
||||||
<button class="section-toggle" onclick={() => pdfOpen = !pdfOpen}>
|
<button class="section-toggle" onclick={() => pdfOpen = !pdfOpen}>
|
||||||
<span class="toggle-arrow">{pdfOpen ? '\u25BC' : '\u25B6'}</span>
|
<span class="chevron" class:open={pdfOpen}></span>
|
||||||
Background PDF
|
Background
|
||||||
{#if layout.pdfLoaded}
|
{#if layout.pdfLoaded}
|
||||||
<span class="section-summary">{Math.round(layout.pdfScale * 100)}%</span>
|
<span class="badge">{Math.round(layout.pdfScale * 100)}%</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
{#if pdfOpen}
|
{#if pdfOpen}
|
||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<div class="setting btn-row">
|
<div class="btn-row">
|
||||||
<button onclick={() => pdfFileEl.click()}>Load PDF</button>
|
<button onclick={() => pdfFileEl.click()}>Load PDF</button>
|
||||||
<input bind:this={pdfFileEl} type="file" accept=".pdf" style="display:none" onchange={onPdfFile}>
|
<input bind:this={pdfFileEl} type="file" accept=".pdf" style="display:none" onchange={onPdfFile}>
|
||||||
</div>
|
|
||||||
<div class="setting btn-row">
|
|
||||||
<button onclick={pdfZoomIn}>PDF +</button>
|
|
||||||
<button onclick={pdfZoomOut}>PDF -</button>
|
|
||||||
</div>
|
|
||||||
<div class="setting btn-row">
|
|
||||||
<button
|
|
||||||
class:edit-bg-active={layout.editingBackground}
|
|
||||||
onclick={toggleEditBackground}
|
|
||||||
>
|
|
||||||
{layout.editingBackground ? 'Done' : 'Edit BG'}
|
|
||||||
</button>
|
|
||||||
<button onclick={removePdf}>Remove</button>
|
<button onclick={removePdf}>Remove</button>
|
||||||
</div>
|
</div>
|
||||||
{#if layout.pdfLoaded}
|
{#if layout.pdfLoaded}
|
||||||
<div class="pdf-info">
|
<div class="btn-row">
|
||||||
scale {Math.round(layout.pdfScale * 100)}% pos: {Math.round(layout.pdfOffsetX)},{Math.round(layout.pdfOffsetY)}
|
<button onclick={pdfZoomOut}>-</button>
|
||||||
|
<span class="zoom-label">{Math.round(layout.pdfScale * 100)}%</span>
|
||||||
|
<button onclick={pdfZoomIn}>+</button>
|
||||||
|
<button
|
||||||
|
class:active={layout.editingBackground}
|
||||||
|
onclick={toggleEditBackground}
|
||||||
|
>
|
||||||
|
{layout.editingBackground ? 'Done' : 'Move'}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Settings section (collapsible) -->
|
<!-- Canvas Settings -->
|
||||||
<button class="section-toggle" onclick={() => settingsOpen = !settingsOpen}>
|
<button class="section-toggle" onclick={() => settingsOpen = !settingsOpen}>
|
||||||
<span class="toggle-arrow">{settingsOpen ? '\u25BC' : '\u25B6'}</span>
|
<span class="chevron" class:open={settingsOpen}></span>
|
||||||
Settings
|
Canvas
|
||||||
</button>
|
</button>
|
||||||
{#if settingsOpen}
|
{#if settingsOpen}
|
||||||
<div class="section-body">
|
<div class="section-body">
|
||||||
<div class="setting canvas-size-row">
|
<div class="inline-row">
|
||||||
<div>
|
<label>W</label>
|
||||||
<label for="canvasW">W:</label>
|
<input type="number" bind:value={layout.canvasW} min="800" max="7680" step="10" onchange={onCanvasSizeChange}>
|
||||||
<input type="number" id="canvasW" bind:value={layout.canvasW} min="800" max="7680" step="10" onchange={onCanvasSizeChange}>
|
<label>H</label>
|
||||||
</div>
|
<input type="number" bind:value={layout.canvasH} min="600" max="4320" step="10" onchange={onCanvasSizeChange}>
|
||||||
<div>
|
|
||||||
<label for="canvasH">H:</label>
|
|
||||||
<input type="number" id="canvasH" bind:value={layout.canvasH} min="600" max="4320" step="10" onchange={onCanvasSizeChange}>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="setting">
|
<div class="inline-row">
|
||||||
<label for="gridSize">Grid Size (px):</label>
|
<label>Grid</label>
|
||||||
<input
|
<input type="number" bind:value={layout.gridSize} min="5" max="100" step="5" onchange={onGridSizeChange}>
|
||||||
type="number" id="gridSize"
|
<label>Gap</label>
|
||||||
bind:value={layout.gridSize}
|
<input type="number" bind:value={layout.minSpacing} min="0" max="100" step="5" onchange={onMinSpacingChange}>
|
||||||
min="5" max="100" step="5"
|
|
||||||
onchange={onGridSizeChange}
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="setting">
|
<div class="toggle-row">
|
||||||
<label for="minSpacing">Min Edge Spacing (px):</label>
|
<label class="toggle-label">
|
||||||
<input
|
|
||||||
type="number" id="minSpacing"
|
|
||||||
bind:value={layout.minSpacing}
|
|
||||||
min="0" max="100" step="5"
|
|
||||||
onchange={onMinSpacingChange}
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="setting">
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" bind:checked={layout.showGrid} onchange={onShowGridChange}>
|
<input type="checkbox" bind:checked={layout.showGrid} onchange={onShowGridChange}>
|
||||||
Show Grid
|
<span>Grid</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
<label class="toggle-label">
|
||||||
<div class="setting">
|
|
||||||
<label class="checkbox-label">
|
|
||||||
<input type="checkbox" bind:checked={layout.snapEnabled}>
|
<input type="checkbox" bind:checked={layout.snapEnabled}>
|
||||||
Snap to Grid
|
<span>Snap</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="setting btn-row">
|
|
||||||
<button onclick={exportSVG}>Save SVG</button>
|
|
||||||
<button onclick={exportJSON}>Save JSON</button>
|
|
||||||
<button onclick={() => importFileEl.click()}>Load JSON</button>
|
|
||||||
<button onclick={clearCanvas}>Clear</button>
|
|
||||||
<input bind:this={importFileEl} type="file" accept=".json,.svg" style="display:none" onchange={onImportFile}>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Symbols section (always open, takes remaining space) -->
|
<div class="divider"></div>
|
||||||
<div class="symbols-header">Symbols</div>
|
|
||||||
|
<!-- Symbols -->
|
||||||
<Palette />
|
<Palette />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -260,10 +206,10 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.toolbar {
|
.toolbar {
|
||||||
width: 240px;
|
width: 220px;
|
||||||
min-width: 240px;
|
min-width: 220px;
|
||||||
background: #16213e;
|
background: #111827;
|
||||||
border-right: 2px solid #0f3460;
|
border-right: 1px solid #1f2937;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -272,35 +218,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toolbar.collapsed {
|
.toolbar.collapsed {
|
||||||
width: 32px;
|
width: 28px;
|
||||||
min-width: 32px;
|
min-width: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse-btn {
|
.collapse-btn {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 6px;
|
||||||
right: 4px;
|
right: 2px;
|
||||||
width: 24px;
|
width: 22px;
|
||||||
height: 24px;
|
height: 22px;
|
||||||
background: #0f3460;
|
background: transparent;
|
||||||
border: 1px solid #1a1a5e;
|
border: none;
|
||||||
color: #e0e0e0;
|
color: #6b7280;
|
||||||
border-radius: 4px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
font-size: 12px;
|
font-size: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
transition: background 0.15s;
|
border-radius: 4px;
|
||||||
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse-btn:hover {
|
.collapse-btn:hover {
|
||||||
background: #e94560;
|
background: #1f2937;
|
||||||
|
color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toolbar-content {
|
.toolbar-content {
|
||||||
padding: 6px;
|
padding: 4px 6px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -308,165 +255,222 @@
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Collapsible section toggle */
|
/* Project bar */
|
||||||
|
.project-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 3px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proj-select, .mcm-select {
|
||||||
|
flex: 1;
|
||||||
|
padding: 4px 6px;
|
||||||
|
background: #1f2937;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
color: #e5e7eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
appearance: none;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.proj-select:focus, .mcm-select:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Action bar */
|
||||||
|
.action-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar button {
|
||||||
|
flex: 1;
|
||||||
|
padding: 4px 2px;
|
||||||
|
background: #1f2937;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
color: #9ca3af;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar button:hover {
|
||||||
|
background: #374151;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-bar .btn-danger:hover {
|
||||||
|
background: #991b1b;
|
||||||
|
border-color: #991b1b;
|
||||||
|
color: #fca5a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #1f2937;
|
||||||
|
margin: 2px 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section toggle */
|
||||||
.section-toggle {
|
.section-toggle {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 6px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 5px 6px;
|
padding: 5px 4px;
|
||||||
margin-bottom: 2px;
|
background: none;
|
||||||
background: #0d2847;
|
border: none;
|
||||||
border: 1px solid #1a1a5e;
|
color: #9ca3af;
|
||||||
border-radius: 4px;
|
|
||||||
color: #e94560;
|
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 0.5px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.15s;
|
transition: color 0.15s;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-toggle:hover {
|
.section-toggle:hover {
|
||||||
background: #122d52;
|
color: #e5e7eb;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-arrow {
|
.chevron {
|
||||||
font-size: 8px;
|
display: inline-block;
|
||||||
width: 10px;
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-top: 3.5px solid transparent;
|
||||||
|
border-bottom: 3.5px solid transparent;
|
||||||
|
border-left: 4.5px solid currentColor;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-summary {
|
.chevron.open {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: #667;
|
color: #6b7280;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
|
background: #1f2937;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-body {
|
.section-body {
|
||||||
padding: 4px 2px 6px;
|
padding: 2px 0 6px 10px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Symbols header (always visible) */
|
/* Button rows */
|
||||||
.symbols-header {
|
|
||||||
font-size: 10px;
|
|
||||||
color: #e94560;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 5px 6px 3px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting {
|
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting label {
|
|
||||||
display: block;
|
|
||||||
font-size: 10px;
|
|
||||||
margin-bottom: 2px;
|
|
||||||
color: #8899aa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox-label {
|
|
||||||
display: flex !important;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 11px !important;
|
|
||||||
color: #ccc !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting input[type="number"],
|
|
||||||
.setting select {
|
|
||||||
width: 100%;
|
|
||||||
padding: 4px 6px;
|
|
||||||
background: #0f3460;
|
|
||||||
border: 1px solid #1a1a5e;
|
|
||||||
color: #e0e0e0;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
transition: border-color 0.15s;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting input[type="number"]:focus,
|
|
||||||
.setting select:focus {
|
|
||||||
border-color: #e94560;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting select {
|
|
||||||
cursor: pointer;
|
|
||||||
appearance: none;
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%238899aa'%3E%3Cpath d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: right 6px center;
|
|
||||||
padding-right: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting input[type="checkbox"] {
|
|
||||||
accent-color: #e94560;
|
|
||||||
width: 13px;
|
|
||||||
height: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-row {
|
.btn-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 3px;
|
gap: 3px;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-row button,
|
.btn-row button, .section-body > button {
|
||||||
.setting > button {
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
padding: 5px 6px;
|
padding: 4px 6px;
|
||||||
background: #0f3460;
|
background: #1f2937;
|
||||||
border: 1px solid #1a1a5e;
|
border: 1px solid #374151;
|
||||||
color: #e0e0e0;
|
color: #9ca3af;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
font-weight: 500;
|
transition: all 0.15s;
|
||||||
transition: all 0.15s ease;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-row button:hover,
|
.btn-row button:hover {
|
||||||
.setting > button:hover {
|
background: #374151;
|
||||||
background: #e94560;
|
color: #e5e7eb;
|
||||||
border-color: #e94560;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-row button:active,
|
.btn-row button.active {
|
||||||
.setting > button:active {
|
background: #3b82f6;
|
||||||
transform: translateY(0);
|
border-color: #3b82f6;
|
||||||
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit-bg-active {
|
.zoom-label {
|
||||||
background: #e94560 !important;
|
font-size: 10px;
|
||||||
border-color: #e94560 !important;
|
color: #9ca3af;
|
||||||
color: #fff !important;
|
min-width: 32px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-size-row {
|
/* Inline settings rows */
|
||||||
|
.inline-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
margin-bottom: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.canvas-size-row > div {
|
.inline-row label {
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pdf-info {
|
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
color: #667;
|
color: #6b7280;
|
||||||
padding: 2px 0;
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
flex-shrink: 0;
|
||||||
|
min-width: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-row input[type="number"] {
|
||||||
|
flex: 1;
|
||||||
|
padding: 3px 4px;
|
||||||
|
background: #1f2937;
|
||||||
|
border: 1px solid #374151;
|
||||||
|
color: #e5e7eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
outline: none;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-row input:focus {
|
||||||
|
border-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 10px;
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label input[type="checkbox"] {
|
||||||
|
accent-color: #3b82f6;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-label span {
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user