semmet-angular 0.11.0 → 0.12.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 (126) hide show
  1. package/README.md +15 -5
  2. package/package.json +1 -1
  3. package/src/badge/files/__name@dasherize__/__name@dasherize__.css.template +102 -0
  4. package/src/badge/files/__name@dasherize__/__name@dasherize__.html.template +14 -0
  5. package/src/badge/files/__name@dasherize__/__name@dasherize__.ts.template +43 -0
  6. package/src/badge/index.d.ts +1 -0
  7. package/src/badge/index.js +6 -0
  8. package/src/badge/index.js.map +1 -0
  9. package/src/badge/index.ts +3 -0
  10. package/src/badge/schema.d.ts +1 -0
  11. package/src/badge/schema.js +3 -0
  12. package/src/badge/schema.js.map +1 -0
  13. package/src/badge/schema.json +29 -0
  14. package/src/badge/schema.ts +1 -0
  15. package/src/button-group/files/__name@dasherize__/__name@dasherize__.css.template +93 -0
  16. package/src/button-group/files/__name@dasherize__/__name@dasherize__.html.template +20 -0
  17. package/src/button-group/files/__name@dasherize__/__name@dasherize__.ts.template +85 -0
  18. package/src/button-group/index.d.ts +1 -0
  19. package/src/button-group/index.js +6 -0
  20. package/src/button-group/index.js.map +1 -0
  21. package/src/button-group/index.ts +3 -0
  22. package/src/button-group/schema.d.ts +1 -0
  23. package/src/button-group/schema.js +3 -0
  24. package/src/button-group/schema.js.map +1 -0
  25. package/src/button-group/schema.json +29 -0
  26. package/src/button-group/schema.ts +1 -0
  27. package/src/close-button/files/__name@dasherize__/__name@dasherize__.css.template +105 -0
  28. package/src/close-button/files/__name@dasherize__/__name@dasherize__.html.template +11 -0
  29. package/src/close-button/files/__name@dasherize__/__name@dasherize__.ts.template +38 -0
  30. package/src/close-button/index.d.ts +1 -0
  31. package/src/close-button/index.js +6 -0
  32. package/src/close-button/index.js.map +1 -0
  33. package/src/close-button/index.ts +3 -0
  34. package/src/close-button/schema.d.ts +1 -0
  35. package/src/close-button/schema.js +3 -0
  36. package/src/close-button/schema.js.map +1 -0
  37. package/src/close-button/schema.json +29 -0
  38. package/src/close-button/schema.ts +1 -0
  39. package/src/collection.json +50 -0
  40. package/src/input-group/files/__name@dasherize__/__name@dasherize__.css.template +119 -0
  41. package/src/input-group/files/__name@dasherize__/__name@dasherize__.html.template +51 -0
  42. package/src/input-group/files/__name@dasherize__/__name@dasherize__.ts.template +64 -0
  43. package/src/input-group/index.d.ts +1 -0
  44. package/src/input-group/index.js +6 -0
  45. package/src/input-group/index.js.map +1 -0
  46. package/src/input-group/index.ts +3 -0
  47. package/src/input-group/schema.d.ts +1 -0
  48. package/src/input-group/schema.js +3 -0
  49. package/src/input-group/schema.js.map +1 -0
  50. package/src/input-group/schema.json +29 -0
  51. package/src/input-group/schema.ts +1 -0
  52. package/src/list-group/files/__name@dasherize__/__name@dasherize__.css.template +121 -0
  53. package/src/list-group/files/__name@dasherize__/__name@dasherize__.html.template +47 -0
  54. package/src/list-group/files/__name@dasherize__/__name@dasherize__.ts.template +55 -0
  55. package/src/list-group/index.d.ts +1 -0
  56. package/src/list-group/index.js +6 -0
  57. package/src/list-group/index.js.map +1 -0
  58. package/src/list-group/index.ts +3 -0
  59. package/src/list-group/schema.d.ts +1 -0
  60. package/src/list-group/schema.js +3 -0
  61. package/src/list-group/schema.js.map +1 -0
  62. package/src/list-group/schema.json +29 -0
  63. package/src/list-group/schema.ts +1 -0
  64. package/src/navbar/files/__name@dasherize__/__name@dasherize__.css.template +158 -0
  65. package/src/navbar/files/__name@dasherize__/__name@dasherize__.html.template +36 -0
  66. package/src/navbar/files/__name@dasherize__/__name@dasherize__.ts.template +47 -0
  67. package/src/navbar/index.d.ts +1 -0
  68. package/src/navbar/index.js +6 -0
  69. package/src/navbar/index.js.map +1 -0
  70. package/src/navbar/index.ts +3 -0
  71. package/src/navbar/schema.d.ts +1 -0
  72. package/src/navbar/schema.js +3 -0
  73. package/src/navbar/schema.js.map +1 -0
  74. package/src/navbar/schema.json +29 -0
  75. package/src/navbar/schema.ts +1 -0
  76. package/src/offcanvas/files/__name@dasherize__/__name@dasherize__.css.template +180 -0
  77. package/src/offcanvas/files/__name@dasherize__/__name@dasherize__.html.template +52 -0
  78. package/src/offcanvas/files/__name@dasherize__/__name@dasherize__.ts.template +96 -0
  79. package/src/offcanvas/index.d.ts +1 -0
  80. package/src/offcanvas/index.js +6 -0
  81. package/src/offcanvas/index.js.map +1 -0
  82. package/src/offcanvas/index.ts +3 -0
  83. package/src/offcanvas/schema.d.ts +1 -0
  84. package/src/offcanvas/schema.js +3 -0
  85. package/src/offcanvas/schema.js.map +1 -0
  86. package/src/offcanvas/schema.json +29 -0
  87. package/src/offcanvas/schema.ts +1 -0
  88. package/src/popover/files/__name@dasherize__/__name@dasherize__.css.template +156 -0
  89. package/src/popover/files/__name@dasherize__/__name@dasherize__.html.template +40 -0
  90. package/src/popover/files/__name@dasherize__/__name@dasherize__.ts.template +65 -0
  91. package/src/popover/index.d.ts +1 -0
  92. package/src/popover/index.js +6 -0
  93. package/src/popover/index.js.map +1 -0
  94. package/src/popover/index.ts +3 -0
  95. package/src/popover/schema.d.ts +1 -0
  96. package/src/popover/schema.js +3 -0
  97. package/src/popover/schema.js.map +1 -0
  98. package/src/popover/schema.json +29 -0
  99. package/src/popover/schema.ts +1 -0
  100. package/src/select/files/__name@dasherize__/__name@dasherize__.css.template +92 -0
  101. package/src/select/files/__name@dasherize__/__name@dasherize__.html.template +33 -0
  102. package/src/select/files/__name@dasherize__/__name@dasherize__.ts.template +51 -0
  103. package/src/select/index.d.ts +1 -0
  104. package/src/select/index.js +6 -0
  105. package/src/select/index.js.map +1 -0
  106. package/src/select/index.ts +3 -0
  107. package/src/select/schema.d.ts +1 -0
  108. package/src/select/schema.js +3 -0
  109. package/src/select/schema.js.map +1 -0
  110. package/src/select/schema.json +29 -0
  111. package/src/select/schema.ts +1 -0
  112. package/src/spinner/files/__name@dasherize__/__name@dasherize__.css.template +85 -0
  113. package/src/spinner/files/__name@dasherize__/__name@dasherize__.html.template +18 -0
  114. package/src/spinner/files/__name@dasherize__/__name@dasherize__.ts.template +33 -0
  115. package/src/spinner/index.d.ts +1 -0
  116. package/src/spinner/index.js +6 -0
  117. package/src/spinner/index.js.map +1 -0
  118. package/src/spinner/index.ts +3 -0
  119. package/src/spinner/schema.d.ts +1 -0
  120. package/src/spinner/schema.js +3 -0
  121. package/src/spinner/schema.js.map +1 -0
  122. package/src/spinner/schema.json +29 -0
  123. package/src/spinner/schema.ts +1 -0
  124. package/src/utils/generate-component.d.ts +1 -1
  125. package/src/utils/generate-component.js +1 -1
  126. package/src/utils/generate-component.ts +1 -1
@@ -0,0 +1,180 @@
1
+ :host {
2
+ --semmet-color-primary: #18181b;
3
+ --semmet-color-on-primary: #ffffff;
4
+ --semmet-color-surface: #ffffff;
5
+ --semmet-color-backdrop: rgba(24, 24, 27, 0.44);
6
+ --semmet-color-outline: #d4d4d8;
7
+ --semmet-color-on-surface: #27272a;
8
+ --semmet-color-on-surface-variant: #71717a;
9
+ --semmet-radius-md: 10px;
10
+ --semmet-elevation-3: 0 20px 48px rgba(20, 24, 23, 0.22), 0 8px 20px rgba(20, 24, 23, 0.14);
11
+ --semmet-motion: 220ms cubic-bezier(0.2, 0, 0, 1);
12
+
13
+ display: inline-block;
14
+ font-family: Roboto, system-ui, -apple-system, 'Segoe UI', sans-serif;
15
+ }
16
+
17
+ .semmet-offcanvas__trigger {
18
+ min-height: 2.5rem;
19
+ padding: 0.5rem 1rem;
20
+ border: 1px solid transparent;
21
+ border-radius: var(--semmet-radius-md);
22
+ color: var(--semmet-color-on-primary);
23
+ background: var(--semmet-color-primary);
24
+ font: inherit;
25
+ font-weight: 650;
26
+ cursor: pointer;
27
+ }
28
+
29
+ .semmet-offcanvas__trigger:focus-visible,
30
+ .semmet-offcanvas__close:focus-visible {
31
+ outline: 2px solid var(--semmet-color-primary);
32
+ outline-offset: 2px;
33
+ }
34
+
35
+ .semmet-offcanvas__backdrop {
36
+ position: fixed;
37
+ inset: 0;
38
+ z-index: 1000;
39
+ background: var(--semmet-color-backdrop);
40
+ }
41
+
42
+ .semmet-offcanvas__panel {
43
+ position: fixed;
44
+ z-index: 1001;
45
+ display: grid;
46
+ grid-template-rows: auto 1fr;
47
+ color: var(--semmet-color-on-surface);
48
+ background: var(--semmet-color-surface);
49
+ box-shadow: var(--semmet-elevation-3);
50
+ animation: semmet-offcanvas-enter var(--semmet-motion);
51
+ }
52
+
53
+ .semmet-offcanvas__panel--start,
54
+ .semmet-offcanvas__panel--end {
55
+ top: 0;
56
+ bottom: 0;
57
+ width: min(26rem, 92vw);
58
+ }
59
+
60
+ .semmet-offcanvas__panel--start {
61
+ left: 0;
62
+ }
63
+
64
+ .semmet-offcanvas__panel--end {
65
+ right: 0;
66
+ }
67
+
68
+ .semmet-offcanvas__panel--top,
69
+ .semmet-offcanvas__panel--bottom {
70
+ left: 0;
71
+ right: 0;
72
+ max-height: min(32rem, 82vh);
73
+ }
74
+
75
+ .semmet-offcanvas__panel--top {
76
+ top: 0;
77
+ }
78
+
79
+ .semmet-offcanvas__panel--bottom {
80
+ bottom: 0;
81
+ }
82
+
83
+ .semmet-offcanvas__header {
84
+ display: flex;
85
+ align-items: start;
86
+ justify-content: space-between;
87
+ gap: 1rem;
88
+ padding: 1.25rem;
89
+ border-block-end: 1px solid var(--semmet-color-outline);
90
+ }
91
+
92
+ .semmet-offcanvas__title,
93
+ .semmet-offcanvas__description,
94
+ .semmet-offcanvas__placeholder {
95
+ margin: 0;
96
+ }
97
+
98
+ .semmet-offcanvas__title {
99
+ font-size: 1.125rem;
100
+ line-height: 1.25;
101
+ }
102
+
103
+ .semmet-offcanvas__description {
104
+ margin-block-start: 0.375rem;
105
+ color: var(--semmet-color-on-surface-variant);
106
+ line-height: 1.4;
107
+ }
108
+
109
+ .semmet-offcanvas__close {
110
+ position: relative;
111
+ display: inline-flex;
112
+ align-items: center;
113
+ justify-content: center;
114
+ flex: 0 0 auto;
115
+ width: 2.5rem;
116
+ height: 2.5rem;
117
+ border: 0;
118
+ border-radius: 999px;
119
+ color: inherit;
120
+ background: transparent;
121
+ cursor: pointer;
122
+ }
123
+
124
+ .semmet-offcanvas__close:hover {
125
+ background: #f4f4f5;
126
+ }
127
+
128
+ .semmet-offcanvas__close span,
129
+ .semmet-offcanvas__close span::before,
130
+ .semmet-offcanvas__close span::after {
131
+ display: block;
132
+ width: 1rem;
133
+ height: 0.125rem;
134
+ }
135
+
136
+ .semmet-offcanvas__close span {
137
+ position: relative;
138
+ }
139
+
140
+ .semmet-offcanvas__close span::before,
141
+ .semmet-offcanvas__close span::after {
142
+ position: absolute;
143
+ top: 50%;
144
+ left: 0;
145
+ border-radius: 999px;
146
+ background: currentColor;
147
+ content: '';
148
+ }
149
+
150
+ .semmet-offcanvas__close span::before {
151
+ transform: rotate(45deg);
152
+ }
153
+
154
+ .semmet-offcanvas__close span::after {
155
+ transform: rotate(-45deg);
156
+ }
157
+
158
+ .semmet-offcanvas__body {
159
+ min-height: 0;
160
+ overflow: auto;
161
+ padding: 1.25rem;
162
+ }
163
+
164
+ .semmet-offcanvas__placeholder {
165
+ color: var(--semmet-color-on-surface-variant);
166
+ line-height: 1.5;
167
+ }
168
+
169
+ @keyframes semmet-offcanvas-enter {
170
+ from {
171
+ opacity: 0;
172
+ transform: translateX(1rem);
173
+ }
174
+ }
175
+
176
+ @media (prefers-reduced-motion: reduce) {
177
+ .semmet-offcanvas__panel {
178
+ animation-duration: 1ms;
179
+ }
180
+ }
@@ -0,0 +1,52 @@
1
+ <!-- Semmet Angular: Offcanvas - modal drawer with dialog semantics and focus management. -->
2
+ <button
3
+ type="button"
4
+ class="semmet-offcanvas__trigger"
5
+ [attr.aria-controls]="instanceId + '-panel'"
6
+ [attr.aria-expanded]="isOpen()"
7
+ (click)="open($event)"
8
+ >
9
+ {{ triggerLabel() }}
10
+ </button>
11
+
12
+ @if (isOpen()) {
13
+ @if (backdrop()) {
14
+ <div class="semmet-offcanvas__backdrop" (click)="handleBackdropClick()"></div>
15
+ }
16
+ <section
17
+ #panel
18
+ tabindex="-1"
19
+ role="dialog"
20
+ aria-modal="true"
21
+ [id]="instanceId + '-panel'"
22
+ [class]="panelClasses()"
23
+ [attr.aria-labelledby]="instanceId + '-title'"
24
+ [attr.aria-describedby]="description() ? instanceId + '-description' : null"
25
+ (keydown)="handlePanelKeydown($event)"
26
+ >
27
+ <header class="semmet-offcanvas__header">
28
+ <div>
29
+ <h2 class="semmet-offcanvas__title" [id]="instanceId + '-title'">{{ title() }}</h2>
30
+ @if (description()) {
31
+ <p class="semmet-offcanvas__description" [id]="instanceId + '-description'">
32
+ {{ description() }}
33
+ </p>
34
+ }
35
+ </div>
36
+ <button
37
+ type="button"
38
+ class="semmet-offcanvas__close"
39
+ [attr.aria-label]="closeLabel()"
40
+ (click)="close()"
41
+ >
42
+ <span aria-hidden="true"></span>
43
+ </button>
44
+ </header>
45
+ <div class="semmet-offcanvas__body">
46
+ <ng-content></ng-content>
47
+ <p class="semmet-offcanvas__placeholder">
48
+ Replace this with filters, navigation, or contextual controls.
49
+ </p>
50
+ </div>
51
+ </section>
52
+ }
@@ -0,0 +1,96 @@
1
+ import { Component, ElementRef, Injector, afterNextRender, booleanAttribute, computed, inject, input, signal, viewChild } from '@angular/core';
2
+
3
+ const FOCUSABLE_SELECTOR =
4
+ 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
5
+
6
+ type <%= classify(name) %>Placement = 'start' | 'end' | 'top' | 'bottom';
7
+
8
+ let nextId = 0;
9
+
10
+ @Component({
11
+ selector: '<%= selector %>',
12
+ imports: [],
13
+ templateUrl: './<%= dasherize(name) %>.html',
14
+ styleUrl: './<%= dasherize(name) %>.css',
15
+ })
16
+ export class <%= classify(name) %> {
17
+ private readonly injector = inject(Injector);
18
+ private readonly panelRef = viewChild<ElementRef<HTMLElement>>('panel');
19
+ private triggerElement: HTMLElement | null = null;
20
+
21
+ protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
22
+ protected readonly isOpen = signal(false);
23
+
24
+ readonly triggerLabel = input('Open panel');
25
+ readonly title = input('Panel title');
26
+ readonly description = input('Use this panel for secondary actions, filters, or contextual content.');
27
+ readonly closeLabel = input('Close panel');
28
+ readonly placement = input<<%= classify(name) %>Placement>('end');
29
+ readonly backdrop = input(true, { transform: booleanAttribute });
30
+ readonly closeOnBackdrop = input(true, { transform: booleanAttribute });
31
+
32
+ protected readonly panelClasses = computed(() => [
33
+ 'semmet-offcanvas__panel',
34
+ `semmet-offcanvas__panel--${this.placement()}`,
35
+ ].join(' '));
36
+
37
+ protected open(event: MouseEvent): void {
38
+ this.triggerElement = event.currentTarget as HTMLElement;
39
+ this.isOpen.set(true);
40
+ afterNextRender(() => this.focusFirstElement(), { injector: this.injector });
41
+ }
42
+
43
+ protected close(): void {
44
+ this.isOpen.set(false);
45
+ this.triggerElement?.focus();
46
+ }
47
+
48
+ protected handleBackdropClick(): void {
49
+ if (this.closeOnBackdrop()) {
50
+ this.close();
51
+ }
52
+ }
53
+
54
+ protected handlePanelKeydown(event: KeyboardEvent): void {
55
+ if (event.key === 'Escape') {
56
+ event.preventDefault();
57
+ this.close();
58
+ return;
59
+ }
60
+
61
+ if (event.key === 'Tab') {
62
+ this.trapFocus(event);
63
+ }
64
+ }
65
+
66
+ private focusFirstElement(): void {
67
+ const panel = this.panelRef()?.nativeElement;
68
+ const firstFocusable = panel?.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);
69
+ (firstFocusable ?? panel)?.focus();
70
+ }
71
+
72
+ // Keeps Tab/Shift+Tab cycling within the offcanvas while it behaves as a modal dialog.
73
+ private trapFocus(event: KeyboardEvent): void {
74
+ const panel = this.panelRef()?.nativeElement;
75
+ if (!panel) {
76
+ return;
77
+ }
78
+
79
+ const focusable = Array.from(panel.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR));
80
+ if (focusable.length === 0) {
81
+ return;
82
+ }
83
+
84
+ const first = focusable[0];
85
+ const last = focusable[focusable.length - 1];
86
+ const active = panel.ownerDocument.activeElement;
87
+
88
+ if (event.shiftKey && active === first) {
89
+ event.preventDefault();
90
+ last.focus();
91
+ } else if (!event.shiftKey && active === last) {
92
+ event.preventDefault();
93
+ first.focus();
94
+ }
95
+ }
96
+ }
@@ -0,0 +1 @@
1
+ export declare const offcanvas: (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.offcanvas = void 0;
4
+ const generate_component_1 = require("../utils/generate-component");
5
+ exports.offcanvas = (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,SAAS,GAAG,IAAA,iDAA4B,GAAE,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { createAriaComponentSchematic } from '../utils/generate-component';
2
+
3
+ export const offcanvas = 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": "SemmetAngularOffcanvas",
4
+ "title": "Semmet Angular Offcanvas Options Schema",
5
+ "type": "object",
6
+ "description": "Generates a standalone Angular Offcanvas component using modal dialog semantics, focus trap, Escape close, and focus restoration.",
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 offcanvas 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,156 @@
1
+ :host {
2
+ --semmet-color-primary: #18181b;
3
+ --semmet-color-on-primary: #ffffff;
4
+ --semmet-color-surface: #ffffff;
5
+ --semmet-color-surface-muted: #f4f4f5;
6
+ --semmet-color-outline: #d4d4d8;
7
+ --semmet-color-on-surface: #27272a;
8
+ --semmet-color-on-surface-variant: #71717a;
9
+ --semmet-radius-md: 10px;
10
+ --semmet-elevation-2: 0 12px 28px rgba(20, 24, 23, 0.16), 0 4px 12px rgba(20, 24, 23, 0.1);
11
+ --semmet-motion: 160ms cubic-bezier(0.2, 0, 0, 1);
12
+
13
+ position: relative;
14
+ display: inline-block;
15
+ font-family: Roboto, system-ui, -apple-system, 'Segoe UI', sans-serif;
16
+ }
17
+
18
+ .semmet-popover__trigger {
19
+ min-height: 2.5rem;
20
+ padding: 0.5rem 1rem;
21
+ border: 1px solid var(--semmet-color-outline);
22
+ border-radius: var(--semmet-radius-md);
23
+ color: var(--semmet-color-on-surface);
24
+ background: var(--semmet-color-surface);
25
+ font: inherit;
26
+ font-weight: 650;
27
+ cursor: pointer;
28
+ }
29
+
30
+ .semmet-popover__trigger:focus-visible,
31
+ .semmet-popover__close:focus-visible,
32
+ .semmet-popover__panel:focus-visible {
33
+ outline: 2px solid var(--semmet-color-primary);
34
+ outline-offset: 2px;
35
+ }
36
+
37
+ .semmet-popover__panel {
38
+ position: absolute;
39
+ z-index: 20;
40
+ width: min(20rem, calc(100vw - 2rem));
41
+ padding: 1rem;
42
+ border: 1px solid var(--semmet-color-outline);
43
+ border-radius: var(--semmet-radius-md);
44
+ color: var(--semmet-color-on-surface);
45
+ background: var(--semmet-color-surface);
46
+ box-shadow: var(--semmet-elevation-2);
47
+ animation: semmet-popover-enter var(--semmet-motion);
48
+ }
49
+
50
+ .semmet-popover__panel--top {
51
+ left: 50%;
52
+ bottom: calc(100% + 0.5rem);
53
+ transform: translateX(-50%);
54
+ }
55
+
56
+ .semmet-popover__panel--right {
57
+ top: 50%;
58
+ left: calc(100% + 0.5rem);
59
+ transform: translateY(-50%);
60
+ }
61
+
62
+ .semmet-popover__panel--bottom {
63
+ top: calc(100% + 0.5rem);
64
+ left: 50%;
65
+ transform: translateX(-50%);
66
+ }
67
+
68
+ .semmet-popover__panel--left {
69
+ top: 50%;
70
+ right: calc(100% + 0.5rem);
71
+ transform: translateY(-50%);
72
+ }
73
+
74
+ .semmet-popover__header {
75
+ display: flex;
76
+ align-items: start;
77
+ justify-content: space-between;
78
+ gap: 1rem;
79
+ }
80
+
81
+ .semmet-popover__title,
82
+ .semmet-popover__body p {
83
+ margin: 0;
84
+ }
85
+
86
+ .semmet-popover__title {
87
+ font-size: 1rem;
88
+ line-height: 1.3;
89
+ }
90
+
91
+ .semmet-popover__body {
92
+ margin-block-start: 0.625rem;
93
+ color: var(--semmet-color-on-surface-variant);
94
+ line-height: 1.5;
95
+ }
96
+
97
+ .semmet-popover__close {
98
+ position: relative;
99
+ display: inline-flex;
100
+ align-items: center;
101
+ justify-content: center;
102
+ flex: 0 0 auto;
103
+ width: 2rem;
104
+ height: 2rem;
105
+ border: 0;
106
+ border-radius: 999px;
107
+ color: inherit;
108
+ background: transparent;
109
+ cursor: pointer;
110
+ }
111
+
112
+ .semmet-popover__close:hover {
113
+ background: var(--semmet-color-surface-muted);
114
+ }
115
+
116
+ .semmet-popover__close span,
117
+ .semmet-popover__close span::before,
118
+ .semmet-popover__close span::after {
119
+ display: block;
120
+ width: 0.875rem;
121
+ height: 0.125rem;
122
+ }
123
+
124
+ .semmet-popover__close span {
125
+ position: relative;
126
+ }
127
+
128
+ .semmet-popover__close span::before,
129
+ .semmet-popover__close span::after {
130
+ position: absolute;
131
+ top: 50%;
132
+ left: 0;
133
+ border-radius: 999px;
134
+ background: currentColor;
135
+ content: '';
136
+ }
137
+
138
+ .semmet-popover__close span::before {
139
+ transform: rotate(45deg);
140
+ }
141
+
142
+ .semmet-popover__close span::after {
143
+ transform: rotate(-45deg);
144
+ }
145
+
146
+ @keyframes semmet-popover-enter {
147
+ from {
148
+ opacity: 0;
149
+ }
150
+ }
151
+
152
+ @media (prefers-reduced-motion: reduce) {
153
+ .semmet-popover__panel {
154
+ animation-duration: 1ms;
155
+ }
156
+ }
@@ -0,0 +1,40 @@
1
+ <!-- Semmet Angular: Popover - disclosure trigger for short, contextual non-modal content. -->
2
+ <button
3
+ type="button"
4
+ class="semmet-popover__trigger"
5
+ aria-haspopup="dialog"
6
+ [attr.aria-expanded]="isOpen()"
7
+ [attr.aria-controls]="instanceId + '-panel'"
8
+ (click)="toggle()"
9
+ (keydown)="handleKeydown($event)"
10
+ >
11
+ {{ triggerLabel() }}
12
+ </button>
13
+
14
+ @if (isOpen()) {
15
+ <section
16
+ #popover
17
+ tabindex="-1"
18
+ role="dialog"
19
+ [id]="instanceId + '-panel'"
20
+ [class]="popoverClasses()"
21
+ [attr.aria-labelledby]="instanceId + '-title'"
22
+ (keydown)="handleKeydown($event)"
23
+ >
24
+ <div class="semmet-popover__header">
25
+ <h2 class="semmet-popover__title" [id]="instanceId + '-title'">{{ title() }}</h2>
26
+ <button
27
+ type="button"
28
+ class="semmet-popover__close"
29
+ [attr.aria-label]="closeLabel()"
30
+ (click)="close()"
31
+ >
32
+ <span aria-hidden="true"></span>
33
+ </button>
34
+ </div>
35
+ <div class="semmet-popover__body">
36
+ <p>{{ content() }}</p>
37
+ <ng-content></ng-content>
38
+ </div>
39
+ </section>
40
+ }
@@ -0,0 +1,65 @@
1
+ import { Component, ElementRef, Injector, afterNextRender, computed, inject, input, signal, viewChild } from '@angular/core';
2
+
3
+ type <%= classify(name) %>Placement = 'top' | 'right' | 'bottom' | 'left';
4
+
5
+ let nextId = 0;
6
+
7
+ @Component({
8
+ selector: '<%= selector %>',
9
+ imports: [],
10
+ templateUrl: './<%= dasherize(name) %>.html',
11
+ styleUrl: './<%= dasherize(name) %>.css',
12
+ host: {
13
+ '(document:click)': 'closeOnOutsideClick($event)',
14
+ },
15
+ })
16
+ export class <%= classify(name) %> {
17
+ private readonly hostRef = inject<ElementRef<HTMLElement>>(ElementRef);
18
+ private readonly injector = inject(Injector);
19
+ private readonly popoverRef = viewChild<ElementRef<HTMLElement>>('popover');
20
+
21
+ protected readonly instanceId = `<%= dasherize(name) %>-${nextId++}`;
22
+ protected readonly isOpen = signal(false);
23
+
24
+ readonly triggerLabel = input('More information');
25
+ readonly title = input('Helpful context');
26
+ readonly content = input('Use popovers for short contextual help that needs a button, link, or richer text.');
27
+ readonly closeLabel = input('Close popover');
28
+ readonly placement = input<<%= classify(name) %>Placement>('bottom');
29
+
30
+ protected readonly popoverClasses = computed(() => [
31
+ 'semmet-popover__panel',
32
+ `semmet-popover__panel--${this.placement()}`,
33
+ ].join(' '));
34
+
35
+ protected toggle(): void {
36
+ this.isOpen.update((value) => !value);
37
+ if (!this.isOpen()) {
38
+ return;
39
+ }
40
+
41
+ afterNextRender(() => this.popoverRef()?.nativeElement.focus(), { injector: this.injector });
42
+ }
43
+
44
+ protected close(): void {
45
+ this.isOpen.set(false);
46
+ }
47
+
48
+ protected handleKeydown(event: KeyboardEvent): void {
49
+ if (event.key === 'Escape') {
50
+ event.preventDefault();
51
+ this.close();
52
+ }
53
+ }
54
+
55
+ protected closeOnOutsideClick(event: MouseEvent): void {
56
+ if (!this.isOpen()) {
57
+ return;
58
+ }
59
+
60
+ const target = event.target as Node;
61
+ if (!this.hostRef.nativeElement.contains(target)) {
62
+ this.close();
63
+ }
64
+ }
65
+ }
@@ -0,0 +1 @@
1
+ export declare const popover: (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.popover = void 0;
4
+ const generate_component_1 = require("../utils/generate-component");
5
+ exports.popover = (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,OAAO,GAAG,IAAA,iDAA4B,GAAE,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { createAriaComponentSchematic } from '../utils/generate-component';
2
+
3
+ export const popover = 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":""}