diff --git a/.gitignore b/.gitignore
index ec34490..c489add 100644
--- a/.gitignore
+++ b/.gitignore
@@ -44,3 +44,7 @@ cabal.project.local~
db*
*.DS_Store
static.out
+
+# Generated test files
+test-output.html
+test-transition.html
diff --git a/ClasshSS.cabal b/ClasshSS.cabal
index a61edab..4bf395d 100644
--- a/ClasshSS.cabal
+++ b/ClasshSS.cabal
@@ -26,7 +26,7 @@ extra-doc-files: CHANGELOG.md
-- extra-source-files:
common warnings
- ghc-options: -Wall
+ ghc-options: -Wall
library
import: warnings
@@ -34,6 +34,7 @@ library
Classh.Box
Classh.Box.Border
Classh.Box.DivInt
+ Classh.Box.Gradient
Classh.Box.Margin
Classh.Box.Padding
Classh.Box.Placement
@@ -93,7 +94,8 @@ library
Classh.TextPosition.WhiteSpace
Classh.TextPosition.WordBreak
Classh.TextPosition.Wrap
-
+ Classh.WithTransition
+
default-extensions:
OverloadedStrings
@@ -115,3 +117,63 @@ library
hs-source-dirs: src
ghc-options: -Wall -Werror -O -threaded -fno-show-valid-hole-fits
default-language: Haskell2010
+
+test-suite transition-test
+ import: warnings
+ type: exitcode-stdio-1.0
+ hs-source-dirs: test
+ main-is: TransitionTest.hs
+ build-depends: base
+ , ClasshSS
+ , data-default
+ , lens
+ , text
+ default-language: Haskell2010
+
+test-suite comprehensive-test
+ import: warnings
+ type: exitcode-stdio-1.0
+ hs-source-dirs: test
+ main-is: ComprehensiveTest.hs
+ build-depends: base
+ , ClasshSS
+ , data-default
+ , lens
+ , text
+ default-language: Haskell2010
+
+test-suite generate-html-test
+ import: warnings
+ type: exitcode-stdio-1.0
+ hs-source-dirs: test
+ main-is: GenerateHTMLTest.hs
+ build-depends: base
+ , ClasshSS
+ , data-default
+ , lens
+ , text
+ default-language: Haskell2010
+
+test-suite transform-test
+ import: warnings
+ type: exitcode-stdio-1.0
+ hs-source-dirs: test
+ main-is: TransformTest.hs
+ build-depends: base
+ , ClasshSS
+ , data-default
+ , lens
+ , text
+ default-language: Haskell2010
+
+test-suite gradient-test
+ import: warnings
+ type: exitcode-stdio-1.0
+ hs-source-dirs: test
+ main-is: GradientTest.hs
+ build-depends: base
+ , ClasshSS
+ , data-default
+ , lens
+ , text
+ default-language: Haskell2010
diff --git a/README.md b/README.md
index cc37876..52a89b3 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,111 @@
-# ClashSS
-typify CSS so that Style or Class tags do not overlap in obvious but easy to miss ways
+# ClasshSS
-Note: use Scrappy for bundled files and fetched files to further detect clashes
+Type-safe CSS-in-Haskell built on Tailwind CSS. Generates class strings with compile-time validation.
+
+## Overview
+
+ClasshSS is a library that generates Tailwind CSS class strings in Haskell with compile-time type safety. It provides two main configuration types:
+
+- `BoxConfig` - Element styling (layout, colors, borders, shadows, spacing)
+- `TextConfigTW` - Text styling (font, size, weight, color)
+
+The library generates `Text` values containing Tailwind classes. These work with any DOM library (Reflex.Dom, Lucid, Blaze, IHP, etc.).
+
+## Installation
+
+Add to your `.cabal` file:
+```cabal
+build-depends:
+ classhss
+```
+
+Or with Stack, add to `stack.yaml`:
+```yaml
+extra-deps:
+ - classhss-0.1.0.0
+```
+
+## Quick Start
+
+```haskell
+{-# LANGUAGE TemplateHaskell #-}
+
+import Classh
+import Reflex.Dom.Core
+
+-- Simple styled div
+myDiv :: DomBuilder t m => m ()
+myDiv = elClass "div" $(classh'
+ [ bgColor .~~ Blue C500
+ , p .~~ TWSize 4
+ , br .~~ R_Md
+ ]) $ text "Hello, ClasshSS!"
+```
+
+### Four Operators
+
+- `.~~` - Constant value (no responsive, no states)
+- `.|~` - Responsive values (mobile-first breakpoints)
+- `.~^` - State-based values (hover, focus, active) with transitions
+- `.~` - Direct setter (mainly for `custom` field)
+
+## Complete Example
+
+See **[docs/EXAMPLE.md](docs/EXAMPLE.md)** for a comprehensive example showing:
+- BoxConfig applied to elements via `classh'`
+- TextConfigTW for text styling (via `textS` from reflex-classh)
+- TextPosition for text positioning (via `textPosition` from reflex-classh)
+- Responsive design with `.|~`
+- State-based transitions with `.~^`
+- Grid positioning
+- Transform composition
+
+## Migrating from Tailwind
+
+If you're familiar with Tailwind CSS, see **[docs/MIGRATION_FROM_TAILWIND.md](docs/MIGRATION_FROM_TAILWIND.md)** for:
+- Class name mappings (e.g., `bg-blue-500` → `bgColor .~~ Blue C500`)
+- How to translate responsive patterns
+- How to translate hover/focus states
+- Common migration patterns
+
+## API Documentation
+
+For complete API reference:
+- Run `cabal haddock` to generate documentation
+- See Haddock comments in source files (especially `src/Classh.hs`)
+- The main module Haddock includes the comprehensive example
+
+## Why ClasshSS?
+
+- **Type safety** - Invalid CSS won't compile
+- **No conflicts** - Can't accidentally set overlapping properties
+- **Responsive by default** - Easy mobile-first design
+- **Transitions built-in** - Type-safe hover/focus states
+- **Tailwind familiar** - If you know Tailwind, you know ClasshSS
+
+## Important Notes
+
+### Type Separation
+
+You cannot mix `BoxConfig` and `TextConfigTW` in the same `classh'` call. This is enforced by the type system:
+
+```haskell
+-- ERROR: Won't compile!
+$(classh' [ bgColor .~~ Blue C500, text_color .~~ White ])
+
+-- CORRECT: Separate configs, nested elements
+elClass "div" $(classh' [bgColor .~~ Blue C500]) $
+ textS $(classhText [text_color .~~ White]) "Text"
+```
+
+### Avoid Flexbox
+
+ClasshSS recommends using CSS Grid instead of flexbox due to flexbox's non-deterministic sizing behavior. Use the `custom` field only as a last resort.
+
+### The `custom` Field
+
+The `custom` field bypasses type safety and can override type-safe properties. Only use it when absolutely necessary, and place it **first** in your config list so type-safe properties take precedence.
+
+## License
+
+BSD-style
diff --git a/docs/EXAMPLE.md b/docs/EXAMPLE.md
new file mode 100644
index 0000000..ff358bf
--- /dev/null
+++ b/docs/EXAMPLE.md
@@ -0,0 +1,245 @@
+# ClasshSS Comprehensive Example
+
+This is **the** example showing all ClasshSS concepts in one place.
+
+## Complete Working Example
+
+```haskell
+{-# LANGUAGE TemplateHaskell #-}
+
+module Example where
+
+import Classh
+import Reflex.Dom.Core
+import Reflex.Classh (textS, textPosition) -- Note: from reflex-classh package
+
+-- Complete example: styled card with positioned text
+exampleCard :: (DomBuilder t m, PostBuild t m) => m ()
+exampleCard =
+ -- BoxConfig: All element-level styling
+ elClass "div" $(classh'
+ [ -- Colors
+ bgColor .~~ White
+ , border . bColor . all .~~ Gray C200
+
+ , -- Spacing
+ p .~~ TWSize 6
+ , m .~~ TWSize 4
+
+ , -- Shape
+ br .~~ R_Lg
+ , border . bWidth . all .~~ B1
+
+ , -- Shadow & hover effect
+ shadow .~^ [ ("def", noTransition Shadow_Sm)
+ , ("hover", Shadow_Lg `withTransition` Duration_300)
+ ]
+
+ , -- Transform on hover
+ transform . scale .~^ [ ("def", noTransition Scale_100)
+ , ("hover", Scale_105 `withTransition` Duration_200)
+ ]
+
+ , -- Grid positioning (NOT flex - avoid flexbox)
+ colStart .~~ 2
+ , colSpan .~~ 4
+
+ , -- Cursor
+ cursor .~~ CursorPointer
+ ]) $ do
+ -- TextConfigTW: Text styling via textS (from reflex-classh)
+ textS $(classhText
+ [ text_color .~~ Gray C900
+ , text_size .~~ TextXl
+ , text_weight .~~ FontBold
+ ]) "Card Title"
+
+ -- TextPosition: Position text (from reflex-classh)
+ el "p" $ textPosition $(classhTextPos
+ [ textAlign .~~ TextCenter
+ , textTransform .~~ Uppercase
+ ]) $ text "Centered uppercase text"
+
+ -- More content with separate styling
+ textS $(classhText
+ [ text_color .~~ Gray C600
+ , text_size .~~ TextSm
+ ]) "Card description text"
+```
+
+## What This Example Shows
+
+### 1. BoxConfig (Element-Level Styling)
+
+Applied to the `
` via `classh'`:
+
+- **Colors**: `bgColor`, `border . bColor`
+- **Spacing**: `p` (padding), `m` (margin)
+- **Shape**: `br` (border radius), `border . bWidth`
+- **Shadows**: With state transitions using `.~^`
+- **Transforms**: Scale on hover with transitions
+- **Grid**: `colStart`, `colSpan` for grid positioning
+- **Cursor**: Mouse cursor style
+
+### 2. TextConfigTW (Text-Level Styling)
+
+Applied via `textS` from **reflex-classh** package:
+
+- **Color**: `text_color`
+- **Size**: `text_size`
+- **Weight**: `text_weight`
+
+**Critical:** Cannot mix BoxConfig and TextConfigTW in the same `classh'` call!
+
+### 3. TextPosition (Text Positioning)
+
+Applied via `textPosition` from **reflex-classh** package:
+
+- **Alignment**: `textAlign`
+- **Transform**: `textTransform` (uppercase, lowercase, etc.)
+
+### 4. The Four Operators
+
+```haskell
+.~~ -- Constant value (no responsive, no states)
+.|~ -- Responsive values (mobile-first breakpoints)
+.~^ -- State-based values (hover, focus, active)
+.~ -- Direct setter (rarely used)
+```
+
+### 5. Type Separation
+
+**This will NOT compile:**
+```haskell
+-- ERROR: Mixing BoxConfig and TextConfigTW!
+$(classh'
+ [ bgColor .~~ Blue C500 -- BoxConfig
+ , text_color .~~ White -- TextConfigTW - ERROR!
+ ])
+```
+
+**Correct:**
+```haskell
+-- Separate configs, nested elements
+elClass "div" $(classh' [bgColor .~~ Blue C500]) $
+ textS $(classhText [text_color .~~ White]) "Text"
+```
+
+### 6. Responsive Design
+
+```haskell
+p .|~ [ ("mobile", TWSize 4) -- 0px+
+ , ("md", TWSize 6) -- 768px+
+ , ("lg", TWSize 8) -- 1024px+
+ ]
+```
+
+### 7. Transitions
+
+```haskell
+shadow .~^ [ ("def", noTransition Shadow_Sm)
+ , ("hover", Shadow_Lg `withTransition` Duration_300)
+ ]
+```
+
+### 8. Grid (Not Flexbox!)
+
+ClasshSS supports grid positioning but **avoid flexbox** due to non-deterministic behavior:
+
+```haskell
+-- GOOD: Grid
+colStart .~~ ColStart_2
+colSpan .~~ ColSpan_4
+
+-- BAD: Flexbox (non-deterministic)
+custom .~ "flex justify-center" -- AVOID!
+```
+
+## Common Patterns
+
+### Simple Button
+
+```haskell
+simpleButton :: (DomBuilder t m, PostBuild t m) => Text -> m ()
+simpleButton label =
+ elClass "button" $(classh'
+ [ bgColor .~~ Blue C500
+ , px .~~ TWSize 6
+ , py .~~ TWSize 3
+ , br .~~ R_Md
+ ]) $
+ textS $(classhText [text_color .~~ White]) label
+```
+
+### Hover Effect
+
+```haskell
+hoverable :: Text
+hoverable = $(classh'
+ [ bgColor .~^ [ ("def", noTransition (Blue C500))
+ , ("hover", Blue C600 `withTransition` Duration_200)
+ ]
+ ])
+```
+
+### Responsive Spacing
+
+```haskell
+responsivePadding :: Text
+responsivePadding = $(classh'
+ [ p .|~ [ ("mobile", TWSize 4)
+ , ("md", TWSize 6)
+ , ("lg", TWSize 8)
+ ]
+ ])
+```
+
+## Important Notes
+
+### Functions from reflex-classh
+
+These functions are in the separate **reflex-classh** package:
+- `textS` - Apply TextConfigTW to text
+- `textPosition` - Apply TextPosition to text
+
+### Functions from ClasshSS
+
+These are from the main **ClasshSS** package:
+- `classh'` - Generate class string from config
+- `classhText` - Same as `classh'`, semantic alias for TextConfigTW
+- `classhTextPos` - Generate class string from TextPosition
+
+### Avoid `custom` Field
+
+The `custom` field bypasses type safety. Only use as last resort:
+
+```haskell
+-- DANGEROUS: custom can override type-safe properties!
+$(classh'
+ [ bgColor .~~ White
+ , custom .~ "bg-blue-500" -- Overrides bgColor! No compile error!
+ ])
+
+-- If you must use custom, place it FIRST:
+$(classh'
+ [ custom .~ "grid grid-cols-3" -- No type-safe alternative
+ , bgColor .~~ White -- Type-safe, takes precedence
+ ])
+```
+
+### Avoid Flexbox
+
+Flexbox has non-deterministic sizing behavior. Use CSS Grid instead:
+
+```haskell
+-- AVOID
+custom .~ "flex items-center justify-between"
+
+-- PREFER
+custom .~ "grid place-items-center"
+```
+
+## See Also
+
+- **API Documentation**: Run `cabal haddock` or see Haddock in source files
+- **Migration Guide**: [MIGRATION_FROM_TAILWIND.md](MIGRATION_FROM_TAILWIND.md)
diff --git a/docs/MIGRATION_FROM_TAILWIND.md b/docs/MIGRATION_FROM_TAILWIND.md
new file mode 100644
index 0000000..5b4b4cf
--- /dev/null
+++ b/docs/MIGRATION_FROM_TAILWIND.md
@@ -0,0 +1,461 @@
+# Migrating from Tailwind CSS to ClasshSS
+
+If you're already familiar with Tailwind CSS, this guide will help you quickly translate your knowledge to ClasshSS.
+
+## Core Philosophy
+
+ClasshSS follows Tailwind's utility-first approach but adds:
+- **Type safety** - Invalid CSS won't compile
+- **No runtime errors** - Catch mistakes at compile-time
+- **Functional composition** - Leverage Haskell's strengths
+- **Template Haskell** - Generate optimized class strings
+
+## Quick Comparison
+
+### HTML/JSX (Tailwind)
+```html
+
+
Hello
+
+```
+
+### Haskell (ClasshSS)
+```haskell
+elClass "div" $(classh'
+ [ bgColor .~^ [("def", noTransition (Blue C500)), ("hover", Blue C700 `withTransition` Duration_300)]
+ , p .~~ TWSize 8
+ , br .~~ R_Lg
+ ]) $ do
+ textS $(classh' [text_size .~~ XL2, text_weight .~~ Bold, text_color .~~ White]) "Hello"
+```
+
+## Class Name Mapping
+
+### Colors
+
+| Tailwind | ClasshSS |
+|----------|----------|
+| `bg-blue-500` | `bgColor .~~ Blue C500` |
+| `bg-gray-300` | `bgColor .~~ Gray C300` |
+| `bg-red-600` | `bgColor .~~ Red C600` |
+| `bg-[#281C40]` | `bgColor .~~ hex "281C40"` |
+| `text-blue-500` | `text_color .~~ Blue C500` |
+| `border-gray-300` | `bc .~~ Gray C300` (shorthand for border color all sides) |
+
+**Pattern:** `ColorFamily CShade`
+
+Available shades: C50, C100, C200, C300, C400, C500, C600, C700, C800, C900, C950
+
+### Spacing (Padding & Margin)
+
+| Tailwind | ClasshSS |
+|----------|----------|
+| `p-4` | `p .~~ TWSize 4` |
+| `pt-4` | `pt .~~ TWSize 4` |
+| `pb-4` | `pb .~~ TWSize 4` |
+| `px-4` | `px .~~ TWSize 4` |
+| `py-4` | `py .~~ TWSize 4` |
+| `pl-8` | `pl .~~ TWSize 8` |
+| `pr-8` | `pr .~~ TWSize 8` |
+| `m-4` | `m .~~ TWSize 4` |
+| `mt-4`, `mb-4`, `ml-4`, `mr-4` | `mt/mb/ml/mr .~~ TWSize 4` |
+| `mx-auto` | `mx .~~ TWSize_Auto` |
+| `p-[20px]` | `p .~~ pix 20` (custom pixel value) |
+
+**Pattern:** Same abbreviations, but use `.~~` operator and `TWSize` constructor
+
+### Sizing
+
+| Tailwind | ClasshSS |
+|----------|----------|
+| `w-64` | `w .~~ TWSize' (TWSize 64)` |
+| `w-full` | `w .~~ TWSize_Full` |
+| `w-screen` | `w .~~ TWSize_Screen` |
+| `w-auto` | `w .~~ TWSize_Auto` |
+| `w-1/2` | `w .~~ TWFraction 1 D2` |
+| `w-11/12` | `w .~~ TWFraction 11 D12` |
+| `w-[400px]` | `w .~~ TWSize_Custom (pix 400)` |
+| `h-64` | `h .~~ TWSize' (TWSize 64)` |
+| `max-w-screen` | `maxW .~~ TWSize_Screen` |
+| `min-h-screen` | `minH .~~ TWSize_Screen` |
+
+### Borders
+
+| Tailwind | ClasshSS |
+|----------|----------|
+| `rounded-md` | `br .~~ R_Md` |
+| `rounded-lg` | `br .~~ R_Lg` |
+| `rounded-full` | `br .~~ R_Full` |
+| `rounded-none` | `br .~~ R_None` |
+| `rounded-t-lg` | `br_t .~~ R_Lg` (top corners) |
+| `border-2` | `bw .~~ B2` (all sides) |
+| `border-t-2` | `bw_t .~~ B2` (top only) |
+| `border-gray-300` | `bc .~~ Gray C300` (all sides) |
+| `border-solid` | `border . bStyle .~~ Solid` |
+
+**Shortcuts:**
+- `br` = border radius (all corners)
+- `br_t`, `br_b`, `br_l`, `br_r` = individual sides
+- `bw` = border width
+- `bc` = border color
+
+### Shadows
+
+| Tailwind | ClasshSS |
+|----------|----------|
+| `shadow-sm` | `shadow .~~ Shadow_Sm` |
+| `shadow` | `shadow .~~ Shadow` |
+| `shadow-md` | `shadow .~~ Shadow_Md` |
+| `shadow-lg` | `shadow .~~ Shadow_Lg` |
+| `shadow-xl` | `shadow .~~ Shadow_Xl` |
+| `shadow-2xl` | `shadow .~~ Shadow_2Xl` |
+| `shadow-none` | `shadow .~~ Shadow_None` |
+
+### Text Styling
+
+| Tailwind | ClasshSS |
+|----------|----------|
+| `text-xs` | `text_size .~~ XS` |
+| `text-sm` | `text_size .~~ SM` |
+| `text-base` | `text_size .~~ Base` |
+| `text-lg` | `text_size .~~ LG` |
+| `text-xl` | `text_size .~~ XL` |
+| `text-2xl` | `text_size .~~ XL2` |
+| `text-3xl` | `text_size .~~ XL3` |
+| `font-bold` | `text_weight .~~ Bold` |
+| `font-semibold` | `text_weight .~~ Semibold` |
+| `font-normal` | `text_weight .~~ Normal` |
+| `italic` | `text_style .~~ Italic` |
+| `text-center` | Use `text_align` in TextPosition |
+
+### Transforms
+
+| Tailwind | ClasshSS |
+|----------|----------|
+| `rotate-45` | `transform . rotate .~~ Rotate_45` |
+| `rotate-90` | `transform . rotate .~~ Rotate_90` |
+| `scale-100` | `transform . scale .~~ Scale_100` |
+| `scale-105` | `transform . scale .~~ Scale_105` |
+| `translate-x-4` | `transform . translateX .~~ Translate_TWSize (TWSize 4)` |
+| `skew-x-3` | `transform . skewX .~~ Skew_3` |
+
+## Responsive Design
+
+### Tailwind
+```html
+
+ Responsive text
+
+```
+
+### ClasshSS
+```haskell
+$(classh' [ text_size .|~ [SM, Base, LG, XL] ])
+```
+
+**Breakpoint mapping:**
+```haskell
+-- Tailwind ClasshSS
+-- (default) [0] = mobile/base
+-- sm: [1] = sm
+-- md: [2] = md
+-- lg: [3] = lg
+-- xl: [4] = xl
+-- 2xl: [5] = 2xl
+
+-- Example: different background at each breakpoint
+bgColor .|~ [Gray C100, Gray C200, Gray C300, Gray C400, Gray C500, Gray C600]
+-- mobile sm md lg xl 2xl
+```
+
+**Tips:**
+- List order: `[mobile, sm, md, lg, xl, 2xl]`
+- You don't need to provide all 6 values - fewer values work too
+- Mobile-first: earlier values apply until overridden
+
+## Hover & Focus States
+
+### Tailwind
+```html
+
+```
+
+### ClasshSS
+```haskell
+$(classh'
+ [ bgColor .~^ [ ("def", noTransition (Blue C500))
+ , ("hover", Blue C700 `withTransition` Duration_300)
+ ]
+ , border . ring . ringWidth .~^ [ ("def", noTransition Ring_0)
+ , ("focus", Ring_2 `withTransition` Duration_200)
+ ]
+ ])
+```
+
+**Available states:**
+- `"def"` - Default/base state
+- `"hover"` - Mouse hover
+- `"focus"` - Keyboard/click focus
+- `"active"` - Active state
+
+**Important differences:**
+- ClasshSS requires explicit `noTransition` for default state
+- Transitions are built into the syntax with `withTransition`
+- Can combine states with screen sizes (advanced)
+
+## Transitions
+
+### Tailwind
+```html
+
+ Hover me
+
+```
+
+### ClasshSS
+```haskell
+$(classh'
+ [ transform . scale .~^ [ ("def", noTransition Scale_100)
+ , ("hover", Scale_105 `withTransition` Duration_300 `withTiming` Ease_InOut)
+ ]
+ ])
+```
+
+**Transition durations:**
+- `Duration_75`, `Duration_100`, `Duration_150`, `Duration_200`, `Duration_300`, `Duration_500`, `Duration_700`, `Duration_1000`
+
+**Timing functions:**
+- `Ease_Linear` (linear)
+- `Ease_In` (ease-in)
+- `Ease_Out` (ease-out)
+- `Ease_InOut` (ease-in-out)
+
+**Builder pattern:**
+```haskell
+value `withTransition` Duration_300 -- Just duration
+value `withTransition` Duration_300 `withTiming` Ease_In -- Duration + timing
+value `withTransition` Duration_300 `withDelay` Delay_100 -- Duration + delay
+
+-- All at once:
+value `withTransitionAll` Duration_300 Ease_InOut Delay_0
+```
+
+## Flexbox & Grid
+
+### WARNING: Avoid Flexbox Due to Non-Determinism
+
+**ClasshSS intentionally does not support flexbox** - and we **strongly recommend avoiding flexbox entirely** due to its non-deterministic behavior.
+
+**Why avoid flexbox:**
+- **Non-deterministic sizing** - Flex items can have unpredictable sizes depending on content
+- **Layout instability** - Changes in one item can affect the entire flex container
+- **Hard to reason about** - Complex interaction between flex properties makes debugging difficult
+- **Browser inconsistencies** - Different browsers may render flex layouts differently
+
+### Tailwind (with flexbox - NOT recommended)
+```html
+
+ Content
+
+```
+
+### ClasshSS - Do NOT use flexbox
+```haskell
+-- DO NOT DO THIS - Non-deterministic!
+$(classh'
+ [ custom .~ "flex flex-col items-center gap-4" -- AVOID!
+ , bgColor .~~ Gray C50
+ , p .~~ TWSize 4
+ ])
+```
+
+**Recommended alternatives:**
+- Use **CSS Grid** for 2D layouts (deterministic, explicit positioning)
+- Use **fixed positioning** with padding/margin for simple layouts
+- Use **absolute positioning** when appropriate
+
+If you absolutely must use flexbox (not recommended), use the `custom` field, but understand the risks.
+
+### Grid Layout (Supported)
+
+ClasshSS supports grid positioning:
+
+```haskell
+$(classh'
+ [ colStart .~~ 1 -- grid-column-start
+ , colSpan .~~ 6 -- grid-column-span
+ ])
+```
+
+## Common Patterns
+
+### Card Component
+
+**Tailwind:**
+```html
+
+ Card content
+
+```
+
+**ClasshSS:**
+```haskell
+$(classh'
+ [ bgColor .~~ White
+ , br .~~ R_Lg
+ , shadow .~~ Shadow_Lg
+ , p .~~ TWSize 6
+ , border . bWidth . allS .~~ B1
+ , border . bColor . allS .~~ Gray C200
+ ])
+```
+
+### Button with Hover
+
+**Tailwind:**
+```html
+
+```
+
+**ClasshSS:**
+```haskell
+buttonClasses = $(classh'
+ [ bgColor .~^ [("def", noTransition (Blue C500)), ("hover", Blue C700 `withTransition` Duration_200)]
+ , py .~~ TWSize 2
+ , px .~~ TWSize 4
+ , br .~~ R_Normal
+ ])
+
+buttonText = $(classh' [text_color .~~ White, text_weight .~~ Bold])
+```
+
+### Container
+
+**Tailwind:**
+```html
+
+ Content
+
+```
+
+**ClasshSS:**
+```haskell
+$(classh'
+ [ custom .~ "container" -- Use Tailwind's container class
+ , mx .~~ TWSize_Auto
+ , px .~~ TWSize 4
+ , maxW .~~ TWSize_Screen
+ ])
+```
+
+### Responsive Grid
+
+**Tailwind:**
+```html
+
+ Items
+
+```
+
+**ClasshSS:**
+```haskell
+$(classh'
+ [ custom .~ "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"
+ ])
+```
+
+## Key Differences Summary
+
+| Aspect | Tailwind | ClasshSS |
+|--------|----------|----------|
+| **Type Safety** | None (runtime strings) | Full (compile-time) |
+| **Errors** | Appear in browser | Caught at compile-time |
+| **Responsive** | `md:text-lg` | `text_size .|~ [SM, Base, LG]` |
+| **Hover** | `hover:bg-blue-700` | `bgColor .~^ [("hover", Blue C700 ...)]` |
+| **Transitions** | Manual classes | Built into state changes |
+| **Flexbox** | Full support | Use `custom` field |
+| **Custom values** | `w-[400px]` | `w .~~ pix 400` |
+| **Color shades** | `-100` to `-900` | `C100` to `C900` |
+
+## Advantages of ClasshSS
+
+1. **No typos** - `bg-blue-50` vs `bg-blue-500`? Compiler catches it
+2. **No conflicts** - Can't set `pt` and `py` together (compile error)
+3. **Better IDE support** - Type-driven autocomplete
+4. **Refactoring safe** - Rename, extract, compose with Haskell tools
+5. **Per-property transitions** - Each property can have its own transition
+6. **Explicit** - No magic, clear what's happening
+
+## Migration Strategy
+
+### Step 1: Start Small
+Begin with simple components (buttons, cards) before tackling complex layouts.
+
+### Step 2: Keep Tailwind for Layout
+Use `custom` field for complex flexbox/grid layouts initially:
+```haskell
+custom .~ "flex flex-col md:flex-row gap-4"
+```
+
+### Step 3: Learn the Operators
+- `.~~` for constants
+- `.|~` for responsive
+- `.~^` for states
+- `.~` for custom
+
+### Step 4: Use the Mapping Table
+Keep the [Tailwind Mapping Reference](reference/TAILWIND_MAPPING.md) handy for quick lookups.
+
+### Step 5: Leverage Type Safety
+Let the compiler guide you - if something doesn't compile, it's usually a good thing!
+
+## Common Pitfalls
+
+### 1. Forgetting noTransition
+
+**Wrong:**
+```haskell
+bgColor .~^ [("def", Blue C500), ("hover", Blue C700)]
+```
+
+**Right:**
+```haskell
+bgColor .~^ [("def", noTransition (Blue C500)), ("hover", Blue C700 `withTransition` Duration_300)]
+```
+
+### 2. Setting Conflicting Properties
+
+**Wrong:**
+```haskell
+$(classh' [ pt .~~ TWSize 4, py .~~ TWSize 2 ]) -- COMPILE ERROR
+```
+
+`py` sets both `pt` and `pb`, so setting `pt` separately conflicts.
+
+**Right:**
+```haskell
+$(classh' [ pt .~~ TWSize 4, pb .~~ TWSize 2 ])
+```
+
+### 3. Wrong List Length for Responsive
+
+You can provide fewer than 6 values, but be aware of what you're doing:
+```haskell
+-- This works - remaining breakpoints inherit last value
+text_size .|~ [SM, Base, LG]
+
+-- Mobile: SM, sm: Base, md: LG, lg+: LG
+```
+
+## Next Steps
+
+- **[Complete Tailwind Mapping](reference/TAILWIND_MAPPING.md)** - Comprehensive class mapping table
+- **[Operator Reference](reference/OPERATOR_REFERENCE.md)** - All operators explained
+- **[Examples](examples/)** - See ClasshSS in action
+- **[Core Concepts](core-concepts/)** - Deep dive into ClasshSS features
+
+Welcome to type-safe styling! 🎨
diff --git a/src/Classh.hs b/src/Classh.hs
index 204c891..62c008f 100644
--- a/src/Classh.hs
+++ b/src/Classh.hs
@@ -14,55 +14,311 @@
-- Stability : provisional
-- Portability : portable
--
--- This module exports all modules in the ClasshSS package
+-- ClasshSS: Type-safe CSS-in-Haskell based on Tailwind CSS.
--
--- The majority of classes in Tailwind are either to handle
--- 1) Elements/Boxes
--- 2) Text
+-- = Overview
--
--- That said, here is a common real example creating a box with some text in it, using reflex-dom to illustrate
+-- ClasshSS provides a type-safe interface to Tailwind CSS classes with compile-time
+-- validation. It prevents common mistakes like conflicting class definitions and
+-- ensures your styles are valid before runtime.
--
--- > elClass "div" $(classh' [ padding . t .~~ pix 20, bgColor .~~ Gray C300 ]) $ do
--- > textS $(classh' [text_size .|~ [XL,XL2]]) "Hey"
+-- The library revolves around two main configuration types:
--
--- Using Classh.Shorthand functions we can make this more ergonomic/take up less space
+-- * 'BoxConfig' - For styling HTML elements (layout, colors, borders, shadows, transforms)
+-- * 'TextConfigTW' - For text styling (font, size, weight, color, decoration)
--
--- for example
+-- = Quick Start
--
--- > padding . t == pt
+-- A simple example creating a styled box with text using reflex-dom:
--
--- The above divs we have created ensure there is no 'classhes'. For example, if we set the top padding but also the
--- y-padding then it will complain at compile time. Hence, the `$(..)` Template Haskell syntax. You can avoid this by
--- using classhUnsafe without this TH syntax
+-- @
+-- {-# LANGUAGE TemplateHaskell #-}
+-- import Classh
+-- import Reflex.Dom.Core
--
--- `classh'` is used twice in our example, once for the div/box and once for Text as its based on the
--- CompileStyle type class which simply allows us to apply mutations to a base 'config'. When we use classh'
--- we use the default value for this type (see Data.Default) however we can use `classh` to pass in a default here
--- instead:
+-- main :: IO ()
+-- main = mainWidget $ do
+-- elClass "div" $(classh' [ pt .~~ TWSize 20, bgColor .~~ Gray C300 ]) $ do
+-- textS $(classh' [text_size .|~ [XL, XL2]]) "Hello, ClasshSS!"
+-- @
--
--- > $(classh myBaseTextConfig [text_size .|~ [XL,XL2]])
+-- This generates type-checked Tailwind classes: @\"pt-20 bg-gray-300\"@ for the div
+-- and responsive text sizing for the content.
--
--- Here we use the text_size lens to set the _text_size field. to XL and XL2 which are of the type TextSize
--- We have also set these properties in two different ways here
+-- = Comprehensive Example
--
--- > 1) (.|~)
--- > 2) (.~~)
+-- A complete example showing all ClasshSS features in a styled card component:
--
--- .|~ takes a list that goes from mobile (less than 640px) -> sm -> md -> lg -> xl -> 2xl (eg. padding)
--- .~~ takes a singular value for all screen sizes (eg. background color / bgColor)
--- The reason is because almost all properties are (WhenTW prop) which is a list of values by screen size
--- this is based on https://tailwindcss.com/docs/responsive-design
+-- @
+-- {-# LANGUAGE TemplateHaskell #-}
--
--- We also have
+-- module Example where
--
--- > (.~)
--- > -- which is mainly used for `custom` as the associated Record field is not a WhenTW but a String. this is just a simple setter
+-- import Classh
+-- import Reflex.Dom.Core
+-- import Reflex.Classh (textS, textPosition) -- Note: from reflex-classh package
--
--- > (.~+)
--- > -- appends the chosen value to what exists (perhaps in a default config)
+-- -- Complete example: styled card with positioned text
+-- exampleCard :: (DomBuilder t m, PostBuild t m) => m ()
+-- exampleCard =
+-- -- BoxConfig: All element-level styling
+-- elClass \"div\" $(classh'
+-- [ -- Colors
+-- bgColor .~~ White
+-- , border . bColor . all .~~ Gray C200
+--
+-- , -- Spacing
+-- p .~~ TWSize 6
+-- , m .~~ TWSize 4
+--
+-- , -- Shape
+-- br .~~ R_Lg
+-- , border . bWidth . all .~~ B1
+--
+-- , -- Shadow & hover effect
+-- shadow .~^ [ (\"def\", noTransition Shadow_Sm)
+-- , (\"hover\", Shadow_Lg \`withTransition\` Duration_300)
+-- ]
+--
+-- , -- Transform on hover
+-- transform . scale .~^ [ (\"def\", noTransition Scale_100)
+-- , (\"hover\", Scale_105 \`withTransition\` Duration_200)
+-- ]
+--
+-- , -- Grid positioning (NOT flex - avoid flexbox)
+-- colStart .~~ 2
+-- , colSpan .~~ 4
+--
+-- , -- Cursor
+-- cursor .~~ CursorPointer
+-- ]) $ do
+-- -- TextConfigTW: Text styling via textS (from reflex-classh)
+-- textS $(classhText
+-- [ text_color .~~ Gray C900
+-- , text_size .~~ TextXl
+-- , text_weight .~~ FontBold
+-- ]) \"Card Title\"
+--
+-- -- TextPosition: Position text (from reflex-classh)
+-- el \"p\" $ textPosition $(classhTextPos
+-- [ textAlign .~~ TextCenter
+-- , textTransform .~~ Uppercase
+-- ]) $ text \"Centered uppercase text\"
+--
+-- -- More content with separate styling
+-- textS $(classhText
+-- [ text_color .~~ Gray C600
+-- , text_size .~~ TextSm
+-- ]) \"Card description text\"
+-- @
+--
+-- __Key Concepts Shown:__
+--
+-- * BoxConfig applied to div via @classh'@
+-- * TextConfigTW applied via @textS@ (from @reflex-classh@ package)
+-- * TextPosition via @textPosition@ (from @reflex-classh@ package)
+-- * Type separation: Cannot mix BoxConfig and TextConfigTW in the same @classh'@ call
+-- * Responsive values with @.|~@ (not shown here - see docs)
+-- * State-based transitions with @.~^@ (shadow, transform)
+-- * Grid positioning (avoiding flexbox)
+-- * Transform composition
+--
+-- For the full example with detailed explanations, see @docs\/EXAMPLE.md@.
+--
+-- = Template Haskell Functions
+--
+-- ClasshSS provides several Template Haskell functions for compile-time class generation:
+--
+-- * 'classh'' - Apply mutations to default config (most common)
+-- * 'classh' - Apply mutations to custom base config
+-- * 'classhUnsafe' - Runtime version without compile-time checking
+-- * 'classhV'' - Single mutation variant of classh'
+-- * 'classhV' - Single mutation variant of classh
+--
+-- == classh' - Default Config
+--
+-- Use when starting from scratch with no base configuration:
+--
+-- @
+-- buttonClasses :: Text
+-- buttonClasses = $(classh'
+-- [ bgColor .~~ Blue C500
+-- , px .~~ TWSize 6
+-- , py .~~ TWSize 3
+-- , br .~~ R_Md
+-- ])
+-- -- Compiles to: \"bg-blue-500 px-6 py-3 rounded-md\"
+-- @
+--
+-- == classh - Custom Base Config
+--
+-- Use when you have a base theme or default configuration:
+--
+-- @
+-- myTheme :: BoxConfig
+-- myTheme = def & bgColor .~~ Gray C50 & p .~~ TWSize 4
+--
+-- customBox :: Text
+-- customBox = $(classh myTheme
+-- [ bgColor .~~ Blue C100 -- Overrides theme background
+-- , br .~~ R_Lg
+-- ])
+-- @
+--
+-- == classhUnsafe - Runtime Generation
+--
+-- Use when you need runtime arguments (no compile-time checking):
+--
+-- @
+-- dynamicClasses :: Color -> Text
+-- dynamicClasses color = classhUnsafe [bgColor .~~ color]
+-- @
+--
+-- = Operators
+--
+-- ClasshSS provides ergonomic operators for setting properties:
+--
+-- == .~~ (Set Constant)
+--
+-- Sets a value for all screen sizes:
+--
+-- @
+-- bgColor .~~ Blue C500 -- bg-blue-500
+-- br .~~ R_3Xl -- rounded-3xl
+-- shadow .~~ Shadow_Md -- shadow-md
+-- @
+--
+-- == .|~ (Set Responsive)
+--
+-- Sets responsive values for each breakpoint [mobile, sm, md, lg, xl, 2xl]:
+--
+-- @
+-- w .|~ [TWSize' (TWSize 12), TWSize' (TWSize 24), TWSize' (TWSize 48)]
+-- -- mobile: w-12, sm: w-24, md: w-48
+--
+-- text_size .|~ [Base, LG, XL, XL2]
+-- -- mobile: text-base, sm: text-lg, md: text-xl, lg: text-2xl
+-- @
+--
+-- == .~^ (Set with Transitions)
+--
+-- Sets stateful values (hover, focus, etc.) with transitions:
+--
+-- @
+-- bgColor .~^
+-- [ (\"def\", noTransition (Blue C600))
+-- , (\"hover\", Blue C400 \`withTransition\` Duration_300 \`withTiming\` Ease_InOut)
+-- ]
+-- @
+--
+-- Available states: @\"def\"@, @\"hover\"@, @\"focus\"@, @\"active\"@
+--
+-- == .~ (Simple Setter)
+--
+-- Direct lens setter, mainly for the @custom@ field:
+--
+-- @
+-- custom .~ \"flex items-center gap-4\"
+-- @
+--
+-- == Additional Operators
+--
+-- * '.~+' - Append to existing WhenTW list
+-- * '.++' - Extend with single conditional value
+-- * '.|+' - Append responsive values to existing
+--
+-- = Shorthand Helpers
+--
+-- ClasshSS provides Tailwind-style shorthand for common properties:
+--
+-- @
+-- -- Instead of: padding . t .~~ TWSize 4
+-- pt .~~ TWSize 4
+--
+-- -- Instead of: border . radius . allS .~~ R_Md
+-- br .~~ R_Md
+--
+-- -- Instead of: border . bWidth . allS .~~ B2
+-- bw .~~ B2
+-- @
+--
+-- Common shortcuts:
+--
+-- * Padding: 'pt', 'pb', 'pl', 'pr', 'px', 'py', 'p'
+-- * Margin: 'mt', 'mb', 'ml', 'mr', 'mx', 'my', 'm'
+-- * Border radius: 'br', 'br_t', 'br_b', 'br_l', 'br_r'
+-- * Border width: 'bw', 'bw_t', 'bw_b', 'bw_l', 'bw_r'
+-- * Border color: 'bc', 'bc_t', 'bc_b', 'bc_l', 'bc_r'
+-- * Sizing: 'w', 'h', 'maxW', 'maxH', 'minW', 'minH'
+--
+-- = Common Use Cases
+--
+-- == Responsive Button
+--
+-- @
+-- $(classh'
+-- [ bgColor .~^ [(\"def\", noTransition (Blue C500)), (\"hover\", Blue C600 \`withTransition\` Duration_300)]
+-- , px .|~ [TWSize 4, TWSize 6, TWSize 8] -- Responsive padding
+-- , py .|~ [TWSize 2, TWSize 3, TWSize 4]
+-- , br .~~ R_Md
+-- , shadow .~^ [(\"def\", noTransition Shadow_Sm), (\"hover\", Shadow_Md \`withTransition\` Duration_200)]
+-- ])
+-- @
+--
+-- == Card Component
+--
+-- @
+-- $(classh'
+-- [ bgColor .~~ White
+-- , br .~~ R_Lg
+-- , shadow .~~ Shadow_Lg
+-- , p .~~ TWSize 6
+-- , border . bWidth . allS .~~ B1
+-- , border . bColor . allS .~~ Gray C200
+-- ])
+-- @
+--
+-- == Centered Container
+--
+-- @
+-- $(classh'
+-- [ w .~~ TWFraction 11 D12 -- 11/12 width
+-- , mx .~~ TWSize_Auto -- Center horizontally
+-- , p .~~ TWSize 8
+-- ])
+-- @
+--
+-- = Compile-Time Safety
+--
+-- ClasshSS catches errors at compile-time:
+--
+-- @
+-- -- ERROR: pt and py overlap (py sets both pt and pb)
+-- $(classh' [ pt .~~ TWSize 4, py .~~ TWSize 2 ])
+--
+-- -- ERROR: Duplicate screen condition
+-- $(classh' [ bgColor .~^ [(\"hover\", Blue C500), (\"hover\", Blue C600)] ])
+-- @
+--
+-- The Template Haskell syntax @$(...)@ enables this validation. For runtime generation
+-- without checking, use 'classhUnsafe'.
+--
+-- = See Also
+--
+-- * "Classh.Box" - BoxConfig type and all box styling properties
+-- * "Classh.Text" - TextConfigTW type and text styling properties
+-- * "Classh.Setters" - Detailed operator documentation
+-- * "Classh.Shorthand" - All shorthand helper functions
+-- * "Classh.WithTransition" - Transition system for smooth animations
+-- * "Classh.Responsive.WhenTW" - Responsive design system
+--
+-- For comprehensive guides, see:
+--
+-- * @docs/GETTING_STARTED.md@ - Step-by-step tutorial
+-- * @docs/MIGRATION_FROM_TAILWIND.md@ - For Tailwind CSS users
+-- * @docs/core-concepts/OPERATORS.md@ - Complete operator reference
+-- * @docs/examples/@ - Real-world component examples
--
--- > (.|+)
--- > -- like .|~ except that it adds to what already exists (perhaps in a default config)
--------------------------------------------------------------------------------
module Classh
@@ -177,50 +433,312 @@ type CompiledClassh = Compiled Expression
defaultClasses :: T.Text
defaultClasses = ""
--- | Apply mutations to BoxConfig or TextConfigTW at compile time with a default
--- > $(classh def' [ bgColor .~~ Black ]) :: Text
--- > $(classh def' [ text_color .~~ Black ]) :: Text
+-- | Apply mutations to a base config at compile-time with validation.
+--
+-- This is the primary function for creating CSS classes with a custom base configuration.
+-- It performs compile-time validation to catch errors like duplicate screen conditions
+-- or conflicting property settings.
+--
+-- === Examples
+--
+-- Basic usage with custom base:
+--
+-- @
+-- myTheme :: BoxConfig
+-- myTheme = def & bgColor .~~ Gray C50 & p .~~ TWSize 4
+--
+-- customBox :: Text
+-- customBox = $(classh myTheme
+-- [ bgColor .~~ Blue C100 -- Override theme background
+-- , br .~~ R_Lg -- Add border radius
+-- ])
+-- @
+--
+-- With TextConfigTW:
+--
+-- @
+-- myTextTheme :: TextConfigTW
+-- myTextTheme = def & text_color .~~ Gray C900 & text_weight .~~ Normal
+--
+-- headingClasses :: Text
+-- headingClasses = $(classh myTextTheme
+-- [ text_size .~~ XL3
+-- , text_weight .~~ Bold -- Override theme weight
+-- ])
+-- @
+--
+-- === Compile-Time Errors
+--
+-- @
+-- -- ERROR: pt and py conflict (py sets both pt and pb)
+-- $(classh def [ pt .~~ TWSize 4, py .~~ TWSize 2 ])
+--
+-- -- ERROR: Duplicate \"hover\" condition
+-- $(classh def [ bgColor .~^ [(\"hover\", Blue C500), (\"hover\", Red C500)] ])
+-- @
+--
+-- @since 0.1.0.0
classh :: CompileStyle s => s -> [(s -> s)] -> Q Exp
classh base muts = case compileS $ foldl (\acc f -> f acc) base muts of
Left e -> fail $ T.unpack e
Right styleString -> [| styleString |]
--- | Apply mutations to BoxConfig or TextConfigTW at compile time
--- > $(classh' [ bgColor .~~ Black ]) :: Text
--- > $(classh' [ text_color .~~ Black ]) :: Text
+-- | Apply mutations to default config at compile-time with validation.
+--
+-- This is the most commonly used function for generating CSS classes. It starts with
+-- the default configuration ('def') and applies your mutations with compile-time checking.
+--
+-- === Examples
+--
+-- Simple box styling:
+--
+-- @
+-- $(classh'
+-- [ bgColor .~~ Blue C500
+-- , p .~~ TWSize 8
+-- , br .~~ R_Md
+-- ])
+-- -- Result: \"bg-blue-500 p-8 rounded-md\"
+-- @
+--
+-- Responsive design:
+--
+-- @
+-- $(classh'
+-- [ w .|~ [TWSize' (TWSize 12), TWSize' (TWSize 24), TWSize' (TWSize 48)]
+-- , bgColor .|~ [Gray C100, Gray C200, Gray C300]
+-- ])
+-- -- Result: \"w-12 sm:w-24 md:w-48 bg-gray-100 sm:bg-gray-200 md:bg-gray-300\"
+-- @
+--
+-- With hover effects:
+--
+-- @
+-- $(classh'
+-- [ bgColor .~^ [ (\"def\", noTransition (Blue C500))
+-- , (\"hover\", Blue C600 \`withTransition\` Duration_300)
+-- ]
+-- ])
+-- @
+--
+-- Text styling:
+--
+-- @
+-- $(classh'
+-- [ text_size .~~ XL2
+-- , text_weight .~~ Bold
+-- , text_color .~~ Gray C900
+-- ])
+-- -- Result: \"text-2xl font-bold text-gray-900\"
+-- @
+--
+-- === Type Inference
+--
+-- The return type is inferred from usage context. Works with both BoxConfig and TextConfigTW:
+--
+-- @
+-- boxClasses :: Text
+-- boxClasses = $(classh' [ bgColor .~~ Blue C500 ])
+--
+-- textClasses :: Text
+-- textClasses = $(classh' [ text_color .~~ Blue C500 ])
+-- @
+--
+-- @since 0.1.0.0
classh' :: (Default s, CompileStyle s) => [(s -> s)] -> Q Exp
classh' muts = case compileS $ foldl (\acc f -> f acc) def muts of
Left e -> fail $ T.unpack e
Right styleString -> [| styleString |]
--- | Doesn't use TemplateHaskell, this is meant for making lib functions since we need args
--- from outside the would-be TH context
--- > (classhUnsafe [ bgColor .~~ Black ])
--- > (classhUnsafe [ text_color .~~ Black ])
+-- | Runtime class generation without Template Haskell.
+--
+-- Use this when you need runtime arguments that can't be known at compile-time.
+-- Note: This skips compile-time validation, so errors will only appear at runtime.
+--
+-- === When to Use
+--
+-- * When creating library functions that accept runtime parameters
+-- * When working with dynamic values from user input
+-- * When integrating with Reflex.Dom's 'Dynamic' types
+--
+-- === Examples
+--
+-- Dynamic color based on input:
+--
+-- @
+-- coloredBox :: Color -> Text
+-- coloredBox color = classhUnsafe [ bgColor .~~ color, p .~~ TWSize 4 ]
+--
+-- -- Usage:
+-- coloredBox (Blue C500) -- \"bg-blue-500 p-4\"
+-- coloredBox (Red C600) -- \"bg-red-600 p-4\"
+-- @
+--
+-- With Reflex.Dom Dynamic:
+--
+-- @
+-- dynClasses <- holdDyn (classhUnsafe [bgColor .~~ Gray C300]) updateEvent
+-- elDynClass \"div\" dynClasses $ text \"Dynamic styling\"
+-- @
+--
+-- In reusable components:
+--
+-- @
+-- styledButton :: Text -> BoxConfig -> m ()
+-- styledButton label customConfig = do
+-- let classes = classhUnsafe [id] -- Apply custom config
+-- elClass \"button\" (boxCSS customConfig) $ text label
+-- @
+--
+-- === Drawbacks
+--
+-- * No compile-time validation
+-- * Can't catch conflicting properties or duplicate conditions
+-- * Slightly less performant (runtime string generation)
+--
+-- @since 0.1.0.0
classhUnsafe :: (Default a, ShowTW a) => [a -> a] -> T.Text
classhUnsafe muts = showTW $ def `applyFs` muts
---classhV, classhV' :: Q Exp
+-- | Single-mutation variant of 'classh'.
+--
+-- Convenience function for applying exactly one mutation to a base config.
+--
+-- === Examples
+--
+-- @
+-- -- Instead of:
+-- $(classh myTheme [ bgColor .~~ Blue C500 ])
+--
+-- -- You can write:
+-- $(classhV myTheme (bgColor .~~ Blue C500))
+-- @
+--
+-- @since 0.1.0.0
classhV :: (CompileStyle a) => a -> (a -> a) -> Q Exp
-classhV base transform = classh base [transform]
+classhV base mutation = classh base [mutation]
+-- | Single-mutation variant of 'classh''.
+--
+-- Convenience function for applying exactly one mutation to the default config.
+--
+-- === Examples
+--
+-- @
+-- -- Instead of:
+-- $(classh' [ bgColor .~~ Blue C500 ])
+--
+-- -- You can write:
+-- $(classhV' (bgColor .~~ Blue C500))
+-- @
+--
+-- @since 0.1.0.0
classhV' :: (Default a, CompileStyle a) => (a -> a) -> Q Exp
-classhV' transform = classh' [transform]
+classhV' mutation = classh' [mutation]
--- | Synonym to showTW
+-- | Synonym for 'showTW' specialized to BoxConfig.
+--
+-- Converts a BoxConfig to its Tailwind CSS class string representation.
+-- Useful when you already have a constructed BoxConfig value.
+--
+-- === Examples
+--
+-- @
+-- myBox :: BoxConfig
+-- myBox = def & bgColor .~~ Blue C500 & p .~~ TWSize 4
+--
+-- classes :: Text
+-- classes = boxCSS myBox
+-- -- Result: \"bg-blue-500 p-4\"
+-- @
+--
+-- @since 0.1.0.0
boxCSS :: BoxConfig -> T.Text
boxCSS = showTW
+-- | Append BoxConfig mutations to an existing class string.
+--
+-- Useful for extending a base set of classes with additional styling.
+--
+-- === Examples
+--
+-- @
+-- baseClasses :: Text
+-- baseClasses = \"container mx-auto\"
+--
+-- extendedClasses :: Text
+-- extendedClasses = alsoF baseClasses [ bgColor .~~ Blue C50, p .~~ TWSize 8 ]
+-- -- Result: \"container mx-auto bg-blue-50 p-8\"
+-- @
+--
+-- @since 0.1.0.0
alsoF :: T.Text -> [BoxConfig -> BoxConfig] -> T.Text
alsoF s cfgMuts = s <> boxCSS (def `applyFs` cfgMuts)
+-- | Append a BoxConfig to an existing class string.
+--
+-- Similar to 'alsoF' but takes a constructed BoxConfig instead of mutations.
+--
+-- === Examples
+--
+-- @
+-- baseClasses :: Text
+-- baseClasses = \"container mx-auto\"
+--
+-- additionalConfig :: BoxConfig
+-- additionalConfig = def & bgColor .~~ Blue C50 & p .~~ TWSize 8
+--
+-- combined :: Text
+-- combined = also baseClasses additionalConfig
+-- -- Result: \"container mx-auto bg-blue-50 p-8\"
+-- @
+--
+-- @since 0.1.0.0
also :: T.Text -> BoxConfig -> T.Text
also s cfg = s <> boxCSS cfg
+-- | Apply a list of functions to a value in sequence.
+--
+-- This is a helper function used internally by ClasshSS to apply
+-- mutations to configurations. Left fold over the functions.
+--
+-- === Examples
+--
+-- @
+-- result = applyFs def
+-- [ bgColor .~~ Blue C500
+-- , p .~~ TWSize 4
+-- , br .~~ R_Md
+-- ]
+-- @
+--
+-- @since 0.1.0.0
applyFs :: a -> [a -> a] -> a
applyFs in_ fs = foldl (\acc f -> f acc) in_ fs
+-- | Collection of config mutations that can be applied together.
+--
+-- Useful for grouping related styling mutations that are frequently used together.
+--
+-- === Examples
+--
+-- @
+-- buttonBase :: ClassCollection BoxConfig
+-- buttonBase = ClassCollection
+-- [ px .~~ TWSize 6
+-- , py .~~ TWSize 3
+-- , br .~~ R_Md
+-- , text_weight .~~ Bold
+-- ]
+--
+-- primaryButton :: Text
+-- primaryButton = $(classh' $
+-- getCollection buttonBase ++
+-- [ bgColor .~~ Blue C500 ])
+-- @
+--
+-- @since 0.1.0.0
newtype ClassCollection tw = ClassCollection { getCollection :: [tw -> tw] }
-- END OF MODULE
diff --git a/src/Classh/Box.hs b/src/Classh/Box.hs
index 3605241..16341fa 100644
--- a/src/Classh/Box.hs
+++ b/src/Classh/Box.hs
@@ -11,42 +11,292 @@
-- Stability : provisional
-- Portability : portable
--
--- The core interface to creating responsive elements, including images.
+-- BoxConfig: The core type for styling HTML elements (divs, sections, images, etc.).
--
--- Here is a common real example creating a box with some text in it, using reflex-dom to illustrate
+-- = Overview
--
--- > elClass "div" $(classh' [ padding . t .~~ pix 20, bgColor .~~ Gray C300 ]) $ do
--- > text "Hey"
+-- This module provides 'BoxConfig', the primary configuration type for styling
+-- HTML elements in ClasshSS. It encompasses all visual properties except text-specific
+-- styling (which uses 'Classh.Text.TextConfigTW').
--
--- This module and all modules which it re-exports are your interface to writing typified classes
--- specifically for Box's ( Box == element )
+-- BoxConfig includes:
--
--- Using Classh.Shorthand functions we can make this more ergonomic/take up less space
+-- * Layout - Grid positioning, sizing, constraints
+-- * Spacing - Padding and margin with responsive support
+-- * Colors - Background colors and opacity (with transitions)
+-- * Borders - Radius, width, color, style, rings, outlines
+-- * Visual Effects - Shadows (with transitions)
+-- * Transforms - Rotate, scale, translate, skew (with transitions)
+-- * Positioning - Justify and align content
+-- * Cursor - Mouse cursor styles
--
--- for example
--- > padding . t == pt
+-- = Quick Example
--
--- The above divs we have created ensure there is no \'classhes\'. For example, if we set the top padding but also the
--- y-padding then it will complain at compile time. Hence, the `$(..)` Template Haskell syntax. You can avoid this by
--- using classhUnsafe without this TH syntax. Classh's type system also enforces that you cannot use text config setters
--- in the same classh expression as one with 'BoxConfig' setters. This is due to the design goal to reduce spooky behavior
--- and misleading code. For example if we have multiple parent divs with text classes, then it will make it challenging to
--- find why a given piece of text appears as such, especially if we refactor components, the reason for its appearance would
--- be even more hidden
+-- Creating a styled card with Reflex.Dom:
--
--- Note that we can also use '.|~' and 'zipScreens' to easily create responsive boxes
--- .|~ takes a list that goes from mobile (less than 640px) -> sm -> md -> lg -> xl -> 2xl (eg. padding)
--- .~~ takes a singular value for all screen sizes (eg. background color / bgColor)
--- The reason is because almost all properties are (WhenTW prop) which is a list of values by screen size
--- this is based on https://tailwindcss.com/docs/responsive-design
+-- @
+-- elClass \"div\" $(classh'
+-- [ bgColor .~~ White
+-- , p .~~ TWSize 6
+-- , br .~~ R_Lg
+-- , shadow .~~ Shadow_Lg
+-- , border . bWidth . allS .~~ B1
+-- , border . bColor . allS .~~ Gray C200
+-- ]) $ do
+-- text \"Card content here\"
+-- @
--
--- We also have
--- (.~) which is mainly used for `custom` as the associated Record field is not a WhenTW but a String.
--- this is just a simple setter
--- (.~+) appends the chosen value to what exists (perhaps in a default config)
--- (.|+) like .|~ except that it adds to what already exists (perhaps in a default config)
+-- = BoxConfig Fields
+--
+-- == Grid Layout
+--
+-- * '_colStart' - Grid column start position (1-12)
+-- * '_colSpan' - Grid column span (1-12)
+--
+-- @
+-- $(classh' [ colStart .~~ 2, colSpan .~~ 4 ])
+-- -- Result: \"col-start-2 col-span-4\"
+-- @
+--
+-- == Background
+--
+-- * '_bgColor' - Background color (transitionable)
+-- * '_bgOpacity' - Background opacity 1-100 (transitionable, default 100)
+--
+-- @
+-- -- Simple background
+-- bgColor .~~ Blue C500
+--
+-- -- With hover transition
+-- bgColor .~^ [ (\"def\", noTransition (Blue C500))
+-- , (\"hover\", Blue C600 \`withTransition\` Duration_300)
+-- ]
+-- @
+--
+-- == Spacing
+--
+-- * '_padding' - 'BoxPadding' with individual sides (transitionable)
+-- * '_margin' - 'BoxMargin' with individual sides (transitionable)
+--
+-- @
+-- -- Using shorthand (see "Classh.Shorthand")
+-- pt .~~ TWSize 4 -- padding-top
+-- pb .~~ TWSize 4 -- padding-bottom
+-- px .~~ TWSize 6 -- padding-left and padding-right
+-- py .~~ TWSize 2 -- padding-top and padding-bottom
+-- p .~~ TWSize 4 -- all sides
+--
+-- -- Same pattern for margin: mt, mb, mx, my, m
+-- @
+--
+-- == Sizing
+--
+-- * '_sizingBand' - 'BoxSizingBand' containing width, height, min/max constraints (all transitionable)
+--
+-- @
+-- -- Using shorthand
+-- w .~~ TWSize_Full -- width: 100%
+-- h .~~ pix 400 -- height: 400px
+-- maxW .~~ TWSize_Screen -- max-width: 100vw
+-- minH .~~ pix 200 -- min-height: 200px
+--
+-- -- Fractional widths
+-- w .~~ TWFraction 11 D12 -- width: 11/12
+-- @
+--
+-- == Borders
+--
+-- * '_border' - 'BorderConfig' with radius, width, color, style, rings, outlines (most transitionable)
+--
+-- @
+-- -- Using shorthand
+-- br .~~ R_Lg -- rounded-lg (all corners)
+-- br_t .~~ R_Md -- rounded-t-md (top corners)
+-- bw .~~ B2 -- border-2 (all sides)
+-- bw_t .~~ B1 -- border-t (top only)
+-- bc .~~ Gray C300 -- border-gray-300 (all sides)
+--
+-- -- Full path for fine control
+-- border . radius . borderRadius_tr .~~ R_Lg -- Top-right corner only
+-- border . bWidth . t .~~ B2 -- Top border width
+-- border . bColor . allS .~~ Gray C200 -- All sides border color
+-- @
+--
+-- == Visual Effects
+--
+-- * '_shadow' - Box shadow (transitionable)
+--
+-- @
+-- shadow .~~ Shadow_Md
+--
+-- -- With hover transition
+-- shadow .~^ [ (\"def\", noTransition Shadow_Sm)
+-- , (\"hover\", Shadow_Lg \`withTransition\` Duration_200)
+-- ]
+-- @
+--
+-- == Transforms
+--
+-- * '_transform' - 'TransformConfig' with rotate, scale, translate, skew, origin (all transitionable)
+--
+-- @
+-- -- Simple rotation
+-- transform . rotate .~~ Rotate_45
+--
+-- -- Scale with hover
+-- transform . scale .~^ [ (\"def\", noTransition Scale_100)
+-- , (\"hover\", Scale_105 \`withTransition\` Duration_300)
+-- ]
+--
+-- -- Translation
+-- transform . translateX .~~ Translate_TWSize (TWSize 4)
+-- transform . translateY .~~ Translate_Fraction 1 D2 -- 50%
+-- @
+--
+-- == Positioning
+--
+-- * '_position' - Tuple of ('Justify', 'Align') for content positioning
+--
+-- @
+-- position .~~ (J_Center, A_Center) -- Center content
+-- position .~~ centered -- Shorthand for above
+-- position .~~ topLeft -- Top-left alignment
+-- @
+--
+-- == Cursor
+--
+-- * '_cursor' - Mouse cursor style
+--
+-- @
+-- cursor .~~ CursorPointer
+-- cursor .~~ CursorNotAllowed
+-- @
+--
+-- == Custom Classes
+--
+-- * '_box_custom' - Arbitrary Tailwind classes (escape hatch)
+--
+-- @
+-- custom .~ \"flex flex-col items-center gap-4\"
+-- @
+--
+-- = Responsive Design
+--
+-- Most BoxConfig properties use 'WhenTW' for responsive values:
+--
+-- @
+-- -- Different backgrounds at each breakpoint
+-- bgColor .|~ [Gray C100, Gray C200, Gray C300, Gray C400, Gray C500, Gray C600]
+-- -- mobile sm md lg xl 2xl
+--
+-- -- Responsive padding
+-- p .|~ [TWSize 2, TWSize 4, TWSize 6, TWSize 8]
+-- -- mobile sm md lg (and larger)
+-- @
+--
+-- = State-Based Styling
+--
+-- Use '.~^' for hover, focus, and other states:
+--
+-- @
+-- bgColor .~^
+-- [ (\"def\", noTransition (Blue C500))
+-- , (\"hover\", Blue C600 \`withTransition\` Duration_300)
+-- , (\"focus\", Blue C700 \`withTransition\` Duration_200)
+-- ]
+-- @
+--
+-- = Type Safety
+--
+-- BoxConfig enforces type safety at compile-time:
+--
+-- * Cannot mix BoxConfig and TextConfigTW setters in same expression
+-- * Cannot set conflicting properties (e.g., @pt@ and @py@ together)
+-- * Catches duplicate screen conditions
+--
+-- @
+-- -- COMPILE ERROR: pt and py conflict (py sets both pt and pb)
+-- $(classh' [ pt .~~ TWSize 4, py .~~ TWSize 2 ])
+-- @
+--
+-- = Common Patterns
+--
+-- == Card Component
+--
+-- @
+-- $(classh'
+-- [ bgColor .~~ White
+-- , br .~~ R_Lg
+-- , shadow .~~ Shadow_Lg
+-- , p .~~ TWSize 6
+-- , border . bWidth . allS .~~ B1
+-- , border . bColor . allS .~~ Gray C200
+-- ])
+-- @
+--
+-- == Centered Container
+--
+-- @
+-- $(classh'
+-- [ w .~~ TWFraction 11 D12
+-- , mx .~~ TWSize_Auto
+-- , p .~~ TWSize 8
+-- ])
+-- @
+--
+-- == Responsive Button
+--
+-- @
+-- $(classh'
+-- [ bgColor .~^ [(\"def\", noTransition (Blue C500)), (\"hover\", Blue C600 \`withTransition\` Duration_300)]
+-- , px .|~ [TWSize 4, TWSize 6, TWSize 8]
+-- , py .|~ [TWSize 2, TWSize 3, TWSize 4]
+-- , br .~~ R_Md
+-- , shadow .~^ [(\"def\", noTransition Shadow_Sm), (\"hover\", Shadow_Md \`withTransition\` Duration_200)]
+-- ])
+-- @
+--
+-- = Shorthand Helpers
+--
+-- For more ergonomic code, use shorthand from "Classh.Shorthand":
+--
+-- @
+-- -- Instead of: padding . t .~~ TWSize 20
+-- pt .~~ TWSize 20
+--
+-- -- Instead of: border . radius . allS .~~ R_Lg
+-- br .~~ R_Lg
+-- @
+--
+-- See "Classh.Shorthand" for complete list of shortcuts.
+--
+-- = Re-Exported Modules
+--
+-- This module re-exports all box-related modules for convenience:
+--
+-- * "Classh.Color" - Color types and hex colors
+-- * "Classh.Cursor" - Cursor styles
+-- * "Classh.Box.TWSize" - Size types and helpers
+-- * "Classh.Box.Padding" - Padding configuration
+-- * "Classh.Box.Margin" - Margin configuration
+-- * "Classh.Box.SizingBand" - Width/height with constraints
+-- * "Classh.Box.Placement" - Justify and Align types
+-- * "Classh.Box.Border" - Border configuration
+-- * "Classh.Box.Shadow" - Shadow types
+-- * "Classh.Box.Transition" - Transition configuration
+-- * "Classh.Box.Transform" - Transform types and configuration
+-- * "Classh.WithTransition" - Transition builder system
+-- * "Classh.Responsive.WhenTW" - Responsive value system
+--
+-- = See Also
+--
+-- * "Classh" - Main module with Template Haskell functions
+-- * "Classh.Text" - For text-specific styling
+-- * "Classh.Setters" - Operator documentation
+-- * "Classh.Shorthand" - Ergonomic shortcuts
+-- * @docs/features/BOX_STYLING.md@ - Complete feature guide
+-- * @docs/examples/@ - Real-world examples
--
--- We can also add any arbitrary classes to the end of the TextConfigTW using its HasCustom instance
--------------------------------------------------------------------------------
@@ -66,7 +316,8 @@ module Classh.Box
, border
, position
, shadow
- , transition
+ , cursor
+ , transform
, box_custom
) where
@@ -80,6 +331,8 @@ import Classh.Internal.TShow
import Classh.Internal.TWNum as X
import Classh.Responsive.WhenTW as X
import Classh.Color as X
+import Classh.Box.Gradient as X
+import Classh.Cursor as X
import Classh.Box.TWSize as X
import Classh.Box.Padding as X
import Classh.Box.Margin as X
@@ -88,36 +341,89 @@ import Classh.Box.Placement as X
import Classh.Box.Border as X
import Classh.Box.Shadow as X
import Classh.Box.Transition as X
+import Classh.Box.Transform as X
+import Classh.WithTransition as X
-import Control.Lens hiding ((<&>))
+import Control.Lens hiding ((<&>), transform)
import Data.Default
import qualified Data.Text as T
+-- | Configuration type for styling HTML box elements (divs, sections, etc.).
+--
+-- BoxConfig contains all visual styling properties for HTML elements except
+-- text-specific properties (which use 'Classh.Text.TextConfigTW').
+--
+-- === Field Overview
+--
+-- * Grid: '_colStart', '_colSpan'
+-- * Background: '_bgColor' (transitionable), '_bgOpacity' (transitionable)
+-- * Spacing: '_padding' (transitionable), '_margin' (transitionable)
+-- * Sizing: '_sizingBand' (width, height, min/max - transitionable)
+-- * Borders: '_border' (radius, width, color, style, rings)
+-- * Layout: '_position' (justify, align)
+-- * Effects: '_shadow' (transitionable)
+-- * Interaction: '_cursor'
+-- * Transforms: '_transform' (rotate, scale, translate, skew - transitionable)
+-- * Escape hatch: '_box_custom'
+--
+-- === Examples
+--
+-- @
+-- -- Simple box
+-- def & bgColor .~~ Blue C500 & p .~~ TWSize 4
+--
+-- -- Card component
+-- def
+-- & bgColor .~~ White
+-- & br .~~ R_Lg
+-- & shadow .~~ Shadow_Lg
+-- & p .~~ TWSize 6
+--
+-- -- Responsive container
+-- def
+-- & w .|~ [TWSize_Full, TWSize' (TWSize 64), TWSize' (TWSize 80)]
+-- & mx .~~ TWSize_Auto
+-- @
+--
+-- @since 0.1.0.0
data BoxConfig = BoxConfig
{ _colStart :: WhenTW Int
+ -- ^ Grid column start position (1-12). Default: empty (no grid positioning)
, _colSpan :: WhenTW Int
- , _bgColor :: WhenTW Color
- , _bgOpacity :: WhenTW Int -- 1 5 10 .. 100 -- def == 519
+ -- ^ Grid column span (1-12). Default: empty (no grid span)
+ , _bgColor :: WhenTW (WithTransition GradientColor)
+ -- ^ Background color or gradient (transitionable). Default: empty (no background)
+ -- Use 'solid' for simple colors, or gradient helpers like 'linearGradient'
+ , _bgOpacity :: WhenTW (WithTransition Int)
+ -- ^ Background opacity 1-100 (transitionable). Default: empty (100% opacity)
, _padding :: BoxPadding
+ -- ^ Padding on all sides (transitionable). See 'BoxPadding' for details
, _margin :: BoxMargin
+ -- ^ Margin on all sides (transitionable). See 'BoxMargin' for details
, _sizingBand :: BoxSizingBand
- , _border :: BorderConfig -- { rounded, thickness, etc .. }
+ -- ^ Width, height, and size constraints (transitionable). See 'BoxSizingBand'
+ , _border :: BorderConfig
+ -- ^ Border configuration (radius, width, color, style, rings, outlines)
, _position :: WhenTW (Justify, Align)
- , _shadow :: WhenTW BoxShadow
- , _transition :: TransitionConfig
- --, _text_align :: Align ... or should we set == position.align
+ -- ^ Content positioning with justify and align
+ , _shadow :: WhenTW (WithTransition BoxShadow)
+ -- ^ Box shadow (transitionable). Default: empty (no shadow)
+ , _cursor :: WhenTW CursorStyle
+ -- ^ Mouse cursor style. Default: empty (default cursor)
+ , _transform :: TransformConfig
+ -- ^ All CSS transforms (rotate, scale, translate, skew, origin - transitionable)
, _box_custom :: T.Text
+ -- ^ Arbitrary custom Tailwind classes. Default: empty string
}
deriving Show
makeLenses ''BoxConfig
-
------------ Defaults of Records
instance Default BoxConfig where
- def = BoxConfig def def def def def def def def def def def ""
+ def = BoxConfig def def def def def def def def def def def def ""
instance CompileStyle BoxConfig where
@@ -130,10 +436,11 @@ instance CompileStyle BoxConfig where
, compileSizingBand (_sizingBand cfg)
, compilePadding (_padding cfg)
, compileMargin (_margin cfg)
- , compileWhenTW (_bgColor cfg) ((<>) "bg-" . showTW)
- , compileWhenTW (_bgOpacity cfg) ((<>) "bg-opacity-" . tshow)
- , compileWhenTW (_shadow cfg) showTW
- , compileTransition (_transition cfg)
+ , compileBgColor (_bgColor cfg)
+ , compileWithTransitionTW (_bgOpacity cfg) ((<>) "bg-opacity-" . tshow) Transition_Opacity
+ , compileWithTransitionTW (_shadow cfg) showTW Transition_Shadow
+ , compileWhenTW (_cursor cfg) showTW
+ , compileS (_transform cfg)
, Right $ _box_custom cfg
]
where
@@ -147,54 +454,47 @@ instance CompileStyle BoxConfig where
]
compileBorderRadius cfg' = pure . foldr (<&>) mempty =<< sequenceA
- [ compileWhenTW (_borderRadius_tr cfg') ((<>) "rounded-tr" . showTW)
- , compileWhenTW (_borderRadius_tl cfg') ((<>) "rounded-tl" . showTW)
- , compileWhenTW (_borderRadius_br cfg') ((<>) "rounded-br" . showTW)
- , compileWhenTW (_borderRadius_bl cfg') ((<>) "rounded-bl" . showTW)
+ [ compileWithTransitionTW (_borderRadius_tr cfg') ((<>) "rounded-tr" . showTW) Transition_All
+ , compileWithTransitionTW (_borderRadius_tl cfg') ((<>) "rounded-tl" . showTW) Transition_All
+ , compileWithTransitionTW (_borderRadius_br cfg') ((<>) "rounded-br" . showTW) Transition_All
+ , compileWithTransitionTW (_borderRadius_bl cfg') ((<>) "rounded-bl" . showTW) Transition_All
]
compileBorderWidth cfg' = pure . foldr (<&>) mempty =<< sequenceA
- [ compileWhenTW (_borderWidth_l cfg') ((<>) "border-l" . showTW)
- , compileWhenTW (_borderWidth_r cfg') ((<>) "border-r" . showTW)
- , compileWhenTW (_borderWidth_t cfg') ((<>) "border-t" . showTW)
- , compileWhenTW (_borderWidth_b cfg') ((<>) "border-b" . showTW)
+ [ compileWithTransitionTW (_borderWidth_l cfg') ((<>) "border-l" . showTW) Transition_All
+ , compileWithTransitionTW (_borderWidth_r cfg') ((<>) "border-r" . showTW) Transition_All
+ , compileWithTransitionTW (_borderWidth_t cfg') ((<>) "border-t" . showTW) Transition_All
+ , compileWithTransitionTW (_borderWidth_b cfg') ((<>) "border-b" . showTW) Transition_All
]
compileBorderColor cfg' = pure . foldr (<&>) mempty =<< sequenceA
- [ compileWhenTW (_borderColor_l cfg') ((<>) "border-l-" . showTW)
- , compileWhenTW (_borderColor_r cfg') ((<>) "border-r-" . showTW)
- , compileWhenTW (_borderColor_t cfg') ((<>) "border-t-" . showTW)
- , compileWhenTW (_borderColor_b cfg') ((<>) "border-b-" . showTW)
+ [ compileWithTransitionTW (_borderColor_l cfg') ((<>) "border-l-" . showTW) Transition_Colors
+ , compileWithTransitionTW (_borderColor_r cfg') ((<>) "border-r-" . showTW) Transition_Colors
+ , compileWithTransitionTW (_borderColor_t cfg') ((<>) "border-t-" . showTW) Transition_Colors
+ , compileWithTransitionTW (_borderColor_b cfg') ((<>) "border-b-" . showTW) Transition_Colors
]
compileRing cfg' = pure . foldr (<&>) mempty =<< sequenceA
- [ compileWhenTW (_ringWidth cfg') showTW
- , compileWhenTW (_ringColor cfg') ((<>) "ring-" . showTW)
- , compileWhenTW (_ringOpacity cfg') ((<>) "ring-opacity-" . tshow)
+ [ compileWithTransitionTW (_ringWidth cfg') showTW Transition_All
+ , compileWithTransitionTW (_ringColor cfg') ((<>) "ring-" . showTW) Transition_Colors
+ , compileWithTransitionTW (_ringOpacity cfg') ((<>) "ring-opacity-" . tshow) Transition_Opacity
]
compileSizingBand cfg' = pure . foldr (<&>) mempty =<< sequenceA
- [ compileWhenTW (_widthC . _maxSize $ cfg') ((<>) "max-w-" . showTW)
- , compileWhenTW (_heightC . _maxSize $ cfg') ((<>) "max-h-" . showTW)
- , compileWhenTW (_widthC . _minSize $ cfg') ((<>) "min-w-" . showTW)
- , compileWhenTW (_heightC . _minSize $ cfg') ((<>) "min-h-" . showTW)
- , compileWhenTW (_width . _size $ cfg') ((<>) "w-" . showTW)
- , compileWhenTW (_height . _size $ cfg') ((<>) "h-" . showTW)
+ [ compileWithTransitionTW (_widthC . _maxSize $ cfg') ((<>) "max-w-" . showTW) Transition_All
+ , compileWithTransitionTW (_heightC . _maxSize $ cfg') ((<>) "max-h-" . showTW) Transition_All
+ , compileWithTransitionTW (_widthC . _minSize $ cfg') ((<>) "min-w-" . showTW) Transition_All
+ , compileWithTransitionTW (_heightC . _minSize $ cfg') ((<>) "min-h-" . showTW) Transition_All
+ , compileWithTransitionTW (_width . _size $ cfg') ((<>) "w-" . showTW) Transition_All
+ , compileWithTransitionTW (_height . _size $ cfg') ((<>) "h-" . showTW) Transition_All
]
compileMargin cfg' = pure . foldr (<&>) mempty =<< sequenceA
- [ compileWhenTW (_marginL cfg') ((<>) "ml-" . showTW)
- , compileWhenTW (_marginR cfg') ((<>) "mr-" . showTW)
- , compileWhenTW (_marginT cfg') ((<>) "mt-" . showTW)
- , compileWhenTW (_marginB cfg') ((<>) "mb-" . showTW)
- ]
-
- compileTransition cfg' = pure . foldr (<&>) mempty =<< sequenceA
- [ compileWhenTW (_transitionProperty cfg') showTW
- , compileWhenTW (_transitionDuration cfg') showTW
- , compileWhenTW (_transitionTiming cfg') showTW
- , compileWhenTW (_transitionDelay cfg') showTW
+ [ compileWithTransitionTW (_marginL cfg') ((<>) "ml-" . showTW) Transition_All
+ , compileWithTransitionTW (_marginR cfg') ((<>) "mr-" . showTW) Transition_All
+ , compileWithTransitionTW (_marginT cfg') ((<>) "mt-" . showTW) Transition_All
+ , compileWithTransitionTW (_marginB cfg') ((<>) "mb-" . showTW) Transition_All
]
compilePos posCfg = case f $ fmap fst posCfg of
@@ -217,8 +517,8 @@ instance ShowTW BoxConfig where
showTW cfg = foldr (<&>) mempty
[ renderWhenTW (_colStart cfg) ((<>) "col-start-" . tshow)
, renderWhenTW (_colSpan cfg) ((<>) "col-span-" . tshow)
- , renderWhenTW (_bgColor cfg) ((<>) "bg-" . showTW)
- , renderWhenTW (_bgOpacity cfg) ((<>) "bg-opacity-" . tshow)
+ , renderBgColorTW (_bgColor cfg)
+ , renderWithTransitionTW (_bgOpacity cfg) ((<>) "bg-opacity-" . tshow) Transition_Opacity
, showTW . _border $ cfg
, showTW . _sizingBand $ cfg
, showTW . _padding $ cfg
@@ -228,9 +528,8 @@ instance ShowTW BoxConfig where
let prefix = if c == "def" then "" else (c <> ":")
in prefix <> "grid" <&> prefix <> (showTW jus) <&> prefix <> (showTW align)
) $ _position cfg
- , renderWhenTW (_shadow cfg) showTW
- , showTW . _transition $ cfg
- --, renderWhenTW (_position cfg) $ \(j,a) -> "grid " <> showTW j <> " " <> showTW a
+ , renderWithTransitionTW (_shadow cfg) showTW Transition_Shadow
+ , showTW . _transform $ cfg
, _box_custom cfg
]
@@ -249,6 +548,52 @@ instance Semigroup BoxConfig where
, _border = _border a <> _border b
, _position = _position a <> _position b
, _shadow = _shadow a <> _shadow b
- , _transition = _transition a <> _transition b
+ , _cursor = _cursor a <> _cursor b
+ , _transform = _transform a <> _transform b
, _box_custom = _box_custom a <> _box_custom b
}
+
+-- | Render a GradientColor for bgColor without any prefix.
+-- Returns the raw class(es) that need prefixing.
+renderBgColorRaw :: GradientColor -> T.Text
+renderBgColorRaw (SolidColor cwo) = "bg-" <> showTW cwo
+renderBgColorRaw (GradientColor cfg) = showTW cfg
+
+-- | Apply a prefix to each space-separated class.
+-- "hover:" "bg-gradient-to-r from-blue-500" -> "hover:bg-gradient-to-r hover:from-blue-500"
+applyPrefixToClasses :: T.Text -> T.Text -> T.Text
+applyPrefixToClasses prefix classes =
+ T.intercalate " " $ fmap (\c -> prefix <> c) $ T.words classes
+
+-- | Custom render for bgColor that handles gradient multi-class output.
+-- Gradients produce multiple space-separated classes that each need
+-- the responsive/state prefix applied.
+renderBgColorTW :: WhenTW (WithTransition GradientColor) -> T.Text
+renderBgColorTW tws = foldr (<&>) mempty $
+ fmap (\(c, WithTransition val mTransCfg) ->
+ let prefix = if c == "def" then "" else (c <> ":")
+ classes = renderBgColorRaw val
+ valueClasses = applyPrefixToClasses prefix classes
+ transitionClasses = case mTransCfg of
+ Nothing -> mempty
+ Just cfg ->
+ let cssProp = transitionPropertyToCSSName Transition_Colors
+ duration = T.drop 9 $ tshow (_transitionDuration cfg)
+ timing = transitionTimingToCSSName (_transitionTiming cfg)
+ delay = T.drop 6 $ tshow (_transitionDelay cfg)
+ transValue = cssProp <> "_" <> duration <> "ms_" <> timing <> "_" <> delay <> "ms"
+ in prefix <> "[transition:" <> transValue <> "]"
+ in valueClasses <&> transitionClasses
+ ) tws
+
+-- | Custom compile for bgColor with duplicate checking.
+compileBgColor :: WhenTW (WithTransition GradientColor) -> Either T.Text T.Text
+compileBgColor tws = case checkDuplicates $ fmap fst tws of
+ Left e -> Left e
+ Right () -> Right $ renderBgColorTW tws
+ where
+ checkDuplicates [] = Right ()
+ checkDuplicates (s:ss) =
+ if elem s ss
+ then Left $ s <> " exists twice"
+ else checkDuplicates ss
diff --git a/src/Classh/Box/Border/Color.hs b/src/Classh/Box/Border/Color.hs
index 4c79305..a749e9f 100644
--- a/src/Classh/Box/Border/Color.hs
+++ b/src/Classh/Box/Border/Color.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE FlexibleInstances #-}
+
module Classh.Box.Border.Color where
import Classh.Class.ShowTW
@@ -5,26 +7,30 @@ import Classh.Class.SetSides
import Classh.Responsive.WhenTW
import Classh.Internal.Chain
import Classh.Color
+import Classh.WithTransition
+import Classh.Box.Transition (TransitionProperty(..))
import Data.Default
import Control.Lens (lens, makeLenses)
--- |Holds Border 'Color' by side
---
+-- |Holds Border 'Color' by side (transitionable)
+--
-- For example:
---
+--
-- > elClass "div" $(classh' [ border . bColor . borderColor_t .~~ Black ])
-- > -- Or with shorthand
-- > elClass "div" $(classh' [ bc_t .~~ Black ])
+-- > -- With transitions:
+-- > elClass "div" $(classh' [ bc_t .~^ [("def", Black), ("hover", Red `withTransition` Duration_300)] ])
data BorderColorSides = BorderColorSides
- { _borderColor_l :: WhenTW Color
+ { _borderColor_l :: WhenTW (WithTransition ColorWithOpacity)
-- ^ border-l-'Color' ... see https://tailwindcss.com/docs/border-color
- , _borderColor_r :: WhenTW Color
+ , _borderColor_r :: WhenTW (WithTransition ColorWithOpacity)
-- ^ border-r-'Color' ... see https://tailwindcss.com/docs/border-color
- , _borderColor_t :: WhenTW Color
+ , _borderColor_t :: WhenTW (WithTransition ColorWithOpacity)
-- ^ border-t-'Color' ... see https://tailwindcss.com/docs/border-color
- , _borderColor_b :: WhenTW Color
- -- ^ border-b-'Color' ... see https://tailwindcss.com/docs/border-color
+ , _borderColor_b :: WhenTW (WithTransition ColorWithOpacity)
+ -- ^ border-b-'Color' ... see https://tailwindcss.com/docs/border-color
} deriving Show
instance Default BorderColorSides where
@@ -33,16 +39,17 @@ instance Default BorderColorSides where
instance ShowTW BorderColorSides where
showTW cfg = foldr (<&>) mempty
- [ renderWhenTW (_borderColor_l cfg) ((<>) "border-l-" . showTW)
- , renderWhenTW (_borderColor_r cfg) ((<>) "border-r-" . showTW)
- , renderWhenTW (_borderColor_t cfg) ((<>) "border-t-" . showTW)
- , renderWhenTW (_borderColor_b cfg) ((<>) "border-b-" . showTW)
+ [ renderWithTransitionTW (_borderColor_l cfg) ((<>) "border-l-" . showTW) Transition_Colors
+ , renderWithTransitionTW (_borderColor_r cfg) ((<>) "border-r-" . showTW) Transition_Colors
+ , renderWithTransitionTW (_borderColor_t cfg) ((<>) "border-t-" . showTW) Transition_Colors
+ , renderWithTransitionTW (_borderColor_b cfg) ((<>) "border-b-" . showTW) Transition_Colors
]
makeLenses ''BorderColorSides
-- | Like border-'Color', eg border-white
-instance SetSides BorderColorSides Color where
+-- Now uses WithTransition ColorWithOpacity so .~~ will auto-wrap, and .~^ allows transitions
+instance SetSides BorderColorSides (WithTransition ColorWithOpacity) where
l = borderColor_l
r = borderColor_r
t = borderColor_t
diff --git a/src/Classh/Box/Border/Radius.hs b/src/Classh/Box/Border/Radius.hs
index 9e3518a..d0adb7b 100644
--- a/src/Classh/Box/Border/Radius.hs
+++ b/src/Classh/Box/Border/Radius.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE FlexibleInstances #-}
+
module Classh.Box.Border.Radius where
import Classh.Class.ShowTW
@@ -7,23 +9,27 @@ import Classh.Responsive.WhenTW
import Classh.Internal.Chain
import Classh.Internal.CSSSize
import Classh.Internal.TShow
+import Classh.WithTransition
+import Classh.Box.Transition (TransitionProperty(..))
import Data.Default
import Control.Lens (Lens', lens, makeLenses)
import qualified Data.Text as T
--- |Holds 'BorderRadius' by corner
+-- |Holds 'BorderRadius' by corner (transitionable)
-- see https://tailwindcss.com/docs/border-radius
---
+--
-- For example:
---
+--
-- > elClass "div" $(classh' [ border . radius . borderRadius_tr .~~ R_3Xl, border . radius . borderRadius_tl .~~ R_3Xl ])
-- > -- Or with shorthand
-- > elClass "div" $(classh' [ br_t .~~ R_3Xl ])
+-- > -- With transitions:
+-- > elClass "div" $(classh' [ br_t .~^ [("def", R_None), ("hover", R_3Xl `withTransition` Duration_300)] ])
data BorderRadiusCorners = BorderRadiusCorners
- { _borderRadius_tr :: WhenTW BorderRadius'
- , _borderRadius_tl :: WhenTW BorderRadius'
- , _borderRadius_br :: WhenTW BorderRadius'
- , _borderRadius_bl :: WhenTW BorderRadius'
+ { _borderRadius_tr :: WhenTW (WithTransition BorderRadius')
+ , _borderRadius_tl :: WhenTW (WithTransition BorderRadius')
+ , _borderRadius_br :: WhenTW (WithTransition BorderRadius')
+ , _borderRadius_bl :: WhenTW (WithTransition BorderRadius')
} deriving Show
-- | Border radius options, eg R_3Xl ==> "rounded-3xl"
@@ -60,14 +66,14 @@ instance Default BorderRadiusCorners where
-- TODO: stop overlaps through conditionals
instance ShowTW BorderRadiusCorners where
showTW cfg = foldr (<&>) mempty
- [ renderWhenTW (_borderRadius_tr cfg) ((<>) "rounded-tr" . showTW)
- , renderWhenTW (_borderRadius_tl cfg) ((<>) "rounded-tl" . showTW)
- , renderWhenTW (_borderRadius_br cfg) ((<>) "rounded-br" . showTW)
- , renderWhenTW (_borderRadius_bl cfg) ((<>) "rounded-bl" . showTW)
+ [ renderWithTransitionTW (_borderRadius_tr cfg) ((<>) "rounded-tr" . showTW) Transition_All
+ , renderWithTransitionTW (_borderRadius_tl cfg) ((<>) "rounded-tl" . showTW) Transition_All
+ , renderWithTransitionTW (_borderRadius_br cfg) ((<>) "rounded-br" . showTW) Transition_All
+ , renderWithTransitionTW (_borderRadius_bl cfg) ((<>) "rounded-bl" . showTW) Transition_All
]
-- | Like rounded-(t|r|b|l|tl|...)-'BorderRadius'', eg rounded-tl-xl
-instance SetSides BorderRadiusCorners BorderRadius' where
+instance SetSides BorderRadiusCorners (WithTransition BorderRadius') where
l = borderRadius_l
r = borderRadius_r
b = borderRadius_b
@@ -84,7 +90,7 @@ instance SetSides BorderRadiusCorners BorderRadius' where
-borderRadius_l, borderRadius_r, borderRadius_t, borderRadius_b :: Lens' BorderRadiusCorners (WhenTW BorderRadius')
+borderRadius_l, borderRadius_r, borderRadius_t, borderRadius_b :: Lens' BorderRadiusCorners (WhenTW (WithTransition BorderRadius'))
borderRadius_l = lens undefined $ \tw new -> tw { _borderRadius_tl = new, _borderRadius_bl = new }
borderRadius_r = lens undefined $ \tw new -> tw { _borderRadius_tr = new, _borderRadius_br = new }
borderRadius_t = lens undefined $ \tw new -> tw { _borderRadius_tl = new, _borderRadius_tr = new }
diff --git a/src/Classh/Box/Border/Width.hs b/src/Classh/Box/Border/Width.hs
index 9070522..92596b0 100644
--- a/src/Classh/Box/Border/Width.hs
+++ b/src/Classh/Box/Border/Width.hs
@@ -1,3 +1,5 @@
+{-# LANGUAGE FlexibleInstances #-}
+
module Classh.Box.Border.Width where
import Classh.Class.ShowTW
@@ -7,23 +9,27 @@ import Classh.Responsive.WhenTW
import Classh.Internal.Chain
import Classh.Internal.CSSSize
import Classh.Internal.TShow
+import Classh.WithTransition
+import Classh.Box.Transition (TransitionProperty(..))
import Data.Default
import Control.Lens (lens, makeLenses)
import qualified Data.Text as T
--- |Holds 'BorderWidth' by side.
+-- |Holds 'BorderWidth' by side (transitionable).
-- see https://tailwindcss.com/docs/border-width
---
+--
-- For example:
--
-- > elClass "div" $(classh' [ border . bWidth . borderWidth_t .~~ B2 ])
-- > -- Or with shorthand
-- > elClass "div" $(classh' [ bw_t .~~ B2 ])
+-- > -- With transitions:
+-- > elClass "div" $(classh' [ bw_t .~^ [("def", B2), ("hover", B4 `withTransition` Duration_300)] ])
data BorderWidthSides = BorderWidthSides
- { _borderWidth_l :: WhenTW BorderWidth
- , _borderWidth_r :: WhenTW BorderWidth
- , _borderWidth_t :: WhenTW BorderWidth
- , _borderWidth_b :: WhenTW BorderWidth
+ { _borderWidth_l :: WhenTW (WithTransition BorderWidth)
+ , _borderWidth_r :: WhenTW (WithTransition BorderWidth)
+ , _borderWidth_t :: WhenTW (WithTransition BorderWidth)
+ , _borderWidth_b :: WhenTW (WithTransition BorderWidth)
} deriving Show
-- | Border Width options, eg. B0 ==> "border-0"
@@ -53,17 +59,17 @@ instance Default BorderWidthSides where
instance ShowTW BorderWidthSides where
showTW cfg = foldr (<&>) mempty
- [ renderWhenTW (_borderWidth_l cfg) ((<>) "border-l" . showTW)
- , renderWhenTW (_borderWidth_r cfg) ((<>) "border-r" . showTW)
- , renderWhenTW (_borderWidth_t cfg) ((<>) "border-t" . showTW)
- , renderWhenTW (_borderWidth_b cfg) ((<>) "border-b" . showTW)
+ [ renderWithTransitionTW (_borderWidth_l cfg) ((<>) "border-l" . showTW) Transition_All
+ , renderWithTransitionTW (_borderWidth_r cfg) ((<>) "border-r" . showTW) Transition_All
+ , renderWithTransitionTW (_borderWidth_t cfg) ((<>) "border-t" . showTW) Transition_All
+ , renderWithTransitionTW (_borderWidth_b cfg) ((<>) "border-b" . showTW) Transition_All
]
-
+
makeLenses ''BorderWidthSides
-- | Like border-l-'BorderWidth', eg border-l-8
-instance SetSides BorderWidthSides BorderWidth where
+instance SetSides BorderWidthSides (WithTransition BorderWidth) where
l = borderWidth_l
r = borderWidth_r
t = borderWidth_t
diff --git a/src/Classh/Box/Gradient.hs b/src/Classh/Box/Gradient.hs
new file mode 100644
index 0000000..6c6f590
--- /dev/null
+++ b/src/Classh/Box/Gradient.hs
@@ -0,0 +1,288 @@
+--------------------------------------------------------------------------------
+-- |
+-- Module : Classh.Box.Gradient
+-- Copyright : (c) 2024, Galen Sprout
+-- License : BSD-style (see end of this file)
+--
+-- Maintainer : Galen Sprout
+-- Stability : provisional
+-- Portability : portable
+--
+-- Type-safe gradient support for Tailwind CSS gradients.
+--
+-- = Overview
+--
+-- This module provides 'GradientColor', a type that encompasses both solid
+-- colors and gradient configurations. It replaces 'Color' in bgColor fields,
+-- allowing gradients to be used anywhere colors are used.
+--
+-- = Quick Example
+--
+-- @
+-- -- Solid color (most common):
+-- bgColor .~~ solid acePrimary
+--
+-- -- Simple two-color gradient:
+-- bgColor .~~ linearGradient To_R (hex \"4E366C\") White
+-- -- Generates: bg-gradient-to-r from-[#4E366C] to-white
+--
+-- -- Gradient with stop positions:
+-- bgColor .~~ linearGradientViaPos To_R
+-- (stopAt (hex \"4E366C\") 10)
+-- (stopAt (Pink C500) 30)
+-- (stopAt White 90)
+-- -- Generates: bg-gradient-to-r from-[#4E366C] from-10% via-pink-500 via-30% to-white to-90%
+-- @
+--
+--------------------------------------------------------------------------------
+
+module Classh.Box.Gradient
+ (
+ -- * Core Types
+ GradientColor(..)
+ , GradientConfig(..)
+ , GradientDirection(..)
+ , ColorStop(..)
+ , StopPosition(..)
+ -- * Solid Color Helper
+ , solidColor
+ , solidColorOpacity
+ -- * Color Stop Helpers
+ , stop
+ , stopAt
+ , stopWithOpacity
+ , stopAtWithOpacity
+ -- * Gradient Builders
+ , linearGradient
+ , linearGradientVia
+ , linearGradientPos
+ , linearGradientViaPos
+ , gradientFrom
+ ) where
+
+import Classh.Class.ShowTW
+import Classh.Color
+import Classh.Internal.TShow
+
+import Data.Default
+import qualified Data.Text as T
+
+-- | Direction for linear gradients.
+--
+-- Maps to Tailwind's gradient direction classes like @bg-gradient-to-r@.
+data GradientDirection
+ = To_T -- ^ to top
+ | To_TR -- ^ to top-right
+ | To_R -- ^ to right (most common)
+ | To_BR -- ^ to bottom-right
+ | To_B -- ^ to bottom
+ | To_BL -- ^ to bottom-left
+ | To_L -- ^ to left
+ | To_TL -- ^ to top-left
+ deriving (Show, Eq)
+
+-- | Percentage for stop positions (0-100).
+--
+-- Maps to Tailwind's position classes like @from-10%@, @via-30%@, @to-90%@.
+newtype StopPosition = StopPosition Int
+ deriving (Show, Eq)
+
+-- | A color stop with optional position.
+--
+-- Uses 'ColorWithOpacity' which embeds opacity directly in the color.
+--
+-- === Examples
+--
+-- @
+-- stop White -- Just the color
+-- stopAt (Blue C500) 30 -- Color at 30%
+-- stopWithOpacity (hex "181422") 90 -- Color with 90% opacity
+-- stopAtWithOpacity (hex "181422") 90 0 -- Opacity + position
+-- @
+data ColorStop = ColorStop
+ { _stop_color :: ColorWithOpacity -- ^ Color with optional opacity
+ , _stop_position :: Maybe StopPosition
+ } deriving (Show, Eq)
+
+-- | Gradient configuration with direction and color stops.
+data GradientConfig = GradientConfig
+ { _gradient_direction :: GradientDirection
+ , _gradient_from :: ColorStop -- ^ Starting color (required)
+ , _gradient_via :: Maybe ColorStop -- ^ Middle color (optional)
+ , _gradient_to :: Maybe ColorStop -- ^ Ending color (optional)
+ } deriving (Show, Eq)
+
+-- | Union type: either a solid color or a gradient.
+--
+-- This type replaces 'Color' in '_bgColor' and similar fields, allowing
+-- both simple colors and gradients to be used interchangeably.
+--
+-- === Examples
+--
+-- @
+-- solidColor White -- bg-white
+-- solidColorOpacity (hex "1e40af") 50 -- bg-[#1e40af]/50
+-- linearGradient To_R ... -- bg-gradient-to-r from-...
+-- @
+data GradientColor
+ = SolidColor ColorWithOpacity -- ^ Solid color (with optional opacity via ColorWithOpacity)
+ | GradientColor GradientConfig
+ deriving (Show, Eq)
+
+-- | Default is transparent solid color
+instance Default GradientColor where
+ def = SolidColor (color Transparent)
+
+instance ShowTW GradientDirection where
+ showTW To_T = "to-t"
+ showTW To_TR = "to-tr"
+ showTW To_R = "to-r"
+ showTW To_BR = "to-br"
+ showTW To_B = "to-b"
+ showTW To_BL = "to-bl"
+ showTW To_L = "to-l"
+ showTW To_TL = "to-tl"
+
+instance ShowTW StopPosition where
+ showTW (StopPosition p) = tshow p <> "%"
+
+-- | Helper to render a color stop with position.
+-- Generates output like @from-blue-500 from-10%@, @via-pink-500/50@, or @to-[#hex]/90 to-100%@
+renderStop :: T.Text -> ColorStop -> T.Text
+renderStop prefix (ColorStop cwo mpos) =
+ prefix <> "-" <> showTW cwo <>
+ maybe "" (\(StopPosition p) -> " " <> prefix <> "-" <> tshow p <> "%") mpos
+
+instance ShowTW GradientConfig where
+ showTW (GradientConfig dir from mvia mto) =
+ "bg-gradient-" <> showTW dir <> " " <>
+ renderStop "from" from <>
+ maybe "" (\v -> " " <> renderStop "via" v) mvia <>
+ maybe "" (\t -> " " <> renderStop "to" t) mto
+
+instance ShowTW GradientColor where
+ showTW (SolidColor cwo) = showTW cwo
+ showTW (GradientColor cfg) = showTW cfg
+
+--------------------------------------------------------------------------------
+-- Helper Functions
+--------------------------------------------------------------------------------
+
+-- | Create a solid (non-gradient) color.
+--
+-- Use this when you want a simple background color without any gradient.
+--
+-- === Example
+--
+-- @
+-- bgColor .~~ solidColor White
+-- bgColor .~~ solidColor (Blue C500)
+-- @
+solidColor :: Color -> GradientColor
+solidColor c = SolidColor (color c)
+
+-- | Create a solid color with opacity.
+--
+-- Use this for semi-transparent backgrounds. Outputs Tailwind's @/opacity@ syntax.
+--
+-- === Example
+--
+-- @
+-- bgColor .~~ solidColorOpacity (hex "221326") 87
+-- -- Generates: bg-[#221326]/87
+--
+-- bgColor .~~ solidColorOpacity (Blue C500) 50
+-- -- Generates: bg-blue-500/50
+-- @
+solidColorOpacity :: Color -> Int -> GradientColor
+solidColorOpacity c opacity = SolidColor (withOpacity c opacity)
+
+-- | Create a color stop without opacity or position.
+--
+-- @
+-- stop White -- ColorStop (color White) Nothing
+-- @
+stop :: Color -> ColorStop
+stop c = ColorStop (color c) Nothing
+
+-- | Create a color stop at a specific position (0-100%).
+--
+-- @
+-- stopAt (Blue C500) 30 -- Blue at 30%
+-- @
+stopAt :: Color -> Int -> ColorStop
+stopAt c p = ColorStop (color c) (Just $ StopPosition p)
+
+-- | Create a color stop with opacity but no position.
+--
+-- @
+-- stopWithOpacity (hex "181422") 90 -- [#181422]/90
+-- @
+stopWithOpacity :: Color -> Int -> ColorStop
+stopWithOpacity c opacity = ColorStop (withOpacity c opacity) Nothing
+
+-- | Create a color stop with both opacity and position.
+--
+-- @
+-- stopAtWithOpacity (hex "181422") 90 0 -- [#181422]/90 at 0%
+-- @
+stopAtWithOpacity :: Color -> Int -> Int -> ColorStop
+stopAtWithOpacity c opacity pos = ColorStop (withOpacity c opacity) (Just $ StopPosition pos)
+
+-- | Create a two-color linear gradient.
+--
+-- === Example
+--
+-- @
+-- linearGradient To_R (hex \"4E366C\") White
+-- -- Generates: bg-gradient-to-r from-[#4E366C] to-white
+-- @
+linearGradient :: GradientDirection -> Color -> Color -> GradientColor
+linearGradient dir from to = GradientColor $ GradientConfig dir (stop from) Nothing (Just $ stop to)
+
+-- | Create a three-color linear gradient with a middle color.
+--
+-- === Example
+--
+-- @
+-- linearGradientVia To_BR (Purple C500) (Pink C500) White
+-- -- Generates: bg-gradient-to-br from-purple-500 via-pink-500 to-white
+-- @
+linearGradientVia :: GradientDirection -> Color -> Color -> Color -> GradientColor
+linearGradientVia dir from via to = GradientColor $ GradientConfig dir (stop from) (Just $ stop via) (Just $ stop to)
+
+-- | Create a two-color gradient with explicit stop positions.
+--
+-- === Example
+--
+-- @
+-- linearGradientPos To_R (stopAt (hex \"4E366C\") 10) (stopAt White 90)
+-- -- Generates: bg-gradient-to-r from-[#4E366C] from-10% to-white to-90%
+-- @
+linearGradientPos :: GradientDirection -> ColorStop -> ColorStop -> GradientColor
+linearGradientPos dir from to = GradientColor $ GradientConfig dir from Nothing (Just to)
+
+-- | Create a three-color gradient with explicit stop positions.
+--
+-- === Example
+--
+-- @
+-- linearGradientViaPos To_R
+-- (stopAt (hex \"4E366C\") 10)
+-- (stopAt (Pink C500) 30)
+-- (stopAt White 90)
+-- -- Generates: bg-gradient-to-r from-[#4E366C] from-10% via-pink-500 via-30% to-white to-90%
+-- @
+linearGradientViaPos :: GradientDirection -> ColorStop -> ColorStop -> ColorStop -> GradientColor
+linearGradientViaPos dir from via to = GradientColor $ GradientConfig dir from (Just via) (Just to)
+
+-- | Create a single-color gradient that fades to transparent.
+--
+-- === Example
+--
+-- @
+-- gradientFrom To_R (hex \"4E366C\")
+-- -- Generates: bg-gradient-to-r from-[#4E366C]
+-- @
+gradientFrom :: GradientDirection -> Color -> GradientColor
+gradientFrom dir from = GradientColor $ GradientConfig dir (stop from) Nothing Nothing
diff --git a/src/Classh/Box/Margin.hs b/src/Classh/Box/Margin.hs
index 42f20f0..8acf8cd 100644
--- a/src/Classh/Box/Margin.hs
+++ b/src/Classh/Box/Margin.hs
@@ -1,5 +1,6 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE FlexibleInstances #-}
--------------------------------------------------------------------------------
-- |
@@ -45,8 +46,10 @@ import Classh.Internal.Chain
import Classh.Class.ShowTW
import Classh.Class.SetSides
import Classh.Responsive.WhenTW
+import Classh.WithTransition
+import Classh.Box.Transition (TransitionProperty(..))
-import Classh.Box.TWSize as X
+import Classh.Box.TWSize as X
import Control.Lens hiding ((<&>))
import Data.Default
@@ -57,22 +60,22 @@ instance Default BoxMargin where
instance ShowTW BoxMargin where
showTW cfg = foldr (<&>) mempty
- [ renderWhenTW (_marginL cfg) ((<>) "ml-" . showTW)
- , renderWhenTW (_marginR cfg) ((<>) "mr-" . showTW)
- , renderWhenTW (_marginT cfg) ((<>) "mt-" . showTW)
- , renderWhenTW (_marginB cfg) ((<>) "mb-" . showTW)
+ [ renderWithTransitionTW (_marginL cfg) ((<>) "ml-" . showTW) Transition_All
+ , renderWithTransitionTW (_marginR cfg) ((<>) "mr-" . showTW) Transition_All
+ , renderWithTransitionTW (_marginT cfg) ((<>) "mt-" . showTW) Transition_All
+ , renderWithTransitionTW (_marginB cfg) ((<>) "mb-" . showTW) Transition_All
]
--- | Type representing '_margin' field of 'BoxConfig'.
+-- | Type representing '_margin' field of 'BoxConfig' (transitionable).
-- | based on https://tailwindcss.com/docs/margin
data BoxMargin = BoxMargin
- { _marginL :: WhenTW TWSize
- -- ^ see shorthand: @ml@
- , _marginR :: WhenTW TWSize
+ { _marginL :: WhenTW (WithTransition TWSize)
+ -- ^ see shorthand: @ml@
+ , _marginR :: WhenTW (WithTransition TWSize)
-- ^ see shorthand: 'mr'
- , _marginT :: WhenTW TWSize
+ , _marginT :: WhenTW (WithTransition TWSize)
-- ^ see shorthand: 'mt'
- , _marginB :: WhenTW TWSize
+ , _marginB :: WhenTW (WithTransition TWSize)
-- ^ see shorthand: 'mb'
} deriving Show
@@ -89,7 +92,7 @@ instance Semigroup BoxMargin where
-- | This is technically an illegal lens however if you ran 2 setters which overlap so that a /= b
-- | where a and b are the fields associated with respective separate fields, then classh' will
-- | most likely catch the error. Additionally, there is a lens way to access any field anyways
-instance SetSides BoxMargin TWSize where
+instance SetSides BoxMargin (WithTransition TWSize) where
l = marginL
r = marginR
b = marginB
diff --git a/src/Classh/Box/Padding.hs b/src/Classh/Box/Padding.hs
index dc3d62f..c253e82 100644
--- a/src/Classh/Box/Padding.hs
+++ b/src/Classh/Box/Padding.hs
@@ -1,5 +1,6 @@
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE FlexibleInstances #-}
--------------------------------------------------------------------------------
-- |
@@ -47,8 +48,10 @@ import Classh.Class.ShowTW
import Classh.Class.SetSides
import Classh.Class.CompileStyle
import Classh.Responsive.WhenTW
+import Classh.WithTransition
+import Classh.Box.Transition (TransitionProperty(..))
-import Classh.Box.TWSize as X
+import Classh.Box.TWSize as X
import Control.Lens hiding ((<&>))
import Data.Default
@@ -63,10 +66,10 @@ instance Default BoxPadding where
instance ShowTW BoxPadding where
showTW cfg = foldr (<&>) mempty
- [ renderWhenTW (_paddingL cfg) ((<>) "pl-" . showTW)
- , renderWhenTW (_paddingR cfg) ((<>) "pr-" . showTW)
- , renderWhenTW (_paddingT cfg) ((<>) "pt-" . showTW)
- , renderWhenTW (_paddingB cfg) ((<>) "pb-" . showTW)
+ [ renderWithTransitionTW (_paddingL cfg) ((<>) "pl-" . showTW) Transition_All
+ , renderWithTransitionTW (_paddingR cfg) ((<>) "pr-" . showTW) Transition_All
+ , renderWithTransitionTW (_paddingT cfg) ((<>) "pt-" . showTW) Transition_All
+ , renderWithTransitionTW (_paddingB cfg) ((<>) "pb-" . showTW) Transition_All
]
-- | For row func
@@ -75,22 +78,22 @@ instance CompileStyle BoxPadding where
compilePadding :: BoxPadding -> Either T.Text T.Text
compilePadding cfg = pure . foldr (<&>) mempty =<< sequenceA
- [ compileWhenTW (_paddingL cfg) ((<>) "pl-" . showTW)
- , compileWhenTW (_paddingR cfg) ((<>) "pr-" . showTW)
- , compileWhenTW (_paddingT cfg) ((<>) "pt-" . showTW)
- , compileWhenTW (_paddingB cfg) ((<>) "pb-" . showTW)
+ [ compileWithTransitionTW (_paddingL cfg) ((<>) "pl-" . showTW) Transition_All
+ , compileWithTransitionTW (_paddingR cfg) ((<>) "pr-" . showTW) Transition_All
+ , compileWithTransitionTW (_paddingT cfg) ((<>) "pt-" . showTW) Transition_All
+ , compileWithTransitionTW (_paddingB cfg) ((<>) "pb-" . showTW) Transition_All
]
--- | Type representing '_padding' field of 'BoxConfig'.
+-- | Type representing '_padding' field of 'BoxConfig' (transitionable).
-- | based on https://tailwindcss.com/docs/padding
data BoxPadding = BoxPadding
- { _paddingL :: WhenTW TWSize
+ { _paddingL :: WhenTW (WithTransition TWSize)
-- ^ see shorthand: pl
- , _paddingR :: WhenTW TWSize
+ , _paddingR :: WhenTW (WithTransition TWSize)
-- ^ see shorthand: pr
- , _paddingT :: WhenTW TWSize
+ , _paddingT :: WhenTW (WithTransition TWSize)
-- ^ see shorthand: pt
- , _paddingB :: WhenTW TWSize
+ , _paddingB :: WhenTW (WithTransition TWSize)
-- ^ see shorthand: pb
} deriving Show
@@ -107,7 +110,7 @@ instance Semigroup BoxPadding where
-- | This is technically an illegal lens however if you ran 2 setters which overlap so that a /= b
-- | where a and b are the fields associated with respective separate fields, then classh' will
-- | most likely catch the error. Additionally, there is a lens way to access any field anyways
-instance SetSides BoxPadding TWSize where
+instance SetSides BoxPadding (WithTransition TWSize) where
l = paddingL
r = paddingR
b = paddingB
diff --git a/src/Classh/Box/Ring.hs b/src/Classh/Box/Ring.hs
index 427a5b4..d32e90a 100644
--- a/src/Classh/Box/Ring.hs
+++ b/src/Classh/Box/Ring.hs
@@ -1,4 +1,5 @@
{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE FlexibleInstances #-}
--------------------------------------------------------------------------------
-- |
@@ -32,6 +33,8 @@ import Classh.Internal.TShow
import Classh.Internal.Chain
import Classh.Responsive.WhenTW
import Classh.Color
+import Classh.WithTransition
+import Classh.Box.Transition (TransitionProperty(..))
import Control.Lens (makeLenses)
import Data.Default
import qualified Data.Text as T
@@ -48,11 +51,11 @@ data RingWidth
| Ring_Inset
deriving Show
--- | Ring configuration
+-- | Ring configuration (transitionable)
data RingConfig = RingConfig
- { _ringWidth :: WhenTW RingWidth
- , _ringColor :: WhenTW Color
- , _ringOpacity :: WhenTW Int -- 0-100
+ { _ringWidth :: WhenTW (WithTransition RingWidth)
+ , _ringColor :: WhenTW (WithTransition ColorWithOpacity)
+ , _ringOpacity :: WhenTW (WithTransition Int) -- 0-100
} deriving Show
makeLenses ''RingConfig
@@ -72,9 +75,9 @@ instance Default RingConfig where
instance ShowTW RingConfig where
showTW cfg = foldr (<&>) mempty
- [ renderWhenTW (_ringWidth cfg) showTW
- , renderWhenTW (_ringColor cfg) ((<>) "ring-" . showTW)
- , renderWhenTW (_ringOpacity cfg) ((<>) "ring-opacity-" . tshow)
+ [ renderWithTransitionTW (_ringWidth cfg) showTW Transition_All
+ , renderWithTransitionTW (_ringColor cfg) ((<>) "ring-" . showTW) Transition_Colors
+ , renderWithTransitionTW (_ringOpacity cfg) ((<>) "ring-opacity-" . tshow) Transition_Opacity
]
instance Semigroup RingConfig where
diff --git a/src/Classh/Box/Sizing/BoxSizing.hs b/src/Classh/Box/Sizing/BoxSizing.hs
index 60c2a6a..4101ae4 100644
--- a/src/Classh/Box/Sizing/BoxSizing.hs
+++ b/src/Classh/Box/Sizing/BoxSizing.hs
@@ -38,13 +38,15 @@ import Classh.Internal.Chain
import Classh.Class.ShowTW
import Classh.Responsive.WhenTW
import Classh.Box.TWSize
+import Classh.WithTransition
+import Classh.Box.Transition (TransitionProperty(..))
import Data.Default
import Control.Lens (makeLenses)
--- | Holds information on target sizing, which will be overrided by constraints
+-- | Holds information on target sizing (transitionable), which will be overrided by constraints
data BoxSizing = BoxSizing
- { _width :: WhenTW TWSizeOrFraction
- , _height :: WhenTW TWSizeOrFraction
+ { _width :: WhenTW (WithTransition TWSizeOrFraction)
+ , _height :: WhenTW (WithTransition TWSizeOrFraction)
}
deriving Show
@@ -54,8 +56,8 @@ instance Default BoxSizing where
instance ShowTW BoxSizing where
showTW cfg = foldr (<&>) mempty
- [ renderWhenTW (_width cfg) ((<>) "w-" . showTW)
- , renderWhenTW (_height cfg) ((<>) "h-" . showTW)
+ [ renderWithTransitionTW (_width cfg) ((<>) "w-" . showTW) Transition_All
+ , renderWithTransitionTW (_height cfg) ((<>) "h-" . showTW) Transition_All
]
makeLenses ''BoxSizing
diff --git a/src/Classh/Box/Sizing/BoxSizingConstraint.hs b/src/Classh/Box/Sizing/BoxSizingConstraint.hs
index c343fc4..48177ac 100644
--- a/src/Classh/Box/Sizing/BoxSizingConstraint.hs
+++ b/src/Classh/Box/Sizing/BoxSizingConstraint.hs
@@ -34,6 +34,7 @@ module Classh.Box.Sizing.BoxSizingConstraint
import Classh.Box.Sizing.DimensionConstraint as X
import Classh.Responsive.WhenTW
+import Classh.WithTransition
import Control.Lens (makeLenses)
import Data.Default
@@ -42,8 +43,8 @@ instance Default BoxSizingConstraint where
def = BoxSizingConstraint def def
data BoxSizingConstraint = BoxSizingConstraint
- { _widthC :: WhenTW DimensionConstraint
- , _heightC :: WhenTW DimensionConstraint
+ { _widthC :: WhenTW (WithTransition DimensionConstraint)
+ , _heightC :: WhenTW (WithTransition DimensionConstraint)
}
deriving Show
diff --git a/src/Classh/Box/SizingBand.hs b/src/Classh/Box/SizingBand.hs
index bdfc213..f808e13 100644
--- a/src/Classh/Box/SizingBand.hs
+++ b/src/Classh/Box/SizingBand.hs
@@ -49,6 +49,8 @@ import Classh.Class.ShowTW
import Classh.Responsive.WhenTW
import Classh.Internal.Chain
import Classh.Responsive.ZipScreens
+import Classh.WithTransition
+import Classh.Box.Transition (TransitionProperty(..))
import Classh.Box.TWSize as X
@@ -56,15 +58,15 @@ import Control.Lens (makeLenses)
import Data.Default
-- move to shorthand?
-fitToContents :: (WhenTW TWSizeOrFraction, WhenTW TWSizeOrFraction)
-fitToContents = (only TWSize_Fit, only TWSize_Fit)
+fitToContents :: (WhenTW (WithTransition TWSizeOrFraction), WhenTW (WithTransition TWSizeOrFraction))
+fitToContents = (only (WithTransition TWSize_Fit Nothing), only (WithTransition TWSize_Fit Nothing))
instance ShowTW BoxSizingBand where
showTW cfg = foldr (<&>) mempty
- [ renderWhenTW (_widthC . _maxSize $ cfg) ((<>) "max-w-" . showTW)
- , renderWhenTW (_heightC . _maxSize $ cfg) ((<>) "max-h-" . showTW)
- , renderWhenTW (_widthC . _minSize $ cfg) ((<>) "min-w-" . showTW)
- , renderWhenTW (_heightC . _minSize $ cfg) ((<>) "min-h-" . showTW)
+ [ renderWithTransitionTW (_widthC . _maxSize $ cfg) ((<>) "max-w-" . showTW) Transition_All
+ , renderWithTransitionTW (_heightC . _maxSize $ cfg) ((<>) "max-h-" . showTW) Transition_All
+ , renderWithTransitionTW (_widthC . _minSize $ cfg) ((<>) "min-w-" . showTW) Transition_All
+ , renderWithTransitionTW (_heightC . _minSize $ cfg) ((<>) "min-h-" . showTW) Transition_All
, showTW $ _size cfg
]
diff --git a/src/Classh/Box/Transform.hs b/src/Classh/Box/Transform.hs
index 1e38019..ddc060c 100644
--- a/src/Classh/Box/Transform.hs
+++ b/src/Classh/Box/Transform.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE TemplateHaskell #-}
--------------------------------------------------------------------------------
-- |
-- Module : Classh.Box.Transform
@@ -8,20 +9,49 @@
-- Stability : provisional
-- Portability : portable
--
--- Types to represent tailwind scale transforms
+-- Types to represent tailwind transforms
-- see https://v3.tailwindcss.com/docs/scale
+-- see https://v3.tailwindcss.com/docs/rotate
+-- see https://v3.tailwindcss.com/docs/translate
+-- see https://v3.tailwindcss.com/docs/skew
+-- see https://v3.tailwindcss.com/docs/transform-origin
--
-- Example use:
--
-- @
--- $(classh' [ scale .~ [("def", Scale_100), ("hover", Scale_105), ("active", Scale_95)] ])
+-- $(classh' [ transform . scale .~^ [("def", Scale_100), ("hover", Scale_105), ("active", Scale_95)] ])
+-- $(classh' [ transform . rotate .~^ [("def", Rotate_0), ("hover", Rotate_180)] ])
+-- $(classh' [ transform . translateX .~~ Translate_0 ])
-- @
--------------------------------------------------------------------------------
-module Classh.Box.Transform where
+module Classh.Box.Transform
+ ( TransformConfig(..)
+ , Scale(..)
+ , Rotate(..)
+ , Translate(..)
+ , Skew(..)
+ , TransformOrigin(..)
+ , rotate
+ , scale
+ , translateX
+ , translateY
+ , skewX
+ , skewY
+ , transformOrigin
+ ) where
import Classh.Class.ShowTW
+import Classh.Class.CompileStyle
import Classh.Internal.TShow
+import Classh.Internal.Chain
+import Classh.Responsive.WhenTW
+import Classh.WithTransition
+import Classh.Box.Transition (TransitionProperty(..))
+import Classh.Box.TWSize (TWSize, DivInt(..))
+import Classh.Internal.CSSSize (CSSSize)
+import Classh.Class.IsCSS (renderCSS)
+import Control.Lens hiding ((<&>))
import Data.Default
import qualified Data.Text as T
@@ -48,3 +78,153 @@ instance ShowTW Scale where
showTW = \case
Scale_Custom val -> "scale-[" <> val <> "%]"
other -> "scale-" <> (T.drop 6 . tshow $ other)
+
+-- | Rotate transform
+-- see https://v3.tailwindcss.com/docs/rotate
+data Rotate
+ = Rotate_0 -- ^ rotate-0: transform: rotate(0deg)
+ | Rotate_1 -- ^ rotate-1: transform: rotate(1deg)
+ | Rotate_2 -- ^ rotate-2: transform: rotate(2deg)
+ | Rotate_3 -- ^ rotate-3: transform: rotate(3deg)
+ | Rotate_6 -- ^ rotate-6: transform: rotate(6deg)
+ | Rotate_12 -- ^ rotate-12: transform: rotate(12deg)
+ | Rotate_45 -- ^ rotate-45: transform: rotate(45deg)
+ | Rotate_90 -- ^ rotate-90: transform: rotate(90deg)
+ | Rotate_180 -- ^ rotate-180: transform: rotate(180deg)
+ | Rotate_Custom T.Text -- ^ e.g., Rotate_Custom "17deg" for rotate-[17deg]
+ deriving Show
+
+instance Default Rotate where
+ def = Rotate_0
+
+instance ShowTW Rotate where
+ showTW = \case
+ Rotate_Custom val -> "rotate-[" <> val <> "]"
+ other -> "rotate-" <> (T.drop 7 . tshow $ other)
+
+-- | Translate transform (for X and Y axes)
+-- see https://v3.tailwindcss.com/docs/translate
+data Translate
+ = Translate_0
+ | Translate_Px
+ | Translate_Full
+ | Translate_TWSize TWSize -- ^ Numeric spacing values (1, 2, 3.5, 4, etc.)
+ | Translate_Fraction Int DivInt -- ^ Fractional values (1/2, 1/3, 2/3, 1/4, etc.)
+ | Translate_Custom CSSSize -- ^ Custom CSS size (e.g., Rem 1.5, Percent 50)
+ deriving Show
+
+instance Default Translate where
+ def = Translate_0
+
+instance ShowTW Translate where
+ showTW = \case
+ Translate_0 -> "0"
+ Translate_Px -> "px"
+ Translate_Full -> "full"
+ Translate_TWSize sz -> showTW sz
+ Translate_Fraction num d -> tshow num <> "/" <> showTW d
+ Translate_Custom css -> "[" <> renderCSS css <> "]"
+
+-- | Skew transform (for X and Y axes)
+-- see https://v3.tailwindcss.com/docs/skew
+data Skew
+ = Skew_0 -- ^ skew-{x|y}-0: transform: skew{X|Y}(0deg)
+ | Skew_1 -- ^ skew-{x|y}-1: transform: skew{X|Y}(1deg)
+ | Skew_2 -- ^ skew-{x|y}-2: transform: skew{X|Y}(2deg)
+ | Skew_3 -- ^ skew-{x|y}-3: transform: skew{X|Y}(3deg)
+ | Skew_6 -- ^ skew-{x|y}-6: transform: skew{X|Y}(6deg)
+ | Skew_12 -- ^ skew-{x|y}-12: transform: skew{X|Y}(12deg)
+ | Skew_Custom T.Text -- ^ e.g., Skew_Custom "17deg" for skew-x-[17deg]
+ deriving Show
+
+instance Default Skew where
+ def = Skew_0
+
+instance ShowTW Skew where
+ showTW = \case
+ Skew_Custom val -> "[" <> val <> "]"
+ other -> T.drop 5 . tshow $ other
+
+-- | Transform origin
+-- see https://v3.tailwindcss.com/docs/transform-origin
+data TransformOrigin
+ = Origin_Center -- ^ origin-center
+ | Origin_Top -- ^ origin-top
+ | Origin_TopRight -- ^ origin-top-right
+ | Origin_Right -- ^ origin-right
+ | Origin_BottomRight -- ^ origin-bottom-right
+ | Origin_Bottom -- ^ origin-bottom
+ | Origin_BottomLeft -- ^ origin-bottom-left
+ | Origin_Left -- ^ origin-left
+ | Origin_TopLeft -- ^ origin-top-left
+ | Origin_Custom T.Text -- ^ e.g., Origin_Custom "33% 75%" for origin-[33%_75%]
+ deriving Show
+
+instance Default TransformOrigin where
+ def = Origin_Center
+
+instance ShowTW TransformOrigin where
+ showTW = \case
+ Origin_Center -> "origin-center"
+ Origin_Top -> "origin-top"
+ Origin_TopRight -> "origin-top-right"
+ Origin_Right -> "origin-right"
+ Origin_BottomRight -> "origin-bottom-right"
+ Origin_Bottom -> "origin-bottom"
+ Origin_BottomLeft -> "origin-bottom-left"
+ Origin_Left -> "origin-left"
+ Origin_TopLeft -> "origin-top-left"
+ Origin_Custom val -> "origin-[" <> val <> "]"
+
+-- | Configuration for all transform properties
+-- see https://v3.tailwindcss.com/docs/transform
+-- All transform properties can be smoothly transitioned using transition-transform
+data TransformConfig = TransformConfig
+ { _rotate :: WhenTW (WithTransition Rotate)
+ , _scale :: WhenTW (WithTransition Scale)
+ , _translateX :: WhenTW (WithTransition Translate)
+ , _translateY :: WhenTW (WithTransition Translate)
+ , _skewX :: WhenTW (WithTransition Skew)
+ , _skewY :: WhenTW (WithTransition Skew)
+ , _transformOrigin :: WhenTW TransformOrigin -- Origin doesn't transition, just changes instantly
+ }
+ deriving Show
+
+makeLenses ''TransformConfig
+
+instance Default TransformConfig where
+ def = TransformConfig def def def def def def def
+
+instance Semigroup TransformConfig where
+ (<>) a b = TransformConfig
+ { _rotate = _rotate a <> _rotate b
+ , _scale = _scale a <> _scale b
+ , _translateX = _translateX a <> _translateX b
+ , _translateY = _translateY a <> _translateY b
+ , _skewX = _skewX a <> _skewX b
+ , _skewY = _skewY a <> _skewY b
+ , _transformOrigin = _transformOrigin a <> _transformOrigin b
+ }
+
+instance CompileStyle TransformConfig where
+ compileS cfg = do
+ pure . foldr (<&>) mempty =<< sequenceA
+ [ compileWithTransitionTW (_rotate cfg) showTW Transition_Transform
+ , compileWithTransitionTW (_scale cfg) showTW Transition_Transform
+ , compileWithTransitionTW (_translateX cfg) ((<>) "translate-x-" . showTW) Transition_Transform
+ , compileWithTransitionTW (_translateY cfg) ((<>) "translate-y-" . showTW) Transition_Transform
+ , compileWithTransitionTW (_skewX cfg) ((<>) "skew-x-" . showTW) Transition_Transform
+ , compileWithTransitionTW (_skewY cfg) ((<>) "skew-y-" . showTW) Transition_Transform
+ , compileWhenTW (_transformOrigin cfg) showTW
+ ]
+
+instance ShowTW TransformConfig where
+ showTW cfg = foldr (<&>) mempty
+ [ renderWithTransitionTW (_rotate cfg) showTW Transition_Transform
+ , renderWithTransitionTW (_scale cfg) showTW Transition_Transform
+ , renderWithTransitionTW (_translateX cfg) ((<>) "translate-x-" . showTW) Transition_Transform
+ , renderWithTransitionTW (_translateY cfg) ((<>) "translate-y-" . showTW) Transition_Transform
+ , renderWithTransitionTW (_skewX cfg) ((<>) "skew-x-" . showTW) Transition_Transform
+ , renderWithTransitionTW (_skewY cfg) ((<>) "skew-y-" . showTW) Transition_Transform
+ , renderWhenTW (_transformOrigin cfg) showTW
+ ]
diff --git a/src/Classh/Box/Transition.hs b/src/Classh/Box/Transition.hs
index 6da4d8b..cab5b1c 100644
--- a/src/Classh/Box/Transition.hs
+++ b/src/Classh/Box/Transition.hs
@@ -58,7 +58,7 @@ data TransitionDuration
| Duration_700
| Duration_1000
| Duration_Custom T.Text -- ^ e.g., Duration_Custom "2000" for duration-[2000ms]
- deriving Show
+ deriving (Show, Eq)
-- | Transition timing function (easing)
-- see https://v3.tailwindcss.com/docs/transition-timing-function
@@ -68,7 +68,7 @@ data TransitionTimingFunction
| Ease_Out
| Ease_InOut
| Ease_Custom T.Text -- ^ e.g., Ease_Custom "cubic-bezier(0.4,0,0.2,1)"
- deriving Show
+ deriving (Show, Eq)
-- | Transition delay
-- see https://v3.tailwindcss.com/docs/transition-delay
@@ -83,19 +83,31 @@ data TransitionDelay
| Delay_700
| Delay_1000
| Delay_Custom T.Text -- ^ e.g., Delay_Custom "2000" for delay-[2000ms]
- deriving Show
+ deriving (Show, Eq)
--- | Transition configuration
+-- | Transition configuration (simplified - no WhenTW!)
+-- When used with WithTransition, the transition property is inferred from context
+-- This prevents WhenTW nesting which cannot be rendered to valid CSS
data TransitionConfig = TransitionConfig
- { _transitionProperty :: WhenTW TransitionProperty
- , _transitionDuration :: WhenTW TransitionDuration
- , _transitionTiming :: WhenTW TransitionTimingFunction
- , _transitionDelay :: WhenTW TransitionDelay
- } deriving Show
+ { _transitionDuration :: TransitionDuration
+ , _transitionTiming :: TransitionTimingFunction
+ , _transitionDelay :: TransitionDelay
+ } deriving (Show, Eq)
-- Template Haskell splice - must come after data type definitions
makeLenses ''TransitionConfig
+-- | Legacy global transition config with WhenTW for backwards compatibility
+-- Used for the global _transition field in BoxConfig
+data TransitionConfigGlobal = TransitionConfigGlobal
+ { _transitionProperty :: WhenTW TransitionProperty
+ , _transitionDurationGlobal :: WhenTW TransitionDuration
+ , _transitionTimingGlobal :: WhenTW TransitionTimingFunction
+ , _transitionDelayGlobal :: WhenTW TransitionDelay
+ } deriving Show
+
+makeLenses ''TransitionConfigGlobal
+
-- Instances for TransitionProperty
instance Default TransitionProperty where
def = Transition_None
@@ -104,7 +116,7 @@ instance ShowTW TransitionProperty where
showTW = \case
Transition -> "transition"
Transition_Custom val -> "transition-[" <> val <> "]"
- other -> T.toLower (tshow other)
+ other -> T.replace "_" "-" $ T.toLower (tshow other)
-- Instances for TransitionDuration
instance Default TransitionDuration where
@@ -123,7 +135,7 @@ instance Default TransitionTimingFunction where
instance ShowTW TransitionTimingFunction where
showTW = \case
Ease_Custom val -> "ease-[" <> val <> "]"
- other -> T.toLower (tshow other)
+ other -> T.replace "_" "-" $ T.toLower (tshow other)
-- Instances for TransitionDelay
instance Default TransitionDelay where
@@ -135,22 +147,29 @@ instance ShowTW TransitionDelay where
Delay_Custom val -> "delay-[" <> val <> "ms]"
other -> "delay-" <> (T.drop 6 . tshow $ other)
--- Instances for TransitionConfig
+-- Instances for TransitionConfig (simplified, no WhenTW)
instance Default TransitionConfig where
- def = TransitionConfig def def def def
+ def = TransitionConfig def def def
+
+-- Note: TransitionConfig doesn't have ShowTW instance anymore
+-- It will be rendered contextually when used with WithTransition
+
+-- Instances for TransitionConfigGlobal (legacy)
+instance Default TransitionConfigGlobal where
+ def = TransitionConfigGlobal def def def def
-instance ShowTW TransitionConfig where
+instance ShowTW TransitionConfigGlobal where
showTW cfg = foldr (<&>) mempty
[ renderWhenTW (_transitionProperty cfg) showTW
- , renderWhenTW (_transitionDuration cfg) showTW
- , renderWhenTW (_transitionTiming cfg) showTW
- , renderWhenTW (_transitionDelay cfg) showTW
+ , renderWhenTW (_transitionDurationGlobal cfg) showTW
+ , renderWhenTW (_transitionTimingGlobal cfg) showTW
+ , renderWhenTW (_transitionDelayGlobal cfg) showTW
]
-instance Semigroup TransitionConfig where
- (<>) a b = TransitionConfig
+instance Semigroup TransitionConfigGlobal where
+ (<>) a b = TransitionConfigGlobal
{ _transitionProperty = _transitionProperty a <> _transitionProperty b
- , _transitionDuration = _transitionDuration a <> _transitionDuration b
- , _transitionTiming = _transitionTiming a <> _transitionTiming b
- , _transitionDelay = _transitionDelay a <> _transitionDelay b
+ , _transitionDurationGlobal = _transitionDurationGlobal a <> _transitionDurationGlobal b
+ , _transitionTimingGlobal = _transitionTimingGlobal a <> _transitionTimingGlobal b
+ , _transitionDelayGlobal = _transitionDelayGlobal a <> _transitionDelayGlobal b
}
diff --git a/src/Classh/Class/SetSides.hs b/src/Classh/Class/SetSides.hs
index 9986639..bd8982f 100644
--- a/src/Classh/Class/SetSides.hs
+++ b/src/Classh/Class/SetSides.hs
@@ -1,4 +1,5 @@
{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE FunctionalDependencies #-}
module Classh.Class.SetSides where
@@ -7,7 +8,7 @@ import Control.Lens
-- | This class allows for shorthand for a config that is based on sides, such
-- | as padding or margin or border
-class SetSides tw a where
+class SetSides tw a | tw -> a where
x :: Lens' tw (WhenTW a)
y :: Lens' tw (WhenTW a)
xy :: Lens' tw (WhenTW a)
diff --git a/src/Classh/Color.hs b/src/Classh/Color.hs
index 8518180..6e69e07 100644
--- a/src/Classh/Color.hs
+++ b/src/Classh/Color.hs
@@ -17,7 +17,18 @@
-- @
--------------------------------------------------------------------------------
-module Classh.Color where
+module Classh.Color
+ ( -- * Color Types
+ Color(..)
+ , ColorNum(..)
+ , Hex(..)
+ -- * Color with Opacity
+ , ColorWithOpacity(..)
+ , color
+ , withOpacity
+ -- * Hex Helper
+ , hex
+ ) where
import Classh.Class.ShowTW
import Classh.Internal.TShow
@@ -72,6 +83,41 @@ data Color
| Color_Custom Hex
deriving (Show, Eq)
+-- | Color with optional opacity (0-100).
+--
+-- Renders using Tailwind's @/opacity@ syntax when opacity is present.
+-- When opacity is @Nothing@, renders as plain color (fully opaque).
+--
+-- === Example
+--
+-- @
+-- color (Blue C500) -- blue-500 (no opacity suffix)
+-- withOpacity (Blue C500) 50 -- blue-500/50
+-- withOpacity (hex "1e40af") 87 -- [#1e40af]/87
+-- @
+data ColorWithOpacity = ColorWithOpacity
+ { _cwo_color :: Color
+ , _cwo_opacity :: Maybe Int -- ^ Nothing = fully opaque, Just n = n% opacity
+ } deriving (Show, Eq)
+
+-- | Create a color without explicit opacity (fully opaque).
+--
+-- @
+-- color (Blue C500) -- blue-500
+-- color White -- white
+-- @
+color :: Color -> ColorWithOpacity
+color c = ColorWithOpacity c Nothing
+
+-- | Create a color with explicit opacity (0-100).
+--
+-- @
+-- withOpacity (Blue C500) 50 -- blue-500/50
+-- withOpacity (hex "1e40af") 87 -- [#1e40af]/87
+-- @
+withOpacity :: Color -> Int -> ColorWithOpacity
+withOpacity c o = ColorWithOpacity c (Just o)
+
-- | Eg. see https://tailwindcss.com/docs/background-color
data ColorNum
= C50
@@ -97,6 +143,12 @@ instance ShowTW Color where
showTW Transparent = "transparent"
showTW Black = "black"
showTW White = "white"
- showTW color = case T.words $ tshow color of
- c:(mag):[] -> (T.toLower c) <> "-" <> (T.drop 1 mag) -- T.words $ tshow color
- _ -> "ClasshSS: failed on input" <> (tshow color)
+ showTW col = case T.words $ tshow col of
+ c:(mag):[] -> (T.toLower c) <> "-" <> (T.drop 1 mag)
+ _ -> "ClasshSS: failed on input" <> (tshow col)
+
+-- | Renders color with optional opacity suffix.
+-- @Nothing@ opacity renders plain color, @Just n@ renders @color/n@.
+instance ShowTW ColorWithOpacity where
+ showTW (ColorWithOpacity c Nothing) = showTW c
+ showTW (ColorWithOpacity c (Just o)) = showTW c <> "/" <> tshow o
diff --git a/src/Classh/Responsive/WhenTW.hs b/src/Classh/Responsive/WhenTW.hs
index 3e83473..98491d7 100644
--- a/src/Classh/Responsive/WhenTW.hs
+++ b/src/Classh/Responsive/WhenTW.hs
@@ -67,3 +67,9 @@ mkConditionPrefix c = if c == "def" then "" else (c <> ":")
-- eg. width2 = width1 + 1px
instance Functor WhenTW' where
fmap f whenTW = WhenTW' $ fmap (\(c,a) -> (c, f a)) $ unWhenTW whenTW
+
+-- | Render WhenTW values that are wrapped in WithTransition
+-- This extracts the value, renders it, and adds transition classes if present
+-- Note: This requires importing Classh.WithTransition and Classh.Box.Transition
+-- but we can't do that here to avoid circular dependencies
+-- So we'll define this in a separate module or inline where needed
diff --git a/src/Classh/Setters.hs b/src/Classh/Setters.hs
index 07c24cc..762488b 100644
--- a/src/Classh/Setters.hs
+++ b/src/Classh/Setters.hs
@@ -1,6 +1,12 @@
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE FunctionalDependencies #-}
+
--------------------------------------------------------------------------------
-- |
--- Module : Classh.Box.Border
+-- Module : Classh.Setters
-- Copyright : (c) 2024, Galen Sprout
-- License : BSD-style (see end of this file)
--
@@ -8,7 +14,7 @@
-- Stability : provisional
-- Portability : portable
--
--- Types to represent tailwind box's border config of 'BoxConfig'
+-- Setter operators for ClasshSS configs
--
-- Any field named _someField has an associated lens `someField`
-- see @defaultNameTransform@ from Lens.Family.THCore
@@ -31,48 +37,89 @@ module Classh.Setters where
import Classh.Responsive.WhenTW
import Classh.Responsive.ZipScreens
+import Classh.WithTransition
import Control.Lens hiding (only)
--- | Append a list to existing WhenTW field of a config
+-- | Append a list to existing WhenTW field of a config (for non-transitionable fields)
infixr 4 .~+
-(.~+) :: ASetter s t [a] [a] -> [a] -> s -> t
+(.~+) :: ASetter s t (WhenTW a) (WhenTW a) -> WhenTW a -> s -> t
someLens .~+ newVals = over someLens (++ newVals)
--- | Append a list to existing WhenTW field of a config
+-- | Append a list to existing WhenTW field of a config (for non-transitionable fields)
infixr 4 .+
-(.+) :: ASetter s t [a] [a] -> [a] -> s -> t
+(.+) :: ASetter s t (WhenTW a) (WhenTW a) -> WhenTW a -> s -> t
(.+) = (.~+)
--- | Extend existing WhenTW field of a config with new value at end of input list
+-- | Extend existing WhenTW field with single value (for non-transitionable fields)
infixr 4 .++
(.++) :: ASetter s t (WhenTW a) (WhenTW a) -> a -> s -> t
someLens .++ newVals = over someLens (++ (only newVals))
+type family UnwrapType a where
+ UnwrapType (WithTransition a) = a
+ UnwrapType a = a
+
-- | Set property to a singular constant value
+-- For WithTransition fields, takes unwrapped value and wraps with noTransition
+-- For plain fields, takes value directly
+-- User always provides unwrapped values to (.~~)
infixr 4 .~~
-(.~~) :: ASetter s t b (WhenTW a) -> a -> s -> t
-someLens .~~ newVals = over someLens (const $ only newVals)
+class SetConstant field where
+ (.~~) :: ASetter s t c (WhenTW field) -> UnwrapType field -> s -> t
+
+instance {-# OVERLAPPING #-} SetConstant (WithTransition a) where
+ someLens .~~ newVals = over someLens (const $ only $ noTransition newVals)
+
+instance {-# OVERLAPPABLE #-} (UnwrapType a ~ a) => SetConstant a where
+ someLens .~~ newVals = over someLens (const $ only newVals)
-- | Zip input list with screen sizes to create a responsive property and override
+-- Works for both transitionable and non-transitionable fields
infixr 4 .|~
-(.|~) :: ASetter s t b (WhenTW a) -> [a] -> s -> t
-someLens .|~ newVals = over someLens (const $ zipScreens newVals)
+class SetResponsive a where
+ (.|~) :: ASetter s t c (WhenTW a) -> [UnwrapType a] -> s -> t
+
+instance SetResponsive (WithTransition a) where
+ someLens .|~ newVals = over someLens (const $ zipScreens $ fmap noTransition newVals)
+
+instance {-# OVERLAPPABLE #-} (UnwrapType a ~ a) => SetResponsive a where
+ someLens .|~ newVals = over someLens (const $ zipScreens newVals)
--- | Zip input list with screen sizes to create a responsive property and add to input property
+-- | Zip input list with screen sizes and add to existing property
+-- Works for both transitionable and non-transitionable fields
infixr 4 .|+
-(.|+) :: ASetter s t (WhenTW a) (WhenTW a) -> [a] -> s -> t
-someLens .|+ newVals = over someLens (++ (zipScreens newVals))
+class AddResponsive a where
+ (.|+) :: ASetter s t (WhenTW a) (WhenTW a) -> [UnwrapType a] -> s -> t
+instance AddResponsive (WithTransition a) where
+ someLens .|+ newVals = over someLens (++ (zipScreens $ fmap noTransition newVals))
+instance {-# OVERLAPPABLE #-} (UnwrapType a ~ a) => AddResponsive a where
+ someLens .|+ newVals = over someLens (++ (zipScreens newVals))
--- | Both are functions from Classh with changed infix precedence to work with <>
-infixr 7 .-
-(.-) :: ASetter s t b (WhenTW a) -> a -> s -> t
-someLens .- newVals = over someLens (const $ only newVals)
+
+
+-- | Both are functions from Classh with changed infix precedence to work with <>
+infixr 7 .-
+(.-) :: SetConstant field => ASetter s t c (WhenTW field) -> UnwrapType field -> s -> t
+(.-) = (.~~)
infixr 7 .|<~
-(.|<~) :: ASetter s t b (WhenTW a) -> [a] -> s -> t
-someLens .|<~ newVals = over someLens (const $ zipScreens newVals)
+(.|<~) :: SetResponsive a => ASetter s t c (WhenTW a) -> [UnwrapType a] -> s -> t
+(.|<~) = (.|~)
+
+-- | Set property with explicit transition support
+-- This operator allows you to specify transitions per-condition
+--
+-- Example:
+-- @
+-- bgColor .~^ [ ("def", purple)
+-- , ("hover", lavender `withTransition` Duration_300)
+-- ]
+-- @
+infixr 4 .~^
+(.~^) :: ASetter s t c (WhenTW (WithTransition a)) -> [(TWCondition, WithTransition a)] -> s -> t
+someLens .~^ newVals = over someLens (const newVals)
-- .:|
diff --git a/src/Classh/Shorthand.hs b/src/Classh/Shorthand.hs
index 5288663..0a5dd69 100644
--- a/src/Classh/Shorthand.hs
+++ b/src/Classh/Shorthand.hs
@@ -40,27 +40,27 @@ import Control.Lens (Lens')
type Setter a b = Lens' a b
-- | Set border radius side(s)
-br_r, br_l, br_t, br_b, br_y, br_x, br :: Setter BoxConfig (WhenTW BorderRadius')
-br_r = border . radius . r
+br_r, br_l, br_t, br_b, br_y, br_x, br :: Setter BoxConfig (WhenTW (WithTransition BorderRadius'))
+br_r = border . radius . r
br_l = border . radius . l
br_t = border . radius . t
br_b = border . radius . b
br_y = border . radius . y
br_x = border . radius . x
-br = border . radius . allS
+br = border . radius . allS
-- | Set border width side(s)
-bw_r, bw_l, bw_t, bw_b, bw_y, bw_x, bw :: Setter BoxConfig (WhenTW BorderWidth)
-bw_r = border . bWidth . r
+bw_r, bw_l, bw_t, bw_b, bw_y, bw_x, bw :: Setter BoxConfig (WhenTW (WithTransition BorderWidth))
+bw_r = border . bWidth . r
bw_l = border . bWidth . l
bw_t = border . bWidth . t
bw_b = border . bWidth . b
bw_y = border . bWidth . y
bw_x = border . bWidth . x
-bw = border . bWidth . allS
+bw = border . bWidth . allS
-- | Set border color side(s)
-bc_r, bc_l, bc_t, bc_b, bc_y, bc_x, bc :: Setter BoxConfig (WhenTW Color)
+bc_r, bc_l, bc_t, bc_b, bc_y, bc_x, bc :: Setter BoxConfig (WhenTW (WithTransition ColorWithOpacity))
bc_r = border . bColor . r
bc_l = border . bColor . l
bc_t = border . bColor . t
@@ -74,33 +74,33 @@ pos :: Setter BoxConfig (WhenTW (Justify, Align))
pos = position
-- | Set width
-width' :: Setter BoxConfig (WhenTW TWSizeOrFraction)
+width' :: Setter BoxConfig (WhenTW (WithTransition TWSizeOrFraction))
width' = sizingBand . size . width
-- | Set width
-w :: Setter BoxConfig (WhenTW TWSizeOrFraction)
+w :: Setter BoxConfig (WhenTW (WithTransition TWSizeOrFraction))
w = width'
-- | Set height
-height' :: Setter BoxConfig (WhenTW TWSizeOrFraction)
+height' :: Setter BoxConfig (WhenTW (WithTransition TWSizeOrFraction))
height' = sizingBand . size . height
-- | Set height
-h :: Setter BoxConfig (WhenTW TWSizeOrFraction)
+h :: Setter BoxConfig (WhenTW (WithTransition TWSizeOrFraction))
h = height'
--- | Set BoxConfig max width
-maxW :: Setter BoxConfig (WhenTW DimensionConstraint)
+-- | Set BoxConfig max width
+maxW :: Setter BoxConfig (WhenTW (WithTransition DimensionConstraint))
maxW = sizingBand . maxSize . widthC
-- | Set BoxConfig min width
-minW :: Setter BoxConfig (WhenTW DimensionConstraint)
+minW :: Setter BoxConfig (WhenTW (WithTransition DimensionConstraint))
minW = sizingBand . minSize . widthC
-- | Set BoxConfig max height
-maxH :: Setter BoxConfig (WhenTW DimensionConstraint)
+maxH :: Setter BoxConfig (WhenTW (WithTransition DimensionConstraint))
maxH = sizingBand . maxSize . heightC
-- | Set BoxConfig min height
-minH :: Setter BoxConfig (WhenTW DimensionConstraint)
-minH = sizingBand . minSize . heightC
+minH :: Setter BoxConfig (WhenTW (WithTransition DimensionConstraint))
+minH = sizingBand . minSize . heightC
-- | Set margin on a given side(s)
-mt, ml, mr, mb, mx, my, m :: Setter BoxConfig (WhenTW TWSize)
+mt, ml, mr, mb, mx, my, m :: Setter BoxConfig (WhenTW (WithTransition TWSize))
mt = margin . t
mb = margin . b
ml = margin . l
@@ -110,7 +110,7 @@ my = margin . y
m = margin . allS
-- | Set padding on a given side(s)
-pt, pl, pr, pb, px, py, p :: Setter BoxConfig (WhenTW TWSize)
+pt, pl, pr, pb, px, py, p :: Setter BoxConfig (WhenTW (WithTransition TWSize))
pt = padding . t
pb = padding . b
pl = padding . l
diff --git a/src/Classh/Text.hs b/src/Classh/Text.hs
index af259aa..1495918 100644
--- a/src/Classh/Text.hs
+++ b/src/Classh/Text.hs
@@ -10,39 +10,250 @@
-- Stability : provisional
-- Portability : portable
--
--- The core interface to creating responsive text
+-- TextConfigTW: The core type for styling text content.
--
--- Here is a common real example creating a box with some text in it, using reflex-dom to illustrate
+-- = Overview
--
--- > elClass "div" "" $ do
--- > textS $(classh' [text_size .|~ [XL,XL2], text_weight .~~ Bold]) "Hey"
+-- This module provides 'TextConfigTW', the configuration type for all text-specific
+-- styling in ClasshSS. It handles typography properties like size, weight, font family,
+-- color, and decoration.
--
--- This module and all modules which it re-exports are your interface to writing typified classes
--- specifically for text
+-- TextConfigTW includes:
--
--- Through CompileStyle, Classh enforces that 'TextConfigTW' is a seperate expression from 'BoxConfig'
--- this is because it undeniably helps to create modularity, reduce phantom CSS behaviour and makes
--- it easy to create themes to be shared by an application. For example
+-- * Typography - Font size, weight, family, style
+-- * Colors - Text color with responsive support
+-- * Decoration - Underline, overline, strikethrough with style and thickness
+-- * Interaction - Cursor styles
--
--- > defText = def { _text_font = Font_Custom "Sarabun" } -- perhaps we want all text to be Sarabun
--- > blueBrandTextSm someText = textS $(classh defText [ text_color .~~ Blue C950, text_size .|~ [XS,SM,Base,Lg]])
--- > elClass "div" "" $ blueBrandTextSm "Sign up now!"
+-- = Quick Example
--
--- Note that we can use '.|~' and 'zipScreens' to easily create responsive text
--- .|~ takes a list that goes from mobile (less than 640px) -> sm -> md -> lg -> xl -> 2xl (eg. text_size)
--- .~~ takes a singular value for all screen sizes (eg. text_weight)
--- .~ is a simple setter that expects the type of the property, so the property text_color is a WhenTW Color
--- and so if we wanted to set a color on hover, we could do:
+-- Creating styled text with Reflex.Dom:
--
--- > $(classh' [ text_color .~ [("hover", hex "FFFFFF"), ("def", Blue C950)] ])
+-- @
+-- textS $(classh'
+-- [ text_size .~~ XL3
+-- , text_weight .~~ Bold
+-- , text_color .~~ Gray C900
+-- , text_font .~~ Font_Custom \"Sarabun\"
+-- ]) \"Hello, ClasshSS!\"
+-- @
--
--- This will set text to white on hover, and normally otherwise "text-blue-950"
+-- = Separation from BoxConfig
--
--- We also have
--- (.~+) appends the chosen value to what exists (perhaps in a default config)
--- (.|+) like .|~ except that it adds to what already exists (perhaps in a default config)
+-- ClasshSS enforces that 'TextConfigTW' and 'Classh.Box.BoxConfig' are used in separate
+-- expressions. This design prevents phantom CSS behavior and improves modularity:
+--
+-- @
+-- -- COMPILE ERROR: Cannot mix BoxConfig and TextConfigTW
+-- $(classh' [ bgColor .~~ Blue C500, text_size .~~ XL ])
+--
+-- -- CORRECT: Separate expressions
+-- elClass \"div\" $(classh' [ bgColor .~~ Blue C500 ]) $ do
+-- textS $(classh' [ text_size .~~ XL ]) \"Text\"
+-- @
+--
+-- = TextConfigTW Fields
+--
+-- == Typography
+--
+-- * '_text_size' - Font size from XS to XL9
+-- * '_text_weight' - Font weight from Thin to Black
+-- * '_text_font' - Font family (Sans, Serif, Mono, or custom)
+-- * '_text_style' - Italic or normal
+--
+-- @
+-- text_size .~~ XL2 -- text-2xl
+-- text_weight .~~ Bold -- font-bold
+-- text_font .~~ Sans -- font-sans
+-- text_style .~~ Italic -- italic
+-- @
+--
+-- == Color
+--
+-- * '_text_color' - Text color with responsive and state support
+--
+-- @
+-- -- Simple color
+-- text_color .~~ Blue C500 -- text-blue-500
+--
+-- -- With hover
+-- text_color .~ [(\"def\", Gray C900), (\"hover\", Blue C600)]
+-- @
+--
+-- == Decoration
+--
+-- * '_text_decoration' - 'TextDecorationTW' for underline, overline, strikethrough
+--
+-- @
+-- text_decoration . textDec_line .~~ Underline
+-- text_decoration . textDec_color .~~ Blue C500
+-- text_decoration . textDec_style .~~ Wavy
+-- text_decoration . textDec_thickness .~~ Thickness_2
+-- @
+--
+-- == Cursor
+--
+-- * '_text_cursor' - Mouse cursor style
+--
+-- @
+-- text_cursor .~~ CursorPointer
+-- @
+--
+-- == Custom Classes
+--
+-- * '_text_custom' - Arbitrary Tailwind classes
+--
+-- @
+-- custom .~ \"text-center uppercase tracking-wide\"
+-- @
+--
+-- = Responsive Text
+--
+-- Use '.|~' for responsive text sizing:
+--
+-- @
+-- text_size .|~ [SM, Base, LG, XL, XL2, XL3]
+-- -- mobile sm md lg xl 2xl
+--
+-- -- Result: \"text-sm sm:text-base md:text-lg lg:text-xl xl:text-2xl 2xl:text-3xl\"
+-- @
+--
+-- = Theming
+--
+-- TextConfigTW makes it easy to create reusable text themes:
+--
+-- @
+-- -- Define a theme
+-- defText :: TextConfigTW
+-- defText = def & text_font .~~ Font_Custom \"Sarabun\"
+--
+-- -- Create styled text helpers
+-- brandHeading :: Text -> Text
+-- brandHeading content =
+-- textS $(classh defText
+-- [ text_size .|~ [XL2, XL3, XL4]
+-- , text_weight .~~ Bold
+-- , text_color .~~ Blue C950
+-- ]) content
+--
+-- -- Use in components
+-- elClass \"div\" \"\" $ brandHeading \"Sign up now!\"
+-- @
+--
+-- = Common Patterns
+--
+-- == Heading
+--
+-- @
+-- $(classh'
+-- [ text_size .~~ XL3
+-- , text_weight .~~ Bold
+-- , text_color .~~ Gray C900
+-- ])
+-- @
+--
+-- == Body Text
+--
+-- @
+-- $(classh'
+-- [ text_size .~~ Base
+-- , text_weight .~~ Normal
+-- , text_color .~~ Gray C700
+-- ])
+-- @
+--
+-- == Link with Hover
+--
+-- @
+-- $(classh'
+-- [ text_color .~ [(\"def\", Blue C600), (\"hover\", Blue C800)]
+-- , text_decoration . textDec_line .~ [(\"hover\", Underline)]
+-- , text_cursor .~~ CursorPointer
+-- ])
+-- @
+--
+-- == Responsive Heading
+--
+-- @
+-- $(classh'
+-- [ text_size .|~ [XL, XL2, XL3, XL4]
+-- , text_weight .~~ Bold
+-- , text_color .|~ [Gray C800, Gray C900, Gray C950]
+-- ])
+-- @
+--
+-- = Integration with Reflex.Dom
+--
+-- ClasshSS provides helper functions for text in Reflex.Dom:
+--
+-- @
+-- import Reflex.Dom.Core
+--
+-- -- Static text with styling
+-- textS :: Text -> Text -> m ()
+-- textS classes content = elClass \"span\" classes $ text content
+--
+-- -- Usage:
+-- textS $(classh' [text_size .~~ XL, text_weight .~~ Bold]) \"Hello!\"
+-- @
+--
+-- = Size Scale
+--
+-- TextSize options (from smallest to largest):
+--
+-- @
+-- XS -- text-xs (0.75rem, 12px)
+-- SM -- text-sm (0.875rem, 14px)
+-- Base -- text-base (1rem, 16px)
+-- LG -- text-lg (1.125rem, 18px)
+-- XL -- text-xl (1.25rem, 20px)
+-- XL2 -- text-2xl (1.5rem, 24px)
+-- XL3 -- text-3xl (1.875rem, 30px)
+-- XL4 -- text-4xl (2.25rem, 36px)
+-- XL5 -- text-5xl (3rem, 48px)
+-- XL6 -- text-6xl (3.75rem, 60px)
+-- XL7 -- text-7xl (4.5rem, 72px)
+-- XL8 -- text-8xl (6rem, 96px)
+-- XL9 -- text-9xl (8rem, 128px)
+-- @
+--
+-- = Weight Scale
+--
+-- TextWeight options:
+--
+-- @
+-- Thin -- font-thin (100)
+-- Extralight -- font-extralight (200)
+-- Light -- font-light (300)
+-- Normal -- font-normal (400)
+-- Medium -- font-medium (500)
+-- Semibold -- font-semibold (600)
+-- Bold -- font-bold (700)
+-- Extrabold -- font-extrabold (800)
+-- Black_TextWeight -- font-black (900)
+-- @
+--
+-- = Re-Exported Modules
+--
+-- This module re-exports all text-related modules for convenience:
+--
+-- * "Classh.Color" - Color types
+-- * "Classh.Cursor" - Cursor styles
+-- * "Classh.Text.Decoration" - Text decoration configuration
+-- * "Classh.Text.FontStyle" - Italic and normal styles
+-- * "Classh.Text.Font" - Font family types
+-- * "Classh.Text.Size" - Text size types
+-- * "Classh.Text.Weight" - Font weight types
+--
+-- = See Also
+--
+-- * "Classh" - Main module with Template Haskell functions
+-- * "Classh.Box" - For element/box styling
+-- * "Classh.Setters" - Operator documentation
+-- * "Classh.Responsive.WhenTW" - Responsive value system
+-- * @docs/features/TEXT_STYLING.md@ - Complete feature guide
+-- * @docs/examples/@ - Real-world examples
--
--- We can also add any arbitrary classes to the end of the TextConfigTW using its HasCustom instance
--------------------------------------------------------------------------------
module Classh.Text
@@ -118,15 +329,55 @@ instance CompileStyle TextConfigTW where
instance Default TextConfigTW where
def = TextConfigTW def def def def def def def ""
+-- | Configuration type for styling text content.
+--
+-- TextConfigTW contains all text-specific styling properties. It is intentionally
+-- separate from 'Classh.Box.BoxConfig' to enforce modularity and prevent CSS conflicts.
+--
+-- === Field Overview
+--
+-- * Typography: '_text_size', '_text_weight', '_text_font', '_text_style'
+-- * Color: '_text_color'
+-- * Decoration: '_text_decoration' (underline, overline, strikethrough)
+-- * Interaction: '_text_cursor'
+-- * Escape hatch: '_text_custom'
+--
+-- === Examples
+--
+-- @
+-- -- Simple heading
+-- def & text_size .~~ XL3 & text_weight .~~ Bold
+--
+-- -- Responsive body text
+-- def
+-- & text_size .|~ [SM, Base, LG]
+-- & text_color .~~ Gray C700
+--
+-- -- Themed text with custom font
+-- def
+-- & text_font .~~ Font_Custom \"Sarabun\"
+-- & text_size .~~ Base
+-- & text_color .~~ Blue C950
+-- @
+--
+-- @since 0.1.0.0
data TextConfigTW = TextConfigTW
{ _text_size :: WhenTW TextSize
+ -- ^ Font size (XS, SM, Base, LG, XL through XL9). Default: empty (browser default)
, _text_weight :: WhenTW TextWeight
- , _text_font :: WhenTW Font -- many options -- EG. Sarabun -> font-[Sarabun]
- , _text_color :: WhenTW Color
+ -- ^ Font weight (Thin through Black_TextWeight). Default: empty (browser default)
+ , _text_font :: WhenTW Font
+ -- ^ Font family (Sans, Serif, Mono, or Font_Custom \"Name\"). Default: empty (browser default)
+ , _text_color :: WhenTW ColorWithOpacity
+ -- ^ Text color with optional opacity. Default: empty (browser default, usually black)
, _text_decoration :: TextDecorationTW
+ -- ^ Text decoration (underline, overline, strikethrough, color, style, thickness, offset)
, _text_style :: WhenTW FontStyle
+ -- ^ Font style (Italic or NotItalic). Default: empty (NotItalic)
, _text_cursor :: WhenTW CursorStyle
+ -- ^ Mouse cursor style. Default: empty (default cursor)
, _text_custom :: T.Text
+ -- ^ Arbitrary custom Tailwind classes. Default: empty string
}
makeLenses ''TextConfigTW
diff --git a/src/Classh/Text/Decoration.hs b/src/Classh/Text/Decoration.hs
index e690a19..34096d8 100644
--- a/src/Classh/Text/Decoration.hs
+++ b/src/Classh/Text/Decoration.hs
@@ -71,7 +71,7 @@ instance ShowTW TextDecorationTW where
data TextDecorationTW = TextDecorationTW
{ _textDec_line :: WhenTW TextDecLineType
-- ^ https://tailwindcss.com/docs/text-decoration
- , _textDec_color :: WhenTW Color
+ , _textDec_color :: WhenTW ColorWithOpacity
-- ^ https://tailwindcss.com/docs/text-decoration-color
, _textDec_style :: WhenTW TextDecStyle
-- ^ https://tailwindcss.com/docs/text-decoration-style
diff --git a/src/Classh/WithTransition.hs b/src/Classh/WithTransition.hs
new file mode 100644
index 0000000..d3875f1
--- /dev/null
+++ b/src/Classh/WithTransition.hs
@@ -0,0 +1,152 @@
+{-# LANGUAGE MultiParamTypeClasses #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+--------------------------------------------------------------------------------
+-- |
+-- Module : Classh.WithTransition
+-- Copyright : (c) 2024, Galen Sprout
+-- License : BSD-style (see end of this file)
+--
+-- Maintainer : Galen Sprout
+-- Stability : provisional
+-- Portability : portable
+--
+-- Types to support CSS transitions bound to specific property values
+--
+-- Example use:
+--
+-- @
+-- -- Builder pattern
+-- bgColor .~^ [ ("def", purple)
+-- , ("hover", lavender `withTransition` Duration_300)
+-- , ("focus", indigo `withTransition` Duration_300 `withTiming` Ease_InOut)
+-- ]
+--
+-- -- All-at-once
+-- bgColor .~^ [ ("def", purple)
+-- , ("hover", lavender `withTransitionAll` Duration_300 Ease_InOut Delay_0)
+-- ]
+-- @
+--------------------------------------------------------------------------------
+
+module Classh.WithTransition where
+
+import Classh.Box.Transition
+import Classh.Responsive.WhenTW
+import Classh.Internal.Chain
+import Classh.Internal.TShow (tshow)
+import Data.Default
+import qualified Data.Text as T
+
+-- | Wraps a value with an optional transition configuration
+-- This allows transitions to be bound to specific property values
+data WithTransition a = WithTransition
+ { _wtValue :: a
+ , _wtTransition :: Maybe TransitionConfig
+ } deriving (Show, Eq)
+
+-- | Builder pattern: Start with duration, optionally chain timing/delay
+-- Example: lavender `withTransition` Duration_300 `withTiming` Ease_InOut
+withTransition :: a -> TransitionDuration -> WithTransition a
+withTransition val duration = WithTransition val (Just $ TransitionConfig
+ { _transitionDuration = duration
+ , _transitionTiming = def
+ , _transitionDelay = def
+ })
+
+-- | Builder: Add timing function to an existing WithTransition
+-- Example: ... `withTiming` Ease_InOut
+withTiming :: WithTransition a -> TransitionTimingFunction -> WithTransition a
+withTiming (WithTransition val Nothing) timing =
+ WithTransition val (Just $ TransitionConfig def timing def)
+withTiming (WithTransition val (Just cfg)) timing =
+ WithTransition val (Just $ cfg { _transitionTiming = timing })
+
+-- | Builder: Add delay to an existing WithTransition
+-- Example: ... `withDelay` Delay_100
+withDelay :: WithTransition a -> TransitionDelay -> WithTransition a
+withDelay (WithTransition val Nothing) delay =
+ WithTransition val (Just $ TransitionConfig def def delay)
+withDelay (WithTransition val (Just cfg)) delay =
+ WithTransition val (Just $ cfg { _transitionDelay = delay })
+
+-- | Create a value with all transition params at once
+-- Example: lavender `withTransitionAll` Duration_300 Ease_InOut Delay_100
+withTransitionAll :: a -> TransitionDuration -> TransitionTimingFunction -> TransitionDelay -> WithTransition a
+withTransitionAll val duration timing delay = WithTransition val (Just $ TransitionConfig duration timing delay)
+
+-- | Create a value with a pre-built transition config
+-- Example: lavender `withTransitionFull` myConfig
+withTransitionFull :: a -> TransitionConfig -> WithTransition a
+withTransitionFull val cfg = WithTransition val (Just cfg)
+
+-- | Create a value without a transition
+noTransition :: a -> WithTransition a
+noTransition val = WithTransition val Nothing
+
+instance Default a => Default (WithTransition a) where
+ def = WithTransition def Nothing
+
+instance Functor WithTransition where
+ fmap f (WithTransition val trans) = WithTransition (f val) trans
+
+-- | Helper for rendering WhenTW values wrapped in WithTransition
+-- Extracts the value, renders it with the provided function,
+-- and adds transition classes if a transition config is present
+renderWithTransitionTW :: WhenTW (WithTransition a)
+ -> (a -> T.Text)
+ -> TransitionProperty
+ -> T.Text
+renderWithTransitionTW tws construct prop = foldr (<&>) mempty $
+ fmap (\(c, WithTransition val mTransCfg) ->
+ let prefix = if c == "def" then "" else (c <> ":")
+ valueClass = prefix <> construct val
+ transitionClasses = case mTransCfg of
+ Nothing -> mempty
+ Just cfg ->
+ let cssProp = transitionPropertyToCSSName prop
+ duration = T.drop 9 $ tshow (_transitionDuration cfg) -- Remove "Duration_" prefix
+ timing = transitionTimingToCSSName (_transitionTiming cfg)
+ delay = T.drop 6 $ tshow (_transitionDelay cfg) -- Remove "Delay_" prefix
+ -- Format: [transition:property_duration_timing_delay]
+ transValue = cssProp <> "_" <> duration <> "ms_" <> timing <> "_" <> delay <> "ms"
+ in prefix <> "[transition:" <> transValue <> "]"
+ in valueClass <&> transitionClasses
+ ) tws
+
+-- | Convert TransitionProperty to CSS property name for arbitrary value syntax
+transitionPropertyToCSSName :: TransitionProperty -> T.Text
+transitionPropertyToCSSName = \case
+ Transition_None -> "none"
+ Transition_All -> "all"
+ Transition -> "all" -- Default transition affects all properties
+ Transition_Colors -> "background-color,border-color,color,fill,stroke"
+ Transition_Opacity -> "opacity"
+ Transition_Shadow -> "box-shadow"
+ Transition_Transform -> "transform"
+ Transition_Custom val -> val
+
+-- | Convert TransitionTimingFunction to CSS timing function name
+transitionTimingToCSSName :: TransitionTimingFunction -> T.Text
+transitionTimingToCSSName = \case
+ Ease_Linear -> "linear"
+ Ease_In -> "in"
+ Ease_Out -> "out"
+ Ease_InOut -> "in-out"
+ Ease_Custom val -> val
+
+-- | Helper for compiling WithTransition values (with duplicate checking)
+compileWithTransitionTW :: WhenTW (WithTransition a)
+ -> (a -> T.Text)
+ -> TransitionProperty
+ -> Either T.Text T.Text
+compileWithTransitionTW tws construct prop = case f $ fmap fst tws of
+ Left e -> Left e
+ Right () -> Right $ renderWithTransitionTW tws construct prop
+ where
+ f [] = Right ()
+ f (s:ss) =
+ if elem s ss
+ then Left $ s <> " exists twice"
+ else f ss
diff --git a/test/ComprehensiveTest.hs b/test/ComprehensiveTest.hs
new file mode 100644
index 0000000..5dafb26
--- /dev/null
+++ b/test/ComprehensiveTest.hs
@@ -0,0 +1,216 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Classh
+import Classh.Box
+import Classh.Box.Padding
+import Classh.Box.Margin
+import Classh.Box.Border
+import Classh.Box.Shadow
+import Classh.Box.Placement
+import Classh.Box.TWSize
+import Classh.Box.SizingBand
+import Classh.Color
+import Classh.Box.Transition
+import Classh.WithTransition
+import Classh.Setters
+import Classh.Class.CompileStyle
+import Classh.Class.SetSides
+import Classh.Text
+import Control.Lens ((&))
+import Data.Default (def)
+import qualified Data.Text as T
+
+-- Box property tests
+testBoxBasics :: BoxConfig
+testBoxBasics = def
+ & colStart .~~ 1
+ & colSpan .~~ 6
+ & bgColor .~~ solidColor (Blue C500)
+ & bgOpacity .~~ 80
+
+-- Responsive properties with .|~
+testResponsive :: BoxConfig
+testResponsive = def
+ & bgColor .|~ [solidColor (Gray C100), solidColor (Gray C200), solidColor (Gray C300), solidColor (Gray C400), solidColor (Gray C500), solidColor (Gray C600)]
+
+-- Padding with SetSides shorthand
+testPaddingSetSides :: BoxConfig
+testPaddingSetSides = def
+ & padding . b .~~ TWSize 4
+ & padding . t .~~ TWSize 8
+ & padding . x .~~ TWSize 2
+
+-- Padding with transitions
+testPaddingTransitions :: BoxConfig
+testPaddingTransitions = def
+ & padding . paddingB .~^ [ ("def", noTransition (TWSize 4))
+ , ("hover", TWSize 8 `withTransition` Duration_300)
+ ]
+
+-- Margin tests
+testMargin :: BoxConfig
+testMargin = def
+ & margin . marginL .~~ TWSize 2
+ & margin . marginR .~~ TWSize 2
+ & margin . y .~~ TWSize 4
+
+-- Border tests
+testBorder :: BoxConfig
+testBorder = def
+ & border . bWidth . b .~~ B2
+ & border . bColor . allS .~~ color (Red C500)
+ & border . radius . borderRadius_tr .~~ R_Lg
+
+-- Border with transitions
+testBorderTransitions :: BoxConfig
+testBorderTransitions = def
+ & border . bColor . allS .~^ [ ("def", noTransition (color (Blue C500)))
+ , ("hover", color (Red C500) `withTransition` Duration_200)
+ ]
+
+-- Shadow tests
+testShadow :: BoxConfig
+testShadow = def
+ & shadow .~~ Shadow_Lg
+
+-- Shadow with transitions
+testShadowTransitions :: BoxConfig
+testShadowTransitions = def
+ & shadow .~^ [ ("def", noTransition Shadow_Sm)
+ , ("hover", Shadow_Xl `withTransition` Duration_300)
+ ]
+
+-- Position tests
+testPosition :: BoxConfig
+testPosition = def
+ & position .~~ (J_Center, A_Center)
+
+-- Sizing tests - TODO: Fix lens composition issue
+-- testSizing :: BoxConfig
+-- testSizing = def
+-- & (sizingBand . w) .~~ TWSize 64
+-- & (sizingBand . h) .~~ TWSize 32
+-- & (sizingBand . maxW) .~~ TWSize_Screen
+
+-- Combined complex test
+testComplex :: BoxConfig
+testComplex = def
+ & bgColor .~^ [ ("def", noTransition (solidColor (Blue C600)))
+ , ("hover", solidColor (Blue C400) `withTransition` Duration_300 `withTiming` Ease_InOut)
+ ]
+ & padding . x .~~ TWSize 4
+ & padding . y .~~ TWSize 2
+ & border . radius . allS .~~ R_Md
+ & shadow .~^ [ ("def", noTransition Shadow_None)
+ , ("hover", Shadow_Lg `withTransition` Duration_200)
+ ]
+
+-- Text tests - TODO: Add Show instance for TextConfigTW
+-- testText :: TextConfigTW
+-- testText = def
+-- & text_size .~~ XL
+-- & text_weight .~~ Bold
+-- & text_color .~~ White
+
+testCase :: (CompileStyle a, Show a) => String -> a -> T.Text -> IO Bool
+testCase name cfg expected = do
+ putStrLn $ "\n" ++ replicate 80 '-'
+ putStrLn $ "TEST: " ++ name
+ putStrLn $ replicate 80 '-'
+ case compileS cfg of
+ Left err -> do
+ putStrLn $ "❌ ERROR: " ++ show err
+ return False
+ Right result -> do
+ let success = result == expected
+ putStrLn $ "Output:"
+ putStrLn $ " " ++ show result
+ putStrLn ""
+ if success
+ then putStrLn "✓ PASS"
+ else do
+ putStrLn "✗ FAIL"
+ putStrLn $ "\nExpected:"
+ putStrLn $ " " ++ show expected
+ return success
+
+main :: IO ()
+main = do
+ putStrLn ""
+ putStrLn $ replicate 80 '='
+ putStrLn " ClasshSS Comprehensive Test Suite"
+ putStrLn $ replicate 80 '='
+
+ results <- sequenceA
+ [ testCase "Box basics (colStart, colSpan, bgColor, bgOpacity)"
+ testBoxBasics
+ "col-start-1 col-span-6 bg-blue-500 bg-opacity-80"
+
+ , testCase "Responsive bgColor (.|~)"
+ testResponsive
+ "bg-gray-100 sm:bg-gray-200 md:bg-gray-300 lg:bg-gray-400 xl:bg-gray-500 2xl:bg-gray-600"
+
+ , testCase "Padding with SetSides shorthand (b, t, x)"
+ testPaddingSetSides
+ "pl-2 pr-2 pt-8 pb-4"
+
+ , testCase "Padding with transitions"
+ testPaddingTransitions
+ "pb-4 hover:pb-8 hover:[transition:all_300ms_linear_0ms]"
+
+ , testCase "Margin (marginL, marginR, marginY)"
+ testMargin
+ "ml-2 mr-2 mt-4 mb-4"
+
+ , testCase "Border (width, color, radius)"
+ testBorder
+ "rounded-tr-lg border-b-2 border-l-red-500 border-r-red-500 border-t-red-500 border-b-red-500"
+
+ , testCase "Border color with transitions"
+ testBorderTransitions
+ "border-l-blue-500 hover:border-l-red-500 hover:[transition:background-color,border-color,color,fill,stroke_200ms_linear_0ms] border-r-blue-500 hover:border-r-red-500 hover:[transition:background-color,border-color,color,fill,stroke_200ms_linear_0ms] border-t-blue-500 hover:border-t-red-500 hover:[transition:background-color,border-color,color,fill,stroke_200ms_linear_0ms] border-b-blue-500 hover:border-b-red-500 hover:[transition:background-color,border-color,color,fill,stroke_200ms_linear_0ms]"
+
+ , testCase "Shadow"
+ testShadow
+ "shadow-lg"
+
+ , testCase "Shadow with transitions"
+ testShadowTransitions
+ "shadow-sm hover:shadow-xl hover:[transition:box-shadow_300ms_linear_0ms]"
+
+ , testCase "Position (center/center)"
+ testPosition
+ "grid justify-items-center content-center"
+
+ -- , testCase "Sizing (w, h, maxW)"
+ -- testSizing
+ -- "w-64 h-32 max-w-screen"
+
+ , testCase "Complex combined properties"
+ testComplex
+ "rounded-tr-md rounded-tl-md rounded-br-md rounded-bl-md pl-4 pr-4 pt-2 pb-2 bg-blue-600 hover:bg-blue-400 hover:[transition:background-color,border-color,color,fill,stroke_300ms_in-out_0ms] shadow-none hover:shadow-lg hover:[transition:box-shadow_200ms_linear_0ms]"
+
+ -- , testCase "Text (size, weight, color)"
+ -- testText
+ -- "text-xl font-bold text-white"
+ ]
+
+ putStrLn ""
+ putStrLn $ replicate 80 '='
+ putStrLn " SUMMARY"
+ putStrLn $ replicate 80 '='
+ let passed = length $ filter id results
+ total = length results
+ putStrLn $ "Tests passed: " ++ show passed ++ "/" ++ show total
+ putStrLn ""
+
+ if and results
+ then do
+ putStrLn "✓ ALL TESTS PASSED!"
+ putStrLn $ replicate 80 '='
+ else do
+ putStrLn "✗ SOME TESTS FAILED"
+ putStrLn $ replicate 80 '='
diff --git a/test/GenerateHTMLTest.hs b/test/GenerateHTMLTest.hs
new file mode 100644
index 0000000..4442b9a
--- /dev/null
+++ b/test/GenerateHTMLTest.hs
@@ -0,0 +1,200 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Classh
+import Classh.Box
+import Classh.Box.Padding
+import Classh.Box.Margin
+import Classh.Box.Border
+import Classh.Box.Shadow
+import Classh.Color
+import Classh.Box.Transition
+import Classh.WithTransition
+import Classh.Setters
+import Classh.Class.CompileStyle
+import Classh.Class.SetSides
+import Control.Lens ((&))
+import Data.Default (def)
+import qualified Data.Text as T
+import Data.Either (fromRight)
+
+-- Test configurations
+tests :: [(String, BoxConfig, String)]
+tests =
+ [ ("Background Color - Hover + Focus + Active", testBgAllStates, "Hover (blue), Focus (green), Click (red)")
+ , ("Border Color - Hover + Focus + Active", testBorderAllStates, "Hover (purple border), Focus (yellow border), Click (pink border)")
+ , ("Combined Bg + Border - All States", testCombined, "Both background and border should transition")
+ , ("Responsive Breakpoints", testResponsive, "Resize window to see color change at breakpoints")
+ , ("Shadow Transitions", testShadow, "Hover to see shadow transition")
+ , ("Complex Combined", testComplex, "Background, border, shadow, and padding all transition")
+
+ -- Comprehensive transition tests
+ , ("Comprehensive Responsive Transitions", testComprehensiveResponsiveTransitions,
+ "Resize window through ALL breakpoints (640px, 768px, 1024px, 1280px, 1536px). Each has unique transition timing! Hover for pink, focus for cyan.")
+ , ("Stacked Transitions (Responsive + Hover)", testStackedTransitions,
+ "Resize AND hover - see how responsive + interactive transitions combine with border color")
+ , ("Delay Showcase", testDelayShowcase,
+ "Compare delays: Hover (500ms wait) vs Focus (1000ms wait)")
+ , ("Speed Comparison", testSpeedComparison,
+ "Lightning fast (75ms) vs slow motion (1000ms)")
+ ]
+
+testBgAllStates :: BoxConfig
+testBgAllStates = def
+ & bgColor .~^ [ ("def", noTransition (solidColor (Gray C500)))
+ , ("hover", solidColor (Blue C500) `withTransition` Duration_300 `withTiming` Ease_InOut)
+ , ("focus", solidColor (Green C500) `withTransition` Duration_300 `withTiming` Ease_InOut)
+ ]
+
+testBorderAllStates :: BoxConfig
+testBorderAllStates = def
+ & border . bWidth . allS .~~ B4
+ & border . bColor . allS .~^ [ ("def", noTransition (color (Gray C400)))
+ , ("hover", color (Purple C500) `withTransition` Duration_300 `withTiming` Ease_InOut)
+ , ("focus", color (Yellow C500) `withTransition` Duration_300 `withTiming` Ease_InOut)
+ ]
+
+testCombined :: BoxConfig
+testCombined = def
+ & bgColor .~^ [ ("def", noTransition (solidColor (Blue C600)))
+ , ("hover", solidColor (Blue C400) `withTransition` Duration_300 `withTiming` Ease_InOut)
+ , ("focus", solidColor (Green C600) `withTransition` Duration_300 `withTiming` Ease_InOut)
+ ]
+ & border . bWidth . allS .~~ B4
+ & border . bColor . allS .~^ [ ("def", noTransition (color (Blue C800)))
+ , ("hover", color (Blue C600) `withTransition` Duration_300 `withTiming` Ease_InOut)
+ , ("focus", color (Green C800) `withTransition` Duration_300 `withTiming` Ease_InOut)
+ ]
+
+testResponsive :: BoxConfig
+testResponsive = def
+ & bgColor .|~ [ solidColor (Gray C800), solidColor (Red C600), solidColor (Orange C600), solidColor (Yellow C600), solidColor (Green C600), solidColor (Blue C600) ]
+
+testShadow :: BoxConfig
+testShadow = def
+ & shadow .~^ [ ("def", noTransition Shadow_Sm)
+ , ("hover", Shadow_Xl `withTransition` Duration_300)
+ ]
+
+testComplex :: BoxConfig
+testComplex = def
+ & bgColor .~^ [ ("def", noTransition (solidColor (Blue C600)))
+ , ("hover", solidColor (Blue C400) `withTransition` Duration_300 `withTiming` Ease_InOut)
+ ]
+ & padding . x .~~ TWSize 4
+ & padding . y .~~ TWSize 2
+ & border . radius . allS .~~ R_Md
+ & shadow .~^ [ ("def", noTransition Shadow_None)
+ , ("hover", Shadow_Lg `withTransition` Duration_200)
+ ]
+
+-- Comprehensive test: All 6 responsive breakpoints with unique transitions
+testComprehensiveResponsiveTransitions :: BoxConfig
+testComprehensiveResponsiveTransitions = def
+ & bgColor .~^
+ [ ("def", solidColor (Red C600) `withTransition` Duration_500 `withTiming` Ease_Linear `withDelay` Delay_0)
+ , ("sm", solidColor (Orange C600) `withTransition` Duration_300 `withTiming` Ease_In `withDelay` Delay_100)
+ , ("md", solidColor (Yellow C600) `withTransition` Duration_700 `withTiming` Ease_Out `withDelay` Delay_150)
+ , ("lg", solidColor (Green C600) `withTransition` Duration_200 `withTiming` Ease_InOut `withDelay` Delay_0)
+ , ("xl", solidColor (Blue C600) `withTransition` Duration_1000 `withTiming` Ease_Linear `withDelay` Delay_300)
+ , ("2xl", solidColor (Purple C600) `withTransition` Duration_500 `withTiming` Ease_InOut `withDelay` Delay_75)
+ , ("hover", solidColor (Pink C400) `withTransition` Duration_150 `withTiming` Ease_Out `withDelay` Delay_0)
+ , ("focus", solidColor (Cyan C400) `withTransition` Duration_300 `withTiming` Ease_InOut `withDelay` Delay_200)
+ ]
+
+testStackedTransitions :: BoxConfig
+testStackedTransitions = def
+ & bgColor .~^
+ [ ("def", solidColor (Gray C800) `withTransition` Duration_300 `withTiming` Ease_Linear)
+ , ("sm", solidColor (Gray C700) `withTransition` Duration_300 `withTiming` Ease_In)
+ , ("hover", solidColor (Green C500) `withTransition` Duration_200 `withTiming` Ease_Out)
+ ]
+ & border . bWidth . allS .~~ B2
+ & border . bColor . allS .~^
+ [ ("def", color (Gray C600) `withTransition` Duration_300)
+ , ("hover", color (Green C400) `withTransition` Duration_200)
+ ]
+
+testDelayShowcase :: BoxConfig
+testDelayShowcase = def
+ & bgColor .~^
+ [ ("def", solidColor (Blue C600) `withTransition` Duration_300 `withTiming` Ease_InOut `withDelay` Delay_0)
+ , ("hover", solidColor (Blue C400) `withTransition` Duration_300 `withTiming` Ease_InOut `withDelay` Delay_500)
+ , ("focus", solidColor (Blue C200) `withTransition` Duration_300 `withTiming` Ease_InOut `withDelay` Delay_1000)
+ ]
+
+testSpeedComparison :: BoxConfig
+testSpeedComparison = def
+ & bgColor .~^
+ [ ("def", solidColor (Purple C600) `withTransition` Duration_75 `withTiming` Ease_Linear)
+ , ("hover", solidColor (Purple C400) `withTransition` Duration_1000 `withTiming` Ease_Linear)
+ ]
+
+generateHTML :: IO ()
+generateHTML = do
+ let htmlHeader = T.unlines
+ [ ""
+ , ""
+ , ""
+ , " "
+ , " "
+ , " ClasshSS Transition Tests"
+ , " "
+ , " "
+ , ""
+ , ""
+ , " "
+ , " Base (<640px)"
+ , " SM (≥640px)"
+ , " MD (≥768px)"
+ , " LG (≥1024px)"
+ , " XL (≥1280px)"
+ , " 2XL (≥1536px)"
+ , "
"
+ , " "
+ , "
ClasshSS Generated Transition Tests
"
+ ]
+
+ let htmlFooter = T.unlines
+ [ "
"
+ , "
Instructions:
"
+ , "
"
+ , " - Hover over each box to test transitions
"
+ , " - Tab or click to focus boxes (green state)
"
+ , " - Resize window to test responsive breakpoints
"
+ , " - Check if transitions are smooth or instant
"
+ , "
"
+ , "
"
+ , "
"
+ , ""
+ , ""
+ ]
+
+ let testSections = map generateTestSection tests
+ let fullHTML = htmlHeader <> T.concat testSections <> htmlFooter
+
+ writeFile "test-output.html" (T.unpack fullHTML)
+ putStrLn "Generated test-output.html"
+
+generateTestSection :: (String, BoxConfig, String) -> T.Text
+generateTestSection (name, cfg, description) =
+ let classes = fromRight "ERROR" (compileS cfg)
+ in T.unlines
+ [ " "
+ , "
" <> T.pack name <> "
"
+ , "
" <> classes <> "
"
+ , "
classes <> " p-6 text-white text-center rounded-lg cursor-pointer\">"
+ , " " <> T.pack description
+ , "
"
+ , "
"
+ ]
+
+main :: IO ()
+main = do
+ generateHTML
+ putStrLn "✓ Test complete! Open test-output.html in your browser to view."
diff --git a/test/GradientTest.hs b/test/GradientTest.hs
new file mode 100644
index 0000000..db72335
--- /dev/null
+++ b/test/GradientTest.hs
@@ -0,0 +1,213 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Classh
+import Classh.Class.CompileStyle
+import Control.Lens ((&))
+import Data.Default (def)
+import qualified Data.Text as T
+
+-- Test 1: Solid color renders correctly (backwards compat)
+testSolid :: BoxConfig
+testSolid = def
+ & bgColor .~~ solidColor (Blue C500)
+
+-- Test 2: Simple two-color gradient
+testLinearGradient :: BoxConfig
+testLinearGradient = def
+ & bgColor .~~ linearGradient To_R (hex "4E366C") White
+
+-- Test 3: Three-color gradient with via
+testLinearGradientVia :: BoxConfig
+testLinearGradientVia = def
+ & bgColor .~~ linearGradientVia To_BR (Purple C500) (Pink C500) White
+
+-- Test 4: Gradient with stop positions
+testGradientPositions :: BoxConfig
+testGradientPositions = def
+ & bgColor .~~ linearGradientViaPos To_R
+ (stopAt (hex "4E366C") 10)
+ (stopAt (Pink C500) 30)
+ (stopAt White 90)
+
+-- Test 5: Single color gradient (fade to transparent)
+testGradientFrom :: BoxConfig
+testGradientFrom = def
+ & bgColor .~~ gradientFrom To_R (hex "4E366C")
+
+-- Test 6: Two-color gradient with positions
+testLinearGradientPos :: BoxConfig
+testLinearGradientPos = def
+ & bgColor .~~ linearGradientPos To_B
+ (stopAt (Blue C500) 0)
+ (stopAt (Purple C500) 100)
+
+-- Test 7: Gradient with hover transition
+testGradientTransition :: BoxConfig
+testGradientTransition = def
+ & bgColor .~^ [ ("def", noTransition (solidColor (Blue C500)))
+ , ("hover", linearGradient To_R (Purple C500) (Pink C500) `withTransition` Duration_300)
+ ]
+
+-- Test 8: Responsive gradients
+testResponsiveGradient :: BoxConfig
+testResponsiveGradient = def
+ & bgColor .|~ [ solidColor (Gray C500)
+ , linearGradient To_R (Blue C500) (Purple C500)
+ ]
+
+-- Test 9: All directions
+testDirectionT :: BoxConfig
+testDirectionT = def & bgColor .~~ linearGradient To_T (Blue C500) White
+
+testDirectionTR :: BoxConfig
+testDirectionTR = def & bgColor .~~ linearGradient To_TR (Blue C500) White
+
+testDirectionBL :: BoxConfig
+testDirectionBL = def & bgColor .~~ linearGradient To_BL (Blue C500) White
+
+testDirectionL :: BoxConfig
+testDirectionL = def & bgColor .~~ linearGradient To_L (Blue C500) White
+
+testDirectionTL :: BoxConfig
+testDirectionTL = def & bgColor .~~ linearGradient To_TL (Blue C500) White
+
+-- Test 10: Solid color with opacity
+testSolidColorOpacity :: BoxConfig
+testSolidColorOpacity = def
+ & bgColor .~~ solidColorOpacity (hex "221326") 87
+
+-- Test 11: Gradient stop with opacity
+testStopWithOpacity :: BoxConfig
+testStopWithOpacity = def
+ & bgColor .~~ linearGradientPos To_BR
+ (stopAtWithOpacity (hex "181422") 90 0)
+ (stopAt (hex "281C40") 100)
+
+-- Test 12: Named color with opacity
+testNamedColorOpacity :: BoxConfig
+testNamedColorOpacity = def
+ & bgColor .~~ solidColorOpacity (Blue C500) 50
+
+-- Test 13: color helper (no opacity suffix)
+testColorHelper :: BoxConfig
+testColorHelper = def
+ & bgColor .~~ solidColor White
+
+testCase :: String -> BoxConfig -> T.Text -> IO Bool
+testCase name cfg expected = do
+ putStrLn $ "\n" ++ replicate 80 '-'
+ putStrLn $ "TEST: " ++ name
+ putStrLn $ replicate 80 '-'
+ case compileS cfg of
+ Left err -> do
+ putStrLn $ "❌ ERROR: " ++ show err
+ return False
+ Right result -> do
+ let success = result == expected
+ putStrLn $ "Output:"
+ putStrLn $ " " ++ show result
+ putStrLn ""
+ if success
+ then putStrLn "✓ PASS"
+ else do
+ putStrLn "✗ FAIL"
+ putStrLn $ "\nExpected:"
+ putStrLn $ " " ++ show expected
+ return success
+
+main :: IO ()
+main = do
+ putStrLn ""
+ putStrLn $ replicate 80 '='
+ putStrLn " Gradient Feature Test Suite"
+ putStrLn $ replicate 80 '='
+
+ results <- sequenceA
+ [ testCase "Solid color (backwards compat)"
+ testSolid
+ "bg-blue-500"
+
+ , testCase "Two-color gradient"
+ testLinearGradient
+ "bg-gradient-to-r from-[#4E366C] to-white"
+
+ , testCase "Three-color gradient with via"
+ testLinearGradientVia
+ "bg-gradient-to-br from-purple-500 via-pink-500 to-white"
+
+ , testCase "Gradient with stop positions"
+ testGradientPositions
+ "bg-gradient-to-r from-[#4E366C] from-10% via-pink-500 via-30% to-white to-90%"
+
+ , testCase "Single color gradient (fade to transparent)"
+ testGradientFrom
+ "bg-gradient-to-r from-[#4E366C]"
+
+ , testCase "Two-color gradient with positions"
+ testLinearGradientPos
+ "bg-gradient-to-b from-blue-500 from-0% to-purple-500 to-100%"
+
+ , testCase "Gradient with hover transition"
+ testGradientTransition
+ "bg-blue-500 hover:bg-gradient-to-r hover:from-purple-500 hover:to-pink-500 hover:[transition:background-color,border-color,color,fill,stroke_300ms_linear_0ms]"
+
+ , testCase "Responsive gradients"
+ testResponsiveGradient
+ "bg-gray-500 sm:bg-gradient-to-r sm:from-blue-500 sm:to-purple-500"
+
+ , testCase "Direction: to-t"
+ testDirectionT
+ "bg-gradient-to-t from-blue-500 to-white"
+
+ , testCase "Direction: to-tr"
+ testDirectionTR
+ "bg-gradient-to-tr from-blue-500 to-white"
+
+ , testCase "Direction: to-bl"
+ testDirectionBL
+ "bg-gradient-to-bl from-blue-500 to-white"
+
+ , testCase "Direction: to-l"
+ testDirectionL
+ "bg-gradient-to-l from-blue-500 to-white"
+
+ , testCase "Direction: to-tl"
+ testDirectionTL
+ "bg-gradient-to-tl from-blue-500 to-white"
+
+ , testCase "Solid color with opacity (hex)"
+ testSolidColorOpacity
+ "bg-[#221326]/87"
+
+ , testCase "Gradient stop with opacity"
+ testStopWithOpacity
+ "bg-gradient-to-br from-[#181422]/90 from-0% to-[#281C40] to-100%"
+
+ , testCase "Named color with opacity"
+ testNamedColorOpacity
+ "bg-blue-500/50"
+
+ , testCase "color helper (no opacity suffix)"
+ testColorHelper
+ "bg-white"
+ ]
+
+ putStrLn ""
+ putStrLn $ replicate 80 '='
+ putStrLn " SUMMARY"
+ putStrLn $ replicate 80 '='
+ let passed = length $ filter id results
+ total = length results
+ putStrLn $ "Tests passed: " ++ show passed ++ "/" ++ show total
+ putStrLn ""
+
+ if and results
+ then do
+ putStrLn "✓ ALL TESTS PASSED!"
+ putStrLn $ replicate 80 '='
+ else do
+ putStrLn "✗ SOME TESTS FAILED"
+ putStrLn $ replicate 80 '='
diff --git a/test/TransformTest.hs b/test/TransformTest.hs
new file mode 100644
index 0000000..4f9b640
--- /dev/null
+++ b/test/TransformTest.hs
@@ -0,0 +1,186 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Classh
+import Classh.Class.CompileStyle
+import Control.Lens ((&))
+import Data.Default (def)
+import qualified Data.Text as T
+
+-- Test 1: Basic rotation
+test1 :: BoxConfig
+test1 = def
+ & transform . rotate .~^ [("def", noTransition Rotate_0), ("hover", noTransition Rotate_180)]
+
+-- Test 2: Scale with transition
+test2 :: BoxConfig
+test2 = def
+ & transform . scale .~^ [ ("def", noTransition Scale_100)
+ , ("hover", Scale_105 `withTransition` Duration_300)
+ ]
+
+-- Test 3: Translate X with TWSize
+test3 :: BoxConfig
+test3 = def
+ & transform . translateX .~^ [("def", noTransition (Translate_TWSize (TWSize 4)))]
+
+-- Test 4: Translate Y with fraction
+test4 :: BoxConfig
+test4 = def
+ & transform . translateY .~^ [("def", noTransition (Translate_Fraction 1 D2))]
+
+-- Test 5: Translate with custom CSS size
+test5 :: BoxConfig
+test5 = def
+ & transform . translateX .~^ [("def", noTransition (Translate_Custom (Rem 1.5)))]
+
+-- Test 6: Skew transform
+test6 :: BoxConfig
+test6 = def
+ & transform . skewX .~^ [("def", noTransition Skew_0), ("hover", noTransition Skew_6)]
+
+-- Test 7: Transform origin
+test7 :: BoxConfig
+test7 = def
+ & transform . transformOrigin .~~ Origin_TopRight
+
+-- Test 8: Combined transforms with transitions
+test8 :: BoxConfig
+test8 = def
+ & transform . rotate .~^ [ ("def", noTransition Rotate_0)
+ , ("hover", Rotate_45 `withTransition` Duration_300)
+ ]
+ & transform . scale .~^ [ ("def", noTransition Scale_100)
+ , ("hover", Scale_110 `withTransition` Duration_300)
+ ]
+
+-- Test 9: All rotation values
+test9 :: BoxConfig
+test9 = def
+ & transform . rotate .~^ [("def", noTransition Rotate_90)]
+
+-- Test 10: All scale values
+test10 :: BoxConfig
+test10 = def
+ & transform . scale .~^ [("def", noTransition Scale_95)]
+
+-- Test 11: Translate with 0
+test11 :: BoxConfig
+test11 = def
+ & transform . translateX .~^ [("def", noTransition Translate_0)]
+
+-- Test 12: Translate with px
+test12 :: BoxConfig
+test12 = def
+ & transform . translateY .~^ [("def", noTransition Translate_Px)]
+
+-- Test 13: Translate with full
+test13 :: BoxConfig
+test13 = def
+ & transform . translateX .~^ [("def", noTransition Translate_Full)]
+
+-- Test 14: Custom rotate
+test14 :: BoxConfig
+test14 = def
+ & transform . rotate .~^ [("def", noTransition (Rotate_Custom "17deg"))]
+
+-- Test 15: Custom scale
+test15 :: BoxConfig
+test15 = def
+ & transform . scale .~^ [("def", noTransition (Scale_Custom "102"))]
+
+testCase :: String -> BoxConfig -> T.Text -> IO Bool
+testCase name cfg expected = do
+ putStrLn $ "\n" ++ replicate 80 '-'
+ putStrLn $ "TEST: " ++ name
+ putStrLn $ replicate 80 '-'
+ case compileS cfg of
+ Left err -> do
+ putStrLn $ "❌ ERROR: " ++ show err
+ return False
+ Right result -> do
+ let success = result == expected
+ putStrLn $ "Output:"
+ putStrLn $ " " ++ show result
+ putStrLn ""
+ if success
+ then putStrLn "✓ PASS"
+ else do
+ putStrLn "✗ FAIL"
+ putStrLn $ "\nExpected:"
+ putStrLn $ " " ++ show expected
+ return success
+
+main :: IO ()
+main = do
+ putStrLn ""
+ putStrLn $ replicate 80 '='
+ putStrLn " Transform Feature Test Suite"
+ putStrLn $ replicate 80 '='
+
+ results <- sequenceA
+ [ testCase "Test 1 (basic rotation)"
+ test1
+ "rotate-0 hover:rotate-180"
+ , testCase "Test 2 (scale with transition)"
+ test2
+ "scale-100 hover:scale-105 hover:[transition:transform_300ms_linear_0ms]"
+ , testCase "Test 3 (translate X with TWSize)"
+ test3
+ "translate-x-4"
+ , testCase "Test 4 (translate Y with fraction)"
+ test4
+ "translate-y-1/2"
+ , testCase "Test 5 (translate X with custom CSS size)"
+ test5
+ "translate-x-[1.5rem]"
+ , testCase "Test 6 (skew transform)"
+ test6
+ "skew-x-0 hover:skew-x-6"
+ , testCase "Test 7 (transform origin)"
+ test7
+ "origin-top-right"
+ , testCase "Test 8 (combined transforms with transitions)"
+ test8
+ "rotate-0 hover:rotate-45 hover:[transition:transform_300ms_linear_0ms] scale-100 hover:scale-110 hover:[transition:transform_300ms_linear_0ms]"
+ , testCase "Test 9 (rotate 90)"
+ test9
+ "rotate-90"
+ , testCase "Test 10 (scale 95)"
+ test10
+ "scale-95"
+ , testCase "Test 11 (translate X 0)"
+ test11
+ "translate-x-0"
+ , testCase "Test 12 (translate Y px)"
+ test12
+ "translate-y-px"
+ , testCase "Test 13 (translate X full)"
+ test13
+ "translate-x-full"
+ , testCase "Test 14 (custom rotate)"
+ test14
+ "rotate-[17deg]"
+ , testCase "Test 15 (custom scale)"
+ test15
+ "scale-[102%]"
+ ]
+
+ putStrLn ""
+ putStrLn $ replicate 80 '='
+ putStrLn " SUMMARY"
+ putStrLn $ replicate 80 '='
+ let passed = length $ filter id results
+ total = length results
+ putStrLn $ "Tests passed: " ++ show passed ++ "/" ++ show total
+ putStrLn ""
+
+ if and results
+ then do
+ putStrLn "✓ ALL TESTS PASSED!"
+ putStrLn $ replicate 80 '='
+ else do
+ putStrLn "✗ SOME TESTS FAILED"
+ putStrLn $ replicate 80 '='
diff --git a/test/TransitionTest.hs b/test/TransitionTest.hs
new file mode 100644
index 0000000..4894776
--- /dev/null
+++ b/test/TransitionTest.hs
@@ -0,0 +1,100 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE OverloadedStrings #-}
+
+module Main where
+
+import Classh
+import Classh.Class.CompileStyle
+import Control.Lens ((&))
+import Data.Default (def)
+import qualified Data.Text as T
+
+-- Test 1: Backwards compatible - no transitions
+test1 :: BoxConfig
+test1 = def
+ & bgColor .~~ solidColor (Gray C500)
+ & colSpan .~~ 2
+
+-- Test 2: Using new (.~^) operator with builder pattern
+test2 :: BoxConfig
+test2 = def
+ & bgColor .~^ [ ("def", noTransition (solidColor (Gray C500)))
+ , ("hover", solidColor (Gray C300) `withTransition` Duration_300)
+ ]
+
+-- Test 3: Builder pattern with chaining
+test3 :: BoxConfig
+test3 = def
+ & bgColor .~^ [ ("def", noTransition (solidColor (Purple C600)))
+ , ("hover", solidColor (Purple C300) `withTransition` Duration_300 `withTiming` Ease_InOut)
+ , ("focus", solidColor (Indigo C500) `withTransition` Duration_500 `withTiming` Ease_Out `withDelay` Delay_100)
+ ]
+
+-- Test 4: All-at-once style
+test4 :: BoxConfig
+test4 = def
+ & bgColor .~^ [ ("def", noTransition (solidColor (Purple C600)))
+ , ("sm", withTransitionAll (solidColor (Indigo C500)) Duration_300 Ease_InOut Delay_0)
+ , ("hover", solidColor (Purple C300) `withTransition` Duration_500)
+ ]
+
+testCase :: String -> BoxConfig -> T.Text -> IO Bool
+testCase name cfg expected = do
+ putStrLn $ "\n" ++ replicate 80 '-'
+ putStrLn $ "TEST: " ++ name
+ putStrLn $ replicate 80 '-'
+ case compileS cfg of
+ Left err -> do
+ putStrLn $ "❌ ERROR: " ++ show err
+ return False
+ Right result -> do
+ let success = result == expected
+ putStrLn $ "Output:"
+ putStrLn $ " " ++ show result
+ putStrLn ""
+ if success
+ then putStrLn "✓ PASS"
+ else do
+ putStrLn "✗ FAIL"
+ putStrLn $ "\nExpected:"
+ putStrLn $ " " ++ show expected
+ return success
+
+main :: IO ()
+main = do
+ putStrLn ""
+ putStrLn $ replicate 80 '='
+ putStrLn " Transition Feature Test Suite"
+ putStrLn $ replicate 80 '='
+
+ results <- sequenceA
+ [ testCase "Test 1 (backwards compatible)"
+ test1
+ "col-span-2 bg-gray-500"
+ , testCase "Test 2 (hover with transition)"
+ test2
+ "bg-gray-500 hover:bg-gray-300 hover:[transition:background-color,border-color,color,fill,stroke_300ms_linear_0ms]"
+ , testCase "Test 3 (builder pattern with chaining)"
+ test3
+ "bg-purple-600 hover:bg-purple-300 hover:[transition:background-color,border-color,color,fill,stroke_300ms_in-out_0ms] focus:bg-indigo-500 focus:[transition:background-color,border-color,color,fill,stroke_500ms_out_100ms]"
+ , testCase "Test 4 (all-at-once style)"
+ test4
+ "bg-purple-600 sm:bg-indigo-500 sm:[transition:background-color,border-color,color,fill,stroke_300ms_in-out_0ms] hover:bg-purple-300 hover:[transition:background-color,border-color,color,fill,stroke_500ms_linear_0ms]"
+ ]
+
+ putStrLn ""
+ putStrLn $ replicate 80 '='
+ putStrLn " SUMMARY"
+ putStrLn $ replicate 80 '='
+ let passed = length $ filter id results
+ total = length results
+ putStrLn $ "Tests passed: " ++ show passed ++ "/" ++ show total
+ putStrLn ""
+
+ if and results
+ then do
+ putStrLn "✓ ALL TESTS PASSED!"
+ putStrLn $ replicate 80 '='
+ else do
+ putStrLn "✗ SOME TESTS FAILED"
+ putStrLn $ replicate 80 '='