@hubpav/hwio-ui 0.2.0 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hubpav/hwio-ui",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "HARDWARIO web design system for Astro: DaisyUI 5 themes generated from the brand tokens, the Tailwind entry stylesheet and the HWio* components.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,9 +31,9 @@
31
31
  "build": "node scripts/build-themes.mjs"
32
32
  },
33
33
  "dependencies": {
34
- "@hubpav/hwio-brand": "0.2.0",
35
- "@hubpav/hwio-web-runtime": "0.2.0",
36
- "@hubpav/hwio-web-worker": "0.2.0",
34
+ "@hubpav/hwio-brand": "0.4.0",
35
+ "@hubpav/hwio-web-runtime": "0.4.0",
36
+ "@hubpav/hwio-web-worker": "0.4.0",
37
37
  "daisyui": "5.7.28"
38
38
  },
39
39
  "peerDependencies": {
@@ -1,14 +1,16 @@
1
1
  ---
2
2
  /** Exclusive accordion on native <details name>, optional FAQPage JSON-LD. Answers are text unless `html` is set. */
3
- interface Props { items: { q: string; a: string }[]; name?: string; html?: boolean; jsonLd?: boolean; class?: string }
3
+ /** An answer is one paragraph, or several as an array. */
4
+ interface Props { items: { q: string; a: string | string[] }[]; name?: string; html?: boolean; jsonLd?: boolean; class?: string }
4
5
  const { items, name = 'hwio-faq', html = false, jsonLd = true, class: className = '' } = Astro.props;
5
- const ld = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: items.map((i) => ({ '@type': 'Question', name: i.q, acceptedAnswer: { '@type': 'Answer', text: i.a } })) };
6
+ const paragraphs = (a: string | string[]) => (Array.isArray(a) ? a : [a]);
7
+ const ld = { '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: items.map((i) => ({ '@type': 'Question', name: i.q, acceptedAnswer: { '@type': 'Answer', text: paragraphs(i.a).join(' ') } })) };
6
8
  ---
7
9
  <div class:list={['space-y-3', className]}>
8
10
  {items.map((item) => (
9
11
  <details class="collapse collapse-arrow border border-base-300 bg-base-100" name={name}>
10
12
  <summary class="collapse-title font-semibold">{item.q}</summary>
11
- <div class="collapse-content opacity-90">{html ? <div set:html={item.a} /> : <p>{item.a}</p>}</div>
13
+ <div class="collapse-content space-y-3 opacity-90">{html ? <div set:html={paragraphs(item.a).join('')} /> : paragraphs(item.a).map((p) => <p>{p}</p>)}</div>
12
14
  </details>
13
15
  ))}
14
16
  </div>
@@ -17,10 +17,19 @@ interface Props {
17
17
  showThemeToggle?: boolean;
18
18
  scrollspy?: boolean;
19
19
  labels: { nav: string; menu: string; close: string; theme?: string; languages?: string };
20
+ /** Viewport from which the inline nav and CTAs replace the drawer (default lg; xl for long navs). */
21
+ desktopFrom?: 'lg' | 'xl';
22
+ /** Desktop language control: dropdown (default) or inline pills (two-language sites). */
23
+ languageVariant?: 'dropdown' | 'pills';
20
24
  id?: string;
21
25
  class?: string;
22
26
  }
23
- const { nav, ctas = [], languages = [], showThemeToggle = true, scrollspy = false, labels, id = 'hwio-nav-drawer', class: className = '' } = Astro.props;
27
+ const { nav, ctas = [], languages = [], showThemeToggle = true, scrollspy = false, labels, desktopFrom = 'lg', languageVariant = 'dropdown', id = 'hwio-nav-drawer', class: className = '' } = Astro.props;
28
+ const BREAK = {
29
+ lg: { nav: 'hidden flex-1 justify-center lg:flex', ctas: 'hidden items-center gap-2 lg:flex', drawer: 'drawer drawer-end w-auto lg:hidden' },
30
+ xl: { nav: 'hidden flex-1 justify-center xl:flex', ctas: 'hidden items-center gap-2 xl:flex', drawer: 'drawer drawer-end w-auto xl:hidden' },
31
+ } as const;
32
+ const bp = BREAK[desktopFrom];
24
33
  const CTA_VARIANT = { primary: 'primary', secondary: 'secondary', ghost: 'ghost' } as const;
25
34
  ---
26
35
  <header class:list={['sticky top-0 z-50 border-b border-base-300 bg-base-100/90 backdrop-blur supports-[backdrop-filter]:bg-base-100/80', className]} data-hwio-font-exempt>
@@ -29,7 +38,7 @@ const CTA_VARIANT = { primary: 'primary', secondary: 'secondary', ghost: 'ghost'
29
38
  <slot name="logo" />
30
39
  </div>
31
40
 
32
- <nav class="hidden flex-1 justify-center lg:flex" aria-label={labels.nav} data-hwio-scrollspy={scrollspy ? 'true' : undefined}>
41
+ <nav class={bp.nav} aria-label={labels.nav} data-hwio-scrollspy={scrollspy ? 'true' : undefined}>
33
42
  <ul class="menu menu-horizontal flex-nowrap gap-0 px-0 whitespace-nowrap">
34
43
  {nav.map((item) => (
35
44
  <li>
@@ -50,13 +59,16 @@ const CTA_VARIANT = { primary: 'primary', secondary: 'secondary', ghost: 'ghost'
50
59
 
51
60
  <div class="ml-auto flex items-center gap-2">
52
61
  {showThemeToggle && <HWioThemeToggle label={labels.theme ?? 'Theme'} />}
53
- {languages.length > 1 && <HWioLanguageSwitcher items={languages} label={labels.languages ?? 'Language'} />}
54
- <div class="hidden items-center gap-2 lg:flex">
62
+ {languages.length > 1 && <HWioLanguageSwitcher items={languages} label={labels.languages ?? 'Language'} variant={languageVariant} />}
63
+ <div class={bp.ctas}>
55
64
  {ctas.map((cta) => (
56
- <HWioButton href={cta.href} external={cta.external} variant={CTA_VARIANT[cta.variant ?? 'primary']} size="sm" track={cta.track ?? { id: 'header-cta', location: 'header' }}>{cta.label}</HWioButton>
65
+ <HWioButton href={cta.href} external={cta.external} variant={CTA_VARIANT[cta.variant ?? 'primary']} size="sm" track={cta.track ?? { id: 'header-cta', location: 'header' }} class={cta.icon ? '2xl:px-4 px-2.5' : ''} aria-label={cta.icon ? cta.label : undefined} title={cta.icon ? cta.label : undefined}>
66
+ {cta.icon && <HWioIcon name={cta.icon as 'bag'} size="sm" />}
67
+ {cta.icon ? <span class="hidden 2xl:inline">{cta.label}</span> : cta.label}
68
+ </HWioButton>
57
69
  ))}
58
70
  </div>
59
- <div class="drawer drawer-end w-auto lg:hidden">
71
+ <div class={bp.drawer}>
60
72
  <input id={id} type="checkbox" class="drawer-toggle" />
61
73
  <div class="drawer-content">
62
74
  <label for={id} class="btn btn-ghost btn-square drawer-button" aria-label={labels.menu} aria-controls={`${id}-panel`} aria-expanded="false" tabindex="0" role="button">
@@ -1,12 +1,13 @@
1
1
  ---
2
2
  /** Customer/partner logos as a static row or an infinite marquee (paused on hover, static under reduced motion). */
3
3
  import type { HWioTone } from '../types';
4
- interface Logo { src: string; alt: string; href?: string; width: number; height: number; class?: string }
4
+ interface Logo { src: string; alt: string; href?: string; width: number; height: number; class?: string; /** Rendered height step (default md = 28 px); logos with thin marks go up, dense ones down. */ size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' }
5
5
  interface Props { id?: string; logos: Logo[]; label?: string; marquee?: boolean; grey?: boolean; tone?: HWioTone; class?: string }
6
6
  const { id, logos, label, marquee = true, grey = true, tone = 'base', class: className = '' } = Astro.props;
7
7
  const TONE = { base: 'bg-base-100', alt: 'bg-base-200', neutral: 'bg-neutral text-neutral-content' } as const;
8
8
  const FADE = { base: 'from-base-100', alt: 'from-base-200', neutral: 'from-neutral' } as const;
9
9
  const groups = marquee ? [0, 1, 2, 3] : [0];
10
+ const SIZE = { xs: 'h-6', sm: 'h-7', md: 'h-7', lg: 'h-10', xl: 'h-12' } as const;
10
11
  ---
11
12
  <section id={id} class:list={['overflow-hidden border-y border-base-300 py-10', TONE[tone], className]}>
12
13
  {label && <div class="container"><p class="mb-8 text-center text-sm font-semibold opacity-70">{label}</p></div>}
@@ -17,7 +18,7 @@ const groups = marquee ? [0, 1, 2, 3] : [0];
17
18
  {groups.map((g) => (
18
19
  <div class="flex shrink-0 items-center" aria-hidden={g > 0 ? 'true' : undefined}>
19
20
  {logos.map((logo) => {
20
- const img = <img src={logo.src} alt={logo.alt} width={logo.width} height={logo.height} loading="lazy" decoding="async" class:list={['h-7 w-auto transition-transform duration-300 hover:scale-105', grey ? 'hwio-logo-grey' : '', logo.class]} />;
21
+ const img = <img src={logo.src} alt={logo.alt} width={logo.width} height={logo.height} loading="lazy" decoding="async" class:list={[SIZE[logo.size ?? 'md'], 'w-auto transition-transform duration-300 hover:scale-105', grey ? 'hwio-logo-grey' : '', logo.class]} />;
21
22
  return logo.href
22
23
  ? <a href={logo.href} target="_blank" rel="noopener noreferrer" class="mx-8" tabindex={g > 0 ? -1 : undefined}>{img}</a>
23
24
  : <span class="mx-8">{img}</span>;
@@ -2,7 +2,7 @@
2
2
  import type { HWioCta } from '../types';
3
3
  import HWioButton from './HWioButton.astro';
4
4
  import HWioIcon from './HWioIcon.astro';
5
- interface Tier { name: string; price?: string; description?: string; features: string[]; cta?: HWioCta; featured?: boolean; badge?: string }
5
+ interface Tier { eyebrow?: string; name: string; price?: string; pricePrefix?: string; period?: string; description?: string; features: string[]; cta?: HWioCta; featured?: boolean; badge?: string }
6
6
  interface Props { tiers: Tier[]; class?: string }
7
7
  const { tiers, class: className = '' } = Astro.props;
8
8
  const COLS = { 1: 'grid gap-6', 2: 'grid gap-6 md:grid-cols-2', 3: 'grid gap-6 md:grid-cols-3', 4: 'grid gap-6 md:grid-cols-2 lg:grid-cols-4' } as const;
@@ -12,11 +12,18 @@ const cols = (Math.min(Math.max(tiers.length, 1), 4) as 1 | 2 | 3 | 4);
12
12
  {tiers.map((tier) => (
13
13
  <article class:list={['card card-border bg-base-100 h-full', tier.featured ? 'border-primary shadow-lg' : '']}>
14
14
  <div class="card-body">
15
+ {tier.eyebrow && <p class="hwio-kicker">{tier.eyebrow}</p>}
15
16
  <div class="flex items-start justify-between gap-2">
16
17
  <h3 class="card-title text-[1.375rem]">{tier.name}</h3>
17
- {tier.badge && <span class="badge badge-primary">{tier.badge}</span>}
18
+ {tier.badge && <span class="badge badge-primary shrink-0 whitespace-nowrap">{tier.badge}</span>}
18
19
  </div>
19
- {tier.price && <p class="text-3xl font-bold">{tier.price}</p>}
20
+ {tier.price && (
21
+ <p class="flex flex-wrap items-baseline gap-x-1.5">
22
+ {tier.pricePrefix && <span class="hwio-muted text-sm">{tier.pricePrefix}</span>}
23
+ <span class="text-3xl font-bold tracking-tight">{tier.price}</span>
24
+ {tier.period && <span class="hwio-muted text-sm">{tier.period}</span>}
25
+ </p>
26
+ )}
20
27
  {tier.description && <p class="opacity-80">{tier.description}</p>}
21
28
  <ul class="my-4 space-y-2 text-sm">
22
29
  {tier.features.map((f) => <li class="flex items-start gap-2"><HWioIcon name="check" size="sm" class="mt-0.5 shrink-0 text-accent" /><span>{f}</span></li>)}
package/src/types.ts CHANGED
@@ -24,6 +24,8 @@ export interface HWioMedia {
24
24
  export interface HWioCta extends HWioLinkItem {
25
25
  /** Visual weight; primary is the DaisyUI btn-primary fill. */
26
26
  variant?: 'primary' | 'secondary' | 'ghost';
27
+ /** HWioIcon name. In HWioHeader a CTA with an icon collapses to the icon alone below 2xl (label as aria-label). */
28
+ icon?: string;
27
29
  }
28
30
 
29
31
  export type HWioTone = 'base' | 'alt' | 'neutral';