├── .env.example ├── .eslintrc.json ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── biome.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── fonts │ └── Inter.ttf ├── images │ ├── avatar.jpg │ ├── gallery │ │ ├── horizontal-1.jpg │ │ ├── horizontal-2.jpg │ │ ├── horizontal-3.jpg │ │ ├── horizontal-4.jpg │ │ ├── vertical-1.jpg │ │ ├── vertical-2.jpg │ │ ├── vertical-3.jpg │ │ └── vertical-4.jpg │ ├── og │ │ └── home.jpg │ └── projects │ │ └── project-01 │ │ ├── avatar-01.jpg │ │ ├── cover-01.jpg │ │ ├── cover-02.jpg │ │ ├── cover-03.jpg │ │ ├── cover-04.jpg │ │ ├── image-01.jpg │ │ ├── image-02.jpg │ │ ├── image-03.jpg │ │ └── video-01.mp4 └── trademark │ ├── icon-dark.svg │ ├── icon-light.svg │ ├── type-dark.svg │ └── type-light.svg ├── src ├── app │ ├── about │ │ └── page.tsx │ ├── api │ │ ├── authenticate │ │ │ └── route.ts │ │ └── check-auth │ │ │ └── route.ts │ ├── blog │ │ ├── [slug] │ │ │ └── page.tsx │ │ ├── page.tsx │ │ └── posts │ │ │ ├── blog.mdx │ │ │ ├── components.mdx │ │ │ ├── content.mdx │ │ │ ├── localization.mdx │ │ │ ├── mailchimp.mdx │ │ │ ├── pages.mdx │ │ │ ├── password.mdx │ │ │ ├── quick-start.mdx │ │ │ ├── seo.mdx │ │ │ ├── styling.mdx │ │ │ └── work.mdx │ ├── favicon.ico │ ├── gallery │ │ └── page.tsx │ ├── layout.tsx │ ├── not-found.tsx │ ├── og │ │ └── route.tsx │ ├── page.tsx │ ├── resources │ │ ├── config.js │ │ ├── content.js │ │ └── index.ts │ ├── robots.ts │ ├── sitemap.ts │ ├── utils │ │ ├── formatDate.ts │ │ └── utils.ts │ └── work │ │ ├── [slug] │ │ └── page.tsx │ │ ├── page.tsx │ │ └── projects │ │ ├── automate-design-handovers-with-a-figma-to-code-pipeline.mdx │ │ ├── building-once-ui-a-customizable-design-system.mdx │ │ └── simple-portfolio-builder.mdx ├── components │ ├── Footer.module.scss │ ├── Footer.tsx │ ├── Header.module.scss │ ├── Header.tsx │ ├── HeadingLink.module.scss │ ├── HeadingLink.tsx │ ├── Mailchimp.tsx │ ├── ProjectCard.module.scss │ ├── ProjectCard.tsx │ ├── RouteGuard.tsx │ ├── ScrollToHash.tsx │ ├── ThemeToggle.module.scss │ ├── ThemeToggle.tsx │ ├── about │ │ ├── TableOfContents.tsx │ │ └── about.module.scss │ ├── blog │ │ ├── Post.tsx │ │ ├── Posts.module.scss │ │ └── Posts.tsx │ ├── gallery │ │ ├── Gallery.module.scss │ │ └── MasonryGrid.tsx │ ├── index.ts │ ├── mdx.tsx │ └── work │ │ ├── Projects.module.scss │ │ └── Projects.tsx └── once-ui │ ├── components │ ├── Accordion.module.scss │ ├── Accordion.tsx │ ├── AccordionGroup.tsx │ ├── Arrow.module.scss │ ├── Arrow.tsx │ ├── Avatar.module.scss │ ├── Avatar.tsx │ ├── AvatarGroup.module.scss │ ├── AvatarGroup.tsx │ ├── Background.module.scss │ ├── Background.tsx │ ├── Badge.module.scss │ ├── Badge.tsx │ ├── Button.module.scss │ ├── Button.tsx │ ├── Card.module.scss │ ├── Card.tsx │ ├── Carousel.tsx │ ├── Checkbox.tsx │ ├── Chip.module.scss │ ├── Chip.tsx │ ├── ColorInput.tsx │ ├── Column.tsx │ ├── CompareImage.module.scss │ ├── CompareImage.tsx │ ├── DateInput.tsx │ ├── DatePicker.module.scss │ ├── DatePicker.tsx │ ├── DateRangeInput.tsx │ ├── DateRangePicker.tsx │ ├── Dialog.module.scss │ ├── Dialog.tsx │ ├── Dropdown.tsx │ ├── DropdownWrapper.module.scss │ ├── DropdownWrapper.tsx │ ├── ElementType.tsx │ ├── Fade.module.scss │ ├── Fade.tsx │ ├── Feedback.tsx │ ├── Flex.tsx │ ├── GlitchFx.module.scss │ ├── GlitchFx.tsx │ ├── Grid.tsx │ ├── Heading.tsx │ ├── HeadingLink.module.scss │ ├── HeadingLink.tsx │ ├── HeadingNav.tsx │ ├── HoloFx.module.scss │ ├── HoloFx.tsx │ ├── Icon.module.scss │ ├── Icon.tsx │ ├── IconButton.module.scss │ ├── IconButton.tsx │ ├── InlineCode.module.scss │ ├── InlineCode.tsx │ ├── Input.module.scss │ ├── Input.tsx │ ├── InteractiveDetails.tsx │ ├── Kbar.module.scss │ ├── Kbar.tsx │ ├── Kbd.tsx │ ├── LetterFx.tsx │ ├── Line.tsx │ ├── Logo.module.scss │ ├── Logo.tsx │ ├── LogoCloud.module.scss │ ├── LogoCloud.tsx │ ├── MegaMenu.module.scss │ ├── MegaMenu.tsx │ ├── NavIcon.module.scss │ ├── NavIcon.tsx │ ├── NumberInput.module.scss │ ├── NumberInput.tsx │ ├── OTPInput.module.scss │ ├── OTPInput.tsx │ ├── Option.module.scss │ ├── Option.tsx │ ├── PasswordInput.tsx │ ├── RadioButton.tsx │ ├── RevealFx.module.scss │ ├── RevealFx.tsx │ ├── Row.tsx │ ├── ScrollToTop.module.scss │ ├── ScrollToTop.tsx │ ├── Scroller.module.scss │ ├── Scroller.tsx │ ├── SegmentedControl.tsx │ ├── Select.tsx │ ├── SharedInteractiveStyles.module.scss │ ├── Skeleton.module.scss │ ├── Skeleton.tsx │ ├── SmartImage.tsx │ ├── SmartLink.tsx │ ├── Spinner.module.scss │ ├── Spinner.tsx │ ├── StatusIndicator.module.scss │ ├── StatusIndicator.tsx │ ├── StyleOverlay.module.scss │ ├── StyleOverlay.tsx │ ├── StylePanel.module.scss │ ├── StylePanel.tsx │ ├── Switch.module.scss │ ├── Switch.tsx │ ├── Table.tsx │ ├── Tag.module.scss │ ├── Tag.tsx │ ├── TagInput.tsx │ ├── Text.tsx │ ├── Textarea.tsx │ ├── ThemeProvider.tsx │ ├── ThemeSwitcher.tsx │ ├── TiltFx.module.scss │ ├── TiltFx.tsx │ ├── Toast.module.scss │ ├── Toast.tsx │ ├── ToastProvider.tsx │ ├── Toaster.module.scss │ ├── Toaster.tsx │ ├── ToggleButton.module.scss │ ├── ToggleButton.tsx │ ├── Tooltip.tsx │ ├── User.tsx │ ├── UserMenu.module.scss │ ├── UserMenu.tsx │ └── index.ts │ ├── hooks │ ├── generateHeadingLinks.ts │ ├── useDebounce.ts │ └── useTheme.ts │ ├── icons.ts │ ├── interfaces.ts │ ├── modules │ ├── code │ │ ├── CodeBlock.module.scss │ │ ├── CodeBlock.tsx │ │ ├── CodeHighlight.css │ │ ├── LineNumber.css │ │ └── prismjs.d.ts │ ├── index.ts │ └── seo │ │ ├── Meta.tsx │ │ └── Schema.tsx │ ├── styles │ ├── background.scss │ ├── border.scss │ ├── breakpoints.scss │ ├── color.scss │ ├── display.scss │ ├── flex.scss │ ├── global.scss │ ├── grid.scss │ ├── index.scss │ ├── layout.scss │ ├── position.scss │ ├── shadow.scss │ ├── size.scss │ ├── spacing.scss │ ├── typography.scss │ └── utilities.scss │ ├── tokens │ ├── border.scss │ ├── function.scss │ ├── index.scss │ ├── layout.scss │ ├── scheme.scss │ ├── shadow.scss │ ├── theme.scss │ └── typography.scss │ └── types.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | # This file is a placeholder for environment variables required to run this project. 2 | # Currently, there are no environment variables needed. 3 | 4 | # Example of what future variables might look like: 5 | # NEXT_PUBLIC_API_KEY=your_api_key_here 6 | # DATABASE_URL=your_database_url_here 7 | # SECRET_KEY=your_secret_key_here 8 | 9 | # Password for protected pages/routes 10 | PAGE_ACCESS_PASSWORD=password 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [once-ui-system, lorant-one] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial 4.0 International 2 | CC BY-NC 4.0 3 | 4 | By using this work, you agree to the following terms: 5 | 6 | You are free to: 7 | - **Share** — copy and redistribute the material in any medium or format 8 | - **Adapt** — remix, transform, and build upon the material 9 | 10 | Under the following conditions: 11 | - **Attribution** — You must give appropriate credit, provide a link to the license, and indicate if changes were made. 12 | - **NonCommercial** — You may not use the material for **commercial purposes**. 13 | 14 | No additional restrictions — You may not apply legal terms or technological measures that legally restrict others from doing anything the license permits. 15 | 16 | Full license text available at: 17 | [https://creativecommons.org/licenses/by-nc/4.0/legalcode](https://creativecommons.org/licenses/by-nc/4.0/legalcode) 18 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "space", 15 | "indentWidth": 2, 16 | "lineWidth": 100 17 | }, 18 | "organizeImports": { 19 | "enabled": true 20 | }, 21 | "linter": { 22 | "enabled": true, 23 | "rules": { "recommended": true } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "double" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import mdx from "@next/mdx"; 2 | 3 | const withMDX = mdx({ 4 | extension: /\.mdx?$/, 5 | options: {}, 6 | }); 7 | 8 | /** @type {import('next').NextConfig} */ 9 | const nextConfig = { 10 | pageExtensions: ["ts", "tsx", "md", "mdx"], 11 | transpilePackages: ["next-mdx-remote"], 12 | sassOptions: { 13 | compiler: "modern", 14 | silenceDeprecations: ["legacy-js-api"], 15 | }, 16 | }; 17 | 18 | export default withMDX(nextConfig); 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@once-ui-system/magic-portfolio", 3 | "version": "2.0.0", 4 | "scripts": { 5 | "dev": "next dev", 6 | "export": "next export", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@floating-ui/react-dom": "^2.1.2", 13 | "@mdx-js/loader": "^3.1.0", 14 | "@next/mdx": "^15.3.1", 15 | "classnames": "^2.5.1", 16 | "cookie": "^1.0.2", 17 | "gray-matter": "^4.0.3", 18 | "next": "^15.3.1", 19 | "next-mdx-remote": "^5.0.0", 20 | "postcss": "^8.5.3", 21 | "postcss-preset-env": "^9.6.0", 22 | "prismjs": "^1.30.0", 23 | "react": "19.0.0", 24 | "react-dom": "19.0.0", 25 | "react-icons": "^5.5.0", 26 | "react-masonry-css": "^1.0.16", 27 | "remark": "^15.0.1", 28 | "remark-html": "^16.0.1", 29 | "sass": "^1.86.3", 30 | "sharp": "^0.34.1" 31 | }, 32 | "devDependencies": { 33 | "@csstools/postcss-global-data": "^2.1.1", 34 | "@types/cookie": "^0.6.0", 35 | "@types/node": "^20.17.30", 36 | "@types/react": "^18.3.20", 37 | "@types/react-dom": "^18.3.6", 38 | "eslint": "^9.25.0", 39 | "postcss-custom-media": "^10.0.8", 40 | "postcss-flexbugs-fixes": "^5.0.2", 41 | "typescript": "^5.8.3" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | [ 4 | "@csstools/postcss-global-data", 5 | { 6 | files: ["src/once-ui/styles/breakpoints.scss"], 7 | }, 8 | ], 9 | "postcss-custom-media", 10 | "postcss-flexbugs-fixes", 11 | [ 12 | "postcss-preset-env", 13 | { 14 | autoprefixer: { 15 | flexbox: "no-2009", 16 | }, 17 | stage: 3, 18 | features: { 19 | "custom-properties": false, 20 | }, 21 | }, 22 | ], 23 | ], 24 | }; 25 | -------------------------------------------------------------------------------- /public/fonts/Inter.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/fonts/Inter.ttf -------------------------------------------------------------------------------- /public/images/avatar.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/avatar.jpg -------------------------------------------------------------------------------- /public/images/gallery/horizontal-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/gallery/horizontal-1.jpg -------------------------------------------------------------------------------- /public/images/gallery/horizontal-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/gallery/horizontal-2.jpg -------------------------------------------------------------------------------- /public/images/gallery/horizontal-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/gallery/horizontal-3.jpg -------------------------------------------------------------------------------- /public/images/gallery/horizontal-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/gallery/horizontal-4.jpg -------------------------------------------------------------------------------- /public/images/gallery/vertical-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/gallery/vertical-1.jpg -------------------------------------------------------------------------------- /public/images/gallery/vertical-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/gallery/vertical-2.jpg -------------------------------------------------------------------------------- /public/images/gallery/vertical-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/gallery/vertical-3.jpg -------------------------------------------------------------------------------- /public/images/gallery/vertical-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/gallery/vertical-4.jpg -------------------------------------------------------------------------------- /public/images/og/home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/og/home.jpg -------------------------------------------------------------------------------- /public/images/projects/project-01/avatar-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/projects/project-01/avatar-01.jpg -------------------------------------------------------------------------------- /public/images/projects/project-01/cover-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/projects/project-01/cover-01.jpg -------------------------------------------------------------------------------- /public/images/projects/project-01/cover-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/projects/project-01/cover-02.jpg -------------------------------------------------------------------------------- /public/images/projects/project-01/cover-03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/projects/project-01/cover-03.jpg -------------------------------------------------------------------------------- /public/images/projects/project-01/cover-04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/projects/project-01/cover-04.jpg -------------------------------------------------------------------------------- /public/images/projects/project-01/image-01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/projects/project-01/image-01.jpg -------------------------------------------------------------------------------- /public/images/projects/project-01/image-02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/projects/project-01/image-02.jpg -------------------------------------------------------------------------------- /public/images/projects/project-01/image-03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/projects/project-01/image-03.jpg -------------------------------------------------------------------------------- /public/images/projects/project-01/video-01.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/public/images/projects/project-01/video-01.mp4 -------------------------------------------------------------------------------- /public/trademark/icon-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /public/trademark/icon-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/app/api/authenticate/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import * as cookie from "cookie"; 3 | 4 | export async function POST(request: NextRequest) { 5 | const body = await request.json(); 6 | const { password } = body; 7 | const correctPassword = process.env.PAGE_ACCESS_PASSWORD; 8 | 9 | if (!correctPassword) { 10 | console.error('PAGE_ACCESS_PASSWORD environment variable is not set'); 11 | return NextResponse.json({ message: "Internal server error" }, { status: 500 }); 12 | } 13 | 14 | if (password === correctPassword) { 15 | const response = NextResponse.json({ success: true }, { status: 200 }); 16 | 17 | response.headers.set( 18 | "Set-Cookie", 19 | cookie.serialize("authToken", "authenticated", { 20 | httpOnly: true, 21 | secure: process.env.NODE_ENV === "production", 22 | maxAge: 60 * 60, 23 | sameSite: "strict", 24 | path: "/", 25 | }) 26 | ); 27 | 28 | return response; 29 | } else { 30 | return NextResponse.json({ message: "Incorrect password" }, { status: 401 }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/api/check-auth/route.ts: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | import * as cookie from "cookie"; 3 | 4 | export async function GET(request: NextRequest) { 5 | const cookieHeader = request.headers.get("cookie") || ""; 6 | const cookies = cookie.parse(cookieHeader); 7 | 8 | if (cookies.authToken === "authenticated") { 9 | return NextResponse.json({ authenticated: true }, { status: 200 }); 10 | } else { 11 | return NextResponse.json({ authenticated: false }, { status: 401 }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/blog/page.tsx: -------------------------------------------------------------------------------- 1 | import { Column, Heading } from "@/once-ui/components"; 2 | import { Mailchimp } from "@/components"; 3 | import { Posts } from "@/components/blog/Posts"; 4 | import { baseURL } from "@/app/resources"; 5 | import { blog, person, newsletter } from "@/app/resources/content"; 6 | import { Meta, Schema } from "@/once-ui/modules"; 7 | 8 | export async function generateMetadata() { 9 | return Meta.generate({ 10 | title: blog.title, 11 | description: blog.description, 12 | baseURL: baseURL, 13 | image: `${baseURL}/og?title=${encodeURIComponent(blog.title)}`, 14 | path: blog.path, 15 | }); 16 | } 17 | 18 | export default function Blog() { 19 | return ( 20 | 21 | 34 | 35 | {blog.title} 36 | 37 | 39 | 40 | 41 | 42 | 43 | {newsletter.display && } 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/app/blog/posts/blog.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Create, edit and delete blog posts" 3 | summary: "Create, edit and delete posts in the blog section of your Magic Portfolio." 4 | publishedAt: "2025-03-17" 5 | tag: "Magic Portfolio" 6 | --- 7 | 8 | ## Manage posts 9 | 10 | You can create, edit and delete posts by adding, modifying or removing `*.mdx` files in the `src/app/blog/posts` directory. 11 | 12 | ## Frontmatter 13 | 14 | The frontmatter is used to set the post's metadata: title, description, image, and tags. It's used in the post's page and in meta- and open graph tags. 15 | 16 | 33 | 34 | ## MDX syntax 35 | 36 | You can use MDX to write the content of your posts. It helps you write rich and dynamic content with minimal code. Some MDX elements will be automatically transformed to Once UI components to integrate better in the design and add additional functionality. 37 | 38 | ## Custom components 39 | 40 | You can use custom components in MDX files, but you need to import them first in the `src/components/mdx.tsx` file. 41 | 42 | 66 | 67 | As you can see, the `Table` and `CodeBlock` components are already imported and available for use. You can add more by simply importing them to this file and passing them to the `components` object. 68 | 69 | ## Hot reload 70 | 71 | Hot reload of MDX files is currently not supported, but we're working on it. -------------------------------------------------------------------------------- /src/app/blog/posts/localization.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Localized version of Magic Portfolio" 3 | summary: "Magic Portfolio was localized by François Hernandez." 4 | publishedAt: "2025-04-15" 5 | tag: "Magic Portfolio" 6 | --- 7 | 8 | In the latest version of Magic Portfolio, localization is no longer supported. However, the localized version created by [François Hernandez](https://github.com/francoishernandez) is still available as a [separate branch](https://github.com/once-ui-system/magic-portfolio/tree/localization-(Once-UI-0.4.2)). This branch will not receive further updates. -------------------------------------------------------------------------------- /src/app/blog/posts/mailchimp.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Set up Mailchimp and collect emails" 3 | summary: "Set up the Mailchimp newsletter block for your Magic Portfolio." 4 | publishedAt: "2025-04-16" 5 | tag: "Magic Portfolio" 6 | --- 7 | 8 | ## Mailchimp account 9 | 10 | Sign up for a [Mailchimp account](https://mailchimp.com) and create a new list if you don't have one. 11 | 12 | ## Embed form 13 | 14 | Create a new [embed form](https://mailchimp.com/help/add-a-signup-form-to-your-website/) and copy the form URL to your Magic Portfolio config. 15 | 16 | 31 | 32 | ## Newsletter content 33 | 34 | You can modify the headline and description in the `content.js` file. 35 | 36 | Subscribe to {person.firstName}'s Newsletter>, 44 | description: ( 45 | <> 46 | I occasionally write about design, technology, and share thoughts on the intersection of creativity and engineering. 47 | > 48 | ), 49 | };`, 50 | language: "tsx", 51 | label: "src/app/resources/content.js" 52 | } 53 | ]} /> 54 | 55 | ## Background effect 56 | 57 | There's a pre-configured background in `Mailchimp.tsx` that you can modify in the `mailchimp` object. Set graphic elements such as gradient, dots, lines, and grid and configure their appearance for the newsletter block. 58 | 59 | -------------------------------------------------------------------------------- /src/app/blog/posts/pages.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Enable or disable pages for your portfolio" 3 | summary: "Magic Portfolio's RouteGuard component takes care of conditionally rendering pages based on your settings." 4 | publishedAt: "2025-04-22" 5 | tag: "Magic Portfolio" 6 | --- 7 | 8 | ## Enable or disable pages 9 | 10 | Magic Portfolio's `RouteGuard` component takes care of conditionally rendering pages based on the `routes` object in the `resources/config.js` file. 11 | 12 | 29 | 30 | The code above will ensure that the `/gallery` page is not accessible and is not displayed in the navigation. 31 | 32 | ## Add new pages 33 | 34 | When creating a new page, it needs to be added to the `routes` object in the `resources/config.js` file. 35 | 36 | 54 | 55 | The code above will ensure that the `/music` page is accessible. Users will be able to navigate to it after adding it to the navigation menu in the `components/Header.tsx` file. -------------------------------------------------------------------------------- /src/app/blog/posts/password.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Create password protected pages" 3 | summary: "Create password protected pages through the RouteGuard component." 4 | publishedAt: "2025-04-17" 5 | tag: "Magic Portfolio" 6 | --- 7 | 8 | ## Add protected pages 9 | 10 | To enable password protection for specific pages, add the page paths you want to protect to the `protectedRoutes` object in the `resources/config.js` file. 11 | 12 | The `RouteGuard` component will automatically handle access control for these pages, requiring password authentication before allowing access. 13 | 14 | 27 | 28 | The code above will ensure that the `/work/once-ui` page is only accessible after providing a password. 29 | 30 | ## Set a password 31 | 32 | The password can be set in the `.env.local` file for local development and as an [environment variable](https://vercel.com/docs/environment-variables) for production. -------------------------------------------------------------------------------- /src/app/blog/posts/quick-start.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Quick start with Magic Portfolio" 3 | summary: "Magic Portfolio is a comprehensive, MDX-based, SEO-friendly, responsive portfolio template built with Once UI and Next.js." 4 | image: "/images/og/home.jpg" 5 | publishedAt: "2025-04-23" 6 | tag: "Magic Portfolio" 7 | --- 8 | 9 | ## About 10 | 11 | Magic Portfolio is a comprehensive, MDX-based, SEO-friendly, responsive portfolio template built with Once UI and Next.js. 12 | 13 | ## License 14 | 15 | Magic Portfolio is licensed under the [CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/). You can only use it for personal purposes, and you must attribute the original work. The attribution is added in the footer by default, but you can place it in any other, visible part of your site. 16 | 17 | Subscribe to the [Once UI Pro plan](https://once-ui.com/pricing) to extend the license to [Dopler CC](https://once-ui.com/products/magic-portfolio). 18 | 19 | ## Quick start 20 | 21 | Clone the git repository: 22 | 23 | 29 | 30 | Install the necessary dependencies: 31 | 32 | 38 | 39 | Start the local development server: 40 | 41 | -------------------------------------------------------------------------------- /src/app/blog/posts/seo.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "SEO settings for Magic Portfolio" 3 | summary: "Manage your Magic Portfolio's SEO settings." 4 | publishedAt: "2025-04-14" 5 | tag: "Magic Portfolio" 6 | --- 7 | 8 | ## Meta tags 9 | 10 | Magic Portfolio automatically generates meta- and open graph tags for your pages based on the `content.js` file. 11 | 12 | ## Schema 13 | 14 | Magic Portfolio automatically generates schema tags for your pages based on the `content.js` file. 15 | 16 | ## Open Graph image 17 | 18 | Social sharing images (open-graph and twitter) are automatically generated with `next/og`. 19 | 20 | 24 | Make sure you have updated the content.js file with your own information and the baseURL variable in the config.js file. 25 | >} 26 | /> 27 | 28 | 45 | 46 | The above declaration generates the social image with the title of the page. You can use a static image by replacing the `ogImage` value with a path to an image in the `public` directory. -------------------------------------------------------------------------------- /src/app/blog/posts/styling.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Styling your portfolio" 3 | summary: "Magic Portfolio's styling is based on Once UI's customization through data-attributes." 4 | publishedAt: "2025-04-21" 5 | tag: "Magic Portfolio" 6 | --- 7 | 8 | ## Global style 9 | 10 | Magic Portfolio's styling is based on Once UI's customization through data-attributes. You can generate a custom color palette for brand, accent and neutral colors on [Once UI](https://once-ui.com/customize) where you'll find instructions on how to apply it. 11 | 12 | 31 | 32 | ## Background effect 33 | 34 | There's a pre-configured background in `layout.tsx` that you can modify in the config file. Set graphic elements such as gradient, dots, lines, and grid and configure their appearance. 35 | 36 | -------------------------------------------------------------------------------- /src/app/blog/posts/work.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Create,edit and delete projects" 3 | summary: "Create, edit and delete projects in the work section of your Magic Portfolio." 4 | publishedAt: "2025-04-19" 5 | tag: "Magic Portfolio" 6 | --- 7 | 8 | ## Manage projects 9 | 10 | You can create, edit and delete projects by adding, modifying or removing `*.mdx` files in the `src/app/work/projects` directory. 11 | 12 | ## Frontmatter 13 | 14 | The frontmatter is used to set the project's metadata: title, description, image, and tags. It's used in the project's page and in meta- and open graph tags. 15 | 16 | 40 | 41 | ## MDX syntax 42 | 43 | You can use MDX to write the content of your projects. It helps you write rich and dynamic content with minimal code. Some MDX elements will be automatically transformed to Once UI components to integrate better in the design and add additional functionality. 44 | 45 | ## Custom components 46 | 47 | You can use custom components in MDX files, but you need to import them first in the `src/components/mdx.tsx` file. 48 | 49 | 73 | 74 | As you can see, the `Table` and `CodeBlock` components are already imported and available for use. You can add more by simply importing them to this file and passing them to the `components` object. 75 | 76 | ## Hot reload 77 | 78 | Hot reload of MDX files is currently not supported, but we're working on it. -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/gallery/page.tsx: -------------------------------------------------------------------------------- 1 | import { Flex } from "@/once-ui/components"; 2 | import MasonryGrid from "@/components/gallery/MasonryGrid"; 3 | import { baseURL } from "@/app/resources"; 4 | import { gallery, person } from "@/app/resources/content"; 5 | import { Meta, Schema } from "@/once-ui/modules"; 6 | 7 | export async function generateMetadata() { 8 | return Meta.generate({ 9 | title: gallery.title, 10 | description: gallery.description, 11 | baseURL: baseURL, 12 | image: `${baseURL}/og?title=${encodeURIComponent(gallery.title)}`, 13 | path: gallery.path, 14 | }); 15 | } 16 | 17 | export default function Gallery() { 18 | return ( 19 | 20 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import { Column, Heading, Text } from "@/once-ui/components"; 2 | 3 | export default function NotFound() { 4 | return ( 5 | 6 | 7 | 404 8 | 9 | 10 | Page Not Found 11 | 12 | The page you are looking for does not exist. 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/app/resources/index.ts: -------------------------------------------------------------------------------- 1 | // import a pre-defined template for config and content options 2 | export { 3 | routes, 4 | protectedRoutes, 5 | effects, 6 | style, 7 | display, 8 | mailchimp, 9 | baseURL, 10 | font, 11 | } from "@/app/resources/config"; 12 | export { 13 | person, 14 | social, 15 | newsletter, 16 | home, 17 | about, 18 | blog, 19 | work, 20 | gallery, 21 | } from "@/app/resources/content"; -------------------------------------------------------------------------------- /src/app/robots.ts: -------------------------------------------------------------------------------- 1 | import { baseURL } from "@/app/resources"; 2 | 3 | export default function robots() { 4 | return { 5 | rules: [ 6 | { 7 | userAgent: "*", 8 | }, 9 | ], 10 | sitemap: `${baseURL}/sitemap.xml`, 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/sitemap.ts: -------------------------------------------------------------------------------- 1 | import { getPosts } from "@/app/utils/utils"; 2 | import { baseURL, routes as routesConfig } from "@/app/resources"; 3 | 4 | export default async function sitemap() { 5 | const blogs = getPosts(["src", "app", "blog", "posts"]).map((post) => ({ 6 | url: `https://${baseURL}/blog/${post.slug}`, 7 | lastModified: post.metadata.publishedAt, 8 | })); 9 | 10 | const works = getPosts(["src", "app", "work", "projects"]).map((post) => ({ 11 | url: `https://${baseURL}/work/${post.slug}`, 12 | lastModified: post.metadata.publishedAt, 13 | })); 14 | 15 | const activeRoutes = Object.keys(routesConfig).filter((route) => routesConfig[route as keyof typeof routesConfig]); 16 | 17 | const routes = activeRoutes.map((route) => ({ 18 | url: `https://${baseURL}${route !== "/" ? route : ""}`, 19 | lastModified: new Date().toISOString().split("T")[0], 20 | })); 21 | 22 | return [...routes, ...blogs, ...works]; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/utils/formatDate.ts: -------------------------------------------------------------------------------- 1 | export function formatDate(date: string, includeRelative = false) { 2 | const currentDate = new Date(); 3 | 4 | if (!date.includes("T")) { 5 | date = `${date}T00:00:00`; 6 | } 7 | 8 | const targetDate = new Date(date); 9 | const yearsAgo = currentDate.getFullYear() - targetDate.getFullYear(); 10 | const monthsAgo = currentDate.getMonth() - targetDate.getMonth(); 11 | const daysAgo = currentDate.getDate() - targetDate.getDate(); 12 | 13 | let formattedDate = ""; 14 | 15 | if (yearsAgo > 0) { 16 | formattedDate = `${yearsAgo}y ago`; 17 | } else if (monthsAgo > 0) { 18 | formattedDate = `${monthsAgo}mo ago`; 19 | } else if (daysAgo > 0) { 20 | formattedDate = `${daysAgo}d ago`; 21 | } else { 22 | formattedDate = "Today"; 23 | } 24 | 25 | const fullDate = targetDate.toLocaleString("en-us", { 26 | month: "long", 27 | day: "numeric", 28 | year: "numeric", 29 | }); 30 | 31 | if (!includeRelative) { 32 | return fullDate; 33 | } 34 | 35 | return `${fullDate} (${formattedDate})`; 36 | } 37 | -------------------------------------------------------------------------------- /src/app/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import matter from "gray-matter"; 4 | 5 | type Team = { 6 | name: string; 7 | role: string; 8 | avatar: string; 9 | linkedIn: string; 10 | }; 11 | 12 | type Metadata = { 13 | title: string; 14 | publishedAt: string; 15 | summary: string; 16 | image?: string; 17 | images: string[]; 18 | tag?: string; 19 | team: Team[]; 20 | link?: string; 21 | }; 22 | 23 | import { notFound } from 'next/navigation'; 24 | 25 | function getMDXFiles(dir: string) { 26 | if (!fs.existsSync(dir)) { 27 | notFound(); 28 | } 29 | 30 | return fs.readdirSync(dir).filter((file) => path.extname(file) === ".mdx"); 31 | } 32 | 33 | function readMDXFile(filePath: string) { 34 | if (!fs.existsSync(filePath)) { 35 | notFound(); 36 | } 37 | 38 | const rawContent = fs.readFileSync(filePath, "utf-8"); 39 | const { data, content } = matter(rawContent); 40 | 41 | const metadata: Metadata = { 42 | title: data.title || "", 43 | publishedAt: data.publishedAt, 44 | summary: data.summary || "", 45 | image: data.image || "", 46 | images: data.images || [], 47 | tag: data.tag || [], 48 | team: data.team || [], 49 | link: data.link || "", 50 | }; 51 | 52 | return { metadata, content }; 53 | } 54 | 55 | function getMDXData(dir: string) { 56 | const mdxFiles = getMDXFiles(dir); 57 | return mdxFiles.map((file) => { 58 | const { metadata, content } = readMDXFile(path.join(dir, file)); 59 | const slug = path.basename(file, path.extname(file)); 60 | 61 | return { 62 | metadata, 63 | slug, 64 | content, 65 | }; 66 | }); 67 | } 68 | 69 | export function getPosts(customPath = ["", "", "", ""]) { 70 | const postsDir = path.join(process.cwd(), ...customPath); 71 | return getMDXData(postsDir); 72 | } 73 | -------------------------------------------------------------------------------- /src/app/work/page.tsx: -------------------------------------------------------------------------------- 1 | import { Column } from "@/once-ui/components"; 2 | import { baseURL } from "@/app/resources"; 3 | import { about, person, work } from "@/app/resources/content"; 4 | import { Meta, Schema } from "@/once-ui/modules"; 5 | import { Projects } from "@/components/work/Projects"; 6 | 7 | export async function generateMetadata() { 8 | return Meta.generate({ 9 | title: work.title, 10 | description: work.description, 11 | baseURL: baseURL, 12 | image: `${baseURL}/og?title=${encodeURIComponent(work.title)}`, 13 | path: work.path, 14 | }); 15 | } 16 | 17 | export default function Work() { 18 | return ( 19 | 20 | 33 | 34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/app/work/projects/building-once-ui-a-customizable-design-system.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Building Once UI, a Customizable Design System" 3 | publishedAt: "2024-04-08" 4 | summary: "Development of a flexible and highly customizable design system using Next.js for front-end and Figma for design collaboration." 5 | images: 6 | - "/images/projects/project-01/cover-01.jpg" 7 | - "/images/projects/project-01/cover-02.jpg" 8 | - "/images/projects/project-01/cover-03.jpg" 9 | - "/images/projects/project-01/cover-04.jpg" 10 | team: 11 | - name: "Selene Yu" 12 | role: "Software Engineer" 13 | avatar: "/images/avatar.jpg" 14 | linkedIn: "https://www.linkedin.com/company/once-ui/" 15 | - name: "Jane Smith" 16 | role: "Product Manager" 17 | avatar: "/images/projects/project-01/avatar-01.jpg" 18 | linkedIn: "https://www.linkedin.com/company/once-ui/" 19 | --- 20 | 21 | ## Overview 22 | 23 | Development of a flexible and highly customizable design system using Next.js for front-end and Figma for design collaboration. 24 | 25 | ## Key Features 26 | 27 | - **Component Library**: Built a set of modular, reusable UI components using React and styled-components in Next.js, focusing on accessibility and responsiveness. 28 | - **Theming and Customization**: Integrated a theming system that allows easy switching and customization of color palettes, typography, and layout styles using CSS variables and Figma tokens. 29 | - **Figma Integration**: Collaborated closely with designers by setting up a shared design library in Figma. This library was synchronized with the codebase, ensuring design handoffs were seamless and that design tokens remained consistent across both platforms. 30 | - **Documentation and Usage Guidelines**: Developed comprehensive documentation with Storybook to showcase components, usage patterns, and best practices, ensuring the design system is easy to adopt by other teams. 31 | 32 | ## Technologies Used 33 | 34 | - **Next.js**: For fast, server-rendered React applications. 35 | - **Figma**: For creating and managing design assets and prototypes. 36 | - **Styled-Components**: For styling React components with a modular, themable approach. 37 | - **Storybook**: For building an interactive, documented component library. 38 | 39 | ## Challenges and Learnings 40 | 41 | One key challenge was balancing the need for flexibility with the desire to maintain design consistency. The solution involved creating well-defined design tokens and establishing clear guidelines for when and how components could be customized. Additionally, setting up effective collaboration workflows between designers and developers using Figma and Git was a learning experience that greatly improved the process. 42 | 43 | ## Outcome 44 | 45 | The design system is now actively used across multiple projects, leading to faster development cycles, fewer design inconsistencies, and improved collaboration between design and development teams. It has become a foundation for scaling our products efficiently while ensuring a cohesive user experience. -------------------------------------------------------------------------------- /src/app/work/projects/simple-portfolio-builder.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Once UI: Open-source design system" 3 | publishedAt: "2024-04-08" 4 | images: 5 | - "/images/projects/project-01/cover-04.jpg" 6 | - "/images/projects/project-01/video-01.mp4" 7 | --- -------------------------------------------------------------------------------- /src/components/Footer.module.scss: -------------------------------------------------------------------------------- 1 | @media (--s) { 2 | .mobile { 3 | text-align: center; 4 | flex-direction: column; 5 | align-items: center; 6 | } 7 | } -------------------------------------------------------------------------------- /src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import { Flex, IconButton, SmartLink, Text } from "@/once-ui/components"; 2 | import { person, social } from "@/app/resources/content"; 3 | import styles from "./Footer.module.scss"; 4 | 5 | export const Footer = () => { 6 | const currentYear = new Date().getFullYear(); 7 | 8 | return ( 9 | 16 | 25 | 26 | © {currentYear} / 27 | {person.name} 28 | 29 | {/* Usage of this template requires attribution. Please don't remove the link to Once UI. */} 30 | / Build your portfolio with{" "} 31 | 34 | Once UI 35 | 36 | 37 | 38 | 39 | {social.map( 40 | (item) => 41 | item.link && ( 42 | 50 | ), 51 | )} 52 | 53 | 54 | 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/Header.module.scss: -------------------------------------------------------------------------------- 1 | .position { 2 | position: sticky; 3 | top: 0; 4 | } 5 | 6 | .mask { 7 | pointer-events: none; 8 | backdrop-filter: blur(0.5rem); 9 | background: linear-gradient(to bottom, var(--page-background), var(--static-transparent)); 10 | mask-image: linear-gradient(rgba(0,0,0) 25%, rgba(0, 0, 0, 0) 100%); 11 | mask-size: 100% 100%; 12 | } 13 | 14 | @media (--s) { 15 | .position { 16 | top: auto; 17 | position: fixed; 18 | bottom: var(--static-space-24); 19 | } 20 | 21 | .mask { 22 | transform: rotate(180deg); 23 | bottom: 0; 24 | } 25 | } -------------------------------------------------------------------------------- /src/components/HeadingLink.module.scss: -------------------------------------------------------------------------------- 1 | .control { 2 | cursor: pointer; 3 | 4 | &:hover { 5 | .visibility { 6 | opacity: 1; 7 | } 8 | 9 | .text { 10 | text-decoration-line: underline; 11 | } 12 | } 13 | } 14 | 15 | .text { 16 | text-decoration-thickness: 1px; 17 | text-underline-offset: 0.25em; 18 | text-decoration-color: var(--neutral-border-strong); 19 | } 20 | 21 | .visibility { 22 | opacity: 0; 23 | transform: scale(0.875); 24 | } -------------------------------------------------------------------------------- /src/components/HeadingLink.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Heading, Flex, IconButton, useToast } from "@/once-ui/components"; 5 | 6 | import styles from "@/components/HeadingLink.module.scss"; 7 | 8 | interface HeadingLinkProps { 9 | id: string; 10 | level: 1 | 2 | 3 | 4 | 5 | 6; 11 | children: React.ReactNode; 12 | style?: React.CSSProperties; 13 | } 14 | 15 | export const HeadingLink: React.FC = ({ id, level, children, style }) => { 16 | const { addToast } = useToast(); 17 | 18 | const copyURL = (id: string): void => { 19 | const url = `${window.location.origin}${window.location.pathname}#${id}`; 20 | navigator.clipboard.writeText(url).then( 21 | () => { 22 | addToast({ 23 | variant: "success", 24 | message: "Link copied to clipboard.", 25 | }); 26 | }, 27 | () => { 28 | addToast({ 29 | variant: "danger", 30 | message: "Failed to copy link.", 31 | }); 32 | }, 33 | ); 34 | }; 35 | 36 | const variantMap = { 37 | 1: "display-strong-xs", 38 | 2: "heading-strong-xl", 39 | 3: "heading-strong-l", 40 | 4: "heading-strong-m", 41 | 5: "heading-strong-s", 42 | 6: "heading-strong-xs", 43 | } as const; 44 | 45 | const variant = variantMap[level]; 46 | const asTag = `h${level}` as keyof JSX.IntrinsicElements; 47 | 48 | return ( 49 | copyURL(id)} 52 | className={styles.control} 53 | vertical="center" 54 | gap="4" 55 | > 56 | 57 | {children} 58 | 59 | 67 | 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/components/ProjectCard.module.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/once-ui-system/magic-portfolio/93aaca14290c4a3fbb038704463fd088eb73ae03/src/components/ProjectCard.module.scss -------------------------------------------------------------------------------- /src/components/ProjectCard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | AvatarGroup, 5 | Carousel, 6 | Column, 7 | Flex, 8 | Heading, 9 | SmartLink, 10 | Text, 11 | } from "@/once-ui/components"; 12 | 13 | interface ProjectCardProps { 14 | href: string; 15 | priority?: boolean; 16 | images: string[]; 17 | title: string; 18 | content: string; 19 | description: string; 20 | avatars: { src: string }[]; 21 | link: string; 22 | } 23 | 24 | export const ProjectCard: React.FC = ({ 25 | href, 26 | images = [], 27 | title, 28 | content, 29 | description, 30 | avatars, 31 | link, 32 | }) => { 33 | return ( 34 | 35 | ({ 38 | src: image, 39 | alt: title, 40 | }))} 41 | /> 42 | 50 | {title && ( 51 | 52 | 53 | {title} 54 | 55 | 56 | )} 57 | {(avatars?.length > 0 || description?.trim() || content?.trim()) && ( 58 | 59 | {avatars?.length > 0 && } 60 | {description?.trim() && ( 61 | 62 | {description} 63 | 64 | )} 65 | 66 | {content?.trim() && ( 67 | 72 | Read case study 73 | 74 | )} 75 | {link && ( 76 | 81 | View project 82 | 83 | )} 84 | 85 | 86 | )} 87 | 88 | 89 | ); 90 | }; 91 | -------------------------------------------------------------------------------- /src/components/ScrollToHash.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import { useRouter } from "next/navigation"; 5 | 6 | export default function ScrollToHash() { 7 | const router = useRouter(); 8 | 9 | useEffect(() => { 10 | // Get the hash from the URL 11 | const hash = window.location.hash; 12 | if (hash) { 13 | // Remove the '#' symbol 14 | const id = hash.replace("#", ""); 15 | const element = document.getElementById(id); 16 | if (element) { 17 | element.scrollIntoView({ behavior: "smooth" }); 18 | } 19 | } 20 | }, [router]); 21 | 22 | return null; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.module.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | 6 | svg { 7 | width: 1em; 8 | height: 1em; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import React from 'react'; 4 | import { ToggleButton, useTheme } from '@/once-ui/components'; 5 | 6 | export const ThemeToggle: React.FC = () => { 7 | const { theme, setTheme } = useTheme(); 8 | 9 | return ( 10 | <> 11 | setTheme(theme === 'light' ? 'dark' : 'light')} 14 | selected={false} 15 | aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`} 16 | /> 17 | > 18 | ); 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/about/TableOfContents.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Column, Flex, Text } from "@/once-ui/components"; 5 | import styles from "./about.module.scss"; 6 | 7 | interface TableOfContentsProps { 8 | structure: { 9 | title: string; 10 | display: boolean; 11 | items: string[]; 12 | }[]; 13 | about: { 14 | tableOfContent: { 15 | display: boolean; 16 | subItems: boolean; 17 | }; 18 | }; 19 | } 20 | 21 | const TableOfContents: React.FC = ({ structure, about }) => { 22 | const scrollTo = (id: string, offset: number) => { 23 | const element = document.getElementById(id); 24 | if (element) { 25 | const elementPosition = element.getBoundingClientRect().top; 26 | const offsetPosition = elementPosition + window.scrollY - offset; 27 | 28 | window.scrollTo({ 29 | top: offsetPosition, 30 | behavior: "smooth", 31 | }); 32 | } 33 | }; 34 | 35 | if (!about.tableOfContent.display) return null; 36 | 37 | return ( 38 | 50 | {structure 51 | .filter((section) => section.display) 52 | .map((section, sectionIndex) => ( 53 | 54 | scrollTo(section.title, 80)} 60 | > 61 | 62 | {section.title} 63 | 64 | {about.tableOfContent.subItems && ( 65 | <> 66 | {section.items.map((item, itemIndex) => ( 67 | scrollTo(item, 80)} 76 | > 77 | 78 | {item} 79 | 80 | ))} 81 | > 82 | )} 83 | 84 | ))} 85 | 86 | ); 87 | }; 88 | 89 | export default TableOfContents; 90 | -------------------------------------------------------------------------------- /src/components/about/about.module.scss: -------------------------------------------------------------------------------- 1 | .hover { 2 | transition: var(--transition-micro-medium); 3 | 4 | &:hover { 5 | transform: translateX(var(--static-space-4)); 6 | } 7 | } 8 | 9 | .avatar { 10 | position: sticky; 11 | height: fit-content; 12 | top: var(--static-space-64); 13 | } 14 | 15 | @media (--s) { 16 | .avatar { 17 | top: auto; 18 | } 19 | 20 | .textAlign { 21 | text-align: center; 22 | } 23 | 24 | .blockAlign { 25 | align-self: center; 26 | } 27 | } -------------------------------------------------------------------------------- /src/components/blog/Post.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Column, Flex, Heading, SmartImage, SmartLink, Tag, Text } from '@/once-ui/components'; 4 | import styles from './Posts.module.scss'; 5 | import { formatDate } from '@/app/utils/formatDate'; 6 | 7 | interface PostProps { 8 | post: any; 9 | thumbnail: boolean; 10 | direction?: "row" | "column"; 11 | } 12 | 13 | export default function Post({ post, thumbnail, direction }: PostProps) { 14 | return ( 15 | 21 | 29 | {post.metadata.image && thumbnail && ( 30 | 41 | )} 42 | 47 | 51 | {post.metadata.title} 52 | 53 | 56 | {formatDate(post.metadata.publishedAt, false)} 57 | 58 | { post.metadata.tag && 59 | 63 | } 64 | 65 | 66 | 67 | ); 68 | } -------------------------------------------------------------------------------- /src/components/blog/Posts.module.scss: -------------------------------------------------------------------------------- 1 | .hover { 2 | border: 1px solid var(--static-transparent); 3 | 4 | &:hover { 5 | border: 1px solid var(--neutral-alpha-medium); 6 | } 7 | } 8 | 9 | .image { 10 | transition: var(--transition-micro-medium); 11 | transform: scale(1); 12 | } -------------------------------------------------------------------------------- /src/components/blog/Posts.tsx: -------------------------------------------------------------------------------- 1 | import { getPosts } from '@/app/utils/utils'; 2 | import { Grid } from '@/once-ui/components'; 3 | import Post from './Post'; 4 | 5 | interface PostsProps { 6 | range?: [number] | [number, number]; 7 | columns?: '1' | '2' | '3'; 8 | thumbnail?: boolean; 9 | direction?: 'row' | 'column'; 10 | } 11 | 12 | export function Posts({ 13 | range, 14 | columns = '1', 15 | thumbnail = false, 16 | direction 17 | }: PostsProps) { 18 | let allBlogs = getPosts(['src', 'app', 'blog', 'posts']); 19 | 20 | const sortedBlogs = allBlogs.sort((a, b) => { 21 | return new Date(b.metadata.publishedAt).getTime() - new Date(a.metadata.publishedAt).getTime(); 22 | }); 23 | 24 | const displayedBlogs = range 25 | ? sortedBlogs.slice( 26 | range[0] - 1, 27 | range.length === 2 ? range[1] : sortedBlogs.length 28 | ) 29 | : sortedBlogs; 30 | 31 | return ( 32 | <> 33 | {displayedBlogs.length > 0 && ( 34 | 37 | {displayedBlogs.map((post) => ( 38 | 44 | ))} 45 | 46 | )} 47 | > 48 | ); 49 | } -------------------------------------------------------------------------------- /src/components/gallery/Gallery.module.scss: -------------------------------------------------------------------------------- 1 | .masonryGrid { 2 | display: flex; 3 | margin-left: calc(-1 * var(--static-space-16)); 4 | width: 100%; 5 | } 6 | 7 | .masonryGridColumn { 8 | padding-left: var(--static-space-16); 9 | background-clip: padding-box; 10 | } 11 | 12 | .gridItem { 13 | margin-bottom: var(--static-space-16); 14 | } -------------------------------------------------------------------------------- /src/components/gallery/MasonryGrid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import Masonry from "react-masonry-css"; 4 | import { SmartImage } from "@/once-ui/components"; 5 | import styles from "./Gallery.module.scss"; 6 | import { gallery } from "@/app/resources/content"; 7 | 8 | export default function MasonryGrid() { 9 | const breakpointColumnsObj = { 10 | default: 2, 11 | 720: 1, 12 | }; 13 | 14 | return ( 15 | 20 | {gallery.images.map((image, index) => ( 21 | 31 | ))} 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { Header } from "@/components/Header"; 2 | export { Footer } from "@/components/Footer"; 3 | export { Mailchimp } from "@/components/Mailchimp"; 4 | export { ProjectCard } from "@/components/ProjectCard"; 5 | export { HeadingLink } from "@/components/HeadingLink"; 6 | export { RouteGuard } from "@/components/RouteGuard"; 7 | -------------------------------------------------------------------------------- /src/components/work/Projects.module.scss: -------------------------------------------------------------------------------- 1 | .hover { 2 | transition: var(--transition-micro-medium); 3 | 4 | &:hover { 5 | transform: translateX(var(--static-space-8)); 6 | 7 | .indicator { 8 | transform: rotate(0); 9 | } 10 | } 11 | } 12 | 13 | .indicator { 14 | transform: rotate(-90deg); 15 | left: -2rem; 16 | transition: var(--transition-micro-medium); 17 | } -------------------------------------------------------------------------------- /src/components/work/Projects.tsx: -------------------------------------------------------------------------------- 1 | import { getPosts } from "@/app/utils/utils"; 2 | import { Column } from "@/once-ui/components"; 3 | import { ProjectCard } from "@/components"; 4 | 5 | interface ProjectsProps { 6 | range?: [number, number?]; 7 | } 8 | 9 | export function Projects({ range }: ProjectsProps) { 10 | let allProjects = getPosts(["src", "app", "work", "projects"]); 11 | 12 | const sortedProjects = allProjects.sort((a, b) => { 13 | return new Date(b.metadata.publishedAt).getTime() - new Date(a.metadata.publishedAt).getTime(); 14 | }); 15 | 16 | const displayedProjects = range 17 | ? sortedProjects.slice(range[0] - 1, range[1] ?? sortedProjects.length) 18 | : sortedProjects; 19 | 20 | return ( 21 | 22 | {displayedProjects.map((post, index) => ( 23 | ({ src: member.avatar })) || []} 32 | link={post.metadata.link || ""} 33 | /> 34 | ))} 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/once-ui/components/Accordion.module.scss: -------------------------------------------------------------------------------- 1 | .accordion { 2 | &:hover { 3 | background: var(--neutral-alpha-weak); 4 | } 5 | } -------------------------------------------------------------------------------- /src/once-ui/components/AccordionGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Column, Accordion, Line, Flex } from "@/once-ui/components"; 3 | 4 | export type AccordionItem = { 5 | title: React.ReactNode; 6 | content: React.ReactNode; 7 | }; 8 | 9 | export interface AccordionGroupProps extends React.ComponentProps { 10 | items: AccordionItem[]; 11 | size?: "s" | "m" | "l"; 12 | } 13 | 14 | const AccordionGroup: React.FC = ({ items, size = "m", ...rest }) => { 15 | if (!items || items.length === 0) { 16 | return null; 17 | } 18 | 19 | return ( 20 | 21 | {items.map((item, index) => ( 22 | 23 | 24 | {item.content} 25 | 26 | {index < items.length - 1 && } 27 | 28 | ))} 29 | 30 | ); 31 | }; 32 | 33 | AccordionGroup.displayName = "AccordionGroup"; 34 | export { AccordionGroup }; 35 | -------------------------------------------------------------------------------- /src/once-ui/components/Arrow.module.scss: -------------------------------------------------------------------------------- 1 | .arrowContainer { 2 | transition: var(--transition-micro-medium); 3 | height: var(--static-space-16); 4 | width: var(--static-space-0); 5 | visibility: hidden; 6 | } 7 | 8 | .arrowHead { 9 | transition: var(--transition-micro-medium); 10 | width: var(--static-space-0); 11 | transition-delay: 0.2s; 12 | right: 0; 13 | transform-origin: right center; 14 | transform: rotate(0); 15 | } 16 | 17 | .active { 18 | width: var(--static-space-16); 19 | visibility: visible; 20 | 21 | .arrow { 22 | width: var(--static-space-12); 23 | } 24 | 25 | .arrowHead { 26 | width: var(--static-space-8); 27 | } 28 | 29 | .arrowHead:nth-child(2) { 30 | transform: rotate(45deg); 31 | } 32 | 33 | .arrowHead:nth-child(3) { 34 | transform: rotate(-45deg); 35 | } 36 | } 37 | 38 | .onSolid { 39 | background: var(--brand-on-solid-strong); 40 | } 41 | 42 | .onBackground { 43 | background: var(--brand-on-background-strong); 44 | } -------------------------------------------------------------------------------- /src/once-ui/components/Arrow.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useRef } from "react"; 4 | import classNames from "classnames"; 5 | import styles from "./Arrow.module.scss"; 6 | import { Flex } from "."; 7 | 8 | interface ArrowProps { 9 | trigger: string; 10 | scale?: number; 11 | color?: "onBackground" | "onSolid"; 12 | style?: React.CSSProperties; 13 | className?: string; 14 | } 15 | 16 | const Arrow: React.FC = ({ 17 | trigger, 18 | scale = 0.8, 19 | color = "onBackground", 20 | style, 21 | className, 22 | }) => { 23 | const ref = useRef(null); 24 | 25 | useEffect(() => { 26 | const triggerElement = document.querySelector(trigger); 27 | 28 | if (triggerElement && ref.current) { 29 | const handleMouseOver = () => { 30 | ref.current?.classList.add(styles.active); 31 | }; 32 | 33 | const handleMouseOut = () => { 34 | ref.current?.classList.remove(styles.active); 35 | }; 36 | 37 | triggerElement.addEventListener("mouseenter", handleMouseOver); 38 | triggerElement.addEventListener("mouseleave", handleMouseOut); 39 | 40 | return () => { 41 | triggerElement.removeEventListener("mouseenter", handleMouseOver); 42 | triggerElement.removeEventListener("mouseleave", handleMouseOut); 43 | }; 44 | } 45 | }, [trigger]); 46 | 47 | return ( 48 | 58 | 59 | 64 | 69 | 70 | ); 71 | }; 72 | 73 | Arrow.displayName = "Arrow"; 74 | export { Arrow }; 75 | -------------------------------------------------------------------------------- /src/once-ui/components/Avatar.module.scss: -------------------------------------------------------------------------------- 1 | .xs { 2 | width: var(--static-space-20); 3 | height: var(--static-space-20); 4 | min-width: var(--static-space-20); 5 | min-height: var(--static-space-20); 6 | } 7 | 8 | .s { 9 | width: var(--static-space-24); 10 | height: var(--static-space-24); 11 | min-width: var(--static-space-24); 12 | min-height: var(--static-space-24); 13 | } 14 | 15 | .m { 16 | width: var(--static-space-32); 17 | height: var(--static-space-32); 18 | min-width: var(--static-space-32); 19 | min-height: var(--static-space-32); 20 | } 21 | 22 | .l { 23 | width: var(--static-space-48); 24 | height: var(--static-space-48); 25 | min-width: var(--static-space-48); 26 | min-height: var(--static-space-48); 27 | } 28 | 29 | .xl { 30 | width: var(--static-space-160); 31 | height: var(--static-space-160); 32 | min-width: var(--static-space-160); 33 | min-height: var(--static-space-160); 34 | 35 | .position { 36 | bottom: var(--static-space-16); 37 | right: var(--static-space-16); 38 | } 39 | } 40 | 41 | .value { 42 | white-space: nowrap; 43 | overflow: hidden; 44 | user-select: none; 45 | } 46 | 47 | .indicator { 48 | box-sizing: content-box; 49 | bottom: 0; 50 | right: 0; 51 | transform: translateX(var(--static-space-2)) translateY(var(--static-space-2)); 52 | } 53 | 54 | .image { 55 | object-position: center; 56 | } -------------------------------------------------------------------------------- /src/once-ui/components/AvatarGroup.module.scss: -------------------------------------------------------------------------------- 1 | .avatarGroup { 2 | z-index: 0; 3 | } 4 | 5 | .avatar { 6 | margin-left: calc(-1 * var(--static-space-8)); 7 | 8 | &:first-child { 9 | margin-left: 0; 10 | } 11 | } -------------------------------------------------------------------------------- /src/once-ui/components/AvatarGroup.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef } from "react"; 4 | 5 | import { Avatar, AvatarProps, Flex } from "."; 6 | import styles from "./AvatarGroup.module.scss"; 7 | import classNames from "classnames"; 8 | 9 | interface AvatarGroupProps extends React.ComponentProps { 10 | avatars: AvatarProps[]; 11 | size?: "xs" | "s" | "m" | "l" | "xl"; 12 | reverse?: boolean; 13 | limit?: number; 14 | className?: string; 15 | style?: React.CSSProperties; 16 | } 17 | 18 | const AvatarGroup = forwardRef( 19 | ({ avatars, size = "m", reverse = false, limit, className, style, ...rest }, ref) => { 20 | const displayedAvatars = limit ? avatars.slice(0, limit) : avatars; 21 | const remainingCount = limit && avatars.length > limit ? avatars.length - limit : 0; 22 | 23 | return ( 24 | 32 | {displayedAvatars.map((avatarProps, index) => ( 33 | 43 | ))} 44 | {remainingCount > 0 && ( 45 | 54 | )} 55 | 56 | ); 57 | }, 58 | ); 59 | 60 | AvatarGroup.displayName = "AvatarGroup"; 61 | 62 | export { AvatarGroup }; 63 | export type { AvatarGroupProps }; 64 | -------------------------------------------------------------------------------- /src/once-ui/components/Background.module.scss: -------------------------------------------------------------------------------- 1 | .mask { 2 | mask-size: 100% 100%; 3 | mask-image: radial-gradient( 4 | var(--mask-radius) at var(--mask-position-x) var(--mask-position-y), 5 | black 0%, 6 | transparent 100% 7 | ); 8 | } 9 | 10 | .gradient { 11 | background: radial-gradient( 12 | ellipse var(--gradient-width) var(--gradient-height) at var(--gradient-position-x) var(--gradient-position-y), 13 | var(--gradient-color-start), 14 | var(--gradient-color-end) 15 | ); 16 | width: 400%; 17 | height: 400%; 18 | left: -150%; 19 | top: -150%; 20 | transform: rotate(var(--gradient-tilt)); 21 | transform-origin: center; 22 | } 23 | 24 | .lines { 25 | background-size: var(--lines-size) var(--lines-size); 26 | background-position: center; 27 | } 28 | 29 | .dots { 30 | background-image: radial-gradient(var(--dots-color) 1px, var(--static-transparent) 1px); 31 | background-size: var(--dots-size) var(--dots-size); 32 | } -------------------------------------------------------------------------------- /src/once-ui/components/Badge.module.scss: -------------------------------------------------------------------------------- 1 | .animation { 2 | position: relative; 3 | overflow: hidden; 4 | 5 | &::before { 6 | content: ''; 7 | opacity: 0; 8 | border-radius: var(--radius-full); 9 | position: absolute; 10 | width: 100%; 11 | height: 100%; 12 | background: linear-gradient( 13 | 120deg, 14 | transparent 20%, 15 | var(--brand-alpha-medium) 50%, 16 | transparent 80% 17 | ); 18 | transform: skewX(-20deg); 19 | animation: shineDefault 9s ease-in-out infinite; 20 | } 21 | 22 | &:hover { 23 | &::before { 24 | animation: shineHover 3s ease-in-out infinite; 25 | } 26 | } 27 | } 28 | 29 | @keyframes shineDefault { 30 | 0% { 31 | left: -100%; 32 | } 33 | 1% { 34 | left: -100%; 35 | opacity: 1; 36 | } 37 | 15% { 38 | left: 100%; 39 | opacity: 1; 40 | } 41 | 16% { 42 | opacity: 0; 43 | } 44 | 100% { 45 | left: -100%; 46 | } 47 | } 48 | 49 | @keyframes shineHover { 50 | 0% { 51 | left: -100%; 52 | } 53 | 1% { 54 | left: -100%; 55 | opacity: 1; 56 | } 57 | 45% { 58 | left: 100%; 59 | opacity: 1; 60 | } 61 | 46% { 62 | opacity: 0; 63 | } 64 | 100% { 65 | left: -100%; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/once-ui/components/Badge.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef } from "react"; 4 | import { Arrow, Flex, Icon, SmartLink, Text } from "."; 5 | 6 | import styles from "./Badge.module.scss"; 7 | import { IconName } from "../icons"; 8 | 9 | interface BadgeProps extends React.ComponentProps { 10 | title?: string; 11 | icon?: IconName; 12 | arrow?: boolean; 13 | children?: React.ReactNode; 14 | href?: string; 15 | effect?: boolean; 16 | } 17 | 18 | const Badge = forwardRef( 19 | ( 20 | { title, icon, arrow = true, children, href, effect = true, ...rest }, 21 | ref 22 | ) => { 23 | const content = ( 24 | 37 | {icon && ( 38 | 44 | )} 45 | {title && ( 46 | 47 | {title} 48 | 49 | )} 50 | {children} 51 | {arrow && } 52 | 53 | ); 54 | 55 | if (href) { 56 | return ( 57 | } 64 | > 65 | {content} 66 | 67 | ); 68 | } 69 | 70 | return React.cloneElement(content, { 71 | ref: ref as React.Ref, 72 | }); 73 | } 74 | ); 75 | 76 | Badge.displayName = "Badge"; 77 | export { Badge }; 78 | -------------------------------------------------------------------------------- /src/once-ui/components/Card.module.scss: -------------------------------------------------------------------------------- 1 | .card { 2 | &:hover { 3 | background-color: var(--neutral-alpha-medium); 4 | } 5 | } -------------------------------------------------------------------------------- /src/once-ui/components/Card.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef } from "react"; 4 | import { Flex } from "."; 5 | import styles from "./Card.module.scss"; 6 | import { ElementType } from "./ElementType"; 7 | import classNames from "classnames"; 8 | 9 | interface CardProps extends React.ComponentProps { 10 | children?: React.ReactNode; 11 | href?: string; 12 | onClick?: () => void; 13 | } 14 | 15 | const Card = forwardRef( 16 | ({ children, href, onClick, style, className, ...rest }, ref) => { 17 | return ( 18 | {}} 29 | role="button" 30 | ref={ref} 31 | > 32 | 43 | {children} 44 | 45 | 46 | ); 47 | }, 48 | ); 49 | 50 | Card.displayName = "Card"; 51 | export { Card }; 52 | -------------------------------------------------------------------------------- /src/once-ui/components/Chip.module.scss: -------------------------------------------------------------------------------- 1 | .chip { 2 | white-space: nowrap; 3 | user-select: none; 4 | 5 | &.selected { 6 | background: var(--brand-alpha-medium); 7 | color: var(--brand-on-background-medium); 8 | 9 | &:hover, 10 | &:focus { 11 | background: var(--brand-alpha-medium); 12 | } 13 | 14 | &:active { 15 | background: var(--brand-alpha-weak); 16 | color: var(--brand-on-background-weak); 17 | } 18 | } 19 | 20 | &.unselected { 21 | background: var(--neutral-alpha-weak); 22 | color: var(--neutral-on-background-medium); 23 | 24 | &:hover, 25 | &:focus { 26 | background: var(--neutral-alpha-medium); 27 | } 28 | 29 | &:active { 30 | background: var(--neutral-alpha-weak); 31 | color: var(--neutral-on-background-weak); 32 | } 33 | } 34 | 35 | &:disabled { 36 | background: var(--neutral-solid-weak); 37 | cursor: not-allowed; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/once-ui/components/Column.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { forwardRef } from "react"; 4 | import { Flex } from "."; 5 | 6 | interface ColumnProps extends React.ComponentProps { 7 | children?: React.ReactNode; 8 | } 9 | 10 | const Column = forwardRef(({ children, ...rest }, ref) => { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }); 17 | 18 | Column.displayName = "Column"; 19 | export { Column }; 20 | -------------------------------------------------------------------------------- /src/once-ui/components/CompareImage.module.scss: -------------------------------------------------------------------------------- 1 | .hitArea { 2 | cursor: col-resize; 3 | transform: translateX(-50%); 4 | } 5 | 6 | .dragIcon { 7 | position: absolute; 8 | top: 50%; 9 | transform: translate(-50%, -50%); 10 | cursor: col-resize; 11 | z-index: 2; 12 | transition: none; 13 | pointer-events: auto; 14 | } -------------------------------------------------------------------------------- /src/once-ui/components/DateInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useCallback, useEffect } from "react"; 4 | import { Input, DropdownWrapper, Flex, DatePicker } from "."; 5 | 6 | interface DateInputProps extends Omit, "onChange" | "value"> { 7 | id: string; 8 | label: string; 9 | value?: Date; 10 | onChange?: (date: Date) => void; 11 | minHeight?: number; 12 | className?: string; 13 | style?: React.CSSProperties; 14 | timePicker?: boolean; 15 | } 16 | 17 | const formatDate = (date: Date, timePicker: boolean) => { 18 | const options: Intl.DateTimeFormatOptions = { 19 | year: "numeric", 20 | month: "short", 21 | day: "numeric", 22 | ...(timePicker && { 23 | hour: "2-digit", 24 | minute: "2-digit", 25 | hour12: false, 26 | }), 27 | }; 28 | 29 | return date.toLocaleString("en-US", options); 30 | }; 31 | 32 | export const DateInput: React.FC = ({ 33 | id, 34 | label, 35 | value, 36 | onChange, 37 | error, 38 | minHeight, 39 | className, 40 | style, 41 | timePicker = false, 42 | ...rest 43 | }) => { 44 | const [isOpen, setIsOpen] = useState(false); 45 | const [inputValue, setInputValue] = useState(value ? formatDate(value, timePicker) : ""); 46 | 47 | useEffect(() => { 48 | if (value) { 49 | setInputValue(formatDate(value, timePicker)); 50 | } 51 | }, [value, timePicker]); 52 | 53 | const handleDateChange = useCallback( 54 | (date: Date) => { 55 | setInputValue(formatDate(date, timePicker)); 56 | onChange?.(date); 57 | if (!timePicker) { 58 | setIsOpen(false); 59 | } 60 | }, 61 | [onChange, timePicker], 62 | ); 63 | 64 | const handleInputClick = useCallback(() => { 65 | setIsOpen(true); 66 | }, []); 67 | 68 | const trigger = ( 69 | 82 | ); 83 | 84 | const dropdown = ( 85 | 86 | 87 | 88 | ); 89 | 90 | return ( 91 | 101 | ); 102 | }; 103 | -------------------------------------------------------------------------------- /src/once-ui/components/Dialog.module.scss: -------------------------------------------------------------------------------- 1 | .overlay { 2 | opacity: 0; 3 | visibility: hidden; 4 | 5 | &.open { 6 | opacity: 1; 7 | visibility: visible; 8 | } 9 | } 10 | 11 | .dialog { 12 | max-width: 40rem; 13 | max-height: 100%; 14 | transform: scale(0.2); 15 | opacity: 0; 16 | 17 | &.open { 18 | transform: scale(1); 19 | opacity: 1; 20 | visibility: visible; 21 | } 22 | 23 | @media (--m) { 24 | position: fixed; 25 | bottom: var(--static-space-8); 26 | left: var(--static-space-8); 27 | right: var(--static-space-8); 28 | max-width: calc(100% - var(--static-space-16)) !important; 29 | } 30 | } -------------------------------------------------------------------------------- /src/once-ui/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { ReactNode, forwardRef, SyntheticEvent } from "react"; 4 | import { Flex } from "."; 5 | 6 | interface DropdownProps extends Omit, "onSelect"> { 7 | selectedOption?: string; 8 | children?: ReactNode; 9 | onEscape?: () => void; 10 | onSelect?: (event: string) => void; 11 | } 12 | 13 | const Dropdown = forwardRef( 14 | ({ selectedOption, className, children, onEscape, onSelect, ...rest }, ref) => { 15 | const handleSelect = (event: SyntheticEvent) => { 16 | const value = event.currentTarget.getAttribute("data-value"); 17 | if (onSelect && value) { 18 | onSelect(value); 19 | } 20 | }; 21 | 22 | return ( 23 | 33 | 34 | {children} 35 | 36 | 37 | ); 38 | }, 39 | ); 40 | 41 | Dropdown.displayName = "Dropdown"; 42 | 43 | export { Dropdown }; 44 | export type { DropdownProps }; 45 | -------------------------------------------------------------------------------- /src/once-ui/components/DropdownWrapper.module.scss: -------------------------------------------------------------------------------- 1 | @keyframes fadeIn { 2 | from { 3 | opacity: 0; 4 | transform: scale(0.9); 5 | } 6 | to { 7 | opacity: 1; 8 | transform: scale(1); 9 | } 10 | } 11 | 12 | .fadeIn { 13 | transform-origin: top right; 14 | animation: fadeIn var(--transition-duration-micro-medium) var(--transition-eased); 15 | } -------------------------------------------------------------------------------- /src/once-ui/components/ElementType.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React, { ReactNode, forwardRef } from "react"; 3 | import { Flex } from "./Flex"; 4 | 5 | interface ElementTypeProps { 6 | href?: string; 7 | onClick?: () => void; 8 | onLinkClick?: () => void; 9 | children: ReactNode; 10 | className?: string; 11 | style?: React.CSSProperties; 12 | [key: string]: any; 13 | } 14 | 15 | const isExternalLink = (url: string) => /^https?:\/\//.test(url); 16 | 17 | const ElementType = forwardRef( 18 | ({ href, type, onClick, onLinkClick, children, className, style, ...props }, ref) => { 19 | if (href) { 20 | const isExternal = isExternalLink(href); 21 | if (isExternal) { 22 | return ( 23 | } 28 | className={className} 29 | style={style} 30 | onClick={() => onLinkClick?.()} 31 | {...(props as React.AnchorHTMLAttributes)} 32 | > 33 | {children} 34 | 35 | ); 36 | } 37 | return ( 38 | } 41 | className={className} 42 | style={style} 43 | onClick={() => onLinkClick?.()} 44 | {...(props as React.AnchorHTMLAttributes)} 45 | > 46 | {children} 47 | 48 | ); 49 | } 50 | 51 | if (onClick || type === "submit" || type === "button") { 52 | return ( 53 | } 55 | className={className} 56 | onClick={onClick} 57 | style={style} 58 | {...(props as React.ButtonHTMLAttributes)} 59 | > 60 | {children} 61 | 62 | ); 63 | } 64 | 65 | return ( 66 | } 68 | className={className} 69 | style={style} 70 | {...(props as React.HTMLAttributes)} 71 | > 72 | {children} 73 | 74 | ); 75 | }, 76 | ); 77 | 78 | ElementType.displayName = "ElementType"; 79 | export { ElementType }; 80 | -------------------------------------------------------------------------------- /src/once-ui/components/Fade.module.scss: -------------------------------------------------------------------------------- 1 | .mask { 2 | backdrop-filter: blur(0.5rem); 3 | background: linear-gradient(var(--gradient-direction), var(--base-color), transparent); 4 | mask-image: linear-gradient(var(--gradient-direction), black 20%, transparent 100%); 5 | mask-size: 100% 100%; 6 | } -------------------------------------------------------------------------------- /src/once-ui/components/Fade.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef, ReactNode } from "react"; 4 | import styles from "./Fade.module.scss"; 5 | 6 | import { Flex } from "."; 7 | import { ColorScheme, ColorWeight, SpacingToken } from "../types"; 8 | 9 | type BaseColor = 10 | | `${ColorScheme}-${ColorWeight}` 11 | | `${ColorScheme}-alpha-${ColorWeight}` 12 | | "surface" 13 | | "overlay" 14 | | "page"; 15 | 16 | interface FadeProps extends React.ComponentProps { 17 | className?: string; 18 | to?: "bottom" | "top" | "left" | "right"; 19 | base?: BaseColor; 20 | blur?: number; 21 | pattern?: { 22 | display?: boolean; 23 | size?: SpacingToken; 24 | }; 25 | style?: React.CSSProperties; 26 | children?: ReactNode; 27 | } 28 | 29 | const Fade = forwardRef( 30 | ( 31 | { 32 | to = "bottom", 33 | base = "page", 34 | pattern = { 35 | display: false, 36 | size: "4", 37 | }, 38 | blur = 0.5, 39 | children, 40 | ...rest 41 | }, 42 | ref, 43 | ) => { 44 | const getBaseVar = (base: BaseColor) => { 45 | if (base === "page") return "var(--page-background)"; 46 | if (base === "surface") return "var(--surface-background)"; 47 | if (base === "overlay") return "var(--backdrop)"; 48 | 49 | const [scheme, weight] = base.includes("alpha") ? base.split("-alpha-") : base.split("-"); 50 | 51 | return base.includes("alpha") 52 | ? `var(--${scheme}-alpha-${weight})` 53 | : `var(--${scheme}-background-${weight})`; 54 | }; 55 | 56 | return ( 57 | 81 | {children} 82 | 83 | ); 84 | }, 85 | ); 86 | 87 | Fade.displayName = "Fade"; 88 | export { Fade }; 89 | -------------------------------------------------------------------------------- /src/once-ui/components/GlitchFx.module.scss: -------------------------------------------------------------------------------- 1 | .glitchLayer { 2 | pointer-events: none; 3 | } 4 | 5 | .blueShift { 6 | filter: hue-rotate(260deg); 7 | animation: glitch-blue 2.5s infinite; 8 | z-index: 1; 9 | } 10 | 11 | .redShift { 12 | filter: hue-rotate(120deg); 13 | animation: glitch-red 2.5s infinite; 14 | z-index: 1; 15 | } 16 | 17 | @keyframes glitch-blue { 18 | 6%, 14%, 70%, 78% { 19 | transform: none; 20 | opacity: 0.25; 21 | clip-path: inset(0 0 0 0); 22 | } 23 | 10%, 12% { 24 | transform: translate(-5px, -3px) skew(1deg, -2deg); 25 | opacity: 0.5; 26 | clip-path: inset(50% 0 25% 0); 27 | } 28 | 11%, 13% { 29 | transform: translate(4px, -7px) skew(-1deg) scaleX(1.5) scaleY(1.25); 30 | opacity: 0.7; 31 | clip-path: inset(0 35% 40% 0); 32 | } 33 | 14%, 82% { 34 | transform: translate(-5px, -3px) skew(1deg, -1deg); 35 | opacity: 0.3; 36 | clip-path: inset(30% 5% 25% 40%); 37 | } 38 | 75%, 83% { 39 | transform: translate(-9px, 2px) skew(-1deg, 0); 40 | opacity: 0.2; 41 | clip-path: inset(0 35% 45% 10%); 42 | } 43 | } 44 | 45 | @keyframes glitch-red { 46 | 6%, 14%, 70%, 78% { 47 | transform: none; 48 | opacity: 0; 49 | clip-path: inset(0 0 0 0); 50 | } 51 | 10%, 12% { 52 | transform: translate(6px, 4px) skew(1deg); 53 | opacity: 0.5; 54 | clip-path: inset(5% 0 10% 0); 55 | } 56 | 11%, 13% { 57 | transform: translate(-4px, 5px) skew(0, -1deg); 58 | opacity: 0.7; 59 | clip-path: inset(5% 0 10% 0); 60 | } 61 | 14%, 82% { 62 | transform: translate(-7px, -4px) skew(1deg, -1deg); 63 | opacity: 0.2; 64 | clip-path: inset(50% 25% 25% 0); 65 | } 66 | 75%, 80% { 67 | transform: translate(4px, -6px) skew(-1deg) scaleX(2) scaleY(1.25); 68 | opacity: 0.3; 69 | clip-path: inset(0 0 20% 50%); 70 | } 71 | } 72 | 73 | .slow { 74 | animation-duration: 3.5s; 75 | } 76 | 77 | .medium { 78 | animation-duration: 2.5s; 79 | } 80 | 81 | .fast { 82 | animation-duration: 1.5s; 83 | } 84 | 85 | .active .blueShift, .active .redShift { 86 | animation-play-state: running; 87 | } 88 | 89 | .glitchFx:not(.active) .blueShift, 90 | .glitchFx:not(.active) .redShift { 91 | animation-play-state: paused; 92 | } -------------------------------------------------------------------------------- /src/once-ui/components/HeadingLink.module.scss: -------------------------------------------------------------------------------- 1 | .control { 2 | cursor: pointer; 3 | 4 | &:hover { 5 | .visibility { 6 | opacity: 1; 7 | } 8 | 9 | .text { 10 | text-decoration-line: underline; 11 | } 12 | } 13 | } 14 | 15 | .text { 16 | text-decoration-thickness: 1px; 17 | text-underline-offset: 0.25em; 18 | text-decoration-color: var(--neutral-border-strong); 19 | } 20 | 21 | .visibility { 22 | opacity: 0; 23 | transform: scale(0.875); 24 | } -------------------------------------------------------------------------------- /src/once-ui/components/HeadingLink.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { Heading, Flex, IconButton, useToast } from "@/once-ui/components"; 5 | 6 | import styles from "./HeadingLink.module.scss"; 7 | 8 | interface HeadingLinkProps { 9 | id: string; 10 | as: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; 11 | children: React.ReactNode; 12 | style?: React.CSSProperties; 13 | } 14 | 15 | export const HeadingLink: React.FC = ({ id, as, children, style }) => { 16 | const { addToast } = useToast(); 17 | 18 | const copyURL = (id: string): void => { 19 | const url = `${window.location.origin}${window.location.pathname}#${id}`; 20 | navigator.clipboard.writeText(url).then( 21 | () => { 22 | addToast({ 23 | variant: "success", 24 | message: "Link copied to clipboard.", 25 | }); 26 | }, 27 | () => { 28 | addToast({ 29 | variant: "danger", 30 | message: "Failed to copy link.", 31 | }); 32 | }, 33 | ); 34 | }; 35 | 36 | const variantMap = { 37 | h1: "display-strong-xs", 38 | h2: "heading-strong-xl", 39 | h3: "heading-strong-l", 40 | h4: "heading-strong-m", 41 | h5: "heading-strong-s", 42 | h6: "heading-strong-xs", 43 | } as const; 44 | 45 | const variant = variantMap[as]; 46 | 47 | return ( 48 | copyURL(id)} 51 | className={styles.control} 52 | vertical="center" 53 | gap="8" 54 | > 55 | 56 | {children} 57 | 58 | 66 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /src/once-ui/components/HoloFx.module.scss: -------------------------------------------------------------------------------- 1 | .overlay { 2 | opacity: 0; 3 | transition: opacity 0.3s ease-in-out; 4 | pointer-events: none; 5 | } 6 | 7 | .holoFx { 8 | isolation: isolate; 9 | z-index: 0; 10 | 11 | &:hover { 12 | .burn { 13 | transform: translateX(1px) translateY(1px); 14 | opacity: var(--burn-opacity); 15 | z-index: 1; 16 | } 17 | 18 | .shine { 19 | transform: translateX(-1px) translateY(-1px); 20 | opacity: var(--light-opacity); 21 | z-index: 2; 22 | } 23 | 24 | .texture { 25 | opacity: var(--texture-opacity); 26 | transform: translateX(calc(var(--gradient-pos-x) / 50)) scale(1.1); 27 | background-size: 150% 150%; 28 | background-position: center; 29 | z-index: 3; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/once-ui/components/Icon.module.scss: -------------------------------------------------------------------------------- 1 | .xs { 2 | font-size: var(--static-space-16) 3 | } 4 | 5 | .s { 6 | font-size: var(--static-space-20) 7 | } 8 | 9 | .m { 10 | font-size: var(--static-space-24) 11 | } 12 | 13 | .l { 14 | font-size: var(--static-space-32) 15 | } 16 | 17 | .xl { 18 | font-size: var(--static-space-40) 19 | } -------------------------------------------------------------------------------- /src/once-ui/components/IconButton.module.scss: -------------------------------------------------------------------------------- 1 | .top { 2 | bottom: calc(100% + var(--static-space-2)); 3 | left: 50%; 4 | transform: translateX(-50%); 5 | } 6 | 7 | .bottom { 8 | top: calc(100% + var(--static-space-2)); 9 | left: 50%; 10 | transform: translateX(-50%); 11 | } 12 | 13 | .left { 14 | right: calc(100% + var(--static-space-2)); 15 | top: 50%; 16 | transform: translateY(-50%); 17 | } 18 | 19 | .right { 20 | left: calc(100% + var(--static-space-2)); 21 | top: 50%; 22 | transform: translateY(-50%); 23 | } 24 | 25 | .s { 26 | min-height: var(--static-space-24); 27 | min-width: var(--static-space-24); 28 | height: var(--static-space-24); 29 | width: var(--static-space-24); 30 | } 31 | 32 | .m { 33 | min-height: var(--static-space-32); 34 | min-width: var(--static-space-32); 35 | height: var(--static-space-32); 36 | width: var(--static-space-32); 37 | } 38 | 39 | .l { 40 | min-height: var(--static-space-40); 41 | min-width: var(--static-space-40); 42 | height: var(--static-space-40); 43 | width: var(--static-space-40); 44 | } -------------------------------------------------------------------------------- /src/once-ui/components/InlineCode.module.scss: -------------------------------------------------------------------------------- 1 | .inlineCode { 2 | font-size: 80%; 3 | line-height: 125%; 4 | vertical-align: middle; 5 | } -------------------------------------------------------------------------------- /src/once-ui/components/InlineCode.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef, ReactNode } from "react"; 4 | import styles from "./InlineCode.module.scss"; 5 | import { Flex } from "./Flex"; 6 | 7 | interface InlineCodeProps extends React.ComponentProps { 8 | children: ReactNode; 9 | } 10 | 11 | const InlineCode = forwardRef(({ children, ...rest }, ref) => { 12 | return ( 13 | 28 | {children} 29 | 30 | ); 31 | }); 32 | 33 | InlineCode.displayName = "InlineCode"; 34 | 35 | export { InlineCode }; 36 | -------------------------------------------------------------------------------- /src/once-ui/components/InteractiveDetails.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef } from "react"; 4 | import { Text, Flex, IconButton, IconButtonProps } from "."; 5 | 6 | interface InteractiveDetailsProps { 7 | label?: React.ReactNode; 8 | description?: React.ReactNode; 9 | iconButtonProps?: IconButtonProps; 10 | onClick: () => void; 11 | className?: string; 12 | id?: string; 13 | } 14 | 15 | const InteractiveDetails: React.FC = forwardRef< 16 | HTMLDivElement, 17 | InteractiveDetailsProps 18 | >(({ label, description, iconButtonProps, onClick, className, id }, ref) => { 19 | return ( 20 | 21 | 22 | 23 | {label} 24 | 25 | {iconButtonProps?.tooltip && ( 26 | e.stopPropagation()}> 27 | 28 | 29 | )} 30 | 31 | {description && ( 32 | 33 | {description} 34 | 35 | )} 36 | 37 | ); 38 | }); 39 | 40 | InteractiveDetails.displayName = "InteractiveDetails"; 41 | 42 | export { InteractiveDetails }; 43 | export type { InteractiveDetailsProps }; 44 | -------------------------------------------------------------------------------- /src/once-ui/components/Kbar.module.scss: -------------------------------------------------------------------------------- 1 | .overlay { 2 | animation: fadeIn 0.2s ease-out forwards; 3 | backdrop-filter: var(--backdrop-filter); 4 | 5 | &.closing { 6 | animation: fadeOut 0.2s ease-out forwards; 7 | } 8 | } 9 | 10 | .content { 11 | animation: scaleIn 0.2s ease-out forwards; 12 | 13 | &.closing { 14 | animation: scaleOut 0.2s ease-out forwards; 15 | } 16 | } 17 | 18 | @keyframes fadeIn { 19 | from { 20 | opacity: 0; 21 | backdrop-filter: blur(0); 22 | } 23 | to { 24 | opacity: 1; 25 | backdrop-filter: var(--backdrop-filter); 26 | } 27 | } 28 | 29 | @keyframes fadeOut { 30 | from { 31 | opacity: 1; 32 | backdrop-filter: var(--backdrop-filter); 33 | } 34 | to { 35 | opacity: 0; 36 | backdrop-filter: blur(0); 37 | } 38 | } 39 | 40 | @keyframes scaleIn { 41 | from { 42 | opacity: 0; 43 | transform: scale(0.9); 44 | } 45 | to { 46 | opacity: 1; 47 | transform: scale(1); 48 | } 49 | } 50 | 51 | @keyframes scaleOut { 52 | from { 53 | opacity: 1; 54 | transform: scale(1); 55 | } 56 | to { 57 | opacity: 0; 58 | transform: scale(0.9); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/once-ui/components/Kbd.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { ReactNode, forwardRef } from "react"; 4 | 5 | import { Flex, Text } from "."; 6 | 7 | interface KbdProps extends React.ComponentProps { 8 | label?: string; 9 | children?: ReactNode; 10 | className?: string; 11 | style?: React.CSSProperties; 12 | } 13 | 14 | const Kbd = forwardRef( 15 | ({ label, children, className, style, ...rest }, ref) => ( 16 | 31 | 32 | {label || children} 33 | 34 | 35 | ), 36 | ); 37 | 38 | Kbd.displayName = "Kbd"; 39 | 40 | export { Kbd }; 41 | export type { KbdProps }; 42 | -------------------------------------------------------------------------------- /src/once-ui/components/Line.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef } from "react"; 4 | import { Flex } from "."; 5 | 6 | interface LineProps extends React.ComponentProps { 7 | vert?: boolean; 8 | style?: React.CSSProperties; 9 | } 10 | 11 | const Line = forwardRef(({ vert, className, style, ...rest }, ref) => { 12 | return ( 13 | 27 | ); 28 | }); 29 | 30 | Line.displayName = "Line"; 31 | export { Line }; 32 | -------------------------------------------------------------------------------- /src/once-ui/components/Logo.module.scss: -------------------------------------------------------------------------------- 1 | .type { 2 | content: var(--type); 3 | } 4 | 5 | .icon { 6 | content: var(--icon); 7 | } 8 | 9 | .type, .icon { 10 | user-select: none; 11 | display: block; 12 | } -------------------------------------------------------------------------------- /src/once-ui/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useEffect } from "react"; 4 | import Link from "next/link"; 5 | import classNames from "classnames"; 6 | import styles from "./Logo.module.scss"; 7 | import { SpacingToken } from "../types"; 8 | import { Flex } from "."; 9 | 10 | const sizeMap: Record = { 11 | xs: "20", 12 | s: "24", 13 | m: "32", 14 | l: "40", 15 | xl: "48", 16 | }; 17 | 18 | interface LogoProps extends React.AnchorHTMLAttributes { 19 | className?: string; 20 | size?: "xs" | "s" | "m" | "l" | "xl"; 21 | style?: React.CSSProperties; 22 | wordmark?: boolean; 23 | icon?: boolean; 24 | iconSrc?: string; 25 | wordmarkSrc?: string; 26 | href?: string; 27 | } 28 | 29 | const Logo: React.FC = ({ 30 | size = "m", 31 | wordmark = true, 32 | icon = true, 33 | href, 34 | iconSrc, 35 | wordmarkSrc, 36 | className, 37 | style, 38 | ...props 39 | }) => { 40 | useEffect(() => { 41 | if (!icon && !wordmark) { 42 | console.warn( 43 | "Both 'icon' and 'wordmark' props are set to false. The logo will not render any content.", 44 | ); 45 | } 46 | }, [icon, wordmark]); 47 | 48 | const content = ( 49 | <> 50 | {icon && !iconSrc && ( 51 | 57 | )} 58 | {iconSrc && ( 59 | // @ts-ignore 60 | 68 | )} 69 | {wordmark && !wordmarkSrc && ( 70 | 76 | )} 77 | {wordmarkSrc && ( 78 | // @ts-ignore 79 | 87 | )} 88 | > 89 | ); 90 | 91 | return href ? ( 92 | 99 | {content} 100 | 101 | ) : ( 102 | 109 | {content} 110 | 111 | ); 112 | }; 113 | 114 | Logo.displayName = "Logo"; 115 | export { Logo }; 116 | -------------------------------------------------------------------------------- /src/once-ui/components/LogoCloud.module.scss: -------------------------------------------------------------------------------- 1 | .logo { 2 | animation: fadeInOut 5s ease-out both; 3 | will-change: opacity, filter, transform; 4 | transform-origin: center; 5 | } 6 | 7 | @keyframes fadeInOut { 8 | 0% { 9 | opacity: 0; 10 | filter: blur(1.5rem); 11 | transform: scale(0.2); 12 | } 13 | 4%, 96% { 14 | opacity: 1; 15 | filter: blur(0); 16 | transform: scale(1); 17 | } 18 | 100% { 19 | opacity: 0; 20 | filter: blur(1.5rem); 21 | transform: scale(0.2); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/once-ui/components/LogoCloud.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef, useState, useEffect } from "react"; 4 | import classNames from "classnames"; 5 | import { Grid } from "./Grid"; 6 | import { Logo } from "./Logo"; 7 | import styles from "./LogoCloud.module.scss"; 8 | import type { ComponentProps } from "react"; 9 | import { Flex } from "./Flex"; 10 | 11 | type LogoProps = ComponentProps; 12 | 13 | interface LogoCloudProps extends React.ComponentProps { 14 | logos: LogoProps[]; 15 | className?: string; 16 | style?: React.CSSProperties; 17 | limit?: number; 18 | rotationInterval?: number; 19 | } 20 | 21 | const ANIMATION_DURATION = 5000; 22 | const STAGGER_DELAY = 25; 23 | 24 | const LogoCloud = forwardRef( 25 | ({ logos, className, style, limit = 6, rotationInterval = ANIMATION_DURATION, ...rest }, ref) => { 26 | const [visibleLogos, setVisibleLogos] = useState(() => logos.slice(0, limit)); 27 | const [key, setKey] = useState(0); 28 | 29 | useEffect(() => { 30 | if (logos.length <= limit) { 31 | setVisibleLogos(logos); 32 | return; 33 | } 34 | 35 | const interval = setInterval( 36 | () => { 37 | setVisibleLogos((currentLogos) => { 38 | const currentIndices = currentLogos.map((logo) => logos.findIndex((l) => l === logo)); 39 | 40 | const nextIndices = currentIndices 41 | .map((index) => (index + 1) % logos.length) 42 | .sort((a, b) => a - b); 43 | 44 | const nextLogos = nextIndices.map((index) => logos[index]); 45 | setKey((k) => k + 1); 46 | return nextLogos; 47 | }); 48 | }, 49 | rotationInterval + STAGGER_DELAY * limit, 50 | ); 51 | 52 | return () => clearInterval(interval); 53 | }, [logos, limit, rotationInterval]); 54 | 55 | return ( 56 | 57 | {visibleLogos.map((logo, index) => ( 58 | 66 | 74 | 75 | ))} 76 | 77 | ); 78 | }, 79 | ); 80 | 81 | LogoCloud.displayName = "LogoCloud"; 82 | export { LogoCloud }; 83 | -------------------------------------------------------------------------------- /src/once-ui/components/MegaMenu.module.scss: -------------------------------------------------------------------------------- 1 | @keyframes fadeInDropdown { 2 | from { 3 | opacity: 0; 4 | transform: translateY(-1rem) scale(0.95); 5 | } 6 | to { 7 | opacity: 1; 8 | transform: translateY(0) scale(1); 9 | } 10 | } 11 | 12 | .dropdown { 13 | animation: fadeInDropdown 0.2s ease-out forwards; 14 | } 15 | -------------------------------------------------------------------------------- /src/once-ui/components/NavIcon.module.scss: -------------------------------------------------------------------------------- 1 | .line { 2 | background-color: var(--neutral-on-background-strong); 3 | height: 1px; 4 | width: var(--static-space-24); 5 | transition: transform 0.3s ease; 6 | position: absolute; 7 | left: 50%; 8 | top: 50%; 9 | 10 | &:first-child { 11 | transform: translateX(-50%) translateY(calc(-1 * var(--static-space-4))); 12 | } 13 | 14 | &:last-child { 15 | transform: translateX(-50%) translateY(var(--static-space-4)); 16 | } 17 | } 18 | 19 | .active:first-child { 20 | transform: translateX(-50%) translateY(0) rotate(45deg); 21 | } 22 | 23 | .active:last-child { 24 | transform: translateX(-50%) translateY(0) rotate(-45deg); 25 | } -------------------------------------------------------------------------------- /src/once-ui/components/NavIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from "react"; 2 | import styles from "./NavIcon.module.scss"; 3 | import { Flex } from "."; 4 | import classNames from "classnames"; 5 | 6 | interface NavIconProps extends React.ComponentProps { 7 | className?: string; 8 | style?: React.CSSProperties; 9 | onClick?: () => void; 10 | isActive: boolean; 11 | } 12 | 13 | const NavIcon = forwardRef>( 14 | ({ className, isActive, style, onClick, ...rest }, ref) => { 15 | return ( 16 | 30 | 31 | 32 | 33 | ); 34 | }, 35 | ); 36 | 37 | NavIcon.displayName = "NavIcon"; 38 | 39 | export { NavIcon }; 40 | -------------------------------------------------------------------------------- /src/once-ui/components/NumberInput.module.scss: -------------------------------------------------------------------------------- 1 | .numberInput { 2 | input[type="number"]::-webkit-inner-spin-button, 3 | input[type="number"]::-webkit-outer-spin-button { 4 | -webkit-appearance: none; 5 | margin: 0; 6 | } 7 | 8 | input[type="number"] { 9 | -moz-appearance: textfield; 10 | } 11 | } 12 | 13 | .stepper { 14 | pointer-events: visibleFill; 15 | &:hover { 16 | background-color: var(--neutral-alpha-medium); 17 | } 18 | } -------------------------------------------------------------------------------- /src/once-ui/components/OTPInput.module.scss: -------------------------------------------------------------------------------- 1 | .inputs { 2 | font-size: var(--font-size-heading-xl); 3 | transition: border-color 0.2s, box-shadow 0.2s; 4 | width: var(--static-space-48); 5 | max-width: var(--static-space-48); 6 | 7 | input { 8 | text-align: center; 9 | } 10 | 11 | &:focus-within { 12 | animation: focusAnimation 0.3s forwards; 13 | } 14 | } 15 | 16 | @keyframes focusAnimation { 17 | 0% { 18 | transform: scale(1); 19 | } 20 | 50% { 21 | transform: scale(1.05); 22 | } 23 | 100% { 24 | transform: scale(1); 25 | } 26 | } -------------------------------------------------------------------------------- /src/once-ui/components/Option.module.scss: -------------------------------------------------------------------------------- 1 | .option { 2 | border-color: var(--static-transparent); 3 | 4 | &:hover, &:focus { 5 | background: var(--neutral-alpha-weak); 6 | border-color: var(--neutral-alpha-medium); 7 | } 8 | 9 | &.selected { 10 | background: var(--neutral-alpha-medium); 11 | border-color: var(--neutral-alpha-medium); 12 | } 13 | 14 | &.highlighted { 15 | background: var(--static-transparent); 16 | border-color: var(--neutral-alpha-medium); 17 | } 18 | 19 | &.danger { 20 | color: var(--danger-on-background-medium); 21 | 22 | &:hover, &:focus { 23 | background: var(--danger-solid-strong); 24 | color: var(--danger-on-solid-strong); 25 | border-color: var(--danger-border-strong); 26 | } 27 | } 28 | } 29 | 30 | .focused { 31 | background: var(--neutral-background-strong); 32 | } -------------------------------------------------------------------------------- /src/once-ui/components/Option.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import { Flex, Text } from "."; 3 | import styles from "./Option.module.scss"; 4 | import { ElementType } from "./ElementType"; 5 | import React, { forwardRef } from "react"; 6 | 7 | export interface OptionProps { 8 | label: React.ReactNode; 9 | href?: string; 10 | value: string; 11 | hasPrefix?: React.ReactNode; 12 | hasSuffix?: React.ReactNode; 13 | description?: React.ReactNode; 14 | danger?: boolean; 15 | selected?: boolean; 16 | highlighted?: boolean; 17 | tabIndex?: number; 18 | onClick?: (value: string) => void; 19 | onLinkClick?: () => void; 20 | } 21 | 22 | const Option = forwardRef( 23 | ( 24 | { 25 | label, 26 | value, 27 | href, 28 | hasPrefix, 29 | hasSuffix, 30 | description, 31 | danger, 32 | selected, 33 | highlighted, 34 | tabIndex, 35 | onClick, 36 | onLinkClick, 37 | ...props 38 | }, 39 | ref, 40 | ) => { 41 | if (href && onClick) { 42 | console.warn("Option should not have both `href` and `onClick` props."); 43 | } 44 | 45 | return ( 46 | 53 | onClick?.(value)} 69 | className={classNames(styles.option, { 70 | [styles.danger]: danger, 71 | [styles.selected]: selected, 72 | [styles.highlighted]: highlighted, 73 | })} 74 | data-value={value} 75 | > 76 | {hasPrefix && {hasPrefix}} 77 | 85 | 86 | {label} 87 | 88 | {description && ( 89 | 90 | {description} 91 | 92 | )} 93 | 94 | {hasSuffix && {hasSuffix}} 95 | 96 | 97 | ); 98 | }, 99 | ); 100 | 101 | Option.displayName = "Option"; 102 | export { Option }; 103 | -------------------------------------------------------------------------------- /src/once-ui/components/PasswordInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, forwardRef } from "react"; 4 | import { Input, InputProps, IconButton } from "."; 5 | 6 | export const PasswordInput = forwardRef((props, ref) => { 7 | const [showPassword, setShowPassword] = useState(false); 8 | 9 | return ( 10 | { 17 | setShowPassword(!showPassword); 18 | }} 19 | variant="ghost" 20 | icon={showPassword ? "eyeOff" : "eye"} 21 | size="s" 22 | type="button" 23 | /> 24 | } 25 | /> 26 | ); 27 | }); 28 | 29 | PasswordInput.displayName = "PasswordInput"; 30 | -------------------------------------------------------------------------------- /src/once-ui/components/RevealFx.module.scss: -------------------------------------------------------------------------------- 1 | .revealFx { 2 | mask-image: linear-gradient(to right, black 0%, black 25%, transparent 50%); 3 | mask-size: 300% 100%; 4 | transition: all ease-in-out; 5 | 6 | &.hidden { 7 | mask-position: 100% 0; 8 | filter: blur(0.5rem); 9 | } 10 | 11 | &.revealed { 12 | mask-position: 0 0; 13 | filter: blur(0); 14 | } 15 | } -------------------------------------------------------------------------------- /src/once-ui/components/RevealFx.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useEffect, forwardRef } from "react"; 4 | import { SpacingToken } from "../types"; 5 | import styles from "./RevealFx.module.scss"; 6 | import { Flex } from "."; 7 | 8 | interface RevealFxProps extends React.ComponentProps { 9 | children: React.ReactNode; 10 | speed?: "slow" | "medium" | "fast"; 11 | delay?: number; 12 | revealedByDefault?: boolean; 13 | translateY?: number | SpacingToken; 14 | trigger?: boolean; 15 | style?: React.CSSProperties; 16 | className?: string; 17 | } 18 | 19 | const RevealFx = forwardRef( 20 | ( 21 | { 22 | children, 23 | speed = "medium", 24 | delay = 0, 25 | revealedByDefault = false, 26 | translateY, 27 | trigger, 28 | style, 29 | className, 30 | ...rest 31 | }, 32 | ref, 33 | ) => { 34 | const [isRevealed, setIsRevealed] = useState(revealedByDefault); 35 | 36 | useEffect(() => { 37 | const timer = setTimeout(() => { 38 | setIsRevealed(true); 39 | }, delay * 1000); 40 | 41 | return () => clearTimeout(timer); 42 | }, [delay]); 43 | 44 | useEffect(() => { 45 | if (trigger !== undefined) { 46 | setIsRevealed(trigger); 47 | } 48 | }, [trigger]); 49 | 50 | const getSpeedDuration = () => { 51 | switch (speed) { 52 | case "fast": 53 | return "1s"; 54 | case "medium": 55 | return "2s"; 56 | case "slow": 57 | return "3s"; 58 | default: 59 | return "2s"; 60 | } 61 | }; 62 | 63 | const getTranslateYValue = () => { 64 | if (typeof translateY === "number") { 65 | return `${translateY}rem`; 66 | } else if (typeof translateY === "string") { 67 | return `var(--static-space-${translateY})`; 68 | } 69 | return undefined; 70 | }; 71 | 72 | const translateValue = getTranslateYValue(); 73 | 74 | const revealStyle: React.CSSProperties = { 75 | transitionDuration: getSpeedDuration(), 76 | transform: isRevealed ? "translateY(0)" : `translateY(${translateValue})`, 77 | ...style, 78 | }; 79 | 80 | return ( 81 | 89 | {children} 90 | 91 | ); 92 | }, 93 | ); 94 | 95 | RevealFx.displayName = "RevealFx"; 96 | export { RevealFx }; 97 | -------------------------------------------------------------------------------- /src/once-ui/components/Row.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { forwardRef } from "react"; 4 | import { Flex } from "."; 5 | 6 | interface RowProps extends React.ComponentProps { 7 | children?: React.ReactNode; 8 | } 9 | 10 | const Row = forwardRef(({ children, ...rest }, ref) => { 11 | return ( 12 | 13 | {children} 14 | 15 | ); 16 | }); 17 | 18 | Row.displayName = "Row"; 19 | export { Row }; 20 | -------------------------------------------------------------------------------- /src/once-ui/components/ScrollToTop.module.scss: -------------------------------------------------------------------------------- 1 | .scrollToTop { 2 | opacity: 0; 3 | visibility: hidden; 4 | transition: opacity 200ms ease-in-out, visibility 0ms linear 200ms; 5 | 6 | &[data-visible="true"] { 7 | opacity: 1; 8 | visibility: visible; 9 | transition: opacity 200ms ease-in-out, visibility 0ms linear; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/once-ui/components/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | import { Flex } from "@/once-ui/components"; 3 | import styles from "./ScrollToTop.module.scss"; 4 | import classNames from "classnames"; 5 | 6 | interface ScrollToTopProps extends React.ComponentProps { 7 | offset?: number; 8 | } 9 | 10 | export const ScrollToTop = ({ 11 | children, 12 | offset = 300, 13 | className, 14 | ...rest 15 | }: ScrollToTopProps) => { 16 | const [isVisible, setIsVisible] = useState(false); 17 | 18 | const handleScroll = () => { 19 | setIsVisible(window.scrollY > offset); 20 | }; 21 | 22 | const scrollToTop = () => { 23 | window.scrollTo({ 24 | top: 0, 25 | behavior: "smooth", 26 | }); 27 | }; 28 | 29 | useEffect(() => { 30 | window.addEventListener("scroll", handleScroll); 31 | return () => window.removeEventListener("scroll", handleScroll); 32 | }, []); 33 | 34 | return ( 35 | 48 | {children} 49 | 50 | ); 51 | }; -------------------------------------------------------------------------------- /src/once-ui/components/Scroller.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | isolation: isolate; 3 | } 4 | 5 | .scroller { 6 | scrollbar-width: none; 7 | isolation: isolate; 8 | 9 | &::-webkit-scrollbar { 10 | display: none; 11 | } 12 | } 13 | 14 | .row { 15 | overflow-x: auto; 16 | } 17 | 18 | .column { 19 | overflow-y: auto; 20 | } 21 | 22 | .scrollButton { 23 | position: absolute; 24 | top: 50%; 25 | transform: translateY(-50%); 26 | z-index: 2; 27 | } 28 | 29 | .scrollButtonPrev { 30 | left: var(--static-space-4); 31 | } 32 | 33 | .scrollButtonNext { 34 | right: var(--static-space-4); 35 | } -------------------------------------------------------------------------------- /src/once-ui/components/SharedInteractiveStyles.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | cursor: var(--cursor-interactive); 3 | isolation: isolate; 4 | 5 | &:hover, &:focus { 6 | .element.checked .element::before { 7 | display: none; 8 | } 9 | } 10 | } 11 | 12 | .element { 13 | box-shadow: inset 0 0 0 var(--solid-inset-color-brand); 14 | border-color: var(--solid-border-color-neutral); 15 | border-style: solid; 16 | border-width: 1px; 17 | width: var(--static-space-20); 18 | height: var(--static-space-20); 19 | min-width: var(--static-space-20); 20 | min-height: var(--static-space-20); 21 | transition: var(--transition-micro-medium); 22 | background-color: var(--background-surface); 23 | outline: none; 24 | 25 | &.checked { 26 | box-shadow: inset 0 var(--solid-inset-distance) var(--solid-inset-size) var(--solid-inset-color-brand); 27 | background-color: var(--brand-solid-medium); 28 | border-color: var(--solid-border-color-brand); 29 | } 30 | } 31 | 32 | .disabled { 33 | .element { 34 | opacity: 0.6; 35 | } 36 | 37 | .element::before { 38 | display: none; 39 | } 40 | } 41 | 42 | .container:hover .element::before, 43 | .element:focus-within::before { 44 | content: ''; 45 | position: absolute; 46 | top: 50%; 47 | left: 50%; 48 | transform: translate(-50%, -50%); 49 | width: var(--static-space-40); 50 | height: var(--static-space-40); 51 | background-color: var(--brand-alpha-medium); 52 | border-radius: var(--radius-full); 53 | z-index: -1; 54 | animation: scaleInCenter 0.2s forwards; 55 | } 56 | 57 | @keyframes scaleInCenter { 58 | from { 59 | transform: translate(-50%, -50%) scale(0); 60 | } 61 | to { 62 | transform: translate(-50%, -50%) scale(1); 63 | } 64 | } 65 | 66 | .icon { 67 | animation: scaleIn 0.2s forwards; 68 | animation-delay: 0.1s; 69 | transform: scale(0); 70 | } 71 | 72 | @keyframes scaleIn { 73 | from { 74 | transform: scale(0); 75 | } 76 | to { 77 | transform: scale(1); 78 | } 79 | } 80 | 81 | .hidden { 82 | position: absolute; 83 | opacity: 0; 84 | pointer-events: none; 85 | } 86 | 87 | .indeterminate { 88 | background: var(--brand-on-solid-strong); 89 | width: var(--static-space-12); 90 | height: var(--static-space-2); 91 | } -------------------------------------------------------------------------------- /src/once-ui/components/Skeleton.module.scss: -------------------------------------------------------------------------------- 1 | @keyframes skeleton-loading { 2 | 0% { 3 | background-color: var(--neutral-background-strong); 4 | } 5 | 50% { 6 | background-color: var(--neutral-background-medium); 7 | } 8 | 100% { 9 | background-color: var(--neutral-background-strong); 10 | } 11 | } 12 | 13 | .delay-1 { 14 | animation-delay: 0.1s; 15 | } 16 | 17 | .delay-2 { 18 | animation-delay: 0.2s; 19 | } 20 | 21 | .delay-3 { 22 | animation-delay: 0.3s; 23 | } 24 | 25 | .delay-4 { 26 | animation-delay: 0.4s; 27 | } 28 | 29 | .delay-5 { 30 | animation-delay: 0.5s; 31 | } 32 | 33 | .delay-6 { 34 | animation-delay: 0.6s; 35 | } 36 | 37 | .skeleton { 38 | animation-name: skeleton-loading; 39 | animation-duration: 1.5s; 40 | animation-iteration-count: infinite; 41 | 42 | &.block { 43 | width: 100%; 44 | height: 100%; 45 | } 46 | 47 | &.line { 48 | &.h-xs { 49 | height: var(--static-space-8); 50 | } 51 | &.h-s { 52 | height: var(--static-space-12); 53 | } 54 | &.h-m { 55 | height: var(--static-space-16); 56 | } 57 | &.h-l { 58 | height: var(--static-space-20); 59 | } 60 | &.h-xl { 61 | height: var(--static-space-24); 62 | } 63 | 64 | &.w-xs { 65 | width: 25%; 66 | } 67 | &.w-s { 68 | width: 33%; 69 | } 70 | &.w-m { 71 | width: 50%; 72 | } 73 | &.w-l { 74 | width: 75%; 75 | } 76 | &.w-xl { 77 | width: 100%; 78 | } 79 | } 80 | 81 | &.circle { 82 | border-radius: var(--radius-full); 83 | 84 | &.w-xs { 85 | width: var(--static-space-20); 86 | height: var(--static-space-20); 87 | } 88 | &.w-s { 89 | width: var(--static-space-24); 90 | height: var(--static-space-24); 91 | } 92 | &.w-m { 93 | width: var(--static-space-32); 94 | height: var(--static-space-32); 95 | } 96 | &.w-l { 97 | width: var(--static-space-40); 98 | height: var(--static-space-40); 99 | } 100 | &.w-xl { 101 | width: var(--static-space-160); 102 | height: var(--static-space-160); 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /src/once-ui/components/Skeleton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef } from "react"; 4 | import classNames from "classnames"; 5 | 6 | import styles from "./Skeleton.module.scss"; 7 | import { Flex } from "./Flex"; 8 | 9 | interface SkeletonProps extends React.ComponentProps { 10 | shape: "line" | "circle" | "block"; 11 | width?: "xl" | "l" | "m" | "s" | "xs"; 12 | height?: "xl" | "l" | "m" | "s" | "xs"; 13 | delay?: "1" | "2" | "3" | "4" | "5" | "6"; 14 | style?: React.CSSProperties; 15 | className?: string; 16 | } 17 | 18 | const Skeleton: React.FC = forwardRef( 19 | ({ shape = "line", width, height, delay, style, className, ...props }, ref) => { 20 | return ( 21 | 36 | ); 37 | }, 38 | ); 39 | 40 | Skeleton.displayName = "Skeleton"; 41 | 42 | export { Skeleton }; 43 | -------------------------------------------------------------------------------- /src/once-ui/components/SmartLink.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef, ReactNode } from "react"; 4 | import classNames from "classnames"; 5 | import { Icon } from "."; 6 | import { ElementType } from "./ElementType"; 7 | import { IconName } from "../icons"; 8 | 9 | interface CommonProps { 10 | prefixIcon?: IconName; 11 | suffixIcon?: IconName; 12 | fillWidth?: boolean; 13 | iconSize?: "xs" | "s" | "m" | "l" | "xl"; 14 | selected?: boolean; 15 | unstyled?: boolean; 16 | children: ReactNode; 17 | href?: string; 18 | style?: React.CSSProperties; 19 | className?: string; 20 | } 21 | 22 | export type SmartLinkProps = CommonProps & 23 | React.AnchorHTMLAttributes; 24 | 25 | const SmartLink = forwardRef( 26 | ( 27 | { 28 | href, 29 | prefixIcon, 30 | suffixIcon, 31 | fillWidth = false, 32 | iconSize = "xs", 33 | style, 34 | className, 35 | selected, 36 | unstyled = false, 37 | children, 38 | ...props 39 | }, 40 | ref 41 | ) => { 42 | const content = ( 43 | <> 44 | {prefixIcon && } 45 | {children} 46 | {suffixIcon && } 47 | > 48 | ); 49 | 50 | const commonProps = { 51 | ref, 52 | className: classNames( 53 | className, 54 | "reset-button-styles focus-ring align-center display-inline-flex g-8 radius-s", 55 | { 56 | "fill-width": fillWidth, 57 | "fit-width": !fillWidth, 58 | "px-2 mx-2": !unstyled, 59 | } 60 | ), 61 | style: !unstyled 62 | ? { 63 | ...(selected && { 64 | textDecoration: "underline", 65 | }), 66 | ...style, 67 | } 68 | : { 69 | textDecoration: "none", 70 | ...style, 71 | }, 72 | ...props, 73 | }; 74 | 75 | return ( 76 | 77 | {content} 78 | 79 | ); 80 | } 81 | ); 82 | 83 | SmartLink.displayName = "SmartLink"; 84 | 85 | export { SmartLink }; 86 | -------------------------------------------------------------------------------- /src/once-ui/components/Spinner.module.scss: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | 0% { 3 | transform: rotate(0deg); 4 | animation-timing-function: cubic-bezier(0.55, 0.2, 0.68, 0.53); 5 | } 6 | 100% { 7 | transform: rotate(360deg); 8 | animation-timing-function: cubic-bezier(0.55, 0.2, 0.68, 0.53); 9 | } 10 | } 11 | 12 | .spinner { 13 | width: 100%; 14 | height: 100%; 15 | border-radius: 50%; 16 | animation: spin 1.5s infinite; 17 | border-style: solid; 18 | border-color: transparent; 19 | border-top-color: currentColor; 20 | } 21 | 22 | .xs { 23 | width: var(--static-space-16); 24 | height: var(--static-space-16); 25 | padding: 2px; 26 | 27 | .spinner { 28 | border-width: 2px; 29 | } 30 | } 31 | 32 | .s { 33 | width: var(--static-space-20); 34 | height: var(--static-space-20); 35 | padding: 2px; 36 | 37 | .spinner { 38 | border-width: 2px; 39 | } 40 | } 41 | 42 | .m { 43 | width: var(--static-space-24); 44 | height: var(--static-space-24); 45 | padding: 3px; 46 | 47 | .spinner { 48 | border-width: 2px; 49 | } 50 | } 51 | 52 | .l { 53 | width: var(--static-space-32); 54 | height: var(--static-space-32); 55 | padding: 4px; 56 | 57 | .spinner { 58 | border-width: 3px; 59 | } 60 | } 61 | 62 | .xl { 63 | width: var(--static-space-40); 64 | height: var(--static-space-40); 65 | padding: 8px; 66 | 67 | .spinner { 68 | border-width: 3px; 69 | } 70 | } -------------------------------------------------------------------------------- /src/once-ui/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef } from "react"; 2 | 3 | import styles from "./Spinner.module.scss"; 4 | import { Flex } from "./Flex"; 5 | 6 | interface SpinnerProps extends React.ComponentProps { 7 | size?: "xs" | "s" | "m" | "l" | "xl"; 8 | ariaLabel?: string; 9 | className?: string; 10 | style?: React.CSSProperties; 11 | } 12 | 13 | const Spinner = forwardRef( 14 | ({ size = "m", ariaLabel = "Loading", className, style, ...rest }, ref) => { 15 | return ( 16 | 17 | 25 | 26 | 27 | 28 | ); 29 | }, 30 | ); 31 | 32 | Spinner.displayName = "Spinner"; 33 | 34 | export { Spinner }; 35 | -------------------------------------------------------------------------------- /src/once-ui/components/StatusIndicator.module.scss: -------------------------------------------------------------------------------- 1 | .statusIndicator { 2 | &.s { 3 | width: var(--static-space-4); 4 | height: var(--static-space-4); 5 | } 6 | 7 | &.m { 8 | width: var(--static-space-8); 9 | height: var(--static-space-8); 10 | } 11 | 12 | &.l { 13 | width: var(--static-space-16); 14 | height: var(--static-space-16); 15 | } 16 | 17 | &.gray { 18 | background-color: var(--scheme-gray-700); 19 | } 20 | 21 | &.blue { 22 | background-color: var(--scheme-blue-700); 23 | } 24 | 25 | &.indigo { 26 | background-color: var(--scheme-indigo-700); 27 | } 28 | 29 | &.violet { 30 | background-color: var(--scheme-violet-700); 31 | } 32 | 33 | &.magenta { 34 | background-color: var(--scheme-magenta-700); 35 | } 36 | 37 | &.pink { 38 | background-color: var(--scheme-pink-700); 39 | } 40 | 41 | &.red { 42 | background-color: var(--scheme-red-700); 43 | } 44 | 45 | &.orange { 46 | background-color: var(--scheme-orange-700); 47 | } 48 | 49 | &.yellow { 50 | background-color: var(--scheme-yellow-700); 51 | } 52 | 53 | &.moss { 54 | background-color: var(--scheme-moss-700); 55 | } 56 | 57 | &.green { 58 | background-color: var(--scheme-green-700); 59 | } 60 | 61 | &.emerald { 62 | background-color: var(--scheme-emerald-700); 63 | } 64 | 65 | &.aqua { 66 | background-color: var(--scheme-aqua-700); 67 | } 68 | 69 | &.cyan { 70 | background-color: var(--scheme-cyan-700); 71 | } 72 | } -------------------------------------------------------------------------------- /src/once-ui/components/StatusIndicator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef } from "react"; 4 | import classNames from "classnames"; 5 | import styles from "./StatusIndicator.module.scss"; 6 | import { Flex } from "./Flex"; 7 | 8 | interface StatusIndicatorProps extends React.ComponentProps { 9 | size?: "s" | "m" | "l"; 10 | color: 11 | | "blue" 12 | | "indigo" 13 | | "violet" 14 | | "magenta" 15 | | "pink" 16 | | "red" 17 | | "orange" 18 | | "yellow" 19 | | "moss" 20 | | "green" 21 | | "emerald" 22 | | "aqua" 23 | | "cyan" 24 | | "gray"; 25 | ariaLabel?: string; 26 | className?: string; 27 | style?: React.CSSProperties; 28 | } 29 | 30 | const StatusIndicator = forwardRef( 31 | ( 32 | { size = "m", color, ariaLabel = `${color} status indicator`, className, style, ...rest }, 33 | ref, 34 | ) => { 35 | return ( 36 | 44 | ); 45 | }, 46 | ); 47 | 48 | StatusIndicator.displayName = "StatusIndicator"; 49 | 50 | export { StatusIndicator }; 51 | -------------------------------------------------------------------------------- /src/once-ui/components/StyleOverlay.module.scss: -------------------------------------------------------------------------------- 1 | .panel { 2 | visibility: hidden; 3 | opacity: 0; 4 | z-index: -1; 5 | transform: scale(0.2); 6 | transform-origin: top right; 7 | 8 | &.open { 9 | transform: scale(1); 10 | visibility: visible; 11 | opacity: 1; 12 | } 13 | 14 | &:not(.open) { 15 | z-index: -1; 16 | filter: blur(0.25rem); 17 | } 18 | } -------------------------------------------------------------------------------- /src/once-ui/components/StyleOverlay.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { forwardRef, useState } from "react"; 4 | import { IconButton, StylePanel, Flex, Background } from "."; 5 | import styles from "./StyleOverlay.module.scss"; 6 | 7 | interface StyleOverlayProps extends React.ComponentProps { 8 | iconButtonProps?: Partial>; 9 | } 10 | 11 | const StyleOverlay = forwardRef( 12 | ({ iconButtonProps, ...rest }, ref) => { 13 | const [isOpen, setIsOpen] = useState(false); 14 | 15 | const togglePanel = () => { 16 | setIsOpen(!isOpen); 17 | }; 18 | 19 | return ( 20 | 21 | 22 | 42 | 43 | 44 | 54 | 60 | 61 | 62 | 63 | ); 64 | }, 65 | ); 66 | 67 | StyleOverlay.displayName = "StyleOverlay"; 68 | export { StyleOverlay }; 69 | -------------------------------------------------------------------------------- /src/once-ui/components/StylePanel.module.scss: -------------------------------------------------------------------------------- 1 | .select { 2 | min-width: var(--static-space-40); 3 | min-height: var(--static-space-40); 4 | border-radius: var(--radius-m-nest-4); 5 | border-color: var(--static-transparent); 6 | border-width: 1px; 7 | border-style: solid; 8 | background: var(--static-transparent); 9 | 10 | &:hover { 11 | background: var(--neutral-alpha-medium); 12 | border-color: var(--neutral-alpha-medium); 13 | } 14 | 15 | &.selected { 16 | background: var(--neutral-alpha-strong); 17 | border-color: var(--neutral-alpha-strong); 18 | } 19 | } 20 | 21 | .swatch { 22 | width: 100%; 23 | height: 100%; 24 | border-radius: var(--radius-m); 25 | border-width: 1px; 26 | border-style: solid; 27 | } 28 | 29 | .slate { 30 | background: var(--scheme-slate-500); 31 | border-color: var(--scheme-slate-700); 32 | } 33 | 34 | .gray { 35 | background: var(--scheme-gray-500); 36 | border-color: var(--scheme-gray-700); 37 | } 38 | 39 | .sand { 40 | background: var(--scheme-sand-500); 41 | border-color: var(--scheme-sand-700); 42 | } 43 | 44 | .blue { 45 | background: var(--scheme-blue-500); 46 | border-color: var(--scheme-blue-700); 47 | } 48 | 49 | .cyan { 50 | background: var(--scheme-cyan-500); 51 | border-color: var(--scheme-cyan-700); 52 | } 53 | 54 | .indigo { 55 | background: var(--scheme-indigo-500); 56 | border-color: var(--scheme-indigo-700); 57 | } 58 | 59 | .violet { 60 | background: var(--scheme-violet-500); 61 | border-color: var(--scheme-violet-700); 62 | } 63 | 64 | .magenta { 65 | background: var(--scheme-magenta-500); 66 | border-color: var(--scheme-magenta-700); 67 | } 68 | 69 | .pink { 70 | background: var(--scheme-pink-500); 71 | border-color: var(--scheme-pink-700); 72 | } 73 | 74 | .yellow { 75 | background: var(--scheme-yellow-500); 76 | border-color: var(--scheme-yellow-700); 77 | } 78 | 79 | .orange { 80 | background: var(--scheme-orange-500); 81 | border-color: var(--scheme-orange-700); 82 | } 83 | 84 | .red { 85 | background: var(--scheme-red-500); 86 | border-color: var(--scheme-red-700); 87 | } 88 | 89 | .moss { 90 | background: var(--scheme-moss-500); 91 | border-color: var(--scheme-moss-700); 92 | } 93 | 94 | .green { 95 | background: var(--scheme-green-500); 96 | border-color: var(--scheme-green-700); 97 | } 98 | 99 | .emerald { 100 | background: var(--scheme-emerald-500); 101 | border-color: var(--scheme-emerald-700); 102 | } 103 | 104 | .aqua { 105 | background: var(--scheme-aqua-500); 106 | border-color: var(--scheme-aqua-700); 107 | } 108 | 109 | .neutral { 110 | background: var(--neutral-solid-medium); 111 | border-color: var(--neutral-alpha-strong); 112 | } -------------------------------------------------------------------------------- /src/once-ui/components/Switch.module.scss: -------------------------------------------------------------------------------- 1 | .container { 2 | cursor: var(--cursor-interactive); 3 | isolation: isolate; 4 | 5 | &:active { 6 | .element { 7 | transform: translateY(-50%) scaleX(1.2); 8 | } 9 | } 10 | 11 | &:hover { 12 | .switch { 13 | background-color: var(--neutral-solid-strong); 14 | } 15 | 16 | .switch.checked { 17 | background-color: var(--brand-solid-strong); 18 | } 19 | 20 | .switch.checked .element::before { 21 | display: none; 22 | } 23 | } 24 | } 25 | 26 | .reverse { 27 | flex-direction: row-reverse; 28 | } 29 | 30 | .switch { 31 | box-shadow: inset 0 0 0 var(--solid-inset-color-brand); 32 | border-color: var(--solid-border-color-neutral); 33 | border-style: solid; 34 | border-width: var(--solid-border-width); 35 | width: var(--static-space-40); 36 | min-width: var(--static-space-40); 37 | height: var(--static-space-24); 38 | border-radius: var(--radius-l-nest-4); 39 | background-color: var(--neutral-solid-medium); 40 | position: relative; 41 | transition: var(--transition-micro-medium); 42 | 43 | &.checked { 44 | box-shadow: inset 0 var(--solid-inset-distance) var(--solid-inset-size) var(--solid-inset-color-brand); 45 | background-color: var(--brand-solid-medium); 46 | border-color: var(--solid-border-color-brand); 47 | 48 | .element { 49 | left: calc(100% - var(--static-space-20)); 50 | transform-origin: right; 51 | } 52 | } 53 | } 54 | 55 | .element { 56 | z-index: 1; 57 | transform-origin: left; 58 | width: var(--static-space-16); 59 | height: var(--static-space-16); 60 | border-radius: var(--radius-l); 61 | background-color: var(--brand-on-solid-strong); 62 | position: absolute; 63 | top: 50%; 64 | transform: translateY(-50%); 65 | left: var(--static-space-4); 66 | transition: left 0.3s, transform 0.3s; 67 | outline: none; 68 | } 69 | 70 | .disabled { 71 | .switch { 72 | opacity: 0.4; 73 | } 74 | 75 | .element::before { 76 | display: none !important; 77 | } 78 | } 79 | 80 | .container:hover .element::before, 81 | .element:focus-within::before { 82 | content: ''; 83 | position: absolute; 84 | top: 50%; 85 | left: 50%; 86 | transform: translate(-50%, -50%); 87 | width: var(--static-space-40); 88 | height: var(--static-space-40); 89 | background-color: var(--brand-alpha-medium); 90 | border-radius: var(--radius-full); 91 | z-index: -1; 92 | animation: scaleInCenter 0.2s forwards; 93 | } 94 | 95 | @keyframes scaleInCenter { 96 | from { 97 | transform: translate(-50%, -50%) scale(0); 98 | } 99 | to { 100 | transform: translate(-50%, -50%) scale(1); 101 | } 102 | } -------------------------------------------------------------------------------- /src/once-ui/components/Tag.module.scss: -------------------------------------------------------------------------------- 1 | .tag { 2 | white-space: nowrap; 3 | user-select: none; 4 | 5 | &.brand { 6 | border-color: var(--brand-border-strong); 7 | background-color: var(--brand-background-strong); 8 | color: var(--brand-on-background-medium); 9 | } 10 | 11 | &.accent { 12 | border-color: var(--accent-border-strong); 13 | background-color: var(--accent-background-strong); 14 | color: var(--accent-on-background-medium); 15 | } 16 | 17 | &.neutral { 18 | border-color: var(--neutral-border-medium); 19 | background-color: var(--surface-background); 20 | color: var(--neutral-on-background-medium); 21 | } 22 | 23 | &.warning { 24 | border-color: var(--warning-border-strong); 25 | background-color: var(--warning-background-strong); 26 | color: var(--warning-on-background-medium); 27 | } 28 | 29 | &.danger { 30 | border-color: var(--danger-border-strong); 31 | background-color: var(--danger-background-strong); 32 | color: var(--danger-on-background-medium); 33 | } 34 | 35 | &.success { 36 | border-color: var(--success-border-strong); 37 | background-color: var(--success-background-strong); 38 | color: var(--success-on-background-medium); 39 | } 40 | 41 | &.info { 42 | border-color: var(--info-border-strong); 43 | background-color: var(--info-background-strong); 44 | color: var(--info-on-background-medium); 45 | } 46 | 47 | &.gradient { 48 | border-color: var(--brand-alpha-medium); 49 | background-image: linear-gradient(45deg, var(--brand-background-strong), var(--accent-background-strong)); 50 | color: var(--brand-on-background-medium); 51 | background-clip: padding-box; 52 | } 53 | } 54 | 55 | .s { 56 | padding: 0 var(--static-space-4); 57 | } 58 | 59 | .m { 60 | padding: var(--static-space-2) var(--static-space-8); 61 | } 62 | 63 | .l { 64 | padding: var(--static-space-4) var(--static-space-12); 65 | } -------------------------------------------------------------------------------- /src/once-ui/components/Tag.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef, ReactNode } from "react"; 4 | import classNames from "classnames"; 5 | 6 | import { Flex, Text, Icon } from "."; 7 | import styles from "./Tag.module.scss"; 8 | import { IconName } from "../icons"; 9 | 10 | interface TagProps extends React.ComponentProps { 11 | variant?: 12 | | "brand" 13 | | "accent" 14 | | "warning" 15 | | "success" 16 | | "danger" 17 | | "neutral" 18 | | "info" 19 | | "gradient"; 20 | size?: "s" | "m" | "l"; 21 | label?: string; 22 | prefixIcon?: IconName; 23 | suffixIcon?: IconName; 24 | children?: ReactNode; 25 | } 26 | 27 | const Tag = forwardRef( 28 | ( 29 | { 30 | variant = "neutral", 31 | size = "m", 32 | label = "", 33 | prefixIcon, 34 | suffixIcon, 35 | className, 36 | children, 37 | ...rest 38 | }, 39 | ref 40 | ) => { 41 | const paddingSize = size === "s" ? "2" : "4"; 42 | 43 | return ( 44 | 60 | {prefixIcon && } 61 | 66 | 67 | {label || children} 68 | 69 | 70 | {suffixIcon && } 71 | 72 | ); 73 | } 74 | ); 75 | 76 | Tag.displayName = "Tag"; 77 | 78 | export { Tag }; 79 | export type { TagProps }; 80 | -------------------------------------------------------------------------------- /src/once-ui/components/TagInput.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { 4 | useState, 5 | KeyboardEventHandler, 6 | ChangeEventHandler, 7 | FocusEventHandler, 8 | forwardRef, 9 | } from "react"; 10 | 11 | import { Flex, Chip, Input, InputProps } from "."; 12 | 13 | interface TagInputProps extends Omit { 14 | value: string[]; 15 | onChange: (value: string[]) => void; 16 | } 17 | 18 | const TagInput = forwardRef( 19 | ({ value, onChange, label, placeholder, ...inputProps }, ref) => { 20 | const [inputValue, setInputValue] = useState(""); 21 | const [isFocused, setIsFocused] = useState(false); 22 | 23 | const handleInputChange: ChangeEventHandler = (e) => { 24 | setInputValue(e.target.value); 25 | }; 26 | 27 | const handleKeyDown: KeyboardEventHandler = (e) => { 28 | if (e.key === "Enter" || e.key === ",") { 29 | e.preventDefault(); 30 | if (inputValue.trim()) { 31 | onChange([...value, inputValue.trim()]); 32 | setInputValue(""); 33 | } 34 | } 35 | }; 36 | 37 | const handleRemoveTag = (index: number) => { 38 | const newValue = value.filter((_, i) => i !== index); 39 | onChange(newValue); 40 | }; 41 | 42 | const handleFocus: FocusEventHandler = () => { 43 | setIsFocused(true); 44 | }; 45 | 46 | const handleBlur: FocusEventHandler = (e) => { 47 | setIsFocused(false); 48 | }; 49 | 50 | return ( 51 | 65 | {value.length > 0 && ( 66 | 76 | {value.map((tag, index) => ( 77 | handleRemoveTag(index)} 81 | aria-label={`Remove tag ${tag}`} 82 | /> 83 | ))} 84 | 85 | )} 86 | 87 | ); 88 | }, 89 | ); 90 | 91 | TagInput.displayName = "TagInput"; 92 | 93 | export { TagInput }; 94 | export type { TagInputProps }; 95 | -------------------------------------------------------------------------------- /src/once-ui/components/ThemeProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createContext, useContext, useEffect, useState } from "react"; 4 | 5 | type Theme = "light" | "dark" | "system"; 6 | 7 | type ThemeProviderProps = { 8 | children: React.ReactNode; 9 | }; 10 | 11 | type ThemeProviderState = { 12 | theme: Theme; 13 | resolvedTheme: 'light' | 'dark'; 14 | setTheme: (theme: Theme) => void; 15 | }; 16 | 17 | const initialState: ThemeProviderState = { 18 | theme: "system", 19 | resolvedTheme: 'dark', 20 | setTheme: () => null, 21 | }; 22 | 23 | const ThemeProviderContext = createContext(initialState); 24 | 25 | export function ThemeProvider({ children }: ThemeProviderProps) { 26 | // Start with system theme on server, will be updated on client 27 | const [theme, setTheme] = useState("system"); 28 | const [resolvedTheme, setResolvedTheme] = useState<'light' | 'dark'>('dark'); 29 | const [mounted, setMounted] = useState(false); 30 | 31 | // Initialize theme from localStorage on mount 32 | useEffect(() => { 33 | const savedTheme = localStorage.getItem('theme') as Theme; 34 | if (savedTheme) { 35 | setTheme(savedTheme); 36 | } 37 | setMounted(true); 38 | }, []); 39 | 40 | // Update resolvedTheme when theme changes 41 | useEffect(() => { 42 | if (!mounted) return; 43 | 44 | const root = document.documentElement; 45 | if (theme === 'system') { 46 | const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches; 47 | setResolvedTheme(isDark ? 'dark' : 'light'); 48 | root.setAttribute('data-theme', isDark ? 'dark' : 'light'); 49 | } else { 50 | setResolvedTheme(theme === 'dark' ? 'dark' : 'light'); 51 | root.setAttribute('data-theme', theme); 52 | } 53 | }, [theme, mounted]); 54 | 55 | // Listen for system theme changes 56 | useEffect(() => { 57 | if (!mounted || theme !== 'system') return; 58 | 59 | const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 60 | const handleChange = (e: MediaQueryListEvent) => { 61 | setResolvedTheme(e.matches ? 'dark' : 'light'); 62 | document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light'); 63 | }; 64 | 65 | mediaQuery.addEventListener('change', handleChange); 66 | return () => mediaQuery.removeEventListener('change', handleChange); 67 | }, [theme, mounted]); 68 | 69 | const value = { 70 | theme, 71 | resolvedTheme, 72 | setTheme: (newTheme: Theme) => { 73 | localStorage.setItem('theme', newTheme); 74 | setTheme(newTheme); 75 | }, 76 | }; 77 | 78 | return {children}; 79 | } 80 | 81 | export const useTheme = () => { 82 | const context = useContext(ThemeProviderContext); 83 | 84 | if (context === undefined) { 85 | throw new Error("useTheme must be used within a ThemeProvider"); 86 | } 87 | 88 | return context; 89 | }; 90 | -------------------------------------------------------------------------------- /src/once-ui/components/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef } from "react"; 4 | import { Row, useTheme, IconButton } from "@/once-ui/components"; 5 | 6 | type ThemeType = "system" | "dark" | "light"; 7 | 8 | interface ThemeSwitchProps extends React.ComponentProps { 9 | defaultTheme?: ThemeType; 10 | } 11 | 12 | const ThemeSwitcher = forwardRef( 13 | ({ defaultTheme = "system", ...rest }, ref) => { 14 | const { theme, setTheme } = useTheme(); 15 | 16 | return ( 17 | 25 | setTheme("system")} 29 | /> 30 | setTheme("dark")} 34 | /> 35 | setTheme("light")} 39 | /> 40 | 41 | ); 42 | }, 43 | ); 44 | 45 | ThemeSwitcher.displayName = "ThemeSwitcher"; 46 | export { ThemeSwitcher }; 47 | -------------------------------------------------------------------------------- /src/once-ui/components/TiltFx.module.scss: -------------------------------------------------------------------------------- 1 | .tiltFx { 2 | transition: transform 0.3s ease-out; 3 | } 4 | 5 | @media (hover: hover) { 6 | .tiltFx { 7 | perspective: 1000px; 8 | transform-style: preserve-3d; 9 | will-change: transform; 10 | } 11 | } -------------------------------------------------------------------------------- /src/once-ui/components/TiltFx.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useRef } from "react"; 4 | import styles from "./TiltFx.module.scss"; 5 | import { Flex } from "."; 6 | 7 | interface TiltFxProps extends React.ComponentProps { 8 | children: React.ReactNode; 9 | } 10 | 11 | const TiltFx: React.FC = ({ children, ...rest }) => { 12 | const ref = useRef(null); 13 | let lastCall = 0; 14 | let resetTimeout: NodeJS.Timeout; 15 | 16 | const handleMouseMove = (e: React.MouseEvent) => { 17 | if ("ontouchstart" in window) return; 18 | 19 | clearTimeout(resetTimeout); 20 | 21 | const now = Date.now(); 22 | if (now - lastCall < 16) return; 23 | lastCall = now; 24 | 25 | const element = ref.current; 26 | if (!element) return; 27 | 28 | const rect = element.getBoundingClientRect(); 29 | const offsetX = e.clientX - rect.left; 30 | const offsetY = e.clientY - rect.top; 31 | 32 | const centerX = rect.width / 2; 33 | const centerY = rect.height / 2; 34 | 35 | const deltaX = (offsetX - centerX) / centerX; 36 | const deltaY = (offsetY - centerY) / centerY; 37 | 38 | const rotateX = -deltaY * 2; 39 | const rotateY = -deltaX * 2; 40 | 41 | window.requestAnimationFrame(() => { 42 | element.style.transform = `perspective(1000px) translate3d(0, 0, 30px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`; 43 | }); 44 | }; 45 | 46 | const handleMouseLeave = () => { 47 | if ("ontouchstart" in window) return; 48 | 49 | const element = ref.current; 50 | if (element) { 51 | resetTimeout = setTimeout(() => { 52 | element.style.transform = 53 | "perspective(1000px) translate3d(0, 0, 0) rotateX(0deg) rotateY(0deg)"; 54 | }, 100); 55 | } 56 | }; 57 | 58 | return ( 59 | 67 | {children} 68 | 69 | ); 70 | }; 71 | 72 | export { TiltFx }; 73 | TiltFx.displayName = "TiltFx"; 74 | -------------------------------------------------------------------------------- /src/once-ui/components/Toast.module.scss: -------------------------------------------------------------------------------- 1 | .toast { 2 | transition: opacity 0.3s, transform 0.3s; 3 | } 4 | 5 | .visible { 6 | opacity: 1; 7 | } 8 | 9 | .hidden { 10 | opacity: 0; 11 | } -------------------------------------------------------------------------------- /src/once-ui/components/Toast.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useEffect, useState, forwardRef } from "react"; 4 | import { IconButton, Icon, Flex, Text } from "."; 5 | import classNames from "classnames"; 6 | import styles from "./Toast.module.scss"; 7 | import { IconName } from "../icons"; 8 | 9 | interface ToastProps { 10 | className?: string; 11 | variant: "success" | "danger"; 12 | icon?: boolean; 13 | onClose?: () => void; 14 | action?: React.ReactNode; 15 | children: React.ReactNode; 16 | } 17 | 18 | const iconMap: { [key in ToastProps["variant"]]: IconName } = { 19 | success: "checkCircle", 20 | danger: "errorCircle", 21 | }; 22 | 23 | const Toast = forwardRef( 24 | ({ variant, className, icon = true, onClose, action, children }, ref) => { 25 | const [visible, setVisible] = useState(true); 26 | 27 | useEffect(() => { 28 | const timer = setTimeout(() => setVisible(false), 6000); 29 | return () => clearTimeout(timer); 30 | }, []); 31 | 32 | useEffect(() => { 33 | if (!visible && onClose) { 34 | onClose(); 35 | } 36 | }, [visible, onClose]); 37 | 38 | return ( 39 | 54 | 55 | {icon && ( 56 | 61 | )} 62 | 63 | {children} 64 | 65 | {action && {action}} 66 | {onClose && ( 67 | setVisible(false)} 74 | /> 75 | )} 76 | 77 | 78 | ); 79 | } 80 | ); 81 | 82 | Toast.displayName = "Toast"; 83 | 84 | export { Toast }; 85 | -------------------------------------------------------------------------------- /src/once-ui/components/ToastProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { createContext, useContext, useState, ReactNode } from "react"; 4 | import { Toaster } from "./Toaster"; 5 | 6 | interface Toast { 7 | id: string; 8 | variant: "success" | "danger"; 9 | message: string; 10 | action?: ReactNode; 11 | } 12 | 13 | interface ToastContextProps { 14 | toasts: Toast[]; 15 | addToast: (toast: Omit) => void; 16 | removeToast: (id: string) => void; 17 | } 18 | 19 | const ToastContext = createContext(undefined); 20 | 21 | export const useToast = () => { 22 | const context = useContext(ToastContext); 23 | if (!context) { 24 | throw new Error("useToast must be used within a ToastProvider"); 25 | } 26 | return context; 27 | }; 28 | 29 | const ToastProvider: React.FC<{ 30 | children: ReactNode; 31 | }> = ({ children }) => { 32 | const [toasts, setToasts] = useState([]); 33 | 34 | const addToast = (toast: Omit) => { 35 | const newToast: Toast = { 36 | id: Math.random().toString(36).substring(7), 37 | ...toast, 38 | }; 39 | setToasts((prev) => [...prev, newToast]); 40 | }; 41 | 42 | const removeToast = (id: string) => { 43 | setToasts((prev) => prev.filter((toast) => toast.id !== id)); 44 | }; 45 | 46 | return ( 47 | 54 | {children} 55 | 56 | 57 | ); 58 | }; 59 | 60 | ToastProvider.displayName = "ToastProvider"; 61 | export { ToastProvider }; 62 | -------------------------------------------------------------------------------- /src/once-ui/components/Toaster.module.scss: -------------------------------------------------------------------------------- 1 | .toastContainer { 2 | bottom: var(--responsive-space-l); 3 | left: 50%; 4 | transform: translateX(-50%); 5 | } 6 | 7 | .toastWrapper { 8 | bottom: 0; 9 | transition: transform 0.3s, opacity 0.3s; 10 | } 11 | 12 | .toastAnimation { 13 | animation: fadeIn 0.3s ease-out forwards; 14 | } 15 | 16 | @keyframes fadeIn { 17 | from { 18 | opacity: 0; 19 | transform: translateY(4rem); 20 | } 21 | to { 22 | opacity: 1; 23 | transform: translateY(0); 24 | } 25 | } -------------------------------------------------------------------------------- /src/once-ui/components/Toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useEffect, useState } from "react"; 4 | import { createPortal } from "react-dom"; 5 | import { Flex, Toast } from "."; 6 | import styles from "./Toaster.module.scss"; 7 | 8 | interface ToasterProps { 9 | toasts: { 10 | id: string; 11 | variant: "success" | "danger"; 12 | message: string; 13 | action?: React.ReactNode; 14 | }[]; 15 | removeToast: (id: string) => void; 16 | } 17 | 18 | const Toaster: React.FC = ({ toasts, removeToast }) => { 19 | const [mounted, setMounted] = useState(false); 20 | 21 | useEffect(() => { 22 | setMounted(true); 23 | return () => setMounted(false); 24 | }, []); 25 | 26 | if (!mounted) return null; 27 | 28 | return createPortal( 29 | 37 | {toasts.map((toast, index, array) => ( 38 | 50 | removeToast(toast.id)} 54 | action={toast.action} 55 | > 56 | {toast.message} 57 | 58 | 59 | ))} 60 | , 61 | document.body, 62 | ); 63 | }; 64 | 65 | Toaster.displayName = "Toaster"; 66 | export { Toaster }; 67 | -------------------------------------------------------------------------------- /src/once-ui/components/ToggleButton.module.scss: -------------------------------------------------------------------------------- 1 | .button { 2 | display: flex; 3 | align-items: center; 4 | gap: var(--static-space-8); 5 | border-width: 1px; 6 | border-style: solid; 7 | background: var(--static-transparent); 8 | color: var(--neutral-on-background-strong); 9 | transition: var(--transition-micro-medium); 10 | user-select: none; 11 | white-space: nowrap; 12 | 13 | &:hover, &:focus { 14 | background: var(--neutral-alpha-weak); 15 | border-color: var(--neutral-alpha-weak); 16 | } 17 | 18 | &.selected { 19 | background-color: var(--neutral-alpha-medium); 20 | border-color: var(--neutral-alpha-weak); 21 | } 22 | 23 | &:disabled { 24 | background: var(--neutral-alpha-weak); 25 | color: var(--neutral-on-background-medium); 26 | border: none; 27 | pointer-events: none; 28 | cursor: not-allowed; 29 | } 30 | } 31 | 32 | .s { 33 | height: var(--static-space-24); 34 | min-height: var(--static-space-24); 35 | padding: var(--static-space-2) var(--static-space-8); 36 | } 37 | 38 | .m { 39 | height: var(--static-space-32); 40 | min-height: var(--static-space-32); 41 | padding: var(--static-space-4) var(--static-space-12); 42 | } 43 | 44 | .l { 45 | height: var(--static-space-40); 46 | min-height: var(--static-space-40); 47 | padding: var(--static-space-8) var(--static-space-16); 48 | } 49 | 50 | .ghost { 51 | border-color: var(--static-transparent); 52 | } 53 | 54 | .outline { 55 | border-color: var(--neutral-alpha-weak); 56 | } -------------------------------------------------------------------------------- /src/once-ui/components/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef, ReactNode } from "react"; 4 | import classNames from "classnames"; 5 | 6 | import { Flex, Icon } from "."; 7 | import { IconName } from "../icons"; 8 | 9 | type TooltipProps = { 10 | label: ReactNode; 11 | prefixIcon?: IconName; 12 | suffixIcon?: IconName; 13 | className?: string; 14 | style?: React.CSSProperties; 15 | }; 16 | 17 | const Tooltip = forwardRef( 18 | ({ label, prefixIcon, suffixIcon, className, style }, ref) => { 19 | return ( 20 | 39 | {prefixIcon && } 40 | 46 | {label} 47 | 48 | {suffixIcon && } 49 | 50 | ); 51 | } 52 | ); 53 | 54 | Tooltip.displayName = "Tooltip"; 55 | 56 | export { Tooltip }; 57 | -------------------------------------------------------------------------------- /src/once-ui/components/User.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { forwardRef } from "react"; 4 | import classNames from "classnames"; 5 | 6 | import { Flex, Text, Skeleton, Tag, TagProps, Avatar, AvatarProps } from "."; 7 | 8 | interface UserProps { 9 | name?: string; 10 | children?: React.ReactNode; 11 | subline?: React.ReactNode; 12 | tag?: string; 13 | tagProps?: TagProps; 14 | loading?: boolean; 15 | avatarProps?: AvatarProps; 16 | className?: string; 17 | } 18 | 19 | const User = forwardRef( 20 | ( 21 | { name, children, subline, tagProps = {}, loading = false, avatarProps = {}, className }, 22 | ref, 23 | ) => { 24 | const { src, value, empty, ...restAvatarProps } = avatarProps; 25 | const isEmpty = empty || (!src && !value); 26 | 27 | return ( 28 | 29 | 37 | {children} 38 | {name && ( 39 | 40 | {loading ? ( 41 | 42 | 43 | 44 | ) : ( 45 | 46 | 47 | {name} 48 | 49 | {tagProps.label && ( 50 | 51 | {tagProps.label} 52 | 53 | )} 54 | 55 | )} 56 | {loading ? ( 57 | 58 | 59 | 60 | ) : ( 61 | 62 | {subline} 63 | 64 | )} 65 | 66 | )} 67 | 68 | ); 69 | }, 70 | ); 71 | 72 | User.displayName = "User"; 73 | 74 | export { User }; 75 | export type { UserProps }; 76 | -------------------------------------------------------------------------------- /src/once-ui/components/UserMenu.module.scss: -------------------------------------------------------------------------------- 1 | .wrapper { 2 | border: 1px solid var(--static-transparent); 3 | transition: var(--transition-micro-medium); 4 | 5 | &:hover { 6 | background-color: var(--neutral-alpha-weak); 7 | border: 1px solid var(--neutral-alpha-medium); 8 | } 9 | 10 | &.selected:hover { 11 | background-color: var(--neutral-background-strong); 12 | border: 1px solid var(--neutral-border-strong); 13 | } 14 | } -------------------------------------------------------------------------------- /src/once-ui/components/UserMenu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import classNames from "classnames"; 5 | import { Flex, DropdownWrapper, User, UserProps } from "."; 6 | import styles from "./UserMenu.module.scss"; 7 | import { DropdownWrapperProps } from "./DropdownWrapper"; 8 | 9 | interface UserMenuProps 10 | extends UserProps, 11 | Pick { 12 | selected?: boolean; 13 | dropdown?: React.ReactNode; 14 | className?: string; 15 | style?: React.CSSProperties; 16 | } 17 | 18 | const UserMenu: React.FC = ({ 19 | selected = false, 20 | dropdown, 21 | minWidth, 22 | maxWidth, 23 | minHeight, 24 | className, 25 | style, 26 | ...userProps 27 | }) => { 28 | return ( 29 | 48 | 49 | 50 | } 51 | dropdown={<>{dropdown}>} 52 | /> 53 | ); 54 | }; 55 | 56 | UserMenu.displayName = "UserMenu"; 57 | export { UserMenu }; 58 | -------------------------------------------------------------------------------- /src/once-ui/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Accordion"; 2 | export * from "./AccordionGroup"; 3 | export * from "./Arrow"; 4 | export * from "./Avatar"; 5 | export * from "./AvatarGroup"; 6 | export * from "./Badge"; 7 | export * from "./Background"; 8 | export * from "./Button"; 9 | export * from "./Carousel"; 10 | export * from "./Card"; 11 | export * from "./Column"; 12 | export * from "./Checkbox"; 13 | export * from "./Chip"; 14 | export * from "./ColorInput"; 15 | export * from "./CompareImage"; 16 | export * from "./DateInput"; 17 | export * from "./DatePicker"; 18 | export * from "./DateRangeInput"; 19 | export * from "./DateRangePicker"; 20 | export * from "./Dialog"; 21 | export * from "./Dropdown"; 22 | export * from "./DropdownWrapper"; 23 | export * from "./Fade"; 24 | export * from "./Feedback"; 25 | export * from "./Flex"; 26 | export * from "./GlitchFx"; 27 | export * from "./Grid"; 28 | export * from "./Heading"; 29 | export * from "./HeadingLink"; 30 | export * from "./HeadingNav"; 31 | export * from "./HoloFx"; 32 | export * from "./Icon"; 33 | export * from "./IconButton"; 34 | export * from "./InlineCode"; 35 | export * from "./Input"; 36 | export * from "./InteractiveDetails"; 37 | export * from "./Kbar"; 38 | export * from "./Kbd"; 39 | export * from "./LetterFx"; 40 | export * from "./Line"; 41 | export * from "./Logo"; 42 | export * from "./LogoCloud"; 43 | export * from "./MegaMenu"; 44 | export * from "./NavIcon"; 45 | export * from "./NumberInput"; 46 | export * from "./Option"; 47 | export * from "./OTPInput"; 48 | export * from "./PasswordInput"; 49 | export * from "./RadioButton"; 50 | export * from "./RevealFx"; 51 | export * from "./Row"; 52 | export * from "./Scroller"; 53 | export * from "./SegmentedControl"; 54 | export * from "./Select"; 55 | export * from "./Skeleton"; 56 | export * from "./SmartImage"; 57 | export * from "./SmartLink"; 58 | export * from "./Spinner"; 59 | export * from "./StatusIndicator"; 60 | export * from "./StylePanel"; 61 | export * from "./StyleOverlay"; 62 | export * from "./Switch"; 63 | export * from "./Table"; 64 | export * from "./Tag"; 65 | export * from "./TagInput"; 66 | export * from "./Text"; 67 | export * from "./Textarea"; 68 | export * from "./TiltFx"; 69 | export * from "./Toast"; 70 | export * from "./Toaster"; 71 | export * from "./ToastProvider"; 72 | export * from "./ToggleButton"; 73 | export * from "./Tooltip"; 74 | export * from "./ThemeSwitcher"; 75 | export * from "./ThemeProvider"; 76 | export * from "./User"; 77 | export * from "./UserMenu"; -------------------------------------------------------------------------------- /src/once-ui/hooks/generateHeadingLinks.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | export const useHeadingLinks = () => { 4 | const [headings, setHeadings] = useState<{ id: string; text: string; level: number }[]>([]); 5 | 6 | useEffect(() => { 7 | const elements = Array.from(document.querySelectorAll("h2, h3, h4, h5, h6")) 8 | .filter((elem) => !elem.hasAttribute("data-exclude-nav")) 9 | .map((elem, index) => ({ 10 | id: elem.id || `heading-${index}`, 11 | text: elem.textContent || "", 12 | level: Number(elem.tagName.substring(1)), 13 | })); 14 | setHeadings(elements); 15 | }, []); 16 | 17 | return headings; 18 | }; 19 | -------------------------------------------------------------------------------- /src/once-ui/hooks/useDebounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | function useDebounce(value: any, delay: number) { 4 | const [debouncedValue, setDebouncedValue] = useState(value); 5 | 6 | useEffect(() => { 7 | const handler = setTimeout(() => { 8 | setDebouncedValue(value); 9 | }, delay); 10 | 11 | return () => { 12 | clearTimeout(handler); 13 | }; 14 | }, [value, delay]); 15 | 16 | return debouncedValue; 17 | } 18 | 19 | export default useDebounce; 20 | -------------------------------------------------------------------------------- /src/once-ui/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useCallback, useEffect, useState } from 'react'; 4 | import { style } from '@/app/resources'; 5 | 6 | export function useTheme() { 7 | const [theme, setTheme] = useState(style.theme); 8 | 9 | // Load theme from localStorage on mount 10 | useEffect(() => { 11 | const savedTheme = localStorage.getItem('theme'); 12 | if (savedTheme) { 13 | setTheme(savedTheme); 14 | document.documentElement.setAttribute('data-theme', savedTheme); 15 | } 16 | }, []); 17 | 18 | const toggleTheme = useCallback(() => { 19 | const newTheme = theme === 'dark' ? 'light' : 'dark'; 20 | setTheme(newTheme); 21 | document.documentElement.setAttribute('data-theme', newTheme); 22 | localStorage.setItem('theme', newTheme); 23 | }, [theme]); 24 | 25 | return { 26 | theme, 27 | toggleTheme 28 | }; 29 | } 30 | -------------------------------------------------------------------------------- /src/once-ui/icons.ts: -------------------------------------------------------------------------------- 1 | import { IconType } from "react-icons"; 2 | 3 | import { 4 | HiChevronUp, 5 | HiChevronDown, 6 | HiChevronRight, 7 | HiChevronLeft, 8 | HiArrowUpRight, 9 | HiOutlineArrowPath, 10 | HiCheck, 11 | HiMiniQuestionMarkCircle, 12 | HiMiniXMark, 13 | HiOutlineLink, 14 | HiExclamationTriangle, 15 | HiInformationCircle, 16 | HiExclamationCircle, 17 | HiCheckCircle, 18 | HiMiniGlobeAsiaAustralia, 19 | HiArrowTopRightOnSquare, 20 | HiEnvelope, 21 | HiCalendarDays, 22 | HiClipboard, 23 | HiArrowRight, 24 | HiOutlineEye, 25 | HiOutlineEyeSlash, 26 | HiMoon, 27 | HiSun, 28 | HiOutlineDocument, 29 | } from "react-icons/hi2"; 30 | 31 | import { 32 | PiHouseDuotone, 33 | PiUserCircleDuotone, 34 | PiGridFourDuotone, 35 | PiBookBookmarkDuotone, 36 | PiImageDuotone, 37 | } from "react-icons/pi"; 38 | 39 | import { FaDiscord, FaGithub, FaLinkedin, FaX } from "react-icons/fa6"; 40 | 41 | export const iconLibrary: Record = { 42 | chevronUp: HiChevronUp, 43 | chevronDown: HiChevronDown, 44 | chevronRight: HiChevronRight, 45 | chevronLeft: HiChevronLeft, 46 | refresh: HiOutlineArrowPath, 47 | arrowUpRight: HiArrowUpRight, 48 | check: HiCheck, 49 | arrowRight: HiArrowRight, 50 | helpCircle: HiMiniQuestionMarkCircle, 51 | infoCircle: HiInformationCircle, 52 | warningTriangle: HiExclamationTriangle, 53 | errorCircle: HiExclamationCircle, 54 | checkCircle: HiCheckCircle, 55 | email: HiEnvelope, 56 | globe: HiMiniGlobeAsiaAustralia, 57 | person: PiUserCircleDuotone, 58 | grid: PiGridFourDuotone, 59 | book: PiBookBookmarkDuotone, 60 | close: HiMiniXMark, 61 | openLink: HiOutlineLink, 62 | calendar: HiCalendarDays, 63 | home: PiHouseDuotone, 64 | gallery: PiImageDuotone, 65 | discord: FaDiscord, 66 | eye: HiOutlineEye, 67 | eyeOff: HiOutlineEyeSlash, 68 | github: FaGithub, 69 | linkedin: FaLinkedin, 70 | x: FaX, 71 | clipboard: HiClipboard, 72 | arrowUpRightFromSquare: HiArrowTopRightOnSquare, 73 | moon: HiMoon, 74 | sun: HiSun, 75 | document: HiOutlineDocument, 76 | }; 77 | 78 | export type IconLibrary = typeof iconLibrary; 79 | export type IconName = keyof IconLibrary; -------------------------------------------------------------------------------- /src/once-ui/modules/code/CodeBlock.module.scss: -------------------------------------------------------------------------------- 1 | .pre { 2 | display: flex; 3 | isolation: isolate; 4 | font-family: var(--font-family-code); 5 | color: var(--neutral-on-background-strong); 6 | font-size: var(--font-size-body-s); 7 | white-space: pre; 8 | width: 100%; 9 | tab-size: 2; 10 | margin: 0; 11 | height: fit-content; 12 | width: 100%; 13 | min-height: var(--static-space-32); 14 | tab-size: 2; 15 | line-height: 1.75; 16 | } 17 | 18 | .padding { 19 | padding: var(--static-space-8); 20 | } 21 | 22 | .lineNumberPadding { 23 | padding: var(--static-space-8) var(--static-space-8) var(--static-space-8) var(--static-space-40); 24 | } 25 | 26 | .code { 27 | flex: 1; 28 | width: 100%; 29 | margin: auto; 30 | padding: 0 var(--static-space-12); 31 | font-family: inherit; 32 | } 33 | 34 | .fullscreen { 35 | position: fixed; 36 | left: var(--static-space-8); 37 | top: var(--static-space-8); 38 | right: var(--static-space-8); 39 | bottom: var(--static-space-8); 40 | width: calc(100% - var(--static-space-16)) !important; 41 | height: calc(100% - var(--static-space-16)) !important; 42 | z-index: 9; 43 | } -------------------------------------------------------------------------------- /src/once-ui/modules/code/CodeHighlight.css: -------------------------------------------------------------------------------- 1 | .token.comment, 2 | .token.prolog, 3 | .token.doctype, 4 | .token.cdata { 5 | color: var(--code-moss); 6 | } 7 | 8 | .token.punctuation { 9 | color: var(--code-gray); 10 | } 11 | 12 | .token.property, 13 | .token.tag, 14 | .token.constant, 15 | .token.symbol, 16 | .token.deleted { 17 | color: var(--code-aqua); 18 | } 19 | 20 | .token.boolean, 21 | .token.number { 22 | color: var(--code-green); 23 | } 24 | 25 | .token.selector, 26 | .token.attr-name, 27 | .token.string, 28 | .token.char, 29 | .token.builtin, 30 | .token.inserted { 31 | color: var(--code-blue); 32 | } 33 | 34 | .token.operator, 35 | .token.entity, 36 | .token.url, 37 | .language-css .token.string, 38 | .style .token.string { 39 | color: var(--code-gray); 40 | } 41 | 42 | .token.atrule, 43 | .token.attr-value, 44 | .token.keyword { 45 | color: var(--code-violet); 46 | } 47 | 48 | .token.function, 49 | .token.class-name { 50 | color: var(--code-blue); 51 | } 52 | 53 | .token.regex, 54 | .token.important, 55 | .token.variable { 56 | color: var(--code-orange); 57 | } 58 | 59 | .token.important, 60 | .token.bold { 61 | font-weight: bold; 62 | } 63 | 64 | .token.italic { 65 | font-style: italic; 66 | } 67 | 68 | .token.entity { 69 | cursor: help; 70 | } 71 | 72 | @media print { 73 | .line-highlight { 74 | print-color-adjust: exact; 75 | color-adjust: exact; 76 | } 77 | } 78 | 79 | .line-highlight { 80 | position: absolute; 81 | background-color: var(--brand-alpha-weak); 82 | border-left: 2px solid var(--brand-alpha-strong); 83 | width: 100%; 84 | left: 0; 85 | margin-top: var(--static-space-8); 86 | z-index: 0; 87 | } 88 | 89 | .line-numbers .line-highlight:before, 90 | .line-numbers .line-highlight:after { 91 | content: none; 92 | } 93 | -------------------------------------------------------------------------------- /src/once-ui/modules/code/LineNumber.css: -------------------------------------------------------------------------------- 1 | .line-numbers { 2 | position: relative; 3 | counter-reset: linenumber; 4 | } 5 | 6 | .line-numbers > code { 7 | position: relative; 8 | white-space: inherit; 9 | } 10 | 11 | .line-numbers-rows { 12 | padding-right: var(--static-space-4); 13 | left: calc(-1 * var(--static-space-40)); 14 | position: absolute; 15 | pointer-events: none; 16 | top: 0; 17 | font-size: 100%; 18 | width: var(--static-space-48); 19 | user-select: none; 20 | } 21 | 22 | .line-numbers-rows > span { 23 | display: block; 24 | counter-increment: linenumber; 25 | } 26 | 27 | .line-numbers-rows > span:before { 28 | content: counter(linenumber); 29 | color: var(--neutral-on-background-weak); 30 | display: block; 31 | padding-right: var(--static-space-8); 32 | text-align: right; 33 | transform: scale(0.9); 34 | } 35 | -------------------------------------------------------------------------------- /src/once-ui/modules/code/prismjs.d.ts: -------------------------------------------------------------------------------- 1 | declare module "prismjs" { 2 | const Prism: { 3 | highlightAll: () => void; 4 | highlight: (code: string, grammar: any, language: string) => string; 5 | languages: { 6 | [language: string]: any; 7 | }; 8 | }; 9 | export default Prism; 10 | } 11 | -------------------------------------------------------------------------------- /src/once-ui/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { CodeBlock } from "./code/CodeBlock"; 2 | 3 | export { Meta } from "./seo/Meta"; 4 | export { Schema } from "./seo/Schema"; 5 | -------------------------------------------------------------------------------- /src/once-ui/modules/seo/Meta.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata as NextMetadata } from "next"; 2 | 3 | export interface MetaProps { 4 | title: string; 5 | description: string; 6 | baseURL: string; 7 | path?: string; 8 | type?: "website" | "article"; 9 | image?: string; 10 | publishedTime?: string; 11 | author?: { 12 | name: string; 13 | url?: string; 14 | }; 15 | } 16 | 17 | export function generateMetadata({ 18 | title, 19 | description, 20 | baseURL, 21 | path = "", 22 | type = "website", 23 | image, 24 | publishedTime, 25 | author, 26 | }: MetaProps): NextMetadata { 27 | const normalizedBaseURL = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL; 28 | const normalizedPath = path.startsWith("/") ? path : `/${path}`; 29 | 30 | const isFullUrl = (url: string) => /^https?:\/\//.test(url); 31 | 32 | const ogImage = image 33 | ? isFullUrl(image) 34 | ? image 35 | : `${normalizedBaseURL}${image.startsWith("/") ? image : `/${image}`}` 36 | : `${normalizedBaseURL}/og?title=${encodeURIComponent(title)}`; 37 | 38 | const url = `${normalizedBaseURL}${normalizedPath}`; 39 | 40 | return { 41 | title, 42 | description, 43 | openGraph: { 44 | title, 45 | description, 46 | type, 47 | ...(publishedTime && type === "article" ? { publishedTime } : {}), 48 | url, 49 | images: [ 50 | { 51 | url: ogImage, 52 | alt: title, 53 | }, 54 | ], 55 | }, 56 | twitter: { 57 | card: "summary_large_image", 58 | title, 59 | description, 60 | images: [ogImage], 61 | }, 62 | ...(author ? { authors: [{ name: author.name, url: author.url }] } : {}), 63 | }; 64 | } 65 | 66 | export const Meta = { 67 | generate: generateMetadata, 68 | }; 69 | 70 | export default Meta; -------------------------------------------------------------------------------- /src/once-ui/modules/seo/Schema.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Script from "next/script"; 3 | import { social } from "@/app/resources/content"; 4 | 5 | export interface SchemaProps { 6 | as: "website" | "article" | "blog" | "blogPosting" | "techArticle" | "webPage" | "organization"; 7 | title: string; 8 | description: string; 9 | baseURL: string; 10 | path: string; 11 | datePublished?: string; 12 | dateModified?: string; 13 | image?: string; 14 | author?: { 15 | name: string; 16 | url?: string; 17 | image?: string; 18 | }; 19 | } 20 | 21 | const schemaTypeMap = { 22 | website: "WebSite", 23 | article: "Article", 24 | blog: "Blog", 25 | blogPosting: "BlogPosting", 26 | techArticle: "TechArticle", 27 | webPage: "WebPage", 28 | organization: "Organization", 29 | }; 30 | 31 | export function Schema({ 32 | as, 33 | title, 34 | description, 35 | baseURL, 36 | path, 37 | datePublished, 38 | dateModified, 39 | image, 40 | author, 41 | }: SchemaProps) { 42 | const normalizedBaseURL = baseURL.endsWith("/") ? baseURL.slice(0, -1) : baseURL; 43 | const normalizedPath = path.startsWith("/") ? path : `/${path}`; 44 | 45 | const imageUrl = image 46 | ? `${normalizedBaseURL}${image.startsWith("/") ? image : `/${image}`}` 47 | : `${normalizedBaseURL}/og?title=${encodeURIComponent(title)}`; 48 | 49 | const url = `${normalizedBaseURL}${normalizedPath}`; 50 | 51 | const schemaType = schemaTypeMap[as]; 52 | 53 | const schema: Record = { 54 | "@context": "https://schema.org", 55 | "@type": schemaType, 56 | url, 57 | }; 58 | 59 | schema.sameAs = Object.values(social).filter(Boolean) 60 | 61 | if (as === "website") { 62 | schema.name = title; 63 | schema.description = description; 64 | schema.image = imageUrl; 65 | } else if (as === "organization") { 66 | schema.name = title; 67 | schema.description = description; 68 | schema.image = imageUrl; 69 | } else { 70 | schema.headline = title; 71 | schema.description = description; 72 | schema.image = imageUrl; 73 | 74 | if (datePublished) { 75 | schema.datePublished = datePublished; 76 | schema.dateModified = dateModified || datePublished; 77 | } 78 | } 79 | 80 | if (author) { 81 | schema.author = { 82 | "@type": "Person", 83 | name: author.name, 84 | ...(author.url && { url: author.url }), 85 | ...(author.image && { 86 | image: { 87 | "@type": "ImageObject", 88 | url: author.image, 89 | }, 90 | }), 91 | }; 92 | } 93 | 94 | return ( 95 | 102 | ); 103 | } 104 | 105 | export default Schema; -------------------------------------------------------------------------------- /src/once-ui/styles/breakpoints.scss: -------------------------------------------------------------------------------- 1 | @custom-media --s (max-width: 768px); 2 | @custom-media --m (max-width: 1024px); 3 | @custom-media --l (max-width: 1440px); -------------------------------------------------------------------------------- /src/once-ui/styles/flex.scss: -------------------------------------------------------------------------------- 1 | .justify-start { 2 | justify-content: flex-start; 3 | } 4 | 5 | .justify-center { 6 | justify-content: center; 7 | } 8 | 9 | .justify-end { 10 | justify-content: flex-end; 11 | } 12 | 13 | .justify-space-between { 14 | justify-content: space-between; 15 | } 16 | 17 | .justify-space-around { 18 | justify-content: space-around; 19 | } 20 | 21 | .justify-space-evenly { 22 | justify-content: space-evenly; 23 | } 24 | 25 | .justify-stretch { 26 | justify-content: stretch; 27 | } 28 | 29 | .align-start { 30 | align-items: flex-start; 31 | } 32 | 33 | .align-center { 34 | align-items: center; 35 | } 36 | 37 | .align-end { 38 | align-items: flex-end; 39 | } 40 | 41 | .align-space-between { 42 | align-items: space-between; 43 | } 44 | 45 | .align-space-around { 46 | align-items: space-around; 47 | } 48 | 49 | .align-space-evenly { 50 | align-items: space-evenly; 51 | } 52 | 53 | .align-stretch { 54 | align-items: stretch; 55 | } 56 | 57 | .center { 58 | align-items: center; 59 | justify-content: center; 60 | } 61 | 62 | .flex-wrap { 63 | flex-wrap: wrap; 64 | } 65 | 66 | .flex-nowrap { 67 | flex-wrap: nowrap; 68 | } 69 | 70 | .flex-wrap-reverse { 71 | flex-wrap: wrap-reverse; 72 | } 73 | 74 | .flex-0 { 75 | flex: 0; 76 | } 77 | 78 | .flex-1 { 79 | flex: 1; 80 | } 81 | 82 | .flex-2 { 83 | flex: 2; 84 | } 85 | 86 | .flex-3 { 87 | flex: 3; 88 | } 89 | 90 | .flex-4 { 91 | flex: 4; 92 | } 93 | 94 | .flex-5 { 95 | flex: 5; 96 | } 97 | 98 | .flex-6 { 99 | flex: 6; 100 | } 101 | 102 | .flex-7 { 103 | flex: 7; 104 | } 105 | 106 | .flex-8 { 107 | flex: 8; 108 | } 109 | 110 | .flex-9 { 111 | flex: 9; 112 | } 113 | 114 | .flex-10 { 115 | flex: 10; 116 | } 117 | 118 | .flex-11 { 119 | flex: 11; 120 | } 121 | 122 | .flex-12 { 123 | flex: 12; 124 | } -------------------------------------------------------------------------------- /src/once-ui/styles/global.scss: -------------------------------------------------------------------------------- 1 | /* BOX-SIZING */ 2 | * { 3 | box-sizing: border-box; 4 | scroll-behavior: smooth; 5 | } 6 | 7 | h1, h2, h3, h4, h5, h6 { 8 | scroll-margin-top: var(--static-space-80); 9 | } 10 | 11 | ul, ol { 12 | margin: 0; 13 | width: 100%; 14 | } 15 | 16 | ul { 17 | padding: 0 0 0 var(--static-space-20); 18 | 19 | li { 20 | padding-left: var(--static-space-8); 21 | } 22 | } 23 | 24 | ol { 25 | padding: 0 0 0 var(--static-space-20); 26 | 27 | li { 28 | padding-left: var(--static-space-16); 29 | } 30 | } 31 | 32 | li { 33 | padding: 0; 34 | line-height: 175%; 35 | font-size: inherit; 36 | color: inherit; 37 | margin-bottom: var(--static-space-12); 38 | 39 | &::marker { 40 | color: var(--brand-on-background-weak); 41 | } 42 | } 43 | 44 | blockquote { 45 | display: flex; 46 | margin: var(--static-space-16) 0; 47 | border-radius: var(--radius-m); 48 | background: var(--brand-alpha-weak); 49 | overflow: hidden; 50 | text-wrap: balance; 51 | backdrop-filter: blur(var(--static-space-2)); 52 | border: 1px dashed var(--brand-alpha-medium); 53 | max-width: var(--responsive-width-xs); 54 | padding: var(--static-space-12) var(--static-space-24) var(--static-space-8) var(--static-space-24); 55 | width: 100%; 56 | 57 | span { 58 | margin: 0 !important; 59 | padding: var(--static-space-16) var(--static-space-24); 60 | color: var(--brand-on-background-medium) !important; 61 | font-family: var(--font-family-code) !important; 62 | } 63 | } 64 | 65 | img { 66 | user-select: none; 67 | } 68 | 69 | /* SELECTION */ 70 | ::selection { 71 | background: var(--neutral-on-background-medium); 72 | color: var(--neutral-background-strong); 73 | } 74 | 75 | /* LINK */ 76 | a:not(.button) { 77 | color: var(--brand-on-background-medium); 78 | text-decoration: none; 79 | transition: var(--transition-micro-medium); 80 | text-decoration-thickness: 1px; 81 | text-underline-offset: 0.25em; 82 | text-decoration-color: var(--neutral-border-strong) !important; 83 | 84 | &:hover { 85 | text-decoration: none; 86 | color: var(--brand-on-background-strong); 87 | } 88 | } 89 | 90 | /* SCROLLBAR */ 91 | ::-webkit-scrollbar { 92 | background: var(--static-transparent); 93 | width: var(--static-space-8); 94 | height: var(--static-space-8); 95 | } 96 | 97 | ::-webkit-scrollbar-track { 98 | background: var(--static-transparent); 99 | } 100 | 101 | ::-webkit-scrollbar-thumb { 102 | background: var(--neutral-alpha-medium); 103 | transition: var(--transition-micro-medium); 104 | 105 | &:hover { 106 | background: var(--neutral-alpha-strong); 107 | } 108 | } 109 | 110 | ::-webkit-scrollbar-corner { 111 | background-color: var(--static-transparent); 112 | } -------------------------------------------------------------------------------- /src/once-ui/styles/index.scss: -------------------------------------------------------------------------------- 1 | @use "@/once-ui/styles/spacing.scss"; 2 | @use "@/once-ui/styles/border.scss"; 3 | @use "@/once-ui/styles/color.scss"; 4 | @use "@/once-ui/styles/background.scss"; 5 | @use "@/once-ui/styles/typography.scss"; 6 | @use "@/once-ui/styles/global.scss"; 7 | @use "@/once-ui/styles/layout.scss"; 8 | @use "@/once-ui/styles/shadow.scss"; 9 | @use "@/once-ui/styles/size.scss"; 10 | @use "@/once-ui/styles/display.scss"; 11 | @use "@/once-ui/styles/position.scss"; 12 | @use "@/once-ui/styles/grid.scss"; 13 | @use "@/once-ui/styles/flex.scss"; 14 | @use "@/once-ui/styles/utilities.scss"; -------------------------------------------------------------------------------- /src/once-ui/styles/layout.scss: -------------------------------------------------------------------------------- 1 | .display-flex { 2 | display: flex; 3 | } 4 | 5 | .display-grid { 6 | display: grid; 7 | } 8 | 9 | .display-inline-flex { 10 | display: inline-flex; 11 | } 12 | 13 | .flex-column { 14 | flex-direction: column; 15 | } 16 | 17 | .flex-row { 18 | flex-direction: row; 19 | } 20 | 21 | .flex-column-reverse { 22 | flex-direction: column-reverse; 23 | } 24 | 25 | .flex-row-reverse { 26 | flex-direction: row-reverse; 27 | } 28 | 29 | .l-flex-show { 30 | display: none !important; 31 | } 32 | 33 | .m-flex-show { 34 | display: none !important; 35 | } 36 | 37 | .s-flex-show { 38 | display: none !important; 39 | } 40 | 41 | @media (--l) { 42 | .l-flex-hide { 43 | display: none !important; 44 | } 45 | 46 | .l-flex-show { 47 | display: flex !important; 48 | } 49 | 50 | .l-flex-column { 51 | flex-direction: column !important; 52 | } 53 | 54 | .l-flex-row { 55 | flex-direction: row !important; 56 | } 57 | 58 | .l-flex-column-reverse { 59 | flex-direction: column-reverse !important; 60 | } 61 | 62 | .l-flex-row-reverse { 63 | flex-direction: row-reverse !important; 64 | } 65 | } 66 | 67 | @media (--m) { 68 | .m-flex-hide { 69 | display: none !important; 70 | } 71 | 72 | .m-flex-show { 73 | display: flex !important; 74 | } 75 | 76 | .m-flex-column { 77 | flex-direction: column !important; 78 | } 79 | 80 | .m-flex-row { 81 | flex-direction: row !important; 82 | } 83 | 84 | .m-flex-column-reverse { 85 | flex-direction: column-reverse !important; 86 | } 87 | 88 | .m-flex-row-reverse { 89 | flex-direction: row-reverse !important; 90 | } 91 | } 92 | 93 | @media (--s) { 94 | .s-flex-hide { 95 | display: none !important; 96 | } 97 | 98 | .s-flex-show { 99 | display: flex !important; 100 | } 101 | 102 | .s-flex-column { 103 | flex-direction: column !important; 104 | } 105 | 106 | .s-flex-row { 107 | flex-direction: row !important; 108 | } 109 | 110 | .s-flex-column-reverse { 111 | flex-direction: column-reverse !important; 112 | } 113 | 114 | .s-flex-row-reverse { 115 | flex-direction: row-reverse !important; 116 | } 117 | } -------------------------------------------------------------------------------- /src/once-ui/styles/shadow.scss: -------------------------------------------------------------------------------- 1 | .shadow-xs { 2 | box-shadow: var(--shadow-xs); 3 | } 4 | 5 | .shadow-s { 6 | box-shadow: var(--shadow-s); 7 | } 8 | 9 | .shadow-m { 10 | box-shadow: var(--shadow-m); 11 | } 12 | 13 | .shadow-l { 14 | box-shadow: var(--shadow-l); 15 | } 16 | 17 | .shadow-xl { 18 | box-shadow: var(--shadow-xl); 19 | } -------------------------------------------------------------------------------- /src/once-ui/styles/size.scss: -------------------------------------------------------------------------------- 1 | .fill-width { 2 | width: 100%; 3 | } 4 | 5 | .fill-height { 6 | height: 100%; 7 | } 8 | 9 | .fill { 10 | width: 100%; 11 | height: 100%; 12 | } 13 | 14 | .fit-width { 15 | width: fit-content; 16 | } 17 | 18 | .fit-height { 19 | height: fit-content; 20 | } 21 | 22 | .fit { 23 | width: fit-content; 24 | height: fit-content; 25 | } 26 | 27 | .min-width-0 { 28 | min-width: 0; 29 | } 30 | 31 | .min-height-0 { 32 | min-height: 0; 33 | } -------------------------------------------------------------------------------- /src/once-ui/styles/utilities.scss: -------------------------------------------------------------------------------- 1 | .text-decoration-none { 2 | text-decoration: none; 3 | } 4 | 5 | .reset-button-styles { 6 | background: none; 7 | padding: 0; 8 | outline: 0; 9 | border: 0; 10 | } 11 | 12 | .focus-ring { 13 | &:focus-visible { 14 | outline: var(--static-space-2) solid var(--brand-solid-strong); 15 | outline-offset: 2px; 16 | } 17 | } -------------------------------------------------------------------------------- /src/once-ui/tokens/border.scss: -------------------------------------------------------------------------------- 1 | [data-border="playful"] { 2 | --radius-xs: 0.25rem; 3 | --radius-xs-nest-4: 0.375rem; 4 | --radius-xs-nest-8: 0.5rem; 5 | 6 | --radius-s: 0.5rem; 7 | --radius-s-nest-4: 0.75rem; 8 | --radius-s-nest-8: 1rem; 9 | 10 | --radius-m: 0.75rem; 11 | --radius-m-nest-4: 1rem; 12 | --radius-m-nest-8: 1.25rem; 13 | 14 | --radius-l: 1rem; 15 | --radius-l-nest-4: 1.25rem; 16 | --radius-l-nest-8: 1.75rem; 17 | 18 | --radius-xl: 1.25rem; 19 | --radius-xl-nest-4: 1.5rem; 20 | --radius-xl-nest-8: 1.875rem; 21 | 22 | --radius-full: 999rem; 23 | } 24 | 25 | [data-border="conservative"] { 26 | --radius-xs: 0.125rem; 27 | --radius-xs-nest-4: 0.25rem; 28 | --radius-xs-nest-8: 0.375rem; 29 | 30 | --radius-s: 0.25rem; 31 | --radius-s-nest-4: 0.375rem; 32 | --radius-s-nest-8: 0.5rem; 33 | 34 | --radius-m: 0.375rem; 35 | --radius-m-nest-4: 0.5rem; 36 | --radius-m-nest-8: 0.625rem; 37 | 38 | --radius-l: 0.625rem; 39 | --radius-l-nest-4: 0.875rem; 40 | --radius-l-nest-8: 1.125rem; 41 | 42 | --radius-xl: 0.75rem; 43 | --radius-xl-nest-4: 1rem; 44 | --radius-xl-nest-8: 1.25rem; 45 | 46 | --radius-full: 999rem; 47 | } 48 | 49 | [data-border="rounded"] { 50 | --radius-xs: 1rem; 51 | --radius-xs-nest-4: 1.25rem; 52 | --radius-xs-nest-8: 1.5rem; 53 | 54 | --radius-s: 1.25rem; 55 | --radius-s-nest-4: 1.5rem; 56 | --radius-s-nest-8: 2rem; 57 | 58 | --radius-m: 1.25rem; 59 | --radius-m-nest-4: 1.5rem; 60 | --radius-m-nest-8: 1.875rem; 61 | 62 | --radius-l: 1.75rem; 63 | --radius-l-nest-4: 2rem; 64 | --radius-l-nest-8: 2.5rem; 65 | 66 | --radius-xl: 2rem; 67 | --radius-xl-nest-4: 2.25rem; 68 | --radius-xl-nest-8: 2.75rem; 69 | 70 | --radius-full: 999rem; 71 | } -------------------------------------------------------------------------------- /src/once-ui/tokens/index.scss: -------------------------------------------------------------------------------- 1 | @use "@/once-ui/tokens/scheme.scss"; 2 | @use "@/once-ui/tokens/function.scss"; 3 | @use "@/once-ui/tokens/layout.scss"; 4 | @use "@/once-ui/tokens/border.scss"; 5 | @use "@/once-ui/tokens/shadow.scss"; 6 | @use "@/once-ui/tokens/typography.scss"; 7 | @use "@/once-ui/tokens/theme.scss"; -------------------------------------------------------------------------------- /src/once-ui/tokens/shadow.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --shadow-xs: 0px 0px 1px rgba(0, 0, 0, 0.12), 0px 1px 2px rgba(0, 0, 0, 0.08), 0px 2px 4px rgba(0, 0, 0, 0.08); 3 | --shadow-s: 0px 0px 2px rgba(0, 0, 0, 0.12), 0px 1px 4px rgba(0, 0, 0, 0.08), 0px 4px 8px rgba(0, 0, 0, 0.08); 4 | --shadow-m: 0px 0px 2px rgba(0, 0, 0, 0.12), 0px 2px 4px rgba(0, 0, 0, 0.08), 0px 8px 8px rgba(0, 0, 0, 0.08); 5 | --shadow-l: 0px 2px 4px rgba(0, 0, 0, 0.12), 0px 8px 12px rgba(0, 0, 0, 0.08), 0px 8px 16px rgba(0, 0, 0, 0.08); 6 | --shadow-xl: 0px 4px 4px rgba(0, 0, 0, 0.12), 0px 8px 12px rgba(0, 0, 0, 0.08), 0px 24px 24px rgba(0, 0, 0, 0.08); 7 | } -------------------------------------------------------------------------------- /src/once-ui/types.ts: -------------------------------------------------------------------------------- 1 | export type StaticSpacingToken = 2 | | "0" 3 | | "1" 4 | | "2" 5 | | "4" 6 | | "8" 7 | | "12" 8 | | "16" 9 | | "20" 10 | | "24" 11 | | "32" 12 | | "40" 13 | | "48" 14 | | "56" 15 | | "64" 16 | | "80" 17 | | "104" 18 | | "128" 19 | | "160"; 20 | 21 | export type Schemes = 22 | | "blue" 23 | | "indigo" 24 | | "violet" 25 | | "magenta" 26 | | "pink" 27 | | "red" 28 | | "orange" 29 | | "yellow" 30 | | "moss" 31 | | "green" 32 | | "emerald" 33 | | "aqua" 34 | | "cyan"; 35 | 36 | export type TShirtSizes = "xs" | "s" | "m" | "l" | "xl"; 37 | 38 | export type ResponsiveSpacingToken = TShirtSizes; 39 | 40 | export type ShadowSize = TShirtSizes; 41 | 42 | export type SpacingToken = StaticSpacingToken | ResponsiveSpacingToken; 43 | 44 | export type opacity = 0 | 10 | 20 | 30 | 40 | 50 | 60 | 70 | 80 | 90 | 100; 45 | 46 | export type ColorScheme = 47 | | "neutral" 48 | | "brand" 49 | | "accent" 50 | | "info" 51 | | "danger" 52 | | "warning" 53 | | "success"; 54 | 55 | export type ColorCategory = "on-solid" | "on-background"; 56 | 57 | export type ColorWeight = "weak" | "medium" | "strong"; 58 | 59 | export type RadiusSize = TShirtSizes | "full"; 60 | 61 | export type RadiusNest = "4" | "8"; 62 | 63 | export type TextType = "body" | "heading" | "display" | "label" | "code"; 64 | 65 | export type TextWeight = "default" | "strong"; 66 | 67 | export type TextSize = TShirtSizes; 68 | 69 | export type TextVariant = `${TextType}-${TextWeight}-${TextSize}`; 70 | 71 | export type gridColumns = 72 | | "1" 73 | | "2" 74 | | "3" 75 | | "4" 76 | | "5" 77 | | "6" 78 | | "7" 79 | | "8" 80 | | "9" 81 | | "10" 82 | | "11" 83 | | "12" 84 | | 1 85 | | 2 86 | | 3 87 | | 4 88 | | 5 89 | | 6 90 | | 7 91 | | 8 92 | | 9 93 | | 10 94 | | 11 95 | | 12; 96 | 97 | export type flex = 98 | | "0" 99 | | "1" 100 | | "2" 101 | | "3" 102 | | "4" 103 | | "5" 104 | | "6" 105 | | "7" 106 | | "8" 107 | | "9" 108 | | "10" 109 | | "11" 110 | | "12" 111 | | 0 112 | | 1 113 | | 2 114 | | 3 115 | | 4 116 | | 5 117 | | 6 118 | | 7 119 | | 8 120 | | 9 121 | | 10 122 | | 11 123 | | 12; 124 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "typeRoots": ["./types", "./node_modules/@types"], 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | }, 24 | "target": "ES2017" 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------