diff --git a/apps/web/src/index.tsx b/apps/web/src/index.tsx
index bb432202046..ebef315a23e 100644
--- a/apps/web/src/index.tsx
+++ b/apps/web/src/index.tsx
@@ -6,6 +6,8 @@ import { initializeApp } from './initializeApp';
import reportWebVitals from './reportWebVitals';
import { LAUNCH_DARKLY_CLIENT_SIDE_ID } from '@novu/shared-web';
+// TODO: would like to figure out a better solution, but this unblocks for now
+import '@novu/novui/components.css';
import '@novu/novui/styles.css';
(async () => {
diff --git a/apps/web/src/studio/components/workflows/WorkflowsListPage.tsx b/apps/web/src/studio/components/workflows/WorkflowsListPage.tsx
index ec3c94ce7dd..98fbf0aaf07 100644
--- a/apps/web/src/studio/components/workflows/WorkflowsListPage.tsx
+++ b/apps/web/src/studio/components/workflows/WorkflowsListPage.tsx
@@ -1,9 +1,11 @@
+import { Button } from '@novu/novui';
import { PageContainer } from '../../layout';
import { WorkflowsTable } from './table';
export const WorkflowsListPage = () => {
return (
+
);
diff --git a/libs/novui/.storybook/preview.tsx b/libs/novui/.storybook/preview.tsx
index 75698172913..73fad068c87 100644
--- a/libs/novui/.storybook/preview.tsx
+++ b/libs/novui/.storybook/preview.tsx
@@ -5,9 +5,7 @@ import { css } from '../styled-system/css';
import { MantineThemeProvider } from '@mantine/core';
import { NovuiProvider } from '../src/components';
-import '@mantine/core/styles.css';
-// Bring in the Panda-generated stylesheets
-import '../src/index.css';
+import '../styles.css';
export const parameters: Parameters = {
layout: 'fullscreen',
diff --git a/libs/novui/package.json b/libs/novui/package.json
index 511662a3952..818aa93ae75 100644
--- a/libs/novui/package.json
+++ b/libs/novui/package.json
@@ -51,7 +51,8 @@
"require": "./styled-system/jsx/index.js",
"import": "./styled-system/jsx/index.js"
},
- "./styles.css": "./styled-system/styles.css"
+ "./styles.css": "./styled-system/styles.css",
+ "./components.css": "./node_modules/@mantine/core/styles.css"
},
"scripts": {
"prepare:lib": "pnpm prepare:panda && pnpm prepare:audit",
diff --git a/libs/novui/src/components/NovuiProvider.tsx b/libs/novui/src/components/NovuiProvider.tsx
index e15b0f4ef24..d85900ccd90 100644
--- a/libs/novui/src/components/NovuiProvider.tsx
+++ b/libs/novui/src/components/NovuiProvider.tsx
@@ -1,12 +1,14 @@
import { MantineProvider } from '@mantine/core';
import { FC, PropsWithChildren } from 'react';
import { IconProvider } from '../icons/IconProvider';
+import { MANTINE_THEME } from './mantine-theme.config';
+
type INovuiProviderProps = PropsWithChildren;
/** Used to export a v7 Mantine provider */
export const NovuiProvider: FC = ({ children }) => {
return (
-
+
{children}
);
diff --git a/libs/novui/src/components/button/Button.stories.tsx b/libs/novui/src/components/button/Button.stories.tsx
new file mode 100644
index 00000000000..4ae5848d0e2
--- /dev/null
+++ b/libs/novui/src/components/button/Button.stories.tsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import { StoryFn, Meta } from '@storybook/react';
+import { Button } from './Button';
+import { Flex } from '../../../styled-system/jsx';
+import { Icon10K, IconInfo } from '../../icons';
+
+export default {
+ title: 'Components/Button',
+ component: Button,
+ argTypes: {},
+} as Meta;
+
+const Template: StoryFn = ({ ...args }) => ;
+
+export const Default = Template.bind({});
+Default.args = {};
+
+export const Loading = Template.bind({});
+Loading.args = {
+ loading: true,
+};
+
+export const icon = () => (
+
+
+
+
+);
+
+export const filled = () => (
+
+
+
+
+);
+
+export const outline = () => (
+
+
+
+
+);
+
+export const disabled = () => (
+
+
+
+
+);
diff --git a/libs/novui/src/components/button/Button.tsx b/libs/novui/src/components/button/Button.tsx
new file mode 100644
index 00000000000..d0c0e98c99d
--- /dev/null
+++ b/libs/novui/src/components/button/Button.tsx
@@ -0,0 +1,25 @@
+import { FC } from 'react';
+import { CorePropsWithChildren } from '../../types';
+import { Button as ExternalButton, ButtonProps } from '@mantine/core';
+import { IconType } from '../../icons';
+import { css } from '../../../styled-system/css';
+
+export interface IButtonProps
+ extends CorePropsWithChildren,
+ React.ButtonHTMLAttributes,
+ Pick {
+ Icon?: IconType;
+}
+
+export const Button: FC = ({ children, Icon, size = 'md', variant = 'filled', ...buttonProps }) => {
+ return (
+ : undefined}
+ variant={variant}
+ {...buttonProps}
+ >
+ {children}
+
+ );
+};
diff --git a/libs/novui/src/components/button/index.ts b/libs/novui/src/components/button/index.ts
new file mode 100644
index 00000000000..8b166a86e4d
--- /dev/null
+++ b/libs/novui/src/components/button/index.ts
@@ -0,0 +1 @@
+export * from './Button';
diff --git a/libs/novui/src/components/index.ts b/libs/novui/src/components/index.ts
index c67e1b4c37b..4456b720ed9 100644
--- a/libs/novui/src/components/index.ts
+++ b/libs/novui/src/components/index.ts
@@ -1,3 +1,4 @@
export * from './NovuiProvider';
export * from './table';
export * from './Test';
+export * from './button';
diff --git a/libs/novui/src/components/mantine-theme.config.ts b/libs/novui/src/components/mantine-theme.config.ts
new file mode 100644
index 00000000000..850d11490d5
--- /dev/null
+++ b/libs/novui/src/components/mantine-theme.config.ts
@@ -0,0 +1,87 @@
+import { MantineColorsTuple, MantineThemeOverride } from '@mantine/core';
+import { COLOR_PALETTE_TOKENS } from '../tokens/colors.tokens';
+import { token, Token } from '../../styled-system/tokens';
+
+/**
+ * Generates a Mantine color tuple for the given Panda color "family"
+ */
+const generateMantineColorTokens = (colorFamily: keyof typeof COLOR_PALETTE_TOKENS): MantineColorsTuple => {
+ return Object.keys(COLOR_PALETTE_TOKENS[colorFamily]).map((paletteNumber) =>
+ token(`colors.${colorFamily}.${paletteNumber}.dark` as Token)
+ ) as unknown as MantineColorsTuple;
+};
+
+/** Maps Panda token values to a mantine theme config */
+export const MANTINE_THEME: MantineThemeOverride = {
+ // colors
+ white: token('colors.legacy.white'),
+ black: token('colors.legacy.black'),
+ primaryColor: 'gradient',
+ primaryShade: 6,
+ colors: {
+ gray: generateMantineColorTokens('mauve'),
+ yellow: generateMantineColorTokens('amber'),
+ blue: generateMantineColorTokens('blue'),
+ green: generateMantineColorTokens('green'),
+ red: generateMantineColorTokens('red'),
+ // must have a tuple of 10 strings, but replace the value at primaryShade with our gradient
+ gradient: ['', '', '', '', '', '', token('gradients.horizontal'), '', '', ''],
+ },
+
+ // typography
+ fontFamily: token('fonts.system'),
+ fontFamilyMonospace: token('fonts.mono'),
+ lineHeights: {
+ sm: token('lineHeights.100'),
+ md: token('lineHeights.125'),
+ lg: token('lineHeights.150'),
+ // missing 175
+ xl: token('lineHeights.200'),
+ },
+ headings: {
+ fontFamily: token('fonts.system'),
+ fontWeight: token('fontWeights.strong'),
+ sizes: {
+ // page title
+ h1: {
+ fontSize: token('fontSizes.150'),
+ lineHeight: token('lineHeights.200'),
+ },
+ // section title
+ h2: {
+ fontSize: token('fontSizes.125'),
+ lineHeight: token('lineHeights.175'),
+ },
+ // subsection title
+ h3: {
+ fontSize: token('fontSizes.100'),
+ lineHeight: token('lineHeights.150'),
+ },
+ },
+ },
+
+ // TODO: these are guesses for how they match up
+ spacing: {
+ xs: token('spacing.25'),
+ sm: token('spacing.50'),
+ md: token('spacing.100'),
+ lg: token('spacing.150'),
+ xl: token('spacing.200'),
+ xxl: token('spacing.250'),
+ xxxl: token('spacing.300'),
+ },
+ radius: {
+ xs: token('radii.xs'),
+ sm: token('radii.s'),
+ md: token('radii.m'),
+ lg: token('radii.l'),
+ },
+ defaultRadius: 'md',
+ shadows: {
+ // TODO: this makes no sense except for md
+ sm: token('shadows.light'),
+ md: token('shadows.medium'),
+ lg: token('shadows.dark'),
+ xl: token('shadows.color'),
+ },
+};