@flamingo-stack/openframe-frontend-core 0.0.584 → 0.0.585-snapshot.20260903083645

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.
Files changed (177) hide show
  1. package/dist/{chunk-3ELW4VML.js → chunk-2Y5UT5VF.js} +3 -3
  2. package/dist/{chunk-YFZDIEVC.js → chunk-3HYHYKNL.js} +6 -6
  3. package/dist/{chunk-5CJKZHWO.js → chunk-3JVYM4ZN.js} +5 -5
  4. package/dist/{chunk-P2VRLSAP.cjs → chunk-3WR5R6H6.cjs} +21 -21
  5. package/dist/{chunk-P2VRLSAP.cjs.map → chunk-3WR5R6H6.cjs.map} +1 -1
  6. package/dist/{chunk-UCNEQKIT.cjs → chunk-4VZFJKPT.cjs} +5 -5
  7. package/dist/{chunk-UCNEQKIT.cjs.map → chunk-4VZFJKPT.cjs.map} +1 -1
  8. package/dist/{chunk-VEHZNQ77.cjs → chunk-5LUPIYPH.cjs} +30 -30
  9. package/dist/{chunk-VEHZNQ77.cjs.map → chunk-5LUPIYPH.cjs.map} +1 -1
  10. package/dist/{chunk-TLKHLGAB.js → chunk-5OIXAV4P.js} +3 -3
  11. package/dist/{chunk-LNKULR4V.js → chunk-6EZGJZVH.js} +2 -2
  12. package/dist/{chunk-JWKAJTNA.cjs → chunk-6ZNWY6WW.cjs} +28 -28
  13. package/dist/{chunk-JWKAJTNA.cjs.map → chunk-6ZNWY6WW.cjs.map} +1 -1
  14. package/dist/{chunk-QJR32JM2.cjs → chunk-A6ETQEWK.cjs} +615 -485
  15. package/dist/chunk-A6ETQEWK.cjs.map +1 -0
  16. package/dist/{chunk-JFKK3ZOA.cjs → chunk-BMORFCT2.cjs} +57 -57
  17. package/dist/{chunk-JFKK3ZOA.cjs.map → chunk-BMORFCT2.cjs.map} +1 -1
  18. package/dist/{chunk-33DHEEMP.cjs → chunk-C3MRLZUI.cjs} +33 -33
  19. package/dist/{chunk-33DHEEMP.cjs.map → chunk-C3MRLZUI.cjs.map} +1 -1
  20. package/dist/{chunk-XWBND4G5.js → chunk-D7YISHXC.js} +17 -2
  21. package/dist/{chunk-XWBND4G5.js.map → chunk-D7YISHXC.js.map} +1 -1
  22. package/dist/{chunk-EJKEDCTH.js → chunk-DQUEVRDU.js} +7 -7
  23. package/dist/{chunk-HCKEDTKJ.cjs → chunk-EAYMX22G.cjs} +15 -15
  24. package/dist/{chunk-HCKEDTKJ.cjs.map → chunk-EAYMX22G.cjs.map} +1 -1
  25. package/dist/{chunk-NNFQLTJ3.cjs → chunk-EMF64FWA.cjs} +31 -5
  26. package/dist/chunk-EMF64FWA.cjs.map +1 -0
  27. package/dist/{chunk-IKUSKKFZ.cjs → chunk-G45RGWHF.cjs} +159 -144
  28. package/dist/chunk-G45RGWHF.cjs.map +1 -0
  29. package/dist/{chunk-TOUD3AGK.js → chunk-KLWJRGNN.js} +5 -5
  30. package/dist/{chunk-MYEG5ZDT.js → chunk-L4WLYGB4.js} +3 -3
  31. package/dist/{chunk-VZSTL5VG.js → chunk-LV6CORVJ.js} +3 -3
  32. package/dist/{chunk-MEJFHD5N.cjs → chunk-M36XSGZV.cjs} +19 -19
  33. package/dist/{chunk-MEJFHD5N.cjs.map → chunk-M36XSGZV.cjs.map} +1 -1
  34. package/dist/{chunk-MCWAZ5XI.js → chunk-NFWAL6UV.js} +2 -2
  35. package/dist/{chunk-3ZA2OYRE.cjs → chunk-NISAGFEM.cjs} +3 -3
  36. package/dist/{chunk-3ZA2OYRE.cjs.map → chunk-NISAGFEM.cjs.map} +1 -1
  37. package/dist/{chunk-7X5TCK4R.cjs → chunk-NYLWMAHP.cjs} +10 -10
  38. package/dist/{chunk-7X5TCK4R.cjs.map → chunk-NYLWMAHP.cjs.map} +1 -1
  39. package/dist/{chunk-RURZ6QLN.js → chunk-OZNFPQT5.js} +3 -3
  40. package/dist/{chunk-VOAZU46B.cjs → chunk-P2CKLIOM.cjs} +80 -80
  41. package/dist/{chunk-VOAZU46B.cjs.map → chunk-P2CKLIOM.cjs.map} +1 -1
  42. package/dist/{chunk-KP6VJP36.js → chunk-PLMJVA2H.js} +4 -4
  43. package/dist/{chunk-JAJQ5P2Q.cjs → chunk-QZYYNBMI.cjs} +28 -28
  44. package/dist/{chunk-JAJQ5P2Q.cjs.map → chunk-QZYYNBMI.cjs.map} +1 -1
  45. package/dist/{chunk-4T5KOD3G.js → chunk-S7YXLZNG.js} +6 -6
  46. package/dist/{chunk-Q66AMADB.cjs → chunk-SSGM2NKA.cjs} +6 -6
  47. package/dist/{chunk-Q66AMADB.cjs.map → chunk-SSGM2NKA.cjs.map} +1 -1
  48. package/dist/{chunk-IIT5HWLY.cjs → chunk-V7ZJTUV2.cjs} +11 -11
  49. package/dist/{chunk-IIT5HWLY.cjs.map → chunk-V7ZJTUV2.cjs.map} +1 -1
  50. package/dist/{chunk-3LMYQGAJ.js → chunk-X7YSF6A3.js} +28 -2
  51. package/dist/chunk-X7YSF6A3.js.map +1 -0
  52. package/dist/{chunk-ZHUBNWEX.cjs → chunk-XMK24INA.cjs} +6 -6
  53. package/dist/{chunk-ZHUBNWEX.cjs.map → chunk-XMK24INA.cjs.map} +1 -1
  54. package/dist/{chunk-XPJKQ63S.js → chunk-XMTGLQSD.js} +3 -3
  55. package/dist/{chunk-6BJYPG2V.js → chunk-YEQAA6I4.js} +4 -4
  56. package/dist/{chunk-JVWRMLQZ.js → chunk-YYC7V7YC.js} +2059 -1929
  57. package/dist/chunk-YYC7V7YC.js.map +1 -0
  58. package/dist/components/case-studies/index.cjs +10 -10
  59. package/dist/components/case-studies/index.js +4 -4
  60. package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
  61. package/dist/components/chat/index.cjs +3 -3
  62. package/dist/components/chat/index.js +2 -2
  63. package/dist/components/contact/index.cjs +5 -5
  64. package/dist/components/contact/index.js +4 -4
  65. package/dist/components/docs/index.cjs +8 -8
  66. package/dist/components/docs/index.js +7 -7
  67. package/dist/components/embeds/index.cjs +5 -5
  68. package/dist/components/embeds/index.js +4 -4
  69. package/dist/components/faq/index.cjs +5 -5
  70. package/dist/components/faq/index.js +4 -4
  71. package/dist/components/features/auth/account-details-fields.d.ts +34 -0
  72. package/dist/components/features/auth/account-details-fields.d.ts.map +1 -0
  73. package/dist/components/features/auth/complete-account-form.d.ts.map +1 -1
  74. package/dist/components/features/auth/create-organization-form.d.ts +43 -6
  75. package/dist/components/features/auth/create-organization-form.d.ts.map +1 -1
  76. package/dist/components/features/auth/index.d.ts +2 -0
  77. package/dist/components/features/auth/index.d.ts.map +1 -1
  78. package/dist/components/features/auth/labeled-divider.d.ts +6 -0
  79. package/dist/components/features/auth/labeled-divider.d.ts.map +1 -0
  80. package/dist/components/features/auth/login-form.d.ts +32 -9
  81. package/dist/components/features/auth/login-form.d.ts.map +1 -1
  82. package/dist/components/features/auth/openframe-sso-signup-form.d.ts.map +1 -1
  83. package/dist/components/features/auth/sso-providers.d.ts +1 -1
  84. package/dist/components/features/auth/sso-providers.d.ts.map +1 -1
  85. package/dist/components/features/index.cjs +8 -4
  86. package/dist/components/features/index.cjs.map +1 -1
  87. package/dist/components/features/index.js +7 -3
  88. package/dist/components/features/media-gallery-manager.d.ts.map +1 -1
  89. package/dist/components/features/push-button-selector.d.ts +9 -7
  90. package/dist/components/features/push-button-selector.d.ts.map +1 -1
  91. package/dist/components/features/release-media-manager.d.ts.map +1 -1
  92. package/dist/components/features/video.d.ts +8 -0
  93. package/dist/components/features/video.d.ts.map +1 -1
  94. package/dist/components/help-center-pages/index.cjs +24 -24
  95. package/dist/components/help-center-pages/index.js +15 -15
  96. package/dist/components/index.cjs +57 -51
  97. package/dist/components/index.cjs.map +1 -1
  98. package/dist/components/index.js +21 -15
  99. package/dist/components/index.js.map +1 -1
  100. package/dist/components/loading/unified-skeleton.d.ts.map +1 -1
  101. package/dist/components/meeting-scheduler/index.cjs +31 -31
  102. package/dist/components/meeting-scheduler/index.js +3 -3
  103. package/dist/components/navigation/index.cjs +7 -7
  104. package/dist/components/navigation/index.js +6 -6
  105. package/dist/components/onboarding-guides/index.cjs +7 -7
  106. package/dist/components/onboarding-guides/index.js +6 -6
  107. package/dist/components/related-content/index.cjs +3 -3
  108. package/dist/components/related-content/index.js +2 -2
  109. package/dist/components/tickets/index.cjs +8 -8
  110. package/dist/components/tickets/index.js +7 -7
  111. package/dist/components/ui/index.cjs +6 -4
  112. package/dist/components/ui/index.cjs.map +1 -1
  113. package/dist/components/ui/index.d.ts +1 -0
  114. package/dist/components/ui/index.d.ts.map +1 -1
  115. package/dist/components/ui/index.js +5 -3
  116. package/dist/components/ui/lazy-mount.d.ts +21 -0
  117. package/dist/components/ui/lazy-mount.d.ts.map +1 -0
  118. package/dist/index.cjs +24 -8
  119. package/dist/index.cjs.map +1 -1
  120. package/dist/index.js +23 -7
  121. package/dist/index.js.map +1 -1
  122. package/dist/types/marketing.d.ts +1 -1
  123. package/dist/types/marketing.d.ts.map +1 -1
  124. package/dist/utils/index.cjs +26 -0
  125. package/dist/utils/index.cjs.map +1 -1
  126. package/dist/utils/index.d.ts +1 -0
  127. package/dist/utils/index.d.ts.map +1 -1
  128. package/dist/utils/index.js +22 -1
  129. package/dist/utils/index.js.map +1 -1
  130. package/dist/utils/media-type.d.ts +26 -0
  131. package/dist/utils/media-type.d.ts.map +1 -0
  132. package/package.json +1 -1
  133. package/src/components/chat/entity-cards/program-card.tsx +2 -2
  134. package/src/components/features/auth/account-details-fields.tsx +124 -0
  135. package/src/components/features/auth/complete-account-form.tsx +13 -52
  136. package/src/components/features/auth/create-organization-form.tsx +128 -38
  137. package/src/components/features/auth/index.ts +2 -0
  138. package/src/components/features/auth/labeled-divider.tsx +16 -0
  139. package/src/components/features/auth/login-form.tsx +102 -23
  140. package/src/components/features/auth/openframe-sso-signup-form.tsx +11 -43
  141. package/src/components/features/auth/sso-providers.tsx +3 -3
  142. package/src/components/features/media-gallery-manager.tsx +4 -3
  143. package/src/components/features/push-button-selector.tsx +15 -12
  144. package/src/components/features/release-media-manager.tsx +3 -2
  145. package/src/components/features/video.tsx +28 -0
  146. package/src/components/loading/__tests__/unified-skeleton.test.tsx +16 -0
  147. package/src/components/loading/unified-skeleton.tsx +5 -1
  148. package/src/components/ui/__tests__/lazy-mount.test.tsx +46 -0
  149. package/src/components/ui/index.ts +3 -0
  150. package/src/components/ui/lazy-mount.tsx +38 -0
  151. package/src/stories/auth/Login.stories.tsx +15 -5
  152. package/src/types/.marketing.md +0 -1
  153. package/src/types/marketing.ts +4 -2
  154. package/src/utils/__tests__/media-type.test.ts +30 -0
  155. package/src/utils/index.ts +3 -0
  156. package/src/utils/media-type.ts +39 -0
  157. package/src/utils/vendor-media-stub.ts +7 -7
  158. package/dist/chunk-3LMYQGAJ.js.map +0 -1
  159. package/dist/chunk-IKUSKKFZ.cjs.map +0 -1
  160. package/dist/chunk-JVWRMLQZ.js.map +0 -1
  161. package/dist/chunk-NNFQLTJ3.cjs.map +0 -1
  162. package/dist/chunk-QJR32JM2.cjs.map +0 -1
  163. /package/dist/{chunk-3ELW4VML.js.map → chunk-2Y5UT5VF.js.map} +0 -0
  164. /package/dist/{chunk-YFZDIEVC.js.map → chunk-3HYHYKNL.js.map} +0 -0
  165. /package/dist/{chunk-5CJKZHWO.js.map → chunk-3JVYM4ZN.js.map} +0 -0
  166. /package/dist/{chunk-TLKHLGAB.js.map → chunk-5OIXAV4P.js.map} +0 -0
  167. /package/dist/{chunk-LNKULR4V.js.map → chunk-6EZGJZVH.js.map} +0 -0
  168. /package/dist/{chunk-EJKEDCTH.js.map → chunk-DQUEVRDU.js.map} +0 -0
  169. /package/dist/{chunk-TOUD3AGK.js.map → chunk-KLWJRGNN.js.map} +0 -0
  170. /package/dist/{chunk-MYEG5ZDT.js.map → chunk-L4WLYGB4.js.map} +0 -0
  171. /package/dist/{chunk-VZSTL5VG.js.map → chunk-LV6CORVJ.js.map} +0 -0
  172. /package/dist/{chunk-MCWAZ5XI.js.map → chunk-NFWAL6UV.js.map} +0 -0
  173. /package/dist/{chunk-RURZ6QLN.js.map → chunk-OZNFPQT5.js.map} +0 -0
  174. /package/dist/{chunk-KP6VJP36.js.map → chunk-PLMJVA2H.js.map} +0 -0
  175. /package/dist/{chunk-4T5KOD3G.js.map → chunk-S7YXLZNG.js.map} +0 -0
  176. /package/dist/{chunk-XPJKQ63S.js.map → chunk-XMTGLQSD.js.map} +0 -0
  177. /package/dist/{chunk-6BJYPG2V.js.map → chunk-YEQAA6I4.js.map} +0 -0
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Media type — THE single source of truth for the `media_type` discriminator
3
+ * on media rows (ai_media, program media, vendor media). Zero imports (a leaf),
4
+ * so every consumer reads the same predicate instead of scattering
5
+ * `=== 'video'` literals. `MediaType` (types/marketing.ts) is the row union;
6
+ * these are the two values a post can publish.
7
+ */
8
+ export declare const MEDIA_TYPE: {
9
+ readonly IMAGE: "image";
10
+ readonly VIDEO: "video";
11
+ };
12
+ /** The two media types a post can publish. */
13
+ export type PublishableMediaType = (typeof MEDIA_TYPE)[keyof typeof MEDIA_TYPE];
14
+ interface MediaTyped {
15
+ media_type?: string | null;
16
+ }
17
+ export declare function isVideoMedia(media: MediaTyped | null | undefined): boolean;
18
+ export declare function isImageMedia(media: MediaTyped | null | undefined): boolean;
19
+ export declare function isPublishableMediaType(mediaType: string | null | undefined): mediaType is PublishableMediaType;
20
+ /** Split a media list into its videos and images, preserving order. */
21
+ export declare function partitionMediaByType<T extends MediaTyped>(media: readonly T[]): {
22
+ videos: T[];
23
+ images: T[];
24
+ };
25
+ export {};
26
+ //# sourceMappingURL=media-type.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"media-type.d.ts","sourceRoot":"","sources":["../../src/utils/media-type.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,eAAO,MAAM,UAAU;;;CAGb,CAAC;AAEX,8CAA8C;AAC9C,MAAM,MAAM,oBAAoB,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAEhF,UAAU,UAAU;IAClB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE1E;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE1E;AAED,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,SAAS,IAAI,oBAAoB,CAE9G;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,GAAG;IAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IAAC,MAAM,EAAE,CAAC,EAAE,CAAA;CAAE,CAK5G"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flamingo-stack/openframe-frontend-core",
3
- "version": "0.0.584",
3
+ "version": "0.0.585-snapshot.20260903083645",
4
4
  "description": "Shared design system and components for all Flamingo platforms",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -26,6 +26,7 @@ import { useState } from 'react';
26
26
  import Image from '../../../embed-shims/next-image';
27
27
  import { cn } from '../../../utils/cn';
28
28
  import { formatDurationCompact, formatTimeWithTimezone, formatDurationFromRange } from '../../../utils/format';
29
+ import { isImageMedia } from '../../../utils/media-type';
29
30
  import { Button } from '../../ui/button/button';
30
31
  import { ImageGalleryModal } from '../../ui/image-gallery-modal';
31
32
  import { SquareAvatar } from '../../ui/square-avatar';
@@ -51,7 +52,6 @@ import {
51
52
  import { EntityPortraitCard } from './entity-portrait-card';
52
53
  import { useEntityCardLink } from './use-entity-card-link';
53
54
  import { useEntityCardPlaceholder } from './use-entity-card-placeholder';
54
-
55
55
  type CardSize = 'default' | 'sm' | 'portrait';
56
56
 
57
57
  /**
@@ -273,7 +273,7 @@ export function ProgramCard<T extends BaseProgramItem>({
273
273
  aspect: size === 'sm' ? 'square' : 'wide',
274
274
  });
275
275
  const coverImage = item.cover_url;
276
- const images = media.filter(m => m.media_type === 'image');
276
+ const images = media.filter(m => isImageMedia(m));
277
277
  const hosts = getHosts(item.hosts);
278
278
  const accentColor = 'var(--color-accent-primary)';
279
279
  // `status` / `duration_seconds` / `location_name` / `start_at` … live on the
@@ -0,0 +1,124 @@
1
+ 'use client';
2
+
3
+ import type { KeyboardEvent } from 'react';
4
+ import { useDeferredError } from '../../../hooks/ui/use-deferred-error';
5
+ import { Input } from '../../ui/input';
6
+ import { PasswordInput } from '../../ui/password-input';
7
+
8
+ export interface AccountDetailsFieldsProps {
9
+ firstName: string;
10
+ lastName: string;
11
+ password: string;
12
+ confirmPassword: string;
13
+ onFirstNameChange: (value: string) => void;
14
+ onLastNameChange: (value: string) => void;
15
+ onPasswordChange: (value: string) => void;
16
+ onConfirmPasswordChange: (value: string) => void;
17
+ errors?: {
18
+ firstName?: string;
19
+ lastName?: string;
20
+ password?: string;
21
+ confirmPassword?: string;
22
+ };
23
+ disabled?: boolean;
24
+ onKeyDown?: (event: KeyboardEvent<HTMLInputElement>) => void;
25
+ /**
26
+ * `paired` puts the two names on one row and the two passwords on another; `stacked` gives each
27
+ * field its own row. Paired is unconditional, with no breakpoint — the approved design pairs
28
+ * them at ~514px and Tailwind's `sm` is 640px, which would stack them on every phone.
29
+ */
30
+ layout?: 'stacked' | 'paired';
31
+ }
32
+
33
+ /**
34
+ * The name-and-password block every account-creating form collects.
35
+ *
36
+ * Shared so the four fields cannot drift apart across the forms that render them — they already
37
+ * had, with one copy wiring `useDeferredError` for blur and another not, so the same field
38
+ * surfaced its error at a different moment depending on which screen you reached it from.
39
+ */
40
+ export function AccountDetailsFields({
41
+ firstName,
42
+ lastName,
43
+ password,
44
+ confirmPassword,
45
+ onFirstNameChange,
46
+ onLastNameChange,
47
+ onPasswordChange,
48
+ onConfirmPasswordChange,
49
+ errors,
50
+ disabled = false,
51
+ onKeyDown,
52
+ layout = 'stacked',
53
+ }: AccountDetailsFieldsProps) {
54
+ const firstNameErr = useDeferredError(errors?.firstName, firstName);
55
+ const lastNameErr = useDeferredError(errors?.lastName, lastName);
56
+ const passwordErr = useDeferredError(errors?.password, password);
57
+ const confirmErr = useDeferredError(errors?.confirmPassword, confirmPassword);
58
+
59
+ const names = [
60
+ <Input
61
+ key="firstName"
62
+ label="First Name"
63
+ placeholder="Enter First Name"
64
+ value={firstName}
65
+ error={firstNameErr.error}
66
+ disabled={disabled}
67
+ onBlur={firstNameErr.onBlur}
68
+ onChange={event => onFirstNameChange(event.target.value)}
69
+ onKeyDown={onKeyDown}
70
+ />,
71
+ <Input
72
+ key="lastName"
73
+ label="Last Name"
74
+ placeholder="Enter Last Name"
75
+ value={lastName}
76
+ error={lastNameErr.error}
77
+ disabled={disabled}
78
+ onBlur={lastNameErr.onBlur}
79
+ onChange={event => onLastNameChange(event.target.value)}
80
+ onKeyDown={onKeyDown}
81
+ />,
82
+ ];
83
+
84
+ const passwords = [
85
+ <PasswordInput
86
+ key="password"
87
+ label="Password"
88
+ placeholder="Enter Password"
89
+ value={password}
90
+ error={passwordErr.error}
91
+ disabled={disabled}
92
+ onBlur={passwordErr.onBlur}
93
+ onChange={event => onPasswordChange(event.target.value)}
94
+ onKeyDown={onKeyDown}
95
+ />,
96
+ <PasswordInput
97
+ key="confirmPassword"
98
+ label="Confirm Password"
99
+ placeholder="Confirm Password"
100
+ value={confirmPassword}
101
+ error={confirmErr.error}
102
+ disabled={disabled}
103
+ onBlur={confirmErr.onBlur}
104
+ onChange={event => onConfirmPasswordChange(event.target.value)}
105
+ onKeyDown={onKeyDown}
106
+ />,
107
+ ];
108
+
109
+ if (layout === 'stacked') {
110
+ return (
111
+ <>
112
+ {names}
113
+ {passwords}
114
+ </>
115
+ );
116
+ }
117
+
118
+ return (
119
+ <>
120
+ <div className="grid grid-cols-2 gap-[var(--spacing-system-l)]">{names}</div>
121
+ <div className="grid grid-cols-2 gap-[var(--spacing-system-l)]">{passwords}</div>
122
+ </>
123
+ );
124
+ }
@@ -1,11 +1,10 @@
1
1
  'use client';
2
2
 
3
3
  import type { KeyboardEvent, ReactNode } from 'react';
4
- import { useDeferredError } from '../../../hooks/ui/use-deferred-error';
5
4
  import { cn } from '../../../utils/cn';
6
5
  import { Button } from '../../ui/button';
7
- import { Input } from '../../ui/input';
8
- import { PasswordInput } from '../../ui/password-input';
6
+ import { AccountDetailsFields } from './account-details-fields';
7
+ import { LabeledDivider } from './labeled-divider';
9
8
  import type { AuthSsoProvider } from './sso-providers';
10
9
  import { SsoProviderButtons } from './sso-providers';
11
10
 
@@ -79,12 +78,6 @@ export function CompleteAccountForm({
79
78
  }: CompleteAccountFormProps) {
80
79
  const fieldsDisabled = disabled || loading;
81
80
 
82
- // Validation messages are deferred while the user is typing (shown on blur or after a pause).
83
- const firstNameErr = useDeferredError(errors?.firstName, firstName);
84
- const lastNameErr = useDeferredError(errors?.lastName, lastName);
85
- const passwordErr = useDeferredError(errors?.password, password);
86
- const confirmErr = useDeferredError(errors?.confirmPassword, confirmPassword);
87
-
88
81
  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
89
82
  if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {
90
83
  onSubmit();
@@ -113,53 +106,21 @@ export function CompleteAccountForm({
113
106
  actionLabel={ssoActionLabel}
114
107
  disabled={fieldsDisabled}
115
108
  />
116
- <div className="flex items-center gap-[var(--spacing-system-s)]">
117
- <div className="h-px flex-1 bg-ods-border" />
118
- <span className="text-ods-text-secondary text-h6">{dividerLabel}</span>
119
- <div className="h-px flex-1 bg-ods-border" />
120
- </div>
109
+ <LabeledDivider label={dividerLabel} />
121
110
  </>
122
111
  )}
123
112
 
124
- {/* Name + password fields — single column on every breakpoint */}
125
- <Input
126
- label="First Name"
127
- placeholder="Enter First Name"
128
- value={firstName}
129
- error={firstNameErr.error}
130
- disabled={fieldsDisabled}
131
- onBlur={firstNameErr.onBlur}
132
- onChange={event => onFirstNameChange(event.target.value)}
133
- onKeyDown={handleKeyDown}
134
- />
135
- <Input
136
- label="Last Name"
137
- placeholder="Enter Last Name"
138
- value={lastName}
139
- error={lastNameErr.error}
140
- disabled={fieldsDisabled}
141
- onBlur={lastNameErr.onBlur}
142
- onChange={event => onLastNameChange(event.target.value)}
143
- onKeyDown={handleKeyDown}
144
- />
145
- <PasswordInput
146
- label="Password"
147
- placeholder="Enter Password"
148
- value={password}
149
- error={passwordErr.error}
150
- disabled={fieldsDisabled}
151
- onBlur={passwordErr.onBlur}
152
- onChange={event => onPasswordChange(event.target.value)}
153
- onKeyDown={handleKeyDown}
154
- />
155
- <PasswordInput
156
- label="Confirm Password"
157
- placeholder="Confirm Password"
158
- value={confirmPassword}
159
- error={confirmErr.error}
113
+ <AccountDetailsFields
114
+ firstName={firstName}
115
+ lastName={lastName}
116
+ password={password}
117
+ confirmPassword={confirmPassword}
118
+ onFirstNameChange={onFirstNameChange}
119
+ onLastNameChange={onLastNameChange}
120
+ onPasswordChange={onPasswordChange}
121
+ onConfirmPasswordChange={onConfirmPasswordChange}
122
+ errors={errors}
160
123
  disabled={fieldsDisabled}
161
- onBlur={confirmErr.onBlur}
162
- onChange={event => onConfirmPasswordChange(event.target.value)}
163
124
  onKeyDown={handleKeyDown}
164
125
  />
165
126
 
@@ -6,29 +6,65 @@ import { cn } from '../../../utils/cn';
6
6
  import { Button } from '../../ui/button';
7
7
  import { CheckboxBlock } from '../../ui/checkbox-block';
8
8
  import { Input } from '../../ui/input';
9
+ import { AccountDetailsFields } from './account-details-fields';
10
+ import { LabeledDivider } from './labeled-divider';
9
11
  import type { AuthSsoProvider } from './sso-providers';
10
12
  import { SsoProviderButtons } from './sso-providers';
11
13
  import { TermsAgreementLabel } from './terms-agreement-label';
12
14
 
13
15
  export interface CreateOrganizationFormProps {
14
- /** Controlled field values */
15
- email: string;
16
+ /**
17
+ * Controlled field values. Omit `email` entirely for flows that neither collect nor show an
18
+ * address — the native Apple signup, where the provider asserted it and may only have handed the
19
+ * app a relay alias the user has never seen. Pass it with `emailReadOnly` to show it uneditable.
20
+ */
21
+ email?: string;
16
22
  organizationName: string;
17
23
  domain: string;
18
24
  agreedToTerms: boolean;
19
- /** Change handlers */
20
- onEmailChange: (value: string) => void;
25
+ // Change handlers
26
+ /** Omit together with `email` when the form collects no address. */
27
+ onEmailChange?: (value: string) => void;
21
28
  onOrganizationNameChange: (value: string) => void;
22
29
  onDomainChange: (value: string) => void;
23
30
  onAgreedToTermsChange: (checked: boolean) => void;
24
31
  /** Primary submit ("Continue") */
25
32
  onSubmit: () => void;
33
+ /**
34
+ * Personal details and credentials, collected on the same screen as the organization. Supply the
35
+ * four handlers to render them; omit them and the form stays organization-only (the SSO paths,
36
+ * where the provider is the credential and there is no password to set).
37
+ */
38
+ firstName?: string;
39
+ lastName?: string;
40
+ password?: string;
41
+ confirmPassword?: string;
42
+ onFirstNameChange?: (value: string) => void;
43
+ onLastNameChange?: (value: string) => void;
44
+ onPasswordChange?: (value: string) => void;
45
+ onConfirmPasswordChange?: (value: string) => void;
46
+ /**
47
+ * Renders the email as a read-only field instead of an input, for flows where the address was
48
+ * fixed by an earlier step and this form cannot change it — a signup that collected it on a
49
+ * previous step, or an SSO login where the provider asserted it and the server reads it from the
50
+ * session. `onEmailChange` is not called in this mode.
51
+ */
52
+ emailReadOnly?: boolean;
53
+ /** Label above the read-only email, e.g. "Signed in with Google". Ignored unless `emailReadOnly`. */
54
+ emailReadOnlyLabel?: string;
55
+ /** Overrides the default heading. */
56
+ title?: string;
57
+ /** Overrides the default sub-heading. */
58
+ subtitle?: ReactNode;
26
59
  /** Suffix rendered inside the domain input, e.g. ".openframe.ai" */
27
60
  domainSuffix?: string;
28
61
  domainPlaceholder?: string;
29
62
  termsUrl?: string;
30
63
  privacyPolicyUrl?: string;
31
64
  submitLabel?: string;
65
+ /** Renders a back action beside the submit when supplied (e.g. to return to an earlier step). */
66
+ onBack?: () => void;
67
+ backLabel?: string;
32
68
  /** Disables just the primary submit (fields stay editable). */
33
69
  submitDisabled?: boolean;
34
70
  loading?: boolean;
@@ -38,6 +74,10 @@ export interface CreateOrganizationFormProps {
38
74
  organizationName?: string;
39
75
  domain?: string;
40
76
  terms?: string;
77
+ firstName?: string;
78
+ lastName?: string;
79
+ password?: string;
80
+ confirmPassword?: string;
41
81
  };
42
82
  /** Informational status under the email field (e.g. live availability). `errors.email` wins. */
43
83
  emailStatus?: { message: string; variant: 'error' | 'warning' | 'success' | 'muted' };
@@ -54,7 +94,7 @@ export interface CreateOrganizationFormProps {
54
94
  onSsoClick?: (provider: AuthSsoProvider) => void;
55
95
  /** Disables the provider buttons (e.g. until the form validates). */
56
96
  ssoDisabled?: boolean;
57
- /** Verb prefix for provider buttons, e.g. "Continue with". Ignored for "openframe". */
97
+ /** Verb prefix for provider buttons, e.g. "Continue with". */
58
98
  ssoActionLabel?: string;
59
99
  /** Divider text between the primary submit and the SSO buttons. */
60
100
  dividerLabel?: string;
@@ -76,11 +116,25 @@ export function CreateOrganizationForm({
76
116
  onDomainChange,
77
117
  onAgreedToTermsChange,
78
118
  onSubmit,
119
+ firstName = '',
120
+ lastName = '',
121
+ password = '',
122
+ confirmPassword = '',
123
+ onFirstNameChange,
124
+ onLastNameChange,
125
+ onPasswordChange,
126
+ onConfirmPasswordChange,
127
+ emailReadOnly = false,
128
+ emailReadOnlyLabel = 'Email',
129
+ title = 'Create Organization',
130
+ subtitle = 'Start your journey with OpenFrame.',
79
131
  domainSuffix,
80
132
  domainPlaceholder = 'company-name',
81
133
  termsUrl = '#',
82
134
  privacyPolicyUrl = '#',
83
135
  submitLabel = 'Continue',
136
+ onBack,
137
+ backLabel = 'Back',
84
138
  submitDisabled = false,
85
139
  loading = false,
86
140
  disabled = false,
@@ -99,12 +153,14 @@ export function CreateOrganizationForm({
99
153
  const fieldsDisabled = disabled || loading;
100
154
 
101
155
  // Validation messages are deferred while the user is typing (shown on blur or after a pause).
102
- const emailErr = useDeferredError(errors?.email, email);
156
+ const emailErr = useDeferredError(errors?.email, email ?? '');
103
157
  const orgNameErr = useDeferredError(errors?.organizationName, organizationName);
104
158
  const domainErr = useDeferredError(errors?.domain, domain);
105
-
106
159
  const handleKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
107
- if (event.key === 'Enter' && !fieldsDisabled) {
160
+ // `submitDisabled` too, not just `fieldsDisabled`: Enter must be the same gate as the button.
161
+ // The fields stay editable while the form is incomplete, so checking only `fieldsDisabled`
162
+ // let Enter submit a form the button was still refusing.
163
+ if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {
108
164
  onSubmit();
109
165
  }
110
166
  };
@@ -118,23 +174,27 @@ export function CreateOrganizationForm({
118
174
  >
119
175
  {/* Header */}
120
176
  <div className="flex flex-col">
121
- <h1 className="tracking-[-0.64px] text-ods-text-primary text-h2">Create Organization</h1>
122
- <p className="text-ods-text-secondary text-h4">Start your journey with OpenFrame.</p>
177
+ <h1 className="tracking-[-0.64px] text-ods-text-primary text-h2">{title}</h1>
178
+ <p className="text-ods-text-secondary text-h4">{subtitle}</p>
123
179
  </div>
124
180
 
125
181
  {/* Email + Organization Name — single column on every breakpoint */}
126
- <Input
127
- type="email"
128
- label="Email"
129
- placeholder="username@mail.com"
130
- value={email}
131
- error={emailErr.error ?? emailStatus?.message}
132
- errorVariant={emailErr.error ? 'error' : emailStatus?.variant}
133
- disabled={fieldsDisabled}
134
- onBlur={emailErr.onBlur}
135
- onChange={event => onEmailChange(event.target.value)}
136
- onKeyDown={handleKeyDown}
137
- />
182
+ {email === undefined ? null : emailReadOnly ? (
183
+ <Input type="email" label={emailReadOnlyLabel} value={email} readOnly disabled />
184
+ ) : (
185
+ <Input
186
+ type="email"
187
+ label="Email"
188
+ placeholder="username@mail.com"
189
+ value={email}
190
+ error={emailErr.error ?? emailStatus?.message}
191
+ errorVariant={emailErr.error ? 'error' : emailStatus?.variant}
192
+ disabled={fieldsDisabled}
193
+ onBlur={emailErr.onBlur}
194
+ onChange={event => onEmailChange?.(event.target.value)}
195
+ onKeyDown={handleKeyDown}
196
+ />
197
+ )}
138
198
  <Input
139
199
  label="Organization Name"
140
200
  placeholder="Your Company Name"
@@ -163,6 +223,25 @@ export function CreateOrganizationForm({
163
223
  {domainSlot && <div className="pt-[var(--spacing-system-s)]">{domainSlot}</div>}
164
224
  </div>
165
225
 
226
+ {/* All four handlers or none: the account block is one decision, and checking them here
227
+ rather than through a derived boolean is what lets TypeScript narrow them. */}
228
+ {onFirstNameChange && onLastNameChange && onPasswordChange && onConfirmPasswordChange && (
229
+ <AccountDetailsFields
230
+ layout="paired"
231
+ firstName={firstName}
232
+ lastName={lastName}
233
+ password={password}
234
+ confirmPassword={confirmPassword}
235
+ onFirstNameChange={onFirstNameChange}
236
+ onLastNameChange={onLastNameChange}
237
+ onPasswordChange={onPasswordChange}
238
+ onConfirmPasswordChange={onConfirmPasswordChange}
239
+ errors={errors}
240
+ disabled={fieldsDisabled}
241
+ onKeyDown={handleKeyDown}
242
+ />
243
+ )}
244
+
166
245
  {/* Terms & Privacy — deliberately NOT `truncateLabel`. That prop suits a
167
246
  one-line value; this label is a sentence carrying the Terms and Privacy
168
247
  Policy links, and ellipsizing it clipped the row to "Agree to Terms &
@@ -178,26 +257,37 @@ export function CreateOrganizationForm({
178
257
  onCheckedChange={onAgreedToTermsChange}
179
258
  />
180
259
 
181
- {/* Actions */}
182
- <Button
183
- type="button"
184
- variant="accent"
185
- fullWidth
186
- loading={loading}
187
- disabled={disabled || submitDisabled}
188
- onClick={onSubmit}
189
- >
190
- {submitLabel}
191
- </Button>
260
+ {/* Actions — optional back + submit */}
261
+ <div className="flex items-center gap-[var(--spacing-system-l)]">
262
+ {onBack && (
263
+ <Button
264
+ type="button"
265
+ variant="transparent"
266
+ fullWidth
267
+ className="flex-1"
268
+ disabled={fieldsDisabled}
269
+ onClick={onBack}
270
+ >
271
+ {backLabel}
272
+ </Button>
273
+ )}
274
+ <Button
275
+ type="button"
276
+ variant="accent"
277
+ fullWidth
278
+ className="flex-1"
279
+ loading={loading}
280
+ disabled={disabled || submitDisabled}
281
+ onClick={onSubmit}
282
+ >
283
+ {submitLabel}
284
+ </Button>
285
+ </div>
192
286
 
193
287
  {/* SSO registration alternatives */}
194
288
  {hasSso && (
195
289
  <>
196
- <div className="flex items-center gap-[var(--spacing-system-s)]">
197
- <div className="h-px flex-1 bg-ods-border" />
198
- <span className="text-ods-text-secondary text-h6">{dividerLabel}</span>
199
- <div className="h-px flex-1 bg-ods-border" />
200
- </div>
290
+ <LabeledDivider label={dividerLabel} />
201
291
  <SsoProviderButtons
202
292
  providers={ssoProviders}
203
293
  onSsoClick={onSsoClick}
@@ -1,3 +1,4 @@
1
+ export * from './account-details-fields';
1
2
  export * from './auth-benefits-panel';
2
3
  export * from './auth-branding';
3
4
  export * from './auth-shell';
@@ -5,6 +6,7 @@ export * from './back-to-login-link';
5
6
  export * from './complete-account-form';
6
7
  export * from './create-organization-form';
7
8
  export * from './invite-link-invalid-modal';
9
+ export * from './labeled-divider';
8
10
  export * from './login-form';
9
11
  export * from './openframe-sso-login-form';
10
12
  export * from './openframe-sso-signup-form';
@@ -0,0 +1,16 @@
1
+ 'use client';
2
+
3
+ export interface LabeledDividerProps {
4
+ label: string;
5
+ }
6
+
7
+ /** A horizontal rule with a centred label. */
8
+ export function LabeledDivider({ label }: LabeledDividerProps) {
9
+ return (
10
+ <div className="flex items-center gap-[var(--spacing-system-s)]">
11
+ <div className="h-px flex-1 bg-ods-border" />
12
+ <span className="text-ods-text-secondary text-h6">{label}</span>
13
+ <div className="h-px flex-1 bg-ods-border" />
14
+ </div>
15
+ );
16
+ }