@xeplr/ui-account 1.0.0 → 1.0.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 +25 -5
- package/src/AccessContext.jsx +86 -4
- package/src/ProtectedRoute.jsx +4 -1
- package/src/activeScope.js +71 -0
- package/src/adminApi.js +0 -29
- package/src/api.js +321 -22
- package/src/authRoutes.jsx +125 -0
- package/src/designs/AccountMenu.jsx +71 -0
- package/src/designs/ActivateSample.jsx +8 -16
- package/src/designs/ChangePasswordSample.jsx +3 -3
- package/src/designs/ForgotPasswordSample.jsx +3 -3
- package/src/designs/LoginSample.jsx +3 -2
- package/src/designs/NavDrawer.jsx +194 -0
- package/src/designs/NavFloatingSettings.jsx +25 -0
- package/src/designs/NavTopSample.jsx +34 -0
- package/src/designs/NotificationsBell.jsx +31 -0
- package/src/designs/ProfileSample.jsx +3 -3
- package/src/designs/RegisterSample.jsx +3 -3
- package/src/designs/ResetPasswordSample.jsx +7 -9
- package/src/designs/admin.css +13 -3
- package/src/designs/auth.css +6 -60
- package/src/designs/index.js +5 -2
- package/src/designs/nav.css +439 -0
- package/src/index.js +24 -8
- package/src/mt.js +31 -0
- package/src/pages.jsx +95 -38
- package/src/returnTo.js +24 -0
- package/src/useActivateController.js +19 -4
- package/src/useChangePasswordController.js +8 -2
- package/src/useForgotPasswordController.js +3 -0
- package/src/useLoginController.js +8 -1
- package/src/useNavController.js +224 -0
- package/src/useProfileController.js +6 -1
- package/src/useRegisterController.js +5 -1
- package/src/useResetPasswordController.js +12 -1
- package/src/validateDesign.js +20 -2
- package/src/designs/TenantPickerSample.jsx +0 -39
- package/src/designs/TenantSample.jsx +0 -182
- package/src/useTenantController.js +0 -146
- package/src/useTenantPickerController.js +0 -97
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/* Uses the shared --xeplr-* theme tokens (see theme.css) — correct in both
|
|
2
|
+
light and dark automatically, as long as the app applies a .xeplr-theme-*
|
|
3
|
+
class (see ThemeContext.jsx / theme.css usage notes). */
|
|
4
|
+
|
|
5
|
+
/* ─── Top bar: 10% / 80% / 10% ───
|
|
6
|
+
flexbox, not grid: fixed percentage grid tracks don't account for `gap`
|
|
7
|
+
or box-sizing the way flex-basis does, and 10%+80%+10% already sums to
|
|
8
|
+
100% before the gaps/padding are added — grid pushed the real content
|
|
9
|
+
width past the container and forced a horizontal scrollbar. flex-basis
|
|
10
|
+
percentages combined with box-sizing:border-box and the default
|
|
11
|
+
flex-shrink correctly negotiate the gaps back out of the 100%. */
|
|
12
|
+
|
|
13
|
+
.xeplr-nav-top {
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
gap: 16px;
|
|
17
|
+
padding: 10px 20px;
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
background: var(--xeplr-bg-tertiary);
|
|
20
|
+
border-bottom: 1px solid var(--xeplr-border-primary);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.xeplr-nav-left { flex: 0 1 10%; display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
24
|
+
.xeplr-nav-right { flex: 0 1 10%; display: flex; align-items: center; justify-content: flex-end; gap: 8px; }
|
|
25
|
+
|
|
26
|
+
.xeplr-nav-logo { height: 32px; width: auto; display: block; }
|
|
27
|
+
|
|
28
|
+
.xeplr-nav-middle {
|
|
29
|
+
flex: 1 1 80%;
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 12px;
|
|
33
|
+
min-width: 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* ─── Settings trigger + dropdown ─── */
|
|
37
|
+
|
|
38
|
+
.xeplr-nav-account { position: relative; }
|
|
39
|
+
|
|
40
|
+
.xeplr-nav-settings-trigger {
|
|
41
|
+
position: relative;
|
|
42
|
+
width: 34px;
|
|
43
|
+
height: 34px;
|
|
44
|
+
border: none;
|
|
45
|
+
background: none;
|
|
46
|
+
border-radius: 50%;
|
|
47
|
+
color: var(--xeplr-text-secondary);
|
|
48
|
+
display: grid;
|
|
49
|
+
place-items: center;
|
|
50
|
+
cursor: pointer;
|
|
51
|
+
align-self: center;
|
|
52
|
+
}
|
|
53
|
+
.xeplr-nav-settings-trigger:hover { background: var(--xeplr-bg-hover); color: var(--xeplr-text-primary); }
|
|
54
|
+
.xeplr-nav-settings-trigger svg { width: 19px; height: 19px; flex-shrink: 0; }
|
|
55
|
+
|
|
56
|
+
/* Drawer, expanded: a full-width labeled row instead of a circular icon
|
|
57
|
+
button, matching .xeplr-nav-drawer-link's look. */
|
|
58
|
+
.xeplr-nav-settings-trigger-labeled {
|
|
59
|
+
width: 100%;
|
|
60
|
+
height: auto;
|
|
61
|
+
border-radius: 6px;
|
|
62
|
+
display: flex;
|
|
63
|
+
justify-content: flex-start;
|
|
64
|
+
gap: 10px;
|
|
65
|
+
padding: 9px 10px;
|
|
66
|
+
}
|
|
67
|
+
.xeplr-nav-settings-trigger-label { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
68
|
+
|
|
69
|
+
.xeplr-nav-avatar {
|
|
70
|
+
width: 34px;
|
|
71
|
+
height: 34px;
|
|
72
|
+
border-radius: 50%;
|
|
73
|
+
background: var(--xeplr-accent);
|
|
74
|
+
color: var(--xeplr-accent-text);
|
|
75
|
+
display: grid;
|
|
76
|
+
place-items: center;
|
|
77
|
+
font-size: 14px;
|
|
78
|
+
font-weight: 700;
|
|
79
|
+
flex-shrink: 0;
|
|
80
|
+
}
|
|
81
|
+
.xeplr-nav-avatar-img { object-fit: cover; }
|
|
82
|
+
|
|
83
|
+
.xeplr-nav-bell {
|
|
84
|
+
position: relative;
|
|
85
|
+
width: 34px;
|
|
86
|
+
height: 34px;
|
|
87
|
+
border: none;
|
|
88
|
+
background: none;
|
|
89
|
+
border-radius: 50%;
|
|
90
|
+
color: var(--xeplr-text-secondary);
|
|
91
|
+
display: grid;
|
|
92
|
+
place-items: center;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
align-self: center;
|
|
95
|
+
}
|
|
96
|
+
.xeplr-nav-bell:hover { background: var(--xeplr-bg-hover); color: var(--xeplr-text-primary); }
|
|
97
|
+
.xeplr-nav-bell svg { width: 19px; height: 19px; flex-shrink: 0; }
|
|
98
|
+
|
|
99
|
+
/* Drawer, expanded: a full-width labeled row instead of a circular icon
|
|
100
|
+
button, matching .xeplr-nav-drawer-link's look. */
|
|
101
|
+
.xeplr-nav-bell-labeled {
|
|
102
|
+
position: relative;
|
|
103
|
+
width: 100%;
|
|
104
|
+
height: auto;
|
|
105
|
+
border-radius: 6px;
|
|
106
|
+
display: flex;
|
|
107
|
+
justify-content: flex-start;
|
|
108
|
+
gap: 10px;
|
|
109
|
+
padding: 9px 10px;
|
|
110
|
+
}
|
|
111
|
+
.xeplr-nav-bell-labeled .xeplr-nav-badge { position: static; margin-left: auto; }
|
|
112
|
+
.xeplr-nav-bell-label { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
113
|
+
|
|
114
|
+
.xeplr-nav-badge {
|
|
115
|
+
position: absolute;
|
|
116
|
+
top: -4px;
|
|
117
|
+
right: -4px;
|
|
118
|
+
min-width: 16px;
|
|
119
|
+
height: 16px;
|
|
120
|
+
padding: 0 4px;
|
|
121
|
+
border-radius: 999px;
|
|
122
|
+
background: var(--xeplr-danger);
|
|
123
|
+
color: #fff;
|
|
124
|
+
font-size: 10px;
|
|
125
|
+
font-weight: 700;
|
|
126
|
+
line-height: 1;
|
|
127
|
+
display: grid;
|
|
128
|
+
place-items: center;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.xeplr-nav-account-menu {
|
|
132
|
+
position: absolute;
|
|
133
|
+
top: calc(100% + 8px);
|
|
134
|
+
right: 0;
|
|
135
|
+
min-width: 220px;
|
|
136
|
+
background: var(--xeplr-bg-tertiary);
|
|
137
|
+
border: 1px solid var(--xeplr-border-primary);
|
|
138
|
+
border-radius: 8px;
|
|
139
|
+
box-shadow: var(--xeplr-shadow-md);
|
|
140
|
+
padding: 6px;
|
|
141
|
+
z-index: 60;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Drawer placement: trigger sits near the bottom-left of the viewport, so the
|
|
145
|
+
menu opens upward (not off the bottom edge) and rightward from the trigger
|
|
146
|
+
(not off the left edge — the default `right: 0` anchoring would run the
|
|
147
|
+
menu's width past x=0 given how narrow/close-to-the-edge the drawer is). */
|
|
148
|
+
.xeplr-nav-account-menu-top {
|
|
149
|
+
top: auto;
|
|
150
|
+
bottom: calc(100% + 8px);
|
|
151
|
+
left: 0;
|
|
152
|
+
right: auto;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.xeplr-nav-account-header {
|
|
156
|
+
display: flex;
|
|
157
|
+
align-items: center;
|
|
158
|
+
gap: 10px;
|
|
159
|
+
padding: 8px 10px 10px;
|
|
160
|
+
margin-bottom: 6px;
|
|
161
|
+
border-bottom: 1px solid var(--xeplr-border-secondary);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.xeplr-nav-account-header-text { min-width: 0; }
|
|
165
|
+
.xeplr-nav-account-name { font-size: 13px; font-weight: 600; color: var(--xeplr-text-primary); }
|
|
166
|
+
.xeplr-nav-account-email { margin-top: 2px; font-size: 12px; color: var(--xeplr-text-muted); }
|
|
167
|
+
|
|
168
|
+
.xeplr-nav-account-item {
|
|
169
|
+
display: block;
|
|
170
|
+
width: 100%;
|
|
171
|
+
text-align: left;
|
|
172
|
+
padding: 8px 10px;
|
|
173
|
+
border: none;
|
|
174
|
+
border-radius: 6px;
|
|
175
|
+
background: none;
|
|
176
|
+
color: var(--xeplr-text-primary);
|
|
177
|
+
font-size: 13px;
|
|
178
|
+
text-decoration: none;
|
|
179
|
+
cursor: pointer;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.xeplr-nav-account-item:hover { background: var(--xeplr-bg-hover); }
|
|
183
|
+
.xeplr-nav-account-logout { color: var(--xeplr-danger); }
|
|
184
|
+
.xeplr-nav-account-divider { height: 1px; margin: 6px 0; background: var(--xeplr-border-secondary); }
|
|
185
|
+
|
|
186
|
+
/* ─── Drawer rail — pinned to the viewport's top-left corner (0,0), full
|
|
187
|
+
height, `position: fixed` with a high z-index. A pure overlay: it never
|
|
188
|
+
pushes, offsets, or otherwise interacts with the top bar or the app's own
|
|
189
|
+
content — there IS no top bar when this renders (see pages.jsx, they're
|
|
190
|
+
mutually exclusive), and content underneath simply renders at its normal
|
|
191
|
+
position, with the drawer floating on top of whatever's in that corner.
|
|
192
|
+
No overflow set here (default: visible) so the account-menu popup in the
|
|
193
|
+
footer can escape this box — only the scrollable nav-links list below
|
|
194
|
+
clips/scrolls on its own. */
|
|
195
|
+
|
|
196
|
+
.xeplr-nav-drawer {
|
|
197
|
+
position: fixed;
|
|
198
|
+
top: 0;
|
|
199
|
+
left: 0;
|
|
200
|
+
/* ABOVE .xeplr-nav-floating (400). Both are global chrome, so the tie goes
|
|
201
|
+
to the one being operated: widen the drawer and it reaches under the
|
|
202
|
+
floating bell/settings in the top-right corner, where those two icons
|
|
203
|
+
swallow clicks meant for the drawer — the resize handle's top edge
|
|
204
|
+
included. The floating cluster keeps 400 and still outranks page-level
|
|
205
|
+
chrome, which is what that value is actually for.
|
|
206
|
+
|
|
207
|
+
THE CEILING THIS SETS: a real full-screen modal must out-stack this, or
|
|
208
|
+
the rail punches through it. Consumers with one are expected to sit above
|
|
209
|
+
500 — xeplr-bi's .dsh-expanded-backdrop was moved to 600 when this
|
|
210
|
+
changed. */
|
|
211
|
+
z-index: 500;
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
height: 100vh;
|
|
215
|
+
padding: 16px 10px;
|
|
216
|
+
background: var(--xeplr-bg-tertiary);
|
|
217
|
+
border-right: 1px solid var(--xeplr-border-primary);
|
|
218
|
+
box-shadow: var(--xeplr-shadow-lg);
|
|
219
|
+
transition: width 0.15s;
|
|
220
|
+
flex-shrink: 0;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.xeplr-nav-drawer-collapsed { width: 60px; }
|
|
224
|
+
/* The 240px is the DEFAULT only — NavDrawer sets an inline width from the
|
|
225
|
+
remembered drag, which outranks this. Kept here so the drawer still has a
|
|
226
|
+
sane width if the controller's stored value is unreadable. */
|
|
227
|
+
.xeplr-nav-drawer-expanded { width: 240px; padding: 16px; }
|
|
228
|
+
|
|
229
|
+
/* Mid-drag the width changes every pointermove, and a 0.15s transition on it
|
|
230
|
+
means the edge chases the cursor instead of tracking it. Kill it for the
|
|
231
|
+
duration; the toggle animation is unaffected because it never coincides
|
|
232
|
+
with a drag. */
|
|
233
|
+
.xeplr-nav-drawer-resizing { transition: none; }
|
|
234
|
+
|
|
235
|
+
/* ── the resize handle ─────────────────────────────────────────────────
|
|
236
|
+
Straddles the right border (right: -3px on a 6px strip) rather than sitting
|
|
237
|
+
inside it, so the grab target covers the edge the user is actually aiming
|
|
238
|
+
at — an inside-only strip makes you undershoot into the nav. */
|
|
239
|
+
.xeplr-nav-drawer-resize {
|
|
240
|
+
position: absolute;
|
|
241
|
+
top: 0;
|
|
242
|
+
right: -3px;
|
|
243
|
+
width: 6px;
|
|
244
|
+
height: 100%;
|
|
245
|
+
cursor: col-resize;
|
|
246
|
+
/* Invisible until wanted: an always-on divider line reads as a border and
|
|
247
|
+
invites clicks that do nothing. */
|
|
248
|
+
background: transparent;
|
|
249
|
+
transition: background 0.12s;
|
|
250
|
+
/* Above the drawer's own content so the grab always lands on the handle.
|
|
251
|
+
Local to the drawer's stacking context, so it stays under the footer's
|
|
252
|
+
account popup rather than competing with the page. */
|
|
253
|
+
z-index: 1;
|
|
254
|
+
}
|
|
255
|
+
.xeplr-nav-drawer-resize:hover,
|
|
256
|
+
.xeplr-nav-drawer-resize:focus-visible,
|
|
257
|
+
.xeplr-nav-drawer-resizing .xeplr-nav-drawer-resize {
|
|
258
|
+
background: var(--xeplr-accent, #2563eb);
|
|
259
|
+
}
|
|
260
|
+
/* The focus ring would be a 6px sliver and easy to miss; the accent fill above
|
|
261
|
+
is the visible focus state, so the default outline only gets in its way. */
|
|
262
|
+
.xeplr-nav-drawer-resize:focus-visible { outline: none; }
|
|
263
|
+
|
|
264
|
+
/* Set on <body> for the length of the drag (see useNavController). A drag
|
|
265
|
+
routinely outruns the 6px handle, and without this the cursor flips back to
|
|
266
|
+
a caret and the page starts blue-selecting the moment it does. */
|
|
267
|
+
body.xeplr-nav-resizing { user-select: none; }
|
|
268
|
+
body.xeplr-nav-resizing * { cursor: col-resize !important; }
|
|
269
|
+
|
|
270
|
+
.xeplr-nav-drawer-toggle {
|
|
271
|
+
display: block;
|
|
272
|
+
flex-shrink: 0;
|
|
273
|
+
border: none;
|
|
274
|
+
background: none;
|
|
275
|
+
padding: 0;
|
|
276
|
+
margin-bottom: 12px;
|
|
277
|
+
cursor: pointer;
|
|
278
|
+
align-self: flex-start;
|
|
279
|
+
}
|
|
280
|
+
/* CENTRED WHEN COLLAPSED, like everything under it.
|
|
281
|
+
The rail's links get `justify-content: center`, so their icons sit in the
|
|
282
|
+
middle of the 40px of content width. The toggle had `padding: 0` and
|
|
283
|
+
`align-self: flex-start`, which pinned the logo hard against the left edge —
|
|
284
|
+
so the mark and the first menu icon were visibly on two different vertical
|
|
285
|
+
lines, which is the one thing a rail of icons cannot afford. */
|
|
286
|
+
.xeplr-nav-drawer-collapsed .xeplr-nav-drawer-toggle { align-self: center; }
|
|
287
|
+
|
|
288
|
+
/* The collapse affordance, when the lockup below is carrying the brand. */
|
|
289
|
+
.xeplr-nav-drawer-collapse {
|
|
290
|
+
display: block;
|
|
291
|
+
width: 22px;
|
|
292
|
+
line-height: 22px;
|
|
293
|
+
text-align: center;
|
|
294
|
+
font-size: 15px;
|
|
295
|
+
color: var(--xeplr-text-secondary, #8b8f9a);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/* Fixed, small, and never swapped for a different image (see NavDrawer.jsx) —
|
|
299
|
+
deliberately smaller than the collapsed rail's content width (60px rail -
|
|
300
|
+
20px padding = 40px) so it can never force the rail wider than intended. */
|
|
301
|
+
.xeplr-nav-drawer-logo { width: 22px; height: 22px; object-fit: contain; display: block; }
|
|
302
|
+
|
|
303
|
+
/* Its own row, real height — typically a stacked icon+wordmark lockup
|
|
304
|
+
(near-square, not a wide banner), so 22px (the toggle icon's height) would
|
|
305
|
+
crush the wordmark to a few illegible pixels. */
|
|
306
|
+
.xeplr-nav-drawer-expanded-logo {
|
|
307
|
+
height: 40px;
|
|
308
|
+
width: auto;
|
|
309
|
+
object-fit: contain;
|
|
310
|
+
display: block;
|
|
311
|
+
margin-bottom: 16px;
|
|
312
|
+
align-self: flex-start;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.xeplr-nav-drawer-search {
|
|
316
|
+
width: 100%;
|
|
317
|
+
margin-bottom: 12px;
|
|
318
|
+
padding: 8px 10px;
|
|
319
|
+
border: 1px solid var(--xeplr-border-secondary);
|
|
320
|
+
border-radius: 6px;
|
|
321
|
+
background: var(--xeplr-bg-primary);
|
|
322
|
+
color: var(--xeplr-text-primary);
|
|
323
|
+
font-size: 13px;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.xeplr-nav-drawer-links {
|
|
327
|
+
display: flex;
|
|
328
|
+
flex-direction: column;
|
|
329
|
+
gap: 2px;
|
|
330
|
+
flex: 1;
|
|
331
|
+
overflow-y: auto;
|
|
332
|
+
overflow-x: hidden;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.xeplr-nav-drawer-group { display: flex; flex-direction: column; gap: 2px; margin-top: 10px; }
|
|
336
|
+
.xeplr-nav-drawer-group-label {
|
|
337
|
+
padding: 4px 10px;
|
|
338
|
+
font-size: 11px;
|
|
339
|
+
font-weight: 600;
|
|
340
|
+
letter-spacing: 0.04em;
|
|
341
|
+
text-transform: uppercase;
|
|
342
|
+
color: var(--xeplr-text-muted);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.xeplr-nav-drawer-link {
|
|
346
|
+
display: flex;
|
|
347
|
+
align-items: center;
|
|
348
|
+
gap: 10px;
|
|
349
|
+
padding: 9px 10px;
|
|
350
|
+
border: none;
|
|
351
|
+
border-radius: 6px;
|
|
352
|
+
background: none;
|
|
353
|
+
color: var(--xeplr-text-primary);
|
|
354
|
+
font-size: 14px;
|
|
355
|
+
text-align: left;
|
|
356
|
+
cursor: pointer;
|
|
357
|
+
}
|
|
358
|
+
.xeplr-nav-drawer-link:hover { background: var(--xeplr-bg-hover); }
|
|
359
|
+
.xeplr-nav-drawer-collapsed .xeplr-nav-drawer-link { justify-content: center; }
|
|
360
|
+
|
|
361
|
+
.xeplr-nav-drawer-icon { display: grid; place-items: center; flex-shrink: 0; }
|
|
362
|
+
.xeplr-nav-drawer-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
363
|
+
|
|
364
|
+
/* ── item badges ─────────────────────────────────────────────────────────
|
|
365
|
+
Optional `item.badge`. The pill takes the remaining width (margin-left
|
|
366
|
+
auto) so counts line up down the rail instead of tracking label lengths.
|
|
367
|
+
The label keeps flex:1 so a long name still ellipsises rather than pushing
|
|
368
|
+
the pill off the edge. */
|
|
369
|
+
.xeplr-nav-drawer-link-badged .xeplr-nav-drawer-label { flex: 1; }
|
|
370
|
+
.xeplr-nav-drawer-badge {
|
|
371
|
+
margin-left: auto;
|
|
372
|
+
flex-shrink: 0;
|
|
373
|
+
min-width: 18px;
|
|
374
|
+
padding: 1px 6px;
|
|
375
|
+
border-radius: 9px;
|
|
376
|
+
background: var(--xeplr-danger, #c0392b);
|
|
377
|
+
color: #fff;
|
|
378
|
+
font-size: 11px;
|
|
379
|
+
font-weight: 600;
|
|
380
|
+
line-height: 16px;
|
|
381
|
+
text-align: center;
|
|
382
|
+
}
|
|
383
|
+
/* Collapsed: the rail is icons only, so the count becomes a dot pinned to the
|
|
384
|
+
icon's corner. `position: relative` on the icon rather than the button
|
|
385
|
+
because the button is centred and the icon is what the eye is on. */
|
|
386
|
+
.xeplr-nav-drawer-collapsed .xeplr-nav-drawer-icon { position: relative; }
|
|
387
|
+
.xeplr-nav-drawer-dot {
|
|
388
|
+
position: absolute;
|
|
389
|
+
top: -2px;
|
|
390
|
+
right: -3px;
|
|
391
|
+
width: 8px;
|
|
392
|
+
height: 8px;
|
|
393
|
+
border-radius: 50%;
|
|
394
|
+
background: var(--xeplr-danger, #c0392b);
|
|
395
|
+
/* Against the rail's own background, so the dot reads as ON the icon rather
|
|
396
|
+
than as part of it. */
|
|
397
|
+
box-shadow: 0 0 0 2px var(--xeplr-bg-elevated, #fff);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.xeplr-nav-drawer-empty { padding: 10px 12px; color: var(--xeplr-text-muted); font-size: 13px; }
|
|
401
|
+
|
|
402
|
+
.xeplr-nav-drawer-promo { padding-top: 12px; }
|
|
403
|
+
|
|
404
|
+
/* Settings (AccountMenu, placement="top") + notifications — the drawer's
|
|
405
|
+
footer, since there's no top bar for them to live in when the drawer is
|
|
406
|
+
the whole nav. `.xeplr-nav-drawer-links` already has flex:1, so this and
|
|
407
|
+
any promo above it are naturally pushed to the bottom without needing
|
|
408
|
+
their own margin-top:auto. */
|
|
409
|
+
.xeplr-nav-drawer-footer {
|
|
410
|
+
display: flex;
|
|
411
|
+
flex-direction: column;
|
|
412
|
+
/* stretch so the expanded (labeled) rows can go full-width; the collapsed
|
|
413
|
+
circular icon buttons self-center via their own align-self: center. */
|
|
414
|
+
align-items: stretch;
|
|
415
|
+
gap: 6px;
|
|
416
|
+
padding-top: 12px;
|
|
417
|
+
margin-top: 12px;
|
|
418
|
+
border-top: 1px solid var(--xeplr-border-secondary);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/* Opt-in alternative to the drawer footer above (see NavPage's `floatingSettings`
|
|
422
|
+
prop) — same bell + account trigger, floating over the page instead of
|
|
423
|
+
pinned to the drawer. Just the two icons themselves, no card/box around
|
|
424
|
+
them — no background, border, or shadow.
|
|
425
|
+
|
|
426
|
+
z-index sits above page-level floating chrome, not below it. The icons
|
|
427
|
+
themselves never overlap anything, but the account menu drops DOWN over the
|
|
428
|
+
page — and a settings menu that opens behind a page toolbar is unusable.
|
|
429
|
+
Global chrome wins over page tools; a consumer's own floating toolbar
|
|
430
|
+
should stay below this and position itself clear of the icons. */
|
|
431
|
+
.xeplr-nav-floating {
|
|
432
|
+
position: fixed;
|
|
433
|
+
top: 16px;
|
|
434
|
+
right: 16px;
|
|
435
|
+
z-index: 400;
|
|
436
|
+
display: flex;
|
|
437
|
+
align-items: center;
|
|
438
|
+
gap: 8px;
|
|
439
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
1
|
// API client
|
|
2
|
-
export { configure, registerUser, loginUser, activateAccount, forgotPassword, resetPassword, changePassword, getProfile, updateProfile,
|
|
2
|
+
export { configure, setSessionExpiredHandler, registerUser, loginUser, activateAccount, forgotPassword, resetPassword, changePassword, getMe, getProfile, updateProfile, uploadAvatar, logoutUser, authFetch } from './api.js';
|
|
3
3
|
|
|
4
4
|
// Token helpers
|
|
5
5
|
export { getToken, setToken, getRefreshToken, setRefreshToken, getUser, setUser, clearAuth, isAuthenticated } from './token.js';
|
|
6
6
|
|
|
7
|
+
// Multi-tenant levels — call registerMTs() once at app boot with the SAME
|
|
8
|
+
// shape passed to @xeplr/db's registerMTs() on the backend. Teaches authFetch
|
|
9
|
+
// which header to attach for each level, read from activeScope. See mt.js.
|
|
10
|
+
export { registerMTs, getMtConfig } from './mt.js';
|
|
11
|
+
|
|
12
|
+
// Active scope (e.g. company/workspace), keyed by MT level (l1, l2, ...) — a
|
|
13
|
+
// bare, app-interpreted value per level, attached to every authFetch call as
|
|
14
|
+
// that level's configured header. See activeScope.js.
|
|
15
|
+
export { getActiveScope, setActiveScope, clearActiveScope, getLastScope, setLastScope } from './activeScope.js';
|
|
16
|
+
|
|
17
|
+
// Where to send the user once a redirect-driven gate (auth, or an app's own
|
|
18
|
+
// gate) resolves — e.g. back to the exact dashboard a deep link pointed at,
|
|
19
|
+
// rather than always home. See returnTo.js.
|
|
20
|
+
export { saveReturnTo, consumeReturnTo } from './returnTo.js';
|
|
21
|
+
|
|
7
22
|
// Controller hooks
|
|
8
23
|
export { useLoginController } from './useLoginController.js';
|
|
9
24
|
export { useRegisterController } from './useRegisterController.js';
|
|
@@ -15,8 +30,7 @@ export { useProfileController } from './useProfileController.js';
|
|
|
15
30
|
export { useUserRolesController } from './useUserRolesController.js';
|
|
16
31
|
export { useAccessMatrixController } from './useAccessMatrixController.js';
|
|
17
32
|
export { useMasterSettingsController } from './useMasterSettingsController.js';
|
|
18
|
-
export {
|
|
19
|
-
export { useTenantPickerController, getActiveTenant, setActiveTenant, clearActiveTenant, hasMultipleTenants } from './useTenantPickerController.js';
|
|
33
|
+
export { useNavController } from './useNavController.js';
|
|
20
34
|
|
|
21
35
|
// Theme
|
|
22
36
|
export { ThemeProvider, useTheme, useThemeStrict, BUILT_IN_THEMES, DEFAULT_THEME } from './ThemeContext.jsx';
|
|
@@ -29,13 +43,15 @@ export { AccessGuard } from './AccessGuard.jsx';
|
|
|
29
43
|
// Admin API (model layer for RBAC management)
|
|
30
44
|
export { getUsers, getRoles, getAccessItems, toggleUserRole, toggleAccessRole, toggleModuleRole } from './adminApi.js';
|
|
31
45
|
export { getMasterItems, saveMasterItem, deleteMasterItem, MASTER_TYPES } from './masterApi.js';
|
|
32
|
-
export { getTenants, saveTenant, deleteTenant, assignUserTenant } from './adminApi.js';
|
|
33
46
|
|
|
34
47
|
// Design validation (use when building custom designs)
|
|
35
|
-
export { useDesignValidator, LOGIN_RULES, REGISTER_RULES, FORGOT_PASSWORD_RULES, RESET_PASSWORD_RULES, CHANGE_PASSWORD_RULES, PROFILE_RULES, USER_ROLES_MATRIX_RULES, ACCESS_MATRIX_RULES, MASTER_SETTINGS_RULES } from './validateDesign.js';
|
|
48
|
+
export { useDesignValidator, LOGIN_RULES, REGISTER_RULES, FORGOT_PASSWORD_RULES, RESET_PASSWORD_RULES, CHANGE_PASSWORD_RULES, PROFILE_RULES, USER_ROLES_MATRIX_RULES, ACCESS_MATRIX_RULES, MASTER_SETTINGS_RULES, NAV_RULES } from './validateDesign.js';
|
|
36
49
|
|
|
37
50
|
// Sample designs (use as reference or starting point)
|
|
38
|
-
export { LoginSample, RegisterSample, ForgotPasswordSample, ResetPasswordSample, ActivateSample, NotActivatedSample, ChangePasswordSample, ProfileSample, UserRolesMatrixSample, AccessMatrixSample, MasterSettingsSample,
|
|
51
|
+
export { LoginSample, RegisterSample, ForgotPasswordSample, ResetPasswordSample, ActivateSample, NotActivatedSample, ChangePasswordSample, ProfileSample, UserRolesMatrixSample, AccessMatrixSample, MasterSettingsSample, NavTopSample, AccountMenu, NavDrawer, NavFloatingSettings, NotificationsBell } from './designs/index.js';
|
|
52
|
+
|
|
53
|
+
// Ready-made pages (controller + sample design wired together; each takes an optional `design` prop)
|
|
54
|
+
export { LoginPage, RegisterPage, ForgotPasswordPage, ResetPasswordPage, ActivatePage, NotActivatedPage, ChangePasswordPage, ProfilePage, UserRolesPage, AccessMatrixPage, MasterSettingsPage, NavPage } from './pages.jsx';
|
|
39
55
|
|
|
40
|
-
//
|
|
41
|
-
export {
|
|
56
|
+
// One-call auth routing — mount every auth page with no boilerplate; override just what you want
|
|
57
|
+
export { authRoutes, authPath } from './authRoutes.jsx';
|
package/src/mt.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Frontend counterpart to @xeplr/db's BaseModel.registerMTs() — call this once
|
|
2
|
+
// at app boot with the SAME config object (same level keys, names, headers).
|
|
3
|
+
// There's no shared runtime package between frontend and backend (by design —
|
|
4
|
+
// duplicated by convention); keep one literal config in your own app and
|
|
5
|
+
// import it into both boot entry points so it can't drift.
|
|
6
|
+
//
|
|
7
|
+
// registerMTs({
|
|
8
|
+
// l1: { name: 'companyId', header: 'x-company-id' },
|
|
9
|
+
// l2: { name: 'workspaceId', header: 'x-workspace-id' }
|
|
10
|
+
// });
|
|
11
|
+
//
|
|
12
|
+
// This only teaches api.js's authFetch which header to attach for each level
|
|
13
|
+
// (read from activeScope.js) — it doesn't validate anything itself.
|
|
14
|
+
|
|
15
|
+
var SLOT_KEYS = ['l1', 'l2', 'l3', 'l4'];
|
|
16
|
+
var _config = { slots: {} };
|
|
17
|
+
|
|
18
|
+
export function registerMTs(config) {
|
|
19
|
+
config = config || {};
|
|
20
|
+
var slots = {};
|
|
21
|
+
SLOT_KEYS.forEach(function(key) {
|
|
22
|
+
if (config[key]) {
|
|
23
|
+
slots[key] = { name: config[key].name, header: String(config[key].header).toLowerCase() };
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
_config = { slots: slots };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getMtConfig() {
|
|
30
|
+
return { slots: Object.assign({}, _config.slots) };
|
|
31
|
+
}
|