@frame-kit/ui-ng 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPONENTS.md +683 -0
- package/DEVELOPMENT_GUIDE.md +1102 -0
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/THEMING.md +130 -0
- package/core/headline/README.md +121 -0
- package/core/icon/README.md +173 -0
- package/core/image/README.md +210 -0
- package/core/link/README.md +297 -0
- package/core/separator/README.md +145 -0
- package/core/text/README.md +240 -0
- package/directives/infinite-scroll/README.md +102 -0
- package/directives/spotlight/README.md +154 -0
- package/directives/tooltip/README.md +147 -0
- package/docs/endpoint-link/README.md +142 -0
- package/docs/method-badge/README.md +154 -0
- package/fesm2022/frame-kit-ui-ng-core-headline.mjs +122 -0
- package/fesm2022/frame-kit-ui-ng-core-headline.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-core-icon.mjs +189 -0
- package/fesm2022/frame-kit-ui-ng-core-icon.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-core-image.mjs +123 -0
- package/fesm2022/frame-kit-ui-ng-core-image.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-core-link.mjs +369 -0
- package/fesm2022/frame-kit-ui-ng-core-link.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-core-separator.mjs +59 -0
- package/fesm2022/frame-kit-ui-ng-core-separator.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-core-text.mjs +204 -0
- package/fesm2022/frame-kit-ui-ng-core-text.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-directives-infinite-scroll.mjs +74 -0
- package/fesm2022/frame-kit-ui-ng-directives-infinite-scroll.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-directives-spotlight.mjs +76 -0
- package/fesm2022/frame-kit-ui-ng-directives-spotlight.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-directives-tooltip.mjs +425 -0
- package/fesm2022/frame-kit-ui-ng-directives-tooltip.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-docs-endpoint-link.mjs +63 -0
- package/fesm2022/frame-kit-ui-ng-docs-endpoint-link.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-docs-method-badge.mjs +43 -0
- package/fesm2022/frame-kit-ui-ng-docs-method-badge.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-forms.mjs +3632 -0
- package/fesm2022/frame-kit-ui-ng-forms.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-layouts-app-shell.mjs +239 -0
- package/fesm2022/frame-kit-ui-ng-layouts-app-shell.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-layouts-content-split.mjs +132 -0
- package/fesm2022/frame-kit-ui-ng-layouts-content-split.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-services-overlay-orchestrator.mjs +133 -0
- package/fesm2022/frame-kit-ui-ng-services-overlay-orchestrator.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-services-spotlight.mjs +60 -0
- package/fesm2022/frame-kit-ui-ng-services-spotlight.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-services-toast.mjs +166 -0
- package/fesm2022/frame-kit-ui-ng-services-toast.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-accordion.mjs +214 -0
- package/fesm2022/frame-kit-ui-ng-ui-accordion.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-alert.mjs +82 -0
- package/fesm2022/frame-kit-ui-ng-ui-alert.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-avatar-stack.mjs +76 -0
- package/fesm2022/frame-kit-ui-ng-ui-avatar-stack.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-avatar.mjs +81 -0
- package/fesm2022/frame-kit-ui-ng-ui-avatar.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-badge.mjs +81 -0
- package/fesm2022/frame-kit-ui-ng-ui-badge.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-breadcrumb.mjs +68 -0
- package/fesm2022/frame-kit-ui-ng-ui-breadcrumb.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-button.mjs +108 -0
- package/fesm2022/frame-kit-ui-ng-ui-button.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-callout.mjs +58 -0
- package/fesm2022/frame-kit-ui-ng-ui-callout.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-card.mjs +70 -0
- package/fesm2022/frame-kit-ui-ng-ui-card.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-copyable-field.mjs +113 -0
- package/fesm2022/frame-kit-ui-ng-ui-copyable-field.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-data-table.mjs +1288 -0
- package/fesm2022/frame-kit-ui-ng-ui-data-table.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-dialog.mjs +456 -0
- package/fesm2022/frame-kit-ui-ng-ui-dialog.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-drawer.mjs +398 -0
- package/fesm2022/frame-kit-ui-ng-ui-drawer.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-dropdown-menu.mjs +398 -0
- package/fesm2022/frame-kit-ui-ng-ui-dropdown-menu.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-editable-field.mjs +125 -0
- package/fesm2022/frame-kit-ui-ng-ui-editable-field.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-icon-badge.mjs +113 -0
- package/fesm2022/frame-kit-ui-ng-ui-icon-badge.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-icon-list.mjs +111 -0
- package/fesm2022/frame-kit-ui-ng-ui-icon-list.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-inline-edit.mjs +103 -0
- package/fesm2022/frame-kit-ui-ng-ui-inline-edit.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-list-editor.mjs +135 -0
- package/fesm2022/frame-kit-ui-ng-ui-list-editor.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-loader.mjs +81 -0
- package/fesm2022/frame-kit-ui-ng-ui-loader.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-menu-item.mjs +79 -0
- package/fesm2022/frame-kit-ui-ng-ui-menu-item.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-nav-brand.mjs +40 -0
- package/fesm2022/frame-kit-ui-ng-ui-nav-brand.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-nav-group.mjs +110 -0
- package/fesm2022/frame-kit-ui-ng-ui-nav-group.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-nav-separator.mjs +91 -0
- package/fesm2022/frame-kit-ui-ng-ui-nav-separator.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-node-tree-breadcrumb.mjs +86 -0
- package/fesm2022/frame-kit-ui-ng-ui-node-tree-breadcrumb.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-node-tree.mjs +443 -0
- package/fesm2022/frame-kit-ui-ng-ui-node-tree.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-note.mjs +56 -0
- package/fesm2022/frame-kit-ui-ng-ui-note.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-numbered-list.mjs +105 -0
- package/fesm2022/frame-kit-ui-ng-ui-numbered-list.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-pagination.mjs +110 -0
- package/fesm2022/frame-kit-ui-ng-ui-pagination.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-progress-bar.mjs +129 -0
- package/fesm2022/frame-kit-ui-ng-ui-progress-bar.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-sidenav-link.mjs +42 -0
- package/fesm2022/frame-kit-ui-ng-ui-sidenav-link.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-tabs.mjs +894 -0
- package/fesm2022/frame-kit-ui-ng-ui-tabs.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-timeline.mjs +81 -0
- package/fesm2022/frame-kit-ui-ng-ui-timeline.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-toast.mjs +179 -0
- package/fesm2022/frame-kit-ui-ng-ui-toast.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-user-menu.mjs +143 -0
- package/fesm2022/frame-kit-ui-ng-ui-user-menu.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng-ui-wizard-dialog.mjs +191 -0
- package/fesm2022/frame-kit-ui-ng-ui-wizard-dialog.mjs.map +1 -0
- package/fesm2022/frame-kit-ui-ng.mjs +58 -0
- package/fesm2022/frame-kit-ui-ng.mjs.map +1 -0
- package/layouts/app-shell/README.md +357 -0
- package/layouts/content-split/README.md +180 -0
- package/package.json +253 -0
- package/services/overlay-orchestrator/README.md +184 -0
- package/services/spotlight/README.md +61 -0
- package/services/toast/README.md +118 -0
- package/types/frame-kit-ui-ng-core-headline.d.ts +38 -0
- package/types/frame-kit-ui-ng-core-icon.d.ts +74 -0
- package/types/frame-kit-ui-ng-core-image.d.ts +93 -0
- package/types/frame-kit-ui-ng-core-link.d.ts +251 -0
- package/types/frame-kit-ui-ng-core-separator.d.ts +28 -0
- package/types/frame-kit-ui-ng-core-text.d.ts +186 -0
- package/types/frame-kit-ui-ng-directives-infinite-scroll.d.ts +42 -0
- package/types/frame-kit-ui-ng-directives-spotlight.d.ts +51 -0
- package/types/frame-kit-ui-ng-directives-tooltip.d.ts +70 -0
- package/types/frame-kit-ui-ng-docs-endpoint-link.d.ts +43 -0
- package/types/frame-kit-ui-ng-docs-method-badge.d.ts +30 -0
- package/types/frame-kit-ui-ng-forms.d.ts +1674 -0
- package/types/frame-kit-ui-ng-layouts-app-shell.d.ts +75 -0
- package/types/frame-kit-ui-ng-layouts-content-split.d.ts +43 -0
- package/types/frame-kit-ui-ng-services-overlay-orchestrator.d.ts +96 -0
- package/types/frame-kit-ui-ng-services-spotlight.d.ts +32 -0
- package/types/frame-kit-ui-ng-services-toast.d.ts +100 -0
- package/types/frame-kit-ui-ng-ui-accordion.d.ts +86 -0
- package/types/frame-kit-ui-ng-ui-alert.d.ts +34 -0
- package/types/frame-kit-ui-ng-ui-avatar-stack.d.ts +38 -0
- package/types/frame-kit-ui-ng-ui-avatar.d.ts +36 -0
- package/types/frame-kit-ui-ng-ui-badge.d.ts +33 -0
- package/types/frame-kit-ui-ng-ui-breadcrumb.d.ts +45 -0
- package/types/frame-kit-ui-ng-ui-button.d.ts +48 -0
- package/types/frame-kit-ui-ng-ui-callout.d.ts +26 -0
- package/types/frame-kit-ui-ng-ui-card.d.ts +30 -0
- package/types/frame-kit-ui-ng-ui-copyable-field.d.ts +62 -0
- package/types/frame-kit-ui-ng-ui-data-table.d.ts +482 -0
- package/types/frame-kit-ui-ng-ui-dialog.d.ts +166 -0
- package/types/frame-kit-ui-ng-ui-drawer.d.ts +130 -0
- package/types/frame-kit-ui-ng-ui-dropdown-menu.d.ts +77 -0
- package/types/frame-kit-ui-ng-ui-editable-field.d.ts +65 -0
- package/types/frame-kit-ui-ng-ui-icon-badge.d.ts +45 -0
- package/types/frame-kit-ui-ng-ui-icon-list.d.ts +67 -0
- package/types/frame-kit-ui-ng-ui-inline-edit.d.ts +44 -0
- package/types/frame-kit-ui-ng-ui-list-editor.d.ts +56 -0
- package/types/frame-kit-ui-ng-ui-loader.d.ts +32 -0
- package/types/frame-kit-ui-ng-ui-menu-item.d.ts +27 -0
- package/types/frame-kit-ui-ng-ui-nav-brand.d.ts +25 -0
- package/types/frame-kit-ui-ng-ui-nav-group.d.ts +60 -0
- package/types/frame-kit-ui-ng-ui-nav-separator.d.ts +33 -0
- package/types/frame-kit-ui-ng-ui-node-tree-breadcrumb.d.ts +35 -0
- package/types/frame-kit-ui-ng-ui-node-tree.d.ts +135 -0
- package/types/frame-kit-ui-ng-ui-note.d.ts +22 -0
- package/types/frame-kit-ui-ng-ui-numbered-list.d.ts +52 -0
- package/types/frame-kit-ui-ng-ui-pagination.d.ts +49 -0
- package/types/frame-kit-ui-ng-ui-progress-bar.d.ts +50 -0
- package/types/frame-kit-ui-ng-ui-sidenav-link.d.ts +24 -0
- package/types/frame-kit-ui-ng-ui-tabs.d.ts +266 -0
- package/types/frame-kit-ui-ng-ui-timeline.d.ts +42 -0
- package/types/frame-kit-ui-ng-ui-toast.d.ts +56 -0
- package/types/frame-kit-ui-ng-ui-user-menu.d.ts +87 -0
- package/types/frame-kit-ui-ng-ui-wizard-dialog.d.ts +116 -0
- package/types/frame-kit-ui-ng.d.ts +53 -0
- package/ui/accordion/README.md +261 -0
- package/ui/alert/README.md +211 -0
- package/ui/avatar/README.md +167 -0
- package/ui/avatar-stack/README.md +164 -0
- package/ui/badge/README.md +162 -0
- package/ui/breadcrumb/README.md +240 -0
- package/ui/button/README.md +184 -0
- package/ui/callout/README.md +159 -0
- package/ui/card/README.md +174 -0
- package/ui/copyable-field/README.md +235 -0
- package/ui/data-table/README.md +408 -0
- package/ui/dialog/README.md +222 -0
- package/ui/drawer/README.md +274 -0
- package/ui/dropdown-menu/README.md +336 -0
- package/ui/editable-field/README.md +171 -0
- package/ui/icon-badge/README.md +131 -0
- package/ui/icon-list/README.md +205 -0
- package/ui/inline-edit/README.md +135 -0
- package/ui/list-editor/README.md +162 -0
- package/ui/loader/README.md +160 -0
- package/ui/menu-item/README.md +204 -0
- package/ui/nav-brand/README.md +111 -0
- package/ui/nav-group/README.md +145 -0
- package/ui/nav-separator/README.md +44 -0
- package/ui/node-tree/README.md +278 -0
- package/ui/node-tree-breadcrumb/README.md +164 -0
- package/ui/note/README.md +146 -0
- package/ui/numbered-list/README.md +187 -0
- package/ui/pagination/README.md +174 -0
- package/ui/progress-bar/README.md +223 -0
- package/ui/sidenav-link/README.md +214 -0
- package/ui/tabs/README.md +204 -0
- package/ui/timeline/README.md +285 -0
- package/ui/toast/README.md +243 -0
- package/ui/user-menu/README.md +260 -0
- package/ui/wizard-dialog/README.md +283 -0
|
@@ -0,0 +1,1102 @@
|
|
|
1
|
+
# FrameKit UI-ng — Component Development Guide
|
|
2
|
+
|
|
3
|
+
This is the single source of truth for building components in `packages/ui-ng`. Follow every section exactly. If a pattern is defined here, it is non-negotiable. The goal is absolute consistency across every component in the library.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Table of Contents
|
|
8
|
+
|
|
9
|
+
1. [Architecture Overview](#architecture-overview)
|
|
10
|
+
2. [Folder and File Structure](#folder-and-file-structure)
|
|
11
|
+
3. [Component TypeScript](#component-typescript)
|
|
12
|
+
4. [Templates](#templates)
|
|
13
|
+
5. [Styling (SCSS)](#styling-scss)
|
|
14
|
+
6. [Token Architecture](#token-architecture)
|
|
15
|
+
7. [Dark Mode](#dark-mode)
|
|
16
|
+
8. [Spacing Rules](#spacing-rules)
|
|
17
|
+
9. [Directives](#directives)
|
|
18
|
+
10. [Services](#services)
|
|
19
|
+
11. [Types](#types)
|
|
20
|
+
12. [Tests](#tests)
|
|
21
|
+
13. [Storybook Stories](#storybook-stories)
|
|
22
|
+
14. [README Documentation](#readme-documentation)
|
|
23
|
+
15. [Public API Exports](#public-api-exports)
|
|
24
|
+
16. [Accessibility](#accessibility)
|
|
25
|
+
17. [Definition of Done](#definition-of-done)
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Architecture Overview
|
|
30
|
+
|
|
31
|
+
FrameKit UI-ng is a style-agnostic, token-driven Angular component library. Components define structure, layout, accessibility, and interaction. They never define brand styling. All visual decisions come from `--fk-*` design tokens provided by the consumer application.
|
|
32
|
+
|
|
33
|
+
### Core principles
|
|
34
|
+
|
|
35
|
+
1. **Components never contain brand styling.** No hardcoded colors, typography scales, brand radii, shadows, or visual identity. All visual styling is expressed via CSS custom properties.
|
|
36
|
+
2. **Components only consume tokens.** Token definitions belong in theme files, never in component SCSS.
|
|
37
|
+
3. **Components are self-sufficient.** Every component renders correctly with zero external dependencies — no theme file required.
|
|
38
|
+
4. **Stable public API.** Consumers use `<fk-*>` components with stable inputs. Internal implementations can change without breaking consumers.
|
|
39
|
+
|
|
40
|
+
### Library structure
|
|
41
|
+
|
|
42
|
+
Components are organized by **purpose**, not Atomic Design. Each layer has clear responsibilities and a strict dependency direction:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
packages/ui-ng/
|
|
46
|
+
index.ts — primary barrel; re-exports every entry point as @frame-kit/ui-ng
|
|
47
|
+
core/ — raw primitives, no other lib component deps (icon, text, headline, link, image, separator)
|
|
48
|
+
ui/ — composed reusable UI (button, dialog, drawer, toast, tabs, data-table, etc.)
|
|
49
|
+
directives/ — standalone behavior directives (tooltip, infinite-scroll, spotlight)
|
|
50
|
+
docs/ — API-reference UI (endpoint-link, method-badge)
|
|
51
|
+
forms/ — self-contained form system; packaged as ONE entry point (holds its own primitives)
|
|
52
|
+
engine/ — schema parser, field registry, validation, error messages, field state
|
|
53
|
+
fields/ — 16 form-field components (input, select, checkbox, etc.)
|
|
54
|
+
composition/ — form wrappers and renderer (form, form-field, form-renderer, label, hint, error, etc.)
|
|
55
|
+
layouts/ — structural page scaffolding (app-shell, content-split)
|
|
56
|
+
services/ — runtime services with no UI (overlay-orchestrator, toast, spotlight)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> Color/date helpers are **not** in this library — they live in `@frame-kit/utils` (framework-agnostic `Color` / `DateUtil`). Don't add UI-less utility services here that have no Angular dependency.
|
|
60
|
+
|
|
61
|
+
**Dependency rules:**
|
|
62
|
+
|
|
63
|
+
- `core` imports nothing from the lib.
|
|
64
|
+
- `ui`, `directives`, and `docs` may use `core` and `services`.
|
|
65
|
+
- `forms` may use `core`, `ui`, `directives`, and `services`.
|
|
66
|
+
- `layouts` may use anything; nothing imports from `layouts`.
|
|
67
|
+
|
|
68
|
+
All layers follow the same conventions defined in this guide.
|
|
69
|
+
|
|
70
|
+
### Packaging: secondary entry points
|
|
71
|
+
|
|
72
|
+
The library is published with **one ng-packagr secondary entry point per component** so consumers can tree-shake and code-split per component. This shapes how source is laid out and how components import each other:
|
|
73
|
+
|
|
74
|
+
- **Each leaf component folder is its own entry point** — `packages/ui-ng/<layer>/<name>/` builds to `@frame-kit/ui-ng/<layer>/<name>` (its own FESM). The one exception is **`forms/`**, which is a single entry point (`@frame-kit/ui-ng/forms`) because `engine`/`fields`/`composition` are mutually dependent — its sub-folders are **not** separate entry points.
|
|
75
|
+
- **The primary `index.ts` barrel** re-exports every entry point, so consumers keep importing from `@frame-kit/ui-ng` and still get per-component splitting.
|
|
76
|
+
- **Cross-component imports must use the package path, not a relative `../`.** ng-packagr forbids relative imports that cross an entry-point boundary. Import a sibling component via its entry point:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
// ✅ in ui/button — importing the loader (a different entry point)
|
|
80
|
+
import { LoaderComponent } from '@frame-kit/ui-ng/ui/loader';
|
|
81
|
+
|
|
82
|
+
// ❌ crosses an entry-point boundary — ng-packagr build fails
|
|
83
|
+
import { LoaderComponent } from '../loader/loader.component';
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Imports **within the same entry point stay relative** (e.g. a forms field importing `../../engine/schema`, or a component importing its own `./button.types`).
|
|
87
|
+
|
|
88
|
+
See [Public API Exports](#public-api-exports) for the files each new component must add.
|
|
89
|
+
|
|
90
|
+
### Token layers
|
|
91
|
+
|
|
92
|
+
| Layer | Location | Purpose |
|
|
93
|
+
| ------------------------ | ------------------------------------------- | ----------------------------------------------------------------- |
|
|
94
|
+
| 1. Component consumption | `packages/ui-ng/**/*.scss` | Consumes `--fk-*` tokens with two-tier fallbacks |
|
|
95
|
+
| 2. Base tokens | `packages/tokens/src/scss/tokens.base.scss` | Default theme opinion — maps semantic tokens to component tokens |
|
|
96
|
+
| 3. Dark mode | `packages/tokens/src/scss/tokens.dark.scss` | Remaps color-sensitive tokens for dark mode |
|
|
97
|
+
| 4. Consumer overrides | Consumer app stylesheets | Consumers override tokens at `:root`, `.theme-*`, or per-instance |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Folder and File Structure
|
|
102
|
+
|
|
103
|
+
Every component must have this exact file set, placed in the correct layer (`core/`, `ui/`, `directives/`, `docs/`, `forms/{engine,fields,composition}/`, `layouts/`):
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
packages/ui-ng/<layer>/<name>/
|
|
107
|
+
<name>.component.ts
|
|
108
|
+
<name>.component.html
|
|
109
|
+
<name>.component.scss
|
|
110
|
+
<name>.component.spec.ts
|
|
111
|
+
<name>.component.stories.ts
|
|
112
|
+
<name>.types.ts
|
|
113
|
+
README.md
|
|
114
|
+
index.ts (entry-point barrel — see note below)
|
|
115
|
+
ng-package.json (entry-point marker — see note below)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`index.ts` and `ng-package.json` make the folder a secondary entry point. Their contents are fixed boilerplate:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
// index.ts — export the component's public files
|
|
122
|
+
export * from './<name>.component';
|
|
123
|
+
export * from './<name>.types';
|
|
124
|
+
// + any associated directives, e.g. export * from './<name>-icon.directive';
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
// ng-package.json
|
|
129
|
+
{ "lib": { "entryFile": "index.ts" } }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Exception — `forms/`:** components under `forms/{engine,fields,composition}/<name>/` do **not** get their own `index.ts`/`ng-package.json`. Forms is a single entry point: add the component's public files to the relevant sub-barrel (`forms/composition/index.ts`, `forms/fields/index.ts`, or `forms/engine/index.ts`) instead.
|
|
133
|
+
|
|
134
|
+
Additional files when the component uses content projection directives:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
<name>-<slot>.directive.ts (e.g. alert-icon.directive.ts)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Naming conventions
|
|
141
|
+
|
|
142
|
+
- Folder name: kebab-case matching the component name (`badge`, `inline-edit`, `data-table`)
|
|
143
|
+
- File names: always `<folder-name>.component.*`
|
|
144
|
+
- Types file: always `<folder-name>.types.ts`
|
|
145
|
+
- Directive files: `<folder-name>-<purpose>.directive.ts`
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Component TypeScript
|
|
150
|
+
|
|
151
|
+
### Required structure
|
|
152
|
+
|
|
153
|
+
Every component follows this exact pattern. Reference the badge component as the canonical atom example and the alert component as the canonical molecule example.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { ChangeDetectionStrategy, Component, HostBinding, computed, input } from '@angular/core';
|
|
157
|
+
|
|
158
|
+
import type { ExampleVariant } from './example.types';
|
|
159
|
+
|
|
160
|
+
@Component({
|
|
161
|
+
selector: 'fk-example',
|
|
162
|
+
standalone: true,
|
|
163
|
+
imports: [],
|
|
164
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
165
|
+
templateUrl: './example.component.html',
|
|
166
|
+
styleUrl: './example.component.scss',
|
|
167
|
+
})
|
|
168
|
+
export class ExampleComponent {
|
|
169
|
+
// ===== INPUTS =====
|
|
170
|
+
readonly variant = input<ExampleVariant>('default');
|
|
171
|
+
|
|
172
|
+
// ===== CONTENT CHILDREN =====
|
|
173
|
+
// (only when component uses content projection directives)
|
|
174
|
+
|
|
175
|
+
// ===== BASE PROPS =====
|
|
176
|
+
readonly className = input<string>('');
|
|
177
|
+
readonly id = input<string | null>(null);
|
|
178
|
+
readonly ariaLabel = input<string | null>(null);
|
|
179
|
+
|
|
180
|
+
// ===== COMPUTED =====
|
|
181
|
+
readonly classes = computed(() => {
|
|
182
|
+
return ['fk-example', `fk-example--${this.variant()}`, this.className()].filter(Boolean).join(' ');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
@HostBinding('class')
|
|
186
|
+
get hostClass() {
|
|
187
|
+
return this.classes();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Rules
|
|
193
|
+
|
|
194
|
+
- **Always standalone.** Every component sets `standalone: true`.
|
|
195
|
+
- **Always OnPush.** Every component uses `ChangeDetectionStrategy.OnPush`.
|
|
196
|
+
- **Signal-based inputs.** Use Angular's `input<Type>(default)` API. Never use the `@Input()` decorator.
|
|
197
|
+
- **Selector prefix.** Always `fk-` followed by the component name in kebab-case.
|
|
198
|
+
- **Section comments.** Organize the class body with `// ===== SECTION =====` dividers in this order: INPUTS, CONTENT CHILDREN, BASE PROPS, COMPUTED.
|
|
199
|
+
|
|
200
|
+
### Base props convention
|
|
201
|
+
|
|
202
|
+
All components should support these inputs when relevant:
|
|
203
|
+
|
|
204
|
+
- `className?: string` — additional CSS classes merged onto the host (default: `""`)
|
|
205
|
+
- `id?: string | null` — optional ID (default: `null`)
|
|
206
|
+
- `ariaLabel?: string | null` — accessible label (default: `null`)
|
|
207
|
+
- `ariaDescribedBy?: string | null` — ID of describing element (default: `null`)
|
|
208
|
+
- `disabled?: boolean` — disables interaction (default: `false`)
|
|
209
|
+
|
|
210
|
+
### Host class binding
|
|
211
|
+
|
|
212
|
+
All components must use this unified mechanism:
|
|
213
|
+
|
|
214
|
+
1. Compute a `classes` signal combining base class + modifiers + consumer `className`
|
|
215
|
+
2. Bind to host via `@HostBinding("class")` getter that returns `this.classes()`
|
|
216
|
+
3. Base class is always `fk-<name>`
|
|
217
|
+
4. Modifier classes follow BEM: `fk-<name>--<modifier>`
|
|
218
|
+
|
|
219
|
+
**Do NOT use:**
|
|
220
|
+
|
|
221
|
+
- `host: { "[class]": ... }` in the `@Component` decorator
|
|
222
|
+
- Multiple class systems in one component
|
|
223
|
+
- Binding classes only to inner elements instead of the host
|
|
224
|
+
|
|
225
|
+
### Semantic variants
|
|
226
|
+
|
|
227
|
+
Variants and sizes must be semantic, library-owned values:
|
|
228
|
+
|
|
229
|
+
- `variant: "primary" | "secondary" | "danger"` — not vendor-specific names
|
|
230
|
+
- `size: "sm" | "md" | "lg"` — not pixel values
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Templates
|
|
235
|
+
|
|
236
|
+
### Content projection
|
|
237
|
+
|
|
238
|
+
Use `<ng-content />` for default slot projection:
|
|
239
|
+
|
|
240
|
+
```html
|
|
241
|
+
<ng-content />
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
For named slots, use attribute selectors matching directive selectors:
|
|
245
|
+
|
|
246
|
+
```html
|
|
247
|
+
<ng-content select="[fkAlertIconStart]" />
|
|
248
|
+
<span class="fk-alert__content">
|
|
249
|
+
<ng-content />
|
|
250
|
+
</span>
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### ID and ARIA binding
|
|
254
|
+
|
|
255
|
+
Pass base props through to the appropriate element in the template:
|
|
256
|
+
|
|
257
|
+
```html
|
|
258
|
+
<span [id]="id() ?? undefined" [attr.aria-label]="ariaLabel()">
|
|
259
|
+
<ng-content />
|
|
260
|
+
</span>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Self-closing syntax
|
|
264
|
+
|
|
265
|
+
Use self-closing tags for void elements:
|
|
266
|
+
|
|
267
|
+
```html
|
|
268
|
+
<ng-content /> <router-outlet />
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Styling (SCSS)
|
|
274
|
+
|
|
275
|
+
### Host element styling
|
|
276
|
+
|
|
277
|
+
Style the host element directly with `:host`:
|
|
278
|
+
|
|
279
|
+
```scss
|
|
280
|
+
:host {
|
|
281
|
+
display: inline-flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
height: var(--fk-badge-height, 1.5rem);
|
|
284
|
+
border-radius: var(--fk-badge-border-radius, 999px);
|
|
285
|
+
font-size: var(--fk-badge-font-size, 0.75rem);
|
|
286
|
+
font-weight: var(--fk-badge-font-weight, var(--fk-font-weight-medium, 500));
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
### Variant styles
|
|
291
|
+
|
|
292
|
+
Apply variant styles using the host class + variant modifier:
|
|
293
|
+
|
|
294
|
+
```scss
|
|
295
|
+
:host.fk-badge--primary {
|
|
296
|
+
background-color: var(--fk-badge-primary-bg, var(--fk-color-primary-subtle, #e8f4ff));
|
|
297
|
+
color: var(--fk-badge-primary-color, var(--fk-color-primary, #0a84ff));
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### BEM naming for internal elements
|
|
302
|
+
|
|
303
|
+
Internal elements use BEM naming under the component namespace:
|
|
304
|
+
|
|
305
|
+
```scss
|
|
306
|
+
.fk-alert__content {
|
|
307
|
+
flex: 1;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.fk-inline-edit__input { ... }
|
|
311
|
+
.fk-inline-edit__action--save { ... }
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Section comments in SCSS
|
|
315
|
+
|
|
316
|
+
Use section dividers for readability:
|
|
317
|
+
|
|
318
|
+
```scss
|
|
319
|
+
// ===== VARIANT COLORS =====
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### What component SCSS must do
|
|
323
|
+
|
|
324
|
+
- Consume tokens with `var(--fk-*)` using the two-tier fallback pattern (see [Token Architecture](#token-architecture))
|
|
325
|
+
- Use hook classes: `fk-<name>` + modifiers like `fk-<name>--primary`
|
|
326
|
+
- Work correctly with `tokens.base.scss` loaded
|
|
327
|
+
- Work correctly with NO theme file loaded (via fallback values)
|
|
328
|
+
|
|
329
|
+
### What component SCSS must NEVER do
|
|
330
|
+
|
|
331
|
+
- **Define `--fk-*` tokens.** Token definitions belong only in theme files (`tokens.base.scss`, `tokens.dark.scss`), never in component SCSS.
|
|
332
|
+
- **Use `::ng-deep`.** Find an alternative approach.
|
|
333
|
+
- **Hardcode hex colors as primary values.** Hex is only allowed as the last-resort fallback inside `var()`.
|
|
334
|
+
- **Use `margin-bottom` for spacing between siblings.** See [Spacing Rules](#spacing-rules).
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
338
|
+
## Token Architecture
|
|
339
|
+
|
|
340
|
+
### The two-tier fallback pattern (required)
|
|
341
|
+
|
|
342
|
+
Every token reference in component SCSS must include a fallback chain so the component renders correctly even when no theme file is loaded:
|
|
343
|
+
|
|
344
|
+
```scss
|
|
345
|
+
/* ✅ Correct — two-tier fallback */
|
|
346
|
+
padding: var(--fk-callout-padding, var(--fk-rhythm-4, 1rem));
|
|
347
|
+
background-color: var(--fk-callout-bg, var(--fk-color-surface-muted, #f7f9fb));
|
|
348
|
+
|
|
349
|
+
/* ❌ Wrong — no fallback, breaks without theme */
|
|
350
|
+
padding: var(--fk-callout-padding);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
The fallback chain is:
|
|
354
|
+
|
|
355
|
+
1. **Component token** (`--fk-callout-padding`) — the consumer override point
|
|
356
|
+
2. **Semantic token** (`--fk-rhythm-4`) — picks up the global theme if loaded
|
|
357
|
+
3. **Raw value** (`1rem`) — the hardcoded baseline that always works
|
|
358
|
+
|
|
359
|
+
This guarantees components are never visually broken regardless of which theme layers are present.
|
|
360
|
+
|
|
361
|
+
### Token naming
|
|
362
|
+
|
|
363
|
+
All tokens are prefixed with `--fk-*`. Component-specific tokens follow the pattern:
|
|
364
|
+
|
|
365
|
+
```
|
|
366
|
+
--fk-<component>-<property>
|
|
367
|
+
--fk-<component>-<variant>-<property>
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Examples:
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
--fk-badge-height
|
|
374
|
+
--fk-badge-font-size
|
|
375
|
+
--fk-badge-primary-bg
|
|
376
|
+
--fk-badge-primary-color
|
|
377
|
+
--fk-alert-padding
|
|
378
|
+
--fk-alert-info-bg
|
|
379
|
+
--fk-alert-info-color
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Semantic tokens (shared across components):
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
--fk-color-primary
|
|
386
|
+
--fk-color-surface-muted
|
|
387
|
+
--fk-color-text
|
|
388
|
+
--fk-font-weight-medium
|
|
389
|
+
--fk-rhythm-4
|
|
390
|
+
--fk-radius-md
|
|
391
|
+
--fk-focus-ring
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Where tokens are defined
|
|
395
|
+
|
|
396
|
+
Tokens are defined ONLY in theme files, NEVER in component SCSS:
|
|
397
|
+
|
|
398
|
+
| File | What it defines |
|
|
399
|
+
| ------------------------------------------- | ----------------------------- |
|
|
400
|
+
| `packages/tokens/src/scss/tokens.base.scss` | All token defaults in `:root` |
|
|
401
|
+
| `packages/tokens/src/scss/tokens.dark.scss` | Dark mode color overrides |
|
|
402
|
+
|
|
403
|
+
Token definitions must only appear in:
|
|
404
|
+
|
|
405
|
+
- `:root`
|
|
406
|
+
- `.theme-*`
|
|
407
|
+
|
|
408
|
+
Token definitions must NEVER appear in:
|
|
409
|
+
|
|
410
|
+
- `.fk-*` selectors
|
|
411
|
+
- `:host`
|
|
412
|
+
- Component selectors
|
|
413
|
+
|
|
414
|
+
This ensures consumer overrides always win — component selectors would increase specificity and break the override chain.
|
|
415
|
+
|
|
416
|
+
### Adding tokens for a new component
|
|
417
|
+
|
|
418
|
+
1. Define component tokens in `tokens.base.scss` following the ITCSS order (primitives, semantic, component tokens grouped by layer: core, ui, directives, forms, layouts, keyframes)
|
|
419
|
+
2. If the component has color-sensitive tokens, add dark mode overrides in `tokens.dark.scss` under both the `@media (prefers-color-scheme: dark)` block and the `html.dark` block
|
|
420
|
+
3. Document the tokens in the component README
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Dark Mode
|
|
425
|
+
|
|
426
|
+
### How it works
|
|
427
|
+
|
|
428
|
+
Dark mode is a token remapping layer. Components are completely unaware of dark mode — they consume the same `--fk-*` tokens regardless of theme. The dark mode file (`tokens.dark.scss`) redefines color-sensitive tokens under two selectors:
|
|
429
|
+
|
|
430
|
+
1. `@media (prefers-color-scheme: dark)` — system preference, automatic
|
|
431
|
+
2. `html.dark` — user override via JavaScript, always wins due to higher specificity
|
|
432
|
+
|
|
433
|
+
### What gets remapped in dark mode
|
|
434
|
+
|
|
435
|
+
Only color-sensitive tokens:
|
|
436
|
+
|
|
437
|
+
- Color primitives (`--fk-color-surface`, `--fk-color-text`, `--fk-color-border`, etc.)
|
|
438
|
+
- Focus ring (`--fk-focus-ring`)
|
|
439
|
+
- Component color tokens (backgrounds, text colors, border colors)
|
|
440
|
+
|
|
441
|
+
### What stays the same
|
|
442
|
+
|
|
443
|
+
Non-color tokens are NOT redefined:
|
|
444
|
+
|
|
445
|
+
- Typography (font sizes, weights, line heights)
|
|
446
|
+
- Spacing / rhythm
|
|
447
|
+
- Radii
|
|
448
|
+
- Sizing tokens
|
|
449
|
+
|
|
450
|
+
### Adding dark mode support for a new component
|
|
451
|
+
|
|
452
|
+
When your component introduces new color tokens, add overrides in `tokens.dark.scss` under both blocks:
|
|
453
|
+
|
|
454
|
+
```scss
|
|
455
|
+
@media (prefers-color-scheme: dark) {
|
|
456
|
+
:root {
|
|
457
|
+
--fk-newcomponent-bg: var(--fk-color-surface-muted);
|
|
458
|
+
--fk-newcomponent-color: var(--fk-color-text);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
html.dark {
|
|
463
|
+
--fk-newcomponent-bg: var(--fk-color-surface-muted);
|
|
464
|
+
--fk-newcomponent-color: var(--fk-color-text);
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
### Verification
|
|
469
|
+
|
|
470
|
+
Always verify appearance in both light and dark modes. Run Storybook (it loads `tokens.base.scss` + `tokens.dark.scss`) and toggle the theme:
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
npm run storybook:ui-ng
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Spacing Rules
|
|
479
|
+
|
|
480
|
+
### Margin direction convention
|
|
481
|
+
|
|
482
|
+
Use **top margin only** for vertical spacing between elements. Bottom margin is always zero.
|
|
483
|
+
|
|
484
|
+
This matches the token definitions:
|
|
485
|
+
|
|
486
|
+
```scss
|
|
487
|
+
--fk-text-margin-block-start: var(--fk-rhythm-4); /* top */
|
|
488
|
+
--fk-text-margin-block-end: 0; /* bottom */
|
|
489
|
+
--fk-headline-margin-block-start: var(--fk-rhythm-4);
|
|
490
|
+
--fk-headline-margin-block-end: 0;
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Rules
|
|
494
|
+
|
|
495
|
+
- Space between elements is created by the **top margin of the next element**, not the bottom margin of the current element
|
|
496
|
+
- Never use `margin-bottom` for spacing between siblings
|
|
497
|
+
- Prefer `gap` (via flex/grid) over directional margins when elements are in a flex or grid container
|
|
498
|
+
- Micro-alignment nudges (e.g. `margin-top: 0.125rem` to align an icon with text) are acceptable
|
|
499
|
+
|
|
500
|
+
### Examples
|
|
501
|
+
|
|
502
|
+
```scss
|
|
503
|
+
/* ✅ Correct — gap-based spacing */
|
|
504
|
+
.container {
|
|
505
|
+
display: flex;
|
|
506
|
+
flex-direction: column;
|
|
507
|
+
gap: var(--fk-rhythm-3, 0.75rem);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/* ✅ Correct — top margin */
|
|
511
|
+
.next-element {
|
|
512
|
+
margin-top: var(--fk-rhythm-4, 1rem);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/* ❌ Wrong — bottom margin for spacing */
|
|
516
|
+
.current-element {
|
|
517
|
+
margin-bottom: var(--fk-rhythm-4, 1rem);
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Field-level vs container-level spacing
|
|
522
|
+
|
|
523
|
+
For atom-tier components — anything in `forms/fields/` (`fk-input`, `fk-textarea`, `fk-select`, etc.) and the `fk-form-field` wrapper — **do not put margin on the host**. Inter-field rhythm is owned by parent containers (`fk-field-group`, `fk-form-renderer__fields`, or any consumer-supplied `display: flex; flex-direction: column; gap: var(--fk-form-field-gap, …)` wrapper).
|
|
524
|
+
|
|
525
|
+
Why this matters: the `:host { margin-top: ... } / :host:first-child { margin-top: 0 }` pattern fires per-parent, not per-form. When fields are wrapped (e.g. each `fk-form-field` sits inside its own `fk-field-outlet` in the schema-driven path), every wrapped field matches `:first-child` and zeros its margin — so the form ends up flush. When fields are direct siblings of a `<form>` element (the manual path), only the literal first sibling matches the reset and the rest carry full margin. The result is two visually different forms even though the components and validators are identical.
|
|
526
|
+
|
|
527
|
+
```scss
|
|
528
|
+
/* ❌ Wrong — host-driven inter-field rhythm */
|
|
529
|
+
:host {
|
|
530
|
+
margin-top: var(--fk-form-field-gap, var(--fk-rhythm-4, 1rem));
|
|
531
|
+
}
|
|
532
|
+
:host:first-child {
|
|
533
|
+
margin-top: 0;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* ✅ Correct — parent-driven inter-field rhythm */
|
|
537
|
+
/* In the parent (fk-field-group, fk-form-renderer, etc.): */
|
|
538
|
+
.fk-field-group__content {
|
|
539
|
+
display: flex;
|
|
540
|
+
flex-direction: column;
|
|
541
|
+
row-gap: var(--fk-form-field-gap, var(--fk-rhythm-4, 1rem));
|
|
542
|
+
}
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
The Storybook regression test for this pattern lives at `Forms/Composition/Form Renderer · SpacingParity` — a side-by-side comparison of a schema-rendered form and a hand-built form with identical fields. They should render pixel-identical; if they ever drift, host-margin has crept back into a field somewhere.
|
|
546
|
+
|
|
547
|
+
---
|
|
548
|
+
|
|
549
|
+
## Directives
|
|
550
|
+
|
|
551
|
+
There are two distinct directive kinds in this library — they look similar in code but have different purposes and different documentation requirements.
|
|
552
|
+
|
|
553
|
+
| Kind | Lives in | Has its own README? | Has stories? |
|
|
554
|
+
| --------------------------- | ---------------------------------------------------- | ------------------------------------------- | ----------------- |
|
|
555
|
+
| **Behavior directives** | `packages/ui-ng/directives/` | Yes — full per-kind README (see below) | Yes |
|
|
556
|
+
| **Content-slot directives** | Inside the parent component's folder (e.g. `alert/`) | No — covered in the parent component's docs | No (no UI on own) |
|
|
557
|
+
|
|
558
|
+
### Behavior directives
|
|
559
|
+
|
|
560
|
+
Standalone directives that add behavior to any element they're applied to (e.g. `fkTooltip`, `fkInfiniteScroll`, `fkSpotlightTrigger`). Each lives in its own folder under `packages/ui-ng/directives/<name>/` and ships its own README + Storybook story alongside the source.
|
|
561
|
+
|
|
562
|
+
#### Required files
|
|
563
|
+
|
|
564
|
+
```
|
|
565
|
+
packages/ui-ng/directives/<name>/
|
|
566
|
+
<name>.directive.ts
|
|
567
|
+
<name>.directive.spec.ts
|
|
568
|
+
<name>.directive.stories.ts
|
|
569
|
+
README.md
|
|
570
|
+
index.ts (entry-point barrel: export * from './<name>.directive')
|
|
571
|
+
ng-package.json ({ "lib": { "entryFile": "index.ts" } })
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
A behavior directive is its own secondary entry point (`@frame-kit/ui-ng/directives/<name>`). When the directive needs companion utilities (positioning helpers, types files), keep them in the same folder and export them from `index.ts`; the README points at the public surface.
|
|
575
|
+
|
|
576
|
+
#### README section order
|
|
577
|
+
|
|
578
|
+
Mirrors the component README order so consumers don't have to re-learn structure:
|
|
579
|
+
|
|
580
|
+
1. **Title + one-line summary** — `# fkTooltip` followed by a sentence describing what the directive does and where it attaches.
|
|
581
|
+
2. **API** — Inputs (table), Outputs (table or "None."), and a Types subsection if the directive exposes string-literal types.
|
|
582
|
+
3. **Features** — short bullet list.
|
|
583
|
+
4. **Quick Start** — smallest useful HTML snippet using the attribute.
|
|
584
|
+
5. **Import** — `@frame-kit/ui-ng` import + standalone `imports: [...]` registration.
|
|
585
|
+
6. **Examples** — one subsection per realistic usage pattern.
|
|
586
|
+
7. **Accessibility** — keyboard behavior, ARIA host bindings, focus interaction, `prefers-reduced-motion` handling, anti-patterns.
|
|
587
|
+
8. **Design Tokens** (when the directive renders any styled UI like a tooltip popover) — tokens consumed + override snippet.
|
|
588
|
+
9. **Behavior Notes** — edge cases, lifecycle (when host is destroyed), and how the directive interacts with the `OverlayOrchestrator` if it owns an overlay.
|
|
589
|
+
|
|
590
|
+
#### Storybook coverage
|
|
591
|
+
|
|
592
|
+
- Default usage attached to a single host element.
|
|
593
|
+
- One story per non-trivial input variant (e.g. each `position` for `fkTooltip`).
|
|
594
|
+
- An `AllVariants` composite when the directive has more than two configuration knobs, so the gallery shows them side-by-side.
|
|
595
|
+
- For directive pairs that only make sense together (e.g. `fkSpotlightTrigger` + `fkSpotlightTarget`), one story may suffice — render both in the same fixture.
|
|
596
|
+
|
|
597
|
+
#### JSDoc on the public API
|
|
598
|
+
|
|
599
|
+
Every `input()`, `output()`, `model()`, and public method on the directive class carries a one-line JSDoc above its declaration. Inputs answer "what does this control?", outputs "when does this fire?", methods "what does this do?". The class itself carries a paragraph-level JSDoc explaining the directive's responsibility (one short paragraph; no marketing copy).
|
|
600
|
+
|
|
601
|
+
### Content-slot directives
|
|
602
|
+
|
|
603
|
+
When a component needs named content slots (e.g. an icon slot in an alert), create a marker directive:
|
|
604
|
+
|
|
605
|
+
```ts
|
|
606
|
+
import { Directive, InjectionToken } from '@angular/core';
|
|
607
|
+
|
|
608
|
+
export const FK_ALERT_ICON_START = new InjectionToken<FkAlertIconStartDirective>('FK_ALERT_ICON_START');
|
|
609
|
+
|
|
610
|
+
@Directive({
|
|
611
|
+
selector: '[fkAlertIconStart]',
|
|
612
|
+
standalone: true,
|
|
613
|
+
providers: [{ provide: FK_ALERT_ICON_START, useExisting: FkAlertIconStartDirective }],
|
|
614
|
+
host: {
|
|
615
|
+
'aria-hidden': 'true',
|
|
616
|
+
},
|
|
617
|
+
})
|
|
618
|
+
export class FkAlertIconStartDirective {}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
#### Rules
|
|
622
|
+
|
|
623
|
+
- Directive selector is always a camelCase attribute: `[fkComponentSlotName]`
|
|
624
|
+
- Provide an `InjectionToken` so the parent component can detect the directive via `contentChild()`
|
|
625
|
+
- Set appropriate `host` attributes (e.g. `aria-hidden` for decorative icons)
|
|
626
|
+
- Directive is always `standalone: true`
|
|
627
|
+
- The parent component detects the directive using `contentChild(FK_TOKEN_NAME)`:
|
|
628
|
+
|
|
629
|
+
```ts
|
|
630
|
+
readonly iconStartRef = contentChild(FK_ALERT_ICON_START);
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
#### Naming conventions
|
|
634
|
+
|
|
635
|
+
- InjectionToken constant: `FK_<COMPONENT>_<SLOT>` (screaming snake case)
|
|
636
|
+
- Directive class: `Fk<Component><Slot>Directive` (PascalCase)
|
|
637
|
+
- Selector attribute: `fk<Component><Slot>` (camelCase)
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Services
|
|
642
|
+
|
|
643
|
+
Runtime services with no UI that **depend on Angular** (e.g. `OverlayOrchestrator`, `ToastService`, `SpotlightService`). Each lives in its own folder under `packages/ui-ng/services/<name>/` and ships a README alongside the source. Framework-agnostic helpers (color, date math) do **not** belong here — they go in `@frame-kit/utils`.
|
|
644
|
+
|
|
645
|
+
### Required files
|
|
646
|
+
|
|
647
|
+
```
|
|
648
|
+
packages/ui-ng/services/<name>/
|
|
649
|
+
<name>.service.ts
|
|
650
|
+
<name>.service.spec.ts
|
|
651
|
+
README.md
|
|
652
|
+
index.ts (entry-point barrel: export * from './<name>.service')
|
|
653
|
+
ng-package.json ({ "lib": { "entryFile": "index.ts" } })
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
A service is its own secondary entry point (`@frame-kit/ui-ng/services/<name>`). Services do not ship Storybook stories — they have no rendered surface. Behavior is exercised through their unit spec.
|
|
657
|
+
|
|
658
|
+
### Provider scope
|
|
659
|
+
|
|
660
|
+
State the provider scope at the top of the README. Two patterns:
|
|
661
|
+
|
|
662
|
+
- **Root-provided** (`@Injectable({ providedIn: "root" })`) — singleton across the app. Default for orchestration, registries, and global mutable state. Document any side effects of construction (subscriptions, listeners) so consumers know what they're opting into.
|
|
663
|
+
- **Component-tree-provided** — when the service must be scoped to a feature (e.g. one toast queue per overlay surface). The README must show where to declare the provider.
|
|
664
|
+
|
|
665
|
+
### README section order
|
|
666
|
+
|
|
667
|
+
1. **Title + one-line summary** — `# OverlayOrchestrator` followed by a sentence stating the service's responsibility.
|
|
668
|
+
2. **Initialization** — provider scope, any setup required, and (if applicable) any built-in integrations the service triggers automatically.
|
|
669
|
+
3. **API** — Public methods and signals as a table or sub-sections. For each: signature, return type, what it does, when to call it, what it emits/mutates.
|
|
670
|
+
4. **Usage** — A realistic snippet wiring the service into a component (`inject(...)`, calling the method, reading the signal).
|
|
671
|
+
5. **Behavior Notes** — Concurrency contract, lifecycle (when handles are released), interaction with other services (e.g. how `DialogService` registers with `OverlayOrchestrator`).
|
|
672
|
+
|
|
673
|
+
Services typically don't have Accessibility or Design Tokens sections. Skip them rather than leaving them empty. If the service exposes a value object that ships with token-driven defaults (e.g. a default toast variant), document that in Behavior Notes.
|
|
674
|
+
|
|
675
|
+
### JSDoc on the public API
|
|
676
|
+
|
|
677
|
+
Every public method, signal, and exported constant on the service carries a one-line JSDoc above its declaration. The class itself carries a paragraph-level JSDoc explaining the service's responsibility and the singleton vs scoped contract.
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
## Types
|
|
682
|
+
|
|
683
|
+
Every component has a `<name>.types.ts` file that exports its variant and configuration types:
|
|
684
|
+
|
|
685
|
+
```ts
|
|
686
|
+
export type BadgeVariant = 'default' | 'primary' | 'success' | 'warning' | 'danger';
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### Rules
|
|
690
|
+
|
|
691
|
+
- One types file per component
|
|
692
|
+
- Export union types for variants, sizes, and any string-literal inputs
|
|
693
|
+
- Import types with `import type { ... }` in the component file
|
|
694
|
+
- Keep types minimal — only what the public API needs
|
|
695
|
+
|
|
696
|
+
---
|
|
697
|
+
|
|
698
|
+
## Tests
|
|
699
|
+
|
|
700
|
+
### Required test file structure
|
|
701
|
+
|
|
702
|
+
```ts
|
|
703
|
+
import { Component } from "@angular/core";
|
|
704
|
+
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
|
705
|
+
import { ExampleComponent } from "./example.component";
|
|
706
|
+
|
|
707
|
+
describe("ExampleComponent", () => {
|
|
708
|
+
let component: ExampleComponent;
|
|
709
|
+
let fixture: ComponentFixture<ExampleComponent>;
|
|
710
|
+
|
|
711
|
+
beforeEach(async () => {
|
|
712
|
+
await TestBed.configureTestingModule({
|
|
713
|
+
imports: [ExampleComponent],
|
|
714
|
+
}).compileComponents();
|
|
715
|
+
|
|
716
|
+
fixture = TestBed.createComponent(ExampleComponent);
|
|
717
|
+
component = fixture.componentInstance;
|
|
718
|
+
fixture.detectChanges();
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
it("should create", () => {
|
|
722
|
+
expect(component).toBeTruthy();
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
describe("Defaults", () => { ... });
|
|
726
|
+
describe("Host classes", () => { ... });
|
|
727
|
+
describe("Variants", () => { ... });
|
|
728
|
+
describe("Style overrides", () => { ... });
|
|
729
|
+
describe("Content projection", () => { ... });
|
|
730
|
+
describe("Base props passthrough", () => { ... });
|
|
731
|
+
});
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
### Required test sections
|
|
735
|
+
|
|
736
|
+
Every component spec must include these `describe` blocks:
|
|
737
|
+
|
|
738
|
+
1. **`should create`** — basic instantiation check
|
|
739
|
+
2. **`Defaults`** — verify every input's default value using `component.inputName()`
|
|
740
|
+
3. **`Host classes`** — verify base class is applied, variant class is applied, variant class updates when input changes, custom `className` is merged without replacing hook classes
|
|
741
|
+
4. **`Variants`** — iterate all variant values and verify each applies the correct class
|
|
742
|
+
5. **`Style overrides`** — verify inline style bindings (color, backgroundColor, etc.) when applicable
|
|
743
|
+
6. **`Content projection`** — create a test host component with projected content and verify it renders
|
|
744
|
+
7. **`Base props passthrough`** — verify `id`, `ariaLabel`, etc. are passed through to the correct DOM element
|
|
745
|
+
|
|
746
|
+
### Testing patterns
|
|
747
|
+
|
|
748
|
+
- Use `fixture.componentRef.setInput("name", value)` to set signal-based inputs
|
|
749
|
+
- For content projection tests, create an inline `@Component` test host:
|
|
750
|
+
|
|
751
|
+
```ts
|
|
752
|
+
@Component({
|
|
753
|
+
standalone: true,
|
|
754
|
+
imports: [BadgeComponent],
|
|
755
|
+
template: `<fk-badge>Active</fk-badge>`,
|
|
756
|
+
})
|
|
757
|
+
class TestHostComponent {}
|
|
758
|
+
|
|
759
|
+
const hostFixture = TestBed.createComponent(TestHostComponent);
|
|
760
|
+
hostFixture.detectChanges();
|
|
761
|
+
```
|
|
762
|
+
|
|
763
|
+
- Access the native element with `fixture.nativeElement as HTMLElement`
|
|
764
|
+
- Check classes with `el.classList.contains("fk-badge")`
|
|
765
|
+
- Check attributes with `el.getAttribute("aria-label")`
|
|
766
|
+
- Check inline styles with `el.style.color`
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
## Storybook Stories
|
|
771
|
+
|
|
772
|
+
### Required file structure
|
|
773
|
+
|
|
774
|
+
```ts
|
|
775
|
+
import type { Meta, StoryObj } from '@storybook/angular';
|
|
776
|
+
import { ExampleComponent } from './example.component';
|
|
777
|
+
|
|
778
|
+
const meta: Meta<ExampleComponent> = {
|
|
779
|
+
title: 'Atoms/Example', // or "Molecules/Example" or "Organisms/Example"
|
|
780
|
+
component: ExampleComponent,
|
|
781
|
+
tags: ['autodocs'],
|
|
782
|
+
argTypes: {
|
|
783
|
+
variant: {
|
|
784
|
+
control: 'select',
|
|
785
|
+
options: ['default', 'primary', 'success', 'warning', 'danger'],
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
export default meta;
|
|
791
|
+
type Story = StoryObj<ExampleComponent>;
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Title convention
|
|
795
|
+
|
|
796
|
+
- Atoms: `"Atoms/<Name>"`
|
|
797
|
+
- Molecules: `"Molecules/<Name>"`
|
|
798
|
+
- Organisms: `"Organisms/<Name>"`
|
|
799
|
+
|
|
800
|
+
### Story pattern
|
|
801
|
+
|
|
802
|
+
Each story uses a render function with props and template:
|
|
803
|
+
|
|
804
|
+
```ts
|
|
805
|
+
export const Default: Story = {
|
|
806
|
+
render: (args) => ({
|
|
807
|
+
props: args,
|
|
808
|
+
template: `<fk-example [variant]="variant">Content</fk-example>`,
|
|
809
|
+
}),
|
|
810
|
+
args: {
|
|
811
|
+
variant: 'default',
|
|
812
|
+
},
|
|
813
|
+
};
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
### Required stories
|
|
817
|
+
|
|
818
|
+
Every component must include stories for:
|
|
819
|
+
|
|
820
|
+
- Default state
|
|
821
|
+
- Each variant
|
|
822
|
+
- Each size (if applicable)
|
|
823
|
+
- Disabled state (if applicable)
|
|
824
|
+
- Loading state (if applicable)
|
|
825
|
+
- An `AllVariants` story showing all variants side by side
|
|
826
|
+
- Accessibility example (if the component has ARIA considerations)
|
|
827
|
+
|
|
828
|
+
### When using other components or directives
|
|
829
|
+
|
|
830
|
+
Use `moduleMetadata` and `applicationConfig` decorators:
|
|
831
|
+
|
|
832
|
+
```ts
|
|
833
|
+
decorators: [
|
|
834
|
+
moduleMetadata({
|
|
835
|
+
imports: [IconComponent, FkAlertIconStartDirective],
|
|
836
|
+
}),
|
|
837
|
+
applicationConfig({
|
|
838
|
+
providers: [
|
|
839
|
+
provideIcons({
|
|
840
|
+
"shield-alert": shieldAlert,
|
|
841
|
+
}),
|
|
842
|
+
],
|
|
843
|
+
}),
|
|
844
|
+
],
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### Storybook theming
|
|
848
|
+
|
|
849
|
+
The `ui-ng` Storybook target loads `packages/tokens/src/scss/tokens.base.scss` and `tokens.dark.scss` (configured in `project.json`), so components render with the **default `@frame-kit/tokens` theme** applied — the same opinion a consumer gets out of the box. To sanity-check the no-theme baseline (fallback values only), temporarily remove those entries from the storybook `styles` array; every component must still render correctly because every token reference carries a two-tier fallback.
|
|
850
|
+
|
|
851
|
+
---
|
|
852
|
+
|
|
853
|
+
## README Documentation
|
|
854
|
+
|
|
855
|
+
Every component must have a `README.md` following this exact section order. Do not skip sections. Do not reorder.
|
|
856
|
+
|
|
857
|
+
### 1. Title + One-Line Summary
|
|
858
|
+
|
|
859
|
+
```markdown
|
|
860
|
+
# fk-example
|
|
861
|
+
|
|
862
|
+
A token-driven example component for [describe primary use case].
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
### 2. API
|
|
866
|
+
|
|
867
|
+
Break into subsections:
|
|
868
|
+
|
|
869
|
+
- **Inputs** — table with Input, Type, Default, Description columns
|
|
870
|
+
- **Outputs** — table with Output, Type, Description columns (or "None." if no outputs)
|
|
871
|
+
- **Content** — describe projection slots with code example
|
|
872
|
+
|
|
873
|
+
```markdown
|
|
874
|
+
## API
|
|
875
|
+
|
|
876
|
+
### Inputs
|
|
877
|
+
|
|
878
|
+
| Input | Type | Default | Description |
|
|
879
|
+
| ----------- | -------------- | ----------- | ------------------------------ |
|
|
880
|
+
| `variant` | `BadgeVariant` | `"default"` | Visual style of the badge |
|
|
881
|
+
| `className` | `string` | `''` | Additional CSS classes on host |
|
|
882
|
+
|
|
883
|
+
### Outputs
|
|
884
|
+
|
|
885
|
+
None.
|
|
886
|
+
|
|
887
|
+
### Content
|
|
888
|
+
|
|
889
|
+
`fk-badge` projects its content:
|
|
890
|
+
|
|
891
|
+
\`\`\`html
|
|
892
|
+
<fk-badge>New</fk-badge>
|
|
893
|
+
\`\`\`
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
### 3. Features
|
|
897
|
+
|
|
898
|
+
Short bullet list of capabilities.
|
|
899
|
+
|
|
900
|
+
### 4. Quick Start
|
|
901
|
+
|
|
902
|
+
The smallest useful example.
|
|
903
|
+
|
|
904
|
+
### 5. Import
|
|
905
|
+
|
|
906
|
+
Show import path and component registration:
|
|
907
|
+
|
|
908
|
+
```markdown
|
|
909
|
+
## Import
|
|
910
|
+
|
|
911
|
+
\`\`\`ts
|
|
912
|
+
import { BadgeComponent } from "@frame-kit/ui-ng";
|
|
913
|
+
\`\`\`
|
|
914
|
+
|
|
915
|
+
\`\`\`ts
|
|
916
|
+
@Component({
|
|
917
|
+
selector: "app-example",
|
|
918
|
+
imports: [BadgeComponent],
|
|
919
|
+
templateUrl: "./example.component.html",
|
|
920
|
+
})
|
|
921
|
+
export class ExampleComponent {}
|
|
922
|
+
\`\`\`
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
The import path is always `@frame-kit/ui-ng`.
|
|
926
|
+
|
|
927
|
+
### 6. Selector
|
|
928
|
+
|
|
929
|
+
Show the element selector.
|
|
930
|
+
|
|
931
|
+
### 7. Examples
|
|
932
|
+
|
|
933
|
+
One subsection per variant or use case with code blocks.
|
|
934
|
+
|
|
935
|
+
### 8. Accessibility
|
|
936
|
+
|
|
937
|
+
Document semantic element, keyboard behavior, ARIA attributes, disabled behavior, recommendations and anti-patterns.
|
|
938
|
+
|
|
939
|
+
### 9. Design Tokens
|
|
940
|
+
|
|
941
|
+
List the tokens the component consumes. Show how to override them:
|
|
942
|
+
|
|
943
|
+
```markdown
|
|
944
|
+
## Design Tokens
|
|
945
|
+
|
|
946
|
+
\`\`\`scss
|
|
947
|
+
--fk-badge-font-size
|
|
948
|
+
--fk-badge-primary-bg
|
|
949
|
+
--fk-badge-primary-color
|
|
950
|
+
\`\`\`
|
|
951
|
+
|
|
952
|
+
Override in your app stylesheet:
|
|
953
|
+
|
|
954
|
+
\`\`\`scss
|
|
955
|
+
:root {
|
|
956
|
+
--fk-badge-radius: 9999px;
|
|
957
|
+
}
|
|
958
|
+
\`\`\`
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
### 10. Behavior Notes
|
|
962
|
+
|
|
963
|
+
Edge cases, default behaviors, and recommendations. Keep short.
|
|
964
|
+
|
|
965
|
+
---
|
|
966
|
+
|
|
967
|
+
## Public API Exports
|
|
968
|
+
|
|
969
|
+
There are two levels of barrel: the component's own entry-point `index.ts`, and the primary `index.ts` that aggregates all entry points.
|
|
970
|
+
|
|
971
|
+
### Adding a new component to the public API
|
|
972
|
+
|
|
973
|
+
For a component in `core/ui/directives/docs/layouts/services` (its own entry point):
|
|
974
|
+
|
|
975
|
+
1. **Write the entry-point `index.ts`** in the component folder, exporting its public files:
|
|
976
|
+
|
|
977
|
+
```ts
|
|
978
|
+
export * from './<name>.component';
|
|
979
|
+
export * from './<name>.types';
|
|
980
|
+
// + associated directives
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
2. **Add `ng-package.json`** in the same folder: `{ "lib": { "entryFile": "index.ts" } }`.
|
|
984
|
+
|
|
985
|
+
3. **Add one line to the primary `packages/ui-ng/index.ts`**, re-exporting the new entry point so it's reachable via the `@frame-kit/ui-ng` barrel:
|
|
986
|
+
|
|
987
|
+
```ts
|
|
988
|
+
export * from '@frame-kit/ui-ng/<layer>/<name>';
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
For a component under `forms/` (part of the single `forms` entry point): skip steps 1–3. Instead add its public files to the relevant sub-barrel (`forms/composition/index.ts`, `forms/fields/index.ts`, or `forms/engine/index.ts`). The primary barrel already re-exports `@frame-kit/ui-ng/forms`.
|
|
992
|
+
|
|
993
|
+
ng-packagr discovers entry points by globbing for `ng-package.json`, so once the two files exist the new entry point is built automatically — no other config changes needed.
|
|
994
|
+
|
|
995
|
+
### What to export (from the entry-point `index.ts`)
|
|
996
|
+
|
|
997
|
+
- The component class
|
|
998
|
+
- The types file
|
|
999
|
+
- Any directives associated with the component
|
|
1000
|
+
|
|
1001
|
+
### What NOT to export
|
|
1002
|
+
|
|
1003
|
+
- Internal utilities or helpers
|
|
1004
|
+
- Test fixtures
|
|
1005
|
+
- Storybook stories
|
|
1006
|
+
|
|
1007
|
+
---
|
|
1008
|
+
|
|
1009
|
+
## Accessibility
|
|
1010
|
+
|
|
1011
|
+
### Required for every component
|
|
1012
|
+
|
|
1013
|
+
- Use semantic HTML elements (`button`, `a`, headings, `nav`, `dialog`)
|
|
1014
|
+
- Support `ariaLabel` and `ariaDescribedBy` inputs
|
|
1015
|
+
- Use tokenized focus styles via `--fk-focus-ring`
|
|
1016
|
+
- Apply `disabled` or `aria-disabled` correctly
|
|
1017
|
+
- Ensure keyboard navigation works (Enter, Space, Escape, Tab as appropriate)
|
|
1018
|
+
- Mark decorative elements as `aria-hidden="true"` (typically via directive host binding)
|
|
1019
|
+
|
|
1020
|
+
### Focus styling
|
|
1021
|
+
|
|
1022
|
+
All focusable elements must use the shared focus ring token:
|
|
1023
|
+
|
|
1024
|
+
```scss
|
|
1025
|
+
&:focus-visible {
|
|
1026
|
+
outline: none;
|
|
1027
|
+
box-shadow: var(--fk-focus-ring, 0 0 0 3px rgba(10, 132, 255, 0.18));
|
|
1028
|
+
}
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
---
|
|
1032
|
+
|
|
1033
|
+
## Definition of Done
|
|
1034
|
+
|
|
1035
|
+
Before a component can be merged, every item must be checked:
|
|
1036
|
+
|
|
1037
|
+
### Structure
|
|
1038
|
+
|
|
1039
|
+
- [ ] Placed in the correct layer (`core/`, `ui/`, `directives/`, `docs/`, `forms/{engine,fields,composition}/`, or `layouts/`) following the dependency rules
|
|
1040
|
+
- [ ] All required files present (`.ts`, `.html`, `.scss`, `.spec.ts`, `.stories.ts`, `.types.ts`, `README.md`)
|
|
1041
|
+
- [ ] Entry-point files present (`index.ts` + `ng-package.json`) — for non-`forms` layers; `forms` components are added to the forms sub-barrels instead
|
|
1042
|
+
- [ ] Cross-component imports use the `@frame-kit/ui-ng/<layer>/<name>` package path, never a relative `../` across an entry-point boundary
|
|
1043
|
+
|
|
1044
|
+
### Component
|
|
1045
|
+
|
|
1046
|
+
- [ ] `standalone: true`
|
|
1047
|
+
- [ ] `ChangeDetectionStrategy.OnPush`
|
|
1048
|
+
- [ ] Signal-based inputs with `input<Type>(default)`
|
|
1049
|
+
- [ ] `@HostBinding("class")` with computed classes pattern
|
|
1050
|
+
- [ ] Consumer `className` merged without replacing hook classes
|
|
1051
|
+
- [ ] Base class `fk-<name>` and modifier classes `fk-<name>--<modifier>`
|
|
1052
|
+
|
|
1053
|
+
### Styling
|
|
1054
|
+
|
|
1055
|
+
- [ ] Consumes only `--fk-*` tokens
|
|
1056
|
+
- [ ] Two-tier fallback on every token reference
|
|
1057
|
+
- [ ] No `::ng-deep`
|
|
1058
|
+
- [ ] No token definitions in component SCSS
|
|
1059
|
+
- [ ] No hardcoded hex colors as primary values
|
|
1060
|
+
- [ ] Top-margin-only spacing convention followed
|
|
1061
|
+
- [ ] No host margin on field-level components — wrap consumers in a flex/grid container and use `gap` instead (see "Field-level vs container-level spacing")
|
|
1062
|
+
- [ ] Renders correctly with no theme file loaded
|
|
1063
|
+
|
|
1064
|
+
### Theme
|
|
1065
|
+
|
|
1066
|
+
- [ ] Tokens added to `tokens.base.scss` if needed
|
|
1067
|
+
- [ ] Dark mode overrides added to `tokens.dark.scss` if component has color tokens
|
|
1068
|
+
- [ ] Verified in both light and dark modes
|
|
1069
|
+
|
|
1070
|
+
### Tests
|
|
1071
|
+
|
|
1072
|
+
- [ ] `should create` test
|
|
1073
|
+
- [ ] Default input values tested
|
|
1074
|
+
- [ ] Host class presence tested
|
|
1075
|
+
- [ ] Variant classes tested
|
|
1076
|
+
- [ ] Content projection tested if applicable
|
|
1077
|
+
- [ ] Base props passthrough tested
|
|
1078
|
+
- [ ] All tests pass
|
|
1079
|
+
|
|
1080
|
+
### Storybook
|
|
1081
|
+
|
|
1082
|
+
- [ ] Default story
|
|
1083
|
+
- [ ] Story for each variant
|
|
1084
|
+
- [ ] Story for each size if applicable
|
|
1085
|
+
- [ ] Disabled story if applicable
|
|
1086
|
+
- [ ] Loading story if applicable
|
|
1087
|
+
- [ ] `AllVariants` composite story
|
|
1088
|
+
- [ ] Stories render correctly in Storybook with fallback defaults
|
|
1089
|
+
|
|
1090
|
+
### Documentation
|
|
1091
|
+
|
|
1092
|
+
- [ ] README follows the exact section order defined in this guide
|
|
1093
|
+
- [ ] API table is complete with all inputs, outputs, and content slots
|
|
1094
|
+
- [ ] Design tokens documented with override examples
|
|
1095
|
+
- [ ] Accessibility behavior documented
|
|
1096
|
+
- [ ] Import path uses `@frame-kit/ui-ng`
|
|
1097
|
+
|
|
1098
|
+
### Public API
|
|
1099
|
+
|
|
1100
|
+
- [ ] Component, types, and any associated directives exported from the component's entry-point `index.ts` (or the forms sub-barrel for `forms` components)
|
|
1101
|
+
- [ ] New entry point re-exported from the primary `packages/ui-ng/index.ts` (skip for `forms` — already aggregated)
|
|
1102
|
+
- [ ] `nx build ui-ng` succeeds and emits the new entry point's FESM + `exports` map entry
|