@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,36 @@
1
+ .icon {
2
+ --icon-size: var(--space-16);
3
+
4
+ vertical-align: middle;
5
+ width: var(--icon-size);
6
+ min-width: var(--icon-size);
7
+ height: var(--icon-size);
8
+ min-height: var(--icon-size);
9
+ font-size: var(--icon-size);
10
+ }
11
+
12
+ /* Maximize performance by declaring styles instead of inline styles */
13
+ .icon_size_sm {
14
+ /* TODO revisit this - we kind of depend on icons to be at least 16 px, so maybe expose an "xs" size? idk */
15
+ --icon-size: var(--space-16);
16
+ }
17
+
18
+ .icon_size_md {
19
+ --icon-size: var(--space-16);
20
+ }
21
+
22
+ .icon_size_lg {
23
+ --icon-size: var(--space-20);
24
+ }
25
+
26
+ .icon_size_xl {
27
+ --icon-size: var(--space-24);
28
+ }
29
+
30
+ .icon_size_2xl {
31
+ --icon-size: var(--space-32);
32
+ }
33
+
34
+ .icon_size_3xl {
35
+ --icon-size: var(--space-48);
36
+ }
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { colorTokens, tokens } from "~/styles/tokens";
4
+
5
+ import { Icon } from ".";
6
+
7
+ const iconSizeOptions = ["sm", "md", "lg", "xl", "2xl", "3xl"] as const;
8
+
9
+ const meta = {
10
+ title: "Icon",
11
+ component: Icon,
12
+ parameters: { layout: "centered" },
13
+ argTypes: {
14
+ color: {
15
+ control: "select",
16
+ options: Object.values(colorTokens),
17
+ },
18
+ size: {
19
+ control: "select",
20
+ options: iconSizeOptions,
21
+ },
22
+ },
23
+ } satisfies Meta<typeof Icon>;
24
+
25
+ export default meta;
26
+
27
+ type Story = StoryObj<typeof meta>;
28
+
29
+ export const Default: Story = {
30
+ args: {
31
+ name: "heart-fill",
32
+ color: tokens.primaryDefault,
33
+ size: "md",
34
+ },
35
+ render: (props) => (
36
+ <div>
37
+ <Icon {...props} />
38
+ </div>
39
+ ),
40
+ };
@@ -0,0 +1,60 @@
1
+ import clsx from "clsx";
2
+ import { type ComponentProps } from "react";
3
+ import svgSprite from "remixicon/fonts/remixicon.symbol.svg";
4
+
5
+ import { type ColorToken, type Size } from "~/styles/tokens";
6
+
7
+ import { type IconName } from "./icon-names";
8
+
9
+ import styles from "./icon.module.css";
10
+
11
+ export interface IconProps extends Omit<ComponentProps<"svg">, "name"> {
12
+ /**
13
+ * Overrides the icon size provider.
14
+ * Defaults to "md".
15
+ */
16
+ size?: Size;
17
+
18
+ /** Icon name */
19
+ name: IconName;
20
+
21
+ /**
22
+ * Icon fill color
23
+ * You probably should provide a color token, but we allow any color
24
+ */
25
+ color?: ColorToken | (string & {});
26
+
27
+ /**
28
+ * Use like `img alt` attribute.
29
+ * Leave empty if icon is purely decorative.
30
+ */
31
+ alt?: string;
32
+ }
33
+
34
+ export function Icon({
35
+ size = "md",
36
+ name,
37
+ color,
38
+ alt,
39
+ className,
40
+ ...props
41
+ }: IconProps) {
42
+ const href = `${svgSprite}#ri-${name}`;
43
+
44
+ return (
45
+ <svg
46
+ preserveAspectRatio="xMidYMin"
47
+ viewBox="0 0 24 24"
48
+ aria-hidden={!alt}
49
+ focusable={false}
50
+ xmlns="http://www.w3.org/2000/svg"
51
+ fill={color || "currentColor"}
52
+ className={clsx(styles["icon"], styles[`icon_size_${size}`], className)}
53
+ {...props}
54
+ >
55
+ {/* https://css-tricks.com/accessible-svg-icons/ */}
56
+ {alt ? <title>{alt}</title> : null}
57
+ <use xlinkHref={href} href={href} />
58
+ </svg>
59
+ );
60
+ }
@@ -0,0 +1,33 @@
1
+ .icon-button {
2
+ display: flex;
3
+ justify-content: center;
4
+ align-items: center;
5
+ border-radius: var(--border-radius-default);
6
+ aspect-ratio: 1 / 1;
7
+ width: var(--icon-button-size);
8
+ height: var(--icon-button-size);
9
+ }
10
+
11
+ .icon-button_size_sm {
12
+ --icon-button-size: var(--space-24);
13
+ }
14
+
15
+ .icon-button_size_md {
16
+ --icon-button-size: var(--space-32);
17
+ }
18
+
19
+ .icon-button_size_lg {
20
+ --icon-button-size: var(--space-40);
21
+ }
22
+
23
+ .icon-button_size_xl {
24
+ --icon-button-size: var(--space-48);
25
+ }
26
+
27
+ .icon-button_size_2xl {
28
+ --icon-button-size: var(--space-56);
29
+ }
30
+
31
+ .icon-button_size_3xl {
32
+ --icon-button-size: var(--space-64);
33
+ }
@@ -0,0 +1,59 @@
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
+
6
+ import { IconButton } from ".";
7
+
8
+ const meta = {
9
+ title: "IconButton",
10
+ component: IconButton,
11
+ parameters: { layout: "centered" },
12
+ argTypes: {
13
+ size: {
14
+ control: "select",
15
+ options: sizes,
16
+ },
17
+ interactive: {
18
+ control: "boolean",
19
+ },
20
+ loading: {
21
+ control: "boolean",
22
+ },
23
+ },
24
+ } satisfies Meta<typeof IconButton>;
25
+
26
+ export default meta;
27
+
28
+ type Story = StoryObj<typeof meta>;
29
+
30
+ export const Default: Story = {
31
+ args: {
32
+ size: "md",
33
+ icon: "add-line",
34
+ alt: "Create a new project.",
35
+ interactive: true,
36
+ loading: false,
37
+ },
38
+ render: (args) => <IconButton {...args} />,
39
+ };
40
+
41
+ export const AllSizes: Story = {
42
+ args: {
43
+ icon: "add-line",
44
+ alt: "Create a new project.",
45
+ },
46
+ argTypes: {
47
+ size: {
48
+ control: false,
49
+ },
50
+ },
51
+ render: (props) => (
52
+ <AllVariants
53
+ variantName="size"
54
+ variants={sizes}
55
+ element={<IconButton interactive {...props} />}
56
+ style={{ flexDirection: "row" }}
57
+ />
58
+ ),
59
+ };
@@ -0,0 +1,48 @@
1
+ import { Button as ButtonPrimitive } from "@base-ui/react/button";
2
+ import clsx from "clsx";
3
+
4
+ import { type Size } from "~/styles/tokens";
5
+
6
+ import { Icon } from "../icon";
7
+ import type { IconName } from "../icon/icon-names";
8
+ import { View, type ViewProps } from "../view";
9
+
10
+ import styles from "./icon-button.module.css";
11
+
12
+ export type IconButtonProps = ViewProps<"button"> & {
13
+ /** Icon to display. */
14
+ icon: IconName;
15
+
16
+ /** Explain what the button does. Required for accessibility. */
17
+ alt: string;
18
+
19
+ /** What size should the IconButton be. Defaults to "md". */
20
+ size?: Size;
21
+ };
22
+
23
+ export function IconButton({
24
+ icon,
25
+ alt,
26
+ size = "md",
27
+ render,
28
+ ...props
29
+ }: IconButtonProps) {
30
+ return (
31
+ <View<"button">
32
+ interactive={true}
33
+ render={
34
+ <ButtonPrimitive
35
+ className={clsx(
36
+ styles["icon-button"],
37
+ styles[`icon-button_size_${size}`],
38
+ )}
39
+ focusableWhenDisabled={!!props.loading}
40
+ render={render}
41
+ />
42
+ }
43
+ {...props}
44
+ >
45
+ <Icon name={icon} size={size} alt={alt} />
46
+ </View>
47
+ );
48
+ }
@@ -0,0 +1,29 @@
1
+ import type { useRender } from "@base-ui/react";
2
+ import clsx from "clsx";
3
+
4
+ import { Surface } from "../surface";
5
+ import { Text } from "../text";
6
+ import type { Color } from "../view";
7
+
8
+ import styles from "./inline-code.module.css";
9
+
10
+ export interface InlineCodeProps extends useRender.ComponentProps<"span"> {
11
+ color?: Color;
12
+ }
13
+
14
+ export const InlineCode = ({ color, ...props }: InlineCodeProps) => {
15
+ return (
16
+ <Surface
17
+ elevated
18
+ className={clsx(
19
+ styles["inline-code"],
20
+ color && styles["inline-code_colorway"],
21
+ )}
22
+ color={color}
23
+ >
24
+ <Text size="sm" color="inherit">
25
+ <code {...props} />
26
+ </Text>
27
+ </Surface>
28
+ );
29
+ };
@@ -0,0 +1,13 @@
1
+ .inline-code {
2
+ display: inline;
3
+ border: 1px solid var(--surface-interactive-border);
4
+ border-radius: var(--border-radius-6);
5
+ padding: 0 var(--space-4);
6
+ line-height: var(--line-height-default);
7
+ font-family: var(--font-family-code);
8
+ overflow-wrap: anywhere;
9
+ }
10
+
11
+ .inline-code_colorway {
12
+ color: var(--view-colorway-default);
13
+ }
@@ -0,0 +1,31 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { tokens } from "~/styles/tokens";
4
+
5
+ import { InlineCode } from ".";
6
+ import { Surface } from "../surface";
7
+ import { Text } from "../text";
8
+
9
+ const meta = {
10
+ title: "Code/Inline Code",
11
+ component: InlineCode,
12
+ parameters: { layout: "centered" },
13
+ } satisfies Meta<typeof InlineCode>;
14
+
15
+ export default meta;
16
+
17
+ type Story = StoryObj<typeof meta>;
18
+
19
+ export const Default: Story = {
20
+ args: {
21
+ color: undefined,
22
+ },
23
+ render: (args) => (
24
+ <Surface style={{ padding: tokens.space16 }} elevated>
25
+ <Text>
26
+ Import the library with{" "}
27
+ <InlineCode {...args}>import * from "natmfat"</InlineCode>
28
+ </Text>
29
+ </Surface>
30
+ ),
31
+ };
@@ -0,0 +1,22 @@
1
+ import { Input as InputPrimitive } from "@base-ui/react/input";
2
+ import clsx from "clsx";
3
+
4
+ import { View } from "../view";
5
+
6
+ import styles from "./input.module.css";
7
+
8
+ export type InputProps = InputPrimitive.Props;
9
+
10
+ export function Input({ className, ...props }: InputProps) {
11
+ return (
12
+ <View
13
+ interactive="fill-outline"
14
+ render={
15
+ <InputPrimitive
16
+ className={clsx(styles["input"], className)}
17
+ {...props}
18
+ />
19
+ }
20
+ />
21
+ );
22
+ }
@@ -0,0 +1,23 @@
1
+ .input {
2
+ flex: 1 1 0%;
3
+ outline: none;
4
+ padding: var(--space-4) var(--space-8);
5
+ height: var(--space-32);
6
+ min-height: var(--space-32);
7
+ font-size: var(--font-size-default);
8
+ line-height: var(--line-height-input);
9
+ font-family: var(--font-family-default);
10
+
11
+ &::placeholder {
12
+ color: var(--foreground-dimmest);
13
+ }
14
+
15
+ &:not([disabled], [data-disabled]) {
16
+ cursor: text;
17
+ color: var(--foreground-default);
18
+ }
19
+ }
20
+
21
+ .input_autosize {
22
+ resize: none;
23
+ }
@@ -0,0 +1,52 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { AllVariants } from "~/stories/components/all-variants";
4
+ import { surfaceBackgrounds } from "~/stories/data";
5
+ import { tokens } from "~/styles/tokens";
6
+
7
+ import { Input } from ".";
8
+ import { Surface } from "../surface";
9
+
10
+ const meta = {
11
+ title: "Input/Text",
12
+ component: Input,
13
+ parameters: { layout: "centered" },
14
+ } satisfies Meta<typeof Input>;
15
+
16
+ export default meta;
17
+
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ export const Default: Story = {
21
+ args: {
22
+ placeholder: "Hello World",
23
+ disabled: false,
24
+ },
25
+ render: (args) => <Input {...args} style={{ width: tokens.space256 }} />,
26
+ };
27
+
28
+ export const AllInteractiveStyles: Story = {
29
+ args: {
30
+ placeholder: "Hello World",
31
+ disabled: false,
32
+ },
33
+ render: (args) => (
34
+ <AllVariants
35
+ variantName="background"
36
+ variants={surfaceBackgrounds}
37
+ style={{ backgroundColor: "transparent" }}
38
+ element={
39
+ <Surface
40
+ style={{
41
+ padding: tokens.space16,
42
+ gap: tokens.space8,
43
+ borderWidth: "1px",
44
+ borderColor: tokens.outlineDimmest,
45
+ }}
46
+ >
47
+ <Input {...args} />
48
+ </Surface>
49
+ }
50
+ />
51
+ ),
52
+ };
@@ -0,0 +1,55 @@
1
+ import { Meter as MeterPrimitive } from "@base-ui/react/meter";
2
+ import clsx from "clsx";
3
+
4
+ import { Text } from "../text";
5
+ import { View, type Color } from "../view";
6
+
7
+ import styles from "./meter.module.css";
8
+
9
+ export type MeterProps = MeterPrimitive.Root.Props & {
10
+ /**
11
+ * What is this meter even for bro?
12
+ * If one is not provided, we render a standard meter (no label, no value).
13
+ * You can still provide one yourself through children.
14
+ * @example
15
+ * <Meter value={24}>
16
+ * <MeterPrimitive.Label render={<Text />} />
17
+ * <MeterPrimitive.Value render={<Text />} />
18
+ * </Meter>
19
+ */
20
+ label?: string;
21
+
22
+ /** Communicate purpose with color. Defaults to "primary". */
23
+ color?: Color;
24
+ };
25
+
26
+ export function Meter({
27
+ label,
28
+ color = "primary",
29
+ className,
30
+ children,
31
+ ...props
32
+ }: MeterProps) {
33
+ return (
34
+ <View color={color}>
35
+ <MeterPrimitive.Root
36
+ className={clsx(styles["meter"], className)}
37
+ {...props}
38
+ >
39
+ {label ? (
40
+ <>
41
+ <Text render={<MeterPrimitive.Label />}>{label}</Text>
42
+ <Text
43
+ className={styles["meter__value"]}
44
+ render={<MeterPrimitive.Value />}
45
+ />
46
+ </>
47
+ ) : null}
48
+ {children}
49
+ <MeterPrimitive.Track className={styles["meter__track"]}>
50
+ <MeterPrimitive.Indicator className={styles["meter__indicator"]} />
51
+ </MeterPrimitive.Track>
52
+ </MeterPrimitive.Root>
53
+ </View>
54
+ );
55
+ }
@@ -0,0 +1,23 @@
1
+ .meter {
2
+ display: grid;
3
+ grid-template-columns: 1fr 1fr;
4
+ row-gap: var(--space-4);
5
+ }
6
+
7
+ .meter__value {
8
+ grid-column-start: 2;
9
+ margin: 0;
10
+ text-align: right;
11
+ }
12
+
13
+ .meter__track {
14
+ grid-column: 1 / 3;
15
+ background-color: var(--surface-interactive-border);
16
+ height: var(--space-8);
17
+ overflow: hidden;
18
+ }
19
+
20
+ .meter__indicator {
21
+ transition: width var(--transition-duration-chill);
22
+ background-color: var(--view-colorway-default);
23
+ }
@@ -0,0 +1,31 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { colors } from "~/stories/data";
4
+ import { tokens } from "~/styles/tokens";
5
+
6
+ import { Meter } from ".";
7
+
8
+ const meta = {
9
+ title: "Meter",
10
+ component: Meter,
11
+ parameters: { layout: "centered" },
12
+ argTypes: {
13
+ color: {
14
+ control: "select",
15
+ options: colors,
16
+ },
17
+ },
18
+ } satisfies Meta<typeof Meter>;
19
+
20
+ export default meta;
21
+
22
+ type Story = StoryObj<typeof meta>;
23
+
24
+ export const Default: Story = {
25
+ args: {
26
+ color: "orange",
27
+ label: "Storage used",
28
+ value: 24,
29
+ },
30
+ render: (args) => <Meter {...args} style={{ width: tokens.space256 }} />,
31
+ };
@@ -0,0 +1,58 @@
1
+ import type { useRender } from "@base-ui/react";
2
+ import autosize from "autosize";
3
+ import clsx from "clsx";
4
+ import { useEffect, useRef } from "react";
5
+
6
+ import { mergeRefs } from "~/-utils";
7
+
8
+ import { View } from "../view";
9
+
10
+ import styles from "../input/input.module.css";
11
+
12
+ export interface MultilineInputProps extends useRender.ComponentProps<"textarea"> {
13
+ /** Automatically resize textarea. Defaults to false. */
14
+ autosize?: boolean;
15
+ }
16
+
17
+ export function MultilineInput({
18
+ autosize = false,
19
+ className,
20
+ ref,
21
+ ...props
22
+ }: MultilineInputProps) {
23
+ const autosizeRef = useAutosize(autosize);
24
+
25
+ return (
26
+ <View
27
+ interactive="fill-outline"
28
+ render={
29
+ <textarea
30
+ className={clsx(
31
+ styles["input"],
32
+ autosize && styles["input_autosize"],
33
+ className,
34
+ )}
35
+ ref={mergeRefs(ref, autosizeRef)}
36
+ {...props}
37
+ ></textarea>
38
+ }
39
+ />
40
+ );
41
+ }
42
+
43
+ function useAutosize(enabled: boolean) {
44
+ const ref = useRef<HTMLTextAreaElement>(null);
45
+
46
+ useEffect(() => {
47
+ const element = ref.current;
48
+ if (!element || !enabled) {
49
+ return;
50
+ }
51
+ autosize(element);
52
+ return () => {
53
+ autosize.destroy(element);
54
+ };
55
+ }, [enabled]);
56
+
57
+ return ref;
58
+ }
@@ -0,0 +1,26 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { tokens } from "~/styles/tokens";
4
+
5
+ import { MultilineInput } from ".";
6
+
7
+ const meta = {
8
+ title: "Input/Textarea",
9
+ component: MultilineInput,
10
+ parameters: { layout: "centered" },
11
+ } satisfies Meta<typeof MultilineInput>;
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ placeholder: "Hello World",
20
+ disabled: false,
21
+ autosize: true,
22
+ },
23
+ render: (args) => (
24
+ <MultilineInput {...args} style={{ width: tokens.space256 }} />
25
+ ),
26
+ };