update: hooks, web

This commit is contained in:
Vick Scarlet
2026-08-16 20:06:16 +08:00
committed by 神戸小鳥
parent 2f91709d1e
commit 322f775796
67 changed files with 2339 additions and 2243 deletions
+1 -1
View File
@@ -5,7 +5,7 @@
"author": "Vick Scarlet <vick@syaro.io>",
"scripts": {
"start": "bun run src/index.ts",
"lint": "eslint .",
"lint": "bunx --bun eslint .",
"test": "bun test"
},
"dependencies": {
+1 -29
View File
@@ -1,32 +1,4 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"types": ["bun"],
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"paths": {
"@/*": ["./src/*"],
},
// Some stricter flags (disabled by default)
"noUnusedLocals": true,
"noUnusedParameters": true,
"noPropertyAccessFromIndexSignature": false,
"noImplicitAny": true
},
"extends": "../../tsconfig.json",
"include": ["**/*"]
}
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="description" content="人生重开模拟器 - 这垃圾人生一秒也不想待了"/>
<meta name="keywords" content="人生重开模拟器 liferestart life restart remake 人生重来"/>
<meta name="color-scheme" content="light dark" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>人生重开模拟器</title>
</head>
<body>
<div id="remake"></div>
<script type="module" src="src/index.tsx"></script>
</body>
</html>
+17 -10
View File
@@ -4,24 +4,31 @@
"version": "3.0.0",
"author": "Vick Scarlet <vick@syaro.io>",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"lint": "eslint",
"test": "vitest"
"dev": "bunx --bun vite",
"build": "bunx --bun vite build",
"preview": "bunx --bun vite preview",
"lint": "bunx --bun eslint",
"test": "bunx --bun vitest"
},
"dependencies": {
"@remake/core": "workspace:*",
"@remake/data": "workspace:*",
"preact": "^10.26.9"
"@remake/hooks": "workspace:*",
"@remake/vitex": "workspace:*",
"classnames": "^2.5.1",
"jotai": "^2.20.2",
"react": "^19.2.8",
"react-dom": "^19.2.8"
},
"devDependencies": {
"@preact/preset-vite": "^2.10.2",
"eslint-config-preact": "^2.0.0",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.0.5",
"eslint-config-react": "^1.1.7",
"jotai-devtools": "^0.14.0",
"vite": "^8.1.3",
"vitest": "^4.1.10"
},
"eslintConfig": {
"extends": "preact"
"extends": "react"
}
}
+12
View File
@@ -0,0 +1,12 @@
.screen {
min-width: 32rem;
max-width: calc(100dvw - 2rem);
position: relative;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> * { width: 100%; }
}
+35
View File
@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react'
import { useInit } from '@/hooks/storage'
import { useStep, Step } from '@remake/hooks'
import Home from '@/containers/Home'
import Pick from '@/containers/TalentPick'
import Alloc from '@/containers/Alloc'
import Play from '@/containers/Play'
import Summary from '@/containers/Summary'
import { Loading } from './containers/Loading'
import './Game.css'
export function Game() {
const [inited, init] = useInit()
const step = useStep()
useEffect(() => {
if (!inited) init()
}, [inited])
if (!inited) return <Loading />
switch (step) {
case Step.Idle:
return <Home />
case Step.Mode:
case Step.Pick:
return <Pick />
case Step.Alloc:
return <Alloc />
case Step.Play:
return <Play />
case Step.Summary:
return <Summary />
default:
return null
}
}
export default Game
+50
View File
@@ -0,0 +1,50 @@
ul.replaced {
display: flex;
flex-direction: column;
gap: 0.5rem;
li {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 0;
.talent {
width: 100%;
}
}
li:not(:first-child) {
position: relative;
&::before {
content: '';
position: absolute;
display: block;
box-sizing: border-box;
border: 0.0625rem solid var(--base-text-color);
border-left: none;
right: 0;
top: -1rem;
margin-right: -1rem;
width: 0.625rem;
height: 1.5rem;
}
&::after {
content: '';
position: absolute;
display: block;
box-sizing: border-box;
border: 0.0625rem solid var(--base-text-color);
border-right: none;
border-bottom: none;
right: 0;
top: 0.5rem;
margin-right: -0.6875rem;
margin-top: -0.03125rem;
width: 0.375rem;
height: 0.375rem;
transform: rotate(-45deg);
transform-origin: 0 0;
}
}
}
+29
View File
@@ -0,0 +1,29 @@
import type { Talent } from '@remake/data/talent'
import TalentComponent from './Talent'
import talents from '@remake/data/talent'
import './Replaced.css'
export interface TalentProps {
id: Talent['id']
chains?: Talent['id'][]
}
export function Replaced({ id, chains }: TalentProps) {
return (
<ul className="replaced">
<li>
<TalentComponent id={id} selected={true} />
</li>
{chains?.map(id => {
const talent = talents.get(id)
if (!talent) return null
return (
<li key={id}>
<TalentComponent id={id} selected={false} />
</li>
)
})}
</ul>
)
}
export default Replaced
+41
View File
@@ -0,0 +1,41 @@
.talent {
--color: var(--grade-color);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 1rem;
line-height: 2rem;
gap: 1rem;
color: var(--color);
background: var(--background, var(--base-background-color));
cursor: pointer;
user-select: none;
border: 0.0625rem solid var(--grade-color);
transition: all 0.2s ease-in-out;
&.talent-grade-0 { --grade-color: var(--color-grade-0); }
&.talent-grade-1 { --grade-color: var(--color-grade-1); }
&.talent-grade-2 { --grade-color: var(--color-grade-2); }
&.talent-grade-3 { --grade-color: var(--color-grade-3); }
&.selected {
--color: var(--base-background-color);
--background: var(--grade-color);
&:hover { --background: oklch(from var(--grade-color) calc(l + 0.1) c h); }
}
&:hover { --background: oklch(from var(--grade-color) calc(l + 0.5) c h); }
.talent-description {
color: oklch(from var(--color) calc(l + 0.1) c h);
font-size: 0.6rem;
align-self: flex-end;
user-select: none;
&::before { content: '['; margin-right: 0.125rem; }
&::after { content: ']'; margin-left: 0.125rem; }
&::before, &::after { color: var(--color); }
}
}
html[data-theme='dark'] {
.talent{
&:hover {--background: oklch(from var(--grade-color) calc(l - 0.5) c h);}
&.selected:hover { --background: oklch(from var(--grade-color) calc(l - 0.1) c h);}
.talent-description { color: oklch(from var(--color) calc(l - 0.2) c h); }
}
}
+22
View File
@@ -0,0 +1,22 @@
import type { Talent } from '@remake/data/talent'
import talents from '@remake/data/talent'
import cx from 'classnames'
import './Talent.css'
export interface TalentProps {
id: Talent['id']
selected: boolean
}
export function TalentComponent({ id, selected }: TalentProps) {
const talent = talents.get(id)
if (!talent) return null
const grade = talent?.grade ?? 0
return (
<div className={cx('talent', `talent-grade-${grade}`, { selected })}>
<span className="talent-name">{talent.name}</span>
<span className="talent-description">{talent.description}</span>
</div>
)
}
export default TalentComponent
+46
View File
@@ -0,0 +1,46 @@
.theme-toggle-btn {
background: transparent;
border: none;
cursor: pointer;
padding: 0.25rem;
border-radius: 50%;
position: fixed;
top: 0.5rem;
right: 0.5rem;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
-webkit-tap-highlight-color: transparent;
transition: background-color 0.3s;
--hover-color: rgba(0, 0, 0, 0.05);
&:hover { background-color: var(--hover-color);}
}
.theme-svg {
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: center;
.sun-center, .sun-beams, .mask-cutter-group {
transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: center;
}
.mask-cutter-group { transform: translate(0.75rem, -0.75rem); }
.sun-center { fill: oklch(from var(--base-text-color) calc(l + 0.2) c h); }
.sun-beams { stroke: oklch(from var(--base-text-color) calc(l + 0.2) c h); }
}
html[data-theme='dark'] {
.theme-toggle-btn { --hover-color: rgba(255, 255, 255, 0.1); }
.mask-cutter-group { transform: translate(0, 0); }
.theme-svg { transform: rotate(375deg); }
.sun-center {
transform: scale(1.5);
fill: #fbbf24;
}
.sun-beams {
opacity: 0;
transform: scale(0.6);
stroke: #fbbf24;
}
}
+66
View File
@@ -0,0 +1,66 @@
import { useState, useEffect } from 'react'
import './ThemeToggle.css'
export function ThemeToggle() {
const [isDark, setIsDark] = useState(
() => matchMedia('(prefers-color-scheme: dark)').matches,
)
useEffect(() => {
document.documentElement.dataset.theme = isDark ? 'dark' : 'light'
}, [isDark])
return (
<button
className="theme-toggle-btn"
onClick={() => setIsDark(!isDark)}
aria-label="Toggle theme"
>
<svg
className="theme-svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<defs>
<mask id="ios-fail-proof-mask">
<rect
x="0"
y="0"
width="24"
height="24"
fill="#ffffff"
/>
<g className="mask-cutter-group">
<circle cx="18" cy="5" r="7" fill="#000000" />
</g>
</mask>
</defs>
<circle
className="sun-center"
cx="12"
cy="12"
r="5"
mask="url(#ios-fail-proof-mask)"
/>
<g className="sun-beams">
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</g>
</svg>
</button>
)
}
export default ThemeToggle
+60
View File
@@ -0,0 +1,60 @@
import type { Config } from '@remake/hooks'
import type { TalentGrade } from '@remake/data/talent'
function gadm(grade: TalentGrade, value: number) {
return new Map([[grade, { mode: 'multiply', value }]])
}
export const AdditonMap = {
times: [
{ value: 0, additions: new Map() },
{ value: 10, additions: gadm(2, 2) },
{ value: 30, additions: gadm(2, 3) },
{ value: 50, additions: gadm(2, 4) },
{ value: 70, additions: gadm(2, 5) },
{ value: 100, additions: gadm(2, 6) },
],
achievements: [
{ value: 0, additions: new Map() },
{ value: 10, additions: gadm(3, 2) },
{ value: 30, additions: gadm(3, 3) },
{ value: 50, additions: gadm(3, 4) },
{ value: 70, additions: gadm(3, 5) },
{ value: 100, additions: gadm(3, 6) },
],
}
export const BasePoints = 20
export const config: Config = {
mode: 10,
pick: 3,
points: BasePoints,
allocate: 10,
pull: {
count: 10,
rate: {
base: new Map([
[0, 889],
[1, 100],
[2, 10],
[3, 1],
]),
additions: {
times: value => {
for (const item of AdditonMap.times.reverse())
if (value >= item.value) return item.additions
return new Map()
},
achievements: value => {
const size = value.size
for (const item of AdditonMap.achievements.reverse())
if (size >= item.value) return item.additions
return new Map()
},
},
},
},
}
export default config
+112
View File
@@ -0,0 +1,112 @@
.screen.point-allocation {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
width: 100%;
.allocation-input {
display: flex;
gap: 0;
border: 0.0625rem solid var(--base-text-color);
box-sizing: border-box;
width: 100%;
button {
padding: 0;
margin: 0;
width: 2rem;
height: 2rem;
border: none;
text-align: center;
}
input {
flex: 1;
height: 2rem;
width: 2rem;
}
}
.points-detail {
min-width: 100%;
color: var(--color-primary);
display: flex;
position: absolute;
flex-direction: column;
white-space: nowrap;
background: var(--base-background-color);
border: 0.0625rem solid var(--color-primary);
box-sizing: border-box;
top: 0;
left: 0;
transform: translateY(-100%);
margin: 0;
li {
display: flex;
justify-content: space-between;
line-height: 2rem;
padding: 0 0.5rem;
gap: 0.5rem;
}
li:not(:first-child) {
border-top: 0.0625rem solid var(--color-primary);
}
}
> ul.alloc {
display: flex;
flex-direction: row;
justify-content: center;
gap: 0.5rem;
padding: 0;
margin: 0;
list-style: none;
width: 100%;
> li {
flex: 1 0;
display: flex;
position: relative;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
> span {
width: 100%;
height: 2rem;
border: 0.0625rem solid var(--base-text-color);
border-bottom: none;
text-align: center;
line-height: 2rem;
box-sizing: border-box;
}
}
> li.left {
> span {
background: var(--color-primary);
color: var(--base-background-color);
border: 0.0625rem solid var(--color-primary);
border-bottom: none;
}
> button {
flex: 1;
font-size: 1rem;
line-height: 2rem;
width: 6rem;
padding: 0;
box-sizing: border-box;
}
}
}
> div {
display: flex;
flex-direction: row;
justify-content: center;
width: 100%;
gap: 0.5rem;
button { flex: 1 0; }
}
> ul.talent-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
li { width: 100%; }
}
}
+107
View File
@@ -0,0 +1,107 @@
import { useState } from 'react'
import { useAllocator, usePointRandomizer, useLeftPoints } from '@remake/hooks'
import { usePicked, useReplaced } from '@remake/hooks'
import type { AdditionalPoint } from '@remake/hooks'
import { properties } from '@/display'
import { keys } from '@remake/vitex'
import { BasePoints } from '@/config'
import { talents } from '@remake/data'
import Replaced from '@/components/Replaced'
import './Alloc.css'
interface AllocInputProps {
point: number
onChange: (point: number) => void
}
function AllocInput(props: AllocInputProps) {
return (
<div className="allocation-input">
<button onClick={() => props.onChange(props.point - 1)}></button>
<input
className="font-mono"
type="number"
value={props.point}
onChange={e => props.onChange(Number(e.target.value))}
/>
<button onClick={() => props.onChange(props.point + 1)}>+</button>
</div>
)
}
interface PointsDetailProps {
source: AdditionalPoint[]
}
function PointsDetail({ source }: PointsDetailProps) {
return (
<ul className="points-detail">
<li>
<span></span>
<span className="font-mono">{BasePoints}</span>
</li>
{source.map(({ talent, points }) => (
<li key={talent}>
<span>{talents.get(talent)?.name ?? talent}</span>
<span className="font-mono">{points}</span>
</li>
))}
</ul>
)
}
export function Alloc() {
const picked = usePicked()
const {
talents: { chains },
additionalPoints: { source },
} = useReplaced()
const left = useLeftPoints()
const [allocation, allocator] = useAllocator()
const random = usePointRandomizer()
const [showDetail, setShowDetail] = useState(false)
const handleNext = () => {
// navigate('/life')
}
return (
<div className="screen point-allocation">
<ul className="talent-list">
{Array.from(picked, id => (
<li key={id}>
<Replaced id={id} chains={chains.get(id)} />
</li>
))}
</ul>
<ul className="alloc">
<li className="left">
<span></span>
<button
className="primary font-mono"
onClick={() => setShowDetail(!showDetail)}
>
{left}
</button>
{showDetail && <PointsDetail source={source} />}
</li>
{keys(allocation).map(key => (
<li key={key}>
<span>{properties[key]}</span>
<AllocInput
point={allocation[key]}
onChange={value => allocator(key, value)}
/>
</li>
))}
</ul>
<div>
<button className="info" onClick={() => random()}>
</button>
<button className="primary" onClick={() => {}}>
</button>
</div>
</div>
)
}
export default Alloc
+17
View File
@@ -0,0 +1,17 @@
.screen.home {
gap: 5rem;
.title {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
h1 {
font-size: 4rem;
margin: 0;
}
h2 {
font-size: 2rem;
margin: 0;
}
}
}
+17
View File
@@ -0,0 +1,17 @@
import { useRemake } from '@remake/hooks'
import './Home.css'
export default function Home() {
const remake = useRemake()
return (
<div className="screen home">
<div className="title">
<h1></h1>
<h2></h2>
</div>
<button className="primary" onClick={remake}>
</button>
</div>
)
}
+9
View File
@@ -0,0 +1,9 @@
export function Loading() {
return (
<div className="screen loading">
<h2>...</h2>
</div>
)
}
export default Loading
+3
View File
@@ -0,0 +1,3 @@
export default function Life() {
return <></>
}
+10
View File
@@ -0,0 +1,10 @@
export default function Summary() {
return (
<div className="screen summary">
<h2></h2>
<p>: {}</p>
{/* 展现解锁的成就等 */}
<button onClick={() => {}}></button>
</div>
)
}
+22
View File
@@ -0,0 +1,22 @@
.screen.talent-pick {
display: flex;
flex-direction: column;
justify-content: center;
gap: 1rem;
width: 100%;
ul.talent-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
li {
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
padding: 0;
.talent {
width: 100%;
}
}
}
}
+39
View File
@@ -0,0 +1,39 @@
// import { useEffect } from 'react'
import { useTalentPuller, useTalentPicker } from '@remake/hooks'
import { useTalentSubmit, useSubmitIsEnable } from '@remake/hooks'
import TalentComponent from '@/components/Talent'
import './TalentPick.css'
export default function TalentPick() {
const [pulled, puller] = useTalentPuller()
const [talents, picker] = useTalentPicker()
const [enabled, limit] = useSubmitIsEnable()
const submit = useTalentSubmit()
// useEffect(puller, [])
if (!pulled)
return (
<div className="screen talent-pick">
<button className="primary" onClick={() => puller()}>
</button>
</div>
)
return (
<div className="screen talent-pick">
<ul className="talent-list">
{pulled.map(id => (
<li key={id} onClick={() => picker(id)}>
<TalentComponent id={id} selected={talents.has(id)} />
</li>
))}
</ul>
{enabled ? (
<button className="primary" onClick={() => submit()}>
</button>
) : (
<button className="error"> {limit} </button>
)}
</div>
)
}
+6
View File
@@ -0,0 +1,6 @@
export const properties = {
charm: '颜值',
intelligence: '智力',
strength: '体质',
money: '家境',
}
+44
View File
@@ -0,0 +1,44 @@
import { useCallback, useTransition } from 'react'
import { atom, useAtom } from 'jotai'
import { useConfigInject, useProfile, useProfileInject } from '@remake/hooks'
import { get, set } from '@/storage'
import { config } from '@/config'
const initedAtom = atom(false)
export const useInit = () => {
const configInject = useConfigInject()
const profileInject = useProfileInject()
const [inited, setInited] = useAtom(initedAtom)
const [_, startTransition] = useTransition()
const loader = useCallback(() => {
configInject(config)
startTransition(async () => {
const { profile } = await get(['profile'])
const parsed = profile ? JSON.parse(profile) || {} : {}
profileInject({
...parsed,
times: parsed.times || 0,
achievements: new Set(parsed.achievements || []),
events: new Set(parsed.events || []),
talents: new Set(parsed.talents || []),
})
setInited(true)
})
}, [])
return [inited, loader] as const
}
export const useSaver = () => {
const [profile] = useProfile()
const saver = useCallback(() => {
const str = JSON.stringify({
times: profile.times,
achievements: Array.from(profile.achievements),
events: Array.from(profile.events),
talents: Array.from(profile.talents),
})
set({ profile: str })
}, [profile])
return saver
}
+16 -5
View File
@@ -1,6 +1,17 @@
import { render } from 'preact'
export function App() {
return <div>Hello</div>
}
import { createRoot } from 'react-dom/client'
import { StrictMode } from 'react'
import { Game } from './Game'
import { ThemeToggle } from './components/ThemeToggle'
import { Jotai } from './jotai'
import './styles/colors.css'
import './styles/common.css'
render(<App />, document.getElementById('app')!)
const container = document.getElementById('remake')!
const root = createRoot(container)
root.render(
<StrictMode>
<Jotai />
<ThemeToggle />
<Game />
</StrictMode>,
)
+6
View File
@@ -0,0 +1,6 @@
import { useAtomsDevtools } from 'jotai-devtools/utils'
export function Jotai() {
useAtomsDevtools('Store')
return null
}
export default Jotai
+12
View File
@@ -0,0 +1,12 @@
export async function get<Key extends string>(keys: Key[]) {
return Object.fromEntries(keys.map(k => [k, localStorage.getItem(k)])) as {
[K in Key]: string | null
}
}
export async function set<Key extends string>(data: Record<Key, string>) {
for (const key in data) {
localStorage.setItem(key, data[key])
}
return true
}
+14
View File
@@ -0,0 +1,14 @@
:root {
color-scheme: light dark;
--base-text-color: light-dark(#121314, #f9fafb);
--base-background-color: light-dark(#f7f7f7, #121314);
--color-grade-0: light-dark(#4e5052, #f9fafb);
--color-grade-1: light-dark(#4aa8da, #afe3ff);
--color-grade-2: light-dark(#b03fe4, #e09eff);
--color-grade-3: light-dark(#ffaf2d, #ffe48d);
--color-primary: light-dark(#61b700, #b9ff69);
--color-info: light-dark(#454ad3, #b5b7ff);
--color-error: light-dark(#e42b2b, #ff6969);
}
+89
View File
@@ -0,0 +1,89 @@
html { transition: all 0.2s ease-in-out; }
html[data-theme='light'] { color-scheme: light; }
html[data-theme='dark'] { color-scheme: dark; }
:root { color: var(--base-text-color); }
html {
overflow-y: auto;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
-webkit-overflow-scrolling: touch;
&::-webkit-scrollbar { display: none; }
}
body {
min-height: calc(100dvh - 2rem);
width: calc(100dvw - 2rem);
display: grid;
place-items: center;
margin: 1rem;
padding: 0;
background: var(--base-background-color);
font-family:
-apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Pro Display", "PingFang SC",
"Noto Sans SC", "Noto Sans CJK SC", "Noto Sans",
"Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.font-mono {
font-family:
"SF Mono", SFMono-Regular, Menlo, Monaco, Consolas,
"Noto Sans Mono CJK SC", "Noto Sans Mono", "Noto Code",
ui-monospace, monospace;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
button {
--style-color: var(--base-text-color);
--style-background: var(--base-background-color);
--border-color: var(--style-color);
--background-color: var(--style-background);
--text-color: var(--style-color);
color: var(--text-color);
cursor: pointer;
box-sizing: border-box;
border: var(--border-color) 0.0625rem solid;
background: var(--background-color);
font-size: 1.2rem;
padding: 0 1rem;
line-height: 2.5rem;
display: inline-flex;
align-items: center;
justify-content: center;
font-family: Arial, sans-serif;
user-select: none;
transition: all 0.2s ease-in-out;
&:hover {
--background-color: var(--style-color);
--text-color: var(--style-background);
}
&.primary { --style-color: var(--color-primary); }
&.info { --style-color: var(--color-info); }
&.error { --style-color: var(--color-error); }
}
ul {
list-style: none;
padding: 0;
margin: 0;
}
input {
font-size: 1rem;
padding: 0;
margin: 0;
border: none;
background: var(--base-background-color);
text-align: center;
-moz-appearance: textfield;
appearance: none;
color: var(--base-text-color);
transition: all 0.2s ease-in-out;
&:focus { outline: none; }
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
}
@media (max-width: 32rem) { :root { font-size: 2.8vw; } }
+4 -30
View File
@@ -1,39 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
"types": ["preact"],
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"jsxImportSource": "preact",
"types": ["react", "vite/client"],
"jsxImportSource": "react",
"paths": {
"@/*": ["./src/*"],
"@assets/*": ["./assets/*"],
"react": ["./node_modules/preact/compat/"],
"react-dom": ["./node_modules/preact/compat/"]
},
// Some stricter flags (disabled by default)
"noUnusedLocals": true,
"noUnusedParameters": true,
"noPropertyAccessFromIndexSignature": false,
"noImplicitAny": true
},
"include": ["node_modules/vite/client.d.ts", "**/*"]
"include": ["**/*"]
}
+3 -2
View File
@@ -1,7 +1,8 @@
import { defineConfig } from 'vite'
import preact from '@preact/preset-vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [preact()],
plugins: [react()],
resolve: { tsconfigPaths: true },
})