@sovereignfs/ui 0.23.0 → 0.33.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.
Files changed (109) 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 +14 -2
  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 +77 -50
  9. package/dist/DragHandleRow.module.css +9 -3
  10. package/dist/Drawer.module.css +61 -0
  11. package/dist/Icon.module.css +5 -0
  12. package/dist/Menu.module.css +82 -0
  13. package/dist/NavTabs.module.css +4 -2
  14. package/dist/OverlayHeader.module.css +98 -0
  15. package/dist/Popover.module.css +31 -1
  16. package/dist/SegmentedControl.module.css +4 -2
  17. package/dist/Sheet.module.css +55 -0
  18. package/dist/SplitPane.module.css +85 -0
  19. package/dist/StatusBadge.module.css +59 -0
  20. package/dist/SystemBanner.module.css +4 -2
  21. package/dist/Tabs.module.css +4 -2
  22. package/dist/TagInput.module.css +141 -0
  23. package/dist/Toast.module.css +4 -2
  24. package/dist/Tooltip.module.css +6 -1
  25. package/dist/index.d.ts +493 -17
  26. package/dist/index.js +1390 -209
  27. package/dist/tokens/primitives.css +19 -0
  28. package/dist/tokens/semantic.css +13 -0
  29. package/package.json +1 -1
  30. package/src/__tests__/motion.test.tsx +105 -0
  31. package/src/components/Button/Button.module.css +75 -8
  32. package/src/components/Button/Button.stories.tsx +6 -0
  33. package/src/components/Button/Button.tsx +17 -1
  34. package/src/components/Button/__tests__/Button.test.tsx +19 -0
  35. package/src/components/Calendar/Calendar.module.css +126 -0
  36. package/src/components/Calendar/Calendar.tsx +213 -0
  37. package/src/components/Calendar/__tests__/Calendar.test.tsx +98 -0
  38. package/src/components/Calendar/dateUtils.ts +97 -0
  39. package/src/components/Card/Card.module.css +4 -2
  40. package/src/components/Checkbox/Checkbox.module.css +14 -2
  41. package/src/components/Checkbox/Checkbox.tsx +22 -12
  42. package/src/components/CodeTextarea/CodeTextarea.module.css +53 -0
  43. package/src/components/CodeTextarea/CodeTextarea.stories.tsx +77 -0
  44. package/src/components/CodeTextarea/CodeTextarea.tsx +36 -0
  45. package/src/components/ConfirmDialog/ConfirmDialog.module.css +109 -0
  46. package/src/components/ConfirmDialog/ConfirmDialog.tsx +124 -0
  47. package/src/components/ConfirmDialog/__tests__/ConfirmDialog.test.tsx +146 -0
  48. package/src/components/DatePicker/DatePicker.module.css +57 -0
  49. package/src/components/DatePicker/DatePicker.tsx +103 -0
  50. package/src/components/DatePicker/__tests__/DatePicker.test.tsx +89 -0
  51. package/src/components/Dialog/Dialog.module.css +77 -50
  52. package/src/components/Dialog/Dialog.stories.tsx +51 -1
  53. package/src/components/Dialog/Dialog.tsx +86 -22
  54. package/src/components/Dialog/__tests__/Dialog.test.tsx +67 -3
  55. package/src/components/DragHandleRow/DragHandleRow.module.css +9 -3
  56. package/src/components/Drawer/Drawer.module.css +61 -0
  57. package/src/components/Drawer/Drawer.tsx +117 -13
  58. package/src/components/Drawer/__tests__/Drawer.test.tsx +123 -2
  59. package/src/components/Icon/Icon.module.css +5 -0
  60. package/src/components/Icon/Icon.stories.tsx +4 -3
  61. package/src/components/Icon/Icon.tsx +1 -1
  62. package/src/components/Icon/icons/calendar.tsx +23 -0
  63. package/src/components/Icon/icons/ellipsis-vertical.tsx +22 -0
  64. package/src/components/Icon/icons/index.ts +8 -4
  65. package/src/components/Icon/icons/sliders-horizontal.tsx +9 -9
  66. package/src/components/Menu/Menu.module.css +82 -0
  67. package/src/components/Menu/Menu.tsx +137 -0
  68. package/src/components/Menu/__tests__/Menu.test.tsx +197 -0
  69. package/src/components/NavTabs/NavTabs.module.css +4 -2
  70. package/src/components/OverlayHeader/OverlayHeader.module.css +98 -0
  71. package/src/components/OverlayHeader/OverlayHeader.tsx +62 -0
  72. package/src/components/OverlayHeader/__tests__/OverlayHeader.test.tsx +51 -0
  73. package/src/components/Popover/Popover.module.css +31 -1
  74. package/src/components/Popover/Popover.stories.tsx +126 -1
  75. package/src/components/Popover/Popover.tsx +140 -7
  76. package/src/components/SegmentedControl/SegmentedControl.module.css +4 -2
  77. package/src/components/Sheet/Sheet.module.css +55 -0
  78. package/src/components/Sheet/Sheet.tsx +151 -0
  79. package/src/components/Sheet/__tests__/Sheet.test.tsx +99 -0
  80. package/src/components/SplitPane/SplitPane.module.css +85 -0
  81. package/src/components/SplitPane/SplitPane.stories.tsx +160 -0
  82. package/src/components/SplitPane/SplitPane.tsx +149 -0
  83. package/src/components/SplitPane/__tests__/SplitPane.test.tsx +51 -0
  84. package/src/components/StatusBadge/StatusBadge.module.css +59 -0
  85. package/src/components/StatusBadge/StatusBadge.stories.tsx +74 -0
  86. package/src/components/StatusBadge/StatusBadge.tsx +65 -0
  87. package/src/components/SystemBanner/SystemBanner.module.css +4 -2
  88. package/src/components/Tabs/Tabs.module.css +4 -2
  89. package/src/components/TagInput/TagInput.module.css +141 -0
  90. package/src/components/TagInput/TagInput.stories.tsx +109 -0
  91. package/src/components/TagInput/TagInput.tsx +174 -0
  92. package/src/components/TagInput/__tests__/TagInput.test.tsx +69 -0
  93. package/src/components/Toast/Toast.module.css +4 -2
  94. package/src/components/Tooltip/Tooltip.module.css +6 -1
  95. package/src/hooks/__tests__/useDoubleTap.test.tsx +117 -0
  96. package/src/hooks/__tests__/useIsMobile.test.tsx +80 -0
  97. package/src/hooks/__tests__/useLongPress.test.tsx +158 -0
  98. package/src/hooks/index.ts +4 -0
  99. package/src/hooks/useDoubleTap.ts +91 -0
  100. package/src/hooks/useIsMobile.ts +33 -0
  101. package/src/hooks/useLongPress.ts +185 -0
  102. package/src/index.ts +30 -1
  103. package/src/motion.ts +78 -0
  104. package/src/stories/DesignSystemOverview.stories.tsx +292 -4
  105. package/src/stories/InteractionHooks.stories.tsx +388 -0
  106. package/src/stories/TokenGallery.stories.tsx +85 -1
  107. package/src/tokens/primitives.css +19 -0
  108. package/src/tokens/semantic.css +13 -0
  109. package/src/components/Icon/icons/terminal.tsx +0 -19
@@ -9,6 +9,9 @@
9
9
  font-weight: var(--sv-font-weight-medium);
10
10
  line-height: 1;
11
11
  cursor: pointer;
12
+ /* Removes the ~300ms double-tap-to-zoom delay on touch — this is a tap
13
+ target, never a pinch-zoom surface. */
14
+ touch-action: manipulation;
12
15
  transition:
13
16
  background-color 0.15s ease,
14
17
  border-color 0.15s ease,
@@ -25,6 +28,15 @@
25
28
  cursor: not-allowed;
26
29
  }
27
30
 
31
+ /* Touch target: gated to (pointer: coarse) — the *primary* pointer, not
32
+ (any-pointer: coarse) — so a touchscreen laptop with mouse/trackpad as
33
+ primary keeps desktop density (see mobile design-system plan, Part 4 #5). */
34
+ @media (pointer: coarse) {
35
+ .button {
36
+ min-height: var(--sv-touch-target-min, 44px);
37
+ }
38
+ }
39
+
28
40
  /* Sizes */
29
41
  .sm {
30
42
  padding: var(--sv-space-1) var(--sv-space-3);
@@ -37,14 +49,51 @@
37
49
  font-weight: var(--sv-font-weight-semibold);
38
50
  }
39
51
 
52
+ /* Loading spinner — a self-contained token-only spinner rather than reusing
53
+ the Spinner component: Spinner's border colors are semantic-fixed
54
+ (`--sv-color-accent`), which would be invisible against a primary button's
55
+ accent background, and overriding a second CSS module's rule on the same
56
+ element is a cross-module cascade-order gamble (see ConfirmDialog's
57
+ `.destructiveConfirm` comment for the same tradeoff). `currentColor` picks
58
+ up each variant's own text color for free. */
59
+ .spinner {
60
+ display: inline-block;
61
+ width: var(--sv-icon-size-sm);
62
+ height: var(--sv-icon-size-sm);
63
+ flex-shrink: 0;
64
+ border: 2px solid currentColor;
65
+ border-top-color: transparent;
66
+ border-radius: var(--sv-radius-full);
67
+ opacity: 0.7;
68
+ animation: button-spin 0.7s linear infinite;
69
+ }
70
+
71
+ @keyframes button-spin {
72
+ to {
73
+ transform: rotate(360deg);
74
+ }
75
+ }
76
+
77
+ @media (prefers-reduced-motion: reduce) {
78
+ .spinner {
79
+ animation-play-state: paused;
80
+ }
81
+ }
82
+
40
83
  /* Variants */
41
84
  .primary {
42
85
  background-color: var(--sv-color-accent);
43
86
  color: var(--sv-color-text-on-accent);
44
87
  }
45
88
 
46
- .primary:hover:not(:disabled) {
47
- background-color: var(--sv-color-accent-hover);
89
+ @media (hover: hover) {
90
+ .primary:hover:not(:disabled) {
91
+ background-color: var(--sv-color-accent-hover);
92
+ }
93
+ }
94
+
95
+ .primary:active:not(:disabled) {
96
+ background-color: color-mix(in srgb, var(--sv-color-accent) 80%, black);
48
97
  }
49
98
 
50
99
  .secondary {
@@ -53,8 +102,14 @@
53
102
  border-color: var(--sv-color-border-strong);
54
103
  }
55
104
 
56
- .secondary:hover:not(:disabled) {
57
- background-color: var(--sv-color-surface-sunken);
105
+ @media (hover: hover) {
106
+ .secondary:hover:not(:disabled) {
107
+ background-color: var(--sv-color-surface-sunken);
108
+ }
109
+ }
110
+
111
+ .secondary:active:not(:disabled) {
112
+ background-color: color-mix(in srgb, var(--sv-color-surface-sunken) 85%, black);
58
113
  }
59
114
 
60
115
  .ghost {
@@ -62,8 +117,14 @@
62
117
  color: var(--sv-color-text-primary);
63
118
  }
64
119
 
65
- .ghost:hover:not(:disabled) {
66
- background-color: var(--sv-color-surface-sunken);
120
+ @media (hover: hover) {
121
+ .ghost:hover:not(:disabled) {
122
+ background-color: var(--sv-color-surface-sunken);
123
+ }
124
+ }
125
+
126
+ .ghost:active:not(:disabled) {
127
+ background-color: color-mix(in srgb, var(--sv-color-surface-sunken) 85%, black);
67
128
  }
68
129
 
69
130
  .destructive {
@@ -72,6 +133,12 @@
72
133
  border-color: var(--sv-color-error-border);
73
134
  }
74
135
 
75
- .destructive:hover:not(:disabled) {
76
- background-color: var(--sv-color-error-surface);
136
+ @media (hover: hover) {
137
+ .destructive:hover:not(:disabled) {
138
+ background-color: var(--sv-color-error-surface);
139
+ }
140
+ }
141
+
142
+ .destructive:active:not(:disabled) {
143
+ background-color: color-mix(in srgb, var(--sv-color-error-surface) 85%, black);
77
144
  }
@@ -0,0 +1,126 @@
1
+ /* Calendar — month grid. Tokens only. Grid columns are equal-width via
2
+ * `1fr` repeated 7 times rather than a fixed px size, so the component
3
+ * scales cleanly inside both a Popover panel (desktop) and a full-width
4
+ * Drawer sheet (mobile). */
5
+
6
+ .calendar {
7
+ display: flex;
8
+ flex-direction: column;
9
+ gap: var(--sv-space-3);
10
+ font-family: var(--sv-font-family);
11
+ }
12
+
13
+ .header {
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ gap: var(--sv-space-2);
18
+ }
19
+
20
+ .navButton {
21
+ display: inline-flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ width: var(--sv-touch-target-min, 44px);
25
+ height: var(--sv-touch-target-min, 44px);
26
+ flex-shrink: 0;
27
+ border: none;
28
+ border-radius: var(--sv-radius-md);
29
+ background: transparent;
30
+ color: var(--sv-color-text-muted);
31
+ cursor: pointer;
32
+ }
33
+
34
+ @media (hover: hover) {
35
+ .navButton:hover {
36
+ background: var(--sv-color-surface-sunken);
37
+ color: var(--sv-color-text-primary);
38
+ }
39
+ }
40
+
41
+ .navButton:focus-visible {
42
+ outline: 2px solid var(--sv-color-focus-ring);
43
+ outline-offset: -2px;
44
+ }
45
+
46
+ .monthLabel {
47
+ font-size: var(--sv-font-size-sm);
48
+ font-weight: var(--sv-font-weight-semibold);
49
+ color: var(--sv-color-text-primary);
50
+ }
51
+
52
+ .grid {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: var(--sv-space-1);
56
+ }
57
+
58
+ .weekdayRow,
59
+ .week {
60
+ display: grid;
61
+ grid-template-columns: repeat(7, 1fr);
62
+ }
63
+
64
+ .weekday {
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ height: var(--sv-space-6);
69
+ font-size: var(--sv-font-size-xs);
70
+ font-weight: var(--sv-font-weight-medium);
71
+ color: var(--sv-color-text-subtle);
72
+ text-transform: uppercase;
73
+ }
74
+
75
+ .day {
76
+ display: flex;
77
+ align-items: center;
78
+ justify-content: center;
79
+ width: 100%;
80
+ /* Square-ish cell that also clears the touch-target minimum — height
81
+ drives the grid's row size; width fills its 1fr column. */
82
+ min-height: var(--sv-touch-target-min, 44px);
83
+ border: none;
84
+ border-radius: var(--sv-radius-md);
85
+ background: transparent;
86
+ color: var(--sv-color-text-primary);
87
+ font-size: var(--sv-font-size-sm);
88
+ cursor: pointer;
89
+ }
90
+
91
+ @media (hover: hover) {
92
+ .day:hover:not(:disabled) {
93
+ background: var(--sv-color-surface-sunken);
94
+ }
95
+ }
96
+
97
+ .day:focus-visible {
98
+ outline: 2px solid var(--sv-color-focus-ring);
99
+ outline-offset: -2px;
100
+ }
101
+
102
+ .day:disabled {
103
+ color: var(--sv-color-text-subtle);
104
+ cursor: not-allowed;
105
+ }
106
+
107
+ .dayOutside {
108
+ color: var(--sv-color-text-subtle);
109
+ }
110
+
111
+ .dayToday {
112
+ font-weight: var(--sv-font-weight-semibold);
113
+ box-shadow: inset 0 0 0 1px var(--sv-color-border-strong);
114
+ }
115
+
116
+ .daySelected,
117
+ .daySelected:disabled {
118
+ background: var(--sv-color-accent);
119
+ color: var(--sv-color-text-on-accent);
120
+ }
121
+
122
+ @media (hover: hover) {
123
+ .daySelected:hover:not(:disabled) {
124
+ background: var(--sv-color-accent-hover);
125
+ }
126
+ }
@@ -24,8 +24,10 @@
24
24
  transition: border-color 0.15s ease;
25
25
  }
26
26
 
27
- .interactive:hover {
28
- border-color: var(--sv-color-border-strong);
27
+ @media (hover: hover) {
28
+ .interactive:hover {
29
+ border-color: var(--sv-color-border-strong);
30
+ }
29
31
  }
30
32
 
31
33
  .interactive:focus-visible {
@@ -7,10 +7,11 @@
7
7
 
8
8
  /* The visual box */
9
9
  .box {
10
+ --checkbox-box-size: 18px;
10
11
  position: relative;
11
12
  flex-shrink: 0;
12
- width: 18px;
13
- height: 18px;
13
+ width: var(--checkbox-box-size);
14
+ height: var(--checkbox-box-size);
14
15
  border-radius: var(--sv-radius-sm);
15
16
  border: 1.5px solid var(--sv-color-border-strong);
16
17
  background: var(--sv-color-surface);
@@ -43,6 +44,17 @@
43
44
  cursor: not-allowed;
44
45
  }
45
46
 
47
+ /* Expand the tappable region to the platform's minimum touch target without
48
+ growing the visible box — consumers (e.g. task rows) align the box to the
49
+ pixel against header icons, so its visual footprint and layout must stay
50
+ identical. Gated to (pointer: coarse), the *primary* pointer, so a
51
+ touchscreen laptop with mouse/trackpad as primary keeps desktop density. */
52
+ @media (pointer: coarse) {
53
+ .input {
54
+ inset: calc((var(--checkbox-box-size) - var(--sv-touch-target-min, 44px)) / 2);
55
+ }
56
+ }
57
+
46
58
  .input:focus-visible + .tick,
47
59
  .input:focus-visible {
48
60
  outline: none;
@@ -0,0 +1,53 @@
1
+ .textarea {
2
+ display: block;
3
+ width: 100%;
4
+ min-height: 180px;
5
+ max-height: min(70vh, 720px);
6
+ padding: var(--sv-space-3);
7
+ font-family: var(--sv-font-family-mono);
8
+ font-size: var(--sv-font-size-sm);
9
+ line-height: 1.6;
10
+ color: var(--sv-color-text-primary);
11
+ tab-size: 2;
12
+ white-space: pre;
13
+ overflow: auto;
14
+ resize: vertical;
15
+ background: var(--sv-color-surface);
16
+ border: 1px solid var(--sv-color-border-strong);
17
+ border-radius: var(--sv-radius-md);
18
+ transition:
19
+ border-color 0.15s ease,
20
+ box-shadow 0.15s ease;
21
+ }
22
+
23
+ .textarea::placeholder {
24
+ color: var(--sv-color-text-muted);
25
+ }
26
+
27
+ .textarea:focus-visible {
28
+ outline: none;
29
+ border-color: var(--sv-color-accent);
30
+ box-shadow: 0 0 0 2px var(--sv-color-focus-ring);
31
+ }
32
+
33
+ .textarea:read-only {
34
+ color: var(--sv-color-text-muted);
35
+ background: var(--sv-color-surface-sunken);
36
+ border-color: var(--sv-color-border);
37
+ cursor: default;
38
+ }
39
+
40
+ .textarea:disabled {
41
+ opacity: 0.5;
42
+ cursor: not-allowed;
43
+ resize: none;
44
+ }
45
+
46
+ .invalid {
47
+ border-color: var(--sv-color-error-border);
48
+ }
49
+
50
+ .invalid:focus-visible {
51
+ border-color: var(--sv-color-error-border);
52
+ box-shadow: 0 0 0 2px var(--sv-color-error-border);
53
+ }
@@ -0,0 +1,109 @@
1
+ /* ConfirmDialog — built on the native <dialog> element (see the component's
2
+ * own doc comment for why: content-sized, top-layer stacking, ::backdrop for
3
+ * free). Tokens only. */
4
+
5
+ .dialog {
6
+ border: none;
7
+ border-radius: var(--sv-radius-lg);
8
+ background: var(--sv-color-surface);
9
+ box-shadow: var(--sv-shadow-overlay);
10
+ padding: 0;
11
+ width: 22rem;
12
+ max-width: calc(100vw - var(--sv-space-8));
13
+ }
14
+
15
+ .dialog::backdrop {
16
+ background: var(--sv-color-scrim);
17
+ }
18
+
19
+ .body {
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: var(--sv-space-4);
23
+ padding: var(--sv-space-6);
24
+ }
25
+
26
+ .title {
27
+ margin: 0;
28
+ font-size: var(--sv-font-size-lg);
29
+ font-weight: var(--sv-font-weight-semibold);
30
+ color: var(--sv-color-text-primary);
31
+ }
32
+
33
+ .message {
34
+ margin: 0;
35
+ font-size: var(--sv-font-size-sm);
36
+ color: var(--sv-color-text-muted);
37
+ }
38
+
39
+ .error {
40
+ margin: 0;
41
+ font-size: var(--sv-font-size-xs);
42
+ color: var(--sv-color-error-text);
43
+ padding: var(--sv-space-2) var(--sv-space-3);
44
+ border: 1px solid var(--sv-color-error-border);
45
+ border-radius: var(--sv-radius-sm);
46
+ background-color: var(--sv-color-error-surface);
47
+ }
48
+
49
+ .actions {
50
+ display: flex;
51
+ gap: var(--sv-space-3);
52
+ justify-content: flex-end;
53
+ }
54
+
55
+ /* Matches Button's own .button + .md structural rules exactly (padding,
56
+ * radius, font weight, focus-visible ring, disabled state, touch target,
57
+ * hover guard, active/pressed state) so it reads as the same control family
58
+ * — only the color scheme differs. Not built as an override of Button's own
59
+ * CSS module: cross-module class-order in the cascade isn't guaranteed, and
60
+ * an inline-style override would defeat Button's own :hover rule (inline
61
+ * styles always win over an external stylesheet, hover or not). A plain
62
+ * button with its own tokens sidesteps both problems. */
63
+ .destructiveConfirm {
64
+ display: inline-flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ gap: var(--sv-space-2);
68
+ padding: 13px var(--sv-space-4);
69
+ border: 1px solid transparent;
70
+ border-radius: var(--sv-radius-md);
71
+ font-family: var(--sv-font-family);
72
+ font-size: var(--sv-font-size-sm);
73
+ font-weight: var(--sv-font-weight-semibold);
74
+ line-height: 1;
75
+ cursor: pointer;
76
+ touch-action: manipulation;
77
+ background-color: var(--sv-color-error-solid);
78
+ color: var(--sv-color-text-on-error);
79
+ transition: background-color 0.15s ease;
80
+ }
81
+
82
+ @media (pointer: coarse) {
83
+ .destructiveConfirm {
84
+ min-height: var(--sv-touch-target-min, 44px);
85
+ }
86
+ }
87
+
88
+ /* Derived via color-mix from the semantic token, not a raw --sv-red-800
89
+ primitive (the original .dangerButton pattern this replaces did reference
90
+ the primitive directly — fixed here, not carried forward). */
91
+ @media (hover: hover) {
92
+ .destructiveConfirm:hover:not(:disabled) {
93
+ background-color: color-mix(in srgb, var(--sv-color-error-solid) 85%, black);
94
+ }
95
+ }
96
+
97
+ .destructiveConfirm:active:not(:disabled) {
98
+ background-color: color-mix(in srgb, var(--sv-color-error-solid) 80%, black);
99
+ }
100
+
101
+ .destructiveConfirm:focus-visible {
102
+ outline: 2px solid var(--sv-color-focus-ring);
103
+ outline-offset: 2px;
104
+ }
105
+
106
+ .destructiveConfirm:disabled {
107
+ opacity: 0.5;
108
+ cursor: not-allowed;
109
+ }
@@ -0,0 +1,57 @@
1
+ /* DatePicker — trigger styled to match Input's height/border/radius/focus
2
+ * ring for visual consistency as a form field. */
3
+
4
+ .trigger {
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: space-between;
8
+ gap: var(--sv-space-2);
9
+ width: 100%;
10
+ padding: var(--sv-space-2) var(--sv-space-3);
11
+ font-family: var(--sv-font-family);
12
+ font-size: var(--sv-font-size-sm);
13
+ line-height: 1.5;
14
+ color: var(--sv-color-text-primary);
15
+ background-color: var(--sv-color-surface);
16
+ border: 1px solid var(--sv-color-border-strong);
17
+ border-radius: var(--sv-radius-md);
18
+ cursor: pointer;
19
+ transition:
20
+ border-color 0.15s ease,
21
+ box-shadow 0.15s ease;
22
+ }
23
+
24
+ /* Same iOS auto-zoom fix as Input — see its own module.css for why. */
25
+ @media (pointer: coarse) {
26
+ .trigger {
27
+ font-size: var(--sv-font-size-md);
28
+ }
29
+ }
30
+
31
+ .trigger:focus-visible {
32
+ outline: none;
33
+ border-color: var(--sv-color-accent);
34
+ box-shadow: 0 0 0 2px var(--sv-color-focus-ring);
35
+ }
36
+
37
+ .trigger:disabled {
38
+ opacity: 0.5;
39
+ cursor: not-allowed;
40
+ }
41
+
42
+ .triggerValue {
43
+ color: var(--sv-color-text-primary);
44
+ }
45
+
46
+ .triggerPlaceholder {
47
+ color: var(--sv-color-text-muted);
48
+ }
49
+
50
+ .popoverBody {
51
+ padding: var(--sv-space-3);
52
+ }
53
+
54
+ .drawerBody {
55
+ padding: var(--sv-space-4);
56
+ padding-bottom: max(var(--sv-space-4), env(safe-area-inset-bottom));
57
+ }
@@ -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;
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);
149
195
  }
150
196
 
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);
160
- }
161
-
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);
167
- }
168
-
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;
197
+ .panelOpen {
198
+ transform: translateY(0);
183
199
  }
184
200
 
185
- .mobileBarClose:hover {
186
- background: var(--sv-color-surface-sunken);
187
- color: var(--sv-color-text-primary);
201
+ /* Desktop close button is replaced by OverlayHeader on mobile. */
202
+ .close {
203
+ display: none;
188
204
  }
189
205
 
190
- /* Remove top padding so the sticky plugin header sits flush against the
191
- mobileBar no gap for scrolling content to bleed through. */
192
- .content {
193
- padding-top: 0;
194
- padding-bottom: calc(var(--sv-shell-footer-height, 60px) + var(--sv-space-6, 24px));
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
+ .content itself needs no mobile override here any more: it used to zero
210
+ out padding-top so a plugin's hand-rolled sticky header (rendered as
211
+ .content's own first child, right below OverlayHeader) would sit flush
212
+ against it. That case no longer exists — a plugin with a tab strip/
213
+ secondary nav now hands it to OverlayHeader's own secondRow slot
214
+ (useOverlaySecondRow) instead of rendering it inside .content; Account
215
+ and Console both migrated to this (mobile design-system plan, Phase
216
+ C1b). With .content's first child back to being genuine page content, it
217
+ needs the same breathing room from the header that .content's own base
218
+ padding (var(--sv-space-6) on every side) already provides everywhere
219
+ else. */
220
+ .mobileHeader {
221
+ display: block;
195
222
  }
196
223
  }