@sanvika/ui 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +18 -36
- package/src/components/buttons/Button.stories.jsx +110 -0
- package/src/components/buttons/FavoriteHeartButton.stories.jsx +48 -0
- package/src/components/buttons/ScrollButton.stories.jsx +21 -0
- package/src/components/buttons/ThreeDotButton.stories.jsx +14 -0
- package/src/components/common/Section.stories.jsx +64 -0
- package/src/components/layout/Footer.stories.jsx +28 -0
- package/src/components/layout/Navbar.stories.jsx +42 -0
- package/src/components/modals/Modal.stories.jsx +93 -0
- package/src/components/progressBar/ProgressBar.stories.jsx +67 -0
- package/src/index.js +3 -0
- package/src/layouts/Layout.jsx +24 -0
- package/src/server/index.js +159 -0
- package/README.md +0 -36
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanvika/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
|
-
"description": "Sanvika Production — shared UI component library for 50+ projects
|
|
6
|
+
"description": "Sanvika Production — shared UI component library for 50+ projects (pure JS, CSS Modules, props-driven) + cloud-driven Navbar/Theme presets via ui.sanvikaproduction.com",
|
|
7
7
|
"author": "Shyam Bharteey <shyam@sanvikaproduction.com>",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
@@ -19,49 +19,31 @@
|
|
|
19
19
|
"classified-ads"
|
|
20
20
|
],
|
|
21
21
|
"main": "./src/index.js",
|
|
22
|
+
"module": "./src/index.js",
|
|
22
23
|
"exports": {
|
|
23
24
|
".": "./src/index.js",
|
|
24
25
|
"./icons": "./src/components/icons/index.js",
|
|
25
|
-
"./styles": "./src/styles/index.css"
|
|
26
|
+
"./styles": "./src/styles/index.css",
|
|
27
|
+
"./server": "./src/server/index.js"
|
|
26
28
|
},
|
|
27
29
|
"files": [
|
|
28
|
-
"
|
|
29
|
-
"src
|
|
30
|
-
"src/styles",
|
|
31
|
-
"src/index.js",
|
|
32
|
-
"!src/**/*.stories.jsx",
|
|
33
|
-
"!src/**/*.stories.js"
|
|
30
|
+
"dist",
|
|
31
|
+
"src"
|
|
34
32
|
],
|
|
35
33
|
"peerDependencies": {
|
|
36
34
|
"react": ">=18.0.0",
|
|
37
35
|
"react-dom": ">=18.0.0"
|
|
38
36
|
},
|
|
39
|
-
"scripts": {
|
|
40
|
-
"dev": "next dev",
|
|
41
|
-
"build": "next build",
|
|
42
|
-
"start": "next start",
|
|
43
|
-
"lint": "eslint",
|
|
44
|
-
"storybook": "storybook dev -p 6006",
|
|
45
|
-
"build-storybook": "storybook build",
|
|
46
|
-
"prepublishOnly": "echo 'Publishing @sanvika/ui...'"
|
|
47
|
-
},
|
|
48
|
-
"dependencies": {
|
|
49
|
-
"mongoose": "^9.4.1"
|
|
50
|
-
},
|
|
51
|
-
"pnpm": {
|
|
52
|
-
"onlyBuiltDependencies": [
|
|
53
|
-
"core-js-pure",
|
|
54
|
-
"esbuild"
|
|
55
|
-
]
|
|
56
|
-
},
|
|
57
37
|
"devDependencies": {
|
|
58
|
-
"@
|
|
59
|
-
"babel-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
38
|
+
"@babel/core": "^7.29.0",
|
|
39
|
+
"@babel/preset-react": "^7.28.5",
|
|
40
|
+
"@rollup/plugin-babel": "^7.0.0",
|
|
41
|
+
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
42
|
+
"rollup": "^4.60.1",
|
|
43
|
+
"rollup-plugin-postcss": "^4.0.2"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "rollup -c rollup.config.js",
|
|
47
|
+
"build:lib": "rollup -c rollup.config.js"
|
|
66
48
|
}
|
|
67
|
-
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/components/buttons/Button.stories.jsx
|
|
2
|
+
import Button from "./Button";
|
|
3
|
+
|
|
4
|
+
/** @type {import('@storybook/react').Meta<typeof Button>} */
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Components/Button",
|
|
7
|
+
component: Button,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "centered",
|
|
10
|
+
},
|
|
11
|
+
argTypes: {
|
|
12
|
+
intent: {
|
|
13
|
+
control: "select",
|
|
14
|
+
options: ["primary", "secondary", "danger", "success"],
|
|
15
|
+
},
|
|
16
|
+
appearance: {
|
|
17
|
+
control: "select",
|
|
18
|
+
options: ["solid", "outline", "ghost"],
|
|
19
|
+
},
|
|
20
|
+
size: {
|
|
21
|
+
control: "select",
|
|
22
|
+
options: ["sm", "md", "lg"],
|
|
23
|
+
},
|
|
24
|
+
disabled: { control: "boolean" },
|
|
25
|
+
fullWidth: { control: "boolean" },
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default meta;
|
|
30
|
+
|
|
31
|
+
export const Primary = {
|
|
32
|
+
args: {
|
|
33
|
+
text: "Get started",
|
|
34
|
+
intent: "primary",
|
|
35
|
+
appearance: "solid",
|
|
36
|
+
size: "md",
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const Secondary = {
|
|
41
|
+
args: { text: "Learn more", intent: "secondary" },
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Danger = {
|
|
45
|
+
args: { text: "Delete", intent: "danger" },
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const Success = {
|
|
49
|
+
args: { text: "Confirm", intent: "success" },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const Outline = {
|
|
53
|
+
args: { text: "Outline button", appearance: "outline" },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const Ghost = {
|
|
57
|
+
args: { text: "Ghost button", appearance: "ghost" },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const Small = {
|
|
61
|
+
args: { text: "Small", size: "sm" },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const Large = {
|
|
65
|
+
args: { text: "Large", size: "lg" },
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export const WithIcon = {
|
|
69
|
+
args: {
|
|
70
|
+
text: "Download",
|
|
71
|
+
icon: <span>⬇</span>,
|
|
72
|
+
intent: "primary",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const WithBadge = {
|
|
77
|
+
args: {
|
|
78
|
+
text: "Notifications",
|
|
79
|
+
badge: 5,
|
|
80
|
+
intent: "secondary",
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const Disabled = {
|
|
85
|
+
args: { text: "Disabled", disabled: true },
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const AsLink = {
|
|
89
|
+
args: { text: "Visit site", href: "#", intent: "primary" },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const AllIntents = {
|
|
93
|
+
render: () => (
|
|
94
|
+
<div style={{ display: "flex", gap: "12px", flexWrap: "wrap" }}>
|
|
95
|
+
{["primary", "secondary", "danger", "success"].map((intent) => (
|
|
96
|
+
<Button key={intent} intent={intent} text={intent} />
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const AllAppearances = {
|
|
103
|
+
render: () => (
|
|
104
|
+
<div style={{ display: "flex", gap: "12px", flexWrap: "wrap" }}>
|
|
105
|
+
{["solid", "outline", "ghost"].map((appearance) => (
|
|
106
|
+
<Button key={appearance} appearance={appearance} text={appearance} />
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
),
|
|
110
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/components/buttons/FavoriteHeartButton.stories.jsx
|
|
2
|
+
"use client";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import FavoriteHeartButton from "./FavoriteHeartButton";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/Buttons/FavoriteHeartButton",
|
|
8
|
+
component: FavoriteHeartButton,
|
|
9
|
+
parameters: { layout: "centered" },
|
|
10
|
+
argTypes: {
|
|
11
|
+
isFavorited: { control: "boolean" },
|
|
12
|
+
disabled: { control: "boolean" },
|
|
13
|
+
isOwner: { control: "boolean" },
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default meta;
|
|
18
|
+
|
|
19
|
+
const FavDemo = ({ initial = false }) => {
|
|
20
|
+
const [fav, setFav] = useState(initial);
|
|
21
|
+
return (
|
|
22
|
+
<FavoriteHeartButton
|
|
23
|
+
isFavorited={fav}
|
|
24
|
+
onClick={() => setFav((v) => !v)}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const NotFavorited = {
|
|
30
|
+
render: () => <FavDemo initial={false} />,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Favorited = {
|
|
34
|
+
render: () => <FavDemo initial={true} />,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const Disabled = {
|
|
38
|
+
args: { isFavorited: false, disabled: true, onClick: () => {} },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const HiddenForOwner = {
|
|
42
|
+
args: { isFavorited: false, isOwner: true, onClick: () => {} },
|
|
43
|
+
parameters: {
|
|
44
|
+
docs: {
|
|
45
|
+
description: { story: "Returns null when isOwner=true — nothing renders." },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// src/components/buttons/ScrollButton.stories.jsx
|
|
2
|
+
import ScrollButton from "./ScrollButton";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/Buttons/ScrollButton",
|
|
6
|
+
component: ScrollButton,
|
|
7
|
+
parameters: { layout: "centered" },
|
|
8
|
+
argTypes: {
|
|
9
|
+
direction: { control: "select", options: ["up", "down"] },
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default meta;
|
|
14
|
+
|
|
15
|
+
export const Up = {
|
|
16
|
+
args: { direction: "up", onClick: () => window.scrollTo({ top: 0, behavior: "smooth" }) },
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const Down = {
|
|
20
|
+
args: { direction: "down", onClick: () => window.scrollTo({ top: document.body.scrollHeight, behavior: "smooth" }) },
|
|
21
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/components/buttons/ThreeDotButton.stories.jsx
|
|
2
|
+
import ThreeDotButton from "./ThreeDotButton";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Components/Buttons/ThreeDotButton",
|
|
6
|
+
component: ThreeDotButton,
|
|
7
|
+
parameters: { layout: "centered" },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
|
|
12
|
+
export const Default = {
|
|
13
|
+
args: { onClick: () => alert("Menu opened"), ariaLabel: "More options" },
|
|
14
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/components/common/Section.stories.jsx
|
|
2
|
+
import Section from "./Section";
|
|
3
|
+
import Button from "../buttons/Button.jsx";
|
|
4
|
+
|
|
5
|
+
/** @type {import('@storybook/react').Meta<typeof Section>} */
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Layout/Section",
|
|
8
|
+
component: Section,
|
|
9
|
+
parameters: { layout: "fullscreen" },
|
|
10
|
+
argTypes: {
|
|
11
|
+
variant: {
|
|
12
|
+
control: "select",
|
|
13
|
+
options: ["default", "alternate", "card"],
|
|
14
|
+
},
|
|
15
|
+
padding: {
|
|
16
|
+
control: "select",
|
|
17
|
+
options: ["default", "tight", "flush"],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
|
|
24
|
+
export const Default = {
|
|
25
|
+
args: {
|
|
26
|
+
title: "Section Title",
|
|
27
|
+
subtitle: "An optional subtitle providing more context about this section.",
|
|
28
|
+
children: (
|
|
29
|
+
<p style={{ textAlign: "center", color: "var(--secondary-text-color)" }}>
|
|
30
|
+
Section content goes here.
|
|
31
|
+
</p>
|
|
32
|
+
),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Alternate = {
|
|
37
|
+
args: {
|
|
38
|
+
...Default.args,
|
|
39
|
+
variant: "alternate",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Card = {
|
|
44
|
+
args: {
|
|
45
|
+
...Default.args,
|
|
46
|
+
variant: "card",
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const WithCTA = {
|
|
51
|
+
args: {
|
|
52
|
+
title: "Ready to get started?",
|
|
53
|
+
subtitle: "Join thousands of creators building on Sanvika.",
|
|
54
|
+
children: (
|
|
55
|
+
<div style={{ display: "flex", justifyContent: "center", gap: "12px" }}>
|
|
56
|
+
<Button intent="primary">Get started</Button>
|
|
57
|
+
<Button intent="secondary" appearance="outline">
|
|
58
|
+
Learn more
|
|
59
|
+
</Button>
|
|
60
|
+
</div>
|
|
61
|
+
),
|
|
62
|
+
variant: "alternate",
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/components/layout/Footer.stories.jsx
|
|
2
|
+
import Footer from "./Footer";
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
title: "Layout/Footer",
|
|
6
|
+
component: Footer,
|
|
7
|
+
parameters: { layout: "fullscreen" },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
|
|
12
|
+
export const Default = {};
|
|
13
|
+
|
|
14
|
+
export const CustomLinks = {
|
|
15
|
+
args: {
|
|
16
|
+
legalLinks: [
|
|
17
|
+
{ href: "/about", label: "About" },
|
|
18
|
+
{ href: "/privacy", label: "Privacy" },
|
|
19
|
+
{ href: "/terms", label: "Terms" },
|
|
20
|
+
],
|
|
21
|
+
trustText: "D-U-N-S® Verified · My Company",
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const NoSocialLinks = {
|
|
26
|
+
args: { socialLinks: [] },
|
|
27
|
+
};
|
|
28
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// src/components/layout/Navbar.stories.jsx
|
|
2
|
+
import Navbar from "./Navbar";
|
|
3
|
+
|
|
4
|
+
const NAV_LINKS = [
|
|
5
|
+
{ href: "/", label: "Home" },
|
|
6
|
+
{ href: "/about", label: "About" },
|
|
7
|
+
{ href: "/listings", label: "Listings" },
|
|
8
|
+
{ href: "/contact", label: "Contact" },
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const meta = {
|
|
12
|
+
title: "Layout/Navbar",
|
|
13
|
+
component: Navbar,
|
|
14
|
+
parameters: { layout: "fullscreen" },
|
|
15
|
+
argTypes: {
|
|
16
|
+
logoText: { control: "text" },
|
|
17
|
+
activePath: { control: "text" },
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default meta;
|
|
22
|
+
|
|
23
|
+
export const Default = {
|
|
24
|
+
args: { navLinks: NAV_LINKS, activePath: "/", logoText: "Sanvika" },
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const ActiveAbout = {
|
|
28
|
+
args: { navLinks: NAV_LINKS, activePath: "/about", logoText: "Sanvika" },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const CustomLogo = {
|
|
32
|
+
args: { navLinks: NAV_LINKS, activePath: "/", logoText: "MyApp" },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const FewLinks = {
|
|
36
|
+
args: {
|
|
37
|
+
navLinks: [{ href: "/", label: "Home" }, { href: "/contact", label: "Contact" }],
|
|
38
|
+
activePath: "/",
|
|
39
|
+
logoText: "Sanvika",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/components/modals/Modal.stories.jsx
|
|
2
|
+
"use client";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import Modal from "./Modal";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/Modal",
|
|
8
|
+
component: Modal,
|
|
9
|
+
parameters: { layout: "centered" },
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default meta;
|
|
13
|
+
|
|
14
|
+
// Interactive wrapper since Modal needs open/close state
|
|
15
|
+
const ModalDemo = ({ buttonLabel = "Open Modal", children }) => {
|
|
16
|
+
const [open, setOpen] = useState(false);
|
|
17
|
+
return (
|
|
18
|
+
<>
|
|
19
|
+
<button
|
|
20
|
+
onClick={() => setOpen(true)}
|
|
21
|
+
style={{
|
|
22
|
+
padding: "8px 20px",
|
|
23
|
+
background: "var(--primary-color, #0984e3)",
|
|
24
|
+
color: "#fff",
|
|
25
|
+
border: "none",
|
|
26
|
+
borderRadius: "6px",
|
|
27
|
+
cursor: "pointer",
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
{buttonLabel}
|
|
31
|
+
</button>
|
|
32
|
+
<Modal isOpen={open} onClose={() => setOpen(false)}>
|
|
33
|
+
{children}
|
|
34
|
+
<button
|
|
35
|
+
onClick={() => setOpen(false)}
|
|
36
|
+
style={{
|
|
37
|
+
position: "absolute",
|
|
38
|
+
top: 12,
|
|
39
|
+
right: 12,
|
|
40
|
+
background: "transparent",
|
|
41
|
+
border: "none",
|
|
42
|
+
fontSize: 20,
|
|
43
|
+
cursor: "pointer",
|
|
44
|
+
color: "var(--secondary-text-color)",
|
|
45
|
+
}}
|
|
46
|
+
aria-label="Close modal"
|
|
47
|
+
>
|
|
48
|
+
✖
|
|
49
|
+
</button>
|
|
50
|
+
</Modal>
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const Default = {
|
|
56
|
+
render: () => (
|
|
57
|
+
<ModalDemo buttonLabel="Open Modal">
|
|
58
|
+
<h2 style={{ margin: "0 0 12px", color: "var(--heading-color)" }}>Modal Title</h2>
|
|
59
|
+
<p style={{ color: "var(--text-color)" }}>
|
|
60
|
+
This is modal content. Click the ✖ button or press ESC to close. Clicking outside also closes.
|
|
61
|
+
</p>
|
|
62
|
+
</ModalDemo>
|
|
63
|
+
),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const WithForm = {
|
|
67
|
+
render: () => (
|
|
68
|
+
<ModalDemo buttonLabel="Open Form Modal">
|
|
69
|
+
<h2 style={{ margin: "0 0 16px", color: "var(--heading-color)" }}>Confirm Action</h2>
|
|
70
|
+
<p style={{ marginBottom: 20, color: "var(--secondary-text-color)" }}>
|
|
71
|
+
Are you sure you want to delete this item?
|
|
72
|
+
</p>
|
|
73
|
+
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
|
|
74
|
+
<button
|
|
75
|
+
style={{ padding: "8px 16px", background: "var(--secondary-color)", color: "#fff", border: "none", borderRadius: 6, cursor: "pointer" }}
|
|
76
|
+
>
|
|
77
|
+
Cancel
|
|
78
|
+
</button>
|
|
79
|
+
<button
|
|
80
|
+
style={{ padding: "8px 16px", background: "var(--error-color, #dc3545)", color: "#fff", border: "none", borderRadius: 6, cursor: "pointer" }}
|
|
81
|
+
>
|
|
82
|
+
Delete
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
<button
|
|
86
|
+
style={{ position: "absolute", top: 12, right: 12, background: "transparent", border: "none", fontSize: 20, cursor: "pointer", color: "var(--secondary-text-color)" }}
|
|
87
|
+
aria-label="Close"
|
|
88
|
+
>
|
|
89
|
+
✖
|
|
90
|
+
</button>
|
|
91
|
+
</ModalDemo>
|
|
92
|
+
),
|
|
93
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// src/components/progressBar/ProgressBar.stories.jsx
|
|
2
|
+
"use client";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import ProgressBar from "./ProgressBar";
|
|
5
|
+
|
|
6
|
+
const meta = {
|
|
7
|
+
title: "Components/ProgressBar",
|
|
8
|
+
component: ProgressBar,
|
|
9
|
+
parameters: { layout: "padded" },
|
|
10
|
+
argTypes: {
|
|
11
|
+
progress: { control: { type: "range", min: 0, max: 100, step: 1 } },
|
|
12
|
+
height: { control: { type: "range", min: 2, max: 24, step: 1 } },
|
|
13
|
+
animate: { control: "boolean" },
|
|
14
|
+
showLabel: { control: "boolean" },
|
|
15
|
+
labelPosition: { control: "select", options: ["top", "bottom"] },
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default meta;
|
|
20
|
+
|
|
21
|
+
export const Default = {
|
|
22
|
+
args: { progress: 60, height: 6, animate: true },
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const WithLabel = {
|
|
26
|
+
args: { progress: 75, height: 8, showLabel: true, labelPosition: "top" },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const Thick = {
|
|
30
|
+
args: { progress: 45, height: 16, showLabel: true },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const Complete = {
|
|
34
|
+
args: { progress: 100, height: 6, showLabel: true },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const NoAnimation = {
|
|
38
|
+
args: { progress: 50, animate: false, height: 6 },
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Interactive story with increment button
|
|
42
|
+
const ProgressBarInteractive = () => {
|
|
43
|
+
const [val, setVal] = useState(0);
|
|
44
|
+
return (
|
|
45
|
+
<div style={{ width: 360, display: "flex", flexDirection: "column", gap: 16 }}>
|
|
46
|
+
<ProgressBar progress={val} height={8} showLabel animate />
|
|
47
|
+
<div style={{ display: "flex", gap: 8 }}>
|
|
48
|
+
<button
|
|
49
|
+
onClick={() => setVal((v) => Math.min(100, v + 10))}
|
|
50
|
+
style={{ padding: "6px 16px", background: "var(--primary-color, #007bff)", color: "#fff", border: "none", borderRadius: 6, cursor: "pointer" }}
|
|
51
|
+
>
|
|
52
|
+
+10
|
|
53
|
+
</button>
|
|
54
|
+
<button
|
|
55
|
+
onClick={() => setVal(0)}
|
|
56
|
+
style={{ padding: "6px 16px", background: "var(--secondary-color, #6c757d)", color: "#fff", border: "none", borderRadius: 6, cursor: "pointer" }}
|
|
57
|
+
>
|
|
58
|
+
Reset
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const Interactive = {
|
|
66
|
+
render: () => <ProgressBarInteractive />,
|
|
67
|
+
};
|
package/src/index.js
CHANGED
|
@@ -28,3 +28,6 @@ export * from "./components/icons/index.js";
|
|
|
28
28
|
|
|
29
29
|
// ─── Theme Context ────────────────────────────────────────────────────────────
|
|
30
30
|
export { ThemeProvider, useTheme } from "./context/ThemeContext.jsx";
|
|
31
|
+
|
|
32
|
+
// ─── Page Layout ─────────────────────────────────────────────────────────────
|
|
33
|
+
export { default as Layout } from "./layouts/Layout.jsx";
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// src/layouts/Layout.jsx
|
|
2
|
+
import Navbar from "../components/layout/Navbar";
|
|
3
|
+
import Footer from "../components/layout/Footer";
|
|
4
|
+
import styles from "../styles/components/layouts/Layout.module.css";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Root page layout — wraps every page with Navbar + Footer.
|
|
8
|
+
* The navbar is fixed; content area has a top margin to compensate.
|
|
9
|
+
*/
|
|
10
|
+
const Layout = ({ children }) => {
|
|
11
|
+
return (
|
|
12
|
+
<div className={styles.wrapper}>
|
|
13
|
+
<Navbar />
|
|
14
|
+
<div className={styles.content}>
|
|
15
|
+
<main className={styles.main}>{children}</main>
|
|
16
|
+
</div>
|
|
17
|
+
<div className={styles.footerWrapper}>
|
|
18
|
+
<Footer />
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default Layout;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// @sanvika/ui/server v0.2.0
|
|
2
|
+
// HTTP client for ui.sanvikaproduction.com — cloud-driven Navbar configs and Theme presets.
|
|
3
|
+
// Env vars: UI_URL, UI_CLIENT_SECRET (or pass via constructor).
|
|
4
|
+
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 8000;
|
|
6
|
+
|
|
7
|
+
function _readEnv(key) {
|
|
8
|
+
try {
|
|
9
|
+
return (typeof process !== "undefined" && process.env?.[key]) || null;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class SanvikaUI {
|
|
16
|
+
#url;
|
|
17
|
+
#secret;
|
|
18
|
+
|
|
19
|
+
constructor({ url, secret } = {}) {
|
|
20
|
+
const finalUrl = url || _readEnv("UI_URL");
|
|
21
|
+
const finalSecret = secret || _readEnv("UI_CLIENT_SECRET");
|
|
22
|
+
if (!finalUrl || !finalSecret) {
|
|
23
|
+
throw new Error("@sanvika/ui/server: UI_URL and UI_CLIENT_SECRET are required");
|
|
24
|
+
}
|
|
25
|
+
this.#url = finalUrl.replace(/\/$/, "");
|
|
26
|
+
this.#secret = finalSecret;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get baseUrl() {
|
|
30
|
+
return this.#url;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async #request(path, { method = "POST", body, query } = {}) {
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
36
|
+
|
|
37
|
+
const qs = query ? "?" + new URLSearchParams(query).toString() : "";
|
|
38
|
+
const init = {
|
|
39
|
+
method,
|
|
40
|
+
headers: { "x-client-secret": this.#secret },
|
|
41
|
+
signal: controller.signal,
|
|
42
|
+
};
|
|
43
|
+
if (body !== undefined) {
|
|
44
|
+
init.headers["Content-Type"] = "application/json";
|
|
45
|
+
init.body = JSON.stringify(body);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch(`${this.#url}${path}${qs}`, init);
|
|
50
|
+
const json = await res.json().catch(() => ({}));
|
|
51
|
+
if (!res.ok || !json.success) {
|
|
52
|
+
return { success: false, status: res.status, error: json?.error || `HTTP_${res.status}` };
|
|
53
|
+
}
|
|
54
|
+
return { success: true, ...json };
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return { success: false, error: err.name === "AbortError" ? "TIMEOUT" : err.message };
|
|
57
|
+
} finally {
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Fetch latest published navbar config (default name = "default"). */
|
|
63
|
+
async getNavbarConfig({ name = "default", version } = {}) {
|
|
64
|
+
const query = {};
|
|
65
|
+
if (version !== undefined) query.version = String(version);
|
|
66
|
+
return this.#request(`/api/v1/navbar/${encodeURIComponent(name)}`, {
|
|
67
|
+
method: "GET",
|
|
68
|
+
query: Object.keys(query).length ? query : undefined,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Save (creates new version) a navbar config. */
|
|
73
|
+
async saveNavbarConfig({ name = "default", items, cta, logoUrl, activeThemePreset, status = "published" } = {}) {
|
|
74
|
+
if (!Array.isArray(items)) throw new Error("saveNavbarConfig: items[] is required");
|
|
75
|
+
return this.#request("/api/v1/navbar", {
|
|
76
|
+
body: { name, items, cta, logoUrl, activeThemePreset, status },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** List latest navbar configs across names. */
|
|
81
|
+
async listNavbarConfigs() {
|
|
82
|
+
return this.#request("/api/v1/navbar", { method: "GET" });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Soft-delete all versions of a navbar config name. */
|
|
86
|
+
async deleteNavbarConfig(name) {
|
|
87
|
+
if (!name) throw new Error("deleteNavbarConfig: name is required");
|
|
88
|
+
return this.#request(`/api/v1/navbar/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Fetch a single theme preset by name. */
|
|
92
|
+
async getThemePreset({ name } = {}) {
|
|
93
|
+
if (!name) throw new Error("getThemePreset: name is required");
|
|
94
|
+
return this.#request(`/api/v1/theme/${encodeURIComponent(name)}`, { method: "GET" });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Upsert a theme preset by name. */
|
|
98
|
+
async saveThemePreset({ name, mode = "light", palette = {}, fontFamily, radius, extras, status = "published" } = {}) {
|
|
99
|
+
if (!name) throw new Error("saveThemePreset: name is required");
|
|
100
|
+
return this.#request("/api/v1/theme", {
|
|
101
|
+
body: { name, mode, palette, fontFamily, radius, extras, status },
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** List theme presets. */
|
|
106
|
+
async listThemePresets() {
|
|
107
|
+
return this.#request("/api/v1/theme", { method: "GET" });
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Soft-delete a theme preset. */
|
|
111
|
+
async deleteThemePreset(name) {
|
|
112
|
+
if (!name) throw new Error("deleteThemePreset: name is required");
|
|
113
|
+
return this.#request(`/api/v1/theme/${encodeURIComponent(name)}`, { method: "DELETE" });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function createUiClient(opts = {}) {
|
|
118
|
+
return new SanvikaUI(opts);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let _instance = null;
|
|
122
|
+
function _getInstance() {
|
|
123
|
+
if (!_instance) _instance = new SanvikaUI();
|
|
124
|
+
return _instance;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function getNavbarConfig(opts = {}) {
|
|
128
|
+
return _getInstance().getNavbarConfig(opts);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function saveNavbarConfig(input) {
|
|
132
|
+
return _getInstance().saveNavbarConfig(input);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function listNavbarConfigs() {
|
|
136
|
+
return _getInstance().listNavbarConfigs();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function deleteNavbarConfig(name) {
|
|
140
|
+
return _getInstance().deleteNavbarConfig(name);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function getThemePreset(opts) {
|
|
144
|
+
return _getInstance().getThemePreset(opts);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function saveThemePreset(input) {
|
|
148
|
+
return _getInstance().saveThemePreset(input);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export async function listThemePresets() {
|
|
152
|
+
return _getInstance().listThemePresets();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function deleteThemePreset(name) {
|
|
156
|
+
return _getInstance().deleteThemePreset(name);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export default createUiClient;
|
package/README.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
|
2
|
-
|
|
3
|
-
## Getting Started
|
|
4
|
-
|
|
5
|
-
First, run the development server:
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm run dev
|
|
9
|
-
# or
|
|
10
|
-
yarn dev
|
|
11
|
-
# or
|
|
12
|
-
pnpm dev
|
|
13
|
-
# or
|
|
14
|
-
bun dev
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
18
|
-
|
|
19
|
-
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
|
20
|
-
|
|
21
|
-
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
|
22
|
-
|
|
23
|
-
## Learn More
|
|
24
|
-
|
|
25
|
-
To learn more about Next.js, take a look at the following resources:
|
|
26
|
-
|
|
27
|
-
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
28
|
-
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
29
|
-
|
|
30
|
-
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
|
31
|
-
|
|
32
|
-
## Deploy on Vercel
|
|
33
|
-
|
|
34
|
-
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
35
|
-
|
|
36
|
-
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|