@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.
Files changed (64) hide show
  1. package/API.md +41 -0
  2. package/dist/actions/validate.svelte.d.ts +24 -4
  3. package/dist/actions/validate.svelte.js +18 -5
  4. package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +21 -0
  5. package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte.d.ts +4 -1
  6. package/dist/components/Input/FieldAssets.svelte +48 -3
  7. package/dist/components/Input/FieldAssets.svelte.d.ts +8 -2
  8. package/dist/components/Input/FieldCheckbox.svelte +34 -3
  9. package/dist/components/Input/FieldCheckbox.svelte.d.ts +8 -1
  10. package/dist/components/Input/FieldCountry.svelte +64 -7
  11. package/dist/components/Input/FieldCountry.svelte.d.ts +8 -1
  12. package/dist/components/Input/FieldFile.svelte +34 -3
  13. package/dist/components/Input/FieldFile.svelte.d.ts +8 -1
  14. package/dist/components/Input/FieldInput.svelte +43 -3
  15. package/dist/components/Input/FieldInput.svelte.d.ts +8 -1
  16. package/dist/components/Input/FieldInputLocalized.svelte +41 -2
  17. package/dist/components/Input/FieldInputLocalized.svelte.d.ts +8 -2
  18. package/dist/components/Input/FieldKeyValues.svelte +37 -2
  19. package/dist/components/Input/FieldKeyValues.svelte.d.ts +8 -2
  20. package/dist/components/Input/FieldLikeButton.svelte +41 -4
  21. package/dist/components/Input/FieldLikeButton.svelte.d.ts +8 -1
  22. package/dist/components/Input/FieldObject.svelte +64 -6
  23. package/dist/components/Input/FieldObject.svelte.d.ts +8 -2
  24. package/dist/components/Input/FieldOptions.svelte +36 -3
  25. package/dist/components/Input/FieldOptions.svelte.d.ts +8 -2
  26. package/dist/components/Input/FieldPhoneNumber.svelte +51 -6
  27. package/dist/components/Input/FieldPhoneNumber.svelte.d.ts +8 -1
  28. package/dist/components/Input/FieldRadios.svelte +36 -2
  29. package/dist/components/Input/FieldRadios.svelte.d.ts +8 -1
  30. package/dist/components/Input/FieldSelect.svelte +34 -3
  31. package/dist/components/Input/FieldSelect.svelte.d.ts +8 -1
  32. package/dist/components/Input/FieldSwitch.svelte +41 -2
  33. package/dist/components/Input/FieldSwitch.svelte.d.ts +8 -1
  34. package/dist/components/Input/FieldTextarea.svelte +34 -3
  35. package/dist/components/Input/FieldTextarea.svelte.d.ts +8 -1
  36. package/dist/components/Input/_internal/FieldRadioInternal.svelte +34 -3
  37. package/dist/components/Input/_internal/FieldRadioInternal.svelte.d.ts +7 -1
  38. package/dist/components/LoginForm/LoginForm.svelte +35 -0
  39. package/dist/components/LoginForm/LoginForm.svelte.d.ts +5 -1
  40. package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte +40 -0
  41. package/dist/components/LoginOrRegisterForm/LoginOrRegisterForm.svelte.d.ts +5 -1
  42. package/dist/components/RegisterForm/RegisterForm.svelte +46 -2
  43. package/dist/components/RegisterForm/RegisterForm.svelte.d.ts +5 -1
  44. package/dist/components/Switch/Switch.svelte +42 -4
  45. package/dist/components/Switch/Switch.svelte.d.ts +7 -1
  46. package/dist/components/UserAvatarMenu/README.md +188 -0
  47. package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte +416 -0
  48. package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte.d.ts +143 -0
  49. package/dist/components/UserAvatarMenu/index.css +95 -0
  50. package/dist/components/UserAvatarMenu/index.d.ts +1 -0
  51. package/dist/components/UserAvatarMenu/index.js +1 -0
  52. package/dist/icons/index.d.ts +3 -0
  53. package/dist/icons/index.js +3 -0
  54. package/dist/index.css +1 -0
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.js +1 -0
  57. package/dist/utils/index.d.ts +1 -0
  58. package/dist/utils/index.js +1 -0
  59. package/dist/utils/validate-fields.d.ts +72 -0
  60. package/dist/utils/validate-fields.js +73 -0
  61. package/docs/domains/actions.md +74 -0
  62. package/docs/domains/components.md +190 -0
  63. package/docs/domains/utils.md +38 -0
  64. package/package.json +1 -1
@@ -72,6 +72,7 @@
72
72
  | Component | Purpose |
73
73
  | --------------- | ------------------------------------------------------------------- |
74
74
  | Avatar | User avatars with fallback |
75
+ | UserAvatarMenu | Avatar trigger + dropdown menu with header tile, color-scheme toggle, authed/unauthed states |
75
76
  | Pill | Inline rounded badge/tag/chip (intent + variant + size, dismissible, dot, polymorphic span/a/button) |
76
77
  | KbdShortcut | Keyboard shortcut hints |
77
78
  | Carousel | Image/content slider with snap, keyboard nav, wheel scroll, arrows |
@@ -102,6 +103,137 @@
102
103
 
103
104
  ---
104
105
 
106
+ ## Imperative validate API
107
+
108
+ Every `Field*` component that uses the `validate` action exposes a small
109
+ imperative API on its component reference, accessed via `bind:this`. Form
110
+ components (`LoginForm`, `RegisterForm`, `LoginOrRegisterForm`,
111
+ `EmailVerifyForm`) compose those into form-level `validate()` /
112
+ `scrollToFirstError()`.
113
+
114
+ ### Why
115
+
116
+ The `validate` action only runs on user-driven DOM events (`change`, first
117
+ `blur`). On a pristine, never-touched field the inline `validation-box` never
118
+ mounts — which silently breaks any flow that pre-populates errors via
119
+ `customValidator` on a fresh form. The imperative API lets a submit handler
120
+ force every "sleeping" field's validator to run, rendering inline errors all
121
+ at once — no synthetic `change` events, no DOM lookups, no id-format coupling.
122
+
123
+ ### The `validate` prop — defaults & opt-out
124
+
125
+ All field components that use the `validate` action treat the prop with one
126
+ consistent rule:
127
+
128
+ | `validate` value | Action |
129
+ | ------------------------- | ---------------------------------------------------------- |
130
+ | (omitted) / `undefined` | **Enabled** with default options (the common case) |
131
+ | `true` | Enabled with default options (explicit; same as omitting) |
132
+ | `false` | **Disabled** — no validation, `validate()` becomes a no-op |
133
+ | `{ customValidator, ... }`| Enabled, with `ValidateOptions` overrides applied |
134
+
135
+ So `<FieldInput required />` works as expected — required is enforced, and a
136
+ failed `validate()` (imperative or event-driven) renders the inline error.
137
+ Use `validate={false}` to bypass stuic's validation entirely.
138
+
139
+ > **Why default-on?** Hidden-input field components (`FieldPhoneNumber`,
140
+ > `FieldCountry`, `FieldObject`, `FieldAssets`, `FieldInputLocalized`,
141
+ > `FieldKeyValues`, `FieldLikeButton`) *must* be default-on because hidden
142
+ > inputs are excluded from native browser constraint validation — without the
143
+ > stuic action enforcing `required` in a `customValidator`, the attribute is a
144
+ > silent no-op. Plain-input field components were harmonized to the same
145
+ > default to keep the rule uniform: "`required` means required."
146
+
147
+ ### Per-field methods
148
+
149
+ Available on `FieldInput`, `FieldTextarea`, `FieldCheckbox`, `FieldSelect`,
150
+ `FieldFile`, `FieldObject`, `FieldAssets`, `FieldInputLocalized`,
151
+ `FieldKeyValues`, `FieldPhoneNumber`, `FieldCountry`, `FieldLikeButton`,
152
+ `FieldRadios`, `FieldSwitch`, `FieldOptions`, and `Switch`:
153
+
154
+ | Method | Returns | Purpose |
155
+ | --------------------------------------------- | -------------------------------- | ------------------------------------------------------------------ |
156
+ | `validate()` | `ValidationResult \| undefined` | Run the validator now. Renders the inline message if invalid. |
157
+ | `clearValidation()` | `void` | Clear the inline message and `setCustomValidity`. |
158
+ | `getValidation()` | `ValidationResult \| undefined` | Read cached state (no re-run). |
159
+ | `focus()` | `void` | Focus the visible interactive element. |
160
+ | `scrollIntoView(opts?)` | `void` | Scroll the field into view. Defaults to `smooth` + `center`. |
161
+
162
+ ```svelte
163
+ <script>
164
+ let nameField = $state<FieldInput>();
165
+
166
+ function checkName() {
167
+ const result = nameField?.validate();
168
+ if (result && !result.valid) {
169
+ console.warn("Name invalid:", result.message);
170
+ }
171
+ }
172
+ </script>
173
+
174
+ <FieldInput bind:this={nameField} bind:value={name} label="Name" required />
175
+ <Button onclick={checkName}>Check now</Button>
176
+ ```
177
+
178
+ ### Form-level methods
179
+
180
+ `LoginForm`, `RegisterForm`, `LoginOrRegisterForm`, and `EmailVerifyForm` each
181
+ expose:
182
+
183
+ | Method | Returns | Purpose |
184
+ | ----------------------------------- | --------- | -------------------------------------------------------------------- |
185
+ | `validate()` | `boolean` | Run every inner field's validator. `true` if all valid. |
186
+ | `scrollToFirstError(opts?)` | `boolean` | Scroll the first invalid field into view + focus it. Call after `validate()`. |
187
+
188
+ ```svelte
189
+ <script>
190
+ let loginForm = $state<LoginForm>();
191
+
192
+ async function handleCustomSubmit() {
193
+ if (!loginForm?.validate()) {
194
+ loginForm?.scrollToFirstError();
195
+ return;
196
+ }
197
+ await api.login(/* ... */);
198
+ }
199
+ </script>
200
+
201
+ <LoginForm bind:this={loginForm} onSubmit={handleCustomSubmit} />
202
+ <Button onclick={handleCustomSubmit}>Submit from outside</Button>
203
+ ```
204
+
205
+ ### Pristine-form errors pattern
206
+
207
+ The trap: an external `errors` prop wired into each field's `customValidator`
208
+ won't render until the user touches the field. Pair the existing prop with
209
+ an imperative `validate()` call from your submit handler:
210
+
211
+ ```svelte
212
+ <FieldInput
213
+ bind:this={nameField}
214
+ bind:value={address.name}
215
+ required
216
+ validate={{
217
+ customValidator() {
218
+ return externalErrors.find((e) => e.field === "name")?.message || "";
219
+ },
220
+ }}
221
+ />
222
+
223
+ <!-- in your submit handler -->
224
+ <script>
225
+ function handleContinue() {
226
+ if (!validateAllFields([nameField, /* ... */])) return;
227
+ // ...submit
228
+ }
229
+ </script>
230
+ ```
231
+
232
+ For aggregation across many fields, see
233
+ [validate-fields utilities](./utils.md#field-validation-aggregators).
234
+
235
+ ---
236
+
105
237
  ## LoginForm
106
238
 
107
239
  Standalone login form with optional modal variant. Supports social logins, forgot password, remember me, client+server validation, i18n, and notifications integration.
@@ -770,6 +902,63 @@ Prefix: `--stuic-tree-*`
770
902
 
771
903
  ---
772
904
 
905
+ ## UserAvatarMenu
906
+
907
+ Thin opinionated wrapper around `Avatar` + `DropdownMenu` for the common "user avatar in the header opens a small menu" pattern. Renders in both authenticated (header tile, View profile, color-scheme toggle, Logout) and unauthenticated (Login, Register) states from the same trigger position. Color scheme is the one built-in side effect; everything else is consumer callbacks.
908
+
909
+ ### Exports
910
+
911
+ | Export | Kind | Description |
912
+ | ---------------------------- | --------- | ------------------------------------------ |
913
+ | `UserAvatarMenu` | component | Main component |
914
+ | `UserAvatarMenuProps` | type | Props type |
915
+ | `UserAvatarMenuIdentity` | type | `{ email, name?, src?, roles? }` |
916
+ | `UserAvatarMenuActions` | type | `{ onProfile?, onSettings?, onLogout?, onLoginOrRegister?, onLogin?, onRegister? }` |
917
+ | `UserAvatarMenuLabels` | type | Label overrides (all optional, English defaults) |
918
+ | `UserAvatarMenuColorScheme` | type | `boolean \| { enabled?, onToggle?, isDark? }` |
919
+
920
+ ### Key Props
921
+
922
+ | Prop | Type | Default | Description |
923
+ | ---------------- | --------------------------------------------- | ------- | -------------------------------------------------------------------------------------- |
924
+ | `identity` | `UserAvatarMenuIdentity \| null` | `null` | Current user. `null` / `undefined` → unauthenticated mode. |
925
+ | `actions` | `UserAvatarMenuActions` | `{}` | Handlers; missing → corresponding item hidden. |
926
+ | `labels` | `UserAvatarMenuLabels` | English | Translated strings for built-in items. |
927
+ | `colorScheme` | `boolean \| { enabled?, onToggle?, isDark? }` | `true` | Built-in dark/light toggle. `false` to disable. |
928
+ | `showHeaderTile` | `boolean` | `true` | Render avatar+email tile inside the dropdown (auth only). |
929
+ | `showRoles` | `boolean` | `false` | Render `identity.roles` under the email in the header tile. |
930
+ | `extraItems` | `DropdownMenuItem[]` | — | Appended to the standard item set. |
931
+ | `items` | `DropdownMenuItem[]` | — | Full override of items (trigger + shell still render). |
932
+ | `avatar` | `Partial<AvatarProps>` | — | Forwarded to the default trigger Avatar (and header-tile Avatar). |
933
+ | `position` | `DropdownMenuPosition` | — | Forwarded to `DropdownMenu`. |
934
+ | `classDropdown` | `string` | — | Forwarded. |
935
+ | `classTrigger` | `string` | — | Class merged onto the default trigger `<button>`. |
936
+ | `trigger` | `Snippet<[{ isOpen, toggle, triggerProps }]>` | — | Custom trigger snippet (replaces default Avatar trigger). |
937
+ | `headerTile` | `Snippet<[{ identity }]>` | — | Custom header-tile snippet (replaces default avatar+email tile). |
938
+ | `isOpen` | `boolean` | `false` | Bindable open state. |
939
+
940
+ ### Default item order
941
+
942
+ **Authenticated:** header tile → View profile → Settings → Color scheme → Divider → Logout → `extraItems`. Each rendered only if its precondition (handler present, etc.) holds.
943
+
944
+ **Unauthenticated:** Login or register → Login → Register → Color scheme → `extraItems`. Each rendered only if the corresponding handler is provided. The three are independent — pass any combination. The combined `onLoginOrRegister` (typically opening a `LoginOrRegisterFormModal`) is the most common variant. No header tile.
945
+
946
+ ### Opinionated decisions
947
+
948
+ - Color scheme is the only built-in side effect (`ColorScheme.toggle()` + re-read on select). Opt out with `colorScheme={false}`.
949
+ - English label defaults; consumers pass `labels` for i18n. No i18n library imported.
950
+ - No auth state ownership, no router, no modal triggering — all consumer callbacks.
951
+
952
+ ### CSS Tokens
953
+
954
+ Prefix: `--stuic-user-avatar-menu-*`
955
+
956
+ `dropdown-width` (16rem), `trigger-radius`, `trigger-opacity-hover`, `trigger-outline-color`, `transition`, `header-gap`, `header-padding`, `header-margin-bottom`, `header-bg`, `header-color`, `header-radius`, `header-email-font-size`, `header-email-color`, `header-roles-font-size`, `header-roles-color`, `header-roles-opacity`
957
+
958
+ The dropdown has a fixed `width` so every instance opens identically — short and long content alike. Email, name, and roles truncate with `text-overflow: ellipsis`. Header tile defaults: `--stuic-color-muted` bg + `--stuic-color-muted-foreground` text.
959
+
960
+ ---
961
+
773
962
  ## Key Files
774
963
 
775
964
  | File | Purpose |
@@ -785,4 +974,5 @@ Prefix: `--stuic-tree-*`
785
974
  | src/lib/components/Checkout/ | E-commerce checkout flow (14 exported sub-components) |
786
975
  | src/lib/components/Card/ | Card with image/title/footer variants |
787
976
  | src/lib/components/Tree/ | Hierarchical tree with drag-and-drop |
977
+ | src/lib/components/UserAvatarMenu/ | Avatar trigger + dropdown for user menu |
788
978
  | src/lib/index.ts | All component exports |
@@ -173,6 +173,43 @@ storage.get("user"); // { name: 'John' }
173
173
  | `generateCssTokens` | Convert token schema to CSS |
174
174
  | `toCssString` | Format tokens as CSS string |
175
175
 
176
+ ---
177
+
178
+ ## Field Validation Aggregators
179
+
180
+ Helpers for orchestrating `validate()` across multiple `Field*` components.
181
+ Pair with the per-field imperative API documented in the
182
+ [Components domain](./components.md#imperative-validate-api).
183
+
184
+ | Util | Purpose |
185
+ | ----------------------------- | ------------------------------------------------------------ |
186
+ | `validateAllFields` | Run `validate()` on every provided field ref. Returns `true` if all valid. |
187
+ | `findFirstInvalidField` | Return the first ref whose cached `getValidation()` is invalid. |
188
+ | `scrollToFirstInvalidField` | Scroll the first invalid field into view + focus (call after `validateAllFields`). |
189
+ | `ValidatableField` (interface)| Minimal shape every STUIC `Field*` satisfies — your own components can too. |
190
+
191
+ ```ts
192
+ import {
193
+ scrollToFirstInvalidField,
194
+ validateAllFields,
195
+ } from "@marianmeres/stuic";
196
+
197
+ let nameField = $state<FieldInput>();
198
+ let emailField = $state<FieldInput>();
199
+ let countryField = $state<FieldCountry>();
200
+
201
+ function handleContinue() {
202
+ const allValid = validateAllFields([nameField, emailField, countryField]);
203
+ if (!allValid) {
204
+ scrollToFirstInvalidField([nameField, emailField, countryField]);
205
+ return;
206
+ }
207
+ // ...submit
208
+ }
209
+ ```
210
+
211
+ `undefined` / `null` entries are skipped so callers can spread conditional refs
212
+ without filtering first.
176
213
 
177
214
  ---
178
215
 
@@ -184,4 +221,5 @@ storage.get("user"); // { name: 'John' }
184
221
  | src/lib/utils/tw-merge.ts | Critical for class merging |
185
222
  | src/lib/utils/persistent-state.svelte.ts | Reactive storage pattern (runes-based) |
186
223
  | src/lib/utils/storage-abstraction.ts | Non-reactive storage (localStorage, etc.) |
224
+ | src/lib/utils/validate-fields.ts | Form-level validation aggregators |
187
225
  | src/lib/utils/design-tokens.ts | Re-exports from `@marianmeres/design-tokens` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/stuic",
3
- "version": "3.86.0",
3
+ "version": "3.88.0",
4
4
  "scripts": {
5
5
  "dev": "vite dev",
6
6
  "build": "vite build && pnpm run prepack",