Tools + Elements: smaller tiles, display-mode dropdown, dark pill look
- Tiles shrunk (minHeight 46/38/30 per mode, tighter padding, smaller icons) - Add display-mode dropdown (Symbol / Text / Symbol+Text) at top of each panel; choice persists in localStorage under 'dossier_tile_mode' and is shared between both panels - Grid column min-width adapts per mode (icon 40 / text 66 / both 58) - Restore dark pill look: --bg-input background + border-light, full border-radius — tiles read as distinct chips again - Icon-only mode shows a small corner chevron on dropdown tiles (Treppe/Stuetze/Traeger) so the right-click menu stays discoverable
This commit is contained in:
+82
-36
@@ -21,6 +21,32 @@ const labelXs = {
|
||||
letterSpacing: '0.06em', textTransform: 'uppercase',
|
||||
}
|
||||
|
||||
// Anzeige-Modus der Kacheln, geteilt mit Werkzeuge-Panel via localStorage.
|
||||
const TILE_MODE_KEY = 'dossier_tile_mode' // 'both' | 'icon' | 'text'
|
||||
function readTileMode() {
|
||||
try { return localStorage.getItem(TILE_MODE_KEY) || 'both' } catch { return 'both' }
|
||||
}
|
||||
function writeTileMode(m) {
|
||||
try { localStorage.setItem(TILE_MODE_KEY, m) } catch { /* WebView ohne Storage */ }
|
||||
}
|
||||
const TILE_MIN_COL = { icon: 40, text: 66, both: 58 }
|
||||
const TILE_MIN_H = { icon: 38, text: 30, both: 46 }
|
||||
|
||||
function TileModeDropdown({ mode, onChange }) {
|
||||
return (
|
||||
<select
|
||||
value={mode}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
title="Anzeige: Symbol / Text / Symbol + Text"
|
||||
style={{ fontSize: 10, padding: '3px 6px', maxWidth: 130 }}
|
||||
>
|
||||
<option value="icon">Symbol</option>
|
||||
<option value="text">Text</option>
|
||||
<option value="both">Symbol + Text</option>
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
function fmtNum(v) {
|
||||
if (v == null || v === '') return ''
|
||||
const n = Number(v)
|
||||
@@ -51,8 +77,11 @@ function ReferenzSelector({ value, onChange }) {
|
||||
|
||||
// Raster-Kachel — Icon oben, Label darunter, einheitliche Zellgroesse.
|
||||
// hasMenu zeigt ein kleines Chevron neben dem Label (Rechtsklick = Untertypen).
|
||||
// mode: 'both' (Icon+Text) | 'icon' (nur Symbol) | 'text' (nur Text)
|
||||
function PillButton({ icon, label, hint, onClick, onContextMenu, disabled,
|
||||
hasMenu, badge }) {
|
||||
hasMenu, badge, mode = 'both' }) {
|
||||
const showIcon = mode !== 'text'
|
||||
const showLabel = mode !== 'icon'
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
@@ -61,12 +90,12 @@ function PillButton({ icon, label, hint, onClick, onContextMenu, disabled,
|
||||
title={hint}
|
||||
style={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
padding: '10px 4px',
|
||||
minHeight: 56,
|
||||
background: 'var(--bg-item)',
|
||||
border: '1px solid transparent',
|
||||
borderRadius: 10,
|
||||
alignItems: 'center', justifyContent: 'center', gap: 4,
|
||||
padding: '6px 4px',
|
||||
minHeight: TILE_MIN_H[mode],
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 999,
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.4 : 1,
|
||||
transition: 'background 0.12s, border-color 0.12s',
|
||||
@@ -81,11 +110,15 @@ function PillButton({ icon, label, hint, onClick, onContextMenu, disabled,
|
||||
e.currentTarget.style.borderColor = 'var(--accent)'
|
||||
}}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'var(--bg-item)'
|
||||
e.currentTarget.style.borderColor = 'transparent'
|
||||
e.currentTarget.style.background = 'var(--bg-input)'
|
||||
e.currentTarget.style.borderColor = 'var(--border-light)'
|
||||
}}
|
||||
>
|
||||
<Icon name={icon} size={19} style={{ color: 'var(--accent)', flexShrink: 0 }} />
|
||||
{showIcon && (
|
||||
<Icon name={icon} size={mode === 'icon' ? 18 : 16}
|
||||
style={{ color: 'var(--accent)', flexShrink: 0 }} />
|
||||
)}
|
||||
{showLabel && (
|
||||
<span style={{
|
||||
display: 'flex', alignItems: 'center', gap: 2,
|
||||
maxWidth: '100%', overflow: 'hidden',
|
||||
@@ -97,9 +130,16 @@ function PillButton({ icon, label, hint, onClick, onContextMenu, disabled,
|
||||
style={{ color: 'var(--text-muted)', flexShrink: 0 }} />
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{/* Im Icon-Modus signalisiert ein kleines Eck-Chevron das Dropdown */}
|
||||
{hasMenu && !showLabel && (
|
||||
<Icon name="expand_more" size={10}
|
||||
style={{ position: 'absolute', bottom: 2, right: 2,
|
||||
color: 'var(--text-muted)' }} />
|
||||
)}
|
||||
{badge && (
|
||||
<span style={{
|
||||
position: 'absolute', top: 4, right: 4,
|
||||
position: 'absolute', top: 3, right: 3,
|
||||
fontSize: 8, padding: '0px 4px', borderRadius: 8,
|
||||
background: 'var(--bg-section)', color: 'var(--text-muted)',
|
||||
}}>{badge}</span>
|
||||
@@ -109,9 +149,9 @@ function PillButton({ icon, label, hint, onClick, onContextMenu, disabled,
|
||||
}
|
||||
|
||||
// Kategorie-Gruppe: Label + einheitliches Raster (auto-fill Spalten)
|
||||
function PillGroup({ label, children }) {
|
||||
function PillGroup({ label, mode = 'both', children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||||
<span style={{
|
||||
fontSize: 9, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.08em', textTransform: 'uppercase',
|
||||
@@ -121,8 +161,8 @@ function PillGroup({ label, children }) {
|
||||
</span>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(68px, 1fr))',
|
||||
gap: 6,
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(${TILE_MIN_COL[mode]}px, 1fr))`,
|
||||
gap: 5,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
@@ -358,6 +398,8 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount, treppe2DSh
|
||||
const [treppeMenuOpen, setTreppeMenuOpen] = useState(false)
|
||||
const [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false)
|
||||
const [traegerMenuOpen, setTraegerMenuOpen] = useState(false)
|
||||
const [tileMode, setTileMode] = useState(readTileMode)
|
||||
const changeTileMode = (m) => { setTileMode(m); writeTileMode(m) }
|
||||
const treppeWrapperRef = useRef(null)
|
||||
const dis = noGeschoss
|
||||
const baseHint = (label) =>
|
||||
@@ -435,37 +477,41 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount, treppe2DSh
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PillGroup label="Konstruktion">
|
||||
<PillButton icon="view_week" label="Wand"
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<TileModeDropdown mode={tileMode} onChange={changeTileMode} />
|
||||
</div>
|
||||
|
||||
<PillGroup label="Konstruktion" mode={tileMode}>
|
||||
<PillButton mode={tileMode} icon="view_week" label="Wand"
|
||||
hint={baseHint('Wand zeichnen')} disabled={dis}
|
||||
onClick={() => createWall({ geschoss: '' })} />
|
||||
<PillButton icon="layers" label="Decke"
|
||||
<PillButton mode={tileMode} icon="layers" label="Decke"
|
||||
hint={baseHint('Decke zeichnen')} disabled={dis}
|
||||
onClick={() => createDecke({ geschoss: '' })} />
|
||||
<PillButton icon="roofing" label="Dach"
|
||||
<PillButton mode={tileMode} icon="roofing" label="Dach"
|
||||
hint={baseHint('Pultdach zeichnen — Traufe = 1. Kante')} disabled={dis}
|
||||
onClick={() => createDach({ geschoss: '' })} />
|
||||
</PillGroup>
|
||||
|
||||
<PillGroup label="Öffnungen">
|
||||
<PillButton icon="window" label="Fenster"
|
||||
<PillGroup label="Öffnungen" mode={tileMode}>
|
||||
<PillButton mode={tileMode} icon="window" label="Fenster"
|
||||
hint={dis ? baseHint('Fenster') :
|
||||
'Erst Wand-Achse wählen, dann Punkt darauf'} disabled={dis}
|
||||
onClick={() => createFenster({})} />
|
||||
<PillButton icon="sensor_door" label="Tür"
|
||||
<PillButton mode={tileMode} icon="sensor_door" label="Tür"
|
||||
hint={dis ? baseHint('Tür') :
|
||||
'Erst Wand-Achse wählen, dann Punkt darauf'} disabled={dis}
|
||||
onClick={() => createTuer({})} />
|
||||
<PillButton icon="rectangle" label="Aussparung"
|
||||
<PillButton mode={tileMode} icon="rectangle" label="Aussparung"
|
||||
hint={dis ? baseHint('Aussparung') :
|
||||
'Outline auf einer Decke zeichnen — wird automatisch ausgeschnitten'}
|
||||
disabled={dis}
|
||||
onClick={() => createAussparung({})} />
|
||||
</PillGroup>
|
||||
|
||||
<PillGroup label="Erschliessung">
|
||||
<PillGroup label="Erschliessung" mode={tileMode}>
|
||||
<div ref={treppeWrapperRef} style={{ position: 'relative' }}>
|
||||
<PillButton icon="stairs" label="Treppe" hasMenu
|
||||
<PillButton mode={tileMode} icon="stairs" label="Treppe" hasMenu
|
||||
hint={dis ? baseHint('Treppe') :
|
||||
'Klick: gerade Treppe · Rechtsklick: Typ wählen'}
|
||||
disabled={dis}
|
||||
@@ -478,9 +524,9 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount, treppe2DSh
|
||||
</div>
|
||||
</PillGroup>
|
||||
|
||||
<PillGroup label="Tragwerk">
|
||||
<PillGroup label="Tragwerk" mode={tileMode}>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<PillButton icon="square_foot" label="Stütze" hasMenu
|
||||
<PillButton mode={tileMode} icon="square_foot" label="Stütze" hasMenu
|
||||
hint={dis ? baseHint('Stütze') :
|
||||
'Klick: Quadrat-Stütze · Rechtsklick: Profil wählen'}
|
||||
disabled={dis}
|
||||
@@ -492,7 +538,7 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount, treppe2DSh
|
||||
)}
|
||||
</div>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<PillButton icon="horizontal_rule" label="Träger" hasMenu
|
||||
<PillButton mode={tileMode} icon="horizontal_rule" label="Träger" hasMenu
|
||||
hint={dis ? baseHint('Träger') :
|
||||
'Klick: Rechteck-Träger · Rechtsklick: Profil wählen'}
|
||||
disabled={dis}
|
||||
@@ -505,32 +551,32 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount, treppe2DSh
|
||||
</div>
|
||||
</PillGroup>
|
||||
|
||||
<PillGroup label="Raeume">
|
||||
<PillButton icon="crop_free" label="Raum"
|
||||
<PillGroup label="Raeume" mode={tileMode}>
|
||||
<PillButton mode={tileMode} icon="crop_free" label="Raum"
|
||||
hint={dis ? baseHint('Raum') :
|
||||
'Outline zeichnen · Stempel zeigt Name + Fläche'}
|
||||
disabled={dis}
|
||||
onClick={() => createRaum({})} />
|
||||
<PillButton icon="receipt_long" label="Stempel"
|
||||
<PillButton mode={tileMode} icon="receipt_long" label="Stempel"
|
||||
hint={dis ? baseHint('Stempel') :
|
||||
'SIA-Bilanz-Stempel platzieren · Default = aktives Geschoss · Properties: Total/Geschoss umstellen'}
|
||||
disabled={dis}
|
||||
onClick={() => createStempel({})} />
|
||||
</PillGroup>
|
||||
|
||||
<PillGroup label="Library">
|
||||
<PillButton icon="inventory_2" label="Symbol"
|
||||
<PillGroup label="Library" mode={tileMode}>
|
||||
<PillButton mode={tileMode} icon="inventory_2" label="Symbol"
|
||||
hint={dis ? baseHint('Symbol') :
|
||||
'Library-Picker oeffnen · Item waehlen · im Viewport Punkt klicken zum Platzieren'}
|
||||
disabled={dis}
|
||||
onClick={() => listLibrary()} />
|
||||
</PillGroup>
|
||||
|
||||
<PillGroup label="Importer">
|
||||
<PillButton icon="map" label="Swisstopo"
|
||||
<PillGroup label="Importer" mode={tileMode}>
|
||||
<PillButton mode={tileMode} icon="map" label="Swisstopo"
|
||||
hint="Vollautomatischer Import via swisstopo STAC-API: Adresse suchen, Radius wählen, Gebäude + Terrain + Luftbild holen"
|
||||
onClick={() => openSwisstopoDialog()} />
|
||||
<PillButton icon="public" label="OSM"
|
||||
<PillButton mode={tileMode} icon="public" label="OSM"
|
||||
hint="OpenStreetMap-Daten via Overpass-API als 2D-Linien: Strassen, Gebäudeumrisse, Wasser, Grünflächen, Wege"
|
||||
onClick={() => openOsmDialog()} />
|
||||
</PillGroup>
|
||||
|
||||
+55
-17
@@ -1,9 +1,20 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (C) 2026 Karim Gabriele Varano
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import Icon from './components/Icon'
|
||||
import { notifyReady, runRhinoCommand } from './lib/rhinoBridge'
|
||||
|
||||
// Anzeige-Modus der Kacheln, geteilt mit Elemente-Panel via localStorage.
|
||||
const TILE_MODE_KEY = 'dossier_tile_mode' // 'both' | 'icon' | 'text'
|
||||
function readTileMode() {
|
||||
try { return localStorage.getItem(TILE_MODE_KEY) || 'both' } catch { return 'both' }
|
||||
}
|
||||
function writeTileMode(m) {
|
||||
try { localStorage.setItem(TILE_MODE_KEY, m) } catch { /* WebView ohne Storage */ }
|
||||
}
|
||||
const TILE_MIN_COL = { icon: 40, text: 66, both: 58 }
|
||||
const TILE_MIN_H = { icon: 38, text: 30, both: 46 }
|
||||
|
||||
// Tool-Definitionen: [icon, label, rhino-command, tooltip]
|
||||
// Material-Symbol-Namen siehe https://fonts.google.com/icons
|
||||
const TOOLS = {
|
||||
@@ -54,7 +65,9 @@ const TOOLS = {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function ToolTile({ icon, label, cmd, tip }) {
|
||||
function ToolTile({ icon, label, cmd, tip, mode }) {
|
||||
const showIcon = mode !== 'text'
|
||||
const showLabel = mode !== 'icon'
|
||||
return (
|
||||
<button
|
||||
onClick={() => runRhinoCommand(cmd)}
|
||||
@@ -64,17 +77,17 @@ function ToolTile({ icon, label, cmd, tip }) {
|
||||
e.currentTarget.style.background = 'var(--bg-item-hover)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.borderColor = 'transparent'
|
||||
e.currentTarget.style.background = 'var(--bg-item)'
|
||||
e.currentTarget.style.borderColor = 'var(--border-light)'
|
||||
e.currentTarget.style.background = 'var(--bg-input)'
|
||||
}}
|
||||
style={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
alignItems: 'center', justifyContent: 'center', gap: 6,
|
||||
padding: '10px 4px',
|
||||
minHeight: 56,
|
||||
background: 'var(--bg-item)',
|
||||
border: '1px solid transparent',
|
||||
borderRadius: 10,
|
||||
alignItems: 'center', justifyContent: 'center', gap: 4,
|
||||
padding: '6px 4px',
|
||||
minHeight: TILE_MIN_H[mode],
|
||||
background: 'var(--bg-input)',
|
||||
border: '1px solid var(--border-light)',
|
||||
borderRadius: 999,
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.12s, border-color 0.12s',
|
||||
fontSize: 10, fontWeight: 500,
|
||||
@@ -82,18 +95,23 @@ function ToolTile({ icon, label, cmd, tip }) {
|
||||
appearance: 'none', WebkitAppearance: 'none',
|
||||
}}
|
||||
>
|
||||
<Icon name={icon} size={19} style={{ color: 'var(--accent)', flexShrink: 0 }} />
|
||||
{showIcon && (
|
||||
<Icon name={icon} size={mode === 'icon' ? 18 : 16}
|
||||
style={{ color: 'var(--accent)', flexShrink: 0 }} />
|
||||
)}
|
||||
{showLabel && (
|
||||
<span style={{
|
||||
maxWidth: '100%', overflow: 'hidden',
|
||||
textOverflow: 'ellipsis', whiteSpace: 'nowrap',
|
||||
}}>{label}</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function GridSection({ label, children }) {
|
||||
function GridSection({ label, mode, children }) {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
|
||||
<span style={{
|
||||
fontSize: 9, color: 'var(--text-muted)',
|
||||
letterSpacing: '0.08em', textTransform: 'uppercase',
|
||||
@@ -103,8 +121,8 @@ function GridSection({ label, children }) {
|
||||
</span>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(68px, 1fr))',
|
||||
gap: 6,
|
||||
gridTemplateColumns: `repeat(auto-fill, minmax(${TILE_MIN_COL[mode]}px, 1fr))`,
|
||||
gap: 5,
|
||||
}}>
|
||||
{children}
|
||||
</div>
|
||||
@@ -112,11 +130,28 @@ function GridSection({ label, children }) {
|
||||
)
|
||||
}
|
||||
|
||||
function TileModeDropdown({ mode, onChange }) {
|
||||
return (
|
||||
<select
|
||||
value={mode}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
title="Anzeige: Symbol / Text / Symbol + Text"
|
||||
style={{ fontSize: 10, padding: '3px 6px', maxWidth: 130 }}
|
||||
>
|
||||
<option value="icon">Symbol</option>
|
||||
<option value="text">Text</option>
|
||||
<option value="both">Symbol + Text</option>
|
||||
</select>
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export default function WerkzeugeApp() {
|
||||
const [mode, setMode] = useState(readTileMode)
|
||||
useEffect(() => { notifyReady() }, [])
|
||||
|
||||
const changeMode = (m) => { setMode(m); writeTileMode(m) }
|
||||
const groups = Object.entries(TOOLS)
|
||||
|
||||
return (
|
||||
@@ -129,10 +164,13 @@ export default function WerkzeugeApp() {
|
||||
boxSizing: 'border-box',
|
||||
overflowY: 'auto', overflowX: 'hidden',
|
||||
}}>
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||
<TileModeDropdown mode={mode} onChange={changeMode} />
|
||||
</div>
|
||||
{groups.map(([title, items]) => (
|
||||
<GridSection key={title} label={title}>
|
||||
<GridSection key={title} label={title} mode={mode}>
|
||||
{items.map(([icon, label, cmd, tip]) => (
|
||||
<ToolTile key={cmd} icon={icon} label={label} cmd={cmd} tip={tip} />
|
||||
<ToolTile key={cmd} icon={icon} label={label} cmd={cmd} tip={tip} mode={mode} />
|
||||
))}
|
||||
</GridSection>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user