Skip to content

Commit

Permalink
chore: add colors panel
Browse files Browse the repository at this point in the history
  • Loading branch information
zouhangwithsweet committed Feb 24, 2024
1 parent ee5fcf0 commit b74742d
Show file tree
Hide file tree
Showing 11 changed files with 472 additions and 11 deletions.
3 changes: 3 additions & 0 deletions entrypoints/injected/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react'

import { CodeArea } from './components/Code'
import { Colors } from './components/Colors'
import { Download } from './components/Download'
import Header from './components/Header'

Expand Down Expand Up @@ -61,6 +62,8 @@ export default () => {
<CodeArea minimized={minimized} />

<Download minimized={minimized} />

<Colors minimized={minimized} />
</div>
)
}
12 changes: 6 additions & 6 deletions entrypoints/injected/components/Code.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { useAtom, useAtomValue } from 'jotai'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Clipboard } from 'react-feather'
import { toTailwindcss } from 'transform-to-tailwindcss-core'
import { toUnocssClass } from 'transform-to-unocss-core'
import { useCopyToClipboard } from 'usehooks-ts'

import { cssEngine, cssUnit, currentSelection } from '@/entrypoints/injected/store'

export const CodeArea = (props: { minimized?: boolean }) => {
export const CodeArea = memo((props: { minimized?: boolean }) => {
const engine = useAtomValue(cssEngine)
const unit = useAtomValue(cssUnit)
const isRem = useMemo(() => unit === 'rem', [unit])
Expand Down Expand Up @@ -44,7 +44,7 @@ export const CodeArea = (props: { minimized?: boolean }) => {

const unoMini = raw
.filter(([key]) =>
['font-feature-settings', 'font-family', 'text-transform'].every((item) => !key.startsWith(item)),
['font-feature-settings', 'font-family', 'text-transform'].every((item) => !key?.startsWith(item)),
)
.map(
([key, value]) =>
Expand All @@ -54,7 +54,7 @@ export const CodeArea = (props: { minimized?: boolean }) => {
.trim()}`,
)
.map((i) => (engine === 'unocss' ? toUnocssClass(i, isRem)[0] : toTailwindcss(i, isRem)))
.filter((i) => ['lh-normal', 'font-not-italic', 'bg-[url(]'].every((item) => !i.startsWith(item)))
.filter((i) => ['lh-normal', 'font-not-italic', 'bg-[url(]'].every((item) => !i?.startsWith(item)))
.join(' ')
.replace(/border-(\d+\.\d+|\d+)/g, (_, $1) => `border-${Number($1) * 4}`)
.replace(/(border-[xylrtb]-)(\d+\.\d+|\d+)/g, (_, $1, $2) => `${$1}${Number($2) * 4}`)
Expand Down Expand Up @@ -157,7 +157,7 @@ export const CodeArea = (props: { minimized?: boolean }) => {
ref={inputRef}
rows={1}
autoComplete="off"
className="px-4 h-auto py-4 lh-4.5 bg-#f5f5f5 cursor-text font-['Roboto_Mono'] resize-none"
className="px-4 h-auto py-4 lh-4.5 bg-#f5f5f5 cursor-text font-['Roboto_Mono'] resize-none scrollbar-hide"
value={u.code}
readOnly
onSelect={(e) => {
Expand All @@ -174,4 +174,4 @@ export const CodeArea = (props: { minimized?: boolean }) => {
</div>
</>
)
}
})
103 changes: 103 additions & 0 deletions entrypoints/injected/components/Colors.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { ChevronDownIcon } from '@radix-ui/react-icons'
import { useAtom, useAtomValue } from 'jotai'
import { memo, useEffect, useMemo, useState } from 'react'
import { useCopyToClipboard } from 'usehooks-ts'

import { figmaRGBToHex, figmaRGBToWebRGB } from '@/entrypoints/utils/convert'

import { colorMode, currentSelection } from '../store'
import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from '../ui/select'

export const Colors = memo((props: { minimized?: boolean }) => {
const node = useAtomValue(currentSelection)
const [mode, setMode] = useAtom(colorMode)

const [paints, setPaints] = useState<Paint[]>([])
const colors = useMemo(
() =>
paints
.filter((p) => p.type === 'SOLID')
.filter((p) => 'color' in p)
.map((p) => ('color' in p ? (mode === 'rgb' ? figmaRGBToWebRGB(p.color) : figmaRGBToHex(p.color)) : null))
.filter((p) => p !== null),
[mode, paints],
)

useEffect(() => {
if (figma) {
const res = figma.getSelectionColors()
setPaints(res?.paints || [])
}
}, [node])

const [_, copy] = useCopyToClipboard()

const handleCopy = (text: string) => () => {
copy(text)
.then(() => {
figma.notify('Copied to clipboard')
})
.catch((error: any) => {
figma.notify('Failed to copy!', {
error: true,
})
})
}

const [showMore, setShowMore] = useState(false)

return (
<div
className={`${props.minimized ? 'hidden' : 'block'} p-4 border-t border-#e5e5e5 border-solid font-600 text-13px`}
>
<div className="flex items-center gap-2">
<span className="flex-1">Colors</span>

<Select onValueChange={(e: 'rgb') => setMode(e)}>
<SelectTrigger className="w-auto h-auto p-0 uppercase [&_span]:uppercase !shadow-[none] text-xs">
<SelectValue placeholder={mode} className="uppercase" />
</SelectTrigger>
<SelectContent className="z-1002 border-1 border-solid border-muted">
<SelectGroup>
{(['rgb', 'hex'] as const).map((m) => (
<SelectItem key={m} value={m} className="w-30 uppercase text-xs">
{m}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<div
className={`mt-4 flex flex-col max-h-60 overflow-auto scrollbar-hide space-y-.5 ${colors.length === 0 ? 'hidden' : ''}`}
onMouseMove={(e) => e.stopPropagation()}
onWheel={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
{colors.slice(0, showMore ? colors.length : 3).map((c, index) => (
<div
className="shrink-0 h-7.5 flex items-center p-1 box-border hover:bg-#e5e5e5/50 rounded-sm cursor-pointer"
onClick={handleCopy(Array.isArray(c) ? `rgba(${c?.join(', ')})` : `${c}`)}
>
<span
className="w-4 h-4 rounded-sm border-1 border-muted border-solid"
style={{
backgroundColor: Array.isArray(c) ? `rgba(${c?.join(', ')})` : `${c}`,
}}
></span>
<input
type="text"
readOnly
value={Array.isArray(c) ? `rgba(${c?.join(', ')})` : `${c}`}
className="ml-4 font-400 text-xs font-['Inter'] bg-transparent"
/>
</div>
))}
<ChevronDownIcon
className={`mx-auto w-5 h-5 shrink-0 text-#000/50 hover:text-#000 cursor-pointer transition-transform ${colors.length > 3 ? '' : 'hidden'} ${showMore ? 'rotate-180' : ''}`}
onClick={() => setShowMore(!showMore)}
></ChevronDownIcon>
</div>
</div>
)
})
6 changes: 3 additions & 3 deletions entrypoints/injected/components/Download.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { useAtomValue } from 'jotai'
import { useEffect, useState } from 'react'
import { memo, useEffect, useState } from 'react'

import { arrayBufferToBase64, arrayBufferToImageFile } from '@/entrypoints/utils/file'

import { currentSelection, exportExt, exportScale } from '../store'

export const Download = (props: { minimized?: boolean }) => {
export const Download = memo((props: { minimized?: boolean }) => {
const node = useAtomValue(currentSelection)
const [imageBase64, setImageBase64] = useState('')
const scale = useAtomValue(exportScale)
Expand Down Expand Up @@ -85,4 +85,4 @@ export const Download = (props: { minimized?: boolean }) => {
)}
</div>
)
}
})
4 changes: 2 additions & 2 deletions entrypoints/injected/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CheckIcon } from '@radix-ui/react-icons'
import { useAtom } from 'jotai'
import { ForwardedRef, forwardRef, MouseEvent } from 'react'
import { ForwardedRef, forwardRef, memo, MouseEvent } from 'react'
import { Maximize2, Minimize2, Settings } from 'react-feather'

import Logo from '@/entrypoints/assets/fubukicss.svg'
Expand Down Expand Up @@ -137,4 +137,4 @@ const Header = forwardRef(function (
)
})

export default Header
export default memo(Header)
2 changes: 2 additions & 0 deletions entrypoints/injected/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ export const cssUnit = atomWithStorage<'px' | 'rem' | 'rpx'>('fubuki_css_unit',
export const exportExt = atomWithStorage<'png' | 'jpg' | 'svg'>('fubuki_export_ext', 'png')

export const exportScale = atomWithStorage<1 | 1.5 | 2 | 3 | 4>('fubuki_export_scale', 3)

export const colorMode = atomWithStorage<'rgb' | 'hex'>('fubuki_export_color_mode', 'hex')
144 changes: 144 additions & 0 deletions entrypoints/injected/ui/select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
'use client'

import { CaretSortIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons'
import * as SelectPrimitive from '@radix-ui/react-select'
import * as React from 'react'

import { cn } from '../../utils/cn'

const Select = SelectPrimitive.Root

const SelectGroup = SelectPrimitive.Group

const SelectValue = SelectPrimitive.Value

const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
'flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
className,
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<CaretSortIcon className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName

const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronUpIcon />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName

const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)}
{...props}
>
<ChevronDownIcon />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName

const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = 'popper', ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
'relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
position === 'popper' &&
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
className,
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]',
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName

const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label ref={ref} className={cn('px-2 py-1.5 text-sm font-semibold', className)} {...props} />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName

const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
'relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
className,
)}
{...props}
>
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<CheckIcon className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName

const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator ref={ref} className={cn('-mx-1 my-1 h-px bg-muted', className)} {...props} />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName

export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}
Loading

0 comments on commit b74742d

Please sign in to comment.