[next]opt:调色板收藏逻辑有问题,没有收藏按钮,参考Java版实现,而且检查下各主题色内容是否有和Java不一致的,包括顺序

This commit is contained in:
RememBerBer
2026-08-27 08:06:45 +08:00
parent fb10df4b6d
commit cd28340128
7 changed files with 351 additions and 53 deletions
+12 -5
View File
@@ -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>
)
}
+9 -1
View File
@@ -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')
})
})
+121 -29
View File
@@ -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)))
}
+22 -13
View File
@@ -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 コード'
+71 -1
View File
@@ -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;
+16 -4
View File
@@ -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')