@medalsocial/meda 3.11.0 → 3.12.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.
@@ -83,10 +83,36 @@ export interface ConfirmationProps {
83
83
  /** The visit's total in minor units. */
84
84
  totalOre: number;
85
85
  address?: string | null;
86
- /** The calendar file (e.g. a `data:text/calendar` URL). `null` draws no link. */
86
+ /**
87
+ * The calendar link: a calendar file the site serves (`text/calendar`, which
88
+ * iOS and macOS open straight into «Add to Calendar»), a calendar's own
89
+ * add-event page (Google Calendar), or a `data:text/calendar` URL. `null`
90
+ * draws no link.
91
+ */
87
92
  calendarHref?: string | null;
88
- /** `download` filename for the calendar link. Required for a `data:` URL to work. */
93
+ /**
94
+ * `download` filename for the calendar link. Only a `data:` or `blob:` URL
95
+ * gets it: browsers refuse to navigate to one, so the attribute is what makes
96
+ * it do anything. On a real URL it would force a download, and an iPhone
97
+ * would save the file to Files instead of opening Calendar.
98
+ */
89
99
  calendarFileName?: string;
100
+ /**
101
+ * The calendar link is another site's page (Google Calendar's add-event
102
+ * form): it opens in a new tab. A calendar file — relative or absolute —
103
+ * opens in place, which is what lets iOS show «Add to Calendar».
104
+ */
105
+ calendarNewTab?: boolean;
106
+ /**
107
+ * A second way into a calendar, drawn as a small link: Google Calendar beside
108
+ * a calendar file, or the file beside Google Calendar. `newTab` as for
109
+ * `calendarNewTab`. `null` draws none.
110
+ */
111
+ calendarAlt?: {
112
+ href: string;
113
+ label: string;
114
+ newTab?: boolean;
115
+ } | null;
90
116
  /** Where all of the visitor's bookings are listed. `null` draws no link. */
91
117
  portalHref?: string | null;
92
118
  /** A way back to an empty wizard. Absent, the button is not drawn. */
@@ -109,4 +135,4 @@ export interface ConfirmationProps {
109
135
  classNames?: SlotClassNames<ConfirmationSlot>;
110
136
  components?: ConfirmationComponents;
111
137
  }
112
- export declare function Confirmation({ lines: rawLines, startTs, totalOre, address, calendarHref, calendarFileName, portalHref, onStartOver, now, layout, directionsHref, freeChangeUntilTs, format, labels, classNames, components, }: ConfirmationProps): import("react").JSX.Element;
138
+ export declare function Confirmation({ lines: rawLines, startTs, totalOre, address, calendarHref, calendarFileName, calendarAlt, calendarNewTab, portalHref, onStartOver, now, layout, directionsHref, freeChangeUntilTs, format, labels, classNames, components, }: ConfirmationProps): import("react").JSX.Element;
@@ -68,10 +68,22 @@ export function DefaultPartyLine({ line, text, manageLabel, manageAriaLabel, lin
68
68
  return (_jsxs("li", { className: "flex flex-wrap items-baseline gap-x-3", children: [_jsx("span", { className: "tabular-nums", children: text }), line.manageHref && (_jsx("a", { href: line.manageHref, "aria-label": manageAriaLabel, className: linkClassName ?? 'text-sm font-semibold text-primary underline underline-offset-4', children: manageLabel }))] }));
69
69
  }
70
70
  const SEPARATOR = ' · ';
71
- export function Confirmation({ lines: rawLines, startTs, totalOre, address = null, calendarHref = null, calendarFileName = 'booking.ics', portalHref = null, onStartOver, now = Date.now(), layout = 'card', directionsHref = null, freeChangeUntilTs = null, format, labels, classNames, components, }) {
71
+ /** `download` only where it is needed (see `calendarFileName`). */
72
+ function downloadFor(href, fileName) {
73
+ return /^(data|blob):/i.test(href) ? fileName : undefined;
74
+ }
75
+ /** A calendar link: the file in place, another calendar's page in a new tab. */
76
+ function calendarLinkProps(href, fileName, newTab = false) {
77
+ return {
78
+ href,
79
+ download: downloadFor(href, fileName),
80
+ ...(newTab ? { target: '_blank', rel: 'noopener' } : {}),
81
+ };
82
+ }
83
+ export function Confirmation({ lines: rawLines, startTs, totalOre, address = null, calendarHref = null, calendarFileName = 'booking.ics', calendarAlt = null, calendarNewTab = false, portalHref = null, onStartOver, now = Date.now(), layout = 'card', directionsHref = null, freeChangeUntilTs = null, format, labels, classNames, components, }) {
72
84
  const { clock } = format;
73
85
  if (layout === 'hero') {
74
- return (_jsx(HeroConfirmation, { lines: rawLines, startTs: startTs, totalOre: totalOre, address: address, calendarHref: calendarHref, calendarFileName: calendarFileName, portalHref: portalHref, onStartOver: onStartOver, now: now, directionsHref: directionsHref, freeChangeUntilTs: freeChangeUntilTs, format: format, labels: labels, classNames: classNames, components: components }));
86
+ return (_jsx(HeroConfirmation, { lines: rawLines, startTs: startTs, totalOre: totalOre, address: address, calendarHref: calendarHref, calendarFileName: calendarFileName, calendarAlt: calendarAlt, calendarNewTab: calendarNewTab, portalHref: portalHref, onStartOver: onStartOver, now: now, directionsHref: directionsHref, freeChangeUntilTs: freeChangeUntilTs, format: format, labels: labels, classNames: classNames, components: components }));
75
87
  }
76
88
  // Cleaned here as well as upstream: a restored confirmation carries whatever
77
89
  // name was resolved when it was stored.
@@ -96,7 +108,7 @@ export function Confirmation({ lines: rawLines, startTs, totalOre, address = nul
96
108
  // This line's own price, which for one person is the whole total.
97
109
  format.price(first.priceOre),
98
110
  ], SEPARATOR) })) : null;
99
- return (_jsxs("section", { "aria-labelledby": "booking-confirmed-heading", className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), lines.length > 1 ? (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })) : (singleCard), address && (_jsx("p", { className: slotClass(classNames, 'address', 'text-sm text-muted-foreground'), children: address })), _jsxs("div", { className: "flex flex-wrap gap-4", children: [calendarHref && (_jsx("a", { href: calendarHref, download: calendarFileName, className: linkClass, children: labels['confirmation.calendar'] })), lines.length === 1 && first?.manageHref && (_jsx("a", { href: first.manageHref, className: linkClass, children: labels['confirmation.manage'] }))] }), portalHref && (_jsx("p", { className: "text-sm", children: _jsx("a", { href: portalHref, className: linkClass, children: labels['confirmation.portal'] }) })), onStartOver && (_jsx("div", { children: _jsx(BookingButton, { variant: "outline", className: slotClass(classNames, 'startOver'), onClick: onStartOver, children: labels['confirmation.startOver'] }) }))] }));
111
+ return (_jsxs("section", { "aria-labelledby": "booking-confirmed-heading", className: slotClass(classNames, 'root', 'space-y-6'), children: [_jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), lines.length > 1 ? (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })) : (singleCard), address && (_jsx("p", { className: slotClass(classNames, 'address', 'text-sm text-muted-foreground'), children: address })), _jsxs("div", { className: "flex flex-wrap gap-4", children: [calendarHref && (_jsx("a", { ...calendarLinkProps(calendarHref, calendarFileName, calendarNewTab), className: linkClass, children: labels['confirmation.calendar'] })), calendarAlt && (_jsx("a", { ...calendarLinkProps(calendarAlt.href, calendarFileName, calendarAlt.newTab), className: linkClass, children: calendarAlt.label })), lines.length === 1 && first?.manageHref && (_jsx("a", { href: first.manageHref, className: linkClass, children: labels['confirmation.manage'] }))] }), portalHref && (_jsx("p", { className: "text-sm", children: _jsx("a", { href: portalHref, className: linkClass, children: labels['confirmation.portal'] }) })), onStartOver && (_jsx("div", { children: _jsx(BookingButton, { variant: "outline", className: slotClass(classNames, 'startOver'), onClick: onStartOver, children: labels['confirmation.startOver'] }) }))] }));
100
112
  }
101
113
  /**
102
114
  * The family card: one line per person in the order they sit down, each with
@@ -139,7 +151,7 @@ function visitMinutes(item) {
139
151
  * booked, and the two things a parent does next — put it in the calendar and
140
152
  * find the way — as buttons rather than links.
141
153
  */
142
- function HeroConfirmation({ lines: rawLines, startTs, totalOre, address, calendarHref, calendarFileName, portalHref, onStartOver, now, directionsHref, freeChangeUntilTs, format, labels, classNames, components, }) {
154
+ function HeroConfirmation({ lines: rawLines, startTs, totalOre, address, calendarHref, calendarFileName, calendarAlt, calendarNewTab, portalHref, onStartOver, now, directionsHref, freeChangeUntilTs, format, labels, classNames, components, }) {
143
155
  const { clock } = format;
144
156
  const lines = rawLines.map((line) => ({
145
157
  ...line,
@@ -201,11 +213,11 @@ function HeroConfirmation({ lines: rawLines, startTs, totalOre, address, calenda
201
213
  ? fillLabel(label('confirmation.leafTime'), {
202
214
  time: clock.formatTime(startTs),
203
215
  })
204
- : clock.formatTime(startTs) })] }), _jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), _jsx("p", { className: "sr-only", children: when }), label('confirmation.sent') && (_jsx("p", { className: "text-sm text-muted-foreground", children: renderLabel(label('confirmation.sent')) }))] }), plainLine, lines.length > 1 && (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })), rows.length > 0 && (_jsx("dl", { className: slotClass(classNames, 'card', 'divide-y divide-border rounded-lg border border-border bg-card px-5'), children: rows.map((row) => (_jsxs("div", { className: "flex items-center gap-4 py-3 text-sm", children: [_jsx("dt", { className: "w-16 shrink-0 text-muted-foreground", children: renderLabel(row.name) }), _jsx("dd", { className: "min-w-0 font-medium", children: row.value })] }, labelText(row.name)))) })), (calendarHref || (directionsHref && label('confirmation.directions'))) && (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [calendarHref && (_jsxs("a", { href: calendarHref, download: calendarFileName, className: bookingButtonClass({ size: 'lg', className: 'gap-2' }), children: [_jsx(CalendarPlus, { "aria-hidden": "true", className: "size-4" }), labels['confirmation.calendar']] })), directionsHref && label('confirmation.directions') && (_jsxs("a", { href: directionsHref, target: "_blank", rel: "noopener", className: bookingButtonClass({
216
+ : clock.formatTime(startTs) })] }), _jsx("h2", { id: "booking-confirmed-heading", tabIndex: -1, className: slotClass(classNames, 'heading', 'font-sans text-2xl font-bold outline-none md:text-3xl'), children: labels['confirmation.heading'] }), _jsx("p", { className: "sr-only", children: when }), label('confirmation.sent') && (_jsx("p", { className: "text-sm text-muted-foreground", children: renderLabel(label('confirmation.sent')) }))] }), plainLine, lines.length > 1 && (_jsx(PartyCard, { lines: lines, totalOre: totalOre, now: now, format: format, labels: labels, classNames: classNames, PartyLine: components?.PartyLine ?? DefaultPartyLine })), rows.length > 0 && (_jsx("dl", { className: slotClass(classNames, 'card', 'divide-y divide-border rounded-lg border border-border bg-card px-5'), children: rows.map((row) => (_jsxs("div", { className: "flex items-center gap-4 py-3 text-sm", children: [_jsx("dt", { className: "w-16 shrink-0 text-muted-foreground", children: renderLabel(row.name) }), _jsx("dd", { className: "min-w-0 font-medium", children: row.value })] }, labelText(row.name)))) })), (calendarHref || (directionsHref && label('confirmation.directions'))) && (_jsxs("div", { className: "grid grid-cols-2 gap-2", children: [calendarHref && (_jsxs("a", { ...calendarLinkProps(calendarHref, calendarFileName, calendarNewTab), className: bookingButtonClass({ size: 'lg', className: 'gap-2' }), children: [_jsx(CalendarPlus, { "aria-hidden": "true", className: "size-4" }), labels['confirmation.calendar']] })), directionsHref && label('confirmation.directions') && (_jsxs("a", { href: directionsHref, target: "_blank", rel: "noopener", className: bookingButtonClass({
205
217
  variant: 'outline',
206
218
  size: 'lg',
207
219
  className: cn('gap-2', !calendarHref && 'col-span-2'),
208
- }), children: [_jsx(MapPin, { "aria-hidden": "true", className: "size-4" }), renderLabel(label('confirmation.directions'))] }))] })), label('confirmation.tip.heading') && label('confirmation.tip.body') && (_jsxs("div", { className: "rounded-lg bg-muted px-5 py-4 text-sm", children: [_jsx("p", { className: "font-semibold", children: renderLabel(label('confirmation.tip.heading')) }), _jsx("p", { className: "text-muted-foreground", children: renderLabel(label('confirmation.tip.body')) })] })), lines.length === 1 && first?.manageHref && (_jsxs("p", { className: "text-center text-sm", children: [_jsx("a", { href: first.manageHref, className: linkClass, children: labels['confirmation.manage'] }), manageUntil && freeChangeUntilTs !== null && freeChangeUntilTs !== undefined && (_jsxs("span", { className: "text-muted-foreground", children: [SEPARATOR, fillLabel(manageUntil, {
220
+ }), children: [_jsx(MapPin, { "aria-hidden": "true", className: "size-4" }), renderLabel(label('confirmation.directions'))] }))] })), calendarAlt && (_jsx("p", { className: "-mt-2 text-center text-sm", children: _jsx("a", { ...calendarLinkProps(calendarAlt.href, calendarFileName, calendarAlt.newTab), className: linkClass, children: calendarAlt.label }) })), label('confirmation.tip.heading') && label('confirmation.tip.body') && (_jsxs("div", { className: "rounded-lg bg-muted px-5 py-4 text-sm", children: [_jsx("p", { className: "font-semibold", children: renderLabel(label('confirmation.tip.heading')) }), _jsx("p", { className: "text-muted-foreground", children: renderLabel(label('confirmation.tip.body')) })] })), lines.length === 1 && first?.manageHref && (_jsxs("p", { className: "text-center text-sm", children: [_jsx("a", { href: first.manageHref, className: linkClass, children: labels['confirmation.manage'] }), manageUntil && freeChangeUntilTs !== null && freeChangeUntilTs !== undefined && (_jsxs("span", { className: "text-muted-foreground", children: [SEPARATOR, fillLabel(manageUntil, {
209
221
  day: clock.dayLabel(freeChangeUntilTs, now),
210
222
  time: clock.formatTime(freeChangeUntilTs),
211
223
  })] }))] })), portalHref && (_jsx("p", { className: "text-center text-sm", children: _jsx("a", { href: portalHref, className: linkClass, children: labels['confirmation.portal'] }) })), onStartOver && (_jsx("div", { className: "flex justify-center", children: _jsx(BookingButton, { variant: "outline", className: slotClass(classNames, 'startOver'), onClick: onStartOver, children: labels['confirmation.startOver'] }) }))] }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medalsocial/meda",
3
- "version": "3.11.0",
3
+ "version": "3.12.0",
4
4
  "description": "Shared Meda UI shell and runtime package.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {