@xihan-ui/styles 1.0.0 → 1.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.
package/css/table.css CHANGED
@@ -226,8 +226,11 @@
226
226
  }
227
227
 
228
228
  /* —— 三个可叠加的外观开关 —— */
229
- /* 斑马纹:表体偶数行换一层浅底;悬停与选中的底色仍按原规则盖过它 */
230
- [data-scope='table'][data-part='root'][data-striped] [data-scope='table'][data-part='body'] > [data-scope='table'][data-part='row']:nth-of-type(even) {
229
+ /* 斑马纹:表体偶数行换一层浅底。它是最底下那一档,悬停、选中、落点都要盖得过它,
230
+ 所以祖先链与 :nth-of-type 都收进 :where()——特指度落到与基础底同档(0-2-0),
231
+ 只靠排在基础底之后压过它一条。照字面写祖先链是 0-8-0,比悬停与选中都高,
232
+ 斑马表里偶数行会既不响应悬停也显不出选中 */
233
+ :where([data-scope='table'][data-part='root'][data-striped] [data-scope='table'][data-part='body']) > [data-scope='table'][data-part='row']:where(:nth-of-type(even)) {
231
234
  background: var(--xh-table-row-bg-striped, var(--xh-bg-subtle));
232
235
  }
233
236
 
@@ -372,6 +375,294 @@
372
375
  display: none;
373
376
  }
374
377
 
378
+ /* 列宽把手贴在表头格的行尾侧。它压在相邻两列的接缝上,因此往外挪半个身位,
379
+ 两边各留出一点可抓的余量——贴着边只有一两个像素能按到。 */
380
+ [data-scope='table'][data-part='column-resize-trigger'] {
381
+ display: inline-flex;
382
+ flex: none;
383
+ align-self: stretch;
384
+ inline-size: var(--xh-table-resize-width, var(--xh-space-2));
385
+ /* 推到行尾侧。把手在标记里是表头格的最后一个孩子,auto 外边距把它顶到那一头,
386
+ 不必给每个表头格加定位上下文——那条会落到所有既有表格上 */
387
+ margin-inline-start: auto;
388
+ align-items: center;
389
+ justify-content: center;
390
+ cursor: col-resize;
391
+ touch-action: none;
392
+ }
393
+
394
+ [data-scope='table'][data-part='column-resize-trigger'][hidden] {
395
+ display: none;
396
+ }
397
+
398
+ /* 接缝上那条线。平时借表头的分隔色,按住与聚焦时换成品牌色好认 */
399
+ [data-scope='table'][data-part='column-resize-trigger']::after {
400
+ content: '';
401
+ display: block;
402
+ inline-size: var(--xh-table-resize-line, var(--xh-stroke-thin));
403
+ block-size: var(--xh-table-resize-line-length, 60%);
404
+ border-radius: var(--xh-table-resize-radius, var(--xh-shape-pill));
405
+ background: var(--xh-table-resize-fg, var(--xh-border-default));
406
+ }
407
+
408
+ [data-scope='table'][data-part='column-resize-trigger']:hover::after,
409
+ [data-scope='table'][data-part='column-resize-trigger'][data-resizing]::after {
410
+ background: var(--xh-table-resize-fg-active, var(--xh-border-control-focus));
411
+ block-size: 100%;
412
+ }
413
+
414
+ [data-scope='table'][data-part='column-resize-trigger']:focus-visible {
415
+ outline: var(--xh-ring-width) solid var(--xh-ring-focus);
416
+ outline-offset: var(--xh-ring-offset);
417
+ }
418
+
419
+ /* 不可改宽的列不显示把手:留着一条抓不动的线只会让人反复去试 */
420
+ [data-scope='table'][data-part='column-resize-trigger'][data-disabled] {
421
+ display: none;
422
+ }
423
+
424
+ /* 列拖拽把手贴在表头格的行首侧,位置由源码顺序定,不写 auto 外边距:
425
+ 同一个表头格里的改宽把手已经用 margin-inline-start: auto 把自己顶到行尾侧,
426
+ 再添一条 auto,剩余空白会在两条外边距之间平分,两个把手谁都到不了自己那一头。
427
+ 待在行首侧还有一层好处:它的位置与这一列有多宽、格里有没有排序把手都无关 */
428
+ [data-scope='table'][data-part='column-drag-trigger'] {
429
+ display: inline-flex;
430
+ flex: none;
431
+ align-items: center;
432
+ justify-content: center;
433
+ inline-size: var(--xh-table-drag-size, var(--xh-control-indicator-size));
434
+ block-size: var(--xh-table-drag-size, var(--xh-control-indicator-size));
435
+ border-radius: var(--xh-table-trigger-radius, var(--xh-shape-control));
436
+ color: var(--xh-table-drag-fg, var(--xh-fg-subtle));
437
+ cursor: grab;
438
+ /* 关掉这一轴的默认手势:不关的话触屏上手指一划就被系统当成滚动收走 */
439
+ touch-action: none;
440
+ }
441
+
442
+ [data-scope='table'][data-part='column-drag-trigger'][hidden] {
443
+ display: none;
444
+ }
445
+
446
+ /* 抓手是两条竖线:盒子取边框盒,两侧描边就是那两条线,宽高与线宽都走令牌。
447
+ :empty 守卫让作者塞进来的图标整个顶掉它,而不是两份叠在一起 */
448
+ [data-scope='table'][data-part='column-drag-trigger']:empty::after {
449
+ content: '';
450
+ display: block;
451
+ inline-size: var(--xh-table-drag-grip-w, var(--xh-space-1));
452
+ block-size: var(--xh-table-drag-grip-h, var(--xh-space-2));
453
+ border-inline: var(--xh-stroke-thin) solid currentColor;
454
+ }
455
+
456
+ [data-scope='table'][data-part='column-drag-trigger']:not([data-disabled]):hover,
457
+ [data-scope='table'][data-part='column-drag-trigger'][data-dragging] {
458
+ color: var(--xh-table-drag-fg-active, var(--xh-fg-default));
459
+ }
460
+
461
+ [data-scope='table'][data-part='column-drag-trigger'][data-dragging] {
462
+ cursor: grabbing;
463
+ }
464
+
465
+ /* 不可拖的列照样带一个把手,这里保住它的位置、只把它压到禁用色。
466
+ 收成 display: none 是不行的:把手在行首侧,藏掉之后同一行里能拖与不能拖的两列,
467
+ 标题起点会错开一格 */
468
+ [data-scope='table'][data-part='column-drag-trigger'][data-disabled] {
469
+ color: var(--xh-table-drag-fg-disabled, var(--xh-fg-disabled));
470
+ cursor: not-allowed;
471
+ }
472
+
473
+ [data-scope='table'][data-part='column-drag-trigger']:focus-visible {
474
+ outline: var(--xh-ring-width) solid var(--xh-ring-focus);
475
+ outline-offset: var(--xh-ring-offset);
476
+ }
477
+
478
+ /* 落点指示线:拖到哪一列的哪一侧,线就画在那一列的那条边上。列头与表体的格子一起画,
479
+ 线贯穿整张表才看得出这一列要落到哪儿。
480
+ 定位上下文由这条规则自己给:列头与格子平时都不定位,缺了它伪元素会一路上溯到表格根,
481
+ 线就飞到整张表的左上角去。
482
+ 冻结列排除在外:它靠 position: sticky 钉住,在这儿改成 relative 会把吸附顶掉;
483
+ 而冻结列既不可拖也不做落点,排除掉不会漏画任何一条线 */
484
+ [data-scope='table'][data-part='column-header'][data-drop]:not([data-frozen]),
485
+ [data-scope='table'][data-part='cell'][data-drop]:not([data-frozen]) {
486
+ position: relative;
487
+ }
488
+
489
+ /* 线整条画在盒内:列头与格子都收 overflow: hidden,压着边画会被裁掉半条 */
490
+ [data-scope='table'][data-part='column-header'][data-drop]:not([data-frozen])::after,
491
+ [data-scope='table'][data-part='cell'][data-drop]:not([data-frozen])::after {
492
+ content: '';
493
+ position: absolute;
494
+ inset-block: 0;
495
+ inline-size: var(--xh-table-drop-line, var(--xh-stroke-thick));
496
+ background: var(--xh-table-drop-fg, var(--xh-border-control-focus));
497
+ }
498
+
499
+ /* 落在哪一侧走逻辑属性:rtl 下 before 自己就换到右边去了。
500
+ 换成 box-shadow: inset 的话偏移量是物理的,书写方向一反 before 与 after 就调个个儿 */
501
+ [data-scope='table'][data-part='column-header'][data-drop='before']:not([data-frozen])::after,
502
+ [data-scope='table'][data-part='cell'][data-drop='before']:not([data-frozen])::after {
503
+ inset-inline-start: 0;
504
+ }
505
+
506
+ [data-scope='table'][data-part='column-header'][data-drop='after']:not([data-frozen])::after,
507
+ [data-scope='table'][data-part='cell'][data-drop='after']:not([data-frozen])::after {
508
+ inset-inline-end: 0;
509
+ }
510
+
511
+ /* 被拖的那一列整列压暗,让人看得出正在搬的是它。这里只动 opacity 与 user-select,
512
+ 一个 transform 都不写:冻结列是 position: sticky,祖先一有 transform 就掉出吸附。
513
+ 位置的事全交给落点指示线,被拖的列自己原地不动 */
514
+ [data-scope='table'][data-part='column-header'][data-dragging],
515
+ [data-scope='table'][data-part='cell'][data-dragging] {
516
+ opacity: var(--xh-table-dragging-opacity, var(--xh-state-dragging-opacity));
517
+ user-select: none;
518
+ }
519
+
520
+ /* —— 行拖拽排序 —— */
521
+ /* 整行都是拖动源,光标换成抓手告诉人这一行按得动。
522
+ 这条不写 touch-action:整行关掉纵向手势,表格自己就滚不动了;
523
+ 触屏那一路交给下面那块专属把手 */
524
+ [data-scope='table'][data-part='row'][data-row-draggable] {
525
+ cursor: grab;
526
+ }
527
+
528
+ /* 行拖拽把手:行里的一小块专属拖动区,按下即拖。触屏靠它起手,
529
+ 代价只是这一小块地方不再跟着表格滚。整行起手那一路原样保留,把手是叠加的第二个入口。
530
+ touch-action 不在这儿写:连接层按「拖不动就不写」把它写进了内联 style,
531
+ 皮肤再写一遍会盖掉那份条件判断,让不可拖的行也白白让出滚动 */
532
+ [data-scope='table'][data-part='row-drag-trigger'] {
533
+ display: inline-flex;
534
+ flex: none;
535
+ /* 行是 align-items: stretch 的 flex 容器,不自己居中就会被拉成整行高 */
536
+ align-self: center;
537
+ align-items: center;
538
+ justify-content: center;
539
+ inline-size: var(--xh-table-row-drag-size, var(--xh-control-indicator-size));
540
+ block-size: var(--xh-table-row-drag-size, var(--xh-control-indicator-size));
541
+ border-radius: var(--xh-table-trigger-radius, var(--xh-shape-control));
542
+ color: var(--xh-table-row-drag-fg, var(--xh-fg-subtle));
543
+ cursor: grab;
544
+ }
545
+
546
+ [data-scope='table'][data-part='row-drag-trigger'][hidden] {
547
+ display: none;
548
+ }
549
+
550
+ /* 抓手是两条横线:盒子取边框盒,块向两侧的描边就是那两条线,宽高与线宽都走令牌。
551
+ 线的走向与能拖的方向垂直——行沿块向上下排,画横线;列沿行向左右排,那边画的是竖线。
552
+ 两处都用逻辑属性,书写模式一换,线跟着排列轴自己转过去。
553
+ :empty 守卫让作者塞进来的图标整个顶掉它,而不是两份叠在一起 */
554
+ [data-scope='table'][data-part='row-drag-trigger']:empty::after {
555
+ content: '';
556
+ display: block;
557
+ inline-size: var(--xh-table-row-drag-grip-long, var(--xh-space-2));
558
+ block-size: var(--xh-table-row-drag-grip-short, var(--xh-space-1));
559
+ border-block: var(--xh-stroke-thin) solid currentColor;
560
+ }
561
+
562
+ [data-scope='table'][data-part='row-drag-trigger']:not([data-disabled]):hover,
563
+ [data-scope='table'][data-part='row-drag-trigger'][data-dragging] {
564
+ color: var(--xh-table-row-drag-fg-active, var(--xh-fg-default));
565
+ }
566
+
567
+ [data-scope='table'][data-part='row-drag-trigger'][data-dragging] {
568
+ cursor: grabbing;
569
+ }
570
+
571
+ /* 不可拖的行照样带一个把手,这里保住它的位置、只把它压到禁用色。
572
+ 收成 display: none 是不行的:把手占着行首那一格,藏掉之后同一张表里能拖与不能拖的两行,
573
+ 首列内容的起点会错开一格 */
574
+ [data-scope='table'][data-part='row-drag-trigger'][data-disabled] {
575
+ color: var(--xh-table-row-drag-fg-disabled, var(--xh-fg-disabled));
576
+ cursor: not-allowed;
577
+ }
578
+
579
+ [data-scope='table'][data-part='row-drag-trigger']:focus-visible {
580
+ outline: var(--xh-ring-width) solid var(--xh-ring-focus);
581
+ outline-offset: var(--xh-ring-offset);
582
+ }
583
+
584
+ /* 被拖的行整行压暗,与被拖的列同一档透明度。这里只动 opacity、cursor 与 user-select,
585
+ 一个 transform 都不写:斑马纹按 nth-of-type 算、行间线按兄弟选择器算,两者认的都是 DOM 位置,
586
+ 跟手让位会让它们在拖动全程与行错开。位置的事全交给落点指示线 */
587
+ [data-scope='table'][data-part='row'][data-dragging] {
588
+ opacity: var(--xh-table-dragging-opacity, var(--xh-state-dragging-opacity));
589
+ cursor: grabbing;
590
+ user-select: none;
591
+ }
592
+
593
+ /* 详情行与它的数据行是同一块,一起压暗 */
594
+ [data-scope='table'][data-part='expanded-row'][data-dragging] {
595
+ opacity: var(--xh-table-dragging-opacity, var(--xh-state-dragging-opacity));
596
+ }
597
+
598
+ /* 落点指示:插到两行之间画一条横线,落进一行里面画一整圈。两档都靠伪元素画,
599
+ 定位上下文只给带 data-drop 的那一行:缺了它伪元素会一路上溯到表格根,指示飞到整张表的左上角;
600
+ 写到所有行上则是给每一行都多担一份,而落点同一时刻只有一个 */
601
+ [data-scope='table'][data-part='row'][data-drop] {
602
+ position: relative;
603
+ }
604
+
605
+ /* 线贯穿整行宽度,并且要压在单元格之上:行是 flex 容器,横向吸附的格子带着层号,
606
+ 伪元素不抬到它上面就会被盖住。
607
+ 选择器逐值列出前后两档,不写成裸 [data-drop]:换父那一档用的是同一个 ::after,
608
+ 裸写会让它先摊上一条线高的横条,再由下面那块整个改写 */
609
+ [data-scope='table'][data-part='row']:is([data-drop='before'], [data-drop='after'])::after {
610
+ content: '';
611
+ position: absolute;
612
+ z-index: calc(var(--xh-table-sticky-column-layer, 1) + 1);
613
+ inset-inline: 0;
614
+ block-size: var(--xh-table-drop-line, var(--xh-stroke-thick));
615
+ background: var(--xh-table-drop-fg, var(--xh-border-control-focus));
616
+ }
617
+
618
+ /* 线整条画在行内,不压着边。压着边画的话,首行那条上沿线会被表格根的圆角与滚动裁掉半条 */
619
+ [data-scope='table'][data-part='row'][data-drop='before']::after {
620
+ inset-block-start: 0;
621
+ }
622
+
623
+ [data-scope='table'][data-part='row'][data-drop='after']::after {
624
+ inset-block-end: 0;
625
+ }
626
+
627
+ /* 落进这一行里面(这一行成为新的父):整行淡底,与上下沿那条横线一眼分得开。
628
+ 底色打在行上而不是伪元素上,横向吸附的格子取的是 background: inherit,跟着一起换色,
629
+ 横向滚过去时冻结列不会留下一块没上色的补丁。
630
+ 祖先写到 root 与 body 两层,是为把特指度抬到 0-8-0:拖动时指针必然停在落点这一行上、
631
+ 悬停恒真,而悬停那条是 0-6-0、选中那条是 0-3-0,少写一层祖先这块淡底就会被那两档盖掉。
632
+ 禁用的行不接换父落点,:not([data-disabled]) 不会漏画任何一处高亮 */
633
+ [data-scope='table'][data-part='root'] [data-scope='table'][data-part='body'] > [data-scope='table'][data-part='row'][data-drop='inside']:not([data-disabled]) {
634
+ background: var(--xh-table-drop-inside-bg, var(--xh-bg-brand-subtle));
635
+ }
636
+
637
+ /* 换父那一档的描边:与底色分两层画,因为它得压在横向吸附的格子之上——
638
+ 格子带着层号,画在行自己的背景层上会被首尾的冻结列盖掉半圈。
639
+ 零偏移的内投影四边同时着色、不涉及方向,书写模式一换也不用调;
640
+ 整圈画在行内不压着边,与上下沿那条横线同一口径。
641
+ 行上不写 transform:冻结列是 position: sticky,祖先一有 transform 就掉出吸附 */
642
+ [data-scope='table'][data-part='row'][data-drop='inside']::after {
643
+ content: '';
644
+ position: absolute;
645
+ z-index: calc(var(--xh-table-sticky-column-layer, 1) + 1);
646
+ inset: 0;
647
+ box-shadow: inset 0 0 0 var(--xh-table-drop-line, var(--xh-stroke-thick))
648
+ var(--xh-table-drop-fg, var(--xh-border-control-focus));
649
+ /* 这一层盖满整行,让它不接指针,落点判定照旧落到行与格子上 */
650
+ pointer-events: none;
651
+ }
652
+
653
+ /* 活动区域只说给读屏听,不占版面。声明与 shared/visually-hidden.ts 逐条同值。 */
654
+ [data-scope='table'][data-part='live-region'] {
655
+ position: absolute;
656
+ inline-size: 1px;
657
+ block-size: 1px;
658
+ padding: 0;
659
+ margin: -1px;
660
+ overflow: hidden;
661
+ clip-path: inset(50%);
662
+ white-space: nowrap;
663
+ border: 0;
664
+ }
665
+
375
666
  /* 排序方向用箭头字形表出来:aria-sort 只报给读屏,视觉侧也得有对应的东西 */
376
667
  [data-scope='table'][data-part='sort-trigger']::after {
377
668
  color: var(--xh-table-sort-fg, var(--xh-fg-subtle));
package/css/tabs.css CHANGED
@@ -160,11 +160,150 @@
160
160
  outline-offset: var(--xh-ring-offset);
161
161
  }
162
162
 
163
+ /* —— 标签拖拽换位 —— */
164
+ /* 整张标签都是拖动源,光标换成抓手告诉人这张标签按得动。
165
+ 这条不写 touch-action:整张标签关掉手势,标签带自己就滚不动了;
166
+ 触屏那一路交给下面那块专属把手 */
167
+ [data-scope='tabs'][data-part='trigger'][data-draggable] {
168
+ cursor: grab;
169
+ }
170
+
171
+ /* 标签拖拽把手:标签里的一小块专属拖动区,按下即拖。触屏靠它起手,
172
+ 代价只是这一小块地方不再跟着标签带滚。整张标签起手那一路原样保留,把手是叠加的第二个入口。
173
+ touch-action 不在这儿写:连接层按「拖不动就不写」把它写进了内联 style,
174
+ 皮肤再写一遍会盖掉那份条件判断,让不可拖的标签也白白让出滚动 */
175
+ [data-scope='tabs'][data-part='tab-drag-trigger'] {
176
+ display: inline-flex;
177
+ flex: none;
178
+ align-items: center;
179
+ justify-content: center;
180
+ inline-size: var(--xh-tabs-drag-size, var(--xh-control-indicator-size));
181
+ block-size: var(--xh-tabs-drag-size, var(--xh-control-indicator-size));
182
+ border-radius: var(--xh-tabs-drag-radius, var(--xh-shape-control));
183
+ color: var(--xh-tabs-drag-fg, var(--xh-fg-subtle));
184
+ cursor: grab;
185
+ }
186
+
187
+ /* 抓手是两条与排列轴垂直的线:盒子取边框盒,两侧描边就是那两条线,宽高与线宽都走令牌。
188
+ 标签带横排时标签左右排开,画的是两条竖线;竖排那一档在下面换成横线。
189
+ 基础这一档同时兜住 root 上没写方向的情形。
190
+ :empty 守卫让作者塞进来的图标整个顶掉它,而不是两份叠在一起 */
191
+ [data-scope='tabs'][data-part='tab-drag-trigger']:empty::after {
192
+ content: '';
193
+ display: block;
194
+ inline-size: var(--xh-tabs-drag-grip-short, var(--xh-space-1));
195
+ block-size: var(--xh-tabs-drag-grip-long, var(--xh-space-2));
196
+ border-inline: var(--xh-stroke-thin) solid currentColor;
197
+ }
198
+
199
+ /* 竖排的标签上下排开,两条线跟着转九十度:长短对调,描边换到块向两侧,
200
+ 行向那两条得显式清零,否则四边都留着描边,抓手成了一个方框 */
201
+ [data-scope='tabs'][data-part='root'][data-orientation='vertical']
202
+ [data-scope='tabs'][data-part='tab-drag-trigger']:empty::after {
203
+ inline-size: var(--xh-tabs-drag-grip-long, var(--xh-space-2));
204
+ block-size: var(--xh-tabs-drag-grip-short, var(--xh-space-1));
205
+ border-inline: 0;
206
+ border-block: var(--xh-stroke-thin) solid currentColor;
207
+ }
208
+
209
+ [data-scope='tabs'][data-part='tab-drag-trigger']:not([data-disabled]):hover,
210
+ [data-scope='tabs'][data-part='tab-drag-trigger'][data-dragging] {
211
+ color: var(--xh-tabs-drag-fg-active, var(--xh-fg-default));
212
+ }
213
+
214
+ [data-scope='tabs'][data-part='tab-drag-trigger'][data-dragging] {
215
+ cursor: grabbing;
216
+ }
217
+
218
+ /* 不可拖的标签照样带一个把手,这里保住它的位置、只把它压到禁用色。
219
+ 收成 display: none 是不行的:把手占着标签内的头一格,藏掉之后同一条标签带里
220
+ 能拖与不能拖的两张标签,文字起点会错开一格 */
221
+ [data-scope='tabs'][data-part='tab-drag-trigger'][data-disabled] {
222
+ color: var(--xh-tabs-drag-fg-disabled, var(--xh-fg-disabled));
223
+ cursor: not-allowed;
224
+ }
225
+
226
+ [data-scope='tabs'][data-part='tab-drag-trigger']:focus-visible {
227
+ outline: var(--xh-ring-width) solid var(--xh-ring-focus);
228
+ outline-offset: var(--xh-ring-offset);
229
+ }
230
+
231
+ /* 被拖的那张标签压暗,让人看得出正在搬的是它。这里只动 opacity、cursor 与 user-select,
232
+ 一个 transform 都不写:标签原地不动,要落到哪儿全交给落点指示 */
233
+ [data-scope='tabs'][data-part='trigger'][data-dragging] {
234
+ opacity: var(--xh-tabs-dragging-opacity, var(--xh-state-dragging-opacity));
235
+ cursor: grabbing;
236
+ user-select: none;
237
+ }
238
+
239
+ /* 落点指示线:插到哪张标签的哪一侧,线就画在那张标签的那条边上。
240
+ 定位上下文不必在这儿再给:标签的基础规则里已经是 position: relative。
241
+ 线走 ::after:选中态那道指示画的是 box-shadow(card 档的顶边条、segment 档的浮起),
242
+ line 档只换文字色,标签上两个伪元素都空着,落点线不与谁抢。
243
+ 标签自身不收 overflow,线不会被裁掉;仍整条画在盒内不压着边 */
244
+ [data-scope='tabs'][data-part='trigger']:is([data-drop='before'], [data-drop='after'])::after {
245
+ content: '';
246
+ position: absolute;
247
+ background: var(--xh-tabs-drop-fg, var(--xh-border-control-focus));
248
+ }
249
+
250
+ /* 线的方向跟着标签带的排法换:方向写在 root 的 data-orientation 上,从根往下选。
251
+ 横排的标签左右排开,线是一条竖的,贯穿标签的块向、贴在行向的前后沿 */
252
+ [data-scope='tabs'][data-part='root'][data-orientation='horizontal']
253
+ [data-scope='tabs'][data-part='trigger']:is([data-drop='before'], [data-drop='after'])::after {
254
+ inset-block: 0;
255
+ inline-size: var(--xh-tabs-drop-line, var(--xh-stroke-thick));
256
+ }
257
+
258
+ /* 落在前沿还是后沿走逻辑属性:rtl 下 before 自己就换到右边去了。
259
+ 换成 box-shadow 的偏移量则是物理的,写不出逻辑方向 */
260
+ [data-scope='tabs'][data-part='root'][data-orientation='horizontal']
261
+ [data-scope='tabs'][data-part='trigger'][data-drop='before']::after {
262
+ inset-inline-start: 0;
263
+ }
264
+
265
+ [data-scope='tabs'][data-part='root'][data-orientation='horizontal']
266
+ [data-scope='tabs'][data-part='trigger'][data-drop='after']::after {
267
+ inset-inline-end: 0;
268
+ }
269
+
270
+ /* 竖排的标签上下排开,线是一条横的,贯穿标签的行向、贴在块向的前后沿 */
271
+ [data-scope='tabs'][data-part='root'][data-orientation='vertical']
272
+ [data-scope='tabs'][data-part='trigger']:is([data-drop='before'], [data-drop='after'])::after {
273
+ inset-inline: 0;
274
+ block-size: var(--xh-tabs-drop-line, var(--xh-stroke-thick));
275
+ }
276
+
277
+ /* 块向同样走逻辑属性,跟着书写模式走 */
278
+ [data-scope='tabs'][data-part='root'][data-orientation='vertical']
279
+ [data-scope='tabs'][data-part='trigger'][data-drop='before']::after {
280
+ inset-block-start: 0;
281
+ }
282
+
283
+ [data-scope='tabs'][data-part='root'][data-orientation='vertical']
284
+ [data-scope='tabs'][data-part='trigger'][data-drop='after']::after {
285
+ inset-block-end: 0;
286
+ }
287
+
288
+ /* 活动区域只说给读屏听,不占版面。声明与 shared/visually-hidden.ts 逐条同值。 */
289
+ [data-scope='tabs'][data-part='live-region'] {
290
+ position: absolute;
291
+ inline-size: 1px;
292
+ block-size: 1px;
293
+ padding: 0;
294
+ margin: -1px;
295
+ overflow: hidden;
296
+ clip-path: inset(50%);
297
+ white-space: nowrap;
298
+ border: 0;
299
+ }
300
+
163
301
  /* 声明过任何 display 就盖掉了 UA 的 [hidden]{display:none},收起态得自己还回去。
164
302
  写在文件末尾:兜底必须排在该部件所有 display 声明之后 */
165
303
  [data-scope='tabs'][data-part='root'][hidden],
166
304
  [data-scope='tabs'][data-part='list'][hidden],
167
- [data-scope='tabs'][data-part='trigger'][hidden] {
305
+ [data-scope='tabs'][data-part='trigger'][hidden],
306
+ [data-scope='tabs'][data-part='tab-drag-trigger'][hidden] {
168
307
  display: none;
169
308
  }
170
309
  }
package/css/tree.css CHANGED
@@ -330,6 +330,135 @@
330
330
  mask: var(--xh-glyph-mark-check) center / contain no-repeat;
331
331
  }
332
332
 
333
+ /* —— 节点拖拽搬家 —— */
334
+ /* 整行都是拖动源,光标换成抓手告诉人这一行按得动。
335
+ 这条不写 touch-action:整行关掉纵向手势,树容器自己就滚不动了;
336
+ 触屏那一路交给下面那块专属把手 */
337
+ [data-scope='tree'][data-part='item'][data-draggable],
338
+ [data-scope='tree'][data-part='branch-control'][data-draggable] {
339
+ cursor: grab;
340
+ }
341
+
342
+ /* 节点拖拽把手:行里的一小块专属拖动区,按下即拖。触屏靠它起手,
343
+ 代价只是这一小块地方不再跟着树滚。整行起手那一路原样保留,把手是叠加的第二个入口。
344
+ touch-action 不在这儿写:连接层按「拖不动就不写」把它写进了内联 style,
345
+ 皮肤再写一遍会盖掉那份条件判断,让不可拖的节点也白白让出滚动 */
346
+ [data-scope='tree'][data-part='node-drag-trigger'] {
347
+ display: inline-flex;
348
+ flex: none;
349
+ align-items: center;
350
+ justify-content: center;
351
+ inline-size: var(--xh-tree-drag-size, var(--xh-control-indicator-size));
352
+ block-size: var(--xh-tree-drag-size, var(--xh-control-indicator-size));
353
+ border-radius: var(--xh-tree-drag-radius, var(--xh-shape-control));
354
+ color: var(--xh-tree-drag-fg, var(--xh-fg-subtle));
355
+ cursor: grab;
356
+ }
357
+
358
+ [data-scope='tree'][data-part='node-drag-trigger'][hidden] {
359
+ display: none;
360
+ }
361
+
362
+ /* 抓手是两条横线:盒子取边框盒,块向两侧的描边就是那两条线,宽高与线宽都走令牌。
363
+ 线的走向与能拖的方向垂直——节点沿块向上下排,画的就是横线。
364
+ 用逻辑属性写,书写模式一换,线跟着排列轴自己转过去。
365
+ :empty 守卫让作者塞进来的图标整个顶掉它,而不是两份叠在一起 */
366
+ [data-scope='tree'][data-part='node-drag-trigger']:empty::after {
367
+ content: '';
368
+ display: block;
369
+ inline-size: var(--xh-tree-drag-grip-long, var(--xh-space-2));
370
+ block-size: var(--xh-tree-drag-grip-short, var(--xh-space-1));
371
+ border-block: var(--xh-stroke-thin) solid currentColor;
372
+ }
373
+
374
+ [data-scope='tree'][data-part='node-drag-trigger']:not([data-disabled]):hover,
375
+ [data-scope='tree'][data-part='node-drag-trigger'][data-dragging] {
376
+ color: var(--xh-tree-drag-fg-active, var(--xh-fg-default));
377
+ }
378
+
379
+ [data-scope='tree'][data-part='node-drag-trigger'][data-dragging] {
380
+ cursor: grabbing;
381
+ }
382
+
383
+ /* 不可拖的节点照样带一个把手,这里保住它的位置、只把它压到禁用色。
384
+ 收成 display: none 是不行的:把手占着行首那一格,藏掉之后同一层里能拖与不能拖的两个节点,
385
+ 文字起点会错开一格,层级关系跟着读歪 */
386
+ [data-scope='tree'][data-part='node-drag-trigger'][data-disabled] {
387
+ color: var(--xh-tree-drag-fg-disabled, var(--xh-fg-disabled));
388
+ cursor: not-allowed;
389
+ }
390
+
391
+ [data-scope='tree'][data-part='node-drag-trigger']:focus-visible {
392
+ outline: var(--xh-ring-width) solid var(--xh-ring-focus);
393
+ outline-offset: var(--xh-ring-offset);
394
+ }
395
+
396
+ /* 被拖的那一行压暗,让人看得出正在搬的是它。这里只动 opacity、cursor 与 user-select,
397
+ 一个 transform 都不写:行原地不动,要落到哪儿全交给落点指示 */
398
+ [data-scope='tree'][data-part='item'][data-dragging],
399
+ [data-scope='tree'][data-part='branch-control'][data-dragging] {
400
+ opacity: var(--xh-tree-dragging-opacity, var(--xh-state-dragging-opacity));
401
+ cursor: grabbing;
402
+ user-select: none;
403
+ }
404
+
405
+ /* 落点指示线:插到哪一行的哪一侧,横线就画在那一行的那条边上。
406
+ 定位上下文只给要画线的那一行:行平时都不定位,缺了它伪元素会一路上溯到最近的定位祖先,
407
+ 线就画到树外面去;写到所有行上则是给每一行都多担一份,而落点同一时刻只有一个。
408
+ 整块高亮那一档不在此列:它不靠伪元素,用不着这层上下文 */
409
+ [data-scope='tree'][data-part='item']:is([data-drop='before'], [data-drop='after']),
410
+ [data-scope='tree'][data-part='branch-control']:is([data-drop='before'], [data-drop='after']) {
411
+ position: relative;
412
+ }
413
+
414
+ /* 线贯穿整行宽度。行自身不收 overflow,线不会被行裁掉;
415
+ 但仍整条画在行内不压着边——压着边画的话,首行那条上沿线会被树容器的圆角与滚动裁掉半条 */
416
+ [data-scope='tree'][data-part='item']:is([data-drop='before'], [data-drop='after'])::after,
417
+ [data-scope='tree'][data-part='branch-control']:is([data-drop='before'], [data-drop='after'])::after {
418
+ content: '';
419
+ position: absolute;
420
+ inset-inline: 0;
421
+ block-size: var(--xh-tree-drop-line, var(--xh-stroke-thick));
422
+ background: var(--xh-tree-drop-fg, var(--xh-border-control-focus));
423
+ }
424
+
425
+ /* 落在上沿还是下沿走逻辑属性,块向跟着书写模式走。
426
+ 换成 box-shadow 的偏移量则是物理的,写不出逻辑方向 */
427
+ [data-scope='tree'][data-part='item'][data-drop='before']::after,
428
+ [data-scope='tree'][data-part='branch-control'][data-drop='before']::after {
429
+ inset-block-start: 0;
430
+ }
431
+
432
+ [data-scope='tree'][data-part='item'][data-drop='after']::after,
433
+ [data-scope='tree'][data-part='branch-control'][data-drop='after']::after {
434
+ inset-block-end: 0;
435
+ }
436
+
437
+ /* 放进这个节点里面:整行淡底加一圈内描边,与上下沿那条横线一眼分得开。
438
+ 描边走零偏移的内投影,四边同时着色、不涉及方向,也跟着行的圆角走。
439
+ 选择器里那条 :not([data-disabled]) 是把特指度补到与悬停那条齐平,再靠排在它之后压过它:
440
+ 指针本来就停在落点这一行上,少了这一段,落点底色会被悬停那档灰盖掉。
441
+ 禁用的节点从来不做落点,这一段因此不会漏画任何一处高亮 */
442
+ [data-scope='tree'][data-part='item'][data-drop='inside']:not([data-disabled]),
443
+ [data-scope='tree'][data-part='branch-control'][data-drop='inside']:not([data-disabled]) {
444
+ background: var(--xh-tree-drop-inside-bg, var(--xh-bg-brand-subtle));
445
+ box-shadow: inset 0 0 0 var(--xh-tree-drop-line, var(--xh-stroke-thick))
446
+ var(--xh-tree-drop-fg, var(--xh-border-control-focus));
447
+ }
448
+
449
+ /* 活动区域只说给读屏听,不占版面。声明与 shared/visually-hidden.ts 逐条同值。 */
450
+ [data-scope='tree'][data-part='live-region'] {
451
+ position: absolute;
452
+ inline-size: 1px;
453
+ block-size: 1px;
454
+ padding: 0;
455
+ margin: -1px;
456
+ overflow: hidden;
457
+ clip-path: inset(50%);
458
+ white-space: nowrap;
459
+ border: 0;
460
+ }
461
+
333
462
  /* 声明过任何 display 就盖掉了 UA 的 [hidden]{display:none},收起态得自己还回去。
334
463
  写在文件末尾:兜底必须排在该部件所有 display 声明之后 */
335
464
  [data-scope='tree'][data-part='branch-checkbox'][hidden] {
package/index.css CHANGED
@@ -41,6 +41,7 @@
41
41
  @import './css/popselect.css';
42
42
  @import './css/qr-code.css';
43
43
  @import './css/reset.css';
44
+ @import './css/resizable.css';
44
45
  @import './css/result.css';
45
46
  @import './css/segmented.css';
46
47
  @import './css/signature-pad.css';
@@ -108,6 +109,7 @@
108
109
  @import './css/separator.css';
109
110
  @import './css/side-nav.css';
110
111
  @import './css/slider.css';
112
+ @import './css/sortable.css';
111
113
  @import './css/splitter.css';
112
114
  @import './css/steps.css';
113
115
  @import './css/switch.css';