@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 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
@@ -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: ButtonVariant;
6
- size: ButtonSize;
7
- disabled: boolean;
8
- 'full-width': boolean;
9
- type: ButtonType;
10
- value: string | null;
11
- href: string | null;
12
- target: string | null;
13
- rel: string | null;
14
- download: string | null;
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)
@@ -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 = host.hasAttribute('disabled');
42
- const type = host.getAttribute('type') || 'button';
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.hasAttribute('disabled'))
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
- if (href != null) {
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=${disabled ? 'true' : nothing}
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
- return html `
75
- <button type=${type} class="button" ?disabled=${disabled} part="button">
76
- ${content}
77
- </button>
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: 36px;
27
- padding: calc(var(--cz-spacing) * 2) calc(var(--cz-spacing) * 3.5);
28
- font-size: var(--cz-text-sm);
29
- line-height: var(--cz-text-sm-line-height);
30
- border-radius: var(--cz-radius-md);
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: 44px;
40
- padding: calc(var(--cz-spacing) * 2.5) calc(var(--cz-spacing) * 4.5);
41
- font-size: var(--cz-text-base);
42
- line-height: var(--cz-text-base-line-height);
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: 48px;
48
- padding: calc(var(--cz-spacing) * 3) calc(var(--cz-spacing) * 5);
49
- font-size: var(--cz-text-base);
50
- line-height: var(--cz-text-base-line-height);
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 (Primary - default)
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-semibold);
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) - default size */
77
- height: 40px;
78
- padding: calc(var(--cz-spacing) * 2.5) calc(var(--cz-spacing) * 4);
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-skeumorphic);
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-skeumorphic), var(--cz-focus-ring);
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-skeumorphic), var(--cz-focus-ring);
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-skeumorphic), var(--cz-focus-ring-error);
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": "1.1.1",
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": "^3.2.0",
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": "^2.27.0",
59
- "@commitlint/cli": "^20.0.0",
60
- "@commitlint/config-conventional": "^20.4.1",
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
  }