mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
Both old names are owned by a different npm account (artemis-prime), so publishing them returns 403 — which is how this surfaced: the 8.0.0 run shipped @hanzogui/logo and @hanzogui/demos depending on [email protected], a version that cannot be published and therefore does not exist. Those two were uninstallable. @hanzo/gui and @hanzogui/* are ours, so the framework now carries the name it is imported by everywhere downstream, and the loader moves under the scope we control. The rewrite is limited to package references. `DEBUG?.startsWith('hanzogui')` is a log-namespace filter and join(dir, 'hanzogui') is a path — rewriting either would change behaviour, so both are untouched. Co-authored-by: Hanzo Dev <[email protected]>
789 lines
20 KiB
TypeScript
789 lines
20 KiB
TypeScript
import {
|
|
ArrowLeft,
|
|
ArrowLeftRight,
|
|
ArrowRight,
|
|
ArrowUpDown,
|
|
} from '@hanzogui/lucide-icons-2'
|
|
import type { BuildPalette, BuildThemeAnchor } from '@hanzogui/theme-builder'
|
|
import { getThemeSuitePalettes, PALETTE_BACKGROUND_OFFSET } from '@hanzogui/theme-builder'
|
|
import { getStore, Store, useStore } from '@hanzogui/use-store'
|
|
import { parseToHsla } from 'color2k'
|
|
import { memo, useState } from 'react'
|
|
import {
|
|
Anchor,
|
|
Button,
|
|
Paragraph,
|
|
SizableText,
|
|
Theme,
|
|
Tooltip,
|
|
TooltipSimple,
|
|
useThemeName,
|
|
View,
|
|
XStack,
|
|
YStack,
|
|
} from '@hanzo/gui'
|
|
import { useDoublePress } from '~/features/studio/hooks/useDoublePress'
|
|
import { useThemeBuilderStore } from '~/features/studio/theme/store/ThemeBuilderStore'
|
|
import { Select, SelectItem } from '../../../../../components/Select'
|
|
import { type HSLA, ColorPickerContents } from '../../../colors/ColorPicker'
|
|
import { rootStore } from '../../../state/RootStore'
|
|
import { toastController } from '../../../ToastProvider'
|
|
import { defaultScaleGrouped } from '../../constants/defaultScaleGrouped'
|
|
import { FieldsetWithLabel } from '../../views/FieldsetWithLabel'
|
|
import { XLabeledItem } from '../../views/XLabeledItem'
|
|
|
|
type StepBaseThemesProps = {
|
|
previewMode?: boolean
|
|
}
|
|
|
|
export const StepBaseThemes = (_props: StepBaseThemesProps) => {
|
|
const store = useThemeBuilderStore()
|
|
const themeBuilder = useThemeBuilderStore()
|
|
|
|
return (
|
|
<YStack
|
|
borderTopRightRadius="$8"
|
|
borderTopLeftRadius="$8"
|
|
overflow="hidden"
|
|
gap="$3"
|
|
py="$3"
|
|
px="$2"
|
|
>
|
|
{Object.entries(store.palettes).map(([name, palette]) => {
|
|
const isAccent = name === 'accent'
|
|
return (
|
|
<FieldsetWithLabel
|
|
key={name}
|
|
label={name}
|
|
afterLabel={
|
|
<XStack gap="$2" items="center">
|
|
{isAccent && (
|
|
<Select
|
|
size="$2"
|
|
defaultValue={themeBuilder.accentSetting}
|
|
onValueChange={(value) => {
|
|
themeBuilder.setAccentSetting(value as any)
|
|
}}
|
|
rounded="$4"
|
|
minW={80}
|
|
borderWidth={0}
|
|
>
|
|
<SelectItem value="off" index={0}>
|
|
Off
|
|
</SelectItem>
|
|
<SelectItem value="inverse" index={1}>
|
|
Inverse
|
|
</SelectItem>
|
|
<SelectItem value="color" index={2}>
|
|
Color
|
|
</SelectItem>
|
|
</Select>
|
|
)}
|
|
</XStack>
|
|
}
|
|
>
|
|
<YStack gap="$4">
|
|
<PaletteView
|
|
onUpdate={(next) => {
|
|
store.updatePalette(name, next)
|
|
}}
|
|
palette={palette}
|
|
/>
|
|
</YStack>
|
|
</FieldsetWithLabel>
|
|
)
|
|
})}
|
|
</YStack>
|
|
)
|
|
}
|
|
|
|
type Props = {
|
|
palette: BuildPalette
|
|
onUpdate: (next: Partial<BuildPalette>) => void
|
|
}
|
|
|
|
class PaletteStore extends Store<{ name: string }> {
|
|
hoveredColor = 0
|
|
selectedColor = 0
|
|
|
|
get activeColor() {
|
|
return this.hoveredColor === -1 ? this.selectedColor : this.hoveredColor
|
|
}
|
|
|
|
setHoveredColor(index: number) {
|
|
this.hoveredColor = index
|
|
}
|
|
|
|
setSelectedColor(index: number) {
|
|
this.selectedColor = index
|
|
this.hoveredColor = -1
|
|
}
|
|
}
|
|
|
|
const usePaletteStore = (name: string) => {
|
|
return useStore(PaletteStore, { name })
|
|
}
|
|
|
|
const PaletteView = memo((props: Props) => {
|
|
const { palette, onUpdate } = props
|
|
const store = usePaletteStore(palette.name)
|
|
const isDark = useThemeName().startsWith('dark')
|
|
const palettes = getThemeSuitePalettes(palette)
|
|
const darkPalette = palettes['dark']!
|
|
const lightPalette = palettes['light']!
|
|
|
|
const colors = {
|
|
light: sliceToPalette(lightPalette),
|
|
dark: sliceToPalette(darkPalette),
|
|
}
|
|
|
|
const { activeColor } = store
|
|
|
|
const hoveredItem = defaultScaleGrouped[activeColor]
|
|
|
|
const { anchors } = palette
|
|
|
|
const anchorRealIndex = +hoveredItem?.value - PALETTE_BACKGROUND_OFFSET
|
|
|
|
const anchorIndex = anchors.findIndex((x) => x.index === anchorRealIndex)
|
|
|
|
const anchor = anchors[anchorIndex]
|
|
const nextAnchor = anchors[anchorIndex + 1]
|
|
const prevAnchor = anchors[anchorIndex - 1]
|
|
|
|
const toggleAnchorAt = async (index: number) => {
|
|
if (anchor) {
|
|
// DELETE
|
|
if (index === 0) {
|
|
toastController.show(`Can't delete anchor at index 0`)
|
|
return
|
|
}
|
|
if (anchors.length === 2) {
|
|
await rootStore.confirmDialog('alert', {
|
|
title: `Can't delete anchor`,
|
|
message: `You must have at least 2 anchors to create a palette.`,
|
|
})
|
|
return
|
|
}
|
|
|
|
if (
|
|
await rootStore.confirmDialog('confirm-delete', {
|
|
thingName: `anchor at ${index}`,
|
|
})
|
|
) {
|
|
onUpdate({
|
|
anchors: anchors.filter((x) => x !== anchor),
|
|
})
|
|
}
|
|
} else {
|
|
const [lightHSLA, darkHSLA] = [
|
|
// adjusts for our transparent indices
|
|
parseToHsla(lightPalette[index + PALETTE_BACKGROUND_OFFSET]),
|
|
parseToHsla(darkPalette[index + PALETTE_BACKGROUND_OFFSET]),
|
|
]
|
|
|
|
const anchor: BuildThemeAnchor = {
|
|
index,
|
|
hue: {
|
|
light: lightHSLA[0],
|
|
dark: darkHSLA[0],
|
|
},
|
|
sat: {
|
|
light: lightHSLA[1],
|
|
dark: darkHSLA[1],
|
|
},
|
|
lum: {
|
|
light: lightHSLA[2],
|
|
dark: darkHSLA[2],
|
|
},
|
|
alpha: {
|
|
light: 1,
|
|
dark: 1,
|
|
},
|
|
}
|
|
|
|
const next = [...anchors]
|
|
const insertIndex = next.findIndex((x) => x.index > index) ?? next.length - 1
|
|
next.splice(insertIndex, 0, anchor)
|
|
|
|
// ADD
|
|
onUpdate({
|
|
anchors: next,
|
|
})
|
|
}
|
|
}
|
|
|
|
const onChangeAnchorColor = (scheme: 'light' | 'dark') => (_: string, hsla: HSLA) => {
|
|
const oppScheme = scheme === 'dark' ? 'light' : 'dark'
|
|
|
|
const anchors = palette.anchors.map((a, i) => {
|
|
if (a !== anchor) return a
|
|
|
|
const next = {
|
|
...a,
|
|
hue: {
|
|
...a.hue,
|
|
[scheme]: hsla.hue,
|
|
},
|
|
sat: {
|
|
...a.sat,
|
|
[scheme]: hsla.sat,
|
|
},
|
|
lum: {
|
|
...a.lum,
|
|
[scheme]: hsla.light,
|
|
},
|
|
}
|
|
|
|
function syncOppositeSchemeHue(_: BuildThemeAnchor) {
|
|
if (_.hue.sync) {
|
|
_.hue[oppScheme] = hsla.hue
|
|
}
|
|
}
|
|
|
|
function syncOppositeSchemeSat(_: BuildThemeAnchor) {
|
|
if (_.sat.sync) {
|
|
_.sat[oppScheme] = hsla.sat
|
|
}
|
|
}
|
|
|
|
syncOppositeSchemeHue(next)
|
|
syncOppositeSchemeSat(next)
|
|
|
|
// sync left does two way sync technically
|
|
// since its "syncing" the left one, they should both sync
|
|
|
|
// sync left
|
|
const prevAnchor = palette.anchors[i - 1]
|
|
if (prevAnchor) {
|
|
if (a.hue.syncLeft) {
|
|
prevAnchor.hue[scheme] = hsla.hue
|
|
syncOppositeSchemeHue(prevAnchor)
|
|
}
|
|
if (a.sat.syncLeft) {
|
|
prevAnchor.sat[scheme] = hsla.sat
|
|
syncOppositeSchemeSat(prevAnchor)
|
|
}
|
|
}
|
|
|
|
// sync right
|
|
const nextAnchor = palette.anchors[i + 1]
|
|
if (nextAnchor) {
|
|
if (nextAnchor.hue.syncLeft) {
|
|
nextAnchor.hue[scheme] = hsla.hue
|
|
syncOppositeSchemeHue(nextAnchor)
|
|
}
|
|
if (nextAnchor.sat.syncLeft) {
|
|
nextAnchor.sat[scheme] = hsla.sat
|
|
syncOppositeSchemeSat(nextAnchor)
|
|
}
|
|
}
|
|
|
|
return next
|
|
})
|
|
|
|
onUpdate({
|
|
anchors,
|
|
})
|
|
}
|
|
|
|
const lightDarkSynced = anchors.every((a) => a.hue.sync && a.sat.sync)
|
|
|
|
const syncButtons = (
|
|
<XStack
|
|
opacity={0}
|
|
$group-content-hover={{ opacity: 1 }}
|
|
gap="$4"
|
|
items="center"
|
|
ml={20}
|
|
flex={1}
|
|
>
|
|
{/* Spacer to match color circle in ColorPickerContents */}
|
|
<View width={24} />
|
|
<XStack gap="$4" flex={1}>
|
|
<XStack justify="space-between" flex={1}>
|
|
<SyncButtons
|
|
anchorKey="hue"
|
|
{...props}
|
|
anchor={anchor}
|
|
prevAnchor={prevAnchor}
|
|
nextAnchor={nextAnchor}
|
|
/>
|
|
</XStack>
|
|
<XStack justify="space-between" flex={1}>
|
|
<SyncButtons
|
|
anchorKey="sat"
|
|
{...props}
|
|
anchor={anchor}
|
|
prevAnchor={prevAnchor}
|
|
nextAnchor={nextAnchor}
|
|
/>
|
|
</XStack>
|
|
<View flex={1} />
|
|
</XStack>
|
|
</XStack>
|
|
)
|
|
|
|
return (
|
|
<YStack contain="paint" p="$4" mx="$-4" mb="$0" flex={1} flexBasis="auto" gap="$4">
|
|
<YStack group="content" containerType="normal" gap="$4">
|
|
<ColorPickerContents
|
|
disabled={!anchor}
|
|
value={lightPalette[hoveredItem?.value ?? 0]}
|
|
onChange={onChangeAnchorColor('light')}
|
|
shouldDim={lightDarkSynced && isDark}
|
|
/>
|
|
|
|
<YStack py="$2">{syncButtons}</YStack>
|
|
|
|
<XLabeledItem label={<SizableText size="$4">Light</SizableText>}>
|
|
<StepThemeHoverablePalette
|
|
palette={palette}
|
|
colors={colors.light}
|
|
onSelect={(color, index) => toggleAnchorAt(index)}
|
|
/>
|
|
</XLabeledItem>
|
|
|
|
<PaletteIndices />
|
|
</YStack>
|
|
|
|
<XLabeledItem label="">
|
|
<YStack gap="$4">
|
|
<XStack gap="$4">
|
|
<DataItem
|
|
width={50}
|
|
labelTop=""
|
|
labelBottom={
|
|
<SizableText
|
|
select="none"
|
|
text="right"
|
|
minW={60}
|
|
px="$2"
|
|
display="block"
|
|
size="$9"
|
|
fontWeight="bold"
|
|
>
|
|
{activeColor + 1}
|
|
</SizableText>
|
|
}
|
|
/>
|
|
|
|
<DataItem
|
|
width={200}
|
|
labelTop={hoveredItem?.name ?? '-'}
|
|
labelBottom={hoveredItem?.keys.join(', ') ?? '-'}
|
|
/>
|
|
|
|
<DataItem
|
|
labelTop={anchor ? 'Anchor' : 'Sync'}
|
|
labelBottom={
|
|
<XStack width={50} overflow="hidden" items="center" justify="center">
|
|
<Button
|
|
chromeless
|
|
size="$2"
|
|
scaleIcon={1.4}
|
|
circular
|
|
icon={anchor ? <Anchor /> : <ArrowLeftRight />}
|
|
onPress={() => {
|
|
toggleAnchorAt(activeColor)
|
|
}}
|
|
/>
|
|
</XStack>
|
|
}
|
|
/>
|
|
|
|
{/* {!anchor && <DataItem top="" bottom={<Button size="$2">Edit</Button>} />} */}
|
|
</XStack>
|
|
</YStack>
|
|
</XLabeledItem>
|
|
|
|
<PaletteIndices />
|
|
|
|
<YStack group="content" containerType="normal" gap="$4">
|
|
<XLabeledItem label={<SizableText size="$4">Dark</SizableText>}>
|
|
<StepThemeHoverablePalette
|
|
palette={palette}
|
|
colors={colors.dark}
|
|
onSelect={(color, index) => toggleAnchorAt(index)}
|
|
/>
|
|
</XLabeledItem>
|
|
|
|
<YStack py="$2">{syncButtons}</YStack>
|
|
|
|
<ColorPickerContents
|
|
isActive={isDark}
|
|
disabled={!anchor}
|
|
value={darkPalette[hoveredItem?.value ?? 0]}
|
|
onChange={onChangeAnchorColor('dark')}
|
|
shouldDim={lightDarkSynced && !isDark}
|
|
/>
|
|
</YStack>
|
|
</YStack>
|
|
)
|
|
})
|
|
|
|
const SyncButtons = memo(
|
|
({
|
|
anchorKey,
|
|
onUpdate,
|
|
anchor,
|
|
prevAnchor,
|
|
nextAnchor,
|
|
palette,
|
|
}: Props & {
|
|
anchorKey: 'hue' | 'sat'
|
|
anchor: BuildThemeAnchor
|
|
prevAnchor: BuildThemeAnchor
|
|
nextAnchor: BuildThemeAnchor
|
|
}) => {
|
|
return (
|
|
<>
|
|
<Theme name={anchor?.[anchorKey].syncLeft ? 'accent' : null}>
|
|
<TooltipSimple label={`Sync ${anchorKey} to last anchor`}>
|
|
<Button
|
|
size={16}
|
|
scaleIcon={1.4}
|
|
circular
|
|
icon={ArrowLeft}
|
|
onPress={() => {
|
|
onUpdate({
|
|
anchors: palette.anchors.map((a) =>
|
|
a === anchor
|
|
? {
|
|
...a,
|
|
[anchorKey]: {
|
|
...a[anchorKey],
|
|
syncLeft: !a[anchorKey].syncLeft,
|
|
},
|
|
}
|
|
: a
|
|
),
|
|
})
|
|
}}
|
|
{...(!prevAnchor
|
|
? {
|
|
opacity: 0.1,
|
|
disabled: true,
|
|
}
|
|
: null)}
|
|
/>
|
|
</TooltipSimple>
|
|
</Theme>
|
|
|
|
<Theme name={anchor?.[anchorKey].sync ? 'accent' : null}>
|
|
<TooltipSimple label={`Sync ${anchorKey} light and dark`}>
|
|
<Button
|
|
size={16}
|
|
scaleIcon={1.4}
|
|
circular
|
|
icon={ArrowUpDown}
|
|
onPress={() => {
|
|
onUpdate({
|
|
anchors: palette.anchors.map((a) =>
|
|
a === anchor
|
|
? {
|
|
...a,
|
|
[anchorKey]: {
|
|
...a[anchorKey],
|
|
sync: !a[anchorKey].sync,
|
|
},
|
|
}
|
|
: a
|
|
),
|
|
})
|
|
}}
|
|
/>
|
|
</TooltipSimple>
|
|
</Theme>
|
|
|
|
<Theme name={nextAnchor?.[anchorKey].syncLeft ? 'accent' : null}>
|
|
<TooltipSimple label={`Sync ${anchorKey} to next anchor`}>
|
|
<Button
|
|
size={16}
|
|
scaleIcon={1.4}
|
|
circular
|
|
icon={ArrowRight}
|
|
onPress={() => {
|
|
onUpdate({
|
|
anchors: palette.anchors.map((a) =>
|
|
a === nextAnchor
|
|
? {
|
|
...a,
|
|
[anchorKey]: {
|
|
...a[anchorKey],
|
|
syncLeft: !a[anchorKey].syncLeft,
|
|
},
|
|
}
|
|
: a
|
|
),
|
|
})
|
|
}}
|
|
{...(!nextAnchor
|
|
? {
|
|
opacity: 0.1,
|
|
disabled: true,
|
|
}
|
|
: null)}
|
|
/>
|
|
</TooltipSimple>
|
|
</Theme>
|
|
</>
|
|
)
|
|
}
|
|
)
|
|
|
|
const DataItem = ({
|
|
labelTop,
|
|
labelBottom,
|
|
width,
|
|
}: {
|
|
labelTop: any
|
|
labelBottom: any
|
|
width?: any
|
|
}) => {
|
|
return (
|
|
<YStack width={width} maxW={width}>
|
|
<SizableText lineHeight="$1" select="none">
|
|
{labelTop}
|
|
</SizableText>
|
|
<SizableText select="none" size="$2">
|
|
{labelBottom}
|
|
</SizableText>
|
|
</YStack>
|
|
)
|
|
}
|
|
|
|
// get the center 12 items:
|
|
const sliceToPalette = (colors: string[]) => {
|
|
const toRemove = colors.length - 12
|
|
return colors.slice(toRemove / 2, colors.length - toRemove / 2)
|
|
}
|
|
|
|
type PaletteProps = {
|
|
colors: string[]
|
|
size?: 'small' | 'medium'
|
|
children?: (color: string, index: number) => React.ReactNode
|
|
palette: BuildPalette
|
|
onSelect?: (color: string, index: number) => void
|
|
isActive?: boolean
|
|
}
|
|
|
|
export const StepThemeHoverablePalette = memo((props: PaletteProps) => {
|
|
const { colors, size = 'medium', palette } = props
|
|
const borderRadius = size === 'medium' ? 200 : 100
|
|
const [tooltipLabel, setTooltipLabel] = useState('')
|
|
const scope = `palette-${palette.name}`
|
|
|
|
return (
|
|
<Tooltip scope={scope} offset={8} placement="top">
|
|
<XStack
|
|
flex={1}
|
|
flexBasis="auto"
|
|
rounded={borderRadius}
|
|
borderWidth={1}
|
|
borderColor="$color7"
|
|
>
|
|
{colors.map((color, i) => {
|
|
return (
|
|
<Tooltip.Trigger
|
|
scope={scope}
|
|
key={i}
|
|
asChild
|
|
onMouseEnter={() =>
|
|
setTooltipLabel(`${i + 1}: ${defaultScaleGrouped[i]?.name ?? color}`)
|
|
}
|
|
>
|
|
<XStack flex={1} width={`${(1 / colors.length) * 100}%`}>
|
|
<PaletteColor {...props} color={color} index={i} />
|
|
</XStack>
|
|
</Tooltip.Trigger>
|
|
)
|
|
})}
|
|
</XStack>
|
|
|
|
<Tooltip.Content
|
|
scope={scope}
|
|
animatePosition
|
|
transition="quick"
|
|
bg="$background"
|
|
elevation="$2"
|
|
rounded="$4"
|
|
px="$2.5"
|
|
py="$1"
|
|
enterStyle={{ y: 4, opacity: 0 }}
|
|
exitStyle={{ y: 4, opacity: 0 }}
|
|
>
|
|
<Tooltip.Arrow scope={scope} />
|
|
<Paragraph size="$3">{tooltipLabel}</Paragraph>
|
|
</Tooltip.Content>
|
|
</Tooltip>
|
|
)
|
|
})
|
|
|
|
// add some delay but no delay if moving across
|
|
|
|
const delay = 50
|
|
let hovered = -1
|
|
let tm
|
|
|
|
const mouseEnter = (i: number, name: string) => {
|
|
clearTimeout(tm)
|
|
const alreadyHovered = hovered !== -1
|
|
const paletteStore = getStore(PaletteStore, { name })
|
|
if (alreadyHovered) {
|
|
// right away
|
|
hovered = i
|
|
paletteStore.setHoveredColor(i)
|
|
return
|
|
}
|
|
tm = setTimeout(() => {
|
|
hovered = i
|
|
paletteStore.setHoveredColor(i)
|
|
}, delay)
|
|
}
|
|
|
|
const mouseLeave = (i: number) => {
|
|
clearTimeout(tm)
|
|
if (hovered === i) {
|
|
tm = setTimeout(() => {
|
|
hovered = -1
|
|
}, delay / 2)
|
|
}
|
|
}
|
|
|
|
const PaletteColor = memo(
|
|
(
|
|
props: PaletteProps & {
|
|
color: string
|
|
index: number
|
|
}
|
|
) => {
|
|
const {
|
|
color,
|
|
index,
|
|
colors,
|
|
size = 'medium',
|
|
children,
|
|
palette,
|
|
onSelect,
|
|
isActive,
|
|
} = props
|
|
const store = usePaletteStore(palette.name)
|
|
const { hoveredColor, selectedColor } = store
|
|
const borderRadius = size === 'medium' ? 100 : 100
|
|
const { anchors } = palette
|
|
const isAnchor = anchors.some((a) => a.index === index)
|
|
|
|
const doublePressProps = useDoublePress({
|
|
eagerSingle: true,
|
|
onSinglePress() {
|
|
store.setSelectedColor(index)
|
|
store.setHoveredColor(index)
|
|
},
|
|
onDoublePress() {
|
|
onSelect?.(color, index)
|
|
},
|
|
})
|
|
|
|
const radiusStyle = {
|
|
...(index === 0 && {
|
|
borderBottomLeftRadius: borderRadius - 1.333,
|
|
borderTopLeftRadius: borderRadius - 1.333,
|
|
}),
|
|
...(index === colors.length - 1 && {
|
|
borderTopRightRadius: borderRadius - 1.333,
|
|
borderBottomRightRadius: borderRadius - 1.333,
|
|
}),
|
|
}
|
|
|
|
return (
|
|
<XStack
|
|
height={isActive ? 42 : 26}
|
|
flex={1}
|
|
overflow="hidden"
|
|
borderWidth={2}
|
|
borderColor={color as any}
|
|
onMouseEnter={() => {
|
|
mouseEnter(index, palette.name)
|
|
}}
|
|
hoverStyle={{
|
|
scale: 1.05,
|
|
}}
|
|
position="relative"
|
|
{...(hoveredColor === index && {
|
|
z: 10000,
|
|
outlineColor: '$accent10',
|
|
outlineStyle: 'solid',
|
|
outlineWidth: 1.5,
|
|
shadowColor: '$blue10',
|
|
shadowRadius: 5,
|
|
shadowOpacity: 1,
|
|
})}
|
|
{...((isAnchor || selectedColor === index) && {
|
|
z: 10000,
|
|
outlineColor: '$accent10',
|
|
outlineStyle: 'solid',
|
|
outlineWidth: 2,
|
|
})}
|
|
{...(selectedColor === index && {
|
|
outlineColor: '$accent1',
|
|
})}
|
|
{...(selectedColor === hoveredColor &&
|
|
hoveredColor === index && {
|
|
shadowColor: '$blue10',
|
|
shadowRadius: 10,
|
|
shadowOpacity: 1,
|
|
z: 100000,
|
|
})}
|
|
{...radiusStyle}
|
|
{...doublePressProps}
|
|
onMouseLeave={() => {
|
|
mouseLeave(index)
|
|
doublePressProps.onMouseLeave()
|
|
if (store.hoveredColor === index) {
|
|
store.hoveredColor = store.selectedColor
|
|
}
|
|
}}
|
|
>
|
|
<XStack fullscreen bg={color as any} items="center" justify="center">
|
|
<SizableText
|
|
selectable={false}
|
|
color={index > 4 ? '$background' : '$color'}
|
|
size="$1"
|
|
scale={size === 'small' ? 0.8 : 1}
|
|
>
|
|
{children?.(color, index)}
|
|
</SizableText>
|
|
</XStack>
|
|
</XStack>
|
|
)
|
|
}
|
|
)
|
|
|
|
const PaletteIndices = () => (
|
|
<YStack my="$-3">
|
|
<XLabeledItem label="">
|
|
<XStack flex={1} flexBasis="auto">
|
|
{new Array(12).fill(0).map((_, i) => {
|
|
return (
|
|
<SizableText
|
|
width={`${1 / 12}%`}
|
|
flex={1}
|
|
key={i}
|
|
size="$1"
|
|
opacity={0.6}
|
|
scale={0.65}
|
|
fontFamily="$mono"
|
|
color="$color12"
|
|
self="center"
|
|
text="center"
|
|
>
|
|
{i + 1}
|
|
</SizableText>
|
|
)
|
|
})}
|
|
</XStack>
|
|
</XLabeledItem>
|
|
</YStack>
|
|
)
|