@marianmeres/stuic 3.10.1 → 3.11.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/AGENTS.md +2 -2
- package/API.md +245 -0
- package/README.md +1 -1
- package/dist/actions/resizable-width.svelte.d.ts +4 -1
- package/dist/actions/resizable-width.svelte.js +10 -8
- package/dist/components/IconSwap/IconSwap.svelte +67 -0
- package/dist/components/IconSwap/IconSwap.svelte.d.ts +23 -0
- package/dist/components/IconSwap/index.css +33 -0
- package/dist/components/IconSwap/index.d.ts +1 -0
- package/dist/components/IconSwap/index.js +1 -0
- package/dist/components/WithSidePanel/README.md +107 -0
- package/dist/components/WithSidePanel/WithSidePanel.svelte +167 -0
- package/dist/components/WithSidePanel/WithSidePanel.svelte.d.ts +45 -0
- package/dist/components/WithSidePanel/index.css +55 -0
- package/dist/components/WithSidePanel/index.d.ts +2 -0
- package/dist/components/WithSidePanel/index.js +1 -0
- package/dist/index.css +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/themes/css/dds.css +217 -0
- package/dist/themes/dds.d.ts +6 -0
- package/dist/themes/dds.js +134 -0
- package/docs/domains/components.md +40 -1
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
```
|
|
25
25
|
src/lib/
|
|
26
|
-
├── components/ #
|
|
26
|
+
├── components/ # 39 UI components
|
|
27
27
|
├── actions/ # 12 Svelte actions
|
|
28
28
|
├── utils/ # 42 utility functions
|
|
29
29
|
├── themes/ # 26 theme definitions (.ts) + generated CSS (css/)
|
|
@@ -72,7 +72,7 @@ src/lib/
|
|
|
72
72
|
- [Tasks](./docs/tasks.md) — Common procedures
|
|
73
73
|
|
|
74
74
|
### Domain Docs
|
|
75
|
-
- [Components](./docs/domains/components.md) —
|
|
75
|
+
- [Components](./docs/domains/components.md) — 38 components, Props pattern, snippets
|
|
76
76
|
- [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
|
|
77
77
|
- [Actions](./docs/domains/actions.md) — 12 Svelte directives
|
|
78
78
|
- [Utils](./docs/domains/utils.md) — 42 utility functions
|
package/API.md
CHANGED
|
@@ -371,6 +371,223 @@ Shopping cart component with quantity controls, pricing, and summary. Supports i
|
|
|
371
371
|
</Cart>
|
|
372
372
|
```
|
|
373
373
|
|
|
374
|
+
#### Checkout
|
|
375
|
+
|
|
376
|
+
Multi-step checkout flow with 13 sub-components: 9 atomic building blocks and 4 composite step pages.
|
|
377
|
+
|
|
378
|
+
**Atomic Components:**
|
|
379
|
+
|
|
380
|
+
##### `CheckoutProgress`
|
|
381
|
+
|
|
382
|
+
Step indicator with navigation for past steps.
|
|
383
|
+
|
|
384
|
+
| Prop | Type | Default | Description |
|
|
385
|
+
|------|------|---------|-------------|
|
|
386
|
+
| `steps` | `CheckoutStep[]` | 4-step flow | Step definitions |
|
|
387
|
+
| `currentStep` | `string` | required | Active step ID |
|
|
388
|
+
| `onNavigate` | `(step: CheckoutStep) => void` | — | Past step click callback |
|
|
389
|
+
| `separator` | `Snippet \| string` | `"→"` | Step separator |
|
|
390
|
+
| `t` | `TranslateFn` | built-in | Translation function |
|
|
391
|
+
|
|
392
|
+
##### `CheckoutOrderSummary`
|
|
393
|
+
|
|
394
|
+
Price totals display (subtotal, shipping, tax, discount, total).
|
|
395
|
+
|
|
396
|
+
| Prop | Type | Default | Description |
|
|
397
|
+
|------|------|---------|-------------|
|
|
398
|
+
| `totals` | `CheckoutOrderTotals` | required | Price totals |
|
|
399
|
+
| `hasShipping` | `boolean` | `true` | Show shipping row |
|
|
400
|
+
| `formatPrice` | `(value: number) => string` | `defaultFormatPrice` | Price formatter |
|
|
401
|
+
| `row` | `Snippet` | — | Custom row rendering |
|
|
402
|
+
| `extraRows` | `Snippet` | — | Extra content before total |
|
|
403
|
+
|
|
404
|
+
##### `CheckoutCartReview`
|
|
405
|
+
|
|
406
|
+
Readonly cart display with summary.
|
|
407
|
+
|
|
408
|
+
| Prop | Type | Default | Description |
|
|
409
|
+
|------|------|---------|-------------|
|
|
410
|
+
| `items` | `CartComponentItem[]` | required | Cart items |
|
|
411
|
+
| `formatPrice` | `(value: number) => string` | `defaultFormatPrice` | Price formatter |
|
|
412
|
+
| `onEditCart` | `() => void` | — | Edit cart callback |
|
|
413
|
+
| `thumbnail` | `Snippet` | — | Custom thumbnail |
|
|
414
|
+
| `title` | `Snippet \| string` | `"Order Summary"` | Header title |
|
|
415
|
+
|
|
416
|
+
##### `CheckoutGuestForm`
|
|
417
|
+
|
|
418
|
+
Guest checkout form with email, name, phone, and optional B2B fields.
|
|
419
|
+
|
|
420
|
+
| Prop | Type | Default | Description |
|
|
421
|
+
|------|------|---------|-------------|
|
|
422
|
+
| `formData` | `CheckoutCustomerFormData` | empty | Bindable form data |
|
|
423
|
+
| `onSubmit` | `(data: CheckoutCustomerFormData) => void` | required | Submit callback |
|
|
424
|
+
| `isSubmitting` | `boolean` | `false` | Disables CTA |
|
|
425
|
+
| `errors` | `CheckoutValidationError[]` | `[]` | Server validation errors |
|
|
426
|
+
| `showB2bFields` | `boolean` | `true` | Show B2B section |
|
|
427
|
+
| `fields` | `object` | all visible | Field visibility overrides |
|
|
428
|
+
| `validate` | `(data) => CheckoutValidationError[]` | — | Custom validator |
|
|
429
|
+
|
|
430
|
+
##### `CheckoutLoginForm`
|
|
431
|
+
|
|
432
|
+
Login form with email and password.
|
|
433
|
+
|
|
434
|
+
| Prop | Type | Default | Description |
|
|
435
|
+
|------|------|---------|-------------|
|
|
436
|
+
| `formData` | `CheckoutLoginFormData` | empty | Bindable login data |
|
|
437
|
+
| `onSubmit` | `(data: CheckoutLoginFormData) => void` | required | Submit callback |
|
|
438
|
+
| `isSubmitting` | `boolean` | `false` | Disables CTA |
|
|
439
|
+
| `errors` | `CheckoutValidationError[]` | `[]` | Field errors |
|
|
440
|
+
| `error` | `string` | — | General error message |
|
|
441
|
+
| `onForgotPassword` | `() => void` | — | Forgot password callback |
|
|
442
|
+
| `footer` | `Snippet` | — | Content below form |
|
|
443
|
+
|
|
444
|
+
##### `CheckoutAddressForm`
|
|
445
|
+
|
|
446
|
+
Address input fieldset with configurable required fields.
|
|
447
|
+
|
|
448
|
+
| Prop | Type | Default | Description |
|
|
449
|
+
|------|------|---------|-------------|
|
|
450
|
+
| `address` | `CheckoutAddressData` | empty | Bindable address data |
|
|
451
|
+
| `label` | `string` | `"address"` | Prefix for field IDs |
|
|
452
|
+
| `errors` | `CheckoutValidationError[]` | `[]` | Validation errors |
|
|
453
|
+
| `requiredFields` | `string[]` | name, street, city, postal_code, country | Required fields |
|
|
454
|
+
| `countryField` | `Snippet` | — | Custom country field (replaces text input) |
|
|
455
|
+
|
|
456
|
+
##### `CheckoutDeliveryOptions`
|
|
457
|
+
|
|
458
|
+
Delivery method radio selection with free shipping threshold logic.
|
|
459
|
+
|
|
460
|
+
| Prop | Type | Default | Description |
|
|
461
|
+
|------|------|---------|-------------|
|
|
462
|
+
| `options` | `CheckoutDeliveryOption[]` | required | Available delivery options |
|
|
463
|
+
| `selectedId` | `string` | — | Bindable selected option ID |
|
|
464
|
+
| `onSelect` | `(optionId: string) => void` | — | Selection callback |
|
|
465
|
+
| `subtotal` | `number` | `0` | Order subtotal (for free shipping calc) |
|
|
466
|
+
| `isUpdating` | `boolean` | `false` | Reduced opacity during API calls |
|
|
467
|
+
| `formatPrice` | `(value: number) => string` | `defaultFormatPrice` | Price formatter |
|
|
468
|
+
| `option` | `Snippet` | — | Custom option card |
|
|
469
|
+
|
|
470
|
+
##### `CheckoutOrderReview`
|
|
471
|
+
|
|
472
|
+
Full order review display with per-section edit callbacks.
|
|
473
|
+
|
|
474
|
+
| Prop | Type | Default | Description |
|
|
475
|
+
|------|------|---------|-------------|
|
|
476
|
+
| `order` | `CheckoutOrderData` | required | Order data |
|
|
477
|
+
| `formatPrice` | `(value: number) => string` | `defaultFormatPrice` | Price formatter |
|
|
478
|
+
| `onEditItems` | `() => void` | — | Edit items callback |
|
|
479
|
+
| `onEditShippingAddress` | `() => void` | — | Edit shipping callback |
|
|
480
|
+
| `onEditBillingAddress` | `() => void` | — | Edit billing callback |
|
|
481
|
+
| `onEditDelivery` | `() => void` | — | Edit delivery callback |
|
|
482
|
+
|
|
483
|
+
##### `CheckoutOrderConfirmation`
|
|
484
|
+
|
|
485
|
+
Order success screen with details and continue shopping CTA.
|
|
486
|
+
|
|
487
|
+
| Prop | Type | Default | Description |
|
|
488
|
+
|------|------|---------|-------------|
|
|
489
|
+
| `order` | `CheckoutOrderData` | required | Completed order |
|
|
490
|
+
| `orderId` | `string` | required | Order ID for display |
|
|
491
|
+
| `emailSent` | `boolean` | `false` | Show email confirmation |
|
|
492
|
+
| `formatPrice` | `(value: number) => string` | `defaultFormatPrice` | Price formatter |
|
|
493
|
+
| `onContinueShopping` | `() => void` | — | Continue shopping callback |
|
|
494
|
+
|
|
495
|
+
**Composite Step Components:**
|
|
496
|
+
|
|
497
|
+
##### `CheckoutReviewStep`
|
|
498
|
+
|
|
499
|
+
Cart review + guest/login forms in 2-column layout.
|
|
500
|
+
|
|
501
|
+
| Prop | Type | Default | Description |
|
|
502
|
+
|------|------|---------|-------------|
|
|
503
|
+
| `items` | `CartComponentItem[]` | required | Cart items |
|
|
504
|
+
| `guestForm` | `object` | — | Guest form config (`formData`, `onSubmit`, `isSubmitting`, `errors`) |
|
|
505
|
+
| `loginForm` | `object` | — | Login form config (`formData`, `onSubmit`, `isSubmitting`, `errors`, `onForgotPassword`) |
|
|
506
|
+
| `formMode` | `"guest-only" \| "login-only" \| "tabbed" \| "stacked"` | `"tabbed"` | Form display mode |
|
|
507
|
+
| `onEditCart` | `() => void` | — | Edit cart callback |
|
|
508
|
+
| `formatPrice` | `(v: number) => string` | `defaultFormatPrice` | Price formatter |
|
|
509
|
+
| `leftColumn` | `Snippet` | — | Override left column |
|
|
510
|
+
| `rightColumn` | `Snippet` | — | Override right column |
|
|
511
|
+
|
|
512
|
+
##### `CheckoutShippingStep`
|
|
513
|
+
|
|
514
|
+
Shipping/billing addresses + delivery selection with sidebar summary.
|
|
515
|
+
|
|
516
|
+
| Prop | Type | Default | Description |
|
|
517
|
+
|------|------|---------|-------------|
|
|
518
|
+
| `order` | `CheckoutOrderData` | required | Order data for totals |
|
|
519
|
+
| `deliveryOptions` | `CheckoutDeliveryOption[]` | required | Available delivery options |
|
|
520
|
+
| `shippingAddress` | `CheckoutAddressData` | — | Bindable shipping address |
|
|
521
|
+
| `billingAddress` | `CheckoutAddressData` | — | Bindable billing address |
|
|
522
|
+
| `billingSameAsShipping` | `boolean` | `true` | Bindable toggle |
|
|
523
|
+
| `selectedDeliveryId` | `string` | — | Bindable delivery selection |
|
|
524
|
+
| `onContinue` | `() => void` | — | Continue callback |
|
|
525
|
+
| `onBack` | `() => void` | — | Back callback |
|
|
526
|
+
| `countryField` | `Snippet` | — | Custom country field |
|
|
527
|
+
|
|
528
|
+
##### `CheckoutConfirmStep`
|
|
529
|
+
|
|
530
|
+
Order review + place order CTA with sidebar summary.
|
|
531
|
+
|
|
532
|
+
| Prop | Type | Default | Description |
|
|
533
|
+
|------|------|---------|-------------|
|
|
534
|
+
| `order` | `CheckoutOrderData` | required | Order to confirm |
|
|
535
|
+
| `validationErrors` | `CheckoutValidationError[]` | `[]` | Validation errors |
|
|
536
|
+
| `isValid` | `boolean` | `true` | Enable/disable place order |
|
|
537
|
+
| `isSubmitting` | `boolean` | `false` | Processing state |
|
|
538
|
+
| `onPlaceOrder` | `() => void` | — | Place order callback |
|
|
539
|
+
| `onBack` | `() => void` | — | Back callback |
|
|
540
|
+
| `onEditItems` | `() => void` | — | Edit items callback |
|
|
541
|
+
| `onEditShippingAddress` | `() => void` | — | Edit shipping callback |
|
|
542
|
+
|
|
543
|
+
##### `CheckoutCompleteStep`
|
|
544
|
+
|
|
545
|
+
Order confirmation with loading/error/success states.
|
|
546
|
+
|
|
547
|
+
| Prop | Type | Default | Description |
|
|
548
|
+
|------|------|---------|-------------|
|
|
549
|
+
| `order` | `CheckoutOrderData` | required | Completed order |
|
|
550
|
+
| `orderId` | `string` | required | Order ID |
|
|
551
|
+
| `emailSent` | `boolean` | `false` | Email confirmation flag |
|
|
552
|
+
| `isLoading` | `boolean` | `false` | Show loading skeleton |
|
|
553
|
+
| `error` | `string \| null` | — | Error message |
|
|
554
|
+
| `onContinueShopping` | `() => void` | — | Continue shopping callback |
|
|
555
|
+
| `onReturnToCheckout` | `() => void` | — | Return callback (error state) |
|
|
556
|
+
|
|
557
|
+
All composite step components also accept `currentStep`, `steps`, `onStepNavigate` (for CheckoutProgress), and `t` (TranslateFn).
|
|
558
|
+
|
|
559
|
+
**Checkout Utilities:**
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
import {
|
|
563
|
+
defaultFormatPrice, // (cents: number) => string — "12.99"
|
|
564
|
+
validateEmail, // (email, t) => string | null
|
|
565
|
+
validateAddress, // (address, prefix, t) => CheckoutValidationError[]
|
|
566
|
+
validateCustomerForm, // (data, t) => CheckoutValidationError[]
|
|
567
|
+
validateLoginForm, // (data, t) => CheckoutValidationError[]
|
|
568
|
+
createEmptyAddress, // () => CheckoutAddressData
|
|
569
|
+
createEmptyCustomerFormData, // () => CheckoutCustomerFormData
|
|
570
|
+
createEmptyLoginFormData, // () => CheckoutLoginFormData
|
|
571
|
+
} from "@marianmeres/stuic";
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Checkout Types:**
|
|
575
|
+
|
|
576
|
+
```ts
|
|
577
|
+
import type {
|
|
578
|
+
CheckoutStep,
|
|
579
|
+
CheckoutAddressData,
|
|
580
|
+
CheckoutCustomerFormData,
|
|
581
|
+
CheckoutLoginFormData,
|
|
582
|
+
CheckoutOrderLineItem,
|
|
583
|
+
CheckoutOrderTotals,
|
|
584
|
+
CheckoutDeliveryOption,
|
|
585
|
+
CheckoutDeliverySnapshot,
|
|
586
|
+
CheckoutOrderData,
|
|
587
|
+
CheckoutValidationError,
|
|
588
|
+
} from "@marianmeres/stuic";
|
|
589
|
+
```
|
|
590
|
+
|
|
374
591
|
---
|
|
375
592
|
|
|
376
593
|
### Display
|
|
@@ -406,6 +623,32 @@ Image/content slider with navigation.
|
|
|
406
623
|
|
|
407
624
|
Animated loading dots ("...").
|
|
408
625
|
|
|
626
|
+
#### `IconSwap`
|
|
627
|
+
|
|
628
|
+
Swap visibility between N states with opacity transitions. Commonly used for hamburger/X icon toggle, but supports any content.
|
|
629
|
+
|
|
630
|
+
| Prop | Type | Default | Description |
|
|
631
|
+
|------|------|---------|-------------|
|
|
632
|
+
| `states` | `Array<string \| Snippet>` | required | Array of visual states (HTML strings or Snippets) |
|
|
633
|
+
| `active` | `number` | `0` | Index of the currently visible state (bindable) |
|
|
634
|
+
| `duration` | `number` | `300` | Transition duration in ms |
|
|
635
|
+
| `easing` | `string` | `"ease"` | CSS transition-timing-function |
|
|
636
|
+
| `stateClass` | `string` | — | Additional CSS classes for each state wrapper |
|
|
637
|
+
|
|
638
|
+
**CSS Variables:** `--stuic-icon-swap-duration`, `--stuic-icon-swap-easing`
|
|
639
|
+
|
|
640
|
+
```svelte
|
|
641
|
+
<script>
|
|
642
|
+
import { IconSwap } from "@marianmeres/stuic";
|
|
643
|
+
import { iconMenu, iconX } from "@marianmeres/stuic";
|
|
644
|
+
let isOpen = $state(false);
|
|
645
|
+
</script>
|
|
646
|
+
|
|
647
|
+
<button onclick={() => (isOpen = !isOpen)}>
|
|
648
|
+
<IconSwap active={isOpen ? 1 : 0} states={[iconMenu(), iconX()]} />
|
|
649
|
+
</button>
|
|
650
|
+
```
|
|
651
|
+
|
|
409
652
|
#### `DataTable`
|
|
410
653
|
|
|
411
654
|
Responsive data table with paging, row selection, batch actions, and mobile card view.
|
|
@@ -970,6 +1213,7 @@ Naming pattern: `{ComponentName}Props`
|
|
|
970
1213
|
|
|
971
1214
|
Additional exported types include:
|
|
972
1215
|
- `CartComponentItem`, `CartVariant` — Cart component types
|
|
1216
|
+
- `CheckoutStep`, `CheckoutAddressData`, `CheckoutCustomerFormData`, `CheckoutLoginFormData`, `CheckoutOrderLineItem`, `CheckoutOrderTotals`, `CheckoutDeliveryOption`, `CheckoutDeliverySnapshot`, `CheckoutOrderData`, `CheckoutValidationError` — Checkout types
|
|
973
1217
|
- `DataTableColumn` — DataTable column definition type
|
|
974
1218
|
- `FieldAsset`, `FieldAssetUrlObj`, `FieldAssetWithBlobUrl` — Asset field types
|
|
975
1219
|
- `FieldOption` — Option type for FieldOptions
|
|
@@ -1055,6 +1299,7 @@ Each component defines customization tokens. Override globally in `:root {}` or
|
|
|
1055
1299
|
| Popover | `--stuic-popover-*` | `bg`, `text`, `border` |
|
|
1056
1300
|
| Skeleton | `--stuic-skeleton-*` | `bg`, `bg-highlight`, `duration` |
|
|
1057
1301
|
| Cart | `--stuic-cart-*` | `gap`, `item-padding`, `item-radius`, `item-border-color`, `item-bg`, `thumbnail-size`, `quantity-border-color`, `remove-color`, `summary-border-color`, `compact-max-height`, `transition` |
|
|
1302
|
+
| Checkout | `--stuic-checkout-*` | `input-border`, `input-bg`, `input-focus-ring`, `input-radius`, `card-border`, `card-bg`, `card-radius`, `step-gap`, `progress-*`, `summary-*`, `guest-*`, `login-*`, `address-*`, `delivery-*`, `review-*`, `confirmation-*` |
|
|
1058
1303
|
|
|
1059
1304
|
### CSS Variable Naming Convention
|
|
1060
1305
|
|
package/README.md
CHANGED
|
@@ -144,7 +144,7 @@ CommandMenu, DropdownMenu, TabbedMenu, TypeaheadInput, KbdShortcut
|
|
|
144
144
|
Avatar, Carousel, AnimatedElipsis, ThemePreview, ColorScheme, Thc, HoverExpandableWidth, AssetsPreview, DataTable
|
|
145
145
|
|
|
146
146
|
### E-commerce
|
|
147
|
-
Cart
|
|
147
|
+
Cart, Checkout (CheckoutProgress, CheckoutOrderSummary, CheckoutCartReview, CheckoutGuestForm, CheckoutLoginForm, CheckoutAddressForm, CheckoutDeliveryOptions, CheckoutOrderReview, CheckoutOrderConfirmation, CheckoutReviewStep, CheckoutShippingStep, CheckoutConfirmStep, CheckoutCompleteStep)
|
|
148
148
|
|
|
149
149
|
## Actions
|
|
150
150
|
|
|
@@ -7,6 +7,8 @@ export interface ResizableWidthOptions {
|
|
|
7
7
|
min?: number;
|
|
8
8
|
max?: number;
|
|
9
9
|
units?: "px" | "%";
|
|
10
|
+
/** Reverses handle position (left instead of right) and drag direction */
|
|
11
|
+
reverse?: boolean;
|
|
10
12
|
key?: string | number | null | undefined;
|
|
11
13
|
storage?: "local" | "session";
|
|
12
14
|
handleClass?: string;
|
|
@@ -21,7 +23,8 @@ export interface ResizableWidthOptions {
|
|
|
21
23
|
/**
|
|
22
24
|
* A Svelte action that makes an element's width resizable via drag handle.
|
|
23
25
|
*
|
|
24
|
-
* Adds a draggable handle to the right edge of the element.
|
|
26
|
+
* Adds a draggable handle to the right (or left with `reverse`) edge of the element.
|
|
27
|
+
* Supports mouse and touch input.
|
|
25
28
|
* Optionally persists the width to localStorage/sessionStorage.
|
|
26
29
|
*
|
|
27
30
|
* Features:
|
|
@@ -3,7 +3,8 @@ import { twMerge } from "../utils/tw-merge.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* A Svelte action that makes an element's width resizable via drag handle.
|
|
5
5
|
*
|
|
6
|
-
* Adds a draggable handle to the right edge of the element.
|
|
6
|
+
* Adds a draggable handle to the right (or left with `reverse`) edge of the element.
|
|
7
|
+
* Supports mouse and touch input.
|
|
7
8
|
* Optionally persists the width to localStorage/sessionStorage.
|
|
8
9
|
*
|
|
9
10
|
* Features:
|
|
@@ -43,9 +44,9 @@ import { twMerge } from "../utils/tw-merge.js";
|
|
|
43
44
|
* ```
|
|
44
45
|
*/
|
|
45
46
|
export function resizableWidth(el, fn) {
|
|
46
|
-
const
|
|
47
|
+
const HANDLE_CLS_BASE = [
|
|
47
48
|
"group",
|
|
48
|
-
"absolute top-0
|
|
49
|
+
"absolute top-0 bottom-0",
|
|
49
50
|
"w-[1px]",
|
|
50
51
|
"bg-black/20 hover:bg-black/30",
|
|
51
52
|
"dark:bg-white/10 dark:hover:bg-white/20",
|
|
@@ -61,7 +62,7 @@ export function resizableWidth(el, fn) {
|
|
|
61
62
|
"transition-colors duration-200",
|
|
62
63
|
"touch-none cursor-ew-resize",
|
|
63
64
|
].join(" ");
|
|
64
|
-
function create_handle(el, handleClass, handleDragClass) {
|
|
65
|
+
function create_handle(el, handleClass, handleDragClass, reverse) {
|
|
65
66
|
const handle = document.createElement("div");
|
|
66
67
|
handle.setAttribute("data-handle", "true");
|
|
67
68
|
const dragHandle = document.createElement("div");
|
|
@@ -69,11 +70,12 @@ export function resizableWidth(el, fn) {
|
|
|
69
70
|
handle.appendChild(dragHandle);
|
|
70
71
|
el.appendChild(handle);
|
|
71
72
|
//
|
|
72
|
-
|
|
73
|
+
const positionCls = reverse ? "left-0" : "right-0";
|
|
74
|
+
handle.classList.add(...twMerge(HANDLE_CLS_BASE, positionCls, handleClass).split(" "));
|
|
73
75
|
return handle;
|
|
74
76
|
}
|
|
75
77
|
$effect(() => {
|
|
76
|
-
const { enabled = true, initial: initialValue = 0, min = 0, max = 0, units = "px", key, storage = "session", handleClass = "", handleDragClass = "", onResize, debug, } = fn?.() || {};
|
|
78
|
+
const { enabled = true, initial: initialValue = 0, min = 0, max = 0, units = "px", reverse = false, key, storage = "session", handleClass = "", handleDragClass = "", onResize, debug, } = fn?.() || {};
|
|
77
79
|
let initial = initialValue;
|
|
78
80
|
const _debug = (...args) => debug?.("[resizable-width]", ...args);
|
|
79
81
|
_debug("$effect");
|
|
@@ -86,7 +88,7 @@ export function resizableWidth(el, fn) {
|
|
|
86
88
|
let startWidth = 0;
|
|
87
89
|
let containerW = undefined;
|
|
88
90
|
//
|
|
89
|
-
const handle = create_handle(el, handleClass, handleDragClass);
|
|
91
|
+
const handle = create_handle(el, handleClass, handleDragClass, reverse);
|
|
90
92
|
const container = el.parentElement;
|
|
91
93
|
// do we have a storage? if so, adjust the initial value...
|
|
92
94
|
const initialBackup = initial;
|
|
@@ -149,7 +151,7 @@ export function resizableWidth(el, fn) {
|
|
|
149
151
|
//
|
|
150
152
|
const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
|
|
151
153
|
const deltaX = clientX - startX;
|
|
152
|
-
const width = startWidth + deltaX;
|
|
154
|
+
const width = reverse ? startWidth - deltaX : startWidth + deltaX;
|
|
153
155
|
set_width_px(width);
|
|
154
156
|
}
|
|
155
157
|
function resize_stop() {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
4
|
+
|
|
5
|
+
export interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "children"> {
|
|
6
|
+
/** Array of visual states (HTML strings or Snippets) */
|
|
7
|
+
states: Array<string | Snippet>;
|
|
8
|
+
/** Index of the currently visible state (0-based) */
|
|
9
|
+
active?: number;
|
|
10
|
+
/** Transition duration in ms (default: 300). Set 0 to disable. */
|
|
11
|
+
duration?: number;
|
|
12
|
+
/** CSS transition-timing-function (default: "ease") */
|
|
13
|
+
easing?: string;
|
|
14
|
+
/** Skip all default styling */
|
|
15
|
+
unstyled?: boolean;
|
|
16
|
+
/** Additional CSS classes for the container */
|
|
17
|
+
class?: string;
|
|
18
|
+
/** Additional CSS classes for each state wrapper */
|
|
19
|
+
stateClass?: string;
|
|
20
|
+
/** Bindable root element reference */
|
|
21
|
+
el?: HTMLSpanElement;
|
|
22
|
+
}
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<script lang="ts">
|
|
26
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
27
|
+
import { prefersReducedMotion } from "../../utils/prefers-reduced-motion.svelte.js";
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
states,
|
|
31
|
+
active = $bindable(0),
|
|
32
|
+
duration: durationProp = 300,
|
|
33
|
+
easing = "ease",
|
|
34
|
+
unstyled = false,
|
|
35
|
+
class: classProp,
|
|
36
|
+
stateClass,
|
|
37
|
+
el = $bindable(),
|
|
38
|
+
...rest
|
|
39
|
+
}: Props = $props();
|
|
40
|
+
|
|
41
|
+
const prefersReduced = prefersReducedMotion();
|
|
42
|
+
let duration = $derived(prefersReduced.current ? 0 : durationProp);
|
|
43
|
+
let _active = $derived(Math.max(0, Math.min(active, states.length - 1)));
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<span
|
|
47
|
+
bind:this={el}
|
|
48
|
+
class={unstyled ? classProp : twMerge("stuic-icon-swap", classProp)}
|
|
49
|
+
style:--stuic-icon-swap-duration="{duration}ms"
|
|
50
|
+
style:--stuic-icon-swap-easing={easing}
|
|
51
|
+
data-active={_active}
|
|
52
|
+
{...rest}
|
|
53
|
+
>
|
|
54
|
+
{#each states as state, i (i)}
|
|
55
|
+
<span
|
|
56
|
+
class={unstyled ? stateClass : twMerge("stuic-icon-swap-state", stateClass)}
|
|
57
|
+
data-visible={i === _active ? "true" : undefined}
|
|
58
|
+
aria-hidden={i !== _active}
|
|
59
|
+
>
|
|
60
|
+
{#if typeof state === "string"}
|
|
61
|
+
{@html state}
|
|
62
|
+
{:else}
|
|
63
|
+
{@render state()}
|
|
64
|
+
{/if}
|
|
65
|
+
</span>
|
|
66
|
+
{/each}
|
|
67
|
+
</span>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { HTMLAttributes } from "svelte/elements";
|
|
3
|
+
export interface Props extends Omit<HTMLAttributes<HTMLSpanElement>, "children"> {
|
|
4
|
+
/** Array of visual states (HTML strings or Snippets) */
|
|
5
|
+
states: Array<string | Snippet>;
|
|
6
|
+
/** Index of the currently visible state (0-based) */
|
|
7
|
+
active?: number;
|
|
8
|
+
/** Transition duration in ms (default: 300). Set 0 to disable. */
|
|
9
|
+
duration?: number;
|
|
10
|
+
/** CSS transition-timing-function (default: "ease") */
|
|
11
|
+
easing?: string;
|
|
12
|
+
/** Skip all default styling */
|
|
13
|
+
unstyled?: boolean;
|
|
14
|
+
/** Additional CSS classes for the container */
|
|
15
|
+
class?: string;
|
|
16
|
+
/** Additional CSS classes for each state wrapper */
|
|
17
|
+
stateClass?: string;
|
|
18
|
+
/** Bindable root element reference */
|
|
19
|
+
el?: HTMLSpanElement;
|
|
20
|
+
}
|
|
21
|
+
declare const IconSwap: import("svelte").Component<Props, {}, "active" | "el">;
|
|
22
|
+
type IconSwap = ReturnType<typeof IconSwap>;
|
|
23
|
+
export default IconSwap;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
ICON SWAP COMPONENT TOKENS
|
|
3
|
+
Override globally: :root { --stuic-icon-swap-duration: 500ms; }
|
|
4
|
+
Override locally: <IconSwap style="--stuic-icon-swap-duration: 500ms;">
|
|
5
|
+
============================================================================ */
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
--stuic-icon-swap-duration: 150ms;
|
|
9
|
+
--stuic-icon-swap-easing: ease;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@layer components {
|
|
13
|
+
.stuic-icon-swap {
|
|
14
|
+
display: grid;
|
|
15
|
+
width: fit-content;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.stuic-icon-swap > .stuic-icon-swap-state {
|
|
19
|
+
grid-column: 1 / -1;
|
|
20
|
+
grid-row: 1 / -1;
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
justify-content: center;
|
|
24
|
+
opacity: 0;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
transition: opacity var(--stuic-icon-swap-duration) var(--stuic-icon-swap-easing);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.stuic-icon-swap > .stuic-icon-swap-state[data-visible="true"] {
|
|
30
|
+
opacity: 1;
|
|
31
|
+
pointer-events: auto;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as IconSwap, type Props as IconSwapProps } from "./IconSwap.svelte";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as IconSwap } from "./IconSwap.svelte";
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# WithSidePanel
|
|
2
|
+
|
|
3
|
+
A flex layout component with a collapsible side panel and main content area.
|
|
4
|
+
Both panels consume full height with independent scrolling. The side panel
|
|
5
|
+
auto-hides when the container width falls below a configurable threshold
|
|
6
|
+
(container-based, not viewport-based), behaving like a mobile drawer.
|
|
7
|
+
|
|
8
|
+
## Props
|
|
9
|
+
|
|
10
|
+
| Prop | Type | Default | Description |
|
|
11
|
+
|------|------|---------|-------------|
|
|
12
|
+
| `children` | `Snippet` | — | Main content |
|
|
13
|
+
| `side` | `Snippet` | — | Side panel content |
|
|
14
|
+
| `position` | `"left" \| "right"` | `"left"` | Side panel position |
|
|
15
|
+
| `width` | `string` | `"300px"` | Side panel width (CSS value, px or %) |
|
|
16
|
+
| `threshold` | `number` | `768` | Container width (px) below which side auto-hides |
|
|
17
|
+
| `transition` | `boolean` | `true` | Enable Svelte slide transition |
|
|
18
|
+
| `transitionDuration` | `number` | `200` | Transition duration in ms |
|
|
19
|
+
| `open` | `boolean` | `true` | Desktop visibility state (bindable) |
|
|
20
|
+
| `resizable` | `boolean \| Partial<ResizableWidthOptions>` | `false` | Enable drag-resizable side panel |
|
|
21
|
+
| `classMain` | `string` | — | Custom class for main content div |
|
|
22
|
+
| `classSide` | `string` | — | Custom class for side panel div |
|
|
23
|
+
| `class` | `string` | — | Custom class for wrapper div |
|
|
24
|
+
| `unstyled` | `boolean` | `false` | Skip all default styling |
|
|
25
|
+
| `el` | `HTMLDivElement` | — | Bindable wrapper element reference |
|
|
26
|
+
|
|
27
|
+
## API (via `bind:this`)
|
|
28
|
+
|
|
29
|
+
| Method | Returns | Description |
|
|
30
|
+
|--------|---------|-------------|
|
|
31
|
+
| `show()` | `void` | Show the side panel |
|
|
32
|
+
| `hide()` | `void` | Hide the side panel |
|
|
33
|
+
| `toggle()` | `void` | Toggle the side panel |
|
|
34
|
+
| `setWidth(w: string)` | `void` | Set side panel width dynamically |
|
|
35
|
+
| `current()` | `{ open: boolean, small: boolean }` | Current state |
|
|
36
|
+
|
|
37
|
+
## Usage
|
|
38
|
+
|
|
39
|
+
### Basic
|
|
40
|
+
|
|
41
|
+
```svelte
|
|
42
|
+
<WithSidePanel>
|
|
43
|
+
{#snippet side()}
|
|
44
|
+
<nav>Navigation</nav>
|
|
45
|
+
{/snippet}
|
|
46
|
+
Main content
|
|
47
|
+
</WithSidePanel>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Programmatic control
|
|
51
|
+
|
|
52
|
+
```svelte
|
|
53
|
+
<script>
|
|
54
|
+
import { WithSidePanel } from '@marianmeres/stuic';
|
|
55
|
+
let panel;
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<button onclick={() => panel.toggle()}>Toggle</button>
|
|
59
|
+
|
|
60
|
+
<WithSidePanel bind:this={panel} width="250px" position="right">
|
|
61
|
+
{#snippet side()}
|
|
62
|
+
Side content
|
|
63
|
+
{/snippet}
|
|
64
|
+
Main content
|
|
65
|
+
</WithSidePanel>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Resizable
|
|
69
|
+
|
|
70
|
+
```svelte
|
|
71
|
+
<WithSidePanel resizable={{ min: 200, max: 500, key: 'sidebar', storage: 'local' }}>
|
|
72
|
+
{#snippet side()}
|
|
73
|
+
Drag the edge to resize
|
|
74
|
+
{/snippet}
|
|
75
|
+
Main content
|
|
76
|
+
</WithSidePanel>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Tracking state
|
|
80
|
+
|
|
81
|
+
```svelte
|
|
82
|
+
<script>
|
|
83
|
+
let isOpen = $state(true);
|
|
84
|
+
</script>
|
|
85
|
+
|
|
86
|
+
<WithSidePanel bind:open={isOpen} threshold={600}>
|
|
87
|
+
{#snippet side()}
|
|
88
|
+
Panel is {isOpen ? 'open' : 'closed'}
|
|
89
|
+
{/snippet}
|
|
90
|
+
Main content
|
|
91
|
+
</WithSidePanel>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## CSS Variables
|
|
95
|
+
|
|
96
|
+
| Variable | Default | Description |
|
|
97
|
+
|----------|---------|-------------|
|
|
98
|
+
| `--stuic-with-side-panel-border-color` | `gray-200` / `gray-700` (dark) | Border between panels |
|
|
99
|
+
| `--stuic-with-side-panel-border-width` | `1px` | Border width |
|
|
100
|
+
|
|
101
|
+
## Data Attributes (on wrapper)
|
|
102
|
+
|
|
103
|
+
| Attribute | Values | Description |
|
|
104
|
+
|-----------|--------|-------------|
|
|
105
|
+
| `data-position` | `"left"` \| `"right"` | Current side panel position |
|
|
106
|
+
| `data-small` | present/absent | Container is below threshold |
|
|
107
|
+
| `data-open` | present/absent | Side panel is visible |
|