Files
workbuddy_skills/oil-ui-pro/assets/cards/gallery-palette.js
T
admin d9445b5888 oil-ui-pro 0.15.4 → 0.22.2(上游 ui.oiloil.org 正式包)+ 记 P0-98 跨盘 rename 坑
- oil-ui-pro 升到 0.22.2:references 13→28 份(新增 16、删 imagery.md)、assets 增 cards/(22 组)与 fonts/、SKILL.md 顶层五步结构未改名。
- 升级路径:官方 `npx github:oil-oil/oil-cli update` 稳定失败(error=replace、报「检查目录权限」)。
  真身为 EXDEV —— 解压在 os.tmpdir()(C 盘)、技能目录在 E 盘,Windows 上 rename 不能跨盘;
  CLI 的 catch 把非 CliError 原生异常一律兜底成 replaceFailed,真错误一个字不吐。
  做法=借其 prepare() 只做下载+校验,再 cp 到同盘 stage、rename 交换;官方命令复验 up_to_date。
- 回归:product-planning 按名引用的 visual-review.md/tools.md/design-direction.md/style-explorer.md 全部健在;命名闸门 2061|0|27。
- session-mechanism/references/pitfalls.md 新增 P0-98(含通则:更新器把原生错误兜底成指错方向的人话)。
2026-10-14 10:21:46 +08:00

48 lines
3.7 KiB
JavaScript

// The editor owns its committed palette; incomplete HEX input is only a draft.
export function createPaletteEditor({query, onChange}) {
const root=document.querySelector('.palette-editor'),modeControl=root.querySelector('#palette-mode');
const slots=[...root.querySelectorAll('[data-color-slot]')],feedback=root.querySelector('.palette-feedback');
const defaults={mode:'single',colors:['#5865c7','#9aa5e8','#d6c9b7']},key='oil.cards.palette.v1';
const modes={single:1,duo:2,trio:3};
const hex=value=>{
if(typeof value!=='string')return null;
const raw=value.trim().replace(/^#/,'');
if(/^[a-f\d]{3}$/i.test(raw))return '#'+[...raw].map(c=>c+c).join('').toLowerCase();
return /^[a-f\d]{6}$/i.test(raw)?'#'+raw.toLowerCase():null;
};
let state={mode:defaults.mode,colors:[...defaults.colors]},frame=0;
try {const saved=JSON.parse(localStorage.getItem(key));if(Object.hasOwn(modes,saved?.mode)&&Array.isArray(saved.colors)&&saved.colors.length===3&&saved.colors.every(hex))state={mode:saved.mode,colors:saved.colors.map(hex)};}catch{}
// Existing screenshot URLs remain valid; explicit URL colors override saved input.
const legacy={dawn:['#7a5cff','#ff4f8b','#ff9a4a'],sage:['#4c9a94','#81bca2','#c7d5aa'],mono:['#44454c','#929399','#dedfe2'],clay:['#ac715d','#cf9d85','#e6caae']};
const requestedPreset=query.get('preset')||query.get('state');
if(Object.hasOwn(legacy,requestedPreset))state={mode:'trio',colors:[...legacy[requestedPreset]]};
const requested=query.get('colors')?.split(',');
if(requested?.length>=1&&requested.length<=3&&requested.every(hex))state={mode:['single','duo','trio'][requested.length-1],colors:[...requested.map(hex),...defaults.colors.slice(requested.length)]};
const controller=new AbortController(),listen=(el,event,fn)=>el.addEventListener(event,fn,{signal:controller.signal});
const save=()=>{try{localStorage.setItem(key,JSON.stringify(state));}catch{}};
const render=()=>{
modeControl.value=state.mode;
slots.forEach((slot,i)=>{slot.hidden=i>=modes[state.mode];slot.querySelector('input[type=color]').value=state.colors[i];const text=slot.querySelector('.palette-hex');text.value=state.colors[i].toUpperCase();text.removeAttribute('aria-invalid');});
feedback.textContent='';
};
const refresh=()=>{save();if(!frame)frame=requestAnimationFrame(()=>{frame=0;onChange();});};
const showErrors=()=>{
const invalid=slots.some((slot,i)=>i<modes[state.mode]&&slot.querySelector('.palette-hex').getAttribute('aria-invalid')==='true');
feedback.textContent=invalid?'请输入 3 或 6 位 HEX 色值。':'';
};
slots.forEach((slot,i)=>{
const color=slot.querySelector('input[type=color]'),text=slot.querySelector('.palette-hex');
listen(color,'input',()=>{state.colors[i]=color.value;text.value=color.value.toUpperCase();text.removeAttribute('aria-invalid');showErrors();refresh();});
listen(text,'input',()=>{const value=hex(text.value);text.setAttribute('aria-invalid',String(!value));if(value){state.colors[i]=value;color.value=value;refresh();}showErrors();});
listen(text,'blur',()=>{const value=hex(text.value);if(value)text.value=value.toUpperCase();});
listen(text,'keydown',e=>{if(e.key==='Escape'){text.value=state.colors[i].toUpperCase();text.removeAttribute('aria-invalid');showErrors();}if(e.key==='Enter')text.blur();});
});
listen(modeControl,'change',()=>{state.mode=modeControl.value;render();refresh();});
listen(root.querySelector('.palette-reset'),'click',()=>{state={mode:defaults.mode,colors:[...defaults.colors]};render();refresh();});
render();
return {
get colors(){const [a,b,c]=state.colors;return state.mode==='single'?[a,a,a]:state.mode==='duo'?[a,b,b]:[a,b,c];},
destroy(){controller.abort();cancelAnimationFrame(frame);}
};
}