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 '='