@getvitops/astro 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/index.d.mts +2 -120
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/index.mjs +3 -181
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +30 -9
  6. package/src/components/Cards.astro +32 -0
  7. package/src/components/Details.astro +76 -0
  8. package/src/components/Drawer.astro +65 -0
  9. package/src/components/NodeRenderer.astro +27 -0
  10. package/src/components/Popover.astro +90 -0
  11. package/src/components/Subgrid.astro +32 -0
  12. package/src/components/WebComponentLoader.astro +48 -0
  13. package/src/schemas/Article.astro +69 -0
  14. package/src/schemas/BookActions.astro +81 -0
  15. package/src/schemas/Breadcrumb.astro +26 -0
  16. package/src/schemas/Carousel.astro +34 -0
  17. package/src/schemas/CourseList.astro +122 -0
  18. package/src/schemas/Dataset.astro +127 -0
  19. package/src/schemas/DiscussionForum.astro +82 -0
  20. package/src/schemas/EducationQA.astro +63 -0
  21. package/src/schemas/EmployerAggregateRating.astro +54 -0
  22. package/src/schemas/Event.astro +115 -0
  23. package/src/schemas/FAQ.astro +34 -0
  24. package/src/schemas/FactCheck.astro +84 -0
  25. package/src/schemas/ImageMetadata.astro +56 -0
  26. package/src/schemas/JobPosting.astro +138 -0
  27. package/src/schemas/LocalBusiness.astro +155 -0
  28. package/src/schemas/MathSolver.astro +54 -0
  29. package/src/schemas/MovieCarousel.astro +92 -0
  30. package/src/schemas/Organization.astro +140 -0
  31. package/src/schemas/Product.astro +250 -0
  32. package/src/schemas/ProfilePage.astro +147 -0
  33. package/src/schemas/QAPage.astro +71 -0
  34. package/src/schemas/Recipe.astro +178 -0
  35. package/src/schemas/Review.astro +138 -0
  36. package/src/schemas/SoftwareApp.astro +172 -0
  37. /package/{Head.astro → src/components/Head.astro} +0 -0
@@ -0,0 +1,27 @@
1
+ ---
2
+ import { nodesToHtml } from '@getvitops/utils';
3
+ import { t } from '@getvitops/utils';
4
+ import type { ContentNode, Localizable, Attrs } from '@getvitops/utils';
5
+
6
+ interface Props {
7
+ nodes: ContentNode[];
8
+ attrs?: Attrs;
9
+ locale?: string;
10
+ defaultLocale?: string;
11
+ }
12
+
13
+ const {
14
+ nodes,
15
+ attrs,
16
+ locale = 'en',
17
+ defaultLocale = 'en',
18
+ } = Astro.props;
19
+
20
+ const resolveText = (value: Localizable) => t(value, locale, defaultLocale);
21
+ const htmlString = nodesToHtml(nodes, resolveText);
22
+ ---
23
+
24
+ {attrs
25
+ ? <div {...attrs} set:html={htmlString} />
26
+ : <Fragment set:html={htmlString} />
27
+ }
@@ -0,0 +1,90 @@
1
+ ---
2
+ import { Icon } from 'astro-icon/components';
3
+
4
+ interface Props {
5
+ id: string;
6
+ trigger?: {
7
+ label: string;
8
+ icon?: string;
9
+ class?: string;
10
+ attrs?: Record<string, string | number | boolean>;
11
+ };
12
+ type?: 'auto' | 'manual';
13
+ as?: 'div' | 'dialog';
14
+ class?: string;
15
+ anchorName?: string;
16
+ positionArea?: string;
17
+ /** Resolve semantic icon names to fully-qualified astro-icon names.
18
+ * Defaults to pass-through (expects already-qualified names like 'fa7-solid:bars'). */
19
+ iconResolver?: (name: string) => string;
20
+ }
21
+
22
+ const {
23
+ id,
24
+ trigger,
25
+ type = 'auto',
26
+ as: Element = 'div',
27
+ class: className,
28
+ anchorName,
29
+ positionArea = 'block-end center',
30
+ iconResolver = (name: string) => name,
31
+ } = Astro.props;
32
+
33
+ const styles = anchorName
34
+ ? `position-anchor: --${anchorName}; position-area: ${positionArea}`
35
+ : undefined;
36
+
37
+ const anchorStyles = anchorName ? `anchor-name: --${anchorName}` : undefined;
38
+
39
+ const { class: triggerAttrClass, ...triggerRestAttrs } = trigger?.attrs ?? {};
40
+ ---
41
+
42
+ {/* Trigger button - slot with default fallback */}
43
+ <slot name="trigger">
44
+ <button
45
+ popovertarget={id}
46
+ class:list={['popover-trigger', trigger?.class, triggerAttrClass]}
47
+ style={anchorStyles}
48
+ aria-label={trigger?.label || 'Open'}
49
+ {...triggerRestAttrs}
50
+ >
51
+ <span class={trigger?.icon ? 'sr-only' : ''}>{trigger?.label || 'Open'}</span>
52
+ {trigger?.icon && <Icon name={iconResolver(trigger.icon)} class="not-sr-only" />}
53
+ </button>
54
+ </slot>
55
+
56
+
57
+ <Element
58
+ id={id}
59
+ popover={type}
60
+ class:list={["popover", className]}
61
+ style={styles}
62
+ >
63
+ <slot />
64
+ </Element>
65
+
66
+ <style>
67
+ .popover {
68
+ margin: 0;
69
+ inset: auto;
70
+ border: none;
71
+ padding: 0;
72
+ }
73
+
74
+ .popover[popover]::backdrop {
75
+ background: rgba(0, 0, 0, 0.5);
76
+ }
77
+
78
+ .popover-trigger {
79
+ display: inline-flex;
80
+ align-items: center;
81
+ gap: 0.5rem;
82
+ background: none;
83
+ border: none;
84
+ cursor: pointer;
85
+ padding: 0;
86
+ font-size: inherit;
87
+ font-family: inherit;
88
+ color: inherit;
89
+ }
90
+ </style>
@@ -0,0 +1,32 @@
1
+ ---
2
+ import type { HTMLAttributes } from 'astro/types';
3
+ import { parseRenderedSlots, getAttribute, serialize, styleList } from '@getvitops/utils';
4
+
5
+ export interface Props extends HTMLAttributes<'ul'> {};
6
+
7
+ const { class: className, style } = Astro.props;
8
+
9
+ const nodes = parseRenderedSlots(await Astro.slots.render('default'));
10
+ const slots = nodes.filter(node => 'tagName' in node).map(node => ({ html: serialize(node), attrs: node.attrs }));
11
+ ---
12
+
13
+ <style>
14
+ ul {
15
+ grid-template-columns: repeat(var(--cols, var(--_cols, 1)), minmax(0, 1fr));
16
+ }
17
+
18
+ li {
19
+ --current-row: round(down, calc((sibling-index() - 1) / var(--cols)), 1);
20
+
21
+ /* Apply the extra gap ONLY if the row index is > 0.
22
+ We use clamp to ensure the multiplier is either 0 or 1.
23
+ */
24
+ --row-gap-multiplier: clamp(0, var(--current-row), 1);
25
+ margin-block-start: calc(var(--row-gap-multiplier) * var(--row-margin));
26
+ }
27
+ </style>
28
+ <ul class:list={["grid", className]} style={styleList({'--num-items': slots.length}, style)}>
29
+ {slots.map((slot) => (
30
+ <li class:list={["grid grid-rows-subgrid row-span-(--row-span)", getAttribute(slot, 'class')]} style={styleList(getAttribute(slot, 'style'))} set:html={slot.html}></li>
31
+ ))}
32
+ </ul>
@@ -0,0 +1,48 @@
1
+ ---
2
+ /**
3
+ * Generic loader for web components.
4
+ * Uses import.meta.glob to include all WC*.ts files in the dependency graph,
5
+ * then dynamically loads the requested one on the client.
6
+ *
7
+ * @example
8
+ * <WebComponentLoader src="WCColorSchemeToggle" />
9
+ * <color-scheme-toggle></color-scheme-toggle>
10
+ */
11
+ interface Props {
12
+ src: string;
13
+ }
14
+
15
+ const { src } = Astro.props;
16
+ ---
17
+
18
+ <wc-loader data-src={src}></wc-loader>
19
+
20
+ <script>
21
+ // All web components matching this pattern are included in the build
22
+ const modules = import.meta.glob<{ default?: unknown }>(
23
+ '/src/components/web-components/WC*.ts'
24
+ );
25
+
26
+ // Load requested modules
27
+ document.querySelectorAll<HTMLElement>('wc-loader[data-src]').forEach(async (el) => {
28
+ const name = el.dataset.src;
29
+ if (!name) return;
30
+
31
+ // Build the full path from the component name
32
+ const path = `/src/components/web-components/${name}.ts`;
33
+ const loader = modules[path];
34
+
35
+ if (loader) {
36
+ await loader();
37
+ el.remove(); // Clean up the loader element
38
+ } else {
39
+ console.warn(`[WebComponentLoader] Module not found: ${path}`);
40
+ }
41
+ });
42
+ </script>
43
+
44
+ <style>
45
+ wc-loader {
46
+ display: none;
47
+ }
48
+ </style>
@@ -0,0 +1,69 @@
1
+ ---
2
+ export interface Props {
3
+ headline: string;
4
+ description?: string;
5
+ image?: string | string[];
6
+ datePublished: string;
7
+ dateModified?: string;
8
+ author: {
9
+ name: string;
10
+ url?: string;
11
+ } | {
12
+ name: string;
13
+ url?: string;
14
+ }[];
15
+ publisher?: {
16
+ name: string;
17
+ logo?: string;
18
+ };
19
+ mainEntityOfPage?: string;
20
+ articleSection?: string;
21
+ wordCount?: number;
22
+ keywords?: string[];
23
+ }
24
+
25
+ const {
26
+ headline,
27
+ description,
28
+ image,
29
+ datePublished,
30
+ dateModified,
31
+ author,
32
+ publisher,
33
+ mainEntityOfPage,
34
+ articleSection,
35
+ wordCount,
36
+ keywords,
37
+ } = Astro.props;
38
+
39
+ const authors = Array.isArray(author) ? author : [author];
40
+
41
+ const jsonLd = {
42
+ '@context': 'https://schema.org',
43
+ '@type': 'Article',
44
+ headline,
45
+ ...(description && { description }),
46
+ ...(image && { image: Array.isArray(image) ? image : [image] }),
47
+ datePublished,
48
+ ...(dateModified && { dateModified }),
49
+ author: authors.map(a => ({
50
+ '@type': 'Person',
51
+ name: a.name,
52
+ ...(a.url && { url: a.url }),
53
+ })),
54
+ ...(publisher && {
55
+ publisher: {
56
+ '@type': 'Organization',
57
+ name: publisher.name,
58
+ ...(publisher.logo && { logo: { '@type': 'ImageObject', url: publisher.logo } }),
59
+ },
60
+ }),
61
+ ...(mainEntityOfPage && { mainEntityOfPage: { '@type': 'WebPage', '@id': mainEntityOfPage } }),
62
+ ...(articleSection && { articleSection }),
63
+ ...(wordCount && { wordCount }),
64
+ ...(keywords && { keywords: keywords.join(', ') }),
65
+ };
66
+ ---
67
+
68
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
69
+ <slot />
@@ -0,0 +1,81 @@
1
+ ---
2
+ export interface Props {
3
+ name: string;
4
+ author: string | string[];
5
+ url: string;
6
+ workExample?: {
7
+ isbn?: string;
8
+ bookEdition?: string;
9
+ bookFormat?: 'AudiobookFormat' | 'EBook' | 'GraphicNovel' | 'Hardcover' | 'Paperback';
10
+ potentialAction?: {
11
+ type: 'ReadAction' | 'BorrowAction' | 'BuyAction';
12
+ target: string;
13
+ expectsAcceptanceOf?: {
14
+ availabilityStarts?: string;
15
+ availabilityEnds?: string;
16
+ category?: 'free' | 'subscription' | 'purchase' | 'rental';
17
+ };
18
+ };
19
+ }[];
20
+ sameAs?: string[];
21
+ image?: string;
22
+ description?: string;
23
+ }
24
+
25
+ const {
26
+ name,
27
+ author,
28
+ url,
29
+ workExample,
30
+ sameAs,
31
+ image,
32
+ description,
33
+ } = Astro.props;
34
+
35
+ const authors = Array.isArray(author) ? author : [author];
36
+
37
+ const jsonLd = {
38
+ '@context': 'https://schema.org',
39
+ '@type': 'Book',
40
+ name,
41
+ author: authors.map(a => ({
42
+ '@type': 'Person',
43
+ name: a,
44
+ })),
45
+ url,
46
+ ...(sameAs && { sameAs }),
47
+ ...(image && { image }),
48
+ ...(description && { description }),
49
+ ...(workExample && {
50
+ workExample: workExample.map(we => ({
51
+ '@type': 'Book',
52
+ ...(we.isbn && { isbn: we.isbn }),
53
+ ...(we.bookEdition && { bookEdition: we.bookEdition }),
54
+ ...(we.bookFormat && { bookFormat: `https://schema.org/${we.bookFormat}` }),
55
+ ...(we.potentialAction && {
56
+ potentialAction: {
57
+ '@type': we.potentialAction.type,
58
+ target: {
59
+ '@type': 'EntryPoint',
60
+ urlTemplate: we.potentialAction.target,
61
+ actionPlatform: [
62
+ 'https://schema.org/DesktopWebPlatform',
63
+ 'https://schema.org/AndroidPlatform',
64
+ 'https://schema.org/IOSPlatform',
65
+ ],
66
+ },
67
+ ...(we.potentialAction.expectsAcceptanceOf && {
68
+ expectsAcceptanceOf: {
69
+ '@type': 'Offer',
70
+ ...we.potentialAction.expectsAcceptanceOf,
71
+ },
72
+ }),
73
+ },
74
+ }),
75
+ })),
76
+ }),
77
+ };
78
+ ---
79
+
80
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
81
+ <slot />
@@ -0,0 +1,26 @@
1
+ ---
2
+ export interface BreadcrumbItem {
3
+ name: string;
4
+ url: string;
5
+ }
6
+
7
+ export interface Props {
8
+ items: BreadcrumbItem[];
9
+ }
10
+
11
+ const { items } = Astro.props;
12
+
13
+ const jsonLd = {
14
+ '@context': 'https://schema.org',
15
+ '@type': 'BreadcrumbList',
16
+ itemListElement: items.map((item, index) => ({
17
+ '@type': 'ListItem',
18
+ position: index + 1,
19
+ name: item.name,
20
+ item: item.url,
21
+ })),
22
+ };
23
+ ---
24
+
25
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
26
+ <slot />
@@ -0,0 +1,34 @@
1
+ ---
2
+ export interface CarouselItem {
3
+ type: 'Article' | 'Recipe' | 'Movie' | 'Course' | 'Restaurant' | 'Product';
4
+ name: string;
5
+ url: string;
6
+ image?: string;
7
+ description?: string;
8
+ }
9
+
10
+ export interface Props {
11
+ items: CarouselItem[];
12
+ }
13
+
14
+ const { items } = Astro.props;
15
+
16
+ const jsonLd = {
17
+ '@context': 'https://schema.org',
18
+ '@type': 'ItemList',
19
+ itemListElement: items.map((item, index) => ({
20
+ '@type': 'ListItem',
21
+ position: index + 1,
22
+ item: {
23
+ '@type': item.type,
24
+ name: item.name,
25
+ url: item.url,
26
+ ...(item.image && { image: item.image }),
27
+ ...(item.description && { description: item.description }),
28
+ },
29
+ })),
30
+ };
31
+ ---
32
+
33
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
34
+ <slot />
@@ -0,0 +1,122 @@
1
+ ---
2
+ export interface CourseInstance {
3
+ courseMode: 'online' | 'onsite' | 'blended';
4
+ startDate?: string;
5
+ endDate?: string;
6
+ location?: {
7
+ name: string;
8
+ address?: string;
9
+ };
10
+ instructor?: {
11
+ name: string;
12
+ url?: string;
13
+ };
14
+ courseSchedule?: {
15
+ repeatFrequency: string;
16
+ repeatCount?: number;
17
+ };
18
+ }
19
+
20
+ export interface Course {
21
+ name: string;
22
+ description: string;
23
+ provider: {
24
+ name: string;
25
+ url?: string;
26
+ };
27
+ url?: string;
28
+ image?: string;
29
+ offers?: {
30
+ price: number | string;
31
+ priceCurrency: string;
32
+ availability?: 'InStock' | 'SoldOut' | 'PreOrder';
33
+ validFrom?: string;
34
+ };
35
+ hasCourseInstance?: CourseInstance[];
36
+ educationalLevel?: string;
37
+ about?: string[];
38
+ teaches?: string[];
39
+ financialAidEligible?: boolean;
40
+ inLanguage?: string;
41
+ duration?: string;
42
+ }
43
+
44
+ export interface Props {
45
+ courses: Course[];
46
+ }
47
+
48
+ const { courses } = Astro.props;
49
+
50
+ const jsonLd = {
51
+ '@context': 'https://schema.org',
52
+ '@type': 'ItemList',
53
+ itemListElement: courses.map((course, index) => ({
54
+ '@type': 'ListItem',
55
+ position: index + 1,
56
+ item: {
57
+ '@type': 'Course',
58
+ name: course.name,
59
+ description: course.description,
60
+ provider: {
61
+ '@type': 'Organization',
62
+ name: course.provider.name,
63
+ ...(course.provider.url && { sameAs: course.provider.url }),
64
+ },
65
+ ...(course.url && { url: course.url }),
66
+ ...(course.image && { image: course.image }),
67
+ ...(course.offers && {
68
+ offers: {
69
+ '@type': 'Offer',
70
+ price: course.offers.price,
71
+ priceCurrency: course.offers.priceCurrency,
72
+ ...(course.offers.availability && { availability: `https://schema.org/${course.offers.availability}` }),
73
+ ...(course.offers.validFrom && { validFrom: course.offers.validFrom }),
74
+ },
75
+ }),
76
+ ...(course.hasCourseInstance && {
77
+ hasCourseInstance: course.hasCourseInstance.map(instance => ({
78
+ '@type': 'CourseInstance',
79
+ courseMode: instance.courseMode,
80
+ ...(instance.startDate && { startDate: instance.startDate }),
81
+ ...(instance.endDate && { endDate: instance.endDate }),
82
+ ...(instance.location && {
83
+ location: {
84
+ '@type': 'Place',
85
+ name: instance.location.name,
86
+ ...(instance.location.address && {
87
+ address: {
88
+ '@type': 'PostalAddress',
89
+ streetAddress: instance.location.address,
90
+ },
91
+ }),
92
+ },
93
+ }),
94
+ ...(instance.instructor && {
95
+ instructor: {
96
+ '@type': 'Person',
97
+ name: instance.instructor.name,
98
+ ...(instance.instructor.url && { url: instance.instructor.url }),
99
+ },
100
+ }),
101
+ ...(instance.courseSchedule && {
102
+ courseSchedule: {
103
+ '@type': 'Schedule',
104
+ repeatFrequency: instance.courseSchedule.repeatFrequency,
105
+ ...(instance.courseSchedule.repeatCount && { repeatCount: instance.courseSchedule.repeatCount }),
106
+ },
107
+ }),
108
+ })),
109
+ }),
110
+ ...(course.educationalLevel && { educationalLevel: course.educationalLevel }),
111
+ ...(course.about && { about: course.about }),
112
+ ...(course.teaches && { teaches: course.teaches }),
113
+ ...(course.financialAidEligible && { financialAidEligible: 'true' }),
114
+ ...(course.inLanguage && { inLanguage: course.inLanguage }),
115
+ ...(course.duration && { timeRequired: course.duration }),
116
+ },
117
+ })),
118
+ };
119
+ ---
120
+
121
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
122
+ <slot />
@@ -0,0 +1,127 @@
1
+ ---
2
+ export interface Props {
3
+ name: string;
4
+ description: string;
5
+ url?: string;
6
+ sameAs?: string;
7
+ identifier?: string | string[];
8
+ keywords?: string[];
9
+ license?: string;
10
+ isAccessibleForFree?: boolean;
11
+ creator?: {
12
+ type: 'Person' | 'Organization';
13
+ name: string;
14
+ url?: string;
15
+ };
16
+ funder?: {
17
+ type: 'Person' | 'Organization';
18
+ name: string;
19
+ };
20
+ includedInDataCatalog?: {
21
+ name: string;
22
+ url?: string;
23
+ };
24
+ distribution?: {
25
+ contentUrl: string;
26
+ encodingFormat: string;
27
+ contentSize?: string;
28
+ }[];
29
+ temporalCoverage?: string;
30
+ spatialCoverage?: {
31
+ type: 'Place';
32
+ geo?: {
33
+ latitude: number;
34
+ longitude: number;
35
+ box?: string;
36
+ };
37
+ };
38
+ variableMeasured?: string | string[];
39
+ measurementTechnique?: string;
40
+ datePublished?: string;
41
+ dateModified?: string;
42
+ }
43
+
44
+ const {
45
+ name,
46
+ description,
47
+ url,
48
+ sameAs,
49
+ identifier,
50
+ keywords,
51
+ license,
52
+ isAccessibleForFree,
53
+ creator,
54
+ funder,
55
+ includedInDataCatalog,
56
+ distribution,
57
+ temporalCoverage,
58
+ spatialCoverage,
59
+ variableMeasured,
60
+ measurementTechnique,
61
+ datePublished,
62
+ dateModified,
63
+ } = Astro.props;
64
+
65
+ const jsonLd = {
66
+ '@context': 'https://schema.org',
67
+ '@type': 'Dataset',
68
+ name,
69
+ description,
70
+ ...(url && { url }),
71
+ ...(sameAs && { sameAs }),
72
+ ...(identifier && { identifier }),
73
+ ...(keywords && { keywords }),
74
+ ...(license && { license }),
75
+ ...(isAccessibleForFree !== undefined && { isAccessibleForFree }),
76
+ ...(creator && {
77
+ creator: {
78
+ '@type': creator.type,
79
+ name: creator.name,
80
+ ...(creator.url && { url: creator.url }),
81
+ },
82
+ }),
83
+ ...(funder && {
84
+ funder: {
85
+ '@type': funder.type,
86
+ name: funder.name,
87
+ },
88
+ }),
89
+ ...(includedInDataCatalog && {
90
+ includedInDataCatalog: {
91
+ '@type': 'DataCatalog',
92
+ name: includedInDataCatalog.name,
93
+ ...(includedInDataCatalog.url && { url: includedInDataCatalog.url }),
94
+ },
95
+ }),
96
+ ...(distribution && {
97
+ distribution: distribution.map(d => ({
98
+ '@type': 'DataDownload',
99
+ contentUrl: d.contentUrl,
100
+ encodingFormat: d.encodingFormat,
101
+ ...(d.contentSize && { contentSize: d.contentSize }),
102
+ })),
103
+ }),
104
+ ...(temporalCoverage && { temporalCoverage }),
105
+ ...(spatialCoverage && {
106
+ spatialCoverage: {
107
+ '@type': 'Place',
108
+ ...(spatialCoverage.geo && {
109
+ geo: spatialCoverage.geo.box
110
+ ? { '@type': 'GeoShape', box: spatialCoverage.geo.box }
111
+ : {
112
+ '@type': 'GeoCoordinates',
113
+ latitude: spatialCoverage.geo.latitude,
114
+ longitude: spatialCoverage.geo.longitude,
115
+ },
116
+ }),
117
+ },
118
+ }),
119
+ ...(variableMeasured && { variableMeasured }),
120
+ ...(measurementTechnique && { measurementTechnique }),
121
+ ...(datePublished && { datePublished }),
122
+ ...(dateModified && { dateModified }),
123
+ };
124
+ ---
125
+
126
+ <script is:inline type="application/ld+json" set:html={JSON.stringify(jsonLd)} />
127
+ <slot />