Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/cn-utility.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: 'cn utility: lint, type-check, test, build'

on:
pull_request:
branches: [main, dev]
push:
branches: [main, dev]

concurrency:
group: '${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}'
cancel-in-progress: true

jobs:
quality:
name: Code Quality & Build
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Bun runtime
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies (locked versions)
run: bun install --frozen-lockfile

- name: Format check & lint (Biome)
run: bunx biome check . --diagnostic-level=warn

- name: Type check (TypeScript)
run: bunx tsc --noEmit

- name: Run unit tests with coverage
run: bunx vitest run --coverage

- name: Build extension (Chrome)
run: bun run build
24 changes: 24 additions & 0 deletions .github/workflows/mirror-to-gitlab.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
name: πŸ” Mirror to GitLab

on:
push:
branches: ['**']
tags: ['**']
delete:
branches: ['**']
tags: ['**']

jobs:
mirror:
runs-on: ubuntu-latest
steps:
- name: Checkout with full history
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Push to GitLab
run: |
git remote add gitlab https://oauth2:${{ secrets.GITLAB_TOKEN }}@gitlab.com/${{ secrets.GITLAB_USERNAME }}/${{ github.event.repository.name }}.git
git push gitlab --all --force
git push gitlab --tags --force
66 changes: 65 additions & 1 deletion src/components/button/button.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,79 @@
import { Icon } from '@/src/icons'
import { cn } from '@/src/utils/cn'
import type React from 'react'

/**
* Props interface for the Button component.
*
* Defines all configurable aspects of button rendering: sizing, styling,
* interaction state, and content. Follows semantic HTML conventions for
* accessibility and compatibility with form submission workflows.
*/
interface ButtonProps {
/** Callback fired when the button is clicked */
onClick?: () => void
/** Disables user interaction and updates visual state */
disabled?: boolean
/** Additional CSS classes merged with default styles (overrides via Tailwind merge) */
className?: string
/** Inline CSS styles (lowest precedence, for special cases only) */
style?: React.CSSProperties
/** Optional icon element displayed alongside or instead of text */
icon?: React.ReactNode
/** If true, replaces children with loading indicator and optional text */
loading?: boolean
/** Text or element shown during loading state (defaults to spinner + Persian "Ψ΅Ψ¨Ψ± Ϊ©Ω†ΫŒΨ―...") */
loadingText?: React.ReactNode
/** HTML button type attribute for form semantics */
type?: 'button' | 'submit' | 'reset'
/** If true, button width expands to fill its container */
fullWidth?: boolean
/** Border radius scale: 'sm' (0.375rem), 'md' (0.5rem), 'lg' (0.75rem), 'xl' (1rem), 'full' (9999px) */
rounded?: 'sm' | 'md' | 'lg' | 'xl' | 'full'
/** Button contentβ€”text, nodes, or components */
children?: React.ReactNode
/** If true, applies primary theme colors (blue background, white text) */
isPrimary?: boolean
/** Controls button dimensions: 'xs' (24px), 'sm' (32px), 'md' (40px), 'lg' (48px), 'xl' (56px) */
size: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
/** React ref for imperative access (e.g., .focus(), .blur()) */
ref?: any
}

/**
* A reusable button component with flexible sizing, theming, and states.
*
* This component wraps a native HTML `<button>` with Tailwind-based styling,
* loading states, and configurable appearance. The `cn()` utility ensures
* that user-provided className overrides are properly merged with built-in
* styles without Tailwind conflicts (e.g., duplicate spacing, color utilities).
*
* Accessibility: Use `disabled` prop to disable the button; avoid disabling
* via className alone as screen readers rely on the HTML attribute.
*
* @example
* ```tsx
* <Button size="md" onClick={handleClick}>
* Click me
* </Button>
*
* <Button
* size="lg"
* isPrimary
* fullWidth
* loading={isSubmitting}
* loadingText="Submitting..."
* type="submit"
* >
* Submit Form
* </Button>
* ```
*/
export function Button(prop: ButtonProps) {
/**
* Maps size prop to Tailwind height and padding utility tokens.
* These classes are combined via `cn()` to avoid conflicts.
*/
const sizes: Record<string, string> = {
xs: 'btn-xs',
sm: 'btn-sm',
Expand All @@ -31,7 +87,15 @@ export function Button(prop: ButtonProps) {
type={prop.type || 'button'}
onClick={prop.onClick}
disabled={prop.disabled}
className={`btn cursor-pointer ${prop.fullWidth ? 'full-width' : ''} ${prop.className} ${prop.rounded ? `rounded-${prop.rounded}` : ''} ${prop.isPrimary ? 'btn-primary text-white' : ''} ${sizes[prop.size] || 'btn-md'} active:!translate-y-0`}
className={cn(
'btn cursor-pointer',
prop.fullWidth && 'full-width',
prop.rounded && `rounded-${prop.rounded}`,
prop.isPrimary && 'btn-primary text-white',
sizes[prop.size] || 'btn-md',
'active:!translate-y-0',
prop.className
)}
style={prop.style}
ref={prop.ref}
>
Expand Down
77 changes: 72 additions & 5 deletions src/components/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,58 @@
import { cn } from '@/src/utils/cn'
import { memo } from 'react'

/**
* Props for the CustomCheckbox component.
*
* Provides granular control over styling, state management, and interaction
* handlers. Supports both controlled and uncontrolled patterns via onChange.
*/
interface CustomCheckboxProps {
/** Whether the checkbox is currently checked (controlled prop) */
checked: boolean
/** Fired when the user toggles the checkbox (controlled mode) */
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
/** Label text displayed next to the checkbox */
label?: string
/** Click handler for custom interaction logic (fires after onChange if not disabled) */
onClick?: (e: React.MouseEvent<HTMLInputElement>) => void
/** Additional CSS classes merged with computed styles via cn() */
className?: string
/** Disables interaction and dims visual appearance */
disabled?: boolean
/** CSS classes applied when checkbox is unchecked (overrides default border-content) */
unCheckedCheckBoxClassName?: string
/** CSS classes applied when checkbox is checked (overrides default blue background) */
checkedCheckBoxClassName?: string
/** Font weight for label text: 'font-light', 'font-normal', or 'font-bold' */
fontSize?: 'font-light' | 'font-normal' | 'font-bold'
}

/**
* A styled, accessible checkbox component built with HTML5 `<input type="checkbox">`.
*
* Features:
* - Animated checkmark that scales in/out on toggle
* - Customizable checked/unchecked styles via className props
* - Disabled state prevents interaction and changes cursor
* - Memoized to prevent unnecessary re-renders
* - Uses semantic `<label>` element for better accessibility and click target
*
* The `cn()` utility merges provided className overrides with computed state-based
* styles, ensuring no conflicting Tailwind utilities (e.g., border colors).
*
* @example
* ```tsx
* const [isChecked, setIsChecked] = useState(false)
*
* <CustomCheckbox
* checked={isChecked}
* onChange={(e) => setIsChecked(e.target.checked)}
* label="I agree to the terms"
* fontSize="font-semibold"
* />
* ```
*/
const CustomCheckbox = ({
checked,
onChange,
Expand All @@ -23,6 +64,12 @@ const CustomCheckbox = ({
checkedCheckBoxClassName = '',
onClick,
}: CustomCheckboxProps) => {
/**
* Determines the appropriate Tailwind classes for the checkbox box
* based on checked state and user-provided overrides.
*
* Priority: checkedCheckBoxClassName > default checked style
*/
const getCheckboxStyle = () => {
if (checked) {
if (checkedCheckBoxClassName) return checkedCheckBoxClassName
Expand All @@ -33,13 +80,21 @@ const CustomCheckbox = ({
return 'border-content'
}

/**
* Wrapper for onChange that prevents default browser behavior
* and respects the disabled state.
*/
const onChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
if (!disabled) {
onChange?.(e)
}
}

/**
* Wrapper for onClick that prevents default browser behavior
* and respects the disabled state.
*/
const onClickEvent = (e: React.MouseEvent<HTMLInputElement>) => {
e.preventDefault()
if (!disabled) {
Expand All @@ -50,26 +105,37 @@ const CustomCheckbox = ({
return (
<label className="relative flex items-center transition-transform cursor-pointer group active:scale95">
<div className="relative">
{/* Hidden native checkbox for accessibility and form submission */}
<input
type="checkbox"
className={'sr-only'}
className="sr-only"
checked={checked}
onChange={onChangeEvent}
disabled={disabled}
onClick={onClickEvent}
/>
{/* Visual checkbox box with animated checkmark */}
<div
className={`w-5 h-5 border rounded-md flex items-center justify-center transition-colors duration-200 ${getCheckboxStyle()} ${className}`}
className={cn(
'w-5 h-5 border rounded-md flex items-center justify-center transition-colors duration-200',
getCheckboxStyle(),
className
)}
>
{/* SVG checkmark animates in/out via scale transform */}
<svg
className={`transition-all duration-150 ${checked ? 'scale-100' : 'scale-0'}`}
className={`transition-all duration-150 ${
checked ? 'scale-100' : 'scale-0'
}`}
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
>
<path
className={`transition-all duration-200 ${checked ? 'stroke-dashoffset-0' : 'stroke-dashoffset-full'}`}
className={`transition-all duration-200 ${
checked ? 'stroke-dashoffset-0' : 'stroke-dashoffset-full'
}`}
d="M2.5 6L5 8.5L9.5 4"
stroke="white"
strokeWidth="2"
Expand All @@ -81,8 +147,9 @@ const CustomCheckbox = ({
</svg>
</div>
</div>
{/* Label text with configurable font weight */}
{label && (
<span className={`ml-2 mr-2 ${fontSize} text-sm text-content`}>
<span className={cn('ml-2 mr-2 text-sm text-content', fontSize)}>
{label}
</span>
)}
Expand Down
62 changes: 58 additions & 4 deletions src/components/chip.component.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,58 @@
import { cn } from '@/src/utils/cn'
import type React from 'react'

/**
* Props interface for the Chip component.
*
* A chip is a compact, dismissible tag-like element often used in
* filter bars, tag selections, or multi-select interfaces.
*/
interface ChipProps {
/** Whether the chip is currently selected/active */
selected: boolean
/** Callback fired when the chip is clicked */
onClick: () => void
/** Chip label or content */
children: React.ReactNode
/** Additional CSS classes merged with computed state styles (Tailwind-safe) */
className?: string
/** Sets text direction: 'ltr' (left-to-right) or 'rtl' (right-to-left) for RTL languages */
dir?: string
}

/**
* A selectable chip component used for filtering, tagging, or option selection.
*
* Visual behavior:
* - **Selected**: Primary blue background with white text, solid border
* - **Unselected**: Light gray background, semi-transparent border, text changes color on hover
*
* The `cn()` utility handles merging selected/unselected state styles with any user-provided
* className overrides, preventing Tailwind utility conflicts (e.g., multiple background colors).
*
* Accessibility: Renders as a native `<button>` for screen reader compatibility and
* keyboard navigation support.
*
* @example
* ```tsx
* const [selected, setSelected] = useState<string[]>([])
*
* const options = ['Filter A', 'Filter B', 'Filter C']
*
* {options.map((opt) => (
* <Chip
* key={opt}
* selected={selected.includes(opt)}
* onClick={() => setSelected(prev => prev.includes(opt)
* ? prev.filter(s => s !== opt)
* : [...prev, opt]
* )}
* >
* {opt}
* </Chip>
* ))}
* ```
*/
export const Chip: React.FC<ChipProps> = ({
selected,
onClick,
Expand All @@ -15,11 +62,18 @@ export const Chip: React.FC<ChipProps> = ({
}) => {
return (
<button
onClick={onClick}
className={`px-4 py-2 cursor-pointer rounded-full text-xs font-bold active:scale-95 transition-all border-2 ${selected ? 'bg-primary border-primary text-white' : 'bg-base-100 border-base-300/30 text-muted hover:border-primary/30'} ${className}`}
dir={dir}
onClick={onClick}
className={cn(
'px-4 py-2 cursor-pointer rounded-full text-xs font-bold transition-all border-2',
selected
? 'bg-primary border-primary text-white'
: 'bg-base-100 border-base-300/30 text-muted hover:border-primary/30',
className
)}
dir={dir}
type="button"
>
{children}
{children}
</button>
)
}
Loading