@marianmeres/stuic 3.115.0 → 3.117.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 +297 -304
- package/dist/actions/dim-behind/index.css +4 -1
- package/dist/actions/focus-trap.fixture.svelte +16 -0
- package/dist/actions/focus-trap.fixture.svelte.d.ts +7 -0
- package/dist/actions/focus-trap.js +3 -1
- package/dist/components/Accordion/README.md +17 -17
- package/dist/components/Accordion/index.css +4 -2
- package/dist/components/AssetsPreview/README.md +7 -7
- package/dist/components/AssetsPreview/_internal/assets-preview-types.d.ts +1 -2
- package/dist/components/AssetsPreview/_internal/assets-preview-utils.d.ts +1 -1
- package/dist/components/AssetsPreview/_internal/assets-preview-utils.js +9 -3
- package/dist/components/Avatar/Avatar.svelte +1 -3
- package/dist/components/Avatar/README.md +33 -27
- package/dist/components/Book/Book.svelte +6 -1
- package/dist/components/Book/README.md +22 -20
- package/dist/components/Book/index.css +4 -2
- package/dist/components/Button/README.md +17 -17
- package/dist/components/Card/Card.svelte +25 -8
- package/dist/components/Card/README.md +52 -56
- package/dist/components/Card/index.css +2 -1
- package/dist/components/Carousel/Carousel.svelte +1 -3
- package/dist/components/Carousel/README.md +28 -28
- package/dist/components/Cart/Cart.svelte +2 -1
- package/dist/components/Cart/README.md +25 -25
- package/dist/components/Checkout/CheckoutGuestOrLoginForm.svelte +8 -3
- package/dist/components/Checkout/CheckoutShippingStep.svelte +1 -2
- package/dist/components/Checkout/README.md +143 -130
- package/dist/components/CommandMenu/CommandMenu.fixture.svelte +24 -0
- package/dist/components/CommandMenu/CommandMenu.fixture.svelte.d.ts +7 -0
- package/dist/components/CommandMenu/CommandMenu.svelte +10 -13
- package/dist/components/CommandMenu/_internal/command-menu-utils.d.ts +22 -0
- package/dist/components/CommandMenu/_internal/command-menu-utils.js +37 -0
- package/dist/components/CronInput/CronInput.svelte +64 -60
- package/dist/components/CronInput/README.md +46 -46
- package/dist/components/DataTable/DataTable.svelte +5 -1
- package/dist/components/DataTable/README.md +78 -63
- package/dist/components/DropdownMenu/DropdownMenu.svelte +14 -29
- package/dist/components/DropdownMenu/README.md +33 -27
- package/dist/components/DropdownMenu/_internal/dropdown-menu-search.d.ts +21 -0
- package/dist/components/DropdownMenu/_internal/dropdown-menu-search.js +47 -0
- package/dist/components/EmailVerifyForm/EmailVerifyForm.svelte +2 -9
- package/dist/components/EmailVerifyForm/README.md +30 -30
- package/dist/components/Header/Header.svelte +161 -165
- package/dist/components/Header/README.md +7 -7
- package/dist/components/IconSwap/README.md +20 -15
- package/dist/components/IconSwap/index.css +2 -1
- package/dist/components/ImageCycler/ImageCycler.svelte +19 -5
- package/dist/components/ImageCycler/ImageCycler.svelte.d.ts +14 -10
- package/dist/components/ImageCycler/README.md +15 -15
- package/dist/components/ImageCycler/index.css +26 -20
- package/dist/components/Input/FieldFile.svelte +1 -3
- package/dist/components/Input/FieldInput.svelte +1 -3
- package/dist/components/Input/FieldKeyValues.svelte +2 -6
- package/dist/components/Input/FieldObject.svelte +2 -1
- package/dist/components/Input/README.md +11 -11
- package/dist/components/Input/node_modules/.vite/vitest/d2a04d71301a8915217dd5faf81d12cffd6cd958/_svelte_metadata.json +1 -0
- package/dist/components/Input/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/_svelte_metadata.json +1 -0
- package/dist/components/KbdShortcut/index.css +2 -1
- package/dist/components/LoginForm/LoginForm.svelte +1 -7
- package/dist/components/LoginForm/README.md +46 -46
- package/dist/components/ModalDialog/ModalDialog.fixture.svelte +19 -0
- package/dist/components/ModalDialog/ModalDialog.fixture.svelte.d.ts +4 -0
- package/dist/components/ModalDialog/index.css +2 -1
- package/dist/components/Notifications/index.css +24 -6
- package/dist/components/OtpInput/OtpInput.svelte +0 -0
- package/dist/components/OtpInput/README.md +15 -19
- package/dist/components/OtpInput/index.css +1 -4
- package/dist/components/OtpInput/index.d.ts +1 -1
- package/dist/components/OtpInput/index.js +1 -1
- package/dist/components/Pill/README.md +41 -40
- package/dist/components/Pill/index.css +3 -6
- package/dist/components/PricingTable/README.md +86 -86
- package/dist/components/PricingTable/index.css +20 -35
- package/dist/components/RegisterForm/README.md +60 -60
- package/dist/components/RegisterForm/RegisterForm.svelte +1 -7
- package/dist/components/Separator/README.md +7 -7
- package/dist/components/SlidingPanels/SlidingPanels.fixture.svelte +20 -0
- package/dist/components/SlidingPanels/SlidingPanels.fixture.svelte.d.ts +6 -0
- package/dist/components/TabbedMenu/index.css +6 -3
- package/dist/components/Tree/README.md +67 -67
- package/dist/components/UserAvatarMenu/UserAvatarMenu.svelte +1 -5
- package/dist/components/WithSidePanel/index.css +4 -4
- package/dist/index.css +12 -8
- package/dist/utils/design-tokens.d.ts +1 -1
- package/dist/utils/design-tokens.js +1 -1
- package/docs/architecture.md +7 -7
- package/docs/component-testing/00-overview-and-roadmap.md +19 -19
- package/docs/component-testing/01-framework-setup.md +6 -6
- package/docs/component-testing/02-test-conventions.md +6 -5
- package/docs/component-testing/03-component-coverage-roadmap.md +27 -27
- package/docs/component-testing/04-hard-cases-and-e2e.md +8 -8
- package/docs/component-testing/05-ci.md +3 -3
- package/docs/component-testing/PROGRESS.md +118 -26
- package/docs/component-testing/README.md +8 -8
- package/docs/conventions.md +25 -25
- package/docs/domains/components.md +386 -385
- package/docs/domains/theming.md +24 -24
- package/docs/domains/utils.md +22 -25
- package/docs/testing.md +2 -2
- package/docs/upgrading.md +32 -28
- package/package.json +2 -1
|
@@ -8,30 +8,30 @@ There is deliberately **no top-level `<Checkout>`** orchestrator. If you want a
|
|
|
8
8
|
|
|
9
9
|
### Step containers (one per route/screen)
|
|
10
10
|
|
|
11
|
-
| Component
|
|
12
|
-
|
|
|
13
|
-
| `CheckoutReviewStep`
|
|
14
|
-
| `CheckoutShippingStep` | Address + delivery option selection
|
|
15
|
-
| `CheckoutConfirmStep`
|
|
16
|
-
| `CheckoutCompleteStep` | Post-purchase confirmation screen
|
|
11
|
+
| Component | Purpose |
|
|
12
|
+
| ---------------------- | --------------------------------------- |
|
|
13
|
+
| `CheckoutReviewStep` | Guest/login + cart review (entry point) |
|
|
14
|
+
| `CheckoutShippingStep` | Address + delivery option selection |
|
|
15
|
+
| `CheckoutConfirmStep` | Final order review + place-order action |
|
|
16
|
+
| `CheckoutCompleteStep` | Post-purchase confirmation screen |
|
|
17
17
|
|
|
18
18
|
Each step container renders a `CheckoutProgress` indicator (unless `hideProgress`), its own heading, and exposes `onBack` / `onContinue` callbacks. The consumer maps those callbacks to route navigation or state changes.
|
|
19
19
|
|
|
20
20
|
### Building blocks (composed inside steps, or used standalone)
|
|
21
21
|
|
|
22
|
-
| Component
|
|
23
|
-
|
|
|
24
|
-
| `CheckoutProgress`
|
|
25
|
-
| `CheckoutCartReview`
|
|
26
|
-
| `CheckoutOrderSummary`
|
|
27
|
-
| `CheckoutOrderReview`
|
|
28
|
-
| `CheckoutOrderConfirmation` | Completed-order summary with order number & next steps
|
|
29
|
-
| `CheckoutGuestOrLoginForm`
|
|
30
|
-
| `CheckoutGuestForm`
|
|
31
|
-
| `CheckoutLoginForm`
|
|
32
|
-
| `CheckoutAddressForm`
|
|
33
|
-
| `CheckoutDeliveryOptions`
|
|
34
|
-
| `CheckoutSectionHeader`
|
|
22
|
+
| Component | Purpose |
|
|
23
|
+
| --------------------------- | ------------------------------------------------------- |
|
|
24
|
+
| `CheckoutProgress` | Multi-step progress indicator (accessible stepper) |
|
|
25
|
+
| `CheckoutCartReview` | Editable line-item list |
|
|
26
|
+
| `CheckoutOrderSummary` | Totals (subtotal/tax/shipping/discount/total) |
|
|
27
|
+
| `CheckoutOrderReview` | Read-only order dump (items + addresses + delivery) |
|
|
28
|
+
| `CheckoutOrderConfirmation` | Completed-order summary with order number & next steps |
|
|
29
|
+
| `CheckoutGuestOrLoginForm` | Guest / login switcher (segmented pill) |
|
|
30
|
+
| `CheckoutGuestForm` | Guest-checkout fields |
|
|
31
|
+
| `CheckoutLoginForm` | Login (adapts the generic `LoginForm` to checkout i18n) |
|
|
32
|
+
| `CheckoutAddressForm` | Structured address input |
|
|
33
|
+
| `CheckoutDeliveryOptions` | Delivery-method selector |
|
|
34
|
+
| `CheckoutSectionHeader` | Consistent section heading |
|
|
35
35
|
|
|
36
36
|
## State ownership
|
|
37
37
|
|
|
@@ -39,25 +39,25 @@ The consumer owns the entire order shape — typically `CheckoutOrderData` from
|
|
|
39
39
|
|
|
40
40
|
```ts
|
|
41
41
|
import type {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
CheckoutOrderData,
|
|
43
|
+
CheckoutAddressData,
|
|
44
|
+
CheckoutCustomerFormData,
|
|
45
|
+
CheckoutLoginFormData,
|
|
46
|
+
CheckoutDeliveryOption,
|
|
47
47
|
} from "@marianmeres/stuic";
|
|
48
48
|
```
|
|
49
49
|
|
|
50
50
|
**Field → owner table:**
|
|
51
51
|
|
|
52
|
-
| Field
|
|
53
|
-
|
|
|
54
|
-
| `currentStep` (or equivalent)
|
|
55
|
-
| `CheckoutCustomerFormData`
|
|
56
|
-
| `CheckoutLoginFormData`
|
|
57
|
-
| `shippingAddress`, `billingAddress` | Page `$state`
|
|
58
|
-
| `selectedDeliveryId`
|
|
59
|
-
| `CheckoutOrderData` (assembled)
|
|
60
|
-
| Per-field validation errors
|
|
52
|
+
| Field | Owned by | Passed to |
|
|
53
|
+
| ----------------------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------ |
|
|
54
|
+
| `currentStep` (or equivalent) | Route/page state | `CheckoutProgress`, step visibility logic |
|
|
55
|
+
| `CheckoutCustomerFormData` | Page `$state` | `CheckoutGuestForm` (two-way bind) |
|
|
56
|
+
| `CheckoutLoginFormData` | Page `$state` | `CheckoutLoginForm` (two-way bind) |
|
|
57
|
+
| `shippingAddress`, `billingAddress` | Page `$state` | `CheckoutShippingStep` (two-way bind) — re-used by `CheckoutConfirmStep` for display |
|
|
58
|
+
| `selectedDeliveryId` | Page `$state` | `CheckoutShippingStep` (two-way bind) |
|
|
59
|
+
| `CheckoutOrderData` (assembled) | Page `$state` / server | `CheckoutOrderReview`, `CheckoutConfirmStep`, `CheckoutCompleteStep` (read-only) |
|
|
60
|
+
| Per-field validation errors | Page `$state` (derived from server response) | Forms via `errors` prop; merged with internal errors |
|
|
61
61
|
|
|
62
62
|
Each form exposes a bindable value plus an `errors` prop for server-driven validation messages. Internal client-side validation (via `validateCustomerForm` / `validateAddress` / `validateLoginForm`) fires on submit and populates internal error state, which is merged with the `errors` prop for display.
|
|
63
63
|
|
|
@@ -67,10 +67,10 @@ Client-side validation helpers live in `@marianmeres/stuic`:
|
|
|
67
67
|
|
|
68
68
|
```ts
|
|
69
69
|
import {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
validateCustomerForm,
|
|
71
|
+
validateAddress,
|
|
72
|
+
validateLoginForm,
|
|
73
|
+
validateEmail,
|
|
74
74
|
} from "@marianmeres/stuic";
|
|
75
75
|
```
|
|
76
76
|
|
|
@@ -78,8 +78,8 @@ Each returns `CheckoutValidationError[]`:
|
|
|
78
78
|
|
|
79
79
|
```ts
|
|
80
80
|
interface CheckoutValidationError {
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
field: string; // e.g. "email" or "shipping.street"
|
|
82
|
+
message: string;
|
|
83
83
|
}
|
|
84
84
|
```
|
|
85
85
|
|
|
@@ -87,36 +87,41 @@ interface CheckoutValidationError {
|
|
|
87
87
|
|
|
88
88
|
```svelte
|
|
89
89
|
<script lang="ts">
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
90
|
+
import {
|
|
91
|
+
CheckoutShippingStep,
|
|
92
|
+
CheckoutAddressForm,
|
|
93
|
+
validateAddress,
|
|
94
|
+
type CheckoutAddressData,
|
|
95
|
+
type CheckoutValidationError,
|
|
96
|
+
} from "@marianmeres/stuic";
|
|
97
|
+
|
|
98
|
+
let shippingAddress = $state<CheckoutAddressData>(/* ... */);
|
|
99
|
+
let errors = $state<CheckoutValidationError[]>([]);
|
|
100
|
+
|
|
101
|
+
async function onContinue() {
|
|
102
|
+
// 1) Client-side gate
|
|
103
|
+
const clientErrors = validateAddress(shippingAddress, "shipping", t);
|
|
104
|
+
if (clientErrors.length) {
|
|
105
|
+
errors = clientErrors;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
// 2) Server round-trip; merge any server errors
|
|
109
|
+
const res = await submitShipping(shippingAddress);
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
errors = res.errors;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// 3) Advance
|
|
115
|
+
goto("/checkout/confirm");
|
|
116
|
+
}
|
|
117
117
|
</script>
|
|
118
118
|
|
|
119
|
-
<CheckoutShippingStep
|
|
119
|
+
<CheckoutShippingStep
|
|
120
|
+
bind:shippingAddress
|
|
121
|
+
{errors}
|
|
122
|
+
{onContinue}
|
|
123
|
+
onBack={() => history.back()}
|
|
124
|
+
/>
|
|
120
125
|
```
|
|
121
126
|
|
|
122
127
|
The step component **does not auto-advance**. It calls `onContinue` when the user clicks "Continue"; the consumer decides whether to actually advance, retry, or show errors.
|
|
@@ -129,8 +134,9 @@ The built-in `defaultFormatPrice(cents)` returns `"12.99"`. Replace it via each
|
|
|
129
134
|
|
|
130
135
|
```ts
|
|
131
136
|
const formatPrice = (cents: number) =>
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
new Intl.NumberFormat("sk-SK", { style: "currency", currency: "EUR" }).format(
|
|
138
|
+
cents / 100
|
|
139
|
+
);
|
|
134
140
|
```
|
|
135
141
|
|
|
136
142
|
## i18n
|
|
@@ -147,67 +153,74 @@ By default `CheckoutGuestOrLoginForm` in tabbed mode renders an inline `<Checkou
|
|
|
147
153
|
|
|
148
154
|
```svelte
|
|
149
155
|
<script lang="ts">
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
156
|
+
import {
|
|
157
|
+
CheckoutGuestOrLoginForm,
|
|
158
|
+
createEmptyCustomerFormData,
|
|
159
|
+
createEmptyLoginFormData,
|
|
160
|
+
type LoginOrRegisterFormMode,
|
|
161
|
+
type LoginFormData,
|
|
162
|
+
type RegisterFormData,
|
|
163
|
+
} from "@marianmeres/stuic";
|
|
164
|
+
|
|
165
|
+
let formData = $state(createEmptyCustomerFormData());
|
|
166
|
+
let loginFormData = $state(createEmptyLoginFormData());
|
|
167
|
+
|
|
168
|
+
let mode = $state<LoginOrRegisterFormMode>("login");
|
|
169
|
+
let verifyEmail = $state("");
|
|
170
|
+
let isSubmitting = $state(false);
|
|
171
|
+
let formError = $state<string | null>(null);
|
|
172
|
+
|
|
173
|
+
const loginProps = $derived({ error: formError ?? undefined, showRememberMe: true });
|
|
174
|
+
const registerProps = $derived({ error: formError ?? undefined });
|
|
175
|
+
const verifyProps = $derived({
|
|
176
|
+
error: formError ?? undefined,
|
|
177
|
+
heading: false as const,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
async function onLogin(d: LoginFormData) {
|
|
181
|
+
// ... call API; on `requiresVerification`, flip:
|
|
182
|
+
// verifyEmail = d.email; mode = "verify";
|
|
183
|
+
}
|
|
184
|
+
async function onRegister(d: RegisterFormData) {
|
|
185
|
+
// ... call API; on success, flip to verify:
|
|
186
|
+
// verifyEmail = d.email; mode = "verify";
|
|
187
|
+
}
|
|
188
|
+
async function onVerify(code: string) {
|
|
189
|
+
// ... call API; on success, modal closes via consumer-managed state.
|
|
190
|
+
}
|
|
191
|
+
async function onResendCode() {
|
|
192
|
+
/* ... */
|
|
193
|
+
}
|
|
183
194
|
</script>
|
|
184
195
|
|
|
185
196
|
<CheckoutGuestOrLoginForm
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
197
|
+
formMode="tabbed"
|
|
198
|
+
guestForm={{ formData, onSubmit: handleStartCheckout, isSubmitting, errors: [] }}
|
|
199
|
+
loginForm={{ formData: loginFormData, onSubmit: onLogin, isSubmitting }}
|
|
200
|
+
loginOrRegisterModal={{
|
|
201
|
+
mode,
|
|
202
|
+
verifyEmail,
|
|
203
|
+
onLogin,
|
|
204
|
+
onRegister,
|
|
205
|
+
onVerify,
|
|
206
|
+
onResendCode,
|
|
207
|
+
onForgotPassword: () => {
|
|
208
|
+
/* ... */
|
|
209
|
+
},
|
|
210
|
+
onModeChange: (next) => {
|
|
211
|
+
// mirror mode changes back into our local state and clear errors
|
|
212
|
+
mode = next;
|
|
213
|
+
formError = null;
|
|
214
|
+
},
|
|
215
|
+
isSubmitting,
|
|
216
|
+
loginProps,
|
|
217
|
+
registerProps,
|
|
218
|
+
verifyProps,
|
|
219
|
+
onClose: () => {
|
|
220
|
+
formError = null;
|
|
221
|
+
mode = "login";
|
|
222
|
+
},
|
|
223
|
+
}}
|
|
211
224
|
/>
|
|
212
225
|
```
|
|
213
226
|
|
|
@@ -237,9 +250,9 @@ Returns `true` when either address is missing or when every `CheckoutAddressData
|
|
|
237
250
|
|
|
238
251
|
```ts
|
|
239
252
|
import {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
253
|
+
createEmptyAddress,
|
|
254
|
+
createEmptyCustomerFormData,
|
|
255
|
+
createEmptyLoginFormData,
|
|
243
256
|
} from "@marianmeres/stuic";
|
|
244
257
|
```
|
|
245
258
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
// Conventions escape hatch (docs/component-testing/02-test-conventions.md):
|
|
3
|
+
// CommandMenu is imperative-only (open()/close() via a ref; it builds on
|
|
4
|
+
// ModalDialog which has no bindable `visible` prop), so a `.svelte.test.ts`
|
|
5
|
+
// file can't drive it directly. This fixture holds the `bind:this` ref and
|
|
6
|
+
// exposes an opener button that calls `.open()`. `getOptions` (a required
|
|
7
|
+
// async prop) is forwarded; `value` is bound so a test can inspect the
|
|
8
|
+
// selected option after a pick.
|
|
9
|
+
import CommandMenu from "./CommandMenu.svelte";
|
|
10
|
+
|
|
11
|
+
let cmd = $state<CommandMenu>();
|
|
12
|
+
let { getOptions, value = $bindable() } = $props();
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<button data-testid="opener" onclick={() => cmd?.open()}>open</button>
|
|
16
|
+
|
|
17
|
+
<!--
|
|
18
|
+
Mirror the bound `value` into the DOM so a test can observe what the menu
|
|
19
|
+
selected (render() from vitest-browser-svelte does not hand back bound props).
|
|
20
|
+
The selected option is an item-collection Item; we surface its `id`.
|
|
21
|
+
-->
|
|
22
|
+
<span data-testid="selected">{value?.id ?? ""}</span>
|
|
23
|
+
|
|
24
|
+
<CommandMenu bind:this={cmd} bind:value {getOptions} />
|
|
@@ -65,6 +65,11 @@
|
|
|
65
65
|
|
|
66
66
|
<script lang="ts">
|
|
67
67
|
import Button from "../Button/Button.svelte";
|
|
68
|
+
import {
|
|
69
|
+
normalizeAndGroupOptions,
|
|
70
|
+
renderOptionLabelOf,
|
|
71
|
+
sortByOptgroupLabel,
|
|
72
|
+
} from "./_internal/command-menu-utils.js";
|
|
68
73
|
|
|
69
74
|
const clog = createClog("CommandMenu");
|
|
70
75
|
|
|
@@ -86,15 +91,14 @@
|
|
|
86
91
|
unstyled = false,
|
|
87
92
|
}: Props = $props();
|
|
88
93
|
|
|
94
|
+
// Pure helpers extracted to _internal/command-menu-utils.ts (node-tested). These
|
|
95
|
+
// thin wrappers keep reading the live props/closures and delegate the logic.
|
|
89
96
|
function _renderOptionLabel(item: Item): string {
|
|
90
|
-
return
|
|
97
|
+
return renderOptionLabelOf(item, renderOptionLabel, itemIdPropName);
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
function sortFn(a: Item, b: Item) {
|
|
94
|
-
|
|
95
|
-
return withOptGroup(a).localeCompare(withOptGroup(b), undefined, {
|
|
96
|
-
sensitivity: "base",
|
|
97
|
-
});
|
|
101
|
+
return sortByOptgroupLabel(a, b, _renderOptionLabel);
|
|
98
102
|
}
|
|
99
103
|
|
|
100
104
|
let modalDialog: ModalDialog = $state()!;
|
|
@@ -155,14 +159,7 @@
|
|
|
155
159
|
|
|
156
160
|
//
|
|
157
161
|
function _normalize_and_group_options(opts: Item[]): Map<string, Item[]> {
|
|
158
|
-
|
|
159
|
-
opts.forEach((o) => {
|
|
160
|
-
const optgLabel = renderOptionGroup(o.optgroup || "");
|
|
161
|
-
if (!groupped.has(optgLabel)) groupped.set(optgLabel, []);
|
|
162
|
-
const optgroup = groupped.get(optgLabel);
|
|
163
|
-
optgroup!.push(o);
|
|
164
|
-
});
|
|
165
|
-
return groupped;
|
|
162
|
+
return normalizeAndGroupOptions(opts, renderOptionGroup);
|
|
166
163
|
}
|
|
167
164
|
|
|
168
165
|
export function close() {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Item } from "@marianmeres/item-collection";
|
|
2
|
+
/**
|
|
3
|
+
* Pure option/result helpers for CommandMenu, extracted from the component so they
|
|
4
|
+
* can be unit-tested in the fast node project (no DOM). The actual searchable
|
|
5
|
+
* matching is delegated to `@marianmeres/item-collection`; these are the structural
|
|
6
|
+
* pieces around the results — how a result's label is derived, how results sort, and
|
|
7
|
+
* how they group by optgroup for rendering.
|
|
8
|
+
*/
|
|
9
|
+
/** Derive an option's display label: the consumer renderer, else its id prop. */
|
|
10
|
+
export declare function renderOptionLabelOf(item: Item, renderOptionLabel: ((item: Item) => string) | undefined, itemIdPropName: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Compare two options by "{optgroup}__{label}" using a case-insensitive locale
|
|
13
|
+
* compare, so options sort grouped-then-alphabetical. `renderLabel` derives the
|
|
14
|
+
* label part (typically `renderOptionLabelOf` bound to the component's props).
|
|
15
|
+
*/
|
|
16
|
+
export declare function sortByOptgroupLabel(a: Item, b: Item, renderLabel: (item: Item) => string): number;
|
|
17
|
+
/**
|
|
18
|
+
* Group options into a Map keyed by their (rendered) optgroup label, preserving
|
|
19
|
+
* input order within each group. `renderOptionGroup` maps a raw optgroup key to its
|
|
20
|
+
* display label (e.g. underscores → spaces).
|
|
21
|
+
*/
|
|
22
|
+
export declare function normalizeAndGroupOptions(opts: Item[], renderOptionGroup: (s: string) => string): Map<string, Item[]>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure option/result helpers for CommandMenu, extracted from the component so they
|
|
3
|
+
* can be unit-tested in the fast node project (no DOM). The actual searchable
|
|
4
|
+
* matching is delegated to `@marianmeres/item-collection`; these are the structural
|
|
5
|
+
* pieces around the results — how a result's label is derived, how results sort, and
|
|
6
|
+
* how they group by optgroup for rendering.
|
|
7
|
+
*/
|
|
8
|
+
/** Derive an option's display label: the consumer renderer, else its id prop. */
|
|
9
|
+
export function renderOptionLabelOf(item, renderOptionLabel, itemIdPropName) {
|
|
10
|
+
return renderOptionLabel?.(item) || `${item[itemIdPropName]}`;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Compare two options by "{optgroup}__{label}" using a case-insensitive locale
|
|
14
|
+
* compare, so options sort grouped-then-alphabetical. `renderLabel` derives the
|
|
15
|
+
* label part (typically `renderOptionLabelOf` bound to the component's props).
|
|
16
|
+
*/
|
|
17
|
+
export function sortByOptgroupLabel(a, b, renderLabel) {
|
|
18
|
+
const withOptGroup = (i) => `${i.optgroup || ""}__${renderLabel(i)}`;
|
|
19
|
+
return withOptGroup(a).localeCompare(withOptGroup(b), undefined, {
|
|
20
|
+
sensitivity: "base",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Group options into a Map keyed by their (rendered) optgroup label, preserving
|
|
25
|
+
* input order within each group. `renderOptionGroup` maps a raw optgroup key to its
|
|
26
|
+
* display label (e.g. underscores → spaces).
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeAndGroupOptions(opts, renderOptionGroup) {
|
|
29
|
+
const groupped = new Map();
|
|
30
|
+
opts.forEach((o) => {
|
|
31
|
+
const optgLabel = renderOptionGroup(o.optgroup || "");
|
|
32
|
+
if (!groupped.has(optgLabel))
|
|
33
|
+
groupped.set(optgLabel, []);
|
|
34
|
+
groupped.get(optgLabel).push(o);
|
|
35
|
+
});
|
|
36
|
+
return groupped;
|
|
37
|
+
}
|