@sovereignfs/ui 0.21.3 → 0.33.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 (130) hide show
  1. package/dist/Button.module.css +75 -8
  2. package/dist/Calendar.module.css +126 -0
  3. package/dist/Card.module.css +4 -2
  4. package/dist/Checkbox.module.css +109 -0
  5. package/dist/CodeTextarea.module.css +53 -0
  6. package/dist/ConfirmDialog.module.css +109 -0
  7. package/dist/DatePicker.module.css +57 -0
  8. package/dist/Dialog.module.css +73 -46
  9. package/dist/DragHandleRow.module.css +57 -0
  10. package/dist/Drawer.module.css +61 -0
  11. package/dist/FormField.module.css +5 -0
  12. package/dist/Icon.module.css +5 -0
  13. package/dist/Input.module.css +10 -0
  14. package/dist/Menu.module.css +82 -0
  15. package/dist/NavTabs.module.css +4 -2
  16. package/dist/OverlayHeader.module.css +98 -0
  17. package/dist/Popover.module.css +31 -1
  18. package/dist/SegmentedControl.module.css +4 -2
  19. package/dist/Select.module.css +9 -0
  20. package/dist/Sheet.module.css +55 -0
  21. package/dist/SplitPane.module.css +85 -0
  22. package/dist/StatusBadge.module.css +59 -0
  23. package/dist/SystemBanner.module.css +4 -2
  24. package/dist/Tabs.module.css +4 -2
  25. package/dist/TagInput.module.css +141 -0
  26. package/dist/Textarea.module.css +39 -0
  27. package/dist/Toast.module.css +7 -5
  28. package/dist/Toggle.module.css +1 -1
  29. package/dist/Tooltip.module.css +6 -1
  30. package/dist/index.d.ts +549 -20
  31. package/dist/index.js +1496 -193
  32. package/dist/tokens/primitives.css +19 -0
  33. package/dist/tokens/semantic.css +19 -0
  34. package/package.json +1 -1
  35. package/src/__tests__/motion.test.tsx +105 -0
  36. package/src/components/Button/Button.module.css +75 -8
  37. package/src/components/Button/Button.stories.tsx +6 -0
  38. package/src/components/Button/Button.tsx +17 -1
  39. package/src/components/Button/__tests__/Button.test.tsx +19 -0
  40. package/src/components/Calendar/Calendar.module.css +126 -0
  41. package/src/components/Calendar/Calendar.tsx +213 -0
  42. package/src/components/Calendar/__tests__/Calendar.test.tsx +98 -0
  43. package/src/components/Calendar/dateUtils.ts +97 -0
  44. package/src/components/Card/Card.module.css +4 -2
  45. package/src/components/Checkbox/Checkbox.module.css +109 -0
  46. package/src/components/Checkbox/Checkbox.tsx +78 -0
  47. package/src/components/CodeTextarea/CodeTextarea.module.css +53 -0
  48. package/src/components/CodeTextarea/CodeTextarea.stories.tsx +77 -0
  49. package/src/components/CodeTextarea/CodeTextarea.tsx +36 -0
  50. package/src/components/ConfirmDialog/ConfirmDialog.module.css +109 -0
  51. package/src/components/ConfirmDialog/ConfirmDialog.tsx +124 -0
  52. package/src/components/ConfirmDialog/__tests__/ConfirmDialog.test.tsx +146 -0
  53. package/src/components/DatePicker/DatePicker.module.css +57 -0
  54. package/src/components/DatePicker/DatePicker.tsx +103 -0
  55. package/src/components/DatePicker/__tests__/DatePicker.test.tsx +89 -0
  56. package/src/components/Dialog/Dialog.module.css +73 -46
  57. package/src/components/Dialog/Dialog.stories.tsx +51 -1
  58. package/src/components/Dialog/Dialog.tsx +86 -22
  59. package/src/components/Dialog/__tests__/Dialog.test.tsx +67 -3
  60. package/src/components/DragHandleRow/DragHandleRow.module.css +57 -0
  61. package/src/components/DragHandleRow/DragHandleRow.tsx +61 -0
  62. package/src/components/Drawer/Drawer.module.css +61 -0
  63. package/src/components/Drawer/Drawer.tsx +117 -13
  64. package/src/components/Drawer/__tests__/Drawer.test.tsx +123 -2
  65. package/src/components/FormField/FormField.module.css +5 -0
  66. package/src/components/FormField/FormField.tsx +36 -11
  67. package/src/components/FormField/__tests__/FormField.test.tsx +83 -0
  68. package/src/components/Icon/Icon.module.css +5 -0
  69. package/src/components/Icon/Icon.stories.tsx +4 -3
  70. package/src/components/Icon/Icon.tsx +1 -1
  71. package/src/components/Icon/icons/calendar.tsx +23 -0
  72. package/src/components/Icon/icons/ellipsis-vertical.tsx +22 -0
  73. package/src/components/Icon/icons/index.ts +8 -4
  74. package/src/components/Icon/icons/sliders-horizontal.tsx +9 -9
  75. package/src/components/Input/Input.module.css +10 -0
  76. package/src/components/Menu/Menu.module.css +82 -0
  77. package/src/components/Menu/Menu.tsx +137 -0
  78. package/src/components/Menu/__tests__/Menu.test.tsx +197 -0
  79. package/src/components/NavTabs/NavTabs.module.css +4 -2
  80. package/src/components/OverlayHeader/OverlayHeader.module.css +98 -0
  81. package/src/components/OverlayHeader/OverlayHeader.tsx +62 -0
  82. package/src/components/OverlayHeader/__tests__/OverlayHeader.test.tsx +51 -0
  83. package/src/components/Popover/Popover.module.css +31 -1
  84. package/src/components/Popover/Popover.stories.tsx +126 -1
  85. package/src/components/Popover/Popover.tsx +140 -7
  86. package/src/components/SegmentedControl/SegmentedControl.module.css +4 -2
  87. package/src/components/Select/Select.module.css +9 -0
  88. package/src/components/Select/Select.tsx +10 -2
  89. package/src/components/Sheet/Sheet.module.css +55 -0
  90. package/src/components/Sheet/Sheet.tsx +151 -0
  91. package/src/components/Sheet/__tests__/Sheet.test.tsx +99 -0
  92. package/src/components/SplitPane/SplitPane.module.css +85 -0
  93. package/src/components/SplitPane/SplitPane.stories.tsx +160 -0
  94. package/src/components/SplitPane/SplitPane.tsx +149 -0
  95. package/src/components/SplitPane/__tests__/SplitPane.test.tsx +51 -0
  96. package/src/components/StatusBadge/StatusBadge.module.css +59 -0
  97. package/src/components/StatusBadge/StatusBadge.stories.tsx +74 -0
  98. package/src/components/StatusBadge/StatusBadge.tsx +65 -0
  99. package/src/components/SystemBanner/SystemBanner.module.css +4 -2
  100. package/src/components/Tabs/Tabs.module.css +4 -2
  101. package/src/components/TagInput/TagInput.module.css +141 -0
  102. package/src/components/TagInput/TagInput.stories.tsx +109 -0
  103. package/src/components/TagInput/TagInput.tsx +174 -0
  104. package/src/components/TagInput/__tests__/TagInput.test.tsx +69 -0
  105. package/src/components/Textarea/Textarea.module.css +39 -0
  106. package/src/components/Textarea/Textarea.tsx +15 -0
  107. package/src/components/Textarea/__tests__/Textarea.test.tsx +29 -0
  108. package/src/components/Toast/Toast.module.css +7 -5
  109. package/src/components/Toggle/Toggle.module.css +1 -1
  110. package/src/components/Tooltip/Tooltip.module.css +6 -1
  111. package/src/hooks/__tests__/useDoubleTap.test.tsx +117 -0
  112. package/src/hooks/__tests__/useIsMobile.test.tsx +80 -0
  113. package/src/hooks/__tests__/useLongPress.test.tsx +158 -0
  114. package/src/hooks/index.ts +4 -0
  115. package/src/hooks/useDoubleTap.ts +91 -0
  116. package/src/hooks/useIsMobile.ts +33 -0
  117. package/src/hooks/useLongPress.ts +185 -0
  118. package/src/index.ts +37 -2
  119. package/src/motion.ts +78 -0
  120. package/src/stories/Checkbox.stories.tsx +45 -0
  121. package/src/stories/DesignSystemOverview.stories.tsx +354 -8
  122. package/src/stories/DragHandleRow.stories.tsx +41 -0
  123. package/src/stories/FormField.stories.tsx +8 -8
  124. package/src/stories/InteractionHooks.stories.tsx +388 -0
  125. package/src/stories/MobilePatterns.stories.tsx +4 -4
  126. package/src/stories/Textarea.stories.tsx +51 -0
  127. package/src/stories/TokenGallery.stories.tsx +87 -1
  128. package/src/tokens/primitives.css +19 -0
  129. package/src/tokens/semantic.css +19 -0
  130. package/src/components/Icon/icons/terminal.tsx +0 -19
@@ -31,6 +31,16 @@
31
31
  padding: var(--sv-space-8);
32
32
  padding-left: calc(var(--sv-dialog-inset-left, 0px) + var(--sv-space-8));
33
33
  background: var(--sv-color-scrim);
34
+ /* Entrance/exit fade. The Dialog component drives this via a two-phase
35
+ mount (see motion.ts) — .scrimOpen is added one frame after mount so this
36
+ transition has somewhere to animate from, and removed (while staying
37
+ mounted) for the exit before the component actually unmounts. */
38
+ opacity: 0;
39
+ transition: opacity var(--sv-motion-duration-base) var(--sv-motion-ease-out);
40
+ }
41
+
42
+ .scrimOpen {
43
+ opacity: 1;
34
44
  }
35
45
 
36
46
  .panel {
@@ -43,6 +53,30 @@
43
53
  border: 1px solid var(--sv-color-border);
44
54
  border-radius: var(--sv-radius-lg);
45
55
  box-shadow: var(--sv-shadow-overlay);
56
+ /* Desktop entrance: fade + gentle scale-up. Same two-phase-mount pattern as
57
+ .scrim above. Mobile overrides this to a slide-up instead (below). */
58
+ opacity: 0;
59
+ transform: scale(0.96);
60
+ transition:
61
+ opacity var(--sv-motion-duration-base) var(--sv-motion-ease-out),
62
+ transform var(--sv-motion-duration-base) var(--sv-motion-ease-out);
63
+ }
64
+
65
+ .panelOpen {
66
+ opacity: 1;
67
+ transform: scale(1);
68
+ }
69
+
70
+ /* prefers-reduced-motion: collapse both fade/scale and slide to near-instant.
71
+ Not literally 0 — a 0ms transition can skip applying entirely in some
72
+ engines, and the JS unmount timer (Dialog.tsx's MOTION_DURATION_MS,
73
+ switched to ~0 via usePrefersReducedMotion) needs a transition to still be
74
+ "present" for the visual state to actually reach its end value in time. */
75
+ @media (prefers-reduced-motion: reduce) {
76
+ .scrim,
77
+ .panel {
78
+ transition-duration: 0.01ms;
79
+ }
46
80
  }
47
81
 
48
82
  .content {
@@ -53,7 +87,7 @@
53
87
  /* Disable elastic overscroll within the dialog: `none` stops both scroll-
54
88
  chaining to the parent and the iOS rubber-band bounce inside the panel.
55
89
  `contain` only prevents chaining — the panel itself still bounces, which
56
- reveals white panel-background between the mobileBar and the sticky tab
90
+ reveals white panel-background between OverlayHeader and the sticky tab
57
91
  strip on iOS PWA. A dialog is a bounded surface where bounce is a visual
58
92
  bug, not a feature. */
59
93
  overscroll-behavior: none;
@@ -100,9 +134,11 @@
100
134
  cursor: pointer;
101
135
  }
102
136
 
103
- .close:hover {
104
- background: var(--sv-color-surface-sunken);
105
- color: var(--sv-color-text-primary);
137
+ @media (hover: hover) {
138
+ .close:hover {
139
+ background: var(--sv-color-surface-sunken);
140
+ color: var(--sv-color-text-primary);
141
+ }
106
142
  }
107
143
 
108
144
  .close:focus-visible {
@@ -110,8 +146,10 @@
110
146
  outline-offset: 2px;
111
147
  }
112
148
 
113
- /* Hidden on desktop; shown only on mobile where it replaces the close button. */
114
- .mobileBar {
149
+ /* Hidden on desktop; shown only on mobile where it replaces the close button.
150
+ * OverlayHeader owns its own background/border/padding — this wrapper class
151
+ * only toggles visibility per breakpoint. */
152
+ .mobileHeader {
115
153
  display: none;
116
154
  }
117
155
 
@@ -122,10 +160,14 @@
122
160
  @media (max-width: 768px) {
123
161
  /* On mobile the overlay feels like a full-page push, not a floating modal.
124
162
  Remove the dim scrim and panel chrome so it's indistinguishable from a
125
- regular page render. router.back() still closes it correctly. */
163
+ regular page render. router.back() still closes it correctly.
164
+ bottom stops the scrim above the footer nav (matching Drawer) instead of
165
+ running the full viewport height — the panel used to extend under the
166
+ footer with only .content's own padding compensating for it. */
126
167
  .scrim {
127
168
  padding: 0;
128
169
  background: transparent;
170
+ bottom: var(--sv-shell-footer-height, 0);
129
171
  }
130
172
 
131
173
  .panel,
@@ -143,54 +185,39 @@
143
185
  background: var(--sv-color-surface);
144
186
  }
145
187
 
146
- /* Desktop close button is replaced by mobileBar on mobile. */
147
- .close {
148
- display: none;
149
- }
150
-
151
- /* Title + close button in one solid row. Being a flex sibling before
152
- .content means it never scrolls — the content area scrolls below it. */
153
- .mobileBar {
154
- display: flex;
155
- align-items: center;
156
- flex-shrink: 0;
157
- padding: var(--sv-space-3) var(--sv-space-4);
158
- background: var(--sv-color-surface);
159
- border-bottom: 1px solid var(--sv-color-border);
188
+ /* Mobile entrance is a slide-up, not the desktop fade+scale — matches "page
189
+ push" framing above. No opacity change (the panel is opaque full-screen
190
+ content, not a floating box), so only transform needs a transition here. */
191
+ .panel {
192
+ opacity: 1;
193
+ transform: translateY(100%);
194
+ transition: transform var(--sv-motion-duration-base) var(--sv-motion-ease-out);
160
195
  }
161
196
 
162
- .mobileBarTitle {
163
- flex: 1;
164
- font-size: var(--sv-font-size-xl);
165
- font-weight: var(--sv-font-weight-semibold);
166
- color: var(--sv-color-text-primary);
197
+ .panelOpen {
198
+ transform: translateY(0);
167
199
  }
168
200
 
169
- .mobileBarClose {
170
- display: inline-flex;
171
- align-items: center;
172
- justify-content: center;
173
- width: var(--sv-space-8);
174
- height: var(--sv-space-8);
175
- flex-shrink: 0;
176
- border: none;
177
- border-radius: var(--sv-radius-md);
178
- background: transparent;
179
- color: var(--sv-color-text-muted);
180
- font-size: var(--sv-font-size-xl);
181
- line-height: 1;
182
- cursor: pointer;
201
+ /* Desktop close button is replaced by OverlayHeader on mobile. */
202
+ .close {
203
+ display: none;
183
204
  }
184
205
 
185
- .mobileBarClose:hover {
186
- background: var(--sv-color-surface-sunken);
187
- color: var(--sv-color-text-primary);
206
+ /* Title + close button in one solid row, via the shared OverlayHeader
207
+ component. Being a flex sibling before .content means it never scrolls —
208
+ the content area scrolls below it. */
209
+ .mobileHeader {
210
+ display: block;
188
211
  }
189
212
 
190
- /* Remove top padding so the sticky plugin header sits flush against the
191
- mobileBar — no gap for scrolling content to bleed through. */
213
+ /* Remove top padding so the sticky plugin header sits flush against
214
+ OverlayHeader — no gap for scrolling content to bleed through.
215
+ padding-bottom no longer adds --sv-shell-footer-height on top of
216
+ --sv-space-6 — the scrim above already stops short of the footer, so
217
+ .content doesn't need to additionally compensate for it here (that used
218
+ to double-count the footer clearance). */
192
219
  .content {
193
220
  padding-top: 0;
194
- padding-bottom: calc(var(--sv-shell-footer-height, 60px) + var(--sv-space-6, 24px));
221
+ padding-bottom: var(--sv-space-6, 24px);
195
222
  }
196
223
  }
@@ -0,0 +1,57 @@
1
+ .row {
2
+ display: flex;
3
+ align-items: flex-start;
4
+ gap: var(--sv-space-1);
5
+ position: relative;
6
+ }
7
+
8
+ /* Hover-reveal only where hover genuinely exists — on touch the handle would
9
+ otherwise never become visible (tap-generated hover doesn't count as a
10
+ sustained hover state, and :not(:hover) is always true with no hover
11
+ capability at all), so it stays visible by default there. */
12
+ @media (hover: hover) {
13
+ .row:not(:hover) .handle,
14
+ .row:not(:focus-within) .handle {
15
+ opacity: 0;
16
+ }
17
+ }
18
+
19
+ .handle {
20
+ flex-shrink: 0;
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ width: 24px;
25
+ height: 24px;
26
+ margin-top: 2px;
27
+ border: none;
28
+ background: none;
29
+ padding: 0;
30
+ border-radius: var(--sv-radius-sm);
31
+ color: var(--sv-color-text-muted);
32
+ cursor: grab;
33
+ opacity: 1;
34
+ transition: opacity 0.1s ease;
35
+ }
36
+
37
+ .handle:focus-visible {
38
+ outline: 2px solid var(--sv-color-focus-ring);
39
+ outline-offset: 2px;
40
+ }
41
+
42
+ .handle:active {
43
+ cursor: grabbing;
44
+ }
45
+
46
+ .icon {
47
+ pointer-events: none;
48
+ }
49
+
50
+ .content {
51
+ flex: 1;
52
+ min-width: 0;
53
+ }
54
+
55
+ .dragging {
56
+ opacity: 0.5;
57
+ }
@@ -14,6 +14,14 @@
14
14
  display: flex;
15
15
  align-items: flex-end;
16
16
  background: var(--sv-color-scrim);
17
+ /* Entrance/exit fade — same two-phase-mount pattern as Dialog (see motion.ts
18
+ and Drawer.tsx). */
19
+ opacity: 0;
20
+ transition: opacity var(--sv-motion-duration-base) var(--sv-motion-ease-out);
21
+ }
22
+
23
+ .scrimOpen {
24
+ opacity: 1;
17
25
  }
18
26
 
19
27
  .panel {
@@ -28,4 +36,57 @@
28
36
  /* Pull content above the home indicator / bottom bar in standalone mode. */
29
37
  padding-bottom: env(safe-area-inset-bottom);
30
38
  outline: none;
39
+ /* Slide up from below the viewport. Always a bottom sheet, so unlike Dialog
40
+ there's no desktop-vs-mobile fork here. */
41
+ transform: translateY(100%);
42
+ transition: transform var(--sv-motion-duration-base) var(--sv-motion-ease-out);
43
+ }
44
+
45
+ .panelOpen {
46
+ transform: translateY(0);
47
+ }
48
+
49
+ /* 'half' snapHeight — a fixed height instead of the default content-sized cap
50
+ * above. Per the design-system's "half screen or less" convention for
51
+ * Drawer; content taller than this belongs in Sheet instead. */
52
+ .panelHalf {
53
+ height: 50dvh;
54
+ max-height: 50dvh;
55
+ }
56
+
57
+ /* See Dialog.module.css's identical rule for why 0.01ms rather than 0. */
58
+ @media (prefers-reduced-motion: reduce) {
59
+ .scrim,
60
+ .panel {
61
+ transition-duration: 0.01ms;
62
+ }
63
+ }
64
+
65
+ /* Grab handle — the sole drag-initiation region for swipe-down-to-dismiss
66
+ * (see Drawer.tsx's doc comment for why not the whole panel). The visual bar
67
+ * (::before) stays the small, established 36x4px size; only the hit area
68
+ * grows to a comfortable touch target. */
69
+ .handle {
70
+ display: flex;
71
+ align-items: center;
72
+ justify-content: center;
73
+ width: 100%;
74
+ height: var(--sv-touch-target-min, 44px);
75
+ flex-shrink: 0;
76
+ cursor: grab;
77
+ /* JS-driven drag — the browser must not treat a press here as a
78
+ scroll/pan gesture that would fight it. */
79
+ touch-action: none;
80
+ }
81
+
82
+ .handle:active {
83
+ cursor: grabbing;
84
+ }
85
+
86
+ .handle::before {
87
+ content: '';
88
+ width: 36px;
89
+ height: 4px;
90
+ border-radius: var(--sv-radius-full);
91
+ background: var(--sv-color-border-strong);
31
92
  }
@@ -2,6 +2,11 @@
2
2
  display: flex;
3
3
  flex-direction: column;
4
4
  gap: var(--sv-space-1);
5
+ /* Every local .field/.fieldGroup class this component replaced set this
6
+ explicitly. Without it, a parent with align-items: flex-start (needed so
7
+ submit buttons don't stretch full-width) shrink-wraps this wrapper to its
8
+ content's intrinsic width instead of filling the available space. */
9
+ width: 100%;
5
10
  }
6
11
 
7
12
  .label {
@@ -6,6 +6,11 @@
6
6
  /* Color follows the surrounding text via currentColor — no token needed. */
7
7
  }
8
8
 
9
+ .xs {
10
+ width: var(--sv-icon-size-xs);
11
+ height: var(--sv-icon-size-xs);
12
+ }
13
+
9
14
  .sm {
10
15
  width: var(--sv-icon-size-sm);
11
16
  height: var(--sv-icon-size-sm);
@@ -14,6 +14,16 @@
14
14
  box-shadow 0.15s ease;
15
15
  }
16
16
 
17
+ /* iOS Safari (tab and standalone PWA) auto-zooms the page whenever a focused
18
+ input's font-size is below 16px. The viewport intentionally allows pinch-zoom
19
+ (no maximum-scale — an accessibility requirement), so the only way to stop the
20
+ focus-zoom is to render the field at >=16px on touch devices. */
21
+ @media (pointer: coarse) {
22
+ .input {
23
+ font-size: var(--sv-font-size-md);
24
+ }
25
+ }
26
+
17
27
  .input::placeholder {
18
28
  color: var(--sv-color-text-muted);
19
29
  }
@@ -0,0 +1,82 @@
1
+ /* Menu — the item list shared by both presentations (Popover panel on
2
+ * desktop, Drawer panel on mobile); the presentational chrome around it is
3
+ * entirely owned by those two components, not duplicated here. */
4
+
5
+ .list {
6
+ list-style: none;
7
+ margin: 0;
8
+ padding: var(--sv-space-1);
9
+ display: flex;
10
+ flex-direction: column;
11
+ }
12
+
13
+ .item {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: var(--sv-space-2);
17
+ width: 100%;
18
+ /* Min touch target (WCAG 2.5.5 / Apple HIG) — this menu renders as a
19
+ Drawer sheet on mobile, so every item must be comfortably tappable. */
20
+ min-height: var(--sv-touch-target-min, 44px);
21
+ padding: var(--sv-space-2) var(--sv-space-3);
22
+ border: none;
23
+ border-radius: var(--sv-radius-sm);
24
+ background: transparent;
25
+ color: var(--sv-color-text-primary);
26
+ font: inherit;
27
+ font-size: var(--sv-font-size-sm);
28
+ text-align: left;
29
+ cursor: pointer;
30
+ }
31
+
32
+ @media (hover: hover) {
33
+ .item:hover:not(:disabled) {
34
+ background: var(--sv-color-surface-sunken);
35
+ }
36
+ }
37
+
38
+ .item:focus-visible {
39
+ outline: 2px solid var(--sv-color-focus-ring);
40
+ outline-offset: -2px;
41
+ }
42
+
43
+ .item:disabled {
44
+ opacity: 0.5;
45
+ cursor: not-allowed;
46
+ }
47
+
48
+ .itemDestructive {
49
+ color: var(--sv-color-error-text);
50
+ }
51
+
52
+ @media (hover: hover) {
53
+ .itemDestructive:hover:not(:disabled) {
54
+ background: var(--sv-color-error-surface);
55
+ }
56
+ }
57
+
58
+ /* Reserved checkmark gutter for a checkable item — sized so unchecked
59
+ siblings in the same group still align with the checked one's icon. */
60
+ .check {
61
+ display: inline-flex;
62
+ flex-shrink: 0;
63
+ width: var(--sv-icon-size-sm);
64
+ height: var(--sv-icon-size-sm);
65
+ align-items: center;
66
+ justify-content: center;
67
+ }
68
+
69
+ .label {
70
+ padding: var(--sv-space-2) var(--sv-space-3) var(--sv-space-1);
71
+ font-size: var(--sv-font-size-label);
72
+ font-weight: var(--sv-font-weight-semibold);
73
+ color: var(--sv-color-text-muted);
74
+ text-transform: uppercase;
75
+ letter-spacing: 0.04em;
76
+ }
77
+
78
+ .separator {
79
+ height: 1px;
80
+ margin: var(--sv-space-1) var(--sv-space-2);
81
+ background: var(--sv-color-border);
82
+ }
@@ -17,8 +17,10 @@
17
17
  transition: color 0.1s ease;
18
18
  }
19
19
 
20
- .link:hover {
21
- color: var(--sv-color-text-primary);
20
+ @media (hover: hover) {
21
+ .link:hover {
22
+ color: var(--sv-color-text-primary);
23
+ }
22
24
  }
23
25
 
24
26
  .link:focus-visible {
@@ -0,0 +1,98 @@
1
+ /* OverlayHeader — shared fixed secondary header for Dialog's mobile mode,
2
+ * Sheet, and Drawer. Not itself pinned via position — the consumer renders it
3
+ * as a flex-shrink: 0 sibling before its own scrolling content region (see
4
+ * the component's own doc comment). */
5
+
6
+ .header {
7
+ flex-shrink: 0;
8
+ background: var(--sv-color-surface);
9
+ border-bottom: 1px solid var(--sv-color-border);
10
+ }
11
+
12
+ .row {
13
+ display: flex;
14
+ align-items: center;
15
+ gap: var(--sv-space-2);
16
+ padding: var(--sv-space-3) var(--sv-space-4);
17
+ }
18
+
19
+ .backButton {
20
+ display: inline-flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ width: var(--sv-touch-target-min, 44px);
24
+ height: var(--sv-touch-target-min, 44px);
25
+ flex-shrink: 0;
26
+ /* Pull the touch-target padding back so the chevron glyph still sits close
27
+ to the title, matching the visual (not hit-area) spacing of .row's own
28
+ gap — otherwise a 44px box reads as a large empty gap before the title. */
29
+ margin-left: calc(-1 * (var(--sv-touch-target-min, 44px) - var(--sv-icon-size-md)) / 2);
30
+ margin-right: calc(
31
+ -1 * (var(--sv-touch-target-min, 44px) - var(--sv-icon-size-md)) / 2 + var(--sv-space-1)
32
+ );
33
+ border: none;
34
+ border-radius: var(--sv-radius-md);
35
+ background: transparent;
36
+ color: var(--sv-color-text-muted);
37
+ cursor: pointer;
38
+ }
39
+
40
+ @media (hover: hover) {
41
+ .backButton:hover {
42
+ background: var(--sv-color-surface-sunken);
43
+ color: var(--sv-color-text-primary);
44
+ }
45
+ }
46
+
47
+ .backButton:focus-visible {
48
+ outline: 2px solid var(--sv-color-focus-ring);
49
+ outline-offset: -2px;
50
+ }
51
+
52
+ .title {
53
+ flex: 1;
54
+ min-width: 0;
55
+ overflow: hidden;
56
+ text-overflow: ellipsis;
57
+ white-space: nowrap;
58
+ font-size: var(--sv-font-size-xl);
59
+ font-weight: var(--sv-font-weight-semibold);
60
+ color: var(--sv-color-text-primary);
61
+ }
62
+
63
+ .action {
64
+ flex-shrink: 0;
65
+ }
66
+
67
+ .closeButton {
68
+ display: inline-flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ width: var(--sv-touch-target-min, 44px);
72
+ height: var(--sv-touch-target-min, 44px);
73
+ flex-shrink: 0;
74
+ /* Pull the touch-target padding back so the × sits flush with the row's own
75
+ edge padding, same reasoning as .backButton above. */
76
+ margin-right: calc(-1 * (var(--sv-touch-target-min, 44px) - var(--sv-icon-size-sm)) / 2);
77
+ border: none;
78
+ border-radius: var(--sv-radius-md);
79
+ background: transparent;
80
+ color: var(--sv-color-text-muted);
81
+ cursor: pointer;
82
+ }
83
+
84
+ @media (hover: hover) {
85
+ .closeButton:hover {
86
+ background: var(--sv-color-surface-sunken);
87
+ color: var(--sv-color-text-primary);
88
+ }
89
+ }
90
+
91
+ .closeButton:focus-visible {
92
+ outline: 2px solid var(--sv-color-focus-ring);
93
+ outline-offset: -2px;
94
+ }
95
+
96
+ .secondRow {
97
+ padding: 0 var(--sv-space-2) var(--sv-space-2);
98
+ }
@@ -11,7 +11,16 @@
11
11
  border: 1px solid var(--sv-color-border);
12
12
  border-radius: var(--sv-radius-2xl);
13
13
  box-shadow: var(--sv-shadow-popover);
14
- overflow: hidden;
14
+ /* auto, not hidden: Popover.tsx applies an inline max-height only when the
15
+ panel is taller than the room actually available on whichever side it
16
+ opens (e.g. a tall calendar on a short mobile viewport, where neither
17
+ above nor below has enough space for the whole thing) — auto lets that
18
+ capped case scroll internally instead of silently clipping content with
19
+ no way to reach it. When no max-height is applied (the common case, panel
20
+ already fits), auto behaves identically to hidden — nothing to scroll,
21
+ content still clips at the rounded corners exactly as before. */
22
+ overflow-x: hidden;
23
+ overflow-y: auto;
15
24
  /* Slide in from slightly above */
16
25
  animation: sv-popover-in 0.12s ease;
17
26
  }
@@ -42,3 +51,24 @@
42
51
  .left {
43
52
  left: 0;
44
53
  }
54
+
55
+ /* Collision flip: opens above the trigger instead of below, when the panel
56
+ wouldn't fit within the viewport underneath it (see Popover.tsx's
57
+ layout-effect measurement). Same slide-in treatment, mirrored to slide up
58
+ from slightly below instead of down from slightly above. */
59
+ .upward {
60
+ top: auto;
61
+ bottom: calc(100% + var(--sv-space-2));
62
+ animation-name: sv-popover-in-upward;
63
+ }
64
+
65
+ @keyframes sv-popover-in-upward {
66
+ from {
67
+ opacity: 0;
68
+ transform: translateY(4px);
69
+ }
70
+ to {
71
+ opacity: 1;
72
+ transform: translateY(0);
73
+ }
74
+ }
@@ -48,6 +48,8 @@
48
48
  color: var(--sv-color-text-muted);
49
49
  }
50
50
 
51
- .inactive:hover {
52
- color: var(--sv-color-text-primary);
51
+ @media (hover: hover) {
52
+ .inactive:hover {
53
+ color: var(--sv-color-text-primary);
54
+ }
53
55
  }
@@ -33,6 +33,15 @@
33
33
  border-radius: var(--sv-radius-sm);
34
34
  }
35
35
 
36
+ /* iOS Safari zooms the page when a <select> under 16px receives focus, exactly
37
+ as it does for text inputs. Lift the default size to >=16px on touch devices;
38
+ the compact `.sm` variant is for dense desktop tables and is left untouched. */
39
+ @media (pointer: coarse) {
40
+ .select {
41
+ font-size: var(--sv-font-size-md);
42
+ }
43
+ }
44
+
36
45
  .select:focus-visible {
37
46
  outline: none;
38
47
  border-color: var(--sv-color-accent);
@@ -0,0 +1,55 @@
1
+ /* Sheet — fills the same rect as a plugin's content area (between the shell's
2
+ * sticky header and footer nav — see --sv-shell-header-height /
3
+ * --sv-shell-footer-height, set by the shell), sliding in from an edge
4
+ * instead of Dialog's centered box or Drawer's partial-height sheet-over-
5
+ * scrim. z-index matches Dialog/Drawer's scrim so it layers consistently
6
+ * with any other overlay. No scrim of its own — see the component doc
7
+ * comment for why. */
8
+
9
+ .panel {
10
+ position: fixed;
11
+ left: 0;
12
+ right: 0;
13
+ top: var(--sv-shell-header-height, 0);
14
+ bottom: var(--sv-shell-footer-height, 0);
15
+ z-index: 100;
16
+ display: flex;
17
+ flex-direction: column;
18
+ background: var(--sv-color-surface);
19
+ outline: none;
20
+ transform: translateY(100%);
21
+ transition: transform var(--sv-motion-duration-base) var(--sv-motion-ease-out);
22
+ }
23
+
24
+ .panelOpen {
25
+ transform: translateY(0);
26
+ }
27
+
28
+ /* Opt-in top variant (slideFrom="top") — a short options/actions menu opened
29
+ * from a header button reads more naturally sliding down from that button's
30
+ * edge than traveling up from off-screen bottom. :not(.panelOpen) is what
31
+ * lets this coexist with .panelOpen above despite equal-looking single-class
32
+ * specificity elsewhere in this file: its (0,2,0) beats .panelOpen's (0,1,0)
33
+ * while closed/entering/closing, and yields to it (transform: translateY(0),
34
+ * same value either direction) once open. */
35
+ .panelFromTop:not(.panelOpen) {
36
+ transform: translateY(-100%);
37
+ }
38
+
39
+ /* See Dialog.module.css's identical rule for why 0.01ms rather than 0. */
40
+ @media (prefers-reduced-motion: reduce) {
41
+ .panel {
42
+ transition-duration: 0.01ms;
43
+ }
44
+ }
45
+
46
+ /* Always the scroll container, with or without OverlayHeader present (see
47
+ * Sheet.tsx). No opinionated padding here — matches Drawer's convention of
48
+ * leaving horizontal/top padding and safe-area-bottom clearance to the
49
+ * content itself, since a Sheet's content shape varies far more than a
50
+ * fixed-size Dialog's. */
51
+ .content {
52
+ flex: 1 1 auto;
53
+ overflow-y: auto;
54
+ overscroll-behavior: contain;
55
+ }