@lark-base-open/base-database-cli 1.3.7 → 1.3.9
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 +42 -0
- package/README.md +2 -0
- package/dist/ai-functions.d.ts +11 -0
- package/dist/ai-functions.d.ts.map +1 -1
- package/dist/ai-functions.js +118 -9
- package/dist/ai-functions.js.map +1 -1
- package/dist/archive.d.ts +16 -2
- package/dist/archive.d.ts.map +1 -1
- package/dist/archive.js +63 -13
- package/dist/archive.js.map +1 -1
- package/dist/deploy.d.ts +4 -0
- package/dist/deploy.d.ts.map +1 -1
- package/dist/deploy.js +26 -8
- package/dist/deploy.js.map +1 -1
- package/dist/function-description.d.ts +6 -0
- package/dist/function-description.d.ts.map +1 -0
- package/dist/function-description.js +29 -0
- package/dist/function-description.js.map +1 -0
- package/dist/init.d.ts +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +4 -3
- package/dist/init.js.map +1 -1
- package/dist/lark-cli-error.d.ts +2 -0
- package/dist/lark-cli-error.d.ts.map +1 -1
- package/dist/lark-cli-error.js +42 -7
- package/dist/lark-cli-error.js.map +1 -1
- package/dist/lark-cli.d.ts.map +1 -1
- package/dist/lark-cli.js +16 -4
- package/dist/lark-cli.js.map +1 -1
- package/dist/mutation.d.ts +1 -1
- package/dist/mutation.d.ts.map +1 -1
- package/dist/mutation.js +5 -5
- package/dist/mutation.js.map +1 -1
- package/dist/program.d.ts.map +1 -1
- package/dist/program.js +7 -2
- package/dist/program.js.map +1 -1
- package/dist/project-context.d.ts.map +1 -1
- package/dist/project-context.js +42 -6
- package/dist/project-context.js.map +1 -1
- package/dist/public-sql-error.d.ts.map +1 -1
- package/dist/public-sql-error.js +2 -18
- package/dist/public-sql-error.js.map +1 -1
- package/dist/pull.d.ts +1 -0
- package/dist/pull.d.ts.map +1 -1
- package/dist/pull.js +13 -0
- package/dist/pull.js.map +1 -1
- package/package.json +1 -1
- package/skills/lark-base-database/SKILL.md +32 -12
- package/skills/lark-base-database/references/ai-function.md +19 -7
- package/skills/lark-base-database/references/base-capability-routing.md +4 -4
- package/skills/lark-base-database/references/base-sql-errors.generated.md +845 -0
- package/skills/lark-base-database/references/database-function-limitations.md +20 -37
- package/skills/lark-base-database/references/database-function.md +39 -42
- package/skills/lark-base-database/references/development.md +4 -2
- package/skills/lark-base-database/references/faas.md +9 -7
- package/skills/lark-base-database/references/file-transfer.md +1 -1
- package/skills/lark-base-database/references/frontend-design-guide.md +163 -33
- package/skills/lark-base-database/references/mutations.md +4 -4
- package/skills/lark-base-database/references/notifications.md +9 -9
- package/skills/lark-base-database/references/release.md +10 -10
- package/skills/lark-base-database/references/style-guide.md +2 -2
- package/skills/lark-base-database/references/verification.md +6 -4
- package/skills/lark-base-database/references/workspace.md +15 -13
- package/templates/database-starter/gitignore +7 -0
- package/templates/database-starter/mutations/README.md +1 -1
- package/templates/database-starter/page/package-lock.json +4 -4
- package/templates/database-starter/page/package.json +1 -1
- package/templates/database-starter/page/src/components/floating-button/assets/toast-close.svg +3 -0
- package/templates/database-starter/page/src/components/floating-button/assets/toast-info.svg +5 -0
- package/templates/database-starter/page/src/components/floating-button/assets/tooltip-caret.svg +3 -0
- package/templates/database-starter/page/src/components/floating-button/index.tsx +140 -25
- package/templates/database-starter/page/src/components/floating-button/utils/request-edit.test.ts +42 -0
- package/templates/database-starter/page/src/components/floating-button/utils/request-edit.ts +21 -0
- package/templates/database-starter/page/src/main.test.ts +28 -0
- package/templates/database-starter/page/src/main.tsx +2 -0
- package/skills/lark-base-database/references/design/components/actions-fields.md +0 -16
- package/skills/lark-base-database/references/design/components/code/controls.css +0 -122
- package/skills/lark-base-database/references/design/components/code/controls.tsx +0 -73
- package/skills/lark-base-database/references/design/components/code/date-picker.tsx +0 -42
- package/skills/lark-base-database/references/design/components/code/floating.tsx +0 -41
- package/skills/lark-base-database/references/design/components/code/overlays.tsx +0 -19
- package/skills/lark-base-database/references/design/components/code/select.tsx +0 -34
- package/skills/lark-base-database/references/design/components/code/styles.d.ts +0 -1
- package/skills/lark-base-database/references/design/components/code/theme.css +0 -49
- package/skills/lark-base-database/references/design/components/code/usage.tsx +0 -38
- package/skills/lark-base-database/references/design/components/date-time-number.md +0 -9
- package/skills/lark-base-database/references/design/components/feedback-navigation.md +0 -9
- package/skills/lark-base-database/references/design/components/index.md +0 -18
- package/skills/lark-base-database/references/design/components/overlays.md +0 -9
- package/skills/lark-base-database/references/design/components/patterns/flat-business.md +0 -7
- package/skills/lark-base-database/references/design/components/selection.md +0 -24
- package/skills/lark-base-database/references/design/components.md +0 -91
- package/skills/lark-base-database/references/design/composition.md +0 -90
- package/skills/lark-base-database/references/design/control-recipes.md +0 -107
- package/skills/lark-base-database/references/design/examples.md +0 -172
- package/skills/lark-base-database/references/design/foundations.md +0 -126
- package/skills/lark-base-database/references/design/review.md +0 -82
- package/skills/lark-base-database/references/design/scenes/analysis-report.md +0 -66
- package/skills/lark-base-database/references/design/scenes/brand-showcase.md +0 -70
- package/skills/lark-base-database/references/design/scenes/business-operations.md +0 -85
- package/skills/lark-base-database/references/design/scenes/campaign-report.md +0 -66
- package/skills/lark-base-database/references/design/scenes/dashboard.md +0 -77
- package/skills/lark-base-database/references/design/scenes/festive-campaign.md +0 -69
- package/skills/lark-base-database/references/design/theme.md +0 -27
- package/skills/lark-base-database/references/design/typography.md +0 -56
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { useEffect, useId, useRef, useState, type KeyboardEvent } from 'react';
|
|
2
|
-
import { Button, Field } from './controls';
|
|
3
|
-
import { Floating } from './floating';
|
|
4
|
-
|
|
5
|
-
const iso = (date: Date) => `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
|
6
|
-
const parse = (value: string) => { const [y, m, d] = value.split('-').map(Number); return new Date(y, m - 1, d); };
|
|
7
|
-
const shift = (value: string, days: number) => { const d = parse(value); d.setDate(d.getDate() + days); return iso(d); };
|
|
8
|
-
const monthShift = (value: string, months: number) => { const d = parse(value), day = d.getDate(); const target = new Date(d.getFullYear(), d.getMonth() + months, 1); target.setDate(Math.min(day, new Date(target.getFullYear(), target.getMonth() + 1, 0).getDate())); return iso(target); };
|
|
9
|
-
|
|
10
|
-
export function DatePicker({ label, value, onChange, min, max, disabledDate, error, help, disabled = false }: { label: string; value: string | null; onChange: (value: string | null) => void; min?: string; max?: string; disabledDate?: (date: string) => boolean; error?: string; help?: string; disabled?: boolean }) {
|
|
11
|
-
const id = useId(), ref = useRef<HTMLButtonElement>(null), [open, setOpen] = useState(false);
|
|
12
|
-
const [month, setMonth] = useState(() => value ? parse(value) : new Date());
|
|
13
|
-
const [active, setActive] = useState(() => value ?? iso(new Date()));
|
|
14
|
-
const today = iso(new Date());
|
|
15
|
-
const start = new Date(month.getFullYear(), month.getMonth(), 1);
|
|
16
|
-
start.setDate(1 - (start.getDay() + 6) % 7);
|
|
17
|
-
const days = Array.from({ length: 42 }, (_, i) => { const d = new Date(start); d.setDate(start.getDate() + i); return iso(d); });
|
|
18
|
-
const allowed = (date: string) => (!min || date >= min) && (!max || date <= max) && !disabledDate?.(date);
|
|
19
|
-
const close = () => setOpen(false);
|
|
20
|
-
const focusDay = (next: string) => { const direction = next >= active ? 1 : -1; let target = next; for (let i = 0; i < 42 && !allowed(target); i++) target = shift(target, direction); if (!allowed(target)) return; setActive(target); setMonth(parse(target)); requestAnimationFrame(() => document.getElementById(`${id}-${target}`)?.focus()); };
|
|
21
|
-
useEffect(() => { if (open) requestAnimationFrame(() => document.getElementById(`${id}-${active}`)?.focus()); }, [open]);
|
|
22
|
-
const onCalendarKey = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
23
|
-
if (!(event.target instanceof HTMLButtonElement) || !event.target.dataset.date) return;
|
|
24
|
-
const current = event.target.dataset.date;
|
|
25
|
-
const offsets: Record<string, number> = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -7, ArrowDown: 7 };
|
|
26
|
-
let next = event.key in offsets ? shift(current, offsets[event.key]) : null;
|
|
27
|
-
if (event.key === 'Home') next = shift(current, -((parse(current).getDay() + 6) % 7));
|
|
28
|
-
if (event.key === 'End') next = shift(current, 6 - ((parse(current).getDay() + 6) % 7));
|
|
29
|
-
if (event.key === 'PageUp' || event.key === 'PageDown') next = monthShift(current, (event.key === 'PageUp' ? -1 : 1) * (event.shiftKey ? 12 : 1));
|
|
30
|
-
if (next) { event.preventDefault(); focusDay(next); }
|
|
31
|
-
};
|
|
32
|
-
return <Field id={id} label={label} error={error} help={help}>
|
|
33
|
-
<button id={id} ref={ref} type="button" className="ds-trigger" aria-label={`${label}:${value ?? '选择日期'}`} aria-haspopup="dialog" aria-expanded={open} aria-invalid={!!error || undefined} aria-describedby={error ? `${id}-message` : help ? `${id}-help` : undefined} disabled={disabled} onClick={() => { if (!open) { setMonth(value ? parse(value) : new Date()); setActive(value ?? today); } setOpen(!open); }}><span className="ds-trigger-value">{value ? value.replaceAll('-', ' / ') : '选择日期'}</span><span className="ds-trigger-caret" aria-hidden="true">▦</span></button>
|
|
34
|
-
<Floating triggerRef={ref} open={open} onClose={close} ariaLabel={`${label}选择器`} width={304}>
|
|
35
|
-
<div onKeyDown={onCalendarKey}><div className="ds-calendar-head"><Button iconOnly aria-label="上个月" onClick={() => focusDay(iso(new Date(month.getFullYear(), month.getMonth() - 1, 1)))}>‹</Button><span aria-live="polite">{month.getFullYear()} 年 {month.getMonth() + 1} 月</span><Button iconOnly aria-label="下个月" onClick={() => focusDay(iso(new Date(month.getFullYear(), month.getMonth() + 1, 1)))}>›</Button></div>
|
|
36
|
-
<div role="grid" aria-label={`${month.getFullYear()}年${month.getMonth() + 1}月`}><div role="row" className="ds-calendar-week ds-calendar-weekdays">{['一','二','三','四','五','六','日'].map(name => <span role="columnheader" key={name}>{name}</span>)}</div>
|
|
37
|
-
{Array.from({ length: 6 }, (_, row) => <div role="row" className="ds-calendar-week" key={row}>{days.slice(row * 7, row * 7 + 7).map(date => <div role="gridcell" aria-selected={date === value} key={date}><button id={`${id}-${date}`} className="ds-calendar-day" type="button" data-date={date} data-outside={parse(date).getMonth() !== month.getMonth()} data-today={date === today} aria-current={date === today ? 'date' : undefined} aria-selected={date === value} aria-label={`${parse(date).getFullYear()}年${parse(date).getMonth() + 1}月${parse(date).getDate()}日`} tabIndex={date === active ? 0 : -1} disabled={!allowed(date)} onClick={() => { onChange(date); close(); ref.current?.focus(); }}>{parse(date).getDate()}</button></div>)}</div>)}</div>
|
|
38
|
-
<div className="ds-popup-footer"><Button onClick={() => { onChange(null); close(); ref.current?.focus(); }}>清空</Button><Button variant="secondary" disabled={!allowed(today)} onClick={() => { onChange(today); close(); ref.current?.focus(); }}>今天</Button></div>
|
|
39
|
-
</div>
|
|
40
|
-
</Floating>
|
|
41
|
-
</Field>;
|
|
42
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { createPortal } from 'react-dom';
|
|
2
|
-
import { useCallback, useLayoutEffect, useRef, type CSSProperties, type ReactNode, type RefObject } from 'react';
|
|
3
|
-
|
|
4
|
-
const roles = ['page','surface','popover','text','muted','placeholder','action','on-action','focus','border','field','danger','warning','success','shadow','scrim'];
|
|
5
|
-
export function portalTheme(trigger: HTMLElement | null): { recipe: string; vars: CSSProperties } {
|
|
6
|
-
const root = trigger?.closest<HTMLElement>('.ds-theme');
|
|
7
|
-
const css = root ? getComputedStyle(root) : null;
|
|
8
|
-
const vars: Record<string, string> = {};
|
|
9
|
-
for (const role of roles) { const value = css?.getPropertyValue(`--ds-${role}`).trim(); if (value) vars[`--ds-${role}`] = value; }
|
|
10
|
-
return { recipe: root?.dataset.dsRecipe ?? 'a', vars: vars as CSSProperties };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function Floating({ triggerRef, open, onClose, children, ariaLabel, role = 'dialog', width }: { triggerRef: RefObject<HTMLElement | null>; open: boolean; onClose: () => void; children: ReactNode; ariaLabel: string; role?: 'dialog' | 'menu'; width?: number }) {
|
|
14
|
-
const panelRef = useRef<HTMLDivElement>(null);
|
|
15
|
-
const position = useCallback(() => {
|
|
16
|
-
const trigger = triggerRef.current, panel = panelRef.current;
|
|
17
|
-
if (!trigger || !panel) return;
|
|
18
|
-
const rect = trigger.getBoundingClientRect(), margin = 12;
|
|
19
|
-
const targetWidth = Math.min(window.innerWidth - margin * 2, width ?? Math.max(rect.width, 240));
|
|
20
|
-
panel.style.width = `${targetWidth}px`;
|
|
21
|
-
panel.style.maxHeight = `${Math.max(0, window.innerHeight - margin * 2)}px`;
|
|
22
|
-
const height = panel.getBoundingClientRect().height;
|
|
23
|
-
const below = window.innerHeight - rect.bottom - margin, above = rect.top - margin;
|
|
24
|
-
const rawTop = below >= height || below >= above ? rect.bottom + 6 : rect.top - height - 6;
|
|
25
|
-
panel.style.top = `${Math.max(margin, Math.min(rawTop, window.innerHeight - height - margin))}px`;
|
|
26
|
-
panel.style.left = `${Math.max(margin, Math.min(rect.left, window.innerWidth - targetWidth - margin))}px`;
|
|
27
|
-
}, [triggerRef, width]);
|
|
28
|
-
useLayoutEffect(() => {
|
|
29
|
-
if (!open) return;
|
|
30
|
-
position();
|
|
31
|
-
const observer = new ResizeObserver(position); if (panelRef.current) observer.observe(panelRef.current);
|
|
32
|
-
const handlePointer = (event: PointerEvent) => { const node = event.target as Node; if (!panelRef.current?.contains(node) && !triggerRef.current?.contains(node)) onClose(); };
|
|
33
|
-
const handleKey = (event: KeyboardEvent) => { if (event.key === 'Escape') { event.preventDefault(); event.stopPropagation(); onClose(); triggerRef.current?.focus(); } };
|
|
34
|
-
document.addEventListener('pointerdown', handlePointer); document.addEventListener('keydown', handleKey, true);
|
|
35
|
-
window.addEventListener('resize', position); window.addEventListener('scroll', position, true);
|
|
36
|
-
return () => { observer.disconnect(); document.removeEventListener('pointerdown', handlePointer); document.removeEventListener('keydown', handleKey, true); window.removeEventListener('resize', position); window.removeEventListener('scroll', position, true); };
|
|
37
|
-
}, [open, onClose, position, triggerRef]);
|
|
38
|
-
if (!open || typeof document === 'undefined') return null;
|
|
39
|
-
const theme = portalTheme(triggerRef.current);
|
|
40
|
-
return createPortal(<div ref={panelRef} className="ds-theme ds-popover" data-ds-recipe={theme.recipe} style={theme.vars} role={role} aria-label={ariaLabel}>{children}</div>, document.body);
|
|
41
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef, useState, type ReactNode } from 'react';
|
|
2
|
-
import { Button } from './controls';
|
|
3
|
-
import { Floating } from './floating';
|
|
4
|
-
|
|
5
|
-
export function Menu({ label, items }: { label: string; items: { id: string; label: string; disabled?: boolean; danger?: boolean; onSelect: () => void }[] }) {
|
|
6
|
-
const ref = useRef<HTMLButtonElement>(null), [open, setOpen] = useState(false);
|
|
7
|
-
useEffect(() => { if (open) requestAnimationFrame(() => document.querySelector<HTMLButtonElement>('[data-ds-menu] [role="menuitem"]:not(:disabled)')?.focus()); }, [open]);
|
|
8
|
-
return <><Button ref={ref} aria-haspopup="menu" aria-expanded={open} onClick={() => setOpen(!open)} onKeyDown={event => { if (['ArrowDown', 'ArrowUp'].includes(event.key)) { event.preventDefault(); setOpen(true); } }}>{label} ⌄</Button>
|
|
9
|
-
<Floating triggerRef={ref} open={open} onClose={() => setOpen(false)} ariaLabel={label} role="menu"><div data-ds-menu onKeyDown={event => { const nodes = [...event.currentTarget.querySelectorAll<HTMLButtonElement>('[role="menuitem"]:not(:disabled)')]; const current = nodes.indexOf(document.activeElement as HTMLButtonElement); if (!['ArrowDown','ArrowUp','Home','End'].includes(event.key) || !nodes.length) return; event.preventDefault(); const next = event.key === 'Home' ? 0 : event.key === 'End' ? nodes.length - 1 : event.key === 'ArrowDown' ? (current + 1) % nodes.length : (current - 1 + nodes.length) % nodes.length; nodes[next].focus(); }}>{items.map(item => <button className="ds-option" type="button" role="menuitem" key={item.id} data-danger={item.danger} disabled={item.disabled} onClick={() => { item.onSelect(); setOpen(false); ref.current?.focus(); }}>{item.label}</button>)}</div></Floating>
|
|
10
|
-
</>;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function Dialog({ title, open, onClose, children, actions, drawer = false }: { title: string; open: boolean; onClose: () => void; children: ReactNode; actions?: ReactNode; drawer?: boolean }) {
|
|
14
|
-
const ref = useRef<HTMLDialogElement>(null);
|
|
15
|
-
useEffect(() => { const el = ref.current; if (!el) return; if (open && !el.open) el.showModal(); else if (!open && el.open) el.close(); }, [open]);
|
|
16
|
-
return <dialog ref={ref} className="ds-dialog" data-drawer={drawer} aria-label={title} onClose={onClose}><div className="ds-dialog-head"><h2>{title}</h2><Button iconOnly aria-label="关闭" onClick={() => { ref.current?.close(); onClose(); }}>×</Button></div>{children}{actions && <div className="ds-dialog-actions">{actions}</div>}</dialog>;
|
|
17
|
-
}
|
|
18
|
-
export function Drawer(props: Omit<Parameters<typeof Dialog>[0], 'drawer'>) { return <Dialog {...props} drawer />; }
|
|
19
|
-
export function Toast({ message }: { message: string | null }) { return message ? <div className="ds-toast" role="status">{message}</div> : null; }
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { useEffect, useId, useRef, useState, type KeyboardEvent } from 'react';
|
|
2
|
-
import { Button, Field, type NamedOption } from './controls';
|
|
3
|
-
import { Floating } from './floating';
|
|
4
|
-
|
|
5
|
-
export function Select<T extends string>({ label, options, value, onChange, searchable = false, multiple = false, disabled = false, error, help, placeholder = '请选择' }: { label: string; options: NamedOption<T>[]; value: T | T[] | null; onChange: (value: T | T[] | null) => void; searchable?: boolean; multiple?: boolean; disabled?: boolean; error?: string; help?: string; placeholder?: string }) {
|
|
6
|
-
const id = useId(), ref = useRef<HTMLButtonElement>(null), searchRef = useRef<HTMLInputElement>(null);
|
|
7
|
-
const [open, setOpen] = useState(false), [query, setQuery] = useState(''), [keyboardFocus, setKeyboardFocus] = useState(false);
|
|
8
|
-
const selected = multiple ? (Array.isArray(value) ? value : []) : value;
|
|
9
|
-
const matches = options.filter(option => option.label.toLocaleLowerCase().includes(query.toLocaleLowerCase()));
|
|
10
|
-
const display = multiple ? options.filter(option => Array.isArray(selected) && selected.includes(option.value)).map(option => option.label) : options.filter(option => selected === option.value).map(option => option.label);
|
|
11
|
-
const caption = display.length > 2 ? `${display.slice(0, 2).join('、')} +${display.length - 2}` : display.join('、') || placeholder;
|
|
12
|
-
const close = () => { setOpen(false); setQuery(''); setKeyboardFocus(false); };
|
|
13
|
-
const openedByKeyboard = useRef(false);
|
|
14
|
-
useEffect(() => { if (!open || !openedByKeyboard.current) return; if (searchable) searchRef.current?.focus(); else document.querySelector<HTMLButtonElement>(`[data-ds-select="${id}"] [role="option"]:not(:disabled)`)?.focus(); }, [open, searchable, id]);
|
|
15
|
-
const show = (fromKeyboard: boolean) => { openedByKeyboard.current = fromKeyboard; setKeyboardFocus(fromKeyboard); setOpen(true); };
|
|
16
|
-
const choose = (option: NamedOption<T>) => { if (option.disabled) return; if (multiple) { const current = Array.isArray(value) ? value : []; onChange(current.includes(option.value) ? current.filter(item => item !== option.value) : [...current, option.value]); } else { onChange(option.value); close(); ref.current?.focus(); } };
|
|
17
|
-
const move = (event: KeyboardEvent<HTMLElement>) => {
|
|
18
|
-
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)) return;
|
|
19
|
-
const nodes = [...document.querySelectorAll<HTMLButtonElement>(`[data-ds-select="${id}"] [role="option"]:not(:disabled)`)];
|
|
20
|
-
if (!nodes.length) return; event.preventDefault(); setKeyboardFocus(true);
|
|
21
|
-
const current = nodes.indexOf(document.activeElement as HTMLButtonElement);
|
|
22
|
-
const index = event.key === 'Home' ? 0 : event.key === 'End' ? nodes.length - 1 : event.key === 'ArrowDown' ? (current + 1) % nodes.length : (current - 1 + nodes.length) % nodes.length;
|
|
23
|
-
nodes[index]?.focus();
|
|
24
|
-
};
|
|
25
|
-
return <Field id={id} label={label} error={error} help={help}>
|
|
26
|
-
<button id={id} ref={ref} type="button" className="ds-trigger" disabled={disabled} aria-label={`${label}:${caption}`} aria-haspopup="dialog" aria-expanded={open} aria-invalid={!!error || undefined} aria-describedby={error ? `${id}-message` : help ? `${id}-help` : undefined} onClick={event => open ? close() : show(event.detail === 0)} onKeyDown={event => { if (!open && ['ArrowDown', 'ArrowUp'].includes(event.key)) { event.preventDefault(); show(true); } }}><span className="ds-trigger-value">{caption}</span><span className="ds-trigger-caret" aria-hidden="true">⌄</span></button>
|
|
27
|
-
<Floating triggerRef={ref} open={open} onClose={close} ariaLabel={label}><div data-ds-select={id} onKeyDown={move}>
|
|
28
|
-
{searchable && <div className="ds-popup-search" data-keyboard-focus={keyboardFocus} onPointerDown={() => setKeyboardFocus(false)}><span aria-hidden="true">⌕</span><input ref={searchRef} aria-label={`搜索${label}`} value={query} placeholder="搜索选项" onChange={event => setQuery(event.target.value)} onKeyDown={event => { if (event.key === 'Tab') setKeyboardFocus(true); }} /></div>}
|
|
29
|
-
<div className="ds-option-list" role="listbox" aria-label={label} aria-multiselectable={multiple || undefined}>
|
|
30
|
-
{matches.length ? matches.map(option => { const on = multiple ? Array.isArray(selected) && selected.includes(option.value) : selected === option.value; return <button className="ds-option" key={option.value} role="option" aria-selected={on} type="button" tabIndex={-1} disabled={option.disabled} onClick={() => choose(option)}><span>{option.label}</span><span aria-hidden="true">{on ? '✓' : ''}</span></button>; }) : <p className="ds-popup-empty" role="status">没有匹配选项</p>}
|
|
31
|
-
</div>{multiple && <div className="ds-popup-footer"><Button onClick={() => onChange([])}>清空</Button><Button variant="secondary" onClick={() => { close(); ref.current?.focus(); }}>完成</Button></div>}
|
|
32
|
-
</div></Floating>
|
|
33
|
-
</Field>;
|
|
34
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
declare module '*.css';
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/* Reference contract. Map these roles to the host application's real CSS colors. */
|
|
2
|
-
.ds-theme {
|
|
3
|
-
--ds-page: Canvas;
|
|
4
|
-
--ds-surface: Canvas;
|
|
5
|
-
--ds-popover: Canvas;
|
|
6
|
-
--ds-text: CanvasText;
|
|
7
|
-
--ds-muted: color-mix(in srgb, var(--ds-text) 62%, var(--ds-page));
|
|
8
|
-
--ds-placeholder: color-mix(in srgb, var(--ds-text) 52%, var(--ds-field));
|
|
9
|
-
--ds-action: CanvasText;
|
|
10
|
-
--ds-on-action: Canvas;
|
|
11
|
-
--ds-focus: var(--ds-action);
|
|
12
|
-
--ds-border: color-mix(in srgb, var(--ds-text) 20%, var(--ds-surface));
|
|
13
|
-
--ds-field: var(--ds-surface);
|
|
14
|
-
--ds-danger: #b42318;
|
|
15
|
-
--ds-warning: #956014;
|
|
16
|
-
--ds-success: #217047;
|
|
17
|
-
--ds-shadow: rgb(0 0 0 / 13%);
|
|
18
|
-
--ds-scrim: rgb(0 0 0 / 24%);
|
|
19
|
-
--ds-action-hover: color-mix(in srgb, var(--ds-action) 86%, var(--ds-text));
|
|
20
|
-
--ds-action-soft: color-mix(in srgb, var(--ds-action) 9%, var(--ds-surface));
|
|
21
|
-
--ds-action-soft-hover: color-mix(in srgb, var(--ds-action) 15%, var(--ds-surface));
|
|
22
|
-
--ds-hover: color-mix(in srgb, var(--ds-text) 5%, var(--ds-popover));
|
|
23
|
-
--ds-field-hover: color-mix(in srgb, var(--ds-text) 4%, var(--ds-field));
|
|
24
|
-
--ds-error-soft: color-mix(in srgb, var(--ds-danger) 5%, var(--ds-field));
|
|
25
|
-
font-family: var(--ds-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif);
|
|
26
|
-
color: var(--ds-text);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/* Palette values above are safe neutral fallbacks, not the product's chosen brand.
|
|
30
|
-
If using a Portal to document.body, put ds-theme and the actual role overrides
|
|
31
|
-
on that portal root too, or use the host theme provider's Portal container. */
|
|
32
|
-
.ds-theme[data-ds-recipe="a"] {
|
|
33
|
-
--ds-height: 36px; --ds-radius: 6px; --ds-font: 14px;
|
|
34
|
-
--ds-pad: 12px; --ds-gap: 6px;
|
|
35
|
-
--ds-field-border: var(--ds-border);
|
|
36
|
-
--ds-field-fill: var(--ds-field);
|
|
37
|
-
}
|
|
38
|
-
.ds-theme[data-ds-recipe="b"] {
|
|
39
|
-
--ds-height: 32px; --ds-radius: 4px; --ds-font: 13px;
|
|
40
|
-
--ds-pad: 10px; --ds-gap: 5px;
|
|
41
|
-
--ds-field-border: transparent;
|
|
42
|
-
--ds-field-fill: color-mix(in srgb, var(--ds-text) 4%, var(--ds-field));
|
|
43
|
-
}
|
|
44
|
-
.ds-theme[data-ds-recipe="c"] {
|
|
45
|
-
--ds-height: 40px; --ds-radius: 10px; --ds-font: 14px;
|
|
46
|
-
--ds-pad: 14px; --ds-gap: 8px;
|
|
47
|
-
--ds-field-border: transparent;
|
|
48
|
-
--ds-field-fill: color-mix(in srgb, var(--ds-text) 5%, var(--ds-field));
|
|
49
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { useState, type CSSProperties } from 'react';
|
|
2
|
-
import { Button, Checkbox, NumberStepper, Pagination, Progress, RadioGroup, Segmented, Slider, Status, Switch, Tabs, Tag, Textarea, TextField, AffixInput, type NamedOption } from './controls';
|
|
3
|
-
import { Select } from './select';
|
|
4
|
-
import { DatePicker } from './date-picker';
|
|
5
|
-
import { Dialog, Drawer, Menu, Toast } from './overlays';
|
|
6
|
-
import './theme.css';
|
|
7
|
-
import './controls.css';
|
|
8
|
-
|
|
9
|
-
/* Reference wiring only. Supply real values, limits and callbacks from the host Page. */
|
|
10
|
-
type ExampleProps = { recipe: 'a' | 'b' | 'c'; theme: CSSProperties; choices: NamedOption[]; timeChoices: NamedOption[]; onSave: (values: { name: string; choice: string | null; time: string | null; date: string | null; count: number; note: string }) => Promise<void>; pageCount: number; onPageChange: (page: number) => void };
|
|
11
|
-
export function ControlReference({ recipe, theme, choices, timeChoices, onSave, pageCount, onPageChange }: ExampleProps) {
|
|
12
|
-
const [name, setName] = useState(''), [choice, setChoice] = useState<string | null>(null), [many, setMany] = useState<string[]>([]);
|
|
13
|
-
const [date, setDate] = useState<string | null>(null), [time, setTime] = useState<string | null>(null), [count, setCount] = useState(1), [note, setNote] = useState('');
|
|
14
|
-
const [checked, setChecked] = useState(false), [enabled, setEnabled] = useState(false), [mode, setMode] = useState('list');
|
|
15
|
-
const [tab, setTab] = useState('one'), [level, setLevel] = useState(1), [page, setPage] = useState(1);
|
|
16
|
-
const [dialog, setDialog] = useState(false), [drawer, setDrawer] = useState(false), [toast, setToast] = useState<string | null>(null), [saving, setSaving] = useState(false);
|
|
17
|
-
const [nameError, setNameError] = useState<string | undefined>();
|
|
18
|
-
const save = async () => { if (!name.trim()) { setNameError('请填写名称'); return; } setNameError(undefined); setSaving(true); try { await onSave({ name, choice, time, date, count, note }); setToast('已保存'); } catch { setToast('保存失败'); } finally { setSaving(false); } };
|
|
19
|
-
return <div className="ds-theme" data-ds-recipe={recipe} style={theme}>
|
|
20
|
-
<header className="ds-flat-toolbar"><h1>页面标题</h1><Button variant="primary" onClick={() => setDialog(true)}>新建</Button></header>
|
|
21
|
-
<div className="ds-flat-toolbar"><TextField label="搜索" type="search" value={name} onChange={event => { setName(event.target.value); setNameError(undefined); }} error={nameError} /><Select label="单选" options={choices} value={choice} onChange={value => setChoice(value as string | null)} searchable /><Select label="多选" options={choices} value={many} onChange={value => setMany(value as string[])} multiple searchable /><DatePicker label="日期" value={date} onChange={setDate} /><Select label="时段" options={timeChoices} value={time} onChange={value => setTime(value as string | null)} /><Menu label="更多" items={[{ id: 'open', label: '查看详情', onSelect: () => setDrawer(true) }]} /></div>
|
|
22
|
-
<form className="ds-flat-form" onSubmit={event => { event.preventDefault(); void save(); }}>
|
|
23
|
-
<NumberStepper label="数量" value={count} onChange={setCount} min={0} /><AffixInput label="金额" prefix="¥" suffix="/ 单位" inputMode="decimal" />
|
|
24
|
-
<div data-full="true"><Textarea label="备注" value={note} onChange={event => setNote(event.target.value)} /></div>
|
|
25
|
-
<Checkbox label="勾选" checked={checked} onChange={event => setChecked(event.target.checked)} /><Switch label="启用" checked={enabled} onChange={setEnabled} />
|
|
26
|
-
<RadioGroup label="类别" name="category" options={[{ value: 'one', label: '甲' }, { value: 'two', label: '乙' }]} value={tab} onChange={setTab} />
|
|
27
|
-
<Segmented label="视图" options={[{ value: 'list', label: '列表' }, { value: 'grid', label: '网格' }]} value={mode} onChange={setMode} />
|
|
28
|
-
<div data-full="true"><Slider label="比例" value={level} onChange={setLevel} min={0} max={10} /><Progress label="进度" value={level} max={10} /></div>
|
|
29
|
-
<div data-full="true"><Status tone="success">正常</Status> <Tag onRemove={() => setMany([])} removeLabel="移除筛选">已选项</Tag></div>
|
|
30
|
-
<div data-full="true"><Button type="submit" variant="primary" loading={saving}>保存</Button> <Button type="reset" onClick={() => { setName(''); setNote(''); }}>清空</Button></div>
|
|
31
|
-
</form>
|
|
32
|
-
<Tabs label="内容切换" options={[{ value: 'one', label: '第一页' }, { value: 'two', label: '第二页' }]} value={tab} onChange={setTab}><p>由业务提供当前页内容</p></Tabs>
|
|
33
|
-
<Pagination page={page} pageCount={pageCount} onPageChange={next => { setPage(next); onPageChange(next); }} />
|
|
34
|
-
<Dialog title="编辑" open={dialog} onClose={() => setDialog(false)} actions={<Button variant="primary" onClick={() => setDialog(false)}>完成</Button>}><p>由业务提供表单内容</p></Dialog>
|
|
35
|
-
<Drawer title="详情" open={drawer} onClose={() => setDrawer(false)}><p>由业务提供详情内容</p></Drawer>
|
|
36
|
-
<Toast message={toast} />
|
|
37
|
-
</div>;
|
|
38
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# 日期、时段与数值
|
|
2
|
-
|
|
3
|
-
日期参考见 [DatePicker](code/date-picker.tsx) 与[共享浮层](code/floating.tsx);时段选择使用[选择控件](code/select.tsx)的同一面板体系;步进和前后缀见[基础控件](code/controls.tsx)。颜色引用[主题](../theme.md),尺寸引用[配方](../control-recipes.md)。本地演示同样使用这套可主题化面板;`input[type=date]`、`input[type=time]` 的系统面板不因数据是 mock 就成为默认实现。时间若需要自由输入、秒级选择或复杂时区,按业务契约采用工程中成熟的可主题化时间组件,不能把预览的固定时段选项硬套进去。
|
|
4
|
-
|
|
5
|
-
日期触发器与输入同高;展开面板可从约 304px 宽起,月份标题和前后月动作同排,星期行与日期按七列对齐。日期格可从 34px 高起,当前日期、已选日期、禁用日期和键盘位置分别表达:今天可用主题文字/下划线,选中用主题实底及对应前景,禁用不能点击,键盘位置不能消失。无用途的外层卡片不参与日期组件布局。
|
|
6
|
-
|
|
7
|
-
日历按本地日期的年/月/日处理,不把仅表示日期的值交给 UTC 时间戳导致跨日。左右键按日、上下键按周、PageUp/PageDown 按月移动;月份标题及时更新。打开后焦点进入可操作日期或导航,Escape/选择/清空/今天后的焦点按现有组件行为恢复。最大最小日期、不可选日期和时段选项由业务传入;预览日期与限制不能复制。日期范围若本次任务需要,沿用当前工程可靠的范围组件或另补对应状态规范,不能把单日期示例假装已覆盖范围行为。
|
|
8
|
-
|
|
9
|
-
数字步进外壳与输入共用一层边界,减/增按钮有各自的键盘位置反馈;值与按钮对齐。步长、精度、最小/最大值和单位来自当前数据契约,不复制预览的 `1–99`。金额前后缀与值各有固定槽位,单位不遮住长值;仅输入状态视觉共用,数值解析、货币精度和提交格式由业务处理。
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# 状态、标签、进度、分页与轻提示
|
|
2
|
-
|
|
3
|
-
参考实现见 [controls.tsx](code/controls.tsx)、[overlays.tsx](code/overlays.tsx)及[共享样式](code/controls.css)。只在当前任务提供相应事实和交互时使用。
|
|
4
|
-
|
|
5
|
-
状态标记可用约 12px 文字配 5px 状态点,状态必须同时有文字;成功、警告、危险是独立语义角色,不用品牌色随机替代。可移除标签轻填充、紧凑内边距,关闭按钮约 22px 视觉槽位且有可访问名称;点击须改变真实选项或筛选状态。
|
|
6
|
-
|
|
7
|
-
滑块轨道可以从 4px 起,当前值与标签对应,支持键盘调整;范围、步长与数值格式来自业务。进度条只显示有依据的进度值,未知总量使用不定进度或现有加载状态,不编造百分比。预览中滑块同步进度条仅用于观察组合,业务页面不自动把两个量绑定。
|
|
8
|
-
|
|
9
|
-
分页沿用当前配方按钮高度、文字和焦点语言;当前页清楚,首页/末页禁用准确。切页连接实际查询并保留筛选、排序和范围含义,不能只改变按钮选中样式。页数多时只呈现需要的窗口及可达入口,不把全部页号挤进工具栏。轻提示用于真实操作结果或需要的局部反馈,避免把示例成功或演示等待复制进业务流程;需要保留的错误与恢复动作不能只靠几秒钟后消失的提示。
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# 组件参考索引
|
|
2
|
-
|
|
3
|
-
本次任务涉及新增、重做或调整交互控件时,在确定样式之前读取[基础配方](../control-recipes.md)、[主题规则](../theme.md)和对应组件章节。通用控件行为仍以[控件设计](../components.md)为准。已有规范只有在**本轮任务开始前已经存在、由实际页面调用且符合当前要求**时才优先继承;本轮刚定义的样式、未处理的库默认外观不能作为跳过配方的依据。
|
|
4
|
-
|
|
5
|
-
使用已有可靠组件库时,保留其语义、定位、键盘与焦点能力,通过主题或 slot 对照对应章节整理外观。首次实现或替换某类组件时,**实现前**读取相应 `.tsx`、`.css` 与调用示例;不能先写原生控件再在完成后补读参考。代码是适配材料,不要求另装整套组件库。只有本次涉及的组件才需要读取,不把所有代码一次加载。参考中的 `usage.tsx` 是最小接线示例,业务数据、值、限制和回调必须来自当前工程。
|
|
6
|
-
|
|
7
|
-
| 实际用到的组件 | 先读参数与行为 | 对照代码 |
|
|
8
|
-
| --- | --- | --- |
|
|
9
|
-
| 按钮主次/危险/图标、搜索和文本输入、标签/帮助/错误、Textarea | [按钮与字段](actions-fields.md) | [controls.tsx](code/controls.tsx)、[controls.css](code/controls.css) |
|
|
10
|
-
| 单选下拉、搜索选择、搜索多选、复选/单选/开关、分段与 Tabs | [选择与切换](selection.md) | [select.tsx](code/select.tsx)、[controls.tsx](code/controls.tsx)、[floating.tsx](code/floating.tsx) |
|
|
11
|
-
| 日期、时段、数字步进、金额前后缀 | [日期与数值](date-time-number.md) | [date-picker.tsx](code/date-picker.tsx)、[controls.tsx](code/controls.tsx)、[floating.tsx](code/floating.tsx) |
|
|
12
|
-
| 状态、可移除标签、滑块、进度、分页、轻提示 | [反馈与导航](feedback-navigation.md) | [controls.tsx](code/controls.tsx)、[overlays.tsx](code/overlays.tsx) |
|
|
13
|
-
| 菜单、Dialog、Drawer | [浮层](overlays.md) | [overlays.tsx](code/overlays.tsx)、[floating.tsx](code/floating.tsx) |
|
|
14
|
-
| 列表工具栏或直接排版的表单 | [组合场景](patterns/flat-business.md) | [usage.tsx](code/usage.tsx)、[controls.css](code/controls.css) |
|
|
15
|
-
|
|
16
|
-
所有组件共用[主题变量](code/theme.css)与[样式](code/controls.css);TypeScript 工程如需识别 CSS 导入,可使用[样式声明](code/styles.d.ts)。选定 A/B/C 后,一次映射当前应用的颜色角色,再把共享定义用于真实页面。颜色不随配方重选,Portal 中的浮层也继承同一主题。
|
|
17
|
-
|
|
18
|
-
这些参考依据已确认的本地组件预览整理;预览的介绍页、固定门店、日期、人数上限、短暂等待和演示成功均不属于生产组件。运行时只读取本目录内的参考。
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# 菜单、对话框与抽屉
|
|
2
|
-
|
|
3
|
-
参考实现见 [overlays.tsx](code/overlays.tsx)、[floating.tsx](code/floating.tsx)及[共享样式](code/controls.css)。展开面板与触发器使用同一套颜色角色和字体,Portal 后也要保留主题变量。
|
|
4
|
-
|
|
5
|
-
菜单面板靠近触发器,适配视口边缘,选项与当前配方密度协调;危险操作有真实危险含义,禁用项不可触发。方向键在可用项间移动,Escape 关闭并恢复触发器焦点。长菜单在面板内部滚动,不能溢出页面。操作由调用方传入,不复制预览中的复制、导出、移除假动作。
|
|
6
|
-
|
|
7
|
-
对话框可从约 480px 最大宽度起,抽屉可从约 440px 起;都受视口限制,内部标题、内容和动作共用对齐线。遮罩与浮层阴影用于层级分离,普通按钮和页内卡片不因此获得常驻阴影。对话框只在真正模态任务中约束背景,处理打开后的焦点、Escape、关闭及焦点恢复;抽屉长内容可滚动。若工程已有成熟组件,复用其定位、滚动锁定和焦点管理,再映射本规范的主题与比例。
|
|
8
|
-
|
|
9
|
-
浮层主体已经提供独立表面,不在内部额外垫一张包裹全部内容的装饰卡;字段本身的边界与错误仍保留。用户提交后的关闭与反馈必须对应业务返回,不能靠浮层样式推断保存成功。
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
# 组合场景:工具栏与直接排版表单
|
|
2
|
-
|
|
3
|
-
此前预览中的列表工具栏与新建表单作为组合参考,不作为固定页面模板。源码接线示意见 [usage.tsx](../code/usage.tsx)。普通业务页遵守[直接排版规则](../../composition.md#普通业务页直接排版):页头、搜索与筛选、列表/表单和动作直接组织在页面上;不先垫整页背景卡片,字段组也不逐组加卡。
|
|
4
|
-
|
|
5
|
-
工具栏让搜索、筛选与作用范围靠近其控制的内容;相同密度的触发器和输入共用高度。窄屏依据内容换行,不把长选择值挤成碎字。列表/表格保留真实字段、行操作、分页和必要网格/分隔;状态与搜索结果由业务提供。
|
|
6
|
-
|
|
7
|
-
表单用字段标签、网格、组标题和留白表达关系;短字段可以并列,长输入占整行。错误就近,提交动作归属清楚,辅助示例用可操作的紧凑行。Textarea 的清楚输入边界继续保留。示例业务对象、时间、人数与本地保存逻辑不得照搬,按主 Skill 的数据契约接入。
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# 选择与切换
|
|
2
|
-
|
|
3
|
-
适用于下拉、搜索选择、多选、复选、单选、开关、分段和 Tabs。参考实现见 [Select](code/select.tsx)、[基础控件](code/controls.tsx)、[浮层定位](code/floating.tsx)及[共享样式](code/controls.css)。工程已有可靠组件时对照状态与比例调整,不为套代码重写键盘行为。
|
|
4
|
-
|
|
5
|
-
## 单选、搜索与多选
|
|
6
|
-
|
|
7
|
-
触发器与同页输入共用配方高度、字号、圆角和输入表面。文字与尾部图标分槽;长值省略时保留可达完整选择。面板最小宽度以触发器与 240px 中较大者为起点,受视口边距约束;面板内边距约 6px,选项最小高度随当前配方,列表约 236px 后局部滚动。明确的任务需求可以调整,不把这些预览数值变成平台硬上限。
|
|
8
|
-
|
|
9
|
-
| 状态 | 外观与行为 |
|
|
10
|
-
| --- | --- |
|
|
11
|
-
| 默认/悬停 | 触发器边界与输入一致;选项悬停用轻量表面变化,不给每行加灰框 |
|
|
12
|
-
| 已选择 | 轻量主题底色和清楚文字,可加勾选标记;已选值不长期画成焦点态 |
|
|
13
|
-
| 键盘焦点 | 触发器单层焦点;展开后选项保留自己的位置反馈。两处是不同对象,不以“去双框”为由删掉选项反馈 |
|
|
14
|
-
| 搜索 | 鼠标打开不自动把搜索框画成聚焦态;点击后只出现插入光标,不新增矩形框、底线或整行填充。键盘进入保留局部焦点标记 |
|
|
15
|
-
| 多选 | 已选值紧凑显示、可回到面板移除;清空与完成入口语义明确。没有匹配选项与未选任何值分别表达 |
|
|
16
|
-
| 关闭 | Escape、外部点击或完成后按现有交互契约关闭;键盘关闭恢复触发器焦点,搜索与选项状态按组件任务处理 |
|
|
17
|
-
|
|
18
|
-
面板、Portal、搜索、选项、选中、禁用和空态必须继承同一主题。若当前平台明确采用原生选择界面,按平台要求;桌面需要自定义面板时,不能只美化原生 `select` 外壳后留下无法主题化的系统列表。
|
|
19
|
-
|
|
20
|
-
## 复选、单选、开关、分段与 Tabs
|
|
21
|
-
|
|
22
|
-
复选与单选标记视觉尺寸可从 16px 起,标签与标记同行对齐,命中区可大于标记本身。开关轨道可从 30×18px 起,选中与禁用状态受主题控制;业务决定“即时生效”还是“待保存”,不能因控件外观自行改变提交模式。它们不强行拉成工具栏按钮的完整高度。
|
|
23
|
-
|
|
24
|
-
分段切换和 Tabs 必须切换真实内容。当前项通过底色或底部指示及文字状态表达;键盘可按方向移动,焦点与选中有区别。B 的高密度风格可用更平面的底部指示,仍沿用同一主题色。未用到的控件不因为预览展示而加入业务页面。
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# 控件设计与复用
|
|
2
|
-
|
|
3
|
-
新建或整体调整控件外观时读取;局部修改继承应用已有变体。先有[字体规范](typography.md)与主题,再给当前需要的控件选定具体规范并复用。规范由 Agent 根据现有工程、业务密度和用户要求内部确定,不增加问卷、确认或单独交付文件。
|
|
4
|
-
|
|
5
|
-
## 决定一套当前够用的规范
|
|
6
|
-
|
|
7
|
-
检查工程现有 token、基础组件、变体与默认样式。**本次新增或调整控件外观时,先读 [A/B/C 控件配方](control-recipes.md)及[组件参考索引](components/index.md),再决定具体规格**;需要配色时读[组件主题](theme.md)。已有规范适用就复用,但必须是本轮开始前已经存在、实际被页面调用且符合要求;本轮刚自行定义的样式和未经处理的库默认外观不算继承依据。保留库提供的键盘、焦点、定位和状态行为,通过其主题接口或共享样式调整外观。只定义本次出现的 Button、Input、Select、Textarea 等,不为了齐全预建整个组件库。页面使用 Card/Panel 前先落实[普通业务页直接排版](composition.md#普通业务页直接排版):控件配方不要求外围卡片,页面和分组不自动加壳,确需的普通内容卡默认无描边、无投影。
|
|
8
|
-
|
|
9
|
-
| 决策 | 应落成的具体选择 |
|
|
10
|
-
| --- | --- |
|
|
11
|
-
| 密度与尺寸 | 当前默认高度、水平/垂直内边距、图标视觉尺寸与图文间距;需要时再增加紧凑或触控尺寸 |
|
|
12
|
-
| 形状与表面 | 圆角、边界颜色/粗细、填充与阴影;输入、普通操作和浮层的分工 |
|
|
13
|
-
| 操作层级 | 当前需要的主、次、文字或危险变体;不为每个按钮指定独立颜色 |
|
|
14
|
-
| 排版 | 引用已选定的控件文本角色;图标、文字、输入值与附件的对齐方式 |
|
|
15
|
-
| 状态 | 基础可用、hover、focus-visible、disabled,以及实际需要的 loading、error、selected 等状态 |
|
|
16
|
-
| 组合 | 工具栏共同高度与换行,字段 label/input/help/error 的关系,图标按钮名称与命中区 |
|
|
17
|
-
|
|
18
|
-
值应进入共享定义,页面调用时选择语义变体。若需求变化使原规范不适用,再扩展有用途的变体,不重新随机选一套。选择一套规范不等于禁止后续根据内容修正;修正规范时同步影响范围内的调用。
|
|
19
|
-
|
|
20
|
-
## 好看的组件如何做
|
|
21
|
-
|
|
22
|
-
| 质量 | 设计方法 |
|
|
23
|
-
| --- | --- |
|
|
24
|
-
| 比例协调 | 用文字实际行高加上下内边距决定高度;图标视觉重量与文字匹配。不要把小字塞进很厚的大按钮,或把多行输入压成单行高度 |
|
|
25
|
-
| 主次清楚 | 一个操作组突出当前主动作;次动作降低填充或描边重量,低频动作可用文字。危险样式仅表示真实危险,不用来随机点缀 |
|
|
26
|
-
| 边界干净 | 普通按钮和输入默认无投影;输入使用纯净表面及一层清楚边界。背景、边框、内/外阴影、常驻 ring 不多重叠加;浮层可用必要投影 |
|
|
27
|
-
| 细节稳定 | 同密度控件的高度、圆角、字形和图标间距一致;前后缀有固定布局槽位。长按钮文字保持完整,必要时改组布局而非挤成碎行 |
|
|
28
|
-
| 组合有节奏 | 关联搜索与筛选成组,主动作有清楚归属;组内间距小于组间距。输入区、标签、帮助和错误遵循同一垂直节奏 |
|
|
29
|
-
| 状态不跳动 | hover/focus/selected/loading 改变颜色或独立轮廓,通常不改变边框占位、字重或宽高;加载内容替换不让相邻控件左右跳动 |
|
|
30
|
-
| 品牌适度 | 通过动作色、形状与字体配合体现产品性格;普通输入、辅助动作和业务状态保持容易识别 |
|
|
31
|
-
|
|
32
|
-
输入边界、次级按钮描边、焦点轮廓和浮层阴影按各自职责保留,不能把它们的边界规则推广到普通内容卡。用户或正式品牌明确的外观优先,在其体系内协调比例与状态。
|
|
33
|
-
|
|
34
|
-
## 采用配方并保留可靠行为
|
|
35
|
-
|
|
36
|
-
需要新定规格时,[配方文件](control-recipes.md)给出 A/B/C 的高度、字体、内边距、圆角、输入表面和操作变体。其选择优先级与数字规格以该文件为准,不在其他页面重复定义一套近似值。按[组件索引](components/index.md)查看本次用到的类型,复杂控件连同展开面板一起落实;首次实现或替换时可适配 `.tsx` 与 `.css` 代码参考。轻量工程可映射为 CSS 变量与共享类,已有组件库则使用其主题/变体接口。代码参考不决定业务数据和业务限制。
|
|
37
|
-
|
|
38
|
-
文字、图标与边界占位应成组协调;长内容可增加高度,不能把多行字段压成单行或裁掉按钮文字。保留组件库的语义、键盘、定位与焦点行为,不为统一外观重写成仅支持鼠标的 div。选择器、日期、菜单、Dialog 与 Drawer 的面板也在当前规范范围内。内联开关、复选框不强行拉成工具栏按钮尺寸。
|
|
39
|
-
|
|
40
|
-
不必为无异步行为的按钮增加假 loading,不为无输入校验的展示卡补 error 变体。业务中的控件连接真实或主流程明确的模拟状态;预览的演示等待、示例范围和单纯改变外观的按钮不能直接充当业务实现。
|
|
41
|
-
|
|
42
|
-
## 一个控件,一层聚焦强调
|
|
43
|
-
|
|
44
|
-
区分 focus、选中值和展开状态。字段已有值不等于持续聚焦;选项被选中可用填充、勾选或文字重量表达,不自动给整个字段再加一圈品牌色框。
|
|
45
|
-
|
|
46
|
-
- 为输入框、Select 触发器及组合输入确定唯一的焦点绘制层。品牌色 border、outline、box-shadow ring 选择一种清楚表达,不同时出现内外两个强调框;不要只把其中一圈变浅。常态边框可在聚焦时被同位置的轮廓替代,几何占位保持稳定。
|
|
47
|
-
- 带图标、前后缀或步进按钮的输入,由一个共享外壳承担边界;不能外壳 `focus-within` 已画框,内部 input 又画一圈。内部可独立操作的按钮仍需清楚的键盘位置反馈,例如局部填充;一个外框不能掩盖当前焦点究竟在输入还是增减按钮。
|
|
48
|
-
- 展开后焦点进入弹层时,触发器可保留一层展开强调;当前选项仍有自己的焦点反馈。这是两个不同交互对象,不将其误判为同一字段的双框。错误与聚焦同时出现时也只使用一个边界层,错误文字和语义保留。
|
|
49
|
-
- 清理库默认 focus ring、全局 outline 和调用方类名的叠加来源。不得使用全局 `outline: none`;只在已有等效焦点表达的具体控件内部去掉重复层,保留键盘可达与焦点可辨。
|
|
50
|
-
|
|
51
|
-
常规输入可从 1px 边框变色开始表达聚焦,不默认加粗为高饱和外圈;焦点色可用与品牌协调、较克制但对当前表面仍清楚的颜色。优先保持原边界位置与几何。实际键盘位置必须可辨,不能只降低透明度把焦点隐藏;特殊背景或既有可访问性要求需要更强表达时保留必要强度。
|
|
52
|
-
|
|
53
|
-
搜索型下拉区分鼠标展开和键盘展开:用户点击选择器时,不默认把焦点自动送进内部搜索框,让两个区域同时出现强调。用户点击搜索区后再输入;键盘展开时可直接进入搜索,鼠标展开后也能通过 Tab/方向键进入。搜索优先的业务明确需要自动输入时可保留自动聚焦,但仍控制视觉重量。面板内搜索采用融入浮层的无框输入,不因获得焦点增加底线、整行填充或矩形框。指针点击输入时用插入光标表达编辑位置;键盘导航进入时保留局部、可辨的焦点标记,例如搜索图标旁的短竖线,不能靠全局删除 focus-visible 隐藏键盘位置。该处理限于面板内搜索,不推广为所有表单输入取消边界。关闭后合理恢复焦点。
|
|
54
|
-
|
|
55
|
-
## 控件规范包含展开后的面板
|
|
56
|
-
|
|
57
|
-
Select、多选、菜单、日期与时间选择的触发器和浮层是一组组件。使用工程已有可靠组件的主题、slot、class 或 Portal 机制,统一面板表面、圆角、内边距、选项高度、排版及状态,继承当前应用或专题主题。不能只美化入口,展开后仍退回未经设计的系统列表或系统日期面板。
|
|
58
|
-
|
|
59
|
-
| 控件 | 本次用到时需落实的部分 |
|
|
60
|
-
| --- | --- |
|
|
61
|
-
| 单选 / 搜索选择 | 触发器、面板、当前值、hover 与键盘焦点;需要搜索时包含搜索框、无匹配反馈及列表滚动 |
|
|
62
|
-
| 多选 | 已选值的紧凑表达、移除入口、多选面板与清空;长标签不把触发器无限撑宽 |
|
|
63
|
-
| 日期 / 时间 | 月份或时段切换、当前与选中值、必要范围/禁用日、清空;规则来自现有业务契约,不自行增加日期限制 |
|
|
64
|
-
| 菜单 | 操作层级、危险操作、可用状态与焦点;宽高受当前视口约束,靠近边缘时正确定位 |
|
|
65
|
-
| Dialog / Drawer | 内容与动作布局、滚动、关闭及焦点进入/恢复;只有真正模态的交互才约束背景与焦点 |
|
|
66
|
-
|
|
67
|
-
桌面需要完整主题外观时,原生 `select`、`input[type=date/time]` 的系统弹出界面通常无法靠触发器 CSS 一并定制,应选择工程中可靠的可主题化组件。**本地、mock、临时演示和不发布仅影响数据接线或交付方式,不降低控件的视觉与交互要求;省开发时间也不能成为换回原生弹层的理由。**若用户或平台明确要求原生交互,按该要求处理;不为了外观新增整套依赖,也不能用失去语义和键盘能力的普通 div 假装选择器。现有能力确有缺口时,在本次范围内补最小实现并处理打开/选择/关闭、键盘移动、Escape 与焦点恢复。
|
|
68
|
-
|
|
69
|
-
实现前先确认选用路径:①工程已有可主题化组件,沿用行为并覆盖触发器与面板;②没有合适组件,按本目录对应代码适配最小实现;③只有明确的用户要求或平台硬约束,才保留系统弹层。选③时必须能指出具体要求或约束,不能以“演示够用”“原生省事”“系统自动适配暗色”代替。原生输入可用于底层值处理,但不能让未设计的系统弹层成为本次页面可见的选择界面。
|
|
70
|
-
|
|
71
|
-
浮层可以使用必要的轻投影,不重复加厚描边与多层阴影。长列表局部滚动、面板不越出视口;选中和焦点不能只靠模糊色差。日期输入同时保留清楚的字段名、值格式和任务所需的输入效率。
|
|
72
|
-
|
|
73
|
-
## 多行输入与字段反馈
|
|
74
|
-
|
|
75
|
-
所有应用多行输入的默认外观都隐藏右下角原生拖拽斜线:在共享 Textarea 样式明确设置 `resize: none`,覆盖组件或全局样式中的 `resize: vertical/both`。这项只作用于多行输入,不是全局去掉用户的缩放能力。
|
|
76
|
-
|
|
77
|
-
取消手动拖拽后必须保留长内容的编辑能力:随内容增高到合理上限,再在内部滚动;需要更大编辑区时可使用已有展开能力。初值、粘贴、快捷预填、重置和窄屏换行后都应更新高度;不能固定矮框并把超出内容裁掉。
|
|
78
|
-
|
|
79
|
-
普通字段按现有校验规则从错误恢复后,移除错误边界、错误描述关联和该条错误文字,返回安静的正常状态。已有值但仍不符合规则的字段继续显示相关错误,不能因“非空”直接清除。正常已填字段不补“已填写”“填写完成”“输入有效”或常驻绿色勾选,也不为被删除的提示保留空白帮助行。原本对正确输入仍必要的帮助文字继续保留。
|
|
80
|
-
|
|
81
|
-
字段已填、校验通过与数据保存是不同事实。必要的异步校验结果、提交中、真正保存结果或有意义的业务状态按实际行为表达,不能因为删去冗余字段提示而掩盖失败或未保存风险。无错误字段只保留标签、输入与必要帮助;同排对齐用控件/标签布局解决,不补成功文案占位。
|
|
82
|
-
|
|
83
|
-
组件类型按本次任务选取:单选、复选、开关、步进、滑块、标签、分页与反馈等沿用共同的文字、密度及状态规则;组件丰富不意味着页面必须全部摆上。各类组件的共同规格与行为边界见[组件类型表](control-recipes.md#其他组件按需纳入同一规范);预览样本的种类与生产页面实际需要分开判断。
|
|
84
|
-
|
|
85
|
-
## 实现与检查证据
|
|
86
|
-
|
|
87
|
-
先实现共享 token/样式/组件变体,再用于本次页面。轻量项目可以用共享 CSS 类或已有配置,不强制封装新 React 组件。复用还应覆盖工具栏、字段分组和同级卡片的布局关系,不能只统一单个 Button 后让每页任意组合。
|
|
88
|
-
|
|
89
|
-
检查实际调用与最终样式来源:组件默认类、变体、调用方 class、全局样式及 UA 样式是否仍叠加阴影、双重焦点或覆盖尺寸;同时核对弹层主题、Textarea 的 resize 与长内容策略、有效字段是否仍渲染成功补句。Card/Panel 及手写容器也检查 border、ring、shadow 和伪元素;修改背景 token 不会自动移除默认边框。使用库支持的机制处理默认外观,不用全局 `*` 重置或粗暴删除 outline,也不靠逐处追加优先级掩盖冲突。若既有 CSS 已对焦点使用 box-shadow,清理普通阴影时保留或替换为同等清楚的焦点表达。
|
|
90
|
-
|
|
91
|
-
完成依据是页面调用共享定义、变体覆盖当前所需状态、布局与文本规则一致。未使用的 token、口头声称“统一”或只做孤立组件示例都不够。对实际观感的判断遵循[review](review.md),不触发浏览器巡检;也不因为新规范去重构本次任务之外的页面。
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# 页面构图
|
|
2
|
-
|
|
3
|
-
新建、整体改版或修改页面结构时读取。沿用[通用规范](foundations.md)的任务、品牌、数据和范围约束;局部改字不触发重新构图。以下判断直接进入布局实现,不要求额外方案文件或用户确认。
|
|
4
|
-
|
|
5
|
-
## 先分配注意力和空间
|
|
6
|
-
|
|
7
|
-
根据主流程已有目标与内容,确定读者先看到什么、接着比较什么、在哪里操作。以此选定:主工作区、辅助区、共同对齐线、密度,以及宽屏扩展和窄屏重排方式。一个页面可有多个任务区,但其面积、位置与对比应体现优先级,避免标题、四个大数字、彩色卡片和多个主按钮同时争抢注意力。
|
|
8
|
-
|
|
9
|
-
| 已知主要任务 | 优先分配空间 | 辅助信息的处理 |
|
|
10
|
-
| --- | --- | --- |
|
|
11
|
-
| 排班与计划 | 日期/人员关系、班次及异常 | 摘要紧凑,范围控制贴近日历;不让页头与指标占去主要工作区 |
|
|
12
|
-
| 经营扫描与决策 | 当前重点、可比较证据、风险与下一步 | 只放支持当前决策的指标;相关图表共用对齐与比较尺度 |
|
|
13
|
-
| 创作与素材处理 | 必要输入、提交、可辨认的产物 | 快捷任务从属于输入;预览高度服务辨认,不以大空色块填版 |
|
|
14
|
-
| 个人任务与记录 | 当前任务、快速输入、待处理清单 | 统计降为辅助层级;桌面形成有效分区,专注文字局部限宽 |
|
|
15
|
-
| 报告与展示 | 结论/主题与相邻证据或真实素材 | 阅读顺序、段落宽度和章节节奏优先,应用控件保持克制 |
|
|
16
|
-
|
|
17
|
-
此表是空间关系,不是必须新增的模块。任务模糊时用已知内容组织清楚的阅读和操作顺序,不新增提问、功能或虚构“核心指标”。正向结构参考见[设计示例](examples.md),只读对应任务。
|
|
18
|
-
|
|
19
|
-
## 让关系形成美感
|
|
20
|
-
|
|
21
|
-
| 设计关系 | 实现方法 | 常见偏差及修正 |
|
|
22
|
-
| --- | --- | --- |
|
|
23
|
-
| 主次 | 用位置、面积、文字层级和有限强调色突出当前重点;次要区降低视觉重量 | 每块都加彩底、大字和主按钮时,先还原任务优先级,再收敛强调 |
|
|
24
|
-
| 比例 | 按内容长度、数据密度与可读最小宽度分配列;主次区不必等分 | 为对称把输入挤窄、图表拉长时,调整轨道比例或堆叠,而非缩字 |
|
|
25
|
-
| 对齐 | 页头、工具栏、主内容共享起止线;同级卡片共享列、行和内部槽位 | 外框等高但数字错位时,修正标题/数值的共享布局,不逐卡加 margin |
|
|
26
|
-
| 留白 | 小间距连接标签与值,中间距分隔组,大间距划分任务或章节 | 全部间距相同会失去分组;空白过大时收紧冗余容器,不补无意义文案 |
|
|
27
|
-
| 表面 | 布局分组无装饰,独立内容表面按下方规则选择;内部优先用留白与对齐分组 | 同组外壳重复时选定承担表面的层级,其余层退为普通布局,不逐层加框 |
|
|
28
|
-
| 色彩 | 先确定中性基底、文字和动作/状态色的分工,再分配强调面积 | 大片饱和底色遮蔽内容时,减少色块面积;异常色不与普通分类混用 |
|
|
29
|
-
| 内容质感 | 真实业务字段、清楚的图形与可辨素材承担视觉内容 | 缺图时缩减预览或使用信息行;不以机器人、圆斑和口号冒充内容 |
|
|
30
|
-
|
|
31
|
-
有意的非对称、留白或品牌装饰可以成立,依据是当前内容与用户要求。不能为了“整齐”把不同任务强行变成相同卡片,也不能为了差异随机换结构。
|
|
32
|
-
|
|
33
|
-
## 普通业务页直接排版
|
|
34
|
-
|
|
35
|
-
列表、台账、详情、编辑、设置、审批、排班和创作输入等普通业务页,默认直接在页面上排列标题、工具栏、内容与动作,不先垫一张包住全页内容的大卡片。先用 Grid/Flex、共同对齐线、标题、间距和必要的局部分隔组织信息;DOM 中的 main、section、滚动容器不因此获得背景、圆角、边框或阴影。
|
|
36
|
-
|
|
37
|
-
应用根节点可以绘制页面背景;这不意味着再加一块圆角白底包住页面主体。即使去掉边框与阴影,无用途的额外底板仍应移除。将 Card 改名为 Panel/div、改浅灰边或只降低阴影,都不能代替这一判断。
|
|
38
|
-
|
|
39
|
-
| 页面任务 | 默认组织方式 |
|
|
40
|
-
| --- | --- |
|
|
41
|
-
| 列表与台账 | 页头 → 平面筛选/搜索工具栏 → 表格或列表 → 分页;不把整组装进 Card |
|
|
42
|
-
| 编辑与设置 | 标题 → 直接排列的字段组 → 保存等动作;用组标题和间距分组,不逐组加底板 |
|
|
43
|
-
| 详情与审批 | 对象标题与状态 → 属性、明细、过程记录 → 相应动作;局部区域按内容分列 |
|
|
44
|
-
| 创作与自然语言输入 | 任务标题或必要标签 → 输入与提交 → 辅助示例/结果;输入边界不再套一层表单卡 |
|
|
45
|
-
| 排班与计划 | 紧凑摘要及范围控制 → 日历/人员网格 → 相关操作;网格直接成为工作区 |
|
|
46
|
-
|
|
47
|
-
只有独立对象需要比较或选择、素材需要预览、看板中的独立图表/指标需要内容面等具体用途,才使用局部卡片。“属于同一组”“有一个标题”不足以成为加卡的理由。少量统计不自动把业务页变成看板,也不要求补齐四张 KPI 卡。看板可有独立内容面,外层仍是无装饰布局,并非每个区块都必须卡片化。
|
|
48
|
-
|
|
49
|
-
“卡片默认无边框”是决定使用卡片之后的约束,不能跳过“是否需要卡片”的判断。保留输入边界、日历/表格网格、选择与焦点/错误状态,以及浮层所需的层级分离。用户明确外观和正式品牌系统优先,只处理本次授权范围,不进行全局无差别去背景、去边框。
|
|
50
|
-
|
|
51
|
-
## 容器与可见边界
|
|
52
|
-
|
|
53
|
-
在确定页面分区时,同步选定需要可见表面的层级。DOM、Grid、section 或业务分组可以有多层,视觉外壳不随结构层级逐层增加。以下默认用于新建及本次授权的视觉整理;保留用户明确的外观和正式品牌系统,不据此改造无关页面。
|
|
54
|
-
|
|
55
|
-
| 容器职责 | 默认表达 | 保留边界的依据 |
|
|
56
|
-
| --- | --- | --- |
|
|
57
|
-
| 页面主体、网格、区块组、工具栏 | 透明布局,标题、对齐、留白 | 不因“包含子项”就增加圆角底板、描边或阴影 |
|
|
58
|
-
| 独立内容区、统计卡、对象卡 | 确需表面时用无描边、无投影的内容面;与背景保持适当明度差 | 明确品牌要求或当前交互确需边界时使用有用途的变体;组件库默认不算依据 |
|
|
59
|
-
| 已有内容面内的次级分组 | 平面行、标题与间距;密集内容必要时用局部分隔线 | 分隔线对应真实行列/信息组,不绕每个分组画一圈 |
|
|
60
|
-
| 输入与选择控件 | 沿用控件规范的清楚输入边界 | 可输入性、选中、焦点和错误需要辨识,不能按装饰卡片处理 |
|
|
61
|
-
| 表格、日历、图表坐标 | 保留读取关系所需的网格或参考线 | 线条帮助比较与定位;所属区域不必再叠多重装饰框 |
|
|
62
|
-
| Dialog、菜单等浮层 | 延续已有主题,用必要的遮罩或阴影分离层级 | 浮层是交互层,不套用普通卡片“无投影”默认 |
|
|
63
|
-
|
|
64
|
-
普通内容卡的常态边框与阴影应显式收敛到共享样式,例如对应范围的 `border: 0` 与 `box-shadow: none`;不要全局重置。不要改为更浅灰边、彩色细边或另一层阴影后,就认为已经解决重复外壳。父子层背景相同时,优先合并为平面内容,用间距或局部分隔建立关系;不为让白色卡片“显形”而自动画框。
|
|
65
|
-
|
|
66
|
-
嵌套对象确有独立选择、拖动或业务边界时,可以保留所需表达,但不让父子每层都同时使用圆角底板、描边与阴影。选中或键盘焦点的轮廓属于状态,不能为了消除边框而删除。可点击内容即使无常驻外框,也要有可发现的动作表达、悬停/焦点反馈和正确语义;不要把按钮改成不可用的普通文字。
|
|
67
|
-
|
|
68
|
-
三种常见组织方式:
|
|
69
|
-
|
|
70
|
-
```text
|
|
71
|
-
普通业务页 → 页头 / 平面工具栏 / 内容 / 动作(无需额外内容面)
|
|
72
|
-
确有独立对象 → 区块标题 + 无装饰布局 → 对象卡并列
|
|
73
|
-
一个必要的内容表面 → 标题 / 平面内容分组 / 操作
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
避免默认生成“页面圆角外壳 → 区块圆角卡 → 每行又一张圆角卡”。复核实际样式的来源,不仅搜索组件名;普通 div 也可能通过背景、圆角、border、ring、shadow 或伪元素形成多层外壳。静态检查按代码和已有证据进行,不启动浏览器。
|
|
77
|
-
|
|
78
|
-
## 在写布局时处理尺寸
|
|
79
|
-
|
|
80
|
-
工作区从宿主可用宽度出发,先扣除导航和合理边距,再分配轨道。表格、日历和看板应获得可用比较空间;长文、表单和专注输入可局部限宽。沿宿主、根节点、主区检查遗留的固定宽度与嵌套 max-width,避免只铺满背景。
|
|
81
|
-
|
|
82
|
-
断点由内容何时开始拥挤决定,并沿用工程断点体系。明确窄屏的信息顺序、列数、导航和动作位置;宽屏增加有用的列宽、比较空间或合适列数,不按屏幕等比放大字体与控件。不要求填满所有留白或把所有内容压进首屏。
|
|
83
|
-
|
|
84
|
-
同级同结构卡片用共享布局保持同排等高、内边距与内容槽位对齐。优先使用共享网格行或目标环境支持的 subgrid;若采用其他实现,应确保最长标题共同决定该排标题区,数值和动作仍对齐。单纯 `stretch` 只能对齐外框,不能保证内部对齐。可选说明缺失时不生成空 DOM 或补句;共享排版产生的必要槽位空间不等于为副标题强留位置。窄屏堆叠后释放无必要的等高约束。
|
|
85
|
-
|
|
86
|
-
用本次真实的长标题、数值、错误消息和空态推敲尺寸规则;不增造业务数据。图片比例与裁切保留辨认所需内容,卡片预览不随宽屏列宽无限长高。按钮文字不碎裂,输入区能编辑完整内容,表格必要的横向滚动限于所属区域。
|
|
87
|
-
|
|
88
|
-
## 完成依据
|
|
89
|
-
|
|
90
|
-
布局代码应能对应主次分区、共享对齐、内容宽度与重排决策;普通业务页没有无用途的整页底板,每个局部卡片有具体用途。整体观感仍需已有或明确安排的视觉证据判断:规则一致只能证明实现意图,不能证明比例与留白已经漂亮。检查方式遵循[设计自检与按需评审](review.md),本指南不增加浏览器或截图步骤。
|