mirror of
https://github.com/rememberber/MooTool.git
synced 2026-09-01 04:57:49 +08:00
[next]opt:调色板收藏逻辑有问题,没有收藏按钮,参考Java版实现,而且检查下各主题色内容是否有和Java不一致的,包括顺序
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import { ArrowLeftRight, Copy, Eye, History, Palette, Star } from 'lucide-react'
|
||||
import { ArrowLeftRight, BookMarked, Copy, Eye, History, Palette, Star } from 'lucide-react'
|
||||
import { useEffect, useEffectEvent, useMemo, useState } from 'react'
|
||||
import { FavoriteDialog } from '@/features/favorites/FavoriteDialog'
|
||||
import { HistoryDialog } from '@/features/history/HistoryDialog'
|
||||
import { ResizableColumns } from '@/shared/components/ResizableColumns'
|
||||
import { ToolPageHeader, WorkspaceDragZone } from '@/shared/components/ToolPage'
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
type ColorThemeId,
|
||||
type RgbColor
|
||||
} from './colorTools'
|
||||
import { ColorFavoritesDialog, SaveColorFavoriteDialog } from './ColorFavoriteDialogs'
|
||||
|
||||
export function ColorBoardTool() {
|
||||
const { t } = useI18n()
|
||||
@@ -28,6 +28,7 @@ export function ColorBoardTool() {
|
||||
const [code, setCode] = useState('#DE8F7D')
|
||||
const [theme, setTheme] = useState<ColorThemeId>('default')
|
||||
const [historyOpen, setHistoryOpen] = useState(false)
|
||||
const [saveFavoriteOpen, setSaveFavoriteOpen] = useState(false)
|
||||
const [favoritesOpen, setFavoritesOpen] = useState(false)
|
||||
const selectedTheme = useMemo(() => colorThemes.find((item) => item.id === theme) ?? colorThemes[0], [theme])
|
||||
const primaryHex = formatColor(primary, 'HEX_UPPER')
|
||||
@@ -101,7 +102,8 @@ export function ColorBoardTool() {
|
||||
<label className="color-code-input"><span>{t('color.code')}</span><input value={code} spellCheck={false} onChange={(event) => setCode(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') applyCode() }} /></label>
|
||||
<button className="toolbar-button toolbar-button--icon" type="button" aria-label={t('common.action.copy')} onClick={() => { void actions.copy(code) }}><Copy size={14} /></button>
|
||||
<WorkspaceDragZone className="p4-toolbar__spacer" />
|
||||
<button className="toolbar-button" type="button" onClick={() => setFavoritesOpen(true)}><Star size={14} />{t('favorite.title')}</button>
|
||||
<button className="toolbar-button" type="button" onClick={() => setSaveFavoriteOpen(true)}><Star size={14} />{t('color.favorite')}</button>
|
||||
<button className="toolbar-button" type="button" onClick={() => setFavoritesOpen(true)}><BookMarked size={14} />{t('color.favorites')}</button>
|
||||
<button className="toolbar-button" type="button" onClick={() => setHistoryOpen(true)}><History size={14} />{t('common.action.history')}</button>
|
||||
</div>
|
||||
<ResizableColumns className="color-board-layout" columns={2} defaultSizes={[0.34, 0.66]} minPaneWidths={[240, 420]} storageKey="color-board">
|
||||
@@ -113,14 +115,19 @@ export function ColorBoardTool() {
|
||||
<section className="color-palette-panel">
|
||||
<header><h2>{t('color.themeColors')}</h2><label><Palette size={14} /><select aria-label={t('color.theme')} value={theme} onChange={(event) => setTheme(event.target.value as ColorThemeId)}>{colorThemes.map((item) => <option key={item.id} value={item.id}>{t(`color.theme.${item.id}` as 'color.theme.default')}</option>)}</select></label></header>
|
||||
<div className="theme-main-colors">{selectedTheme.main.map((color) => <ColorChip key={color} color={color} onSelect={(secondarySelection) => selectColor(color, secondarySelection)} />)}</div>
|
||||
<div className="theme-shade-grid">{selectedTheme.shades.flatMap((row, column) => row.map((color) => <ColorChip key={`${column}-${color}`} color={color} onSelect={(secondarySelection) => selectColor(color, secondarySelection)} />))}</div>
|
||||
<div className="theme-shade-grid">{selectedTheme.shades.map((column, columnIndex) => (
|
||||
<div className="theme-shade-column" key={`${selectedTheme.id}-${columnIndex}`}>
|
||||
{column.map((color, rowIndex) => <ColorChip key={`${rowIndex}-${color}`} color={color} onSelect={(secondarySelection) => selectColor(color, secondarySelection)} />)}
|
||||
</div>
|
||||
))}</div>
|
||||
<h2>{t('color.standardColors')}</h2>
|
||||
<div className="standard-colors">{standardColors.map((color) => <ColorChip key={color} color={color} onSelect={(secondarySelection) => selectColor(color, secondarySelection)} />)}</div>
|
||||
<p className="color-shift-hint">{t('color.shiftHint')}</p>
|
||||
</section>
|
||||
</ResizableColumns>
|
||||
</div>
|
||||
<FavoriteDialog kind="color" open={favoritesOpen} currentValue={primaryHex} onClose={() => setFavoritesOpen(false)} onApply={(value) => selectColor(value, false, t('favorite.title'))} />
|
||||
<SaveColorFavoriteDialog color={primaryHex} open={saveFavoriteOpen} onClose={() => setSaveFavoriteOpen(false)} />
|
||||
<ColorFavoritesDialog open={favoritesOpen} onClose={() => setFavoritesOpen(false)} onApply={(value) => selectColor(value, false, t('color.favorites'))} />
|
||||
<HistoryDialog funcType="colorBoard" open={historyOpen} onClose={() => setHistoryOpen(false)} onApply={(value) => selectColor(value)} onApplyRecord={(record) => {
|
||||
const color = (record.outputText || record.inputText).match(/#[0-9a-fA-F]{6}/)?.[0]
|
||||
if (color) selectColor(color, false, record.summary)
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { Star, Trash2 } from 'lucide-react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Dialog } from '@/shared/components/Dialog'
|
||||
import type { FavoriteRecord } from '@/shared/contracts/favorites'
|
||||
import { useToast } from '@/shared/feedback/ToastProvider'
|
||||
import { useI18n } from '@/shared/i18n/I18nProvider'
|
||||
|
||||
export function SaveColorFavoriteDialog({ color, open, onClose }: {
|
||||
color: string
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const [name, setName] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (open) setName(`Color-${color}`)
|
||||
}, [color, open])
|
||||
|
||||
async function save(): Promise<void> {
|
||||
if (!name.trim()) return
|
||||
await window.mootool.saveFavorite({ kind: 'color', name: name.trim(), value: color })
|
||||
toast.success(t('favorite.saved'))
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={t('color.favoriteDialog')}
|
||||
open={open}
|
||||
width={420}
|
||||
onClose={onClose}
|
||||
footer={(
|
||||
<>
|
||||
<button className="dialog-button" type="button" onClick={onClose}>{t('common.cancel')}</button>
|
||||
<button className="dialog-button dialog-button--primary" type="button" disabled={!name.trim()} onClick={() => { void save() }}>
|
||||
<Star size={14} />{t('color.favorite')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
>
|
||||
<div className="color-favorite-preview">
|
||||
<span style={{ background: color }} />
|
||||
<strong>{color}</strong>
|
||||
</div>
|
||||
<label className="color-favorite-name">
|
||||
<span>{t('common.name')}</span>
|
||||
<input
|
||||
autoFocus
|
||||
value={name}
|
||||
aria-label={t('common.name')}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
onKeyDown={(event) => { if (event.key === 'Enter') void save() }}
|
||||
/>
|
||||
</label>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export function ColorFavoritesDialog({ open, onClose, onApply }: {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
onApply: (value: string) => void
|
||||
}) {
|
||||
const { t } = useI18n()
|
||||
const toast = useToast()
|
||||
const [records, setRecords] = useState<FavoriteRecord[]>([])
|
||||
const load = useCallback(async () => setRecords(await window.mootool.listFavorites('color')), [])
|
||||
|
||||
useEffect(() => { if (open) void load() }, [load, open])
|
||||
|
||||
async function remove(id: number): Promise<void> {
|
||||
await window.mootool.deleteFavorite(id)
|
||||
await load()
|
||||
toast.success(t('favorite.deleted'))
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={t('color.favorites')}
|
||||
open={open}
|
||||
width={620}
|
||||
onClose={onClose}
|
||||
footer={<button className="dialog-button" type="button" onClick={onClose}>{t('common.close')}</button>}
|
||||
>
|
||||
<div className="favorite-list color-favorite-list">
|
||||
{records.length === 0 ? <div className="history-empty">{t('favorite.empty')}</div> : records.map((record) => (
|
||||
<article className="favorite-item color-favorite-item" key={record.id}>
|
||||
<button type="button" onClick={() => { onApply(record.value); onClose() }}>
|
||||
<span className="color-favorite-item__swatch" style={{ background: record.value }} />
|
||||
<span className="color-favorite-item__text"><strong>{record.name}</strong><code>{record.value}</code></span>
|
||||
</button>
|
||||
<button className="icon-button" type="button" aria-label={t('common.action.delete')} onClick={() => { void remove(record.id) }}><Trash2 size={14} /></button>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { applyColorOperation, bestTextColor, formatColor, parseColor } from './colorTools'
|
||||
import { applyColorOperation, bestTextColor, colorThemes, formatColor, parseColor, standardColors } from './colorTools'
|
||||
|
||||
describe('color tools', () => {
|
||||
it('parses short/long hex and RGB strings', () => {
|
||||
@@ -23,4 +24,11 @@ describe('color tools', () => {
|
||||
expect(bestTextColor(parseColor('#ffffff'))).toBe('#000000')
|
||||
expect(bestTextColor(parseColor('#111111'))).toBe('#FFFFFF')
|
||||
})
|
||||
|
||||
it('keeps every Java theme color and its column order', () => {
|
||||
expect(colorThemes.map((theme) => theme.id)).toEqual(['default', 'theme1', 'theme2', 'theme3', 'theme4', 'theme5', 'china'])
|
||||
expect(colorThemes.every((theme) => theme.main.length === 10 && theme.shades.length === 10 && theme.shades.every((column) => column.length === 5))).toBe(true)
|
||||
expect(createHash('sha256').update(JSON.stringify(colorThemes)).digest('hex')).toBe('72e2218c6dcffce0def99f1317075f2041127035730a0379875f5e875e893575')
|
||||
expect(createHash('sha256').update(JSON.stringify(standardColors)).digest('hex')).toBe('1994345359abd00b901c22614eeef6b400775120cebc04083e05bab9875b535c')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -4,23 +4,129 @@ export type ColorOperation = 'invert' | 'intersect' | 'add' | 'difference' | 'av
|
||||
|
||||
export const standardColors = ['#C00000', '#FF0000', '#FFC000', '#FFFF00', '#92D050', '#00B050', '#00B0F0', '#0070C0', '#002060', '#7030A0']
|
||||
|
||||
const themeMain = {
|
||||
default: ['#000000', '#FFFFFF', '#880015', '#ED1C24', '#FF7F27', '#FFF200', '#22B14C', '#00A2E8', '#3F48CC', '#A349A4'],
|
||||
theme1: ['#FFFFFF', '#000000', '#1F497D', '#EEECE1', '#4F81BD', '#C0504D', '#9BBB59', '#8064A2', '#4BACC6', '#F79646'],
|
||||
theme2: ['#FFFFFF', '#000000', '#69676D', '#C9C2D1', '#CEB966', '#9CB084', '#6BB1C9', '#6585CF', '#7E6BC9', '#A379BB'],
|
||||
theme3: ['#FFFFFF', '#000000', '#323232', '#E3DED1', '#F07F09', '#9F2936', '#1B587C', '#4E8542', '#604878', '#C19859'],
|
||||
theme4: ['#FFFFFF', '#000000', '#646B86', '#C5D1D7', '#D16349', '#CCB400', '#8CADAE', '#8C7B70', '#8FB08C', '#D19049'],
|
||||
theme5: ['#FFFFFF', '#000000', '#464646', '#DEF5FA', '#2DA2BF', '#DA1F28', '#EB641B', '#39639D', '#474B78', '#7D3C4A'],
|
||||
china: ['#FFFEF9', '#3D3B4F', '#9D2933', '#FF461F', '#C91F37', '#CA6924', '#F0C239', '#789262', '#177CB0', '#815463']
|
||||
} as const
|
||||
export type ColorThemeId = 'default' | 'theme1' | 'theme2' | 'theme3' | 'theme4' | 'theme5' | 'china'
|
||||
|
||||
export type ColorThemeId = keyof typeof themeMain
|
||||
type ColorTheme = {
|
||||
id: ColorThemeId
|
||||
main: readonly string[]
|
||||
shades: ReadonlyArray<readonly string[]>
|
||||
}
|
||||
|
||||
export const colorThemes: Array<{ id: ColorThemeId; main: string[]; shades: string[][] }> = Object.entries(themeMain).map(([id, main]) => ({
|
||||
id: id as ColorThemeId,
|
||||
main: [...main],
|
||||
shades: main.map((color) => createShades(color))
|
||||
}))
|
||||
// Keep these fixed values and their column order aligned with Java ColorConsts.
|
||||
export const colorThemes: readonly ColorTheme[] = [
|
||||
{
|
||||
id: 'default',
|
||||
main: ['#000000', '#FFFFFF', '#880015', '#ED1C24', '#FF7F27', '#FFF200', '#22B14C', '#00A2E8', '#3F48CC', '#A349A4'],
|
||||
shades: [
|
||||
['#808080', '#595959', '#404040', '#262626', '#0D0D0D'],
|
||||
['#F2F2F2', '#D9D9D9', '#BFBFBF', '#A6A6A6', '#808080'],
|
||||
['#E7B9C0', '#CF7C89', '#B84A5B', '#660010', '#44000A'],
|
||||
['#FBCFD0', '#F8A1A4', '#F47378', '#B21016', '#77070B'],
|
||||
['#FFE5D4', '#FFCCA9', '#FFB27D', '#BF5B16', '#803A0A'],
|
||||
['#FFFCCC', '#FFFA99', '#FFF766', '#BFB500', '#807900'],
|
||||
['#C8EFD4', '#98E0AD', '#6BD089', '#138535', '#085820'],
|
||||
['#C8EBFA', '#94D8F6', '#60C5F1', '#007AAE', '#005174'],
|
||||
['#D3D5F5', '#AAAEEB', '#8389E0', '#232B99', '#101566'],
|
||||
['#EDD3ED', '#DAAADB', '#C785C8', '#7A297B', '#511252']
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'theme1',
|
||||
main: ['#FFFFFF', '#000000', '#1F497D', '#EEECE1', '#4F81BD', '#C0504D', '#9BBB59', '#8064A2', '#4BACC6', '#F79646'],
|
||||
shades: [
|
||||
['#F2F2F2', '#D9D9D9', '#BFBFBF', '#A6A6A6', '#808080'],
|
||||
['#808080', '#595959', '#404040', '#262626', '#0D0D0D'],
|
||||
['#C3D2E5', '#8EA9CB', '#6185B1', '#11345E', '#08203E'],
|
||||
['#D6D1B6', '#B2AA7E', '#776D38', '#3C350E', '#181502'],
|
||||
['#D6E3F2', '#B0C8E5', '#8CAED7', '#2D598E', '#14365F'],
|
||||
['#F2D6D5', '#E6B0AF', '#D98D8B', '#902E2B', '#601513'],
|
||||
['#E9F1D8', '#D4E4B4', '#C0D693', '#6F8C32', '#465D16'],
|
||||
['#E2DAEC', '#C8B9DA', '#AE99C7', '#553879', '#321951'],
|
||||
['#D6EEF4', '#AEDCE8', '#8BCCDD', '#2A7E95', '#135263'],
|
||||
['#FDE9D9', '#FCD4B4', '#FABF8F', '#B96927', '#7C4212']
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'theme2',
|
||||
main: ['#FFFFFF', '#000000', '#69676D', '#C9C2D1', '#CEB966', '#9CB084', '#6BB1C9', '#6585CF', '#7E6BC9', '#A379BB'],
|
||||
shades: [
|
||||
['#F2F2F2', '#D9D9D9', '#BFBFBF', '#A6A6A6', '#808080'],
|
||||
['#808080', '#595959', '#404040', '#262626', '#0D0D0D'],
|
||||
['#E0E0E2', '#C2C1C5', '#A3A1A7', '#423A52', '#231A36'],
|
||||
['#AB9DBC', '#846D9D', '#4A3068', '#1F0C34', '#0B0215'],
|
||||
['#F5F0DC', '#EBE1BC', '#E2D49E', '#9B873A', '#67571A'],
|
||||
['#EAEFE3', '#D5DFC9', '#C2D0B1', '#6A844A', '#3F5821'],
|
||||
['#DDEEF4', '#BDDEE9', '#A0CFDF', '#3C8097', '#1B5164'],
|
||||
['#DCE3F5', '#BCCAEC', '#9DB2E2', '#39569B', '#193168'],
|
||||
['#E2DDF4', '#C6BDE9', '#ADA0DF', '#4F3C97', '#291B64'],
|
||||
['#EBE0F1', '#D8C4E4', '#C6A9D6', '#72448C', '#461E5D']
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'theme3',
|
||||
main: ['#FFFFFF', '#000000', '#323232', '#E3DED1', '#F07F09', '#9F2936', '#1B587C', '#4E8542', '#604878', '#C19859'],
|
||||
shades: [
|
||||
['#F2F2F2', '#D9D9D9', '#BFBFBF', '#A6A6A6', '#808080'],
|
||||
['#808080', '#595959', '#404040', '#262626', '#0D0D0D'],
|
||||
['#D6D6D6', '#ADADAD', '#848484', '#262626', '#191919'],
|
||||
['#CCC2A9', '#AA9B75', '#716034', '#392D0D', '#171102'],
|
||||
['#FCE4CB', '#F9CA99', '#F6B168', '#B45E05', '#783E02'],
|
||||
['#ECC9CD', '#D999A0', '#C56D77', '#771722', '#500A12'],
|
||||
['#C1D8E5', '#8BB3CB', '#5D91B0', '#0F405D', '#07293E'],
|
||||
['#D4E7D0', '#ACCEA4', '#89B67F', '#306425', '#1A4311'],
|
||||
['#DBD2E4', '#B9A9C9', '#9984AE', '#41285A', '#27123C'],
|
||||
['#F3E9D9', '#E6D2B4', '#DABE94', '#916C32', '#604316']
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'theme4',
|
||||
main: ['#FFFFFF', '#000000', '#646B86', '#C5D1D7', '#D16349', '#CCB400', '#8CADAE', '#8C7B70', '#8FB08C', '#D19049'],
|
||||
shades: [
|
||||
['#F2F2F2', '#D9D9D9', '#BFBFBF', '#A6A6A6', '#808080'],
|
||||
['#808080', '#595959', '#404040', '#262626', '#0D0D0D'],
|
||||
['#DBDEE7', '#BABECF', '#9AA0B6', '#394265', '#192243'],
|
||||
['#9FB6C1', '#6F90A1', '#31586C', '#0C2836', '#020F16'],
|
||||
['#F6DCD6', '#EDBBAF', '#E39B8A', '#9D3F29', '#682312'],
|
||||
['#F5EFC4', '#EBE08D', '#E0D05A', '#998700', '#665A00'],
|
||||
['#E6EFEF', '#CEDEDF', '#B6CDCE', '#4E8082', '#235557'],
|
||||
['#E8E2DF', '#D1C7C0', '#BAACA4', '#69503F', '#462D1C'],
|
||||
['#E6EFE5', '#CEDFCD', '#B9D0B6', '#53844F', '#275823'],
|
||||
['#F6E7D6', '#EDD0AF', '#E3B98A', '#9D6629', '#683F12']
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'theme5',
|
||||
main: ['#FFFFFF', '#000000', '#464646', '#DEF5FA', '#2DA2BF', '#DA1F28', '#EB641B', '#39639D', '#474B78', '#7D3C4A'],
|
||||
shades: [
|
||||
['#F2F2F2', '#D9D9D9', '#BFBFBF', '#A6A6A6', '#808080'],
|
||||
['#808080', '#595959', '#404040', '#262626', '#0D0D0D'],
|
||||
['#DADADA', '#B5B5B5', '#909090', '#353535', '#232323'],
|
||||
['#B4D9E1', '#7DB0BB', '#37717D', '#0E353E', '#021519'],
|
||||
['#CDEBF2', '#9FD7E5', '#75C5D9', '#19788F', '#0B4F60'],
|
||||
['#F8CDD0', '#F09EA2', '#E97177', '#A31118', '#6D080D'],
|
||||
['#FBDECF', '#F7BEA0', '#F39F72', '#B0480F', '#752D07'],
|
||||
['#CDDAEB', '#A1B8D8', '#7999C4', '#204476', '#0E294F'],
|
||||
['#D1D3E4', '#A8ABC9', '#8387AE', '#282C5A', '#12153C'],
|
||||
['#E5CDD2', '#CBA1AA', '#B17A86', '#5E222F', '#3E0F19']
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'china',
|
||||
main: ['#FFFEF9', '#3D3B4F', '#9D2933', '#FF461F', '#C91F37', '#CA6924', '#F0C239', '#789262', '#177CB0', '#815463'],
|
||||
shades: [
|
||||
['#FFFEFD', '#FFFEF9', '#FFF8EF', '#FFF2E5', '#FFE8D5'],
|
||||
['#D5D4DC', '#ACABB9', '#828190', '#59576C', '#3D3B4F'],
|
||||
['#EBD4D6', '#D7A9AD', '#C37E84', '#AF535C', '#7A1E26'],
|
||||
['#FFE2D9', '#FFBFAD', '#FF9C81', '#FF7955', '#CC2E08'],
|
||||
['#F2D0D4', '#E5A1A9', '#D8727E', '#B83D4D', '#8F1525'],
|
||||
['#F5E6D9', '#EBCCB3', '#E1B28D', '#C48542', '#8F5118'],
|
||||
['#FCF3D4', '#F8E7A9', '#F4DB7E', '#D9AE2A', '#A67E15'],
|
||||
['#E5EDE0', '#CBDBC1', '#B1C9A2', '#93A97E', '#5A7348'],
|
||||
['#D5EBF4', '#ABDAEA', '#81C9E0', '#45A8CC', '#0E5F85'],
|
||||
['#E5D8DD', '#CBACB8', '#B18099', '#946B7B', '#5C3445']
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export function parseColor(input: string): RgbColor {
|
||||
const value = input.trim().replace(/,/g, ',')
|
||||
@@ -56,20 +162,6 @@ export function bestTextColor(color: RgbColor): '#000000' | '#FFFFFF' {
|
||||
return luminance > 0.55 ? '#000000' : '#FFFFFF'
|
||||
}
|
||||
|
||||
function createShades(hex: string): string[] {
|
||||
const color = parseColor(hex)
|
||||
return [0.78, 0.55, 0.3].map((ratio) => formatColor(blend(color, { r: 255, g: 255, b: 255 }, ratio), 'HEX_UPPER'))
|
||||
.concat([0.25, 0.5].map((ratio) => formatColor(blend(color, { r: 0, g: 0, b: 0 }, ratio), 'HEX_UPPER')))
|
||||
}
|
||||
|
||||
function blend(color: RgbColor, target: RgbColor, ratio: number): RgbColor {
|
||||
return {
|
||||
r: Math.round(color.r + (target.r - color.r) * ratio),
|
||||
g: Math.round(color.g + (target.g - color.g) * ratio),
|
||||
b: Math.round(color.b + (target.b - color.b) * ratio)
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(value: number): number {
|
||||
return Math.min(255, Math.max(0, Math.round(value)))
|
||||
}
|
||||
|
||||
@@ -688,6 +688,9 @@ export const messages = {
|
||||
,'color.format': '格式'
|
||||
,'color.code': '颜色代码'
|
||||
,'color.current': '当前颜色'
|
||||
,'color.favorite': '收藏'
|
||||
,'color.favorites': '收藏夹'
|
||||
,'color.favoriteDialog': '颜色收藏'
|
||||
,'color.compare': '对比色'
|
||||
,'color.operation.invert': '反色'
|
||||
,'color.operation.intersect': '交集'
|
||||
@@ -698,12 +701,12 @@ export const messages = {
|
||||
,'color.themeColors': '主题色'
|
||||
,'color.theme': '主题'
|
||||
,'color.theme.default': '默认'
|
||||
,'color.theme.theme1': '主题 1'
|
||||
,'color.theme.theme2': '主题 2'
|
||||
,'color.theme.theme3': '主题 3'
|
||||
,'color.theme.theme4': '主题 4'
|
||||
,'color.theme.theme5': '主题 5'
|
||||
,'color.theme.china': '中国传统色'
|
||||
,'color.theme.theme1': '主题1'
|
||||
,'color.theme.theme2': '主题2'
|
||||
,'color.theme.theme3': '主题3'
|
||||
,'color.theme.theme4': '主题4'
|
||||
,'color.theme.theme5': '主题5'
|
||||
,'color.theme.china': '中国色'
|
||||
,'color.standardColors': '标准色'
|
||||
,'color.shiftHint': '按住 Shift 选择对比色。'
|
||||
,'qrcode.title': '二维码'
|
||||
@@ -1925,6 +1928,9 @@ export const messages = {
|
||||
,'color.format': 'Format'
|
||||
,'color.code': 'Color code'
|
||||
,'color.current': 'Current color'
|
||||
,'color.favorite': 'Favorite'
|
||||
,'color.favorites': 'Favorites'
|
||||
,'color.favoriteDialog': 'Save Color'
|
||||
,'color.compare': 'Comparison color'
|
||||
,'color.operation.invert': 'Invert'
|
||||
,'color.operation.intersect': 'Intersect'
|
||||
@@ -1940,7 +1946,7 @@ export const messages = {
|
||||
,'color.theme.theme3': 'Theme 3'
|
||||
,'color.theme.theme4': 'Theme 4'
|
||||
,'color.theme.theme5': 'Theme 5'
|
||||
,'color.theme.china': 'Traditional Chinese'
|
||||
,'color.theme.china': 'Chinese Colors'
|
||||
,'color.standardColors': 'Standard colors'
|
||||
,'color.shiftHint': 'Hold Shift to select the comparison color.'
|
||||
,'qrcode.title': 'QR Code'
|
||||
@@ -3162,6 +3168,9 @@ export const messages = {
|
||||
,'color.format': '形式'
|
||||
,'color.code': 'カラーコード'
|
||||
,'color.current': '現在の色'
|
||||
,'color.favorite': 'お気に入りに追加'
|
||||
,'color.favorites': 'お気に入り'
|
||||
,'color.favoriteDialog': '色を保存'
|
||||
,'color.compare': '比較色'
|
||||
,'color.operation.invert': '反転'
|
||||
,'color.operation.intersect': '交差'
|
||||
@@ -3172,12 +3181,12 @@ export const messages = {
|
||||
,'color.themeColors': 'テーマカラー'
|
||||
,'color.theme': 'テーマ'
|
||||
,'color.theme.default': 'デフォルト'
|
||||
,'color.theme.theme1': 'テーマ 1'
|
||||
,'color.theme.theme2': 'テーマ 2'
|
||||
,'color.theme.theme3': 'テーマ 3'
|
||||
,'color.theme.theme4': 'テーマ 4'
|
||||
,'color.theme.theme5': 'テーマ 5'
|
||||
,'color.theme.china': '中国伝統色'
|
||||
,'color.theme.theme1': 'テーマ1'
|
||||
,'color.theme.theme2': 'テーマ2'
|
||||
,'color.theme.theme3': 'テーマ3'
|
||||
,'color.theme.theme4': 'テーマ4'
|
||||
,'color.theme.theme5': 'テーマ5'
|
||||
,'color.theme.china': '中国色'
|
||||
,'color.standardColors': '標準色'
|
||||
,'color.shiftHint': 'Shift を押しながら比較色を選択します。'
|
||||
,'qrcode.title': 'QR コード'
|
||||
|
||||
@@ -6489,10 +6489,80 @@ select:not([multiple]) {
|
||||
.theme-shade-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, minmax(22px, 1fr));
|
||||
gap: 4px;
|
||||
gap: 5px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.theme-shade-column {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(5, auto);
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.color-favorite-preview {
|
||||
display: grid;
|
||||
grid-template-columns: 80px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.color-favorite-preview > span {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border: 1px solid var(--border-control);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.color-favorite-preview strong {
|
||||
color: var(--text-body);
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.color-favorite-name {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.color-favorite-name input {
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
color: var(--text-body);
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border-control);
|
||||
border-radius: 6px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.color-favorite-name input:focus {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 18%, transparent);
|
||||
}
|
||||
|
||||
.color-favorite-item > button:first-child {
|
||||
display: grid;
|
||||
grid-template-columns: 36px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.color-favorite-item__swatch {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--border-control);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.color-favorite-item__text {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.color-chip {
|
||||
width: 100%;
|
||||
min-width: 22px;
|
||||
|
||||
@@ -967,16 +967,28 @@ test('runs P4 reformat, crypto, Protobuf and QR workflows', async () => {
|
||||
|
||||
test('runs P4 color favorites, image persistence and PDF workspace', async () => {
|
||||
await openTool('调色板', '调色板')
|
||||
await expect(mainPage.getByLabel('主题').locator('option')).toHaveText(['默认', '主题1', '主题2', '主题3', '主题4', '主题5', '中国色'])
|
||||
await expect(mainPage.locator('.theme-shade-column')).toHaveCount(10)
|
||||
expect(await mainPage.locator('.theme-shade-column').first().locator('.color-chip').evaluateAll((chips) => chips.map((chip) => chip.getAttribute('aria-label')))).toEqual(['#808080', '#595959', '#404040', '#262626', '#0D0D0D'])
|
||||
const colorCode = mainPage.locator('.color-code-input input')
|
||||
await colorCode.fill('#123456')
|
||||
await colorCode.press('Enter')
|
||||
await expect(mainPage.locator('.color-preview strong')).toHaveText('#123456')
|
||||
await mainPage.getByRole('button', { name: '收藏夹' }).click()
|
||||
await expect(mainPage.getByRole('button', { name: '收藏', exact: true })).toBeVisible()
|
||||
await mainPage.getByRole('button', { name: '收藏', exact: true }).click()
|
||||
const saveColorFavorite = mainPage.getByRole('dialog', { name: '颜色收藏' })
|
||||
await expect(saveColorFavorite.getByLabel('名称')).toHaveValue('Color-#123456')
|
||||
await saveColorFavorite.getByLabel('名称').fill('E2E color')
|
||||
await saveColorFavorite.getByRole('button', { name: '收藏', exact: true }).click()
|
||||
await expect(saveColorFavorite).toHaveCount(0)
|
||||
|
||||
await colorCode.fill('#654321')
|
||||
await colorCode.press('Enter')
|
||||
await mainPage.getByRole('button', { name: '收藏夹', exact: true }).click()
|
||||
const colorFavorites = mainPage.getByRole('dialog', { name: '收藏夹' })
|
||||
await colorFavorites.getByLabel('名称').fill('E2E color')
|
||||
await colorFavorites.getByRole('button', { name: '收藏当前内容' }).click()
|
||||
await expect(colorFavorites.locator('.favorite-item')).toContainText('E2E color')
|
||||
await colorFavorites.getByRole('button', { name: '关闭' }).click()
|
||||
await colorFavorites.locator('.favorite-item > button').first().click()
|
||||
await expect(mainPage.locator('.color-preview strong')).toHaveText('#123456')
|
||||
|
||||
const imageName = await mainPage.evaluate(async () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
|
||||
Reference in New Issue
Block a user