@getmicdrop/svelte-components 5.10.1 → 5.12.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/dist/components/Layout/Stack.spec.js +1 -1
- package/dist/datetime/__tests__/format.test.js +1 -1
- package/dist/datetime/__tests__/parse.test.js +1 -1
- package/dist/datetime/__tests__/timezone.test.js +1 -1
- package/dist/datetime/parse.js +1 -1
- package/dist/forms/createFormStore.svelte.js +0 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/primitives/Breadcrumb/Breadcrumb.spec.js +6 -5
- package/dist/primitives/Breadcrumb/Breadcrumb.svelte +9 -8
- package/dist/primitives/Button/Button.svelte +33 -2
- package/dist/primitives/Button/Button.svelte.d.ts +2 -0
- package/dist/primitives/Button/Button.svelte.d.ts.map +1 -1
- package/dist/primitives/Modal/Modal.svelte +17 -0
- package/dist/primitives/Modal/Modal.svelte.d.ts +2 -0
- package/dist/primitives/Modal/Modal.svelte.d.ts.map +1 -1
- package/dist/primitives/Toggle.svelte +10 -0
- package/dist/primitives/Toggle.svelte.d.ts +2 -0
- package/dist/primitives/Toggle.svelte.d.ts.map +1 -1
- package/dist/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.svelte +32 -18
- package/dist/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.svelte.d.ts.map +1 -1
- package/dist/recipes/modals/ConfirmationModal.svelte +14 -4
- package/dist/recipes/modals/ConfirmationModal.svelte.d.ts +2 -0
- package/dist/recipes/modals/ConfirmationModal.svelte.d.ts.map +1 -1
- package/dist/utils/haptic.d.ts +41 -0
- package/dist/utils/haptic.d.ts.map +1 -0
- package/dist/utils/haptic.js +115 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { formatCleanTimeRange, formatDateRange, formatDayOfWeek, formatEventDate, formatEventDateTime, formatEventTime, formatHour, formatMonth, formatNotificationTime, formatRelativeTime, formatTimeRange, getDateInTimezone, getDateParts, getHourInTimezone, isToday, } from '../format';
|
|
3
|
-
import { DateTimeError
|
|
3
|
+
import { DateTimeError } from '../types';
|
|
4
4
|
describe('format utilities', () => {
|
|
5
5
|
describe('formatEventTime', () => {
|
|
6
6
|
it('formats time in specified timezone', () => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { combineDateAndTime, formatDateTimeForAPI, isNextDayTime, minutesToTimeString, parseDateTimeFromAPI, parseEndOfDay, parseLocalToUTC, parseStartOfDay, parseTimeToMinutes, parseUTCToLocal, stripNextDayPrefix, } from '../parse';
|
|
3
|
-
import { DateTimeError
|
|
3
|
+
import { DateTimeError } from '../types';
|
|
4
4
|
describe('parse utilities', () => {
|
|
5
5
|
describe('parseLocalToUTC', () => {
|
|
6
6
|
it('converts local datetime to UTC', () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { getTimezoneDisplayName, getTimezoneOffset, getUserTimezone, getVenueTimezone, isDST, isValidTimezone, normalizeTimezone, getIANATimezone, isValidIANATimezone, getAllTimezones, formatTimezoneForDisplay, getTimezoneOptions, getCommonUSTimezoneOptions, } from '../timezone';
|
|
3
3
|
import { DateTimeError, DateTimeErrorCode } from '../types';
|
|
4
4
|
describe('timezone utilities', () => {
|
package/dist/datetime/parse.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module datetime/parse
|
|
8
8
|
*/
|
|
9
|
-
import { format
|
|
9
|
+
import { format } from 'date-fns';
|
|
10
10
|
import { fromZonedTime, toZonedTime } from 'date-fns-tz';
|
|
11
11
|
import { DATE_FORMATS } from './constants';
|
|
12
12
|
import { isValidTimezone } from './timezone';
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
* - UI states (loading, saving, saved)
|
|
8
8
|
* - Section-level validation for progressive forms
|
|
9
9
|
*/
|
|
10
|
-
import { z } from 'zod';
|
|
11
10
|
// ============================================================================
|
|
12
11
|
// Implementation
|
|
13
12
|
// ============================================================================
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export * from "./constants/validation.js";
|
|
|
8
8
|
export * from "./presets/index.js";
|
|
9
9
|
export { portal } from "./utils/portal.js";
|
|
10
10
|
export { typography } from "./tokens/typography.js";
|
|
11
|
+
export type HapticStyle = import("./utils/haptic.js").HapticStyle;
|
|
11
12
|
export { safeSlide, bloom } from "./utils/transitions.js";
|
|
12
13
|
export { optimizeImage, supportsWebP, createImage } from "./utils/imageOptimizer.js";
|
|
14
|
+
export { triggerHaptic, isHapticAvailable, getHapticForButtonVariant } from "./utils/haptic.js";
|
|
13
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.js"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.js"],"names":[],"mappings":";;;;;;;;;;0BAqDc,OAAO,mBAAmB,EAAE,WAAW"}
|
package/dist/index.js
CHANGED
|
@@ -46,6 +46,12 @@ export * from './presets/index.js';
|
|
|
46
46
|
export { portal } from './utils/portal.js';
|
|
47
47
|
export { safeSlide, bloom } from './utils/transitions.js';
|
|
48
48
|
export { optimizeImage, supportsWebP, createImage } from './utils/imageOptimizer.js';
|
|
49
|
+
export {
|
|
50
|
+
triggerHaptic,
|
|
51
|
+
isHapticAvailable,
|
|
52
|
+
getHapticForButtonVariant,
|
|
53
|
+
} from './utils/haptic.js';
|
|
54
|
+
/** @typedef {import('./utils/haptic.js').HapticStyle} HapticStyle */
|
|
49
55
|
|
|
50
56
|
// Design Tokens
|
|
51
57
|
export { typography } from './tokens/typography.js';
|
|
@@ -48,12 +48,13 @@ describe("Breadcrumb Component Tests", () => {
|
|
|
48
48
|
expect(icons.length).toBeGreaterThan(0);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
test("Shows separator
|
|
51
|
+
test("Shows separator characters between items", () => {
|
|
52
52
|
setupTest();
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
// Separators appear between items (n-1 separators for n items)
|
|
54
|
+
// We use text character "›" instead of SVG arrows for consistent spacing
|
|
55
|
+
const separators = document.querySelectorAll("span.text-gray-400");
|
|
56
|
+
// 3 items means 2 separators
|
|
57
|
+
expect(separators.length).toBe(2);
|
|
57
58
|
});
|
|
58
59
|
|
|
59
60
|
test("Last item is not a link", () => {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
showHomeIcon = true,
|
|
29
29
|
title = '',
|
|
30
30
|
subtitle = '',
|
|
31
|
-
onclick
|
|
31
|
+
onclick,
|
|
32
32
|
}: Props = $props();
|
|
33
33
|
|
|
34
34
|
function handleClick(crumb: BreadcrumbItem) {
|
|
@@ -39,16 +39,16 @@
|
|
|
39
39
|
<div class="flex flex-col items-start gap-2 min-w-0">
|
|
40
40
|
{#if data.length > 0}
|
|
41
41
|
<nav class={`flex items-center ${typography.smMuted} font-medium ${className}`} aria-label="Breadcrumb">
|
|
42
|
-
<ol class="inline-flex items-center
|
|
42
|
+
<ol class="inline-flex items-center rtl:space-x-reverse flex-wrap">
|
|
43
43
|
{#each data as crumb, index}
|
|
44
44
|
<li class="inline-flex items-center">
|
|
45
45
|
{#if index > 0}
|
|
46
|
-
<
|
|
46
|
+
<span class="text-gray-400" style="margin: 0 6px;">›</span>
|
|
47
47
|
{/if}
|
|
48
48
|
{#if index === 0 && showHomeIcon && data.length === 1}
|
|
49
49
|
<!-- Single item with home icon - show as non-clickable label -->
|
|
50
|
-
<span class="{typography.smMuted} inline-flex items-center font-medium">
|
|
51
|
-
<HomeSolid class="w-3 h-3
|
|
50
|
+
<span class="{typography.smMuted} inline-flex items-center font-medium" style="gap: 6px;">
|
|
51
|
+
<HomeSolid class="w-3 h-3" />
|
|
52
52
|
{crumb.name}
|
|
53
53
|
</span>
|
|
54
54
|
{:else if index === 0 && showHomeIcon}
|
|
@@ -57,13 +57,14 @@
|
|
|
57
57
|
href={crumb.href}
|
|
58
58
|
onclick={() => handleClick(crumb)}
|
|
59
59
|
class="{typography.smMuted} inline-flex items-center font-medium hover:text-blue-600 dark:hover:text-white"
|
|
60
|
+
style="gap: 6px;"
|
|
60
61
|
>
|
|
61
|
-
<HomeSolid class="w-3 h-3
|
|
62
|
+
<HomeSolid class="w-3 h-3" />
|
|
62
63
|
{crumb.name}
|
|
63
64
|
</a>
|
|
64
65
|
{:else if index === data.length - 1}
|
|
65
66
|
<!-- Last item - non-clickable -->
|
|
66
|
-
<span class={
|
|
67
|
+
<span class={`${typography.smMuted} font-medium max-w-48 truncate`} title={crumb.name}>
|
|
67
68
|
{crumb.name}
|
|
68
69
|
</span>
|
|
69
70
|
{:else}
|
|
@@ -71,7 +72,7 @@
|
|
|
71
72
|
<a
|
|
72
73
|
href={crumb.href}
|
|
73
74
|
onclick={() => handleClick(crumb)}
|
|
74
|
-
class="{typography.smMuted}
|
|
75
|
+
class="{typography.smMuted} font-medium hover:text-blue-600 dark:hover:text-white max-w-48 truncate"
|
|
75
76
|
title={crumb.name}
|
|
76
77
|
>
|
|
77
78
|
{crumb.name}
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
* - landing-secondary: Secondary hero CTA (outline, shadow, rounded-xl)
|
|
24
24
|
*
|
|
25
25
|
* Sizes (Flowbite native): xs, sm, md, lg, xl, landing
|
|
26
|
+
*
|
|
27
|
+
* Haptic Feedback:
|
|
28
|
+
* - Automatic haptic on success state transition
|
|
29
|
+
* - Optional haptic on click via `haptic` prop
|
|
30
|
+
* - Variant-aware feedback intensity
|
|
26
31
|
*/
|
|
27
32
|
import { CheckOutline } from '../Icons';
|
|
28
33
|
import { twMerge } from 'tailwind-merge';
|
|
@@ -34,6 +39,7 @@
|
|
|
34
39
|
buttonMenuItemSizes,
|
|
35
40
|
buttonCardSizes,
|
|
36
41
|
} from '../../tokens/sizing.js';
|
|
42
|
+
import { triggerHaptic, getHapticForButtonVariant } from '../../utils/haptic.js';
|
|
37
43
|
|
|
38
44
|
interface Props {
|
|
39
45
|
variant?: string;
|
|
@@ -45,6 +51,8 @@
|
|
|
45
51
|
active?: boolean;
|
|
46
52
|
href?: string | null;
|
|
47
53
|
type?: 'button' | 'submit' | 'reset';
|
|
54
|
+
/** Enable haptic feedback on click. When true, uses variant-aware haptic style. */
|
|
55
|
+
haptic?: boolean;
|
|
48
56
|
children?: Snippet;
|
|
49
57
|
trailing?: Snippet;
|
|
50
58
|
class?: string;
|
|
@@ -62,6 +70,7 @@
|
|
|
62
70
|
active = false,
|
|
63
71
|
href = null,
|
|
64
72
|
type = "button",
|
|
73
|
+
haptic = false,
|
|
65
74
|
children,
|
|
66
75
|
trailing,
|
|
67
76
|
class: className = "",
|
|
@@ -69,6 +78,17 @@
|
|
|
69
78
|
...restProps
|
|
70
79
|
}: Props = $props();
|
|
71
80
|
|
|
81
|
+
// Track previous success state to detect transitions
|
|
82
|
+
let prevSuccess = $state(false);
|
|
83
|
+
|
|
84
|
+
// Trigger haptic on success state transition (QOL Bible)
|
|
85
|
+
$effect(() => {
|
|
86
|
+
if (success && !prevSuccess) {
|
|
87
|
+
triggerHaptic('success');
|
|
88
|
+
}
|
|
89
|
+
prevSuccess = success;
|
|
90
|
+
});
|
|
91
|
+
|
|
72
92
|
// Legacy variant name mapping
|
|
73
93
|
const variantMap: Record<string, string> = {
|
|
74
94
|
"blue-solid": "default",
|
|
@@ -181,6 +201,17 @@ let sizeClass = $derived((() => {
|
|
|
181
201
|
let isLandingVariant = $derived(resolvedVariant === "landing" || resolvedVariant === "landing-secondary");
|
|
182
202
|
let roundedClass = $derived(isLandingVariant ? "rounded-xl" : "rounded-lg");
|
|
183
203
|
|
|
204
|
+
// Click handler with optional haptic feedback
|
|
205
|
+
function handleClick(e: MouseEvent) {
|
|
206
|
+
if (haptic && !effectiveDisabled) {
|
|
207
|
+
const hapticStyle = getHapticForButtonVariant(resolvedVariant);
|
|
208
|
+
if (hapticStyle) {
|
|
209
|
+
triggerHaptic(hapticStyle);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
onclick?.(e);
|
|
213
|
+
}
|
|
214
|
+
|
|
184
215
|
let buttonClasses = $derived(twMerge(
|
|
185
216
|
"relative",
|
|
186
217
|
isLeftAligned
|
|
@@ -204,7 +235,7 @@ let sizeClass = $derived((() => {
|
|
|
204
235
|
<a
|
|
205
236
|
{href}
|
|
206
237
|
class="{buttonClasses} {loading ? 'loading-pulse' : ''}"
|
|
207
|
-
{
|
|
238
|
+
onclick={handleClick}
|
|
208
239
|
{...restProps}
|
|
209
240
|
>
|
|
210
241
|
{#if loading}
|
|
@@ -220,7 +251,7 @@ let sizeClass = $derived((() => {
|
|
|
220
251
|
{type}
|
|
221
252
|
class="{buttonClasses} {loading ? 'loading-pulse' : ''}"
|
|
222
253
|
disabled={effectiveDisabled}
|
|
223
|
-
{
|
|
254
|
+
onclick={handleClick}
|
|
224
255
|
{...restProps}
|
|
225
256
|
>
|
|
226
257
|
{#if loading}
|
|
@@ -9,6 +9,8 @@ interface Props {
|
|
|
9
9
|
active?: boolean;
|
|
10
10
|
href?: string | null;
|
|
11
11
|
type?: 'button' | 'submit' | 'reset';
|
|
12
|
+
/** Enable haptic feedback on click. When true, uses variant-aware haptic style. */
|
|
13
|
+
haptic?: boolean;
|
|
12
14
|
children?: Snippet;
|
|
13
15
|
trailing?: Snippet;
|
|
14
16
|
class?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Button.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Button/Button.svelte.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Button.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Button/Button.svelte.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAWpC,UAAU,KAAK;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;IACrC,mFAAmF;IACnF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IAClC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAqNH,QAAA,MAAM,MAAM,2CAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Modal Component - Flowbite Native
|
|
4
4
|
* Migrated to Svelte 5 runes
|
|
5
|
+
*
|
|
6
|
+
* Haptic Feedback:
|
|
7
|
+
* - Light haptic on modal open (subtle attention cue)
|
|
5
8
|
*/
|
|
6
9
|
import { onDestroy } from "svelte";
|
|
7
10
|
import { fade, fly } from "svelte/transition";
|
|
8
11
|
import { cubicOut } from "svelte/easing";
|
|
9
12
|
import { portal } from "../../utils/portal.js";
|
|
13
|
+
import { triggerHaptic } from "../../utils/haptic.js";
|
|
10
14
|
|
|
11
15
|
/** @type {{
|
|
12
16
|
show?: boolean,
|
|
@@ -14,6 +18,7 @@
|
|
|
14
18
|
isSuccess?: boolean,
|
|
15
19
|
size?: 'default' | 'small' | 'large' | 'xlarge',
|
|
16
20
|
persistent?: boolean,
|
|
21
|
+
haptic?: boolean,
|
|
17
22
|
oncancel?: () => void,
|
|
18
23
|
header?: import('svelte').Snippet,
|
|
19
24
|
body?: import('svelte').Snippet,
|
|
@@ -26,6 +31,7 @@
|
|
|
26
31
|
isSuccess = false,
|
|
27
32
|
size = "default",
|
|
28
33
|
persistent = false,
|
|
34
|
+
haptic = true,
|
|
29
35
|
oncancel,
|
|
30
36
|
header,
|
|
31
37
|
body,
|
|
@@ -37,6 +43,17 @@
|
|
|
37
43
|
// Store scroll position for iOS scroll lock
|
|
38
44
|
let scrollY = $state(0);
|
|
39
45
|
|
|
46
|
+
// Track previous show state to detect open transitions
|
|
47
|
+
let prevShow = $state(false);
|
|
48
|
+
|
|
49
|
+
// Trigger haptic on modal open (QOL Bible)
|
|
50
|
+
$effect(() => {
|
|
51
|
+
if (show && !prevShow && haptic) {
|
|
52
|
+
triggerHaptic('light');
|
|
53
|
+
}
|
|
54
|
+
prevShow = show;
|
|
55
|
+
});
|
|
56
|
+
|
|
40
57
|
// Handle escape key
|
|
41
58
|
function handleKeydown(event) {
|
|
42
59
|
if (event.key === "Escape" && show && !persistent) {
|
|
@@ -7,6 +7,7 @@ type Modal = {
|
|
|
7
7
|
isSuccess?: boolean | undefined;
|
|
8
8
|
size?: "small" | "large" | "default" | "xlarge" | undefined;
|
|
9
9
|
persistent?: boolean | undefined;
|
|
10
|
+
haptic?: boolean | undefined;
|
|
10
11
|
oncancel?: (() => void) | undefined;
|
|
11
12
|
header?: Snippet<[]> | undefined;
|
|
12
13
|
body?: Snippet<[]> | undefined;
|
|
@@ -20,6 +21,7 @@ declare const Modal: import("svelte").Component<{
|
|
|
20
21
|
isSuccess?: boolean;
|
|
21
22
|
size?: "default" | "small" | "large" | "xlarge";
|
|
22
23
|
persistent?: boolean;
|
|
24
|
+
haptic?: boolean;
|
|
23
25
|
oncancel?: () => void;
|
|
24
26
|
header?: import("svelte").Snippet;
|
|
25
27
|
body?: import("svelte").Snippet;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Modal.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Modal/Modal.svelte.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Modal.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/primitives/Modal/Modal.svelte.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAsLA;WAZW,OAAO;mBACC,OAAO;gBACV,OAAO;WACZ,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ;iBAClC,OAAO;aACX,OAAO;eACL,MAAM,IAAI;aACZ,OAAO,QAAQ,EAAE,OAAO;WAC1B,OAAO,QAAQ,EAAE,OAAO;aACtB,OAAO,QAAQ,EAAE,OAAO;YACzB,MAAM;eAEkC"}
|
|
@@ -6,12 +6,17 @@
|
|
|
6
6
|
* Note: Uses CSS style block for pseudo-element styling instead of Tailwind
|
|
7
7
|
* after: classes, because Tailwind v4 doesn't generate after: classes from
|
|
8
8
|
* node_modules when this component is consumed by other apps.
|
|
9
|
+
*
|
|
10
|
+
* Haptic Feedback:
|
|
11
|
+
* - Selection haptic on state change (very light, tactile confirmation)
|
|
9
12
|
*/
|
|
13
|
+
import { triggerHaptic } from '../utils/haptic.js';
|
|
10
14
|
|
|
11
15
|
/** @type {{
|
|
12
16
|
checked?: boolean,
|
|
13
17
|
disabled?: boolean,
|
|
14
18
|
size?: 'sm' | 'md' | 'lg',
|
|
19
|
+
haptic?: boolean,
|
|
15
20
|
class?: string,
|
|
16
21
|
onchange?: (detail: { checked: boolean }) => void,
|
|
17
22
|
children?: import('svelte').Snippet,
|
|
@@ -20,6 +25,7 @@
|
|
|
20
25
|
checked = $bindable(false),
|
|
21
26
|
disabled = false,
|
|
22
27
|
size = 'md',
|
|
28
|
+
haptic = true,
|
|
23
29
|
class: className = '',
|
|
24
30
|
onchange,
|
|
25
31
|
children,
|
|
@@ -28,6 +34,10 @@
|
|
|
28
34
|
|
|
29
35
|
function handleChange(event) {
|
|
30
36
|
checked = event.target.checked;
|
|
37
|
+
// Haptic feedback on toggle change (QOL Bible)
|
|
38
|
+
if (haptic && !disabled) {
|
|
39
|
+
triggerHaptic('selection');
|
|
40
|
+
}
|
|
31
41
|
onchange?.({ checked });
|
|
32
42
|
}
|
|
33
43
|
</script>
|
|
@@ -5,6 +5,7 @@ type Toggle = {
|
|
|
5
5
|
checked?: boolean | undefined;
|
|
6
6
|
disabled?: boolean | undefined;
|
|
7
7
|
size?: "sm" | "md" | "lg" | undefined;
|
|
8
|
+
haptic?: boolean | undefined;
|
|
8
9
|
class?: string | undefined;
|
|
9
10
|
onchange?: ((detail: {
|
|
10
11
|
checked: boolean;
|
|
@@ -16,6 +17,7 @@ declare const Toggle: import("svelte").Component<{
|
|
|
16
17
|
checked?: boolean;
|
|
17
18
|
disabled?: boolean;
|
|
18
19
|
size?: "sm" | "md" | "lg";
|
|
20
|
+
haptic?: boolean;
|
|
19
21
|
class?: string;
|
|
20
22
|
onchange?: (detail: {
|
|
21
23
|
checked: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Toggle.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/primitives/Toggle.svelte.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"Toggle.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/primitives/Toggle.svelte.js"],"names":[],"mappings":";;;;;;;;;;;;;;;AAwEA;cARc,OAAO;eACN,OAAO;WACX,IAAI,GAAG,IAAI,GAAG,IAAI;aAChB,OAAO;YACR,MAAM;eACH,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI;eACtC,OAAO,QAAQ,EAAE,OAAO;kBAEc"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { passwordStrength } from "check-password-strength";
|
|
4
4
|
import { safeSlide } from "../../../utils/transitions.js";
|
|
5
5
|
import { cubicOut } from "svelte/easing";
|
|
6
|
+
import { untrack } from "svelte";
|
|
6
7
|
|
|
7
8
|
interface Props {
|
|
8
9
|
password?: string;
|
|
@@ -21,7 +22,8 @@
|
|
|
21
22
|
}: Props = $props();
|
|
22
23
|
|
|
23
24
|
let debouncedPassword = $state("");
|
|
24
|
-
|
|
25
|
+
// Use a plain variable for timer to avoid reactive dependency
|
|
26
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
25
27
|
|
|
26
28
|
const customOptions = [
|
|
27
29
|
{
|
|
@@ -50,7 +52,7 @@
|
|
|
50
52
|
},
|
|
51
53
|
] as const;
|
|
52
54
|
|
|
53
|
-
// Debounce password updates
|
|
55
|
+
// Debounce password updates - timer is not reactive to avoid dependency cycle
|
|
54
56
|
$effect(() => {
|
|
55
57
|
clearTimeout(timer);
|
|
56
58
|
if (password.length === 0) {
|
|
@@ -67,33 +69,45 @@
|
|
|
67
69
|
? passwordStrength(debouncedPassword, customOptions as any)
|
|
68
70
|
: null);
|
|
69
71
|
|
|
70
|
-
//
|
|
72
|
+
// Derive score based on password length and strength
|
|
73
|
+
let computedScore = $derived(debouncedPassword.length > 12 ? 3 : (strength?.id ?? -1));
|
|
74
|
+
|
|
75
|
+
// Sync computed values to bindable props using effects with untrack
|
|
71
76
|
$effect(() => {
|
|
72
|
-
|
|
77
|
+
const newScore = computedScore;
|
|
78
|
+
if (untrack(() => score) !== newScore) {
|
|
79
|
+
score = newScore;
|
|
80
|
+
}
|
|
73
81
|
});
|
|
74
82
|
|
|
75
|
-
//
|
|
83
|
+
// Derive text and color from computedScore (not from bindable score)
|
|
84
|
+
let computedStrengthText = $derived(
|
|
85
|
+
computedScore === 0 ? "Too weak" :
|
|
86
|
+
computedScore === 1 ? "Weak" :
|
|
87
|
+
computedScore === 2 ? "Good" :
|
|
88
|
+
computedScore === 3 ? "Strong" : ""
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
let computedTextColor = $derived(computedScore <= 1 ? "text-red-600" : "text-green-600");
|
|
92
|
+
|
|
76
93
|
$effect(() => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
? "Weak"
|
|
82
|
-
: score === 2
|
|
83
|
-
? "Good"
|
|
84
|
-
: score === 3
|
|
85
|
-
? "Strong"
|
|
86
|
-
: "";
|
|
94
|
+
const newText = computedStrengthText;
|
|
95
|
+
if (untrack(() => strengthText) !== newText) {
|
|
96
|
+
strengthText = newText;
|
|
97
|
+
}
|
|
87
98
|
});
|
|
88
99
|
|
|
89
|
-
let strengthColor = $derived(
|
|
100
|
+
let strengthColor = $derived(computedScore <= 1 ? "bg-red-600" : "bg-green-600");
|
|
90
101
|
|
|
91
102
|
$effect(() => {
|
|
92
|
-
|
|
103
|
+
const newColor = computedTextColor;
|
|
104
|
+
if (untrack(() => textColor) !== newColor) {
|
|
105
|
+
textColor = newColor;
|
|
106
|
+
}
|
|
93
107
|
});
|
|
94
108
|
|
|
95
109
|
// Calculate how many bars to fill (1-3)
|
|
96
|
-
let filledBars = $derived(
|
|
110
|
+
let filledBars = $derived(computedScore === 0 ? 1 : computedScore === 1 ? 2 : computedScore >= 2 ? 3 : 0);
|
|
97
111
|
</script>
|
|
98
112
|
|
|
99
113
|
{#if debouncedPassword.length > 0}
|
package/dist/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.svelte.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PasswordStrengthIndicator.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"PasswordStrengthIndicator.svelte.d.ts","sourceRoot":"","sources":["../../../../src/lib/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAOlC,UAAU,KAAK;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA2HL,QAAA,MAAM,yBAAyB,+EAAwC,CAAC;AACxE,KAAK,yBAAyB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC9E,eAAe,yBAAyB,CAAC"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import Cancel from "../../assets/svg/cancel.svg";
|
|
4
4
|
import Modal from "../../primitives/Modal/Modal.svelte";
|
|
5
5
|
import { typography } from '../../tokens/typography';
|
|
6
|
+
import { triggerHaptic } from '../../utils/haptic.js';
|
|
6
7
|
|
|
7
8
|
let {
|
|
8
9
|
show = $bindable(false),
|
|
@@ -19,6 +20,8 @@
|
|
|
19
20
|
variant = "default",
|
|
20
21
|
loading = false,
|
|
21
22
|
disabled = false,
|
|
23
|
+
/** Enable haptic feedback on action clicks (default: true) */
|
|
24
|
+
haptic = true,
|
|
22
25
|
onconfirm,
|
|
23
26
|
oncancel,
|
|
24
27
|
onclose,
|
|
@@ -54,6 +57,14 @@
|
|
|
54
57
|
|
|
55
58
|
const handleAction = (action) => {
|
|
56
59
|
if (disabled || loading) return;
|
|
60
|
+
|
|
61
|
+
// Trigger haptic feedback based on action type (QOL Bible)
|
|
62
|
+
if (haptic) {
|
|
63
|
+
const actionVariant = getVariant(action);
|
|
64
|
+
const isDanger = actionVariant === 'red' || actionVariant === 'red-outline' || actionVariant === 'ghost-red';
|
|
65
|
+
triggerHaptic(isDanger ? 'heavy' : 'medium');
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
action.onClick?.();
|
|
58
69
|
show = false;
|
|
59
70
|
};
|
|
@@ -98,7 +109,7 @@
|
|
|
98
109
|
|
|
99
110
|
<Modal bind:show {size} oncancel={handleClose} {...restProps}>
|
|
100
111
|
{#snippet header()}
|
|
101
|
-
<div
|
|
112
|
+
<div>
|
|
102
113
|
{#if closeBtn}
|
|
103
114
|
<div class="flex justify-end -mt-2 -mr-2 mb-2">
|
|
104
115
|
<Button variant="icon" size="xs" onclick={handleClose} {disabled}>
|
|
@@ -118,7 +129,7 @@
|
|
|
118
129
|
{/snippet}
|
|
119
130
|
|
|
120
131
|
{#snippet body()}
|
|
121
|
-
<div class="
|
|
132
|
+
<div class="mt-4">
|
|
122
133
|
{#if description}
|
|
123
134
|
<p class={`${typography.smMuted} leading-relaxed`}>
|
|
124
135
|
{description}
|
|
@@ -133,11 +144,10 @@
|
|
|
133
144
|
{/snippet}
|
|
134
145
|
|
|
135
146
|
{#snippet footer()}
|
|
136
|
-
<div class="flex gap-3">
|
|
147
|
+
<div class="flex justify-end gap-3">
|
|
137
148
|
{#each resolvedActions as action}
|
|
138
149
|
<Button
|
|
139
150
|
size="md"
|
|
140
|
-
class="flex-1"
|
|
141
151
|
variant={getVariant(action)}
|
|
142
152
|
{...cleanActionProps(action)}
|
|
143
153
|
disabled={disabled || action.disabled}
|
|
@@ -18,6 +18,7 @@ declare const ConfirmationModal: import("svelte").Component<{
|
|
|
18
18
|
variant?: string;
|
|
19
19
|
loading?: boolean;
|
|
20
20
|
disabled?: boolean;
|
|
21
|
+
haptic?: boolean;
|
|
21
22
|
onconfirm: any;
|
|
22
23
|
oncancel: any;
|
|
23
24
|
onclose: any;
|
|
@@ -37,6 +38,7 @@ type $$ComponentProps = {
|
|
|
37
38
|
variant?: string;
|
|
38
39
|
loading?: boolean;
|
|
39
40
|
disabled?: boolean;
|
|
41
|
+
haptic?: boolean;
|
|
40
42
|
onconfirm: any;
|
|
41
43
|
oncancel: any;
|
|
42
44
|
onclose: any;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConfirmationModal.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/recipes/modals/ConfirmationModal.svelte.js"],"names":[],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"ConfirmationModal.svelte.d.ts","sourceRoot":"","sources":["../../../src/lib/recipes/modals/ConfirmationModal.svelte.js"],"names":[],"mappings":";;;;;AAuKA;WAvJ4B,OAAO;WAAS,MAAM;YAAU,MAAM;kBAAgB,MAAM;kBAAgB,MAAM;cAAY,GAAG,EAAE;WAAS,GAAG;iBAAe,GAAG;eAAa,OAAO;wBAAsB,MAAM;0BAAwB,MAAM;cAAY,MAAM;cAAY,OAAO;eAAa,OAAO;aAAW,OAAO;eAAa,GAAG;cAAY,GAAG;aAAW,GAAG;qCAuJnS;wBAvJ7C;IAAE,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,GAAG,CAAC;IAAC,QAAQ,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Haptic Feedback Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides tactile feedback for user actions across iOS, Android, and web.
|
|
5
|
+
* Part of QOL Bible - "Instant Response to Every Touch"
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - iOS WebKit (native app wrapper)
|
|
9
|
+
* - iOS TapticEngine (older API)
|
|
10
|
+
* - Android/Web Vibration API (fallback)
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* import { triggerHaptic } from './haptic';
|
|
14
|
+
* triggerHaptic('success'); // On form submission success
|
|
15
|
+
* triggerHaptic('light'); // On toggle/selection
|
|
16
|
+
* triggerHaptic('heavy'); // On destructive action confirmation
|
|
17
|
+
*/
|
|
18
|
+
export type HapticStyle = 'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error' | 'selection';
|
|
19
|
+
/**
|
|
20
|
+
* Trigger haptic feedback
|
|
21
|
+
*
|
|
22
|
+
* @param style - The haptic intensity/pattern
|
|
23
|
+
* - light: Subtle confirmation (toggles, selections)
|
|
24
|
+
* - medium: Standard confirmation (form saves, status changes)
|
|
25
|
+
* - heavy: Strong confirmation (destructive actions, transfers)
|
|
26
|
+
* - success: Double-tap pattern for achievements
|
|
27
|
+
* - warning: Alert pattern
|
|
28
|
+
* - error: Triple-pulse for errors
|
|
29
|
+
* - selection: Very light for toggle/checkbox state changes
|
|
30
|
+
*/
|
|
31
|
+
export declare function triggerHaptic(style?: HapticStyle): void;
|
|
32
|
+
/**
|
|
33
|
+
* Check if haptic feedback is available on this device
|
|
34
|
+
*/
|
|
35
|
+
export declare function isHapticAvailable(): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Map button variants to appropriate haptic styles
|
|
38
|
+
* Used by Button component to provide context-aware feedback
|
|
39
|
+
*/
|
|
40
|
+
export declare function getHapticForButtonVariant(variant: string, isSuccess?: boolean): HapticStyle | null;
|
|
41
|
+
//# sourceMappingURL=haptic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"haptic.d.ts","sourceRoot":"","sources":["../../src/lib/utils/haptic.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,MAAM,MAAM,WAAW,GACnB,OAAO,GACP,QAAQ,GACR,OAAO,GACP,SAAS,GACT,SAAS,GACT,OAAO,GACP,WAAW,CAAC;AAahB;;;;;;;;;;;GAWG;AACH,wBAAgB,aAAa,CAAC,KAAK,GAAE,WAAqB,GAAG,IAAI,CAgChE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAU3C;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,MAAM,EACf,SAAS,CAAC,EAAE,OAAO,GAClB,WAAW,GAAG,IAAI,CA4BpB"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Haptic Feedback Utility
|
|
3
|
+
*
|
|
4
|
+
* Provides tactile feedback for user actions across iOS, Android, and web.
|
|
5
|
+
* Part of QOL Bible - "Instant Response to Every Touch"
|
|
6
|
+
*
|
|
7
|
+
* Supports:
|
|
8
|
+
* - iOS WebKit (native app wrapper)
|
|
9
|
+
* - iOS TapticEngine (older API)
|
|
10
|
+
* - Android/Web Vibration API (fallback)
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* import { triggerHaptic } from './haptic';
|
|
14
|
+
* triggerHaptic('success'); // On form submission success
|
|
15
|
+
* triggerHaptic('light'); // On toggle/selection
|
|
16
|
+
* triggerHaptic('heavy'); // On destructive action confirmation
|
|
17
|
+
*/
|
|
18
|
+
// Vibration durations in ms for each style
|
|
19
|
+
const VIBRATION_PATTERNS = {
|
|
20
|
+
light: 10,
|
|
21
|
+
medium: 20,
|
|
22
|
+
heavy: 30,
|
|
23
|
+
success: [10, 50, 20], // double tap feel
|
|
24
|
+
warning: [20, 40, 20],
|
|
25
|
+
error: [30, 50, 30, 50, 30], // triple pulse
|
|
26
|
+
selection: 8, // very light for toggles/checkboxes
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Trigger haptic feedback
|
|
30
|
+
*
|
|
31
|
+
* @param style - The haptic intensity/pattern
|
|
32
|
+
* - light: Subtle confirmation (toggles, selections)
|
|
33
|
+
* - medium: Standard confirmation (form saves, status changes)
|
|
34
|
+
* - heavy: Strong confirmation (destructive actions, transfers)
|
|
35
|
+
* - success: Double-tap pattern for achievements
|
|
36
|
+
* - warning: Alert pattern
|
|
37
|
+
* - error: Triple-pulse for errors
|
|
38
|
+
* - selection: Very light for toggle/checkbox state changes
|
|
39
|
+
*/
|
|
40
|
+
export function triggerHaptic(style = 'light') {
|
|
41
|
+
if (typeof window === 'undefined')
|
|
42
|
+
return;
|
|
43
|
+
// iOS WebKit bridge (native app wrapper)
|
|
44
|
+
// @ts-expect-error - iOS WebKit
|
|
45
|
+
if (window.webkit?.messageHandlers?.haptic) {
|
|
46
|
+
// @ts-expect-error - iOS WebKit
|
|
47
|
+
window.webkit.messageHandlers.haptic.postMessage(style);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// iOS TapticEngine (older native API)
|
|
51
|
+
// @ts-expect-error - Taptic Engine
|
|
52
|
+
if (window.TapticEngine) {
|
|
53
|
+
// Map our styles to TapticEngine styles
|
|
54
|
+
const tapticStyle = style === 'success' ||
|
|
55
|
+
style === 'warning' ||
|
|
56
|
+
style === 'error' ||
|
|
57
|
+
style === 'selection'
|
|
58
|
+
? 'medium'
|
|
59
|
+
: style;
|
|
60
|
+
// @ts-expect-error - Taptic Engine
|
|
61
|
+
window.TapticEngine.impact({ style: tapticStyle });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Android/Web Vibration API fallback
|
|
65
|
+
if (navigator.vibrate) {
|
|
66
|
+
const pattern = VIBRATION_PATTERNS[style];
|
|
67
|
+
navigator.vibrate(pattern);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Check if haptic feedback is available on this device
|
|
72
|
+
*/
|
|
73
|
+
export function isHapticAvailable() {
|
|
74
|
+
if (typeof window === 'undefined')
|
|
75
|
+
return false;
|
|
76
|
+
return !!(
|
|
77
|
+
// @ts-expect-error - iOS WebKit
|
|
78
|
+
window.webkit?.messageHandlers?.haptic ||
|
|
79
|
+
// @ts-expect-error - Taptic Engine
|
|
80
|
+
window.TapticEngine ||
|
|
81
|
+
navigator.vibrate);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Map button variants to appropriate haptic styles
|
|
85
|
+
* Used by Button component to provide context-aware feedback
|
|
86
|
+
*/
|
|
87
|
+
export function getHapticForButtonVariant(variant, isSuccess) {
|
|
88
|
+
if (isSuccess)
|
|
89
|
+
return 'success';
|
|
90
|
+
switch (variant) {
|
|
91
|
+
case 'red':
|
|
92
|
+
case 'red-outline':
|
|
93
|
+
case 'ghost-red':
|
|
94
|
+
case 'menu-item-danger':
|
|
95
|
+
return 'heavy'; // Destructive actions get strong feedback
|
|
96
|
+
case 'default':
|
|
97
|
+
case 'outline':
|
|
98
|
+
case 'landing':
|
|
99
|
+
return 'medium'; // Primary actions get standard feedback
|
|
100
|
+
case 'alternative':
|
|
101
|
+
case 'ghost':
|
|
102
|
+
case 'landing-secondary':
|
|
103
|
+
return 'light'; // Secondary actions get subtle feedback
|
|
104
|
+
case 'toggle':
|
|
105
|
+
case 'nav':
|
|
106
|
+
return 'selection'; // Selection changes get very light feedback
|
|
107
|
+
case 'icon':
|
|
108
|
+
case 'menu-item':
|
|
109
|
+
case 'search-result':
|
|
110
|
+
case 'link':
|
|
111
|
+
return null; // These don't need haptic (too frequent or navigation)
|
|
112
|
+
default:
|
|
113
|
+
return 'light';
|
|
114
|
+
}
|
|
115
|
+
}
|