@neovici/cosmoz-button 1.1.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +264 -0
- package/dist/cosmoz-button.d.ts +18 -10
- package/dist/cosmoz-button.js +29 -18
- package/dist/styles.js +170 -38
- package/package.json +6 -9
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# cosmoz-button
|
|
2
|
+
|
|
3
|
+
A customizable button web component built with [Untitled UI](https://www.untitledui.com/) design tokens.
|
|
4
|
+
|
|
5
|
+
Part of the [Neovici](https://neovici.se) design system.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @neovici/cosmoz-button
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import '@neovici/cosmoz-button';
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```html
|
|
20
|
+
<!-- Primary button (default) -->
|
|
21
|
+
<cosmoz-button>Save</cosmoz-button>
|
|
22
|
+
|
|
23
|
+
<!-- Secondary button -->
|
|
24
|
+
<cosmoz-button variant="secondary">Cancel</cosmoz-button>
|
|
25
|
+
|
|
26
|
+
<!-- Destructive button -->
|
|
27
|
+
<cosmoz-button variant="destructive">Delete</cosmoz-button>
|
|
28
|
+
|
|
29
|
+
<!-- Tertiary button -->
|
|
30
|
+
<cosmoz-button variant="tertiary">Learn more</cosmoz-button>
|
|
31
|
+
|
|
32
|
+
<!-- Link button -->
|
|
33
|
+
<cosmoz-button variant="link">View details</cosmoz-button>
|
|
34
|
+
|
|
35
|
+
<!-- Icon-only utility button with tooltip -->
|
|
36
|
+
<cosmoz-button icon-only variant="tertiary" tooltip="Close" aria-label="Close">
|
|
37
|
+
<svg width="20" height="20">...</svg>
|
|
38
|
+
</cosmoz-button>
|
|
39
|
+
|
|
40
|
+
<!-- Icon-only button with selected (toggled) state -->
|
|
41
|
+
<cosmoz-button icon-only aria-pressed="true">
|
|
42
|
+
<svg width="20" height="20">...</svg>
|
|
43
|
+
</cosmoz-button>
|
|
44
|
+
|
|
45
|
+
<!-- Anchor link (renders as <a> when href is present) -->
|
|
46
|
+
<cosmoz-button href="/home" target="_blank" rel="noopener"
|
|
47
|
+
>Open in new tab</cosmoz-button
|
|
48
|
+
>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Attributes
|
|
52
|
+
|
|
53
|
+
| Attribute | Type | Default | Description |
|
|
54
|
+
| ------------------- | ------- | --------- | ----------------------------------------------------------- |
|
|
55
|
+
| `variant` | string | `primary` | primary, secondary, tertiary, destructive, link |
|
|
56
|
+
| `size` | string | `md` | sm, md, lg, xl |
|
|
57
|
+
| `disabled` | boolean | `false` | Disables the button |
|
|
58
|
+
| `full-width` | boolean | `false` | Makes the button take 100% width |
|
|
59
|
+
| `icon-only` | boolean | `false` | Renders a compact square icon-only button (utility button) |
|
|
60
|
+
| `tooltip` | string | - | Wraps the button in a `cosmoz-tooltip` with this heading |
|
|
61
|
+
| `tooltip-placement` | string | `top` | Tooltip placement: top, bottom, left, right |
|
|
62
|
+
| `aria-pressed` | string | - | Set to `true` to show the selected/toggled state |
|
|
63
|
+
| `type` | string | `button` | Button type: button, submit, reset |
|
|
64
|
+
| `value` | string | - | Value associated with the button |
|
|
65
|
+
| `href` | string | - | When present, renders as an anchor link instead of a button |
|
|
66
|
+
| `target` | string | - | Target attribute for the anchor (only with href) |
|
|
67
|
+
| `rel` | string | - | Rel attribute for the anchor (only with href) |
|
|
68
|
+
| `download` | string | - | Download attribute for the anchor (only with href) |
|
|
69
|
+
| `aria-label` | string | - | Accessible label for icon-only buttons |
|
|
70
|
+
| `aria-describedby` | string | - | ID of element that describes the button |
|
|
71
|
+
|
|
72
|
+
## Variants
|
|
73
|
+
|
|
74
|
+
### Style Variants
|
|
75
|
+
|
|
76
|
+
| Variant | Description |
|
|
77
|
+
| ------------- | ---------------------------------- |
|
|
78
|
+
| `primary` | Brand solid background, white text |
|
|
79
|
+
| `secondary` | White background, gray border |
|
|
80
|
+
| `tertiary` | Transparent, text only |
|
|
81
|
+
| `destructive` | Error/danger styling |
|
|
82
|
+
| `link` | No padding, underline on hover |
|
|
83
|
+
|
|
84
|
+
### Size Variants
|
|
85
|
+
|
|
86
|
+
| Size | Height | Icon-only Size | Font Size |
|
|
87
|
+
| ---- | ------ | -------------- | --------- |
|
|
88
|
+
| `sm` | 28px | 28px | 12px |
|
|
89
|
+
| `md` | 32px | 32px | 14px |
|
|
90
|
+
| `lg` | 36px | 36px | 14px |
|
|
91
|
+
| `xl` | 40px | 40px | 14px |
|
|
92
|
+
|
|
93
|
+
## Icons
|
|
94
|
+
|
|
95
|
+
The button supports prefix and suffix icon slots:
|
|
96
|
+
|
|
97
|
+
```html
|
|
98
|
+
<!-- Prefix icon -->
|
|
99
|
+
<cosmoz-button>
|
|
100
|
+
<svg slot="prefix" width="20" height="20">...</svg>
|
|
101
|
+
Add Item
|
|
102
|
+
</cosmoz-button>
|
|
103
|
+
|
|
104
|
+
<!-- Suffix icon -->
|
|
105
|
+
<cosmoz-button variant="secondary">
|
|
106
|
+
Download
|
|
107
|
+
<svg slot="suffix" width="20" height="20">...</svg>
|
|
108
|
+
</cosmoz-button>
|
|
109
|
+
|
|
110
|
+
<!-- Both icons -->
|
|
111
|
+
<cosmoz-button>
|
|
112
|
+
<svg slot="prefix">...</svg>
|
|
113
|
+
Action
|
|
114
|
+
<svg slot="suffix">...</svg>
|
|
115
|
+
</cosmoz-button>
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Icon-Only Buttons
|
|
119
|
+
|
|
120
|
+
Set the `icon-only` attribute to render a compact square utility button
|
|
121
|
+
([Untitled UI utility button](https://www.untitledui.com/react/components/utility-buttons)):
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<cosmoz-button icon-only variant="tertiary" aria-label="Close panel">
|
|
125
|
+
<svg width="20" height="20">...</svg>
|
|
126
|
+
</cosmoz-button>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
- Square sizing per `size`: sm 28px, md 32px, lg 36px, xl 40px, with `p-1.5` padding
|
|
130
|
+
- Quiet variants (`secondary`, `tertiary`) get the muted utility look: icon
|
|
131
|
+
color `--cz-color-text-tertiary`, hover `--cz-color-text-secondary` on
|
|
132
|
+
`--cz-color-bg-primary-hover` (correct contrast in both light and dark
|
|
133
|
+
mode)
|
|
134
|
+
- Solid variants (`primary`, `destructive`) keep their own on-brand /
|
|
135
|
+
on-error icon colors
|
|
136
|
+
- Slotted SVG sizing: 16px (sm), 20px (md and up)
|
|
137
|
+
- All existing variants, focus rings, disabled and pressed-down `:active`
|
|
138
|
+
states apply as usual
|
|
139
|
+
|
|
140
|
+
### Selected / Toggled State
|
|
141
|
+
|
|
142
|
+
Set `aria-pressed="true"` to show the selected state. It is available on
|
|
143
|
+
every variant, with colors matching each variant's intent:
|
|
144
|
+
|
|
145
|
+
- **primary / secondary / tertiary** (quiet variants) shift to the selected
|
|
146
|
+
brand chip: `brand-50`/`brand-600` surface with a `brand-700`/`gray-50`
|
|
147
|
+
icon or text
|
|
148
|
+
- **destructive** keeps the error intent: `error-100`/`error-600` surface
|
|
149
|
+
with an `error-800`/`gray-50` icon or text
|
|
150
|
+
- **link** emphasizes the text (deeper brand color + underline), no fill
|
|
151
|
+
|
|
152
|
+
```html
|
|
153
|
+
<cosmoz-button icon-only aria-pressed="true" aria-label="Invoice image">
|
|
154
|
+
<svg width="20" height="20">...</svg>
|
|
155
|
+
</cosmoz-button>
|
|
156
|
+
|
|
157
|
+
<cosmoz-button variant="destructive" aria-pressed="true">Delete</cosmoz-button>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Tooltips
|
|
161
|
+
|
|
162
|
+
Set the `tooltip` attribute to wrap the button in a
|
|
163
|
+
[`cosmoz-tooltip`](https://github.com/Neovici/cosmoz-tooltip) with the given
|
|
164
|
+
heading. Use `tooltip-placement` to position it (`top`, `bottom`, `left`,
|
|
165
|
+
`right` — default `top`):
|
|
166
|
+
|
|
167
|
+
```html
|
|
168
|
+
<cosmoz-button icon-only tooltip="Invoice image" tooltip-placement="left">
|
|
169
|
+
<svg width="20" height="20">...</svg>
|
|
170
|
+
</cosmoz-button>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Notes:
|
|
174
|
+
|
|
175
|
+
- The tooltip wrapper is **always rendered**; `cosmoz-tooltip` degrades to a
|
|
176
|
+
pass-through `<slot>` when no heading/content is set, so not setting
|
|
177
|
+
`tooltip` costs nothing and no conditional rendering is needed.
|
|
178
|
+
- The wrapper fills the host (`display: flex; width: 100%`), so host-driven
|
|
179
|
+
sizing (`flex: 1`, explicit widths, e.g. dialog action buttons) keeps
|
|
180
|
+
working unchanged.
|
|
181
|
+
- The tooltip is disabled automatically when the button is disabled.
|
|
182
|
+
|
|
183
|
+
## Anchor Links
|
|
184
|
+
|
|
185
|
+
When `href` is present, the button renders as an anchor link with the same
|
|
186
|
+
visual styles:
|
|
187
|
+
|
|
188
|
+
```html
|
|
189
|
+
<cosmoz-button href="/home">Home</cosmoz-button>
|
|
190
|
+
<cosmoz-button href="/report.pdf" download>Download report</cosmoz-button>
|
|
191
|
+
<cosmoz-button href="https://example.com" target="_blank" rel="noopener"
|
|
192
|
+
>External link</cosmoz-button
|
|
193
|
+
>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Styling
|
|
197
|
+
|
|
198
|
+
The button exposes a `button` part for external styling:
|
|
199
|
+
|
|
200
|
+
```css
|
|
201
|
+
cosmoz-button::part(button) {
|
|
202
|
+
/* Custom styles */
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## Design Tokens
|
|
207
|
+
|
|
208
|
+
This component uses CSS custom properties from `@neovici/cosmoz-tokens`. The tokens are automatically applied but can be customized at the application level.
|
|
209
|
+
|
|
210
|
+
## Accessibility
|
|
211
|
+
|
|
212
|
+
### Button Type
|
|
213
|
+
|
|
214
|
+
The button defaults to `type="button"` to prevent unintended form submissions. Use `type="submit"` explicitly when needed:
|
|
215
|
+
|
|
216
|
+
```html
|
|
217
|
+
<form>
|
|
218
|
+
<cosmoz-button type="submit">Submit Form</cosmoz-button>
|
|
219
|
+
</form>
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Icon-Only Buttons
|
|
223
|
+
|
|
224
|
+
When using buttons with only an icon (no visible text), provide an accessible
|
|
225
|
+
label. Prefer the `icon-only` attribute together with `tooltip` — the tooltip
|
|
226
|
+
heading doubles as the visual affordance while `aria-label` keeps it
|
|
227
|
+
accessible:
|
|
228
|
+
|
|
229
|
+
```html
|
|
230
|
+
<cosmoz-button icon-only tooltip="Delete item" aria-label="Delete item">
|
|
231
|
+
<svg width="20" height="20">...</svg>
|
|
232
|
+
</cosmoz-button>
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
For toggles, reflect the state with `aria-pressed`.
|
|
236
|
+
|
|
237
|
+
### Descriptive Context
|
|
238
|
+
|
|
239
|
+
Use `aria-describedby` to reference additional help text:
|
|
240
|
+
|
|
241
|
+
```html
|
|
242
|
+
<cosmoz-button aria-describedby="delete-warning">Delete</cosmoz-button>
|
|
243
|
+
<p id="delete-warning">This action cannot be undone.</p>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Development
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
# Install dependencies
|
|
250
|
+
npm install
|
|
251
|
+
|
|
252
|
+
# Start Storybook
|
|
253
|
+
npm run storybook:start
|
|
254
|
+
|
|
255
|
+
# Run tests
|
|
256
|
+
npm run test
|
|
257
|
+
|
|
258
|
+
# Build
|
|
259
|
+
npm run build
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
Apache-2.0
|
package/dist/cosmoz-button.d.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
+
import '@neovici/cosmoz-tooltip';
|
|
1
2
|
export type ButtonVariant = 'primary' | 'secondary' | 'tertiary' | 'destructive' | 'link';
|
|
2
3
|
export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
|
|
3
4
|
export type ButtonType = 'button' | 'submit' | 'reset';
|
|
4
5
|
export interface CosmozButtonElement extends HTMLElement {
|
|
5
|
-
variant
|
|
6
|
-
size
|
|
7
|
-
disabled
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
variant?: ButtonVariant;
|
|
7
|
+
size?: ButtonSize;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
fullWidth?: boolean;
|
|
10
|
+
iconOnly?: boolean;
|
|
11
|
+
tooltip?: string | null;
|
|
12
|
+
tooltipPlacement?: string | null;
|
|
13
|
+
type?: ButtonType;
|
|
14
|
+
value?: string | null;
|
|
15
|
+
href?: string | null;
|
|
16
|
+
target?: string | null;
|
|
17
|
+
rel?: string | null;
|
|
18
|
+
download?: string | null;
|
|
15
19
|
}
|
|
16
20
|
/**
|
|
17
21
|
* A customizable button component using Untitled UI design tokens.
|
|
@@ -22,12 +26,16 @@ export interface CosmozButtonElement extends HTMLElement {
|
|
|
22
26
|
* @attr {string} size - Button size: sm, md (default), lg, xl
|
|
23
27
|
* @attr {boolean} disabled - Disables the button
|
|
24
28
|
* @attr {boolean} full-width - Makes the button 100% width
|
|
29
|
+
* @attr {boolean} icon-only - Renders a compact square icon-only button (Untitled UI utility button)
|
|
30
|
+
* @attr {string} tooltip - When set, wraps the button in a cosmoz-tooltip with this heading
|
|
31
|
+
* @attr {string} tooltip-placement - Tooltip placement (top, bottom, left, right; default top)
|
|
25
32
|
* @attr {string} type - Button type: button (default), submit, reset
|
|
26
33
|
* @attr {string} value - Value associated with the button (optional)
|
|
27
34
|
* @attr {string} href - When present, renders as an anchor link instead of a button
|
|
28
35
|
* @attr {string} target - Target attribute for the anchor (only with href)
|
|
29
36
|
* @attr {string} rel - Rel attribute for the anchor (only with href)
|
|
30
37
|
* @attr {string} download - Download attribute for the anchor (only with href)
|
|
38
|
+
* @attr {string} aria-pressed - Reflects a toggled/selected state (styled for icon-only buttons)
|
|
31
39
|
*
|
|
32
40
|
* @slot - Default slot for button text content
|
|
33
41
|
* @slot prefix - Slot for prefix icon (before text)
|
package/dist/cosmoz-button.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { normalize } from '@neovici/cosmoz-tokens/normalize';
|
|
2
|
+
import '@neovici/cosmoz-tooltip';
|
|
2
3
|
import { component, html, useEffect } from '@pionjs/pion';
|
|
3
4
|
import { nothing } from 'lit-html';
|
|
4
5
|
import { ifDefined } from 'lit-html/directives/if-defined.js';
|
|
6
|
+
import { when } from 'lit-html/directives/when.js';
|
|
5
7
|
import { styles } from './styles';
|
|
6
8
|
const observedAttributes = [
|
|
7
9
|
'variant',
|
|
8
10
|
'size',
|
|
9
11
|
'disabled',
|
|
10
12
|
'full-width',
|
|
13
|
+
'icon-only',
|
|
14
|
+
'tooltip',
|
|
15
|
+
'tooltip-placement',
|
|
11
16
|
'type',
|
|
12
17
|
'value',
|
|
13
18
|
'href',
|
|
@@ -24,12 +29,16 @@ const observedAttributes = [
|
|
|
24
29
|
* @attr {string} size - Button size: sm, md (default), lg, xl
|
|
25
30
|
* @attr {boolean} disabled - Disables the button
|
|
26
31
|
* @attr {boolean} full-width - Makes the button 100% width
|
|
32
|
+
* @attr {boolean} icon-only - Renders a compact square icon-only button (Untitled UI utility button)
|
|
33
|
+
* @attr {string} tooltip - When set, wraps the button in a cosmoz-tooltip with this heading
|
|
34
|
+
* @attr {string} tooltip-placement - Tooltip placement (top, bottom, left, right; default top)
|
|
27
35
|
* @attr {string} type - Button type: button (default), submit, reset
|
|
28
36
|
* @attr {string} value - Value associated with the button (optional)
|
|
29
37
|
* @attr {string} href - When present, renders as an anchor link instead of a button
|
|
30
38
|
* @attr {string} target - Target attribute for the anchor (only with href)
|
|
31
39
|
* @attr {string} rel - Rel attribute for the anchor (only with href)
|
|
32
40
|
* @attr {string} download - Download attribute for the anchor (only with href)
|
|
41
|
+
* @attr {string} aria-pressed - Reflects a toggled/selected state (styled for icon-only buttons)
|
|
33
42
|
*
|
|
34
43
|
* @slot - Default slot for button text content
|
|
35
44
|
* @slot prefix - Slot for prefix icon (before text)
|
|
@@ -38,44 +47,46 @@ const observedAttributes = [
|
|
|
38
47
|
* @csspart button - The native button or anchor element
|
|
39
48
|
*/
|
|
40
49
|
const CosmozButton = (host) => {
|
|
41
|
-
const disabled =
|
|
42
|
-
const
|
|
43
|
-
const href = host.getAttribute('href');
|
|
50
|
+
const { disabled = false, tooltip, tooltipPlacement, type = 'button', href, target, rel, download, } = host;
|
|
51
|
+
const isDisabled = Boolean(disabled);
|
|
44
52
|
useEffect(() => {
|
|
45
53
|
const handler = (e) => {
|
|
46
|
-
if (host.
|
|
54
|
+
if (host.disabled)
|
|
47
55
|
e.stopImmediatePropagation();
|
|
48
56
|
};
|
|
49
57
|
host.addEventListener('click', handler, { capture: true });
|
|
50
58
|
return () => host.removeEventListener('click', handler, { capture: true });
|
|
51
|
-
}, []);
|
|
59
|
+
}, [host.disabled]);
|
|
52
60
|
const content = html `
|
|
53
61
|
<slot name="prefix"></slot>
|
|
54
62
|
<slot></slot>
|
|
55
63
|
<slot name="suffix"></slot>
|
|
56
64
|
`;
|
|
57
|
-
|
|
58
|
-
const target = host.getAttribute('target');
|
|
59
|
-
const rel = host.getAttribute('rel');
|
|
60
|
-
const download = host.getAttribute('download');
|
|
61
|
-
return html `
|
|
65
|
+
const control = when(href != null, () => html `
|
|
62
66
|
<a
|
|
63
67
|
href=${href}
|
|
64
68
|
class="button"
|
|
65
69
|
part="button"
|
|
66
|
-
aria-disabled=${
|
|
70
|
+
aria-disabled=${isDisabled ? 'true' : nothing}
|
|
67
71
|
target=${ifDefined(target)}
|
|
68
72
|
rel=${ifDefined(rel)}
|
|
69
73
|
download=${ifDefined(download)}
|
|
70
74
|
>${content}</a
|
|
71
75
|
>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
`, () => html `
|
|
77
|
+
<button type=${type} class="button" part="button" ?disabled=${isDisabled}>
|
|
78
|
+
${content}
|
|
79
|
+
</button>
|
|
80
|
+
`);
|
|
81
|
+
/* Always rendered; cosmoz-tooltip degrades to a pass-through unless
|
|
82
|
+
`tooltip` is set. */
|
|
83
|
+
return html `<cosmoz-tooltip
|
|
84
|
+
heading=${ifDefined(tooltip ?? undefined)}
|
|
85
|
+
placement=${ifDefined(tooltipPlacement ?? undefined)}
|
|
86
|
+
?disabled=${isDisabled}
|
|
87
|
+
>
|
|
88
|
+
${control}
|
|
89
|
+
</cosmoz-tooltip>`;
|
|
79
90
|
};
|
|
80
91
|
customElements.define('cosmoz-button', component(CosmozButton, {
|
|
81
92
|
observedAttributes,
|
package/dist/styles.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { skeumorphicHighlight } from '@neovici/cosmoz-tokens/skeumorphic';
|
|
2
1
|
import { css } from '@pionjs/pion';
|
|
3
2
|
/**
|
|
4
3
|
* Button styles using cosmoz-tokens design system
|
|
@@ -18,16 +17,22 @@ export const styles = css `
|
|
|
18
17
|
display: none;
|
|
19
18
|
}
|
|
20
19
|
|
|
20
|
+
/* Keeps the inner control stretching with host-driven sizing. */
|
|
21
|
+
:host > cosmoz-tooltip {
|
|
22
|
+
display: flex;
|
|
23
|
+
width: 100%;
|
|
24
|
+
}
|
|
25
|
+
|
|
21
26
|
/* ========================================
|
|
22
27
|
* SIZE VARIANTS
|
|
23
28
|
* ======================================== */
|
|
24
29
|
|
|
25
30
|
:host([size='sm']) .button {
|
|
26
|
-
height:
|
|
27
|
-
padding: calc(var(--cz-spacing) *
|
|
28
|
-
font-size: var(--cz-text-
|
|
29
|
-
line-height: var(--cz-text-
|
|
30
|
-
border-radius: var(--cz-radius-
|
|
31
|
+
height: 28px;
|
|
32
|
+
padding: calc(var(--cz-spacing) * 1) calc(var(--cz-spacing) * 2.5);
|
|
33
|
+
font-size: var(--cz-text-xs);
|
|
34
|
+
line-height: var(--cz-text-xs-line-height);
|
|
35
|
+
border-radius: var(--cz-radius-sm);
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
:host([size='sm']) ::slotted(svg) {
|
|
@@ -36,23 +41,64 @@ export const styles = css `
|
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
:host([size='lg']) .button {
|
|
39
|
-
height:
|
|
40
|
-
padding: calc(var(--cz-spacing) * 2
|
|
41
|
-
font-size: var(--cz-text-
|
|
42
|
-
line-height: var(--cz-text-
|
|
44
|
+
height: 36px;
|
|
45
|
+
padding: calc(var(--cz-spacing) * 2) calc(var(--cz-spacing) * 3.5);
|
|
46
|
+
font-size: var(--cz-text-sm);
|
|
47
|
+
line-height: var(--cz-text-sm-line-height);
|
|
43
48
|
border-radius: var(--cz-radius-md);
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
:host([size='xl']) .button {
|
|
47
|
-
height:
|
|
48
|
-
padding: calc(var(--cz-spacing) *
|
|
49
|
-
font-size: var(--cz-text-
|
|
50
|
-
line-height: var(--cz-text-
|
|
52
|
+
height: 40px;
|
|
53
|
+
padding: calc(var(--cz-spacing) * 2.5) calc(var(--cz-spacing) * 4);
|
|
54
|
+
font-size: var(--cz-text-sm);
|
|
55
|
+
line-height: var(--cz-text-sm-line-height);
|
|
56
|
+
border-radius: var(--cz-radius-md);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* ========================================
|
|
60
|
+
* ICON ONLY
|
|
61
|
+
* ======================================== */
|
|
62
|
+
|
|
63
|
+
:host([icon-only]) .button {
|
|
64
|
+
width: 32px;
|
|
65
|
+
height: 32px;
|
|
66
|
+
padding: calc(var(--cz-spacing) * 1.5);
|
|
67
|
+
font-size: var(--cz-text-sm);
|
|
68
|
+
line-height: var(--cz-text-sm-line-height);
|
|
51
69
|
border-radius: var(--cz-radius-md);
|
|
52
70
|
}
|
|
53
71
|
|
|
72
|
+
:host([icon-only][size='sm']) .button {
|
|
73
|
+
width: 28px;
|
|
74
|
+
height: 28px;
|
|
75
|
+
padding: calc(var(--cz-spacing) * 1.5);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
:host([icon-only][size='lg']) .button {
|
|
79
|
+
width: 36px;
|
|
80
|
+
height: 36px;
|
|
81
|
+
padding: calc(var(--cz-spacing) * 1.5);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
:host([icon-only][size='xl']) .button {
|
|
85
|
+
width: 40px;
|
|
86
|
+
height: 40px;
|
|
87
|
+
padding: calc(var(--cz-spacing) * 1.5);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
:host([icon-only]) ::slotted(svg) {
|
|
91
|
+
width: 20px;
|
|
92
|
+
height: 20px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
:host([icon-only][size='sm']) ::slotted(svg) {
|
|
96
|
+
width: 16px;
|
|
97
|
+
height: 16px;
|
|
98
|
+
}
|
|
99
|
+
|
|
54
100
|
/* ========================================
|
|
55
|
-
* BUTTON BASE STYLES
|
|
101
|
+
* BUTTON BASE STYLES
|
|
56
102
|
* ======================================== */
|
|
57
103
|
|
|
58
104
|
.button {
|
|
@@ -62,41 +108,44 @@ export const styles = css `
|
|
|
62
108
|
gap: 8px;
|
|
63
109
|
cursor: pointer;
|
|
64
110
|
font-family: var(--cz-font-body);
|
|
65
|
-
font-weight: var(--cz-font-weight-
|
|
111
|
+
font-weight: var(--cz-font-weight-medium);
|
|
66
112
|
text-decoration: none;
|
|
67
113
|
transition:
|
|
68
114
|
background-color 0.15s ease,
|
|
69
|
-
box-shadow 0.15s ease
|
|
115
|
+
box-shadow 0.15s ease,
|
|
116
|
+
transform 0.1s ease;
|
|
70
117
|
width: 100%;
|
|
71
118
|
white-space: nowrap;
|
|
72
119
|
border: none;
|
|
73
120
|
background: none;
|
|
74
121
|
text-align: center;
|
|
75
122
|
|
|
76
|
-
/* Medium (md)
|
|
77
|
-
height:
|
|
78
|
-
padding: calc(var(--cz-spacing) *
|
|
123
|
+
/* Medium (md) default size */
|
|
124
|
+
height: 32px;
|
|
125
|
+
padding: calc(var(--cz-spacing) * 1.5) calc(var(--cz-spacing) * 3);
|
|
79
126
|
font-size: var(--cz-text-sm);
|
|
80
127
|
line-height: var(--cz-text-sm-line-height);
|
|
81
128
|
border-radius: var(--cz-radius-md);
|
|
82
129
|
|
|
83
|
-
/* Primary - default variant */
|
|
84
|
-
${skeumorphicHighlight}
|
|
85
130
|
background-color: var(--cz-color-bg-brand-solid);
|
|
86
131
|
color: var(--cz-color-text-on-brand);
|
|
87
|
-
box-shadow: var(--cz-shadow-xs
|
|
132
|
+
box-shadow: var(--cz-shadow-xs);
|
|
88
133
|
|
|
89
134
|
&:hover {
|
|
90
135
|
background-color: var(--cz-color-bg-brand-solid-hover);
|
|
91
136
|
}
|
|
92
137
|
|
|
138
|
+
&:active:not(:disabled) {
|
|
139
|
+
transform: translateY(1px);
|
|
140
|
+
}
|
|
141
|
+
|
|
93
142
|
&:active {
|
|
94
143
|
background-color: var(--cz-color-brand-800);
|
|
95
144
|
}
|
|
96
145
|
|
|
97
146
|
&:focus-visible {
|
|
98
147
|
outline: none;
|
|
99
|
-
box-shadow: var(--cz-shadow-xs
|
|
148
|
+
box-shadow: var(--cz-shadow-xs), var(--cz-focus-ring);
|
|
100
149
|
}
|
|
101
150
|
}
|
|
102
151
|
|
|
@@ -107,6 +156,9 @@ export const styles = css `
|
|
|
107
156
|
:host([variant='secondary']) .button {
|
|
108
157
|
background-color: var(--cz-color-bg-primary);
|
|
109
158
|
color: var(--cz-color-text-secondary);
|
|
159
|
+
box-shadow:
|
|
160
|
+
inset 0 0 0 1px var(--cz-color-border-primary),
|
|
161
|
+
var(--cz-shadow-xs);
|
|
110
162
|
|
|
111
163
|
&:hover {
|
|
112
164
|
background-color: var(--cz-color-bg-primary-hover);
|
|
@@ -118,7 +170,7 @@ export const styles = css `
|
|
|
118
170
|
}
|
|
119
171
|
|
|
120
172
|
&:focus-visible {
|
|
121
|
-
box-shadow: var(--cz-shadow-xs
|
|
173
|
+
box-shadow: var(--cz-shadow-xs), var(--cz-focus-ring);
|
|
122
174
|
}
|
|
123
175
|
}
|
|
124
176
|
|
|
@@ -127,10 +179,6 @@ export const styles = css `
|
|
|
127
179
|
color: var(--cz-color-text-secondary);
|
|
128
180
|
box-shadow: none;
|
|
129
181
|
|
|
130
|
-
&::before {
|
|
131
|
-
display: none;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
182
|
&:hover {
|
|
135
183
|
background-color: var(--cz-color-bg-primary-hover);
|
|
136
184
|
color: var(--cz-color-text-secondary-hover);
|
|
@@ -157,7 +205,7 @@ export const styles = css `
|
|
|
157
205
|
}
|
|
158
206
|
|
|
159
207
|
&:focus-visible {
|
|
160
|
-
box-shadow: var(--cz-shadow-xs
|
|
208
|
+
box-shadow: var(--cz-shadow-xs), var(--cz-focus-ring-error);
|
|
161
209
|
}
|
|
162
210
|
}
|
|
163
211
|
|
|
@@ -168,10 +216,6 @@ export const styles = css `
|
|
|
168
216
|
padding: 0;
|
|
169
217
|
height: auto;
|
|
170
218
|
|
|
171
|
-
&::before {
|
|
172
|
-
display: none;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
219
|
&:hover {
|
|
176
220
|
text-decoration: underline;
|
|
177
221
|
color: var(--cz-color-text-brand-hover);
|
|
@@ -179,6 +223,7 @@ export const styles = css `
|
|
|
179
223
|
|
|
180
224
|
&:active {
|
|
181
225
|
color: var(--cz-color-brand-800);
|
|
226
|
+
transform: none;
|
|
182
227
|
}
|
|
183
228
|
|
|
184
229
|
&:focus-visible {
|
|
@@ -188,6 +233,97 @@ export const styles = css `
|
|
|
188
233
|
}
|
|
189
234
|
}
|
|
190
235
|
|
|
236
|
+
/* ========================================
|
|
237
|
+
* ICON ONLY COLORS (Untitled UI utility button)
|
|
238
|
+
* ======================================== */
|
|
239
|
+
|
|
240
|
+
:host([icon-only][variant='secondary']) .button,
|
|
241
|
+
:host([icon-only][variant='tertiary']) .button {
|
|
242
|
+
color: var(--cz-color-text-tertiary);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
:host([icon-only][variant='secondary']:not([disabled]):hover) .button,
|
|
246
|
+
:host([icon-only][variant='secondary']) .button:hover,
|
|
247
|
+
:host([icon-only][variant='tertiary']:not([disabled]):hover) .button,
|
|
248
|
+
:host([icon-only][variant='tertiary']) .button:hover {
|
|
249
|
+
color: var(--cz-color-text-secondary);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* ========================================
|
|
253
|
+
* PRESSED / SELECTED STATE (aria-pressed)
|
|
254
|
+
* Quiet variants (secondary, tertiary) shift to the selected brand
|
|
255
|
+
* chip; primary and destructive stay solid but visually "sink" with
|
|
256
|
+
* an inset shadow; link emphasizes text with no surface change.
|
|
257
|
+
* ======================================== */
|
|
258
|
+
|
|
259
|
+
:host([aria-pressed='true']) .button {
|
|
260
|
+
box-shadow: var(--cz-shadow-pressed-3d);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
:host([variant='secondary'][aria-pressed='true']) .button {
|
|
264
|
+
box-shadow:
|
|
265
|
+
var(--cz-shadow-pressed-3d),
|
|
266
|
+
inset 0 0 0 1px
|
|
267
|
+
light-dark(var(--cz-color-brand-300), var(--cz-color-brand-500));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
:host([aria-pressed='true']) .button {
|
|
271
|
+
background-color: light-dark(
|
|
272
|
+
var(--cz-color-brand-50),
|
|
273
|
+
var(--cz-color-brand-900)
|
|
274
|
+
);
|
|
275
|
+
color: light-dark(var(--cz-color-brand-700), var(--cz-color-brand-300));
|
|
276
|
+
|
|
277
|
+
&:hover {
|
|
278
|
+
background-color: light-dark(
|
|
279
|
+
var(--cz-color-brand-100),
|
|
280
|
+
var(--cz-color-brand-800)
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* Overrides the muted icon-only colors above (higher specificity). */
|
|
286
|
+
:host([icon-only][variant='secondary'][aria-pressed='true']) .button,
|
|
287
|
+
:host([icon-only][variant='tertiary'][aria-pressed='true']) .button {
|
|
288
|
+
color: light-dark(var(--cz-color-brand-700), var(--cz-color-brand-300));
|
|
289
|
+
|
|
290
|
+
&:hover {
|
|
291
|
+
color: light-dark(var(--cz-color-brand-700), var(--cz-color-brand-200));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
:host(:not([variant])[aria-pressed='true']) .button,
|
|
296
|
+
:host([variant='primary'][aria-pressed='true']) .button {
|
|
297
|
+
background-color: var(--cz-color-bg-brand-solid);
|
|
298
|
+
color: var(--cz-color-text-on-brand);
|
|
299
|
+
box-shadow: var(--cz-shadow-xs), var(--cz-shadow-pressed-3d-solid);
|
|
300
|
+
|
|
301
|
+
&:hover {
|
|
302
|
+
background-color: var(--cz-color-bg-brand-solid-hover);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
:host([variant='destructive'][aria-pressed='true']) .button {
|
|
307
|
+
background-color: var(--cz-color-bg-error-solid);
|
|
308
|
+
color: var(--cz-color-text-on-brand);
|
|
309
|
+
box-shadow: var(--cz-shadow-xs), var(--cz-shadow-pressed-3d-solid);
|
|
310
|
+
|
|
311
|
+
&:hover {
|
|
312
|
+
background-color: var(--cz-color-bg-error-solid-hover);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
:host([variant='link'][aria-pressed='true']) .button {
|
|
317
|
+
background-color: transparent;
|
|
318
|
+
box-shadow: none;
|
|
319
|
+
color: light-dark(var(--cz-color-brand-700), var(--cz-color-brand-300));
|
|
320
|
+
text-decoration: underline;
|
|
321
|
+
|
|
322
|
+
&:hover {
|
|
323
|
+
background-color: transparent;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
191
327
|
/* ========================================
|
|
192
328
|
* DISABLED STATE
|
|
193
329
|
* ======================================== */
|
|
@@ -195,10 +331,6 @@ export const styles = css `
|
|
|
195
331
|
:host([disabled]) .button {
|
|
196
332
|
cursor: not-allowed;
|
|
197
333
|
pointer-events: none;
|
|
198
|
-
|
|
199
|
-
&::before {
|
|
200
|
-
display: none;
|
|
201
|
-
}
|
|
202
334
|
}
|
|
203
335
|
|
|
204
336
|
:host([disabled]) .button,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neovici/cosmoz-button",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A customizable button web component built with Untitled UI design tokens",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web-components",
|
|
@@ -50,15 +50,15 @@
|
|
|
50
50
|
"./cosmoz-button": "./dist/cosmoz-button.js"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@neovici/cosmoz-tokens": "^
|
|
53
|
+
"@neovici/cosmoz-tokens": "^4.2.0",
|
|
54
|
+
"@neovici/cosmoz-tooltip": "^1.4.0",
|
|
54
55
|
"@pionjs/pion": "^2.5.2",
|
|
55
56
|
"lit-html": "^3.3.1"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
|
-
"@changesets/cli": "^
|
|
59
|
-
"@
|
|
60
|
-
"@
|
|
61
|
-
"@neovici/cfg": "^2.13.0",
|
|
59
|
+
"@changesets/cli": "^3.0.3",
|
|
60
|
+
"@fontsource-variable/geist": "^5.3.0",
|
|
61
|
+
"@neovici/cfg": "^2.15.0",
|
|
62
62
|
"@neovici/testing": "^2.0.0",
|
|
63
63
|
"@open-wc/testing": "^4.0.0",
|
|
64
64
|
"@playwright/test": "1.60.0",
|
|
@@ -74,8 +74,5 @@
|
|
|
74
74
|
"sinon": "^21.0.1",
|
|
75
75
|
"storybook": "^10.2.4",
|
|
76
76
|
"typescript": "^5.4.3"
|
|
77
|
-
},
|
|
78
|
-
"overrides": {
|
|
79
|
-
"conventional-changelog-conventionalcommits": ">= 8.0.0"
|
|
80
77
|
}
|
|
81
78
|
}
|