diff --git a/client/BeastDevtools.btsx b/client/BeastDevtools.btsx index 2755fc0..6428ac2 100644 --- a/client/BeastDevtools.btsx +++ b/client/BeastDevtools.btsx @@ -44,6 +44,7 @@ module module interface DevtoolsInterfaceProps { + theme: 'dark' | 'light' open: boolean componentFinderActive: boolean componentFinderTitle: 'Stop Component Finder (Esc)' | 'Component Finder (Alt+Shift+C)' @@ -54,8 +55,8 @@ module } component DevtoolsInterface - props { open, componentFinderActive, componentFinderTitle, setActiveElementTool, elementPickerActive, elementPickerTitle, setOpen }: DevtoolsInterfaceProps - div(className={cx('bdt-root', 'bdt-launcher-host', open && 'is-hidden')} aria-hidden={open ? 'true' : undefined}) + props { theme, open, componentFinderActive, componentFinderTitle, setActiveElementTool, elementPickerActive, elementPickerTitle, setOpen }: DevtoolsInterfaceProps + div(data-theme={theme} className={cx('bdt-root', 'bdt-launcher-host', open && 'is-hidden')} aria-hidden={open ? 'true' : undefined}) button(type="button" className={cx('bdt-launcher-component-finder bg-white/50 backdrop-blur-3xl', componentFinderActive && 'is-component-finder-active')} aria-pressed={componentFinderActive} aria-label="Component Finder" title={componentFinderTitle} onClick={() => setActiveElementTool((value) => value === 'component-finder' ? null : 'component-finder')}) ComponentFinderIcon button( type="button" className={cx('bdt-launcher-element-picker bg-white/70 backdrop-blur-3xl hover:text-active!', elementPickerActive && 'is-element-picker-active')} aria-pressed={elementPickerActive} aria-label="Element Picker" title={elementPickerTitle} onClick={() => setActiveElementTool((value) => value === 'element-picker' ? null : 'element-picker')}) @@ -66,6 +67,7 @@ component DevtoolsInterface setup const runtime = useSyncExternalStore(subscribeRuntime, getRuntimeSnapshot, getRuntimeSnapshot); const [open, setOpen] = useState(initial.open); + const [theme, setTheme] = useState(initial.theme); // The dock stays mounted after closing until its slide-out ends. const [docked, setDocked] = useState(initial.open); const [tab, setTab] = useState(initial.tab); @@ -105,8 +107,8 @@ setup : runtime.status === 'connecting' ? 'Connecting…' : 'Runtime off'; useEffect(() => { - savePreferences({ open, tab, height, file, showControlFlow, componentsView, settings }); - }, [open, tab, height, file, showControlFlow, componentsView, settings]); + savePreferences({ theme, open, tab, height, file, showControlFlow, componentsView, settings }); + }, [theme, open, tab, height, file, showControlFlow, componentsView, settings]); useEffect(() => onSourceChanged(() => setRevision((value) => value + 1)), []); @@ -243,6 +245,7 @@ if open || docked ~ aria-label="Beast DevTools" ~ aria-hidden={open ? undefined : 'true'} ~ className={cx('bdt-root', 'bdt-dock', open && 'is-open')} + ~ data-theme={theme} ~ style={{ height: `${height}px` }} ~ onTransitionEnd={onDockTransitionEnd} ~ ) @@ -253,6 +256,8 @@ if open || docked ~ onPointerDown={(event) => startResize(event, height, setHeight)} ~ ) Topbar( + ~ theme={theme} + ~ onToggleTheme={() => setTheme((value) => value === 'dark' ? 'light' : 'dark')} ~ tab={tab} ~ setTab={setTab} ~ suggestionCount={suggestionCount} @@ -310,4 +315,4 @@ if open || docked ~ onSettingsChange={setSettings} ~ onRecentChange={(value) => { if (generation === projectGeneration.current) setRecent(value); }} ~ ) -DevtoolsInterface(open={open} componentFinderActive={componentFinderActive} componentFinderTitle={componentFinderTitle} setActiveElementTool={setActiveElementTool} elementPickerActive={elementPickerActive} elementPickerTitle={elementPickerTitle} setOpen={setOpen}) +DevtoolsInterface(theme={theme} open={open} componentFinderActive={componentFinderActive} componentFinderTitle={componentFinderTitle} setActiveElementTool={setActiveElementTool} elementPickerActive={elementPickerActive} elementPickerTitle={elementPickerTitle} setOpen={setOpen}) diff --git a/client/ElementsPanel.btsx b/client/ElementsPanel.btsx index e6b5c8d..657f015 100644 --- a/client/ElementsPanel.btsx +++ b/client/ElementsPanel.btsx @@ -41,9 +41,11 @@ component ElementPropertyRow const on = property.value === 'true'; .bdt-element-property if property.type === 'boolean' - SquishSwitch(checked={on} disabled={disabled || !property.editable} ariaLabel={property.name} width={28} height={18} trackColor="var(--bdt-bg-4)" trackOnColor="var(--bdt-ok)" thumbColor="var(--bdt-text)" thumbOnColor="var(--bdt-bg)" onChange={(next) => onCommit(property.name, String(next))}) + fragment + label.bdt-element-property-name(htmlFor={inputId} title={property.name}) #{property.name} + SquishSwitch(id={inputId} checked={on} disabled={disabled || !property.editable} ariaLabel={property.name} width={28} height={18} onChange={(next) => onCommit(property.name, String(next))}) elseif numeric !== null && property.editable - ScrubField.bdt-scrub-delta(label={property.name} value={numeric.value} defaultValue={original?.value ?? numeric.value} suffix={numeric.unit} min={(original?.value ?? numeric.value)/2} max={numeric.max} step={numeric.step} size="md" showDelta showDirty={true} fineMultiplier={0.1} coarseMultiplier={2} sensitivity={2} showFill={true} fillMin={original?.fillMin ?? numeric.fillMin} fillMax={original?.fillMax ?? numeric.fillMax} chipColor="#2a2a2a" accent="#666" onCommit={(next) => onCommit(property.name, `${next}${numeric.unit}${numeric.important}`)} disabled={disabled} className="bdt-style-scrub") + ScrubField.bdt-scrub-delta(label={property.name} value={numeric.value} defaultValue={original?.value ?? numeric.value} suffix={numeric.unit} min={(original?.value ?? numeric.value)/2} max={numeric.max} step={numeric.step} size="md" showDelta showDirty={true} fineMultiplier={0.1} coarseMultiplier={2} sensitivity={2} showFill={true} fillMin={original?.fillMin ?? numeric.fillMin} fillMax={original?.fillMax ?? numeric.fillMax} chipColor="var(--bdt-bg-4)" accent="var(--bdt-scrub-accent)" onCommit={(next) => onCommit(property.name, `${next}${numeric.unit}${numeric.important}`)} disabled={disabled} className="bdt-style-scrub") elseif property.editable .bdt-element-property-field label.bdt-element-property-name(htmlFor={inputId} title={property.name}) #{property.name} @@ -59,7 +61,9 @@ component ElementPropertyRow ~ onKeyDown={(event) => { if (event.key === 'Enter') event.currentTarget.blur(); if (event.key === 'Escape') { setDraft(property.value); setEditing(false); } }} ~ ) else - span.bdt-element-property-readonly(title={property.value}) #{property.value.length > 240 ? property.value.slice(0, 240) + '…' : property.value} + fragment + span.bdt-element-property-name(title={property.name}) #{property.name} + span.bdt-element-property-readonly(title={property.value}) #{property.value.length > 240 ? property.value.slice(0, 240) + '…' : property.value} if removable && property.editable button.bdt-icon-button(type="button" disabled={disabled} title={`Remove ${property.name}`} aria-label={`Remove ${property.name}`} onClick={() => onCommit(property.name, null)}) × elseif property.inline @@ -188,12 +192,13 @@ section.bdt-pane.bdt-elements-panel(aria-label="Selected element") .bdt-element-filter input.bdt-input(type="search" aria-label="Filter element properties" placeholder="Find a property or value…" value={query} onInput={(event) => setQuery(event.currentTarget.value)}) if group === 'styles' - SquishSwitch(checked={showAll} onChange={setShowAll} label="All computed styles" width={28} height={18} trackColor="var(--bdt-bg-4)" trackOnColor="var(--bdt-ok)" thumbColor="var(--bdt-text)" thumbOnColor="var(--bdt-bg)") - span.bdt-dim #{rows.length} properties + SquishSwitch(checked={showAll} onChange={setShowAll} label="All computed styles" width={28} height={18} trackColor="var(--bdt-bg-4)" trackOnColor="var(--bdt-ok)" thumbColor="var(--bdt-line-2)" thumbOnColor="#fafafa") + span.bdt-dim.bdt-prop-count #{rows.length} + span.bdt-dim properties .bdt-element-properties if rows.length === 0 p.bdt-element-note No matching properties. - div(className={cx(group === 'styles' && 'bdt-style-columns')}) + div(className={cx(group === 'styles' && 'bdt-style-columns', group === 'properties' && 'bdt-property-columns')}) each category in sections key `${group}:${category.id}` .bdt-element-section if category.label !== null @@ -208,7 +213,7 @@ section.bdt-pane.bdt-elements-panel(aria-label="Selected element") span #{category.label} span.bdt-dim #{category.properties.length} if isOpen(category) - div(className={cx('bdt-element-grid', category.columns === 2 && 'is-split')}) + div(className={cx('bdt-element-grid', group !== 'properties' && category.columns === 2 && 'is-split')}) each property in category.properties key `${group}:${property.name}` ElementPropertyRow(originalValue={savedSnapshot?.[group].find((entry) => entry.name === property.name)?.value} styleControl={group === 'styles'} property={property} disabled={saving || !snapshot.source || !snapshot.connected} removable={group === 'attributes' || (group === 'styles' && !!property.inline)} onCommit={edit}) if group !== 'properties' diff --git a/client/Topbar.btsx b/client/Topbar.btsx index 3cba70a..d964e1f 100644 --- a/client/Topbar.btsx +++ b/client/Topbar.btsx @@ -7,6 +7,8 @@ import { cx, TABS, type TabId } from './util.ts' module interface TopbarProps { + theme: 'dark' | 'light' + onToggleTheme: () => void tab: TabId setTab: (next: TabId | ((prev: TabId) => TabId)) => void suggestionCount: number @@ -46,7 +48,7 @@ component MainTabs if item.id === 'refactor' && suggestionCount > 0 span.bdt-tab-count.bdt-refactor-count(title={`${suggestionCount} refactor suggestion${suggestionCount === 1 ? '' : 's'}`}) #{suggestionCount} -props { tab, setTab, suggestionCount, componentFinderActive, componentFinderTitle, setActiveElementTool, elementPickerActive, elementPickerTitle, project, projectId, selectProject, runtime, status, setOpen }: TopbarProps +props { theme, onToggleTheme, tab, setTab, suggestionCount, componentFinderActive, componentFinderTitle, setActiveElementTool, elementPickerActive, elementPickerTitle, project, projectId, selectProject, runtime, status, setOpen }: TopbarProps header.bdt-topbar .bdt-brand BeastLogo @@ -54,6 +56,14 @@ header.bdt-topbar span.bdt-brand-sub Devtools MainTabs(tab={tab} setTab={setTab} suggestionCount={suggestionCount} componentCount={runtime.componentCount}) .bdt-spacer + button.bdt-icon-button(type="button" aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'} title={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'} onClick={onToggleTheme}) + svg(viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true") + if theme === 'dark' + fragment + circle(cx="12" cy="12" r="4") + path(d="M12 2v2m0 16v2M2 12h2m16 0h2M5 5l1.5 1.5m11 11L19 19M5 19l1.5-1.5m11-11L19 5") + else + path(d="M20.5 14A9 9 0 0 1 10 3.5 9 9 0 1 0 20.5 14Z") button(type="button" className={cx('bdt-icon-button', componentFinderActive && 'is-component-finder-active')} aria-pressed={componentFinderActive} aria-label="Component Finder" title={componentFinderTitle} onClick={() => setActiveElementTool((value) => value === 'component-finder' ? null : 'component-finder')}) ComponentFinderIcon button(type="button" className={cx('bdt-icon-button', elementPickerActive && 'is-element-picker-active')} aria-pressed={elementPickerActive} aria-label="Element Picker" title={elementPickerTitle} onClick={() => setActiveElementTool((value) => value === 'element-picker' ? null : 'element-picker')}) diff --git a/client/devtools.css b/client/devtools.css index 2d0db6d..8964bd8 100644 --- a/client/devtools.css +++ b/client/devtools.css @@ -25,10 +25,21 @@ } .bdt-root { + color-scheme: dark; + --bdt-syntax-keyword: #ff7a90; + --bdt-syntax-string: #b5e08d; + --bdt-syntax-selector: #8fd3ff; + --bdt-syntax-attr: #c9a7ff; + --bdt-syntax-type: #7fe0d0; + --color-text: var(--bdt-text); + --lime: var(--bdt-lime); --bdt-bg: #121312; --bdt-bg-2: #181818; --bdt-bg-3: #202020; --bdt-bg-4: #2a2a2a; + --bdt-scrub-accent: #666; + --bdt-scrub-delta-ink: #fff; + --bdt-scrub-change: color-mix(in srgb, var(--bdt-bg-4) 65%, #000); --bdt-line: rgba(255, 255, 255, 0.07); --bdt-lime: oklch(0.857 0.118 130.81); --bdt-line-2: rgba(255, 255, 255, 0.13); @@ -64,6 +75,33 @@ letter-spacing: normal; } +.bdt-root[data-theme='light'] { + color-scheme: light; + --bdt-syntax-keyword: #a32e53; + --bdt-syntax-string: #3c6e21; + --bdt-syntax-selector: #216f89; + --bdt-syntax-attr: #7346a3; + --bdt-syntax-type: #18796b; + --bdt-bg: #fafafa; + --bdt-bg-2: #f4f4f5; + --bdt-bg-3: #e4e4e7; + --bdt-bg-4: #e4e4e7; + --bdt-line: #18181b18; + --bdt-line-2: #18181b30; + --bdt-text: #18181b; + --bdt-muted: #52525b; + --bdt-dim: #71717a; + --bdt-trel: #18181b; + --bdt-lime: #466c1d; + --bdt-accent: #c6203e; + --bdt-accent-soft: #c6203e14; + --bdt-ok: #50ae80; + --bdt-warn: #97500a; + --bdt-bad: #be2739; + --bdt-info: #235eac; + --bdt-focus: #217249; +} + .bdt-root *, .bdt-root *::before, .bdt-root *::after { @@ -101,7 +139,7 @@ } .bdt-root ::-webkit-scrollbar-thumb { - background: rgba(255, 255, 255, 0.1); + background: var(--bdt-line-2); border: 3px solid transparent; background-clip: padding-box; border-radius: 999px; @@ -541,7 +579,7 @@ html.bdt-resizing * { .bdt-button.is-primary { border-color: rgba(255, 65, 90, 0.45); background: var(--bdt-accent-soft); - color: #ffd5db; + color: var(--bdt-accent); } .bdt-input { @@ -605,7 +643,7 @@ html.bdt-resizing * { .bdt-badge.is-accent { border-color: rgba(255, 65, 90, 0.35); - color: #ff8b9b; + color: var(--bdt-accent); } .bdt-badge.is-ok { @@ -631,6 +669,9 @@ html.bdt-resizing * { .bdt-mono { font-family: var(--bdt-mono); } +.bdt-prop-count { + width: 12px; +} .bdt-muted { color: var(--bdt-muted); @@ -649,7 +690,7 @@ html.bdt-resizing * { } .bdt-link:hover { - color: #b3d4ff; + color: var(--bdt-info); } .bdt-empty { @@ -787,7 +828,7 @@ html.bdt-resizing * { } .bdt-tree-name { - color: #ffb86b; + color: var(--bdt-warn); } .bdt-tree-name.is-flow { @@ -1007,7 +1048,7 @@ html.bdt-resizing * { font-family: var(--bdt-mono); font-size: 14px; font-weight: 600; - color: #ffb86b; + color: var(--bdt-warn); } .bdt-detail-actions { @@ -1040,7 +1081,7 @@ html.bdt-resizing * { display: inline-flex; align-items: center; gap: 6px; - color: #c9a7ff; + color: var(--bdt-syntax-attr); white-space: nowrap; } @@ -1070,7 +1111,7 @@ html.bdt-resizing * { } .bdt-v-string { - color: #b5e08d; + color: var(--bdt-syntax-string); } .bdt-v-number { color: var(--bdt-warn); @@ -1116,7 +1157,7 @@ html.bdt-resizing * { } .bdt-line:hover { - background: rgba(255, 255, 255, 0.035); + background: var(--bdt-bg-2); } .bdt-line.is-range { @@ -1169,7 +1210,7 @@ html.bdt-resizing * { .bdt-line.is-linked .bdt-ln, .bdt-line.is-active .bdt-ln { color: var(--bdt-accent); - background: #251417; + background: var(--bdt-accent-soft); } .bdt-depth { @@ -1213,10 +1254,10 @@ html.bdt-resizing * { } .tk-keyword { - color: #ff7a90; + color: var(--bdt-syntax-keyword); } .tk-string { - color: #b5e08d; + color: var(--bdt-syntax-string); } .tk-number { color: var(--bdt-warn); @@ -1229,13 +1270,13 @@ html.bdt-resizing * { color: var(--bdt-info); } .tk-component { - color: #ffb86b; + color: var(--bdt-warn); } .tk-selector { - color: #8fd3ff; + color: var(--bdt-syntax-selector); } .tk-attr { - color: #c9a7ff; + color: var(--bdt-syntax-attr); } .tk-directive { color: var(--bdt-accent); @@ -1245,10 +1286,10 @@ html.bdt-resizing * { color: var(--bdt-warn); } .tk-punct { - color: #8b8c86; + color: var(--bdt-dim); } .tk-type { - color: #7fe0d0; + color: var(--bdt-syntax-type); } /* ------------------------------------------------------------------------ */ @@ -1315,7 +1356,7 @@ html.bdt-resizing * { border: 0.33px solid var(--bdt-bg-2); border-radius: 4px; background: var(--bdt-bg-2); - color: oklch(0.783 0.015 142.21); + color: var(--bdt-trel); } .bdt-metric-value { @@ -1459,7 +1500,7 @@ html.bdt-resizing * { font-family: var(--bdt-mono); font-weight: 600; font-size: 14px; - color: #ffb86b; + color: var(--bdt-warn); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -1510,7 +1551,7 @@ html.bdt-resizing * { } .bdt-chip-type { - color: #7fe0d0; + color: var(--bdt-syntax-type); } .bdt-card-actions { @@ -1586,8 +1627,8 @@ html.bdt-resizing * { 33deg, /* slant angle: adjust for steeper/shallower lines */ transparent, transparent 10px, - #18181810 10px, - /* line color */ #1a1a1a 12px /* line thickness: 1px wide */ + var(--bdt-line) 10px, + /* line color */ var(--bdt-line) 12px /* line thickness: 1px wide */ ); } @@ -1613,13 +1654,13 @@ html.bdt-resizing * { padding: 4px 8px 4px 12px; border-bottom: 1px solid var(--bdt-line); background: rgba(95, 211, 154, 0.08); - color: #bff0d6; + color: var(--bdt-ok); flex: none; } .bdt-toast.is-error { background: rgba(255, 93, 108, 0.08); - color: #ffc2c8; + color: var(--bdt-bad); } .bdt-plan { @@ -1698,12 +1739,12 @@ html.bdt-resizing * { .bdt-diff-line.is-add { background: rgba(95, 211, 154, 0.12); - color: #bff0d6; + color: var(--bdt-ok); } .bdt-diff-line.is-remove { background: rgba(255, 93, 108, 0.12); - color: #ffc2c8; + color: var(--bdt-bad); } /* Editable suggestion name */ @@ -2006,7 +2047,9 @@ html.bdt-layout-dragging:is([data-bdt-layout-edge='top'], [data-bdt-layout-edge= font-size: var(--cc-font); box-shadow: 0 4px 20px #0006; } -.bdt-call-chip > span:not(.bdt-call-chip-fill) { position: relative; } +.bdt-call-chip > span:not(.bdt-call-chip-fill) { + position: relative; +} .bdt-call-chip > .bdt-call-chip-fill { position: absolute; inset: 0; @@ -2015,23 +2058,78 @@ html.bdt-layout-dragging:is([data-bdt-layout-edge='top'], [data-bdt-layout-edge= pointer-events: none; background: color-mix(in srgb, var(--cc-progress) var(--cc-progress-pct), transparent); } -.bdt-call-chip[data-status='running'] > .bdt-call-chip-fill { transition: transform var(--cc-expected) linear; } +.bdt-call-chip[data-status='running'] > .bdt-call-chip-fill { + transition: transform var(--cc-expected) linear; +} .bdt-call-chip[data-status='done'] > .bdt-call-chip-fill { background: color-mix(in srgb, var(--cc-done) var(--cc-wash-pct), transparent); - transition: transform 200ms ease, background-color 120ms ease; -} -.bdt-call-chip[data-status='error'] > .bdt-call-chip-fill { background: color-mix(in srgb, var(--cc-error) var(--cc-wash-pct), transparent); } -.bdt-call-chip-icons { flex: none; width: calc(var(--cc-font) + 2px); height: calc(var(--cc-font) + 2px); overflow: hidden; } -.bdt-call-chip-glyph { position: absolute; inset: 0; display: grid; place-items: center; opacity: 0; transform: translateY(70%); transition: opacity 160ms ease, transform 240ms ease; } -.bdt-call-chip-glyph[data-state='in'] { opacity: 1; transform: none; } -.bdt-call-chip-glyph[data-state='out'] { transform: translateY(-70%); } -.bdt-call-chip[data-status='done'] .bdt-call-chip-glyph { color: var(--cc-done); } -.bdt-call-chip[data-status='error'] .bdt-call-chip-glyph { color: var(--cc-error); } -.bdt-call-chip-timer { min-width: 8ch; text-align: right; font-variant-numeric: tabular-nums; opacity: .5; } -.bdt-call-chip-retry { position: absolute; inset: 0; background: transparent; border-radius: inherit; cursor: pointer; } -.bdt-call-chip-retry:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; } -.bdt-call-chip > .sr-only { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); } -@media (prefers-reduced-motion: reduce) { .bdt-call-chip-glyph, .bdt-call-chip > .bdt-call-chip-fill { transition: none; } } + transition: + transform 200ms ease, + background-color 120ms ease; +} +.bdt-call-chip[data-status='error'] > .bdt-call-chip-fill { + background: color-mix(in srgb, var(--cc-error) var(--cc-wash-pct), transparent); +} +.bdt-call-chip-icons { + flex: none; + width: calc(var(--cc-font) + 2px); + height: calc(var(--cc-font) + 2px); + overflow: hidden; +} +.bdt-call-chip-glyph { + position: absolute; + inset: 0; + display: grid; + place-items: center; + opacity: 0; + transform: translateY(70%); + transition: + opacity 160ms ease, + transform 240ms ease; +} +.bdt-call-chip-glyph[data-state='in'] { + opacity: 1; + transform: none; +} +.bdt-call-chip-glyph[data-state='out'] { + transform: translateY(-70%); +} +.bdt-call-chip[data-status='done'] .bdt-call-chip-glyph { + color: var(--cc-done); +} +.bdt-call-chip[data-status='error'] .bdt-call-chip-glyph { + color: var(--cc-error); +} +.bdt-call-chip-timer { + min-width: 8ch; + text-align: right; + font-variant-numeric: tabular-nums; + opacity: 0.5; +} +.bdt-call-chip-retry { + position: absolute; + inset: 0; + background: transparent; + border-radius: inherit; + cursor: pointer; +} +.bdt-call-chip-retry:focus-visible { + outline: 2px solid currentColor; + outline-offset: -2px; +} +.bdt-call-chip > .sr-only { + position: absolute !important; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); +} +@media (prefers-reduced-motion: reduce) { + .bdt-call-chip-glyph, + .bdt-call-chip > .bdt-call-chip-fill { + transition: none; + } +} .bdt-element-toolbar { padding: 10px; @@ -2176,15 +2274,33 @@ html.bdt-layout-dragging:is([data-bdt-layout-edge='top'], [data-bdt-layout-edge= container-type: inline-size; } -.bdt-style-columns { +.bdt-style-columns, +.bdt-property-columns { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; align-items: start; } -.bdt-style-columns .bdt-element-section + .bdt-element-section { +.bdt-property-columns { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} +.bdt-property-columns .bdt-element-property-readonly { + text-align: right; +} +.bdt-property-columns .bdt-squish-switch { + justify-self: end; +} +.bdt-style-columns .bdt-element-section + .bdt-element-section, +.bdt-property-columns .bdt-element-section + .bdt-element-section { margin-top: 0; } +.bdt-property-columns .bdt-element-grid .bdt-element-property { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 8px; +} +.bdt-property-columns .bdt-element-property-field { + grid-column: 1 / -1; +} .bdt-style-columns .bdt-element-grid:not(.is-split) .bdt-element-property { grid-template-columns: minmax(0, 1fr) auto; gap: 6px; @@ -2213,12 +2329,14 @@ html.bdt-layout-dragging:is([data-bdt-layout-edge='top'], [data-bdt-layout-edge= min-width: 0; } @container (max-width: 900px) { - .bdt-style-columns { + .bdt-style-columns, + .bdt-property-columns { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @container (max-width: 450px) { - .bdt-style-columns { + .bdt-style-columns, + .bdt-property-columns { grid-template-columns: minmax(0, 1fr); } } @@ -2301,7 +2419,9 @@ html.bdt-layout-dragging:is([data-bdt-layout-edge='top'], [data-bdt-layout-edge= .bdt-scrub-field:focus-within { outline: 1px solid var(--bdt-focus); } -.bdt-scrub-field { isolation: isolate; } +.bdt-scrub-field { + isolation: isolate; +} .bdt-scrub-field > .bdt-scrub-fill { position: absolute; inset: 0; @@ -2326,7 +2446,7 @@ html.bdt-layout-dragging:is([data-bdt-layout-edge='top'], [data-bdt-layout-edge= inset: 0; z-index: 1; transform-origin: left center; - background: color-mix(in srgb, var(--sf-chip) 65%, #000); + background: var(--bdt-scrub-change); pointer-events: none; } .bdt-scrub-field[data-dirty='true'] { @@ -2368,7 +2488,7 @@ html.bdt-layout-dragging:is([data-bdt-layout-edge='top'], [data-bdt-layout-edge= padding: 3px 6px; border-radius: 6px; background: var(--sf-accent); - color: var(--sf-ghost-ink); + color: var(--bdt-scrub-delta-ink); } .bdt-scrub-field[data-dragging='true'] > .bdt-scrub-delta { opacity: 1; @@ -2500,6 +2620,7 @@ html.bdt-layout-dragging:is([data-bdt-layout-edge='top'], [data-bdt-layout-edge= .bdt-element-property-name { color: var(--bdt-muted); overflow: hidden; + text-overflow: ellipsis; white-space: nowrap; font-family: var(--bdt-mono); font-size: 11px; diff --git a/client/util.ts b/client/util.ts index 61647fb..40a0df0 100644 --- a/client/util.ts +++ b/client/util.ts @@ -21,7 +21,8 @@ export function preview(value: unknown, budget = 90): string { function previewInner(value: unknown, depth: number): string { if (value === undefined) return 'undefined' if (value === null) return 'null' - if (typeof value === 'string') return /^\[(Function|Node|Getter|Array|Object|Unavailable)\]$/.test(value) ? value : JSON.stringify(value) + if (typeof value === 'string') + return /^\[(Function|Node|Getter|Array|Object|Unavailable)\]$/.test(value) ? value : JSON.stringify(value) if (typeof value !== 'object') return String(value) if (depth >= 2) return Array.isArray(value) ? `Array(${value.length})` : '{…}' if (Array.isArray(value)) return `[${value.map((item) => previewInner(item, depth + 1)).join(', ')}]` @@ -30,8 +31,10 @@ function previewInner(value: unknown, depth: number): string { } export function pretty(value: unknown): string { - return JSON.stringify(value, (_key, item) => (item === undefined ? '__undefined__' : item), 2) - .replaceAll('"__undefined__"', 'undefined') + return JSON.stringify(value, (_key, item) => (item === undefined ? '__undefined__' : item), 2).replaceAll( + '"__undefined__"', + 'undefined' + ) } export function valueTone(value: unknown): string { @@ -48,13 +51,14 @@ export function valueTone(value: unknown): string { export type TabId = 'components' | 'inspector' | 'refactor' | 'elements' export const TABS: ReadonlyArray<{ id: TabId; label: string }> = [ - { id: 'inspector', label: 'BTSX → TSRX' }, + { id: 'inspector', label: 'Beast → Octane' }, { id: 'components', label: 'Components' }, { id: 'elements', label: 'Elements' }, - { id: 'refactor', label: 'Refactor' }, + { id: 'refactor', label: 'Refactor' } ] export interface Preferences { + theme: 'dark' | 'light' open: boolean tab: TabId height: number @@ -67,13 +71,14 @@ export interface Preferences { const STORAGE_KEY = 'beast-devtools:preferences' export const DEFAULT_PREFERENCES: Preferences = { + theme: 'dark', open: false, tab: 'components', height: 360, file: null, showControlFlow: true, componentsView: 'blocks', - settings: DEFAULT_SETTINGS, + settings: DEFAULT_SETTINGS } export function loadPreferences(): Preferences { @@ -82,9 +87,10 @@ export function loadPreferences(): Preferences { return { ...DEFAULT_PREFERENCES, ...stored, + theme: stored.theme === 'light' ? 'light' : 'dark', settings: { ...DEFAULT_SETTINGS, ...stored.settings }, componentsView: stored.componentsView === 'tree' ? 'tree' : 'blocks', - height: clampHeight(stored.height ?? DEFAULT_PREFERENCES.height), + height: clampHeight(stored.height ?? DEFAULT_PREFERENCES.height) } } catch { return DEFAULT_PREFERENCES diff --git a/components/squishy.btsx b/components/squishy.btsx index df05fd7..0d4058e 100644 --- a/components/squishy.btsx +++ b/components/squishy.btsx @@ -38,7 +38,7 @@ module const STRETCH_SPEED = 600; const TAP_SLOP = { fine: 4, coarse: 8 }; -props { checked, defaultChecked = false, onChange, label, disabled = false, trackColor = "var(--border)", trackOnColor = "var(--foreground)", thumbColor='var(--sidebar)', thumbOnColor='var(--muted)', width = 52, height = 32, radius = 16, speed = 50, stretch = 50, hoverScale = 1.08, colorDuration = 180, ariaLabel, className, id }: SquishSwitchProps +props { checked, defaultChecked = false, onChange, label, disabled = false, trackColor = "var(--bdt-bg-4)", trackOnColor = "var(--bdt-ok)", thumbColor='var(--bdt-line-2)', thumbOnColor='#fafafa', width = 52, height = 32, radius = 16, speed = 50, stretch = 50, hoverScale = 1.08, colorDuration = 180, ariaLabel, className, id }: SquishSwitchProps setup const reduce = useReducedMotion();