| name | build |
| description | CurlyFramework full project context — architecture, build pipeline, coding conventions, Tachyons CSS reference, and component patterns. Use for any work in this project. |
| allowed-tools | Read, Edit, Bash, Glob, Grep |
You are working on CurlyFramework, a lightweight frontend framework for accessibility and sustainability, designed to work with CMS solutions like MODX, Kirby, and Statamic. It combines Tachyons (custom fork) utility-first CSS with Alpine.js reactive components. Published to npm as curlyframework.
Build Process
SCSS compilation and JS bundling are handled by CodeKit (macOS app). Additionally, npm scripts are available for SCSS compilation (via Dart Sass) and CSS post-processing (via Lightning CSS). Source files live in /resources, compiled output goes to /public.
| Command | What it does |
|---|
npm run build | Full production build: compile + minify all CSS and JS in one step |
npm run scss | Compile SCSS only (no post-processing) |
npm run compile | Compile SCSS + Lightning CSS without minify (dev use) |
npm run compile:css | Run Lightning CSS only on .src.css files (no sass step) |
npm run lcss | Compile SCSS + Lightning CSS with minification (CSS production build) |
npm run bundle:js | Bundle resources/js/bundle.js with esbuild + minify with Terser (full CodeKit JS fallback) |
npm run minify:js | Minify already-bundled public/js/script.js with Terser only |
npm run purge | Remove unused CSS from style.css based on *.html + JS content |
npm run dev | Watch CSS + JS + browser-sync hot reload (full dev workflow) |
npm run serve | Start browser-sync server only (no watching) |
npx prettier --write . | Format code |
npm install | Install dependencies |
Compiled files in /public/css/ and /public/js/ should be committed — they are the distributed assets.
CSS pipeline details
- Dart Sass compiles SCSS from
resources/css/ → public/css/ with --no-source-map
- Lightning CSS post-processes in place with
--targets 'defaults' --minify
- Watch mode uses
& between commands to run all three watchers in parallel
When adding a new SCSS entry point
- Add the source file in
resources/css/
- Add it to the relevant scripts in
package.json: scss, compile:css, lcss, and dev
- Add it to the Source → Output mapping in this file and in
AGENTS.md
- Register it in
.config.codekit3 if CodeKit is also used
Architecture
Source → Output mapping:
resources/css/style.scss → public/css/style.css (main stylesheet, ~165KB compiled)
resources/css/print.scss → public/css/print.css
resources/css/modx.scss → public/css/modx.css
resources/js/bundle.js → public/js/script.js (Alpine.js + plugins, ~62KB compiled)
SCSS structure in resources/css/:
_vars.scss — CSS custom properties, breakpoints (small: 30em, medium: 48em, large: 60em), responsive scaling
_bundle.scss — imports Tachyons + all custom modules
_a11y.scss — skiplinks, reduced-motion, ARIA patterns
_mixins.scss — reusable SCSS helpers
_typo.scss — typography utilities (--typo-1 through --typo-5)
_form.scss, _animation.scss — component-specific modules
JS (resources/js/bundle.js): Initializes Alpine.js with plugins: @alpinejs/collapse, @alpinejs/focus, @alpinejs/intersect, @alpinejs/persist.
Coding Conventions
- Indentation: 4 spaces for CSS, JS, JSON; 2 spaces for Markdown and YAML
- Quotes: Single quotes in JS/CSS
- Line endings: LF
- CSS naming: kebab-case with BEM patterns (
.component__element--modifier)
- SCSS partials: underscore prefix (
_filename.scss)
- JS: camelCase variables/functions, PascalCase classes
- CSS custom properties: kebab-case (
--animation-time-small)
- Commit messages: Conventional commits —
feat:, fix:, docs:, release:, chore:
Key Constraints
- Accessibility first, native-first: All components require proper ARIA attributes, keyboard navigation, and screen reader compatibility (WCAG 2.2) — but reach for native HTML semantics (
<details>, <dialog>, native form validation) before custom ARIA patterns.
- HTML/CSS first: Solve with semantic HTML and CSS before reaching for JavaScript. Add Alpine.js only when interactivity genuinely requires it.
- Check contrast: Verify color combinations meet WCAG 2.2 AA contrast ratios.
- Avoid overlays/sliders where possible: modals, carousels, and sliders add complexity and accessibility risk for often-marginal UX benefit — use only when the content genuinely needs it.
- Avoid fixed-position elements where possible: they can break or obscure content at high browser zoom levels.
- Sustainability: Minimize bundle size, prefer CSS-only solutions over JS when possible, avoid heavy dependencies.
- New dependencies must align with accessibility and sustainability goals — prefer lightweight, well-maintained packages.
- CSS classes: Only use classes from the compiled stylesheet (
public/css/). Do not use Tailwind, Bootstrap, or any other external CSS framework.
- Templates: Match syntax to the target consumer system — Fenom for MODX Revolution, Antlers for Statamic, native Kirby PHP templates/Kirbytext for Kirby.
Tachyons CSS Reference
Tachyons is a functional/atomic CSS framework providing single-purpose utility classes. Classes are composed directly in HTML — no custom CSS needed for most styling. Mobile-first, ~14kB, optimized for gzip compression.
Naming Pattern
{property}{value} or {property}{direction}{value} — e.g. pa3 = padding-all 1rem, mt2 = margin-top 0.5rem, f4 = font-size 1.25rem.
Responsive Suffixes
| Suffix | Breakpoint |
|---|
| (none) | All (mobile-first base) |
-s | ≥30em |
-m | ≥48em |
-l | ≥60em |
All cumulative (min-width), not exclusive ranges. Example: dn db-m = hidden on mobile, block on ≥48em and up.
Spacing Scale (8px baseline)
| Step | CSS Variable | Value |
|---|
| 0 | var(--spacing-0) | 0 |
| 1 | var(--spacing-1) | 0.25rem (4px) |
| 2 | var(--spacing-2) | 0.5rem (8px) |
| 3 | var(--spacing-3) | 1rem (16px) |
| 4 | var(--spacing-4) | 2rem (32px) |
| 5 | var(--spacing-5) | 4rem (64px) |
| 6 | var(--spacing-6) | 8rem (128px) |
| 7 | var(--spacing-7) | 16rem (256px) |
Padding: pa, pl, pr, pt, pb, pv (vertical), ph (horizontal) + scale number
Margin: ma, ml, mr, mt, mb, mv, mh + scale number
Type Scale
| Class | CSS Variable | :root default |
|---|
.f1 | var(--typo-1) | 0.75rem |
.f2 | var(--typo-2) | 0.875rem |
.f3 | var(--typo-3) | 1rem |
.f4 | var(--typo-4) | 1.25rem |
.f5 | var(--typo-5) | 1.25rem |
.f6 | var(--typo-6) | 1.5rem |
.f7 | var(--typo-7) | 2rem |
.f8 | var(--typo-8) | 3rem |
.f9 | var(--typo-9) | 4rem |
Note: inside .curlyframework, --typo-1–--typo-5 are overridden to 1rem, 1.25rem, 1.5rem, 2rem, 4rem. The scale also extends to --typo-12 (12rem).
Width Scale
Fixed (powers of 2): w1–w5 (1rem–16rem)
Percentage: w-10, w-20, w-25, w-33, w-50, w-75, w-100, w-third, w-two-thirds, w-auto
Color System
Grayscale: black, near-black, dark-gray, mid-gray, gray, silver, light-silver, moon-gray, light-gray, near-white, white
Transparency: black-90 … black-05, white-90 … white-10
Colors: dark-red, red, orange, gold, yellow, purple, dark-pink, hot-pink, pink, dark-green, green, light-green, navy, dark-blue, blue, light-blue, and washed-* variants
- Text color: class name directly (e.g.
blue, dark-gray)
- Background:
bg- prefix (e.g. bg-blue)
- Border color:
b-- prefix (e.g. b--dark-gray)
Common Classes
Display: dn, di, db, dib, flex, inline-flex
Flexbox: flex-column, flex-row, flex-wrap, items-center, items-start, items-end, justify-center, justify-between, justify-around, flex-auto, flex-none
Position: static, relative, absolute, fixed, sticky + coordinates top-0, right-0, bottom-0, left-0
Border radius: br0–br4, br-100 (circle), br-pill
Directional: br--top, br--bottom, br--left, br--right
Borders: Width bw0–bw5 · Style b--solid, b--dashed, b--none · Sides ba, bt, br, bb, bl, bn
Typography: Weight fw1–fw9, b, normal · Alignment tl, tr, tc, tj · Transform ttu, ttl, ttc · Decoration underline, no-underline, strike · Line height lh-solid (1), lh-title (1.25), lh-copy (1.5)
Visibility: Opacity o-0–o-100 · v-hidden, v-visible, clip
Z-index: z-0–z-5, z-999, z-9999, z-max
Hovers: Prefix any color/bg class with hover- (e.g. hover-bg-blue, hover-black)
Figma Export Workflow
When asked to export a Figma design into code:
- Ask first which consumer system it's for (MODX Revolution, Kirby, or Statamic) and which Figma file and frame(s)/page(s) to export.
- Ignore the "Checkliste" frame/page — it's a project checklist, not exportable design content.
- Group frames sharing a name prefix with a
Mobile/Desktop suffix as responsive variants of the same component, not separate components.
- Export Figma variables into
resources/css/_vars.scss as CSS custom properties, matching the existing token structure.
- Export to the correct folder/path for the target system — ask if unclear.
Component Patterns
<ul class="skiplinks">
<li><a href="#main">main content</a></li>
</ul>
<button
@click="open = !open"
:aria-pressed="open"
class="pointer p1 ph4-m pv3-m bg-transparent ba b--inherit br-100 transition-small"
>
<span class="clip">Label</span>
</button>
<button
@click="open = true"
:aria-expanded="open"
aria-controls="popup"
class="btn pointer p0 bg-transparent bn"
>
<span class="bb">open <span aria-hidden="true">⇢</span></span>
</button>
<div x-data="{ open: false }">
<button @click="open = true" :aria-expanded="open" aria-controls="popup" class="btn pointer p0 bg-transparent bn">
<span class="bb">open <span aria-hidden="true">⇢</span></span>
</button>
<div x-cloak x-transition x-show="open" @click="open = false" class="fixed top-0 left-0 right-0 bottom-0 bg-blur"></div>
<div
x-cloak x-transition x-trap.inert.noscroll="open" x-show="open"
@keyup.escape.window="open = false"
id="popup" aria-modal="true" role="dialog"
class="fixed top-0 right-0 bottom-0 z-max w-80 w-third-m p3 p5-m"
>
<button @click="open = false" class="btn pointer p0 bg-transparent bn">
<span class="bb">close <span aria-hidden="true">×</span></span>
</button>
</div>
</div>
<details x-data="{ open: false }" @toggle="open = $el.open" class="w-100 mb1 ba">
<summary class="pointer flex justify-between w-100 p2 bg-transparent bn">
<span>Accordion title</span>
<span :class="{ 'rotate-180': open }" aria-hidden="true" class="transition-small">⇣</span>
</summary>
<p class="p2 m0 bt">Answer text.</p>
</details>
<div x-data="{ tabs: 3, selected: 1 }">
<div @keydown.right.prevent.stop="$focus.wrap().next()" @keydown.left.prevent.stop="$focus.wrap().prev()" role="tablist" class="list flex p0 m0">
<template x-for="i in tabs">
<button @click="selected = i" :id="`tab${i}`" :tabindex="selected == i ? 0 : -1"
:aria-selected="selected == i" :aria-controls="`tabcontent${i}`" role="tab" class="btn btn--clean">
Tab <span x-text="i"></span>
</button>
</template>
</div>
<template x-for="i in tabs">
<section x-show="selected == i" :id="`tabcontent${i}`" :aria-labelledby="`tab${i}`" role="tabpanel" class="p3">
Content <span x-text="i"></span>
</section>
</template>
</div>
<div class="form__item">
<label for="name" class="form__label">
Name
<span aria-hidden="true">*</span>
<span class="clip">(required)</span>
</label>
<div class="form__field form__field--input">
<input type="text" name="name" id="name" required />
</div>
</div>
<div class="form__item">
<div class="form__field form__field--checkbox">
<input type="checkbox" name="accept" id="accept" value="yes" required />
<label for="accept" class="form__label">
<span>I agree. <span aria-hidden="true">*</span><span class="clip">(required)</span></span>
</label>
</div>
</div>
<div x-ref="message" aria-live="assertive" role="status" class="form__message">
<p class="b p2 ba bw2 br2 mb3">Success</p>
</div>
<ul class="dg cols-2 cols-4-m g2 list p0 m0">
<template x-for="item in items">
<li class="ba br2 aspect-ratio-square">
<span class="flex justify-center items-center h-100"></span>
</li>
</template>
</ul>
<div class="richtext">
<h4>Headline</h4>
<p>A <strong>paragraph</strong> with a <a href="#">link</a>.</p>
</div>