@webority/theme 0.7.4 → 0.7.6

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.
@@ -36,3 +36,141 @@
36
36
  .card-compact > .card-body {
37
37
  padding: 16px;
38
38
  }
39
+
40
+ // ── Variants ──────────────────────────────────────────────────────────────────
41
+ // `default` is the flat surface above. The rest are opt-in visual treatments,
42
+ // chosen by the `variant` prop (React) / attribute (Razor).
43
+ .card-elevated {
44
+ border-color: transparent;
45
+ box-shadow: var(--shadow-md);
46
+ }
47
+ .card-outlined {
48
+ border-width: 2px;
49
+ border-color: var(--color-border-strong);
50
+ }
51
+ .card-muted {
52
+ --bs-card-bg: var(--color-bg-muted);
53
+ }
54
+ // Accent — a coloured left stripe, tinted by `tone` (default primary).
55
+ .card-accent {
56
+ --card-accent: var(--color-primary);
57
+ position: relative;
58
+ overflow: hidden;
59
+ }
60
+ .card-accent.tone-success {
61
+ --card-accent: var(--color-success);
62
+ }
63
+ .card-accent.tone-warning {
64
+ --card-accent: var(--color-warning);
65
+ }
66
+ .card-accent.tone-danger {
67
+ --card-accent: var(--color-danger);
68
+ }
69
+ .card-accent.tone-info {
70
+ --card-accent: var(--color-info);
71
+ }
72
+ .card-accent::before {
73
+ content: "";
74
+ position: absolute;
75
+ top: 0;
76
+ bottom: 0;
77
+ left: 0;
78
+ width: 4px;
79
+ background: var(--card-accent);
80
+ }
81
+
82
+ // ── Padding scale ── (header + body + footer). `md` is the Bootstrap default.
83
+ .card-pad-none > .card-header,
84
+ .card-pad-none > .card-body,
85
+ .card-pad-none > .card-footer {
86
+ padding: 0;
87
+ }
88
+ .card-pad-sm > .card-header,
89
+ .card-pad-sm > .card-body,
90
+ .card-pad-sm > .card-footer {
91
+ padding: 14px 16px;
92
+ }
93
+ .card-pad-lg > .card-header,
94
+ .card-pad-lg > .card-body,
95
+ .card-pad-lg > .card-footer {
96
+ padding: 28px 32px;
97
+ }
98
+
99
+ // ── Constrained width ── (`maxWidth`): caps + centres a standalone card. Cards
100
+ // are fluid (fill their column) unless this is set.
101
+ .card-constrained {
102
+ max-width: var(--card-max-width, none);
103
+ margin-inline: auto;
104
+ }
105
+
106
+ // ── Footer ── (React `footer` / Razor <app-card-footer>): mirrors the header row.
107
+ .card-footer {
108
+ --bs-card-cap-bg: var(--color-surface);
109
+ display: flex;
110
+ align-items: center;
111
+ gap: 12px;
112
+ border-top: 1px solid var(--color-border);
113
+ }
114
+
115
+ // ── Header icon + title stack ──
116
+ // A leading glyph before the title, with the title/subtitle stacked; the group
117
+ // stays left while header actions push right.
118
+ .card-header-main {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 10px;
122
+ min-width: 0;
123
+ }
124
+ .card-header-icon {
125
+ flex: none;
126
+ display: inline-grid;
127
+ place-items: center;
128
+ color: var(--color-text-muted);
129
+ }
130
+ .card-header-titles {
131
+ min-width: 0;
132
+ }
133
+
134
+ // ── Media ── (React `media` / Razor <app-card-media>): a top image/video band.
135
+ .card-media {
136
+ display: block;
137
+ overflow: hidden;
138
+ }
139
+ .card-media > img,
140
+ .card-media > video {
141
+ display: block;
142
+ width: 100%;
143
+ height: auto;
144
+ object-fit: cover;
145
+ }
146
+ // Media flush at the top follows the card's rounded corners.
147
+ .card > .card-media:first-child {
148
+ border-top-left-radius: var(--radius-lg);
149
+ border-top-right-radius: var(--radius-lg);
150
+ }
151
+
152
+ // ── Clickable card ── (`href` / `onClick`): the whole surface is one control.
153
+ .card-clickable {
154
+ display: block;
155
+ color: inherit;
156
+ text-align: inherit;
157
+ text-decoration: none;
158
+ cursor: pointer;
159
+ transition:
160
+ border-color 0.15s ease,
161
+ transform 0.2s var(--wui-ease-out-quart),
162
+ box-shadow 0.2s ease;
163
+ }
164
+ button.card-clickable {
165
+ width: 100%;
166
+ font: inherit;
167
+ background: var(--bs-card-bg, var(--color-surface));
168
+ }
169
+ .card-clickable:hover {
170
+ border-color: var(--color-primary);
171
+ transform: translateY(-1px);
172
+ }
173
+ .card-clickable:focus-visible {
174
+ outline: 2px solid var(--color-primary);
175
+ outline-offset: 2px;
176
+ }
@@ -24,6 +24,10 @@
24
24
  .struct-card:hover {
25
25
  border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
26
26
  }
27
+ .struct-card:has(.struct-card-input:focus-visible) {
28
+ outline: 2px solid var(--color-primary);
29
+ outline-offset: 2px;
30
+ }
27
31
  .struct-card.checked {
28
32
  border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
29
33
  background: color-mix(in srgb, var(--color-primary) 5%, transparent);
@@ -854,8 +858,8 @@ button.datepicker-month:hover {
854
858
  font-size: 12px;
855
859
  color: var(--color-text-subtle);
856
860
  }
857
- @media (max-width: 660px) {
858
- // Below the two-month width, stack the calendars so the popover still fits the viewport.
861
+ @media (max-width: 767.98px) {
862
+ // Below Bootstrap md tier: stack the calendars so the popover still fits the viewport.
859
863
  .daterange-pop {
860
864
  width: 320px;
861
865
  }
@@ -1133,3 +1137,121 @@ button.datepicker-month:hover {
1133
1137
  color: var(--color-text-subtle);
1134
1138
  font-size: 14px;
1135
1139
  }
1140
+
1141
+ // timepicker — time-of-day field with listbox popover (min/max bounds, step granularity, 12/24 display).
1142
+ .timepicker {
1143
+ position: relative;
1144
+ width: var(--wui-timepicker-width, 12rem);
1145
+ max-width: 100%;
1146
+ }
1147
+ .timepicker-trigger {
1148
+ display: flex;
1149
+ align-items: center;
1150
+ justify-content: space-between;
1151
+ gap: 8px;
1152
+ text-align: left;
1153
+ cursor: pointer;
1154
+ }
1155
+ .timepicker-trigger [data-trigger-label] {
1156
+ overflow: hidden;
1157
+ white-space: nowrap;
1158
+ text-overflow: ellipsis;
1159
+ }
1160
+ .timepicker-trigger > .bi {
1161
+ flex-shrink: 0;
1162
+ color: var(--color-text-subtle);
1163
+ }
1164
+ // The label span carries this while nothing is selected — same dim ink the trigger's
1165
+ // :not(.has-value) fallback uses, kept as its own rule so the span reads as placeholder
1166
+ // even when the trigger classes are toggled independently.
1167
+ .timepicker-placeholder {
1168
+ color: var(--color-text-subtle);
1169
+ }
1170
+ .timepicker-trigger:not(.has-value) {
1171
+ color: var(--color-text-subtle);
1172
+ }
1173
+ // Width presets.
1174
+ .time-w-sm {
1175
+ --wui-timepicker-width: 10rem;
1176
+ }
1177
+ .time-w-md {
1178
+ --wui-timepicker-width: 12rem;
1179
+ }
1180
+ .time-w-lg {
1181
+ --wui-timepicker-width: 16rem;
1182
+ }
1183
+ .time-w-full {
1184
+ --wui-timepicker-width: 100%;
1185
+ }
1186
+ .timepicker.open .timepicker-trigger {
1187
+ background-color: var(--color-bg-muted);
1188
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
1189
+ box-shadow: none;
1190
+ }
1191
+ .timepicker-pop {
1192
+ position: fixed;
1193
+ z-index: var(--wui-z-menu);
1194
+ width: 180px;
1195
+ max-width: calc(100vw - 16px);
1196
+ max-height: 360px;
1197
+ overflow-y: auto;
1198
+ padding: 8px;
1199
+ background: var(--color-surface);
1200
+ border: 1px solid var(--color-border);
1201
+ border-radius: var(--radius-lg);
1202
+ corner-shape: squircle;
1203
+ box-shadow: var(--shadow-overlay);
1204
+ animation: timepicker-in 0.14s ease;
1205
+ }
1206
+ .timepicker-pop.drop-up {
1207
+ animation: timepicker-in-up 0.14s ease;
1208
+ }
1209
+ @keyframes timepicker-in {
1210
+ from {
1211
+ opacity: 0;
1212
+ transform: translateY(-4px);
1213
+ }
1214
+ to {
1215
+ opacity: 1;
1216
+ transform: translateY(0);
1217
+ }
1218
+ }
1219
+ @keyframes timepicker-in-up {
1220
+ from {
1221
+ opacity: 0;
1222
+ transform: translateY(4px);
1223
+ }
1224
+ to {
1225
+ opacity: 1;
1226
+ transform: translateY(0);
1227
+ }
1228
+ }
1229
+ .timepicker-option {
1230
+ display: block;
1231
+ width: 100%;
1232
+ padding: 8px 12px;
1233
+ border: none;
1234
+ background: transparent;
1235
+ text-align: left;
1236
+ font-size: 14px;
1237
+ color: var(--color-text);
1238
+ cursor: pointer;
1239
+ border-radius: var(--radius-sm);
1240
+ transition:
1241
+ background 0.1s,
1242
+ color 0.1s;
1243
+ }
1244
+ .timepicker-option:hover,
1245
+ .timepicker-option.is-active {
1246
+ background: var(--color-primary-soft, color-mix(in srgb, var(--color-primary) 8%, transparent));
1247
+ color: var(--color-primary);
1248
+ }
1249
+ .timepicker-option.is-selected {
1250
+ background: var(--color-primary);
1251
+ color: var(--color-primary-text);
1252
+ font-weight: 600;
1253
+ }
1254
+ .timepicker-option:focus-visible {
1255
+ outline: none;
1256
+ box-shadow: inset 0 0 0 2px var(--color-primary);
1257
+ }
@@ -10,11 +10,11 @@
10
10
  // Bootstrap `.form-check` + `.form-check-input` are the public selection skin
11
11
  // (AppCheckbox / AppRadio / AppSwitch). `.wui-check` / `.wui-radio` are aliases
12
12
  // for the same glyph (AppDataTable select column + legacy `.wui-control`
13
- // markup) — never a second look (TASK-06077). One 20px brand glyph language.
13
+ // markup) — never a second look. One 20px brand glyph language.
14
14
  //
15
15
  // Layout wrappers:
16
- // .form-check / .form-check-label — App* path
17
- // .wui-control / .wui-control-label — optional compact label wrap (alias)
16
+ // .form-check / .form-check-label — App* path
17
+ // .wui-control / .wui-control-label — optional compact label wrap (alias)
18
18
 
19
19
  .form-check,
20
20
  .wui-control {
@@ -141,7 +141,7 @@
141
141
  }
142
142
 
143
143
  // Compact row-select — ONLY inside the select column (AppDataTable / Razor
144
- // data-table → `.wui-select-cell`). Scope is load-bearing (BUG-06678).
144
+ // data-table → `.wui-select-cell`). Scope is load-bearing.
145
145
  .wui-select-cell .wui-check,
146
146
  .wui-select-cell .form-check-input {
147
147
  width: 16px;
@@ -187,7 +187,7 @@
187
187
  // ── Field / Input ──
188
188
  // Bootstrap `.form-control` / `.form-select` are the field skin. `.input` /
189
189
  // `.select` / `.textarea` are aliases (and App* size/shell hooks) that MUST
190
- // share the same chrome — never a second look (TASK-06079). The only WUI
190
+ // share the same chrome — never a second look. The only WUI
191
191
  // deltas on top of Bootstrap's $input-* vars: shared control height, hover
192
192
  // fill, brand-border focus (no glow), soft invalid border.
193
193
  //
@@ -566,6 +566,10 @@ textarea.form-control.autosize {
566
566
  color: var(--color-primary);
567
567
  background: transparent;
568
568
  }
569
+ .pw-toggle:focus-visible {
570
+ outline: 2px solid var(--color-primary);
571
+ outline-offset: 2px;
572
+ }
569
573
 
570
574
  // Clearable ✕ — a trailing reset button, placed inside .input-wrap / .input-icon. It only
571
575
  // renders while the field has a value; .has-clear reserves the room so text never sits under it.
@@ -594,11 +598,15 @@ textarea.form-control.autosize {
594
598
  }
595
599
  .input-clear:hover {
596
600
  color: var(--color-text);
597
- background: var(--wui-bg-muted);
601
+ background: var(--color-bg-muted);
602
+ }
603
+ .input-clear:focus-visible {
604
+ outline: 2px solid var(--color-primary);
605
+ outline-offset: 2px;
598
606
  }
599
607
  @keyframes wui-clear-in {
600
608
  from { opacity: 0; transform: translateY(-50%) scale(0.72); }
601
- to { opacity: 1; transform: translateY(-50%) scale(1); }
609
+ to { opacity: 1; transform: translateY(-50%) scale(1); }
602
610
  }
603
611
 
604
612
  // Prefix / suffix addon — a bordered box before/after the input (₹, https://, .com). The
@@ -633,7 +641,7 @@ textarea.form-control.autosize {
633
641
  display: flex;
634
642
  align-items: center;
635
643
  padding: 0 12px;
636
- background: var(--wui-bg-muted);
644
+ background: var(--color-bg-muted);
637
645
  color: var(--color-text-subtle);
638
646
  font-size: 14px;
639
647
  white-space: nowrap;
@@ -789,6 +797,7 @@ textarea.form-control.autosize {
789
797
  wui-select,
790
798
  wui-autocomplete,
791
799
  wui-multiselect,
800
+ wui-taginput,
792
801
  wui-phone,
793
802
  wui-datepicker,
794
803
  wui-date-range {
@@ -805,6 +814,7 @@ wui-select[compact] {
805
814
  .field > wui-select,
806
815
  .field > wui-autocomplete,
807
816
  .field > wui-multiselect,
817
+ .field > wui-taginput,
808
818
  .field > wui-phone,
809
819
  .field > wui-datepicker,
810
820
  .field > wui-date-range,
@@ -886,10 +896,104 @@ wui-select[compact] {
886
896
  font-size: 14px;
887
897
  font-weight: 500;
888
898
  color: var(--color-text-muted);
889
- font-family: "Inter", sans-serif;
899
+ font-family: var(--font-sans);
900
+ display: inline-flex;
901
+ align-items: center;
902
+ white-space: nowrap;
903
+ }
904
+
905
+ // Number input with stepper — decrements/increments by step, clamped to min/max.
906
+ // One field, one border: the WRAPPER owns the border + hover/focus/error ring
907
+ // so the input and stepper buttons light up together.
908
+ .num-wrap {
909
+ display: flex;
910
+ align-items: stretch;
911
+ border: 1px solid var(--color-border);
912
+ border-radius: var(--radius-lg);
913
+ corner-shape: squircle;
914
+ background: var(--color-surface);
915
+ transition:
916
+ border-color 0.12s,
917
+ background-color 0.12s;
918
+ overflow: hidden;
919
+ }
920
+ .num-wrap:hover:not(:focus-within) {
921
+ background: var(--color-bg-muted);
922
+ border-color: var(--color-border-strong);
923
+ }
924
+ .num-wrap:focus-within {
925
+ background: var(--color-bg-muted);
926
+ border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
927
+ }
928
+ .num-wrap:has(.num-input.error) {
929
+ border-color: var(--color-danger);
930
+ }
931
+
932
+ // Numeric input inside the stepper — plain text field, no native spinner.
933
+ // Flows seamlessly between the stepper buttons; the wrapper carries the border.
934
+ .num-wrap .num-input {
935
+ flex: 1;
936
+ border: none;
937
+ border-radius: 0;
938
+ background: transparent;
939
+ min-width: 0;
940
+ }
941
+ .num-wrap .num-input:focus {
942
+ box-shadow: none;
943
+ }
944
+
945
+ // Stepper buttons — decrease/increase the value by step.
946
+ // Positioned left and right of the input, disabled at min/max or when the
947
+ // control is disabled.
948
+ .num-btn {
949
+ display: grid;
950
+ place-items: center;
951
+ width: 40px;
952
+ height: 100%;
953
+ border: none;
954
+ background: transparent;
955
+ padding: 0;
956
+ color: var(--color-text-muted);
957
+ cursor: pointer;
958
+ transition:
959
+ color 0.12s ease,
960
+ background 0.12s ease;
961
+ flex: none;
962
+ }
963
+ .num-btn:hover:not(:disabled) {
964
+ color: var(--color-primary);
965
+ background: var(--color-bg-muted);
966
+ }
967
+ .num-btn:focus-visible {
968
+ outline: none;
969
+ color: var(--color-primary);
970
+ }
971
+ .num-btn:active:not(:disabled) {
972
+ color: color-mix(in srgb, var(--color-primary) 80%, black);
973
+ }
974
+ .num-btn:disabled {
975
+ color: var(--color-text-subtle);
976
+ cursor: not-allowed;
977
+ opacity: 0.5;
978
+ }
979
+ .num-btn:not(:first-child) {
980
+ border-left: 1px solid var(--color-border);
981
+ }
982
+
983
+ // Affix (unit) in the num-wrap — a trailing pill after the input.
984
+ .num-wrap .affix {
985
+ background: var(--color-bg-muted);
986
+ border: none;
987
+ border-left: 1px solid var(--color-border);
988
+ padding: 9px 14px;
989
+ font-size: 14px;
990
+ font-weight: 500;
991
+ color: var(--color-text-muted);
992
+ font-family: var(--font-sans);
890
993
  display: inline-flex;
891
994
  align-items: center;
892
995
  white-space: nowrap;
996
+ flex: none;
893
997
  }
894
998
 
895
999
  // ── Status badge ──
@@ -959,8 +1063,8 @@ wui-select[compact] {
959
1063
  //
960
1064
  // Two other vocabularies existed and are accepted as DEPRECATED ALIASES so a
961
1065
  // consumer can migrate without a flag day:
962
- // - colour-named .tone-green / -amber / -red / -blue / -grey (was the library's)
963
- // - short semantic .tone-ok / -warn / -bad (a short-semantic set)
1066
+ // - colour-named .tone-green / -amber / -red / -blue / -grey (was the library's)
1067
+ // - short semantic .tone-ok / -warn / -bad (a short-semantic set)
964
1068
  // Author new markup with the semantic names; the aliases will be removed.
965
1069
  .status-label.success,
966
1070
  .status-label.tone-ok,
@@ -1024,6 +1128,10 @@ wui-select[compact] {
1024
1128
  .choice-chip:hover {
1025
1129
  border-color: color-mix(in srgb, var(--color-primary) 30%, transparent);
1026
1130
  }
1131
+ .choice-chip:has(input:focus-visible) {
1132
+ outline: 2px solid var(--color-primary);
1133
+ outline-offset: 2px;
1134
+ }
1027
1135
  .choice-chip input {
1028
1136
  position: absolute;
1029
1137
  opacity: 0;
@@ -1159,3 +1267,120 @@ wui-otp {
1159
1267
  width: 38px;
1160
1268
  }
1161
1269
  }
1270
+
1271
+ // ── Slider (AppSlider / <wui-slider>) ──
1272
+ // Single-thumb and dual-thumb (range) slider. Horizontal track with filled segment,
1273
+ // thumb button(s) (role="slider"), and optional value label. Pointer dragging and
1274
+ // keyboard navigation (arrow keys, Page Up/Down, Home/End). Form-associated for
1275
+ // single/range value submission.
1276
+ .slider {
1277
+ display: flex;
1278
+ align-items: center;
1279
+ gap: 12px;
1280
+ position: relative;
1281
+ user-select: none;
1282
+ height: 32px;
1283
+ min-width: 200px;
1284
+ }
1285
+ .slider-track {
1286
+ flex: 1;
1287
+ position: relative;
1288
+ height: 6px;
1289
+ background-color: var(--color-border);
1290
+ border-radius: 3px;
1291
+ cursor: pointer;
1292
+ }
1293
+ .slider-fill {
1294
+ position: absolute;
1295
+ top: 0;
1296
+ height: 6px;
1297
+ background-color: var(--color-primary);
1298
+ border-radius: 3px;
1299
+ pointer-events: none;
1300
+ }
1301
+ .slider-thumb {
1302
+ position: absolute;
1303
+ top: 50%;
1304
+ width: 20px;
1305
+ height: 20px;
1306
+ margin-top: -10px;
1307
+ margin-left: -10px;
1308
+ padding: 0;
1309
+ appearance: none;
1310
+ -webkit-appearance: none;
1311
+ background-color: var(--color-primary);
1312
+ border: 2px solid var(--color-surface);
1313
+ border-radius: 50%;
1314
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
1315
+ cursor: pointer;
1316
+ transition:
1317
+ background-color 0.15s ease,
1318
+ box-shadow 0.15s ease,
1319
+ border-color 0.15s ease;
1320
+ }
1321
+ .slider-thumb:hover:not(:disabled) {
1322
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
1323
+ }
1324
+ .slider-thumb:focus-visible {
1325
+ outline: 2px solid var(--color-primary);
1326
+ outline-offset: 4px;
1327
+ }
1328
+ .slider-thumb:disabled,
1329
+ .slider:has([disabled]) .slider-thumb {
1330
+ background-color: var(--color-text-subtle);
1331
+ border-color: var(--color-border);
1332
+ cursor: not-allowed;
1333
+ opacity: 0.6;
1334
+ }
1335
+ .slider-value {
1336
+ display: inline-block;
1337
+ min-width: 60px;
1338
+ text-align: center;
1339
+ font-size: 14px;
1340
+ font-weight: 500;
1341
+ color: var(--color-text);
1342
+ white-space: nowrap;
1343
+ }
1344
+
1345
+ // Size variants
1346
+ .slider-sm {
1347
+ height: 28px;
1348
+ gap: 8px;
1349
+ }
1350
+ .slider-sm .slider-track {
1351
+ height: 4px;
1352
+ }
1353
+ .slider-sm .slider-fill {
1354
+ height: 4px;
1355
+ }
1356
+ .slider-sm .slider-thumb {
1357
+ width: 16px;
1358
+ height: 16px;
1359
+ margin-top: -8px;
1360
+ margin-left: -8px;
1361
+ }
1362
+ .slider-sm .slider-value {
1363
+ font-size: 12px;
1364
+ min-width: 50px;
1365
+ }
1366
+
1367
+ .slider-lg {
1368
+ height: 36px;
1369
+ gap: 16px;
1370
+ }
1371
+ .slider-lg .slider-track {
1372
+ height: 8px;
1373
+ }
1374
+ .slider-lg .slider-fill {
1375
+ height: 8px;
1376
+ }
1377
+ .slider-lg .slider-thumb {
1378
+ width: 24px;
1379
+ height: 24px;
1380
+ margin-top: -12px;
1381
+ margin-left: -12px;
1382
+ }
1383
+ .slider-lg .slider-value {
1384
+ font-size: 15px;
1385
+ min-width: 70px;
1386
+ }
@@ -255,7 +255,7 @@
255
255
  padding: 0 12px;
256
256
  background: var(--color-bg-muted);
257
257
  border-bottom: none;
258
- font-family: "Inter", var(--font-sans, sans-serif);
258
+ font-family: var(--font-sans);
259
259
  font-size: 12px;
260
260
  font-weight: 500;
261
261
  line-height: 17px;
@@ -279,7 +279,7 @@
279
279
  height: 54px;
280
280
  padding: 12px;
281
281
  border-bottom: 0.5px solid var(--color-border);
282
- font-family: "Inter", var(--font-sans, sans-serif);
282
+ font-family: var(--font-sans);
283
283
  font-size: 12px;
284
284
  font-weight: 500;
285
285
  line-height: 15px;
@@ -361,17 +361,6 @@
361
361
  gap: 6px;
362
362
  }
363
363
 
364
- // Ledger tables: 20px edge padding so header labels + first/last cells align
365
- // with the card edge — shared across every .table-ledger surface.
366
- .table-ledger thead th:first-child,
367
- .table-ledger tbody td:first-child {
368
- padding-left: 20px;
369
- }
370
- .table-ledger thead th:last-child,
371
- .table-ledger tbody td:last-child {
372
- padding-right: 20px;
373
- }
374
-
375
364
  // Pagination footer — rows-per-page + range + prev/next, sits flush under a
376
365
  // table.
377
366
  .table-pagination {
@@ -486,6 +475,30 @@
486
475
  user-select: none;
487
476
  }
488
477
 
478
+ // ── Standalone pagination (AppPagination) ──
479
+ // Bootstrap's .pagination is already styled; these rules refine it for the common case
480
+ // of a nav wrapper and ellipsis representation.
481
+ .pagination-nav {
482
+ display: flex;
483
+ align-items: center;
484
+ justify-content: center;
485
+ }
486
+ .pagination-md {
487
+ // Default Bootstrap pagination size (no-op, but explicit for parallelism with sm)
488
+ }
489
+ .pagination-sm .page-link {
490
+ padding: 0.25rem 0.5rem;
491
+ font-size: 12px;
492
+ }
493
+ .page-ellipsis {
494
+ display: grid;
495
+ place-items: center;
496
+ min-width: 24px;
497
+ color: var(--color-text-subtle);
498
+ font-size: 13px;
499
+ user-select: none;
500
+ }
501
+
489
502
  // Horizontal scroll guard + flush-fit inside a card. Wrapping the table here
490
503
  // also stops `.card > .table` header-corner rounding from applying mid-card.
491
504
  .table-wrap {