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:
2026-06-06 15:14:29 +02:00
parent e531217cb7
commit c0624c0a62
2 changed files with 152 additions and 68 deletions
+82 -36
View File
@@ -21,6 +21,32 @@ const labelXs = {
letterSpacing: '0.06em', textTransform: 'uppercase', 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) { function fmtNum(v) {
if (v == null || v === '') return '' if (v == null || v === '') return ''
const n = Number(v) const n = Number(v)
@@ -51,8 +77,11 @@ function ReferenzSelector({ value, onChange }) {
// Raster-Kachel — Icon oben, Label darunter, einheitliche Zellgroesse. // Raster-Kachel — Icon oben, Label darunter, einheitliche Zellgroesse.
// hasMenu zeigt ein kleines Chevron neben dem Label (Rechtsklick = Untertypen). // 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, function PillButton({ icon, label, hint, onClick, onContextMenu, disabled,
hasMenu, badge }) { hasMenu, badge, mode = 'both' }) {
const showIcon = mode !== 'text'
const showLabel = mode !== 'icon'
return ( return (
<button <button
onClick={onClick} onClick={onClick}
@@ -61,12 +90,12 @@ function PillButton({ icon, label, hint, onClick, onContextMenu, disabled,
title={hint} title={hint}
style={{ style={{
display: 'flex', flexDirection: 'column', display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', gap: 6, alignItems: 'center', justifyContent: 'center', gap: 4,
padding: '10px 4px', padding: '6px 4px',
minHeight: 56, minHeight: TILE_MIN_H[mode],
background: 'var(--bg-item)', background: 'var(--bg-input)',
border: '1px solid transparent', border: '1px solid var(--border-light)',
borderRadius: 10, borderRadius: 999,
cursor: disabled ? 'not-allowed' : 'pointer', cursor: disabled ? 'not-allowed' : 'pointer',
opacity: disabled ? 0.4 : 1, opacity: disabled ? 0.4 : 1,
transition: 'background 0.12s, border-color 0.12s', 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)' e.currentTarget.style.borderColor = 'var(--accent)'
}}} }}}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.background = 'var(--bg-item)' e.currentTarget.style.background = 'var(--bg-input)'
e.currentTarget.style.borderColor = 'transparent' 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={{ <span style={{
display: 'flex', alignItems: 'center', gap: 2, display: 'flex', alignItems: 'center', gap: 2,
maxWidth: '100%', overflow: 'hidden', maxWidth: '100%', overflow: 'hidden',
@@ -97,9 +130,16 @@ function PillButton({ icon, label, hint, onClick, onContextMenu, disabled,
style={{ color: 'var(--text-muted)', flexShrink: 0 }} /> style={{ color: 'var(--text-muted)', flexShrink: 0 }} />
)} )}
</span> </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 && ( {badge && (
<span style={{ <span style={{
position: 'absolute', top: 4, right: 4, position: 'absolute', top: 3, right: 3,
fontSize: 8, padding: '0px 4px', borderRadius: 8, fontSize: 8, padding: '0px 4px', borderRadius: 8,
background: 'var(--bg-section)', color: 'var(--text-muted)', background: 'var(--bg-section)', color: 'var(--text-muted)',
}}>{badge}</span> }}>{badge}</span>
@@ -109,9 +149,9 @@ function PillButton({ icon, label, hint, onClick, onContextMenu, disabled,
} }
// Kategorie-Gruppe: Label + einheitliches Raster (auto-fill Spalten) // Kategorie-Gruppe: Label + einheitliches Raster (auto-fill Spalten)
function PillGroup({ label, children }) { function PillGroup({ label, mode = 'both', children }) {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
<span style={{ <span style={{
fontSize: 9, color: 'var(--text-muted)', fontSize: 9, color: 'var(--text-muted)',
letterSpacing: '0.08em', textTransform: 'uppercase', letterSpacing: '0.08em', textTransform: 'uppercase',
@@ -121,8 +161,8 @@ function PillGroup({ label, children }) {
</span> </span>
<div style={{ <div style={{
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(68px, 1fr))', gridTemplateColumns: `repeat(auto-fill, minmax(${TILE_MIN_COL[mode]}px, 1fr))`,
gap: 6, gap: 5,
}}> }}>
{children} {children}
</div> </div>
@@ -358,6 +398,8 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount, treppe2DSh
const [treppeMenuOpen, setTreppeMenuOpen] = useState(false) const [treppeMenuOpen, setTreppeMenuOpen] = useState(false)
const [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false) const [stuetzeMenuOpen, setStuetzeMenuOpen] = useState(false)
const [traegerMenuOpen, setTraegerMenuOpen] = useState(false) const [traegerMenuOpen, setTraegerMenuOpen] = useState(false)
const [tileMode, setTileMode] = useState(readTileMode)
const changeTileMode = (m) => { setTileMode(m); writeTileMode(m) }
const treppeWrapperRef = useRef(null) const treppeWrapperRef = useRef(null)
const dis = noGeschoss const dis = noGeschoss
const baseHint = (label) => const baseHint = (label) =>
@@ -435,37 +477,41 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount, treppe2DSh
)} )}
</div> </div>
<PillGroup label="Konstruktion"> <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<PillButton icon="view_week" label="Wand" <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} hint={baseHint('Wand zeichnen')} disabled={dis}
onClick={() => createWall({ geschoss: '' })} /> onClick={() => createWall({ geschoss: '' })} />
<PillButton icon="layers" label="Decke" <PillButton mode={tileMode} icon="layers" label="Decke"
hint={baseHint('Decke zeichnen')} disabled={dis} hint={baseHint('Decke zeichnen')} disabled={dis}
onClick={() => createDecke({ geschoss: '' })} /> onClick={() => createDecke({ geschoss: '' })} />
<PillButton icon="roofing" label="Dach" <PillButton mode={tileMode} icon="roofing" label="Dach"
hint={baseHint('Pultdach zeichnen — Traufe = 1. Kante')} disabled={dis} hint={baseHint('Pultdach zeichnen — Traufe = 1. Kante')} disabled={dis}
onClick={() => createDach({ geschoss: '' })} /> onClick={() => createDach({ geschoss: '' })} />
</PillGroup> </PillGroup>
<PillGroup label="Öffnungen"> <PillGroup label="Öffnungen" mode={tileMode}>
<PillButton icon="window" label="Fenster" <PillButton mode={tileMode} icon="window" label="Fenster"
hint={dis ? baseHint('Fenster') : hint={dis ? baseHint('Fenster') :
'Erst Wand-Achse wählen, dann Punkt darauf'} disabled={dis} 'Erst Wand-Achse wählen, dann Punkt darauf'} disabled={dis}
onClick={() => createFenster({})} /> onClick={() => createFenster({})} />
<PillButton icon="sensor_door" label="Tür" <PillButton mode={tileMode} icon="sensor_door" label="Tür"
hint={dis ? baseHint('Tür') : hint={dis ? baseHint('Tür') :
'Erst Wand-Achse wählen, dann Punkt darauf'} disabled={dis} 'Erst Wand-Achse wählen, dann Punkt darauf'} disabled={dis}
onClick={() => createTuer({})} /> onClick={() => createTuer({})} />
<PillButton icon="rectangle" label="Aussparung" <PillButton mode={tileMode} icon="rectangle" label="Aussparung"
hint={dis ? baseHint('Aussparung') : hint={dis ? baseHint('Aussparung') :
'Outline auf einer Decke zeichnen — wird automatisch ausgeschnitten'} 'Outline auf einer Decke zeichnen — wird automatisch ausgeschnitten'}
disabled={dis} disabled={dis}
onClick={() => createAussparung({})} /> onClick={() => createAussparung({})} />
</PillGroup> </PillGroup>
<PillGroup label="Erschliessung"> <PillGroup label="Erschliessung" mode={tileMode}>
<div ref={treppeWrapperRef} style={{ position: 'relative' }}> <div ref={treppeWrapperRef} style={{ position: 'relative' }}>
<PillButton icon="stairs" label="Treppe" hasMenu <PillButton mode={tileMode} icon="stairs" label="Treppe" hasMenu
hint={dis ? baseHint('Treppe') : hint={dis ? baseHint('Treppe') :
'Klick: gerade Treppe · Rechtsklick: Typ wählen'} 'Klick: gerade Treppe · Rechtsklick: Typ wählen'}
disabled={dis} disabled={dis}
@@ -478,9 +524,9 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount, treppe2DSh
</div> </div>
</PillGroup> </PillGroup>
<PillGroup label="Tragwerk"> <PillGroup label="Tragwerk" mode={tileMode}>
<div style={{ position: 'relative' }}> <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') : hint={dis ? baseHint('Stütze') :
'Klick: Quadrat-Stütze · Rechtsklick: Profil wählen'} 'Klick: Quadrat-Stütze · Rechtsklick: Profil wählen'}
disabled={dis} disabled={dis}
@@ -492,7 +538,7 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount, treppe2DSh
)} )}
</div> </div>
<div style={{ position: 'relative' }}> <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') : hint={dis ? baseHint('Träger') :
'Klick: Rechteck-Träger · Rechtsklick: Profil wählen'} 'Klick: Rechteck-Träger · Rechtsklick: Profil wählen'}
disabled={dis} disabled={dis}
@@ -505,32 +551,32 @@ function NeuesElementSection({ noGeschoss, activeName, elementsCount, treppe2DSh
</div> </div>
</PillGroup> </PillGroup>
<PillGroup label="Raeume"> <PillGroup label="Raeume" mode={tileMode}>
<PillButton icon="crop_free" label="Raum" <PillButton mode={tileMode} icon="crop_free" label="Raum"
hint={dis ? baseHint('Raum') : hint={dis ? baseHint('Raum') :
'Outline zeichnen · Stempel zeigt Name + Fläche'} 'Outline zeichnen · Stempel zeigt Name + Fläche'}
disabled={dis} disabled={dis}
onClick={() => createRaum({})} /> onClick={() => createRaum({})} />
<PillButton icon="receipt_long" label="Stempel" <PillButton mode={tileMode} icon="receipt_long" label="Stempel"
hint={dis ? baseHint('Stempel') : hint={dis ? baseHint('Stempel') :
'SIA-Bilanz-Stempel platzieren · Default = aktives Geschoss · Properties: Total/Geschoss umstellen'} 'SIA-Bilanz-Stempel platzieren · Default = aktives Geschoss · Properties: Total/Geschoss umstellen'}
disabled={dis} disabled={dis}
onClick={() => createStempel({})} /> onClick={() => createStempel({})} />
</PillGroup> </PillGroup>
<PillGroup label="Library"> <PillGroup label="Library" mode={tileMode}>
<PillButton icon="inventory_2" label="Symbol" <PillButton mode={tileMode} icon="inventory_2" label="Symbol"
hint={dis ? baseHint('Symbol') : hint={dis ? baseHint('Symbol') :
'Library-Picker oeffnen · Item waehlen · im Viewport Punkt klicken zum Platzieren'} 'Library-Picker oeffnen · Item waehlen · im Viewport Punkt klicken zum Platzieren'}
disabled={dis} disabled={dis}
onClick={() => listLibrary()} /> onClick={() => listLibrary()} />
</PillGroup> </PillGroup>
<PillGroup label="Importer"> <PillGroup label="Importer" mode={tileMode}>
<PillButton icon="map" label="Swisstopo" <PillButton mode={tileMode} icon="map" label="Swisstopo"
hint="Vollautomatischer Import via swisstopo STAC-API: Adresse suchen, Radius wählen, Gebäude + Terrain + Luftbild holen" hint="Vollautomatischer Import via swisstopo STAC-API: Adresse suchen, Radius wählen, Gebäude + Terrain + Luftbild holen"
onClick={() => openSwisstopoDialog()} /> 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" hint="OpenStreetMap-Daten via Overpass-API als 2D-Linien: Strassen, Gebäudeumrisse, Wasser, Grünflächen, Wege"
onClick={() => openOsmDialog()} /> onClick={() => openOsmDialog()} />
</PillGroup> </PillGroup>
+55 -17
View File
@@ -1,9 +1,20 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2026 Karim Gabriele Varano // Copyright (C) 2026 Karim Gabriele Varano
import { useEffect } from 'react' import { useEffect, useState } from 'react'
import Icon from './components/Icon' import Icon from './components/Icon'
import { notifyReady, runRhinoCommand } from './lib/rhinoBridge' 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] // Tool-Definitionen: [icon, label, rhino-command, tooltip]
// Material-Symbol-Namen siehe https://fonts.google.com/icons // Material-Symbol-Namen siehe https://fonts.google.com/icons
const TOOLS = { 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 ( return (
<button <button
onClick={() => runRhinoCommand(cmd)} onClick={() => runRhinoCommand(cmd)}
@@ -64,17 +77,17 @@ function ToolTile({ icon, label, cmd, tip }) {
e.currentTarget.style.background = 'var(--bg-item-hover)' e.currentTarget.style.background = 'var(--bg-item-hover)'
}} }}
onMouseLeave={(e) => { onMouseLeave={(e) => {
e.currentTarget.style.borderColor = 'transparent' e.currentTarget.style.borderColor = 'var(--border-light)'
e.currentTarget.style.background = 'var(--bg-item)' e.currentTarget.style.background = 'var(--bg-input)'
}} }}
style={{ style={{
display: 'flex', flexDirection: 'column', display: 'flex', flexDirection: 'column',
alignItems: 'center', justifyContent: 'center', gap: 6, alignItems: 'center', justifyContent: 'center', gap: 4,
padding: '10px 4px', padding: '6px 4px',
minHeight: 56, minHeight: TILE_MIN_H[mode],
background: 'var(--bg-item)', background: 'var(--bg-input)',
border: '1px solid transparent', border: '1px solid var(--border-light)',
borderRadius: 10, borderRadius: 999,
cursor: 'pointer', cursor: 'pointer',
transition: 'background 0.12s, border-color 0.12s', transition: 'background 0.12s, border-color 0.12s',
fontSize: 10, fontWeight: 500, fontSize: 10, fontWeight: 500,
@@ -82,18 +95,23 @@ function ToolTile({ icon, label, cmd, tip }) {
appearance: 'none', WebkitAppearance: 'none', 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={{ <span style={{
maxWidth: '100%', overflow: 'hidden', maxWidth: '100%', overflow: 'hidden',
textOverflow: 'ellipsis', whiteSpace: 'nowrap', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
}}>{label}</span> }}>{label}</span>
)}
</button> </button>
) )
} }
function GridSection({ label, children }) { function GridSection({ label, mode, children }) {
return ( return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 5 }}>
<span style={{ <span style={{
fontSize: 9, color: 'var(--text-muted)', fontSize: 9, color: 'var(--text-muted)',
letterSpacing: '0.08em', textTransform: 'uppercase', letterSpacing: '0.08em', textTransform: 'uppercase',
@@ -103,8 +121,8 @@ function GridSection({ label, children }) {
</span> </span>
<div style={{ <div style={{
display: 'grid', display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(68px, 1fr))', gridTemplateColumns: `repeat(auto-fill, minmax(${TILE_MIN_COL[mode]}px, 1fr))`,
gap: 6, gap: 5,
}}> }}>
{children} {children}
</div> </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() { export default function WerkzeugeApp() {
const [mode, setMode] = useState(readTileMode)
useEffect(() => { notifyReady() }, []) useEffect(() => { notifyReady() }, [])
const changeMode = (m) => { setMode(m); writeTileMode(m) }
const groups = Object.entries(TOOLS) const groups = Object.entries(TOOLS)
return ( return (
@@ -129,10 +164,13 @@ export default function WerkzeugeApp() {
boxSizing: 'border-box', boxSizing: 'border-box',
overflowY: 'auto', overflowX: 'hidden', overflowY: 'auto', overflowX: 'hidden',
}}> }}>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<TileModeDropdown mode={mode} onChange={changeMode} />
</div>
{groups.map(([title, items]) => ( {groups.map(([title, items]) => (
<GridSection key={title} label={title}> <GridSection key={title} label={title} mode={mode}>
{items.map(([icon, label, cmd, tip]) => ( {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> </GridSection>
))} ))}