@marianmeres/stuic 3.86.0 → 3.88.0
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/API.md +41 -0
- package/dist/actions/validate.svelte.d.ts +24 -4
- package/dist/actions/validate.svelte.js +18 -5
- package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +21 -0
- package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte.d.ts +4 -1
- package/dist/components/Input/FieldAssets.svelte +48 -3
- package/dist/components/Input/FieldAssets.svelte.d.ts +8 -2
- package/dist/components/Input/FieldCheckbox.svelte +34 -3
- package/dist/components/Input/FieldCheckbox.svelte.d.ts +8 -1
- package/dist/components/Input/FieldCountry.svelte +64 -7
- package/dist/components/Input/FieldCountry.svelte.d.ts +8 -1
- package/dist/components/Input/FieldFile.svelte +34 -3
- package/dist/components/Input/FieldFile.svelte.d.ts +8 -1
- package/dist/components/Input/FieldInput.svelte +43 -3
- package/dist/components/Input/FieldInput.svelte.d.ts +8 -1
- package/dist/components/Input/FieldInputLocalized.svelte +41 -2
- package/dist/components/Input/FieldInputLocalized.svelte.d.ts +8 -2
- package/dist/components/Input/FieldKeyValues.svelte +37 -2
- package/dist/components/Input/FieldKeyValues.svelte.d.ts +8 -2
- package/dist/components/Input/FieldLikeButton.svelte +41 -4
- package/dist/components/Input/FieldLikeButton.svelte.d.ts +8 -1
- package/dist/components/Input/FieldObject.svelte +64 -6
- package/dist/components/Input/FieldObject.svelte.d.ts +8 -2
- package/dist/components/Input/FieldOptions.svelte +36 -3
- package/dist/components/Input/FieldOptions.svelte.d.ts +8 -2
- package/dist/components/Input/FieldPhoneNumber.svelte +51 -6
- package/dist/components/Input/FieldPhoneNumber.svelte.d.ts +8 -1
- package/dist/components/Input/FieldRadios.svelte +36 -2
- package/dist/components/Input/FieldRadios.svelte.d.ts +8 -1
- package/dist/components/Input/FieldSelect.svelte +34 -3
- package/dist/components/Input/FieldSelect.svelte.d.ts +8 -1
- package/dist/components/Input/FieldSwitch.svelte +41 -2
- package/dist/components/Input/FieldSwitch.svelte.d.ts +8 -1
- package/dist/components/Input/FieldTextarea.svelte +34 -3
- package/dist/components/Input/FieldTextarea.svelte.d.ts +8 -1
- package/dist/components/Input/_internal/FieldRadioInternal.svelte +34 -3
- package/dist/components/Input/_internal/FieldRadioInternal.svelte.d.ts +7 -1
- package/dist/components/LoginForm/LoginForm.svelte +35 -0
- package/dist/components/LoginForm/LoginForm.svelte.d.ts +5 -1
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +40 -0
- package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +5 -1
- package/dist/components/RegisterForm/RegisterForm.svelte +46 -2
- package/dist/components/RegisterForm/RegisterForm.svelte.d.ts +5 -1
- package/dist/components/Switch/Switch.svelte +42 -4
- package/dist/components/Switch/Switch.svelte.d.ts +7 -1
- package/dist/components/UserAvatarMenu/README.md +188 -0
- package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte +416 -0
- package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte.d.ts +143 -0
- package/dist/components/UserAvatarMenu/index.css +95 -0
- package/dist/components/UserAvatarMenu/index.d.ts +1 -0
- package/dist/components/UserAvatarMenu/index.js +1 -0
- package/dist/icons/index.d.ts +3 -0
- package/dist/icons/index.js +3 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/validate-fields.d.ts +72 -0
- package/dist/utils/validate-fields.js +73 -0
- package/docs/domains/actions.md +74 -0
- package/docs/domains/components.md +190 -0
- package/docs/domains/utils.md +38 -0
- package/package.json +1 -1
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { DropdownMenuItem, DropdownMenuPosition } from "../DropdownMenu/DropdownMenu.svelte";
|
|
3
|
+
import type { Props as AvatarProps } from "../Avatar/Avatar.svelte";
|
|
4
|
+
/**
|
|
5
|
+
* Identity passed by the consumer. When `null` / `undefined`, the menu
|
|
6
|
+
* renders in unauthenticated mode. When provided, the menu renders the
|
|
7
|
+
* header tile (avatar + email) and the authenticated item set.
|
|
8
|
+
*/
|
|
9
|
+
export interface UserAvatarMenuIdentity {
|
|
10
|
+
/** Used for initials + auto-color hashing. Required when `identity` is set. */
|
|
11
|
+
email: string;
|
|
12
|
+
/** Optional display name (preferred over email in the header tile if present). */
|
|
13
|
+
name?: string;
|
|
14
|
+
/** Optional photo URL (forwarded to Avatar `src`). */
|
|
15
|
+
src?: string;
|
|
16
|
+
/** Optional role list — rendered under the email if `showRoles` is enabled. */
|
|
17
|
+
roles?: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Standard built-in actions. Consumers wire these by passing handlers;
|
|
21
|
+
* omitting a handler hides the corresponding item.
|
|
22
|
+
*
|
|
23
|
+
* The component never navigates, logs out, or toggles theme on its own —
|
|
24
|
+
* it only invokes callbacks. The only built-in side effect is
|
|
25
|
+
* `ColorScheme.toggle()` inside the color-scheme item (see `colorScheme`).
|
|
26
|
+
*/
|
|
27
|
+
export interface UserAvatarMenuActions {
|
|
28
|
+
/** "View profile" item. Hidden when not provided. (Auth state only.) */
|
|
29
|
+
onProfile?: () => void;
|
|
30
|
+
/** "Settings" item. Hidden when not provided. (Auth state only.) */
|
|
31
|
+
onSettings?: () => void;
|
|
32
|
+
/** "Logout" item. Hidden when not provided. (Auth state only.) */
|
|
33
|
+
onLogout?: () => void;
|
|
34
|
+
/**
|
|
35
|
+
* "Login or register" item — combined affordance, typically wired to
|
|
36
|
+
* open a `LoginOrRegisterFormModal`. Hidden when not provided.
|
|
37
|
+
* (Unauth state only.) Independent of `onLogin` / `onRegister`; pass
|
|
38
|
+
* any combination of the three.
|
|
39
|
+
*/
|
|
40
|
+
onLoginOrRegister?: () => void;
|
|
41
|
+
/** "Login" item. Hidden when not provided. (Unauth state only.) */
|
|
42
|
+
onLogin?: () => void;
|
|
43
|
+
/** "Register" item. Hidden when not provided. (Unauth state only.) */
|
|
44
|
+
onRegister?: () => void;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Labels for built-in items. All optional — defaults are English strings.
|
|
48
|
+
* Consumers running i18n pass already-translated strings here.
|
|
49
|
+
*/
|
|
50
|
+
export interface UserAvatarMenuLabels {
|
|
51
|
+
viewProfile?: string;
|
|
52
|
+
settings?: string;
|
|
53
|
+
logout?: string;
|
|
54
|
+
loginOrRegister?: string;
|
|
55
|
+
login?: string;
|
|
56
|
+
register?: string;
|
|
57
|
+
lightMode?: string;
|
|
58
|
+
darkMode?: string;
|
|
59
|
+
/** Trigger aria-label when authenticated. Default: "User menu". */
|
|
60
|
+
triggerAuthed?: string;
|
|
61
|
+
/** Trigger aria-label when unauthenticated. Default: "Sign in". */
|
|
62
|
+
triggerAnon?: string;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Color-scheme item config. Defaults to enabled.
|
|
66
|
+
* Pass `false` to disable; pass an object to customize.
|
|
67
|
+
*/
|
|
68
|
+
export type UserAvatarMenuColorScheme = boolean | {
|
|
69
|
+
enabled?: boolean;
|
|
70
|
+
/** Override the default `ColorScheme.toggle()` behavior. */
|
|
71
|
+
onToggle?: () => void;
|
|
72
|
+
/** Read current "is dark" state. Defaults to `ColorScheme.getValue() === "dark"`. */
|
|
73
|
+
isDark?: () => boolean;
|
|
74
|
+
};
|
|
75
|
+
export interface Props {
|
|
76
|
+
/** Current user. `null` / `undefined` → unauthenticated mode. */
|
|
77
|
+
identity?: UserAvatarMenuIdentity | null;
|
|
78
|
+
/** Action handlers (see `UserAvatarMenuActions`). */
|
|
79
|
+
actions?: UserAvatarMenuActions;
|
|
80
|
+
/** Translated / customized labels. */
|
|
81
|
+
labels?: UserAvatarMenuLabels;
|
|
82
|
+
/** Color-scheme toggle config. Default: enabled. */
|
|
83
|
+
colorScheme?: UserAvatarMenuColorScheme;
|
|
84
|
+
/** Render the identity header tile (avatar + email) in the dropdown. Default: `true`. */
|
|
85
|
+
showHeaderTile?: boolean;
|
|
86
|
+
/** Render roles under the email in the header tile. Default: `false`. */
|
|
87
|
+
showRoles?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Extra items appended to the standard set (after Logout / Register).
|
|
90
|
+
* Use for app-specific actions ("Switch project", "Billing", etc.).
|
|
91
|
+
*/
|
|
92
|
+
extraItems?: DropdownMenuItem[];
|
|
93
|
+
/**
|
|
94
|
+
* Full override. When provided, the standard item set is IGNORED and the
|
|
95
|
+
* component renders exactly these items. `identity` is still consulted for
|
|
96
|
+
* the trigger; everything else (`actions`, `labels`, `colorScheme`,
|
|
97
|
+
* `showHeaderTile`, `showRoles`) is not.
|
|
98
|
+
*/
|
|
99
|
+
items?: DropdownMenuItem[];
|
|
100
|
+
/** Forwarded to the default `Avatar` trigger and header-tile avatar. */
|
|
101
|
+
avatar?: Partial<Omit<AvatarProps, "onclick" | "initials" | "src" | "el">>;
|
|
102
|
+
/** Forwarded to `DropdownMenu`. */
|
|
103
|
+
position?: DropdownMenuPosition;
|
|
104
|
+
offset?: string;
|
|
105
|
+
maxHeight?: string;
|
|
106
|
+
closeOnSelect?: boolean;
|
|
107
|
+
classDropdown?: string;
|
|
108
|
+
classTrigger?: string;
|
|
109
|
+
/** Bindable open state (forwarded to `DropdownMenu`). */
|
|
110
|
+
isOpen?: boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Optional custom trigger snippet. Receives the same args as
|
|
113
|
+
* `DropdownMenu`'s `trigger` snippet — `isOpen`, `toggle`, `triggerProps`.
|
|
114
|
+
*/
|
|
115
|
+
trigger?: Snippet<[
|
|
116
|
+
{
|
|
117
|
+
isOpen: boolean;
|
|
118
|
+
toggle: () => void;
|
|
119
|
+
triggerProps: {
|
|
120
|
+
id: string;
|
|
121
|
+
"aria-haspopup": "menu";
|
|
122
|
+
"aria-expanded": boolean;
|
|
123
|
+
"aria-controls": string;
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
]>;
|
|
127
|
+
/**
|
|
128
|
+
* Optional custom header-tile snippet. Replaces the default
|
|
129
|
+
* avatar + email tile rendered at the top of the menu.
|
|
130
|
+
*/
|
|
131
|
+
headerTile?: Snippet<[{
|
|
132
|
+
identity: UserAvatarMenuIdentity;
|
|
133
|
+
}]>;
|
|
134
|
+
/** Skip default styling. */
|
|
135
|
+
unstyled?: boolean;
|
|
136
|
+
/** Additional CSS classes on the wrapper. */
|
|
137
|
+
class?: string;
|
|
138
|
+
/** Bindable wrapper element. */
|
|
139
|
+
el?: HTMLDivElement;
|
|
140
|
+
}
|
|
141
|
+
declare const UserAvatarMenu: import("svelte").Component<Props, {}, "el" | "isOpen">;
|
|
142
|
+
type UserAvatarMenu = ReturnType<typeof UserAvatarMenu>;
|
|
143
|
+
export default UserAvatarMenu;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
.stuic-user-avatar-menu {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
/* Fixed dropdown width so every instance opens at the same size, regardless
|
|
6
|
+
* of content length. Override the var, or pass `classDropdown` (Tailwind
|
|
7
|
+
* class wins via tw-merge). Long emails/names/roles truncate against this
|
|
8
|
+
* bounded width. */
|
|
9
|
+
.stuic-user-avatar-menu-dropdown {
|
|
10
|
+
width: var(--stuic-user-avatar-menu-dropdown-width, 16rem);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.stuic-user-avatar-menu-trigger {
|
|
14
|
+
display: inline-flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
background: transparent;
|
|
18
|
+
border: 0;
|
|
19
|
+
padding: 0;
|
|
20
|
+
margin: 0;
|
|
21
|
+
cursor: pointer;
|
|
22
|
+
border-radius: var(
|
|
23
|
+
--stuic-user-avatar-menu-trigger-radius,
|
|
24
|
+
var(--stuic-radius, var(--radius-md))
|
|
25
|
+
);
|
|
26
|
+
transition: opacity
|
|
27
|
+
var(--stuic-user-avatar-menu-transition, var(--stuic-transition, 150ms)) ease;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.stuic-user-avatar-menu-trigger:hover {
|
|
31
|
+
opacity: var(--stuic-user-avatar-menu-trigger-opacity-hover, 0.85);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.stuic-user-avatar-menu-trigger:focus-visible {
|
|
35
|
+
outline: 2px solid
|
|
36
|
+
var(
|
|
37
|
+
--stuic-user-avatar-menu-trigger-outline-color,
|
|
38
|
+
var(--stuic-color-primary, currentColor)
|
|
39
|
+
);
|
|
40
|
+
outline-offset: 2px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.stuic-user-avatar-menu-header {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: var(--stuic-user-avatar-menu-header-gap, 0.5rem);
|
|
48
|
+
padding: var(--stuic-user-avatar-menu-header-padding, 0.75rem 0.5rem);
|
|
49
|
+
margin-bottom: var(--stuic-user-avatar-menu-header-margin-bottom, 0.25rem);
|
|
50
|
+
background: var(
|
|
51
|
+
--stuic-user-avatar-menu-header-bg,
|
|
52
|
+
var(--stuic-color-muted, transparent)
|
|
53
|
+
);
|
|
54
|
+
color: var(
|
|
55
|
+
--stuic-user-avatar-menu-header-color,
|
|
56
|
+
var(--stuic-color-muted-foreground, inherit)
|
|
57
|
+
);
|
|
58
|
+
border-radius: var(
|
|
59
|
+
--stuic-user-avatar-menu-header-radius,
|
|
60
|
+
var(--stuic-radius, var(--radius-md))
|
|
61
|
+
);
|
|
62
|
+
/* fill the dropdown's content width so children can truncate against a
|
|
63
|
+
* known boundary (instead of growing to their intrinsic width) */
|
|
64
|
+
width: 100%;
|
|
65
|
+
min-width: 0;
|
|
66
|
+
overflow: hidden;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* Direct text children of the header tile must each get the full row width
|
|
70
|
+
* so `text-overflow: ellipsis` has something to clip against (under
|
|
71
|
+
* align-items: center, children otherwise shrink to their intrinsic width). */
|
|
72
|
+
.stuic-user-avatar-menu-header-email,
|
|
73
|
+
.stuic-user-avatar-menu-header-roles {
|
|
74
|
+
display: block;
|
|
75
|
+
width: 100%;
|
|
76
|
+
min-width: 0;
|
|
77
|
+
text-align: center;
|
|
78
|
+
overflow: hidden;
|
|
79
|
+
text-overflow: ellipsis;
|
|
80
|
+
white-space: nowrap;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.stuic-user-avatar-menu-header-email {
|
|
84
|
+
font-size: var(--stuic-user-avatar-menu-header-email-font-size, inherit);
|
|
85
|
+
color: var(--stuic-user-avatar-menu-header-email-color, inherit);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.stuic-user-avatar-menu-header-roles {
|
|
89
|
+
font-size: var(--stuic-user-avatar-menu-header-roles-font-size, 0.75rem);
|
|
90
|
+
color: var(
|
|
91
|
+
--stuic-user-avatar-menu-header-roles-color,
|
|
92
|
+
var(--stuic-color-muted-foreground, currentColor)
|
|
93
|
+
);
|
|
94
|
+
opacity: var(--stuic-user-avatar-menu-header-roles-opacity, 0.7);
|
|
95
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as UserAvatarMenu, type Props as UserAvatarMenuProps, type UserAvatarMenuIdentity, type UserAvatarMenuActions, type UserAvatarMenuLabels, type UserAvatarMenuColorScheme, } from "./UserAvatarMenu.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as UserAvatarMenu, } from "./UserAvatarMenu.svelte";
|
package/dist/icons/index.d.ts
CHANGED
|
@@ -40,11 +40,14 @@ export { iconLucideGripHorizontal as iconGripHorizontal } from "@marianmeres/ico
|
|
|
40
40
|
export { iconLucideGripVertical as iconGripVertical } from "@marianmeres/icons-fns/lucide/iconLucideGripVertical.js";
|
|
41
41
|
export { iconLucideLanguages as iconLanguages } from "@marianmeres/icons-fns/lucide/iconLucideLanguages.js";
|
|
42
42
|
export { iconLucideList as iconList } from "@marianmeres/icons-fns/lucide/iconLucideList.js";
|
|
43
|
+
export { iconLucideLogOut as iconLogOut } from "@marianmeres/icons-fns/lucide/iconLucideLogOut.js";
|
|
43
44
|
export { iconLucideMenu as iconMenu } from "@marianmeres/icons-fns/lucide/iconLucideMenu.js";
|
|
45
|
+
export { iconLucideMoon as iconMoon } from "@marianmeres/icons-fns/lucide/iconLucideMoon.js";
|
|
44
46
|
export { iconLucideSearch as iconSearch } from "@marianmeres/icons-fns/lucide/iconLucideSearch.js";
|
|
45
47
|
export { iconLucideSlidersHorizontal as iconSlidersHorizontal } from "@marianmeres/icons-fns/lucide/iconLucideSlidersHorizontal.js";
|
|
46
48
|
export { iconLucideSettings as iconSettings } from "@marianmeres/icons-fns/lucide/iconLucideSettings.js";
|
|
47
49
|
export { iconLucideSquare as iconSquare } from "@marianmeres/icons-fns/lucide/iconLucideSquare.js";
|
|
50
|
+
export { iconLucideSun as iconSun } from "@marianmeres/icons-fns/lucide/iconLucideSun.js";
|
|
48
51
|
export { iconLucideUser as iconUser } from "@marianmeres/icons-fns/lucide/iconLucideUser.js";
|
|
49
52
|
export { iconLucideX as iconX } from "@marianmeres/icons-fns/lucide/iconLucideX.js";
|
|
50
53
|
export { iconLucidePencil as iconPencil } from "@marianmeres/icons-fns/lucide/iconLucidePencil.js";
|
package/dist/icons/index.js
CHANGED
|
@@ -45,11 +45,14 @@ export { iconLucideGripHorizontal as iconGripHorizontal } from "@marianmeres/ico
|
|
|
45
45
|
export { iconLucideGripVertical as iconGripVertical } from "@marianmeres/icons-fns/lucide/iconLucideGripVertical.js";
|
|
46
46
|
export { iconLucideLanguages as iconLanguages } from "@marianmeres/icons-fns/lucide/iconLucideLanguages.js";
|
|
47
47
|
export { iconLucideList as iconList } from "@marianmeres/icons-fns/lucide/iconLucideList.js";
|
|
48
|
+
export { iconLucideLogOut as iconLogOut } from "@marianmeres/icons-fns/lucide/iconLucideLogOut.js";
|
|
48
49
|
export { iconLucideMenu as iconMenu } from "@marianmeres/icons-fns/lucide/iconLucideMenu.js";
|
|
50
|
+
export { iconLucideMoon as iconMoon } from "@marianmeres/icons-fns/lucide/iconLucideMoon.js";
|
|
49
51
|
export { iconLucideSearch as iconSearch } from "@marianmeres/icons-fns/lucide/iconLucideSearch.js";
|
|
50
52
|
export { iconLucideSlidersHorizontal as iconSlidersHorizontal } from "@marianmeres/icons-fns/lucide/iconLucideSlidersHorizontal.js";
|
|
51
53
|
export { iconLucideSettings as iconSettings } from "@marianmeres/icons-fns/lucide/iconLucideSettings.js";
|
|
52
54
|
export { iconLucideSquare as iconSquare } from "@marianmeres/icons-fns/lucide/iconLucideSquare.js";
|
|
55
|
+
export { iconLucideSun as iconSun } from "@marianmeres/icons-fns/lucide/iconLucideSun.js";
|
|
53
56
|
export { iconLucideUser as iconUser } from "@marianmeres/icons-fns/lucide/iconLucideUser.js";
|
|
54
57
|
export { iconLucideX as iconX } from "@marianmeres/icons-fns/lucide/iconLucideX.js";
|
|
55
58
|
export { iconLucidePencil as iconPencil } from "@marianmeres/icons-fns/lucide/iconLucidePencil.js";
|
package/dist/index.css
CHANGED
|
@@ -95,6 +95,7 @@ In practice:
|
|
|
95
95
|
@import "./components/ThemePreview/index.css";
|
|
96
96
|
@import "./components/Tree/index.css";
|
|
97
97
|
@import "./components/TwCheck/index.css";
|
|
98
|
+
@import "./components/UserAvatarMenu/index.css";
|
|
98
99
|
@import "./components/WithSidePanel/index.css";
|
|
99
100
|
@import "./components/X/index.css";
|
|
100
101
|
|
package/dist/index.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ export * from "./components/ThemePreview/index.js";
|
|
|
74
74
|
export * from "./components/Tree/index.js";
|
|
75
75
|
export * from "./components/TwCheck/index.js";
|
|
76
76
|
export * from "./components/TypeaheadInput/index.js";
|
|
77
|
+
export * from "./components/UserAvatarMenu/index.js";
|
|
77
78
|
export * from "./components/WithSidePanel/index.js";
|
|
78
79
|
export * from "./components/X/index.js";
|
|
79
80
|
export * from "./utils/index.js";
|
package/dist/index.js
CHANGED
|
@@ -75,6 +75,7 @@ export * from "./components/ThemePreview/index.js";
|
|
|
75
75
|
export * from "./components/Tree/index.js";
|
|
76
76
|
export * from "./components/TwCheck/index.js";
|
|
77
77
|
export * from "./components/TypeaheadInput/index.js";
|
|
78
|
+
export * from "./components/UserAvatarMenu/index.js";
|
|
78
79
|
export * from "./components/WithSidePanel/index.js";
|
|
79
80
|
export * from "./components/X/index.js";
|
|
80
81
|
// utils
|
package/dist/utils/index.d.ts
CHANGED
package/dist/utils/index.js
CHANGED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ValidationResult } from "../actions/validate.svelte.js";
|
|
2
|
+
/**
|
|
3
|
+
* Minimal shape any STUIC field component implements once it exposes the
|
|
4
|
+
* imperative validate API. Form-level helpers and consumer aggregators consume
|
|
5
|
+
* this — they don't care what specific field type they're dealing with.
|
|
6
|
+
*
|
|
7
|
+
* Every STUIC `Field*` component (FieldInput, FieldPhoneNumber, FieldCountry,
|
|
8
|
+
* FieldSelect, FieldCheckbox, FieldTextarea, FieldFile, FieldObject,
|
|
9
|
+
* FieldAssets, FieldInputLocalized, FieldKeyValues, FieldLikeButton,
|
|
10
|
+
* FieldRadios, FieldSwitch) satisfies this interface via `export function`.
|
|
11
|
+
*/
|
|
12
|
+
export interface ValidatableField {
|
|
13
|
+
/** Run the validator now. Renders the inline error if invalid. */
|
|
14
|
+
validate(): ValidationResult | undefined;
|
|
15
|
+
/** Reset the inline error and clear `el.setCustomValidity`. */
|
|
16
|
+
clearValidation?(): void;
|
|
17
|
+
/** Current validation state, or undefined if validator has never run. */
|
|
18
|
+
getValidation?(): ValidationResult | undefined;
|
|
19
|
+
/** Focus the visible interactive element. */
|
|
20
|
+
focus?(): void;
|
|
21
|
+
/** Scroll the field into view. Defaults to smooth + center. */
|
|
22
|
+
scrollIntoView?(opts?: ScrollIntoViewOptions): void;
|
|
23
|
+
}
|
|
24
|
+
type FieldArg = ValidatableField | undefined | null;
|
|
25
|
+
/**
|
|
26
|
+
* Run `validate()` on every provided field. Returns `true` if all are valid.
|
|
27
|
+
*
|
|
28
|
+
* `undefined` / `null` entries are skipped so callers can spread conditional
|
|
29
|
+
* refs without filtering first.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```svelte
|
|
33
|
+
* <script>
|
|
34
|
+
* let nameField = $state();
|
|
35
|
+
* let emailField = $state();
|
|
36
|
+
*
|
|
37
|
+
* function handleSubmit() {
|
|
38
|
+
* if (!validateAllFields([nameField, emailField])) {
|
|
39
|
+
* scrollToFirstInvalidField([nameField, emailField]);
|
|
40
|
+
* return;
|
|
41
|
+
* }
|
|
42
|
+
* // ...submit
|
|
43
|
+
* }
|
|
44
|
+
* </script>
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export declare function validateAllFields(fields: FieldArg[]): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Return the first field whose current validation state is invalid, or
|
|
50
|
+
* `undefined` if all are valid (or never validated).
|
|
51
|
+
*
|
|
52
|
+
* Reads cached state via `getValidation()` — call `validateAllFields()` first
|
|
53
|
+
* if you need fresh results.
|
|
54
|
+
*/
|
|
55
|
+
export declare function findFirstInvalidField(fields: FieldArg[]): ValidatableField | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Scroll the first invalid field into view and (by default) focus it.
|
|
58
|
+
* Returns `true` if a field was scrolled, `false` if all were valid.
|
|
59
|
+
*
|
|
60
|
+
* Call **after** `validateAllFields()` — this reads cached validation state.
|
|
61
|
+
*
|
|
62
|
+
* @param fields - Field refs (in display order — first match wins)
|
|
63
|
+
* @param opts.focus - Whether to also call `focus()` on the field. Default true.
|
|
64
|
+
* @param opts.behavior - ScrollIntoView behavior. Default `"smooth"`.
|
|
65
|
+
* @param opts.block - ScrollIntoView block alignment. Default `"center"`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function scrollToFirstInvalidField(fields: FieldArg[], opts?: {
|
|
68
|
+
focus?: boolean;
|
|
69
|
+
behavior?: ScrollBehavior;
|
|
70
|
+
block?: ScrollLogicalPosition;
|
|
71
|
+
}): boolean;
|
|
72
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `validate()` on every provided field. Returns `true` if all are valid.
|
|
3
|
+
*
|
|
4
|
+
* `undefined` / `null` entries are skipped so callers can spread conditional
|
|
5
|
+
* refs without filtering first.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```svelte
|
|
9
|
+
* <script>
|
|
10
|
+
* let nameField = $state();
|
|
11
|
+
* let emailField = $state();
|
|
12
|
+
*
|
|
13
|
+
* function handleSubmit() {
|
|
14
|
+
* if (!validateAllFields([nameField, emailField])) {
|
|
15
|
+
* scrollToFirstInvalidField([nameField, emailField]);
|
|
16
|
+
* return;
|
|
17
|
+
* }
|
|
18
|
+
* // ...submit
|
|
19
|
+
* }
|
|
20
|
+
* </script>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function validateAllFields(fields) {
|
|
24
|
+
let allValid = true;
|
|
25
|
+
for (const f of fields) {
|
|
26
|
+
if (!f)
|
|
27
|
+
continue;
|
|
28
|
+
const res = f.validate();
|
|
29
|
+
if (res && !res.valid)
|
|
30
|
+
allValid = false;
|
|
31
|
+
}
|
|
32
|
+
return allValid;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Return the first field whose current validation state is invalid, or
|
|
36
|
+
* `undefined` if all are valid (or never validated).
|
|
37
|
+
*
|
|
38
|
+
* Reads cached state via `getValidation()` — call `validateAllFields()` first
|
|
39
|
+
* if you need fresh results.
|
|
40
|
+
*/
|
|
41
|
+
export function findFirstInvalidField(fields) {
|
|
42
|
+
for (const f of fields) {
|
|
43
|
+
if (!f)
|
|
44
|
+
continue;
|
|
45
|
+
const v = f.getValidation?.();
|
|
46
|
+
if (v && !v.valid)
|
|
47
|
+
return f;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Scroll the first invalid field into view and (by default) focus it.
|
|
53
|
+
* Returns `true` if a field was scrolled, `false` if all were valid.
|
|
54
|
+
*
|
|
55
|
+
* Call **after** `validateAllFields()` — this reads cached validation state.
|
|
56
|
+
*
|
|
57
|
+
* @param fields - Field refs (in display order — first match wins)
|
|
58
|
+
* @param opts.focus - Whether to also call `focus()` on the field. Default true.
|
|
59
|
+
* @param opts.behavior - ScrollIntoView behavior. Default `"smooth"`.
|
|
60
|
+
* @param opts.block - ScrollIntoView block alignment. Default `"center"`.
|
|
61
|
+
*/
|
|
62
|
+
export function scrollToFirstInvalidField(fields, opts) {
|
|
63
|
+
const field = findFirstInvalidField(fields);
|
|
64
|
+
if (!field)
|
|
65
|
+
return false;
|
|
66
|
+
field.scrollIntoView?.({
|
|
67
|
+
behavior: opts?.behavior ?? "smooth",
|
|
68
|
+
block: opts?.block ?? "center",
|
|
69
|
+
});
|
|
70
|
+
if (opts?.focus !== false)
|
|
71
|
+
field.focus?.();
|
|
72
|
+
return true;
|
|
73
|
+
}
|
package/docs/domains/actions.md
CHANGED
|
@@ -64,6 +64,80 @@ Actions using `$effect()` accept a function returning options:
|
|
|
64
64
|
/>
|
|
65
65
|
```
|
|
66
66
|
|
|
67
|
+
### Imperative validate() trigger
|
|
68
|
+
|
|
69
|
+
The `validate` action only runs `_doValidate` in response to user-driven DOM
|
|
70
|
+
events (`change`, first `blur`). On a pristine, never-touched field the inline
|
|
71
|
+
validation message never mounts — which silently breaks any flow that
|
|
72
|
+
pre-populates errors via `customValidator` on a fresh form.
|
|
73
|
+
|
|
74
|
+
Pass a `setDoValidate` callback to capture a reference to the action's internal
|
|
75
|
+
validator function and trigger it imperatively (e.g., from a submit handler):
|
|
76
|
+
|
|
77
|
+
```svelte
|
|
78
|
+
<script>
|
|
79
|
+
let doValidate: (() => void) | undefined = $state();
|
|
80
|
+
|
|
81
|
+
function handleSubmit() {
|
|
82
|
+
// Force every "sleeping" field's validator to run, rendering inline
|
|
83
|
+
// errors even on fields the user never touched.
|
|
84
|
+
doValidate?.();
|
|
85
|
+
// ...check validationResult.valid here, or use validateAllFields().
|
|
86
|
+
}
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<input
|
|
90
|
+
required
|
|
91
|
+
use:validate={() => ({
|
|
92
|
+
enabled: true,
|
|
93
|
+
setValidationResult: (res) => (validationResult = res),
|
|
94
|
+
setDoValidate: (fn) => (doValidate = fn),
|
|
95
|
+
})}
|
|
96
|
+
/>
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
> **You generally don't write this by hand.** Every STUIC `Field*` component
|
|
100
|
+
> already wires `setDoValidate` internally and exposes the result as
|
|
101
|
+
> `export function validate()` on its component reference. See the
|
|
102
|
+
> [Components domain doc](./components.md#imperative-validate-api) for the
|
|
103
|
+
> per-field method list and the [validate-fields utility](./utils.md) for
|
|
104
|
+
> `validateAllFields()` / `scrollToFirstInvalidField()` aggregators.
|
|
105
|
+
|
|
106
|
+
### Pristine forms and external errors
|
|
107
|
+
|
|
108
|
+
A common trap: setting an `errors` prop on a brand-new form and expecting the
|
|
109
|
+
inline messages to render. They won't — the `validate` action's
|
|
110
|
+
`customValidator` is only invoked on user events. Two fixes:
|
|
111
|
+
|
|
112
|
+
1. **Wrap the form in `<form use:onSubmitValidityCheck>`** and listen for
|
|
113
|
+
`submit_valid` / `submit_invalid` (works for native submit flows).
|
|
114
|
+
2. **Call the field component's imperative `validate()` from your submit
|
|
115
|
+
handler** (works for any flow — wizards, multi-step, custom CTAs).
|
|
116
|
+
|
|
117
|
+
### Hidden inputs and `required`
|
|
118
|
+
|
|
119
|
+
Per the HTML spec, `<input type="hidden">` is *barred from constraint
|
|
120
|
+
validation* — `validity.valueMissing` stays `false` regardless of the
|
|
121
|
+
`required` attribute, and native browser submit blocking is skipped. Several
|
|
122
|
+
STUIC field components (`FieldPhoneNumber`, `FieldCountry`, `FieldObject`,
|
|
123
|
+
`FieldAssets`, `FieldInputLocalized`, `FieldKeyValues`, `FieldLikeButton`)
|
|
124
|
+
use a hidden input to participate in `FormData`, so they each enforce
|
|
125
|
+
`required` themselves inside their `customValidator`:
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
customValidator(val, ctx, el) {
|
|
129
|
+
if (required && (val == null || val === "")) {
|
|
130
|
+
return "This field requires attention. Please review and try again.";
|
|
131
|
+
}
|
|
132
|
+
return userValidator?.(val, ctx, el) || "";
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
This means `<FieldCountry required />` and `<FieldPhoneNumber required />`
|
|
137
|
+
correctly fail validation when empty — both through the imperative
|
|
138
|
+
`validate()` path and via `use:onSubmitValidityCheck`. Without this wrap
|
|
139
|
+
they'd silently accept empty values.
|
|
140
|
+
|
|
67
141
|
### File Dropzone
|
|
68
142
|
|
|
69
143
|
```svelte
|