luxen-ui 0.16.2 → 0.17.1

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 (47) hide show
  1. package/README.md +50 -49
  2. package/cdn/custom-elements.json +237 -1
  3. package/cdn/elements/avatar/avatar.d.ts.map +1 -1
  4. package/cdn/elements/avatar/avatar.js +1 -1
  5. package/cdn/elements/avatar/avatar.js.map +1 -1
  6. package/cdn/elements/dropdown-item/dropdown-item.d.ts +4 -0
  7. package/cdn/elements/dropdown-item/dropdown-item.d.ts.map +1 -1
  8. package/cdn/elements/dropdown-item/dropdown-item.js +2 -2
  9. package/cdn/elements/dropdown-item/dropdown-item.js.map +1 -1
  10. package/cdn/elements/segmented-control/index.d.ts +2 -0
  11. package/cdn/elements/segmented-control/index.d.ts.map +1 -0
  12. package/cdn/elements/segmented-control/index.js +2 -0
  13. package/cdn/elements/segmented-control/index.js.map +1 -0
  14. package/cdn/elements/segmented-control/segmented-control.d.ts +80 -0
  15. package/cdn/elements/segmented-control/segmented-control.d.ts.map +1 -0
  16. package/cdn/elements/segmented-control/segmented-control.js +2 -0
  17. package/cdn/elements/segmented-control/segmented-control.js.map +1 -0
  18. package/cdn/registry.d.ts +1 -1
  19. package/cdn/registry.d.ts.map +1 -1
  20. package/cdn/registry.js.map +1 -1
  21. package/cdn/standalone.css +167 -0
  22. package/cdn/standalone.js +25 -22
  23. package/cdn/standalone.js.map +1 -1
  24. package/cdn/styles/elements/segmented-control.css +165 -0
  25. package/dist/css/elements/segmented-control.css +165 -0
  26. package/dist/custom-elements.json +237 -1
  27. package/dist/elements/avatar/avatar.d.ts.map +1 -1
  28. package/dist/elements/avatar/avatar.js +27 -7
  29. package/dist/elements/dropdown-item/dropdown-item.css +18 -0
  30. package/dist/elements/dropdown-item/dropdown-item.d.ts +4 -0
  31. package/dist/elements/dropdown-item/dropdown-item.d.ts.map +1 -1
  32. package/dist/elements/dropdown-item/dropdown-item.js +11 -2
  33. package/dist/elements/segmented-control/index.d.ts +2 -0
  34. package/dist/elements/segmented-control/index.d.ts.map +1 -0
  35. package/dist/elements/segmented-control/index.js +4 -0
  36. package/dist/elements/segmented-control/segmented-control.d.ts +80 -0
  37. package/dist/elements/segmented-control/segmented-control.d.ts.map +1 -0
  38. package/dist/elements/segmented-control/segmented-control.js +307 -0
  39. package/dist/metadata/dropdown-item.json +16 -1
  40. package/dist/metadata/index.json +122 -2
  41. package/dist/metadata/segmented-control.json +105 -0
  42. package/dist/registry.d.ts +1 -1
  43. package/dist/registry.d.ts.map +1 -1
  44. package/dist/templates/elements/dropdown.md +52 -0
  45. package/dist/templates/elements/segmented-control.md +252 -0
  46. package/elements.json +8 -0
  47. package/package.json +1 -1
@@ -0,0 +1,165 @@
1
+ @layer components {
2
+ /*
3
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
4
+ Segmented control
5
+ A single-select switch: a muted track with a sliding pill behind the
6
+ selected segment. Shares its visual language with l-tabs[variant='enclosed']
7
+ (same neutral-subtle track + surface pill), but carries radio-group
8
+ semantics instead of tabs.
9
+
10
+ Structural selectors (not [role='…']) so the control is styled from plain
11
+ light DOM — it is progressive, so it must look right before JS adds ARIA.
12
+ The host is the flex track; its `<button>` children are the segments. JS
13
+ only enhances (roles, keyboard, and the sliding pill offsets).
14
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
15
+ */
16
+
17
+ l-segmented-control {
18
+ --_height: var(--l-size-control-md);
19
+ --_pad: 0.1875rem;
20
+ --_duration: 150ms;
21
+ --_easing: cubic-bezier(0.25, 0.1, 0.25, 1);
22
+
23
+ display: inline-flex;
24
+ position: relative;
25
+ align-items: stretch;
26
+ height: var(--_height);
27
+ padding: var(--_pad);
28
+ gap: calc(var(--spacing) * 0.5);
29
+ background: var(--l-color-bg-fill-neutral-subtle);
30
+ border-radius: var(--border-radius, var(--l-radius-lg));
31
+ /* Match the form-control border (--l-color-border, as used by .l-button,
32
+ l-select, and l-input) so the track lines up with dropdowns in a toolbar
33
+ — not the lighter --l-color-divider that enclosed tabs use. */
34
+ box-shadow: inset 0 0 0 1px var(--l-color-border);
35
+
36
+ /* Sliding pill behind the selected segment. Positioned by JS via
37
+ --_indicator-left / --_indicator-width (button offsetLeft / offsetWidth);
38
+ width 0 until JS runs, so it stays invisible in the no-JS fallback. */
39
+ &::before {
40
+ content: '';
41
+ position: absolute;
42
+ top: var(--_pad);
43
+ bottom: var(--_pad);
44
+ left: 0;
45
+ width: var(--_indicator-width, 0);
46
+ transform: translateX(var(--_indicator-left, 0));
47
+ background: var(--indicator-color, var(--l-color-surface));
48
+ border-radius: var(--l-radius-md);
49
+ box-shadow:
50
+ 0 1px 3px 0 rgb(0 0 0 / 0.08),
51
+ 0 1px 2px -1px rgb(0 0 0 / 0.08);
52
+ pointer-events: none;
53
+ will-change: transform, width;
54
+ transition:
55
+ transform var(--_duration) var(--_easing),
56
+ width var(--_duration) var(--_easing);
57
+ }
58
+ }
59
+
60
+ /*
61
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
62
+ Sizes — align with .l-button and form controls at the same size.
63
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
64
+ */
65
+
66
+ l-segmented-control[size='sm'] {
67
+ --_height: var(--l-size-control-sm);
68
+ }
69
+
70
+ l-segmented-control[size='lg'] {
71
+ --_height: var(--l-size-control-lg);
72
+ }
73
+
74
+ l-segmented-control[size='xl'] {
75
+ --_height: var(--l-size-control-xl);
76
+ }
77
+
78
+ /* Whole-control disabled (host `disabled` attribute, form-associated). */
79
+ l-segmented-control[disabled] {
80
+ opacity: 0.5;
81
+ pointer-events: none;
82
+ }
83
+
84
+ /*
85
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
86
+ Segments
87
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
88
+ */
89
+
90
+ l-segmented-control button {
91
+ position: relative;
92
+ z-index: 1;
93
+ display: inline-flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ gap: calc(var(--spacing) * 1.5);
97
+ padding-inline: calc(var(--spacing) * 3);
98
+ margin: 0;
99
+ appearance: none;
100
+ border: none;
101
+ background: none;
102
+ border-radius: var(--l-radius-md);
103
+ color: var(--l-color-text-secondary);
104
+ font-family: inherit;
105
+ font-size: var(--l-text-sm);
106
+ font-weight: 500;
107
+ line-height: 1;
108
+ white-space: nowrap;
109
+ cursor: pointer;
110
+ -webkit-tap-highlight-color: transparent;
111
+ transition: color 150ms;
112
+
113
+ /* Icon-only segments (JS-detected, no visible text): square them. */
114
+ &[data-icon-only] {
115
+ padding-inline: 0;
116
+ width: calc(var(--_height) - 2 * var(--_pad));
117
+ }
118
+
119
+ &[aria-checked='true'] {
120
+ color: var(--l-color-text-primary);
121
+ }
122
+
123
+ @media (hover: hover) {
124
+ &:hover:not([aria-checked='true']):not(:disabled):not([aria-disabled='true']) {
125
+ color: var(--l-color-text-primary);
126
+ }
127
+ }
128
+
129
+ &:disabled,
130
+ &[aria-disabled='true'] {
131
+ cursor: not-allowed;
132
+ opacity: 0.4;
133
+ }
134
+
135
+ &:focus-visible {
136
+ outline: 2px solid var(--l-focus-ring);
137
+ outline-offset: -2px;
138
+ border-radius: var(--l-radius-sm);
139
+ }
140
+ }
141
+
142
+ /*
143
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
144
+ Full width
145
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
146
+ */
147
+
148
+ l-segmented-control[full-width] {
149
+ display: flex;
150
+ }
151
+
152
+ l-segmented-control[full-width] button {
153
+ flex: 1;
154
+ }
155
+
156
+ @media (prefers-reduced-motion: reduce) {
157
+ l-segmented-control::before {
158
+ transition-duration: 0ms;
159
+ }
160
+
161
+ l-segmented-control button {
162
+ transition-duration: 0ms;
163
+ }
164
+ }
165
+ }
@@ -0,0 +1,165 @@
1
+ @layer components {
2
+ /*
3
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
4
+ Segmented control
5
+ A single-select switch: a muted track with a sliding pill behind the
6
+ selected segment. Shares its visual language with l-tabs[variant='enclosed']
7
+ (same neutral-subtle track + surface pill), but carries radio-group
8
+ semantics instead of tabs.
9
+
10
+ Structural selectors (not [role='…']) so the control is styled from plain
11
+ light DOM — it is progressive, so it must look right before JS adds ARIA.
12
+ The host is the flex track; its `<button>` children are the segments. JS
13
+ only enhances (roles, keyboard, and the sliding pill offsets).
14
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
15
+ */
16
+
17
+ l-segmented-control {
18
+ --_height: var(--l-size-control-md);
19
+ --_pad: 0.1875rem;
20
+ --_duration: 150ms;
21
+ --_easing: cubic-bezier(0.25, 0.1, 0.25, 1);
22
+
23
+ display: inline-flex;
24
+ position: relative;
25
+ align-items: stretch;
26
+ height: var(--_height);
27
+ padding: var(--_pad);
28
+ gap: calc(var(--spacing) * 0.5);
29
+ background: var(--l-color-bg-fill-neutral-subtle);
30
+ border-radius: var(--border-radius, var(--l-radius-lg));
31
+ /* Match the form-control border (--l-color-border, as used by .l-button,
32
+ l-select, and l-input) so the track lines up with dropdowns in a toolbar
33
+ — not the lighter --l-color-divider that enclosed tabs use. */
34
+ box-shadow: inset 0 0 0 1px var(--l-color-border);
35
+
36
+ /* Sliding pill behind the selected segment. Positioned by JS via
37
+ --_indicator-left / --_indicator-width (button offsetLeft / offsetWidth);
38
+ width 0 until JS runs, so it stays invisible in the no-JS fallback. */
39
+ &::before {
40
+ content: '';
41
+ position: absolute;
42
+ top: var(--_pad);
43
+ bottom: var(--_pad);
44
+ left: 0;
45
+ width: var(--_indicator-width, 0);
46
+ transform: translateX(var(--_indicator-left, 0));
47
+ background: var(--indicator-color, var(--l-color-surface));
48
+ border-radius: var(--l-radius-md);
49
+ box-shadow:
50
+ 0 1px 3px 0 rgb(0 0 0 / 0.08),
51
+ 0 1px 2px -1px rgb(0 0 0 / 0.08);
52
+ pointer-events: none;
53
+ will-change: transform, width;
54
+ transition:
55
+ transform var(--_duration) var(--_easing),
56
+ width var(--_duration) var(--_easing);
57
+ }
58
+ }
59
+
60
+ /*
61
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
62
+ Sizes — align with .l-button and form controls at the same size.
63
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
64
+ */
65
+
66
+ l-segmented-control[size='sm'] {
67
+ --_height: var(--l-size-control-sm);
68
+ }
69
+
70
+ l-segmented-control[size='lg'] {
71
+ --_height: var(--l-size-control-lg);
72
+ }
73
+
74
+ l-segmented-control[size='xl'] {
75
+ --_height: var(--l-size-control-xl);
76
+ }
77
+
78
+ /* Whole-control disabled (host `disabled` attribute, form-associated). */
79
+ l-segmented-control[disabled] {
80
+ opacity: 0.5;
81
+ pointer-events: none;
82
+ }
83
+
84
+ /*
85
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
86
+ Segments
87
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
88
+ */
89
+
90
+ l-segmented-control button {
91
+ position: relative;
92
+ z-index: 1;
93
+ display: inline-flex;
94
+ align-items: center;
95
+ justify-content: center;
96
+ gap: calc(var(--spacing) * 1.5);
97
+ padding-inline: calc(var(--spacing) * 3);
98
+ margin: 0;
99
+ appearance: none;
100
+ border: none;
101
+ background: none;
102
+ border-radius: var(--l-radius-md);
103
+ color: var(--l-color-text-secondary);
104
+ font-family: inherit;
105
+ font-size: var(--l-text-sm);
106
+ font-weight: 500;
107
+ line-height: 1;
108
+ white-space: nowrap;
109
+ cursor: pointer;
110
+ -webkit-tap-highlight-color: transparent;
111
+ transition: color 150ms;
112
+
113
+ /* Icon-only segments (JS-detected, no visible text): square them. */
114
+ &[data-icon-only] {
115
+ padding-inline: 0;
116
+ width: calc(var(--_height) - 2 * var(--_pad));
117
+ }
118
+
119
+ &[aria-checked='true'] {
120
+ color: var(--l-color-text-primary);
121
+ }
122
+
123
+ @media (hover: hover) {
124
+ &:hover:not([aria-checked='true']):not(:disabled):not([aria-disabled='true']) {
125
+ color: var(--l-color-text-primary);
126
+ }
127
+ }
128
+
129
+ &:disabled,
130
+ &[aria-disabled='true'] {
131
+ cursor: not-allowed;
132
+ opacity: 0.4;
133
+ }
134
+
135
+ &:focus-visible {
136
+ outline: 2px solid var(--l-focus-ring);
137
+ outline-offset: -2px;
138
+ border-radius: var(--l-radius-sm);
139
+ }
140
+ }
141
+
142
+ /*
143
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
144
+ Full width
145
+ ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬
146
+ */
147
+
148
+ l-segmented-control[full-width] {
149
+ display: flex;
150
+ }
151
+
152
+ l-segmented-control[full-width] button {
153
+ flex: 1;
154
+ }
155
+
156
+ @media (prefers-reduced-motion: reduce) {
157
+ l-segmented-control::before {
158
+ transition-duration: 0ms;
159
+ }
160
+
161
+ l-segmented-control button {
162
+ transition-duration: 0ms;
163
+ }
164
+ }
165
+ }
@@ -3750,6 +3750,17 @@
3750
3750
  "description": "The type of item: `normal` or `checkbox`.",
3751
3751
  "attribute": "type"
3752
3752
  },
3753
+ {
3754
+ "kind": "field",
3755
+ "name": "variant",
3756
+ "type": {
3757
+ "text": "'destructive' | undefined"
3758
+ },
3759
+ "default": "undefined",
3760
+ "description": "Visual variant. `destructive` marks a delete-style action in red.",
3761
+ "attribute": "variant",
3762
+ "reflects": true
3763
+ },
3753
3764
  {
3754
3765
  "kind": "field",
3755
3766
  "name": "checked",
@@ -3813,6 +3824,15 @@
3813
3824
  }
3814
3825
  ],
3815
3826
  "attributes": [
3827
+ {
3828
+ "description": "Visual variant. `destructive` marks a delete-style action in red.",
3829
+ "name": "variant",
3830
+ "type": {
3831
+ "text": "'destructive' | undefined"
3832
+ },
3833
+ "default": "undefined",
3834
+ "fieldName": "variant"
3835
+ },
3816
3836
  {
3817
3837
  "name": "value",
3818
3838
  "type": {
@@ -3864,7 +3884,13 @@
3864
3884
  "module": "/src/html/shared/luxen-element.js"
3865
3885
  },
3866
3886
  "tagName": "l-dropdown-item",
3867
- "customElement": true
3887
+ "customElement": true,
3888
+ "_attributes": [
3889
+ {
3890
+ "name": "variant",
3891
+ "description": "destructive — Renders the item as a destructive action (red), for delete-style entries."
3892
+ }
3893
+ ]
3868
3894
  }
3869
3895
  ],
3870
3896
  "exports": [
@@ -6041,6 +6067,216 @@
6041
6067
  }
6042
6068
  ]
6043
6069
  },
6070
+ {
6071
+ "kind": "javascript-module",
6072
+ "path": "src/html/elements/segmented-control/index.ts",
6073
+ "declarations": [],
6074
+ "exports": [
6075
+ {
6076
+ "kind": "js",
6077
+ "name": "*",
6078
+ "declaration": {
6079
+ "name": "*",
6080
+ "module": "src/html/elements/segmented-control/segmented-control.js"
6081
+ }
6082
+ }
6083
+ ]
6084
+ },
6085
+ {
6086
+ "kind": "javascript-module",
6087
+ "path": "src/html/elements/segmented-control/segmented-control.ts",
6088
+ "declarations": [
6089
+ {
6090
+ "kind": "class",
6091
+ "description": "Fired when the selected segment changes. Bubbles; not composed.",
6092
+ "name": "SegmentChangeEvent",
6093
+ "members": [
6094
+ {
6095
+ "kind": "field",
6096
+ "name": "value",
6097
+ "type": {
6098
+ "text": "string"
6099
+ },
6100
+ "readonly": true,
6101
+ "default": "value"
6102
+ },
6103
+ {
6104
+ "kind": "field",
6105
+ "name": "index",
6106
+ "type": {
6107
+ "text": "number"
6108
+ },
6109
+ "readonly": true,
6110
+ "default": "index"
6111
+ }
6112
+ ],
6113
+ "superclass": {
6114
+ "name": "Event",
6115
+ "module": "src/html/elements/segmented-control/segmented-control.ts"
6116
+ }
6117
+ },
6118
+ {
6119
+ "kind": "class",
6120
+ "description": "",
6121
+ "name": "SegmentedControl",
6122
+ "cssProperties": [
6123
+ {
6124
+ "description": "Background of the sliding pill behind the selected segment.",
6125
+ "name": "--indicator-color",
6126
+ "default": "var(--l-color-surface)"
6127
+ },
6128
+ {
6129
+ "description": "Corner radius of the track.",
6130
+ "name": "--border-radius",
6131
+ "default": "var(--l-radius-lg)"
6132
+ }
6133
+ ],
6134
+ "members": [
6135
+ {
6136
+ "kind": "field",
6137
+ "name": "value",
6138
+ "type": {
6139
+ "text": "string"
6140
+ },
6141
+ "default": "''",
6142
+ "description": "Value of the selected segment (its `value` attribute, else its index).",
6143
+ "attribute": "value",
6144
+ "reflects": true
6145
+ },
6146
+ {
6147
+ "kind": "field",
6148
+ "name": "size",
6149
+ "type": {
6150
+ "text": "SegmentedControlSize"
6151
+ },
6152
+ "default": "'md'",
6153
+ "description": "Control height, aligned with buttons and form controls at the same size.",
6154
+ "attribute": "size",
6155
+ "reflects": true
6156
+ },
6157
+ {
6158
+ "kind": "field",
6159
+ "name": "label",
6160
+ "type": {
6161
+ "text": "string | undefined"
6162
+ },
6163
+ "description": "Accessible label announced for the group. Not displayed on screen.",
6164
+ "attribute": "label",
6165
+ "reflects": true
6166
+ },
6167
+ {
6168
+ "kind": "field",
6169
+ "name": "fullWidth",
6170
+ "type": {
6171
+ "text": "boolean"
6172
+ },
6173
+ "default": "false",
6174
+ "description": "Stretch segments to fill the container width.",
6175
+ "attribute": "full-width",
6176
+ "reflects": true
6177
+ },
6178
+ {
6179
+ "kind": "method",
6180
+ "name": "formResetCallback"
6181
+ },
6182
+ {
6183
+ "kind": "method",
6184
+ "name": "formStateRestoreCallback",
6185
+ "parameters": [
6186
+ {
6187
+ "name": "state",
6188
+ "type": {
6189
+ "text": "string"
6190
+ }
6191
+ },
6192
+ {
6193
+ "name": "mode",
6194
+ "type": {
6195
+ "text": "'restore' | 'autocomplete'"
6196
+ }
6197
+ }
6198
+ ]
6199
+ }
6200
+ ],
6201
+ "events": [
6202
+ {
6203
+ "name": "change",
6204
+ "description": "Fired when the selected segment changes. Not cancelable. Bubbles. Properties: `value: string`, `index: number`.",
6205
+ "cancelable": false
6206
+ }
6207
+ ],
6208
+ "attributes": [
6209
+ {
6210
+ "name": "value",
6211
+ "type": {
6212
+ "text": "string"
6213
+ },
6214
+ "default": "''",
6215
+ "description": "Value of the selected segment (its `value` attribute, else its index).",
6216
+ "fieldName": "value"
6217
+ },
6218
+ {
6219
+ "name": "size",
6220
+ "type": {
6221
+ "text": "SegmentedControlSize"
6222
+ },
6223
+ "default": "'md'",
6224
+ "description": "Control height, aligned with buttons and form controls at the same size.",
6225
+ "fieldName": "size"
6226
+ },
6227
+ {
6228
+ "name": "label",
6229
+ "type": {
6230
+ "text": "string | undefined"
6231
+ },
6232
+ "description": "Accessible label announced for the group. Not displayed on screen.",
6233
+ "fieldName": "label"
6234
+ },
6235
+ {
6236
+ "name": "full-width",
6237
+ "type": {
6238
+ "text": "boolean"
6239
+ },
6240
+ "default": "false",
6241
+ "description": "Stretch segments to fill the container width.",
6242
+ "fieldName": "fullWidth"
6243
+ }
6244
+ ],
6245
+ "superclass": {
6246
+ "name": "LuxenFormAssociatedElement",
6247
+ "module": "/src/html/shared/luxen-form-associated-element.js"
6248
+ },
6249
+ "tagName": "l-segmented-control",
6250
+ "customElement": true,
6251
+ "summary": "A single-select segmented control: a compact, mutually-exclusive\nswitch between a few options (radio-group semantics with a sliding pill).\nProgressively enhances light-DOM `<button>`s so it works before JS runs.\nForm-associated: give it a `name` and its selected `value` is submitted with\nthe form (and restored on reset), like a native radio group.",
6252
+ "examples": [
6253
+ {
6254
+ "title": "Mark the initially-selected segment with `aria-checked=\"true\"` (or set",
6255
+ "language": "html",
6256
+ "code": "`value` to its `value` attribute) so it is styled before the script upgrades\nthe element.\n```html\n<l-segmented-control label=\"Metric\" value=\"cost\">\n <button value=\"volume\">Volume</button>\n <button value=\"cost\" aria-checked=\"true\">Cost</button>\n</l-segmented-control>\n```"
6257
+ }
6258
+ ]
6259
+ }
6260
+ ],
6261
+ "exports": [
6262
+ {
6263
+ "kind": "js",
6264
+ "name": "SegmentChangeEvent",
6265
+ "declaration": {
6266
+ "name": "SegmentChangeEvent",
6267
+ "module": "src/html/elements/segmented-control/segmented-control.ts"
6268
+ }
6269
+ },
6270
+ {
6271
+ "kind": "js",
6272
+ "name": "SegmentedControl",
6273
+ "declaration": {
6274
+ "name": "SegmentedControl",
6275
+ "module": "src/html/elements/segmented-control/segmented-control.ts"
6276
+ }
6277
+ }
6278
+ ]
6279
+ },
6044
6280
  {
6045
6281
  "kind": "javascript-module",
6046
6282
  "path": "src/html/elements/select/index.ts",
@@ -1 +1 @@
1
- {"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../../src/html/elements/avatar/avatar.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAqB7D;;;;;;;;;;;;GAYG;AACH,qBAAa,MAAO,SAAQ,YAAY;IACtC,MAAM,CAAC,MAAM,4BAAwB;IAErC,qFAAqF;IAErF,GAAG,SAAM;IAET,6EAA6E;IAE7E,IAAI,SAAM;IAEV,8DAA8D;IAE9D,IAAI,SAAQ;IAEZ,4DAA4D;IAE5D,KAAK,SAAK;IAEV,+DAA+D;IAE/D,WAAW,UAAS;IAEX,OAAO,CAAC,SAAS,CAAS;IAEnC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAMxC,iBAAiB;IAOjB,OAAO;IAgBP,OAAO,KAAK,IAAI,GAEf;IAED,MAAM;IAkCN,OAAO,CAAC,QAAQ,CAEd;CACH"}
1
+ {"version":3,"file":"avatar.d.ts","sourceRoot":"","sources":["../../../src/html/elements/avatar/avatar.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AA0C7D;;;;;;;;;;;;GAYG;AACH,qBAAa,MAAO,SAAQ,YAAY;IACtC,MAAM,CAAC,MAAM,4BAAwB;IAErC,qFAAqF;IAErF,GAAG,SAAM;IAET,6EAA6E;IAE7E,IAAI,SAAM;IAEV,8DAA8D;IAE9D,IAAI,SAAQ;IAEZ,4DAA4D;IAE5D,KAAK,SAAK;IAEV,+DAA+D;IAE/D,WAAW,UAAS;IAEX,OAAO,CAAC,SAAS,CAAS;IAEnC,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC;IAMxC,iBAAiB;IAOjB,OAAO;IAgBP,OAAO,KAAK,IAAI,GAEf;IAED,MAAM;IAkCN,OAAO,CAAC,QAAQ,CAEd;CACH"}
@@ -11,13 +11,33 @@ import { property, state } from 'lit/decorators.js';
11
11
  import hostStyles from '../../shared/styles/host.styles.js';
12
12
  import rawStyles from './avatar.css?inline';
13
13
  const styles = unsafeCSS(rawStyles);
14
- function getInitials(name) {
15
- return name
16
- .match(/(\b\S)?/g)
17
- .join('')
18
- .match(/(^\S|\S$)?/g)
19
- .join('')
20
- .toUpperCase();
14
+ /**
15
+ * Extracts up to two uppercase initials from a name: the first letter of the
16
+ * first word, plus the first letter of the last word when there is more than one.
17
+ *
18
+ * Splits on whitespace rather than relying on a `\b`/`\w` regex, which only
19
+ * recognises ASCII and mangles accented or non-ASCII names.
20
+ *
21
+ * @param name - The name to extract initials from. Defaults to an empty string.
22
+ * @return The uppercased initials, or an empty string when `name` is blank.
23
+ *
24
+ * @example getInitials('John Doe') // 'JD'
25
+ * @example getInitials('Markus Nösterer') // 'MN' (accents preserved)
26
+ * @example getInitials('Cher') // 'C' (single word → one initial)
27
+ * @example getInitials(' John Doe ') // 'JD' (extra whitespace ignored)
28
+ * @example getInitials('') // ''
29
+ */
30
+ function getInitials(name = '') {
31
+ const words = name.normalize('NFC').trim().split(/\s+/).filter(Boolean);
32
+ if (words.length === 0)
33
+ return '';
34
+ // Spreading iterates by code point, so an accented letter (or an astral
35
+ // character like an emoji) is taken whole instead of a broken half.
36
+ // oxlint-disable-next-line typescript/no-misused-spread -- code-point-safe first char is the intent
37
+ const firstLetter = (word) => [...word][0];
38
+ const first = firstLetter(words[0]);
39
+ const last = words.length > 1 ? firstLetter(words[words.length - 1]) : '';
40
+ return (first + last).toUpperCase();
21
41
  }
22
42
  const defaultIcon = svg `<svg class="icon" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
23
43
  <path d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z" />
@@ -44,6 +44,24 @@
44
44
  background: var(--l-color-bg-state-hover);
45
45
  }
46
46
 
47
+ /* Destructive variant — a delete-style action. Red label at rest, danger-tinted
48
+ fill and focus ring on hover/focus so the highlight reads as "danger" rather
49
+ than the neutral hover tint. */
50
+ :host([variant='destructive']) .item {
51
+ color: var(--l-color-text-danger);
52
+ }
53
+
54
+ :host([variant='destructive']) .item:focus-visible {
55
+ background: var(--l-color-bg-fill-danger-soft);
56
+ outline-color: var(--l-color-text-danger);
57
+ }
58
+
59
+ @media (hover: hover) {
60
+ :host([variant='destructive']) .item:hover {
61
+ background: var(--l-color-bg-fill-danger-soft);
62
+ }
63
+ }
64
+
47
65
  .check {
48
66
  display: flex;
49
67
  width: 16px;
@@ -12,6 +12,8 @@ import { LuxenElement } from '../../shared/luxen-element.js';
12
12
  *
13
13
  * @cssproperty --color - Text color.
14
14
  *
15
+ * @attribute variant - destructive — Renders the item as a destructive action (red), for delete-style entries.
16
+ *
15
17
  * @customElement l-dropdown-item
16
18
  */
17
19
  export declare class DropdownItem extends LuxenElement {
@@ -23,6 +25,8 @@ export declare class DropdownItem extends LuxenElement {
23
25
  accessor disabled: boolean;
24
26
  /** The type of item: `normal` or `checkbox`. */
25
27
  accessor type: 'normal' | 'checkbox';
28
+ /** Visual variant. `destructive` marks a delete-style action in red. */
29
+ accessor variant: 'destructive' | undefined;
26
30
  /** Whether the checkbox item is checked. */
27
31
  accessor checked: boolean;
28
32
  /** Whether this item's submenu is open. Managed by the parent `l-dropdown`. */