@enderfall/ui 0.1.0 → 0.1.4

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 (45) hide show
  1. package/assets/brand/enderfall-lockup.png +0 -0
  2. package/assets/brand/enderfall-lockup.svg +8 -0
  3. package/assets/brand/enderfall-mark.png +0 -0
  4. package/assets/brand/enderfall-mark.svg +8 -0
  5. package/dist/components/Button.d.ts +2 -1
  6. package/dist/components/Button.d.ts.map +1 -1
  7. package/dist/components/Button.js +8 -1
  8. package/dist/components/Dropdown.d.ts.map +1 -1
  9. package/dist/components/Dropdown.js +2 -2
  10. package/package.json +5 -2
  11. package/src/base.css +160 -0
  12. package/src/components/AccessGate.css +24 -0
  13. package/src/components/AccessGate.tsx +61 -0
  14. package/src/components/BookmarkDropdown.css +220 -0
  15. package/src/components/Button.css +183 -0
  16. package/src/components/Button.tsx +20 -0
  17. package/src/components/Dropdown.tsx +570 -0
  18. package/src/components/FloatingFooter.css +49 -0
  19. package/src/components/FloatingFooter.tsx +27 -0
  20. package/src/components/FormField.tsx +29 -0
  21. package/src/components/HeaderMenu.css +280 -0
  22. package/src/components/Input.css +68 -0
  23. package/src/components/Input.tsx +23 -0
  24. package/src/components/MainHeader.css +167 -0
  25. package/src/components/MainHeader.tsx +51 -0
  26. package/src/components/Modal.css +282 -0
  27. package/src/components/Modal.tsx +142 -0
  28. package/src/components/Panel.css +71 -0
  29. package/src/components/Panel.tsx +31 -0
  30. package/src/components/PreferencesModal.tsx +67 -0
  31. package/src/components/SideMenu.tsx +239 -0
  32. package/src/components/Slider.css +114 -0
  33. package/src/components/Slider.tsx +33 -0
  34. package/src/components/StackedCard.css +180 -0
  35. package/src/components/StackedCard.tsx +125 -0
  36. package/src/components/StatDots.css +122 -0
  37. package/src/components/StatDots.tsx +53 -0
  38. package/src/components/Tabs.css +108 -0
  39. package/src/components/Tabs.tsx +68 -0
  40. package/src/components/Toggle.css +161 -0
  41. package/src/components/Toggle.tsx +38 -0
  42. package/src/components/UserMenu.css +273 -0
  43. package/src/index.ts +45 -0
  44. package/src/theme.css +353 -0
  45. package/styles.css +1 -0
@@ -0,0 +1,282 @@
1
+ .ef-modal-backdrop {
2
+ position: fixed;
3
+ inset: 0;
4
+ background: rgba(6, 10, 14, 0.6);
5
+ backdrop-filter: blur(2px);
6
+ display: grid;
7
+ place-items: center;
8
+ z-index: 200;
9
+ opacity: 1;
10
+ transition: opacity 220ms ease;
11
+ }
12
+
13
+ .ef-modal-backdrop.is-entering {
14
+ opacity: 0;
15
+ }
16
+
17
+ .ef-modal-backdrop.is-open {
18
+ opacity: 1;
19
+ }
20
+
21
+ .ef-modal {
22
+ width: min(520px, 90vw);
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: 16px;
26
+ max-height: min(90dvh, 860px);
27
+ overflow: hidden;
28
+ position: relative;
29
+ isolation: isolate;
30
+ transform-origin: calc(50% + var(--ef-modal-enter-x, 0px)) calc(50% + var(--ef-modal-enter-y, 0px));
31
+ opacity: 1;
32
+ transform: translate3d(0, 0, 0) scale(1);
33
+ transition: transform 260ms cubic-bezier(0.2, 0.9, 0.3, 1), opacity 220ms ease;
34
+ will-change: transform, opacity;
35
+ }
36
+
37
+ .ef-modal.is-entering {
38
+ opacity: 0;
39
+ transform: translate3d(var(--ef-modal-enter-x, 0px), var(--ef-modal-enter-y, 0px), 0) scale(0.965);
40
+ }
41
+
42
+ .ef-modal.is-open {
43
+ opacity: 1;
44
+ transform: translate3d(0, 0, 0) scale(1);
45
+ }
46
+
47
+ .ef-modal-backdrop.is-exiting {
48
+ opacity: 0;
49
+ }
50
+
51
+ .ef-modal.is-exiting {
52
+ opacity: 0;
53
+ transform: translate3d(0, 42px, 0) scale(0.985);
54
+ }
55
+
56
+ .ef-modal--wide {
57
+ width: min(760px, 92vw);
58
+ }
59
+
60
+ .ef-modal-header {
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: space-between;
64
+ }
65
+
66
+ .ef-modal-header-actions {
67
+ display: inline-flex;
68
+ align-items: center;
69
+ gap: 8px;
70
+ }
71
+
72
+ .ef-modal-title {
73
+ font-size: 1.2rem;
74
+ }
75
+
76
+ .ef-modal-subtitle {
77
+ color: var(--text-soft);
78
+ }
79
+
80
+ .ef-modal-actions {
81
+ display: grid;
82
+ gap: 14px;
83
+ margin-top: 0;
84
+ position: sticky;
85
+ bottom: 0;
86
+ z-index: 50;
87
+ padding-top: 8px;
88
+ padding-bottom: 4px;
89
+ }
90
+
91
+ .ef-modal-body {
92
+ display: grid;
93
+ grid-template-rows: minmax(0, 1fr) auto;
94
+ gap: 12px;
95
+ flex: 1 1 auto;
96
+ min-height: 0;
97
+ overflow: hidden;
98
+ position: relative;
99
+ }
100
+
101
+ .ef-modal-body > :not(.ef-modal-actions) {
102
+ min-height: 0;
103
+ overflow-y: auto;
104
+ overflow-x: hidden;
105
+ scrollbar-width: none;
106
+ -ms-overflow-style: none;
107
+ }
108
+
109
+ .ef-modal-body > :not(.ef-modal-actions) * {
110
+ scrollbar-width: none;
111
+ -ms-overflow-style: none;
112
+ }
113
+
114
+ .ef-modal-body > .ef-modal-actions {
115
+ position: relative;
116
+ bottom: auto;
117
+ margin-top: 0;
118
+ z-index: 80;
119
+ }
120
+
121
+ .ef-modal-body > :not(.ef-modal-actions)::-webkit-scrollbar {
122
+ width: 0;
123
+ height: 0;
124
+ }
125
+
126
+ .ef-modal-body > :not(.ef-modal-actions) *::-webkit-scrollbar {
127
+ width: 0;
128
+ height: 0;
129
+ }
130
+
131
+ .ef-modal-form {
132
+ display: flex;
133
+ flex-direction: column;
134
+ gap: 12px;
135
+ min-height: 0;
136
+ }
137
+
138
+ .ef-modal-form label:not(.ef-toggle) {
139
+ display: grid;
140
+ gap: 6px;
141
+ font-size: 0.85rem;
142
+ }
143
+
144
+ .ef-modal-row {
145
+ display: flex;
146
+ align-items: center;
147
+ gap: 12px;
148
+ }
149
+
150
+ .ef-modal-row-label {
151
+ font-size: 0.85rem;
152
+ color: var(--ef-field-label, var(--text-muted));
153
+ font-weight: 500;
154
+ }
155
+
156
+ .ef-modal-row-control {
157
+ flex: 1;
158
+ display: flex;
159
+ }
160
+
161
+ .ef-modal-row-control > * {
162
+ width: 100%;
163
+ }
164
+
165
+ .ef-modal-form .ef-toggle {
166
+ width: 100%;
167
+ align-items: center;
168
+ justify-content: flex-start;
169
+ flex-direction: row;
170
+ }
171
+
172
+ .ef-modal-form .ef-toggle-label {
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 8px;
176
+ }
177
+
178
+ .prefs-section .ef-toggle {
179
+ display: flex;
180
+ flex-direction: row;
181
+ align-items: center;
182
+ justify-content: flex-start;
183
+ }
184
+
185
+ .prefs-section .ef-toggle-label {
186
+ display: flex;
187
+ align-items: center;
188
+ gap: 8px;
189
+ flex-direction: row;
190
+ }
191
+
192
+ .prefs-section {
193
+ display: grid;
194
+ gap: 10px;
195
+ padding: 12px;
196
+ border-radius: var(--ef-control-radius, 12px);
197
+ border: 1px solid var(--line);
198
+ background: var(--card);
199
+ }
200
+
201
+ .prefs-section-title {
202
+ font-size: 0.72rem;
203
+ font-weight: 700;
204
+ text-transform: uppercase;
205
+ letter-spacing: 0.12em;
206
+ color: var(--text-muted);
207
+ }
208
+
209
+ .ef-modal-note {
210
+ color: var(--text-soft);
211
+ font-size: 0.85rem;
212
+ }
213
+
214
+ .ef-modal-close {
215
+ font-size: 1rem;
216
+ }
217
+
218
+ .ef-modal-user-dropdown .avatar {
219
+ display: none;
220
+ }
221
+
222
+ .ef-modal-user-dropdown .user-button {
223
+ min-height: 40px;
224
+ height: auto;
225
+ padding: 8px 12px;
226
+ gap: 8px;
227
+ }
228
+
229
+ .ef-modal-user-dropdown .user-section {
230
+ display: block;
231
+ }
232
+
233
+ .ef-modal-user-dropdown .user-button,
234
+ .ef-modal-user-dropdown .dropdown {
235
+ width: 100%;
236
+ }
237
+
238
+ .ef-modal-user-dropdown .user-name {
239
+ max-width: 100%;
240
+ }
241
+
242
+ @media (max-width: 760px) {
243
+ .ef-modal-backdrop {
244
+ place-items: stretch;
245
+ }
246
+
247
+ .ef-modal,
248
+ .ef-modal--wide {
249
+ width: 100vw !important;
250
+ max-width: 100vw;
251
+ height: 100dvh;
252
+ min-height: 0;
253
+ max-height: 100dvh;
254
+ margin: 0;
255
+ gap: 12px;
256
+ border-radius: 0 !important;
257
+ border: 0 !important;
258
+ overflow: hidden;
259
+ padding-top: max(8px, env(safe-area-inset-top));
260
+ transform-origin: center bottom;
261
+ transition: transform 300ms cubic-bezier(0.2, 0.9, 0.3, 1), opacity 220ms ease;
262
+ }
263
+
264
+ .ef-modal-actions {
265
+ padding-bottom: max(8px, env(safe-area-inset-bottom));
266
+ }
267
+
268
+ .ef-modal.is-entering {
269
+ opacity: 0;
270
+ transform: translate3d(0, 36px, 0);
271
+ }
272
+
273
+ .ef-modal.is-open {
274
+ opacity: 1;
275
+ transform: translate3d(0, 0, 0);
276
+ }
277
+
278
+ .ef-modal.is-exiting {
279
+ opacity: 0;
280
+ transform: translate3d(0, 100px, 0);
281
+ }
282
+ }
@@ -0,0 +1,142 @@
1
+ import { useEffect, useRef, useState, type CSSProperties, type ReactNode } from "react";
2
+ import { Panel } from "./Panel";
3
+
4
+ type ModalSize = "default" | "wide";
5
+
6
+ const MODAL_TRANSITION_MS = 240;
7
+
8
+ type ModalProps = {
9
+ isOpen: boolean;
10
+ title: string;
11
+ subtitle?: string;
12
+ size?: ModalSize;
13
+ onClose: () => void;
14
+ actions?: ReactNode;
15
+ headerActions?: ReactNode;
16
+ className?: string;
17
+ children?: ReactNode;
18
+ };
19
+
20
+ export const Modal = ({
21
+ isOpen,
22
+ title,
23
+ subtitle,
24
+ size = "default",
25
+ onClose,
26
+ actions,
27
+ headerActions,
28
+ className,
29
+ children,
30
+ }: ModalProps) => {
31
+ const reduceMotion =
32
+ typeof document !== "undefined" && document.documentElement.getAttribute("data-reduce-motion") === "true";
33
+ const transitionMs = reduceMotion ? 0 : MODAL_TRANSITION_MS;
34
+ const [isRendered, setIsRendered] = useState(isOpen);
35
+ const [isEntering, setIsEntering] = useState(false);
36
+ const [isExiting, setIsExiting] = useState(false);
37
+ const [enterOffset, setEnterOffset] = useState<{ x: number; y: number }>({ x: 0, y: 0 });
38
+ const exitTimerRef = useRef<number | null>(null);
39
+ const enterFrameRef = useRef<number | null>(null);
40
+
41
+ useEffect(() => {
42
+ return () => {
43
+ if (exitTimerRef.current !== null) window.clearTimeout(exitTimerRef.current);
44
+ if (enterFrameRef.current !== null) window.cancelAnimationFrame(enterFrameRef.current);
45
+ };
46
+ }, []);
47
+
48
+ useEffect(() => {
49
+ if (isOpen) {
50
+ if (exitTimerRef.current !== null) {
51
+ window.clearTimeout(exitTimerRef.current);
52
+ exitTimerRef.current = null;
53
+ }
54
+
55
+ const activeElement = document.activeElement as HTMLElement | null;
56
+ const activeRect = activeElement?.getBoundingClientRect();
57
+ const sourceX = activeRect ? activeRect.left + activeRect.width / 2 : window.innerWidth / 2;
58
+ const sourceY = activeRect ? activeRect.top + activeRect.height / 2 : window.innerHeight - 56;
59
+ const centerX = window.innerWidth / 2;
60
+ const centerY = window.innerHeight / 2;
61
+
62
+ setEnterOffset({
63
+ x: (sourceX - centerX) * 0.18,
64
+ y: (sourceY - centerY) * 0.22,
65
+ });
66
+
67
+ setIsRendered(true);
68
+ setIsExiting(false);
69
+ setIsEntering(!reduceMotion);
70
+
71
+ if (reduceMotion) return;
72
+
73
+ if (enterFrameRef.current !== null) window.cancelAnimationFrame(enterFrameRef.current);
74
+ enterFrameRef.current = window.requestAnimationFrame(() => {
75
+ enterFrameRef.current = window.requestAnimationFrame(() => {
76
+ setIsEntering(false);
77
+ enterFrameRef.current = null;
78
+ });
79
+ });
80
+ return;
81
+ }
82
+
83
+ if (!isRendered) return;
84
+ if (reduceMotion) {
85
+ setIsEntering(false);
86
+ setIsExiting(false);
87
+ setIsRendered(false);
88
+ return;
89
+ }
90
+
91
+ setIsEntering(false);
92
+ setIsExiting(true);
93
+
94
+ if (exitTimerRef.current !== null) window.clearTimeout(exitTimerRef.current);
95
+ exitTimerRef.current = window.setTimeout(() => {
96
+ setIsRendered(false);
97
+ setIsExiting(false);
98
+ exitTimerRef.current = null;
99
+ }, transitionMs);
100
+ }, [isOpen, isRendered, reduceMotion, transitionMs]);
101
+
102
+ if (!isRendered) return null;
103
+
104
+ const sizeClass = size === "wide" ? "ef-modal--wide" : "";
105
+ const modalStateClass = isExiting ? "is-exiting" : isEntering ? "is-entering" : "is-open";
106
+ const backdropClasses = ["ef-modal-backdrop", modalStateClass].filter(Boolean).join(" ");
107
+ const classes = ["ef-modal", sizeClass, modalStateClass, className].filter(Boolean).join(" ");
108
+ const modalStyle = {
109
+ "--ef-modal-enter-x": `${enterOffset.x}px`,
110
+ "--ef-modal-enter-y": `${enterOffset.y}px`,
111
+ } as CSSProperties;
112
+
113
+ return (
114
+ <div className={backdropClasses} onClick={onClose}>
115
+ <Panel
116
+ variant="card"
117
+ borderWidth={2}
118
+ className={classes}
119
+ style={modalStyle}
120
+ onClick={(event) => event.stopPropagation()}
121
+ >
122
+ <div className="ef-modal-header">
123
+ <div className="ef-modal-title">{title}</div>
124
+ <div className="ef-modal-header-actions">
125
+ {headerActions}
126
+ <button
127
+ className="icon-action small ef-modal-close"
128
+ onClick={onClose}
129
+ type="button"
130
+ aria-label="Close"
131
+ >
132
+ {"\u00D7"}
133
+ </button>
134
+ </div>
135
+ </div>
136
+ {subtitle ? <div className="ef-modal-subtitle">{subtitle}</div> : null}
137
+ {children ? <div className="ef-modal-body">{children}</div> : null}
138
+ {actions ? <div className="ef-modal-actions">{actions}</div> : null}
139
+ </Panel>
140
+ </div>
141
+ );
142
+ };
@@ -0,0 +1,71 @@
1
+ .ef-panel {
2
+ border: var(--ef-panel-border-width) solid transparent;
3
+ }
4
+
5
+ .ef-panel--border-1 {
6
+ --ef-panel-border-width: 1px;
7
+ }
8
+
9
+ .ef-panel--border-2 {
10
+ --ef-panel-border-width: 2px;
11
+ }
12
+
13
+ .ef-panel--card {
14
+ padding: 24px;
15
+ border-radius: var(--radius);
16
+ background: var(--card);
17
+ box-shadow: var(--shadow);
18
+ border-color: var(--line);
19
+ display: grid;
20
+ gap: 16px;
21
+ }
22
+
23
+ :root[data-theme="galaxy"] .ef-panel--card,
24
+ :root[data-theme="plain-dark"] .ef-panel--card,
25
+ :root[data-theme="light"] .ef-panel--card,
26
+ :root[data-theme="plain-light"] .ef-panel--card {
27
+ background:
28
+ linear-gradient(var(--ef-surface), var(--ef-surface)) padding-box,
29
+ var(--ef-border-gradient) border-box;
30
+ border-color: transparent;
31
+ }
32
+
33
+ .ef-panel--highlight {
34
+ padding: 14px 16px;
35
+ border-radius: 16px;
36
+ background:
37
+ linear-gradient(var(--ef-surface), var(--ef-surface)) padding-box,
38
+ var(--ef-border-gradient) border-box;
39
+ color: var(--text-strong);
40
+ display: grid;
41
+ gap: 6px;
42
+ }
43
+
44
+ .ef-panel--full {
45
+ padding: 18px 0px;
46
+ border-radius: 24px;
47
+ background:
48
+ linear-gradient(var(--ef-surface), var(--ef-surface)) padding-box,
49
+ var(--ef-border-gradient) border-box;
50
+ border-color: transparent;
51
+ box-shadow: var(--shadow);
52
+ display: flex;
53
+ }
54
+
55
+ .ef-panel--header {
56
+ padding: 8px 12px;
57
+ border-radius: 18px;
58
+ background:
59
+ linear-gradient(var(--ef-surface), var(--ef-surface)) padding-box,
60
+ var(--ef-border-gradient) border-box;
61
+ border-color: transparent;
62
+ box-shadow: var(--shadow);
63
+ display: flex;
64
+ }
65
+
66
+ :root[data-theme="atelier"] .ef-panel--card,
67
+ :root[data-theme="atelier"] .ef-panel--highlight,
68
+ :root[data-theme="atelier"] .ef-panel--full,
69
+ :root[data-theme="atelier"] .ef-panel--header {
70
+ border-radius: 0;
71
+ }
@@ -0,0 +1,31 @@
1
+ import { forwardRef, type HTMLAttributes } from "react";
2
+
3
+ type PanelVariant = "card" | "highlight" | "full" | "header";
4
+ type BorderWidth = 1 | 2;
5
+
6
+ type PanelProps = HTMLAttributes<HTMLDivElement> & {
7
+ variant?: PanelVariant;
8
+ borderWidth?: BorderWidth;
9
+ };
10
+
11
+ export const Panel = forwardRef<HTMLDivElement, PanelProps>(
12
+ ({ variant = "card", borderWidth = 1, className, children, ...rest }, ref) => {
13
+ const variantClass =
14
+ variant === "highlight"
15
+ ? "ef-panel--highlight"
16
+ : variant === "header"
17
+ ? "ef-panel--header"
18
+ : variant === "full"
19
+ ? "ef-panel--full"
20
+ : "ef-panel--card";
21
+ const borderClass = borderWidth === 2 ? "ef-panel--border-2" : "ef-panel--border-1";
22
+ const classes = ["ef-panel", variantClass, borderClass, className].filter(Boolean).join(" ");
23
+ return (
24
+ <div ref={ref} className={classes} {...rest}>
25
+ {children}
26
+ </div>
27
+ );
28
+ }
29
+ );
30
+
31
+ Panel.displayName = "Panel";
@@ -0,0 +1,67 @@
1
+ import type { ReactNode } from "react";
2
+ import { Button } from "./Button";
3
+ import { Dropdown } from "./Dropdown";
4
+ import { Toggle } from "./Toggle";
5
+ import { Modal } from "./Modal";
6
+
7
+ type ThemeOption = { value: string; label: string };
8
+
9
+ type PreferencesModalProps = {
10
+ isOpen: boolean;
11
+ onClose: () => void;
12
+ themeMode: string;
13
+ onThemeChange: (value: string) => void;
14
+ themeOptions: ThemeOption[];
15
+ animationsEnabled: boolean;
16
+ onAnimationsChange: (enabled: boolean) => void;
17
+ note?: string;
18
+ children?: ReactNode;
19
+ };
20
+
21
+ export const PreferencesModal = ({
22
+ isOpen,
23
+ onClose,
24
+ themeMode,
25
+ onThemeChange,
26
+ themeOptions,
27
+ animationsEnabled,
28
+ onAnimationsChange,
29
+ note = "Syncs across all Enderfall apps.",
30
+ children,
31
+ }: PreferencesModalProps) => {
32
+ const selectedTheme =
33
+ themeOptions.find((option) => option.value === themeMode) ?? themeOptions[0];
34
+ const dropdownLabel = selectedTheme?.label ?? "Select theme";
35
+ const dropdownItems = themeOptions.map((option) => ({
36
+ id: option.value,
37
+ label: option.label,
38
+ onClick: () => onThemeChange(option.value),
39
+ className: `theme-preview theme-preview--${option.value}`,
40
+ variant: "theme-preview" as const,
41
+ }));
42
+
43
+ return (
44
+ <Modal isOpen={isOpen} onClose={onClose} title="Preferences" subtitle="Applies across Enderfall apps.">
45
+ <div className="ef-modal-form">
46
+ <label>
47
+ Theme
48
+ <div className="ef-modal-user-dropdown">
49
+ <Dropdown variant="user" name={dropdownLabel} items={dropdownItems} />
50
+ </div>
51
+ </label>
52
+ <Toggle
53
+ checked={animationsEnabled}
54
+ onChange={(event) => onAnimationsChange(event.target.checked)}
55
+ label="Enable animations"
56
+ />
57
+ {children}
58
+ {note ? <div className="ef-modal-note">{note}</div> : null}
59
+ </div>
60
+ <div className="ef-modal-actions">
61
+ <Button variant="primary" type="button" onClick={onClose}>
62
+ Close
63
+ </Button>
64
+ </div>
65
+ </Modal>
66
+ );
67
+ };