@manohub/ui 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -38,6 +38,7 @@ import { Upload, formatFileSize } from "./components/upload/index.js";
38
38
  import { QueryState } from "./components/query-state/index.js";
39
39
  import { Loading } from "./components/loading/index.js";
40
40
  import { mergePartStyle, partPaddingStyle } from "./components/part-padding.js";
41
+ import { NumberInput } from "./components/input/number-input.js";
41
42
  import { SelectTree } from "./components/select/select-tree.js";
42
43
  import { alert, closeAllDialogs, confirm } from "./services/dialog.js";
43
44
  import { clearToasts, toast } from "./services/toast.js";
@@ -66,6 +67,7 @@ export {
66
67
  Nav,
67
68
  Notice,
68
69
  Notification,
70
+ NumberInput,
69
71
  Page,
70
72
  Pagination,
71
73
  Panel,
@@ -415,6 +415,20 @@
415
415
  color: var(--ui-base-content);
416
416
  }
417
417
 
418
+ /* 标题左端的品牌色竖标(主色胶囊,值与文档站示例标题同一套:见 `form.tokens.css`)。
419
+ * **只在有标题文本时出现**:这一行也会在「只给 `extra`」时渲染(筛选条那种),
420
+ * 那时凭空多一条竖标就成了坏的排版 —— 用 `:has()` 守住真正有标题的那一种。
421
+ * `flex: none`:标题换行时竖标不许被挤扁。竖标与文本之间的间距由标题行既有的 `gap` 给,
422
+ * 本句不另写间距(两处各写一遍必然漂)。 */
423
+ .mh-form__title:has(> .mh-form__title-text)::before {
424
+ content: '';
425
+ flex: none;
426
+ width: var(--ui-form-title-bar-width);
427
+ height: var(--ui-form-title-bar-height);
428
+ border-radius: var(--ui-radius-full);
429
+ background: var(--ui-primary);
430
+ }
431
+
418
432
  /* 标题文本允许被挤窄(右端操作位不参与收缩);长标题**换行**而不是截断
419
433
  —— 截断会让这一块读不出是什么。 */
420
434
 
@@ -12,4 +12,8 @@
12
12
  [data-manohub-ui] {
13
13
  --ui-form-label-width: 120px;
14
14
  --ui-form-column-min: 320px;
15
+ /* 标题行左端的品牌色竖标(3×12 胶囊)。与文档站示例标题(`.mh-demo__title::before`)
16
+ 是同一套值:「页面里的一块标题」这一语言在库内与文档站只留一份规格。 */
17
+ --ui-form-title-bar-width: 3px;
18
+ --ui-form-title-bar-height: 12px;
15
19
  }
@@ -188,34 +188,45 @@
188
188
  inset: 0;
189
189
  }
190
190
 
191
- /* ===== 无边框档(表格内编辑 / 行内编辑:没有框也没有底,靠悬停给反馈) ===== */
191
+ /* ===== 无边框档(`bordered={false}`:框与底全退,**任何状态都不再出现**) =====
192
+ * 本档的效果**只有一种**:无框。悬停不亮底、聚焦不显框也不留环 —— 「常态无框、聚焦才显框」
193
+ * 是**另一档** `bordered="focus"`(见下一节),不要把两种效果叠在同一档上。
194
+ *
195
+ * **只读值档**(`readonly` + `bordered={false}`):几何与可编辑输入框**完全一致**(同高 / 同内距 /
196
+ * 值文字同左缘),仍可聚焦、可选中复制(只读 ≠ 禁用:要的就是能选、能复制),
197
+ * 但「可编辑」的线索本来就一条都没有 —— 它是摘要 / 详情里值回显的推荐写法,
198
+ * 也是 `Form.Item` 的 `readonly` 行配的那一档。
199
+ *
200
+ * 中和句与通用状态规则**同为 0-2-0**(`.mh-input:hover` / `.mh-input:focus`),靠写在它们之后取胜。 */
192
201
  .mh-input--borderless {
193
202
  border-color: transparent;
194
203
  background: transparent;
195
204
  border-radius: 0;
196
205
  }
197
206
  .mh-input--borderless:hover,
198
- .mh-input--borderless.is-hover {
199
- border-color: transparent;
200
- background: var(--ui-base-200);
201
- }
207
+ .mh-input--borderless.is-hover,
202
208
  .mh-input--borderless:focus,
203
209
  .mh-input--borderless.is-focus {
204
- border-color: var(--ui-input-line-hover);
205
- background: var(--ui-base-100);
206
- box-shadow: 0 0 0 2px var(--ui-primary-weak);
210
+ border-color: transparent;
211
+ background: transparent;
212
+ box-shadow: none;
207
213
  }
208
- /* **只读值档**(`readonly` + `bordered={false}`):几何照旧(与可编辑输入框同高、同内距),
209
- 但把「可编辑」的线索全退掉 —— 只读值不该在滑过时亮起浅底、被点中时亮出边框与主色环。
210
- 仍然可聚焦、可选中复制(只读 ≠ 禁用:要的就是能选、能复制)。
211
- 它是摘要 / 详情里值回显的推荐写法,也是 `Form.Item` 的 `readonly` 行配的那一档。
212
- 特异性 0-3-0,压过上面两条(0-2-0);带壳时框主是壳,同款规则见文件下半部分。 */
213
- .mh-input--borderless[readonly]:hover,
214
- .mh-input--borderless[readonly].is-hover,
215
- .mh-input--borderless[readonly]:focus,
216
- .mh-input--borderless[readonly].is-focus {
214
+
215
+ /* ===== 聚焦显框档(`bordered="focus"`:行内编辑那档)=====
216
+ * 常态无框无底;**悬停与聚焦复用有框档那两条**(`.mh-input:hover` / `.mh-input:focus`:框转 hover
217
+ * 线色、聚焦再加主色弱底环)—— 本档**不另写任何状态描写**,于是「聚焦 = 有框档的样子」这句话
218
+ * 在面上只有一份。
219
+ * 那两条是 0-2-0、本档常态是 0-1-0 ⇒ 悬停 / 聚焦必定压过常态,**与顺序无关**(裸控件这一层)。
220
+ * 配 `readonly`:把复用的线索中和掉(只读值不许看起来能编辑;推荐写法是 `readonly` + `false`)。 */
221
+ .mh-input--border-on-focus {
217
222
  border-color: transparent;
218
223
  background: transparent;
224
+ }
225
+ .mh-input--border-on-focus[readonly]:hover,
226
+ .mh-input--border-on-focus[readonly].is-hover,
227
+ .mh-input--border-on-focus[readonly]:focus,
228
+ .mh-input--border-on-focus[readonly].is-focus {
229
+ border-color: transparent;
219
230
  box-shadow: none;
220
231
  }
221
232
 
@@ -262,13 +273,17 @@
262
273
  * **裸 `.mh-input`(不带壳)仍由控件自己画框**(Select 的搜索行就靠它),见上一节。
263
274
  */
264
275
  .mh-input__wrap {
276
+ /* 左右内距走**内部变量**再吃令牌:壳是框与内距的主,而档位类挂在控件上(壳是它的父),
277
+ 档位规则要能改到壳的内距 —— 见下面两条 `:has()`。写成 `--mh-*` 是刻意的(内部中转),
278
+ 消费方要改内距请重设 `--ui-input-padding-x*` 那三个令牌。 */
279
+ --mh-input-padding-x: var(--ui-input-padding-x);
265
280
  position: relative;
266
281
  display: inline-flex;
267
282
  align-items: center;
268
283
  gap: var(--ui-input-affix-gap);
269
284
  width: 100%;
270
285
  height: var(--ui-input-height);
271
- padding: 0 var(--ui-input-padding-x);
286
+ padding: 0 var(--mh-input-padding-x);
272
287
  border: 1px solid var(--ui-input-line);
273
288
  border-radius: var(--ui-input-radius);
274
289
  background: var(--ui-base-100);
@@ -276,16 +291,21 @@
276
291
  transition-duration: 0.15s;
277
292
  transition-timing-function: ease;
278
293
  }
294
+ /* ⚠️ 档位**必须连内距一起换**,不能只换高度:壳内控件的内距被归零(见 `.mh-input__wrap > .mh-input`),
295
+ 壳的 12px 若原样留着,sm(24 高)就成了「矮框里塞着标准档的内距」—— 左右各 12 对 13 号字太挤,
296
+ 看起来像内距坏了。2026-10-08 修:三档的高度与左右内距成对下发。 */
279
297
  .mh-input__wrap:has(> .mh-input--sm) {
298
+ --mh-input-padding-x: var(--ui-input-padding-x-sm);
280
299
  height: var(--ui-input-height-sm);
281
300
  }
282
301
  .mh-input__wrap:has(> .mh-input--lg) {
302
+ --mh-input-padding-x: var(--ui-input-padding-x-lg);
283
303
  height: var(--ui-input-height-lg);
284
304
  }
285
- /* 多行:高归内容(`resize` 要能改高),框仍归壳 */
305
+ /* 多行:高归内容(`resize` 要能改高),框仍归壳;左右内距同样随档(走同一个变量) */
286
306
  .mh-input__wrap:has(> .mh-input--textarea) {
287
307
  height: auto;
288
- padding: var(--ui-space-2) var(--ui-input-padding-x);
308
+ padding: var(--ui-space-2) var(--mh-input-padding-x);
289
309
  }
290
310
  /* 壳内控件占满剩余宽度,且允许被压缩(min-width:0);附加位增减时输入区自动伸缩 */
291
311
  .mh-input__wrap > .mh-input {
@@ -309,6 +329,15 @@
309
329
  .mh-input__wrap > .mh-input.is-focus {
310
330
  box-shadow: none;
311
331
  }
332
+ /* **聚焦显框档(`bordered="focus"`)的常态**:框与底全退。
333
+ * ⚠️ 这个位置是**语义要求**,不是排版偏好:本句与下面「悬停 / 聚焦」两条**同为 0-2-0**
334
+ * (`:has()` 按参数里最具体的那个算,这里是一个类),同特异性时 CSS 取**后写者** ——
335
+ * 本句若写在那两条之后,聚焦时框永远显不出来。本档的悬停 / 聚焦表现**完全复用**下面那两条
336
+ * (框转 hover 线色 + 主色弱底环),故本档在这里只有「常态」这一句,不另写状态描写。 */
337
+ .mh-input__wrap:has(> .mh-input--border-on-focus) {
338
+ border-color: transparent;
339
+ background: transparent;
340
+ }
312
341
  /* 悬停:反馈画在壳上(壳铺满整件,附加位区域也一起响应) */
313
342
  .mh-input__wrap:hover {
314
343
  border-color: var(--ui-input-line-hover);
@@ -344,29 +373,23 @@
344
373
  background: var(--ui-base-200);
345
374
  border-color: var(--ui-base-300);
346
375
  }
347
- /* 无边框档(表格内编辑 / 行内编辑):框与底全退,悬停给浅底、聚焦才显出框 */
376
+ /* 无边框档(带壳时框主是壳,`bordered={false}`):框与底全退,**任何状态都不再出现**
377
+ * —— 口径与裸控件那节完全一致(本档的效果只有一种)。
378
+ * 中和句 0-3-0 压过通用悬停 / 聚焦(0-2-0),故写在本节(它们之后)也成立;
379
+ * 配 `readonly` 即**只读值档**,几何照旧、线索全无(说明见裸控件那节)。
380
+ * 带壳的 `bordered="focus"` 档只有「常态」那一句,在文件上半部分(与通用悬停 / 聚焦同段,位置有讲究)。 */
348
381
  .mh-input__wrap:has(> .mh-input--borderless) {
349
382
  border-color: transparent;
350
383
  border-radius: 0;
351
384
  background: transparent;
352
385
  }
353
- .mh-input__wrap:has(> .mh-input--borderless):hover {
354
- border-color: transparent;
355
- background: var(--ui-base-200);
356
- }
357
- .mh-input__wrap:has(> .mh-input--borderless):focus-within {
358
- border-color: var(--ui-input-line-hover);
359
- background: var(--ui-base-100);
360
- box-shadow: 0 0 0 2px var(--ui-primary-weak);
361
- }
362
- /* **只读值档**同款(带壳时框主是壳):退掉悬停底与聚焦的边框 / 底色 / 主色环。
363
- 特异性 0-4-0,压过上面两条(0-3-0),并且写在它们之后。 */
364
- .mh-input__wrap:has(> .mh-input--borderless[readonly]):hover,
365
- .mh-input__wrap:has(> .mh-input--borderless[readonly]).is-hover {
366
- border-color: transparent;
367
- background: transparent;
368
- }
369
- .mh-input__wrap:has(> .mh-input--borderless[readonly]):focus-within {
386
+ .mh-input__wrap:has(> .mh-input--borderless):hover,
387
+ .mh-input__wrap:has(> .mh-input--borderless).is-hover,
388
+ .mh-input__wrap:has(> .mh-input--borderless):focus-within,
389
+ /* 静态档(`.is-hover` / `.is-focus` 挂在**控件**上,与通用的 `:has(> .mh-input.is-focus)` 同源)——
390
+ 漏了这两条,截图 / 单测走静态档时框就冒出来了(`:focus-within` 那条盖不住它,两者都是 0-3-0) */
391
+ .mh-input__wrap:has(> .mh-input--borderless.is-hover),
392
+ .mh-input__wrap:has(> .mh-input--borderless.is-focus) {
370
393
  border-color: transparent;
371
394
  background: transparent;
372
395
  box-shadow: none;
@@ -488,6 +511,104 @@
488
511
  bottom: var(--ui-space-1);
489
512
  }
490
513
 
514
+ /* ===== 数字输入(NumberInput;与 Input 同族同面) =====
515
+ *
516
+ * 表壳 / 内距 / 状态协议全部复用上面几节 —— 本件只多两样:控件上的 `.mh-input--number`
517
+ * 与右端的一对步进按钮。故**没有**独立的面文件,也不进 `styles/index.css` 的登记清单
518
+ * (那是「一组件一面」的清单,本件与 `Input` 共用 `input.css`)。
519
+ *
520
+ * 类名**与面文件同根**(`mh-input*`),口径与 `Textarea` 的 `.mh-input--textarea` 一致:
521
+ * 同族两件共用一份面时,族根就是 `mh-input`,另开 `mh-number-*` 会被
522
+ * `__tests__/contract-class-surface.spec.ts` 的「与面文件名同根」那条判据拦下 —— 那条判据
523
+ * 正是为了让「共用一份面」这件事在命名上看得见。
524
+ *
525
+ * `.mh-input--number` 只声明 `font-variant-numeric`:数值等宽,编辑时数字不会因字形宽度
526
+ * 变化而左右跳(比例数字下 `1111` 比 `0000` 窄一截,逐位输入时值区会抖)。它**不承载几何** ——
527
+ * 框与内距归 `.mh-input__wrap` / `.mh-input`,重复声明就是又开一份会漂的规格。 */
528
+ .mh-input--number {
529
+ font-variant-numeric: tabular-nums;
530
+ }
531
+
532
+ /* 步进位:右端的一对上下箭头(列排、整条居中)。
533
+ * 取值(两条约束一起解出来的):md 16 / 压 4(32 − 4 = 28 ≤ 30)、lg 16 / 压 0(32 ≤ 38)、
534
+ * sm 13 / 压 4(26 − 4 = 22 ≤ 22;13 是「放得下 **且** 压不过字形」的最大值 —— 14 的两条约束互相打架)。
535
+ * 尺寸声明落在**步进位自己**上(基准),两档改写靠表壳的 `:has()` 选到它 —— 档位类挂在控件上
536
+ * (`.mh-input--sm`),而箭头是控件的**兄弟**:自定义属性只向下继承,把变量声明在控件上,
537
+ * 箭头取不到值(与 `input.tokens.css` 头注释记的同一个坑)。 */
538
+ .mh-input__step {
539
+ /* md 基准:箭头盒取**常规图标档 16**(一对 32 比内容高 30 多 2,故两枚之间压 4 → 一对 28 落进 30) */
540
+ --mh-input-step-icon: 16px;
541
+ --mh-input-step-overlap: 4px;
542
+ flex: none;
543
+ display: inline-flex;
544
+ flex-direction: column;
545
+ align-self: stretch;
546
+ justify-content: center;
547
+ /* **默认隐形且不可点**:步进位是「聚焦后调值」的辅助位,常显会让页面上每个数字框的右端
548
+ 都挂着一对箭头(密集表单里那一列箭头比数字本身还抢眼)。
549
+ 为什么用 `opacity` 而不是 `display`:① 它仍在流中**占位** —— 显隐时值区宽度不变,
550
+ 否则聚焦的一瞬间数字会横向跳一格;② `pointer-events: none` 让隐形时的热区一并失效,
551
+ 避免「点了看不见的箭头、值莫名其妙变了」。读屏仍能读到这两个按钮(不丢可达性)。 */
552
+ opacity: 0;
553
+ pointer-events: none;
554
+ transition-property: opacity;
555
+ transition-duration: 0.15s;
556
+ transition-timing-function: ease;
557
+ }
558
+ /* 显形:**控件拿到焦点时**(点箭头本身焦点也不会掉出壳 —— 按钮在壳内,`:focus-within` 照旧成立) */
559
+ .mh-input__wrap:focus-within > .mh-input__step {
560
+ opacity: 1;
561
+ pointer-events: auto;
562
+ }
563
+ /* sm:控件只 24 高(内容高 22),16 的一对(32)放不下、14 也放不下(压到安全上限 4.67 时还有 23.3 > 22)。
564
+ 13 是**两条约束的唯一交集**:一对 26 − 4 = 22(正好落进),压缩 4 ≤ 13/3(不侵可点区)。 */
565
+ .mh-input__wrap:has(> .mh-input--sm) > .mh-input__step {
566
+ --mh-input-step-icon: 13px;
567
+ --mh-input-step-overlap: 4px;
568
+ }
569
+ /* lg:内容高 38 放得下 16 的一对,不必压(上下各留 3,字形离边框 9) */
570
+ .mh-input__wrap:has(> .mh-input--lg) > .mh-input__step {
571
+ --mh-input-step-icon: 16px;
572
+ --mh-input-step-overlap: 0px;
573
+ }
574
+
575
+ /* 步进按钮:**裸按钮必须自带 border / background / padding**(基线 reset 不再代消费方归零,
576
+ 见 `styles/reset.css` 与 `__tests__/contract-reset.spec.ts` 的第三组)。
577
+ 盒宽取附加位档(16)—— 热区比字形大一档;常态同附加位色档,滑过转主色;到界即 `:disabled`(不回绕)。 */
578
+ .mh-input__step-btn {
579
+ flex: none;
580
+ display: inline-flex;
581
+ align-items: center;
582
+ justify-content: center;
583
+ width: var(--ui-input-affix-size);
584
+ min-height: 0;
585
+ padding: 0;
586
+ border: none;
587
+ background: transparent;
588
+ color: var(--ui-base-content-subtle);
589
+ cursor: pointer;
590
+ }
591
+ .mh-input__step-btn:hover:not(:disabled) {
592
+ color: var(--ui-primary);
593
+ }
594
+ .mh-input__step-btn:disabled {
595
+ color: var(--ui-base-content-muted);
596
+ cursor: not-allowed;
597
+ }
598
+ /* 第二枚往上**压一档**:一对箭头(两个常规图标档的盒)比控件内容高还高,靠负 margin 挤进去
599
+ (`gap` 不接受负值,故用 `margin-top` 的负值;量由 `--mh-input-step-overlap` 给,改档只动一个数)。
600
+ ⚠️ 压缩量有上限(**盒 / 3**):chevron 的笔画含描边占盒高 1/3,压过它,上箭头字形的下半截
601
+ 就落进下箭头的可点区 —— 用户点「增」会被判成「减」。 */
602
+ .mh-input__step-btn + .mh-input__step-btn {
603
+ margin-top: calc(-1 * var(--mh-input-step-overlap));
604
+ }
605
+ /* 箭头图标:尺寸随档(见上面的 `--mh-input-step-icon`),**不在组件里写死 `size`** */
606
+ .mh-input__step-btn > svg {
607
+ flex: none;
608
+ width: var(--mh-input-step-icon);
609
+ height: var(--mh-input-step-icon);
610
+ }
611
+
491
612
  /* 附加位里下拉件的面板皮肤(与操作层并列组合:`class="mh-popup mh-input__menu"`) */
492
613
  .mh-input__menu {
493
614
  width: 96px;
@@ -313,6 +313,12 @@
313
313
  color: var(--ui-error);
314
314
  }
315
315
 
316
+ /* 单元格里的编辑控件:**表格面不为它写规则**。
317
+ * 编辑格由调用方在 `#cell` 插槽里自己摆(`Input` / `Select` / `NumberInput`,外层可套 `Form.Item`),
318
+ * 框、内距、错误档、只读档全部沿用 form / input 两族既有口径 —— 表格侧再写一份就是开第二套会漂的规格。
319
+ * 唯一要提醒的是**列宽**:`table-layout: auto` 下「输入框的最小内容宽」会把窄列顶开,
320
+ * 列宽要守得住就开 `layout="fixed"`(列宽由 `<colgroup>` 决定,不再被内容撑破)。 */
321
+
316
322
  /* 状态点(语义四档) */
317
323
  .mh-table__status-dot {
318
324
  display: inline-block;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manohub/ui",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "AIHub 自绘组件层:不依赖 farris 的手写 Vue TSX 组件 + 自带令牌层(Farris 规范视觉 · daisyUI 式语义令牌 · shadcn 式 API 词汇)。",
@@ -35,7 +35,7 @@
35
35
  "vue": "^3.5.0"
36
36
  },
37
37
  "dependencies": {
38
- "@manohub/icon": "^1.0.0"
38
+ "@manohub/icon": "^1.0.1"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^24.0.0",