Compare commits
101 Commits
Author | SHA1 | Date | |
---|---|---|---|
f122310c37 | |||
ef2bdbf3e0 | |||
a4da53a890 | |||
90a458268f | |||
fe1a9afeb0 | |||
a6af5622dd | |||
81854841ce | |||
57f9a9b118 | |||
a90cec770c | |||
371453b544 | |||
054568a2ec | |||
4b6724ed90 | |||
8728df1f60 | |||
3ccfa3b519 | |||
d10fec90ad | |||
3d55a98821 | |||
96d64e57bb | |||
e1c5e09872 | |||
67207d7daf | |||
617b7be249 | |||
bfbce754d9 | |||
00ebabe8b3 | |||
71751f7e22 | |||
878016d3cc | |||
79b41e45ff | |||
c2178ef473 | |||
086cbe77a9 | |||
c8f1912bae | |||
05953bbebe | |||
ad62a4ed86 | |||
d0af00db63 | |||
6dcf44fc60 | |||
9113b8e095 | |||
13cc43de2e | |||
7251bf2400 | |||
ef17f6f1ce | |||
bef29ebf19 | |||
bc2020f560 | |||
6c14cb1e51 | |||
14619f5311 | |||
b56242c497 | |||
c08a81badd | |||
b5e35aa905 | |||
61a263addb | |||
13f1d9def3 | |||
8c2986700e | |||
db5c36a3bf | |||
8465837f63 | |||
366ac90eb8 | |||
ec6192d32b | |||
2f9e155cbe | |||
508d58fa71 | |||
9d0188eacc | |||
2e441ff1e5 | |||
80c7542803 | |||
ec574d3841 | |||
52996d3a76 | |||
ce8dc422a4 | |||
952e235fda | |||
0916c483d4 | |||
321b47ab3f | |||
985a2b5297 | |||
e9d9bef4bf | |||
232c832b25 | |||
98dd58febb | |||
da628710a4 | |||
eb8ea83336 | |||
78ea14c568 | |||
46430bec41 | |||
0e73321c3a | |||
9573fd1e1a | |||
5276eb8ba9 | |||
7aa0618ae3 | |||
c52a8843e8 | |||
c1289b63ea | |||
113366d27c | |||
c1e930ba59 | |||
7228ab794f | |||
692c5fb7b3 | |||
ba04aa7cf5 | |||
780eed20d5 | |||
4d33e78454 | |||
8e9b178f5e | |||
2414b70ca5 | |||
6ed20835f6 | |||
65a6597a8e | |||
8e6aa36dab | |||
a47e9b8427 | |||
f4f2f2b820 | |||
fd7317e597 | |||
180cae69af | |||
5c12c00cec | |||
21a2bfc3d0 | |||
d9d6d033f9 | |||
5debb80330 | |||
3b88ad4e51 | |||
ffba99a229 | |||
d930c44bb0 | |||
a1e7baa6c4 | |||
871e1b8aed | |||
a5aa709656 |
4
.github/workflows/lint.yml
vendored
4
.github/workflows/lint.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
run: |
|
||||
yarn install
|
||||
- name: Lint
|
||||
run: yarn cli lint
|
||||
run: yarn cli lint --write
|
||||
- name: Build
|
||||
run: yarn cli build
|
||||
|
||||
@ -57,6 +57,6 @@ jobs:
|
||||
run: |
|
||||
yarn install
|
||||
- name: Lint
|
||||
run: yarn react lint
|
||||
run: yarn react lint --write
|
||||
- name: TypeScript Compile
|
||||
run: yarn react tsc
|
7
.idea/biome.xml
generated
Normal file
7
.idea/biome.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="BiomeSettings">
|
||||
<option name="applySafeFixesOnSave" value="true" />
|
||||
<option name="formatOnSave" value="true" />
|
||||
</component>
|
||||
</project>
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -4,4 +4,3 @@
|
||||
["[cC]olors\\s*\\=\\s*{([^]*(?=}))}", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
]
|
||||
}
|
||||
|
||||
|
20
.zed/settings.json
Normal file
20
.zed/settings.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"lsp": {
|
||||
"tailwindcss-language-server": {
|
||||
"settings": {
|
||||
"experimental": {
|
||||
"classRegex": [
|
||||
["vcn\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
|
||||
["[cC]olors\\s*\\=\\s*{([^]*(?=}))}", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"formatter": {
|
||||
"language_server": {
|
||||
"name": "biome"
|
||||
}
|
||||
},
|
||||
"format_on_save": "on"
|
||||
}
|
83
README.md
83
README.md
@ -1,11 +1,58 @@
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/pswui/.github/main/Square%20Logo%20Rounded.png" width="200" height="200" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
Meet our beautiful <a href="https://ui.psw.kr">web documentation</a>.
|
||||
</p>
|
||||
|
||||
# PSW-UI
|
||||
|
||||
Shared UI Component Repository.
|
||||
> Build your components in isolation
|
||||
|
||||
My goal is to create **fully typesafe**, **highly customizable** component with **minimum complexity**.
|
||||
**There are a lot of component libraries out there, but why it install so many things?**
|
||||
|
||||
Meet our [web documentation](https://ui.psw.kr)!
|
||||
## Introduction
|
||||
|
||||
> Beautifully designed, easily copy & pastable, fully customizable - that means it only depends on few dependencies.
|
||||
|
||||
This is **UI kit**, a collection of re-usable components that you can copy and paste into your apps.
|
||||
This UI kit is inspired by [shadcn/ui](https://ui.shadcn.com/) - but it is more customizable.
|
||||
|
||||
**More customizable?**
|
||||
|
||||
shadcn/ui depends on a lot of packages to provide functionality to its components.
|
||||
Radix UI, React DayPicker, Embla Carousel...
|
||||
|
||||
I'm not saying it's a bad thing.
|
||||
|
||||
But when you depends on a lot of package - you easily mess up your package.json, and you can't even edit a single feature.
|
||||
The only thing you can customize is **style**.
|
||||
|
||||
If there's a bug that needs to be fixed quickly, or a feature that doesn't work the way you want it to,
|
||||
you'll want to tweak it to your liking. This is where relying on a lot of packages can be poison.
|
||||
|
||||
PSW/UI solves this - by **NOT** depending on any other packages than framework like React, TailwindCSS, and tailwind-merge.
|
||||
|
||||
You can just copy it, and all functionality is contained in a single file.
|
||||
|
||||
## Roadmap
|
||||
|
||||
First of all, this project is alpha.
|
||||
|
||||
You can see a lot of components are missing, and some component is buggy.
|
||||
|
||||
I'm working with this priority:
|
||||
|
||||
1. Adding new component, with stable features.
|
||||
2. Fixing bugs with existing components.
|
||||
3. Make it looks nice.
|
||||
|
||||
Also, there is a [Github README](https://github.com/pswui/ui) for component implementation plans.
|
||||
|
||||
You can see what component is implemented, or planned to be implemented.
|
||||
|
||||
If you have any ideas or suggestions, please let me know in [Github Issues](https://github.com/pswui/ui/issues).
|
||||
|
||||
## Milestones
|
||||
|
||||
@ -15,11 +62,6 @@ Meet our [web documentation](https://ui.psw.kr)!
|
||||
- [ ] FileInput
|
||||
- [ ] ImageInput
|
||||
- [ ] Form
|
||||
- [ ] FormItem
|
||||
- [ ] FormLabel
|
||||
- [ ] FormControl
|
||||
- [ ] FormDescription
|
||||
- [ ] FormMessage
|
||||
- [ ] Textarea
|
||||
- [ ] Accordion
|
||||
- [ ] Alert
|
||||
@ -53,24 +95,35 @@ Meet our [web documentation](https://ui.psw.kr)!
|
||||
- [ ] Toggle
|
||||
- [ ] Toggle Group
|
||||
- [x] Tooltip
|
||||
- Library/Framework Support
|
||||
- [ ] React
|
||||
- [ ] Svelte
|
||||
- CLI
|
||||
- [x] Add
|
||||
- [x] List
|
||||
- [x] Search
|
||||
|
||||
## Building local development environment
|
||||
|
||||
```bash
|
||||
# Corepack - Yarn 4.2.2
|
||||
corepack enable
|
||||
corepack install yarn@4.2.2
|
||||
corepack use yarn@4.2.2
|
||||
|
||||
# Install Packages
|
||||
yarn install
|
||||
|
||||
# Run Storybook
|
||||
yarn workspace react storybook
|
||||
# Script running in workspace
|
||||
yarn react dev # `yarn dev` in react workspace
|
||||
yarn cli build # `yarn build` in cli workspace
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
* `registry.json` - for CLI component registry
|
||||
* `packages/react` - React components
|
||||
* `components` - component files & directories
|
||||
* `lib` - shared utility used by component
|
||||
* `src` - small test area
|
||||
* `packages/cli` - CLI package using [oclif](https://oclif.io)
|
||||
* `src/commands` - command class declaration
|
||||
* `src/components` - React component used by CLI via [ink](https://www.npmjs.com/package/ink)
|
||||
* `src/helpers` - utility functions that helps CLI
|
||||
* `const.ts` - constant & small value builder (like URL) & types & interfaces
|
||||
* `public.ts` - will be exported
|
||||
|
27
biome.json
Normal file
27
biome.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.8.3/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true,
|
||||
"defaultBranch": "main"
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"formatWithErrors": false,
|
||||
"attributePosition": "multiline",
|
||||
"indentWidth": 2,
|
||||
"indentStyle": "space",
|
||||
"lineEnding": "lf",
|
||||
"lineWidth": 80
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true
|
||||
}
|
||||
}
|
||||
}
|
18
lefthook.yml
Normal file
18
lefthook.yml
Normal file
@ -0,0 +1,18 @@
|
||||
pre-commit:
|
||||
commands:
|
||||
biome_write:
|
||||
glob: "*.{ts,tsx,json}"
|
||||
run: yarn biome check --write --no-errors-on-unmatched --files-ignore-unknown=true {staged_files} && git update-index --again
|
||||
pre-push:
|
||||
commands:
|
||||
biome_check:
|
||||
glob: "*.{ts,tsx,json}"
|
||||
run: yarn biome check --no-errors-on-unmatched --files-ignore-unknown=true {all_files}
|
||||
react_tsc:
|
||||
glob: "packages/react/*.{ts,tsx}"
|
||||
root: "packages/react/"
|
||||
run: yarn tsc
|
||||
cli_tsc:
|
||||
glob: "packages/cli/*.{ts,tsx}"
|
||||
root: "packages/cli/"
|
||||
run: yarn tsc
|
12
package.json
12
package.json
@ -5,11 +5,9 @@
|
||||
"repository": "https://github.com/pswui/ui",
|
||||
"author": "p-sw <shinwoo.park@psw.kr>",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"components"
|
||||
],
|
||||
"workspaces": ["packages/*", "components"],
|
||||
"scripts": {
|
||||
"postinstall": "lefthook install",
|
||||
"react": "yarn workspace react",
|
||||
"cli": "yarn workspace @psw-ui/cli",
|
||||
"react:build": "yarn workspace react build",
|
||||
@ -17,5 +15,9 @@
|
||||
"cli:build": "yarn workspace @psw-ui/cli build"
|
||||
},
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.2.2+sha512.c44e283c54e02de9d1da8687025b030078c1b9648d2895a65aab8e64225bfb7becba87e1809fc0b4b6778bbd47a1e2ab6ac647de4c5e383a53a7c17db6c3ff4b"
|
||||
"packageManager": "yarn@4.4.0+sha512.91d93b445d9284e7ed52931369bc89a663414e5582d00eea45c67ddc459a2582919eece27c412d6ffd1bd0793ff35399381cb229326b961798ce4f4cc60ddfdb",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.8.3",
|
||||
"lefthook": "^1.6.18"
|
||||
}
|
||||
}
|
||||
|
@ -1 +0,0 @@
|
||||
/dist
|
@ -1,93 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"xo-space",
|
||||
"plugin:n/recommended",
|
||||
"plugin:unicorn/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
"plugin:perfectionist/recommended-natural",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"n",
|
||||
"unicorn",
|
||||
"import"
|
||||
],
|
||||
"rules": {
|
||||
"capitalized-comments": 0,
|
||||
"comma-dangle": ["error", "always-multiline"],
|
||||
"default-case": 0,
|
||||
"no-multi-spaces": 0,
|
||||
"n/shebang": 0,
|
||||
"curly": 0,
|
||||
"quotes": ["error","single",{
|
||||
"avoidEscape": true
|
||||
}],
|
||||
"semi": ["error","never"],
|
||||
"unicorn/prevent-abbreviations": "off",
|
||||
"unicorn/no-await-expression-member": "off",
|
||||
"unicorn/no-null": "off",
|
||||
"unicorn/prefer-module": "warn",
|
||||
"logical-assignment-operators": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-useless-constructor": "error",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"import/no-unresolved": "error",
|
||||
"import/default": "off",
|
||||
"import/no-named-as-default-member": "off",
|
||||
"n/no-missing-import": "off",
|
||||
"n/no-unsupported-features/es-syntax": "off",
|
||||
"no-unused-expressions": "off",
|
||||
"no-useless-constructor": "off",
|
||||
"perfectionist/sort-classes": [
|
||||
"error",
|
||||
{
|
||||
"groups": [
|
||||
"index-signature",
|
||||
"static-property",
|
||||
"property",
|
||||
"private-property",
|
||||
"constructor",
|
||||
"static-method",
|
||||
"static-private-method",
|
||||
["get-method", "set-method"],
|
||||
"method",
|
||||
"private-method",
|
||||
"unknown"
|
||||
],
|
||||
"order": "asc",
|
||||
"type": "alphabetical"
|
||||
}
|
||||
],
|
||||
"valid-jsdoc": ["warn", {
|
||||
"requireParamType": false,
|
||||
"requireReturnType": false
|
||||
}]
|
||||
},
|
||||
"settings": {
|
||||
"import/parsers": {
|
||||
"@typescript-eslint/parser": [
|
||||
".ts", ".tsx"
|
||||
]
|
||||
},
|
||||
"import/resolver": {
|
||||
"typescript": {
|
||||
"alwaysTryTypes": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"describe": true,
|
||||
"it": true
|
||||
}
|
||||
}
|
@ -1,10 +1,6 @@
|
||||
{
|
||||
"require": [
|
||||
"ts-node/register"
|
||||
],
|
||||
"watch-extensions": [
|
||||
"ts"
|
||||
],
|
||||
"require": ["ts-node/register"],
|
||||
"watch-extensions": ["ts"],
|
||||
"recursive": true,
|
||||
"reporter": "spec",
|
||||
"timeout": 60000,
|
||||
|
@ -1 +0,0 @@
|
||||
"@oclif/prettier-config"
|
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env -S node --loader ts-node/esm --no-warnings=ExperimentalWarning
|
||||
|
||||
// eslint-disable-next-line n/shebang
|
||||
import {execute} from '@oclif/core'
|
||||
import { execute } from "@oclif/core";
|
||||
|
||||
await execute({development: true, dir: import.meta.url})
|
||||
await execute({ development: true, dir: import.meta.url });
|
||||
|
@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {execute} from '@oclif/core'
|
||||
import { execute } from "@oclif/core";
|
||||
|
||||
await execute({dir: import.meta.url})
|
||||
await execute({ dir: import.meta.url });
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@psw-ui/cli",
|
||||
"description": "CLI for PSW/UI",
|
||||
"version": "0.5.0",
|
||||
"version": "0.5.1",
|
||||
"author": "p-sw",
|
||||
"bin": {
|
||||
"pswui": "./bin/run.js"
|
||||
@ -14,31 +14,17 @@
|
||||
"@types/treeify": "^1.0.3",
|
||||
"ink": "^5.0.1",
|
||||
"ink-text-input": "^6.0.0",
|
||||
"node-fetch": "^3.3.2",
|
||||
"ora": "^8.0.1",
|
||||
"react": "^18.3.1",
|
||||
"treeify": "^1.1.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@oclif/prettier-config": "^0.2.1",
|
||||
"@oclif/test": "^4",
|
||||
"@types/chai": "^4",
|
||||
"@types/ink-divider": "^2.0.4",
|
||||
"@types/node": "^18",
|
||||
"@typescript-eslint/eslint-plugin": "^7.13.0",
|
||||
"@typescript-eslint/parser": "^7.13.0",
|
||||
"chai": "^4",
|
||||
"eslint": "^8",
|
||||
"eslint-config-oclif": "^5",
|
||||
"eslint-config-oclif-typescript": "^3",
|
||||
"eslint-config-prettier": "^9",
|
||||
"eslint-config-xo-space": "^0.35.0",
|
||||
"eslint-import-resolver-typescript": "^3.6.1",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-n": "^17.9.0",
|
||||
"eslint-plugin-perfectionist": "^2.11.0",
|
||||
"eslint-plugin-unicorn": "^54.0.0",
|
||||
"oclif": "^4",
|
||||
"shx": "^0.3.3",
|
||||
"tailwind-scrollbar": "^3.1.0",
|
||||
@ -48,15 +34,9 @@
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"files": [
|
||||
"/bin",
|
||||
"/dist",
|
||||
"/oclif.manifest.json"
|
||||
],
|
||||
"files": ["/bin", "/dist", "/oclif.manifest.json"],
|
||||
"homepage": "https://ui.psw.kr",
|
||||
"keywords": [
|
||||
"oclif"
|
||||
],
|
||||
"keywords": ["oclif"],
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
@ -64,9 +44,7 @@
|
||||
"bin": "pswui",
|
||||
"dirname": "pswui",
|
||||
"commands": "./dist/commands",
|
||||
"plugins": [
|
||||
"@oclif/plugin-help"
|
||||
],
|
||||
"plugins": ["@oclif/plugin-help"],
|
||||
"topicSeparator": " ",
|
||||
"topics": {
|
||||
"hello": {
|
||||
@ -77,7 +55,7 @@
|
||||
"repository": "pswui/ui",
|
||||
"scripts": {
|
||||
"build": "shx rm -rf dist && tsc",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint": "biome check --no-errors-on-unmatched",
|
||||
"prepack": "yarn build"
|
||||
},
|
||||
"types": "dist/index.d.ts"
|
||||
|
@ -1,36 +1,43 @@
|
||||
import {Args, Command, Flags} from '@oclif/core'
|
||||
import {loadConfig, validateConfig} from '../helpers/config.js'
|
||||
import {existsSync} from 'node:fs'
|
||||
import {mkdir, writeFile} from 'node:fs/promises'
|
||||
import {join} from 'node:path'
|
||||
import {getComponentURL, getDirComponentURL, getRegistry} from '../helpers/registry.js'
|
||||
import ora from 'ora'
|
||||
import React, {ComponentPropsWithoutRef} from 'react'
|
||||
import {render, Box} from 'ink'
|
||||
import {SearchBox} from '../components/SearchBox.js'
|
||||
import {getDirComponentRequiredFiles, checkComponentInstalled} from '../helpers/path.js'
|
||||
import {Choice} from '../components/Choice.js'
|
||||
import {colorize} from '@oclif/core/ux'
|
||||
import {safeFetch} from '../helpers/safe-fetcher.js'
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { Args, Command, Flags } from "@oclif/core";
|
||||
import { colorize } from "@oclif/core/ux";
|
||||
import { Box, render } from "ink";
|
||||
import ora from "ora";
|
||||
import React, { type ComponentPropsWithoutRef } from "react";
|
||||
import { Choice } from "../components/Choice.js";
|
||||
import { SearchBox } from "../components/SearchBox.js";
|
||||
import { loadConfig, validateConfig } from "../helpers/config.js";
|
||||
import {
|
||||
checkComponentInstalled,
|
||||
getDirComponentRequiredFiles,
|
||||
} from "../helpers/path.js";
|
||||
import {
|
||||
getComponentURL,
|
||||
getDirComponentURL,
|
||||
getRegistry,
|
||||
} from "../helpers/registry.js";
|
||||
import { safeFetch } from "../helpers/safe-fetcher.js";
|
||||
|
||||
function Generator() {
|
||||
let complete: boolean = false
|
||||
let complete = false;
|
||||
|
||||
function ComponentSelector<T extends {displayName: string; key: string; installed: boolean}>(
|
||||
props: Omit<ComponentPropsWithoutRef<typeof SearchBox<T>>, 'helper'>,
|
||||
) {
|
||||
function ComponentSelector<
|
||||
T extends { displayName: string; key: string; installed: boolean },
|
||||
>(props: Omit<ComponentPropsWithoutRef<typeof SearchBox<T>>, "helper">) {
|
||||
return (
|
||||
<Box>
|
||||
<SearchBox
|
||||
helper={'Press Enter to select component.'}
|
||||
helper={"Press Enter to select component."}
|
||||
{...props}
|
||||
onSubmit={(value) => {
|
||||
complete = true
|
||||
props.onSubmit?.(value)
|
||||
complete = true;
|
||||
props.onSubmit?.(value);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
@ -38,29 +45,31 @@ function Generator() {
|
||||
new Promise<void>((r) => {
|
||||
const i = setInterval(() => {
|
||||
if (complete) {
|
||||
r()
|
||||
clearInterval(i)
|
||||
r();
|
||||
clearInterval(i);
|
||||
}
|
||||
}, 100)
|
||||
}, 100);
|
||||
}),
|
||||
] as const
|
||||
] as const;
|
||||
}
|
||||
|
||||
function Generator2() {
|
||||
let complete = false
|
||||
let complete = false;
|
||||
|
||||
function ForceSelector({onComplete}: {onComplete: (value: 'yes' | 'no') => void}) {
|
||||
function ForceSelector({
|
||||
onComplete,
|
||||
}: { onComplete: (value: "yes" | "no") => void }) {
|
||||
return (
|
||||
<Choice
|
||||
question={'You already installed this component. Overwrite?'}
|
||||
yes={'Yes, overwrite existing file and install it.'}
|
||||
no={'No, cancel the action.'}
|
||||
question={"You already installed this component. Overwrite?"}
|
||||
yes={"Yes, overwrite existing file and install it."}
|
||||
no={"No, cancel the action."}
|
||||
onSubmit={(value) => {
|
||||
complete = true
|
||||
onComplete(value)
|
||||
complete = true;
|
||||
onComplete(value);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return [
|
||||
@ -68,190 +77,237 @@ function Generator2() {
|
||||
new Promise<void>((r) => {
|
||||
const i = setInterval(() => {
|
||||
if (complete) {
|
||||
r()
|
||||
clearInterval(i)
|
||||
r();
|
||||
clearInterval(i);
|
||||
}
|
||||
}, 100)
|
||||
}, 100);
|
||||
}),
|
||||
] as const
|
||||
] as const;
|
||||
}
|
||||
|
||||
export default class Add extends Command {
|
||||
static override args = {
|
||||
name: Args.string({description: 'name of component to install'}),
|
||||
}
|
||||
name: Args.string({ description: "name of component to install" }),
|
||||
};
|
||||
|
||||
static override description = 'Add a component to the project.'
|
||||
static override description = "Add a component to the project.";
|
||||
|
||||
static override examples = ['<%= config.bin %> <%= command.id %>']
|
||||
static override examples = ["<%= config.bin %> <%= command.id %>"];
|
||||
|
||||
static override flags = {
|
||||
branch: Flags.string({char: 'r', description: 'use other branch instead of main'}),
|
||||
force: Flags.boolean({char: 'f', description: 'override the existing file'}),
|
||||
config: Flags.string({char: 'p', description: 'path to config'}),
|
||||
shared: Flags.string({char: 's', description: 'place for installation of shared.ts'}),
|
||||
components: Flags.string({char: 'c', description: 'place for installation of components'}),
|
||||
}
|
||||
branch: Flags.string({
|
||||
char: "r",
|
||||
description: "use other branch instead of main",
|
||||
}),
|
||||
force: Flags.boolean({
|
||||
char: "f",
|
||||
description: "override the existing file",
|
||||
}),
|
||||
config: Flags.string({ char: "p", description: "path to config" }),
|
||||
shared: Flags.string({
|
||||
char: "s",
|
||||
description: "place for installation of shared.ts",
|
||||
}),
|
||||
components: Flags.string({
|
||||
char: "c",
|
||||
description: "place for installation of components",
|
||||
}),
|
||||
};
|
||||
|
||||
public async run(): Promise<void> {
|
||||
let {
|
||||
args,
|
||||
flags: { force, ...flags },
|
||||
} = await this.parse(Add)
|
||||
} = await this.parse(Add);
|
||||
|
||||
const resolvedConfig = await validateConfig((message: string) => this.log(message), await loadConfig(flags.config))
|
||||
const componentFolder = join(process.cwd(), resolvedConfig.paths.components)
|
||||
const libFolder = join(process.cwd(), resolvedConfig.paths.lib)
|
||||
const resolvedConfig = await validateConfig(
|
||||
(message: string) => this.log(message),
|
||||
await loadConfig(flags.config),
|
||||
);
|
||||
const componentFolder = join(
|
||||
process.cwd(),
|
||||
resolvedConfig.paths.components,
|
||||
);
|
||||
const libFolder = join(process.cwd(), resolvedConfig.paths.lib);
|
||||
if (!existsSync(componentFolder)) {
|
||||
await mkdir(componentFolder, {recursive: true})
|
||||
await mkdir(componentFolder, { recursive: true });
|
||||
}
|
||||
if (!existsSync(libFolder)) {
|
||||
await mkdir(libFolder, {recursive: true})
|
||||
await mkdir(libFolder, { recursive: true });
|
||||
}
|
||||
|
||||
const loadRegistryOra = ora('Fetching registry...').start()
|
||||
const loadRegistryOra = ora("Fetching registry...").start();
|
||||
if (flags.registry) {
|
||||
this.log(`Using ${flags.branch} for branch.`)
|
||||
this.log(`Using ${flags.branch} for branch.`);
|
||||
}
|
||||
const unsafeRegistry = await getRegistry(flags.branch)
|
||||
const unsafeRegistry = await getRegistry(flags.branch);
|
||||
if (!unsafeRegistry.ok) {
|
||||
loadRegistryOra.fail(unsafeRegistry.message)
|
||||
return
|
||||
loadRegistryOra.fail(unsafeRegistry.message);
|
||||
return;
|
||||
}
|
||||
const registry = unsafeRegistry.registry
|
||||
const componentNames = Object.keys(registry.components)
|
||||
loadRegistryOra.succeed(`Successfully fetched registry! (${componentNames.length} components)`)
|
||||
const searchBoxComponent: {displayName: string; key: string; installed: boolean}[] = []
|
||||
const registry = unsafeRegistry.registry;
|
||||
const componentNames = Object.keys(registry.components);
|
||||
loadRegistryOra.succeed(
|
||||
`Successfully fetched registry! (${componentNames.length} components)`,
|
||||
);
|
||||
const searchBoxComponent: {
|
||||
displayName: string;
|
||||
key: string;
|
||||
installed: boolean;
|
||||
}[] = [];
|
||||
for await (const name of componentNames) {
|
||||
const installed = await checkComponentInstalled(registry.components[name], resolvedConfig)
|
||||
const installed = await checkComponentInstalled(
|
||||
registry.components[name],
|
||||
resolvedConfig,
|
||||
);
|
||||
searchBoxComponent.push({
|
||||
displayName: installed ? `${name} (installed)` : name,
|
||||
key: name,
|
||||
installed,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
let name: string | undefined = args.name?.toLowerCase?.()
|
||||
let name: string | undefined = args.name?.toLowerCase?.();
|
||||
let requireForce: boolean =
|
||||
!name || !componentNames.includes(name.toLowerCase())
|
||||
? false
|
||||
: searchBoxComponent.find(({ key }) => key === name)?.installed
|
||||
? !force
|
||||
: false
|
||||
: false;
|
||||
|
||||
if (!name || !componentNames.includes(name.toLowerCase())) {
|
||||
const [ComponentSelector, waitForComplete] = Generator()
|
||||
const [ComponentSelector, waitForComplete] = Generator();
|
||||
|
||||
const inkInstance = render(
|
||||
<ComponentSelector
|
||||
components={searchBoxComponent}
|
||||
initialQuery={args.name}
|
||||
onSubmit={(comp) => {
|
||||
name = comp.key
|
||||
requireForce = comp.installed
|
||||
inkInstance.clear()
|
||||
name = comp.key;
|
||||
requireForce = comp.installed;
|
||||
inkInstance.clear();
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
await waitForComplete
|
||||
inkInstance.unmount()
|
||||
);
|
||||
await waitForComplete;
|
||||
inkInstance.unmount();
|
||||
}
|
||||
|
||||
let quit = false
|
||||
let quit = false;
|
||||
|
||||
if (requireForce) {
|
||||
const [ForceSelector, waitForComplete] = Generator2()
|
||||
const [ForceSelector, waitForComplete] = Generator2();
|
||||
|
||||
const inkInstance = render(
|
||||
<ForceSelector
|
||||
onComplete={(value) => {
|
||||
force = value === 'yes'
|
||||
quit = value === 'no'
|
||||
inkInstance.clear()
|
||||
force = value === "yes";
|
||||
quit = value === "no";
|
||||
inkInstance.clear();
|
||||
}}
|
||||
/>,
|
||||
)
|
||||
await waitForComplete
|
||||
inkInstance.unmount()
|
||||
);
|
||||
await waitForComplete;
|
||||
inkInstance.unmount();
|
||||
if (quit) {
|
||||
this.log(colorize('redBright', 'Installation canceled by user.'))
|
||||
return
|
||||
this.log(colorize("redBright", "Installation canceled by user."));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!name || !componentNames.includes(name.toLowerCase())) {
|
||||
this.error('Component name is not provided, or not selected.')
|
||||
this.error("Component name is not provided, or not selected.");
|
||||
}
|
||||
|
||||
const libFileOra = ora('Installing required library...').start()
|
||||
let successCount = 0
|
||||
const libFileOra = ora("Installing required library...").start();
|
||||
let successCount = 0;
|
||||
for await (const libFile of registry.lib) {
|
||||
const filePath = join(libFolder, libFile)
|
||||
const filePath = join(libFolder, libFile);
|
||||
if (!existsSync(filePath)) {
|
||||
const libFileContentResponse = await safeFetch(registry.base + registry.paths.lib.replace('{libName}', libFile))
|
||||
const libFileContentResponse = await safeFetch(
|
||||
registry.base + registry.paths.lib.replace("{libName}", libFile),
|
||||
);
|
||||
if (!libFileContentResponse.ok) {
|
||||
libFileOra.fail(libFileContentResponse.message)
|
||||
return
|
||||
libFileOra.fail(libFileContentResponse.message);
|
||||
return;
|
||||
}
|
||||
const libFileContent = await libFileContentResponse.response.text()
|
||||
await writeFile(filePath, libFileContent)
|
||||
successCount++
|
||||
const libFileContent = await libFileContentResponse.response.text();
|
||||
await writeFile(filePath, libFileContent);
|
||||
successCount++;
|
||||
}
|
||||
}
|
||||
if (successCount > 1) {
|
||||
libFileOra.succeed('Successfully installed library files!')
|
||||
libFileOra.succeed("Successfully installed library files!");
|
||||
} else {
|
||||
libFileOra.succeed('Library files are already installed!')
|
||||
libFileOra.succeed("Library files are already installed!");
|
||||
}
|
||||
|
||||
const componentFileOra = ora(`Installing ${name} component...`).start()
|
||||
const componentObject = registry.components[name]
|
||||
if (componentObject.type === 'file') {
|
||||
const componentFile = join(componentFolder, registry.components[name].name)
|
||||
const componentFileOra = ora(`Installing ${name} component...`).start();
|
||||
const componentObject = registry.components[name];
|
||||
if (componentObject.type === "file") {
|
||||
const componentFile = join(
|
||||
componentFolder,
|
||||
registry.components[name].name,
|
||||
);
|
||||
if (existsSync(componentFile) && !force) {
|
||||
componentFileOra.succeed(`Component is already installed! (${componentFile})`)
|
||||
componentFileOra.succeed(
|
||||
`Component is already installed! (${componentFile})`,
|
||||
);
|
||||
} else {
|
||||
const componentFileContentResponse = await safeFetch(await getComponentURL(registry, componentObject))
|
||||
const componentFileContentResponse = await safeFetch(
|
||||
await getComponentURL(registry, componentObject),
|
||||
);
|
||||
if (!componentFileContentResponse.ok) {
|
||||
componentFileOra.fail(componentFileContentResponse.message)
|
||||
return
|
||||
componentFileOra.fail(componentFileContentResponse.message);
|
||||
return;
|
||||
}
|
||||
const componentFileContent = (await componentFileContentResponse.response.text()).replaceAll(
|
||||
/import\s+{[^}]*}\s+from\s+"@pswui-lib"/g,
|
||||
(match) => match.replace(/@pswui-lib/, resolvedConfig.import.lib),
|
||||
)
|
||||
await writeFile(componentFile, componentFileContent)
|
||||
componentFileOra.succeed('Component is successfully installed!')
|
||||
const componentFileContent = (
|
||||
await componentFileContentResponse.response.text()
|
||||
).replaceAll(/import\s+{[^}]*}\s+from\s+"@pswui-lib"/g, (match) =>
|
||||
match.replace(/@pswui-lib/, resolvedConfig.import.lib),
|
||||
);
|
||||
await writeFile(componentFile, componentFileContent);
|
||||
componentFileOra.succeed("Component is successfully installed!");
|
||||
}
|
||||
} else if (componentObject.type === 'dir') {
|
||||
const componentDir = join(componentFolder, componentObject.name)
|
||||
} else if (componentObject.type === "dir") {
|
||||
const componentDir = join(componentFolder, componentObject.name);
|
||||
if (!existsSync(componentDir)) {
|
||||
await mkdir(componentDir, {recursive: true})
|
||||
await mkdir(componentDir, { recursive: true });
|
||||
}
|
||||
const requiredFiles = await getDirComponentRequiredFiles(componentObject, resolvedConfig)
|
||||
const requiredFiles = await getDirComponentRequiredFiles(
|
||||
componentObject,
|
||||
resolvedConfig,
|
||||
);
|
||||
if (requiredFiles.length === 0 && !force) {
|
||||
componentFileOra.succeed(`Component is already installed! (${componentDir})`)
|
||||
componentFileOra.succeed(
|
||||
`Component is already installed! (${componentDir})`,
|
||||
);
|
||||
} else {
|
||||
const requiredFilesURLs = await getDirComponentURL(registry, componentObject, requiredFiles)
|
||||
const requiredFilesURLs = await getDirComponentURL(
|
||||
registry,
|
||||
componentObject,
|
||||
requiredFiles,
|
||||
);
|
||||
for await (const [filename, url] of requiredFilesURLs) {
|
||||
const componentFile = join(componentDir, filename)
|
||||
const componentFile = join(componentDir, filename);
|
||||
if (!existsSync(componentFile) || force) {
|
||||
const componentFileContentResponse = await safeFetch(url)
|
||||
const componentFileContentResponse = await safeFetch(url);
|
||||
if (!componentFileContentResponse.ok) {
|
||||
componentFileOra.fail(componentFileContentResponse.message)
|
||||
return
|
||||
componentFileOra.fail(componentFileContentResponse.message);
|
||||
return;
|
||||
}
|
||||
const componentFileContent = (await componentFileContentResponse.response.text()).replaceAll(
|
||||
/import\s+{[^}]*}\s+from\s+"@pswui-lib"/g,
|
||||
(match) => match.replace(/@pswui-lib/, resolvedConfig.import.lib),
|
||||
)
|
||||
await writeFile(componentFile, componentFileContent)
|
||||
const componentFileContent = (
|
||||
await componentFileContentResponse.response.text()
|
||||
).replaceAll(/import\s+{[^}]*}\s+from\s+"@pswui-lib"/g, (match) =>
|
||||
match.replace(/@pswui-lib/, resolvedConfig.import.lib),
|
||||
);
|
||||
await writeFile(componentFile, componentFileContent);
|
||||
}
|
||||
}
|
||||
componentFileOra.succeed('Component is successfully installed!')
|
||||
componentFileOra.succeed("Component is successfully installed!");
|
||||
}
|
||||
}
|
||||
|
||||
this.log('Now you can import the component.')
|
||||
this.log("Now you can import the component.");
|
||||
}
|
||||
}
|
||||
|
@ -1,65 +1,89 @@
|
||||
import {Command, Flags} from '@oclif/core'
|
||||
import ora from 'ora'
|
||||
import treeify from 'treeify'
|
||||
import { Command, Flags } from "@oclif/core";
|
||||
import ora from "ora";
|
||||
import treeify from "treeify";
|
||||
|
||||
import {loadConfig, validateConfig} from '../helpers/config.js'
|
||||
import {checkComponentInstalled} from '../helpers/path.js'
|
||||
import {getComponentURL, getDirComponentURL, getRegistry} from '../helpers/registry.js'
|
||||
import { loadConfig, validateConfig } from "../helpers/config.js";
|
||||
import { checkComponentInstalled } from "../helpers/path.js";
|
||||
import {
|
||||
getComponentURL,
|
||||
getDirComponentURL,
|
||||
getRegistry,
|
||||
} from "../helpers/registry.js";
|
||||
|
||||
export default class List extends Command {
|
||||
static override description = 'Prints all available components in registry and components installed in this project.'
|
||||
static override description =
|
||||
"Prints all available components in registry and components installed in this project.";
|
||||
|
||||
static override examples = ['<%= config.bin %> <%= command.id %>']
|
||||
static override examples = ["<%= config.bin %> <%= command.id %>"];
|
||||
|
||||
static override flags = {
|
||||
branch: Flags.string({char: 'r', description: 'use other branch instead of main'}),
|
||||
config: Flags.string({char: 'p', description: 'path to config'}),
|
||||
url: Flags.boolean({char: 'u', description: 'include component file URL'}),
|
||||
}
|
||||
branch: Flags.string({
|
||||
char: "r",
|
||||
description: "use other branch instead of main",
|
||||
}),
|
||||
config: Flags.string({ char: "p", description: "path to config" }),
|
||||
url: Flags.boolean({
|
||||
char: "u",
|
||||
description: "include component file URL",
|
||||
}),
|
||||
};
|
||||
|
||||
public async run(): Promise<void> {
|
||||
const {flags} = await this.parse(List)
|
||||
const { flags } = await this.parse(List);
|
||||
|
||||
const registrySpinner = ora('Fetching registry...')
|
||||
const registrySpinner = ora("Fetching registry...");
|
||||
|
||||
const loadedConfig = await validateConfig((message: string) => this.log(message), await loadConfig(flags.config))
|
||||
const loadedConfig = await validateConfig(
|
||||
(message: string) => this.log(message),
|
||||
await loadConfig(flags.config),
|
||||
);
|
||||
|
||||
registrySpinner.start()
|
||||
registrySpinner.start();
|
||||
if (flags.branch) {
|
||||
this.log(`Using ${flags.branch} for registry.`)
|
||||
this.log(`Using ${flags.branch} for registry.`);
|
||||
}
|
||||
|
||||
const unsafeRegistry = await getRegistry(flags.branch)
|
||||
const unsafeRegistry = await getRegistry(flags.branch);
|
||||
if (!unsafeRegistry.ok) {
|
||||
registrySpinner.fail(unsafeRegistry.message)
|
||||
return
|
||||
registrySpinner.fail(unsafeRegistry.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const {registry} = unsafeRegistry
|
||||
const names = Object.keys(registry.components)
|
||||
const { registry } = unsafeRegistry;
|
||||
const names = Object.keys(registry.components);
|
||||
|
||||
registrySpinner.succeed(`Fetched ${names.length} components.`)
|
||||
registrySpinner.succeed(`Fetched ${names.length} components.`);
|
||||
|
||||
let final: Record<string, {URL?: Record<string, string>; installed: 'no' | 'yes'}> = {}
|
||||
let final: Record<
|
||||
string,
|
||||
{ URL?: Record<string, string>; installed: "no" | "yes" }
|
||||
> = {};
|
||||
for await (const name of names) {
|
||||
const componentObject = registry.components[name]
|
||||
const installed = (await checkComponentInstalled(componentObject, loadedConfig)) ? 'yes' : 'no'
|
||||
const componentObject = registry.components[name];
|
||||
const installed = (await checkComponentInstalled(
|
||||
componentObject,
|
||||
loadedConfig,
|
||||
))
|
||||
? "yes"
|
||||
: "no";
|
||||
if (flags.url) {
|
||||
let url: Record<string, string> = {}
|
||||
let url: Record<string, string> = {};
|
||||
|
||||
if (componentObject.type === 'file') {
|
||||
url[name] = await getComponentURL(registry, componentObject)
|
||||
} else if (componentObject.type === 'dir') {
|
||||
url = Object.fromEntries(await getDirComponentURL(registry, componentObject))
|
||||
if (componentObject.type === "file") {
|
||||
url[name] = await getComponentURL(registry, componentObject);
|
||||
} else if (componentObject.type === "dir") {
|
||||
url = Object.fromEntries(
|
||||
await getDirComponentURL(registry, componentObject),
|
||||
);
|
||||
}
|
||||
|
||||
final = {...final, [name]: {URL: url, installed}}
|
||||
final = { ...final, [name]: { URL: url, installed } };
|
||||
} else {
|
||||
final = {...final, [name]: {installed}}
|
||||
final = { ...final, [name]: { installed } };
|
||||
}
|
||||
}
|
||||
|
||||
this.log('AVAILABLE COMPONENTS')
|
||||
this.log(treeify.asTree(final, true, true))
|
||||
this.log("AVAILABLE COMPONENTS");
|
||||
this.log(treeify.asTree(final, true, true));
|
||||
}
|
||||
}
|
||||
|
@ -1,42 +1,45 @@
|
||||
import {Command, Args, Flags} from '@oclif/core'
|
||||
import {render} from 'ink'
|
||||
import {SearchBox} from '../components/SearchBox.js'
|
||||
import {getRegistry} from '../helpers/registry.js'
|
||||
import React from 'react'
|
||||
import { Args, Command, Flags } from "@oclif/core";
|
||||
import { render } from "ink";
|
||||
import React from "react";
|
||||
import { SearchBox } from "../components/SearchBox.js";
|
||||
import { getRegistry } from "../helpers/registry.js";
|
||||
|
||||
export default class Search extends Command {
|
||||
static override args = {
|
||||
query: Args.string({description: 'search query'}),
|
||||
}
|
||||
query: Args.string({ description: "search query" }),
|
||||
};
|
||||
|
||||
static override flags = {
|
||||
branch: Flags.string({char: 'r', description: 'use other branch instead of main'}),
|
||||
}
|
||||
branch: Flags.string({
|
||||
char: "r",
|
||||
description: "use other branch instead of main",
|
||||
}),
|
||||
};
|
||||
|
||||
static override description = 'Search components.'
|
||||
static override description = "Search components.";
|
||||
|
||||
static override examples = ['<%= config.bin %> <%= command.id %>']
|
||||
static override examples = ["<%= config.bin %> <%= command.id %>"];
|
||||
|
||||
public async run(): Promise<void> {
|
||||
const {args, flags} = await this.parse(Search)
|
||||
const { args, flags } = await this.parse(Search);
|
||||
|
||||
if (flags.branch) {
|
||||
this.log(`Using ${flags.branch} for registry.`)
|
||||
this.log(`Using ${flags.branch} for registry.`);
|
||||
}
|
||||
const registryResult = await getRegistry(flags.branch)
|
||||
const registryResult = await getRegistry(flags.branch);
|
||||
if (!registryResult.ok) {
|
||||
this.error(registryResult.message)
|
||||
this.error(registryResult.message);
|
||||
}
|
||||
const registry = registryResult.registry
|
||||
const componentNames = Object.keys(registry.components)
|
||||
const registry = registryResult.registry;
|
||||
const componentNames = Object.keys(registry.components);
|
||||
|
||||
await render(
|
||||
<SearchBox
|
||||
components={componentNames.map((v) => ({ key: v, displayName: v }))}
|
||||
initialQuery={args.query}
|
||||
helper={'Press ESC to quit'}
|
||||
helper={"Press ESC to quit"}
|
||||
onKeyDown={(_, k, app) => k.escape && app.exit()}
|
||||
/>,
|
||||
).waitUntilExit()
|
||||
).waitUntilExit();
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,26 @@
|
||||
import React, {useState} from 'react'
|
||||
import {Box, Text, useInput} from 'ink'
|
||||
import { Box, Text, useInput } from "ink";
|
||||
import React, { useState } from "react";
|
||||
|
||||
function isUnicodeSupported() {
|
||||
if (process.platform !== 'win32') {
|
||||
return process.env['TERM'] !== 'linux' // Linux console (kernel)
|
||||
if (process.platform !== "win32") {
|
||||
return process.env.TERM !== "linux"; // Linux console (kernel)
|
||||
}
|
||||
|
||||
return (
|
||||
Boolean(process.env['WT_SESSION']) || // Windows Terminal
|
||||
Boolean(process.env['TERMINUS_SUBLIME']) || // Terminus (<0.2.27)
|
||||
process.env['ConEmuTask'] === '{cmd::Cmder}' || // ConEmu and cmder
|
||||
process.env['TERM_PROGRAM'] === 'Terminus-Sublime' ||
|
||||
process.env['TERM_PROGRAM'] === 'vscode' ||
|
||||
process.env['TERM'] === 'xterm-256color' ||
|
||||
process.env['TERM'] === 'alacritty' ||
|
||||
process.env['TERMINAL_EMULATOR'] === 'JetBrains-JediTerm'
|
||||
)
|
||||
Boolean(process.env.WT_SESSION) || // Windows Terminal
|
||||
Boolean(process.env.TERMINUS_SUBLIME) || // Terminus (<0.2.27)
|
||||
process.env.ConEmuTask === "{cmd::Cmder}" || // ConEmu and cmder
|
||||
process.env.TERM_PROGRAM === "Terminus-Sublime" ||
|
||||
process.env.TERM_PROGRAM === "vscode" ||
|
||||
process.env.TERM === "xterm-256color" ||
|
||||
process.env.TERM === "alacritty" ||
|
||||
process.env.TERMINAL_EMULATOR === "JetBrains-JediTerm"
|
||||
);
|
||||
}
|
||||
|
||||
const shouldUseMain = isUnicodeSupported()
|
||||
const SELECTED: string = shouldUseMain ? '◉' : '(*)'
|
||||
const UNSELECTED: string = shouldUseMain ? '◯' : '( )'
|
||||
const shouldUseMain = isUnicodeSupported();
|
||||
const SELECTED: string = shouldUseMain ? "◉" : "(*)";
|
||||
const UNSELECTED: string = shouldUseMain ? "◯" : "( )";
|
||||
|
||||
export function Choice({
|
||||
question,
|
||||
@ -29,35 +29,38 @@ export function Choice({
|
||||
onSubmit,
|
||||
initial,
|
||||
}: {
|
||||
question: string
|
||||
yes: string
|
||||
no: string
|
||||
onSubmit?: (vaule: 'yes' | 'no') => void
|
||||
initial?: 'yes' | 'no'
|
||||
question: string;
|
||||
yes: string;
|
||||
no: string;
|
||||
onSubmit?: (vaule: "yes" | "no") => void;
|
||||
initial?: "yes" | "no";
|
||||
}) {
|
||||
const [state, setState] = useState<'yes' | 'no'>(initial ?? 'yes')
|
||||
const [state, setState] = useState<"yes" | "no">(initial ?? "yes");
|
||||
|
||||
useInput((_, k) => {
|
||||
if (k.upArrow) {
|
||||
setState('yes')
|
||||
setState("yes");
|
||||
} else if (k.downArrow) {
|
||||
setState('no')
|
||||
setState("no");
|
||||
}
|
||||
|
||||
if (k.return) {
|
||||
onSubmit?.(state)
|
||||
onSubmit?.(state);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return (
|
||||
<Box display={'flex'} flexDirection={'column'}>
|
||||
<Text color={'greenBright'}>{question}</Text>
|
||||
<Text color={state === 'yes' ? undefined : 'gray'}>
|
||||
{state === 'yes' ? SELECTED : UNSELECTED} {yes}
|
||||
<Box
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
>
|
||||
<Text color={"greenBright"}>{question}</Text>
|
||||
<Text color={state === "yes" ? undefined : "gray"}>
|
||||
{state === "yes" ? SELECTED : UNSELECTED} {yes}
|
||||
</Text>
|
||||
<Text color={state === 'no' ? undefined : 'gray'}>
|
||||
{state === 'no' ? SELECTED : UNSELECTED} {no}
|
||||
<Text color={state === "no" ? undefined : "gray"}>
|
||||
{state === "no" ? SELECTED : UNSELECTED} {no}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,24 +1,32 @@
|
||||
import React from 'react'
|
||||
import {Box, Text} from 'ink'
|
||||
import { Box, Text } from "ink";
|
||||
import React from "react";
|
||||
|
||||
export function Divider({width = 50, padding = 1, title}: {width?: number; padding?: number; title: string}) {
|
||||
const length = Math.floor((width - title.length - padding * 2) / 2)
|
||||
export function Divider({
|
||||
width = 50,
|
||||
padding = 1,
|
||||
title,
|
||||
}: { width?: number; padding?: number; title: string }) {
|
||||
const length = Math.floor((width - title.length - padding * 2) / 2);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{Array.from(Array(length)).map((_, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: there's nothing to be key except index
|
||||
<Text key={i}>─</Text>
|
||||
))}
|
||||
{Array.from(Array(padding)).map((_, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: there's nothing to be key except index
|
||||
<Text key={i}> </Text>
|
||||
))}
|
||||
<Text>{title}</Text>
|
||||
{Array.from(Array(padding)).map((_, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: there's nothing to be key except index
|
||||
<Text key={i}> </Text>
|
||||
))}
|
||||
{Array.from(Array(length)).map((_, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: there's nothing to be key except index
|
||||
<Text key={i}>─</Text>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, {useEffect, useState} from 'react'
|
||||
import {getSuggestion} from '../helpers/search.js'
|
||||
import Input from 'ink-text-input'
|
||||
import {Divider} from './Divider.js'
|
||||
import {Box, Text, useInput, useApp, type Key} from 'ink'
|
||||
import { Box, type Key, Text, useApp, useInput } from "ink";
|
||||
import Input from "ink-text-input";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { getSuggestion } from "../helpers/search.js";
|
||||
import { Divider } from "./Divider.js";
|
||||
|
||||
export function SearchBox<T extends { key: string; displayName: string }>({
|
||||
components,
|
||||
@ -12,92 +12,115 @@ export function SearchBox<T extends {key: string; displayName: string}>({
|
||||
onChange,
|
||||
onSubmit,
|
||||
}: {
|
||||
components: T[]
|
||||
helper: string
|
||||
initialQuery?: string
|
||||
onKeyDown?: (i: string, k: Key, app: ReturnType<typeof useApp>) => void
|
||||
onChange?: (item: T) => void
|
||||
onSubmit?: (item: T) => void
|
||||
components: T[];
|
||||
helper: string;
|
||||
initialQuery?: string;
|
||||
onKeyDown?: (i: string, k: Key, app: ReturnType<typeof useApp>) => void;
|
||||
onChange?: (item: T) => void;
|
||||
onSubmit?: (item: T) => void;
|
||||
}) {
|
||||
const [query, setQuery] = useState<string>(initialQuery ?? '')
|
||||
const [queryMode, setQueryMode] = useState<boolean>(true)
|
||||
const [isLoading, setLoading] = useState<boolean>(false)
|
||||
const [suggestions, setSuggestions] = useState<string[]>([])
|
||||
const [selected, setSelected] = useState<number>(-1)
|
||||
const [query, setQuery] = useState<string>(initialQuery ?? "");
|
||||
const [queryMode, setQueryMode] = useState<boolean>(true);
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
const [suggestions, setSuggestions] = useState<string[]>([]);
|
||||
const [selected, setSelected] = useState<number>(-1);
|
||||
|
||||
useEffect(() => {
|
||||
if (queryMode) {
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
getSuggestion(
|
||||
components.map(({ key }) => key),
|
||||
query,
|
||||
).then((result) => {
|
||||
setSuggestions(result)
|
||||
setSelected(-1)
|
||||
})
|
||||
setSuggestions(result);
|
||||
setSelected(-1);
|
||||
});
|
||||
}
|
||||
}, [query, queryMode])
|
||||
}, [query, queryMode, components]);
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
const found = components.find(({key}) => key === suggestions[selected])
|
||||
found && onChange(found)
|
||||
const found = components.find(({ key }) => key === suggestions[selected]);
|
||||
found && onChange(found);
|
||||
}
|
||||
}, [selected, suggestions, onChange])
|
||||
}, [selected, suggestions, onChange, components]);
|
||||
|
||||
const app = useApp()
|
||||
const app = useApp();
|
||||
|
||||
useInput((i, k) => {
|
||||
if (k.downArrow) {
|
||||
setSelected((p) => (p >= suggestions.length - 1 ? 0 : p + 1))
|
||||
setQueryMode(false)
|
||||
setSelected((p) => (p >= suggestions.length - 1 ? 0 : p + 1));
|
||||
setQueryMode(false);
|
||||
}
|
||||
if (k.upArrow) {
|
||||
setSelected((p) => (p <= 0 ? suggestions.length - 1 : p - 1))
|
||||
setQueryMode(false)
|
||||
setSelected((p) => (p <= 0 ? suggestions.length - 1 : p - 1));
|
||||
setQueryMode(false);
|
||||
}
|
||||
onKeyDown?.(i, k, app)
|
||||
})
|
||||
onKeyDown?.(i, k, app);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!queryMode && suggestions[selected]) {
|
||||
setQuery(suggestions[selected])
|
||||
setQuery(suggestions[selected]);
|
||||
}
|
||||
}, [queryMode, selected])
|
||||
}, [queryMode, selected, suggestions]);
|
||||
|
||||
return (
|
||||
<Box width={50} display={'flex'} flexDirection={'column'}>
|
||||
<Text color={'gray'}>{helper}</Text>
|
||||
<Box display={'flex'} flexDirection={'row'}>
|
||||
<Box marginRight={1} display={'flex'} flexDirection={'row'}>
|
||||
<Text color={'greenBright'}>Search?</Text>
|
||||
<Box
|
||||
width={50}
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
>
|
||||
<Text color={"gray"}>{helper}</Text>
|
||||
<Box
|
||||
display={"flex"}
|
||||
flexDirection={"row"}
|
||||
>
|
||||
<Box
|
||||
marginRight={1}
|
||||
display={"flex"}
|
||||
flexDirection={"row"}
|
||||
>
|
||||
<Text color={"greenBright"}>Search?</Text>
|
||||
</Box>
|
||||
<Input
|
||||
value={query}
|
||||
onChange={(v) => {
|
||||
setQueryMode(true)
|
||||
setQuery(v)
|
||||
setQueryMode(true);
|
||||
setQuery(v);
|
||||
}}
|
||||
showCursor
|
||||
placeholder={' query'}
|
||||
placeholder={" query"}
|
||||
onSubmit={() => {
|
||||
const found = components.find(({key}) => key === suggestions[selected])
|
||||
found && onSubmit?.(found)
|
||||
const found = components.find(
|
||||
({ key }) => key === suggestions[selected],
|
||||
);
|
||||
found && onSubmit?.(found);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Divider title={isLoading ? 'Loading...' : `${suggestions.length} components found.`} />
|
||||
<Box display={'flex'} flexDirection={'column'}>
|
||||
<Divider
|
||||
title={
|
||||
isLoading ? "Loading..." : `${suggestions.length} components found.`
|
||||
}
|
||||
/>
|
||||
<Box
|
||||
display={"flex"}
|
||||
flexDirection={"column"}
|
||||
>
|
||||
{suggestions.map((name, index) => {
|
||||
return (
|
||||
<Box key={name}>
|
||||
<Text color={selected === index ? undefined : 'gray'}>
|
||||
{components[components.findIndex(({key}) => key === name)].displayName}
|
||||
<Text color={selected === index ? undefined : "gray"}>
|
||||
{
|
||||
components[components.findIndex(({ key }) => key === name)]
|
||||
.displayName
|
||||
}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,27 +1,28 @@
|
||||
import {z} from 'zod'
|
||||
import { z } from "zod";
|
||||
|
||||
export const registryURL = (branch: string) => `https://raw.githubusercontent.com/pswui/ui/${branch}/registry.json`
|
||||
export const CONFIG_DEFAULT_PATH = 'pswui.config.js'
|
||||
export const registryURL = (branch: string) =>
|
||||
`https://raw.githubusercontent.com/pswui/ui/${branch}/registry.json`;
|
||||
export const CONFIG_DEFAULT_PATH = "pswui.config.js";
|
||||
|
||||
export type RegistryComponent =
|
||||
| {
|
||||
files: string[]
|
||||
name: string
|
||||
type: 'dir'
|
||||
files: string[];
|
||||
name: string;
|
||||
type: "dir";
|
||||
}
|
||||
| {
|
||||
name: string
|
||||
type: 'file'
|
||||
}
|
||||
name: string;
|
||||
type: "file";
|
||||
};
|
||||
|
||||
export interface Registry {
|
||||
base: string
|
||||
components: Record<string, RegistryComponent>
|
||||
lib: string[]
|
||||
base: string;
|
||||
components: Record<string, RegistryComponent>;
|
||||
lib: string[];
|
||||
paths: {
|
||||
components: string
|
||||
lib: string
|
||||
}
|
||||
components: string;
|
||||
lib: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
@ -29,29 +30,31 @@ export interface Config {
|
||||
* Absolute path that will used for import in component
|
||||
*/
|
||||
import?: {
|
||||
lib?: '@pswui-lib' | string
|
||||
}
|
||||
lib?: "@pswui-lib" | string;
|
||||
};
|
||||
/**
|
||||
* Path that cli will create a file.
|
||||
*/
|
||||
paths?: {
|
||||
components?: 'src/pswui/components' | string
|
||||
lib?: 'src/pswui/lib' | string
|
||||
}
|
||||
components?: "src/pswui/components" | string;
|
||||
lib?: "src/pswui/lib" | string;
|
||||
};
|
||||
}
|
||||
export type ResolvedConfig<T = Config> = {
|
||||
[k in keyof T]-?: NonNullable<T[k]> extends object ? ResolvedConfig<NonNullable<T[k]>> : T[k]
|
||||
}
|
||||
[k in keyof T]-?: NonNullable<T[k]> extends object
|
||||
? ResolvedConfig<NonNullable<T[k]>>
|
||||
: T[k];
|
||||
};
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
import: {
|
||||
lib: '@pswui-lib',
|
||||
lib: "@pswui-lib",
|
||||
},
|
||||
paths: {
|
||||
components: 'src/pswui/components',
|
||||
lib: 'src/pswui/lib',
|
||||
components: "src/pswui/components",
|
||||
lib: "src/pswui/lib",
|
||||
},
|
||||
}
|
||||
};
|
||||
export const configZod = z.object({
|
||||
import: z
|
||||
.object({
|
||||
@ -61,9 +64,12 @@ export const configZod = z.object({
|
||||
.default(DEFAULT_CONFIG.import),
|
||||
paths: z
|
||||
.object({
|
||||
components: z.string().optional().default(DEFAULT_CONFIG.paths.components),
|
||||
components: z
|
||||
.string()
|
||||
.optional()
|
||||
.default(DEFAULT_CONFIG.paths.components),
|
||||
lib: z.string().optional().default(DEFAULT_CONFIG.paths.lib),
|
||||
})
|
||||
.optional()
|
||||
.default(DEFAULT_CONFIG.paths),
|
||||
})
|
||||
});
|
||||
|
@ -1,43 +1,67 @@
|
||||
import {colorize} from '@oclif/core/ux'
|
||||
import {existsSync} from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import { existsSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { colorize } from "@oclif/core/ux";
|
||||
|
||||
import {CONFIG_DEFAULT_PATH, DEFAULT_CONFIG, ResolvedConfig, configZod} from '../const.js'
|
||||
import {changeExtension} from './path.js'
|
||||
import {
|
||||
CONFIG_DEFAULT_PATH,
|
||||
DEFAULT_CONFIG,
|
||||
type ResolvedConfig,
|
||||
configZod,
|
||||
} from "../const.js";
|
||||
import { changeExtension } from "./path.js";
|
||||
|
||||
export async function loadConfig(config?: string): Promise<unknown> {
|
||||
const userConfigPath = config ? path.join(process.cwd(), config) : null
|
||||
const defaultConfigPath = path.join(process.cwd(), CONFIG_DEFAULT_PATH)
|
||||
const cjsConfigPath = path.join(process.cwd(), await changeExtension(CONFIG_DEFAULT_PATH, '.cjs'))
|
||||
const mjsConfigPath = path.join(process.cwd(), await changeExtension(CONFIG_DEFAULT_PATH, '.mjs'))
|
||||
const userConfigPath = config ? path.join(process.cwd(), config) : null;
|
||||
const defaultConfigPath = path.join(process.cwd(), CONFIG_DEFAULT_PATH);
|
||||
const cjsConfigPath = path.join(
|
||||
process.cwd(),
|
||||
await changeExtension(CONFIG_DEFAULT_PATH, ".cjs"),
|
||||
);
|
||||
const mjsConfigPath = path.join(
|
||||
process.cwd(),
|
||||
await changeExtension(CONFIG_DEFAULT_PATH, ".mjs"),
|
||||
);
|
||||
|
||||
if (userConfigPath) {
|
||||
if (existsSync(userConfigPath)) {
|
||||
return (await import(userConfigPath)).default
|
||||
return (await import(userConfigPath)).default;
|
||||
}
|
||||
|
||||
throw new Error(`Error: config ${userConfigPath} not found.`)
|
||||
throw new Error(`Error: config ${userConfigPath} not found.`);
|
||||
}
|
||||
|
||||
if (existsSync(defaultConfigPath)) {
|
||||
return (await import(defaultConfigPath)).default
|
||||
return (await import(defaultConfigPath)).default;
|
||||
}
|
||||
|
||||
if (existsSync(cjsConfigPath)) {
|
||||
return (await import(cjsConfigPath)).default
|
||||
return (await import(cjsConfigPath)).default;
|
||||
}
|
||||
|
||||
if (existsSync(mjsConfigPath)) {
|
||||
return (await import(mjsConfigPath)).default
|
||||
return (await import(mjsConfigPath)).default;
|
||||
}
|
||||
|
||||
return DEFAULT_CONFIG
|
||||
return DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
export async function validateConfig(log: (message: string) => void, config?: unknown): Promise<ResolvedConfig> {
|
||||
const parsedConfig: ResolvedConfig = await configZod.parseAsync(config)
|
||||
log(colorize('gray', `Install component to: ${path.join(process.cwd(), parsedConfig.paths.components)}`))
|
||||
log(colorize('gray', `Install shared module to: ${path.join(process.cwd(), parsedConfig.paths.lib)}`))
|
||||
log(colorize('gray', `Import shared with: ${parsedConfig.import.lib}`))
|
||||
return parsedConfig
|
||||
export async function validateConfig(
|
||||
log: (message: string) => void,
|
||||
config?: unknown,
|
||||
): Promise<ResolvedConfig> {
|
||||
const parsedConfig: ResolvedConfig = await configZod.parseAsync(config);
|
||||
log(
|
||||
colorize(
|
||||
"gray",
|
||||
`Install component to: ${path.join(process.cwd(), parsedConfig.paths.components)}`,
|
||||
),
|
||||
);
|
||||
log(
|
||||
colorize(
|
||||
"gray",
|
||||
`Install shared module to: ${path.join(process.cwd(), parsedConfig.paths.lib)}`,
|
||||
),
|
||||
);
|
||||
log(colorize("gray", `Import shared with: ${parsedConfig.import.lib}`));
|
||||
return parsedConfig;
|
||||
}
|
||||
|
@ -1,38 +1,52 @@
|
||||
import {existsSync} from 'node:fs'
|
||||
import {readdir} from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { existsSync } from "node:fs";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import {RegistryComponent, ResolvedConfig} from '../const.js'
|
||||
import type { RegistryComponent, ResolvedConfig } from "../const.js";
|
||||
|
||||
export async function getDirComponentRequiredFiles<T extends {type: 'dir'} & RegistryComponent>(
|
||||
componentObject: T,
|
||||
config: ResolvedConfig,
|
||||
) {
|
||||
const componentPath = path.join(process.cwd(), config.paths.components, componentObject.name)
|
||||
export async function getDirComponentRequiredFiles<
|
||||
T extends { type: "dir" } & RegistryComponent,
|
||||
>(componentObject: T, config: ResolvedConfig) {
|
||||
const componentPath = path.join(
|
||||
process.cwd(),
|
||||
config.paths.components,
|
||||
componentObject.name,
|
||||
);
|
||||
if (!existsSync(componentPath)) {
|
||||
return componentObject.files
|
||||
return componentObject.files;
|
||||
}
|
||||
|
||||
const dir = await readdir(componentPath)
|
||||
const dir = await readdir(componentPath);
|
||||
|
||||
return componentObject.files.filter((filename) => !dir.includes(filename))
|
||||
return componentObject.files.filter((filename) => !dir.includes(filename));
|
||||
}
|
||||
|
||||
export async function checkComponentInstalled(component: RegistryComponent, config: ResolvedConfig): Promise<boolean> {
|
||||
const componentDirRoot = path.join(process.cwd(), config.paths.components)
|
||||
if (!existsSync(componentDirRoot)) return false
|
||||
export async function checkComponentInstalled(
|
||||
component: RegistryComponent,
|
||||
config: ResolvedConfig,
|
||||
): Promise<boolean> {
|
||||
const componentDirRoot = path.join(process.cwd(), config.paths.components);
|
||||
if (!existsSync(componentDirRoot)) return false;
|
||||
|
||||
if (component.type === 'file') {
|
||||
const dir = await readdir(componentDirRoot)
|
||||
return dir.includes(component.name)
|
||||
if (component.type === "file") {
|
||||
const dir = await readdir(componentDirRoot);
|
||||
return dir.includes(component.name);
|
||||
}
|
||||
|
||||
const componentDir = path.join(componentDirRoot, component.name)
|
||||
if (!existsSync(componentDir)) return false
|
||||
const dir = await readdir(componentDir)
|
||||
return component.files.filter((filename) => !dir.includes(filename)).length === 0
|
||||
const componentDir = path.join(componentDirRoot, component.name);
|
||||
if (!existsSync(componentDir)) return false;
|
||||
const dir = await readdir(componentDir);
|
||||
return (
|
||||
component.files.filter((filename) => !dir.includes(filename)).length === 0
|
||||
);
|
||||
}
|
||||
|
||||
export async function changeExtension(_path: string, extension: string): Promise<string> {
|
||||
return path.join(path.dirname(_path), path.basename(_path, path.extname(_path)) + extension)
|
||||
export async function changeExtension(
|
||||
_path: string,
|
||||
extension: string,
|
||||
): Promise<string> {
|
||||
return path.join(
|
||||
path.dirname(_path),
|
||||
path.basename(_path, path.extname(_path)) + extension,
|
||||
);
|
||||
}
|
||||
|
@ -1,37 +1,49 @@
|
||||
import {Registry, RegistryComponent, registryURL} from '../const.js'
|
||||
import {safeFetch} from './safe-fetcher.js'
|
||||
import {
|
||||
type Registry,
|
||||
type RegistryComponent,
|
||||
registryURL,
|
||||
} from "../const.js";
|
||||
import { safeFetch } from "./safe-fetcher.js";
|
||||
|
||||
export async function getRegistry(
|
||||
branch?: string,
|
||||
): Promise<{ message: string; ok: false } | { ok: true; registry: Registry }> {
|
||||
const registryResponse = await safeFetch(registryURL(branch ?? 'main'))
|
||||
const registryResponse = await safeFetch(registryURL(branch ?? "main"));
|
||||
|
||||
if (registryResponse.ok) {
|
||||
const registryJson = (await registryResponse.response.json()) as Registry
|
||||
registryJson.base = registryJson.base.replace('{branch}', branch ?? 'main')
|
||||
const registryJson = (await registryResponse.response.json()) as Registry;
|
||||
registryJson.base = registryJson.base.replace("{branch}", branch ?? "main");
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
registry: registryJson,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return registryResponse
|
||||
return registryResponse;
|
||||
}
|
||||
|
||||
export async function getComponentURL(
|
||||
registry: Registry,
|
||||
component: {type: 'file'} & RegistryComponent,
|
||||
component: { type: "file" } & RegistryComponent,
|
||||
): Promise<string> {
|
||||
return registry.base + registry.paths.components.replace('{componentName}', component.name)
|
||||
return (
|
||||
registry.base +
|
||||
registry.paths.components.replace("{componentName}", component.name)
|
||||
);
|
||||
}
|
||||
|
||||
export async function getDirComponentURL(
|
||||
registry: Registry,
|
||||
component: {type: 'dir'} & RegistryComponent,
|
||||
component: { type: "dir" } & RegistryComponent,
|
||||
files?: string[],
|
||||
): Promise<[string, string][]> {
|
||||
const base = registry.base + registry.paths.components.replace('{componentName}', component.name)
|
||||
const base =
|
||||
registry.base +
|
||||
registry.paths.components.replace("{componentName}", component.name);
|
||||
|
||||
return (files ?? component.files).map((filename) => [filename, base + '/' + filename])
|
||||
return (files ?? component.files).map((filename) => [
|
||||
filename,
|
||||
`${base}/${filename}`,
|
||||
]);
|
||||
}
|
||||
|
@ -1,19 +1,20 @@
|
||||
import fetch, {Response} from 'node-fetch'
|
||||
|
||||
export async function safeFetch(
|
||||
url: string,
|
||||
): Promise<{message: string; ok: false; response: Response} | {ok: true; response: Response}> {
|
||||
const response = await fetch(url)
|
||||
): Promise<
|
||||
| { message: string; ok: false; response: Response }
|
||||
| { ok: true; response: Response }
|
||||
> {
|
||||
const response = await fetch(url, { cache: "no-cache" });
|
||||
if (response.ok) {
|
||||
return {
|
||||
ok: true,
|
||||
response,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Error while fetching from ${response.url}: ${response.status} ${response.statusText}`,
|
||||
ok: false,
|
||||
response,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -1,63 +1,69 @@
|
||||
export async function jaroWinkler(a: string, b: string): Promise<number> {
|
||||
const p = 0.1
|
||||
const p = 0.1;
|
||||
|
||||
if (a.length === 0 || b.length === 0) return 0
|
||||
if (a === b) return 1
|
||||
if (a.length === 0 || b.length === 0) return 0;
|
||||
if (a === b) return 1;
|
||||
|
||||
const range = Math.floor(Math.max(a.length, b.length) / 2) - 1
|
||||
let matches = 0
|
||||
const range = Math.floor(Math.max(a.length, b.length) / 2) - 1;
|
||||
let matches = 0;
|
||||
|
||||
const aMatches = Array.from({length: a.length})
|
||||
const bMatches = Array.from({length: b.length})
|
||||
const aMatches = Array.from({ length: a.length });
|
||||
const bMatches = Array.from({ length: b.length });
|
||||
|
||||
for (const [i, element] of Object.entries(a).map(
|
||||
([index, element]) => [Number.parseInt(index, 10), element] as const,
|
||||
)) {
|
||||
const start = i >= range ? i - range : 0
|
||||
const end = i + range <= b.length - 1 ? i + range : b.length - 1
|
||||
const start = i >= range ? i - range : 0;
|
||||
const end = i + range <= b.length - 1 ? i + range : b.length - 1;
|
||||
|
||||
for (let j = start; j <= end; j++) {
|
||||
if (bMatches[j] !== true && element === b[j]) {
|
||||
++matches
|
||||
aMatches[i] = true
|
||||
bMatches[j] = true
|
||||
break
|
||||
++matches;
|
||||
aMatches[i] = true;
|
||||
bMatches[j] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matches === 0) return 0
|
||||
if (matches === 0) return 0;
|
||||
|
||||
let t = 0
|
||||
let t = 0;
|
||||
|
||||
let point: number
|
||||
let point: number;
|
||||
|
||||
for (point = 0; point < a.length; point++) if (aMatches[point]) break
|
||||
for (point = 0; point < a.length; point++) if (aMatches[point]) break;
|
||||
|
||||
for (let i = point; i < a.length; i++)
|
||||
if (aMatches[i]) {
|
||||
let j
|
||||
let j: number;
|
||||
for (j = point; j < b.length; j++)
|
||||
if (bMatches[j]) {
|
||||
point = j + 1
|
||||
break
|
||||
point = j + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (a[i] !== b[j]) ++t
|
||||
if (a[i] !== b[j]) ++t;
|
||||
}
|
||||
|
||||
t /= 2
|
||||
t /= 2;
|
||||
|
||||
const J = (matches / a.length + matches / b.length + (matches - t) / matches) / 3
|
||||
return J + Math.min((p * t) / matches, 1) * (1 - J)
|
||||
const J =
|
||||
(matches / a.length + matches / b.length + (matches - t) / matches) / 3;
|
||||
return J + Math.min((p * t) / matches, 1) * (1 - J);
|
||||
}
|
||||
|
||||
export async function getSuggestion(componentNames: string[], input: string): Promise<string[]> {
|
||||
export async function getSuggestion(
|
||||
componentNames: string[],
|
||||
input: string,
|
||||
): Promise<string[]> {
|
||||
const componentJw = await Promise.all(
|
||||
componentNames.map(async (name) => [name, await jaroWinkler(name, input)] as const),
|
||||
)
|
||||
componentNames.map(
|
||||
async (name) => [name, await jaroWinkler(name, input)] as const,
|
||||
),
|
||||
);
|
||||
return componentJw
|
||||
.filter(([_, score]) => score > 0)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([name]) => name)
|
||||
.map(([name]) => name);
|
||||
}
|
||||
|
@ -1,2 +1,2 @@
|
||||
export * from './public.js'
|
||||
export {run} from '@oclif/core'
|
||||
export * from "./public.js";
|
||||
export { run } from "@oclif/core";
|
||||
|
@ -1,9 +1,9 @@
|
||||
import {Config} from './const.js'
|
||||
import type { Config } from "./const.js";
|
||||
|
||||
function buildConfig(config: Config): Config {
|
||||
return config
|
||||
return config;
|
||||
}
|
||||
|
||||
export {buildConfig}
|
||||
export { buildConfig };
|
||||
|
||||
export {Config} from './const.js'
|
||||
export { Config } from "./const.js";
|
||||
|
@ -1,18 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react-hooks/recommended', 'plugin:storybook/recommended'],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
"error",
|
||||
{ varsIgnorePattern: '^_' }
|
||||
]
|
||||
},
|
||||
}
|
4
packages/react/.gitignore
vendored
4
packages/react/.gitignore
vendored
@ -24,3 +24,7 @@ dist-ssr
|
||||
*.sw?
|
||||
|
||||
*storybook.log
|
||||
|
||||
src
|
||||
src/main.tsx
|
||||
!src/tailwind.css
|
@ -1,7 +1,9 @@
|
||||
import { type AsChild, Slot, type VariantProps, vcn } from "@pswui-lib";
|
||||
import React from "react";
|
||||
import { vcn, VariantProps, Slot, AsChild } from "@pswui-lib";
|
||||
|
||||
const colors = {
|
||||
disabled:
|
||||
"disabled:brightness-50 disabled:cursor-not-allowed disabled:opacity-50 disabled:saturate-50",
|
||||
outline: {
|
||||
focus: "dark:focus-visible:outline-white/20 focus-visible:outline-black/10",
|
||||
},
|
||||
@ -12,21 +14,18 @@ const colors = {
|
||||
danger: "border-red-400 dark:border-red-600",
|
||||
},
|
||||
background: {
|
||||
default:
|
||||
"bg-white dark:bg-black hover:bg-neutral-200 dark:hover:bg-neutral-800",
|
||||
default: "bg-white dark:bg-black",
|
||||
ghost:
|
||||
"bg-black/0 dark:bg-white/0 hover:bg-black/20 dark:hover:bg-white/20",
|
||||
success:
|
||||
"bg-green-100 dark:bg-green-900 hover:bg-green-200 dark:hover:bg-green-800",
|
||||
warning:
|
||||
"bg-yellow-100 dark:bg-yellow-900 hover:bg-yellow-200 dark:hover:bg-yellow-800",
|
||||
danger: "bg-red-100 dark:bg-red-900 hover:bg-red-200 dark:hover:bg-red-800",
|
||||
success: "bg-green-100 dark:bg-green-900",
|
||||
warning: "bg-yellow-100 dark:bg-yellow-900",
|
||||
danger: "bg-red-100 dark:bg-red-900",
|
||||
},
|
||||
underline: "decoration-current",
|
||||
};
|
||||
|
||||
const [buttonVariants, resolveVariants] = vcn({
|
||||
base: `w-fit flex flex-row items-center justify-between rounded-md outline outline-1 outline-transparent outline-offset-2 ${colors.outline.focus} transition-all`,
|
||||
base: `w-fit flex flex-row items-center justify-between rounded-md outline outline-1 outline-transparent outline-offset-2 hover:brightness-90 dark:hover:brightness-110 ${colors.outline.focus} ${colors.disabled} transition-all cursor-pointer`,
|
||||
variants: {
|
||||
size: {
|
||||
link: "p-0 text-base",
|
||||
@ -109,16 +108,22 @@ export interface ButtonProps
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(props, ref) => {
|
||||
const [variantProps, otherPropsCompressed] = resolveVariants(props);
|
||||
const { asChild, ...otherPropsExtracted } = otherPropsCompressed;
|
||||
const { asChild, type, role, ...otherPropsExtracted } =
|
||||
otherPropsCompressed;
|
||||
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const compProps = {
|
||||
...otherPropsExtracted,
|
||||
className: buttonVariants(variantProps),
|
||||
};
|
||||
|
||||
return <Comp ref={ref} {...compProps} />;
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
type={type ?? "button"}
|
||||
className={buttonVariants(variantProps)}
|
||||
role={role ?? "button"}
|
||||
{...otherPropsExtracted}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button };
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { type VariantProps, vcn } from "@pswui-lib";
|
||||
import React from "react";
|
||||
import { VariantProps, vcn } from "@pswui-lib";
|
||||
|
||||
const checkboxColors = {
|
||||
background: {
|
||||
@ -18,12 +18,10 @@ const checkboxColors = {
|
||||
disabledCheckedHover:
|
||||
"has-[input[type='checkbox']:disabled:checked]:hover:bg-neutral-300 dark:has-[input[type='checkbox']:disabled:checked]:hover:bg-neutral-700",
|
||||
},
|
||||
checkmark:
|
||||
"text-black dark:text-white has-[input[type=checkbox]:disabled]:text-neutral-400 dark:has-[input[type=checkbox]:disabled]:text-neutral-500",
|
||||
};
|
||||
|
||||
const [checkboxVariant, resolveCheckboxVariantProps] = vcn({
|
||||
base: `inline-block rounded-md ${checkboxColors.checkmark} ${checkboxColors.background.disabled} ${checkboxColors.background.default} ${checkboxColors.background.hover} ${checkboxColors.background.checked} ${checkboxColors.background.checkedHover} ${checkboxColors.background.disabledChecked} ${checkboxColors.background.disabledCheckedHover} has-[input[type="checkbox"]:disabled]:cursor-not-allowed transition-colors duration-75 ease-in-out`,
|
||||
base: `inline-block rounded-md ${checkboxColors.background.disabled} ${checkboxColors.background.default} ${checkboxColors.background.hover} ${checkboxColors.background.checked} ${checkboxColors.background.checkedHover} ${checkboxColors.background.disabledChecked} ${checkboxColors.background.disabledCheckedHover} has-[input[type="checkbox"]:disabled]:cursor-not-allowed transition-colors duration-150 ease-in-out`,
|
||||
variants: {
|
||||
size: {
|
||||
base: "size-[1em] p-0 [&>svg]:size-[1em]",
|
||||
@ -92,22 +90,11 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
className={`${checked ? "opacity-100" : "opacity-0"} transition-opacity duration-75 ease-in-out`}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M21 7L9 19l-5.5-5.5l1.41-1.41L9 16.17L19.59 5.59z"
|
||||
></path>
|
||||
</svg>
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
Checkbox.displayName = "Checkbox";
|
||||
|
||||
export { Checkbox };
|
||||
|
@ -1,12 +1,20 @@
|
||||
import React, { useState } from "react";
|
||||
import { Slot, VariantProps, vcn } from "@pswui-lib";
|
||||
import {
|
||||
Slot,
|
||||
type VariantProps,
|
||||
useAnimatedMount,
|
||||
useDocument,
|
||||
vcn,
|
||||
} from "@pswui-lib";
|
||||
import React, { type ReactNode, useId, useRef, useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import {
|
||||
DialogContext,
|
||||
type IDialogContext,
|
||||
InnerDialogContext,
|
||||
initialDialogContext,
|
||||
useDialogContext,
|
||||
IDialogContext,
|
||||
useInnerDialogContext,
|
||||
} from "./Context";
|
||||
|
||||
/**
|
||||
@ -20,7 +28,10 @@ interface DialogRootProps {
|
||||
}
|
||||
|
||||
const DialogRoot = ({ children }: DialogRootProps) => {
|
||||
const state = useState<IDialogContext>(initialDialogContext);
|
||||
const state = useState<IDialogContext>({
|
||||
...initialDialogContext,
|
||||
ids: { dialog: useId(), title: useId(), description: useId() },
|
||||
});
|
||||
return (
|
||||
<DialogContext.Provider value={state}>{children}</DialogContext.Provider>
|
||||
);
|
||||
@ -37,15 +48,17 @@ interface DialogTriggerProps {
|
||||
}
|
||||
|
||||
const DialogTrigger = ({ children }: DialogTriggerProps) => {
|
||||
const [_, setState] = useDialogContext();
|
||||
const [{ ids }, setState] = useDialogContext();
|
||||
const onClick = () => setState((p) => ({ ...p, opened: true }));
|
||||
|
||||
const slotProps = {
|
||||
onClick,
|
||||
children,
|
||||
};
|
||||
|
||||
return <Slot {...slotProps} />;
|
||||
return (
|
||||
<Slot
|
||||
onClick={onClick}
|
||||
aria-controls={ids.dialog}
|
||||
>
|
||||
{children}
|
||||
</Slot>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -55,33 +68,15 @@ const DialogTrigger = ({ children }: DialogTriggerProps) => {
|
||||
*/
|
||||
|
||||
const [dialogOverlayVariant, resolveDialogOverlayVariant] = vcn({
|
||||
base: "fixed inset-0 z-50 w-full h-full max-w-screen transition-all duration-300 flex flex-col justify-center items-center",
|
||||
base: "fixed inset-0 z-50 w-full h-screen overflow-y-auto max-w-screen transition-all duration-300 backdrop-blur-md backdrop-brightness-75 [&>div]:p-6",
|
||||
variants: {
|
||||
opened: {
|
||||
true: "pointer-events-auto opacity-100",
|
||||
false: "pointer-events-none opacity-0",
|
||||
},
|
||||
blur: {
|
||||
sm: "backdrop-blur-sm",
|
||||
md: "backdrop-blur-md",
|
||||
lg: "backdrop-blur-lg",
|
||||
},
|
||||
darken: {
|
||||
sm: "backdrop-brightness-90",
|
||||
md: "backdrop-brightness-75",
|
||||
lg: "backdrop-brightness-50",
|
||||
},
|
||||
padding: {
|
||||
sm: "p-4",
|
||||
md: "p-6",
|
||||
lg: "p-8",
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
opened: false,
|
||||
blur: "md",
|
||||
darken: "md",
|
||||
padding: "md",
|
||||
},
|
||||
});
|
||||
|
||||
@ -93,20 +88,36 @@ interface DialogOverlay
|
||||
|
||||
const DialogOverlay = React.forwardRef<HTMLDivElement, DialogOverlay>(
|
||||
(props, ref) => {
|
||||
const [{ opened }, setContext] = useDialogContext();
|
||||
const [variantProps, otherPropsCompressed] = resolveDialogOverlayVariant({
|
||||
...props,
|
||||
opened,
|
||||
});
|
||||
const [{ opened, ids }, setContext] = useDialogContext();
|
||||
const [variantProps, otherPropsCompressed] =
|
||||
resolveDialogOverlayVariant(props);
|
||||
const { children, closeOnClick, onClick, ...otherPropsExtracted } =
|
||||
otherPropsCompressed;
|
||||
return (
|
||||
<>
|
||||
{ReactDOM.createPortal(
|
||||
|
||||
const internalRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const { isMounted, isRendered } = useAnimatedMount(opened, internalRef);
|
||||
|
||||
const document = useDocument();
|
||||
if (!document) return null;
|
||||
|
||||
return isMounted
|
||||
? ReactDOM.createPortal(
|
||||
<div
|
||||
{...otherPropsExtracted}
|
||||
ref={ref}
|
||||
className={dialogOverlayVariant(variantProps)}
|
||||
id={ids.dialog}
|
||||
ref={(el) => {
|
||||
internalRef.current = el;
|
||||
if (typeof ref === "function") {
|
||||
ref(el);
|
||||
} else if (ref) {
|
||||
ref.current = el;
|
||||
}
|
||||
}}
|
||||
className={dialogOverlayVariant({
|
||||
...variantProps,
|
||||
opened: isRendered,
|
||||
})}
|
||||
onClick={(e) => {
|
||||
if (closeOnClick) {
|
||||
setContext((p) => ({ ...p, opened: false }));
|
||||
@ -114,14 +125,23 @@ const DialogOverlay = React.forwardRef<HTMLDivElement, DialogOverlay>(
|
||||
onClick?.(e);
|
||||
}}
|
||||
>
|
||||
{/* Layer for overflow positioning */}
|
||||
<div
|
||||
className={
|
||||
"w-screen max-w-full min-h-screen flex flex-col justify-center items-center"
|
||||
}
|
||||
>
|
||||
<InnerDialogContext.Provider value={{ isMounted, isRendered }}>
|
||||
{children}
|
||||
</InnerDialogContext.Provider>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
)
|
||||
: null;
|
||||
},
|
||||
);
|
||||
DialogOverlay.displayName = "DialogOverlay";
|
||||
|
||||
/**
|
||||
* =========================
|
||||
@ -130,69 +150,51 @@ const DialogOverlay = React.forwardRef<HTMLDivElement, DialogOverlay>(
|
||||
*/
|
||||
|
||||
const [dialogContentVariant, resolveDialogContentVariant] = vcn({
|
||||
base: "transition-transform duration-300 bg-white dark:bg-black border border-neutral-200 dark:border-neutral-800",
|
||||
base: "transition-transform duration-300 bg-white dark:bg-black border border-neutral-200 dark:border-neutral-800 p-6 w-full max-w-xl rounded-md flex flex-col justify-start items-start gap-6",
|
||||
variants: {
|
||||
opened: {
|
||||
true: "scale-100",
|
||||
false: "scale-50",
|
||||
},
|
||||
size: {
|
||||
fit: "w-fit",
|
||||
fullSm: "w-full max-w-sm",
|
||||
fullMd: "w-full max-w-md",
|
||||
fullLg: "w-full max-w-lg",
|
||||
fullXl: "w-full max-w-xl",
|
||||
full2xl: "w-full max-w-2xl",
|
||||
},
|
||||
rounded: {
|
||||
sm: "rounded-sm",
|
||||
md: "rounded-md",
|
||||
lg: "rounded-lg",
|
||||
xl: "rounded-xl",
|
||||
},
|
||||
padding: {
|
||||
sm: "p-4",
|
||||
md: "p-6",
|
||||
lg: "p-8",
|
||||
},
|
||||
gap: {
|
||||
sm: "space-y-4",
|
||||
md: "space-y-6",
|
||||
lg: "space-y-8",
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
opened: false,
|
||||
size: "fit",
|
||||
rounded: "md",
|
||||
padding: "md",
|
||||
gap: "md",
|
||||
},
|
||||
});
|
||||
|
||||
interface DialogContent
|
||||
interface DialogContentProps
|
||||
extends React.ComponentPropsWithoutRef<"div">,
|
||||
Omit<VariantProps<typeof dialogContentVariant>, "opened"> {}
|
||||
|
||||
const DialogContent = React.forwardRef<HTMLDivElement, DialogContent>(
|
||||
const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
|
||||
(props, ref) => {
|
||||
const [{ opened }] = useDialogContext();
|
||||
const [variantProps, otherPropsCompressed] = resolveDialogContentVariant({
|
||||
...props,
|
||||
opened,
|
||||
});
|
||||
const { children, ...otherPropsExtracted } = otherPropsCompressed;
|
||||
const [{ ids }] = useDialogContext();
|
||||
const [variantProps, otherPropsCompressed] =
|
||||
resolveDialogContentVariant(props);
|
||||
const { isRendered } = useInnerDialogContext();
|
||||
const { children, onClick, ...otherPropsExtracted } = otherPropsCompressed;
|
||||
return (
|
||||
<div
|
||||
{...otherPropsExtracted}
|
||||
ref={ref}
|
||||
className={dialogContentVariant(variantProps)}
|
||||
role="dialog"
|
||||
aria-labelledby={ids.title}
|
||||
aria-describedby={ids.description}
|
||||
className={dialogContentVariant({
|
||||
...variantProps,
|
||||
opened: isRendered,
|
||||
})}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClick?.(e);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
DialogContent.displayName = "DialogContent";
|
||||
|
||||
/**
|
||||
* =========================
|
||||
@ -223,17 +225,9 @@ const DialogClose = ({ children }: DialogCloseProps) => {
|
||||
*/
|
||||
|
||||
const [dialogHeaderVariant, resolveDialogHeaderVariant] = vcn({
|
||||
base: "flex flex-col",
|
||||
variants: {
|
||||
gap: {
|
||||
sm: "gap-2",
|
||||
md: "gap-4",
|
||||
lg: "gap-6",
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
gap: "sm",
|
||||
},
|
||||
base: "flex flex-col gap-2",
|
||||
variants: {},
|
||||
defaults: {},
|
||||
});
|
||||
|
||||
interface DialogHeaderProps
|
||||
@ -257,6 +251,8 @@ const DialogHeader = React.forwardRef<HTMLElement, DialogHeaderProps>(
|
||||
},
|
||||
);
|
||||
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
/**
|
||||
* =========================
|
||||
* DialogTitle / DialogSubtitle
|
||||
@ -264,91 +260,69 @@ const DialogHeader = React.forwardRef<HTMLElement, DialogHeaderProps>(
|
||||
*/
|
||||
|
||||
const [dialogTitleVariant, resolveDialogTitleVariant] = vcn({
|
||||
variants: {
|
||||
size: {
|
||||
sm: "text-lg",
|
||||
md: "text-xl",
|
||||
lg: "text-2xl",
|
||||
},
|
||||
weight: {
|
||||
sm: "font-medium",
|
||||
md: "font-semibold",
|
||||
lg: "font-bold",
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
size: "md",
|
||||
weight: "lg",
|
||||
},
|
||||
base: "text-xl font-bold",
|
||||
variants: {},
|
||||
defaults: {},
|
||||
});
|
||||
|
||||
interface DialogTitleProps
|
||||
extends React.ComponentPropsWithoutRef<"h1">,
|
||||
VariantProps<typeof dialogTitleVariant> {}
|
||||
|
||||
const [dialogSubtitleVariant, resolveDialogSubtitleVariant] = vcn({
|
||||
variants: {
|
||||
size: {
|
||||
sm: "text-sm",
|
||||
md: "text-base",
|
||||
lg: "text-lg",
|
||||
},
|
||||
opacity: {
|
||||
sm: "opacity-60",
|
||||
md: "opacity-70",
|
||||
lg: "opacity-80",
|
||||
},
|
||||
weight: {
|
||||
sm: "font-light",
|
||||
md: "font-normal",
|
||||
lg: "font-medium",
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
size: "sm",
|
||||
opacity: "sm",
|
||||
weight: "md",
|
||||
},
|
||||
const [dialogDescriptionVariant, resolveDialogDescriptionVariant] = vcn({
|
||||
base: "text-sm opacity-60 font-normal",
|
||||
variants: {},
|
||||
defaults: {},
|
||||
});
|
||||
|
||||
interface DialogSubtitleProps
|
||||
interface DialogDescriptionProps
|
||||
extends React.ComponentPropsWithoutRef<"h2">,
|
||||
VariantProps<typeof dialogSubtitleVariant> {}
|
||||
VariantProps<typeof dialogDescriptionVariant> {}
|
||||
|
||||
const DialogTitle = React.forwardRef<HTMLHeadingElement, DialogTitleProps>(
|
||||
(props, ref) => {
|
||||
const [variantProps, otherPropsCompressed] =
|
||||
resolveDialogTitleVariant(props);
|
||||
const { children, ...otherPropsExtracted } = otherPropsCompressed;
|
||||
const [{ ids }] = useDialogContext();
|
||||
return (
|
||||
<h1
|
||||
{...otherPropsExtracted}
|
||||
ref={ref}
|
||||
className={dialogTitleVariant(variantProps)}
|
||||
id={ids.title}
|
||||
>
|
||||
{children}
|
||||
</h1>
|
||||
);
|
||||
},
|
||||
);
|
||||
DialogTitle.displayName = "DialogTitle";
|
||||
|
||||
const DialogSubtitle = React.forwardRef<
|
||||
const DialogDescription = React.forwardRef<
|
||||
HTMLHeadingElement,
|
||||
DialogSubtitleProps
|
||||
DialogDescriptionProps
|
||||
>((props, ref) => {
|
||||
const [variantProps, otherPropsCompressed] =
|
||||
resolveDialogSubtitleVariant(props);
|
||||
resolveDialogDescriptionVariant(props);
|
||||
const { children, ...otherPropsExtracted } = otherPropsCompressed;
|
||||
const [{ ids }] = useDialogContext();
|
||||
return (
|
||||
<h2
|
||||
{...otherPropsExtracted}
|
||||
ref={ref}
|
||||
className={dialogSubtitleVariant(variantProps)}
|
||||
className={dialogDescriptionVariant(variantProps)}
|
||||
id={ids.description}
|
||||
>
|
||||
{children}
|
||||
</h2>
|
||||
);
|
||||
});
|
||||
DialogDescription.displayName = "DialogDescription";
|
||||
|
||||
// renamed DialogSubtitle -> DialogDescription
|
||||
// keep DialogSubtitle for backward compatibility
|
||||
const DialogSubtitle = DialogDescription;
|
||||
|
||||
/**
|
||||
* =========================
|
||||
@ -357,17 +331,9 @@ const DialogSubtitle = React.forwardRef<
|
||||
*/
|
||||
|
||||
const [dialogFooterVariant, resolveDialogFooterVariant] = vcn({
|
||||
base: "flex flex-col items-end sm:flex-row sm:items-center sm:justify-end",
|
||||
variants: {
|
||||
gap: {
|
||||
sm: "gap-2",
|
||||
md: "gap-4",
|
||||
lg: "gap-6",
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
gap: "md",
|
||||
},
|
||||
base: "flex w-full flex-col items-end sm:flex-row sm:items-center sm:justify-end gap-2",
|
||||
variants: {},
|
||||
defaults: {},
|
||||
});
|
||||
|
||||
interface DialogFooterProps
|
||||
@ -380,16 +346,44 @@ const DialogFooter = React.forwardRef<HTMLDivElement, DialogFooterProps>(
|
||||
resolveDialogFooterVariant(props);
|
||||
const { children, ...otherPropsExtracted } = otherPropsCompressed;
|
||||
return (
|
||||
<div
|
||||
<footer
|
||||
{...otherPropsExtracted}
|
||||
ref={ref}
|
||||
className={dialogFooterVariant(variantProps)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
},
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
interface DialogControllers {
|
||||
context: IDialogContext;
|
||||
|
||||
setContext: React.Dispatch<React.SetStateAction<IDialogContext>>;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
interface DialogControllerProps {
|
||||
children: (controllers: DialogControllers) => ReactNode;
|
||||
}
|
||||
|
||||
const DialogController = (props: DialogControllerProps) => {
|
||||
return (
|
||||
<DialogContext.Consumer>
|
||||
{([context, setContext]) =>
|
||||
props.children({
|
||||
context,
|
||||
setContext,
|
||||
close() {
|
||||
setContext((p) => ({ ...p, opened: false }));
|
||||
},
|
||||
})
|
||||
}
|
||||
</DialogContext.Consumer>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
DialogRoot,
|
||||
@ -400,5 +394,7 @@ export {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogSubtitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogController,
|
||||
};
|
||||
|
@ -1,4 +1,9 @@
|
||||
import { Dispatch, SetStateAction, useContext, createContext } from "react";
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
createContext,
|
||||
useContext,
|
||||
} from "react";
|
||||
|
||||
/**
|
||||
* =========================
|
||||
@ -8,9 +13,17 @@ import { Dispatch, SetStateAction, useContext, createContext } from "react";
|
||||
|
||||
export interface IDialogContext {
|
||||
opened: boolean;
|
||||
ids: {
|
||||
dialog: string;
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const initialDialogContext: IDialogContext = { opened: false };
|
||||
export const initialDialogContext: IDialogContext = {
|
||||
opened: false,
|
||||
ids: { title: "", dialog: "", description: "" },
|
||||
};
|
||||
export const DialogContext = createContext<
|
||||
[IDialogContext, Dispatch<SetStateAction<IDialogContext>>]
|
||||
>([
|
||||
@ -25,3 +38,23 @@ export const DialogContext = createContext<
|
||||
]);
|
||||
|
||||
export const useDialogContext = () => useContext(DialogContext);
|
||||
|
||||
/**
|
||||
* ===================
|
||||
* InnerDialogContext
|
||||
* ===================
|
||||
*/
|
||||
|
||||
export interface IInnerDialogContext {
|
||||
isMounted: boolean;
|
||||
isRendered: boolean;
|
||||
}
|
||||
export const initialInnerDialogContext: IInnerDialogContext = {
|
||||
isMounted: false,
|
||||
isRendered: false,
|
||||
};
|
||||
export const InnerDialogContext = createContext<IInnerDialogContext>(
|
||||
initialInnerDialogContext,
|
||||
);
|
||||
|
||||
export const useInnerDialogContext = () => useContext(InnerDialogContext);
|
||||
|
@ -1,13 +1,20 @@
|
||||
import {
|
||||
type AsChild,
|
||||
Slot,
|
||||
type VariantProps,
|
||||
useAnimatedMount,
|
||||
useDocument,
|
||||
vcn,
|
||||
} from "@pswui-lib";
|
||||
import React, {
|
||||
ComponentPropsWithoutRef,
|
||||
TouchEvent as ReactTouchEvent,
|
||||
type ComponentPropsWithoutRef,
|
||||
type TouchEvent as ReactTouchEvent,
|
||||
forwardRef,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { AsChild, Slot, VariantProps, vcn } from "@pswui-lib";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
interface IDrawerContext {
|
||||
@ -15,6 +22,8 @@ interface IDrawerContext {
|
||||
closeThreshold: number;
|
||||
movePercentage: number;
|
||||
isDragging: boolean;
|
||||
isMounted: boolean;
|
||||
isRendered: boolean;
|
||||
leaveWhileDragging: boolean;
|
||||
}
|
||||
const DrawerContextInitial: IDrawerContext = {
|
||||
@ -22,6 +31,8 @@ const DrawerContextInitial: IDrawerContext = {
|
||||
closeThreshold: 0.3,
|
||||
movePercentage: 0,
|
||||
isDragging: false,
|
||||
isMounted: false,
|
||||
isRendered: false,
|
||||
leaveWhileDragging: false,
|
||||
};
|
||||
const DrawerContext = React.createContext<
|
||||
@ -57,8 +68,7 @@ const DrawerRoot = ({ children, closeThreshold, opened }: DrawerRootProps) => {
|
||||
opened: opened ?? prev.opened,
|
||||
closeThreshold: closeThreshold ?? prev.closeThreshold,
|
||||
}));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [closeThreshold, opened]);
|
||||
}, [closeThreshold, opened, setState]);
|
||||
|
||||
return (
|
||||
<DrawerContext.Provider value={state}>{children}</DrawerContext.Provider>
|
||||
@ -79,7 +89,7 @@ const [drawerOverlayVariant, resolveDrawerOverlayVariantProps] = vcn({
|
||||
base: "fixed inset-0 transition-[backdrop-filter] duration-75",
|
||||
variants: {
|
||||
opened: {
|
||||
true: "pointer-events-auto select-auto",
|
||||
true: "pointer-events-auto select-auto touch-none", // touch-none to prevent outside scrolling
|
||||
false: "pointer-events-none select-none",
|
||||
},
|
||||
},
|
||||
@ -97,8 +107,14 @@ interface DrawerOverlayProps
|
||||
|
||||
const DrawerOverlay = forwardRef<HTMLDivElement, DrawerOverlayProps>(
|
||||
(props, ref) => {
|
||||
const internalRef = useRef<HTMLDivElement | null>(null);
|
||||
const [state, setState] = useContext(DrawerContext);
|
||||
|
||||
const { isMounted, isRendered } = useAnimatedMount(
|
||||
state.isDragging ? true : state.opened,
|
||||
internalRef,
|
||||
);
|
||||
|
||||
const [variantProps, restPropsCompressed] =
|
||||
resolveDrawerOverlayVariantProps(props);
|
||||
const { asChild, ...restPropsExtracted } = restPropsCompressed;
|
||||
@ -120,12 +136,21 @@ const DrawerOverlay = forwardRef<HTMLDivElement, DrawerOverlayProps>(
|
||||
: 1
|
||||
})`;
|
||||
|
||||
return createPortal(
|
||||
const document = useDocument();
|
||||
if (!document) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DrawerContext.Provider
|
||||
value={[{ ...state, isMounted, isRendered }, setState]}
|
||||
>
|
||||
{isMounted
|
||||
? createPortal(
|
||||
<Comp
|
||||
{...restPropsExtracted}
|
||||
className={drawerOverlayVariant({
|
||||
...variantProps,
|
||||
opened: state.isDragging ? true : state.opened,
|
||||
opened: isRendered,
|
||||
})}
|
||||
onClick={onOutsideClick}
|
||||
style={{
|
||||
@ -133,12 +158,24 @@ const DrawerOverlay = forwardRef<HTMLDivElement, DrawerOverlayProps>(
|
||||
WebkitBackdropFilter: backdropFilter,
|
||||
transitionDuration: state.isDragging ? "0s" : undefined,
|
||||
}}
|
||||
ref={ref}
|
||||
ref={(el: HTMLDivElement) => {
|
||||
internalRef.current = el;
|
||||
if (typeof ref === "function") {
|
||||
ref(el);
|
||||
} else if (ref) {
|
||||
ref.current = el;
|
||||
}
|
||||
}}
|
||||
/>,
|
||||
document.body,
|
||||
)
|
||||
: null}
|
||||
</DrawerContext.Provider>
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
DrawerOverlay.displayName = "DrawerOverlay";
|
||||
|
||||
const drawerContentColors = {
|
||||
background: "bg-white dark:bg-black",
|
||||
@ -146,28 +183,55 @@ const drawerContentColors = {
|
||||
};
|
||||
|
||||
const [drawerContentVariant, resolveDrawerContentVariantProps] = vcn({
|
||||
base: `fixed ${drawerContentColors.background} ${drawerContentColors.border} transition-all p-4 flex flex-col justify-between gap-8`,
|
||||
base: `fixed ${drawerContentColors.background} ${drawerContentColors.border} transition-all p-4 flex flex-col justify-between gap-8 overflow-auto`,
|
||||
variants: {
|
||||
position: {
|
||||
top: "top-0 inset-x-0 w-full max-w-screen rounded-t-lg border-b-2",
|
||||
bottom: "bottom-0 inset-x-0 w-full max-w-screen rounded-b-lg border-t-2",
|
||||
left: "left-0 inset-y-0 h-screen rounded-l-lg border-r-2",
|
||||
right: "right-0 inset-y-0 h-screen rounded-r-lg border-l-2",
|
||||
top: "top-0 w-full max-w-screen rounded-t-lg border-b-2",
|
||||
bottom: "bottom-0 w-full max-w-screen rounded-b-lg border-t-2",
|
||||
left: "left-0 h-screen rounded-l-lg border-r-2",
|
||||
right: "right-0 h-screen rounded-r-lg border-l-2",
|
||||
},
|
||||
maxSize: {
|
||||
sm: "[&.left-0]:max-w-sm [&.right-0]:max-w-sm",
|
||||
md: "[&.left-0]:max-w-md [&.right-0]:max-w-md",
|
||||
lg: "[&.left-0]:max-w-lg [&.right-0]:max-w-lg",
|
||||
xl: "[&.left-0]:max-w-xl [&.right-0]:max-w-xl",
|
||||
},
|
||||
opened: {
|
||||
true: "touch-none",
|
||||
true: "",
|
||||
false:
|
||||
"[&.top-0]:-translate-y-full [&.bottom-0]:translate-y-full [&.left-0]:-translate-x-full [&.right-0]:translate-x-full",
|
||||
},
|
||||
internal: {
|
||||
true: "relative",
|
||||
false: "fixed",
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
position: "left",
|
||||
opened: false,
|
||||
maxSize: "sm",
|
||||
internal: false,
|
||||
},
|
||||
dynamics: [
|
||||
({ position, internal }) => {
|
||||
if (!internal) {
|
||||
if (["top", "bottom"].includes(position)) {
|
||||
return "inset-x-0";
|
||||
}
|
||||
return "inset-y-0";
|
||||
}
|
||||
|
||||
return "w-fit";
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
interface DrawerContentProps
|
||||
extends Omit<VariantProps<typeof drawerContentVariant>, "opened">,
|
||||
extends Omit<
|
||||
VariantProps<typeof drawerContentVariant>,
|
||||
"opened" | "internal"
|
||||
>,
|
||||
AsChild,
|
||||
ComponentPropsWithoutRef<"div"> {}
|
||||
|
||||
@ -302,16 +366,15 @@ const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
||||
window.removeEventListener("touchmove", onMouseMove);
|
||||
window.removeEventListener("touchend", onMouseUp);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [state, dragState, position]);
|
||||
}, [state, setState, dragState, position]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={drawerContentVariant({
|
||||
...variantProps,
|
||||
opened: true,
|
||||
opened: state.isRendered,
|
||||
className: dragState.isDragging
|
||||
? "transition-[width_0ms]"
|
||||
? "transition-[width] duration-0"
|
||||
: variantProps.className,
|
||||
})}
|
||||
style={
|
||||
@ -323,6 +386,7 @@ const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
||||
0) +
|
||||
(position === "top" ? dragState.delta : -dragState.delta),
|
||||
padding: 0,
|
||||
[`padding${position.slice(0, 1).toUpperCase()}${position.slice(1)}`]: `${dragState.delta}px`,
|
||||
}
|
||||
: {
|
||||
width:
|
||||
@ -330,6 +394,7 @@ const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
||||
0) +
|
||||
(position === "left" ? dragState.delta : -dragState.delta),
|
||||
padding: 0,
|
||||
[`padding${position.slice(0, 1).toUpperCase()}${position.slice(1)}`]: `${dragState.delta}px`,
|
||||
}
|
||||
: { width: 0, height: 0, padding: 0 }
|
||||
}
|
||||
@ -338,13 +403,15 @@ const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
||||
{...restPropsExtracted}
|
||||
className={drawerContentVariant({
|
||||
...variantProps,
|
||||
opened: state.opened,
|
||||
opened: state.isRendered,
|
||||
internal: true,
|
||||
})}
|
||||
style={{
|
||||
transform: dragState.isDragging
|
||||
? `translate${["top", "bottom"].includes(position) ? "Y" : "X"}(${
|
||||
dragState.delta
|
||||
}px)`
|
||||
transform:
|
||||
dragState.isDragging &&
|
||||
((["top", "left"].includes(position) && dragState.delta < 0) ||
|
||||
(["bottom", "right"].includes(position) && dragState.delta > 0))
|
||||
? `translate${["top", "bottom"].includes(position) ? "Y" : "X"}(${dragState.delta}px)`
|
||||
: undefined,
|
||||
transitionDuration: dragState.isDragging ? "0s" : undefined,
|
||||
userSelect: dragState.isDragging ? "none" : undefined,
|
||||
@ -376,6 +443,7 @@ const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
||||
);
|
||||
},
|
||||
);
|
||||
DrawerContent.displayName = "DrawerContent";
|
||||
|
||||
const DrawerClose = forwardRef<
|
||||
HTMLButtonElement,
|
||||
@ -390,6 +458,7 @@ const DrawerClose = forwardRef<
|
||||
/>
|
||||
);
|
||||
});
|
||||
DrawerClose.displayName = "DrawerClose";
|
||||
|
||||
const [drawerHeaderVariant, resolveDrawerHeaderVariantProps] = vcn({
|
||||
base: "flex flex-col gap-2",
|
||||
@ -419,9 +488,10 @@ const DrawerHeader = forwardRef<HTMLDivElement, DrawerHeaderProps>(
|
||||
);
|
||||
},
|
||||
);
|
||||
DrawerHeader.displayName = "DrawerHeader";
|
||||
|
||||
const [drawerBodyVariant, resolveDrawerBodyVariantProps] = vcn({
|
||||
base: "flex-grow",
|
||||
base: "grow",
|
||||
variants: {},
|
||||
defaults: {},
|
||||
});
|
||||
@ -446,6 +516,7 @@ const DrawerBody = forwardRef<HTMLDivElement, DrawerBodyProps>((props, ref) => {
|
||||
/>
|
||||
);
|
||||
});
|
||||
DrawerBody.displayName = "DrawerBody";
|
||||
|
||||
const [drawerFooterVariant, resolveDrawerFooterVariantProps] = vcn({
|
||||
base: "flex flex-row justify-end gap-2",
|
||||
@ -475,6 +546,7 @@ const DrawerFooter = forwardRef<HTMLDivElement, DrawerFooterProps>(
|
||||
);
|
||||
},
|
||||
);
|
||||
DrawerFooter.displayName = "DrawerFooter";
|
||||
|
||||
export {
|
||||
DrawerRoot,
|
||||
|
@ -0,0 +1,183 @@
|
||||
import { type AsChild, Slot, type VariantProps, vcn } from "@pswui-lib";
|
||||
import {
|
||||
type ComponentPropsWithoutRef,
|
||||
createContext,
|
||||
forwardRef,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
|
||||
/**
|
||||
Form Item Context
|
||||
**/
|
||||
interface IFormItemContext {
|
||||
invalid?: string | null | undefined;
|
||||
}
|
||||
const FormItemContext = createContext<IFormItemContext>({});
|
||||
|
||||
/**
|
||||
FormItem
|
||||
**/
|
||||
const [formItemVariant, resolveFormItemVariantProps] = vcn({
|
||||
base: "flex flex-col gap-2 items-start w-full",
|
||||
variants: {},
|
||||
defaults: {},
|
||||
});
|
||||
|
||||
interface FormItemProps
|
||||
extends VariantProps<typeof formItemVariant>,
|
||||
AsChild,
|
||||
ComponentPropsWithoutRef<"label"> {
|
||||
invalid?: string | null | undefined;
|
||||
}
|
||||
|
||||
const FormItem = forwardRef<HTMLLabelElement, FormItemProps>((props, ref) => {
|
||||
const [variantProps, restPropsCompressed] =
|
||||
resolveFormItemVariantProps(props);
|
||||
const { asChild, children, invalid, ...restPropsExtracted } =
|
||||
restPropsCompressed;
|
||||
const innerRef = useRef<HTMLLabelElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const invalidAsString = invalid ? invalid : "";
|
||||
|
||||
const input = innerRef.current?.querySelector?.("input");
|
||||
if (!input) return;
|
||||
|
||||
input.setCustomValidity(invalidAsString);
|
||||
}, [invalid]);
|
||||
|
||||
const Comp = asChild ? Slot : "label";
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ invalid }}>
|
||||
<Comp
|
||||
ref={(el: HTMLLabelElement | null) => {
|
||||
innerRef.current = el;
|
||||
if (typeof ref === "function") {
|
||||
ref(el);
|
||||
} else if (ref) {
|
||||
ref.current = el;
|
||||
}
|
||||
}}
|
||||
className={formItemVariant(variantProps)}
|
||||
{...restPropsExtracted}
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
});
|
||||
FormItem.displayName = "FormItem";
|
||||
|
||||
/**
|
||||
FormLabel
|
||||
**/
|
||||
const [formLabelVariant, resolveFormLabelVariantProps] = vcn({
|
||||
base: "text-sm font-bold",
|
||||
variants: {},
|
||||
defaults: {},
|
||||
});
|
||||
|
||||
interface FormLabelProps
|
||||
extends VariantProps<typeof formLabelVariant>,
|
||||
AsChild,
|
||||
ComponentPropsWithoutRef<"span"> {}
|
||||
|
||||
const FormLabel = forwardRef<HTMLSpanElement, FormLabelProps>((props, ref) => {
|
||||
const [variantProps, otherPropsCompressed] =
|
||||
resolveFormLabelVariantProps(props);
|
||||
const { children, asChild, ...otherPropsExtracted } = otherPropsCompressed;
|
||||
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={formLabelVariant(variantProps)}
|
||||
{...otherPropsExtracted}
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
);
|
||||
});
|
||||
FormLabel.displayName = "FormLabel";
|
||||
|
||||
/**
|
||||
FormHelper
|
||||
**/
|
||||
const [formHelperVariant, resolveFormHelperVariantProps] = vcn({
|
||||
base: "opacity-75 text-sm font-light",
|
||||
variants: {},
|
||||
defaults: {},
|
||||
});
|
||||
|
||||
interface FormHelperProps
|
||||
extends VariantProps<typeof formHelperVariant>,
|
||||
AsChild,
|
||||
ComponentPropsWithoutRef<"span"> {
|
||||
hiddenOnInvalid?: boolean;
|
||||
}
|
||||
|
||||
const FormHelper = forwardRef<HTMLSpanElement, FormHelperProps>(
|
||||
(props, ref) => {
|
||||
const [variantProps, otherPropsCompressed] =
|
||||
resolveFormHelperVariantProps(props);
|
||||
const { asChild, children, hiddenOnInvalid, ...otherPropsExtracted } =
|
||||
otherPropsCompressed;
|
||||
const item = useContext(FormItemContext);
|
||||
|
||||
if (item.invalid && hiddenOnInvalid) return null;
|
||||
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={formHelperVariant(variantProps)}
|
||||
{...otherPropsExtracted}
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
);
|
||||
},
|
||||
);
|
||||
FormHelper.displayName = "FormHelper";
|
||||
|
||||
/**
|
||||
FormError
|
||||
**/
|
||||
const [formErrorVariant, resolveFormErrorVariantProps] = vcn({
|
||||
base: "text-sm text-red-500",
|
||||
variants: {},
|
||||
defaults: {},
|
||||
});
|
||||
|
||||
interface FormErrorProps
|
||||
extends VariantProps<typeof formErrorVariant>,
|
||||
AsChild,
|
||||
Omit<ComponentPropsWithoutRef<"span">, "children"> {}
|
||||
|
||||
const FormError = forwardRef<HTMLSpanElement, FormErrorProps>((props, ref) => {
|
||||
const [variantProps, otherPropsCompressed] =
|
||||
resolveFormErrorVariantProps(props);
|
||||
const { asChild, ...otherPropsExtracted } = otherPropsCompressed;
|
||||
|
||||
const item = useContext(FormItemContext);
|
||||
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return item.invalid ? (
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={formErrorVariant(variantProps)}
|
||||
{...otherPropsExtracted}
|
||||
>
|
||||
{item.invalid}
|
||||
</Comp>
|
||||
) : null;
|
||||
});
|
||||
FormError.displayName = "FormError";
|
||||
|
||||
export { FormItem, FormLabel, FormHelper, FormError };
|
@ -1,37 +1,38 @@
|
||||
import { type AsChild, Slot, type VariantProps, vcn } from "@pswui-lib";
|
||||
import React from "react";
|
||||
import { VariantProps, vcn } from "@pswui-lib";
|
||||
|
||||
const inputColors = {
|
||||
background: {
|
||||
default: "bg-neutral-50 dark:bg-neutral-900",
|
||||
hover: "hover:bg-neutral-100 dark:hover:bg-neutral-800",
|
||||
hover:
|
||||
"hover:bg-neutral-100 dark:hover:bg-neutral-800 has-[input:hover]:bg-neutral-100 dark:has-[input:hover]:bg-neutral-800",
|
||||
invalid:
|
||||
"invalid:bg-red-100 invalid:dark:bg-red-900 has-[input:invalid]:bg-red-100 dark:has-[input:invalid]:bg-red-900",
|
||||
"invalid:bg-red-100 dark:invalid:bg-red-900 has-[input:invalid]:bg-red-100 dark:has-[input:invalid]:bg-red-900",
|
||||
invalidHover:
|
||||
"hover:invalid:bg-red-200 dark:hover:invalid:bg-red-800 has-[input:invalid:hover]:bg-red-200 dark:has-[input:invalid:hover]:bg-red-800",
|
||||
},
|
||||
border: {
|
||||
default: "border-neutral-400 dark:border-neutral-600",
|
||||
invalid:
|
||||
"invalid:border-red-400 invalid:dark:border-red-600 has-[input:invalid]:border-red-400 dark:has-[input:invalid]:border-red-600",
|
||||
"invalid:border-red-400 dark:invalid:border-red-600 has-[input:invalid]:border-red-400 dark:has-[input:invalid]:border-red-600",
|
||||
},
|
||||
ring: {
|
||||
default: "ring-transparent focus-within:ring-current",
|
||||
invalid:
|
||||
"invalid:focus-within:ring-red-400 invalid:focus-within:dark:ring-red-600 has-[input:invalid]:focus-within:ring-red-400 dark:has-[input:invalid]:focus-within:ring-red-600",
|
||||
"invalid:focus-within:ring-red-400 dark:invalid:focus-within:ring-red-600 has-[input:invalid]:focus-within:ring-red-400 dark:has-[input:invalid]:focus-within:ring-red-600",
|
||||
},
|
||||
};
|
||||
|
||||
const [inputVariant, resolveInputVariantProps] = vcn({
|
||||
base: `rounded-md p-2 border ring-1 outline-none transition-all duration-200 [appearance:textfield] disabled:brightness-50 disabled:saturate-0 disabled:cursor-not-allowed ${inputColors.background.default} ${inputColors.background.hover} ${inputColors.border.default} ${inputColors.ring.default} ${inputColors.background.invalid} ${inputColors.background.invalidHover} ${inputColors.border.invalid} ${inputColors.ring.invalid} [&:has(input)]:flex [&:has(input)]:w-fit`,
|
||||
base: `rounded-md p-2 border ring-1 outline-hidden transition-all duration-200 [appearance:textfield] disabled:brightness-50 disabled:saturate-0 disabled:cursor-not-allowed ${inputColors.background.default} ${inputColors.background.hover} ${inputColors.border.default} ${inputColors.ring.default} ${inputColors.background.invalid} ${inputColors.background.invalidHover} ${inputColors.border.invalid} ${inputColors.ring.invalid} [&:has(input)]:flex`,
|
||||
variants: {
|
||||
unstyled: {
|
||||
true: "bg-transparent border-none p-0 ring-0 hover:bg-transparent invalid:hover:bg-transparent invalid:focus-within:bg-transparent invalid:focus-within:ring-0",
|
||||
false: "",
|
||||
},
|
||||
full: {
|
||||
true: "w-full",
|
||||
false: "w-fit",
|
||||
true: "[&:has(input)]:w-full w-full",
|
||||
false: "[&:has(input)]:w-fit w-fit",
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
@ -42,7 +43,8 @@ const [inputVariant, resolveInputVariantProps] = vcn({
|
||||
|
||||
interface InputFrameProps
|
||||
extends VariantProps<typeof inputVariant>,
|
||||
React.ComponentPropsWithoutRef<"label"> {
|
||||
React.ComponentPropsWithoutRef<"label">,
|
||||
AsChild {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
@ -50,19 +52,22 @@ const InputFrame = React.forwardRef<HTMLLabelElement, InputFrameProps>(
|
||||
(props, ref) => {
|
||||
const [variantProps, otherPropsCompressed] =
|
||||
resolveInputVariantProps(props);
|
||||
const { children, ...otherPropsExtracted } = otherPropsCompressed;
|
||||
const { children, asChild, ...otherPropsExtracted } = otherPropsCompressed;
|
||||
|
||||
const Comp = asChild ? Slot : "label";
|
||||
|
||||
return (
|
||||
<label
|
||||
<Comp
|
||||
ref={ref}
|
||||
className={`group/input-frame ${inputVariant(variantProps)}`}
|
||||
{...otherPropsExtracted}
|
||||
>
|
||||
{children}
|
||||
</label>
|
||||
</Comp>
|
||||
);
|
||||
},
|
||||
);
|
||||
InputFrame.displayName = "InputFrame";
|
||||
|
||||
interface InputProps
|
||||
extends VariantProps<typeof inputVariant>,
|
||||
@ -92,7 +97,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
const innerRef = React.useRef<HTMLInputElement | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (innerRef && innerRef.current) {
|
||||
if (innerRef?.current) {
|
||||
innerRef.current.setCustomValidity(invalid ?? "");
|
||||
}
|
||||
}, [invalid]);
|
||||
@ -113,5 +118,6 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||
/>
|
||||
);
|
||||
});
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { InputFrame, Input };
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { type VariantProps, vcn } from "@pswui-lib";
|
||||
import React from "react";
|
||||
import { VariantProps, vcn } from "@pswui-lib";
|
||||
|
||||
const [labelVariant, resolveLabelVariantProps] = vcn({
|
||||
base: "has-[input[disabled]]:brightness-75 has-[input[disabled]]:cursor-not-allowed has-[input:invalid]:text-red-500",
|
||||
@ -29,5 +29,6 @@ const Label = React.forwardRef<HTMLLabelElement, LabelProps>((props, ref) => {
|
||||
/>
|
||||
);
|
||||
});
|
||||
Label.displayName = "Label";
|
||||
|
||||
export { Label };
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { type AsChild, Slot, type VariantProps, vcn } from "@pswui-lib";
|
||||
import React, { useContext, useEffect, useRef } from "react";
|
||||
import { AsChild, Slot, VariantProps, vcn } from "@pswui-lib";
|
||||
|
||||
interface IPopoverContext {
|
||||
controlled: boolean;
|
||||
opened: boolean;
|
||||
}
|
||||
|
||||
@ -10,6 +11,7 @@ const PopoverContext = React.createContext<
|
||||
>([
|
||||
{
|
||||
opened: false,
|
||||
controlled: false,
|
||||
},
|
||||
() => {
|
||||
if (process.env.NODE_ENV && process.env.NODE_ENV === "development") {
|
||||
@ -26,14 +28,23 @@ interface PopoverProps extends AsChild {
|
||||
}
|
||||
|
||||
const Popover = ({ children, opened, asChild }: PopoverProps) => {
|
||||
const state = React.useState<IPopoverContext>({
|
||||
const [state, setState] = React.useState<IPopoverContext>({
|
||||
opened: opened ?? false,
|
||||
controlled: opened !== undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setState((p) => ({
|
||||
...p,
|
||||
controlled: opened !== undefined,
|
||||
opened: opened !== undefined ? opened : p.opened,
|
||||
}));
|
||||
}, [opened]);
|
||||
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<PopoverContext.Provider value={state}>
|
||||
<PopoverContext.Provider value={[state, setState]}>
|
||||
<Comp className="relative">{children}</Comp>
|
||||
</PopoverContext.Provider>
|
||||
);
|
||||
@ -54,26 +65,25 @@ const popoverColors = {
|
||||
};
|
||||
|
||||
const [popoverContentVariant, resolvePopoverContentVariantProps] = vcn({
|
||||
base: `absolute transition-all duration-150 border rounded-lg p-0.5 [&>*]:w-full ${popoverColors.background} ${popoverColors.border}`,
|
||||
base: `absolute transition-all duration-150 border rounded-lg p-0.5 z-10 *:w-full ${popoverColors.background} ${popoverColors.border}`,
|
||||
variants: {
|
||||
direction: {
|
||||
row: "",
|
||||
col: "",
|
||||
},
|
||||
anchor: {
|
||||
topLeft:
|
||||
"bottom-[calc(100%+var(--popover-offset))] right-[calc(100%+var(--popover-offset))] origin-bottom-right",
|
||||
topCenter:
|
||||
"bottom-[calc(100%+var(--popover-offset))] left-1/2 -translate-x-1/2 origin-bottom-center",
|
||||
topRight:
|
||||
"bottom-[calc(100%+var(--popover-offset))] left-[calc(100%+var(--popover-offset))] origin-bottom-left",
|
||||
middleLeft: "top-1/2 translate-y-1/2 right-full origin-right",
|
||||
middleCenter:
|
||||
"top-1/2 translate-y-1/2 left-1/2 -translate-x-1/2 origin-center",
|
||||
middleRight:
|
||||
"top-1/2 translate-y-1/2 left-[calc(100%+var(--popover-offset))] origin-left",
|
||||
bottomLeft:
|
||||
"top-[calc(100%+var(--popover-offset))] right-[calc(100%+var(--popover-offset))] origin-top-right",
|
||||
bottomCenter:
|
||||
"top-[calc(100%+var(--popover-offset))] left-1/2 -translate-x-1/2 origin-top-center",
|
||||
bottomRight:
|
||||
"top-[calc(100%+var(--popover-offset))] left-[calc(100%+var(--popover-offset))] origin-top-left",
|
||||
start: "",
|
||||
middle: "",
|
||||
end: "",
|
||||
},
|
||||
align: {
|
||||
start: "",
|
||||
middle: "",
|
||||
end: "",
|
||||
},
|
||||
position: {
|
||||
start: "",
|
||||
end: "",
|
||||
},
|
||||
offset: {
|
||||
sm: "[--popover-offset:2px]",
|
||||
@ -81,15 +91,104 @@ const [popoverContentVariant, resolvePopoverContentVariantProps] = vcn({
|
||||
lg: "[--popover-offset:8px]",
|
||||
},
|
||||
opened: {
|
||||
true: "opacity-1 scale-100",
|
||||
false: "opacity-0 scale-75",
|
||||
true: "opacity-1 scale-100 pointer-events-auto select-auto touch-auto",
|
||||
false: "opacity-0 scale-75 pointer-events-none select-none touch-none",
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
anchor: "bottomCenter",
|
||||
direction: "col",
|
||||
anchor: "middle",
|
||||
align: "middle",
|
||||
position: "end",
|
||||
opened: false,
|
||||
offset: "md",
|
||||
},
|
||||
dynamics: [
|
||||
function originClass({ direction, anchor, position }) {
|
||||
switch (`${direction} ${position} ${anchor}` as const) {
|
||||
// left
|
||||
case "row start start":
|
||||
return "origin-top-right";
|
||||
case "row start middle":
|
||||
return "origin-right";
|
||||
case "row start end":
|
||||
return "origin-bottom-right";
|
||||
// right
|
||||
case "row end start":
|
||||
return "origin-top-left";
|
||||
case "row end middle":
|
||||
return "origin-left";
|
||||
case "row end end":
|
||||
return "origin-bottom-left";
|
||||
// top
|
||||
case "col start start":
|
||||
return "origin-bottom-left";
|
||||
case "col start middle":
|
||||
return "origin-bottom";
|
||||
case "col start end":
|
||||
return "origin-bottom-right";
|
||||
// bottom
|
||||
case "col end start":
|
||||
return "origin-top-left";
|
||||
case "col end middle":
|
||||
return "origin-top";
|
||||
case "col end end":
|
||||
return "origin-top-right";
|
||||
}
|
||||
},
|
||||
function basePositionClass({ position, direction }) {
|
||||
switch (`${direction} ${position}` as const) {
|
||||
case "col start":
|
||||
return "bottom-[calc(100%+var(--popover-offset))]";
|
||||
case "col end":
|
||||
return "top-[calc(100%+var(--popover-offset))]";
|
||||
case "row start":
|
||||
return "right-[calc(100%+var(--popover-offset))]";
|
||||
case "row end":
|
||||
return "left-[calc(100%+var(--popover-offset))]";
|
||||
}
|
||||
},
|
||||
function directionPositionClass({ direction, anchor, align }) {
|
||||
switch (`${direction} ${anchor} ${align}` as const) {
|
||||
case "col start start":
|
||||
return "left-0";
|
||||
case "col start middle":
|
||||
return "left-1/2";
|
||||
case "col start end":
|
||||
return "left-full";
|
||||
case "col middle start":
|
||||
return "left-0 -translate-x-1/2";
|
||||
case "col middle middle":
|
||||
return "left-1/2 -translate-x-1/2";
|
||||
case "col middle end":
|
||||
return "right-0 translate-x-1/2";
|
||||
case "col end start":
|
||||
return "right-full";
|
||||
case "col end middle":
|
||||
return "right-1/2";
|
||||
case "col end end":
|
||||
return "right-0";
|
||||
case "row start start":
|
||||
return "top-0";
|
||||
case "row start middle":
|
||||
return "top-1/2";
|
||||
case "row start end":
|
||||
return "top-full";
|
||||
case "row middle start":
|
||||
return "top-0 -translate-y-1/2";
|
||||
case "row middle middle":
|
||||
return "top-1/2 -translate-y-1/2";
|
||||
case "row middle end":
|
||||
return "bottom-0 translate-y-1/2";
|
||||
case "row end start":
|
||||
return "bottom-full";
|
||||
case "row end middle":
|
||||
return "bottom-1/2";
|
||||
case "row end end":
|
||||
return "bottom-0";
|
||||
}
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
interface PopoverContentProps
|
||||
@ -101,7 +200,7 @@ const PopoverContent = React.forwardRef<HTMLDivElement, PopoverContentProps>(
|
||||
(props, ref) => {
|
||||
const [variantProps, otherPropsCompressed] =
|
||||
resolvePopoverContentVariantProps(props);
|
||||
const { children, ...otherPropsExtracted } = otherPropsCompressed;
|
||||
const { children, asChild, ...otherPropsExtracted } = otherPropsCompressed;
|
||||
const [state, setState] = useContext(PopoverContext);
|
||||
|
||||
const internalRef = useRef<HTMLDivElement | null>(null);
|
||||
@ -115,28 +214,36 @@ const PopoverContent = React.forwardRef<HTMLDivElement, PopoverContentProps>(
|
||||
setState((prev) => ({ ...prev, opened: false }));
|
||||
}
|
||||
}
|
||||
!state.controlled &&
|
||||
document.addEventListener("mousedown", handleOutsideClick);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleOutsideClick);
|
||||
};
|
||||
}, [internalRef, setState]);
|
||||
}, [state.controlled, setState]);
|
||||
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<div
|
||||
<Comp
|
||||
{...otherPropsExtracted}
|
||||
className={popoverContentVariant({
|
||||
...variantProps,
|
||||
opened: state.opened,
|
||||
})}
|
||||
ref={(el) => {
|
||||
ref={(el: HTMLDivElement) => {
|
||||
internalRef.current = el;
|
||||
typeof ref === "function" ? ref(el) : ref && (ref.current = el);
|
||||
if (typeof ref === "function") {
|
||||
ref(el);
|
||||
} else if (ref) {
|
||||
ref.current = el;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</Comp>
|
||||
);
|
||||
},
|
||||
);
|
||||
PopoverContent.displayName = "PopoverContent";
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent };
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { type VariantProps, vcn } from "@pswui-lib";
|
||||
import React from "react";
|
||||
import { VariantProps, vcn } from "@pswui-lib";
|
||||
|
||||
const switchColors = {
|
||||
background: {
|
||||
@ -80,5 +80,6 @@ const Switch = React.forwardRef<HTMLInputElement, SwitchProps>((props, ref) => {
|
||||
</label>
|
||||
);
|
||||
});
|
||||
Switch.displayName = "Switch";
|
||||
|
||||
export { Switch };
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { AsChild, Slot, VariantProps, vcn } from "@pswui-lib";
|
||||
import { type AsChild, Slot, type VariantProps, vcn } from "@pswui-lib";
|
||||
import React from "react";
|
||||
|
||||
import { TabContextBody, TabContext, Tab } from "./Context";
|
||||
import { type Tab, TabContext, type TabContextBody } from "./Context";
|
||||
|
||||
interface TabProviderProps {
|
||||
defaultName: string;
|
||||
@ -30,11 +30,16 @@ interface TabListProps
|
||||
const TabList = (props: TabListProps) => {
|
||||
const [variantProps, restProps] = resolveTabListVariantProps(props);
|
||||
|
||||
return <div className={TabListVariant(variantProps)} {...restProps} />;
|
||||
return (
|
||||
<div
|
||||
className={TabListVariant(variantProps)}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const [TabTriggerVariant, resolveTabTriggerVariantProps] = vcn({
|
||||
base: "py-1.5 rounded-md flex-grow transition-all text-sm",
|
||||
base: "py-1.5 rounded-md grow transition-all text-sm",
|
||||
variants: {
|
||||
active: {
|
||||
true: "bg-white/100 dark:bg-black/100 text-black dark:text-white",
|
||||
@ -76,7 +81,7 @@ const TabTrigger = (props: TabTriggerProps) => {
|
||||
});
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [name]);
|
||||
}, [name, setContext]);
|
||||
|
||||
const Comp = props.asChild ? Slot : "button";
|
||||
|
||||
@ -119,7 +124,10 @@ const TabContent = (props: TabContentProps) => {
|
||||
const { name, ...restProps } = restPropsBeforeParse;
|
||||
const [context] = React.useContext(TabContext);
|
||||
|
||||
if (context.active[1] === name) {
|
||||
if (context.active[1] !== name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Slot
|
||||
className={tabContentVariant({
|
||||
@ -128,9 +136,6 @@ const TabContent = (props: TabContentProps) => {
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export { TabProvider, TabList, TabTrigger, TabContent };
|
||||
|
@ -1,20 +1,25 @@
|
||||
import React, { useEffect, useId, useRef } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { VariantProps, vcn } from "@pswui-lib";
|
||||
|
||||
import { toastVariant } from "./Variant";
|
||||
import {
|
||||
ToastOption,
|
||||
toasts,
|
||||
subscribeSingle,
|
||||
getSingleSnapshot,
|
||||
notifySingle,
|
||||
type VariantProps,
|
||||
getCalculatedTransitionDuration,
|
||||
useDocument,
|
||||
vcn,
|
||||
} from "@pswui-lib";
|
||||
import React, { type MutableRefObject, useEffect, useId, useRef } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import {
|
||||
type ToastOption,
|
||||
close,
|
||||
notify,
|
||||
defaultToastOption,
|
||||
subscribe,
|
||||
getSingleSnapshot,
|
||||
getSnapshot,
|
||||
notify,
|
||||
notifySingle,
|
||||
subscribe,
|
||||
subscribeSingle,
|
||||
toasts,
|
||||
} from "./Store";
|
||||
import { toastVariant } from "./Variant";
|
||||
|
||||
const ToastTemplate = ({
|
||||
id,
|
||||
@ -41,9 +46,16 @@ const ToastTemplate = ({
|
||||
|
||||
React.useEffect(() => {
|
||||
if (toastData.life === "born") {
|
||||
requestAnimationFrame(() => {
|
||||
// To make sure that the toast is rendered as "born" state
|
||||
// and then change to "normal" state
|
||||
requestAnimationFrame(function untilBorn() {
|
||||
/*
|
||||
To confirm that the toast is rendered as "born" state and then change to "normal" state
|
||||
This way will make sure born -> normal stage transition animation will work.
|
||||
*/
|
||||
const elm = document.querySelector(
|
||||
`div[data-toaster-root] > div[data-toast-id="${id}"][data-toast-lifecycle="born"]`,
|
||||
);
|
||||
if (!elm) return requestAnimationFrame(untilBorn);
|
||||
|
||||
toasts[id] = {
|
||||
...toasts[id],
|
||||
life: "normal",
|
||||
@ -58,45 +70,18 @@ const ToastTemplate = ({
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
if (toastData.life === "dead") {
|
||||
let transitionDuration: {
|
||||
value: number;
|
||||
unit: string;
|
||||
} | null;
|
||||
if (!ref.current) {
|
||||
transitionDuration = null;
|
||||
} else if (ref.current.computedStyleMap !== undefined) {
|
||||
transitionDuration = ref.current
|
||||
.computedStyleMap()
|
||||
.get("transition-duration") as { value: number; unit: string };
|
||||
} else {
|
||||
const style = /(\d+(\.\d+)?)(.+)/.exec(
|
||||
window.getComputedStyle(ref.current).transitionDuration,
|
||||
let calculatedTransitionDurationMs = 1;
|
||||
if (ref.current)
|
||||
calculatedTransitionDurationMs = getCalculatedTransitionDuration(
|
||||
ref as MutableRefObject<HTMLDivElement>,
|
||||
);
|
||||
transitionDuration = style
|
||||
? {
|
||||
value: parseFloat(style[1] ?? "0"),
|
||||
unit: style[3] ?? style[2] ?? "s",
|
||||
}
|
||||
: null;
|
||||
}
|
||||
if (!transitionDuration) {
|
||||
delete toasts[id];
|
||||
notify();
|
||||
return;
|
||||
}
|
||||
const calculatedTransitionDuration =
|
||||
transitionDuration.value *
|
||||
({
|
||||
s: 1000,
|
||||
ms: 1,
|
||||
}[transitionDuration.unit] ?? 1);
|
||||
const timeout = setTimeout(() => {
|
||||
delete toasts[id];
|
||||
notify();
|
||||
}, calculatedTransitionDuration);
|
||||
}, calculatedTransitionDurationMs);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [id, toastData.life, toastData.closeTimeout, toastData.closeButton]);
|
||||
}, [id, toastData.life, toastData.closeTimeout]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -105,15 +90,22 @@ const ToastTemplate = ({
|
||||
life: toastData.life,
|
||||
})}
|
||||
ref={ref}
|
||||
data-toast-id={id}
|
||||
data-toast-lifecycle={toastData.life}
|
||||
>
|
||||
{toastData.closeButton && (
|
||||
<button className="absolute top-2 right-2" onClick={() => close(id)}>
|
||||
<button
|
||||
className="absolute top-2 right-2"
|
||||
onClick={() => close(id)}
|
||||
type={"button"}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2rem"
|
||||
height="1.2rem"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<title>Close</title>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12z"
|
||||
@ -163,11 +155,15 @@ const Toaster = React.forwardRef<HTMLDivElement, ToasterProps>((props, ref) => {
|
||||
};
|
||||
}, [defaultOption]);
|
||||
|
||||
const document = useDocument();
|
||||
|
||||
if (!document) return null;
|
||||
|
||||
const toasterInstance = document.querySelector("div[data-toaster-root]");
|
||||
if (toasterInstance && id !== toasterInstance.id) {
|
||||
if (process.env.NODE_ENV === "development" && !muteDuplicationWarning) {
|
||||
console.warn(
|
||||
`Multiple Toaster instances detected. Only one Toaster is allowed.`,
|
||||
"Multiple Toaster instances detected. Only one Toaster is allowed.",
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@ -203,5 +199,6 @@ const Toaster = React.forwardRef<HTMLDivElement, ToasterProps>((props, ref) => {
|
||||
</>
|
||||
);
|
||||
});
|
||||
Toaster.displayName = "Toaster";
|
||||
|
||||
export { Toaster };
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { addToast, update, close } from "./Store";
|
||||
import { addToast, close, update } from "./Store";
|
||||
|
||||
export function useToast() {
|
||||
return {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ToastBody } from "./Variant";
|
||||
import type { ToastBody } from "./Variant";
|
||||
|
||||
export interface ToastOption {
|
||||
closeButton: boolean;
|
||||
@ -54,11 +54,11 @@ export function getSingleSnapshot(id: `${number}`) {
|
||||
}
|
||||
|
||||
export function notify() {
|
||||
subscribers.forEach((subscriber) => subscriber());
|
||||
for (const subscriber of subscribers) subscriber();
|
||||
}
|
||||
|
||||
export function notifySingle(id: `${number}`) {
|
||||
toasts[id].subscribers.forEach((subscriber) => subscriber());
|
||||
for (const subscriber of toasts[id].subscribers) subscriber();
|
||||
}
|
||||
|
||||
export function close(id: `${number}`) {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { VariantProps, vcn } from "@pswui-lib";
|
||||
import { type VariantProps, vcn } from "@pswui-lib";
|
||||
|
||||
const toastColors = {
|
||||
background: "bg-white dark:bg-black",
|
||||
|
@ -0,0 +1,2 @@
|
||||
export { Toaster } from "./Component";
|
||||
export { useToast } from "./Hook";
|
@ -1,5 +1,5 @@
|
||||
import { type AsChild, Slot, type VariantProps, vcn } from "@pswui-lib";
|
||||
import React, { useState } from "react";
|
||||
import { AsChild, Slot, VariantProps, vcn } from "@pswui-lib";
|
||||
|
||||
interface TooltipContextBody {
|
||||
position: "top" | "bottom" | "left" | "right";
|
||||
@ -71,6 +71,7 @@ const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>((props, ref) => {
|
||||
</TooltipContext.Provider>
|
||||
);
|
||||
});
|
||||
Tooltip.displayName = "Tooltip";
|
||||
|
||||
const tooltipContentColors = {
|
||||
variants: {
|
||||
@ -139,10 +140,12 @@ const TooltipContent = React.forwardRef<HTMLDivElement, TooltipContentProps>(
|
||||
...variantProps,
|
||||
position: contextState.position,
|
||||
})}
|
||||
role="tooltip"
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
TooltipContent.displayName = "TooltipContent";
|
||||
|
||||
export { Tooltip, TooltipContent };
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import React from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
/**
|
||||
* Merges the react props.
|
||||
@ -60,14 +60,15 @@ function mergeReactProps(
|
||||
* @returns The single ref.
|
||||
*/
|
||||
function combinedRef<I>(refs: React.Ref<I | null>[]) {
|
||||
return (instance: I | null) =>
|
||||
refs.forEach((ref) => {
|
||||
return (instance: I | null) => {
|
||||
for (const ref of refs) {
|
||||
if (ref instanceof Function) {
|
||||
ref(instance);
|
||||
} else if (ref) {
|
||||
(ref as React.MutableRefObject<I | null>).current = instance;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
interface SlotProps {
|
||||
@ -84,7 +85,10 @@ export const Slot = React.forwardRef<
|
||||
return null;
|
||||
}
|
||||
return React.cloneElement(children, {
|
||||
...mergeReactProps(safeSlotProps, children.props),
|
||||
...mergeReactProps(
|
||||
safeSlotProps,
|
||||
children.props as Record<string, unknown>,
|
||||
),
|
||||
ref: combinedRef([
|
||||
ref,
|
||||
(children as unknown as { ref: React.Ref<HTMLElement> }).ref,
|
||||
|
@ -1,2 +1,4 @@
|
||||
export * from "./vcn";
|
||||
export * from "./Slot";
|
||||
export * from "./useDocument";
|
||||
export * from "./useAnimatedMount";
|
||||
|
85
packages/react/lib/useAnimatedMount.ts
Normal file
85
packages/react/lib/useAnimatedMount.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { type MutableRefObject, useCallback, useEffect, useState } from "react";
|
||||
|
||||
function getCalculatedTransitionDuration(
|
||||
ref: MutableRefObject<HTMLElement>,
|
||||
): number {
|
||||
let transitionDuration: {
|
||||
value: number;
|
||||
unit: string;
|
||||
} | null;
|
||||
if (ref.current.computedStyleMap !== undefined) {
|
||||
transitionDuration = ref.current
|
||||
.computedStyleMap()
|
||||
.get("transition-duration") as { value: number; unit: string };
|
||||
} else {
|
||||
const style = /(\d+(\.\d+)?)(.+)/.exec(
|
||||
window.getComputedStyle(ref.current).transitionDuration,
|
||||
);
|
||||
if (!style) return 0;
|
||||
transitionDuration = {
|
||||
value: Number.parseFloat(style[1] ?? "0"),
|
||||
unit: style[3] ?? style[2] ?? "s",
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
transitionDuration.value *
|
||||
({
|
||||
s: 1000,
|
||||
ms: 1,
|
||||
}[transitionDuration.unit] ?? 1)
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* isMounted: true isRendered: true isRendered: false isMounted: false
|
||||
* Component Mount Component Appear Component Disappear Component Unmount
|
||||
* v v v v
|
||||
* |-|=================|------------------------|======================|-|
|
||||
*/
|
||||
|
||||
function useAnimatedMount(
|
||||
visible: boolean,
|
||||
ref: MutableRefObject<HTMLElement | null>,
|
||||
callbacks?: { onMount: () => void; onUnmount: () => void },
|
||||
) {
|
||||
const [state, setState] = useState<{
|
||||
isMounted: boolean;
|
||||
isRendered: boolean;
|
||||
}>({ isMounted: visible, isRendered: visible });
|
||||
|
||||
const umountCallback = useCallback(() => {
|
||||
setState((p) => ({ ...p, isRendered: false }));
|
||||
|
||||
const calculatedTransitionDuration = ref.current
|
||||
? getCalculatedTransitionDuration(ref as MutableRefObject<HTMLElement>)
|
||||
: 0;
|
||||
|
||||
setTimeout(() => {
|
||||
setState((p) => ({ ...p, isMounted: false }));
|
||||
callbacks?.onUnmount?.();
|
||||
}, calculatedTransitionDuration);
|
||||
}, [ref, callbacks]);
|
||||
|
||||
const mountCallback = useCallback(() => {
|
||||
setState((p) => ({ ...p, isMounted: true }));
|
||||
callbacks?.onMount?.();
|
||||
requestAnimationFrame(function onMount() {
|
||||
if (!ref.current) return requestAnimationFrame(onMount);
|
||||
setState((p) => ({ ...p, isRendered: true }));
|
||||
});
|
||||
}, [ref.current, callbacks]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log(state);
|
||||
if (!visible && state.isRendered) {
|
||||
umountCallback();
|
||||
} else if (visible && !state.isMounted) {
|
||||
mountCallback();
|
||||
}
|
||||
}, [state, visible, mountCallback, umountCallback]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
export { getCalculatedTransitionDuration, useAnimatedMount };
|
21
packages/react/lib/useDocument.ts
Normal file
21
packages/react/lib/useDocument.ts
Normal file
@ -0,0 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* This hook allows components to use `document` as like they're always in the client side.
|
||||
* Return undefined if there is no `document` (which represents it's server side) or initial render(to avoid hydration error).
|
||||
*/
|
||||
function useDocument(): undefined | Document {
|
||||
const [initialRender, setInitialState] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
setInitialState(false);
|
||||
}, []);
|
||||
|
||||
if (typeof document === "undefined" || initialRender) return undefined;
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
export { useDocument };
|
@ -57,6 +57,31 @@ type VariantKV<V extends VariantType> = {
|
||||
[VariantKey in keyof V]: BooleanString<keyof V[VariantKey] & string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Used for safely casting `Object.entries(<VariantKV>)`
|
||||
*/
|
||||
type VariantKVEntry<V extends VariantType> = [
|
||||
keyof V,
|
||||
BooleanString<keyof V[keyof V] & string>,
|
||||
][];
|
||||
|
||||
/**
|
||||
* Takes VariantKV as parameter, return className string.
|
||||
*
|
||||
* @example
|
||||
* vcn({
|
||||
* /* ... *\/
|
||||
* dynamics: [
|
||||
* ({ a, b }) => {
|
||||
* return a === "something" ? "asdf" : b
|
||||
* },
|
||||
* ]
|
||||
* })
|
||||
*/
|
||||
type DynamicClassName<V extends VariantType> = (
|
||||
variantProps: VariantKV<V>,
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* Takes VariantType, and returns a type that represents the preset object.
|
||||
*
|
||||
@ -94,6 +119,7 @@ export function vcn<V extends VariantType>(param: {
|
||||
*/
|
||||
base?: string | undefined;
|
||||
variants: V;
|
||||
dynamics?: DynamicClassName<V>[];
|
||||
defaults: VariantKV<V>;
|
||||
presets?: undefined;
|
||||
}): [
|
||||
@ -108,7 +134,7 @@ export function vcn<V extends VariantType>(param: {
|
||||
/**
|
||||
* Any Props -> Variant Props, Other Props
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// biome-ignore lint/suspicious/noExplicitAny: using unknown causes error `Index signature for type 'string' is missing in type --Props`.
|
||||
<AnyPropBeforeResolve extends Record<string, any>>(
|
||||
anyProps: AnyPropBeforeResolve,
|
||||
) => [
|
||||
@ -124,6 +150,7 @@ export function vcn<V extends VariantType, P extends PresetType<V>>(param: {
|
||||
*/
|
||||
base?: string | undefined;
|
||||
variants: V /* VariantType */;
|
||||
dynamics?: DynamicClassName<V>[];
|
||||
defaults: VariantKV<V>;
|
||||
presets: P;
|
||||
}): [
|
||||
@ -139,7 +166,7 @@ export function vcn<V extends VariantType, P extends PresetType<V>>(param: {
|
||||
/**
|
||||
* Any Props -> Variant Props, Other Props
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
// biome-ignore lint/suspicious/noExplicitAny: using unknown causes error `Index signature for type 'string' is missing in type --Props`.
|
||||
<AnyPropBeforeResolve extends Record<string, any>>(
|
||||
anyProps: AnyPropBeforeResolve,
|
||||
) => [
|
||||
@ -159,19 +186,58 @@ export function vcn<
|
||||
>({
|
||||
base,
|
||||
variants,
|
||||
dynamics = [],
|
||||
defaults,
|
||||
presets,
|
||||
}: {
|
||||
base?: string | undefined;
|
||||
variants: V;
|
||||
dynamics?: DynamicClassName<V>[];
|
||||
defaults: VariantKV<V>;
|
||||
presets?: P;
|
||||
}) {
|
||||
/**
|
||||
* --Internal utility function--
|
||||
* After transforming props to final version (which means "after overriding default, preset, and variant props sent via component props")
|
||||
* It turns final version of variant props to className
|
||||
*/
|
||||
function __transformer__(
|
||||
final: VariantKV<V>,
|
||||
dynamics: string[],
|
||||
propClassName?: string,
|
||||
): string {
|
||||
const classNames: string[] = [];
|
||||
|
||||
for (const [variantName, variantKey] of Object.entries(
|
||||
final,
|
||||
) as VariantKVEntry<V>) {
|
||||
classNames.push(variants[variantName][variantKey.toString()]);
|
||||
}
|
||||
|
||||
return twMerge(base, ...classNames, ...dynamics, propClassName);
|
||||
}
|
||||
|
||||
return [
|
||||
/**
|
||||
* Takes any props (including className), and returns the class name.
|
||||
* If there is no variant specified in props, then it will fallback to preset, and then default.
|
||||
*
|
||||
*
|
||||
* Process priority of variant will be:
|
||||
*
|
||||
* --- Processed as string
|
||||
* 1. Base
|
||||
*
|
||||
* --- Processed as object (it will ignore rest of "not duplicated classname" in lower priority)
|
||||
* 2. Default
|
||||
* 3. Preset (overriding default)
|
||||
* 4. Variant props via component (overriding preset)
|
||||
*
|
||||
* --- Processed as string
|
||||
* 5. Dynamic classNames using variant props
|
||||
* 6. User's className (overriding dynamic)
|
||||
*
|
||||
*
|
||||
* @param variantProps - The variant props including className.
|
||||
* @returns The class name.
|
||||
*/
|
||||
@ -180,42 +246,43 @@ export function vcn<
|
||||
VariantKV<V>
|
||||
>,
|
||||
) => {
|
||||
const { className, preset, ...otherVariantProps } = variantProps;
|
||||
const { className, preset, ..._otherVariantProps } = variantProps;
|
||||
|
||||
const currentPreset: P[keyof P] | null =
|
||||
presets && preset ? (presets as NonNullable<P>)[preset] ?? null : null;
|
||||
const presetVariantKeys: (keyof V)[] = Object.keys(currentPreset ?? {});
|
||||
return twMerge(
|
||||
base,
|
||||
...(
|
||||
Object.entries(defaults) as [keyof V, keyof V[keyof V] & string][]
|
||||
).map<string>(([variantKey, defaultValue]) => {
|
||||
// Omit<Partial<VariantKV<V>> & { className; preset; }, className | preset> = Partial<VariantKV<V>> (safe to cast)
|
||||
// Partial<VariantKV<V>>[keyof V] => { [k in keyof V]?: BooleanString<keyof V[keyof V] & string> } => BooleanString<keyof V[keyof V]>
|
||||
// We all know `keyof V` = string, right? (but typescript says it's not, so.. attacking typescript with unknown lol)
|
||||
const otherVariantProps = _otherVariantProps as unknown as Partial<
|
||||
VariantKV<V>
|
||||
>;
|
||||
|
||||
const directVariantValue: (keyof V[keyof V] & string) | undefined = (
|
||||
otherVariantProps as unknown as Partial<VariantKV<V>>
|
||||
)[variantKey]?.toString?.(); // BooleanString<> -> string (safe to index V[keyof V])
|
||||
const kv: VariantKV<V> = { ...defaults };
|
||||
|
||||
const currentPresetVariantValue:
|
||||
| (keyof V[keyof V] & string)
|
||||
| undefined =
|
||||
!!currentPreset && presetVariantKeys.includes(variantKey)
|
||||
? (currentPreset as Partial<VariantKV<V>>)[
|
||||
variantKey
|
||||
]?.toString?.()
|
||||
: undefined;
|
||||
// Preset Processing
|
||||
if (presets && preset && preset in presets) {
|
||||
for (const [variantName, variantKey] of Object.entries(
|
||||
// typescript bug (casting to NonNullable<P> required)
|
||||
(presets as NonNullable<P>)[preset],
|
||||
) as VariantKVEntry<V>) {
|
||||
kv[variantName] = variantKey;
|
||||
}
|
||||
}
|
||||
|
||||
const variantValue: keyof V[keyof V] & string =
|
||||
directVariantValue ?? currentPresetVariantValue ?? defaultValue;
|
||||
return variants[variantKey][variantValue];
|
||||
}),
|
||||
(
|
||||
currentPreset as Partial<VariantKV<V>> | null
|
||||
)?.className?.toString?.(), // preset's classname comes after user's variant props? huh..
|
||||
className,
|
||||
);
|
||||
// VariantProps Processing
|
||||
for (const [variantName, variantKey] of Object.entries(
|
||||
otherVariantProps,
|
||||
) as VariantKVEntry<V>) {
|
||||
if (typeof variantKey === "undefined") continue;
|
||||
kv[variantName] = variantKey;
|
||||
}
|
||||
|
||||
// make dynamics result
|
||||
const dynamicClasses: string[] = [];
|
||||
for (const dynamicFunction of dynamics) {
|
||||
dynamicClasses.push(dynamicFunction(kv));
|
||||
}
|
||||
|
||||
return __transformer__(kv, dynamicClasses, className);
|
||||
},
|
||||
|
||||
/**
|
||||
* Takes any props, parse variant props and other props.
|
||||
* If `options.excludeA` is true, then it will parse `A` as "other" props.
|
||||
|
@ -6,28 +6,21 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build && cp ./404.html ./dist",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"lint": "biome check --no-errors-on-unmatched",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.0.12",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"tailwind-merge": "^2.3.0"
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss": "^4.0.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.13",
|
||||
"@types/react": "^18.2.66",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"postcss": "^8.4.38",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
|
@ -1,7 +0,0 @@
|
||||
import "./tailwind.css";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode></React.StrictMode>,
|
||||
);
|
@ -1,7 +1,5 @@
|
||||
@import tailwindcss;
|
||||
@import url("https://cdn.jsdelivr.net/gh/wanteddev/wanted-sans@v1.0.3/packages/wanted-sans/fonts/webfonts/variable/split/WantedSansVariable.min.css");
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
@ -34,4 +32,3 @@
|
||||
@apply transition-colors;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,14 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./{components,stories,src}/**/*.{js,jsx,ts,tsx,css,mdx}"],
|
||||
darkMode: [
|
||||
"variant",
|
||||
[
|
||||
"@media (prefers-color-scheme: dark) { &:is(.system *) }",
|
||||
"&:is(.dark *)",
|
||||
],
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
};
|
@ -1,21 +1,16 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import tailwindcss from "tailwindcss";
|
||||
import { resolve } from "node:path";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
},
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@components": resolve(__dirname, "./components"),
|
||||
"@": resolve(__dirname, "./src"),
|
||||
"@pswui-lib": resolve(__dirname, "./vcn.ts"),
|
||||
"@pswui-lib": resolve(__dirname, "./lib/index.ts"),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -7,19 +7,40 @@
|
||||
"lib": [
|
||||
"index.ts",
|
||||
"Slot.tsx",
|
||||
"vcn.ts"
|
||||
"vcn.ts",
|
||||
"useDocument.ts",
|
||||
"useAnimatedMount.ts"
|
||||
],
|
||||
"components": {
|
||||
"button": { "type": "file", "name": "Button.tsx" },
|
||||
"checkbox": { "type": "file", "name": "Checkbox.tsx" },
|
||||
"dialog": { "type": "dir", "name": "Dialog", "files": ["index.ts", "Component.tsx", "Context.ts"] },
|
||||
"dialog": {
|
||||
"type": "dir",
|
||||
"name": "Dialog",
|
||||
"files": ["index.ts", "Component.tsx", "Context.ts"]
|
||||
},
|
||||
"drawer": { "type": "file", "name": "Drawer.tsx" },
|
||||
"form": { "type": "file", "name": "Form.tsx" },
|
||||
"input": { "type": "file", "name": "Input.tsx" },
|
||||
"label": { "type": "file", "name": "Label.tsx" },
|
||||
"popover": { "type": "file", "name": "Popover.tsx" },
|
||||
"switch": { "type": "file", "name": "Switch.tsx" },
|
||||
"tabs": { "type": "dir", "name": "Tabs", "files": ["index.ts", "Context.ts", "Hook.ts", "Component.tsx"] },
|
||||
"toast": { "type": "dir", "name": "Toast", "files": ["index.ts", "Component.tsx", "Hook.ts", "Store.ts", "Variant.ts"] },
|
||||
"tabs": {
|
||||
"type": "dir",
|
||||
"name": "Tabs",
|
||||
"files": ["index.ts", "Context.ts", "Hook.ts", "Component.tsx"]
|
||||
},
|
||||
"toast": {
|
||||
"type": "dir",
|
||||
"name": "Toast",
|
||||
"files": [
|
||||
"index.ts",
|
||||
"Component.tsx",
|
||||
"Hook.ts",
|
||||
"Store.ts",
|
||||
"Variant.ts"
|
||||
]
|
||||
},
|
||||
"tooltip": { "type": "file", "name": "Tooltip.tsx" }
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user