@ansiversa/components 0.0.3 → 0.0.4
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/package.json +1 -1
- package/src/AvBrand.astro +41 -0
- package/src/AvButton.astro +69 -0
- package/src/AvCard.astro +38 -0
- package/src/AvCheckbox.astro +56 -0
- package/src/AvContainer.astro +30 -0
- package/src/AvDivider.astro +13 -0
- package/src/AvFeatureItem.astro +27 -0
- package/src/AvFeatureList.astro +15 -0
- package/src/AvFooter.astro +48 -0
- package/src/AvInput.astro +96 -0
- package/src/AvNavbar.astro +20 -0
- package/src/AvNavbarActions.astro +168 -0
- package/src/AvTimeline.astro +99 -0
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Link destination (default: home) */
|
|
4
|
+
href?: string;
|
|
5
|
+
|
|
6
|
+
/** Title text — default ANSIVERSA */
|
|
7
|
+
title?: string;
|
|
8
|
+
|
|
9
|
+
/** Subtitle text — default Curated Apps */
|
|
10
|
+
subtitle?: string;
|
|
11
|
+
|
|
12
|
+
/** Path to the logo icon (default: /favicon.svg) */
|
|
13
|
+
logo?: string;
|
|
14
|
+
|
|
15
|
+
/** Extra classes if needed */
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
href = "/",
|
|
21
|
+
title = "ANSIVERSA",
|
|
22
|
+
subtitle = "Curated Apps",
|
|
23
|
+
logo = "/favicon.svg",
|
|
24
|
+
className = "",
|
|
25
|
+
} = Astro.props;
|
|
26
|
+
|
|
27
|
+
const classes = ["av-navbar-brand"];
|
|
28
|
+
if (className) classes.push(className);
|
|
29
|
+
const classAttr = classes.join(" ");
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<a href={href} class={classAttr}>
|
|
33
|
+
<div class="av-navbar-brand__logo">
|
|
34
|
+
<img src={logo} alt="Ansiversa logo" />
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
<div class="av-navbar-brand__meta">
|
|
38
|
+
<span class="av-navbar-brand__title av-gradient-text">{title}</span>
|
|
39
|
+
<span class="av-navbar-brand__subtitle">{subtitle}</span>
|
|
40
|
+
</div>
|
|
41
|
+
</a>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
/** visual style */
|
|
4
|
+
variant?: "primary" | "ghost" | "outline";
|
|
5
|
+
/** size */
|
|
6
|
+
size?: "sm" | "md" | "lg";
|
|
7
|
+
/** full width */
|
|
8
|
+
block?: boolean;
|
|
9
|
+
/** if set, renders as <a> instead of <button> */
|
|
10
|
+
href?: string;
|
|
11
|
+
/** button type when rendering <button> */
|
|
12
|
+
type?: "button" | "submit" | "reset";
|
|
13
|
+
/** disabled state (for <button>) */
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** extra classes if ever needed */
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
variant = "primary",
|
|
21
|
+
size = "md",
|
|
22
|
+
block = false,
|
|
23
|
+
href,
|
|
24
|
+
type = "button",
|
|
25
|
+
disabled = false,
|
|
26
|
+
className,
|
|
27
|
+
} = Astro.props;
|
|
28
|
+
|
|
29
|
+
// Build class list based on our global.css system
|
|
30
|
+
const classes: string[] = [];
|
|
31
|
+
|
|
32
|
+
// Variant (each variant class already includes av-btn base via @apply)
|
|
33
|
+
if (variant === "primary") {
|
|
34
|
+
classes.push("av-btn-primary");
|
|
35
|
+
} else if (variant === "ghost") {
|
|
36
|
+
classes.push("av-btn-ghost");
|
|
37
|
+
} else if (variant === "outline") {
|
|
38
|
+
classes.push("av-btn-outline");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Size
|
|
42
|
+
if (size === "sm") {
|
|
43
|
+
classes.push("av-btn-sm");
|
|
44
|
+
} else if (size === "lg") {
|
|
45
|
+
classes.push("av-btn-lg");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Block
|
|
49
|
+
if (block) {
|
|
50
|
+
classes.push("av-btn-block");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Extra classes (optional)
|
|
54
|
+
if (className) {
|
|
55
|
+
classes.push(className);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const classAttr = classes.join(" ");
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
{href ? (
|
|
62
|
+
<a href={href} class={classAttr}>
|
|
63
|
+
<slot />
|
|
64
|
+
</a>
|
|
65
|
+
) : (
|
|
66
|
+
<button type={type} class={classAttr} disabled={disabled}>
|
|
67
|
+
<slot />
|
|
68
|
+
</button>
|
|
69
|
+
)}
|
package/src/AvCard.astro
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Visual style of the card */
|
|
4
|
+
variant?: "default" | "auth" | "soft";
|
|
5
|
+
/** Extra classes for the outer wrapper */
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
variant = "default",
|
|
11
|
+
className = "",
|
|
12
|
+
} = Astro.props as Props;
|
|
13
|
+
|
|
14
|
+
const classes: string[] = [];
|
|
15
|
+
|
|
16
|
+
// Base card class
|
|
17
|
+
if (variant === "soft") {
|
|
18
|
+
classes.push("av-card-soft");
|
|
19
|
+
} else {
|
|
20
|
+
// default and auth both use av-card base
|
|
21
|
+
classes.push("av-card");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Auth-specific modifier
|
|
25
|
+
if (variant === "auth") {
|
|
26
|
+
classes.push("av-card-auth");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (className) {
|
|
30
|
+
classes.push(className);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const classAttr = classes.join(" ");
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<div class={classAttr}>
|
|
37
|
+
<slot />
|
|
38
|
+
</div>
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Checkbox label text (if no children slot is provided) */
|
|
4
|
+
label?: string;
|
|
5
|
+
/** Input name */
|
|
6
|
+
name?: string;
|
|
7
|
+
/** Input id */
|
|
8
|
+
id?: string;
|
|
9
|
+
/** Whether the checkbox is required */
|
|
10
|
+
required?: boolean;
|
|
11
|
+
/** Whether the checkbox is checked by default */
|
|
12
|
+
checked?: boolean;
|
|
13
|
+
/** Extra classes for the outer wrapper */
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Optional hint text below the checkbox */
|
|
16
|
+
hint?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = Astro.props as Props;
|
|
20
|
+
|
|
21
|
+
const {
|
|
22
|
+
label,
|
|
23
|
+
name,
|
|
24
|
+
required = false,
|
|
25
|
+
checked = false,
|
|
26
|
+
className = "",
|
|
27
|
+
hint = "",
|
|
28
|
+
} = props;
|
|
29
|
+
|
|
30
|
+
const id = props.id ?? name ?? `checkbox-${Math.random().toString(36).slice(2)}`;
|
|
31
|
+
|
|
32
|
+
const wrapperClasses: string[] = [];
|
|
33
|
+
if (className) wrapperClasses.push(className);
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
<div class={wrapperClasses.join(" ")}>
|
|
37
|
+
<label class="av-inline-check" for={id}>
|
|
38
|
+
<input
|
|
39
|
+
id={id}
|
|
40
|
+
name={name}
|
|
41
|
+
type="checkbox"
|
|
42
|
+
class="av-checkbox"
|
|
43
|
+
required={required}
|
|
44
|
+
checked={checked}
|
|
45
|
+
/>
|
|
46
|
+
<span>
|
|
47
|
+
{label ? label : <slot />}
|
|
48
|
+
</span>
|
|
49
|
+
</label>
|
|
50
|
+
|
|
51
|
+
{hint && (
|
|
52
|
+
<p class="av-form-hint">
|
|
53
|
+
{hint}
|
|
54
|
+
</p>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Container size variant */
|
|
4
|
+
size?: "default" | "narrow" | "wide" | "full";
|
|
5
|
+
|
|
6
|
+
/** Extra classes */
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
size = "default",
|
|
12
|
+
className = "",
|
|
13
|
+
} = Astro.props;
|
|
14
|
+
|
|
15
|
+
// Base
|
|
16
|
+
const classes = ["av-container"];
|
|
17
|
+
|
|
18
|
+
// Variants mapped to our custom classes from global.css
|
|
19
|
+
if (size === "narrow") classes.push("av-container-narrow");
|
|
20
|
+
if (size === "wide") classes.push("av-container-wide");
|
|
21
|
+
if (size === "full") classes.push("av-container-full");
|
|
22
|
+
|
|
23
|
+
if (className) classes.push(className);
|
|
24
|
+
|
|
25
|
+
const classAttr = classes.join(" ");
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
<div class={classAttr}>
|
|
29
|
+
<slot />
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Extra classes for customization */
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { className = "" } = Astro.props as Props;
|
|
8
|
+
const classes = ["av-divider"];
|
|
9
|
+
if (className) classes.push(className);
|
|
10
|
+
const classAttr = classes.join(" ");
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div class={classAttr} />
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Dot style */
|
|
4
|
+
variant?: "primary" | "secondary" | "default";
|
|
5
|
+
/** Extra classes for the item wrapper */
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
variant = "default",
|
|
11
|
+
className = "",
|
|
12
|
+
} = Astro.props as Props;
|
|
13
|
+
|
|
14
|
+
const itemClasses = ["av-feature-item"];
|
|
15
|
+
if (className) itemClasses.push(className);
|
|
16
|
+
|
|
17
|
+
const dotClasses = ["av-feature-dot"];
|
|
18
|
+
if (variant === "primary") dotClasses.push("av-feature-dot--primary");
|
|
19
|
+
if (variant === "secondary") dotClasses.push("av-feature-dot--secondary");
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
<div class={itemClasses.join(" ")}>
|
|
23
|
+
<span class={dotClasses.join(" ")}></span>
|
|
24
|
+
<span>
|
|
25
|
+
<slot />
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Extra classes for the list wrapper */
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const { className = "" } = Astro.props as Props;
|
|
8
|
+
const classes = ["av-feature-list"];
|
|
9
|
+
if (className) classes.push(className);
|
|
10
|
+
const classAttr = classes.join(" ");
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div class={classAttr}>
|
|
14
|
+
<slot />
|
|
15
|
+
</div>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface FooterLink {
|
|
3
|
+
label: string;
|
|
4
|
+
href: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** List of footer navigation links */
|
|
9
|
+
links?: FooterLink[];
|
|
10
|
+
|
|
11
|
+
/** Footer tagline text */
|
|
12
|
+
tagline?: string;
|
|
13
|
+
|
|
14
|
+
/** Extra classes for the wrapper */
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const {
|
|
19
|
+
links = [
|
|
20
|
+
{ label: "Terms", href: "#terms" },
|
|
21
|
+
{ label: "Privacy", href: "#privacy" },
|
|
22
|
+
],
|
|
23
|
+
tagline = "Advanced Next-Gen Software Innovation and Versatility.",
|
|
24
|
+
className = "",
|
|
25
|
+
} = Astro.props;
|
|
26
|
+
|
|
27
|
+
const classes = ["av-footer"];
|
|
28
|
+
if (className) classes.push(className);
|
|
29
|
+
const classAttr = classes.join(" ");
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
<footer class={classAttr}>
|
|
33
|
+
<div class="av-footer-inner">
|
|
34
|
+
<p class="av-caption">
|
|
35
|
+
© {new Date().getFullYear()} Ansiversa. All rights reserved.
|
|
36
|
+
</p>
|
|
37
|
+
|
|
38
|
+
<div class="av-footer-meta">
|
|
39
|
+
{links.map((item) => (
|
|
40
|
+
<a href={item.href} class="av-nav-link">{item.label}</a>
|
|
41
|
+
))}
|
|
42
|
+
|
|
43
|
+
<span class="av-footer-separator">•</span>
|
|
44
|
+
|
|
45
|
+
<span class="av-footer-quote">{tagline}</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</footer>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
label?: string;
|
|
4
|
+
name?: string;
|
|
5
|
+
placeholder?: string;
|
|
6
|
+
type?: astroHTML.JSX.HTMLInputTypeAttribute;
|
|
7
|
+
value?: string;
|
|
8
|
+
required?: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
className?: string;
|
|
11
|
+
inputClass?: string;
|
|
12
|
+
id?: string;
|
|
13
|
+
hint?: string;
|
|
14
|
+
autocomplete?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const props = Astro.props as Props;
|
|
18
|
+
|
|
19
|
+
const {
|
|
20
|
+
label,
|
|
21
|
+
name,
|
|
22
|
+
placeholder = "",
|
|
23
|
+
type = "text",
|
|
24
|
+
value,
|
|
25
|
+
required = false,
|
|
26
|
+
disabled = false,
|
|
27
|
+
className = "",
|
|
28
|
+
inputClass = "",
|
|
29
|
+
id = name ?? `input-${Math.random().toString(36).slice(2)}`,
|
|
30
|
+
hint = "",
|
|
31
|
+
autocomplete,
|
|
32
|
+
} = props;
|
|
33
|
+
|
|
34
|
+
const wrapperClasses = ["av-form-group", "av-input-wrapper"];
|
|
35
|
+
if (className) wrapperClasses.push(className);
|
|
36
|
+
|
|
37
|
+
const inputClasses = ["av-input"];
|
|
38
|
+
if (inputClass) inputClasses.push(inputClass);
|
|
39
|
+
|
|
40
|
+
const isPassword = type === "password";
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
<div
|
|
44
|
+
class={wrapperClasses.join(" ")}
|
|
45
|
+
x-data={isPassword ? `{ show: false }` : undefined}
|
|
46
|
+
>
|
|
47
|
+
{label && (
|
|
48
|
+
<label class="av-label" for={id}>
|
|
49
|
+
{label}
|
|
50
|
+
{required && <span aria-hidden="true"> *</span>}
|
|
51
|
+
</label>
|
|
52
|
+
)}
|
|
53
|
+
|
|
54
|
+
<div class="av-input-password-container">
|
|
55
|
+
<input
|
|
56
|
+
id={id}
|
|
57
|
+
name={name}
|
|
58
|
+
type={isPassword ? undefined : type}
|
|
59
|
+
:type={isPassword ? `(show ? 'text' : 'password')` : undefined}
|
|
60
|
+
class={inputClasses.join(" ")}
|
|
61
|
+
placeholder={placeholder}
|
|
62
|
+
value={value}
|
|
63
|
+
required={required}
|
|
64
|
+
disabled={disabled}
|
|
65
|
+
autocomplete={autocomplete}
|
|
66
|
+
/>
|
|
67
|
+
|
|
68
|
+
<!-- PASSWORD TOGGLE BUTTON -->
|
|
69
|
+
{isPassword && (
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
class="av-password-toggle"
|
|
73
|
+
@click="show = !show"
|
|
74
|
+
:aria-label="show ? 'Hide password' : 'Show password'"
|
|
75
|
+
>
|
|
76
|
+
<template x-if="!show">
|
|
77
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="av-password-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
78
|
+
<path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/>
|
|
79
|
+
<circle cx="12" cy="12" r="3"/>
|
|
80
|
+
</svg>
|
|
81
|
+
</template>
|
|
82
|
+
|
|
83
|
+
<template x-if="show">
|
|
84
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="av-password-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
|
85
|
+
<path d="M17.94 17.94A10.94 10.94 0 0 1 12 19c-7 0-11-7-11-7a21.77 21.77 0 0 1 5.06-5.94m3.07-1.6A10.94 10.94 0 0 1 12 5c7 0 11 7 11 7a21.77 21.77 0 0 1-2.25 3.2"/>
|
|
86
|
+
<line x1="1" y1="1" x2="23" y2="23"/>
|
|
87
|
+
</svg>
|
|
88
|
+
</template>
|
|
89
|
+
</button>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{hint && (
|
|
94
|
+
<p class="av-form-hint">{hint}</p>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Extra classes for the header element */
|
|
4
|
+
className?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const {
|
|
8
|
+
className = "",
|
|
9
|
+
} = Astro.props;
|
|
10
|
+
|
|
11
|
+
const classes = ["av-navbar"];
|
|
12
|
+
if (className) classes.push(className);
|
|
13
|
+
const classAttr = classes.join(" ");
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
<header class={classAttr}>
|
|
17
|
+
<div class="av-navbar-inner">
|
|
18
|
+
<slot />
|
|
19
|
+
</div>
|
|
20
|
+
</header>
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
import AvButton from "./AvButton.astro";
|
|
3
|
+
import { actions } from "astro:actions";
|
|
4
|
+
|
|
5
|
+
interface NavLink {
|
|
6
|
+
label: string;
|
|
7
|
+
href: string;
|
|
8
|
+
variant?: "primary" | "ghost" | "outline";
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface User {
|
|
13
|
+
id: string;
|
|
14
|
+
email: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
className?: string;
|
|
20
|
+
user?: User;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const { className = "", user } = Astro.props as Props;
|
|
24
|
+
|
|
25
|
+
const classes = ["av-navbar-actions"];
|
|
26
|
+
if (className) classes.push(className);
|
|
27
|
+
const classAttr = classes.join(" ");
|
|
28
|
+
|
|
29
|
+
const iconMap: Record<string, string> = {
|
|
30
|
+
Apps: `
|
|
31
|
+
<svg class="av-icon" viewBox='0 0 24 24'>
|
|
32
|
+
<rect x='3' y='3' width='7' height='7' rx='2'/>
|
|
33
|
+
<rect x='14' y='3' width='7' height='7' rx='2'/>
|
|
34
|
+
<rect x='3' y='14' width='7' height='7' rx='2'/>
|
|
35
|
+
<rect x='14' y='14' width='7' height='7' rx='2'/>
|
|
36
|
+
</svg>
|
|
37
|
+
`,
|
|
38
|
+
Pricing: `
|
|
39
|
+
<svg class="av-icon" viewBox='0 0 24 24'>
|
|
40
|
+
<path d='M6 3h7.586a2 2 0 0 1 1.414.586l4.414 4.414a2 2 0 0 1 0 2.828L13 18.242a2 2 0 0 1-2.828 0L3.586 11.657A2 2 0 0 1 3 10.243V6a3 3 0 0 1 3-3Z'/>
|
|
41
|
+
<path d='M9 6h.01'/>
|
|
42
|
+
</svg>
|
|
43
|
+
`,
|
|
44
|
+
About: `
|
|
45
|
+
<svg class="av-icon" viewBox='0 0 24 24'>
|
|
46
|
+
<circle cx='12' cy='8' r='3.25'/>
|
|
47
|
+
<path d='M6 19v-.5a5.5 5.5 0 0 1 11 0V19'/>
|
|
48
|
+
<path d='M12 12.5v2'/>
|
|
49
|
+
</svg>
|
|
50
|
+
`,
|
|
51
|
+
default: `
|
|
52
|
+
<svg class="av-icon" viewBox='0 0 24 24'>
|
|
53
|
+
<circle cx='12' cy='12' r='9'/>
|
|
54
|
+
</svg>
|
|
55
|
+
`,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function renderIcon(label: string) {
|
|
59
|
+
return iconMap[label] ?? iconMap.default;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ALWAYS SHOW THESE LINKS
|
|
63
|
+
const baseLinks: NavLink[] = [
|
|
64
|
+
{ label: "Apps", href: "/apps" },
|
|
65
|
+
{ label: "Pricing", href: "/pricing" },
|
|
66
|
+
{ label: "About", href: "/about" },
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// SIGN-IN link only for guests
|
|
70
|
+
const authLink: NavLink = {
|
|
71
|
+
label: "Sign in",
|
|
72
|
+
href: "/login",
|
|
73
|
+
variant: "primary",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const userInitial =
|
|
77
|
+
(user?.name?.charAt(0) ?? user?.email?.charAt(0) ?? "?").toUpperCase();
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
<div class={classAttr}>
|
|
81
|
+
<!-- Left: Always visible -->
|
|
82
|
+
{baseLinks.map((item) => (
|
|
83
|
+
<AvButton
|
|
84
|
+
href={item.href}
|
|
85
|
+
variant="ghost"
|
|
86
|
+
className="av-nowrap"
|
|
87
|
+
>
|
|
88
|
+
<span class="av-nav-icon" set:html={renderIcon(item.label)}></span>
|
|
89
|
+
<span class="av-nav-label">{item.label}</span>
|
|
90
|
+
</AvButton>
|
|
91
|
+
))}
|
|
92
|
+
|
|
93
|
+
<!-- Right: Sign-in OR User dropdown -->
|
|
94
|
+
{user ? (
|
|
95
|
+
<div class="av-user-menu-wrapper" x-data="{ open: false }">
|
|
96
|
+
<button
|
|
97
|
+
type="button"
|
|
98
|
+
class="av-user-avatar-button"
|
|
99
|
+
aria-label="Account menu"
|
|
100
|
+
aria-haspopup="menu"
|
|
101
|
+
x-on:click="open = !open"
|
|
102
|
+
x-bind:aria-expanded="open"
|
|
103
|
+
>
|
|
104
|
+
<span class="av-user-avatar-circle" aria-hidden="true">
|
|
105
|
+
{userInitial}
|
|
106
|
+
</span>
|
|
107
|
+
<span class="sr-only">Open user menu</span>
|
|
108
|
+
</button>
|
|
109
|
+
|
|
110
|
+
<div
|
|
111
|
+
x-show="open"
|
|
112
|
+
x-cloak
|
|
113
|
+
x-transition.origin.top.right
|
|
114
|
+
x-on:click.outside="open = false"
|
|
115
|
+
class="av-user-menu av-nav-user-menu"
|
|
116
|
+
role="menu"
|
|
117
|
+
aria-label="User menu"
|
|
118
|
+
>
|
|
119
|
+
<div class="av-user-menu-header">
|
|
120
|
+
<div class="av-user-menu-avatar">{userInitial}</div>
|
|
121
|
+
<div class="av-user-menu-meta">
|
|
122
|
+
<p class="av-user-menu-name">{user.name}</p>
|
|
123
|
+
<p class="av-user-menu-email">{user.email}</p>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div class="av-user-menu-divider"></div>
|
|
128
|
+
|
|
129
|
+
<div class="av-nav-user-menu">
|
|
130
|
+
<a href="/dashboard" class="av-user-menu-item av-dropdown-item" role="menuitem">
|
|
131
|
+
<span>Dashboard</span>
|
|
132
|
+
</a>
|
|
133
|
+
|
|
134
|
+
<a
|
|
135
|
+
href="/change-password"
|
|
136
|
+
class="av-user-menu-item av-dropdown-item"
|
|
137
|
+
role="menuitem"
|
|
138
|
+
>
|
|
139
|
+
<span>Change password</span>
|
|
140
|
+
</a>
|
|
141
|
+
|
|
142
|
+
<a href="/settings" class="av-user-menu-item av-dropdown-item" role="menuitem">
|
|
143
|
+
<span>Settings</span>
|
|
144
|
+
</a>
|
|
145
|
+
|
|
146
|
+
<form method="POST" action="/signout">
|
|
147
|
+
<button
|
|
148
|
+
type="submit"
|
|
149
|
+
class="av-user-menu-item av-dropdown-item av-user-menu-item--danger"
|
|
150
|
+
role="menuitem"
|
|
151
|
+
>
|
|
152
|
+
<span>Sign out</span>
|
|
153
|
+
</button>
|
|
154
|
+
</form>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
) : (
|
|
159
|
+
<AvButton
|
|
160
|
+
href={authLink.href}
|
|
161
|
+
variant={authLink.variant}
|
|
162
|
+
className="av-nowrap"
|
|
163
|
+
>
|
|
164
|
+
<span class="av-nav-icon" set:html={renderIcon(authLink.label)}></span>
|
|
165
|
+
<span class="av-nav-label">{authLink.label}</span>
|
|
166
|
+
</AvButton>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
export interface TimelineItem {
|
|
3
|
+
date: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
body: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
items: TimelineItem[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { items = [] } = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div class="av-timeline">
|
|
16
|
+
{items.map((item, index) => (
|
|
17
|
+
<div
|
|
18
|
+
class={`av-timeline-row${
|
|
19
|
+
index === items.length - 1 ? " av-timeline-row--last" : ""
|
|
20
|
+
}`}
|
|
21
|
+
>
|
|
22
|
+
<div class="av-timeline-dot"></div>
|
|
23
|
+
|
|
24
|
+
<div class="av-timeline-content">
|
|
25
|
+
<h3 class="av-timeline-date">{item.date}</h3>
|
|
26
|
+
{item.title && <h4 class="av-timeline-title">{item.title}</h4>}
|
|
27
|
+
<p class="av-timeline-body">{item.body}</p>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
))}
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.av-timeline {
|
|
35
|
+
margin-top: 2rem;
|
|
36
|
+
text-align: left; /* override parent center alignment */
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.av-timeline-row {
|
|
40
|
+
position: relative;
|
|
41
|
+
padding-left: 2.2rem;
|
|
42
|
+
padding-bottom: 1.8rem;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/* Vertical line (per row, so it connects dot → next dot) */
|
|
46
|
+
.av-timeline-row::before {
|
|
47
|
+
content: "";
|
|
48
|
+
position: absolute;
|
|
49
|
+
left: 7px;
|
|
50
|
+
top: 0;
|
|
51
|
+
bottom: 0;
|
|
52
|
+
width: 2px;
|
|
53
|
+
background: var(--ans-border-subtle, rgba(148, 163, 184, 0.6));
|
|
54
|
+
border-radius: 999px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Shorten the line on the last item so it ends nicely */
|
|
58
|
+
.av-timeline-row--last::before {
|
|
59
|
+
bottom: 0.9rem;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Dot sitting on the line */
|
|
63
|
+
.av-timeline-dot {
|
|
64
|
+
position: absolute;
|
|
65
|
+
left: 0;
|
|
66
|
+
top: 0.25rem;
|
|
67
|
+
width: 15px;
|
|
68
|
+
height: 15px;
|
|
69
|
+
border-radius: 50%;
|
|
70
|
+
background: var(--ans-primary, #6366f1);
|
|
71
|
+
border: 2px solid var(--ans-bg, #020617);
|
|
72
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.35);
|
|
73
|
+
z-index: 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Text content */
|
|
77
|
+
.av-timeline-content {
|
|
78
|
+
position: relative;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.av-timeline-date {
|
|
82
|
+
margin: 0;
|
|
83
|
+
font-size: 1.05rem;
|
|
84
|
+
font-weight: 700;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.av-timeline-title {
|
|
88
|
+
margin: 0.15rem 0 0.4rem;
|
|
89
|
+
font-size: 0.98rem;
|
|
90
|
+
font-weight: 500;
|
|
91
|
+
opacity: 0.9;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.av-timeline-body {
|
|
95
|
+
margin: 0;
|
|
96
|
+
line-height: 1.6;
|
|
97
|
+
opacity: 0.88;
|
|
98
|
+
}
|
|
99
|
+
</style>
|