diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000..e5b6d8d --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,8 @@ +# Changesets + +Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works +with multi-package repos, or single-package repos to help you version and publish your code. You can +find the full documentation for it [in our repository](https://github.com/changesets/changesets) + +We have a quick list of common questions to get you started engaging with this project in +[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000..80bf895 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": ["website"] +} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0650281 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: CI Test + +on: [pull_request] + +jobs: + test: + timeout-minutes: 20 + + runs-on: ubuntu-latest + + steps: + - name: Checkout Repo + uses: actions/checkout@v2 + + - name: Use Node.js + uses: actions/setup-node@v1 + with: + node-version: 20 + + - name: Use pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build + + - name: Test + run: pnpm test diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..a3d8888 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +name: Release + +on: + workflow_dispatch: + push: + paths: + - '.changeset/**' + - 'packages/**' + branches: + - main + +concurrency: ${{ github.workflow }}-${{ github.ref }} + +jobs: + release: + name: Release + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + + - name: Setup pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Install Dependencies + run: pnpm install --frozen-lockfile + + - name: Build + run: pnpm build + + - name: Creating .npmrc + run: | + cat << EOF > "$HOME/.npmrc" + //registry.npmjs.org/:_authToken=$NPM_TOKEN + EOF + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + + - name: Create Release Pull Request or Release to npm + id: changesets + uses: changesets/action@v1 + with: + # This expects you to have a script called release which does a build for your packages and calls changeset publish + publish: pnpm release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b570de0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +node_modules +.pnp +.pnp.js + +# testing +coverage + +# dist +.next/ +out/ +build +dist + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# turbo +.turbo + +# vercel +.vercel diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..d97b929 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +registry=https://registry.npmjs.org/ +public-hoist-pattern[]=*@nextui-org/* diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..01aa840 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1 @@ +"@nnecec/prettier-config" diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d642209 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "command": "./node_modules/.bin/astro dev", + "name": "Development server", + "request": "launch", + "type": "node-terminal" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..25fa621 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4ab8403 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 nnecec + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/apps/website/.gitignore b/apps/website/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/apps/website/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/website/.npmrc b/apps/website/.npmrc new file mode 100644 index 0000000..1778f10 --- /dev/null +++ b/apps/website/.npmrc @@ -0,0 +1 @@ +public-hoist-pattern[]=*@nextui-org/* diff --git a/apps/website/app/favicon.ico b/apps/website/app/favicon.ico new file mode 100644 index 0000000..685cbad Binary files /dev/null and b/apps/website/app/favicon.ico differ diff --git a/apps/website/app/globals.css b/apps/website/app/globals.css new file mode 100644 index 0000000..61f52f0 --- /dev/null +++ b/apps/website/app/globals.css @@ -0,0 +1,7 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --slate: #ff0000; +} diff --git a/apps/website/app/layout.tsx b/apps/website/app/layout.tsx new file mode 100644 index 0000000..daddd92 --- /dev/null +++ b/apps/website/app/layout.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from 'next' + +// eslint-disable-next-line camelcase +import { Roboto_Mono } from 'next/font/google' + +import './globals.css' + +const font = Roboto_Mono({ subsets: ['latin'] }) + +export const metadata: Metadata = { + description: 'Welcome to the ColorKit world!', + icons: [ + { rel: 'icon', sizes: '32x32', url: '/favicon-32x32.png' }, + { rel: 'icon', sizes: '16x16', url: '/favicon-16x16.png' }, + { rel: 'apple-touch-icon', sizes: '180x180', url: '/apple-touch-icon.png' }, + ], + title: 'tailwind-plugin-palette - ColorKit', +} +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} diff --git a/apps/website/app/page.tsx b/apps/website/app/page.tsx new file mode 100644 index 0000000..e16fc6a --- /dev/null +++ b/apps/website/app/page.tsx @@ -0,0 +1,24 @@ +'use client' + +import { MotionConfig } from 'framer-motion' +import { Provider } from 'jotai' + +import { PaletteFooter } from '../components/palette/footer' +import { PaletteIntro } from '../components/palette/intro' +import { PaletteSwatches } from '../components/palette/swatches' +import { PaletteTools } from '../components/palette/tools' + +export default function RootPage() { + return ( +
+ + + + + + + + +
+ ) +} diff --git a/apps/website/app/provider.tsx b/apps/website/app/provider.tsx new file mode 100644 index 0000000..71a34a2 --- /dev/null +++ b/apps/website/app/provider.tsx @@ -0,0 +1,7 @@ +'use client' + +import { NextUIProvider } from '@nextui-org/react' + +export default function Providers({ children }: { children: React.ReactNode }) { + return {children} +} diff --git a/apps/website/app/site.webmanifest b/apps/website/app/site.webmanifest new file mode 100644 index 0000000..4e81430 --- /dev/null +++ b/apps/website/app/site.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "ColorKit", + "short_name": "ColorKit", + "icons": [ + { "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, + { "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }, + { + "src": "/favicon.ico", + "sizes": "any", + "type": "image/x-icon" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone", + "start_url": "/" +} diff --git a/apps/website/components/color-picker.tsx b/apps/website/components/color-picker.tsx new file mode 100644 index 0000000..8b5c879 --- /dev/null +++ b/apps/website/components/color-picker.tsx @@ -0,0 +1,46 @@ +import type { ButtonProps } from '@nextui-org/react' + +import { useRef } from 'react' + +import { Button } from '@nextui-org/react' + +import { usePropsValue } from '../utils/use-props-value' + +export const ColorPicker = ({ + defaultValue, + onChange, + value, + ...props +}: { defaultValue?: string; onChange?: (value: string) => void; value?: string } & Omit) => { + const ref = useRef(null) + + const [val, setVal] = usePropsValue({ defaultValue: defaultValue ?? '', onChange, value }) + + return ( + + ) +} diff --git a/apps/website/components/palette/footer.tsx b/apps/website/components/palette/footer.tsx new file mode 100644 index 0000000..26cd6a6 --- /dev/null +++ b/apps/website/components/palette/footer.tsx @@ -0,0 +1,53 @@ +import { useState } from 'react' + +import { motion } from 'framer-motion' +import Image from 'next/image' + +import { Link } from '@nextui-org/react' + +const MotionImage = motion(Image) + +export function PaletteFooter() { + const [isHovered, setHovered] = useState(false) + return ( +
+ setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + + +
+ + Github + + + X + +
+
+ Built by{' '} + + nnecec + +
+
+
+
+ ) +} diff --git a/apps/website/components/palette/intro.tsx b/apps/website/components/palette/intro.tsx new file mode 100644 index 0000000..35c02e2 --- /dev/null +++ b/apps/website/components/palette/intro.tsx @@ -0,0 +1,62 @@ +'use client' + +import { useState } from 'react' + +import { motion, useMotionValueEvent, useScroll, useTransform } from 'framer-motion' + +import { Snippet } from '@nextui-org/react' + +export function PaletteIntro() { + const innerHeight = typeof window === undefined ? 600 : window.innerHeight + + const [inPaletteView, setInPaletteView] = useState(false) + + const { scrollY } = useScroll() + + useMotionValueEvent(scrollY, 'change', latest => { + if (latest > innerHeight / 2) { + !inPaletteView && setInPaletteView(true) + } else { + inPaletteView && setInPaletteView(false) + } + }) + + const titleY = useTransform(scrollY, [0, innerHeight], [0, innerHeight / 2.5]) + + return ( +
+ +

tailwind-plugin-palette

+

+ tailwind +
+ plugin +
+ palette +

+ +
+
Install via
+
+ + npm + + + pnpm + + + bun + +
+
+
+
+ ) +} diff --git a/apps/website/components/palette/swatches.tsx b/apps/website/components/palette/swatches.tsx new file mode 100644 index 0000000..27dcc37 --- /dev/null +++ b/apps/website/components/palette/swatches.tsx @@ -0,0 +1,190 @@ +'use client' + +import { useEffect, useRef, useState } from 'react' + +import { CheckIcon, XMarkIcon } from '@heroicons/react/20/solid' +import clsx from 'clsx' +import { motion, useMotionValueEvent, useScroll } from 'framer-motion' +import { useAtom } from 'jotai' + +import { Button, Input, Tooltip } from '@nextui-org/react' + +import { getColorName } from '../../utils/color' +import { ColorPicker } from '../color-picker' +import { colorsAtom, editingSwatchesAtom, paletteAtom } from './utils' + +export function PaletteSwatches() { + const [inPaletteView, setInPaletteView] = useState(false) + const [colors, setColors] = useAtom(colorsAtom) + const [palette] = useAtom(paletteAtom) + + const [editingSwatches, setEditingSwatches] = useAtom(editingSwatchesAtom) + + const isEmptyPalette = colors.length === 0 + + const { scrollY } = useScroll() + + useMotionValueEvent(scrollY, 'change', latest => { + if (latest > window.innerHeight / 2) { + !inPaletteView && setInPaletteView(true) + } else { + inPaletteView && setInPaletteView(false) + } + }) + + return ( +
+ {isEmptyPalette ? +
+

There is no swatch yet,

+

Please click the plus button or choose a primary color

+
+ : colors.map((swatch, index) => { + const editingSwatch = editingSwatches.get(index) + const isInvalid = Boolean(editingSwatch?.errorMessage) + + return ( +
+ + { + const newSwatch = { ...swatch, name: value } + editingSwatches.set(index, newSwatch) + setEditingSwatches(new Map(editingSwatches)) + }} + value={editingSwatch?.name ?? swatch.name} + /> + + {!!swatch.hex && ( + { + const newSwatch = { ...swatch, hex: value, name: getColorName(value) } + editingSwatches.set(index, newSwatch) + setEditingSwatches(new Map(editingSwatches)) + }} + size="sm" + value={editingSwatch?.hex ?? swatch.hex} + /> + )} + + {editingSwatch && (editingSwatch.name !== swatch.name || editingSwatch.hex !== swatch.hex) ? + <> + + + + : null} + + {!!palette[swatch.name] && } +
+ ) + }) + } +
+ ) +} + +function Swatches({ name, swatches }: { name: string; swatches: Record }) { + const ref = useRef(null) + + useEffect(() => { + ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' }) + }, []) + + return ( + + {Object.entries(swatches as Record).map(([step, shade]) => ( + +
+ +
+ + + ))} + + ) +} diff --git a/apps/website/components/palette/tools.tsx b/apps/website/components/palette/tools.tsx new file mode 100644 index 0000000..8050197 --- /dev/null +++ b/apps/website/components/palette/tools.tsx @@ -0,0 +1,224 @@ +'use client' + +import { useMemo, useRef, useState, useTransition } from 'react' + +import { BookOpenIcon, CogIcon, PlusIcon, XMarkIcon } from '@heroicons/react/20/solid' +import { motion, useMotionValueEvent, useScroll } from 'framer-motion' +import { useAtom } from 'jotai' + +import { + Badge, + Checkbox, + Modal, + ModalBody, + ModalContent, + ModalHeader, + Popover, + PopoverContent, + PopoverTrigger, + Snippet, + Tooltip, + useDisclosure, +} from '@nextui-org/react' + +import { ColorPicker } from '../../components/color-picker' +import { randomColor } from '../../utils/color' +import { IconButton, colorsAtom, optionsAtom } from './utils' + +export function PaletteTools() { + const [inPaletteView, setInPaletteView] = useState(false) + const [options, setOptions] = useAtom(optionsAtom) + const [colors, setColors] = useAtom(colorsAtom) + const { isOpen, onOpen, onOpenChange } = useDisclosure() + const toolsRef = useRef() + + const [, startTransition] = useTransition() + + const isEmptyPalette = colors.length === 0 + + const addSwatch = () => { + let color = randomColor() + + while (colors.some(({ name }) => name === color.name)) { + color = randomColor() + } + setColors([...colors, color]) + } + + const { scrollY } = useScroll() + + useMotionValueEvent(scrollY, 'change', latest => { + if (latest > window.innerHeight / 2) { + !inPaletteView && setInPaletteView(true) + } else { + inPaletteView && setInPaletteView(false) + } + }) + + const copiedOptions = useMemo(() => { + return JSON.stringify( + Object.fromEntries([ + ['colors', Object.fromEntries(colors.map(color => [color.name, color.hex]))], + ...Object.entries(options).filter(([, value]) => Boolean(value)), + ]), + null, + 2, + ) + }, [colors, options]) + + return ( +
+ + + } + isInvisible={!options.primary} + isOneChar + onClick={() => { + setOptions({ ...options, primary: '' }) + setColors(colors.filter(color => !['primary', 'secondary'].includes(color.name))) + }} + shape="circle" + showOutline={false} + > + { + if (!options.primary) { + colors.unshift({ disabled: true, name: 'primary' }, { disabled: true, name: 'secondary' }) + } + startTransition(() => { + setOptions(options => ({ ...options, primary: value })) + }) + }} + value={options.primary} + /> + + + + + + + + + + + + + + + + +
+

More options

+ setOptions({ ...options, dark: value })}> + Dark + + + setOptions({ ...options, reversed: value })} + > + Reversed + + + setOptions({ ...options, harmonize: value })} + > + Harmonize + +
+
+
+ + + + + + + + + +
+
+
+ + + + How to configure your Tailwind.CSS? + +
+

1. Install tailwind-plugin-palette

+
+ + npm + + pnpm + + bun + +
+

2. Configure your tailwind config file

+
{`import palette, { getTailwindColors } from 'tailwind-plugin-palette'
+
+export default {
+  plugins: [
+    palette({
+      colors: getTailwindColors(400),
+      primary: "#ADCE91",
+      dark: true,
+      reversed: true,
+      harmonize: true
+    })
+  ]
+}`}
+

3. Options

+
    +
  • + {'colors: Record'}: A colors object, where the key is the name of the + color and the value is the hexadecimal value of the color. eg,{' '} + {'colors: { red: "#ff0000" }'} +
  • +
  • + primary: string: Provide a hex value as primary color, automatically generate a secondary + color +
  • +
  • + dark: boolean: Reduce the brightness to adapt to the dark mode +
  • +
  • + reversed: boolean: Reverse the color value +
  • +
  • + harmonize: boolean: Make the palette more harmonious with the primary color(primary + required) +
  • +
+
+
+
+
+
+ ) +} diff --git a/apps/website/components/palette/utils.tsx b/apps/website/components/palette/utils.tsx new file mode 100644 index 0000000..a3f62d8 --- /dev/null +++ b/apps/website/components/palette/utils.tsx @@ -0,0 +1,48 @@ +import type { ButtonProps } from '@nextui-org/react' + +import { forwardRef } from 'react' + +import { motion } from 'framer-motion' +import { atom } from 'jotai' +import { createPalette } from 'tailwind-plugin-palette' + +import { Button } from '@nextui-org/react' + +export const IconButton = forwardRef(function IconButton(props: ButtonProps, ref: any) { + return