semmet-angular 0.8.0 → 0.9.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 +5 -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 +20 -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/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 (32)
|
|
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 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "semmet-angular",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -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';
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
--semmet-color-primary: #18181b;
|
|
3
|
+
--semmet-color-danger: #b42318;
|
|
4
|
+
--semmet-color-surface: #ffffff;
|
|
5
|
+
--semmet-color-outline: #d4d4d8;
|
|
6
|
+
--semmet-color-on-surface: #18181b;
|
|
7
|
+
--semmet-color-on-surface-variant: #52525b;
|
|
8
|
+
--semmet-color-disabled: #f4f4f5;
|
|
9
|
+
--semmet-radius-sm: 8px;
|
|
10
|
+
--semmet-motion: 160ms cubic-bezier(0.2, 0, 0, 1);
|
|
11
|
+
|
|
12
|
+
display: block;
|
|
13
|
+
font-family: Roboto, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
14
|
+
font-size: 0.9375rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.semmet-field {
|
|
18
|
+
display: grid;
|
|
19
|
+
gap: 0.375rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
label {
|
|
23
|
+
font-weight: 500;
|
|
24
|
+
color: var(--semmet-color-on-surface);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
label span {
|
|
28
|
+
color: var(--semmet-color-danger);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
input {
|
|
32
|
+
width: 100%;
|
|
33
|
+
min-height: 2.5rem;
|
|
34
|
+
box-sizing: border-box;
|
|
35
|
+
border: 1px solid var(--semmet-color-outline);
|
|
36
|
+
border-radius: var(--semmet-radius-sm);
|
|
37
|
+
padding: 0.5rem 0.75rem;
|
|
38
|
+
font: inherit;
|
|
39
|
+
color: var(--semmet-color-on-surface);
|
|
40
|
+
background: var(--semmet-color-surface);
|
|
41
|
+
transition:
|
|
42
|
+
border-color var(--semmet-motion),
|
|
43
|
+
box-shadow var(--semmet-motion),
|
|
44
|
+
background-color var(--semmet-motion);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
input::placeholder {
|
|
48
|
+
color: var(--semmet-color-on-surface-variant);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
input[aria-invalid='true'] {
|
|
52
|
+
border-color: var(--semmet-color-danger);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
input:disabled {
|
|
56
|
+
color: var(--semmet-color-on-surface-variant);
|
|
57
|
+
background: var(--semmet-color-disabled);
|
|
58
|
+
cursor: not-allowed;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
input:focus-visible {
|
|
62
|
+
outline: 2px solid var(--semmet-color-primary);
|
|
63
|
+
outline-offset: 1px;
|
|
64
|
+
border-color: var(--semmet-color-primary);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.semmet-field__hint,
|
|
68
|
+
.semmet-field__error {
|
|
69
|
+
margin: 0;
|
|
70
|
+
font-size: 0.8125rem;
|
|
71
|
+
line-height: 1.4;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.semmet-field__hint {
|
|
75
|
+
color: var(--semmet-color-on-surface-variant);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.semmet-field__error {
|
|
79
|
+
color: var(--semmet-color-danger);
|
|
80
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<!-- Semmet Angular: Input - native <input> with label, hint, and validation wiring. -->
|
|
2
|
+
<div class="semmet-field">
|
|
3
|
+
<label [for]="controlId()">
|
|
4
|
+
{{ label() }}
|
|
5
|
+
@if (required()) {
|
|
6
|
+
<span aria-hidden="true">*</span>
|
|
7
|
+
}
|
|
8
|
+
</label>
|
|
9
|
+
|
|
10
|
+
<input
|
|
11
|
+
[id]="controlId()"
|
|
12
|
+
[type]="inputType()"
|
|
13
|
+
[value]="value()"
|
|
14
|
+
[placeholder]="placeholder()"
|
|
15
|
+
[autocomplete]="autocomplete()"
|
|
16
|
+
[required]="required()"
|
|
17
|
+
[disabled]="disabled()"
|
|
18
|
+
[readOnly]="readOnly()"
|
|
19
|
+
[attr.aria-required]="required() ? 'true' : null"
|
|
20
|
+
[attr.aria-invalid]="error() ? 'true' : null"
|
|
21
|
+
[attr.aria-describedby]="describedBy()"
|
|
22
|
+
(input)="updateValue($event)"
|
|
23
|
+
/>
|
|
24
|
+
|
|
25
|
+
@if (hint()) {
|
|
26
|
+
<p class="semmet-field__hint" [id]="hintId()">{{ hint() }}</p>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@if (error()) {
|
|
30
|
+
<p class="semmet-field__error" [id]="errorId()" role="alert">{{ error() }}</p>
|
|
31
|
+
}
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Component, computed, input, model } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
type <%= classify(name) %>Type = 'text' | 'email' | 'password' | 'search' | 'tel' | 'url';
|
|
4
|
+
|
|
5
|
+
let nextId = 0;
|
|
6
|
+
|
|
7
|
+
@Component({
|
|
8
|
+
selector: '<%= selector %>',
|
|
9
|
+
imports: [],
|
|
10
|
+
templateUrl: './<%= dasherize(name) %>.html',
|
|
11
|
+
styleUrl: './<%= dasherize(name) %>.css',
|
|
12
|
+
})
|
|
13
|
+
export class <%= classify(name) %> {
|
|
14
|
+
protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
|
|
15
|
+
|
|
16
|
+
readonly label = input('Email address');
|
|
17
|
+
readonly inputType = input<<%= classify(name) %>Type>('email');
|
|
18
|
+
readonly placeholder = input('name@example.com');
|
|
19
|
+
readonly hint = input('Use the email address associated with your account.');
|
|
20
|
+
readonly error = input('');
|
|
21
|
+
readonly required = input(false);
|
|
22
|
+
readonly disabled = input(false);
|
|
23
|
+
readonly readOnly = input(false);
|
|
24
|
+
readonly autocomplete = input('email');
|
|
25
|
+
readonly value = model('');
|
|
26
|
+
|
|
27
|
+
protected readonly controlId = computed(() => `${this.instanceId}-control`);
|
|
28
|
+
protected readonly hintId = computed(() => `${this.instanceId}-hint`);
|
|
29
|
+
protected readonly errorId = computed(() => `${this.instanceId}-error`);
|
|
30
|
+
|
|
31
|
+
protected readonly describedBy = computed(() => {
|
|
32
|
+
const ids = [];
|
|
33
|
+
|
|
34
|
+
if (this.hint()) {
|
|
35
|
+
ids.push(this.hintId());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (this.error()) {
|
|
39
|
+
ids.push(this.errorId());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return ids.length > 0 ? ids.join(' ') : null;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
protected updateValue(event: Event): void {
|
|
46
|
+
this.value.set((event.target as HTMLInputElement).value);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const input: (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.input = void 0;
|
|
4
|
+
const generate_component_1 = require("../utils/generate-component");
|
|
5
|
+
exports.input = (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,KAAK,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": "SemmetAngularInput",
|
|
4
|
+
"title": "Semmet Angular Input Options Schema",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"description": "Generates a standalone Angular Input component with native input semantics, label, hint text, validation message wiring, and signal-based value 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 input 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';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
:host {
|
|
2
|
+
--semmet-color-primary: #18181b;
|
|
3
|
+
--semmet-color-danger: #b42318;
|
|
4
|
+
--semmet-color-surface: #ffffff;
|
|
5
|
+
--semmet-color-outline: #d4d4d8;
|
|
6
|
+
--semmet-color-on-surface: #18181b;
|
|
7
|
+
--semmet-color-on-surface-variant: #52525b;
|
|
8
|
+
--semmet-color-disabled: #f4f4f5;
|
|
9
|
+
--semmet-radius-sm: 8px;
|
|
10
|
+
--semmet-motion: 160ms cubic-bezier(0.2, 0, 0, 1);
|
|
11
|
+
|
|
12
|
+
display: block;
|
|
13
|
+
font-family: Roboto, system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
14
|
+
font-size: 0.9375rem;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.semmet-field {
|
|
18
|
+
display: grid;
|
|
19
|
+
gap: 0.375rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
label {
|
|
23
|
+
font-weight: 500;
|
|
24
|
+
color: var(--semmet-color-on-surface);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
label span {
|
|
28
|
+
color: var(--semmet-color-danger);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
textarea {
|
|
32
|
+
width: 100%;
|
|
33
|
+
min-height: 7rem;
|
|
34
|
+
box-sizing: border-box;
|
|
35
|
+
resize: vertical;
|
|
36
|
+
border: 1px solid var(--semmet-color-outline);
|
|
37
|
+
border-radius: var(--semmet-radius-sm);
|
|
38
|
+
padding: 0.625rem 0.75rem;
|
|
39
|
+
font: inherit;
|
|
40
|
+
line-height: 1.5;
|
|
41
|
+
color: var(--semmet-color-on-surface);
|
|
42
|
+
background: var(--semmet-color-surface);
|
|
43
|
+
transition:
|
|
44
|
+
border-color var(--semmet-motion),
|
|
45
|
+
box-shadow var(--semmet-motion),
|
|
46
|
+
background-color var(--semmet-motion);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
textarea::placeholder {
|
|
50
|
+
color: var(--semmet-color-on-surface-variant);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
textarea[aria-invalid='true'] {
|
|
54
|
+
border-color: var(--semmet-color-danger);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
textarea:disabled {
|
|
58
|
+
color: var(--semmet-color-on-surface-variant);
|
|
59
|
+
background: var(--semmet-color-disabled);
|
|
60
|
+
cursor: not-allowed;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
textarea:focus-visible {
|
|
64
|
+
outline: 2px solid var(--semmet-color-primary);
|
|
65
|
+
outline-offset: 1px;
|
|
66
|
+
border-color: var(--semmet-color-primary);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.semmet-field__meta {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: flex-start;
|
|
72
|
+
justify-content: space-between;
|
|
73
|
+
gap: 1rem;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.semmet-field__hint,
|
|
77
|
+
.semmet-field__counter,
|
|
78
|
+
.semmet-field__error {
|
|
79
|
+
margin: 0;
|
|
80
|
+
font-size: 0.8125rem;
|
|
81
|
+
line-height: 1.4;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.semmet-field__hint,
|
|
85
|
+
.semmet-field__counter {
|
|
86
|
+
color: var(--semmet-color-on-surface-variant);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.semmet-field__counter {
|
|
90
|
+
white-space: nowrap;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.semmet-field__error {
|
|
94
|
+
color: var(--semmet-color-danger);
|
|
95
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<!-- Semmet Angular: Textarea - native <textarea> with label, hint, counter, and validation wiring. -->
|
|
2
|
+
<div class="semmet-field">
|
|
3
|
+
<label [for]="controlId()">
|
|
4
|
+
{{ label() }}
|
|
5
|
+
@if (required()) {
|
|
6
|
+
<span aria-hidden="true">*</span>
|
|
7
|
+
}
|
|
8
|
+
</label>
|
|
9
|
+
|
|
10
|
+
<textarea
|
|
11
|
+
[id]="controlId()"
|
|
12
|
+
[value]="value()"
|
|
13
|
+
[placeholder]="placeholder()"
|
|
14
|
+
[rows]="rows()"
|
|
15
|
+
[attr.maxlength]="maxLength()"
|
|
16
|
+
[required]="required()"
|
|
17
|
+
[disabled]="disabled()"
|
|
18
|
+
[readOnly]="readOnly()"
|
|
19
|
+
[attr.aria-required]="required() ? 'true' : null"
|
|
20
|
+
[attr.aria-invalid]="error() ? 'true' : null"
|
|
21
|
+
[attr.aria-describedby]="describedBy()"
|
|
22
|
+
(input)="updateValue($event)"
|
|
23
|
+
></textarea>
|
|
24
|
+
|
|
25
|
+
<div class="semmet-field__meta">
|
|
26
|
+
@if (hint()) {
|
|
27
|
+
<p class="semmet-field__hint" [id]="hintId()">{{ hint() }}</p>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@if (maxLength() !== null) {
|
|
31
|
+
<p class="semmet-field__counter" [id]="counterId()" aria-live="polite">
|
|
32
|
+
{{ remainingCharacters() }} characters left
|
|
33
|
+
</p>
|
|
34
|
+
}
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
@if (error()) {
|
|
38
|
+
<p class="semmet-field__error" [id]="errorId()" role="alert">{{ error() }}</p>
|
|
39
|
+
}
|
|
40
|
+
</div>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Component, computed, input, model } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
let nextId = 0;
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: '<%= selector %>',
|
|
7
|
+
imports: [],
|
|
8
|
+
templateUrl: './<%= dasherize(name) %>.html',
|
|
9
|
+
styleUrl: './<%= dasherize(name) %>.css',
|
|
10
|
+
})
|
|
11
|
+
export class <%= classify(name) %> {
|
|
12
|
+
protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
|
|
13
|
+
|
|
14
|
+
readonly label = input('Message');
|
|
15
|
+
readonly placeholder = input('Write your message...');
|
|
16
|
+
readonly hint = input('Keep it concise and include the details someone needs to respond.');
|
|
17
|
+
readonly error = input('');
|
|
18
|
+
readonly required = input(false);
|
|
19
|
+
readonly disabled = input(false);
|
|
20
|
+
readonly readOnly = input(false);
|
|
21
|
+
readonly rows = input(4);
|
|
22
|
+
readonly maxLength = input<number | null>(280);
|
|
23
|
+
readonly value = model('');
|
|
24
|
+
|
|
25
|
+
protected readonly controlId = computed(() => `${this.instanceId}-control`);
|
|
26
|
+
protected readonly hintId = computed(() => `${this.instanceId}-hint`);
|
|
27
|
+
protected readonly errorId = computed(() => `${this.instanceId}-error`);
|
|
28
|
+
protected readonly counterId = computed(() => `${this.instanceId}-counter`);
|
|
29
|
+
|
|
30
|
+
protected readonly remainingCharacters = computed(() => {
|
|
31
|
+
const maxLength = this.maxLength();
|
|
32
|
+
return maxLength === null ? null : maxLength - this.value().length;
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
protected readonly describedBy = computed(() => {
|
|
36
|
+
const ids = [];
|
|
37
|
+
|
|
38
|
+
if (this.hint()) {
|
|
39
|
+
ids.push(this.hintId());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (this.maxLength() !== null) {
|
|
43
|
+
ids.push(this.counterId());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (this.error()) {
|
|
47
|
+
ids.push(this.errorId());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return ids.length > 0 ? ids.join(' ') : null;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
protected updateValue(event: Event): void {
|
|
54
|
+
this.value.set((event.target as HTMLTextAreaElement).value);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const textarea: (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.textarea = void 0;
|
|
4
|
+
const generate_component_1 = require("../utils/generate-component");
|
|
5
|
+
exports.textarea = (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,QAAQ,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": "SemmetAngularTextarea",
|
|
4
|
+
"title": "Semmet Angular Textarea Options Schema",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"description": "Generates a standalone Angular Textarea component with native textarea semantics, label, hint text, validation message wiring, and signal-based value 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 textarea 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';
|