@styloviz/accordion 0.1.1
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/LICENSE +21 -0
- package/README.md +55 -0
- package/fesm2022/styloviz-accordion.mjs +272 -0
- package/fesm2022/styloviz-accordion.mjs.map +1 -0
- package/package.json +42 -0
- package/types/styloviz-accordion.d.ts +124 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 StyloViz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @styloviz/accordion
|
|
2
|
+
|
|
3
|
+
> 10 varieties: default, flush, separated, filled, multi-expand, icon placements, badges & subtitles and disabled items. Sizes (sm / md / lg), CSS-grid animation and keyboard navigation are free; multi-expand is available in the Pro package.
|
|
4
|
+
|
|
5
|
+
Part of the **StyloViz UI Kit** — a premium Angular 21 + Tailwind CSS 4 dashboard component library. Standalone, `OnPush`, strongly typed, dark-mode ready.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @styloviz/core @styloviz/accordion
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires `@angular/core` and `@angular/common` >= 21. `@styloviz/core` is a peer dependency shared by every component. Prefer everything at once? `@styloviz/all` installs the whole free tier in one command.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { SvAccordionComponent } from '@styloviz/accordion';
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
standalone: true,
|
|
22
|
+
imports: [SvAccordionComponent],
|
|
23
|
+
template: `
|
|
24
|
+
<sv-accordion [items]="items" expandMode="single" />
|
|
25
|
+
`,
|
|
26
|
+
})
|
|
27
|
+
export class DemoComponent {}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Inputs
|
|
31
|
+
|
|
32
|
+
| Input | Type | Default | Description |
|
|
33
|
+
| --- | --- | --- | --- |
|
|
34
|
+
| `items` | `AccordionItem[]` | `[]` | Array of panel definitions. |
|
|
35
|
+
| `variant` | `AccordionVariant` | `'default'` | Visual style. |
|
|
36
|
+
| `expandMode` | `AccordionExpand` | `'single'` | Whether one or many panels can be open at once. |
|
|
37
|
+
| `size` | `AccordionSize` | `'md'` | Padding / font scale. |
|
|
38
|
+
| `iconPosition` | `AccordionIconPos` | `'right'` | Which side the chevron appears on. |
|
|
39
|
+
| `customIconPath` | `string` | `''` | Custom SVG `<path d="…">` used as the expand/collapse indicator instead of the default chevron. Leave empty to use the built-in chevron. When only this is set the icon rotates 180° on open (good for arrows). |
|
|
40
|
+
| `customIconOpenPath` | `string` | `''` | Optional alternate SVG `<path d="…">` shown when the panel is **open**. Set this alongside `customIconPath` for a true swap (e.g. plus → minus). When set, rotation is disabled — the paths switch instead. |
|
|
41
|
+
| `customClass` | `string` | `''` | Extra CSS classes applied to the root wrapper. |
|
|
42
|
+
|
|
43
|
+
## Outputs
|
|
44
|
+
|
|
45
|
+
| Output | Type | Description |
|
|
46
|
+
| --- | --- | --- |
|
|
47
|
+
| `itemToggled` | `AccordionToggleEvent` | Fires whenever a panel is opened or closed. |
|
|
48
|
+
|
|
49
|
+
## Documentation
|
|
50
|
+
|
|
51
|
+
Full API reference and live demos: https://styloviz.dev/docs/accordion
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, output, signal, computed, ChangeDetectionStrategy, Component } from '@angular/core';
|
|
3
|
+
import { NgClass } from '@angular/common';
|
|
4
|
+
|
|
5
|
+
// ─── Badge palette ────────────────────────────────────────────────────────────
|
|
6
|
+
const BADGE_CLASSES = {
|
|
7
|
+
primary: 'bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300',
|
|
8
|
+
success: 'bg-accent-50 text-accent-600 dark:bg-accent-500/20 dark:text-accent-400',
|
|
9
|
+
warning: 'bg-warning-50 text-warning-600 dark:bg-warning-500/20 dark:text-warning-400',
|
|
10
|
+
danger: 'bg-danger-50 text-danger-600 dark:bg-danger-500/20 dark:text-danger-400',
|
|
11
|
+
info: 'bg-sky-50 text-sky-600 dark:bg-sky-500/20 dark:text-sky-400',
|
|
12
|
+
neutral: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300',
|
|
13
|
+
};
|
|
14
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
15
|
+
/**
|
|
16
|
+
* AccordionComponent — A fully-featured, accessible expand/collapse panel set.
|
|
17
|
+
*
|
|
18
|
+
* ## Variants
|
|
19
|
+
* | variant | Description |
|
|
20
|
+
* |------------|---------------------------------------------------------------|
|
|
21
|
+
* | `default` | Outer border wrapping all panels with interior dividers |
|
|
22
|
+
* | `flush` | No outer border — only horizontal dividers between panels |
|
|
23
|
+
* | `separated`| Each panel is an independent floating card with shadow |
|
|
24
|
+
* | `filled` | Active panel header gets a tinted colour fill |
|
|
25
|
+
*
|
|
26
|
+
* ## Features
|
|
27
|
+
* - Single or multi-expand mode
|
|
28
|
+
* - Left or right chevron icon position
|
|
29
|
+
* - Optional custom SVG icon path per panel (leading icon)
|
|
30
|
+
* - Optional subtitle under the header title
|
|
31
|
+
* - Optional badge with 6 semantic colour variants
|
|
32
|
+
* - Disabled panel support
|
|
33
|
+
* - `defaultOpen` per item to pre-open panels
|
|
34
|
+
* - CSS-grid based height animation — no JavaScript height calculation
|
|
35
|
+
* - Keyboard navigation: Enter/Space to toggle, ↑↓ to move focus
|
|
36
|
+
* - ARIA: `role="button"`, `aria-expanded`, `aria-disabled`, `aria-controls`
|
|
37
|
+
* - Full dark-mode support
|
|
38
|
+
* - 3 sizes: `sm` / `md` / `lg`
|
|
39
|
+
*
|
|
40
|
+
* ## Usage
|
|
41
|
+
* ```html
|
|
42
|
+
* <sv-accordion
|
|
43
|
+
* [items]="faqItems"
|
|
44
|
+
* variant="separated"
|
|
45
|
+
* expandMode="multi"
|
|
46
|
+
* [size]="'md'"
|
|
47
|
+
* />
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
class SvAccordionComponent {
|
|
51
|
+
// ── Inputs ─────────────────────────────────────────────────────────────────
|
|
52
|
+
/** Array of panel definitions. */
|
|
53
|
+
items = input([], ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
|
|
54
|
+
/** Visual style. @default 'default' */
|
|
55
|
+
variant = input('default', ...(ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []));
|
|
56
|
+
/** Whether one or many panels can be open at once. @default 'single' */
|
|
57
|
+
expandMode = input('single', ...(ngDevMode ? [{ debugName: "expandMode" }] : /* istanbul ignore next */ []));
|
|
58
|
+
/** Padding / font scale. @default 'md' */
|
|
59
|
+
size = input('md', ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
|
|
60
|
+
/** Which side the chevron appears on. @default 'right' */
|
|
61
|
+
iconPosition = input('right', ...(ngDevMode ? [{ debugName: "iconPosition" }] : /* istanbul ignore next */ []));
|
|
62
|
+
/**
|
|
63
|
+
* Custom SVG `<path d="…">` used as the expand/collapse indicator instead
|
|
64
|
+
* of the default chevron. Leave empty to use the built-in chevron.
|
|
65
|
+
* When only this is set the icon rotates 180° on open (good for arrows).
|
|
66
|
+
*/
|
|
67
|
+
customIconPath = input('', ...(ngDevMode ? [{ debugName: "customIconPath" }] : /* istanbul ignore next */ []));
|
|
68
|
+
/**
|
|
69
|
+
* Optional alternate SVG `<path d="…">` shown when the panel is **open**.
|
|
70
|
+
* Set this alongside `customIconPath` for a true swap (e.g. plus → minus).
|
|
71
|
+
* When set, rotation is disabled — the paths switch instead.
|
|
72
|
+
*/
|
|
73
|
+
customIconOpenPath = input('', ...(ngDevMode ? [{ debugName: "customIconOpenPath" }] : /* istanbul ignore next */ []));
|
|
74
|
+
/** Extra CSS classes applied to the root wrapper. */
|
|
75
|
+
customClass = input('', ...(ngDevMode ? [{ debugName: "customClass" }] : /* istanbul ignore next */ []));
|
|
76
|
+
// ── Outputs ────────────────────────────────────────────────────────────────
|
|
77
|
+
/** Fires whenever a panel is opened or closed. */
|
|
78
|
+
itemToggled = output();
|
|
79
|
+
// ── Internal state ─────────────────────────────────────────────────────────
|
|
80
|
+
/** Set of currently-open item IDs. */
|
|
81
|
+
openIds = signal(new Set(), ...(ngDevMode ? [{ debugName: "openIds" }] : /* istanbul ignore next */ []));
|
|
82
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────────
|
|
83
|
+
ngOnInit() {
|
|
84
|
+
const initial = new Set();
|
|
85
|
+
for (const item of this.items()) {
|
|
86
|
+
if (item.defaultOpen && !item.disabled) {
|
|
87
|
+
if (this.expandMode() === 'single') {
|
|
88
|
+
initial.clear();
|
|
89
|
+
initial.add(item.id);
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
initial.add(item.id);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
this.openIds.set(initial);
|
|
96
|
+
}
|
|
97
|
+
// ── Public queries ─────────────────────────────────────────────────────────
|
|
98
|
+
isOpen(id) {
|
|
99
|
+
return this.openIds().has(id);
|
|
100
|
+
}
|
|
101
|
+
// ── Interaction ────────────────────────────────────────────────────────────
|
|
102
|
+
toggle(item) {
|
|
103
|
+
if (item.disabled)
|
|
104
|
+
return;
|
|
105
|
+
const current = new Set(this.openIds());
|
|
106
|
+
const wasOpen = current.has(item.id);
|
|
107
|
+
if (this.expandMode() === 'single') {
|
|
108
|
+
current.clear();
|
|
109
|
+
if (!wasOpen)
|
|
110
|
+
current.add(item.id);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
wasOpen ? current.delete(item.id) : current.add(item.id);
|
|
114
|
+
}
|
|
115
|
+
this.openIds.set(current);
|
|
116
|
+
this.itemToggled.emit({ id: item.id, open: !wasOpen });
|
|
117
|
+
}
|
|
118
|
+
onKeydown(event, item, _index) {
|
|
119
|
+
const items = this.items().filter(i => !i.disabled);
|
|
120
|
+
const currentIndex = items.findIndex(i => i.id === item.id);
|
|
121
|
+
switch (event.key) {
|
|
122
|
+
case 'Enter':
|
|
123
|
+
case ' ':
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
this.toggle(item);
|
|
126
|
+
break;
|
|
127
|
+
case 'ArrowDown': {
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
const next = items[currentIndex + 1];
|
|
130
|
+
if (next)
|
|
131
|
+
this.focusHeader(next.id);
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
case 'ArrowUp': {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
const prev = items[currentIndex - 1];
|
|
137
|
+
if (prev)
|
|
138
|
+
this.focusHeader(prev.id);
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case 'Home': {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
if (items.length)
|
|
144
|
+
this.focusHeader(items[0].id);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case 'End': {
|
|
148
|
+
event.preventDefault();
|
|
149
|
+
if (items.length)
|
|
150
|
+
this.focusHeader(items[items.length - 1].id);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
focusHeader(id) {
|
|
156
|
+
const el = document.getElementById(`acc-header-${id}`);
|
|
157
|
+
el?.focus();
|
|
158
|
+
}
|
|
159
|
+
// ── CSS class helpers ──────────────────────────────────────────────────────
|
|
160
|
+
rootClasses = computed(() => {
|
|
161
|
+
const v = this.variant();
|
|
162
|
+
const base = 'w-full';
|
|
163
|
+
const separated = '';
|
|
164
|
+
const bordered = v === 'default' || v === 'filled'
|
|
165
|
+
? 'rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden divide-y divide-gray-200 dark:divide-gray-700'
|
|
166
|
+
: v === 'flush'
|
|
167
|
+
? 'divide-y divide-gray-200 dark:divide-gray-700'
|
|
168
|
+
: '';
|
|
169
|
+
return [base, separated, bordered, this.customClass()].filter(Boolean).join(' ');
|
|
170
|
+
}, ...(ngDevMode ? [{ debugName: "rootClasses" }] : /* istanbul ignore next */ []));
|
|
171
|
+
itemWrapperClass(_item) {
|
|
172
|
+
return '';
|
|
173
|
+
}
|
|
174
|
+
headerClasses(item) {
|
|
175
|
+
const v = this.variant();
|
|
176
|
+
const s = this.size();
|
|
177
|
+
const open = this.isOpen(item.id);
|
|
178
|
+
const padding = s === 'sm' ? 'px-4 py-2.5' :
|
|
179
|
+
s === 'lg' ? 'px-6 py-5' :
|
|
180
|
+
'px-5 py-4';
|
|
181
|
+
const base = [
|
|
182
|
+
'w-full flex items-center gap-3 text-left select-none',
|
|
183
|
+
'transition-colors duration-150 focus-visible:outline-none',
|
|
184
|
+
'focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-inset',
|
|
185
|
+
padding,
|
|
186
|
+
].join(' ');
|
|
187
|
+
const cursor = item.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer';
|
|
188
|
+
let bg = '';
|
|
189
|
+
if (v === 'filled') {
|
|
190
|
+
bg = open
|
|
191
|
+
? 'bg-primary-50 dark:bg-primary-900/30'
|
|
192
|
+
: 'bg-gray-50 dark:bg-gray-800/50 hover:bg-gray-100 dark:hover:bg-gray-800';
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
bg = item.disabled
|
|
196
|
+
? 'bg-white dark:bg-gray-900'
|
|
197
|
+
: 'bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800/60';
|
|
198
|
+
}
|
|
199
|
+
return [base, cursor, bg].join(' ');
|
|
200
|
+
}
|
|
201
|
+
titleClasses(item) {
|
|
202
|
+
const s = this.size();
|
|
203
|
+
const open = this.isOpen(item.id);
|
|
204
|
+
const size = s === 'sm' ? 'text-sm' :
|
|
205
|
+
s === 'lg' ? 'text-base' :
|
|
206
|
+
'text-sm';
|
|
207
|
+
return [
|
|
208
|
+
size,
|
|
209
|
+
'font-semibold leading-snug tracking-tight',
|
|
210
|
+
open
|
|
211
|
+
? 'text-primary-600 dark:text-primary-400'
|
|
212
|
+
: 'text-gray-900 dark:text-gray-100',
|
|
213
|
+
].join(' ');
|
|
214
|
+
}
|
|
215
|
+
subtitleClasses() {
|
|
216
|
+
const s = this.size();
|
|
217
|
+
return [
|
|
218
|
+
s === 'sm' ? 'text-[11px]' : 'text-xs',
|
|
219
|
+
'mt-0.5 text-gray-500 dark:text-gray-400 leading-none',
|
|
220
|
+
].join(' ');
|
|
221
|
+
}
|
|
222
|
+
contentClasses() {
|
|
223
|
+
const s = this.size();
|
|
224
|
+
const padding = s === 'sm' ? 'px-4 pt-2 pb-4' :
|
|
225
|
+
s === 'lg' ? 'px-6 pt-4 pb-7' :
|
|
226
|
+
'px-5 pt-3 pb-6';
|
|
227
|
+
return [
|
|
228
|
+
padding,
|
|
229
|
+
'text-sm text-gray-600 dark:text-gray-400 leading-relaxed',
|
|
230
|
+
'bg-white dark:bg-gray-900',
|
|
231
|
+
].join(' ');
|
|
232
|
+
}
|
|
233
|
+
badgeClass(color = 'neutral') {
|
|
234
|
+
return [
|
|
235
|
+
'inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold leading-none',
|
|
236
|
+
BADGE_CLASSES[color],
|
|
237
|
+
].join(' ');
|
|
238
|
+
}
|
|
239
|
+
chevronClasses(item) {
|
|
240
|
+
return [
|
|
241
|
+
'shrink-0 w-4 h-4 transition-transform duration-200',
|
|
242
|
+
this.isOpen(item.id) ? 'rotate-180' : 'rotate-0',
|
|
243
|
+
this.isOpen(item.id)
|
|
244
|
+
? 'text-primary-500 dark:text-primary-400'
|
|
245
|
+
: 'text-gray-500 dark:text-gray-400',
|
|
246
|
+
].join(' ');
|
|
247
|
+
}
|
|
248
|
+
leadingIconClasses(item) {
|
|
249
|
+
return [
|
|
250
|
+
'shrink-0 w-4 h-4',
|
|
251
|
+
this.isOpen(item.id)
|
|
252
|
+
? 'text-primary-500 dark:text-primary-400'
|
|
253
|
+
: 'text-gray-500 dark:text-gray-400',
|
|
254
|
+
].join(' ');
|
|
255
|
+
}
|
|
256
|
+
panelClasses() {
|
|
257
|
+
return 'grid transition-[grid-template-rows] duration-250 ease-in-out';
|
|
258
|
+
}
|
|
259
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvAccordionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
260
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.13", type: SvAccordionComponent, isStandalone: true, selector: "sv-accordion", inputs: { items: { classPropertyName: "items", publicName: "items", isSignal: true, isRequired: false, transformFunction: null }, variant: { classPropertyName: "variant", publicName: "variant", isSignal: true, isRequired: false, transformFunction: null }, expandMode: { classPropertyName: "expandMode", publicName: "expandMode", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, iconPosition: { classPropertyName: "iconPosition", publicName: "iconPosition", isSignal: true, isRequired: false, transformFunction: null }, customIconPath: { classPropertyName: "customIconPath", publicName: "customIconPath", isSignal: true, isRequired: false, transformFunction: null }, customIconOpenPath: { classPropertyName: "customIconOpenPath", publicName: "customIconOpenPath", isSignal: true, isRequired: false, transformFunction: null }, customClass: { classPropertyName: "customClass", publicName: "customClass", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { itemToggled: "itemToggled" }, ngImport: i0, template: "<!-- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n \u2551 AccordionComponent \u2014 Accessible expand/collapse panel set \u2551\n \u2551 Variants: default \u00B7 flush \u00B7 separated \u00B7 filled \u2551\n \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D -->\n\n<div [ngClass]=\"rootClasses()\" role=\"list\">\n @for (item of items(); track item.id; let idx = $index) {\n\n <!-- \u2500\u2500 Panel wrapper (separated variant gets its own card) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [ngClass]=\"itemWrapperClass(item)\" role=\"listitem\">\n\n <!-- \u2500\u2500 Header / trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button\n [id]=\"'acc-header-' + item.id\"\n type=\"button\"\n role=\"button\"\n [attr.aria-expanded]=\"isOpen(item.id)\"\n [attr.aria-controls]=\"'acc-panel-' + item.id\"\n [attr.aria-disabled]=\"item.disabled || null\"\n [attr.tabindex]=\"item.disabled ? '-1' : '0'\"\n [ngClass]=\"headerClasses(item)\"\n (click)=\"toggle(item)\"\n (keydown)=\"onKeydown($event, item, idx)\"\n >\n <!-- \u2500\u2500 Left chevron (when iconPosition = 'left') \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (iconPosition() === 'left') {\n <span aria-hidden=\"true\">\n @if (customIconPath()) {\n <svg class=\"shrink-0 w-4 h-4 transition-transform duration-200\"\n [class.rotate-90]=\"!customIconOpenPath() && isOpen(item.id)\"\n [ngClass]=\"isOpen(item.id) ? 'text-primary-500 dark:text-primary-400' : 'text-gray-500 dark:text-gray-400'\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"customIconOpenPath() && isOpen(item.id) ? customIconOpenPath() : customIconPath()\" />\n </svg>\n } @else {\n <svg [ngClass]=\"chevronClasses(item)\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n aria-hidden=\"true\">\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n }\n </span>\n }\n\n <!-- \u2500\u2500 Leading item icon \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (item.iconPath) {\n <span class=\"shrink-0\" aria-hidden=\"true\">\n <svg [ngClass]=\"leadingIconClasses(item)\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"item.iconPath\" />\n </svg>\n </span>\n }\n\n <!-- \u2500\u2500 Title + subtitle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <span class=\"min-w-0 flex-1\">\n <span [ngClass]=\"titleClasses(item)\" class=\"block truncate\">\n {{ item.title }}\n </span>\n @if (item.subtitle) {\n <span [ngClass]=\"subtitleClasses()\" class=\"block\">\n {{ item.subtitle }}\n </span>\n }\n </span>\n\n <!-- \u2500\u2500 Badge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (item.badge) {\n <span [ngClass]=\"badgeClass(item.badgeColor ?? 'neutral')\">\n {{ item.badge }}\n </span>\n }\n\n <!-- \u2500\u2500 Right chevron (when iconPosition = 'right', default) \u2500\u2500\u2500\u2500 -->\n @if (iconPosition() === 'right') {\n <span class=\"ml-auto shrink-0\" aria-hidden=\"true\">\n @if (customIconPath()) {\n <svg class=\"w-4 h-4 transition-transform duration-200\"\n [class.rotate-180]=\"!customIconOpenPath() && isOpen(item.id)\"\n [ngClass]=\"isOpen(item.id) ? 'text-primary-500 dark:text-primary-400' : 'text-gray-500 dark:text-gray-400'\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"customIconOpenPath() && isOpen(item.id) ? customIconOpenPath() : customIconPath()\" />\n </svg>\n } @else {\n <svg [ngClass]=\"chevronClasses(item)\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n aria-hidden=\"true\">\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n }\n </span>\n }\n </button>\n\n <!-- \u2500\u2500 Panel body \u2014 CSS grid animation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n [id]=\"'acc-panel-' + item.id\"\n role=\"region\"\n [attr.aria-labelledby]=\"'acc-header-' + item.id\"\n class=\"grid transition-[grid-template-rows] duration-[250ms] ease-in-out\"\n [style.grid-template-rows]=\"isOpen(item.id) ? '1fr' : '0fr'\"\n >\n <!-- Inner wrapper MUST have overflow:hidden for the grid trick -->\n <div class=\"overflow-hidden\">\n <div [ngClass]=\"contentClasses()\">\n {{ item.content }}\n </div>\n </div>\n </div>\n\n </div><!-- /panel wrapper -->\n\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
261
|
+
}
|
|
262
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.13", ngImport: i0, type: SvAccordionComponent, decorators: [{
|
|
263
|
+
type: Component,
|
|
264
|
+
args: [{ selector: 'sv-accordion', standalone: true, imports: [NgClass], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557\n \u2551 AccordionComponent \u2014 Accessible expand/collapse panel set \u2551\n \u2551 Variants: default \u00B7 flush \u00B7 separated \u00B7 filled \u2551\n \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D -->\n\n<div [ngClass]=\"rootClasses()\" role=\"list\">\n @for (item of items(); track item.id; let idx = $index) {\n\n <!-- \u2500\u2500 Panel wrapper (separated variant gets its own card) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div [ngClass]=\"itemWrapperClass(item)\" role=\"listitem\">\n\n <!-- \u2500\u2500 Header / trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <button\n [id]=\"'acc-header-' + item.id\"\n type=\"button\"\n role=\"button\"\n [attr.aria-expanded]=\"isOpen(item.id)\"\n [attr.aria-controls]=\"'acc-panel-' + item.id\"\n [attr.aria-disabled]=\"item.disabled || null\"\n [attr.tabindex]=\"item.disabled ? '-1' : '0'\"\n [ngClass]=\"headerClasses(item)\"\n (click)=\"toggle(item)\"\n (keydown)=\"onKeydown($event, item, idx)\"\n >\n <!-- \u2500\u2500 Left chevron (when iconPosition = 'left') \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (iconPosition() === 'left') {\n <span aria-hidden=\"true\">\n @if (customIconPath()) {\n <svg class=\"shrink-0 w-4 h-4 transition-transform duration-200\"\n [class.rotate-90]=\"!customIconOpenPath() && isOpen(item.id)\"\n [ngClass]=\"isOpen(item.id) ? 'text-primary-500 dark:text-primary-400' : 'text-gray-500 dark:text-gray-400'\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"customIconOpenPath() && isOpen(item.id) ? customIconOpenPath() : customIconPath()\" />\n </svg>\n } @else {\n <svg [ngClass]=\"chevronClasses(item)\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n aria-hidden=\"true\">\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n }\n </span>\n }\n\n <!-- \u2500\u2500 Leading item icon \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (item.iconPath) {\n <span class=\"shrink-0\" aria-hidden=\"true\">\n <svg [ngClass]=\"leadingIconClasses(item)\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"item.iconPath\" />\n </svg>\n </span>\n }\n\n <!-- \u2500\u2500 Title + subtitle \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <span class=\"min-w-0 flex-1\">\n <span [ngClass]=\"titleClasses(item)\" class=\"block truncate\">\n {{ item.title }}\n </span>\n @if (item.subtitle) {\n <span [ngClass]=\"subtitleClasses()\" class=\"block\">\n {{ item.subtitle }}\n </span>\n }\n </span>\n\n <!-- \u2500\u2500 Badge \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n @if (item.badge) {\n <span [ngClass]=\"badgeClass(item.badgeColor ?? 'neutral')\">\n {{ item.badge }}\n </span>\n }\n\n <!-- \u2500\u2500 Right chevron (when iconPosition = 'right', default) \u2500\u2500\u2500\u2500 -->\n @if (iconPosition() === 'right') {\n <span class=\"ml-auto shrink-0\" aria-hidden=\"true\">\n @if (customIconPath()) {\n <svg class=\"w-4 h-4 transition-transform duration-200\"\n [class.rotate-180]=\"!customIconOpenPath() && isOpen(item.id)\"\n [ngClass]=\"isOpen(item.id) ? 'text-primary-500 dark:text-primary-400' : 'text-gray-500 dark:text-gray-400'\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"customIconOpenPath() && isOpen(item.id) ? customIconOpenPath() : customIconPath()\" />\n </svg>\n } @else {\n <svg [ngClass]=\"chevronClasses(item)\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n aria-hidden=\"true\">\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n }\n </span>\n }\n </button>\n\n <!-- \u2500\u2500 Panel body \u2014 CSS grid animation \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 -->\n <div\n [id]=\"'acc-panel-' + item.id\"\n role=\"region\"\n [attr.aria-labelledby]=\"'acc-header-' + item.id\"\n class=\"grid transition-[grid-template-rows] duration-[250ms] ease-in-out\"\n [style.grid-template-rows]=\"isOpen(item.id) ? '1fr' : '0fr'\"\n >\n <!-- Inner wrapper MUST have overflow:hidden for the grid trick -->\n <div class=\"overflow-hidden\">\n <div [ngClass]=\"contentClasses()\">\n {{ item.content }}\n </div>\n </div>\n </div>\n\n </div><!-- /panel wrapper -->\n\n }\n</div>\n" }]
|
|
265
|
+
}], propDecorators: { items: [{ type: i0.Input, args: [{ isSignal: true, alias: "items", required: false }] }], variant: [{ type: i0.Input, args: [{ isSignal: true, alias: "variant", required: false }] }], expandMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandMode", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], iconPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "iconPosition", required: false }] }], customIconPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "customIconPath", required: false }] }], customIconOpenPath: [{ type: i0.Input, args: [{ isSignal: true, alias: "customIconOpenPath", required: false }] }], customClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "customClass", required: false }] }], itemToggled: [{ type: i0.Output, args: ["itemToggled"] }] } });
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Generated bundle index. Do not edit.
|
|
269
|
+
*/
|
|
270
|
+
|
|
271
|
+
export { SvAccordionComponent };
|
|
272
|
+
//# sourceMappingURL=styloviz-accordion.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styloviz-accordion.mjs","sources":["../../../../projects/accordion-free/src/lib/accordion.component.ts","../../../../projects/accordion-free/src/lib/accordion.component.html","../../../../projects/accordion-free/src/styloviz-accordion.ts"],"sourcesContent":["import {\n ChangeDetectionStrategy,\n Component,\n computed,\n input,\n OnInit,\n output,\n signal,\n} from '@angular/core';\nimport { NgClass } from '@angular/common';\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\n/** Visual presentation style. */\nexport type AccordionVariant = 'default' | 'flush' | 'filled';\n\n/** Header + content padding scale. */\nexport type AccordionSize = 'sm' | 'md' | 'lg';\n\n/** Whether one or many panels may be open simultaneously. */\nexport type AccordionExpand = 'single' | 'multi';\n\n/** Which side the expand/collapse chevron appears on. */\nexport type AccordionIconPos = 'left' | 'right';\n\n/** Badge color variant for panel headers. */\nexport type AccordionBadgeColor =\n | 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral';\n\n// ─── Interfaces ───────────────────────────────────────────────────────────────\n\n/** Describes a single accordion panel. */\nexport interface AccordionItem {\n /** Unique identifier — used as the open-state key. */\n id: string;\n /** Main header text. */\n title: string;\n /** Body text (plain text). Projected content can be added via template. */\n content: string;\n /** Secondary line beneath the title. */\n subtitle?: string;\n /** SVG `<path d=\"…\">` rendered as a leading icon in the header. */\n iconPath?: string;\n /** Small badge label on the right side of the header (before the chevron). */\n badge?: string;\n /** Semantic color of the badge. @default 'neutral' */\n badgeColor?: AccordionBadgeColor;\n /** Prevents the panel from being toggled. @default false */\n disabled?: boolean;\n /** Opens this panel when the component initialises. @default false */\n defaultOpen?: boolean;\n}\n\n/** Emitted every time a panel is toggled. */\nexport interface AccordionToggleEvent {\n id: string;\n open: boolean;\n}\n\n// ─── Badge palette ────────────────────────────────────────────────────────────\n\nconst BADGE_CLASSES: Record<AccordionBadgeColor, string> = {\n primary: 'bg-primary-100 text-primary-700 dark:bg-primary-900/40 dark:text-primary-300',\n success: 'bg-accent-50 text-accent-600 dark:bg-accent-500/20 dark:text-accent-400',\n warning: 'bg-warning-50 text-warning-600 dark:bg-warning-500/20 dark:text-warning-400',\n danger: 'bg-danger-50 text-danger-600 dark:bg-danger-500/20 dark:text-danger-400',\n info: 'bg-sky-50 text-sky-600 dark:bg-sky-500/20 dark:text-sky-400',\n neutral: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300',\n};\n\n// ─── Component ────────────────────────────────────────────────────────────────\n\n/**\n * AccordionComponent — A fully-featured, accessible expand/collapse panel set.\n *\n * ## Variants\n * | variant | Description |\n * |------------|---------------------------------------------------------------|\n * | `default` | Outer border wrapping all panels with interior dividers |\n * | `flush` | No outer border — only horizontal dividers between panels |\n * | `separated`| Each panel is an independent floating card with shadow |\n * | `filled` | Active panel header gets a tinted colour fill |\n *\n * ## Features\n * - Single or multi-expand mode\n * - Left or right chevron icon position\n * - Optional custom SVG icon path per panel (leading icon)\n * - Optional subtitle under the header title\n * - Optional badge with 6 semantic colour variants\n * - Disabled panel support\n * - `defaultOpen` per item to pre-open panels\n * - CSS-grid based height animation — no JavaScript height calculation\n * - Keyboard navigation: Enter/Space to toggle, ↑↓ to move focus\n * - ARIA: `role=\"button\"`, `aria-expanded`, `aria-disabled`, `aria-controls`\n * - Full dark-mode support\n * - 3 sizes: `sm` / `md` / `lg`\n *\n * ## Usage\n * ```html\n * <sv-accordion\n * [items]=\"faqItems\"\n * variant=\"separated\"\n * expandMode=\"multi\"\n * [size]=\"'md'\"\n * />\n * ```\n */\n@Component({\n selector: 'sv-accordion',\n standalone: true,\n imports: [NgClass],\n templateUrl: './accordion.component.html',\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport class SvAccordionComponent implements OnInit {\n\n // ── Inputs ─────────────────────────────────────────────────────────────────\n\n /** Array of panel definitions. */\n items = input<AccordionItem[]>([]);\n\n /** Visual style. @default 'default' */\n variant = input<AccordionVariant>('default');\n\n /** Whether one or many panels can be open at once. @default 'single' */\n expandMode = input<AccordionExpand>('single');\n\n /** Padding / font scale. @default 'md' */\n size = input<AccordionSize>('md');\n\n /** Which side the chevron appears on. @default 'right' */\n iconPosition = input<AccordionIconPos>('right');\n\n /**\n * Custom SVG `<path d=\"…\">` used as the expand/collapse indicator instead\n * of the default chevron. Leave empty to use the built-in chevron.\n * When only this is set the icon rotates 180° on open (good for arrows).\n */\n customIconPath = input<string>('');\n\n /**\n * Optional alternate SVG `<path d=\"…\">` shown when the panel is **open**.\n * Set this alongside `customIconPath` for a true swap (e.g. plus → minus).\n * When set, rotation is disabled — the paths switch instead.\n */\n customIconOpenPath = input<string>('');\n\n /** Extra CSS classes applied to the root wrapper. */\n customClass = input<string>('');\n\n // ── Outputs ────────────────────────────────────────────────────────────────\n\n /** Fires whenever a panel is opened or closed. */\n itemToggled = output<AccordionToggleEvent>();\n\n // ── Internal state ─────────────────────────────────────────────────────────\n\n /** Set of currently-open item IDs. */\n private readonly openIds = signal<Set<string>>(new Set());\n\n // ── Lifecycle ──────────────────────────────────────────────────────────────\n\n ngOnInit(): void {\n const initial = new Set<string>();\n for (const item of this.items()) {\n if (item.defaultOpen && !item.disabled) {\n if (this.expandMode() === 'single') {\n initial.clear();\n initial.add(item.id);\n break;\n }\n initial.add(item.id);\n }\n }\n this.openIds.set(initial);\n }\n\n // ── Public queries ─────────────────────────────────────────────────────────\n\n isOpen(id: string): boolean {\n return this.openIds().has(id);\n }\n\n // ── Interaction ────────────────────────────────────────────────────────────\n\n toggle(item: AccordionItem): void {\n if (item.disabled) return;\n\n const current = new Set(this.openIds());\n const wasOpen = current.has(item.id);\n\n if (this.expandMode() === 'single') {\n current.clear();\n if (!wasOpen) current.add(item.id);\n } else {\n wasOpen ? current.delete(item.id) : current.add(item.id);\n }\n\n this.openIds.set(current);\n this.itemToggled.emit({ id: item.id, open: !wasOpen });\n }\n\n onKeydown(event: KeyboardEvent, item: AccordionItem, _index: number): void {\n const items = this.items().filter(i => !i.disabled);\n const currentIndex = items.findIndex(i => i.id === item.id);\n\n switch (event.key) {\n case 'Enter':\n case ' ':\n event.preventDefault();\n this.toggle(item);\n break;\n case 'ArrowDown': {\n event.preventDefault();\n const next = items[currentIndex + 1];\n if (next) this.focusHeader(next.id);\n break;\n }\n case 'ArrowUp': {\n event.preventDefault();\n const prev = items[currentIndex - 1];\n if (prev) this.focusHeader(prev.id);\n break;\n }\n case 'Home': {\n event.preventDefault();\n if (items.length) this.focusHeader(items[0].id);\n break;\n }\n case 'End': {\n event.preventDefault();\n if (items.length) this.focusHeader(items[items.length - 1].id);\n break;\n }\n }\n }\n\n private focusHeader(id: string): void {\n const el = document.getElementById(`acc-header-${id}`);\n el?.focus();\n }\n\n // ── CSS class helpers ──────────────────────────────────────────────────────\n\n readonly rootClasses = computed(() => {\n const v = this.variant();\n const base = 'w-full';\n const separated = '';\n const bordered =\n v === 'default' || v === 'filled'\n ? 'rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden divide-y divide-gray-200 dark:divide-gray-700'\n : v === 'flush'\n ? 'divide-y divide-gray-200 dark:divide-gray-700'\n : '';\n return [base, separated, bordered, this.customClass()].filter(Boolean).join(' ');\n });\n\n itemWrapperClass(_item: AccordionItem): string {\n return '';\n }\n\n headerClasses(item: AccordionItem): string {\n const v = this.variant();\n const s = this.size();\n const open = this.isOpen(item.id);\n\n const padding =\n s === 'sm' ? 'px-4 py-2.5' :\n s === 'lg' ? 'px-6 py-5' :\n 'px-5 py-4';\n\n const base = [\n 'w-full flex items-center gap-3 text-left select-none',\n 'transition-colors duration-150 focus-visible:outline-none',\n 'focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-inset',\n padding,\n ].join(' ');\n\n const cursor = item.disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer';\n\n let bg = '';\n if (v === 'filled') {\n bg = open\n ? 'bg-primary-50 dark:bg-primary-900/30'\n : 'bg-gray-50 dark:bg-gray-800/50 hover:bg-gray-100 dark:hover:bg-gray-800';\n } else {\n bg = item.disabled\n ? 'bg-white dark:bg-gray-900'\n : 'bg-white dark:bg-gray-900 hover:bg-gray-50 dark:hover:bg-gray-800/60';\n }\n\n return [base, cursor, bg].join(' ');\n }\n\n titleClasses(item: AccordionItem): string {\n const s = this.size();\n const open = this.isOpen(item.id);\n const size =\n s === 'sm' ? 'text-sm' :\n s === 'lg' ? 'text-base' :\n 'text-sm';\n\n return [\n size,\n 'font-semibold leading-snug tracking-tight',\n open\n ? 'text-primary-600 dark:text-primary-400'\n : 'text-gray-900 dark:text-gray-100',\n ].join(' ');\n }\n\n subtitleClasses(): string {\n const s = this.size();\n return [\n s === 'sm' ? 'text-[11px]' : 'text-xs',\n 'mt-0.5 text-gray-500 dark:text-gray-400 leading-none',\n ].join(' ');\n }\n\n contentClasses(): string {\n const s = this.size();\n const padding =\n s === 'sm' ? 'px-4 pt-2 pb-4' :\n s === 'lg' ? 'px-6 pt-4 pb-7' :\n 'px-5 pt-3 pb-6';\n return [\n padding,\n 'text-sm text-gray-600 dark:text-gray-400 leading-relaxed',\n 'bg-white dark:bg-gray-900',\n ].join(' ');\n }\n\n badgeClass(color: AccordionBadgeColor = 'neutral'): string {\n return [\n 'inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold leading-none',\n BADGE_CLASSES[color],\n ].join(' ');\n }\n\n chevronClasses(item: AccordionItem): string {\n return [\n 'shrink-0 w-4 h-4 transition-transform duration-200',\n this.isOpen(item.id) ? 'rotate-180' : 'rotate-0',\n this.isOpen(item.id)\n ? 'text-primary-500 dark:text-primary-400'\n : 'text-gray-500 dark:text-gray-400',\n ].join(' ');\n }\n\n leadingIconClasses(item: AccordionItem): string {\n return [\n 'shrink-0 w-4 h-4',\n this.isOpen(item.id)\n ? 'text-primary-500 dark:text-primary-400'\n : 'text-gray-500 dark:text-gray-400',\n ].join(' ');\n }\n\n panelClasses(): string {\n return 'grid transition-[grid-template-rows] duration-250 ease-in-out';\n }\n}\n","<!-- ╔═══════════════════════════════════════════════════════════════════════╗\n ║ AccordionComponent — Accessible expand/collapse panel set ║\n ║ Variants: default · flush · separated · filled ║\n ╚═══════════════════════════════════════════════════════════════════════╝ -->\n\n<div [ngClass]=\"rootClasses()\" role=\"list\">\n @for (item of items(); track item.id; let idx = $index) {\n\n <!-- ── Panel wrapper (separated variant gets its own card) ─────────── -->\n <div [ngClass]=\"itemWrapperClass(item)\" role=\"listitem\">\n\n <!-- ── Header / trigger ──────────────────────────────────────────── -->\n <button\n [id]=\"'acc-header-' + item.id\"\n type=\"button\"\n role=\"button\"\n [attr.aria-expanded]=\"isOpen(item.id)\"\n [attr.aria-controls]=\"'acc-panel-' + item.id\"\n [attr.aria-disabled]=\"item.disabled || null\"\n [attr.tabindex]=\"item.disabled ? '-1' : '0'\"\n [ngClass]=\"headerClasses(item)\"\n (click)=\"toggle(item)\"\n (keydown)=\"onKeydown($event, item, idx)\"\n >\n <!-- ── Left chevron (when iconPosition = 'left') ─────────────── -->\n @if (iconPosition() === 'left') {\n <span aria-hidden=\"true\">\n @if (customIconPath()) {\n <svg class=\"shrink-0 w-4 h-4 transition-transform duration-200\"\n [class.rotate-90]=\"!customIconOpenPath() && isOpen(item.id)\"\n [ngClass]=\"isOpen(item.id) ? 'text-primary-500 dark:text-primary-400' : 'text-gray-500 dark:text-gray-400'\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"customIconOpenPath() && isOpen(item.id) ? customIconOpenPath() : customIconPath()\" />\n </svg>\n } @else {\n <svg [ngClass]=\"chevronClasses(item)\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n aria-hidden=\"true\">\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n }\n </span>\n }\n\n <!-- ── Leading item icon ─────────────────────────────────────── -->\n @if (item.iconPath) {\n <span class=\"shrink-0\" aria-hidden=\"true\">\n <svg [ngClass]=\"leadingIconClasses(item)\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"item.iconPath\" />\n </svg>\n </span>\n }\n\n <!-- ── Title + subtitle ───────────────────────────────────────── -->\n <span class=\"min-w-0 flex-1\">\n <span [ngClass]=\"titleClasses(item)\" class=\"block truncate\">\n {{ item.title }}\n </span>\n @if (item.subtitle) {\n <span [ngClass]=\"subtitleClasses()\" class=\"block\">\n {{ item.subtitle }}\n </span>\n }\n </span>\n\n <!-- ── Badge ─────────────────────────────────────────────────── -->\n @if (item.badge) {\n <span [ngClass]=\"badgeClass(item.badgeColor ?? 'neutral')\">\n {{ item.badge }}\n </span>\n }\n\n <!-- ── Right chevron (when iconPosition = 'right', default) ──── -->\n @if (iconPosition() === 'right') {\n <span class=\"ml-auto shrink-0\" aria-hidden=\"true\">\n @if (customIconPath()) {\n <svg class=\"w-4 h-4 transition-transform duration-200\"\n [class.rotate-180]=\"!customIconOpenPath() && isOpen(item.id)\"\n [ngClass]=\"isOpen(item.id) ? 'text-primary-500 dark:text-primary-400' : 'text-gray-500 dark:text-gray-400'\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\">\n <path [attr.d]=\"customIconOpenPath() && isOpen(item.id) ? customIconOpenPath() : customIconPath()\" />\n </svg>\n } @else {\n <svg [ngClass]=\"chevronClasses(item)\"\n viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\"\n stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\n aria-hidden=\"true\">\n <path d=\"M6 9l6 6 6-6\" />\n </svg>\n }\n </span>\n }\n </button>\n\n <!-- ── Panel body — CSS grid animation ───────────────────────────── -->\n <div\n [id]=\"'acc-panel-' + item.id\"\n role=\"region\"\n [attr.aria-labelledby]=\"'acc-header-' + item.id\"\n class=\"grid transition-[grid-template-rows] duration-[250ms] ease-in-out\"\n [style.grid-template-rows]=\"isOpen(item.id) ? '1fr' : '0fr'\"\n >\n <!-- Inner wrapper MUST have overflow:hidden for the grid trick -->\n <div class=\"overflow-hidden\">\n <div [ngClass]=\"contentClasses()\">\n {{ item.content }}\n </div>\n </div>\n </div>\n\n </div><!-- /panel wrapper -->\n\n }\n</div>\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@styloviz/accordion",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "10 varieties: default, flush, separated, filled, multi-expand, icon placements, badges & subtitles and disabled items. Sizes (sm / md / lg), CSS-grid animation and keyboard navigation are free; multi-expand is available in the Pro package.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "sazzad-bs23",
|
|
7
|
+
"homepage": "https://styloviz.dev",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/rhossain/angular-tailwind-dashboard-components.git",
|
|
11
|
+
"directory": "projects/accordion-free"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/rhossain/angular-tailwind-dashboard-components/issues"
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@angular/common": ">=21.0.0",
|
|
18
|
+
"@angular/core": ">=21.0.0"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
|
25
|
+
},
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"module": "fesm2022/styloviz-accordion.mjs",
|
|
28
|
+
"typings": "types/styloviz-accordion.d.ts",
|
|
29
|
+
"exports": {
|
|
30
|
+
"./package.json": {
|
|
31
|
+
"default": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
".": {
|
|
34
|
+
"types": "./types/styloviz-accordion.d.ts",
|
|
35
|
+
"default": "./fesm2022/styloviz-accordion.mjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"type": "module",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"tslib": "^2.3.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as _angular_core from '@angular/core';
|
|
2
|
+
import { OnInit } from '@angular/core';
|
|
3
|
+
|
|
4
|
+
/** Visual presentation style. */
|
|
5
|
+
type AccordionVariant = 'default' | 'flush' | 'filled';
|
|
6
|
+
/** Header + content padding scale. */
|
|
7
|
+
type AccordionSize = 'sm' | 'md' | 'lg';
|
|
8
|
+
/** Whether one or many panels may be open simultaneously. */
|
|
9
|
+
type AccordionExpand = 'single' | 'multi';
|
|
10
|
+
/** Which side the expand/collapse chevron appears on. */
|
|
11
|
+
type AccordionIconPos = 'left' | 'right';
|
|
12
|
+
/** Badge color variant for panel headers. */
|
|
13
|
+
type AccordionBadgeColor = 'primary' | 'success' | 'warning' | 'danger' | 'info' | 'neutral';
|
|
14
|
+
/** Describes a single accordion panel. */
|
|
15
|
+
interface AccordionItem {
|
|
16
|
+
/** Unique identifier — used as the open-state key. */
|
|
17
|
+
id: string;
|
|
18
|
+
/** Main header text. */
|
|
19
|
+
title: string;
|
|
20
|
+
/** Body text (plain text). Projected content can be added via template. */
|
|
21
|
+
content: string;
|
|
22
|
+
/** Secondary line beneath the title. */
|
|
23
|
+
subtitle?: string;
|
|
24
|
+
/** SVG `<path d="…">` rendered as a leading icon in the header. */
|
|
25
|
+
iconPath?: string;
|
|
26
|
+
/** Small badge label on the right side of the header (before the chevron). */
|
|
27
|
+
badge?: string;
|
|
28
|
+
/** Semantic color of the badge. @default 'neutral' */
|
|
29
|
+
badgeColor?: AccordionBadgeColor;
|
|
30
|
+
/** Prevents the panel from being toggled. @default false */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
/** Opens this panel when the component initialises. @default false */
|
|
33
|
+
defaultOpen?: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Emitted every time a panel is toggled. */
|
|
36
|
+
interface AccordionToggleEvent {
|
|
37
|
+
id: string;
|
|
38
|
+
open: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* AccordionComponent — A fully-featured, accessible expand/collapse panel set.
|
|
42
|
+
*
|
|
43
|
+
* ## Variants
|
|
44
|
+
* | variant | Description |
|
|
45
|
+
* |------------|---------------------------------------------------------------|
|
|
46
|
+
* | `default` | Outer border wrapping all panels with interior dividers |
|
|
47
|
+
* | `flush` | No outer border — only horizontal dividers between panels |
|
|
48
|
+
* | `separated`| Each panel is an independent floating card with shadow |
|
|
49
|
+
* | `filled` | Active panel header gets a tinted colour fill |
|
|
50
|
+
*
|
|
51
|
+
* ## Features
|
|
52
|
+
* - Single or multi-expand mode
|
|
53
|
+
* - Left or right chevron icon position
|
|
54
|
+
* - Optional custom SVG icon path per panel (leading icon)
|
|
55
|
+
* - Optional subtitle under the header title
|
|
56
|
+
* - Optional badge with 6 semantic colour variants
|
|
57
|
+
* - Disabled panel support
|
|
58
|
+
* - `defaultOpen` per item to pre-open panels
|
|
59
|
+
* - CSS-grid based height animation — no JavaScript height calculation
|
|
60
|
+
* - Keyboard navigation: Enter/Space to toggle, ↑↓ to move focus
|
|
61
|
+
* - ARIA: `role="button"`, `aria-expanded`, `aria-disabled`, `aria-controls`
|
|
62
|
+
* - Full dark-mode support
|
|
63
|
+
* - 3 sizes: `sm` / `md` / `lg`
|
|
64
|
+
*
|
|
65
|
+
* ## Usage
|
|
66
|
+
* ```html
|
|
67
|
+
* <sv-accordion
|
|
68
|
+
* [items]="faqItems"
|
|
69
|
+
* variant="separated"
|
|
70
|
+
* expandMode="multi"
|
|
71
|
+
* [size]="'md'"
|
|
72
|
+
* />
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
declare class SvAccordionComponent implements OnInit {
|
|
76
|
+
/** Array of panel definitions. */
|
|
77
|
+
items: _angular_core.InputSignal<AccordionItem[]>;
|
|
78
|
+
/** Visual style. @default 'default' */
|
|
79
|
+
variant: _angular_core.InputSignal<AccordionVariant>;
|
|
80
|
+
/** Whether one or many panels can be open at once. @default 'single' */
|
|
81
|
+
expandMode: _angular_core.InputSignal<AccordionExpand>;
|
|
82
|
+
/** Padding / font scale. @default 'md' */
|
|
83
|
+
size: _angular_core.InputSignal<AccordionSize>;
|
|
84
|
+
/** Which side the chevron appears on. @default 'right' */
|
|
85
|
+
iconPosition: _angular_core.InputSignal<AccordionIconPos>;
|
|
86
|
+
/**
|
|
87
|
+
* Custom SVG `<path d="…">` used as the expand/collapse indicator instead
|
|
88
|
+
* of the default chevron. Leave empty to use the built-in chevron.
|
|
89
|
+
* When only this is set the icon rotates 180° on open (good for arrows).
|
|
90
|
+
*/
|
|
91
|
+
customIconPath: _angular_core.InputSignal<string>;
|
|
92
|
+
/**
|
|
93
|
+
* Optional alternate SVG `<path d="…">` shown when the panel is **open**.
|
|
94
|
+
* Set this alongside `customIconPath` for a true swap (e.g. plus → minus).
|
|
95
|
+
* When set, rotation is disabled — the paths switch instead.
|
|
96
|
+
*/
|
|
97
|
+
customIconOpenPath: _angular_core.InputSignal<string>;
|
|
98
|
+
/** Extra CSS classes applied to the root wrapper. */
|
|
99
|
+
customClass: _angular_core.InputSignal<string>;
|
|
100
|
+
/** Fires whenever a panel is opened or closed. */
|
|
101
|
+
itemToggled: _angular_core.OutputEmitterRef<AccordionToggleEvent>;
|
|
102
|
+
/** Set of currently-open item IDs. */
|
|
103
|
+
private readonly openIds;
|
|
104
|
+
ngOnInit(): void;
|
|
105
|
+
isOpen(id: string): boolean;
|
|
106
|
+
toggle(item: AccordionItem): void;
|
|
107
|
+
onKeydown(event: KeyboardEvent, item: AccordionItem, _index: number): void;
|
|
108
|
+
private focusHeader;
|
|
109
|
+
readonly rootClasses: _angular_core.Signal<string>;
|
|
110
|
+
itemWrapperClass(_item: AccordionItem): string;
|
|
111
|
+
headerClasses(item: AccordionItem): string;
|
|
112
|
+
titleClasses(item: AccordionItem): string;
|
|
113
|
+
subtitleClasses(): string;
|
|
114
|
+
contentClasses(): string;
|
|
115
|
+
badgeClass(color?: AccordionBadgeColor): string;
|
|
116
|
+
chevronClasses(item: AccordionItem): string;
|
|
117
|
+
leadingIconClasses(item: AccordionItem): string;
|
|
118
|
+
panelClasses(): string;
|
|
119
|
+
static ɵfac: _angular_core.ɵɵFactoryDeclaration<SvAccordionComponent, never>;
|
|
120
|
+
static ɵcmp: _angular_core.ɵɵComponentDeclaration<SvAccordionComponent, "sv-accordion", never, { "items": { "alias": "items"; "required": false; "isSignal": true; }; "variant": { "alias": "variant"; "required": false; "isSignal": true; }; "expandMode": { "alias": "expandMode"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "iconPosition": { "alias": "iconPosition"; "required": false; "isSignal": true; }; "customIconPath": { "alias": "customIconPath"; "required": false; "isSignal": true; }; "customIconOpenPath": { "alias": "customIconOpenPath"; "required": false; "isSignal": true; }; "customClass": { "alias": "customClass"; "required": false; "isSignal": true; }; }, { "itemToggled": "itemToggled"; }, never, never, true, never>;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export { SvAccordionComponent };
|
|
124
|
+
export type { AccordionBadgeColor, AccordionExpand, AccordionIconPos, AccordionItem, AccordionSize, AccordionToggleEvent, AccordionVariant };
|