feat: Dekstop Revamp (#2877)

* feat: desktop revamp

* feat: refactor system monitor

* fix linter CI

* remove unused import component

* added responsive and resizeable component

* responsive and resizeable local server page

* finalize responsive and resizeable component

* fix scroll custom ui

* remove react scroll to bottom from modal troubleshoot

* fix modal troubleshoot ui

* fix setting gpu list

* text area custom scroll bar

* fix padding message input

* cleanup classname

* update inference engine model dropdown

* update loader style

* update quick ask ui

* prepare theme provider

* update dark theme

* remove update hotkey list model and navigation

* fix: cleanup hardcode classname

* fix: update feedback

* Set native theme electron

* update destop ui revamp from feedback

* update button icon component insider icon chat input message

* update model dropdown ui

* update tranaparent baclground

* update logo model provider

* fix: set background material acrylic support to blur background windows

* fix: update tranparent left and right panel

* fix: linter CI

* update app using frameless window

* styling custom style minimize, maximize and close app

* temporary hidden maximize window

* fix: responsive left and right panel

* fix: enable click outside when leftpanel responsive

* fix: remove unused import

* update transparent variable css windows

* fix: ui import model

* feat: Support Theme system (#2946)

* feat: update support theme system

* update select component

* feat: add theme folder in root project

* fix: padding left and right center panel

* fix: update padding left and right

* chore: migrate themes

* fix: rmdirsync error

* chore: update gitignore

* fix: cp recursive

* fix: files electron package json

* fix: migration

* fix: update fgit ignore

---------

Co-authored-by: Louis <louis@jan.ai>

* fix: update feedback missing state when refrash app

* fix: error test CI

* chore: refactor useLoadThemes

* chore: cleanup unused vars

* fix: revert back menubar windows

* fix minor ui

* fix: minor ui

---------

Co-authored-by: Louis <louis@jan.ai>
This commit is contained in:
Faisal Amir 2024-05-29 13:37:18 +07:00 committed by GitHub
parent 611a361672
commit faa09bd2bf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
287 changed files with 8712 additions and 7660 deletions

View File

@ -6,17 +6,17 @@ on:
- main
- dev
paths:
- "electron/**"
- 'electron/**'
- .github/workflows/jan-electron-linter-and-test.yml
- "web/**"
- "uikit/**"
- "package.json"
- "node_modules/**"
- "yarn.lock"
- "core/**"
- "extensions/**"
- "!README.md"
- "Makefile"
- 'web/**'
- 'joi/**'
- 'package.json'
- 'node_modules/**'
- 'yarn.lock'
- 'core/**'
- 'extensions/**'
- '!README.md'
- 'Makefile'
pull_request:
branches:
@ -24,17 +24,17 @@ on:
- dev
- release/**
paths:
- "electron/**"
- 'electron/**'
- .github/workflows/jan-electron-linter-and-test.yml
- "web/**"
- "uikit/**"
- "package.json"
- "node_modules/**"
- "yarn.lock"
- "Makefile"
- "extensions/**"
- "core/**"
- "!README.md"
- 'web/**'
- 'joi/**'
- 'package.json'
- 'node_modules/**'
- 'yarn.lock'
- 'Makefile'
- 'extensions/**'
- 'core/**'
- '!README.md'
jobs:
test-on-macos:
@ -51,23 +51,23 @@ jobs:
with:
node-version: 20
- name: "Cleanup cache"
- name: 'Cleanup cache'
continue-on-error: true
run: |
rm -rf ~/jan
make clean
- name: Get Commit Message for PR
if : github.event_name == 'pull_request'
if: github.event_name == 'pull_request'
run: |
echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}})" >> $GITHUB_ENV
- name: Get Commit Message for push event
if : github.event_name == 'push'
if: github.event_name == 'push'
run: |
echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}})" >> $GITHUB_ENV
- name: "Config report portal"
- name: 'Config report portal'
run: |
make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App macos" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
@ -77,10 +77,10 @@ jobs:
yarn config set registry ${{ secrets.NPM_PROXY }} --global
make test
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
TURBO_API: "${{ secrets.TURBO_API }}"
TURBO_TEAM: "macos"
TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}"
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
TURBO_API: '${{ secrets.TURBO_API }}'
TURBO_TEAM: 'macos'
TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}'
test-on-macos-pr-target:
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
@ -96,7 +96,7 @@ jobs:
with:
node-version: 20
- name: "Cleanup cache"
- name: 'Cleanup cache'
continue-on-error: true
run: |
rm -rf ~/jan
@ -108,14 +108,14 @@ jobs:
yarn config set registry https://registry.npmjs.org --global
make test
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
test-on-windows:
if: github.event_name == 'push'
strategy:
fail-fast: false
matrix:
antivirus-tools: ['mcafee', 'default-windows-security','bit-defender']
antivirus-tools: ['mcafee', 'default-windows-security', 'bit-defender']
runs-on: windows-desktop-${{ matrix.antivirus-tools }}
steps:
- name: Getting the repo
@ -129,7 +129,7 @@ jobs:
node-version: 20
# Clean cache, continue on error
- name: "Cleanup cache"
- name: 'Cleanup cache'
shell: powershell
continue-on-error: true
run: |
@ -140,14 +140,14 @@ jobs:
Write-Output "Folder does not exist."
}
make clean
- name: Get Commit Message for push event
if : github.event_name == 'push'
if: github.event_name == 'push'
shell: bash
run: |
echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}}" >> $GITHUB_ENV
- name: "Config report portal"
- name: 'Config report portal'
shell: bash
run: |
make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Windows ${{ matrix.antivirus-tools }}" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
@ -159,9 +159,9 @@ jobs:
yarn config set registry ${{ secrets.NPM_PROXY }} --global
make test
env:
TURBO_API: "${{ secrets.TURBO_API }}"
TURBO_TEAM: "windows"
TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}"
TURBO_API: '${{ secrets.TURBO_API }}'
TURBO_TEAM: 'windows'
TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}'
test-on-windows-pr:
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
runs-on: windows-desktop-default-windows-security
@ -177,7 +177,7 @@ jobs:
node-version: 20
# Clean cache, continue on error
- name: "Cleanup cache"
- name: 'Cleanup cache'
shell: powershell
continue-on-error: true
run: |
@ -190,12 +190,12 @@ jobs:
make clean
- name: Get Commit Message for PR
if : github.event_name == 'pull_request'
if: github.event_name == 'pull_request'
shell: bash
run: |
echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}}" >> $GITHUB_ENV
- name: "Config report portal"
- name: 'Config report portal'
shell: bash
run: |
make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Windows" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
@ -207,9 +207,9 @@ jobs:
yarn config set registry ${{ secrets.NPM_PROXY }} --global
make test
env:
TURBO_API: "${{ secrets.TURBO_API }}"
TURBO_TEAM: "windows"
TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}"
TURBO_API: '${{ secrets.TURBO_API }}'
TURBO_TEAM: 'windows'
TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}'
test-on-windows-pr-target:
if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository
@ -226,7 +226,7 @@ jobs:
node-version: 20
# Clean cache, continue on error
- name: "Cleanup cache"
- name: 'Cleanup cache'
shell: powershell
continue-on-error: true
run: |
@ -245,7 +245,6 @@ jobs:
yarn config set registry https://registry.npmjs.org --global
make test
test-on-ubuntu:
runs-on: [self-hosted, Linux, ubuntu-desktop]
if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
@ -260,23 +259,23 @@ jobs:
with:
node-version: 20
- name: "Cleanup cache"
- name: 'Cleanup cache'
continue-on-error: true
run: |
rm -rf ~/jan
make clean
- name: Get Commit Message for PR
if : github.event_name == 'pull_request'
if: github.event_name == 'pull_request'
run: |
echo "REPORT_PORTAL_DESCRIPTION=${{github.event.after}}" >> $GITHUB_ENV
- name: Get Commit Message for push event
if : github.event_name == 'push'
if: github.event_name == 'push'
run: |
echo "REPORT_PORTAL_DESCRIPTION=${{github.sha}}" >> $GITHUB_ENV
- name: "Config report portal"
- name: 'Config report portal'
shell: bash
run: |
make update-playwright-config REPORT_PORTAL_URL=${{ secrets.REPORT_PORTAL_URL }} REPORT_PORTAL_API_KEY=${{ secrets.REPORT_PORTAL_API_KEY }} REPORT_PORTAL_PROJECT_NAME=${{ secrets.REPORT_PORTAL_PROJECT_NAME }} REPORT_PORTAL_LAUNCH_NAME="Jan App Linux" REPORT_PORTAL_DESCRIPTION="${{env.REPORT_PORTAL_DESCRIPTION}}"
@ -289,9 +288,9 @@ jobs:
yarn config set registry ${{ secrets.NPM_PROXY }} --global
make test
env:
TURBO_API: "${{ secrets.TURBO_API }}"
TURBO_TEAM: "linux"
TURBO_TOKEN: "${{ secrets.TURBO_TOKEN }}"
TURBO_API: '${{ secrets.TURBO_API }}'
TURBO_TEAM: 'linux'
TURBO_TOKEN: '${{ secrets.TURBO_TOKEN }}'
test-on-ubuntu-pr-target:
runs-on: [self-hosted, Linux, ubuntu-desktop]
@ -307,16 +306,16 @@ jobs:
with:
node-version: 20
- name: "Cleanup cache"
- name: 'Cleanup cache'
continue-on-error: true
run: |
rm -rf ~/jan
make clean
- name: Linter and test
run: |
export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
echo -e "Display ID: $DISPLAY"
npm config set registry https://registry.npmjs.org --global
yarn config set registry https://registry.npmjs.org --global
make test
make test

1
.gitignore vendored
View File

@ -16,6 +16,7 @@ electron/renderer
electron/models
electron/docs
electron/engines
electron/themes
electron/playwright-report
server/pre-install
package-lock.json

View File

@ -39,10 +39,10 @@ COPY --from=builder /app/docs/openapi ./docs/openapi/
COPY --from=builder /app/pre-install ./pre-install/
# Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache
COPY --from=builder /app/uikit ./uikit/
COPY --from=builder /app/joi ./joi/
COPY --from=builder /app/web ./web/
RUN yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build
RUN yarn workspace @janhq/joi install && yarn workspace @janhq/joi build
RUN yarn workspace @janhq/web install
RUN npm install -g serve@latest

View File

@ -63,10 +63,10 @@ COPY --from=builder /app/docs/openapi ./docs/openapi/
COPY --from=builder /app/pre-install ./pre-install/
# Copy the package.json, yarn.lock, and output of web yarn space to leverage Docker cache
COPY --from=builder /app/uikit ./uikit/
COPY --from=builder /app/joi ./joi/
COPY --from=builder /app/web ./web/
RUN yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build
RUN yarn workspace @janhq/joi install && yarn workspace @janhq/joi build
RUN yarn workspace @janhq/web install
RUN npm install -g serve@latest

View File

@ -11,15 +11,15 @@ all:
@echo "Specify a target to run"
# Builds the UI kit
build-uikit:
build-joi:
ifeq ($(OS),Windows_NT)
cd uikit && yarn config set network-timeout 300000 && yarn install && yarn build
cd joi && yarn config set network-timeout 300000 && yarn install && yarn build
else
cd uikit && yarn install && yarn build
cd joi && yarn install && yarn build
endif
# Installs yarn dependencies and builds core and extensions
install-and-build: build-uikit
install-and-build: build-joi
ifeq ($(OS),Windows_NT)
yarn config set network-timeout 300000
endif

View File

@ -11,6 +11,13 @@ export enum NativeRoute {
selectDirectory = 'selectDirectory',
selectFiles = 'selectFiles',
relaunch = 'relaunch',
setNativeThemeLight = 'setNativeThemeLight',
setNativeThemeDark = 'setNativeThemeDark',
setMinimizeApp = 'setMinimizeApp',
setCloseApp = 'setCloseApp',
setMaximizeApp = 'setMaximizeApp',
showOpenMenu = 'showOpenMenu',
hideQuickAskWindow = 'hideQuickAskWindow',
sendQuickAskInput = 'sendQuickAskInput',

View File

@ -15,11 +15,16 @@ export type ModelInfo = {
*/
export enum InferenceEngine {
anthropic = 'anthropic',
mistral = 'mistral',
martian = 'martian',
openrouter = 'openrouter',
nitro = 'nitro',
openai = 'openai',
groq = 'groq',
triton_trtllm = 'triton_trtllm',
nitro_tensorrt_llm = 'nitro-tensorrt-llm',
cohere = 'cohere',
}
export type ModelArtifact = {

View File

@ -1,4 +1,4 @@
import { app, ipcMain, dialog, shell } from 'electron'
import { app, ipcMain, dialog, shell, nativeTheme, screen } from 'electron'
import { join } from 'path'
import { windowManager } from '../managers/window'
import {
@ -10,7 +10,10 @@ import {
NativeRoute,
SelectFileProp,
} from '@janhq/core/node'
import { SelectFileOption } from '@janhq/core/.'
import { SelectFileOption } from '@janhq/core'
import { menu } from '../utils/menu'
const isMac = process.platform === 'darwin'
export function handleAppIPCs() {
/**
@ -22,6 +25,41 @@ export function handleAppIPCs() {
shell.openPath(getJanDataFolderPath())
})
/**
* Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light".
* This will change the appearance of the app to the light theme.
*/
ipcMain.handle(NativeRoute.setNativeThemeLight, () => {
nativeTheme.themeSource = 'light'
})
ipcMain.handle(NativeRoute.setCloseApp, () => {
windowManager.mainWindow?.close()
})
ipcMain.handle(NativeRoute.setMinimizeApp, () => {
windowManager.mainWindow?.minimize()
})
ipcMain.handle(NativeRoute.setMaximizeApp, async () => {
if (windowManager.mainWindow?.isMaximized()) {
// const bounds = await getBounds()
// windowManager.mainWindow?.setSize(bounds.width, bounds.height)
// windowManager.mainWindow?.setPosition(Number(bounds.x), Number(bounds.y))
windowManager.mainWindow.restore()
} else {
windowManager.mainWindow?.maximize()
}
})
/**
* Handles the "setNativeThemeDark" IPC message by setting the native theme source to "dark".
* This will change the appearance of the app to the dark theme.
*/
ipcMain.handle(NativeRoute.setNativeThemeDark, () => {
nativeTheme.themeSource = 'dark'
})
/**
* Opens a URL in the user's default browser.
* @param _event - The IPC event object.
@ -136,6 +174,16 @@ export function handleAppIPCs() {
}
)
ipcMain.handle(NativeRoute.showOpenMenu, function (e, args) {
if (!isMac && windowManager.mainWindow) {
menu.popup({
window: windowManager.mainWindow,
x: args.x,
y: args.y,
})
}
})
ipcMain.handle(
NativeRoute.hideMainWindow,
async (): Promise<void> => windowManager.hideMainWindow()

View File

@ -19,7 +19,7 @@ import { handleAppIPCs } from './handlers/native'
**/
import { setupMenu } from './utils/menu'
import { createUserSpace } from './utils/path'
import { migrateExtensions } from './utils/migration'
import { migrate } from './utils/migration'
import { cleanUpAndQuit } from './utils/clean'
import { setupExtensions } from './utils/extension'
import { setupCore } from './utils/setup'
@ -79,7 +79,7 @@ app
})
.then(setupCore)
.then(createUserSpace)
.then(migrateExtensions)
.then(migrate)
.then(setupExtensions)
.then(setupMenu)
.then(handleIPCs)

View File

@ -1,17 +1,17 @@
const DEFAULT_WIDTH = 1200
const DEFAULT_MIN_WIDTH = 400
const DEFAULT_HEIGHT = 800
export const mainWindowConfig: Electron.BrowserWindowConstructorOptions = {
width: DEFAULT_WIDTH,
minWidth: DEFAULT_MIN_WIDTH,
height: DEFAULT_HEIGHT,
skipTaskbar: false,
minWidth: DEFAULT_MIN_WIDTH,
show: true,
titleBarStyle: 'hidden',
vibrancy: 'fullscreen-ui',
visualEffectState: 'active',
backgroundMaterial: 'acrylic',
maximizable: false,
autoHideMenuBar: true,
trafficLightPosition: {
x: 10,
y: 15,
x: 16,
y: 10,
},
titleBarStyle: 'hiddenInset',
vibrancy: 'sidebar',
}

View File

@ -2,12 +2,14 @@ import { BrowserWindow, app, shell } from 'electron'
import { quickAskWindowConfig } from './quickAskWindowConfig'
import { mainWindowConfig } from './mainWindowConfig'
import { getAppConfigurations, AppEvent } from '@janhq/core/node'
import { getBounds, saveBounds } from '../utils/setup'
/**
* Manages the current window instance.
*/
// TODO: refactor this
let isAppQuitting = false
class WindowManager {
public mainWindow?: BrowserWindow
private _quickAskWindow: BrowserWindow | undefined = undefined
@ -19,9 +21,15 @@ class WindowManager {
* Creates a new window instance.
* @returns The created window instance.
*/
createMainWindow(preloadPath: string, startUrl: string) {
async createMainWindow(preloadPath: string, startUrl: string) {
const bounds = await getBounds()
this.mainWindow = new BrowserWindow({
...mainWindowConfig,
width: bounds.width,
height: bounds.height,
x: bounds.x,
y: bounds.y,
webPreferences: {
nodeIntegration: true,
preload: preloadPath,
@ -40,6 +48,14 @@ class WindowManager {
}
}
this.mainWindow.on('resized', () => {
saveBounds(this.mainWindow?.getBounds())
})
this.mainWindow.on('moved', () => {
saveBounds(this.mainWindow?.getBounds())
})
/* Load frontend app to the window */
this.mainWindow.loadURL(startUrl)

View File

@ -14,15 +14,19 @@
"renderer/**/*",
"build/**/*.{js,map}",
"pre-install",
"themes",
"docs/**/*",
"scripts/**/*",
"icons/**/*"
"icons/**/*",
"themes"
],
"asarUnpack": [
"pre-install",
"themes",
"docs",
"scripts",
"icons"
"icons",
"themes"
],
"publish": [
{
@ -114,7 +118,7 @@
"@types/request": "^2.48.12",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3",
"electron": "28.0.0",
"electron": "30.0.6",
"electron-builder": "^24.13.3",
"electron-builder-squirrel-windows": "^24.13.3",
"electron-devtools-installer": "^3.2.0",

View File

@ -112,7 +112,8 @@ const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
},
]
export const menu = Menu.buildFromTemplate(template)
export const setupMenu = () => {
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}

View File

@ -1,29 +1,42 @@
import { app } from 'electron'
import { rmdir } from 'fs'
import { join } from 'path'
import { rmdirSync, cpSync, existsSync } from 'fs'
import Store from 'electron-store'
import { getJanExtensionsPath } from '@janhq/core/node'
import {
getJanExtensionsPath,
getJanDataFolderPath,
appResourcePath,
} from '@janhq/core/node'
/**
* Migrates the extensions by deleting the `extensions` directory in the user data path.
* Migrates the extensions & themes.
* If the `migrated_version` key in the `Store` object does not match the current app version,
* the function deletes the `extensions` directory and sets the `migrated_version` key to the current app version.
* @returns A Promise that resolves when the migration is complete.
*/
export function migrateExtensions() {
return new Promise((resolve) => {
const store = new Store()
if (store.get('migrated_version') !== app.getVersion()) {
console.debug('start migration:', store.get('migrated_version'))
export async function migrate() {
const store = new Store()
if (store.get('migrated_version') !== app.getVersion()) {
console.debug('start migration:', store.get('migrated_version'))
rmdir(getJanExtensionsPath(), { recursive: true }, function (err) {
if (err) console.error(err)
store.set('migrated_version', app.getVersion())
console.debug('migrate extensions done')
resolve(undefined)
})
} else {
resolve(undefined)
}
})
if (existsSync(getJanExtensionsPath()))
rmdirSync(getJanExtensionsPath(), { recursive: true })
await migrateThemes()
store.set('migrated_version', app.getVersion())
console.debug('migrate extensions done')
} else if (!existsSync(join(getJanDataFolderPath(), 'themes'))) {
await migrateThemes()
}
}
async function migrateThemes() {
if (existsSync(join(getJanDataFolderPath(), 'themes')))
rmdirSync(join(getJanDataFolderPath(), 'themes'), { recursive: true })
cpSync(
join(await appResourcePath(), 'themes'),
join(getJanDataFolderPath(), 'themes'),
{ recursive: true }
)
}

View File

@ -1,4 +1,10 @@
import { app } from 'electron'
import Store from 'electron-store'
const DEFAULT_WIDTH = 1000
const DEFAULT_HEIGHT = 700
const storage = new Store()
export const setupCore = async () => {
// Setup core api for main process
@ -7,3 +13,24 @@ export const setupCore = async () => {
appPath: () => app.getPath('userData'),
}
}
export const getBounds = async () => {
const defaultBounds = {
x: undefined,
y: undefined,
width: DEFAULT_WIDTH,
height: DEFAULT_HEIGHT,
}
const bounds = await storage.get('windowBounds')
if (bounds) {
return bounds as Electron.Rectangle
} else {
storage.set('windowBounds', defaultBounds)
return defaultBounds
}
}
export const saveBounds = (bounds: Electron.Rectangle | undefined) => {
storage.set('windowBounds', bounds)
}

View File

@ -2,4 +2,5 @@
node_modules/
dist/
*.hbs
*.mdx
*.mdx
*.mjs

13
joi/README.md Normal file
View File

@ -0,0 +1,13 @@
# @janhq/joi
To install dependencies:
```bash
yarn install
```
To run:
```bash
yarn run dev
```

59
joi/package.json Normal file
View File

@ -0,0 +1,59 @@
{
"name": "@janhq/joi",
"version": "0.0.0",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/index.d.ts",
"description": "A collection of UI component",
"private": true,
"files": [
"dist"
],
"keywords": [
"design-system"
],
"license": "MIT",
"homepage": "https://github.com/codecentrum/piksel#readme",
"repository": {
"type": "git",
"url": "https://github.com/codecentrum/piksel.git"
},
"bugs": "https://github.com/codecentrum/piksel/issues",
"scripts": {
"dev": "rollup -c -w",
"build": "rimraf ./dist && rollup -c"
},
"peerDependencies": {
"class-variance-authority": "^0.7.0",
"react": "^18",
"typescript": "^5.0.0"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"tailwind-merge": "^2.2.0",
"autoprefixer": "10.4.16",
"tailwindcss": "^3.4.1"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.6",
"rollup": "^4.12.0",
"rollup-plugin-bundle-size": "^1.0.3",
"rollup-plugin-commonjs": "^10.1.0",
"rollup-plugin-copy": "^3.5.0",
"rollup-plugin-dts": "^6.1.0",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-typescript2": "^0.36.0"
}
}

73
joi/rollup.config.mjs Normal file
View File

@ -0,0 +1,73 @@
import { readFileSync } from 'fs'
import dts from 'rollup-plugin-dts'
import terser from '@rollup/plugin-terser'
import autoprefixer from 'autoprefixer'
import commonjs from 'rollup-plugin-commonjs'
import bundleSize from 'rollup-plugin-bundle-size'
import peerDepsExternal from 'rollup-plugin-peer-deps-external'
import postcss from 'rollup-plugin-postcss'
import typescript from 'rollup-plugin-typescript2'
import tailwindcss from 'tailwindcss'
import typescriptEngine from 'typescript'
import resolve from '@rollup/plugin-node-resolve'
import copy from 'rollup-plugin-copy'
const packageJson = JSON.parse(readFileSync('./package.json'))
import tailwindConfig from './tailwind.config.js'
export default [
{
input: `./src/index.ts`,
output: [
{
file: packageJson.main,
format: 'cjs',
sourcemap: false,
exports: 'named',
name: packageJson.name,
},
{
file: packageJson.module,
format: 'es',
exports: 'named',
sourcemap: false,
},
],
plugins: [
postcss({
plugins: [autoprefixer(), tailwindcss(tailwindConfig)],
sourceMap: true,
use: ['sass'],
minimize: true,
extract: 'main.css',
}),
peerDepsExternal({ includeDependencies: true }),
resolve(),
commonjs(),
typescript({
tsconfig: './tsconfig.json',
typescript: typescriptEngine,
sourceMap: false,
exclude: ['docs', 'dist', 'node_modules/**'],
}),
terser(),
],
watch: {
clearScreen: false,
},
},
{
input: 'dist/esm/index.d.ts',
output: [{ file: 'dist/index.d.ts', format: 'esm' }],
external: [/\.(sc|sa|c)ss$/],
plugins: [
dts(),
peerDepsExternal({ includeDependencies: true }),
copy({
targets: [{ src: 'dist/esm/main.css', dest: 'dist' }],
}),
bundleSize(),
],
},
]

View File

@ -0,0 +1,45 @@
import React, { ReactNode } from 'react'
import * as AccordionPrimitive from '@radix-ui/react-accordion'
import { ChevronDownIcon } from '@radix-ui/react-icons'
import './styles.scss'
type AccordionProps = {
defaultValue: string[]
children: ReactNode
}
type AccordionItemProps = {
children: ReactNode
value: string
title: string
}
const AccordionItem = ({ children, value, title }: AccordionItemProps) => {
return (
<AccordionPrimitive.Item className="accordion__item" value={value}>
<AccordionPrimitive.Header className="accordion__header">
<AccordionPrimitive.Trigger className="accordion__trigger">
<h6>{title}</h6>
<ChevronDownIcon className="accordion__chevron" aria-hidden />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
<AccordionPrimitive.Content className="accordion__content">
<div className="accordion__content--wrapper">{children}</div>
</AccordionPrimitive.Content>
</AccordionPrimitive.Item>
)
}
const Accordion = ({ defaultValue, children }: AccordionProps) => (
<AccordionPrimitive.Root
className="accordion"
type="multiple"
defaultValue={defaultValue}
>
{children}
</AccordionPrimitive.Root>
)
export { Accordion, AccordionItem }

View File

@ -0,0 +1,73 @@
.accordion {
border-top: 1px solid hsla(var(--app-border));
&__item {
overflow: hidden;
margin-top: 1px;
border-bottom: 1px solid hsla(var(--app-border));
:focus-within {
position: relative;
z-index: 1;
}
}
&__header {
display: flex;
}
&__trigger {
font-family: inherit;
background-color: transparent;
padding: 0 16px;
height: 40px;
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-weight: 500;
}
&__content {
overflow: hidden;
&--wrapper {
padding: 4px 16px 16px 16px;
}
}
&__chevron {
color: hsla(var(--text-secondary));
transition: transform 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
}
.accordion__content[data-state='open'] {
animation: slideDown 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
.accordion__content[data-state='closed'] {
animation: slideUp 300ms cubic-bezier(0.87, 0, 0.13, 1);
}
.accordion__trigger[data-state='open'] > .accordion__chevron {
transform: rotate(180deg);
}
@keyframes slideDown {
from {
height: 0;
}
to {
height: var(--radix-accordion-content-height);
}
}
@keyframes slideUp {
from {
height: var(--radix-accordion-content-height);
}
to {
height: 0;
}
}

View File

@ -0,0 +1,50 @@
import React, { HTMLAttributes } from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { twMerge } from 'tailwind-merge'
import './styles.scss'
const badgeVariants = cva('badge', {
variants: {
theme: {
primary: 'badge--primary',
secondary: 'badge--secondary',
warning: 'badge--warning',
success: 'badge--success',
info: 'badge--info',
destructive: 'badge--destructive',
},
variant: {
solid: 'badge--solid',
soft: 'badge--soft',
outline: 'badge--outline',
},
size: {
small: 'badge--small',
medium: 'badge--medium',
large: 'badge--large',
},
},
defaultVariants: {
theme: 'primary',
size: 'medium',
variant: 'solid',
},
})
export interface BadgeProps
extends HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
const Badge = ({ className, theme, size, variant, ...props }: BadgeProps) => {
return (
<div
className={twMerge(badgeVariants({ theme, size, variant, className }))}
{...props}
/>
)
}
export { Badge }

View File

@ -0,0 +1,131 @@
.badge {
@apply inline-flex items-center justify-center px-2 font-medium transition-all;
// Primary
&--primary {
color: hsla(var(--primary-fg));
background-color: hsla(var(--primary-bg));
// Variant soft primary
&.badge--soft {
background-color: hsla(var(--primary-bg-soft));
color: hsla(var(--primary-bg));
}
// Variant outline primary
&.badge--outline {
background-color: transparent;
border: 1px solid hsla(var(--primary-bg));
color: hsla(var(--primary-bg));
}
}
// Secondary
&--secondary {
background-color: hsla(var(--secondary-bg));
color: hsla(var(--secondary-fg));
&.badge--soft {
background-color: hsla(var(--secondary-bg-soft));
color: hsla(var(--secondary-bg));
}
// Variant outline secondary
&.badge--outline {
background-color: transparent;
border: 1px solid hsla(var(--secondary-bg));
}
}
// Destructive
&--destructive {
color: hsla(var(--destructive-fg));
background-color: hsla(var(--destructive-bg));
// Variant soft destructive
&.badge--soft {
background-color: hsla(var(--destructive-bg-soft));
color: hsla(var(--destructive-bg));
}
// Variant outline destructive
&.badge--outline {
background-color: transparent;
border: 1px solid hsla(var(--destructive-bg));
color: hsla(var(--destructive-bg));
}
}
// Success
&--success {
@apply text-white;
background-color: hsla(var(--success-bg));
// Variant soft success
&.badge--soft {
background-color: hsla(var(--success-bg-soft));
color: hsla(var(--success-bg));
}
// Variant outline success
&.badge--outline {
background-color: transparent;
border: 1px solid hsla(var(--success-bg));
color: hsla(var(--success-bg));
}
}
// Warning
&--warning {
@apply text-white;
background-color: hsla(var(--warning-bg));
// Variant soft warning
&.badge--soft {
background-color: hsla(var(--warning-bg-soft));
color: hsla(var(--warning-bg));
}
// Variant outline warning
&.badge--outline {
background-color: transparent;
border: 1px solid hsla(var(--warning-bg));
color: hsla(var(--warning-bg));
}
}
// Info
&--info {
@apply text-white;
background-color: hsla(var(--info-bg));
// Variant soft info
&.badge--soft {
background-color: hsla(var(--info-bg-soft));
color: hsla(var(--info-bg));
}
// Variant outline info
&.badge--outline {
background-color: transparent;
border: 1px solid hsla(var(--info-bg));
color: hsla(var(--info-bg));
}
}
// Size
&--small {
@apply h-5;
border-radius: 4px;
}
&--medium {
@apply h-6;
border-radius: 6px;
}
&--large {
@apply h-7;
border-radius: 8px;
}
}

View File

@ -0,0 +1,64 @@
import React, { forwardRef, ButtonHTMLAttributes } from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { twMerge } from 'tailwind-merge'
import './styles.scss'
const buttonVariants = cva('btn', {
variants: {
theme: {
primary: 'btn--primary',
ghost: 'btn--ghost',
icon: 'btn--icon',
destructive: 'btn--destructive',
},
variant: {
solid: 'btn--solid',
soft: 'btn--soft',
outline: 'btn--outline',
},
size: {
small: 'btn--small',
medium: 'btn--medium',
large: 'btn--large',
},
block: {
true: 'btn--block',
},
},
defaultVariants: {
theme: 'primary',
size: 'medium',
variant: 'solid',
block: false,
},
})
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{ className, theme, size, variant, block, asChild = false, ...props },
ref
) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={twMerge(
buttonVariants({ theme, size, variant, block, className })
)}
ref={ref}
{...props}
/>
)
}
)
export { Button }

View File

@ -0,0 +1,134 @@
.btn {
@apply inline-flex items-center justify-center px-4 font-semibold transition-all;
&:focus,
&:focus-within {
@apply outline-2 outline-offset-4;
}
&:hover {
filter: brightness(95%);
}
// Primary
&--primary {
color: hsla(var(--primary-fg));
background-color: hsla(var(--primary-bg)) !important;
&:hover {
filter: brightness(65%);
}
// Variant soft primary
&.btn--soft {
background-color: hsla(var(--primary-bg-soft)) !important;
color: hsla(var(--primary-bg));
}
// Variant outline primary
&.btn--outline {
background-color: transparent !important;
border: 1px solid hsla(var(--primary-bg));
color: hsla(var(--primary-bg));
}
}
// Ghost
&--ghost {
background-color: transparent !important;
&.btn--soft {
background-color: transparent !important;
}
// Variant outline ghost
&.btn--outline {
background-color: transparent !important;
border: 1px solid hsla(var(--ghost-border));
}
}
// Destructive
&--destructive {
color: hsla(var(--destructive-fg));
background-color: hsla(var(--destructive-bg)) !important;
&:hover {
filter: brightness(65%);
}
// Variant soft destructive
&.btn--soft {
background-color: hsla(var(--destructive-bg-soft)) !important;
color: hsla(var(--destructive-bg));
}
// Variant outline destructive
&.btn--outline {
background-color: transparent !important;
border: 1px solid hsla(var(--destructive-bg));
color: hsla(var(--destructive-bg));
}
}
// Disabled
&:disabled {
color: hsla(var(--disabled-fg));
background-color: hsla(var(--disabled-bg)) !important;
cursor: not-allowed;
&:hover {
filter: brightness(100%);
}
}
// Icon
&--icon {
width: 24px;
height: 24px;
padding: 2px;
&:hover {
background-color: hsla(var(--icon-bg)) !important;
}
&.btn--outline {
background-color: transparent !important;
border: 1px solid hsla(var(--icon-border));
&:hover {
background-color: hsla(var(--icon-bg)) !important;
}
}
}
// Size
&--small {
@apply h-6 px-2;
font-size: 12px;
border-radius: 4px;
&.btn--icon {
width: 24px;
height: 24px;
padding: 2px;
}
}
&--medium {
@apply h-8;
border-radius: 6px;
&.btn--icon {
width: 24px;
height: 24px;
padding: 2px;
}
}
&--large {
@apply h-9;
border-radius: 8px;
&.btn--icon {
width: 24px;
height: 24px;
padding: 2px;
}
}
&--block {
@apply w-full;
}
}

View File

@ -0,0 +1,51 @@
import React, { ChangeEvent, InputHTMLAttributes, ReactNode } from 'react'
import { twMerge } from 'tailwind-merge'
import './styles.scss'
export interface CheckboxProps extends InputHTMLAttributes<HTMLInputElement> {
disabled?: boolean
className?: string
label?: ReactNode
helperDescription?: ReactNode
errorMessage?: string
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
}
const Checkbox = ({
id,
name,
checked,
disabled,
label,
defaultChecked,
helperDescription,
errorMessage,
className,
onChange,
...props
}: CheckboxProps) => {
return (
<div className={twMerge('checkbox', className)}>
<input
id={id}
type="checkbox"
name={name}
defaultChecked={defaultChecked}
checked={checked}
disabled={disabled}
onChange={onChange}
{...props}
/>
<div>
<label htmlFor={id} className="checkbox__label">
{label}
</label>
<p className="checkbox__helper">{helperDescription}</p>
{errorMessage && <p className="checkbox__error">{errorMessage}</p>}
</div>
</div>
)
}
export { Checkbox }

View File

@ -0,0 +1,51 @@
.checkbox {
@apply inline-flex items-start space-x-2;
> input[type='checkbox'] {
@apply flex h-4 w-4 flex-shrink-0 cursor-pointer appearance-none items-center justify-center;
background-color: transparent;
margin-top: 1px;
border: 1px solid hsla(var(--app-border));
border-radius: 4px;
&:focus,
&:focus-within {
@apply outline-2 outline-offset-4;
}
&:checked {
background-color: hsla(var(--primary-bg));
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E");
}
&:disabled {
background-color: hsla(var(----disabled-bg));
color: hsla(var(--disabled-fg));
&:checked {
background-color: hsla(var(--primary-bg));
@apply cursor-not-allowed opacity-50;
}
& + div > .checkbox__label {
@apply cursor-not-allowed opacity-50;
}
}
}
&__helper {
font-size: 12px;
}
&__error {
color: hsla(var(--destructive-bg));
}
&__label {
@apply inline-block cursor-pointer;
}
&:disabled {
background-color: hsla(var(----disabled-bg));
color: hsla(var(--disabled-fg));
}
}

View File

@ -0,0 +1,46 @@
import React, { ReactNode, forwardRef } from 'react'
import { twMerge } from 'tailwind-merge'
import './styles.scss'
export interface Props extends React.InputHTMLAttributes<HTMLInputElement> {
textAlign?: 'left' | 'right'
prefixIcon?: ReactNode
suffixIcon?: ReactNode
onCLick?: () => void
}
const Input = forwardRef<HTMLInputElement, Props>(
(
{ className, type, textAlign, prefixIcon, suffixIcon, onClick, ...props },
ref
) => {
return (
<div className="input__wrapper">
{prefixIcon && (
<div className="input__prefix-icon" onClick={onClick}>
{prefixIcon}
</div>
)}
{suffixIcon && (
<div className="input__suffix-icon" onClick={onClick}>
{suffixIcon}
</div>
)}
<input
type={type}
className={twMerge(
'input',
className,
textAlign === 'right' && 'text-right'
)}
ref={ref}
onClick={onClick}
{...props}
/>
</div>
)
}
)
export { Input }

View File

@ -0,0 +1,43 @@
.input {
background-color: hsla(var(--input-bg));
border: 1px solid hsla(var(--app-border));
@apply inline-flex h-8 w-full items-center rounded-md border px-3 transition-colors;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-1 focus-visible:ring-[hsla(var(--primary-bg))] focus-visible:ring-offset-0;
@apply file:border-0 file:bg-transparent file:font-medium;
@apply hover:border-[hsla(var(--primary-bg))];
&__wrapper {
position: relative;
}
&.text-right {
text-align: right;
}
&::placeholder {
color: hsla(var(--input-placeholder));
}
&:disabled {
color: hsla(var(--disabled-fg));
background-color: hsla(var(--disabled-bg));
cursor: not-allowed;
border: none;
}
&__prefix-icon {
@apply absolute left-3 top-1/2 -translate-y-1/2 cursor-pointer;
color: hsla(var(--input-icon));
+ .input {
padding-left: 32px;
}
}
&__suffix-icon {
@apply absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer;
color: hsla(var(--input-icon));
+ .input {
padding-right: 32px;
}
}
}

View File

@ -0,0 +1,56 @@
import React, { ReactNode } from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { Cross2Icon } from '@radix-ui/react-icons'
import './styles.scss'
import { twMerge } from 'tailwind-merge'
type Props = {
trigger?: ReactNode
content: ReactNode
open?: boolean
className?: string
fullPage?: boolean
hideClose?: boolean
title?: ReactNode
onOpenChange?: (open: boolean) => void
}
const ModalClose = DialogPrimitive.Close
const Modal = ({
trigger,
content,
open,
title,
fullPage,
className,
onOpenChange,
hideClose,
}: Props) => (
<DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
<DialogPrimitive.Trigger asChild>{trigger}</DialogPrimitive.Trigger>
<DialogPrimitive.Portal>
<DialogPrimitive.Overlay className="modal__overlay" />
<DialogPrimitive.Content
className={twMerge(
'modal__content',
fullPage && 'modal__content--fullpage',
className
)}
>
<div className="modal__title">{title}</div>
{content}
{!hideClose && (
<ModalClose asChild>
<button className="modal__close-icon" aria-label="Close">
<Cross2Icon />
</button>
</ModalClose>
)}
</DialogPrimitive.Content>
</DialogPrimitive.Portal>
</DialogPrimitive.Root>
)
export { Modal, ModalClose }

View File

@ -0,0 +1,85 @@
/* reset */
button,
fieldset,
.modal {
&__overlay {
@apply backdrop-blur-lg;
background-color: hsla(var(--modal-overlay));
z-index: 200;
position: fixed;
inset: 0;
animation: overlayShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
}
&__content {
color: hsla(var(--modal-fg));
overflow: hidden;
background-color: hsla(var(--modal-bg));
border-radius: 8px;
position: fixed;
z-index: 300;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50vw;
max-width: 560px;
max-height: 85vh;
padding: 16px;
animation: contentShow 150ms cubic-bezier(0.16, 1, 0.3, 1);
border: 1px solid hsla(var(--app-border));
@apply w-full;
&--fullpage {
max-width: none;
width: 90vw;
max-height: 90vh;
}
&:focus {
outline: none;
}
}
&__title {
@apply line-clamp-1;
margin: 0 0 8px 0;
padding-right: 16px;
font-weight: 600;
color: hsla(var(--modal-fg));
font-size: 18px;
}
&__close-icon {
font-family: inherit;
border-radius: 100%;
height: 24px;
width: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
color: hsla(var(--modal-fg));
position: absolute;
top: 8px;
right: 16px;
}
}
@keyframes overlayShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes contentShow {
from {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}

View File

@ -0,0 +1,39 @@
import React, { HTMLAttributes } from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { twMerge } from 'tailwind-merge'
import './styles.scss'
const progressVariants = cva('progress', {
variants: {
size: {
small: 'progress--small',
medium: 'progress--medium',
large: 'progress--large',
},
},
defaultVariants: {
size: 'medium',
},
})
export interface ProgressProps
extends HTMLAttributes<HTMLDivElement>,
VariantProps<typeof progressVariants> {
value: number
}
const Progress = ({ className, size, value, ...props }: ProgressProps) => {
return (
<div className={twMerge(progressVariants({ size, className }))} {...props}>
<div
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
className="progress--indicator"
/>
</div>
)
}
export { Progress }

View File

@ -0,0 +1,25 @@
.progress {
background-color: hsla(var(--progress-track-bg));
border-radius: 8px;
position: relative;
overflow: hidden;
@apply transition-all;
&--indicator {
background-color: hsla(var(--primary-bg));
position: absolute;
border-radius: 8px;
width: 100%;
height: 100%;
}
&--small {
height: 6px;
}
&--medium {
@apply h-2;
}
&--large {
@apply h-3;
}
}

View File

@ -0,0 +1,35 @@
import React, { PropsWithChildren, forwardRef } from 'react'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import { twMerge } from 'tailwind-merge'
import './styles.scss'
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
type="scroll"
className={twMerge('scroll-area__root', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="scroll-area__viewport" ref={ref}>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollAreaPrimitive.Scrollbar
className="scroll-area__bar"
orientation="horizontal"
>
<ScrollAreaPrimitive.Thumb />
</ScrollAreaPrimitive.Scrollbar>
<ScrollAreaPrimitive.Scrollbar
className="scroll-area__bar"
orientation="vertical"
>
<ScrollAreaPrimitive.Thumb className="scroll-area__thumb" />
</ScrollAreaPrimitive.Scrollbar>
<ScrollAreaPrimitive.Corner className="scroll-area__corner" />
</ScrollAreaPrimitive.Root>
))
export { ScrollArea }

View File

@ -0,0 +1,69 @@
.scroll-area {
position: relative;
z-index: 999;
&__root {
width: 200px;
height: 225px;
overflow: hidden;
}
&__viewport {
width: 100%;
height: 100%;
border-radius: inherit;
}
&__bar {
display: flex;
user-select: none;
touch-action: none;
padding: 1px;
background: hsla(var(--scrollbar-tracker));
transition: background 160ms ease-out;
}
&__thumb {
flex: 1;
background: hsla(var(--scrollbar-thumb));
border-radius: 20px;
position: relative;
::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100%;
height: 100%;
min-width: 44px;
min-height: 44px;
}
}
}
.scroll-area__bar[data-orientation='vertical'] {
width: 8px;
}
.scroll-area__bar[data-orientation='horizontal'] {
flex-direction: column;
height: 8px;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track,
::-webkit-scrollbar-thumb {
background-clip: content-box;
border-radius: inherit;
}
::-webkit-scrollbar-track {
background: hsla(var(--scrollbar-tracker));
}
::-webkit-scrollbar-thumb {
background: hsla(var(--scrollbar-thumb));
}

View File

@ -0,0 +1,85 @@
import React, { ReactNode } from 'react'
import * as SelectPrimitive from '@radix-ui/react-select'
import {
CheckIcon,
ChevronDownIcon,
ChevronUpIcon,
} from '@radix-ui/react-icons'
import './styles.scss'
import { twMerge } from 'tailwind-merge'
type Props = {
options?: { name: string; value: string }[]
open?: boolean
block?: boolean
value?: string
placeholder?: string
disabled?: boolean
containerPortal?: HTMLDivElement | undefined | null
className?: string
onValueChange?: (value: string) => void
onOpenChange?: (open: boolean) => void
}
const Select = ({
placeholder,
options,
value,
disabled,
containerPortal,
block,
className,
open,
onValueChange,
onOpenChange,
}: Props) => (
<SelectPrimitive.Root
open={open}
onValueChange={onValueChange}
value={value}
onOpenChange={onOpenChange}
>
<SelectPrimitive.Trigger
className={twMerge(
'select',
className,
disabled && 'select__disabled',
block && 'w-full'
)}
>
<SelectPrimitive.Value placeholder={placeholder} />
<SelectPrimitive.Icon className="select__icon">
<ChevronDownIcon />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
<SelectPrimitive.Portal container={containerPortal}>
<SelectPrimitive.Content className="select__content">
<SelectPrimitive.Viewport className="select__viewport">
{options &&
options.map((item, i) => {
return (
<SelectPrimitive.Item
key={i}
className="select__item"
value={item.value}
>
<SelectPrimitive.ItemText>
{item.name}
</SelectPrimitive.ItemText>
<SelectPrimitive.ItemIndicator className="select__item-indicator">
<CheckIcon />
</SelectPrimitive.ItemIndicator>
</SelectPrimitive.Item>
)
})}
</SelectPrimitive.Viewport>
<SelectPrimitive.Arrow />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
</SelectPrimitive.Root>
)
export { Select }

View File

@ -0,0 +1,77 @@
.select {
padding: 0 16px;
background-color: hsla(var(--select-input-bg)) !important;
border: 1px solid hsla(var(--app-border));
@apply inline-flex h-8 items-center justify-between gap-8 rounded-md px-3 transition-colors;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-1 focus-visible:ring-[hsla(var(--primary-bg))] focus-visible:ring-offset-0;
@apply text-sm hover:border-[hsla(var(--primary-bg))];
&[data-placeholder] {
color: hsla(var(--select-placeholder));
}
&__icon {
color: hsla(var(--select-icon));
}
&__content {
overflow: hidden;
background-color: hsla(var(--select-bg));
z-index: 999;
border: 1px solid hsla(var(--select-border));
border-radius: 8px;
box-shadow:
0px 10px 38px -10px rgba(22, 23, 24, 0.35),
0px 10px 20px -15px rgba(22, 23, 24, 0.2);
}
&__viewport {
}
&__disabled {
cursor: not-allowed;
pointer-events: none;
background-color: hsla(var(--disabled-bg)) !important;
color: hsla(var(--disabled-fg));
border: none;
}
&__item {
display: flex;
align-items: center;
padding: 8px 32px 8px 16px;
position: relative;
cursor: pointer;
@apply text-sm;
&:hover {
background-color: hsla(var(--select-options-active-bg));
}
&[data-disabled] {
pointer-events: none;
}
&[data-highlighted] {
outline: none;
}
}
&__item-indicator {
position: absolute;
right: 0;
width: 25px;
display: inline-flex;
align-items: center;
justify-content: center;
}
&__scroll-button {
display: flex;
align-items: center;
justify-content: center;
height: 25px;
background-color: white;
cursor: default;
}
}

View File

@ -0,0 +1,45 @@
import React from 'react'
import * as SliderPrimitive from '@radix-ui/react-slider'
import './styles.scss'
type Props = {
name?: string
min?: number
max?: number
onValueChange?(value: number[]): void
value?: number[]
defaultValue?: number[]
step?: number
disabled?: boolean
}
const Slider = ({
name,
min,
max,
onValueChange,
value,
defaultValue,
step,
disabled,
}: Props) => (
<SliderPrimitive.Root
className="slider"
name={name}
min={min}
max={max}
onValueChange={onValueChange}
value={value}
defaultValue={defaultValue}
step={step}
disabled={disabled}
>
<SliderPrimitive.Track className="slider__track">
<SliderPrimitive.Range className="slider__range" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="slider__thumb" />
</SliderPrimitive.Root>
)
export { Slider }

View File

@ -0,0 +1,38 @@
.slider {
position: relative;
display: flex;
align-items: center;
user-select: none;
touch-action: none;
height: 16px;
&__track {
background-color: hsla(var(--slider-track-bg));
position: relative;
flex-grow: 1;
border-radius: 9999px;
height: 4px;
}
&__range {
position: absolute;
background-color: hsla(var(--primary-bg));
border-radius: 9999px;
height: 100%;
}
&__thumb {
display: block;
width: 16px;
height: 16px;
background-color: hsla(var(--slider-thumb-bg));
border-radius: 10px;
padding: 2px;
border: 2px solid hsla(var(--primary-bg));
&:focus {
outline: none;
box-shadow: 0 0 0 5px hsla(var(--slider-track-bg), 50%);
}
}
}

View File

@ -0,0 +1,37 @@
import React, { ChangeEvent, InputHTMLAttributes } from 'react'
import { twMerge } from 'tailwind-merge'
import './styles.scss'
export interface SwitchProps extends InputHTMLAttributes<HTMLInputElement> {
disabled?: boolean
className?: string
onChange?: (e: ChangeEvent<HTMLInputElement>) => void
}
const Switch = ({
name,
checked,
disabled,
defaultChecked,
className,
onChange,
...props
}: SwitchProps) => {
return (
<label className={twMerge('switch', className)}>
<input
type="checkbox"
name={name}
defaultChecked={defaultChecked}
checked={checked}
disabled={disabled}
onChange={onChange}
{...props}
/>
<span className="switch--thumb" />
</label>
)
}
export { Switch }

View File

@ -0,0 +1,67 @@
.switch {
position: relative;
display: inline-block;
width: 32px;
height: 18px;
> input {
opacity: 0;
width: 0;
height: 0;
// disabled
&:disabled {
+ .switch--thumb {
cursor: not-allowed;
background-color: hsla(var(--disabled-bg));
&:before {
background-color: hsla(var(--disabled-fg));
}
}
// disabled and checked
&:checked + .switch--thumb {
cursor: not-allowed;
background-color: hsla(var(--primary-bg));
&:before {
background-color: hsla(var(--disabled-fg));
}
}
}
&:checked + .switch--thumb {
background-color: hsla(var(--primary-bg));
&::before {
-webkit-transform: translateX(14px);
-ms-transform: translateX(14px);
transform: translateX(14px);
}
}
}
&--thumb {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: hsla(var(--switch-bg));
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: 20px;
&:before {
position: absolute;
content: '';
height: 14px;
width: 14px;
left: 2px;
bottom: 2px;
background-color: hsla(var(--switch-fg));
-webkit-transition: 0.4s;
transition: 0.4s;
border-radius: 50%;
}
}
}

View File

@ -0,0 +1,59 @@
import React, { ReactNode } from 'react'
import * as TabsPrimitive from '@radix-ui/react-tabs'
import './styles.scss'
type TabsProps = {
options: { name: string; value: string }[]
children: ReactNode
defaultValue?: string
value: string
onValueChange?: (value: string) => void
}
type TabsContentProps = {
value: string
children: ReactNode
}
const TabsContent = ({ value, children }: TabsContentProps) => {
return (
<TabsPrimitive.Content className="tabs__content" value={value}>
{children}
</TabsPrimitive.Content>
)
}
const Tabs = ({
options,
children,
defaultValue,
value,
onValueChange,
}: TabsProps) => (
<TabsPrimitive.Root
className="tabs"
value={value}
defaultValue={defaultValue}
onValueChange={onValueChange}
>
<TabsPrimitive.List className="tabs__list">
{options.map((option, i) => {
return (
<TabsPrimitive.Trigger
key={i}
className="tabs__trigger"
value={option.value}
>
{option.name}
</TabsPrimitive.Trigger>
)
})}
</TabsPrimitive.List>
{children}
</TabsPrimitive.Root>
)
export { Tabs, TabsContent }

View File

@ -0,0 +1,37 @@
.tabs {
display: flex;
flex-direction: column;
width: 100%;
&__list {
flex-shrink: 0;
display: flex;
border-bottom: 1px solid hsla(var(--app-border));
}
&__trigger {
padding: 0 12px;
flex: 1;
height: 38px;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
user-select: none;
&:focus {
position: relative;
}
}
&__content {
flex-grow: 1;
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
outline: none;
}
}
.tabs__trigger[data-state='active'] {
border-bottom: 1px solid hsla(var(--primary-bg));
font-weight: 600;
}

View File

@ -0,0 +1,24 @@
import React, { ReactNode, forwardRef } from 'react'
import { twMerge } from 'tailwind-merge'
import './styles.scss'
import { ScrollArea } from '../ScrollArea'
export interface TextAreaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(
({ className, ...props }, ref) => {
return (
<div className="textarea__wrapper">
<textarea
className={twMerge('textarea', className)}
ref={ref}
{...props}
/>
</div>
)
}
)
export { TextArea }

View File

@ -0,0 +1,54 @@
.textarea {
background-color: hsla(var(--textarea-bg));
border: 1px solid hsla(var(--app-border));
@apply inline-flex w-full items-center rounded-md border p-3 transition-colors;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-1 focus-visible:ring-[hsla(var(--primary-bg))] focus-visible:ring-offset-0;
@apply file:border-0 file:bg-transparent file:font-medium;
@apply hover:border-[hsla(var(--primary-bg))];
&__wrapper {
position: relative;
}
&.text-right {
text-align: right;
}
&::placeholder {
color: hsla(var(--textarea-placeholder));
}
&:disabled {
color: hsla(var(--disabled-fg));
background-color: hsla(var(--disabled-bg));
cursor: not-allowed;
border: none;
}
&__prefix-icon {
@apply absolute left-2 top-1/2 -translate-y-1/2;
color: hsla(var(--textarea-icon));
+ .textarea {
padding-left: 32px;
}
}
& {
/* Arrow mouse cursor over the scrollbar */
cursor: auto;
}
&::-webkit-scrollbar {
width: 6px;
height: 6px;
}
&::-webkit-scrollbar-track,
&::-webkit-scrollbar-thumb {
background-clip: content-box;
border-radius: inherit;
}
&::-webkit-scrollbar-track {
background: hsla(var(--scrollbar-tracker));
}
&::-webkit-scrollbar-thumb {
background: hsla(var(--scrollbar-thumb));
}
}

View File

@ -0,0 +1,53 @@
import React, { ReactNode } from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import './styles.scss'
export interface TooltipProps {
trigger?: ReactNode
content: ReactNode
side?: 'top' | 'right' | 'bottom' | 'left'
open?: boolean
disabled?: boolean
withArrow?: boolean
onOpenChange?: (open: boolean) => void
}
export const Tooltip = ({
trigger,
disabled,
content,
side = 'top',
withArrow = true,
open,
onOpenChange,
}: TooltipProps) => {
return (
<TooltipPrimitive.Provider>
<TooltipPrimitive.Root
delayDuration={200}
open={open}
onOpenChange={onOpenChange}
>
<TooltipPrimitive.Trigger asChild className="tooltip__trigger">
{trigger}
</TooltipPrimitive.Trigger>
<TooltipPrimitive.Portal>
{!disabled && content && (
<TooltipPrimitive.Content
className="tooltip__content"
collisionPadding={16}
sideOffset={6}
side={side}
>
{content}
{withArrow && (
<TooltipPrimitive.Arrow className="tooltip__arrow" />
)}
</TooltipPrimitive.Content>
)}
</TooltipPrimitive.Portal>
</TooltipPrimitive.Root>
</TooltipPrimitive.Provider>
)
}

View File

@ -0,0 +1,82 @@
.tooltip {
&__content {
border-radius: 8px;
padding: 8px 14px;
line-height: 1;
color: hsla(var(--tooltip-fg));
background-color: hsla(var(--tooltip-bg));
user-select: none;
animation-duration: 400ms;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
will-change: transform, opacity;
font-weight: 500;
z-index: 100;
max-width: 240px;
@apply text-sm leading-normal;
}
&__arrow {
fill: hsla(var(--tooltip-bg));
}
&__trigger {
@apply cursor-pointer;
}
}
.tooltip__content[data-state='delayed-open'][data-side='top'] {
animation-name: slideDownAndFade;
}
.tooltip__content[data-state='delayed-open'][data-side='right'] {
animation-name: slideLeftAndFade;
}
.tooltip__content[data-state='delayed-open'][data-side='bottom'] {
animation-name: slideUpAndFade;
}
.tooltip__content[data-state='delayed-open'][data-side='left'] {
animation-name: slideRightAndFade;
}
@keyframes slideUpAndFade {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideRightAndFade {
from {
opacity: 0;
transform: translateX(-4px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@keyframes slideDownAndFade {
from {
opacity: 0;
transform: translateY(-4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideLeftAndFade {
from {
opacity: 0;
transform: translateX(4px);
}
to {
opacity: 1;
transform: translateX(0);
}
}

View File

@ -35,7 +35,6 @@ export function useClickOutside<T extends HTMLElement = any>(
document.removeEventListener(fn, listener)
)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref, handler, nodes])
return ref

View File

@ -0,0 +1,34 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState } from 'react'
export function useClipboard({ timeout = 2000 } = {}) {
const [error, setError] = useState<Error | null>(null)
const [copied, setCopied] = useState(false)
const [copyTimeout, setCopyTimeout] = useState<number | null>(null)
const handleCopyResult = (value: boolean) => {
window.clearTimeout(copyTimeout!)
setCopyTimeout(window.setTimeout(() => setCopied(false), timeout))
setCopied(value)
}
const copy = (valueToCopy: any) => {
if ('clipboard' in navigator) {
navigator.clipboard
.writeText(valueToCopy)
.then(() => handleCopyResult(true))
.catch((err) => setError(err))
} else {
setError(new Error('useClipboard: navigator.clipboard is not supported'))
}
}
const reset = () => {
setCopied(false)
setError(null)
window.clearTimeout(copyTimeout!)
}
return { copy, reset, error, copied }
}

View File

@ -0,0 +1,63 @@
import { useEffect, useRef, useState } from 'react'
export interface UseMediaQueryOptions {
getInitialValueInEffect: boolean
}
type MediaQueryCallback = (event: { matches: boolean; media: string }) => void
/**
* Older versions of Safari (shipped withCatalina and before) do not support addEventListener on matchMedia
* https://stackoverflow.com/questions/56466261/matchmedia-addlistener-marked-as-deprecated-addeventlistener-equivalent
* */
function attachMediaListener(
query: MediaQueryList,
callback: MediaQueryCallback
) {
try {
query.addEventListener('change', callback)
return () => query.removeEventListener('change', callback)
} catch (e) {
query.addListener(callback)
return () => query.removeListener(callback)
}
}
function getInitialValue(query: string, initialValue?: boolean) {
if (typeof initialValue === 'boolean') {
return initialValue
}
if (typeof window !== 'undefined' && 'matchMedia' in window) {
return window.matchMedia(query).matches
}
return false
}
export function useMediaQuery(
query: string,
initialValue?: boolean,
{ getInitialValueInEffect }: UseMediaQueryOptions = {
getInitialValueInEffect: true,
}
) {
const [matches, setMatches] = useState(
getInitialValueInEffect ? initialValue : getInitialValue(query)
)
const queryRef = useRef<MediaQueryList>()
useEffect(() => {
if ('matchMedia' in window) {
queryRef.current = window.matchMedia(query)
setMatches(queryRef.current.matches)
return attachMediaListener(queryRef.current, (event) =>
setMatches(event.matches)
)
}
return undefined
}, [query])
return matches
}

View File

@ -0,0 +1,56 @@
import { useLayoutEffect, useState } from 'react'
export type OS =
| 'undetermined'
| 'macos'
| 'ios'
| 'windows'
| 'android'
| 'linux'
function getOS(): OS {
if (typeof window === 'undefined') {
return 'undetermined'
}
const { userAgent } = window.navigator
const macosPlatforms = /(Macintosh)|(MacIntel)|(MacPPC)|(Mac68K)/i
const windowsPlatforms = /(Win32)|(Win64)|(Windows)|(WinCE)/i
const iosPlatforms = /(iPhone)|(iPad)|(iPod)/i
if (macosPlatforms.test(userAgent)) {
return 'macos'
}
if (iosPlatforms.test(userAgent)) {
return 'ios'
}
if (windowsPlatforms.test(userAgent)) {
return 'windows'
}
if (/Android/i.test(userAgent)) {
return 'android'
}
if (/Linux/i.test(userAgent)) {
return 'linux'
}
return 'undetermined'
}
interface UseOsOptions {
getValueInEffect: boolean
}
export function useOs(options: UseOsOptions = { getValueInEffect: true }): OS {
const [value, setValue] = useState<OS>(
options.getValueInEffect ? 'undetermined' : getOS()
)
useLayoutEffect(() => {
if (options.getValueInEffect) {
setValue(getOS)
}
}, [])
return value
}

View File

@ -0,0 +1,9 @@
import { useEffect } from 'react'
export function usePageLeave(onPageLeave: () => void) {
useEffect(() => {
document.documentElement.addEventListener('mouseleave', onPageLeave)
return () =>
document.documentElement.removeEventListener('mouseleave', onPageLeave)
}, [])
}

View File

@ -0,0 +1,24 @@
import { useEffect, useState } from 'react'
import { useReducer } from 'react'
const reducer = (value: number) => (value + 1) % 1000000
export function useTextSelection(): Selection | null {
const [, update] = useReducer(reducer, 0)
const [selection, setSelection] = useState<Selection | null>(null)
const handleSelectionChange = () => {
setSelection(document.getSelection())
update()
}
useEffect(() => {
setSelection(document.getSelection())
document.addEventListener('selectionchange', handleSelectionChange)
return () =>
document.removeEventListener('selectionchange', handleSelectionChange)
}, [])
return selection
}

21
joi/src/index.ts Normal file
View File

@ -0,0 +1,21 @@
export * from './core/Tooltip'
export * from './core/ScrollArea'
export * from './core/Button'
export * from './core/Switch'
export * from './core/Progress'
export * from './core/Checkbox'
export * from './core/Badge'
export * from './core/Modal'
export * from './core/Slider'
export * from './core/Input'
export * from './core/Select'
export * from './core/TextArea'
export * from './core/Tabs'
export * from './core/Accordion'
export * from './hooks/useClipboard'
export * from './hooks/usePageLeave'
export * from './hooks/useTextSelection'
export * from './hooks/useClickOutside'
export * from './hooks/useOs'
export * from './hooks/useMediaQuery'

10
joi/tailwind.config.js Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('tailwindcss').Config} */
// eslint-disable-next-line no-undef
module.exports = {
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
theme: {
extend: {},
},
plugins: [],
}

16
joi/tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "esnext",
"declaration": true,
"declarationDir": "dist/types",
"module": "esnext",
"lib": ["es6", "dom", "es2016", "es2017"],
"sourceMap": true,
"jsx": "react",
"moduleResolution": "Node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}

View File

@ -3,14 +3,14 @@
"private": true,
"workspaces": {
"packages": [
"uikit",
"joi",
"core",
"electron",
"web",
"server"
],
"nohoist": [
"uikit",
"joi",
"core",
"electron",
"web",
@ -26,13 +26,11 @@
"pre-install:linux": "find extensions -type f -path \"**/*.tgz\" -exec cp {} pre-install \\;",
"pre-install:win32": "powershell -Command \"Get-ChildItem -Path \"extensions\" -Recurse -File -Filter \"*.tgz\" | ForEach-Object { Copy-Item -Path $_.FullName -Destination \"pre-install\" }\"",
"pre-install": "run-script-os",
"copy:assets": "cpx \"pre-install/*.tgz\" \"electron/pre-install/\" && cpx \"docs/openapi/**\" \"electron/docs/openapi\"",
"copy:assets": "cpx \"pre-install/*.tgz\" \"electron/pre-install/\" && cpx \"themes/**\" \"electron/themes\" && cpx \"docs/openapi/**\" \"electron/docs/openapi\"",
"dev:electron": "yarn copy:assets && yarn workspace jan dev",
"dev:web": "yarn workspace @janhq/web dev",
"dev:server": "yarn copy:assets && yarn workspace @janhq/server dev",
"dev": "turbo run dev --parallel --filter=!@janhq/server",
"dev:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit dev",
"build:uikit": "yarn workspace @janhq/uikit install && yarn workspace @janhq/uikit build",
"build:server": "yarn copy:assets && cd server && yarn install && yarn run build",
"build:core": "cd core && yarn install && yarn run build",
"build:web": "yarn workspace @janhq/web build && cpx \"web/out/**\" \"electron/renderer/\"",
@ -41,7 +39,9 @@
"build:extensions": "rimraf ./pre-install/*.tgz && turbo run @janhq/core#build && cd extensions && yarn install && turbo run build:publish && cd .. && yarn pre-install",
"build:test": "yarn copy:assets && turbo run @janhq/web#build && cpx \"web/out/**\" \"electron/renderer/\" && turbo run build:test",
"build": "yarn build:web && yarn build:electron",
"build:publish": "yarn copy:assets && yarn build:web && yarn workspace jan build:publish"
"build:publish": "yarn copy:assets && yarn build:web && yarn workspace jan build:publish",
"dev:joi": "yarn workspace @janhq/joi install && yarn workspace @janhq/joi dev",
"build:joi": "yarn workspace @janhq/joi install && yarn workspace @janhq/joi build"
},
"devDependencies": {
"concurrently": "^8.2.1",

View File

@ -0,0 +1,144 @@
{
"id": "dark-dimmed",
"displayName": "Dark Dimmed",
"reduceTransparent": false,
"nativeTheme": "dark",
"variables": {
"app": {
"bg": "215, 25%, 9%, 1",
"transparent": "0, 0%, 13%, 0.3",
"border": "0, 0%, 100%, 0.1",
"link": "221, 79%, 59%, 1",
"code-block": "0, 0%, 10%, 1"
},
"primary": {
"bg": {
"__default": "175, 84%, 32%, 1",
"soft": "175, 84%, 10%, 1"
},
"fg": "0, 0%, 100%, 1"
},
"secondary": {
"bg": "0, 0%, 100%, 0.2",
"fg": "0, 0%, 80%, 1"
},
"tertiary": {
"bg": "0, 0%, 0%, 0.02"
},
"disabled": {
"bg": "0, 0%, 0%, 0.2",
"fg": "0, 0%, 100%, 0.2"
},
"top-panel": {
"bg": "0, 0%, 13%, 0.3"
},
"bottom-panel": {
"bg": "0, 0%, 13%, 0.3"
},
"ribbon-panel": {
"bg": "0, 0%, 13%, 0.3",
"border": "0, 0%, 100%, 0.1",
"icon": "0, 0%, 68%, 1",
"icon-hover": "0, 0%, 28%, 0.2",
"icon-active": "0, 0%, 100%, 1",
"icon-active-bg": "175, 84%, 32%, 1"
},
"left-panel": {
"bg": "0, 0%, 13%, 0",
"menu": "0, 0%, 95%, 1",
"menu-hover": "0, 0%, 28%, 0.2",
"menu-active": "0, 0%, 100%, 1",
"icon-active-bg": "175, 84%, 20%, 0.2"
},
"center-panel": {
"bg": "215, 25%, 9%, 1"
},
"right-panel": {
"bg": "0, 0%, 13%, 0"
},
"tooltip": {
"bg": "0, 0%, 100%, 1",
"fg": "0, 0%, 0%, 1"
},
"switch": {
"bg": "0, 0%, 80%, 0.1",
"fg": "0, 0%, 100%, 1"
},
"dropdown-menu": {
"hover-bg": "0, 0%, 28%, 0.2"
},
"scrollbar": {
"tracker": "0, 0%, 28%, 0.2",
"thumb": "0, 0%, 20%, 1"
},
"input": {
"bg": "0, 0%, 13%, 0",
"placeholder": "0, 0%, 70%, 0.5",
"icon": "0, 0%, 68%, 1",
"border": "0, 0%, 100%, 0.1"
},
"textarea": {
"bg": "0, 0%, 13%, 0",
"placeholder": "0, 0%, 70%, 0.5",
"icon": "0, 0%, 68%, 1",
"border": "0, 0%, 100%, 0.1"
},
"select": {
"input-bg": "0, 0%, 13%, 0",
"bg": "215, 25%, 9%, 1",
"placeholder": "0, 0%, 70%, 0.5",
"icon": "0, 0%, 68%, 1",
"options-active-bg": "0, 0%, 28%, 0.2",
"border": "0, 0%, 100%, 0.1"
},
"progress-track": {
"bg": "0, 0%, 100%, 0.1"
},
"modal": {
"overlay": "0, 0%, 0%, 0.5",
"bg": "215, 25%, 9%, 1",
"fg": "0, 0%, 100%, 11"
},
"loader": {
"bg": "221, 83%, 53%, 0.1",
"fg": "0, 0%, 80%, 1",
"active-bg": "221, 83%, 53%, 0.2"
},
"toaster": {
"bg": "240, 6%, 10%, 1",
"text-title": "0, 0%, 100%, 1",
"close-icon": "0, 0%, 100%, 0.8",
"text-desc": "0, 0%, 100%, 0.88"
},
"slider": {
"track-bg": "0, 0%, 100%, 0.1",
"thumb-bg": "0, 0%, 100%, 1"
},
"resize": {
"bg": "0, 0%, 0%, 0.1"
}
}
}

144
themes/joi-dark/theme.json Normal file
View File

@ -0,0 +1,144 @@
{
"id": "joi-dark",
"displayName": "Joi Dark",
"reduceTransparent": true,
"nativeTheme": "dark",
"variables": {
"app": {
"bg": "0, 0%, 13%, 1",
"transparent": "0, 0%, 13%, 0.3",
"border": "0, 0%, 100%, 0.1",
"link": "221, 83%, 53%, 1",
"code-block": "0, 0%, 17%, 1"
},
"primary": {
"bg": {
"__default": "221, 79%, 59%, 1",
"soft": "221, 79%, 59%, 0.2"
},
"fg": "0, 0%, 100%, 1"
},
"secondary": {
"bg": "0, 0%, 100%, 0.2",
"fg": "0, 0%, 80%, 1"
},
"tertiary": {
"bg": "0, 0%, 0%, 0.02"
},
"disabled": {
"bg": "0, 0%, 0%, 0.2",
"fg": "0, 0%, 100%, 0.2"
},
"top-panel": {
"bg": "0, 0%, 13%, 0.3"
},
"bottom-panel": {
"bg": "0, 0%, 13%, 0.3"
},
"ribbon-panel": {
"bg": "0, 0%, 13%, 0.3",
"border": "0, 0%, 100%, 0.1",
"icon": "0, 0%, 68%, 1",
"icon-hover": "0, 0%, 28%, 0.2",
"icon-active": "0, 0%, 100%, 1",
"icon-active-bg": "0, 0%, 100%, 0.1"
},
"left-panel": {
"bg": "0, 0%, 13%, 0",
"menu": "0, 0%, 95%, 1",
"menu-hover": "0, 0%, 28%, 0.2",
"menu-active": "0, 0%, 100%, 1",
"icon-active-bg": "0, 0%, 100%, 0.1"
},
"center-panel": {
"bg": "0, 0%, 13%, 1"
},
"right-panel": {
"bg": "0, 0%, 13%, 0"
},
"tooltip": {
"bg": "0, 0%, 100%, 1",
"fg": "0, 0%, 0%, 1"
},
"switch": {
"bg": "0, 0%, 80%, 0.1",
"fg": "0, 0%, 100%, 1"
},
"dropdown-menu": {
"hover-bg": "0, 0%, 28%, 0.2"
},
"scrollbar": {
"tracker": "0, 0%, 28%, 0.2",
"thumb": "0, 0%, 50%, 1"
},
"input": {
"bg": "0, 0%, 13%, 0",
"placeholder": "0, 0%, 70%, 0.5",
"icon": "0, 0%, 68%, 1",
"border": "0, 0%, 100%, 0.1"
},
"textarea": {
"bg": "0, 0%, 13%, 0",
"placeholder": "0, 0%, 70%, 0.5",
"icon": "0, 0%, 68%, 1",
"border": "0, 0%, 100%, 0.1"
},
"select": {
"input-bg": "0, 0%, 13%, 0",
"bg": "0, 0%, 13%, 1",
"placeholder": "0, 0%, 70%, 0.5",
"icon": "0, 0%, 68%, 1",
"options-active-bg": "0, 0%, 28%, 0.2",
"border": "0, 0%, 100%, 0.1"
},
"progress-track": {
"bg": "0, 0%, 100%, 0.1"
},
"modal": {
"overlay": "0, 0%, 0%, 0.7",
"bg": "0, 0%, 13%, 1",
"fg": "0, 0%, 100%, 11"
},
"loader": {
"bg": "221, 83%, 53%, 0.1",
"fg": "0, 0%, 80%, 1",
"active-bg": "221, 83%, 53%, 0.2"
},
"toaster": {
"bg": "240, 6%, 10%, 1",
"text-title": "0, 0%, 100%, 1",
"close-icon": "0, 0%, 100%, 0.8",
"text-desc": "0, 0%, 100%, 0.88"
},
"slider": {
"track-bg": "0, 0%, 100%, 0.1",
"thumb-bg": "0, 0%, 100%, 1"
},
"resize": {
"bg": "0, 0%, 0%, 0.1"
}
}
}

144
themes/joi-light/theme.json Normal file
View File

@ -0,0 +1,144 @@
{
"id": "joi-light",
"displayName": "Joi Light",
"reduceTransparent": true,
"nativeTheme": "light",
"variables": {
"app": {
"bg": "0, 0%, 100%, 1",
"transparent": "0, 0%, 100%, 0.8",
"border": "0, 0%, 0%, 0.1",
"link": "221, 83%, 53%, 1",
"code-block": "0, 0%, 17%, 1"
},
"primary": {
"bg": {
"__default": "221, 83%, 53%, 1",
"soft": "221, 83%, 53%, 0.1"
},
"fg": "0, 0%, 100%, 1"
},
"secondary": {
"bg": " 0, 0%, 0%, 0.1",
"fg": " 0, 0%, 0%, 1"
},
"tertiary": {
"bg": "0, 0%, 0%, 0.02"
},
"disabled": {
"bg": " 0, 0%, 0%, 0.05",
"fg": " 0, 0%, 0%, 0.2"
},
"top-panel": {
"bg": "0, 0%, 100%, 0.8"
},
"bottom-panel": {
"bg": "0, 0%, 100%, 0.8"
},
"ribbon-panel": {
"bg": "0, 0%, 100%, 0.8",
"border": "0, 0%, 0%, 0.1",
"icon": "0, 0%, 0%, 0.5",
"icon-hover": "0, 0%, 0%, 0.03",
"icon-active": "0, 0%, 0%, 1",
"icon-active-bg": "0, 0%, 0%, 0.05"
},
"left-panel": {
"bg": "0, 0%, 100%, 1",
"menu": "0, 0%, 0%, 0.8",
"menu-hover": "0, 0%, 0%, 0.03",
"menu-active": "0, 0%, 0%, 1",
"icon-active-bg": "0, 0%, 0%, 0.05"
},
"center-panel": {
"bg": "0, 0%, 100%, 1"
},
"right-panel": {
"bg": "0, 0%, 100%, 1"
},
"tooltip": {
"bg": "0, 0%, 0%, 1",
"fg": "0, 0%, 100%, 1"
},
"switch": {
"bg": "0, 0%, 0%, 0.1",
"fg": "0, 0%, 100%, 1"
},
"dropdown-menu": {
"hover-bg": "0, 0%, 0%, 0.03"
},
"scrollbar": {
"tracker": "0, 0%, 95%, 0.1",
"thumb": "0, 0%, 0%, 0.1"
},
"input": {
"bg": "0, 0%, 100%, 0",
"placeholder": "0, 0%, 0%, 0.5",
"icon": "0, 0%, 0%, 0.5",
"border": "0, 0%, 0%, 0.1"
},
"textarea": {
"bg": "0, 0%, 100%, 0",
"placeholder": "0, 0%, 0%, 0.5",
"icon": "0, 0%, 0%, 0.5",
"border": "0, 0%, 0%, 0.1"
},
"select": {
"input-bg": "0, 0%, 100%, 0",
"bg": "0, 0%, 100%, 1",
"placeholder": "0, 0%, 0%, 0.5",
"icon": "0, 0%, 0%, 0.5",
"options-active-bg": "0, 0%, 0%, 0.03",
"border": "0, 0%, 0%, 0.1"
},
"progress-track": {
"bg": "0, 0%, 0%, 0.1"
},
"modal": {
"overlay": "0, 0%, 0%, 0.5",
"bg": "0, 0%, 100%, 1",
"fg": "0, 0%, 0%, 1"
},
"loader": {
"bg": "221, 83%, 53%, 0.1",
"fg": "221, 83%, 53%, 1",
"active-bg": "221, 83%, 53%, 0.2"
},
"toaster": {
"bg": "240, 6%, 10%, 1",
"text-title": "0, 0%, 100%, 1",
"close-icon": "0, 0%, 100%, 0.8",
"text-desc": "0, 0%, 100%, 0.8"
},
"slider": {
"track-bg": "0, 0%, 0%, 0.1",
"thumb-bg": "0, 0%, 100%, 1"
},
"resize": {
"bg": "0, 0%, 0%, 0.1"
}
}
}

View File

@ -0,0 +1,144 @@
{
"id": "night-blue",
"displayName": "Night Blue",
"reduceTransparent": false,
"nativeTheme": "dark",
"variables": {
"app": {
"bg": "211, 100%, 15%, 1",
"transparent": "221, 79%, 59%, 0.08",
"border": "0, 0%, 100%, 0.1",
"link": "142, 76%, 36%, 1",
"code-block": "222, 96%, 10%, 1"
},
"primary": {
"bg": {
"__default": "221, 79%, 59%, 1",
"soft": "221, 79%, 59%, 0.2"
},
"fg": "0, 0%, 100%, 1"
},
"secondary": {
"bg": "0, 0%, 100%, 0.2",
"fg": "0, 0%, 80%, 1"
},
"tertiary": {
"bg": "0, 0%, 0%, 0.02"
},
"disabled": {
"bg": "0, 0%, 0%, 0.2",
"fg": "0, 0%, 100%, 0.2"
},
"top-panel": {
"bg": "211, 100%, 9%, 1"
},
"bottom-panel": {
"bg": "211, 100%, 9%, 1"
},
"ribbon-panel": {
"bg": "211, 100%, 10%, 1",
"border": "0, 0%, 100%, 0.1",
"icon": "0, 0%, 68%, 1",
"icon-hover": "0, 0%, 28%, 0.2",
"icon-active": "0, 0%, 100%, 1",
"icon-active-bg": "0, 0%, 100%, 0.1"
},
"left-panel": {
"bg": "211, 100%, 12%, 1",
"menu": "0, 0%, 95%, 1",
"menu-hover": "0, 0%, 28%, 0.2",
"menu-active": "0, 0%, 100%, 1",
"icon-active-bg": "0, 0%, 100%, 0.1"
},
"center-panel": {
"bg": "211, 100%, 15%, 1"
},
"right-panel": {
"bg": "211, 100%, 12%, 1"
},
"tooltip": {
"bg": "0, 0%, 100%, 1",
"fg": "0, 0%, 0%, 1"
},
"switch": {
"bg": "0, 0%, 80%, 0.1",
"fg": "0, 0%, 100%, 1"
},
"dropdown-menu": {
"hover-bg": "0, 0%, 28%, 0.2"
},
"scrollbar": {
"tracker": "0, 0%, 28%, 0.2",
"thumb": "0, 0%, 50%, 1"
},
"input": {
"bg": "0, 0%, 13%, 0",
"placeholder": "0, 0%, 70%, 0.5",
"icon": "0, 0%, 68%, 1",
"border": "0, 0%, 100%, 0.1"
},
"textarea": {
"bg": "0, 0%, 13%, 0",
"placeholder": "0, 0%, 70%, 0.5",
"icon": "0, 0%, 68%, 1",
"border": "0, 0%, 100%, 0.1"
},
"select": {
"input-bg": "0, 0%, 13%, 0",
"bg": "211, 100%, 15%, 1",
"placeholder": "0, 0%, 70%, 0.5",
"icon": "0, 0%, 68%, 1",
"options-active-bg": "0, 0%, 28%, 0.2",
"border": "0, 0%, 100%, 0.1"
},
"progress-track": {
"bg": "0, 0%, 100%, 0.1"
},
"modal": {
"overlay": "0, 0%, 0%, 0.5",
"bg": "222, 96%, 16%, 1",
"fg": "0, 0%, 100%, 11"
},
"loader": {
"bg": "221, 83%, 53%, 0.1",
"fg": "0, 0%, 80%, 1",
"active-bg": "221, 83%, 53%, 0.2"
},
"toaster": {
"bg": "222, 100%, 15%, 1",
"text-title": "0, 0%, 100%, 1",
"close-icon": "0, 0%, 100%, 0.8",
"text-desc": "0, 0%, 100%, 0.88"
},
"slider": {
"track-bg": "0, 0%, 100%, 0.1",
"thumb-bg": "0, 0%, 100%, 1"
},
"resize": {
"bg": "0, 0%, 0%, 0.1"
}
}
}

View File

@ -10,7 +10,7 @@
"@janhq/web#dev": {
"cache": false,
"persistent": true,
"dependsOn": ["@janhq/core#build", "@janhq/uikit#build"]
"dependsOn": ["@janhq/core#build", "@janhq/joi#build"]
},
"@janhq/server#build": {
"outputs": ["dist/**"],
@ -26,7 +26,7 @@
},
"@janhq/web#build": {
"outputs": ["out/**"],
"dependsOn": ["@janhq/core#build", "@janhq/uikit#build"]
"dependsOn": ["@janhq/core#build", "@janhq/joi#build"]
},
"jan#build": {
"outputs": ["dist/**"],

View File

@ -1,57 +0,0 @@
{
"name": "@janhq/uikit",
"version": "0.1.0",
"license": "MIT",
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"files": [
"dist/**"
],
"scripts": {
"build:styles": "postcss src/main.scss -o dist/index.css --use postcss-import",
"build:react": "tsup src/index.{ts,tsx} --format cjs,esm --dts --external react react-dom --minify terser --splitting --sourcemap",
"dev:react": "tsup src/index.{ts,tsx} --format cjs,esm --watch --dts",
"dev:styles": "postcss src/main.scss -o dist/index.css -u postcss-import -w",
"build": "yarn build:styles && yarn build:react",
"dev": "concurrently --kill-others \"yarn dev:styles\" \"yarn dev:react\""
},
"dependencies": {
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-context": "^1.0.1",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-slider": "^1.1.2",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-toast": "^1.1.5",
"@radix-ui/react-tooltip": "^1.0.7",
"autoprefixer": "^10.4.16",
"class-variance-authority": "^0.7.0",
"cmdk": "^0.2.0",
"lucide-react": "^0.292.0",
"postcss": "^8.4.31",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.47.0",
"scss": "^0.2.4",
"tailwindcss": "^3.3.5"
},
"devDependencies": {
"concurrently": "^8.2.2",
"postcss-cli": "^10.1.0",
"postcss-import": "^15.1.0",
"prejss-cli": "^0.3.3",
"prettier": "^3.0.3",
"prettier-plugin-tailwindcss": "^0.5.6",
"tailwind-merge": "^2.0.0",
"terser": "^5.24.0",
"tsup": "^7.2.0",
"typescript": "^5.3.3"
}
}

View File

@ -1,8 +0,0 @@
module.exports = {
plugins: {
"tailwindcss/nesting": {},
tailwindcss: {},
autoprefixer: {},
"postcss-import": {},
},
};

View File

@ -1,43 +0,0 @@
'use client'
import { forwardRef, ElementRef, ComponentPropsWithoutRef } from 'react'
import * as AvatarPrimitive from '@radix-ui/react-avatar'
import { twMerge } from 'tailwind-merge'
const Avatar = forwardRef<
ElementRef<typeof AvatarPrimitive.Root>,
ComponentPropsWithoutRef<typeof AvatarPrimitive.Root>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Root
ref={ref}
className={twMerge('avatar', className)}
{...props}
/>
))
Avatar.displayName = AvatarPrimitive.Root.displayName
const AvatarImage = forwardRef<
ElementRef<typeof AvatarPrimitive.Image>,
ComponentPropsWithoutRef<typeof AvatarPrimitive.Image>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Image
ref={ref}
className={twMerge('avatar-image', className)}
{...props}
/>
))
AvatarImage.displayName = AvatarPrimitive.Image.displayName
const AvatarFallback = forwardRef<
ElementRef<typeof AvatarPrimitive.Fallback>,
ComponentPropsWithoutRef<typeof AvatarPrimitive.Fallback>
>(({ className, ...props }, ref) => (
<AvatarPrimitive.Fallback
ref={ref}
className={twMerge('avatar-fallback', className)}
{...props}
/>
))
AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
export { Avatar, AvatarImage, AvatarFallback }

View File

@ -1,11 +0,0 @@
.avatar {
@apply relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full;
&-image {
@apply aspect-square h-full w-full;
}
&-fallback {
@apply bg-muted flex h-full w-full items-center justify-center rounded-full font-bold uppercase;
}
}

View File

@ -1,32 +0,0 @@
import * as React from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { twMerge } from 'tailwind-merge'
const badgeVariants = cva('badge', {
variants: {
themes: {
primary: 'badge-primary',
warning: 'badge-warning',
success: 'badge-success',
secondary: 'badge-secondary',
danger: 'badge-danger',
outline: 'badge-outline',
pink: 'badge-pink',
},
},
defaultVariants: {
themes: 'primary',
},
})
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, themes, ...props }: BadgeProps) {
return (
<div className={twMerge(badgeVariants({ themes }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -1,31 +0,0 @@
.badge {
@apply focus:ring-ring border-border inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2;
&-primary {
@apply border-transparent bg-blue-100 text-blue-600;
}
&-pink {
@apply border-transparent bg-pink-100 text-pink-700;
}
&-success {
@apply border-transparent bg-green-100 text-green-600;
}
&-secondary {
@apply bg-secondary text-secondary-foreground hover:bg-secondary/80;
}
&-danger {
@apply border-transparent bg-red-100 text-red-700;
}
&-warning {
@apply border-transparent bg-yellow-100 text-yellow-700;
}
&-outline {
@apply text-foreground border-border border;
}
}

View File

@ -1,101 +0,0 @@
'use client'
import { forwardRef, ButtonHTMLAttributes } from 'react'
import { Slot } from '@radix-ui/react-slot'
import { cva, type VariantProps } from 'class-variance-authority'
import { twMerge } from 'tailwind-merge'
const buttonVariants = cva('btn', {
variants: {
themes: {
primary: 'btn-primary',
danger: 'btn-danger',
outline: 'btn-outline',
secondary: 'btn-secondary',
secondaryBlue: 'btn-secondary-blue',
secondaryDanger: 'btn-secondary-danger',
ghost: 'btn-ghost',
success: 'btn-success',
},
size: {
sm: 'btn-sm',
md: 'btn-md',
lg: 'btn-lg',
},
block: {
true: 'w-full',
},
loading: {
true: 'btn-loading',
},
},
defaultVariants: {
themes: 'primary',
size: 'md',
},
})
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
className,
themes,
size,
block,
loading,
asChild = false,
children,
...props
},
ref
) => {
const Comp = asChild ? Slot : 'button'
return (
<Comp
className={twMerge(
buttonVariants({ themes, size, block, loading, className })
)}
ref={ref}
{...props}
>
{loading ? (
<>
<svg
aria-hidden="true"
role="status"
className="btn-loading-circle"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{children}
</>
) : (
children
)}
</Comp>
)
}
)
Button.displayName = 'Button'
export { Button, buttonVariants }

View File

@ -1,84 +0,0 @@
.btn {
@apply inline-flex items-center justify-center whitespace-nowrap rounded-lg font-semibold transition-colors;
@apply cursor-pointer;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
&-primary {
@apply bg-primary hover:bg-primary/90 text-white;
}
&-secondary-blue {
@apply bg-blue-200 text-blue-600 hover:bg-blue-300/50 dark:hover:bg-blue-200/80;
}
&-danger {
@apply bg-danger text-danger-foreground hover:bg-danger/90;
}
&-secondary-danger {
@apply bg-red-200 text-red-600 hover:bg-red-300/50 dark:hover:bg-red-200/80;
}
&-outline {
@apply border-input border bg-transparent;
}
&-secondary {
@apply bg-secondary text-secondary-foreground hover:bg-secondary/80;
}
&-success {
@apply bg-green-500 text-white hover:bg-green-500/80;
}
&-ghost {
@apply hover:bg-secondary hover:text-secondary-foreground;
}
&-sm {
@apply h-7 rounded-md px-3 text-xs;
}
&-md {
@apply h-9 px-4 py-2;
}
&-lg {
@apply h-10 rounded-md px-8;
}
&-loading {
@apply pointer-events-none opacity-50;
&-circle {
@apply mr-2 h-4 animate-spin opacity-50;
> circle {
opacity: 0.25;
}
> path {
opacity: 0.75;
}
}
}
}
[type='button'],
[type='reset'],
[type='submit'] {
&.btn-primary {
@apply bg-primary hover:bg-primary/90;
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
}
&.btn-secondary {
@apply bg-secondary hover:bg-secondary/80;
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
}
&.btn-secondary-blue {
@apply bg-blue-200 text-blue-900 hover:bg-blue-200/80;
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
}
&.btn-danger {
@apply bg-danger hover:bg-danger/90;
@apply disabled:pointer-events-none disabled:bg-zinc-100 disabled:text-zinc-400;
}
}

View File

@ -1,29 +0,0 @@
'use client'
import * as React from 'react'
import * as CheckboxPrimitive from '@radix-ui/react-checkbox'
import { CheckIcon } from '@radix-ui/react-icons'
import { twMerge } from 'tailwind-merge'
const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={twMerge('checkbox', className)}
{...props}
>
<CheckboxPrimitive.Indicator
className={twMerge(
'flex flex-shrink-0 items-center justify-center text-current'
)}
>
<CheckIcon className="checkbox--icon" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -1,7 +0,0 @@
.checkbox {
@apply border-border data-[state=checked]:bg-primary h-5 w-5 flex-shrink-0 rounded-md border data-[state=checked]:text-white;
&--icon {
@apply h-4 w-4;
}
}

View File

@ -1,66 +0,0 @@
/*
* react-circular-progressbar styles
* All of the styles in this file are configurable!
*/
.CircularProgressbar {
/*
* This fixes an issue where the CircularProgressbar svg has
* 0 width inside a "display: flex" container, and thus not visible.
*/
width: 100%;
/*
* This fixes a centering issue with CircularProgressbarWithChildren:
* https://github.com/kevinsqi/react-circular-progressbar/issues/94
*/
vertical-align: middle;
}
.CircularProgressbar .CircularProgressbar-path {
stroke: #3e98c7;
stroke-linecap: round;
transition: stroke-dashoffset 0.5s ease 0s;
}
.CircularProgressbar .CircularProgressbar-trail {
stroke: #d6d6d6;
/* Used when trail is not full diameter, i.e. when props.circleRatio is set */
stroke-linecap: round;
}
.CircularProgressbar .CircularProgressbar-text {
fill: #3e98c7;
font-size: 20px;
dominant-baseline: middle;
text-anchor: middle;
}
.CircularProgressbar .CircularProgressbar-background {
fill: #d6d6d6;
}
/*
* Sample background styles. Use these with e.g.:
*
* <CircularProgressbar
* className="CircularProgressbar-inverted"
* background
* percentage={50}
* />
*/
.CircularProgressbar.CircularProgressbar-inverted
.CircularProgressbar-background {
fill: #3e98c7;
}
.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-text {
fill: #fff;
}
.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-path {
stroke: #fff;
}
.CircularProgressbar.CircularProgressbar-inverted .CircularProgressbar-trail {
stroke: transparent;
}

View File

@ -1,138 +0,0 @@
'use client'
import * as React from 'react'
import { DialogProps } from '@radix-ui/react-dialog'
import { Command as CommandPrimitive } from 'cmdk'
import { Search } from 'lucide-react'
import { Modal, ModalContent } from '../modal'
import { twMerge } from 'tailwind-merge'
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={twMerge('command', className)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
interface CommandModalProps extends DialogProps {}
const CommandModal = ({ children, ...props }: CommandModalProps) => {
return (
<Modal {...props}>
<ModalContent className="command-modal-content">
<Command
filter={(value, search) => {
if (value.includes(search)) return 1
return 0
}}
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5"
>
{children}
</Command>
</ModalContent>
</Modal>
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="command-input-wrapper" cmdk-input-wrapper="">
<Search className="command-search-icon" />
<CommandPrimitive.Input
ref={ref}
className={twMerge('command-input', className)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={twMerge('command-list', className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => (
<CommandPrimitive.Empty ref={ref} className="command-empty" {...props} />
))
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={twMerge('command-group', className)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={twMerge('bg-border -mx-1 h-px', className)}
{...props}
/>
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={twMerge('command-list-item', className)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={twMerge('command-sc', className)} {...props} />
}
CommandShortcut.displayName = 'CommandShortcut'
export {
Command,
CommandModal,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
CommandShortcut,
CommandSeparator,
}

View File

@ -1,45 +0,0 @@
.command {
@apply bg-background/80 text-muted-foreground flex h-full w-full flex-col overflow-hidden rounded-md text-left;
&-modal-content {
@apply overflow-hidden p-0;
> .modal-close {
top: 12px;
}
}
&-input-wrapper {
@apply border-border flex items-center border-b px-3;
}
&-input {
@apply placeholder:text-muted-foreground flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50;
}
&-search-icon {
@apply mr-2 h-4 w-4 shrink-0 opacity-50;
}
&-list {
@apply max-h-[300px] overflow-y-auto overflow-x-hidden py-2;
}
&-list-item {
@apply text-foreground aria-selected:bg-secondary relative flex cursor-pointer select-none items-center rounded-md px-2 py-2 text-sm outline-none;
}
&-empty {
@apply py-6 text-center text-sm;
}
&-group {
@apply text-muted-foreground overflow-hidden p-1 px-2 py-1.5 text-xs font-medium;
> [cmdk-group-heading] {
@apply mb-2 pl-2;
}
}
&-sc {
@apply text-muted-foreground ml-auto text-xs tracking-widest;
}
}

View File

@ -1,175 +0,0 @@
import * as React from 'react'
import * as LabelPrimitive from '@radix-ui/react-label'
import { Slot } from '@radix-ui/react-slot'
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from 'react-hook-form'
import { twMerge } from 'tailwind-merge'
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error('useFormField should be used within <FormField>')
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={twMerge(className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = 'FormItem'
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<label
ref={ref}
className={twMerge('form-label', className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = 'FormLabel'
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
className={error && 'form-input-error'}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = 'FormControl'
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={twMerge('form-description', className)}
{...props}
/>
)
})
FormDescription.displayName = 'FormDescription'
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={twMerge('form-message', className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = 'FormMessage'
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -1,21 +0,0 @@
.form {
&-item {
@apply space-y-2;
}
&-input-error {
@apply border-danger;
}
&-label {
@apply mb-2 inline-block cursor-pointer font-medium;
}
&-description {
@apply text-muted-foreground text-xs;
}
&-message {
@apply text-danger mt-2 text-xs font-medium;
}
}

View File

@ -1,15 +0,0 @@
export * from './avatar'
export * from './switch'
export * from './button'
export * from './scroll-area'
export * from './form'
export * from './input'
export * from './progress'
export * from './badge'
export * from './tooltip'
export * from './modal'
export * from './command'
export * from './textarea'
export * from './select'
export * from './slider'
export * from './checkbox'

View File

@ -1,27 +0,0 @@
import { forwardRef } from 'react'
import { twMerge } from 'tailwind-merge'
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
textAlign?: 'left' | 'right'
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ className, type, textAlign, ...props }, ref) => {
return (
<input
type={type}
className={twMerge(
'input',
className,
textAlign === 'right' && 'text-right'
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = 'Input'
export { Input }

View File

@ -1,9 +0,0 @@
.input {
@apply border-border placeholder:text-muted-foreground flex h-9 w-full rounded-lg border bg-transparent px-3 py-1 transition-colors;
@apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
@apply file:border-0 file:bg-transparent file:font-medium;
&.text-right {
text-align: right;
}
}

View File

@ -1,111 +0,0 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
@import './avatar/styles.scss';
@import './switch/styles.scss';
@import './button/styles.scss';
@import './scroll-area/styles.scss';
@import './form/styles.scss';
@import './input/styles.scss';
@import './progress/styles.scss';
@import './badge/styles.scss';
@import './tooltip/styles.scss';
@import './modal/styles.scss';
@import './command/styles.scss';
@import './textarea/styles.scss';
@import './select/styles.scss';
@import './slider/styles.scss';
@import './checkbox/styles.scss';
@import './circular-progress/styles.scss';
.animate-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
:root {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--danger: 346.8 77.2% 49.8%;
--danger-foreground: 355.7 100% 97.3%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 20 14.3% 4.1%;
--scroll-bar: 60, 3%, 86%;
.primary-blue {
--primary: 221 83% 53%;
--primary-foreground: 210 40% 98%;
--secondary: 60 4.8% 95.9%;
--secondary-foreground: 24 9.8% 10%;
}
.primary-green {
--primary: 142.1 76.2% 36.3%;
--primary-foreground: 355.7 100% 97.3%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
}
.primary-purple {
--primary: 262.1 83.3% 57.8%;
--primary-foreground: 210 20% 98%;
--secondary: 220 14.3% 95.9%;
--secondary-foreground: 220.9 39.3% 11%;
}
}
.dark {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--danger: 346.8 77.2% 49.8%;
--danger-foreground: 355.7 100% 97.3%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 35.5 91.7% 32.9%;
.primary-blue {
--primary: 221 83% 53%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 12 6.5% 15.1%;
--secondary-foreground: 60 9.1% 97.8%;
}
.primary-green {
--primary: 142.1 70.6% 45.3%;
--primary-foreground: 144.9 80.4% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
}
.primary-purple {
--primary: 263.4 70% 50.4%;
--primary-foreground: 210 20% 98%;
--secondary: 215 27.9% 16.9%;
--secondary-foreground: 210 20% 98%;
}
}

View File

@ -1,99 +0,0 @@
'use client'
import * as React from 'react'
import * as ModalPrimitive from '@radix-ui/react-dialog'
import { X } from 'lucide-react'
import { twMerge } from 'tailwind-merge'
const Modal = ModalPrimitive.Root
const ModalTrigger = ModalPrimitive.Trigger
const ModalPortal = ModalPrimitive.Portal
const ModalClose = ModalPrimitive.Close
const ModalOverlay = React.forwardRef<
React.ElementRef<typeof ModalPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof ModalPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<ModalPrimitive.Overlay
ref={ref}
className={twMerge('modal-backdrop', className)}
{...props}
/>
))
ModalOverlay.displayName = ModalPrimitive.Overlay.displayName
const ModalContent = React.forwardRef<
React.ElementRef<typeof ModalPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof ModalPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<ModalPortal>
<ModalOverlay />
<ModalPrimitive.Content
ref={ref}
className={twMerge('modal-content', className)}
{...props}
>
{children}
<ModalPrimitive.Close className="modal-close">
<X size={20} />
</ModalPrimitive.Close>
</ModalPrimitive.Content>
</ModalPortal>
))
ModalContent.displayName = ModalPrimitive.Content.displayName
const ModalHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={twMerge('modal-header', className)} {...props} />
)
ModalHeader.displayName = 'ModalHeader'
const ModalFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div className={twMerge('modal-footer', className)} {...props} />
)
ModalFooter.displayName = 'ModalFooter'
const ModalTitle = React.forwardRef<
React.ElementRef<typeof ModalPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof ModalPrimitive.Title>
>(({ className, ...props }, ref) => (
<ModalPrimitive.Title
ref={ref}
className={twMerge('modal-title', className)}
{...props}
/>
))
ModalTitle.displayName = ModalPrimitive.Title.displayName
const ModalDescription = React.forwardRef<
React.ElementRef<typeof ModalPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof ModalPrimitive.Description>
>(({ className, ...props }, ref) => (
<ModalPrimitive.Description
ref={ref}
className={twMerge('modal-description', className)}
{...props}
/>
))
ModalDescription.displayName = ModalPrimitive.Description.displayName
export {
Modal,
ModalPortal,
ModalOverlay,
ModalClose,
ModalTrigger,
ModalContent,
ModalHeader,
ModalFooter,
ModalTitle,
ModalDescription,
}

View File

@ -1,32 +0,0 @@
.modal {
&-backdrop {
@apply bg-background/80 fixed inset-0 z-50 backdrop-blur-sm;
}
&-content {
@apply bg-background border-border fixed left-[50%] top-[50%] z-50 grid max-h-[calc(100%-48px)] w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 overflow-y-auto border p-4 shadow-lg duration-200 sm:rounded-lg md:w-full;
}
&-close {
@apply absolute right-4 top-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none disabled:pointer-events-none;
> svg {
@apply text-muted-foreground;
}
}
&-header {
@apply flex flex-col space-y-1.5 text-center sm:text-left;
}
&-footer {
@apply flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2;
}
&-title {
@apply text-lg font-semibold leading-none tracking-tight;
}
&-description {
@apply text-muted-foreground text-sm;
}
}

View File

@ -1,24 +0,0 @@
'use client'
import * as React from 'react'
import * as ProgressPrimitive from '@radix-ui/react-progress'
import { twMerge } from 'tailwind-merge'
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={twMerge('progress', className)}
{...props}
>
<ProgressPrimitive.Indicator
className="progress-indicator"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@ -1,7 +0,0 @@
.progress {
@apply bg-secondary relative h-4 w-full overflow-hidden rounded-full;
&-indicator {
@apply bg-primary h-full w-full flex-1 transition-all;
}
}

View File

@ -1,51 +0,0 @@
'use client'
import { forwardRef, ElementRef, ComponentPropsWithoutRef } from 'react'
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'
import { twMerge } from 'tailwind-merge'
const ScrollArea = forwardRef<
ElementRef<typeof ScrollAreaPrimitive.Root>,
ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={twMerge('scroll-area', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="scroll-area-viewport">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = forwardRef<
ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = 'vertical', ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={twMerge(
'scroll-bar',
orientation === 'vertical' && 'scroll-bar-vertical',
orientation === 'horizontal' && 'scroll-bar-vertical ',
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
className={twMerge(
'scroll-bar-thumb',
orientation === 'vertical' && 'flex-1'
)}
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@ -1,58 +0,0 @@
.scroll-area {
@apply relative overflow-hidden;
&-viewport {
@apply h-full w-full rounded-[inherit];
}
}
.scroll-bar {
@apply flex touch-none select-none transition-colors;
&-vertical {
@apply h-full w-2.5 border-l border-l-transparent p-[1px];
}
&-horizontal {
@apply h-2.5 flex-col border-t border-t-transparent p-[1px];
}
&-thumb {
@apply bg-border relative z-50 w-[10px] rounded-full;
}
}
// Customized scroll bar
::-webkit-scrollbar {
width: 7px;
}
::-webkit-scrollbar-thumb {
background-color: hsl(var(--scroll-bar));
border-radius: 4px;
}
::-webkit-scrollbar-track {
background-color: hsl(var(--background));
}
::-webkit-scrollbar-corner {
background-color: hsl(var(--background));
}
::-moz-scrollbar {
width: 7px;
}
::-moz-scrollbar-thumb {
background-color: hsl(var(--scroll-bar));
border-radius: 4px;
}
::-moz-scrollbar-track {
background-color: hsl(var(--background));
}
::-moz-scrollbar-corner {
background-color: hsl(var(--background));
}

View File

@ -1,136 +0,0 @@
'use client'
import * as React from 'react'
import {
CaretSortIcon,
ChevronDownIcon,
ChevronUpIcon,
} from '@radix-ui/react-icons'
import * as SelectPrimitive from '@radix-ui/react-select'
import { twMerge } from 'tailwind-merge'
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectPortal = SelectPrimitive.Portal
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={twMerge('select', className)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="select-caret" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={twMerge('select-scroll-up-button', className)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={twMerge('select-scroll-down-button', className)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={twMerge(
'select-content',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={twMerge(
'select-trigger-viewport',
position === 'popper' && 'w-full'
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={twMerge('select-label', className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={twMerge('select-item', className)}
{...props}
>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectPortal,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -1,34 +0,0 @@
.select {
@apply placeholder:text-muted-foreground border-border flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm disabled:cursor-not-allowed [&>span]:line-clamp-1;
@apply disabled:text-muted-foreground disabled:cursor-not-allowed disabled:bg-zinc-100 disabled:dark:bg-zinc-800 disabled:dark:text-zinc-600;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
&-caret {
@apply h-4 w-4 opacity-50;
}
&-scroll-up-button {
@apply flex cursor-default items-center justify-center py-1;
}
&-scroll-down-button {
@apply flex cursor-default items-center justify-center py-1;
}
&-label {
@apply px-2 py-1.5 text-sm font-semibold;
}
&-item {
@apply hover:bg-secondary relative my-1 block w-full cursor-pointer select-none items-center rounded-sm px-4 py-2 text-sm data-[disabled]:pointer-events-none data-[disabled]:opacity-50;
@apply focus:outline-none focus-visible:outline-0;
}
&-trigger-viewport {
@apply w-full py-1;
}
&-content {
@apply bg-background border-border relative z-50 mt-1 block max-h-96 w-full min-w-[8rem] overflow-hidden rounded-md border shadow-md;
}
}

View File

@ -1,25 +0,0 @@
'use client'
import * as React from 'react'
import * as SliderPrimitive from '@radix-ui/react-slider'
import { twMerge } from 'tailwind-merge'
const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={twMerge('slider', className)}
{...props}
>
<SliderPrimitive.Track className="slider-track">
<SliderPrimitive.Range className="slider-range" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="slider-thumb" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName
export { Slider }

View File

@ -1,18 +0,0 @@
.slider {
@apply relative flex w-full touch-none select-none items-center;
&-track {
@apply relative h-1.5 w-full grow overflow-hidden rounded-full bg-gray-200 dark:bg-gray-800;
[data-disabled] {
@apply cursor-not-allowed opacity-50;
}
}
&-range {
@apply absolute h-full bg-blue-600;
}
&-thumb {
@apply border-primary/50 bg-background focus-visible:ring-ring block h-4 w-4 rounded-full border shadow transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50;
}
}

View File

@ -1,22 +0,0 @@
'use client'
import * as SwitchPrimitives from '@radix-ui/react-switch'
import { twMerge } from 'tailwind-merge'
import { forwardRef, ElementRef, ComponentPropsWithoutRef } from 'react'
const Switch = forwardRef<
ElementRef<typeof SwitchPrimitives.Root>,
ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={twMerge('switch peer', className)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb className={twMerge('switch-toggle')} />
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@ -1,10 +0,0 @@
.switch {
@apply inline-flex h-[20px] w-[36px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent;
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2;
@apply data-[state=checked]:bg-primary data-[state=unchecked]:bg-input;
@apply disabled:cursor-not-allowed disabled:opacity-50;
&-toggle {
@apply bg-background pointer-events-none block h-4 w-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0;
}
}

View File

@ -1,21 +0,0 @@
import * as React from 'react'
import { twMerge } from 'tailwind-merge'
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={twMerge('textarea-input', className)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = 'Textarea'
export { Textarea }

View File

@ -1,6 +0,0 @@
.textarea-input {
@apply border-border placeholder:text-muted-foreground flex w-full rounded-md border bg-transparent px-3 py-2 transition-colors;
@apply disabled:cursor-not-allowed disabled:opacity-50;
@apply focus-within:outline-none focus-visible:outline-0 focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-1;
@apply file:border-0 file:bg-transparent file:font-medium;
}

View File

@ -1,43 +0,0 @@
'use client'
import * as React from 'react'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import { twMerge } from 'tailwind-merge'
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = TooltipPrimitive.Root
const TooltipPortal = TooltipPrimitive.Portal
const TooltipTrigger = TooltipPrimitive.Trigger
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={twMerge('tooltip', className)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
const TooltipArrow = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className }, ref) => (
<TooltipPrimitive.Arrow className={twMerge('tooltip-arrow', className)} />
))
TooltipArrow.displayName = TooltipPrimitive.Arrow.displayName
export {
Tooltip,
TooltipTrigger,
TooltipContent,
TooltipProvider,
TooltipArrow,
TooltipPortal,
}

View File

@ -1,6 +0,0 @@
.tooltip {
@apply dark:bg-input dark:text-foreground z-50 overflow-hidden rounded-md bg-gray-950 px-2 py-1.5 text-xs font-medium text-gray-200 shadow-md;
&-arrow {
@apply dark:fill-input fill-gray-950;
}
}

View File

@ -1,32 +0,0 @@
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx,scss,css}'],
extend: {
colors: {
'background': 'hsl(var(--background))',
'foreground': 'hsl(var(--foreground))',
'card': 'hsl(var(--card))',
'card-foreground': 'hsl(var(--card-foreground))',
'primary': 'hsl(var(--primary))',
'primary-foreground': 'hsl(var(--primary-foreground))',
'secondary': 'hsl(var(--secondary))',
'secondary-foreground': 'hsl(var(--secondary-foreground))',
'muted': 'hsl(var(--muted))',
'muted-foreground': 'hsl(var(--muted-foreground))',
'accent': 'hsl(var(--accent))',
'accent-foreground': 'hsl(var(--accent-foreground))',
'danger': 'hsl(var(--danger))',
'danger-foreground': 'hsl(var(--danger-foreground))',
'border': 'hsl(var(--border))',
'input': 'hsl(var(--input))',
'ring': 'hsl(var(--ring))',
},
},
plugins: [],
}

View File

@ -1,24 +0,0 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "React Library",
"compilerOptions": {
"jsx": "react-jsx",
"lib": ["dom", "ES2015"],
"module": "ESNext",
"target": "es6",
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "node",
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true
},
"exclude": ["node_modules"]
}

Some files were not shown because too many files have changed in this diff Show More