diff --git a/.eslintrc.json b/.eslintrc.json
index 3722418..2b5b39b 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,3 +1,7 @@
{
- "extends": ["next/core-web-vitals", "next/typescript"]
-}
+ "extends": [
+ "next/core-web-vitals",
+ "next/typescript",
+ "plugin:storybook/recommended"
+ ]
+}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 26b002a..fb95c82 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,3 +38,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+*storybook.log
+storybook-static
diff --git a/.storybook/main.ts b/.storybook/main.ts
new file mode 100644
index 0000000..f8de731
--- /dev/null
+++ b/.storybook/main.ts
@@ -0,0 +1,19 @@
+import type { StorybookConfig } from '@storybook/nextjs';
+
+const config: StorybookConfig = {
+ stories: ['../components/**/*stories.@(js|jsx|ts|tsx|mdx)'],
+ addons: [
+ '@storybook/addon-essentials',
+ '@storybook/addon-a11y',
+ '@storybook/addon-docs',
+ ],
+ framework: {
+ name: '@storybook/nextjs',
+ options: {},
+ },
+ docs: {
+ autodocs: 'tag',
+ },
+};
+
+export default config;
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
new file mode 100644
index 0000000..c75b301
--- /dev/null
+++ b/.storybook/preview.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import { withThemeByClassName } from '@storybook/addon-styling';
+import '../app/globals.css';
+
+export const decorators = [
+ (Story: any) => (
+
+
+
+ ),
+];
+
+export const parameters = {
+ actions: { argTypesRegex: '^on[A-Z].*' },
+ controls: { matchers: { color: /(background|color)$/i, date: /Date$/ } },
+};
diff --git a/.storybook/story-template.tsx b/.storybook/story-template.tsx
new file mode 100644
index 0000000..bd243a2
--- /dev/null
+++ b/.storybook/story-template.tsx
@@ -0,0 +1,11 @@
+import React from 'react';
+
+type ModuleType = Record;
+
+export function Template({ module, props }: { module: ModuleType; props?: any }) {
+ const Component = module?.Root || module?.default || Object.values(module).find((v) => typeof v === 'function');
+ if (!Component) return null;
+ return ;
+}
+
+export default Template;
diff --git a/app/globals.css b/app/globals.css
index 8ec8b37..cce712f 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,3 +1,4 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;700;1,400&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -390,3 +391,8 @@
transform: scale(0.8996);
transform-origin: center;
}
+
+/* Set default font to Inter */
+html, body {
+ font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
+}
diff --git a/components/ui/accordion.stories.tsx b/components/ui/accordion.stories.tsx
new file mode 100644
index 0000000..ceaaafe
--- /dev/null
+++ b/components/ui/accordion.stories.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import * as Module from './accordion';
+import Template from '../../.storybook/story-template';
+
+export default { title: 'UI/Accordion' };
+
+export const Default = {
+ render: () => ,
+};
diff --git a/components/ui/alert.stories.tsx b/components/ui/alert.stories.tsx
new file mode 100644
index 0000000..a308b62
--- /dev/null
+++ b/components/ui/alert.stories.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import * as Module from './alert';
+import Template from '../../.storybook/story-template';
+
+export default { title: 'UI/Alert' };
+
+export const Default = {
+ render: () => ,
+};
diff --git a/components/ui/avatar-empty-icons.stories.tsx b/components/ui/avatar-empty-icons.stories.tsx
new file mode 100644
index 0000000..fc347e8
--- /dev/null
+++ b/components/ui/avatar-empty-icons.stories.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import * as Module from './avatar-empty-icons';
+import Template from '../../.storybook/story-template';
+
+export default { title: 'UI/AvatarEmptyIcons' };
+
+export const Default = {
+ render: () => ,
+};
diff --git a/components/ui/avatar-group-compact.stories.tsx b/components/ui/avatar-group-compact.stories.tsx
new file mode 100644
index 0000000..9885d8e
--- /dev/null
+++ b/components/ui/avatar-group-compact.stories.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import * as Module from './avatar-group-compact';
+import Template from '../../.storybook/story-template';
+
+export default { title: 'UI/AvatarGroupCompact' };
+
+export const Default = {
+ render: () => ,
+};
diff --git a/components/ui/avatar-group.stories.tsx b/components/ui/avatar-group.stories.tsx
new file mode 100644
index 0000000..f9691c5
--- /dev/null
+++ b/components/ui/avatar-group.stories.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import * as Module from './avatar-group';
+import Template from '../../.storybook/story-template';
+
+export default { title: 'UI/AvatarGroup' };
+
+export const Default = {
+ render: () => ,
+};
diff --git a/components/ui/avatar.stories.tsx b/components/ui/avatar.stories.tsx
new file mode 100644
index 0000000..05eb8d8
--- /dev/null
+++ b/components/ui/avatar.stories.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import * as Module from './avatar';
+import Template from '../../.storybook/story-template';
+
+export default { title: 'UI/Avatar' };
+
+export const Default = {
+ render: () => ,
+};
diff --git a/components/ui/badge.stories.tsx b/components/ui/badge.stories.tsx
new file mode 100644
index 0000000..4cc9d49
--- /dev/null
+++ b/components/ui/badge.stories.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import * as Module from './badge';
+import Template from '../../.storybook/story-template';
+
+export default { title: 'UI/Badge' };
+
+export const Default = {
+ render: () => ,
+};
diff --git a/components/ui/breadcrumb.stories.tsx b/components/ui/breadcrumb.stories.tsx
new file mode 100644
index 0000000..34d70b9
--- /dev/null
+++ b/components/ui/breadcrumb.stories.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import * as Module from './breadcrumb';
+import Template from '../../.storybook/story-template';
+
+export default { title: 'UI/Breadcrumb' };
+
+export const Default = {
+ render: () => ,
+};
diff --git a/components/ui/button-group.stories.tsx b/components/ui/button-group.stories.tsx
new file mode 100644
index 0000000..bf4756c
--- /dev/null
+++ b/components/ui/button-group.stories.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import * as Module from './button-group';
+import Template from '../../.storybook/story-template';
+
+export default { title: 'UI/ButtonGroup' };
+
+export const Default = {
+ render: () => ,
+};
diff --git a/components/ui/button.stories.tsx b/components/ui/button.stories.tsx
new file mode 100644
index 0000000..4b9762b
--- /dev/null
+++ b/components/ui/button.stories.tsx
@@ -0,0 +1,98 @@
+import type { Meta, StoryObj } from '@storybook/react';
+import { Root } from './button'; // ← این مهمه: Root نه Button
+import { Icon } from './button';
+
+// یا میتونی اینجوری هم بنویسی (هر دو درسته):
+// import { Root as Button, Icon } from './button';
+
+const meta = {
+ title: 'UI/Button',
+ component: Root, // ← اینجا هم Root
+ parameters: {
+ layout: 'centered',
+ },
+ tags: ['autodocs'],
+ argTypes: {
+ variant: {
+ control: 'select',
+ options: ['primary', 'neutral', 'error'],
+ },
+ mode: {
+ control: 'select',
+ options: ['filled', 'stroke', 'lighter', 'ghost'],
+ },
+ size: {
+ control: 'select',
+ options: ['medium', 'small', 'xsmall', 'xxsmall'],
+ },
+ asChild: { control: 'boolean' },
+ disabled: { control: 'boolean' },
+ },
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+// استوریهای مختلف
+export const PrimaryFilled: Story = {
+ args: {
+ children: 'Primary Filled',
+ variant: 'primary',
+ mode: 'filled',
+ },
+};
+
+export const PrimaryStroke: Story = {
+ args: {
+ children: 'Primary Stroke',
+ variant: 'primary',
+ mode: 'stroke',
+ },
+};
+
+export const WithIcon: Story = {
+ args: {
+ children: (
+ <>
+
+
+
+ Like
+ >
+ ),
+ variant: 'primary',
+ mode: 'filled',
+ },
+};
+
+export const NeutralGhost: Story = {
+ args: {
+ children: 'Neutral Ghost',
+ variant: 'neutral',
+ mode: 'ghost',
+ },
+};
+
+export const ErrorFilled: Story = {
+ args: {
+ children: 'Delete',
+ variant: 'error',
+ mode: 'filled',
+ },
+};
+
+export const Small: Story = {
+ args: {
+ children: 'Small Button',
+ size: 'small',
+ },
+};
+
+export const Disabled: Story = {
+ args: {
+ children: 'Disabled',
+ disabled: true,
+ },
+};
\ No newline at end of file
diff --git a/components/ui/checkbox.stories.tsx b/components/ui/checkbox.stories.tsx
new file mode 100644
index 0000000..7d823a2
--- /dev/null
+++ b/components/ui/checkbox.stories.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import * as Module from './checkbox';
+import Template from '../../.storybook/story-template';
+
+export default { title: 'UI/Checkbox' };
+
+export const Default = {
+ render: () => ,
+};
diff --git a/components/ui/color-picker.stories.tsx b/components/ui/color-picker.stories.tsx
new file mode 100644
index 0000000..357733d
--- /dev/null
+++ b/components/ui/color-picker.stories.tsx
@@ -0,0 +1,9 @@
+import React from 'react';
+import * as Module from './color-picker';
+import Template from '../../.storybook/story-template';
+
+export default { title: 'UI/ColorPicker' };
+
+export const Default = {
+ render: () => ,
+};
diff --git a/components/ui/color-picker.tsx b/components/ui/color-picker.tsx
index 5bcf262..efdfb81 100644
--- a/components/ui/color-picker.tsx
+++ b/components/ui/color-picker.tsx
@@ -137,27 +137,37 @@ const EyeDropperButton = React.forwardRef<
HTMLButtonElement,
React.HTMLAttributes
>(({ ...rest }, forwardedRef) => {
- const state = React.useContext(ColorPickerStateContext)!;
+ const state = React.useContext(ColorPickerStateContext);
- // eslint-disable-next-line
- // @ts-ignore
- if (typeof EyeDropper === 'undefined') {
+ if (!state) return null;
+
+ // Access EyeDropper from global in a typed, safe way
+ const EyeDropperAPI = (globalThis as any).EyeDropper as
+ | { new (): { open: () => Promise<{ sRGBHex: string }> } }
+ | undefined;
+
+ if (!EyeDropperAPI) {
return null;
}
+ const handleClick = async () => {
+ try {
+ const picker = new EyeDropperAPI();
+ const result = await picker.open();
+ if (result?.sRGBHex) {
+ state.setColor(parseColor(result.sRGBHex));
+ }
+ } catch (err) {
+ // swallow errors silently or optionally surface a notification
+ // console.error('EyeDropper failed', err);
+ }
+ };
+
return (