GameCult theme, steward-grouped project rooms, preview base override
Adds the gamecult-theme (brand palette, softer radii, Ubuntu/Montserrat/Plex Mono), makes it the default, hides the member drawer by default, defaults to bubble layout, groups rooms by org.gamecult.steward in the space nav (collapsed on phones), and lets CINNY_BASE set Vite's base for a sub-path preview build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
1e6412ed5b
commit
a588ab82b3
13 changed files with 471 additions and 28 deletions
|
|
@ -1,3 +1,3 @@
|
|||
export default {
|
||||
base: '/',
|
||||
base: process.env.CINNY_BASE ?? '/',
|
||||
};
|
||||
|
|
|
|||
11
package.json
11
package.json
|
|
@ -18,7 +18,8 @@
|
|||
"typecheck": "tsc --noEmit",
|
||||
"prepare": "husky install",
|
||||
"commit": "git-cz",
|
||||
"bump": "node scripts/update-version.js"
|
||||
"bump": "node scripts/update-version.js",
|
||||
"test": "vitest run"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,jsx}": "eslint",
|
||||
|
|
@ -87,7 +88,10 @@
|
|||
"slate-dom": "0.123.0",
|
||||
"slate-history": "0.113.1",
|
||||
"slate-react": "0.123.0",
|
||||
"ua-parser-js": "1.0.35"
|
||||
"ua-parser-js": "1.0.35",
|
||||
"@fontsource/ubuntu": "5.2.5",
|
||||
"@fontsource/montserrat": "5.2.5",
|
||||
"@fontsource/ibm-plex-mono": "5.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@element-hq/element-call-embedded": "0.20.1",
|
||||
|
|
@ -123,6 +127,7 @@
|
|||
"vite": "5.4.19",
|
||||
"vite-plugin-pwa": "0.20.5",
|
||||
"vite-plugin-static-copy": "1.0.4",
|
||||
"vite-plugin-top-level-await": "1.4.4"
|
||||
"vite-plugin-top-level-await": "1.4.4",
|
||||
"vitest": "1.6.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
63
src/app/features/steward-groups/stewardGroups.test.ts
Normal file
63
src/app/features/steward-groups/stewardGroups.test.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { groupByStewards, parseSteward, StewardEntry } from './stewardGroups';
|
||||
|
||||
const room = (roomId: string, parentId = '!projects') => ({ roomId, parentId });
|
||||
const space = (roomId: string) => ({ roomId, space: true as const });
|
||||
|
||||
const base = {
|
||||
spaceId: '!top',
|
||||
isGroupOpen: () => true,
|
||||
keepVisible: () => false,
|
||||
};
|
||||
|
||||
const names = (out: StewardEntry<{ roomId: string }>[]) =>
|
||||
out.map((e) => (e.type === 'group' ? `G:${e.label}:${e.count}` : e.item.roomId));
|
||||
|
||||
describe('groupByStewards', () => {
|
||||
const stewards: Record<string, string> = { '!a': 'Void', '!b': 'Eureka', '!c': 'Void' };
|
||||
const stewardOf = (id: string) => stewards[id];
|
||||
|
||||
it('groups by steward, sorted by name, with unstewarded rooms first', () => {
|
||||
const out = groupByStewards([room('!a'), room('!x'), room('!b'), room('!c')], {
|
||||
...base,
|
||||
stewardOf,
|
||||
});
|
||||
expect(names(out)).toEqual(['!x', 'G:Eureka:1', '!b', 'G:Void:2', '!a', '!c']);
|
||||
});
|
||||
|
||||
it('collapsed groups hide rooms except kept-visible ones', () => {
|
||||
const out = groupByStewards([room('!a'), room('!b'), room('!c')], {
|
||||
...base,
|
||||
stewardOf,
|
||||
isGroupOpen: () => false,
|
||||
keepVisible: (id) => id === '!c',
|
||||
});
|
||||
expect(names(out)).toEqual(['G:Eureka:1', 'G:Void:2', '!c']);
|
||||
});
|
||||
|
||||
it('degrades to the original order when no room has steward state', () => {
|
||||
const out = groupByStewards([room('!1'), space('!s'), room('!2', '!s')], {
|
||||
...base,
|
||||
stewardOf: () => undefined,
|
||||
});
|
||||
expect(names(out)).toEqual(['!1', '!s', '!2']);
|
||||
});
|
||||
|
||||
it('does not merge runs separated by a space header', () => {
|
||||
const out = groupByStewards([room('!a'), space('!s'), room('!c', '!s')], {
|
||||
...base,
|
||||
stewardOf,
|
||||
});
|
||||
expect(out.filter((e) => e.type === 'group')).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseSteward', () => {
|
||||
it('accepts a string and rejects junk', () => {
|
||||
expect(parseSteward({ steward: ' Void ' })).toBe('Void');
|
||||
expect(parseSteward({ steward: 3 })).toBeUndefined();
|
||||
expect(parseSteward({ steward: ' ' })).toBeUndefined();
|
||||
expect(parseSteward(undefined)).toBeUndefined();
|
||||
expect(parseSteward(null)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
91
src/app/features/steward-groups/stewardGroups.ts
Normal file
91
src/app/features/steward-groups/stewardGroups.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
/**
|
||||
* Splits runs of rooms that carry an `org.gamecult.steward` state event into
|
||||
* collapsible per-steward groups. Pure: the caller supplies the lookups, so the
|
||||
* grouping is testable without a Matrix client.
|
||||
*/
|
||||
export const STEWARD_EVENT_TYPE = 'org.gamecult.steward';
|
||||
|
||||
export type HierarchyLike = { roomId: string; parentId?: string; space?: true };
|
||||
|
||||
export type StewardEntry<T extends HierarchyLike> =
|
||||
| { type: 'item'; item: T }
|
||||
| { type: 'group'; id: string; label: string; open: boolean; count: number };
|
||||
|
||||
export const makeStewardGroupId = (spaceId: string, parentId: string, steward: string) =>
|
||||
`${spaceId}|${parentId}|steward:${steward}`;
|
||||
|
||||
export type StewardGroupOptions = {
|
||||
spaceId: string;
|
||||
/** steward name of a room, or undefined when the room has no (valid) steward state */
|
||||
stewardOf: (roomId: string) => string | undefined;
|
||||
isGroupOpen: (groupId: string) => boolean;
|
||||
/** rooms that stay visible inside a collapsed group (selected, unread, in a call) */
|
||||
keepVisible: (roomId: string) => boolean;
|
||||
};
|
||||
|
||||
const byLabel = (a: string, b: string) =>
|
||||
a.localeCompare(b, undefined, { sensitivity: 'base' }) || a.localeCompare(b);
|
||||
|
||||
export const groupByStewards = <T extends HierarchyLike>(
|
||||
items: T[],
|
||||
{ spaceId, stewardOf, isGroupOpen, keepVisible }: StewardGroupOptions
|
||||
): StewardEntry<T>[] => {
|
||||
const out: StewardEntry<T>[] = [];
|
||||
|
||||
const flush = (run: T[]) => {
|
||||
if (run.length === 0) return;
|
||||
const groups = new Map<string, T[]>();
|
||||
const loose: T[] = [];
|
||||
run.forEach((item) => {
|
||||
const steward = stewardOf(item.roomId);
|
||||
if (steward === undefined) {
|
||||
loose.push(item);
|
||||
return;
|
||||
}
|
||||
const list = groups.get(steward);
|
||||
if (list) list.push(item);
|
||||
else groups.set(steward, [item]);
|
||||
});
|
||||
|
||||
// No steward state anywhere in this run: leave the list exactly as Cinny built it.
|
||||
if (groups.size === 0) {
|
||||
run.forEach((item) => out.push({ type: 'item', item }));
|
||||
return;
|
||||
}
|
||||
|
||||
loose.forEach((item) => out.push({ type: 'item', item }));
|
||||
Array.from(groups.keys())
|
||||
.sort(byLabel)
|
||||
.forEach((steward) => {
|
||||
const rooms = groups.get(steward) ?? [];
|
||||
const id = makeStewardGroupId(spaceId, rooms[0].parentId ?? spaceId, steward);
|
||||
const open = isGroupOpen(id);
|
||||
out.push({ type: 'group', id, label: steward, open, count: rooms.length });
|
||||
rooms
|
||||
.filter((item) => open || keepVisible(item.roomId))
|
||||
.forEach((item) => out.push({ type: 'item', item }));
|
||||
});
|
||||
};
|
||||
|
||||
let run: T[] = [];
|
||||
items.forEach((item) => {
|
||||
if (item.space) {
|
||||
flush(run);
|
||||
run = [];
|
||||
out.push({ type: 'item', item });
|
||||
return;
|
||||
}
|
||||
run.push(item);
|
||||
});
|
||||
flush(run);
|
||||
return out;
|
||||
};
|
||||
|
||||
/** Reads `steward` from raw state event content; anything unusable is "no steward". */
|
||||
export const parseSteward = (content: unknown): string | undefined => {
|
||||
if (typeof content !== 'object' || content === null) return undefined;
|
||||
const { steward } = content as { steward?: unknown };
|
||||
if (typeof steward !== 'string') return undefined;
|
||||
const trimmed = steward.trim();
|
||||
return trimmed === '' ? undefined : trimmed;
|
||||
};
|
||||
71
src/app/features/steward-groups/useStewardGroups.ts
Normal file
71
src/app/features/steward-groups/useStewardGroups.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { MatrixClient, MatrixEvent, RoomStateEvent } from 'matrix-js-sdk';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
import { useAtom } from 'jotai';
|
||||
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
|
||||
import {
|
||||
groupByStewards,
|
||||
HierarchyLike,
|
||||
parseSteward,
|
||||
StewardEntry,
|
||||
STEWARD_EVENT_TYPE,
|
||||
} from './stewardGroups';
|
||||
|
||||
// Explicit open/closed choices by group id. Absent = default: closed on phones, open elsewhere.
|
||||
const stewardGroupChoicesAtom = atomWithStorage<Record<string, boolean>>(
|
||||
'gamecultStewardGroupOpen',
|
||||
{}
|
||||
);
|
||||
|
||||
type Options<T> = {
|
||||
mx: MatrixClient;
|
||||
spaceId: string;
|
||||
items: T[];
|
||||
keepVisible: (roomId: string) => boolean;
|
||||
};
|
||||
|
||||
export const useStewardGroups = <T extends HierarchyLike>({
|
||||
mx,
|
||||
spaceId,
|
||||
items,
|
||||
keepVisible,
|
||||
}: Options<T>) => {
|
||||
const mobile = useScreenSizeContext() === ScreenSize.Mobile;
|
||||
const [choices, setChoices] = useAtom(stewardGroupChoicesAtom);
|
||||
const [stateVersion, setStateVersion] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const onState = (mEvent: MatrixEvent) => {
|
||||
if (mEvent.getType() === STEWARD_EVENT_TYPE) setStateVersion((v) => v + 1);
|
||||
};
|
||||
mx.on(RoomStateEvent.Events, onState);
|
||||
return () => {
|
||||
mx.removeListener(RoomStateEvent.Events, onState);
|
||||
};
|
||||
}, [mx]);
|
||||
|
||||
const stewardOf = useCallback(
|
||||
(roomId: string) =>
|
||||
// stateVersion is read so a late-arriving steward event re-derives this lookup
|
||||
stateVersion >= 0
|
||||
? parseSteward(
|
||||
mx.getRoom(roomId)?.currentState.getStateEvents(STEWARD_EVENT_TYPE, '')?.getContent()
|
||||
)
|
||||
: undefined,
|
||||
[mx, stateVersion]
|
||||
);
|
||||
|
||||
const isGroupOpen = useCallback((id: string) => choices[id] ?? !mobile, [choices, mobile]);
|
||||
|
||||
const entries: StewardEntry<T>[] = useMemo(
|
||||
() => groupByStewards(items, { spaceId, stewardOf, isGroupOpen, keepVisible }),
|
||||
[items, spaceId, stewardOf, isGroupOpen, keepVisible]
|
||||
);
|
||||
|
||||
const toggleGroup = useCallback(
|
||||
(id: string) => setChoices((prev) => ({ ...prev, [id]: !(prev[id] ?? !mobile) })),
|
||||
[setChoices, mobile]
|
||||
);
|
||||
|
||||
return { entries, toggleGroup };
|
||||
};
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { lightTheme } from 'folds';
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { onDarkFontWeight, onLightFontWeight } from '../../config.css';
|
||||
import { butterTheme, darkTheme, silverTheme } from '../../colors.css';
|
||||
import { butterTheme, darkTheme, gamecultTheme, silverTheme } from '../../colors.css';
|
||||
import '../../gamecult.css';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
|
||||
|
|
@ -38,8 +39,14 @@ export const ButterTheme: Theme = {
|
|||
classNames: ['butter-theme', butterTheme, onDarkFontWeight, 'prism-dark'],
|
||||
};
|
||||
|
||||
export const GameCultTheme: Theme = {
|
||||
id: 'gamecult-theme',
|
||||
kind: ThemeKind.Dark,
|
||||
classNames: ['gamecult-theme', gamecultTheme, onDarkFontWeight, 'prism-dark'],
|
||||
};
|
||||
|
||||
export const useThemes = (): Theme[] => {
|
||||
const themes: Theme[] = useMemo(() => [LightTheme, SilverTheme, DarkTheme, ButterTheme], []);
|
||||
const themes: Theme[] = useMemo(() => [GameCultTheme, LightTheme, SilverTheme, DarkTheme, ButterTheme], []);
|
||||
|
||||
return themes;
|
||||
};
|
||||
|
|
@ -47,6 +54,7 @@ export const useThemes = (): Theme[] => {
|
|||
export const useThemeNames = (): Record<string, string> =>
|
||||
useMemo(
|
||||
() => ({
|
||||
[GameCultTheme.id]: 'GameCult',
|
||||
[LightTheme.id]: 'Light',
|
||||
[SilverTheme.id]: 'Silver',
|
||||
[DarkTheme.id]: 'Dark',
|
||||
|
|
@ -84,15 +92,15 @@ export const useActiveTheme = (): Theme => {
|
|||
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
|
||||
|
||||
if (!systemTheme) {
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? GameCultTheme;
|
||||
|
||||
return selectedTheme;
|
||||
}
|
||||
|
||||
const selectedTheme =
|
||||
systemThemeKind === ThemeKind.Dark
|
||||
? themes.find((theme) => theme.id === darkThemeId) ?? DarkTheme
|
||||
: themes.find((theme) => theme.id === lightThemeId) ?? LightTheme;
|
||||
? themes.find((theme) => theme.id === darkThemeId) ?? GameCultTheme
|
||||
: themes.find((theme) => theme.id === lightThemeId) ?? GameCultTheme;
|
||||
|
||||
return selectedTheme;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,29 +1,20 @@
|
|||
import React, { ReactNode, useEffect } from 'react';
|
||||
import { configClass, varsClass } from 'folds';
|
||||
import {
|
||||
DarkTheme,
|
||||
LightTheme,
|
||||
GameCultTheme,
|
||||
ThemeContextProvider,
|
||||
ThemeKind,
|
||||
useActiveTheme,
|
||||
useSystemThemeKind,
|
||||
} from '../hooks/useTheme';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
|
||||
export function UnAuthRouteThemeManager() {
|
||||
const systemThemeKind = useSystemThemeKind();
|
||||
|
||||
useEffect(() => {
|
||||
document.body.className = '';
|
||||
document.body.classList.add(configClass, varsClass);
|
||||
if (systemThemeKind === ThemeKind.Dark) {
|
||||
document.body.classList.add(...DarkTheme.classNames);
|
||||
}
|
||||
if (systemThemeKind === ThemeKind.Light) {
|
||||
document.body.classList.add(...LightTheme.classNames);
|
||||
}
|
||||
}, [systemThemeKind]);
|
||||
// The GameCult identity is single-theme: signed-out pages ignore the system scheme.
|
||||
document.body.classList.add(...GameCultTheme.classNames);
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
|
|||
import { makeNavCategoryId } from '../../../state/closedNavCategories';
|
||||
import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
|
||||
import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
|
||||
import { useStewardGroups } from '../../../features/steward-groups/useStewardGroups';
|
||||
import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper';
|
||||
import { useRoomName } from '../../../hooks/useRoomMeta';
|
||||
import { useSpaceJoinedHierarchy } from '../../../hooks/useSpaceHierarchy';
|
||||
|
|
@ -425,8 +426,18 @@ export function Space() {
|
|||
)
|
||||
);
|
||||
|
||||
const { entries, toggleGroup } = useStewardGroups({
|
||||
mx,
|
||||
spaceId: space.roomId,
|
||||
items: hierarchy,
|
||||
keepVisible: useCallback(
|
||||
(rId: string) => roomToUnread.has(rId) || rId === selectedRoomId || callEmbed?.roomId === rId,
|
||||
[roomToUnread, selectedRoomId, callEmbed]
|
||||
),
|
||||
});
|
||||
|
||||
const virtualizer = useVirtualizer({
|
||||
count: hierarchy.length,
|
||||
count: entries.length,
|
||||
getScrollElement: () => scrollRef.current,
|
||||
estimateSize: () => 0,
|
||||
overscan: 10,
|
||||
|
|
@ -491,7 +502,27 @@ export function Space() {
|
|||
}}
|
||||
>
|
||||
{virtualizer.getVirtualItems().map((vItem) => {
|
||||
const { roomId } = hierarchy[vItem.index] ?? {};
|
||||
const entry = entries[vItem.index];
|
||||
if (!entry) return null;
|
||||
if (entry.type === 'group') {
|
||||
return (
|
||||
<VirtualTile
|
||||
virtualItem={vItem}
|
||||
key={vItem.index}
|
||||
ref={virtualizer.measureElement}
|
||||
>
|
||||
<NavCategoryHeader>
|
||||
<RoomNavCategoryButton
|
||||
onClick={() => toggleGroup(entry.id)}
|
||||
closed={!entry.open}
|
||||
>
|
||||
{entry.label} · {entry.count}
|
||||
</RoomNavCategoryButton>
|
||||
</NavCategoryHeader>
|
||||
</VirtualTile>
|
||||
);
|
||||
}
|
||||
const { roomId } = entry.item;
|
||||
const room = mx.getRoom(roomId);
|
||||
if (!room) return null;
|
||||
|
||||
|
|
|
|||
|
|
@ -50,8 +50,8 @@ export interface Settings {
|
|||
}
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
themeId: undefined,
|
||||
useSystemTheme: true,
|
||||
themeId: 'gamecult-theme',
|
||||
useSystemTheme: false,
|
||||
lightThemeId: undefined,
|
||||
darkThemeId: undefined,
|
||||
monochromeMode: false,
|
||||
|
|
@ -61,10 +61,10 @@ const defaultSettings: Settings = {
|
|||
pageZoom: 100,
|
||||
hideActivity: false,
|
||||
|
||||
isPeopleDrawer: true,
|
||||
isPeopleDrawer: false,
|
||||
memberSortFilterIndex: 0,
|
||||
enterForNewline: false,
|
||||
messageLayout: 0,
|
||||
messageLayout: 2,
|
||||
messageSpacing: '400',
|
||||
hideMembershipEvents: false,
|
||||
hideNickAvatarEvents: true,
|
||||
|
|
|
|||
|
|
@ -236,3 +236,95 @@ export const butterTheme = createTheme(color, {
|
|||
OnContainer: '#F2EED3',
|
||||
},
|
||||
});
|
||||
|
||||
// GameCult identity (gamecult-site quartz.config.ts): single dark theme on #07111a,
|
||||
// orange #ff8a2a accent, sky #59b7ff links. The brand defines no status colours, so
|
||||
// Success/Warning/Critical are invented here and kept distinct from the orange accent.
|
||||
export const gamecultTheme = createTheme(color, {
|
||||
Background: {
|
||||
Container: '#07111a',
|
||||
ContainerHover: '#0d1a26',
|
||||
ContainerActive: '#122130',
|
||||
ContainerLine: '#16212c',
|
||||
OnContainer: '#b7c7d9',
|
||||
},
|
||||
Surface: {
|
||||
Container: '#09141f',
|
||||
ContainerHover: '#0f1c29',
|
||||
ContainerActive: '#152433',
|
||||
ContainerLine: '#16212c',
|
||||
OnContainer: '#b7c7d9',
|
||||
},
|
||||
SurfaceVariant: {
|
||||
Container: '#0d1a26',
|
||||
ContainerHover: '#122130',
|
||||
ContainerActive: '#16273a',
|
||||
ContainerLine: '#1b2a38',
|
||||
OnContainer: '#b7c7d9',
|
||||
},
|
||||
Primary: {
|
||||
Main: '#ff8a2a',
|
||||
MainHover: '#ff9640',
|
||||
MainActive: '#ffa155',
|
||||
MainLine: '#ffab66',
|
||||
OnMain: '#2a1300',
|
||||
Container: '#2b1a0e',
|
||||
ContainerHover: '#35200f',
|
||||
ContainerActive: '#402611',
|
||||
ContainerLine: '#4d2d13',
|
||||
OnContainer: '#ffd3ab',
|
||||
},
|
||||
Secondary: {
|
||||
Main: '#eef5ff',
|
||||
MainHover: '#dbe7f6',
|
||||
MainActive: '#cddced',
|
||||
MainLine: '#bfd0e4',
|
||||
OnMain: '#07111a',
|
||||
Container: '#16212c',
|
||||
ContainerHover: '#1c2a38',
|
||||
ContainerActive: '#233342',
|
||||
ContainerLine: '#2a3b4c',
|
||||
OnContainer: '#eef5ff',
|
||||
},
|
||||
Success: {
|
||||
Main: '#5fd0a4',
|
||||
MainHover: '#72d8b0',
|
||||
MainActive: '#84deba',
|
||||
MainLine: '#93e3c2',
|
||||
OnMain: '#042014',
|
||||
Container: '#0c2a21',
|
||||
ContainerHover: '#10342a',
|
||||
ContainerActive: '#143e32',
|
||||
ContainerLine: '#19493a',
|
||||
OnContainer: '#c4f1de',
|
||||
},
|
||||
Warning: {
|
||||
Main: '#f2d04b',
|
||||
MainHover: '#f5d963',
|
||||
MainActive: '#f7e078',
|
||||
MainLine: '#f9e68b',
|
||||
OnMain: '#2a2100',
|
||||
Container: '#2a2410',
|
||||
ContainerHover: '#352d13',
|
||||
ContainerActive: '#403616',
|
||||
ContainerLine: '#4d411a',
|
||||
OnContainer: '#f8ebb0',
|
||||
},
|
||||
Critical: {
|
||||
Main: '#ff7a90',
|
||||
MainHover: '#ff8fa1',
|
||||
MainActive: '#ffa0af',
|
||||
MainLine: '#ffb0bc',
|
||||
OnMain: '#3a0710',
|
||||
Container: '#2e1118',
|
||||
ContainerHover: '#391520',
|
||||
ContainerActive: '#441a27',
|
||||
ContainerLine: '#51202e',
|
||||
OnContainer: '#ffd0d8',
|
||||
},
|
||||
Other: {
|
||||
FocusRing: 'rgba(89, 183, 255, 0.55)',
|
||||
Shadow: 'rgba(0, 0, 0, 0.6)',
|
||||
Overlay: 'rgba(3, 7, 13, 0.8)',
|
||||
},
|
||||
});
|
||||
|
|
|
|||
20
src/gamecult.css.ts
Normal file
20
src/gamecult.css.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { globalStyle } from '@vanilla-extract/css';
|
||||
import { config } from 'folds';
|
||||
|
||||
// Softer, rounder shape language and an airier line height. Scoped to the GameCult
|
||||
// theme class so the stock themes keep folds' defaults.
|
||||
const scope = 'body.gamecult-theme';
|
||||
|
||||
globalStyle(scope, {
|
||||
vars: {
|
||||
[config.radii.R300]: '10px',
|
||||
[config.radii.R400]: '16px',
|
||||
[config.radii.R500]: '22px',
|
||||
[config.lineHeight.T300]: '1.6',
|
||||
[config.lineHeight.T400]: '1.7',
|
||||
[config.lineHeight.T500]: '1.7',
|
||||
[config.lineHeight.B400]: '1.6',
|
||||
[config.lineHeight.B500]: '1.6',
|
||||
[config.lineHeight.D400]: '1.7',
|
||||
},
|
||||
});
|
||||
|
|
@ -135,3 +135,67 @@ li p::before {
|
|||
content: '';
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* GameCult identity: Ubuntu 300 prose, Montserrat thin titles, IBM Plex Mono labels. */
|
||||
.gamecult-theme {
|
||||
--tc-link: #59b7ff;
|
||||
|
||||
--mx-uc-1: hsl(208, 100%, 72%);
|
||||
--mx-uc-2: hsl(280, 100%, 78%);
|
||||
--mx-uc-3: hsl(163, 70%, 65%);
|
||||
--mx-uc-4: hsl(343, 100%, 76%);
|
||||
--mx-uc-5: hsl(28, 100%, 66%);
|
||||
--mx-uc-6: hsl(181, 70%, 62%);
|
||||
--mx-uc-7: hsl(243, 100%, 80%);
|
||||
--mx-uc-8: hsl(94, 70%, 70%);
|
||||
|
||||
--font-secondary: 'Ubuntu', var(--font-emoji), sans-serif;
|
||||
--font-title: 'Montserrat', 'Ubuntu', sans-serif;
|
||||
--font-mono: 'IBM Plex Mono', monospace;
|
||||
--gamecult-rule: rgba(148, 163, 184, 0.22);
|
||||
--gamecult-wash: 0.5;
|
||||
|
||||
background-color: #07111a;
|
||||
background-image: radial-gradient(
|
||||
circle at 78% 14%,
|
||||
rgb(255 138 42 / calc(0.18 * var(--gamecult-wash))),
|
||||
transparent 18%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 18% 10%,
|
||||
rgb(109 96 255 / calc(0.16 * var(--gamecult-wash))),
|
||||
transparent 24%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 52% 0%,
|
||||
rgb(89 183 255 / calc(0.14 * var(--gamecult-wash))),
|
||||
transparent 28%
|
||||
),
|
||||
linear-gradient(180deg, #03070d 0%, #07111a 44%, #09141f 100%);
|
||||
background-attachment: fixed;
|
||||
color-scheme: dark;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.gamecult-theme b,
|
||||
.gamecult-theme strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.gamecult-theme h1,
|
||||
.gamecult-theme h2,
|
||||
.gamecult-theme h3 {
|
||||
font-family: var(--font-title);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.gamecult-theme code,
|
||||
.gamecult-theme pre {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
/* Quiet secondary metadata: timestamps, edited marks and the like sit back. */
|
||||
.gamecult-theme time {
|
||||
opacity: 0.55;
|
||||
font-size: 0.78em;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@ import React from 'react';
|
|||
import { createRoot } from 'react-dom/client';
|
||||
import { enableMapSet } from 'immer';
|
||||
import '@fontsource/inter/variable.css';
|
||||
import '@fontsource/ubuntu/300.css';
|
||||
import '@fontsource/ubuntu/400.css';
|
||||
import '@fontsource/ubuntu/500.css';
|
||||
import '@fontsource/ubuntu/700.css';
|
||||
import '@fontsource/montserrat/200.css';
|
||||
import '@fontsource/montserrat/300.css';
|
||||
import '@fontsource/ibm-plex-mono/400.css';
|
||||
import 'folds/dist/style.css';
|
||||
import { configClass, varsClass } from 'folds';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue