@fluixi-ui/tokens 0.4.0-alpha.1 → 0.4.0-alpha.10
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/dist/css/accordion.css +81 -3
- package/dist/css/alert.css +52 -23
- package/dist/css/avatar.css +19 -0
- package/dist/css/badge.css +129 -5
- package/dist/css/base.css +3 -1
- package/dist/css/bottom-sheet.css +4 -4
- package/dist/css/breadcrumb.css +1 -1
- package/dist/css/bulk-editor.css +2 -17
- package/dist/css/button.css +76 -16
- package/dist/css/calendar.css +9 -7
- package/dist/css/card.css +5 -1
- package/dist/css/carousel.css +80 -0
- package/dist/css/checkbox-radio.css +55 -1
- package/dist/css/date-field.css +1 -1
- package/dist/css/date-picker.css +2 -2
- package/dist/css/descriptions.css +29 -0
- package/dist/css/drawer.css +4 -4
- package/dist/css/empty.css +40 -0
- package/dist/css/fab.css +48 -0
- package/dist/css/field-variants.css +139 -0
- package/dist/css/file-upload.css +40 -0
- package/dist/css/glow.css +127 -0
- package/dist/css/icon-button.css +42 -0
- package/dist/css/image.css +67 -0
- package/dist/css/input.css +15 -0
- package/dist/css/layout.css +10 -0
- package/dist/css/navigation-menu.css +8 -7
- package/dist/css/number-field.css +3 -3
- package/dist/css/pagination.css +3 -3
- package/dist/css/parts/listbox.css +9 -4
- package/dist/css/pin-input.css +40 -0
- package/dist/css/progress.css +56 -0
- package/dist/css/rating.css +59 -0
- package/dist/css/scroll-area.css +44 -0
- package/dist/css/select-combobox.css +19 -4
- package/dist/css/sidenav.css +180 -10
- package/dist/css/slider.css +4 -4
- package/dist/css/speed-dial.css +85 -0
- package/dist/css/statistic.css +34 -0
- package/dist/css/stepper.css +3 -3
- package/dist/css/surface.css +71 -0
- package/dist/css/switch.css +12 -0
- package/dist/css/table.css +11 -0
- package/dist/css/tabs.css +10 -1
- package/dist/css/tags-input.css +91 -0
- package/dist/css/timeline.css +59 -0
- package/dist/css/toggle.css +6 -0
- package/dist/css/tree-view.css +70 -0
- package/dist/lib/css/index.d.ts +8 -12
- package/dist/lib/css/index.d.ts.map +1 -1
- package/dist/lib/css/index.js +19 -7
- package/dist/lib/reference/colors.d.ts.map +1 -1
- package/dist/lib/reference/colors.js +15 -1
- package/dist/preset.css +20 -1
- package/dist/tokens.css +52 -4
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
@layer flx.components {
|
|
2
|
+
/* ----------------------------------------------------------------------------
|
|
3
|
+
* Carousel
|
|
4
|
+
* ------------------------------------------------------------------------- */
|
|
5
|
+
:where([data-carousel]) {
|
|
6
|
+
position: relative;
|
|
7
|
+
}
|
|
8
|
+
:where([data-carousel]:focus-visible) {
|
|
9
|
+
outline: none;
|
|
10
|
+
border-radius: var(--flx-ui-radius-md);
|
|
11
|
+
box-shadow: var(--flx-ui-ctl-ring);
|
|
12
|
+
}
|
|
13
|
+
:where([data-carousel-viewport]) {
|
|
14
|
+
overflow: hidden;
|
|
15
|
+
/* Let the page scroll along the cross axis; the carousel owns the main axis. */
|
|
16
|
+
touch-action: pan-y;
|
|
17
|
+
}
|
|
18
|
+
:where([data-carousel-viewport][data-orientation='vertical']) {
|
|
19
|
+
touch-action: pan-x;
|
|
20
|
+
}
|
|
21
|
+
:where([data-carousel-track]) {
|
|
22
|
+
display: flex;
|
|
23
|
+
transition: transform var(--flx-ui-duration-normal, 250ms) var(--flx-ui-ease-standard, ease);
|
|
24
|
+
}
|
|
25
|
+
/* Follow the pointer 1:1 while dragging — no animated lag. */
|
|
26
|
+
:where([data-carousel-track][data-dragging]) {
|
|
27
|
+
transition: none;
|
|
28
|
+
user-select: none;
|
|
29
|
+
}
|
|
30
|
+
:where([data-carousel-track][data-orientation='vertical']) {
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
height: 100%;
|
|
33
|
+
}
|
|
34
|
+
/* One slide per view; the track translates by whole viewports. */
|
|
35
|
+
:where([data-carousel-slide]) {
|
|
36
|
+
flex: 0 0 100%;
|
|
37
|
+
min-width: 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
:where([data-carousel-dots]) {
|
|
41
|
+
display: flex;
|
|
42
|
+
justify-content: center;
|
|
43
|
+
gap: var(--flx-ui-space-2);
|
|
44
|
+
margin-top: var(--flx-ui-space-3);
|
|
45
|
+
}
|
|
46
|
+
:where([data-carousel-dot]) {
|
|
47
|
+
width: 8px;
|
|
48
|
+
height: 8px;
|
|
49
|
+
padding: 0;
|
|
50
|
+
border: 0;
|
|
51
|
+
border-radius: var(--flx-ui-radius-full, 999px);
|
|
52
|
+
background: var(--flx-ui-color-border-strong);
|
|
53
|
+
cursor: pointer;
|
|
54
|
+
transition: background var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
|
|
55
|
+
}
|
|
56
|
+
:where([data-carousel-dot]:hover) {
|
|
57
|
+
background: var(--flx-ui-color-fg-subtle);
|
|
58
|
+
}
|
|
59
|
+
:where([data-carousel-dot][data-active]) {
|
|
60
|
+
background: var(--flx-ui-color-primary);
|
|
61
|
+
}
|
|
62
|
+
:where([data-carousel-dot]:focus-visible) {
|
|
63
|
+
outline: none;
|
|
64
|
+
box-shadow: var(--flx-ui-ctl-ring);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
:where([data-carousel-prev], [data-carousel-next]) {
|
|
68
|
+
cursor: pointer;
|
|
69
|
+
}
|
|
70
|
+
:where([data-carousel-prev][disabled], [data-carousel-next][disabled]) {
|
|
71
|
+
opacity: var(--flx-ui-opacity-disabled);
|
|
72
|
+
cursor: not-allowed;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@media (prefers-reduced-motion: reduce) {
|
|
77
|
+
:where([data-carousel-track]) {
|
|
78
|
+
transition: none;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
background: var(--flx-ui-color-primary);
|
|
24
24
|
border-color: var(--flx-ui-color-primary);
|
|
25
25
|
}
|
|
26
|
+
:where([role='checkbox']:active:not([data-disabled])) {
|
|
27
|
+
transform: scale(0.92);
|
|
28
|
+
}
|
|
26
29
|
:where([role='checkbox'][data-state='checked'])::after {
|
|
27
30
|
content: '';
|
|
28
31
|
width: 5px;
|
|
@@ -31,6 +34,8 @@
|
|
|
31
34
|
border: solid currentColor;
|
|
32
35
|
border-width: 0 2px 2px 0;
|
|
33
36
|
transform: rotate(45deg);
|
|
37
|
+
transform-origin: center;
|
|
38
|
+
animation: flx-ui-check-pop var(--flx-ui-duration-normal) var(--flx-ui-ease-emphasized);
|
|
34
39
|
}
|
|
35
40
|
:where([role='checkbox'][data-state='indeterminate'])::after {
|
|
36
41
|
content: '';
|
|
@@ -38,6 +43,7 @@
|
|
|
38
43
|
height: 2px;
|
|
39
44
|
background: currentColor;
|
|
40
45
|
border-radius: 1px;
|
|
46
|
+
animation: flx-ui-scale-pop var(--flx-ui-duration-fast) var(--flx-ui-ease-emphasized);
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
:where([role='radio']) {
|
|
@@ -50,7 +56,12 @@
|
|
|
50
56
|
border: var(--flx-ui-border-width-thin) solid var(--flx-ui-color-border-strong);
|
|
51
57
|
border-radius: var(--flx-ui-radius-full);
|
|
52
58
|
background: var(--flx-ui-color-bg);
|
|
53
|
-
transition:
|
|
59
|
+
transition:
|
|
60
|
+
border-color var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
|
|
61
|
+
border-width var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
|
|
62
|
+
}
|
|
63
|
+
:where([role='radio']:active:not([data-disabled])) {
|
|
64
|
+
transform: scale(0.92);
|
|
54
65
|
}
|
|
55
66
|
:where([role='radio'][data-state='checked']) {
|
|
56
67
|
border-color: var(--flx-ui-color-primary);
|
|
@@ -62,5 +73,48 @@
|
|
|
62
73
|
inset: 3px;
|
|
63
74
|
border-radius: var(--flx-ui-radius-full);
|
|
64
75
|
background: var(--flx-ui-color-primary);
|
|
76
|
+
animation: flx-ui-scale-pop var(--flx-ui-duration-normal) var(--flx-ui-ease-emphasized);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
:where([role='checkbox']:hover:not([data-disabled])),
|
|
80
|
+
:where([role='radio']:hover:not([data-disabled])) {
|
|
81
|
+
border-color: var(--flx-ui-color-primary);
|
|
82
|
+
}
|
|
83
|
+
:where([role='checkbox']:focus-visible),
|
|
84
|
+
:where([role='radio']:focus-visible) {
|
|
85
|
+
outline: none;
|
|
86
|
+
box-shadow: var(--flx-ui-ctl-ring);
|
|
87
|
+
}
|
|
88
|
+
:where([role='checkbox'][data-disabled]),
|
|
89
|
+
:where([role='radio'][data-disabled]) {
|
|
90
|
+
opacity: var(--flx-ui-opacity-disabled);
|
|
91
|
+
cursor: not-allowed;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@keyframes flx-ui-check-pop {
|
|
95
|
+
from {
|
|
96
|
+
transform: rotate(45deg) scale(0.3);
|
|
97
|
+
opacity: 0;
|
|
98
|
+
}
|
|
99
|
+
to {
|
|
100
|
+
transform: rotate(45deg) scale(1);
|
|
101
|
+
opacity: 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
@keyframes flx-ui-scale-pop {
|
|
105
|
+
from {
|
|
106
|
+
transform: scale(0);
|
|
107
|
+
opacity: 0;
|
|
108
|
+
}
|
|
109
|
+
to {
|
|
110
|
+
transform: scale(1);
|
|
111
|
+
opacity: 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
@media (prefers-reduced-motion: reduce) {
|
|
115
|
+
:where([role='checkbox'][data-state]::after),
|
|
116
|
+
:where([role='radio'][data-state='checked'])::after {
|
|
117
|
+
animation: none;
|
|
118
|
+
}
|
|
65
119
|
}
|
|
66
120
|
}
|
package/dist/css/date-field.css
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
font-size: var(--flx-ui-font-size-sm);
|
|
12
12
|
font-weight: var(--flx-ui-font-weight-medium);
|
|
13
13
|
color: var(--flx-ui-color-fg);
|
|
14
|
-
line-height: var(--flx-ui-
|
|
14
|
+
line-height: var(--flx-ui-line-height-none);
|
|
15
15
|
}
|
|
16
16
|
:where([data-date-field-input]) {
|
|
17
17
|
display: inline-flex;
|
package/dist/css/date-picker.css
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
font-size: var(--flx-ui-font-size-sm);
|
|
12
12
|
font-weight: var(--flx-ui-font-weight-medium);
|
|
13
13
|
color: var(--flx-ui-color-fg);
|
|
14
|
-
line-height: var(--flx-ui-
|
|
14
|
+
line-height: var(--flx-ui-line-height-none);
|
|
15
15
|
}
|
|
16
16
|
/* The control box owns the border; the embedded field input goes borderless. */
|
|
17
17
|
:where([data-date-picker-control]) {
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
border-radius: var(--flx-ui-radius-lg);
|
|
72
72
|
background: var(--flx-ui-color-bg);
|
|
73
73
|
box-shadow: var(--flx-ui-shadow-lg, 0 10px 30px rgba(17, 24, 39, 0.12));
|
|
74
|
-
animation: flx-ui-pop-in var(--flx-ui-duration-fast) var(--flx-ui-
|
|
74
|
+
animation: flx-ui-pop-in var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
|
|
75
75
|
}
|
|
76
76
|
@media (prefers-reduced-motion: reduce) {
|
|
77
77
|
:where([data-date-picker-content]) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
@layer flx.components {
|
|
2
|
+
/* Descriptions (from @fluixi-ui/descriptions). A key/value grid; `columns` drives
|
|
3
|
+
the track count and an item's `span` stretches it. */
|
|
4
|
+
:where([data-descriptions]) {
|
|
5
|
+
display: grid;
|
|
6
|
+
grid-template-columns: repeat(var(--flx-descriptions-columns, 1), minmax(0, 1fr));
|
|
7
|
+
gap: var(--flx-ui-space-4) var(--flx-ui-space-6);
|
|
8
|
+
margin: 0;
|
|
9
|
+
}
|
|
10
|
+
:where([data-descriptions-item]) {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
gap: var(--flx-ui-space-1);
|
|
14
|
+
min-width: 0;
|
|
15
|
+
}
|
|
16
|
+
:where([data-descriptions-term]) {
|
|
17
|
+
font-size: var(--flx-ui-font-size-xs);
|
|
18
|
+
font-weight: var(--flx-ui-font-weight-medium);
|
|
19
|
+
color: var(--flx-ui-color-fg-subtle);
|
|
20
|
+
letter-spacing: var(--flx-ui-letter-spacing-wide);
|
|
21
|
+
}
|
|
22
|
+
:where([data-descriptions-detail]) {
|
|
23
|
+
margin: 0;
|
|
24
|
+
font-size: var(--flx-ui-font-size-sm);
|
|
25
|
+
color: var(--flx-ui-color-fg);
|
|
26
|
+
min-width: 0;
|
|
27
|
+
overflow-wrap: anywhere;
|
|
28
|
+
}
|
|
29
|
+
}
|
package/dist/css/drawer.css
CHANGED
|
@@ -70,14 +70,14 @@
|
|
|
70
70
|
align-items: flex-start;
|
|
71
71
|
justify-content: space-between;
|
|
72
72
|
gap: var(--flx-ui-space-3);
|
|
73
|
-
padding: var(--flx-ui-space-
|
|
73
|
+
padding: var(--flx-ui-space-4);
|
|
74
74
|
border-bottom: var(--flx-ui-border-width-thin) solid var(--flx-ui-color-border);
|
|
75
75
|
}
|
|
76
76
|
:where([data-drawer-title]) {
|
|
77
77
|
margin: 0;
|
|
78
78
|
font-size: var(--flx-ui-font-size-lg);
|
|
79
79
|
font-weight: var(--flx-ui-font-weight-semibold);
|
|
80
|
-
line-height: var(--flx-ui-
|
|
80
|
+
line-height: var(--flx-ui-line-height-tight);
|
|
81
81
|
color: var(--flx-ui-color-fg);
|
|
82
82
|
}
|
|
83
83
|
:where([data-drawer-description]) {
|
|
@@ -89,14 +89,14 @@
|
|
|
89
89
|
flex: 1;
|
|
90
90
|
min-height: 0;
|
|
91
91
|
overflow-y: auto;
|
|
92
|
-
padding: var(--flx-ui-space-
|
|
92
|
+
padding: var(--flx-ui-space-4);
|
|
93
93
|
}
|
|
94
94
|
:where([data-drawer-footer]) {
|
|
95
95
|
display: flex;
|
|
96
96
|
align-items: center;
|
|
97
97
|
justify-content: flex-end;
|
|
98
98
|
gap: var(--flx-ui-space-2);
|
|
99
|
-
padding: var(--flx-ui-space-
|
|
99
|
+
padding: var(--flx-ui-space-4);
|
|
100
100
|
border-top: var(--flx-ui-border-width-thin) solid var(--flx-ui-color-border);
|
|
101
101
|
}
|
|
102
102
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
@layer flx.components {
|
|
2
|
+
/* Empty (from @fluixi-ui/empty). A centred empty-state: icon tile, title,
|
|
3
|
+
description, and an actions row. */
|
|
4
|
+
:where([data-empty]) {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
text-align: center;
|
|
10
|
+
gap: var(--flx-ui-space-2);
|
|
11
|
+
padding: var(--flx-ui-space-8) var(--flx-ui-space-4);
|
|
12
|
+
color: var(--flx-ui-color-fg-subtle);
|
|
13
|
+
}
|
|
14
|
+
:where([data-empty-icon]) {
|
|
15
|
+
display: inline-flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
justify-content: center;
|
|
18
|
+
width: 44px;
|
|
19
|
+
height: 44px;
|
|
20
|
+
margin-bottom: var(--flx-ui-space-1);
|
|
21
|
+
border-radius: var(--flx-ui-radius-lg);
|
|
22
|
+
background: var(--flx-ui-color-bg-subtle);
|
|
23
|
+
color: var(--flx-ui-color-fg-subtle);
|
|
24
|
+
}
|
|
25
|
+
:where([data-empty-title]) {
|
|
26
|
+
font-size: var(--flx-ui-font-size-sm);
|
|
27
|
+
font-weight: var(--flx-ui-font-weight-semibold);
|
|
28
|
+
color: var(--flx-ui-color-fg);
|
|
29
|
+
}
|
|
30
|
+
:where([data-empty-description]) {
|
|
31
|
+
font-size: var(--flx-ui-font-size-sm);
|
|
32
|
+
color: var(--flx-ui-color-fg-subtle);
|
|
33
|
+
max-width: 42ch;
|
|
34
|
+
}
|
|
35
|
+
:where([data-empty-actions]) {
|
|
36
|
+
margin-top: var(--flx-ui-space-3);
|
|
37
|
+
display: flex;
|
|
38
|
+
gap: var(--flx-ui-space-2);
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/css/fab.css
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
@layer flx.components {
|
|
2
|
+
/* ----------------------------------------------------------------------------
|
|
3
|
+
* FAB — a circular, elevated [data-variant] button. Colours / states / ripple
|
|
4
|
+
* come from the button slice + skin; this owns size, shape and elevation.
|
|
5
|
+
* ------------------------------------------------------------------------- */
|
|
6
|
+
:where([data-fab]) {
|
|
7
|
+
width: 56px;
|
|
8
|
+
height: 56px;
|
|
9
|
+
padding: 0;
|
|
10
|
+
min-width: 0;
|
|
11
|
+
flex: none;
|
|
12
|
+
border-radius: var(--flx-ui-radius-full);
|
|
13
|
+
box-shadow: var(--flx-ui-shadow-lg);
|
|
14
|
+
}
|
|
15
|
+
:where([data-fab]:hover:not([data-disabled])) {
|
|
16
|
+
box-shadow: var(--flx-ui-shadow-xl);
|
|
17
|
+
}
|
|
18
|
+
:where([data-fab][data-fab-size='sm']) {
|
|
19
|
+
width: 40px;
|
|
20
|
+
height: 40px;
|
|
21
|
+
}
|
|
22
|
+
:where([data-fab][data-fab-size='lg']) {
|
|
23
|
+
width: 64px;
|
|
24
|
+
height: 64px;
|
|
25
|
+
}
|
|
26
|
+
:where([data-fab] svg) {
|
|
27
|
+
width: 24px;
|
|
28
|
+
height: 24px;
|
|
29
|
+
}
|
|
30
|
+
:where([data-fab][data-fab-size='sm'] svg) {
|
|
31
|
+
width: 20px;
|
|
32
|
+
height: 20px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* Extended: a pill holding an icon plus a label. */
|
|
36
|
+
:where([data-fab][data-fab-extended]) {
|
|
37
|
+
width: auto;
|
|
38
|
+
min-width: 56px;
|
|
39
|
+
padding: 0 var(--flx-ui-space-5);
|
|
40
|
+
gap: var(--flx-ui-space-2);
|
|
41
|
+
border-radius: var(--flx-ui-radius-full);
|
|
42
|
+
font-weight: var(--flx-ui-font-weight-semibold);
|
|
43
|
+
}
|
|
44
|
+
:where([data-fab][data-fab-extended][data-fab-size='sm']) {
|
|
45
|
+
min-width: 40px;
|
|
46
|
+
padding: 0 var(--flx-ui-space-4);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
@layer flx.components {
|
|
2
|
+
/* ----------------------------------------------------------------------------
|
|
3
|
+
* Field variants — an appearance axis shared by every field control that opts in
|
|
4
|
+
* via `data-field-variant` (Input, the Select trigger `button[role=combobox]`,
|
|
5
|
+
* the Combobox control, a field-styled dropdown trigger). `default` (no attr)
|
|
6
|
+
* keeps each skin's baseline; `outlined` and `filled` are explicit Material-style
|
|
7
|
+
* modes that every skin renders.
|
|
8
|
+
*
|
|
9
|
+
* These deliberately DON'T use `:where()` — the `[data-field-variant]` attribute
|
|
10
|
+
* carries a little specificity so the variant wins over a control's own baseline
|
|
11
|
+
* slice regardless of import order (per-component self-imports dedupe to the
|
|
12
|
+
* first position). Consumer CSS is unlayered and still wins over this whole
|
|
13
|
+
* layer; the skins (a higher @layer) still override per skin.
|
|
14
|
+
* ------------------------------------------------------------------------- */
|
|
15
|
+
|
|
16
|
+
/* Give a plain trigger button (not the select's role=combobox, which already has
|
|
17
|
+
a field box) the field box model when it opts into a variant. */
|
|
18
|
+
button[data-field-variant]:not([role='combobox']) {
|
|
19
|
+
box-sizing: border-box;
|
|
20
|
+
display: inline-flex;
|
|
21
|
+
align-items: center;
|
|
22
|
+
gap: var(--flx-ui-space-2);
|
|
23
|
+
min-height: var(--flx-ui-size-md);
|
|
24
|
+
padding: 0 var(--flx-ui-space-3);
|
|
25
|
+
font: inherit;
|
|
26
|
+
color: var(--flx-ui-color-fg);
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
transition:
|
|
29
|
+
border-color var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
|
|
30
|
+
background var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
|
|
31
|
+
box-shadow var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Outlined — a bordered box on a transparent ground, with a more defined border
|
|
35
|
+
than the baseline so the two read as distinct. */
|
|
36
|
+
[data-field-variant='outlined'] {
|
|
37
|
+
background: transparent;
|
|
38
|
+
border: var(--flx-ui-border-width-thin) solid
|
|
39
|
+
color-mix(in oklab, var(--flx-ui-color-border-strong), var(--flx-ui-color-fg) 22%);
|
|
40
|
+
border-radius: var(--flx-ui-radius-md);
|
|
41
|
+
}
|
|
42
|
+
[data-field-variant='outlined']:hover:not(:focus):not(:focus-within):not([data-invalid]) {
|
|
43
|
+
border-color: var(--flx-ui-color-fg-muted);
|
|
44
|
+
}
|
|
45
|
+
[data-field-variant='outlined']:focus,
|
|
46
|
+
[data-field-variant='outlined']:focus-within {
|
|
47
|
+
outline: none;
|
|
48
|
+
border-color: var(--flx-ui-color-focus-ring);
|
|
49
|
+
box-shadow: var(--flx-ui-ctl-ring);
|
|
50
|
+
}
|
|
51
|
+
[data-field-variant='outlined'][data-invalid] {
|
|
52
|
+
border-color: var(--flx-ui-color-danger);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Filled — a tinted fill with a bottom rule that thickens/colours on focus. */
|
|
56
|
+
[data-field-variant='filled'] {
|
|
57
|
+
background: var(--flx-ui-color-bg-muted);
|
|
58
|
+
border: var(--flx-ui-border-width-thin) solid transparent;
|
|
59
|
+
border-bottom-color: var(--flx-ui-color-border-strong);
|
|
60
|
+
border-radius: var(--flx-ui-radius-md) var(--flx-ui-radius-md) 0 0;
|
|
61
|
+
}
|
|
62
|
+
[data-field-variant='filled']:hover:not(:focus):not(:focus-within):not([data-invalid]) {
|
|
63
|
+
background: color-mix(in oklab, var(--flx-ui-color-bg-muted), var(--flx-ui-color-fg) 4%);
|
|
64
|
+
}
|
|
65
|
+
[data-field-variant='filled']:focus,
|
|
66
|
+
[data-field-variant='filled']:focus-within {
|
|
67
|
+
outline: none;
|
|
68
|
+
border-bottom-width: 2px;
|
|
69
|
+
border-bottom-color: var(--flx-ui-color-focus-ring);
|
|
70
|
+
box-shadow: none;
|
|
71
|
+
}
|
|
72
|
+
[data-field-variant='filled'][data-invalid] {
|
|
73
|
+
border-bottom-color: var(--flx-ui-color-danger);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* ----------------------------------------------------------------------------
|
|
77
|
+
* Floating label — a frameless positioning wrapper. The INPUT keeps its own
|
|
78
|
+
* frame (base + variant + skin), so there's exactly one border and it always
|
|
79
|
+
* matches the active skin. The label overlays and lifts into the top strip on
|
|
80
|
+
* focus / when filled (driven by the input's :placeholder-shown). The input is
|
|
81
|
+
* taller with bottom-weighted text so the lifted label never overlaps the value.
|
|
82
|
+
* ------------------------------------------------------------------------- */
|
|
83
|
+
[data-field-float] {
|
|
84
|
+
position: relative;
|
|
85
|
+
display: block;
|
|
86
|
+
width: 100%;
|
|
87
|
+
}
|
|
88
|
+
[data-field-float] > [data-input] {
|
|
89
|
+
width: 100%;
|
|
90
|
+
min-height: calc(var(--flx-ui-size-md) + 14px);
|
|
91
|
+
padding-top: 20px;
|
|
92
|
+
padding-bottom: 4px;
|
|
93
|
+
}
|
|
94
|
+
/* Outlined notches the top border (label rides on the edge), so the value text
|
|
95
|
+
stays vertically centred rather than pushed down for an inside label. */
|
|
96
|
+
[data-field-float] > [data-input][data-field-variant='outlined'] {
|
|
97
|
+
padding-top: var(--flx-ui-space-2);
|
|
98
|
+
padding-bottom: var(--flx-ui-space-2);
|
|
99
|
+
}
|
|
100
|
+
[data-field-float] > [data-field-float-label] {
|
|
101
|
+
position: absolute;
|
|
102
|
+
left: var(--flx-ui-space-3);
|
|
103
|
+
top: 50%;
|
|
104
|
+
transform: translateY(-50%);
|
|
105
|
+
transform-origin: left top;
|
|
106
|
+
max-width: calc(100% - 2 * var(--flx-ui-space-3));
|
|
107
|
+
overflow: hidden;
|
|
108
|
+
text-overflow: ellipsis;
|
|
109
|
+
white-space: nowrap;
|
|
110
|
+
color: var(--flx-ui-color-fg-muted);
|
|
111
|
+
pointer-events: none;
|
|
112
|
+
transition:
|
|
113
|
+
top var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
|
|
114
|
+
transform var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
|
|
115
|
+
color var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
|
|
116
|
+
}
|
|
117
|
+
/* Lift to the top strip when focused or filled (default + filled: inside). */
|
|
118
|
+
[data-field-float] > [data-input]:focus ~ [data-field-float-label],
|
|
119
|
+
[data-field-float] > [data-input]:not(:placeholder-shown) ~ [data-field-float-label] {
|
|
120
|
+
top: 8px;
|
|
121
|
+
transform: translateY(0) scale(0.8);
|
|
122
|
+
}
|
|
123
|
+
/* Outlined lifts ONTO the top border and notches it (paints the surface behind
|
|
124
|
+
the label). Only outlined is reliably a transparent bordered box across skins. */
|
|
125
|
+
[data-field-float] > [data-input][data-field-variant='outlined']:focus ~ [data-field-float-label],
|
|
126
|
+
[data-field-float] > [data-input][data-field-variant='outlined']:not(:placeholder-shown) ~ [data-field-float-label] {
|
|
127
|
+
top: 0;
|
|
128
|
+
transform: translateY(-50%) scale(0.8);
|
|
129
|
+
padding: 0 var(--flx-ui-space-1);
|
|
130
|
+
background: var(--flx-ui-color-bg);
|
|
131
|
+
}
|
|
132
|
+
/* Accent the label only while the field is focused. */
|
|
133
|
+
[data-field-float] > [data-input]:focus ~ [data-field-float-label] {
|
|
134
|
+
color: var(--flx-ui-color-focus-ring);
|
|
135
|
+
}
|
|
136
|
+
[data-field-float] > [data-input][data-invalid] ~ [data-field-float-label] {
|
|
137
|
+
color: var(--flx-ui-color-danger);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
@layer flx.components {
|
|
2
|
+
/* File upload (from @fluixi-ui/file-upload). The dropzone is a dashed drop area
|
|
3
|
+
that also opens the file dialog on click; it lifts while a drag is over it. */
|
|
4
|
+
:where([data-file-upload-dropzone]) {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
align-items: center;
|
|
8
|
+
justify-content: center;
|
|
9
|
+
gap: var(--flx-ui-space-2);
|
|
10
|
+
padding: var(--flx-ui-space-6) var(--flx-ui-space-4);
|
|
11
|
+
text-align: center;
|
|
12
|
+
font-size: var(--flx-ui-font-size-sm);
|
|
13
|
+
color: var(--flx-ui-color-fg-muted);
|
|
14
|
+
background: var(--flx-ui-color-bg-subtle);
|
|
15
|
+
border: var(--flx-ui-border-width-thin) dashed var(--flx-ui-color-border-strong);
|
|
16
|
+
border-radius: var(--flx-ui-radius-md);
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
transition:
|
|
19
|
+
border-color var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
|
|
20
|
+
background var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
|
|
21
|
+
color var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
|
|
22
|
+
}
|
|
23
|
+
:where([data-file-upload-dropzone]:hover),
|
|
24
|
+
:where([data-file-upload-dropzone][data-dragging]) {
|
|
25
|
+
border-color: var(--flx-ui-color-focus-ring);
|
|
26
|
+
background: var(--flx-ui-color-primary-subtle);
|
|
27
|
+
color: var(--flx-ui-color-fg);
|
|
28
|
+
}
|
|
29
|
+
:where([data-file-upload-dropzone][data-dragging]) {
|
|
30
|
+
border-style: solid;
|
|
31
|
+
}
|
|
32
|
+
:where([data-file-upload-dropzone]:focus-visible) {
|
|
33
|
+
outline: none;
|
|
34
|
+
box-shadow: var(--flx-ui-ctl-ring);
|
|
35
|
+
}
|
|
36
|
+
:where([data-file-upload-dropzone][data-disabled]) {
|
|
37
|
+
opacity: 0.6;
|
|
38
|
+
cursor: not-allowed;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
@layer flx.components {
|
|
2
|
+
/* ----------------------------------------------------------------------------
|
|
3
|
+
* Glow — an animated gradient border ring, opt-in via `data-glow` on any element
|
|
4
|
+
* (button, surface, card…). The ::after covers the border box and is masked to
|
|
5
|
+
* just the ring, so the element keeps its own fill and centred content. Colours
|
|
6
|
+
* follow the accent and are overridable per-instance with --flx-glow-from / -via
|
|
7
|
+
* / -to (and rest speed via --flx-glow-speed). `data-glow-side` restricts it to
|
|
8
|
+
* one edge that slides to the opposite side on hover.
|
|
9
|
+
* ------------------------------------------------------------------------- */
|
|
10
|
+
:where([data-glow]) {
|
|
11
|
+
position: relative;
|
|
12
|
+
border-color: transparent;
|
|
13
|
+
/* Show the edge ring even when the host would otherwise clip (e.g. a card). */
|
|
14
|
+
overflow: visible;
|
|
15
|
+
}
|
|
16
|
+
:where([data-glow])::after {
|
|
17
|
+
content: '';
|
|
18
|
+
position: absolute;
|
|
19
|
+
inset: 0;
|
|
20
|
+
border-radius: inherit;
|
|
21
|
+
padding: 2px;
|
|
22
|
+
background: conic-gradient(
|
|
23
|
+
from var(--flx-angle, 0deg),
|
|
24
|
+
var(--flx-glow-from, var(--flx-ui-color-primary)),
|
|
25
|
+
var(--flx-glow-via, var(--flx-ui-accent-5)),
|
|
26
|
+
var(--flx-glow-to, var(--flx-glow-from, var(--flx-ui-color-primary)))
|
|
27
|
+
);
|
|
28
|
+
-webkit-mask:
|
|
29
|
+
linear-gradient(#000 0 0) content-box,
|
|
30
|
+
linear-gradient(#000 0 0);
|
|
31
|
+
-webkit-mask-composite: xor;
|
|
32
|
+
mask:
|
|
33
|
+
linear-gradient(#000 0 0) content-box,
|
|
34
|
+
linear-gradient(#000 0 0);
|
|
35
|
+
mask-composite: exclude;
|
|
36
|
+
animation: flx-border-spin var(--flx-glow-speed, 4s) linear infinite;
|
|
37
|
+
transition: filter var(--flx-ui-duration-normal) var(--flx-ui-ease-standard);
|
|
38
|
+
pointer-events: none;
|
|
39
|
+
}
|
|
40
|
+
/* Comes alive on hover: the ring spins faster and blooms in its accent colour. */
|
|
41
|
+
:where([data-glow]:hover:not([data-disabled]))::after {
|
|
42
|
+
--flx-glow-speed: 1.2s;
|
|
43
|
+
filter: drop-shadow(0 0 5px var(--flx-glow-via, var(--flx-ui-color-primary)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/* Single-edge glow: reuse the fixed full ring mask, but paint the colour as a
|
|
47
|
+
band positioned on one side, so only that edge (and its corners) lights up.
|
|
48
|
+
Because it's the *background* that's positioned, hover slides it to the opposite
|
|
49
|
+
side smoothly. Positional, so no spin. */
|
|
50
|
+
:where([data-glow][data-glow-side])::after {
|
|
51
|
+
animation: none;
|
|
52
|
+
background-repeat: no-repeat;
|
|
53
|
+
/* A soft resting glow so the lit edge reads as intentional even before hover. */
|
|
54
|
+
filter: drop-shadow(0 0 2px var(--flx-glow-via, var(--flx-ui-color-primary)));
|
|
55
|
+
transition:
|
|
56
|
+
background-position 450ms var(--flx-ui-ease-standard),
|
|
57
|
+
filter var(--flx-ui-duration-normal) var(--flx-ui-ease-standard);
|
|
58
|
+
}
|
|
59
|
+
/* Wider colour plateau (12%→88%) so the whole edge lights evenly and only the
|
|
60
|
+
far edge fades, rather than a thin central sliver. */
|
|
61
|
+
:where([data-glow][data-glow-side='top'])::after,
|
|
62
|
+
:where([data-glow][data-glow-side='bottom'])::after {
|
|
63
|
+
background-image: linear-gradient(
|
|
64
|
+
to bottom,
|
|
65
|
+
transparent 12%,
|
|
66
|
+
var(--flx-glow-from, var(--flx-ui-color-primary)) 38%,
|
|
67
|
+
var(--flx-glow-via, var(--flx-ui-accent-5)) 50%,
|
|
68
|
+
var(--flx-glow-from, var(--flx-ui-color-primary)) 62%,
|
|
69
|
+
transparent 88%
|
|
70
|
+
);
|
|
71
|
+
background-size: 100% 200%;
|
|
72
|
+
}
|
|
73
|
+
:where([data-glow][data-glow-side='left'])::after,
|
|
74
|
+
:where([data-glow][data-glow-side='right'])::after {
|
|
75
|
+
background-image: linear-gradient(
|
|
76
|
+
to right,
|
|
77
|
+
transparent 12%,
|
|
78
|
+
var(--flx-glow-from, var(--flx-ui-color-primary)) 38%,
|
|
79
|
+
var(--flx-glow-via, var(--flx-ui-accent-5)) 50%,
|
|
80
|
+
var(--flx-glow-from, var(--flx-ui-color-primary)) 62%,
|
|
81
|
+
transparent 88%
|
|
82
|
+
);
|
|
83
|
+
background-size: 200% 100%;
|
|
84
|
+
}
|
|
85
|
+
:where([data-glow][data-glow-side='bottom'])::after {
|
|
86
|
+
background-position: 50% 0%;
|
|
87
|
+
}
|
|
88
|
+
:where([data-glow][data-glow-side='bottom']:hover:not([data-disabled]))::after {
|
|
89
|
+
background-position: 50% 100%;
|
|
90
|
+
}
|
|
91
|
+
:where([data-glow][data-glow-side='top'])::after {
|
|
92
|
+
background-position: 50% 100%;
|
|
93
|
+
}
|
|
94
|
+
:where([data-glow][data-glow-side='top']:hover:not([data-disabled]))::after {
|
|
95
|
+
background-position: 50% 0%;
|
|
96
|
+
}
|
|
97
|
+
:where([data-glow][data-glow-side='left'])::after {
|
|
98
|
+
background-position: 100% 50%;
|
|
99
|
+
}
|
|
100
|
+
:where([data-glow][data-glow-side='left']:hover:not([data-disabled]))::after {
|
|
101
|
+
background-position: 0% 50%;
|
|
102
|
+
}
|
|
103
|
+
:where([data-glow][data-glow-side='right'])::after {
|
|
104
|
+
background-position: 0% 50%;
|
|
105
|
+
}
|
|
106
|
+
:where([data-glow][data-glow-side='right']:hover:not([data-disabled]))::after {
|
|
107
|
+
background-position: 100% 50%;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@media (prefers-reduced-motion: reduce) {
|
|
111
|
+
:where([data-glow])::after {
|
|
112
|
+
animation: none;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Registered so the angle can actually animate; unlayered like other keyframes. */
|
|
118
|
+
@property --flx-angle {
|
|
119
|
+
syntax: '<angle>';
|
|
120
|
+
inherits: false;
|
|
121
|
+
initial-value: 0deg;
|
|
122
|
+
}
|
|
123
|
+
@keyframes flx-border-spin {
|
|
124
|
+
to {
|
|
125
|
+
--flx-angle: 360deg;
|
|
126
|
+
}
|
|
127
|
+
}
|