feat: initial commit
This commit is contained in:
commit
fc8fb230e0
11
.cursor/mcp.json
Normal file
11
.cursor/mcp.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"shadcn": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "shadcn@canary", "registry:mcp"],
|
||||||
|
"env": {
|
||||||
|
"REGISTRY_URL": "https://animate-ui.com/r/registry.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
.eslintrc.js
Normal file
10
.eslintrc.js
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// This configuration only applies to the package manager root.
|
||||||
|
/** @type {import("eslint").Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
ignorePatterns: ['apps/**', 'packages/**'],
|
||||||
|
extends: ['@workspace/eslint-config/library.js'],
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
parserOptions: {
|
||||||
|
project: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage
|
||||||
|
|
||||||
|
# Turbo
|
||||||
|
.turbo
|
||||||
|
|
||||||
|
# Vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# Build Outputs
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
|
||||||
|
|
||||||
|
# Debug
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
1
.husky/commit-msg
Executable file
1
.husky/commit-msg
Executable file
@ -0,0 +1 @@
|
|||||||
|
pnpm exec commitlint --edit $1
|
||||||
1
.husky/pre-push
Normal file
1
.husky/pre-push
Normal file
@ -0,0 +1 @@
|
|||||||
|
pnpm lint && pnpm build
|
||||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "all",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
||||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
}
|
||||||
348
CONTRIBUTING.md
Normal file
348
CONTRIBUTING.md
Normal file
@ -0,0 +1,348 @@
|
|||||||
|
# Contributing to Animate UI
|
||||||
|
|
||||||
|
Thank you for your interest in **contributing to Animate UI**! Your support is highly appreciated, and we look forward to your contributions. This guide will help you understand the project structure and provide detailed instructions for adding a new component or effect to Animate UI.
|
||||||
|
|
||||||
|
**Note:** You only need to modify a few files to add a new component, and it should take you around 10 minutes to complete.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Fork and Clone the Repository
|
||||||
|
|
||||||
|
#### 1. Fork the Repository
|
||||||
|
|
||||||
|
Click [here](https://github.com/animate-ui/animate-ui/fork) to fork the repository.
|
||||||
|
|
||||||
|
#### 2. Clone your Fork to Your Local Machine
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/<YOUR_USERNAME>/animate-ui.git
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Navigate to the Project Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd animate-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Create a New Branch for Your Changes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b my-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Install Dependencies
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm i
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Run the Project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Edit a Component
|
||||||
|
|
||||||
|
If you need to modify a component to correct or improve it, you must :
|
||||||
|
|
||||||
|
- add a screenshot (photo or video as appropriate) of before and after the modification
|
||||||
|
- clearly explain why you made the modification
|
||||||
|
|
||||||
|
### Edit the code
|
||||||
|
|
||||||
|
Edit the component in the `registry` folder. Don't forget to adapt the demo and documentation if necessary.
|
||||||
|
|
||||||
|
You shouldn't change your behavior completely unless there's a good reason.
|
||||||
|
|
||||||
|
### Build the Registry
|
||||||
|
|
||||||
|
To update the registry, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm registry:build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding a New Component
|
||||||
|
|
||||||
|
The addition of a new component must comply with certain rules:
|
||||||
|
|
||||||
|
- The component must be animated in some way (css, motion, ...).
|
||||||
|
- You can't just copy/paste component code from other libraries. You can be inspired by a component, but it must have added value. For example, I took Shadcn's components and animated them. So I didn't copy and paste the component, I added something to it.
|
||||||
|
- If you take inspiration from a component (CodePen, another library, etc.), remember to add the “Credits” section to your documentation. It's important to respect the work of other developers.
|
||||||
|
|
||||||
|
To submit your component, please include a demo video in the MR. Once the component has been submitted, it must be validated by @imskyleen.
|
||||||
|
|
||||||
|
To **add a new component to Animate UI**, you will need to update several files. Follow these steps:
|
||||||
|
|
||||||
|
### Create the Component
|
||||||
|
|
||||||
|
#### Basics
|
||||||
|
|
||||||
|
Create your main component in `apps/www/registry/[category]/my-component/index.tsx`.
|
||||||
|
|
||||||
|
```tsx title="my-component/index.tsx"
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
type MyComponentProps = {
|
||||||
|
myProps: string;
|
||||||
|
} & React.ComponentProps<'div'>;
|
||||||
|
|
||||||
|
function MyComponent({ myProps, ...props }) {
|
||||||
|
return <div {...props}>{/* Your component */}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { MyComponent, type MyComponentProps };
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Registry item
|
||||||
|
|
||||||
|
Create a `apps/www/registry/[category]/my-component/registry-item.json` file to export your component :
|
||||||
|
|
||||||
|
```json title="my-component/registry-item.json"
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||||
|
"name": "my-component",
|
||||||
|
"type": "registry:ui",
|
||||||
|
"title": "My Component",
|
||||||
|
"description": "My Component Description",
|
||||||
|
"dependencies": [...],
|
||||||
|
"devDependencies": [...],
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "registry/[category]/my-component/index.tsx",
|
||||||
|
"type": "registry:ui",
|
||||||
|
"target": "components/animate-ui/demo/[category]/my-component.tsx"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create a demo
|
||||||
|
|
||||||
|
#### Basics
|
||||||
|
|
||||||
|
Create your demo in `apps/www/registry/demo/[category]/my-component/index.tsx`.
|
||||||
|
|
||||||
|
```tsx title="my-component/index.tsx"
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
MyComponent,
|
||||||
|
type MyComponentProps,
|
||||||
|
} from '@/registry/[category]/my-component';
|
||||||
|
|
||||||
|
type MyComponentDemoProps = {
|
||||||
|
myProps: string;
|
||||||
|
} & MyComponentProps;
|
||||||
|
|
||||||
|
export const MyComponentDemo = ({ myProps }) => {
|
||||||
|
return <MyComponent myProps={myProps} />;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Registry item
|
||||||
|
|
||||||
|
```json title="my-component/registry-item.json"
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
|
||||||
|
"name": "my-component-demo",
|
||||||
|
"type": "registry:ui",
|
||||||
|
"title": "My Component Deo",
|
||||||
|
"description": "Demo showing my component",
|
||||||
|
"registryDependencies": ["https://animate-ui.com/r/my-component"],
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "registry/demo/[category]/my-component/index.tsx",
|
||||||
|
"type": "registry:ui",
|
||||||
|
"target": "components/[category]/demo/my-component.tsx"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Add a Tweakpane
|
||||||
|
|
||||||
|
You can add a Tweakpane allowing users to play with your demo props.
|
||||||
|
|
||||||
|
Your demo must accept the props you want in your tweakpane :
|
||||||
|
|
||||||
|
```tsx title="my-component-demo/index.tsx"
|
||||||
|
import { MyComponent } from '@/registry/[category]/my-component';
|
||||||
|
|
||||||
|
type MyComponentDemoProps = {
|
||||||
|
props1: number;
|
||||||
|
props2: number;
|
||||||
|
props3: string;
|
||||||
|
props4: string;
|
||||||
|
props5: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MyComponentDemo({
|
||||||
|
props1,
|
||||||
|
props2,
|
||||||
|
props3,
|
||||||
|
props4,
|
||||||
|
props5,
|
||||||
|
}: MyComponentDemoProps) {
|
||||||
|
return <MyComponent />;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You must then specify the demo props information in your demo's `registry-item.json` file:
|
||||||
|
|
||||||
|
```json title="my-component-demo/registry-item.json"
|
||||||
|
{
|
||||||
|
...
|
||||||
|
"meta": {
|
||||||
|
"demoProps": {
|
||||||
|
"MyComponent": {
|
||||||
|
"props1": { "value": 700, "min": 0, "max": 2000, "step": 100 },
|
||||||
|
"props2": { "value": 0 },
|
||||||
|
"props3": { "value": "foo" },
|
||||||
|
"props4": {
|
||||||
|
"value": "center",
|
||||||
|
"options": {
|
||||||
|
"start": "start",
|
||||||
|
"center": "center",
|
||||||
|
"end": "end"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"props5": { "value": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**You need to run `pnpm registry:build` to see the updated tweakpane in the demo.**
|
||||||
|
|
||||||
|
#### How to use `demoProps`
|
||||||
|
|
||||||
|
##### Number
|
||||||
|
|
||||||
|
Simple number input:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"myNumber": { "value": 10 }
|
||||||
|
```
|
||||||
|
|
||||||
|
Slider:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"myNumber": { "value": 10, "min": 0, "max": 100, "step": 1 }
|
||||||
|
```
|
||||||
|
|
||||||
|
Select:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"myNumber": {
|
||||||
|
"value": 10,
|
||||||
|
"options": {
|
||||||
|
"Big": 30,
|
||||||
|
"Medium": 20,
|
||||||
|
"Small": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### String
|
||||||
|
|
||||||
|
Simple text input:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"myString": { "value": "Hello World" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Select:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"myNumber": {
|
||||||
|
"value": "small",
|
||||||
|
"options": {
|
||||||
|
"Big": "big",
|
||||||
|
"Medium": "medium",
|
||||||
|
"Small": "small"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Boolean
|
||||||
|
|
||||||
|
```json
|
||||||
|
"myBoolean": { "value": true }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update the Documentation Sidebar
|
||||||
|
|
||||||
|
Add your component to the documentation sidebar by updating the file `content/docs/meta.json`.
|
||||||
|
|
||||||
|
```json title="meta.json"
|
||||||
|
{
|
||||||
|
"title": "Animate UI",
|
||||||
|
"root": true,
|
||||||
|
"pages": [
|
||||||
|
...,
|
||||||
|
"[category]/my-component"
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Create the Component Documentation
|
||||||
|
|
||||||
|
Create an MDX file to document your component in `content/docs/[category]/my-component.mdx`.
|
||||||
|
|
||||||
|
```mdx
|
||||||
|
---
|
||||||
|
title: My Component
|
||||||
|
description: Description for the new component
|
||||||
|
author:
|
||||||
|
name: your name
|
||||||
|
url: https://link-to-your-profile.com
|
||||||
|
new: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="my-component-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="my-component" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
[Basic usage of the component]
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
myProps: {
|
||||||
|
description: 'Description for my props',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [you](https://link-to-your-profile.com) for creating the component
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build the Registry
|
||||||
|
|
||||||
|
To update the registry, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm registry:build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ask for Help
|
||||||
|
|
||||||
|
If you need any assistance or have questions, please feel free to open a [GitHub issue](https://github.com/animate-ui/animate-ui/issues/new). We are here to help!
|
||||||
|
|
||||||
|
Thank you again for your contribution to Animate UI! We look forward to seeing your improvements and new components.
|
||||||
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Elliot Sutton
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
45
README.md
Normal file
45
README.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<div align="center">
|
||||||
|
<h1 style="position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0">Fortura Data Solutions</h1>
|
||||||
|
<p align="center">
|
||||||
|
A high-performance, cost-effective, and privacy-first alternative to cloud hosting for enterprises.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a href="https://github.com/fortura-data/landing-page/stargazers"><img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/fortura-data/landing-page?style=for-the-badge"></a>
|
||||||
|
<a href="https://twitter.com/forturadatasol"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/forturadatasol?style=for-the-badge&logo=x"></a>
|
||||||
|
<a href="https://github.com/fortura-data/landing-page/blob/main/LICENSE.md"><img alt="License" src="https://img.shields.io/badge/License-Proprietary-blue.svg?style=for-the-badge"></a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## About Fortura Data Solutions
|
||||||
|
|
||||||
|
Fortura Data Solutions provides a modern, enterprise-grade infrastructure stack that allows organizations to **own their hardware, slash their cloud bills, and maintain full control over their data**.
|
||||||
|
|
||||||
|
We built our platform for high-demand environments like VFX studios, where petabytes of data move in real-time on tight budgets. Now, we're bringing that same performance and cost-efficiency to every business serious about their bottom line and data sovereignty.
|
||||||
|
|
||||||
|
### Why Fortura?
|
||||||
|
|
||||||
|
* **Cost Reduction:** Stop paying rent. Own the hardware and see your operational costs drop significantly, often achieving break-even within 12 months.
|
||||||
|
* **Performance:** Get cloud-level performance with sub-10ms latencies on colocated hardware, tailored to your workloads.
|
||||||
|
* **Data Ownership & Privacy:** Your data stays on your hardware, governed by your rules. AI-native workflows keep your data private, with zero leakage to third parties.
|
||||||
|
* **Integrated Stack:** We help you build on the tools you already use, wiring them together and automating the boring parts.
|
||||||
|
* **Minimal Retraining:** Our "desire paths" approach means we embed with your team, shape the system to how you *actually* work, then automate.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This repository contains the code for the Fortura Data Solutions marketing website. It's built with Next.js, TypeScript, and Tailwind CSS.
|
||||||
|
|
||||||
|
For information on how to set up the development environment, run the site locally, or contribute, please see our [contributing guide](CONTRIBUTING.md).
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Detailed documentation for Fortura's services, migration guides, and operational procedures is available to clients on our internal portal.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Visit our [contributing guide](CONTRIBUTING.md) to learn how to contribute to the website's codebase.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This website's code is licensed under a proprietary license. See [LICENSE.md](LICENSE.md) for details.
|
||||||
28
apps/www/.gitignore
vendored
Normal file
28
apps/www/.gitignore
vendored
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# deps
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# generated content
|
||||||
|
.contentlayer
|
||||||
|
.content-collections
|
||||||
|
.source
|
||||||
|
|
||||||
|
# test & build
|
||||||
|
/coverage
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
/build
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# others
|
||||||
|
.env*.local
|
||||||
|
.vercel
|
||||||
|
next-env.d.ts
|
||||||
29
apps/www/README.md
Normal file
29
apps/www/README.md
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<div align="center">
|
||||||
|
<h1 style="position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0">Animate UI</h1>
|
||||||
|
<p align="center">
|
||||||
|
A fully animated, open-source component distribution built with React, TypeScript, Tailwind CSS, and Motion.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a href="https://github.com/animate-ui/animate-ui/stargazers"><img alt="GitHub Repo stars" src="https://img.shields.io/github/stars/animate-ui/animate-ui?style=for-the-badge"></a>
|
||||||
|
<a href="https://twitter.com/animate_ui"><img alt="Twitter Follow" src="https://img.shields.io/twitter/follow/animate_ui?style=for-the-badge&logo=x"></a>
|
||||||
|
<a href="https://github.com/animate-ui/animate-ui/blob/main/LICENSE.md"><img alt="License" src="https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge"></a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Beta
|
||||||
|
|
||||||
|
Animate UI is currently in **Beta phase**, so it's possible that the components contain **bugs** and that these will be **modified regularly**.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Visit [animate-ui.com](https://animate-ui.com/docs) to view the documentation.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Visit our [contributing guide](https://github.com/animate-ui/animate-ui/blob/main/CONTRIBUTING.md) to learn how to contribute.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under the [MIT license](https://github.com/animate-ui/animate-ui/blob/main/LICENSE.md).
|
||||||
1
apps/www/__registry__/.autogenerated
Normal file
1
apps/www/__registry__/.autogenerated
Normal file
@ -0,0 +1 @@
|
|||||||
|
// The content of this directory is autogenerated by the registry server.
|
||||||
0
apps/www/__registry__/.gitkeep
Normal file
0
apps/www/__registry__/.gitkeep
Normal file
13139
apps/www/__registry__/index.tsx
Normal file
13139
apps/www/__registry__/index.tsx
Normal file
File diff suppressed because one or more lines are too long
365
apps/www/app/(home)/page.tsx
Normal file
365
apps/www/app/(home)/page.tsx
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
// app/(home)/page.tsx
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
// Import layout components from our new shared `ui` library
|
||||||
|
import { Navbar } from '@workspace/ui/components/layout';
|
||||||
|
import { Footer } from '@workspace/ui/components/layout';
|
||||||
|
import { Container, Section, Grid } from '@workspace/ui/components/layout';
|
||||||
|
|
||||||
|
// Import the useTone hook to access the global tone setting
|
||||||
|
import { useTone } from '@workspace/ui/hooks/use-tone';
|
||||||
|
|
||||||
|
// Import Lucide icons for the value proposition section
|
||||||
|
import { DollarSignIcon, WrenchIcon, MapIcon, LockIcon, CloudOffIcon, ZapIcon } from 'lucide-react';
|
||||||
|
import { PlaceholderIcon } from '@workspace/ui/components/icons';
|
||||||
|
|
||||||
|
// Import UI components from our shared library
|
||||||
|
import { Button } from '@workspace/ui/components/ui';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
|
||||||
|
|
||||||
|
// Import new content components
|
||||||
|
import { MetricsKPI } from '@workspace/ui/components/content';
|
||||||
|
import { LogosMarquee } from '@workspace/ui/components/content';
|
||||||
|
import { Steps } from '@workspace/ui/components/content';
|
||||||
|
import { FeatureCard, CaseStudyCard } from '@workspace/ui/components/content/cards';
|
||||||
|
import { TrustSection } from '@workspace/ui/components/content';
|
||||||
|
import { FAQAccordion } from '@workspace/ui/components/interactive';
|
||||||
|
import { FinalCTASection } from '@workspace/ui/components/content';
|
||||||
|
|
||||||
|
export default function HomePage() {
|
||||||
|
const { tone } = useTone(); // Access the current tone
|
||||||
|
|
||||||
|
// Content definitions based on tone
|
||||||
|
const heroHeadline = tone === 'direct'
|
||||||
|
? "Own your infrastructure. Slash your bill."
|
||||||
|
: "Cut waste, keep performance.";
|
||||||
|
const heroSubcopyLine1 = tone === 'direct'
|
||||||
|
? "Your hosting provider is fucking you. Cloud rent bleeds $25k–$500k/mo."
|
||||||
|
: "Cloud rent is bleeding your budget dry.";
|
||||||
|
const heroSubcopyLine2 = tone === 'direct'
|
||||||
|
? "Own the hardware. Keep the performance. Kill the waste."
|
||||||
|
: "Own your stack. Colocate it. Keep the performance, ditch the waste.";
|
||||||
|
|
||||||
|
const valuePropositions = tone === 'direct' ? [
|
||||||
|
{
|
||||||
|
icon: <DollarSignIcon className="size-6" />,
|
||||||
|
title: "Stop getting scammed.",
|
||||||
|
description: "Cloud rent bleeds $25k–$500k/mo. Own the hardware. Keep the performance. Kill the waste."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <WrenchIcon className="size-6" />,
|
||||||
|
title: "Build on what works.",
|
||||||
|
description: "Keep your stack. We wire it together and automate the boring."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <MapIcon className="size-6" />,
|
||||||
|
title: "Pave desire paths.",
|
||||||
|
description: "We embed with your team, shape the system to how they actually work."
|
||||||
|
}
|
||||||
|
] : [
|
||||||
|
{
|
||||||
|
icon: <DollarSignIcon className="size-6" />,
|
||||||
|
title: "Cut waste, keep performance.",
|
||||||
|
description: "Move from renting to owning. Dramatically reduce monthly costs without sacrificing speed or reliability."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <WrenchIcon className="size-6" />,
|
||||||
|
title: "Integrate, don't replace.",
|
||||||
|
description: "We work with your existing tools and workflows. No disruptive rip-and-replace projects."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <MapIcon className="size-6" />,
|
||||||
|
title: "Build around real workflows.",
|
||||||
|
description: "We map your actual processes, then build automation that fits like a glove."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock data for LogosMarquee
|
||||||
|
const logos = Array(6).fill(null).map((_, i) => ({
|
||||||
|
name: `Client ${i + 1}`,
|
||||||
|
icon: <PlaceholderIcon className="size-8 md:size-10" />
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Mock data for Process Steps
|
||||||
|
const processSteps = [
|
||||||
|
{
|
||||||
|
title: "Discovery",
|
||||||
|
description: "We deep-dive into your current infrastructure, workloads, and pain points.",
|
||||||
|
youDo: "Share access to cloud billing, architecture docs, and team interviews.",
|
||||||
|
weDo: "Analyze costs, identify waste, and map technical dependencies."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Shadow & Map",
|
||||||
|
description: "We observe how your teams actually work and map desire paths.",
|
||||||
|
youDo: "Participate in workflow observations and workshops.",
|
||||||
|
weDo: "Document real workflows, identify automation opportunities, and draft the future state."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Architect",
|
||||||
|
description: "We design a tailored, owned infrastructure solution.",
|
||||||
|
youDo: "Review and approve the proposed architecture and migration plan.",
|
||||||
|
weDo: "Specify hardware, select software stack, and design for performance and cost."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Pilot",
|
||||||
|
description: "We build and test a critical component in the new environment.",
|
||||||
|
youDo: "Provide feedback on performance, usability, and integration.",
|
||||||
|
weDo: "Deploy, test, and iterate on the pilot component with your team."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Migrate",
|
||||||
|
description: "We execute the planned migration with zero downtime.",
|
||||||
|
youDo: "Validate functionality and performance post-migration.",
|
||||||
|
weDo: "Cutover workloads, decommission old systems, and optimize the new stack."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Operate",
|
||||||
|
description: "We manage the infrastructure, you focus on your core business.",
|
||||||
|
youDo: "Use the system and provide ongoing feedback for improvements.",
|
||||||
|
weDo: "Monitor, maintain, upgrade, and support the infrastructure 24/7."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock data for Case Studies
|
||||||
|
const caseStudies = [
|
||||||
|
{
|
||||||
|
title: "VFX Studio Migration",
|
||||||
|
description: "Migrated a major VFX studio from cloud rendering to a colocated HPC cluster, reducing costs by 65%.",
|
||||||
|
kpi1: { value: "65%", label: "Cost ↓" },
|
||||||
|
kpi2: { value: "2x", label: "Performance ↑" },
|
||||||
|
ctaText: "See build",
|
||||||
|
ctaHref: "/case-studies/vfx-studio"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Legal Document Management",
|
||||||
|
description: "Consolidated scattered legal tech stack into a secure, private Nextcloud instance with AI search.",
|
||||||
|
kpi1: { value: "$120k/mo", label: "Saved" },
|
||||||
|
kpi2: { value: "0", label: "Egress Fees" },
|
||||||
|
ctaText: "See build",
|
||||||
|
ctaHref: "/case-studies/legal-firm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Research Data Pipeline",
|
||||||
|
description: "Built a local ML training environment for a research institute, eliminating data transfer bottlenecks.",
|
||||||
|
kpi1: { value: "90%", label: "Time-to-Train ↓" },
|
||||||
|
kpi2: { value: "Break-even: 8mo", label: "" },
|
||||||
|
ctaText: "See build",
|
||||||
|
ctaHref: "/case-studies/research-institute"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock data for Trust Section
|
||||||
|
const trustItems = [
|
||||||
|
{
|
||||||
|
icon: <LockIcon className="size-5" />,
|
||||||
|
text: "Your data. Your hardware. Your rules."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <CloudOffIcon className="size-5" />,
|
||||||
|
text: "Colocated. Monitored. Upgradable without downtime."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <ZapIcon className="size-5" />,
|
||||||
|
text: "AI-native, privacy-first. No third-party model training."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock data for FAQ
|
||||||
|
// (This is just for homepage preview; full FAQ is on /faq page)
|
||||||
|
const faqItems = [
|
||||||
|
{
|
||||||
|
question: "What uptime can I expect?",
|
||||||
|
answer: "We design for 99.9%+ uptime through redundant hardware, proactive monitoring, and colocation in Tier III+ facilities."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "How is support handled?",
|
||||||
|
answer: "You get direct access to our engineering team via Slack, email, or phone. We provide 24/7 monitoring and alerting."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<Section className="flex flex-col items-center text-center py-20 md:py-32">
|
||||||
|
<Container maxWidth="3xl">
|
||||||
|
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight">
|
||||||
|
{heroHeadline}
|
||||||
|
</h1>
|
||||||
|
<p className="mt-6 text-lg md:text-xl text-muted-foreground max-w-2xl">
|
||||||
|
{heroSubcopyLine1}
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-lg md:text-xl text-muted-foreground max-w-2xl">
|
||||||
|
{heroSubcopyLine2}
|
||||||
|
</p>
|
||||||
|
<div className="mt-10 flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
|
<Button size="lg" variant="default" asChild>
|
||||||
|
<a href="/contact">Book Architecture Call</a>
|
||||||
|
</Button>
|
||||||
|
<Button size="lg" variant="outline" asChild>
|
||||||
|
<a href="/pricing">Run My Numbers</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/* KPI Metrics */}
|
||||||
|
<div className="mt-16 grid grid-cols-1 sm:grid-cols-3 gap-8 max-w-2xl mx-auto">
|
||||||
|
<MetricsKPI value="$350/mo" label="after year 1" />
|
||||||
|
<MetricsKPI value="65%" label="avg. cost ↓" />
|
||||||
|
<MetricsKPI value="12mo" label="typical break-even" />
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Value Proposition Section */}
|
||||||
|
<Section background="muted">
|
||||||
|
<Container>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||||
|
{valuePropositions.map((item, index) => (
|
||||||
|
<Card key={index} className="flex flex-col">
|
||||||
|
<CardHeader>
|
||||||
|
<div className="mb-2 text-primary">
|
||||||
|
{item.icon}
|
||||||
|
</div>
|
||||||
|
<CardTitle>{item.title}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex-grow">
|
||||||
|
<CardDescription>{item.description}</CardDescription>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Proof Strip / Logos Marquee */}
|
||||||
|
<Section>
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold text-center mb-12">Trusted by forward-thinking teams</h2>
|
||||||
|
<LogosMarquee logos={logos} />
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Process Snapshot */}
|
||||||
|
<Section background="muted">
|
||||||
|
<Container>
|
||||||
|
<Steps
|
||||||
|
steps={processSteps.slice(0, 3)} // Show only first 3 steps on homepage
|
||||||
|
title="Our 6-Step Process"
|
||||||
|
ctaText="See Full Process"
|
||||||
|
ctaHref="/process"
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Case Study Rail */}
|
||||||
|
<Section>
|
||||||
|
<Container>
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h2 className="text-2xl font-bold">Proven Results</h2>
|
||||||
|
<p className="mt-2 text-muted-foreground">
|
||||||
|
Real outcomes from real migrations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Grid columns={{ initial: 1, md: 3 }} gap="6">
|
||||||
|
{caseStudies.map((study, index) => (
|
||||||
|
<CaseStudyCard
|
||||||
|
key={index}
|
||||||
|
title={study.title}
|
||||||
|
description={study.description}
|
||||||
|
kpi1={study.kpi1}
|
||||||
|
kpi2={study.kpi2}
|
||||||
|
ctaText={study.ctaText}
|
||||||
|
ctaHref={study.ctaHref}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Pricing Preview / Own vs Rent */}
|
||||||
|
<Section background="muted">
|
||||||
|
<Container>
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h2 className="text-2xl font-bold">Own vs Rent</h2>
|
||||||
|
<p className="mt-2 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Cloud is a rental agreement with infinite term. Owned hardware + colocation breaks even in under 12 months.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="max-w-4xl mx-auto bg-card border rounded-xl p-6 md:p-8">
|
||||||
|
<h3 className="text-xl font-semibold mb-4">Why Owned Infrastructure Wins</h3>
|
||||||
|
<ul className="space-y-2 text-muted-foreground">
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="mr-2 text-primary">•</span>
|
||||||
|
<span><span className="font-medium">Capex:</span> You buy the hardware once. Depreciate over 3-5 years.</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="mr-2 text-primary">•</span>
|
||||||
|
<span><span className="font-medium">Low Opex:</span> Colocation is ~$200/server/mo. Managed ops is ~$500/server/mo.</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="mr-2 text-primary">•</span>
|
||||||
|
<span><span className="font-medium">No Egress Fees:</span> Move data freely within your network.</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="mr-2 text-primary">•</span>
|
||||||
|
<span><span className="font-medium">Performance:</span> Direct-attached storage and local networks are faster than VPCs.</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start">
|
||||||
|
<span className="mr-2 text-primary">•</span>
|
||||||
|
<span><span className="font-medium">No Vendor Lock-in:</span> Standard hardware and open-source software.</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<div className="mt-6 p-4 bg-muted rounded-lg">
|
||||||
|
<p className="text-center font-medium">
|
||||||
|
Break-even is typically < 12 months. After that, it's pure savings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Trust Section */}
|
||||||
|
<Section>
|
||||||
|
<Container>
|
||||||
|
<TrustSection items={trustItems} title="Your Data, Your Rules" />
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* FAQ Accordion Preview */}
|
||||||
|
<Section background="muted">
|
||||||
|
<Container>
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h2 className="text-2xl font-bold">Common Questions</h2>
|
||||||
|
<p className="mt-2 text-muted-foreground">
|
||||||
|
Answers to key concerns about migrating and self-hosting.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<FAQAccordion />
|
||||||
|
<div className="mt-8 text-center">
|
||||||
|
<Button variant="link" asChild>
|
||||||
|
<a href="/faq">See All FAQs</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Final CTA */}
|
||||||
|
<Section>
|
||||||
|
<Container>
|
||||||
|
<FinalCTASection
|
||||||
|
title="Ready to cut your cloud bill?"
|
||||||
|
description="Book a free architecture call to see how much you can save."
|
||||||
|
primaryCtaText="Book Architecture Call"
|
||||||
|
primaryCtaHref="/contact"
|
||||||
|
secondaryCtaText="Download Blueprint"
|
||||||
|
secondaryCtaHref="/download-blueprint" // Placeholder
|
||||||
|
/>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
129
apps/www/app/about/page.tsx
Normal file
129
apps/www/app/about/page.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// app/about/page.tsx
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ExternalLinkIcon, AwardIcon, UsersIcon, ZapIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Navbar } from '@workspace/ui/components/layout';
|
||||||
|
import { Footer } from '@workspace/ui/components/layout';
|
||||||
|
import { Container, Section, Grid } from '@workspace/ui/components/layout';
|
||||||
|
import { Callout } from '@workspace/ui/components/content';
|
||||||
|
import { Badge } from '@workspace/ui/components/content';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
|
||||||
|
import { Button } from '@workspace/ui/components/ui';
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<Section className="flex-grow">
|
||||||
|
<Container>
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold">About Fortura</h1>
|
||||||
|
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
We built it for VFX first. That's why it works for everyone else.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<Container>
|
||||||
|
<div className="flex flex-col md:flex-row gap-8 items-center">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Our Origin Story</h2>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
Fortura was born in the high-stakes world of Visual Effects (VFX) production. VFX studios operate on razor-thin margins, require massive computational power, and deal with enormous datasets (petabytes) that need to move fast.
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground mb-4">
|
||||||
|
We saw firsthand how the cloud billing model was financially unsustainable for these studios. They were hemorrhaging money on egress fees, rent for virtual machines that sat idle half the time, and paying premium prices for "enterprise" support that often lagged behind community forums.
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
So, we built a better way. A way to own the hardware, colocate it for low-cost power and bandwidth, and automate the hell out of operations so a small team could manage massive infrastructure.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex justify-center">
|
||||||
|
{/* Placeholder for an image or graphic */}
|
||||||
|
<div className="bg-muted border rounded-xl w-full h-64 flex items-center justify-center">
|
||||||
|
<span className="text-muted-foreground">VFX Studio Image/Graphic</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section background="muted">
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-6 text-center">Why That Matters</h2>
|
||||||
|
<Grid columns={{ initial: 1, md: 3 }} gap="6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<AwardIcon className="size-8 text-primary mb-2" />
|
||||||
|
<CardTitle>Petabyte Scale</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<CardDescription>
|
||||||
|
We cut our teeth on systems handling hundreds of terabytes to petabytes of data. This means we know how to design for scale, performance, and cost-efficiency from the ground up.
|
||||||
|
</CardDescription>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<UsersIcon className="size-8 text-primary mb-2" />
|
||||||
|
<CardTitle>Low Budgets</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<CardDescription>
|
||||||
|
VFX studios have tight budgets. Every dollar spent on infrastructure is a dollar not spent on creative talent or compute rendering the next shot. This discipline forces us to find the absolute cheapest, most efficient solutions.
|
||||||
|
</CardDescription>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<ZapIcon className="size-8 text-primary mb-2" />
|
||||||
|
<CardTitle>Real-Time Demands</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<CardDescription>
|
||||||
|
In VFX, deadlines are absolute. A render farm that's down is a studio that's losing money. This means we obsess over uptime, redundancy, and zero-downtime upgrades. What works for 24/7 VFX pipelines works for any business-critical application.
|
||||||
|
</CardDescription>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Our Mission</h2>
|
||||||
|
<p className="text-muted-foreground mb-4 max-w-3xl">
|
||||||
|
Our mission is to fundamentally change how enterprises think about infrastructure. The cloud model of "rent forever" is a bad deal for anyone with significant scale or data. It's a wealth transfer from innovative businesses to cloud providers.
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground mb-6 max-w-3xl">
|
||||||
|
We show you how to own your infrastructure, colocate it for performance and cost, and operate it with a tiny fraction of the overhead of a cloud provider. You keep the performance, you keep the data, and you keep the savings.
|
||||||
|
</p>
|
||||||
|
<Callout variant="default">
|
||||||
|
<span className="font-semibold">"Your hosting provider is fucking you."</span> This isn't just a tagline; it's a technical reality. We're here to help you take control.
|
||||||
|
</Callout>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section background="muted" className="text-center">
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Ready to take control?</h2>
|
||||||
|
<p className="mb-6 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Book a free architecture call to see how much you can save by moving from renting to owning.
|
||||||
|
</p>
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<a href="/contact">
|
||||||
|
Book Architecture Call
|
||||||
|
<ExternalLinkIcon className="ml-2 size-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
apps/www/app/api/search/route.ts
Normal file
4
apps/www/app/api/search/route.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { source } from '@/lib/source';
|
||||||
|
import { createFromSource } from 'fumadocs-core/search/server';
|
||||||
|
|
||||||
|
export const { GET } = createFromSource(source);
|
||||||
175
apps/www/app/contact/page.tsx
Normal file
175
apps/www/app/contact/page.tsx
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
// app/contact/page.tsx
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ExternalLinkIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Navbar } from '@workspace/ui/components/layout';
|
||||||
|
import { Footer } from '@workspace/ui/components/layout';
|
||||||
|
import { Container, Section } from '@workspace/ui/components/layout';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
|
||||||
|
import { Button } from '@workspace/ui/components/ui';
|
||||||
|
import { Input } from '@workspace/ui/components/ui';
|
||||||
|
import { Textarea } from '@workspace/ui/components/ui';
|
||||||
|
import { Label } from '@workspace/ui/components/ui';
|
||||||
|
|
||||||
|
export default function ContactPage() {
|
||||||
|
// State for form inputs
|
||||||
|
const [name, setName] = React.useState('');
|
||||||
|
const [email, setEmail] = React.useState('');
|
||||||
|
const [company, setCompany] = React.useState('');
|
||||||
|
const [message, setMessage] = React.useState('');
|
||||||
|
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
||||||
|
const [submitSuccess, setSubmitSuccess] = React.useState(false);
|
||||||
|
const [submitError, setSubmitError] = React.useState('');
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
setSubmitError('');
|
||||||
|
setSubmitSuccess(false);
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if (!name || !email || !message) {
|
||||||
|
setSubmitError('Please fill in all required fields.');
|
||||||
|
setIsSubmitting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mock submission logic (replace with actual API call)
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log({ name, email, company, message });
|
||||||
|
// Simulate success
|
||||||
|
setSubmitSuccess(true);
|
||||||
|
setIsSubmitting(false);
|
||||||
|
// Reset form
|
||||||
|
setName('');
|
||||||
|
setEmail('');
|
||||||
|
setCompany('');
|
||||||
|
setMessage('');
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<Section className="flex-grow">
|
||||||
|
<Container>
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold">Contact Us</h1>
|
||||||
|
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Book a free architecture call, request a cost estimate, or ask a question.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-6xl mx-auto">
|
||||||
|
{/* Contact Form */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Send us a message</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Fill out the form and we'll get back to you as soon as possible.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{submitSuccess ? (
|
||||||
|
<div className="bg-green-500/10 border border-green-500/30 rounded-md p-4 text-green-500">
|
||||||
|
<p className="font-semibold">Message Sent!</p>
|
||||||
|
<p>Thank you for reaching out. We'll get back to you soon.</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="name">Name *</Label>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="email">Email *</Label>
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="company">Company</Label>
|
||||||
|
<Input
|
||||||
|
id="company"
|
||||||
|
value={company}
|
||||||
|
onChange={(e) => setCompany(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="message">Message *</Label>
|
||||||
|
<Textarea
|
||||||
|
id="message"
|
||||||
|
rows={5}
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{submitError && (
|
||||||
|
<div className="text-red-500 text-sm">{submitError}</div>
|
||||||
|
)}
|
||||||
|
<Button type="submit" disabled={isSubmitting} className="w-full">
|
||||||
|
{isSubmitting ? 'Sending...' : 'Send Message'}
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Calendly Embed and Info */}
|
||||||
|
<div>
|
||||||
|
<Card className="mb-6">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Book an Architecture Call</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
30-minute consultation to discuss your infrastructure and potential savings.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="mb-4 text-muted-foreground">
|
||||||
|
During this call, we'll review your current setup, identify cost-cutting opportunities, and outline a migration path.
|
||||||
|
</p>
|
||||||
|
{/* Calendly Inline Embed */}
|
||||||
|
<div className="bg-muted rounded-lg p-4 text-center">
|
||||||
|
<p className="text-muted-foreground">Calendly Embed Placeholder</p>
|
||||||
|
<p className="text-sm mt-2">(This would be a real Calendly widget)</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Security Inquiries</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p className="mb-4 text-muted-foreground">
|
||||||
|
For sensitive security-related questions, please use our dedicated channel.
|
||||||
|
</p>
|
||||||
|
<Button asChild variant="outline" className="w-full">
|
||||||
|
<a href="mailto:security@fortura.ai">
|
||||||
|
security@fortura.ai
|
||||||
|
<ExternalLinkIcon className="ml-2 size-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
135
apps/www/app/docs/[[...slug]]/page.tsx
Normal file
135
apps/www/app/docs/[[...slug]]/page.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import { source } from '@/lib/source';
|
||||||
|
import {
|
||||||
|
DocsPage,
|
||||||
|
DocsBody,
|
||||||
|
DocsDescription,
|
||||||
|
DocsTitle,
|
||||||
|
EditOnGitHub,
|
||||||
|
} from 'fumadocs-ui/page';
|
||||||
|
import { TypeTable } from 'fumadocs-ui/components/type-table';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||||
|
import { ComponentPreview } from '@/components/docs/component-preview';
|
||||||
|
import { ComponentInstallation } from '@/components/docs/component-installation';
|
||||||
|
import { ExternalLink } from '@/components/docs/external-link';
|
||||||
|
import { Steps, Step } from 'fumadocs-ui/components/steps';
|
||||||
|
import { Footer } from '@workspace/ui/components/docs/footer';
|
||||||
|
import {
|
||||||
|
CodeBlock,
|
||||||
|
type CodeBlockProps,
|
||||||
|
Pre,
|
||||||
|
} from '@/components/docs/codeblock';
|
||||||
|
import { DocsAuthor } from '@/components/docs/docs-author';
|
||||||
|
import { DocsBreadcrumb } from '@/components/docs/docs-breadcrumb';
|
||||||
|
|
||||||
|
export default async function Page(props: {
|
||||||
|
params: Promise<{ slug?: string[] }>;
|
||||||
|
}) {
|
||||||
|
const params = await props.params;
|
||||||
|
const page = source.getPage(params.slug);
|
||||||
|
if (!page) notFound();
|
||||||
|
const MDX = page.data.body;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DocsPage
|
||||||
|
toc={page.data.toc}
|
||||||
|
full={page.data.full}
|
||||||
|
footer={{ component: <Footer /> }}
|
||||||
|
tableOfContent={{ style: 'clerk' }}
|
||||||
|
>
|
||||||
|
<DocsBreadcrumb slug={params.slug} />
|
||||||
|
<DocsTitle>{page.data.title}</DocsTitle>
|
||||||
|
<DocsDescription className="-my-1.5">
|
||||||
|
{page.data.description}
|
||||||
|
</DocsDescription>
|
||||||
|
|
||||||
|
{page.data.author && (
|
||||||
|
<DocsAuthor name={page.data.author.name} url={page.data.author?.url} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
<EditOnGitHub
|
||||||
|
className="border-0"
|
||||||
|
href={`https://github.com/animate-ui/animate-ui/blob/main/apps/www/content/docs/${params.slug ? `${params.slug.join('/')}.mdx` : 'index.mdx'}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DocsBody>
|
||||||
|
<MDX
|
||||||
|
components={{
|
||||||
|
...defaultMdxComponents,
|
||||||
|
ComponentPreview,
|
||||||
|
ComponentInstallation,
|
||||||
|
TypeTable,
|
||||||
|
ExternalLink,
|
||||||
|
Steps,
|
||||||
|
Step,
|
||||||
|
pre: (props: CodeBlockProps) => (
|
||||||
|
<CodeBlock {...props} className="">
|
||||||
|
<Pre>{props.children}</Pre>
|
||||||
|
</CodeBlock>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DocsBody>
|
||||||
|
</DocsPage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
return source.generateParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata(props: {
|
||||||
|
params: Promise<{ slug?: string[] }>;
|
||||||
|
}) {
|
||||||
|
const params = await props.params;
|
||||||
|
const page = source.getPage(params.slug);
|
||||||
|
if (!page) notFound();
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: page.data.title,
|
||||||
|
description: page.data.description,
|
||||||
|
authors: page.data?.author
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: page.data.author.name,
|
||||||
|
...(page.data.author?.url && { url: page.data.author.url }),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: {
|
||||||
|
name: 'imskyleen',
|
||||||
|
url: 'https://github.com/imskyleen',
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: page.data.title,
|
||||||
|
description: page.data.description,
|
||||||
|
url: 'https://animate-ui.com',
|
||||||
|
siteName: 'Animate UI',
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://animate-ui.com/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Animate UI',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
locale: 'en_US',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: 'summary_large_image',
|
||||||
|
site: '@animate_ui',
|
||||||
|
title: page.data.title,
|
||||||
|
description: page.data.description,
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: 'https://animate-ui.com/og-image.png',
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: 'Animate UI',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
29
apps/www/app/docs/layout.tsx
Normal file
29
apps/www/app/docs/layout.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
import { baseOptions } from '@/app/layout.config';
|
||||||
|
import { source } from '@/lib/source';
|
||||||
|
import { ThemeSwitcher } from '@/components/theme-switcher';
|
||||||
|
import XIcon from '@workspace/ui/components/icons/x-icon';
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<DocsLayout
|
||||||
|
githubUrl="https://github.com/animate-ui/animate-ui"
|
||||||
|
links={[
|
||||||
|
{
|
||||||
|
icon: <XIcon />,
|
||||||
|
url: 'https://x.com/animate_ui',
|
||||||
|
text: 'X',
|
||||||
|
type: 'icon',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
tree={source.pageTree}
|
||||||
|
themeSwitch={{
|
||||||
|
component: <ThemeSwitcher />,
|
||||||
|
}}
|
||||||
|
{...baseOptions}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</DocsLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
apps/www/app/examples/layout.tsx
Normal file
9
apps/www/app/examples/layout.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
robots: 'noindex,nofollow',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
5
apps/www/app/examples/radix-sidebar-demo/page.tsx
Normal file
5
apps/www/app/examples/radix-sidebar-demo/page.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { RadixSidebarDemo } from '@/registry/demo/radix/sidebar';
|
||||||
|
|
||||||
|
const RadixSidebarDemoPage = () => <RadixSidebarDemo />;
|
||||||
|
|
||||||
|
export default RadixSidebarDemoPage;
|
||||||
30
apps/www/app/faq/page.tsx
Normal file
30
apps/www/app/faq/page.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// app/faq/page.tsx
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { Navbar } from '@workspace/ui/components/layout';
|
||||||
|
import { Footer } from '@workspace/ui/components/layout';
|
||||||
|
import { Container, Section } from '@workspace/ui/components/layout';
|
||||||
|
import { FAQAccordion } from '@workspace/ui/components/interactive';
|
||||||
|
|
||||||
|
export default function FAQPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<Section className="flex-grow">
|
||||||
|
<Container>
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold">Frequently Asked Questions</h1>
|
||||||
|
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Answers to common questions about Fortura's approach, process, and services.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<FAQAccordion />
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
apps/www/app/favicon.ico
Normal file
BIN
apps/www/app/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
8
apps/www/app/layout.config.tsx
Normal file
8
apps/www/app/layout.config.tsx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Logo } from '@/components/logo';
|
||||||
|
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
|
||||||
|
|
||||||
|
export const baseOptions: BaseLayoutProps = {
|
||||||
|
nav: {
|
||||||
|
title: <Logo containerClassName="md:mt-0.5 md:mb-2.5" size="sm" betaTag />,
|
||||||
|
},
|
||||||
|
};
|
||||||
55
apps/www/app/layout.tsx
Normal file
55
apps/www/app/layout.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { RootProvider } from 'fumadocs-ui/provider';
|
||||||
|
import { Inter } from 'next/font/google';
|
||||||
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
|
import '@workspace/ui/globals.css';
|
||||||
|
import { Providers } from './providers';
|
||||||
|
// import { jsonLd } from '@/lib/json-ld'; // Commented out as it's Animate UI specific
|
||||||
|
|
||||||
|
// --- Update metadata for Fortura ---
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: {
|
||||||
|
template: '%s - Fortura Data Solutions',
|
||||||
|
default: 'Fortura Data Solutions - Own Your Infrastructure, Slash Your Bill',
|
||||||
|
},
|
||||||
|
description:
|
||||||
|
'Fortura helps you cut cloud costs by moving to owned, colocated infrastructure. Performance parity with zero vendor lock-in.',
|
||||||
|
keywords: [
|
||||||
|
'Fortura',
|
||||||
|
'Cloud Alternatives',
|
||||||
|
'Self-hosted',
|
||||||
|
'Colocation',
|
||||||
|
'Cloud Cost Reduction',
|
||||||
|
'Data Ownership',
|
||||||
|
'Enterprise Infrastructure',
|
||||||
|
],
|
||||||
|
// Icons, authors, openGraph, twitter should be updated for Fortura
|
||||||
|
// For now, keeping minimal changes. In practice, update these sections.
|
||||||
|
};
|
||||||
|
|
||||||
|
const inter = Inter({
|
||||||
|
subsets: ['latin'],
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="en" className={inter.className} suppressHydrationWarning>
|
||||||
|
<head>
|
||||||
|
{/* <script
|
||||||
|
type="application/ld+json"
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
||||||
|
/> */}
|
||||||
|
{/* Add Fortura-specific SEO/script tags here if needed */}
|
||||||
|
</head>
|
||||||
|
<body className="flex flex-col min-h-dvh">
|
||||||
|
{/* Theme provider is handled by RootProvider, but we need our custom providers like ToneProvider */}
|
||||||
|
{/* Assuming RootProvider handles dark mode, we can nest our providers inside */}
|
||||||
|
<RootProvider theme={{ defaultTheme: 'dark' }}>
|
||||||
|
<Providers>
|
||||||
|
{children}
|
||||||
|
</Providers>
|
||||||
|
</RootProvider>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
apps/www/app/manifest.ts
Normal file
26
apps/www/app/manifest.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import type { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
export default function manifest(): MetadataRoute.Manifest {
|
||||||
|
return {
|
||||||
|
name: 'Animate UI',
|
||||||
|
short_name: 'Animate UI',
|
||||||
|
description:
|
||||||
|
'Fully animated, open-source component distribution built with React, TypeScript, Tailwind CSS, Motion and Shadcn CLI. Browse a list of components you can install, modify, and use in your projects.',
|
||||||
|
start_url: '/',
|
||||||
|
display: 'standalone',
|
||||||
|
background_color: '#fff',
|
||||||
|
theme_color: '#fff',
|
||||||
|
icons: [
|
||||||
|
{
|
||||||
|
src: '/android-chrome-192x192.png',
|
||||||
|
sizes: '192x192',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: '/android-chrome-512x512.png',
|
||||||
|
sizes: '512x512',
|
||||||
|
type: 'image/png',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
30
apps/www/app/pricing/page.tsx
Normal file
30
apps/www/app/pricing/page.tsx
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
// app/pricing/page.tsx
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { Navbar } from '@workspace/ui/components/layout';
|
||||||
|
import { Footer } from '@workspace/ui/components/layout';
|
||||||
|
import { Container, Section } from '@workspace/ui/components/layout';
|
||||||
|
import { CostCalculator } from '@workspace/ui/components/interactive';
|
||||||
|
|
||||||
|
export default function PricingPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<Section className="flex-grow">
|
||||||
|
<Container>
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold">Pricing</h1>
|
||||||
|
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Calculate your Total Cost of Ownership (TCO) and see how much you can save by moving from renting to owning your infrastructure.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<CostCalculator />
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
101
apps/www/app/process/page.tsx
Normal file
101
apps/www/app/process/page.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// app/process/page.tsx
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { MoveUpRightIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Navbar } from '@workspace/ui/components/layout';
|
||||||
|
import { Footer } from '@workspace/ui/components/layout';
|
||||||
|
import { Container, Section, Grid } from '@workspace/ui/components/layout';
|
||||||
|
import { Steps } from '@workspace/ui/components/content';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
|
||||||
|
import { Button } from '@workspace/ui/components/ui';
|
||||||
|
|
||||||
|
// Mock data for Process Steps (full 6 steps)
|
||||||
|
const PROCESS_STEPS = [
|
||||||
|
{
|
||||||
|
title: "Discovery",
|
||||||
|
description: "We deep-dive into your current infrastructure, workloads, and pain points.",
|
||||||
|
youDo: "Share access to cloud billing, architecture docs, and team interviews.",
|
||||||
|
weDo: "Analyze costs, identify waste, and map technical dependencies."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Shadow & Map",
|
||||||
|
description: "We observe how your teams actually work and map desire paths.",
|
||||||
|
youDo: "Participate in workflow observations and workshops.",
|
||||||
|
weDo: "Document real workflows, identify automation opportunities, and draft the future state."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Architect",
|
||||||
|
description: "We design a tailored, owned infrastructure solution.",
|
||||||
|
youDo: "Review and approve the proposed architecture and migration plan.",
|
||||||
|
weDo: "Specify hardware, select software stack, and design for performance and cost."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Pilot",
|
||||||
|
description: "We build and test a critical component in the new environment.",
|
||||||
|
youDo: "Provide feedback on performance, usability, and integration.",
|
||||||
|
weDo: "Deploy, test, and iterate on the pilot component with your team."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Migrate",
|
||||||
|
description: "We execute the planned migration with zero downtime.",
|
||||||
|
youDo: "Validate functionality and performance post-migration.",
|
||||||
|
weDo: "Cutover workloads, decommission old systems, and optimize the new stack."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Operate",
|
||||||
|
description: "We manage the infrastructure, you focus on your core business.",
|
||||||
|
youDo: "Use the system and provide ongoing feedback for improvements.",
|
||||||
|
weDo: "Monitor, maintain, upgrade, and support the infrastructure 24/7."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ProcessPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<Section className="flex-grow">
|
||||||
|
<Container>
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold">Our Process</h1>
|
||||||
|
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
A proven 6-step approach to migrate from cloud rent to owned infrastructure with minimal retraining and maximum savings.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Steps steps={PROCESS_STEPS} />
|
||||||
|
|
||||||
|
{/* Embedding Card */}
|
||||||
|
<Section background="muted" className="mt-16">
|
||||||
|
<Container>
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center">
|
||||||
|
<span>Embedding</span>
|
||||||
|
<MoveUpRightIcon className="ml-2 size-5 text-primary" />
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<CardDescription>
|
||||||
|
We sit with your teams, map real workflows, then automate the boring.
|
||||||
|
</CardDescription>
|
||||||
|
<p className="mt-2 text-sm">
|
||||||
|
Our "Desire Paths" methodology ensures the system we build fits how your team actually works, not how a generic cloud platform prescribes. This minimizes retraining and maximizes adoption.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button variant="outline" asChild>
|
||||||
|
<a href="/contact">Book Discovery Call</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
apps/www/app/providers.tsx
Normal file
11
apps/www/app/providers.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { ToneProvider } from '@workspace/ui/hooks/use-tone';
|
||||||
|
|
||||||
|
export function Providers({ children }: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<ToneProvider>
|
||||||
|
{children}
|
||||||
|
</ToneProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
apps/www/app/robots.ts
Normal file
11
apps/www/app/robots.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
export default function robots(): MetadataRoute.Robots {
|
||||||
|
return {
|
||||||
|
rules: {
|
||||||
|
userAgent: '*',
|
||||||
|
allow: '/',
|
||||||
|
},
|
||||||
|
sitemap: 'https://animate-ui.com/sitemap.xml',
|
||||||
|
};
|
||||||
|
}
|
||||||
42
apps/www/app/sitemap.ts
Normal file
42
apps/www/app/sitemap.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import type { MetadataRoute } from 'next';
|
||||||
|
|
||||||
|
import { source } from '@/lib/source';
|
||||||
|
import { getGithubLastEdit } from 'fumadocs-core/server';
|
||||||
|
|
||||||
|
export const dynamic = 'force-dynamic';
|
||||||
|
|
||||||
|
export const revalidate = false;
|
||||||
|
|
||||||
|
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
||||||
|
const url = (path: string): string =>
|
||||||
|
new URL(path, 'https://animate-ui.com').toString();
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
url: url('/'),
|
||||||
|
changeFrequency: 'monthly',
|
||||||
|
priority: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: url('/docs'),
|
||||||
|
changeFrequency: 'monthly',
|
||||||
|
priority: 0.8,
|
||||||
|
},
|
||||||
|
...(await Promise.all(
|
||||||
|
source.getPages().map(async (page) => {
|
||||||
|
const time = await getGithubLastEdit({
|
||||||
|
owner: 'imskyleen',
|
||||||
|
repo: 'animate-ui',
|
||||||
|
path: `content/docs/${page.file.path}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: url(page.url),
|
||||||
|
lastModified: time ? new Date(time) : undefined,
|
||||||
|
changeFrequency: 'weekly',
|
||||||
|
priority: 0.5,
|
||||||
|
} as MetadataRoute.Sitemap[number];
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
}
|
||||||
256
apps/www/app/solutions/[industry]/page.tsx
Normal file
256
apps/www/app/solutions/[industry]/page.tsx
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
// app/solutions/[industry]/page.tsx
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { MoveUpRightIcon, DownloadIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Navbar } from '@workspace/ui/components/layout';
|
||||||
|
import { Footer } from '@workspace/ui/components/layout';
|
||||||
|
import { Container, Section, Grid } from '@workspace/ui/components/layout';
|
||||||
|
import { FeatureGrid } from '@workspace/ui/components/content';
|
||||||
|
import { Callout } from '@workspace/ui/components/content';
|
||||||
|
import { Badge } from '@workspace/ui/components/content';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
|
||||||
|
import { Button } from '@workspace/ui/components/ui';
|
||||||
|
import { PlaceholderIcon } from '@workspace/ui/components/icons'; // Using placeholder for now
|
||||||
|
|
||||||
|
// Mock data for industries and their specific details
|
||||||
|
// In a real app, this would likely come from a CMS or database
|
||||||
|
const INDUSTRY_DATA: Record<string, any> = {
|
||||||
|
technology: {
|
||||||
|
name: "Technology",
|
||||||
|
title: "Cloud Alternatives for Technology Companies",
|
||||||
|
description: "From SaaS startups to enterprise software, we help you scale efficiently without the cloud tax.",
|
||||||
|
painPoints: [
|
||||||
|
"High and unpredictable cloud bills that scale with user growth.",
|
||||||
|
"Vendor lock-in making it hard to switch providers or adopt hybrid models.",
|
||||||
|
"Complexity and overhead of managing cloud-native architectures.",
|
||||||
|
"Need for high performance and low latency for user-facing applications."
|
||||||
|
],
|
||||||
|
proposedStack: [
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-5" />,
|
||||||
|
title: "Kubernetes (K3s/RKE2)",
|
||||||
|
description: "Lightweight, certified Kubernetes for efficient container orchestration."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-5" />,
|
||||||
|
title: "PostgreSQL/MySQL",
|
||||||
|
description: "Self-hosted, high-availability databases with point-in-time recovery."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-5" />,
|
||||||
|
title: "MinIO",
|
||||||
|
description: "S3-compatible object storage for unstructured data."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-5" />,
|
||||||
|
title: "Nextcloud",
|
||||||
|
description: "Private file sync, sharing, and collaboration platform."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
migrationPlan: [
|
||||||
|
"Audit current cloud spend and architecture.",
|
||||||
|
"Design a colocated Kubernetes cluster tailored to your workloads.",
|
||||||
|
"Migrate stateful services with zero downtime using blue-green deployments.",
|
||||||
|
"Implement CI/CD pipelines for automated testing and deployment.",
|
||||||
|
"Optimize for cost and performance post-migration."
|
||||||
|
],
|
||||||
|
opsModel: "We provide 24/7 monitoring, managed upgrades, and direct access to our SREs. You retain control over application configuration and scaling decisions.",
|
||||||
|
outcomes: [
|
||||||
|
{
|
||||||
|
metric: "60%",
|
||||||
|
description: "Reduction in monthly infrastructure costs."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "Sub-10ms",
|
||||||
|
description: "Improved database query latency."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "Zero",
|
||||||
|
description: "Vendor lock-in with portable, open-source tools."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
legal: {
|
||||||
|
name: "Legal",
|
||||||
|
title: "Secure Infrastructure for Legal Firms",
|
||||||
|
description: "Secure, private document management and collaboration environments with strict access controls.",
|
||||||
|
painPoints: [
|
||||||
|
"Stringent data privacy regulations (GDPR, CCPA) requiring data residency and control.",
|
||||||
|
"High cost of secure cloud storage and collaboration tools.",
|
||||||
|
"Risk of data breaches from third-party SaaS providers.",
|
||||||
|
"Need for comprehensive audit trails and access logging."
|
||||||
|
],
|
||||||
|
proposedStack: [
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-5" />,
|
||||||
|
title: "Nextcloud",
|
||||||
|
description: "Enterprise-grade file sync, sharing, and collaboration with full audit capabilities."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-5" />,
|
||||||
|
title: "OnlyOffice",
|
||||||
|
description: "Private, self-hosted office suite for document editing and collaboration."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-5" />,
|
||||||
|
title: "Keycloak",
|
||||||
|
description: "Centralized identity management with SAML/OIDC integration for existing directories."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-5" />,
|
||||||
|
title: "OpenSearch",
|
||||||
|
description: "Self-hosted search and analytics engine for document discovery."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
migrationPlan: [
|
||||||
|
"Inventory and classify all documents and data flows.",
|
||||||
|
"Implement a secure, private Nextcloud instance with custom compliance policies.",
|
||||||
|
"Migrate existing document repositories with full version history.",
|
||||||
|
"Integrate with existing case management and time-tracking systems.",
|
||||||
|
"Train staff on new tools and security protocols."
|
||||||
|
],
|
||||||
|
opsModel: "We manage the underlying infrastructure, security patches, and backups. Your team manages user accounts, permissions, and document workflows.",
|
||||||
|
outcomes: [
|
||||||
|
{
|
||||||
|
metric: "100%",
|
||||||
|
description: "Data sovereignty and compliance with legal regulations."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "$50k/year",
|
||||||
|
description: "Savings vs. proprietary legal tech SaaS bundles."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
metric: "Zero",
|
||||||
|
description: "Third-party access to sensitive client data."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// Add more industries here...
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SolutionPageProps {
|
||||||
|
params: { industry: string };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SolutionPage({ params }: SolutionPageProps) {
|
||||||
|
const { industry } = params;
|
||||||
|
const solution = INDUSTRY_DATA[industry];
|
||||||
|
|
||||||
|
if (!solution) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<Section className="flex-grow">
|
||||||
|
<Container>
|
||||||
|
<div className="mb-8">
|
||||||
|
<Button variant="link" asChild className="px-0">
|
||||||
|
<a href="/solutions">← All Solutions</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold">{solution.title}</h1>
|
||||||
|
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
{solution.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Key Pain Points</h2>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{solution.painPoints.map((point: string, index: number) => (
|
||||||
|
<li key={index} className="flex items-start">
|
||||||
|
<span className="mr-2 text-primary">•</span>
|
||||||
|
<span>{point}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section background="muted">
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Proposed Stack</h2>
|
||||||
|
<FeatureGrid features={solution.proposedStack} columns={{ initial: 1, sm: 2 }} />
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Migration Plan</h2>
|
||||||
|
<ol className="space-y-4">
|
||||||
|
{solution.migrationPlan.map((step: string, index: number) => (
|
||||||
|
<li key={index} className="flex items-start">
|
||||||
|
<span className="mr-3 flex h-6 w-6 flex-shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground text-sm font-bold">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
<span>{step}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section background="muted">
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Operations Model</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
{solution.opsModel}
|
||||||
|
</p>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Expected Outcomes</h2>
|
||||||
|
<Grid columns={{ initial: 1, sm: 3 }} gap="6">
|
||||||
|
{solution.outcomes.map((outcome: { metric: string; description: string }, index: number) => (
|
||||||
|
<Card key={index}>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-3xl font-bold">{outcome.metric}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<CardDescription>{outcome.description}</CardDescription>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section background="muted" className="text-center">
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Ready to transform your infrastructure?</h2>
|
||||||
|
<p className="mb-6 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Download our industry-specific blueprint or book a free architecture call.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 justify-center">
|
||||||
|
<Button asChild>
|
||||||
|
<a href={`/download-blueprint/${industry}`}>
|
||||||
|
<DownloadIcon className="mr-2 size-4" />
|
||||||
|
Download Blueprint
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="outline">
|
||||||
|
<a href="/contact">
|
||||||
|
Book Architecture Call
|
||||||
|
<MoveUpRightIcon className="ml-2 size-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
79
apps/www/app/solutions/page.tsx
Normal file
79
apps/www/app/solutions/page.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// app/solutions/page.tsx
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ExternalLinkIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Navbar } from '@workspace/ui/components/layout';
|
||||||
|
import { Footer } from '@workspace/ui/components/layout';
|
||||||
|
import { Container, Section, Grid } from '@workspace/ui/components/layout';
|
||||||
|
import { FeatureGrid } from '@workspace/ui/components/content';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
|
||||||
|
import { Button } from '@workspace/ui/components/ui';
|
||||||
|
import { PlaceholderIcon } from '@workspace/ui/components/icons'; // Using placeholder for now
|
||||||
|
|
||||||
|
// Mock data for Industries
|
||||||
|
const INDUSTRIES = [
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Technology",
|
||||||
|
description: "From SaaS startups to enterprise software, we help you scale efficiently without the cloud tax."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Legal",
|
||||||
|
description: "Secure, private document management and collaboration environments with strict access controls."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Medical",
|
||||||
|
description: "HIPAA-compliant infrastructure for storing, processing, and analyzing sensitive patient data."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Media",
|
||||||
|
description: "High-bandwidth, low-latency solutions for content creation, storage, and distribution."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Research",
|
||||||
|
description: "Powerful compute and storage for data-intensive scientific research and simulations."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function SolutionsPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<Section className="flex-grow">
|
||||||
|
<Container>
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold">Solutions by Industry</h1>
|
||||||
|
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
We tailor our approach to the unique challenges and requirements of your vertical.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FeatureGrid features={INDUSTRIES} columns={{ initial: 1, sm: 2, md: 3 }} />
|
||||||
|
|
||||||
|
<Section background="muted" className="text-center mt-16">
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Don't see your industry?</h2>
|
||||||
|
<p className="mb-6 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Our core principles apply broadly. Contact us to discuss how we can adapt our approach for your specific use case.
|
||||||
|
</p>
|
||||||
|
<Button asChild>
|
||||||
|
<a href="/contact">
|
||||||
|
Contact Us
|
||||||
|
<ExternalLinkIcon className="ml-2 size-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
171
apps/www/app/stack/page.tsx
Normal file
171
apps/www/app/stack/page.tsx
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
// app/stack/page.tsx
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ExternalLinkIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { Navbar } from '@workspace/ui/components/layout';
|
||||||
|
import { Footer } from '@workspace/ui/components/layout';
|
||||||
|
import { Container, Section, Grid } from '@workspace/ui/components/layout';
|
||||||
|
import { FeatureGrid } from '@workspace/ui/components/content';
|
||||||
|
import { Callout } from '@workspace/ui/components/content';
|
||||||
|
import { Badge } from '@workspace/ui/components/content';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@workspace/ui/components/ui';
|
||||||
|
import { Button } from '@workspace/ui/components/ui';
|
||||||
|
import { PlaceholderIcon } from '@workspace/ui/components/icons'; // Using placeholder for now
|
||||||
|
|
||||||
|
// Mock data for Core Frameworks
|
||||||
|
const CORE_FRAMEWORKS = [
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Nextcloud",
|
||||||
|
description: "Your self-hosted productivity platform. Files, collaboration, office suite, and more."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Seafile",
|
||||||
|
description: "High-performance file sync and sharing with robust access controls."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "n8n",
|
||||||
|
description: "Workflow automation. Connect your tools and data with low-code automation."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Keycloak",
|
||||||
|
description: "Identity and Access Management (IAM). Single Sign-On (SSO) for all your apps."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "PostgreSQL",
|
||||||
|
description: "The world's most advanced open-source relational database."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "MinIO",
|
||||||
|
description: "High-performance, S3-compatible object storage."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock data for AI-Native Stack
|
||||||
|
const AI_NATIVE_STACK = [
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Local LLM Inference",
|
||||||
|
description: "Run large language models on your own hardware for complete privacy."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "RAG Engine",
|
||||||
|
description: "Retrieval-Augmented Generation over your private data collections."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Vector DB",
|
||||||
|
description: "Store and query embeddings for semantic search and AI applications."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mock data for Integrations
|
||||||
|
const INTEGRATIONS = [
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "SSO (SAML, OIDC)",
|
||||||
|
description: "Seamless single sign-on with existing identity providers."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "LDAP",
|
||||||
|
description: "Integrate with existing directory services for user management."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Jira",
|
||||||
|
description: "Link issues, automate workflows, and sync data with Jira."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Slack",
|
||||||
|
description: "Send notifications, trigger workflows, and collaborate in Slack."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "Git",
|
||||||
|
description: "Self-host Git repositories and integrate with CI/CD pipelines."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PlaceholderIcon className="size-6" />,
|
||||||
|
title: "API Connectors",
|
||||||
|
description: "Pre-built connectors for hundreds of SaaS and enterprise APIs."
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function StackPage() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col min-h-screen">
|
||||||
|
<Navbar />
|
||||||
|
<Section className="flex-grow">
|
||||||
|
<Container>
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h1 className="text-3xl md:text-4xl font-bold">Our Stack</h1>
|
||||||
|
<p className="mt-4 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
We build on proven, open-source foundations. No vendor lock-in, no proprietary black boxes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Core Frameworks</h2>
|
||||||
|
<FeatureGrid features={CORE_FRAMEWORKS} columns={{ initial: 1, sm: 2, md: 3 }} />
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section background="muted">
|
||||||
|
<Container>
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||||
|
<h2 className="text-2xl font-bold">AI-Native</h2>
|
||||||
|
<Badge variant="info">Private & Secure</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="mb-6 text-muted-foreground max-w-3xl">
|
||||||
|
Leverage AI on your terms. All inference happens on hardware you control, over data you own. Zero leakage to third parties.
|
||||||
|
</p>
|
||||||
|
<FeatureGrid features={AI_NATIVE_STACK} columns={{ initial: 1, md: 3 }} />
|
||||||
|
|
||||||
|
<Callout variant="info" className="mt-8">
|
||||||
|
<span className="font-semibold">AI Compliance:</span> Our local AI stack ensures strict adherence to data privacy regulations (GDPR, HIPAA) and eliminates risks associated with sending sensitive data to external LLM providers.
|
||||||
|
</Callout>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section>
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-6">Integrations</h2>
|
||||||
|
<p className="mb-6 text-muted-foreground max-w-3xl">
|
||||||
|
We wire your new stack into your existing ecosystem. Connect to SSO, directories, project management, communication tools, and more.
|
||||||
|
</p>
|
||||||
|
<FeatureGrid features={INTEGRATIONS} columns={{ initial: 1, sm: 2, md: 3 }} />
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section background="muted" className="text-center">
|
||||||
|
<Container>
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Want to see a specific tool?</h2>
|
||||||
|
<p className="mb-6 text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Our stack is flexible. If you have a specific open-source tool in mind, we can likely integrate it.
|
||||||
|
</p>
|
||||||
|
<Button asChild>
|
||||||
|
<a href="/contact">
|
||||||
|
Discuss Your Stack
|
||||||
|
<ExternalLinkIcon className="ml-2 size-4" />
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
apps/www/components.json
Normal file
20
apps/www/components.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "../../packages/ui/src/styles/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@workspace/ui/components",
|
||||||
|
"utils": "@workspace/ui/lib/utils",
|
||||||
|
"ui": "@workspace/ui/components",
|
||||||
|
"lib": "@workspace/ui/lib",
|
||||||
|
"hooks": "@workspace/ui/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
191
apps/www/components/animate-ui/files.tsx
Normal file
191
apps/www/components/animate-ui/files.tsx
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import * as React from 'react';
|
||||||
|
import { AnimatePresence, type HTMLMotionProps, motion } from 'motion/react';
|
||||||
|
import { FileIcon, FolderIcon, FolderOpenIcon } from 'lucide-react';
|
||||||
|
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionItemProps,
|
||||||
|
AccordionTrigger,
|
||||||
|
AccordionTriggerProps,
|
||||||
|
useAccordionItem,
|
||||||
|
} from '@/registry/radix/accordion';
|
||||||
|
import {
|
||||||
|
MotionHighlight,
|
||||||
|
MotionHighlightItem,
|
||||||
|
} from '@/registry/effects/motion-highlight';
|
||||||
|
|
||||||
|
interface FileButtonProps extends HTMLMotionProps<'div'> {
|
||||||
|
icons?: {
|
||||||
|
close: React.ReactNode;
|
||||||
|
open: React.ReactNode;
|
||||||
|
};
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
open?: boolean;
|
||||||
|
layoutId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FileButton = React.forwardRef<HTMLDivElement, FileButtonProps>(
|
||||||
|
({ children, icons, icon, open, layoutId, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<MotionHighlightItem className="size-full">
|
||||||
|
<motion.div
|
||||||
|
ref={ref}
|
||||||
|
className="flex [&_svg]:size-4 items-center gap-2 p-2 h-10 relative z-10 rounded-lg w-full cursor-default"
|
||||||
|
{...props}
|
||||||
|
layoutId={layoutId}
|
||||||
|
>
|
||||||
|
{icon
|
||||||
|
? typeof icon !== 'string'
|
||||||
|
? icon
|
||||||
|
: null
|
||||||
|
: icons && (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.span
|
||||||
|
key={open ? 'open' : 'close'}
|
||||||
|
initial={{ scale: 0.9 }}
|
||||||
|
animate={{ scale: 1 }}
|
||||||
|
exit={{ scale: 0.9 }}
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
>
|
||||||
|
{open
|
||||||
|
? typeof icons.open !== 'string'
|
||||||
|
? icons.open
|
||||||
|
: null
|
||||||
|
: typeof icons.close !== 'string'
|
||||||
|
? icons.close
|
||||||
|
: null}
|
||||||
|
</motion.span>
|
||||||
|
</AnimatePresence>
|
||||||
|
)}
|
||||||
|
<motion.span className="text-sm block truncate">
|
||||||
|
{children}
|
||||||
|
</motion.span>
|
||||||
|
</motion.div>
|
||||||
|
</MotionHighlightItem>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
FileButton.displayName = 'FileButton';
|
||||||
|
|
||||||
|
type FilesProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||||
|
defaultOpen?: string[];
|
||||||
|
open?: string[];
|
||||||
|
onOpenChange?: (open: string[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Files = React.forwardRef<HTMLDivElement, FilesProps>(
|
||||||
|
({ children, className, defaultOpen, open, onOpenChange, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
'relative size-full rounded-xl border bg-background overflow-auto',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<MotionHighlight
|
||||||
|
controlledItems
|
||||||
|
mode="parent"
|
||||||
|
hover
|
||||||
|
className="bg-muted rounded-lg pointer-events-none"
|
||||||
|
>
|
||||||
|
<Accordion
|
||||||
|
type="multiple"
|
||||||
|
className="p-2"
|
||||||
|
defaultValue={defaultOpen}
|
||||||
|
value={open}
|
||||||
|
onValueChange={onOpenChange}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Accordion>
|
||||||
|
</MotionHighlight>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Files.displayName = 'Files';
|
||||||
|
|
||||||
|
type FolderTriggerProps = AccordionTriggerProps & {
|
||||||
|
layoutId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FolderTrigger = React.forwardRef<HTMLButtonElement, FolderTriggerProps>(
|
||||||
|
({ children, layoutId, ...props }, ref) => {
|
||||||
|
const { isOpen } = useAccordionItem();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccordionTrigger
|
||||||
|
ref={ref}
|
||||||
|
className="h-auto py-0 hover:no-underline font-normal relative z-10"
|
||||||
|
{...props}
|
||||||
|
chevron={false}
|
||||||
|
>
|
||||||
|
<FileButton
|
||||||
|
open={isOpen}
|
||||||
|
icons={{ open: <FolderOpenIcon />, close: <FolderIcon /> }}
|
||||||
|
layoutId={layoutId}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</FileButton>
|
||||||
|
</AccordionTrigger>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
FolderTrigger.displayName = 'FolderTrigger';
|
||||||
|
|
||||||
|
type FolderProps = Omit<AccordionItemProps, 'value'> & {
|
||||||
|
name: string;
|
||||||
|
open?: string[];
|
||||||
|
onOpenChange?: (open: string[]) => void;
|
||||||
|
defaultOpen?: string[];
|
||||||
|
layoutId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Folder = React.forwardRef<HTMLDivElement, FolderProps>(
|
||||||
|
(
|
||||||
|
{ children, name, open, defaultOpen, onOpenChange, layoutId, ...props },
|
||||||
|
ref,
|
||||||
|
) => (
|
||||||
|
<AccordionItem
|
||||||
|
ref={ref}
|
||||||
|
value={name}
|
||||||
|
className="relative border-b-0"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<FolderTrigger layoutId={layoutId}>{name}</FolderTrigger>
|
||||||
|
<AccordionContent className="relative pb-0 !ml-7 before:absolute before:-left-3 before:inset-y-0 before:w-px before:h-full before:bg-border">
|
||||||
|
<Accordion
|
||||||
|
type="multiple"
|
||||||
|
defaultValue={defaultOpen}
|
||||||
|
value={open}
|
||||||
|
onValueChange={onOpenChange}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Accordion>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
Folder.displayName = 'Folder';
|
||||||
|
|
||||||
|
type FileProps = Omit<HTMLMotionProps<'div'>, 'children'> & {
|
||||||
|
name: string;
|
||||||
|
layoutId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const File = React.forwardRef<HTMLDivElement, FileProps>(
|
||||||
|
({ name, layoutId, ...props }, ref) => (
|
||||||
|
<FileButton ref={ref} icon={<FileIcon />} layoutId={layoutId} {...props}>
|
||||||
|
{name}
|
||||||
|
</FileButton>
|
||||||
|
),
|
||||||
|
);
|
||||||
|
File.displayName = 'File';
|
||||||
|
|
||||||
|
export { Files, Folder, File };
|
||||||
70
apps/www/components/cards.tsx
Normal file
70
apps/www/components/cards.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import {
|
||||||
|
MotionHighlight,
|
||||||
|
MotionHighlightItem,
|
||||||
|
} from '@/registry/effects/motion-highlight';
|
||||||
|
import { MotionEffect } from '@/registry/effects/motion-effect';
|
||||||
|
import { Blocks, BringToFront, GitPullRequest } from 'lucide-react';
|
||||||
|
import ShadcnIcon from '@workspace/ui/components/icons/shadcn-icon';
|
||||||
|
|
||||||
|
const CARDS = [
|
||||||
|
{
|
||||||
|
value: '1',
|
||||||
|
icon: BringToFront,
|
||||||
|
title: 'Animated Components',
|
||||||
|
description: 'Beautiful Motion-animated components for dynamic websites.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '2',
|
||||||
|
icon: GitPullRequest,
|
||||||
|
title: 'Open Source',
|
||||||
|
description:
|
||||||
|
'A project built for the dev community with the dev community.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '3',
|
||||||
|
icon: ShadcnIcon,
|
||||||
|
title: 'Complementary to Shadcn UI',
|
||||||
|
description:
|
||||||
|
'The components are designed to be used with Shadcn UI components.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: '4',
|
||||||
|
icon: Blocks,
|
||||||
|
title: 'Component Distribution',
|
||||||
|
description:
|
||||||
|
'Install the components in your project and modify them as you wish.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Cards = () => {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<MotionHighlight hover controlledItems className="rounded-xl">
|
||||||
|
{CARDS.map((card, index) => (
|
||||||
|
<MotionEffect
|
||||||
|
key={card.value}
|
||||||
|
slide={{
|
||||||
|
direction: 'down',
|
||||||
|
}}
|
||||||
|
fade
|
||||||
|
zoom
|
||||||
|
delay={index * 0.1}
|
||||||
|
inView
|
||||||
|
>
|
||||||
|
<MotionHighlightItem data-value={card.value} className="h-full">
|
||||||
|
<div className="p-4 flex flex-col border rounded-xl cursor-default h-full">
|
||||||
|
<div className="flex items-center justify-around size-10 rounded-lg bg-blue-500/10 mb-2">
|
||||||
|
<card.icon className="size-5 text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<p className="text-base font-medium mb-1">{card.title}</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{card.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</MotionHighlightItem>
|
||||||
|
</MotionEffect>
|
||||||
|
))}
|
||||||
|
</MotionHighlight>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
135
apps/www/components/components-section.tsx
Normal file
135
apps/www/components/components-section.tsx
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsContents,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
} from '@/registry/components/tabs';
|
||||||
|
import TextIcon from '@workspace/ui/components/icons/text-icon';
|
||||||
|
import PrimitivesIcon from '@workspace/ui/components/icons/primitives-icon';
|
||||||
|
import EffectsIcon from '@workspace/ui/components/icons/effects-icon';
|
||||||
|
import ComponentsIcon from '@workspace/ui/components/icons/components-icon';
|
||||||
|
import BackgroundIcon from '@workspace/ui/components/icons/background-icon';
|
||||||
|
import { CodeEditor } from '@/registry/components/code-editor';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { index } from '@/__registry__';
|
||||||
|
import { SectionWrapper } from './section-wrapper';
|
||||||
|
import { MotionEffect } from '@/registry/effects/motion-effect';
|
||||||
|
|
||||||
|
const TABS = [
|
||||||
|
{
|
||||||
|
value: 'effects',
|
||||||
|
label: 'Effects',
|
||||||
|
icon: EffectsIcon,
|
||||||
|
name: 'motion-effect-image-grid-demo',
|
||||||
|
code: index['motion-effect-image-grid-demo'].files[0].content,
|
||||||
|
demo: index['motion-effect-image-grid-demo'].component,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'components',
|
||||||
|
label: 'Components',
|
||||||
|
icon: ComponentsIcon,
|
||||||
|
name: 'cursor-demo',
|
||||||
|
code: index['cursor-demo'].files[0].content,
|
||||||
|
demo: index['cursor-demo'].component,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'primitives',
|
||||||
|
label: 'Primitives',
|
||||||
|
icon: PrimitivesIcon,
|
||||||
|
name: 'radix-accordion-demo',
|
||||||
|
code: index['radix-accordion-demo'].files[0].content,
|
||||||
|
demo: index['radix-accordion-demo'].component,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'text',
|
||||||
|
label: 'Text',
|
||||||
|
icon: TextIcon,
|
||||||
|
name: 'writing-text-demo',
|
||||||
|
code: index['writing-text-demo'].files[0].content,
|
||||||
|
demo: index['writing-text-demo'].component,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'background',
|
||||||
|
label: 'Background',
|
||||||
|
icon: BackgroundIcon,
|
||||||
|
name: 'fireworks-background-demo',
|
||||||
|
code: index['fireworks-background-demo'].files[0].content,
|
||||||
|
demo: index['fireworks-background-demo'].component,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ComponentsSection = () => {
|
||||||
|
const [currentTab, setCurrentTab] = useState(TABS[0]?.value ?? '');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionEffect
|
||||||
|
slide={{
|
||||||
|
offset: 200,
|
||||||
|
}}
|
||||||
|
fade
|
||||||
|
inView
|
||||||
|
delay={0.25}
|
||||||
|
inViewMargin="-50px"
|
||||||
|
>
|
||||||
|
<SectionWrapper
|
||||||
|
subtitle="Components"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Various <span className="text-blue-500">types</span> of animated
|
||||||
|
components
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
description="Find all types of animated components on Animate UI. Dynamic backgrounds, primitive components animated with Motion and styled with Shadcn's style, animated text and effects to let you easily animate your own components."
|
||||||
|
>
|
||||||
|
<Tabs value={currentTab} onValueChange={setCurrentTab}>
|
||||||
|
<TabsList
|
||||||
|
className="bg-transparent gap-10 mb-7 max-w-full h-30 pr-4 overflow-x-auto justify-start"
|
||||||
|
activeClassName="bg-blue-500/10 shadow-none rounded-lg size-full h-16"
|
||||||
|
>
|
||||||
|
{TABS.map((tab) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={tab.value}
|
||||||
|
value={tab.value}
|
||||||
|
className="relative aspect-square size-16 flex-col gap-y-4 text-neutral-400 dark:text-neutral-500 transition-colors duration-300 data-[state=active]:text-blue-500"
|
||||||
|
>
|
||||||
|
<tab.icon className="size-12" />
|
||||||
|
<p className="absolute top-[calc(100%+12px)]">{tab.label}</p>
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContents className="w-full">
|
||||||
|
{TABS.map((tab) => (
|
||||||
|
<TabsContent
|
||||||
|
key={tab.value}
|
||||||
|
value={tab.value}
|
||||||
|
className="flex flex-row gap-x-4 h-[450px]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
key={currentTab}
|
||||||
|
className="relative flex-1 shrink-0 p-4 border rounded-xl h-full flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<tab.demo />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CodeEditor
|
||||||
|
lang="tsx"
|
||||||
|
writing={false}
|
||||||
|
className="flex-1 w-auto hidden lg:block h-full"
|
||||||
|
title={`${tab.name}.tsx`}
|
||||||
|
copyButton
|
||||||
|
>
|
||||||
|
{tab.code}
|
||||||
|
</CodeEditor>
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<TabsContent value="text">TEST</TabsContent>
|
||||||
|
<TabsContent value="background">TEST</TabsContent>
|
||||||
|
</TabsContents>
|
||||||
|
</Tabs>
|
||||||
|
</SectionWrapper>
|
||||||
|
</MotionEffect>
|
||||||
|
);
|
||||||
|
};
|
||||||
108
apps/www/components/distribution-section.tsx
Normal file
108
apps/www/components/distribution-section.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import { MotionEffect } from '@/registry/effects/motion-effect';
|
||||||
|
import { SectionWrapper } from './section-wrapper';
|
||||||
|
import {
|
||||||
|
AnimatedSpan,
|
||||||
|
Terminal,
|
||||||
|
TypingAnimation,
|
||||||
|
} from '@workspace/ui/components/magicui/terminal';
|
||||||
|
import { useInView, motion } from 'motion/react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { File, Files, Folder } from '@/components/animate-ui/files';
|
||||||
|
|
||||||
|
export const DistributionSection = () => {
|
||||||
|
const localRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const isInView = useInView(localRef, {
|
||||||
|
once: true,
|
||||||
|
margin: '50px',
|
||||||
|
});
|
||||||
|
|
||||||
|
const [openStructureFile, setOpenStructureFile] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isInView) return;
|
||||||
|
const interval = setInterval(() => setOpenStructureFile(true), 6000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isInView]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MotionEffect
|
||||||
|
slide={{
|
||||||
|
offset: 200,
|
||||||
|
}}
|
||||||
|
fade
|
||||||
|
inView
|
||||||
|
delay={0.25}
|
||||||
|
inViewMargin="-50px"
|
||||||
|
>
|
||||||
|
<SectionWrapper
|
||||||
|
subtitle="Distribution"
|
||||||
|
title={
|
||||||
|
<>
|
||||||
|
Not a library but a{' '}
|
||||||
|
<span className="text-emerald-500">component distribution</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
description="Animate UI is not a library, it's a component distribution system. You can use the components in your project simply by installing them with the shadcn/ui CLI, or by copying the code and pasting it into your project."
|
||||||
|
color="text-emerald-500"
|
||||||
|
backgroundColor="bg-emerald-500/10 hover:bg-emerald-500/20"
|
||||||
|
>
|
||||||
|
<div ref={localRef} className="flex lg:flex-row flex-col gap-4">
|
||||||
|
{isInView && (
|
||||||
|
<>
|
||||||
|
<Terminal className="h-[418px] w-full lg:flex-1 max-w-none">
|
||||||
|
<TypingAnimation>
|
||||||
|
> pnpm dlx shadcn@latest add
|
||||||
|
"https://animate-ui.com/r/counter"
|
||||||
|
</TypingAnimation>
|
||||||
|
|
||||||
|
<AnimatedSpan delay={4000} className="text-green-500">
|
||||||
|
<span>✔ Checking registry.</span>
|
||||||
|
</AnimatedSpan>
|
||||||
|
<AnimatedSpan delay={4500} className="text-green-500">
|
||||||
|
<span>✔ Installing dependencies.</span>
|
||||||
|
</AnimatedSpan>
|
||||||
|
<AnimatedSpan delay={5000} className="text-green-500">
|
||||||
|
<span>✔ Created 1 file:</span>
|
||||||
|
</AnimatedSpan>
|
||||||
|
<AnimatedSpan delay={5500} className="text-green-500">
|
||||||
|
{!openStructureFile ? (
|
||||||
|
<motion.span layoutId="counter-file">
|
||||||
|
- components/animate-ui/counter.tsx
|
||||||
|
</motion.span>
|
||||||
|
) : (
|
||||||
|
<span> - components/animate-ui/counter.tsx</span>
|
||||||
|
)}
|
||||||
|
</AnimatedSpan>
|
||||||
|
</Terminal>
|
||||||
|
|
||||||
|
<Files
|
||||||
|
defaultOpen={['components']}
|
||||||
|
className="h-[418px] w-full flex-1"
|
||||||
|
>
|
||||||
|
<Folder name="app" defaultOpen={['(home)']}>
|
||||||
|
<File name="layout.tsx" />
|
||||||
|
<File name="page.tsx" />
|
||||||
|
<File name="global.css" />
|
||||||
|
</Folder>
|
||||||
|
<Folder name="components" defaultOpen={['animate-ui']}>
|
||||||
|
<Folder name="animate-ui">
|
||||||
|
<File name="cursor.tsx" />
|
||||||
|
<File name="tabs.tsx" />
|
||||||
|
{openStructureFile && (
|
||||||
|
<File name="counter.tsx" layoutId="counter-file" />
|
||||||
|
)}
|
||||||
|
</Folder>
|
||||||
|
<File name="button.tsx" />
|
||||||
|
<File name="tabs.tsx" />
|
||||||
|
<File name="dialog.tsx" />
|
||||||
|
</Folder>
|
||||||
|
<File name="package.json" />
|
||||||
|
</Files>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SectionWrapper>
|
||||||
|
</MotionEffect>
|
||||||
|
);
|
||||||
|
};
|
||||||
134
apps/www/components/docs/codeblock.tsx
Normal file
134
apps/www/components/docs/codeblock.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type HTMLAttributes,
|
||||||
|
type ReactNode,
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
import {
|
||||||
|
ScrollArea,
|
||||||
|
ScrollBar,
|
||||||
|
ScrollViewport,
|
||||||
|
} from '@workspace/ui/components/ui/scroll-area';
|
||||||
|
import type { ScrollArea as ScrollAreaPrimitive } from 'radix-ui';
|
||||||
|
import { CopyButton } from '@/registry/buttons/copy';
|
||||||
|
|
||||||
|
export type CodeBlockProps = HTMLAttributes<HTMLElement> & {
|
||||||
|
icon?: ReactNode;
|
||||||
|
allowCopy?: boolean;
|
||||||
|
viewportProps?: ScrollAreaPrimitive.ScrollAreaViewportProps;
|
||||||
|
onCopy?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Pre = forwardRef<HTMLPreElement, HTMLAttributes<HTMLPreElement>>(
|
||||||
|
({ className, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<pre
|
||||||
|
ref={ref}
|
||||||
|
className={cn('p-4 focus-visible:outline-none', className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
Pre.displayName = 'Pre';
|
||||||
|
|
||||||
|
export const CodeBlock = forwardRef<HTMLElement, CodeBlockProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
allowCopy = true,
|
||||||
|
icon,
|
||||||
|
viewportProps,
|
||||||
|
onCopy: onCopyEvent,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const areaRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const onCopy = useCallback(() => {
|
||||||
|
const pre = areaRef.current?.getElementsByTagName('pre').item(0);
|
||||||
|
|
||||||
|
if (!pre) return;
|
||||||
|
|
||||||
|
const clone = pre.cloneNode(true) as HTMLElement;
|
||||||
|
clone.querySelectorAll('.nd-copy-ignore').forEach((node) => {
|
||||||
|
node.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
void navigator.clipboard.writeText(clone.textContent ?? '').then(() => {
|
||||||
|
setIsCopied(true);
|
||||||
|
onCopyEvent?.();
|
||||||
|
setTimeout(() => setIsCopied(false), 3000);
|
||||||
|
});
|
||||||
|
}, [onCopyEvent]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<figure
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
className={cn(
|
||||||
|
'not-prose group fd-codeblock relative my-6 overflow-hidden rounded-xl border border-border text-sm [&.shiki]:!bg-muted/50',
|
||||||
|
props.className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{title ? (
|
||||||
|
<div className="flex flex-row items-center gap-2 bg-muted border-b border-border/75 dark:border-border/50 px-4 h-10">
|
||||||
|
{icon ? (
|
||||||
|
<div
|
||||||
|
className="text-muted-foreground [&_svg]:size-3.5"
|
||||||
|
dangerouslySetInnerHTML={
|
||||||
|
typeof icon === 'string' ? { __html: icon } : undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{typeof icon !== 'string' ? icon : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<figcaption className="flex-1 truncate text-muted-foreground">
|
||||||
|
{title}
|
||||||
|
</figcaption>
|
||||||
|
{allowCopy ? (
|
||||||
|
<CopyButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="-me-2 bg-transparent hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
|
onClick={onCopy}
|
||||||
|
isCopied={isCopied}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
allowCopy && (
|
||||||
|
<CopyButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute right-2 top-2 z-[2] backdrop-blur-md bg-transparent hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
|
onClick={onCopy}
|
||||||
|
isCopied={isCopied}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<ScrollArea ref={areaRef} dir="ltr">
|
||||||
|
<ScrollViewport
|
||||||
|
{...viewportProps}
|
||||||
|
className={cn('max-h-[600px]', viewportProps?.className)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</ScrollViewport>
|
||||||
|
<ScrollBar orientation="horizontal" />
|
||||||
|
</ScrollArea>
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
CodeBlock.displayName = 'CodeBlock';
|
||||||
78
apps/www/components/docs/component-installation.tsx
Normal file
78
apps/www/components/docs/component-installation.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { index } from '@/__registry__';
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
TabsContent,
|
||||||
|
TabsContents,
|
||||||
|
} from '@/registry/radix/tabs';
|
||||||
|
import { CodeTabs } from '@/registry/components/code-tabs';
|
||||||
|
import { ComponentManualInstallation } from './component-manual-installation';
|
||||||
|
|
||||||
|
interface ComponentInstallationProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ComponentInstallation({
|
||||||
|
name,
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: ComponentInstallationProps) {
|
||||||
|
const component = index[name];
|
||||||
|
|
||||||
|
const commands = {
|
||||||
|
npm: `npx shadcn@latest add "${component.command}"`,
|
||||||
|
pnpm: `pnpm dlx shadcn@latest add "${component.command}"`,
|
||||||
|
yarn: `npx shadcn@latest add "${component.command}"`,
|
||||||
|
bun: `bun x --bun shadcn@latest add "${component.command}"`,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative my-4 flex flex-col space-y-2 lg:max-w-[120ch]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Tabs defaultValue="cli" className="relative mr-auto w-full">
|
||||||
|
<TabsList
|
||||||
|
className="justify-start mb-2 rounded-xl h-10 bg-transparent p-0"
|
||||||
|
activeClassName="bg-neutral-100 dark:bg-neutral-800 shadow-none rounded-lg"
|
||||||
|
>
|
||||||
|
<TabsTrigger
|
||||||
|
value="cli"
|
||||||
|
className="relative border-none rounded-lg px-4 py-2 h-full font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
||||||
|
>
|
||||||
|
CLI
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="manual"
|
||||||
|
className="relative border-none rounded-lg px-4 py-2 h-full font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
||||||
|
>
|
||||||
|
Manual
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<TabsContents>
|
||||||
|
<TabsContent value="cli">
|
||||||
|
<CodeTabs codes={commands} />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="manual">
|
||||||
|
<ComponentManualInstallation
|
||||||
|
path={component.files[0].target}
|
||||||
|
dependencies={component.dependencies}
|
||||||
|
devDependencies={component.devDependencies}
|
||||||
|
registryDependencies={component.registryDependencies}
|
||||||
|
code={component.files[0].content}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
</TabsContents>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
130
apps/www/components/docs/component-manual-installation.tsx
Normal file
130
apps/www/components/docs/component-manual-installation.tsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { DynamicCodeBlock } from '@/components/docs/dynamic-codeblock';
|
||||||
|
import { CodeTabs } from '@/registry/components/code-tabs';
|
||||||
|
import { Step, Steps } from 'fumadocs-ui/components/steps';
|
||||||
|
import { CollapsibleContent } from 'fumadocs-ui/components/ui/collapsible';
|
||||||
|
import { Collapsible } from 'fumadocs-ui/components/ui/collapsible';
|
||||||
|
import { CollapsibleTrigger } from 'fumadocs-ui/components/ui/collapsible';
|
||||||
|
import { Button } from '@workspace/ui/components/ui/button';
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import ReactIcon from '@workspace/ui/components/icons/react-icon';
|
||||||
|
|
||||||
|
const getDepsCommands = (dependencies?: string[]) => {
|
||||||
|
if (!dependencies) return undefined;
|
||||||
|
return {
|
||||||
|
npm: `npm install ${dependencies?.join(' ')}`,
|
||||||
|
pnpm: `pnpm add ${dependencies?.join(' ')}`,
|
||||||
|
yarn: `yarn add ${dependencies?.join(' ')}`,
|
||||||
|
bun: `bun add ${dependencies?.join(' ')}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRegistryDepsCommands = (dependencies?: string[]) => {
|
||||||
|
if (!dependencies) return undefined;
|
||||||
|
const quotedDependencies = dependencies.map((dep) => `"${dep}"`).join(' ');
|
||||||
|
return {
|
||||||
|
npm: `npx shadcn@latest add ${quotedDependencies}`,
|
||||||
|
pnpm: `pnpm dlx shadcn@latest add ${quotedDependencies}`,
|
||||||
|
yarn: `npx shadcn@latest add ${quotedDependencies}`,
|
||||||
|
bun: `bun x --bun shadcn@latest add ${quotedDependencies}`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ComponentManualInstallation = ({
|
||||||
|
path,
|
||||||
|
dependencies,
|
||||||
|
devDependencies,
|
||||||
|
registryDependencies,
|
||||||
|
code,
|
||||||
|
}: {
|
||||||
|
path: string;
|
||||||
|
dependencies?: string[];
|
||||||
|
devDependencies?: string[];
|
||||||
|
registryDependencies?: string[];
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
const depsCommands = getDepsCommands(dependencies);
|
||||||
|
const devDepsCommands = getDepsCommands(devDependencies);
|
||||||
|
const registryDepsCommands = getRegistryDepsCommands(registryDependencies);
|
||||||
|
|
||||||
|
const [isOpened, setIsOpened] = useState(false);
|
||||||
|
const collapsibleRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Steps>
|
||||||
|
{dependencies && depsCommands && (
|
||||||
|
<Step>
|
||||||
|
<h4 className="pt-1 pb-4">Install the following dependencies:</h4>
|
||||||
|
<CodeTabs codes={depsCommands} />
|
||||||
|
</Step>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{devDependencies && devDepsCommands && (
|
||||||
|
<Step>
|
||||||
|
<h4 className="pt-1 pb-4">Install the following dev dependencies:</h4>
|
||||||
|
<CodeTabs codes={devDepsCommands} />
|
||||||
|
</Step>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{registryDependencies && registryDepsCommands && (
|
||||||
|
<Step>
|
||||||
|
<h4 className="pt-1 pb-4">
|
||||||
|
Install the following registry dependencies:
|
||||||
|
</h4>
|
||||||
|
<CodeTabs codes={registryDepsCommands} />
|
||||||
|
</Step>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
<h4 className="pt-1 pb-4">
|
||||||
|
Copy and paste the following code into your project:
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
<Collapsible open={isOpened} onOpenChange={setIsOpened}>
|
||||||
|
<div ref={collapsibleRef} className="relative overflow-hidden">
|
||||||
|
<CollapsibleContent
|
||||||
|
forceMount
|
||||||
|
className={cn('overflow-hidden', !isOpened && 'max-h-32')}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'[&_pre]:my-0 [&_pre]:max-h-[650px] [&_code]:pb-[60px]',
|
||||||
|
!isOpened
|
||||||
|
? '[&_pre]:overflow-hidden'
|
||||||
|
: '[&_pre]:overflow-auto]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<DynamicCodeBlock
|
||||||
|
code={code}
|
||||||
|
lang="tsx"
|
||||||
|
title={path}
|
||||||
|
icon={<ReactIcon />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CollapsibleContent>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute flex items-center justify-center bg-gradient-to-b rounded-t-xl from-neutral-300/30 to-white dark:from-neutral-700/30 dark:to-neutral-950 p-2',
|
||||||
|
isOpened ? 'inset-x-0 bottom-0 h-12' : 'inset-0',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CollapsibleTrigger asChild>
|
||||||
|
<Button variant="secondary" className="h-8 text-xs">
|
||||||
|
{isOpened ? 'Collapse' : 'Expand'}
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
</Step>
|
||||||
|
|
||||||
|
<Step>
|
||||||
|
<h4 className="pt-1 pb-4">
|
||||||
|
Update the import paths to match your project setup.
|
||||||
|
</h4>
|
||||||
|
</Step>
|
||||||
|
</Steps>
|
||||||
|
);
|
||||||
|
};
|
||||||
181
apps/www/components/docs/component-preview.tsx
Normal file
181
apps/www/components/docs/component-preview.tsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { index } from '@/__registry__';
|
||||||
|
import { ComponentWrapper } from '@/components/docs/component-wrapper';
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsContent,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
TabsContents,
|
||||||
|
} from '@/registry/radix/tabs';
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
import { Loader } from 'lucide-react';
|
||||||
|
import { Suspense, useEffect, useMemo, useState } from 'react';
|
||||||
|
import { DynamicCodeBlock } from '@/components/docs/dynamic-codeblock';
|
||||||
|
import ReactIcon from '@workspace/ui/components/icons/react-icon';
|
||||||
|
import { type Binds, Tweakpane } from '@workspace/ui/components/docs/tweakpane';
|
||||||
|
|
||||||
|
interface ComponentPreviewProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
name: string;
|
||||||
|
iframe?: boolean;
|
||||||
|
bigScreen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenFirstLevel<T>(input: Record<string, any>): T {
|
||||||
|
return Object.values(input).reduce((acc, current) => {
|
||||||
|
return { ...acc, ...current };
|
||||||
|
}, {} as T);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrapValues(obj: Record<string, any>): Record<string, any> {
|
||||||
|
if (obj !== null && typeof obj === 'object' && !Array.isArray(obj)) {
|
||||||
|
if ('value' in obj) {
|
||||||
|
return obj.value;
|
||||||
|
}
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
for (const key in obj) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||||
|
result[key] = unwrapValues(obj[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ComponentPreview({
|
||||||
|
name,
|
||||||
|
className,
|
||||||
|
iframe = false,
|
||||||
|
bigScreen = false,
|
||||||
|
...props
|
||||||
|
}: ComponentPreviewProps) {
|
||||||
|
const [binds, setBinds] = useState<Binds | null>(null);
|
||||||
|
const [componentProps, setComponentProps] = useState<Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
|
const code = useMemo(() => {
|
||||||
|
const code = index[name]?.files?.[0]?.content;
|
||||||
|
|
||||||
|
if (!code) {
|
||||||
|
console.error(`Component with name "${name}" not found in registry.`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
const preview = useMemo(() => {
|
||||||
|
const Component = index[name]?.component;
|
||||||
|
|
||||||
|
if (Object.keys(Component?.demoProps ?? {}).length !== 0) {
|
||||||
|
if (componentProps === null)
|
||||||
|
setComponentProps(unwrapValues(Component?.demoProps));
|
||||||
|
if (binds === null) setBinds(Component?.demoProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Component) {
|
||||||
|
console.error(`Component with name "${name}" not found in registry.`);
|
||||||
|
return (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Component{' '}
|
||||||
|
<code className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm">
|
||||||
|
{name}
|
||||||
|
</code>{' '}
|
||||||
|
not found in registry.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Component {...flattenFirstLevel(componentProps ?? {})} />;
|
||||||
|
}, [name, componentProps, binds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!binds) return;
|
||||||
|
setComponentProps(unwrapValues(binds));
|
||||||
|
}, [binds]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'relative my-4 flex flex-col space-y-2 lg:max-w-[120ch] not-prose',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Tabs defaultValue="preview" className="relative mr-auto w-full">
|
||||||
|
<div className="flex items-center justify-between pb-2">
|
||||||
|
<TabsList
|
||||||
|
className="justify-start rounded-xl h-10 bg-transparent p-0"
|
||||||
|
activeClassName="bg-neutral-100 dark:bg-neutral-800 shadow-none rounded-lg"
|
||||||
|
>
|
||||||
|
<TabsTrigger
|
||||||
|
value="preview"
|
||||||
|
className="relative border-none rounded-lg px-4 py-2 h-full font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
||||||
|
>
|
||||||
|
Preview
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger
|
||||||
|
value="code"
|
||||||
|
className="relative border-none rounded-lg px-4 py-2 h-full font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
||||||
|
>
|
||||||
|
Code
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TabsContents>
|
||||||
|
<TabsContent
|
||||||
|
value="preview"
|
||||||
|
className="relative rounded-md h-full"
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 10 }}
|
||||||
|
>
|
||||||
|
<ComponentWrapper
|
||||||
|
name={name}
|
||||||
|
iframe={iframe}
|
||||||
|
bigScreen={bigScreen}
|
||||||
|
tweakpane={
|
||||||
|
binds && <Tweakpane binds={binds} onBindsChange={setBinds} />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="flex items-center text-sm text-muted-foreground">
|
||||||
|
<Loader className="mr-2 size-4 animate-spin" />
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{preview}
|
||||||
|
</Suspense>
|
||||||
|
</ComponentWrapper>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent
|
||||||
|
value="code"
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 10 }}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col space-y-4">
|
||||||
|
<div className="w-full rounded-md [&_pre]:my-0 [&_pre]:max-h-[400px] [&_pre]:overflow-auto">
|
||||||
|
<DynamicCodeBlock
|
||||||
|
code={code}
|
||||||
|
lang="tsx"
|
||||||
|
title={`${name}.tsx`}
|
||||||
|
icon={<ReactIcon />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</TabsContents>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
127
apps/www/components/docs/component-wrapper.tsx
Normal file
127
apps/www/components/docs/component-wrapper.tsx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { OpenInV0Button } from '@workspace/ui/components/docs/open-in-v0-button';
|
||||||
|
import { Button } from '@workspace/ui/components/ui/button';
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
import { Fullscreen, RotateCcw, SlidersHorizontal } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { motion } from 'motion/react';
|
||||||
|
import Iframe from './iframe';
|
||||||
|
import { useIsMobile } from '@workspace/ui/hooks/use-mobile';
|
||||||
|
|
||||||
|
interface ComponentWrapperProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
name: string;
|
||||||
|
iframe?: boolean;
|
||||||
|
bigScreen?: boolean;
|
||||||
|
tweakpane?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ComponentWrapper = ({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
name,
|
||||||
|
iframe = false,
|
||||||
|
bigScreen = false,
|
||||||
|
tweakpane,
|
||||||
|
}: ComponentWrapperProps) => {
|
||||||
|
const [tweakMode, setTweakMode] = useState(false);
|
||||||
|
const [key, setKey] = useState(0);
|
||||||
|
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={cn(
|
||||||
|
'max-w-screen relative rounded-xl border bg-background flex flex-col md:flex-row',
|
||||||
|
bigScreen && 'overflow-hidden',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<motion.div className="relative size-full flex-1">
|
||||||
|
{!iframe && (
|
||||||
|
<div className="absolute top-3 right-3 z-[9] bg-background flex items-center justify-end gap-2 p-1 rounded-[11px]">
|
||||||
|
<OpenInV0Button url={`https://animate-ui.com/r/${name}.json`} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={() => setKey((prev) => prev + 1)}
|
||||||
|
className="flex items-center rounded-lg"
|
||||||
|
variant="neutral"
|
||||||
|
size="icon-sm"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<RotateCcw aria-label="restart-btn" size={14} />
|
||||||
|
</motion.button>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{iframe && (
|
||||||
|
<Button
|
||||||
|
onClick={() => window.open(`/examples/${name}`, '_blank')}
|
||||||
|
className="flex items-center rounded-lg"
|
||||||
|
variant="neutral"
|
||||||
|
size="icon-sm"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<Fullscreen aria-label="fullscreen-btn" size={14} />
|
||||||
|
</motion.button>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tweakpane && (
|
||||||
|
<Button
|
||||||
|
onClick={() => setTweakMode((prev) => !prev)}
|
||||||
|
className="flex items-center rounded-lg"
|
||||||
|
variant="neutral"
|
||||||
|
size="icon-sm"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
<SlidersHorizontal aria-label="tweak-btn" size={14} />
|
||||||
|
</motion.button>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{iframe ? (
|
||||||
|
<Iframe key={key} name={name} bigScreen={bigScreen} />
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex min-h-[400px] w-full items-center justify-center px-10 py-16"
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={false}
|
||||||
|
animate={{
|
||||||
|
width: isMobile ? '100%' : tweakMode ? '250px' : '0px',
|
||||||
|
height: isMobile ? (tweakMode ? '250px' : '0px') : 'auto',
|
||||||
|
opacity: tweakMode ? 1 : 0,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
stiffness: 300,
|
||||||
|
damping: 30,
|
||||||
|
restDelta: 0.01,
|
||||||
|
}}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 overflow-y-auto">{tweakpane}</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
32
apps/www/components/docs/docs-author.tsx
Normal file
32
apps/www/components/docs/docs-author.tsx
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
|
||||||
|
interface DocsAuthorProps {
|
||||||
|
name: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameClassName =
|
||||||
|
'text-foreground underline underline-offset-2 decoration-blue-500 font-medium';
|
||||||
|
|
||||||
|
export const DocsAuthor = ({ name, url }: DocsAuthorProps) => {
|
||||||
|
return (
|
||||||
|
<span className={'text-sm text-fd-muted-foreground italic'}>
|
||||||
|
Made by{' '}
|
||||||
|
{url ? (
|
||||||
|
<a
|
||||||
|
className={cn(
|
||||||
|
nameClassName,
|
||||||
|
'cursor-pointer hover:decoration-foreground',
|
||||||
|
)}
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span className={nameClassName}>{name}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
43
apps/www/components/docs/docs-breadcrumb.tsx
Normal file
43
apps/www/components/docs/docs-breadcrumb.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Breadcrumb,
|
||||||
|
BreadcrumbItem,
|
||||||
|
BreadcrumbLink,
|
||||||
|
BreadcrumbList,
|
||||||
|
BreadcrumbSeparator,
|
||||||
|
} from '@workspace/ui/components/ui/breadcrumb';
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
|
||||||
|
export const DocsBreadcrumb = ({ slug }: { slug?: string[] }) => {
|
||||||
|
return (
|
||||||
|
<Breadcrumb>
|
||||||
|
<BreadcrumbList>
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink href="/docs">Docs</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
|
||||||
|
{slug &&
|
||||||
|
slug.length > 0 &&
|
||||||
|
slug.map((item, index) => (
|
||||||
|
<React.Fragment key={item}>
|
||||||
|
<BreadcrumbSeparator />
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<BreadcrumbLink
|
||||||
|
className={cn(
|
||||||
|
'capitalize',
|
||||||
|
index === slug.length - 1 && 'text-foreground',
|
||||||
|
)}
|
||||||
|
href={
|
||||||
|
index === slug.length - 1 ? `/docs/${slug.join('/')}` : `#`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item.replace(/-/g, ' ')}
|
||||||
|
</BreadcrumbLink>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</BreadcrumbList>
|
||||||
|
</Breadcrumb>
|
||||||
|
);
|
||||||
|
};
|
||||||
66
apps/www/components/docs/dynamic-codeblock.tsx
Normal file
66
apps/www/components/docs/dynamic-codeblock.tsx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { CodeBlock, Pre } from '@/components/docs/codeblock';
|
||||||
|
import type {
|
||||||
|
HighlightOptionsCommon,
|
||||||
|
HighlightOptionsThemes,
|
||||||
|
} from 'fumadocs-core/highlight';
|
||||||
|
import { useShiki } from 'fumadocs-core/highlight/client';
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
|
||||||
|
const getComponents = ({
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
onCopy,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
title?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
onCopy?: () => void;
|
||||||
|
className?: string;
|
||||||
|
}) =>
|
||||||
|
({
|
||||||
|
pre(props) {
|
||||||
|
return (
|
||||||
|
<CodeBlock
|
||||||
|
{...props}
|
||||||
|
title={title}
|
||||||
|
icon={icon}
|
||||||
|
onCopy={onCopy}
|
||||||
|
className={cn('my-0', props.className, className)}
|
||||||
|
>
|
||||||
|
<Pre>{props.children}</Pre>
|
||||||
|
</CodeBlock>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}) satisfies HighlightOptionsCommon['components'];
|
||||||
|
|
||||||
|
export function DynamicCodeBlock({
|
||||||
|
lang,
|
||||||
|
code,
|
||||||
|
options,
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
onCopy,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
lang: string;
|
||||||
|
code: string;
|
||||||
|
title?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
onCopy?: () => void;
|
||||||
|
options?: Omit<HighlightOptionsCommon, 'lang'> & HighlightOptionsThemes;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const components = getComponents({ title, icon, onCopy, className });
|
||||||
|
|
||||||
|
return useShiki(code, {
|
||||||
|
lang,
|
||||||
|
...options,
|
||||||
|
components: {
|
||||||
|
...components,
|
||||||
|
...options?.components,
|
||||||
|
},
|
||||||
|
withPrerenderScript: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
26
apps/www/components/docs/external-link.tsx
Normal file
26
apps/www/components/docs/external-link.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ExternalLinkIcon } from 'lucide-react';
|
||||||
|
import { motion } from 'motion/react';
|
||||||
|
|
||||||
|
export const ExternalLink = ({
|
||||||
|
href,
|
||||||
|
text,
|
||||||
|
}: {
|
||||||
|
href: string;
|
||||||
|
text: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<motion.a
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
href={href}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="not-prose w-fit flex flex-row items-center rounded-md bg-muted hover:bg-muted/70 transition pl-3 pr-2.5 py-1 text-sm font-medium text-muted-foreground"
|
||||||
|
>
|
||||||
|
<span>{text}</span>
|
||||||
|
<ExternalLinkIcon className="ml-1.5 h-4 w-4" />
|
||||||
|
</motion.a>
|
||||||
|
);
|
||||||
|
};
|
||||||
22
apps/www/components/docs/icon-showcase.tsx
Normal file
22
apps/www/components/docs/icon-showcase.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import type { IconProps } from '@/registry/icons/icon';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const IconShowcase = ({
|
||||||
|
icon: Icon,
|
||||||
|
displayTitle = true,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
icon: React.ComponentType<IconProps<string>>;
|
||||||
|
displayTitle?: boolean;
|
||||||
|
} & IconProps<string>) => {
|
||||||
|
return (
|
||||||
|
<div className="relative h-[200px] w-full lg:w-[250px] max-w-[250px] mx-auto rounded-2xl aspect-square bg-muted/50 border flex items-center justify-center">
|
||||||
|
{props.animation && displayTitle ? (
|
||||||
|
<p className="absolute whitespace-nowrap -top-4.5 py-1.5 px-3 bg-border rounded-b-lg left-1/2 -translate-x-1/2 text-sm text-muted-foreground">
|
||||||
|
{props.animation}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<Icon animate className="text-current size-[100px]" {...props} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
313
apps/www/components/docs/icons.tsx
Normal file
313
apps/www/components/docs/icons.tsx
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
import { index } from '@/__registry__';
|
||||||
|
import { MotionHighlight } from '@/registry/effects/motion-highlight';
|
||||||
|
import { AnimateIcon, staticAnimations } from '@/registry/icons/icon';
|
||||||
|
import { X } from '@/registry/icons/x';
|
||||||
|
import { Input } from '@workspace/ui/components/ui/input';
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { motion } from 'motion/react';
|
||||||
|
import { CodeTabs } from '@/registry/components/code-tabs';
|
||||||
|
import { DynamicCodeBlock } from './dynamic-codeblock';
|
||||||
|
import {
|
||||||
|
Tabs,
|
||||||
|
TabsList,
|
||||||
|
TabsTrigger,
|
||||||
|
TabsContent,
|
||||||
|
TabsContents,
|
||||||
|
} from '@/registry/components/tabs';
|
||||||
|
import ReactIcon from '@workspace/ui/components/icons/react-icon';
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@workspace/ui/components/ui/select';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/registry/components/tooltip';
|
||||||
|
import { Button } from '@workspace/ui/components/ui/button';
|
||||||
|
import { RotateCcw } from '@/registry/icons/rotate-ccw';
|
||||||
|
|
||||||
|
const staticAnimationsLength = Object.keys(staticAnimations).length;
|
||||||
|
|
||||||
|
export const Icons = () => {
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
const [animationKey, setAnimationKey] = useState(0);
|
||||||
|
const [activeIcon, setActiveIcon] = useState<string | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState<string>('cli');
|
||||||
|
const [isCopied, setIsCopied] = useState(false);
|
||||||
|
const [activeAnimation, setActiveAnimation] = useState<string>('default');
|
||||||
|
|
||||||
|
const icons = Object.values(index).filter((icon) =>
|
||||||
|
icon.name.endsWith('-icon'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const fuse = useMemo(() => {
|
||||||
|
return new Fuse(icons, {
|
||||||
|
keys: ['name', 'keywords'],
|
||||||
|
threshold: 0.3,
|
||||||
|
ignoreLocation: true,
|
||||||
|
});
|
||||||
|
}, [icons]);
|
||||||
|
|
||||||
|
const filteredIcons = useMemo(() => {
|
||||||
|
const q = search.trim();
|
||||||
|
if (!q) return icons;
|
||||||
|
return fuse.search(q).map((result) => result.item);
|
||||||
|
}, [search, fuse, icons]);
|
||||||
|
|
||||||
|
const icon = useMemo(
|
||||||
|
() => icons.find((icon) => icon.name === activeIcon),
|
||||||
|
[activeIcon, icons],
|
||||||
|
);
|
||||||
|
const iconName = useMemo(
|
||||||
|
() =>
|
||||||
|
icon?.name
|
||||||
|
.replace('-icon', '')
|
||||||
|
.split('-')
|
||||||
|
.map((word: string) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||||
|
.join(''),
|
||||||
|
[icon],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setActiveAnimation('default');
|
||||||
|
}, [activeIcon]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="-mt-4.5 text-black dark:text-white">
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{filteredIcons.length} icons {search.length ? 'found' : 'available'}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder="Search icons"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{filteredIcons.length ? (
|
||||||
|
<div className="grid lg:grid-cols-11 sm:grid-cols-9 xs:grid-cols-7 grid-cols-5 gap-4 mt-6">
|
||||||
|
<TooltipProvider>
|
||||||
|
<MotionHighlight
|
||||||
|
hover
|
||||||
|
className="ring-2 ring-blue-500 bg-transparent rounded-lg -z-1"
|
||||||
|
>
|
||||||
|
{filteredIcons.map((icon) => {
|
||||||
|
const animationsLength = Object.keys(
|
||||||
|
icon?.component?.animations ?? {},
|
||||||
|
).length;
|
||||||
|
return (
|
||||||
|
<Tooltip side="bottom" key={icon.name}>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<div>
|
||||||
|
<AnimateIcon animateOnHover>
|
||||||
|
<button
|
||||||
|
data-value={icon.name}
|
||||||
|
onClick={() => setActiveIcon(icon.name)}
|
||||||
|
className="relative group flex items-center justify-center size-full aspect-square rounded-lg p-3.5"
|
||||||
|
>
|
||||||
|
{icon?.component && (
|
||||||
|
<icon.component className="text-current size-full" />
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'absolute inset-0 bg-muted rounded-lg -z-2 transition-colors duration-200',
|
||||||
|
activeIcon === icon.name && 'bg-blue-500/20',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="absolute -bottom-2.5 -right-2.5 flex items-center justify-center text-muted-foreground font-medium size-5 bg-background border group-hover:border-blue-500 group-hover:ring group-hover:ring-blue-500 transition-colors duration-200 rounded-full">
|
||||||
|
<span className="text-[11px] leading-none">
|
||||||
|
{staticAnimationsLength + animationsLength}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</AnimateIcon>
|
||||||
|
</div>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{icon.name.replace('-icon', '')}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</MotionHighlight>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-center justify-center h-[200px]">
|
||||||
|
<p className="text-sm text-muted-foreground">No icons found</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="fixed z-50 w-[325px] right-0 inset-y-12 rounded-l-2xl border-l border-y bg-background shadow-sm p-4"
|
||||||
|
initial={{ opacity: 0, x: '100%' }}
|
||||||
|
animate={activeIcon ? { opacity: 1, x: 0 } : { opacity: 0, x: '100%' }}
|
||||||
|
exit={{ opacity: 0, x: '100%' }}
|
||||||
|
transition={{ type: 'spring', stiffness: 150, damping: 25 }}
|
||||||
|
>
|
||||||
|
<h2 className="text-lg font-medium mt-1.5">
|
||||||
|
{activeIcon?.replace('-icon', '')}
|
||||||
|
</h2>
|
||||||
|
<AnimateIcon animateOnHover>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveIcon(null)}
|
||||||
|
className="absolute cursor-pointer top-5 right-5 size-8 rounded-full flex items-center justify-center bg-background hover:bg-muted transition-colors duration-200"
|
||||||
|
>
|
||||||
|
<X className="size-5 text-neutral-500" />
|
||||||
|
</button>
|
||||||
|
</AnimateIcon>
|
||||||
|
|
||||||
|
<div className="h-[calc(100%-3.25rem)] overflow-y-auto">
|
||||||
|
<div className="h-full flex flex-col justify-between gap-y-4">
|
||||||
|
<div>
|
||||||
|
<Tabs
|
||||||
|
value={activeTab}
|
||||||
|
onValueChange={(value) => setActiveTab(value)}
|
||||||
|
className="gap-0"
|
||||||
|
>
|
||||||
|
<div className="w-full flex justify-between items-center mb-3">
|
||||||
|
<h3 className="text-base font-medium pt-0 pb-0 mt-0 mb-0">
|
||||||
|
Installation
|
||||||
|
</h3>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="cli" className="w-[70px]">
|
||||||
|
CLI
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="manual" className="w-[70px]">
|
||||||
|
Manual
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TabsContents>
|
||||||
|
<TabsContent value="cli">
|
||||||
|
<CodeTabs
|
||||||
|
codes={{
|
||||||
|
npm: `npx shadcn@latest add "https://animate-ui.com/r/${activeIcon}.json"`,
|
||||||
|
pnpm: `pnpm dlx shadcn@latest add "https://animate-ui.com/r/${activeIcon}.json"`,
|
||||||
|
yarn: `npx shadcn@latest add "https://animate-ui.com/r/${activeIcon}.json"`,
|
||||||
|
bun: `bun x --bun shadcn@latest add "https://animate-ui.com/r/${activeIcon}.json"`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="manual" className="relative group">
|
||||||
|
{activeIcon && (
|
||||||
|
<DynamicCodeBlock
|
||||||
|
code={icon?.files?.[0]?.content}
|
||||||
|
lang="jsx"
|
||||||
|
title={`${icon?.name}.tsx`}
|
||||||
|
icon={<ReactIcon />}
|
||||||
|
className="max-h-[92px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(
|
||||||
|
icon?.files?.[0]?.content ?? '',
|
||||||
|
);
|
||||||
|
setIsCopied(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
}}
|
||||||
|
className="absolute cursor-pointer inset-px top-[41px] rounded-b-[13px] bg-black/20 invisible group-hover:visible opacity-0 group-hover:opacity-100 transition-all duration-200 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<p className="text-sm font-medium text-white">
|
||||||
|
{isCopied ? 'Copied' : 'Copy'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</TabsContents>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<h3 className="text-base font-medium mt-4">Usage</h3>
|
||||||
|
{activeIcon && (
|
||||||
|
<DynamicCodeBlock
|
||||||
|
code={`<${iconName} animateOnHover />
|
||||||
|
// Or use with the AnimateIcon component
|
||||||
|
<AnimateIcon animateOnHover>
|
||||||
|
<${iconName} />
|
||||||
|
</AnimateIcon>`}
|
||||||
|
lang="jsx"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{activeIcon && (
|
||||||
|
<>
|
||||||
|
<div className="relative h-[200px] w-full mx-auto rounded-2xl aspect-square bg-muted/50 border flex items-center justify-center">
|
||||||
|
{icon?.component && (
|
||||||
|
<icon.component
|
||||||
|
key={`${activeAnimation}-${activeIcon}-${animationKey}`}
|
||||||
|
animate
|
||||||
|
animation={activeAnimation}
|
||||||
|
className="text-current size-[100px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AnimateIcon animateOnHover>
|
||||||
|
<Button
|
||||||
|
size="icon-sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="absolute right-2 top-2 z-[2] backdrop-blur-md bg-transparent hover:bg-black/5 dark:hover:bg-white/10 size-6"
|
||||||
|
onClick={() => setAnimationKey((prev) => prev + 1)}
|
||||||
|
>
|
||||||
|
<RotateCcw className="size-3.5" />
|
||||||
|
</Button>
|
||||||
|
</AnimateIcon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
value={activeAnimation}
|
||||||
|
onValueChange={(value) => setActiveAnimation(value)}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full !h-11 px-1.5 rounded-lg">
|
||||||
|
<SelectValue placeholder="Select an animation" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<div className="space-y-1.5 p-0.5">
|
||||||
|
{Object.keys({
|
||||||
|
...staticAnimations,
|
||||||
|
...(icon?.component?.animations ?? {}),
|
||||||
|
}).map((animation) => (
|
||||||
|
<SelectItem
|
||||||
|
key={animation}
|
||||||
|
value={animation}
|
||||||
|
className="!h-8 rounded-md px-0 focus:bg-muted"
|
||||||
|
>
|
||||||
|
<div className="gap-2 flex items-center">
|
||||||
|
<div className="size-8 rounded-md p-1.5 bg-muted">
|
||||||
|
{icon?.component && (
|
||||||
|
<icon.component className="text-current size-full" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span>{animation}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
28
apps/www/components/docs/iframe.tsx
Normal file
28
apps/www/components/docs/iframe.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export default function Iframe({
|
||||||
|
name,
|
||||||
|
bigScreen = false,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
bigScreen?: boolean;
|
||||||
|
}) {
|
||||||
|
const [iframeUrl, setIframeUrl] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const origin = window.location.origin;
|
||||||
|
setIframeUrl(`${origin}/examples/${name}`);
|
||||||
|
}, [name]);
|
||||||
|
|
||||||
|
if (!iframeUrl) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<iframe
|
||||||
|
src={iframeUrl}
|
||||||
|
className={cn('h-[500px] rounded-xl', bigScreen && 'w-[1600px]')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
795
apps/www/components/docs/motion-grid-editor.tsx
Normal file
795
apps/www/components/docs/motion-grid-editor.tsx
Normal file
@ -0,0 +1,795 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type Frame,
|
||||||
|
type Frames,
|
||||||
|
MotionGrid,
|
||||||
|
} from '@/registry/components/motion-grid';
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from '@/registry/components/tooltip';
|
||||||
|
import { Trash2Icon } from '@/registry/icons/trash-2';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/registry/radix/dropdown-menu';
|
||||||
|
import { Button } from '@workspace/ui/components/ui/button';
|
||||||
|
import { Input } from '@workspace/ui/components/ui/input';
|
||||||
|
import { Label } from '@workspace/ui/components/ui/label';
|
||||||
|
import { ScrollArea } from '@workspace/ui/components/ui/scroll-area';
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
import {
|
||||||
|
ArrowLeftIcon,
|
||||||
|
ArrowRightIcon,
|
||||||
|
CopyIcon,
|
||||||
|
PlusIcon,
|
||||||
|
RotateCcwIcon,
|
||||||
|
SaveIcon,
|
||||||
|
CheckIcon,
|
||||||
|
XIcon,
|
||||||
|
Timer,
|
||||||
|
SquareRoundCorner,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
const GRID_SIZE = [7, 7] as [number, number];
|
||||||
|
const GRID_SIZE_MAX = 20;
|
||||||
|
const GRID_SIZE_MIN = 4;
|
||||||
|
const DEFAULT_DURATION = '200';
|
||||||
|
const DEFAULT_BORDER_RADIUS = '100';
|
||||||
|
const DEFAULT_BORDER_RADIUS_UNIT = '%';
|
||||||
|
const BORDER_RADIUS_UNITS = ['px', 'rem', 'em', '%'];
|
||||||
|
|
||||||
|
const formatGridSizeNumber = (value: number) => {
|
||||||
|
if (value < GRID_SIZE_MIN) return GRID_SIZE_MIN;
|
||||||
|
if (value > GRID_SIZE_MAX) return GRID_SIZE_MAX;
|
||||||
|
return Math.round(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MyAnimation = ({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
selectAnimation,
|
||||||
|
deleteAnimation,
|
||||||
|
active,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
value: {
|
||||||
|
gridSize: [number, number];
|
||||||
|
frames: Frames;
|
||||||
|
duration: string;
|
||||||
|
borderRadius: string;
|
||||||
|
borderRadiusUnit: string;
|
||||||
|
};
|
||||||
|
selectAnimation: () => void;
|
||||||
|
deleteAnimation: () => void;
|
||||||
|
active: boolean;
|
||||||
|
}) => {
|
||||||
|
const [isDeleting, setIsDeleting] = React.useState<boolean>(false);
|
||||||
|
const [isHovering, setIsHovering] = React.useState<boolean>(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={selectAnimation}
|
||||||
|
onMouseEnter={() => setIsHovering(true)}
|
||||||
|
onMouseLeave={() => setIsHovering(false)}
|
||||||
|
className={cn(
|
||||||
|
'group flex flex-row gap-3 items-center hover:bg-neutral-100 dark:hover:bg-neutral-900 cursor-pointer rounded-xl p-2',
|
||||||
|
active && 'bg-neutral-100 dark:bg-neutral-900',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex size-20 shrink-0 aspect-square justify-center items-center">
|
||||||
|
<MotionGrid
|
||||||
|
className={cn(
|
||||||
|
'dark:bg-neutral-900 bg-neutral-100 group-hover:ring-2 group-hover:ring-neutral-200 dark:group-hover:ring-neutral-800 rounded-md p-1.5',
|
||||||
|
active && 'ring-2 ring-neutral-300 dark:ring-neutral-700',
|
||||||
|
value.gridSize[0] > value.gridSize[1]
|
||||||
|
? 'w-20 h-auto'
|
||||||
|
: 'h-20 w-auto',
|
||||||
|
Math.max(value.gridSize[0], value.gridSize[1]) > 10
|
||||||
|
? 'gap-px'
|
||||||
|
: 'gap-0.5',
|
||||||
|
)}
|
||||||
|
cellProps={{
|
||||||
|
style: {
|
||||||
|
borderRadius: `${value.borderRadius}${value.borderRadiusUnit}`,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
duration={Number(value.duration)}
|
||||||
|
animate={isHovering}
|
||||||
|
cellClassName="!size-full"
|
||||||
|
cellActiveClassName="bg-neutral-800 dark:bg-neutral-200"
|
||||||
|
cellInactiveClassName="bg-neutral-200 dark:bg-neutral-800"
|
||||||
|
gridSize={value.gridSize}
|
||||||
|
frames={value.frames}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2 justify-between items-center w-full">
|
||||||
|
<div className="flex flex-col gap-1 text-left">
|
||||||
|
<Label className="text-sm">{name}</Label>
|
||||||
|
<p className="text-xs text-neutral-500 mt-0 mb-0">
|
||||||
|
{value.gridSize[0]}x{value.gridSize[1]} • {value.frames.length}{' '}
|
||||||
|
frames
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isDeleting ? (
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<Button
|
||||||
|
size="icon-xs"
|
||||||
|
variant="neutral"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDeleting(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="icon-xs"
|
||||||
|
variant="destructive"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteAnimation();
|
||||||
|
setIsDeleting(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
size="icon-xs"
|
||||||
|
variant="neutral"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDeleting(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2Icon />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Animation {
|
||||||
|
gridSize: [number, number];
|
||||||
|
frames: Frames;
|
||||||
|
duration: string;
|
||||||
|
borderRadius: string;
|
||||||
|
borderRadiusUnit: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MotionGridEditor = () => {
|
||||||
|
const [gridSizeInput, setGridSizeInput] = React.useState<[string, string]>(
|
||||||
|
GRID_SIZE.map((n) => n.toString()) as [string, string],
|
||||||
|
);
|
||||||
|
const [gridSize, setGridSize] = React.useState<[number, number]>(GRID_SIZE);
|
||||||
|
const [frames, setFrames] = React.useState<Frames>([[]]);
|
||||||
|
const [activeFrame, setActiveFrame] = React.useState<number>(0);
|
||||||
|
const [isCopied, setIsCopied] = React.useState<boolean>(false);
|
||||||
|
const [isSaved, setIsSaved] = React.useState<boolean>(false);
|
||||||
|
const [animationName, setAnimationName] = React.useState<string>('');
|
||||||
|
const [selectedAnimation, setSelectedAnimation] = React.useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [animations, setAnimations] = React.useState<Record<string, Animation>>(
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
const [isDrawing, setIsDrawing] = React.useState(false);
|
||||||
|
const [drawAction, setDrawAction] = React.useState<'add' | 'remove' | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [duration, setDuration] = React.useState<string>(DEFAULT_DURATION);
|
||||||
|
const [borderRadius, setBorderRadius] = React.useState<string>(
|
||||||
|
DEFAULT_BORDER_RADIUS,
|
||||||
|
);
|
||||||
|
const [borderRadiusUnit, setBorderRadiusUnit] = React.useState<string>('px');
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleUp = () => {
|
||||||
|
setIsDrawing(false);
|
||||||
|
setDrawAction(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('mouseup', handleUp);
|
||||||
|
return () => window.removeEventListener('mouseup', handleUp);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const normalizeAnimation = (anim: Partial<Animation>): Animation =>
|
||||||
|
({
|
||||||
|
...anim,
|
||||||
|
duration: anim.duration ?? DEFAULT_DURATION,
|
||||||
|
borderRadius: anim.borderRadius ?? DEFAULT_BORDER_RADIUS,
|
||||||
|
borderRadiusUnit: anim.borderRadiusUnit ?? DEFAULT_BORDER_RADIUS_UNIT,
|
||||||
|
}) as Animation;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const raw = localStorage.getItem('animations');
|
||||||
|
const parsed: Record<string, Partial<Animation>> = raw
|
||||||
|
? JSON.parse(raw)
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const fixed: Record<string, Animation> = Object.fromEntries(
|
||||||
|
Object.entries(parsed).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
normalizeAnimation(value),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
setAnimations(fixed);
|
||||||
|
|
||||||
|
localStorage.setItem('animations', JSON.stringify(fixed));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const activeFrameDots = new Set<number>(
|
||||||
|
frames[activeFrame]?.map(([x, y]) => y * gridSize[0]! + x) ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const applyDot = (x: number, y: number, action: 'add' | 'remove') => {
|
||||||
|
setFrames((prev) => {
|
||||||
|
const clone = [...prev];
|
||||||
|
const current = clone[activeFrame]!;
|
||||||
|
const idx = current.findIndex(([px, py]) => px === x && py === y);
|
||||||
|
|
||||||
|
if (action === 'add' && idx === -1) {
|
||||||
|
clone[activeFrame] = [...current, [x, y]];
|
||||||
|
} else if (action === 'remove' && idx !== -1) {
|
||||||
|
clone[activeFrame] = [
|
||||||
|
...current.slice(0, idx),
|
||||||
|
...current.slice(idx + 1),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveCurrentFrame = (direction: -1 | 1) => {
|
||||||
|
if (frames.length <= 1) return;
|
||||||
|
|
||||||
|
setFrames((prev) => {
|
||||||
|
const clone = [...prev];
|
||||||
|
const len = clone.length;
|
||||||
|
const newIndex = (activeFrame + direction + len) % len;
|
||||||
|
|
||||||
|
const [moved] = clone.splice(activeFrame, 1);
|
||||||
|
clone.splice(newIndex, 0, moved!);
|
||||||
|
|
||||||
|
setActiveFrame(newIndex);
|
||||||
|
return clone;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeDotNotInGrid = () => {
|
||||||
|
setFrames((prev) =>
|
||||||
|
prev.map((frame) =>
|
||||||
|
frame.filter(([x, y]) => x < gridSize[0] && y < gridSize[1]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const createNewAnimation = () => {
|
||||||
|
setGridSize(GRID_SIZE);
|
||||||
|
setGridSizeInput(GRID_SIZE.map((n) => n.toString()) as [string, string]);
|
||||||
|
setDuration(DEFAULT_DURATION);
|
||||||
|
setBorderRadius(DEFAULT_BORDER_RADIUS);
|
||||||
|
setBorderRadiusUnit(DEFAULT_BORDER_RADIUS_UNIT);
|
||||||
|
setFrames([[]]);
|
||||||
|
setActiveFrame(0);
|
||||||
|
setAnimationName('');
|
||||||
|
setIsSaved(false);
|
||||||
|
setIsCopied(false);
|
||||||
|
setSelectedAnimation(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-12 gap-4 lg:h-[506px]">
|
||||||
|
<div className="lg:col-span-5 col-span-12 border rounded-xl overflow-hidden flex flex-col min-h-0 h-[506px]">
|
||||||
|
<div className="relative flex flex-row p-4 bg-muted border-b border-border/75 dark:border-border/50">
|
||||||
|
<Label>My Animations</Label>
|
||||||
|
<Button
|
||||||
|
size="icon-xs"
|
||||||
|
variant="neutral"
|
||||||
|
className="absolute top-1/2 -translate-y-1/2 right-2"
|
||||||
|
onClick={createNewAnimation}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 flex flex-col gap-y-2 justify-between overflow-y-auto p-2">
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
{Object.entries(animations).map(([name, value]) => (
|
||||||
|
<MyAnimation
|
||||||
|
key={name}
|
||||||
|
name={name}
|
||||||
|
value={value}
|
||||||
|
selectAnimation={() => {
|
||||||
|
setGridSize(value.gridSize);
|
||||||
|
setFrames(value.frames);
|
||||||
|
setDuration(value.duration);
|
||||||
|
setBorderRadius(value.borderRadius);
|
||||||
|
setBorderRadiusUnit(value.borderRadiusUnit);
|
||||||
|
setActiveFrame(0);
|
||||||
|
setAnimationName(name);
|
||||||
|
setIsSaved(false);
|
||||||
|
setIsCopied(false);
|
||||||
|
setGridSizeInput(
|
||||||
|
value.gridSize.map((n) => n.toString()) as [string, string],
|
||||||
|
);
|
||||||
|
setSelectedAnimation(name);
|
||||||
|
}}
|
||||||
|
deleteAnimation={() => {
|
||||||
|
if (selectedAnimation === name) createNewAnimation();
|
||||||
|
const newAnimations = { ...animations };
|
||||||
|
delete newAnimations[name];
|
||||||
|
setAnimations(newAnimations);
|
||||||
|
localStorage.setItem(
|
||||||
|
'animations',
|
||||||
|
JSON.stringify(newAnimations),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
active={selectedAnimation === name}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<span className="text-xs text-neutral-400 dark:text-neutral-600 text-center italic">
|
||||||
|
Animations stored in local storage
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="lg:col-span-7 col-span-12 border rounded-xl overflow-hidden">
|
||||||
|
<div className="relative flex flex-row justify-between items-center gap-2 p-4 bg-muted border-b border-border/75 dark:border-border/50">
|
||||||
|
<Label className="whitespace-nowrap">
|
||||||
|
{selectedAnimation ? 'Edit Animation' : 'Create New Animation'}
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex flex-row gap-1 items-center">
|
||||||
|
<Input
|
||||||
|
placeholder="X"
|
||||||
|
type="number"
|
||||||
|
className="border-none h-8 text-sm max-w-8 p-0 bg-white text-center dark:bg-neutral-900 shadow-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
value={gridSizeInput[0]}
|
||||||
|
onChange={(e) => {
|
||||||
|
setGridSizeInput((prev) => [e.target.value, prev[1]]);
|
||||||
|
setGridSize((prev) => [
|
||||||
|
formatGridSizeNumber(Number(e.target.value)),
|
||||||
|
prev[1],
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setGridSizeInput(
|
||||||
|
gridSize.map((n) => n.toString()) as [string, string],
|
||||||
|
);
|
||||||
|
removeDotNotInGrid();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<XIcon className="size-3 text-neutral-500 stroke-3" />
|
||||||
|
<Input
|
||||||
|
placeholder="Y"
|
||||||
|
type="number"
|
||||||
|
className="border-none h-8 text-sm max-w-8 p-0 bg-white text-center dark:bg-neutral-900 shadow-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
value={gridSizeInput[1]}
|
||||||
|
onChange={(e) => {
|
||||||
|
setGridSizeInput((prev) => [prev[0], e.target.value]);
|
||||||
|
setGridSize((prev) => [
|
||||||
|
prev[0],
|
||||||
|
formatGridSizeNumber(Number(e.target.value)),
|
||||||
|
]);
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setGridSizeInput(
|
||||||
|
gridSize.map((n) => n.toString()) as [string, string],
|
||||||
|
);
|
||||||
|
removeDotNotInGrid();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="p-4 relative">
|
||||||
|
<div className="flex size-20 shrink-0 aspect-square justify-end items-start absolute right-4 top-4">
|
||||||
|
<MotionGrid
|
||||||
|
className={cn(
|
||||||
|
'dark:bg-neutral-900 bg-neutral-100 rounded-md p-1.5',
|
||||||
|
gridSize[0] > gridSize[1] ? 'w-20 h-auto' : 'h-20 w-auto',
|
||||||
|
Math.max(gridSize[0], gridSize[1]) > 10
|
||||||
|
? 'gap-px'
|
||||||
|
: 'gap-0.5',
|
||||||
|
)}
|
||||||
|
cellProps={{
|
||||||
|
style: {
|
||||||
|
borderRadius: `${borderRadius}${borderRadiusUnit}`,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
duration={Number(duration)}
|
||||||
|
cellClassName="!size-full"
|
||||||
|
cellActiveClassName="bg-neutral-800 dark:bg-neutral-200"
|
||||||
|
cellInactiveClassName="bg-neutral-200 dark:bg-neutral-800"
|
||||||
|
gridSize={gridSize}
|
||||||
|
frames={frames}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-[150px] sm:max-w-[200px] flex items-center justify-center aspect-square mx-auto">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'grid w-fit p-3 dark:bg-neutral-900 bg-neutral-100 rounded-xl',
|
||||||
|
gridSize[0] > gridSize[1]
|
||||||
|
? 'w-[150px] sm:w-[200px]'
|
||||||
|
: 'h-[150px] sm:h-[200px]',
|
||||||
|
Math.max(gridSize[0], gridSize[1]) > 10
|
||||||
|
? 'gap-0.5'
|
||||||
|
: Math.max(gridSize[0], gridSize[1]) > 10
|
||||||
|
? 'gap-1'
|
||||||
|
: 'gap-1.5',
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${gridSize[0]}, minmax(0, 1fr))`,
|
||||||
|
gridAutoRows: '1fr',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from({ length: gridSize[0]! * gridSize[1]! }).map(
|
||||||
|
(_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
onMouseDown={() => {
|
||||||
|
const x = i % gridSize[0]!;
|
||||||
|
const y = Math.floor(i / gridSize[0]!);
|
||||||
|
const action: 'add' | 'remove' = activeFrameDots.has(i)
|
||||||
|
? 'remove'
|
||||||
|
: 'add';
|
||||||
|
|
||||||
|
setDrawAction(action);
|
||||||
|
setIsDrawing(true);
|
||||||
|
applyDot(x, y, action);
|
||||||
|
}}
|
||||||
|
onMouseEnter={() => {
|
||||||
|
if (!isDrawing || !drawAction) return;
|
||||||
|
const x = i % gridSize[0]!;
|
||||||
|
const y = Math.floor(i / gridSize[0]!);
|
||||||
|
applyDot(x, y, drawAction);
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
'rounded-full aspect-square hover:ring hover:ring-neutral-300 dark:hover:ring-neutral-700',
|
||||||
|
activeFrameDots.has(i)
|
||||||
|
? 'bg-neutral-800 dark:bg-neutral-200'
|
||||||
|
: 'bg-neutral-200 dark:bg-neutral-800',
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
borderRadius: `${borderRadius}${borderRadiusUnit}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TooltipProvider>
|
||||||
|
<div className="p-4 space-y-4 pt-0">
|
||||||
|
<div className="h-20 w-full flex flex-row items-center gap-2">
|
||||||
|
<ScrollArea className="w-full h-full flex-1 bg-neutral-100 dark:bg-neutral-900 rounded-2xl overflow-x-auto">
|
||||||
|
<div className="w-max p-2 h-full">
|
||||||
|
<div className="flex h-full flex-row gap-2 items-center">
|
||||||
|
{frames.map((_, index) => {
|
||||||
|
const activeDot = new Set<number>(
|
||||||
|
frames[index]?.map(
|
||||||
|
([x, y]) => y * gridSize[0]! + x,
|
||||||
|
) ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className={cn(
|
||||||
|
'h-full cursor-pointer shrink-0 bg-background rounded-lg',
|
||||||
|
activeFrame === index
|
||||||
|
? 'ring-2 ring-neutral-300 dark:ring-neutral-700'
|
||||||
|
: '',
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveFrame(index)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'grid p-[5px] h-full',
|
||||||
|
Math.max(gridSize[0], gridSize[1]) > 10
|
||||||
|
? 'gap-[0.5px]'
|
||||||
|
: Math.max(gridSize[0], gridSize[1]) > 10
|
||||||
|
? 'gap-px'
|
||||||
|
: 'gap-[1.5px]',
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
gridTemplateColumns: `repeat(${gridSize[0]}, minmax(0, 1fr))`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Array.from({
|
||||||
|
length: gridSize[0]! * gridSize[1]!,
|
||||||
|
}).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className={cn(
|
||||||
|
'bg-neutral-200 dark:bg-neutral-800 aspect-square w-full',
|
||||||
|
activeDot.has(i)
|
||||||
|
? 'bg-neutral-800 dark:bg-neutral-200'
|
||||||
|
: 'bg-neutral-200 dark:bg-neutral-800',
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
borderRadius: `${borderRadius}${borderRadiusUnit}`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
setFrames((prev) => {
|
||||||
|
const currentIndex = activeFrame;
|
||||||
|
const currentFrame = prev[currentIndex] ?? [];
|
||||||
|
const newFrame: Frame = currentFrame.map(
|
||||||
|
([x, y]) => [x, y],
|
||||||
|
);
|
||||||
|
const updatedFrames = [...prev];
|
||||||
|
updatedFrames.splice(currentIndex + 1, 0, newFrame);
|
||||||
|
setActiveFrame(currentIndex + 1);
|
||||||
|
return updatedFrames;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Add Frame</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
variant="destructive"
|
||||||
|
disabled={frames.length <= 1}
|
||||||
|
onClick={() => {
|
||||||
|
if (frames.length <= 1) return;
|
||||||
|
setFrames((prev) => {
|
||||||
|
const newFrames = prev.filter(
|
||||||
|
(_, i) => i !== activeFrame,
|
||||||
|
);
|
||||||
|
setActiveFrame(
|
||||||
|
newFrames.length - 1 > 0
|
||||||
|
? newFrames.length - 1
|
||||||
|
: 0,
|
||||||
|
);
|
||||||
|
return newFrames;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Trash2Icon />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Remove Frame</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
className="h-8"
|
||||||
|
variant="neutral"
|
||||||
|
disabled={frames.length <= 1}
|
||||||
|
onClick={() => moveCurrentFrame(-1)}
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Move Frame Left</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
className="h-8"
|
||||||
|
variant="neutral"
|
||||||
|
disabled={frames.length <= 1}
|
||||||
|
onClick={() => moveCurrentFrame(1)}
|
||||||
|
>
|
||||||
|
<ArrowRightIcon />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Move Frame Right</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-10 flex flex-row gap-4 items-center">
|
||||||
|
<div className="relative flex-1 h-full bg-neutral-100 dark:bg-neutral-900 rounded-lg">
|
||||||
|
<div className="absolute inset-y-0 left-0 h-full aspect-square bg-neutral-200 dark:bg-neutral-800 rounded-l-lg flex items-center justify-center">
|
||||||
|
<Timer className="size-5 text-neutral-400 dark:text-neutral-600" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
className="size-full px-13 border-none bg-transparent shadow-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
value={duration}
|
||||||
|
placeholder="Duration"
|
||||||
|
step={10}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value;
|
||||||
|
if (v === '') {
|
||||||
|
setDuration('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = Number(v);
|
||||||
|
if (!Number.isNaN(n) && n >= 0) {
|
||||||
|
setDuration(v);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-y-0 right-0 h-full aspect-square bg-neutral-200 dark:bg-neutral-800 rounded-r-lg flex items-center justify-center">
|
||||||
|
<span className="text-neutral-400 dark:text-neutral-600 text-sm">
|
||||||
|
ms
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative flex-1 h-full bg-neutral-100 dark:bg-neutral-900 rounded-lg">
|
||||||
|
<div className="absolute inset-y-0 left-0 h-full aspect-square bg-neutral-200 dark:bg-neutral-800 rounded-l-lg flex items-center justify-center">
|
||||||
|
<SquareRoundCorner className="size-5 text-neutral-400 dark:text-neutral-600" />
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
className="size-full px-13 border-none bg-transparent shadow-none [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none"
|
||||||
|
value={borderRadius}
|
||||||
|
placeholder="Border Radius"
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value;
|
||||||
|
if (v === '') {
|
||||||
|
setBorderRadius('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const n = Number(v);
|
||||||
|
if (!Number.isNaN(n) && n >= 0) {
|
||||||
|
setBorderRadius(v);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<div className="absolute inset-y-0 right-0 h-full aspect-square bg-neutral-200 dark:bg-neutral-800 rounded-r-lg flex items-center justify-center">
|
||||||
|
<button className="text-neutral-400 dark:text-neutral-600 text-sm">
|
||||||
|
{borderRadiusUnit}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
{BORDER_RADIUS_UNITS.map((unit) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={unit}
|
||||||
|
onClick={() => setBorderRadiusUnit(unit)}
|
||||||
|
>
|
||||||
|
{unit}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex p-4 flex-row gap-2 border-t">
|
||||||
|
<Input
|
||||||
|
required
|
||||||
|
placeholder="Animation Name"
|
||||||
|
className="border-none bg-neutral-100 dark:bg-neutral-900 shadow-none"
|
||||||
|
value={animationName}
|
||||||
|
onChange={(e) => setAnimationName(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
variant="neutral"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => setFrames([[]])}
|
||||||
|
>
|
||||||
|
<RotateCcwIcon />
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Reset Animation</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
variant="neutral"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(JSON.stringify(frames));
|
||||||
|
setIsCopied(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsCopied(false);
|
||||||
|
}, 2000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isCopied ? <CheckIcon /> : <CopyIcon />}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
{isCopied ? 'Copied' : 'Copy Animation'}
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<Button
|
||||||
|
size="icon"
|
||||||
|
disabled={!animationName.trim()}
|
||||||
|
onClick={() => {
|
||||||
|
if (!animationName.trim())
|
||||||
|
return alert('Please enter a name');
|
||||||
|
|
||||||
|
const nameAlreadyExists = selectedAnimation
|
||||||
|
? animationName !== selectedAnimation &&
|
||||||
|
animations[animationName]
|
||||||
|
: Boolean(animations[animationName]);
|
||||||
|
|
||||||
|
if (nameAlreadyExists) {
|
||||||
|
return alert('Animation with this name already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newAnimations = { ...animations };
|
||||||
|
|
||||||
|
if (selectedAnimation) {
|
||||||
|
if (selectedAnimation !== animationName) {
|
||||||
|
delete newAnimations[selectedAnimation];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newAnimations[animationName] = {
|
||||||
|
gridSize,
|
||||||
|
frames,
|
||||||
|
duration,
|
||||||
|
borderRadius,
|
||||||
|
borderRadiusUnit,
|
||||||
|
};
|
||||||
|
|
||||||
|
setAnimations(newAnimations);
|
||||||
|
setSelectedAnimation(animationName);
|
||||||
|
localStorage.setItem(
|
||||||
|
'animations',
|
||||||
|
JSON.stringify(newAnimations),
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsSaved(true);
|
||||||
|
setTimeout(() => setIsSaved(false), 2000);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSaved ? <CheckIcon /> : <SaveIcon />}
|
||||||
|
</Button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Save Animation</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
29
apps/www/components/footer.tsx
Normal file
29
apps/www/components/footer.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
export const Footer = () => {
|
||||||
|
return (
|
||||||
|
<div className="h-[55px] border-t">
|
||||||
|
<div className="max-w-7xl mx-auto h-full">
|
||||||
|
<div className="size-full px-4 md:px-6 flex items-center justify-start prose prose-sm text-sm text-muted-foreground">
|
||||||
|
<p className="text-start truncate">
|
||||||
|
Built by{' '}
|
||||||
|
<a
|
||||||
|
href="https://x.com/imskyleen"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Skyleen
|
||||||
|
</a>
|
||||||
|
. The source code is available on{' '}
|
||||||
|
<a
|
||||||
|
href="https://github.com/animate-ui/animate-ui"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
GitHub
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
124
apps/www/components/header.tsx
Normal file
124
apps/www/components/header.tsx
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { motion } from 'motion/react';
|
||||||
|
|
||||||
|
import { Logo } from '@/components/logo';
|
||||||
|
import GithubIcon from '@workspace/ui/components/icons/github-icon';
|
||||||
|
import XIcon from '@workspace/ui/components/icons/x-icon';
|
||||||
|
import { useIsMobile } from '@workspace/ui/hooks/use-mobile';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { ThemeSwitcher } from './theme-switcher';
|
||||||
|
|
||||||
|
const LOGO_WRAPPER_VARIANTS = {
|
||||||
|
center: {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
topLeft: {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 'auto',
|
||||||
|
height: 'auto',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const logoVariants = (isScroll: boolean, isMobile: boolean) => ({
|
||||||
|
center: {
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
x: '-50%',
|
||||||
|
y: '-50%',
|
||||||
|
scale: 1,
|
||||||
|
},
|
||||||
|
topLeft: {
|
||||||
|
top: isScroll ? (isMobile ? 4 : 0) : 20,
|
||||||
|
left: isScroll ? (isMobile ? -36 : -61) : isMobile ? -36 : -43,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
scale: isScroll ? (isMobile ? 0.6 : 0.5) : 0.6,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Header = ({ transition }: { transition: boolean }) => {
|
||||||
|
const isMobile = useIsMobile();
|
||||||
|
const [isScroll, setIsScroll] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => setIsScroll(window.scrollY > 10);
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
variants={LOGO_WRAPPER_VARIANTS}
|
||||||
|
initial="center"
|
||||||
|
animate={transition ? 'topLeft' : 'center'}
|
||||||
|
transition={{ type: 'spring', stiffness: 200, damping: 30 }}
|
||||||
|
className="fixed z-40 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<motion.div className="absolute inset-x-0 top-0 h-14 z-100 w-full bg-background/70 backdrop-blur-md" />
|
||||||
|
<div className="relative max-w-7xl size-full">
|
||||||
|
<motion.div
|
||||||
|
className="absolute z-110"
|
||||||
|
variants={logoVariants(isScroll, isMobile)}
|
||||||
|
initial="center"
|
||||||
|
animate={transition ? 'topLeft' : 'center'}
|
||||||
|
transition={{ type: 'spring', stiffness: 200, damping: 30 }}
|
||||||
|
>
|
||||||
|
<Logo size={isMobile ? 'lg' : 'xl'} draw betaTag />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
initial={{
|
||||||
|
top: isScroll ? (isMobile ? 12 : 7.5) : 28,
|
||||||
|
right: -43,
|
||||||
|
opacity: 0,
|
||||||
|
}}
|
||||||
|
animate={
|
||||||
|
transition
|
||||||
|
? {
|
||||||
|
top: isScroll ? (isMobile ? 12 : 7.5) : 28,
|
||||||
|
right: 20,
|
||||||
|
opacity: 1,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
top: isScroll ? (isMobile ? 12 : 7.5) : 28,
|
||||||
|
right: -43,
|
||||||
|
opacity: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
transition={{ type: 'spring', stiffness: 200, damping: 30 }}
|
||||||
|
className="absolute z-110 flex items-center gap-x-4"
|
||||||
|
>
|
||||||
|
<div className="hidden xs:flex items-center gap-x-1">
|
||||||
|
<a
|
||||||
|
href="https://github.com/animate-ui/animate-ui"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
className="inline-flex sm:mt-1 items-center justify-center rounded-md text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 hover:bg-fd-accent hover:text-fd-accent-foreground p-1.5 [&_svg]:size-5 text-fd-muted-foreground sm:[&_svg]:size-5.5"
|
||||||
|
data-active="false"
|
||||||
|
>
|
||||||
|
<GithubIcon />
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href="https://x.com/animate_ui"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
className="inline-flex sm:mt-1 items-center justify-center rounded-md text-sm font-medium transition-colors duration-100 disabled:pointer-events-none disabled:opacity-50 hover:bg-fd-accent hover:text-fd-accent-foreground p-1.5 [&_svg]:size-5 text-fd-muted-foreground sm:[&_svg]:size-5.5"
|
||||||
|
data-active="false"
|
||||||
|
>
|
||||||
|
<XIcon />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ThemeSwitcher className="mt-1 xs:mt-0 sm:mt-1" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
200
apps/www/components/hero.tsx
Normal file
200
apps/www/components/hero.tsx
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { ArrowRightIcon, Moon, Sun } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Button } from '@workspace/ui/components/ui/button';
|
||||||
|
import { HighlightText } from '@/registry/text/highlight';
|
||||||
|
import { motion } from 'motion/react';
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from '@/registry/components/tabs';
|
||||||
|
import { Switch } from '@/registry/radix/switch';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
AccordionContent,
|
||||||
|
} from '@/registry/radix/accordion';
|
||||||
|
import { RadixProgressDemo } from '@/registry/demo/radix/progress';
|
||||||
|
import { CodeTabs } from '@/registry/components/code-tabs';
|
||||||
|
import { Checkbox } from '@/registry/radix/checkbox';
|
||||||
|
import { GitHubStarsButton } from '@/registry/buttons/github-stars';
|
||||||
|
import ReactIcon from '@workspace/ui/components/icons/react-icon';
|
||||||
|
import TSIcon from '@workspace/ui/components/icons/ts-icon';
|
||||||
|
import TailwindIcon from '@workspace/ui/components/icons/tailwind-icon';
|
||||||
|
import MotionIcon from '@workspace/ui/components/icons/motion-icon';
|
||||||
|
import ShadcnIcon from '@workspace/ui/components/icons/shadcn-icon';
|
||||||
|
|
||||||
|
const COMMANDS = {
|
||||||
|
npm: `npx shadcn@latest add "https://animate-ui.com/r/install-tabs"`,
|
||||||
|
pnpm: `pnpm dlx shadcn@latest add "https://animate-ui.com/r/install-tabs"`,
|
||||||
|
yarn: `npx shadcn@latest add "https://animate-ui.com/r/install-tabs"`,
|
||||||
|
bun: `bun x --bun shadcn@latest add "https://animate-ui.com/r/install-tabs"`,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ACCORDION_ITEMS = [
|
||||||
|
{
|
||||||
|
value: 'item-1',
|
||||||
|
title: 'What is Animate UI?',
|
||||||
|
content:
|
||||||
|
'Animate UI is an open-source distribution of React components built with TypeScript, Tailwind CSS, and Motion.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'item-2',
|
||||||
|
title: 'How is it different from other libraries?',
|
||||||
|
content:
|
||||||
|
'Instead of installing via NPM, you copy and paste the components directly. This gives you full control to modify or customize them as needed.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'item-3',
|
||||||
|
title: 'Is Animate UI free to use?',
|
||||||
|
content:
|
||||||
|
'Absolutely! Animate UI is fully open-source. You can use, modify, and adapt it to fit your needs.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Hero = () => {
|
||||||
|
const { resolvedTheme: theme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
const [checkboxChecked, setCheckboxChecked] = useState<
|
||||||
|
boolean | 'indeterminate'
|
||||||
|
>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeout(() => setCheckboxChecked(true), 1000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative mx-auto max-w-7xl px-6 pt-8 w-full flex flex-col gap-10">
|
||||||
|
<div className="lg:max-w-[50%] max-w-[700px] space-y-6 md:mt-20 mt-8">
|
||||||
|
<h1 className="text-3xl md:text-4xl lg:text-[43px] sm:text-start text-center font-semibold text-neutral-800 dark:text-white !leading-relaxed lg:!leading-snug">
|
||||||
|
Elevate your UI with fluid,{' '}
|
||||||
|
<HighlightText
|
||||||
|
transition={{ duration: 2, delay: 0.5, ease: 'easeInOut' }}
|
||||||
|
inView
|
||||||
|
text="animated components"
|
||||||
|
className="from-blue-100 to-blue-100 dark:from-blue-500 dark:to-blue-500"
|
||||||
|
/>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p className="text-base sm:text-start text-center text-neutral-500 dark:text-neutral-400 max-w-2xl">
|
||||||
|
A fully animated, open-source component distribution built with{' '}
|
||||||
|
<strong>
|
||||||
|
React, TypeScript, Tailwind CSS, Motion and Shadcn CLI
|
||||||
|
</strong>
|
||||||
|
. Browse a list of components you can install, modify, and use in your
|
||||||
|
projects.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex sm:flex-row flex-col sm:gap-5 gap-3 my-8">
|
||||||
|
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="w-full rounded-full pr-5 bg-blue-500 text-white hover:bg-blue-500/90"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/docs">
|
||||||
|
Get Started <ArrowRightIcon className="!size-5" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||||
|
<Button
|
||||||
|
size="lg"
|
||||||
|
className="w-full rounded-full"
|
||||||
|
variant="neutral"
|
||||||
|
asChild
|
||||||
|
>
|
||||||
|
<Link href="/docs/components">Browse Components</Link>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 justify-center sm:justify-start">
|
||||||
|
<ReactIcon className="size-8" />
|
||||||
|
<TSIcon className="size-8" />
|
||||||
|
<TailwindIcon className="size-8" />
|
||||||
|
<MotionIcon className="size-12" />
|
||||||
|
<ShadcnIcon className="size-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<div className="absolute top-10 right-8">
|
||||||
|
<Tabs>
|
||||||
|
<TabsList className="w-[250px]">
|
||||||
|
<TabsTrigger className="w-full" value="code">
|
||||||
|
Code
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger className="w-full" value="issues">
|
||||||
|
Issues
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger className="w-full" value="docs">
|
||||||
|
Docs
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-28 right-4">
|
||||||
|
<CodeTabs
|
||||||
|
defaultValue="pnpm"
|
||||||
|
className="max-w-[520px]"
|
||||||
|
codes={COMMANDS}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-62 right-37">
|
||||||
|
<RadixProgressDemo />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-80 right-115">
|
||||||
|
<Checkbox
|
||||||
|
className="scale-125"
|
||||||
|
checked={checkboxChecked}
|
||||||
|
onCheckedChange={(checked) => setCheckboxChecked(checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-58 right-14">
|
||||||
|
<Switch
|
||||||
|
className="scale-125"
|
||||||
|
leftIcon={<Sun />}
|
||||||
|
rightIcon={<Moon />}
|
||||||
|
checked={theme === 'dark'}
|
||||||
|
onCheckedChange={(checked) => setTheme(checked ? 'dark' : 'light')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-4 right-85">
|
||||||
|
<GitHubStarsButton repo="animate-ui" username="animate-ui" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute top-75 right-4">
|
||||||
|
<Accordion
|
||||||
|
type="single"
|
||||||
|
defaultValue="item-1"
|
||||||
|
collapsible
|
||||||
|
className="w-[400px] rounded-xl border overflow-hidden"
|
||||||
|
>
|
||||||
|
{ACCORDION_ITEMS.map((item) => (
|
||||||
|
<AccordionItem
|
||||||
|
key={item.value}
|
||||||
|
value={item.value}
|
||||||
|
className="last:border-b-0"
|
||||||
|
>
|
||||||
|
<AccordionTrigger className="text-sm px-4 py-3 bg-muted">
|
||||||
|
{item.title}
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="p-4 border-t text-muted-foreground">
|
||||||
|
{item.content}
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
))}
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
85
apps/www/components/logo.tsx
Normal file
85
apps/www/components/logo.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
import { motion, type SVGMotionProps } from 'motion/react';
|
||||||
|
|
||||||
|
const pathVariants = {
|
||||||
|
hidden: {
|
||||||
|
pathLength: 0,
|
||||||
|
fillOpacity: 0,
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
pathLength: 1,
|
||||||
|
fillOpacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 2,
|
||||||
|
ease: 'easeInOut',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
sm: {
|
||||||
|
svg: 'h-6',
|
||||||
|
betaTag: 'bottom-[2px] left-[calc(100%+6px)] px-1.5 py-0.5 text-[9px]',
|
||||||
|
},
|
||||||
|
lg: {
|
||||||
|
svg: 'h-12',
|
||||||
|
betaTag: 'bottom-[4px] left-[calc(100%+10px)] px-2 py-0.5 text-base',
|
||||||
|
},
|
||||||
|
xl: {
|
||||||
|
svg: 'h-14',
|
||||||
|
betaTag: 'bottom-[7px] left-[calc(100%+15px)] px-2.5 py-1 text-base',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Logo = ({
|
||||||
|
betaTag = false,
|
||||||
|
draw = false,
|
||||||
|
size = 'sm',
|
||||||
|
className,
|
||||||
|
containerClassName,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
containerClassName?: string;
|
||||||
|
betaTag?: boolean;
|
||||||
|
draw?: boolean;
|
||||||
|
size?: keyof typeof sizes;
|
||||||
|
} & SVGMotionProps<SVGSVGElement>) => {
|
||||||
|
return (
|
||||||
|
<div className={cn('relative', containerClassName)}>
|
||||||
|
<motion.svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 4899.14 783.54"
|
||||||
|
className={cn(sizes[size].svg, className)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<motion.path
|
||||||
|
variants={draw ? pathVariants : {}}
|
||||||
|
initial={draw ? 'hidden' : false}
|
||||||
|
animate={draw ? 'visible' : false}
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={4}
|
||||||
|
className="fill-neutral-900 dark:fill-neutral-100"
|
||||||
|
d="M612.79 783.54H227.43c-41.6 0-82.15-11.19-117.26-32.37-32.96-19.88-60.58-48.18-79.88-81.85C10.99 635.66.52 597.52.02 559.03c-.54-41 10.29-81.64 31.32-117.54l192.67-329.07c20.81-35.55 50.62-64.69 86.19-84.28C343.64 9.73 381.64 0 420.11 0s76.47 9.73 109.9 28.14c35.57 19.59 65.38 48.73 86.19 84.28l192.68 329.07c21.02 35.9 31.85 76.55 31.31 117.54-.5 38.49-10.97 76.62-30.27 110.29s-46.92 61.97-79.88 81.85c-35.11 21.18-75.65 32.37-117.26 32.37ZM396.6 213.47 203.92 542.54c-6.59 11.26-3.71 21.06-.13 27.31s10.59 13.69 23.63 13.69h385.36c13.04 0 20.05-7.45 23.63-13.69 3.58-6.25 6.46-16.05-.12-27.31L443.61 213.47c-6.52-11.14-16.38-13.47-23.5-13.47s-16.98 2.34-23.5 13.47ZM1574.55 673.25h112.71l-218.43-546.46h-87.06l-220.76 546.46h111.94l38.89-101.83h224.17zM1345.68 482.8l78.75-206.19 78.04 206.19zM2067.37 364.26c-12.44-22.02-29.42-39.9-50.91-53.64-21.51-13.73-46.25-20.6-74.24-20.6s-54.02 6.22-76.57 18.66c-12.2 6.73-22.94 14.88-32.26 24.42v-35.3h-101.83v375.45h101.83V457.93c0-14.5 3.23-27.59 9.72-39.25q9.705-17.49 26.82-26.82c11.39-6.22 24.35-9.33 38.87-9.33 21.77 0 39.64 7 53.64 20.99s20.99 32.14 20.99 54.41v215.32h102.61V434.61c0-24.88-6.22-48.32-18.66-70.35ZM2204.18 237.17c-16.59 0-30.32-5.56-41.2-16.71-10.88-11.14-16.32-25-16.32-41.59s5.44-29.79 16.32-41.2c10.88-11.39 24.61-17.1 41.2-17.1s30.96 5.71 41.59 17.1c10.62 11.41 15.93 25.14 15.93 41.2s-5.32 30.45-15.93 41.59c-10.63 11.15-24.49 16.71-41.59 16.71m-52.08 436.08V297.8h103.38v375.45zM2902.6 357.65c-13.21-21.77-30.97-38.48-53.25-50.14-22.29-11.66-47.42-17.49-75.4-17.49s-53.38 6.1-76.18 18.27c-18.12 9.68-33.52 22.38-46.24 38.11-11.59-16.2-26.23-29.17-43.93-38.88q-31.875-17.49-70.74-17.49c-27.47 0-52.08 5.83-73.85 17.49-11.67 6.25-22.03 13.9-31.09 22.91V297.8h-101.83v375.45h101.83V452.49c0-15.02 3.23-27.72 9.72-38.09 6.47-10.36 15.03-18.27 25.65-23.71s22.66-8.16 36.15-8.16c20.21 0 37.04 6.1 50.53 18.27 13.47 12.18 20.21 29.15 20.21 50.92v221.54h102.61V452.5c0-15.02 3.23-27.72 9.72-38.09 6.47-10.36 15.02-18.27 25.65-23.71 10.62-5.44 22.93-8.16 36.92-8.16 19.69 0 36.27 6.1 49.75 18.27 13.47 12.18 20.21 29.15 20.21 50.92v221.54h103.38V436.18c0-30.57-6.61-56.75-19.82-78.51ZM3253.56 297.8v35.43c-10.39-10.75-22.56-19.72-36.54-26.88-21.25-10.88-45.08-16.32-71.51-16.32-34.73 0-65.69 8.55-92.89 25.65-27.21 17.1-48.58 40.42-64.13 69.96s-23.32 62.96-23.32 100.28 7.77 69.96 23.32 99.5 36.92 52.86 64.13 69.96 57.91 25.65 92.11 25.65c26.94 0 51.17-5.44 72.68-16.32 13.89-7.03 25.93-15.85 36.15-26.45v35h102.61V297.81h-102.61Zm-20.21 260.4c-17.62 19.18-40.69 28.76-69.18 28.76-18.15 0-34.47-4.4-48.97-13.21-14.51-8.81-25.79-20.72-33.81-35.76-8.04-15.02-12.05-32.65-12.05-52.86s4.01-37.04 12.05-52.08c8.03-15.02 19.17-26.94 33.43-35.76 14.25-8.81 30.71-13.21 49.36-13.21s35.76 4.28 49.75 12.83 25.13 20.6 33.43 36.15c8.28 15.55 12.44 33.17 12.44 52.86 0 29.03-8.82 53.13-26.43 72.29ZM3674.09 297.8h-87.84V141.55h-101.83V297.8h-87.84v89.39h87.84v286.06h101.83V387.19h87.84zM4049.53 380.2c-15.55-28.49-37.31-50.78-65.3-66.85-27.98-16.06-59.6-24.1-94.83-24.1-37.31 0-71 8.55-101.05 25.65q-45.09 25.65-71.13 69.96c-17.37 29.54-26.04 62.96-26.04 100.28s8.81 71.51 26.43 101.05c17.61 29.54 41.84 52.86 72.68 69.96q46.245 25.65 105.33 25.65c30.57 0 58.81-5.44 84.73-16.32 25.91-10.88 48.45-27.21 67.63-48.97l-60.63-60.63c-11.4 13.48-24.87 23.32-40.42 29.54s-32.92 9.33-52.08 9.33c-21.25 0-39.91-4.4-55.97-13.21-16.07-8.81-28.37-21.63-36.92-38.48-3.4-6.69-6.11-13.85-8.15-21.48l274.39-.67c2.06-8.28 3.36-15.93 3.89-22.93.51-7 .78-13.86.78-20.6 0-36.27-7.77-68.66-23.32-97.17Zm-213.76 7.77c15.02-8.81 32.65-13.21 52.86-13.21 19.17 0 35.24 3.89 48.2 11.66 12.95 7.77 23.05 19.18 30.32 34.2 3.49 7.23 6.19 15.32 8.12 24.23l-181.45.52c1.88-6.99 4.34-13.56 7.37-19.7 8.03-16.32 19.55-28.88 34.59-37.7ZM4484.04 681.8c-43.02 0-81.36-9.58-115.05-28.76-33.69-19.17-60.24-45.34-79.68-78.51-19.43-33.16-29.15-70.74-29.15-112.71V126.78h106.49v338.14c0 24.36 5.17 45.35 15.55 62.96 10.36 17.62 24.35 31.23 41.98 40.81 17.61 9.59 37.57 14.38 59.85 14.38s41.84-4.79 58.69-14.38c16.83-9.58 30.32-23.19 40.42-40.81 10.1-17.61 15.16-38.34 15.16-62.19V126.78h106.5v335.81c0 41.98-9.46 79.42-28.37 112.33q-28.38 49.38-78.12 78.12c-33.17 19.18-71.26 28.76-114.27 28.76M4793.42 673.25V126.78h105.72v546.46h-105.72Z"
|
||||||
|
></motion.path>
|
||||||
|
</motion.svg>
|
||||||
|
|
||||||
|
{betaTag && (
|
||||||
|
<motion.div
|
||||||
|
className={cn(
|
||||||
|
sizes[size].betaTag,
|
||||||
|
'absolute bg-neutral-200 dark:bg-neutral-800 text-neutral-600 dark:text-neutral-400 rounded-full',
|
||||||
|
)}
|
||||||
|
initial={draw ? { opacity: 0 } : undefined}
|
||||||
|
animate={draw ? { opacity: 1 } : undefined}
|
||||||
|
transition={{ duration: 4, ease: 'easeInOut' }}
|
||||||
|
>
|
||||||
|
Beta
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span className="sr-only">Animate UI</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
39
apps/www/components/section-wrapper.tsx
Normal file
39
apps/www/components/section-wrapper.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { cn } from '@workspace/ui/lib/utils';
|
||||||
|
import { Button } from '@workspace/ui/components/ui/button';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { motion } from 'motion/react';
|
||||||
|
|
||||||
|
export const SectionWrapper = ({
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
description,
|
||||||
|
color = 'text-blue-500',
|
||||||
|
backgroundColor = 'bg-blue-500/10 hover:bg-blue-500/20',
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
title: React.ReactNode;
|
||||||
|
subtitle: React.ReactNode;
|
||||||
|
description: React.ReactNode;
|
||||||
|
color?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<section className="mt-30">
|
||||||
|
<p className={cn('text-base font-medium mb-4', color)}>{subtitle}</p>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-semibold">{title}</h2>
|
||||||
|
<p className="mt-4 text-muted-foreground max-w-3xl">{description}</p>
|
||||||
|
<motion.div className="my-8 w-fit" whileTap={{ scale: 0.95 }}>
|
||||||
|
<Button
|
||||||
|
asChild
|
||||||
|
className={cn('rounded-full transition-colors', backgroundColor)}
|
||||||
|
>
|
||||||
|
<Link href="/docs">
|
||||||
|
<span className={color}>Get Started</span>
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
28
apps/www/components/theme-switcher.tsx
Normal file
28
apps/www/components/theme-switcher.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Switch } from '@/registry/radix/switch';
|
||||||
|
import { Moon, Sun } from 'lucide-react';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export const ThemeSwitcher = ({ className }: { className?: string }) => {
|
||||||
|
const { resolvedTheme: theme, setTheme } = useTheme();
|
||||||
|
|
||||||
|
const [isClient, setIsClient] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsClient(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
isClient && (
|
||||||
|
<Switch
|
||||||
|
className={className}
|
||||||
|
leftIcon={<Sun />}
|
||||||
|
rightIcon={<Moon />}
|
||||||
|
checked={theme === 'dark'}
|
||||||
|
onCheckedChange={(checked) => setTheme(checked ? 'dark' : 'light')}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
60
apps/www/content/docs/backgrounds/bubble.mdx
Normal file
60
apps/www/content/docs/backgrounds/bubble.mdx
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
---
|
||||||
|
title: Bubble Background
|
||||||
|
description: An interactive background featuring smoothly animated gradient bubbles, creating a playful, dynamic, and visually engaging backdrop.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="bubble-background-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="bubble-background" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<BubbleBackground />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the component',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
interactive: {
|
||||||
|
description: 'Whether the background is interactive',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'false',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the interactive bubble',
|
||||||
|
type: 'SpringOptions',
|
||||||
|
required: false,
|
||||||
|
default: '{ stiffness: 100, damping: 20 }',
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
description: 'The colors of the background',
|
||||||
|
type: `object`,
|
||||||
|
default: `{
|
||||||
|
first: '18,113,255',
|
||||||
|
second: '221,74,255',
|
||||||
|
third: '0,220,255',
|
||||||
|
fourth: '200,50,50',
|
||||||
|
fifth: '180,180,50',
|
||||||
|
sixth: '140,100,255',
|
||||||
|
}`,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [GlitchWorker](https://codepen.io/glitchworker/pen/jENZGOV) for the inspiration
|
||||||
97
apps/www/content/docs/backgrounds/fireworks.mdx
Normal file
97
apps/www/content/docs/backgrounds/fireworks.mdx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
title: Fireworks Background
|
||||||
|
description: A background component that displays a fireworks animation.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="fireworks-background-demo" />
|
||||||
|
|
||||||
|
_Click on the background to make it rain fireworks._
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="fireworks-background" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<FireworksBackground />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### With a high population
|
||||||
|
|
||||||
|
<ComponentPreview name="fireworks-background-population-demo" />
|
||||||
|
|
||||||
|
### With a high size and speed
|
||||||
|
|
||||||
|
<ComponentPreview name="fireworks-background-size-speed-demo" />
|
||||||
|
|
||||||
|
### Circle effect with a fixed size and speed
|
||||||
|
|
||||||
|
<ComponentPreview name="fireworks-background-fix-size-speed-demo" />
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the component',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the component',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
canvasProps: {
|
||||||
|
description: 'The props for the canvas element',
|
||||||
|
type: 'React.HTMLAttributes<HTMLCanvasElement>',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
description: 'The color of the fireworks',
|
||||||
|
type: 'string | string[]',
|
||||||
|
required: false,
|
||||||
|
default: 'randColor()',
|
||||||
|
},
|
||||||
|
population: {
|
||||||
|
description: 'The number of fireworks to display',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
fireworkSpeed: {
|
||||||
|
description: 'The speed of the fireworks',
|
||||||
|
type: '{ min: number; max: number } | number',
|
||||||
|
required: false,
|
||||||
|
default: '{ min: 4, max: 8 }',
|
||||||
|
},
|
||||||
|
fireworkSize: {
|
||||||
|
description: 'The size of the fireworks',
|
||||||
|
type: '{ min: number; max: number } | number',
|
||||||
|
required: false,
|
||||||
|
default: '{ min: 2, max: 5 }',
|
||||||
|
},
|
||||||
|
particleSpeed: {
|
||||||
|
description: 'The speed of the particles',
|
||||||
|
type: '{ min: number; max: number } | number',
|
||||||
|
required: false,
|
||||||
|
default: '{ min: 2, max: 7 }',
|
||||||
|
},
|
||||||
|
particleSize: {
|
||||||
|
description: 'The size of the particles',
|
||||||
|
type: '{ min: number; max: number } | number',
|
||||||
|
required: false,
|
||||||
|
default: '{ min: 1, max: 5 }',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Matt Cannon](https://codepen.io/matt-cannon/pen/YPKGBGm) for the inspiration
|
||||||
42
apps/www/content/docs/backgrounds/gradient.mdx
Normal file
42
apps/www/content/docs/backgrounds/gradient.mdx
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
title: Gradient Background
|
||||||
|
description: A background component featuring a subtle yet engaging animated gradient effect, smoothly transitioning colors to enhance visual depth.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="gradient-background-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="gradient-background" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<GradientBackground />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the component',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the component',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the gradient',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: "{ duration: 15, ease: 'easeInOut', repeat: Infinity }",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
57
apps/www/content/docs/backgrounds/hexagon.mdx
Normal file
57
apps/www/content/docs/backgrounds/hexagon.mdx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: Hexagon Background
|
||||||
|
description: A background component featuring an interactive hexagon grid.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="hexagon-background-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="hexagon-background" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<HexagonBackground />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the component',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the component',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
hexagonProps: {
|
||||||
|
description: 'The props of the hexagon',
|
||||||
|
type: 'React.HTMLAttributes<HTMLDivElement>',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
hexagonSize: {
|
||||||
|
description: 'The size of the hexagon',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '75',
|
||||||
|
},
|
||||||
|
hexagonMargin: {
|
||||||
|
description: 'The margin of the hexagon',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '3',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Denis Klak](https://codepen.io/d3nis031/pen/QWyeNYx) for the inspiration
|
||||||
64
apps/www/content/docs/backgrounds/hole.mdx
Normal file
64
apps/www/content/docs/backgrounds/hole.mdx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: Hole Background
|
||||||
|
description: A background with a hole animation effect.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="hole-background-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="hole-background" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<HoleBackground />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the component',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the component',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
strokeColor: {
|
||||||
|
description: 'The color of the stroke',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
default: '#737373',
|
||||||
|
},
|
||||||
|
numberOfLines: {
|
||||||
|
description: 'The number of lines',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '50',
|
||||||
|
},
|
||||||
|
numberOfDiscs: {
|
||||||
|
description: 'The number of discs',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '50',
|
||||||
|
},
|
||||||
|
particleRGBColor: {
|
||||||
|
description: 'The RGB color of the particles',
|
||||||
|
type: 'number[]',
|
||||||
|
required: false,
|
||||||
|
default: '[255, 255, 255]',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Antoine Wodniack](https://codepen.io/wodniack/pen/VYwGEGg) for the inspiration
|
||||||
104
apps/www/content/docs/backgrounds/stars.mdx
Normal file
104
apps/www/content/docs/backgrounds/stars.mdx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
---
|
||||||
|
title: Stars Background
|
||||||
|
description: A dark, interactive background featuring animated dots of varying sizes and speeds, simulating a dynamic and immersive starry space effect.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="stars-background-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="stars-background" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<StarsBackground />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### StarsBackground
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the component',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the component',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
factor: {
|
||||||
|
description: 'The factor of the component',
|
||||||
|
type: 'number',
|
||||||
|
default: '0.05',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
speed: {
|
||||||
|
description: 'The speed of the component',
|
||||||
|
type: 'number',
|
||||||
|
default: '50',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the interactive movement',
|
||||||
|
type: 'SpringOptions',
|
||||||
|
required: false,
|
||||||
|
default: '{ stiffness: 50, damping: 20 }',
|
||||||
|
},
|
||||||
|
starColor: {
|
||||||
|
description: 'The color of the stars',
|
||||||
|
type: 'string',
|
||||||
|
default: '#fff',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
pointerEvents: {
|
||||||
|
description:
|
||||||
|
'Controls whether pointer events are caught by the component or pass through to elements behind it',
|
||||||
|
type: 'boolean',
|
||||||
|
default: 'true',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### StarLayer
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
count: {
|
||||||
|
description: 'The number of stars',
|
||||||
|
type: 'number',
|
||||||
|
default: '1000',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
description: 'The size of the stars',
|
||||||
|
type: 'number',
|
||||||
|
default: '1',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the stars',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: '{ repeat: Infinity, duration: 50, ease: "linear" }',
|
||||||
|
},
|
||||||
|
starColor: {
|
||||||
|
description: 'The color of the stars',
|
||||||
|
type: 'string',
|
||||||
|
default: '#fff',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [umangladani](https://codepen.io/umangladani/pen/wvgwgjK) for the inspiration
|
||||||
84
apps/www/content/docs/base/accordion.mdx
Normal file
84
apps/www/content/docs/base/accordion.mdx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
title: Accordion
|
||||||
|
description: A set of collapsible panels with headings.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="base-accordion-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="base-accordion" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Accordion>
|
||||||
|
<AccordionItem>
|
||||||
|
<AccordionTrigger>Accordion Item 1</AccordionTrigger>
|
||||||
|
<AccordionPanel>Accordion Panel 1</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem>
|
||||||
|
<AccordionTrigger>Accordion Item 2</AccordionTrigger>
|
||||||
|
<AccordionPanel>Accordion Panel 2</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
<AccordionItem>
|
||||||
|
<AccordionTrigger>Accordion Item 3</AccordionTrigger>
|
||||||
|
<AccordionPanel>Accordion Panel 3</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-x-3">
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/accordion"
|
||||||
|
text="Docs"
|
||||||
|
/>
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/accordion#api-reference"
|
||||||
|
text="API Reference"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Animate UI Props
|
||||||
|
|
||||||
|
#### AccordionTrigger
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the accordion chevron',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: "{ type: 'spring', stiffness: 150, damping: 17 }",
|
||||||
|
},
|
||||||
|
chevron: {
|
||||||
|
description: 'Whether to show the accordion chevron',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
#### AccordionPanel
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the accordion panel',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: "{ type: 'spring', stiffness: 150, damping: 22 }",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- We use [Base UI](https://base-ui.com/react/components/accordion) for the accordion component.
|
||||||
|
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/accordion) for the accordion style.
|
||||||
51
apps/www/content/docs/base/checkbox.mdx
Normal file
51
apps/www/content/docs/base/checkbox.mdx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
title: Checkbox
|
||||||
|
description: An easily stylable checkbox component.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="base-checkbox-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="base-checkbox" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Checkbox />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-x-3">
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/checkbox"
|
||||||
|
text="Docs"
|
||||||
|
/>
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/checkbox#api-reference"
|
||||||
|
text="API Reference"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Animate UI Props
|
||||||
|
|
||||||
|
#### Checkbox
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
motionProps: {
|
||||||
|
description: 'The props to pass to the motion component.',
|
||||||
|
type: "HTMLMotionProps<'button'>",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- We use [Base UI](https://base-ui.com/react/components/checkbox) for the checkbox component.
|
||||||
|
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/checkbox) for the checkbox style.
|
||||||
70
apps/www/content/docs/base/popover.mdx
Normal file
70
apps/www/content/docs/base/popover.mdx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
title: Popover
|
||||||
|
description: An accessible popup anchored to a button.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="base-popover-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="base-popover" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger>Open Popover</PopoverTrigger>
|
||||||
|
<PopoverContent>Popover Content</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-x-3">
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/popover"
|
||||||
|
text="Docs"
|
||||||
|
/>
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/popover#api-reference"
|
||||||
|
text="API Reference"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Animate UI Props
|
||||||
|
|
||||||
|
#### PopoverContent
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
positionerClassName: {
|
||||||
|
description: 'The class name to apply to the positioner.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
motionProps: {
|
||||||
|
description: 'The props to pass to the motion component.',
|
||||||
|
type: "HTMLMotionProps<'div'>",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
popupProps: {
|
||||||
|
description: 'The props to pass to the popup component.',
|
||||||
|
type: 'typeof PopoverPrimitive.Popup',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition to use for the popover.',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: '{ type: "spring", stiffness: 300, damping: 25 }',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- We use [Base UI](https://base-ui.com/react/components/popover) for the popover component.
|
||||||
|
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/popover) for the popover style.
|
||||||
70
apps/www/content/docs/base/preview-card.mdx
Normal file
70
apps/www/content/docs/base/preview-card.mdx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
title: Preview Card
|
||||||
|
description: A popup that appears when a link is hovered, showing a preview for sighted users.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="base-preview-card-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="base-preview-card" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<PreviewCard>
|
||||||
|
<PreviewCardTrigger>Hover me</PreviewCardTrigger>
|
||||||
|
<PreviewCardContent>Preview card content</PreviewCardContent>
|
||||||
|
</PreviewCard>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-x-3">
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/preview-card"
|
||||||
|
text="Docs"
|
||||||
|
/>
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/preview-card#api-reference"
|
||||||
|
text="API Reference"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Animate UI Props
|
||||||
|
|
||||||
|
#### PreviewCardContent
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
positionerClassName: {
|
||||||
|
description: 'The class name to apply to the positioner.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
motionProps: {
|
||||||
|
description: 'The props to pass to the motion component.',
|
||||||
|
type: "HTMLMotionProps<'div'>",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
popupProps: {
|
||||||
|
description: 'The props to pass to the popup component.',
|
||||||
|
type: 'typeof PopoverPrimitive.Popup',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition to use for the popover.',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: '{ type: "spring", stiffness: 300, damping: 25 }',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- We use [Base UI](https://base-ui.com/react/components/preview-card) for the preview card component.
|
||||||
|
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/preview-card) for the preview card style.
|
||||||
68
apps/www/content/docs/base/progress.mdx
Normal file
68
apps/www/content/docs/base/progress.mdx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
title: Progress
|
||||||
|
description: Displays an indicator showing the completion progress of a task, typically displayed as a progress bar.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="base-progress-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="base-progress" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Progress value={50}>
|
||||||
|
<ProgressLabel />
|
||||||
|
<ProgressValue />
|
||||||
|
<ProgressTrack />
|
||||||
|
</Progress>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-x-3">
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/progress"
|
||||||
|
text="Docs"
|
||||||
|
/>
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/progress#api-reference"
|
||||||
|
text="API Reference"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Animate UI Props
|
||||||
|
|
||||||
|
#### ProgressTrack
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
transition: {
|
||||||
|
description: 'The transition to use for the progress track.',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: "{ type: 'spring', stiffness: 100, damping: 30 }",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
#### ProgressLabel
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
countingNumberProps: {
|
||||||
|
description: 'The props to pass to the counting number.',
|
||||||
|
type: 'CountingNumberProps',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- We use [Base UI](https://base-ui.com/react/components/progress) for the progress component.
|
||||||
|
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/progress) for the progress style.
|
||||||
66
apps/www/content/docs/base/switch.mdx
Normal file
66
apps/www/content/docs/base/switch.mdx
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
title: Switch
|
||||||
|
description: A control that indicates whether a setting is on or off.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="base-switch-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="base-switch" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Switch />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-x-3">
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/switch"
|
||||||
|
text="Docs"
|
||||||
|
/>
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/switch#api-reference"
|
||||||
|
text="API Reference"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Animate UI Props
|
||||||
|
|
||||||
|
#### Switch
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
motionProps: {
|
||||||
|
description: 'The props to pass to the motion component.',
|
||||||
|
type: "HTMLMotionProps<'button'>",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
leftIcon: {
|
||||||
|
description: 'The icon to display on the left of the switch',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
rightIcon: {
|
||||||
|
description: 'The icon to display on the right of the switch',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
thumbIcon: {
|
||||||
|
description: 'The icon to display on the thumb of the switch',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- We use [Base UI](https://base-ui.com/react/components/switch) for the switch component.
|
||||||
|
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/switch) for the switch style.
|
||||||
108
apps/www/content/docs/base/toggle-group.mdx
Normal file
108
apps/www/content/docs/base/toggle-group.mdx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
title: Toggle Group
|
||||||
|
description: Provides a shared state to a series of toggle buttons.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="base-toggle-group-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="base-toggle-group" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ToggleGroup>
|
||||||
|
<ToggleGroupItem>A</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem>B</ToggleGroupItem>
|
||||||
|
<ToggleGroupItem>C</ToggleGroupItem>
|
||||||
|
</ToggleGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-x-3">
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/toggle-group"
|
||||||
|
text="Docs"
|
||||||
|
/>
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/toggle-group#api-reference"
|
||||||
|
text="API Reference"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Animate UI Props
|
||||||
|
|
||||||
|
#### ToggleGroup
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the toggle group',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: "{ type: 'spring', bounce: 0, stiffness: 200, damping: 25 }",
|
||||||
|
},
|
||||||
|
activeClassName: {
|
||||||
|
description: 'The class name of the active toggle group item',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
description: 'The variant of the toggle group',
|
||||||
|
type: 'default' | 'outline',
|
||||||
|
required: false,
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
description: 'The size of the toggle group',
|
||||||
|
type: 'default' | 'sm' | 'lg',
|
||||||
|
required: false,
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
#### ToggleGroupItem
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the toggle group item',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
buttonProps: {
|
||||||
|
description: 'The props of the toggle group item button',
|
||||||
|
type: 'HTMLMotionProps<"button">',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
spanProps: {
|
||||||
|
description: 'The props of the toggle group item span',
|
||||||
|
type: 'React.HTMLAttributes<HTMLSpanElement>',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
description: 'The variant of the toggle group item',
|
||||||
|
type: 'default' | 'outline',
|
||||||
|
required: false,
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
description: 'The size of the toggle group item',
|
||||||
|
type: 'default' | 'sm' | 'lg',
|
||||||
|
required: false,
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- We use [Base UI](https://base-ui.com/react/components/accordion) for the accordion component.
|
||||||
|
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/accordion) for the accordion style.
|
||||||
79
apps/www/content/docs/base/tooltip.mdx
Normal file
79
apps/www/content/docs/base/tooltip.mdx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
title: Tooltip
|
||||||
|
description: A popup that appears when an element is hovered or focused, showing a hint for sighted users.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="base-tooltip-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="base-tooltip" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>Hover me</TooltipTrigger>
|
||||||
|
<TooltipContent>Tooltip content</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-x-3">
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/tooltip"
|
||||||
|
text="Docs"
|
||||||
|
/>
|
||||||
|
<ExternalLink
|
||||||
|
href="https://base-ui.com/react/components/tooltip#api-reference"
|
||||||
|
text="API Reference"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Animate UI Props
|
||||||
|
|
||||||
|
#### TooltipContent
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
positionerClassName: {
|
||||||
|
description: 'The class name to apply to the positioner.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
motionProps: {
|
||||||
|
description: 'The props to pass to the motion component.',
|
||||||
|
type: "HTMLMotionProps<'div'>",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
popupProps: {
|
||||||
|
description: 'The props to pass to the popup component.',
|
||||||
|
type: 'typeof PopoverPrimitive.Popup',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition to use for the popover.',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: '{ type: "spring", stiffness: 300, damping: 25 }',
|
||||||
|
},
|
||||||
|
arrow: {
|
||||||
|
description:
|
||||||
|
'Whether to show the arrow. (only available in shadcn-new-york or default style)',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- We use [Base UI](https://base-ui.com/react/components/tooltip) for the tooltip component.
|
||||||
|
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/tooltip) for the tooltip style.
|
||||||
58
apps/www/content/docs/buttons/copy.mdx
Normal file
58
apps/www/content/docs/buttons/copy.mdx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
title: Copy Button
|
||||||
|
description: A button with a copy to clipboard animation.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="copy-button-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="copy-button" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<CopyButton content="Hello world!" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
content: {
|
||||||
|
description: 'The content to copy to the clipboard.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
delay: {
|
||||||
|
description: 'The delay in milliseconds before the button resets.',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '3000',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
description: 'The size of the button.',
|
||||||
|
type: "'default' | 'sm' | 'md' | 'lg'",
|
||||||
|
required: false,
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
variant: {
|
||||||
|
description: 'The variant of the button.',
|
||||||
|
type: "'default' | 'muted' | 'outline' | 'secondary' | 'ghost'",
|
||||||
|
required: false,
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
onCopy: {
|
||||||
|
description: 'The function to call when copy is successful.',
|
||||||
|
type: '(content: string) => void',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Shadcn UI](https://ui.shadcn.com/docs/components/button) for the button style.
|
||||||
71
apps/www/content/docs/buttons/flip.mdx
Normal file
71
apps/www/content/docs/buttons/flip.mdx
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
title: Flip Button
|
||||||
|
description: A clickable button featuring a smooth flipping animation triggered on hover.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="flip-button-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="flip-button" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<FlipButton frontText="Front" backText="Back" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### From Left
|
||||||
|
|
||||||
|
Use the `from` prop to change the direction of the flip.
|
||||||
|
|
||||||
|
<ComponentPreview name="flip-button-from-demo" />
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
frontText: {
|
||||||
|
description: 'The text to display on the front of the button',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
backText: {
|
||||||
|
description: 'The text to display on the back of the button',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the button',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: '{ type: "spring", stiffness: 280, damping: 20 }',
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the component',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
frontClassName: {
|
||||||
|
description: 'The className of the front of the button',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
backClassName: {
|
||||||
|
description: 'The className of the back of the button',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
from: {
|
||||||
|
description: 'The direction of the flip',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
default: 'top',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
70
apps/www/content/docs/buttons/github-stars.mdx
Normal file
70
apps/www/content/docs/buttons/github-stars.mdx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
title: GitHub Stars Button
|
||||||
|
description: A clickable button that links to a GitHub repository and displays the number of stars.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="github-stars-button-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="github-stars-button" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<GitHubStarsButton username="animate-ui" repo="animate-ui" />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
username: {
|
||||||
|
description: 'The username of the repository',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
repo: {
|
||||||
|
description: 'The repository name',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the component',
|
||||||
|
type: 'object',
|
||||||
|
required: false,
|
||||||
|
default: '{ stiffness: 90, damping: 50 }',
|
||||||
|
},
|
||||||
|
formatted: {
|
||||||
|
description: 'Whether to format the number with a unit (e.g. 18k)',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
inView: {
|
||||||
|
description: 'Enable animation when in view.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
inViewMargin: {
|
||||||
|
description: 'The margin of the in view.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
default: '0px',
|
||||||
|
},
|
||||||
|
inViewOnce: {
|
||||||
|
description: 'Enable animation when in view once.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [SHSF UI](https://www.shsfui.com/primitives/buttons/heart-button) for the icon animation inspiration
|
||||||
65
apps/www/content/docs/buttons/icon.mdx
Normal file
65
apps/www/content/docs/buttons/icon.mdx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: Icon Button
|
||||||
|
description: An icon button that displays particles when clicked.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="icon-button-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="icon-button" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<IconButton icon={Heart} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
icon: {
|
||||||
|
description: 'The icon to display',
|
||||||
|
type: 'React.ElementType',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
description: 'Whether the button is active',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
animate: {
|
||||||
|
description: 'Whether the button should display animations',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
description: 'The size of the button',
|
||||||
|
type: "'default' | 'sm' | 'md' | 'lg'",
|
||||||
|
required: false,
|
||||||
|
default: 'default',
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
description: 'The color of the button',
|
||||||
|
type: '[number, number, number]',
|
||||||
|
required: false,
|
||||||
|
default: '[59, 130, 246]',
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the button',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: 'spring',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [SHSF UI](https://www.shsfui.com/primitives/buttons/heart-button) for the icon animation inspiration
|
||||||
79
apps/www/content/docs/buttons/input.mdx
Normal file
79
apps/www/content/docs/buttons/input.mdx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
---
|
||||||
|
title: Input Button
|
||||||
|
description: A button that shows an input when clicked.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="input-button-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="input-button" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<InputButton>
|
||||||
|
<Buttons>
|
||||||
|
<Button>Button</Button>
|
||||||
|
<SubmitButton>Submit</SubmitButton>
|
||||||
|
</Buttons>
|
||||||
|
<Input />
|
||||||
|
</InputButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Newsletter Form with Loading State
|
||||||
|
|
||||||
|
<ComponentPreview name="input-button-loading-demo" />
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### InputButton
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the component',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: "{ type: 'spring', stiffness: 300, damping: 20 }",
|
||||||
|
},
|
||||||
|
showInput: {
|
||||||
|
description: 'Whether to show the input',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
setShowInput: {
|
||||||
|
description: 'The function to set the showInput state',
|
||||||
|
type: 'React.Dispatch<React.SetStateAction<boolean>>',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
description: 'The id of the component',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### SubmitButton
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
icon: {
|
||||||
|
description: 'The icon of the component',
|
||||||
|
type: 'React.ElementType',
|
||||||
|
required: false,
|
||||||
|
default: 'ArrowRight',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Adam Wathan](https://x.com/adamwathan/status/1890105358519341205) for the inspiration
|
||||||
40
apps/www/content/docs/buttons/liquid.mdx
Normal file
40
apps/www/content/docs/buttons/liquid.mdx
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: Liquid Button
|
||||||
|
description: A clickable button featuring a dynamic, fluid-like animation effect upon hover, creating an engaging, liquid-inspired interaction.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="liquid-button-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="liquid-button" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<LiquidButton>Button</LiquidButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the component',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the component',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Emadamerho Nefe](https://codepen.io/nefejames/pen/yyBedGg) for the inspiration
|
||||||
53
apps/www/content/docs/buttons/ripple.mdx
Normal file
53
apps/www/content/docs/buttons/ripple.mdx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
title: Ripple Button
|
||||||
|
description: A clickable button featuring a ripple animation effect on click.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="ripple-button-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="ripple-button" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<RippleButton>Button</RippleButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the component',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the component',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
rippleClassName: {
|
||||||
|
description: 'The className of the ripple',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the ripple',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: '{ duration: 0.6, ease: "easeOut" }',
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
description: 'The scale of the ripple',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: 10,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
108
apps/www/content/docs/cli.mdx
Normal file
108
apps/www/content/docs/cli.mdx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
---
|
||||||
|
title: CLI
|
||||||
|
description: Use the shadcn CLI to add components to your project.
|
||||||
|
---
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
**Note:** We rely on the [shadcn/ui
|
||||||
|
CLI](https://ui.shadcn.com/docs/installation/cli) to add components to your
|
||||||
|
project.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## init
|
||||||
|
|
||||||
|
Use the `init` command to initialize configuration and dependencies for a new project.
|
||||||
|
|
||||||
|
The `init` command installs dependencies, adds the `cn` util and configures CSS variables for the project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest init
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Usage: shadcn init [options] [components...]
|
||||||
|
|
||||||
|
initialize your project and install dependencies
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
components the components to add or a url to the component.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-y, --yes skip confirmation prompt. (default: true)
|
||||||
|
-d, --defaults, use default configuration. (default: false)
|
||||||
|
-f, --force force overwrite of existing configuration. (default: false)
|
||||||
|
-c, --cwd <cwd> the working directory. defaults to the current directory. (default: "/Users/shadcn/Desktop")
|
||||||
|
-s, --silent mute output. (default: false)
|
||||||
|
--src-dir use the src directory when creating a new project. (default: false)
|
||||||
|
--no-src-dir do not use the src directory when creating a new project.
|
||||||
|
--css-variables use css variables for theming. (default: true)
|
||||||
|
--no-css-variables do not use css variables for theming.
|
||||||
|
-h, --help display help for command
|
||||||
|
```
|
||||||
|
|
||||||
|
## add
|
||||||
|
|
||||||
|
Use the `add` command to add components and dependencies to your project.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add [component]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Usage: shadcn add [options] [components...]
|
||||||
|
|
||||||
|
add a component to your project
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
components the components to add or a url to the component.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-y, --yes skip confirmation prompt. (default: false)
|
||||||
|
-o, --overwrite overwrite existing files. (default: false)
|
||||||
|
-c, --cwd <cwd> the working directory. defaults to the current directory. (default: "/Users/shadcn/Desktop")
|
||||||
|
-a, --all add all available components (default: false)
|
||||||
|
-p, --path <path> the path to add the component to.
|
||||||
|
-s, --silent mute output. (default: false)
|
||||||
|
--src-dir use the src directory when creating a new project. (default: false)
|
||||||
|
--no-src-dir do not use the src directory when creating a new project.
|
||||||
|
--css-variables use css variables for theming. (default: true)
|
||||||
|
--no-css-variables do not use css variables for theming.
|
||||||
|
-h, --help display help for command
|
||||||
|
```
|
||||||
|
|
||||||
|
## build
|
||||||
|
|
||||||
|
Use the `build` command to generate the registry JSON files.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest build
|
||||||
|
```
|
||||||
|
|
||||||
|
This command reads the `registry.json` file and generates the registry JSON files in the `public/r` directory.
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```txt
|
||||||
|
Usage: shadcn build [options] [registry]
|
||||||
|
|
||||||
|
build components for a shadcn registry
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
registry path to registry.json file (default: "./registry.json")
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-o, --output <path> destination directory for json files (default: "./public/r")
|
||||||
|
-c, --cwd <cwd> the working directory. defaults to the current directory. (default:
|
||||||
|
"/Users/shadcn/Code/shadcn/ui/packages/shadcn")
|
||||||
|
-h, --help display help for command
|
||||||
|
```
|
||||||
|
|
||||||
|
To customize the output directory, use the `--output` option.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest build --output ./public/registry
|
||||||
|
```
|
||||||
214
apps/www/content/docs/components/avatar-group.mdx
Normal file
214
apps/www/content/docs/components/avatar-group.mdx
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
---
|
||||||
|
title: Avatar Group
|
||||||
|
description: An animated avatar group that displays overlapping user images and smoothly shifts each avatar forward on hover to highlight it.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
## AvatarGroup
|
||||||
|
|
||||||
|
An animated avatar group built with TailwindCSS and Motion, where avatars gently shift forward on hover to highlight each user.
|
||||||
|
For a CSS-only version with a mask effect that blends seamlessly into any background, check out the [AvatarGroupMask](#avatargroupmask) component.
|
||||||
|
|
||||||
|
<ComponentPreview name="avatar-group-demo" />
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="avatar-group" />
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<AvatarGroup>
|
||||||
|
{AVATARS.map((avatar, index) => (
|
||||||
|
<Avatar key={index}>
|
||||||
|
<AvatarImage src={avatar.src} />
|
||||||
|
<AvatarFallback>{avatar.fallback}</AvatarFallback>
|
||||||
|
<AvatarGroupTooltip>
|
||||||
|
<p>{avatar.tooltip}</p>
|
||||||
|
</AvatarGroupTooltip>
|
||||||
|
</Avatar>
|
||||||
|
))}
|
||||||
|
</AvatarGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
#### Bottom translation
|
||||||
|
|
||||||
|
<ComponentPreview name="avatar-group-bottom-demo" />
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
#### AvatarGroup
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The avatars to display in the avatar group',
|
||||||
|
type: 'React.ReactElement[]',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the avatar group',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the avatar group',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: "{ type: 'spring', stiffness: 300, damping: 17 }",
|
||||||
|
},
|
||||||
|
invertOverlap: {
|
||||||
|
description:
|
||||||
|
'By default, the avatar group is rendered in the same z-index as the avatars. This prop allows you to invert the z-index of the avatar group.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
translate: {
|
||||||
|
description: 'The translate y of the avatar group',
|
||||||
|
type: 'string | number',
|
||||||
|
required: false,
|
||||||
|
default: '-30%',
|
||||||
|
},
|
||||||
|
tooltipProps: {
|
||||||
|
description: 'The props to pass to the tooltip',
|
||||||
|
type: 'TooltipProps',
|
||||||
|
required: false,
|
||||||
|
default: "{ side: 'top', sideOffset: 20 }",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
#### AvatarGroupTooltip
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the avatar group tooltip',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## AvatarGroupMask
|
||||||
|
|
||||||
|
A sleek, CSS-only avatar group using TailwindCSS for the translate animation and a mask effect. Perfect for layered or custom backgrounds — inspired by [Jhey](https://codepen.io/jh3y/pen/yyLmmMW)'s awesome work.
|
||||||
|
|
||||||
|
<Callout type="warn">
|
||||||
|
You need **TailwindCSS 4.1 or higher** to use this component.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<ComponentPreview name="avatar-group-mask-demo" />
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="avatar-group-mask" />
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<AvatarGroup>
|
||||||
|
{AVATARS.map((avatar, index) => (
|
||||||
|
<Avatar key={index}>
|
||||||
|
<AvatarImage src={avatar.src} />
|
||||||
|
<AvatarFallback>{avatar.fallback}</AvatarFallback>
|
||||||
|
<AvatarGroupTooltip>
|
||||||
|
<p>{avatar.tooltip}</p>
|
||||||
|
</AvatarGroupTooltip>
|
||||||
|
</Avatar>
|
||||||
|
))}
|
||||||
|
</AvatarGroup>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
#### Bottom translation
|
||||||
|
|
||||||
|
<ComponentPreview name="avatar-group-mask-bottom-demo" />
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
**Note:** Children's avatars must have a `data-slot="avatar"` attribute.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
#### AvatarGroup
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The avatars to display in the avatar group',
|
||||||
|
type: 'React.ReactElement[]',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the avatar group',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
invertOverlap: {
|
||||||
|
description:
|
||||||
|
'By default, mask is rendered in the left side of the avatar group. This prop allows you to invert the mask to the right side.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
description: 'The size of the avatar group',
|
||||||
|
type: 'string | number',
|
||||||
|
required: false,
|
||||||
|
default: '43px',
|
||||||
|
},
|
||||||
|
border: {
|
||||||
|
description: 'The border of the avatar group',
|
||||||
|
type: 'string | number',
|
||||||
|
required: false,
|
||||||
|
default: '3px',
|
||||||
|
},
|
||||||
|
columnSize: {
|
||||||
|
description: 'The size of the avatar group',
|
||||||
|
type: 'string | number',
|
||||||
|
required: false,
|
||||||
|
default: '37px',
|
||||||
|
},
|
||||||
|
align: {
|
||||||
|
description: 'The alignment of the avatar group',
|
||||||
|
type: '"start" | "center" | "end"',
|
||||||
|
required: false,
|
||||||
|
default: 'end',
|
||||||
|
},
|
||||||
|
translate: {
|
||||||
|
description: 'The translate y of the avatar group',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '-30%',
|
||||||
|
},
|
||||||
|
tooltipProps: {
|
||||||
|
description: 'The props to pass to the tooltip',
|
||||||
|
type: 'TooltipProps',
|
||||||
|
required: false,
|
||||||
|
default: "{ side: 'top', sideOffset: 20 }",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
#### AvatarGroupTooltip
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the avatar group tooltip',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Jhey](https://codepen.io/jh3y/pen/yyLmmMW) for the inspiration.
|
||||||
127
apps/www/content/docs/components/code-editor.mdx
Normal file
127
apps/www/content/docs/components/code-editor.mdx
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
---
|
||||||
|
title: Code Editor
|
||||||
|
description: A code editor component featuring syntax highlighting and animation.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="code-editor-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="code-editor" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<CodeEditor lang="tsx">
|
||||||
|
{`const a = 1;
|
||||||
|
const b = 2;
|
||||||
|
const c = a + b;`}
|
||||||
|
</CodeEditor>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The code to display in the editor.',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
lang: {
|
||||||
|
description: 'The language of the code.',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
themes: {
|
||||||
|
description:
|
||||||
|
'The theme of the code editor (see https://shiki.style/themes).',
|
||||||
|
type: '{ light: string; dark: string }',
|
||||||
|
required: false,
|
||||||
|
default: '{ light: "github-light"; dark: "github-dark" }',
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
description: 'The duration of the animation.',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '5',
|
||||||
|
},
|
||||||
|
delay: {
|
||||||
|
description: 'The delay of the animation.',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '0',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
description: 'Whether to show the header.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
dots: {
|
||||||
|
description: 'Whether to show the top left dots.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
description: 'The icon to display in the header.',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
cursor: {
|
||||||
|
description: 'Whether to show the cursor.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
inView: {
|
||||||
|
description: 'Enable animation when in view.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
inViewMargin: {
|
||||||
|
description: 'The margin of the in view.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
default: '0px',
|
||||||
|
},
|
||||||
|
inViewOnce: {
|
||||||
|
description: 'Enable animation only once when in view.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
copyButton: {
|
||||||
|
description: 'Whether to show the copy button.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
writing: {
|
||||||
|
description: 'Enable writing animation.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
description: 'The file name.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
onDone: {
|
||||||
|
description: 'Callback when the animation is done.',
|
||||||
|
type: '() => void',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
onCopy: {
|
||||||
|
description: 'The function to call when copy is successful.',
|
||||||
|
type: '(content: string) => void',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
65
apps/www/content/docs/components/code-tabs.mdx
Normal file
65
apps/www/content/docs/components/code-tabs.mdx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: Code Tabs
|
||||||
|
description: A tabs component that displays code for different languages.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
The InstallTabs component has been updated and is now called CodeTabs. It can
|
||||||
|
now be used to make both install tabs and code tabs!
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
<ComponentPreview name="code-tabs-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="code-tabs" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<CodeTabs codes={codes} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Install Tabs
|
||||||
|
|
||||||
|
<ComponentPreview name="install-tabs-demo" />
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
codes: {
|
||||||
|
description: 'The codes to display in the tabs',
|
||||||
|
type: 'Record<string, string>',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
lang: {
|
||||||
|
description: 'The language of the code',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
default: 'bash',
|
||||||
|
},
|
||||||
|
themes: {
|
||||||
|
description: 'The shiki themes to display in the tabs',
|
||||||
|
type: 'Record<string, string>',
|
||||||
|
required: false,
|
||||||
|
default: "{ light: 'github-light', dark: 'github-dark' }",
|
||||||
|
},
|
||||||
|
copyButton: {
|
||||||
|
description: 'Whether to show the copy button.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
onCopy: {
|
||||||
|
description: 'The function to call when copy is successful.',
|
||||||
|
type: '(content: string) => void',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
57
apps/www/content/docs/components/counter.mdx
Normal file
57
apps/www/content/docs/components/counter.mdx
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: Counter
|
||||||
|
description: A numeric input control featuring increment and decrement buttons, smoothly animating number transitions using the SlidingNumber component.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="counter-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="counter" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Counter number={number} setNumber={setNumber} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
buttonProps: {
|
||||||
|
description: 'The props to pass to the Button component',
|
||||||
|
type: "Omit<React.ComponentProps<typeof Button>, 'onClick'>",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className to pass to the component',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
number: {
|
||||||
|
description: 'The number to display',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
setNumber: {
|
||||||
|
description: 'The function to set the number',
|
||||||
|
type: '(number: number) => void',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
slidingNumberProps: {
|
||||||
|
description: 'The props to pass to the SlidingNumber component',
|
||||||
|
type: "Omit<SlidingNumberProps, 'number'>",
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the counter width',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: '{ type: "spring", bounce: 0, stiffness: 300, damping: 30 }',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
90
apps/www/content/docs/components/cursor.mdx
Normal file
90
apps/www/content/docs/components/cursor.mdx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
title: Cursor
|
||||||
|
description: An animated cursor component that allows you to customize both the cursor and cursor follow elements with smooth animations.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="cursor-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="cursor" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<CursorProvider>
|
||||||
|
<Cursor>
|
||||||
|
<svg>{...}</svg>
|
||||||
|
</Cursor>
|
||||||
|
<CursorFollow>
|
||||||
|
<div>Follow me</div>
|
||||||
|
</CursorFollow>
|
||||||
|
</CursorProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Cursor Follow Only
|
||||||
|
|
||||||
|
You can use only the `CursorFollow` (like the example below) component or the `Cursor` component.
|
||||||
|
|
||||||
|
<ComponentPreview name="cursor-follow-only-demo" />
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### CursorProvider
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the cursor provider',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### Cursor
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the cursor',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### CursorFollow
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the cursor follow',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
sideOffset: {
|
||||||
|
description: 'The side offset of the cursor follow',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: 15,
|
||||||
|
},
|
||||||
|
align: {
|
||||||
|
description: 'The align of the cursor follow',
|
||||||
|
type: "'top' | 'top-left' | 'top-right' | 'bottom' | 'bottom-left' | 'bottom-right' | 'left' | 'right' | 'center'",
|
||||||
|
required: false,
|
||||||
|
default: 'bottom-right',
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the cursor follow',
|
||||||
|
type: 'SpringOptions',
|
||||||
|
required: false,
|
||||||
|
default: '{ stiffness: 500, damping: 50, bounce: 0 }',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
137
apps/www/content/docs/components/files.mdx
Normal file
137
apps/www/content/docs/components/files.mdx
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
---
|
||||||
|
title: Files
|
||||||
|
description: A component that allows you to display a list of files and folders.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="files-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="files" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Files>
|
||||||
|
<Folder name="Folder 1">
|
||||||
|
<File name="file1.txt" />
|
||||||
|
<File name="file2.txt" />
|
||||||
|
</Folder>
|
||||||
|
<Folder name="Folder 2">
|
||||||
|
<File name="file3.txt" />
|
||||||
|
<File name="file4.txt" />
|
||||||
|
</Folder>
|
||||||
|
</Files>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Files Advanced
|
||||||
|
|
||||||
|
<ComponentPreview name="files-advanced-demo" />
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### Files
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the component.',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The class name of the component.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
activeClassName: {
|
||||||
|
description: 'The class name of the highlight.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
defaultOpen: {
|
||||||
|
description: 'The child folders open by default.',
|
||||||
|
type: 'string[]',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
open: {
|
||||||
|
description: 'The child folders open.',
|
||||||
|
type: 'string[]',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
onOpenChange: {
|
||||||
|
description: 'The callback function when the child folders open change.',
|
||||||
|
type: 'function',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
sideComponent: {
|
||||||
|
description:
|
||||||
|
'The component to display on the right side of the component.',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### Folder
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
name: {
|
||||||
|
description: 'The name of the folder.',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
description: 'The children of the component.',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The class name of the component.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
defaultOpen: {
|
||||||
|
description: 'The child folders open by default.',
|
||||||
|
type: 'string[]',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
open: {
|
||||||
|
description: 'The child folders open.',
|
||||||
|
type: 'string[]',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
onOpenChange: {
|
||||||
|
description: 'The callback function when the child folders open change.',
|
||||||
|
type: 'function',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### File
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
className: {
|
||||||
|
description: 'The class name of the component.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
description: 'The name of the file.',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Fumadocs](https://fumadocs.vercel.app/docs/ui/components/files) for the inspiration
|
||||||
65
apps/www/content/docs/components/liquid-glass.mdx
Normal file
65
apps/www/content/docs/components/liquid-glass.mdx
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
title: Liquid Glass
|
||||||
|
description: A component that allows you to display a liquid glass effect, inspired by iOS26.
|
||||||
|
author:
|
||||||
|
name: lucasporrini
|
||||||
|
url: https://github.com/lucasporrini
|
||||||
|
new: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="liquid-glass-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="liquid-glass" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<LiquidGlass>Hello world</LiquidGlass>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
as: {
|
||||||
|
description: 'The element or component the liquid glass should render as',
|
||||||
|
type: 'String | Component',
|
||||||
|
default: 'div',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
radius: {
|
||||||
|
description: 'The radius of the liquid glass',
|
||||||
|
type: 'Number',
|
||||||
|
default: '25',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
blur: {
|
||||||
|
description: 'The blur of the liquid glass background',
|
||||||
|
type: 'Number',
|
||||||
|
default: '0',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
childClassName: {
|
||||||
|
description: 'The class name of the child element',
|
||||||
|
type: 'String',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The class name of the liquid glass',
|
||||||
|
type: 'String',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
description: 'The children of the liquid glass',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Lucas Porrini](https://github.com/lucasporrini) for creating the component
|
||||||
|
- Credits to [@rebane2001](https://x.com/rebane2001/status/1932451851523264749?s=46&t=SBwgWoAagRgIPNE-RF4R9w) for effect inspiration
|
||||||
76
apps/www/content/docs/components/motion-grid.mdx
Normal file
76
apps/www/content/docs/components/motion-grid.mdx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
title: Motion Grid
|
||||||
|
description: A grid that displays animations in a grid.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
new: true
|
||||||
|
---
|
||||||
|
|
||||||
|
import { MotionGridEditor } from '../../../components/docs/motion-grid-editor';
|
||||||
|
|
||||||
|
<ComponentPreview name="motion-grid-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="motion-grid" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<MotionGrid gridSize={[5, 5]} frames={frames} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Editor
|
||||||
|
|
||||||
|
<MotionGridEditor />
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### MotionGrid
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
gridSize: {
|
||||||
|
description: 'The size of the grid as [rows, columns].',
|
||||||
|
type: '[number, number]',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
frames: {
|
||||||
|
description: 'Array of frame data for the grid.',
|
||||||
|
type: '[number, number][][]',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
duration: {
|
||||||
|
description: 'The duration of the animation in milliseconds.',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '200',
|
||||||
|
},
|
||||||
|
animate: {
|
||||||
|
description: 'Whether to animate the grid.',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
cellClassName: {
|
||||||
|
description: 'Class name for the cells.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
cellActiveClassName: {
|
||||||
|
description: 'Class name for the active cell.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
cellInactiveClassName: {
|
||||||
|
description: 'Class name for the inactive cell.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Adrien Griveau](https://x.com/Griveau) for [this X post](https://x.com/Griveau/status/1932832554354163804) as inspiration
|
||||||
88
apps/www/content/docs/components/pin-list.mdx
Normal file
88
apps/www/content/docs/components/pin-list.mdx
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
title: Pin List
|
||||||
|
description: A playful list for pinning and unpinning items, with smooth animated transitions as items move between groups.
|
||||||
|
author:
|
||||||
|
name: arhamkhnz
|
||||||
|
url: https://github.com/arhamkhnz
|
||||||
|
new: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="pin-list-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="pin-list" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<PinList items={ITEMS} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### PinList
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
items: {
|
||||||
|
description:
|
||||||
|
'Array of items to show in the list. Each item should have id, name, info, icon, and pinned.',
|
||||||
|
type: 'PinListItem[]',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
description:
|
||||||
|
'Custom labels for the pinned and unpinned sections. Example: { pinned: "Favorites", unpinned: "All" }.',
|
||||||
|
type: '{ pinned?: string; unpinned?: string; }',
|
||||||
|
required: false,
|
||||||
|
default: '{ pinned: "Pinned Items", unpinned: "All Items" }',
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description:
|
||||||
|
'Spring animation config for item transitions. Controls the movement physics. Example: { stiffness: 320, damping: 20, mass: 0.8, type: "spring" }',
|
||||||
|
type: '{ stiffness?: number; damping?: number; mass?: number; type?: string; }',
|
||||||
|
required: false,
|
||||||
|
default: '{ stiffness: 320, damping: 20, mass: 0.8, type: "spring" }',
|
||||||
|
},
|
||||||
|
labelMotionProps: {
|
||||||
|
description:
|
||||||
|
'Motion props for the section labels. Allows customizing the label enter/exit animation. Example: { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.22, ease: "easeInOut" } }',
|
||||||
|
type: '{ initial?: object; animate?: object; exit?: object; transition?: object; }',
|
||||||
|
required: false,
|
||||||
|
default:
|
||||||
|
'{ initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: { duration: 0.22, ease: "easeInOut" } }',
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'Class name for the outermost container.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
labelClassName: {
|
||||||
|
description: 'Class name for the section labels.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
pinnedSectionClassName: {
|
||||||
|
description: 'Class name for the pinned items group.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
unpinnedSectionClassName: {
|
||||||
|
description: 'Class name for the unpinned items group.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
zIndexResetDelay: {
|
||||||
|
description:
|
||||||
|
'Delay (in ms) before resetting z-index after toggle, should match your animation duration if changed.',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '500',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Nitish Khagwal](https://x.com/nitishkmrk) for [this X post](https://x.com/nitishkmrk/status/1933050634594660800) as inspiration
|
||||||
50
apps/www/content/docs/components/scroll-progress.mdx
Normal file
50
apps/www/content/docs/components/scroll-progress.mdx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
---
|
||||||
|
title: Scroll Progress
|
||||||
|
description: A scroll progress component that displays the progress of the scroll.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="scroll-progress-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="scroll-progress" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Global Scroll Progress
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ScrollProgress />
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local Scroll Progress
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<ScrollProgress progressProps={{ className: 'absolute' }}>...</ScrollProgress>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
progressProps: {
|
||||||
|
description: 'The props for the progress element',
|
||||||
|
type: 'HTMLMotionProps<"div">',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The class name for the scroll progress',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
description:
|
||||||
|
'The children for the scroll progress (if not provided, the progress will be global)',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
82
apps/www/content/docs/components/spring-element.mdx
Normal file
82
apps/www/content/docs/components/spring-element.mdx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
title: Spring Element
|
||||||
|
description: A flexible, animated spring component that attaches a draggable element (avatar, text, icon, or any React node) to its origin with a spring line.
|
||||||
|
author:
|
||||||
|
name: arhamkhnz
|
||||||
|
url: https://github.com/arhamkhnz
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="spring-element-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="spring-element" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<SpringElement>
|
||||||
|
<Avatar className="size-12">
|
||||||
|
<AvatarImage draggable={false} src={USER.src} />
|
||||||
|
<AvatarFallback>{USER.fallback}</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</SpringElement>
|
||||||
|
```
|
||||||
|
|
||||||
|
<Callout type="info">
|
||||||
|
**Note:** Add <code>draggable={false}</code> to your element
|
||||||
|
(avatar, image, etc.) when passing it as children. This prevents the browser's
|
||||||
|
native drag image from interfering with the spring drag animation.
|
||||||
|
</Callout>
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### SpringElement
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description:
|
||||||
|
'The element to attach the spring to. Can be any React node (avatar, text, icon, etc.).',
|
||||||
|
type: 'React.ReactElement',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'ClassName for the element.',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
springClassName: {
|
||||||
|
description:
|
||||||
|
'ClassName for the spring path (to control stroke color, thickness, etc.). Default is "stroke-2 stroke-neutral-900 fill-none".',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
dragElastic: {
|
||||||
|
description:
|
||||||
|
'How elastic the drag interaction feels. Lower values = less stretch. (Framer Motion prop)',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '0.4',
|
||||||
|
},
|
||||||
|
springConfig: {
|
||||||
|
description:
|
||||||
|
'Spring physics config for the element motion. (stiffness, damping)',
|
||||||
|
type: '{ stiffness?: number; damping?: number }',
|
||||||
|
required: false,
|
||||||
|
default: '{ stiffness: 200, damping: 16 }',
|
||||||
|
},
|
||||||
|
springPathConfig: {
|
||||||
|
description:
|
||||||
|
'Config for the spring path itself: number of coils, amplitude, curve ratios, etc.',
|
||||||
|
type: '{ coilCount?: number; amplitudeMin?: number; amplitudeMax?: number; curveRatioMin?: number; curveRatioMax?: number; bezierOffset?: number; }',
|
||||||
|
required: false,
|
||||||
|
default:
|
||||||
|
'{ coilCount: 8, amplitudeMin: 8, amplitudeMax: 20, curveRatioMin: 0.5, curveRatioMax: 1, bezierOffset: 8 }',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Anh](https://x.com/pwign) for [this X post](https://x.com/pwign/status/1927760004183970067) as inspiration
|
||||||
80
apps/www/content/docs/components/stars-scrolling-wheel.mdx
Normal file
80
apps/www/content/docs/components/stars-scrolling-wheel.mdx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
---
|
||||||
|
title: Stars Scrolling Wheel
|
||||||
|
description: A scrolling wheel that displays stars count.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
new: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="stars-scrolling-wheel-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="stars-scrolling-wheel" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<StarsScrollingWheel stars={1000} />
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
stars: {
|
||||||
|
description: 'The number of stars to display',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
step: {
|
||||||
|
description: 'The step of the stars',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '100',
|
||||||
|
},
|
||||||
|
itemHeight: {
|
||||||
|
description: 'The height of the item',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '48',
|
||||||
|
},
|
||||||
|
sideItemsCount: {
|
||||||
|
description: 'The number of side items',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '2',
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The spring transition of the stars scrolling wheel',
|
||||||
|
type: 'SpringOptions',
|
||||||
|
required: false,
|
||||||
|
default: '{ stiffness: 90, damping: 30 }',
|
||||||
|
},
|
||||||
|
inView: {
|
||||||
|
description: 'Whether the component is in view',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
inViewOnce: {
|
||||||
|
description: 'Whether the component should only animate once',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
inViewMargin: {
|
||||||
|
description: 'The margin of the component',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
default: '0px',
|
||||||
|
},
|
||||||
|
delay: {
|
||||||
|
description: 'The delay of the component',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '0',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
157
apps/www/content/docs/components/tabs.mdx
Normal file
157
apps/www/content/docs/components/tabs.mdx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
---
|
||||||
|
title: Tabs
|
||||||
|
description: A set of layered sections with the same height of content—known as tab panels—that are displayed one at a time.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="tabs-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="tabs" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Tabs>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="tab1">Tab 1</TabsTrigger>
|
||||||
|
<TabsTrigger value="tab2">Tab 2</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContents>
|
||||||
|
<TabsContent value="tab1">Content 1</TabsContent>
|
||||||
|
<TabsContent value="tab2">Content 2</TabsContent>
|
||||||
|
</TabsContents>
|
||||||
|
</Tabs>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### Tabs
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the tabs',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the tabs',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
description: 'The default value of the tabs',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
onValueChange: {
|
||||||
|
description: 'The function to handle the value change',
|
||||||
|
type: '(value: string) => void',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
description: 'The value of the tabs',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### TabsList
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
activeClassName: {
|
||||||
|
description: 'The className of the active tab',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
description: 'The children of the tabs list',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the tabs list',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the tabs list',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: '{ type: "spring", stiffness: 200, damping: 25 }',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### TabsTrigger
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the tabs trigger',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the tabs trigger',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
description: 'The value of the tabs trigger',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### TabsContents
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the tabs contents',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the tabs contents',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the tabs contents',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: '{ duration: 0.4, ease: "easeInOut" }',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### TabsContent
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the tabs content',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the tabs content',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
description: 'The value of the tabs content',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
128
apps/www/content/docs/components/tooltip.mdx
Normal file
128
apps/www/content/docs/components/tooltip.mdx
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
title: Tooltip
|
||||||
|
description: An animated tooltip that shows contextual info on hover or focus and smoothly glides to the next element without disappearing between transitions.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="tooltip-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="tooltip" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<span>Hover me</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Tooltip content</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger>
|
||||||
|
<span>And me</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>Tooltip content</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### TooltipProvider
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the tooltip provider',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
openDelay: {
|
||||||
|
description: 'The delay in milliseconds before the tooltip is shown',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: 700,
|
||||||
|
},
|
||||||
|
closeDelay: {
|
||||||
|
description: 'The delay in milliseconds before the tooltip is hidden',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: 300,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the tooltip',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: "{ type: 'spring', stiffness: 300, damping: 25 }",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### Tooltip
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the tooltip',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
side: {
|
||||||
|
description: 'The side of the tooltip',
|
||||||
|
type: '"top" | "bottom" | "left" | "right"',
|
||||||
|
required: false,
|
||||||
|
default: 'top',
|
||||||
|
},
|
||||||
|
sideOffset: {
|
||||||
|
description: 'The offset of the tooltip',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: 4,
|
||||||
|
},
|
||||||
|
align: {
|
||||||
|
description: 'The alignment of the tooltip',
|
||||||
|
type: '"start" | "center" | "end"',
|
||||||
|
required: false,
|
||||||
|
default: 'center',
|
||||||
|
},
|
||||||
|
alignOffset: {
|
||||||
|
description: 'The offset of the tooltip',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### TooltipContent
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the tooltip content',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### TooltipTrigger
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the tooltip trigger',
|
||||||
|
type: 'React.ReactElement',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/tooltip) for the tooltip style.
|
||||||
64
apps/www/content/docs/effects/magnetic.mdx
Normal file
64
apps/www/content/docs/effects/magnetic.mdx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
title: Magnetic
|
||||||
|
description: A magnetic effect that clings to the cursor, creating a magnetic attraction effect.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
new: true
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="magnetic-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="magnetic" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Magnetic>
|
||||||
|
<p>Magnetic</p>
|
||||||
|
</Magnetic>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the magnetic',
|
||||||
|
type: 'React.ReactElement',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
disableOnTouch: {
|
||||||
|
description: 'Whether the magnetic effect should be disabled on touch',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
defaultValue: 'true',
|
||||||
|
},
|
||||||
|
onlyOnHover: {
|
||||||
|
description: 'Whether the magnetic effect should only be active on hover',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
defaultValue: 'false',
|
||||||
|
},
|
||||||
|
strength: {
|
||||||
|
description: 'The strength of the magnetic effect',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
defaultValue: '0.5',
|
||||||
|
},
|
||||||
|
range: {
|
||||||
|
description: 'The range of the magnetic effect',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
defaultValue: '120',
|
||||||
|
},
|
||||||
|
springOptions: {
|
||||||
|
description: 'The spring options of the magnetic effect',
|
||||||
|
type: 'SpringOptions',
|
||||||
|
required: false,
|
||||||
|
defaultValue: '{ stiffness: 100, damping: 10, mass: 0.5 }',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
106
apps/www/content/docs/effects/motion-effect.mdx
Normal file
106
apps/www/content/docs/effects/motion-effect.mdx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
title: Motion Effect
|
||||||
|
description: Motion effect component that displays the motion effect (fade in, slide in, etc.).
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="motion-effect-image-grid-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="motion-effect" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<MotionEffect slide>
|
||||||
|
<p>Motion Effect</p>
|
||||||
|
</MotionEffect>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Slide
|
||||||
|
|
||||||
|
<ComponentPreview name="motion-effect-slide-demo" />
|
||||||
|
|
||||||
|
### Fade Blur
|
||||||
|
|
||||||
|
<ComponentPreview name="motion-effect-fade-blur-demo" />
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the slide in',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the slide in',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the slide in',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: "{ type: 'spring', stiffness: 200, damping: 20 }",
|
||||||
|
},
|
||||||
|
inView: {
|
||||||
|
description: 'Whether the slide in is in view',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
inViewMargin: {
|
||||||
|
description: 'The margin of the slide in when it is in view',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
default: '0px',
|
||||||
|
},
|
||||||
|
inViewOnce: {
|
||||||
|
description: 'Whether the slide in is in view once',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
delay: {
|
||||||
|
description: 'The delay of the slide in',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '0',
|
||||||
|
},
|
||||||
|
blur: {
|
||||||
|
description: 'The blur of the slide in',
|
||||||
|
type: 'boolean | string',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
slide: {
|
||||||
|
description: 'The slide of the slide in',
|
||||||
|
type: "boolean | { direction?: 'up' | 'down' | 'left' | 'right'; offset?: number }",
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
fade: {
|
||||||
|
description: 'The fade of the slide in',
|
||||||
|
type: 'boolean | { initialOpacity?: number; opacity?: number }',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
description: 'The zoom of the slide in',
|
||||||
|
type: 'boolean | { initialScale?: number; scale?: number }',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [Magic UI](https://magicui.design/docs/components/blur-fade) for the inspiration
|
||||||
345
apps/www/content/docs/effects/motion-highlight.mdx
Normal file
345
apps/www/content/docs/effects/motion-highlight.mdx
Normal file
@ -0,0 +1,345 @@
|
|||||||
|
---
|
||||||
|
title: Motion Highlight
|
||||||
|
description: Motion highlight component that displays the motion highlight effect.
|
||||||
|
author:
|
||||||
|
name: imskyleen
|
||||||
|
url: https://github.com/imskyleen
|
||||||
|
---
|
||||||
|
|
||||||
|
<ComponentPreview name="motion-highlight-cards-hover-demo" />
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<ComponentInstallation name="motion-highlight" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<MotionHighlight>
|
||||||
|
<div>Item 1</div>
|
||||||
|
<div>Item 2</div>
|
||||||
|
<div>Item 3</div>
|
||||||
|
</MotionHighlight>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Tabs
|
||||||
|
|
||||||
|
<ComponentPreview name="motion-highlight-tabs-demo" />
|
||||||
|
|
||||||
|
### Tabs Hover
|
||||||
|
|
||||||
|
<ComponentPreview name="motion-highlight-tabs-hover-demo" />
|
||||||
|
|
||||||
|
### Tabs Hover with Parent Mode
|
||||||
|
|
||||||
|
<ComponentPreview name="motion-highlight-tabs-hover-parent-demo" />
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Uncontrolled Items
|
||||||
|
|
||||||
|
By default, child items will have the highlight effect. This will map the children elements and surround them with the `MotionHighlightItem` element.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<MotionHighlight>
|
||||||
|
<div>Item 1</div>
|
||||||
|
<div>Item 2</div>
|
||||||
|
<div>Item 3</div>
|
||||||
|
</MotionHighlight>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Controlled Items
|
||||||
|
|
||||||
|
Using the `controlledItems` props, you place your `MotionHighlightItem` wherever you like in the `MotionHighlight` component.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<MotionHighlight controlledItems>
|
||||||
|
<MotionHighlightItem>Item 1</MotionHighlightItem>
|
||||||
|
<div>
|
||||||
|
<MotionHighlightItem>Item 2</MotionHighlightItem>
|
||||||
|
</div>
|
||||||
|
<MotionHighlightItem>Item 3</MotionHighlightItem>
|
||||||
|
</MotionHighlight>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Children Mode
|
||||||
|
|
||||||
|
By default, in `mode` with the value `children` the highlight is placed in the `MotionHighlightItem` component using Motion's `layoutId` technique. This technique should be preferred in most cases. However, in some cases, you may run into z-index problems, so it's best to use the `parent` mode to avoid visual bugs.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<MotionHighlight>
|
||||||
|
<div>Item 1</div>
|
||||||
|
<div>Item 2</div>
|
||||||
|
<div>Item 3</div>
|
||||||
|
</MotionHighlight>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parent Mode
|
||||||
|
|
||||||
|
By using the `mode` property with the value `parent`, the `MotionHighlight` component will set the highlight to absolute in `MotionHighlight`. This may be useful in some cases to avoid the visual z-index bugs you can get with `mode` `children`.
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<MotionHighlight mode="parent">
|
||||||
|
<div>Item 1</div>
|
||||||
|
<div>Item 2</div>
|
||||||
|
<div>Item 3</div>
|
||||||
|
</MotionHighlight>
|
||||||
|
```
|
||||||
|
|
||||||
|
### MotionHighlightItem asChild
|
||||||
|
|
||||||
|
#### Children Mode
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// With asChild
|
||||||
|
<MotionHighlight>
|
||||||
|
<MotionHighlightItem asChild>
|
||||||
|
<div id="item-1">Item 1</div>
|
||||||
|
</MotionHighlightItem>
|
||||||
|
<MotionHighlightItem asChild>
|
||||||
|
<div id="item-2">Item 2</div>
|
||||||
|
</MotionHighlightItem>
|
||||||
|
</MotionHighlight>
|
||||||
|
// Result
|
||||||
|
<div className="relative" id="item-1">
|
||||||
|
<div /> {/* Highlight */}
|
||||||
|
<div className="relative z-[1]">Item 1</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative" id="item-2">
|
||||||
|
<div /> {/* Highlight */}
|
||||||
|
<div className="relative z-[1]">Item 2</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Without asChild
|
||||||
|
<MotionHighlight>
|
||||||
|
<MotionHighlightItem>
|
||||||
|
<div id="item-1">Item 1</div>
|
||||||
|
</MotionHighlightItem>
|
||||||
|
<MotionHighlightItem>
|
||||||
|
<div id="item-2">Item 2</div>
|
||||||
|
</MotionHighlightItem>
|
||||||
|
</MotionHighlight>
|
||||||
|
// Result
|
||||||
|
<div className="relative" >
|
||||||
|
<div /> {/* Highlight */}
|
||||||
|
<div className="relative z-[1]" id="item-1">Item 1</div>
|
||||||
|
</div>
|
||||||
|
<div className="relative" >
|
||||||
|
<div /> {/* Highlight */}
|
||||||
|
<div className="relative z-[1]" id="item-2">Item 2</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Parent Mode
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// With asChild
|
||||||
|
<MotionHighlight mode="parent">
|
||||||
|
<MotionHighlightItem asChild>
|
||||||
|
<div id="item-1">Item 1</div>
|
||||||
|
</MotionHighlightItem>
|
||||||
|
<MotionHighlightItem asChild>
|
||||||
|
<div id="item-2">Item 2</div>
|
||||||
|
</MotionHighlightItem>
|
||||||
|
</MotionHighlight>
|
||||||
|
// Result
|
||||||
|
<div className="relative">
|
||||||
|
<div /> {/* Highlight */}
|
||||||
|
<div id="item-1">Item 1</div>
|
||||||
|
<div id="item-2">Item 2</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Without asChild
|
||||||
|
<MotionHighlight mode="parent">
|
||||||
|
<MotionHighlightItem>
|
||||||
|
<div id="item-1">Item 1</div>
|
||||||
|
</MotionHighlightItem>
|
||||||
|
<MotionHighlightItem>
|
||||||
|
<div id="item-2">Item 2</div>
|
||||||
|
</MotionHighlightItem>
|
||||||
|
</MotionHighlight>
|
||||||
|
// Result
|
||||||
|
<div className="relative">
|
||||||
|
<div /> {/* Highlight */}
|
||||||
|
<div>
|
||||||
|
<div id="item-1">Item 1</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div id="item-2">Item 2</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
### MotionHighlight
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the motion highlight',
|
||||||
|
type: 'React.ReactElement | React.ReactElement[]',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
children: {
|
||||||
|
description: 'The children of the motion highlight',
|
||||||
|
type: 'React.ReactNode',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the motion highlight',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
containerClassName: {
|
||||||
|
description:
|
||||||
|
'The className of the container of the motion highlight (only used when mode is "parent")',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
itemsClassName: {
|
||||||
|
description:
|
||||||
|
'The className of the items of the motion highlight (only used when controlledItems is true)',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
controlledItems: {
|
||||||
|
description: 'Whether the motion highlight is controlled',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
description: 'The value of the motion highlight',
|
||||||
|
type: 'string | null',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
defaultValue: {
|
||||||
|
description: 'The default value of the motion highlight',
|
||||||
|
type: 'string | null',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
onValueChange: {
|
||||||
|
description: 'The function to call when the value changes',
|
||||||
|
type: '(value: string | null) => void',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the motion highlight',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
default: '{ type: "spring", stiffness: 200, damping: 25 }',
|
||||||
|
},
|
||||||
|
hover: {
|
||||||
|
description: 'Whether the motion highlight is on hover',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
enabled: {
|
||||||
|
description: 'Whether the motion highlight is enabled',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'true',
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
description: 'Whether the motion highlight items is disabled',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
exitDelay: {
|
||||||
|
description: 'The delay of the exit of the motion highlight items',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
default: '0.2',
|
||||||
|
},
|
||||||
|
mode: {
|
||||||
|
description: 'The mode of the motion highlight',
|
||||||
|
type: "'children' | 'parent'",
|
||||||
|
required: false,
|
||||||
|
default: 'children',
|
||||||
|
},
|
||||||
|
boundsOffset: {
|
||||||
|
description:
|
||||||
|
'The offset of the bounds of the motion highlight (only used when mode is "parent"). For example, you need to add an offset of the same size as the border when setting a border in containerClassName.',
|
||||||
|
type: 'Partial<Bounds>',
|
||||||
|
required: false,
|
||||||
|
default: 'undefined',
|
||||||
|
},
|
||||||
|
forceUpdateBounds: {
|
||||||
|
description:
|
||||||
|
'Whether to force update the bounds of the motion highlight items (only used when mode is "parent")',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'undefined',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
### MotionHighlightItem
|
||||||
|
|
||||||
|
<TypeTable
|
||||||
|
type={{
|
||||||
|
children: {
|
||||||
|
description: 'The children of the motion highlight item',
|
||||||
|
type: 'React.ReactElement',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
description: 'The className of the motion highlight item',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
activeClassName: {
|
||||||
|
description: 'The className of the active motion highlight item',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
transition: {
|
||||||
|
description: 'The transition of the motion highlight item',
|
||||||
|
type: 'Transition',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
description: 'The id of the motion highlight item',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
description: 'The value of the motion highlight item',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
description: 'Whether the motion highlight item is disabled',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
exitDelay: {
|
||||||
|
description: 'The delay of the exit of the motion highlight item',
|
||||||
|
type: 'number',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
asChild: {
|
||||||
|
description: 'Whether to use the child component as the component',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'false',
|
||||||
|
},
|
||||||
|
forceUpdateBounds: {
|
||||||
|
description:
|
||||||
|
'Whether to force update the bounds of the motion highlight item (only used when mode is "parent")',
|
||||||
|
type: 'boolean',
|
||||||
|
required: false,
|
||||||
|
default: 'undefined',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
- Credits to [motion-primitives](https://motion-primitives.com/docs/animated-background) for the inspiration
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user