@enderfall/ui 0.2.20 → 0.2.21

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,7 +1,7 @@
1
1
  {
2
2
  "name": "@enderfall/ui",
3
3
  "private": false,
4
- "version": "0.2.20",
4
+ "version": "0.2.21",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -28,22 +28,22 @@
28
28
  }
29
29
 
30
30
  .ef-button--glow {
31
- box-shadow: var(--ef-button-current-glow-shadow, var(--ef-button-glow-shadow, 0 0 24px rgba(124, 77, 255, 0.45)));
31
+ box-shadow: var(--ef-button-current-hover-shadow, var(--ef-button-hover-shadow, 0 0 18px rgba(124, 77, 255, 0.35)));
32
32
  }
33
33
 
34
34
  .ef-button--glow:not(:disabled):hover {
35
- box-shadow: var(--ef-button-current-hover-shadow, var(--ef-button-hover-shadow, 0 0 18px rgba(124, 77, 255, 0.35)));
35
+ box-shadow: var(--ef-button-current-glow-shadow, var(--ef-button-glow-shadow, 0 0 24px rgba(124, 77, 255, 0.45)));
36
36
  animation: none;
37
37
  }
38
38
 
39
39
  .ef-button--glow:focus-visible {
40
- box-shadow: var(--ef-button-current-focus-shadow, var(--ef-button-focus-shadow, 0 0 18px rgba(124, 77, 255, 0.35)));
40
+ box-shadow: var(--ef-button-current-glow-shadow, var(--ef-button-glow-shadow, 0 0 24px rgba(124, 77, 255, 0.45)));
41
41
  animation: none;
42
42
  }
43
43
 
44
44
  .ef-button--hover-glow:not(:disabled):hover,
45
45
  .ef-button--hover-glow:focus-visible {
46
- box-shadow: var(--ef-button-current-hover-shadow, var(--ef-button-hover-shadow, 0 0 18px rgba(124, 77, 255, 0.35)));
46
+ box-shadow: var(--ef-button-current-glow-shadow, var(--ef-button-glow-shadow, 0 0 24px rgba(124, 77, 255, 0.45)));
47
47
  animation: none;
48
48
  }
49
49
 
@@ -168,17 +168,17 @@
168
168
  }
169
169
 
170
170
  .ef-button.tab.ef-button--glow {
171
- box-shadow: var(--ef-button-current-glow-shadow, var(--ef-button-glow-shadow, 0 0 24px rgba(124, 77, 255, 0.45)));
171
+ box-shadow: var(--ef-button-current-hover-shadow, var(--ef-button-hover-shadow, 0 0 18px rgba(124, 77, 255, 0.35)));
172
172
  }
173
173
 
174
174
  .ef-button.tab.ef-button--glow:not(:disabled):hover,
175
175
  .ef-button.tab.ef-button--hover-glow:not(:disabled):hover {
176
- box-shadow: var(--ef-button-current-hover-shadow, var(--ef-button-hover-shadow, 0 0 18px rgba(124, 77, 255, 0.35)));
176
+ box-shadow: var(--ef-button-current-glow-shadow, var(--ef-button-glow-shadow, 0 0 24px rgba(124, 77, 255, 0.45)));
177
177
  }
178
178
 
179
179
  .ef-button.tab.ef-button--glow:focus-visible,
180
180
  .ef-button.tab.ef-button--hover-glow:focus-visible {
181
- box-shadow: var(--ef-button-current-focus-shadow, var(--ef-button-focus-shadow, 0 0 18px rgba(124, 77, 255, 0.35)));
181
+ box-shadow: var(--ef-button-current-glow-shadow, var(--ef-button-glow-shadow, 0 0 24px rgba(124, 77, 255, 0.45)));
182
182
  }
183
183
 
184
184
  .theme-preview {
@@ -15,6 +15,17 @@ export type DropdownUserItem = {
15
15
  className?: string;
16
16
  title?: string;
17
17
  variant?: "default" | "theme-preview";
18
+ buttonVariant?:
19
+ | "primary"
20
+ | "ghost"
21
+ | "locked"
22
+ | "danger"
23
+ | "delete"
24
+ | "warning"
25
+ | "info"
26
+ | "success"
27
+ | "tab";
28
+ buttonSubvariant?: "default" | "glow" | "hover-glow";
18
29
  };
19
30
 
20
31
  export type DropdownUserListItem = {
@@ -31,12 +42,12 @@ export type DropdownUserListItem = {
31
42
  subtitle?: string;
32
43
  };
33
44
 
34
- export type DropdownBookmarkOption = {
35
- value: string;
36
- label: string;
37
- meta?: unknown;
38
- className?: string;
39
- };
45
+ export type DropdownBookmarkOption = {
46
+ value: string;
47
+ label: string;
48
+ meta?: unknown;
49
+ className?: string;
50
+ };
40
51
 
41
52
  export type DropdownBookmarkSection = {
42
53
  label?: string;
@@ -77,16 +88,16 @@ type UserListVariantProps = {
77
88
  emptyClassName?: string;
78
89
  };
79
90
 
80
- type BookmarkVariantProps = {
81
- variant: "bookmark";
82
- label?: string;
83
- layout?: "row" | "field";
84
- value: string;
85
- triggerLabel?: string;
86
- placeholder?: string;
87
- sections: DropdownBookmarkSection[];
88
- onChange: (value: string, option?: DropdownBookmarkOption) => void;
89
- renderTriggerIcon?: ReactNode;
91
+ type BookmarkVariantProps = {
92
+ variant: "bookmark";
93
+ label?: string;
94
+ layout?: "row" | "field";
95
+ value: string;
96
+ triggerLabel?: string;
97
+ placeholder?: string;
98
+ sections: DropdownBookmarkSection[];
99
+ onChange: (value: string, option?: DropdownBookmarkOption) => void;
100
+ renderTriggerIcon?: ReactNode;
90
101
  renderItemIcon?: (option: DropdownBookmarkOption) => ReactNode;
91
102
  caret?: ReactNode;
92
103
  emptyLabel?: string;
@@ -206,28 +217,28 @@ export const Dropdown = (props: DropdownProps) => {
206
217
 
207
218
  return (
208
219
  <div className="ef-menu-bar">
209
- {props.menus.map((menu) => (
210
- <div
211
- key={menu.id}
212
- className="ef-menu-group"
213
- data-open={props.menuOpen === menu.id ? "true" : "false"}
214
- onMouseEnter={() => {
215
- cancelScheduledClose();
216
- props.onOpenMenu(menu.id);
217
- }}
218
- onMouseLeave={scheduleClose}
219
- >
220
- <Button
221
- className={["ef-menu-button", "ef-tab", props.menuOpen === menu.id ? "is-active" : ""]
222
- .filter(Boolean)
223
- .join(" ")}
224
- type="button"
225
- variant="tab"
226
- subvariant="default"
227
- data-open={props.menuOpen === menu.id ? "true" : "false"}
228
- >
229
- {menu.label}
230
- </Button>
220
+ {props.menus.map((menu) => (
221
+ <div
222
+ key={menu.id}
223
+ className="ef-menu-group"
224
+ data-open={props.menuOpen === menu.id ? "true" : "false"}
225
+ onMouseEnter={() => {
226
+ cancelScheduledClose();
227
+ props.onOpenMenu(menu.id);
228
+ }}
229
+ onMouseLeave={scheduleClose}
230
+ >
231
+ <Button
232
+ className={["ef-menu-button", "ef-tab", props.menuOpen === menu.id ? "is-active" : ""]
233
+ .filter(Boolean)
234
+ .join(" ")}
235
+ type="button"
236
+ variant="tab"
237
+ subvariant="default"
238
+ data-open={props.menuOpen === menu.id ? "true" : "false"}
239
+ >
240
+ {menu.label}
241
+ </Button>
231
242
  {props.menuOpen === menu.id ? (
232
243
  <div
233
244
  className="ef-menu-popover"
@@ -289,30 +300,34 @@ export const Dropdown = (props: DropdownProps) => {
289
300
  : null;
290
301
 
291
302
  const renderUserListItems = () => {
292
- if (props.variant !== "user-list") {
303
+ if (props.variant !== "user-list") {
293
304
  return props.items.map((item, index) => {
294
305
  const key = item.id ?? `${item.label}-${index}`;
295
306
  if (item.variant === "theme-preview") {
296
307
  return (
297
- <Button
298
- key={key}
299
- type="button"
308
+ <Button
309
+ key={key}
310
+ type="button"
300
311
  variant="primary"
312
+ subvariant={item.buttonSubvariant ?? "default"}
301
313
  className={["theme-preview", item.className].filter(Boolean).join(" ")}
302
314
  onClick={() => {
303
315
  item.onClick();
304
316
  setOpen(false);
305
317
  }}
306
- disabled={item.disabled}
307
- title={item.title}
318
+ disabled={item.disabled}
319
+ title={item.title}
308
320
  >
309
321
  {item.label}
310
322
  </Button>
311
323
  );
312
324
  }
325
+ const isLogoutAction = /\b(log\s*out|sign\s*out)\b/i.test(item.label);
313
326
  return (
314
- <button
327
+ <Button
315
328
  key={key}
329
+ variant={item.buttonVariant ?? (isLogoutAction ? "danger" : "primary")}
330
+ subvariant={item.buttonSubvariant ?? "default"}
316
331
  className={["dropdown-item", item.className].filter(Boolean).join(" ")}
317
332
  type="button"
318
333
  onClick={() => {
@@ -323,7 +338,7 @@ export const Dropdown = (props: DropdownProps) => {
323
338
  title={item.title}
324
339
  >
325
340
  {item.label}
326
- </button>
341
+ </Button>
327
342
  );
328
343
  });
329
344
  }
@@ -368,20 +383,20 @@ export const Dropdown = (props: DropdownProps) => {
368
383
  }}
369
384
  title={item.title}
370
385
  >
371
- <span className="dropdown-avatar">
372
- {item.avatarUrl ? (
373
- <img
374
- src={item.avatarUrl}
375
- alt={item.label}
376
- loading="lazy"
377
- decoding="async"
378
- referrerPolicy="no-referrer"
379
- crossOrigin="anonymous"
380
- />
381
- ) : (
382
- <span className="dropdown-avatar-fallback">{fallback}</span>
383
- )}
384
- </span>
386
+ <span className="dropdown-avatar">
387
+ {item.avatarUrl ? (
388
+ <img
389
+ src={item.avatarUrl}
390
+ alt={item.label}
391
+ loading="lazy"
392
+ decoding="async"
393
+ referrerPolicy="no-referrer"
394
+ crossOrigin="anonymous"
395
+ />
396
+ ) : (
397
+ <span className="dropdown-avatar-fallback">{fallback}</span>
398
+ )}
399
+ </span>
385
400
  <span className="dropdown-item-text">
386
401
  <span className="dropdown-item-label">{item.label}</span>
387
402
  {item.subtitle ? (
@@ -430,19 +445,19 @@ export const Dropdown = (props: DropdownProps) => {
430
445
  <div className="user-section" ref={ref} data-open={isOpen ? "true" : "false"}>
431
446
  <button className="user-button" onClick={() => setOpen(!isOpen)} type="button">
432
447
  <span className="avatar">
433
- {currentAvatar ? (
434
- <img
435
- src={currentAvatar}
436
- alt={props.avatarAlt ?? props.name}
437
- loading="eager"
438
- decoding="async"
439
- referrerPolicy="no-referrer"
440
- crossOrigin="anonymous"
441
- onError={() => {
442
- if (avatarState === "primary" && fallbackAvatar) {
443
- setAvatarState("fallback");
444
- } else {
445
- setAvatarState("none");
448
+ {currentAvatar ? (
449
+ <img
450
+ src={currentAvatar}
451
+ alt={props.avatarAlt ?? props.name}
452
+ loading="eager"
453
+ decoding="async"
454
+ referrerPolicy="no-referrer"
455
+ crossOrigin="anonymous"
456
+ onError={() => {
457
+ if (avatarState === "primary" && fallbackAvatar) {
458
+ setAvatarState("fallback");
459
+ } else {
460
+ setAvatarState("none");
446
461
  }
447
462
  }}
448
463
  />
@@ -467,13 +482,13 @@ export const Dropdown = (props: DropdownProps) => {
467
482
  placeholder = "Select a saved connection",
468
483
  sections,
469
484
  onChange,
470
- renderTriggerIcon,
471
- renderItemIcon,
472
- triggerLabel,
473
- caret,
474
- emptyLabel = "No saved connections.",
475
- emptyClassName,
476
- } = props;
485
+ renderTriggerIcon,
486
+ renderItemIcon,
487
+ triggerLabel,
488
+ caret,
489
+ emptyLabel = "No saved connections.",
490
+ emptyClassName,
491
+ } = props;
477
492
  const [open, setOpen] = useState(false);
478
493
  const ref = useRef<HTMLDivElement | null>(null);
479
494
  const options = useMemo(
@@ -493,20 +508,20 @@ export const Dropdown = (props: DropdownProps) => {
493
508
  return () => window.removeEventListener("pointerdown", handlePointer);
494
509
  }, [open]);
495
510
 
496
- const handleEllipsisTooltip = (event: React.MouseEvent<HTMLElement>) => {
497
- const target = event.currentTarget;
498
- if (target.scrollWidth > target.clientWidth) {
499
- target.setAttribute("title", target.textContent ?? "");
500
- } else {
501
- target.removeAttribute("title");
502
- }
503
- };
504
-
505
- const clearEllipsisTooltip = (event: React.MouseEvent<HTMLElement>) => {
506
- event.currentTarget.removeAttribute("title");
507
- };
508
-
509
- const dropdownBody = (
511
+ const handleEllipsisTooltip = (event: React.MouseEvent<HTMLElement>) => {
512
+ const target = event.currentTarget;
513
+ if (target.scrollWidth > target.clientWidth) {
514
+ target.setAttribute("title", target.textContent ?? "");
515
+ } else {
516
+ target.removeAttribute("title");
517
+ }
518
+ };
519
+
520
+ const clearEllipsisTooltip = (event: React.MouseEvent<HTMLElement>) => {
521
+ event.currentTarget.removeAttribute("title");
522
+ };
523
+
524
+ const dropdownBody = (
510
525
  <div className={`bookmark-dropdown ${open ? "open" : ""}`}>
511
526
  <button
512
527
  className="bookmark-trigger"
@@ -514,9 +529,9 @@ export const Dropdown = (props: DropdownProps) => {
514
529
  type="button"
515
530
  >
516
531
  {renderTriggerIcon ? <span className="bookmark-icon">{renderTriggerIcon}</span> : null}
517
- <span className="bookmark-text">
518
- {triggerLabel ?? (active ? active.label : placeholder)}
519
- </span>
532
+ <span className="bookmark-text">
533
+ {triggerLabel ?? (active ? active.label : placeholder)}
534
+ </span>
520
535
  <span className="bookmark-caret">{caret ?? <DefaultCaret />}</span>
521
536
  </button>
522
537
  {open ? (
@@ -529,35 +544,35 @@ export const Dropdown = (props: DropdownProps) => {
529
544
  sections.map((section) => (
530
545
  <div key={section.label ?? "options"}>
531
546
  {section.label ? <div className="bookmark-group">{section.label}</div> : null}
532
- {section.options.map((item) => (
533
- <button
534
- key={item.value}
535
- className={[
536
- "bookmark-item",
537
- item.value === value ? "active" : "",
538
- item.className,
539
- ]
540
- .filter(Boolean)
541
- .join(" ")}
542
- type="button"
543
- onClick={() => {
544
- onChange(item.value, item);
545
- setOpen(false);
546
- }}
547
- >
547
+ {section.options.map((item) => (
548
+ <button
549
+ key={item.value}
550
+ className={[
551
+ "bookmark-item",
552
+ item.value === value ? "active" : "",
553
+ item.className,
554
+ ]
555
+ .filter(Boolean)
556
+ .join(" ")}
557
+ type="button"
558
+ onClick={() => {
559
+ onChange(item.value, item);
560
+ setOpen(false);
561
+ }}
562
+ >
548
563
  {renderItemIcon ? (
549
564
  <span className="bookmark-icon">{renderItemIcon(item)}</span>
550
565
  ) : null}
551
- <span
552
- className="bookmark-text"
553
- onMouseEnter={handleEllipsisTooltip}
554
- onMouseLeave={clearEllipsisTooltip}
555
- >
556
- {item.label}
557
- </span>
558
- </button>
559
- ))}
560
- </div>
566
+ <span
567
+ className="bookmark-text"
568
+ onMouseEnter={handleEllipsisTooltip}
569
+ onMouseLeave={clearEllipsisTooltip}
570
+ >
571
+ {item.label}
572
+ </span>
573
+ </button>
574
+ ))}
575
+ </div>
561
576
  ))
562
577
  )}
563
578
  </div>
@@ -30,15 +30,15 @@
30
30
  border-bottom-color: transparent;
31
31
  }
32
32
 
33
- .avatar {
34
- width: 32px;
35
- height: 32px;
36
- border-radius: var(--ef-control-radius, 12px);
37
- background: rgba(255, 255, 255, 0.12);
38
- display: grid;
39
- place-items: center;
40
- overflow: hidden;
41
- flex-shrink: 0;
33
+ .avatar {
34
+ width: 32px;
35
+ height: 32px;
36
+ border-radius: var(--ef-control-radius, 12px);
37
+ background: rgba(255, 255, 255, 0.12);
38
+ display: grid;
39
+ place-items: center;
40
+ overflow: hidden;
41
+ flex-shrink: 0;
42
42
  }
43
43
 
44
44
  .avatar img {
@@ -105,59 +105,69 @@
105
105
  pointer-events: auto;
106
106
  }
107
107
 
108
- .dropdown-item {
109
- border: 2px solid transparent;
110
- padding: 10px 16px;
111
- border-radius: var(--ef-control-radius, 12px);
112
- background:
113
- linear-gradient(var(--ef-surface), var(--ef-surface)) padding-box,
114
- var(--ef-border-gradient) border-box;
115
- color: var(--text-strong);
116
- font-weight: 600;
117
- font-size: 0.85rem;
118
- text-align: left;
119
- cursor: pointer;
120
- width: 100%;
121
- transition: box-shadow 0.2s ease, transform 0.2s ease;
122
- box-shadow: 0 0 24px rgba(124, 77, 255, 0.45);
123
- }
124
-
125
- .dropdown-item:hover {
126
- box-shadow: 0 0 18px rgba(124, 77, 255, 0.35);
127
- transform: translateY(-1px);
108
+ .dropdown-item {
109
+ display: inline-flex;
110
+ align-items: center;
111
+ justify-content: flex-start;
112
+ font-size: 0.85rem;
113
+ line-height: 1.25;
114
+ width: 100%;
115
+ text-align: left;
116
+ cursor: pointer;
128
117
  }
129
118
 
130
- .dropdown-item.theme-preview {
119
+ .dropdown-item:not(.ef-button) {
131
120
  border: 2px solid transparent;
121
+ padding: 10px 16px;
132
122
  border-radius: var(--ef-control-radius, 12px);
133
123
  background:
134
- linear-gradient(var(--ef-button-surface), var(--ef-button-surface)) padding-box,
135
- var(--ef-button-border) border-box;
136
- color: var(--ef-button-text);
137
- box-shadow: var(--shadow);
124
+ linear-gradient(var(--ef-surface), var(--ef-surface)) padding-box,
125
+ var(--ef-border-gradient) border-box;
126
+ color: var(--text-strong);
127
+ font-weight: 600;
128
+ transition: box-shadow 0.2s ease, transform 0.2s ease;
129
+ box-shadow: var(--ef-button-glow-shadow, 0 0 24px rgba(124, 77, 255, 0.45));
138
130
  }
139
131
 
140
- .dropdown-item.theme-preview:hover {
141
- box-shadow: var(--shadow);
142
- filter: brightness(1.04);
132
+ .dropdown-item:hover {
143
133
  transform: translateY(-1px);
144
134
  }
145
135
 
146
- .dropdown-item.theme-preview.is-disabled,
147
- .dropdown-item.theme-preview.is-disabled:hover {
148
- opacity: 0.6;
149
- box-shadow: none;
150
- filter: none;
151
- transform: none;
152
- background:
153
- linear-gradient(var(--ef-button-surface), var(--ef-button-surface)) padding-box,
154
- var(--ef-button-border-soft) border-box;
136
+ .dropdown-item:not(.ef-button):hover {
137
+ box-shadow: var(--ef-button-hover-glow-shadow, 0 0 18px rgba(124, 77, 255, 0.35));
155
138
  }
156
-
157
- .dropdown-item.is-disabled {
158
- opacity: 0.55;
159
- cursor: not-allowed;
160
- box-shadow: none;
139
+
140
+ .dropdown-item.theme-preview {
141
+ border: 2px solid transparent;
142
+ border-radius: var(--ef-control-radius, 12px);
143
+ background:
144
+ linear-gradient(var(--ef-button-surface), var(--ef-button-surface)) padding-box,
145
+ var(--ef-button-border) border-box;
146
+ color: var(--ef-button-text);
147
+ box-shadow: var(--shadow);
148
+ }
149
+
150
+ .dropdown-item.theme-preview:hover {
151
+ box-shadow: var(--shadow);
152
+ filter: brightness(1.04);
153
+ transform: translateY(-1px);
154
+ }
155
+
156
+ .dropdown-item.theme-preview.is-disabled,
157
+ .dropdown-item.theme-preview.is-disabled:hover {
158
+ opacity: 0.6;
159
+ box-shadow: none;
160
+ filter: none;
161
+ transform: none;
162
+ background:
163
+ linear-gradient(var(--ef-button-surface), var(--ef-button-surface)) padding-box,
164
+ var(--ef-button-border-soft) border-box;
165
+ }
166
+
167
+ .dropdown-item.is-disabled {
168
+ opacity: 0.55;
169
+ cursor: not-allowed;
170
+ box-shadow: none;
161
171
  }
162
172
 
163
173
  .dropdown-item.is-disabled:hover {
@@ -183,15 +193,15 @@
183
193
  padding: 10px 12px;
184
194
  }
185
195
 
186
- .dropdown-avatar {
187
- width: 34px;
188
- height: 34px;
189
- border-radius: var(--ef-control-radius, 12px);
190
- background: rgba(255, 255, 255, 0.12);
191
- display: grid;
192
- place-items: center;
193
- overflow: hidden;
194
- font-size: 0.7rem;
196
+ .dropdown-avatar {
197
+ width: 34px;
198
+ height: 34px;
199
+ border-radius: var(--ef-control-radius, 12px);
200
+ background: rgba(255, 255, 255, 0.12);
201
+ display: grid;
202
+ place-items: center;
203
+ overflow: hidden;
204
+ font-size: 0.7rem;
195
205
  font-weight: 700;
196
206
  text-transform: uppercase;
197
207
  }
@@ -256,18 +266,18 @@
256
266
  color: rgb(255, 164, 164);
257
267
  }
258
268
 
259
- .dropdown-action:hover {
260
- filter: brightness(1.1);
261
- }
262
-
263
- @media (max-width: 760px) {
264
- .user-button {
265
- gap: 8px;
266
- padding: 8px 10px;
267
- }
268
-
269
- .user-name {
270
- display: inline;
271
- max-width: 32vw;
272
- }
273
- }
269
+ .dropdown-action:hover {
270
+ filter: brightness(1.1);
271
+ }
272
+
273
+ @media (max-width: 760px) {
274
+ .user-button {
275
+ gap: 8px;
276
+ padding: 8px 10px;
277
+ }
278
+
279
+ .user-name {
280
+ display: inline;
281
+ max-width: 32vw;
282
+ }
283
+ }