@flamingo-stack/openframe-frontend-core 0.0.398 → 0.0.399-snapshot.20260710120252

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 (127) hide show
  1. package/dist/{chunk-GKRJOS6S.js → chunk-463X3AW2.js} +3 -3
  2. package/dist/{chunk-MNQVBME4.js → chunk-4HAVKWTJ.js} +4 -4
  3. package/dist/{chunk-PE37UDPF.cjs → chunk-556BG7SX.cjs} +40 -40
  4. package/dist/{chunk-PE37UDPF.cjs.map → chunk-556BG7SX.cjs.map} +1 -1
  5. package/dist/{chunk-N4R7TRF3.js → chunk-56XCRZ7I.js} +3 -3
  6. package/dist/{chunk-VDDNJ6XC.js → chunk-6VR7BJF6.js} +6 -6
  7. package/dist/{chunk-FZIMFLJG.cjs → chunk-6XQJSN7G.cjs} +19 -19
  8. package/dist/{chunk-FZIMFLJG.cjs.map → chunk-6XQJSN7G.cjs.map} +1 -1
  9. package/dist/{chunk-HMTOQGYU.cjs → chunk-ABRDOCVN.cjs} +12 -12
  10. package/dist/{chunk-HMTOQGYU.cjs.map → chunk-ABRDOCVN.cjs.map} +1 -1
  11. package/dist/{chunk-ZSZRJWY3.js → chunk-DH6HTYOV.js} +153 -134
  12. package/dist/chunk-DH6HTYOV.js.map +1 -0
  13. package/dist/{chunk-XYT5PIKZ.cjs → chunk-EFTKJFEF.cjs} +5 -5
  14. package/dist/{chunk-XYT5PIKZ.cjs.map → chunk-EFTKJFEF.cjs.map} +1 -1
  15. package/dist/{chunk-MGJZQ3EE.js → chunk-FHEIG7QO.js} +3 -3
  16. package/dist/{chunk-NYIZVI4I.cjs → chunk-GA5AN24T.cjs} +10 -3
  17. package/dist/chunk-GA5AN24T.cjs.map +1 -0
  18. package/dist/{chunk-X2FPWS3P.js → chunk-HGKWTKW7.js} +10 -3
  19. package/dist/chunk-HGKWTKW7.js.map +1 -0
  20. package/dist/{chunk-G42EXM23.js → chunk-I4XG5PJA.js} +2 -2
  21. package/dist/{chunk-YEWGFWNM.cjs → chunk-ILWSJCWD.cjs} +33 -33
  22. package/dist/{chunk-YEWGFWNM.cjs.map → chunk-ILWSJCWD.cjs.map} +1 -1
  23. package/dist/{chunk-JWBRDNYH.cjs → chunk-J4XR6YYZ.cjs} +23 -4
  24. package/dist/chunk-J4XR6YYZ.cjs.map +1 -0
  25. package/dist/{chunk-VVO7AZKZ.cjs → chunk-J6IQQJJQ.cjs} +1011 -986
  26. package/dist/chunk-J6IQQJJQ.cjs.map +1 -0
  27. package/dist/{chunk-4BVRLCCA.cjs → chunk-KIC4ZEZ7.cjs} +93 -93
  28. package/dist/{chunk-4BVRLCCA.cjs.map → chunk-KIC4ZEZ7.cjs.map} +1 -1
  29. package/dist/{chunk-Y3FREU4B.js → chunk-LAL63W5J.js} +6 -6
  30. package/dist/{chunk-7Q2DZEUA.cjs → chunk-MFRP2UDK.cjs} +32 -32
  31. package/dist/{chunk-7Q2DZEUA.cjs.map → chunk-MFRP2UDK.cjs.map} +1 -1
  32. package/dist/{chunk-URODM26E.js → chunk-MQRTT3DO.js} +6 -6
  33. package/dist/{chunk-4KWIQEZT.js → chunk-MTBFE3JF.js} +2 -2
  34. package/dist/{chunk-2DGA5LHT.cjs → chunk-OPD5V57R.cjs} +19 -19
  35. package/dist/{chunk-2DGA5LHT.cjs.map → chunk-OPD5V57R.cjs.map} +1 -1
  36. package/dist/{chunk-WXOIEYXW.js → chunk-PD5MXNTJ.js} +5 -5
  37. package/dist/{chunk-WBLRUO3F.cjs → chunk-Q3AO2QWK.cjs} +3 -3
  38. package/dist/{chunk-WBLRUO3F.cjs.map → chunk-Q3AO2QWK.cjs.map} +1 -1
  39. package/dist/{chunk-C4DHYDXB.cjs → chunk-QGQ76SSR.cjs} +12 -12
  40. package/dist/{chunk-C4DHYDXB.cjs.map → chunk-QGQ76SSR.cjs.map} +1 -1
  41. package/dist/{chunk-WJSUUOPU.cjs → chunk-RSFHAFZN.cjs} +40 -40
  42. package/dist/{chunk-WJSUUOPU.cjs.map → chunk-RSFHAFZN.cjs.map} +1 -1
  43. package/dist/{chunk-MQXUIYYN.js → chunk-RYPCNUXA.js} +4 -4
  44. package/dist/{chunk-2TBXIW7L.js → chunk-UFP45JLE.js} +3 -3
  45. package/dist/{chunk-B2K5COVE.js → chunk-UYBNOHTC.js} +170 -145
  46. package/dist/{chunk-B2K5COVE.js.map → chunk-UYBNOHTC.js.map} +1 -1
  47. package/dist/{chunk-LBDKAMOH.cjs → chunk-VTSEL7CJ.cjs} +73 -73
  48. package/dist/{chunk-LBDKAMOH.cjs.map → chunk-VTSEL7CJ.cjs.map} +1 -1
  49. package/dist/components/case-studies/index.cjs +10 -10
  50. package/dist/components/case-studies/index.js +4 -4
  51. package/dist/components/chat/index.cjs +4 -4
  52. package/dist/components/chat/index.js +3 -3
  53. package/dist/components/contact/index.cjs +5 -5
  54. package/dist/components/contact/index.js +4 -4
  55. package/dist/components/docs/index.cjs +7 -7
  56. package/dist/components/docs/index.js +6 -6
  57. package/dist/components/embeds/index.cjs +5 -5
  58. package/dist/components/embeds/index.js +4 -4
  59. package/dist/components/faq/index.cjs +6 -6
  60. package/dist/components/faq/index.js +5 -5
  61. package/dist/components/features/auth/auth-benefits-panel.d.ts.map +1 -1
  62. package/dist/components/features/auth/complete-account-form.d.ts.map +1 -1
  63. package/dist/components/features/auth/create-organization-form.d.ts.map +1 -1
  64. package/dist/components/features/auth/login-form.d.ts.map +1 -1
  65. package/dist/components/features/auth/openframe-sso-login-form.d.ts.map +1 -1
  66. package/dist/components/features/auth/openframe-sso-signup-form.d.ts.map +1 -1
  67. package/dist/components/features/auth/password-reset-form.d.ts.map +1 -1
  68. package/dist/components/features/index.cjs +4 -4
  69. package/dist/components/features/index.js +3 -3
  70. package/dist/components/help-center-pages/index.cjs +23 -23
  71. package/dist/components/help-center-pages/index.js +14 -14
  72. package/dist/components/index.cjs +149 -149
  73. package/dist/components/index.js +12 -12
  74. package/dist/components/navigation/index.cjs +4 -4
  75. package/dist/components/navigation/index.js +3 -3
  76. package/dist/components/onboarding-guides/index.cjs +8 -8
  77. package/dist/components/onboarding-guides/index.js +7 -7
  78. package/dist/components/related-content/index.cjs +6 -6
  79. package/dist/components/related-content/index.js +5 -5
  80. package/dist/components/tickets/index.cjs +7 -7
  81. package/dist/components/tickets/index.js +6 -6
  82. package/dist/components/ui/field-wrapper.d.ts +1 -1
  83. package/dist/components/ui/field-wrapper.d.ts.map +1 -1
  84. package/dist/components/ui/file-manager/index.cjs +50 -50
  85. package/dist/components/ui/file-manager/index.js +1 -1
  86. package/dist/components/ui/index.cjs +4 -4
  87. package/dist/components/ui/index.js +3 -3
  88. package/dist/hooks/index.cjs +4 -2
  89. package/dist/hooks/index.cjs.map +1 -1
  90. package/dist/hooks/index.js +3 -1
  91. package/dist/hooks/ui/index.d.ts +1 -0
  92. package/dist/hooks/ui/index.d.ts.map +1 -1
  93. package/dist/hooks/ui/use-deferred-error.d.ts +16 -0
  94. package/dist/hooks/ui/use-deferred-error.d.ts.map +1 -0
  95. package/dist/index.cjs +6 -4
  96. package/dist/index.cjs.map +1 -1
  97. package/dist/index.js +5 -3
  98. package/package.json +1 -1
  99. package/src/components/features/auth/auth-benefits-panel.tsx +1 -1
  100. package/src/components/features/auth/complete-account-form.tsx +48 -51
  101. package/src/components/features/auth/create-organization-form.tsx +33 -31
  102. package/src/components/features/auth/login-form.tsx +6 -1
  103. package/src/components/features/auth/openframe-sso-login-form.tsx +9 -2
  104. package/src/components/features/auth/openframe-sso-signup-form.tsx +51 -52
  105. package/src/components/features/auth/password-reset-form.tsx +9 -2
  106. package/src/components/ui/field-wrapper.tsx +6 -3
  107. package/src/hooks/ui/index.ts +1 -0
  108. package/src/hooks/ui/use-deferred-error.ts +38 -0
  109. package/src/stories/Input.stories.tsx +44 -0
  110. package/src/stories/auth/CreateOrganization.stories.tsx +20 -0
  111. package/dist/chunk-JWBRDNYH.cjs.map +0 -1
  112. package/dist/chunk-NYIZVI4I.cjs.map +0 -1
  113. package/dist/chunk-VVO7AZKZ.cjs.map +0 -1
  114. package/dist/chunk-X2FPWS3P.js.map +0 -1
  115. package/dist/chunk-ZSZRJWY3.js.map +0 -1
  116. /package/dist/{chunk-GKRJOS6S.js.map → chunk-463X3AW2.js.map} +0 -0
  117. /package/dist/{chunk-MNQVBME4.js.map → chunk-4HAVKWTJ.js.map} +0 -0
  118. /package/dist/{chunk-N4R7TRF3.js.map → chunk-56XCRZ7I.js.map} +0 -0
  119. /package/dist/{chunk-VDDNJ6XC.js.map → chunk-6VR7BJF6.js.map} +0 -0
  120. /package/dist/{chunk-MGJZQ3EE.js.map → chunk-FHEIG7QO.js.map} +0 -0
  121. /package/dist/{chunk-G42EXM23.js.map → chunk-I4XG5PJA.js.map} +0 -0
  122. /package/dist/{chunk-Y3FREU4B.js.map → chunk-LAL63W5J.js.map} +0 -0
  123. /package/dist/{chunk-URODM26E.js.map → chunk-MQRTT3DO.js.map} +0 -0
  124. /package/dist/{chunk-4KWIQEZT.js.map → chunk-MTBFE3JF.js.map} +0 -0
  125. /package/dist/{chunk-WXOIEYXW.js.map → chunk-PD5MXNTJ.js.map} +0 -0
  126. /package/dist/{chunk-MQXUIYYN.js.map → chunk-RYPCNUXA.js.map} +0 -0
  127. /package/dist/{chunk-2TBXIW7L.js.map → chunk-UFP45JLE.js.map} +0 -0
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type * as React from 'react'
4
4
  import { cn } from '../../../utils/cn'
5
+ import { useDeferredError } from '../../../hooks/ui/use-deferred-error'
5
6
  import { Button } from '../../ui/button'
6
7
  import { Input } from '../../ui/input'
7
8
  import { PasswordInput } from '../../ui/password-input'
@@ -75,6 +76,12 @@ export function CompleteAccountForm({
75
76
  }: CompleteAccountFormProps) {
76
77
  const fieldsDisabled = disabled || loading
77
78
 
79
+ // Validation messages are deferred while the user is typing (shown on blur or after a pause).
80
+ const firstNameErr = useDeferredError(errors?.firstName, firstName)
81
+ const lastNameErr = useDeferredError(errors?.lastName, lastName)
82
+ const passwordErr = useDeferredError(errors?.password, password)
83
+ const confirmErr = useDeferredError(errors?.confirmPassword, confirmPassword)
84
+
78
85
  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
79
86
  if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {
80
87
  onSubmit()
@@ -111,57 +118,47 @@ export function CompleteAccountForm({
111
118
  </>
112
119
  )}
113
120
 
114
- {/* First + Last name */}
115
- <div className="flex gap-[var(--spacing-system-l)]">
116
- <div className="min-w-0 flex-1">
117
- <Input
118
- label="First Name"
119
- placeholder="Enter First Name"
120
- value={firstName}
121
- error={errors?.firstName}
122
- disabled={fieldsDisabled}
123
- onChange={(event) => onFirstNameChange(event.target.value)}
124
- onKeyDown={handleKeyDown}
125
- />
126
- </div>
127
- <div className="min-w-0 flex-1">
128
- <Input
129
- label="Last Name"
130
- placeholder="Enter Last Name"
131
- value={lastName}
132
- error={errors?.lastName}
133
- disabled={fieldsDisabled}
134
- onChange={(event) => onLastNameChange(event.target.value)}
135
- onKeyDown={handleKeyDown}
136
- />
137
- </div>
138
- </div>
139
-
140
- {/* Password + Confirm */}
141
- <div className="flex gap-[var(--spacing-system-l)]">
142
- <div className="min-w-0 flex-1">
143
- <PasswordInput
144
- label="Password"
145
- placeholder="Enter Password"
146
- value={password}
147
- error={errors?.password}
148
- disabled={fieldsDisabled}
149
- onChange={(event) => onPasswordChange(event.target.value)}
150
- onKeyDown={handleKeyDown}
151
- />
152
- </div>
153
- <div className="min-w-0 flex-1">
154
- <PasswordInput
155
- label="Confirm Password"
156
- placeholder="Confirm Password"
157
- value={confirmPassword}
158
- error={errors?.confirmPassword}
159
- disabled={fieldsDisabled}
160
- onChange={(event) => onConfirmPasswordChange(event.target.value)}
161
- onKeyDown={handleKeyDown}
162
- />
163
- </div>
164
- </div>
121
+ {/* Name + password fields — single column on every breakpoint */}
122
+ <Input
123
+ label="First Name"
124
+ placeholder="Enter First Name"
125
+ value={firstName}
126
+ error={firstNameErr.error}
127
+ disabled={fieldsDisabled}
128
+ onBlur={firstNameErr.onBlur}
129
+ onChange={(event) => onFirstNameChange(event.target.value)}
130
+ onKeyDown={handleKeyDown}
131
+ />
132
+ <Input
133
+ label="Last Name"
134
+ placeholder="Enter Last Name"
135
+ value={lastName}
136
+ error={lastNameErr.error}
137
+ disabled={fieldsDisabled}
138
+ onBlur={lastNameErr.onBlur}
139
+ onChange={(event) => onLastNameChange(event.target.value)}
140
+ onKeyDown={handleKeyDown}
141
+ />
142
+ <PasswordInput
143
+ label="Password"
144
+ placeholder="Enter Password"
145
+ value={password}
146
+ error={passwordErr.error}
147
+ disabled={fieldsDisabled}
148
+ onBlur={passwordErr.onBlur}
149
+ onChange={(event) => onPasswordChange(event.target.value)}
150
+ onKeyDown={handleKeyDown}
151
+ />
152
+ <PasswordInput
153
+ label="Confirm Password"
154
+ placeholder="Confirm Password"
155
+ value={confirmPassword}
156
+ error={confirmErr.error}
157
+ disabled={fieldsDisabled}
158
+ onBlur={confirmErr.onBlur}
159
+ onChange={(event) => onConfirmPasswordChange(event.target.value)}
160
+ onKeyDown={handleKeyDown}
161
+ />
165
162
 
166
163
  {/* Actions — optional back + submit */}
167
164
  <div className="flex items-center gap-[var(--spacing-system-l)]">
@@ -2,6 +2,7 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { cn } from '../../../utils/cn'
5
+ import { useDeferredError } from '../../../hooks/ui/use-deferred-error'
5
6
  import { Button } from '../../ui/button'
6
7
  import { CheckboxBlock } from '../../ui/checkbox-block'
7
8
  import { Input } from '../../ui/input'
@@ -91,6 +92,11 @@ export function CreateOrganizationForm({
91
92
  const isSsoMode = !!ssoProviders && ssoProviders.length > 0
92
93
  const fieldsDisabled = disabled || loading || isSsoMode
93
94
 
95
+ // Validation messages are deferred while the user is typing (shown on blur or after a pause).
96
+ const emailErr = useDeferredError(errors?.email, email)
97
+ const orgNameErr = useDeferredError(errors?.organizationName, organizationName)
98
+ const domainErr = useDeferredError(errors?.domain, domain)
99
+
94
100
  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
95
101
  if (event.key === 'Enter' && !fieldsDisabled) {
96
102
  onSubmit()
@@ -110,33 +116,29 @@ export function CreateOrganizationForm({
110
116
  <p className="text-h4 text-ods-text-secondary">Start your journey with OpenFrame.</p>
111
117
  </div>
112
118
 
113
- {/* Email + Organization Name — side by side on every breakpoint */}
114
- <div className="flex gap-[var(--spacing-system-l)]">
115
- <div className="min-w-0 flex-1">
116
- <Input
117
- type="email"
118
- label="Email"
119
- placeholder="username@mail.com"
120
- value={email}
121
- error={errors?.email ?? emailStatus?.message}
122
- errorVariant={errors?.email ? 'error' : emailStatus?.variant}
123
- disabled={fieldsDisabled}
124
- onChange={(event) => onEmailChange(event.target.value)}
125
- onKeyDown={handleKeyDown}
126
- />
127
- </div>
128
- <div className="min-w-0 flex-1">
129
- <Input
130
- label="Organization Name"
131
- placeholder="Your Company Name"
132
- value={organizationName}
133
- error={errors?.organizationName}
134
- disabled={fieldsDisabled}
135
- onChange={(event) => onOrganizationNameChange(event.target.value)}
136
- onKeyDown={handleKeyDown}
137
- />
138
- </div>
139
- </div>
119
+ {/* Email + Organization Name — single column on every breakpoint */}
120
+ <Input
121
+ type="email"
122
+ label="Email"
123
+ placeholder="username@mail.com"
124
+ value={email}
125
+ error={emailErr.error ?? emailStatus?.message}
126
+ errorVariant={emailErr.error ? 'error' : emailStatus?.variant}
127
+ disabled={fieldsDisabled}
128
+ onBlur={emailErr.onBlur}
129
+ onChange={(event) => onEmailChange(event.target.value)}
130
+ onKeyDown={handleKeyDown}
131
+ />
132
+ <Input
133
+ label="Organization Name"
134
+ placeholder="Your Company Name"
135
+ value={organizationName}
136
+ error={orgNameErr.error}
137
+ disabled={fieldsDisabled}
138
+ onBlur={orgNameErr.onBlur}
139
+ onChange={(event) => onOrganizationNameChange(event.target.value)}
140
+ onKeyDown={handleKeyDown}
141
+ />
140
142
 
141
143
  {/* Domain */}
142
144
  <div className="flex flex-col">
@@ -144,15 +146,15 @@ export function CreateOrganizationForm({
144
146
  label="Domain"
145
147
  placeholder={domainPlaceholder}
146
148
  value={domain}
147
- error={errors?.domain ?? domainStatus?.message}
148
- errorVariant={errors?.domain ? 'error' : domainStatus?.variant}
149
+ error={domainErr.error ?? domainStatus?.message}
150
+ errorVariant={domainErr.error ? 'error' : domainStatus?.variant}
149
151
  disabled={fieldsDisabled}
152
+ onBlur={domainErr.onBlur}
150
153
  endAdornment={domainSuffix ? <span className="whitespace-nowrap">{domainSuffix}</span> : undefined}
151
154
  onChange={(event) => onDomainChange(event.target.value)}
152
155
  onKeyDown={handleKeyDown}
153
156
  />
154
- {/* Top padding clears the absolutely-positioned field message */}
155
- {domainSlot && <div className="pt-[var(--spacing-system-l)]">{domainSlot}</div>}
157
+ {domainSlot && <div className="pt-[var(--spacing-system-s)]">{domainSlot}</div>}
156
158
  </div>
157
159
 
158
160
  {/* Terms & Privacy */}
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type * as React from 'react'
4
4
  import { cn } from '../../../utils/cn'
5
+ import { useDeferredError } from '../../../hooks/ui/use-deferred-error'
5
6
  import { Button } from '../../ui/button'
6
7
  import { Input } from '../../ui/input'
7
8
  import type { AuthSsoProvider } from './sso-providers'
@@ -61,6 +62,9 @@ export function LoginForm({
61
62
  const isSsoMode = !!ssoProviders && ssoProviders.length > 0
62
63
  const fieldDisabled = disabled || loading || isSsoMode
63
64
 
65
+ // Validation messages are deferred while the user is typing (shown on blur or after a pause).
66
+ const emailErr = useDeferredError(errors?.email, email)
67
+
64
68
  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
65
69
  if (event.key === 'Enter' && !fieldDisabled) {
66
70
  onSubmit()
@@ -86,8 +90,9 @@ export function LoginForm({
86
90
  label="Email"
87
91
  placeholder={emailPlaceholder}
88
92
  value={email}
89
- error={errors?.email}
93
+ error={emailErr.error}
90
94
  disabled={fieldDisabled}
95
+ onBlur={emailErr.onBlur}
91
96
  onChange={(event) => onEmailChange(event.target.value)}
92
97
  onKeyDown={handleKeyDown}
93
98
  />
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type * as React from 'react'
4
4
  import { cn } from '../../../utils/cn'
5
+ import { useDeferredError } from '../../../hooks/ui/use-deferred-error'
5
6
  import { Button } from '../../ui/button'
6
7
  import { Input } from '../../ui/input'
7
8
  import { PasswordInput } from '../../ui/password-input'
@@ -60,6 +61,10 @@ export function OpenFrameSsoLoginForm({
60
61
  }: OpenFrameSsoLoginFormProps) {
61
62
  const fieldsDisabled = disabled || loading
62
63
 
64
+ // Validation messages are deferred while the user is typing (shown on blur or after a pause).
65
+ const emailErr = useDeferredError(errors?.email, email)
66
+ const passwordErr = useDeferredError(errors?.password, password)
67
+
63
68
  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
64
69
  if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {
65
70
  onSubmit()
@@ -84,8 +89,9 @@ export function OpenFrameSsoLoginForm({
84
89
  label={emailLabel}
85
90
  placeholder={emailPlaceholder}
86
91
  value={email}
87
- error={errors?.email}
92
+ error={emailErr.error}
88
93
  disabled={fieldsDisabled}
94
+ onBlur={emailErr.onBlur}
89
95
  onChange={(event) => onEmailChange(event.target.value)}
90
96
  onKeyDown={handleKeyDown}
91
97
  />
@@ -94,8 +100,9 @@ export function OpenFrameSsoLoginForm({
94
100
  label={passwordLabel}
95
101
  placeholder={passwordPlaceholder}
96
102
  value={password}
97
- error={errors?.password}
103
+ error={passwordErr.error}
98
104
  disabled={fieldsDisabled}
105
+ onBlur={passwordErr.onBlur}
99
106
  onChange={(event) => onPasswordChange(event.target.value)}
100
107
  onKeyDown={handleKeyDown}
101
108
  />
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type * as React from 'react'
4
4
  import { cn } from '../../../utils/cn'
5
+ import { useDeferredError } from '../../../hooks/ui/use-deferred-error'
5
6
  import { Button } from '../../ui/button'
6
7
  import { Input } from '../../ui/input'
7
8
  import { PasswordInput } from '../../ui/password-input'
@@ -72,6 +73,13 @@ export function OpenFrameSsoSignUpForm({
72
73
  }: OpenFrameSsoSignUpFormProps) {
73
74
  const fieldsDisabled = disabled || loading
74
75
 
76
+ // Validation messages are deferred while the user is typing (shown on blur or after a pause).
77
+ const emailErr = useDeferredError(errors?.email, email)
78
+ const firstNameErr = useDeferredError(errors?.firstName, firstName)
79
+ const lastNameErr = useDeferredError(errors?.lastName, lastName)
80
+ const passwordErr = useDeferredError(errors?.password, password)
81
+ const confirmErr = useDeferredError(errors?.confirmPassword, confirmPassword)
82
+
75
83
  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
76
84
  if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {
77
85
  onSubmit()
@@ -96,63 +104,54 @@ export function OpenFrameSsoSignUpForm({
96
104
  label={emailLabel}
97
105
  placeholder="username@mail.com"
98
106
  value={email}
99
- error={errors?.email}
107
+ error={emailErr.error}
100
108
  disabled={fieldsDisabled || emailReadOnly}
109
+ onBlur={emailErr.onBlur}
101
110
  onChange={(event) => onEmailChange(event.target.value)}
102
111
  onKeyDown={handleKeyDown}
103
112
  />
104
113
 
105
- {/* First + Last name */}
106
- <div className="flex gap-[var(--spacing-system-l)]">
107
- <div className="min-w-0 flex-1">
108
- <Input
109
- label="First Name"
110
- placeholder="Enter First Name"
111
- value={firstName}
112
- error={errors?.firstName}
113
- disabled={fieldsDisabled}
114
- onChange={(event) => onFirstNameChange(event.target.value)}
115
- onKeyDown={handleKeyDown}
116
- />
117
- </div>
118
- <div className="min-w-0 flex-1">
119
- <Input
120
- label="Last Name"
121
- placeholder="Enter Last Name"
122
- value={lastName}
123
- error={errors?.lastName}
124
- disabled={fieldsDisabled}
125
- onChange={(event) => onLastNameChange(event.target.value)}
126
- onKeyDown={handleKeyDown}
127
- />
128
- </div>
129
- </div>
130
-
131
- {/* Password + Confirm */}
132
- <div className="flex gap-[var(--spacing-system-l)]">
133
- <div className="min-w-0 flex-1">
134
- <PasswordInput
135
- label="Password"
136
- placeholder="Enter Password"
137
- value={password}
138
- error={errors?.password}
139
- disabled={fieldsDisabled}
140
- onChange={(event) => onPasswordChange(event.target.value)}
141
- onKeyDown={handleKeyDown}
142
- />
143
- </div>
144
- <div className="min-w-0 flex-1">
145
- <PasswordInput
146
- label="Confirm Password"
147
- placeholder="Confirm Password"
148
- value={confirmPassword}
149
- error={errors?.confirmPassword}
150
- disabled={fieldsDisabled}
151
- onChange={(event) => onConfirmPasswordChange(event.target.value)}
152
- onKeyDown={handleKeyDown}
153
- />
154
- </div>
155
- </div>
114
+ {/* Name + password fields — single column on every breakpoint */}
115
+ <Input
116
+ label="First Name"
117
+ placeholder="Enter First Name"
118
+ value={firstName}
119
+ error={firstNameErr.error}
120
+ disabled={fieldsDisabled}
121
+ onBlur={firstNameErr.onBlur}
122
+ onChange={(event) => onFirstNameChange(event.target.value)}
123
+ onKeyDown={handleKeyDown}
124
+ />
125
+ <Input
126
+ label="Last Name"
127
+ placeholder="Enter Last Name"
128
+ value={lastName}
129
+ error={lastNameErr.error}
130
+ disabled={fieldsDisabled}
131
+ onBlur={lastNameErr.onBlur}
132
+ onChange={(event) => onLastNameChange(event.target.value)}
133
+ onKeyDown={handleKeyDown}
134
+ />
135
+ <PasswordInput
136
+ label="Password"
137
+ placeholder="Enter Password"
138
+ value={password}
139
+ error={passwordErr.error}
140
+ disabled={fieldsDisabled}
141
+ onBlur={passwordErr.onBlur}
142
+ onChange={(event) => onPasswordChange(event.target.value)}
143
+ onKeyDown={handleKeyDown}
144
+ />
145
+ <PasswordInput
146
+ label="Confirm Password"
147
+ placeholder="Confirm Password"
148
+ value={confirmPassword}
149
+ error={confirmErr.error}
150
+ disabled={fieldsDisabled}
151
+ onBlur={confirmErr.onBlur}
152
+ onChange={(event) => onConfirmPasswordChange(event.target.value)}
153
+ onKeyDown={handleKeyDown}
154
+ />
156
155
 
157
156
  {/* Forgot password + Continue */}
158
157
  <div className="flex items-center gap-[var(--spacing-system-l)]">
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type * as React from 'react'
4
4
  import { cn } from '../../../utils/cn'
5
+ import { useDeferredError } from '../../../hooks/ui/use-deferred-error'
5
6
  import { Button } from '../../ui/button'
6
7
  import { Input } from '../../ui/input'
7
8
  import { BackToLoginLink } from './back-to-login-link'
@@ -66,6 +67,10 @@ export function PasswordResetForm({
66
67
  }: PasswordResetFormProps) {
67
68
  const fieldsDisabled = disabled || loading
68
69
 
70
+ // Validation messages are deferred while the user is typing (shown on blur or after a pause).
71
+ const passwordErr = useDeferredError(errors?.password, password)
72
+ const confirmErr = useDeferredError(errors?.confirmPassword, confirmPassword)
73
+
69
74
  const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
70
75
  if (event.key === 'Enter' && !fieldsDisabled && !submitDisabled) {
71
76
  onSubmit()
@@ -91,8 +96,9 @@ export function PasswordResetForm({
91
96
  label={newPasswordLabel}
92
97
  placeholder={newPasswordPlaceholder}
93
98
  value={password}
94
- error={errors?.password}
99
+ error={passwordErr.error}
95
100
  disabled={fieldsDisabled}
101
+ onBlur={passwordErr.onBlur}
96
102
  onChange={(event) => onPasswordChange(event.target.value)}
97
103
  onKeyDown={handleKeyDown}
98
104
  />
@@ -103,8 +109,9 @@ export function PasswordResetForm({
103
109
  label={confirmPasswordLabel}
104
110
  placeholder={confirmPasswordPlaceholder}
105
111
  value={confirmPassword}
106
- error={errors?.confirmPassword}
112
+ error={confirmErr.error}
107
113
  disabled={fieldsDisabled}
114
+ onBlur={confirmErr.onBlur}
108
115
  onChange={(event) => onConfirmPasswordChange(event.target.value)}
109
116
  onKeyDown={handleKeyDown}
110
117
  />
@@ -6,7 +6,7 @@ import { cn } from "../../utils/cn"
6
6
  export interface FieldWrapperProps {
7
7
  /** Label text displayed above the field */
8
8
  label?: string
9
- /** Status message displayed below the field. Space is always reserved to prevent layout shifts. */
9
+ /** Status message displayed below the field. Wraps up to two lines, then ellipsizes. */
10
10
  error?: string
11
11
  /** Color variant for the message: "error" (red), "warning" (yellow), "success" (green) or "muted" (grey) */
12
12
  errorVariant?: "error" | "warning" | "success" | "muted"
@@ -27,7 +27,7 @@ const FieldWrapper = React.forwardRef<HTMLDivElement, FieldWrapperProps>(
27
27
  const hasChrome = label != null || error != null
28
28
 
29
29
  return (
30
- <div ref={ref} className={cn(hasChrome ? "relative flex w-full flex-col" : "contents", className)}>
30
+ <div ref={ref} className={cn(hasChrome ? "flex w-full flex-col" : "contents", className)}>
31
31
  {label && (
32
32
  <label className="text-h4 text-ods-text-primary mb-1">
33
33
  {label}
@@ -35,7 +35,10 @@ const FieldWrapper = React.forwardRef<HTMLDivElement, FieldWrapperProps>(
35
35
  )}
36
36
  {children}
37
37
  {error && (
38
- <p className={cn("absolute bottom-0 left-0 right-0 translate-y-full text-h6 truncate", errorVariantClasses[errorVariant])} title={error}>
38
+ <p
39
+ className={cn("mt-1 text-h6 break-words line-clamp-2", errorVariantClasses[errorVariant])}
40
+ title={error}
41
+ >
39
42
  {error}
40
43
  </p>
41
44
  )}
@@ -1,6 +1,7 @@
1
1
  // UI Hooks exports
2
2
  export * from './use-auto-limit-tags'
3
3
  export * from './use-debounce'
4
+ export * from './use-deferred-error'
4
5
  export * from './use-header-height'
5
6
  export * from './use-horizontal-scrollbar'
6
7
  export * from './use-image-edge-color'
@@ -0,0 +1,38 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+
5
+ export interface UseDeferredErrorResult {
6
+ /** The message to render (undefined while the reveal is deferred or the field is valid). */
7
+ error: string | undefined
8
+ /** Attach to the field's onBlur — reveals validation immediately and keeps it live from then on. */
9
+ onBlur: () => void
10
+ }
11
+
12
+ /**
13
+ * Defers showing a field validation message while the user is still typing.
14
+ *
15
+ * The message is revealed when the field loses focus (touched) or when the
16
+ * value hasn't changed for `delay` ms; every keystroke restarts the timer.
17
+ * Once touched, validation stays live. A message that clears (the value became
18
+ * valid) disappears immediately.
19
+ */
20
+ export function useDeferredError(error: string | undefined, value: string, delay = 1500): UseDeferredErrorResult {
21
+ const [touched, setTouched] = React.useState(false)
22
+ const [paused, setPaused] = React.useState(false)
23
+
24
+ // Typing hides the message and restarts the pause timer.
25
+ React.useEffect(() => {
26
+ if (touched) return
27
+ setPaused(false)
28
+ const timer = setTimeout(() => setPaused(true), delay)
29
+ return () => clearTimeout(timer)
30
+ }, [value, delay, touched])
31
+
32
+ const onBlur = React.useCallback(() => setTouched(true), [])
33
+
34
+ return {
35
+ error: error && (touched || paused) ? error : undefined,
36
+ onBlur,
37
+ }
38
+ }
@@ -219,3 +219,47 @@ export const AllVariants: Story = {
219
219
  </div>
220
220
  ),
221
221
  };
222
+
223
+ /**
224
+ * Error / status states. Long messages wrap up to two lines, then ellipsize.
225
+ */
226
+ export const ErrorState: Story = {
227
+ args: {
228
+ placeholder: 'Enter text...',
229
+ },
230
+ render: () => (
231
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '2.5rem', width: '480px' }}>
232
+ <Input label="Short error (no interaction)" placeholder="username@mail.com" error="Required field" />
233
+ <Input
234
+ label="Long error — wraps to two lines"
235
+ defaultValue="ada@example.com"
236
+ error="This email is already registered. Sign in instead or use a different email address to create the organization."
237
+ />
238
+ {/* Half-width pair, as on the Create Organization screen */}
239
+ <div style={{ display: 'flex', gap: '1.5rem' }}>
240
+ <Input
241
+ label="Email"
242
+ defaultValue="ada@example.com"
243
+ error="This email is already registered. Sign in instead."
244
+ />
245
+ <Input
246
+ label="Organization Name"
247
+ defaultValue="A"
248
+ error="Organization Name must be 2-100 characters"
249
+ />
250
+ </div>
251
+ <Input
252
+ label="Success status"
253
+ defaultValue="ada@example.com"
254
+ error="Email is available"
255
+ errorVariant="success"
256
+ />
257
+ <Input
258
+ label="Muted status (long, truncating)"
259
+ defaultValue="ada-lovelace-computing"
260
+ error="Checking availability against every registered organization domain, this may take a moment…"
261
+ errorVariant="muted"
262
+ />
263
+ </div>
264
+ ),
265
+ };
@@ -97,6 +97,26 @@ export const Filled: Story = {
97
97
  ),
98
98
  }
99
99
 
100
+ /**
101
+ * Error state: long messages under the fields wrap up to two lines.
102
+ * Validation is deferred — messages appear after a 1.5s typing pause or on blur.
103
+ */
104
+ export const ErrorState: Story = {
105
+ render: () => (
106
+ <CreateOrganizationPage
107
+ email="ada@example.com"
108
+ organizationName="A"
109
+ domainSuffix=".openframe.ai"
110
+ termsUrl="#terms"
111
+ privacyPolicyUrl="#privacy"
112
+ emailStatus={{ message: 'This email is already registered. Sign in instead.', variant: 'error' }}
113
+ errors={{
114
+ organizationName: 'Organization Name must be 2-100 characters',
115
+ }}
116
+ />
117
+ ),
118
+ }
119
+
100
120
  const SSO_PROVIDERS: AuthSsoProvider[] = ['openframe', 'google', 'microsoft']
101
121
 
102
122
  /** SSO configured: fields disabled, submit replaced by provider buttons. */