feat: implement global abacus display configuration and remove client-side SVG generation
- Add React Context for app-wide abacus styling (localStorage persistence) - Create AbacusDisplayDropdown with style controls in nav bar - Add AppNavBar component with adaptive behavior (minimal for games) - Remove duplicate navigation headers from guide and home pages - Delete client-side SorobanSVG component - use only server-side Python/Typst - Clean up generateSorobanSVG and generateMockSorobanSVG functions - All abacus displays now use consistent global configuration - Settings persist across page reloads and browser sessions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -62,7 +62,12 @@ export async function POST(request: NextRequest) {
|
||||
console.error(`❌ Failed to generate SVG for number ${number}:`, error instanceof Error ? error.message : error)
|
||||
samples.push({
|
||||
number,
|
||||
front: generateMockSorobanSVG(number),
|
||||
front: `<svg width="200" height="300" viewBox="0 0 200 300" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="10" y="10" width="180" height="280" fill="none" stroke="#ccc" stroke-width="2"/>
|
||||
<line x1="20" y1="150" x2="180" y2="150" stroke="#ccc" stroke-width="2"/>
|
||||
<text x="100" y="160" text-anchor="middle" font-size="24" fill="#666">SVG Error</text>
|
||||
<text x="100" y="180" text-anchor="middle" font-size="16" fill="#999">${number}</text>
|
||||
</svg>`,
|
||||
back: number.toString()
|
||||
})
|
||||
}
|
||||
@@ -144,63 +149,17 @@ function getMockPreviewData(config: any) {
|
||||
count: numbers.length,
|
||||
samples: numbers.map(number => ({
|
||||
number,
|
||||
front: generateMockSorobanSVG(number),
|
||||
front: `<svg width="200" height="300" viewBox="0 0 200 300" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="10" y="10" width="180" height="280" fill="none" stroke="#ccc" stroke-width="2"/>
|
||||
<line x1="20" y1="150" x2="180" y2="150" stroke="#ccc" stroke-width="2"/>
|
||||
<text x="100" y="160" text-anchor="middle" font-size="24" fill="#666">Preview Error</text>
|
||||
<text x="100" y="180" text-anchor="middle" font-size="16" fill="#999">${number}</text>
|
||||
</svg>`,
|
||||
back: number.toString()
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a simple mock soroban SVG for preview
|
||||
function generateMockSorobanSVG(number: number): string {
|
||||
const width = 200
|
||||
const height = 300
|
||||
const rodWidth = 4
|
||||
const beadRadius = 8
|
||||
const heavenBeadHeight = 40
|
||||
const earthBeadHeight = 40
|
||||
const rods = 3 // Show 3 rods for preview
|
||||
|
||||
let svg = `<svg width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">`
|
||||
|
||||
// Frame
|
||||
svg += `<rect x="10" y="10" width="${width-20}" height="${height-20}" fill="none" stroke="#8B4513" stroke-width="3"/>`
|
||||
|
||||
// Crossbar (divider between heaven and earth)
|
||||
const crossbarY = height / 2
|
||||
svg += `<line x1="15" y1="${crossbarY}" x2="${width-15}" y2="${crossbarY}" stroke="#8B4513" stroke-width="3"/>`
|
||||
|
||||
// Generate rods and beads based on the number
|
||||
const digits = number.toString().padStart(rods, '0').split('').map(d => parseInt(d))
|
||||
|
||||
for (let i = 0; i < rods; i++) {
|
||||
const rodX = 40 + i * 50
|
||||
const digit = digits[rods - 1 - i] // Rightmost digit first
|
||||
|
||||
// Rod
|
||||
svg += `<line x1="${rodX}" y1="20" x2="${rodX}" y2="${height-20}" stroke="#654321" stroke-width="${rodWidth}"/>`
|
||||
|
||||
// Calculate bead positions for this digit
|
||||
const heavenValue = digit >= 5 ? 1 : 0
|
||||
const earthValue = digit % 5
|
||||
|
||||
// Heaven bead (worth 5)
|
||||
const heavenY = heavenValue > 0 ? crossbarY - 15 : 30
|
||||
const heavenColor = heavenValue > 0 ? '#FF6B6B' : '#DDD'
|
||||
svg += `<circle cx="${rodX}" cy="${heavenY}" r="${beadRadius}" fill="${heavenColor}" stroke="#333" stroke-width="1"/>`
|
||||
|
||||
// Earth beads (worth 1 each)
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const isActive = j < earthValue
|
||||
const earthY = crossbarY + 20 + j * 25
|
||||
const earthColor = isActive ? '#4ECDC4' : '#DDD'
|
||||
svg += `<circle cx="${rodX}" cy="${earthY}" r="${beadRadius}" fill="${earthColor}" stroke="#333" stroke-width="1"/>`
|
||||
}
|
||||
}
|
||||
|
||||
svg += '</svg>'
|
||||
return svg
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
|
||||
@@ -10,6 +10,7 @@ import { ConfigurationFormWithoutGenerate } from '@/components/ConfigurationForm
|
||||
import { LivePreview } from '@/components/LivePreview'
|
||||
import { GenerationProgress } from '@/components/GenerationProgress'
|
||||
import { StyleControls } from '@/components/StyleControls'
|
||||
import { useAbacusConfig } from '@/contexts/AbacusDisplayContext'
|
||||
|
||||
// Complete, validated configuration ready for generation
|
||||
export interface FlashcardConfig {
|
||||
@@ -106,6 +107,7 @@ type GenerationStatus = 'idle' | 'generating' | 'error'
|
||||
export default function CreatePage() {
|
||||
const [generationStatus, setGenerationStatus] = useState<GenerationStatus>('idle')
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const globalConfig = useAbacusConfig()
|
||||
|
||||
const form = useForm<FlashcardFormState>({
|
||||
defaultValues: {
|
||||
@@ -122,11 +124,12 @@ export default function CreatePage() {
|
||||
fontSize: '48pt',
|
||||
columns: 'auto',
|
||||
showEmptyColumns: false,
|
||||
hideInactiveBeads: false,
|
||||
beadShape: 'diamond',
|
||||
colorScheme: 'place-value',
|
||||
coloredNumerals: false,
|
||||
scaleFactor: 0.9,
|
||||
// Use global config for abacus display settings
|
||||
hideInactiveBeads: globalConfig.hideInactiveBeads,
|
||||
beadShape: globalConfig.beadShape,
|
||||
colorScheme: globalConfig.colorScheme,
|
||||
coloredNumerals: globalConfig.coloredNumerals,
|
||||
scaleFactor: globalConfig.scaleFactor,
|
||||
format: 'pdf'
|
||||
}
|
||||
})
|
||||
@@ -183,55 +186,6 @@ export default function CreatePage() {
|
||||
|
||||
return (
|
||||
<div className={css({ minHeight: '100vh', bg: 'gray.50' })}>
|
||||
{/* Header */}
|
||||
<header className={css({ bg: 'white', shadow: 'card', position: 'sticky', top: 0, zIndex: 10 })}>
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '4' })}>
|
||||
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
|
||||
<Link
|
||||
href="/"
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'brand.800',
|
||||
textDecoration: 'none'
|
||||
})}
|
||||
>
|
||||
🧮 Soroban Generator
|
||||
</Link>
|
||||
|
||||
<div className={hstack({ gap: '3' })}>
|
||||
<Link
|
||||
href="/guide"
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2',
|
||||
color: 'brand.600',
|
||||
fontWeight: 'medium',
|
||||
rounded: 'lg',
|
||||
transition: 'all',
|
||||
_hover: { bg: 'brand.50' }
|
||||
})}
|
||||
>
|
||||
Guide
|
||||
</Link>
|
||||
<Link
|
||||
href="/games"
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2',
|
||||
color: 'brand.600',
|
||||
fontWeight: 'medium',
|
||||
rounded: 'lg',
|
||||
transition: 'all',
|
||||
_hover: { bg: 'brand.50' }
|
||||
})}
|
||||
>
|
||||
Games
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '8' })}>
|
||||
|
||||
@@ -12,41 +12,6 @@ export default function GuidePage() {
|
||||
const [activeTab, setActiveTab] = useState<TabType>('reading')
|
||||
return (
|
||||
<div className={css({ minHeight: '100vh', bg: 'gray.50' })}>
|
||||
{/* Header */}
|
||||
<header className={css({ bg: 'white', shadow: 'card', position: 'sticky', top: 0, zIndex: 10 })}>
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '4' })}>
|
||||
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
|
||||
<Link
|
||||
href="/"
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'brand.800',
|
||||
textDecoration: 'none'
|
||||
})}
|
||||
>
|
||||
🧮 Soroban Generator
|
||||
</Link>
|
||||
|
||||
<div className={hstack({ gap: '3' })}>
|
||||
<Link
|
||||
href="/create"
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2',
|
||||
color: 'brand.600',
|
||||
fontWeight: 'medium',
|
||||
rounded: 'lg',
|
||||
transition: 'all',
|
||||
_hover: { bg: 'brand.50' }
|
||||
})}
|
||||
>
|
||||
Create Flashcards
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<div className={css({
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
import { AbacusDisplayProvider } from '@/contexts/AbacusDisplayContext'
|
||||
import { AppNavBar } from '@/components/AppNavBar'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Soroban Flashcard Generator',
|
||||
@@ -13,7 +15,12 @@ export default function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
<body>
|
||||
<AbacusDisplayProvider>
|
||||
<AppNavBar />
|
||||
{children}
|
||||
</AbacusDisplayProvider>
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
@@ -7,65 +7,6 @@ import Link from 'next/link'
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className={css({ minHeight: '100vh', bg: 'gradient-to-br from-brand.50 to-brand.100' })}>
|
||||
{/* Header */}
|
||||
<header className={css({ py: '6', px: '4' })}>
|
||||
<div className={container({ maxW: '7xl' })}>
|
||||
<nav className={hstack({ justify: 'space-between', alignItems: 'center' })}>
|
||||
<h1 className={css({
|
||||
fontSize: '2xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'brand.800'
|
||||
})}>
|
||||
🧮 Soroban Generator
|
||||
</h1>
|
||||
<div className={hstack({ gap: '4' })}>
|
||||
<Link
|
||||
href="/guide"
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2',
|
||||
color: 'brand.600',
|
||||
rounded: 'lg',
|
||||
fontWeight: 'medium',
|
||||
transition: 'all',
|
||||
_hover: { bg: 'brand.50' }
|
||||
})}
|
||||
>
|
||||
Guide
|
||||
</Link>
|
||||
<Link
|
||||
href="/games"
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2',
|
||||
color: 'brand.600',
|
||||
rounded: 'lg',
|
||||
fontWeight: 'medium',
|
||||
transition: 'all',
|
||||
_hover: { bg: 'brand.50' }
|
||||
})}
|
||||
>
|
||||
Games
|
||||
</Link>
|
||||
<Link
|
||||
href="/create"
|
||||
className={css({
|
||||
px: '4',
|
||||
py: '2',
|
||||
bg: 'brand.600',
|
||||
color: 'white',
|
||||
rounded: 'lg',
|
||||
fontWeight: 'medium',
|
||||
transition: 'all',
|
||||
_hover: { bg: 'brand.700', transform: 'translateY(-1px)' }
|
||||
})}
|
||||
>
|
||||
Create Flashcards
|
||||
</Link>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Hero Section */}
|
||||
<main className={container({ maxW: '6xl', px: '4' })}>
|
||||
|
||||
286
apps/web/src/components/AbacusDisplayDropdown.tsx
Normal file
286
apps/web/src/components/AbacusDisplayDropdown.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import * as Label from '@radix-ui/react-label'
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import * as Switch from '@radix-ui/react-switch'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { stack, hstack } from '../../styled-system/patterns'
|
||||
import { useAbacusDisplay, ColorScheme, BeadShape } from '@/contexts/AbacusDisplayContext'
|
||||
|
||||
export function AbacusDisplayDropdown() {
|
||||
const [open, setOpen] = useState(false)
|
||||
const { config, updateConfig, resetToDefaults } = useAbacusDisplay()
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root open={open} onOpenChange={setOpen}>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<button
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
px: '3',
|
||||
py: '2',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.600',
|
||||
bg: 'white',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
rounded: 'lg',
|
||||
shadow: 'sm',
|
||||
transition: 'all',
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
bg: 'gray.50',
|
||||
borderColor: 'gray.300'
|
||||
},
|
||||
_focus: {
|
||||
outline: 'none',
|
||||
ring: '2px',
|
||||
ringColor: 'brand.500',
|
||||
ringOffset: '1px'
|
||||
}
|
||||
})}
|
||||
>
|
||||
<span className={css({ fontSize: 'lg' })}>🧮</span>
|
||||
<span>Style</span>
|
||||
<svg
|
||||
className={css({
|
||||
w: '4',
|
||||
h: '4',
|
||||
transition: 'transform',
|
||||
transform: open ? 'rotate(180deg)' : 'rotate(0deg)'
|
||||
})}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</DropdownMenu.Trigger>
|
||||
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: 'xl',
|
||||
shadow: 'modal',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
p: '6',
|
||||
minW: '320px',
|
||||
maxW: '400px',
|
||||
zIndex: 50,
|
||||
animation: 'fadeIn 0.2s ease-out'
|
||||
})}
|
||||
sideOffset={8}
|
||||
align="end"
|
||||
>
|
||||
<div className={stack({ gap: '6' })}>
|
||||
{/* Header */}
|
||||
<div className={stack({ gap: '1' })}>
|
||||
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
|
||||
<h3 className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'semibold',
|
||||
color: 'gray.900'
|
||||
})}>
|
||||
🎨 Abacus Style
|
||||
</h3>
|
||||
<button
|
||||
onClick={resetToDefaults}
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'gray.500',
|
||||
_hover: { color: 'gray.700' }
|
||||
})}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
<p className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'gray.600'
|
||||
})}>
|
||||
Configure display across the entire app
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Color Scheme */}
|
||||
<FormField label="Color Scheme">
|
||||
<RadioGroupField
|
||||
value={config.colorScheme}
|
||||
onValueChange={(value) => updateConfig({ colorScheme: value as ColorScheme })}
|
||||
options={[
|
||||
{ value: 'monochrome', label: 'Monochrome' },
|
||||
{ value: 'place-value', label: 'Place Value' },
|
||||
{ value: 'heaven-earth', label: 'Heaven-Earth' },
|
||||
{ value: 'alternating', label: 'Alternating' }
|
||||
]}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
{/* Bead Shape */}
|
||||
<FormField label="Bead Shape">
|
||||
<RadioGroupField
|
||||
value={config.beadShape}
|
||||
onValueChange={(value) => updateConfig({ beadShape: value as BeadShape })}
|
||||
options={[
|
||||
{ value: 'diamond', label: '💎 Diamond' },
|
||||
{ value: 'circle', label: '⭕ Circle' },
|
||||
{ value: 'square', label: '⬜ Square' }
|
||||
]}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
{/* Toggle Options */}
|
||||
<div className={stack({ gap: '4' })}>
|
||||
<FormField label="Hide Inactive Beads">
|
||||
<SwitchField
|
||||
checked={config.hideInactiveBeads}
|
||||
onCheckedChange={(checked) => updateConfig({ hideInactiveBeads: checked })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Colored Numerals">
|
||||
<SwitchField
|
||||
checked={config.coloredNumerals}
|
||||
onCheckedChange={(checked) => updateConfig({ coloredNumerals: checked })}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
// Helper Components (simplified versions of StyleControls components)
|
||||
function FormField({
|
||||
label,
|
||||
children
|
||||
}: {
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<div className={stack({ gap: '2' })}>
|
||||
<Label.Root className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.900'
|
||||
})}>
|
||||
{label}
|
||||
</Label.Root>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SwitchField({
|
||||
checked,
|
||||
onCheckedChange
|
||||
}: {
|
||||
checked: boolean
|
||||
onCheckedChange: (checked: boolean) => void
|
||||
}) {
|
||||
return (
|
||||
<Switch.Root
|
||||
checked={checked}
|
||||
onCheckedChange={onCheckedChange}
|
||||
className={css({
|
||||
w: '11',
|
||||
h: '6',
|
||||
bg: checked ? 'brand.600' : 'gray.300',
|
||||
rounded: 'full',
|
||||
position: 'relative',
|
||||
transition: 'all',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: checked ? 'brand.700' : 'gray.400' }
|
||||
})}
|
||||
>
|
||||
<Switch.Thumb
|
||||
className={css({
|
||||
display: 'block',
|
||||
w: '5',
|
||||
h: '5',
|
||||
bg: 'white',
|
||||
rounded: 'full',
|
||||
shadow: 'sm',
|
||||
transition: 'transform 0.2s',
|
||||
transform: checked ? 'translateX(20px)' : 'translateX(0px)',
|
||||
willChange: 'transform'
|
||||
})}
|
||||
/>
|
||||
</Switch.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function RadioGroupField({
|
||||
value,
|
||||
onValueChange,
|
||||
options
|
||||
}: {
|
||||
value: string
|
||||
onValueChange: (value: string) => void
|
||||
options: Array<{ value: string; label: string }>
|
||||
}) {
|
||||
return (
|
||||
<RadioGroup.Root
|
||||
value={value}
|
||||
onValueChange={onValueChange}
|
||||
className={stack({ gap: '2' })}
|
||||
>
|
||||
{options.map((option) => (
|
||||
<div key={option.value} className={hstack({ gap: '3', alignItems: 'center' })}>
|
||||
<RadioGroup.Item
|
||||
value={option.value}
|
||||
className={css({
|
||||
w: '4',
|
||||
h: '4',
|
||||
rounded: 'full',
|
||||
border: '2px solid',
|
||||
borderColor: 'gray.300',
|
||||
bg: 'white',
|
||||
cursor: 'pointer',
|
||||
transition: 'all',
|
||||
_hover: { borderColor: 'brand.400' },
|
||||
'&[data-state=checked]': { borderColor: 'brand.600' }
|
||||
})}
|
||||
>
|
||||
<RadioGroup.Indicator
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
w: 'full',
|
||||
h: 'full',
|
||||
position: 'relative',
|
||||
_after: {
|
||||
content: '""',
|
||||
display: 'block',
|
||||
w: '1.5',
|
||||
h: '1.5',
|
||||
rounded: 'full',
|
||||
bg: 'brand.600'
|
||||
}
|
||||
})}
|
||||
/>
|
||||
</RadioGroup.Item>
|
||||
<label className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'gray.900',
|
||||
cursor: 'pointer'
|
||||
})}>
|
||||
{option.label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup.Root>
|
||||
)
|
||||
}
|
||||
121
apps/web/src/components/AppNavBar.tsx
Normal file
121
apps/web/src/components/AppNavBar.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { container, hstack } from '../../styled-system/patterns'
|
||||
import { AbacusDisplayDropdown } from './AbacusDisplayDropdown'
|
||||
|
||||
interface AppNavBarProps {
|
||||
variant?: 'full' | 'minimal'
|
||||
}
|
||||
|
||||
export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
|
||||
const pathname = usePathname()
|
||||
const isGamePage = pathname?.startsWith('/games')
|
||||
|
||||
// Auto-detect variant based on context
|
||||
const actualVariant = variant === 'full' && isGamePage ? 'minimal' : variant
|
||||
|
||||
if (actualVariant === 'minimal') {
|
||||
return (
|
||||
<header className={css({
|
||||
position: 'fixed',
|
||||
top: '4',
|
||||
right: '4',
|
||||
zIndex: 40,
|
||||
// Make it less prominent during games
|
||||
opacity: '0.8',
|
||||
_hover: { opacity: '1' },
|
||||
transition: 'opacity'
|
||||
})}>
|
||||
<div className={hstack({ gap: '2' })}>
|
||||
<AbacusDisplayDropdown />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<header className={css({
|
||||
bg: 'white',
|
||||
shadow: 'sm',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 30
|
||||
})}>
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '3' })}>
|
||||
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'brand.800',
|
||||
textDecoration: 'none',
|
||||
_hover: { color: 'brand.900' }
|
||||
})}
|
||||
>
|
||||
🧮 Soroban Generator
|
||||
</Link>
|
||||
|
||||
<div className={hstack({ gap: '6', alignItems: 'center' })}>
|
||||
{/* Navigation Links */}
|
||||
<nav className={hstack({ gap: '4' })}>
|
||||
<NavLink href="/create" currentPath={pathname}>
|
||||
Create
|
||||
</NavLink>
|
||||
<NavLink href="/guide" currentPath={pathname}>
|
||||
Guide
|
||||
</NavLink>
|
||||
<NavLink href="/games" currentPath={pathname}>
|
||||
Games
|
||||
</NavLink>
|
||||
</nav>
|
||||
|
||||
{/* Abacus Style Dropdown */}
|
||||
<AbacusDisplayDropdown />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
function NavLink({
|
||||
href,
|
||||
currentPath,
|
||||
children
|
||||
}: {
|
||||
href: string
|
||||
currentPath: string | null
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const isActive = currentPath === href || (href !== '/' && currentPath?.startsWith(href))
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className={css({
|
||||
px: '3',
|
||||
py: '2',
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: isActive ? 'brand.700' : 'gray.600',
|
||||
bg: isActive ? 'brand.50' : 'transparent',
|
||||
rounded: 'lg',
|
||||
transition: 'all',
|
||||
textDecoration: 'none',
|
||||
_hover: {
|
||||
color: isActive ? 'brand.800' : 'gray.900',
|
||||
bg: isActive ? 'brand.100' : 'gray.50'
|
||||
}
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { useAbacusConfig } from '@/contexts/AbacusDisplayContext'
|
||||
|
||||
interface ServerSorobanSVGProps {
|
||||
number: number
|
||||
@@ -15,13 +16,19 @@ interface ServerSorobanSVGProps {
|
||||
|
||||
export function ServerSorobanSVG({
|
||||
number,
|
||||
colorScheme = 'place-value',
|
||||
hideInactiveBeads = false,
|
||||
beadShape = 'diamond',
|
||||
colorScheme,
|
||||
hideInactiveBeads,
|
||||
beadShape,
|
||||
width = 240,
|
||||
height = 320,
|
||||
className = ''
|
||||
}: ServerSorobanSVGProps) {
|
||||
const globalConfig = useAbacusConfig()
|
||||
|
||||
// Use global config as defaults, allow props to override
|
||||
const actualColorScheme = colorScheme ?? globalConfig.colorScheme
|
||||
const actualHideInactiveBeads = hideInactiveBeads ?? globalConfig.hideInactiveBeads
|
||||
const actualBeadShape = beadShape ?? globalConfig.beadShape
|
||||
const [svgContent, setSvgContent] = useState<string>('')
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -34,9 +41,9 @@ export function ServerSorobanSVG({
|
||||
try {
|
||||
const config = {
|
||||
range: number.toString(),
|
||||
colorScheme,
|
||||
hideInactiveBeads,
|
||||
beadShape,
|
||||
colorScheme: actualColorScheme,
|
||||
hideInactiveBeads: actualHideInactiveBeads,
|
||||
beadShape: actualBeadShape,
|
||||
format: 'svg'
|
||||
}
|
||||
|
||||
@@ -69,7 +76,7 @@ export function ServerSorobanSVG({
|
||||
}
|
||||
|
||||
generateSVG()
|
||||
}, [number, colorScheme, hideInactiveBeads, beadShape])
|
||||
}, [number, actualColorScheme, actualHideInactiveBeads, actualBeadShape])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { css } from '../../styled-system/css'
|
||||
|
||||
interface SorobanSVGProps {
|
||||
number: number
|
||||
colorScheme?: 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
|
||||
width?: number
|
||||
height?: number
|
||||
hideInactiveBeads?: boolean
|
||||
beadShape?: 'diamond' | 'circle' | 'square'
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function SorobanSVG({
|
||||
number,
|
||||
colorScheme = 'place-value',
|
||||
width = 240,
|
||||
height = 320,
|
||||
hideInactiveBeads = false,
|
||||
beadShape = 'diamond',
|
||||
className = ''
|
||||
}: SorobanSVGProps) {
|
||||
const svg = generateSorobanSVG({
|
||||
number,
|
||||
colorScheme,
|
||||
width,
|
||||
height,
|
||||
hideInactiveBeads,
|
||||
beadShape
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: svg }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function generateSorobanSVG({
|
||||
number,
|
||||
colorScheme,
|
||||
width,
|
||||
height,
|
||||
hideInactiveBeads,
|
||||
beadShape
|
||||
}: {
|
||||
number: number
|
||||
colorScheme: string
|
||||
width: number
|
||||
height: number
|
||||
hideInactiveBeads: boolean
|
||||
beadShape: string
|
||||
}): string {
|
||||
const rodWidth = 4
|
||||
const beadSize = beadShape === 'circle' ? 10 : 8
|
||||
const heavenBeadHeight = 40
|
||||
const earthBeadHeight = 40
|
||||
|
||||
// Determine number of rods needed
|
||||
const numString = Math.abs(number).toString()
|
||||
const rods = Math.max(numString.length, 2) // At least 2 rods for display
|
||||
|
||||
// Adjust width based on number of rods
|
||||
const actualWidth = Math.max(width, 80 + rods * 50)
|
||||
|
||||
let svg = `<svg width="${actualWidth}" height="${height}" viewBox="0 0 ${actualWidth} ${height}" xmlns="http://www.w3.org/2000/svg">`
|
||||
|
||||
// Frame
|
||||
svg += `<rect x="10" y="10" width="${actualWidth-20}" height="${height-20}" fill="none" stroke="#8B4513" stroke-width="3"/>`
|
||||
|
||||
// Crossbar (divider between heaven and earth)
|
||||
const crossbarY = height / 2
|
||||
svg += `<line x1="15" y1="${crossbarY}" x2="${actualWidth-15}" y2="${crossbarY}" stroke="#8B4513" stroke-width="3"/>`
|
||||
|
||||
// Generate digits with proper padding
|
||||
const digits = numString.padStart(rods, '0').split('').map(d => parseInt(d))
|
||||
|
||||
for (let i = 0; i < rods; i++) {
|
||||
const rodX = 40 + i * 50
|
||||
const digit = digits[i]
|
||||
const placeValue = Math.pow(10, rods - 1 - i) // 100s, 10s, 1s place etc.
|
||||
|
||||
// Rod
|
||||
svg += `<line x1="${rodX}" y1="20" x2="${rodX}" y2="${height-20}" stroke="#654321" stroke-width="${rodWidth}"/>`
|
||||
|
||||
// Calculate bead positions for this digit
|
||||
const heavenValue = digit >= 5 ? 1 : 0
|
||||
const earthValue = digit % 5
|
||||
|
||||
// Get colors based on scheme and place value
|
||||
const colors = getBeadColors(colorScheme, placeValue)
|
||||
|
||||
// Heaven bead (worth 5)
|
||||
const heavenActive = heavenValue > 0
|
||||
const heavenY = heavenActive ? crossbarY - 15 : 30
|
||||
const heavenColor = heavenActive ? colors.heaven : (hideInactiveBeads ? 'transparent' : '#E5E5E5')
|
||||
const heavenStroke = heavenActive || !hideInactiveBeads ? '#333' : 'transparent'
|
||||
|
||||
if (heavenColor !== 'transparent') {
|
||||
svg += createBead(rodX, heavenY, beadSize, heavenColor, heavenStroke, beadShape)
|
||||
}
|
||||
|
||||
// Earth beads (worth 1 each)
|
||||
for (let j = 0; j < 4; j++) {
|
||||
const isActive = j < earthValue
|
||||
const earthY = crossbarY + 20 + j * 25
|
||||
const earthColor = isActive ? colors.earth : (hideInactiveBeads ? 'transparent' : '#E5E5E5')
|
||||
const earthStroke = isActive || !hideInactiveBeads ? '#333' : 'transparent'
|
||||
|
||||
if (earthColor !== 'transparent') {
|
||||
svg += createBead(rodX, earthY, beadSize, earthColor, earthStroke, beadShape)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg += '</svg>'
|
||||
return svg
|
||||
}
|
||||
|
||||
function createBead(x: number, y: number, size: number, fill: string, stroke: string, shape: string): string {
|
||||
switch (shape) {
|
||||
case 'circle':
|
||||
return `<circle cx="${x}" cy="${y}" r="${size}" fill="${fill}" stroke="${stroke}" stroke-width="1"/>`
|
||||
case 'square':
|
||||
return `<rect x="${x - size}" y="${y - size}" width="${size * 2}" height="${size * 2}" fill="${fill}" stroke="${stroke}" stroke-width="1"/>`
|
||||
case 'diamond':
|
||||
default:
|
||||
const points = `${x},${y - size} ${x + size},${y} ${x},${y + size} ${x - size},${y}`
|
||||
return `<polygon points="${points}" fill="${fill}" stroke="${stroke}" stroke-width="1"/>`
|
||||
}
|
||||
}
|
||||
|
||||
function getBeadColors(colorScheme: string, placeValue: number): { heaven: string; earth: string } {
|
||||
switch (colorScheme) {
|
||||
case 'monochrome':
|
||||
return { heaven: '#666', earth: '#666' }
|
||||
|
||||
case 'heaven-earth':
|
||||
return { heaven: '#FF6B6B', earth: '#4ECDC4' }
|
||||
|
||||
case 'alternating':
|
||||
return placeValue % 2 === 0
|
||||
? { heaven: '#FF6B6B', earth: '#4ECDC4' }
|
||||
: { heaven: '#4ECDC4', earth: '#FF6B6B' }
|
||||
|
||||
case 'place-value':
|
||||
default:
|
||||
// Colors based on place value (ones, tens, hundreds, etc.)
|
||||
if (placeValue >= 1000) return { heaven: '#9B59B6', earth: '#8E44AD' } // Purple for thousands
|
||||
if (placeValue >= 100) return { heaven: '#E74C3C', earth: '#C0392B' } // Red for hundreds
|
||||
if (placeValue >= 10) return { heaven: '#3498DB', earth: '#2980B9' } // Blue for tens
|
||||
return { heaven: '#2ECC71', earth: '#27AE60' } // Green for ones
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,23 @@ import * as Switch from '@radix-ui/react-switch'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { stack, hstack, grid } from '../../styled-system/patterns'
|
||||
import { FlashcardFormState } from '@/app/create/page'
|
||||
import { useAbacusDisplay } from '@/contexts/AbacusDisplayContext'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
interface StyleControlsProps {
|
||||
form: FormApi<FlashcardFormState>
|
||||
}
|
||||
|
||||
export function StyleControls({ form }: StyleControlsProps) {
|
||||
const { config, updateConfig } = useAbacusDisplay()
|
||||
|
||||
// Sync form values with global context
|
||||
useEffect(() => {
|
||||
form.setFieldValue('colorScheme', config.colorScheme)
|
||||
form.setFieldValue('beadShape', config.beadShape)
|
||||
form.setFieldValue('hideInactiveBeads', config.hideInactiveBeads)
|
||||
form.setFieldValue('coloredNumerals', config.coloredNumerals)
|
||||
}, [config, form])
|
||||
return (
|
||||
<div className={stack({ gap: '4' })}>
|
||||
<FormField
|
||||
@@ -23,7 +34,10 @@ export function StyleControls({ form }: StyleControlsProps) {
|
||||
{(field) => (
|
||||
<RadioGroupField
|
||||
value={field.state.value || 'place-value'}
|
||||
onValueChange={(value) => field.handleChange(value as any)}
|
||||
onValueChange={(value) => {
|
||||
field.handleChange(value as any)
|
||||
updateConfig({ colorScheme: value as any })
|
||||
}}
|
||||
options={[
|
||||
{ value: 'monochrome', label: 'Monochrome', desc: 'Classic black and white' },
|
||||
{ value: 'place-value', label: 'Place Value', desc: 'Colors by digit position' },
|
||||
@@ -43,7 +57,10 @@ export function StyleControls({ form }: StyleControlsProps) {
|
||||
{(field) => (
|
||||
<RadioGroupField
|
||||
value={field.state.value || 'diamond'}
|
||||
onValueChange={(value) => field.handleChange(value as any)}
|
||||
onValueChange={(value) => {
|
||||
field.handleChange(value as any)
|
||||
updateConfig({ beadShape: value as any })
|
||||
}}
|
||||
options={[
|
||||
{ value: 'diamond', label: '💎 Diamond', desc: 'Realistic 3D appearance' },
|
||||
{ value: 'circle', label: '⭕ Circle', desc: 'Traditional round beads' },
|
||||
@@ -63,7 +80,10 @@ export function StyleControls({ form }: StyleControlsProps) {
|
||||
{(field) => (
|
||||
<SwitchField
|
||||
checked={field.state.value || false}
|
||||
onCheckedChange={field.handleChange}
|
||||
onCheckedChange={(checked) => {
|
||||
field.handleChange(checked)
|
||||
updateConfig({ coloredNumerals: checked })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
@@ -77,7 +97,10 @@ export function StyleControls({ form }: StyleControlsProps) {
|
||||
{(field) => (
|
||||
<SwitchField
|
||||
checked={field.state.value || false}
|
||||
onCheckedChange={field.handleChange}
|
||||
onCheckedChange={(checked) => {
|
||||
field.handleChange(checked)
|
||||
updateConfig({ hideInactiveBeads: checked })
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</form.Field>
|
||||
|
||||
131
apps/web/src/contexts/AbacusDisplayContext.tsx
Normal file
131
apps/web/src/contexts/AbacusDisplayContext.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
'use client'
|
||||
|
||||
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'
|
||||
|
||||
// Abacus display configuration types
|
||||
export type ColorScheme = 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
|
||||
export type BeadShape = 'diamond' | 'circle' | 'square'
|
||||
|
||||
export interface AbacusDisplayConfig {
|
||||
colorScheme: ColorScheme
|
||||
beadShape: BeadShape
|
||||
hideInactiveBeads: boolean
|
||||
coloredNumerals: boolean
|
||||
scaleFactor: number
|
||||
}
|
||||
|
||||
export interface AbacusDisplayContextType {
|
||||
config: AbacusDisplayConfig
|
||||
updateConfig: (updates: Partial<AbacusDisplayConfig>) => void
|
||||
resetToDefaults: () => void
|
||||
}
|
||||
|
||||
// Default configuration - matches current create page defaults
|
||||
const DEFAULT_CONFIG: AbacusDisplayConfig = {
|
||||
colorScheme: 'place-value',
|
||||
beadShape: 'diamond',
|
||||
hideInactiveBeads: false,
|
||||
coloredNumerals: false,
|
||||
scaleFactor: 1.0 // Normalized for display, can be scaled per component
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'soroban-abacus-display-config'
|
||||
|
||||
// Load config from localStorage with fallback to defaults
|
||||
function loadConfigFromStorage(): AbacusDisplayConfig {
|
||||
if (typeof window === 'undefined') return DEFAULT_CONFIG
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY)
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored)
|
||||
// Validate that all required fields are present and have valid values
|
||||
return {
|
||||
colorScheme: ['monochrome', 'place-value', 'heaven-earth', 'alternating'].includes(parsed.colorScheme)
|
||||
? parsed.colorScheme : DEFAULT_CONFIG.colorScheme,
|
||||
beadShape: ['diamond', 'circle', 'square'].includes(parsed.beadShape)
|
||||
? parsed.beadShape : DEFAULT_CONFIG.beadShape,
|
||||
hideInactiveBeads: typeof parsed.hideInactiveBeads === 'boolean'
|
||||
? parsed.hideInactiveBeads : DEFAULT_CONFIG.hideInactiveBeads,
|
||||
coloredNumerals: typeof parsed.coloredNumerals === 'boolean'
|
||||
? parsed.coloredNumerals : DEFAULT_CONFIG.coloredNumerals,
|
||||
scaleFactor: typeof parsed.scaleFactor === 'number' && parsed.scaleFactor > 0
|
||||
? parsed.scaleFactor : DEFAULT_CONFIG.scaleFactor
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load abacus config from localStorage:', error)
|
||||
}
|
||||
|
||||
return DEFAULT_CONFIG
|
||||
}
|
||||
|
||||
// Save config to localStorage
|
||||
function saveConfigToStorage(config: AbacusDisplayConfig): void {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(config))
|
||||
} catch (error) {
|
||||
console.warn('Failed to save abacus config to localStorage:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const AbacusDisplayContext = createContext<AbacusDisplayContextType | null>(null)
|
||||
|
||||
export function useAbacusDisplay() {
|
||||
const context = useContext(AbacusDisplayContext)
|
||||
if (!context) {
|
||||
throw new Error('useAbacusDisplay must be used within an AbacusDisplayProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
interface AbacusDisplayProviderProps {
|
||||
children: ReactNode
|
||||
initialConfig?: Partial<AbacusDisplayConfig>
|
||||
}
|
||||
|
||||
export function AbacusDisplayProvider({
|
||||
children,
|
||||
initialConfig = {}
|
||||
}: AbacusDisplayProviderProps) {
|
||||
const [config, setConfig] = useState<AbacusDisplayConfig>(() => {
|
||||
const stored = loadConfigFromStorage()
|
||||
return { ...stored, ...initialConfig }
|
||||
})
|
||||
|
||||
// Save to localStorage whenever config changes
|
||||
useEffect(() => {
|
||||
saveConfigToStorage(config)
|
||||
}, [config])
|
||||
|
||||
const updateConfig = useCallback((updates: Partial<AbacusDisplayConfig>) => {
|
||||
setConfig(prev => {
|
||||
const newConfig = { ...prev, ...updates }
|
||||
return newConfig
|
||||
})
|
||||
}, [])
|
||||
|
||||
const resetToDefaults = useCallback(() => {
|
||||
setConfig(DEFAULT_CONFIG)
|
||||
}, [])
|
||||
|
||||
const value: AbacusDisplayContextType = {
|
||||
config,
|
||||
updateConfig,
|
||||
resetToDefaults
|
||||
}
|
||||
|
||||
return (
|
||||
<AbacusDisplayContext.Provider value={value}>
|
||||
{children}
|
||||
</AbacusDisplayContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// Convenience hook for components that need specific config values
|
||||
export function useAbacusConfig() {
|
||||
const { config } = useAbacusDisplay()
|
||||
return config
|
||||
}
|
||||
@@ -40,6 +40,14 @@
|
||||
color: var(--colors-brand-600)
|
||||
}
|
||||
|
||||
.justify_space-between {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.gap_10px {
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
.fs_3xl {
|
||||
font-size: var(--font-sizes-3xl)
|
||||
}
|
||||
@@ -108,6 +116,10 @@
|
||||
padding-inline: var(--spacing-6)
|
||||
}
|
||||
|
||||
.py_4 {
|
||||
padding-block: var(--spacing-4)
|
||||
}
|
||||
|
||||
.bg_brand\.600 {
|
||||
background: var(--colors-brand-600)
|
||||
}
|
||||
@@ -230,10 +242,6 @@
|
||||
transition-duration: var(--transition-duration, 150ms)
|
||||
}
|
||||
|
||||
.py_4 {
|
||||
padding-block: var(--spacing-4)
|
||||
}
|
||||
|
||||
.pos_relative {
|
||||
position: relative
|
||||
}
|
||||
@@ -254,30 +262,6 @@
|
||||
padding-block: var(--spacing-8)
|
||||
}
|
||||
|
||||
.justify_space-between {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.gap_10px {
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
.justify_center {
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.items_center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.gap_3 {
|
||||
gap: var(--spacing-3)
|
||||
}
|
||||
|
||||
.flex_row {
|
||||
flex-direction: row
|
||||
}
|
||||
|
||||
.mb_8 {
|
||||
margin-bottom: var(--spacing-8)
|
||||
}
|
||||
@@ -298,10 +282,6 @@
|
||||
gap: var(--spacing-6)
|
||||
}
|
||||
|
||||
.d_flex {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.flex_column {
|
||||
flex-direction: column
|
||||
}
|
||||
@@ -326,6 +306,26 @@
|
||||
align-items: start
|
||||
}
|
||||
|
||||
.justify_center {
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.d_flex {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.items_center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.gap_3 {
|
||||
gap: var(--spacing-3)
|
||||
}
|
||||
|
||||
.flex_row {
|
||||
flex-direction: row
|
||||
}
|
||||
|
||||
.hover\:bg_brand\.700:is(:hover, [data-hover]) {
|
||||
background: var(--colors-brand-700)
|
||||
}
|
||||
@@ -346,7 +346,7 @@
|
||||
background: var(--colors-red-700)
|
||||
}
|
||||
@media screen and (min-width: 64em) {
|
||||
.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\) {
|
||||
.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr))
|
||||
}
|
||||
|
||||
|
||||
@@ -938,7 +938,7 @@
|
||||
color: var(--colors-gray-800)
|
||||
}
|
||||
@media screen and (min-width: 48em) {
|
||||
.md\:px_4,.md\:px_4,.md\:px_4,.md\:px_4 {
|
||||
.md\:px_4,.md\:px_4,.md\:px_4,.md\:px_4,.md\:px_4,.md\:px_4,.md\:px_4 {
|
||||
padding-inline: var(--spacing-4)
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,18 @@
|
||||
padding-block: var(--spacing-2)
|
||||
}
|
||||
|
||||
.justify_space-between {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.gap_10px {
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
.gap_3 {
|
||||
gap: var(--spacing-3)
|
||||
}
|
||||
|
||||
.bg_linear-gradient\(135deg\,_\#667eea_0\%\,_\#764ba2_100\%\) {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)
|
||||
}
|
||||
@@ -72,6 +84,10 @@
|
||||
background: var(--colors-transparent)
|
||||
}
|
||||
|
||||
.py_4 {
|
||||
padding-block: var(--spacing-4)
|
||||
}
|
||||
|
||||
.border-b_2px_solid {
|
||||
border-bottom: 2px solid
|
||||
}
|
||||
@@ -527,10 +543,6 @@
|
||||
height: 120px
|
||||
}
|
||||
|
||||
.py_4 {
|
||||
padding-block: var(--spacing-4)
|
||||
}
|
||||
|
||||
.max-w_4xl {
|
||||
max-width: var(--sizes-4xl)
|
||||
}
|
||||
@@ -559,18 +571,6 @@
|
||||
padding-block: var(--spacing-12)
|
||||
}
|
||||
|
||||
.justify_space-between {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.gap_10px {
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
.gap_3 {
|
||||
gap: var(--spacing-3)
|
||||
}
|
||||
|
||||
.gap_0 {
|
||||
gap: var(--spacing-0)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,18 @@
|
||||
font-weight: var(--font-weights-medium)
|
||||
}
|
||||
|
||||
.max-w_7xl {
|
||||
max-width: var(--sizes-7xl)
|
||||
}
|
||||
|
||||
.justify_space-between {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.gap_10px {
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
.leading_tight {
|
||||
line-height: var(--line-heights-tight)
|
||||
}
|
||||
@@ -162,10 +174,6 @@
|
||||
line-height: var(--line-heights-relaxed)
|
||||
}
|
||||
|
||||
.max-w_7xl {
|
||||
max-width: var(--sizes-7xl)
|
||||
}
|
||||
|
||||
.pos_relative {
|
||||
position: relative
|
||||
}
|
||||
@@ -182,12 +190,36 @@
|
||||
padding-inline: var(--spacing-4)
|
||||
}
|
||||
|
||||
.justify_space-between {
|
||||
justify-content: space-between
|
||||
.gap_12 {
|
||||
gap: var(--spacing-12)
|
||||
}
|
||||
|
||||
.gap_10px {
|
||||
gap: 10px
|
||||
.py_16 {
|
||||
padding-block: var(--spacing-16)
|
||||
}
|
||||
|
||||
.text_center {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.flex_column {
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.gap_6 {
|
||||
gap: var(--spacing-6)
|
||||
}
|
||||
|
||||
.max-w_4xl {
|
||||
max-width: var(--sizes-4xl)
|
||||
}
|
||||
|
||||
.d_flex {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.items_center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.justify_center {
|
||||
@@ -206,38 +238,6 @@
|
||||
margin-top: var(--spacing-8)
|
||||
}
|
||||
|
||||
.items_center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.gap_12 {
|
||||
gap: var(--spacing-12)
|
||||
}
|
||||
|
||||
.py_16 {
|
||||
padding-block: var(--spacing-16)
|
||||
}
|
||||
|
||||
.text_center {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.d_flex {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.flex_column {
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.gap_6 {
|
||||
gap: var(--spacing-6)
|
||||
}
|
||||
|
||||
.max-w_4xl {
|
||||
max-width: var(--sizes-4xl)
|
||||
}
|
||||
|
||||
.hover\:bg_brand\.50:is(:hover, [data-hover]) {
|
||||
background: var(--colors-brand-50)
|
||||
}
|
||||
@@ -279,10 +279,6 @@
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr))
|
||||
}
|
||||
|
||||
.md\:px_6 {
|
||||
padding-inline: var(--spacing-6)
|
||||
}
|
||||
|
||||
.md\:fs_6xl {
|
||||
font-size: var(--font-sizes-6xl)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
@layer utilities {
|
||||
|
||||
.px_3 {
|
||||
padding-inline: var(--spacing-3)
|
||||
}
|
||||
|
||||
.py_2 {
|
||||
padding-block: var(--spacing-2)
|
||||
}
|
||||
|
||||
.rounded_lg {
|
||||
border-radius: var(--radii-lg)
|
||||
}
|
||||
|
||||
.transform_rotate\(180deg\) {
|
||||
transform: rotate(180deg)
|
||||
}
|
||||
|
||||
.transform_rotate\(0deg\) {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
|
||||
.transition_transform {
|
||||
transition-property: var(--transition-prop, transform);
|
||||
transition-timing-function: var(--transition-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
||||
transition-duration: var(--transition-duration, 150ms)
|
||||
}
|
||||
|
||||
.rounded_xl {
|
||||
border-radius: var(--radii-xl)
|
||||
}
|
||||
|
||||
.shadow_modal {
|
||||
box-shadow: var(--shadows-modal)
|
||||
}
|
||||
|
||||
.border_1px_solid {
|
||||
border: 1px solid
|
||||
}
|
||||
|
||||
.border_gray\.200 {
|
||||
border-color: var(--colors-gray-200)
|
||||
}
|
||||
|
||||
.p_6 {
|
||||
padding: var(--spacing-6)
|
||||
}
|
||||
|
||||
.min-w_320px {
|
||||
min-width: 320px
|
||||
}
|
||||
|
||||
.max-w_400px {
|
||||
max-width: 400px
|
||||
}
|
||||
|
||||
.z_50 {
|
||||
z-index: 50
|
||||
}
|
||||
|
||||
.animation_fadeIn_0\.2s_ease-out {
|
||||
animation: fadeIn 0.2s ease-out
|
||||
}
|
||||
|
||||
.fs_lg {
|
||||
font-size: var(--font-sizes-lg)
|
||||
}
|
||||
|
||||
.font_semibold {
|
||||
font-weight: var(--font-weights-semibold)
|
||||
}
|
||||
|
||||
.fs_xs {
|
||||
font-size: var(--font-sizes-xs)
|
||||
}
|
||||
|
||||
.text_gray\.500 {
|
||||
color: var(--colors-gray-500)
|
||||
}
|
||||
|
||||
.text_gray\.600 {
|
||||
color: var(--colors-gray-600)
|
||||
}
|
||||
|
||||
.font_medium {
|
||||
font-weight: var(--font-weights-medium)
|
||||
}
|
||||
|
||||
.bg_brand\.600 {
|
||||
background: var(--colors-brand-600)
|
||||
}
|
||||
|
||||
.bg_gray\.300 {
|
||||
background: var(--colors-gray-300)
|
||||
}
|
||||
|
||||
.w_11 {
|
||||
width: var(--sizes-11)
|
||||
}
|
||||
|
||||
.h_6 {
|
||||
height: var(--sizes-6)
|
||||
}
|
||||
|
||||
.transform_translateX\(20px\) {
|
||||
transform: translateX(20px)
|
||||
}
|
||||
|
||||
.transform_translateX\(0px\) {
|
||||
transform: translateX(0px)
|
||||
}
|
||||
|
||||
.d_block {
|
||||
display: block
|
||||
}
|
||||
|
||||
.w_5 {
|
||||
width: var(--sizes-5)
|
||||
}
|
||||
|
||||
.h_5 {
|
||||
height: var(--sizes-5)
|
||||
}
|
||||
|
||||
.shadow_sm {
|
||||
box-shadow: var(--shadows-sm)
|
||||
}
|
||||
|
||||
.transition_transform_0\.2s {
|
||||
transition: transform 0.2s
|
||||
}
|
||||
|
||||
.will-change_transform {
|
||||
will-change: transform
|
||||
}
|
||||
|
||||
.w_4 {
|
||||
width: var(--sizes-4)
|
||||
}
|
||||
|
||||
.h_4 {
|
||||
height: var(--sizes-4)
|
||||
}
|
||||
|
||||
.rounded_full {
|
||||
border-radius: var(--radii-full)
|
||||
}
|
||||
|
||||
.border_2px_solid {
|
||||
border: 2px solid
|
||||
}
|
||||
|
||||
.border_gray\.300 {
|
||||
border-color: var(--colors-gray-300)
|
||||
}
|
||||
|
||||
.bg_white {
|
||||
background: var(--colors-white)
|
||||
}
|
||||
|
||||
.transition_all {
|
||||
transition-property: var(--transition-prop, all);
|
||||
transition-timing-function: var(--transition-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
||||
transition-duration: var(--transition-duration, 150ms)
|
||||
}
|
||||
.\[\&\[data-state\=checked\]\]\:border_brand\.600[data-state=checked] {
|
||||
border-color: var(--colors-brand-600)
|
||||
}
|
||||
|
||||
.justify_center {
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.w_full {
|
||||
width: var(--sizes-full)
|
||||
}
|
||||
|
||||
.h_full {
|
||||
height: var(--sizes-full)
|
||||
}
|
||||
|
||||
.pos_relative {
|
||||
position: relative
|
||||
}
|
||||
.after\:content_\"\"::after {
|
||||
content: ""
|
||||
}
|
||||
.after\:d_block::after {
|
||||
display: block
|
||||
}
|
||||
.after\:w_1\.5::after {
|
||||
width: var(--sizes-1\.5)
|
||||
}
|
||||
.after\:h_1\.5::after {
|
||||
height: var(--sizes-1\.5)
|
||||
}
|
||||
.after\:rounded_full::after {
|
||||
border-radius: var(--radii-full)
|
||||
}
|
||||
.after\:bg_brand\.600::after {
|
||||
background: var(--colors-brand-600)
|
||||
}
|
||||
|
||||
.fs_sm {
|
||||
font-size: var(--font-sizes-sm)
|
||||
}
|
||||
|
||||
.text_gray\.900 {
|
||||
color: var(--colors-gray-900)
|
||||
}
|
||||
|
||||
.cursor_pointer {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.gap_6 {
|
||||
gap: var(--spacing-6)
|
||||
}
|
||||
|
||||
.gap_1 {
|
||||
gap: var(--spacing-1)
|
||||
}
|
||||
|
||||
.gap_4 {
|
||||
gap: var(--spacing-4)
|
||||
}
|
||||
|
||||
.flex_column {
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.gap_2 {
|
||||
gap: var(--spacing-2)
|
||||
}
|
||||
|
||||
.justify_space-between {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.gap_10px {
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
.d_flex {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.items_center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.gap_3 {
|
||||
gap: var(--spacing-3)
|
||||
}
|
||||
|
||||
.flex_row {
|
||||
flex-direction: row
|
||||
}
|
||||
|
||||
.focus\:ring_2px:is(:focus, [data-focus]) {
|
||||
outline: 2px
|
||||
}
|
||||
|
||||
.focus\:ring_brand\.500:is(:focus, [data-focus]) {
|
||||
outline-color: var(--colors-brand-500)
|
||||
}
|
||||
|
||||
.focus\:ring_1px:is(:focus, [data-focus]) {
|
||||
outline-offset: 1px
|
||||
}
|
||||
|
||||
.hover\:bg_gray\.50:is(:hover, [data-hover]) {
|
||||
background: var(--colors-gray-50)
|
||||
}
|
||||
|
||||
.hover\:border_gray\.300:is(:hover, [data-hover]) {
|
||||
border-color: var(--colors-gray-300)
|
||||
}
|
||||
|
||||
.hover\:text_gray\.700:is(:hover, [data-hover]) {
|
||||
color: var(--colors-gray-700)
|
||||
}
|
||||
|
||||
.hover\:bg_brand\.700:is(:hover, [data-hover]) {
|
||||
background: var(--colors-brand-700)
|
||||
}
|
||||
|
||||
.hover\:bg_gray\.400:is(:hover, [data-hover]) {
|
||||
background: var(--colors-gray-400)
|
||||
}
|
||||
|
||||
.hover\:border_brand\.400:is(:hover, [data-hover]) {
|
||||
border-color: var(--colors-brand-400)
|
||||
}
|
||||
}
|
||||
190
apps/web/styled-system/chunks/src__components__AppNavBar.css
Normal file
190
apps/web/styled-system/chunks/src__components__AppNavBar.css
Normal file
@@ -0,0 +1,190 @@
|
||||
@layer utilities {
|
||||
|
||||
.pos_fixed {
|
||||
position: fixed
|
||||
}
|
||||
|
||||
.top_4 {
|
||||
top: var(--spacing-4)
|
||||
}
|
||||
|
||||
.right_4 {
|
||||
right: var(--spacing-4)
|
||||
}
|
||||
|
||||
.z_40 {
|
||||
z-index: 40
|
||||
}
|
||||
|
||||
.opacity_0\.8 {
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
.transition_opacity {
|
||||
transition-property: var(--transition-prop, opacity);
|
||||
transition-timing-function: var(--transition-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
||||
transition-duration: var(--transition-duration, 150ms)
|
||||
}
|
||||
|
||||
.bg_white {
|
||||
background: var(--colors-white)
|
||||
}
|
||||
|
||||
.shadow_sm {
|
||||
box-shadow: var(--shadows-sm)
|
||||
}
|
||||
|
||||
.border-b_1px_solid {
|
||||
border-bottom: 1px solid
|
||||
}
|
||||
|
||||
.border_gray\.200 {
|
||||
border-color: var(--colors-gray-200)
|
||||
}
|
||||
|
||||
.pos_sticky {
|
||||
position: sticky
|
||||
}
|
||||
|
||||
.top_0 {
|
||||
top: var(--spacing-0)
|
||||
}
|
||||
|
||||
.z_30 {
|
||||
z-index: 30
|
||||
}
|
||||
|
||||
.fs_xl {
|
||||
font-size: var(--font-sizes-xl)
|
||||
}
|
||||
|
||||
.font_bold {
|
||||
font-weight: var(--font-weights-bold)
|
||||
}
|
||||
|
||||
.text_brand\.800 {
|
||||
color: var(--colors-brand-800)
|
||||
}
|
||||
|
||||
.text_brand\.700 {
|
||||
color: var(--colors-brand-700)
|
||||
}
|
||||
|
||||
.text_gray\.600 {
|
||||
color: var(--colors-gray-600)
|
||||
}
|
||||
|
||||
.bg_brand\.50 {
|
||||
background: var(--colors-brand-50)
|
||||
}
|
||||
|
||||
.bg_transparent {
|
||||
background: var(--colors-transparent)
|
||||
}
|
||||
|
||||
.px_3 {
|
||||
padding-inline: var(--spacing-3)
|
||||
}
|
||||
|
||||
.py_2 {
|
||||
padding-block: var(--spacing-2)
|
||||
}
|
||||
|
||||
.fs_sm {
|
||||
font-size: var(--font-sizes-sm)
|
||||
}
|
||||
|
||||
.font_medium {
|
||||
font-weight: var(--font-weights-medium)
|
||||
}
|
||||
|
||||
.rounded_lg {
|
||||
border-radius: var(--radii-lg)
|
||||
}
|
||||
|
||||
.transition_all {
|
||||
transition-property: var(--transition-prop, all);
|
||||
transition-timing-function: var(--transition-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
||||
transition-duration: var(--transition-duration, 150ms)
|
||||
}
|
||||
|
||||
.text-decor_none {
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.gap_2 {
|
||||
gap: var(--spacing-2)
|
||||
}
|
||||
|
||||
.justify_space-between {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.gap_10px {
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
.gap_6 {
|
||||
gap: var(--spacing-6)
|
||||
}
|
||||
|
||||
.d_flex {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.items_center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.gap_4 {
|
||||
gap: var(--spacing-4)
|
||||
}
|
||||
|
||||
.flex_row {
|
||||
flex-direction: row
|
||||
}
|
||||
|
||||
.pos_relative {
|
||||
position: relative
|
||||
}
|
||||
|
||||
.max-w_7xl {
|
||||
max-width: var(--sizes-7xl)
|
||||
}
|
||||
|
||||
.mx_auto {
|
||||
margin-inline: auto
|
||||
}
|
||||
|
||||
.px_4 {
|
||||
padding-inline: var(--spacing-4)
|
||||
}
|
||||
|
||||
.py_3 {
|
||||
padding-block: var(--spacing-3)
|
||||
}
|
||||
|
||||
.hover\:opacity_1:is(:hover, [data-hover]) {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
.hover\:text_brand\.900:is(:hover, [data-hover]) {
|
||||
color: var(--colors-brand-900)
|
||||
}
|
||||
|
||||
.hover\:text_brand\.800:is(:hover, [data-hover]) {
|
||||
color: var(--colors-brand-800)
|
||||
}
|
||||
|
||||
.hover\:text_gray\.900:is(:hover, [data-hover]) {
|
||||
color: var(--colors-gray-900)
|
||||
}
|
||||
|
||||
.hover\:bg_brand\.100:is(:hover, [data-hover]) {
|
||||
background: var(--colors-brand-100)
|
||||
}
|
||||
|
||||
.hover\:bg_gray\.50:is(:hover, [data-hover]) {
|
||||
background: var(--colors-gray-50)
|
||||
}
|
||||
}
|
||||
@@ -321,10 +321,6 @@
|
||||
border: 3px solid #5f3dc4
|
||||
}
|
||||
|
||||
.opacity_0\.8 {
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
.backface_hidden {
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden
|
||||
@@ -483,10 +479,6 @@
|
||||
animation: blink 1s infinite
|
||||
}
|
||||
|
||||
.pos_fixed {
|
||||
position: fixed
|
||||
}
|
||||
|
||||
.left_0 {
|
||||
left: var(--spacing-0)
|
||||
}
|
||||
@@ -823,14 +815,6 @@
|
||||
space: y-2
|
||||
}
|
||||
|
||||
.pos_sticky {
|
||||
position: sticky
|
||||
}
|
||||
|
||||
.top_0 {
|
||||
top: var(--spacing-0)
|
||||
}
|
||||
|
||||
.bg_linear-gradient\(135deg\,_\#667eea_0\%\,_\#764ba2_100\%\) {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%)
|
||||
}
|
||||
@@ -847,10 +831,6 @@
|
||||
opacity: 0.95
|
||||
}
|
||||
|
||||
.border-b_1px_solid {
|
||||
border-bottom: 1px solid
|
||||
}
|
||||
|
||||
.border_transparent {
|
||||
border-color: var(--colors-transparent)
|
||||
}
|
||||
@@ -979,10 +959,6 @@
|
||||
color: var(--colors-red-700)
|
||||
}
|
||||
|
||||
.shadow_sm {
|
||||
box-shadow: var(--shadows-sm)
|
||||
}
|
||||
|
||||
.mb_6 {
|
||||
margin-bottom: var(--spacing-6)
|
||||
}
|
||||
@@ -1047,10 +1023,6 @@
|
||||
color: var(--colors-purple-600)
|
||||
}
|
||||
|
||||
.text-decor_none {
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.w_120 {
|
||||
width: 120px
|
||||
}
|
||||
@@ -1107,10 +1079,6 @@
|
||||
padding-inline: var(--spacing-8)
|
||||
}
|
||||
|
||||
.text_brand\.700 {
|
||||
color: var(--colors-brand-700)
|
||||
}
|
||||
|
||||
.border_brand\.200 {
|
||||
border-color: var(--colors-brand-200)
|
||||
}
|
||||
@@ -1131,22 +1099,10 @@
|
||||
margin-bottom: var(--spacing-4)
|
||||
}
|
||||
|
||||
.max-w_7xl {
|
||||
max-width: var(--sizes-7xl)
|
||||
}
|
||||
|
||||
.max-w_6xl {
|
||||
max-width: var(--sizes-6xl)
|
||||
}
|
||||
|
||||
.mx_auto {
|
||||
margin-inline: auto
|
||||
}
|
||||
|
||||
.mt_8 {
|
||||
margin-top: var(--spacing-8)
|
||||
}
|
||||
|
||||
.gap_12 {
|
||||
gap: var(--spacing-12)
|
||||
}
|
||||
@@ -1159,6 +1115,94 @@
|
||||
max-width: var(--sizes-4xl)
|
||||
}
|
||||
|
||||
.mt_8 {
|
||||
margin-top: var(--spacing-8)
|
||||
}
|
||||
|
||||
.transform_rotate\(180deg\) {
|
||||
transform: rotate(180deg)
|
||||
}
|
||||
|
||||
.transform_rotate\(0deg\) {
|
||||
transform: rotate(0deg)
|
||||
}
|
||||
|
||||
.transition_transform {
|
||||
transition-property: var(--transition-prop, transform);
|
||||
transition-timing-function: var(--transition-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
||||
transition-duration: var(--transition-duration, 150ms)
|
||||
}
|
||||
|
||||
.max-w_400px {
|
||||
max-width: 400px
|
||||
}
|
||||
|
||||
.animation_fadeIn_0\.2s_ease-out {
|
||||
animation: fadeIn 0.2s ease-out
|
||||
}
|
||||
|
||||
.after\:w_1\.5::after {
|
||||
width: var(--sizes-1\.5)
|
||||
}
|
||||
|
||||
.after\:h_1\.5::after {
|
||||
height: var(--sizes-1\.5)
|
||||
}
|
||||
|
||||
.pos_fixed {
|
||||
position: fixed
|
||||
}
|
||||
|
||||
.z_40 {
|
||||
z-index: 40
|
||||
}
|
||||
|
||||
.opacity_0\.8 {
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
.transition_opacity {
|
||||
transition-property: var(--transition-prop, opacity);
|
||||
transition-timing-function: var(--transition-easing, cubic-bezier(0.4, 0, 0.2, 1));
|
||||
transition-duration: var(--transition-duration, 150ms)
|
||||
}
|
||||
|
||||
.shadow_sm {
|
||||
box-shadow: var(--shadows-sm)
|
||||
}
|
||||
|
||||
.border-b_1px_solid {
|
||||
border-bottom: 1px solid
|
||||
}
|
||||
|
||||
.pos_sticky {
|
||||
position: sticky
|
||||
}
|
||||
|
||||
.top_0 {
|
||||
top: var(--spacing-0)
|
||||
}
|
||||
|
||||
.z_30 {
|
||||
z-index: 30
|
||||
}
|
||||
|
||||
.text_brand\.700 {
|
||||
color: var(--colors-brand-700)
|
||||
}
|
||||
|
||||
.text-decor_none {
|
||||
text-decoration: none
|
||||
}
|
||||
|
||||
.max-w_7xl {
|
||||
max-width: var(--sizes-7xl)
|
||||
}
|
||||
|
||||
.mx_auto {
|
||||
margin-inline: auto
|
||||
}
|
||||
|
||||
.align_start {
|
||||
align: start
|
||||
}
|
||||
@@ -1914,6 +1958,18 @@
|
||||
flex-direction: row
|
||||
}
|
||||
|
||||
.focus\:ring_2px:is(:focus, [data-focus]) {
|
||||
outline: 2px
|
||||
}
|
||||
|
||||
.focus\:ring_brand\.500:is(:focus, [data-focus]) {
|
||||
outline-color: var(--colors-brand-500)
|
||||
}
|
||||
|
||||
.focus\:ring_1px:is(:focus, [data-focus]) {
|
||||
outline-offset: 1px
|
||||
}
|
||||
|
||||
.focus\:ring_none:is(:focus, [data-focus]) {
|
||||
outline: var(--borders-none)
|
||||
}
|
||||
@@ -1950,10 +2006,6 @@
|
||||
background: var(--colors-blue-700)
|
||||
}
|
||||
|
||||
.hover\:bg_gray\.50:is(:hover, [data-hover]) {
|
||||
background: var(--colors-gray-50)
|
||||
}
|
||||
|
||||
.hover\:text_brand\.600:is(:hover, [data-hover]) {
|
||||
color: var(--colors-brand-600)
|
||||
}
|
||||
@@ -1970,6 +2022,34 @@
|
||||
transform: translateY(-4px)
|
||||
}
|
||||
|
||||
.hover\:border_gray\.300:is(:hover, [data-hover]) {
|
||||
border-color: var(--colors-gray-300)
|
||||
}
|
||||
|
||||
.hover\:text_gray\.700:is(:hover, [data-hover]) {
|
||||
color: var(--colors-gray-700)
|
||||
}
|
||||
|
||||
.hover\:opacity_1:is(:hover, [data-hover]) {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
.hover\:text_brand\.900:is(:hover, [data-hover]) {
|
||||
color: var(--colors-brand-900)
|
||||
}
|
||||
|
||||
.hover\:text_brand\.800:is(:hover, [data-hover]) {
|
||||
color: var(--colors-brand-800)
|
||||
}
|
||||
|
||||
.hover\:bg_brand\.100:is(:hover, [data-hover]) {
|
||||
background: var(--colors-brand-100)
|
||||
}
|
||||
|
||||
.hover\:bg_gray\.50:is(:hover, [data-hover]) {
|
||||
background: var(--colors-gray-50)
|
||||
}
|
||||
|
||||
.hover\:text_gray\.900:is(:hover, [data-hover]) {
|
||||
color: var(--colors-gray-900)
|
||||
}
|
||||
@@ -2035,7 +2115,7 @@
|
||||
}
|
||||
|
||||
@media screen and (min-width: 48em) {
|
||||
.md\:px_4,.md\:px_4,.md\:px_4,.md\:px_4 {
|
||||
.md\:px_4,.md\:px_4,.md\:px_4,.md\:px_4,.md\:px_4,.md\:px_4,.md\:px_4 {
|
||||
padding-inline: var(--spacing-4)
|
||||
}
|
||||
|
||||
@@ -2081,7 +2161,7 @@
|
||||
}
|
||||
|
||||
@media screen and (min-width: 64em) {
|
||||
.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\) {
|
||||
.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\),.lg\:grid-cols_repeat\(3\,_minmax\(0\,_1fr\)\) {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user