@loidolt/theme-styles 0.3.0 → 0.5.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/README.md ADDED
@@ -0,0 +1,18 @@
1
+ # @loidolt/theme-styles
2
+
3
+ Fonts, foundations, component classes, accessibility rules, and layout utilities for the Loidolt
4
+ design system.
5
+
6
+ ```sh
7
+ npm install @loidolt/theme-styles
8
+ ```
9
+
10
+ ```css
11
+ @import '@loidolt/theme-styles';
12
+ ```
13
+
14
+ Deep entry points include `/tokens`, `/base`, `/components`, `/utilities`, `/accessibility`,
15
+ `/fonts`, `/dark`, and `/dark-auto`. Every slice except `/tokens` expects the token custom
16
+ properties to be loaded first.
17
+
18
+ See the [design-system documentation](https://theme.loidolt.space) for customization guidance.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loidolt/theme-styles",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Base styles, fonts, accessibility helpers, and utilities for Loidolt applications",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -24,14 +24,11 @@
24
24
  "sideEffects": [
25
25
  "**/*.css"
26
26
  ],
27
- "engines": {
28
- "node": ">=22"
29
- },
30
27
  "scripts": {
31
28
  "build": "node -e \"console.log('theme-styles: source CSS is publish-ready')\""
32
29
  },
33
30
  "dependencies": {
34
- "@loidolt/theme-tokens": "^0.3.0"
31
+ "@loidolt/theme-tokens": "^0.5.0"
35
32
  },
36
33
  "publishConfig": {
37
34
  "access": "public",
@@ -13,9 +13,19 @@
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, .ldt-drawer__body)
16
+ :where(
17
+ .ldt-topbar__nav,
18
+ .ldt-table-wrap,
19
+ .ldt-sidebar,
20
+ .ldt-dialog__body,
21
+ .ldt-drawer__body,
22
+ .ldt-tabs__list,
23
+ .ldt-toggle-group
24
+ )
17
25
  :where(:focus-visible) {
18
26
  outline-offset: -2px;
27
+ /* Follow the control's accessible foreground pairing so inset focus stays visible on fills. */
28
+ outline-color: currentColor;
19
29
  }
20
30
 
21
31
  .ldt-visually-hidden,
@@ -49,6 +59,35 @@
49
59
  min-height: 44px;
50
60
  }
51
61
 
62
+ /* Preserve the compact visual density for mouse/keyboard layouts, but give touch-first devices
63
+ the 44px target size recommended for comfortable coarse-pointer operation. */
64
+ @media (pointer: coarse) {
65
+ :where(
66
+ .ldt-button,
67
+ .ldt-nav-menu,
68
+ .ldt-tabs__trigger,
69
+ .ldt-toggle-group__item,
70
+ .ldt-input,
71
+ .ldt-select,
72
+ .ldt-number-field__input,
73
+ .ldt-choice,
74
+ .ldt-switch,
75
+ .ldt-number-field__button,
76
+ .ldt-menu__item
77
+ ) {
78
+ min-height: 44px;
79
+ }
80
+ :where(.ldt-icon-button, .ldt-number-field__button) {
81
+ min-width: 44px;
82
+ }
83
+ :where(.ldt-icon-button) {
84
+ width: 44px;
85
+ }
86
+ .ldt-number-field {
87
+ grid-template-columns: 44px minmax(0, 1fr) 44px;
88
+ }
89
+ }
90
+
52
91
  /*
53
92
  * No `!important` here. Inside a cascade layer it would beat even an app's unlayered
54
93
  * `!important`, since importance reverses layer order — an app could not opt a deliberate
@@ -0,0 +1,134 @@
1
+ /* Deep slice: assumes the token custom properties are already loaded.
2
+ Import '@loidolt/theme-styles/tokens' (or the whole '@loidolt/theme-styles') first. */
3
+ .ldt-button {
4
+ max-width: 100%;
5
+ min-width: 0;
6
+ min-height: var(--loidolt-size-control);
7
+ display: inline-flex;
8
+ align-items: center;
9
+ justify-content: center;
10
+ gap: var(--loidolt-space-2);
11
+ border: var(--loidolt-border-width) solid var(--loidolt-border-strong);
12
+ border-radius: var(--loidolt-border-radius);
13
+ padding: 0.5rem 0.85rem;
14
+ color: var(--loidolt-text);
15
+ background: transparent;
16
+ font: var(--loidolt-font-size-sm) var(--loidolt-font-utility);
17
+ letter-spacing: 0.08em;
18
+ line-height: 1.2;
19
+ overflow-wrap: anywhere;
20
+ text-align: center;
21
+ text-decoration: none;
22
+ text-transform: uppercase;
23
+ white-space: normal;
24
+ }
25
+ .ldt-button:hover:not(:disabled, [aria-disabled='true']) {
26
+ color: var(--loidolt-text-inverse);
27
+ background: var(--loidolt-surface-inverse);
28
+ }
29
+ .ldt-button--primary {
30
+ color: var(--loidolt-on-accent);
31
+ border-color: var(--loidolt-accent);
32
+ background: var(--loidolt-accent);
33
+ }
34
+ .ldt-button--primary:hover:not(:disabled, [aria-disabled='true']) {
35
+ color: var(--loidolt-on-accent);
36
+ border-color: var(--loidolt-accent-hover);
37
+ background: var(--loidolt-accent-hover);
38
+ }
39
+ .ldt-button--quiet {
40
+ color: var(--loidolt-text-muted);
41
+ border-color: var(--loidolt-border);
42
+ }
43
+ .ldt-button--danger {
44
+ color: var(--loidolt-on-danger);
45
+ border-color: var(--loidolt-danger);
46
+ background: var(--loidolt-danger);
47
+ }
48
+ /* Without this, the base hover would flip a destructive button to the neutral inverse
49
+ surface — the danger affordance must survive hover. */
50
+ .ldt-button--danger:hover:not(:disabled, [aria-disabled='true']) {
51
+ color: var(--loidolt-on-danger);
52
+ border-color: var(--loidolt-danger-hover);
53
+ background: var(--loidolt-danger-hover);
54
+ }
55
+ .ldt-button--ghost,
56
+ .ldt-button--text {
57
+ border-color: transparent;
58
+ background: transparent;
59
+ }
60
+ .ldt-button--text {
61
+ /* 1.5rem is the WCAG 2.2 minimum target size; `auto` left this at ~22px. */
62
+ min-height: 1.5rem;
63
+ padding: 0.25rem;
64
+ color: var(--loidolt-text-muted);
65
+ }
66
+ .ldt-button--text:hover:not(:disabled, [aria-disabled='true']) {
67
+ color: var(--loidolt-text-accent);
68
+ background: transparent;
69
+ }
70
+ .ldt-button--sm {
71
+ min-height: var(--loidolt-size-control-sm);
72
+ padding: 0.38rem 0.65rem;
73
+ font-size: var(--loidolt-font-size-xs);
74
+ }
75
+ .ldt-button--lg {
76
+ min-height: var(--loidolt-size-control-lg);
77
+ padding-inline: 1.2rem;
78
+ }
79
+ .ldt-button:disabled,
80
+ .ldt-button[aria-disabled='true'] {
81
+ opacity: 0.4;
82
+ cursor: not-allowed;
83
+ }
84
+ .ldt-icon-button {
85
+ width: var(--loidolt-size-control);
86
+ min-width: var(--loidolt-size-control);
87
+ padding: 0;
88
+ }
89
+ .ldt-icon-button.ldt-button--sm {
90
+ width: var(--loidolt-size-control-sm);
91
+ min-width: var(--loidolt-size-control-sm);
92
+ padding: 0;
93
+ }
94
+ .ldt-icon-button.ldt-button--lg {
95
+ width: var(--loidolt-size-control-lg);
96
+ min-width: var(--loidolt-size-control-lg);
97
+ padding: 0;
98
+ }
99
+ .ldt-spinner {
100
+ width: 1em;
101
+ height: 1em;
102
+ border: 2px solid currentColor;
103
+ border-right-color: transparent;
104
+ border-radius: 50%;
105
+ animation: ldt-spin 0.7s linear infinite;
106
+ }
107
+ @keyframes ldt-spin {
108
+ to {
109
+ transform: rotate(360deg);
110
+ }
111
+ }
112
+ /* A pressable row: full-width, hover surface, optional selected state. Link or button. */
113
+ .ldt-list-row {
114
+ width: 100%;
115
+ display: flex;
116
+ align-items: center;
117
+ gap: var(--loidolt-space-2);
118
+ border: 0;
119
+ padding: var(--loidolt-space-1) var(--loidolt-space-2);
120
+ color: inherit;
121
+ background: transparent;
122
+ font: inherit;
123
+ text-align: start;
124
+ text-decoration: none;
125
+ cursor: pointer;
126
+ }
127
+ .ldt-list-row:hover {
128
+ background: var(--loidolt-surface-sunken);
129
+ text-decoration: none;
130
+ }
131
+ .ldt-list-row--selected {
132
+ box-shadow: inset 2px 0 0 var(--loidolt-accent);
133
+ background: var(--loidolt-surface-sunken);
134
+ }
@@ -0,0 +1,198 @@
1
+ .ldt-table-wrap {
2
+ width: 100%;
3
+ overflow-x: auto;
4
+ border: var(--loidolt-border-width) solid var(--loidolt-border);
5
+ }
6
+ .ldt-table {
7
+ width: 100%;
8
+ border-collapse: collapse;
9
+ color: var(--loidolt-text);
10
+ background: var(--loidolt-surface);
11
+ }
12
+ .ldt-table :where(th, td) {
13
+ padding: 0.7rem 0.8rem;
14
+ border-bottom: var(--loidolt-border-width) solid var(--loidolt-border-soft);
15
+ text-align: left;
16
+ }
17
+ .ldt-table th {
18
+ color: var(--loidolt-text-muted);
19
+ font: var(--loidolt-font-size-xs) var(--loidolt-font-utility);
20
+ letter-spacing: var(--loidolt-font-tracking-utility);
21
+ text-transform: uppercase;
22
+ }
23
+ .ldt-table tbody tr:last-child td {
24
+ border-bottom: 0;
25
+ }
26
+ /* Logical values: an `end`-aligned column follows the writing direction rather than pinning
27
+ itself to the right in RTL. */
28
+ .ldt-table :where(th, td)[data-align='start'] {
29
+ text-align: start;
30
+ }
31
+ .ldt-table :where(th, td)[data-align='center'] {
32
+ text-align: center;
33
+ }
34
+ .ldt-table :where(th, td)[data-align='end'] {
35
+ text-align: end;
36
+ }
37
+ /*
38
+ * Sticky header. The scroll container has to be bounded or there is nothing to stick within —
39
+ * `--ldt-table-height` is that bound, and the wrapper switches to two-axis scrolling.
40
+ */
41
+ .ldt-table-wrap--sticky {
42
+ max-height: var(--ldt-table-height, 24rem);
43
+ overflow: auto;
44
+ }
45
+ .ldt-table--sticky thead :where(th) {
46
+ position: sticky;
47
+ z-index: var(--loidolt-z-raised);
48
+ top: 0;
49
+ background: var(--loidolt-surface-alt);
50
+ /* `border-collapse: collapse` drops the borders of a sticky cell — the collapsed edge belongs
51
+ to the table, which scrolls away. An inset shadow is painted by the cell itself and stays. */
52
+ border-bottom: 0;
53
+ box-shadow: inset 0 -1px 0 var(--loidolt-border);
54
+ }
55
+ .ldt-table__sort {
56
+ display: inline-flex;
57
+ align-items: center;
58
+ gap: 0.4rem;
59
+ border: 0;
60
+ padding: 0;
61
+ color: inherit;
62
+ background: transparent;
63
+ font: inherit;
64
+ letter-spacing: inherit;
65
+ text-transform: inherit;
66
+ cursor: pointer;
67
+ }
68
+ .ldt-table__sort:hover {
69
+ color: var(--loidolt-text);
70
+ }
71
+ /* A CSS triangle rather than a glyph: no icon dependency, and nothing that can fall back to
72
+ tofu in a font that lacks the arrow. */
73
+ .ldt-table__sort-indicator {
74
+ width: 0;
75
+ height: 0;
76
+ border-inline: 0.3rem solid transparent;
77
+ border-bottom: 0.34rem solid currentColor;
78
+ opacity: 0;
79
+ }
80
+ .ldt-table__sort:hover .ldt-table__sort-indicator {
81
+ opacity: 0.4;
82
+ }
83
+ .ldt-table__sort[data-sort='ascending'] .ldt-table__sort-indicator,
84
+ .ldt-table__sort[data-sort='descending'] .ldt-table__sort-indicator {
85
+ opacity: 1;
86
+ }
87
+ .ldt-table__sort[data-sort='descending'] .ldt-table__sort-indicator {
88
+ transform: rotate(180deg);
89
+ }
90
+ .ldt-table__empty td {
91
+ padding: 0;
92
+ border-bottom: 0;
93
+ }
94
+ /* Inside a table the surrounding rule is already the boundary; a second dashed one reads as a
95
+ rendering fault. */
96
+ .ldt-table__empty .ldt-empty-state {
97
+ border: 0;
98
+ }
99
+ .ldt-empty-state {
100
+ display: grid;
101
+ justify-items: center;
102
+ gap: 0.5rem;
103
+ padding: var(--loidolt-space-10) var(--loidolt-space-6);
104
+ border: var(--loidolt-border-width) dashed var(--loidolt-border);
105
+ color: var(--loidolt-text-muted);
106
+ background: var(--loidolt-surface);
107
+ text-align: center;
108
+ }
109
+ .ldt-empty-state__icon {
110
+ color: var(--loidolt-text-muted);
111
+ font-size: var(--loidolt-font-size-xl);
112
+ line-height: 1;
113
+ }
114
+ .ldt-empty-state__title {
115
+ margin: 0;
116
+ color: var(--loidolt-text);
117
+ font-size: var(--loidolt-font-size-md);
118
+ }
119
+ .ldt-empty-state__description {
120
+ max-width: 42rem;
121
+ margin: 0;
122
+ }
123
+ .ldt-empty-state__actions {
124
+ display: flex;
125
+ flex-wrap: wrap;
126
+ justify-content: center;
127
+ gap: var(--loidolt-space-2);
128
+ margin-top: var(--loidolt-space-2);
129
+ }
130
+ /* Trailing action cell in a data table: content hugs the right edge, inline forms welcome. */
131
+ .ldt-table__actions {
132
+ text-align: end;
133
+ white-space: nowrap;
134
+ }
135
+ .ldt-table__actions form {
136
+ display: inline;
137
+ }
138
+ .ldt-table__actions > :not(:first-child) {
139
+ margin-inline-start: var(--loidolt-space-2);
140
+ }
141
+ /* A message thread: author + meta line over a pre-wrapped body, optional numbered marker. */
142
+ .ldt-comment-list {
143
+ list-style: none;
144
+ margin: 0;
145
+ padding: 0;
146
+ display: flex;
147
+ flex-direction: column;
148
+ gap: var(--loidolt-space-2);
149
+ }
150
+ .ldt-comment {
151
+ display: flex;
152
+ align-items: flex-start;
153
+ gap: var(--loidolt-space-2);
154
+ padding: var(--loidolt-space-1) var(--loidolt-space-2);
155
+ }
156
+ .ldt-comment--active {
157
+ background: color-mix(in srgb, var(--loidolt-accent) 12%, transparent);
158
+ }
159
+ .ldt-comment__content {
160
+ min-width: 0;
161
+ }
162
+ .ldt-comment__meta {
163
+ color: var(--loidolt-text-muted);
164
+ font-size: var(--loidolt-font-size-sm);
165
+ }
166
+ .ldt-comment__body {
167
+ margin: 0;
168
+ white-space: pre-wrap;
169
+ overflow-wrap: anywhere;
170
+ }
171
+ /* A monospace block for tokens and URLs — select-all text, optional one-press copy. */
172
+ .ldt-code-block {
173
+ display: flex;
174
+ align-items: center;
175
+ gap: var(--loidolt-space-2);
176
+ padding: var(--loidolt-space-2) var(--loidolt-space-3);
177
+ border: var(--loidolt-border-width) solid var(--loidolt-border);
178
+ background: var(--loidolt-surface-sunken);
179
+ }
180
+ .ldt-code-block__code {
181
+ flex: 1;
182
+ min-width: 0;
183
+ font-family: var(--loidolt-font-mono);
184
+ font-size: var(--loidolt-font-size-sm);
185
+ word-break: break-all;
186
+ }
187
+ .ldt-code-block--selectable .ldt-code-block__code {
188
+ user-select: all;
189
+ }
190
+ /* Previous/next through an ordered set with an "n of N" readout — Pagination's sibling. */
191
+ .ldt-record-stepper {
192
+ display: inline-flex;
193
+ align-items: center;
194
+ gap: var(--loidolt-space-1);
195
+ }
196
+ .ldt-record-stepper__count {
197
+ white-space: nowrap;
198
+ }
@@ -0,0 +1,177 @@
1
+ .ldt-alert {
2
+ padding: 0.8rem 1rem;
3
+ border: var(--loidolt-border-width) solid var(--loidolt-border);
4
+ border-left: 3px solid var(--loidolt-info);
5
+ color: var(--loidolt-text);
6
+ background: var(--loidolt-surface);
7
+ }
8
+ .ldt-alert--info {
9
+ border-left-color: var(--loidolt-info);
10
+ }
11
+ .ldt-alert--success {
12
+ border-left-color: var(--loidolt-success);
13
+ }
14
+ .ldt-alert--warning {
15
+ border-left-color: var(--loidolt-warning);
16
+ }
17
+ .ldt-alert--error {
18
+ border-left-color: var(--loidolt-danger);
19
+ }
20
+ .ldt-alert__title {
21
+ margin: 0 0 0.2rem;
22
+ font: var(--loidolt-font-size-sm) var(--loidolt-font-utility);
23
+ letter-spacing: 0.06em;
24
+ text-transform: uppercase;
25
+ }
26
+ .ldt-alert__description {
27
+ margin: 0;
28
+ color: var(--loidolt-text-muted);
29
+ }
30
+ .ldt-progress {
31
+ width: 100%;
32
+ height: 0.35rem;
33
+ overflow: hidden;
34
+ background: var(--loidolt-border-soft);
35
+ }
36
+ .ldt-progress__bar {
37
+ width: 100%;
38
+ height: 100%;
39
+ background: var(--loidolt-accent);
40
+ /*
41
+ * Scale rather than animate `width`: a width transition relayouts on every frame, while a
42
+ * transform is composited. `--ldt-progress-value` is the 0..1 fraction, set by the component.
43
+ */
44
+ transform: scaleX(var(--ldt-progress-value, 0));
45
+ transform-origin: left center;
46
+ transition: transform var(--loidolt-motion-normal) var(--loidolt-motion-easing);
47
+ }
48
+ .ldt-progress--indeterminate .ldt-progress__bar {
49
+ width: 40%;
50
+ transform: none;
51
+ animation: ldt-progress-slide 1.2s var(--loidolt-motion-easing) infinite;
52
+ }
53
+ @keyframes ldt-progress-slide {
54
+ from {
55
+ transform: translateX(-100%);
56
+ }
57
+ to {
58
+ transform: translateX(250%);
59
+ }
60
+ }
61
+ .ldt-skeleton {
62
+ min-height: 1rem;
63
+ background: linear-gradient(
64
+ 90deg,
65
+ var(--loidolt-border-soft),
66
+ var(--loidolt-surface),
67
+ var(--loidolt-border-soft)
68
+ );
69
+ background-size: 200% 100%;
70
+ animation: ldt-shimmer 1.5s linear infinite;
71
+ }
72
+ @keyframes ldt-shimmer {
73
+ to {
74
+ background-position: -200% 0;
75
+ }
76
+ }
77
+ .ldt-toast-viewport {
78
+ position: fixed;
79
+ z-index: var(--loidolt-z-toast);
80
+ right: 1rem;
81
+ bottom: 1rem;
82
+ display: grid;
83
+ width: min(24rem, calc(100vw - 2rem));
84
+ gap: 0.5rem;
85
+ }
86
+ .ldt-toast {
87
+ display: grid;
88
+ grid-template-columns: minmax(0, 1fr) auto;
89
+ gap: 0.4rem 1rem;
90
+ padding: 0.8rem 1rem;
91
+ border: var(--loidolt-border-width) solid var(--loidolt-border-strong);
92
+ border-left: 3px solid var(--loidolt-border-strong);
93
+ color: var(--loidolt-text);
94
+ background: var(--loidolt-surface);
95
+ box-shadow: var(--loidolt-shadow-popover);
96
+ }
97
+ .ldt-toast--info {
98
+ border-left-color: var(--loidolt-info);
99
+ }
100
+ .ldt-toast--success {
101
+ border-left-color: var(--loidolt-success);
102
+ }
103
+ .ldt-toast--warning {
104
+ border-left-color: var(--loidolt-warning);
105
+ }
106
+ .ldt-toast--error {
107
+ border-left-color: var(--loidolt-danger);
108
+ }
109
+ .ldt-toast__title {
110
+ font: var(--loidolt-font-size-sm) var(--loidolt-font-utility);
111
+ letter-spacing: 0.06em;
112
+ text-transform: uppercase;
113
+ }
114
+ .ldt-toast__description {
115
+ grid-column: 1;
116
+ margin: 0;
117
+ color: var(--loidolt-text-muted);
118
+ }
119
+ .ldt-toast__dismiss {
120
+ grid-area: 1 / 2 / span 2 / auto;
121
+ align-self: start;
122
+ }
123
+ /* A colored dot that reports state where a Badge would be too loud. */
124
+ .ldt-status-dot {
125
+ display: inline-block;
126
+ width: 0.6rem;
127
+ height: 0.6rem;
128
+ border-radius: var(--loidolt-border-radius-pill);
129
+ background: var(--loidolt-text-muted);
130
+ opacity: 0.5;
131
+ flex: none;
132
+ }
133
+ .ldt-status-dot--accent {
134
+ background: var(--loidolt-accent);
135
+ opacity: 1;
136
+ }
137
+ .ldt-status-dot--success {
138
+ background: var(--loidolt-success);
139
+ opacity: 1;
140
+ }
141
+ .ldt-status-dot--warning {
142
+ background: var(--loidolt-warning);
143
+ opacity: 1;
144
+ }
145
+ .ldt-status-dot--error {
146
+ background: var(--loidolt-danger);
147
+ opacity: 1;
148
+ }
149
+ .ldt-status-dot--info {
150
+ background: var(--loidolt-info);
151
+ opacity: 1;
152
+ }
153
+ /* A numbered accent chip — dot form for lists, pin form for points on a canvas. */
154
+ .ldt-marker {
155
+ display: inline-flex;
156
+ align-items: center;
157
+ justify-content: center;
158
+ min-width: 1.25rem;
159
+ height: 1.25rem;
160
+ padding: 0 0.25rem;
161
+ border-radius: var(--loidolt-border-radius-pill);
162
+ color: var(--loidolt-on-accent);
163
+ background: var(--loidolt-accent);
164
+ font-size: var(--loidolt-font-size-xs);
165
+ font-weight: 700;
166
+ white-space: nowrap;
167
+ flex: none;
168
+ }
169
+ .ldt-marker--pin {
170
+ min-width: 1.5rem;
171
+ height: 1.5rem;
172
+ border-radius: 50% 50% 50% 0;
173
+ box-shadow: var(--loidolt-shadow-popover);
174
+ }
175
+ .ldt-marker--active {
176
+ outline: 2px solid var(--loidolt-surface);
177
+ }