---
description: Ply — standalone, zoneless, Tailwind v4 copy-in components
alwaysApply: true
---

# Ply coding rules

You are generating Angular for a project that uses **Ply** at https://ply-ui.com — copy-in components, not an npm UI kit. Not MUI Base UI (React).

## Install, don't invent

- Install with `npx ply-ui-cli add <name> --yes`. Source lands under `aliases.components` in `ply-ui.json` (default `src/app/components/<name>/`). `--yes` skips a folder that already exists; add `--overwrite` to replace it. Humans can run `npx ply-ui-cli add` with no name to pick from the catalog. Never use `add --yes` without a name.
- Import from that local path. Never `from 'base-ui'` / `@base-ui/*`.
- Prefer an existing registry item over a custom control. Check `npx ply-ui-cli list`, https://ply-ui.com/llms.txt, or MCP (`npx -y ply-ui-mcp`) before building a dialog, table, combobox, or form control.
- If components look unstyled or icons are missing, run `npx ply-ui-cli doctor` (read-only). It checks `ply-ui.json`, Angular CDK, Tailwind v4 `@source` paths, `ply-ui.css`, and icon sprites.
- Free items need no license. Pro items need `PLY_LICENSE_KEY` — do not scrape keys or paste Pro source from memory.

## Angular 22 shape

- New components: `standalone: true`, `changeDetection: ChangeDetectionStrategy.OnPush`.
- Public APIs: `input()`, `output()`, `model()` — not `@Input()` / `@Output()`.
- Templates: `@if` / `@for` / `@empty` — not `*ngIf` / `*ngFor`.
- Prefer zoneless. Do not add `zone.js`. After async work, write to signals (or signal-backed CVA) so the view updates without Zone.
- App selectors stay `app-`. Only Ply selectors use the `ply-` prefix.

## Selectors and forms

- Components: `<ply-card>`, `<ply-dialog>` is not a tag — open modals with `inject(DialogService).open(MyComponent)`.
- Directives: `[ply-button]`, `[ply-stroked-button]`, `[ply-input]`, `[ply-textarea]`.
- Native fields: `<input ply-input [(ngModel)]="…">` or Angular 22 `[formField]` + `ply-input`.
- `data-table` cells: project `<ng-template plyTableCell="columnKey" let-row let-value="value">`. Combobox options: `plyComboboxOption`. Custom select: `plySelectOption`. Do not fork those components to render a badge or a row action.
- Chrome strings (empty states, Close, paginator): `provideBaseUiI18n({ close: 'Fermer' })` in `app.config.ts`. Per-instance `emptyMessage` / `emptyText` still override. Not ngx-translate.
- Icon sprites: `provideBaseUI({ iconPath: 'assets/icons.svg' })` in `app.config.ts`. Per-instance `[path]` / `[filledPath]` / `[size]` still win. `w-*`/`h-*` classes keep class-based sizing.
- Tests: use the copied `*.harness.ts` for data-table, custom-select, and dialog. `TestbedHarnessEnvironment.loader(fixture)`; dialogs need `documentRootLoader` (overlay on `document.body`). Do not invent query helpers for those three.
- CVA hosts (`ply-combobox`, `ply-tags-input`, `ply-toggle`, `ply-custom-select`, `ply-file-upload`, `ply-mention-input`, `ply-password-input`) bind `[(ngModel)]` / `[formField]` on the host, not on an inner native input.
- Do not nest composite controls inside `ply-input-group` — only `[ply-input]`, `[ply-textarea]`, and `ply-mention-input` are projected. Use `ply-password-input` for password fields (it already includes a group).
- Confirm destructive actions with `inject(DialogService).confirm({ title, destructive: true })` — do not copy a one-off confirm dialog.

## Tailwind v4

- Style with utilities. Pair light and `dark:` variants (`bg-white dark:bg-slate-800`). Dark mode is the `dark` class on `<html>`.
- Spacing uses the standard scale (`p-4`, `gap-2`, `w-64`). Do not use `[Npx]` for padding, margin, or gap. A fixed control size that is not on the scale (a drawer width) may stay a pixel value.
- `color="primary"` reads `--ply-primary` from `ply-ui.css`. That file aliases `--color-blue-*` to the same scale. Theme Studio sets `--ply-brand` and `--ply-scale-*`. Live preview: https://ply-ui.com/theme/.
- Button and input `class` values merge via `cn()`. `class="rounded-full"` and `class="absolute …"` override defaults. Existing `utility!` still works.

## Directive styles own the element

`ply-button` is not a tag. There is no `<ply-button>` and no template to restyle. The look is an attribute directive on `<button>` or `<a>`, and its host `[class]` binding replaces the element's class list. A parent stylesheet (`button { … }`, `ply-button { }`, `::ng-deep`) does not override it. Do not edit the copied directive for one screen.

Restyle on the directive:

1. `color` and `size` first.
2. Extra utilities go in `class` on that same element when the directive merges `class` with `cn()`: `<button ply-button color="primary" class="w-full">Save</button>`.
3. Button text color is important (`text-white!`, `text-[var(--ply-primary-foreground)]!`). Override it with `class="text-slate-900!"`. A plain `text-slate-900` loses.

`class` is merged on `ply-button`, `ply-stroked-button`, `ply-icon-button`, `ply-stroked-icon-button` (that attribute, not `ply-icon-stroked-button`), `ply-input`, `ply-textarea`, `plyTypography` / `plyHeadingText`, and the `ply-table` parts (`ply-table`, `ply-table-header`, `ply-table-body`, `ply-table-footer`, `ply-table-row`, `ply-table-head`, `ply-table-cell`, `ply-table-caption`). `ply-button` and `ply-stroked-button` also take `width`. Icon buttons do not; their `size` sets `min-w-*` and `max-w-*`, so a width override in `class` has to include those.

`ply-link` and `ply-group-button` replace the class attribute and do not read `class`. Use `color`, `size`, and `width` only. `ply-group-button` also replaces the inner `ply-icon` class.

These add classes and keep yours, but their utilities still paint the control: `ply-dropdown-menu-item`, `ply-context-menu-item`, `ply-navigation-menu-link`, `ply-list-item`, `ply-addon-start`, `ply-addon-end`, `[code]`, `[container]`, `[badge-icon]`. Field chrome (border, radius, focus ring) is on `ply-input-group`, not on `[ply-input]`.
- `<ply-icon>` outlines: `stroke-*`. Filled icons: `[filled]="true"` and `text-*` / `fill-*`.

## Writing direction (RTL)

- Set `<html lang="ar" dir="rtl">`. Switch at runtime with `inject(DirectionRegistry).setDocumentDirection('rtl')`. A section can use `plyDir="rtl"` or `plyDir="auto"` (Arabic, Hebrew, Persian, Urdu, and the other RTL locales). Guide: https://ply-ui.com/configuration/#rtl
- Install: `npx ply-ui-cli add direction --yes`. Tabs, drawer, icon, shell, and the other components that follow direction already depend on it.
- Imports: `PlyDirDirective` from `./components/direction/ply-dir.directive`, `DirectionRegistry` from `./components/direction/direction.service`.
- New layout uses logical utilities: `ms-*` / `me-*`, `ps-*` / `pe-*`, `start-*` / `end-*`, `text-start` / `text-end`, `border-s` / `border-e`, `rounded-s` / `rounded-e`. Leave `left-1/2` + `-translate-x-1/2` centering alone.
- `left` and `right` stay on those physical edges. `start` and `end` follow `dir`. Drawer default is `end` (physical right in LTR). Toast default is `top-end`. Scroll-to-top and speed dial default to `bottom-end`. The shell `left` slot is the reading-start edge.
- ArrowRight moves to the next item in LTR and the previous item in RTL. Trees and submenus open on the forward arrow (ArrowRight in LTR, ArrowLeft in RTL).
- `ply-icon` names that contain `left` or `right` mirror under `dir="rtl"` when `mirror` is `auto` (the default). `mirror="off"` keeps a physical arrow. Charts, knobs, and rich-text align left/center/right stay physical.

## Async, overlays, SSR

- `DialogService.open()` is a no-op during SSR. `DialogService.confirm()` emits `false` on the server. Toast and dialog hosts append via `DOCUMENT`.
- Drawers need `provideAnimationsAsync()`.
- Close popovers/drawers yourself on in-panel navigation (`popover.close()`, `drawer.closed.emit()`).
- Overlay menus (dropdown, context) use `a11y-keyboard` (roving tabindex, Home/End, typeahead). Combobox, custom-select, and command-palette use `aria-activedescendant` — do not switch those to the menu helper.
- Do not put `ply-toast-container` in templates; the toast service owns the overlay.

## Agents

- MCP optional: `.cursor/mcp.json` → `npx -y ply-ui-mcp` with `PLY_CWD` = this app root.
- Facts: https://ply-ui.com/llms.txt · catalog: https://ply-ui.com/docs/ai/components.md
- UI design skill: https://ply-ui.com/docs/ai/skills/ply-ui-design/SKILL.md — copy to `.cursor/skills/ply-ui-design/` (`init` writes it when missing). Walkthrough: https://ply-ui.com/learn/ply-ui-design-skill/.
- Usage snippets (VS Code / Cursor): `.vscode/ply-ui.code-snippets` — expand prefixes like `ply-card` after the CLI has copied the component in.
