@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.
- package/dist/{chunk-3ELW4VML.js → chunk-2Y5UT5VF.js} +3 -3
- package/dist/{chunk-YFZDIEVC.js → chunk-3HYHYKNL.js} +6 -6
- package/dist/{chunk-5CJKZHWO.js → chunk-3JVYM4ZN.js} +5 -5
- package/dist/{chunk-P2VRLSAP.cjs → chunk-3WR5R6H6.cjs} +21 -21
- package/dist/{chunk-P2VRLSAP.cjs.map → chunk-3WR5R6H6.cjs.map} +1 -1
- package/dist/{chunk-UCNEQKIT.cjs → chunk-4VZFJKPT.cjs} +5 -5
- package/dist/{chunk-UCNEQKIT.cjs.map → chunk-4VZFJKPT.cjs.map} +1 -1
- package/dist/{chunk-VEHZNQ77.cjs → chunk-5LUPIYPH.cjs} +30 -30
- package/dist/{chunk-VEHZNQ77.cjs.map → chunk-5LUPIYPH.cjs.map} +1 -1
- package/dist/{chunk-TLKHLGAB.js → chunk-5OIXAV4P.js} +3 -3
- package/dist/{chunk-LNKULR4V.js → chunk-6EZGJZVH.js} +2 -2
- package/dist/{chunk-JWKAJTNA.cjs → chunk-6ZNWY6WW.cjs} +28 -28
- package/dist/{chunk-JWKAJTNA.cjs.map → chunk-6ZNWY6WW.cjs.map} +1 -1
- package/dist/{chunk-QJR32JM2.cjs → chunk-A6ETQEWK.cjs} +615 -485
- package/dist/chunk-A6ETQEWK.cjs.map +1 -0
- package/dist/{chunk-JFKK3ZOA.cjs → chunk-BMORFCT2.cjs} +57 -57
- package/dist/{chunk-JFKK3ZOA.cjs.map → chunk-BMORFCT2.cjs.map} +1 -1
- package/dist/{chunk-33DHEEMP.cjs → chunk-C3MRLZUI.cjs} +33 -33
- package/dist/{chunk-33DHEEMP.cjs.map → chunk-C3MRLZUI.cjs.map} +1 -1
- package/dist/{chunk-XWBND4G5.js → chunk-D7YISHXC.js} +17 -2
- package/dist/{chunk-XWBND4G5.js.map → chunk-D7YISHXC.js.map} +1 -1
- package/dist/{chunk-EJKEDCTH.js → chunk-DQUEVRDU.js} +7 -7
- package/dist/{chunk-HCKEDTKJ.cjs → chunk-EAYMX22G.cjs} +15 -15
- package/dist/{chunk-HCKEDTKJ.cjs.map → chunk-EAYMX22G.cjs.map} +1 -1
- package/dist/{chunk-NNFQLTJ3.cjs → chunk-EMF64FWA.cjs} +31 -5
- package/dist/chunk-EMF64FWA.cjs.map +1 -0
- package/dist/{chunk-IKUSKKFZ.cjs → chunk-G45RGWHF.cjs} +159 -144
- package/dist/chunk-G45RGWHF.cjs.map +1 -0
- package/dist/{chunk-TOUD3AGK.js → chunk-KLWJRGNN.js} +5 -5
- package/dist/{chunk-MYEG5ZDT.js → chunk-L4WLYGB4.js} +3 -3
- package/dist/{chunk-VZSTL5VG.js → chunk-LV6CORVJ.js} +3 -3
- package/dist/{chunk-MEJFHD5N.cjs → chunk-M36XSGZV.cjs} +19 -19
- package/dist/{chunk-MEJFHD5N.cjs.map → chunk-M36XSGZV.cjs.map} +1 -1
- package/dist/{chunk-MCWAZ5XI.js → chunk-NFWAL6UV.js} +2 -2
- package/dist/{chunk-3ZA2OYRE.cjs → chunk-NISAGFEM.cjs} +3 -3
- package/dist/{chunk-3ZA2OYRE.cjs.map → chunk-NISAGFEM.cjs.map} +1 -1
- package/dist/{chunk-7X5TCK4R.cjs → chunk-NYLWMAHP.cjs} +10 -10
- package/dist/{chunk-7X5TCK4R.cjs.map → chunk-NYLWMAHP.cjs.map} +1 -1
- package/dist/{chunk-RURZ6QLN.js → chunk-OZNFPQT5.js} +3 -3
- package/dist/{chunk-VOAZU46B.cjs → chunk-P2CKLIOM.cjs} +80 -80
- package/dist/{chunk-VOAZU46B.cjs.map → chunk-P2CKLIOM.cjs.map} +1 -1
- package/dist/{chunk-KP6VJP36.js → chunk-PLMJVA2H.js} +4 -4
- package/dist/{chunk-JAJQ5P2Q.cjs → chunk-QZYYNBMI.cjs} +28 -28
- package/dist/{chunk-JAJQ5P2Q.cjs.map → chunk-QZYYNBMI.cjs.map} +1 -1
- package/dist/{chunk-4T5KOD3G.js → chunk-S7YXLZNG.js} +6 -6
- package/dist/{chunk-Q66AMADB.cjs → chunk-SSGM2NKA.cjs} +6 -6
- package/dist/{chunk-Q66AMADB.cjs.map → chunk-SSGM2NKA.cjs.map} +1 -1
- package/dist/{chunk-IIT5HWLY.cjs → chunk-V7ZJTUV2.cjs} +11 -11
- package/dist/{chunk-IIT5HWLY.cjs.map → chunk-V7ZJTUV2.cjs.map} +1 -1
- package/dist/{chunk-3LMYQGAJ.js → chunk-X7YSF6A3.js} +28 -2
- package/dist/chunk-X7YSF6A3.js.map +1 -0
- package/dist/{chunk-ZHUBNWEX.cjs → chunk-XMK24INA.cjs} +6 -6
- package/dist/{chunk-ZHUBNWEX.cjs.map → chunk-XMK24INA.cjs.map} +1 -1
- package/dist/{chunk-XPJKQ63S.js → chunk-XMTGLQSD.js} +3 -3
- package/dist/{chunk-6BJYPG2V.js → chunk-YEQAA6I4.js} +4 -4
- package/dist/{chunk-JVWRMLQZ.js → chunk-YYC7V7YC.js} +2059 -1929
- package/dist/chunk-YYC7V7YC.js.map +1 -0
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/entity-cards/program-card.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +3 -3
- package/dist/components/chat/index.js +2 -2
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +8 -8
- package/dist/components/docs/index.js +7 -7
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +5 -5
- package/dist/components/faq/index.js +4 -4
- package/dist/components/features/auth/account-details-fields.d.ts +34 -0
- package/dist/components/features/auth/account-details-fields.d.ts.map +1 -0
- package/dist/components/features/auth/complete-account-form.d.ts.map +1 -1
- package/dist/components/features/auth/create-organization-form.d.ts +43 -6
- package/dist/components/features/auth/create-organization-form.d.ts.map +1 -1
- package/dist/components/features/auth/index.d.ts +2 -0
- package/dist/components/features/auth/index.d.ts.map +1 -1
- package/dist/components/features/auth/labeled-divider.d.ts +6 -0
- package/dist/components/features/auth/labeled-divider.d.ts.map +1 -0
- package/dist/components/features/auth/login-form.d.ts +32 -9
- package/dist/components/features/auth/login-form.d.ts.map +1 -1
- package/dist/components/features/auth/openframe-sso-signup-form.d.ts.map +1 -1
- package/dist/components/features/auth/sso-providers.d.ts +1 -1
- package/dist/components/features/auth/sso-providers.d.ts.map +1 -1
- package/dist/components/features/index.cjs +8 -4
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.js +7 -3
- package/dist/components/features/media-gallery-manager.d.ts.map +1 -1
- package/dist/components/features/push-button-selector.d.ts +9 -7
- package/dist/components/features/push-button-selector.d.ts.map +1 -1
- package/dist/components/features/release-media-manager.d.ts.map +1 -1
- package/dist/components/features/video.d.ts +8 -0
- package/dist/components/features/video.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +24 -24
- package/dist/components/help-center-pages/index.js +15 -15
- package/dist/components/index.cjs +57 -51
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +21 -15
- package/dist/components/index.js.map +1 -1
- package/dist/components/loading/unified-skeleton.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +31 -31
- package/dist/components/meeting-scheduler/index.js +3 -3
- package/dist/components/navigation/index.cjs +7 -7
- package/dist/components/navigation/index.js +6 -6
- package/dist/components/onboarding-guides/index.cjs +7 -7
- package/dist/components/onboarding-guides/index.js +6 -6
- package/dist/components/related-content/index.cjs +3 -3
- package/dist/components/related-content/index.js +2 -2
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/tickets/index.js +7 -7
- package/dist/components/ui/index.cjs +6 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.d.ts +1 -0
- package/dist/components/ui/index.d.ts.map +1 -1
- package/dist/components/ui/index.js +5 -3
- package/dist/components/ui/lazy-mount.d.ts +21 -0
- package/dist/components/ui/lazy-mount.d.ts.map +1 -0
- package/dist/index.cjs +24 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +23 -7
- package/dist/index.js.map +1 -1
- package/dist/types/marketing.d.ts +1 -1
- package/dist/types/marketing.d.ts.map +1 -1
- package/dist/utils/index.cjs +26 -0
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +22 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/media-type.d.ts +26 -0
- package/dist/utils/media-type.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/chat/entity-cards/program-card.tsx +2 -2
- package/src/components/features/auth/account-details-fields.tsx +124 -0
- package/src/components/features/auth/complete-account-form.tsx +13 -52
- package/src/components/features/auth/create-organization-form.tsx +128 -38
- package/src/components/features/auth/index.ts +2 -0
- package/src/components/features/auth/labeled-divider.tsx +16 -0
- package/src/components/features/auth/login-form.tsx +102 -23
- package/src/components/features/auth/openframe-sso-signup-form.tsx +11 -43
- package/src/components/features/auth/sso-providers.tsx +3 -3
- package/src/components/features/media-gallery-manager.tsx +4 -3
- package/src/components/features/push-button-selector.tsx +15 -12
- package/src/components/features/release-media-manager.tsx +3 -2
- package/src/components/features/video.tsx +28 -0
- package/src/components/loading/__tests__/unified-skeleton.test.tsx +16 -0
- package/src/components/loading/unified-skeleton.tsx +5 -1
- package/src/components/ui/__tests__/lazy-mount.test.tsx +46 -0
- package/src/components/ui/index.ts +3 -0
- package/src/components/ui/lazy-mount.tsx +38 -0
- package/src/stories/auth/Login.stories.tsx +15 -5
- package/src/types/.marketing.md +0 -1
- package/src/types/marketing.ts +4 -2
- package/src/utils/__tests__/media-type.test.ts +30 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/media-type.ts +39 -0
- package/src/utils/vendor-media-stub.ts +7 -7
- package/dist/chunk-3LMYQGAJ.js.map +0 -1
- package/dist/chunk-IKUSKKFZ.cjs.map +0 -1
- package/dist/chunk-JVWRMLQZ.js.map +0 -1
- package/dist/chunk-NNFQLTJ3.cjs.map +0 -1
- package/dist/chunk-QJR32JM2.cjs.map +0 -1
- /package/dist/{chunk-3ELW4VML.js.map → chunk-2Y5UT5VF.js.map} +0 -0
- /package/dist/{chunk-YFZDIEVC.js.map → chunk-3HYHYKNL.js.map} +0 -0
- /package/dist/{chunk-5CJKZHWO.js.map → chunk-3JVYM4ZN.js.map} +0 -0
- /package/dist/{chunk-TLKHLGAB.js.map → chunk-5OIXAV4P.js.map} +0 -0
- /package/dist/{chunk-LNKULR4V.js.map → chunk-6EZGJZVH.js.map} +0 -0
- /package/dist/{chunk-EJKEDCTH.js.map → chunk-DQUEVRDU.js.map} +0 -0
- /package/dist/{chunk-TOUD3AGK.js.map → chunk-KLWJRGNN.js.map} +0 -0
- /package/dist/{chunk-MYEG5ZDT.js.map → chunk-L4WLYGB4.js.map} +0 -0
- /package/dist/{chunk-VZSTL5VG.js.map → chunk-LV6CORVJ.js.map} +0 -0
- /package/dist/{chunk-MCWAZ5XI.js.map → chunk-NFWAL6UV.js.map} +0 -0
- /package/dist/{chunk-RURZ6QLN.js.map → chunk-OZNFPQT5.js.map} +0 -0
- /package/dist/{chunk-KP6VJP36.js.map → chunk-PLMJVA2H.js.map} +0 -0
- /package/dist/{chunk-4T5KOD3G.js.map → chunk-S7YXLZNG.js.map} +0 -0
- /package/dist/{chunk-XPJKQ63S.js.map → chunk-XMTGLQSD.js.map} +0 -0
- /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
|
@@ -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
|
|
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 {
|
|
8
|
-
import {
|
|
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
|
-
<
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
/**
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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".
|
|
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
|
-
|
|
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">
|
|
122
|
-
<p className="text-ods-text-secondary text-h4">
|
|
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
|
-
|
|
127
|
-
type="email"
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
<
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
<
|
|
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
|
+
}
|