diff --git a/packages/app/.gitignore b/packages/app/.gitignore
index 30685c898..371b45136 100644
--- a/packages/app/.gitignore
+++ b/packages/app/.gitignore
@@ -1 +1,2 @@
-public/storybook
\ No newline at end of file
+public/storybook
+public/ui
diff --git a/packages/app/next.config.mjs b/packages/app/next.config.mjs
index a8e5f8ca0..bd8b33adb 100644
--- a/packages/app/next.config.mjs
+++ b/packages/app/next.config.mjs
@@ -31,6 +31,11 @@ const config = {
destination: '/storybook/index.html',
permanent: false,
},
+ {
+ source: '/ui',
+ destination: '/ui/index.html',
+ permanent: false,
+ },
];
},
};
diff --git a/packages/storybook-addon-theme/package.json b/packages/storybook-addon-theme/package.json
index cf484a798..b93eb44d7 100644
--- a/packages/storybook-addon-theme/package.json
+++ b/packages/storybook-addon-theme/package.json
@@ -1,5 +1,6 @@
{
"name": "storybook-addon-theme",
+ "private": true,
"version": "0.0.1",
"access": "public",
"main": "./src/index.tsx",
diff --git a/packages/ui/.storybook/main.ts b/packages/ui/.storybook/main.ts
new file mode 100644
index 000000000..bfb8759b4
--- /dev/null
+++ b/packages/ui/.storybook/main.ts
@@ -0,0 +1,37 @@
+import tsconfigpath from 'vite-tsconfig-paths';
+import { UserConfig, mergeConfig } from 'vite';
+import type { StorybookConfig } from '@storybook/react-vite';
+
+const config: StorybookConfig = {
+ stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.tsx'],
+ addons: [
+ '@storybook/addon-links',
+ '@storybook/addon-essentials',
+ '@storybook/addon-interactions',
+ '@storybook/addon-storysource',
+ '@storybook/addon-a11y',
+ 'storybook-addon-theme/register',
+ ],
+ staticDirs: ['../public'],
+ core: {
+ disableTelemetry: true,
+ builder: '@storybook/builder-vite',
+ },
+ framework: {
+ name: '@storybook/react-vite',
+ options: {},
+ },
+ typescript: {
+ check: false,
+ reactDocgen: 'react-docgen',
+ },
+ async viteFinal(config: UserConfig) {
+ return mergeConfig(config, {
+ plugins: [tsconfigpath()],
+ define: {
+ __STORYBOOK_FUEL_UI__: '"true"',
+ },
+ });
+ },
+};
+export default config;
diff --git a/packages/ui/.storybook/preview.tsx b/packages/ui/.storybook/preview.tsx
new file mode 100644
index 000000000..70ababedc
--- /dev/null
+++ b/packages/ui/.storybook/preview.tsx
@@ -0,0 +1,44 @@
+import '@fontsource-variable/inter/slnt.css';
+import '../src/theme/index.css';
+
+import { withThemeDecorator } from 'storybook-addon-theme';
+import { Preview } from '@storybook/react';
+import { INITIAL_VIEWPORTS } from '@storybook/addon-viewport';
+import { Theme } from '../src/components/Theme';
+import { Toaster } from '../src/components/Toast';
+import { ReactNode } from 'react';
+
+function ThemeWrapper({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
+
+const preview: Preview = {
+ decorators: [withThemeDecorator(ThemeWrapper)],
+
+ parameters: {
+ actions: {
+ argTypesRegex: '^on[A-Z].*',
+ },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/,
+ },
+ },
+ options: {
+ storySort: {
+ order: ['Base', 'Layout', 'Form', 'Overlay', 'UI', 'Helpers', 'Web3'],
+ },
+ },
+ viewport: {
+ viewports: INITIAL_VIEWPORTS,
+ },
+ },
+};
+
+export default preview;
diff --git a/packages/ui/package.json b/packages/ui/package.json
new file mode 100644
index 000000000..0aa7f9ab9
--- /dev/null
+++ b/packages/ui/package.json
@@ -0,0 +1,116 @@
+{
+ "name": "@fuels/ui",
+ "version": "0.0.1",
+ "access": "public",
+ "private": true,
+ "main": "./src/index.tsx",
+ "exports": {
+ ".": "./src/index.tsx",
+ "./tailwind-preset": "./src/theme/tailwind-preset.ts",
+ "./styles.css": "./src/theme/index.css"
+ },
+ "typesVersions": {
+ "*": {
+ "tailwind-preset": [
+ "./src/theme/tailwind-preset.ts"
+ ]
+ }
+ },
+ "publishConfig": {
+ "main": "./dist/index.cjs.js",
+ "module": "./dist/index.cjs.js",
+ "types": "./dist/index.d.ts",
+ "typings": "./dist/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./dist/index.esm.js",
+ "require": "./dist/index.cjs.jg",
+ "default": "./dist/index.cjs.js",
+ "types": "./dist/index.d.ts",
+ "typings": "./dist/index.d.ts"
+ },
+ "./tailwind-preset": {
+ "import": "./dist/theme/index.cjs.js",
+ "require": "./dist/theme/index.cjs.js",
+ "default": "./dist/theme/index.cjs.js",
+ "types": "./dist/theme/tailwind-preset.d.ts",
+ "typings": "./dist/theme/tailwind-preset.d.ts"
+ },
+ "./styles.css": "./dist/index.css"
+ },
+ "files": [
+ "dist"
+ ]
+ },
+ "scripts": {
+ "build:storybook": "pnpm ts:check && storybook build -o ../app/public/ui",
+ "build:preview": "pnpm build:storybook",
+ "build": "tsup --dts",
+ "build:watch": "tsup --watch",
+ "dev:storybook": "storybook dev -p 6006",
+ "test": "jest --verbose --passWithNoTests",
+ "test:dev": "pnpm ts:check && pnpm test",
+ "ts:check": "tsc --noEmit",
+ "generate:defs": "node scripts/create-defs.mjs"
+ },
+ "dependencies": {
+ "@fontsource-variable/inter": "5.0.8",
+ "@fuel-ts/math": "0.59.0",
+ "@radix-ui/colors": "3.0.0-rc.4",
+ "@radix-ui/react-accordion": "1.1.2",
+ "@radix-ui/react-aspect-ratio": "1.0.3",
+ "@radix-ui/react-dialog": "1.0.5",
+ "@radix-ui/react-slot": "1.0.2",
+ "@radix-ui/react-toast": "1.1.5",
+ "@radix-ui/themes": "^1.1.2",
+ "@react-aria/focus": "3.14.1",
+ "@tabler/icons-react": "2.35.0",
+ "@tailwindcss/typography": "0.5.10",
+ "clsx": "2.0.0",
+ "csstype": "3.1.2",
+ "deepmerge-json": "1.5.0",
+ "framer-motion": "10.16.4",
+ "modern-normalize": "2.0.0",
+ "radix-ui-themes-with-tailwind": "1.2.6",
+ "react": "^18.2.0",
+ "react-aria": "3.28.0",
+ "react-dom": "^18.2.0",
+ "react-stately": "3.26.0",
+ "react-use": "17.4.0",
+ "tailwind-merge": "1.14.0",
+ "tailwind-variants": "0.1.14",
+ "tailwindcss-animate": "1.0.7",
+ "tailwindcss-radix": "2.8.0",
+ "tailwindcss-themer": "3.1.0"
+ },
+ "devDependencies": {
+ "@chialab/esbuild-plugin-meta-url": "0.17.7",
+ "@storybook/addon-a11y": "^7.4.5",
+ "@storybook/addon-actions": "^7.4.5",
+ "@storybook/addon-essentials": "^7.4.5",
+ "@storybook/addon-interactions": "^7.4.5",
+ "@storybook/addon-links": "^7.4.5",
+ "@storybook/addon-storysource": "^7.4.5",
+ "@storybook/addon-viewport": "7.4.5",
+ "@storybook/addons": "^7.4.5",
+ "@storybook/react": "^7.4.5",
+ "@storybook/react-vite": "^7.4.5",
+ "@storybook/testing-library": "^0.2.1",
+ "@storybook/types": "^7.4.5",
+ "@types/lodash": "4.14.199",
+ "@types/react": "^18.2.22",
+ "@types/react-dom": "^18.2.7",
+ "autoprefixer": "10.4.16",
+ "globby": "13.2.2",
+ "lodash": "^4.17.21",
+ "postcss": "8.4.30",
+ "postcss-import": "15.1.0",
+ "storybook": "^7.4.5",
+ "storybook-addon-theme": "workspace:*",
+ "tailwindcss": "3.3.3",
+ "tsconfig-paths-webpack-plugin": "^4.1.0",
+ "typescript": "5.2.2",
+ "vite": "^4.4.9",
+ "vite-tsconfig-paths": "^4.2.1"
+ }
+}
diff --git a/packages/ui/postcss.config.js b/packages/ui/postcss.config.js
new file mode 100644
index 000000000..e569373fd
--- /dev/null
+++ b/packages/ui/postcss.config.js
@@ -0,0 +1,8 @@
+module.exports = {
+ plugins: {
+ 'postcss-import': {},
+ 'tailwindcss/nesting': {},
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+};
diff --git a/packages/ui/public/assets/eth.svg b/packages/ui/public/assets/eth.svg
new file mode 100644
index 000000000..7b3245b2a
--- /dev/null
+++ b/packages/ui/public/assets/eth.svg
@@ -0,0 +1,19 @@
+
diff --git a/packages/ui/public/favicon.svg b/packages/ui/public/favicon.svg
new file mode 100644
index 000000000..dff6a0dbb
--- /dev/null
+++ b/packages/ui/public/favicon.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/ui/public/icons.svg b/packages/ui/public/icons.svg
new file mode 100644
index 000000000..76d27f961
--- /dev/null
+++ b/packages/ui/public/icons.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/packages/ui/public/logo.svg b/packages/ui/public/logo.svg
new file mode 100644
index 000000000..dff6a0dbb
--- /dev/null
+++ b/packages/ui/public/logo.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/ui/scripts/create-defs.mjs b/packages/ui/scripts/create-defs.mjs
new file mode 100644
index 000000000..7a3ab5eac
--- /dev/null
+++ b/packages/ui/scripts/create-defs.mjs
@@ -0,0 +1,283 @@
+import { promises as fs } from 'fs';
+import { globby } from 'globby';
+import _ from 'lodash';
+import path from 'path';
+import prettier from 'prettier';
+import ts from 'typescript';
+import * as url from 'url';
+
+const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
+
+const COMPONENT_DIR = path.join(__dirname, '../src/components');
+const INDEX_FILE = path.join(__dirname, '../src/index.tsx');
+
+const PRETTIER_CONFIG = {
+ printWidth: 80,
+ semi: true,
+ tabWidth: 2,
+ useTabs: false,
+ singleQuote: true,
+ bracketSpacing: true,
+ arrowParens: 'always',
+ quoteProps: 'as-needed',
+};
+
+function prettierFormat(str) {
+ return prettier.format(str, { parser: 'typescript', ...PRETTIER_CONFIG });
+}
+
+function extractExports(sourceFile) {
+ const exports = {
+ valueExports: [],
+ typeExports: [],
+ };
+
+ function visit(node) {
+ if (ts.isExportAssignment(node)) {
+ exports.valueExports.push('default');
+ } else if (ts.isExportDeclaration(node)) {
+ if (node.exportClause && ts.isNamedExports(node.exportClause)) {
+ for (const element of node.exportClause.elements) {
+ if (node.isTypeOnly) {
+ exports.typeExports.push(element.name.getText(sourceFile));
+ } else {
+ exports.valueExports.push(element.name.getText(sourceFile));
+ }
+ }
+ }
+ } else if (
+ ts.isFunctionDeclaration(node) ||
+ ts.isClassDeclaration(node) ||
+ ts.isVariableStatement(node) ||
+ ts.isEnumDeclaration(node)
+ ) {
+ if (
+ node.modifiers &&
+ node.modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
+ ) {
+ if (ts.isVariableStatement(node)) {
+ for (const declaration of node.declarationList.declarations) {
+ exports.valueExports.push(declaration.name.getText(sourceFile));
+ }
+ } else {
+ exports.valueExports.push(node.name.text);
+ }
+ }
+ } else if (
+ ts.isTypeAliasDeclaration(node) ||
+ ts.isInterfaceDeclaration(node)
+ ) {
+ if (
+ node.modifiers &&
+ node.modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword)
+ ) {
+ exports.typeExports.push(node.name.text);
+ }
+ }
+ ts.forEachChild(node, visit);
+ }
+
+ visit(sourceFile);
+
+ exports.valueExports = _.uniq(exports.valueExports);
+ exports.typeExports = _.uniq(exports.typeExports);
+ return exports;
+}
+
+async function getAllComponents() {
+ const allComponents = await globby(
+ [
+ '**/*.tsx',
+ '**/use**.ts',
+ '!**/*.stories.tsx',
+ '!**/*.test.{tsx,ts}',
+ '!**/styles.ts',
+ ],
+ {
+ deep: 2,
+ cwd: COMPONENT_DIR,
+ absolute: true,
+ onlyFiles: true,
+ },
+ );
+
+ const components = await Promise.all(
+ allComponents
+ .map(async (comp) => {
+ const name = path.parse(comp).name;
+ const baseComponent = path.parse(path.dirname(comp)).name;
+ const component = name === baseComponent ? null : name;
+ const sourceFile = ts.createSourceFile(
+ comp,
+ await fs.readFile(comp, 'utf8'),
+ );
+
+ const { valueExports, typeExports } = extractExports(sourceFile);
+ return {
+ dir: path.dirname(comp),
+ component,
+ baseComponent,
+ exports: [...new Set(valueExports)],
+ types: [...new Set(typeExports)],
+ };
+ })
+ .filter(Boolean),
+ );
+
+ return components;
+}
+
+function createExportStr(from, exports, isType, pathPrefix = '') {
+ const listStr = exports?.sort().join(',');
+ return exports.length
+ ? `export ${
+ isType ? 'type' : ''
+ } { ${listStr} } from './${pathPrefix}${from}';`
+ : '';
+}
+
+function createNestedExportStr(main, nested, isType) {
+ if (!nested.length) return '';
+ const key = isType ? 'types' : 'exports';
+ let items = [];
+ for (const n of nested) {
+ let list = n[key]
+ .filter((i) => main.indexOf(i) === -1)
+ .filter((i) => items.every((item) => item.list.indexOf(i) === -1));
+ const from = n.component;
+ items = items.concat({ from, list });
+ }
+
+ return items
+ .map((n) => {
+ const res = createExportStr(n.from, n.list, isType);
+ return res.length ? res : null;
+ })
+ .filter(Boolean)
+ .sort()
+ .join('\n');
+}
+
+async function createComponentIndex(components) {
+ const mainComponents = components.filter((s) => !s.component);
+ for (const item of mainComponents) {
+ const component = item.baseComponent;
+ const nested = components.filter(
+ (c) =>
+ c.baseComponent === component &&
+ c.component &&
+ !c.component?.startsWith('index'),
+ );
+
+ const mainExportsStr = createExportStr(component, item.exports);
+ const mainTypesStr = createExportStr(component, item.types, true);
+ const nestedExportStr = createNestedExportStr(item.exports, nested);
+ const nestedTypesStr = createNestedExportStr(item.types, nested, true);
+
+ const index = [
+ mainExportsStr,
+ mainTypesStr,
+ nestedExportStr,
+ nestedTypesStr,
+ ].join('\n\n');
+
+ const content = await prettierFormat(index);
+ await fs.writeFile(`${item.dir}/index.tsx`, content);
+ }
+}
+
+async function createMainComponentsIndex(components) {
+ const mainComponents = components.filter((s) => !s.component);
+ let list = [];
+
+ for (const item of mainComponents) {
+ const mainPath = `${item.dir}/index.tsx`;
+ const mainStr = await fs.readFile(mainPath, 'utf8');
+ const sourceFile = ts.createSourceFile(mainPath, mainStr);
+ const { valueExports, typeExports } = extractExports(sourceFile);
+ const uniqueExports = [...new Set(valueExports)];
+ const uniqueTypes = [...new Set(typeExports)];
+ const exportsStr = createExportStr(
+ item.baseComponent,
+ uniqueExports,
+ false,
+ 'components/',
+ );
+ const typesStr = createExportStr(
+ item.baseComponent,
+ uniqueTypes,
+ true,
+ 'components/',
+ );
+ if (exportsStr.length) list.push(exportsStr);
+ if (typesStr.length) list.push(typesStr);
+ }
+
+ list = list.join('\n\n');
+ const content = await prettierFormat(list);
+ await fs.writeFile(INDEX_FILE, content);
+}
+
+async function pkgJSON(components) {
+ const pkgJSONBuffer = await fs.readFile(
+ path.join(__dirname, '../package.json'),
+ );
+ let pkgJSON = JSON.parse(pkgJSONBuffer.toString());
+ let typesVersions = { '*': {} };
+ let exportsConfig = {};
+
+ const comps = Array.from(new Set(components.map((c) => c.baseComponent)));
+ comps.forEach((component) => {
+ const name = component;
+ const folder = path.join('./dist/components', name);
+ typesVersions['*'][name] = [`./${folder}/index.d.ts`];
+ exportsConfig[`./${name}`] = {
+ import: `./${folder}/index.esm.js`,
+ types: `./${folder}/index.d.ts`,
+ typings: `./${folder}/index.d.ts`,
+ };
+ });
+ typesVersions['*']['.'] = ['./dist/index.d.ts'];
+ exportsConfig['.'] = {
+ import: './dist/index.esm.mjs',
+ types: './dist/index.d.ts',
+ typings: './dist/index.d.ts',
+ };
+ exportsConfig['./index.css'] = './dist/index.css';
+ exportsConfig['./theme'] = {
+ import: './dist/theme.esm.js',
+ types: './dist/theme.d.ts',
+ typings: './dist/theme.d.ts',
+ };
+
+ pkgJSON.exports = exportsConfig;
+ pkgJSON.typesVersions = typesVersions;
+
+ await fs.writeFile(
+ path.join(__dirname, '../package.json'),
+ JSON.stringify(pkgJSON, null, 2),
+ );
+}
+
+export async function tsup(components) {
+ const comps = Array.from(new Set(components.map((c) => c.baseComponent)));
+ const entryPoints = comps.map((c) => `src/components/${c}/index.tsx`);
+
+ await fs.writeFile(
+ path.join(__dirname, '../tsup.text'),
+ JSON.stringify({ entry: entryPoints }),
+ );
+}
+
+async function main() {
+ const components = await getAllComponents();
+ // const allIndex = await globby(`${COMPONENT_DIR}/**/index.tsx`);
+ // await Promise.all(allIndex.map((i) => fs.rm(i)));
+
+ // await createComponentIndex(components);
+ // await createMainComponentsIndex(components);
+ await pkgJSON(components);
+ // await tsup(components);
+}
+
+main();
diff --git a/packages/ui/src/components/Accordion/Accordion.stories.tsx b/packages/ui/src/components/Accordion/Accordion.stories.tsx
new file mode 100644
index 000000000..a89a59a16
--- /dev/null
+++ b/packages/ui/src/components/Accordion/Accordion.stories.tsx
@@ -0,0 +1,32 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Box } from '../Box';
+
+import { Accordion } from './Accordion';
+
+const meta: Meta = {
+ title: 'UI/Accordion',
+ component: Accordion,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+ What's Fuel?
+
+ The world's fastest modular execution layer.
+
+
+
+ Is it really fast?
+ Yes, it's blazingly fast.
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Accordion/Accordion.tsx b/packages/ui/src/components/Accordion/Accordion.tsx
new file mode 100644
index 000000000..ca242ece6
--- /dev/null
+++ b/packages/ui/src/components/Accordion/Accordion.tsx
@@ -0,0 +1,72 @@
+import * as AC from '@radix-ui/react-accordion';
+import { IconChevronDown } from '@tabler/icons-react';
+
+import { createComponent, withNamespace } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+import { Icon } from '../Icon/Icon';
+
+import { styles } from './styles';
+
+export type AccordionProps = PropsOf;
+export type AccordionContentProps = PropsOf;
+export type AccordionItemProps = PropsOf;
+export type AccordionHeaderProps = PropsOf;
+export type AccordionTriggerProps = PropsOf;
+
+export const AccordionRoot = createComponent({
+ id: 'Accordion',
+ className: () => styles().root(),
+ baseElement: AC.Root,
+});
+
+export const AccordionContent = createComponent<
+ AccordionContentProps,
+ typeof AC.Content
+>({
+ id: 'AccordionContent',
+ className: () => styles().content(),
+ baseElement: AC.Content,
+});
+
+export const AccordionItem = createComponent<
+ AccordionItemProps,
+ typeof AC.Item
+>({
+ id: 'AccordionItem',
+ className: () => styles().item(),
+ baseElement: AC.Item,
+});
+
+export const AccordionHeader = createComponent<
+ AccordionHeaderProps,
+ typeof AC.Header
+>({
+ id: 'AccordionHeader',
+ className: () => styles().header(),
+ baseElement: AC.Header,
+});
+
+export const AccordionTrigger = createComponent<
+ AccordionTriggerProps,
+ typeof AC.Trigger
+>({
+ id: 'AccordionTrigger',
+ baseElement: AC.Trigger,
+ render: (Comp, { children, className, ...props }) => {
+ const classes = styles();
+ return (
+
+
+ {children}
+
+
+
+ );
+ },
+});
+
+export const Accordion = withNamespace(AccordionRoot, {
+ Item: AccordionItem,
+ Content: AccordionContent,
+ Trigger: AccordionTrigger,
+});
diff --git a/packages/ui/src/components/Accordion/index.tsx b/packages/ui/src/components/Accordion/index.tsx
new file mode 100644
index 000000000..9313c46eb
--- /dev/null
+++ b/packages/ui/src/components/Accordion/index.tsx
@@ -0,0 +1,18 @@
+'use client';
+
+export {
+ Accordion,
+ AccordionContent,
+ AccordionHeader,
+ AccordionItem,
+ AccordionRoot,
+ AccordionTrigger,
+} from './Accordion';
+
+export type {
+ AccordionContentProps,
+ AccordionHeaderProps,
+ AccordionItemProps,
+ AccordionProps,
+ AccordionTriggerProps,
+} from './Accordion';
diff --git a/packages/ui/src/components/Accordion/styles.ts b/packages/ui/src/components/Accordion/styles.ts
new file mode 100644
index 000000000..7e0f1686f
--- /dev/null
+++ b/packages/ui/src/components/Accordion/styles.ts
@@ -0,0 +1,23 @@
+import { tv } from 'tailwind-variants';
+
+export const styles = tv({
+ slots: {
+ root: 'max-w-full',
+ content: [
+ 'overflow-hidden p-4 text-color',
+ 'state-open:animate-accordion-open',
+ 'state-closed:animate-accordion-closed',
+ ],
+ item: ['overflow-hidden rounded-none not-first:mt-1'],
+ trigger: [
+ 'group bg-card-bg rounded-md transition-colors px-4 flex text-lg font-medium',
+ 'w-full h-[45px] items-center justify-between border border-border',
+ 'focus:outline-none text-heading hover:text-accent',
+ 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent',
+ ],
+ header: 'flex',
+ icon: [
+ 'transition-transform text-icon group-hover:rotate-180 group-state-open:rotate-180',
+ ],
+ },
+});
diff --git a/packages/ui/src/components/Alert/Alert.stories.tsx b/packages/ui/src/components/Alert/Alert.stories.tsx
new file mode 100644
index 000000000..dadb3d907
--- /dev/null
+++ b/packages/ui/src/components/Alert/Alert.stories.tsx
@@ -0,0 +1,27 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconAlertCircle } from '@tabler/icons-react';
+
+import { Icon } from '../Icon/Icon';
+
+import { Alert } from './Alert';
+
+const meta: Meta = {
+ title: 'UI/Alert',
+ component: Alert,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+
+
+ You will need admin privileges to install and access this application.
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Alert/Alert.tsx b/packages/ui/src/components/Alert/Alert.tsx
new file mode 100644
index 000000000..ad94e3400
--- /dev/null
+++ b/packages/ui/src/components/Alert/Alert.tsx
@@ -0,0 +1,31 @@
+import { Callout as RC } from '@radix-ui/themes';
+
+import { createComponent, withNamespace } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type AlertProps = PropsOf;
+export type AlertIconProps = PropsOf;
+export type AlertTextProps = PropsOf;
+
+export const AlertRoot = createComponent({
+ id: 'Alert',
+ baseElement: RC.Root,
+ defaultProps: {
+ color: 'blue',
+ },
+});
+
+export const AlertIcon = createComponent({
+ id: 'AlertIcon',
+ baseElement: RC.Icon,
+});
+
+export const AlertText = createComponent({
+ id: 'AlertText',
+ baseElement: RC.Text,
+});
+
+export const Alert = withNamespace(AlertRoot, {
+ Icon: AlertIcon,
+ Text: AlertText,
+});
diff --git a/packages/ui/src/components/Alert/index.tsx b/packages/ui/src/components/Alert/index.tsx
new file mode 100644
index 000000000..9f73dd22b
--- /dev/null
+++ b/packages/ui/src/components/Alert/index.tsx
@@ -0,0 +1,5 @@
+'use client';
+
+export { Alert, AlertIcon, AlertRoot, AlertText } from './Alert';
+
+export type { AlertIconProps, AlertProps, AlertTextProps } from './Alert';
diff --git a/packages/ui/src/components/AlertDialog/AlertDialog.stories.tsx b/packages/ui/src/components/AlertDialog/AlertDialog.stories.tsx
new file mode 100644
index 000000000..e2d56e1cf
--- /dev/null
+++ b/packages/ui/src/components/AlertDialog/AlertDialog.stories.tsx
@@ -0,0 +1,46 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { HStack } from '../Box';
+import { Button } from '../Button/Button';
+
+import { AlertDialog } from './AlertDialog';
+
+const meta: Meta = {
+ title: 'Overlay/AlertDialog',
+ component: AlertDialog,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+
+
+ Revoke access
+
+ Are you sure? This application will no longer be accessible and any
+ existing sessions will be expired.
+
+
+
+
+
+
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/AlertDialog/AlertDialog.tsx b/packages/ui/src/components/AlertDialog/AlertDialog.tsx
new file mode 100644
index 000000000..f0beca084
--- /dev/null
+++ b/packages/ui/src/components/AlertDialog/AlertDialog.tsx
@@ -0,0 +1,77 @@
+import { AlertDialog as RAD } from '@radix-ui/themes';
+
+import { createComponent, withNamespace } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type AlertDialogProps = PropsOf;
+export type AlertDialogTriggerProps = PropsOf;
+export type AlertDialogContentProps = PropsOf;
+export type AlertDialogTitleProps = PropsOf;
+export type AlertDialogDescriptionProps = PropsOf;
+export type AlertDialogActionProps = PropsOf;
+export type AlertDialogCancelProps = PropsOf;
+
+export const AlertDialogRoot = createComponent<
+ AlertDialogProps,
+ typeof RAD.Root
+>({
+ id: 'AlertDialog',
+ baseElement: RAD.Root,
+});
+
+export const AlertDialogTrigger = createComponent<
+ AlertDialogTriggerProps,
+ typeof RAD.Trigger
+>({
+ id: 'AlertDialogTrigger',
+ baseElement: RAD.Trigger,
+});
+
+export const AlertDialogContent = createComponent<
+ AlertDialogContentProps,
+ typeof RAD.Content
+>({
+ id: 'AlertDialogContent',
+ baseElement: RAD.Content,
+});
+
+export const AlertDialogTitle = createComponent<
+ AlertDialogTitleProps,
+ typeof RAD.Title
+>({
+ id: 'AlertDialogTitle',
+ baseElement: RAD.Title,
+});
+
+export const AlertDialogDescription = createComponent<
+ AlertDialogDescriptionProps,
+ typeof RAD.Description
+>({
+ id: 'AlertDialogDescription',
+ baseElement: RAD.Description,
+});
+
+export const AlertDialogAction = createComponent<
+ AlertDialogActionProps,
+ typeof RAD.Action
+>({
+ id: 'AlertDialogAction',
+ baseElement: RAD.Action,
+});
+
+export const AlertDialogCancel = createComponent<
+ AlertDialogCancelProps,
+ typeof RAD.Cancel
+>({
+ id: 'AlertDialogCancel',
+ baseElement: RAD.Cancel,
+});
+
+export const AlertDialog = withNamespace(AlertDialogRoot, {
+ Trigger: AlertDialogTrigger,
+ Content: AlertDialogContent,
+ Title: AlertDialogTitle,
+ Description: AlertDialogDescription,
+ Action: AlertDialogAction,
+ Cancel: AlertDialogCancel,
+});
diff --git a/packages/ui/src/components/AlertDialog/index.tsx b/packages/ui/src/components/AlertDialog/index.tsx
new file mode 100644
index 000000000..f5014f83e
--- /dev/null
+++ b/packages/ui/src/components/AlertDialog/index.tsx
@@ -0,0 +1,20 @@
+export {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogRoot,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+} from './AlertDialog';
+
+export type {
+ AlertDialogActionProps,
+ AlertDialogCancelProps,
+ AlertDialogContentProps,
+ AlertDialogDescriptionProps,
+ AlertDialogProps,
+ AlertDialogTitleProps,
+ AlertDialogTriggerProps,
+} from './AlertDialog';
diff --git a/packages/ui/src/components/AspectRatio/AspectRatio.stories.tsx b/packages/ui/src/components/AspectRatio/AspectRatio.stories.tsx
new file mode 100644
index 000000000..d1d3feb30
--- /dev/null
+++ b/packages/ui/src/components/AspectRatio/AspectRatio.stories.tsx
@@ -0,0 +1,27 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Box } from '../Box';
+
+import { AspectRatio } from './AspectRatio';
+
+const meta: Meta = {
+ title: 'UI/AspectRatio',
+ component: AspectRatio,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/AspectRatio/AspectRatio.tsx b/packages/ui/src/components/AspectRatio/AspectRatio.tsx
new file mode 100644
index 000000000..00e28dbe3
--- /dev/null
+++ b/packages/ui/src/components/AspectRatio/AspectRatio.tsx
@@ -0,0 +1,15 @@
+///
+import { AspectRatio as RadixAspectRatio } from '@radix-ui/themes';
+
+import { createComponent } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type AspectRatioProps = PropsOf;
+
+export const AspectRatio = createComponent<
+ AspectRatioProps,
+ typeof RadixAspectRatio
+>({
+ id: 'AspectRatio',
+ baseElement: RadixAspectRatio,
+});
diff --git a/packages/ui/src/components/AspectRatio/index.tsx b/packages/ui/src/components/AspectRatio/index.tsx
new file mode 100644
index 000000000..6d9841f82
--- /dev/null
+++ b/packages/ui/src/components/AspectRatio/index.tsx
@@ -0,0 +1,3 @@
+export { AspectRatio } from './AspectRatio';
+
+export type { AspectRatioProps } from './AspectRatio';
diff --git a/packages/ui/src/components/Asset/Asset.stories.tsx b/packages/ui/src/components/Asset/Asset.stories.tsx
new file mode 100644
index 000000000..3ff73d257
--- /dev/null
+++ b/packages/ui/src/components/Asset/Asset.stories.tsx
@@ -0,0 +1,143 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconCurrencyEthereum } from '@tabler/icons-react';
+
+import { HStack, VStack } from '../Box';
+import { Icon } from '../Icon/Icon';
+
+import { Asset } from './Asset';
+import { MOCK_ASSETS } from './__mocks__/assets';
+
+const meta: Meta = {
+ title: 'Web3/Asset',
+ component: Asset,
+};
+
+export default meta;
+type Story = StoryObj;
+
+const DEFAULT_ARGS = {
+ asset: MOCK_ASSETS.eth,
+ amount: '1000000000',
+};
+
+export const Usage: Story = {
+ render: (args) => (
+
+
+
+
+
+
+
+
+ ),
+};
+
+export const IconName: Story = {
+ name: 'Icon + Name',
+ render: (args) => (
+
+
+
+
+ ),
+};
+
+export const CustomIcon: Story = {
+ render: (args) => (
+
+ } />
+
+
+ ),
+};
+
+export const NoIcon: Story = {
+ render: (args) => (
+
+
+
+
+ ),
+};
+
+export const AmountSymbol: Story = {
+ name: 'Amount + Symbol',
+ render: (args) => (
+
+
+
+
+ ),
+};
+
+const AMOUNT_ARGS = {
+ asset: MOCK_ASSETS.eth,
+ amount: '1000000001',
+ precision: 9,
+};
+export const AmountExamples: Story = {
+ render: (args) => (
+
+
+
+
+
+
+
+
+
+
+ ),
+};
+
+export const Sizes: Story = {
+ render: (args) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Asset/Asset.tsx b/packages/ui/src/components/Asset/Asset.tsx
new file mode 100644
index 000000000..be89b586b
--- /dev/null
+++ b/packages/ui/src/components/Asset/Asset.tsx
@@ -0,0 +1,189 @@
+import { bn } from '@fuel-ts/math';
+import type { BNInput } from '@fuel-ts/math';
+import { IconArrowDown, IconArrowUp } from '@tabler/icons-react';
+import type { ReactNode } from 'react';
+
+import { useStrictedChildren } from '../../hooks/useStrictedChildren';
+import { createComponent, withNamespace } from '../../utils/component';
+import { cx } from '../../utils/css';
+import type { PropsOf } from '../../utils/types';
+import { Badge } from '../Badge/Badge';
+import { Box, HStack } from '../Box';
+import type { HStackProps } from '../Box';
+import type { TextProps } from '../Text/Text';
+import { Text } from '../Text/Text';
+
+import { AssetProvider, useAssetContext } from './useAssetContext';
+
+export type AssetIconSize = 'xs' | 'sm' | 'md' | 'lg';
+
+export type AssetObj = {
+ name: string;
+ symbol: string;
+ imageUrl?: string;
+};
+
+export type AssetProps = HStackProps & {
+ asset: AssetObj;
+ amount?: BNInput;
+ units?: number;
+ precision?: number;
+ iconSize?: AssetIconSize | number;
+ negative?: boolean;
+ hideIcon?: boolean;
+};
+
+export type AssetIconProps = Omit<
+ Omit, 'width' | 'height' | 'color'> & { icon?: ReactNode },
+ 'children'
+>;
+
+export type AssetNameProps = Omit, 'children'>;
+export type AssetSymbolProps = PropsOf<'span'>;
+export type AssetAmountProps = Omit<
+ TextProps,
+ 'leftIcon' | 'rightIcon' | 'iconColor'
+>;
+
+const CHILD_ITEMS = [
+ 'AssetIcon',
+ 'AssetSymbol',
+ 'AssetName',
+ 'AssetAmount',
+ 'HStack',
+ 'VStack',
+];
+
+export const AssetRoot = createComponent({
+ id: 'AssetRoot',
+ render: (
+ _,
+ {
+ asset,
+ amount,
+ units,
+ precision,
+ iconSize,
+ negative,
+ hideIcon,
+ children,
+ gap = '4',
+ ...props
+ },
+ ) => {
+ {
+ const newChildren = useStrictedChildren('Asset', CHILD_ITEMS, children);
+ const amountStr = bn(amount).format({ units, precision });
+ const isNegative = negative || bn(amount).lt(0);
+ return (
+
+
+ {newChildren}
+
+
+ );
+ }
+ },
+});
+
+const SIZES_MAP = {
+ xs: 20,
+ sm: 24,
+ md: 32,
+ lg: 40,
+};
+
+export const AssetIcon = createComponent({
+ id: 'AssetIcon',
+ render: (_, { icon, ...props }) => {
+ const { asset, iconSize = 'md' } = useAssetContext();
+ const size = typeof iconSize === 'string' ? SIZES_MAP[iconSize] : iconSize;
+
+ if (icon) {
+ return (
+
+ {icon}
+
+ );
+ }
+
+ if (!asset.imageUrl) {
+ return (
+
+ {asset.symbol.slice(0, 2).toUpperCase()}
+
+ );
+ }
+
+ return (
+
+ );
+ },
+});
+
+export const AssetSymbol = createComponent({
+ id: 'AssetSymbol',
+ render: (_, props) => {
+ const assetProps = useAssetContext();
+ return {assetProps.asset.symbol};
+ },
+});
+
+export const AssetName = createComponent({
+ id: 'AssetName',
+ render: (_, props) => {
+ const assetProps = useAssetContext();
+ return {assetProps.asset.name};
+ },
+});
+
+export const AssetAmount = createComponent({
+ id: 'AssetAmount',
+ render: (_, props) => {
+ const { hideIcon, amountStr, isNegative } = useAssetContext();
+ return (
+
+ {amountStr}
+
+ );
+ },
+});
+
+export const Asset = withNamespace(AssetRoot, {
+ Amount: AssetAmount,
+ Icon: AssetIcon,
+ Name: AssetName,
+ Symbol: AssetSymbol,
+});
diff --git a/packages/ui/src/components/Asset/__mocks__/assets.ts b/packages/ui/src/components/Asset/__mocks__/assets.ts
new file mode 100644
index 000000000..337a5cef8
--- /dev/null
+++ b/packages/ui/src/components/Asset/__mocks__/assets.ts
@@ -0,0 +1,7 @@
+export const MOCK_ASSETS = {
+ eth: {
+ symbol: 'ETH',
+ name: 'Ethereum',
+ imageUrl: '/assets/eth.svg',
+ },
+};
diff --git a/packages/ui/src/components/Asset/index.tsx b/packages/ui/src/components/Asset/index.tsx
new file mode 100644
index 000000000..2879a4076
--- /dev/null
+++ b/packages/ui/src/components/Asset/index.tsx
@@ -0,0 +1,22 @@
+'use client';
+
+export {
+ Asset,
+ AssetAmount,
+ AssetIcon,
+ AssetName,
+ AssetRoot,
+ AssetSymbol,
+} from './Asset';
+
+export type {
+ AssetAmountProps,
+ AssetIconProps,
+ AssetIconSize,
+ AssetNameProps,
+ AssetObj,
+ AssetProps,
+ AssetSymbolProps,
+} from './Asset';
+
+export { AssetProvider, useAssetContext } from './useAssetContext';
diff --git a/packages/ui/src/components/Asset/useAssetContext.tsx b/packages/ui/src/components/Asset/useAssetContext.tsx
new file mode 100644
index 000000000..76fd7ed53
--- /dev/null
+++ b/packages/ui/src/components/Asset/useAssetContext.tsx
@@ -0,0 +1,17 @@
+'use client';
+
+import { createContext, useContext } from 'react';
+
+import type { AssetProps } from './Asset';
+
+type ContextProps = AssetProps & {
+ amountStr: string;
+ isNegative: boolean;
+};
+
+const ctx = createContext({} as ContextProps);
+export function useAssetContext() {
+ return useContext(ctx);
+}
+
+export const AssetProvider = ctx.Provider;
diff --git a/packages/ui/src/components/Avatar/Avatar.stories.tsx b/packages/ui/src/components/Avatar/Avatar.stories.tsx
new file mode 100644
index 000000000..3275edb32
--- /dev/null
+++ b/packages/ui/src/components/Avatar/Avatar.stories.tsx
@@ -0,0 +1,25 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { HStack } from '../Box';
+
+import { Avatar } from './Avatar';
+
+const meta: Meta = {
+ title: 'UI/Avatar',
+ component: Avatar,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Avatar/Avatar.tsx b/packages/ui/src/components/Avatar/Avatar.tsx
new file mode 100644
index 000000000..eef8a5ca7
--- /dev/null
+++ b/packages/ui/src/components/Avatar/Avatar.tsx
@@ -0,0 +1,14 @@
+import { Avatar as RadixAvatar } from '@radix-ui/themes';
+
+import { createComponent } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type AvatarProps = PropsOf;
+
+export const Avatar = createComponent({
+ id: 'Avatar',
+ baseElement: RadixAvatar,
+ defaultProps: {
+ radius: 'full',
+ },
+});
diff --git a/packages/ui/src/components/Avatar/index.tsx b/packages/ui/src/components/Avatar/index.tsx
new file mode 100644
index 000000000..d3eb3b2b9
--- /dev/null
+++ b/packages/ui/src/components/Avatar/index.tsx
@@ -0,0 +1,3 @@
+export { Avatar } from './Avatar';
+
+export type { AvatarProps } from './Avatar';
diff --git a/packages/ui/src/components/Badge/Badge.stories.tsx b/packages/ui/src/components/Badge/Badge.stories.tsx
new file mode 100644
index 000000000..1b951d25b
--- /dev/null
+++ b/packages/ui/src/components/Badge/Badge.stories.tsx
@@ -0,0 +1,32 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { HStack } from '../Box';
+import { ButtonClose } from '../ButtonClose/ButtonClose';
+
+import { Badge } from './Badge';
+
+const meta: Meta = {
+ title: 'UI/Badge',
+ component: Badge,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+ In progress
+ In review
+ Complete
+
+ ),
+};
+
+export const WithClose: Story = {
+ render: () => (
+
+ Selected
+
+ ),
+};
diff --git a/packages/ui/src/components/Badge/Badge.tsx b/packages/ui/src/components/Badge/Badge.tsx
new file mode 100644
index 000000000..68f76b1b1
--- /dev/null
+++ b/packages/ui/src/components/Badge/Badge.tsx
@@ -0,0 +1,11 @@
+import { Badge as RadixBadge } from '@radix-ui/themes';
+
+import { createComponent } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type BadgeProps = PropsOf;
+
+export const Badge = createComponent({
+ id: 'Badge',
+ baseElement: RadixBadge,
+});
diff --git a/packages/ui/src/components/Badge/index.tsx b/packages/ui/src/components/Badge/index.tsx
new file mode 100644
index 000000000..8451ab604
--- /dev/null
+++ b/packages/ui/src/components/Badge/index.tsx
@@ -0,0 +1,3 @@
+export { Badge } from './Badge';
+
+export type { BadgeProps } from './Badge';
diff --git a/packages/ui/src/components/Box/Box.stories.tsx b/packages/ui/src/components/Box/Box.stories.tsx
new file mode 100644
index 000000000..a7d56439e
--- /dev/null
+++ b/packages/ui/src/components/Box/Box.stories.tsx
@@ -0,0 +1,43 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Box } from './Box';
+import { Flex } from './Flex';
+import type { FlexProps } from './Flex';
+
+const meta: Meta = {
+ title: 'Layout/Box',
+ component: Box,
+};
+
+export default meta;
+type Story = StoryObj;
+
+const DecorativeBox = (props: FlexProps) => {
+ return (
+
+ );
+};
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+ ),
+};
+
+export const AsChild: Story = {
+ name: 'AsChild',
+ render: () => (
+
+ I'm a span
+
+ ),
+};
+
+export const Polymorphic: Story = {
+ render: () => I'm a span,
+};
diff --git a/packages/ui/src/components/Box/Box.tsx b/packages/ui/src/components/Box/Box.tsx
new file mode 100644
index 000000000..4632bd6f3
--- /dev/null
+++ b/packages/ui/src/components/Box/Box.tsx
@@ -0,0 +1,11 @@
+import { Box as RadixBox } from '@radix-ui/themes';
+
+import { createPolymorphicComponent } from '../../utils/component';
+import type { PropsOf, WithAsProps } from '../../utils/types';
+
+export type BoxProps = WithAsProps & PropsOf;
+
+export const Box = createPolymorphicComponent({
+ id: 'Box',
+ baseElement: RadixBox,
+});
diff --git a/packages/ui/src/components/Box/Container.stories.tsx b/packages/ui/src/components/Box/Container.stories.tsx
new file mode 100644
index 000000000..b69fa567a
--- /dev/null
+++ b/packages/ui/src/components/Box/Container.stories.tsx
@@ -0,0 +1,36 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Box } from './Box';
+import type { ContainerProps } from './Container';
+import { Container } from './Container';
+
+const meta: Meta = {
+ title: 'Layout/Container',
+ component: Container,
+ parameters: {
+ layout: 'fullscreen',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+const DecorativeBox = (props: ContainerProps) => {
+ return (
+
+ );
+};
+
+export const UsageContainer: Story = {
+ name: 'Container',
+ render: () => (
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Box/Container.tsx b/packages/ui/src/components/Box/Container.tsx
new file mode 100644
index 000000000..6905e837b
--- /dev/null
+++ b/packages/ui/src/components/Box/Container.tsx
@@ -0,0 +1,14 @@
+import { Container as RadixContainer } from '@radix-ui/themes';
+
+import { createPolymorphicComponent } from '../../utils/component';
+import type { PropsOf, WithAsProps } from '../../utils/types';
+
+export type ContainerProps = WithAsProps & PropsOf;
+
+export const Container = createPolymorphicComponent<
+ ContainerProps,
+ typeof RadixContainer
+>({
+ id: 'Container',
+ baseElement: RadixContainer,
+});
diff --git a/packages/ui/src/components/Box/Flex.stories.tsx b/packages/ui/src/components/Box/Flex.stories.tsx
new file mode 100644
index 000000000..a15d10601
--- /dev/null
+++ b/packages/ui/src/components/Box/Flex.stories.tsx
@@ -0,0 +1,44 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import type { FlexProps } from './';
+import { Box, Flex } from './';
+
+const meta: Meta = {
+ title: 'Layout/Flex',
+ component: Flex,
+};
+
+export default meta;
+type Story = StoryObj;
+
+const DecorativeBox = (props: FlexProps) => {
+ return (
+
+ );
+};
+
+export const Usage: Story = {
+ name: 'Flex',
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Box/Flex.tsx b/packages/ui/src/components/Box/Flex.tsx
new file mode 100644
index 000000000..3c0b3dd88
--- /dev/null
+++ b/packages/ui/src/components/Box/Flex.tsx
@@ -0,0 +1,11 @@
+import { Flex as RadixFlex } from '@radix-ui/themes';
+
+import { createPolymorphicComponent } from '../../utils/component';
+import type { PropsOf, WithAsProps } from '../../utils/types';
+
+export type FlexProps = WithAsProps & PropsOf;
+
+export const Flex = createPolymorphicComponent({
+ id: 'Flex',
+ baseElement: RadixFlex,
+});
diff --git a/packages/ui/src/components/Box/Grid.stories.tsx b/packages/ui/src/components/Box/Grid.stories.tsx
new file mode 100644
index 000000000..7aa3db491
--- /dev/null
+++ b/packages/ui/src/components/Box/Grid.stories.tsx
@@ -0,0 +1,47 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import type { GridProps } from './';
+import { Box, Grid } from './';
+
+const meta: Meta = {
+ title: 'Layout/Grid',
+ component: Grid,
+};
+
+export default meta;
+type Story = StoryObj;
+
+const DecorativeBox = (props: GridProps) => {
+ return (
+
+ );
+};
+
+export const Usage: Story = {
+ name: 'Grid',
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Box/Grid.tsx b/packages/ui/src/components/Box/Grid.tsx
new file mode 100644
index 000000000..9d1579e77
--- /dev/null
+++ b/packages/ui/src/components/Box/Grid.tsx
@@ -0,0 +1,11 @@
+import { Grid as RadixGrid } from '@radix-ui/themes';
+
+import { createPolymorphicComponent } from '../../utils/component';
+import type { PropsOf, WithAsProps } from '../../utils/types';
+
+export type GridProps = WithAsProps & PropsOf;
+
+export const Grid = createPolymorphicComponent({
+ id: 'Grid',
+ baseElement: RadixGrid,
+});
diff --git a/packages/ui/src/components/Box/HStack.stories.tsx b/packages/ui/src/components/Box/HStack.stories.tsx
new file mode 100644
index 000000000..c4feeeb91
--- /dev/null
+++ b/packages/ui/src/components/Box/HStack.stories.tsx
@@ -0,0 +1,44 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import type { HStackProps } from './';
+import { Box, HStack } from './';
+
+const meta: Meta = {
+ title: 'Layout/HStack',
+ component: HStack,
+};
+
+export default meta;
+type Story = StoryObj;
+
+const DecorativeBox = (props: HStackProps) => {
+ return (
+
+ );
+};
+
+export const Usage: Story = {
+ name: 'HStack',
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Box/HStack.tsx b/packages/ui/src/components/Box/HStack.tsx
new file mode 100644
index 000000000..792677848
--- /dev/null
+++ b/packages/ui/src/components/Box/HStack.tsx
@@ -0,0 +1,17 @@
+import { Flex as RadixFlex } from '@radix-ui/themes';
+
+import { createPolymorphicComponent } from '../../utils/component';
+
+import type { FlexProps } from './Flex';
+
+export type HStackProps = Omit;
+
+export const HStack = createPolymorphicComponent(
+ {
+ id: 'HStack',
+ baseElement: RadixFlex,
+ defaultProps: {
+ gap: '4',
+ } as FlexProps,
+ },
+);
diff --git a/packages/ui/src/components/Box/VStack.stories.tsx b/packages/ui/src/components/Box/VStack.stories.tsx
new file mode 100644
index 000000000..b3d69c3fc
--- /dev/null
+++ b/packages/ui/src/components/Box/VStack.stories.tsx
@@ -0,0 +1,44 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import type { VStackProps } from './';
+import { Box, VStack } from './';
+
+const meta: Meta = {
+ title: 'Layout/VStack',
+ component: VStack,
+};
+
+export default meta;
+type Story = StoryObj;
+
+const DecorativeBox = (props: VStackProps) => {
+ return (
+
+ );
+};
+
+export const Usage: Story = {
+ name: 'VStack',
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Box/VStack.tsx b/packages/ui/src/components/Box/VStack.tsx
new file mode 100644
index 000000000..54d841549
--- /dev/null
+++ b/packages/ui/src/components/Box/VStack.tsx
@@ -0,0 +1,18 @@
+import { Flex as RadixFlex } from '@radix-ui/themes';
+
+import { createPolymorphicComponent } from '../../utils/component';
+
+import type { FlexProps } from './Flex';
+
+export type VStackProps = Omit;
+
+export const VStack = createPolymorphicComponent(
+ {
+ id: 'VStack',
+ baseElement: RadixFlex,
+ defaultProps: {
+ direction: 'column',
+ gap: '4',
+ } as FlexProps,
+ },
+);
diff --git a/packages/ui/src/components/Box/index.tsx b/packages/ui/src/components/Box/index.tsx
new file mode 100644
index 000000000..d0dc5bdda
--- /dev/null
+++ b/packages/ui/src/components/Box/index.tsx
@@ -0,0 +1,17 @@
+export { Box } from './Box';
+export type { BoxProps } from './Box';
+
+export { Flex } from './Flex';
+export type { FlexProps } from './Flex';
+
+export { Grid } from './Grid';
+export type { GridProps } from './Grid';
+
+export { Container } from './Container';
+export type { ContainerProps } from './Container';
+
+export { VStack } from './VStack';
+export type { VStackProps } from './VStack';
+
+export { HStack } from './HStack';
+export type { HStackProps } from './HStack';
diff --git a/packages/ui/src/components/Breadcrumb/Breadcrumb.stories.tsx b/packages/ui/src/components/Breadcrumb/Breadcrumb.stories.tsx
new file mode 100644
index 000000000..d4e664d2a
--- /dev/null
+++ b/packages/ui/src/components/Breadcrumb/Breadcrumb.stories.tsx
@@ -0,0 +1,26 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconHome } from '@tabler/icons-react';
+
+import { Icon } from '../Icon/Icon';
+
+import { Breadcrumb } from './Breadcrumb';
+
+const meta: Meta = {
+ title: 'UI/Breadcrumb',
+ component: Breadcrumb,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: (args) => (
+
+
+
+
+ Components
+ Dropdown
+
+ ),
+};
diff --git a/packages/ui/src/components/Breadcrumb/Breadcrumb.tsx b/packages/ui/src/components/Breadcrumb/Breadcrumb.tsx
new file mode 100644
index 000000000..e5d3424c0
--- /dev/null
+++ b/packages/ui/src/components/Breadcrumb/Breadcrumb.tsx
@@ -0,0 +1,63 @@
+import { IconChevronRight } from '@tabler/icons-react';
+import { Children } from 'react';
+
+import { createComponent, withNamespace } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+import { HStack } from '../Box';
+import type { HStackProps } from '../Box';
+import { Icon } from '../Icon/Icon';
+import { Link } from '../Link/Link';
+import type { LinkProps } from '../Link/Link';
+
+export type BreadcrumbProps = PropsOf<'ul'> & Omit;
+export type BreadcrumbItemProps = PropsOf<'li'>;
+export type BreadcrumbLinkProps = LinkProps;
+
+export const BreadcrumbRoot = createComponent({
+ id: 'Breadcrumb',
+ baseElement: 'ul',
+ render: (Comp, { children, ...props }) => {
+ const newChildren = Children.toArray(children).flatMap((child, index) => {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const id = (child as any)?.type?.id;
+ if (id !== 'BreadcrumbItem' && id !== 'BreadcrumbLink') {
+ throw new Error(
+ 'Breadcrumb only accepts Breadcrumb.Item or Breadcrumb.Link as children',
+ );
+ }
+
+ const count = Children.count(children);
+ if (index < count - 1) {
+ return [
+ child,
+
+
+ ,
+ ];
+ }
+ return child;
+ });
+ return (
+
+ {newChildren}
+
+ );
+ },
+});
+
+export const BreadcrumbItem = createComponent({
+ id: 'BreadcrumbItem',
+ baseElement: 'li',
+});
+
+export const BreadcrumbLink = createComponent(
+ {
+ id: 'BreadcrumbLink',
+ baseElement: Link,
+ },
+);
+
+export const Breadcrumb = withNamespace(BreadcrumbRoot, {
+ Item: BreadcrumbItem,
+ Link: BreadcrumbLink,
+});
diff --git a/packages/ui/src/components/Breadcrumb/index.tsx b/packages/ui/src/components/Breadcrumb/index.tsx
new file mode 100644
index 000000000..49cd69c78
--- /dev/null
+++ b/packages/ui/src/components/Breadcrumb/index.tsx
@@ -0,0 +1,12 @@
+export {
+ Breadcrumb,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbRoot,
+} from './Breadcrumb';
+
+export type {
+ BreadcrumbItemProps,
+ BreadcrumbLinkProps,
+ BreadcrumbProps,
+} from './Breadcrumb';
diff --git a/packages/ui/src/components/Button/Button.stories.tsx b/packages/ui/src/components/Button/Button.stories.tsx
new file mode 100644
index 000000000..255b27e2b
--- /dev/null
+++ b/packages/ui/src/components/Button/Button.stories.tsx
@@ -0,0 +1,76 @@
+import { action } from '@storybook/addon-actions';
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconBookmark } from '@tabler/icons-react';
+
+import { HStack } from '../Box';
+
+import { Button } from './Button';
+
+const meta: Meta = {
+ title: 'UI/Button',
+ component: Button,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => ,
+};
+
+export const Variant: Story = {
+ render: () => (
+
+
+
+
+
+
+ ),
+};
+
+export const Colors: Story = {
+ render: () => (
+
+
+
+
+
+
+ ),
+};
+
+export const Sizes: Story = {
+ render: () => (
+
+
+
+
+
+
+ ),
+};
+
+export const Loading: Story = {
+ render: () => ,
+};
+
+export const onClick: Story = {
+ render: () => ,
+};
diff --git a/packages/ui/src/components/Button/Button.tsx b/packages/ui/src/components/Button/Button.tsx
new file mode 100644
index 000000000..fdd5faf10
--- /dev/null
+++ b/packages/ui/src/components/Button/Button.tsx
@@ -0,0 +1,24 @@
+import { Button as RadixButton } from '@radix-ui/themes';
+
+import type { WithIconProps } from '../../hooks/useIconProps';
+import { useIconProps } from '../../hooks/useIconProps';
+import type { WithVariants } from '../../hooks/useVariants';
+import { useVariants } from '../../hooks/useVariants';
+import { createComponent } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+type RadixButtonProps = PropsOf;
+export type ButtonProps = WithVariants & WithIconProps;
+
+export const Button = createComponent({
+ id: 'Button',
+ render: (_, props) => {
+ const itemProps = useIconProps(props);
+ const variantProps = useVariants(props);
+ return ;
+ },
+ defaultProps: {
+ size: '2',
+ variant: 'solid',
+ },
+});
diff --git a/packages/ui/src/components/Button/index.tsx b/packages/ui/src/components/Button/index.tsx
new file mode 100644
index 000000000..5daf68915
--- /dev/null
+++ b/packages/ui/src/components/Button/index.tsx
@@ -0,0 +1,3 @@
+export { Button } from './Button';
+
+export type { ButtonProps } from './Button';
diff --git a/packages/ui/src/components/ButtonClose/ButtonClose.tsx b/packages/ui/src/components/ButtonClose/ButtonClose.tsx
new file mode 100644
index 000000000..b3ae149d1
--- /dev/null
+++ b/packages/ui/src/components/ButtonClose/ButtonClose.tsx
@@ -0,0 +1,19 @@
+import { IconX } from '@tabler/icons-react';
+
+import { createComponent } from '../../utils/component';
+import type { IconButtonProps } from '../IconButton/IconButton';
+import { IconButton } from '../IconButton/IconButton';
+
+export type ButtonCloseProps = Partial;
+
+export const ButtonClose = createComponent(
+ {
+ id: 'ButtonClose',
+ baseElement: IconButton,
+ defaultProps: {
+ 'aria-label': 'Close',
+ icon: IconX,
+ variant: 'link',
+ },
+ },
+);
diff --git a/packages/ui/src/components/ButtonClose/index.tsx b/packages/ui/src/components/ButtonClose/index.tsx
new file mode 100644
index 000000000..9232ba563
--- /dev/null
+++ b/packages/ui/src/components/ButtonClose/index.tsx
@@ -0,0 +1,3 @@
+export { ButtonClose } from './ButtonClose';
+
+export type { ButtonCloseProps } from './ButtonClose';
diff --git a/packages/ui/src/components/ButtonGroup/ButtonGroup.stories.tsx b/packages/ui/src/components/ButtonGroup/ButtonGroup.stories.tsx
new file mode 100644
index 000000000..5a1b7dff1
--- /dev/null
+++ b/packages/ui/src/components/ButtonGroup/ButtonGroup.stories.tsx
@@ -0,0 +1,24 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconCalendar } from '@tabler/icons-react';
+
+import { Button } from '../Button/Button';
+
+import { ButtonGroup } from './ButtonGroup';
+
+const meta: Meta = {
+ title: 'UI/ButtonGroup',
+ component: ButtonGroup,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: (args) => (
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/ButtonGroup/ButtonGroup.tsx b/packages/ui/src/components/ButtonGroup/ButtonGroup.tsx
new file mode 100644
index 000000000..1675be52a
--- /dev/null
+++ b/packages/ui/src/components/ButtonGroup/ButtonGroup.tsx
@@ -0,0 +1,52 @@
+import type { ReactElement } from 'react';
+import { Children, cloneElement } from 'react';
+import { mergeProps } from 'react-aria';
+
+import { createComponent } from '../../utils/component';
+import { pick } from '../../utils/helpers';
+import type { PropsOf } from '../../utils/types';
+import type { ButtonProps } from '../Button/Button';
+import { Focus } from '../Focus/Focus';
+
+import { styles } from './styles';
+
+type PropsToOmit =
+ | 'className'
+ | 'onClick'
+ | 'iconSize'
+ | 'leftIcon'
+ | 'leftIconAriaLabel'
+ | 'rightIcon'
+ | 'rightIconAriaLabel'
+ | 'isLoading'
+ | 'loadingText'
+ | 'isDisabled'
+ | 'justIcon'
+ | 'isLink'
+ | 'onClick';
+
+export type ButtonGroupProps = Omit & PropsOf<'div'>;
+
+const BUTTON_BASE_PROPS = ['size', 'color', 'variant', 'isDisabled', 'intent'];
+
+export const ButtonGroup = createComponent({
+ id: 'ButtonGroup',
+ baseElement: 'div',
+ className: () => styles().root(),
+ render: (Comp, { children, ...props }) => {
+ const buttons = (Children.toArray(children) as ReactElement[]).map(
+ (child: ReactElement) =>
+ cloneElement(
+ child,
+ mergeProps(child.props, pick(BUTTON_BASE_PROPS, props), {
+ className: styles().button(),
+ }),
+ ),
+ );
+ return (
+
+ {buttons}
+
+ );
+ },
+});
diff --git a/packages/ui/src/components/ButtonGroup/index.tsx b/packages/ui/src/components/ButtonGroup/index.tsx
new file mode 100644
index 000000000..cb16e523f
--- /dev/null
+++ b/packages/ui/src/components/ButtonGroup/index.tsx
@@ -0,0 +1,5 @@
+'use client';
+
+export { ButtonGroup } from './ButtonGroup';
+
+export type { ButtonGroupProps } from './ButtonGroup';
diff --git a/packages/ui/src/components/ButtonGroup/styles.ts b/packages/ui/src/components/ButtonGroup/styles.ts
new file mode 100644
index 000000000..0a06fe8c5
--- /dev/null
+++ b/packages/ui/src/components/ButtonGroup/styles.ts
@@ -0,0 +1,14 @@
+import { tv } from 'tailwind-variants';
+
+export const styles = tv({
+ slots: {
+ root: ['flex items-center'],
+ button: [
+ 'not-first:ml-px',
+ 'first-type:rounded-tr-none first-type:rounded-br-none',
+ 'last-type:rounded-tl-none last-type:rounded-bl-none',
+ 'not-first-last:rounded-none',
+ 'focus-visible:z-10 focus-visible:relative',
+ ],
+ },
+});
diff --git a/packages/ui/src/components/Card/Card.stories.tsx b/packages/ui/src/components/Card/Card.stories.tsx
new file mode 100644
index 000000000..fa64f3651
--- /dev/null
+++ b/packages/ui/src/components/Card/Card.stories.tsx
@@ -0,0 +1,67 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconCalendar } from '@tabler/icons-react';
+
+import { Button } from '../Button/Button';
+import { Icon } from '../Icon/Icon';
+
+import { Card } from './Card';
+
+const meta: Meta = {
+ title: 'UI/Card',
+ component: Card,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+ Hello world from Fuel
+
+ ),
+};
+
+export const FullVersion: Story = {
+ render: () => (
+
+
+
+
+ Calendar
+
+ This is a subtitle
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut vulputate
+ rutrum est non sollicitudin. Donec tortor ligula, bibendum ac luctus ac,
+ efficitur a sem.
+
+
+
+
+
+
+ ),
+};
+
+export const AsChild: Story = {
+ name: 'AsChild',
+ render: () => (
+
+
+ Hello world from Fuel
+
+
+ ),
+};
+
+export const Polymorphic: Story = {
+ render: () => (
+
+ Hello world from Fuel
+
+ ),
+};
diff --git a/packages/ui/src/components/Card/Card.tsx b/packages/ui/src/components/Card/Card.tsx
new file mode 100644
index 000000000..0fa2b3c5c
--- /dev/null
+++ b/packages/ui/src/components/Card/Card.tsx
@@ -0,0 +1,85 @@
+import {
+ createComponent,
+ createPolymorphicComponent,
+ withNamespace,
+} from '../../utils/component';
+import type { BoxProps } from '../Box';
+import { Box } from '../Box';
+import type { HeadingProps } from '../Heading';
+import { Heading } from '../Heading';
+import { Text } from '../Text';
+import type { TextProps } from '../Text';
+
+import { styles } from './styles';
+
+export type CardProps = BoxProps;
+export type CardHeaderProps = BoxProps;
+export type CardTitleProps = HeadingProps;
+export type CardBodyProps = BoxProps;
+export type CardDescriptionProps = TextProps;
+export type CardFooterProps = BoxProps;
+
+export const CardRoot = createPolymorphicComponent({
+ id: 'Card',
+ baseElement: Box,
+ className: () => styles().root(),
+ defaultProps: {
+ as: 'article',
+ },
+});
+
+export const CardHeader = createPolymorphicComponent<
+ CardHeaderProps,
+ typeof Box
+>({
+ id: 'CardHeader',
+ baseElement: Box,
+ className: () => styles().header(),
+ defaultProps: {
+ as: 'header',
+ },
+});
+
+export const CardTitle = createComponent({
+ id: 'CardTitle',
+ baseElement: Heading,
+ className: () => styles().title(),
+ defaultProps: {
+ size: '6',
+ },
+});
+
+export const CardBody = createPolymorphicComponent({
+ id: 'CardBody',
+ baseElement: Box,
+ className: () => styles().body(),
+});
+
+export const CardDescription = createComponent<
+ CardDescriptionProps,
+ typeof Text
+>({
+ id: 'CardDescription',
+ baseElement: Text,
+ className: () => styles().description(),
+});
+
+export const CardFooter = createPolymorphicComponent<
+ CardFooterProps,
+ typeof Box
+>({
+ id: 'CardFooter',
+ baseElement: Box,
+ className: () => styles().footer(),
+ defaultProps: {
+ as: 'footer',
+ },
+});
+
+export const Card = withNamespace(CardRoot, {
+ Header: CardHeader,
+ Title: CardTitle,
+ Body: CardBody,
+ Description: CardDescription,
+ Footer: CardFooter,
+});
diff --git a/packages/ui/src/components/Card/index.tsx b/packages/ui/src/components/Card/index.tsx
new file mode 100644
index 000000000..d3116c391
--- /dev/null
+++ b/packages/ui/src/components/Card/index.tsx
@@ -0,0 +1,18 @@
+export {
+ Card,
+ CardBody,
+ CardDescription,
+ CardFooter,
+ CardHeader,
+ CardRoot,
+ CardTitle,
+} from './Card';
+
+export type {
+ CardBodyProps,
+ CardDescriptionProps,
+ CardFooterProps,
+ CardHeaderProps,
+ CardProps,
+ CardTitleProps,
+} from './Card';
diff --git a/packages/ui/src/components/Card/styles.ts b/packages/ui/src/components/Card/styles.ts
new file mode 100644
index 000000000..4da315783
--- /dev/null
+++ b/packages/ui/src/components/Card/styles.ts
@@ -0,0 +1,17 @@
+import { tv } from 'tailwind-variants';
+
+export const styles = tv({
+ slots: {
+ root: [
+ 'flex flex-col py-4 gap-4 rounded-md bg-card-bg text-color overflow-clip',
+ 'border border-solid border-card-border',
+ ],
+ header: ['flex flex-col gap-1.5 px-4 text-heading'],
+ title: [
+ 'm-0 font-semibold leading-none tracking-tight text-xl flex items-center gap-2',
+ ],
+ description: ['m-0 text-sm text-secondary'],
+ body: ['px-4 text-base'],
+ footer: ['px-4 pt-0 self-end flex gap-2'],
+ },
+});
diff --git a/packages/ui/src/components/CardList/CardList.stories.tsx b/packages/ui/src/components/CardList/CardList.stories.tsx
new file mode 100644
index 000000000..562fc33b6
--- /dev/null
+++ b/packages/ui/src/components/CardList/CardList.stories.tsx
@@ -0,0 +1,51 @@
+import { action } from '@storybook/addon-actions';
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconArrowRight } from '@tabler/icons-react';
+
+import { Avatar } from '../Avatar/Avatar';
+import { Heading } from '../Heading/Heading';
+import { IconButton } from '../IconButton/IconButton';
+
+import { CardList } from './CardList';
+
+const meta: Meta = {
+ title: 'UI/CardList',
+ component: CardList,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+ }
+ onClick={action('onClick')}
+ >
+
+ Colm Tuite
+
+
+
+ Colm Tuite
+
+
+ ),
+};
diff --git a/packages/ui/src/components/CardList/CardList.tsx b/packages/ui/src/components/CardList/CardList.tsx
new file mode 100644
index 000000000..fd92a3858
--- /dev/null
+++ b/packages/ui/src/components/CardList/CardList.tsx
@@ -0,0 +1,75 @@
+import { createComponent, withNamespace } from '../../utils/component';
+import { HStack, VStack } from '../Box';
+import type { VStackProps } from '../Box';
+import { Card } from '../Card/Card';
+import type { CardProps } from '../Card/Card';
+import { Focus } from '../Focus/Focus';
+
+import { styles } from './styles';
+import { CardListContext, useCardListContext } from './useCardListContext';
+
+export type CardListProps = VStackProps &
+ Omit;
+
+export type CardListItemProps = CardProps & {
+ isActive?: boolean;
+ rightEl?: React.ReactNode;
+};
+
+export const CardListRoot = createComponent({
+ id: 'CardList',
+ baseElement: VStack,
+ render: (Comp, { children, gap = '3', isClickable, autoFocus, ...props }) => {
+ return (
+
+
+ {isClickable ? (
+
+ {children}
+
+ ) : (
+ children
+ )}
+
+
+ );
+ },
+});
+
+export const CardListItem = createComponent({
+ id: 'CardListItem',
+ baseElement: Card,
+ render: (
+ Comp,
+ {
+ className,
+ children,
+ isActive,
+ rightEl,
+ autoFocus: initAutoFocus,
+ ...props
+ },
+ ) => {
+ const ctx = useCardListContext();
+ const isClickable = Boolean(props.onClick || ctx.isClickable);
+ const classes = styles({ clickable: isClickable });
+ return (
+
+ {isActive && }
+
+ {children}
+
+ {rightEl}
+
+ );
+ },
+});
+
+export const CardList = withNamespace(CardListRoot, {
+ Item: CardListItem,
+});
diff --git a/packages/ui/src/components/CardList/index.tsx b/packages/ui/src/components/CardList/index.tsx
new file mode 100644
index 000000000..c80a2daf0
--- /dev/null
+++ b/packages/ui/src/components/CardList/index.tsx
@@ -0,0 +1,9 @@
+'use client';
+
+export { CardList, CardListItem, CardListRoot } from './CardList';
+
+export type { CardListItemProps, CardListProps } from './CardList';
+
+export { CardListProvider, useCardListContext } from './useCardListContext';
+
+export type { CardListContext } from './useCardListContext';
diff --git a/packages/ui/src/components/CardList/styles.ts b/packages/ui/src/components/CardList/styles.ts
new file mode 100644
index 000000000..79faf626e
--- /dev/null
+++ b/packages/ui/src/components/CardList/styles.ts
@@ -0,0 +1,21 @@
+import { tv } from 'tailwind-variants';
+
+export const styles = tv({
+ slots: {
+ root: [
+ 'relative transition-colors duration-150 flex flex-row',
+ 'items-center px-5 focus:outline-none',
+ ],
+ activeMark: ['block absolute inset-0 bg-brand w-1 h-full'],
+ },
+ variants: {
+ clickable: {
+ true: {
+ root: 'cursor-pointer focus:ring-2 focus:ring-gray-3 hover:border-border-hover',
+ },
+ },
+ },
+ defaultVariants: {
+ clickable: false,
+ },
+});
diff --git a/packages/ui/src/components/CardList/useCardListContext.tsx b/packages/ui/src/components/CardList/useCardListContext.tsx
new file mode 100644
index 000000000..3c40bd56e
--- /dev/null
+++ b/packages/ui/src/components/CardList/useCardListContext.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import { createContext, useContext } from 'react';
+
+export type CardListContext = {
+ isClickable?: boolean;
+ autoFocus?: boolean;
+ isFocused?: boolean;
+};
+
+export const CardListContext = createContext(
+ {} as CardListContext,
+);
+
+export function useCardListContext() {
+ return useContext(CardListContext);
+}
+
+export const CardListProvider = CardListContext.Provider;
diff --git a/packages/ui/src/components/Checkbox/Checkbox.stories.tsx b/packages/ui/src/components/Checkbox/Checkbox.stories.tsx
new file mode 100644
index 000000000..2e92fb447
--- /dev/null
+++ b/packages/ui/src/components/Checkbox/Checkbox.stories.tsx
@@ -0,0 +1,24 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Text } from '../Text/Text';
+
+import { Checkbox } from './Checkbox';
+
+const meta: Meta = {
+ title: 'Form/Checkbox',
+ component: Checkbox,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Checkbox/Checkbox.tsx b/packages/ui/src/components/Checkbox/Checkbox.tsx
new file mode 100644
index 000000000..b49a5b510
--- /dev/null
+++ b/packages/ui/src/components/Checkbox/Checkbox.tsx
@@ -0,0 +1,11 @@
+import { Checkbox as RadixCheckbox } from '@radix-ui/themes';
+
+import { createComponent } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type CheckboxProps = PropsOf;
+
+export const Checkbox = createComponent({
+ id: 'Checkbox',
+ baseElement: RadixCheckbox,
+});
diff --git a/packages/ui/src/components/Checkbox/index.tsx b/packages/ui/src/components/Checkbox/index.tsx
new file mode 100644
index 000000000..482670bde
--- /dev/null
+++ b/packages/ui/src/components/Checkbox/index.tsx
@@ -0,0 +1,3 @@
+export { Checkbox } from './Checkbox';
+
+export type { CheckboxProps } from './Checkbox';
diff --git a/packages/ui/src/components/ContextMenu/ContextMenu.stories.tsx b/packages/ui/src/components/ContextMenu/ContextMenu.stories.tsx
new file mode 100644
index 000000000..36fa7c1f2
--- /dev/null
+++ b/packages/ui/src/components/ContextMenu/ContextMenu.stories.tsx
@@ -0,0 +1,60 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import type { FlexProps } from '../Box';
+import { Flex } from '../Box';
+
+import { ContextMenu } from './ContextMenu';
+
+const meta: Meta = {
+ title: 'Overlay/ContextMenu',
+ component: ContextMenu,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+const RightClickZone = (props: FlexProps) => {
+ return (
+
+ );
+};
+
+export const Usage: Story = {
+ render: () => (
+
+
+ Click here
+
+
+ Edit
+ Duplicate
+
+ Archive
+
+
+ More
+
+ Move to project…
+ Move to folder…
+
+ Advanced options…
+
+
+
+
+ Share
+ Add to favorites
+
+
+ Delete
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/ContextMenu/ContextMenu.tsx b/packages/ui/src/components/ContextMenu/ContextMenu.tsx
new file mode 100644
index 000000000..550c96096
--- /dev/null
+++ b/packages/ui/src/components/ContextMenu/ContextMenu.tsx
@@ -0,0 +1,127 @@
+import { ContextMenu as RC } from '@radix-ui/themes';
+
+import { createComponent, withNamespace } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type ContextMenuProps = PropsOf;
+export type ContextMenuTriggerProps = PropsOf;
+export type ContextMenuContentProps = PropsOf;
+export type ContextMenuLabelProps = PropsOf;
+export type ContextMenuItemProps = PropsOf;
+export type ContextMenuGroupProps = PropsOf;
+export type ContextMenuRadioGroupProps = PropsOf;
+export type ContextMenuCheckboxItemProps = PropsOf;
+export type ContextMenuSubProps = PropsOf;
+export type ContextMenuSubTriggerProps = PropsOf;
+export type ContextMenuSubContentProps = PropsOf;
+export type ContextMenuSeparatorProps = PropsOf;
+
+export const ContextMenuRoot = createComponent<
+ ContextMenuProps,
+ typeof RC.Root
+>({
+ id: 'ContextMenu',
+ baseElement: RC.Root,
+});
+
+export const ContextMenuTrigger = createComponent<
+ ContextMenuTriggerProps,
+ typeof RC.Trigger
+>({
+ id: 'ContextMenuTrigger',
+ baseElement: RC.Trigger,
+});
+
+export const ContextMenuContent = createComponent<
+ ContextMenuContentProps,
+ typeof RC.Content
+>({
+ id: 'ContextMenuContent',
+ baseElement: RC.Content,
+});
+
+export const ContextMenuLabel = createComponent<
+ ContextMenuLabelProps,
+ typeof RC.Label
+>({
+ id: 'ContextMenuLabel',
+ baseElement: RC.Label,
+});
+
+export const ContextMenuItem = createComponent<
+ ContextMenuItemProps,
+ typeof RC.Item
+>({
+ id: 'ContextMenuItem',
+ baseElement: RC.Item,
+});
+
+export const ContextMenuGroup = createComponent<
+ ContextMenuGroupProps,
+ typeof RC.Group
+>({
+ id: 'ContextMenuGroup',
+ baseElement: RC.Group,
+});
+
+export const ContextMenuRadioGroup = createComponent<
+ ContextMenuRadioGroupProps,
+ typeof RC.RadioGroup
+>({
+ id: 'ContextMenuRadioGroup',
+ baseElement: RC.RadioGroup,
+});
+
+export const ContextMenuCheckboxItem = createComponent<
+ ContextMenuCheckboxItemProps,
+ typeof RC.CheckboxItem
+>({
+ id: 'ContextMenuCheckboxItem',
+ baseElement: RC.CheckboxItem,
+});
+
+export const ContextMenuSub = createComponent<
+ ContextMenuSubProps,
+ typeof RC.Sub
+>({
+ id: 'ContextMenuSub',
+ baseElement: RC.Sub,
+});
+
+export const ContextMenuSubContent = createComponent<
+ ContextMenuSubContentProps,
+ typeof RC.SubContent
+>({
+ id: 'ContextMenuSubContent',
+ baseElement: RC.SubContent,
+});
+
+export const ContextMenuSubTrigger = createComponent<
+ ContextMenuSubTriggerProps,
+ typeof RC.SubTrigger
+>({
+ id: 'ContextMenuSubTrigger',
+ baseElement: RC.SubTrigger,
+});
+
+export const ContextMenuSeparator = createComponent<
+ ContextMenuSeparatorProps,
+ typeof RC.Separator
+>({
+ id: 'ContextSeparator',
+ baseElement: RC.Separator,
+});
+
+export const ContextMenu = withNamespace(ContextMenuRoot, {
+ Trigger: ContextMenuTrigger,
+ Content: ContextMenuContent,
+ Label: ContextMenuLabel,
+ Item: ContextMenuItem,
+ Group: ContextMenuGroup,
+ RadioGroup: ContextMenuRadioGroup,
+ CheckboxItem: ContextMenuCheckboxItem,
+ Sub: ContextMenuSub,
+ SubTrigger: ContextMenuSubTrigger,
+ SubContent: ContextMenuSubContent,
+ Separator: ContextMenuSeparator,
+});
diff --git a/packages/ui/src/components/ContextMenu/index.tsx b/packages/ui/src/components/ContextMenu/index.tsx
new file mode 100644
index 000000000..412f1acef
--- /dev/null
+++ b/packages/ui/src/components/ContextMenu/index.tsx
@@ -0,0 +1,30 @@
+export {
+ ContextMenu,
+ ContextMenuCheckboxItem,
+ ContextMenuContent,
+ ContextMenuGroup,
+ ContextMenuItem,
+ ContextMenuLabel,
+ ContextMenuRadioGroup,
+ ContextMenuRoot,
+ ContextMenuSeparator,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
+ ContextMenuTrigger,
+} from './ContextMenu';
+
+export type {
+ ContextMenuCheckboxItemProps,
+ ContextMenuContentProps,
+ ContextMenuGroupProps,
+ ContextMenuItemProps,
+ ContextMenuLabelProps,
+ ContextMenuProps,
+ ContextMenuRadioGroupProps,
+ ContextMenuSeparatorProps,
+ ContextMenuSubContentProps,
+ ContextMenuSubProps,
+ ContextMenuSubTriggerProps,
+ ContextMenuTriggerProps,
+} from './ContextMenu';
diff --git a/packages/ui/src/components/Copyable/Copyable.stories.tsx b/packages/ui/src/components/Copyable/Copyable.stories.tsx
new file mode 100644
index 000000000..b1f18a938
--- /dev/null
+++ b/packages/ui/src/components/Copyable/Copyable.stories.tsx
@@ -0,0 +1,35 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconClipboardCopy } from '@tabler/icons-react';
+
+import { Copyable } from './Copyable';
+
+const meta: Meta = {
+ title: 'Helpers/Copyable',
+ component: Copyable,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => Standard Icon,
+};
+
+export const CustomIcon: Story = {
+ render: () => (
+
+ Different Icon
+
+ ),
+};
+
+export const Polymorphic: Story = {
+ render: () => (
+
+ Standard Icon
+
+ ),
+};
diff --git a/packages/ui/src/components/Copyable/Copyable.tsx b/packages/ui/src/components/Copyable/Copyable.tsx
new file mode 100644
index 000000000..9c6026a4a
--- /dev/null
+++ b/packages/ui/src/components/Copyable/Copyable.tsx
@@ -0,0 +1,71 @@
+import { Tooltip } from '@radix-ui/themes';
+import { IconCopy } from '@tabler/icons-react';
+
+import { createPolymorphicComponent } from '../../utils/component';
+import type { Colors } from '../../utils/types';
+import { Box } from '../Box';
+import type { BoxProps } from '../Box';
+import type { IconContext } from '../Icon/useIconContext';
+import { IconButton } from '../IconButton/IconButton';
+import { toast } from '../Toast/useToast';
+
+export type CopyableBaseProps = {
+ value: string;
+ tooltipMessage?: string;
+ icon?: React.ComponentType>;
+ iconSize?: number;
+ iconStroke?: number;
+ iconClassName?: string;
+ iconColor?: Colors;
+ iconAriaLabel?: string;
+};
+
+export type CopyableProps = BoxProps & CopyableBaseProps;
+
+export const Copyable = createPolymorphicComponent({
+ id: 'Copyable',
+ className: 'inline-flex items-center gap-2',
+ baseElement: Box,
+ render: (
+ Comp,
+ {
+ children,
+ value,
+ tooltipMessage = 'Click here to copy to clipboard',
+ icon: CopyIcon = IconCopy,
+ iconSize,
+ iconStroke,
+ iconClassName,
+ iconColor = 'text-icon',
+ iconAriaLabel: ariaLabel = 'Copy to clipboard',
+ ...props
+ },
+ ) => {
+ async function handleCopy() {
+ await navigator.clipboard.writeText(value);
+ toast.success('Copied to clipboard');
+ }
+
+ return (
+
+ {children}
+
+
+
+
+ );
+ },
+ defaultProps: {
+ as: 'span',
+ },
+});
diff --git a/packages/ui/src/components/Copyable/index.tsx b/packages/ui/src/components/Copyable/index.tsx
new file mode 100644
index 000000000..f3cec0400
--- /dev/null
+++ b/packages/ui/src/components/Copyable/index.tsx
@@ -0,0 +1,3 @@
+export { Copyable } from './Copyable';
+
+export type { CopyableBaseProps, CopyableProps } from './Copyable';
diff --git a/packages/ui/src/components/Dialog/Dialog.stories.tsx b/packages/ui/src/components/Dialog/Dialog.stories.tsx
new file mode 100644
index 000000000..31fad1e4f
--- /dev/null
+++ b/packages/ui/src/components/Dialog/Dialog.stories.tsx
@@ -0,0 +1,66 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { HStack, VStack } from '../Box';
+import { Button } from '../Button/Button';
+import { Input } from '../Input/Input';
+import { Text } from '../Text/Text';
+
+import { Dialog } from './Dialog';
+
+const meta: Meta = {
+ title: 'Overlay/Dialog',
+ component: Dialog,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+ ),
+};
diff --git a/packages/ui/src/components/Dialog/Dialog.tsx b/packages/ui/src/components/Dialog/Dialog.tsx
new file mode 100644
index 000000000..fb4e06ce3
--- /dev/null
+++ b/packages/ui/src/components/Dialog/Dialog.tsx
@@ -0,0 +1,53 @@
+import { DialogTitle, Dialog as RD } from '@radix-ui/themes';
+
+import { createComponent, withNamespace } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type DialogProps = PropsOf;
+export type DialogTriggerProps = PropsOf;
+export type DialogTitleProps = PropsOf;
+export type DialogContentProps = PropsOf;
+export type DialogCloseProps = PropsOf;
+export type DialogDescriptionProps = PropsOf;
+
+export const DialogRoot = createComponent({
+ id: 'Dialog',
+ baseElement: RD.Root,
+});
+
+export const DialogTrigger = createComponent<
+ DialogTriggerProps,
+ typeof RD.Trigger
+>({
+ id: 'DialogTrigger',
+ baseElement: RD.Trigger,
+});
+
+export const DialogContent = createComponent<
+ DialogContentProps,
+ typeof RD.Content
+>({
+ id: 'DialogContent',
+ baseElement: RD.Content,
+});
+
+export const DialogClose = createComponent({
+ id: 'DialogClose',
+ baseElement: RD.Close,
+});
+
+export const DialogDescription = createComponent<
+ DialogDescriptionProps,
+ typeof RD.Description
+>({
+ id: 'DialogDescription',
+ baseElement: RD.Description,
+});
+
+export const Dialog = withNamespace(DialogRoot, {
+ Trigger: DialogTrigger,
+ Content: DialogContent,
+ Close: DialogClose,
+ Description: DialogDescription,
+ Title: DialogTitle,
+});
diff --git a/packages/ui/src/components/Dialog/index.tsx b/packages/ui/src/components/Dialog/index.tsx
new file mode 100644
index 000000000..6c79eb1e3
--- /dev/null
+++ b/packages/ui/src/components/Dialog/index.tsx
@@ -0,0 +1,17 @@
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogRoot,
+ DialogTrigger,
+} from './Dialog';
+
+export type {
+ DialogCloseProps,
+ DialogContentProps,
+ DialogDescriptionProps,
+ DialogProps,
+ DialogTitleProps,
+ DialogTriggerProps,
+} from './Dialog';
diff --git a/packages/ui/src/components/Drawer/Drawer.stories.tsx b/packages/ui/src/components/Drawer/Drawer.stories.tsx
new file mode 100644
index 000000000..275eb0d43
--- /dev/null
+++ b/packages/ui/src/components/Drawer/Drawer.stories.tsx
@@ -0,0 +1,99 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { HStack, VStack } from '../Box';
+import { Button } from '../Button/Button';
+import { Input } from '../Input/Input';
+
+import type { DrawerContentProps } from './Drawer';
+import { Drawer } from './Drawer';
+
+const meta: Meta = {
+ title: 'Overlay/Drawer',
+ component: Drawer,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+function Content(props?: DrawerContentProps) {
+ return (
+
+
+ Edit profile
+
+ Make changes to your profile here. Click save when you're done.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+
+
+
+ ),
+};
+
+export const Position: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Drawer/Drawer.tsx b/packages/ui/src/components/Drawer/Drawer.tsx
new file mode 100644
index 000000000..932bc3869
--- /dev/null
+++ b/packages/ui/src/components/Drawer/Drawer.tsx
@@ -0,0 +1,175 @@
+import * as RD from '@radix-ui/react-dialog';
+
+import {
+ createComponent,
+ createPolymorphicComponent,
+ withNamespace,
+} from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+import { Box } from '../Box';
+import type { BoxProps } from '../Box';
+import type { ButtonCloseProps } from '../ButtonClose/ButtonClose';
+import { ButtonClose } from '../ButtonClose/ButtonClose';
+import type { IconButtonProps } from '../IconButton';
+import { Theme } from '../Theme';
+
+import { styles } from './styles';
+
+export type DrawerProps = PropsOf;
+export type DrawerPortalProps = PropsOf;
+export type DrawerTriggerProps = PropsOf;
+export type DrawerOverlayProps = PropsOf;
+export type DrawerContentProps = PropsOf & {
+ side?: 'left' | 'right' | 'top' | 'bottom';
+};
+export type DrawerCloseProps = PropsOf;
+export type DrawerCloseIconProps = ButtonCloseProps;
+export type DrawerTitleProps = PropsOf;
+export type DrawerDescriptionProps = PropsOf;
+
+export type DrawerHeaderProps = BoxProps;
+export type DrawerBodyProps = BoxProps;
+export type DrawerFooterProps = BoxProps;
+
+export const DrawerRoot = createComponent({
+ id: 'Drawer',
+ baseElement: RD.Root,
+});
+
+export const DrawerTrigger = createComponent<
+ DrawerTriggerProps,
+ typeof RD.Trigger
+>({
+ id: 'DrawerTrigger',
+ baseElement: RD.Trigger,
+ defaultProps: {
+ asChild: true,
+ },
+});
+
+export const DrawerPortal = createComponent<
+ DrawerPortalProps,
+ typeof RD.Portal
+>({
+ id: 'DrawerPortal',
+ baseElement: RD.Portal,
+});
+
+export const DrawerOverlay = createComponent<
+ DrawerOverlayProps,
+ typeof RD.Overlay
+>({
+ id: 'DrawerOverlay',
+ baseElement: RD.Overlay,
+ className: () => styles().overlay(),
+});
+
+export const DrawerClose = createComponent({
+ id: 'DrawerClose',
+ baseElement: RD.Close,
+ defaultProps: {
+ asChild: true,
+ },
+});
+
+export const DrawerCloseIcon = createComponent<
+ DrawerCloseIconProps,
+ typeof ButtonClose
+>({
+ id: 'DrawerCloseIcon',
+ className: () => styles().closeIcon(),
+ baseElement: ButtonClose,
+ render: (Comp, props) => {
+ return (
+
+
+
+ );
+ },
+ defaultProps: {
+ variant: 'link',
+ color: 'gray',
+ },
+});
+
+export const DrawerContent = createComponent<
+ DrawerContentProps,
+ typeof RD.Content
+>({
+ id: 'DrawerContent',
+ baseElement: RD.Content,
+ render: (Comp, { className, children, side = 'right', ...props }) => {
+ const classes = styles({ side });
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+ },
+});
+
+export const DrawerDescription = createComponent<
+ DrawerDescriptionProps,
+ typeof RD.Description
+>({
+ id: 'DrawerDescription',
+ baseElement: RD.Description,
+ className: () => styles().description(),
+});
+
+export const DrawerTitle = createComponent({
+ id: 'DrawerTitle',
+ baseElement: RD.Title,
+ className: () => styles().title(),
+});
+
+export const DrawerHeader = createPolymorphicComponent<
+ DrawerHeaderProps,
+ typeof Box
+>({
+ id: 'DrawerHeader',
+ baseElement: Box,
+ className: () => styles().header(),
+ defaultProps: {
+ as: 'header',
+ },
+});
+
+export const DrawerBody = createPolymorphicComponent<
+ DrawerBodyProps,
+ typeof Box
+>({
+ id: 'DrawerBody',
+ baseElement: Box,
+ className: () => styles().body(),
+});
+
+export const DrawerFooter = createPolymorphicComponent<
+ DrawerFooterProps,
+ typeof Box
+>({
+ id: 'DrawerFooter',
+ baseElement: Box,
+ className: () => styles().footer(),
+ defaultProps: {
+ as: 'header',
+ },
+});
+
+export const Drawer = withNamespace(DrawerRoot, {
+ Trigger: DrawerTrigger,
+ Close: DrawerClose,
+ CloseIcon: DrawerCloseIcon,
+ Content: DrawerContent,
+ Header: DrawerHeader,
+ Description: DrawerDescription,
+ Title: DrawerTitle,
+ Body: DrawerBody,
+ Footer: DrawerFooter,
+});
diff --git a/packages/ui/src/components/Drawer/index.tsx b/packages/ui/src/components/Drawer/index.tsx
new file mode 100644
index 000000000..c6bf97e3b
--- /dev/null
+++ b/packages/ui/src/components/Drawer/index.tsx
@@ -0,0 +1,30 @@
+'use client';
+
+export {
+ Drawer,
+ DrawerBody,
+ DrawerClose,
+ DrawerCloseIcon,
+ DrawerContent,
+ DrawerDescription,
+ DrawerFooter,
+ DrawerHeader,
+ DrawerOverlay,
+ DrawerRoot,
+ DrawerTitle,
+ DrawerTrigger,
+} from './Drawer';
+
+export type {
+ DrawerBodyProps,
+ DrawerCloseIconProps,
+ DrawerCloseProps,
+ DrawerContentProps,
+ DrawerDescriptionProps,
+ DrawerFooterProps,
+ DrawerHeaderProps,
+ DrawerOverlayProps,
+ DrawerProps,
+ DrawerTitleProps,
+ DrawerTriggerProps,
+} from './Drawer';
diff --git a/packages/ui/src/components/Drawer/styles.ts b/packages/ui/src/components/Drawer/styles.ts
new file mode 100644
index 000000000..a3fd6c0ff
--- /dev/null
+++ b/packages/ui/src/components/Drawer/styles.ts
@@ -0,0 +1,64 @@
+import { tv } from 'tailwind-variants';
+
+export const styles = tv({
+ slots: {
+ overlay: [
+ 'fixed inset-0 z-50 backdrop-blur-sm',
+ 'state-open:animate-in state-closed:animate-out',
+ 'state-closed:fade-out-0 state-open:fade-in-0',
+ ],
+ closeIcon: [
+ 'absolute right-4 top-4 rounded-sm opacity-70',
+ 'transition-opacity hover:opacity-100 focus:outline-none',
+ 'focus:ring-2 focus:ring-gray-4 focus:ring-offset-2',
+ 'disabled:pointer-events-none state-open:bg-secondary',
+ ],
+ content: [
+ 'flex flex-col fixed z-50 gap-4 bg-card-bg p-6',
+ 'text-color border-border shadow-lg transition ease-in-out',
+ 'state-open:animate-in state-closed:animate-out',
+ 'state-closed:duration-500 state-open:duration-700',
+ ],
+ header: ['flex flex-col space-y-2'],
+ description: ['text-sm text-muted'],
+ footer: [
+ 'flex flex-col-reverse',
+ 'sm:flex-row sm:justify-end sm:space-x-2',
+ ],
+ title: ['text-lg font-semibold text-heading'],
+ body: ['py-4 flex-1'],
+ },
+ variants: {
+ side: {
+ top: {
+ content: [
+ 'inset-x-0 top-0 border-b',
+ 'state-closed:slide-out-to-top state-open:slide-in-from-top',
+ ],
+ },
+ bottom: {
+ content: [
+ 'inset-x-0 bottom-0 border-t',
+ 'state-closed:slide-out-to-bottom state-open:slide-in-from-bottom',
+ ],
+ },
+ left: {
+ content: [
+ 'inset-y-0 left-0 h-full w-3/4 border-r',
+ 'state-closed:slide-out-to-left state-open:slide-in-from-left',
+ 'sm:max-w-sm',
+ ],
+ },
+ right: {
+ content: [
+ 'inset-y-0 right-0 h-full w-3/4 border-l',
+ 'state-closed:slide-out-to-right state-open:slide-in-from-right',
+ 'sm:max-w-sm',
+ ],
+ },
+ },
+ },
+ defaultVariants: {
+ side: 'right',
+ },
+});
diff --git a/packages/ui/src/components/Dropdown/Dropdown.stories.tsx b/packages/ui/src/components/Dropdown/Dropdown.stories.tsx
new file mode 100644
index 000000000..7b065e408
--- /dev/null
+++ b/packages/ui/src/components/Dropdown/Dropdown.stories.tsx
@@ -0,0 +1,51 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconChevronDown } from '@tabler/icons-react';
+
+import { Button } from '../Button/Button';
+
+import { Dropdown } from './Dropdown';
+
+const meta: Meta = {
+ title: 'Overlay/Dropdown',
+ component: Dropdown,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+
+
+ Edit
+ Duplicate
+
+ Archive
+
+ More
+
+ Move to project…
+ Move to folder…
+
+ Advanced options…
+
+
+
+ Share
+ Add to favorites
+
+
+ Delete
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Dropdown/Dropdown.tsx b/packages/ui/src/components/Dropdown/Dropdown.tsx
new file mode 100644
index 000000000..414550e22
--- /dev/null
+++ b/packages/ui/src/components/Dropdown/Dropdown.tsx
@@ -0,0 +1,118 @@
+import { DropdownMenu as RD } from '@radix-ui/themes';
+
+import { createComponent, withNamespace } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type DropdownProps = PropsOf;
+export type DropdownTriggerProps = PropsOf;
+export type DropdownContentProps = PropsOf;
+export type DropdownLabelProps = PropsOf;
+export type DropdownItemProps = PropsOf;
+export type DropdownGroupProps = PropsOf;
+export type DropdownRadioGroupProps = PropsOf;
+export type DropdownCheckboxItemProps = PropsOf;
+export type DropdownSubProps = PropsOf;
+export type DropdownSubTriggerProps = PropsOf;
+export type DropdownSubContentProps = PropsOf;
+export type DropdownSeparatorProps = PropsOf;
+
+export const DropdownRoot = createComponent({
+ id: 'Dropdown',
+ baseElement: RD.Root,
+});
+
+export const DropdownTrigger = createComponent<
+ DropdownTriggerProps,
+ typeof RD.Trigger
+>({
+ id: 'DropdownTrigger',
+ baseElement: RD.Trigger,
+});
+
+export const DropdownContent = createComponent<
+ DropdownContentProps,
+ typeof RD.Content
+>({
+ id: 'DropdownContent',
+ baseElement: RD.Content,
+});
+
+export const DropdownLabel = createComponent<
+ DropdownLabelProps,
+ typeof RD.Label
+>({
+ id: 'DropdownLabel',
+ baseElement: RD.Label,
+});
+
+export const DropdownItem = createComponent({
+ id: 'DropdownItem',
+ baseElement: RD.Item,
+});
+
+export const DropdownGroup = createComponent<
+ DropdownGroupProps,
+ typeof RD.Group
+>({
+ id: 'DropdownGroup',
+ baseElement: RD.Group,
+});
+
+export const DropdownRadioGroup = createComponent<
+ DropdownRadioGroupProps,
+ typeof RD.RadioGroup
+>({
+ id: 'DropdownRadioGroup',
+ baseElement: RD.RadioGroup,
+});
+
+export const DropdownCheckboxItem = createComponent<
+ DropdownCheckboxItemProps,
+ typeof RD.CheckboxItem
+>({
+ id: 'DropdownCheckboxItem',
+ baseElement: RD.CheckboxItem,
+});
+
+export const DropdownSub = createComponent({
+ id: 'DropdownSub',
+ baseElement: RD.Sub,
+});
+
+export const DropdownSubContent = createComponent<
+ DropdownSubContentProps,
+ typeof RD.SubContent
+>({
+ id: 'DropdownSubContent',
+ baseElement: RD.SubContent,
+});
+
+export const DropdownSubTrigger = createComponent<
+ DropdownSubTriggerProps,
+ typeof RD.SubTrigger
+>({
+ id: 'DropdownSubTrigger',
+ baseElement: RD.SubTrigger,
+});
+
+export const DropdownSeparator = createComponent<
+ DropdownSeparatorProps,
+ typeof RD.Separator
+>({
+ id: 'DropdownSeparator',
+ baseElement: RD.Separator,
+});
+
+export const Dropdown = withNamespace(DropdownRoot, {
+ Trigger: DropdownTrigger,
+ Content: DropdownContent,
+ Label: DropdownLabel,
+ Item: DropdownItem,
+ Group: DropdownGroup,
+ RadioGroup: DropdownRadioGroup,
+ CheckboxItem: DropdownCheckboxItem,
+ Sub: DropdownSub,
+ SubTrigger: DropdownSubTrigger,
+ SubContent: DropdownSubContent,
+ Separator: DropdownSeparator,
+});
diff --git a/packages/ui/src/components/Dropdown/index.tsx b/packages/ui/src/components/Dropdown/index.tsx
new file mode 100644
index 000000000..2a541e920
--- /dev/null
+++ b/packages/ui/src/components/Dropdown/index.tsx
@@ -0,0 +1,30 @@
+export {
+ Dropdown,
+ DropdownCheckboxItem,
+ DropdownContent,
+ DropdownGroup,
+ DropdownItem,
+ DropdownLabel,
+ DropdownRadioGroup,
+ DropdownRoot,
+ DropdownSeparator,
+ DropdownSub,
+ DropdownSubContent,
+ DropdownSubTrigger,
+ DropdownTrigger,
+} from './Dropdown';
+
+export type {
+ DropdownCheckboxItemProps,
+ DropdownContentProps,
+ DropdownGroupProps,
+ DropdownItemProps,
+ DropdownLabelProps,
+ DropdownProps,
+ DropdownRadioGroupProps,
+ DropdownSeparatorProps,
+ DropdownSubContentProps,
+ DropdownSubProps,
+ DropdownSubTriggerProps,
+ DropdownTriggerProps,
+} from './Dropdown';
diff --git a/packages/ui/src/components/EntityItem/EntityItem.stories.tsx b/packages/ui/src/components/EntityItem/EntityItem.stories.tsx
new file mode 100644
index 000000000..334bd6c7e
--- /dev/null
+++ b/packages/ui/src/components/EntityItem/EntityItem.stories.tsx
@@ -0,0 +1,51 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconCode } from '@tabler/icons-react';
+
+import { VStack } from '../Box';
+import { Icon } from '../Icon';
+
+import { EntityItem } from './EntityItem';
+
+const meta: Meta = {
+ title: 'Web3/EntityItem',
+ component: EntityItem,
+};
+
+export default meta;
+type Story = StoryObj;
+
+const DEFAULT_ARGS = {
+ children: (
+ <>
+
+
+
+
+ >
+ ),
+};
+
+export const Default: Story = {
+ args: DEFAULT_ARGS,
+};
+
+export const Variations: Story = {
+ render: () => (
+
+
+
+
+
+
+
+ {DEFAULT_ARGS.children}
+
+ ),
+};
diff --git a/packages/ui/src/components/EntityItem/EntityItem.tsx b/packages/ui/src/components/EntityItem/EntityItem.tsx
new file mode 100644
index 000000000..c7ef96a42
--- /dev/null
+++ b/packages/ui/src/components/EntityItem/EntityItem.tsx
@@ -0,0 +1,75 @@
+import type { ReactNode } from 'react';
+import type { VariantProps } from 'tailwind-variants';
+import { tv } from 'tailwind-variants';
+
+import { createComponent, withNamespace } from '../../utils/component';
+import { shortAddress } from '../../utils/helpers';
+import type { BoxProps, HStackProps } from '../Box';
+import { Box, HStack } from '../Box';
+import { Copyable } from '../Copyable';
+import { Text } from '../Text';
+
+export type EntityItemVariantProps = VariantProps;
+export type EntityItemProps = Omit &
+ EntityItemVariantProps;
+
+export type EntityItemSlotProps = BoxProps;
+export type EntityItemInfo = BoxProps & {
+ title: ReactNode;
+ id: string;
+ shortId?: boolean;
+};
+
+export const EntityItemRoot = createComponent({
+ id: 'EntityItem',
+ baseElement: HStack,
+ render: (Comp, { gap = '2', className, ...props }) => {
+ const classes = styles();
+ return (
+
+ );
+ },
+});
+
+export const EntityItemSlot = createComponent({
+ id: 'EntityItemSlot',
+ baseElement: Box,
+});
+
+export const EntityItemInfo = createComponent({
+ id: 'EntityItemInfo',
+ baseElement: Box,
+ render: (
+ Comp,
+ { title, id, children, className, shortId = true, ...props },
+ ) => {
+ const classes = styles();
+ return (
+
+
+ {title}
+
+
+ {shortId ? shortAddress(id) : id}
+
+ {children}
+
+ );
+ },
+});
+
+export const EntityItem = withNamespace(EntityItemRoot, {
+ Slot: EntityItemSlot,
+ Info: EntityItemInfo,
+});
+
+const styles = tv({
+ slots: {
+ root: 'gap-4 items-center',
+ name: 'mt-0 font-medium',
+ info: 'flex flex-col justify-center gap-1',
+ tag: 'mt-0',
+ copyable: 'text-sm text-muted',
+ assetId: 'text-sm leading-tight',
+ },
+});
diff --git a/packages/ui/src/components/EntityItem/index.tsx b/packages/ui/src/components/EntityItem/index.tsx
new file mode 100644
index 000000000..295e731b8
--- /dev/null
+++ b/packages/ui/src/components/EntityItem/index.tsx
@@ -0,0 +1,12 @@
+export type {
+ EntityItemProps,
+ EntityItemSlotProps,
+ EntityItemVariantProps,
+} from './EntityItem';
+
+export {
+ EntityItem,
+ EntityItemRoot,
+ EntityItemSlot,
+ EntityItemInfo,
+} from './EntityItem';
diff --git a/packages/ui/src/components/Focus/Focus.stories.tsx b/packages/ui/src/components/Focus/Focus.stories.tsx
new file mode 100644
index 000000000..e42c2f326
--- /dev/null
+++ b/packages/ui/src/components/Focus/Focus.stories.tsx
@@ -0,0 +1,29 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { HStack, VStack } from '../Box';
+import { Button } from '../Button/Button';
+import { Text } from '../Text/Text';
+
+import { Focus } from './Focus';
+
+const meta: Meta = {
+ title: 'Helpers/Focus',
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+ Try to navigate between buttons using arrow keys
+
+ ),
+};
diff --git a/packages/ui/src/components/Focus/Focus.tsx b/packages/ui/src/components/Focus/Focus.tsx
new file mode 100644
index 000000000..282458279
--- /dev/null
+++ b/packages/ui/src/components/Focus/Focus.tsx
@@ -0,0 +1,41 @@
+// eslint-disable-next-line @typescript-eslint/triple-slash-reference
+///
+import type { FocusScopeProps } from '@react-aria/focus';
+import { FocusScope } from '@react-aria/focus';
+import { Children, cloneElement } from 'react';
+import type { ReactElement } from 'react';
+import { mergeProps } from 'react-aria';
+
+import { createComponent } from '../../utils/component';
+
+import { useFocusNavigator, isRightChildrenType } from './useFocusNavigator';
+
+export type FocusArrowNavigatorProps = FocusScopeProps;
+
+export const FocusArrowNavigator = createComponent<
+ FocusArrowNavigatorProps,
+ typeof FocusScope
+>({
+ id: 'FocusArrowNavigator',
+ render: (_, { children, ...props }) => {
+ const { onKeyDown } = useFocusNavigator();
+
+ if (isRightChildrenType(children)) {
+ const child = Children.map(
+ children as ReactElement[],
+ (child: ReactElement) => {
+ return cloneElement(child, mergeProps(child.props, { onKeyDown }));
+ },
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ) as any;
+ return {child};
+ }
+
+ throw new Error('Children type not accepted');
+ },
+});
+
+export const Focus = {
+ ArrowNavigator: FocusArrowNavigator,
+ Scope: FocusScope,
+};
diff --git a/packages/ui/src/components/Focus/index.tsx b/packages/ui/src/components/Focus/index.tsx
new file mode 100644
index 000000000..08511f104
--- /dev/null
+++ b/packages/ui/src/components/Focus/index.tsx
@@ -0,0 +1,7 @@
+'use client';
+
+export { Focus, FocusArrowNavigator } from './Focus';
+
+export type { FocusArrowNavigatorProps } from './Focus';
+
+export { isRightChildrenType, useFocusNavigator } from './useFocusNavigator';
diff --git a/packages/ui/src/components/Focus/useFocusNavigator.tsx b/packages/ui/src/components/Focus/useFocusNavigator.tsx
new file mode 100644
index 000000000..df828e06c
--- /dev/null
+++ b/packages/ui/src/components/Focus/useFocusNavigator.tsx
@@ -0,0 +1,36 @@
+'use client';
+
+import type { ReactNode } from 'react';
+import { useFocusManager } from 'react-aria';
+
+export function useFocusNavigator() {
+ const focusManager = useFocusManager();
+
+ const onKeyDown = (e: KeyboardEvent) => {
+ if (e.key === 'ArrowRight') {
+ focusManager.focusNext();
+ }
+ if (e.key === 'ArrowLeft') {
+ focusManager.focusPrevious();
+ }
+ if (e.key === 'ArrowUp') {
+ focusManager.focusNext();
+ }
+ if (e.key === 'ArrowDown') {
+ focusManager.focusPrevious();
+ }
+ };
+
+ return {
+ onKeyDown,
+ };
+}
+
+export function isRightChildrenType(children: ReactNode) {
+ return (
+ typeof children !== 'boolean' &&
+ typeof children !== 'string' &&
+ typeof children !== 'undefined' &&
+ typeof children !== 'number'
+ );
+}
diff --git a/packages/ui/src/components/FuelLogo/FuelLogo.stories.tsx b/packages/ui/src/components/FuelLogo/FuelLogo.stories.tsx
new file mode 100644
index 000000000..118bb35a0
--- /dev/null
+++ b/packages/ui/src/components/FuelLogo/FuelLogo.stories.tsx
@@ -0,0 +1,31 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { FuelLogo } from './FuelLogo';
+
+const meta: Meta = {
+ title: 'UI/FuelLogo',
+ component: FuelLogo,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ args: {
+ showLettering: true,
+ showSymbol: true,
+ },
+};
+
+export const NoLettering: Story = {
+ args: {
+ showLettering: false,
+ },
+};
+
+export const NoSymbol: Story = {
+ args: {
+ showLettering: true,
+ showSymbol: false,
+ },
+};
diff --git a/packages/ui/src/components/FuelLogo/FuelLogo.tsx b/packages/ui/src/components/FuelLogo/FuelLogo.tsx
new file mode 100644
index 000000000..fa6b7ae23
--- /dev/null
+++ b/packages/ui/src/components/FuelLogo/FuelLogo.tsx
@@ -0,0 +1,42 @@
+import { createComponent } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type FuelLogoProps = PropsOf<'span'> & {
+ size?: number;
+ showLettering?: boolean;
+ showSymbol?: boolean;
+};
+
+export const FuelLogo = createComponent({
+ id: 'FuelLogo',
+ className: 'inline-flex items-center gap-3',
+ render: (_, { size = 40, showLettering, showSymbol = true, ...props }) => {
+ return (
+
+ {showSymbol && (
+
+ )}
+ {showLettering && (
+
+ )}
+
+ );
+ },
+});
diff --git a/packages/ui/src/components/FuelLogo/index.tsx b/packages/ui/src/components/FuelLogo/index.tsx
new file mode 100644
index 000000000..bdbefda8a
--- /dev/null
+++ b/packages/ui/src/components/FuelLogo/index.tsx
@@ -0,0 +1,3 @@
+export { FuelLogo } from './FuelLogo';
+
+export type { FuelLogoProps } from './FuelLogo';
diff --git a/packages/ui/src/components/Heading/Heading.stories.tsx b/packages/ui/src/components/Heading/Heading.stories.tsx
new file mode 100644
index 000000000..18fda7db0
--- /dev/null
+++ b/packages/ui/src/components/Heading/Heading.stories.tsx
@@ -0,0 +1,47 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconCalendar } from '@tabler/icons-react';
+
+import { VStack } from '../Box';
+
+import { Heading } from './Heading';
+
+const meta: Meta = {
+ title: 'Base/Heading',
+ component: Heading,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+ The quick brown fox jumps over the lazy dog
+
+
+ The quick brown fox jumps over the lazy dog
+
+
+ The quick brown fox jumps over the lazy dog
+
+
+ The quick brown fox jumps over the lazy dog
+
+
+ The quick brown fox jumps over the lazy dog
+
+
+ The quick brown fox jumps over the lazy dog
+
+
+ ),
+};
+
+export const WithIcon: Story = {
+ render: () => (
+
+ Calendar
+
+ ),
+};
diff --git a/packages/ui/src/components/Heading/Heading.tsx b/packages/ui/src/components/Heading/Heading.tsx
new file mode 100644
index 000000000..f0dec5aa3
--- /dev/null
+++ b/packages/ui/src/components/Heading/Heading.tsx
@@ -0,0 +1,38 @@
+import { useIconProps } from '../../hooks/useIconProps';
+import type { WithIconProps } from '../../hooks/useIconProps';
+import { createComponent } from '../../utils/component';
+import type { PropsOf, WithAsProps } from '../../utils/types';
+
+import { styles } from './styles';
+
+export type HeadingProps = Omit &
+ PropsOf<'h2'> &
+ WithAsProps & {
+ size?: '1' | '2' | '3' | '4' | '5' | '6';
+ };
+
+export const Heading = createComponent({
+ id: 'Heading',
+ baseElement: 'h2',
+ render(
+ _,
+ {
+ as: Root = 'h2',
+ size = '2',
+ className,
+ iconColor = 'text-icon',
+ ...props
+ },
+ ) {
+ const { size: __, ...itemProps } = useIconProps({
+ iconColor,
+ ...props,
+ } as WithIconProps);
+
+ const classes = styles({
+ className,
+ withIcon: !!itemProps['data-icon'],
+ });
+ return ;
+ },
+});
diff --git a/packages/ui/src/components/Heading/index.tsx b/packages/ui/src/components/Heading/index.tsx
new file mode 100644
index 000000000..832a471e4
--- /dev/null
+++ b/packages/ui/src/components/Heading/index.tsx
@@ -0,0 +1,3 @@
+export { Heading } from './Heading';
+
+export type { HeadingProps } from './Heading';
diff --git a/packages/ui/src/components/Heading/styles.ts b/packages/ui/src/components/Heading/styles.ts
new file mode 100644
index 000000000..fe60b8539
--- /dev/null
+++ b/packages/ui/src/components/Heading/styles.ts
@@ -0,0 +1,21 @@
+import { tv } from 'tailwind-variants';
+
+export const styles = tv({
+ base: [
+ 'm-0 font-medium tracking-tight text-heading',
+ 'data-[size="1"]:text-h1',
+ 'data-[size="2"]:text-h2',
+ 'data-[size="3"]:text-h3',
+ 'data-[size="4"]:text-h4',
+ 'data-[size="5"]:text-h5',
+ 'data-[size="6"]:text-h6',
+ ],
+ variants: {
+ withIcon: {
+ true: 'flex items-center gap-2',
+ },
+ },
+ defaultVariants: {
+ withIcon: false,
+ },
+});
diff --git a/packages/ui/src/components/HelperIcon/HelperIcon.stories.tsx b/packages/ui/src/components/HelperIcon/HelperIcon.stories.tsx
new file mode 100644
index 000000000..8ef938624
--- /dev/null
+++ b/packages/ui/src/components/HelperIcon/HelperIcon.stories.tsx
@@ -0,0 +1,20 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { HelperIcon } from './HelperIcon';
+
+const meta: Meta = {
+ title: 'Helpers/HelperIcon',
+ component: HelperIcon,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+ Some information
+ ),
+};
diff --git a/packages/ui/src/components/HelperIcon/HelperIcon.tsx b/packages/ui/src/components/HelperIcon/HelperIcon.tsx
new file mode 100644
index 000000000..e3f32e266
--- /dev/null
+++ b/packages/ui/src/components/HelperIcon/HelperIcon.tsx
@@ -0,0 +1,54 @@
+import { Tooltip } from '@radix-ui/themes';
+import { IconHelpCircle } from '@tabler/icons-react';
+
+import { createComponent } from '../../utils/component';
+import type { Colors, PropsOf } from '../../utils/types';
+import { Icon } from '../Icon/Icon';
+import type { IconContext } from '../Icon/useIconContext';
+
+export type HelperIconBaseProps = {
+ message: string;
+ icon?: React.ComponentType>;
+ iconSize?: number;
+ iconStroke?: number;
+ iconClassName?: string;
+ iconColor?: Colors;
+ iconAriaLabel?: string;
+};
+export type HelperIconProps = PropsOf<'span'> & HelperIconBaseProps;
+
+export const HelperIcon = createComponent({
+ id: 'HelperIcon',
+ baseElement: 'span',
+ className: 'inline-flex items-center gap-2',
+ render: (
+ Comp,
+ {
+ children,
+ message,
+ icon: HelperIcon = IconHelpCircle,
+ iconSize,
+ iconStroke,
+ iconClassName,
+ iconColor = 'text-icon',
+ iconAriaLabel: ariaLabel = 'Helper Icon',
+ ...props
+ },
+ ) => {
+ return (
+
+ {children}
+
+
+
+
+ );
+ },
+});
diff --git a/packages/ui/src/components/HelperIcon/index.tsx b/packages/ui/src/components/HelperIcon/index.tsx
new file mode 100644
index 000000000..927a2f23d
--- /dev/null
+++ b/packages/ui/src/components/HelperIcon/index.tsx
@@ -0,0 +1,3 @@
+export { HelperIcon } from './HelperIcon';
+
+export type { HelperIconBaseProps, HelperIconProps } from './HelperIcon';
diff --git a/packages/ui/src/components/HoverCard/HoverCard.stories.tsx b/packages/ui/src/components/HoverCard/HoverCard.stories.tsx
new file mode 100644
index 000000000..70e8f28b5
--- /dev/null
+++ b/packages/ui/src/components/HoverCard/HoverCard.stories.tsx
@@ -0,0 +1,61 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Avatar } from '../Avatar/Avatar';
+import { Box, HStack } from '../Box';
+import { Heading } from '../Heading/Heading';
+import { Link } from '../Link/Link';
+import { Text } from '../Text/Text';
+
+import { HoverCard } from './HoverCard';
+
+const meta: Meta = {
+ title: 'Overlay/HoverCard',
+ component: HoverCard,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+ Follow{' '}
+
+
+
+ @fuel_network
+
+
+
+
+
+
+
+ Fuel
+
+
+ @fuel_network
+
+
+
+ The fastest execution layer.
+
+
+
+
+ {' '}
+ for updates.
+
+ ),
+};
diff --git a/packages/ui/src/components/HoverCard/HoverCard.tsx b/packages/ui/src/components/HoverCard/HoverCard.tsx
new file mode 100644
index 000000000..15222d605
--- /dev/null
+++ b/packages/ui/src/components/HoverCard/HoverCard.tsx
@@ -0,0 +1,34 @@
+import { HoverCard as RH } from '@radix-ui/themes';
+
+import { createComponent, withNamespace } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type HoverCardProps = PropsOf;
+export type HoverCardTriggerProps = PropsOf;
+export type HoverCardContentProps = PropsOf;
+
+export const HoverCardRoot = createComponent({
+ id: 'HoverCard',
+ baseElement: RH.Root,
+});
+
+export const HoverCardTrigger = createComponent<
+ HoverCardTriggerProps,
+ typeof RH.Trigger
+>({
+ id: 'HoverCardTrigger',
+ baseElement: RH.Trigger,
+});
+
+export const HoverCardContent = createComponent<
+ HoverCardContentProps,
+ typeof RH.Content
+>({
+ id: 'HoverCardContent',
+ baseElement: RH.Content,
+});
+
+export const HoverCard = withNamespace(HoverCardRoot, {
+ Trigger: HoverCardTrigger,
+ Content: HoverCardContent,
+});
diff --git a/packages/ui/src/components/HoverCard/index.tsx b/packages/ui/src/components/HoverCard/index.tsx
new file mode 100644
index 000000000..679aa17d8
--- /dev/null
+++ b/packages/ui/src/components/HoverCard/index.tsx
@@ -0,0 +1,12 @@
+export {
+ HoverCard,
+ HoverCardContent,
+ HoverCardRoot,
+ HoverCardTrigger,
+} from './HoverCard';
+
+export type {
+ HoverCardContentProps,
+ HoverCardProps,
+ HoverCardTriggerProps,
+} from './HoverCard';
diff --git a/packages/ui/src/components/Icon/Icon.stories.tsx b/packages/ui/src/components/Icon/Icon.stories.tsx
new file mode 100644
index 000000000..6188c0608
--- /dev/null
+++ b/packages/ui/src/components/Icon/Icon.stories.tsx
@@ -0,0 +1,26 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconHome } from '@tabler/icons-react';
+
+import { Theme } from '../Theme/Theme';
+
+import { Icon } from './Icon';
+
+const meta: Meta = {
+ title: 'UI/Icon',
+ component: Icon,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => ,
+};
+
+export const WithTheme: Story = {
+ render: () => (
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Icon/Icon.tsx b/packages/ui/src/components/Icon/Icon.tsx
new file mode 100644
index 000000000..6fdbded34
--- /dev/null
+++ b/packages/ui/src/components/Icon/Icon.tsx
@@ -0,0 +1,34 @@
+import { createComponent } from '../../utils/component';
+import { cx } from '../../utils/css';
+import type { PropsOf } from '../../utils/types';
+
+import type { IconComponent, IconContext } from './useIconContext';
+import { useIconContext } from './useIconContext';
+
+type SvgIconProps = Omit, 'size' | 'stroke'>;
+export type IconBaseProps = Partial & { icon: IconComponent };
+export type IconProps = IconBaseProps & SvgIconProps;
+
+export const Icon = createComponent({
+ id: 'Icon',
+ baseElement: 'svg',
+ render: (
+ _,
+ {
+ className,
+ color: initColor,
+ size: initSize,
+ stroke: initStroke,
+ icon: IconComponent,
+ ...props
+ },
+ ) => {
+ const iconContext = useIconContext();
+ const size = initSize || iconContext.size;
+ const stroke = initStroke || iconContext.stroke;
+ const color = initColor || iconContext.color;
+ const itemProps = { ...props, size, stroke };
+ const classes = cx(color, 'inline-flex items-center', className);
+ return ;
+ },
+});
diff --git a/packages/ui/src/components/Icon/index.tsx b/packages/ui/src/components/Icon/index.tsx
new file mode 100644
index 000000000..bf8be93b8
--- /dev/null
+++ b/packages/ui/src/components/Icon/index.tsx
@@ -0,0 +1,9 @@
+'use client';
+
+export { Icon } from './Icon';
+
+export type { IconBaseProps, IconProps } from './Icon';
+
+export { IconProvider, useIconContext } from './useIconContext';
+
+export type { IconComponent, IconContext } from './useIconContext';
diff --git a/packages/ui/src/components/Icon/useIconContext.tsx b/packages/ui/src/components/Icon/useIconContext.tsx
new file mode 100644
index 000000000..e29ff1d68
--- /dev/null
+++ b/packages/ui/src/components/Icon/useIconContext.tsx
@@ -0,0 +1,27 @@
+'use client';
+
+import type { ComponentType } from 'react';
+import { createContext, useContext } from 'react';
+
+import type { Colors } from '../../utils/types';
+
+export type IconContext = {
+ stroke?: number;
+ size?: number;
+ color?: Colors;
+};
+
+export type IconComponent = ComponentType<
+ Partial & { className?: string }
+>;
+
+const context = createContext({
+ size: 18,
+ stroke: 1.2,
+} as IconContext);
+
+export function useIconContext() {
+ return useContext(context);
+}
+
+export const IconProvider = context.Provider;
diff --git a/packages/ui/src/components/IconButton/IconButton.stories.tsx b/packages/ui/src/components/IconButton/IconButton.stories.tsx
new file mode 100644
index 000000000..b7883cb1d
--- /dev/null
+++ b/packages/ui/src/components/IconButton/IconButton.stories.tsx
@@ -0,0 +1,22 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconSettings } from '@tabler/icons-react';
+
+import { IconButton } from './IconButton';
+
+const meta: Meta = {
+ title: 'UI/IconButton',
+ component: IconButton,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => ,
+};
+
+export const Loading: Story = {
+ render: () => (
+
+ ),
+};
diff --git a/packages/ui/src/components/IconButton/IconButton.tsx b/packages/ui/src/components/IconButton/IconButton.tsx
new file mode 100644
index 000000000..c1bb452d2
--- /dev/null
+++ b/packages/ui/src/components/IconButton/IconButton.tsx
@@ -0,0 +1,66 @@
+import { IconButton as RadixIconButton } from '@radix-ui/themes';
+
+import { getIconSize } from '../../hooks/useIconProps';
+import { useVariants } from '../../hooks/useVariants';
+import type { WithVariants } from '../../hooks/useVariants';
+import { createComponent } from '../../utils/component';
+import type { Colors, PropsOf } from '../../utils/types';
+import { Icon } from '../Icon/Icon';
+import type { IconContext } from '../Icon/useIconContext';
+import { Spinner } from '../Spinner/Spinner';
+
+type RadixIconButtonProps = Omit, 'children'>;
+
+export type IconButtonProps = WithVariants<
+ RadixIconButtonProps & {
+ disabled?: boolean;
+ isLoading?: boolean;
+ icon: React.ComponentType>;
+ iconSize?: number;
+ iconStroke?: number;
+ iconClassName?: string;
+ iconColor?: Colors;
+ 'aria-label'?: string;
+ }
+>;
+
+export const IconButton = createComponent({
+ id: 'IconButton',
+ render: (
+ _,
+ {
+ size,
+ disabled,
+ isLoading,
+ icon,
+ iconSize,
+ iconStroke,
+ iconClassName,
+ iconColor,
+ ...props
+ },
+ ) => {
+ const variantProps = useVariants(props);
+ const isDisabled = Boolean(disabled || isLoading);
+ return (
+
+ {isLoading ? (
+
+ ) : (
+
+ )}
+
+ );
+ },
+});
diff --git a/packages/ui/src/components/IconButton/index.tsx b/packages/ui/src/components/IconButton/index.tsx
new file mode 100644
index 000000000..5656460ea
--- /dev/null
+++ b/packages/ui/src/components/IconButton/index.tsx
@@ -0,0 +1,3 @@
+export { IconButton } from './IconButton';
+
+export type { IconButtonProps } from './IconButton';
diff --git a/packages/ui/src/components/Input/Input.stories.tsx b/packages/ui/src/components/Input/Input.stories.tsx
new file mode 100644
index 000000000..6ff526968
--- /dev/null
+++ b/packages/ui/src/components/Input/Input.stories.tsx
@@ -0,0 +1,38 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconSearch } from '@tabler/icons-react';
+
+import { Box, VStack } from '../Box';
+import { Icon } from '../Icon/Icon';
+
+import { Input } from './Input';
+
+const meta: Meta = {
+ title: 'Form/Input',
+ component: Input,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+ ),
+};
+
+export const Sizes: Story = {
+ render: () => (
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Input/Input.tsx b/packages/ui/src/components/Input/Input.tsx
new file mode 100644
index 000000000..c8fa17b01
--- /dev/null
+++ b/packages/ui/src/components/Input/Input.tsx
@@ -0,0 +1,28 @@
+import { TextField as RT } from '@radix-ui/themes';
+
+import { createComponent, withNamespace } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type InputProps = PropsOf;
+export type InputSlotProps = PropsOf;
+export type InputFieldProps = PropsOf;
+
+export const InputRoot = createComponent({
+ id: 'Input',
+ baseElement: RT.Root,
+});
+
+export const InputSlot = createComponent({
+ id: 'InputSlot',
+ baseElement: RT.Slot,
+});
+
+export const InputField = createComponent({
+ id: 'InputField',
+ baseElement: RT.Input,
+});
+
+export const Input = withNamespace(InputRoot, {
+ Slot: InputSlot,
+ Field: InputField,
+});
diff --git a/packages/ui/src/components/Input/index.tsx b/packages/ui/src/components/Input/index.tsx
new file mode 100644
index 000000000..4e9f9d83f
--- /dev/null
+++ b/packages/ui/src/components/Input/index.tsx
@@ -0,0 +1,3 @@
+export { Input, InputField, InputRoot, InputSlot } from './Input';
+
+export type { InputFieldProps, InputProps, InputSlotProps } from './Input';
diff --git a/packages/ui/src/components/InputPassword/InputPassword.stories.tsx b/packages/ui/src/components/InputPassword/InputPassword.stories.tsx
new file mode 100644
index 000000000..76a52de79
--- /dev/null
+++ b/packages/ui/src/components/InputPassword/InputPassword.stories.tsx
@@ -0,0 +1,17 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { InputPassword } from './InputPassword';
+
+const meta: Meta = {
+ title: 'Form/InputPassword',
+ component: InputPassword,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+ ),
+};
diff --git a/packages/ui/src/components/InputPassword/InputPassword.tsx b/packages/ui/src/components/InputPassword/InputPassword.tsx
new file mode 100644
index 000000000..6471f8db4
--- /dev/null
+++ b/packages/ui/src/components/InputPassword/InputPassword.tsx
@@ -0,0 +1,37 @@
+import { IconEye, IconEyeOff, IconLock } from '@tabler/icons-react';
+import { useState } from 'react';
+
+import { createComponent } from '../../utils/component';
+import { Icon } from '../Icon/Icon';
+import { IconButton } from '../IconButton/IconButton';
+import type { InputFieldProps, InputProps } from '../Input/Input';
+import { Input } from '../Input/Input';
+
+export type InputPasswordProps = InputProps & Omit;
+
+export const InputPassword = createComponent({
+ id: 'InputPassword',
+ render: (_, { size, className, variant, color, ...props }) => {
+ const [opened, setOpened] = useState(false);
+ const type = opened ? 'text' : 'password';
+ return (
+
+
+
+
+
+
+ setOpened(!opened)}
+ />
+
+
+ );
+ },
+});
diff --git a/packages/ui/src/components/InputPassword/index.tsx b/packages/ui/src/components/InputPassword/index.tsx
new file mode 100644
index 000000000..65b0004c3
--- /dev/null
+++ b/packages/ui/src/components/InputPassword/index.tsx
@@ -0,0 +1,5 @@
+'use client';
+
+export { InputPassword } from './InputPassword';
+
+export type { InputPasswordProps } from './InputPassword';
diff --git a/packages/ui/src/components/Inset/Inset.stories.tsx b/packages/ui/src/components/Inset/Inset.stories.tsx
new file mode 100644
index 000000000..a938e1a1c
--- /dev/null
+++ b/packages/ui/src/components/Inset/Inset.stories.tsx
@@ -0,0 +1,44 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconBrandGithub } from '@tabler/icons-react';
+
+import { Flex, HStack, VStack } from '../Box';
+import { Card } from '../Card/Card';
+import { Icon } from '../Icon/Icon';
+import { Text } from '../Text/Text';
+
+import { Inset } from './Inset';
+
+const meta: Meta = {
+ title: 'UI/Inset',
+ component: Inset,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+ github.com
+
+
+ Official Node.js SDK for interacting with the AcmeCorp API.
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Inset/Inset.tsx b/packages/ui/src/components/Inset/Inset.tsx
new file mode 100644
index 000000000..d3ef7aa6c
--- /dev/null
+++ b/packages/ui/src/components/Inset/Inset.tsx
@@ -0,0 +1,11 @@
+import { Inset as RadixInset } from '@radix-ui/themes';
+
+import { createComponent } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type InsetProps = PropsOf;
+
+export const Inset = createComponent({
+ id: 'Inset',
+ baseElement: RadixInset,
+});
diff --git a/packages/ui/src/components/Inset/index.tsx b/packages/ui/src/components/Inset/index.tsx
new file mode 100644
index 000000000..6e68ee13d
--- /dev/null
+++ b/packages/ui/src/components/Inset/index.tsx
@@ -0,0 +1,3 @@
+export { Inset } from './Inset';
+
+export type { InsetProps } from './Inset';
diff --git a/packages/ui/src/components/Link/Link.stories.tsx b/packages/ui/src/components/Link/Link.stories.tsx
new file mode 100644
index 000000000..eeca5728c
--- /dev/null
+++ b/packages/ui/src/components/Link/Link.stories.tsx
@@ -0,0 +1,33 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Link } from './Link';
+
+const meta: Meta = {
+ title: 'Base/Link',
+ component: Link,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => Link,
+};
+
+export const External: Story = {
+ render: () => (
+
+ Visit our website
+
+ ),
+};
+
+export const Polymorphic: Story = {
+ render: () => (
+
+
+ Visit our website
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Link/Link.tsx b/packages/ui/src/components/Link/Link.tsx
new file mode 100644
index 000000000..16e9f1bdb
--- /dev/null
+++ b/packages/ui/src/components/Link/Link.tsx
@@ -0,0 +1,66 @@
+import { Link as RadixLink } from '@radix-ui/themes';
+import { IconLink } from '@tabler/icons-react';
+import { tv } from 'tailwind-variants';
+import type { VariantProps } from 'tailwind-variants';
+
+import { createComponent } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+import { Icon } from '../Icon/Icon';
+
+const link = tv({
+ variants: {
+ isExternal: {
+ true: 'inline-flex items-center gap-2',
+ },
+ },
+ defaultVariants: {
+ isExternal: false,
+ },
+});
+
+export type LinkVariantProps = VariantProps;
+export type LinkProps = PropsOf &
+ LinkVariantProps & {
+ externalIcon?: React.ComponentType | null;
+ iconSize?: number;
+ };
+
+export const Link = createComponent({
+ id: 'Link',
+ baseElement: RadixLink,
+ render: (
+ Comp,
+ {
+ children,
+ className,
+ isExternal: initIsExternal,
+ externalIcon: ExternalIcon = IconLink,
+ iconSize = 18,
+ ...props
+ },
+ ) => {
+ const isExternal =
+ initIsExternal ||
+ props.href?.startsWith('http') ||
+ props.target === '_blank';
+
+ const classes = link({ isExternal, className });
+ if (isExternal) {
+ return (
+
+
+ {children}
+ {ExternalIcon && (
+
+ )}
+
+
+ );
+ }
+ return (
+
+ {children}
+
+ );
+ },
+});
diff --git a/packages/ui/src/components/Link/index.tsx b/packages/ui/src/components/Link/index.tsx
new file mode 100644
index 000000000..71a51f5a2
--- /dev/null
+++ b/packages/ui/src/components/Link/index.tsx
@@ -0,0 +1,3 @@
+export { Link } from './Link';
+
+export type { LinkProps, LinkVariantProps } from './Link';
diff --git a/packages/ui/src/components/List/List.stories.tsx b/packages/ui/src/components/List/List.stories.tsx
new file mode 100644
index 000000000..31e1f87c9
--- /dev/null
+++ b/packages/ui/src/components/List/List.stories.tsx
@@ -0,0 +1,52 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconCheck } from '@tabler/icons-react';
+
+import { List } from './List';
+
+const meta: Meta = {
+ title: 'Base/List',
+ component: List,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ render: (args) => (
+
+ First item
+ Second item
+ Third item
+
+ ),
+};
+
+export const Unordered: Story = {
+ render: () => (
+
+ First item
+ Second item
+ Third item
+
+ ),
+};
+
+export const Ordered: Story = {
+ render: () => (
+
+ First item
+ Second item
+ Third item
+
+ ),
+};
+
+export const WithIcon: Story = {
+ render: () => (
+
+ First item
+ Second item
+ Third item
+
+ ),
+};
diff --git a/packages/ui/src/components/List/List.tsx b/packages/ui/src/components/List/List.tsx
new file mode 100644
index 000000000..3f0935af4
--- /dev/null
+++ b/packages/ui/src/components/List/List.tsx
@@ -0,0 +1,76 @@
+import { createComponent, withNamespace } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+import { Icon } from '../Icon/Icon';
+import type { TextProps } from '../Text/Text';
+import { Text } from '../Text/Text';
+
+import { styles } from './styles';
+import type { ListContext } from './useListContext';
+import { ListProvider, useListContext } from './useListContext';
+
+export type ListBaseProps = PropsOf<'ul'> & { type?: 'none' | 'ol' | 'ul' };
+export type ListProps = ListContext & ListBaseProps;
+export type ListULProps = Omit;
+export type ListOLProps = Omit;
+export type ListItemProps = PropsOf<'li'>;
+
+export const ListRoot = createComponent({
+ id: 'List',
+ className: ({ className, icon, type = 'none' }) => {
+ return styles({ type, withIcon: Boolean(icon) }).root({ className });
+ },
+ render: (
+ _,
+ { type = 'none', icon, iconColor, iconSize, iconAriaLabel, ...props },
+ ) => {
+ const El = type === 'ol' ? 'ol' : 'ul';
+ return (
+
+
+
+ );
+ },
+});
+
+export const ListItem = createComponent({
+ id: 'ListItem',
+ render: (_, { children, className, ...props }) => {
+ const { icon, iconColor, iconSize, iconAriaLabel } = useListContext();
+ const classes = styles({ withIcon: Boolean(icon) }).item({ className });
+ const iconEl = icon && (
+
+ );
+ return (
+
+ {iconEl} {children}
+
+ );
+ },
+});
+
+export const ListUL = createComponent({
+ id: 'ListUL',
+ baseElement: ListRoot,
+ defaultProps: {
+ type: 'ul',
+ } as ListProps,
+});
+
+export const ListOL = createComponent({
+ id: 'ListOL',
+ baseElement: ListRoot,
+ defaultProps: {
+ type: 'ol',
+ } as ListProps,
+});
+
+export const List = withNamespace(ListRoot, {
+ UL: ListUL,
+ OL: ListOL,
+ Item: ListItem,
+});
diff --git a/packages/ui/src/components/List/index.tsx b/packages/ui/src/components/List/index.tsx
new file mode 100644
index 000000000..493a41328
--- /dev/null
+++ b/packages/ui/src/components/List/index.tsx
@@ -0,0 +1,15 @@
+'use client';
+
+export { List, ListItem, ListOL, ListRoot, ListUL } from './List';
+
+export type {
+ ListBaseProps,
+ ListItemProps,
+ ListOLProps,
+ ListProps,
+ ListULProps,
+} from './List';
+
+export { ListProvider, useListContext } from './useListContext';
+
+export type { ListContext } from './useListContext';
diff --git a/packages/ui/src/components/List/styles.ts b/packages/ui/src/components/List/styles.ts
new file mode 100644
index 000000000..7e2b9718d
--- /dev/null
+++ b/packages/ui/src/components/List/styles.ts
@@ -0,0 +1,31 @@
+import { tv } from 'tailwind-variants';
+
+export const styles = tv({
+ slots: {
+ root: 'm-0 p-0 fuel-[Text]:leading-relaxed',
+ item: 'marker:text-icon',
+ },
+ variants: {
+ type: {
+ ol: {
+ root: 'list-decimal pl-4',
+ },
+ ul: {
+ root: 'list-disc pl-4',
+ },
+ none: {
+ root: 'list-none',
+ },
+ },
+ withIcon: {
+ true: {
+ root: 'list-none',
+ item: 'flex items-center gap-2',
+ },
+ },
+ },
+ defaultVariants: {
+ type: 'ul',
+ withIcon: false,
+ },
+});
diff --git a/packages/ui/src/components/List/useListContext.tsx b/packages/ui/src/components/List/useListContext.tsx
new file mode 100644
index 000000000..7bc5148ee
--- /dev/null
+++ b/packages/ui/src/components/List/useListContext.tsx
@@ -0,0 +1,21 @@
+'use client';
+
+import { createContext, useContext } from 'react';
+
+import type { Colors } from '../../utils/types';
+import type { IconProps } from '../Icon/Icon';
+
+export type ListContext = {
+ icon?: IconProps['icon'];
+ iconColor?: Colors;
+ iconSize?: number;
+ iconAriaLabel?: string;
+};
+
+const ctx = createContext({} as ListContext);
+
+export function useListContext() {
+ return useContext(ctx);
+}
+
+export const ListProvider = ctx.Provider;
diff --git a/packages/ui/src/components/Nav/Nav.stories.tsx b/packages/ui/src/components/Nav/Nav.stories.tsx
new file mode 100644
index 000000000..9704fa977
--- /dev/null
+++ b/packages/ui/src/components/Nav/Nav.stories.tsx
@@ -0,0 +1,109 @@
+import { action } from '@storybook/addon-actions';
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { Nav } from './Nav';
+
+const meta: Meta = {
+ title: 'Layout/Nav',
+ component: Nav,
+ parameters: {
+ layout: 'fullscreen',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+const ACCOUNT =
+ 'fuel10va6297tkerdcn5u8mxjm9emudsmkj85pq5x7t7stkmzmc4nvs3qvn99qz';
+
+const NETWORK = {
+ id: '1',
+ name: 'Mainnet',
+ url: 'https://mainnet.fuel.sh',
+};
+
+export const Usage: Story = {
+ render: () => (
+
+ ),
+};
+
+export const NoConnection: Story = {
+ render: () => (
+
+ ),
+};
+
+export const Mobile: Story = {
+ render: () => (
+
+ ),
+ parameters: {
+ viewport: {
+ defaultViewport: 'iphonex',
+ },
+ },
+};
diff --git a/packages/ui/src/components/Nav/Nav.tsx b/packages/ui/src/components/Nav/Nav.tsx
new file mode 100644
index 000000000..1bbf0b831
--- /dev/null
+++ b/packages/ui/src/components/Nav/Nav.tsx
@@ -0,0 +1,447 @@
+import {
+ IconMenu2,
+ IconMoonFilled,
+ IconSunFilled,
+ IconWallet,
+ IconX,
+} from '@tabler/icons-react';
+import { AnimatePresence, motion } from 'framer-motion';
+import { Children, cloneElement, useEffect, useState } from 'react';
+import { useWindowSize } from 'react-use';
+
+import { useStrictedChildren } from '../../hooks/useStrictedChildren';
+import { createComponent, withNamespace } from '../../utils/component';
+import type { AsChildProp, PropsOf, WithAsProps } from '../../utils/types';
+import { Badge } from '../Badge/Badge';
+import { Box, HStack } from '../Box';
+import type { BoxProps, HStackProps } from '../Box';
+import { Button } from '../Button/Button';
+import { FuelLogo } from '../FuelLogo/FuelLogo';
+import type { FuelLogoProps } from '../FuelLogo/FuelLogo';
+import { Icon } from '../Icon/Icon';
+import { IconButton } from '../IconButton';
+import type { LinkProps } from '../Link/Link';
+import { Link } from '../Link/Link';
+import { useTheme } from '../Theme/useTheme';
+
+import { styles } from './styles';
+import { NavProvider, useNavContext } from './useNavContext';
+import { NavMobileProvider, useNavMobileContext } from './useNavMobileContext';
+
+/**
+ * Types
+ */
+
+export type NetworkObj = {
+ id?: string;
+ name: string;
+ url: string;
+};
+
+export type NavProps = {
+ network?: NetworkObj;
+ account?: string;
+ onConnect?: () => void;
+ children?: React.ReactNode;
+};
+
+export type NavLogoProps = FuelLogoProps;
+export type NavMenuProps = HStackProps;
+export type NavMenuItemProps = LinkProps & { isActive?: boolean };
+export type NavDesktopProps = PropsOf<'nav'>;
+
+export type NavConnectionProps = HStackProps & {
+ whenOpened?: 'hide' | 'show' | 'no-effect';
+};
+
+export type NavThemeToggleProps = AsChildProp &
+ PropsOf<'span'> & { whenOpened?: 'hide' | 'show' | 'no-effect' };
+
+export type NavMobileProps = WithAsProps &
+ PropsOf<'nav'> & {
+ isOpen?: boolean;
+ onOpenChange?: (isOpen: boolean) => void;
+ };
+
+export type NavMobileContentProps = WithAsProps & BoxProps;
+
+/**
+ * NavRoot
+ */
+
+const ROOT_CHILD_ITEMS = ['NavDesktop', 'NavMobile'];
+
+export const NavRoot = createComponent({
+ id: 'Nav',
+ render: (_, { network, account, onConnect, children }) => {
+ const newChildren = useStrictedChildren('Nav', ROOT_CHILD_ITEMS, children);
+ return (
+
+ {newChildren}
+
+ );
+ },
+});
+
+/**
+ * NavDesktop
+ */
+
+const DESKTOP_CHILD_ITEMS = [
+ 'NavLogo',
+ 'NavMenu',
+ 'NavSpacer',
+ 'NavConnection',
+ 'NavThemeToggle',
+];
+
+export const NavDesktop = createComponent({
+ id: 'NavDesktop',
+ baseElement: 'nav',
+ render: (Root, { className, ...props }) => {
+ const classes = styles();
+ const { width } = useWindowSize();
+ const children = useStrictedChildren(
+ 'NavDesktop',
+ DESKTOP_CHILD_ITEMS,
+ props.children,
+ );
+
+ if (width < 1024) return null;
+ return (
+
+ );
+ },
+});
+
+/**
+ * NavMobile
+ */
+
+const MOBILE_CHILD_ITEMS = [
+ 'NavLogo',
+ 'NavMenu',
+ 'NavSpacer',
+ 'NavConnection',
+ 'NavThemeToggle',
+ 'NavMobileContent',
+];
+
+export const NavMobile = createComponent({
+ id: 'NavMobile',
+ baseElement: 'nav',
+ className: () => styles().mobile(),
+ render: (Root, { isOpen, onOpenChange, ...props }) => {
+ const { width } = useWindowSize();
+ const [open, setOpen] = useState(() => Boolean(isOpen));
+ const children = useStrictedChildren(
+ 'NavMobile',
+ MOBILE_CHILD_ITEMS,
+ props.children,
+ );
+
+ useEffect(() => {
+ onOpenChange?.(Boolean(open));
+ }, [open]);
+
+ if (width >= 1024) return null;
+ return (
+
+
+ {children}
+
+
+ );
+ },
+});
+
+export const NavMobileContent = createComponent<
+ NavMobileContentProps,
+ 'header'
+>({
+ id: 'NavMobileContent',
+ baseElement: 'header',
+ className: () => styles().mobileContent(),
+ render: (Root, { children, ...props }) => {
+ const { isOpen, onOpenChange } = useNavMobileContext();
+
+ return (
+
+
+ {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}
+ {Children.toArray(children).map((child: any) => {
+ return cloneElement(child, { key: child.type.id });
+ })}
+
+ onOpenChange((s) => !s)}
+ />
+
+ );
+ },
+});
+
+/**
+ * NavSpacer
+ */
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+export const NavSpacer = createComponent<{}, 'hr'>({
+ id: 'NavSpacer',
+ baseElement: 'hr',
+ className: 'flex-1 opacity-0',
+});
+
+/**
+ * NavLogo
+ */
+
+export const NavLogo = createComponent({
+ id: 'NavLogo',
+ className: () => styles().logo(),
+ render: (_, { size, ...props }) => {
+ const { width } = useWindowSize();
+ const defaultSize = width < 1024 ? 28 : 32;
+ return ;
+ },
+});
+
+/**
+ * NavMenu
+ */
+
+export const NavMenu = createComponent({
+ id: 'NavMenu',
+ className: () => styles().menu(),
+ render: (Root, props) => {
+ const mobileProps = useNavMobileContext();
+ const content = ;
+
+ if (!mobileProps?.onOpenChange) {
+ return content;
+ }
+
+ return (
+
+ {mobileProps.isOpen && (
+
+ {content}
+
+ )}
+
+ );
+ },
+});
+
+/**
+ * NavMenuItem
+ */
+
+export const NavMenuItem = createComponent({
+ id: 'NavMenuItem',
+ baseElement: Link,
+ className: () => styles().menuItem(),
+ render: (Comp, { isActive, ...props }) => {
+ return ;
+ },
+});
+
+/**
+ * NavConnection
+ */
+const MotionHStack = motion(HStack);
+
+export const NavConnection = createComponent(
+ {
+ id: 'NavConnection',
+ className: () => styles().navConnection(),
+ render: (_, { whenOpened = 'show', ...props }) => {
+ const navProps = useNavContext();
+ const mobileProps = useNavMobileContext();
+ const hasProps = navProps.network || navProps.account;
+ const connectButton = (
+
+ );
+
+ const content = (
+ <>
+ {navProps.network && (
+
+
+ {navProps.network.name}
+
+ )}
+ {/* {navProps.account && ( */}
+ {/* */}
+ {/* )} */}
+ >
+ );
+
+ if (!mobileProps?.onOpenChange && !hasProps) {
+ return connectButton;
+ }
+ if (!mobileProps?.onOpenChange || whenOpened === 'no-effect') {
+ return {content};
+ }
+
+ const animContent = (
+
+ {content}
+
+ );
+
+ return (
+ <>
+ {!mobileProps.isOpen && whenOpened === 'hide' && animContent}
+ {mobileProps.isOpen && whenOpened === 'show' && animContent}
+ >
+ );
+ },
+ },
+);
+
+/**
+ * NavThemeToggle
+ */
+
+export const NavThemeToggle = createComponent({
+ id: 'NavThemeToggle',
+ baseElement: 'span',
+ render: (Root, { className, whenOpened = 'hide', ...props }) => {
+ const { theme: current, toggleTheme } = useTheme();
+ const mobileProps = useNavMobileContext();
+ const classes = styles();
+ const content = (
+
+
+
+
+ );
+
+ if (!mobileProps?.onOpenChange || whenOpened === 'no-effect') {
+ return content;
+ }
+
+ const animContent = (
+
+ {content}
+
+ );
+
+ return (
+ <>
+ {!mobileProps.isOpen && whenOpened === 'hide' && animContent}
+ {mobileProps.isOpen && whenOpened === 'show' && animContent}
+ >
+ );
+ },
+});
+
+/**
+ * Nav
+ */
+
+export const Nav = withNamespace(NavRoot, {
+ Desktop: NavDesktop,
+ Mobile: NavMobile,
+ Logo: NavLogo,
+ Menu: NavMenu,
+ MenuItem: NavMenuItem,
+ Spacer: NavSpacer,
+ Connection: NavConnection,
+ ThemeToggle: NavThemeToggle,
+ MobileContent: NavMobileContent,
+});
diff --git a/packages/ui/src/components/Nav/index.tsx b/packages/ui/src/components/Nav/index.tsx
new file mode 100644
index 000000000..d01a77778
--- /dev/null
+++ b/packages/ui/src/components/Nav/index.tsx
@@ -0,0 +1,31 @@
+'use client';
+
+export {
+ Nav,
+ NavConnection,
+ NavDesktop,
+ NavLogo,
+ NavMenu,
+ NavMenuItem,
+ NavMobile,
+ NavMobileContent,
+ NavRoot,
+ NavSpacer,
+ NavThemeToggle,
+} from './Nav';
+
+export type {
+ NavConnectionProps,
+ NavDesktopProps,
+ NavLogoProps,
+ NavMenuItemProps,
+ NavMenuProps,
+ NavMobileContentProps,
+ NavMobileProps,
+ NavProps,
+ NavThemeToggleProps,
+ NetworkObj,
+} from './Nav';
+
+export { NavMobileProvider, useNavMobileContext } from './useNavMobileContext';
+export { NavProvider, useNavContext } from './useNavContext';
diff --git a/packages/ui/src/components/Nav/styles.ts b/packages/ui/src/components/Nav/styles.ts
new file mode 100644
index 000000000..76a361cbd
--- /dev/null
+++ b/packages/ui/src/components/Nav/styles.ts
@@ -0,0 +1,54 @@
+import { tv } from 'tailwind-variants';
+
+export const styles = tv({
+ slots: {
+ logo: 'items-center justify-start',
+ menu: [
+ 'flex gap-1 md:flex-col py-3 px-4 laptop:px-0 laptop:gap-4 laptop:flex-row',
+ 'not-first:border-t not-first:border-border laptop:not-first:border-t-0',
+ ],
+ menuItem: [
+ 'relative h-auto text-color data-[active=true]:text-brand hover:text-brand',
+ 'laptop:data-[active=true]:before:content-[""]',
+ 'laptop:data-[active=true]:before:absolute',
+ 'laptop:data-[active=true]:before:block',
+ 'laptop:data-[active=true]:before:w-full',
+ 'laptop:data-[active=true]:before:h-1',
+ 'laptop:data-[active=true]:before:bg-brand',
+ 'laptop:data-[active=true]:before:top-[-24px]',
+ ],
+ navConnection: 'items-center',
+ navNetwork: 'h-8',
+ themeToggle: [
+ 'relative cursor-pointer flex items-center px-2 w-12 h-8 rounded-full border-border',
+ 'bg-gray-3 select-none',
+ ],
+ themeToggleIcon: [
+ 'absolute opacity-100 transition-all duration-200 text-icon transform',
+ 'aria-[label=Sun]:right-2',
+ 'aria-[label=Moon]:left-2',
+ 'dark-theme:aria-[label=Sun]:transform',
+ 'dark-theme:aria-[label=Sun]:opacity-0',
+ 'dark-theme:aria-[label=Sun]:-translate-x-full',
+ 'light-theme:aria-[label=Moon]:transform',
+ 'light-theme:aria-[label=Moon]:opacity-0',
+ 'light-theme:aria-[label=Moon]:translate-x-full',
+ ],
+ desktop: [
+ 'gap-8 container mx-auto flex-row items-center',
+ 'laptop:px-4 laptop:flex min-h-[var(--nav-height)]',
+ ],
+ mobile: [
+ 'flex-col border-b border-border',
+ 'laptop:hidden fuel-[NavLogo]:flex-1',
+ ],
+ navWrapper: 'border-b border-border min-h-[var(--nav-height)]',
+ mobileContent: [
+ 'flex items-center py-2 px-4 border-b border-transparent',
+ 'transition-colors duration-200 ease-in-out',
+ 'min-h-[var(--nav-height)] data-[open=true]:border-border',
+ 'fuel-[NavLogo]:flex-1',
+ 'fuel-[IconButton]:ml-2 fuel-[IconButton]:text-icon',
+ ],
+ },
+});
diff --git a/packages/ui/src/components/Nav/useNavContext.tsx b/packages/ui/src/components/Nav/useNavContext.tsx
new file mode 100644
index 000000000..fd864a3c8
--- /dev/null
+++ b/packages/ui/src/components/Nav/useNavContext.tsx
@@ -0,0 +1,15 @@
+'use client';
+
+import { createContext, useContext } from 'react';
+
+import type { NavProps } from './Nav';
+
+type ContextProps = NavProps;
+
+// TODO: put this inside the component
+const ctx = createContext({} as ContextProps);
+export function useNavContext() {
+ return useContext(ctx);
+}
+
+export const NavProvider = ctx.Provider;
diff --git a/packages/ui/src/components/Nav/useNavMobileContext.tsx b/packages/ui/src/components/Nav/useNavMobileContext.tsx
new file mode 100644
index 000000000..badd97078
--- /dev/null
+++ b/packages/ui/src/components/Nav/useNavMobileContext.tsx
@@ -0,0 +1,15 @@
+'use client';
+
+import { createContext, useContext } from 'react';
+
+type ContextProps = {
+ isOpen: boolean;
+ onOpenChange: React.Dispatch>;
+};
+
+const ctx = createContext({} as ContextProps);
+export function useNavMobileContext() {
+ return useContext(ctx);
+}
+
+export const NavMobileProvider = ctx.Provider;
diff --git a/packages/ui/src/components/Popover/Popover.stories.tsx b/packages/ui/src/components/Popover/Popover.stories.tsx
new file mode 100644
index 000000000..a666d09e3
--- /dev/null
+++ b/packages/ui/src/components/Popover/Popover.stories.tsx
@@ -0,0 +1,62 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+import type { Meta, StoryObj } from '@storybook/react';
+import { IconMessageDots } from '@tabler/icons-react';
+
+import { Avatar } from '../Avatar/Avatar';
+import { Box, Flex, HStack } from '../Box';
+import { Button } from '../Button/Button';
+import { Checkbox } from '../Checkbox/Checkbox';
+import { Icon } from '../Icon/Icon';
+import { Text } from '../Text/Text';
+import { TextArea } from '../TextArea/TextArea';
+
+import { Popover } from './Popover';
+
+const meta: Meta = {
+ title: 'Overlay/Popover',
+ component: Popover,
+ parameters: {
+ layout: 'centered',
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+};
diff --git a/packages/ui/src/components/Popover/Popover.tsx b/packages/ui/src/components/Popover/Popover.tsx
new file mode 100644
index 000000000..693fe02ea
--- /dev/null
+++ b/packages/ui/src/components/Popover/Popover.tsx
@@ -0,0 +1,43 @@
+import { Popover as RP } from '@radix-ui/themes';
+
+import { createComponent, withNamespace } from '../../utils/component';
+import type { PropsOf } from '../../utils/types';
+
+export type PopoverProps = PropsOf;
+export type PopoverTriggerProps = PropsOf;
+export type PopoverContentProps = PropsOf;
+export type PopoverCloseProps = PropsOf;
+
+export const PopoverRoot = createComponent({
+ id: 'Popover',
+ baseElement: RP.Root,
+});
+
+export const PopoverTrigger = createComponent<
+ PopoverTriggerProps,
+ typeof RP.Trigger
+>({
+ id: 'PopoverTrigger',
+ baseElement: RP.Trigger,
+});
+
+export const PopoverContent = createComponent<
+ PopoverContentProps,
+ typeof RP.Content
+>({
+ id: 'PopoverContent',
+ baseElement: RP.Content,
+});
+
+export const PopoverClose = createComponent(
+ {
+ id: 'PopoverClose',
+ baseElement: RP.Close,
+ },
+);
+
+export const Popover = withNamespace(PopoverRoot, {
+ Trigger: PopoverTrigger,
+ Content: PopoverContent,
+ Close: PopoverClose,
+});
diff --git a/packages/ui/src/components/Popover/index.tsx b/packages/ui/src/components/Popover/index.tsx
new file mode 100644
index 000000000..1a0b56bc5
--- /dev/null
+++ b/packages/ui/src/components/Popover/index.tsx
@@ -0,0 +1,14 @@
+export {
+ Popover,
+ PopoverClose,
+ PopoverContent,
+ PopoverRoot,
+ PopoverTrigger,
+} from './Popover';
+
+export type {
+ PopoverCloseProps,
+ PopoverContentProps,
+ PopoverProps,
+ PopoverTriggerProps,
+} from './Popover';
diff --git a/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx b/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx
new file mode 100644
index 000000000..ebaba9b8e
--- /dev/null
+++ b/packages/ui/src/components/RadioGroup/RadioGroup.stories.tsx
@@ -0,0 +1,42 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { HStack, VStack } from '../Box';
+import { Text } from '../Text/Text';
+
+import { RadioGroup } from './RadioGroup';
+
+const meta: Meta = {
+ title: 'Form/RadioGroup',
+ component: RadioGroup,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Usage: Story = {
+ render: () => (
+
+
+