@contentrain/astro-kit 0.49.2 → 0.50.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/catalog.json CHANGED
@@ -3072,6 +3072,11 @@
3072
3072
  "required": true,
3073
3073
  "description": "Site title (site.title or site.organization)."
3074
3074
  },
3075
+ "homeHref": {
3076
+ "type": "url",
3077
+ "default": "/",
3078
+ "description": "Where the brand (the site name) links: the site's home."
3079
+ },
3075
3080
  "tagline": {
3076
3081
  "type": "string",
3077
3082
  "description": "A line under the name (site.tagline, or the source footer's about text); line breaks are kept, so a credit under it stays its own line."
@@ -3561,7 +3566,7 @@
3561
3566
  {
3562
3567
  "id": "header",
3563
3568
  "name": "Header",
3564
- "description": "Site header: brand (logo and/or name), navigation and an optional call-to-action button.",
3569
+ "description": "Site header: brand (logo and/or name), navigation, an optional call-to-action button and an optional utility row above the bar (phone, mail, address, hours).",
3565
3570
  "category": "chrome",
3566
3571
  "uses": [
3567
3572
  "nav",
@@ -3734,7 +3739,23 @@
3734
3739
  ]
3735
3740
  }
3736
3741
  },
3737
- "description": "Header button, shown on wide screens."
3742
+ "description": "Header button, on every screen width."
3743
+ },
3744
+ "strip": {
3745
+ "type": "array",
3746
+ "items": {
3747
+ "type": "object",
3748
+ "fields": {
3749
+ "label": {
3750
+ "type": "string",
3751
+ "required": true
3752
+ },
3753
+ "href": {
3754
+ "type": "url"
3755
+ }
3756
+ }
3757
+ },
3758
+ "description": "A row above the bar for what the source's header prints next to its menu: a phone number, a mail address, the address, the opening hours. A line with an href is a link (tel:, mailto:, a page). On a phone it folds into the menu drawer after the menu; with no menu it stays above the bar."
3738
3759
  },
3739
3760
  "search": {
3740
3761
  "type": "object",
@@ -4659,6 +4680,22 @@
4659
4680
  "part": {
4660
4681
  "type": "string",
4661
4682
  "description": "What the navigation is, for checks that find a page's parts (data-cr-part), e.g. \"nav-header\"."
4683
+ },
4684
+ "extra": {
4685
+ "type": "array",
4686
+ "items": {
4687
+ "type": "object",
4688
+ "fields": {
4689
+ "label": {
4690
+ "type": "string",
4691
+ "required": true
4692
+ },
4693
+ "href": {
4694
+ "type": "url"
4695
+ }
4696
+ }
4697
+ },
4698
+ "description": "Lines after the menu in the narrow-screen menu (a header's phone, address, hours); a line with an href is a link."
4662
4699
  }
4663
4700
  },
4664
4701
  "slots": {},
@@ -45,6 +45,8 @@ const footer = tv({
45
45
 
46
46
  interface Props {
47
47
  siteName: string
48
+ /** Where the brand (the site name) links: the site's home, `/` when absent — a source footer's site title links there. */
49
+ homeHref?: string | undefined
48
50
  tagline?: string | undefined
49
51
  /** Link groups for the `columns` layout. `titleHref`: the heading is a link to it (a source heading printed as one). */
50
52
  columns?: Array<{ title?: string | undefined, titleHref?: string | undefined, label?: string | undefined, links: LinkInput[] }> | undefined
@@ -86,7 +88,7 @@ interface Props {
86
88
  wordmarkOrder?: 'last' | 'first' | undefined
87
89
  }
88
90
 
89
- const { siteName, tagline, columns = [], links = [], social = [], contact, copyright, consentLabel, navLabel = 'Footer', layout = 'simple', tone, align, pack, packGap, narrow, packGapNarrow, linkSize, linkSizeNarrow, border, wordmark, wordmarkOrder } = Astro.props
91
+ const { siteName, homeHref = '/', tagline, columns = [], links = [], social = [], contact, copyright, consentLabel, navLabel = 'Footer', layout = 'simple', tone, align, pack, packGap, narrow, packGapNarrow, linkSize, linkSizeNarrow, border, wordmark, wordmarkOrder } = Astro.props
90
92
  const { root, inner, muted, link } = footer({ layout, tone, align, pack: layout === 'columns' ? pack : undefined, narrow: layout === 'columns' && pack === 'end' ? narrow : undefined, border: (border ?? (tone ?? 'default') === 'default') ? 'on' : 'off' })
91
93
  // Custom properties the packed columns and sized links read; none when none is set.
92
94
  const vars = footerVars({ layout, columnCount: columns.length, pack, narrow, packGap, packGapNarrow, linkSize, linkSizeNarrow })
@@ -108,7 +110,7 @@ const mark = wordmark === 'fit' || wordmark === 'crop' ? wordmarkBox(siteName, w
108
110
  {mark && wordmarkOrder === 'first' && <div class="container-page pt-10" data-kit-wordmark><svg viewBox={`0 0 1000 ${mark.height}`} class="block h-auto w-full fill-current" aria-hidden="true" focusable="false"><text x="0" y={mark.baseline} textLength="1000" lengthAdjust="spacing" font-size={mark.size} class="[font-family:var(--font-heading,inherit)] [font-weight:var(--font-weight-heading,700)]">{siteName}</text></svg></div>}
109
111
  <div class:list={[inner(), ...footerLinkSizeClasses({ linkSize, linkSizeNarrow })]} data-kit-footer style={vars || undefined}>
110
112
  <div class="flex flex-col gap-3">
111
- <p class="text-base font-semibold" data-kit-brand>{siteName}</p>
113
+ <p class="text-base font-semibold" data-kit-brand><a href={safeHref(homeHref)} class="text-current no-underline hover:underline">{siteName}</a></p>
112
114
  {/* Line breaks are kept: an about line and the designer's credit under it ("Web Tasarım: …") stay two lines. */}
113
115
  {tagline && <p class={`${muted()} whitespace-pre-line`} data-kit-tagline>{tagline}</p>}
114
116
  {hasContact && (
@@ -75,6 +75,11 @@
75
75
  "required": true,
76
76
  "description": "Site title (site.title or site.organization)."
77
77
  },
78
+ "homeHref": {
79
+ "type": "url",
80
+ "default": "/",
81
+ "description": "Where the brand (the site name) links: the site's home."
82
+ },
78
83
  "tagline": {
79
84
  "type": "string",
80
85
  "description": "A line under the name (site.tagline, or the source footer's about text); line breaks are kept, so a credit under it stays its own line."
@@ -1,6 +1,6 @@
1
1
  ---
2
- // Site header: brand (logo and/or name), navigation and an optional call to
3
- // action.
2
+ // Site header: brand (logo and/or name), navigation, an optional call to
3
+ // action and an optional utility row above the bar (phone, mail, address, hours).
4
4
  import { tv } from 'tailwind-variants'
5
5
  import Button from '../_shared/Button.astro'
6
6
  import KitImage from '../_shared/KitImage.astro'
@@ -63,7 +63,14 @@ interface Props {
63
63
  showName?: boolean | undefined
64
64
  homeHref?: string | undefined
65
65
  items: NavItem[]
66
+ /** The header's button, on every screen width. */
66
67
  action?: ActionInput | undefined
68
+ /**
69
+ * A row above the bar for what the source's header prints next to its menu: a phone number, a mail address, the
70
+ * address, the opening hours. A line with an `href` is a link (`tel:`, `mailto:`, a page). On a phone the row folds
71
+ * into the menu drawer, after the menu; with no menu it stays above the bar.
72
+ */
73
+ strip?: Array<{ label: string, href?: string | undefined }> | undefined
67
74
  /** A search field in the bar (a GET form to the site's search page); below the bar until lg. */
68
75
  search?: { label?: string | undefined, placeholder?: string | undefined, submitLabel?: string | undefined, action?: string | undefined } | undefined
69
76
  navLabel?: string | undefined
@@ -76,7 +83,7 @@ interface Props {
76
83
  menu?: 'flyout' | 'mega' | undefined
77
84
  }
78
85
 
79
- const { siteName, tagline, brandSize, brandWeight, brandTracking, bordered, offsetTop, padTop, padBottom, linkNav, brandColor, logo, showName = true, homeHref = '/', items, action, search, navLabel, menuLabel, closeLabel, layout, tone, position, menu } = Astro.props
86
+ const { siteName, tagline, brandSize, brandWeight, brandTracking, bordered, offsetTop, padTop, padBottom, linkNav, brandColor, logo, showName = true, homeHref = '/', items, action, strip = [], search, navLabel, menuLabel, closeLabel, layout, tone, position, menu } = Astro.props
80
87
  const { root, inner, brand } = header({ layout, tone, position, bordered })
81
88
  // The header's search field: `header-search` on a page, a second header's its own (its label points at its own input).
82
89
  const searchId = search ? instanceId(Astro.locals, undefined, 'header-search') : undefined
@@ -84,11 +91,22 @@ const rootStyle = offsetTop ? `margin-top: ${offsetTop}` : undefined
84
91
  const innerStyle = [padTop && `padding-top: ${padTop}`, padBottom && `padding-bottom: ${padBottom}`].filter(Boolean).join('; ') || undefined
85
92
  const brandColour = brandColorValue(brandColor)
86
93
  const brandStyle = [brandSize && `font-size: ${brandSize}`, brandWeight && `font-weight: ${brandWeight}`, brandTracking && `letter-spacing: ${brandTracking}`, brandColour && `color: ${brandColour}`].filter(Boolean).join('; ') || undefined
94
+ // The row above the bar folds into the drawer only where there is one: Nav prints nothing without items.
95
+ const stripFolds = items.length > 0
87
96
  // A tagline under the name keeps its line on a phone, as themes print it: the navigation wraps below.
88
97
  // The search field sits in the bar from lg; at md the bar already holds the navigation, so the field keeps its own row.
89
98
  const wrap = search ? 'flex-wrap lg:flex-nowrap' : tagline ? 'flex-wrap md:flex-nowrap' : undefined
90
99
  ---
91
100
  <header class={root()} style={rootStyle}>
101
+ {strip.length > 0 && (
102
+ <div class:list={['border-b border-current/10 text-sm', stripFolds && 'hidden md:block']} data-kit-strip>
103
+ <ul class={`container-page flex flex-wrap items-center justify-end gap-x-6 gap-y-1 py-2 ${muted}`}>
104
+ {strip.map(line => (
105
+ <li>{line.href ? <a href={safeHref(line.href)} class="text-current no-underline hover:underline">{line.label}</a> : line.label}</li>
106
+ ))}
107
+ </ul>
108
+ </div>
109
+ )}
92
110
  <div class={inner({ class: wrap })} style={innerStyle}>
93
111
  {tagline
94
112
  ? (
@@ -112,8 +130,8 @@ const wrap = search ? 'flex-wrap lg:flex-nowrap' : tagline ? 'flex-wrap md:flex-
112
130
  </div>
113
131
  )}
114
132
  <div class:list={['flex items-center gap-4', layout === 'centered' ? 'justify-center' : 'ms-auto']}>
115
- <Nav items={items} menu={menu} label={navLabel} menuLabel={menuLabel} closeLabel={closeLabel} part="nav-header" color={linkNav ? 'link' : 'current'} align={layout === 'centered' ? 'center' : 'end'} />
116
- {action && <Button action={action} size="sm" class="hidden md:inline-flex" />}
133
+ <Nav items={items} menu={menu} label={navLabel} menuLabel={menuLabel} closeLabel={closeLabel} part="nav-header" extra={stripFolds ? strip : undefined} color={linkNav ? 'link' : 'current'} align={layout === 'centered' ? 'center' : 'end'} />
134
+ {action && <Button action={action} size="sm" />}
117
135
  </div>
118
136
  </div>
119
137
  </header>
@@ -20,7 +20,16 @@
20
20
  "action": {
21
21
  "label": "Subscribe",
22
22
  "href": "/subscribe/"
23
- }
23
+ },
24
+ "strip": [
25
+ {
26
+ "label": "(417) 522-7711",
27
+ "href": "tel:+14175227711"
28
+ },
29
+ {
30
+ "label": "Mon–Fri 9:00–17:00"
31
+ }
32
+ ]
24
33
  }
25
34
  },
26
35
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "header",
3
3
  "name": "Header",
4
- "description": "Site header: brand (logo and/or name), navigation and an optional call-to-action button.",
4
+ "description": "Site header: brand (logo and/or name), navigation, an optional call-to-action button and an optional utility row above the bar (phone, mail, address, hours).",
5
5
  "category": "chrome",
6
6
  "uses": ["nav", "search-box"],
7
7
  "shared": ["Button.astro", "KitImage.astro", "image-widths.ts", "instance-id.ts", "muted.ts", "safe-url.ts", "types.ts"],
@@ -27,7 +27,8 @@
27
27
  "showName": { "type": "boolean", "default": true, "description": "Show the site name next to the logo." },
28
28
  "homeHref": { "type": "url", "default": "/", "description": "Where the brand links." },
29
29
  "items": { "type": "array", "required": true, "items": { "type": "object", "fields": { "label": { "type": "string", "required": true }, "href": { "type": "url", "required": true }, "newTab": { "type": "boolean" }, "children": { "type": "array" } } }, "description": "Primary menu (menus → menu-items)." },
30
- "action": { "type": "object", "fields": { "label": { "type": "string", "required": true }, "href": { "type": "url", "required": true }, "style": { "type": "select", "options": ["primary", "secondary", "ghost"] } }, "description": "Header button, shown on wide screens." },
30
+ "action": { "type": "object", "fields": { "label": { "type": "string", "required": true }, "href": { "type": "url", "required": true }, "style": { "type": "select", "options": ["primary", "secondary", "ghost"] } }, "description": "Header button, on every screen width." },
31
+ "strip": { "type": "array", "items": { "type": "object", "fields": { "label": { "type": "string", "required": true }, "href": { "type": "url" } } }, "description": "A row above the bar for what the source's header prints next to its menu: a phone number, a mail address, the address, the opening hours. A line with an href is a link (tel:, mailto:, a page). On a phone it folds into the menu drawer after the menu; with no menu it stays above the bar." },
31
32
  "search": { "type": "object", "fields": { "label": { "type": "string" }, "placeholder": { "type": "string" }, "submitLabel": { "type": "string" }, "action": { "type": "url" } }, "description": "A search field in the bar: a GET form to the site's search page (default /search); below the bar on a phone." },
32
33
  "navLabel": { "type": "string", "description": "Accessible name of the navigation (ui-strings nav.label)." },
33
34
  "menuLabel": { "type": "string", "description": "Narrow-screen toggle text (ui-strings nav.menu)." },
@@ -50,11 +50,13 @@ interface Props {
50
50
  color?: 'current' | 'link' | undefined
51
51
  /** Unique per page when a page has two navigations. */
52
52
  id?: string | undefined
53
+ /** Lines after the menu in the narrow-screen menu (a header's phone, address, hours); a line with an `href` is a link. */
54
+ extra?: Array<{ label: string, href?: string | undefined }> | undefined
53
55
  /** What the navigation is, for checks that find a page's parts (`data-cr-part`, e.g. "nav-header"). */
54
56
  part?: string | undefined
55
57
  }
56
58
 
57
- const { items, label = 'Main', menuLabel = 'Menu', closeLabel = 'Close', mobile = 'popover', menu, align, density, color, id: givenId, part } = Astro.props
59
+ const { items, label = 'Main', menuLabel = 'Menu', closeLabel = 'Close', mobile = 'popover', menu, align, density, color, extra = [], id: givenId, part } = Astro.props
58
60
  // Two on one page never share an id: their labels and controls would point into each other.
59
61
  const id = instanceId(Astro.locals, givenId, 'site-nav')
60
62
  const { list, link, sub, toggle } = nav({ menu, align, density, color })
@@ -117,6 +119,13 @@ const real = (href: string | undefined) => !!href && href !== '#'
117
119
  {item.children && item.children.length > 0 && <NavTree items={item.children} current={current} kind="drawer" />}
118
120
  </li>
119
121
  ))}
122
+ {extra.map((line, index) => (
123
+ <li class={index === 0 ? 'mt-4 border-t border-line pt-4' : undefined}>
124
+ {line.href
125
+ ? <a href={safeHref(line.href)} class="block rounded-md px-3 py-2 text-sm text-ink no-underline hover:bg-surface-muted">{line.label}</a>
126
+ : <span class="block px-3 py-2 text-sm text-ink">{line.label}</span>}
127
+ </li>
128
+ ))}
120
129
  </ul>
121
130
  </div>
122
131
  </>
@@ -136,6 +145,13 @@ const real = (href: string | undefined) => !!href && href !== '#'
136
145
  {item.children && item.children.length > 0 && <NavTree items={item.children} current={current} kind="drawer" />}
137
146
  </li>
138
147
  ))}
148
+ {extra.map((line, index) => (
149
+ <li class={index === 0 ? 'mt-2 border-t border-line pt-2' : undefined}>
150
+ {line.href
151
+ ? <a href={safeHref(line.href)} class="block rounded-md px-3 py-2 text-sm text-ink no-underline hover:bg-surface-muted">{line.label}</a>
152
+ : <span class="block px-3 py-2 text-sm text-ink">{line.label}</span>}
153
+ </li>
154
+ ))}
139
155
  </ul>
140
156
  </details>
141
157
  )}
@@ -97,6 +97,22 @@
97
97
  "part": {
98
98
  "type": "string",
99
99
  "description": "What the navigation is, for checks that find a page's parts (data-cr-part), e.g. \"nav-header\"."
100
+ },
101
+ "extra": {
102
+ "type": "array",
103
+ "items": {
104
+ "type": "object",
105
+ "fields": {
106
+ "label": {
107
+ "type": "string",
108
+ "required": true
109
+ },
110
+ "href": {
111
+ "type": "url"
112
+ }
113
+ }
114
+ },
115
+ "description": "Lines after the menu in the narrow-screen menu (a header's phone, address, hours); a line with an href is a link."
100
116
  }
101
117
  },
102
118
  "slots": {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contentrain/astro-kit",
3
- "version": "0.49.2",
3
+ "version": "0.50.0",
4
4
  "license": "MIT",
5
5
  "description": "Copy-in Astro components for Contentrain sites (shadcn model): typed props, tailwind-variants, zero JavaScript by default — with a machine-readable catalog",
6
6
  "type": "module",
@@ -42,7 +42,7 @@
42
42
  "mapping"
43
43
  ],
44
44
  "dependencies": {
45
- "@contentrain/types": "1.75.0"
45
+ "@contentrain/types": "1.76.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^22.10.0",