Improving Win95 styling

This commit is contained in:
2026-07-03 23:57:26 +02:00
parent 0d8d96d5e3
commit 7fae2bd64e
18 changed files with 1471 additions and 200 deletions
+55 -7
View File
@@ -33,6 +33,7 @@ const SKIN_STORAGE_KEY = 'ca-studio-skin'
const GALLERY_TAB_STORAGE_KEY = 'ca-studio-gallery-tab'
const ENGINE_DIMENSION_STORAGE_KEY = 'ca-studio-engine-dimension'
const ICG_DIMENSION_STORAGE_KEY = 'ca-studio-icg-dimension'
const DEFAULT_SKIN = 'windows-95'
const SKINS = [
{ id: 'retro-crt', label: 'Retro CRT' },
@@ -571,7 +572,7 @@ function App() {
const [route, setRoute] = React.useState(routeFromLocation)
const [skin, setSkin] = React.useState<StudioSkin>(() => {
const stored = window.localStorage.getItem(SKIN_STORAGE_KEY)
return SKINS.some((item) => item.id === stored) ? (stored as StudioSkin) : 'retro-crt'
return SKINS.some((item) => item.id === stored) ? (stored as StudioSkin) : DEFAULT_SKIN
})
React.useEffect(() => {
@@ -614,8 +615,16 @@ function AppFooter({
skin: StudioSkin
onSkinChange: (skin: StudioSkin) => void
}) {
const [clock, setClock] = React.useState(() => formatTaskbarClock(new Date()))
React.useEffect(() => {
const timer = window.setInterval(() => setClock(formatTaskbarClock(new Date())), 30_000)
return () => window.clearInterval(timer)
}, [])
return (
<footer className="app-footer">
<div className="footer-start" aria-hidden="true">Start</div>
<p>CA Lab is developed by Glitch University.</p>
<label className="skin-picker">
<span>Skin</span>
@@ -627,10 +636,25 @@ function AppFooter({
))}
</select>
</label>
<div className="footer-clock" aria-label={`Current time ${clock}`}>{clock}</div>
</footer>
)
}
function formatTaskbarClock(date: Date) {
return date.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })
}
function Win95WindowControls() {
return (
<span className="win95-window-controls" aria-hidden="true">
<span>_</span>
<span></span>
<span>×</span>
</span>
)
}
function DeckGallery({ initialTab }: { initialTab: GalleryTab }) {
const [activeTab, setActiveTab] = React.useState<GalleryTab>(initialTab)
const [engineDimension, setEngineDimension] = React.useState<EngineDimension>(() => {
@@ -960,6 +984,7 @@ function DeckGallery({ initialTab }: { initialTab: GalleryTab }) {
</p>
</div>
<div className="status">{status}</div>
<Win95WindowControls />
</header>
<nav aria-label="Gallery sections" className="gallery-tabs">
@@ -999,7 +1024,7 @@ function DeckGallery({ initialTab }: { initialTab: GalleryTab }) {
{decks.map((deck) => (
<article
aria-label={`Open ${deck.title}`}
className="deck-card gallery-entity-card panel"
className="deck-card gallery-entity-card deck-entity-card panel"
key={deck.id}
role="link"
tabIndex={0}
@@ -1012,6 +1037,7 @@ function DeckGallery({ initialTab }: { initialTab: GalleryTab }) {
}
}}
>
<span className="gallery-card-icon" aria-hidden="true" />
<span>{deck.slug}</span>
<strong>{deck.title}</strong>
<small>{getDeckStudio(deck).presetTreeId ? 'tree selected' : 'choose a node tree in editor'}</small>
@@ -1070,6 +1096,7 @@ function DeckGallery({ initialTab }: { initialTab: GalleryTab }) {
}
}}
>
<span className="gallery-card-icon" aria-hidden="true" />
<span>{tree.slug}</span>
<strong>{tree.name}</strong>
<small>
@@ -1133,7 +1160,7 @@ function DeckGallery({ initialTab }: { initialTab: GalleryTab }) {
<div className="gallery-list">
{visibleEngines.map((engine) => (
<article
className="deck-card engine-gallery-card panel"
className="deck-card engine-gallery-card engine-entity-card panel"
key={engine.id}
>
<button
@@ -1142,6 +1169,7 @@ function DeckGallery({ initialTab }: { initialTab: GalleryTab }) {
type="button"
onClick={() => setEditingEngine(engine)}
>
<span className="gallery-card-icon" aria-hidden="true" />
<span>{engine.engine_kind}</span>
<strong>{engine.name}</strong>
<small>{formatSupportedClasses(engine.supported_classes)}</small>
@@ -1222,7 +1250,7 @@ function DeckGallery({ initialTab }: { initialTab: GalleryTab }) {
{visibleIcgs.map((generator) => (
<article
aria-label={`Edit ${generator.name}`}
className="deck-card gallery-entity-card panel"
className="deck-card gallery-entity-card icg-entity-card panel"
key={generator.id}
role="button"
tabIndex={0}
@@ -1235,6 +1263,7 @@ function DeckGallery({ initialTab }: { initialTab: GalleryTab }) {
}
}}
>
<span className="gallery-card-icon" aria-hidden="true" />
<span>{generator.generator_kind}</span>
<strong>{generator.name}</strong>
<small>{formatSupportedClasses(generator.supported_classes)}</small>
@@ -1248,6 +1277,11 @@ function DeckGallery({ initialTab }: { initialTab: GalleryTab }) {
</>
) : null}
</section>
<div className="win95-statusbar" aria-hidden="true">
<span>Objects: {activeTab === 'decks' ? decks.length : activeTab === 'cas' ? presetTrees.length : activeTab === 'engines' ? visibleEngines.length : visibleIcgs.length}</span>
<span>Status: {status}</span>
<span>CA Lab Studio</span>
</div>
{creationOpen ? (
<GalleryCreationModal
activeTab={activeTab}
@@ -1352,7 +1386,10 @@ function GalleryCreationModal({
<p className="eyebrow">Create</p>
<h2>New {labels[activeTab]}</h2>
</div>
<button type="button" onClick={onClose}>Close</button>
<div className="window-action-cluster">
<Win95WindowControls />
<button type="button" onClick={onClose}>Close</button>
</div>
</header>
<div className="gallery-create-body">
{activeTab === 'decks' ? (
@@ -1583,7 +1620,10 @@ function EngineEditModal({
<p className="eyebrow">CA Engine</p>
<h2>Edit {engine.name}</h2>
</div>
<button disabled={saving} type="button" onClick={onClose}>Close</button>
<div className="window-action-cluster">
<Win95WindowControls />
<button disabled={saving} type="button" onClick={onClose}>Close</button>
</div>
</header>
<div className="gallery-create-body">
<EngineConfigFields config={config} onChange={setConfig} />
@@ -1827,7 +1867,10 @@ function IcgEditModal({
<p className="eyebrow">Initial Condition Generator</p>
<h2>Edit {generator.name}</h2>
</div>
<button disabled={saving} type="button" onClick={onClose}>Close</button>
<div className="window-action-cluster">
<Win95WindowControls />
<button disabled={saving} type="button" onClick={onClose}>Close</button>
</div>
</header>
<div className="gallery-create-body">
<IcgConfigFields config={config} onChange={setConfig} />
@@ -2596,6 +2639,7 @@ function LibraryEditor({
</div>
<div className="topbar-actions">
<div className="status">{status}</div>
<Win95WindowControls />
</div>
</header>
@@ -2617,6 +2661,7 @@ function LibraryEditor({
<button aria-label="Close preset drawer" className="icon-button drawer-close" type="button" onClick={() => setTreeDrawerOpen(false)}>
×
</button>
<Win95WindowControls />
</div>
<div className="drawer-scroll">
<div className="root-selector">
@@ -2707,6 +2752,7 @@ function LibraryEditor({
<button aria-label="Close inspector" className="icon-button drawer-close" type="button" onClick={() => setInspectorOpen(false)}>
×
</button>
<Win95WindowControls />
</div>
<div className="inspector-tabs" role="tablist" aria-label="Preset inspector sections">
<button className={inspectorTab === 'ca' ? 'active' : ''} role="tab" type="button" onClick={() => setInspectorTab('ca')}>CA</button>
@@ -3739,6 +3785,7 @@ function DeckEditor({ deckId, initialSceneId }: { deckId: string; initialSceneId
</div>
<div className="topbar-actions">
<div className="status">{status}</div>
<Win95WindowControls />
</div>
</header>
@@ -3794,6 +3841,7 @@ function DeckEditor({ deckId, initialSceneId }: { deckId: string; initialSceneId
<h2>{activeScene ? slideTitle : sceneName}</h2>
</div>
<div className="button-row">
<Win95WindowControls />
<button
aria-label="Previous slide"
className="deck-nav-button"
+244 -1
View File
@@ -360,15 +360,52 @@ label {
color: var(--text-muted);
}
.footer-start,
.footer-clock,
.win95-window-controls,
.win95-statusbar,
.gallery-card-icon {
display: none;
}
:root[data-skin="windows-95"] .app-footer {
width: 100%;
margin-top: 0;
padding: 4px 8px;
padding: 4px 6px;
border-top: 2px solid #ffffff;
background: #c0c0c0;
color: #000000;
}
:root[data-skin="windows-95"] .footer-start,
:root[data-skin="windows-95"] .footer-clock {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 28px;
padding: 3px 10px;
border: 2px solid;
border-color: #ffffff #404040 #404040 #ffffff;
background: #c0c0c0;
color: #000000;
font-weight: 700;
line-height: 1;
}
:root[data-skin="windows-95"] .footer-start::before {
content: "▦";
margin-right: 6px;
color: #008000;
font-size: 0.95rem;
}
:root[data-skin="windows-95"] .footer-clock {
min-width: 76px;
border-color: #404040 #ffffff #ffffff #404040;
font-family: var(--admin-font-mono);
font-weight: 400;
}
.app-footer p {
margin: 0;
font-family: var(--admin-font-mono);
@@ -397,6 +434,23 @@ label {
color: #000000;
}
:root[data-skin="windows-95"] .app-footer p {
flex: 1;
min-height: 28px;
padding: 6px 10px;
border: 2px solid;
border-color: #404040 #ffffff #ffffff #404040;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.window-action-cluster {
display: flex;
align-items: center;
gap: 8px;
}
.toggle-field {
display: flex;
align-items: center;
@@ -556,6 +610,34 @@ h2 {
width: 100%;
}
:root[data-skin="windows-95"] .win95-window-controls {
display: inline-grid;
grid-auto-flow: column;
gap: 2px;
margin-left: auto;
}
:root[data-skin="windows-95"] .win95-window-controls span {
display: grid;
place-items: center;
width: 18px;
height: 17px;
border: 2px solid;
border-color: #ffffff #404040 #404040 #ffffff;
background: #c0c0c0;
color: #000000;
font-family: "MS Sans Serif", Tahoma, Arial, sans-serif;
font-size: 0.72rem;
font-weight: 700;
line-height: 1;
}
:root[data-skin="windows-95"] .gallery-hero .win95-window-controls {
position: absolute;
top: 7px;
right: 7px;
}
:root[data-skin="windows-95"] .gallery-tabs {
gap: 0;
margin-top: 10px;
@@ -590,6 +672,97 @@ h2 {
transform: none;
}
:root[data-skin="windows-95"] .gallery-list {
gap: 8px;
}
:root[data-skin="windows-95"] .deck-card {
grid-template-columns: 34px minmax(0, 1fr) auto;
align-items: center;
min-height: 78px;
gap: 4px 10px;
}
:root[data-skin="windows-95"] .engine-gallery-card {
display: block;
}
:root[data-skin="windows-95"] .engine-card-main {
grid-template-columns: 34px minmax(0, 1fr);
align-items: center;
gap: 4px 10px;
border: 0;
}
:root[data-skin="windows-95"] .gallery-card-icon {
display: grid;
grid-row: 1 / span 3;
place-items: center;
width: 30px;
height: 30px;
color: #000000;
font-size: 1.35rem;
line-height: 1;
}
:root[data-skin="windows-95"] .deck-entity-card .gallery-card-icon::before {
content: "▤";
color: #000080;
}
:root[data-skin="windows-95"] .library-card .gallery-card-icon::before {
content: "▰";
color: #d4a000;
text-shadow: 1px 0 #ffff80;
}
:root[data-skin="windows-95"] .engine-entity-card .gallery-card-icon::before {
content: "⚙";
color: #404040;
}
:root[data-skin="windows-95"] .icg-entity-card .gallery-card-icon::before {
content: "✣";
color: #008000;
}
:root[data-skin="windows-95"] .deck-card > span:not(.gallery-card-icon),
:root[data-skin="windows-95"] .deck-card > strong,
:root[data-skin="windows-95"] .deck-card > small,
:root[data-skin="windows-95"] .engine-card-main > span:not(.gallery-card-icon),
:root[data-skin="windows-95"] .engine-card-main > strong,
:root[data-skin="windows-95"] .engine-card-main > small {
grid-column: 2;
}
:root[data-skin="windows-95"] .gallery-card-controls {
grid-column: 3;
grid-row: 1 / span 3;
}
:root[data-skin="windows-95"] .win95-statusbar {
display: grid;
grid-template-columns: minmax(110px, auto) minmax(0, 1fr) auto;
gap: 4px;
padding: 4px;
border: 2px solid;
border-color: #ffffff #404040 #404040 #ffffff;
background: #c0c0c0;
color: #000000;
font-family: var(--admin-font-mono);
font-size: 0.76rem;
}
:root[data-skin="windows-95"] .win95-statusbar span {
min-width: 0;
padding: 3px 6px;
border: 2px solid;
border-color: #404040 #ffffff #ffffff #404040;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
:root[data-skin="windows-95"] .deck-card strong,
:root[data-skin="windows-95"] h1,
:root[data-skin="windows-95"] h2,
@@ -2234,6 +2407,53 @@ h2 {
border-bottom: 1px solid var(--border);
}
:root[data-skin="windows-95"] .modal-backdrop {
background:
repeating-linear-gradient(45deg, rgb(0 0 0 / 0.06) 0 2px, transparent 2px 4px),
rgb(0 0 0 / 0.36);
backdrop-filter: none;
}
:root[data-skin="windows-95"] .property-modal {
border: 2px solid;
border-color: #ffffff #404040 #404040 #ffffff;
border-radius: 0;
background: #c0c0c0;
box-shadow: 4px 4px 0 rgb(0 0 0 / 0.42);
}
:root[data-skin="windows-95"] .property-modal-header {
margin: 2px;
padding: 5px 7px;
border: 0;
background: linear-gradient(90deg, #000080, #1084d0);
color: #ffffff;
}
:root[data-skin="windows-95"] .property-modal-header h2,
:root[data-skin="windows-95"] .property-modal-header .eyebrow {
color: #ffffff;
}
:root[data-skin="windows-95"] .gallery-create-body,
:root[data-skin="windows-95"] .property-modal-list,
:root[data-skin="windows-95"] .slide-metadata-body {
margin: 2px;
border: 2px solid;
border-color: #808080 #ffffff #ffffff #808080;
background: #c0c0c0;
}
:root[data-skin="windows-95"] .preset-modal-stage,
:root[data-skin="windows-95"] .property-row,
:root[data-skin="windows-95"] .slide-ca-asset,
:root[data-skin="windows-95"] .slide-metadata-grid span {
border: 2px solid;
border-color: #808080 #ffffff #ffffff #808080;
border-radius: 0;
background: #d0d0d0;
}
.property-modal-list {
display: grid;
gap: 8px;
@@ -2908,6 +3128,29 @@ h2 {
}
}
@media (max-width: 1180px) {
:root[data-skin="windows-95"] .app-footer {
align-items: center;
flex-flow: row wrap;
gap: 4px;
}
:root[data-skin="windows-95"] .app-footer p {
order: 3;
width: 100%;
flex-basis: 100%;
}
:root[data-skin="windows-95"] .skin-picker {
flex: 1;
min-width: 172px;
}
:root[data-skin="windows-95"] .footer-clock {
margin-left: auto;
}
}
@media (max-width: 560px) {
.gallery-tabs {
gap: 3px;