@fluixi-ui/tokens 0.4.0-alpha.1 → 0.4.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/dist/css/accordion.css +81 -3
  2. package/dist/css/alert.css +52 -23
  3. package/dist/css/avatar.css +19 -0
  4. package/dist/css/badge.css +129 -5
  5. package/dist/css/base.css +3 -1
  6. package/dist/css/bottom-sheet.css +4 -4
  7. package/dist/css/breadcrumb.css +1 -1
  8. package/dist/css/bulk-editor.css +2 -17
  9. package/dist/css/button.css +76 -16
  10. package/dist/css/calendar.css +9 -7
  11. package/dist/css/card.css +5 -1
  12. package/dist/css/carousel.css +80 -0
  13. package/dist/css/checkbox-radio.css +55 -1
  14. package/dist/css/chip.css +175 -0
  15. package/dist/css/date-field.css +1 -1
  16. package/dist/css/date-picker.css +2 -2
  17. package/dist/css/descriptions.css +29 -0
  18. package/dist/css/drawer.css +4 -4
  19. package/dist/css/empty.css +40 -0
  20. package/dist/css/fab.css +48 -0
  21. package/dist/css/field-variants.css +139 -0
  22. package/dist/css/file-upload.css +40 -0
  23. package/dist/css/glow.css +127 -0
  24. package/dist/css/icon-button.css +42 -0
  25. package/dist/css/image.css +67 -0
  26. package/dist/css/input.css +15 -0
  27. package/dist/css/layout.css +10 -0
  28. package/dist/css/navigation-menu.css +8 -7
  29. package/dist/css/number-field.css +3 -3
  30. package/dist/css/page.css +138 -0
  31. package/dist/css/pagination.css +3 -3
  32. package/dist/css/parts/listbox.css +9 -4
  33. package/dist/css/pin-input.css +40 -0
  34. package/dist/css/progress.css +56 -0
  35. package/dist/css/rating.css +59 -0
  36. package/dist/css/scroll-area.css +44 -0
  37. package/dist/css/select-combobox.css +19 -4
  38. package/dist/css/sidenav.css +180 -10
  39. package/dist/css/slider.css +4 -4
  40. package/dist/css/speed-dial.css +85 -0
  41. package/dist/css/splitter.css +81 -0
  42. package/dist/css/statistic.css +34 -0
  43. package/dist/css/stepper.css +3 -3
  44. package/dist/css/surface.css +71 -0
  45. package/dist/css/switch.css +12 -0
  46. package/dist/css/table.css +11 -0
  47. package/dist/css/tabs.css +10 -1
  48. package/dist/css/tags-input.css +91 -0
  49. package/dist/css/text.css +124 -0
  50. package/dist/css/timeline.css +59 -0
  51. package/dist/css/toggle.css +6 -0
  52. package/dist/css/tree-view.css +70 -0
  53. package/dist/lib/css/index.d.ts +10 -12
  54. package/dist/lib/css/index.d.ts.map +1 -1
  55. package/dist/lib/css/index.js +21 -7
  56. package/dist/lib/reference/colors.d.ts.map +1 -1
  57. package/dist/lib/reference/colors.js +15 -1
  58. package/dist/preset.css +23 -1
  59. package/dist/tokens.css +52 -4
  60. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  61. package/package.json +2 -2
@@ -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
+ }
@@ -0,0 +1,42 @@
1
+ @layer flx.components {
2
+ /* ----------------------------------------------------------------------------
3
+ * Icon button — a square [data-variant] button. Colours / states / ripple come
4
+ * from the button slice + the active skin; this only makes it square and shapes
5
+ * the corners.
6
+ * ------------------------------------------------------------------------- */
7
+ :where([data-icon-button]) {
8
+ width: var(--flx-ui-size-md);
9
+ padding: 0;
10
+ min-width: 0;
11
+ flex: none;
12
+ }
13
+ :where([data-icon-button][data-size='sm']) {
14
+ width: var(--flx-ui-size-sm);
15
+ }
16
+ :where([data-icon-button][data-size='lg']) {
17
+ width: var(--flx-ui-size-lg);
18
+ }
19
+
20
+ /* Shape */
21
+ :where([data-icon-button][data-shape='circle']) {
22
+ border-radius: var(--flx-ui-radius-full);
23
+ }
24
+ :where([data-icon-button][data-shape='square']) {
25
+ border-radius: var(--flx-ui-radius-sm);
26
+ }
27
+
28
+ /* A sensible default icon size that tracks the button size (consumers can still
29
+ size their own glyphs). */
30
+ :where([data-icon-button] svg) {
31
+ width: 20px;
32
+ height: 20px;
33
+ }
34
+ :where([data-icon-button][data-size='sm'] svg) {
35
+ width: 16px;
36
+ height: 16px;
37
+ }
38
+ :where([data-icon-button][data-size='lg'] svg) {
39
+ width: 24px;
40
+ height: 24px;
41
+ }
42
+ }
@@ -0,0 +1,67 @@
1
+ @layer flx.components {
2
+ /* Image (from @fluixi-ui/image). A box that reserves the image's space and layers
3
+ a loading placeholder / blur-up and an error fallback over the <img>. Size it via
4
+ a width/height, an aspect-ratio, or a sized parent — the <img> fills it. */
5
+ :where([data-image]) {
6
+ position: relative;
7
+ display: inline-block;
8
+ overflow: hidden;
9
+ line-height: 0;
10
+ vertical-align: middle;
11
+ background: var(--flx-ui-color-bg-subtle);
12
+ color: var(--flx-ui-color-fg-subtle);
13
+ --flx-ui-image-fallback-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23000' d='M21 3H3a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2Zm0 16H3V5h18v14ZM8.5 13.5l2 2.41L13.5 12l4 5.5H6.5l2-4Z'/%3E%3C/svg%3E");
14
+ }
15
+
16
+ /* The image stays in normal flow so the box reserves its footprint (and keeps its
17
+ height when the source errors); object-fit crops it to a sized box. */
18
+ :where([data-image-img]) {
19
+ display: block;
20
+ width: 100%;
21
+ height: 100%;
22
+ object-fit: cover;
23
+ opacity: 0;
24
+ transition: opacity var(--flx-ui-duration-normal) var(--flx-ui-ease-standard);
25
+ }
26
+ :where([data-image][data-status='loaded'] [data-image-img]) {
27
+ opacity: 1;
28
+ }
29
+ :where([data-image][data-status='error'] [data-image-img]) {
30
+ opacity: 0;
31
+ }
32
+
33
+ /* Overlays — blur-up, custom placeholder, and the error fallback — fill the box. */
34
+ :where([data-image-blur], [data-image-placeholder], [data-image-fallback]) {
35
+ position: absolute;
36
+ inset: 0;
37
+ width: 100%;
38
+ height: 100%;
39
+ }
40
+ :where([data-image-blur]) {
41
+ background-size: cover;
42
+ background-position: center;
43
+ filter: blur(12px);
44
+ transform: scale(1.1);
45
+ }
46
+
47
+ /* Default error fallback: a centered picture glyph on the subtle surface. Passing a
48
+ custom `fallback` element replaces it entirely. */
49
+ :where([data-image-fallback]) {
50
+ display: flex;
51
+ align-items: center;
52
+ justify-content: center;
53
+ background: var(--flx-ui-color-bg-subtle);
54
+ color: var(--flx-ui-color-fg-subtle);
55
+ }
56
+ :where([data-image-fallback])::before {
57
+ content: "";
58
+ width: 45%;
59
+ height: 45%;
60
+ max-width: 28px;
61
+ max-height: 28px;
62
+ background: currentColor;
63
+ opacity: 0.55;
64
+ -webkit-mask: center / contain no-repeat var(--flx-ui-image-fallback-glyph);
65
+ mask: center / contain no-repeat var(--flx-ui-image-fallback-glyph);
66
+ }
67
+ }
@@ -29,6 +29,21 @@
29
29
  :where([data-input][data-invalid]) {
30
30
  border-color: var(--flx-ui-color-danger);
31
31
  }
32
+ /* Drop the browser's native number spinners — they can't be styled consistently
33
+ across engines and clash with the field look. Covers both the bare input and
34
+ the one nested inside an adornment group (which carries no [data-input]). Reach
35
+ for @fluixi-ui/number-field when you want proper increment / decrement steppers. */
36
+ :where([data-input][type='number'], [data-input-group] input[type='number']) {
37
+ -moz-appearance: textfield;
38
+ appearance: textfield;
39
+ }
40
+ :where([data-input][type='number']::-webkit-outer-spin-button),
41
+ :where([data-input][type='number']::-webkit-inner-spin-button),
42
+ :where([data-input-group] input[type='number']::-webkit-outer-spin-button),
43
+ :where([data-input-group] input[type='number']::-webkit-inner-spin-button) {
44
+ -webkit-appearance: none;
45
+ margin: 0;
46
+ }
32
47
 
33
48
  /* Input with leading/trailing adornments: the group owns the frame. */
34
49
  :where([data-input-group]) {
@@ -0,0 +1,10 @@
1
+ @layer flx.components {
2
+ /* ----------------------------------------------------------------------------
3
+ * Layout primitives (Stack / Grid / Container) — the geometry is set inline per
4
+ * instance; this only guarantees a predictable box model.
5
+ * ------------------------------------------------------------------------- */
6
+ :where([data-stack], [data-grid], [data-container]) {
7
+ box-sizing: border-box;
8
+ min-width: 0;
9
+ }
10
+ }
@@ -42,20 +42,21 @@
42
42
  :where([data-navigation-menu-trigger]:focus-visible, [data-navigation-menu-link]:focus-visible) {
43
43
  box-shadow: var(--flx-ui-ctl-ring);
44
44
  }
45
- /* The chevron affordance: rotates when the panel opens. */
45
+ /* The chevron affordance: the shared theme chevron, flipping 180° when open. */
46
46
  :where([data-navigation-menu-trigger])::after {
47
47
  content: '';
48
- width: 0.55em;
49
- height: 0.55em;
48
+ width: 1em;
49
+ height: 1em;
50
+ flex: none;
50
51
  margin-inline-start: var(--flx-ui-space-1);
51
- border-right: 1.5px solid currentColor;
52
- border-bottom: 1.5px solid currentColor;
53
- transform: rotate(45deg) translateY(-0.1em);
52
+ background: currentColor;
54
53
  opacity: 0.6;
54
+ -webkit-mask: var(--flx-ui-chevron-down) center / contain no-repeat;
55
+ mask: var(--flx-ui-chevron-down) center / contain no-repeat;
55
56
  transition: transform var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
56
57
  }
57
58
  :where([data-navigation-menu-trigger][data-state='open'])::after {
58
- transform: rotate(225deg) translateY(-0.1em);
59
+ transform: rotate(180deg);
59
60
  }
60
61
 
61
62
  :where([data-navigation-menu-content]) {
@@ -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-leading-none);
14
+ line-height: var(--flx-ui-line-height-none);
15
15
  }
16
16
  :where([data-number-field-label][data-disabled]) {
17
17
  color: var(--flx-ui-color-fg-subtle);
@@ -61,8 +61,8 @@
61
61
  color: var(--flx-ui-color-fg-muted);
62
62
  cursor: pointer;
63
63
  font-size: var(--flx-ui-font-size-sm);
64
- transition: background var(--flx-ui-duration-fast) var(--flx-ui-easing-standard),
65
- color var(--flx-ui-duration-fast) var(--flx-ui-easing-standard);
64
+ transition: background var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
65
+ color var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
66
66
  }
67
67
  :where([data-number-field-decrement]) {
68
68
  border-right: 1px solid var(--flx-ui-color-border);
@@ -0,0 +1,138 @@
1
+ @layer flx.components {
2
+ /*
3
+ * The page shell. Structure only: a bounded column whose middle scrolls.
4
+ *
5
+ * `min-height: 0` on the body is the load-bearing rule — without it a flex item
6
+ * refuses to shrink below its content, so a long page pushes the footer out of
7
+ * view instead of scrolling.
8
+ */
9
+ :where([data-page]) {
10
+ --flx-ui-page-gutter: var(--flx-ui-space-6);
11
+ --flx-ui-page-header-gap: var(--flx-ui-space-3);
12
+
13
+ display: flex;
14
+ flex-direction: column;
15
+ min-height: 0;
16
+ height: 100%;
17
+ }
18
+
19
+ :where([data-page-header]) {
20
+ display: flex;
21
+ align-items: center;
22
+ gap: var(--flx-ui-page-header-gap);
23
+ padding: var(--flx-ui-space-4) var(--flx-ui-page-gutter);
24
+ border-bottom: var(--flx-ui-border-width-thin) solid var(--flx-ui-color-border);
25
+ background: var(--flx-ui-color-bg);
26
+ }
27
+
28
+ :where([data-page-header][data-sticky]) {
29
+ position: sticky;
30
+ top: 0;
31
+ z-index: 1;
32
+ }
33
+
34
+ :where([data-page-heading-group]) {
35
+ display: flex;
36
+ flex-direction: column;
37
+ gap: var(--flx-ui-space-1);
38
+ min-width: 0;
39
+ }
40
+
41
+ :where([data-page-heading]) {
42
+ margin: 0;
43
+ font-size: var(--flx-ui-font-size-xl);
44
+ font-weight: var(--flx-ui-font-weight-semibold);
45
+ line-height: var(--flx-ui-line-height-tight);
46
+ color: var(--flx-ui-color-fg);
47
+ /* Long titles truncate rather than pushing the actions off the row. */
48
+ overflow: hidden;
49
+ text-overflow: ellipsis;
50
+ white-space: nowrap;
51
+ }
52
+
53
+ :where([data-page-subtitle]) {
54
+ margin: 0;
55
+ font-size: var(--flx-ui-font-size-sm);
56
+ line-height: var(--flx-ui-line-height-normal);
57
+ color: var(--flx-ui-color-fg-muted);
58
+ }
59
+
60
+ /* Actions sit at the end of the row whatever precedes them. */
61
+ :where([data-page-actions]) {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: var(--flx-ui-space-2);
65
+ margin-inline-start: auto;
66
+ }
67
+
68
+ :where([data-page-body]) {
69
+ display: flex;
70
+ flex-direction: column;
71
+ gap: var(--flx-ui-space-4);
72
+ padding: var(--flx-ui-space-5) var(--flx-ui-page-gutter);
73
+ flex: 1;
74
+ min-height: 0;
75
+ }
76
+
77
+ /* Only the body scrolls; the header and footer stay put. */
78
+ :where([data-page-body][data-scroll='body']) {
79
+ overflow-y: auto;
80
+ }
81
+
82
+ /* The whole page scrolls, so the body must not create a second scroll context. */
83
+ :where([data-page-body][data-scroll='page']) {
84
+ overflow: visible;
85
+ }
86
+
87
+ :where([data-page][data-scroll='page']) {
88
+ overflow-y: auto;
89
+ }
90
+
91
+ :where([data-page-footer]) {
92
+ display: flex;
93
+ align-items: center;
94
+ justify-content: flex-end;
95
+ gap: var(--flx-ui-space-3);
96
+ padding: var(--flx-ui-space-4) var(--flx-ui-page-gutter);
97
+ border-top: var(--flx-ui-border-width-thin) solid var(--flx-ui-color-border);
98
+ background: var(--flx-ui-color-bg);
99
+ }
100
+
101
+ /*
102
+ * The back affordance carries `data-variant`, so the button rules and each
103
+ * skin's state layer/ripple already apply. Only the square geometry is left —
104
+ * and the anchor form needs the button's own resets, which it doesn't inherit.
105
+ */
106
+ :where([data-page-back]) {
107
+ display: inline-flex;
108
+ align-items: center;
109
+ justify-content: center;
110
+ flex: none;
111
+ width: var(--flx-ui-size-sm);
112
+ padding: 0;
113
+ text-decoration: none;
114
+ }
115
+
116
+ /* A CSS chevron, so the back button needs no icon library. */
117
+ :where([data-page-back])::before {
118
+ content: '';
119
+ width: 0.5rem;
120
+ height: 0.5rem;
121
+ border-inline-start: 2px solid currentColor;
122
+ border-block-start: 2px solid currentColor;
123
+ transform: rotate(-45deg);
124
+ /* Optical centring: the glyph's mass sits right of its box. */
125
+ margin-inline-start: 0.15em;
126
+ }
127
+
128
+ /* A supplied icon replaces the drawn chevron. */
129
+ :where([data-page-back]:has(> *))::before {
130
+ content: none;
131
+ }
132
+
133
+ :where([dir='rtl']) :where([data-page-back])::before {
134
+ transform: rotate(135deg);
135
+ margin-inline-start: 0;
136
+ margin-inline-end: 0.15em;
137
+ }
138
+ }
@@ -31,9 +31,9 @@
31
31
  border-radius: var(--flx-ui-radius-md);
32
32
  cursor: pointer;
33
33
  user-select: none;
34
- transition: background var(--flx-ui-duration-fast) var(--flx-ui-easing-standard),
35
- color var(--flx-ui-duration-fast) var(--flx-ui-easing-standard),
36
- border-color var(--flx-ui-duration-fast) var(--flx-ui-easing-standard);
34
+ transition: background var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
35
+ color var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
36
+ border-color var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
37
37
  }
38
38
  :where([data-pagination-page]:hover:not([disabled])),
39
39
  :where([data-pagination-prev]:hover:not([disabled])),
@@ -89,10 +89,15 @@
89
89
  border-color: var(--flx-ui-color-primary);
90
90
  }
91
91
  :where([data-mode='multiple'] [role='option'][data-selected] > [data-select-item-indicator])::after {
92
- content: '✓';
93
- color: var(--flx-ui-color-primary-fg);
94
- font-size: 0.7rem;
95
- line-height: 1;
92
+ content: '';
93
+ box-sizing: content-box;
94
+ flex: none;
95
+ width: 4px;
96
+ height: 8px;
97
+ margin-top: -2px;
98
+ border: solid var(--flx-ui-color-primary-fg);
99
+ border-width: 0 2px 2px 0;
100
+ transform: rotate(45deg);
96
101
  }
97
102
  :where([data-mode='multiple'] [role='option'][data-selected])::after {
98
103
  content: none;
@@ -0,0 +1,40 @@
1
+ @layer flx.components {
2
+ /* ----------------------------------------------------------------------------
3
+ * PIN / OTP input — a row of single-character cells
4
+ * ------------------------------------------------------------------------- */
5
+ :where([data-pin-input]) {
6
+ display: inline-flex;
7
+ align-items: center;
8
+ gap: var(--flx-ui-pin-gap, var(--flx-ui-space-2));
9
+ }
10
+ :where([data-pin-input-field]) {
11
+ box-sizing: border-box;
12
+ width: var(--flx-ui-pin-size, 2.75rem);
13
+ height: var(--flx-ui-pin-size, 2.75rem);
14
+ padding: 0;
15
+ text-align: center;
16
+ font: inherit;
17
+ font-size: var(--flx-ui-font-size-lg);
18
+ font-weight: var(--flx-ui-font-weight-medium);
19
+ font-variant-numeric: tabular-nums;
20
+ color: var(--flx-ui-color-fg);
21
+ background: var(--flx-ui-color-bg);
22
+ border: var(--flx-ui-border-width-thin) solid var(--flx-ui-color-border-strong);
23
+ border-radius: var(--flx-ui-radius-md);
24
+ transition:
25
+ border-color var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
26
+ box-shadow var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
27
+ }
28
+ :where([data-pin-input-field]::placeholder) {
29
+ color: var(--flx-ui-color-fg-muted);
30
+ }
31
+ :where([data-pin-input-field]:focus-visible) {
32
+ outline: none;
33
+ border-color: var(--flx-ui-color-focus-ring);
34
+ box-shadow: var(--flx-ui-ctl-ring);
35
+ }
36
+ :where([data-pin-input-field]:disabled) {
37
+ opacity: var(--flx-ui-opacity-disabled, 0.5);
38
+ cursor: not-allowed;
39
+ }
40
+ }
@@ -37,4 +37,60 @@
37
37
  0% { transform: translateX(-100%); }
38
38
  100% { transform: translateX(250%); }
39
39
  }
40
+
41
+ /* ----------------------------------------------------------------------------
42
+ * Circular progress
43
+ * ------------------------------------------------------------------------- */
44
+ :where([data-circular]) {
45
+ position: relative;
46
+ /* Stack the ring and any centred label in a single grid cell. */
47
+ display: inline-grid;
48
+ place-items: center;
49
+ vertical-align: middle;
50
+ /* Undo the linear bar's box styling that keys off [role='progressbar']
51
+ (fixed 6px height / 240px width / clipped overflow / filled track). */
52
+ width: auto;
53
+ height: auto;
54
+ overflow: visible;
55
+ background: none;
56
+ border-radius: 0;
57
+ }
58
+ :where([data-circular]) > * {
59
+ grid-area: 1 / 1;
60
+ }
61
+ :where([data-circular] [data-progress-svg]) {
62
+ display: block;
63
+ /* Start the arc at 12 o'clock rather than 3 o'clock. */
64
+ transform: rotate(-90deg);
65
+ }
66
+ :where([data-progress-track]) {
67
+ fill: none;
68
+ stroke: var(--flx-ui-color-bg-muted);
69
+ }
70
+ :where([data-progress-indicator]) {
71
+ fill: none;
72
+ stroke: var(--flx-ui-color-primary);
73
+ stroke-linecap: round;
74
+ transition: stroke-dashoffset var(--flx-ui-duration-normal) var(--flx-ui-ease-standard);
75
+ }
76
+ /* Indeterminate: a short arc that sweeps around the ring. */
77
+ :where([data-circular][data-state='indeterminate'] [data-progress-svg]) {
78
+ animation: flx-ui-circular-spin 0.9s linear infinite;
79
+ }
80
+ :where([data-circular][data-state='indeterminate'] [data-progress-indicator]) {
81
+ stroke-dasharray: 25 200;
82
+ transition: none;
83
+ }
84
+ @media (prefers-reduced-motion: reduce) {
85
+ :where([data-circular][data-state='indeterminate'] [data-progress-svg]) {
86
+ animation: none;
87
+ }
88
+ :where([data-circular][data-state='indeterminate'] [data-progress-indicator]) {
89
+ stroke-dasharray: none;
90
+ opacity: 0.5;
91
+ }
92
+ }
93
+ @keyframes flx-ui-circular-spin {
94
+ to { transform: rotate(270deg); }
95
+ }
40
96
  }