@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.
Files changed (29) hide show
  1. package/dist/components/Layout/Stack.spec.js +1 -1
  2. package/dist/datetime/__tests__/format.test.js +1 -1
  3. package/dist/datetime/__tests__/parse.test.js +1 -1
  4. package/dist/datetime/__tests__/timezone.test.js +1 -1
  5. package/dist/datetime/parse.js +1 -1
  6. package/dist/forms/createFormStore.svelte.js +0 -1
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +6 -0
  10. package/dist/primitives/Breadcrumb/Breadcrumb.spec.js +6 -5
  11. package/dist/primitives/Breadcrumb/Breadcrumb.svelte +9 -8
  12. package/dist/primitives/Button/Button.svelte +33 -2
  13. package/dist/primitives/Button/Button.svelte.d.ts +2 -0
  14. package/dist/primitives/Button/Button.svelte.d.ts.map +1 -1
  15. package/dist/primitives/Modal/Modal.svelte +17 -0
  16. package/dist/primitives/Modal/Modal.svelte.d.ts +2 -0
  17. package/dist/primitives/Modal/Modal.svelte.d.ts.map +1 -1
  18. package/dist/primitives/Toggle.svelte +10 -0
  19. package/dist/primitives/Toggle.svelte.d.ts +2 -0
  20. package/dist/primitives/Toggle.svelte.d.ts.map +1 -1
  21. package/dist/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.svelte +32 -18
  22. package/dist/recipes/inputs/PasswordStrengthIndicator/PasswordStrengthIndicator.svelte.d.ts.map +1 -1
  23. package/dist/recipes/modals/ConfirmationModal.svelte +14 -4
  24. package/dist/recipes/modals/ConfirmationModal.svelte.d.ts +2 -0
  25. package/dist/recipes/modals/ConfirmationModal.svelte.d.ts.map +1 -1
  26. package/dist/utils/haptic.d.ts +41 -0
  27. package/dist/utils/haptic.d.ts.map +1 -0
  28. package/dist/utils/haptic.js +115 -0
  29. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
- import { render, screen } from '@testing-library/svelte';
1
+ import { render } from '@testing-library/svelte';
2
2
  import { expect, describe, test } from 'vitest';
3
3
  import Stack from './Stack.svelte';
4
4
  describe('Stack Component', () => {
@@ -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, DateTimeErrorCode } from '../types';
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, DateTimeErrorCode } from '../types';
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 { beforeEach, describe, expect, it, vi } from 'vitest';
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', () => {
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module datetime/parse
8
8
  */
9
- import { format, parse } from 'date-fns';
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
@@ -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 arrows between items", () => {
51
+ test("Shows separator characters between items", () => {
52
52
  setupTest();
53
- // Arrows appear between items (n-1 arrows for n items)
54
- const svgs = document.querySelectorAll("svg");
55
- // 1 home icon + 2 chevron arrows = 3 SVGs minimum
56
- expect(svgs.length).toBeGreaterThanOrEqual(3);
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 space-x-1 md:space-x-2 rtl:space-x-reverse flex-wrap">
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
- <ChevronRightOutline class="rtl:rotate-180 w-3 h-3 text-gray-400 mx-1" />
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 me-2.5" />
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 me-2.5" />
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={`ms-1 ${typography.smMuted} font-medium md:ms-2 max-w-48 truncate`} title={crumb.name}>
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} ms-1 font-medium hover:text-blue-600 md:ms-2 dark:hover:text-white max-w-48 truncate"
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
- {onclick}
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
- {onclick}
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":"AA8BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAUpC,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,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;AA6LH,QAAA,MAAM,MAAM,2CAAwC,CAAC;AACrD,KAAK,MAAM,GAAG,UAAU,CAAC,OAAO,MAAM,CAAC,CAAC;AACxC,eAAe,MAAM,CAAC"}
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":";;;;;;;;;;;;;;;;AAmKA;WAXW,OAAO;mBACC,OAAO;gBACV,OAAO;WACZ,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ;iBAClC,OAAO;eACT,MAAM,IAAI;aACZ,OAAO,QAAQ,EAAE,OAAO;WAC1B,OAAO,QAAQ,EAAE,OAAO;aACtB,OAAO,QAAQ,EAAE,OAAO;YACzB,MAAM;eAEkC"}
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":";;;;;;;;;;;;;;AA0DA;cAPc,OAAO;eACN,OAAO;WACX,IAAI,GAAG,IAAI,GAAG,IAAI;YACjB,MAAM;eACH,CAAC,MAAM,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI;eACtC,OAAO,QAAQ,EAAE,OAAO;kBAEc"}
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
- let timer = $state<ReturnType<typeof setTimeout> | undefined>();
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
- // Compute score based on password length and strength
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
- score = debouncedPassword.length > 12 ? 3 : (strength?.id ?? -1);
77
+ const newScore = computedScore;
78
+ if (untrack(() => score) !== newScore) {
79
+ score = newScore;
80
+ }
73
81
  });
74
82
 
75
- // Map score to display text and color
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
- strengthText =
78
- score === 0
79
- ? "Too weak"
80
- : score === 1
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(score <= 1 ? "bg-red-600" : "bg-green-600");
100
+ let strengthColor = $derived(computedScore <= 1 ? "bg-red-600" : "bg-green-600");
90
101
 
91
102
  $effect(() => {
92
- textColor = score <= 1 ? "text-red-600" : "text-green-600";
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(score === 0 ? 1 : score === 1 ? 2 : score >= 2 ? 3 : 0);
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}
@@ -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;AAMlC,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;AA6GL,QAAA,MAAM,yBAAyB,+EAAwC,CAAC;AACxE,KAAK,yBAAyB,GAAG,UAAU,CAAC,OAAO,yBAAyB,CAAC,CAAC;AAC9E,eAAe,yBAAyB,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 class="text-center">
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="text-center mt-4">
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":";;;;;AA2JA;WA7I4B,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;eAAa,GAAG;cAAY,GAAG;aAAW,GAAG;qCA6IjR;wBA7I7C;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,SAAS,EAAE,GAAG,CAAC;IAAC,QAAQ,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC"}
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmicdrop/svelte-components",
3
- "version": "5.10.1",
3
+ "version": "5.12.0",
4
4
  "description": "Shared component library for Micdrop applications",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",