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.
Files changed (63) hide show
  1. package/README.md +6 -1
  2. package/package.json +1 -1
  3. package/src/button/files/__name@dasherize__/__name@dasherize__.css.template +115 -0
  4. package/src/button/files/__name@dasherize__/__name@dasherize__.html.template +15 -0
  5. package/src/button/files/__name@dasherize__/__name@dasherize__.ts.template +45 -0
  6. package/src/button/index.d.ts +1 -0
  7. package/src/button/index.js +6 -0
  8. package/src/button/index.js.map +1 -0
  9. package/src/button/index.ts +3 -0
  10. package/src/button/schema.d.ts +1 -0
  11. package/src/button/schema.js +3 -0
  12. package/src/button/schema.js.map +1 -0
  13. package/src/button/schema.json +29 -0
  14. package/src/button/schema.ts +1 -0
  15. package/src/collection.json +25 -0
  16. package/src/form/files/__name@dasherize__/__name@dasherize__.css.template +122 -0
  17. package/src/form/files/__name@dasherize__/__name@dasherize__.html.template +70 -0
  18. package/src/form/files/__name@dasherize__/__name@dasherize__.ts.template +76 -0
  19. package/src/form/index.d.ts +1 -0
  20. package/src/form/index.js +6 -0
  21. package/src/form/index.js.map +1 -0
  22. package/src/form/index.ts +3 -0
  23. package/src/form/schema.d.ts +1 -0
  24. package/src/form/schema.js +3 -0
  25. package/src/form/schema.js.map +1 -0
  26. package/src/form/schema.json +29 -0
  27. package/src/form/schema.ts +1 -0
  28. package/src/input/files/__name@dasherize__/__name@dasherize__.css.template +80 -0
  29. package/src/input/files/__name@dasherize__/__name@dasherize__.html.template +32 -0
  30. package/src/input/files/__name@dasherize__/__name@dasherize__.ts.template +48 -0
  31. package/src/input/index.d.ts +1 -0
  32. package/src/input/index.js +6 -0
  33. package/src/input/index.js.map +1 -0
  34. package/src/input/index.ts +3 -0
  35. package/src/input/schema.d.ts +1 -0
  36. package/src/input/schema.js +3 -0
  37. package/src/input/schema.js.map +1 -0
  38. package/src/input/schema.json +29 -0
  39. package/src/input/schema.ts +1 -0
  40. package/src/skeleton/files/__name@dasherize__/__name@dasherize__.css.template +91 -0
  41. package/src/skeleton/files/__name@dasherize__/__name@dasherize__.html.template +31 -0
  42. package/src/skeleton/files/__name@dasherize__/__name@dasherize__.ts.template +69 -0
  43. package/src/skeleton/index.d.ts +1 -0
  44. package/src/skeleton/index.js +6 -0
  45. package/src/skeleton/index.js.map +1 -0
  46. package/src/skeleton/index.ts +3 -0
  47. package/src/skeleton/schema.d.ts +1 -0
  48. package/src/skeleton/schema.js +3 -0
  49. package/src/skeleton/schema.js.map +1 -0
  50. package/src/skeleton/schema.json +29 -0
  51. package/src/skeleton/schema.ts +1 -0
  52. package/src/textarea/files/__name@dasherize__/__name@dasherize__.css.template +95 -0
  53. package/src/textarea/files/__name@dasherize__/__name@dasherize__.html.template +40 -0
  54. package/src/textarea/files/__name@dasherize__/__name@dasherize__.ts.template +56 -0
  55. package/src/textarea/index.d.ts +1 -0
  56. package/src/textarea/index.js +6 -0
  57. package/src/textarea/index.js.map +1 -0
  58. package/src/textarea/index.ts +3 -0
  59. package/src/textarea/schema.d.ts +1 -0
  60. package/src/textarea/schema.js +3 -0
  61. package/src/textarea/schema.js.map +1 -0
  62. package/src/textarea/schema.json +29 -0
  63. package/src/textarea/schema.ts +1 -0
@@ -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,3 @@
1
+ import { createAriaComponentSchematic } from '../utils/generate-component';
2
+
3
+ export const input = createAriaComponentSchematic();
@@ -0,0 +1 @@
1
+ export { ComponentSchematicOptions as Schema } from '../utils/schema-types';
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=schema.js.map
@@ -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,91 @@
1
+ :host {
2
+ --semmet-color-surface: #ffffff;
3
+ --semmet-color-outline: #d4d4d8;
4
+ --semmet-color-skeleton: #e4e4e7;
5
+ --semmet-color-skeleton-highlight: #f4f4f5;
6
+ --semmet-radius-sm: 8px;
7
+ --semmet-radius-md: 12px;
8
+
9
+ display: block;
10
+ width: 100%;
11
+ }
12
+
13
+ .semmet-skeleton,
14
+ .semmet-skeleton__line {
15
+ position: relative;
16
+ display: block;
17
+ overflow: hidden;
18
+ box-sizing: border-box;
19
+ background: var(--semmet-color-skeleton);
20
+ border-radius: var(--semmet-radius-sm);
21
+ }
22
+
23
+ .semmet-skeleton {
24
+ width: 100%;
25
+ height: 1rem;
26
+ }
27
+
28
+ .semmet-skeleton--circle {
29
+ width: 2.75rem;
30
+ height: 2.75rem;
31
+ border-radius: 999px;
32
+ }
33
+
34
+ .semmet-skeleton--card {
35
+ min-height: 16rem;
36
+ border: 1px solid var(--semmet-color-outline);
37
+ border-radius: var(--semmet-radius-md);
38
+ }
39
+
40
+ .semmet-skeleton--media {
41
+ height: 12.5rem;
42
+ border-radius: var(--semmet-radius-md);
43
+ }
44
+
45
+ .semmet-skeleton-text {
46
+ display: grid;
47
+ width: 100%;
48
+ gap: 0.65rem;
49
+ }
50
+
51
+ .semmet-skeleton__line {
52
+ width: 100%;
53
+ height: 1rem;
54
+ }
55
+
56
+ .semmet-skeleton--animated::after,
57
+ .semmet-skeleton-text--animated .semmet-skeleton__line::after {
58
+ content: '';
59
+ position: absolute;
60
+ inset: 0;
61
+ transform: translateX(-100%);
62
+ background: linear-gradient(
63
+ 90deg,
64
+ transparent,
65
+ var(--semmet-color-skeleton-highlight),
66
+ transparent
67
+ );
68
+ animation: semmet-skeleton-shimmer 1.35s ease-in-out infinite;
69
+ }
70
+
71
+ @keyframes semmet-skeleton-shimmer {
72
+ 100% {
73
+ transform: translateX(100%);
74
+ }
75
+ }
76
+
77
+ @media (prefers-color-scheme: dark) {
78
+ :host {
79
+ --semmet-color-surface: #18181b;
80
+ --semmet-color-outline: #3f3f46;
81
+ --semmet-color-skeleton: #27272a;
82
+ --semmet-color-skeleton-highlight: #3f3f46;
83
+ }
84
+ }
85
+
86
+ @media (prefers-reduced-motion: reduce) {
87
+ .semmet-skeleton--animated::after,
88
+ .semmet-skeleton-text--animated .semmet-skeleton__line::after {
89
+ animation: none;
90
+ }
91
+ }
@@ -0,0 +1,31 @@
1
+ <!-- Semmet Angular: Skeleton Loading - decorative placeholder for pending content. -->
2
+ @if (variant() === 'text') {
3
+ <span
4
+ [id]="instanceId"
5
+ [class]="textClasses()"
6
+ [attr.aria-hidden]="ariaHidden()"
7
+ [attr.aria-label]="ariaLabel()"
8
+ [attr.aria-live]="ariaLive()"
9
+ [attr.role]="role()"
10
+ >
11
+ @for (line of lineItems(); track line) {
12
+ <span
13
+ class="semmet-skeleton__line"
14
+ [style.width]="lineWidth(line)"
15
+ [style.height]="heightStyle()"
16
+ [style.border-radius]="radiusStyle()"
17
+ ></span>
18
+ }
19
+ </span>
20
+ } @else {
21
+ <span
22
+ [id]="instanceId"
23
+ [class]="skeletonClasses()"
24
+ [style.width]="widthStyle()"
25
+ [style.height]="heightStyle()"
26
+ [style.border-radius]="radiusStyle()"
27
+ [attr.aria-hidden]="ariaHidden()"
28
+ [attr.aria-label]="ariaLabel()"
29
+ [attr.aria-live]="ariaLive()"
30
+ [attr.role]="role()"
31
+ ></span>
@@ -0,0 +1,69 @@
1
+ import { Component, booleanAttribute, computed, input, numberAttribute } from '@angular/core';
2
+
3
+ type <%= classify(name) %>Variant = 'rect' | 'text' | 'circle' | 'card' | 'media';
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 variant = input<<%= classify(name) %>Variant>('rect');
17
+ readonly width = input<string | null>(null);
18
+ readonly height = input<string | null>(null);
19
+ readonly size = input<string | null>(null);
20
+ readonly radius = input<string | null>(null);
21
+ readonly lines = input(1, { transform: numberAttribute });
22
+ readonly animated = input(true, { transform: booleanAttribute });
23
+ readonly decorative = input(true, { transform: booleanAttribute });
24
+ readonly label = input('Loading content');
25
+
26
+ protected readonly normalizedLines = computed(() => Math.max(1, Math.floor(this.lines() || 1)));
27
+ protected readonly lineItems = computed(() =>
28
+ Array.from({ length: this.normalizedLines() }, (_item, index) => index),
29
+ );
30
+
31
+ protected readonly skeletonClasses = computed(() =>
32
+ [
33
+ 'semmet-skeleton',
34
+ `semmet-skeleton--${this.variant()}`,
35
+ this.animated() ? 'semmet-skeleton--animated' : '',
36
+ ]
37
+ .filter(Boolean)
38
+ .join(' '),
39
+ );
40
+
41
+ protected readonly textClasses = computed(() =>
42
+ [
43
+ 'semmet-skeleton-text',
44
+ this.animated() ? 'semmet-skeleton-text--animated' : '',
45
+ ]
46
+ .filter(Boolean)
47
+ .join(' '),
48
+ );
49
+
50
+ protected readonly widthStyle = computed(() => this.size() ?? this.width());
51
+ protected readonly heightStyle = computed(() => this.size() ?? this.height());
52
+ protected readonly radiusStyle = computed(() => this.radius());
53
+ protected readonly ariaHidden = computed(() => (this.decorative() ? 'true' : null));
54
+ protected readonly role = computed(() => (this.decorative() ? null : 'status'));
55
+ protected readonly ariaLive = computed(() => (this.decorative() ? null : 'polite'));
56
+ protected readonly ariaLabel = computed(() => (this.decorative() ? null : this.label()));
57
+
58
+ protected lineWidth(index: number): string | null {
59
+ if (this.width()) {
60
+ return this.width();
61
+ }
62
+
63
+ if (this.normalizedLines() > 1 && index === this.normalizedLines() - 1) {
64
+ return '68%';
65
+ }
66
+
67
+ return null;
68
+ }
69
+ }
@@ -0,0 +1 @@
1
+ export declare const skeleton: (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.skeleton = void 0;
4
+ const generate_component_1 = require("../utils/generate-component");
5
+ exports.skeleton = (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,3 @@
1
+ import { createAriaComponentSchematic } from '../utils/generate-component';
2
+
3
+ export const skeleton = createAriaComponentSchematic();
@@ -0,0 +1 @@
1
+ export { ComponentSchematicOptions as Schema } from '../utils/schema-types';
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=schema.js.map
@@ -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": "SemmetAngularSkeleton",
4
+ "title": "Semmet Angular Skeleton Options Schema",
5
+ "type": "object",
6
+ "description": "Generates a standalone Angular Skeleton loading placeholder component for composable loading states.",
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 skeleton 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;