@loidolt/theme-styles 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loidolt/theme-styles",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Base styles, fonts, accessibility helpers, and utilities for Loidolt applications",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -14,6 +14,7 @@
14
14
  "./accessibility": "./src/accessibility.css",
15
15
  "./utilities": "./src/utilities.css",
16
16
  "./components": "./src/components.css",
17
+ "./fonts": "./src/fonts.css",
17
18
  "./fonts/*": "./src/fonts/*",
18
19
  "./package.json": "./package.json"
19
20
  },
@@ -30,7 +31,7 @@
30
31
  "build": "node -e \"console.log('theme-styles: source CSS is publish-ready')\""
31
32
  },
32
33
  "dependencies": {
33
- "@loidolt/theme-tokens": "^0.2.0"
34
+ "@loidolt/theme-tokens": "^0.3.0"
34
35
  },
35
36
  "publishConfig": {
36
37
  "access": "public",
@@ -13,7 +13,8 @@
13
13
  * just on the other side of the border box. Zero specificity, declared after the rule above,
14
14
  * so it overrides by document order without escalating specificity.
15
15
  */
16
- :where(.ldt-topbar__nav, .ldt-table-wrap, .ldt-sidebar, .ldt-dialog__body) :where(:focus-visible) {
16
+ :where(.ldt-topbar__nav, .ldt-table-wrap, .ldt-sidebar, .ldt-dialog__body, .ldt-drawer__body)
17
+ :where(:focus-visible) {
17
18
  outline-offset: -2px;
18
19
  }
19
20
 
@@ -55,10 +56,17 @@
55
56
  * plain declarations already override every component animation while app CSS still wins.
56
57
  */
57
58
  @media (prefers-reduced-motion: reduce) {
58
- :where(*, *::before, *::after) {
59
+ /* Pseudo-elements are invalid inside `:where()` and get dropped from its forgiving
60
+ list, so they are listed separately — animations on `::before`/`::after` must slow
61
+ down too. Delays are zeroed so nothing appears to hang waiting on a dead animation. */
62
+ *,
63
+ ::before,
64
+ ::after {
59
65
  scroll-behavior: auto;
66
+ animation-delay: 0.01ms;
60
67
  animation-duration: 0.01ms;
61
68
  animation-iteration-count: 1;
69
+ transition-delay: 0.01ms;
62
70
  transition-duration: 0.01ms;
63
71
  }
64
72
  }
package/src/base.css CHANGED
@@ -1,6 +1,11 @@
1
1
  /* Deep slice: assumes the token custom properties are already loaded.
2
2
  Import '@loidolt/theme-styles/tokens' (or the whole '@loidolt/theme-styles') first. */
3
- :where(*, *::before, *::after) {
3
+ /* Pseudo-elements are invalid inside `:where()` and get dropped from its forgiving list,
4
+ so they are listed separately. `*` already has zero specificity; `::before`/`::after`
5
+ carry (0,0,1), the minimum a pseudo-element can have. */
6
+ *,
7
+ ::before,
8
+ ::after {
4
9
  box-sizing: border-box;
5
10
  }
6
11
  :where(html) {
@@ -46,7 +51,8 @@
46
51
  :where(code, pre, kbd, samp) {
47
52
  font-family: var(--loidolt-font-mono);
48
53
  }
49
- :where(::selection) {
54
+ /* Pseudo-elements cannot appear inside `:where()`; bare `::selection` is (0,0,1). */
55
+ ::selection {
50
56
  color: var(--loidolt-on-accent);
51
57
  background: var(--loidolt-accent);
52
58
  }
@@ -41,6 +41,13 @@
41
41
  border-color: var(--loidolt-danger);
42
42
  background: var(--loidolt-danger);
43
43
  }
44
+ /* Without this, the base hover would flip a destructive button to the neutral inverse
45
+ surface — the danger affordance must survive hover. */
46
+ .ldt-button--danger:hover:not(:disabled, [aria-disabled='true']) {
47
+ color: var(--loidolt-on-danger);
48
+ border-color: var(--loidolt-danger-hover);
49
+ background: var(--loidolt-danger-hover);
50
+ }
44
51
  .ldt-button--ghost,
45
52
  .ldt-button--text {
46
53
  border-color: transparent;
@@ -183,6 +190,19 @@
183
190
  padding: 0;
184
191
  border: 0;
185
192
  }
193
+ /* A legend is shrink-to-fit by default, which collapses the space-between that pushes the
194
+ "optional" marker to the far end. */
195
+ .ldt-legend {
196
+ width: 100%;
197
+ }
198
+ .ldt-fieldset__body {
199
+ display: grid;
200
+ gap: var(--ldt-gap, var(--loidolt-space-3));
201
+ margin-top: 0.35rem;
202
+ }
203
+ .ldt-fieldset > :where(.ldt-field__description, .ldt-field__error) {
204
+ margin-top: 0.35rem;
205
+ }
186
206
  .ldt-radio-group {
187
207
  display: flex;
188
208
  flex-wrap: wrap;
@@ -277,17 +297,47 @@
277
297
  font-family: var(--loidolt-font-utility);
278
298
  font-size: var(--loidolt-font-size-sm);
279
299
  }
300
+ /*
301
+ * A badge reports state; it is never pressable. Standing at control height it would otherwise be
302
+ * indistinguishable from a button, so three things separate them, none of which depend on hover:
303
+ * the pill silhouette against the system's hard corners, the recessed fill instead of a raised
304
+ * one, and text that stays a step smaller than button text at every size.
305
+ */
280
306
  .ldt-badge {
281
307
  display: inline-flex;
282
308
  align-items: center;
283
309
  min-height: 1.4rem;
284
- padding: 0.15rem 0.45rem;
310
+ padding: 0.15rem 0.5rem;
285
311
  border: var(--loidolt-border-width) solid var(--loidolt-border);
312
+ border-radius: var(--loidolt-border-radius-pill);
286
313
  color: var(--loidolt-text-muted);
314
+ background: var(--loidolt-surface-sunken);
287
315
  font: var(--loidolt-font-size-xs) var(--loidolt-font-utility);
288
- letter-spacing: 0.08em;
316
+ /* The label voice (`.ldt-eyebrow`, `.ldt-field__label`), not the tighter control voice. */
317
+ letter-spacing: var(--loidolt-font-tracking-wide);
289
318
  text-transform: uppercase;
290
319
  }
320
+ /*
321
+ * Size steps take the button scale's *box* so a badge in a row of controls lines up with them,
322
+ * but not its type scale — the smaller text is part of what marks the badge as inert. Without a
323
+ * size class the badge keeps its compact inline metrics, which is what table cells and running
324
+ * copy want.
325
+ */
326
+ .ldt-badge--sm {
327
+ min-height: var(--loidolt-size-control-sm);
328
+ padding: 0.38rem 0.7rem;
329
+ line-height: 1;
330
+ }
331
+ .ldt-badge--md {
332
+ min-height: var(--loidolt-size-control);
333
+ padding: 0.5rem 0.9rem;
334
+ line-height: 1;
335
+ }
336
+ .ldt-badge--lg {
337
+ min-height: var(--loidolt-size-control-lg);
338
+ padding: 0.5rem 1.25rem;
339
+ line-height: 1;
340
+ }
291
341
  .ldt-badge--accent {
292
342
  color: var(--loidolt-on-accent);
293
343
  border-color: var(--loidolt-accent);
@@ -350,6 +400,110 @@
350
400
  .ldt-table tbody tr:last-child td {
351
401
  border-bottom: 0;
352
402
  }
403
+ /* Logical values: an `end`-aligned column follows the writing direction rather than pinning
404
+ itself to the right in RTL. */
405
+ .ldt-table :where(th, td)[data-align='start'] {
406
+ text-align: start;
407
+ }
408
+ .ldt-table :where(th, td)[data-align='center'] {
409
+ text-align: center;
410
+ }
411
+ .ldt-table :where(th, td)[data-align='end'] {
412
+ text-align: end;
413
+ }
414
+ /*
415
+ * Sticky header. The scroll container has to be bounded or there is nothing to stick within —
416
+ * `--ldt-table-height` is that bound, and the wrapper switches to two-axis scrolling.
417
+ */
418
+ .ldt-table-wrap--sticky {
419
+ max-height: var(--ldt-table-height, 24rem);
420
+ overflow: auto;
421
+ }
422
+ .ldt-table--sticky thead :where(th) {
423
+ position: sticky;
424
+ z-index: var(--loidolt-z-raised);
425
+ top: 0;
426
+ background: var(--loidolt-surface-alt);
427
+ /* `border-collapse: collapse` drops the borders of a sticky cell — the collapsed edge belongs
428
+ to the table, which scrolls away. An inset shadow is painted by the cell itself and stays. */
429
+ border-bottom: 0;
430
+ box-shadow: inset 0 -1px 0 var(--loidolt-border);
431
+ }
432
+ .ldt-table__sort {
433
+ display: inline-flex;
434
+ align-items: center;
435
+ gap: 0.4rem;
436
+ border: 0;
437
+ padding: 0;
438
+ color: inherit;
439
+ background: transparent;
440
+ font: inherit;
441
+ letter-spacing: inherit;
442
+ text-transform: inherit;
443
+ cursor: pointer;
444
+ }
445
+ .ldt-table__sort:hover {
446
+ color: var(--loidolt-text);
447
+ }
448
+ /* A CSS triangle rather than a glyph: no icon dependency, and nothing that can fall back to
449
+ tofu in a font that lacks the arrow. */
450
+ .ldt-table__sort-indicator {
451
+ width: 0;
452
+ height: 0;
453
+ border-inline: 0.3rem solid transparent;
454
+ border-bottom: 0.34rem solid currentColor;
455
+ opacity: 0;
456
+ }
457
+ .ldt-table__sort:hover .ldt-table__sort-indicator {
458
+ opacity: 0.4;
459
+ }
460
+ .ldt-table__sort[data-sort='ascending'] .ldt-table__sort-indicator,
461
+ .ldt-table__sort[data-sort='descending'] .ldt-table__sort-indicator {
462
+ opacity: 1;
463
+ }
464
+ .ldt-table__sort[data-sort='descending'] .ldt-table__sort-indicator {
465
+ transform: rotate(180deg);
466
+ }
467
+ .ldt-table__empty td {
468
+ padding: 0;
469
+ border-bottom: 0;
470
+ }
471
+ /* Inside a table the surrounding rule is already the boundary; a second dashed one reads as a
472
+ rendering fault. */
473
+ .ldt-table__empty .ldt-empty-state {
474
+ border: 0;
475
+ }
476
+ .ldt-empty-state {
477
+ display: grid;
478
+ justify-items: center;
479
+ gap: 0.5rem;
480
+ padding: var(--loidolt-space-10) var(--loidolt-space-6);
481
+ border: var(--loidolt-border-width) dashed var(--loidolt-border);
482
+ color: var(--loidolt-text-muted);
483
+ background: var(--loidolt-surface);
484
+ text-align: center;
485
+ }
486
+ .ldt-empty-state__icon {
487
+ color: var(--loidolt-text-muted);
488
+ font-size: var(--loidolt-font-size-xl);
489
+ line-height: 1;
490
+ }
491
+ .ldt-empty-state__title {
492
+ margin: 0;
493
+ color: var(--loidolt-text);
494
+ font-size: var(--loidolt-font-size-md);
495
+ }
496
+ .ldt-empty-state__description {
497
+ max-width: 42rem;
498
+ margin: 0;
499
+ }
500
+ .ldt-empty-state__actions {
501
+ display: flex;
502
+ flex-wrap: wrap;
503
+ justify-content: center;
504
+ gap: var(--loidolt-space-2);
505
+ margin-top: var(--loidolt-space-2);
506
+ }
353
507
 
354
508
  .ldt-alert {
355
509
  padding: 0.8rem 1rem;
@@ -536,6 +690,101 @@
536
690
  margin: 0.25rem 0 0;
537
691
  color: var(--loidolt-text-muted);
538
692
  }
693
+ /*
694
+ * Drawer: a dialog anchored to an edge. `--ldt-drawer-size` is the measure across that edge —
695
+ * width for left/right, height for top/bottom — always capped so the scrim stays reachable and
696
+ * the panel never becomes a full-screen trap with no visible way out.
697
+ */
698
+ .ldt-drawer {
699
+ position: fixed;
700
+ z-index: var(--loidolt-z-dialog);
701
+ display: grid;
702
+ grid-template-rows: auto minmax(0, 1fr) auto;
703
+ overflow: hidden;
704
+ border: 0 solid var(--loidolt-border-strong);
705
+ color: var(--loidolt-text);
706
+ background: var(--loidolt-surface);
707
+ box-shadow: var(--loidolt-shadow-modal);
708
+ }
709
+ .ldt-drawer--left,
710
+ .ldt-drawer--right {
711
+ inset-block: 0;
712
+ width: min(var(--ldt-drawer-size, 20rem), calc(100vw - 3rem));
713
+ }
714
+ .ldt-drawer--left {
715
+ left: 0;
716
+ border-right-width: var(--loidolt-border-width);
717
+ animation: ldt-drawer-in-left var(--loidolt-motion-slow) var(--loidolt-motion-easing);
718
+ }
719
+ .ldt-drawer--right {
720
+ right: 0;
721
+ border-left-width: var(--loidolt-border-width);
722
+ animation: ldt-drawer-in-right var(--loidolt-motion-slow) var(--loidolt-motion-easing);
723
+ }
724
+ .ldt-drawer--top,
725
+ .ldt-drawer--bottom {
726
+ inset-inline: 0;
727
+ height: min(var(--ldt-drawer-size, 20rem), calc(100dvh - 3rem));
728
+ }
729
+ .ldt-drawer--top {
730
+ top: 0;
731
+ border-bottom-width: var(--loidolt-border-width);
732
+ animation: ldt-drawer-in-top var(--loidolt-motion-slow) var(--loidolt-motion-easing);
733
+ }
734
+ .ldt-drawer--bottom {
735
+ bottom: 0;
736
+ border-top-width: var(--loidolt-border-width);
737
+ animation: ldt-drawer-in-bottom var(--loidolt-motion-slow) var(--loidolt-motion-easing);
738
+ }
739
+ @keyframes ldt-drawer-in-left {
740
+ from {
741
+ transform: translateX(-100%);
742
+ }
743
+ }
744
+ @keyframes ldt-drawer-in-right {
745
+ from {
746
+ transform: translateX(100%);
747
+ }
748
+ }
749
+ @keyframes ldt-drawer-in-top {
750
+ from {
751
+ transform: translateY(-100%);
752
+ }
753
+ }
754
+ @keyframes ldt-drawer-in-bottom {
755
+ from {
756
+ transform: translateY(100%);
757
+ }
758
+ }
759
+ .ldt-drawer__header,
760
+ .ldt-drawer__body,
761
+ .ldt-drawer__footer {
762
+ padding: 1rem 1.25rem;
763
+ }
764
+ .ldt-drawer__header {
765
+ display: flex;
766
+ align-items: start;
767
+ justify-content: space-between;
768
+ gap: 1rem;
769
+ border-bottom: var(--loidolt-border-width) solid var(--loidolt-border);
770
+ }
771
+ .ldt-drawer__body {
772
+ overflow-y: auto;
773
+ }
774
+ .ldt-drawer__footer {
775
+ display: flex;
776
+ justify-content: flex-end;
777
+ gap: 0.5rem;
778
+ border-top: var(--loidolt-border-width) solid var(--loidolt-border);
779
+ }
780
+ .ldt-drawer__title {
781
+ margin: 0;
782
+ font-size: var(--loidolt-font-size-lg);
783
+ }
784
+ .ldt-drawer__description {
785
+ margin: 0.25rem 0 0;
786
+ color: var(--loidolt-text-muted);
787
+ }
539
788
  .ldt-popover,
540
789
  .ldt-menu {
541
790
  z-index: var(--loidolt-z-menu);
@@ -571,11 +820,22 @@
571
820
  text-align: left;
572
821
  text-decoration: none;
573
822
  }
574
- .ldt-menu__item:hover,
823
+ .ldt-menu__item:hover:not(:disabled, [data-disabled]),
575
824
  .ldt-menu__item[data-highlighted] {
576
825
  color: var(--loidolt-text-inverse);
577
826
  background: var(--loidolt-surface-inverse);
578
827
  }
828
+ /* Exposed to assistive tech (no aria-hidden): a shortcut nobody can hear may as well
829
+ not exist. Muted normally; inherits the inverse ink while highlighted. */
830
+ .ldt-menu__hint {
831
+ color: var(--loidolt-text-muted);
832
+ font: var(--loidolt-font-size-xs) var(--loidolt-font-utility);
833
+ letter-spacing: 0.08em;
834
+ }
835
+ .ldt-menu__item:hover:not(:disabled, [data-disabled]) .ldt-menu__hint,
836
+ .ldt-menu__item[data-highlighted] .ldt-menu__hint {
837
+ color: inherit;
838
+ }
579
839
  .ldt-menu__item[data-disabled],
580
840
  .ldt-menu__item:disabled {
581
841
  opacity: 0.4;
@@ -645,6 +905,171 @@
645
905
  padding-block: 1rem;
646
906
  }
647
907
 
908
+ .ldt-accordion {
909
+ border-top: var(--loidolt-border-width) solid var(--loidolt-border);
910
+ }
911
+ .ldt-accordion__item {
912
+ border-bottom: var(--loidolt-border-width) solid var(--loidolt-border);
913
+ }
914
+ .ldt-accordion__heading {
915
+ margin: 0;
916
+ }
917
+ .ldt-accordion__trigger {
918
+ width: 100%;
919
+ min-height: var(--loidolt-size-control-lg);
920
+ display: flex;
921
+ align-items: center;
922
+ justify-content: space-between;
923
+ gap: 1rem;
924
+ border: 0;
925
+ padding: 0.75rem 0;
926
+ color: var(--loidolt-text);
927
+ background: transparent;
928
+ font: var(--loidolt-font-size-sm) var(--loidolt-font-utility);
929
+ letter-spacing: 0.08em;
930
+ text-align: left;
931
+ text-transform: uppercase;
932
+ }
933
+ .ldt-accordion__trigger:hover:not([data-disabled]) {
934
+ color: var(--loidolt-text-accent);
935
+ }
936
+ .ldt-accordion__trigger[data-disabled] {
937
+ opacity: 0.4;
938
+ cursor: not-allowed;
939
+ }
940
+ .ldt-accordion__indicator {
941
+ width: 0;
942
+ height: 0;
943
+ flex: 0 0 auto;
944
+ border-inline: 0.3rem solid transparent;
945
+ border-top: 0.34rem solid currentColor;
946
+ transition: transform var(--loidolt-motion-normal) var(--loidolt-motion-easing);
947
+ }
948
+ .ldt-accordion__trigger[data-state='open'] .ldt-accordion__indicator {
949
+ transform: rotate(180deg);
950
+ }
951
+ .ldt-accordion__body {
952
+ padding-bottom: 1rem;
953
+ color: var(--loidolt-text-muted);
954
+ }
955
+ .ldt-toggle-group {
956
+ display: inline-flex;
957
+ border: var(--loidolt-border-width) solid var(--loidolt-border-strong);
958
+ }
959
+ .ldt-toggle-group[data-orientation='vertical'] {
960
+ flex-direction: column;
961
+ }
962
+ .ldt-toggle-group__item {
963
+ min-height: var(--loidolt-size-control);
964
+ display: inline-flex;
965
+ align-items: center;
966
+ justify-content: center;
967
+ gap: 0.4rem;
968
+ border: 0;
969
+ padding: 0 0.85rem;
970
+ color: var(--loidolt-text-muted);
971
+ background: transparent;
972
+ font: var(--loidolt-font-size-sm) var(--loidolt-font-utility);
973
+ letter-spacing: 0.08em;
974
+ line-height: 1;
975
+ text-transform: uppercase;
976
+ white-space: nowrap;
977
+ }
978
+ /* One shared rule between neighbours, so the group reads as a single object rather than a row
979
+ of separate buttons. */
980
+ .ldt-toggle-group__item + .ldt-toggle-group__item {
981
+ border-left: var(--loidolt-border-width) solid var(--loidolt-border);
982
+ }
983
+ .ldt-toggle-group[data-orientation='vertical'] .ldt-toggle-group__item + .ldt-toggle-group__item {
984
+ border-left: 0;
985
+ border-top: var(--loidolt-border-width) solid var(--loidolt-border);
986
+ }
987
+ .ldt-toggle-group__item:hover:not([data-disabled], :disabled) {
988
+ color: var(--loidolt-text);
989
+ }
990
+ .ldt-toggle-group__item[data-state='on'] {
991
+ color: var(--loidolt-on-accent);
992
+ background: var(--loidolt-accent);
993
+ }
994
+ .ldt-toggle-group__item[data-state='on']:hover:not([data-disabled], :disabled) {
995
+ color: var(--loidolt-on-accent);
996
+ background: var(--loidolt-accent-hover);
997
+ }
998
+ .ldt-toggle-group__item[data-disabled],
999
+ .ldt-toggle-group__item:disabled {
1000
+ opacity: 0.4;
1001
+ cursor: not-allowed;
1002
+ }
1003
+ .ldt-toggle-group--sm .ldt-toggle-group__item {
1004
+ min-height: var(--loidolt-size-control-sm);
1005
+ padding: 0 0.65rem;
1006
+ font-size: var(--loidolt-font-size-xs);
1007
+ }
1008
+ .ldt-toggle-group--lg .ldt-toggle-group__item {
1009
+ min-height: var(--loidolt-size-control-lg);
1010
+ padding: 0 1.2rem;
1011
+ }
1012
+ .ldt-breadcrumbs__list {
1013
+ display: flex;
1014
+ flex-wrap: wrap;
1015
+ align-items: center;
1016
+ gap: 0.35rem 0.5rem;
1017
+ margin: 0;
1018
+ padding: 0;
1019
+ list-style: none;
1020
+ }
1021
+ .ldt-breadcrumbs__item {
1022
+ min-width: 0;
1023
+ display: inline-flex;
1024
+ align-items: center;
1025
+ gap: 0.5rem;
1026
+ }
1027
+ /* Generated content, so the separator is never selected, copied, or read aloud. */
1028
+ .ldt-breadcrumbs__item + .ldt-breadcrumbs__item::before {
1029
+ content: var(--ldt-breadcrumb-separator, '/');
1030
+ color: var(--loidolt-text-muted);
1031
+ }
1032
+ .ldt-breadcrumbs__link,
1033
+ .ldt-breadcrumbs__current {
1034
+ overflow: hidden;
1035
+ font: var(--loidolt-font-size-xs) var(--loidolt-font-utility);
1036
+ letter-spacing: var(--loidolt-font-tracking-utility);
1037
+ text-overflow: ellipsis;
1038
+ text-transform: uppercase;
1039
+ white-space: nowrap;
1040
+ }
1041
+ .ldt-breadcrumbs__link {
1042
+ color: var(--loidolt-text-muted);
1043
+ text-decoration: none;
1044
+ }
1045
+ .ldt-breadcrumbs__link:hover {
1046
+ color: var(--loidolt-text-accent);
1047
+ text-decoration: underline;
1048
+ }
1049
+ .ldt-breadcrumbs__current {
1050
+ color: var(--loidolt-text);
1051
+ }
1052
+ .ldt-pagination__list {
1053
+ display: flex;
1054
+ flex-wrap: wrap;
1055
+ align-items: center;
1056
+ gap: 0.3rem;
1057
+ }
1058
+ .ldt-pagination__ellipsis {
1059
+ padding-inline: 0.35rem;
1060
+ color: var(--loidolt-text-muted);
1061
+ }
1062
+ .ldt-pagination__page {
1063
+ min-width: var(--loidolt-size-control-sm);
1064
+ }
1065
+ .ldt-pagination__page[aria-current='page'],
1066
+ /* The base button hover is more specific than a plain `[aria-current]` rule, so the current
1067
+ page needs its own hover or it flips to the neutral inverse surface on the way past. */
1068
+ .ldt-pagination__page[aria-current='page']:hover:not(:disabled, [aria-disabled='true']) {
1069
+ color: var(--loidolt-on-accent);
1070
+ border-color: var(--loidolt-accent);
1071
+ background: var(--loidolt-accent);
1072
+ }
648
1073
  .ldt-app-shell {
649
1074
  min-height: 100dvh;
650
1075
  display: grid;