semmet-angular 0.12.0 → 0.14.0

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.
Files changed (50) hide show
  1. package/README.md +51 -7
  2. package/package.json +1 -1
  3. package/src/alert/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  4. package/src/alert-dialog/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  5. package/src/badge/files/__name@dasherize__/__name@dasherize__.html.template +19 -1
  6. package/src/badge/files/__name@dasherize__/__name@dasherize__.ts.template +11 -2
  7. package/src/breadcrumb/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  8. package/src/button/files/__name@dasherize__/__name@dasherize__.css.template +5 -0
  9. package/src/button/files/__name@dasherize__/__name@dasherize__.html.template +28 -1
  10. package/src/button/files/__name@dasherize__/__name@dasherize__.ts.template +27 -2
  11. package/src/button-group/files/__name@dasherize__/__name@dasherize__.html.template +19 -1
  12. package/src/button-group/files/__name@dasherize__/__name@dasherize__.ts.template +12 -3
  13. package/src/card/files/__name@dasherize__/__name@dasherize__.css.template +20 -3
  14. package/src/card/files/__name@dasherize__/__name@dasherize__.html.template +60 -4
  15. package/src/card/files/__name@dasherize__/__name@dasherize__.ts.template +45 -2
  16. package/src/checkbox/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  17. package/src/close-button/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  18. package/src/combobox/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  19. package/src/dialog/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  20. package/src/form/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  21. package/src/input/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  22. package/src/input-group/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  23. package/src/landmarks/files/__name@dasherize__/__name@dasherize__.html.template +12 -0
  24. package/src/list-group/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  25. package/src/listbox/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  26. package/src/menu-button/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  27. package/src/meter/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  28. package/src/navbar/files/__name@dasherize__/__name@dasherize__.css.template +17 -2
  29. package/src/navbar/files/__name@dasherize__/__name@dasherize__.html.template +23 -1
  30. package/src/navbar/files/__name@dasherize__/__name@dasherize__.ts.template +20 -3
  31. package/src/navigation-menu/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  32. package/src/offcanvas/files/__name@dasherize__/__name@dasherize__.html.template +8 -0
  33. package/src/pagination/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  34. package/src/popover/files/__name@dasherize__/__name@dasherize__.html.template +8 -0
  35. package/src/progress-bar/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  36. package/src/radio-group/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  37. package/src/select/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  38. package/src/skeleton/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  39. package/src/skip-link/files/__name@dasherize__/__name@dasherize__.html.template +7 -0
  40. package/src/slider/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  41. package/src/spinbutton/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  42. package/src/spinner/files/__name@dasherize__/__name@dasherize__.html.template +7 -0
  43. package/src/switch/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  44. package/src/table/files/__name@dasherize__/__name@dasherize__.html.template +29 -2
  45. package/src/table/files/__name@dasherize__/__name@dasherize__.ts.template +24 -5
  46. package/src/textarea/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  47. package/src/toast/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  48. package/src/toolbar/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  49. package/src/tooltip/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
  50. package/src/tree-view/files/__name@dasherize__/__name@dasherize__.html.template +6 -0
package/README.md CHANGED
@@ -21,7 +21,7 @@ Each generated component is dropped into your project exactly like `ng generate
21
21
 
22
22
  Some generated components are containers: they own the ARIA wiring, keyboard behavior, focus management, and visual shell, while your app owns the content rendered inside them.
23
23
 
24
- `accordion`, `tabs`, `carousel`, and `disclosure` use Angular's modern projection pattern with `ng-template`, signal queries, and `NgTemplateOutlet`. This means you can place real app components inside generated components without dynamic component factories.
24
+ `accordion`, `tabs`, `carousel`, `disclosure`, `card`, `button`, `badge`, `navbar`, `table`, and `button-group` use Angular's modern projection pattern with `ng-template`, signal queries, and `NgTemplateOutlet`. This means you can place real app components inside generated components without dynamic component factories.
25
25
 
26
26
  Example after generating `faq`:
27
27
 
@@ -79,6 +79,50 @@ The same pattern applies to `tabs`, `carousel`, and `disclosure`:
79
79
 
80
80
  By default, projected panel/slide content is preserved after its first render. Set `[preserveContent]="false"` on the generated container to destroy inactive content when it closes or becomes inactive.
81
81
 
82
+ The smaller structural schematics keep their simple input APIs and add safe slots where the generated component still owns the accessible structure:
83
+
84
+ ```
85
+ <app-product-card title="Accessible components">
86
+ <ng-template productCardMedia>
87
+ <img src="/assets/card.png" alt="Generated Angular component preview" />
88
+ </ng-template>
89
+
90
+ <ng-template productCardDescription>
91
+ Generate Angular components with ARIA, keyboard behavior and signals.
92
+ </ng-template>
93
+
94
+ <ng-template productCardActions>
95
+ <a href="/docs">Read docs</a>
96
+ </ng-template>
97
+ </app-product-card>
98
+
99
+ <app-save-button label="Save">
100
+ <ng-template saveButtonLeading>
101
+ <app-save-icon aria-hidden="true" />
102
+ </ng-template>
103
+ </app-save-button>
104
+
105
+ <app-status-badge accessibleLabel="Build passed">
106
+ <ng-template statusBadgeLabel>Passed</ng-template>
107
+ </app-status-badge>
108
+
109
+ <app-main-navbar brand="Acme">
110
+ <ng-template mainNavbarActions>
111
+ <a href="/account">Account</a>
112
+ </ng-template>
113
+ </app-main-navbar>
114
+
115
+ <app-results-table caption="Quarterly revenue" [rows]="results">
116
+ <ng-template resultsTableEmpty>No revenue results yet.</ng-template>
117
+ </app-results-table>
118
+
119
+ <app-view-switcher [items]="views">
120
+ <ng-template viewSwitcherItem let-item>
121
+ {{ item.label }}
122
+ </ng-template>
123
+ </app-view-switcher>
124
+ ```
125
+
82
126
  ## Schematics (43)
83
127
 
84
128
  **Overlays & disclosure**
@@ -93,8 +137,8 @@ By default, projected panel/slide content is preserved after its first render. S
93
137
  | `ng generate semmet-angular:popover <name>` | Popover | Disclosure trigger with `aria-haspopup="dialog"`; Escape closes; outside click dismisses |
94
138
  | `ng generate semmet-angular:tooltip <name>` | [Tooltip](https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/) | Shows on hover **and** focus (required for keyboard users); hides on blur/mouseleave/Escape |
95
139
  | `ng generate semmet-angular:menu-button <name>` | [Menu Button](https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/) | Up/Down/Home/End/Escape inside the menu; closes on outside click; focus returns to the trigger |
96
- | `ng generate semmet-angular:button <name>` | Button | Native `<button>` with variant, size, disabled, busy, and optional pressed state |
97
- | `ng generate semmet-angular:button-group <name>` | Button Group | Native grouped buttons with `role="group"` and optional `aria-pressed` toggle state |
140
+ | `ng generate semmet-angular:button <name>` | Button | Native `<button>` with variant, size, disabled, busy, optional pressed state, and safe label/icon slots |
141
+ | `ng generate semmet-angular:button-group <name>` | Button Group | Native grouped buttons with `role="group"`, optional `aria-pressed` toggle state, and safe item-label projection |
98
142
  | `ng generate semmet-angular:close-button <name>` | Close Button | Native icon button with required accessible label |
99
143
 
100
144
  **Navigation & structure**
@@ -104,15 +148,15 @@ By default, projected panel/slide content is preserved after its first render. S
104
148
  | `ng generate semmet-angular:tabs <name>` | [Tabs](https://www.w3.org/WAI/ARIA/apg/patterns/tabs/) | Automatic activation; Left/Right/Home/End with roving `tabindex` |
105
149
  | `ng generate semmet-angular:breadcrumb <name>` | [Breadcrumb](https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/) | Static — plain links + `aria-current="page"` |
106
150
  | `ng generate semmet-angular:navigation-menu <name>` | [Navigation Menu (Disclosure)](https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/) | Click/Enter/Space toggles the submenu — deliberately avoids `role="menu"`, reserved for app-style menus |
107
- | `ng generate semmet-angular:navbar <name>` | Navbar | Semantic `nav`/list/link structure with responsive disclosure toggle and `aria-current` |
151
+ | `ng generate semmet-angular:navbar <name>` | Navbar | Semantic `nav`/list/link structure with responsive disclosure toggle, `aria-current`, and safe brand/action slots |
108
152
  | `ng generate semmet-angular:pagination <name>` | Pagination | Working page state; Previous/Next disable at the boundaries |
109
153
  | `ng generate semmet-angular:skip-link <name>` | [Skip Link](https://www.w3.org/WAI/WCAG21/Techniques/general/G1) | Hidden until focused, per the standard skip-link pattern |
110
154
  | `ng generate semmet-angular:landmarks <name>` | [Landmarks (Page Skeleton)](https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/) | Content-projected layout wrapper (`header`/`nav`/`main`/`aside`/`footer` slots) |
111
155
  | `ng generate semmet-angular:tree-view <name>` | [Tree View](https://www.w3.org/WAI/ARIA/apg/patterns/treeview/) | Up/Down/Left/Right/Home/End/Enter with roving `tabindex` across visible nodes |
112
156
  | `ng generate semmet-angular:toolbar <name>` | [Toolbar](https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/) | Left/Right/Home/End with roving `tabindex` |
113
- | `ng generate semmet-angular:card <name>` | Card | Static — semantic `<article>` + heading structure |
157
+ | `ng generate semmet-angular:card <name>` | Card | Semantic `<article>` with owned heading/description/action/media structure and safe projection slots |
114
158
  | `ng generate semmet-angular:list-group <name>` | List Group | Semantic list structure with action items, active state, optional badges, and disabled states |
115
- | `ng generate semmet-angular:table <name>` | [Table](https://www.w3.org/WAI/ARIA/apg/patterns/table/) | Static — caption + scoped headers |
159
+ | `ng generate semmet-angular:table <name>` | [Table](https://www.w3.org/WAI/ARIA/apg/patterns/table/) | Caption + scoped headers, input rows, and safe caption/empty-state slots |
116
160
 
117
161
  **Forms & inputs**
118
162
 
@@ -140,7 +184,7 @@ By default, projected panel/slide content is preserved after its first render. S
140
184
  | `ng generate semmet-angular:alert <name>` | [Alert](https://www.w3.org/WAI/ARIA/apg/patterns/alert/) | Dismissible assertive live region |
141
185
  | `ng generate semmet-angular:toast <name>` | Toast / Notification | Auto-dismisses after 5s (cleaned up via `DestroyRef`); dismissible early |
142
186
  | `ng generate semmet-angular:skeleton <name>` | Skeleton Loading | Composable loading placeholder for text, media, circles, cards, and custom blocks |
143
- | `ng generate semmet-angular:badge <name>` | Badge | Compact status label/counter with decorative mode and optional accessible label |
187
+ | `ng generate semmet-angular:badge <name>` | Badge | Compact status label/counter with decorative mode, optional accessible label, and safe label projection |
144
188
  | `ng generate semmet-angular:spinner <name>` | Spinner | Loading status with `role="status"`, visually hidden label by default, and reduced-motion support |
145
189
  | `ng generate semmet-angular:carousel <name>` | [Carousel](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/) | Previous/Next controls; `aria-live="polite"` slide region |
146
190
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "semmet-angular",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "Angular schematics that generate real, ARIA-conformant standalone components (Accordion, Tabs, Dialog, Disclosure, Tooltip...) with signals-based state and keyboard interaction built in.",
5
5
  "publisher": "danilodevsilva",
6
6
  "license": "MIT",
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Alert — https://www.w3.org/WAI/ARIA/apg/patterns/alert/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  @if (visible()) {
3
9
  <div role="alert" [id]="instanceId">
4
10
  <span>Your changes have been saved.</span>
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Alert Dialog — https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <button type="button" (click)="open($event)">Delete item</button>
3
9
 
4
10
  @if (isOpen()) {
@@ -1,4 +1,16 @@
1
1
  <!-- Semmet Angular: Badge - compact text metadata, status labels, or counters. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> label="New" variant="success" />
7
+ <<%= selector %> label="7" accessibleLabel="7 unread notifications" />
8
+ <<%= selector %> accessibleLabel="Build passed">
9
+ <ng-template <%= camelize(name) %>Label>
10
+ Passed
11
+ </ng-template>
12
+ </<%= selector %>>
13
+ -->
2
14
  <span
3
15
  [id]="instanceId"
4
16
  [class]="badgeClasses()"
@@ -9,6 +21,12 @@
9
21
  <span class="semmet-badge__dot" aria-hidden="true"></span>
10
22
  }
11
23
  @if (!dot() || label()) {
12
- <span class="semmet-badge__label">{{ label() }}</span>
24
+ <span class="semmet-badge__label">
25
+ @if (labelTemplate(); as labelSlot) {
26
+ <ng-container [ngTemplateOutlet]="labelSlot.templateRef" />
27
+ } @else {
28
+ {{ label() }}
29
+ }
30
+ </span>
13
31
  }
14
32
  </span>
@@ -1,17 +1,26 @@
1
- import { Component, booleanAttribute, computed, input } from '@angular/core';
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import { Component, Directive, TemplateRef, booleanAttribute, computed, contentChild, inject, input } from '@angular/core';
2
3
 
3
4
  type <%= classify(name) %>Variant = 'neutral' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
4
5
  type <%= classify(name) %>Size = 'sm' | 'md' | 'lg';
5
6
 
6
7
  let nextId = 0;
7
8
 
9
+ @Directive({
10
+ selector: 'ng-template[<%= camelize(name) %>Label]',
11
+ })
12
+ export class <%= classify(name) %>Label {
13
+ readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
14
+ }
15
+
8
16
  @Component({
9
17
  selector: '<%= selector %>',
10
- imports: [],
18
+ imports: [NgTemplateOutlet],
11
19
  templateUrl: './<%= dasherize(name) %>.html',
12
20
  styleUrl: './<%= dasherize(name) %>.css',
13
21
  })
14
22
  export class <%= classify(name) %> {
23
+ protected readonly labelTemplate = contentChild(<%= classify(name) %>Label);
15
24
  protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
16
25
 
17
26
  readonly label = input('Badge');
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Breadcrumb — https://www.w3.org/WAI/ARIA/apg/patterns/breadcrumb/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <nav aria-label="Breadcrumb">
3
9
  <ol>
4
10
  @for (item of items; track item.id) {
@@ -108,6 +108,11 @@
108
108
  animation: semmet-button-spin 700ms linear infinite;
109
109
  }
110
110
 
111
+ .semmet-button__slot {
112
+ display: inline-flex;
113
+ align-items: center;
114
+ }
115
+
111
116
  @keyframes semmet-button-spin {
112
117
  to {
113
118
  transform: rotate(360deg);
@@ -1,4 +1,16 @@
1
1
  <!-- Semmet Angular: Button - native <button> keeps form, focus, and keyboard semantics. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> label="Save changes" variant="primary" (click)="save()" />
7
+ <<%= selector %> label="Saving" [busy]="isSaving()" />
8
+ <<%= selector %> label="Upload">
9
+ <ng-template <%= camelize(name) %>Leading>
10
+ <app-upload-icon aria-hidden="true" />
11
+ </ng-template>
12
+ </<%= selector %>>
13
+ -->
2
14
  <button
3
15
  type="button"
4
16
  [id]="instanceId"
@@ -10,6 +22,21 @@
10
22
  >
11
23
  @if (busy()) {
12
24
  <span class="semmet-button__spinner" aria-hidden="true"></span>
25
+ } @else if (leading(); as leadingSlot) {
26
+ <span class="semmet-button__slot" aria-hidden="true">
27
+ <ng-container [ngTemplateOutlet]="leadingSlot.templateRef" />
28
+ </span>
29
+ }
30
+ <span>
31
+ @if (labelTemplate(); as labelSlot) {
32
+ <ng-container [ngTemplateOutlet]="labelSlot.templateRef" />
33
+ } @else {
34
+ {{ label() }}
35
+ }
36
+ </span>
37
+ @if (trailing(); as trailingSlot) {
38
+ <span class="semmet-button__slot" aria-hidden="true">
39
+ <ng-container [ngTemplateOutlet]="trailingSlot.templateRef" />
40
+ </span>
13
41
  }
14
- <span>{{ label() }}</span>
15
42
  </button>
@@ -1,17 +1,42 @@
1
- import { Component, computed, input, output } from '@angular/core';
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import { Component, Directive, TemplateRef, computed, contentChild, inject, input, output } from '@angular/core';
2
3
 
3
4
  type <%= classify(name) %>Variant = 'primary' | 'secondary' | 'danger' | 'ghost';
4
5
  type <%= classify(name) %>Size = 'sm' | 'md' | 'lg';
5
6
 
6
7
  let nextId = 0;
7
8
 
9
+ @Directive({
10
+ selector: 'ng-template[<%= camelize(name) %>Leading]',
11
+ })
12
+ export class <%= classify(name) %>Leading {
13
+ readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
14
+ }
15
+
16
+ @Directive({
17
+ selector: 'ng-template[<%= camelize(name) %>Label]',
18
+ })
19
+ export class <%= classify(name) %>Label {
20
+ readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
21
+ }
22
+
23
+ @Directive({
24
+ selector: 'ng-template[<%= camelize(name) %>Trailing]',
25
+ })
26
+ export class <%= classify(name) %>Trailing {
27
+ readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
28
+ }
29
+
8
30
  @Component({
9
31
  selector: '<%= selector %>',
10
- imports: [],
32
+ imports: [NgTemplateOutlet],
11
33
  templateUrl: './<%= dasherize(name) %>.html',
12
34
  styleUrl: './<%= dasherize(name) %>.css',
13
35
  })
14
36
  export class <%= classify(name) %> {
37
+ protected readonly leading = contentChild(<%= classify(name) %>Leading);
38
+ protected readonly labelTemplate = contentChild(<%= classify(name) %>Label);
39
+ protected readonly trailing = contentChild(<%= classify(name) %>Trailing);
15
40
  protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
16
41
 
17
42
  readonly label = input('Continue');
@@ -1,4 +1,15 @@
1
1
  <!-- Semmet Angular: Button Group - grouped native buttons with optional toggle state. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> label="View options" />
7
+ <<%= selector %> [items]="views">
8
+ <ng-template <%= camelize(name) %>Item let-item let-pressed="pressed">
9
+ {{ item.label }}
10
+ </ng-template>
11
+ </<%= selector %>>
12
+ -->
2
13
  <div
3
14
  [id]="instanceId"
4
15
  [class]="groupClasses()"
@@ -14,7 +25,14 @@
14
25
  [attr.aria-pressed]="ariaPressed(item)"
15
26
  (click)="activate(item)"
16
27
  >
17
- {{ item.label }}
28
+ @if (itemTemplate(); as itemSlot) {
29
+ <ng-container
30
+ [ngTemplateOutlet]="itemSlot.templateRef"
31
+ [ngTemplateOutletContext]="{ $implicit: item, pressed: isPressed(item) }"
32
+ />
33
+ } @else {
34
+ {{ item.label }}
35
+ }
18
36
  </button>
19
37
  }
20
38
  </div>
@@ -1,10 +1,11 @@
1
- import { Component, booleanAttribute, computed, input, signal } from '@angular/core';
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import { Component, Directive, TemplateRef, booleanAttribute, computed, contentChild, inject, input, signal } from '@angular/core';
2
3
 
3
4
  type <%= classify(name) %>Orientation = 'horizontal' | 'vertical';
4
5
  type <%= classify(name) %>SelectionMode = 'none' | 'single' | 'multiple';
5
6
  type <%= classify(name) %>Size = 'sm' | 'md' | 'lg';
6
7
 
7
- interface <%= classify(name) %>Item {
8
+ export interface <%= classify(name) %>Item {
8
9
  id: string;
9
10
  label: string;
10
11
  disabled?: boolean;
@@ -19,13 +20,21 @@ const DEFAULT_ITEMS: readonly <%= classify(name) %>Item[] = [
19
20
 
20
21
  let nextId = 0;
21
22
 
23
+ @Directive({
24
+ selector: 'ng-template[<%= camelize(name) %>Item]',
25
+ })
26
+ export class <%= classify(name) %>ItemTemplate {
27
+ readonly templateRef = inject<TemplateRef<{ $implicit: <%= classify(name) %>Item; pressed: boolean | null }>>(TemplateRef);
28
+ }
29
+
22
30
  @Component({
23
31
  selector: '<%= selector %>',
24
- imports: [],
32
+ imports: [NgTemplateOutlet],
25
33
  templateUrl: './<%= dasherize(name) %>.html',
26
34
  styleUrl: './<%= dasherize(name) %>.css',
27
35
  })
28
36
  export class <%= classify(name) %> {
37
+ protected readonly itemTemplate = contentChild(<%= classify(name) %>ItemTemplate);
29
38
  protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
30
39
 
31
40
  readonly label = input('View options');
@@ -13,7 +13,7 @@
13
13
  }
14
14
 
15
15
  article {
16
- padding: 1.25rem;
16
+ overflow: hidden;
17
17
  background: var(--semmet-color-surface);
18
18
  border: 1px solid var(--semmet-color-outline);
19
19
  border-radius: var(--semmet-radius-md);
@@ -21,17 +21,34 @@ article {
21
21
  }
22
22
 
23
23
  h3 {
24
- margin: 0 0 0.5rem;
24
+ margin: 1.25rem 1.25rem 0.5rem;
25
25
  font-size: 1.0625rem;
26
26
  font-weight: 500;
27
27
  color: var(--semmet-color-on-surface);
28
28
  }
29
29
 
30
30
  p {
31
- margin: 0 0 1rem;
31
+ margin: 0;
32
+ }
33
+
34
+ .semmet-card__media :is(img, video, svg) {
35
+ display: block;
36
+ width: 100%;
37
+ height: auto;
38
+ }
39
+
40
+ .semmet-card__description {
41
+ margin: 0 1.25rem 1rem;
32
42
  color: var(--semmet-color-on-surface-variant);
33
43
  }
34
44
 
45
+ .semmet-card__actions {
46
+ display: flex;
47
+ flex-wrap: wrap;
48
+ gap: 0.75rem;
49
+ margin: 0 1.25rem 1.25rem;
50
+ }
51
+
35
52
  a {
36
53
  color: var(--semmet-color-primary);
37
54
  font-weight: 500;
@@ -1,6 +1,62 @@
1
1
  <!-- Semmet Angular: Card — https://www.w3.org/WAI/tutorials/page-structure/headings/ -->
2
- <article [attr.aria-labelledby]="instanceId + '-title'">
3
- <h3 [id]="instanceId + '-title'">Card title</h3>
4
- <p>Card description text.</p>
5
- <a href="#">Learn more</a>
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> and any slot directives you use in that parent component.
5
+
6
+ <<%= selector %> title="Accessible components" description="Generate Angular components with ARIA." />
7
+
8
+ <<%= selector %>>
9
+ <ng-template <%= camelize(name) %>Media>
10
+ <img src="..." alt="..." />
11
+ </ng-template>
12
+
13
+ <ng-template <%= camelize(name) %>Title>
14
+ Accessible components
15
+ </ng-template>
16
+
17
+ <ng-template <%= camelize(name) %>Description>
18
+ Generate Angular components with ARIA, keyboard behavior and signals.
19
+ </ng-template>
20
+
21
+ <ng-template <%= camelize(name) %>Actions>
22
+ <a href="/docs">Read docs</a>
23
+ </ng-template>
24
+ </<%= selector %>>
25
+ -->
26
+ <article [attr.aria-labelledby]="labelledBy()">
27
+ @if (media(); as mediaSlot) {
28
+ <div class="semmet-card__media">
29
+ <ng-container [ngTemplateOutlet]="mediaSlot.templateRef" />
30
+ </div>
31
+ }
32
+
33
+ @if (hasTitle()) {
34
+ <h3 [id]="instanceId + '-title'">
35
+ @if (titleTemplate(); as titleSlot) {
36
+ <ng-container [ngTemplateOutlet]="titleSlot.templateRef" />
37
+ } @else {
38
+ {{ title() }}
39
+ }
40
+ </h3>
41
+ }
42
+
43
+ @if (hasDescription()) {
44
+ <div class="semmet-card__description">
45
+ @if (descriptionTemplate(); as descriptionSlot) {
46
+ <ng-container [ngTemplateOutlet]="descriptionSlot.templateRef" />
47
+ } @else {
48
+ <p>{{ description() }}</p>
49
+ }
50
+ </div>
51
+ }
52
+
53
+ @if (hasActions()) {
54
+ <div class="semmet-card__actions">
55
+ @if (actions(); as actionsSlot) {
56
+ <ng-container [ngTemplateOutlet]="actionsSlot.templateRef" />
57
+ } @else {
58
+ <a [href]="linkHref()">{{ linkLabel() }}</a>
59
+ }
60
+ </div>
61
+ }
6
62
  </article>
@@ -1,13 +1,56 @@
1
- import { Component } from '@angular/core';
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import { Component, Directive, TemplateRef, computed, contentChild, inject, input } from '@angular/core';
2
3
 
3
4
  let nextId = 0;
4
5
 
6
+ @Directive({
7
+ selector: 'ng-template[<%= camelize(name) %>Media]',
8
+ })
9
+ export class <%= classify(name) %>Media {
10
+ readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
11
+ }
12
+
13
+ @Directive({
14
+ selector: 'ng-template[<%= camelize(name) %>Title]',
15
+ })
16
+ export class <%= classify(name) %>Title {
17
+ readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
18
+ }
19
+
20
+ @Directive({
21
+ selector: 'ng-template[<%= camelize(name) %>Description]',
22
+ })
23
+ export class <%= classify(name) %>Description {
24
+ readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
25
+ }
26
+
27
+ @Directive({
28
+ selector: 'ng-template[<%= camelize(name) %>Actions]',
29
+ })
30
+ export class <%= classify(name) %>Actions {
31
+ readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
32
+ }
33
+
5
34
  @Component({
6
35
  selector: '<%= selector %>',
7
- imports: [],
36
+ imports: [NgTemplateOutlet],
8
37
  templateUrl: './<%= dasherize(name) %>.html',
9
38
  styleUrl: './<%= dasherize(name) %>.css',
10
39
  })
11
40
  export class <%= classify(name) %> {
41
+ protected readonly media = contentChild(<%= classify(name) %>Media);
42
+ protected readonly titleTemplate = contentChild(<%= classify(name) %>Title);
43
+ protected readonly descriptionTemplate = contentChild(<%= classify(name) %>Description);
44
+ protected readonly actions = contentChild(<%= classify(name) %>Actions);
12
45
  protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
46
+
47
+ readonly title = input('Card title');
48
+ readonly description = input('Card description text.');
49
+ readonly linkHref = input('#');
50
+ readonly linkLabel = input('Learn more');
51
+
52
+ protected readonly hasTitle = computed(() => Boolean(this.titleTemplate()) || this.title().trim().length > 0);
53
+ protected readonly labelledBy = computed(() => this.hasTitle() ? `${this.instanceId}-title` : null);
54
+ protected readonly hasDescription = computed(() => Boolean(this.descriptionTemplate()) || this.description().trim().length > 0);
55
+ protected readonly hasActions = computed(() => Boolean(this.actions()) || this.linkLabel().trim().length > 0);
13
56
  }
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Checkbox — https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <div
3
9
  role="checkbox"
4
10
  [attr.aria-checked]="checked()"
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Close Button - native button with a required accessible name. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> label="Close dialog" (click)="close()" />
7
+ -->
2
8
  <button
3
9
  type="button"
4
10
  [id]="instanceId"
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Combobox — https://www.w3.org/WAI/ARIA/apg/patterns/combobox/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <label [id]="instanceId + '-label'" [for]="instanceId + '-input'">Choose a fruit</label>
3
9
  <input
4
10
  type="text"
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Dialog (Modal) — https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <button type="button" (click)="open($event)">Open dialog</button>
3
9
 
4
10
  @if (isOpen()) {
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Form - native form with labels, inline validation, and status messaging. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <form [attr.aria-labelledby]="instanceId + '-title'" (submit)="submit($event)" novalidate>
3
9
  <h2 [id]="instanceId + '-title'">Contact form</h2>
4
10
 
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Input - native <input> with label, hint, and validation wiring. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> label="Email address" [(value)]="email" [required]="true" />
7
+ -->
2
8
  <div class="semmet-field">
3
9
  <label [for]="controlId()">
4
10
  {{ label() }}
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Input Group - native input with addon text announced through aria-describedby. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> label="Website" prefix="https://" suffix=".com" [(value)]="website" />
7
+ -->
2
8
  <label class="semmet-input-group__label" [for]="instanceId">
3
9
  {{ label() }}
4
10
  @if (required()) {
@@ -1,4 +1,16 @@
1
1
  <!-- Semmet Angular: Landmarks (Page Skeleton) — https://www.w3.org/WAI/ARIA/apg/practices/landmark-regions/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %>>
7
+ <h1 header>Dashboard</h1>
8
+ <a nav routerLink="/reports">Reports</a>
9
+ <section>Primary page content</section>
10
+ <aside aside>Related actions</aside>
11
+ <small footer>Copyright 2026</small>
12
+ </<%= selector %>>
13
+ -->
2
14
  <!-- Project content into a region with the matching attribute, e.g. <div header>...</div>. Unmatched content goes to <main>. -->
3
15
  <header>
4
16
  <ng-content select="[header]"></ng-content>
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: List Group - semantic list with optional action and active states. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> label="Settings sections" />
7
+ -->
2
8
  <ul [id]="instanceId" [class]="listClasses()" [attr.aria-label]="label()">
3
9
  @for (item of items(); track item.id) {
4
10
  <li class="semmet-list-group__item">
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Listbox — https://www.w3.org/WAI/ARIA/apg/patterns/listbox/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <span [id]="instanceId + '-label'">Choose an option</span>
3
9
  <ul
4
10
  role="listbox"
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Menu Button — https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <button
3
9
  #menuButton
4
10
  type="button"
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Meter — https://www.w3.org/TR/wai-aria-1.2/#meter -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <label [for]="instanceId">Disk usage</label>
3
9
  <meter [id]="instanceId" min="0" max="100" [value]="value()" low="50" high="90" optimum="0">
4
10
  {{ value() }}%
@@ -96,6 +96,13 @@
96
96
  list-style: none;
97
97
  }
98
98
 
99
+ .semmet-navbar__actions {
100
+ display: flex;
101
+ flex-wrap: wrap;
102
+ gap: 0.5rem;
103
+ padding-top: 0.75rem;
104
+ }
105
+
99
106
  .semmet-navbar__link {
100
107
  display: flex;
101
108
  align-items: center;
@@ -128,7 +135,8 @@
128
135
 
129
136
  .semmet-navbar__brand:focus-visible,
130
137
  .semmet-navbar__toggle:focus-visible,
131
- .semmet-navbar__link:focus-visible {
138
+ .semmet-navbar__link:focus-visible,
139
+ .semmet-navbar__actions :is(a, button):focus-visible {
132
140
  outline: 2px solid var(--semmet-color-primary);
133
141
  outline-offset: 2px;
134
142
  }
@@ -144,7 +152,10 @@
144
152
  }
145
153
 
146
154
  .semmet-navbar__menu {
147
- display: block;
155
+ display: flex;
156
+ align-items: center;
157
+ justify-content: flex-end;
158
+ gap: 0.75rem;
148
159
  grid-column: auto;
149
160
  }
150
161
 
@@ -155,4 +166,8 @@
155
166
  gap: 0.25rem;
156
167
  padding: 0;
157
168
  }
169
+
170
+ .semmet-navbar__actions {
171
+ padding-top: 0;
172
+ }
158
173
  }
@@ -1,7 +1,24 @@
1
1
  <!-- Semmet Angular: Navbar - semantic navigation with responsive disclosure behavior. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> brand="Acme" brandHref="/" />
7
+ <<%= selector %>>
8
+ <ng-template <%= camelize(name) %>Actions>
9
+ <a href="/account">Account</a>
10
+ </ng-template>
11
+ </<%= selector %>>
12
+ -->
2
13
  <!-- Deliberately avoids role="menu"; regular site navigation should stay as nav/list/link semantics. -->
3
14
  <nav class="semmet-navbar" [attr.aria-label]="label()">
4
- <a class="semmet-navbar__brand" [href]="brandHref()">{{ brand() }}</a>
15
+ <a class="semmet-navbar__brand" [href]="brandHref()">
16
+ @if (brandTemplate(); as brandSlot) {
17
+ <ng-container [ngTemplateOutlet]="brandSlot.templateRef" />
18
+ } @else {
19
+ {{ brand() }}
20
+ }
21
+ </a>
5
22
 
6
23
  <button
7
24
  type="button"
@@ -32,5 +49,10 @@
32
49
  </li>
33
50
  }
34
51
  </ul>
52
+ @if (actions(); as actionsSlot) {
53
+ <div class="semmet-navbar__actions">
54
+ <ng-container [ngTemplateOutlet]="actionsSlot.templateRef" />
55
+ </div>
56
+ }
35
57
  </div>
36
58
  </nav>
@@ -1,6 +1,7 @@
1
- import { Component, computed, input, signal } from '@angular/core';
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import { Component, Directive, TemplateRef, computed, contentChild, inject, input, signal } from '@angular/core';
2
3
 
3
- interface <%= classify(name) %>Item {
4
+ export interface <%= classify(name) %>Item {
4
5
  label: string;
5
6
  href: string;
6
7
  current?: boolean;
@@ -16,13 +17,29 @@ const DEFAULT_ITEMS: readonly <%= classify(name) %>Item[] = [
16
17
 
17
18
  let nextId = 0;
18
19
 
20
+ @Directive({
21
+ selector: 'ng-template[<%= camelize(name) %>Brand]',
22
+ })
23
+ export class <%= classify(name) %>Brand {
24
+ readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
25
+ }
26
+
27
+ @Directive({
28
+ selector: 'ng-template[<%= camelize(name) %>Actions]',
29
+ })
30
+ export class <%= classify(name) %>Actions {
31
+ readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
32
+ }
33
+
19
34
  @Component({
20
35
  selector: '<%= selector %>',
21
- imports: [],
36
+ imports: [NgTemplateOutlet],
22
37
  templateUrl: './<%= dasherize(name) %>.html',
23
38
  styleUrl: './<%= dasherize(name) %>.css',
24
39
  })
25
40
  export class <%= classify(name) %> {
41
+ protected readonly brandTemplate = contentChild(<%= classify(name) %>Brand);
42
+ protected readonly actions = contentChild(<%= classify(name) %>Actions);
26
43
  protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
27
44
  protected readonly expanded = signal(false);
28
45
 
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Navigation Menu (Disclosure) — https://www.w3.org/WAI/ARIA/apg/patterns/disclosure/examples/disclosure-navigation/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <!-- Deliberately avoids the ARIA menu/menuitem roles — APG reserves those for app-style menus. -->
3
9
  <nav aria-label="Main">
4
10
  <ul>
@@ -1,4 +1,12 @@
1
1
  <!-- Semmet Angular: Offcanvas - modal drawer with dialog semantics and focus management. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> triggerLabel="Open filters" title="Filters">
7
+ <app-filter-panel />
8
+ </<%= selector %>>
9
+ -->
2
10
  <button
3
11
  type="button"
4
12
  class="semmet-offcanvas__trigger"
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Pagination — https://www.w3.org/TR/wai-aria-1.2/#aria-current -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <nav aria-label="Pagination">
3
9
  <ul>
4
10
  <li>
@@ -1,4 +1,12 @@
1
1
  <!-- Semmet Angular: Popover - disclosure trigger for short, contextual non-modal content. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> triggerLabel="More information" title="Helpful context">
7
+ <a routerLink="/help">Read the guide</a>
8
+ </<%= selector %>>
9
+ -->
2
10
  <button
3
11
  type="button"
4
12
  class="semmet-popover__trigger"
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Progress Bar — https://www.w3.org/TR/wai-aria-1.2/#progressbar -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <label [for]="instanceId">Uploading file</label>
3
9
  <progress [id]="instanceId" [value]="value()" max="100">{{ value() }}%</progress>
4
10
  <button type="button" (click)="advance()" [disabled]="isComplete()">
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Radio Group — https://www.w3.org/WAI/ARIA/apg/patterns/radio/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <div role="radiogroup" [attr.aria-labelledby]="instanceId + '-label'">
3
9
  <span [id]="instanceId + '-label'">Choose a plan</span>
4
10
  @for (option of options; track option.id; let i = $index) {
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Select - native select keeps platform semantics and form behavior. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> label="Category" [(value)]="category" [required]="true" />
7
+ -->
2
8
  <label class="semmet-select" [for]="instanceId">
3
9
  <span class="semmet-select__label">
4
10
  {{ label() }}
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Skeleton Loading - decorative placeholder for pending content. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> [lines]="3" label="Loading profile" />
7
+ -->
2
8
  @if (variant() === 'text') {
3
9
  <span
4
10
  [id]="instanceId"
@@ -1,3 +1,10 @@
1
1
  <!-- Semmet Angular: Skip Link — https://www.w3.org/WAI/WCAG21/Techniques/general/G1 -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ <main id="main-content">Main content</main>
8
+ -->
2
9
  <a class="skip-link" href="#main-content">Skip to main content</a>
3
10
  <!-- Add id="main-content" to your <main> element -->
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Slider — https://www.w3.org/WAI/ARIA/apg/patterns/slider/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <label [id]="instanceId + '-label'">Volume</label>
3
9
  <div #track class="track" (pointerdown)="handlePointerDown($event)" (pointermove)="handlePointerMove($event)">
4
10
  <div class="fill" [style.width.%]="value()"></div>
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Spinbutton — https://www.w3.org/WAI/ARIA/apg/patterns/spinbutton/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <label [id]="instanceId + '-label'" [for]="instanceId + '-input'">Quantity</label>
3
9
  <div class="stepper">
4
10
  <button type="button" (click)="decrement()" aria-label="Decrease quantity">−</button>
@@ -1,4 +1,11 @@
1
1
  <!-- Semmet Angular: Spinner - use decorative=true when another element already announces loading. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> label="Loading results" />
7
+ <<%= selector %> [decorative]="true" />
8
+ -->
2
9
  <span
3
10
  [id]="instanceId"
4
11
  [class]="spinnerClasses()"
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Switch — https://www.w3.org/WAI/ARIA/apg/patterns/switch/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <button type="button" role="switch" [attr.aria-checked]="checked()" [id]="instanceId" (click)="toggle()">
3
9
  Enable notifications
4
10
  </button>
@@ -1,6 +1,23 @@
1
1
  <!-- Semmet Angular: Table — https://www.w3.org/WAI/ARIA/apg/patterns/table/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> caption="Quarterly revenue" [rows]="results" />
7
+ <<%= selector %>>
8
+ <ng-template <%= camelize(name) %>Caption>
9
+ Quarterly results, fiscal year 2026
10
+ </ng-template>
11
+ </<%= selector %>>
12
+ -->
2
13
  <table>
3
- <caption>Quarterly results</caption>
14
+ <caption>
15
+ @if (captionTemplate(); as captionSlot) {
16
+ <ng-container [ngTemplateOutlet]="captionSlot.templateRef" />
17
+ } @else {
18
+ {{ caption() }}
19
+ }
20
+ </caption>
4
21
  <thead>
5
22
  <tr>
6
23
  <th scope="col">Quarter</th>
@@ -8,11 +25,21 @@
8
25
  </tr>
9
26
  </thead>
10
27
  <tbody>
11
- @for (row of rows; track row.quarter) {
28
+ @for (row of rows(); track row.quarter) {
12
29
  <tr>
13
30
  <th scope="row">{{ row.quarter }}</th>
14
31
  <td>{{ row.revenue }}</td>
15
32
  </tr>
33
+ } @empty {
34
+ <tr>
35
+ <td colspan="2">
36
+ @if (emptyTemplate(); as emptySlot) {
37
+ <ng-container [ngTemplateOutlet]="emptySlot.templateRef" />
38
+ } @else {
39
+ No results available.
40
+ }
41
+ </td>
42
+ </tr>
16
43
  }
17
44
  </tbody>
18
45
  </table>
@@ -1,19 +1,38 @@
1
- import { Component } from '@angular/core';
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import { Component, Directive, TemplateRef, contentChild, inject, input } from '@angular/core';
2
3
 
3
- interface <%= classify(name) %>Row {
4
+ export interface <%= classify(name) %>Row {
4
5
  quarter: string;
5
6
  revenue: string;
6
7
  }
7
8
 
9
+ @Directive({
10
+ selector: 'ng-template[<%= camelize(name) %>Caption]',
11
+ })
12
+ export class <%= classify(name) %>Caption {
13
+ readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
14
+ }
15
+
16
+ @Directive({
17
+ selector: 'ng-template[<%= camelize(name) %>Empty]',
18
+ })
19
+ export class <%= classify(name) %>Empty {
20
+ readonly templateRef = inject<TemplateRef<unknown>>(TemplateRef);
21
+ }
22
+
8
23
  @Component({
9
24
  selector: '<%= selector %>',
10
- imports: [],
25
+ imports: [NgTemplateOutlet],
11
26
  templateUrl: './<%= dasherize(name) %>.html',
12
27
  styleUrl: './<%= dasherize(name) %>.css',
13
28
  })
14
29
  export class <%= classify(name) %> {
15
- protected readonly rows: <%= classify(name) %>Row[] = [
30
+ protected readonly captionTemplate = contentChild(<%= classify(name) %>Caption);
31
+ protected readonly emptyTemplate = contentChild(<%= classify(name) %>Empty);
32
+
33
+ readonly caption = input('Quarterly results');
34
+ readonly rows = input<readonly <%= classify(name) %>Row[]>([
16
35
  { quarter: 'Q1', revenue: '$12,000' },
17
36
  { quarter: 'Q2', revenue: '$15,500' },
18
- ];
37
+ ]);
19
38
  }
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Textarea - native <textarea> with label, hint, counter, and validation wiring. -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> label="Message" [(value)]="message" [required]="true" />
7
+ -->
2
8
  <div class="semmet-field">
3
9
  <label [for]="controlId()">
4
10
  {{ label() }}
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Toast / Notification — https://www.w3.org/TR/wai-aria-1.2/#status -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <button type="button" (click)="show()">Save</button>
3
9
 
4
10
  @if (visible()) {
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Toolbar — https://www.w3.org/WAI/ARIA/apg/patterns/toolbar/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <div role="toolbar" aria-label="Text formatting">
3
9
  @for (action of actions; track action.id; let i = $index) {
4
10
  <button
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Tooltip — https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <button
3
9
  type="button"
4
10
  [attr.aria-describedby]="instanceId"
@@ -1,4 +1,10 @@
1
1
  <!-- Semmet Angular: Tree View — https://www.w3.org/WAI/ARIA/apg/patterns/treeview/ -->
2
+ <!--
3
+ Usage from a parent component template:
4
+ Import <%= classify(name) %> in that parent component.
5
+
6
+ <<%= selector %> />
7
+ -->
2
8
  <ul role="tree" aria-label="Files">
3
9
  @for (root of tree; track root.id) {
4
10
  <li