@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,104 @@
1
+ import { Toast as ToastPrimitive } from "@base-ui/react/toast";
2
+ import clsx from "clsx";
3
+
4
+ import { Button } from "../button";
5
+ import { Heading } from "../heading";
6
+ import { IconButton } from "../icon-button";
7
+ import { Surface } from "../surface";
8
+ import { Text } from "../text";
9
+ import { View, type Color } from "../view";
10
+ import { useToastManager } from "./use-toast-manager";
11
+
12
+ import styles from "./toast.module.css";
13
+
14
+ export const ToastProvider = ToastPrimitive.Provider;
15
+
16
+ const toastTypeColor: Record<string, Color> = {
17
+ info: "blue",
18
+ error: "red",
19
+ warning: "yellow",
20
+ success: "green",
21
+ };
22
+
23
+ export function ToastViewport() {
24
+ return (
25
+ <ToastPrimitive.Portal>
26
+ <ToastPrimitive.Viewport className={styles["toast__viewport"]}>
27
+ <ToastList />
28
+ </ToastPrimitive.Viewport>
29
+ </ToastPrimitive.Portal>
30
+ );
31
+ }
32
+
33
+ // TODO we're not using the toast primitives here...
34
+ // TODO specify toast background in viewport?
35
+
36
+ function ToastList() {
37
+ const toastManager = useToastManager();
38
+ return toastManager.toasts.map((toast) => {
39
+ const color =
40
+ toast.type && toast.type in toastTypeColor
41
+ ? toastTypeColor[toast.type]
42
+ : undefined;
43
+
44
+ return (
45
+ <ToastPrimitive.Root
46
+ key={toast.id}
47
+ toast={toast}
48
+ className={clsx(
49
+ styles["toast"],
50
+ color
51
+ ? styles[`toast_variant_colorway`]
52
+ : styles["toast_variant_neutral"],
53
+ )}
54
+ render={<Surface background="default" color={color} />}
55
+ >
56
+ <View
57
+ render={<ToastPrimitive.Content />}
58
+ className={styles["toast__content"]}
59
+ >
60
+ <View className={styles["toast__row"]}>
61
+ <View className={styles["toast__header"]}>
62
+ {toast.title ? (
63
+ <Heading
64
+ level={2}
65
+ size="lg"
66
+ color="inherit"
67
+ className={styles["toast__title"]}
68
+ >
69
+ {toast.title}
70
+ </Heading>
71
+ ) : null}
72
+
73
+ <ToastPrimitive.Description
74
+ render={<Text multiline color={color ? "inherit" : "dimmer"} />}
75
+ className={styles["toast__description"]}
76
+ />
77
+ </View>
78
+
79
+ {/* close icon will appear over content UNLESS we disable aboslute positioning */}
80
+ <ToastPrimitive.Close
81
+ render={
82
+ <IconButton
83
+ className={toast.title ? styles["toast__close"] : undefined}
84
+ interactive={color ? `${color}_no-fill` : "no-fill"}
85
+ icon="close-line"
86
+ size="sm"
87
+ alt="Close"
88
+ />
89
+ }
90
+ />
91
+ </View>
92
+ {toast.action ? (
93
+ <Button
94
+ {...toast.action}
95
+ interactive={color ? `${color}_fill` : true}
96
+ >
97
+ {toast.action.children}
98
+ </Button>
99
+ ) : null}
100
+ </View>
101
+ </ToastPrimitive.Root>
102
+ );
103
+ });
104
+ }
@@ -0,0 +1,63 @@
1
+ import {
2
+ Toast,
3
+ type ToastManagerAddOptions,
4
+ type UseToastManagerReturnValue,
5
+ } from "@base-ui/react/toast";
6
+
7
+ import type { Override } from "~/-types";
8
+
9
+ import type { ButtonProps } from "../button";
10
+
11
+ /**
12
+ * A whole lot of useless effort
13
+ * Basically I want "actionProps" to be "action"
14
+ * And I want "action" to use ButtonProps with text
15
+ */
16
+
17
+ type ActionProps = {
18
+ type?: "info" | "error" | "warning" | "success" | (string & {});
19
+ action?: ButtonProps;
20
+ };
21
+
22
+ type FrameworkAddOptions<Data extends object> = Omit<
23
+ ToastManagerAddOptions<Data>,
24
+ "actionProps"
25
+ > &
26
+ ActionProps;
27
+
28
+ type FrameworkUpdateOptions<Data extends object> = Partial<
29
+ FrameworkAddOptions<Data>
30
+ >;
31
+
32
+ type FrameworkPromiseOptions<Value, Data extends object> = {
33
+ loading: string | FrameworkUpdateOptions<Data>;
34
+ success:
35
+ | string
36
+ | FrameworkUpdateOptions<Data>
37
+ | ((result: Value) => string | FrameworkUpdateOptions<Data>);
38
+ error:
39
+ | string
40
+ | FrameworkUpdateOptions<Data>
41
+ | ((error: unknown) => string | FrameworkUpdateOptions<Data>);
42
+ };
43
+
44
+ type FrameworkToastManager = Override<
45
+ UseToastManagerReturnValue,
46
+ {
47
+ toasts: Array<UseToastManagerReturnValue["toasts"][number] & ActionProps>;
48
+ add<Data extends object>(options: FrameworkAddOptions<Data>): string;
49
+ update<Data extends object>(
50
+ id: string,
51
+ updates: FrameworkUpdateOptions<Data>,
52
+ ): void;
53
+ promise<Value, Data extends object>(
54
+ promise: Promise<Value>,
55
+ options: FrameworkPromiseOptions<Value, Data>,
56
+ ): Promise<Value>;
57
+ }
58
+ >;
59
+
60
+ export function useToastManager() {
61
+ const toastManager = Toast.useToastManager();
62
+ return toastManager as unknown as FrameworkToastManager;
63
+ }
@@ -0,0 +1,61 @@
1
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
2
+ import clsx from "clsx";
3
+ import type { ReactElement, ReactNode } from "react";
4
+
5
+ import { textify } from "~/-utils";
6
+
7
+ import { Surface } from "../surface";
8
+ import { TooltipArrow } from "./tooltip-arrow";
9
+
10
+ import transitionStyles from "../../styles/transitions.module.css";
11
+ import styles from "./tooltip.module.css";
12
+
13
+ export const TooltipProvider = TooltipPrimitive.Provider;
14
+
15
+ type TooltipRootProps = Omit<TooltipPrimitive.Root.Props, "children">;
16
+ type TooltipPositionerProps = Pick<
17
+ TooltipPrimitive.Positioner.Props,
18
+ "align" | "side"
19
+ >;
20
+ export type TooltipProps = TooltipRootProps &
21
+ TooltipPositionerProps & {
22
+ children?: ReactNode;
23
+
24
+ /** Specify trigger to open tooltip. */
25
+ trigger?: ReactElement;
26
+
27
+ /** Apply className to tooltip content */
28
+ className?: string;
29
+ };
30
+
31
+ export function Tooltip({
32
+ children,
33
+ trigger,
34
+ align,
35
+ side,
36
+ className,
37
+ ...props
38
+ }: TooltipProps) {
39
+ return (
40
+ <TooltipPrimitive.Root {...props}>
41
+ {trigger ? <TooltipPrimitive.Trigger render={trigger} /> : null}
42
+ <TooltipPrimitive.Portal>
43
+ <TooltipPrimitive.Positioner align={align} side={side} sideOffset={8}>
44
+ <TooltipPrimitive.Popup
45
+ render={<Surface background="highest" />}
46
+ className={clsx(
47
+ styles["tooltip"],
48
+ transitionStyles["transition_scale"],
49
+ className,
50
+ )}
51
+ >
52
+ <TooltipPrimitive.Arrow className={styles["tooltip__arrow"]}>
53
+ <TooltipArrow />
54
+ </TooltipPrimitive.Arrow>
55
+ {textify(children)}
56
+ </TooltipPrimitive.Popup>
57
+ </TooltipPrimitive.Positioner>
58
+ </TooltipPrimitive.Portal>
59
+ </TooltipPrimitive.Root>
60
+ );
61
+ }
@@ -0,0 +1,17 @@
1
+ import clsx from "clsx";
2
+
3
+ import styles from "./tooltip.module.css";
4
+
5
+ export function TooltipArrow(props: { className?: string }) {
6
+ return (
7
+ <svg
8
+ width="10"
9
+ height="5"
10
+ viewBox="0 0 30 10"
11
+ preserveAspectRatio="none"
12
+ className={clsx(styles["tooltip__arrow-icon"], props.className)}
13
+ >
14
+ <polygon points="0,0 30,0 15,10"></polygon>
15
+ </svg>
16
+ );
17
+ }
@@ -0,0 +1,44 @@
1
+ .tooltip {
2
+ z-index: 50;
3
+ box-shadow: var(--shadow-1);
4
+ border: 1px solid var(--outline-dimmer);
5
+ border-radius: var(--border-radius-default);
6
+ background-color: var(--surface-background);
7
+ padding: var(--space-4) var(--space-8);
8
+ }
9
+
10
+ .tooltip__arrow {
11
+ &[data-instant] {
12
+ transition: none;
13
+ }
14
+
15
+ &[data-side="top"] {
16
+ bottom: -4px;
17
+ }
18
+
19
+ &[data-side="bottom"] {
20
+ top: -4px;
21
+ rotate: 180deg;
22
+ }
23
+
24
+ &[data-side="left"] {
25
+ right: -7px;
26
+ rotate: -90deg;
27
+ }
28
+
29
+ &[data-side="right"] {
30
+ left: -7px;
31
+ rotate: 90deg;
32
+ }
33
+ }
34
+
35
+ .tooltip__arrow-icon {
36
+ display: block;
37
+ position: relative;
38
+ fill: var(--surface-background) !important;
39
+ stroke: var(--outline-dimmer);
40
+ stroke-dasharray: 0 30 22 0 22;
41
+ stroke-linejoin: round;
42
+ stroke-width: 2;
43
+ border-top: 1px solid var(--surface-background);
44
+ }
@@ -0,0 +1,76 @@
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 { Tooltip, TooltipProvider } from ".";
7
+ import { IconButton } from "../icon-button";
8
+ import { View } from "../view";
9
+
10
+ const meta = {
11
+ title: "Tooltip",
12
+ component: Tooltip,
13
+ parameters: { layout: "centered" },
14
+ argTypes: {
15
+ children: {
16
+ control: "text",
17
+ },
18
+ align: {
19
+ control: "inline-radio",
20
+ options: ["start", "center", "end"],
21
+ },
22
+ open: {
23
+ control: "boolean",
24
+ },
25
+ ...disable(["className"]),
26
+ },
27
+ } satisfies Meta<typeof Tooltip>;
28
+
29
+ export default meta;
30
+
31
+ type Story = StoryObj<typeof meta>;
32
+
33
+ export const Default: Story = {
34
+ argTypes: {
35
+ trigger: {
36
+ table: {
37
+ disable: true,
38
+ },
39
+ },
40
+ },
41
+ args: {
42
+ children: "Bold",
43
+ align: "center",
44
+ open: true,
45
+ },
46
+ render: (args) => (
47
+ <Tooltip
48
+ trigger={<IconButton icon="bold" alt="Bold" interactive />}
49
+ {...args}
50
+ />
51
+ ),
52
+ };
53
+
54
+ export const Provider: Story = {
55
+ argTypes: {
56
+ ...disable(["message", "trigger", "open", "children"]),
57
+ },
58
+ render: () => (
59
+ <TooltipProvider>
60
+ <View style={{ flexDirection: "row", gap: tokens.space4 }}>
61
+ <Tooltip
62
+ children="Bold"
63
+ trigger={<IconButton icon="bold" alt="Bold" interactive />}
64
+ />
65
+ <Tooltip
66
+ children="Italic"
67
+ trigger={<IconButton icon="italic" alt="Italic" interactive />}
68
+ />
69
+ <Tooltip
70
+ children="Underline"
71
+ trigger={<IconButton icon="underline" alt="Underline" interactive />}
72
+ />
73
+ </View>
74
+ </TooltipProvider>
75
+ ),
76
+ };
@@ -0,0 +1,137 @@
1
+ import { mergeProps, useRender } from "@base-ui/react";
2
+ import clsx from "clsx";
3
+ import type { ElementType } from "react";
4
+
5
+ import colorwayStyles from "./view_colorway.module.css";
6
+ import interactiveStyles from "./view_interactive.module.css";
7
+ import loadingStyles from "./view_loading.module.css";
8
+ import styles from "./view.module.css";
9
+
10
+ export type InteractiveStyle =
11
+ | "fill"
12
+ | "no-fill"
13
+ | "outline"
14
+ | "fill-outline"
15
+ | "list-item";
16
+
17
+ export type Color =
18
+ | "primary"
19
+ | "positive"
20
+ | "negative"
21
+ | "warning"
22
+ | "red"
23
+ | "orange"
24
+ | "yellow"
25
+ | "green"
26
+ | "teal"
27
+ | "blue"
28
+ | "blurple"
29
+ | "purple"
30
+ | "magenta"
31
+ | "pink"
32
+ | "grey";
33
+
34
+ export type ColorStyle =
35
+ | "outline"
36
+ | "outline-static"
37
+ | "mute-static"
38
+ | "fill"
39
+ | "no-fill"
40
+ | "fill-static"
41
+ | "fill-outline";
42
+
43
+ export type ColorVariant = `${Color}_${ColorStyle}`;
44
+
45
+ export type LoadingVariant = "background" | "foreground";
46
+
47
+ export type ViewProps<T extends ElementType = "div"> = Omit<
48
+ useRender.ComponentProps<T>,
49
+ "color"
50
+ > & {
51
+ /**
52
+ * Centralized property to define either an interactive variant or colorway.
53
+ * We know which is which because colorways have an underscore (Color_ColorVariant, like primary_fill).
54
+ *
55
+ * Static variants will not apply transitions or cursor effects! Do not use them for interactive elements.
56
+ *
57
+ * Setting `interactive` to true will use "fill".
58
+ */
59
+ interactive?: boolean | InteractiveStyle | ColorVariant;
60
+
61
+ /**
62
+ * Add CSS color variables but do nothing else.
63
+ * Useful for using View as a sort of "Color Provider" so children can use colors and create custom variants.
64
+ */
65
+ color?: Color;
66
+
67
+ /**
68
+ * Make this element look like it's loading.
69
+ * Setting `loading` to true will use "foreground".
70
+ * It'll use sensible defaults based on the colorway or interactive prop.
71
+ */
72
+ loading?: boolean | LoadingVariant;
73
+ };
74
+
75
+ export const View = <T extends ElementType = "div">({
76
+ interactive,
77
+ loading,
78
+ color,
79
+ render,
80
+ ...props
81
+ }: ViewProps<T>) => {
82
+ const normalized = normalize({ interactive, loading });
83
+ const element = useRender({
84
+ defaultTagName: "div",
85
+ render,
86
+ props: mergeProps(
87
+ {
88
+ className: clsx(
89
+ styles["view"],
90
+ color && colorwayStyles[`view_colorway_color-${color}`],
91
+ normalized.interactive && [
92
+ interactiveStyles["view_interactive"],
93
+ interactiveStyles[`view_interactive_${normalized.interactive}`],
94
+ ],
95
+ normalized.colorway && [
96
+ normalized.colorway[1].endsWith("static")
97
+ ? colorwayStyles["view_colorway_static"]
98
+ : colorwayStyles["view_colorway"],
99
+ colorwayStyles[`view_colorway_${normalized.colorway[1]}`],
100
+ colorwayStyles[`view_colorway_color-${normalized.colorway[0]}`],
101
+ ],
102
+ normalized.loading && [
103
+ loadingStyles["view_loading"],
104
+ loadingStyles[`view_loading_${normalized.loading}`],
105
+ ],
106
+ ),
107
+ },
108
+ props,
109
+ ),
110
+ });
111
+ return element;
112
+ };
113
+
114
+ const normalize = (props: Pick<ViewProps, "interactive" | "loading">) => {
115
+ let interactive: InteractiveStyle | null = null;
116
+ let colorway: [Color, ColorStyle] | null = null;
117
+
118
+ if (props.interactive && typeof props.interactive === "boolean") {
119
+ interactive = "fill";
120
+ } else if (typeof props.interactive === "string") {
121
+ if (
122
+ typeof props.interactive === "string" &&
123
+ props.interactive.includes("_")
124
+ ) {
125
+ colorway = props.interactive.split("_") as [Color, ColorStyle];
126
+ } else {
127
+ interactive = props.interactive as InteractiveStyle;
128
+ }
129
+ }
130
+
131
+ let loading: LoadingVariant | null = null;
132
+ if (props.loading) {
133
+ loading = typeof props.loading === "boolean" ? "foreground" : props.loading;
134
+ }
135
+
136
+ return { interactive, colorway, loading };
137
+ };
@@ -0,0 +1,11 @@
1
+ .view {
2
+ display: flex;
3
+ position: relative;
4
+ flex-shrink: 0;
5
+ flex-basis: auto;
6
+ flex-direction: column;
7
+ align-items: stretch;
8
+ box-sizing: border-box;
9
+ outline: none;
10
+ border-style: solid;
11
+ }
@@ -0,0 +1,131 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import {
4
+ colorway,
5
+ interactiveStyles,
6
+ surfaceBackgrounds,
7
+ } from "~/stories/data";
8
+
9
+ import { View, type ViewProps } from ".";
10
+ import { tokens } from "../../styles/tokens";
11
+ import { Surface } from "../surface";
12
+ import { Text } from "../text";
13
+
14
+ const meta = {
15
+ title: "View",
16
+ component: View,
17
+ parameters: { layout: "centered" },
18
+ argTypes: {
19
+ interactive: {
20
+ control: "select",
21
+ options: [...interactiveStyles, ...colorway],
22
+ },
23
+ loading: {
24
+ control: "boolean",
25
+ },
26
+ "aria-disabled": {
27
+ control: "boolean",
28
+ },
29
+ },
30
+ } satisfies Meta<ViewProps & { "aria-disabled"?: boolean }>;
31
+
32
+ export default meta;
33
+
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ export const Interactive: Story = {
37
+ args: {
38
+ interactive: "fill",
39
+ loading: false,
40
+ },
41
+ render: (args) => (
42
+ <View
43
+ {...args}
44
+ style={{
45
+ paddingInline: tokens.space16,
46
+ height: tokens.space32,
47
+ justifyContent: "center",
48
+ }}
49
+ >
50
+ Hello world
51
+ </View>
52
+ ),
53
+ };
54
+
55
+ export const Colorway: Story = {
56
+ args: {
57
+ interactive: "primary_fill",
58
+ loading: false,
59
+ },
60
+ argTypes: {
61
+ interactive: {
62
+ table: {
63
+ disable: true,
64
+ },
65
+ },
66
+ },
67
+ render: (args) => (
68
+ <View
69
+ {...args}
70
+ style={{
71
+ paddingInline: tokens.space16,
72
+ height: tokens.space32,
73
+ justifyContent: "center",
74
+ }}
75
+ >
76
+ Hello world
77
+ </View>
78
+ ),
79
+ };
80
+
81
+ export const AllInteractiveStyles: Story = {
82
+ argTypes: {
83
+ interactive: {
84
+ table: {
85
+ disable: true,
86
+ },
87
+ },
88
+ },
89
+ args: {
90
+ loading: false,
91
+ },
92
+ render: (args) => (
93
+ <View
94
+ style={{
95
+ flexDirection: "row",
96
+ flexWrap: "wrap",
97
+ gap: tokens.space8,
98
+ }}
99
+ >
100
+ {surfaceBackgrounds.map((background) => (
101
+ <Surface
102
+ style={{
103
+ border: `1px solid ${tokens.outlineDimmest}`,
104
+ padding: tokens.space16,
105
+ width: tokens.space256,
106
+ gap: tokens.space6,
107
+ }}
108
+ key={background}
109
+ background={background}
110
+ >
111
+ <p style={{ fontWeight: 500 }}>{background}</p>
112
+
113
+ {interactiveStyles.map((variant) => (
114
+ <View
115
+ key={variant}
116
+ interactive={variant}
117
+ style={{
118
+ paddingInline: tokens.space16,
119
+ height: tokens.space32,
120
+ justifyContent: "center",
121
+ }}
122
+ {...args}
123
+ >
124
+ <Text>{variant}</Text>
125
+ </View>
126
+ ))}
127
+ </Surface>
128
+ ))}
129
+ </View>
130
+ ),
131
+ };