@ansiversa/components 0.0.18 → 0.0.19

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/index.ts CHANGED
@@ -10,7 +10,18 @@ export { default as AvFeatureItem } from './src/AvFeatureItem.astro';
10
10
  export { default as AvFeatureList } from './src/AvFeatureList.astro';
11
11
  export { default as AvFooter } from './src/AvFooter.astro';
12
12
  export { default as AvInput } from './src/AvInput.astro';
13
+ export { default as AvSelect } from './src/AvSelect.astro';
14
+ export { default as AvTextarea } from './src/AvTextarea.astro';
13
15
  export { default as AvNavbar } from './src/AvNavbar.astro';
14
16
  export { default as AvNavbarActions } from './src/AvNavbarActions.astro';
15
17
  export { default as AvTimeline } from './src/AvTimeline.astro';
16
-
18
+ export { default as AvBreadcrumb } from './src/AvBreadcrumb.astro';
19
+ export { default as AvActionSwitcher } from './src/AvActionSwitcher.astro';
20
+ export { default as AvPageHeader } from './src/AvPageHeader.astro';
21
+ export { default as AvToolbar } from './src/AvToolbar.astro';
22
+ export { default as AvEmptyState } from './src/AvEmptyState.astro';
23
+ export { default as AvItemList } from './src/AvItemList.astro';
24
+ export { default as AvChipList } from './src/AvChipList.astro';
25
+ export { default as AvTemplateCard } from './src/AvTemplateCard.astro';
26
+ export { default as AvTemplateGrid } from './src/AvTemplateGrid.astro';
27
+ export { default as AvDrawer } from './src/AvDrawer.astro';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ansiversa/components",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "description": "Shared UI components and layouts for the Ansiversa ecosystem",
5
5
  "type": "module",
6
6
  "exports": {
@@ -0,0 +1,177 @@
1
+ ---
2
+ import AvButton from "./AvButton.astro";
3
+ import AvCard from "./AvCard.astro";
4
+
5
+ const actions = [
6
+ {
7
+ label: "Home",
8
+ description: "Learn what the builder offers and start a resume quickly.",
9
+ href: "/",
10
+ },
11
+ {
12
+ label: "Resume Templates",
13
+ description: "Pick a layout before you start editing.",
14
+ href: "/templates",
15
+ },
16
+ {
17
+ label: "Create Resume",
18
+ description: "Open the guided editor with your chosen template.",
19
+ href: "/app/resume/new",
20
+ },
21
+ {
22
+ label: "My Resumes",
23
+ description: "Review and edit saved resumes in one place.",
24
+ href: "/app/resumes",
25
+ },
26
+ {
27
+ label: "Edit Sample Resume",
28
+ description: "Jump into the sample editor to see the workflow.",
29
+ href: "/app/resume/r1",
30
+ },
31
+ {
32
+ label: "Download Sample PDF",
33
+ description: "Preview the PDF output for a resume.",
34
+ href: "/app/resume/r1/pdf",
35
+ },
36
+ ];
37
+ ---
38
+
39
+ <div
40
+ class="av-action-switcher"
41
+ x-data="{ open: false }"
42
+ x-init="window.addEventListener('keydown', (event) => { if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') { event.preventDefault(); open = !open; } })"
43
+ @keydown.escape.window="open = false"
44
+ @click.outside="open = false"
45
+ >
46
+ <button
47
+ class="av-action-switcher__toggle"
48
+ type="button"
49
+ aria-label="Open quick actions"
50
+ :aria-expanded="open"
51
+ aria-haspopup="true"
52
+ @keydown.enter.prevent="open = !open"
53
+ @click="open = !open"
54
+ >
55
+ <span x-show="!open">Open actions</span>
56
+ <span x-show="open">Close actions</span>
57
+ </button>
58
+
59
+ <div
60
+ class="av-action-switcher__panel"
61
+ x-show="open"
62
+ x-cloak
63
+ x-transition.origin.bottom.right
64
+ role="menu"
65
+ aria-label="Quick navigation"
66
+ >
67
+ <AvCard className="av-action-switcher__card">
68
+ <div class="av-action-switcher__header">
69
+ <p class="av-action-switcher__title">Quick actions</p>
70
+ <p class="av-action-switcher__subtitle">Move between pages without losing context.</p>
71
+ </div>
72
+ <ul class="av-action-switcher__list">
73
+ {actions.map((action) => (
74
+ <li role="none">
75
+ <div class="av-action-switcher__item">
76
+ <div class="av-action-switcher__item-text">
77
+ <p class="av-action-switcher__item-label">{action.label}</p>
78
+ <p class="av-action-switcher__item-description">{action.description}</p>
79
+ </div>
80
+ <AvButton href={action.href} size="sm" aria-label={`Go to ${action.label}`} role="menuitem">
81
+ Go
82
+ </AvButton>
83
+ </div>
84
+ </li>
85
+ ))}
86
+ </ul>
87
+ </AvCard>
88
+ </div>
89
+ </div>
90
+
91
+ <style>
92
+ .av-action-switcher {
93
+ position: fixed;
94
+ right: 1.5rem;
95
+ bottom: 1.5rem;
96
+ display: flex;
97
+ flex-direction: column;
98
+ gap: 0.75rem;
99
+ z-index: 40;
100
+ }
101
+
102
+ .av-action-switcher__toggle {
103
+ background: linear-gradient(135deg, #2563eb, #0ea5e9);
104
+ color: #fff;
105
+ border: none;
106
+ border-radius: 9999px;
107
+ padding: 0.75rem 1.25rem;
108
+ font-weight: 600;
109
+ box-shadow: 0 10px 25px rgba(37, 99, 235, 0.25);
110
+ cursor: pointer;
111
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
112
+ }
113
+
114
+ .av-action-switcher__toggle:hover {
115
+ transform: translateY(-1px);
116
+ box-shadow: 0 14px 30px rgba(37, 99, 235, 0.35);
117
+ }
118
+
119
+ .av-action-switcher__panel {
120
+ width: min(420px, calc(100vw - 2rem));
121
+ align-self: flex-end;
122
+ }
123
+
124
+ .av-action-switcher__card {
125
+ border: 1px solid rgb(226, 232, 240);
126
+ box-shadow: 0 10px 30px rgba(15, 23, 42, 0.14);
127
+ }
128
+
129
+ .av-action-switcher__header {
130
+ margin-bottom: 0.75rem;
131
+ }
132
+
133
+ .av-action-switcher__title {
134
+ font-weight: 700;
135
+ color: rgb(15, 23, 42);
136
+ }
137
+
138
+ .av-action-switcher__subtitle {
139
+ font-size: 0.9375rem;
140
+ color: rgb(100, 116, 139);
141
+ }
142
+
143
+ .av-action-switcher__list {
144
+ display: flex;
145
+ flex-direction: column;
146
+ gap: 0.75rem;
147
+ }
148
+
149
+ .av-action-switcher__item {
150
+ display: flex;
151
+ align-items: center;
152
+ justify-content: space-between;
153
+ gap: 0.75rem;
154
+ padding: 0.75rem;
155
+ border-radius: 0.75rem;
156
+ background-color: rgb(248, 250, 252);
157
+ }
158
+
159
+ .av-action-switcher__item-text {
160
+ display: grid;
161
+ gap: 0.25rem;
162
+ }
163
+
164
+ .av-action-switcher__item-label {
165
+ font-weight: 600;
166
+ color: rgb(30, 41, 59);
167
+ }
168
+
169
+ .av-action-switcher__item-description {
170
+ font-size: 0.9375rem;
171
+ color: rgb(100, 116, 139);
172
+ }
173
+
174
+ [x-cloak] {
175
+ display: none !important;
176
+ }
177
+ </style>
package/src/AvBrand.astro CHANGED
@@ -14,22 +14,39 @@ interface Props {
14
14
 
15
15
  /** Extra classes if needed */
16
16
  className?: string;
17
+
18
+ /** Additional HTML attributes */
19
+ [attrs: string]: any;
17
20
  }
18
21
 
19
22
  const {
20
- href = "/",
23
+ href = (() => {
24
+ const rawDomain = (import.meta.env.ANSIVERSA_COOKIE_DOMAIN || "ansiversa.com").trim();
25
+ const normalizedDomain = rawDomain.replace(/^(https?:\/\/)/i, "").replace(/\/+$/, "");
26
+ const protocol = rawDomain.match(/^https?:\/\//i)
27
+ ? undefined
28
+ : import.meta.env.PROD
29
+ ? "https"
30
+ : "http";
31
+ const ROOT_URL = rawDomain.match(/^https?:\/\//i)
32
+ ? rawDomain.replace(/\/+$/, "")
33
+ : `${protocol}://${normalizedDomain}`;
34
+
35
+ return ROOT_URL;
36
+ })(),
21
37
  title = "ANSIVERSA",
22
38
  subtitle = "Curated Apps",
23
39
  logo = "/favicon.svg",
24
40
  className = "",
25
- } = Astro.props;
41
+ ...attrs
42
+ } = Astro.props as Props;
26
43
 
27
44
  const classes = ["av-navbar-brand"];
28
45
  if (className) classes.push(className);
29
46
  const classAttr = classes.join(" ");
30
47
  ---
31
48
 
32
- <a href={href} class={classAttr}>
49
+ <a href={href} class={classAttr} {...attrs}>
33
50
  <div class="av-navbar-brand__logo">
34
51
  <img src={logo} alt="Ansiversa logo" />
35
52
  </div>
@@ -0,0 +1,110 @@
1
+ ---
2
+ import AvButton from "./AvButton.astro";
3
+ import AvContainer from "./AvContainer.astro";
4
+
5
+ interface Crumb {
6
+ label: string;
7
+ href?: string;
8
+ }
9
+
10
+ interface Props {
11
+ items: Crumb[];
12
+ className?: string;
13
+ [attr: string]: any; // allow extra attrs like x-data, x-ref, etc.
14
+ }
15
+
16
+ const {
17
+ items,
18
+ className = "",
19
+ ...attrs
20
+ } = Astro.props as Props;
21
+
22
+ const rootClasses = ["av-breadcrumb"];
23
+ if (className) rootClasses.push(className);
24
+ const rootClassAttr = rootClasses.join(" ");
25
+ ---
26
+
27
+ <nav class={rootClassAttr} aria-label="Breadcrumb" {...attrs}>
28
+ <AvContainer className="av-breadcrumb__container">
29
+ <ol class="av-breadcrumb__list">
30
+ {items.map((item, index) => {
31
+ const isLast = index === items.length - 1;
32
+
33
+ return (
34
+ <li
35
+ class="av-breadcrumb__item"
36
+ aria-current={isLast ? "page" : undefined}
37
+ >
38
+ {item.href && !isLast ? (
39
+ <AvButton
40
+ href={item.href}
41
+ variant="ghost"
42
+ size="sm"
43
+ className="av-breadcrumb__link"
44
+ >
45
+ {item.label}
46
+ </AvButton>
47
+ ) : (
48
+ <span class={isLast ? "av-breadcrumb__current" : "av-breadcrumb__label"}>
49
+ {item.label}
50
+ </span>
51
+ )}
52
+
53
+ {!isLast && (
54
+ <span class="av-breadcrumb__separator" aria-hidden="true">
55
+ /
56
+ </span>
57
+ )}
58
+ </li>
59
+ );
60
+ })}
61
+ </ol>
62
+ </AvContainer>
63
+ </nav>
64
+
65
+ <style>
66
+ .av-breadcrumb {
67
+ width: 100%;
68
+ }
69
+
70
+ .av-breadcrumb__container {
71
+ /* Left aligned by default – container just gives padding/max-width */
72
+ }
73
+
74
+ .av-breadcrumb__list {
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 0.25rem;
78
+ padding: 0;
79
+ margin: 0;
80
+ list-style: none;
81
+ font-size: var(--ans-font-size-caption, 0.8125rem);
82
+ color: var(--ans-muted-fg, #6b7280);
83
+ }
84
+
85
+ .av-breadcrumb__item {
86
+ display: inline-flex;
87
+ align-items: center;
88
+ gap: 0.25rem;
89
+ white-space: nowrap;
90
+ }
91
+
92
+ .av-breadcrumb__link {
93
+ padding-inline: 0.5rem;
94
+ padding-block: 0.25rem;
95
+ }
96
+
97
+ .av-breadcrumb__label {
98
+ color: var(--ans-muted-fg, #6b7280);
99
+ }
100
+
101
+ .av-breadcrumb__current {
102
+ font-weight: 500;
103
+ color: var(--ans-fg, #111827);
104
+ }
105
+
106
+ .av-breadcrumb__separator {
107
+ margin-inline: 0.125rem;
108
+ color: var(--ans-border-subtle, #d1d5db);
109
+ }
110
+ </style>
package/src/AvCard.astro CHANGED
@@ -4,11 +4,15 @@ interface Props {
4
4
  variant?: "default" | "auth" | "soft";
5
5
  /** Extra classes for the outer wrapper */
6
6
  className?: string;
7
+
8
+ /** Additional HTML attributes */
9
+ [attrs: string]: any;
7
10
  }
8
11
 
9
12
  const {
10
13
  variant = "default",
11
14
  className = "",
15
+ ...attrs
12
16
  } = Astro.props as Props;
13
17
 
14
18
  const classes: string[] = [];
@@ -33,6 +37,6 @@ if (className) {
33
37
  const classAttr = classes.join(" ");
34
38
  ---
35
39
 
36
- <div class={classAttr}>
40
+ <div class={classAttr} {...attrs}>
37
41
  <slot />
38
42
  </div>
@@ -14,6 +14,9 @@ interface Props {
14
14
  className?: string;
15
15
  /** Optional hint text below the checkbox */
16
16
  hint?: string;
17
+
18
+ /** Additional HTML attributes */
19
+ [attrs: string]: any;
17
20
  }
18
21
 
19
22
  const props = Astro.props as Props;
@@ -25,6 +28,7 @@ const {
25
28
  checked = false,
26
29
  className = "",
27
30
  hint = "",
31
+ ...attrs
28
32
  } = props;
29
33
 
30
34
  const id = props.id ?? name ?? `checkbox-${Math.random().toString(36).slice(2)}`;
@@ -42,6 +46,7 @@ if (className) wrapperClasses.push(className);
42
46
  class="av-checkbox"
43
47
  required={required}
44
48
  checked={checked}
49
+ {...attrs}
45
50
  />
46
51
  <span>
47
52
  {label ? label : <slot />}
@@ -0,0 +1,18 @@
1
+ ---
2
+ interface Chip {
3
+ id: string;
4
+ label: string;
5
+ }
6
+
7
+ interface Props {
8
+ chips: Chip[];
9
+ }
10
+
11
+ const { chips } = Astro.props as Props;
12
+ ---
13
+
14
+ <div class="av-chip-list">
15
+ {chips.map((chip) => (
16
+ <span class="av-chip-list__chip">{chip.label}</span>
17
+ ))}
18
+ </div>
@@ -5,12 +5,16 @@ interface Props {
5
5
 
6
6
  /** Extra classes */
7
7
  className?: string;
8
+
9
+ /** Additional HTML attributes */
10
+ [attrs: string]: any;
8
11
  }
9
12
 
10
13
  const {
11
14
  size = "default",
12
15
  className = "",
13
- } = Astro.props;
16
+ ...attrs
17
+ } = Astro.props as Props;
14
18
 
15
19
  // Base
16
20
  const classes = ["av-container"];
@@ -25,6 +29,6 @@ if (className) classes.push(className);
25
29
  const classAttr = classes.join(" ");
26
30
  ---
27
31
 
28
- <div class={classAttr}>
32
+ <div class={classAttr} {...attrs}>
29
33
  <slot />
30
34
  </div>
@@ -2,12 +2,18 @@
2
2
  interface Props {
3
3
  /** Extra classes for customization */
4
4
  className?: string;
5
+
6
+ /** Additional HTML attributes */
7
+ [attrs: string]: any;
5
8
  }
6
9
 
7
- const { className = "" } = Astro.props as Props;
10
+ const {
11
+ className = "",
12
+ ...attrs
13
+ } = Astro.props as Props;
8
14
  const classes = ["av-divider"];
9
15
  if (className) classes.push(className);
10
16
  const classAttr = classes.join(" ");
11
17
  ---
12
18
 
13
- <div class={classAttr} />
19
+ <div class={classAttr} {...attrs} />
@@ -0,0 +1,133 @@
1
+ ---
2
+ interface Props {
3
+ title?: string;
4
+ description?: string;
5
+ }
6
+
7
+ const { title, description } = Astro.props as Props;
8
+ ---
9
+
10
+ <div class="av-drawer-root">
11
+ <div class="av-drawer-backdrop" aria-hidden="true"></div>
12
+ <aside
13
+ class="av-drawer"
14
+ role="dialog"
15
+ aria-modal="true"
16
+ aria-labelledby={title ? "av-drawer-title" : undefined}
17
+ >
18
+ <header class="av-drawer__header">
19
+ {title && (
20
+ <h2 id="av-drawer-title" class="av-drawer__title">
21
+ {title}
22
+ </h2>
23
+ )}
24
+ {description && <p class="av-drawer__description">{description}</p>}
25
+ <button
26
+ type="button"
27
+ class="av-drawer__close"
28
+ aria-label="Close"
29
+ x-on:click="$dispatch('close-drawer')"
30
+ >
31
+ <span aria-hidden="true">×</span>
32
+ </button>
33
+ </header>
34
+
35
+ <div class="av-drawer__body">
36
+ <slot />
37
+ </div>
38
+
39
+ <footer class="av-drawer__footer">
40
+ <slot name="footer" />
41
+ </footer>
42
+ </aside>
43
+ </div>
44
+
45
+ <style>
46
+ .av-drawer-root {
47
+ position: fixed;
48
+ inset: 0;
49
+ z-index: 40;
50
+ display: flex;
51
+ justify-content: flex-end;
52
+ }
53
+
54
+ .av-drawer-backdrop {
55
+ position: absolute;
56
+ inset: 0;
57
+ background: rgba(15, 23, 42, 0.55);
58
+ backdrop-filter: blur(2px);
59
+ }
60
+
61
+ .av-drawer {
62
+ position: relative;
63
+ width: 100%;
64
+ max-width: 420px;
65
+ height: 100%;
66
+ background: var(--av-surface, #020617);
67
+ border-left: 1px solid rgba(148, 163, 184, 0.35);
68
+ box-shadow: -12px 0 40px rgba(15, 23, 42, 0.7);
69
+ display: flex;
70
+ flex-direction: column;
71
+ }
72
+
73
+ .av-drawer__header {
74
+ padding: 1.25rem 1.5rem;
75
+ border-bottom: 1px solid rgba(51, 65, 85, 0.8);
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 0.25rem;
79
+ position: relative;
80
+ }
81
+
82
+ .av-drawer__title {
83
+ font-size: 1rem;
84
+ font-weight: 600;
85
+ color: var(--av-fg-strong, #e5e7eb);
86
+ }
87
+
88
+ .av-drawer__description {
89
+ font-size: 0.875rem;
90
+ color: var(--av-fg-muted, #9ca3af);
91
+ }
92
+
93
+ .av-drawer__close {
94
+ position: absolute;
95
+ right: 1.25rem;
96
+ top: 1.1rem;
97
+ border-radius: 999px;
98
+ width: 1.75rem;
99
+ height: 1.75rem;
100
+ display: inline-flex;
101
+ align-items: center;
102
+ justify-content: center;
103
+ border: none;
104
+ background: transparent;
105
+ color: var(--av-fg-muted, #9ca3af);
106
+ cursor: pointer;
107
+ }
108
+
109
+ .av-drawer__close:hover {
110
+ background: rgba(15, 23, 42, 0.8);
111
+ color: var(--av-fg-strong, #e5e7eb);
112
+ }
113
+
114
+ .av-drawer__body {
115
+ padding: 1.25rem 1.5rem;
116
+ flex: 1;
117
+ overflow-y: auto;
118
+ }
119
+
120
+ .av-drawer__footer {
121
+ padding: 0.75rem 1.5rem;
122
+ border-top: 1px solid rgba(51, 65, 85, 0.8);
123
+ display: flex;
124
+ justify-content: flex-end;
125
+ gap: 0.5rem;
126
+ }
127
+
128
+ @media (max-width: 640px) {
129
+ .av-drawer {
130
+ max-width: 100%;
131
+ }
132
+ }
133
+ </style>
@@ -0,0 +1,25 @@
1
+ ---
2
+ import AvButton from "./AvButton.astro";
3
+ import AvCard from "./AvCard.astro";
4
+
5
+ interface Props {
6
+ title: string;
7
+ description?: string;
8
+ ctaLabel?: string;
9
+ ctaHref?: string;
10
+ }
11
+
12
+ const { title, description, ctaLabel, ctaHref } = Astro.props as Props;
13
+ ---
14
+
15
+ <AvCard className="av-empty-state">
16
+ <div class="av-empty-state__body">
17
+ <h3 class="av-empty-state__title">{title}</h3>
18
+ {description && <p class="av-empty-state__description">{description}</p>}
19
+ {ctaLabel && ctaHref && (
20
+ <AvButton href={ctaHref} variant="primary">
21
+ {ctaLabel}
22
+ </AvButton>
23
+ )}
24
+ </div>
25
+ </AvCard>
@@ -4,11 +4,15 @@ interface Props {
4
4
  variant?: "primary" | "secondary" | "default";
5
5
  /** Extra classes for the item wrapper */
6
6
  className?: string;
7
+
8
+ /** Additional HTML attributes */
9
+ [attrs: string]: any;
7
10
  }
8
11
 
9
12
  const {
10
13
  variant = "default",
11
14
  className = "",
15
+ ...attrs
12
16
  } = Astro.props as Props;
13
17
 
14
18
  const itemClasses = ["av-feature-item"];
@@ -19,7 +23,7 @@ if (variant === "primary") dotClasses.push("av-feature-dot--primary");
19
23
  if (variant === "secondary") dotClasses.push("av-feature-dot--secondary");
20
24
  ---
21
25
 
22
- <div class={itemClasses.join(" ")}>
26
+ <div class={itemClasses.join(" ")} {...attrs}>
23
27
  <span class={dotClasses.join(" ")}></span>
24
28
  <span>
25
29
  <slot />
@@ -2,14 +2,20 @@
2
2
  interface Props {
3
3
  /** Extra classes for the list wrapper */
4
4
  className?: string;
5
+
6
+ /** Additional HTML attributes */
7
+ [attrs: string]: any;
5
8
  }
6
9
 
7
- const { className = "" } = Astro.props as Props;
10
+ const {
11
+ className = "",
12
+ ...attrs
13
+ } = Astro.props as Props;
8
14
  const classes = ["av-feature-list"];
9
15
  if (className) classes.push(className);
10
16
  const classAttr = classes.join(" ");
11
17
  ---
12
18
 
13
- <div class={classAttr}>
19
+ <div class={classAttr} {...attrs}>
14
20
  <slot />
15
21
  </div>
@@ -13,6 +13,9 @@ interface Props {
13
13
 
14
14
  /** Extra classes for the wrapper */
15
15
  className?: string;
16
+
17
+ /** Additional HTML attributes */
18
+ [attrs: string]: any;
16
19
  }
17
20
 
18
21
  const {
@@ -22,14 +25,15 @@ const {
22
25
  ],
23
26
  tagline = "Advanced Next-Gen Software Innovation and Versatility.",
24
27
  className = "",
25
- } = Astro.props;
28
+ ...attrs
29
+ } = Astro.props as Props;
26
30
 
27
31
  const classes = ["av-footer"];
28
32
  if (className) classes.push(className);
29
33
  const classAttr = classes.join(" ");
30
34
  ---
31
35
 
32
- <footer class={classAttr}>
36
+ <footer class={classAttr} {...attrs}>
33
37
  <div class="av-footer-inner">
34
38
  <p class="av-caption">
35
39
  © {new Date().getFullYear()} Ansiversa. All rights reserved.
package/src/AvInput.astro CHANGED
@@ -12,6 +12,7 @@ interface Props {
12
12
  id?: string;
13
13
  hint?: string;
14
14
  autocomplete?: string;
15
+ [attrs: string]: any;
15
16
  }
16
17
 
17
18
  const props = Astro.props as Props;
@@ -29,6 +30,7 @@ const {
29
30
  id = name ?? `input-${Math.random().toString(36).slice(2)}`,
30
31
  hint = "",
31
32
  autocomplete,
33
+ ...attrs
32
34
  } = props;
33
35
 
34
36
  const wrapperClasses = ["av-form-group", "av-input-wrapper"];
@@ -63,6 +65,7 @@ const isPassword = type === "password";
63
65
  required={required}
64
66
  disabled={disabled}
65
67
  autocomplete={autocomplete}
68
+ {...attrs}
66
69
  />
67
70
 
68
71
  <!-- PASSWORD TOGGLE BUTTON -->
@@ -0,0 +1,25 @@
1
+ ---
2
+ interface Item {
3
+ id: string;
4
+ title: string;
5
+ subtitle?: string;
6
+ meta?: string;
7
+ isCurrent?: boolean;
8
+ }
9
+
10
+ interface Props {
11
+ items: Item[];
12
+ }
13
+
14
+ const { items } = Astro.props as Props;
15
+ ---
16
+
17
+ <ul class="av-item-list">
18
+ {items.map((item) => (
19
+ <li class="av-item-list__item" data-current={item.isCurrent ? "true" : undefined}>
20
+ <p class="av-item-list__title">{item.title}</p>
21
+ {item.subtitle && <p class="av-item-list__subtitle">{item.subtitle}</p>}
22
+ {item.meta && <p class="av-item-list__meta">{item.meta}</p>}
23
+ </li>
24
+ ))}
25
+ </ul>
@@ -2,18 +2,22 @@
2
2
  interface Props {
3
3
  /** Extra classes for the header element */
4
4
  className?: string;
5
+
6
+ /** Additional HTML attributes */
7
+ [attrs: string]: any;
5
8
  }
6
9
 
7
10
  const {
8
11
  className = "",
9
- } = Astro.props;
12
+ ...attrs
13
+ } = Astro.props as Props;
10
14
 
11
15
  const classes = ["av-navbar"];
12
16
  if (className) classes.push(className);
13
17
  const classAttr = classes.join(" ");
14
18
  ---
15
19
 
16
- <header class={classAttr}>
20
+ <header class={classAttr} {...attrs}>
17
21
  <div class="av-navbar-inner">
18
22
  <slot />
19
23
  </div>
@@ -17,9 +17,16 @@ interface User {
17
17
  interface Props {
18
18
  className?: string;
19
19
  user?: User;
20
+
21
+ /** Additional HTML attributes */
22
+ [attrs: string]: any;
20
23
  }
21
24
 
22
- const { className = "", user } = Astro.props as Props;
25
+ const {
26
+ className = "",
27
+ user,
28
+ ...attrs
29
+ } = Astro.props as Props;
23
30
 
24
31
  const classes = ["av-navbar-actions"];
25
32
  if (className) classes.push(className);
@@ -87,7 +94,7 @@ const userInitial =
87
94
  (user?.name?.charAt(0) ?? user?.email?.charAt(0) ?? "?").toUpperCase();
88
95
  ---
89
96
 
90
- <div class={classAttr}>
97
+ <div class={classAttr} {...attrs}>
91
98
  <!-- Left: Always visible -->
92
99
  {baseLinks.map((item) => (
93
100
  <AvButton
@@ -0,0 +1,42 @@
1
+ ---
2
+ import AvButton from "./AvButton.astro";
3
+ import AvContainer from "./AvContainer.astro";
4
+
5
+ interface Action {
6
+ label: string;
7
+ href: string;
8
+ variant?: string;
9
+ }
10
+
11
+ interface Props {
12
+ title: string;
13
+ description?: string;
14
+ primaryAction?: Action;
15
+ secondaryAction?: Action;
16
+ }
17
+
18
+ const { title, description, primaryAction, secondaryAction } = Astro.props as Props;
19
+ ---
20
+
21
+ <section class="av-page-header">
22
+ <AvContainer className="av-page-header__container">
23
+ <div class="av-page-header__body">
24
+ <h1 class="av-page-header__title">{title}</h1>
25
+ {description && <p class="av-page-header__description">{description}</p>}
26
+ {(primaryAction || secondaryAction) && (
27
+ <div class="av-page-header__actions">
28
+ {primaryAction && (
29
+ <AvButton href={primaryAction.href} variant={primaryAction.variant ?? "primary"}>
30
+ {primaryAction.label}
31
+ </AvButton>
32
+ )}
33
+ {secondaryAction && (
34
+ <AvButton href={secondaryAction.href} variant={secondaryAction.variant ?? "ghost"}>
35
+ {secondaryAction.label}
36
+ </AvButton>
37
+ )}
38
+ </div>
39
+ )}
40
+ </div>
41
+ </AvContainer>
42
+ </section>
@@ -0,0 +1,59 @@
1
+ ---
2
+ interface Props {
3
+ label?: string;
4
+ name?: string;
5
+ required?: boolean;
6
+ disabled?: boolean;
7
+ className?: string;
8
+ selectClass?: string;
9
+ id?: string;
10
+ hint?: string;
11
+
12
+ /** Additional HTML attributes */
13
+ [attrs: string]: any;
14
+ }
15
+
16
+ const props = Astro.props as Props;
17
+
18
+ const {
19
+ label,
20
+ name,
21
+ required = false,
22
+ disabled = false,
23
+ className = "",
24
+ selectClass = "",
25
+ id = name ?? `select-${Math.random().toString(36).slice(2)}`,
26
+ hint = "",
27
+ ...attrs
28
+ } = props;
29
+
30
+ const wrapperClasses: string[] = ["av-form-group", "av-input-wrapper"];
31
+ if (className) wrapperClasses.push(className);
32
+
33
+ const selectClasses: string[] = ["av-input"];
34
+ if (selectClass) selectClasses.push(selectClass);
35
+ ---
36
+
37
+ <div class={wrapperClasses.join(" ")}>
38
+ {label && (
39
+ <label class="av-input-label" for={id}>
40
+ {label}
41
+ {required && <span class="av-input-required">*</span>}
42
+ </label>
43
+ )}
44
+
45
+ <div class="av-input-select-wrapper">
46
+ <select
47
+ id={id}
48
+ name={name}
49
+ class={selectClasses.join(" ")}
50
+ disabled={disabled}
51
+ required={required}
52
+ {...attrs}
53
+ >
54
+ <slot />
55
+ </select>
56
+ </div>
57
+
58
+ {hint && <p class="av-form-hint">{hint}</p>}
59
+ </div>
@@ -0,0 +1,24 @@
1
+ ---
2
+ import AvButton from "./AvButton.astro";
3
+ import AvCard from "./AvCard.astro";
4
+
5
+ interface Props {
6
+ name: string;
7
+ description: string;
8
+ accentClass?: string;
9
+ href?: string;
10
+ }
11
+
12
+ const { name, description, accentClass, href = "#" } = Astro.props as Props;
13
+ ---
14
+
15
+ <AvCard className={`av-template-card ${accentClass ?? ""}`.trim()}>
16
+ <div class="av-template-card__header">
17
+ <p class="av-template-card__name">{name}</p>
18
+ <span class="av-template-card__accent" aria-hidden="true"></span>
19
+ </div>
20
+ <p class="av-template-card__description">{description}</p>
21
+ <div class="av-template-card__actions">
22
+ <AvButton href={href}>Use Template</AvButton>
23
+ </div>
24
+ </AvCard>
@@ -0,0 +1,9 @@
1
+ ---
2
+ import AvContainer from "./AvContainer.astro";
3
+ ---
4
+
5
+ <AvContainer className="av-template-grid">
6
+ <div class="av-template-grid__items">
7
+ <slot />
8
+ </div>
9
+ </AvContainer>
@@ -0,0 +1,35 @@
1
+ ---
2
+ interface Props {
3
+ label?: string;
4
+ name?: string;
5
+ placeholder?: string;
6
+ rows?: number;
7
+ value?: string;
8
+ required?: boolean;
9
+
10
+ /** Additional HTML attributes */
11
+ [attrs: string]: any;
12
+ }
13
+
14
+ const {
15
+ label,
16
+ name,
17
+ placeholder,
18
+ rows = 4,
19
+ value,
20
+ required,
21
+ ...attrs
22
+ } = Astro.props as Props;
23
+ ---
24
+
25
+ <div class="av-field">
26
+ {label ? <label class="av-field__label">{label}</label> : null}
27
+ <textarea
28
+ class="av-textarea"
29
+ placeholder={placeholder}
30
+ rows={rows}
31
+ name={name}
32
+ required={required}
33
+ {...attrs}
34
+ >{value}</textarea>
35
+ </div>
@@ -7,12 +7,23 @@ export interface TimelineItem {
7
7
 
8
8
  interface Props {
9
9
  items: TimelineItem[];
10
+ className?: string;
11
+
12
+ /** Additional HTML attributes */
13
+ [attrs: string]: any;
10
14
  }
11
15
 
12
- const { items = [] } = Astro.props;
16
+ const {
17
+ items = [],
18
+ className = "",
19
+ ...attrs
20
+ } = Astro.props as Props;
21
+
22
+ const classes = ["av-timeline"];
23
+ if (className) classes.push(className);
13
24
  ---
14
25
 
15
- <div class="av-timeline">
26
+ <div class={classes.join(" ")} {...attrs}>
16
27
  {items.map((item, index) => (
17
28
  <div
18
29
  class={`av-timeline-row${
@@ -0,0 +1,42 @@
1
+ ---
2
+ import AvButton from "./AvButton.astro";
3
+ import AvContainer from "./AvContainer.astro";
4
+ import AvDivider from "./AvDivider.astro";
5
+
6
+ interface ToolbarAction {
7
+ label: string;
8
+ href: string;
9
+ }
10
+
11
+ interface Props {
12
+ saveStatus: "idle" | "saving" | "saved";
13
+ lastSavedAt?: string;
14
+ actions?: ToolbarAction[];
15
+ }
16
+
17
+ const { saveStatus, lastSavedAt, actions = [] } = Astro.props as Props;
18
+
19
+ const statusLabel =
20
+ saveStatus === "saving" ? "Saving…" : saveStatus === "saved" ? "Changes saved" : "Not saved yet";
21
+ ---
22
+
23
+ <div class="av-toolbar">
24
+ <AvContainer className="av-toolbar__container">
25
+ <div class="av-toolbar__status">
26
+ <span class="av-toolbar__status-dot" data-state={saveStatus}></span>
27
+ <span class="av-toolbar__status-text">{statusLabel}</span>
28
+ {lastSavedAt && <span class="av-toolbar__meta">{lastSavedAt}</span>}
29
+ </div>
30
+
31
+ {actions.length > 0 && (
32
+ <div class="av-toolbar__actions">
33
+ {actions.map((action) => (
34
+ <AvButton variant="ghost" href={action.href} size="sm">
35
+ {action.label}
36
+ </AvButton>
37
+ ))}
38
+ </div>
39
+ )}
40
+ </AvContainer>
41
+ <AvDivider />
42
+ </div>