@akonwi/mica 0.9.0 → 0.10.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/mica.css +260 -12
- package/package.json +1 -1
- package/tabs.d.ts +2 -1
- package/tabs.js +139 -17
- package/types/react.d.ts +3 -1
package/mica.css
CHANGED
|
@@ -847,6 +847,85 @@
|
|
|
847
847
|
);
|
|
848
848
|
}
|
|
849
849
|
|
|
850
|
+
/* --- skeleton: quiet CSS-only loading placeholder ---------------- */
|
|
851
|
+
:where(m-skeleton) {
|
|
852
|
+
display: block;
|
|
853
|
+
flex: none;
|
|
854
|
+
inline-size: var(--skeleton-width, 100%);
|
|
855
|
+
block-size: var(--skeleton-height, 0.75rem);
|
|
856
|
+
overflow: hidden;
|
|
857
|
+
border-radius: var(--skeleton-radius, var(--radius-md));
|
|
858
|
+
background-color: var(--color-surface-raised);
|
|
859
|
+
animation: mica-skeleton-pulse 1.6s ease-in-out infinite alternate;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
@keyframes mica-skeleton-pulse {
|
|
863
|
+
to {
|
|
864
|
+
background-color: var(--color-border);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
@media (prefers-reduced-motion: reduce) {
|
|
869
|
+
:where(m-skeleton) {
|
|
870
|
+
animation: none;
|
|
871
|
+
background-color: var(--color-border);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/* --- stat grid: semantic summaries in a fused frame -------------- */
|
|
876
|
+
:where(dl.stat-grid) {
|
|
877
|
+
display: flex;
|
|
878
|
+
flex-wrap: wrap;
|
|
879
|
+
gap: 1px;
|
|
880
|
+
margin-block: 0;
|
|
881
|
+
padding: 1px;
|
|
882
|
+
overflow: hidden;
|
|
883
|
+
border-radius: var(--radius-md);
|
|
884
|
+
background: var(--color-border);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
:where(dl.stat-grid) > :where(div.stat) {
|
|
888
|
+
display: flex;
|
|
889
|
+
flex-direction: column;
|
|
890
|
+
flex: 1 1 min(100%, 8rem);
|
|
891
|
+
gap: var(--space-sm);
|
|
892
|
+
min-inline-size: 0;
|
|
893
|
+
padding: var(--space-md);
|
|
894
|
+
background: var(--color-surface);
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
:where(dl.stat-grid) > :where(div.stat) > :where(dt) {
|
|
898
|
+
color: var(--color-text-muted);
|
|
899
|
+
font-size: 0.75rem;
|
|
900
|
+
font-weight: 500;
|
|
901
|
+
line-height: 1.4;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
:where(dl.stat-grid) > :where(div.stat) > :where(dd) {
|
|
905
|
+
display: flex;
|
|
906
|
+
flex-direction: column;
|
|
907
|
+
flex: 1 1 auto;
|
|
908
|
+
gap: var(--space-2xs);
|
|
909
|
+
min-inline-size: 0;
|
|
910
|
+
margin-block: 0;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
:where(dl.stat-grid) > :where(div.stat) > :where(dd) > :first-child {
|
|
914
|
+
color: var(--color-text);
|
|
915
|
+
font-size: 1.5rem;
|
|
916
|
+
font-weight: 600;
|
|
917
|
+
font-variant-numeric: tabular-nums;
|
|
918
|
+
line-height: 1.2;
|
|
919
|
+
overflow-wrap: anywhere;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
:where(dl.stat-grid) > :where(div.stat) > :where(dd) > :where(small) {
|
|
923
|
+
margin-block-start: auto;
|
|
924
|
+
color: var(--color-text-muted);
|
|
925
|
+
font-size: 0.75rem;
|
|
926
|
+
line-height: 1.4;
|
|
927
|
+
}
|
|
928
|
+
|
|
850
929
|
/* --- content: tables, lists, code, quotes ------------------------ */
|
|
851
930
|
:where(table) {
|
|
852
931
|
inline-size: 100%;
|
|
@@ -983,6 +1062,71 @@
|
|
|
983
1062
|
background: var(--color-danger);
|
|
984
1063
|
}
|
|
985
1064
|
|
|
1065
|
+
/* --- stepper: framed sequence controls --------------------------- *
|
|
1066
|
+
* Mica owns only the fused layout. Consumers own labels, disabled
|
|
1067
|
+
* state, events, routing, and value changes. The frame deliberately does
|
|
1068
|
+
* not clip overflow: native focus rings must remain fully visible.
|
|
1069
|
+
* ------------------------------------------------------------------ */
|
|
1070
|
+
:where(m-stepper) {
|
|
1071
|
+
display: grid;
|
|
1072
|
+
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
1073
|
+
align-items: stretch;
|
|
1074
|
+
min-inline-size: 0;
|
|
1075
|
+
border: 1px solid var(--color-border-strong);
|
|
1076
|
+
border-radius: var(--radius-md);
|
|
1077
|
+
background: var(--color-surface);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
:where(m-stepper:has(> [data-reset])) {
|
|
1081
|
+
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
:where(m-stepper) > :where(button) {
|
|
1085
|
+
border: 0;
|
|
1086
|
+
border-radius: 0;
|
|
1087
|
+
background: transparent;
|
|
1088
|
+
|
|
1089
|
+
&:focus-visible {
|
|
1090
|
+
z-index: 1;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
:where(m-stepper) > :where(button:first-child) {
|
|
1095
|
+
border-start-start-radius: inherit;
|
|
1096
|
+
border-end-start-radius: inherit;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
:where(m-stepper) > :where(button:last-child) {
|
|
1100
|
+
border-start-end-radius: inherit;
|
|
1101
|
+
border-end-end-radius: inherit;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
:where(m-stepper) > :where([data-step]) {
|
|
1105
|
+
inline-size: var(--control-height);
|
|
1106
|
+
padding-inline: 0;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
:where(m-stepper) > :where(:nth-child(2)) {
|
|
1110
|
+
display: grid;
|
|
1111
|
+
place-items: center;
|
|
1112
|
+
min-inline-size: 0;
|
|
1113
|
+
block-size: var(--control-height);
|
|
1114
|
+
padding-inline: var(--space-sm);
|
|
1115
|
+
overflow: hidden;
|
|
1116
|
+
border-inline: 1px solid var(--color-border);
|
|
1117
|
+
background: var(--color-surface-raised);
|
|
1118
|
+
color: var(--color-text);
|
|
1119
|
+
font-size: 0.875rem;
|
|
1120
|
+
font-weight: 600;
|
|
1121
|
+
text-align: center;
|
|
1122
|
+
text-overflow: ellipsis;
|
|
1123
|
+
white-space: nowrap;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
:where(m-stepper) > :where([data-reset]) {
|
|
1127
|
+
border-inline-start: 1px solid var(--color-border);
|
|
1128
|
+
}
|
|
1129
|
+
|
|
986
1130
|
/* --- dialog: modal + drawer --------------------------------------- *
|
|
987
1131
|
* The browser brings focus trapping, Esc, top layer, ::backdrop, and
|
|
988
1132
|
* form[method=dialog] closing. Invoker commands (commandfor/command)
|
|
@@ -1535,38 +1679,136 @@
|
|
|
1535
1679
|
/* --- tabs: m-tabs + tabs.js (JS-enhanced) ------------------------------ *
|
|
1536
1680
|
* without the module: inert nav, all panels visible in order —
|
|
1537
1681
|
* complete content. with it: tablist semantics + roving focus.
|
|
1538
|
-
*
|
|
1682
|
+
* default: muted track + surface active tab. variant="underline":
|
|
1683
|
+
* quiet rail + accent selection line; behavior stays identical.
|
|
1539
1684
|
* ------------------------------------------------------------------ */
|
|
1540
1685
|
:where(m-tabs) {
|
|
1686
|
+
--m-tabs-rail-gap: var(--space-2xs);
|
|
1687
|
+
--m-tabs-rail-padding: var(--space-2xs);
|
|
1688
|
+
--m-tabs-rail-border: none;
|
|
1689
|
+
--m-tabs-rail-radius: var(--radius-md);
|
|
1690
|
+
--m-tabs-rail-background: var(--neutral-3);
|
|
1691
|
+
--m-tabs-overflow-cover: var(--neutral-3);
|
|
1692
|
+
--m-tabs-overflow-duration: 0.15s;
|
|
1693
|
+
--m-tabs-overflow-marker: color-mix(in oklch, var(--color-text-muted) 70%, transparent);
|
|
1694
|
+
--m-tabs-overflow-start-gutter: 0px;
|
|
1695
|
+
--m-tabs-overflow-end-gutter: 0px;
|
|
1696
|
+
--m-tabs-overflow-start: 0;
|
|
1697
|
+
--m-tabs-overflow-end: 0;
|
|
1698
|
+
--m-tabs-tab-radius: var(--radius-sm);
|
|
1699
|
+
--m-tabs-tab-edge-padding: var(--space-sm);
|
|
1700
|
+
--m-tabs-tab-focus-offset: var(--focus-ring-offset);
|
|
1701
|
+
--m-tabs-tab-selected-background: var(--color-surface);
|
|
1702
|
+
--m-tabs-tab-selected-shadow: 0 1px 2px oklch(0 0 0 / 0.08);
|
|
1541
1703
|
display: block;
|
|
1542
1704
|
}
|
|
1543
1705
|
|
|
1706
|
+
:where(m-tabs[variant="underline"]) {
|
|
1707
|
+
--m-tabs-rail-gap: 0;
|
|
1708
|
+
--m-tabs-rail-padding: 0;
|
|
1709
|
+
--m-tabs-rail-border: 1px solid var(--color-border);
|
|
1710
|
+
--m-tabs-rail-radius: 0;
|
|
1711
|
+
--m-tabs-rail-background: transparent;
|
|
1712
|
+
--m-tabs-overflow-cover: var(--color-surface);
|
|
1713
|
+
--m-tabs-tab-radius: 0;
|
|
1714
|
+
--m-tabs-tab-edge-padding: 0;
|
|
1715
|
+
--m-tabs-tab-focus-offset: -2px;
|
|
1716
|
+
--m-tabs-tab-selected-background: transparent;
|
|
1717
|
+
--m-tabs-tab-selected-shadow: inset 0 -2px var(--color-accent);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1544
1720
|
:where(m-tabs) > :where(nav) {
|
|
1545
1721
|
display: inline-flex;
|
|
1546
|
-
gap: var(--
|
|
1547
|
-
padding: var(--
|
|
1548
|
-
|
|
1549
|
-
border-radius: var(--radius
|
|
1722
|
+
gap: var(--m-tabs-rail-gap);
|
|
1723
|
+
padding: var(--m-tabs-rail-padding);
|
|
1724
|
+
border-block-end: var(--m-tabs-rail-border);
|
|
1725
|
+
border-radius: var(--m-tabs-rail-radius);
|
|
1726
|
+
background: var(--m-tabs-rail-background);
|
|
1550
1727
|
|
|
1551
1728
|
/* Graceful collapse: labels never wrap inside a tab — when the rail
|
|
1552
|
-
is narrower than its tabs it scrolls horizontally instead.
|
|
1553
|
-
|
|
1554
|
-
|
|
1729
|
+
is narrower than its tabs it scrolls horizontally instead. Before
|
|
1730
|
+
enhancement, the native scrollbar remains the overflow affordance.
|
|
1731
|
+
tabs.js hides it, activates sticky direction markers in clear
|
|
1732
|
+
gutters, and reveals a selected tab within those gutters. Scroll
|
|
1733
|
+
chaining stays contained. */
|
|
1555
1734
|
max-inline-size: 100%;
|
|
1556
1735
|
overflow-x: auto;
|
|
1557
1736
|
overscroll-behavior-x: contain;
|
|
1558
1737
|
scrollbar-width: thin;
|
|
1559
1738
|
|
|
1739
|
+
&[role="tablist"] {
|
|
1740
|
+
scrollbar-width: none;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
&::before,
|
|
1744
|
+
&::after {
|
|
1745
|
+
content: "";
|
|
1746
|
+
position: sticky;
|
|
1747
|
+
z-index: 1;
|
|
1748
|
+
align-self: stretch;
|
|
1749
|
+
background-color: var(--m-tabs-overflow-cover);
|
|
1750
|
+
background-position: center;
|
|
1751
|
+
background-size: 6px 10px;
|
|
1752
|
+
background-repeat: no-repeat;
|
|
1753
|
+
pointer-events: none;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
&::before {
|
|
1757
|
+
inset-inline-start: 0;
|
|
1758
|
+
flex: 0 0 var(--space-md);
|
|
1759
|
+
margin-inline-end: calc(var(--m-tabs-overflow-start-gutter) - var(--space-md));
|
|
1760
|
+
opacity: var(--m-tabs-overflow-start);
|
|
1761
|
+
background-image: conic-gradient(from 45deg at left center,
|
|
1762
|
+
var(--m-tabs-overflow-marker) 0 25%, transparent 0);
|
|
1763
|
+
transition:
|
|
1764
|
+
opacity var(--m-tabs-overflow-duration),
|
|
1765
|
+
margin-inline-end var(--m-tabs-overflow-duration);
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
&::after {
|
|
1769
|
+
inset-inline-end: 0;
|
|
1770
|
+
flex: 0 0 var(--space-md);
|
|
1771
|
+
margin-inline-start: calc(var(--m-tabs-overflow-end-gutter) - var(--space-md));
|
|
1772
|
+
opacity: var(--m-tabs-overflow-end);
|
|
1773
|
+
background-image: conic-gradient(from 225deg at right center,
|
|
1774
|
+
var(--m-tabs-overflow-marker) 0 25%, transparent 0);
|
|
1775
|
+
transition:
|
|
1776
|
+
opacity var(--m-tabs-overflow-duration),
|
|
1777
|
+
margin-inline-start var(--m-tabs-overflow-duration);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
&:dir(rtl)::before {
|
|
1781
|
+
background-image: conic-gradient(from 225deg at right center,
|
|
1782
|
+
var(--m-tabs-overflow-marker) 0 25%, transparent 0);
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
&:dir(rtl)::after {
|
|
1786
|
+
background-image: conic-gradient(from 45deg at left center,
|
|
1787
|
+
var(--m-tabs-overflow-marker) 0 25%, transparent 0);
|
|
1788
|
+
}
|
|
1789
|
+
|
|
1560
1790
|
& > :where(button) {
|
|
1561
1791
|
flex-shrink: 0;
|
|
1562
1792
|
white-space: nowrap;
|
|
1563
1793
|
block-size: var(--control-height-sm);
|
|
1564
1794
|
padding-inline: var(--space-sm);
|
|
1565
1795
|
border: 0;
|
|
1566
|
-
border-radius: var(--radius
|
|
1796
|
+
border-radius: var(--m-tabs-tab-radius);
|
|
1567
1797
|
background: transparent;
|
|
1568
1798
|
color: var(--color-text-muted);
|
|
1569
1799
|
|
|
1800
|
+
&:first-of-type {
|
|
1801
|
+
padding-inline-start: var(--m-tabs-tab-edge-padding);
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
&:last-of-type {
|
|
1805
|
+
padding-inline-end: var(--m-tabs-tab-edge-padding);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
&:focus-visible {
|
|
1809
|
+
outline-offset: var(--m-tabs-tab-focus-offset);
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1570
1812
|
/* :not() guards mirror the base button states (house rule) */
|
|
1571
1813
|
&:hover:not(:disabled, [aria-disabled="true"]),
|
|
1572
1814
|
&:active:not(:disabled, [aria-disabled="true"]) {
|
|
@@ -1575,19 +1817,25 @@
|
|
|
1575
1817
|
}
|
|
1576
1818
|
|
|
1577
1819
|
&[aria-selected="true"] {
|
|
1578
|
-
background: var(--
|
|
1820
|
+
background: var(--m-tabs-tab-selected-background);
|
|
1579
1821
|
color: var(--color-text);
|
|
1580
|
-
box-shadow:
|
|
1822
|
+
box-shadow: var(--m-tabs-tab-selected-shadow);
|
|
1581
1823
|
|
|
1582
1824
|
&:hover:not(:disabled, [aria-disabled="true"]),
|
|
1583
1825
|
&:active:not(:disabled, [aria-disabled="true"]) {
|
|
1584
|
-
background: var(--
|
|
1826
|
+
background: var(--m-tabs-tab-selected-background);
|
|
1585
1827
|
color: var(--color-text);
|
|
1586
1828
|
}
|
|
1587
1829
|
}
|
|
1588
1830
|
}
|
|
1589
1831
|
}
|
|
1590
1832
|
|
|
1833
|
+
/* Vendor pseudo gets its own rule: one unknown selector in a list
|
|
1834
|
+
invalidates the whole list. */
|
|
1835
|
+
:where(m-tabs) > :where(nav[role="tablist"])::-webkit-scrollbar {
|
|
1836
|
+
display: none;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1591
1839
|
:where(m-tabs) > :where(section) {
|
|
1592
1840
|
padding-block: var(--space-md);
|
|
1593
1841
|
}
|
package/package.json
CHANGED
package/tabs.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Type declarations for mica/tabs.js — side-effect module.
|
|
2
|
-
* Registers <m-tabs>: roving tabindex, arrow keys, aria-selected wiring
|
|
2
|
+
* Registers <m-tabs>: roving tabindex, arrow keys, aria-selected wiring,
|
|
3
|
+
* selected-tab reveal, and directional overflow markers.
|
|
3
4
|
*/
|
|
4
5
|
declare global {
|
|
5
6
|
interface HTMLElementTagNameMap {
|
package/tabs.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Enhances working light-DOM markup; never renders it. Without this
|
|
4
4
|
* module, the nav buttons are inert and every panel is visible in
|
|
5
5
|
* order — complete, readable content. With it: real tablist semantics,
|
|
6
|
-
* roving tabindex, arrow-key navigation, automatic activation
|
|
6
|
+
* roving tabindex, arrow-key navigation, automatic activation, and
|
|
7
|
+
* selected-tab reveal with position-aware overflow markers.
|
|
7
8
|
*
|
|
8
9
|
* <m-tabs>
|
|
9
10
|
* <nav>
|
|
@@ -23,10 +24,43 @@ let uid = 0;
|
|
|
23
24
|
class MTabs extends HTMLElement {
|
|
24
25
|
#tabs = [];
|
|
25
26
|
#panels = [];
|
|
27
|
+
#nav = null;
|
|
28
|
+
#overflowing = false;
|
|
29
|
+
#resizeObserver = null;
|
|
30
|
+
#revealFrame = null;
|
|
31
|
+
#onScroll = () => this.#syncOverflow();
|
|
32
|
+
#cancelReveal = () => {
|
|
33
|
+
if (this.#revealFrame !== null) cancelAnimationFrame(this.#revealFrame);
|
|
34
|
+
this.#revealFrame = null;
|
|
35
|
+
};
|
|
36
|
+
#onClick = (event) => {
|
|
37
|
+
const tab = event.target instanceof Element ? event.target.closest("button") : null;
|
|
38
|
+
const index = this.#tabs.indexOf(tab);
|
|
39
|
+
if (index === -1) return;
|
|
40
|
+
this.select(index);
|
|
41
|
+
this.#reveal(index);
|
|
42
|
+
};
|
|
43
|
+
#onKeyDown = (event) => {
|
|
44
|
+
const n = this.#tabs.length;
|
|
45
|
+
const current = this.#tabs.findIndex(
|
|
46
|
+
(tab) => tab.getAttribute("aria-selected") === "true",
|
|
47
|
+
);
|
|
48
|
+
let next = null;
|
|
49
|
+
if (event.key === "ArrowRight") next = (current + 1) % n;
|
|
50
|
+
else if (event.key === "ArrowLeft") next = (current - 1 + n) % n;
|
|
51
|
+
else if (event.key === "Home") next = 0;
|
|
52
|
+
else if (event.key === "End") next = n - 1;
|
|
53
|
+
if (next === null) return;
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
this.select(next); // automatic activation, radix-style
|
|
56
|
+
this.#tabs[next].focus({ preventScroll: true });
|
|
57
|
+
this.#reveal(next);
|
|
58
|
+
};
|
|
26
59
|
|
|
27
60
|
connectedCallback() {
|
|
28
61
|
const nav = this.querySelector(":scope > nav");
|
|
29
62
|
if (!nav) return;
|
|
63
|
+
this.#nav = nav;
|
|
30
64
|
this.#tabs = [...nav.querySelectorAll("button")];
|
|
31
65
|
const sections = [...this.querySelectorAll(":scope > section")];
|
|
32
66
|
this.#panels = this.#tabs.map((b, i) => {
|
|
@@ -44,29 +78,117 @@ class MTabs extends HTMLElement {
|
|
|
44
78
|
panel.setAttribute("aria-labelledby", tab.id);
|
|
45
79
|
panel.tabIndex = 0;
|
|
46
80
|
}
|
|
47
|
-
tab.addEventListener("click", () => this.select(i));
|
|
48
81
|
});
|
|
49
82
|
|
|
50
|
-
nav.addEventListener("
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
);
|
|
55
|
-
let next = null;
|
|
56
|
-
if (e.key === "ArrowRight") next = (current + 1) % n;
|
|
57
|
-
else if (e.key === "ArrowLeft") next = (current - 1 + n) % n;
|
|
58
|
-
else if (e.key === "Home") next = 0;
|
|
59
|
-
else if (e.key === "End") next = n - 1;
|
|
60
|
-
if (next === null) return;
|
|
61
|
-
e.preventDefault();
|
|
62
|
-
this.select(next); // automatic activation, radix-style
|
|
63
|
-
this.#tabs[next].focus();
|
|
64
|
-
});
|
|
83
|
+
nav.addEventListener("click", this.#onClick);
|
|
84
|
+
nav.addEventListener("keydown", this.#onKeyDown);
|
|
85
|
+
nav.addEventListener("pointerdown", this.#cancelReveal, { passive: true });
|
|
86
|
+
nav.addEventListener("wheel", this.#cancelReveal, { passive: true });
|
|
65
87
|
|
|
66
88
|
const initial = this.#tabs.findIndex(
|
|
67
89
|
(t) => t.hasAttribute("selected") || t.getAttribute("aria-selected") === "true",
|
|
68
90
|
);
|
|
69
91
|
this.select(initial === -1 ? 0 : initial);
|
|
92
|
+
|
|
93
|
+
nav.addEventListener("scroll", this.#onScroll, { passive: true });
|
|
94
|
+
this.#resizeObserver = new ResizeObserver(() => this.#measureOverflow());
|
|
95
|
+
this.#resizeObserver.observe(nav);
|
|
96
|
+
this.#tabs.forEach((tab) => this.#resizeObserver.observe(tab));
|
|
97
|
+
this.#measureOverflow();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
disconnectedCallback() {
|
|
101
|
+
this.#nav?.removeEventListener("click", this.#onClick);
|
|
102
|
+
this.#nav?.removeEventListener("keydown", this.#onKeyDown);
|
|
103
|
+
this.#nav?.removeEventListener("pointerdown", this.#cancelReveal);
|
|
104
|
+
this.#nav?.removeEventListener("wheel", this.#cancelReveal);
|
|
105
|
+
this.#nav?.removeEventListener("scroll", this.#onScroll);
|
|
106
|
+
this.#resizeObserver?.disconnect();
|
|
107
|
+
this.#cancelReveal();
|
|
108
|
+
for (const property of [
|
|
109
|
+
"--m-tabs-overflow-start-gutter",
|
|
110
|
+
"--m-tabs-overflow-end-gutter",
|
|
111
|
+
"--m-tabs-overflow-start",
|
|
112
|
+
"--m-tabs-overflow-end",
|
|
113
|
+
]) this.#nav?.style.removeProperty(property);
|
|
114
|
+
this.#nav = null;
|
|
115
|
+
this.#resizeObserver = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
#measureOverflow() {
|
|
119
|
+
const nav = this.#nav;
|
|
120
|
+
if (!nav) return;
|
|
121
|
+
const style = getComputedStyle(nav);
|
|
122
|
+
const gap = Number.parseFloat(style.columnGap) || 0;
|
|
123
|
+
const padding = (Number.parseFloat(style.paddingInlineStart) || 0) +
|
|
124
|
+
(Number.parseFloat(style.paddingInlineEnd) || 0);
|
|
125
|
+
const tabsWidth = this.#tabs.reduce(
|
|
126
|
+
(width, tab) => width + tab.getBoundingClientRect().width,
|
|
127
|
+
padding + gap * Math.max(0, this.#tabs.length - 1),
|
|
128
|
+
);
|
|
129
|
+
this.#overflowing = tabsWidth > nav.clientWidth + 1;
|
|
130
|
+
this.#syncOverflow();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#syncOverflow() {
|
|
134
|
+
const nav = this.#nav;
|
|
135
|
+
if (!nav) return;
|
|
136
|
+
const offset = Math.abs(nav.scrollLeft);
|
|
137
|
+
const max = nav.scrollWidth - nav.clientWidth;
|
|
138
|
+
const start = this.#overflowing && offset > 1;
|
|
139
|
+
const end = this.#overflowing && offset < max - 1;
|
|
140
|
+
nav.style.setProperty("--m-tabs-overflow-start", String(start ? 1 : 0));
|
|
141
|
+
nav.style.setProperty("--m-tabs-overflow-end", String(end ? 1 : 0));
|
|
142
|
+
nav.style.setProperty(
|
|
143
|
+
"--m-tabs-overflow-start-gutter",
|
|
144
|
+
start ? "var(--space-md)" : "0px",
|
|
145
|
+
);
|
|
146
|
+
nav.style.setProperty(
|
|
147
|
+
"--m-tabs-overflow-end-gutter",
|
|
148
|
+
end ? "var(--space-md)" : "0px",
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#reveal(index) {
|
|
153
|
+
const nav = this.#nav;
|
|
154
|
+
if (!nav || !this.#tabs[index]) return;
|
|
155
|
+
this.#cancelReveal();
|
|
156
|
+
const durations = getComputedStyle(nav, "::before").transitionDuration.split(",");
|
|
157
|
+
const duration = durations.reduce((longest, value) => {
|
|
158
|
+
const time = Number.parseFloat(value) * (value.trim().endsWith("ms") ? 1 : 1000);
|
|
159
|
+
return Math.max(longest, time);
|
|
160
|
+
}, 0);
|
|
161
|
+
// Follow the moving gutters through their transition, plus two frames
|
|
162
|
+
// for the scroll event that activates the newly exposed marker.
|
|
163
|
+
const settleAt = performance.now() + duration + 34;
|
|
164
|
+
let firstFrame = true;
|
|
165
|
+
const reveal = (now) => {
|
|
166
|
+
const nav = this.#nav;
|
|
167
|
+
const tab = this.#tabs[index];
|
|
168
|
+
if (!nav || !tab) return;
|
|
169
|
+
const navRect = nav.getBoundingClientRect();
|
|
170
|
+
const tabRect = tab.getBoundingClientRect();
|
|
171
|
+
const beforeStyle = getComputedStyle(nav, "::before");
|
|
172
|
+
const afterStyle = getComputedStyle(nav, "::after");
|
|
173
|
+
const before = Math.max(0,
|
|
174
|
+
(Number.parseFloat(beforeStyle.inlineSize) || 0) +
|
|
175
|
+
(Number.parseFloat(beforeStyle.marginInlineEnd) || 0));
|
|
176
|
+
const after = Math.max(0,
|
|
177
|
+
(Number.parseFloat(afterStyle.inlineSize) || 0) +
|
|
178
|
+
(Number.parseFloat(afterStyle.marginInlineStart) || 0));
|
|
179
|
+
const rtl = getComputedStyle(nav).direction === "rtl";
|
|
180
|
+
const left = navRect.left + (rtl ? after : before);
|
|
181
|
+
const right = navRect.right - (rtl ? before : after);
|
|
182
|
+
if (tabRect.left < left) nav.scrollBy({ left: tabRect.left - left });
|
|
183
|
+
else if (tabRect.right > right) nav.scrollBy({ left: tabRect.right - right });
|
|
184
|
+
if (firstFrame) {
|
|
185
|
+
this.#syncOverflow();
|
|
186
|
+
firstFrame = false;
|
|
187
|
+
}
|
|
188
|
+
if (now < settleAt) this.#revealFrame = requestAnimationFrame(reveal);
|
|
189
|
+
else this.#revealFrame = null;
|
|
190
|
+
};
|
|
191
|
+
this.#revealFrame = requestAnimationFrame(reveal);
|
|
70
192
|
}
|
|
71
193
|
|
|
72
194
|
select(index) {
|
package/types/react.d.ts
CHANGED
|
@@ -51,7 +51,8 @@ declare module 'react' {
|
|
|
51
51
|
|
|
52
52
|
// components
|
|
53
53
|
'm-segmented': MicaElement
|
|
54
|
-
'm-
|
|
54
|
+
'm-stepper': MicaElement
|
|
55
|
+
'm-tabs': MicaElement<{ variant?: 'underline' }>
|
|
55
56
|
'm-badge': MicaElement<{
|
|
56
57
|
variant?: 'primary' | 'success' | 'warning' | 'danger'
|
|
57
58
|
count?: boolean
|
|
@@ -60,6 +61,7 @@ declare module 'react' {
|
|
|
60
61
|
blobatar?: string
|
|
61
62
|
contained?: boolean
|
|
62
63
|
}>
|
|
64
|
+
'm-skeleton': MicaElement
|
|
63
65
|
'm-toast': MicaElement<{
|
|
64
66
|
popover?: 'manual'
|
|
65
67
|
variant?: 'success' | 'warning' | 'danger'
|