@fragment-build/ui 3.3.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/components/code-snippet/CodeSnippet.d.ts +9 -0
  2. package/components/code-snippet/CodeSnippet.js +14 -4
  3. package/components/code-snippet/CodeSnippet.js.map +1 -1
  4. package/components/code-snippet/code-snippet.css +18 -0
  5. package/components/segmented-control/SegmentedControl.d.ts +16 -0
  6. package/components/segmented-control/SegmentedControl.js +18 -0
  7. package/components/segmented-control/SegmentedControl.js.map +1 -0
  8. package/components/segmented-control/SegmentedControl.styles.d.ts +46 -0
  9. package/components/segmented-control/SegmentedControl.styles.js +20 -0
  10. package/components/segmented-control/SegmentedControl.styles.js.map +1 -0
  11. package/components/segmented-control/segmented-control.css +41 -0
  12. package/fragments/empty-state/EmptyState.d.ts +12 -0
  13. package/fragments/empty-state/EmptyState.js +12 -0
  14. package/fragments/empty-state/EmptyState.js.map +1 -0
  15. package/fragments/empty-state/EmptyState.styles.d.ts +55 -0
  16. package/fragments/empty-state/EmptyState.styles.js +23 -0
  17. package/fragments/empty-state/EmptyState.styles.js.map +1 -0
  18. package/fragments/empty-state/empty-state.css +39 -0
  19. package/fragments/form/FormCard.d.ts +14 -0
  20. package/fragments/form/FormCard.js +14 -0
  21. package/fragments/form/FormCard.js.map +1 -0
  22. package/fragments/form/FormCard.styles.d.ts +49 -0
  23. package/fragments/form/FormCard.styles.js +23 -0
  24. package/fragments/form/FormCard.styles.js.map +1 -0
  25. package/fragments/form/FormSection.d.ts +3 -1
  26. package/fragments/form/FormSection.js +2 -2
  27. package/fragments/form/FormSection.js.map +1 -1
  28. package/fragments/form/FormSection.styles.d.ts +6 -0
  29. package/fragments/form/FormSection.styles.js +2 -0
  30. package/fragments/form/FormSection.styles.js.map +1 -1
  31. package/fragments/form/form-card.css +42 -0
  32. package/fragments/form/form-section.css +22 -10
  33. package/fragments/form/form.css +1 -1
  34. package/fragments/page/PageHeader.d.ts +7 -2
  35. package/fragments/page/PageHeader.js +3 -3
  36. package/fragments/page/PageHeader.js.map +1 -1
  37. package/fragments/page/PageHeader.styles.d.ts +30 -21
  38. package/fragments/page/PageHeader.styles.js +10 -0
  39. package/fragments/page/PageHeader.styles.js.map +1 -1
  40. package/fragments/page/page-header.css +23 -3
  41. package/fragments/save-bar/SaveBar.d.ts +12 -0
  42. package/fragments/save-bar/SaveBar.js +15 -0
  43. package/fragments/save-bar/SaveBar.js.map +1 -0
  44. package/fragments/save-bar/SaveBar.styles.d.ts +37 -0
  45. package/fragments/save-bar/SaveBar.styles.js +16 -0
  46. package/fragments/save-bar/SaveBar.styles.js.map +1 -0
  47. package/fragments/save-bar/save-bar.css +28 -0
  48. package/fragments/shell/shell.css +8 -4
  49. package/fragments/sidebar/Sidebar.js +4 -1
  50. package/fragments/sidebar/Sidebar.js.map +1 -1
  51. package/fragments/sidebar/sidebar.css +48 -24
  52. package/fragments/stat/StatCard.d.ts +29 -0
  53. package/fragments/stat/StatCard.js +20 -0
  54. package/fragments/stat/StatCard.js.map +1 -0
  55. package/fragments/stat/StatCard.styles.d.ts +85 -0
  56. package/fragments/stat/StatCard.styles.js +26 -0
  57. package/fragments/stat/StatCard.styles.js.map +1 -0
  58. package/fragments/stat/stat-card.css +64 -0
  59. package/fragments/widget/Widget.d.ts +9 -2
  60. package/fragments/widget/Widget.js +4 -3
  61. package/fragments/widget/Widget.js.map +1 -1
  62. package/fragments/widget/Widget.styles.d.ts +51 -0
  63. package/fragments/widget/Widget.styles.js +12 -0
  64. package/fragments/widget/Widget.styles.js.map +1 -1
  65. package/fragments/widget/widget.css +50 -4
  66. package/index.css +142 -38
  67. package/index.d.ts +5 -0
  68. package/index.js +5 -0
  69. package/index.js.map +1 -1
  70. package/package.json +1 -1
  71. package/src/components/code-snippet/code-snippet.css +18 -0
  72. package/src/components/segmented-control/segmented-control.css +41 -0
  73. package/src/fragments/empty-state/empty-state.css +39 -0
  74. package/src/fragments/form/form-card.css +42 -0
  75. package/src/fragments/form/form-section.css +22 -10
  76. package/src/fragments/form/form.css +1 -1
  77. package/src/fragments/page/page-header.css +23 -3
  78. package/src/fragments/save-bar/save-bar.css +28 -0
  79. package/src/fragments/shell/shell.css +8 -4
  80. package/src/fragments/sidebar/sidebar.css +48 -24
  81. package/src/fragments/stat/stat-card.css +64 -0
  82. package/src/fragments/widget/widget.css +50 -4
  83. package/src/index.css +142 -38
package/index.css CHANGED
@@ -1,7 +1,12 @@
1
- @theme {
2
- --text-xs: 13px;
3
- --text-sm: 15px;
4
- }
1
+ /*
2
+ * Type scale.
3
+ *
4
+ * Tailwind's own steps (12 / 14 / 16). Fragment used to pin `text-xs` to 13px and `text-sm` to
5
+ * 15px, which made every HeroUI control one step larger than the component library was drawn for
6
+ * and pushed dashboards towards a consumer-app density. Body copy is set to `text-sm` below, which
7
+ * is the size HeroUI's fields, list boxes and tables already use, so running text and controls now
8
+ * share one size.
9
+ */
5
10
 
6
11
  /*
7
12
  * Fragment surface roles.
@@ -20,39 +25,64 @@
20
25
 
21
26
  @layer base {
22
27
  body {
23
- @apply bg-background text-foreground;
28
+ @apply bg-background text-foreground text-sm antialiased;
24
29
  }
25
30
 
31
+ /*
32
+ * Page title, section, card. A dashboard has many more card titles than page titles, so the
33
+ * steps are tight and the weight carries the hierarchy rather than the size.
34
+ */
26
35
  h1 {
27
- @apply text-3xl font-semibold;
36
+ @apply text-2xl font-semibold tracking-tight;
28
37
  }
29
38
 
30
39
  h2 {
31
- @apply text-2xl font-semibold;
40
+ @apply text-lg font-semibold tracking-tight;
32
41
  }
33
42
 
34
43
  h3 {
35
- @apply text-xl font-semibold;
44
+ @apply text-base font-semibold;
45
+ }
46
+
47
+ h4 {
48
+ @apply text-sm font-semibold;
36
49
  }
37
50
  }
38
51
 
39
52
  @layer components {
53
+ /*
54
+ * Fields keep HeroUI's own height and padding. They get a hairline border instead of the
55
+ * default shadow, so a form reads as a set of crisp boxes on a flat surface.
56
+ */
40
57
  .input, .textarea, .select__trigger {
41
- @apply border border-border px-6 py-4 shadow-none;
58
+ @apply border border-border shadow-none;
59
+ }
60
+
61
+ .input-group, .search-field__group, .number-field__group {
62
+ @apply border border-border shadow-none;
42
63
  }
43
64
 
44
- .input-group, .search-field__group, .number-field__group{
45
- @apply border border-border shadow-none h-auto;
65
+ /*
66
+ * Text that has to fit, in any language. Single-line controls keep one line and end in an
67
+ * ellipsis when a translation is longer than the room it has: HeroUI breaks a Select's value
68
+ * onto a second line instead ("Alle / Oberflächen"), which grows the trigger out of its row.
69
+ * The trigger may shrink, so the ellipsis — not an overflow — is what absorbs the difference.
70
+ */
71
+ .select__trigger {
72
+ @apply min-w-0 items-center;
73
+ }
46
74
 
47
- .input-group__input, .search-field__input, .number-field__input {
48
- @apply px-6 py-4;
49
- }
75
+ .select__value {
76
+ @apply min-w-0 truncate;
77
+ overflow-wrap: normal;
50
78
  }
51
79
 
80
+ .chip {
81
+ @apply max-w-full whitespace-nowrap;
82
+ }
83
+
84
+ /* The thumb keeps HeroUI's accent ring: transparent, only a white knob was left, invisible on a white card. */
52
85
  .slider {
53
- .slider__thumb {
54
- @apply bg-transparent;
55
- }
56
86
  &[data-orientation="horizontal"] {
57
87
  .slider__track {
58
88
  @apply h-1;
@@ -65,33 +95,38 @@
65
95
  }
66
96
  }
67
97
 
68
- .select__indicator {
69
- @apply right-4;
98
+ .select__popover {
99
+ @apply rounded-xl;
70
100
  }
71
101
 
72
- .select__trigger:has(.select__indicator) {
73
- @apply pr-10;
102
+ /*
103
+ * HeroUI sets a modal's body in the muted colour, which suits a one-line confirmation and greys
104
+ * out every heading and label in a dialog that holds a form. Body copy is ink here; a dialog that
105
+ * only says something mutes its own message. The heading gets the weight of a page section.
106
+ */
107
+ .modal__body {
108
+ @apply text-foreground;
74
109
  }
75
110
 
76
- .select__popover {
77
- @apply rounded-xl;
111
+ .modal__heading {
112
+ @apply text-lg font-semibold;
78
113
  }
79
114
 
80
115
  .tabs {
81
116
  &:not(.tabs--secondary) {
82
117
  .tabs__list {
83
- @apply bg-surface p-0;
84
-
85
- border-radius: calc(var(--radius-3xl) + 0.25rem);
118
+ @apply bg-default p-1;
119
+
120
+ border-radius: calc(var(--radius-xl) + 0.25rem);
86
121
  }
87
122
 
88
123
  .tabs__indicator {
89
- @apply shadow-none bg-surface-tertiary;
124
+ @apply bg-surface shadow-sm;
90
125
  }
91
126
 
92
127
  .tabs__tab {
93
128
  &[data-selected="true"] {
94
- @apply text-surface-tertiary-foreground;
129
+ @apply text-surface-foreground;
95
130
  }
96
131
  }
97
132
 
@@ -101,7 +136,7 @@
101
136
  }
102
137
 
103
138
  .tabs__tab {
104
- @apply h-10 px-6 text-xs text-nowrap;
139
+ @apply h-8 px-4 text-sm text-nowrap;
105
140
  }
106
141
 
107
142
  .tabs__list {
@@ -110,25 +145,89 @@
110
145
  }
111
146
  }
112
147
 
148
+ /* Underline tabs size to their label, so a short list sits left instead of spreading out. */
113
149
  &.tabs--secondary {
150
+ .tabs__list[data-orientation="horizontal"] {
151
+ @apply gap-6;
152
+ }
153
+
114
154
  .tabs__tab {
115
- @apply h-12 px-10;
155
+ @apply h-11 w-auto min-w-0 px-0.5 gap-2;
156
+ & svg {
157
+ @apply size-4;
158
+ }
159
+
160
+ /*
161
+ * A static underline for tab lists that render no <Tabs.Indicator>. The animated indicator
162
+ * slides from the previous tab with an inline translate, and when the selection is decided
163
+ * during hydration (a tab read from the URL) its first frame can stick, leaving the line
164
+ * under the wrong tab.
165
+ */
166
+ &[data-selected="true"]:not(:has(.tabs__indicator))::after {
167
+ content: '';
168
+ @apply absolute inset-x-0 bottom-0 h-0.5 bg-accent;
169
+ }
116
170
  }
117
171
  }
118
172
  }
119
173
 
120
- .card {
121
- @apply border border-border shadow-none;
122
- }
174
+ /*
175
+ * The primary table as one flat card: a hairline frame, a header band and row dividers, instead
176
+ * of HeroUI's grey frame with a white card inset in it — which on a tinted page reads as a box
177
+ * in a box. Rows get a visible hover, since the default (`surface` at 40%) vanishes on white.
178
+ */
179
+ .table-root--primary {
180
+ @apply border border-border bg-surface p-0;
181
+
182
+ border-radius: var(--radius-3xl);
183
+ box-shadow: var(--fragment-card-shadow, none);
123
184
 
124
- .button {
125
- &.button--md:not(.button--icon-only) {
126
- @apply h-12 px-6;
185
+ .table__column {
186
+ @apply py-2.5 whitespace-nowrap;
127
187
 
128
- @media (width >= 48rem) {
129
- @apply h-12 px-6;
188
+ &::after {
189
+ content: none;
130
190
  }
131
191
  }
192
+
193
+ .table__header {
194
+ @apply border-b border-separator;
195
+ }
196
+
197
+ .table__body tr td {
198
+ border-radius: 0 !important;
199
+ }
200
+
201
+ .table__cell {
202
+ @apply border-b border-separator;
203
+ }
204
+
205
+ .table__body tr:last-child .table__cell {
206
+ @apply border-b-0;
207
+ }
208
+
209
+ .table__row {
210
+ @media (hover: hover) {
211
+ &:hover .table__cell,
212
+ &[data-hovered="true"] .table__cell {
213
+ @apply bg-surface-secondary;
214
+ }
215
+ }
216
+ }
217
+
218
+ .table__footer {
219
+ @apply border-t border-separator;
220
+ }
221
+ }
222
+
223
+ /*
224
+ * Cards sit apart from the page by a hairline and an optional, very soft lift. The lift is a
225
+ * token so a theme on a white page (where the border is all there is) can turn it off.
226
+ */
227
+ .card {
228
+ @apply border border-border;
229
+
230
+ box-shadow: var(--fragment-card-shadow, none);
132
231
  }
133
232
  }
134
233
 
@@ -139,10 +238,15 @@
139
238
  @import "./fragments/widget/widget.css";
140
239
  @import "./components/grid/grid.css";
141
240
  @import "./components/multi-switch/multi-switch.css";
241
+ @import "./components/segmented-control/segmented-control.css";
142
242
  @import "./components/code/code.css";
143
243
  @import "./components/code-snippet/code-snippet.css";
144
244
  @import "./fragments/form/form.css";
145
245
  @import "./fragments/form/form-section.css";
246
+ @import "./fragments/form/form-card.css";
247
+ @import "./fragments/save-bar/save-bar.css";
248
+ @import "./fragments/stat/stat-card.css";
249
+ @import "./fragments/empty-state/empty-state.css";
146
250
 
147
251
  .light,
148
252
  .default,
package/index.d.ts CHANGED
@@ -4,10 +4,15 @@ export { CodeSnippet, type CodeSnippetProps } from './components/code-snippet/Co
4
4
  export { Grid, type GridProps } from './components/grid/Grid';
5
5
  export { MultiSwitch, type MultiSwitchProps } from './components/multi-switch/MultiSwitch';
6
6
  export { ThemeSwitch, type ThemeSwitchProps } from './components/theme-switch/ThemeSwitch';
7
+ export { SegmentedControl, type SegmentedControlProps, type SegmentedControlOption, } from './components/segmented-control/SegmentedControl';
7
8
  export { Form, type FormProps } from './fragments/form/Form';
8
9
  export { FormSection, type FormSectionProps } from './fragments/form/FormSection';
10
+ export { FormCard, type FormCardProps } from './fragments/form/FormCard';
11
+ export { SaveBar, type SaveBarProps } from './fragments/save-bar/SaveBar';
9
12
  export { PageHeader, type PageHeaderProps } from './fragments/page/PageHeader';
10
13
  export { Navbar, type NavbarProps } from './fragments/navbar/Navbar';
11
14
  export { Shell, type ShellProps } from './fragments/shell/Shell';
12
15
  export { Sidebar, type SidebarProps } from './fragments/sidebar/Sidebar';
13
16
  export { Widget, type WidgetProps } from './fragments/widget/Widget';
17
+ export { StatCard, type StatCardProps, type StatCardDelta } from './fragments/stat/StatCard';
18
+ export { EmptyState, type EmptyStateProps } from './fragments/empty-state/EmptyState';
package/index.js CHANGED
@@ -6,11 +6,16 @@ export { CodeSnippet } from './components/code-snippet/CodeSnippet';
6
6
  export { Grid } from './components/grid/Grid';
7
7
  export { MultiSwitch } from './components/multi-switch/MultiSwitch';
8
8
  export { ThemeSwitch } from './components/theme-switch/ThemeSwitch';
9
+ export { SegmentedControl, } from './components/segmented-control/SegmentedControl';
9
10
  export { Form } from './fragments/form/Form';
10
11
  export { FormSection } from './fragments/form/FormSection';
12
+ export { FormCard } from './fragments/form/FormCard';
13
+ export { SaveBar } from './fragments/save-bar/SaveBar';
11
14
  export { PageHeader } from './fragments/page/PageHeader';
12
15
  export { Navbar } from './fragments/navbar/Navbar';
13
16
  export { Shell } from './fragments/shell/Shell';
14
17
  export { Sidebar } from './fragments/sidebar/Sidebar';
15
18
  export { Widget } from './fragments/widget/Widget';
19
+ export { StatCard } from './fragments/stat/StatCard';
20
+ export { EmptyState } from './fragments/empty-state/EmptyState';
16
21
  //# sourceMappingURL=index.js.map
package/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD,sBAAsB;AACtB,OAAO,EAAE,IAAI,EAAkB,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAyB,MAAM,uCAAuC,CAAC;AAC3F,OAAO,EAAE,IAAI,EAAkB,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAyB,MAAM,uCAAuC,CAAC;AAC3F,OAAO,EAAE,WAAW,EAAyB,MAAO,uCAAuC,CAAC;AAE5F,OAAO,EAAE,IAAI,EAAkB,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAyB,MAAM,8BAA8B,CAAC;AAClF,OAAO,EAAE,UAAU,EAAwB,MAAM,6BAA6B,CAAC;AAC/E,OAAO,EAAE,MAAM,EAAoB,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,KAAK,EAAmB,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,OAAO,EAAqB,MAAM,6BAA6B,CAAC;AACzE,OAAO,EAAE,MAAM,EAAoB,MAAM,2BAA2B,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["src/index.ts"],"names":[],"mappings":"AAAA,2BAA2B;AAC3B,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD,sBAAsB;AACtB,OAAO,EAAE,IAAI,EAAkB,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAyB,MAAM,uCAAuC,CAAC;AAC3F,OAAO,EAAE,IAAI,EAAkB,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAyB,MAAM,uCAAuC,CAAC;AAC3F,OAAO,EAAE,WAAW,EAAyB,MAAO,uCAAuC,CAAC;AAC5F,OAAO,EACL,gBAAgB,GACjB,MAAM,iDAAiD,CAAC;AAEzD,OAAO,EAAE,IAAI,EAAkB,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAyB,MAAM,8BAA8B,CAAC;AAClF,OAAO,EAAE,QAAQ,EAAsB,MAAM,2BAA2B,CAAC;AACzE,OAAO,EAAE,OAAO,EAAqB,MAAM,8BAA8B,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAwB,MAAM,6BAA6B,CAAC;AAC/E,OAAO,EAAE,MAAM,EAAoB,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,KAAK,EAAmB,MAAM,yBAAyB,CAAC;AACjE,OAAO,EAAE,OAAO,EAAqB,MAAM,6BAA6B,CAAC;AACzE,OAAO,EAAE,MAAM,EAAoB,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,QAAQ,EAA0C,MAAM,2BAA2B,CAAC;AAC7F,OAAO,EAAE,UAAU,EAAwB,MAAM,oCAAoC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fragment-build/ui",
3
- "version": "3.3.0",
3
+ "version": "4.0.1",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -11,3 +11,21 @@
11
11
  @apply text-muted;
12
12
  }
13
13
  }
14
+
15
+ @layer components {
16
+ .fragment-code-snippet--block {
17
+ @apply overflow-hidden rounded-xl border border-border bg-surface-secondary;
18
+ }
19
+
20
+ .fragment-code-snippet__header {
21
+ @apply flex items-center justify-between gap-2 border-b border-border py-1 pr-1 pl-4;
22
+ }
23
+
24
+ .fragment-code-snippet__title {
25
+ @apply text-xs font-medium text-muted;
26
+ }
27
+
28
+ .fragment-code-snippet__block-code {
29
+ @apply overflow-x-auto px-4 py-3 font-mono text-xs leading-relaxed text-foreground;
30
+ }
31
+ }
@@ -0,0 +1,41 @@
1
+ @layer components {
2
+ .fragment-segmented-control {
3
+ @apply inline-flex items-center gap-0.5 rounded-[var(--field-radius)] bg-default p-0.5;
4
+ }
5
+
6
+ .fragment-segmented-control--full-width {
7
+ @apply flex w-full;
8
+
9
+ .fragment-segmented-control__item {
10
+ @apply flex-1;
11
+ }
12
+ }
13
+
14
+ .fragment-segmented-control__item.toggle-button {
15
+ @apply h-8 rounded-[calc(var(--field-radius)-0.125rem)] px-3 text-sm font-medium text-muted md:h-8;
16
+
17
+ --toggle-button-bg: transparent;
18
+ --toggle-button-bg-hover: transparent;
19
+ --toggle-button-bg-pressed: transparent;
20
+ --toggle-button-bg-selected: var(--surface);
21
+ --toggle-button-bg-selected-hover: var(--surface);
22
+ --toggle-button-bg-selected-pressed: var(--surface);
23
+ --toggle-button-fg-selected: var(--surface-foreground, var(--foreground));
24
+
25
+ &:hover, &[data-hovered="true"] {
26
+ @apply text-foreground;
27
+ }
28
+
29
+ &[data-selected="true"] {
30
+ @apply shadow-sm text-foreground;
31
+ }
32
+
33
+ & svg {
34
+ @apply size-4;
35
+ }
36
+ }
37
+
38
+ .fragment-segmented-control--sm .fragment-segmented-control__item.toggle-button {
39
+ @apply h-7 px-2.5 text-xs md:h-7;
40
+ }
41
+ }
@@ -0,0 +1,39 @@
1
+ @layer components {
2
+ .fragment-empty-state {
3
+ @apply flex flex-col items-center justify-center text-center px-6 py-12;
4
+ }
5
+
6
+ .fragment-empty-state--sm {
7
+ @apply py-8;
8
+
9
+ .fragment-empty-state__icon {
10
+ @apply size-9 mb-3;
11
+ & svg {
12
+ @apply size-[18px];
13
+ }
14
+ }
15
+ }
16
+
17
+ .fragment-empty-state--bordered {
18
+ @apply rounded-2xl border border-dashed border-border bg-surface;
19
+ }
20
+
21
+ .fragment-empty-state__icon {
22
+ @apply mb-4 flex size-11 items-center justify-center rounded-xl bg-default text-muted;
23
+ & svg {
24
+ @apply size-5;
25
+ }
26
+ }
27
+
28
+ .fragment-empty-state__title {
29
+ @apply text-sm font-semibold text-foreground;
30
+ }
31
+
32
+ .fragment-empty-state__description {
33
+ @apply mt-1 max-w-sm text-sm text-muted text-balance;
34
+ }
35
+
36
+ .fragment-empty-state__actions {
37
+ @apply mt-5 flex flex-wrap items-center justify-center gap-2;
38
+ }
39
+ }
@@ -0,0 +1,42 @@
1
+ @layer components {
2
+ .fragment-form-card {
3
+ @apply p-0 gap-0 overflow-visible;
4
+ }
5
+
6
+ .fragment-form-card__header {
7
+ @apply flex items-start gap-4 px-6 pt-5;
8
+ }
9
+
10
+ .fragment-form-card__heading {
11
+ @apply min-w-0 flex-1;
12
+ }
13
+
14
+ .fragment-form-card__title {
15
+ @apply text-base font-semibold;
16
+ }
17
+
18
+ .fragment-form-card__description {
19
+ @apply mt-1 text-sm text-muted;
20
+ }
21
+
22
+ .fragment-form-card__header-end {
23
+ @apply shrink-0 flex items-center gap-2;
24
+ }
25
+
26
+ .fragment-form-card__body {
27
+ @apply flex flex-col gap-5 px-6 pt-5 pb-6;
28
+ }
29
+
30
+ .fragment-form-card__footer {
31
+ @apply flex items-center justify-between gap-3 rounded-b-[inherit] border-t border-separator bg-surface-secondary px-6 py-3
32
+ text-xs text-muted;
33
+ }
34
+
35
+ .fragment-form-card--danger {
36
+ @apply border-danger/30;
37
+
38
+ .fragment-form-card__title {
39
+ @apply text-danger;
40
+ }
41
+ }
42
+ }
@@ -1,41 +1,53 @@
1
1
  @layer components {
2
2
  .fragment-form-section__main {
3
- @apply flex flex-col;
3
+ @apply flex flex-col gap-4;
4
4
  }
5
5
 
6
6
  .fragment-form-section__info {
7
- @apply flex-1 pb-4;
7
+ @apply min-w-0;
8
8
  }
9
9
 
10
- .fragment-form-section__content {
11
- @apply flex flex-col gap-4 flex-1 min-w-0;
10
+ .fragment-form-section__title-row {
11
+ @apply flex items-center gap-2;
12
+ }
13
+
14
+ .fragment-form-section__title {
15
+ @apply text-sm font-semibold;
12
16
  }
13
17
 
14
18
  .fragment-form-section__description {
15
- @apply text-sm text-muted;
19
+ @apply mt-1 text-sm text-muted;
20
+ }
21
+
22
+ .fragment-form-section__content {
23
+ @apply flex flex-col gap-5 flex-1 min-w-0;
16
24
  }
17
25
 
26
+ /*
27
+ * Label column and control column. The label column is narrow and fixed so that, down a long
28
+ * settings page, every control starts on the same vertical line.
29
+ */
18
30
  .fragment-form-section--horizontal {
19
31
  .fragment-form-section__main {
20
- @apply sm:flex-row;
32
+ @apply md:flex-row md:gap-10;
21
33
  }
22
34
  .fragment-form-section__info {
23
- @apply sm:max-w-sm sm:pr-4 sm:pb-0;
35
+ @apply md:w-72 md:shrink-0;
24
36
  }
25
37
  .fragment-form-section__content {
26
- @apply sm:items-end sm:justify-center sm:max-w-sm;
38
+ @apply md:max-w-xl;
27
39
  }
28
40
  }
29
41
 
30
42
  .fragment-form-section--vertical {
31
43
  .fragment-form-section__content {
32
- @apply sm:max-w-3xl;
44
+ @apply md:max-w-3xl;
33
45
  }
34
46
  }
35
47
 
36
48
  .fragment-form-section--full-width {
37
49
  .fragment-form-section__content {
38
- @apply sm:max-w-full;
50
+ @apply md:max-w-full;
39
51
  }
40
52
  }
41
53
  }
@@ -1,5 +1,5 @@
1
1
  @layer components {
2
2
  .fragment-form__separator {
3
- @apply my-6;
3
+ @apply my-7;
4
4
  }
5
5
  }
@@ -1,13 +1,33 @@
1
1
  @layer components {
2
2
  .fragment-page-header {
3
- @apply flex gap-4 justify-between sm:items-end mb-8 flex-col sm:flex-row;
3
+ @apply flex flex-col gap-5 mb-8;
4
+ }
5
+
6
+ .fragment-page-header--with-footer {
7
+ @apply mb-6;
8
+ }
9
+
10
+ .fragment-page-header__row {
11
+ @apply flex gap-4 justify-between flex-col sm:flex-row sm:items-end;
12
+ }
13
+
14
+ .fragment-page-header__heading {
15
+ @apply min-w-0;
16
+ }
17
+
18
+ .fragment-page-header__eyebrow {
19
+ @apply mb-1.5 flex items-center gap-1.5 text-xs font-medium text-muted;
4
20
  }
5
21
 
6
22
  .fragment-page-header__description {
7
- @apply mt-2 text-muted;
23
+ @apply mt-1 max-w-2xl text-sm text-muted;
8
24
  }
9
25
 
10
26
  .fragment-page-header__actions {
11
- @apply flex gap-4;
27
+ @apply flex flex-wrap gap-2 items-center shrink-0;
28
+ }
29
+
30
+ .fragment-page-header__footer {
31
+ @apply min-w-0;
12
32
  }
13
33
  }
@@ -0,0 +1,28 @@
1
+ @layer components {
2
+ /* Sticky inside the form rather than fixed to the viewport, so it never needs to know how wide the rail is. */
3
+ /* Takes no room while hidden, so a clean form ends where its last field does. */
4
+ .fragment-save-bar {
5
+ @apply pointer-events-none sticky bottom-20 z-30 flex h-0 justify-center overflow-visible md:bottom-6;
6
+ @apply translate-y-4 opacity-0 transition-[opacity,translate] duration-200 motion-reduce:transition-none;
7
+ }
8
+
9
+ .fragment-save-bar--visible {
10
+ @apply pointer-events-auto mt-6 h-auto translate-y-0 opacity-100;
11
+ }
12
+
13
+ .fragment-save-bar__bar {
14
+ @apply flex w-full max-w-2xl items-center justify-between gap-4 rounded-2xl border border-border bg-overlay py-2 pr-2 pl-4
15
+ text-sm shadow-lg;
16
+ }
17
+
18
+ .fragment-save-bar__message {
19
+ @apply flex items-center gap-2 font-medium text-foreground;
20
+ & svg {
21
+ @apply shrink-0 text-warning;
22
+ }
23
+ }
24
+
25
+ .fragment-save-bar__actions {
26
+ @apply flex shrink-0 items-center gap-1;
27
+ }
28
+ }
@@ -7,16 +7,20 @@
7
7
  @apply flex-col;
8
8
  }
9
9
 
10
+ /*
11
+ * The content column is capped at a width a table or a two-column dashboard still reads well at.
12
+ * Past ~1600px the eye travels too far between a label and its value, and a wide monitor is
13
+ * better spent on margin than on stretching every card.
14
+ */
10
15
  .fragment-shell__main {
11
- @apply flex-1 min-w-0 mx-auto;
16
+ @apply flex-1 min-w-0 mx-auto w-full;
12
17
 
13
18
  .fragment-shell--horizontal & {
14
- @apply px-8 lg:px-12 xl:px-20 py-14 max-w-[2100px];
19
+ @apply px-4 sm:px-6 lg:px-10 pt-6 pb-24 md:pt-8 md:pb-12 max-w-[1600px];
15
20
  }
16
21
 
17
22
  .fragment-shell--vertical & {
18
- @apply px-8 py-6 max-w-[2100px] w-full;
23
+ @apply px-4 sm:px-6 lg:px-10 py-6 max-w-[1600px];
19
24
  }
20
-
21
25
  }
22
26
  }