@medalsocial/meda 3.9.1 → 3.10.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.
@@ -131,6 +131,13 @@ export interface WhoScreenProps {
131
131
  adult: WizardPerson & {
132
132
  adult: true;
133
133
  };
134
+ /**
135
+ * Start with nobody, rather than one child: for a salon where a grown-up
136
+ * booking for themselves is as common as a child. Children go down to 0
137
+ * and the grown-up's row is a free choice either way; «next» waits for an
138
+ * answer. Default `false`.
139
+ */
140
+ startEmpty?: boolean;
134
141
  };
135
142
  /** Shown under the heading for a guest — e.g. an offer to log in. */
136
143
  loginRow?: ReactNode;
@@ -142,7 +142,8 @@ export function WhoScreen({ labels, format, people, family, guestChoices, addedC
142
142
  * A guest's party: how many children (a stepper) and whether the grown-up
143
143
  * comes too (a check row), so «a child and me» is one booking without an
144
144
  * account. Starts at one child, which is the common answer, so «next» is live
145
- * from the first frame.
145
+ * from the first frame — or, with `startEmpty`, at nobody, so a grown-up
146
+ * booking for themselves is one tap like anyone else.
146
147
  */
147
148
  function GuestParty({ party, people, maxPeople, labels, classNames, onChoose, }) {
148
149
  const adultOn = people.some((person) => person.key === party.adult.key);
@@ -154,8 +155,10 @@ function GuestParty({ party, people, maxPeople, labels, classNames, onChoose, })
154
155
  // stepper adds and removes the unnamed ones after them.
155
156
  const generatedKeys = new Set(Array.from({ length: maxPeople }, (_, index) => party.child(index + 1).key));
156
157
  const named = childSeats.filter((person) => !generatedKeys.has(person.key));
158
+ const startEmpty = party.startEmpty === true;
157
159
  // The stepper never takes a named child away: their own card does that.
158
- const floor = Math.max(named.length, adultOn ? 0 : 1);
160
+ // Without `startEmpty` it never empties the booking either.
161
+ const floor = Math.max(named.length, adultOn || startEmpty ? 0 : 1);
159
162
  const answer = (count, withAdult) => {
160
163
  const kept = named;
161
164
  const used = new Set(kept.map((person) => person.key));
@@ -174,9 +177,11 @@ function GuestParty({ party, people, maxPeople, labels, classNames, onChoose, })
174
177
  const wasEmpty = useRef(false);
175
178
  useEffect(() => {
176
179
  const empty = people.length === 0;
177
- if (empty && !wasEmpty.current)
180
+ if (empty && !wasEmpty.current && !startEmpty)
178
181
  answer(1, false);
179
- wasEmpty.current = empty;
182
+ // An empty answer `startEmpty` left alone is not one already seen: if the
183
+ // flag goes, the child is still filled in.
184
+ wasEmpty.current = empty && !startEmpty;
180
185
  });
181
186
  const text = (key, values = {}) => {
182
187
  const label = labels[key];
@@ -184,8 +189,9 @@ function GuestParty({ party, people, maxPeople, labels, classNames, onChoose, })
184
189
  };
185
190
  const stepButton = 'flex size-11 shrink-0 items-center justify-center rounded-full border border-border bg-background text-foreground transition-colors hover:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-40';
186
191
  return (_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: cn('flex items-center gap-4 rounded-lg border border-border bg-card px-5 py-4', classNames?.card), children: [_jsxs("div", { className: "min-w-0 flex-1", children: [_jsx("p", { className: "font-semibold", children: text('who.party.children') }), labels['who.party.childrenNote'] && (_jsx("p", { className: "text-sm text-muted-foreground", children: renderLabel(labels['who.party.childrenNote']) }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx("button", { type: "button", "aria-label": text('who.party.fewer'), disabled: children <= floor, onClick: () => answer(children - 1, adultOn), className: stepButton, children: _jsx(Minus, { "aria-hidden": "true", className: "size-4" }) }), _jsxs("span", { "aria-live": "polite", "aria-atomic": "true", className: "w-6 text-center text-lg font-bold tabular-nums", children: [_jsx("span", { "aria-hidden": "true", children: children }), _jsx("span", { className: "sr-only", children: text('who.party.count', { count: children }) })] }), _jsx("button", { type: "button", "aria-label": text('who.party.more'), "aria-describedby": full ? LIMIT_ID : undefined, disabled: full, onClick: () => answer(children + 1, adultOn), className: stepButton, children: _jsx(Plus, { "aria-hidden": "true", className: "size-4" }) })] })] }), _jsxs(CheckRow, { checked: adultOn,
187
- // Never down to nobody: the last person in the booking stays.
188
- disabled: (!adultOn && full) || (adultOn && children === 0), describedBy: !adultOn && full ? LIMIT_ID : undefined, onToggle: () => answer(children, !adultOn), className: cn('min-h-14 justify-between px-5 py-3 text-sm', classNames?.card), selectedClassName: classNames?.cardSelected, children: [_jsxs("span", { className: "flex min-w-0 flex-col text-left", children: [_jsx("span", { className: "font-medium", children: text('who.party.adult') }), labels['who.party.adultNote'] && (_jsx("span", { className: "text-muted-foreground", children: renderLabel(labels['who.party.adultNote']) }))] }), _jsx(CheckCircle, { checked: adultOn })] }), _jsx("p", { id: LIMIT_ID, "aria-live": "polite", className: slotClass(classNames, 'limit', 'min-h-5 text-sm text-muted-foreground'), children: full ? renderLabel(labels['who.family.limit'], { max: maxPeople }) : '' })] }));
192
+ // Never down to nobody: the last person in the booking stays — unless
193
+ // the booking may start (and so stand) empty.
194
+ disabled: (!adultOn && full) || (adultOn && children === 0 && !startEmpty), describedBy: !adultOn && full ? LIMIT_ID : undefined, onToggle: () => answer(children, !adultOn), className: cn('min-h-14 justify-between px-5 py-3 text-sm', classNames?.card), selectedClassName: classNames?.cardSelected, children: [_jsxs("span", { className: "flex min-w-0 flex-col text-left", children: [_jsx("span", { className: "font-medium", children: text('who.party.adult') }), labels['who.party.adultNote'] && children > 0 && (_jsx("span", { className: "text-muted-foreground", children: renderLabel(labels['who.party.adultNote']) }))] }), _jsx(CheckCircle, { checked: adultOn })] }), _jsx("p", { id: LIMIT_ID, "aria-live": "polite", className: slotClass(classNames, 'limit', 'min-h-5 text-sm text-muted-foreground'), children: full ? renderLabel(labels['who.family.limit'], { max: maxPeople }) : '' })] }));
189
195
  }
190
196
  /**
191
197
  * The guest chips as a radio group. Roving focus: one chip is in the tab
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medalsocial/meda",
3
- "version": "3.9.1",
3
+ "version": "3.10.0",
4
4
  "description": "Shared Meda UI shell and runtime package.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {