@widgetic/design-system 0.5.23 → 0.5.25

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 (57) hide show
  1. package/dist/components/CopyableHeader.svelte +5 -3
  2. package/dist/components/index.d.ts +1 -1
  3. package/dist/components/index.js +1 -1
  4. package/dist/components/modals/BaseModal.svelte +257 -166
  5. package/dist/components/modals/BaseModal.svelte.d.ts +3 -0
  6. package/dist/components/modals/NewItemDialog.svelte +5 -7
  7. package/dist/components/modals/Portal.svelte +9 -7
  8. package/dist/components/modals/RenameDialog.svelte +5 -7
  9. package/dist/components/modals/modalStore.d.ts +9 -1
  10. package/dist/components/modals/modalStore.js +44 -11
  11. package/dist/components/navigation/Breadcrumbs.svelte +10 -4
  12. package/dist/components/navigation/breadcrumbsAvailableWidth.d.ts +10 -0
  13. package/dist/components/navigation/breadcrumbsAvailableWidth.js +10 -0
  14. package/dist/components/search-input/SearchInput.svelte +25 -13
  15. package/dist/components/search-input/SearchInput.svelte.d.ts +4 -0
  16. package/dist/components/search-input/index2.d.ts +14 -9
  17. package/dist/components/shell/NavDrawer.svelte +1 -1
  18. package/dist/components/shell/NavRow.svelte +4 -1
  19. package/dist/components/shell/SettingsDialog.svelte +48 -29
  20. package/dist/components/shell/SettingsDialog.svelte.d.ts +0 -4
  21. package/dist/components/shell/WidgetRow.svelte.d.ts +1 -1
  22. package/dist/components/shell/index.d.ts +0 -1
  23. package/dist/components/shell/index.js +0 -1
  24. package/dist/components/shell/nested-modal-guard.d.ts +25 -0
  25. package/dist/components/shell/nested-modal-guard.js +33 -0
  26. package/dist/components/ui/alert/index.d.ts +2 -2
  27. package/dist/components/ui/alert/index.js +4 -1
  28. package/dist/components/ui/avatar/avatar.svelte +22 -3
  29. package/dist/components/ui/button/button.svelte +1 -1
  30. package/dist/components/ui/button/button.svelte.d.ts +2 -2
  31. package/dist/components/ui/calendar/calendar.svelte.d.ts +1 -1
  32. package/dist/components/ui/command/command-item.svelte +1 -1
  33. package/dist/components/ui/command/command-link-item.svelte +1 -1
  34. package/dist/components/ui/dialog/dialog-content.svelte +8 -1
  35. package/dist/components/ui/dialog/dialog-overlay.svelte +8 -1
  36. package/dist/components/ui/drawer/drawer-overlay.svelte +1 -1
  37. package/dist/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +1 -1
  38. package/dist/components/ui/dropdown-menu/dropdown-menu-item.svelte +1 -1
  39. package/dist/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +1 -1
  40. package/dist/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  41. package/dist/components/ui/icon-button/icon-button.svelte +1 -1
  42. package/dist/components/ui/icon-button/icon-button.svelte.d.ts +2 -2
  43. package/dist/components/ui/range-calendar/range-calendar.svelte.d.ts +1 -1
  44. package/dist/components/ui/select/select-content.svelte +5 -0
  45. package/dist/components/ui/select/select-item.svelte +8 -1
  46. package/dist/components/ui/select/select-trigger.svelte +3 -2
  47. package/dist/components/ui/stepper-button/stepper-button.svelte +1 -1
  48. package/dist/components/ui/switch/switch.svelte +4 -1
  49. package/dist/styles/global.css +3 -2
  50. package/dist/styles/tokens.css +21 -1
  51. package/dist/utils/inputModality.d.ts +16 -0
  52. package/dist/utils/inputModality.js +55 -0
  53. package/dist/utils/overlayMotion.d.ts +49 -0
  54. package/dist/utils/overlayMotion.js +71 -0
  55. package/package.json +1 -1
  56. package/dist/components/shell/messages.d.ts +0 -7
  57. package/dist/components/shell/messages.js +0 -5
@@ -50,14 +50,33 @@
50
50
  } = $props();
51
51
 
52
52
  const initials = $derived(avatarInitials(name || alt));
53
+
54
+ // The picture: the initials (or the icon) are always drawn first, and the picture fades in on top of them once it has
55
+ // loaded. A picture that fails (a Gravatar asked with `d=404` answers 404 for everybody who has none, a blocked or an
56
+ // offline request) never shows the browser's broken-image glyph: the initials simply stay. Both states belong to the
57
+ // address that produced them, so a new `src` starts from scratch.
58
+ let loadedSource = $state<string | undefined>(undefined);
59
+ let failedSource = $state<string | undefined>(undefined);
60
+ const showPicture = $derived(Boolean(src) && src !== failedSource);
61
+ const pictureLoaded = $derived(Boolean(src) && src === loadedSource);
53
62
  </script>
54
63
 
55
64
  <span class={cn(avatarVariants({ size, type }), className)} {...restProps}>
56
- {#if src}
57
- <img {src} {alt} class="size-full object-cover" />
58
- {:else if type === "icon" && icon}
65
+ {#if type === "icon" && icon}
59
66
  <span class="flex items-center justify-center">{@render icon()}</span>
60
67
  {:else}
61
68
  <span aria-hidden="true">{initials}</span>
62
69
  {/if}
70
+ {#if showPicture}
71
+ <img
72
+ {src}
73
+ {alt}
74
+ class={cn(
75
+ "absolute inset-0 size-full object-cover transition-opacity duration-base ease-out",
76
+ pictureLoaded ? "opacity-100" : "opacity-0",
77
+ )}
78
+ onload={() => (loadedSource = src)}
79
+ onerror={() => (failedSource = src)}
80
+ />
81
+ {/if}
63
82
  </span>
@@ -13,7 +13,7 @@
13
13
  import type { VariantProps } from "tailwind-variants";
14
14
 
15
15
  export const buttonVariants = tv({
16
- base: "inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-lg text-label transition-colors duration-fast press-feedback focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]",
16
+ base: "inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-lg text-label press-feedback focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]",
17
17
  variants: {
18
18
  variant: {
19
19
  default: "bg-primary text-primary-foreground hover:bg-primary/90",
@@ -23,7 +23,7 @@ export declare const buttonVariants: import("tailwind-variants").TVReturnType<{
23
23
  true: string;
24
24
  false: string;
25
25
  };
26
- }, undefined, "inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-lg text-label transition-colors duration-fast press-feedback focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]", {
26
+ }, undefined, "inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-lg text-label press-feedback focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]", {
27
27
  responsiveVariants?: boolean | (string | number | symbol)[] | {
28
28
  size?: boolean | (string | number | symbol)[] | undefined;
29
29
  variant?: boolean | (string | number | symbol)[] | undefined;
@@ -81,7 +81,7 @@ export declare const buttonVariants: import("tailwind-variants").TVReturnType<{
81
81
  true: string;
82
82
  false: string;
83
83
  };
84
- }, undefined, "inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-lg text-label transition-colors duration-fast press-feedback focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]", import("tailwind-variants/dist/config.js").TVConfig<{
84
+ }, undefined, "inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-lg text-label press-feedback focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]", import("tailwind-variants/dist/config.js").TVConfig<{
85
85
  variant: {
86
86
  default: string;
87
87
  destructive: string;
@@ -6,6 +6,6 @@ export type CalendarProps = WithoutChildrenOrChild<CalendarPrimitive.RootProps>
6
6
  nextLabel?: string;
7
7
  };
8
8
  import * as Calendar from "./index.js";
9
- declare const Calendar: import("svelte").Component<CalendarProps, {}, "placeholder" | "ref" | "value">;
9
+ declare const Calendar: import("svelte").Component<CalendarProps, {}, "ref" | "value" | "placeholder">;
10
10
  type Calendar = ReturnType<typeof Calendar>;
11
11
  export default Calendar;
@@ -15,7 +15,7 @@
15
15
 
16
16
  <CommandPrimitive.Item
17
17
  class={cn(
18
- "text-body aria-selected:bg-accent aria-selected:text-foreground relative flex h-8 cursor-default select-none items-center gap-2 rounded-lg px-2 py-1.5 outline-hidden transition-colors duration-fast data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]",
18
+ "text-body aria-selected:bg-accent aria-selected:text-foreground relative flex h-8 cursor-pointer select-none items-center gap-2 rounded-lg px-2 py-1.5 outline-hidden transition-colors duration-fast data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]",
19
19
  className
20
20
  )}
21
21
  bind:ref
@@ -15,7 +15,7 @@
15
15
 
16
16
  <CommandPrimitive.LinkItem
17
17
  class={cn(
18
- "aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:opacity-50",
18
+ "aria-selected:bg-accent aria-selected:text-accent-foreground relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50",
19
19
  className
20
20
  )}
21
21
  bind:ref
@@ -4,6 +4,10 @@
4
4
  import type { Snippet } from "svelte";
5
5
  import * as Dialog from "./index.js";
6
6
  import { cn } from "../../../utils.js";
7
+ import {
8
+ overlayDialogContentClasses,
9
+ overlayReducedMotionClasses
10
+ } from "../../../utils/overlayMotion.js";
7
11
 
8
12
  let {
9
13
  ref = $bindable(null),
@@ -24,8 +28,11 @@
24
28
  <Dialog.Overlay />
25
29
  <DialogPrimitive.Content
26
30
  bind:ref
31
+ data-widgetic-overlay="dialog-panel"
27
32
  class={cn(
28
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 bg-background border-border fixed start-1/2 top-1/2 z-50 grid w-dialog max-w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-panel border p-6 duration-base",
33
+ "bg-background border-border fixed start-1/2 top-1/2 z-50 grid w-dialog max-w-[calc(100vw-3rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-panel border p-6",
34
+ overlayDialogContentClasses,
35
+ overlayReducedMotionClasses,
29
36
  className
30
37
  )}
31
38
  {...restProps}
@@ -5,6 +5,10 @@
5
5
 
6
6
  <script lang="ts">
7
7
  import { cn } from "../../../utils.js";
8
+ import {
9
+ overlayDialogOverlayClasses,
10
+ overlayReducedMotionClasses
11
+ } from "../../../utils/overlayMotion.js";
8
12
 
9
13
  let {
10
14
  ref = $bindable(null),
@@ -15,8 +19,11 @@
15
19
 
16
20
  <DialogPrimitive.Overlay
17
21
  bind:ref
22
+ data-widgetic-overlay="dialog-backdrop"
18
23
  class={cn(
19
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-foreground/40 backdrop-blur-sm",
24
+ "fixed inset-0 z-50",
25
+ overlayDialogOverlayClasses,
26
+ overlayReducedMotionClasses,
20
27
  className
21
28
  )}
22
29
  {...restProps}
@@ -15,6 +15,6 @@
15
15
 
16
16
  <DrawerPrimitive.Overlay
17
17
  bind:ref
18
- class={cn("fixed inset-0 z-50 bg-foreground/40 backdrop-blur-sm", className)}
18
+ class={cn("fixed inset-0 z-50 bg-muted-foreground/80 backdrop-blur-[3px]", className)}
19
19
  {...restProps}
20
20
  />
@@ -22,7 +22,7 @@
22
22
  bind:checked
23
23
  bind:indeterminate
24
24
  class={cn(
25
- "text-body data-highlighted:bg-accent data-highlighted:text-foreground relative flex h-8 cursor-default select-none items-center rounded-lg py-1.5 ps-8 pe-2 outline-hidden transition-colors duration-fast data-disabled:pointer-events-none data-disabled:opacity-50",
25
+ "text-body data-highlighted:bg-accent data-highlighted:text-foreground relative flex h-8 cursor-pointer select-none items-center rounded-lg py-1.5 ps-8 pe-2 outline-hidden transition-colors duration-fast data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50",
26
26
  className
27
27
  )}
28
28
  {...restProps}
@@ -22,7 +22,7 @@
22
22
  <DropdownMenuPrimitive.Item
23
23
  bind:ref
24
24
  class={cn(
25
- "text-body data-highlighted:bg-accent data-highlighted:text-foreground relative flex cursor-default select-none items-center gap-2 rounded-lg px-2 py-1.5 outline-hidden transition-colors duration-fast data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333] data-[variant=destructive]:text-destructive data-highlighted:data-[variant=destructive]:text-destructive",
25
+ "text-body data-highlighted:bg-accent data-highlighted:text-foreground relative flex cursor-pointer select-none items-center gap-2 rounded-lg px-2 py-1.5 outline-hidden transition-colors duration-fast data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333] data-[variant=destructive]:text-destructive data-highlighted:data-[variant=destructive]:text-destructive",
26
26
  description ? "min-h-12" : "min-h-8",
27
27
  inset && "ps-8",
28
28
  className
@@ -18,7 +18,7 @@
18
18
  <DropdownMenuPrimitive.RadioItem
19
19
  bind:ref
20
20
  class={cn(
21
- "text-body data-highlighted:bg-accent data-highlighted:text-foreground relative flex h-8 cursor-default select-none items-center rounded-lg py-1.5 ps-8 pe-2 outline-hidden transition-colors duration-fast data-disabled:pointer-events-none data-disabled:opacity-50",
21
+ "text-body data-highlighted:bg-accent data-highlighted:text-foreground relative flex h-8 cursor-pointer select-none items-center rounded-lg py-1.5 ps-8 pe-2 outline-hidden transition-colors duration-fast data-disabled:pointer-events-none data-disabled:cursor-not-allowed data-disabled:opacity-50",
22
22
  className
23
23
  )}
24
24
  {...restProps}
@@ -17,7 +17,7 @@
17
17
  <DropdownMenuPrimitive.SubTrigger
18
18
  bind:ref
19
19
  class={cn(
20
- "text-body data-highlighted:bg-accent data-[state=open]:bg-accent flex h-8 cursor-default select-none items-center gap-2 rounded-lg px-2 py-1.5 outline-hidden transition-colors duration-fast [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]",
20
+ "text-body data-highlighted:bg-accent data-[state=open]:bg-accent flex h-8 cursor-pointer select-none items-center gap-2 rounded-lg px-2 py-1.5 outline-hidden transition-colors duration-fast [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]",
21
21
  inset && "ps-8",
22
22
  className
23
23
  )}
@@ -8,7 +8,7 @@
8
8
  import type { VariantProps } from "tailwind-variants";
9
9
 
10
10
  export const iconButtonVariants = tv({
11
- base: "inline-flex shrink-0 items-center justify-center transition-colors duration-fast press-feedback focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]",
11
+ base: "inline-flex shrink-0 items-center justify-center press-feedback focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]",
12
12
  variants: {
13
13
  variant: {
14
14
  ghost:
@@ -13,7 +13,7 @@ export declare const iconButtonVariants: import("tailwind-variants").TVReturnTyp
13
13
  default: string;
14
14
  sm: string;
15
15
  };
16
- }, undefined, "inline-flex shrink-0 items-center justify-center transition-colors duration-fast press-feedback focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]", {
16
+ }, undefined, "inline-flex shrink-0 items-center justify-center press-feedback focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]", {
17
17
  responsiveVariants?: boolean | (string | number | symbol)[] | {
18
18
  size?: boolean | (string | number | symbol)[] | undefined;
19
19
  variant?: boolean | (string | number | symbol)[] | undefined;
@@ -50,7 +50,7 @@ export declare const iconButtonVariants: import("tailwind-variants").TVReturnTyp
50
50
  default: string;
51
51
  sm: string;
52
52
  };
53
- }, undefined, "inline-flex shrink-0 items-center justify-center transition-colors duration-fast press-feedback focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]", import("tailwind-variants/dist/config.js").TVConfig<{
53
+ }, undefined, "inline-flex shrink-0 items-center justify-center press-feedback focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:stroke-[1.333]", import("tailwind-variants/dist/config.js").TVConfig<{
54
54
  variant: {
55
55
  ghost: string;
56
56
  secondary: string;
@@ -6,6 +6,6 @@ export type RangeCalendarProps = WithoutChildrenOrChild<RangeCalendarPrimitive.R
6
6
  nextLabel?: string;
7
7
  };
8
8
  import * as RangeCalendar from "./index.js";
9
- declare const RangeCalendar: import("svelte").Component<RangeCalendarProps, {}, "placeholder" | "ref" | "value">;
9
+ declare const RangeCalendar: import("svelte").Component<RangeCalendarProps, {}, "ref" | "value" | "placeholder">;
10
10
  type RangeCalendar = ReturnType<typeof RangeCalendar>;
11
11
  export default RangeCalendar;
@@ -2,11 +2,16 @@
2
2
  import { Select as SelectPrimitive, type WithoutChild } from "bits-ui";
3
3
  import SelectScrollUpButton from "./select-scroll-up-button.svelte";
4
4
  import SelectScrollDownButton from "./select-scroll-down-button.svelte";
5
+ import { onMount } from "svelte";
5
6
  import { cn } from "../../../utils.js";
6
7
  import { useTextDirection } from "../../textDirection.js";
8
+ import { installInputModality } from "../../../utils/inputModality.js";
7
9
 
8
10
  const textDirection = useTextDirection();
9
11
 
12
+ // The rows paint the highlight only while the keyboard drives the list (see select-item.svelte): this is what tells them so
13
+ onMount(() => installInputModality());
14
+
10
15
  let {
11
16
  ref = $bindable(null),
12
17
  class: className,
@@ -18,11 +18,18 @@
18
18
  }: SelectItemProps = $props();
19
19
  </script>
20
20
 
21
+ <!--
22
+ Which row is painted. bits-ui highlights the row the pointer moves over and never takes the highlight back when the pointer
23
+ leaves, so painting `data-highlighted` left the last row crossed lit after the pointer had gone. So:
24
+ - the pointer paints with :hover, which ends when the pointer does (a selected row keeps its own look);
25
+ - the keyboard paints `data-highlighted`, while the last input was a key press (data-input-modality="keyboard", set by
26
+ utils/inputModality.ts), and the hover of a pointer that is resting somewhere does not paint a second row then.
27
+ -->
21
28
  <SelectPrimitive.Item
22
29
  bind:ref
23
30
  {value}
24
31
  class={cn(
25
- "relative flex h-8 w-full cursor-default select-none items-center gap-2 rounded-lg px-2 py-1.5 ps-8 text-body outline-hidden data-[highlighted]:bg-accent data-[selected]:bg-brand-subtle data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
32
+ "relative flex h-8 w-full cursor-pointer select-none items-center gap-2 rounded-lg px-2 py-1.5 ps-8 text-body outline-hidden data-[selected]:bg-brand-subtle hover:not-data-[selected]:bg-accent [[data-input-modality=keyboard]_&]:not-data-[selected]:data-[highlighted]:bg-accent [[data-input-modality=keyboard]_&]:not-data-[selected]:not-data-[highlighted]:hover:bg-transparent data-[disabled]:pointer-events-none data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50",
26
33
  className
27
34
  )}
28
35
  {...restProps}
@@ -19,11 +19,12 @@
19
19
  <SelectPrimitive.Trigger
20
20
  bind:ref
21
21
  class={cn(
22
- "flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-input bg-background px-3 py-2 text-start text-body transition-colors duration-fast hover:bg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-[placeholder]:text-muted-foreground aria-invalid:border-destructive [&>span]:line-clamp-1",
22
+ "flex h-9 w-full items-center justify-between gap-2 rounded-lg border border-input bg-background px-3 py-2 text-start text-body transition-colors duration-fast hover:bg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-[placeholder]:text-muted-foreground aria-invalid:border-destructive [&>span]:line-clamp-1 [&[data-state=open]>svg:last-child]:rotate-180",
23
23
  className
24
24
  )}
25
25
  {...restProps}
26
26
  >
27
27
  {@render children?.()}
28
- <IconChevronDown class="size-4 shrink-0 stroke-[1.333] opacity-50" aria-hidden="true" />
28
+ <!-- Figma: the open trigger shows the arrow up. It turns with the motion of the accordion's arrow (transition-transform duration-base). -->
29
+ <IconChevronDown class="size-4 shrink-0 stroke-[1.333] opacity-50 transition-transform duration-base" aria-hidden="true" />
29
30
  </SelectPrimitive.Trigger>
@@ -24,7 +24,7 @@
24
24
  aria-label={label}
25
25
  {disabled}
26
26
  class={cn(
27
- "inline-flex size-6 shrink-0 items-center justify-center rounded-lg bg-transparent text-foreground transition-colors duration-fast hover:bg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 press-feedback",
27
+ "inline-flex size-6 shrink-0 items-center justify-center rounded-lg bg-transparent text-foreground hover:bg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 press-feedback",
28
28
  className
29
29
  )}
30
30
  {...restProps}
@@ -18,12 +18,15 @@
18
18
  <!--
19
19
  The track is 36x20. `relative before:absolute before:inset-x-0 before:-inset-y-0.5` adds an invisible 2px above and below,
20
20
  so the tap target is 36x24 (the minimum height of a target). Figma keeps the same 24px frame around the 20px track.
21
+ The thumb (the "scrub") is 16x16 and sits 2px from every edge of the track, as Figma's track pads it (2/2/2/2). The track has no
22
+ border of its own: a transparent 1px border here would add a pixel of padding on every side, which left the thumb 3px from the
23
+ start and 1px from the end.
21
24
  -->
22
25
  <SwitchPrimitive.Root
23
26
  bind:ref
24
27
  bind:checked
25
28
  class={cn(
26
- "peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-input p-0.5 transition-colors duration-base ease-out before:absolute before:inset-x-0 before:-inset-y-0.5 hover:bg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:hover:bg-primary/90",
29
+ "peer relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full bg-input p-0.5 transition-colors duration-base ease-out before:absolute before:inset-x-0 before:-inset-y-0.5 hover:bg-accent focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:hover:bg-primary/90",
27
30
  className
28
31
  )}
29
32
  {...restProps}
@@ -609,10 +609,11 @@ go 3 levels up in development mode, 2 levels up in production mode*/
609
609
  }
610
610
 
611
611
  /* Modal System */
612
+ /* The scrim of every overlay: the muted foreground token at 80%, set on the colour, with a 3px blur (Figma `Overlay/Scrim blur`) */
612
613
  .modal-overlay {
613
614
  @apply fixed inset-0 z-50;
614
- backdrop-filter: blur(30px);
615
- background-color: rgba(255, 255, 255, 0.5);
615
+ backdrop-filter: blur(3px);
616
+ background-color: color-mix(in srgb, var(--muted-foreground) 80%, transparent);
616
617
  }
617
618
 
618
619
  .modal-shadow {
@@ -238,8 +238,28 @@
238
238
  transition-timing-function: var(--ease-drawer);
239
239
  }
240
240
 
241
+ /*
242
+ * A pressable control shrinks to `--scale-press` while it is pressed and goes back to 100% when it is released
243
+ * (Figma: the Pressed variants of Button and Icon Button are 0.97 of the Default size).
244
+ *
245
+ * The utility owns the whole transition of the control: the colours that hover changes and the transform of the press.
246
+ * `transition-colors` sets `transition-property` too, with the same specificity, so whichever of the two came last in the
247
+ * stylesheet won, and when that was `transition-colors` the press had no transition at all: the control snapped in and
248
+ * snapped back. `:not([data-no-press])` raises the specificity above `transition-colors` and `duration-*`, so they compose
249
+ * in any order, and `data-no-press` is the way to opt one control out.
250
+ */
241
251
  @utility press-feedback {
242
- transition: transform var(--duration-base) var(--ease-out);
252
+ &:not([data-no-press]) {
253
+ transition:
254
+ color var(--duration-fast) cubic-bezier(0.4, 0, 0.2, 1),
255
+ background-color var(--duration-fast) cubic-bezier(0.4, 0, 0.2, 1),
256
+ border-color var(--duration-fast) cubic-bezier(0.4, 0, 0.2, 1),
257
+ outline-color var(--duration-fast) cubic-bezier(0.4, 0, 0.2, 1),
258
+ text-decoration-color var(--duration-fast) cubic-bezier(0.4, 0, 0.2, 1),
259
+ fill var(--duration-fast) cubic-bezier(0.4, 0, 0.2, 1),
260
+ stroke var(--duration-fast) cubic-bezier(0.4, 0, 0.2, 1),
261
+ transform var(--duration-base) var(--ease-out);
262
+ }
243
263
 
244
264
  &:active:not(:disabled):not([aria-disabled='true']) {
245
265
  transform: scale(var(--scale-press));
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Tells the styles whether the last thing a person did was press a key or use the pointer.
3
+ *
4
+ * The root element carries `data-input-modality="keyboard"` after a key press and `data-input-modality="pointer"` after the
5
+ * pointer moved or pressed. A list can then paint the row that the keyboard highlighted without painting, for ever, the row
6
+ * the pointer happened to cross last: bits-ui's Select never takes the highlight back when the pointer leaves, so what the
7
+ * pointer highlighted stayed painted after it had gone (Figma Select Option has Default, Hover and Selected looks, not a
8
+ * "was hovered" one).
9
+ *
10
+ * `installInputModality` is safe to call as often as needed: it listens once per document.
11
+ */
12
+ /** The attribute on the root element that carries the modality. */
13
+ export declare const inputModalityAttribute = "data-input-modality";
14
+ export type InputModality = 'keyboard' | 'pointer';
15
+ /** Start listening in `doc` (the page by default). Does nothing on the server. */
16
+ export declare function installInputModality(doc?: Document | undefined): void;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Tells the styles whether the last thing a person did was press a key or use the pointer.
3
+ *
4
+ * The root element carries `data-input-modality="keyboard"` after a key press and `data-input-modality="pointer"` after the
5
+ * pointer moved or pressed. A list can then paint the row that the keyboard highlighted without painting, for ever, the row
6
+ * the pointer happened to cross last: bits-ui's Select never takes the highlight back when the pointer leaves, so what the
7
+ * pointer highlighted stayed painted after it had gone (Figma Select Option has Default, Hover and Selected looks, not a
8
+ * "was hovered" one).
9
+ *
10
+ * `installInputModality` is safe to call as often as needed: it listens once per document.
11
+ */
12
+ /** The attribute on the root element that carries the modality. */
13
+ export const inputModalityAttribute = 'data-input-modality';
14
+ /** The documents that already listen, so that a second call does nothing. */
15
+ const listeningDocuments = new WeakSet();
16
+ /**
17
+ * Keys that act on a list. A modifier on its own (Shift, Meta…) or a shortcut (Cmd+C) is not the keyboard driving a list:
18
+ * a person who opens a menu with the pointer and then presses Shift must not see the highlight appear.
19
+ */
20
+ function isListNavigationKey(event) {
21
+ if (event.metaKey || event.ctrlKey || event.altKey)
22
+ return false;
23
+ return (event.key === 'ArrowUp' ||
24
+ event.key === 'ArrowDown' ||
25
+ event.key === 'ArrowLeft' ||
26
+ event.key === 'ArrowRight' ||
27
+ event.key === 'Home' ||
28
+ event.key === 'End' ||
29
+ event.key === 'PageUp' ||
30
+ event.key === 'PageDown' ||
31
+ event.key === 'Enter' ||
32
+ event.key === ' ' ||
33
+ event.key === 'Tab' ||
34
+ // Type-ahead: a single character moves the highlight to the row that starts with it
35
+ event.key.length === 1);
36
+ }
37
+ /** Start listening in `doc` (the page by default). Does nothing on the server. */
38
+ export function installInputModality(doc = typeof document === 'undefined' ? undefined : document) {
39
+ if (!doc || listeningDocuments.has(doc))
40
+ return;
41
+ listeningDocuments.add(doc);
42
+ const root = doc.documentElement;
43
+ const setModality = (modality) => {
44
+ // Compared first: the pointer moves hundreds of times a second and nothing has to be written for that
45
+ if (root.getAttribute(inputModalityAttribute) !== modality) {
46
+ root.setAttribute(inputModalityAttribute, modality);
47
+ }
48
+ };
49
+ doc.addEventListener('keydown', (event) => {
50
+ if (isListNavigationKey(event))
51
+ setModality('keyboard');
52
+ }, true);
53
+ doc.addEventListener('pointermove', () => setModality('pointer'), { capture: true, passive: true });
54
+ doc.addEventListener('pointerdown', () => setModality('pointer'), { capture: true, passive: true });
55
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shared overlay motion tokens so BaseModal, bits-ui Dialog, and related surfaces stay in sync.
3
+ * FilePreview / legacy modals: backdrop fades 200ms with cubicOut; the card scales from 0.95 with the same timing.
4
+ * Change here when Figma updates overlay motion.
5
+ */
6
+ import { cubicOut } from 'svelte/easing';
7
+ /** Backdrop scrim + card motion duration (ms). Intentionally 200ms; DS `--duration-base` is 160ms for small UI. */
8
+ export declare const overlayTransitionDurationMs = 200;
9
+ /** Easing for open/close (Svelte transition easing function — matches “cubic out” in the legacy modals). */
10
+ export declare const overlayTransitionEasing: typeof cubicOut;
11
+ /**
12
+ * Backdrop blur (px) of the scrim behind every modal, dialog, drawer and sheet. It is the Figma effect style
13
+ * `Overlay/Scrim blur` (a background blur of 3).
14
+ */
15
+ export declare const overlayBackdropBlurPx = 3;
16
+ /** Content scale at the start of the open transition / end of the close transition. */
17
+ export declare const overlayContentScaleStart = 0.95;
18
+ /**
19
+ * Tailwind duration class — must be a source literal so Tailwind's scanner emits `duration-[200ms]`.
20
+ * Kept in sync with `overlayTransitionDurationMs` (see tests/overlayMotionDuration.test.ts).
21
+ */
22
+ export declare const overlayDurationClass = "duration-[200ms]";
23
+ /**
24
+ * bits-ui applies `data-[state=open]:animate-in` at specificity 0,2,0 with a 150ms default duration, which beats a
25
+ * plain `duration-[200ms]` (0,1,0). These state-scoped arbitrary properties match open/closed and win in cascade.
26
+ * Literal strings — keep in sync with `overlayTransitionDurationMs`.
27
+ */
28
+ export declare const overlayDialogOpenClosedAnimationDurationClasses: readonly ["data-[state=open]:[animation-duration:200ms]", "data-[state=closed]:[animation-duration:200ms]"];
29
+ /**
30
+ * The scrim colour: the muted foreground token (neutral 550 in the design system) at 80%. The opacity is part of the colour, not of
31
+ * the layer, because an element that is itself translucent would not blur what is behind it. Literal so Tailwind's
32
+ * scanner emits it; `bg-muted-foreground/80` is the Figma scrim (`Foundations / Overlay`).
33
+ */
34
+ export declare const overlayScrimColorClass = "bg-muted-foreground/80";
35
+ /** Backdrop blur class — matches `overlayBackdropBlurPx`. Literal for the same reason as the colour. */
36
+ export declare const overlayBackdropBlurClass = "backdrop-blur-[3px]";
37
+ /**
38
+ * The whole scrim: colour and blur. Every surface that dims the page behind a panel (dialogs, modals, drawers, the
39
+ * navigation drawer) uses these two classes, so the overlay looks the same everywhere. Written out, not joined from the
40
+ * two constants above, so the scanner of Tailwind finds both classes in this one literal as well.
41
+ */
42
+ export declare const overlayScrimClasses = "bg-muted-foreground/80 backdrop-blur-[3px]";
43
+ /** Tailwind arbitrary easing approximating Svelte `cubicOut` (ease-out cubic, 1 − (1 − t)³). */
44
+ export declare const overlayEasingClass = "ease-[cubic-bezier(0.33,1,0.68,1)]";
45
+ /** Overlay + panel classes for bits-ui Dialog (aligned with BaseModal motion: fade, 200ms, cubic-out-like, zoom from 0.95). */
46
+ export declare const overlayDialogOverlayClasses: string;
47
+ export declare const overlayDialogContentClasses: string;
48
+ /** Applied with overlay dialog classes so users who prefer reduced motion get instant open/close. */
49
+ export declare const overlayReducedMotionClasses = "motion-reduce:!duration-0 motion-reduce:data-[state=open]:animate-none motion-reduce:data-[state=closed]:animate-none";
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Shared overlay motion tokens so BaseModal, bits-ui Dialog, and related surfaces stay in sync.
3
+ * FilePreview / legacy modals: backdrop fades 200ms with cubicOut; the card scales from 0.95 with the same timing.
4
+ * Change here when Figma updates overlay motion.
5
+ */
6
+ import { cubicOut } from 'svelte/easing';
7
+ /** Backdrop scrim + card motion duration (ms). Intentionally 200ms; DS `--duration-base` is 160ms for small UI. */
8
+ export const overlayTransitionDurationMs = 200;
9
+ /** Easing for open/close (Svelte transition easing function — matches “cubic out” in the legacy modals). */
10
+ export const overlayTransitionEasing = cubicOut;
11
+ /**
12
+ * Backdrop blur (px) of the scrim behind every modal, dialog, drawer and sheet. It is the Figma effect style
13
+ * `Overlay/Scrim blur` (a background blur of 3).
14
+ */
15
+ export const overlayBackdropBlurPx = 3;
16
+ /** Content scale at the start of the open transition / end of the close transition. */
17
+ export const overlayContentScaleStart = 0.95;
18
+ /**
19
+ * Tailwind duration class — must be a source literal so Tailwind's scanner emits `duration-[200ms]`.
20
+ * Kept in sync with `overlayTransitionDurationMs` (see tests/overlayMotionDuration.test.ts).
21
+ */
22
+ export const overlayDurationClass = 'duration-[200ms]';
23
+ /**
24
+ * bits-ui applies `data-[state=open]:animate-in` at specificity 0,2,0 with a 150ms default duration, which beats a
25
+ * plain `duration-[200ms]` (0,1,0). These state-scoped arbitrary properties match open/closed and win in cascade.
26
+ * Literal strings — keep in sync with `overlayTransitionDurationMs`.
27
+ */
28
+ export const overlayDialogOpenClosedAnimationDurationClasses = [
29
+ 'data-[state=open]:[animation-duration:200ms]',
30
+ 'data-[state=closed]:[animation-duration:200ms]'
31
+ ];
32
+ /**
33
+ * The scrim colour: the muted foreground token (neutral 550 in the design system) at 80%. The opacity is part of the colour, not of
34
+ * the layer, because an element that is itself translucent would not blur what is behind it. Literal so Tailwind's
35
+ * scanner emits it; `bg-muted-foreground/80` is the Figma scrim (`Foundations / Overlay`).
36
+ */
37
+ export const overlayScrimColorClass = 'bg-muted-foreground/80';
38
+ /** Backdrop blur class — matches `overlayBackdropBlurPx`. Literal for the same reason as the colour. */
39
+ export const overlayBackdropBlurClass = 'backdrop-blur-[3px]';
40
+ /**
41
+ * The whole scrim: colour and blur. Every surface that dims the page behind a panel (dialogs, modals, drawers, the
42
+ * navigation drawer) uses these two classes, so the overlay looks the same everywhere. Written out, not joined from the
43
+ * two constants above, so the scanner of Tailwind finds both classes in this one literal as well.
44
+ */
45
+ export const overlayScrimClasses = 'bg-muted-foreground/80 backdrop-blur-[3px]';
46
+ /** Tailwind arbitrary easing approximating Svelte `cubicOut` (ease-out cubic, 1 − (1 − t)³). */
47
+ export const overlayEasingClass = 'ease-[cubic-bezier(0.33,1,0.68,1)]';
48
+ /** Overlay + panel classes for bits-ui Dialog (aligned with BaseModal motion: fade, 200ms, cubic-out-like, zoom from 0.95). */
49
+ export const overlayDialogOverlayClasses = [
50
+ overlayScrimClasses,
51
+ overlayDurationClass,
52
+ overlayEasingClass,
53
+ ...overlayDialogOpenClosedAnimationDurationClasses,
54
+ `data-[state=open]:animate-in`,
55
+ `data-[state=closed]:animate-out`,
56
+ `data-[state=closed]:fade-out-0`,
57
+ `data-[state=open]:fade-in-0`
58
+ ].join(' ');
59
+ export const overlayDialogContentClasses = [
60
+ overlayDurationClass,
61
+ overlayEasingClass,
62
+ ...overlayDialogOpenClosedAnimationDurationClasses,
63
+ `data-[state=open]:animate-in`,
64
+ `data-[state=closed]:animate-out`,
65
+ `data-[state=closed]:fade-out-0`,
66
+ `data-[state=open]:fade-in-0`,
67
+ `data-[state=closed]:zoom-out-95`,
68
+ `data-[state=open]:zoom-in-95`
69
+ ].join(' ');
70
+ /** Applied with overlay dialog classes so users who prefer reduced motion get instant open/close. */
71
+ export const overlayReducedMotionClasses = 'motion-reduce:!duration-0 motion-reduce:data-[state=open]:animate-none motion-reduce:data-[state=closed]:animate-none';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@widgetic/design-system",
3
- "version": "0.5.23",
3
+ "version": "0.5.25",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "type": "module",
6
6
  "svelte": "./dist/index.js",
@@ -1,7 +0,0 @@
1
- /** Footer links in SettingsDialog (Figma Settings Dialog 107:618). */
2
- export interface SettingsDialogMessages {
3
- privacyPolicy: string;
4
- termsOfService: string;
5
- cookieSettings: string;
6
- }
7
- export declare const defaultSettingsDialogMessages: SettingsDialogMessages;
@@ -1,5 +0,0 @@
1
- export const defaultSettingsDialogMessages = {
2
- privacyPolicy: 'Privacy policy',
3
- termsOfService: 'Terms of service',
4
- cookieSettings: 'Cookie settings'
5
- };