chordia-ui 3.3.0 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chordia-ui",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "description": "Chordia Design System - UI components, tokens, and Tailwind preset",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs.js",
@@ -153,7 +153,7 @@ export default function FileUploadingState({
153
153
  */
154
154
  export function FileUploadSuccessState({
155
155
  title = "File uploaded successfully!",
156
- subtitle = "13 files have been uploaded to your project",
156
+ subtitle = "Redirecting...",
157
157
  actionLabel = "Add more files",
158
158
  onAction,
159
159
  width = 648,
@@ -210,15 +210,15 @@ export function FileUploadSuccessState({
210
210
  {/* Subtitle */}
211
211
  <p
212
212
  style={{
213
- fontSize: "13px",
214
- fontWeight: 400,
213
+ fontSize: "14px",
214
+ fontWeight: 500,
215
215
  color: "var(--Grey-Strong, #808183)",
216
216
  margin: 0,
217
217
  lineHeight: "140%",
218
218
  textAlign: "center",
219
219
  }}
220
220
  >
221
- {/* {subtitle} */}
221
+ {subtitle}
222
222
  </p>
223
223
 
224
224
  {/* Action button */}
@@ -180,6 +180,7 @@ export default function LoginPage({
180
180
  const [signupConfirm, setSignupConfirm] = useState('');
181
181
  const [showSignupPassword, setShowSignupPassword] = useState(false);
182
182
  const [showSignupConfirm, setShowSignupConfirm] = useState(false);
183
+ const [signupError, setSignupError] = useState('');
183
184
  const [resetEmail, setResetEmail] = useState('');
184
185
  const [activeSlide, setActiveSlide] = useState(0);
185
186
  const SLIDE_COUNT = 3;
@@ -513,7 +514,7 @@ export default function LoginPage({
513
514
  <Field label="Create Password" gap={8}>
514
515
  <div style={{ position: 'relative' }}>
515
516
  <input type={showSignupPassword ? 'text' : 'password'} value={signupPassword}
516
- onChange={(e) => setSignupPassword(e.target.value)}
517
+ onChange={(e) => { setSignupPassword(e.target.value); setSignupError(''); }}
517
518
  placeholder="Enter password" autoComplete="new-password"
518
519
  style={{ ...inputBase, padding: '10px 44px 10px 14px' }}
519
520
  onFocus={focusGreen} onBlur={blurGray} />
@@ -524,7 +525,7 @@ export default function LoginPage({
524
525
  <Field label="Confirm Password" gap={8}>
525
526
  <div style={{ position: 'relative' }}>
526
527
  <input type={showSignupConfirm ? 'text' : 'password'} value={signupConfirm}
527
- onChange={(e) => setSignupConfirm(e.target.value)}
528
+ onChange={(e) => { setSignupConfirm(e.target.value); setSignupError(''); }}
528
529
  placeholder="Enter password" autoComplete="new-password"
529
530
  style={{ ...inputBase, padding: '10px 44px 10px 14px' }}
530
531
  onFocus={focusGreen} onBlur={blurGray} />
@@ -532,10 +533,27 @@ export default function LoginPage({
532
533
  </div>
533
534
  </Field>
534
535
 
536
+ {signupError && (
537
+ <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', padding: 8, gap: 6, width: '100%', boxSizing: 'border-box', background: 'var(--color-error-bg)', borderRadius: 5, fontSize: 15, fontWeight: 400, lineHeight: '22px', color: 'var(--color-text)', fontFamily: FF }}>
538
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="var(--color-text)" style={{ flexShrink: 0 }}>
539
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
540
+ </svg>
541
+ {signupError}
542
+ </div>
543
+ )}
544
+
535
545
  {(() => {
536
546
  const canCreate = firstName && lastName && signupEmail && signupPassword && signupConfirm;
537
547
  return (
538
- <GreenButton onClick={() => { onSignUp?.({ firstName, lastName, email: signupEmail, password: signupPassword }); setView('verifyemail'); }} disabled={!canCreate}>
548
+ <GreenButton onClick={() => {
549
+ setSignupError('');
550
+ if (signupPassword !== signupConfirm) {
551
+ setSignupError('Password and Confirm Password do not match!');
552
+ return;
553
+ }
554
+ onSignUp?.({ firstName, lastName, email: signupEmail, password: signupPassword });
555
+ setView('verifyemail');
556
+ }} disabled={!canCreate}>
539
557
  Create Account
540
558
  </GreenButton>
541
559
  );
@@ -1,5 +1,5 @@
1
1
  import { useState, useRef, useEffect } from 'react';
2
- import { ChevronDown, Trash, SquareUser } from 'lucide-react';
2
+ import { ChevronDown, Trash, SquareUser, Check } from 'lucide-react';
3
3
 
4
4
  const FF = 'var(--font-sans)';
5
5
 
@@ -29,14 +29,21 @@ const AddTeammates = ({
29
29
  onInvite,
30
30
  }) => {
31
31
  const [inputValue, setInputValue] = useState('');
32
+ const [emailTags, setEmailTags] = useState([]);
33
+ const [editingIndex, setEditingIndex] = useState(null);
34
+ const [editingValue, setEditingValue] = useState('');
35
+ const [emailError, setEmailError] = useState('');
32
36
  const [members, setMembers] = useState([]);
33
37
  const [openDropdownIdx, setOpenDropdownIdx] = useState(null);
34
38
  const [openProjectIdx, setOpenProjectIdx] = useState(null);
35
39
  const inputRef = useRef(null);
40
+ const editInputRef = useRef(null);
36
41
  const dropdownRef = useRef(null);
37
42
 
38
43
  const resolvedDefaultProject = defaultProject || projects[0] || '';
39
44
 
45
+ const isValidEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
46
+
40
47
  // Close dropdowns on outside click
41
48
  useEffect(() => {
42
49
  if (openDropdownIdx === null && openProjectIdx === null) return;
@@ -50,19 +57,102 @@ const AddTeammates = ({
50
57
  return () => document.removeEventListener('mousedown', handleClick);
51
58
  }, [openDropdownIdx, openProjectIdx]);
52
59
 
60
+ // Focus the inline edit input when editingIndex changes
61
+ useEffect(() => {
62
+ if (editingIndex !== null && editInputRef.current) {
63
+ editInputRef.current.focus();
64
+ }
65
+ }, [editingIndex]);
66
+
67
+ const handleInputChange = (e) => {
68
+ const val = e.target.value;
69
+ setEmailError('');
70
+ if (val.endsWith(',')) {
71
+ const email = val.slice(0, -1).trim();
72
+ if (!email) { setInputValue(''); return; }
73
+ if (!isValidEmail(email)) {
74
+ setEmailError(`"${email}" is not a valid email address.`);
75
+ return;
76
+ }
77
+ if (!emailTags.includes(email)) {
78
+ setEmailTags([...emailTags, email]);
79
+ }
80
+ setInputValue('');
81
+ } else {
82
+ setInputValue(val);
83
+ }
84
+ };
85
+
86
+ const handleTagClick = (idx) => {
87
+ setEditingIndex(idx);
88
+ setEditingValue(emailTags[idx]);
89
+ };
90
+
91
+ const commitEdit = (idx) => {
92
+ const trimmed = editingValue.trim();
93
+ setEmailError('');
94
+ if (!trimmed) {
95
+ setEmailTags(emailTags.filter((_, i) => i !== idx));
96
+ } else if (!isValidEmail(trimmed)) {
97
+ setEmailError(`"${trimmed}" is not a valid email address.`);
98
+ return;
99
+ } else if (trimmed !== emailTags[idx]) {
100
+ setEmailTags(emailTags.map((t, i) => (i === idx ? trimmed : t)));
101
+ }
102
+ setEditingIndex(null);
103
+ setEditingValue('');
104
+ };
105
+
106
+ const handleEditChange = (e) => {
107
+ const val = e.target.value;
108
+ if (val.endsWith(',')) {
109
+ // Commit the edit (strip comma)
110
+ setEditingValue(val.slice(0, -1));
111
+ commitEdit(editingIndex);
112
+ } else {
113
+ setEditingValue(val);
114
+ }
115
+ };
116
+
117
+ const handleEditKeyDown = (e, idx) => {
118
+ if (e.key === 'Enter') {
119
+ e.preventDefault();
120
+ commitEdit(idx);
121
+ }
122
+ if (e.key === 'Escape') {
123
+ // Cancel editing, revert
124
+ setEditingIndex(null);
125
+ setEditingValue('');
126
+ }
127
+ if (e.key === 'Backspace' && !editingValue) {
128
+ // Empty and backspace — remove the tag
129
+ setEmailTags(emailTags.filter((_, i) => i !== idx));
130
+ setEditingIndex(null);
131
+ setEditingValue('');
132
+ }
133
+ };
134
+
53
135
  const addEmails = () => {
54
- const raw = inputValue
55
- .split(',')
56
- .map((e) => e.trim())
57
- .filter((e) => e.includes('@'));
136
+ setEmailError('');
137
+ const trimmed = inputValue.trim();
138
+ if (trimmed && !isValidEmail(trimmed)) {
139
+ setEmailError(`"${trimmed}" is not a valid email address.`);
140
+ return;
141
+ }
142
+ const allEmails = [...emailTags];
143
+ if (trimmed && isValidEmail(trimmed)) {
144
+ allEmails.push(trimmed);
145
+ }
146
+ if (!allEmails.length) return;
58
147
  const existing = new Set(members.map((m) => m.email));
59
- const newMembers = raw
148
+ const newMembers = [...new Set(allEmails)]
60
149
  .filter((e) => !existing.has(e))
61
- .map((email) => ({ email, role: defaultRole, project: resolvedDefaultProject }));
150
+ .map((email) => ({ email, role: defaultRole, project: resolvedDefaultProject, invited: false }));
62
151
  if (newMembers.length) {
63
152
  setMembers([...members, ...newMembers]);
64
- setInputValue('');
65
153
  }
154
+ setEmailTags([]);
155
+ setInputValue('');
66
156
  };
67
157
 
68
158
  const handleKeyDown = (e) => {
@@ -70,6 +160,10 @@ const AddTeammates = ({
70
160
  e.preventDefault();
71
161
  addEmails();
72
162
  }
163
+ if (e.key === 'Backspace' && !inputValue && emailTags.length) {
164
+ // Start editing the last tag
165
+ handleTagClick(emailTags.length - 1);
166
+ }
73
167
  };
74
168
 
75
169
  const removeMember = (idx) => {
@@ -83,11 +177,14 @@ const AddTeammates = ({
83
177
 
84
178
  const handleSendInvite = () => {
85
179
  if (members.length) {
180
+ setMembers(members.map((m) => ({ ...m, invited: true })));
86
181
  onSendInvite?.(members);
87
182
  onInvite?.(members.map((m) => m.email));
88
183
  }
89
184
  };
90
185
 
186
+ const hasPermissions = (m) => m.role && m.project;
187
+
91
188
  return (
92
189
  <div style={{ fontFamily: FF }}>
93
190
  {/* Header */}
@@ -115,40 +212,68 @@ const AddTeammates = ({
115
212
  flexWrap: 'wrap',
116
213
  }}
117
214
  >
118
- {/* Email tags from input */}
119
- {inputValue.split(',').filter((s) => s.trim()).length > 1 &&
120
- inputValue
121
- .split(',')
122
- .map((s) => s.trim())
123
- .filter((s) => s.includes('@'))
124
- .map((email, i) => (
215
+ {emailTags.map((email, i) => (
216
+ <span
217
+ key={i}
218
+ style={{ display: 'inline-flex', alignItems: 'center', gap: 2 }}
219
+ >
220
+ {editingIndex === i ? (
221
+ <input
222
+ ref={editInputRef}
223
+ type="text"
224
+ autoComplete="off"
225
+ value={editingValue}
226
+ onChange={handleEditChange}
227
+ onKeyDown={(e) => handleEditKeyDown(e, i)}
228
+ onBlur={() => commitEdit(i)}
229
+ style={{
230
+ border: 'none',
231
+ outline: 'none',
232
+ fontSize: 14,
233
+ fontWeight: 500,
234
+ fontFamily: FF,
235
+ color: 'var(--grey-strong, #2E3236)',
236
+ background: '#F3F7F7',
237
+ borderRadius: 6,
238
+ padding: '4px 8px',
239
+ width: `${Math.max(editingValue.length, 1) * 8.5 + 16}px`,
240
+ }}
241
+ />
242
+ ) : (
125
243
  <span
126
- key={i}
244
+ onClick={(e) => { e.stopPropagation(); handleTagClick(i); }}
127
245
  style={{
128
- display: 'inline-flex',
246
+ display: 'flex',
247
+ padding: '4px 8px',
248
+ justifyContent: 'center',
129
249
  alignItems: 'center',
130
- padding: '4px 10px',
250
+ gap: 10,
131
251
  borderRadius: 6,
132
- background: 'var(--hover-warm, #F5F0E8)',
252
+ background: '#F3F7F7',
133
253
  fontSize: 14,
134
254
  fontWeight: 500,
135
255
  color: 'var(--grey-strong, #2E3236)',
136
256
  whiteSpace: 'nowrap',
257
+ cursor: 'text',
137
258
  }}
138
259
  >
139
260
  {email}
140
261
  </span>
141
- ))}
262
+ )}
263
+ <span style={{ fontSize: 14, color: 'var(--grey-strong, #2E3236)' }}>,</span>
264
+ </span>
265
+ ))}
142
266
  <input
143
267
  ref={inputRef}
144
268
  type="text"
145
- placeholder="Enter email addresses separated by commas"
269
+ autoComplete="off"
270
+ placeholder={emailTags.length ? '' : 'Enter email addresses separated by commas'}
146
271
  value={inputValue}
147
- onChange={(e) => setInputValue(e.target.value)}
272
+ onChange={handleInputChange}
148
273
  onKeyDown={handleKeyDown}
149
274
  style={{
150
275
  flex: 1,
151
- minWidth: 200,
276
+ minWidth: 80,
152
277
  border: 'none',
153
278
  outline: 'none',
154
279
  fontSize: 14,
@@ -183,6 +308,16 @@ const AddTeammates = ({
183
308
  </button>
184
309
  </div>
185
310
 
311
+ {/* Validation error */}
312
+ {emailError && (
313
+ <div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', padding: 8, gap: 6, width: '100%', boxSizing: 'border-box', background: 'var(--color-error-bg, #F5F0E8)', borderRadius: 5, fontSize: 15, fontWeight: 400, lineHeight: '22px', color: 'var(--color-text, #2E3236)', fontFamily: FF, marginTop: 8 }}>
314
+ <svg width="17" height="17" viewBox="0 0 24 24" fill="var(--color-text, #2E3236)" style={{ flexShrink: 0 }}>
315
+ <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
316
+ </svg>
317
+ {emailError}
318
+ </div>
319
+ )}
320
+
186
321
  {/* Invited Members section */}
187
322
  {members.length > 0 && (
188
323
  <div style={{ marginTop: 24 }}>
@@ -191,27 +326,53 @@ const AddTeammates = ({
191
326
  <span style={{ fontSize: 14, fontWeight: 400, fontFamily: 'Varta, var(--font-sans)', fontStyle: 'normal', lineHeight: 'normal', color: 'var(--Content-Tertiary, #676767)' }}>
192
327
  Invited Members
193
328
  </span>
194
- <button
195
- onClick={handleSendInvite}
196
- style={{
197
- display: 'flex',
198
- height: 28,
199
- padding: 10,
200
- justifyContent: 'center',
201
- alignItems: 'center',
202
- gap: 10,
203
- borderRadius: 10,
204
- border: '1px solid var(--Base-absent, #D9D9D9)',
205
- background: 'var(--Base-White, #FFF)',
206
- fontSize: 14,
207
- fontWeight: 600,
208
- fontFamily: FF,
209
- color: 'var(--Content-Primary, #2E3236)',
210
- cursor: 'pointer',
211
- }}
212
- >
213
- Send Invite
214
- </button>
329
+ {members.every((m) => m.invited) ? (
330
+ <button
331
+ style={{
332
+ display: 'flex',
333
+ height: 28,
334
+ minWidth: 80,
335
+ padding: '16px 12px',
336
+ justifyContent: 'center',
337
+ alignItems: 'center',
338
+ gap: 10,
339
+ borderRadius: 10,
340
+ border: '1px solid var(--Base-absent, #D9D9D9)',
341
+ background: 'var(--Base-White, #FFF)',
342
+ fontSize: 14,
343
+ fontWeight: 500,
344
+ fontFamily: FF,
345
+ color: 'var(--Content-Primary, #2E3236)',
346
+ cursor: 'default',
347
+ }}
348
+ >
349
+ <Check size={16} />
350
+ Sent
351
+ </button>
352
+ ) : (
353
+ <button
354
+ onClick={handleSendInvite}
355
+ style={{
356
+ display: 'flex',
357
+ height: 28,
358
+ minWidth: 80,
359
+ padding: '16px 12px',
360
+ justifyContent: 'center',
361
+ alignItems: 'center',
362
+ gap: 10,
363
+ borderRadius: 10,
364
+ border: '1px solid var(--Base-absent, #D9D9D9)',
365
+ background: 'var(--Base-White, #FFF)',
366
+ fontSize: 14,
367
+ fontWeight: 500,
368
+ fontFamily: FF,
369
+ color: 'var(--Content-Primary, #2E3236)',
370
+ cursor: 'pointer',
371
+ }}
372
+ >
373
+ Send Invite
374
+ </button>
375
+ )}
215
376
  </div>
216
377
 
217
378
  {/* Members list */}
@@ -256,28 +417,46 @@ const AddTeammates = ({
256
417
  </div>
257
418
  </div>
258
419
 
259
- {/* Right: Set Permissions dropdown */}
260
- <div style={{ position: 'relative' }} ref={openDropdownIdx === idx ? dropdownRef : null}>
261
- <button
262
- onClick={() => setOpenDropdownIdx(openDropdownIdx === idx ? null : idx)}
263
- style={{
264
- display: 'flex',
265
- alignItems: 'center',
266
- gap: 8,
267
- padding: '8px 12px',
268
- borderRadius: 10,
269
- border: '1px solid var(--Border-Subtle, #E6E6E6)',
270
- background: 'var(--Base-White, #FFF)',
271
- fontSize: 14,
272
- fontWeight: 500,
273
- fontFamily: FF,
274
- color: 'var(--Content-Primary, #2E3236)',
275
- cursor: 'pointer',
276
- whiteSpace: 'nowrap',
277
- }}
278
- >
279
- Set Permissions
280
- <ChevronDown size={16} color="var(--Grey-Strong, #808183)" />
420
+ {/* Right: Invite sent + dropdown */}
421
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
422
+ {/* Invite sent label */}
423
+ {member.invited && hasPermissions(member) && (
424
+ <span style={{ fontFamily: 'Varta, var(--font-sans)', fontSize: 14, fontStyle: 'normal', fontWeight: 400, lineHeight: 'normal', color: 'var(--Content-Tertiary, #676767)', whiteSpace: 'nowrap' }}>
425
+ Invite sent
426
+ </span>
427
+ )}
428
+
429
+ {/* Dropdown trigger */}
430
+ <div style={{ position: 'relative' }} ref={openDropdownIdx === idx ? dropdownRef : null}>
431
+ <button
432
+ onClick={() => setOpenDropdownIdx(openDropdownIdx === idx ? null : idx)}
433
+ style={{
434
+ display: 'flex',
435
+ width: 160,
436
+ height: 32,
437
+ minWidth: 96,
438
+ padding: '16px 12px 16px 16px',
439
+ justifyContent: 'center',
440
+ alignItems: 'center',
441
+ gap: 10,
442
+ borderRadius: 10,
443
+ border: '1px solid var(--Base-absent, #D9D9D9)',
444
+ background: 'var(--Base-White, #FFF)',
445
+ fontSize: 13,
446
+ fontWeight: 500,
447
+ fontFamily: FF,
448
+ color: 'var(--Content-Primary, #2E3236)',
449
+ cursor: 'pointer',
450
+ whiteSpace: 'nowrap',
451
+ overflow: 'hidden',
452
+ textOverflow: 'ellipsis',
453
+ boxSizing: 'border-box',
454
+ }}
455
+ >
456
+ <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', flex: 1, textAlign: 'left' }}>
457
+ {hasPermissions(member) ? `${member.role}, ${member.project}` : 'Set Permissions'}
458
+ </span>
459
+ <ChevronDown size={16} color="var(--Grey-Strong, #808183)" style={{ flexShrink: 0 }} />
281
460
  </button>
282
461
 
283
462
  {/* Dropdown */}
@@ -438,6 +617,7 @@ const AddTeammates = ({
438
617
  </div>
439
618
  </div>
440
619
  )}
620
+ </div>
441
621
  </div>
442
622
  </div>
443
623
  ))}