semmet-angular 0.8.0 → 0.10.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 +6 -1
- package/package.json +1 -1
- package/src/button/files/__name@dasherize__/__name@dasherize__.css.template +115 -0
- package/src/button/files/__name@dasherize__/__name@dasherize__.html.template +15 -0
- package/src/button/files/__name@dasherize__/__name@dasherize__.ts.template +45 -0
- package/src/button/index.d.ts +1 -0
- package/src/button/index.js +6 -0
- package/src/button/index.js.map +1 -0
- package/src/button/index.ts +3 -0
- package/src/button/schema.d.ts +1 -0
- package/src/button/schema.js +3 -0
- package/src/button/schema.js.map +1 -0
- package/src/button/schema.json +29 -0
- package/src/button/schema.ts +1 -0
- package/src/collection.json +25 -0
- package/src/form/files/__name@dasherize__/__name@dasherize__.css.template +122 -0
- package/src/form/files/__name@dasherize__/__name@dasherize__.html.template +70 -0
- package/src/form/files/__name@dasherize__/__name@dasherize__.ts.template +76 -0
- package/src/form/index.d.ts +1 -0
- package/src/form/index.js +6 -0
- package/src/form/index.js.map +1 -0
- package/src/form/index.ts +3 -0
- package/src/form/schema.d.ts +1 -0
- package/src/form/schema.js +3 -0
- package/src/form/schema.js.map +1 -0
- package/src/form/schema.json +29 -0
- package/src/form/schema.ts +1 -0
- package/src/input/files/__name@dasherize__/__name@dasherize__.css.template +80 -0
- package/src/input/files/__name@dasherize__/__name@dasherize__.html.template +32 -0
- package/src/input/files/__name@dasherize__/__name@dasherize__.ts.template +48 -0
- package/src/input/index.d.ts +1 -0
- package/src/input/index.js +6 -0
- package/src/input/index.js.map +1 -0
- package/src/input/index.ts +3 -0
- package/src/input/schema.d.ts +1 -0
- package/src/input/schema.js +3 -0
- package/src/input/schema.js.map +1 -0
- package/src/input/schema.json +29 -0
- package/src/input/schema.ts +1 -0
- package/src/skeleton/files/__name@dasherize__/__name@dasherize__.css.template +91 -0
- package/src/skeleton/files/__name@dasherize__/__name@dasherize__.html.template +31 -0
- package/src/skeleton/files/__name@dasherize__/__name@dasherize__.ts.template +69 -0
- package/src/skeleton/index.d.ts +1 -0
- package/src/skeleton/index.js +6 -0
- package/src/skeleton/index.js.map +1 -0
- package/src/skeleton/index.ts +3 -0
- package/src/skeleton/schema.d.ts +1 -0
- package/src/skeleton/schema.js +3 -0
- package/src/skeleton/schema.js.map +1 -0
- package/src/skeleton/schema.json +29 -0
- package/src/skeleton/schema.ts +1 -0
- package/src/textarea/files/__name@dasherize__/__name@dasherize__.css.template +95 -0
- package/src/textarea/files/__name@dasherize__/__name@dasherize__.html.template +40 -0
- package/src/textarea/files/__name@dasherize__/__name@dasherize__.ts.template +56 -0
- package/src/textarea/index.d.ts +1 -0
- package/src/textarea/index.js +6 -0
- package/src/textarea/index.js.map +1 -0
- package/src/textarea/index.ts +3 -0
- package/src/textarea/schema.d.ts +1 -0
- package/src/textarea/schema.js +3 -0
- package/src/textarea/schema.js.map +1 -0
- package/src/textarea/schema.json +29 -0
- package/src/textarea/schema.ts +1 -0
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ 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
|
-
## Schematics (
|
|
82
|
+
## Schematics (33)
|
|
83
83
|
|
|
84
84
|
**Overlays & disclosure**
|
|
85
85
|
|
|
@@ -91,6 +91,7 @@ By default, projected panel/slide content is preserved after its first render. S
|
|
|
91
91
|
| `ng generate semmet-angular:alert-dialog <name>` | [Alert Dialog](https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/) | Same focus trap as Dialog; initial focus on the least destructive action (Cancel) |
|
|
92
92
|
| `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 |
|
|
93
93
|
| `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 |
|
|
94
|
+
| `ng generate semmet-angular:button <name>` | Button | Native `<button>` with variant, size, disabled, busy, and optional pressed state |
|
|
94
95
|
|
|
95
96
|
**Navigation & structure**
|
|
96
97
|
|
|
@@ -111,6 +112,9 @@ By default, projected panel/slide content is preserved after its first render. S
|
|
|
111
112
|
|
|
112
113
|
| Command | Generates | Keyboard/focus behavior implemented |
|
|
113
114
|
|---|---|---|
|
|
115
|
+
| `ng generate semmet-angular:form <name>` | Form | Native `<form>` with labeled controls, inline validation, and status messaging |
|
|
116
|
+
| `ng generate semmet-angular:input <name>` | Input | Native `<input>` with label, hint, validation message wiring, and signal-backed value |
|
|
117
|
+
| `ng generate semmet-angular:textarea <name>` | Textarea | Native `<textarea>` with label, hint, counter, validation message wiring, and signal-backed value |
|
|
114
118
|
| `ng generate semmet-angular:checkbox <name>` | [Checkbox](https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/) | Click/Enter/Space toggle |
|
|
115
119
|
| `ng generate semmet-angular:radio-group <name>` | [Radio Group](https://www.w3.org/WAI/ARIA/apg/patterns/radio/) | Up/Down/Left/Right/Home/End move focus and selection together |
|
|
116
120
|
| `ng generate semmet-angular:switch <name>` | [Switch](https://www.w3.org/WAI/ARIA/apg/patterns/switch/) | Native `<button>` — Click/Enter/Space toggle |
|
|
@@ -127,6 +131,7 @@ By default, projected panel/slide content is preserved after its first render. S
|
|
|
127
131
|
|---|---|---|
|
|
128
132
|
| `ng generate semmet-angular:alert <name>` | [Alert](https://www.w3.org/WAI/ARIA/apg/patterns/alert/) | Dismissible assertive live region |
|
|
129
133
|
| `ng generate semmet-angular:toast <name>` | Toast / Notification | Auto-dismisses after 5s (cleaned up via `DestroyRef`); dismissible early |
|
|
134
|
+
| `ng generate semmet-angular:skeleton <name>` | Skeleton Loading | Composable loading placeholder for text, media, circles, cards, and custom blocks |
|
|
130
135
|
| `ng generate semmet-angular:carousel <name>` | [Carousel](https://www.w3.org/WAI/ARIA/apg/patterns/carousel/) | Previous/Next controls; `aria-live="polite"` slide region |
|
|
131
136
|
|
|
132
137
|
## What every generated component looks like
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "semmet-angular",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
--semmet-color-primary: #18181b;
|
|
3
|
+
--semmet-color-on-primary: #ffffff;
|
|
4
|
+
--semmet-color-danger: #b42318;
|
|
5
|
+
--semmet-color-on-danger: #ffffff;
|
|
6
|
+
--semmet-color-surface: #ffffff;
|
|
7
|
+
--semmet-color-outline: #d4d4d8;
|
|
8
|
+
--semmet-color-on-surface: #18181b;
|
|
9
|
+
--semmet-color-on-surface-variant: #52525b;
|
|
10
|
+
--semmet-radius-sm: 8px;
|
|
11
|
+
--semmet-elevation-1: 0 1px 2px rgba(20, 24, 23, 0.1), 0 1px 1px rgba(20, 24, 23, 0.06);
|
|
12
|
+
--semmet-motion: 160ms cubic-bezier(0.2, 0, 0, 1);
|
|
13
|
+
|
|
14
|
+
display: inline-block;
|
|
15
|
+
font-family: Roboto, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
16
|
+
font-size: 0.9375rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.semmet-button {
|
|
20
|
+
display: inline-flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
gap: 0.5rem;
|
|
24
|
+
min-width: 2.75rem;
|
|
25
|
+
border: 1px solid transparent;
|
|
26
|
+
border-radius: var(--semmet-radius-sm);
|
|
27
|
+
font: inherit;
|
|
28
|
+
font-weight: 600;
|
|
29
|
+
line-height: 1.2;
|
|
30
|
+
text-decoration: none;
|
|
31
|
+
white-space: nowrap;
|
|
32
|
+
cursor: pointer;
|
|
33
|
+
user-select: none;
|
|
34
|
+
box-shadow: var(--semmet-elevation-1);
|
|
35
|
+
transition:
|
|
36
|
+
background-color var(--semmet-motion),
|
|
37
|
+
border-color var(--semmet-motion),
|
|
38
|
+
color var(--semmet-motion),
|
|
39
|
+
box-shadow var(--semmet-motion),
|
|
40
|
+
transform var(--semmet-motion);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.semmet-button--sm {
|
|
44
|
+
min-height: 2rem;
|
|
45
|
+
padding: 0.375rem 0.75rem;
|
|
46
|
+
font-size: 0.875rem;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.semmet-button--md {
|
|
50
|
+
min-height: 2.5rem;
|
|
51
|
+
padding: 0.5rem 1rem;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.semmet-button--lg {
|
|
55
|
+
min-height: 3rem;
|
|
56
|
+
padding: 0.75rem 1.25rem;
|
|
57
|
+
font-size: 1rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.semmet-button--primary {
|
|
61
|
+
color: var(--semmet-color-on-primary);
|
|
62
|
+
background: var(--semmet-color-primary);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.semmet-button--secondary {
|
|
66
|
+
color: var(--semmet-color-on-surface);
|
|
67
|
+
background: var(--semmet-color-surface);
|
|
68
|
+
border-color: var(--semmet-color-outline);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.semmet-button--danger {
|
|
72
|
+
color: var(--semmet-color-on-danger);
|
|
73
|
+
background: var(--semmet-color-danger);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.semmet-button--ghost {
|
|
77
|
+
color: var(--semmet-color-on-surface);
|
|
78
|
+
background: transparent;
|
|
79
|
+
border-color: transparent;
|
|
80
|
+
box-shadow: none;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.semmet-button:hover:not(:disabled) {
|
|
84
|
+
transform: translateY(-1px);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.semmet-button:active:not(:disabled) {
|
|
88
|
+
transform: translateY(0);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.semmet-button:disabled {
|
|
92
|
+
cursor: not-allowed;
|
|
93
|
+
opacity: 0.56;
|
|
94
|
+
box-shadow: none;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.semmet-button:focus-visible {
|
|
98
|
+
outline: 2px solid var(--semmet-color-primary);
|
|
99
|
+
outline-offset: 2px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.semmet-button__spinner {
|
|
103
|
+
width: 1em;
|
|
104
|
+
height: 1em;
|
|
105
|
+
border: 2px solid currentColor;
|
|
106
|
+
border-right-color: transparent;
|
|
107
|
+
border-radius: 50%;
|
|
108
|
+
animation: semmet-button-spin 700ms linear infinite;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@keyframes semmet-button-spin {
|
|
112
|
+
to {
|
|
113
|
+
transform: rotate(360deg);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- Semmet Angular: Button - native <button> keeps form, focus, and keyboard semantics. -->
|
|
2
|
+
<button
|
|
3
|
+
type="button"
|
|
4
|
+
[id]="instanceId"
|
|
5
|
+
[class]="buttonClasses()"
|
|
6
|
+
[disabled]="disabled() || busy()"
|
|
7
|
+
[attr.aria-busy]="busy() ? 'true' : null"
|
|
8
|
+
[attr.aria-pressed]="ariaPressed()"
|
|
9
|
+
(click)="handleClick($event)"
|
|
10
|
+
>
|
|
11
|
+
@if (busy()) {
|
|
12
|
+
<span class="semmet-button__spinner" aria-hidden="true"></span>
|
|
13
|
+
}
|
|
14
|
+
<span>{{ label() }}</span>
|
|
15
|
+
</button>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Component, computed, input, output } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
type <%= classify(name) %>Variant = 'primary' | 'secondary' | 'danger' | 'ghost';
|
|
4
|
+
type <%= classify(name) %>Size = 'sm' | 'md' | 'lg';
|
|
5
|
+
|
|
6
|
+
let nextId = 0;
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: '<%= selector %>',
|
|
10
|
+
imports: [],
|
|
11
|
+
templateUrl: './<%= dasherize(name) %>.html',
|
|
12
|
+
styleUrl: './<%= dasherize(name) %>.css',
|
|
13
|
+
})
|
|
14
|
+
export class <%= classify(name) %> {
|
|
15
|
+
protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
|
|
16
|
+
|
|
17
|
+
readonly label = input('Continue');
|
|
18
|
+
readonly variant = input<<%= classify(name) %>Variant>('primary');
|
|
19
|
+
readonly size = input<<%= classify(name) %>Size>('md');
|
|
20
|
+
readonly disabled = input(false);
|
|
21
|
+
readonly busy = input(false);
|
|
22
|
+
readonly pressed = input<boolean | null>(null);
|
|
23
|
+
readonly buttonClick = output<MouseEvent>();
|
|
24
|
+
|
|
25
|
+
protected readonly buttonClasses = computed(() => [
|
|
26
|
+
'semmet-button',
|
|
27
|
+
`semmet-button--${this.variant()}`,
|
|
28
|
+
`semmet-button--${this.size()}`,
|
|
29
|
+
].join(' '));
|
|
30
|
+
|
|
31
|
+
protected readonly ariaPressed = computed(() => {
|
|
32
|
+
const pressed = this.pressed();
|
|
33
|
+
return pressed === null ? null : String(pressed);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
protected handleClick(event: MouseEvent): void {
|
|
37
|
+
if (this.disabled() || this.busy()) {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
event.stopPropagation();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.buttonClick.emit(event);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const button: (options: import("./schema").Schema) => import("@angular-devkit/schematics").Rule;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.button = void 0;
|
|
4
|
+
const generate_component_1 = require("../utils/generate-component");
|
|
5
|
+
exports.button = (0, generate_component_1.createAriaComponentSchematic)();
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;AAAA,oEAA2E;AAE9D,QAAA,MAAM,GAAG,IAAA,iDAA4B,GAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ComponentSchematicOptions as Schema } from '../utils/schema-types';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["schema.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "SemmetAngularButton",
|
|
4
|
+
"title": "Semmet Angular Button Options Schema",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"description": "Generates a standalone Angular Button component using a native button with variants, sizes, disabled state, busy state, and optional pressed state.",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"name": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "The name for the new component. Used to derive the class name, selector, and file names.",
|
|
12
|
+
"$default": { "$source": "argv", "index": 0 },
|
|
13
|
+
"x-prompt": "What name would you like to use for the button component?"
|
|
14
|
+
},
|
|
15
|
+
"path": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"format": "path",
|
|
18
|
+
"$default": { "$source": "workingDirectory" },
|
|
19
|
+
"description": "The path where the component files should be created, relative to the workspace root. Defaults to the project's source root.",
|
|
20
|
+
"visible": false
|
|
21
|
+
},
|
|
22
|
+
"project": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "The name of the project to add the component to.",
|
|
25
|
+
"$default": { "$source": "projectName" }
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"required": ["name", "project"]
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ComponentSchematicOptions as Schema } from '../utils/schema-types';
|
package/src/collection.json
CHANGED
|
@@ -36,6 +36,11 @@
|
|
|
36
36
|
"factory": "./menu-button/index#menuButton",
|
|
37
37
|
"schema": "./menu-button/schema.json"
|
|
38
38
|
},
|
|
39
|
+
"button": {
|
|
40
|
+
"description": "Generates a standalone Angular Button component using a native button with variants, sizes, disabled state, busy state, and optional pressed state.",
|
|
41
|
+
"factory": "./button/index#button",
|
|
42
|
+
"schema": "./button/schema.json"
|
|
43
|
+
},
|
|
39
44
|
"combobox": {
|
|
40
45
|
"description": "Generates a standalone Angular Combobox component following the W3C ARIA Authoring Practices Guide.",
|
|
41
46
|
"factory": "./combobox/index#combobox",
|
|
@@ -86,6 +91,21 @@
|
|
|
86
91
|
"factory": "./table/index#table",
|
|
87
92
|
"schema": "./table/schema.json"
|
|
88
93
|
},
|
|
94
|
+
"form": {
|
|
95
|
+
"description": "Generates a standalone Angular Form component with native form semantics, labeled controls, inline validation messages, submit handling, and signal-based state.",
|
|
96
|
+
"factory": "./form/index#form",
|
|
97
|
+
"schema": "./form/schema.json"
|
|
98
|
+
},
|
|
99
|
+
"input": {
|
|
100
|
+
"description": "Generates a standalone Angular Input component with native input semantics, label, hint text, validation message wiring, and signal-based value state.",
|
|
101
|
+
"factory": "./input/index#input",
|
|
102
|
+
"schema": "./input/schema.json"
|
|
103
|
+
},
|
|
104
|
+
"textarea": {
|
|
105
|
+
"description": "Generates a standalone Angular Textarea component with native textarea semantics, label, hint text, validation message wiring, and signal-based value state.",
|
|
106
|
+
"factory": "./textarea/index#textarea",
|
|
107
|
+
"schema": "./textarea/schema.json"
|
|
108
|
+
},
|
|
89
109
|
"checkbox": {
|
|
90
110
|
"description": "Generates a standalone Angular Checkbox component following the W3C ARIA Authoring Practices Guide.",
|
|
91
111
|
"factory": "./checkbox/index#checkbox",
|
|
@@ -126,6 +146,11 @@
|
|
|
126
146
|
"factory": "./progress-bar/index#progressBar",
|
|
127
147
|
"schema": "./progress-bar/schema.json"
|
|
128
148
|
},
|
|
149
|
+
"skeleton": {
|
|
150
|
+
"description": "Generates a standalone Angular Skeleton loading placeholder component for composable loading states.",
|
|
151
|
+
"factory": "./skeleton/index#skeleton",
|
|
152
|
+
"schema": "./skeleton/schema.json"
|
|
153
|
+
},
|
|
129
154
|
"alert": {
|
|
130
155
|
"description": "Generates a standalone Angular Alert component following the W3C ARIA Authoring Practices Guide.",
|
|
131
156
|
"factory": "./alert/index#alert",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
--semmet-color-primary: #18181b;
|
|
3
|
+
--semmet-color-on-primary: #ffffff;
|
|
4
|
+
--semmet-color-danger: #b42318;
|
|
5
|
+
--semmet-color-surface: #ffffff;
|
|
6
|
+
--semmet-color-outline: #d4d4d8;
|
|
7
|
+
--semmet-color-on-surface: #18181b;
|
|
8
|
+
--semmet-color-on-surface-variant: #52525b;
|
|
9
|
+
--semmet-radius-sm: 8px;
|
|
10
|
+
--semmet-radius-md: 12px;
|
|
11
|
+
--semmet-elevation-1: 0 1px 2px rgba(20, 24, 23, 0.1), 0 1px 1px rgba(20, 24, 23, 0.06);
|
|
12
|
+
--semmet-motion: 160ms cubic-bezier(0.2, 0, 0, 1);
|
|
13
|
+
|
|
14
|
+
display: block;
|
|
15
|
+
max-width: 34rem;
|
|
16
|
+
font-family: Roboto, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
17
|
+
font-size: 0.9375rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
form {
|
|
21
|
+
display: grid;
|
|
22
|
+
gap: 1rem;
|
|
23
|
+
padding: 1.25rem;
|
|
24
|
+
color: var(--semmet-color-on-surface);
|
|
25
|
+
background: var(--semmet-color-surface);
|
|
26
|
+
border: 1px solid var(--semmet-color-outline);
|
|
27
|
+
border-radius: var(--semmet-radius-md);
|
|
28
|
+
box-shadow: var(--semmet-elevation-1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
h2 {
|
|
32
|
+
margin: 0;
|
|
33
|
+
font-size: 1.125rem;
|
|
34
|
+
font-weight: 600;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.semmet-field {
|
|
38
|
+
display: grid;
|
|
39
|
+
gap: 0.375rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
label {
|
|
43
|
+
font-weight: 500;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
label span {
|
|
47
|
+
color: var(--semmet-color-danger);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
input,
|
|
51
|
+
textarea {
|
|
52
|
+
width: 100%;
|
|
53
|
+
box-sizing: border-box;
|
|
54
|
+
border: 1px solid var(--semmet-color-outline);
|
|
55
|
+
border-radius: var(--semmet-radius-sm);
|
|
56
|
+
padding: 0.5rem 0.75rem;
|
|
57
|
+
font: inherit;
|
|
58
|
+
color: var(--semmet-color-on-surface);
|
|
59
|
+
background: var(--semmet-color-surface);
|
|
60
|
+
transition:
|
|
61
|
+
border-color var(--semmet-motion),
|
|
62
|
+
box-shadow var(--semmet-motion);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
input {
|
|
66
|
+
min-height: 2.5rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
textarea {
|
|
70
|
+
min-height: 7rem;
|
|
71
|
+
resize: vertical;
|
|
72
|
+
line-height: 1.5;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
input[aria-invalid='true'],
|
|
76
|
+
textarea[aria-invalid='true'] {
|
|
77
|
+
border-color: var(--semmet-color-danger);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
input:focus-visible,
|
|
81
|
+
textarea:focus-visible,
|
|
82
|
+
button:focus-visible {
|
|
83
|
+
outline: 2px solid var(--semmet-color-primary);
|
|
84
|
+
outline-offset: 2px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
button {
|
|
88
|
+
justify-self: start;
|
|
89
|
+
min-height: 2.5rem;
|
|
90
|
+
border: 0;
|
|
91
|
+
border-radius: var(--semmet-radius-sm);
|
|
92
|
+
padding: 0.5rem 1rem;
|
|
93
|
+
font: inherit;
|
|
94
|
+
font-weight: 600;
|
|
95
|
+
color: var(--semmet-color-on-primary);
|
|
96
|
+
background: var(--semmet-color-primary);
|
|
97
|
+
cursor: pointer;
|
|
98
|
+
transition:
|
|
99
|
+
background-color var(--semmet-motion),
|
|
100
|
+
transform var(--semmet-motion);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
button:hover {
|
|
104
|
+
transform: translateY(-1px);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.semmet-hint,
|
|
108
|
+
.semmet-error,
|
|
109
|
+
.semmet-status {
|
|
110
|
+
margin: 0;
|
|
111
|
+
font-size: 0.8125rem;
|
|
112
|
+
line-height: 1.4;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.semmet-hint,
|
|
116
|
+
.semmet-status {
|
|
117
|
+
color: var(--semmet-color-on-surface-variant);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.semmet-error {
|
|
121
|
+
color: var(--semmet-color-danger);
|
|
122
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<!-- Semmet Angular: Form - native form with labels, inline validation, and status messaging. -->
|
|
2
|
+
<form [attr.aria-labelledby]="instanceId + '-title'" (submit)="submit($event)" novalidate>
|
|
3
|
+
<h2 [id]="instanceId + '-title'">Contact form</h2>
|
|
4
|
+
|
|
5
|
+
<div class="semmet-field">
|
|
6
|
+
<label [for]="instanceId + '-name'">
|
|
7
|
+
Name
|
|
8
|
+
<span aria-hidden="true">*</span>
|
|
9
|
+
</label>
|
|
10
|
+
<input
|
|
11
|
+
[id]="instanceId + '-name'"
|
|
12
|
+
type="text"
|
|
13
|
+
autocomplete="name"
|
|
14
|
+
required
|
|
15
|
+
[value]="form().name"
|
|
16
|
+
[attr.aria-invalid]="nameError() ? 'true' : null"
|
|
17
|
+
[attr.aria-describedby]="nameError() ? instanceId + '-name-error' : null"
|
|
18
|
+
(input)="updateField('name', $event)"
|
|
19
|
+
/>
|
|
20
|
+
@if (nameError()) {
|
|
21
|
+
<p class="semmet-error" [id]="instanceId + '-name-error'" role="alert">{{ nameError() }}</p>
|
|
22
|
+
}
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="semmet-field">
|
|
26
|
+
<label [for]="instanceId + '-email'">
|
|
27
|
+
Email
|
|
28
|
+
<span aria-hidden="true">*</span>
|
|
29
|
+
</label>
|
|
30
|
+
<input
|
|
31
|
+
[id]="instanceId + '-email'"
|
|
32
|
+
type="email"
|
|
33
|
+
autocomplete="email"
|
|
34
|
+
required
|
|
35
|
+
[value]="form().email"
|
|
36
|
+
[attr.aria-invalid]="emailError() ? 'true' : null"
|
|
37
|
+
[attr.aria-describedby]="emailError() ? instanceId + '-email-error' : null"
|
|
38
|
+
(input)="updateField('email', $event)"
|
|
39
|
+
/>
|
|
40
|
+
@if (emailError()) {
|
|
41
|
+
<p class="semmet-error" [id]="instanceId + '-email-error'" role="alert">{{ emailError() }}</p>
|
|
42
|
+
}
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="semmet-field">
|
|
46
|
+
<label [for]="instanceId + '-message'">
|
|
47
|
+
Message
|
|
48
|
+
<span aria-hidden="true">*</span>
|
|
49
|
+
</label>
|
|
50
|
+
<textarea
|
|
51
|
+
[id]="instanceId + '-message'"
|
|
52
|
+
rows="4"
|
|
53
|
+
required
|
|
54
|
+
[value]="form().message"
|
|
55
|
+
[attr.aria-invalid]="messageError() ? 'true' : null"
|
|
56
|
+
[attr.aria-describedby]="messageError() ? instanceId + '-message-error' : instanceId + '-message-hint'"
|
|
57
|
+
(input)="updateField('message', $event)"
|
|
58
|
+
></textarea>
|
|
59
|
+
<p class="semmet-hint" [id]="instanceId + '-message-hint'">Minimum 10 characters.</p>
|
|
60
|
+
@if (messageError()) {
|
|
61
|
+
<p class="semmet-error" [id]="instanceId + '-message-error'" role="alert">{{ messageError() }}</p>
|
|
62
|
+
}
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<button type="submit">Submit</button>
|
|
66
|
+
|
|
67
|
+
@if (statusMessage()) {
|
|
68
|
+
<p class="semmet-status" role="status">{{ statusMessage() }}</p>
|
|
69
|
+
}
|
|
70
|
+
</form>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Component, computed, signal } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
interface <%= classify(name) %>State {
|
|
4
|
+
name: string;
|
|
5
|
+
email: string;
|
|
6
|
+
message: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let nextId = 0;
|
|
10
|
+
|
|
11
|
+
@Component({
|
|
12
|
+
selector: '<%= selector %>',
|
|
13
|
+
imports: [],
|
|
14
|
+
templateUrl: './<%= dasherize(name) %>.html',
|
|
15
|
+
styleUrl: './<%= dasherize(name) %>.css',
|
|
16
|
+
})
|
|
17
|
+
export class <%= classify(name) %> {
|
|
18
|
+
private readonly emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
19
|
+
|
|
20
|
+
protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
|
|
21
|
+
protected readonly submitted = signal(false);
|
|
22
|
+
protected readonly statusMessage = signal('');
|
|
23
|
+
protected readonly form = signal<<%= classify(name) %>State>({
|
|
24
|
+
name: '',
|
|
25
|
+
email: '',
|
|
26
|
+
message: '',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
protected readonly nameError = computed(() => {
|
|
30
|
+
if (!this.submitted() || this.form().name.trim()) {
|
|
31
|
+
return '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return 'Enter your name.';
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
protected readonly emailError = computed(() => {
|
|
38
|
+
if (!this.submitted()) {
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const email = this.form().email.trim();
|
|
43
|
+
if (!email) {
|
|
44
|
+
return 'Enter your email address.';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return this.emailPattern.test(email) ? '' : 'Enter a valid email address.';
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
protected readonly messageError = computed(() => {
|
|
51
|
+
if (!this.submitted() || this.form().message.trim().length >= 10) {
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return 'Enter a message with at least 10 characters.';
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
protected readonly isInvalid = computed(() => Boolean(this.nameError() || this.emailError() || this.messageError()));
|
|
59
|
+
|
|
60
|
+
protected updateField(field: keyof <%= classify(name) %>State, event: Event): void {
|
|
61
|
+
const value = (event.target as HTMLInputElement | HTMLTextAreaElement).value;
|
|
62
|
+
this.form.update((current) => ({ ...current, [field]: value }));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
protected submit(event: SubmitEvent): void {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
this.submitted.set(true);
|
|
68
|
+
|
|
69
|
+
if (this.isInvalid()) {
|
|
70
|
+
this.statusMessage.set('Check the highlighted fields and try again.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
this.statusMessage.set('Form is valid and ready to submit.');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const form: (options: import("./schema").Schema) => import("@angular-devkit/schematics").Rule;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.form = void 0;
|
|
4
|
+
const generate_component_1 = require("../utils/generate-component");
|
|
5
|
+
exports.form = (0, generate_component_1.createAriaComponentSchematic)();
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":";;;AAAA,oEAA2E;AAE9D,QAAA,IAAI,GAAG,IAAA,iDAA4B,GAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ComponentSchematicOptions as Schema } from '../utils/schema-types';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["schema.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema",
|
|
3
|
+
"$id": "SemmetAngularForm",
|
|
4
|
+
"title": "Semmet Angular Form Options Schema",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"description": "Generates a standalone Angular Form component with native form semantics, labeled controls, inline validation messages, submit handling, and signal-based state.",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"name": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "The name for the new component. Used to derive the class name, selector, and file names.",
|
|
12
|
+
"$default": { "$source": "argv", "index": 0 },
|
|
13
|
+
"x-prompt": "What name would you like to use for the form component?"
|
|
14
|
+
},
|
|
15
|
+
"path": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"format": "path",
|
|
18
|
+
"$default": { "$source": "workingDirectory" },
|
|
19
|
+
"description": "The path where the component files should be created, relative to the workspace root. Defaults to the project's source root.",
|
|
20
|
+
"visible": false
|
|
21
|
+
},
|
|
22
|
+
"project": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"description": "The name of the project to add the component to.",
|
|
25
|
+
"$default": { "$source": "projectName" }
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"required": ["name", "project"]
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ComponentSchematicOptions as Schema } from '../utils/schema-types';
|