@atelic-action/ui 3.2.0 → 3.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.
package/README.md CHANGED
@@ -512,6 +512,8 @@ The primitives that came with them, in `@atelic-action/ui/primitives`: `ImageFra
512
512
 
513
513
  The entry also exports the helpers the sections use, for a site's own code: `formatClock`, `formatHoursRange`, and `isClosed` for hours, `isHoneypotFilled`, and `trackEvent`.
514
514
 
515
+ Hours print on the 12 hour clock unless the site's config sets `clock: "24h"`, which turns "9a to 5p" into "09:00 to 17:00" everywhere hours appear: the footer through `SiteShell`, `LocationSection`, and `HoursTable` (which takes `clock` as a prop when used on its own). `formatClock` and `formatHoursRange` take the same `clock` option for a site's own code.
516
+
515
517
  ### The Marketing Vite Config and Commands
516
518
 
517
519
  ```ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atelic-action/ui",
3
- "version": "3.2.0",
3
+ "version": "3.3.0",
4
4
  "description": "Shared UI for the Atelic templates: site chrome, base styles, the component library, the access gate, and the app shell they install",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -124,7 +124,7 @@ export function SiteShell({
124
124
  email={site.email}
125
125
  hours={site.hours.map((row) => ({
126
126
  label: row.labelShort,
127
- range: formatHoursRange(row, { short: true }),
127
+ range: formatHoursRange(row, { short: true, clock: site.clock }),
128
128
  }))}
129
129
  attribution={site.attribution}
130
130
  note={site.footer?.note}
package/src/lib/hours.ts CHANGED
@@ -1,13 +1,23 @@
1
- import type { OpeningHours } from "../sections/siteConfig";
1
+ import type { Clock, OpeningHours } from "../sections/siteConfig";
2
+
3
+ /** How a time prints: `short` drops a whole hour's minutes on the 12 hour clock. */
4
+ export interface ClockOptions {
5
+ short?: boolean;
6
+ /** "12h" (the default) or "24h"; see `SiteConfig.clock`. */
7
+ clock?: Clock;
8
+ }
2
9
 
3
10
  /**
4
11
  * Formats a 24 hour "HH:MM" time as the display style used across the
5
12
  * marketing components: "11:00" -> "11:00a", "19:00" -> "7:00p".
6
13
  * With short: true, whole hours drop their minutes: "11a", "7p".
14
+ * With clock: "24h", the time prints as "HH:MM" with a leading zero
15
+ * ("09:00", "17:00") and short changes nothing.
7
16
  */
8
- export function formatClock(time: string, opts: { short?: boolean } = {}): string {
17
+ export function formatClock(time: string, opts: ClockOptions = {}): string {
9
18
  const [hourStr, minuteStr = "00"] = time.split(":");
10
19
  const hour24 = Number(hourStr);
20
+ if (opts.clock === "24h") return `${String(hour24 % 24).padStart(2, "0")}:${minuteStr}`;
11
21
  const suffix = hour24 % 24 >= 12 ? "p" : "a";
12
22
  const hour = hour24 % 12 || 12;
13
23
  if (opts.short && minuteStr === "00") return `${hour}${suffix}`;
@@ -19,10 +29,11 @@ export function isClosed(row: OpeningHours): boolean {
19
29
  }
20
30
 
21
31
  /**
22
- * "11:00a to 7:00p" (or "11a to 7p" with short: true). Rows without times
23
- * render their note ("By appointment") when present, else "Closed".
32
+ * "11:00a to 7:00p" (or "11a to 7p" with short: true, or "11:00 to 19:00"
33
+ * with clock: "24h"). Rows without times render their note ("By
34
+ * appointment") when present, else "Closed".
24
35
  */
25
- export function formatHoursRange(row: OpeningHours, opts: { short?: boolean } = {}): string {
36
+ export function formatHoursRange(row: OpeningHours, opts: ClockOptions = {}): string {
26
37
  if (!row.opens || !row.closes) return row.note ?? "Closed";
27
38
  return `${formatClock(row.opens, opts)} to ${formatClock(row.closes, opts)}`;
28
39
  }
@@ -1,12 +1,14 @@
1
1
  import { formatHoursRange, isClosed } from "../lib/hours";
2
- import type { OpeningHours } from "./siteConfig";
2
+ import type { Clock, OpeningHours } from "./siteConfig";
3
3
 
4
4
  export interface HoursTableProps {
5
5
  hours: OpeningHours[];
6
+ /** "12h" (the default) or "24h"; pass the site's `SiteConfig.clock`. */
7
+ clock?: Clock;
6
8
  }
7
9
 
8
10
  /** Day-by-day hours rows with dashed rules and mono times. */
9
- export function HoursTable({ hours }: HoursTableProps) {
11
+ export function HoursTable({ hours, clock }: HoursTableProps) {
10
12
  return (
11
13
  <div>
12
14
  {hours.map((row) => (
@@ -15,7 +17,7 @@ export function HoursTable({ hours }: HoursTableProps) {
15
17
  className={["hours-row", isClosed(row) && "closed"].filter(Boolean).join(" ")}
16
18
  >
17
19
  <span className="day">{row.label}</span>
18
- <span className="time">{formatHoursRange(row)}</span>
20
+ <span className="time">{formatHoursRange(row, { clock })}</span>
19
21
  </div>
20
22
  ))}
21
23
  </div>
@@ -7,7 +7,7 @@ import type { SiteConfig } from "./siteConfig";
7
7
 
8
8
  /** The keys of the site config the section reads; a whole `SiteConfig` passes as it is. */
9
9
  export type LocationSectionSite = MapEmbedSite &
10
- Pick<SiteConfig, "serviceAreas" | "hours" | "phone">;
10
+ Pick<SiteConfig, "serviceAreas" | "hours" | "clock" | "phone">;
11
11
 
12
12
  export interface LocationSectionProps {
13
13
  site: LocationSectionSite;
@@ -41,7 +41,7 @@ export function LocationSection({ site, eyebrow = "Find us", title, lead }: Loca
41
41
 
42
42
  <div className="loc-block">
43
43
  <h4>Hours</h4>
44
- <HoursTable hours={site.hours} />
44
+ <HoursTable hours={site.hours} clock={site.clock} />
45
45
  </div>
46
46
 
47
47
  <div className="loc-actions">
@@ -6,7 +6,7 @@
6
6
  * its own.
7
7
  */
8
8
  export { isHoneypotFilled } from "../lib/honeypot";
9
- export { formatClock, formatHoursRange, isClosed } from "../lib/hours";
9
+ export { type ClockOptions, formatClock, formatHoursRange, isClosed } from "../lib/hours";
10
10
  export { type EventParams, trackEvent } from "../lib/trackEvent";
11
11
  export { BlogIndex, type BlogIndexItem, type BlogIndexProps } from "./BlogIndex";
12
12
  export { BlogPost, type BlogPostDoc, type BlogPostProps } from "./BlogPost";
@@ -65,6 +65,7 @@ export { type SocialPhoto, SocialStrip, type SocialStripProps } from "./SocialSt
65
65
  export { SplitSection, type SplitSectionProps } from "./SplitSection";
66
66
  export type {
67
67
  Address,
68
+ Clock,
68
69
  CTA,
69
70
  Founder,
70
71
  GateConfig,
@@ -24,6 +24,9 @@ export interface CTA {
24
24
  external?: boolean;
25
25
  }
26
26
 
27
+ /** How opening hours print: "12h" reads "9a to 5p", "24h" reads "09:00 to 17:00". */
28
+ export type Clock = "12h" | "24h";
29
+
27
30
  export interface OpeningHours {
28
31
  /** Row label as displayed, e.g. "Monday" or "Friday to Sunday". */
29
32
  label: string;
@@ -137,6 +140,14 @@ export interface SiteConfig {
137
140
  */
138
141
  mapImage?: string;
139
142
  hours: OpeningHours[];
143
+ /**
144
+ * The clock the hours print on, wherever they appear (the footer, the
145
+ * hours table, the location section). "12h" (the default) reads "9a to 5p"
146
+ * in the footer and "9:00a to 5:00p" in the table; "24h" reads "09:00 to
147
+ * 17:00" in both, with leading zeros. Structured data is unaffected, since
148
+ * `opens` and `closes` are already 24 hour.
149
+ */
150
+ clock?: Clock;
140
151
  /** Neighborhoods or cities served; feeds local SEO copy and structured data. */
141
152
  serviceAreas: string[];
142
153
  priceRange?: string;