Improving Win95 styling
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user