@wycm9527/morganite 0.0.0-stage → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/LICENSE +33 -0
  3. package/README.md +62 -2
  4. package/bridge/base.css +138 -0
  5. package/bridge/echarts.d.ts +44 -0
  6. package/bridge/echarts.js +217 -0
  7. package/bridge/element-plus.css +785 -0
  8. package/bridge/fonts.css +124 -0
  9. package/bridge/icon-merge.d.ts +5 -0
  10. package/bridge/icon-merge.js +103 -0
  11. package/bridge/iconpark.config.ts +75 -0
  12. package/bridge/iconpark.css +29 -0
  13. package/bridge/react/ConfirmBar.tsx +43 -0
  14. package/bridge/react/EChart.tsx +45 -0
  15. package/bridge/react/PageHeader.tsx +24 -0
  16. package/bridge/react/SectionHeader.tsx +17 -0
  17. package/bridge/react/StatCard.tsx +33 -0
  18. package/bridge/react/TableSkeleton.tsx +11 -0
  19. package/bridge/react/TrendChart.tsx +8 -0
  20. package/bridge/recipes.css +444 -0
  21. package/bridge/shadcn-globals.css +6 -0
  22. package/bridge/shadcn.css +238 -0
  23. package/bridge/vue/ConfirmBar.vue +23 -0
  24. package/bridge/vue/EChart.vue +43 -0
  25. package/bridge/vue/KitchenSink.vue +507 -0
  26. package/bridge/vue/PageHeader.vue +23 -0
  27. package/bridge/vue/SectionHeader.vue +15 -0
  28. package/bridge/vue/StatCard.vue +33 -0
  29. package/bridge/vue/TableSkeleton.vue +14 -0
  30. package/bridge/vue/TrendChart.vue +11 -0
  31. package/bridge/vue/confirm.js +18 -0
  32. package/bridge/vue/inlineConfirm.js +21 -0
  33. package/design-system/AUDIT.md +277 -0
  34. package/design-system/DESIGN.md +176 -0
  35. package/design-system/contrast.md +71 -0
  36. package/design-system/preview/drawer.html +38 -0
  37. package/design-system/preview/form.html +40 -0
  38. package/design-system/preview/index.html +21 -0
  39. package/design-system/preview/preview.css +190 -0
  40. package/design-system/preview/preview.local.css +17 -0
  41. package/design-system/preview/shell.html +50 -0
  42. package/design-system/scope-map.json +4 -0
  43. package/design-system/style-dictionary.config.mjs +172 -0
  44. package/design-system/theme-map.json +16 -0
  45. package/design-system/themes/dark/THEME.md +29 -0
  46. package/design-system/themes/dark/tokens/semantic.tokens.json +354 -0
  47. package/design-system/token-board.html +137 -0
  48. package/design-system/tokens/primitives.tokens.json +1898 -0
  49. package/design-system/tokens/semantic.tokens.json +1080 -0
  50. package/design-system.json +217 -0
  51. package/migration/roles.json +743 -0
  52. package/package.json +82 -4
  53. package/templates/AGENTS.md +13 -0
  54. package/templates/KitchenSink.tsx +179 -0
  55. package/templates/accept.config.mjs +28 -0
  56. package/templates/entry-css.css +6 -0
  57. package/templates/entry-element-plus.css +9 -0
  58. package/templates/entry-shadcn.css +7 -0
  59. package/templates/notes-css.md +4 -0
  60. package/templates/notes-element-plus.md +7 -0
  61. package/templates/notes-shadcn.md +7 -0
@@ -0,0 +1,217 @@
1
+ // ECharts 桥接:ECharts 画在 canvas 上,不读 CSS 变量。这里在运行时从 :root 读取 token 的计算值,生成 ECharts 主题与几条图表配方。
2
+ // 用法:
3
+ // import * as echarts from 'echarts/core'
4
+ // import { registerTheme, trendLine, areaGradient, rankBars, barGradient, withBarEnds } from '<seed>/bridge/echarts.js'
5
+ // const name = registerTheme(echarts) // 每次亮 / 暗切换后重新调用,再 dispose + init(el, name)
6
+ // const chart = echarts.init(el, name)
7
+ // chart.setOption(trendLine({ days, series: [{ name: '本周', data }, { name: '上周', data }] }))
8
+ // series: [{ type: 'bar', data, itemStyle: { color: barGradient(chartTokens().palette[0]) } }] // 自己写的柱状图也用柱子渐变
9
+ // chart.setOption(withBarEnds(option)) // 柱子端头;种子的 EChart 容器(vue / react)已自动套用,并在图例开关后重算
10
+ // 规则见 DESIGN.md「组件原则」图表条:轴线与网格线是 border.width.default 发丝线;趋势折线 y 轴贴数据(不强制从 0)、底线虚线、平滑 0.5 + smoothMonotone、单序列显示空心点并填渐隐面积、多序列不显示点也不填面积;柱状图必须从 0 开始,柱子起点(基线一端)直角、末端圆角,堆叠时段与段直接相接、只圆最外一段,柱子渐变起点深、末端浅(竖柱下深上浅),品牌金用 chart.gradient.start → end,其他颜色同色饱和度渐变;提示气泡是白底浮层(color.chart.tooltip 底、正文色字、细边、elevation.tooltip 大投影);悬停时浅金竖条(color.chart.area)盖住悬停的那一格,不画竖线。
11
+
12
+ const read = (name, el = document.documentElement) => getComputedStyle(el).getPropertyValue(name).trim();
13
+
14
+ /** 读出图表要用的 token 计算值(亮 / 暗切换后重新读) */
15
+ export function chartTokens(el = document.documentElement) {
16
+ const num = (v, d) => { const n = parseFloat(v); return Number.isFinite(n) ? n : d; };
17
+ return {
18
+ palette: [1, 2, 3, 4, 5, 6].map((i) => read(`--color-chart-${i}`, el)),
19
+ sequential: [1, 2, 3, 4, 5].map((i) => read(`--color-chart-sequential-${i}`, el)),
20
+ area: read('--color-chart-area', el),
21
+ text: read('--color-text-secondary', el), muted: read('--color-text-muted', el), primary: read('--color-text-primary', el),
22
+ line: read('--color-border-default', el), strong: read('--color-border-strong', el),
23
+ lineWidth: num(read('--border-width-default', el), 0.6), baselineDash: num(read('--chart-baseline-dash', el), 4),
24
+ surface: read('--color-bg-surface', el), inverseBg: read('--color-bg-inverse', el), inverseText: read('--color-text-inverse', el),
25
+ tooltipBg: read('--color-chart-tooltip', el) || read('--color-bg-elevated', el),
26
+ tooltipShadowY: num(read('--elevation-tooltip-y', el), 12), tooltipShadowBlur: num(read('--elevation-tooltip-blur', el), 32), tooltipShadowColor: read('--elevation-tooltip-color', el) || 'rgba(0, 0, 0, 0.12)',
27
+ tooltipRadius: num(read('--radius-md', el), 8), tooltipPadding: [num(read('--spacing-2', el), 8), num(read('--spacing-3', el), 12)],
28
+ fontFamily: read('--font-family-body', el), font: num(read('--text-small-size', el), 12),
29
+ numericFamily: read('--text-numeric-family', el) || read('--font-family-body', el),
30
+ radius: num(read('--radius-xs', el), 2),
31
+ gradientSaturation: num(read('--chart-gradient-saturation', el), 0.4),
32
+ gradientStart: read('--color-chart-gradient-start', el), gradientEnd: read('--color-chart-gradient-end', el),
33
+ };
34
+ }
35
+
36
+ /** HSB 明度不变、饱和度乘 k:每个通道向最大通道收拢(c → max − (max − c) × k);灰色饱和度为 0,原样返回 */
37
+ function desaturate(color, k) {
38
+ const c = color.trim();
39
+ let rgb, a = 1;
40
+ if (c.startsWith('#')) {
41
+ const h = c.length <= 5 ? c.slice(1).split('').map((x) => x + x).join('') : c.slice(1);
42
+ rgb = [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16));
43
+ if (h.length === 8) a = parseInt(h.slice(6, 8), 16) / 255;
44
+ } else {
45
+ const m = c.match(/rgba?\(([^)]+)\)/);
46
+ if (!m) return c;
47
+ const p = m[1].split(/[\s,/]+/).filter(Boolean).map(Number);
48
+ rgb = p.slice(0, 3);
49
+ if (p.length > 3) a = p[3];
50
+ }
51
+ const max = Math.max(...rgb);
52
+ const out = rgb.map((v) => Math.round(max - (max - v) * k));
53
+ return a < 1 ? `rgba(${out.join(', ')}, ${a})` : `rgb(${out.join(', ')})`;
54
+ }
55
+
56
+ /** 一根柱子的渐变,起点深、末端浅:竖柱底端深、顶端浅;横柱基线端(左)深、数值端(右)浅。
57
+ 品牌金(chart.1 / 顺序色封顶)用指定两色 chart.gradient.start → end;其他颜色同色饱和度渐变,深端是原色、浅端饱和度 × gradientSaturation */
58
+ function linearFill(color, t, horizontal) {
59
+ const gold = [t.palette[0], t.sequential[t.sequential.length - 1]].some((g) => g && g.toLowerCase() === color.trim().toLowerCase());
60
+ const [deep, light] = gold && t.gradientStart && t.gradientEnd ? [t.gradientStart, t.gradientEnd] : [color, desaturate(color, t.gradientSaturation)];
61
+ return horizontal
62
+ ? { type: 'linear', x: 0, y: 0, x2: 1, y2: 0, colorStops: [{ offset: 0, color: deep }, { offset: 1, color: light }] }
63
+ : { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [{ offset: 0, color: light }, { offset: 1, color: deep }] };
64
+ }
65
+
66
+ /**
67
+ * 柱状图的柱子渐变(DESIGN 图表条:起点深、末端浅,竖柱下深上浅、横柱左深右浅;品牌金用 chart.gradient.start → end,
68
+ * 其他颜色同色饱和度渐变——HSB 明度不变,浅端饱和度 × chart.gradient.saturation)。
69
+ * 返回 ECharts 线性渐变对象,series 级与数据项级的 itemStyle.color 都能用。
70
+ * @param {string} color token 读出的实色(如 chartTokens().palette[0];品牌金按这个值认出来)
71
+ * @param {{ horizontal?: boolean, el?: Element }} [opts] horizontal:横向柱
72
+ */
73
+ export function barGradient(color, { horizontal = false, el = document.documentElement } = {}) {
74
+ return linearFill(color, chartTokens(el), horizontal);
75
+ }
76
+
77
+ const FLIPPED = Symbol('morganite.bar-gradient-flipped');
78
+ /** 负值柱的渐变:柱子从基线往下(横柱往左)长,相对柱子自身的线性渐变要倒过来,才是基线一端深、末端浅。
79
+ 全局坐标的渐变不动;翻过的打记号,同一份 option 经过两次 barEnds(如 rankBars 的结果再进 EChart 容器)也只翻一次 */
80
+ function flipForNegative(fill) {
81
+ if (!fill || typeof fill !== 'object' || fill.type !== 'linear' || fill.global || fill[FLIPPED] || !Array.isArray(fill.colorStops)) return fill;
82
+ const colorStops = fill.colorStops.map((s) => ({ ...s, offset: 1 - s.offset })).reverse();
83
+ return { type: 'linear', x: fill.x, y: fill.y, x2: fill.x2, y2: fill.y2, colorStops, [FLIPPED]: true };
84
+ }
85
+
86
+ /**
87
+ * 柱子的端头:起点(基线一端)直角、末端圆角 radius.xs;负值柱的末端朝下(横柱朝左),渐变也跟着倒过来(仍是基线一端深)。
88
+ * 堆叠时段与段直接相接、接缝直角,只有每根柱子最外一段的末端圆角。最外一段按 ECharts 的堆叠规则算:
89
+ * 按 series 声明顺序往外堆(组里第一个可见序列设了 stackOrder: 'seriesDesc' 时反过来),正负值分开堆,值为 0 的段不算,
90
+ * 图例关掉的序列(selected[name] === false)不参与堆叠——所以图例开关后要带上新的 selected 再算一次。
91
+ * 返回新的 series 数组;非柱状、极坐标的 series 原样返回。数据写在 series.data 里(数字、[x, y] 或 { value }),dataset 写法算不了,只剩主题的默认端头。
92
+ * @param {object[]} series
93
+ * @param {{ horizontal?: boolean, selected?: Record<string, boolean>, el?: Element }} [opts] horizontal:横向柱(类目在 y 轴);selected:图例的开关状态
94
+ */
95
+ export function barEnds(series, { horizontal = false, selected = {}, el = document.documentElement } = {}) {
96
+ const r = chartTokens(el).radius;
97
+ const isBar = (s) => s && s.type === 'bar' && s.coordinateSystem !== 'polar';
98
+ const valueOf = (d) => {
99
+ const v = d !== null && typeof d === 'object' && !Array.isArray(d) ? d.value : d;
100
+ return Number(Array.isArray(v) ? v[horizontal ? 0 : 1] : v);
101
+ };
102
+ const groupOf = (s, si) => (s.stack ? `stack:${s.stack}` : `#${si}`);
103
+ const desc = new Map(); // 堆叠组 → 是否反向堆
104
+ const outer = new Map(); // 堆叠组 × 类目 × 正负方向 → 最外一段是哪个 series
105
+ series.forEach((s, si) => {
106
+ if (!isBar(s) || selected[s.name] === false) return;
107
+ const g = groupOf(s, si);
108
+ if (!desc.has(g)) desc.set(g, s.stackOrder === 'seriesDesc');
109
+ (s.data || []).forEach((d, i) => {
110
+ const v = valueOf(d);
111
+ const key = `${g}|${i}|${v > 0}`;
112
+ if (v && !(desc.get(g) && outer.has(key))) outer.set(key, si);
113
+ });
114
+ });
115
+ const [endUp, endDown] = horizontal ? [[0, r, r, 0], [r, 0, 0, r]] : [[r, r, 0, 0], [0, 0, r, r]];
116
+ return series.map((s, si) => (!isBar(s) || !Array.isArray(s.data) ? s : {
117
+ ...s,
118
+ data: s.data.map((d, i) => {
119
+ const v = valueOf(d);
120
+ const item = d !== null && typeof d === 'object' && !Array.isArray(d) ? d : { value: d };
121
+ if (!v) return item;
122
+ const end = outer.get(`${groupOf(s, si)}|${i}|${v > 0}`) === si ? (v > 0 ? endUp : endDown) : 0;
123
+ const itemStyle = { ...item.itemStyle, borderRadius: end };
124
+ if (v < 0) {
125
+ const fill = itemStyle.color ?? s.itemStyle?.color;
126
+ const flipped = flipForNegative(fill);
127
+ if (flipped !== fill) itemStyle.color = flipped;
128
+ }
129
+ return { ...item, itemStyle };
130
+ }),
131
+ }));
132
+ }
133
+
134
+ /**
135
+ * 整份 option 套 barEnds,方向按 y 轴是不是类目轴判断(整张图一个方向)。
136
+ * 种子的 EChart 容器每次 setOption 都经过这里,图例开关后带上 selected 再算一次;直接 echarts.init 的项目自己调。
137
+ * @param {object} option
138
+ * @param {{ selected?: Record<string, boolean>, el?: Element }} [opts]
139
+ */
140
+ export function withBarEnds(option, { selected, el = document.documentElement } = {}) {
141
+ const list = option && option.series ? [].concat(option.series) : [];
142
+ if (!list.some((s) => s && s.type === 'bar')) return option;
143
+ const horizontal = [].concat(option.yAxis ?? []).some((a) => a && a.type === 'category');
144
+ return { ...option, series: barEnds(list, { horizontal, selected, el }) };
145
+ }
146
+
147
+ /** ECharts 主题对象:轴、网格、图例、提示框、折线 / 柱 / 饼的默认形态 */
148
+ export function theme(el = document.documentElement) {
149
+ const t = chartTokens(el);
150
+ return {
151
+ color: t.palette,
152
+ backgroundColor: 'transparent',
153
+ textStyle: { fontFamily: t.fontFamily, color: t.text, fontSize: t.font },
154
+ legend: { icon: 'roundRect', itemWidth: 10, itemHeight: 10, itemGap: 16, textStyle: { color: t.text, fontSize: t.font } },
155
+ // 大投影走 elevation.tooltip:ECharts 把 shadow* 拼成 box-shadow,没有扩散半径,token 也只给 y / blur / color。
156
+ // 挂到 body(同 Element 浮层):el-card 是 overflow: hidden,靠边的气泡和投影会被卡片裁掉
157
+ // 悬停指示默认是浅金竖条(shadow):竖条宽度取类目间距,时间轴 / 数值轴上会缩成 1px,这类图自己设 axisPointer.type: 'line'
158
+ tooltip: { appendTo: 'body', backgroundColor: t.tooltipBg, borderColor: t.line, borderWidth: t.lineWidth, borderRadius: t.tooltipRadius, padding: t.tooltipPadding, shadowOffsetX: 0, shadowOffsetY: t.tooltipShadowY, shadowBlur: t.tooltipShadowBlur, shadowColor: t.tooltipShadowColor, textStyle: { color: t.primary, fontSize: t.font }, axisPointer: { type: 'shadow', lineStyle: { color: t.strong }, shadowStyle: { color: t.area } } },
159
+ categoryAxis: { axisLine: { lineStyle: { color: t.line, width: t.lineWidth } }, axisTick: { show: false }, axisLabel: { color: t.muted, fontSize: t.font }, splitLine: { show: false } },
160
+ valueAxis: { axisLine: { show: false }, axisTick: { show: false }, axisLabel: { color: t.muted, fontSize: t.font, fontFamily: t.numericFamily }, splitLine: { lineStyle: { color: t.line, width: t.lineWidth } } }, // 数值刻度用数字字体,类目轴多是中文,保持正文字体
161
+ // 平滑 0.5 + smoothMonotone 'x':曲线丝滑但不越过数据点(只用 0.3 会在数据点处显硬;不加 monotone 则会在点之间鼓包);线端与拐角圆头
162
+ line: { smooth: 0.5, smoothMonotone: 'x', symbol: 'emptyCircle', symbolSize: 6, showSymbol: false, lineStyle: { width: 2, cap: 'round', join: 'round' }, emphasis: { focus: 'series' } },
163
+ // 柱子起点(基线一端)直角、末端圆角:这里是竖柱正值的默认值;横柱、堆叠、负值的端头不同,交给 withBarEnds() 按每根柱子算(种子的 EChart 容器自动套用)
164
+ bar: { barMaxWidth: 24, itemStyle: { borderRadius: [t.radius, t.radius, 0, 0] } },
165
+ pie: { itemStyle: { borderColor: t.surface, borderWidth: 2 }, label: { color: t.text } },
166
+ };
167
+ }
168
+
169
+ /** 注册主题并返回主题名。亮 / 暗切换后再调用一次,然后 dispose 旧实例、用同名主题重新 init。 */
170
+ export function registerTheme(echarts, name = 'morganite', el = document.documentElement) {
171
+ echarts.registerTheme(name, theme(el));
172
+ return name;
173
+ }
174
+
175
+ /** 单序列折线的面积:从 chart.area 到透明的纵向渐变;多序列对比不要用面积 */
176
+ export function areaGradient(echarts, el = document.documentElement) {
177
+ const t = chartTokens(el);
178
+ return { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: t.area }, { offset: 1, color: 'transparent' }]) };
179
+ }
180
+
181
+ /**
182
+ * 趋势折线配方:类目 x 轴无留白,y 轴贴数据范围(scale),所有序列统一 2px 细线(主序列靠品牌金区分,不靠粗细);单序列显示空心点、可传渐隐面积;图例右上。
183
+ * 底线(x 轴线)是发丝细的虚线(chart.baseline.dash),其余网格线实线。
184
+ * 计数型趋势(申请数、订单数这类整数):y 轴从 0 起、刻度只取整数(minInterval 1)——否则会出现 0.5、1.5 这种"半张申请"的刻度;默认按数据自动判断(全是整数且最大值 ≤ 20),也可显式传 count。
185
+ * @param {{ days: string[], series: { name: string, data: number[] }[], area?: object, count?: boolean, el?: Element }} p area 只在单序列时传 areaGradient(echarts)
186
+ */
187
+ export function trendLine({ days, series, area, count, el = document.documentElement }) {
188
+ const t = chartTokens(el);
189
+ const values = series.flatMap((s) => s.data).filter((v) => v !== null && v !== undefined);
190
+ const isCount = count ?? (values.length > 0 && values.every((v) => Number.isInteger(v)) && Math.max(...values) <= 20);
191
+ const splitLine = { showMinLine: false }; // 最底那条网格线和 x 轴线重合,留着会在虚线下面垫一条实线
192
+ return {
193
+ grid: { left: 8, right: 8, top: 36, bottom: 0, containLabel: true },
194
+ legend: { top: 0, right: 0 },
195
+ tooltip: { trigger: 'axis' },
196
+ xAxis: { type: 'category', data: days, boundaryGap: false, axisLine: { lineStyle: { type: [t.baselineDash, t.baselineDash] } } },
197
+ yAxis: isCount ? { type: 'value', min: 0, minInterval: 1, splitNumber: 4, splitLine } : { type: 'value', scale: true, splitNumber: 4, splitLine },
198
+ series: series.map((s) => ({ name: s.name, type: 'line', data: s.data, showSymbol: series.length === 1, areaStyle: area && series.length === 1 ? area : undefined })), // 线宽走主题(2px),不按序列加粗
199
+ };
200
+ }
201
+
202
+ /**
203
+ * 排行横向柱状配方(DESIGN「数据可视化」:排行榜前三名品牌金、其余中性灰——单序列强度按离散分段,不在灰与金之间插值)。
204
+ * categories 与 values 已按大小排好序(第一项是第一名);value 为 0 的项不着色为金。
205
+ * @param {{ categories: string[], values: number[], top?: number, formatter?: (v: number) => string, el?: Element }} p
206
+ */
207
+ export function rankBars({ categories, values, top = 3, formatter, el = document.documentElement }) {
208
+ const t = chartTokens(el);
209
+ const hi = t.sequential[t.sequential.length - 1], lo = t.sequential[1]; // 封顶的品牌金 / 灰阶第二档
210
+ return {
211
+ grid: { left: 8, right: 96, top: 8, bottom: 8, containLabel: true }, // 右侧留出金额标签(¥12,345.00)的宽度
212
+ tooltip: { trigger: 'axis', axisPointer: { type: 'none' }, valueFormatter: formatter },
213
+ xAxis: { type: 'value', min: 0, axisLabel: { show: false }, splitLine: { show: false } },
214
+ yAxis: { type: 'category', data: categories.slice().reverse(), axisLine: { show: false } }, // 第一名在最上
215
+ series: barEnds([{ type: 'bar', data: values.slice().reverse().map((v, i, arr) => ({ value: v, itemStyle: { color: linearFill(arr.length - 1 - i < top && v > 0 ? hi : lo, t, true) } })), barMaxWidth: 16, label: { show: true, position: 'right', color: t.text, fontSize: t.font, fontFamily: t.numericFamily, formatter: formatter ? (p) => formatter(p.value) : undefined } }], { horizontal: true, el }),
216
+ };
217
+ }