@widgetic/design-system 0.5.24 → 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.
- package/dist/components/CopyableHeader.svelte +5 -3
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/shell/NavRow.svelte +4 -1
- package/dist/components/shell/SettingsDialog.svelte +0 -26
- package/dist/components/shell/SettingsDialog.svelte.d.ts +0 -4
- package/dist/components/shell/index.d.ts +0 -1
- package/dist/components/shell/index.js +0 -1
- package/dist/components/ui/alert/index.d.ts +2 -2
- package/dist/components/ui/alert/index.js +4 -1
- package/dist/components/ui/avatar/avatar.svelte +22 -3
- package/dist/components/ui/button/button.svelte +1 -1
- package/dist/components/ui/button/button.svelte.d.ts +2 -2
- package/dist/components/ui/icon-button/icon-button.svelte +1 -1
- package/dist/components/ui/icon-button/icon-button.svelte.d.ts +2 -2
- package/dist/components/ui/select/select-content.svelte +5 -0
- package/dist/components/ui/select/select-item.svelte +8 -1
- package/dist/components/ui/select/select-trigger.svelte +3 -2
- package/dist/components/ui/stepper-button/stepper-button.svelte +1 -1
- package/dist/components/ui/switch/switch.svelte +4 -1
- package/dist/styles/tokens.css +21 -1
- package/dist/utils/inputModality.d.ts +16 -0
- package/dist/utils/inputModality.js +55 -0
- package/package.json +1 -1
- package/dist/components/shell/messages.d.ts +0 -7
- package/dist/components/shell/messages.js +0 -5
|
@@ -130,9 +130,10 @@
|
|
|
130
130
|
}
|
|
131
131
|
</script>
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
<!-- `min-w-0` on the root and on the title row: a `truncate` title has its whole text as min-content, so inside a grid or flex parent (a dialog) the row would push the copy button out of the parent instead of cutting the title. -->
|
|
134
|
+
<div class={cn('copyable-header flex w-full min-w-0 flex-col gap-3', className)} {style}>
|
|
134
135
|
<!-- Title row: section label + copy control (Figma 32px). -->
|
|
135
|
-
<div class={cn('flex h-8 items-center justify-between gap-3', headerBackgroundColor)}>
|
|
136
|
+
<div class={cn('flex h-8 min-w-0 items-center justify-between gap-3', headerBackgroundColor)}>
|
|
136
137
|
<div class={cn('text-heading-section text-foreground min-w-0 truncate', headerTextClass)}>
|
|
137
138
|
{headerText}
|
|
138
139
|
</div>
|
|
@@ -262,7 +263,8 @@
|
|
|
262
263
|
</div>
|
|
263
264
|
|
|
264
265
|
{#if showHelpRow}
|
|
265
|
-
|
|
266
|
+
<!-- Figma: the help row is 20 high (the 14/20 link sets it), whatever the size of the link. -->
|
|
267
|
+
<div class="flex min-h-5 flex-wrap items-center gap-1">
|
|
266
268
|
{#if helpPrompt}
|
|
267
269
|
<span class="text-caption text-muted-foreground">{helpPrompt}</span>
|
|
268
270
|
{/if}
|
|
@@ -16,7 +16,7 @@ export { Breadcrumbs, type BreadcrumbsProps } from './navigation';
|
|
|
16
16
|
export { default as PDFViewer } from './pdf/PDFViewer.svelte';
|
|
17
17
|
export { SearchInput } from './search-input';
|
|
18
18
|
export { ShadEditor, checkShadEditorMessages, type ShadEditorMessageKey, type TipTapEditor, type TipTapEditorLevel } from './shad-editor';
|
|
19
|
-
export { NavRow, navRowVariants, NavSectionLabel, ProfileRow, profileRowVariants, Sidebar, WidgetRow, widgetRowVariants, PageHeader, TopBar, NavDrawer, AppShell, ErrorState, defaultErrorStateMessages, type ErrorStateMessages, NotificationRow, type NotificationRowProps, SettingsNav, type SettingsNavProps, SettingsDialog, type SettingsDialogProps, Stat, type StatProps, statVariants, Banner, type BannerProps, type BannerTone, bannerVariants, Link, type LinkProps, type LinkSize, linkVariants, type SettingsNavGroup, type SettingsNavItem, settingsNavItemIsActive, externalLinkAttributes, statHintClass, type StatTone
|
|
19
|
+
export { NavRow, navRowVariants, NavSectionLabel, ProfileRow, profileRowVariants, Sidebar, WidgetRow, widgetRowVariants, PageHeader, TopBar, NavDrawer, AppShell, ErrorState, defaultErrorStateMessages, type ErrorStateMessages, NotificationRow, type NotificationRowProps, SettingsNav, type SettingsNavProps, SettingsDialog, type SettingsDialogProps, Stat, type StatProps, statVariants, Banner, type BannerProps, type BannerTone, bannerVariants, Link, type LinkProps, type LinkSize, linkVariants, type SettingsNavGroup, type SettingsNavItem, settingsNavItemIsActive, externalLinkAttributes, statHintClass, type StatTone } from './shell';
|
|
20
20
|
export { ToasterModal, showToast } from './toast';
|
|
21
21
|
export * as Accordion from './ui/accordion';
|
|
22
22
|
export * as Alert from './ui/alert';
|
package/dist/components/index.js
CHANGED
|
@@ -17,7 +17,7 @@ export { Breadcrumbs } from './navigation';
|
|
|
17
17
|
export { default as PDFViewer } from './pdf/PDFViewer.svelte';
|
|
18
18
|
export { SearchInput } from './search-input';
|
|
19
19
|
export { ShadEditor, checkShadEditorMessages } from './shad-editor';
|
|
20
|
-
export { NavRow, navRowVariants, NavSectionLabel, ProfileRow, profileRowVariants, Sidebar, WidgetRow, widgetRowVariants, PageHeader, TopBar, NavDrawer, AppShell, ErrorState, defaultErrorStateMessages, NotificationRow, SettingsNav, SettingsDialog, Stat, statVariants, Banner, bannerVariants, Link, linkVariants, settingsNavItemIsActive, externalLinkAttributes, statHintClass
|
|
20
|
+
export { NavRow, navRowVariants, NavSectionLabel, ProfileRow, profileRowVariants, Sidebar, WidgetRow, widgetRowVariants, PageHeader, TopBar, NavDrawer, AppShell, ErrorState, defaultErrorStateMessages, NotificationRow, SettingsNav, SettingsDialog, Stat, statVariants, Banner, bannerVariants, Link, linkVariants, settingsNavItemIsActive, externalLinkAttributes, statHintClass } from './shell';
|
|
21
21
|
export { ToasterModal, showToast } from './toast';
|
|
22
22
|
export * as Accordion from './ui/accordion';
|
|
23
23
|
export * as Alert from './ui/alert';
|
|
@@ -8,7 +8,10 @@
|
|
|
8
8
|
variants: {
|
|
9
9
|
active: {
|
|
10
10
|
true: 'bg-sidebar-accent text-sidebar-accent-foreground text-body-medium',
|
|
11
|
-
|
|
11
|
+
// The inactive row carries its own type style (14/20 regular, Figma Nav Row): without it the label took the size
|
|
12
|
+
// of whatever it sat in (16px in a dialog), bigger than the active row's 14/20 medium.
|
|
13
|
+
false:
|
|
14
|
+
'text-body text-muted-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground'
|
|
12
15
|
}
|
|
13
16
|
},
|
|
14
17
|
defaultVariants: {
|
|
@@ -11,10 +11,6 @@
|
|
|
11
11
|
groups: import('./settings-nav-types.js').SettingsNavGroup[];
|
|
12
12
|
onSelect?: (id: string) => void;
|
|
13
13
|
closeLabel?: string;
|
|
14
|
-
messages?: import('./messages.js').SettingsDialogMessages;
|
|
15
|
-
privacyHref?: string;
|
|
16
|
-
termsHref?: string;
|
|
17
|
-
cookieHref?: string;
|
|
18
14
|
children: import('svelte').Snippet;
|
|
19
15
|
descriptionSlot?: import('svelte').Snippet;
|
|
20
16
|
};
|
|
@@ -28,7 +24,6 @@
|
|
|
28
24
|
import { cn } from '../../utils.js';
|
|
29
25
|
import { modalState } from '../modals/modalStore.js';
|
|
30
26
|
import SettingsNav from './SettingsNav.svelte';
|
|
31
|
-
import { defaultSettingsDialogMessages, type SettingsDialogMessages } from './messages.js';
|
|
32
27
|
import type { SettingsNavGroup } from './settings-nav-types.js';
|
|
33
28
|
import { createNestedModalTracker } from './nested-modal-guard.js';
|
|
34
29
|
|
|
@@ -41,10 +36,6 @@
|
|
|
41
36
|
groups,
|
|
42
37
|
onSelect,
|
|
43
38
|
closeLabel = 'Close',
|
|
44
|
-
messages = defaultSettingsDialogMessages,
|
|
45
|
-
privacyHref = '#',
|
|
46
|
-
termsHref = '#',
|
|
47
|
-
cookieHref = '#',
|
|
48
39
|
children,
|
|
49
40
|
descriptionSlot
|
|
50
41
|
}: SettingsDialogProps = $props();
|
|
@@ -110,23 +101,6 @@
|
|
|
110
101
|
class="border-border bg-sidebar flex w-full shrink-0 flex-col border-b p-3 lg:w-64 lg:border-e lg:border-b-0"
|
|
111
102
|
>
|
|
112
103
|
<SettingsNav {groups} {activeId} {onSelect} class="flex-1" />
|
|
113
|
-
<div class="mt-auto flex flex-col gap-1 px-2 pt-4 pb-1">
|
|
114
|
-
<a
|
|
115
|
-
href={privacyHref}
|
|
116
|
-
class="text-caption text-muted-foreground duration-fast hover:text-foreground w-fit transition-colors"
|
|
117
|
-
>{messages.privacyPolicy}</a
|
|
118
|
-
>
|
|
119
|
-
<a
|
|
120
|
-
href={termsHref}
|
|
121
|
-
class="text-caption text-muted-foreground duration-fast hover:text-foreground w-fit transition-colors"
|
|
122
|
-
>{messages.termsOfService}</a
|
|
123
|
-
>
|
|
124
|
-
<a
|
|
125
|
-
href={cookieHref}
|
|
126
|
-
class="text-caption text-muted-foreground duration-fast hover:text-foreground w-fit transition-colors"
|
|
127
|
-
>{messages.cookieSettings}</a
|
|
128
|
-
>
|
|
129
|
-
</div>
|
|
130
104
|
</aside>
|
|
131
105
|
<div class="flex min-h-0 min-w-0 flex-1 flex-col gap-6 overflow-y-auto p-8">
|
|
132
106
|
<header class="flex flex-col gap-1 pe-10">
|
|
@@ -7,10 +7,6 @@ export type SettingsDialogProps = {
|
|
|
7
7
|
groups: import('./settings-nav-types.js').SettingsNavGroup[];
|
|
8
8
|
onSelect?: (id: string) => void;
|
|
9
9
|
closeLabel?: string;
|
|
10
|
-
messages?: import('./messages.js').SettingsDialogMessages;
|
|
11
|
-
privacyHref?: string;
|
|
12
|
-
termsHref?: string;
|
|
13
|
-
cookieHref?: string;
|
|
14
10
|
children: import('svelte').Snippet;
|
|
15
11
|
descriptionSlot?: import('svelte').Snippet;
|
|
16
12
|
};
|
|
@@ -19,4 +19,3 @@ export { default as Link, type LinkProps, type LinkSize, linkVariants } from './
|
|
|
19
19
|
export { type SettingsNavGroup, type SettingsNavItem, settingsNavItemIsActive } from './settings-nav-types.js';
|
|
20
20
|
export { externalLinkAttributes } from './link-external.js';
|
|
21
21
|
export { statHintClass, type StatTone } from './stat-tone.js';
|
|
22
|
-
export { type SettingsDialogMessages, defaultSettingsDialogMessages } from './messages.js';
|
|
@@ -22,4 +22,3 @@ export { default as Link, linkVariants } from './Link.svelte';
|
|
|
22
22
|
export { settingsNavItemIsActive } from './settings-nav-types.js';
|
|
23
23
|
export { externalLinkAttributes } from './link-external.js';
|
|
24
24
|
export { statHintClass } from './stat-tone.js';
|
|
25
|
-
export { defaultSettingsDialogMessages } from './messages.js';
|
|
@@ -7,7 +7,7 @@ export declare const alertVariants: import("tailwind-variants").TVReturnType<{
|
|
|
7
7
|
default: string;
|
|
8
8
|
destructive: string;
|
|
9
9
|
};
|
|
10
|
-
}, undefined, "relative grid w-full grid-cols-
|
|
10
|
+
}, undefined, "relative grid w-full grid-cols-1 gap-x-3 gap-y-0.5 rounded-xl border border-border bg-background p-3 pe-4 ps-4 text-foreground has-[>svg]:grid-cols-[auto_1fr] [&>svg]:col-start-1 [&>svg]:row-start-1 [&>svg]:mt-0.5 [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:stroke-[1.333] [&_[data-slot=alert-title]]:row-start-1 [&_[data-slot=alert-description]]:row-start-1 has-[[data-slot=alert-title]]:[&_[data-slot=alert-description]]:row-start-2 has-[>svg]:[&_[data-slot=alert-title]]:col-start-2 has-[>svg]:[&_[data-slot=alert-description]]:col-start-2", {
|
|
11
11
|
responsiveVariants?: boolean | (string | number | symbol)[] | {
|
|
12
12
|
variant?: boolean | (string | number | symbol)[] | undefined;
|
|
13
13
|
} | undefined;
|
|
@@ -29,7 +29,7 @@ export declare const alertVariants: import("tailwind-variants").TVReturnType<{
|
|
|
29
29
|
default: string;
|
|
30
30
|
destructive: string;
|
|
31
31
|
};
|
|
32
|
-
}, undefined, "relative grid w-full grid-cols-
|
|
32
|
+
}, undefined, "relative grid w-full grid-cols-1 gap-x-3 gap-y-0.5 rounded-xl border border-border bg-background p-3 pe-4 ps-4 text-foreground has-[>svg]:grid-cols-[auto_1fr] [&>svg]:col-start-1 [&>svg]:row-start-1 [&>svg]:mt-0.5 [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:stroke-[1.333] [&_[data-slot=alert-title]]:row-start-1 [&_[data-slot=alert-description]]:row-start-1 has-[[data-slot=alert-title]]:[&_[data-slot=alert-description]]:row-start-2 has-[>svg]:[&_[data-slot=alert-title]]:col-start-2 has-[>svg]:[&_[data-slot=alert-description]]:col-start-2", import("tailwind-variants/dist/config.js").TVConfig<{
|
|
33
33
|
variant: {
|
|
34
34
|
default: string;
|
|
35
35
|
destructive: string;
|
|
@@ -3,7 +3,10 @@ import Root from "./alert.svelte";
|
|
|
3
3
|
import Description from "./alert-description.svelte";
|
|
4
4
|
import Title from "./alert-title.svelte";
|
|
5
5
|
export const alertVariants = tv({
|
|
6
|
-
|
|
6
|
+
// The icon column exists only when the alert has an icon (a direct <svg> child): an alert without one starts its text at
|
|
7
|
+
// the left padding instead of leaving the empty icon column and its 12px gap in front of the text. The description sits
|
|
8
|
+
// on the second row only under a title: alone (an icon and a sentence, or a sentence) it shares the first row with the icon.
|
|
9
|
+
base: "relative grid w-full grid-cols-1 gap-x-3 gap-y-0.5 rounded-xl border border-border bg-background p-3 pe-4 ps-4 text-foreground has-[>svg]:grid-cols-[auto_1fr] [&>svg]:col-start-1 [&>svg]:row-start-1 [&>svg]:mt-0.5 [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:stroke-[1.333] [&_[data-slot=alert-title]]:row-start-1 [&_[data-slot=alert-description]]:row-start-1 has-[[data-slot=alert-title]]:[&_[data-slot=alert-description]]:row-start-2 has-[>svg]:[&_[data-slot=alert-title]]:col-start-2 has-[>svg]:[&_[data-slot=alert-description]]:col-start-2",
|
|
7
10
|
variants: {
|
|
8
11
|
variant: {
|
|
9
12
|
default: "text-foreground [&>svg]:text-foreground",
|
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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;
|
|
@@ -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
|
|
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
|
|
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
|
|
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;
|
|
@@ -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-pointer 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-
|
|
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
|
-
|
|
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
|
|
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
|
|
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}
|
package/dist/styles/tokens.css
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/package.json
CHANGED
|
@@ -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;
|