feat: create shared EditorComponents library for tutorial UI consistency
Add comprehensive shared component library containing: - EditorLayout: Consistent header/close/delete layout for all editors - FormGroup: Responsive grid layout for form elements - TextInput/NumberInput: Standardized form inputs with validation styling - CompactStepItem: Ultra-compact step display component for tight layouts - BetweenStepAdd: Elegant hover-based add functionality between steps - Button: Reusable button component with consistent styling - GridLayout: Flexible grid system for responsive layouts This establishes a unified design system across concept and practice step editors while dramatically reducing code duplication and improving maintainability. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
640
apps/web/src/components/tutorial/shared/EditorComponents.tsx
Normal file
640
apps/web/src/components/tutorial/shared/EditorComponents.tsx
Normal file
@@ -0,0 +1,640 @@
|
||||
'use client'
|
||||
|
||||
import { ReactNode, useState } from 'react'
|
||||
import { css } from '../../../../styled-system/css'
|
||||
import { hstack, vstack } from '../../../../styled-system/patterns'
|
||||
|
||||
// Shared input styles
|
||||
export const inputStyles = {
|
||||
w: 'full',
|
||||
px: 2,
|
||||
py: 1,
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
rounded: 'sm',
|
||||
fontSize: 'xs'
|
||||
} as const
|
||||
|
||||
export const labelStyles = {
|
||||
fontSize: 'xs',
|
||||
color: 'gray.600',
|
||||
mb: 1,
|
||||
display: 'block'
|
||||
} as const
|
||||
|
||||
// Shared Editor Layout Component - wraps entire editor with consistent styling
|
||||
interface EditorLayoutProps {
|
||||
title: string
|
||||
onClose: () => void
|
||||
onDelete?: () => void
|
||||
deleteLabel?: string
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function EditorLayout({ title, onClose, onDelete, deleteLabel = 'Delete', children, className }: EditorLayoutProps) {
|
||||
return (
|
||||
<div className={css({
|
||||
p: 3,
|
||||
bg: 'purple.50',
|
||||
border: '1px solid',
|
||||
borderColor: 'purple.200',
|
||||
rounded: 'lg',
|
||||
height: '100%',
|
||||
overflowY: 'auto'
|
||||
}, className)}>
|
||||
<div className={vstack({ gap: 3, alignItems: 'stretch' })}>
|
||||
{/* Header */}
|
||||
<EditorHeader
|
||||
title={title}
|
||||
onClose={onClose}
|
||||
onDelete={onDelete}
|
||||
deleteLabel={deleteLabel}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Shared Editor Header Component
|
||||
interface EditorHeaderProps {
|
||||
title: string
|
||||
onClose: () => void
|
||||
onDelete?: () => void
|
||||
deleteLabel?: string
|
||||
}
|
||||
|
||||
export function EditorHeader({ title, onClose, onDelete, deleteLabel = 'Delete' }: EditorHeaderProps) {
|
||||
return (
|
||||
<div className={hstack({ justifyContent: 'space-between', alignItems: 'center' })}>
|
||||
<h3 className={css({
|
||||
fontSize: 'md',
|
||||
fontWeight: 'semibold',
|
||||
color: 'purple.800'
|
||||
})}>
|
||||
{title}
|
||||
</h3>
|
||||
<div className={hstack({ gap: 1 })}>
|
||||
{onDelete && (
|
||||
<button
|
||||
onClick={onDelete}
|
||||
className={css({
|
||||
px: 2,
|
||||
py: 1,
|
||||
bg: 'red.500',
|
||||
color: 'white',
|
||||
rounded: 'sm',
|
||||
fontSize: 'xs',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'red.600' }
|
||||
})}
|
||||
>
|
||||
{deleteLabel}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={css({
|
||||
p: 1,
|
||||
borderRadius: 'sm',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'gray.100' }
|
||||
})}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Shared Field Component
|
||||
interface FieldProps {
|
||||
label: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function Field({ label, children }: FieldProps) {
|
||||
return (
|
||||
<div>
|
||||
<label className={css(labelStyles)}>
|
||||
{label}
|
||||
</label>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Shared Text Input Component
|
||||
interface TextInputProps {
|
||||
label: string
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
placeholder?: string
|
||||
multiline?: boolean
|
||||
rows?: number
|
||||
}
|
||||
|
||||
export function TextInput({ label, value, onChange, placeholder, multiline = false, rows = 2 }: TextInputProps) {
|
||||
return (
|
||||
<Field label={label}>
|
||||
{multiline ? (
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
rows={rows}
|
||||
className={css({
|
||||
...inputStyles,
|
||||
resize: 'none'
|
||||
})}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={css(inputStyles)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
// Shared Number Input Component
|
||||
interface NumberInputProps {
|
||||
label: string
|
||||
value: number | string
|
||||
onChange: (value: number) => void
|
||||
min?: number
|
||||
max?: number
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export function NumberInput({ label, value, onChange, min, max, placeholder }: NumberInputProps) {
|
||||
return (
|
||||
<Field label={label}>
|
||||
<input
|
||||
type="number"
|
||||
min={min}
|
||||
max={max}
|
||||
value={value}
|
||||
onChange={(e) => onChange(parseInt(e.target.value) || 0)}
|
||||
className={css(inputStyles)}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</Field>
|
||||
)
|
||||
}
|
||||
|
||||
// Shared Grid Layout Component
|
||||
interface GridLayoutProps {
|
||||
columns: 1 | 2 | 3 | 4
|
||||
gap?: number
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export function GridLayout({ columns, gap = 2, children }: GridLayoutProps) {
|
||||
const gridCols = {
|
||||
1: '1fr',
|
||||
2: '1fr 1fr',
|
||||
3: '1fr 1fr 1fr',
|
||||
4: '1fr 1fr 1fr 1fr'
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: gridCols[columns],
|
||||
gap: gap
|
||||
})}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Shared Section Component
|
||||
interface SectionProps {
|
||||
title?: string
|
||||
children: ReactNode
|
||||
collapsible?: boolean
|
||||
defaultOpen?: boolean
|
||||
background?: 'white' | 'gray' | 'none'
|
||||
}
|
||||
|
||||
export function Section({ title, children, background = 'white' }: SectionProps) {
|
||||
const bgStyles = {
|
||||
white: { p: 2, bg: 'white', border: '1px solid', borderColor: 'gray.200', rounded: 'md' },
|
||||
gray: { p: 2, bg: 'gray.50', border: '1px solid', borderColor: 'gray.200', rounded: 'sm' },
|
||||
none: {}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={css(bgStyles[background])}>
|
||||
{title && (
|
||||
<h4 className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
mb: 2,
|
||||
color: 'gray.800'
|
||||
})}>
|
||||
{title}
|
||||
</h4>
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Shared Form Group Component - handles common form layouts
|
||||
interface FormGroupProps {
|
||||
children: ReactNode
|
||||
columns?: 1 | 2 | 3
|
||||
gap?: number
|
||||
}
|
||||
|
||||
export function FormGroup({ children, columns = 1, gap = 2 }: FormGroupProps) {
|
||||
if (columns === 1) {
|
||||
return (
|
||||
<div className={vstack({ gap: gap, alignItems: 'stretch' })}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<GridLayout columns={columns} gap={gap}>
|
||||
{children}
|
||||
</GridLayout>
|
||||
)
|
||||
}
|
||||
|
||||
// Shared Compact Step Item Component
|
||||
interface CompactStepItemProps {
|
||||
type: 'concept' | 'practice'
|
||||
index: number
|
||||
title: string
|
||||
subtitle?: string
|
||||
description?: string
|
||||
isSelected?: boolean
|
||||
hasErrors?: boolean
|
||||
hasWarnings?: boolean
|
||||
errorCount?: number
|
||||
warningCount?: number
|
||||
onClick?: () => void
|
||||
onPreview?: () => void
|
||||
onDelete?: () => void
|
||||
children?: ReactNode
|
||||
// Hover-add functionality
|
||||
onAddStepBefore?: () => void
|
||||
onAddPracticeStepBefore?: () => void
|
||||
onAddStepAfter?: () => void
|
||||
onAddPracticeStepAfter?: () => void
|
||||
}
|
||||
|
||||
export function CompactStepItem({
|
||||
type,
|
||||
index,
|
||||
title,
|
||||
subtitle,
|
||||
description,
|
||||
isSelected = false,
|
||||
hasErrors = false,
|
||||
hasWarnings = false,
|
||||
errorCount = 0,
|
||||
warningCount = 0,
|
||||
onClick,
|
||||
onPreview,
|
||||
onDelete,
|
||||
children,
|
||||
onAddStepBefore,
|
||||
onAddPracticeStepBefore,
|
||||
onAddStepAfter,
|
||||
onAddPracticeStepAfter
|
||||
}: CompactStepItemProps) {
|
||||
const getBorderColor = () => {
|
||||
if (isSelected) return 'blue.500'
|
||||
if (hasErrors) return 'red.300'
|
||||
return 'gray.200'
|
||||
}
|
||||
|
||||
const getBgColor = () => {
|
||||
if (isSelected) return 'blue.50'
|
||||
if (hasWarnings) return 'yellow.50'
|
||||
if (hasErrors) return 'red.50'
|
||||
return 'white'
|
||||
}
|
||||
|
||||
const getHoverBg = () => {
|
||||
return isSelected ? 'blue.100' : 'gray.50'
|
||||
}
|
||||
|
||||
const typeIcon = type === 'concept' ? '📝' : '🎯'
|
||||
const typeLabel = type === 'concept' ? 'Step' : 'Practice'
|
||||
|
||||
const hasAddActions = onAddStepBefore || onAddPracticeStepBefore || onAddStepAfter || onAddPracticeStepAfter
|
||||
|
||||
return (
|
||||
<div className={css({ position: 'relative' })}>
|
||||
{/* Main step item */}
|
||||
<div
|
||||
onClick={onClick}
|
||||
className={css({
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
border: '1px solid',
|
||||
borderColor: getBorderColor(),
|
||||
borderRadius: 'sm',
|
||||
bg: getBgColor(),
|
||||
cursor: onClick ? 'pointer' : 'default',
|
||||
_hover: onClick ? { bg: getHoverBg() } : {}
|
||||
})}
|
||||
>
|
||||
<div className={hstack({ justifyContent: 'space-between', alignItems: 'center', gap: 2 })}>
|
||||
<div className={css({ flex: 1, minWidth: 0 })}>
|
||||
{/* Inline header: badges, title, and subtitle on same line when possible */}
|
||||
<div className={hstack({ gap: 1.5, alignItems: 'center', flexWrap: 'wrap' })}>
|
||||
<span className={css({
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'bold',
|
||||
color: type === 'concept' ? 'blue.800' : 'purple.800',
|
||||
bg: type === 'concept' ? 'blue.100' : 'purple.100',
|
||||
px: 0.5,
|
||||
py: 0.5,
|
||||
borderRadius: 'xs',
|
||||
flexShrink: 0
|
||||
})}>
|
||||
{typeIcon} {typeLabel} {index + 1}
|
||||
</span>
|
||||
|
||||
{/* Title inline */}
|
||||
<div className={css({
|
||||
fontWeight: 'medium',
|
||||
fontSize: 'xs',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1,
|
||||
minWidth: 0
|
||||
})}>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
{/* Error/warning badges - compact */}
|
||||
{hasErrors && (
|
||||
<span className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'red.600',
|
||||
bg: 'red.100',
|
||||
px: 1,
|
||||
py: 0.5,
|
||||
borderRadius: 'xs',
|
||||
flexShrink: 0
|
||||
})}>
|
||||
{errorCount}❌
|
||||
</span>
|
||||
)}
|
||||
|
||||
{hasWarnings && (
|
||||
<span className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'yellow.600',
|
||||
bg: 'yellow.100',
|
||||
px: 1,
|
||||
py: 0.5,
|
||||
borderRadius: 'xs',
|
||||
flexShrink: 0
|
||||
})}>
|
||||
{warningCount}⚠️
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Subtitle on separate line if provided */}
|
||||
{subtitle && (
|
||||
<div className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'gray.600',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
mt: 0.5
|
||||
})}>
|
||||
{subtitle}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom children content */}
|
||||
{children}
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className={hstack({ gap: 1, flexShrink: 0 })}>
|
||||
{onPreview && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onPreview()
|
||||
}}
|
||||
className={css({
|
||||
p: 1,
|
||||
bg: 'blue.100',
|
||||
color: 'blue.700',
|
||||
border: '1px solid',
|
||||
borderColor: 'blue.300',
|
||||
borderRadius: 'sm',
|
||||
fontSize: 'xs',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'blue.200' }
|
||||
})}
|
||||
title="Preview"
|
||||
>
|
||||
👁
|
||||
</button>
|
||||
)}
|
||||
|
||||
{onDelete && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onDelete()
|
||||
}}
|
||||
className={css({
|
||||
p: 1,
|
||||
bg: 'red.100',
|
||||
color: 'red.700',
|
||||
border: '1px solid',
|
||||
borderColor: 'red.300',
|
||||
borderRadius: 'sm',
|
||||
fontSize: 'xs',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'red.200' }
|
||||
})}
|
||||
title="Delete"
|
||||
>
|
||||
🗑
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Between-step hover area with dropdown
|
||||
interface BetweenStepAddProps {
|
||||
onAddStep: () => void
|
||||
onAddPracticeStep: () => void
|
||||
}
|
||||
|
||||
export function BetweenStepAdd({ onAddStep, onAddPracticeStep }: BetweenStepAddProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className={css({
|
||||
position: 'relative',
|
||||
height: '16px',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
'&:hover .add-button': {
|
||||
opacity: 0.5
|
||||
}
|
||||
})}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className={`${css({
|
||||
px: 2,
|
||||
py: 0.5,
|
||||
bg: 'gray.100',
|
||||
color: 'gray.600',
|
||||
border: '1px dashed',
|
||||
borderColor: 'gray.300',
|
||||
borderRadius: 'sm',
|
||||
fontSize: 'xs',
|
||||
cursor: 'pointer',
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.2s ease',
|
||||
zIndex: 30,
|
||||
position: 'relative',
|
||||
_hover: {
|
||||
opacity: '1 !important',
|
||||
bg: 'gray.200',
|
||||
borderColor: 'gray.400'
|
||||
}
|
||||
})} add-button`}
|
||||
>
|
||||
+ New
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className={css({
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
mt: 1,
|
||||
bg: 'white',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
borderRadius: 'md',
|
||||
shadow: 'md',
|
||||
zIndex: 20,
|
||||
minW: '150px'
|
||||
})}>
|
||||
<button
|
||||
onClick={() => {
|
||||
onAddStep()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={css({
|
||||
w: 'full',
|
||||
px: 3,
|
||||
py: 2,
|
||||
textAlign: 'left',
|
||||
fontSize: 'sm',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'blue.50', color: 'blue.700' },
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'gray.100'
|
||||
})}
|
||||
>
|
||||
📝 Concept Step
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onAddPracticeStep()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={css({
|
||||
w: 'full',
|
||||
px: 3,
|
||||
py: 2,
|
||||
textAlign: 'left',
|
||||
fontSize: 'sm',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'purple.50', color: 'purple.700' }
|
||||
})}
|
||||
>
|
||||
🎯 Problem Page
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Shared Button Component
|
||||
interface ButtonProps {
|
||||
children: ReactNode
|
||||
onClick: () => void
|
||||
variant?: 'primary' | 'secondary' | 'outline'
|
||||
size?: 'xs' | 'sm' | 'md'
|
||||
disabled?: boolean
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
onClick,
|
||||
variant = 'secondary',
|
||||
size = 'sm',
|
||||
disabled = false,
|
||||
title
|
||||
}: ButtonProps) {
|
||||
const variantStyles = {
|
||||
primary: { bg: 'blue.500', color: 'white', _hover: { bg: 'blue.600' } },
|
||||
secondary: { bg: 'blue.100', color: 'blue.800', border: '1px solid', borderColor: 'blue.300', _hover: { bg: 'blue.200' } },
|
||||
outline: { bg: 'gray.100', color: 'gray.700', border: '1px solid', borderColor: 'gray.300', _hover: { bg: 'gray.200' } }
|
||||
}
|
||||
|
||||
const sizeStyles = {
|
||||
xs: { px: 1, py: 1, fontSize: 'xs' },
|
||||
sm: { px: 2, py: 1, fontSize: 'xs' },
|
||||
md: { px: 3, py: 2, fontSize: 'sm' }
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
className={css({
|
||||
...variantStyles[variant],
|
||||
...sizeStyles[size],
|
||||
rounded: 'sm',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
textAlign: 'center'
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
@layer utilities {
|
||||
|
||||
.p_3 {
|
||||
padding: var(--spacing-3)
|
||||
}
|
||||
|
||||
.bg_purple\.50 {
|
||||
background: var(--colors-purple-50)
|
||||
}
|
||||
|
||||
.border_purple\.200 {
|
||||
border-color: var(--colors-purple-200)
|
||||
}
|
||||
|
||||
.rounded_lg {
|
||||
border-radius: var(--radii-lg)
|
||||
}
|
||||
|
||||
.h_100\% {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.overflow-y_auto {
|
||||
overflow-y: auto
|
||||
}
|
||||
|
||||
.bottom_-8px {
|
||||
bottom: -8px
|
||||
}
|
||||
|
||||
.left_0 {
|
||||
left: var(--spacing-0)
|
||||
}
|
||||
|
||||
.right_0 {
|
||||
right: var(--spacing-0)
|
||||
}
|
||||
|
||||
.z_10 {
|
||||
z-index: 10
|
||||
}
|
||||
|
||||
.text_purple\.700 {
|
||||
color: var(--colors-purple-700)
|
||||
}
|
||||
|
||||
.border_none {
|
||||
border: var(--borders-none)
|
||||
}
|
||||
|
||||
.fs_md {
|
||||
font-size: var(--font-sizes-md)
|
||||
}
|
||||
|
||||
.font_semibold {
|
||||
font-weight: var(--font-weights-semibold)
|
||||
}
|
||||
|
||||
.bg_red\.500 {
|
||||
background: var(--colors-red-500)
|
||||
}
|
||||
|
||||
.text_white {
|
||||
color: var(--colors-white)
|
||||
}
|
||||
|
||||
.mb_1 {
|
||||
margin-bottom: var(--spacing-1)
|
||||
}
|
||||
|
||||
.d_block {
|
||||
display: block
|
||||
}
|
||||
|
||||
.resize_none {
|
||||
resize: none
|
||||
}
|
||||
|
||||
.d_grid {
|
||||
display: grid
|
||||
}
|
||||
|
||||
.p_2 {
|
||||
padding: var(--spacing-2)
|
||||
}
|
||||
|
||||
.mb_2 {
|
||||
margin-bottom: var(--spacing-2)
|
||||
}
|
||||
|
||||
.text_gray\.800 {
|
||||
color: var(--colors-gray-800)
|
||||
}
|
||||
|
||||
.cursor_default {
|
||||
cursor: default
|
||||
}
|
||||
|
||||
.px_1\.5 {
|
||||
padding-inline: var(--spacing-1\.5)
|
||||
}
|
||||
|
||||
.text_purple\.800 {
|
||||
color: var(--colors-purple-800)
|
||||
}
|
||||
|
||||
.bg_purple\.100 {
|
||||
background: var(--colors-purple-100)
|
||||
}
|
||||
|
||||
.font_bold {
|
||||
font-weight: var(--font-weights-bold)
|
||||
}
|
||||
|
||||
.px_0\.5 {
|
||||
padding-inline: var(--spacing-0\.5)
|
||||
}
|
||||
|
||||
.font_medium {
|
||||
font-weight: var(--font-weights-medium)
|
||||
}
|
||||
|
||||
.flex_1 {
|
||||
flex: 1 1 0%
|
||||
}
|
||||
|
||||
.min-w_0 {
|
||||
min-width: var(--sizes-0)
|
||||
}
|
||||
|
||||
.text_red\.600 {
|
||||
color: var(--colors-red-600)
|
||||
}
|
||||
|
||||
.text_yellow\.600 {
|
||||
color: var(--colors-yellow-600)
|
||||
}
|
||||
|
||||
.bg_yellow\.100 {
|
||||
background: var(--colors-yellow-100)
|
||||
}
|
||||
|
||||
.px_1 {
|
||||
padding-inline: var(--spacing-1)
|
||||
}
|
||||
|
||||
.rounded_xs {
|
||||
border-radius: var(--radii-xs)
|
||||
}
|
||||
|
||||
.overflow_hidden {
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.text_ellipsis {
|
||||
text-overflow: ellipsis
|
||||
}
|
||||
|
||||
.white-space_nowrap {
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.mt_0\.5 {
|
||||
margin-top: var(--spacing-0\.5)
|
||||
}
|
||||
|
||||
.text_blue\.700 {
|
||||
color: var(--colors-blue-700)
|
||||
}
|
||||
|
||||
.p_1 {
|
||||
padding: var(--spacing-1)
|
||||
}
|
||||
|
||||
.bg_red\.100 {
|
||||
background: var(--colors-red-100)
|
||||
}
|
||||
|
||||
.text_red\.700 {
|
||||
color: var(--colors-red-700)
|
||||
}
|
||||
|
||||
.border_red\.300 {
|
||||
border-color: var(--colors-red-300)
|
||||
}
|
||||
|
||||
.h_16px {
|
||||
height: 16px
|
||||
}
|
||||
|
||||
.justify_center {
|
||||
justify-content: center
|
||||
}
|
||||
|
||||
.py_0\.5 {
|
||||
padding-block: var(--spacing-0\.5)
|
||||
}
|
||||
|
||||
.bg_gray\.100 {
|
||||
background: var(--colors-gray-100)
|
||||
}
|
||||
|
||||
.text_gray\.600 {
|
||||
color: var(--colors-gray-600)
|
||||
}
|
||||
|
||||
.border_1px_dashed {
|
||||
border: 1px dashed
|
||||
}
|
||||
|
||||
.border_gray\.300 {
|
||||
border-color: var(--colors-gray-300)
|
||||
}
|
||||
|
||||
.opacity_0 {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
.transition_opacity_0\.2s_ease {
|
||||
transition: opacity 0.2s ease
|
||||
}
|
||||
|
||||
.z_30 {
|
||||
z-index: 30
|
||||
}
|
||||
|
||||
.pos_relative {
|
||||
position: relative
|
||||
}
|
||||
|
||||
.pos_absolute {
|
||||
position: absolute
|
||||
}
|
||||
|
||||
.top_100\% {
|
||||
top: 100%
|
||||
}
|
||||
|
||||
.left_50\% {
|
||||
left: 50%
|
||||
}
|
||||
|
||||
.transform_translateX\(-50\%\) {
|
||||
transform: translateX(-50%)
|
||||
}
|
||||
|
||||
.mt_1 {
|
||||
margin-top: var(--spacing-1)
|
||||
}
|
||||
|
||||
.bg_white {
|
||||
background: var(--colors-white)
|
||||
}
|
||||
|
||||
.border_gray\.200 {
|
||||
border-color: var(--colors-gray-200)
|
||||
}
|
||||
|
||||
.rounded_md {
|
||||
border-radius: var(--radii-md)
|
||||
}
|
||||
|
||||
.shadow_md {
|
||||
box-shadow: var(--shadows-md)
|
||||
}
|
||||
|
||||
.z_20 {
|
||||
z-index: 20
|
||||
}
|
||||
|
||||
.min-w_150px {
|
||||
min-width: 150px
|
||||
}
|
||||
|
||||
.border-b_1px_solid {
|
||||
border-bottom: 1px solid
|
||||
}
|
||||
|
||||
.border_gray\.100 {
|
||||
border-color: var(--colors-gray-100)
|
||||
}
|
||||
|
||||
.w_full {
|
||||
width: var(--sizes-full)
|
||||
}
|
||||
|
||||
.px_3 {
|
||||
padding-inline: var(--spacing-3)
|
||||
}
|
||||
|
||||
.py_2 {
|
||||
padding-block: var(--spacing-2)
|
||||
}
|
||||
|
||||
.text_left {
|
||||
text-align: left
|
||||
}
|
||||
|
||||
.fs_sm {
|
||||
font-size: var(--font-sizes-sm)
|
||||
}
|
||||
|
||||
.cursor_not-allowed {
|
||||
cursor: not-allowed
|
||||
}
|
||||
|
||||
.cursor_pointer {
|
||||
cursor: pointer
|
||||
}
|
||||
|
||||
.opacity_0\.5 {
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
.opacity_1 {
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
.bg_blue\.100 {
|
||||
background: var(--colors-blue-100)
|
||||
}
|
||||
|
||||
.text_blue\.800 {
|
||||
color: var(--colors-blue-800)
|
||||
}
|
||||
|
||||
.border_1px_solid {
|
||||
border: 1px solid
|
||||
}
|
||||
|
||||
.border_blue\.300 {
|
||||
border-color: var(--colors-blue-300)
|
||||
}
|
||||
|
||||
.px_2 {
|
||||
padding-inline: var(--spacing-2)
|
||||
}
|
||||
|
||||
.py_1 {
|
||||
padding-block: var(--spacing-1)
|
||||
}
|
||||
|
||||
.fs_xs {
|
||||
font-size: var(--font-sizes-xs)
|
||||
}
|
||||
|
||||
.rounded_sm {
|
||||
border-radius: var(--radii-sm)
|
||||
}
|
||||
|
||||
.text_center {
|
||||
text-align: center
|
||||
}
|
||||
|
||||
.columns_1 {
|
||||
columns: 1
|
||||
}
|
||||
|
||||
.gap_3 {
|
||||
gap: var(--spacing-3)
|
||||
}
|
||||
|
||||
.items_stretch {
|
||||
align-items: stretch
|
||||
}
|
||||
|
||||
.flex_column {
|
||||
flex-direction: column
|
||||
}
|
||||
|
||||
.gap_10px {
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
.justify_space-between {
|
||||
justify-content: space-between
|
||||
}
|
||||
|
||||
.gap_2 {
|
||||
gap: var(--spacing-2)
|
||||
}
|
||||
|
||||
.gap_1\.5 {
|
||||
gap: var(--spacing-1\.5)
|
||||
}
|
||||
|
||||
.flex-wrap_wrap {
|
||||
flex-wrap: wrap
|
||||
}
|
||||
|
||||
.d_flex {
|
||||
display: flex
|
||||
}
|
||||
|
||||
.items_center {
|
||||
align-items: center
|
||||
}
|
||||
|
||||
.gap_1 {
|
||||
gap: var(--spacing-1)
|
||||
}
|
||||
|
||||
.flex_row {
|
||||
flex-direction: row
|
||||
}
|
||||
|
||||
.shrink_0 {
|
||||
flex-shrink: 0
|
||||
}
|
||||
|
||||
.hover\:opacity_1\!:is(:hover, [data-hover]) {
|
||||
opacity: 1 !important
|
||||
}
|
||||
|
||||
.hover\:bg_gray\.200:is(:hover, [data-hover]) {
|
||||
background: var(--colors-gray-200)
|
||||
}
|
||||
|
||||
.hover\:border_gray\.400:is(:hover, [data-hover]) {
|
||||
border-color: var(--colors-gray-400)
|
||||
}
|
||||
|
||||
.hover\:bg_blue\.50:is(:hover, [data-hover]) {
|
||||
background: var(--colors-blue-50)
|
||||
}
|
||||
|
||||
.hover\:text_blue\.700:is(:hover, [data-hover]) {
|
||||
color: var(--colors-blue-700)
|
||||
}
|
||||
|
||||
.hover\:bg_purple\.50:is(:hover, [data-hover]) {
|
||||
background: var(--colors-purple-50)
|
||||
}
|
||||
|
||||
.hover\:text_purple\.700:is(:hover, [data-hover]) {
|
||||
color: var(--colors-purple-700)
|
||||
}
|
||||
|
||||
.hover\:bg_red\.600:is(:hover, [data-hover]) {
|
||||
background: var(--colors-red-600)
|
||||
}
|
||||
|
||||
.hover\:bg_gray\.100:is(:hover, [data-hover]) {
|
||||
background: var(--colors-gray-100)
|
||||
}
|
||||
|
||||
.hover\:bg_red\.200:is(:hover, [data-hover]) {
|
||||
background: var(--colors-red-200)
|
||||
}
|
||||
|
||||
.hover\:bg_purple\.200:is(:hover, [data-hover]) {
|
||||
background: var(--colors-purple-200)
|
||||
}
|
||||
|
||||
.hover\:bg_blue\.200:is(:hover, [data-hover]) {
|
||||
background: var(--colors-blue-200)
|
||||
}
|
||||
.\[\&\:hover_\.add-button\]\:opacity_0\.5:hover .add-button {
|
||||
opacity: 0.5
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user