@egose/shadcn-theme-ng-tw 0.1.0 → 0.2.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 +1 -1
- package/accordion/README.md +405 -2
- package/alert/README.md +372 -2
- package/alert-dialog/README.md +471 -5
- package/aspect-ratio/README.md +272 -5
- package/autocomplete/README.md +502 -2
- package/avatar/README.md +357 -5
- package/badge/README.md +318 -2
- package/basic-alert/README.md +353 -2
- package/breadcrumb/README.md +406 -5
- package/button/README.md +482 -2
- package/button/fesm2022/button.mjs +85 -107
- package/button/types/button.d.ts +5 -8
- package/button-group/README.md +318 -5
- package/button-group/fesm2022/button-group.mjs +1 -1
- package/calendar/README.md +357 -2
- package/card/README.md +331 -5
- package/carousel/README.md +333 -5
- package/carousel/fesm2022/carousel.mjs +4 -1
- package/checkbox/README.md +320 -2
- package/collapsible/README.md +332 -5
- package/combobox/README.md +507 -5
- package/combobox/fesm2022/combobox.mjs +4 -1
- package/command/README.md +435 -5
- package/confirmation-dialog/README.md +301 -2
- package/context-menu/README.md +366 -5
- package/date-picker/README.md +465 -2
- package/date-picker/fesm2022/date-picker.mjs +2 -2
- package/dialog/README.md +448 -2
- package/drawer/README.md +395 -5
- package/dropdown-menu/README.md +417 -5
- package/empty/README.md +329 -5
- package/field/README.md +385 -5
- package/form-checkbox/README.md +312 -2
- package/form-date-picker/README.md +322 -2
- package/form-field/README.md +356 -2
- package/form-field-simple/README.md +340 -2
- package/form-searchable-multiselect/README.md +361 -2
- package/form-select/README.md +350 -2
- package/form-text-input/README.md +371 -2
- package/form-textarea/README.md +347 -2
- package/hover-card/README.md +256 -5
- package/icon/README.md +239 -2
- package/input/README.md +269 -2
- package/input-group/README.md +335 -5
- package/input-group/fesm2022/input-group.mjs +3 -3
- package/input-otp/README.md +375 -5
- package/item/README.md +385 -5
- package/item/fesm2022/item.mjs +3 -3
- package/kbd/README.md +291 -5
- package/label/README.md +272 -2
- package/layout-simple/README.md +193 -2
- package/layout-simple/fesm2022/layout-simple.mjs +877 -409
- package/layout-simple/types/layout-simple.d.ts +174 -137
- package/menu/README.md +417 -2
- package/menubar/README.md +343 -5
- package/native-select/README.md +323 -5
- package/navigation-menu/README.md +369 -5
- package/package.json +1 -1
- package/pagination/README.md +388 -5
- package/popover/README.md +331 -2
- package/progress/README.md +311 -5
- package/radio-group/README.md +364 -2
- package/radio-group/fesm2022/radio-group.mjs +5 -1
- package/resizable/README.md +269 -5
- package/scroll-area/README.md +233 -5
- package/searchable-multiselect/README.md +323 -2
- package/select/README.md +437 -2
- package/separator/README.md +222 -2
- package/sheet/README.md +311 -2
- package/sidebar/README.md +457 -5
- package/skeleton/README.md +217 -5
- package/slider/README.md +273 -5
- package/slider/fesm2022/slider.mjs +17 -13
- package/sonner/README.md +346 -2
- package/spinner/README.md +284 -2
- package/switch/README.md +310 -2
- package/table/README.md +423 -5
- package/tabs/README.md +411 -2
- package/tabs/fesm2022/tabs.mjs +12 -2
- package/textarea/README.md +282 -5
- package/toggle/README.md +270 -5
- package/toggle-group/README.md +340 -5
- package/tooltip/README.md +269 -2
- package/typography/README.md +271 -5
- package/utils/README.md +303 -2
package/form-textarea/README.md
CHANGED
|
@@ -1,3 +1,348 @@
|
|
|
1
|
-
# Form Textarea
|
|
1
|
+
# Form Textarea (`@egose/shadcn-theme-ng/form-textarea`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
`EgFormTextarea` is a reactive-forms wrapper that bundles a `<label>`, a styled `<textarea hlmInput>`, and error/hint text into one form-ready row. It is the Angular equivalent of shadcn/ui's `<FormField> + <Textarea>` pattern for multiline input (comments, descriptions, bios), with generated ids, `aria-describedby` wiring, and `disabled` resolution handled for you.
|
|
4
|
+
|
|
5
|
+
> **Ships as:** `@egose/shadcn-theme-ng/form-textarea` (plain Tailwind) and `@egose/shadcn-theme-ng-tw/form-textarea` (`tw:`-prefixed variant). See the [package README](../../README.md) for installation, peer dependencies, and the Tailwind-variant contract. Do not publish this project directory independently.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
# Plain Tailwind (no prefix)
|
|
11
|
+
npm install @egose/shadcn-theme-ng
|
|
12
|
+
|
|
13
|
+
# Or the tw:-prefixed variant
|
|
14
|
+
npm install @egose/shadcn-theme-ng-tw
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Peer dependencies (Angular, `@angular/forms`, CDK, `@spartan-ng/brain`, `rxjs`) are documented in the [package README](../../README.md#peer-dependencies).
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { EgFormTextarea } from '@egose/shadcn-theme-ng/form-textarea';
|
|
21
|
+
// tw variant:
|
|
22
|
+
// import { EgFormTextarea } from '@egose/shadcn-theme-ng-tw/form-textarea';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Imports
|
|
26
|
+
|
|
27
|
+
The public API (`src/public-api.ts`) exports exactly one symbol — the standalone component `EgFormTextarea`. There is no `*Imports` array and no `*Module` for this subpath; import the component class directly.
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
import { EgFormTextarea } from '@egose/shadcn-theme-ng/form-textarea';
|
|
31
|
+
|
|
32
|
+
@Component({
|
|
33
|
+
standalone: true,
|
|
34
|
+
imports: [ReactiveFormsModule, EgFormTextarea],
|
|
35
|
+
template: `
|
|
36
|
+
<form [formGroup]="form">
|
|
37
|
+
<eg-form-textarea controlName="bio" label="Bio" placeholder="Tell us about yourself" />
|
|
38
|
+
</form>
|
|
39
|
+
`,
|
|
40
|
+
})
|
|
41
|
+
export class MyForm {}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Requirements:
|
|
45
|
+
|
|
46
|
+
- Must sit inside a `<form [formGroup]>` (it injects `FormGroupDirective` and provides `ControlContainer`).
|
|
47
|
+
- `controlName` is required — forwarded as `[formControlName]` to the inner `<textarea>`.
|
|
48
|
+
- The bound `FormControl` holds a `string`.
|
|
49
|
+
|
|
50
|
+
## Anatomy / Structure
|
|
51
|
+
|
|
52
|
+
```html
|
|
53
|
+
<eg-form-textarea controlName="bio" label="Bio">
|
|
54
|
+
<!-- rendered internally -->
|
|
55
|
+
<hlm-form-field>
|
|
56
|
+
<label hlmLabel for="<effectiveId>">Bio <span>*</span></label>
|
|
57
|
+
<textarea
|
|
58
|
+
hlmInput
|
|
59
|
+
id="<effectiveId>"
|
|
60
|
+
name="bio"
|
|
61
|
+
formControlName="bio"
|
|
62
|
+
rows="3"
|
|
63
|
+
placeholder="…"
|
|
64
|
+
aria-describedby="<effectiveId>-error | <effectiveId>-hint"
|
|
65
|
+
></textarea>
|
|
66
|
+
<hlm-error id="<effectiveId>-error">…</hlm-error>
|
|
67
|
+
<hlm-hint id="<effectiveId>-hint">…</hlm-hint>
|
|
68
|
+
</hlm-form-field>
|
|
69
|
+
</eg-form-textarea>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Real selectors: `eg-form-textarea`, `hlm-form-field`, `label[hlmLabel]`, `textarea[hlmInput]`, `hlm-error`, `hlm-hint`. Note the inner textarea reuses the `hlmInput` styling directive (same class hook as single-line inputs).
|
|
73
|
+
|
|
74
|
+
## API reference
|
|
75
|
+
|
|
76
|
+
### `eg-form-textarea` — `EgFormTextarea`
|
|
77
|
+
|
|
78
|
+
| Input | Type | Default | Description |
|
|
79
|
+
| --------------------- | ------------------------------- | ----------- | ------------------------------------------------------------------------------------------------ |
|
|
80
|
+
| `label` | `string \| undefined` | `undefined` | Field label rendered as `<label hlmLabel>` bound to the textarea id. |
|
|
81
|
+
| `controlName` | `string` | `''` | **Required.** Control name in the parent `FormGroup`; forwarded as `formControlName` and `name`. |
|
|
82
|
+
| `controlId` | `string \| undefined` | `undefined` | Explicit id; falls back to `id`, then generated `eg-form-textarea-…`. |
|
|
83
|
+
| `id` | `string \| undefined` | `undefined` | Alias for an explicit id (same fallback chain). |
|
|
84
|
+
| `error` | `string \| undefined` | `undefined` | Error text rendered in `<hlm-error>`. |
|
|
85
|
+
| `hint` | `string \| undefined` | `undefined` | Hint text rendered in `<hlm-hint>`. |
|
|
86
|
+
| `name` | `string \| undefined` | `undefined` | `name` attribute fallback when `controlName` is empty. |
|
|
87
|
+
| `placeholder` | `string` | `''` | Placeholder text. |
|
|
88
|
+
| `readonly` | `boolean` | `false` | Native `readonly` attribute. |
|
|
89
|
+
| `disabled` | `boolean` | `false` | Wrapper-level disable (OR-ed with the reactive disabled state — see `effectiveDisabled()`). |
|
|
90
|
+
| `maxlength` | `string \| number \| null` | `null` | Native `maxlength`. |
|
|
91
|
+
| `minlength` | `string \| number \| null` | `null` | Native `minlength`. |
|
|
92
|
+
| `required` | `boolean` | `false` | Native `required` + red `*` on the label. |
|
|
93
|
+
| `rows` | `string \| number \| undefined` | `3` | Native `rows` (visible height). |
|
|
94
|
+
| `cols` | `string \| number \| undefined` | `undefined` | Native `cols` (visible width). |
|
|
95
|
+
| `class` (`userClass`) | `ClassValue` | `''` | Extra host classes (merged over `tw:flex tw:flex-col`). |
|
|
96
|
+
| `labelClass` | `string` | `''` | Extra label classes (merged over `tw:mb-1 tw:gap-0`). |
|
|
97
|
+
| `textareaClass` | `string` | `''` | Extra textarea classes (merged over `tw:mb-1`). |
|
|
98
|
+
| `errorClass` | `string` | `''` | Extra error classes (merged over `tw:mt-0`). |
|
|
99
|
+
| `hintClass` | `string` | `''` | Extra hint classes (merged over `tw:mt-0`). |
|
|
100
|
+
|
|
101
|
+
No outputs. Methods: `describedBy(): string | null` (error id when invalid + dirty/touched, else hint id, else `null`); `effectiveDisabled(): boolean` (`disabled()` OR the control's reactive disabled state). Readonly computeds: `effectiveId()`, `errorId()`, `hintId()`.
|
|
102
|
+
|
|
103
|
+
## Examples
|
|
104
|
+
|
|
105
|
+
### 1. Basic comment box
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { Component } from '@angular/core';
|
|
109
|
+
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
110
|
+
import { EgFormTextarea } from '@egose/shadcn-theme-ng/form-textarea';
|
|
111
|
+
|
|
112
|
+
@Component({
|
|
113
|
+
standalone: true,
|
|
114
|
+
imports: [ReactiveFormsModule, EgFormTextarea],
|
|
115
|
+
template: `
|
|
116
|
+
<form [formGroup]="form" (ngSubmit)="post()">
|
|
117
|
+
<eg-form-textarea controlName="comment" label="Comment" placeholder="Write a comment…" [rows]="4" />
|
|
118
|
+
<button type="submit">Post</button>
|
|
119
|
+
</form>
|
|
120
|
+
`,
|
|
121
|
+
})
|
|
122
|
+
export class BasicExample {
|
|
123
|
+
readonly form = new FormGroup({
|
|
124
|
+
comment: new FormControl<string>('', { nonNullable: true }),
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
post() {
|
|
128
|
+
console.log(this.form.value.comment);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 2. Sizes via `rows` / `cols` / `textareaClass`
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
import { Component } from '@angular/core';
|
|
137
|
+
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
138
|
+
import { EgFormTextarea } from '@egose/shadcn-theme-ng/form-textarea';
|
|
139
|
+
|
|
140
|
+
@Component({
|
|
141
|
+
standalone: true,
|
|
142
|
+
imports: [ReactiveFormsModule, EgFormTextarea],
|
|
143
|
+
template: `
|
|
144
|
+
<form [formGroup]="form" class="tw:grid tw:gap-4">
|
|
145
|
+
<eg-form-textarea controlName="short" label="Compact (2 rows)" [rows]="2" />
|
|
146
|
+
<eg-form-textarea controlName="default" label="Default (3 rows)" />
|
|
147
|
+
<eg-form-textarea controlName="tall" label="Tall composer" [rows]="8" textareaClass="tw:resize-y tw:min-h-40" />
|
|
148
|
+
</form>
|
|
149
|
+
`,
|
|
150
|
+
})
|
|
151
|
+
export class SizesExample {
|
|
152
|
+
readonly form = new FormGroup({
|
|
153
|
+
short: new FormControl<string>('', { nonNullable: true }),
|
|
154
|
+
default: new FormControl<string>('', { nonNullable: true }),
|
|
155
|
+
tall: new FormControl<string>('', { nonNullable: true }),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### 3. Validation with a live character counter
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { Component } from '@angular/core';
|
|
164
|
+
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
165
|
+
import { EgFormTextarea } from '@egose/shadcn-theme-ng/form-textarea';
|
|
166
|
+
|
|
167
|
+
@Component({
|
|
168
|
+
standalone: true,
|
|
169
|
+
imports: [ReactiveFormsModule, EgFormTextarea],
|
|
170
|
+
template: `
|
|
171
|
+
<form [formGroup]="form" (ngSubmit)="submit()">
|
|
172
|
+
<eg-form-textarea
|
|
173
|
+
controlName="bio"
|
|
174
|
+
label="Bio"
|
|
175
|
+
placeholder="A few sentences about you"
|
|
176
|
+
[rows]="5"
|
|
177
|
+
[maxlength]="280"
|
|
178
|
+
[hint]="counter()"
|
|
179
|
+
error="Bio must be 20–280 characters"
|
|
180
|
+
required
|
|
181
|
+
/>
|
|
182
|
+
<button type="submit" [disabled]="form.invalid">Save</button>
|
|
183
|
+
</form>
|
|
184
|
+
`,
|
|
185
|
+
})
|
|
186
|
+
export class CounterExample {
|
|
187
|
+
readonly form = new FormGroup({
|
|
188
|
+
bio: new FormControl<string>('', {
|
|
189
|
+
nonNullable: true,
|
|
190
|
+
validators: [Validators.required, Validators.minLength(20), Validators.maxLength(280)],
|
|
191
|
+
}),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
counter(): string {
|
|
195
|
+
return `${this.form.controls.bio.value.length}/280`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
submit() {
|
|
199
|
+
if (this.form.invalid) {
|
|
200
|
+
this.form.markAllAsTouched();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### 4. Disabled and readonly states
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
import { Component, signal } from '@angular/core';
|
|
211
|
+
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
212
|
+
import { EgFormTextarea } from '@egose/shadcn-theme-ng/form-textarea';
|
|
213
|
+
|
|
214
|
+
@Component({
|
|
215
|
+
standalone: true,
|
|
216
|
+
imports: [ReactiveFormsModule, EgFormTextarea],
|
|
217
|
+
template: `
|
|
218
|
+
<form [formGroup]="form">
|
|
219
|
+
<eg-form-textarea
|
|
220
|
+
controlName="notes"
|
|
221
|
+
label="Internal notes"
|
|
222
|
+
[rows]="4"
|
|
223
|
+
[disabled]="locked()"
|
|
224
|
+
hint="Toggle the lock to edit"
|
|
225
|
+
/>
|
|
226
|
+
<eg-form-textarea controlName="audit" label="Audit trail" [rows]="3" [readonly]="true" />
|
|
227
|
+
<button type="button" (click)="locked.update((v) => !v)">Toggle lock</button>
|
|
228
|
+
</form>
|
|
229
|
+
`,
|
|
230
|
+
})
|
|
231
|
+
export class StatesExample {
|
|
232
|
+
readonly form = new FormGroup({
|
|
233
|
+
notes: new FormControl<string>('Called back twice.', { nonNullable: true }),
|
|
234
|
+
audit: new FormControl<string>('Created 2026-09-01 · Updated 2026-09-20', { nonNullable: true }),
|
|
235
|
+
});
|
|
236
|
+
readonly locked = signal(true);
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### 5. Feedback form composition (input + textarea + select-side hint)
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { Component } from '@angular/core';
|
|
244
|
+
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
245
|
+
import { EgFormTextInput } from '@egose/shadcn-theme-ng/form-text-input';
|
|
246
|
+
import { EgFormTextarea } from '@egose/shadcn-theme-ng/form-textarea';
|
|
247
|
+
|
|
248
|
+
@Component({
|
|
249
|
+
standalone: true,
|
|
250
|
+
imports: [ReactiveFormsModule, EgFormTextInput, EgFormTextarea],
|
|
251
|
+
template: `
|
|
252
|
+
<form [formGroup]="form" (ngSubmit)="send()" class="tw:grid tw:gap-4">
|
|
253
|
+
<eg-form-text-input controlName="subject" label="Subject" required />
|
|
254
|
+
<eg-form-textarea
|
|
255
|
+
controlName="message"
|
|
256
|
+
label="Message"
|
|
257
|
+
placeholder="Describe the issue in detail…"
|
|
258
|
+
[rows]="6"
|
|
259
|
+
[maxlength]="2000"
|
|
260
|
+
[hint]="messageHint()"
|
|
261
|
+
error="Message is required (min 20 characters)"
|
|
262
|
+
required
|
|
263
|
+
/>
|
|
264
|
+
<button type="submit">Send feedback</button>
|
|
265
|
+
</form>
|
|
266
|
+
`,
|
|
267
|
+
})
|
|
268
|
+
export class FeedbackExample {
|
|
269
|
+
readonly form = new FormGroup({
|
|
270
|
+
subject: new FormControl<string>('', { nonNullable: true, validators: Validators.required }),
|
|
271
|
+
message: new FormControl<string>('', {
|
|
272
|
+
nonNullable: true,
|
|
273
|
+
validators: [Validators.required, Validators.minLength(20), Validators.maxLength(2000)],
|
|
274
|
+
}),
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
messageHint(): string {
|
|
278
|
+
return `${this.form.controls.message.value.length}/2000 · Markdown supported`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
send() {
|
|
282
|
+
if (this.form.invalid) {
|
|
283
|
+
this.form.markAllAsTouched();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
console.log(this.form.getRawValue());
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 6. Programmatic control (templates, reset, autosize note)
|
|
292
|
+
|
|
293
|
+
There is no auto-grow behavior — height is fixed by `rows` (plus `textareaClass` resize utilities). Insert canned text and reset programmatically:
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import { Component } from '@angular/core';
|
|
297
|
+
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
|
|
298
|
+
import { EgFormTextarea } from '@egose/shadcn-theme-ng/form-textarea';
|
|
299
|
+
|
|
300
|
+
@Component({
|
|
301
|
+
standalone: true,
|
|
302
|
+
imports: [ReactiveFormsModule, EgFormTextarea],
|
|
303
|
+
template: `
|
|
304
|
+
<form [formGroup]="form">
|
|
305
|
+
<eg-form-textarea
|
|
306
|
+
#descField
|
|
307
|
+
controlName="description"
|
|
308
|
+
label="Description"
|
|
309
|
+
[rows]="6"
|
|
310
|
+
textareaClass="tw:resize-y"
|
|
311
|
+
hint="Tip: allow vertical resize for long drafts"
|
|
312
|
+
/>
|
|
313
|
+
<div class="tw:flex tw:gap-2">
|
|
314
|
+
<button type="button" (click)="insertTemplate()">Insert template</button>
|
|
315
|
+
<button type="button" (click)="form.controls.description.reset()">Clear</button>
|
|
316
|
+
</div>
|
|
317
|
+
<p>Field id: {{ descField.effectiveId() }}</p>
|
|
318
|
+
</form>
|
|
319
|
+
`,
|
|
320
|
+
})
|
|
321
|
+
export class ProgrammaticExample {
|
|
322
|
+
readonly form = new FormGroup({
|
|
323
|
+
description: new FormControl<string>('', { nonNullable: true }),
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
insertTemplate() {
|
|
327
|
+
this.form.controls.description.setValue('## Summary\n\n## Steps to reproduce\n\n1. \n2. \n');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## Accessibility notes
|
|
333
|
+
|
|
334
|
+
- `<label [for]>` targets the textarea id (explicit `controlId`/`id` or generated), so label clicks focus the field.
|
|
335
|
+
- `aria-describedby` points at the error element only when invalid and dirty/touched, otherwise at the hint — pair long hints (formatting tips, counters) with concise `error` strings so screen-reader users hear the failure, not the help text, on error.
|
|
336
|
+
- The textarea reuses `HlmInput` styling (spartan invalid-state hooks included). Use `maxlength` + a counter hint for constrained fields so users get feedback before hitting the native limit.
|
|
337
|
+
- Avoid `autofocus`-style behavior here (the component exposes none) — move focus deliberately in wizards instead.
|
|
338
|
+
|
|
339
|
+
## Theming / CSS variables
|
|
340
|
+
|
|
341
|
+
No component-specific CSS variables; visuals come from the shared theme tokens via the `hlmInput` class hook. Control height with `rows` and `textareaClass` (`tw:min-h-*`, `tw:resize-y` / `tw:resize-none`).
|
|
342
|
+
|
|
343
|
+
## Related subpaths
|
|
344
|
+
|
|
345
|
+
- `@egose/shadcn-theme-ng/form-text-input` — the single-line sibling.
|
|
346
|
+
- `@egose/shadcn-theme-ng/textarea` — the raw `HlmTextarea` directive for non-form usage.
|
|
347
|
+
- `@egose/shadcn-theme-ng/input` — `HlmInput`, the class hook shared by both.
|
|
348
|
+
- `@egose/shadcn-theme-ng/form-field` — `HlmFormField`, `HlmError`, `HlmHint`, `HlmFormIdGenerator`.
|
package/hover-card/README.md
CHANGED
|
@@ -1,11 +1,262 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Hover Card (`@egose/shadcn-theme-ng/hover-card`)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The hover card shows a rich preview panel when the user hovers (or focuses) a trigger — the shadcn/ui _HoverCard_ equivalent (docs link with an author preview, usercard on an `@mention`, product peek on hover). Behavior comes from spartan-ng's `BrnHoverCard` family (CDK overlay under the hood); this subpath adds the shadcn styling and `data-slot` hooks.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> **Ships as:** `@egose/shadcn-theme-ng/hover-card` (plain Tailwind) and `@egose/shadcn-theme-ng-tw/hover-card` (`tw:`-prefixed variant). See the [package README](../../README.md) for installation, peer dependencies, and the Tailwind-variant contract. Do not publish this project directory independently.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Installation
|
|
8
8
|
|
|
9
9
|
```bash
|
|
10
|
-
|
|
10
|
+
# Plain Tailwind (no prefix)
|
|
11
|
+
npm install @egose/shadcn-theme-ng
|
|
12
|
+
|
|
13
|
+
# Or the tw:-prefixed variant
|
|
14
|
+
npm install @egose/shadcn-theme-ng-tw
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Peer dependencies (Angular, CDK, `@spartan-ng/brain`, `rxjs`) are documented in the [package README](../../README.md#peer-dependencies).
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { HlmHoverCardImports } from '@egose/shadcn-theme-ng/hover-card';
|
|
21
|
+
// tw variant:
|
|
22
|
+
// import { HlmHoverCardImports } from '@egose/shadcn-theme-ng-tw/hover-card';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Imports
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
// Standalone component — spread the imports array:
|
|
29
|
+
import { HlmHoverCardImports } from '@egose/shadcn-theme-ng/hover-card';
|
|
30
|
+
|
|
31
|
+
@Component({
|
|
32
|
+
standalone: true,
|
|
33
|
+
imports: [HlmHoverCardImports],
|
|
34
|
+
template: `…`,
|
|
35
|
+
})
|
|
36
|
+
export class MyComp {}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// NgModule-based — import the module:
|
|
41
|
+
import { HlmHoverCardModule } from '@egose/shadcn-theme-ng/hover-card';
|
|
42
|
+
|
|
43
|
+
@NgModule({ imports: [HlmHoverCardModule] })
|
|
44
|
+
export class MyModule {}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Individual symbols (all exported from `src/public-api.ts`): `HlmHoverCard`, `HlmHoverCardTrigger`, `HlmHoverCardContent`, `HlmHoverCardPortal`, plus `HlmHoverCardImports` and `HlmHoverCardModule`.
|
|
48
|
+
|
|
49
|
+
## Anatomy / Structure
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<div hlmHoverCard>
|
|
53
|
+
<a href="https://example.com" hlmHoverCardTrigger>Hover me</a>
|
|
54
|
+
|
|
55
|
+
<!-- structural portal directive + styling directive on the same element -->
|
|
56
|
+
<hlm-hover-card-content *hlmHoverCardPortal>
|
|
57
|
+
<p class="tw:font-semibold">@example</p>
|
|
58
|
+
<p class="tw:text-sm tw:text-gray-500">Preview content goes here.</p>
|
|
59
|
+
</hlm-hover-card-content>
|
|
60
|
+
</div>
|
|
11
61
|
```
|
|
62
|
+
|
|
63
|
+
Real selectors: `[hlmHoverCard]` / `hlm-hover-card` (root, `hostDirectives: [BrnHoverCard]`), `[hlmHoverCardTrigger]` (trigger, `hostDirectives: [BrnHoverCardTrigger]`), `[hlmHoverCardPortal]` / `hlm-hover-card-portal` (structural overlay portal, `hostDirectives: [BrnHoverCardContent]` — used as `*hlmHoverCardPortal`), `[hlmHoverCardContent]` / `hlm-hover-card-content` (styling shell: popover colors, `w-64`, rounded, shadow, open/close animations; mirrors `data-state`/`data-side` attributes).
|
|
64
|
+
|
|
65
|
+
## API reference
|
|
66
|
+
|
|
67
|
+
### `hlmHoverCard` / `hlm-hover-card` — `HlmHoverCard`
|
|
68
|
+
|
|
69
|
+
Thin directive wrapper: `hostDirectives: [BrnHoverCard]`, `data-slot="hover-card"`. No inputs/outputs of its own — it owns the open state shared by trigger and portal.
|
|
70
|
+
|
|
71
|
+
### `[hlmHoverCardTrigger]` — `HlmHoverCardTrigger`
|
|
72
|
+
|
|
73
|
+
Thin directive wrapper forwarding these inputs to `BrnHoverCardTrigger` (plus `data-slot="hover-card-trigger"`):
|
|
74
|
+
|
|
75
|
+
| Input | Type | Description |
|
|
76
|
+
| ------------------------ | ------- | ----------------------------------------------------------------------------------- |
|
|
77
|
+
| `showDelay` | (brain) | Delay before the card opens on hover. |
|
|
78
|
+
| `hideDelay` | (brain) | Delay before the card closes after pointer leave. |
|
|
79
|
+
| `animationDelay` | (brain) | Delay applied to the open/close animation. |
|
|
80
|
+
| `sideOffset` | (brain) | Pixel offset between trigger and card. |
|
|
81
|
+
| `align` | (brain) | Overlay alignment (`start` / `center` / `end`). |
|
|
82
|
+
| `hlmHoverCardTriggerFor` | (brain) | Explicit content reference (`brnHoverCardTriggerFor` alias) for non-default wiring. |
|
|
83
|
+
|
|
84
|
+
### `[hlmHoverCardPortal]` / `hlm-hover-card-portal` — `HlmHoverCardPortal`
|
|
85
|
+
|
|
86
|
+
Structural directive (`hostDirectives: [BrnHoverCardContent]`). Apply as `*hlmHoverCardPortal` on the content element so the card renders in the CDK overlay. No inputs of its own.
|
|
87
|
+
|
|
88
|
+
### `[hlmHoverCardContent]` / `hlm-hover-card-content` — `HlmHoverCardContent`
|
|
89
|
+
|
|
90
|
+
Styling directive for the panel. No inputs/outputs; it reads the overlay `state` (`open`/`closed`) and `side` (`top`/`bottom`/`left`/`right`) signals from the brain providers and reflects them as `data-state` / `data-side` attributes for the animation classes. Default width is `tw:w-64` — override with `class`.
|
|
91
|
+
|
|
92
|
+
## Examples
|
|
93
|
+
|
|
94
|
+
### 1. Basic link preview
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { Component } from '@angular/core';
|
|
98
|
+
import { HlmHoverCardImports } from '@egose/shadcn-theme-ng/hover-card';
|
|
99
|
+
|
|
100
|
+
@Component({
|
|
101
|
+
standalone: true,
|
|
102
|
+
imports: [HlmHoverCardImports],
|
|
103
|
+
template: `
|
|
104
|
+
<div hlmHoverCard>
|
|
105
|
+
<a href="https://angular.dev" hlmHoverCardTrigger class="tw:cursor-pointer tw:underline tw:text-blue-600">
|
|
106
|
+
Angular
|
|
107
|
+
</a>
|
|
108
|
+
<hlm-hover-card-content *hlmHoverCardPortal>
|
|
109
|
+
<p class="tw:font-semibold">Angular</p>
|
|
110
|
+
<p class="tw:text-sm tw:text-gray-500">The web development framework for the modern web.</p>
|
|
111
|
+
</hlm-hover-card-content>
|
|
112
|
+
</div>
|
|
113
|
+
`,
|
|
114
|
+
})
|
|
115
|
+
export class BasicExample {}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 2. Open/close delays
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { Component } from '@angular/core';
|
|
122
|
+
import { HlmHoverCardImports } from '@egose/shadcn-theme-ng/hover-card';
|
|
123
|
+
|
|
124
|
+
@Component({
|
|
125
|
+
standalone: true,
|
|
126
|
+
imports: [HlmHoverCardImports],
|
|
127
|
+
template: `
|
|
128
|
+
<div hlmHoverCard>
|
|
129
|
+
<button hlmHoverCardTrigger type="button" [showDelay]="400" [hideDelay]="200">Hover (opens after 400ms)</button>
|
|
130
|
+
<hlm-hover-card-content *hlmHoverCardPortal>
|
|
131
|
+
<p class="tw:text-sm">Delays keep accidental hovers from flashing the card.</p>
|
|
132
|
+
</hlm-hover-card-content>
|
|
133
|
+
</div>
|
|
134
|
+
`,
|
|
135
|
+
})
|
|
136
|
+
export class DelaysExample {}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 3. Placement: side, align, offset
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import { Component } from '@angular/core';
|
|
143
|
+
import { HlmHoverCardImports } from '@egose/shadcn-theme-ng/hover-card';
|
|
144
|
+
|
|
145
|
+
@Component({
|
|
146
|
+
standalone: true,
|
|
147
|
+
imports: [HlmHoverCardImports],
|
|
148
|
+
template: `
|
|
149
|
+
<div hlmHoverCard>
|
|
150
|
+
<span hlmHoverCardTrigger [sideOffset]="12" align="start"> Hover for right-side card </span>
|
|
151
|
+
<hlm-hover-card-content *hlmHoverCardPortal class="tw:w-72">
|
|
152
|
+
<p class="tw:text-sm">Offset by 12px, aligned to the trigger start edge.</p>
|
|
153
|
+
</hlm-hover-card-content>
|
|
154
|
+
</div>
|
|
155
|
+
`,
|
|
156
|
+
})
|
|
157
|
+
export class PlacementExample {}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
> `side` itself is owned by the brain content directive — set alignment/offset on the trigger; the `data-side` attribute on the content element reflects the resolved side for animations.
|
|
161
|
+
|
|
162
|
+
### 4. User card with avatar + stats
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
import { Component } from '@angular/core';
|
|
166
|
+
import { HlmHoverCardImports } from '@egose/shadcn-theme-ng/hover-card';
|
|
167
|
+
import { HlmAvatarImports } from '@egose/shadcn-theme-ng/avatar';
|
|
168
|
+
|
|
169
|
+
@Component({
|
|
170
|
+
standalone: true,
|
|
171
|
+
imports: [HlmHoverCardImports, HlmAvatarImports],
|
|
172
|
+
template: `
|
|
173
|
+
<div hlmHoverCard>
|
|
174
|
+
<span hlmHoverCardTrigger class="tw:cursor-pointer tw:font-medium tw:underline">@jane</span>
|
|
175
|
+
<hlm-hover-card-content *hlmHoverCardPortal class="tw:w-80">
|
|
176
|
+
<div class="tw:flex tw:gap-4">
|
|
177
|
+
<hlm-avatar size="sm">
|
|
178
|
+
<img hlmAvatarImage src="https://github.com/jane.png" alt="Jane's avatar" />
|
|
179
|
+
<span hlmAvatarFallback>JA</span>
|
|
180
|
+
</hlm-avatar>
|
|
181
|
+
<div>
|
|
182
|
+
<p class="tw:font-semibold">@jane</p>
|
|
183
|
+
<p class="tw:text-sm tw:text-gray-500">Design engineer. Ships accessible UI.</p>
|
|
184
|
+
<p class="tw:mt-2 tw:text-xs tw:text-gray-500">1.2k followers · 84 following</p>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</hlm-hover-card-content>
|
|
188
|
+
</div>
|
|
189
|
+
`,
|
|
190
|
+
})
|
|
191
|
+
export class UserCardExample {}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### 5. Wide content (override the default `w-64`)
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { Component } from '@angular/core';
|
|
198
|
+
import { HlmHoverCardImports } from '@egose/shadcn-theme-ng/hover-card';
|
|
199
|
+
|
|
200
|
+
@Component({
|
|
201
|
+
standalone: true,
|
|
202
|
+
imports: [HlmHoverCardImports],
|
|
203
|
+
template: `
|
|
204
|
+
<div hlmHoverCard>
|
|
205
|
+
<button hlmHoverCardTrigger type="button">Product peek</button>
|
|
206
|
+
<hlm-hover-card-content *hlmHoverCardPortal class="tw:w-96">
|
|
207
|
+
<div class="tw:grid tw:grid-cols-[64px_1fr] tw:gap-3">
|
|
208
|
+
<div class="tw:h-16 tw:w-16 tw:rounded-md tw:bg-slate-200"></div>
|
|
209
|
+
<div>
|
|
210
|
+
<p class="tw:font-semibold">Ergonomic keyboard</p>
|
|
211
|
+
<p class="tw:text-sm tw:text-gray-500">Silent switches · $149 · In stock</p>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</hlm-hover-card-content>
|
|
215
|
+
</div>
|
|
216
|
+
`,
|
|
217
|
+
})
|
|
218
|
+
export class WideExample {}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### 6. NgModule usage + keyboard-focusable trigger
|
|
222
|
+
|
|
223
|
+
Hover cards also open on keyboard focus, so a natively focusable trigger keeps them reachable without a mouse:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import { NgModule, Component } from '@angular/core';
|
|
227
|
+
import { HlmHoverCardModule } from '@egose/shadcn-theme-ng/hover-card';
|
|
228
|
+
|
|
229
|
+
@Component({
|
|
230
|
+
selector: 'app-hover-demo',
|
|
231
|
+
template: `
|
|
232
|
+
<div hlmHoverCard>
|
|
233
|
+
<button hlmHoverCardTrigger type="button">Focus me with Tab, then hover away</button>
|
|
234
|
+
<hlm-hover-card-content *hlmHoverCardPortal>
|
|
235
|
+
<p class="tw:text-sm">Focus opens the card too — no mouse required.</p>
|
|
236
|
+
</hlm-hover-card-content>
|
|
237
|
+
</div>
|
|
238
|
+
`,
|
|
239
|
+
})
|
|
240
|
+
export class HoverDemoComponent {}
|
|
241
|
+
|
|
242
|
+
@NgModule({ declarations: [HoverDemoComponent], imports: [HlmHoverCardModule] })
|
|
243
|
+
export class HoverDemoModule {}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Accessibility notes
|
|
247
|
+
|
|
248
|
+
- Use a natively focusable trigger (`<a href>`, `<button>`) — the card opens on focus as well as hover, which keeps keyboard users in the loop.
|
|
249
|
+
- Keep the card content supplementary: never put the _only_ copy of critical information (or interactive controls) inside a hover-only panel, since touch users and keyboard users may dismiss it easily.
|
|
250
|
+
- The brain layer handles overlay dismissal (Escape / outside pointer). Avoid `showDelay`s so long that keyboard focus has already moved on.
|
|
251
|
+
- If the trigger is an icon or avatar without text, give it an `aria-label` describing what the preview shows.
|
|
252
|
+
|
|
253
|
+
## Theming / CSS variables
|
|
254
|
+
|
|
255
|
+
No component-specific CSS variables; the panel uses the shared `--popover` / `--popover-foreground` / `--ring` tokens. Widen with `class="tw:w-…"`, or restyle padding via `class` overrides.
|
|
256
|
+
|
|
257
|
+
## Related subpaths
|
|
258
|
+
|
|
259
|
+
- `@egose/shadcn-theme-ng/popover` — click-triggered counterpart (same portal/content pattern).
|
|
260
|
+
- `@egose/shadcn-theme-ng/tooltip` — lightweight text-only hover hints.
|
|
261
|
+
- `@egose/shadcn-theme-ng/avatar` — typical media inside user preview cards.
|
|
262
|
+
- `@egose/shadcn-theme-ng/dialog` — modal alternative when the content needs interaction.
|