@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,67 @@
1
+ import { Radio as RadioPrimitive } from "@base-ui/react/radio";
2
+ import { RadioGroup as RadioGroupPrimitive } from "@base-ui/react/radio-group";
3
+ import clsx from "clsx";
4
+ import { createContext, useContext } from "react";
5
+
6
+ import { Text } from "../text";
7
+ import { View, type Color } from "../view";
8
+
9
+ import radioGroupStyles from "./radio-group.module.css";
10
+ import radioStyles from "./radio.module.css";
11
+
12
+ const RadioContext = createContext<Color>("primary");
13
+
14
+ export function RadioGroup({
15
+ color = "primary",
16
+ className,
17
+ ...props
18
+ }: RadioGroupPrimitive.Props & {
19
+ /** Optional color for child radio elements. Defaults to "primary". */
20
+ color?: Color;
21
+ }) {
22
+ return (
23
+ <RadioContext.Provider value={color}>
24
+ <RadioGroupPrimitive
25
+ className={clsx(radioGroupStyles["radio-group"], className)}
26
+ {...props}
27
+ />
28
+ </RadioContext.Provider>
29
+ );
30
+ }
31
+
32
+ export function Radio({
33
+ color,
34
+ label,
35
+ className,
36
+ ...props
37
+ }: RadioPrimitive.Root.Props & {
38
+ /** Optional label. If one is provided, we wrap radio in a label element. Otherwise, we just return a standard radio. */
39
+ label?: string;
40
+
41
+ /** Optional color. Defaults to the radio group color, often primary. */
42
+ color?: Color;
43
+ }) {
44
+ const colorContext = useContext(RadioContext);
45
+ const normalizedColor: Color = color || colorContext || "primary";
46
+
47
+ const radio = (
48
+ <View
49
+ render={<RadioPrimitive.Root {...props} />}
50
+ interactive="fill-outline"
51
+ color={normalizedColor}
52
+ className={clsx(radioStyles["radio"], className)}
53
+ >
54
+ <RadioPrimitive.Indicator className={radioStyles["radio__indicator"]} />
55
+ </View>
56
+ );
57
+
58
+ if (label) {
59
+ return (
60
+ <label className={radioStyles["radio__label"]}>
61
+ {radio} <Text>{label}</Text>
62
+ </label>
63
+ );
64
+ }
65
+
66
+ return radio;
67
+ }
@@ -0,0 +1,5 @@
1
+ .radio-group {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--space-2);
5
+ }
@@ -0,0 +1,36 @@
1
+ .radio {
2
+ border-radius: var(--border-radius-round);
3
+ aspect-ratio: 1 / 1;
4
+ width: 20px;
5
+ height: 20px;
6
+ }
7
+
8
+ .radio__indicator {
9
+ display: flex;
10
+ position: relative;
11
+ justify-content: center;
12
+ align-items: center;
13
+ width: 100%;
14
+ height: 100%;
15
+
16
+ &::after {
17
+ display: block;
18
+ border-radius: var(--border-radius-round);
19
+ background: var(--view-colorway-dimmer);
20
+ width: 12px;
21
+ height: 12px;
22
+ pointer-events: none;
23
+ content: "";
24
+ }
25
+
26
+ &:not([disabled], [data-disabled])::after {
27
+ background: var(--view-colorway-default);
28
+ }
29
+ }
30
+
31
+ .radio__label {
32
+ display: flex;
33
+ flex-direction: row;
34
+ align-items: center;
35
+ gap: var(--space-8);
36
+ }
@@ -0,0 +1,27 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { tokens } from "~/styles/tokens";
4
+
5
+ import { Radio, RadioGroup } from ".";
6
+
7
+ const meta = {
8
+ title: "Input/Radio",
9
+ component: RadioGroup,
10
+ parameters: { layout: "centered" },
11
+ } satisfies Meta<typeof RadioGroup>;
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ export const Default: Story = {
18
+ args: {
19
+ disabled: false,
20
+ },
21
+ render: (args) => (
22
+ <RadioGroup style={{ width: tokens.space256 }} {...args}>
23
+ <Radio value="public" label="Public" />
24
+ <Radio value="private" label="Private" />
25
+ </RadioGroup>
26
+ ),
27
+ };
@@ -0,0 +1,60 @@
1
+ import clsx from "clsx";
2
+ import React from "react";
3
+
4
+ import { Icon } from "../icon";
5
+ import { IconButton } from "../icon-button";
6
+ import { Input } from "../input";
7
+ import { Spinner } from "../spinner";
8
+ import { View } from "../view";
9
+
10
+ import styles from "./search-bar.module.css";
11
+
12
+ export interface SearchBarProps extends React.ComponentPropsWithoutRef<"input"> {
13
+ /**
14
+ * Is the search bar loading or processing?
15
+ */
16
+ loading?: boolean;
17
+
18
+ /**
19
+ * Called when the input is cleared
20
+ */
21
+ onClear?: () => void;
22
+ }
23
+
24
+ export function SearchBar({
25
+ loading,
26
+ onClear,
27
+ className,
28
+ ...props
29
+ }: SearchBarProps) {
30
+ return (
31
+ <View className={clsx(styles["search-bar"], className)}>
32
+ <Input className={styles["search-bar__input"]} {...props} />
33
+
34
+ {!loading && !props.value ? (
35
+ <View className={styles["search-bar__icon"]}>
36
+ <Icon name="search-line" />
37
+ </View>
38
+ ) : null}
39
+
40
+ {loading ? <Spinner className={styles["search-bar__icon"]} /> : null}
41
+
42
+ {!loading && props.value ? (
43
+ <View
44
+ className={clsx(
45
+ styles["search-bar__icon"],
46
+ styles["search-bar__icon_close"],
47
+ )}
48
+ >
49
+ <IconButton
50
+ icon="close-line"
51
+ alt="Clear"
52
+ size="sm"
53
+ disabled={props.disabled}
54
+ onClick={onClear}
55
+ />
56
+ </View>
57
+ ) : null}
58
+ </View>
59
+ );
60
+ }
@@ -0,0 +1,29 @@
1
+ .search-bar {
2
+ position: relative;
3
+ height: var(--space-32);
4
+ }
5
+
6
+ .search-bar__input {
7
+ position: relative;
8
+ padding-right: var(--space-32);
9
+ width: 100%;
10
+ height: 100%;
11
+ line-height: var(--line-height-input);
12
+ }
13
+
14
+ .search-bar__icon {
15
+ display: flex;
16
+ position: absolute !important;
17
+ top: 0;
18
+ right: 0;
19
+ justify-content: center;
20
+ align-items: center;
21
+ padding: var(--space-8);
22
+ height: 100%;
23
+ pointer-events: none;
24
+ }
25
+
26
+ .search-bar__icon_close {
27
+ padding: var(--space-4);
28
+ pointer-events: auto;
29
+ }
@@ -0,0 +1,37 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { useState } from "react";
3
+
4
+ import { tokens } from "~/styles/tokens";
5
+
6
+ import { SearchBar } from ".";
7
+
8
+ const meta = {
9
+ title: "Search Bar",
10
+ component: SearchBar,
11
+ parameters: { layout: "centered" },
12
+ } satisfies Meta<typeof SearchBar>;
13
+
14
+ export default meta;
15
+
16
+ type Story = StoryObj<typeof meta>;
17
+
18
+ export const Default: Story = {
19
+ args: {
20
+ disabled: false,
21
+ loading: false,
22
+ placeholder: "Search & run commands.",
23
+ },
24
+ render: (args) => {
25
+ // eslint-disable-next-line -- this is okay in storybook
26
+ const [value, setValue] = useState("");
27
+ return (
28
+ <SearchBar
29
+ style={{ width: tokens.space256 }}
30
+ value={value}
31
+ onChange={(e) => setValue(e.target.value)}
32
+ onClear={() => setValue("")}
33
+ {...args}
34
+ />
35
+ );
36
+ },
37
+ };
@@ -0,0 +1,132 @@
1
+ import { Select as SelectPrimitive } from "@base-ui/react/select";
2
+ import clsx from "clsx";
3
+ import { type CSSProperties, type ReactNode } from "react";
4
+
5
+ import { textify } from "~/-utils";
6
+
7
+ import { Icon } from "../icon";
8
+ import { View } from "../view";
9
+
10
+ import transitionStyles from "../../styles/transitions.module.css";
11
+ import styles from "./select.module.css";
12
+
13
+ export function Select<Value>({
14
+ placeholder,
15
+ className,
16
+ style,
17
+ ...props
18
+ }: SelectPrimitive.Root.Props<Value, boolean> & {
19
+ /**
20
+ * Select placeholder if no value is chosen.
21
+ *
22
+ * Behaves differently depending on the type of select.
23
+ * For a normal select, we simply render an item with a value of null.
24
+ * For a `multiple` select, we replace the {@link https://base-ui.com/react/components/select#multiple-selection renderValue} function.
25
+ */
26
+ placeholder?: string;
27
+
28
+ /** Apply styles to SelectPrimitive.trigger */
29
+ className?: string;
30
+
31
+ /** Apply styles to SelectPrimitive.trigger */
32
+ style?: CSSProperties;
33
+ }) {
34
+ return (
35
+ <SelectPrimitive.Root {...props}>
36
+ <View
37
+ interactive="fill-outline"
38
+ render={<SelectPrimitive.Trigger />}
39
+ style={style}
40
+ className={clsx(styles["select__trigger"], className)}
41
+ >
42
+ <SelectPrimitive.Value>
43
+ {createRenderText(placeholder, props.items)}
44
+ </SelectPrimitive.Value>
45
+ <SelectPrimitive.Icon>
46
+ <Icon
47
+ name="arrow-down-s-line"
48
+ className={styles["select__trigger-icon"]}
49
+ />
50
+ </SelectPrimitive.Icon>
51
+ </View>
52
+ <SelectPrimitive.Portal>
53
+ <SelectPrimitive.Positioner sideOffset={4} alignItemWithTrigger={false}>
54
+ <SelectPrimitive.Popup
55
+ className={transitionStyles["transition_scale"]}
56
+ >
57
+ <SelectPrimitive.List className={styles["select__list"]}>
58
+ {placeholder && !props.multiple ? (
59
+ <SelectItem value={null} label={placeholder} />
60
+ ) : null}
61
+
62
+ {Array.isArray(props.items)
63
+ ? props.items.map(({ value, label }) => (
64
+ <SelectItem key={value} value={value} label={label} />
65
+ ))
66
+ : typeof props.items === "object"
67
+ ? Object.entries(props.items).map(([value, label]) => (
68
+ <SelectItem key={value} value={value} label={label} />
69
+ ))
70
+ : null}
71
+ {/* TODO support record as well */}
72
+ </SelectPrimitive.List>
73
+ </SelectPrimitive.Popup>
74
+ </SelectPrimitive.Positioner>
75
+ </SelectPrimitive.Portal>
76
+ </SelectPrimitive.Root>
77
+ );
78
+ }
79
+
80
+ function SelectItem<T>(props: { value: T; label: string }) {
81
+ return (
82
+ <SelectPrimitive.Item
83
+ className={styles["select__item"]}
84
+ value={props.value}
85
+ >
86
+ <SelectPrimitive.ItemIndicator
87
+ className={styles["select__item-indicator"]}
88
+ >
89
+ <Icon name="check-line" />
90
+ </SelectPrimitive.ItemIndicator>
91
+ <SelectPrimitive.ItemText className={styles["select__item-text"]}>
92
+ {textify(props.label)}
93
+ </SelectPrimitive.ItemText>
94
+ </SelectPrimitive.Item>
95
+ );
96
+ }
97
+
98
+ /**
99
+ * Create the renderText function for multiple selections
100
+ * @param placeholder Default select value if nothing is selected
101
+ * @param items Record or array of items defined by SelectPrimitive.Root
102
+ * @returns renderText function
103
+ */
104
+ function createRenderText<Value>(
105
+ placeholder: string | undefined,
106
+ items: SelectPrimitive.Root.Props<Value, true>["items"],
107
+ ) {
108
+ function findValue(value: string) {
109
+ if (Array.isArray(items)) {
110
+ return items.find((item) => item.value === value)?.label;
111
+ } else if (typeof items === "object" && value in items) {
112
+ return items[value as keyof typeof items] as ReactNode;
113
+ }
114
+ }
115
+
116
+ function renderText(value: undefined | string | string[]) {
117
+ if (!value || !items || value.length === 0) {
118
+ return placeholder;
119
+ }
120
+
121
+ if (typeof value === "string") {
122
+ return findValue(value);
123
+ } else {
124
+ return [
125
+ findValue(value[0]) || value[0],
126
+ value.length > 1 ? ` (+${value.length - 1} more)` : "",
127
+ ];
128
+ }
129
+ }
130
+
131
+ return renderText;
132
+ }
@@ -0,0 +1,63 @@
1
+ .select__trigger {
2
+ display: flex;
3
+ flex-direction: row;
4
+ justify-content: space-between;
5
+ align-items: center;
6
+ gap: var(--space-4);
7
+ padding: 0 var(--space-8);
8
+ height: var(--space-32);
9
+
10
+ &:not([disabled], [data-disabled]) {
11
+ color: var(--foreground-default);
12
+ }
13
+ }
14
+
15
+ /* stylelint-disable-next-line */
16
+ [data-popup-open] .select__trigger-icon {
17
+ transform: rotate(180deg);
18
+ }
19
+
20
+ .select__list {
21
+ position: relative;
22
+ z-index: 40;
23
+ box-shadow: var(--shadow-1);
24
+ border: 1px solid var(--surface-interactive-border);
25
+ border-radius: var(--border-radius-default);
26
+ background-color: var(--surface-interactive-background);
27
+ min-width: var(--anchor-width);
28
+ max-height: var(--available-height);
29
+ overflow: hidden;
30
+ }
31
+
32
+ .select__item {
33
+ display: grid;
34
+ position: relative;
35
+ grid-template-columns: var(--space-16) 1fr;
36
+ align-items: center;
37
+ gap: var(--space-4);
38
+ cursor: default;
39
+ cursor: not-allowed;
40
+ outline: none;
41
+ padding: 0 var(--space-8);
42
+ width: 100%;
43
+ height: var(--space-32);
44
+ color: var(--foreground-dimmest);
45
+ user-select: none;
46
+
47
+ &:not([disabled], [data-disabled]) {
48
+ cursor: pointer;
49
+ color: var(--foreground-default);
50
+ }
51
+
52
+ &[data-highlighted] {
53
+ background-color: var(--surface-interactive-background-active);
54
+ }
55
+ }
56
+
57
+ .select__item-indicator {
58
+ grid-column-start: 1;
59
+ }
60
+
61
+ .select__item-text {
62
+ grid-column-start: 2;
63
+ }
@@ -0,0 +1,49 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { tokens } from "~/styles/tokens";
4
+
5
+ import { Select } from ".";
6
+
7
+ const meta = {
8
+ title: "Input/Select",
9
+ component: Select,
10
+ parameters: { layout: "centered" },
11
+ } satisfies Meta<typeof Select>;
12
+
13
+ export default meta;
14
+
15
+ type Story = StoryObj<typeof meta>;
16
+
17
+ const items = {
18
+ cpp: "C++",
19
+ python: "Python",
20
+ javascript: "JavaScript",
21
+ ruby: "Ruby",
22
+ };
23
+
24
+ export const Default: Story = {
25
+ args: {},
26
+ render: (args) => (
27
+ <Select
28
+ {...args}
29
+ style={{ width: tokens.space256 }}
30
+ placeholder="Select language"
31
+ items={items}
32
+ />
33
+ ),
34
+ };
35
+
36
+ export const SelectMultiple: Story = {
37
+ args: {
38
+ placeholder: "Select languages...",
39
+ },
40
+ render: (args) => (
41
+ <Select
42
+ style={{ width: tokens.space256 }}
43
+ multiple
44
+ defaultValue={["javascript"]}
45
+ items={items}
46
+ {...args}
47
+ />
48
+ ),
49
+ };
@@ -0,0 +1,28 @@
1
+ import { Separator as SeparatorPrimitive } from "@base-ui/react/separator";
2
+ import clsx from "clsx";
3
+ import type { ComponentProps } from "react";
4
+
5
+ import styles from "./separator.module.css";
6
+
7
+ export type SeparatorProps = ComponentProps<typeof SeparatorPrimitive> & {
8
+ /** Separator color */
9
+ color?: "default" | "stronger";
10
+ };
11
+
12
+ export function Separator({
13
+ color = "default",
14
+ className,
15
+ ...props
16
+ }: SeparatorProps) {
17
+ return (
18
+ <SeparatorPrimitive
19
+ className={clsx(
20
+ styles["separator"],
21
+ styles[`separator_color_${color}`],
22
+ styles[`separator_orientation_${props.orientation || "horizontal"}`],
23
+ className,
24
+ )}
25
+ {...props}
26
+ />
27
+ );
28
+ }
@@ -0,0 +1,24 @@
1
+ .separator {
2
+ flex-grow: 0;
3
+ flex-shrink: 0;
4
+ outline: none;
5
+ border: none;
6
+ }
7
+
8
+ .separator_color_default {
9
+ background-color: var(--surface-interactive-border);
10
+ }
11
+
12
+ .separator_color_stronger {
13
+ background-color: var(--surface-interactive-border-hover);
14
+ }
15
+
16
+ .separator_orientation_horizontal {
17
+ height: 1px;
18
+ min-height: 1px;
19
+ }
20
+
21
+ .separator_orientation_vertical {
22
+ width: 1px;
23
+ min-width: 1px;
24
+ }
@@ -0,0 +1,40 @@
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+
3
+ import { tokens } from "~/styles/tokens";
4
+
5
+ import { Separator } from ".";
6
+ import { Heading } from "../heading";
7
+ import { Text } from "../text";
8
+ import { View } from "../view";
9
+
10
+ const meta = {
11
+ title: "Separator",
12
+ component: Separator,
13
+ parameters: { layout: "centered" },
14
+ } satisfies Meta<typeof Separator>;
15
+
16
+ export default meta;
17
+
18
+ type Story = StoryObj<typeof meta>;
19
+
20
+ export const Default: Story = {
21
+ args: {
22
+ color: "default",
23
+ },
24
+ render: (args) => (
25
+ <View style={{ gap: tokens.space12 }}>
26
+ <View style={{ gap: tokens.space2 }}>
27
+ <Heading level={2}>@koide-labs/ui</Heading>
28
+ <Text>It's @koide-labs/ui, built with CSS Modules and Base UI.</Text>
29
+ </View>
30
+ <Separator {...args} orientation="horizontal" />
31
+ <View style={{ flexDirection: "row", gap: tokens.space12 }}>
32
+ <Text>Home</Text>
33
+ <Separator {...args} orientation="vertical" />
34
+ <Text>Pricing</Text>
35
+ <Separator {...args} orientation="vertical" />
36
+ <Text>Blog</Text>
37
+ </View>
38
+ </View>
39
+ ),
40
+ };
@@ -0,0 +1,28 @@
1
+ import { Slider as SliderPrimitive } from "@base-ui/react/slider";
2
+ import clsx from "clsx";
3
+
4
+ import { View, type Color } from "../view";
5
+
6
+ import styles from "./slider.module.css";
7
+
8
+ export function Slider({
9
+ color = "primary",
10
+ ...props
11
+ }: SliderPrimitive.Root.Props & {
12
+ color?: Color;
13
+ }) {
14
+ return (
15
+ <SliderPrimitive.Root {...props}>
16
+ <SliderPrimitive.Control className={styles["slider__control"]}>
17
+ <View
18
+ render={<SliderPrimitive.Track />}
19
+ color={color}
20
+ className={styles["slider__track"]}
21
+ >
22
+ <SliderPrimitive.Indicator className={styles["slider__indicator"]} />
23
+ <SliderPrimitive.Thumb className={clsx(styles["slider__thumb"])} />
24
+ </View>
25
+ </SliderPrimitive.Control>
26
+ </SliderPrimitive.Root>
27
+ );
28
+ }
@@ -0,0 +1,52 @@
1
+ .slider__control {
2
+ display: flex;
3
+ align-items: center;
4
+ touch-action: none;
5
+ user-select: none;
6
+ }
7
+
8
+ .slider__track {
9
+ border-radius: var(--border-radius-round);
10
+ background-color: var(--surface-interactive-border);
11
+ width: 100%;
12
+ height: var(--space-4);
13
+ user-select: none;
14
+ }
15
+
16
+ .slider__indicator {
17
+ border-color: var(--view-colorway-default);
18
+ border-radius: var(--border-radius-round);
19
+ background-color: var(--view-colorway-dimmer);
20
+ user-select: none;
21
+
22
+ &:not([disabled], [data-disabled]) {
23
+ background-color: var(--view-colorway-default);
24
+ }
25
+ }
26
+
27
+ .slider__thumb {
28
+ cursor: not-allowed;
29
+ border-width: 1px;
30
+ border-style: solid;
31
+ border-radius: var(--border-radius-round);
32
+ background-color: var(--view-colorway-dimmer);
33
+ aspect-ratio: 1 / 1;
34
+ width: var(--space-16);
35
+ height: var(--space-16);
36
+ user-select: none;
37
+
38
+ &:not([disabled], [data-disabled]) {
39
+ cursor: ew-resize;
40
+ border-color: var(--view-colorway-default);
41
+ background-color: var(--view-colorway-default);
42
+ }
43
+
44
+ &:not([disabled], [data-disabled]):hover {
45
+ border-color: var(--view-colorway-stronger);
46
+ }
47
+
48
+ &:not([disabled], [data-disabled])[data-dragging] {
49
+ border-color: var(--view-colorway-stronger);
50
+ background-color: var(--view-colorway-default);
51
+ }
52
+ }