|
| 1 | +--- |
| 2 | +name: gts-component-conventions |
| 3 | +description: Styling and authoring conventions for `.gts` components and their CSS in the host and boxel-ui packages, plus the content-tag `<template>`-detection hazards that silently break `.gts` parsing. Use whenever writing, reviewing, or refactoring a `.gts` component, a `<style scoped>` block, or a glimmer template — especially component-styling review passes ("apply these design-review notes", "clean up this component's CSS") and when a `.gts` file mysteriously fails to type-check or lints with phantom unused-import errors. Triggers on editing component markup/CSS, adding SVG icons, writing conditional class names, choosing colors or units, and any cascading "Cannot find name 'template'" or template-was-dropped symptom. |
| 4 | +--- |
| 5 | + |
| 6 | +# `.gts` Component Conventions |
| 7 | + |
| 8 | +Two things live here: |
| 9 | + |
| 10 | +1. **Design-review guidelines** (from Burcu) for component markup and CSS — apply when writing or reviewing any `.gts` component or `<style scoped>` block. |
| 11 | +2. **content-tag `<template>` hazards** — the parser gotchas that silently drop or mangle a template. Read part 2 the moment a `.gts` file fails to type-check or lints with phantom errors after an edit. |
| 12 | + |
| 13 | +Root font-size is the browser default **16px**, so `1rem === 16px`. There is no `html { font-size }` override. |
| 14 | + |
| 15 | +--- |
| 16 | + |
| 17 | +## Part 1 — Component & CSS guidelines |
| 18 | + |
| 19 | +### 1. Target DOM elements with `data-*` attributes, not class names |
| 20 | + |
| 21 | +Class names are a styling concern; JS that reaches into the DOM (`closest`, `querySelector`, `matches`, `hasAttribute`) should key off a `data-*` attribute so refactoring CSS classes never breaks behavior. Keep the class for the `<style>` selector **and** add a `data-*` marker for the JS hook. |
| 22 | + |
| 23 | +```ts |
| 24 | +// Wrong — JS coupled to a styling class |
| 25 | +let marker = el.closest('.adorn-context'); |
| 26 | + |
| 27 | +// Right — class stays for CSS; data attribute is the JS contract |
| 28 | +let marker = el.closest('[data-adorn-context]'); |
| 29 | +``` |
| 30 | + |
| 31 | +```hbs |
| 32 | +<div class='adorn-context' data-adorn-context ...attributes> |
| 33 | +``` |
| 34 | + |
| 35 | +`data-test-*` attributes (used by the test suite) already follow this — extend the same habit to runtime DOM lookups. |
| 36 | + |
| 37 | +### 2. Use scalable units (`rem`) instead of `px` |
| 38 | + |
| 39 | +Convert dimensional CSS values — `width`/`height`, `padding`, `margin`, `gap`, `border-radius`, `font-size`, `box-shadow` spreads, `clip-path` offsets, positioning insets — to `rem` (divide px by 16). Prefer the existing `--boxel-sp-*`, `--boxel-font-*`, and radius tokens when one fits. |
| 40 | + |
| 41 | +```css |
| 42 | +gap: 0.3125rem; /* was 5px */ |
| 43 | +padding: 0.1875rem 0.4375rem; /* was 3px 7px */ |
| 44 | +font-size: 0.625rem; /* was 10px */ |
| 45 | +box-shadow: 0 0 0 0.125rem var(--c); /* was 2px */ |
| 46 | +``` |
| 47 | + |
| 48 | +**Leave as-is:** SVG-internal coordinates (`viewBox`, `cx`, `r`, `stroke-width`, path `d`), and JS pixel math against `getBoundingClientRect()` — those aren't CSS layout units. Hairline values like `letter-spacing: 0.5px` are fine to leave (converting gains nothing). |
| 49 | + |
| 50 | +### 3. Save hardcoded colors as CSS variables |
| 51 | + |
| 52 | +Never ship a raw hex/rgb in a component. If a color recurs across components, promote it to a shared token in `packages/boxel-ui/addon/src/styles/variables.css`; if it's truly local, define a component-scoped custom property. Falling back to another variable is fine (`var(--token, var(--other))`); a hardcoded literal fallback is not. |
| 53 | + |
| 54 | +Reuse an existing semantic token before inventing a new one, and **name tokens by role, not by hue**. A color named for its appearance (`--boxel-teal-ink`, `--boxel-dark-teal`) is a palette primitive; a color named for its job (`--boxel-highlight`, `--boxel-highlight-hover`) is what components should reference. |
| 55 | + |
| 56 | +For "readable text/icons on a colored surface," the codebase uses the pervasive **`<surface>` / `<surface>-foreground` pairing** (shadcn/Tailwind-style): `--foreground`, `--muted-foreground`, `--primary`/`--primary-foreground`, `--accent-foreground`, `--card-foreground`, plus component-local `--boxel-*-foreground`. The idiom is `color: var(--primary-foreground, var(--boxel-dark))`. |
| 57 | + |
| 58 | +The adorn refactor therefore landed on existing/semantic tokens rather than hue-named ones: |
| 59 | + |
| 60 | +- darker teal for hover/selected → `--boxel-highlight-hover` (resolves through `--boxel-dark-teal: #00da9f`). Don't add a parallel "teal-hover" variable. |
| 61 | +- dark foreground on a highlight surface → `--boxel-highlight-foreground: #0a2e1c` (the companion to `--boxel-highlight` / `--boxel-highlight-hover`, following the `-foreground` convention). |
| 62 | + |
| 63 | +```css |
| 64 | +/* role-named, not hue-named or hardcoded */ |
| 65 | +color: var(--boxel-highlight-foreground); /* was #0a2e1c */ |
| 66 | +background-color: var(--boxel-highlight-hover); /* was #00da9f */ |
| 67 | +``` |
| 68 | + |
| 69 | +### 4. Use the `cn` helper for conditional class names |
| 70 | + |
| 71 | +Don't hand-concatenate classes with inline `{{if}}`/`{{unless}}` inside a class string. Use `cn` from `@cardstack/boxel-ui/helpers` — positional base classes, named boolean classes. |
| 72 | + |
| 73 | +```hbs |
| 74 | +{{! Wrong }} |
| 75 | +<div class='adorn-label {{if @compact "compact"}} {{unless (has-block "dropdown") "no-menu"}}'> |
| 76 | +
|
| 77 | +{{! Right }} |
| 78 | +<div class={{cn 'adorn-label' compact=@compact no-menu=(unless (has-block 'dropdown') true)}}> |
| 79 | +``` |
| 80 | + |
| 81 | +`cn` emits the same space-separated string, so this is behavior-preserving — existing `data-test`/class selectors keep matching. |
| 82 | + |
| 83 | +### 5. SVGs: keep them separate, stroke/fill with `currentColor` |
| 84 | + |
| 85 | +- Factor SVG artwork into dedicated icon **components** (the repo convention — e.g. `selection-checkmark-icon.gts`) or `@cardstack/boxel-icons` / `@cardstack/boxel-ui/icons`, rather than duplicating raw `<svg>` markup. Ship compressed/optimized SVG. |
| 86 | +- Make the themeable parts `stroke='currentColor'` / `fill='currentColor'` so a parent can color the icon via `color:`. Parts that are intentionally a fixed brand color (e.g. a dark circle behind a themeable check) reference a token instead of a literal. |
| 87 | + |
| 88 | +```hbs |
| 89 | +{{! themeable check follows the parent's color }} |
| 90 | +<path d='…' stroke='currentColor' /> |
| 91 | +{{! fixed dark disc → token, not a hex literal }} |
| 92 | +<circle cx='7' cy='7' r='7' fill='var(--boxel-highlight-foreground)' /> |
| 93 | +``` |
| 94 | + |
| 95 | +### 6. Use `--boxel-highlight`, not `--boxel-teal`, for the default highlight |
| 96 | + |
| 97 | +`--boxel-highlight` resolves to `--boxel-teal` today, but it's the app-wide semantic token for the highlight accent. Referencing it keeps highlight color consistent and re-themeable across the app. (Same idea for `--boxel-highlight-hover`.) |
| 98 | + |
| 99 | +```css |
| 100 | +/* prefer the semantic token, not the raw palette color */ |
| 101 | +--adorn-accent-light: var(--boxel-highlight); /* not var(--boxel-teal) */ |
| 102 | +background-color: var(--boxel-highlight); /* not var(--boxel-teal) */ |
| 103 | +``` |
| 104 | + |
| 105 | +--- |
| 106 | + |
| 107 | +## Part 2 — content-tag `<template>` hazards |
| 108 | + |
| 109 | +`content-tag` (the preprocessor glint and `ember-eslint-parser` use to parse `.gts`) has JavaScript-lexer bugs that make it lose track of `<template>` tags. When that happens the template is silently dropped or misparsed — and the symptom is **not** where the bad character is. AGENTS.md (§ "`.gts` file gotcha") is the canonical list; the known triggers: |
| 110 | + |
| 111 | +**1. Backticks inside a regex literal** — mistaken for template-literal delimiters. |
| 112 | + |
| 113 | +```ts |
| 114 | +.replace(/`([^`]+)`/g, '$1') // BROKEN |
| 115 | +const INLINE_CODE_RE = new RegExp('`([^`]+)`', 'g'); // FIX |
| 116 | +``` |
| 117 | + |
| 118 | +**2. `!/regex/` (negation before a regex literal)** — the `/` after `!` is misread. |
| 119 | + |
| 120 | +```ts |
| 121 | +lines.some((line) => !/^\s*#/.test(line)); // BROKEN |
| 122 | +const HEADING_RE = /^\s*#/; // FIX — extract to a const |
| 123 | +lines.some((line) => !HEADING_RE.test(line)); |
| 124 | +``` |
| 125 | + |
| 126 | +**3. A backtick-wrapped bracket token inside the _template body_** — e.g. a `<style>` CSS comment. |
| 127 | + |
| 128 | +```hbs |
| 129 | +<template> |
| 130 | + <style scoped> |
| 131 | + /* BROKEN: backtick-wrapped `[data-adorn-context]` here drops the template */ |
| 132 | + /* FIX: drop the backticks or the brackets in template-region comments */ |
| 133 | + </style> |
| 134 | +</template> |
| 135 | +``` |
| 136 | + |
| 137 | +The identical text in a `//` comment _outside_ `<template>…</template>` is harmless — content-tag only runs its template-mode lexer between the tags. So keep backtick-wrapped selectors like `` `[data-foo]` `` out of comments inside the template; describe the marker in a regular JS comment above the component instead. |
| 138 | + |
| 139 | +### Recognizing it |
| 140 | + |
| 141 | +- **Outside-template triggers (1 & 2):** cascading TypeScript errors beginning with `Cannot find name 'template'` at the first `<template>` in the file. |
| 142 | +- **Inside-template trigger (3):** the template body is silently dropped, so **type-check may still pass** while ESLint reports phantom `no-unused-vars` on imports/consts that the template references (e.g. `hash`, a yielded const). **ESLint is the canary** here — if a refactor that only touched a `<template>`/`<style>` block suddenly makes prior imports "unused," suspect a swallowed template before you touch the imports. |
| 143 | + |
| 144 | +### Bisecting |
| 145 | + |
| 146 | +Revert to the last-good file and re-apply changes one hunk at a time, running `npx eslint <file>` between each, until the phantom errors reappear. The offending hunk is almost always a comment or string edit inside the `<template>` block, not a code change. |
0 commit comments