@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
package/PATTERNS.md ADDED
@@ -0,0 +1,134 @@
1
+ # omit-design preset-mobile — 设计模式目录
2
+
3
+ 所有业务页面(`design/**/*.tsx`)必须从这里挑一个模式作为骨架,并在文件头标注 `// @pattern: <name>`。
4
+
5
+ 新增模式 → 用 `.claude/skills/add-pattern`。
6
+
7
+ 每个 pattern 都有可复制的 **Template**。`new-design` skill 优先复制对应模板,再替换占位符。
8
+
9
+ ---
10
+
11
+ ## list-view
12
+
13
+ **用途**:一屏内展示同质条目的集合,每个条目可点击进详情。
14
+
15
+ **骨架**:
16
+ - `OmHeader` — 标题(含或不含返回)
17
+ - 可选:`OmSearchBar` / tab 条 / 分类胶囊
18
+ - `IonList` 或原生 `div` 包裹一组行(`OmListRow` / `OmCouponCard` / `OmSettingRow` / 自定义卡)
19
+ - 列表空态:`OmEmptyState` 居中提示
20
+
21
+ **Template**:[./templates/list-view.tmpl.tsx](./templates/list-view.tmpl.tsx)
22
+
23
+ **何时不用**:条目数 ≤ 3 → 用 `dashboard` 或单独卡片
24
+
25
+ ---
26
+
27
+ ## detail-view
28
+
29
+ **用途**:从列表跳进的单条详情,通常带主操作按钮。
30
+
31
+ **骨架**:
32
+ - `OmHeader` + `IonBackButton`(返回上一级)
33
+ - `OmCard`(基本信息)
34
+ - 0~N 个 `OmCard` / 列表分块(关联信息分区)
35
+ - 底部 1 个主操作 `OmButton expand="block"` 或「取消 / 确认」双按钮
36
+
37
+ **Template**:[./templates/detail-view.tmpl.tsx](./templates/detail-view.tmpl.tsx)
38
+
39
+ **何时不用**:编辑场景 → 用 `form-view`
40
+
41
+ ---
42
+
43
+ ## form-view
44
+
45
+ **用途**:新建或编辑一条记录。
46
+
47
+ **骨架**:
48
+ - `OmHeader` + `IonBackButton`(返回/取消)
49
+ - 一组 `OmInput` / `OmSelect`,按业务分组
50
+ - 内联错误态(红框 + 红色辅助文案)
51
+ - 底部固定 `OmButton expand="block"` 提交
52
+ - 可选:提交结果用 `OmDialog` 弹出
53
+
54
+ **Template**:[./templates/form-view.tmpl.tsx](./templates/form-view.tmpl.tsx)
55
+
56
+ ---
57
+
58
+ ## sheet-action
59
+
60
+ **用途**:从底部弹起的操作菜单 / 详情抽屉,避免离开当前上下文。设计稿语境下不走 JS 回调,而是每个 sheet 一张独立稿(同 `dialog-view` 哲学)。
61
+
62
+ **骨架**:
63
+ - `OmPage padding="none"`
64
+ - `OmSheet title="..." dismissHref="..."` 内部放:
65
+ - 一组按钮条(菜单形态)
66
+ - 或纯信息块(明细形态)
67
+ - 关闭行为:点击 scrim / 右上 × / 菜单条任意一项跳转走
68
+
69
+ **Template**:[./templates/sheet-action.tmpl.tsx](./templates/sheet-action.tmpl.tsx)
70
+
71
+ **何时不用**:只展示一句提示 + 单按钮 → `dialog-view`;需要录入 → `form-view`
72
+
73
+ ---
74
+
75
+ ## dialog-view
76
+
77
+ **用途**:把一个"对话框状态"当成一张独立的设计稿。设计稿工具语境下,弹窗不是 JS 回调,而是一张可独立访问的稿 — 每个状态有 URL、侧边目录里可点。
78
+
79
+ **骨架**:
80
+ - `OmPage padding="none" header={<OmHeader .../>}`(通常复用来源页的 header)
81
+ - 背景层:来源页的 frozen 快照(表单禁用,仅作视觉背景),或纯渐变背景
82
+ - `OmDialog` — icon / title / subtitle + 一个主按钮;按钮通过 `confirmHref` 跳转到下一张稿
83
+
84
+ **Template**:[./templates/dialog-view.tmpl.tsx](./templates/dialog-view.tmpl.tsx)
85
+
86
+ **何时不用**:需要输入 / 选项 → 独立 `form-view`;纯操作菜单 → `sheet-action`
87
+
88
+ ---
89
+
90
+ ## welcome-view
91
+
92
+ **用途**:启动 / 欢迎 / 引导页。品牌 logo + 一句话欢迎语 + 单一主 CTA。不接任何表单。
93
+
94
+ **骨架**:
95
+ - `OmPage padding="none"` 自己控制留白
96
+ - 品牌头区:LOGO + 品名(可含副标题)
97
+ - 欢迎语:主标题 + 描述
98
+ - 底部单一 `OmButton expand="block"` 主操作
99
+ - 可选:最底部版本号
100
+
101
+ **Template**:[./templates/welcome-view.tmpl.tsx](./templates/welcome-view.tmpl.tsx)
102
+
103
+ **何时不用**:带输入 → `form-view`;功能介绍多屏滑动 → 需新增 `onboarding-carousel` 模式
104
+
105
+ ---
106
+
107
+ ## dashboard
108
+
109
+ **用途**:工作台 / 首页式聚合页 — 顶部状态指标(数字卡)+ 功能宫格入口。
110
+ 非 tab 导航、非列表、非表单,而是多入口聚合。
111
+
112
+ **骨架**:
113
+ - `OmHeader` + 返回
114
+ - 若干 `OmStatCard`(指标区)
115
+ - 3 列 `OmMenuCard` 宫格(主功能入口,可用 `disabled + badge="二期"` 标灰态)
116
+
117
+ **Template**:[./templates/dashboard.tmpl.tsx](./templates/dashboard.tmpl.tsx)
118
+
119
+ **何时不用**:单一主操作 → `welcome-view`;扁平列表 → `list-view`
120
+
121
+ ---
122
+
123
+ ## tab-view
124
+
125
+ **用途**:底部主导航 tab 之一 — 主品牌 header + 主体内容 + 底部 4 个 tab 切换。
126
+
127
+ **骨架**:
128
+ - `OmAppBar variant="brand"` 顶部品牌标题 + 右上角头像
129
+ - 主体内容(表单 / 列表 / 卡片 / 空态)
130
+ - 底部 `OmTabBar` 多个主 tab
131
+
132
+ **Template**:[./templates/tab-view.tmpl.tsx](./templates/tab-view.tmpl.tsx)
133
+
134
+ **何时不用**:无底部 tab 导航(如独立表单页)→ `form-view`
package/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # @omit-design/preset-mobile
2
+
3
+ omit-design 的默认移动端 preset:Om* 组件白名单 + `--om-*` token 体系 + Ionic 8 运行时。
4
+
5
+ ## 三条硬规则(由 [@omit-design/eslint-plugin](../eslint-plugin/) 强制)
6
+
7
+ 1. **Token 优先**:所有颜色、间距、字号、圆角、阴影必须走 token,**禁止字面量**(`#FF6B00`、`12px`、`16px` 等都不允许出现在业务代码里)
8
+ 2. **组件白名单**:业务页面(`design/**`)只能 import `@omit-design/preset-mobile`,**禁止**直接 import `@ionic/react`(例外:`IonList` / `IonBackButton` / `IonIcon`,仅做排版/图标宿主)
9
+ 3. **模式标注**:每个业务页面文件头第一行必须是 `// @pattern: <name>`,name 必须存在于 [PATTERNS.md](./PATTERNS.md)
10
+
11
+ ## 组件清单
12
+
13
+ 21 个 Om* 组件,全部从 `@omit-design/preset-mobile` 导出:
14
+
15
+ `OmPage` `OmHeader` `OmAppBar` `OmButton` `OmCard` `OmListRow` `OmInput` `OmSelect` `OmDialog` `OmTabBar` `OmNumpad` `OmSearchBar` `OmProductCard` `OmEmptyState` `OmTag` `OmOrderFooter` `OmCouponCard` `OmStatCard` `OmMenuCard` `OmSettingRow` `OmSheet`
16
+
17
+ 详见 [components/index.ts](./components/index.ts)。
18
+
19
+ ## 设计模式
20
+
21
+ 8 个开箱即用的 pattern,每个有可复制的 template:
22
+
23
+ `list-view` `detail-view` `form-view` `sheet-action` `dialog-view` `welcome-view` `dashboard` `tab-view`
24
+
25
+ 详见 [PATTERNS.md](./PATTERNS.md)。
26
+
27
+ ## Token 命名
28
+
29
+ `--om-color-*` `--om-spacing-*` `--om-radius-*` `--om-font-size-*` `--om-shadow-*` 详见 [theme/variables.css](./theme/variables.css)。
package/catalog.tsx ADDED
@@ -0,0 +1,365 @@
1
+ /**
2
+ * preset-mobile 组件目录 —— 主题编辑器 WYSIWYG 预览用。
3
+ * 这个文件不受 ESLint whitelist 约束(属于 preset,不是业务稿)。
4
+ */
5
+
6
+ import { cartOutline, gridOutline, personOutline } from "ionicons/icons";
7
+ import type { CatalogGroup } from "@omit-design/engine/registry";
8
+ import {
9
+ OmButton,
10
+ OmCard,
11
+ OmTag,
12
+ OmListRow,
13
+ OmInput,
14
+ OmSearchBar,
15
+ OmStatCard,
16
+ OmProductCard,
17
+ OmMenuCard,
18
+ OmTabBar,
19
+ OmEmptyState,
20
+ OmSettingRow,
21
+ OmOrderFooter,
22
+ OmCouponCard,
23
+ } from "./components";
24
+
25
+ export const catalog: CatalogGroup[] = [
26
+ // ─── 基础 ────────────────────────────────────────────────────────────────
27
+ {
28
+ id: "foundation",
29
+ label: "基础",
30
+ icon: "🎨",
31
+ items: [
32
+ {
33
+ id: "colors",
34
+ name: "颜色 / Colors",
35
+ description: "当前主题的语义色",
36
+ render: () => (
37
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
38
+ {[
39
+ { key: "primary", cssVar: "--ion-color-primary" },
40
+ { key: "secondary", cssVar: "--ion-color-secondary" },
41
+ { key: "success", cssVar: "--ion-color-success" },
42
+ { key: "warning", cssVar: "--ion-color-warning" },
43
+ { key: "danger", cssVar: "--ion-color-danger" },
44
+ { key: "dark", cssVar: "--ion-color-dark" },
45
+ { key: "medium", cssVar: "--ion-color-medium" },
46
+ { key: "light", cssVar: "--ion-color-light" },
47
+ ].map((c) => (
48
+ <div key={c.key} style={{ textAlign: "center" }}>
49
+ <div
50
+ style={{
51
+ width: 40,
52
+ height: 40,
53
+ borderRadius: "var(--om-radius-full)",
54
+ background: `var(${c.cssVar})`,
55
+ border: "1px solid rgba(0,0,0,0.1)",
56
+ marginBottom: 4,
57
+ }}
58
+ />
59
+ <div style={{ fontSize: 10, color: "#999" }}>{c.key}</div>
60
+ </div>
61
+ ))}
62
+ </div>
63
+ ),
64
+ },
65
+ {
66
+ id: "spacing",
67
+ name: "间距 / Spacing",
68
+ description: "spacing token 视觉参考",
69
+ render: () => (
70
+ <div style={{ display: "flex", flexDirection: "column", gap: 6, width: "100%" }}>
71
+ {[
72
+ { key: "xs", cssVar: "--om-spacing-xs" },
73
+ { key: "sm", cssVar: "--om-spacing-sm" },
74
+ { key: "md", cssVar: "--om-spacing-md" },
75
+ { key: "lg", cssVar: "--om-spacing-lg" },
76
+ { key: "xl", cssVar: "--om-spacing-xl" },
77
+ { key: "2xl", cssVar: "--om-spacing-2xl" },
78
+ ].map((s) => (
79
+ <div key={s.key} style={{ display: "flex", alignItems: "center", gap: 8 }}>
80
+ <div
81
+ style={{
82
+ height: 12,
83
+ width: `var(${s.cssVar})`,
84
+ minWidth: `var(${s.cssVar})`,
85
+ background: "var(--ion-color-primary)",
86
+ borderRadius: 2,
87
+ opacity: 0.7,
88
+ }}
89
+ />
90
+ <span style={{ fontSize: 11, color: "#999" }}>{s.key}</span>
91
+ </div>
92
+ ))}
93
+ </div>
94
+ ),
95
+ },
96
+ ],
97
+ },
98
+
99
+ // ─── 按钮 ────────────────────────────────────────────────────────────────
100
+ {
101
+ id: "button",
102
+ label: "按钮",
103
+ icon: "🔲",
104
+ items: [
105
+ {
106
+ id: "button-variants",
107
+ name: "OmButton",
108
+ description: "solid / outline / clear × 颜色",
109
+ render: () => (
110
+ <div style={{ display: "flex", flexDirection: "column", gap: 8, width: "100%" }}>
111
+ <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
112
+ <OmButton color="primary">Primary</OmButton>
113
+ <OmButton color="primary" variant="outline">Outline</OmButton>
114
+ <OmButton color="primary" variant="clear">Clear</OmButton>
115
+ </div>
116
+ <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
117
+ <OmButton color="secondary">Secondary</OmButton>
118
+ <OmButton color="danger">Danger</OmButton>
119
+ <OmButton color="primary" disabled>Disabled</OmButton>
120
+ </div>
121
+ <OmButton color="primary" expand="block">Block 按钮</OmButton>
122
+ </div>
123
+ ),
124
+ },
125
+ ],
126
+ },
127
+
128
+ // ─── 输入 ────────────────────────────────────────────────────────────────
129
+ {
130
+ id: "input",
131
+ label: "输入",
132
+ icon: "✏️",
133
+ items: [
134
+ {
135
+ id: "input-basic",
136
+ name: "OmInput",
137
+ description: "文本输入框(正常 + 错误态)",
138
+ render: () => (
139
+ <div style={{ display: "flex", flexDirection: "column", gap: 8, width: "100%" }}>
140
+ <OmInput label="商品名称" placeholder="请输入商品名称" />
141
+ <OmInput label="价格" placeholder="0.00" type="number" />
142
+ <OmInput label="手机号" value="138****8888" errorText="格式不正确" />
143
+ </div>
144
+ ),
145
+ },
146
+ {
147
+ id: "search-bar",
148
+ name: "OmSearchBar",
149
+ description: "圆角胶囊搜索框",
150
+ render: () => (
151
+ <div style={{ width: "100%" }}>
152
+ <OmSearchBar placeholder="搜索商品名称…" />
153
+ </div>
154
+ ),
155
+ },
156
+ ],
157
+ },
158
+
159
+ // ─── 卡片 ────────────────────────────────────────────────────────────────
160
+ {
161
+ id: "card",
162
+ label: "卡片",
163
+ icon: "🃏",
164
+ items: [
165
+ {
166
+ id: "card-basic",
167
+ name: "OmCard",
168
+ description: "通用内容卡片",
169
+ render: () => (
170
+ <OmCard title="今日营业额" subtitle="含 2 笔待支付">
171
+ <div style={{ fontSize: 28, fontWeight: 700, color: "var(--ion-color-primary)" }}>
172
+ ¥ 12,480.50
173
+ </div>
174
+ </OmCard>
175
+ ),
176
+ },
177
+ {
178
+ id: "stat-card",
179
+ name: "OmStatCard",
180
+ description: "核心数字指标卡",
181
+ render: () => (
182
+ <div style={{ display: "flex", gap: 8, width: "100%" }}>
183
+ <OmStatCard label="今日订单" value="128" caption="+12 较昨日" />
184
+ <OmStatCard label="客单价" value="¥97.5" caption="↑ 3.2%" />
185
+ </div>
186
+ ),
187
+ },
188
+ {
189
+ id: "product-card",
190
+ name: "OmProductCard",
191
+ description: "商品卡(含加购按钮)",
192
+ render: () => (
193
+ <OmProductCard
194
+ name="奥利奥饼干 256g"
195
+ sku="SKU-001"
196
+ price={12.5}
197
+ unit="件"
198
+ stock={208}
199
+ />
200
+ ),
201
+ },
202
+ {
203
+ id: "menu-card",
204
+ name: "OmMenuCard",
205
+ description: "宫格快捷入口卡",
206
+ render: () => (
207
+ <div style={{ display: "flex", gap: 8 }}>
208
+ <OmMenuCard icon={cartOutline} label="收银" />
209
+ <OmMenuCard icon={personOutline} label="会员" />
210
+ <OmMenuCard icon={gridOutline} label="商品" badge={3} />
211
+ </div>
212
+ ),
213
+ },
214
+ {
215
+ id: "coupon-card",
216
+ name: "OmCouponCard",
217
+ description: "优惠券 / 奖励卡片",
218
+ render: () => (
219
+ <div style={{ display: "flex", flexDirection: "column", gap: 8, width: "100%" }}>
220
+ <OmCouponCard
221
+ valueLabel="¥20"
222
+ unitLabel="代金券"
223
+ title="满 100 减 20"
224
+ condition="全场通用"
225
+ expireDate="2026-12-31"
226
+ />
227
+ <OmCouponCard
228
+ valueLabel="8.5"
229
+ unitLabel="折"
230
+ title="85折优惠"
231
+ selected
232
+ />
233
+ </div>
234
+ ),
235
+ },
236
+ ],
237
+ },
238
+
239
+ // ─── 列表 ────────────────────────────────────────────────────────────────
240
+ {
241
+ id: "list",
242
+ label: "列表",
243
+ icon: "📋",
244
+ items: [
245
+ {
246
+ id: "list-row",
247
+ name: "OmListRow",
248
+ description: "通用列表行(左右 slot)",
249
+ render: () => (
250
+ <div style={{ width: "100%" }}>
251
+ <OmListRow title="桌号 A01" detail="2人 · 30 min" trailing="已点" />
252
+ <OmListRow title="会员手机号" trailing="138****8888" />
253
+ <OmListRow title="收银员" trailing="张三" />
254
+ </div>
255
+ ),
256
+ },
257
+ {
258
+ id: "setting-row",
259
+ name: "OmSettingRow",
260
+ description: "设置行(toggle / navigate)",
261
+ render: () => (
262
+ <div style={{ width: "100%" }}>
263
+ <OmSettingRow label="打印小票" kind="toggle" enabled={true} />
264
+ <OmSettingRow label="结账方式" kind="navigate" />
265
+ <OmSettingRow label="声音提示" kind="toggle" enabled={false} description="结账完成时播放提示音" />
266
+ </div>
267
+ ),
268
+ },
269
+ ],
270
+ },
271
+
272
+ // ─── 标签 ────────────────────────────────────────────────────────────────
273
+ {
274
+ id: "badge",
275
+ label: "标签",
276
+ icon: "🏷️",
277
+ items: [
278
+ {
279
+ id: "tag-colors",
280
+ name: "OmTag",
281
+ description: "状态标签 —— 语义色",
282
+ render: () => (
283
+ <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
284
+ <OmTag color="primary">已完成</OmTag>
285
+ <OmTag color="secondary">进行中</OmTag>
286
+ <OmTag color="success">已支付</OmTag>
287
+ <OmTag color="warning">待处理</OmTag>
288
+ <OmTag color="danger">已取消</OmTag>
289
+ <OmTag color="medium">草稿</OmTag>
290
+ </div>
291
+ ),
292
+ },
293
+ ],
294
+ },
295
+
296
+ // ─── 导航 ────────────────────────────────────────────────────────────────
297
+ {
298
+ id: "nav",
299
+ label: "导航",
300
+ icon: "🧭",
301
+ items: [
302
+ {
303
+ id: "tab-bar",
304
+ name: "OmTabBar",
305
+ description: "底部 Tab 导航条",
306
+ render: () => (
307
+ <div style={{ width: "100%" }}>
308
+ <OmTabBar
309
+ items={[
310
+ { tab: "sales", href: "#", label: "销售", icon: cartOutline },
311
+ { tab: "workstation", href: "#", label: "工作台", icon: gridOutline },
312
+ { tab: "member", href: "#", label: "会员", icon: personOutline },
313
+ ]}
314
+ />
315
+ </div>
316
+ ),
317
+ },
318
+ ],
319
+ },
320
+
321
+ // ─── 操作 ────────────────────────────────────────────────────────────────
322
+ {
323
+ id: "action",
324
+ label: "操作",
325
+ icon: "⚡",
326
+ items: [
327
+ {
328
+ id: "order-footer",
329
+ name: "OmOrderFooter",
330
+ description: "购物车 / 结账底部操作栏",
331
+ render: () => (
332
+ <div style={{ width: "100%" }}>
333
+ <OmOrderFooter
334
+ primaryAmount="¥128.50"
335
+ ctaLabel="结算 (3)"
336
+ cartCount={3}
337
+ layout="amount-split"
338
+ />
339
+ </div>
340
+ ),
341
+ },
342
+ ],
343
+ },
344
+
345
+ // ─── 空态 ────────────────────────────────────────────────────────────────
346
+ {
347
+ id: "empty",
348
+ label: "空态",
349
+ icon: "🫙",
350
+ items: [
351
+ {
352
+ id: "empty-state",
353
+ name: "OmEmptyState",
354
+ description: "无数据 / 搜索结果为空",
355
+ render: () => (
356
+ <OmEmptyState
357
+ icon="🔍"
358
+ title="没有找到商品"
359
+ description="请尝试其他关键词"
360
+ />
361
+ ),
362
+ },
363
+ ],
364
+ },
365
+ ];
@@ -0,0 +1,63 @@
1
+ import type { ReactNode } from "react";
2
+ import { IonIcon } from "@ionic/react";
3
+ import { storefront } from "ionicons/icons";
4
+ import { inspectAttrs } from "./inspect-attrs";
5
+ import "./om-app-bar.css";
6
+
7
+ interface BrandVariantProps {
8
+ variant: "brand";
9
+ brandTitle: string;
10
+ /** 右侧按钮组(IonIcon 按钮等) */
11
+ right?: ReactNode;
12
+ /** 右侧头像 slot(圆形) */
13
+ avatar?: ReactNode;
14
+ }
15
+
16
+ interface StoreVariantProps {
17
+ variant?: "store";
18
+ /** 门店名 + 门店编码:e.g. "文庙后街店(01001-029)" */
19
+ storeTitle: string;
20
+ /** 银台描述:e.g. "银台(0048)" */
21
+ tillTitle: string;
22
+ /** 右侧按钮组(扫码 / 更多) */
23
+ right?: ReactNode;
24
+ }
25
+
26
+ type OmAppBarProps = BrandVariantProps | StoreVariantProps;
27
+
28
+ /**
29
+ * POS 应用级 header。
30
+ * - `variant="store"` —— 销售主页:显示门店 + 银台 + 右侧图标区。
31
+ * - `variant="brand"` —— 商户中心:显示品牌 logo 文案 + 右侧头像/按钮。
32
+ *
33
+ * 作为 `<OmPage header={...}>` 的传参,与 IonContent 同级,不随内容滚动。
34
+ */
35
+ export function OmAppBar(props: OmAppBarProps) {
36
+ if (props.variant === "brand") {
37
+ const { brandTitle, right, avatar } = props;
38
+ return (
39
+ <div className="om-app-bar pos-app-bar--brand" {...inspectAttrs("OmAppBar", { bg: "background", spacing: "lg" })}>
40
+ <div className="om-app-bar__brand">
41
+ <IonIcon icon={storefront} color="primary" className="om-app-bar__brand-icon" aria-hidden />
42
+ <span className="om-app-bar__brand-title">{brandTitle}</span>
43
+ </div>
44
+ <div className="om-app-bar__right">
45
+ {right}
46
+ {avatar && <div className="om-app-bar__avatar">{avatar}</div>}
47
+ </div>
48
+ </div>
49
+ );
50
+ }
51
+
52
+ const { storeTitle, tillTitle, right } = props;
53
+ return (
54
+ <div className="om-app-bar pos-app-bar--store" {...inspectAttrs("OmAppBar", { bg: "background", spacing: "lg" })}>
55
+ <div className="om-app-bar__info">
56
+ <p className="om-app-bar__info-primary">{storeTitle}</p>
57
+ <p className="om-app-bar__info-secondary">{tillTitle}</p>
58
+ </div>
59
+ <div className="om-app-bar__divider" aria-hidden />
60
+ <div className="om-app-bar__right">{right}</div>
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,41 @@
1
+ import type { ReactNode } from "react";
2
+ import { IonButton } from "@ionic/react";
3
+ import { inspectAttrs } from "./inspect-attrs";
4
+ import type { ColorTokenName } from "../tokens";
5
+
6
+ type OmButtonVariant = "solid" | "outline" | "clear";
7
+ type OmButtonSize = "small" | "default" | "large";
8
+
9
+ interface OmButtonProps {
10
+ children: ReactNode;
11
+ variant?: OmButtonVariant;
12
+ size?: OmButtonSize;
13
+ color?: ColorTokenName;
14
+ expand?: "block" | "full";
15
+ disabled?: boolean;
16
+ onClick?: () => void;
17
+ }
18
+
19
+ export function OmButton({
20
+ children,
21
+ variant = "solid",
22
+ size = "default",
23
+ color = "primary",
24
+ expand,
25
+ disabled,
26
+ onClick,
27
+ }: OmButtonProps) {
28
+ return (
29
+ <IonButton
30
+ fill={variant === "solid" ? "solid" : variant === "outline" ? "outline" : "clear"}
31
+ size={size}
32
+ color={color}
33
+ expand={expand}
34
+ disabled={disabled}
35
+ onClick={onClick}
36
+ {...inspectAttrs("OmButton", { color, radius: "md" })}
37
+ >
38
+ {children}
39
+ </IonButton>
40
+ );
41
+ }
@@ -0,0 +1,28 @@
1
+ import type { ReactNode } from "react";
2
+ import { IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle } from "@ionic/react";
3
+ import { inspectAttrs } from "./inspect-attrs";
4
+
5
+ interface OmCardProps {
6
+ title?: string;
7
+ subtitle?: string;
8
+ children?: ReactNode;
9
+ onClick?: () => void;
10
+ }
11
+
12
+ export function OmCard({ title, subtitle, children, onClick }: OmCardProps) {
13
+ return (
14
+ <IonCard
15
+ button={!!onClick}
16
+ onClick={onClick}
17
+ {...inspectAttrs("OmCard", { bg: "background", radius: "lg", shadow: "md" })}
18
+ >
19
+ {(title || subtitle) && (
20
+ <IonCardHeader>
21
+ {subtitle && <IonCardSubtitle>{subtitle}</IonCardSubtitle>}
22
+ {title && <IonCardTitle>{title}</IonCardTitle>}
23
+ </IonCardHeader>
24
+ )}
25
+ {children && <IonCardContent>{children}</IonCardContent>}
26
+ </IonCard>
27
+ );
28
+ }