@linhey/rhine-design 0.8.0 → 0.9.1
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.
- package/CHANGELOG.md +15 -0
- package/README.md +3 -1
- package/components/AdvancedForms.tsx +40 -10
- package/components/ListFilterBar.tsx +104 -0
- package/components/OrbitBackdrop.tsx +9 -3
- package/components/README.md +3 -2
- package/components/SPACING.md +6 -0
- package/components/components.tsx +5 -2
- package/components/extensions.css +10 -0
- package/components/index.ts +2 -1
- package/components/list-filter-bar.css +33 -0
- package/components/orbit.css +21 -0
- package/components/styles.css +1 -0
- package/components/timeline.css +2 -1
- package/docs/APPLICATION_COMPONENTS.md +4 -0
- package/docs/BACKGROUND_FILTER_VERIFICATION.md +12 -0
- package/docs/COMPACT_ACCOUNT_PATTERNS.md +3 -1
- package/docs/COMPACT_LAYOUT_VERIFICATION.md +53 -0
- package/docs/DESIGN_SYSTEM.md +6 -0
- package/docs/INTERACTION_PATTERNS.md +11 -1
- package/docs/LIST_FILTER_BAR.md +76 -0
- package/docs/ORBIT_BACKDROP.md +25 -0
- package/docs/assets/compact-layouts/account.png +0 -0
- package/docs/assets/compact-layouts/date-arknights.png +0 -0
- package/docs/assets/compact-layouts/date-neo-brutalism.png +0 -0
- package/docs/assets/compact-layouts/date-rhine.png +0 -0
- package/docs/assets/compact-layouts/table.png +0 -0
- package/package.json +1 -1
- package/preview/BackgroundFiltersFixture.tsx +28 -0
- package/preview/CompactAccountGallery.tsx +18 -18
- package/preview/DateRangeFilterGallery.tsx +18 -1
- package/preview/GalleryShell.tsx +1 -1
- package/preview/ListFilterBarGallery.tsx +170 -0
- package/preview/OrbitGallery.tsx +14 -0
- package/preview/SpacingGallery.tsx +15 -1
- package/preview/main.tsx +4 -1
- package/preview/preview.css +24 -2
- package/scripts/check-background-filters.js +58 -0
- package/scripts/check-background-motion.js +169 -0
- package/scripts/check-compact-layouts.js +136 -0
- package/scripts/check-filter-native-date.js +47 -0
- package/skills/rhine-design/SKILL.md +3 -3
- package/skills/rhine-design/references/API_MAP.md +1 -1
- package/skills/rhine-design/references/integration.md +1 -1
- package/tests/CompactAccountGallery.test.tsx +2 -1
- package/tests/DateNativeInput.test.tsx +147 -0
- package/tests/DesignSystem.test.tsx +12 -1
- package/tests/ListFilterBar.test.tsx +198 -0
- package/tests/OrbitBackdrop.test.tsx +28 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Run against a fixed production preview on 18484 with playwright-cli run-code --filename.
|
|
2
|
+
async page => {
|
|
3
|
+
const errors=[],failures=[]; let filterCases=0,orbitCases=0;
|
|
4
|
+
page.on('pageerror',e=>errors.push(String(e)));
|
|
5
|
+
const check=(ok,detail)=>{if(!ok) failures.push(detail);};
|
|
6
|
+
const horizontal=async root=>root.evaluate(e=>{const r=e.getBoundingClientRect();return r.left>=-1&&r.right<=innerWidth+1&&e.scrollWidth<=e.clientWidth+1;});
|
|
7
|
+
for(const theme of ['rhine','arknights','neo-brutalism']) for(const width of [400,520,880]) for(const zoom of [1,2]) {
|
|
8
|
+
await page.setViewportSize({width,height:1000});
|
|
9
|
+
await page.goto(`http://127.0.0.1:18484/?fixture=list-filters&theme=${theme}&zoom=${zoom}`);
|
|
10
|
+
const bars=page.locator('.rhine-list-filter-bar');
|
|
11
|
+
for(let i=0;i<3;i++) {
|
|
12
|
+
const bar=bars.nth(i), trigger=bar.getByRole('button',{name:/^更多筛选/});
|
|
13
|
+
if(i<2) check(await bar.locator('.rhine-list-filter-bar__search label').evaluate(e=>{const r=e.getBoundingClientRect();return r.width>20&&r.height>10;}),{theme,width,zoom,i,kind:'visible-search-label'});
|
|
14
|
+
check(await bar.getByRole('combobox').count()===2,{theme,width,zoom,i,kind:'primary-limit'});
|
|
15
|
+
await trigger.click(); const dialog=page.getByRole('dialog');
|
|
16
|
+
await dialog.evaluate(e=>Promise.all(e.getAnimations().map(a=>a.finished)));
|
|
17
|
+
check(await horizontal(dialog),{theme,width,zoom,i,kind:'drawer-overflow'});
|
|
18
|
+
if(i<2) await dialog.getByRole('checkbox',{name:'只看已审查'}).check();
|
|
19
|
+
else await dialog.getByRole('combobox',{name:'统计范围'}).selectOption({label:'示例团队 / 跨部门体验与可访问性协作小组'});
|
|
20
|
+
await page.keyboard.press('Escape'); await dialog.waitFor({state:'detached'});
|
|
21
|
+
await page.waitForFunction(e=>e===document.activeElement,await trigger.elementHandle());
|
|
22
|
+
check(await trigger.evaluate(e=>e===document.activeElement),{theme,width,zoom,i,kind:'return-focus'});
|
|
23
|
+
check(await horizontal(bar),{theme,width,zoom,i,kind:'summary-overflow'});
|
|
24
|
+
const chip=bar.getByRole('button',{name:/^清除/}).first(); await chip.click();
|
|
25
|
+
check(await bar.evaluate(e=>e.contains(document.activeElement)),{theme,width,zoom,i,kind:'clear-focus'});
|
|
26
|
+
await page.getByRole('combobox',{name:'示例状态'}).selectOption('loading');
|
|
27
|
+
await trigger.click();
|
|
28
|
+
check(await dialog.getByRole('status').textContent().then(t=>t.includes('正在更新结果')),{theme,width,zoom,i,kind:'pending'});
|
|
29
|
+
await page.keyboard.press('Escape'); await dialog.waitFor({state:'detached'});
|
|
30
|
+
await page.getByRole('combobox',{name:'示例状态'}).selectOption('error');
|
|
31
|
+
await trigger.click(); check(await dialog.getByText('未能更新结果',{exact:true}).isVisible(),{theme,width,zoom,i,kind:'error'});
|
|
32
|
+
await page.keyboard.press('Escape'); await dialog.waitFor({state:'detached'});
|
|
33
|
+
await page.getByRole('combobox',{name:'示例状态'}).selectOption('empty');
|
|
34
|
+
check(await bar.getByRole('status').textContent().then(t=>/0 条|无用量/.test(t)),{theme,width,zoom,i,kind:'empty'});
|
|
35
|
+
await page.getByRole('combobox',{name:'示例状态'}).selectOption('disabled');
|
|
36
|
+
check(await bar.getByRole('button',{name:'刷新',exact:true}).isDisabled(),{theme,width,zoom,i,kind:'disabled'});
|
|
37
|
+
await page.getByRole('combobox',{name:'示例状态'}).selectOption('normal');
|
|
38
|
+
filterCases++;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for(const theme of ['rhine','arknights','neo-brutalism']) for(const width of [400,520,880,1280]) for(const zoom of [1,2]) {
|
|
42
|
+
await page.setViewportSize({width,height:1000});
|
|
43
|
+
await page.goto(`http://127.0.0.1:18484/?fixture=orbit-entry&theme=${theme}&zoom=${zoom}`);
|
|
44
|
+
const input=page.getByRole('textbox',{name:'示例昵称'}); await input.fill('保留草稿');
|
|
45
|
+
const handle=await input.elementHandle();
|
|
46
|
+
const canvas=await page.locator('.rhine-orbit canvas').first().elementHandle();
|
|
47
|
+
const layout=await page.locator('.ds-orbit-entry').evaluate(e=>{
|
|
48
|
+
const r=e.getBoundingClientRect(), bg=e.querySelector('.rhine-orbit'), c=bg.querySelector('canvas').getBoundingClientRect(), intro=e.querySelector('.ds-orbit-entry-intro').getBoundingClientRect(), form=e.querySelector('.ds-orbit-entry-form');
|
|
49
|
+
return {inside:r.left>=0&&r.right<=innerWidth+1,clear:intro.bottom<=c.top,formClear:getComputedStyle(form).backgroundColor!=='rgba(0, 0, 0, 0)',noControl:!bg.querySelector('button'),noFrame:getComputedStyle(bg).borderTopWidth==='0px'&&getComputedStyle(bg).boxShadow==='none',full:Math.abs(bg.getBoundingClientRect().width-r.width)<2, narrow:e.parentElement.clientWidth<=760};
|
|
50
|
+
});
|
|
51
|
+
check(layout.inside&&layout.clear&&layout.formClear&&layout.noControl&&layout.noFrame&&layout.full===layout.narrow,{theme,width,zoom,kind:'orbit-layout',layout});
|
|
52
|
+
await page.getByRole('combobox',{name:'验收主题'}).selectOption(theme==='rhine'?'arknights':'rhine');
|
|
53
|
+
await page.setViewportSize({width:width+80,height:1000});
|
|
54
|
+
check(await handle.evaluate(e=>e.isConnected&&e.value==='保留草稿')&&await canvas.evaluate(e=>e.isConnected),{theme,width,zoom,kind:'stable-nodes'});
|
|
55
|
+
orbitCases++;
|
|
56
|
+
}
|
|
57
|
+
return {pass:!failures.length&&!errors.length,filterCases,orbitCases,failures,errors};
|
|
58
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
async page => {
|
|
2
|
+
await page.addInitScript(() => {
|
|
3
|
+
if (window.__donutProof) return;
|
|
4
|
+
const proof = window.__donutProof = { contexts: [], pending: new Set(), rafScheduled: 0, rafFired: 0, rafCancelled: 0, listeners: new Map(), observerCreates: {}, observerDisconnects: {}, orbitObservers: {}, orbitDisconnects: {} };
|
|
5
|
+
let targetID = 0, listenerID = 0;
|
|
6
|
+
const targets = new WeakMap(), listeners = new WeakMap();
|
|
7
|
+
const targetName = target => {
|
|
8
|
+
if (!targets.has(target)) targets.set(target, ++targetID);
|
|
9
|
+
const kind = target === window ? 'window' : target === document ? 'document' : target instanceof HTMLCanvasElement ? 'canvas' : target instanceof MediaQueryList ? 'media' : '';
|
|
10
|
+
return kind ? `${kind}:${targets.get(target)}` : '';
|
|
11
|
+
};
|
|
12
|
+
const nativeAdd = EventTarget.prototype.addEventListener;
|
|
13
|
+
const nativeRemove = EventTarget.prototype.removeEventListener;
|
|
14
|
+
EventTarget.prototype.addEventListener = function (type, listener, options) {
|
|
15
|
+
const target = targetName(this);
|
|
16
|
+
if (target && listener && (typeof listener === 'object' || typeof listener === 'function')) {
|
|
17
|
+
if (!listeners.has(listener)) listeners.set(listener, ++listenerID);
|
|
18
|
+
const key = `${target}:${type}:${listeners.get(listener)}:${Boolean(typeof options === 'boolean' ? options : options?.capture)}`;
|
|
19
|
+
proof.listeners.set(key, { target, type });
|
|
20
|
+
}
|
|
21
|
+
return nativeAdd.call(this, type, listener, options);
|
|
22
|
+
};
|
|
23
|
+
EventTarget.prototype.removeEventListener = function (type, listener, options) {
|
|
24
|
+
const target = targetName(this);
|
|
25
|
+
if (target && listener) proof.listeners.delete(`${target}:${type}:${listeners.get(listener)}:${Boolean(typeof options === 'boolean' ? options : options?.capture)}`);
|
|
26
|
+
return nativeRemove.call(this, type, listener, options);
|
|
27
|
+
};
|
|
28
|
+
const nativeRequest = requestAnimationFrame, nativeCancel = cancelAnimationFrame;
|
|
29
|
+
window.requestAnimationFrame = callback => {
|
|
30
|
+
proof.rafScheduled++;
|
|
31
|
+
const id = nativeRequest(time => { proof.pending.delete(id); proof.rafFired++; callback(time); });
|
|
32
|
+
proof.pending.add(id);
|
|
33
|
+
return id;
|
|
34
|
+
};
|
|
35
|
+
window.cancelAnimationFrame = id => { proof.pending.delete(id); proof.rafCancelled++; nativeCancel(id); };
|
|
36
|
+
for (const name of ['ResizeObserver', 'IntersectionObserver', 'MutationObserver']) {
|
|
37
|
+
const Native = window[name];
|
|
38
|
+
window[name] = class extends Native {
|
|
39
|
+
constructor(callback) { super(callback); proof.observerCreates[name] = (proof.observerCreates[name] || 0) + 1; }
|
|
40
|
+
observe(target, ...args) { if (target instanceof HTMLCanvasElement && target.classList.contains('rhine-orbit-canvas') && !this.orbit) { this.orbit = true; proof.orbitObservers[name] = (proof.orbitObservers[name] || 0) + 1; } return super.observe(target, ...args); }
|
|
41
|
+
disconnect() { if(this.orbit) proof.orbitDisconnects[name] = (proof.orbitDisconnects[name] || 0) + 1; proof.observerDisconnects[name] = (proof.observerDisconnects[name] || 0) + 1; return super.disconnect(); }
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const locations = new WeakMap(), contexts = new WeakMap();
|
|
45
|
+
const record = gl => {
|
|
46
|
+
if (!contexts.has(gl)) {
|
|
47
|
+
const entry = { canvas: gl.canvas, draws: 0, frames: 0, samples: [], time: null, matrices: [], resources: {}, lost: 0, restored: 0, queued: false };
|
|
48
|
+
contexts.set(gl, entry); proof.contexts.push(entry);
|
|
49
|
+
nativeAdd.call(gl.canvas, 'webglcontextlost', () => entry.lost++);
|
|
50
|
+
nativeAdd.call(gl.canvas, 'webglcontextrestored', () => entry.restored++);
|
|
51
|
+
}
|
|
52
|
+
return contexts.get(gl);
|
|
53
|
+
};
|
|
54
|
+
const scheduleSample = gl => {
|
|
55
|
+
const entry = record(gl);
|
|
56
|
+
if (entry.queued) return;
|
|
57
|
+
entry.queued = true;
|
|
58
|
+
queueMicrotask(() => {
|
|
59
|
+
entry.queued = false; entry.frames++;
|
|
60
|
+
const now = performance.now();
|
|
61
|
+
const retainSample = !entry.samples.length || now - entry.samples.at(-1).at >= 180;
|
|
62
|
+
try {
|
|
63
|
+
const canvas = document.createElement('canvas'); canvas.width = 100; canvas.height = 72;
|
|
64
|
+
const context = canvas.getContext('2d'); context.drawImage(gl.canvas, 0, 0, 100, 72);
|
|
65
|
+
const pixels = context.getImageData(0, 0, 100, 72).data;
|
|
66
|
+
let hash = 2166136261, painted = 0, bright = 0, min = 255, max = 0;
|
|
67
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
68
|
+
for (let j = 0; j < 4; j++) hash = Math.imul(hash ^ pixels[i + j], 16777619);
|
|
69
|
+
if (pixels[i + 3]) painted++;
|
|
70
|
+
const luminance = (pixels[i] + pixels[i + 1] + pixels[i + 2]) / 3;
|
|
71
|
+
if (luminance > 100) bright++;
|
|
72
|
+
min = Math.min(min, luminance); max = Math.max(max, luminance);
|
|
73
|
+
}
|
|
74
|
+
entry.latest = { at: now, time: entry.time, hash: hash >>> 0, painted, bright, min, max, matrices: entry.matrices.slice(0, 40) };
|
|
75
|
+
if (retainSample) entry.samples.push(entry.latest);
|
|
76
|
+
if (entry.samples.length > 700) entry.samples.shift();
|
|
77
|
+
} catch (error) { entry.sampleError = String(error); }
|
|
78
|
+
entry.matrices = [];
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
for (const Proto of [window.WebGLRenderingContext?.prototype, window.WebGL2RenderingContext?.prototype].filter(Boolean)) {
|
|
82
|
+
for (const name of ['drawArrays', 'drawElements', 'drawArraysInstanced', 'drawElementsInstanced']) {
|
|
83
|
+
const native = Proto[name]; if (!native || native.__donutProof) continue;
|
|
84
|
+
const wrapped = function (...args) { const result = native.apply(this, args); record(this).draws++; scheduleSample(this); return result; };
|
|
85
|
+
wrapped.__donutProof = true; Proto[name] = wrapped;
|
|
86
|
+
}
|
|
87
|
+
const nativeLocation = Proto.getUniformLocation;
|
|
88
|
+
if (!nativeLocation.__donutProof) {
|
|
89
|
+
const wrapped = function (program, name) { const value = nativeLocation.call(this, program, name); if (value) locations.set(value, name); return value; };
|
|
90
|
+
wrapped.__donutProof = true; Proto.getUniformLocation = wrapped;
|
|
91
|
+
}
|
|
92
|
+
for (const name of ['uniform1f', 'uniformMatrix4fv']) {
|
|
93
|
+
const native = Proto[name]; if (native.__donutProof) continue;
|
|
94
|
+
const wrapped = function (...args) {
|
|
95
|
+
const result = native.apply(this, args), uniform = locations.get(args[0]);
|
|
96
|
+
if (name === 'uniform1f' && /^(time|uTime)$/.test(uniform)) record(this).time = args[1];
|
|
97
|
+
if (name === 'uniformMatrix4fv' && uniform === 'modelViewMatrix') record(this).matrices.push(Array.from(args[2], value => Number(value.toFixed(5))));
|
|
98
|
+
return result;
|
|
99
|
+
};
|
|
100
|
+
wrapped.__donutProof = true; Proto[name] = wrapped;
|
|
101
|
+
}
|
|
102
|
+
for (const kind of ['Buffer', 'Texture', 'Program', 'Shader', 'Framebuffer', 'Renderbuffer', 'VertexArray']) {
|
|
103
|
+
for (const action of ['create', 'delete']) {
|
|
104
|
+
const name = action + kind, native = Proto[name]; if (!native || native.__donutProof) continue;
|
|
105
|
+
const wrapped = function (...args) { const result = native.apply(this, args); const entry = record(this); if (action === 'create' ? result : args[0]) entry.resources[name] = (entry.resources[name] || 0) + 1; return result; };
|
|
106
|
+
wrapped.__donutProof = true; Proto[name] = wrapped;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
window.__donutSnapshot = () => ({
|
|
111
|
+
pendingRafs: proof.pending.size, rafScheduled: proof.rafScheduled, rafFired: proof.rafFired, rafCancelled: proof.rafCancelled,
|
|
112
|
+
listeners: [...proof.listeners.values()], observerCreates: proof.observerCreates, observerDisconnects: proof.observerDisconnects, orbitObservers: proof.orbitObservers, orbitDisconnects: proof.orbitDisconnects,
|
|
113
|
+
contexts: proof.contexts.filter(entry => entry.canvas.classList.contains('rhine-orbit-canvas')).map(entry => ({
|
|
114
|
+
example: entry.canvas.closest('.rhine-orbit')?.getAttribute('aria-label') || null,
|
|
115
|
+
kind: entry.canvas.dataset.orbitArtwork || 'donut', active: !entry.canvas.hidden, canvasConnected: entry.canvas.isConnected, draws: entry.draws, frames: entry.frames, time: entry.time, samples: entry.samples, latest: entry.latest,
|
|
116
|
+
resources: entry.resources, lost: entry.lost, restored: entry.restored, sampleError: entry.sampleError, width: entry.canvas.width, height: entry.canvas.height,
|
|
117
|
+
})),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const results=[],errors=[]; page.on('pageerror',e=>errors.push(String(e)));
|
|
123
|
+
const snap=()=>page.evaluate(()=>window.__donutSnapshot());
|
|
124
|
+
for(const theme of ['rhine','arknights','neo-brutalism']) {
|
|
125
|
+
await page.setViewportSize({width:1280,height:1000});
|
|
126
|
+
await page.goto(`http://127.0.0.1:18484/?fixture=orbit-entry&theme=${theme}`);
|
|
127
|
+
const root=page.locator('.rhine-orbit'), canvas=root.locator('canvas').first();
|
|
128
|
+
await root.scrollIntoViewIfNeeded();
|
|
129
|
+
await page.waitForFunction(()=>window.__donutSnapshot().contexts.some(c=>c.canvasConnected&&c.active&&c.latest?.painted>50));
|
|
130
|
+
const active=s=>s.contexts.find(c=>c.canvasConnected&&c.active);
|
|
131
|
+
const a=active(await snap()); await page.waitForTimeout(450); const b=active(await snap());
|
|
132
|
+
const playing=a.latest.hash!==b.latest.hash&&b.draws>a.draws;
|
|
133
|
+
await page.getByRole('checkbox',{name:'减少入口动态'}).check();
|
|
134
|
+
await page.waitForTimeout(150);const c=active(await snap());await page.waitForTimeout(350); const d=active(await snap());
|
|
135
|
+
const manual=c.draws===d.draws&&c.latest.hash===d.latest.hash;
|
|
136
|
+
await page.getByRole('checkbox',{name:'减少入口动态'}).uncheck();
|
|
137
|
+
await page.emulateMedia({reducedMotion:'reduce'});await page.waitForTimeout(150);const e=active(await snap());await page.waitForTimeout(350);const f=active(await snap());
|
|
138
|
+
const system=e.draws===f.draws;
|
|
139
|
+
await page.emulateMedia({reducedMotion:'no-preference'});await page.waitForTimeout(350);const resumed=active(await snap()).draws>f.draws;
|
|
140
|
+
await root.evaluate(e=>e.style.display='none');await page.waitForTimeout(150);const g=active(await snap());await page.waitForTimeout(350);const h=active(await snap());
|
|
141
|
+
const empty=g.draws===h.draws;
|
|
142
|
+
await root.evaluate(e=>e.style.display='');await page.waitForTimeout(200);
|
|
143
|
+
await page.locator('.ds-orbit-entry-container').evaluate(e=>e.style.marginTop='2200px');await page.evaluate(()=>scrollTo(0,0));await page.waitForTimeout(150);const j=active(await snap());await page.waitForTimeout(350);const k=active(await snap());
|
|
144
|
+
const offscreen=j.draws===k.draws;
|
|
145
|
+
await page.locator('.ds-orbit-entry-container').evaluate(e=>e.style.marginTop=''); await root.scrollIntoViewIfNeeded();await page.waitForTimeout(250);
|
|
146
|
+
const other=await page.context().newPage();await other.goto('about:blank');await other.bringToFront();await page.waitForTimeout(250);
|
|
147
|
+
const actualHidden=await page.evaluate(()=>document.hidden);
|
|
148
|
+
await page.evaluate(()=>{Object.defineProperty(document,'hidden',{configurable:true,value:true});document.dispatchEvent(new Event('visibilitychange'));});
|
|
149
|
+
await page.waitForTimeout(150);
|
|
150
|
+
const hidden=await page.evaluate(()=>document.hidden);const l=active(await snap());await page.waitForTimeout(350);const m=active(await snap());
|
|
151
|
+
const hiddenPause=hidden&&l.draws===m.draws;
|
|
152
|
+
await other.close();await page.bringToFront();
|
|
153
|
+
await page.evaluate(()=>{delete document.hidden;document.dispatchEvent(new Event('visibilitychange'));});
|
|
154
|
+
await page.getByRole('checkbox',{name:'挂载入口'}).uncheck();await page.waitForTimeout(150);const disposed=await snap();
|
|
155
|
+
await page.waitForTimeout(350);const end=await snap();
|
|
156
|
+
const cleanup=end.contexts.every(c=>!c.canvasConnected)&&end.pendingRafs===0&&JSON.stringify(end.contexts.map(c=>c.draws))===JSON.stringify(disposed.contexts.map(c=>c.draws))&&['ResizeObserver','IntersectionObserver','MutationObserver'].every(n=>end.orbitObservers[n]>0&&end.orbitObservers[n]===end.orbitDisconnects[n]);
|
|
157
|
+
results.push({theme,playing,manual,system,resumed,empty,offscreen,actualHidden,hidden,hiddenPause,cleanup,pending:end.pendingRafs,observers:{created:end.observerCreates,disconnected:end.observerDisconnects,orbitCreated:end.orbitObservers,orbitDisconnected:end.orbitDisconnects},resources:end.contexts.map(c=>c.resources)});
|
|
158
|
+
}
|
|
159
|
+
const fallback=await page.context().newPage();
|
|
160
|
+
await fallback.addInitScript(()=>{HTMLCanvasElement.prototype.getContext=function(){return null;};});
|
|
161
|
+
const fallbacks=[];
|
|
162
|
+
for(const theme of ['rhine','arknights','neo-brutalism']) {
|
|
163
|
+
await fallback.goto(`http://127.0.0.1:18484/?fixture=orbit-entry&theme=${theme}`);
|
|
164
|
+
await fallback.waitForFunction(()=>document.querySelector('.rhine-orbit')?.dataset.rhineOrbitState==='unavailable');
|
|
165
|
+
fallbacks.push(await fallback.locator('.rhine-orbit').evaluate(e=>{const c=getComputedStyle(e.querySelector('canvas')),p=getComputedStyle(e,'::after');return {theme:e.closest('[data-rhine-theme]').dataset.rhineTheme,pass:c.width===p.width&&c.height===p.height&&c.right===p.right&&c.bottom===p.bottom&&p.backgroundImage.includes('svg')};}));
|
|
166
|
+
}
|
|
167
|
+
await fallback.close();
|
|
168
|
+
return {pass:results.every(r=>r.playing&&r.manual&&r.system&&r.resumed&&r.empty&&r.offscreen&&r.hiddenPause&&r.cleanup)&&fallbacks.every(r=>r.pass)&&!errors.length,results,fallbacks,errors};
|
|
169
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
// Open a fixed production preview, then run with playwright-cli run-code --filename.
|
|
2
|
+
// CSS zoom evidence is separate from native browser-menu zoom.
|
|
3
|
+
async page => {
|
|
4
|
+
page.setDefaultTimeout(10000);
|
|
5
|
+
const origin = new URL(page.url()).origin;
|
|
6
|
+
const failures = [], errors = [], counts = { drawer: 0, date: 0, table: 0, account: 0 };
|
|
7
|
+
page.on('pageerror', error => errors.push(String(error)));
|
|
8
|
+
const check = (pass, context) => { if (!pass) failures.push(context); };
|
|
9
|
+
const open = async (section, theme, width, zoom) => {
|
|
10
|
+
await page.setViewportSize({ width, height: 1000 });
|
|
11
|
+
await page.goto(`${origin}/?theme=${theme}#${section}`);
|
|
12
|
+
await page.locator(`#${section}`).waitFor();
|
|
13
|
+
await page.evaluate(value => { document.documentElement.style.zoom = String(value); }, zoom);
|
|
14
|
+
};
|
|
15
|
+
const editLastSegment = async (input, context) => {
|
|
16
|
+
await input.fill('2026-10-24');
|
|
17
|
+
await input.focus();
|
|
18
|
+
// The preview's zh-CN native date order is year/month/day.
|
|
19
|
+
await page.keyboard.press('ArrowRight');
|
|
20
|
+
await page.keyboard.press('ArrowRight');
|
|
21
|
+
await page.keyboard.press('ArrowUp');
|
|
22
|
+
check(await input.inputValue() === '2026-10-25', { ...context, kind: 'edit-last-segment' });
|
|
23
|
+
await page.keyboard.press('ArrowDown');
|
|
24
|
+
check(await input.inputValue() === '2026-10-24', { ...context, kind: 'restore-last-segment' });
|
|
25
|
+
};
|
|
26
|
+
const measureDate = async (input, context) => {
|
|
27
|
+
const dimensions = await input.evaluate(e => {
|
|
28
|
+
const style = getComputedStyle(e);
|
|
29
|
+
return { width: parseFloat(style.width), height: parseFloat(style.height), font: parseFloat(style.fontSize), padding: parseFloat(style.paddingLeft) };
|
|
30
|
+
});
|
|
31
|
+
check(dimensions.width >= 148 && dimensions.height >= 44 && dimensions.font >= 14, { ...context, kind: 'date-dimensions', dimensions });
|
|
32
|
+
};
|
|
33
|
+
for (const theme of ['rhine', 'arknights', 'neo-brutalism']) {
|
|
34
|
+
for (const width of [400, 520, 880]) for (const zoom of [1, 2]) {
|
|
35
|
+
const context = { theme, width, zoom };
|
|
36
|
+
await open('list-filter-bar', theme, width, zoom);
|
|
37
|
+
const bar = page.getByRole('group', { name: '录制会话筛选', exact: true });
|
|
38
|
+
const trigger = bar.getByRole('button', { name: /^更多筛选/ });
|
|
39
|
+
await trigger.click();
|
|
40
|
+
const dialog = page.getByRole('dialog', { name: '录制会话筛选', exact: true });
|
|
41
|
+
await dialog.evaluate(e => Promise.all(e.getAnimations().map(a => a.finished)));
|
|
42
|
+
const start = dialog.getByLabel('开始日期', { exact: true });
|
|
43
|
+
await measureDate(start, context);
|
|
44
|
+
await editLastSegment(start, context);
|
|
45
|
+
await dialog.getByLabel('结束日期', { exact: true }).fill('2026-10-26');
|
|
46
|
+
await dialog.getByRole('button', { name: '清空日期', exact: true }).click();
|
|
47
|
+
check(await start.inputValue() === '', { ...context, kind: 'clear-range' });
|
|
48
|
+
await dialog.getByRole('checkbox', { name: '只看已审查' }).check();
|
|
49
|
+
await start.focus(); await page.keyboard.type('2');
|
|
50
|
+
check(await start.getAttribute('aria-invalid') === 'true', { ...context, kind: 'incomplete-date' });
|
|
51
|
+
await dialog.getByRole('button', { name: '重置筛选', exact: true }).click();
|
|
52
|
+
check(await start.evaluate(e => !e.value && !e.validity.badInput && e.getAttribute('aria-invalid') !== 'true'), { ...context, kind: 'reset-draft' });
|
|
53
|
+
if (width === 400 && zoom === 2) {
|
|
54
|
+
await start.fill('2026-10-24');
|
|
55
|
+
await start.screenshot({ path: `output/playwright/issues-23-25/drawer-${theme}.png` });
|
|
56
|
+
}
|
|
57
|
+
await page.keyboard.press('Escape'); await dialog.waitFor({ state: 'detached' });
|
|
58
|
+
await page.waitForFunction(e => e === document.activeElement, await trigger.elementHandle());
|
|
59
|
+
counts.drawer++;
|
|
60
|
+
|
|
61
|
+
await open('spacing', theme, width, zoom);
|
|
62
|
+
const surface = page.locator('#spacing .rhine-surface--padding-none');
|
|
63
|
+
check(await surface.evaluate(e => getComputedStyle(e).padding === '0px' && getComputedStyle(e).overflow !== 'hidden'), { ...context, kind: 'surface-padding' });
|
|
64
|
+
check(await surface.locator('td').first().evaluate(e => parseFloat(getComputedStyle(e).paddingLeft) > 0), { ...context, kind: 'table-cell-spacing' });
|
|
65
|
+
const selection = surface.getByRole('checkbox').nth(1);
|
|
66
|
+
await selection.uncheck(); await selection.check();
|
|
67
|
+
await selection.focus(); await page.keyboard.press('Space');
|
|
68
|
+
check(!await selection.isChecked(), { ...context, kind: 'table-keyboard-selection' });
|
|
69
|
+
check(await surface.evaluate(e => e.getBoundingClientRect().right <= innerWidth + 1), { ...context, kind: 'table-bounds' });
|
|
70
|
+
const scroll = surface.getByRole('region', { name: /可横向滚动/ });
|
|
71
|
+
await scroll.focus();
|
|
72
|
+
await scroll.evaluate(e => { e.scrollLeft = e.scrollWidth; });
|
|
73
|
+
check(await scroll.evaluate(e => e === document.activeElement && e.querySelector('tbody tr td:last-child').getBoundingClientRect().right <= e.getBoundingClientRect().right + 1), { ...context, kind: 'table-last-column-reachable' });
|
|
74
|
+
await scroll.evaluate(e => { e.scrollLeft = 0; });
|
|
75
|
+
if (width === 400 && zoom === 2) await surface.screenshot({ path: `output/playwright/issues-23-25/table-${theme}.png` });
|
|
76
|
+
counts.table++;
|
|
77
|
+
|
|
78
|
+
await open('compact-account', theme, width, 1);
|
|
79
|
+
await page.getByRole('combobox', { name: '样机宽度' }).selectOption(String(width));
|
|
80
|
+
await page.getByRole('combobox', { name: '内容缩放' }).selectOption(String(zoom));
|
|
81
|
+
for (const state of ['normal', 'zero', 'unknown', 'long']) {
|
|
82
|
+
await page.getByRole('combobox', { name: '账号读数' }).selectOption(state);
|
|
83
|
+
const card = page.getByTestId('compact-account-card');
|
|
84
|
+
const layout = await card.evaluate(e => {
|
|
85
|
+
const bars = e.querySelector('.rhine-timeline__bars').getBoundingClientRect();
|
|
86
|
+
const legend = e.querySelector('.rhine-timeline__legend').getBoundingClientRect();
|
|
87
|
+
const slider = e.querySelector('input[type=range]');
|
|
88
|
+
const pairs = [...e.querySelectorAll('.compact-gallery-fact-pair')];
|
|
89
|
+
return {
|
|
90
|
+
gap: legend.top - bars.bottom,
|
|
91
|
+
sliderHeight: parseFloat(getComputedStyle(slider).height),
|
|
92
|
+
triggerHeight: parseFloat(getComputedStyle(e.querySelector('.rhine-timeline__legend-trigger')).height),
|
|
93
|
+
aligned: [...pairs[0].children].every((child, i) => Math.abs(child.getBoundingClientRect().left - pairs[1].children[i].getBoundingClientRect().left) < 1),
|
|
94
|
+
overflow: [...e.querySelectorAll('.compact-gallery-facts, .compact-gallery-facts span')].some(node => node.scrollWidth > node.clientWidth + 1),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
check(Math.abs(layout.gap / zoom - 12) < 1 && layout.sliderHeight >= 44 && layout.triggerHeight >= 44 && layout.aligned && !layout.overflow, { ...context, state, kind: 'account-layout', layout });
|
|
98
|
+
if (state === 'normal') {
|
|
99
|
+
const slider = card.getByRole('slider');
|
|
100
|
+
await slider.focus(); await page.keyboard.press('ArrowRight');
|
|
101
|
+
const detail = page.getByRole('dialog', { name: /时段详情$/ });
|
|
102
|
+
await detail.waitFor(); await page.keyboard.press('Escape'); await detail.waitFor({ state: 'detached' });
|
|
103
|
+
await page.getByRole('combobox', { name: '账号读数' }).focus();
|
|
104
|
+
await slider.hover(); await detail.waitFor();
|
|
105
|
+
await page.mouse.move(0, 0); await detail.waitFor({ state: 'detached' });
|
|
106
|
+
const legend = card.getByRole('button', { name: '有输出', exact: true });
|
|
107
|
+
await legend.click();
|
|
108
|
+
const rule = page.getByRole('dialog', { name: '有输出规则', exact: true });
|
|
109
|
+
await rule.waitFor(); await page.keyboard.press('Escape'); await rule.waitFor({ state: 'detached' });
|
|
110
|
+
}
|
|
111
|
+
if (width === 400 && zoom === 2 && state === 'long') await card.screenshot({ path: `output/playwright/issues-23-25/account-${theme}.png` });
|
|
112
|
+
counts.account++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Leave room for the gallery sidebar: max-width must not clamp the 280px fixture.
|
|
116
|
+
await open('date-range-filter', theme, 1280, 2);
|
|
117
|
+
for (const containerWidth of [150, 200, 280]) {
|
|
118
|
+
await page.getByRole('combobox', { name: '日期容器宽度' }).selectOption(String(containerWidth));
|
|
119
|
+
const context = { theme, containerWidth, zoom: 2 };
|
|
120
|
+
const range = page.getByRole('group', { name: '局部日期', exact: true });
|
|
121
|
+
check(await range.evaluate((e, expected) => Math.abs(parseFloat(getComputedStyle(e.parentElement).width) - expected) < 1, containerWidth), { ...context, kind: 'actual-container-width' });
|
|
122
|
+
const start = range.getByLabel('开始日期', { exact: true });
|
|
123
|
+
const single = page.getByLabel('独立日期', { exact: true });
|
|
124
|
+
await measureDate(start, context); await measureDate(single, context);
|
|
125
|
+
await editLastSegment(start, context); await editLastSegment(single, context);
|
|
126
|
+
await single.fill(''); check(await single.inputValue() === '', { ...context, kind: 'clear-single' });
|
|
127
|
+
await range.getByRole('button', { name: '清空日期', exact: true }).click();
|
|
128
|
+
await page.getByRole('button', { name: '重置示例日期' }).click();
|
|
129
|
+
check(await start.inputValue() === '2026-10-24' && await single.inputValue() === '2026-10-24', { ...context, kind: 'reset-example' });
|
|
130
|
+
check(await page.getByRole('button', { name: '重置示例日期' }).evaluate(e => e === document.activeElement), { ...context, kind: 'reset-example-focus' });
|
|
131
|
+
await single.screenshot({ path: `output/playwright/issues-23-25/single-${theme}-${containerWidth}.png` });
|
|
132
|
+
counts.date++;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return { pass: !failures.length && !errors.length, counts, failures, errors, scope: 'Chromium; real gallery, CSS zoom, native keyboard editing. Inspect date screenshots for full fragments/calendar visibility.' };
|
|
136
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
async page => {
|
|
2
|
+
await page.goto(`${new URL(page.url()).origin}/?fixture=list-filters&theme=neo-brutalism&proof=${Date.now()}#list-filter-bar`);
|
|
3
|
+
const checks = [];
|
|
4
|
+
const check = (name, pass, evidence = {}) => checks.push({ name, pass: Boolean(pass), ...evidence });
|
|
5
|
+
await page.getByRole('group', { name: '录制会话筛选', exact: true }).getByRole('button', { name: /^更多筛选/ }).click();
|
|
6
|
+
const dialog = page.getByRole('dialog', { name: '录制会话筛选', exact: true });
|
|
7
|
+
const start = dialog.getByLabel('开始日期', { exact: true });
|
|
8
|
+
const end = dialog.getByLabel('结束日期', { exact: true });
|
|
9
|
+
const inspect = () => start.evaluate(element => ({ value: element.value, badInput: element.validity.badInput, invalid: element.getAttribute('aria-invalid') }));
|
|
10
|
+
await start.fill('2026-10-20'); await end.fill('2026-10-24');
|
|
11
|
+
await start.focus(); await page.keyboard.press('ArrowLeft'); await page.keyboard.press('ArrowLeft'); await page.keyboard.press('Backspace');
|
|
12
|
+
await page.waitForTimeout(100);
|
|
13
|
+
let value = await inspect();
|
|
14
|
+
check('Deleting a native date segment keeps accepted range and reports invalid', value.badInput && value.invalid === 'true' && (await page.locator('#list-filter-bar .rhine-list-filter-bar__active').first().innerText()).includes('2026-10-20 — 2026-10-24'), value);
|
|
15
|
+
await page.keyboard.type('2026'); await page.waitForTimeout(100); value = await inspect();
|
|
16
|
+
check('Typing the complete year repairs native draft', value.value === '2026-10-20' && !value.badInput && value.invalid !== 'true', value);
|
|
17
|
+
await dialog.getByRole('button', { name: '清空日期', exact: true }).click();
|
|
18
|
+
await start.evaluate(element => { window.nativeDateEvents = []; for (const name of ['input', 'change', 'keyup', 'blur']) element.addEventListener(name, event => window.nativeDateEvents.push({ event: event.type, key: event.key, value: element.value, badInput: element.validity.badInput })); });
|
|
19
|
+
await start.focus(); await page.keyboard.type('2'); await page.waitForTimeout(100); value = await inspect();
|
|
20
|
+
check('Empty date native fragment is invalid even when no input/change fires', value.badInput && value.invalid === 'true' && await dialog.getByRole('button', { name: '清空日期', exact: true }).isEnabled(), { ...value, events: await page.evaluate(() => window.nativeDateEvents) });
|
|
21
|
+
if (await dialog.getByRole('button', { name: '清空日期', exact: true }).isEnabled()) {
|
|
22
|
+
await dialog.getByRole('button', { name: '清空日期', exact: true }).click(); await page.waitForTimeout(100); value = await inspect();
|
|
23
|
+
check('Clear removes an empty native fragment and validation error', value.value === '' && !value.badInput && value.invalid !== 'true', value);
|
|
24
|
+
}
|
|
25
|
+
await dialog.getByRole('button', { name: '今天', exact: true }).click();
|
|
26
|
+
await start.focus(); await page.keyboard.press('ArrowLeft'); await page.keyboard.press('ArrowLeft'); await page.keyboard.press('Backspace');
|
|
27
|
+
await dialog.getByRole('button', { name: '今天', exact: true }).click(); await page.waitForTimeout(100); value = await inspect();
|
|
28
|
+
check('Applying the same preset clears a rejected native draft', value.value === '2026-10-24' && !value.badInput && value.invalid !== 'true', value);
|
|
29
|
+
await page.keyboard.press('Escape'); await dialog.waitFor({state:'detached'});
|
|
30
|
+
for (let i=0;i<3;i++) {
|
|
31
|
+
const bar=page.locator('.rhine-list-filter-bar').nth(i);
|
|
32
|
+
if(await bar.getByRole('button',{name:'重置筛选',exact:true}).count()) await bar.getByRole('button',{name:'重置筛选',exact:true}).click();
|
|
33
|
+
await bar.getByRole('button',{name:/^更多筛选/}).click(); const panel=page.getByRole('dialog');
|
|
34
|
+
if(i<2) await panel.getByRole('checkbox',{name:'只看已审查'}).check();
|
|
35
|
+
else await panel.getByRole('combobox',{name:'统计范围'}).selectOption({label:'示例团队 / 跨部门体验与可访问性协作小组'});
|
|
36
|
+
const date=panel.getByLabel('开始日期',{exact:true});
|
|
37
|
+
await date.focus();
|
|
38
|
+
if(i===2){await page.keyboard.press('ArrowLeft');await page.keyboard.press('ArrowLeft');await page.keyboard.press('Backspace');}
|
|
39
|
+
else await page.keyboard.type('2');
|
|
40
|
+
await page.waitForFunction(e=>e.validity.badInput&&e.getAttribute('aria-invalid')==='true',await date.elementHandle());
|
|
41
|
+
await panel.getByRole('button',{name:'重置筛选',exact:true}).click();
|
|
42
|
+
const clean=await date.evaluate(e=>!e.validity.badInput&&e.getAttribute('aria-invalid')!=='true');
|
|
43
|
+
check(`Overall reset clears default-valued native draft in example ${i}`,clean&&await panel.evaluate(e=>e.contains(document.activeElement)));
|
|
44
|
+
await page.keyboard.press('Escape');await panel.waitFor({state:'detached'});
|
|
45
|
+
}
|
|
46
|
+
return { complete: checks.every(item => item.pass), checks, scope: 'Real native date segmentation in isolated Chromium; keyboard presses use page.keyboard so locator focus does not reset year digit buffering.' };
|
|
47
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: rhine-design
|
|
3
3
|
description: Use Rhine Design components and specifications to design, implement, or review React UI, typography, themes, and motion. Apply when a project uses Rhine Design or the user requests its visual language or component system.
|
|
4
4
|
metadata:
|
|
5
|
-
version: "0.
|
|
5
|
+
version: "0.9.1"
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Rhine Design · 莱茵设计
|
|
@@ -13,7 +13,7 @@ Use the installed version's components and contracts to build consistent interfa
|
|
|
13
13
|
|
|
14
14
|
## Locate the matching source
|
|
15
15
|
|
|
16
|
-
Package: `@linhey/rhine-design`. GitHub: `linhay/rhine-design` (different account spelling). This skill targets 0.
|
|
16
|
+
Package: `@linhey/rhine-design`. GitHub: `linhay/rhine-design` (different account spelling). This skill targets 0.9.1, including ListFilterBar, Surface padding, narrow date layouts, OrbitBackdrop, application shells, advanced forms, media, controlled tables and monitoring charts. Theme and navigation APIs require at least 0.4.0; application APIs require 0.5.0 and OrbitBackdrop requires 0.6.0. Check installed exports before adoption.
|
|
17
17
|
|
|
18
18
|
Find the package in the host's dependencies, normally `node_modules/@linhey/rhine-design/`, or its registered source checkout. Verify `package.json` name/version before using the paths below. Paths in references are relative to that package root, not the copied skill directory. Read installed types and source when versions differ; do not infer a new API from this skill. If the package is unavailable, use the task's supplied design sources and identify the missing implementation evidence.
|
|
19
19
|
|
|
@@ -56,6 +56,6 @@ Required issue content:
|
|
|
56
56
|
- Host state determines data, selection and operation outcomes. Search and pagination remain host/server responsibilities; busy indicators never imply success.
|
|
57
57
|
- For multi-field failures, preserve input and focus a summary only after failed submission; links return to fields without changing routing. Use SideNav/Breadcrumbs/Steps only when exported by the installed source.
|
|
58
58
|
- Monitoring shows units, freshness and stale/unknown states; errors must not rely on color, flashing or transient toasts.
|
|
59
|
-
- Design from the user's current task: keep identity, available actions, results and decision-relevant consequences; remove duplicate state labels and implementation prose. Reveal history on demand. Inside an existing Surface prefer borderless read-only summaries and paired facts; never compress by shrinking business text, cropping values, hiding unknown/error states or merging distinct costs/expiry meanings. See `docs/COMPACT_ACCOUNT_PATTERNS.md` when present; its
|
|
59
|
+
- Design from the user's current task: keep identity, available actions, results and decision-relevant consequences; remove duplicate state labels and implementation prose. Reveal history on demand. Inside an existing Surface prefer borderless read-only summaries and paired facts; never compress by shrinking business text, cropping values, hiding unknown/error states or merging distinct costs/expiry meanings. See `docs/COMPACT_ACCOUNT_PATTERNS.md` when present; its base APIs require 0.8.0, and the spacing fixes and Surface padding option require 0.9.1.
|
|
60
60
|
- Motion explains a change. Respect reduced motion, cancellation and replay; startup decoration never blocks real readiness. Preview-only experiments are not exported components.
|
|
61
61
|
- Keep independent release boundaries: do not change unrelated products or publish packages merely because this skill was invoked.
|
|
@@ -30,7 +30,7 @@ All paths are relative to the matching package root. `package.json` exports, `co
|
|
|
30
30
|
| SideNav, Breadcrumbs, Steps (0.4.0) | `components/Navigation.tsx` |
|
|
31
31
|
| Fieldset, ErrorSummary (0.4.0) | `components/FormWorkflow.tsx` |
|
|
32
32
|
| Toast | `components/Toast.tsx` |
|
|
33
|
-
| Button, Badge, Surface, SelectionCard, TextField, Tabs, SegmentedControl, Notice, Progress, Skeleton, Disclosure, EmptyState | `components/components.tsx` |
|
|
33
|
+
| Button, Badge, Surface, SelectionCard, TextField, Tabs, SegmentedControl, Notice, Progress, Skeleton, Disclosure, EmptyState | `components/components.tsx`;Surface 的 `padding="none"` 仅取消表格容器外围内距,默认不变;此选项自 0.9.1 提供,见 `CHANGELOG.md` |
|
|
34
34
|
|
|
35
35
|
Primary contracts: `components/README.md` and `components/COMMON_COMPONENTS.md`. Find behavioral evidence in `tests/` and composition examples in `preview/`.
|
|
36
36
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Supported baseline
|
|
4
4
|
|
|
5
|
-
Rhine Design 0.
|
|
5
|
+
Rhine Design 0.9.1 ships TypeScript/TSX, CSS and SVG source for React 19 + Vite. Install `@linhey/rhine-design`; GitHub remains `linhay/rhine-design`. Check the installed package's engine and peer dependencies. Node 24 is the development baseline. Do not assume precompiled JavaScript, arbitrary SSR support or a complete dark theme.
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
8
|
import { Button, Surface, Text } from '@linhey/rhine-design';
|
|
@@ -19,7 +19,8 @@ describe('compact account composition', () => {
|
|
|
19
19
|
view.rerender(<div className="rhine-theme"><CompactAccountExample state="unknown" /></div>);
|
|
20
20
|
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
|
|
21
21
|
expect(screen.getByText('7D 剩余 — · 额度暂不可用')).toBeVisible();
|
|
22
|
-
expect(screen.getByText(
|
|
22
|
+
expect(screen.getByText(/10\/23 00:00 – 10\/24 00:00 · 上海/)).toBeVisible();
|
|
23
|
+
expect(screen.getByText(/历史记录不完整,仅展示已确认的时段。/)).toBeVisible();
|
|
23
24
|
expect(within(screen.getByTestId('compact-account-facts')).getAllByText('—')).toHaveLength(5);
|
|
24
25
|
view.rerender(<div className="rhine-theme"><CompactAccountExample state="long" /></div>);
|
|
25
26
|
expect(screen.getByText('到期')).toBeVisible();
|