@marianmeres/stuic 3.75.0 → 3.76.1
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/dist/components/Button/Button.svelte +2 -0
- package/dist/components/Button/index.css +18 -0
- package/dist/components/DismissibleMessage/DismissibleMessage.svelte +28 -4
- package/dist/components/DismissibleMessage/index.css +0 -7
- package/dist/components/LoginForm/LoginForm.svelte +28 -0
- package/dist/components/LoginForm/LoginFormModal.svelte +9 -0
- package/dist/components/LoginForm/LoginFormModal.svelte.d.ts +6 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +14 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +7 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte +19 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterFormModal.svelte.d.ts +13 -0
- package/dist/components/Modal/Modal.svelte +4 -0
- package/dist/components/Modal/Modal.svelte.d.ts +2 -0
- package/package.json +1 -1
|
@@ -142,6 +142,7 @@
|
|
|
142
142
|
data-rounded-full={!unstyled && roundedFull ? "true" : undefined}
|
|
143
143
|
data-aspect1={!unstyled && _isAspect1 ? "true" : undefined}
|
|
144
144
|
data-icon-button={!unstyled && _isIconButton ? "true" : undefined}
|
|
145
|
+
data-x={!unstyled && !!_xProps ? "true" : undefined}
|
|
145
146
|
use:tooltip={_tooltipConfig}
|
|
146
147
|
{...rest as HTMLAnchorAttributes}
|
|
147
148
|
>
|
|
@@ -175,6 +176,7 @@
|
|
|
175
176
|
data-rounded-full={!unstyled && roundedFull ? "true" : undefined}
|
|
176
177
|
data-aspect1={!unstyled && _isAspect1 ? "true" : undefined}
|
|
177
178
|
data-icon-button={!unstyled && _isIconButton ? "true" : undefined}
|
|
179
|
+
data-x={!unstyled && !!_xProps ? "true" : undefined}
|
|
178
180
|
use:tooltip={_tooltipConfig}
|
|
179
181
|
{...rest}
|
|
180
182
|
>
|
|
@@ -42,6 +42,14 @@
|
|
|
42
42
|
/* Raised (3D push) effect */
|
|
43
43
|
--stuic-button-raised-offset: 2px;
|
|
44
44
|
--stuic-button-raised-color: rgb(0 0 0 / 0.8);
|
|
45
|
+
|
|
46
|
+
/* Neutral overlay hover for "pure rounded X" (ghost + x + roundedFull).
|
|
47
|
+
Intentionally non-themed — sits on top of any background color. */
|
|
48
|
+
--stuic-button-x-bg-hover: rgb(0 0 0 / 0.1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
:root.dark {
|
|
52
|
+
--stuic-button-x-bg-hover: rgb(255 255 255 / 0.05);
|
|
45
53
|
}
|
|
46
54
|
|
|
47
55
|
@layer components {
|
|
@@ -416,4 +424,14 @@
|
|
|
416
424
|
.stuic-button[data-icon-button] {
|
|
417
425
|
--stuic-button-radius: 9999px;
|
|
418
426
|
}
|
|
427
|
+
|
|
428
|
+
/* ============================================================================
|
|
429
|
+
PURE ROUNDED X
|
|
430
|
+
Ghost variant + x icon + fully rounded — replace the intent-tinted ghost
|
|
431
|
+
hover with a neutral overlay so the button reads on any background.
|
|
432
|
+
============================================================================ */
|
|
433
|
+
.stuic-button[data-variant="ghost"][data-x][data-rounded-full] {
|
|
434
|
+
--_bg-hover: var(--stuic-button-x-bg-hover);
|
|
435
|
+
--_bg-active: var(--stuic-button-x-bg-hover);
|
|
436
|
+
}
|
|
419
437
|
}
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
</script>
|
|
19
19
|
|
|
20
20
|
<script lang="ts">
|
|
21
|
+
import { untrack } from "svelte";
|
|
21
22
|
import { slide } from "svelte/transition";
|
|
22
23
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
23
24
|
import Thc, { isTHCNotEmpty } from "../Thc/Thc.svelte";
|
|
@@ -44,13 +45,36 @@
|
|
|
44
45
|
intent,
|
|
45
46
|
forceAsHtml = true,
|
|
46
47
|
duration = 150,
|
|
47
|
-
onDismiss
|
|
48
|
+
onDismiss,
|
|
48
49
|
withIcon,
|
|
49
50
|
iconFn,
|
|
50
51
|
}: Props = $props();
|
|
51
52
|
|
|
53
|
+
// Track dismissal in local state instead of mutating the (non-bindable) `message`
|
|
54
|
+
// prop. Mutating a destructured prop var creates a local shadow that Svelte 5
|
|
55
|
+
// won't always overwrite when the parent re-passes the same value — so a user
|
|
56
|
+
// who dismissed an error would never see the SAME error message again, even
|
|
57
|
+
// after the parent re-set it. Keeping `_dismissed` separate sidesteps that and
|
|
58
|
+
// makes the dismiss state reset cleanly whenever the message changes.
|
|
52
59
|
let _message = $derived(message ? String(message) : "");
|
|
53
|
-
let
|
|
60
|
+
let _dismissed = $state(false);
|
|
61
|
+
let _show = $derived(isTHCNotEmpty(_message) && !_dismissed);
|
|
62
|
+
|
|
63
|
+
// Reset the dismissed flag whenever the message changes — a new (or re-set)
|
|
64
|
+
// message from the parent should re-show, even if the user previously dismissed.
|
|
65
|
+
$effect(() => {
|
|
66
|
+
void _message;
|
|
67
|
+
untrack(() => {
|
|
68
|
+
if (_dismissed) _dismissed = false;
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Default dismiss handler hides the message locally. Parent state is left alone
|
|
73
|
+
// (this prop isn't bindable). Consumers wanting parent-side cleanup pass their own.
|
|
74
|
+
// `null`/`false` mean "no dismiss button" and are handled at the render site.
|
|
75
|
+
let _onDismiss = $derived(
|
|
76
|
+
typeof onDismiss === "function" ? onDismiss : () => (_dismissed = true)
|
|
77
|
+
);
|
|
54
78
|
|
|
55
79
|
let _iconHtml = $derived.by(() => {
|
|
56
80
|
if (iconFn === false) return "";
|
|
@@ -77,7 +101,7 @@
|
|
|
77
101
|
<Thc thc={_message} {forceAsHtml} />
|
|
78
102
|
</div>
|
|
79
103
|
|
|
80
|
-
{#if
|
|
104
|
+
{#if onDismiss !== false && onDismiss !== null}
|
|
81
105
|
<div class="dismiss">
|
|
82
106
|
<Button
|
|
83
107
|
x
|
|
@@ -86,7 +110,7 @@
|
|
|
86
110
|
roundedFull
|
|
87
111
|
size="sm"
|
|
88
112
|
type="button"
|
|
89
|
-
onclick={() =>
|
|
113
|
+
onclick={() => _onDismiss()}
|
|
90
114
|
/>
|
|
91
115
|
</div>
|
|
92
116
|
{/if}
|
|
@@ -8,11 +8,6 @@
|
|
|
8
8
|
--stuic-dismissible-message-padding-x: calc(var(--spacing) * 4);
|
|
9
9
|
--stuic-dismissible-message-padding-y: calc(var(--spacing) * 3);
|
|
10
10
|
--stuic-dismissible-x-padding: calc(var(--spacing) * 1);
|
|
11
|
-
--stuic-dismissible-x-bg-hover: rgb(0 0 0 / 0.1);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
:root.dark {
|
|
15
|
-
--stuic-dismissible-x-bg-hover: rgb(255 255 255 / 0.05);
|
|
16
11
|
}
|
|
17
12
|
|
|
18
13
|
@layer components {
|
|
@@ -73,8 +68,6 @@
|
|
|
73
68
|
/* Dismiss button inherits message text color */
|
|
74
69
|
.stuic-dismissible-message > .dismiss .stuic-button {
|
|
75
70
|
color: var(--_text);
|
|
76
|
-
--_bg-hover: var(--stuic-dismissible-x-bg-hover);
|
|
77
|
-
--_bg-active: var(--stuic-dismissible-x-bg-hover);
|
|
78
71
|
}
|
|
79
72
|
|
|
80
73
|
/* =============================================================================
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
</script>
|
|
78
78
|
|
|
79
79
|
<script lang="ts">
|
|
80
|
+
import { untrack } from "svelte";
|
|
80
81
|
import { twMerge } from "../../utils/tw-merge.js";
|
|
81
82
|
import { t_default } from "./_internal/login-form-i18n-defaults.js";
|
|
82
83
|
import {
|
|
@@ -130,6 +131,19 @@
|
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
function handleSubmitValid() {
|
|
134
|
+
// Defensively clear any stale customValidity left on form fields by a prior
|
|
135
|
+
// validation pass. The `validate` action already clears it when the field's
|
|
136
|
+
// customValidator returns an empty string, but a field can skip that path
|
|
137
|
+
// (e.g., it was disabled, or its $effect torn down between submits) and the
|
|
138
|
+
// stale flag would then make `el.validity.valid` return false on the next
|
|
139
|
+
// submit, silently routing the form to `submit_invalid` and never calling
|
|
140
|
+
// `onSubmit`. Resetting here gives every retry a clean slate.
|
|
141
|
+
if (el) {
|
|
142
|
+
for (const node of Array.from(el.elements) as HTMLInputElement[]) {
|
|
143
|
+
if (typeof node.setCustomValidity === "function") node.setCustomValidity("");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
133
147
|
const validationErrors = validateLoginForm(formData, t);
|
|
134
148
|
internalErrors = validationErrors;
|
|
135
149
|
|
|
@@ -138,6 +152,20 @@
|
|
|
138
152
|
}
|
|
139
153
|
}
|
|
140
154
|
|
|
155
|
+
// Clear internal field errors as soon as the user edits the form, so a previous
|
|
156
|
+
// failed-submit's errors don't linger after the user has fixed them. Re-validation
|
|
157
|
+
// on the next submit will repopulate `internalErrors` if anything is still wrong.
|
|
158
|
+
// `untrack` for the read+write so this effect only re-runs on formData changes —
|
|
159
|
+
// otherwise `handleSubmitValid` setting `internalErrors` would immediately re-fire
|
|
160
|
+
// this effect and wipe the errors back out.
|
|
161
|
+
$effect(() => {
|
|
162
|
+
void formData.email;
|
|
163
|
+
void formData.password;
|
|
164
|
+
untrack(() => {
|
|
165
|
+
if (internalErrors.length) internalErrors = [];
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
141
169
|
$effect(() => {
|
|
142
170
|
if (error && notifications) notifications.error(error);
|
|
143
171
|
});
|
|
@@ -88,6 +88,13 @@
|
|
|
88
88
|
|
|
89
89
|
noXClose?: boolean;
|
|
90
90
|
onClose?: () => false | void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Disable close on backdrop / outside click. Defaults to `true` because
|
|
94
|
+
* accidentally losing typed credentials due to a stray backdrop click is a
|
|
95
|
+
* worse UX than requiring an explicit close. Set to `false` to opt back in.
|
|
96
|
+
*/
|
|
97
|
+
noClickOutsideClose?: boolean;
|
|
91
98
|
}
|
|
92
99
|
</script>
|
|
93
100
|
|
|
@@ -126,6 +133,7 @@
|
|
|
126
133
|
unstyled = false,
|
|
127
134
|
noXClose = false,
|
|
128
135
|
onClose,
|
|
136
|
+
noClickOutsideClose = true,
|
|
129
137
|
}: Props = $props();
|
|
130
138
|
|
|
131
139
|
let t = $derived(tProp ?? t_default);
|
|
@@ -152,6 +160,7 @@
|
|
|
152
160
|
class={classModal}
|
|
153
161
|
classInner={twMerge("max-w-sm md:max-w-sm", "h-auto md:h-auto m-auto", classInner)}
|
|
154
162
|
classDialog="flex items-center justify-center"
|
|
163
|
+
{noClickOutsideClose}
|
|
155
164
|
>
|
|
156
165
|
{#snippet header()}
|
|
157
166
|
<div class="flex items-center justify-between p-4">
|
|
@@ -67,6 +67,12 @@ export interface Props {
|
|
|
67
67
|
unstyled?: boolean;
|
|
68
68
|
noXClose?: boolean;
|
|
69
69
|
onClose?: () => false | void;
|
|
70
|
+
/**
|
|
71
|
+
* Disable close on backdrop / outside click. Defaults to `true` because
|
|
72
|
+
* accidentally losing typed credentials due to a stray backdrop click is a
|
|
73
|
+
* worse UX than requiring an explicit close. Set to `false` to opt back in.
|
|
74
|
+
*/
|
|
75
|
+
noClickOutsideClose?: boolean;
|
|
70
76
|
}
|
|
71
77
|
declare const LoginFormModal: import("svelte").Component<Props, {
|
|
72
78
|
open: (openerOrEvent?: null | HTMLElement | MouseEvent) => void;
|
|
@@ -117,6 +117,15 @@
|
|
|
117
117
|
>;
|
|
118
118
|
|
|
119
119
|
notifications?: NotificationsStack;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Called when the active mode changes (login/register/verify). Receives
|
|
123
|
+
* `(next, prev)`. Use this to clear parent-owned, mode-specific state — e.g.,
|
|
124
|
+
* a general `error` string that shouldn't survive a transition between Login
|
|
125
|
+
* and Sign up.
|
|
126
|
+
*/
|
|
127
|
+
onModeChange?: (next: LoginOrRegisterFormMode, prev: LoginOrRegisterFormMode) => void;
|
|
128
|
+
|
|
120
129
|
t?: TranslateFn;
|
|
121
130
|
unstyled?: boolean;
|
|
122
131
|
class?: string;
|
|
@@ -155,6 +164,7 @@
|
|
|
155
164
|
socialDividerLabel,
|
|
156
165
|
footer,
|
|
157
166
|
notifications,
|
|
167
|
+
onModeChange,
|
|
158
168
|
t: tProp,
|
|
159
169
|
unstyled = false,
|
|
160
170
|
class: classProp,
|
|
@@ -173,6 +183,7 @@
|
|
|
173
183
|
// effect (which would be prone to loops).
|
|
174
184
|
function setMode(next: LoginOrRegisterFormMode) {
|
|
175
185
|
if (next === mode) return;
|
|
186
|
+
const prev = mode;
|
|
176
187
|
const sourceEmail =
|
|
177
188
|
mode === "verify"
|
|
178
189
|
? verifyEmail
|
|
@@ -187,6 +198,9 @@
|
|
|
187
198
|
// next === "verify"
|
|
188
199
|
verifyEmail = sourceEmail;
|
|
189
200
|
}
|
|
201
|
+
// Notify before mutating so consumers can clear parent-owned, mode-specific
|
|
202
|
+
// state (e.g., a stale `error`) before the new view renders with it.
|
|
203
|
+
onModeChange?.(next, prev);
|
|
190
204
|
mode = next;
|
|
191
205
|
}
|
|
192
206
|
|
|
@@ -78,6 +78,13 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
|
|
|
78
78
|
}
|
|
79
79
|
]>;
|
|
80
80
|
notifications?: NotificationsStack;
|
|
81
|
+
/**
|
|
82
|
+
* Called when the active mode changes (login/register/verify). Receives
|
|
83
|
+
* `(next, prev)`. Use this to clear parent-owned, mode-specific state — e.g.,
|
|
84
|
+
* a general `error` string that shouldn't survive a transition between Login
|
|
85
|
+
* and Sign up.
|
|
86
|
+
*/
|
|
87
|
+
onModeChange?: (next: LoginOrRegisterFormMode, prev: LoginOrRegisterFormMode) => void;
|
|
81
88
|
t?: TranslateFn;
|
|
82
89
|
unstyled?: boolean;
|
|
83
90
|
class?: string;
|
|
@@ -84,6 +84,21 @@
|
|
|
84
84
|
|
|
85
85
|
noXClose?: boolean;
|
|
86
86
|
onClose?: () => false | void;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Disable close on backdrop / outside click. Defaults to `true` because
|
|
90
|
+
* accidentally losing typed credentials due to a stray backdrop click is a
|
|
91
|
+
* worse UX than requiring an explicit close. Set to `false` to opt back in.
|
|
92
|
+
*/
|
|
93
|
+
noClickOutsideClose?: boolean;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Called when the active form mode changes (login/register/verify). Receives
|
|
97
|
+
* `(next, prev)`. Use this to clear parent-owned, mode-specific state — e.g.,
|
|
98
|
+
* a general `error` string that shouldn't survive a transition between Login
|
|
99
|
+
* and Sign up.
|
|
100
|
+
*/
|
|
101
|
+
onModeChange?: (next: LoginOrRegisterFormMode, prev: LoginOrRegisterFormMode) => void;
|
|
87
102
|
}
|
|
88
103
|
</script>
|
|
89
104
|
|
|
@@ -129,6 +144,8 @@
|
|
|
129
144
|
unstyled = false,
|
|
130
145
|
noXClose = false,
|
|
131
146
|
onClose,
|
|
147
|
+
noClickOutsideClose = true,
|
|
148
|
+
onModeChange,
|
|
132
149
|
}: Props = $props();
|
|
133
150
|
|
|
134
151
|
let t = $derived(tProp ?? t_default);
|
|
@@ -163,6 +180,7 @@
|
|
|
163
180
|
class={classModal}
|
|
164
181
|
classInner={twMerge("max-w-sm md:max-w-sm", "h-auto md:h-auto m-auto", classInner)}
|
|
165
182
|
classDialog="flex items-center justify-center"
|
|
183
|
+
{noClickOutsideClose}
|
|
166
184
|
>
|
|
167
185
|
{#snippet header()}
|
|
168
186
|
<div class="flex items-center justify-between p-4">
|
|
@@ -207,6 +225,7 @@
|
|
|
207
225
|
{socialDividerLabel}
|
|
208
226
|
{footer}
|
|
209
227
|
{notifications}
|
|
228
|
+
{onModeChange}
|
|
210
229
|
t={tProp}
|
|
211
230
|
{unstyled}
|
|
212
231
|
class={classForm}
|
|
@@ -59,6 +59,19 @@ export interface Props {
|
|
|
59
59
|
unstyled?: boolean;
|
|
60
60
|
noXClose?: boolean;
|
|
61
61
|
onClose?: () => false | void;
|
|
62
|
+
/**
|
|
63
|
+
* Disable close on backdrop / outside click. Defaults to `true` because
|
|
64
|
+
* accidentally losing typed credentials due to a stray backdrop click is a
|
|
65
|
+
* worse UX than requiring an explicit close. Set to `false` to opt back in.
|
|
66
|
+
*/
|
|
67
|
+
noClickOutsideClose?: boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Called when the active form mode changes (login/register/verify). Receives
|
|
70
|
+
* `(next, prev)`. Use this to clear parent-owned, mode-specific state — e.g.,
|
|
71
|
+
* a general `error` string that shouldn't survive a transition between Login
|
|
72
|
+
* and Sign up.
|
|
73
|
+
*/
|
|
74
|
+
onModeChange?: (next: LoginOrRegisterFormMode, prev: LoginOrRegisterFormMode) => void;
|
|
62
75
|
}
|
|
63
76
|
declare const LoginOrRegisterFormModal: import("svelte").Component<Props, {
|
|
64
77
|
open: (openerOrEvent?: null | HTMLElement | MouseEvent) => void;
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
onEscape?: () => void;
|
|
23
23
|
/** Disable body scroll lock when modal is open */
|
|
24
24
|
noScrollLock?: boolean;
|
|
25
|
+
/** Disable close on backdrop / outside click */
|
|
26
|
+
noClickOutsideClose?: boolean;
|
|
25
27
|
}
|
|
26
28
|
</script>
|
|
27
29
|
|
|
@@ -45,6 +47,7 @@
|
|
|
45
47
|
el = $bindable(),
|
|
46
48
|
onEscape,
|
|
47
49
|
noScrollLock = false,
|
|
50
|
+
noClickOutsideClose = false,
|
|
48
51
|
}: Props = $props();
|
|
49
52
|
|
|
50
53
|
let modalDialog: ModalDialog = $state()!;
|
|
@@ -89,6 +92,7 @@
|
|
|
89
92
|
ariaLabelledby={labelledby}
|
|
90
93
|
ariaDescribedby={describedby}
|
|
91
94
|
{noScrollLock}
|
|
95
|
+
{noClickOutsideClose}
|
|
92
96
|
preEscapeClose={handlePreEscapeClose}
|
|
93
97
|
preClose={handlePreClose}
|
|
94
98
|
class={twMerge(
|
|
@@ -20,6 +20,8 @@ export interface Props {
|
|
|
20
20
|
onEscape?: () => void;
|
|
21
21
|
/** Disable body scroll lock when modal is open */
|
|
22
22
|
noScrollLock?: boolean;
|
|
23
|
+
/** Disable close on backdrop / outside click */
|
|
24
|
+
noClickOutsideClose?: boolean;
|
|
23
25
|
}
|
|
24
26
|
declare const Modal: import("svelte").Component<Props, {
|
|
25
27
|
close: () => void;
|