diff --git a/package-lock.json b/package-lock.json
index 3eec3ee..3d1413b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,7 +16,8 @@
"fs": "^0.0.1-security",
"react": "^19.1.0",
"react-dom": "^19.1.0",
- "react-router": "^7.7.1"
+ "react-router": "^7.7.1",
+ "react-router-dom": "^7.8.1"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
@@ -3529,9 +3530,9 @@
}
},
"node_modules/react-router": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.7.1.tgz",
- "integrity": "sha512-jVKHXoWRIsD/qS6lvGveckwb862EekvapdHJN/cGmzw40KnJH5gg53ujOJ4qX6EKIK9LSBfFed/xiQ5yeXNrUA==",
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.1.tgz",
+ "integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
@@ -3550,6 +3551,22 @@
}
}
},
+ "node_modules/react-router-dom": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.8.1.tgz",
+ "integrity": "sha512-NkgBCF3sVgCiAWIlSt89GR2PLaksMpoo3HDCorpRfnCEfdtRPLiuTf+CNXvqZMI5SJLZCLpVCvcZrTdtGW64xQ==",
+ "license": "MIT",
+ "dependencies": {
+ "react-router": "7.8.1"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=18",
+ "react-dom": ">=18"
+ }
+ },
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
diff --git a/package.json b/package.json
index a874ef1..3d08f46 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,8 @@
"fs": "^0.0.1-security",
"react": "^19.1.0",
"react-dom": "^19.1.0",
- "react-router": "^7.7.1"
+ "react-router": "^7.7.1",
+ "react-router-dom": "^7.8.1"
},
"devDependencies": {
"@eslint/js": "^9.30.1",
diff --git a/src/App.tsx b/src/App.tsx
index 2a17b2b..251f609 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,13 +1,11 @@
-import Header from './components/common/Layout/Header'
+import { Routes, Route } from 'react-router-dom'
+import LibraryPage from './pages/LiberyPage'
function App() {
return (
- <>
-
- >
+
+ } />
+
)
}
diff --git a/src/components/common/Layout/Header/styled.ts b/src/components/common/Layout/Header/styled.ts
index ec340a6..232bb57 100644
--- a/src/components/common/Layout/Header/styled.ts
+++ b/src/components/common/Layout/Header/styled.ts
@@ -10,7 +10,7 @@ export const Header = styled.header`
display: flex;
justify-content: space-between;
align-items: center;
- padding: ${tokens.padding.CONTAINER}px;
+ padding: ${tokens.padding.CONTAINER}px ${tokens.padding.CONTAINER / 2}px;
& > div {
display: flex;
diff --git a/src/components/common/molecules/List/List.tsx b/src/components/common/molecules/List/List.tsx
index e69de29..21f022e 100644
--- a/src/components/common/molecules/List/List.tsx
+++ b/src/components/common/molecules/List/List.tsx
@@ -0,0 +1,23 @@
+import { Typography as T } from '../../../../core/typography'
+import { FlexContainer } from '../../styled'
+import * as S from './styled'
+
+type ListProps = {
+ title: string
+ items: string[]
+}
+
+export default function List({ title, items }: ListProps) {
+ return (
+
+
+ {title}
+
+
+ {items.map((item) => (
+ {item}
+ ))}
+
+
+ )
+}
diff --git a/src/components/common/molecules/List/index.ts b/src/components/common/molecules/List/index.ts
index e69de29..6cd88f9 100644
--- a/src/components/common/molecules/List/index.ts
+++ b/src/components/common/molecules/List/index.ts
@@ -0,0 +1,3 @@
+import List from './List'
+
+export default List
diff --git a/src/components/common/molecules/List/styled.ts b/src/components/common/molecules/List/styled.ts
index e69de29..e04adb7 100644
--- a/src/components/common/molecules/List/styled.ts
+++ b/src/components/common/molecules/List/styled.ts
@@ -0,0 +1,13 @@
+import styled from '@emotion/styled'
+import tokens from '../../../../core/tokens'
+
+export const List = styled.ul`
+ margin: ${tokens.spacing.BASELINE}px 0;
+ padding-left: ${tokens.spacing.BASELINE * 2}px;
+ list-style-type: disc;
+`
+
+export const ListItem = styled.li`
+ margin-bottom: ${tokens.spacing.BASELINE}px;
+ color: ${({ theme }) => theme.colors.text.primary};
+`
diff --git a/src/components/common/organisms/Tabs/Tabs.tsx b/src/components/common/organisms/Tabs/Tabs.tsx
new file mode 100644
index 0000000..2b973be
--- /dev/null
+++ b/src/components/common/organisms/Tabs/Tabs.tsx
@@ -0,0 +1,68 @@
+import { useEffect, useMemo, useState } from 'react'
+import * as S from './styled'
+
+export type TabItem = {
+ key: string
+ label: string
+ content: React.ReactNode
+}
+
+type Props = { items: TabItem[]; defaultKey?: string }
+
+export default function Tabs({ items, defaultKey }: Props) {
+ const keys = useMemo(() => items.map((i) => i.key), [items])
+ const [active, setActive] = useState(defaultKey ?? keys[0])
+
+ useEffect(() => {
+ if (!keys.includes(active)) setActive(keys[0])
+ }, [keys, active])
+
+ const onKeyDown = (e: React.KeyboardEvent) => {
+ const i = keys.indexOf(active)
+ if (e.key === 'ArrowRight') setActive(keys[(i + 1) % keys.length])
+ if (e.key === 'ArrowLeft')
+ setActive(keys[(i - 1 + keys.length) % keys.length])
+ if (e.key === 'Home') setActive(keys[0])
+ if (e.key === 'End') setActive(keys[keys.length - 1])
+ }
+
+ return (
+ <>
+
+ {items.map((t) => {
+ const isActive = t.key === active
+ return (
+ setActive(t.key)}
+ >
+ {t.label}
+
+ )
+ })}
+
+
+ {items.map((t) => {
+ const isActive = t.key === active
+ return (
+
+ {isActive && t.content}
+
+ )
+ })}
+ >
+ )
+}
diff --git a/src/components/common/organisms/Tabs/index.ts b/src/components/common/organisms/Tabs/index.ts
new file mode 100644
index 0000000..7cb9e7e
--- /dev/null
+++ b/src/components/common/organisms/Tabs/index.ts
@@ -0,0 +1,2 @@
+export { default } from './Tabs'
+export type { TabItem } from './Tabs'
diff --git a/src/components/common/organisms/Tabs/styled.ts b/src/components/common/organisms/Tabs/styled.ts
new file mode 100644
index 0000000..0b50ff3
--- /dev/null
+++ b/src/components/common/organisms/Tabs/styled.ts
@@ -0,0 +1,45 @@
+import styled from '@emotion/styled'
+import tokens from '../../../../core/tokens'
+
+const DIVIDER_W = `${tokens.size.BASELINE / 8}px`
+const INDICATOR_H = '2px'
+
+export const Bar = styled.nav`
+ display: flex;
+ align-items: flex-end; /* tabs sit on the divider */
+ gap: ${tokens.gap.LARGE}px;
+ margin: ${tokens.spacing.BASELINE * 2}px 0 ${tokens.spacing.BASELINE}px;
+ border-bottom: ${DIVIDER_W} solid ${({ theme }) => theme.colors.scrollBar};
+`
+
+export const TabBtn = styled.button<{ active: boolean }>`
+ position: relative;
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ color: ${(p) =>
+ p.active ? p.theme.colors.primary : p.theme.colors.text.primary};
+ font-size: ${tokens.text.fontSize.BASELINE}px;
+ font-weight: ${({ active }) =>
+ active ? tokens.text.fontWeight.semiBold : tokens.text.fontWeight.normal};
+
+ padding: ${tokens.spacing.BASELINE}px ${tokens.spacing.BASELINE * 2}px;
+ padding-bottom: ${tokens.spacing.BASELINE * 1.5}px;
+
+ &::after {
+ content: '';
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: calc(-1 * ${DIVIDER_W});
+ height: ${INDICATOR_H};
+ background: ${({ theme }) => theme.colors.primary};
+ transform: ${({ active }) => (active ? 'scaleX(1)' : 'scaleX(0)')};
+ transform-origin: left;
+ transition: transform 160ms ease;
+ }
+`
+
+export const Panel = styled.section`
+ padding-top: ${tokens.spacing.BASELINE * 2}px;
+`
diff --git a/src/components/common/styled.ts b/src/components/common/styled.ts
new file mode 100644
index 0000000..74f4340
--- /dev/null
+++ b/src/components/common/styled.ts
@@ -0,0 +1,29 @@
+import styled from '@emotion/styled'
+import tokens from '../../core/tokens'
+
+export const PaddedContainer = styled.div<{ padding?: number }>`
+ padding: ${(p) => p.padding ?? tokens.padding.BASELINE * 2}px;
+`
+
+export const Main = styled(PaddedContainer)``
+
+export const DividerBar = styled.hr`
+ width: 100%;
+ margin: 0;
+ border: 0;
+ border-top: ${tokens.size.BASELINE / 8}px solid
+ ${({ theme }) => theme.colors.scrollBar};
+`
+
+export const FlexContainer = styled.div<{
+ direction?: 'row' | 'column'
+ gap?: number
+ align?: string
+ justify?: string
+}>`
+ display: flex;
+ flex-direction: ${(p) => p.direction ?? 'column'};
+ gap: ${(p) => p.gap ?? tokens.gap.BASELINE}px;
+ align-items: ${(p) => p.align ?? 'stretch'};
+ justify-content: ${(p) => p.justify ?? 'flex-start'};
+`
diff --git a/src/components/common/templates/SectionCard/SectionCard.tsx b/src/components/common/templates/SectionCard/SectionCard.tsx
new file mode 100644
index 0000000..8738fa0
--- /dev/null
+++ b/src/components/common/templates/SectionCard/SectionCard.tsx
@@ -0,0 +1,24 @@
+import type { ReactNode } from 'react'
+import * as S from './styled'
+import { Typography as T } from '../../../../core/typography'
+import { DividerBar, PaddedContainer } from '../../styled'
+import tokens from '../../../../core/tokens'
+
+type Props = {
+ title: string
+ children: ReactNode
+}
+
+export default function SectionCard({ title, children }: Props) {
+ return (
+
+
+
+ {title}
+
+
+
+ {children}
+
+ )
+}
diff --git a/src/components/common/templates/SectionCard/index.ts b/src/components/common/templates/SectionCard/index.ts
new file mode 100644
index 0000000..321bbc7
--- /dev/null
+++ b/src/components/common/templates/SectionCard/index.ts
@@ -0,0 +1,3 @@
+import SectionCard from './SectionCard'
+
+export default SectionCard
diff --git a/src/components/common/templates/SectionCard/styled.ts b/src/components/common/templates/SectionCard/styled.ts
new file mode 100644
index 0000000..3b0ba2b
--- /dev/null
+++ b/src/components/common/templates/SectionCard/styled.ts
@@ -0,0 +1,15 @@
+import styled from '@emotion/styled'
+import tokens from '../../../../core/tokens'
+import { FlexContainer } from '../../styled'
+
+export const Wrapper = styled.section`
+ background: ${({ theme }) => theme.colors.white};
+ border-radius: ${tokens.borderRadius.BASELINE}px;
+ box-shadow: 0 1px 3px 0 ${({ theme }) => theme.colors.boxShadow};
+ overflow: hidden;
+`
+
+export const Body = styled(FlexContainer)`
+ gap: ${tokens.gap.LARGE}px;
+ padding: ${tokens.padding.BASELINE * 2.5}px;
+`
diff --git a/src/core/theme.ts b/src/core/theme.ts
index 9147816..553d7eb 100644
--- a/src/core/theme.ts
+++ b/src/core/theme.ts
@@ -13,7 +13,7 @@ const brandColors = {
}
const background = {
- primaryGrey: '#f4f4f4',
+ primaryGrey: '#EFEFE9',
secondaryGrey: '#F5F5F5',
tertiaryGrey: '#CCCCCC',
}
diff --git a/src/index.css b/src/index.css
index 8b89f1d..7d028ba 100644
--- a/src/index.css
+++ b/src/index.css
@@ -26,4 +26,5 @@ select {
body {
margin: 0;
+ padding: 0;
}
diff --git a/src/main.tsx b/src/main.tsx
index 284468d..8218efb 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,19 +1,16 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
-import './index.css'
-import App from './App.tsx'
-import { BrowserRouter, Route, Routes } from 'react-router'
+import { BrowserRouter } from 'react-router-dom'
import { ThemeProvider } from '@emotion/react'
-import theme from './core/theme.ts'
+import theme from './core/theme'
+import './index.css'
+import App from './App'
createRoot(document.getElementById('root')!).render(
-
- //TODO: Add more routes that takes you to different template pages
- } />
-
+
diff --git a/src/pages/LiberyPage/index.tsx b/src/pages/LiberyPage/index.tsx
new file mode 100644
index 0000000..ee0d4cc
--- /dev/null
+++ b/src/pages/LiberyPage/index.tsx
@@ -0,0 +1,19 @@
+import Tabs from '../../components/common/organisms/Tabs'
+import { Main } from '../../components/common/styled'
+import { LiberyTabs } from './tabs.config'
+import Header from '../../components/common/Layout/Header'
+
+export default function LibraryPage() {
+ return (
+ <>
+
+
+
+
+
+ >
+ )
+}
diff --git a/src/pages/LiberyPage/tabs.config.tsx b/src/pages/LiberyPage/tabs.config.tsx
new file mode 100644
index 0000000..308524e
--- /dev/null
+++ b/src/pages/LiberyPage/tabs.config.tsx
@@ -0,0 +1,10 @@
+import type { TabItem } from '../../components/common/organisms/Tabs'
+import OverviewTab from './tabs/Overview'
+import ComponentsTab from './tabs/Components'
+import HooksTab from './tabs/Hooks'
+
+export const LiberyTabs: TabItem[] = [
+ { key: 'overview', label: 'Overview', content: },
+ { key: 'components', label: 'Components', content: },
+ { key: 'hooks', label: 'Hooks', content: },
+]
diff --git a/src/pages/LiberyPage/tabs/Components/index.tsx b/src/pages/LiberyPage/tabs/Components/index.tsx
new file mode 100644
index 0000000..a8c9f7c
--- /dev/null
+++ b/src/pages/LiberyPage/tabs/Components/index.tsx
@@ -0,0 +1,5 @@
+import { Typography as T } from '../../../../core/typography'
+
+export default function ComponentsTab() {
+ return Components content… Buttons, badges, modals…
+}
diff --git a/src/pages/LiberyPage/tabs/Components/previews/ButtonPreview.tsx b/src/pages/LiberyPage/tabs/Components/previews/ButtonPreview.tsx
new file mode 100644
index 0000000..e69de29
diff --git a/src/pages/LiberyPage/tabs/Hooks/index.tsx b/src/pages/LiberyPage/tabs/Hooks/index.tsx
new file mode 100644
index 0000000..2abd84c
--- /dev/null
+++ b/src/pages/LiberyPage/tabs/Hooks/index.tsx
@@ -0,0 +1,9 @@
+import { Typography as T } from '../../../../core/typography'
+
+export default function HooksTab() {
+ return (
+
+ Hooks content… useToggle, useLocalStorage, useCounter…
+
+ )
+}
diff --git a/src/pages/LiberyPage/tabs/Overview/index.tsx b/src/pages/LiberyPage/tabs/Overview/index.tsx
new file mode 100644
index 0000000..19e7333
--- /dev/null
+++ b/src/pages/LiberyPage/tabs/Overview/index.tsx
@@ -0,0 +1,21 @@
+import List from '../../../../components/common/molecules/List/List'
+import { FlexContainer } from '../../../../components/common/styled'
+import SectionCard from '../../../../components/common/templates/SectionCard'
+import tokens from '../../../../core/tokens'
+import { Typography as T } from '../../../../core/typography'
+import { overviewSections } from './overview'
+
+export default function OverviewTab() {
+ return (
+
+ {overviewSections.map((section) => (
+
+ {section.content && {section.content}}
+ {section.lists?.map((list) => (
+
+ ))}
+
+ ))}
+
+ )
+}
diff --git a/src/pages/LiberyPage/tabs/Overview/overview.ts b/src/pages/LiberyPage/tabs/Overview/overview.ts
new file mode 100644
index 0000000..9851be2
--- /dev/null
+++ b/src/pages/LiberyPage/tabs/Overview/overview.ts
@@ -0,0 +1,32 @@
+export const overviewSections = [
+ {
+ title: '🚀 React-Vite Template',
+ content: `A modern, production-ready React template built with Vite,
+ featuring reusable components, custom hooks, and best practices
+ for scalable applications.`,
+ },
+ {
+ title: "📦 What's Included",
+ lists: [
+ {
+ title: 'Components',
+ items: [
+ 'Button (multiple variants & sizes)',
+ 'Card (flexible content container)',
+ 'Modal (accessible & responsive)',
+ 'Badge (status indicators)',
+ 'Tabs (navigation component)',
+ ],
+ },
+ {
+ title: 'Custom Hooks',
+ items: [
+ 'useLocalStorage (persistent state)',
+ 'useToggle (boolean state management)',
+ 'useCounter (numeric state with actions)',
+ 'useTheme (theme switching)',
+ ],
+ },
+ ],
+ },
+]