summaryrefslogtreecommitdiff
path: root/src/components/ui
diff options
context:
space:
mode:
authorDawid Rycerz <dawid@rycerz.xyz>2025-07-21 21:56:55 +0300
committerDawid Rycerz <dawid@rycerz.xyz>2025-07-21 21:56:55 +0300
commitc735556726e75428550a3d28a2cf58e2c8490b7d (patch)
treefd0ae29d1636b825abeedff6b99d3376bb383135 /src/components/ui
Initial template
Diffstat (limited to 'src/components/ui')
-rw-r--r--src/components/ui/Background.astro11
-rw-r--r--src/components/ui/Button.astro40
-rw-r--r--src/components/ui/DListItem.astro22
-rw-r--r--src/components/ui/Form.astro87
-rw-r--r--src/components/ui/Headline.astro35
-rw-r--r--src/components/ui/ItemGrid.astro65
-rw-r--r--src/components/ui/ItemGrid2.astro59
-rw-r--r--src/components/ui/Timeline.astro60
-rw-r--r--src/components/ui/WidgetWrapper.astro34
9 files changed, 413 insertions, 0 deletions
diff --git a/src/components/ui/Background.astro b/src/components/ui/Background.astro
new file mode 100644
index 0000000..1093c17
--- /dev/null
+++ b/src/components/ui/Background.astro
@@ -0,0 +1,11 @@
+---
+export interface Props {
+ isDark?: boolean;
+}
+
+const { isDark = false } = Astro.props;
+---
+
+<div class:list={['absolute inset-0', { 'bg-dark dark:bg-transparent': isDark }]}>
+ <slot />
+</div>
diff --git a/src/components/ui/Button.astro b/src/components/ui/Button.astro
new file mode 100644
index 0000000..d3c2398
--- /dev/null
+++ b/src/components/ui/Button.astro
@@ -0,0 +1,40 @@
+---
+import { Icon } from 'astro-icon/components';
+import { twMerge } from 'tailwind-merge';
+import type { CallToAction as Props } from '~/types';
+
+const {
+ variant = 'secondary',
+ target,
+ text = Astro.slots.render('default'),
+ icon = '',
+ class: className = '',
+ type,
+ ...rest
+} = Astro.props;
+
+const variants = {
+ primary: 'btn-primary',
+ secondary: 'btn-secondary',
+ tertiary: 'btn btn-tertiary',
+ link: 'cursor-pointer hover:text-primary',
+};
+---
+
+{
+ type === 'button' || type === 'submit' || type === 'reset' ? (
+ <button type={type} class={twMerge(variants[variant] || '', className)} {...rest}>
+ <Fragment set:html={text} />
+ {icon && <Icon name={icon} class="w-5 h-5 ml-1 -mr-1.5 rtl:mr-1 rtl:-ml-1.5 inline-block" />}
+ </button>
+ ) : (
+ <a
+ class={twMerge(variants[variant] || '', className)}
+ {...(target ? { target: target, rel: 'noopener noreferrer' } : {})}
+ {...rest}
+ >
+ <Fragment set:html={text} />
+ {icon && <Icon name={icon} class="w-5 h-5 ml-1 -mr-1.5 rtl:mr-1 rtl:-ml-1.5 inline-block" />}
+ </a>
+ )
+}
diff --git a/src/components/ui/DListItem.astro b/src/components/ui/DListItem.astro
new file mode 100644
index 0000000..36d4072
--- /dev/null
+++ b/src/components/ui/DListItem.astro
@@ -0,0 +1,22 @@
+---
+// component: DListItem
+//
+// Mimics the html 'dl' (description list)
+//
+// The 'dt' item is the item 'term' and is inserted into an 'h6' tag.
+// Caller needs to style the 'h6' tag appropriately.
+//
+// You can put pretty much any content you want between the open and
+// closing tags - it's simply contained in an enclosing div with a
+// margin left. No need for 'dd' items.
+//
+const { dt } = Astro.props;
+interface Props {
+ dt: string;
+}
+
+const content: string = await Astro.slots.render('default');
+---
+
+<h6 set:html={dt} />
+<div class="dd ml-8" set:html={content} />
diff --git a/src/components/ui/Form.astro b/src/components/ui/Form.astro
new file mode 100644
index 0000000..276b39f
--- /dev/null
+++ b/src/components/ui/Form.astro
@@ -0,0 +1,87 @@
+---
+import type { Form as Props } from '~/types';
+import Button from '~/components/ui/Button.astro';
+
+const { inputs, textarea, disclaimer, button = 'Contact us', description = '' } = Astro.props;
+---
+
+<form>
+ {
+ inputs &&
+ inputs.map(
+ ({ type = 'text', name, label = '', autocomplete = 'on', placeholder = '' }) =>
+ name && (
+ <div class="mb-6">
+ {label && (
+ <label for={name} class="block text-sm font-medium">
+ {label}
+ </label>
+ )}
+ <input
+ type={type}
+ name={name}
+ id={name}
+ autocomplete={autocomplete}
+ placeholder={placeholder}
+ class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
+ />
+ </div>
+ )
+ )
+ }
+
+ {
+ textarea && (
+ <div>
+ <label for="textarea" class="block text-sm font-medium">
+ {textarea.label}
+ </label>
+ <textarea
+ id="textarea"
+ name={textarea.name ? textarea.name : 'message'}
+ rows={textarea.rows ? textarea.rows : 4}
+ placeholder={textarea.placeholder}
+ class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
+ />
+ </div>
+ )
+ }
+
+ {
+ disclaimer && (
+ <div class="mt-3 flex items-start">
+ <div class="flex mt-0.5">
+ <input
+ id="disclaimer"
+ name="disclaimer"
+ type="checkbox"
+ class="cursor-pointer mt-1 py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
+ />
+ </div>
+ <div class="ml-3">
+ <label for="disclaimer" class="cursor-pointer select-none text-sm text-gray-600 dark:text-gray-400">
+ {disclaimer.label}
+ </label>
+ </div>
+ </div>
+ )
+ }
+
+ {
+ button && (
+ <div class="mt-10 grid">
+ <Button variant="primary" type="submit">
+ {button}
+ </Button>
+ </div>
+ )
+ }
+
+ {
+ description && (
+ <div class="mt-3 text-center">
+ <p class="text-sm text-gray-600 dark:text-gray-400">{description}</p>
+ </div>
+ )
+ }
+</form>
diff --git a/src/components/ui/Headline.astro b/src/components/ui/Headline.astro
new file mode 100644
index 0000000..621b23d
--- /dev/null
+++ b/src/components/ui/Headline.astro
@@ -0,0 +1,35 @@
+---
+import type { Headline as Props } from '~/types';
+import { twMerge } from 'tailwind-merge';
+
+const {
+ title = await Astro.slots.render('title'),
+ subtitle = await Astro.slots.render('subtitle'),
+ tagline,
+ classes = {},
+} = Astro.props;
+
+const {
+ container: containerClass = 'max-w-3xl',
+ title: titleClass = 'text-3xl md:text-4xl ',
+ subtitle: subtitleClass = 'text-xl',
+} = classes;
+---
+
+{
+ (title || subtitle || tagline) && (
+ <div class={twMerge('mb-8 md:mx-auto md:mb-12 text-center', containerClass)}>
+ {tagline && (
+ <p class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase" set:html={tagline} />
+ )}
+ {title && (
+ <h2
+ class={twMerge('font-bold leading-tighter tracking-tighter font-heading text-heading text-3xl', titleClass)}
+ set:html={title}
+ />
+ )}
+
+ {subtitle && <p class={twMerge('mt-4 text-muted', subtitleClass)} set:html={subtitle} />}
+ </div>
+ )
+}
diff --git a/src/components/ui/ItemGrid.astro b/src/components/ui/ItemGrid.astro
new file mode 100644
index 0000000..79931b9
--- /dev/null
+++ b/src/components/ui/ItemGrid.astro
@@ -0,0 +1,65 @@
+---
+import type { ItemGrid as Props } from '~/types';
+import { twMerge } from 'tailwind-merge';
+import Button from './Button.astro';
+import { Icon } from 'astro-icon/components';
+
+const { items = [], columns, defaultIcon = '', classes = {} } = Astro.props;
+
+const {
+ container: containerClass = '',
+ panel: panelClass = '',
+ title: titleClass = '',
+ description: descriptionClass = '',
+ icon: defaultIconClass = 'text-primary',
+ action: actionClass = '',
+} = classes;
+---
+
+{
+ items && items.length > 0 && (
+ <div
+ class={twMerge(
+ `grid mx-auto gap-8 md:gap-y-12 ${
+ columns === 4
+ ? 'lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2'
+ : columns === 3
+ ? 'lg:grid-cols-3 sm:grid-cols-2'
+ : columns === 2
+ ? 'sm:grid-cols-2 '
+ : ''
+ }`,
+ containerClass
+ )}
+ >
+ {items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
+ <div class="intersect-once motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade">
+ <div class={twMerge('flex flex-row max-w-md', panelClass, itemClasses?.panel)}>
+ <div class="flex justify-center">
+ {(icon || defaultIcon) && (
+ <Icon
+ name={icon || defaultIcon}
+ class={twMerge('w-7 h-7 mr-2 rtl:mr-0 rtl:ml-2', defaultIconClass, itemClasses?.icon)}
+ />
+ )}
+ </div>
+ <div class="mt-0.5">
+ {title && <h3 class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)}>{title}</h3>}
+ {description && (
+ <p
+ class={twMerge(`${title ? 'mt-3' : ''} text-muted`, descriptionClass, itemClasses?.description)}
+ set:html={description}
+ />
+ )}
+ {callToAction && (
+ <div class={twMerge(`${title || description ? 'mt-3' : ''}`, actionClass, itemClasses?.actionClass)}>
+ <Button variant="link" {...callToAction} />
+ </div>
+ )}
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+ )
+}
diff --git a/src/components/ui/ItemGrid2.astro b/src/components/ui/ItemGrid2.astro
new file mode 100644
index 0000000..81faadf
--- /dev/null
+++ b/src/components/ui/ItemGrid2.astro
@@ -0,0 +1,59 @@
+---
+import type { ItemGrid as Props } from '~/types';
+import { Icon } from 'astro-icon/components';
+import { twMerge } from 'tailwind-merge';
+import Button from './Button.astro';
+
+const { items = [], columns, defaultIcon = '', classes = {} } = Astro.props;
+
+const {
+ container: containerClass = '',
+ // container: containerClass = "sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
+ panel: panelClass = '',
+ title: titleClass = '',
+ description: descriptionClass = '',
+ icon: defaultIconClass = 'text-primary',
+} = classes;
+---
+
+{
+ items && items.length > 0 && (
+ <div
+ class={twMerge(
+ `grid gap-8 gap-x-12 sm:gap-y-8 ${
+ columns === 4
+ ? 'lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2'
+ : columns === 3
+ ? 'lg:grid-cols-3 sm:grid-cols-2'
+ : columns === 2
+ ? 'sm:grid-cols-2 '
+ : ''
+ }`,
+ containerClass
+ )}
+ >
+ {items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
+ <div
+ class={twMerge(
+ 'relative flex flex-col intersect-once intersect-quarter intercept-no-queue motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
+ panelClass,
+ itemClasses?.panel
+ )}
+ >
+ {(icon || defaultIcon) && (
+ <Icon name={icon || defaultIcon} class={twMerge('mb-2 w-10 h-10', defaultIconClass, itemClasses?.icon)} />
+ )}
+ <div class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)}>{title}</div>
+ {description && (
+ <p class={twMerge('text-muted mt-2', descriptionClass, itemClasses?.description)} set:html={description} />
+ )}
+ {callToAction && (
+ <div class="mt-2">
+ <Button {...callToAction} />
+ </div>
+ )}
+ </div>
+ ))}
+ </div>
+ )
+}
diff --git a/src/components/ui/Timeline.astro b/src/components/ui/Timeline.astro
new file mode 100644
index 0000000..51e09f0
--- /dev/null
+++ b/src/components/ui/Timeline.astro
@@ -0,0 +1,60 @@
+---
+import { Icon } from 'astro-icon/components';
+import { twMerge } from 'tailwind-merge';
+import type { Item } from '~/types';
+
+export interface Props {
+ items?: Array<Item>;
+ defaultIcon?: string;
+ classes?: Record<string, string>;
+}
+
+const { items = [], classes = {}, defaultIcon } = Astro.props as Props;
+
+const {
+ container: containerClass = '',
+ panel: panelClass = '',
+ title: titleClass = '',
+ description: descriptionClass = '',
+ icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
+} = classes;
+---
+
+{
+ items && items.length > 0 && (
+ <div class={containerClass}>
+ {items.map(({ title, description, icon, classes: itemClasses = {} }, index = 0) => (
+ <div
+ class={twMerge(
+ 'flex intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
+ panelClass,
+ itemClasses?.panel
+ )}
+ >
+ <div class="flex flex-col items-center mr-4 rtl:mr-0 rtl:ml-4">
+ <div>
+ <div class="flex items-center justify-center">
+ {(icon || defaultIcon) && (
+ <Icon
+ name={icon || defaultIcon}
+ class={twMerge('w-10 h-10 p-2 rounded-full border-2', defaultIconClass, itemClasses?.icon)}
+ />
+ )}
+ </div>
+ </div>
+ {index !== items.length - 1 && <div class="w-px h-full bg-black/10 dark:bg-slate-400/50" />}
+ </div>
+ <div class={`pt-1 ${index !== items.length - 1 ? 'pb-8' : ''}`}>
+ {title && <p class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)} set:html={title} />}
+ {description && (
+ <p
+ class={twMerge('text-muted mt-2', descriptionClass, itemClasses?.description)}
+ set:html={description}
+ />
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ )
+}
diff --git a/src/components/ui/WidgetWrapper.astro b/src/components/ui/WidgetWrapper.astro
new file mode 100644
index 0000000..c42c751
--- /dev/null
+++ b/src/components/ui/WidgetWrapper.astro
@@ -0,0 +1,34 @@
+---
+import type { HTMLTag } from 'astro/types';
+import type { Widget } from '~/types';
+import { twMerge } from 'tailwind-merge';
+import Background from './Background.astro';
+
+export interface Props extends Widget {
+ containerClass?: string;
+ ['as']?: HTMLTag;
+}
+
+const { id, isDark = false, containerClass = '', bg, as = 'section' } = Astro.props;
+
+const WrapperTag = as;
+---
+
+<WrapperTag class="relative not-prose scroll-mt-[72px]" {...id ? { id } : {}}>
+ <div class="absolute inset-0 pointer-events-none -z-[1]" aria-hidden="true">
+ <slot name="bg">
+ {bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} />}
+ </slot>
+ </div>
+ <div
+ class:list={[
+ twMerge(
+ 'relative mx-auto max-w-7xl px-4 md:px-6 py-12 md:py-16 lg:py-20 text-default intersect-once intersect-quarter intercept-no-queue motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
+ containerClass
+ ),
+ { dark: isDark },
+ ]}
+ >
+ <slot />
+ </div>
+</WrapperTag>