Initialization
This commit is contained in:
+129
@@ -0,0 +1,129 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'tw-animate-css';
|
||||
@import 'shadcn/tailwind.css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-geist-sans), 'Geist Fallback';
|
||||
--font-mono: var(--font-geist-mono), 'Geist Mono Fallback';
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-ring: var(--ring);
|
||||
--color-input: var(--input);
|
||||
--color-border: var(--border);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-card: var(--card);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-background: var(--background);
|
||||
--radius-sm: 2px;
|
||||
--radius-md: 2px;
|
||||
--radius-lg: 2px;
|
||||
--radius-xl: 2px;
|
||||
--radius-2xl: 2px;
|
||||
--radius-3xl: 2px;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* FUI 极简浅色系 */
|
||||
--background: #ffffff;
|
||||
--foreground: #16191f;
|
||||
--card: #f7f8fa;
|
||||
--card-foreground: #16191f;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #16191f;
|
||||
/* 主色:近黑 */
|
||||
--primary: #16191f;
|
||||
--primary-foreground: #ffffff;
|
||||
/* 强调色:近黑 — 唯一的高亮 */
|
||||
--accent: #0f141a;
|
||||
--accent-foreground: #ffffff;
|
||||
--secondary: #f1f3f5;
|
||||
--secondary-foreground: #4a5563;
|
||||
--muted: #f1f3f5;
|
||||
--muted-foreground: #8a95a1;
|
||||
--destructive: #d92d20;
|
||||
--border: #e2e6ea;
|
||||
--input: #f7f8fa;
|
||||
--ring: #16191f00;
|
||||
--radius: 2px;
|
||||
|
||||
--sidebar: #f7f8fa;
|
||||
--sidebar-foreground: #16191f;
|
||||
--sidebar-primary: #16191f;
|
||||
--sidebar-primary-foreground: #ffffff;
|
||||
--sidebar-accent: #f1f3f5;
|
||||
--sidebar-accent-foreground: #4a5563;
|
||||
--sidebar-border: #e2e6ea;
|
||||
--sidebar-ring: #16191f00;
|
||||
|
||||
/* FUI 专用色标 */
|
||||
--fui-accent: #0f141a;
|
||||
--fui-dim: #e2e6ea;
|
||||
--fui-panel: #f7f8fa;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground font-mono antialiased;
|
||||
}
|
||||
html {
|
||||
@apply bg-background;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.scrollbar-thin {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #d0d5da transparent;
|
||||
}
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background: #d0d5da;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* FUI 动画 — 只保留淡入,无弹跳、无光晕 */
|
||||
@keyframes fui-fadein {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fui-slide-up {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.fui-fadein {
|
||||
animation: fui-fadein 0.2s ease both;
|
||||
}
|
||||
|
||||
.fui-slide-up {
|
||||
animation: fui-slide-up 0.2s ease both;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { Geist, Geist_Mono } from 'next/font/google'
|
||||
import './globals.css'
|
||||
|
||||
const geistSans = Geist({ variable: '--font-geist-sans', subsets: ['latin'] })
|
||||
const geistMono = Geist_Mono({
|
||||
variable: '--font-geist-mono',
|
||||
subsets: ['latin'],
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'RollCall',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="zh-CN" className={`${geistSans.variable} ${geistMono.variable} bg-background`}>
|
||||
<body className="font-sans antialiased">
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
"use client"
|
||||
|
||||
import { NumberBall } from "@/components/number-ball"
|
||||
import { SettingsPanel } from "@/components/settings"
|
||||
import { ResultPanel } from "@/components/result"
|
||||
import { SingleModePanel } from "@/components/single-panel"
|
||||
import { useRollCallLogic } from "@/components/logic"
|
||||
|
||||
export default function Page() {
|
||||
const [state, actions] = useRollCallLogic()
|
||||
const isSingleMode = state.mode === "single"
|
||||
|
||||
return (
|
||||
<div className="h-screen w-full bg-[#ffffff] text-[#16191f] font-mono flex flex-col lg:flex-row overflow-hidden">
|
||||
|
||||
{/* Left sidebar — config (fixed width, full height) */}
|
||||
<aside className="w-full lg:w-72 flex-shrink-0 border-b lg:border-b-0 lg:border-r border-[#e2e6ea] bg-[#f7f8fa] overflow-y-auto scrollbar-thin">
|
||||
<div className="p-5">
|
||||
<SettingsPanel
|
||||
mode={state.mode}
|
||||
totalTasks={state.totalTasks}
|
||||
pickCount={state.pickCount}
|
||||
rounds={state.rounds}
|
||||
allowRepeat={state.allowRepeat}
|
||||
singleAllowRepeat={state.singleAllowRepeat}
|
||||
selectedRecords={state.selectedRecords}
|
||||
onModeChange={actions.setMode}
|
||||
onTotalTasksChange={actions.setTotalTasks}
|
||||
onPickCountChange={actions.setPickCount}
|
||||
onRoundsChange={actions.setRounds}
|
||||
onAllowRepeatChange={actions.setAllowRepeat}
|
||||
onSingleAllowRepeatChange={actions.setSingleAllowRepeat}
|
||||
onRoll={isSingleMode ? actions.handleSingleRoll : actions.handleRoll}
|
||||
onReset={actions.resetSingle}
|
||||
isRolling={state.isRolling}
|
||||
errorMsg={state.errorMsg}
|
||||
/>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Right content — fills remaining space */}
|
||||
<main className="flex-1 flex flex-col min-w-0 min-h-0">
|
||||
|
||||
{/* Single mode: Full height matrix with records */}
|
||||
{isSingleMode ? (
|
||||
<>
|
||||
{/* Matrix area - full width, dynamic centering */}
|
||||
<section className="flex flex-col flex-1 min-h-0">
|
||||
<div className="flex items-center gap-3 px-5 py-2.5 border-b border-[#e2e6ea] flex-shrink-0">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-[#8a95a1]">MATRIX</span>
|
||||
<span className="flex-1 h-px bg-[#e2e6ea]" />
|
||||
{state.selectedRecords.length > 0 && (
|
||||
<span className="text-[10px] text-[#0f141a]">
|
||||
{state.selectedRecords.length} SELECTED
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 flex items-center justify-center overflow-auto scrollbar-thin p-8">
|
||||
<div className="flex flex-wrap gap-2 justify-center max-w-2xl">
|
||||
{Array.from({ length: state.totalTasks }, (_, i) => i + 1).map((n) => (
|
||||
<NumberBall
|
||||
key={n}
|
||||
number={n}
|
||||
state={actions.getBallState(n)}
|
||||
colorIndex={actions.getBallColorIndex(n)}
|
||||
size="large"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Single mode records panel */}
|
||||
<section className="flex flex-col border-t border-[#e2e6ea] max-h-[35vh]">
|
||||
<SingleModePanel singleBatches={state.singleBatches} />
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{/* Group mode: Matrix grid */}
|
||||
<section className="flex flex-col flex-shrink-0 border-b border-[#e2e6ea]">
|
||||
<div className="flex items-center gap-3 px-5 py-2.5 border-b border-[#e2e6ea]">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-[#8a95a1]">MATRIX</span>
|
||||
<span className="flex-1 h-px bg-[#e2e6ea]" />
|
||||
{state.hasResult && (
|
||||
<span className="text-[10px] text-[#0f141a]">
|
||||
{state.highlighted.size} SELECTED
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 p-4 max-h-[40vh] overflow-y-auto scrollbar-thin">
|
||||
{Array.from({ length: state.totalTasks }, (_, i) => i + 1).map((n) => (
|
||||
<NumberBall
|
||||
key={n}
|
||||
number={n}
|
||||
state={actions.getBallState(n)}
|
||||
colorIndex={actions.getBallColorIndex(n)}
|
||||
size="large"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Group mode result panel */}
|
||||
<section className="flex flex-col flex-1 min-h-0">
|
||||
<div className="flex items-center gap-3 px-5 py-2.5 border-b border-[#e2e6ea] flex-shrink-0">
|
||||
<span className="text-[10px] tracking-[0.2em] uppercase text-[#8a95a1]">OUTPUT</span>
|
||||
<span className="flex-1 h-px bg-[#e2e6ea]" />
|
||||
{state.hasResult && state.groups.length > 0 && (
|
||||
<button
|
||||
onClick={actions.clearResult}
|
||||
className="text-[10px] text-[#8a95a1] hover:text-[#16191f] transition-colors tracking-widest uppercase"
|
||||
>
|
||||
CLEAR
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto scrollbar-thin px-4 py-3">
|
||||
<ResultPanel groups={state.groups} hasResult={state.hasResult} placeholder={state.placeholder} />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)}
|
||||
|
||||
</main>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user