@ansiversa/components 0.0.122 → 0.0.123
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/resume-templates/ResumeTemplateClassic.astro +2 -22
- package/src/resume-templates/ResumeTemplateExecutiveTimeline.astro +3 -13
- package/src/resume-templates/ResumeTemplateMinimal.astro +6 -22
- package/src/resume-templates/ResumeTemplateModernTwoTone.astro +15 -23
- package/src/resume-templates/typescript-schema.ts +147 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { ResumeData } from "./typescript-schema";
|
|
3
|
-
import { formatDateRange } from "./typescript-schema";
|
|
3
|
+
import { buildContactEntries, formatDateRange } from "./typescript-schema";
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
data: ResumeData;
|
|
@@ -13,27 +13,7 @@ const locationLabel =
|
|
|
13
13
|
basics.location?.label ||
|
|
14
14
|
[basics.location?.city, basics.location?.country].filter(Boolean).join(", ");
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
value.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
18
|
-
|
|
19
|
-
const contactItems: Array<{ label: string; href?: string }> = [];
|
|
20
|
-
if (locationLabel) contactItems.push({ label: locationLabel });
|
|
21
|
-
if (basics.contact.email) {
|
|
22
|
-
contactItems.push({ label: basics.contact.email, href: `mailto:${basics.contact.email}` });
|
|
23
|
-
}
|
|
24
|
-
if (basics.contact.phone) {
|
|
25
|
-
const phoneHref = basics.contact.phone.replace(/\s+/g, "");
|
|
26
|
-
contactItems.push({ label: basics.contact.phone, href: `tel:${phoneHref}` });
|
|
27
|
-
}
|
|
28
|
-
if (basics.contact.website) {
|
|
29
|
-
contactItems.push({ label: formatUrlLabel(basics.contact.website), href: basics.contact.website });
|
|
30
|
-
}
|
|
31
|
-
for (const link of basics.links ?? []) {
|
|
32
|
-
const urlLabel = link.url ? formatUrlLabel(link.url) : link.label;
|
|
33
|
-
if (urlLabel) {
|
|
34
|
-
contactItems.push({ label: urlLabel, href: link.url });
|
|
35
|
-
}
|
|
36
|
-
}
|
|
16
|
+
const contactItems = buildContactEntries(basics, locationLabel);
|
|
37
17
|
|
|
38
18
|
const experienceItems = data.experience ?? [];
|
|
39
19
|
const projectItems = data.projects ?? [];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { ResumeData } from "./typescript-schema";
|
|
3
|
-
import { formatDateRange } from "./typescript-schema";
|
|
3
|
+
import { buildContactEntries, formatDateRange, truncateText } from "./typescript-schema";
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
data: ResumeData;
|
|
@@ -13,17 +13,7 @@ const locationLabel =
|
|
|
13
13
|
basics.location?.label ||
|
|
14
14
|
[basics.location?.city, basics.location?.country].filter(Boolean).join(", ");
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
value.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
18
|
-
|
|
19
|
-
const contactChips: string[] = [];
|
|
20
|
-
if (locationLabel) contactChips.push(locationLabel);
|
|
21
|
-
if (basics.contact.email) contactChips.push(basics.contact.email);
|
|
22
|
-
if (basics.contact.phone) contactChips.push(basics.contact.phone);
|
|
23
|
-
if (basics.contact.website) contactChips.push(formatUrlLabel(basics.contact.website));
|
|
24
|
-
for (const link of basics.links ?? []) {
|
|
25
|
-
contactChips.push(link.label ?? formatUrlLabel(link.url));
|
|
26
|
-
}
|
|
16
|
+
const contactChips = buildContactEntries(basics, locationLabel).map((item) => item.label);
|
|
27
17
|
|
|
28
18
|
const normalizeSkillLabel = (value?: string) =>
|
|
29
19
|
(value ?? "")
|
|
@@ -92,7 +82,7 @@ const hasDeclaration = Boolean(declaration.text || declaration.place || declarat
|
|
|
92
82
|
{item.bullets && item.bullets.length > 0 && (
|
|
93
83
|
<ul class="mt-3 list-disc space-y-2 pl-5 text-sm leading-6 text-slate-700">
|
|
94
84
|
{item.bullets.map((bullet) => (
|
|
95
|
-
<li>{bullet}</li>
|
|
85
|
+
<li class="break-words print:line-clamp-2">{truncateText(bullet, 140)}</li>
|
|
96
86
|
))}
|
|
97
87
|
</ul>
|
|
98
88
|
)}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { ResumeData } from "./typescript-schema";
|
|
3
|
-
import { formatDateRange } from "./typescript-schema";
|
|
3
|
+
import { buildContactEntries, formatDateRange, truncateText } from "./typescript-schema";
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
data: ResumeData;
|
|
@@ -13,23 +13,7 @@ const locationLabel =
|
|
|
13
13
|
basics.location?.label ||
|
|
14
14
|
[basics.location?.city, basics.location?.country].filter(Boolean).join(", ");
|
|
15
15
|
|
|
16
|
-
const
|
|
17
|
-
value.replace(/^https?:\/\//, "").replace(/\/$/, "");
|
|
18
|
-
|
|
19
|
-
const contactLinks: Array<{ label: string; href?: string }> = [];
|
|
20
|
-
if (basics.contact.email) {
|
|
21
|
-
contactLinks.push({ label: basics.contact.email, href: `mailto:${basics.contact.email}` });
|
|
22
|
-
}
|
|
23
|
-
if (basics.contact.phone) {
|
|
24
|
-
const phoneHref = basics.contact.phone.replace(/\s+/g, "");
|
|
25
|
-
contactLinks.push({ label: basics.contact.phone, href: `tel:${phoneHref}` });
|
|
26
|
-
}
|
|
27
|
-
if (basics.contact.website) {
|
|
28
|
-
contactLinks.push({ label: formatUrlLabel(basics.contact.website), href: basics.contact.website });
|
|
29
|
-
}
|
|
30
|
-
for (const link of basics.links ?? []) {
|
|
31
|
-
contactLinks.push({ label: link.label ?? formatUrlLabel(link.url), href: link.url });
|
|
32
|
-
}
|
|
16
|
+
const contactLinks = buildContactEntries(basics, locationLabel);
|
|
33
17
|
|
|
34
18
|
const headlineLine = [basics.headline, locationLabel].filter(Boolean).join(" · ");
|
|
35
19
|
const experienceItems = data.experience ?? [];
|
|
@@ -64,7 +48,7 @@ const hasDeclaration = Boolean(declaration.text || declaration.place || declarat
|
|
|
64
48
|
</div>
|
|
65
49
|
|
|
66
50
|
{basics.summary && (
|
|
67
|
-
<p class="max-w-3xl text-sm leading-7 text-slate-700">{basics.summary}</p>
|
|
51
|
+
<p class="max-w-3xl text-sm leading-7 text-slate-700">{truncateText(basics.summary, 220)}</p>
|
|
68
52
|
)}
|
|
69
53
|
</header>
|
|
70
54
|
|
|
@@ -86,13 +70,13 @@ const hasDeclaration = Boolean(declaration.text || declaration.place || declarat
|
|
|
86
70
|
<p class="text-sm text-slate-500">{formatDateRange(item)}</p>
|
|
87
71
|
</div>
|
|
88
72
|
{item.summary && (
|
|
89
|
-
<p class="mt-3 text-sm leading-7 text-slate-700">{item.summary}</p>
|
|
73
|
+
<p class="mt-3 text-sm leading-7 text-slate-700">{truncateText(item.summary, 220)}</p>
|
|
90
74
|
)}
|
|
91
75
|
{item.bullets && item.bullets.length > 0 && (
|
|
92
76
|
<ul class="mt-3 space-y-2 text-sm leading-7 text-slate-700">
|
|
93
77
|
{item.bullets.map((bullet) => (
|
|
94
78
|
<li>
|
|
95
|
-
<span class="font-medium">•</span> {bullet}
|
|
79
|
+
<span class="font-medium">•</span> {truncateText(bullet, 140)}
|
|
96
80
|
</li>
|
|
97
81
|
))}
|
|
98
82
|
</ul>
|
|
@@ -128,7 +112,7 @@ const hasDeclaration = Boolean(declaration.text || declaration.place || declarat
|
|
|
128
112
|
)}
|
|
129
113
|
</div>
|
|
130
114
|
{project.summary && (
|
|
131
|
-
<p class="mt-2 text-sm leading-7 text-slate-700">{project.summary}</p>
|
|
115
|
+
<p class="mt-2 text-sm leading-7 text-slate-700">{truncateText(project.summary, 220)}</p>
|
|
132
116
|
)}
|
|
133
117
|
{project.tags && project.tags.length > 0 && (
|
|
134
118
|
<div class="mt-3 flex flex-wrap gap-2">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { ResumeData } from "./typescript-schema";
|
|
3
|
-
import { formatDateRange } from "./typescript-schema";
|
|
3
|
+
import { buildContactEntries, formatDateRange } from "./typescript-schema";
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
data: ResumeData;
|
|
@@ -17,26 +17,14 @@ const locationLabel =
|
|
|
17
17
|
basics.location?.label ||
|
|
18
18
|
[basics.location?.city, basics.location?.country].filter(Boolean).join(", ");
|
|
19
19
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (basics.contact.email) {
|
|
26
|
-
contactItems.push({ label: basics.contact.email, href: `mailto:${basics.contact.email}` });
|
|
27
|
-
}
|
|
28
|
-
if (basics.contact.phone) {
|
|
29
|
-
const phoneHref = basics.contact.phone.replace(/\s+/g, "");
|
|
30
|
-
contactItems.push({ label: basics.contact.phone, href: `tel:${phoneHref}` });
|
|
31
|
-
}
|
|
32
|
-
if (basics.contact.website) {
|
|
33
|
-
contactItems.push({ label: formatUrlLabel(basics.contact.website), href: basics.contact.website });
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const directContactLine = [basics.contact.email, basics.contact.phone].filter(Boolean).join(" • ");
|
|
20
|
+
const contactItems = buildContactEntries(basics, locationLabel);
|
|
21
|
+
const directContactLine = contactItems
|
|
22
|
+
.filter((item) => item.kind === "email" || item.kind === "phone")
|
|
23
|
+
.map((item) => item.label)
|
|
24
|
+
.join(" • ");
|
|
37
25
|
|
|
38
26
|
const skills = data.skills ?? [];
|
|
39
|
-
const links =
|
|
27
|
+
const links = contactItems.filter((item) => item.kind === "link");
|
|
40
28
|
const languages = data.languages ?? [];
|
|
41
29
|
const experienceItems = data.experience ?? [];
|
|
42
30
|
const projects = data.projects ?? [];
|
|
@@ -66,7 +54,7 @@ const hasDeclaration = Boolean(declaration.text || declaration.place || declarat
|
|
|
66
54
|
<div class="space-y-1 text-slate-200 print:text-slate-700">
|
|
67
55
|
{directContactLine && <div class="font-medium">{directContactLine}</div>}
|
|
68
56
|
{contactItems
|
|
69
|
-
.filter((item) => item.
|
|
57
|
+
.filter((item) => item.kind !== "email" && item.kind !== "phone")
|
|
70
58
|
.map((item) =>
|
|
71
59
|
item.href ? (
|
|
72
60
|
<a class="underline underline-offset-2 print:no-underline" href={item.href}>
|
|
@@ -96,9 +84,13 @@ const hasDeclaration = Boolean(declaration.text || declaration.place || declarat
|
|
|
96
84
|
<div class="text-sm font-semibold text-slate-100 print:text-slate-900">LINKS</div>
|
|
97
85
|
<div class="space-y-1 text-sm text-slate-200 print:text-slate-700">
|
|
98
86
|
{links.map((link) => (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
87
|
+
link.href ? (
|
|
88
|
+
<a class="underline underline-offset-2 print:no-underline" href={link.href}>
|
|
89
|
+
{link.label}
|
|
90
|
+
</a>
|
|
91
|
+
) : (
|
|
92
|
+
<div>{link.label}</div>
|
|
93
|
+
)
|
|
102
94
|
))}
|
|
103
95
|
</div>
|
|
104
96
|
</div>
|
|
@@ -117,3 +117,150 @@ export function formatDateRange(item: {
|
|
|
117
117
|
const end = item.end.month ? `${m(item.end.month)}/${item.end.year}` : `${item.end.year}`;
|
|
118
118
|
return `${start} — ${end}`;
|
|
119
119
|
}
|
|
120
|
+
|
|
121
|
+
export type ResumeContactEntryKind = "location" | "email" | "phone" | "website" | "link";
|
|
122
|
+
|
|
123
|
+
export type ResumeContactEntry = {
|
|
124
|
+
kind: ResumeContactEntryKind;
|
|
125
|
+
label: string;
|
|
126
|
+
href?: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const clean = (value?: string | null) => (value ?? "").replace(/\s+/g, " ").trim();
|
|
130
|
+
|
|
131
|
+
const normalizeEmail = (value?: string | null) => clean(value).toLowerCase();
|
|
132
|
+
|
|
133
|
+
const normalizePhone = (value?: string | null) => clean(value).replace(/[^\d+]/g, "");
|
|
134
|
+
|
|
135
|
+
const normalizeUrlHref = (value?: string | null) => {
|
|
136
|
+
const raw = clean(value);
|
|
137
|
+
if (!raw) return "";
|
|
138
|
+
if (/^mailto:/i.test(raw) || /^tel:/i.test(raw)) return raw;
|
|
139
|
+
const withoutScheme = raw.replace(/^https?:\/\//i, "").replace(/^\/+/, "");
|
|
140
|
+
if (!withoutScheme) return "";
|
|
141
|
+
return `https://${withoutScheme}`;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const canonicalUrlKey = (value?: string | null) => {
|
|
145
|
+
const href = normalizeUrlHref(value);
|
|
146
|
+
if (!href) return "";
|
|
147
|
+
if (/^mailto:/i.test(href) || /^tel:/i.test(href)) return href.toLowerCase();
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const parsed = new URL(href);
|
|
151
|
+
const pathname = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
|
152
|
+
return `${parsed.protocol}//${parsed.hostname.toLowerCase()}${pathname}${parsed.search}`.toLowerCase();
|
|
153
|
+
} catch {
|
|
154
|
+
return href.toLowerCase();
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const formatUrlLabel = (value?: string | null) => {
|
|
159
|
+
const href = normalizeUrlHref(value);
|
|
160
|
+
if (!href) return "";
|
|
161
|
+
if (/^mailto:/i.test(href) || /^tel:/i.test(href)) return href;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const parsed = new URL(href);
|
|
165
|
+
const pathname = parsed.pathname === "/" ? "" : parsed.pathname.replace(/\/$/, "");
|
|
166
|
+
return `${parsed.hostname.toLowerCase()}${pathname}${parsed.search}`;
|
|
167
|
+
} catch {
|
|
168
|
+
return href.replace(/^https?:\/\//i, "").replace(/\/$/, "");
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const truncateText = (value?: string | null, max = 160) => {
|
|
173
|
+
const normalized = clean(value);
|
|
174
|
+
if (!normalized) return "";
|
|
175
|
+
if (normalized.length <= max) return normalized;
|
|
176
|
+
return `${normalized.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const buildContactEntries = (
|
|
180
|
+
basics: ResumeData["basics"],
|
|
181
|
+
locationLabel?: string,
|
|
182
|
+
): ResumeContactEntry[] => {
|
|
183
|
+
const seen = new Set<string>();
|
|
184
|
+
const entries: ResumeContactEntry[] = [];
|
|
185
|
+
|
|
186
|
+
const pushEntry = (entry: ResumeContactEntry, key: string) => {
|
|
187
|
+
if (!entry.label || !key || seen.has(key)) return;
|
|
188
|
+
seen.add(key);
|
|
189
|
+
entries.push(entry);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const normalizedLocation = clean(locationLabel);
|
|
193
|
+
if (normalizedLocation) {
|
|
194
|
+
pushEntry(
|
|
195
|
+
{ kind: "location", label: normalizedLocation },
|
|
196
|
+
`location:${normalizedLocation.toLowerCase()}`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const email = normalizeEmail(basics.contact.email);
|
|
201
|
+
if (email) {
|
|
202
|
+
pushEntry({ kind: "email", label: email, href: `mailto:${email}` }, `email:${email}`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const phoneLabel = clean(basics.contact.phone);
|
|
206
|
+
const phone = normalizePhone(basics.contact.phone);
|
|
207
|
+
if (phoneLabel && phone) {
|
|
208
|
+
pushEntry({ kind: "phone", label: phoneLabel, href: `tel:${phone}` }, `phone:${phone}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const websiteHref = normalizeUrlHref(basics.contact.website);
|
|
212
|
+
if (websiteHref) {
|
|
213
|
+
const websiteKey = canonicalUrlKey(websiteHref);
|
|
214
|
+
pushEntry(
|
|
215
|
+
{
|
|
216
|
+
kind: "website",
|
|
217
|
+
label: formatUrlLabel(websiteHref),
|
|
218
|
+
href: websiteHref,
|
|
219
|
+
},
|
|
220
|
+
`url:${websiteKey}`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
for (const link of basics.links ?? []) {
|
|
225
|
+
const rawLabel = clean(link.label);
|
|
226
|
+
const href = normalizeUrlHref(link.url);
|
|
227
|
+
|
|
228
|
+
if (/^mailto:/i.test(href)) {
|
|
229
|
+
const linkedEmail = normalizeEmail(href.replace(/^mailto:/i, ""));
|
|
230
|
+
if (linkedEmail) {
|
|
231
|
+
pushEntry({ kind: "email", label: linkedEmail, href: `mailto:${linkedEmail}` }, `email:${linkedEmail}`);
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (/^tel:/i.test(href)) {
|
|
237
|
+
const linkedPhone = normalizePhone(href.replace(/^tel:/i, ""));
|
|
238
|
+
if (linkedPhone) {
|
|
239
|
+
pushEntry(
|
|
240
|
+
{ kind: "phone", label: linkedPhone, href: `tel:${linkedPhone}` },
|
|
241
|
+
`phone:${linkedPhone}`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (href) {
|
|
248
|
+
const key = canonicalUrlKey(href);
|
|
249
|
+
pushEntry(
|
|
250
|
+
{
|
|
251
|
+
kind: "link",
|
|
252
|
+
label: rawLabel || formatUrlLabel(href),
|
|
253
|
+
href,
|
|
254
|
+
},
|
|
255
|
+
`url:${key}`,
|
|
256
|
+
);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (rawLabel) {
|
|
261
|
+
pushEntry({ kind: "link", label: rawLabel }, `text:${rawLabel.toLowerCase()}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return entries;
|
|
266
|
+
};
|