@flamingo-stack/openframe-frontend-core 0.0.675 → 0.0.676-2437.7317.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.675",
3
+ "version": "0.0.676-2437.7317.1",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -164,4 +164,25 @@ describe('HubSpotMeetingScheduler — which links fall back to HubSpot', () => {
164
164
  expect(screen.queryByText(BOOKED_ON_HUBSPOT)).not.toBeInTheDocument();
165
165
  expect(screen.queryByLabelText(/^Upload/)).not.toBeInTheDocument();
166
166
  });
167
+ it('fieldCopy replaces a field label and placeholder, and derives the one it is not given', async () => {
168
+ availability.formFields = [];
169
+ render(
170
+ scheduler({
171
+ detailsFormProps: {
172
+ fieldCopy: {
173
+ email: { label: 'Business Email', placeholder: 'username@company.com' },
174
+ firstName: { label: 'Given Name' },
175
+ },
176
+ },
177
+ }),
178
+ );
179
+ const email = await screen.findByLabelText(/^Business Email/);
180
+ expect(email).toHaveAttribute('placeholder', 'username@company.com');
181
+ // A label alone still moves the placeholder the TYPE derives from it.
182
+ expect(screen.getByLabelText(/^Given Name/)).toHaveAttribute('placeholder', 'Enter Given Name');
183
+ // The overridden field keeps everything else: its own label is gone, and the
184
+ // untouched one is drawn exactly as the widget declares it.
185
+ expect(screen.queryByLabelText(/^Email/)).not.toBeInTheDocument();
186
+ expect(screen.getByLabelText(/^Last Name/)).toHaveAttribute('placeholder', 'Enter Last Name');
187
+ });
167
188
  });
@@ -60,6 +60,21 @@ export type BookingFieldSpan = keyof typeof SPAN_CLASS;
60
60
 
61
61
  export type BookingFieldRow = BookingFieldSlot[];
62
62
 
63
+ /**
64
+ * Display copy a HOST overrides for ONE field, keyed by its name — a built-in
65
+ * (`email`) or a HubSpot-declared question. It replaces what is DRAWN and
66
+ * nothing else: the name the answer rides under, the validation and the
67
+ * schema's own messages are untouched, so a campaign page can ask for a
68
+ * "Business Email" without owning a second email rule.
69
+ *
70
+ * An overridden `label` also feeds the type's derived placeholder
71
+ * (`Enter <label>`), so overriding the label alone keeps the pair consistent.
72
+ */
73
+ export interface BookingFieldCopy {
74
+ label?: string;
75
+ placeholder?: string;
76
+ }
77
+
63
78
  /**
64
79
  * A HOST-supplied consent row — the block the waitlist form draws for its SMS
65
80
  * consent, here for "I agree to the Privacy Policy and to be contacted". It is
@@ -365,6 +380,8 @@ export interface BookingFormProps {
365
380
  * appended full width. Both are deliberate — see `slotNode`/`unplacedFields`.
366
381
  */
367
382
  fieldRows?: BookingFieldRow[];
383
+ /** Per-field display overrides, keyed by field name — see `BookingFieldCopy`. */
384
+ fieldCopy?: Record<string, BookingFieldCopy>;
368
385
  /** Host-supplied consent row, rendered after the fields — see `BookingFormConsent`. */
369
386
  consent?: BookingFormConsent;
370
387
  isSubmitting: boolean;
@@ -404,6 +421,7 @@ export function BookingForm({
404
421
  submitLabel,
405
422
  footerNote,
406
423
  fieldRows,
424
+ fieldCopy,
407
425
  consent,
408
426
  isSubmitting,
409
427
  onSubmit,
@@ -546,18 +564,32 @@ export function BookingForm({
546
564
  const renderField = (
547
565
  field: ControlArgs['field'],
548
566
  where: { id: string; registerName: string; error?: string },
549
- ): ReactNode => (
550
- <FieldWrapper key={field.name} label={field.label} htmlFor={where.id} required={field.required} error={where.error}>
551
- {FIELD_CONTROLS[field.type]({
552
- field,
553
- id: where.id,
554
- registerName: where.registerName,
555
- error: where.error,
556
- register,
557
- control,
558
- })}
559
- </FieldWrapper>
560
- );
567
+ ): ReactNode => {
568
+ // DISPLAY only: the host's copy replaces what this control draws, never the
569
+ // name the answer registers under, the validation, or the schema's messages.
570
+ const copy = fieldCopy?.[field.name];
571
+ const shown = copy
572
+ ? { ...field, label: copy.label ?? field.label, placeholder: copy.placeholder ?? field.placeholder }
573
+ : field;
574
+ return (
575
+ <FieldWrapper
576
+ key={shown.name}
577
+ label={shown.label}
578
+ htmlFor={where.id}
579
+ required={shown.required}
580
+ error={where.error}
581
+ >
582
+ {FIELD_CONTROLS[shown.type]({
583
+ field: shown,
584
+ id: where.id,
585
+ registerName: where.registerName,
586
+ error: where.error,
587
+ register,
588
+ control,
589
+ })}
590
+ </FieldWrapper>
591
+ );
592
+ };
561
593
 
562
594
  const builtInFields: Record<string, ReactNode> = Object.fromEntries(
563
595
  BUILT_IN_BOOKING_FIELDS.map(field => [
@@ -147,11 +147,12 @@ export interface HubSpotMeetingSchedulerProps {
147
147
  */
148
148
  detailsForm?: ComponentType<BookingFormProps>;
149
149
  /**
150
- * The DATA form of the same override: `fieldRows` and a host consent row,
151
- * spread onto whichever form renders. Serialisable, so a Server Component
152
- * can pass it across the RSC boundary where a component cannot.
150
+ * The DATA form of the same override: `fieldRows`, a host consent row and
151
+ * per-field display copy, spread onto whichever form renders. Serialisable,
152
+ * so a Server Component can pass it across the RSC boundary where a component
153
+ * cannot.
153
154
  */
154
- detailsFormProps?: Pick<BookingFormProps, 'fieldRows' | 'consent'>;
155
+ detailsFormProps?: Pick<BookingFormProps, 'fieldRows' | 'consent' | 'fieldCopy'>;
155
156
  /** Form rescue for the details form (`RESCUE_FORMS.meetingBooking`). OPT-IN:
156
157
  * omitted or `null` saves nothing. */
157
158
  rescue?: FormRescueDefinition | null;
@@ -986,6 +987,7 @@ export {
986
987
  type BookingFormProps,
987
988
  type BookingFieldRow,
988
989
  type BookingFieldSlot,
990
+ type BookingFieldCopy,
989
991
  type BookingFormConsent,
990
992
  } from './booking-form';
991
993