|
1 | 1 | # Development Rules |
2 | 2 |
|
3 | | -## Tailwind CSS v4 |
| 3 | +This is a **Kirby 5 CMS** project. Individual pages available as markdown by appending `.md` to any URL. |
| 4 | + |
| 5 | +Docs: https://getkirby.com/llms.txt |
| 6 | + |
| 7 | +## Existing Project Patterns |
| 8 | + |
| 9 | +Before implementing anything, ALWAYS check what already exists in the project: |
| 10 | + |
| 11 | +- **`site/plugins/`** — Check for installed plugins that already solve the problem. Also check `composer.json` for dependencies. |
| 12 | +- **`site/snippets/`** — Use existing snippets. NEVER use raw `<img>` tags — use the `image` snippet for all images. It handles responsive images, lazy loading, and SVG inlining. |
| 13 | +- **Existing blocks/templates** — Look at similar blocks for patterns before writing new ones. |
| 14 | + |
| 15 | +When told to "use X", check if X is an existing package/plugin in the project before doing anything else. |
| 16 | + |
| 17 | +## HTML / Markup |
| 18 | + |
| 19 | +### Cards |
| 20 | + |
| 21 | +When creating cards that have a link, wrap the anchor tag only around the card's title, then expand it to cover the entire card using absolutely positioned pseudo-elements: |
| 22 | + |
| 23 | +```html |
| 24 | +<div class="relative"> |
| 25 | + <a href="/page" class="after:absolute after:inset-0"> Card Title </a> |
| 26 | + <p class="relative z-10">Long-form text that can still be selected and copied.</p> |
| 27 | +</div> |
| 28 | +``` |
| 29 | + |
| 30 | +This ensures screen readers only announce the link once. Use `relative z-10` on elements that should remain interactive above the link overlay. |
| 31 | + |
| 32 | +### Strings / i18n |
| 33 | + |
| 34 | +ALWAYS use the `t()` function for hardcoded labels and strings. Pass the English translation as the argument, it serves as both the key and default value: |
| 35 | + |
| 36 | +```php |
| 37 | +<?= t('Read more') ?> |
| 38 | +<?= tt('Open submenu: {title}', ['title' => $title]) ?> |
| 39 | +<?= tc('{{ count }} members', $count) ?> |
| 40 | +``` |
| 41 | + |
| 42 | +After adding new strings, run `kirby trawl:extract` to extract them into the JSON translation files. Let the user translate. DO NOT manually edit translation files. |
| 43 | + |
| 44 | +In blueprints, most properties (`label`, `help`, `placeholder`, `before`, `after`, section `headline`/`text`) auto-resolve plain strings as translation keys — no special syntax needed. |
| 45 | + |
| 46 | +Option `text` in select/radio/checkbox fields does NOT auto-resolve. Use `*:` to reference a translation key: |
| 47 | + |
| 48 | +```yaml |
| 49 | +fields: |
| 50 | + category: |
| 51 | + type: select |
| 52 | + options: |
| 53 | + summer: |
| 54 | + "*": Summer |
| 55 | + autumn: |
| 56 | + "*": Fall |
| 57 | +``` |
| 58 | +
|
| 59 | +## Styling (Tailwind CSS v4) |
| 60 | +
|
| 61 | +**IMPORTANT:** This project resets ALL default Tailwind theme values (colors, font sizes, radii, shadows, etc.) using `--*: initial` in `src/styles/index.css` and only defines project-specific values. ALWAYS check `src/styles/index.css` for available theme tokens before using any Tailwind utility. For example, `text-red-500` or `rounded-lg` will NOT work unless explicitly defined in the theme. |
| 62 | + |
| 63 | +### Custom classes |
| 64 | + |
| 65 | +Custom classes are valid in two cases: |
| 66 | + |
| 67 | +1. **Single HTML elements** that need consistent styling (e.g. `.button`) — because creating a snippet for one element is overkill |
| 68 | +2. **Nested/prose content** where you need to style child elements you don't control (e.g. `.prose` targeting `p`, `h2`, `li` inside rich text) |
| 69 | + |
| 70 | +If a component combines markup with styling (e.g. a badge with text + icon), create a **Kirby snippet** instead of a custom class, the rendering logic belongs with the styles. |
| 71 | + |
| 72 | +### Focus Styles |
| 73 | + |
| 74 | +ALWAYS add custom focus styles. Silence browser defaults with `outline-none` and add custom styles with `focus-visible:`: |
| 75 | + |
| 76 | +- Prefer `ring` utilities for interactive elements |
| 77 | +- Use `underline` for links unless specified otherwise |
| 78 | +- Use `hocus:` (custom variant) to apply styles on both hover and focus |
4 | 79 |
|
5 | 80 | ### Desktop-first |
6 | 81 |
|
@@ -54,37 +129,44 @@ All support conditional syntax with arrays: |
54 | 129 |
|
55 | 130 | Define components with a default export in `src/components/`. These will be automatically registered and can be used in the frontend with `x-data`. |
56 | 131 |
|
57 | | -NEVER use document.querySelector() or similar document-level methods. ALWAYS scope to the component's elements, e.g. using `this.$root.querySelector()`. Generally, prefer Alpine's built-in methods like `x-on`, `x-show`, `x-transition`, to trigger methods, etc. Avoid global event listeners. |
| 132 | +NEVER use document.querySelector() or similar document-level methods. ALWAYS scope to the component's elements, e.g. using `this.$root.querySelector()`. Generally, prefer Alpine's built-in methods like `x-on`, `x-show`, `x-transition`, to trigger methods, etc. Avoid global event listeners. Use Alpine.js plugins like Focus, Intersect, Resize, etc. when required. |
58 | 133 |
|
59 | | -## PHP |
| 134 | +Documentation: https://alpinejs.dev |
60 | 135 |
|
61 | | -### Code Style |
| 136 | +## Icons |
62 | 137 |
|
63 | | -AVOID setting preliminary variables in files. Prefer inline expressions instead UNLESS you'd need to repeat a statement. |
64 | | - |
65 | | -BAD: unnecessary variable: |
| 138 | +SVG icons live in `assets/icons/` and are compiled into a sprite by Vite. Use the `icon()` helper to render them. Never inline SVGs or use `<img>` tags for icons. |
66 | 139 |
|
67 | 140 | ```php |
68 | | -<?php |
69 | | -$image = $block->image()->toFile(); |
70 | | -snippet('picture', ['image' => $image]); |
71 | | -?> |
| 141 | +<?= icon('angle-right', class: 'size-4') ?> |
72 | 142 | ``` |
73 | 143 |
|
| 144 | +Icons use `currentcolor` and are `aria-hidden="true"` by default. Size them with Tailwind's `size-*` utilities. |
| 145 | + |
| 146 | +## PHP |
| 147 | + |
| 148 | +### Kirby Fields |
| 149 | + |
| 150 | +Fields store raw strings and need casting (`->toPage()`, `->toFiles()`, `->toBool()`, etc.). For output, use `->esc()` on text fields and `->permalinksToUrls()` on writer fields. |
| 151 | + |
| 152 | +### Code Style |
| 153 | + |
| 154 | +AVOID setting preliminary variables in files. Prefer inline expressions instead UNLESS you'd need to repeat a statement. |
| 155 | + |
74 | 156 | GOOD: inline when used once: |
75 | 157 |
|
76 | 158 | ```php |
77 | | -<?php snippet('picture', ['image' => $block->image()->toFile()]) ?> |
| 159 | +<?php snippet('image', ['image' => $block->image()->toFile()]) ?> |
78 | 160 | ``` |
79 | 161 |
|
80 | 162 | GOOD: assign in condition when reused: |
81 | 163 |
|
82 | 164 | ```php |
83 | 165 | <?php if ($isLtr = $block->order()->value() === 'ltr') : ?> |
84 | | - <?php snippet('picture', ['image' => $block->image()->toFile()]) ?> |
| 166 | + <?php snippet('image', ['image' => $block->image()->toFile()]) ?> |
85 | 167 | <?php endif ?> |
86 | 168 |
|
87 | 169 | <?php if (!$isLtr) : ?> |
88 | | - <?php snippet('picture', ['image' => $block->image()->toFile()]) ?> |
| 170 | + <?php snippet('image', ['image' => $block->image()->toFile()]) ?> |
89 | 171 | <?php endif ?> |
90 | 172 | ``` |
0 commit comments