iter(g332): 主题工具下沉到组件库
This commit is contained in:
@@ -3374,6 +3374,15 @@
|
|||||||
"motivation": "editable 与列的 render 都刚落地,但 Agent 拿到两个组件未必拼得对:把草稿信号当数据源(Esc 取消后表格显示未保存的值)、提交后整表换源冲掉别的格草稿、用 format 塞 DOM、在格内点击里又触发整行回调——这四条都是拼装顺序问题,不是组件问题,正好该由配方承接。",
|
"motivation": "editable 与列的 render 都刚落地,但 Agent 拿到两个组件未必拼得对:把草稿信号当数据源(Esc 取消后表格显示未保存的值)、提交后整表换源冲掉别的格草稿、用 format 塞 DOM、在格内点击里又触发整行回调——这四条都是拼装顺序问题,不是组件问题,正好该由配方承接。",
|
||||||
"acceptance": "① recipes.md 新增配方 22(编号连续、含最小骨架与要点),骨架用 dataTable({ data: rows }) + 列 render 包 editable,草稿信号与数据源分离;② 要点覆盖「唯一数据源 / render 只决定画什么 / 整表换源的副作用 / 校验口径与 form 一致 / 格内点击被拦 / 纯文本列仍用 format」六条;③ ua recipes 22 能取出骨架与组件清单,MCP get_recipe 同源可见;④ npm run check 全绿。",
|
"acceptance": "① recipes.md 新增配方 22(编号连续、含最小骨架与要点),骨架用 dataTable({ data: rows }) + 列 render 包 editable,草稿信号与数据源分离;② 要点覆盖「唯一数据源 / render 只决定画什么 / 整表换源的副作用 / 校验口径与 form 一致 / 格内点击被拦 / 纯文本列仍用 format」六条;③ ua recipes 22 能取出骨架与组件清单,MCP get_recipe 同源可见;④ npm run check 全绿。",
|
||||||
"notes": ""
|
"notes": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "g332",
|
||||||
|
"domain": "tuning",
|
||||||
|
"status": "open",
|
||||||
|
"title": "主题工具下沉:URL 参数 / 记忆 / 跟随系统三档只写一次",
|
||||||
|
"motivation": "演示站自己写了二十行主题样板(读 ?theme= 参数、读 localStorage、判系统偏好、写 data-theme、落盘),任何接入方都得再抄一遍——抄漏「跟随系统」就永远停在默认亮色,抄漏「只记显式选择」则用户切过一次就再也不会跟着系统变。这类样板属于库该提供的能力,而不是使用说明里的一段示例。",
|
||||||
|
"acceptance": "① 新增 theme(options) 工具(skin 包导出,走 --ua-* 令牌,不新增组件):优先级 URL 参数 > 本地记忆 > 跟随系统,mode='system' 时监听 prefers-color-scheme 并在变化时换色;② 返回 { mode, dark, set, toggle, dispose },dark 是可直接绑 switchButton 的布尔信号;③ 只有显式选择才落盘,system 档不写记忆;④ 演示站改用 theme({ storageKey: 'ua-demo-theme' }),删掉原先手写的样板且行为不变(?theme=dark 联动仍在);⑤ manifest 导出说明补齐,npm run check 全绿。",
|
||||||
|
"notes": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "ui-arch-registry-index/0.1",
|
"$schema": "ui-arch-registry-index/0.1",
|
||||||
"generatedAt": "2026-10-26T18:02:35.274Z",
|
"generatedAt": "2026-10-26T18:05:09.130Z",
|
||||||
"repoRoot": ".",
|
"repoRoot": ".",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
@@ -234,7 +234,8 @@
|
|||||||
"noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报",
|
"noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报",
|
||||||
"sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄",
|
"sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄",
|
||||||
"masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列",
|
"masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列",
|
||||||
"editable(props):行内编辑,静止态是文字、点一下就地变输入框,Enter 提交 / Esc 取消 / 失焦提交"
|
"editable(props):行内编辑,静止态是文字、点一下就地变输入框,Enter 提交 / Esc 取消 / 失焦提交",
|
||||||
|
"theme(options):主题工具(非组件),URL 参数 > 本地记忆 > 跟随系统,写 html[data-theme] 换令牌,返回 { mode, dark, set, toggle, dispose }"
|
||||||
],
|
],
|
||||||
"keyTypes": [
|
"keyTypes": [
|
||||||
"ButtonProps",
|
"ButtonProps",
|
||||||
|
|||||||
+5
-10
@@ -64,6 +64,7 @@ import {
|
|||||||
menu,
|
menu,
|
||||||
masonry,
|
masonry,
|
||||||
editable,
|
editable,
|
||||||
|
theme,
|
||||||
modal,
|
modal,
|
||||||
mentions,
|
mentions,
|
||||||
multiSelect,
|
multiSelect,
|
||||||
@@ -326,15 +327,9 @@ const orgTree = [
|
|||||||
{ key: 'qa', label: '质量中心', children: [{ key: 'reg', label: '回归测试', disabled: true }] },
|
{ key: 'qa', label: '质量中心', children: [{ key: 'reg', label: '回归测试', disabled: true }] },
|
||||||
];
|
];
|
||||||
|
|
||||||
// —— 主题:优先读 URL 参数(文档站 iframe 联动入口),其次上次选择,最后跟随系统;写 html[data-theme] 让令牌换色 ——
|
// —— 主题:URL 参数(文档站 iframe 联动)> 上次选择 > 跟随系统;优先级、落盘与系统偏好监听都收在 theme() 里 ——
|
||||||
const THEME_KEY = 'ua-demo-theme';
|
const th = theme({ storageKey: 'ua-demo-theme' }); // 与文档站 iframe 共用 ?theme=dark|light 入口
|
||||||
const themeParam = new URLSearchParams(location.search).get('theme'); // ?theme=dark|light,iframe 带参重载时生效
|
const dark = th.dark; // 实际生效是否暗色:system 档下也跟随系统,控件直接绑它
|
||||||
const dark = signal(((themeParam === 'dark' || themeParam === 'light' ? themeParam : null) ?? localStorage.getItem(THEME_KEY) ?? (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')) === 'dark');
|
|
||||||
effect(() => {
|
|
||||||
const mode = dark() ? 'dark' : 'light';
|
|
||||||
document.documentElement.dataset.theme = mode; // 令牌按 data-theme 切换
|
|
||||||
localStorage.setItem(THEME_KEY, mode); // 记住选择,刷新不闪
|
|
||||||
});
|
|
||||||
|
|
||||||
// —— 拖拽排序演示:顺序写回信号,下方摘要随之刷新 ——
|
// —— 拖拽排序演示:顺序写回信号,下方摘要随之刷新 ——
|
||||||
const sortableOrder = signal(['需求评审', '接口联调', '灰度发布', '线上回归']);
|
const sortableOrder = signal(['需求评审', '接口联调', '灰度发布', '线上回归']);
|
||||||
@@ -349,7 +344,7 @@ const content = h(
|
|||||||
h('div', null,
|
h('div', null,
|
||||||
h('h1', null, '@ui-arch 组件库 · 现场演示'),
|
h('h1', null, '@ui-arch 组件库 · 现场演示'),
|
||||||
h('p', { class: 'sub' }, '驱动:@ui-arch/frame(自研微框架) + @ui-arch/ui(组件库) + @ui-arch/table-core(表格内核)')),
|
h('p', { class: 'sub' }, '驱动:@ui-arch/frame(自研微框架) + @ui-arch/ui(组件库) + @ui-arch/table-core(表格内核)')),
|
||||||
switchButton({ label: '暗色主题', checked: dark }), // 切换 html[data-theme],令牌整套换色
|
switchButton({ label: '暗色主题', checked: dark, onChange: (v) => th.set(v ? 'dark' : 'light') }), // 切换 html[data-theme],令牌整套换色(记忆写回由 theme 负责)
|
||||||
),
|
),
|
||||||
section(
|
section(
|
||||||
'sec-counter',
|
'sec-counter',
|
||||||
|
|||||||
+2
-1
@@ -1,6 +1,6 @@
|
|||||||
# @ui-arch 组件库 Agent 文档(无图形版)
|
# @ui-arch 组件库 Agent 文档(无图形版)
|
||||||
|
|
||||||
> 自动生成于 2026-10-26T18:02:35.274Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
> 自动生成于 2026-10-26T18:05:09.130Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。
|
||||||
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
> 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。
|
||||||
|
|
||||||
## 快速上手
|
## 快速上手
|
||||||
@@ -2686,6 +2686,7 @@ API 速览:
|
|||||||
- sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄
|
- sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄
|
||||||
- masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列
|
- masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列
|
||||||
- editable(props):行内编辑,静止态是文字、点一下就地变输入框,Enter 提交 / Esc 取消 / 失焦提交
|
- editable(props):行内编辑,静止态是文字、点一下就地变输入框,Enter 提交 / Esc 取消 / 失焦提交
|
||||||
|
- theme(options):主题工具(非组件),URL 参数 > 本地记忆 > 跟随系统,写 html[data-theme] 换令牌,返回 { mode, dark, set, toggle, dispose }
|
||||||
|
|
||||||
一行用法:
|
一行用法:
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,7 @@
|
|||||||
- sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄
|
- sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄
|
||||||
- masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列
|
- masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列
|
||||||
- editable(props):行内编辑,静止态是文字、点一下就地变输入框,Enter 提交 / Esc 取消 / 失焦提交
|
- editable(props):行内编辑,静止态是文字、点一下就地变输入框,Enter 提交 / Esc 取消 / 失焦提交
|
||||||
|
- theme(options):主题工具(非组件),URL 参数 > 本地记忆 > 跟随系统,写 html[data-theme] 换令牌,返回 { mode, dark, set, toggle, dispose }
|
||||||
- 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')`
|
- 用法: `const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')`
|
||||||
- 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
|
- 约定: 皮肤不持有业务状态,一律委托 headless 内核或调用方信号
|
||||||
- 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
|
- 约定: 表单类组件统一约定:*Props.value/checked/active 为 Signal 双向绑定,onChange/onInput 为回调
|
||||||
|
|||||||
@@ -90,3 +90,4 @@ export { noticeBar } from './noticeBar.js';
|
|||||||
export { sortableList } from './sortableList.js';
|
export { sortableList } from './sortableList.js';
|
||||||
export { masonry } from './masonry.js';
|
export { masonry } from './masonry.js';
|
||||||
export { editable } from './editable.js';
|
export { editable } from './editable.js';
|
||||||
|
export { theme, currentThemeDark } from './theme.js';
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { effect, signal } from '@ui-arch/frame';
|
||||||
|
import type { ThemeHandle, ThemeMode, ThemeOptions } from './types.js';
|
||||||
|
|
||||||
|
/*** 主题:把「URL 参数 > 本地记忆 > 跟随系统」的优先级收进一处,写 html[data-theme] 换整包令牌;接入方的二十行样板(读参数 / 读记忆 / 听系统 / 落盘)只剩一行调用 ***/
|
||||||
|
export function theme(options: ThemeOptions = {}): ThemeHandle {
|
||||||
|
// —— 状态集中声明 ——
|
||||||
|
const key = options.storageKey ?? 'ua-theme'; // 本地记忆键
|
||||||
|
const param = options.param ?? 'theme'; // URL 参数名,值 light / dark 时生效(iframe 联动入口)
|
||||||
|
const darkName = options.dark ?? 'dark'; // 写进 data-theme 的值
|
||||||
|
const media = typeof matchMedia === 'function' ? matchMedia('(prefers-color-scheme: dark)') : null;
|
||||||
|
const systemDark = signal(media?.matches ?? false); // 系统偏好落成信号,mode='system' 时它才是真相
|
||||||
|
const mode = signal<ThemeMode>(options.initial ?? readEnv(param, key)); // 初始选择:环境优先,兜底 system
|
||||||
|
const dark = signal(resolveDark(mode(), systemDark())); // 实际生效是否暗色(写给出品方直接绑控件)
|
||||||
|
|
||||||
|
/*** 系统偏好变化:只在跟随系统时影响视觉,显式选过的不被系统改回去 ***/
|
||||||
|
const onSystem = (e: MediaQueryListEvent): void => { systemDark(e.matches); };
|
||||||
|
media?.addEventListener('change', onSystem);
|
||||||
|
|
||||||
|
effect(() => {
|
||||||
|
const now = resolveDark(mode(), systemDark()); // 读两个依赖:选择变了或系统偏好变了都要重算
|
||||||
|
dark(now);
|
||||||
|
document.documentElement.dataset.theme = now ? darkName : ''; // 亮色靠 :root 缺省,清掉属性即可
|
||||||
|
if (mode() !== 'system') localStorage.setItem(key, mode()); // 只记显式选择:跟随系统不该被冻成某一档
|
||||||
|
});
|
||||||
|
|
||||||
|
/*** 切到指定档:'system' 交还给系统偏好,其余落盘后刷新时仍生效 ***/
|
||||||
|
const set = (m: ThemeMode): void => { mode(m); };
|
||||||
|
|
||||||
|
/*** 明暗互换:控件只有一个开关时用它,system 态下按当前生效值反着切 ***/
|
||||||
|
const toggle = (): void => { mode(dark() ? 'light' : 'dark'); };
|
||||||
|
|
||||||
|
/*** 摘监听:单页应用卸载前调用,避免重复实例各挂一份 media 监听 ***/
|
||||||
|
const dispose = (): void => { media?.removeEventListener('change', onSystem); };
|
||||||
|
|
||||||
|
return { mode, dark, set, toggle, dispose };
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 环境里的初值:URL 参数(联动入口)> 本地记忆 > 跟随系统 ***/
|
||||||
|
function readEnv(param: string, key: string): ThemeMode {
|
||||||
|
const fromUrl = new URLSearchParams(location.search).get(param);
|
||||||
|
if (fromUrl === 'dark' || fromUrl === 'light') return fromUrl; // 卫语句:参数是合法档位才认
|
||||||
|
const stored = localStorage.getItem(key);
|
||||||
|
return stored === 'dark' || stored === 'light' ? stored : 'system'; // 记住的选择优先于系统偏好
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 选择 + 系统偏好 → 是否暗色:显式档直接给答案,system 档才看系统 ***/
|
||||||
|
function resolveDark(mode: ThemeMode, systemDark: boolean): boolean {
|
||||||
|
return mode === 'system' ? systemDark : mode === 'dark';
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 读当前生效值(不建实例):只要一个「现在是不是暗色」的场景用得上,比如按主题切图表配色 ***/
|
||||||
|
export function currentThemeDark(darkName = 'dark'): boolean {
|
||||||
|
return document.documentElement.dataset.theme === darkName;
|
||||||
|
}
|
||||||
@@ -1170,3 +1170,23 @@ export interface EditableProps {
|
|||||||
validate?: (v: string) => string | null; // 提交校验:返回文案即拒绝提交、保持编辑态并标红;null 表示通过
|
validate?: (v: string) => string | null; // 提交校验:返回文案即拒绝提交、保持编辑态并标红;null 表示通过
|
||||||
onCommit?: (v: string) => void; // 提交回调(值已写回信号之后触发)
|
onCommit?: (v: string) => void; // 提交回调(值已写回信号之后触发)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** 主题档位:显式亮 / 暗,或交还给系统偏好 ***/
|
||||||
|
export type ThemeMode = 'light' | 'dark' | 'system';
|
||||||
|
|
||||||
|
/*** 主题工具入参 ***/
|
||||||
|
export interface ThemeOptions {
|
||||||
|
storageKey?: string; // 本地记忆键;缺省 ua-theme
|
||||||
|
param?: string; // URL 参数名,值 light / dark 时生效(iframe 联动入口);缺省 theme
|
||||||
|
dark?: string; // 写进 html[data-theme] 的暗色值;缺省 dark
|
||||||
|
initial?: ThemeMode; // 强制初值,忽略 URL 与本地记忆;缺省按「参数 > 记忆 > 系统」解析
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** 主题工具句柄 ***/
|
||||||
|
export interface ThemeHandle {
|
||||||
|
mode: Signal<ThemeMode>; // 当前选择(light / dark / system),可读也可直接写
|
||||||
|
dark: Signal<boolean>; // 实际生效是否暗色:system 档下跟随系统偏好,可直接绑 switchButton 的 checked
|
||||||
|
set: (m: ThemeMode) => void; // 切到指定档;非 system 档会落盘
|
||||||
|
toggle: () => void; // 明暗互换(只有一个开关时用)
|
||||||
|
dispose: () => void; // 摘掉系统偏好监听
|
||||||
|
}
|
||||||
|
|||||||
@@ -104,7 +104,8 @@
|
|||||||
"noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报",
|
"noticeBar(props):单行通知条,语义色 + 左侧符号 + 可选跑马灯与关闭,适合页顶常驻播报",
|
||||||
"sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄",
|
"sortableList(props):拖拽排序列表,整行可拖 + Alt+↑↓ 键盘搬位,返回 { el, move, order } 句柄",
|
||||||
"masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列",
|
"masonry(props):瀑布流多列布局,列宽自适应或固定列数,卡片不被拆到两列",
|
||||||
"editable(props):行内编辑,静止态是文字、点一下就地变输入框,Enter 提交 / Esc 取消 / 失焦提交"
|
"editable(props):行内编辑,静止态是文字、点一下就地变输入框,Enter 提交 / Esc 取消 / 失焦提交",
|
||||||
|
"theme(options):主题工具(非组件),URL 参数 > 本地记忆 > 跟随系统,写 html[data-theme] 换令牌,返回 { mode, dark, set, toggle, dispose }"
|
||||||
],
|
],
|
||||||
"keyTypes": [
|
"keyTypes": [
|
||||||
"ButtonProps",
|
"ButtonProps",
|
||||||
|
|||||||
Reference in New Issue
Block a user