@koide-labs/ui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.husky/pre-commit +1 -0
- package/.storybook/main.ts +25 -0
- package/.storybook/preview-head.html +6 -0
- package/.storybook/preview.tsx +48 -0
- package/.storybook/vitest.setup.ts +8 -0
- package/README.md +11 -0
- package/eslint.config.mjs +29 -0
- package/lint-staged.config.js +15 -0
- package/package.json +95 -0
- package/pnpm-workspace.yaml +2 -0
- package/postcss.config.mjs +7 -0
- package/prettier.config.mjs +24 -0
- package/scripts/build-icon-types.ts +38 -0
- package/src/-types.ts +8 -0
- package/src/-utils.tsx +64 -0
- package/src/components/accordion/accordion.module.css +44 -0
- package/src/components/accordion/accordion.stories.tsx +36 -0
- package/src/components/accordion/index.tsx +67 -0
- package/src/components/alert-dialog/alert-dialog.module.css +5 -0
- package/src/components/alert-dialog/alert-dialog.stories.tsx +53 -0
- package/src/components/alert-dialog/index.tsx +138 -0
- package/src/components/anchor/anchor.module.css +18 -0
- package/src/components/anchor/anchor.stories.tsx +28 -0
- package/src/components/anchor/index.tsx +45 -0
- package/src/components/avatar/avatar.module.css +56 -0
- package/src/components/avatar/avatar.stories.tsx +61 -0
- package/src/components/avatar/index.tsx +82 -0
- package/src/components/badge/badge.module.css +35 -0
- package/src/components/badge/badge.stories.tsx +60 -0
- package/src/components/badge/index.tsx +71 -0
- package/src/components/button/button.module.css +42 -0
- package/src/components/button/button.stories.tsx +108 -0
- package/src/components/button/index.tsx +63 -0
- package/src/components/checkbox/checkbox.module.css +36 -0
- package/src/components/checkbox/checkbox.stories.tsx +21 -0
- package/src/components/checkbox/index.tsx +41 -0
- package/src/components/code/code.module.css +20 -0
- package/src/components/code/code.stories.tsx +42 -0
- package/src/components/code/index.tsx +73 -0
- package/src/components/collapse/collapse.module.css +27 -0
- package/src/components/collapse/collapse.stories.tsx +27 -0
- package/src/components/collapse/index.tsx +59 -0
- package/src/components/command/command.module.css +95 -0
- package/src/components/command/command.stories.tsx +38 -0
- package/src/components/command/index.tsx +108 -0
- package/src/components/context-menu/context-menu.module.css +36 -0
- package/src/components/context-menu/context-menu.stories.tsx +99 -0
- package/src/components/context-menu/index.tsx +242 -0
- package/src/components/dialog/dialog.module.css +71 -0
- package/src/components/dialog/dialog.stories.tsx +29 -0
- package/src/components/dialog/index.tsx +148 -0
- package/src/components/heading/heading.module.css +3 -0
- package/src/components/heading/heading.stories.tsx +52 -0
- package/src/components/heading/index.tsx +112 -0
- package/src/components/icon/icon-names.ts +3189 -0
- package/src/components/icon/icon.module.css +36 -0
- package/src/components/icon/icon.stories.tsx +40 -0
- package/src/components/icon/index.tsx +60 -0
- package/src/components/icon-button/icon-button.module.css +33 -0
- package/src/components/icon-button/icon-button.stories.tsx +59 -0
- package/src/components/icon-button/index.tsx +48 -0
- package/src/components/inline-code/index.tsx +29 -0
- package/src/components/inline-code/inline-code.module.css +13 -0
- package/src/components/inline-code/inline-code.stories.tsx +31 -0
- package/src/components/input/index.tsx +22 -0
- package/src/components/input/input.module.css +23 -0
- package/src/components/input/input.stories.tsx +52 -0
- package/src/components/meter/index.tsx +55 -0
- package/src/components/meter/meter.module.css +23 -0
- package/src/components/meter/meter.stories.tsx +31 -0
- package/src/components/multiline-input/index.tsx +58 -0
- package/src/components/multiline-input/multiline-input.stories.tsx +26 -0
- package/src/components/number-input/index.tsx +74 -0
- package/src/components/number-input/number-input.module.css +41 -0
- package/src/components/number-input/number-input.stories.tsx +24 -0
- package/src/components/password-input/index.tsx +24 -0
- package/src/components/password-input/password-input.module.css +10 -0
- package/src/components/password-input/password-input.stories.tsx +24 -0
- package/src/components/pill/index.tsx +45 -0
- package/src/components/pill/pill.module.css +22 -0
- package/src/components/pill/pill.stories.tsx +83 -0
- package/src/components/popover/index.tsx +94 -0
- package/src/components/popover/popover.module.css +8 -0
- package/src/components/popover/popover.stories.tsx +53 -0
- package/src/components/preview-card/index.tsx +68 -0
- package/src/components/preview-card/preview-card.module.css +5 -0
- package/src/components/preview-card/preview-card.stories.tsx +58 -0
- package/src/components/radio/index.tsx +67 -0
- package/src/components/radio/radio-group.module.css +5 -0
- package/src/components/radio/radio.module.css +36 -0
- package/src/components/radio/radio.stories.tsx +27 -0
- package/src/components/search-bar/index.tsx +60 -0
- package/src/components/search-bar/search-bar.module.css +29 -0
- package/src/components/search-bar/search-bar.stories.tsx +37 -0
- package/src/components/select/index.tsx +132 -0
- package/src/components/select/select.module.css +63 -0
- package/src/components/select/select.stories.tsx +49 -0
- package/src/components/separator/index.tsx +28 -0
- package/src/components/separator/separator.module.css +24 -0
- package/src/components/separator/separator.stories.tsx +40 -0
- package/src/components/slider/index.tsx +28 -0
- package/src/components/slider/slider.module.css +52 -0
- package/src/components/slider/slider.stories.tsx +53 -0
- package/src/components/spinner/index.tsx +14 -0
- package/src/components/spinner/spinner.module.css +13 -0
- package/src/components/spinner/spinner.stories.tsx +17 -0
- package/src/components/stacked-avatars/index.tsx +88 -0
- package/src/components/stacked-avatars/stacked-avatars.module.css +79 -0
- package/src/components/stacked-avatars/stacked-avatars.stories.tsx +48 -0
- package/src/components/status-banner/index.tsx +96 -0
- package/src/components/status-banner/status-banner.module.css +52 -0
- package/src/components/status-banner/status-banner.stories.tsx +44 -0
- package/src/components/surface/index.tsx +83 -0
- package/src/components/surface/surface.module.css +35 -0
- package/src/components/surface/surface.stories.tsx +84 -0
- package/src/components/switch/index.tsx +23 -0
- package/src/components/switch/switch.module.css +45 -0
- package/src/components/switch/switch.stories.tsx +48 -0
- package/src/components/tabs/index.tsx +126 -0
- package/src/components/tabs/tabs.module.css +134 -0
- package/src/components/tabs/tabs.stories.tsx +88 -0
- package/src/components/text/index.tsx +69 -0
- package/src/components/text/text.module.css +76 -0
- package/src/components/text/text.stories.tsx +107 -0
- package/src/components/theme-provider/index.ts +2 -0
- package/src/components/theme-provider/theme-context.tsx +18 -0
- package/src/components/theme-provider/theme-provider.stories.tsx +47 -0
- package/src/components/theme-provider/theme-provider.tsx +77 -0
- package/src/components/timestamp/index.tsx +131 -0
- package/src/components/timestamp/timestamp.module.css +8 -0
- package/src/components/timestamp/timestamp.stories.tsx +37 -0
- package/src/components/toast/index.ts +2 -0
- package/src/components/toast/toast.module.css +163 -0
- package/src/components/toast/toast.stories.tsx +53 -0
- package/src/components/toast/toast.tsx +104 -0
- package/src/components/toast/use-toast-manager.ts +63 -0
- package/src/components/tooltip/index.tsx +61 -0
- package/src/components/tooltip/tooltip-arrow.tsx +17 -0
- package/src/components/tooltip/tooltip.module.css +44 -0
- package/src/components/tooltip/tooltip.stories.tsx +76 -0
- package/src/components/view/index.tsx +137 -0
- package/src/components/view/view.module.css +11 -0
- package/src/components/view/view.stories.tsx +131 -0
- package/src/components/view/view_colorway.module.css +280 -0
- package/src/components/view/view_interactive.module.css +127 -0
- package/src/components/view/view_loading.module.css +58 -0
- package/src/components/visually-hidden/index.ts +1 -0
- package/src/index.ts +49 -0
- package/src/integrations/react-markdown/index.tsx +134 -0
- package/src/integrations/react-markdown/react-markdown.module.css +62 -0
- package/src/integrations/react-markdown/react-markdown.stories.tsx +31 -0
- package/src/integrations/remix.ts +12 -0
- package/src/integrations/tailwind.css +173 -0
- package/src/integrations/twemoij/index.tsx +13 -0
- package/src/integrations/twemoij/twemoji.module.css +7 -0
- package/src/integrations/twemoij/twemoji.stories.tsx +40 -0
- package/src/stories/components/all-variants.tsx +40 -0
- package/src/stories/data.ts +72 -0
- package/src/stories/utils.ts +20 -0
- package/src/styles/core.css +153 -0
- package/src/styles/themes/dark.css +86 -0
- package/src/styles/themes/light.css +86 -0
- package/src/styles/tokens.ts +282 -0
- package/src/styles/transitions.module.css +31 -0
- package/stylelint.config.mjs +29 -0
- package/tsconfig.app.json +35 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +103 -0
- package/vitest.shims.d.ts +1 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { AllVariants } from "~/stories/components/all-variants";
|
|
4
|
+
import { sizes } from "~/stories/data";
|
|
5
|
+
import { tokens } from "~/styles/tokens";
|
|
6
|
+
|
|
7
|
+
import { Text } from ".";
|
|
8
|
+
import { Surface } from "../surface";
|
|
9
|
+
import { View } from "../view";
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "Typography/Text",
|
|
13
|
+
component: Text,
|
|
14
|
+
parameters: { layout: "centered" },
|
|
15
|
+
argTypes: {
|
|
16
|
+
color: {
|
|
17
|
+
control: "select",
|
|
18
|
+
options: ["inherit", "default", "dimmer", "dimmest"],
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
control: "select",
|
|
22
|
+
options: sizes,
|
|
23
|
+
},
|
|
24
|
+
maxLines: {
|
|
25
|
+
control: "number",
|
|
26
|
+
},
|
|
27
|
+
multiline: {
|
|
28
|
+
control: "boolean",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
} satisfies Meta<typeof Text>;
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
|
|
35
|
+
type Story = StoryObj<typeof meta>;
|
|
36
|
+
|
|
37
|
+
export const Default: Story = {
|
|
38
|
+
args: {
|
|
39
|
+
size: "md",
|
|
40
|
+
color: "default",
|
|
41
|
+
},
|
|
42
|
+
render: (props) => (
|
|
43
|
+
<Surface style={{ width: "512px", padding: tokens.space16 }} elevated>
|
|
44
|
+
<Text {...props}>
|
|
45
|
+
The quick brown fox jumps over the lazy dog. Swiftly, it leaped across
|
|
46
|
+
the murmuring stream, its bushy tail a blur of motion. The old badger,
|
|
47
|
+
observing from its cozy burrow, merely yawned, accustomed to such
|
|
48
|
+
energetic displays. A curious squirrel chattered from a nearby oak,
|
|
49
|
+
dropping an acorn in surprise.
|
|
50
|
+
</Text>
|
|
51
|
+
</Surface>
|
|
52
|
+
),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const AllSizes: Story = {
|
|
56
|
+
argTypes: {
|
|
57
|
+
size: {
|
|
58
|
+
table: {
|
|
59
|
+
disable: true,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
args: {
|
|
64
|
+
color: "default",
|
|
65
|
+
},
|
|
66
|
+
render: (args) => (
|
|
67
|
+
<AllVariants
|
|
68
|
+
variantName="size"
|
|
69
|
+
variants={sizes}
|
|
70
|
+
element={
|
|
71
|
+
<Text {...args}>The quick brown fox jumps over the lazy dog.</Text>
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const Multiline: Story = {
|
|
78
|
+
argTypes: {
|
|
79
|
+
maxLines: {
|
|
80
|
+
table: {
|
|
81
|
+
disable: true,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
multiline: {
|
|
85
|
+
table: {
|
|
86
|
+
disable: true,
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
args: {
|
|
91
|
+
color: "default",
|
|
92
|
+
size: "md",
|
|
93
|
+
},
|
|
94
|
+
render: (args) => (
|
|
95
|
+
<View style={{ maxWidth: tokens.space256 }}>
|
|
96
|
+
<Text multiline {...args}>
|
|
97
|
+
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
|
|
98
|
+
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
|
|
99
|
+
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
|
|
100
|
+
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
|
|
101
|
+
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
|
|
102
|
+
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
|
|
103
|
+
mollit anim id est laborum.
|
|
104
|
+
</Text>
|
|
105
|
+
</View>
|
|
106
|
+
),
|
|
107
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createContext, useContext } from "react";
|
|
2
|
+
|
|
3
|
+
interface ThemeContextType {
|
|
4
|
+
/** Current theme */
|
|
5
|
+
theme: string;
|
|
6
|
+
|
|
7
|
+
/** Set a new current theme */
|
|
8
|
+
setTheme: (theme: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const ThemeContext = createContext<ThemeContextType>({
|
|
12
|
+
theme: "light",
|
|
13
|
+
setTheme: () => {},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export function useThemeContext() {
|
|
17
|
+
return useContext(ThemeContext);
|
|
18
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { disable } from "~/stories/utils";
|
|
4
|
+
import { tokens } from "~/styles/tokens";
|
|
5
|
+
|
|
6
|
+
import { ThemeProvider } from ".";
|
|
7
|
+
import { Button } from "../button";
|
|
8
|
+
import { Surface } from "../surface";
|
|
9
|
+
import { Text } from "../text";
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "Theme Provider",
|
|
13
|
+
component: ThemeProvider,
|
|
14
|
+
parameters: { layout: "centered" },
|
|
15
|
+
argTypes: {
|
|
16
|
+
value: {
|
|
17
|
+
control: "inline-radio",
|
|
18
|
+
options: ["light", "dark"],
|
|
19
|
+
},
|
|
20
|
+
overrideBody: {
|
|
21
|
+
control: "boolean",
|
|
22
|
+
},
|
|
23
|
+
...disable(["interactive", "loading"]),
|
|
24
|
+
},
|
|
25
|
+
} satisfies Meta<typeof ThemeProvider>;
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
|
|
29
|
+
type Story = StoryObj<typeof meta>;
|
|
30
|
+
|
|
31
|
+
export const Default: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
value: "light",
|
|
34
|
+
defaultValue: "light",
|
|
35
|
+
overrideBody: true,
|
|
36
|
+
},
|
|
37
|
+
render: (args) => (
|
|
38
|
+
<ThemeProvider {...args}>
|
|
39
|
+
<Surface elevated style={{ padding: tokens.space16, gap: tokens.space4 }}>
|
|
40
|
+
<Text>Hello there!</Text>
|
|
41
|
+
<Button interactive="primary_fill" leftIcon="square-line">
|
|
42
|
+
A button
|
|
43
|
+
</Button>
|
|
44
|
+
</Surface>
|
|
45
|
+
</ThemeProvider>
|
|
46
|
+
),
|
|
47
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
import { View, type ViewProps } from "../view";
|
|
4
|
+
import { ThemeContext } from "./theme-context";
|
|
5
|
+
|
|
6
|
+
export type ThemeProviderProps = ViewProps & {
|
|
7
|
+
/** Override theme. */
|
|
8
|
+
value?: string;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Default theme if you don't want to manage it yourself.
|
|
12
|
+
* Defaults to user preferences and then light.
|
|
13
|
+
*/
|
|
14
|
+
defaultValue?: string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Override theme set on body.
|
|
18
|
+
* This prevents mismatched themes on overscroll.
|
|
19
|
+
* Defaults to true.
|
|
20
|
+
*/
|
|
21
|
+
overrideBody?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function ThemeProvider({
|
|
25
|
+
value,
|
|
26
|
+
defaultValue,
|
|
27
|
+
overrideBody = true,
|
|
28
|
+
...props
|
|
29
|
+
}: ThemeProviderProps) {
|
|
30
|
+
const isControlled = value !== undefined;
|
|
31
|
+
const [uncontrolledTheme, setUncontrolledTheme] = useState<string>(
|
|
32
|
+
() => defaultValue || getDefaultTheme(),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const theme = isControlled ? value : uncontrolledTheme;
|
|
36
|
+
const setTheme = setUncontrolledTheme;
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
// also set theme of the body b/c overscroll will probably show mismatched themes
|
|
40
|
+
if (overrideBody) {
|
|
41
|
+
document.body.dataset.theme = theme;
|
|
42
|
+
}
|
|
43
|
+
}, [theme, overrideBody]);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<ThemeContext.Provider value={{ theme, setTheme }}>
|
|
47
|
+
<View data-theme={theme} {...props} />
|
|
48
|
+
</ThemeContext.Provider>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Try to get theme from user system preferences.
|
|
54
|
+
* Otherwise just default to "light".
|
|
55
|
+
* @returns Theme name
|
|
56
|
+
*/
|
|
57
|
+
function getDefaultTheme(): string {
|
|
58
|
+
if (typeof window === "undefined") {
|
|
59
|
+
return "light";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// try & get theme from user preferences
|
|
63
|
+
try {
|
|
64
|
+
if (
|
|
65
|
+
window.matchMedia &&
|
|
66
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
67
|
+
) {
|
|
68
|
+
return "dark";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return "light";
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(error);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return "light";
|
|
77
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import clsx from "clsx";
|
|
2
|
+
import { format } from "date-fns/format";
|
|
3
|
+
import { formatDistanceToNow } from "date-fns/formatDistanceToNow";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
|
|
6
|
+
import { Text, type TextProps } from "../text";
|
|
7
|
+
import { Tooltip } from "../tooltip";
|
|
8
|
+
import { View } from "../view";
|
|
9
|
+
|
|
10
|
+
import styles from "./timestamp.module.css";
|
|
11
|
+
|
|
12
|
+
export const A_LONG_TIME_AGO_DATE = new Date(0);
|
|
13
|
+
|
|
14
|
+
export interface TimestampProps {
|
|
15
|
+
date: string | number | Date;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* By default (`'relative'`), the timestamp will show a relative
|
|
19
|
+
* date and an absolute date on hover.
|
|
20
|
+
*
|
|
21
|
+
* Passing `'absolute'` will show the absolute date instead. During server
|
|
22
|
+
* side rendering, this option is not respected, it will use relative date/time
|
|
23
|
+
* instead to avoid hydration issues.
|
|
24
|
+
*
|
|
25
|
+
* This value can also be set to `'switch'` which will show the relative
|
|
26
|
+
* date but allow the user to switch to absolute by clicking on the component
|
|
27
|
+
* by clicking on the timestamp, all "switch" timestamps are synchronized
|
|
28
|
+
* with a global boolean.
|
|
29
|
+
*/
|
|
30
|
+
dateFormat?: "relative" | "absolute" | "switch";
|
|
31
|
+
|
|
32
|
+
color?: TextProps["color"];
|
|
33
|
+
|
|
34
|
+
size?: TextProps["size"];
|
|
35
|
+
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const Timestamp = ({
|
|
40
|
+
date,
|
|
41
|
+
dateFormat: initialDateFormat = "relative",
|
|
42
|
+
color,
|
|
43
|
+
size,
|
|
44
|
+
className,
|
|
45
|
+
}: TimestampProps) => {
|
|
46
|
+
const switchable = initialDateFormat === "switch";
|
|
47
|
+
const [showAbsolute, setShowAbsolute] = useState(
|
|
48
|
+
() => initialDateFormat === "absolute",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const { triggerDate, contentDate } = getDate(date, showAbsolute);
|
|
52
|
+
const triggerText = (
|
|
53
|
+
<Text
|
|
54
|
+
color={color}
|
|
55
|
+
size={size}
|
|
56
|
+
className={clsx(styles["timestamp"], className)}
|
|
57
|
+
>
|
|
58
|
+
{triggerDate}
|
|
59
|
+
</Text>
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Tooltip
|
|
64
|
+
trigger={
|
|
65
|
+
switchable ? (
|
|
66
|
+
<View
|
|
67
|
+
render={<button />}
|
|
68
|
+
className={styles["timestamp_interactive"]}
|
|
69
|
+
onClick={() => setShowAbsolute((absolute) => !absolute)}
|
|
70
|
+
interactive="no-fill"
|
|
71
|
+
role="switch"
|
|
72
|
+
tabIndex={0}
|
|
73
|
+
aria-checked={showAbsolute}
|
|
74
|
+
onKeyDown={(e) => {
|
|
75
|
+
if (e.code === "Space" || e.code === "Enter") {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
setShowAbsolute((absolute) => !absolute);
|
|
78
|
+
}
|
|
79
|
+
}}
|
|
80
|
+
>
|
|
81
|
+
{triggerText}
|
|
82
|
+
</View>
|
|
83
|
+
) : (
|
|
84
|
+
<span>{triggerText}</span>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
{contentDate}
|
|
89
|
+
</Tooltip>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the absolute and relative dates \
|
|
95
|
+
* Also assigns them to the correct properties (content vs trigger)
|
|
96
|
+
* @param date Timestamp
|
|
97
|
+
* @param showAbsolute Show the absolute date if true, relative if false
|
|
98
|
+
* @returns The dates of the tooltip content and tooltip trigger
|
|
99
|
+
*/
|
|
100
|
+
function getDate(date: TimestampProps["date"], showAbsolute: boolean) {
|
|
101
|
+
const absoluteDate = toAbsoluteDate(date);
|
|
102
|
+
const relativeDate = toRelativeDate(date);
|
|
103
|
+
|
|
104
|
+
// default is relative date
|
|
105
|
+
let contentDate = absoluteDate;
|
|
106
|
+
let triggerDate = relativeDate;
|
|
107
|
+
|
|
108
|
+
// if absolute, swap the dates
|
|
109
|
+
if (showAbsolute) {
|
|
110
|
+
contentDate = relativeDate;
|
|
111
|
+
triggerDate = absoluteDate;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
contentDate,
|
|
116
|
+
triggerDate,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function toAbsoluteDate(date: TimestampProps["date"]) {
|
|
121
|
+
return format(new Date(date), "h:mm bbb, MMM dd, yyyy");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function toRelativeDate(date: TimestampProps["date"]) {
|
|
125
|
+
const asDate = new Date(date);
|
|
126
|
+
if (asDate <= A_LONG_TIME_AGO_DATE) {
|
|
127
|
+
return "a long time ago";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return formatDistanceToNow(asDate, { addSuffix: true });
|
|
131
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { sizes, textColors } from "~/stories/data";
|
|
4
|
+
import { disable } from "~/stories/utils";
|
|
5
|
+
|
|
6
|
+
import { Timestamp } from ".";
|
|
7
|
+
|
|
8
|
+
const meta = {
|
|
9
|
+
title: "Timestamp",
|
|
10
|
+
component: Timestamp,
|
|
11
|
+
parameters: { layout: "centered" },
|
|
12
|
+
argTypes: {
|
|
13
|
+
color: {
|
|
14
|
+
control: "select",
|
|
15
|
+
options: textColors,
|
|
16
|
+
},
|
|
17
|
+
size: {
|
|
18
|
+
control: "select",
|
|
19
|
+
options: sizes,
|
|
20
|
+
},
|
|
21
|
+
...disable(["className"]),
|
|
22
|
+
},
|
|
23
|
+
} satisfies Meta<typeof Timestamp>;
|
|
24
|
+
|
|
25
|
+
export default meta;
|
|
26
|
+
|
|
27
|
+
type Story = StoryObj<typeof meta>;
|
|
28
|
+
|
|
29
|
+
export const Default: Story = {
|
|
30
|
+
args: {
|
|
31
|
+
date: new Date(),
|
|
32
|
+
dateFormat: "relative",
|
|
33
|
+
size: "md",
|
|
34
|
+
color: "default",
|
|
35
|
+
},
|
|
36
|
+
render: (args) => <Timestamp key={args.dateFormat} {...args} />,
|
|
37
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
.toast__viewport {
|
|
2
|
+
position: fixed;
|
|
3
|
+
z-index: 1;
|
|
4
|
+
margin: 0 auto;
|
|
5
|
+
inset: auto var(--space-8) var(--space-8) auto;
|
|
6
|
+
width: var(--space-256);
|
|
7
|
+
|
|
8
|
+
@media (width >= 512px) {
|
|
9
|
+
right: var(--space-16);
|
|
10
|
+
bottom: var(--space-16);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.toast {
|
|
15
|
+
/* we add -custom to prevent collisions with Base UI */
|
|
16
|
+
--toast-custom-gap: var(--space-12);
|
|
17
|
+
--toast-custom-peek: var(--space-12);
|
|
18
|
+
--toast-custom-scale: calc(max(0, 1 - (var(--toast-index) * 0.1)));
|
|
19
|
+
--toast-custom-shrink: calc(1 - var(--toast-custom-scale));
|
|
20
|
+
--toast-custom-height: var(--toast-frontmost-height, var(--toast-height));
|
|
21
|
+
--toast-custom-offset-y: calc(
|
|
22
|
+
var(--toast-offset-y) * -1 +
|
|
23
|
+
(var(--toast-index) * var(--toast-custom-gap) * -1) +
|
|
24
|
+
var(--toast-swipe-movement-y)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
display: flex;
|
|
28
|
+
position: absolute;
|
|
29
|
+
right: 0;
|
|
30
|
+
bottom: 0;
|
|
31
|
+
left: auto;
|
|
32
|
+
flex-direction: row;
|
|
33
|
+
align-items: center;
|
|
34
|
+
gap: var(--space-8);
|
|
35
|
+
transform: translateX(var(--toast-swipe-movement-x))
|
|
36
|
+
translateY(
|
|
37
|
+
calc(
|
|
38
|
+
var(--toast-swipe-movement-y) -
|
|
39
|
+
(var(--toast-index) * var(--toast-custom-peek)) -
|
|
40
|
+
(var(--toast-custom-shrink) * var(--toast-custom-height))
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
scale(var(--toast-custom-scale));
|
|
44
|
+
transform-origin: bottom center;
|
|
45
|
+
z-index: calc(1000 - var(--toast-index));
|
|
46
|
+
transition:
|
|
47
|
+
transform 600ms cubic-bezier(0.22, 1, 0.36, 1),
|
|
48
|
+
opacity 600ms,
|
|
49
|
+
height var(--transition-duration-snappy);
|
|
50
|
+
cursor: default;
|
|
51
|
+
box-sizing: border-box;
|
|
52
|
+
margin: 0 0 0 auto;
|
|
53
|
+
border-width: 1px;
|
|
54
|
+
border-style: solid;
|
|
55
|
+
border-radius: var(--border-radius-default);
|
|
56
|
+
background-clip: padding-box;
|
|
57
|
+
width: 100%;
|
|
58
|
+
height: var(--toast-custom-height);
|
|
59
|
+
user-select: none;
|
|
60
|
+
|
|
61
|
+
&[data-expanded] {
|
|
62
|
+
transform: translateX(var(--toast-swipe-movement-x))
|
|
63
|
+
translateY(var(--toast-custom-offset-y));
|
|
64
|
+
height: var(--toast-height);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
&[data-starting-style],
|
|
68
|
+
&[data-ending-style] {
|
|
69
|
+
transform: translateY(150%);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
&[data-limited] {
|
|
73
|
+
opacity: 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
&[data-ending-style] {
|
|
77
|
+
opacity: 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
&[data-ending-style][data-swipe-direction="up"] {
|
|
81
|
+
transform: translateY(calc(var(--toast-swipe-movement-y) - 50%));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&[data-ending-style][data-swipe-direction="left"] {
|
|
85
|
+
transform: translateX(calc(var(--toast-swipe-movement-x) - 50%))
|
|
86
|
+
translateY(var(--toast-custom-offset-y));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
&[data-ending-style][data-swipe-direction="right"] {
|
|
90
|
+
transform: translateX(calc(var(--toast-swipe-movement-x) + 50%))
|
|
91
|
+
translateY(var(--toast-custom-offset-y));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
&[data-ending-style][data-swipe-direction="down"] {
|
|
95
|
+
transform: translateY(calc(var(--toast-swipe-movement-y) + 50%));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
&::after {
|
|
99
|
+
display: block;
|
|
100
|
+
position: absolute;
|
|
101
|
+
top: 100%;
|
|
102
|
+
left: 0;
|
|
103
|
+
width: 100%;
|
|
104
|
+
height: calc(var(--toast-custom-gap) + 1px);
|
|
105
|
+
content: "";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.toast_variant_neutral {
|
|
110
|
+
border-color: var(--surface-interactive-border);
|
|
111
|
+
background: var(--surface-background);
|
|
112
|
+
color: var(--foreground-default);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.toast_variant_colorway {
|
|
116
|
+
border-color: var(--view-colorway-dimmer);
|
|
117
|
+
background: var(--view-colorway-dimmest);
|
|
118
|
+
color: var(--view-colorway-strongest);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.toast__content {
|
|
122
|
+
flex-grow: 1;
|
|
123
|
+
flex-shrink: 1;
|
|
124
|
+
gap: var(--space-16);
|
|
125
|
+
transition: opacity var(--transition-duration-snappy)
|
|
126
|
+
var(--transition-timing-function-snappy);
|
|
127
|
+
padding: var(--space-12);
|
|
128
|
+
overflow: hidden;
|
|
129
|
+
|
|
130
|
+
&[data-behind] {
|
|
131
|
+
opacity: 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
&[data-expanded] {
|
|
135
|
+
opacity: 1;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.toast__header {
|
|
140
|
+
flex: 1 1 auto;
|
|
141
|
+
gap: var(--space-4);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.toast__row {
|
|
145
|
+
flex: 1 1 auto;
|
|
146
|
+
flex-direction: row;
|
|
147
|
+
justify-content: space-between;
|
|
148
|
+
gap: var(--space-4);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.toast__title {
|
|
152
|
+
flex: 1 1 auto;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.toast__description {
|
|
156
|
+
flex: 1 1 auto;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.toast__close {
|
|
160
|
+
position: absolute;
|
|
161
|
+
top: 0;
|
|
162
|
+
right: 0;
|
|
163
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { ToastProvider, ToastViewport, useToastManager } from ".";
|
|
4
|
+
import { Button } from "../button";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Toast",
|
|
8
|
+
component: ToastProvider,
|
|
9
|
+
parameters: { layout: "centered" },
|
|
10
|
+
argTypes: {
|
|
11
|
+
limit: {
|
|
12
|
+
control: "number",
|
|
13
|
+
},
|
|
14
|
+
timeout: {
|
|
15
|
+
control: "number",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
} satisfies Meta<typeof ToastProvider>;
|
|
19
|
+
|
|
20
|
+
export default meta;
|
|
21
|
+
|
|
22
|
+
type Story = StoryObj<typeof meta>;
|
|
23
|
+
|
|
24
|
+
export const Default: Story = {
|
|
25
|
+
render: (args) => (
|
|
26
|
+
<ToastProvider {...args}>
|
|
27
|
+
<ToastButton />
|
|
28
|
+
<ToastViewport />
|
|
29
|
+
</ToastProvider>
|
|
30
|
+
),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function ToastButton() {
|
|
34
|
+
const toastManager = useToastManager();
|
|
35
|
+
|
|
36
|
+
function createToast() {
|
|
37
|
+
toastManager.add({
|
|
38
|
+
title: "Toast created",
|
|
39
|
+
description:
|
|
40
|
+
"This is a toast notification. You got rid of that or something.",
|
|
41
|
+
action: {
|
|
42
|
+
leftIcon: "arrow-go-back-line",
|
|
43
|
+
children: "Undo",
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Button interactive onClick={createToast}>
|
|
50
|
+
Create toast
|
|
51
|
+
</Button>
|
|
52
|
+
);
|
|
53
|
+
}
|