@fluixi-ui/tokens 0.4.0-alpha.8 → 0.4.0-alpha.9

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.
@@ -33,4 +33,23 @@
33
33
  width: 100%;
34
34
  height: 100%;
35
35
  }
36
+
37
+ /* ----------------------------------------------------------------------------
38
+ * Avatar group — overlapping cluster
39
+ * ------------------------------------------------------------------------- */
40
+ :where([data-avatar-group]) {
41
+ display: inline-flex;
42
+ align-items: center;
43
+ vertical-align: middle;
44
+ }
45
+ :where([data-avatar-group]) > * {
46
+ /* A ring in the page background so the overlap reads as separate discs. */
47
+ box-shadow: 0 0 0 2px var(--flx-ui-color-bg);
48
+ }
49
+ :where([data-avatar-group]) > * + * {
50
+ margin-inline-start: calc(-1 * var(--flx-ui-avatar-group-spacing, 8px));
51
+ }
52
+ :where([data-avatar-group-overflow]) {
53
+ font-size: var(--flx-ui-font-size-xs);
54
+ }
36
55
  }
@@ -1,13 +1,136 @@
1
1
  @layer flx.components {
2
- /* Badge */
3
- :where([data-variant].flx-ui-badge, .flx-ui-badge[data-variant]) {
4
- height: auto;
2
+ /* Badge (from @fluixi-ui/badge). A small status/label pill: a `data-variant` fill
3
+ style (soft/solid/outline) crossed with a semantic `data-tone`. Each tone sets
4
+ three custom props — the strong colour, the soft background, and the on-solid
5
+ foreground — and the variants consume them, so tone × variant needs no per-combo
6
+ rules. */
7
+ :where([data-badge]) {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ gap: var(--flx-ui-space-1);
5
11
  min-width: 0;
6
- padding: 2px var(--flx-ui-space-2);
12
+ /* A compact chip, not a button: tight vertical padding + line-height so the
13
+ height tracks the text, no wide tracking. */
14
+ padding: 1px var(--flx-ui-space-2);
7
15
  border-radius: var(--flx-ui-radius-full);
8
16
  font-size: var(--flx-ui-font-size-xs);
9
17
  font-weight: var(--flx-ui-font-weight-semibold);
10
- letter-spacing: var(--flx-ui-letter-spacing-wide);
18
+ line-height: 1.35;
19
+ white-space: nowrap;
20
+ border: var(--flx-ui-border-width-thin) solid transparent;
11
21
  box-shadow: none;
22
+ /* Each tone sets four props: `color` (strong — solid fill / outline border),
23
+ `text` (a legible-on-tint shade — soft/outline label), `bg` (soft fill) and
24
+ `on` (label on the solid fill). neutral defaults: */
25
+ --flx-ui-badge-color: var(--flx-ui-color-fg-muted);
26
+ --flx-ui-badge-text: var(--flx-ui-color-fg-muted);
27
+ --flx-ui-badge-bg: var(--flx-ui-color-bg-muted);
28
+ --flx-ui-badge-on: var(--flx-ui-color-bg);
29
+ }
30
+
31
+ :where([data-badge][data-tone='primary']),
32
+ :where([data-badge][data-tone='info']) {
33
+ --flx-ui-badge-color: var(--flx-ui-color-primary);
34
+ --flx-ui-badge-text: var(--flx-ui-color-primary-text);
35
+ --flx-ui-badge-bg: var(--flx-ui-color-primary-subtle);
36
+ --flx-ui-badge-on: var(--flx-ui-color-primary-fg);
37
+ }
38
+ :where([data-badge][data-tone='success']) {
39
+ --flx-ui-badge-color: var(--flx-ui-color-success);
40
+ --flx-ui-badge-text: var(--flx-ui-color-success-text);
41
+ --flx-ui-badge-bg: var(--flx-ui-color-success-subtle);
42
+ --flx-ui-badge-on: var(--flx-ui-color-success-fg);
43
+ }
44
+ :where([data-badge][data-tone='warning']) {
45
+ --flx-ui-badge-color: var(--flx-ui-color-warning);
46
+ --flx-ui-badge-text: var(--flx-ui-color-warning-text);
47
+ --flx-ui-badge-bg: var(--flx-ui-color-warning-subtle);
48
+ --flx-ui-badge-on: var(--flx-ui-color-warning-fg);
49
+ }
50
+ :where([data-badge][data-tone='danger']) {
51
+ --flx-ui-badge-color: var(--flx-ui-color-danger);
52
+ --flx-ui-badge-text: var(--flx-ui-color-danger-text);
53
+ --flx-ui-badge-bg: var(--flx-ui-color-danger-subtle);
54
+ --flx-ui-badge-on: var(--flx-ui-color-danger-fg);
55
+ }
56
+
57
+ :where([data-badge][data-variant='soft']) {
58
+ background: var(--flx-ui-badge-bg);
59
+ color: var(--flx-ui-badge-text);
60
+ }
61
+ :where([data-badge][data-variant='solid']) {
62
+ background: var(--flx-ui-badge-color);
63
+ color: var(--flx-ui-badge-on);
64
+ }
65
+ :where([data-badge][data-variant='outline']) {
66
+ color: var(--flx-ui-badge-text);
67
+ border-color: var(--flx-ui-badge-color);
68
+ }
69
+
70
+ /* ----------------------------------------------------------------------------
71
+ * Overlay badge (BadgeAnchor) — a count / dot pinned to a corner of a wrapped
72
+ * element (a Material-style notification badge). Reuses the [data-badge] fill.
73
+ * ------------------------------------------------------------------------- */
74
+ :where([data-badge-anchor]) {
75
+ position: relative;
76
+ display: inline-flex;
77
+ vertical-align: middle;
78
+ }
79
+ :where([data-badge][data-overlay]) {
80
+ position: absolute;
81
+ z-index: 1;
82
+ min-width: 1.25rem;
83
+ height: 1.25rem;
84
+ padding: 0 var(--flx-ui-space-1);
85
+ justify-content: center;
86
+ font-size: var(--flx-ui-font-size-2xs, 0.6875rem);
87
+ font-variant-numeric: tabular-nums;
88
+ pointer-events: none;
89
+ /* A surface-coloured ring so the badge reads as floating above the element. */
90
+ box-shadow: 0 0 0 2px var(--flx-ui-color-bg);
91
+ }
92
+ :where([data-badge][data-overlay][data-dot]) {
93
+ min-width: 0;
94
+ width: 0.5rem;
95
+ height: 0.5rem;
96
+ padding: 0;
97
+ }
98
+ :where([data-badge-anchor][data-placement='top-end']) > [data-overlay] {
99
+ top: 0;
100
+ right: 0;
101
+ transform: translate(50%, -50%);
102
+ }
103
+ :where([data-badge-anchor][data-placement='top-start']) > [data-overlay] {
104
+ top: 0;
105
+ left: 0;
106
+ transform: translate(-50%, -50%);
107
+ }
108
+ :where([data-badge-anchor][data-placement='bottom-end']) > [data-overlay] {
109
+ bottom: 0;
110
+ right: 0;
111
+ transform: translate(50%, 50%);
112
+ }
113
+ :where([data-badge-anchor][data-placement='bottom-start']) > [data-overlay] {
114
+ bottom: 0;
115
+ left: 0;
116
+ transform: translate(-50%, 50%);
117
+ }
118
+ /* Circular anchor (avatar / circle): nudge the badge in along the diagonal so it
119
+ sits on the round edge instead of the bounding-box corner. */
120
+ :where([data-badge-anchor][data-overlap='circular'][data-placement='top-end']) > [data-overlay] {
121
+ top: 14%;
122
+ right: 14%;
123
+ }
124
+ :where([data-badge-anchor][data-overlap='circular'][data-placement='top-start']) > [data-overlay] {
125
+ top: 14%;
126
+ left: 14%;
127
+ }
128
+ :where([data-badge-anchor][data-overlap='circular'][data-placement='bottom-end']) > [data-overlay] {
129
+ bottom: 14%;
130
+ right: 14%;
131
+ }
132
+ :where([data-badge-anchor][data-overlap='circular'][data-placement='bottom-start']) > [data-overlay] {
133
+ bottom: 14%;
134
+ left: 14%;
12
135
  }
13
136
  }
@@ -183,12 +183,8 @@
183
183
  align-items: center;
184
184
  justify-content: center;
185
185
  }
186
- :where(.flx-ui-be-check) {
187
- width: 15px;
188
- height: 15px;
189
- cursor: pointer;
190
- accent-color: var(--flx-ui-color-primary);
191
- }
186
+ /* Selection checkboxes render the <Checkbox> component ([role='checkbox']),
187
+ which styles itself; the wrap just centres it in the cell. */
192
188
 
193
189
  /* Fill handle on the active cell */
194
190
  :where(.flx-ui-be-fill-handle) {
@@ -406,17 +402,6 @@
406
402
  text-align: left;
407
403
  }
408
404
 
409
- /* Bulk editor — column header select-all checkbox */
410
- :where(.flx-ui-be-head-check) {
411
- width: 14px;
412
- height: 14px;
413
- cursor: pointer;
414
- accent-color: var(--flx-ui-color-primary);
415
- flex-shrink: 0;
416
- }
417
- :where(.flx-ui-be-check:disabled) {
418
- cursor: not-allowed;
419
- }
420
405
 
421
406
  /* Bulk editor — column resize grip */
422
407
  :where(.flx-ui-be-col-resize) {
@@ -1,6 +1,8 @@
1
1
  @layer flx.components {
2
2
  /* ----------------------------------------------------------------------------
3
- * Button (and any [data-variant] control, e.g. dialog triggers)
3
+ * Button (and any [data-variant] control, e.g. dialog triggers). Badges also
4
+ * carry [data-variant], so every rule here excludes [data-badge] — a badge must
5
+ * never pick up button sizing/colour.
4
6
  * ------------------------------------------------------------------------- */
5
7
 
6
8
  /* Tone (data-tone) is orthogonal to variant: it remaps the button's own
@@ -35,7 +37,7 @@
35
37
  --flx-ui-color-primary-fg: var(--flx-ui-color-bg);
36
38
  --flx-ui-color-primary-subtle: var(--flx-ui-color-bg-muted);
37
39
  }
38
- :where([data-variant]) {
40
+ :where([data-variant]:not([data-badge])) {
39
41
  box-sizing: border-box;
40
42
  display: inline-flex;
41
43
  align-items: center;
@@ -63,13 +65,13 @@
63
65
  border-color var(--flx-ui-duration-fast) var(--flx-ui-ease-standard),
64
66
  box-shadow var(--flx-ui-duration-fast) var(--flx-ui-ease-standard);
65
67
  }
66
- :where([data-variant][data-size='sm']) {
68
+ :where([data-variant]:not([data-badge])[data-size='sm']) {
67
69
  height: var(--flx-ui-size-sm);
68
70
  min-width: var(--flx-ui-size-sm);
69
71
  padding: 0 var(--flx-ui-space-3);
70
72
  font-size: var(--flx-ui-font-size-sm);
71
73
  }
72
- :where([data-variant][data-size='lg']) {
74
+ :where([data-variant]:not([data-badge])[data-size='lg']) {
73
75
  height: var(--flx-ui-size-lg);
74
76
  min-width: var(--flx-ui-size-lg);
75
77
  padding: 0 var(--flx-ui-space-5);
@@ -78,41 +80,42 @@
78
80
 
79
81
  /* Solid: the primary action — a flat, confident accent. Depth comes from the
80
82
  color step change on interaction, not from shadow or sheen. */
81
- :where([data-variant='solid']) {
83
+ :where([data-variant='solid']:not([data-badge])) {
82
84
  background: var(--flx-ui-color-primary);
83
85
  color: var(--flx-ui-color-primary-fg);
84
86
  }
85
- :where([data-variant='solid']:hover:not([data-disabled])) {
87
+ :where([data-variant='solid']:not([data-badge]):hover:not([data-disabled])) {
86
88
  background: var(--flx-ui-color-primary-hover);
87
89
  }
88
- :where([data-variant='solid']:active:not([data-disabled])) {
90
+ :where([data-variant='solid']:not([data-badge]):active:not([data-disabled])) {
89
91
  background: var(--flx-ui-color-primary-active);
90
92
  }
91
93
 
92
- /* Soft: a tinted, low-key primary. */
93
- :where([data-variant='soft']) {
94
- background: var(--flx-ui-color-primary-subtle);
94
+ /* Soft: a tinted, low-key primary. Deepened a touch over the bare subtle tint so
95
+ it carries enough weight to read as a control, then steps up on hover. */
96
+ :where([data-variant='soft']:not([data-badge])) {
97
+ background: color-mix(in oklab, var(--flx-ui-color-primary-subtle), var(--flx-ui-color-primary) 10%);
95
98
  color: var(--flx-ui-color-primary-active);
96
99
  }
97
- :where([data-variant='soft']:hover:not([data-disabled])) {
98
- background: color-mix(in oklab, var(--flx-ui-color-primary-subtle), var(--flx-ui-color-primary) 12%);
100
+ :where([data-variant='soft']:not([data-badge]):hover:not([data-disabled])) {
101
+ background: color-mix(in oklab, var(--flx-ui-color-primary-subtle), var(--flx-ui-color-primary) 20%);
99
102
  }
100
103
 
101
104
  /* Outline: a flat hairline-bordered control. */
102
- :where([data-variant='outline']) {
105
+ :where([data-variant='outline']:not([data-badge])) {
103
106
  border-color: var(--flx-ui-color-border-strong);
104
107
  background: var(--flx-ui-color-bg);
105
108
  }
106
- :where([data-variant='outline']:hover:not([data-disabled])) {
109
+ :where([data-variant='outline']:not([data-badge]):hover:not([data-disabled])) {
107
110
  background: var(--flx-ui-color-bg-subtle);
108
111
  border-color: var(--flx-ui-color-fg-muted);
109
112
  }
110
113
 
111
114
  /* Ghost: text-only until hovered. */
112
- :where([data-variant='ghost']) {
115
+ :where([data-variant='ghost']:not([data-badge])) {
113
116
  color: var(--flx-ui-color-fg);
114
117
  }
115
- :where([data-variant='ghost']:hover:not([data-disabled])) {
118
+ :where([data-variant='ghost']:not([data-badge]):hover:not([data-disabled])) {
116
119
  background: var(--flx-ui-color-bg-muted);
117
120
  }
118
121
 
@@ -121,6 +124,25 @@
121
124
  cursor: not-allowed;
122
125
  pointer-events: none;
123
126
  }
127
+ /* Buttons get a flat, neutral disabled instead of a dimmed accent (which reads
128
+ muddy). Filled variants drop to a grey fill; outline/ghost stay flat. */
129
+ :where([data-variant]:not([data-badge])[data-disabled]) {
130
+ opacity: 1;
131
+ box-shadow: none;
132
+ color: var(--flx-ui-color-fg-muted);
133
+ }
134
+ :where([data-variant='solid']:not([data-badge])[data-disabled]),
135
+ :where([data-variant='soft']:not([data-badge])[data-disabled]) {
136
+ background: var(--flx-ui-color-bg-muted);
137
+ border-color: transparent;
138
+ }
139
+ :where([data-variant='outline']:not([data-badge])[data-disabled]) {
140
+ background: transparent;
141
+ border-color: var(--flx-ui-color-border);
142
+ }
143
+ :where([data-variant='ghost']:not([data-badge])[data-disabled]) {
144
+ background: transparent;
145
+ }
124
146
 
125
147
  /* The signature focus ring: a surface-colored gap then an accent halo. */
126
148
  :where(:focus-visible) {
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }
package/dist/css/glow.css CHANGED
@@ -50,19 +50,23 @@
50
50
  :where([data-glow][data-glow-side])::after {
51
51
  animation: none;
52
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)));
53
55
  transition:
54
56
  background-position 450ms var(--flx-ui-ease-standard),
55
57
  filter var(--flx-ui-duration-normal) var(--flx-ui-ease-standard);
56
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. */
57
61
  :where([data-glow][data-glow-side='top'])::after,
58
62
  :where([data-glow][data-glow-side='bottom'])::after {
59
63
  background-image: linear-gradient(
60
64
  to bottom,
61
- transparent 28%,
62
- var(--flx-glow-from, var(--flx-ui-color-primary)) 44%,
65
+ transparent 12%,
66
+ var(--flx-glow-from, var(--flx-ui-color-primary)) 38%,
63
67
  var(--flx-glow-via, var(--flx-ui-accent-5)) 50%,
64
- var(--flx-glow-from, var(--flx-ui-color-primary)) 56%,
65
- transparent 72%
68
+ var(--flx-glow-from, var(--flx-ui-color-primary)) 62%,
69
+ transparent 88%
66
70
  );
67
71
  background-size: 100% 200%;
68
72
  }
@@ -70,11 +74,11 @@
70
74
  :where([data-glow][data-glow-side='right'])::after {
71
75
  background-image: linear-gradient(
72
76
  to right,
73
- transparent 28%,
74
- var(--flx-glow-from, var(--flx-ui-color-primary)) 44%,
77
+ transparent 12%,
78
+ var(--flx-glow-from, var(--flx-ui-color-primary)) 38%,
75
79
  var(--flx-glow-via, var(--flx-ui-accent-5)) 50%,
76
- var(--flx-glow-from, var(--flx-ui-color-primary)) 56%,
77
- transparent 72%
80
+ var(--flx-glow-from, var(--flx-ui-color-primary)) 62%,
81
+ transparent 88%
78
82
  );
79
83
  background-size: 200% 100%;
80
84
  }
@@ -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
+ }