diff --git a/README.md b/README.md index 0ae69bff..9d2a6ff3 100644 --- a/README.md +++ b/README.md @@ -1 +1,79 @@ -druid theme, generated from starterkit_theme. Additional information on generating themes can be found in the [Starterkit documentation](https://www.drupal.org/docs/core-modules-and-themes/core-themes/starterkit-theme). \ No newline at end of file +# Druid Theme for Drupal 11 + +## What this is + +`druid` is a Drupal 11 custom theme generated from core's `starterkit_theme` (generator: `starterkit_theme:11.3.7`). It declares `stable9` as its base theme in `druid.info.yml`, so any template not overridden here falls back to `stable9`'s version. The theme lives at `web/themes/custom/druid` inside a larger Drupal site (sibling theme `orion` lives next to it; site-level `composer.json` is at the repo root). + +There is no build pipeline, no test suite, no linter, and no preprocessor in this directory. CSS is hand-authored modern CSS (custom properties, cascade layers, nesting, container queries, `:where()`, `color-mix()`, etc.) and consumed directly by Drupal. "Building" the theme means clearing Drupal's cache so it picks up `.yml` and template changes — CSS _content_ edits need no rebuild. See `todo.md` for the design rationale that informed this stack choice (no Sass, no PostCSS, no Pico). + +## CSS architecture: cascade layers + +This is the central architectural decision and everything below it depends on understanding it. + +**Layer order**, declared once in `css/layers.css` and authoritative for the whole theme: + +```css +@layer reset, tokens, base, layout, components, utilities; +``` + +Later layers win against earlier ones regardless of selector specificity. Each layer is filled by exactly one file (except `components`, which is split per UI element), and each file is registered as its own library so loading is deterministic: + +| Layer | File(s) | Library | SMACSS cat. | Weight | +| ------------ | --------------------------------- | ------------------ | ----------- | -------- | +| (order decl) | `css/layers.css` | `druid/layers` | `base` | -100 | +| reset | `css/lib/modern-normalize.css` | `druid/reset` | `base` | -90 | +| (font-face) | `css/fonts.css` | `druid/fonts` | `base` | -85 | +| tokens | `css/tokens.css` | `druid/tokens` | `base` | -80 | +| base | `css/base.css` | `druid/base` | `base` | -70 | +| layout | `css/layout.css` | `druid/layout` | `layout` | -60 | +| components | `css/components/*.css` (32 files) | `druid/components` | `component` | -10/file | +| utilities | `css/utilities.css` | `druid/utilities` | `theme` | -50 | + +All eight libraries are attached on every page via `druid.info.yml` `libraries:` in cascade order. **Do not remove or reorder that list** — the layer-order declaration in `layers.css` must parse before any `@layer foo { … }` block, or the order becomes whatever Drupal happens to load first. + +`css/fonts.css` is the one architecture file that intentionally lives _outside_ any `@layer` block. `@font-face` declarations register font resources rather than cascade rules — they don't compete in the cascade, so layer membership is irrelevant. The font files themselves live in `fonts//` (e.g. `fonts/adelle/`, `fonts/open-sans/`). + +**Critical footgun: unlayered CSS beats all layered CSS.** Any rule outside a `@layer { … }` block silently wins against everything inside layers — this is per the CSS spec, not a bug. Every CSS file in this theme is wrapped; any new rule you author must go _inside_ the appropriate `@layer NAME { … }` block, never alongside it. + +**`!important` inverts layer order.** An `!important` declaration in an _earlier_ layer beats `!important` in a later layer. Avoid `!important` inside `@layer utilities` — the layer is already designed to win. + +### Where to add a new CSS rule + +| Kind of rule | Goes in | +| --------------------------------------------------------------------- | --------------------------------------------------- | +| A `--custom-property` definition (color, size, font, radius, etc.) | `css/tokens.css` | +| A new `@font-face` / web font | `css/fonts.css` (+ woff2 file in `fonts//`) | +| Bare element selector (`body`, `a`, `h1`, `ul`, …) | `css/base.css` | +| Region/grid/container arrangement (`.layout-container`, page shell) | `css/layout.css` | +| A discrete UI widget (button, card, menu, form control, Drupal block) | `css/components/.css` | +| Single-purpose helper (`.text-center`, `.mt-4`, print toggles) | `css/utilities.css` | +| A browser default override | `css/lib/modern-normalize.css`\* | + +\*Only when re-vendoring a new modern-normalize release — see the file header for the workflow. + +## JavaScript + +There's one JS file: `js/druid.behaviors.js`, registered as the `druid/behaviors` library with `core/drupal` + `core/once` as dependencies, attached on every page. It's currently empty inside its IIFE — a scaffold ready for the first behavior. + +All theme JS should use Drupal's behaviors system rather than plain `DOMContentLoaded` listeners, because Drupal swaps DOM fragments at runtime (AJAX views, form errors, contextual links, modals) and `attach()` is what re-runs on each injected fragment. Use `once()` to prevent double-initialization. The file's own header comment is the detailed pattern reference; if you split JS into multiple files, either co-locate them in the `behaviors:` library entry or register new libraries for conditional loading via `libraries-extend`. + +### Tokens / Open Props + +`css/tokens.css` holds CSS custom properties on `:root`. We do **not** import the full Open Props library — instead, we cherry-pick individual tokens by copying their declarations into this file. The workflow is documented in the file's header comment; re-read it before adding tokens. The short version: browse , copy the `--prop: value;` line, paste into the relevant section, preserve the Open Props name. + +## Other layout that matters + +- `druid.info.yml` — declares base theme, always-on libraries (one per cascade layer), and `libraries-extend` entries that attach extra component CSS only when the corresponding core library is in use (e.g. `core/drupal.dialog` → `druid/dialog`). The `libraries-extend` libraries (`dialog`, `dropbutton`, `file`, `item-list`, `progress`, `drupal.tablesort`, `user`) are separate from the always-on `components` library because they target widgets that don't appear on every page. +- `druid.libraries.yml` — every CSS library. New component CSS files added to `css/components/` need a corresponding entry under the `components:` library (or their own library if they should be conditional via `libraries-extend`). +- `druid.theme` — `hook_preprocess_HOOK` functions. Currently only `druid_preprocess_image_widget()`, which strips the preview container when the user lacks access. +- `templates/` — Twig overrides organized by Drupal's own taxonomy (`block/`, `content/`, `content-edit/`, `dataset/`, `field/`, `form/`, `layout/`, `misc/`, `navigation/`, `user/`, `views/`). Filenames follow Drupal's theme-hook-suggestion convention (`field--node--title.html.twig`, `block--system-menu-block.html.twig`, etc.) — renaming them breaks the override. + +## Working in this theme + +- **Cache clear**: required after any `.yml`, `.theme`, or template change — `drush cr` from the site root. CSS _content_ edits do not require it; only adding/removing files or library entries does. +- **New component CSS**: create the file in `css/components/`, wrap its rules in `@layer components { … }`, register it under the `components:` library in `druid.libraries.yml` with `weight: -10`. If the styles should only load when a specific core library is in use, give it its own library and wire it via `libraries-extend` in `druid.info.yml` instead. +- **New JS behavior**: add it inside `js/druid.behaviors.js` following the `Drupal.behaviors.druidX = { attach(context) { once('druid-x', '.selector', context).forEach(…) } }` pattern. Always scope selectors to `context`, not `document`. For a separate file, register it in the `behaviors:` library (same dependencies) or define a new library if it should load conditionally. +- **New Twig override**: copy the upstream template from `core/themes/stable9/templates/…` (or the originating module) into the matching subdirectory here — don't write from scratch; the docblock comments document available variables and are worth keeping. +- **New preprocess hook**: follow the existing signature in `druid.theme` (`array &$variables`, `: void` return type, `/** Implements hook_X(). */` docblock). +- **Drupal core / contrib CSS is unlayered.** That means it wins against any `@layer components` rule you write — this is the one place the architecture leaks. Options: bump specificity for the override, or wrap the offending core CSS via a future `hook_css_alter` (not currently implemented). Don't reach for `!important` reflexively. +- The repo is generated from a starter kit; upstream Drupal docs at are the canonical reference for regeneration and theme-API conventions.