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:
parent
611a361672
commit
faa09bd2bf
111
.github/workflows/jan-electron-linter-and-test.yml
vendored
111
.github/workflows/jan-electron-linter-and-test.yml
vendored
@ -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
1
.gitignore
vendored
@ -16,6 +16,7 @@ electron/renderer
|
||||
electron/models
|
||||
electron/docs
|
||||
electron/engines
|
||||
electron/themes
|
||||
electron/playwright-report
|
||||
server/pre-install
|
||||
package-lock.json
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
8
Makefile
8
Makefile
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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',
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -2,4 +2,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.hbs
|
||||
*.mdx
|
||||
*.mdx
|
||||
*.mjs
|
||||
13
joi/README.md
Normal file
13
joi/README.md
Normal 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
59
joi/package.json
Normal 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
73
joi/rollup.config.mjs
Normal 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(),
|
||||
],
|
||||
},
|
||||
]
|
||||
45
joi/src/core/Accordion/index.tsx
Normal file
45
joi/src/core/Accordion/index.tsx
Normal 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 }
|
||||
73
joi/src/core/Accordion/styles.scss
Normal file
73
joi/src/core/Accordion/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
50
joi/src/core/Badge/index.tsx
Normal file
50
joi/src/core/Badge/index.tsx
Normal 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 }
|
||||
131
joi/src/core/Badge/styles.scss
Normal file
131
joi/src/core/Badge/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
64
joi/src/core/Button/index.tsx
Normal file
64
joi/src/core/Button/index.tsx
Normal 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 }
|
||||
134
joi/src/core/Button/styles.scss
Normal file
134
joi/src/core/Button/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
51
joi/src/core/Checkbox/index.tsx
Normal file
51
joi/src/core/Checkbox/index.tsx
Normal 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 }
|
||||
51
joi/src/core/Checkbox/styles.scss
Normal file
51
joi/src/core/Checkbox/styles.scss
Normal 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));
|
||||
}
|
||||
}
|
||||
46
joi/src/core/Input/index.tsx
Normal file
46
joi/src/core/Input/index.tsx
Normal 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 }
|
||||
43
joi/src/core/Input/styles.scss
Normal file
43
joi/src/core/Input/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
joi/src/core/Modal/index.tsx
Normal file
56
joi/src/core/Modal/index.tsx
Normal 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 }
|
||||
85
joi/src/core/Modal/styles.scss
Normal file
85
joi/src/core/Modal/styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
39
joi/src/core/Progress/index.tsx
Normal file
39
joi/src/core/Progress/index.tsx
Normal 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 }
|
||||
25
joi/src/core/Progress/styles.scss
Normal file
25
joi/src/core/Progress/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
35
joi/src/core/ScrollArea/index.tsx
Normal file
35
joi/src/core/ScrollArea/index.tsx
Normal 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 }
|
||||
69
joi/src/core/ScrollArea/styles.scss
Normal file
69
joi/src/core/ScrollArea/styles.scss
Normal 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));
|
||||
}
|
||||
85
joi/src/core/Select/index.tsx
Normal file
85
joi/src/core/Select/index.tsx
Normal 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 }
|
||||
77
joi/src/core/Select/styles.scss
Normal file
77
joi/src/core/Select/styles.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
45
joi/src/core/Slider/index.tsx
Normal file
45
joi/src/core/Slider/index.tsx
Normal 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 }
|
||||
38
joi/src/core/Slider/styles.scss
Normal file
38
joi/src/core/Slider/styles.scss
Normal 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%);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
joi/src/core/Switch/index.tsx
Normal file
37
joi/src/core/Switch/index.tsx
Normal 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 }
|
||||
67
joi/src/core/Switch/styles.scss
Normal file
67
joi/src/core/Switch/styles.scss
Normal 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%;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
joi/src/core/Tabs/index.tsx
Normal file
59
joi/src/core/Tabs/index.tsx
Normal 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 }
|
||||
37
joi/src/core/Tabs/styles.scss
Normal file
37
joi/src/core/Tabs/styles.scss
Normal 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;
|
||||
}
|
||||
24
joi/src/core/TextArea/index.tsx
Normal file
24
joi/src/core/TextArea/index.tsx
Normal 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 }
|
||||
54
joi/src/core/TextArea/styles.scss
Normal file
54
joi/src/core/TextArea/styles.scss
Normal 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));
|
||||
}
|
||||
}
|
||||
53
joi/src/core/Tooltip/index.tsx
Normal file
53
joi/src/core/Tooltip/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
82
joi/src/core/Tooltip/styles.scss
Normal file
82
joi/src/core/Tooltip/styles.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
34
joi/src/hooks/useClipboard/index.ts
Normal file
34
joi/src/hooks/useClipboard/index.ts
Normal 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 }
|
||||
}
|
||||
63
joi/src/hooks/useMediaQuery/index.ts
Normal file
63
joi/src/hooks/useMediaQuery/index.ts
Normal 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
|
||||
}
|
||||
56
joi/src/hooks/useOs/index.tsx
Normal file
56
joi/src/hooks/useOs/index.tsx
Normal 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
|
||||
}
|
||||
9
joi/src/hooks/usePageLeave/index.ts
Normal file
9
joi/src/hooks/usePageLeave/index.ts
Normal 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)
|
||||
}, [])
|
||||
}
|
||||
24
joi/src/hooks/useTextSelection/index.ts
Normal file
24
joi/src/hooks/useTextSelection/index.ts
Normal 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
21
joi/src/index.ts
Normal 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
10
joi/tailwind.config.js
Normal 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
16
joi/tsconfig.json
Normal 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"]
|
||||
}
|
||||
12
package.json
12
package.json
@ -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",
|
||||
|
||||
144
themes/dark-dimmed/theme.json
Normal file
144
themes/dark-dimmed/theme.json
Normal 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
144
themes/joi-dark/theme.json
Normal 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
144
themes/joi-light/theme.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
144
themes/night-blue/theme.json
Normal file
144
themes/night-blue/theme.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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/**"],
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"tailwindcss/nesting": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
"postcss-import": {},
|
||||
},
|
||||
};
|
||||
@ -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 }
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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'
|
||||
@ -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 }
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
@ -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));
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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 }
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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: [],
|
||||
}
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user