2026-04-08 07:58:52 +02:00
import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
2026-04-08 07:41:16 +02:00
import { Link } from 'react-router-dom'
2026-04-08 07:58:52 +02:00
import { ChevronDown , ChevronUp , GripVertical , LayoutDashboard , Plus , Search , X } from 'lucide-react'
2026-04-08 07:41:16 +02:00
import { api , formatFastApiDetail } from '../utils/api'
import { ensurePilotLabWidgetsRegistered } from '../widgetSystem/registerPilotLabWidgets'
import {
BODY _CHART _DAYS _DEFAULT ,
BODY _CHART _DAYS _MAX ,
BODY _CHART _DAYS _MIN ,
normalizeBodyChartDays ,
} from '../widgetSystem/bodyChartDays'
import KpiBoardConfigEditor from '../widgetSystem/KpiBoardConfigEditor'
import QuickCaptureConfigEditor from '../widgetSystem/QuickCaptureConfigEditor'
2026-04-22 08:51:06 +02:00
import BodyHistoryVizConfigEditor from '../widgetSystem/BodyHistoryVizConfigEditor'
2026-04-08 07:58:52 +02:00
import {
moveWidget ,
moveWidgetToIndex ,
normalizeLayoutForEditor ,
toggleWidget ,
} from '../widgetSystem/layoutEditor'
2026-04-08 07:41:16 +02:00
const CHART _DAYS _WIDGET _IDS = new Set ( [
'body_overview' ,
2026-04-22 08:38:38 +02:00
'body_history_viz' ,
2026-04-08 07:41:16 +02:00
'activity_overview' ,
'nutrition_detail_charts' ,
'recovery_charts_panel' ,
] )
2026-04-08 07:58:52 +02:00
const DESKTOP _DND _MQ = '(min-width: 768px)'
2026-04-08 07:41:16 +02:00
function catalogMetaById ( catalog ) {
if ( ! catalog ? . widgets ? . length ) return { }
return Object . fromEntries ( catalog . widgets . map ( ( w ) => [ w . id , w ] ) )
}
2026-04-08 10:32:18 +02:00
/ * *
* @ param { { adminMode ? : boolean } } [ props ]
* /
export default function DashboardConfigurePage ( { adminMode = false } = { } ) {
2026-04-08 07:41:16 +02:00
ensurePilotLabWidgetsRegistered ( )
const [ bundle , setBundle ] = useState ( null )
2026-04-08 10:32:18 +02:00
const [ adminFromDatabase , setAdminFromDatabase ] = useState ( null )
2026-04-08 07:41:16 +02:00
const [ catalog , setCatalog ] = useState ( null )
const [ layout , setLayout ] = useState ( null )
2026-04-08 07:58:52 +02:00
const [ addPanelOpen , setAddPanelOpen ] = useState ( false )
const [ pickerSearch , setPickerSearch ] = useState ( '' )
const [ viewportDesktop , setViewportDesktop ] = useState ( ( ) =>
typeof window !== 'undefined' ? window . matchMedia ( DESKTOP _DND _MQ ) . matches : false
)
2026-04-08 07:41:16 +02:00
const [ busy , setBusy ] = useState ( false )
const [ msg , setMsg ] = useState ( null )
const [ err , setErr ] = useState ( null )
const [ chartDaysDraftByWidgetId , setChartDaysDraftByWidgetId ] = useState ( { } )
2026-04-08 07:58:52 +02:00
const [ dragOverFullIndex , setDragOverFullIndex ] = useState ( null )
const pickerSearchRef = useRef ( null )
const dndEnabled = viewportDesktop
useEffect ( ( ) => {
const mq = window . matchMedia ( DESKTOP _DND _MQ )
const fn = ( ) => setViewportDesktop ( mq . matches )
fn ( )
mq . addEventListener ( 'change' , fn )
return ( ) => mq . removeEventListener ( 'change' , fn )
} , [ ] )
useEffect ( ( ) => {
if ( ! addPanelOpen ) return
const t = window . setTimeout ( ( ) => pickerSearchRef . current ? . focus ( ) , 50 )
const prev = document . body . style . overflow
document . body . style . overflow = 'hidden'
const onKey = ( e ) => {
if ( e . key === 'Escape' ) setAddPanelOpen ( false )
}
window . addEventListener ( 'keydown' , onKey )
return ( ) => {
window . clearTimeout ( t )
document . body . style . overflow = prev
window . removeEventListener ( 'keydown' , onKey )
}
} , [ addPanelOpen ] )
2026-04-08 07:41:16 +02:00
const metaById = useMemo ( ( ) => catalogMetaById ( catalog ) , [ catalog ] )
const isWidgetCatalogAllowed = useCallback (
( widgetId ) => {
const m = metaById [ widgetId ]
if ( m == null ) return true
return m . allowed !== false
} ,
[ metaById ] ,
)
const commitChartDaysDraftToLayout = useCallback ( ( draftStr , baseLayout , widgetId ) => {
const clamped = normalizeBodyChartDays (
draftStr === '' || draftStr == null ? BODY _CHART _DAYS _DEFAULT : draftStr
)
return {
... baseLayout ,
widgets : baseLayout . widgets . map ( ( x ) =>
x . id !== widgetId ? x : { ... x , config : { ... x . config , chart _days : clamped } }
) ,
}
} , [ ] )
const load = useCallback ( async ( ) => {
setErr ( null )
try {
2026-04-08 10:32:18 +02:00
if ( adminMode ) {
const [ cat , d ] = await Promise . all ( [
api . adminGetWidgetsCatalogFull ( ) ,
api . adminGetDashboardProductDefault ( ) ,
] )
setCatalog ( cat )
setBundle ( { custom : false , product _default _layout : d . layout } )
setAdminFromDatabase ( ! ! d . from _database )
setChartDaysDraftByWidgetId ( { } )
setLayout ( normalizeLayoutForEditor ( structuredClone ( d . layout ) ) )
} else {
const [ cat , b ] = await Promise . all ( [ api . getAppWidgetsCatalog ( ) , api . getAppDashboardLayout ( ) ] )
setCatalog ( cat )
setBundle ( b )
setAdminFromDatabase ( null )
setChartDaysDraftByWidgetId ( { } )
const base = b . custom ? b . layout : structuredClone ( b . product _default _layout )
setLayout ( normalizeLayoutForEditor ( base ) )
}
2026-04-08 07:41:16 +02:00
} catch ( e ) {
setErr ( formatFastApiDetail ( null , e . message ) )
}
2026-04-08 10:32:18 +02:00
} , [ adminMode ] )
2026-04-08 07:41:16 +02:00
useEffect ( ( ) => {
load ( )
} , [ load ] )
2026-04-08 07:58:52 +02:00
const openAddPanel = ( ) => {
setPickerSearch ( '' )
setAddPanelOpen ( true )
}
2026-04-08 07:41:16 +02:00
const save = async ( ) => {
if ( ! layout ) return
let toSave = layout
const draftEntries = Object . entries ( chartDaysDraftByWidgetId )
if ( draftEntries . length ) {
for ( const [ wid , val ] of draftEntries ) {
toSave = normalizeLayoutForEditor ( commitChartDaysDraftToLayout ( val , toSave , wid ) )
}
setLayout ( toSave )
setChartDaysDraftByWidgetId ( { } )
}
setBusy ( true )
setMsg ( null )
setErr ( null )
try {
2026-04-08 10:32:18 +02:00
if ( adminMode ) {
await api . adminPutDashboardProductDefault ( toSave )
setMsg ( 'System-Standard für die Übersicht wurde gespeichert.' )
} else {
await api . putAppDashboardLayout ( toSave )
setMsg ( 'Dein Dashboard wurde gespeichert.' )
}
2026-04-08 07:41:16 +02:00
await load ( )
} catch ( e ) {
setErr ( formatFastApiDetail ( null , e . message ) )
} finally {
setBusy ( false )
}
}
const resetToSystem = async ( ) => {
2026-04-08 10:32:18 +02:00
const ok = adminMode
? window . confirm (
'Eintrag in der Datenbank löschen und Layout aus dem Code (widget_catalog) wiederherstellen?'
)
: window . confirm ( 'Dein individuelles Layout löschen und System-Standard wiederherstellen?' )
if ( ! ok ) return
2026-04-08 07:41:16 +02:00
setBusy ( true )
setMsg ( null )
setErr ( null )
try {
2026-04-08 10:32:18 +02:00
if ( adminMode ) {
const r = await api . adminDeleteDashboardProductDefault ( )
setChartDaysDraftByWidgetId ( { } )
setLayout ( normalizeLayoutForEditor ( r . layout ) )
setMsg ( 'Code-Standard wiederhergestellt (kein DB-Override mehr).' )
} else {
const r = await api . resetAppDashboardLayout ( )
setChartDaysDraftByWidgetId ( { } )
setLayout ( normalizeLayoutForEditor ( r . layout ) )
setMsg ( 'Auf System-Standard zurückgesetzt.' )
}
2026-04-08 07:41:16 +02:00
await load ( )
} catch ( e ) {
setErr ( formatFastApiDetail ( null , e . message ) )
} finally {
setBusy ( false )
}
}
2026-04-08 07:58:52 +02:00
const pickerLower = pickerSearch . trim ( ) . toLowerCase ( )
2026-04-08 07:41:16 +02:00
const libraryIndices = useMemo ( ( ) => {
if ( ! layout ? . widgets ) return [ ]
return layout . widgets
. map ( ( w , i ) => i )
. filter ( ( i ) => {
const w = layout . widgets [ i ]
if ( w . enabled || ! isWidgetCatalogAllowed ( w . id ) ) return false
2026-04-08 07:58:52 +02:00
if ( ! pickerLower ) return true
2026-04-08 07:41:16 +02:00
const m = metaById [ w . id ]
const hay = ` ${ m ? . title || '' } ${ m ? . description || '' } ${ w . id } ` . toLowerCase ( )
2026-04-08 07:58:52 +02:00
return hay . includes ( pickerLower )
2026-04-08 07:41:16 +02:00
} )
2026-04-08 07:58:52 +02:00
} , [ layout , pickerLower , metaById , isWidgetCatalogAllowed ] )
2026-04-08 07:41:16 +02:00
const activeIndices = useMemo ( ( ) => {
if ( ! layout ? . widgets ) return [ ]
return layout . widgets
. map ( ( w , i ) => i )
. filter ( ( i ) => layout . widgets [ i ] . enabled && isWidgetCatalogAllowed ( layout . widgets [ i ] . id ) )
} , [ layout , isWidgetCatalogAllowed ] )
2026-04-08 07:58:52 +02:00
const addableCount = useMemo ( ( ) => {
if ( ! layout ? . widgets ) return 0
return layout . widgets . filter ( ( w ) => ! w . enabled && isWidgetCatalogAllowed ( w . id ) ) . length
} , [ layout , isWidgetCatalogAllowed ] )
const onDragStartRow = ( e , fullIndex ) => {
if ( ! dndEnabled ) return
e . dataTransfer . effectAllowed = 'move'
e . dataTransfer . setData ( 'text/plain' , String ( fullIndex ) )
try {
e . dataTransfer . setDragImage ( e . currentTarget , 0 , 0 )
} catch {
/* ok */
}
}
const onDragOverRow = ( e , fullIndex ) => {
if ( ! dndEnabled ) return
e . preventDefault ( )
e . dataTransfer . dropEffect = 'move'
setDragOverFullIndex ( fullIndex )
}
const onDragLeaveRow = ( ) => {
setDragOverFullIndex ( null )
}
const onDropRow = ( e , dropFullIndex ) => {
if ( ! dndEnabled ) return
e . preventDefault ( )
setDragOverFullIndex ( null )
const raw = e . dataTransfer . getData ( 'text/plain' )
const from = Number . parseInt ( raw , 10 )
if ( ! Number . isFinite ( from ) ) return
setLayout ( ( L ) => normalizeLayoutForEditor ( moveWidgetToIndex ( L , from , dropFullIndex ) ) )
}
const onDragEndRow = ( ) => {
setDragOverFullIndex ( null )
}
2026-04-08 07:41:16 +02:00
if ( err && ! layout ) {
return (
< div style = { { padding : 24 , maxWidth : 640 , margin : '0 auto' } } >
< p style = { { color : '#D85A30' } } > { err } < / p >
< button type = "button" className = "btn btn-secondary" onClick = { load } >
Erneut laden
< / button >
< / div >
)
}
if ( ! layout ) {
return (
< div style = { { padding : 48 , textAlign : 'center' } } >
< div className = "spinner" style = { { width : 32 , height : 32 , margin : '0 auto' } } / >
< / div >
)
}
return (
< div style = { { paddingBottom : 96 , textAlign : 'left' , maxWidth : 720 , margin : '0 auto' } } >
< div style = { { marginBottom : 20 } } >
< Link
2026-04-08 10:32:18 +02:00
to = { adminMode ? '/admin/g/system' : '/settings' }
2026-04-08 07:41:16 +02:00
className = "btn btn-secondary"
style = { { display : 'inline-flex' , marginBottom : 12 , textDecoration : 'none' } }
>
2026-04-08 10:32:18 +02:00
{ adminMode ? '← Basiseinstellungen (Admin)' : '← Einstellungen' }
2026-04-08 07:41:16 +02:00
< / Link >
< h1 className = "page-title" style = { { display : 'flex' , alignItems : 'center' , gap : 10 } } >
< LayoutDashboard size = { 26 } color = "var(--accent)" / >
2026-04-08 10:32:18 +02:00
{ adminMode ? 'Produkt-Übersicht: Systemstandard' : 'Übersicht anpassen' }
2026-04-08 07:41:16 +02:00
< / h1 >
< p style = { { fontSize : 13 , color : 'var(--text2)' , lineHeight : 1.6 , marginTop : 8 } } >
2026-04-08 10:32:18 +02:00
{ adminMode
? 'Globales Standard-Dashboard für alle Nutzer ohne eigenes Layout. Gespeichert in der Datenbank; mit „Code-Standard wiederherstellen“ wird der Eintrag entfernt und der Fallback aus dem Code genutzt.'
: 'Kacheln für die Startseite sortieren und entfernen. Neue Kacheln über „Kachel hinzufügen“ – mit Suche direkt im eigenen Fenster, ohne langes Scrollen.' }
2026-04-08 07:41:16 +02:00
< / p >
2026-04-08 10:32:18 +02:00
{ adminMode && adminFromDatabase != null && (
< p style = { { fontSize : 12 , color : 'var(--accent)' , marginTop : 10 , lineHeight : 1.5 } } >
{ adminFromDatabase ? (
< >
Aktuell gilt ein < strong > gespeicherter Systemstandard < / strong > ( Datenbank ) .
< / >
) : (
< >
Es liegt < strong > kein DB - Override < / strong > vor — es wird der Code - Standard aus dem Widget - Katalog
verwendet .
< / >
) }
< / p >
) }
{ ! adminMode && ! bundle ? . custom && (
2026-04-08 07:41:16 +02:00
< p style = { { fontSize : 12 , color : 'var(--accent)' , marginTop : 10 , lineHeight : 1.5 } } >
Du bearbeitest gerade das < strong > System - Standardlayout < / strong > . Mit „ Speichern “ legst du deine persönliche
Version ab .
< / p >
) }
< / div >
< div className = "card section-gap" style = { { marginBottom : 16 } } >
2026-04-08 07:58:52 +02:00
< div className = "card-title" style = { { fontSize : 15 , display : 'flex' , justifyContent : 'space-between' , alignItems : 'center' , gap : 12 , flexWrap : 'wrap' } } >
< span >
Aktive Kacheln · { activeIndices . length }
{ dndEnabled && (
< span style = { { fontSize : 12 , fontWeight : 400 , color : 'var(--text3)' , marginLeft : 8 } } >
( ab Desktop : ziehen oder Pfeile )
< / span >
) }
< / span >
< button type = "button" className = "btn btn-primary" style = { { fontSize : 13 } } onClick = { openAddPanel } disabled = { addableCount === 0 } >
< Plus size = { 16 } style = { { marginRight : 6 , verticalAlign : 'middle' } } / >
Kachel hinzufügen { addableCount > 0 ? ` ( ${ addableCount } ) ` : '' }
< / button >
2026-04-08 07:41:16 +02:00
< / div >
2026-04-08 07:58:52 +02:00
{ addableCount === 0 && (
< p style = { { fontSize : 12 , color : 'var(--text3)' , marginTop : 8 , marginBottom : 0 } } >
Alle freigeschalteten Kacheln sind aktiv .
< / p >
) }
{ err && < p style = { { fontSize : 12 , color : '#D85A30' , marginTop : 12 , marginBottom : 0 } } > { err } < / p > }
{ msg && < p style = { { fontSize : 12 , color : 'var(--accent)' , marginTop : 12 , marginBottom : 0 } } > { msg } < / p > }
< ul style = { { listStyle : 'none' , padding : 0 , margin : '12px 0 0' } } >
2026-04-08 07:41:16 +02:00
{ activeIndices . map ( ( i ) => {
const w = layout . widgets [ i ]
const label = metaById [ w . id ] ? . title || w . id
const chartDaysVal =
w . config ? . chart _days != null
? normalizeBodyChartDays ( w . config . chart _days )
: BODY _CHART _DAYS _DEFAULT
2026-04-08 07:58:52 +02:00
const dragOver = dragOverFullIndex === i
2026-04-08 07:41:16 +02:00
return (
< li
key = { w . id }
2026-04-08 07:58:52 +02:00
onDragOver = { ( e ) => onDragOverRow ( e , i ) }
onDragLeave = { onDragLeaveRow }
onDrop = { ( e ) => onDropRow ( e , i ) }
2026-04-08 07:41:16 +02:00
style = { {
padding : '10px 0' ,
borderBottom : '1px solid var(--border)' ,
2026-04-08 07:58:52 +02:00
background : dragOver ? 'var(--surface2)' : undefined ,
borderRadius : dragOver ? 8 : undefined ,
2026-04-08 07:41:16 +02:00
} }
>
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 , flexWrap : 'wrap' } } >
2026-04-08 07:58:52 +02:00
{ dndEnabled && (
< span
draggable
role = "button"
tabIndex = { 0 }
aria - label = { ` ${ label } verschieben ` }
style = { { cursor : 'grab' , color : 'var(--text3)' , display : 'flex' , touchAction : 'none' } }
onDragStart = { ( e ) => onDragStartRow ( e , i ) }
onDragEnd = { onDragEndRow }
>
< GripVertical size = { 18 } / >
< / span >
) }
2026-04-08 07:41:16 +02:00
< label style = { { display : 'flex' , alignItems : 'center' , gap : 8 , flex : '1 1 160px' } } >
< input type = "checkbox" checked = { w . enabled } onChange = { ( ) => setLayout ( ( L ) => toggleWidget ( L , i ) ) } / >
< span style = { { fontSize : 14 } } > { label } < / span >
< / label >
< div style = { { display : 'flex' , gap : 6 } } >
< button
type = "button"
className = "btn btn-secondary"
style = { { padding : '6px 10px' } }
aria - label = "Nach oben"
onClick = { ( ) => setLayout ( ( L ) => moveWidget ( L , i , - 1 ) ) }
>
< ChevronUp size = { 18 } / >
< / button >
< button
type = "button"
className = "btn btn-secondary"
style = { { padding : '6px 10px' } }
aria - label = "Nach unten"
onClick = { ( ) => setLayout ( ( L ) => moveWidget ( L , i , 1 ) ) }
>
< ChevronDown size = { 18 } / >
< / button >
< / div >
< / div >
{ w . id === 'quick_capture' && (
< QuickCaptureConfigEditor
config = { w . config || { } }
onChange = { ( next ) =>
setLayout ( ( L ) =>
normalizeLayoutForEditor ( {
... L ,
widgets : L . widgets . map ( ( x , j ) => {
if ( j !== i ) return x
const cfg = { ... ( x . config || { } ) }
for ( const k of [ 'show_weight' , 'show_resting_hr' , 'show_hrv' , 'show_vo2_max' ] ) {
delete cfg [ k ]
}
Object . assign ( cfg , next )
return { ... x , config : cfg }
} ) ,
} )
)
}
/ >
) }
{ w . id === 'kpi_board' && (
< KpiBoardConfigEditor
tiles = { Object . prototype . hasOwnProperty . call ( w . config || { } , 'tiles' ) ? w . config . tiles : undefined }
onChange = { ( next ) =>
setLayout ( ( L ) =>
normalizeLayoutForEditor ( {
... L ,
widgets : L . widgets . map ( ( x , j ) => {
if ( j !== i ) return x
const cfg = { ... ( x . config || { } ) }
if ( next === undefined ) {
delete cfg . tiles
} else {
cfg . tiles = next
}
return { ... x , config : cfg }
} ) ,
} )
)
}
/ >
) }
{ CHART _DAYS _WIDGET _IDS . has ( w . id ) && (
< div style = { { marginTop : 10 , marginLeft : 28 } } >
< label style = { { fontSize : 12 , color : 'var(--text2)' , display : 'block' , marginBottom : 4 } } >
Zeitraum ( Tage ) : { BODY _CHART _DAYS _MIN } – { BODY _CHART _DAYS _MAX }
< / label >
< input
type = "text"
inputMode = "numeric"
autoComplete = "off"
className = "form-input"
style = { { maxWidth : 120 } }
value = {
chartDaysDraftByWidgetId [ w . id ] !== undefined
? chartDaysDraftByWidgetId [ w . id ]
: String ( chartDaysVal )
}
onFocus = { ( ) =>
setChartDaysDraftByWidgetId ( ( prev ) => ( {
... prev ,
[ w . id ] : String ( chartDaysVal ) ,
} ) )
}
onChange = { ( e ) =>
setChartDaysDraftByWidgetId ( ( prev ) => ( {
... prev ,
[ w . id ] : e . target . value ,
} ) )
}
onBlur = { ( e ) => {
const raw = e . target . value
setLayout ( ( L ) =>
normalizeLayoutForEditor ( commitChartDaysDraftToLayout ( raw , L , w . id ) )
)
setChartDaysDraftByWidgetId ( ( prev ) => {
const next = { ... prev }
delete next [ w . id ]
return next
} )
} }
onKeyDown = { ( e ) => {
if ( e . key === 'Enter' ) e . currentTarget . blur ( )
} }
/ >
< / div >
) }
2026-04-22 08:51:06 +02:00
{ w . id === 'body_history_viz' && (
< BodyHistoryVizConfigEditor
config = { w . config || { } }
onChange = { ( next ) =>
setLayout ( ( L ) =>
normalizeLayoutForEditor ( {
... L ,
widgets : L . widgets . map ( ( x , j ) => {
if ( j !== i ) return x
if ( Object . keys ( next ) . length === 0 ) return { ... x , config : { } }
return { ... x , config : { ... ( x . config || { } ) , ... next } }
} ) ,
} )
)
}
/ >
) }
2026-04-08 07:41:16 +02:00
< / li >
)
} ) }
< / ul >
< / div >
< div style = { { display : 'flex' , flexWrap : 'wrap' , gap : 8 } } >
< button type = "button" className = "btn btn-primary" disabled = { busy } onClick = { save } >
Speichern
< / button >
< button type = "button" className = "btn btn-secondary" disabled = { busy } onClick = { resetToSystem } >
2026-04-08 10:32:18 +02:00
{ adminMode ? 'Code-Standard wiederherstellen' : 'System-Standard wiederherstellen' }
2026-04-08 07:41:16 +02:00
< / button >
2026-04-08 10:32:18 +02:00
< Link
to = { adminMode ? '/admin' : '/' }
className = "btn btn-secondary"
style = { { textDecoration : 'none' , textAlign : 'center' } }
>
{ adminMode ? 'Admin-Übersicht' : 'Zur Übersicht' }
2026-04-08 07:41:16 +02:00
< / Link >
< / div >
2026-04-08 07:58:52 +02:00
{ addPanelOpen && (
< div
style = { {
position : 'fixed' ,
inset : 0 ,
zIndex : 300 ,
display : 'flex' ,
alignItems : viewportDesktop ? 'center' : 'flex-end' ,
justifyContent : 'center' ,
padding : viewportDesktop ? 16 : 0 ,
paddingBottom : viewportDesktop ? 16 : undefined ,
} }
>
< button
type = "button"
aria - label = "Schließen"
style = { {
position : 'absolute' ,
inset : 0 ,
border : 'none' ,
padding : 0 ,
margin : 0 ,
background : 'rgba(0,0,0,0.45)' ,
cursor : 'pointer' ,
} }
onClick = { ( ) => setAddPanelOpen ( false ) }
/ >
< div
role = "dialog"
aria - modal = "true"
aria - labelledby = "dashboard-add-widget-title"
onClick = { ( e ) => e . stopPropagation ( ) }
style = { {
position : 'relative' ,
width : '100%' ,
maxWidth : viewportDesktop ? 520 : '100%' ,
maxHeight : viewportDesktop ? 'min(85vh, 640px)' : 'min(92vh, 100%)' ,
background : 'var(--surface)' ,
borderRadius : viewportDesktop ? 12 : '16px 16px 0 0' ,
boxShadow : viewportDesktop ? '0 12px 40px rgba(0,0,0,0.2)' : '0 -4px 24px rgba(0,0,0,0.12)' ,
display : 'flex' ,
flexDirection : 'column' ,
overflow : 'hidden' ,
paddingBottom : 'max(12px, env(safe-area-inset-bottom))' ,
} }
>
< div
style = { {
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'space-between' ,
gap : 12 ,
padding : '14px 16px' ,
borderBottom : '1px solid var(--border)' ,
flexShrink : 0 ,
} }
>
< h2 id = "dashboard-add-widget-title" className = "card-title" style = { { fontSize : 16 , margin : 0 } } >
Kachel hinzufügen
< / h2 >
< button
type = "button"
className = "btn btn-secondary"
style = { { padding : '8px 10px' } }
aria - label = "Schließen"
onClick = { ( ) => setAddPanelOpen ( false ) }
>
< X size = { 20 } / >
< / button >
< / div >
< div style = { { padding : '12px 16px' , flexShrink : 0 , borderBottom : '1px solid var(--border)' } } >
< div style = { { display : 'flex' , alignItems : 'center' , gap : 8 } } >
< Search size = { 18 } color = "var(--text3)" / >
< input
ref = { pickerSearchRef }
type = "search"
className = "form-input"
placeholder = "Suchen nach Titel, Beschreibung, ID …"
value = { pickerSearch }
onChange = { ( e ) => setPickerSearch ( e . target . value ) }
aria - label = "Widgets durchsuchen"
style = { { flex : 1 } }
/ >
< / div >
< / div >
< div style = { { overflowY : 'auto' , flex : 1 , padding : '8px 16px 16px' } } >
{ libraryIndices . length === 0 ? (
< p style = { { fontSize : 13 , color : 'var(--text3)' , margin : 12 } } >
{ pickerLower ? 'Keine Treffer.' : 'Keine weiteren Kacheln verfügbar.' }
< / p >
) : (
< ul style = { { listStyle : 'none' , padding : 0 , margin : 0 } } >
{ libraryIndices . map ( ( i ) => {
const w = layout . widgets [ i ]
const m = metaById [ w . id ]
return (
< li
key = { w . id }
style = { {
display : 'flex' ,
alignItems : 'center' ,
justifyContent : 'space-between' ,
gap : 12 ,
padding : '12px 0' ,
borderBottom : '1px solid var(--border)' ,
} }
>
< div style = { { minWidth : 0 } } >
< div style = { { fontSize : 14 , fontWeight : 600 } } > { m ? . title || w . id } < / div >
< div style = { { fontSize : 12 , color : 'var(--text3)' , marginTop : 2 } } > { m ? . description || '' } < / div >
< / div >
< button
type = "button"
className = "btn btn-primary"
style = { { flexShrink : 0 , fontSize : 12 , padding : '8px 14px' } }
onClick = { ( ) => {
setLayout ( ( L ) =>
normalizeLayoutForEditor ( {
... L ,
widgets : L . widgets . map ( ( x , j ) => ( j === i ? { ... x , enabled : true } : x ) ) ,
} )
)
} }
>
Hinzufügen
< / button >
< / li >
)
} ) }
< / ul >
) }
< / div >
< / div >
< / div >
) }
2026-04-08 07:41:16 +02:00
< / div >
)
}