@omit-design/preset-mobile 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/PATTERNS.md +134 -0
  2. package/README.md +29 -0
  3. package/catalog.tsx +365 -0
  4. package/components/OmAppBar.tsx +63 -0
  5. package/components/OmButton.tsx +41 -0
  6. package/components/OmCard.tsx +28 -0
  7. package/components/OmCouponCard.tsx +71 -0
  8. package/components/OmDialog.tsx +96 -0
  9. package/components/OmEmptyState.tsx +32 -0
  10. package/components/OmHeader.tsx +21 -0
  11. package/components/OmInput.tsx +38 -0
  12. package/components/OmListRow.tsx +30 -0
  13. package/components/OmMenuCard.tsx +47 -0
  14. package/components/OmNumpad.tsx +82 -0
  15. package/components/OmOrderFooter.tsx +78 -0
  16. package/components/OmPage.tsx +30 -0
  17. package/components/OmProductCard.tsx +75 -0
  18. package/components/OmSearchBar.tsx +51 -0
  19. package/components/OmSelect.tsx +47 -0
  20. package/components/OmSettingRow.tsx +95 -0
  21. package/components/OmSheet.tsx +49 -0
  22. package/components/OmStatCard.tsx +30 -0
  23. package/components/OmTabBar.tsx +26 -0
  24. package/components/OmTag.tsx +28 -0
  25. package/components/index.ts +30 -0
  26. package/components/inspect-attrs.ts +34 -0
  27. package/components/om-app-bar.css +83 -0
  28. package/components/om-coupon-card.css +107 -0
  29. package/components/om-dialog.css +81 -0
  30. package/components/om-empty-state.css +55 -0
  31. package/components/om-input.css +43 -0
  32. package/components/om-menu-card.css +68 -0
  33. package/components/om-numpad.css +49 -0
  34. package/components/om-order-footer.css +121 -0
  35. package/components/om-page.css +43 -0
  36. package/components/om-product-card.css +124 -0
  37. package/components/om-search-bar.css +39 -0
  38. package/components/om-select.css +28 -0
  39. package/components/om-setting-row.css +82 -0
  40. package/components/om-sheet.css +73 -0
  41. package/components/om-stat-card.css +40 -0
  42. package/components/om-tag.css +51 -0
  43. package/index.ts +14 -0
  44. package/package.json +48 -0
  45. package/preset.manifest.ts +62 -0
  46. package/templates/dashboard.tmpl.tsx +90 -0
  47. package/templates/detail-view.tmpl.tsx +60 -0
  48. package/templates/dialog-view.tmpl.tsx +34 -0
  49. package/templates/form-view.tmpl.tsx +52 -0
  50. package/templates/list-view.tmpl.tsx +58 -0
  51. package/templates/sheet-action.tmpl.tsx +52 -0
  52. package/templates/tab-view.tmpl.tsx +51 -0
  53. package/templates/welcome-view.tmpl.tsx +38 -0
  54. package/theme/baseline.ts +32 -0
  55. package/theme/presets/light.ts +8 -0
  56. package/theme/variables.css +183 -0
  57. package/tokens/index.ts +51 -0
@@ -0,0 +1,47 @@
1
+ import { IonItem, IonLabel, IonSelect, IonSelectOption } from "@ionic/react";
2
+ import { inspectAttrs } from "./inspect-attrs";
3
+ import "./om-select.css";
4
+
5
+ export interface OmSelectOption {
6
+ label: string;
7
+ value: string;
8
+ }
9
+
10
+ interface OmSelectProps {
11
+ label: string;
12
+ value?: string;
13
+ options: OmSelectOption[];
14
+ placeholder?: string;
15
+ onChange?: (value: string) => void;
16
+ /** Ionic 弹出样式;默认 action-sheet,移动端更贴 iOS */
17
+ interfaceType?: "alert" | "action-sheet" | "popover";
18
+ }
19
+
20
+ export function OmSelect({
21
+ label,
22
+ value,
23
+ options,
24
+ placeholder,
25
+ onChange,
26
+ interfaceType = "action-sheet",
27
+ }: OmSelectProps) {
28
+ return (
29
+ <div className="om-select">
30
+ <IonItem {...inspectAttrs("OmSelect", { spacing: "md" })}>
31
+ <IonLabel position="stacked">{label}</IonLabel>
32
+ <IonSelect
33
+ interface={interfaceType}
34
+ value={value}
35
+ placeholder={placeholder}
36
+ onIonChange={(e) => onChange?.(e.detail.value)}
37
+ >
38
+ {options.map((o) => (
39
+ <IonSelectOption key={o.value} value={o.value}>
40
+ {o.label}
41
+ </IonSelectOption>
42
+ ))}
43
+ </IonSelect>
44
+ </IonItem>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,95 @@
1
+ import type { ReactNode } from "react";
2
+ import { IonIcon, IonToggle } from "@ionic/react";
3
+ import { chevronForward } from "ionicons/icons";
4
+ import { inspectAttrs } from "./inspect-attrs";
5
+ import "./om-setting-row.css";
6
+
7
+ type OmSettingKind =
8
+ | {
9
+ kind: "toggle";
10
+ enabled: boolean;
11
+ onToggle?: (next: boolean) => void;
12
+ }
13
+ | {
14
+ kind: "navigate";
15
+ onClick?: () => void;
16
+ href?: string;
17
+ }
18
+ | {
19
+ kind: "value";
20
+ value: ReactNode;
21
+ onClick?: () => void;
22
+ };
23
+
24
+ interface OmSettingRowBaseProps {
25
+ label: string;
26
+ description?: string;
27
+ /** 左侧图标(可选) */
28
+ icon?: string;
29
+ }
30
+
31
+ type OmSettingRowProps = OmSettingRowBaseProps & OmSettingKind;
32
+
33
+ /**
34
+ * 设置项单行 —— 三种形态:toggle / navigate / value。
35
+ * 用于 workstation / settings 设置页,列表语义统一。
36
+ */
37
+ export function OmSettingRow(props: OmSettingRowProps) {
38
+ const { label, description, icon } = props;
39
+ const interactive = props.kind !== "toggle";
40
+ const onClick =
41
+ props.kind === "navigate" || props.kind === "value" ? props.onClick : undefined;
42
+
43
+ const inspect = inspectAttrs("OmSettingRow", { bg: "background", spacing: "lg" });
44
+ const className = `om-setting-row${interactive ? " pos-setting-row--interactive" : ""}`;
45
+
46
+ const inner = (
47
+ <>
48
+ {icon && (
49
+ <div className="om-setting-row__icon" aria-hidden>
50
+ <IonIcon icon={icon} />
51
+ </div>
52
+ )}
53
+ <div className="om-setting-row__body">
54
+ <span className="om-setting-row__label">{label}</span>
55
+ {description && <span className="om-setting-row__desc">{description}</span>}
56
+ </div>
57
+ <div className="om-setting-row__right">
58
+ {props.kind === "toggle" && (
59
+ <IonToggle
60
+ checked={props.enabled}
61
+ onIonChange={(e) => props.onToggle?.(e.detail.checked)}
62
+ aria-label={label}
63
+ />
64
+ )}
65
+ {props.kind === "value" && (
66
+ <>
67
+ <span className="om-setting-row__value">{props.value}</span>
68
+ <IonIcon icon={chevronForward} className="om-setting-row__chevron" aria-hidden />
69
+ </>
70
+ )}
71
+ {props.kind === "navigate" && (
72
+ <IonIcon icon={chevronForward} className="om-setting-row__chevron" aria-hidden />
73
+ )}
74
+ </div>
75
+ </>
76
+ );
77
+
78
+ if (props.kind === "navigate" && props.href) {
79
+ return (
80
+ <a className={className} href={props.href} {...inspect}>
81
+ {inner}
82
+ </a>
83
+ );
84
+ }
85
+
86
+ return interactive ? (
87
+ <button type="button" className={className} onClick={onClick} {...inspect}>
88
+ {inner}
89
+ </button>
90
+ ) : (
91
+ <div className={className} {...inspect}>
92
+ {inner}
93
+ </div>
94
+ );
95
+ }
@@ -0,0 +1,49 @@
1
+ import type { ReactNode } from "react";
2
+ import { IonIcon } from "@ionic/react";
3
+ import { closeOutline } from "ionicons/icons";
4
+ import { useNavigate } from "react-router-dom";
5
+ import { inspectAttrs } from "./inspect-attrs";
6
+ import "./om-sheet.css";
7
+
8
+ interface OmSheetProps {
9
+ /** 顶部标题(可选;传 null 则不展示 title bar) */
10
+ title?: ReactNode;
11
+ children: ReactNode;
12
+ /** 关闭:点击 scrim 或右上角 × —— 默认 navigate(-1) */
13
+ onDismiss?: () => void;
14
+ /** 关闭后的跳转路由(和 onDismiss 二选一) */
15
+ dismissHref?: string;
16
+ /** sheet 高度策略:auto(内容自适应)/ tall(70% 视口) */
17
+ size?: "auto" | "tall";
18
+ }
19
+
20
+ /**
21
+ * 底部抽屉 sheet —— 从下往上弹出的内容面板。
22
+ * 用于:优惠详情、快捷操作菜单、行内动作列表。
23
+ * 作为独立"弹窗稿"时,放在 OmPage 里叠一层(参考 dialog-view 的做法)。
24
+ */
25
+ export function OmSheet({ title, children, onDismiss, dismissHref, size = "auto" }: OmSheetProps) {
26
+ const navigate = useNavigate();
27
+ const dismiss = () => {
28
+ if (onDismiss) return onDismiss();
29
+ if (dismissHref) return navigate(dismissHref);
30
+ navigate(-1);
31
+ };
32
+
33
+ return (
34
+ <div className="om-sheet" {...inspectAttrs("OmSheet", { bg: "background", radius: "xl" })}>
35
+ <div className="om-sheet__scrim" aria-hidden onClick={dismiss} />
36
+ <div className={`pos-sheet__panel pos-sheet__panel--${size}`} role="dialog">
37
+ {title && (
38
+ <div className="om-sheet__head">
39
+ <span className="om-sheet__title">{title}</span>
40
+ <button className="om-sheet__close" type="button" onClick={dismiss} aria-label="关闭">
41
+ <IonIcon icon={closeOutline} />
42
+ </button>
43
+ </div>
44
+ )}
45
+ <div className="om-sheet__body">{children}</div>
46
+ </div>
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,30 @@
1
+ import type { ReactNode } from "react";
2
+ import { inspectAttrs } from "./inspect-attrs";
3
+ import "./om-stat-card.css";
4
+
5
+ interface OmStatCardProps {
6
+ label: string;
7
+ /** 大号数字 —— 可传格式化好的字符串("¥12,480.50") */
8
+ value: string;
9
+ /** 右上角辅助 slot(如「月份」小字) */
10
+ meta?: ReactNode;
11
+ /** 副标题(如单位、对比) */
12
+ caption?: string;
13
+ }
14
+
15
+ /**
16
+ * 统计数字卡 —— 用于工作台营收 / 客单等核心指标展示。
17
+ * 单一职责:标签 + 大号数字 + 可选副标题 / meta。
18
+ */
19
+ export function OmStatCard({ label, value, meta, caption }: OmStatCardProps) {
20
+ return (
21
+ <div className="om-stat" {...inspectAttrs("OmStatCard", { bg: "background", radius: "lg", shadow: "sm" })}>
22
+ <div className="om-stat__head">
23
+ <span className="om-stat__label">{label}</span>
24
+ {meta && <span className="om-stat__meta">{meta}</span>}
25
+ </div>
26
+ <div className="om-stat__value">{value}</div>
27
+ {caption && <div className="om-stat__caption">{caption}</div>}
28
+ </div>
29
+ );
30
+ }
@@ -0,0 +1,26 @@
1
+ import { IonIcon, IonLabel, IonTabBar, IonTabButton } from "@ionic/react";
2
+ import { inspectAttrs } from "./inspect-attrs";
3
+
4
+ export interface OmTabItem {
5
+ tab: string;
6
+ href: string;
7
+ label: string;
8
+ icon: string;
9
+ }
10
+
11
+ interface OmTabBarProps {
12
+ items: OmTabItem[];
13
+ }
14
+
15
+ export function OmTabBar({ items }: OmTabBarProps) {
16
+ return (
17
+ <IonTabBar slot="bottom" {...inspectAttrs("OmTabBar", { bg: "background" })}>
18
+ {items.map((item) => (
19
+ <IonTabButton key={item.tab} tab={item.tab} href={item.href}>
20
+ <IonIcon icon={item.icon} aria-hidden="true" />
21
+ <IonLabel>{item.label}</IonLabel>
22
+ </IonTabButton>
23
+ ))}
24
+ </IonTabBar>
25
+ );
26
+ }
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from "react";
2
+ import { inspectAttrs } from "./inspect-attrs";
3
+ import type { ColorTokenName } from "../tokens";
4
+ import "./om-tag.css";
5
+
6
+ interface OmTagProps {
7
+ children: ReactNode;
8
+ color?: ColorTokenName;
9
+ /** 视觉:solid 填充色、soft 浅色底、outline 描边 */
10
+ variant?: "solid" | "soft" | "outline";
11
+ size?: "sm" | "md";
12
+ }
13
+
14
+ /**
15
+ * 小尺寸的 chip / badge / 标签。
16
+ * 用于:门槛标签("无门槛")、VIP 级别、状态徽章。
17
+ */
18
+ export function OmTag({ children, color = "primary", variant = "soft", size = "sm" }: OmTagProps) {
19
+ return (
20
+ <span
21
+ className={`pos-tag pos-tag--${variant} pos-tag--${size}`}
22
+ data-color={color}
23
+ {...inspectAttrs("OmTag", { color, radius: "sm", fontSize: "xs" })}
24
+ >
25
+ {children}
26
+ </span>
27
+ );
28
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * preset-mobile 组件白名单 — 业务页面(design/**)唯一允许 import 的来源。
3
+ *
4
+ * **不要**在业务页面里 `import from '@ionic/react'`,
5
+ * 如果发现某个移动端模式 Om* 没覆盖到,先来这里加封装。
6
+ */
7
+
8
+ export { OmPage } from "./OmPage";
9
+ export { OmHeader } from "./OmHeader";
10
+ export { OmAppBar } from "./OmAppBar";
11
+ export { OmButton } from "./OmButton";
12
+ export { OmCard } from "./OmCard";
13
+ export { OmListRow } from "./OmListRow";
14
+ export { OmInput } from "./OmInput";
15
+ export { OmSelect } from "./OmSelect";
16
+ export type { OmSelectOption } from "./OmSelect";
17
+ export { OmDialog } from "./OmDialog";
18
+ export { OmTabBar } from "./OmTabBar";
19
+ export type { OmTabItem } from "./OmTabBar";
20
+ export { OmNumpad } from "./OmNumpad";
21
+ export { OmSearchBar } from "./OmSearchBar";
22
+ export { OmProductCard } from "./OmProductCard";
23
+ export { OmEmptyState } from "./OmEmptyState";
24
+ export { OmTag } from "./OmTag";
25
+ export { OmOrderFooter } from "./OmOrderFooter";
26
+ export { OmCouponCard } from "./OmCouponCard";
27
+ export { OmStatCard } from "./OmStatCard";
28
+ export { OmMenuCard } from "./OmMenuCard";
29
+ export { OmSettingRow } from "./OmSettingRow";
30
+ export { OmSheet } from "./OmSheet";
@@ -0,0 +1,34 @@
1
+ import type { ColorTokenName, SpacingTokenName, RadiusTokenName, FontSizeTokenName, ShadowTokenName } from "../tokens";
2
+
3
+ export type TokenRefs = {
4
+ color?: ColorTokenName;
5
+ bg?: ColorTokenName;
6
+ spacing?: SpacingTokenName | SpacingTokenName[];
7
+ radius?: RadiusTokenName;
8
+ fontSize?: FontSizeTokenName;
9
+ shadow?: ShadowTokenName;
10
+ };
11
+
12
+ /**
13
+ * Inspect 用:把组件用到的 token 编码到 data-omit-tokens 属性,
14
+ * 运行时由 packages/engine/src/inspect 读取并展示「token 名」而不是字面量值。
15
+ */
16
+ export function inspectAttrs(component: string, tokens?: TokenRefs) {
17
+ const attrs: Record<string, string> = {
18
+ "data-omit-component": component,
19
+ };
20
+ if (tokens) {
21
+ const flat: string[] = [];
22
+ if (tokens.color) flat.push(`color:${tokens.color}`);
23
+ if (tokens.bg) flat.push(`bg:${tokens.bg}`);
24
+ if (tokens.spacing) {
25
+ const arr = Array.isArray(tokens.spacing) ? tokens.spacing : [tokens.spacing];
26
+ flat.push(`spacing:${arr.join(",")}`);
27
+ }
28
+ if (tokens.radius) flat.push(`radius:${tokens.radius}`);
29
+ if (tokens.fontSize) flat.push(`fontSize:${tokens.fontSize}`);
30
+ if (tokens.shadow) flat.push(`shadow:${tokens.shadow}`);
31
+ if (flat.length) attrs["data-omit-tokens"] = flat.join("|");
32
+ }
33
+ return attrs;
34
+ }
@@ -0,0 +1,83 @@
1
+ /* OmAppBar —— POS 应用 header(非表单页的 header)。
2
+ 与 OmHeader(标准标题栏)区分:OmAppBar 是信息密度更高的 app shell 级 header。 */
3
+
4
+ .om-app-bar {
5
+ display: flex;
6
+ align-items: center;
7
+ gap: var(--om-spacing-md);
8
+ padding: var(--om-spacing-sm) var(--om-spacing-lg);
9
+ background: var(--ion-background-color);
10
+ min-height: var(--om-control-height-xl);
11
+ border-bottom: 1px solid var(--ion-color-light);
12
+ }
13
+
14
+ .om-app-bar--store .om-app-bar__info {
15
+ display: flex;
16
+ flex-direction: column;
17
+ gap: var(--om-spacing-xs);
18
+ flex: 1 1 auto;
19
+ min-width: 0;
20
+ }
21
+
22
+ .om-app-bar__info-primary {
23
+ margin: 0;
24
+ font-size: var(--om-font-size-sm);
25
+ color: var(--ion-color-dark);
26
+ font-weight: 500;
27
+ white-space: nowrap;
28
+ overflow: hidden;
29
+ text-overflow: ellipsis;
30
+ }
31
+
32
+ .om-app-bar__info-secondary {
33
+ margin: 0;
34
+ font-size: var(--om-font-size-sm);
35
+ color: var(--ion-color-dark);
36
+ font-weight: 500;
37
+ }
38
+
39
+ .om-app-bar__divider {
40
+ width: 1px;
41
+ height: var(--om-spacing-2xl);
42
+ background: var(--ion-color-light);
43
+ }
44
+
45
+ .om-app-bar__right {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: var(--om-spacing-sm);
49
+ }
50
+
51
+ /* brand 变体(商户中心) */
52
+ .om-app-bar--brand {
53
+ justify-content: space-between;
54
+ }
55
+
56
+ .om-app-bar__brand {
57
+ display: flex;
58
+ align-items: center;
59
+ gap: var(--om-spacing-sm);
60
+ }
61
+
62
+ .om-app-bar__brand-icon {
63
+ font-size: var(--om-icon-size-lg);
64
+ }
65
+
66
+ .om-app-bar__brand-title {
67
+ font-size: var(--om-font-size-md);
68
+ font-weight: 500;
69
+ color: var(--ion-color-dark);
70
+ }
71
+
72
+ .om-app-bar__avatar {
73
+ width: var(--om-spacing-2xl);
74
+ height: var(--om-spacing-2xl);
75
+ border-radius: var(--om-radius-full);
76
+ background: var(--ion-color-light);
77
+ display: flex;
78
+ align-items: center;
79
+ justify-content: center;
80
+ overflow: hidden;
81
+ font-size: var(--om-font-size-sm);
82
+ color: var(--ion-color-medium);
83
+ }
@@ -0,0 +1,107 @@
1
+ /* OmCouponCard —— 优惠券 / 奖励卡(会员详情 & 奖励确认两处复用) */
2
+
3
+ .om-coupon {
4
+ display: flex;
5
+ gap: var(--om-spacing-md);
6
+ padding: var(--om-spacing-md);
7
+ background: var(--ion-background-color);
8
+ border-radius: var(--om-radius-md);
9
+ border: 1px solid transparent;
10
+ align-items: center;
11
+ width: 100%;
12
+ text-align: left;
13
+ appearance: none;
14
+ cursor: pointer;
15
+ font: inherit;
16
+ color: inherit;
17
+ }
18
+
19
+ button.om-coupon {
20
+ cursor: pointer;
21
+ }
22
+
23
+ .om-coupon--selected {
24
+ border-color: var(--ion-color-primary);
25
+ background: var(--om-surface-primary-softest);
26
+ }
27
+
28
+ .om-coupon__badge {
29
+ flex: 0 0 auto;
30
+ width: 72px;
31
+ min-height: var(--om-control-height-md);
32
+ text-align: center;
33
+ border-radius: var(--om-radius-sm);
34
+ padding: var(--om-spacing-sm);
35
+ background: var(--ion-color-light);
36
+ display: flex;
37
+ flex-direction: column;
38
+ align-items: center;
39
+ justify-content: center;
40
+ gap: var(--om-spacing-xs);
41
+ }
42
+
43
+ .om-coupon--selected .om-coupon__badge {
44
+ background: var(--om-surface-primary-soft);
45
+ }
46
+
47
+ .om-coupon__badge-value {
48
+ display: block;
49
+ font-size: var(--om-font-size-lg);
50
+ font-weight: 700;
51
+ color: var(--ion-color-dark);
52
+ line-height: 1;
53
+ }
54
+
55
+ .om-coupon--selected .om-coupon__badge-value {
56
+ color: var(--ion-color-primary);
57
+ }
58
+
59
+ .om-coupon__badge-unit {
60
+ display: block;
61
+ font-size: var(--om-font-size-xs);
62
+ color: var(--ion-color-medium);
63
+ }
64
+
65
+ .om-coupon__body {
66
+ flex: 1 1 auto;
67
+ min-width: 0;
68
+ display: flex;
69
+ flex-direction: column;
70
+ gap: var(--om-spacing-xs);
71
+ }
72
+
73
+ .om-coupon__title {
74
+ margin: 0;
75
+ font-size: var(--om-font-size-sm);
76
+ color: var(--ion-color-dark);
77
+ font-weight: 500;
78
+ }
79
+
80
+ .om-coupon__condition,
81
+ .om-coupon__expire {
82
+ margin: 0;
83
+ font-size: var(--om-font-size-xs);
84
+ color: var(--ion-color-medium);
85
+ }
86
+
87
+ .om-coupon__radio {
88
+ flex: 0 0 auto;
89
+ width: var(--om-spacing-xl);
90
+ height: var(--om-spacing-xl);
91
+ border-radius: var(--om-radius-full);
92
+ border: 1px solid var(--ion-color-light-shade);
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ color: transparent;
97
+ }
98
+
99
+ .om-coupon--selected .om-coupon__radio {
100
+ background: var(--ion-color-primary);
101
+ border-color: var(--ion-color-primary);
102
+ color: var(--ion-color-primary-contrast);
103
+ }
104
+
105
+ .om-coupon__radio ion-icon {
106
+ font-size: var(--om-icon-size-sm);
107
+ }
@@ -0,0 +1,81 @@
1
+ /* OmDialog —— 独立一张稿的对话框形态:全屏 scrim + 居中卡片。
2
+ 不使用 IonModal —— 作为 dialog-view pattern,本身就是一个页面的主体。 */
3
+
4
+ .om-dialog {
5
+ position: absolute;
6
+ inset: 0;
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ padding: var(--om-spacing-xl);
11
+ z-index: 10;
12
+ }
13
+
14
+ .om-dialog__scrim {
15
+ position: absolute;
16
+ inset: 0;
17
+ background: var(--om-overlay-scrim);
18
+ }
19
+
20
+ .om-dialog__card {
21
+ position: relative;
22
+ background: var(--ion-background-color);
23
+ border-radius: var(--om-radius-xl);
24
+ padding: var(--om-spacing-xl) var(--om-spacing-lg) var(--om-spacing-lg);
25
+ width: 100%;
26
+ max-width: 334px;
27
+ display: flex;
28
+ flex-direction: column;
29
+ align-items: center;
30
+ gap: var(--om-spacing-lg);
31
+ text-align: center;
32
+ box-shadow: var(--om-shadow-lg);
33
+ }
34
+
35
+ .om-dialog__icon {
36
+ display: inline-flex;
37
+ align-items: center;
38
+ justify-content: center;
39
+ width: 64px;
40
+ height: 64px;
41
+ line-height: 1;
42
+ }
43
+ .om-dialog__icon ion-icon {
44
+ font-size: 64px;
45
+ }
46
+
47
+ .om-dialog__title {
48
+ margin: 0;
49
+ font-size: var(--om-font-size-lg);
50
+ font-weight: 600;
51
+ color: var(--ion-color-dark);
52
+ }
53
+
54
+ .om-dialog__subtitle {
55
+ margin: 0;
56
+ font-size: var(--om-font-size-sm);
57
+ color: var(--ion-color-medium);
58
+ }
59
+
60
+ .om-dialog__actions {
61
+ width: 100%;
62
+ margin-top: var(--om-spacing-sm);
63
+ display: flex;
64
+ flex-direction: column;
65
+ gap: var(--om-spacing-sm);
66
+ }
67
+ .om-dialog__actions--two {
68
+ flex-direction: row;
69
+ }
70
+ .om-dialog__actions--two ion-button {
71
+ flex: 1 1 0;
72
+ }
73
+ .om-dialog__actions ion-button {
74
+ --border-radius: var(--om-radius-md);
75
+ height: var(--om-control-height-xl);
76
+ font-size: var(--om-font-size-lg);
77
+ font-weight: 600;
78
+ }
79
+ .om-dialog__body {
80
+ width: 100%;
81
+ }
@@ -0,0 +1,55 @@
1
+ /* OmEmptyState —— 空态面板 */
2
+
3
+ .om-empty {
4
+ display: flex;
5
+ flex-direction: column;
6
+ align-items: center;
7
+ justify-content: center;
8
+ gap: var(--om-spacing-lg);
9
+ padding: var(--om-spacing-xl) var(--om-spacing-lg);
10
+ text-align: center;
11
+ }
12
+
13
+ .om-empty__icon {
14
+ /* 64×64 = 两倍 spacing-2xl,画报级占位大小 */
15
+ width: calc(var(--om-spacing-2xl) * 2);
16
+ height: calc(var(--om-spacing-2xl) * 2);
17
+ border-radius: var(--om-radius-full);
18
+ background: var(--ion-color-light);
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: center;
22
+ }
23
+
24
+ .om-empty__icon ion-icon {
25
+ font-size: var(--om-icon-size-xl);
26
+ }
27
+
28
+ .om-empty__title {
29
+ margin: 0;
30
+ font-size: var(--om-font-size-md);
31
+ font-weight: 500;
32
+ color: var(--ion-color-dark);
33
+ }
34
+
35
+ .om-empty__desc {
36
+ margin: 0;
37
+ font-size: var(--om-font-size-sm);
38
+ color: var(--ion-color-medium);
39
+ max-width: 280px;
40
+ line-height: 1.5;
41
+ }
42
+
43
+ .om-empty__actions {
44
+ display: flex;
45
+ flex-direction: column;
46
+ align-items: center;
47
+ gap: var(--om-spacing-sm);
48
+ width: 100%;
49
+ max-width: 240px;
50
+ margin-top: var(--om-spacing-sm);
51
+ }
52
+
53
+ .om-empty__actions ion-button {
54
+ width: 100%;
55
+ }