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 ( + + ) + })} + + ) +} 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)', + ], + }, + ], + }, +]