@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.
Files changed (170) hide show
  1. package/.husky/pre-commit +1 -0
  2. package/.storybook/main.ts +25 -0
  3. package/.storybook/preview-head.html +6 -0
  4. package/.storybook/preview.tsx +48 -0
  5. package/.storybook/vitest.setup.ts +8 -0
  6. package/README.md +11 -0
  7. package/eslint.config.mjs +29 -0
  8. package/lint-staged.config.js +15 -0
  9. package/package.json +95 -0
  10. package/pnpm-workspace.yaml +2 -0
  11. package/postcss.config.mjs +7 -0
  12. package/prettier.config.mjs +24 -0
  13. package/scripts/build-icon-types.ts +38 -0
  14. package/src/-types.ts +8 -0
  15. package/src/-utils.tsx +64 -0
  16. package/src/components/accordion/accordion.module.css +44 -0
  17. package/src/components/accordion/accordion.stories.tsx +36 -0
  18. package/src/components/accordion/index.tsx +67 -0
  19. package/src/components/alert-dialog/alert-dialog.module.css +5 -0
  20. package/src/components/alert-dialog/alert-dialog.stories.tsx +53 -0
  21. package/src/components/alert-dialog/index.tsx +138 -0
  22. package/src/components/anchor/anchor.module.css +18 -0
  23. package/src/components/anchor/anchor.stories.tsx +28 -0
  24. package/src/components/anchor/index.tsx +45 -0
  25. package/src/components/avatar/avatar.module.css +56 -0
  26. package/src/components/avatar/avatar.stories.tsx +61 -0
  27. package/src/components/avatar/index.tsx +82 -0
  28. package/src/components/badge/badge.module.css +35 -0
  29. package/src/components/badge/badge.stories.tsx +60 -0
  30. package/src/components/badge/index.tsx +71 -0
  31. package/src/components/button/button.module.css +42 -0
  32. package/src/components/button/button.stories.tsx +108 -0
  33. package/src/components/button/index.tsx +63 -0
  34. package/src/components/checkbox/checkbox.module.css +36 -0
  35. package/src/components/checkbox/checkbox.stories.tsx +21 -0
  36. package/src/components/checkbox/index.tsx +41 -0
  37. package/src/components/code/code.module.css +20 -0
  38. package/src/components/code/code.stories.tsx +42 -0
  39. package/src/components/code/index.tsx +73 -0
  40. package/src/components/collapse/collapse.module.css +27 -0
  41. package/src/components/collapse/collapse.stories.tsx +27 -0
  42. package/src/components/collapse/index.tsx +59 -0
  43. package/src/components/command/command.module.css +95 -0
  44. package/src/components/command/command.stories.tsx +38 -0
  45. package/src/components/command/index.tsx +108 -0
  46. package/src/components/context-menu/context-menu.module.css +36 -0
  47. package/src/components/context-menu/context-menu.stories.tsx +99 -0
  48. package/src/components/context-menu/index.tsx +242 -0
  49. package/src/components/dialog/dialog.module.css +71 -0
  50. package/src/components/dialog/dialog.stories.tsx +29 -0
  51. package/src/components/dialog/index.tsx +148 -0
  52. package/src/components/heading/heading.module.css +3 -0
  53. package/src/components/heading/heading.stories.tsx +52 -0
  54. package/src/components/heading/index.tsx +112 -0
  55. package/src/components/icon/icon-names.ts +3189 -0
  56. package/src/components/icon/icon.module.css +36 -0
  57. package/src/components/icon/icon.stories.tsx +40 -0
  58. package/src/components/icon/index.tsx +60 -0
  59. package/src/components/icon-button/icon-button.module.css +33 -0
  60. package/src/components/icon-button/icon-button.stories.tsx +59 -0
  61. package/src/components/icon-button/index.tsx +48 -0
  62. package/src/components/inline-code/index.tsx +29 -0
  63. package/src/components/inline-code/inline-code.module.css +13 -0
  64. package/src/components/inline-code/inline-code.stories.tsx +31 -0
  65. package/src/components/input/index.tsx +22 -0
  66. package/src/components/input/input.module.css +23 -0
  67. package/src/components/input/input.stories.tsx +52 -0
  68. package/src/components/meter/index.tsx +55 -0
  69. package/src/components/meter/meter.module.css +23 -0
  70. package/src/components/meter/meter.stories.tsx +31 -0
  71. package/src/components/multiline-input/index.tsx +58 -0
  72. package/src/components/multiline-input/multiline-input.stories.tsx +26 -0
  73. package/src/components/number-input/index.tsx +74 -0
  74. package/src/components/number-input/number-input.module.css +41 -0
  75. package/src/components/number-input/number-input.stories.tsx +24 -0
  76. package/src/components/password-input/index.tsx +24 -0
  77. package/src/components/password-input/password-input.module.css +10 -0
  78. package/src/components/password-input/password-input.stories.tsx +24 -0
  79. package/src/components/pill/index.tsx +45 -0
  80. package/src/components/pill/pill.module.css +22 -0
  81. package/src/components/pill/pill.stories.tsx +83 -0
  82. package/src/components/popover/index.tsx +94 -0
  83. package/src/components/popover/popover.module.css +8 -0
  84. package/src/components/popover/popover.stories.tsx +53 -0
  85. package/src/components/preview-card/index.tsx +68 -0
  86. package/src/components/preview-card/preview-card.module.css +5 -0
  87. package/src/components/preview-card/preview-card.stories.tsx +58 -0
  88. package/src/components/radio/index.tsx +67 -0
  89. package/src/components/radio/radio-group.module.css +5 -0
  90. package/src/components/radio/radio.module.css +36 -0
  91. package/src/components/radio/radio.stories.tsx +27 -0
  92. package/src/components/search-bar/index.tsx +60 -0
  93. package/src/components/search-bar/search-bar.module.css +29 -0
  94. package/src/components/search-bar/search-bar.stories.tsx +37 -0
  95. package/src/components/select/index.tsx +132 -0
  96. package/src/components/select/select.module.css +63 -0
  97. package/src/components/select/select.stories.tsx +49 -0
  98. package/src/components/separator/index.tsx +28 -0
  99. package/src/components/separator/separator.module.css +24 -0
  100. package/src/components/separator/separator.stories.tsx +40 -0
  101. package/src/components/slider/index.tsx +28 -0
  102. package/src/components/slider/slider.module.css +52 -0
  103. package/src/components/slider/slider.stories.tsx +53 -0
  104. package/src/components/spinner/index.tsx +14 -0
  105. package/src/components/spinner/spinner.module.css +13 -0
  106. package/src/components/spinner/spinner.stories.tsx +17 -0
  107. package/src/components/stacked-avatars/index.tsx +88 -0
  108. package/src/components/stacked-avatars/stacked-avatars.module.css +79 -0
  109. package/src/components/stacked-avatars/stacked-avatars.stories.tsx +48 -0
  110. package/src/components/status-banner/index.tsx +96 -0
  111. package/src/components/status-banner/status-banner.module.css +52 -0
  112. package/src/components/status-banner/status-banner.stories.tsx +44 -0
  113. package/src/components/surface/index.tsx +83 -0
  114. package/src/components/surface/surface.module.css +35 -0
  115. package/src/components/surface/surface.stories.tsx +84 -0
  116. package/src/components/switch/index.tsx +23 -0
  117. package/src/components/switch/switch.module.css +45 -0
  118. package/src/components/switch/switch.stories.tsx +48 -0
  119. package/src/components/tabs/index.tsx +126 -0
  120. package/src/components/tabs/tabs.module.css +134 -0
  121. package/src/components/tabs/tabs.stories.tsx +88 -0
  122. package/src/components/text/index.tsx +69 -0
  123. package/src/components/text/text.module.css +76 -0
  124. package/src/components/text/text.stories.tsx +107 -0
  125. package/src/components/theme-provider/index.ts +2 -0
  126. package/src/components/theme-provider/theme-context.tsx +18 -0
  127. package/src/components/theme-provider/theme-provider.stories.tsx +47 -0
  128. package/src/components/theme-provider/theme-provider.tsx +77 -0
  129. package/src/components/timestamp/index.tsx +131 -0
  130. package/src/components/timestamp/timestamp.module.css +8 -0
  131. package/src/components/timestamp/timestamp.stories.tsx +37 -0
  132. package/src/components/toast/index.ts +2 -0
  133. package/src/components/toast/toast.module.css +163 -0
  134. package/src/components/toast/toast.stories.tsx +53 -0
  135. package/src/components/toast/toast.tsx +104 -0
  136. package/src/components/toast/use-toast-manager.ts +63 -0
  137. package/src/components/tooltip/index.tsx +61 -0
  138. package/src/components/tooltip/tooltip-arrow.tsx +17 -0
  139. package/src/components/tooltip/tooltip.module.css +44 -0
  140. package/src/components/tooltip/tooltip.stories.tsx +76 -0
  141. package/src/components/view/index.tsx +137 -0
  142. package/src/components/view/view.module.css +11 -0
  143. package/src/components/view/view.stories.tsx +131 -0
  144. package/src/components/view/view_colorway.module.css +280 -0
  145. package/src/components/view/view_interactive.module.css +127 -0
  146. package/src/components/view/view_loading.module.css +58 -0
  147. package/src/components/visually-hidden/index.ts +1 -0
  148. package/src/index.ts +49 -0
  149. package/src/integrations/react-markdown/index.tsx +134 -0
  150. package/src/integrations/react-markdown/react-markdown.module.css +62 -0
  151. package/src/integrations/react-markdown/react-markdown.stories.tsx +31 -0
  152. package/src/integrations/remix.ts +12 -0
  153. package/src/integrations/tailwind.css +173 -0
  154. package/src/integrations/twemoij/index.tsx +13 -0
  155. package/src/integrations/twemoij/twemoji.module.css +7 -0
  156. package/src/integrations/twemoij/twemoji.stories.tsx +40 -0
  157. package/src/stories/components/all-variants.tsx +40 -0
  158. package/src/stories/data.ts +72 -0
  159. package/src/stories/utils.ts +20 -0
  160. package/src/styles/core.css +153 -0
  161. package/src/styles/themes/dark.css +86 -0
  162. package/src/styles/themes/light.css +86 -0
  163. package/src/styles/tokens.ts +282 -0
  164. package/src/styles/transitions.module.css +31 -0
  165. package/stylelint.config.mjs +29 -0
  166. package/tsconfig.app.json +35 -0
  167. package/tsconfig.json +7 -0
  168. package/tsconfig.node.json +26 -0
  169. package/vite.config.ts +103 -0
  170. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,74 @@
1
+ import { NumberField } from "@base-ui/react/number-field";
2
+ import clsx from "clsx";
3
+ import { useId } from "react";
4
+
5
+ import { IconButton } from "../icon-button";
6
+ import { Input } from "../input";
7
+ import { Separator } from "../separator";
8
+ import { Text } from "../text";
9
+ import { View } from "../view";
10
+
11
+ import styles from "./number-input.module.css";
12
+
13
+ // TODO find icon for scrub area
14
+
15
+ export type NumberInputProps = NumberField.Root.Props & {
16
+ /** Optional label, includes scrub area */
17
+ label?: string;
18
+ };
19
+
20
+ export function NumberInput({
21
+ label,
22
+ /** Apply className to NumberField.Input */
23
+ className,
24
+ /** Apply style to NumberField.Input */
25
+ style,
26
+ ...props
27
+ }: NumberInputProps) {
28
+ const defaultId = useId();
29
+ const id = props.id || defaultId;
30
+ return (
31
+ <NumberField.Root
32
+ id={id}
33
+ className={styles["number-input__root"]}
34
+ {...props}
35
+ >
36
+ {label ? (
37
+ <NumberField.ScrubArea className={styles["number-input__scrub-area"]}>
38
+ <label htmlFor={id}>
39
+ <Text color="dimmer">{label}</Text>
40
+ </label>
41
+ </NumberField.ScrubArea>
42
+ ) : null}
43
+
44
+ <NumberField.Group className={styles["number-input__group"]}>
45
+ <Input
46
+ render={
47
+ <NumberField.Input
48
+ className={clsx(styles["number-input"], className)}
49
+ style={style}
50
+ />
51
+ }
52
+ />
53
+ <View className={styles["number-input__controls"]}>
54
+ <IconButton
55
+ size="sm"
56
+ alt="Decrement"
57
+ icon="subtract-line"
58
+ render={<NumberField.Decrement />}
59
+ />
60
+ <Separator
61
+ orientation="vertical"
62
+ className={styles["number-input__separator"]}
63
+ />
64
+ <IconButton
65
+ size="sm"
66
+ alt="Increment"
67
+ icon="add-line"
68
+ render={<NumberField.Increment />}
69
+ />
70
+ </View>
71
+ </NumberField.Group>
72
+ </NumberField.Root>
73
+ );
74
+ }
@@ -0,0 +1,41 @@
1
+ .number-input__root {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--space-2);
5
+ }
6
+
7
+ /*
8
+ https://stackoverflow.com/questions/72789466/remove-the-up-down-increment-decrement-buttons-on-number-inputs-using-css
9
+ Hide increment/decrement buttons on input
10
+ */
11
+ .number-input[type="number"]::-webkit-inner-spin-button,
12
+ .number-input[type="number"]::-webkit-outer-spin-button {
13
+ /* stylelint-disable-next-line */
14
+ -webkit-appearance: none;
15
+ padding-right: var(--space-32);
16
+ text-overflow: ellipsis;
17
+ }
18
+
19
+ .number-input__scrub-area {
20
+ cursor: ew-resize;
21
+ user-select: none;
22
+ }
23
+
24
+ .number-input__group {
25
+ position: relative;
26
+ }
27
+
28
+ .number-input__controls {
29
+ display: flex;
30
+ position: absolute;
31
+ top: 50%;
32
+ right: var(--space-4);
33
+ flex-direction: row;
34
+ align-items: center;
35
+ gap: var(--space-4);
36
+ transform: translateY(-50%);
37
+ }
38
+
39
+ .number-input__separator {
40
+ height: var(--space-16);
41
+ }
@@ -0,0 +1,24 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { tokens } from "~/styles/tokens";
4
+
5
+ import { NumberInput } from ".";
6
+
7
+ const meta = {
8
+ title: "Input/Number",
9
+ component: NumberInput,
10
+ parameters: { layout: "centered" },
11
+ } satisfies Meta<typeof NumberInput>;
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ label: "Amount",
20
+ },
21
+ render: (args) => (
22
+ <NumberInput {...args} style={{ width: tokens.space256 }} />
23
+ ),
24
+ };
@@ -0,0 +1,24 @@
1
+ import { useState } from "react";
2
+
3
+ import { IconButton } from "../icon-button";
4
+ import { Input, type InputProps } from "../input";
5
+ import { View } from "../view";
6
+
7
+ import styles from "./password-input.module.css";
8
+
9
+ export function PasswordInput(props: Omit<InputProps, "type">) {
10
+ const [show, setShow] = useState(false);
11
+ return (
12
+ <View className={styles["password-input__root"]}>
13
+ <Input type={show ? "text" : "password"} {...props} />
14
+ <IconButton
15
+ alt={show ? "Hide password" : "Show password"}
16
+ icon={show ? "eye-line" : "eye-close-line"}
17
+ className={styles["password-input__eye"]}
18
+ type="button"
19
+ size="sm"
20
+ onClick={() => setShow((prevShow) => !prevShow)}
21
+ />
22
+ </View>
23
+ );
24
+ }
@@ -0,0 +1,10 @@
1
+ .password-input__root {
2
+ position: relative;
3
+ }
4
+
5
+ .password-input__eye {
6
+ position: absolute;
7
+ top: 50%;
8
+ right: var(--space-2);
9
+ transform: translateY(-50%);
10
+ }
@@ -0,0 +1,24 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { tokens } from "~/styles/tokens";
4
+
5
+ import { PasswordInput } from ".";
6
+
7
+ const meta = {
8
+ title: "Input/Password",
9
+ component: PasswordInput,
10
+ parameters: { layout: "centered" },
11
+ } satisfies Meta<typeof PasswordInput>;
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ placeholder: "Password",
20
+ },
21
+ render: (args) => (
22
+ <PasswordInput {...args} style={{ width: tokens.space256 }} />
23
+ ),
24
+ };
@@ -0,0 +1,45 @@
1
+ import clsx from "clsx";
2
+
3
+ import { textify } from "~/-utils";
4
+
5
+ import { Icon } from "../icon";
6
+ import type { IconName } from "../icon/icon-names";
7
+ import { View, type ViewProps } from "../view";
8
+
9
+ import styles from "./pill.module.css";
10
+
11
+ export type PillProps = ViewProps & {
12
+ /**
13
+ * Icon to display on the left side of the pill content
14
+ */
15
+ leftIcon?: IconName;
16
+
17
+ /**
18
+ * Icon to display on the right side of the pill content
19
+ */
20
+ rightIcon?: IconName;
21
+ };
22
+
23
+ export function Pill({
24
+ leftIcon,
25
+ rightIcon,
26
+ className,
27
+ children,
28
+ ...props
29
+ }: PillProps) {
30
+ return (
31
+ <View
32
+ className={clsx(
33
+ styles["pill"],
34
+ leftIcon && styles["pill_icon_left"],
35
+ rightIcon && styles["pill_icon_right"],
36
+ className,
37
+ )}
38
+ {...props}
39
+ >
40
+ {leftIcon ? <Icon name={leftIcon} /> : null}
41
+ {textify(children, { color: "inherit" })}
42
+ {rightIcon ? <Icon name={rightIcon} /> : null}
43
+ </View>
44
+ );
45
+ }
@@ -0,0 +1,22 @@
1
+ .pill {
2
+ flex-shrink: 1;
3
+ flex-direction: row;
4
+ justify-content: center;
5
+ align-items: center;
6
+ gap: var(--space-4);
7
+ border-radius: var(--border-radius-round) !important;
8
+ padding-right: var(--space-8);
9
+ padding-left: var(--space-8);
10
+ height: var(--space-24);
11
+ font-size: var(--font-size-small);
12
+ user-select: none;
13
+ white-space: nowrap;
14
+ }
15
+
16
+ .pill_icon_left {
17
+ padding-left: var(--space-4);
18
+ }
19
+
20
+ .pill_icon_right {
21
+ padding-right: var(--space-4);
22
+ }
@@ -0,0 +1,83 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { colors, colorway, interactiveStyles } from "~/stories/data";
4
+ import { disable } from "~/stories/utils";
5
+ import { tokens } from "~/styles/tokens";
6
+
7
+ import { Pill } from ".";
8
+ import { Text } from "../text";
9
+ import { View } from "../view";
10
+
11
+ const meta = {
12
+ title: "Pill",
13
+ component: Pill,
14
+ parameters: { layout: "centered" },
15
+ argTypes: {
16
+ ...disable(["color", "loading"]),
17
+ interactive: {
18
+ control: "select",
19
+ options: [...interactiveStyles, ...colorway],
20
+ },
21
+ },
22
+ } satisfies Meta<typeof Pill>;
23
+
24
+ export default meta;
25
+
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ export const Default: Story = {
29
+ args: {
30
+ interactive: "primary_mute-static",
31
+ leftIcon: "time-line",
32
+ },
33
+ render: (args) => <Pill {...args} children="Beginner friendly" />,
34
+ };
35
+
36
+ export const AllVariants: Story = {
37
+ argTypes: {
38
+ ...disable(["interactive", "color", "loading"]),
39
+ },
40
+ render: () => (
41
+ <View style={{ gap: tokens.space8 }}>
42
+ <View
43
+ style={{
44
+ flexDirection: "row",
45
+ alignItems: "center",
46
+ gap: tokens.space8,
47
+ width: "100%",
48
+ }}
49
+ >
50
+ <Pill interactive="fill">#python</Pill>
51
+ <Pill interactive="outline">#game</Pill>
52
+ <Text size="sm">interactive</Text>
53
+ </View>
54
+
55
+ {colors.map((color) => (
56
+ <View
57
+ style={{
58
+ flexDirection: "row",
59
+ alignItems: "center",
60
+ gap: tokens.space8,
61
+ }}
62
+ key={color}
63
+ >
64
+ <Pill interactive={`${color}_fill`} render={<button />}>
65
+ #python
66
+ </Pill>
67
+
68
+ <Pill interactive={`${color}_fill-static`}>#game</Pill>
69
+
70
+ <Pill leftIcon="time-line" interactive={`${color}_mute-static`}>
71
+ Beginner friendly
72
+ </Pill>
73
+
74
+ <Pill rightIcon="star-line" interactive={`${color}_outline-static`}>
75
+ Beginner friendly
76
+ </Pill>
77
+
78
+ <Text size="sm">{color}</Text>
79
+ </View>
80
+ ))}
81
+ </View>
82
+ ),
83
+ };
@@ -0,0 +1,94 @@
1
+ import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
2
+ import clsx from "clsx";
3
+ import type { ReactElement, ReactNode } from "react";
4
+
5
+ import { Heading } from "../heading";
6
+ import { Surface, type Background } from "../surface";
7
+ import { Text } from "../text";
8
+ import { TooltipArrow } from "../tooltip/tooltip-arrow";
9
+ import { View } from "../view";
10
+
11
+ import transitionStyles from "../../styles/transitions.module.css";
12
+ import tooltipStyles from "../tooltip/tooltip.module.css";
13
+ import styles from "./popover.module.css";
14
+
15
+ type PopoverRootProps = Omit<PopoverPrimitive.Root.Props, "children">;
16
+ type PopoverPositionerProps = Pick<
17
+ PopoverPrimitive.Positioner.Props,
18
+ "align" | "side"
19
+ >;
20
+
21
+ export type PopoverProps = PopoverRootProps &
22
+ PopoverPositionerProps & {
23
+ /** Optional title description. */
24
+ title?: string;
25
+
26
+ /** Optional popover description. */
27
+ description?: string;
28
+
29
+ /** Specify trigger to open popover. */
30
+ trigger?: ReactElement;
31
+
32
+ /** Surface background popover should use. Defaults to "root". */
33
+ background?: Background;
34
+
35
+ /** Apply className to PopoverPrimitive.Popup */
36
+ className?: string;
37
+
38
+ /** Popover content */
39
+ children?: ReactNode;
40
+ };
41
+
42
+ export function Popover({
43
+ align,
44
+ side,
45
+ title,
46
+ description,
47
+ background = "root",
48
+ trigger,
49
+ className,
50
+ children,
51
+ ...props
52
+ }: PopoverProps) {
53
+ return (
54
+ <PopoverPrimitive.Root {...props}>
55
+ {trigger ? <PopoverPrimitive.Trigger render={trigger} /> : null}
56
+ <PopoverPrimitive.Portal>
57
+ <PopoverPrimitive.Positioner align={align} side={side} sideOffset={8}>
58
+ <PopoverPrimitive.Popup
59
+ render={<Surface background={background} />}
60
+ className={clsx(
61
+ tooltipStyles["tooltip"],
62
+ styles["popover"],
63
+ transitionStyles["transition_scale"],
64
+ className,
65
+ )}
66
+ >
67
+ <PopoverPrimitive.Arrow className={tooltipStyles["tooltip__arrow"]}>
68
+ <TooltipArrow />
69
+ </PopoverPrimitive.Arrow>
70
+ {title || description ? (
71
+ <View className={styles["popover__header"]}>
72
+ {title ? (
73
+ <PopoverPrimitive.Title
74
+ render={<Heading level={2} size="2xl" />}
75
+ >
76
+ {title}
77
+ </PopoverPrimitive.Title>
78
+ ) : null}
79
+ {description ? (
80
+ <PopoverPrimitive.Description
81
+ render={<Text color="dimmer" />}
82
+ >
83
+ {description}
84
+ </PopoverPrimitive.Description>
85
+ ) : null}
86
+ </View>
87
+ ) : null}
88
+ {children}
89
+ </PopoverPrimitive.Popup>
90
+ </PopoverPrimitive.Positioner>
91
+ </PopoverPrimitive.Portal>
92
+ </PopoverPrimitive.Root>
93
+ );
94
+ }
@@ -0,0 +1,8 @@
1
+ .popover {
2
+ gap: var(--space-8);
3
+ padding: var(--space-8);
4
+ }
5
+
6
+ .popover__header {
7
+ gap: var(--space-4);
8
+ }
@@ -0,0 +1,53 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { surfaceBackgrounds } from "~/stories/data";
4
+ import { disable } from "~/stories/utils";
5
+
6
+ import { Popover } from ".";
7
+ import { IconButton } from "../icon-button";
8
+
9
+ const meta = {
10
+ title: "Popover",
11
+ component: Popover,
12
+ parameters: { layout: "centered" },
13
+ argTypes: {
14
+ align: {
15
+ control: "inline-radio",
16
+ options: ["start", "center", "end"],
17
+ },
18
+ side: {
19
+ control: "select",
20
+ options: ["top", "bottom", "left", "right"],
21
+ },
22
+ background: {
23
+ control: "select",
24
+ options: surfaceBackgrounds,
25
+ },
26
+ },
27
+ } satisfies Meta<typeof Popover>;
28
+
29
+ export default meta;
30
+
31
+ type Story = StoryObj<typeof meta>;
32
+
33
+ export const Default: Story = {
34
+ argTypes: {
35
+ ...disable(["trigger", "children", "className"]),
36
+ },
37
+ args: {
38
+ title: "Notifications",
39
+ description: "You're all caught up!",
40
+ align: "center",
41
+ side: "bottom",
42
+ },
43
+ render: (args) => (
44
+ <Popover
45
+ trigger={
46
+ <IconButton icon="notification-line" alt="Notifications" interactive />
47
+ }
48
+ {...args}
49
+ />
50
+ ),
51
+ };
52
+
53
+ // TODO notifications story (unfortunately we need button group (tabs) b4 we do this)
@@ -0,0 +1,68 @@
1
+ import { PreviewCard as PreviewCardPrimitive } from "@base-ui/react/preview-card";
2
+ import clsx from "clsx";
3
+ import type { ReactElement, ReactNode } from "react";
4
+
5
+ import { Surface, type Background } from "../surface";
6
+ import { TooltipArrow } from "../tooltip/tooltip-arrow";
7
+
8
+ import transitionStyles from "../../styles/transitions.module.css";
9
+ import tooltipStyles from "../tooltip/tooltip.module.css";
10
+ import styles from "./preview-card.module.css";
11
+
12
+ export type PreviewCardProps = PreviewCardPrimitive.Root.Props &
13
+ Pick<PreviewCardPrimitive.Positioner.Props, "align" | "side"> & {
14
+ /**
15
+ * Specify trigger to open preview.
16
+ * You should use an Anchor component.
17
+ */
18
+ trigger?: ReactElement;
19
+
20
+ /** Surface background preview card should use. Defaults to "root". */
21
+ background?: Background;
22
+
23
+ /** Apply styles to preview card content. */
24
+ className?: string;
25
+
26
+ /** Preview card content */
27
+ children?: ReactNode;
28
+ };
29
+
30
+ export function PreviewCard({
31
+ align,
32
+ side,
33
+ background,
34
+ trigger,
35
+ children,
36
+ className,
37
+ ...props
38
+ }: PreviewCardProps) {
39
+ return (
40
+ <PreviewCardPrimitive.Root {...props}>
41
+ {trigger ? <PreviewCardPrimitive.Trigger render={trigger} /> : null}
42
+ <PreviewCardPrimitive.Portal>
43
+ <PreviewCardPrimitive.Positioner
44
+ align={align}
45
+ side={side}
46
+ sideOffset={8}
47
+ >
48
+ <PreviewCardPrimitive.Popup
49
+ render={<Surface background={background} />}
50
+ className={clsx(
51
+ tooltipStyles["tooltip"],
52
+ styles["preview-card"],
53
+ transitionStyles["transition_scale"],
54
+ className,
55
+ )}
56
+ >
57
+ <PreviewCardPrimitive.Arrow
58
+ className={tooltipStyles["tooltip__arrow"]}
59
+ >
60
+ <TooltipArrow />
61
+ </PreviewCardPrimitive.Arrow>
62
+ {children}
63
+ </PreviewCardPrimitive.Popup>
64
+ </PreviewCardPrimitive.Positioner>
65
+ </PreviewCardPrimitive.Portal>
66
+ </PreviewCardPrimitive.Root>
67
+ );
68
+ }
@@ -0,0 +1,5 @@
1
+ .preview-card {
2
+ gap: var(--space-8);
3
+ padding: var(--space-8);
4
+ max-width: var(--space-256);
5
+ }
@@ -0,0 +1,58 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { surfaceBackgrounds } from "~/stories/data";
4
+ import { disable } from "~/stories/utils";
5
+ import { tokens } from "~/styles/tokens";
6
+
7
+ import { PreviewCard } from ".";
8
+ import { Anchor } from "../anchor";
9
+ import { Text } from "../text";
10
+
11
+ const meta = {
12
+ title: "Preview Card",
13
+ component: PreviewCard,
14
+ parameters: { layout: "centered" },
15
+ argTypes: {
16
+ ...disable(["trigger", "className", "children"]),
17
+ background: {
18
+ control: "select",
19
+ options: surfaceBackgrounds,
20
+ },
21
+ },
22
+ } satisfies Meta<typeof PreviewCard>;
23
+
24
+ export default meta;
25
+
26
+ type Story = StoryObj<typeof meta>;
27
+
28
+ export const Default: Story = {
29
+ args: {
30
+ background: "highest",
31
+ },
32
+ render: (args) => (
33
+ <Text>
34
+ The principles of good{" "}
35
+ <PreviewCard {...args} trigger={<Anchor>typography</Anchor>}>
36
+ <img
37
+ width="448"
38
+ height="300"
39
+ src="https://images.unsplash.com/photo-1619615391095-dfa29e1672ef?q=80&w=448&h=300"
40
+ alt="Station Hofplein signage in Rotterdam, Netherlands"
41
+ style={{
42
+ borderRadius: tokens.borderRadiusDefault,
43
+ width: "100%",
44
+ height: "auto",
45
+ }}
46
+ />
47
+ <Text style={{ flex: "1 1 auto" }} multiline>
48
+ <strong style={{ fontWeight: tokens.fontWeightMedium }}>
49
+ Typography
50
+ </strong>{" "}
51
+ is the art and science of arranging type to make written language
52
+ clear, visually appealing, and effective in communication.
53
+ </Text>
54
+ </PreviewCard>{" "}
55
+ remain into the digital age.
56
+ </Text>
57
+ ),
58
+ };