semmet-angular 0.13.0 → 0.15.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.
- package/README.md +51 -7
- package/package.json +1 -1
- package/src/badge/files/__name@dasherize__/__name@dasherize__.html.template +12 -1
- package/src/badge/files/__name@dasherize__/__name@dasherize__.ts.template +11 -2
- package/src/button/files/__name@dasherize__/__name@dasherize__.css.template +5 -0
- package/src/button/files/__name@dasherize__/__name@dasherize__.html.template +21 -1
- package/src/button/files/__name@dasherize__/__name@dasherize__.ts.template +27 -2
- package/src/button-group/files/__name@dasherize__/__name@dasherize__.html.template +14 -1
- package/src/button-group/files/__name@dasherize__/__name@dasherize__.ts.template +18 -9
- package/src/card/files/__name@dasherize__/__name@dasherize__.css.template +20 -3
- package/src/card/files/__name@dasherize__/__name@dasherize__.html.template +56 -6
- package/src/card/files/__name@dasherize__/__name@dasherize__.ts.template +45 -2
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.css.template +17 -2
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.html.template +17 -1
- package/src/navbar/files/__name@dasherize__/__name@dasherize__.ts.template +20 -3
- package/src/table/files/__name@dasherize__/__name@dasherize__.html.template +24 -3
- package/src/table/files/__name@dasherize__/__name@dasherize__.ts.template +24 -5
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 `
|
|
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,
|
|
97
|
-
| `ng generate semmet-angular:button-group <name>` | Button Group | Native grouped buttons with `role="group"
|
|
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
|
|
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 |
|
|
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/) |
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -5,6 +5,11 @@ Import <%= classify(name) %> in that parent component.
|
|
|
5
5
|
|
|
6
6
|
<<%= selector %> label="New" variant="success" />
|
|
7
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 %>>
|
|
8
13
|
-->
|
|
9
14
|
<span
|
|
10
15
|
[id]="instanceId"
|
|
@@ -16,6 +21,12 @@ Import <%= classify(name) %> in that parent component.
|
|
|
16
21
|
<span class="semmet-badge__dot" aria-hidden="true"></span>
|
|
17
22
|
}
|
|
18
23
|
@if (!dot() || label()) {
|
|
19
|
-
<span class="semmet-badge__label">
|
|
24
|
+
<span class="semmet-badge__label">
|
|
25
|
+
@if (labelTemplate(); as labelSlot) {
|
|
26
|
+
<ng-container [ngTemplateOutlet]="labelSlot.templateRef" />
|
|
27
|
+
} @else {
|
|
28
|
+
{{ label() }}
|
|
29
|
+
}
|
|
30
|
+
</span>
|
|
20
31
|
}
|
|
21
32
|
</span>
|
|
@@ -1,17 +1,26 @@
|
|
|
1
|
-
import {
|
|
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');
|
|
@@ -5,6 +5,11 @@ Import <%= classify(name) %> in that parent component.
|
|
|
5
5
|
|
|
6
6
|
<<%= selector %> label="Save changes" variant="primary" (click)="save()" />
|
|
7
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 %>>
|
|
8
13
|
-->
|
|
9
14
|
<button
|
|
10
15
|
type="button"
|
|
@@ -17,6 +22,21 @@ Import <%= classify(name) %> in that parent component.
|
|
|
17
22
|
>
|
|
18
23
|
@if (busy()) {
|
|
19
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>
|
|
20
41
|
}
|
|
21
|
-
<span>{{ label() }}</span>
|
|
22
42
|
</button>
|
|
@@ -1,17 +1,42 @@
|
|
|
1
|
-
import {
|
|
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');
|
|
@@ -4,6 +4,12 @@ Usage from a parent component template:
|
|
|
4
4
|
Import <%= classify(name) %> in that parent component.
|
|
5
5
|
|
|
6
6
|
<<%= selector %> label="View options" />
|
|
7
|
+
<<%= selector %> label="View options" [items]="views" [(selectedIds)]="selectedViewIds" />
|
|
8
|
+
<<%= selector %> [items]="views">
|
|
9
|
+
<ng-template <%= camelize(name) %>Item let-item let-pressed="pressed">
|
|
10
|
+
{{ item.label }}
|
|
11
|
+
</ng-template>
|
|
12
|
+
</<%= selector %>>
|
|
7
13
|
-->
|
|
8
14
|
<div
|
|
9
15
|
[id]="instanceId"
|
|
@@ -20,7 +26,14 @@ Import <%= classify(name) %> in that parent component.
|
|
|
20
26
|
[attr.aria-pressed]="ariaPressed(item)"
|
|
21
27
|
(click)="activate(item)"
|
|
22
28
|
>
|
|
23
|
-
|
|
29
|
+
@if (itemTemplate(); as itemSlot) {
|
|
30
|
+
<ng-container
|
|
31
|
+
[ngTemplateOutlet]="itemSlot.templateRef"
|
|
32
|
+
[ngTemplateOutletContext]="{ $implicit: item, pressed: isPressed(item) }"
|
|
33
|
+
/>
|
|
34
|
+
} @else {
|
|
35
|
+
{{ item.label }}
|
|
36
|
+
}
|
|
24
37
|
</button>
|
|
25
38
|
}
|
|
26
39
|
</div>
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { NgTemplateOutlet } from '@angular/common';
|
|
2
|
+
import { Component, Directive, TemplateRef, booleanAttribute, computed, contentChild, inject, input, model } 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;
|
|
@@ -16,16 +17,25 @@ const DEFAULT_ITEMS: readonly <%= classify(name) %>Item[] = [
|
|
|
16
17
|
{ id: 'week', label: 'Week' },
|
|
17
18
|
{ id: 'month', label: 'Month' },
|
|
18
19
|
];
|
|
20
|
+
const DEFAULT_SELECTED_IDS = DEFAULT_ITEMS.filter((item) => item.pressed).map((item) => item.id);
|
|
19
21
|
|
|
20
22
|
let nextId = 0;
|
|
21
23
|
|
|
24
|
+
@Directive({
|
|
25
|
+
selector: 'ng-template[<%= camelize(name) %>Item]',
|
|
26
|
+
})
|
|
27
|
+
export class <%= classify(name) %>ItemTemplate {
|
|
28
|
+
readonly templateRef = inject<TemplateRef<{ $implicit: <%= classify(name) %>Item; pressed: boolean | null }>>(TemplateRef);
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
@Component({
|
|
23
32
|
selector: '<%= selector %>',
|
|
24
|
-
imports: [],
|
|
33
|
+
imports: [NgTemplateOutlet],
|
|
25
34
|
templateUrl: './<%= dasherize(name) %>.html',
|
|
26
35
|
styleUrl: './<%= dasherize(name) %>.css',
|
|
27
36
|
})
|
|
28
37
|
export class <%= classify(name) %> {
|
|
38
|
+
protected readonly itemTemplate = contentChild(<%= classify(name) %>ItemTemplate);
|
|
29
39
|
protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
|
|
30
40
|
|
|
31
41
|
readonly label = input('View options');
|
|
@@ -34,10 +44,9 @@ export class <%= classify(name) %> {
|
|
|
34
44
|
readonly size = input<<%= classify(name) %>Size>('md');
|
|
35
45
|
readonly disabled = input(false, { transform: booleanAttribute });
|
|
36
46
|
readonly items = input<readonly <%= classify(name) %>Item[]>(DEFAULT_ITEMS);
|
|
47
|
+
readonly selectedIds = model<readonly string[]>(DEFAULT_SELECTED_IDS);
|
|
37
48
|
|
|
38
|
-
protected readonly
|
|
39
|
-
new Set(DEFAULT_ITEMS.filter((item) => item.pressed).map((item) => item.id)),
|
|
40
|
-
);
|
|
49
|
+
protected readonly selectedIdSet = computed<ReadonlySet<string>>(() => new Set(this.selectedIds()));
|
|
41
50
|
|
|
42
51
|
protected readonly groupClasses = computed(() => [
|
|
43
52
|
'semmet-button-group',
|
|
@@ -50,7 +59,7 @@ export class <%= classify(name) %> {
|
|
|
50
59
|
return item.pressed ?? null;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
return this.
|
|
62
|
+
return this.selectedIdSet().has(item.id);
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
protected ariaPressed(item: <%= classify(name) %>Item): string | null {
|
|
@@ -68,7 +77,7 @@ export class <%= classify(name) %> {
|
|
|
68
77
|
}
|
|
69
78
|
|
|
70
79
|
if (this.selectionMode() === 'single') {
|
|
71
|
-
this.selectedIds.set(
|
|
80
|
+
this.selectedIds.set([item.id]);
|
|
72
81
|
return;
|
|
73
82
|
}
|
|
74
83
|
|
|
@@ -79,7 +88,7 @@ export class <%= classify(name) %> {
|
|
|
79
88
|
} else {
|
|
80
89
|
next.add(item.id);
|
|
81
90
|
}
|
|
82
|
-
return next;
|
|
91
|
+
return Array.from(next);
|
|
83
92
|
});
|
|
84
93
|
}
|
|
85
94
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
article {
|
|
16
|
-
|
|
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:
|
|
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
|
|
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,12 +1,62 @@
|
|
|
1
1
|
<!-- Semmet Angular: Card — https://www.w3.org/WAI/tutorials/page-structure/headings/ -->
|
|
2
2
|
<!--
|
|
3
3
|
Usage from a parent component template:
|
|
4
|
-
Import <%= classify(name) %> in that parent component.
|
|
4
|
+
Import <%= classify(name) %> and any slot directives you use in that parent component.
|
|
5
5
|
|
|
6
|
-
<<%= selector %> />
|
|
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 %>>
|
|
7
25
|
-->
|
|
8
|
-
<article [attr.aria-labelledby]="
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
+
}
|
|
12
62
|
</article>
|
|
@@ -1,13 +1,56 @@
|
|
|
1
|
-
import {
|
|
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
|
}
|
|
@@ -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:
|
|
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
|
}
|
|
@@ -4,10 +4,21 @@ Usage from a parent component template:
|
|
|
4
4
|
Import <%= classify(name) %> in that parent component.
|
|
5
5
|
|
|
6
6
|
<<%= selector %> brand="Acme" brandHref="/" />
|
|
7
|
+
<<%= selector %>>
|
|
8
|
+
<ng-template <%= camelize(name) %>Actions>
|
|
9
|
+
<a href="/account">Account</a>
|
|
10
|
+
</ng-template>
|
|
11
|
+
</<%= selector %>>
|
|
7
12
|
-->
|
|
8
13
|
<!-- Deliberately avoids role="menu"; regular site navigation should stay as nav/list/link semantics. -->
|
|
9
14
|
<nav class="semmet-navbar" [attr.aria-label]="label()">
|
|
10
|
-
<a class="semmet-navbar__brand" [href]="brandHref()">
|
|
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>
|
|
11
22
|
|
|
12
23
|
<button
|
|
13
24
|
type="button"
|
|
@@ -38,5 +49,10 @@ Import <%= classify(name) %> in that parent component.
|
|
|
38
49
|
</li>
|
|
39
50
|
}
|
|
40
51
|
</ul>
|
|
52
|
+
@if (actions(); as actionsSlot) {
|
|
53
|
+
<div class="semmet-navbar__actions">
|
|
54
|
+
<ng-container [ngTemplateOutlet]="actionsSlot.templateRef" />
|
|
55
|
+
</div>
|
|
56
|
+
}
|
|
41
57
|
</div>
|
|
42
58
|
</nav>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
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
|
|
|
@@ -3,10 +3,21 @@
|
|
|
3
3
|
Usage from a parent component template:
|
|
4
4
|
Import <%= classify(name) %> in that parent component.
|
|
5
5
|
|
|
6
|
-
<<%= selector %> />
|
|
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 %>>
|
|
7
12
|
-->
|
|
8
13
|
<table>
|
|
9
|
-
<caption>
|
|
14
|
+
<caption>
|
|
15
|
+
@if (captionTemplate(); as captionSlot) {
|
|
16
|
+
<ng-container [ngTemplateOutlet]="captionSlot.templateRef" />
|
|
17
|
+
} @else {
|
|
18
|
+
{{ caption() }}
|
|
19
|
+
}
|
|
20
|
+
</caption>
|
|
10
21
|
<thead>
|
|
11
22
|
<tr>
|
|
12
23
|
<th scope="col">Quarter</th>
|
|
@@ -14,11 +25,21 @@ Import <%= classify(name) %> in that parent component.
|
|
|
14
25
|
</tr>
|
|
15
26
|
</thead>
|
|
16
27
|
<tbody>
|
|
17
|
-
@for (row of rows; track row.quarter) {
|
|
28
|
+
@for (row of rows(); track row.quarter) {
|
|
18
29
|
<tr>
|
|
19
30
|
<th scope="row">{{ row.quarter }}</th>
|
|
20
31
|
<td>{{ row.revenue }}</td>
|
|
21
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>
|
|
22
43
|
}
|
|
23
44
|
</tbody>
|
|
24
45
|
</table>
|
|
@@ -1,19 +1,38 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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
|
}
|