@atelic-action/ui 3.2.0 → 3.3.1

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
@@ -23,6 +23,7 @@ Every export, grouped by the entry it is imported from. The gallery column links
23
23
  | `IconList` | `@atelic-action/ui/components` | A list whose bullets are icons, one for the list or one per item, each in a tone | [#icon-list](https://ui.atelic.me/#icon-list) |
24
24
  | `DataTable` | `@atelic-action/ui/components` | A table whose first column names each row, sortable, stacking on a phone | [#data-table](https://ui.atelic.me/#data-table) |
25
25
  | `ReportCard` | `@atelic-action/ui/components` | A name, a grade, and a line per row, in labeled groups, each row a link to its section | [#report-card](https://ui.atelic.me/#report-card) |
26
+ | `reportCardLabels`, `ReportCardLabels` | `@atelic-action/ui/components` | The words a screen reader hears around a report card's letters by default, and their type | none |
26
27
  | `Rank` | `@atelic-action/ui/components` | A figure with its badge in a tone, for a table cell: a rank, a place in an answer, or a dash | [#rank](https://ui.atelic.me/#rank) |
27
28
  | `StatRow` | `@atelic-action/ui/components` | A row of large figures with periods, deltas, and trends | [#stat-row](https://ui.atelic.me/#stat-row) |
28
29
  | `Callout` | `@atelic-action/ui/components` | One finding: an icon in its tone, the title, the explainer | [#callout](https://ui.atelic.me/#callout) |
@@ -512,6 +513,8 @@ The primitives that came with them, in `@atelic-action/ui/primitives`: `ImageFra
512
513
 
513
514
  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
515
 
516
+ 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.
517
+
515
518
  ### The Marketing Vite Config and Commands
516
519
 
517
520
  ```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.1",
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;
@@ -661,11 +661,23 @@
661
661
  grid-column: 4;
662
662
  }
663
663
  }
664
+ /* A clipping card can defeat a row's break-inside in print engines other
665
+ than Chrome, so the card stops clipping there and the first group
666
+ label rounds its own top corners instead. A label or a heading never
667
+ ends a page alone. */
664
668
  @media print {
665
669
  .atelic-ui .rc-card {
670
+ overflow: visible;
666
671
  box-shadow: none;
667
672
  border-color: var(--neutral-200);
668
673
  }
674
+ .atelic-ui .rc-card > .rc-group:first-child {
675
+ border-radius: calc(var(--radius-md) - 1px) calc(var(--radius-md) - 1px) 0 0;
676
+ }
677
+ .atelic-ui .rc-head,
678
+ .atelic-ui .rc-group {
679
+ break-after: avoid;
680
+ }
669
681
  .atelic-ui .rc-row {
670
682
  break-inside: avoid;
671
683
  }