diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-22 15:08:37 +0300 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-22 15:08:37 +0300 |
| commit | fcc2f4704e39b0e69b377cc138f75027721dac22 (patch) | |
| tree | 732fc94b354a26c08fba9cc9059f9c6c900182be /src/components/ui | |
Initial template
Diffstat (limited to 'src/components/ui')
| -rw-r--r-- | src/components/ui/Background.astro | 11 | ||||
| -rw-r--r-- | src/components/ui/Button.astro | 40 | ||||
| -rw-r--r-- | src/components/ui/DListItem.astro | 22 | ||||
| -rw-r--r-- | src/components/ui/Form.astro | 87 | ||||
| -rw-r--r-- | src/components/ui/Headline.astro | 35 | ||||
| -rw-r--r-- | src/components/ui/ItemGrid.astro | 65 | ||||
| -rw-r--r-- | src/components/ui/ItemGrid2.astro | 59 | ||||
| -rw-r--r-- | src/components/ui/Timeline.astro | 60 | ||||
| -rw-r--r-- | src/components/ui/WidgetWrapper.astro | 34 |
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..f220487 --- /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-section dark:bg-dark': 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..6b906b0 --- /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-primary 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..b25c9de --- /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-secondary', +} = 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> |
