@descope-ui/descope-button 0.0.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
+
5
+ ## 0.0.1 (2025-03-04)
6
+
7
+ ### Dependency Updates
8
+
9
+ * `e2e-utils` updated to version `0.0.1`
10
+ * `test-assets` updated to version `0.0.1`
11
+ * `@descope-ui/common` updated to version `0.0.7`
12
+ * `@descope-ui/theme-globals` updated to version `0.0.7`
13
+ * `@descope-ui/descope-icon` updated to version `0.0.1`
14
+ # Changelog
@@ -0,0 +1,73 @@
1
+ import { expect, test } from '@playwright/test';
2
+ import { getStoryUrl, loopConfig, loopPresets } from 'e2e-utils';
3
+
4
+ const componentAttributes = {
5
+ mode: ['primary', 'secondary', 'success', 'error'],
6
+ variant: ['contained', 'outline', 'link'],
7
+ size: ['xs', 'sm', 'md', 'lg'],
8
+ 'full-width': ['true', 'false'],
9
+ square: ['true', 'false'],
10
+ icon: 'true',
11
+ // loading: 'true',
12
+ };
13
+
14
+ const presets = {
15
+ 'direction rtl': { direction: 'rtl', text: '-Click Me' },
16
+ 'direction rtl with icon': { direction: 'rtl', text: '-Click Me', icon: 'true' },
17
+ };
18
+
19
+ const storyName = 'descope-button';
20
+ const componentName = 'descope-button';
21
+
22
+ test.describe('theme', () => {
23
+ loopConfig(componentAttributes, (attr, value) => {
24
+ test.describe(`${attr}: ${value}`, () => {
25
+ test.beforeEach(async ({ page }) => {
26
+ await page.goto(getStoryUrl(storyName, { [attr]: value }), { waitUntil: 'load' });
27
+ });
28
+ test('hover', async ({ page }) => {
29
+ const component = page.locator(componentName);
30
+
31
+ await component.hover();
32
+
33
+ expect(await component.screenshot()).toMatchSnapshot();
34
+ });
35
+
36
+ test('focus', async ({ page }) => {
37
+ const component = page.locator(componentName);
38
+
39
+ await component.focus();
40
+
41
+ expect(await component.screenshot()).toMatchSnapshot();
42
+ });
43
+
44
+ test('active', async ({ page }) => {
45
+ const component = page.locator(componentName);
46
+
47
+ await component.hover();
48
+ await page.mouse.down();
49
+
50
+ expect(await component.screenshot()).toMatchSnapshot();
51
+ });
52
+ });
53
+ });
54
+
55
+ loopPresets(presets, (preset, name) => {
56
+ test(name, async ({ page }) => {
57
+ await page.goto(getStoryUrl(storyName, preset));
58
+ await page.waitForSelector(componentName);
59
+ const component = page.locator(componentName);
60
+ expect(
61
+ await component.screenshot({ animations: 'disabled', timeout: 3000, caret: 'hide' })
62
+ ).toMatchSnapshot();
63
+ });
64
+ });
65
+ });
66
+
67
+ test.describe('other', () => {
68
+ test('disabled', async ({ page }) => {
69
+ await page.goto(getStoryUrl(storyName, { disabled: 'true' }), { waitUntil: 'networkidle' });
70
+ const component = page.locator(componentName);
71
+ expect(await component.screenshot()).toMatchSnapshot();
72
+ });
73
+ });
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@descope-ui/descope-button",
3
+ "version": "0.0.1",
4
+ "exports": {
5
+ ".": {
6
+ "import": "./src/component/index.js"
7
+ },
8
+ "./theme": {
9
+ "import": "./src/theme.js"
10
+ },
11
+ "./class": {
12
+ "import": "./src/component/ButtonClass.js"
13
+ }
14
+ },
15
+ "devDependencies": {
16
+ "@playwright/test": "1.38.1",
17
+ "e2e-utils": "0.0.1",
18
+ "test-assets": "0.0.1"
19
+ },
20
+ "dependencies": {
21
+ "@vaadin/button": "24.3.4",
22
+ "@descope-ui/common": "0.0.7",
23
+ "@descope-ui/theme-globals": "0.0.7",
24
+ "@descope-ui/descope-icon": "0.0.1"
25
+ },
26
+ "publishConfig": {
27
+ "link-workspace-packages": false
28
+ },
29
+ "scripts": {
30
+ "test": "echo 'No tests defined' && exit 0",
31
+ "test:e2e": "echo 'No e2e tests defined' && exit 0"
32
+ }
33
+ }
package/project.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@descope-ui/descope-button",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/web-components/components/descope-button/src",
5
+ "projectType": "library",
6
+ "targets": {
7
+ "version": {
8
+ "executor": "@jscutlery/semver:version",
9
+ "options": {
10
+ "trackDeps": true,
11
+ "push": false,
12
+ "preset": "conventional"
13
+ }
14
+ }
15
+ },
16
+ "tags": []
17
+ }
@@ -0,0 +1,163 @@
1
+ import { compose } from '@descope-ui/common/utils';
2
+ import { getComponentName } from '@descope-ui/common/components-helpers';
3
+ import {
4
+ createStyleMixin,
5
+ draggableMixin,
6
+ createProxy,
7
+ componentNameValidationMixin,
8
+ } from '@descope-ui/common/components-mixins';
9
+ import { IconClass } from '@descope-ui/descope-icon/class';
10
+ import { clickableMixin } from './clickableMixin';
11
+
12
+ export const componentName = getComponentName('button');
13
+
14
+ const resetStyles = `
15
+ :host {
16
+ display: inline-block;
17
+ box-sizing: border-box;
18
+ }
19
+ vaadin-button::before,
20
+ vaadin-button::after {
21
+ opacity: 0;
22
+ }
23
+ vaadin-button {
24
+ margin: 0;
25
+ min-width: 0;
26
+ width: 100%;
27
+ height: auto;
28
+ box-shadow: none;
29
+ }
30
+ vaadin-button::part(label) {
31
+ padding: 0;
32
+ width: 100%;
33
+ }
34
+ vaadin-button::part(prefix) {
35
+ margin-left: 0;
36
+ margin-right: 0;
37
+ }
38
+ `;
39
+
40
+ const iconStyles = `
41
+ vaadin-button::part(prefix),
42
+ vaadin-button::part(label) {
43
+ display: flex;
44
+ align-items: center;
45
+ }
46
+ `;
47
+
48
+ const editorOverrides = `vaadin-button::part(label) { pointer-events: none; }`;
49
+
50
+ const { host, label, slottedIcon } = {
51
+ host: { selector: () => ':host' },
52
+ label: { selector: '::part(label)' },
53
+ slottedIcon: { selector: () => '::slotted(descope-icon)' },
54
+ };
55
+
56
+ let loadingIndicatorStyles;
57
+
58
+ export const ButtonClass = compose(
59
+ createStyleMixin({
60
+ mappings: {
61
+ hostWidth: { property: 'width' },
62
+ hostHeight: { property: 'height' },
63
+ hostDirection: { ...host, property: 'direction' },
64
+ fontSize: {},
65
+ fontFamily: {},
66
+
67
+ cursor: {},
68
+ backgroundColor: {},
69
+
70
+ outlineOffset: {},
71
+ outlineColor: {},
72
+ outlineStyle: {},
73
+ outlineWidth: {},
74
+
75
+ borderRadius: {},
76
+ borderColor: {},
77
+ borderStyle: {},
78
+ borderWidth: {},
79
+
80
+ verticalPadding: [{ property: 'padding-top' }, { property: 'padding-bottom' }],
81
+ horizontalPadding: [
82
+ { property: 'padding-right', fallback: '0.875em' },
83
+ { property: 'padding-left', fallback: '0.875em' },
84
+ ],
85
+
86
+ labelTextColor: { property: 'color' },
87
+ iconColor: {
88
+ selector: () => `::slotted(*)`,
89
+ property: IconClass.cssVarList.fill,
90
+ },
91
+ labelTextDecoration: { ...label, property: 'text-decoration' },
92
+ labelSpacing: { ...label, property: 'gap' },
93
+ textAlign: { ...label, property: 'justify-content', fallback: 'center' },
94
+
95
+ iconSize: [
96
+ { ...slottedIcon, property: 'width' },
97
+ { ...slottedIcon, property: 'height' },
98
+ ],
99
+ },
100
+ }),
101
+ clickableMixin,
102
+ draggableMixin,
103
+ componentNameValidationMixin
104
+ )(
105
+ createProxy({
106
+ slots: ['', 'prefix', 'label', 'suffix'],
107
+ wrappedEleName: 'vaadin-button',
108
+ style: () => `
109
+ ${resetStyles}
110
+ ${iconStyles}
111
+ ${loadingIndicatorStyles}
112
+ ${editorOverrides}
113
+ :host {
114
+ padding: calc(var(${ButtonClass.cssVarList.outlineWidth}) + var(${ButtonClass.cssVarList.outlineOffset}));
115
+ }
116
+ :host([full-width="true"]) {
117
+ width: var(${ButtonClass.cssVarList.hostWidth});
118
+ }
119
+ vaadin-button {
120
+ height: calc(var(${ButtonClass.cssVarList.hostHeight}) - var(${ButtonClass.cssVarList.outlineWidth}) - var(${ButtonClass.cssVarList.outlineOffset}));
121
+ }
122
+ [square="true"]:not([full-width="true"]) {
123
+ width: calc(var(${ButtonClass.cssVarList.hostWidth}) - var(${ButtonClass.cssVarList.outlineWidth}) - var(${ButtonClass.cssVarList.outlineOffset}));
124
+ padding: 0;
125
+ }
126
+ `,
127
+ excludeAttrsSync: ['tabindex'],
128
+ componentName,
129
+ })
130
+ );
131
+
132
+ const { color, fontSize } = ButtonClass.cssVarList;
133
+ loadingIndicatorStyles = `
134
+ @keyframes spin {
135
+ 0% { -webkit-transform: rotate(0deg); }
136
+ 100% { -webkit-transform: rotate(360deg); }
137
+ }
138
+ :host([loading="true"]) ::before {
139
+ animation: spin 2s linear infinite;
140
+ position: absolute;
141
+ content: '';
142
+ z-index: 1;
143
+ box-sizing: border-box;
144
+ border-radius: 50%;
145
+ border-bottom-color: transparent;
146
+ border-left-color: transparent;
147
+ border-style: solid;
148
+ color: var(${color});
149
+ top: calc(50% - (var(${fontSize}) / 2));
150
+ left: calc(50% - (var(${fontSize}) / 2));
151
+ border-width: calc(var(${fontSize}) / 10);
152
+ width: var(${fontSize});
153
+ height: var(${fontSize});
154
+ }
155
+ :host([disabled="true"]),
156
+ :host([loading="true"]) {
157
+ pointer-events: none;
158
+ }
159
+ :host([loading="true"])::part(prefix),
160
+ :host([loading="true"])::part(label) {
161
+ visibility: hidden;
162
+ }
163
+ `;
@@ -0,0 +1,10 @@
1
+ export const clickableMixin = (superclass) =>
2
+ class ClickableMixinClass extends superclass {
3
+ get isLoading() {
4
+ return this.getAttribute('loading') === 'true';
5
+ }
6
+
7
+ click() {
8
+ this.isLoading || super.click();
9
+ }
10
+ };
@@ -0,0 +1,6 @@
1
+ import { componentName, ButtonClass } from './ButtonClass';
2
+ import '@vaadin/button';
3
+
4
+ customElements.define(componentName, ButtonClass);
5
+
6
+ export { ButtonClass, componentName };
package/src/theme.js ADDED
@@ -0,0 +1,133 @@
1
+ import globals from '@descope-ui/theme-globals';
2
+ import { getThemeRefs, createHelperVars } from '@descope-ui/common/theme-helpers';
3
+ import { componentName, ButtonClass } from './component/ButtonClass';
4
+
5
+ const globalRefs = getThemeRefs(globals);
6
+ const compVars = ButtonClass.cssVarList;
7
+
8
+ const mode = {
9
+ primary: globalRefs.colors.primary,
10
+ secondary: globalRefs.colors.secondary,
11
+ success: globalRefs.colors.success,
12
+ error: globalRefs.colors.error,
13
+ surface: globalRefs.colors.surface,
14
+ };
15
+
16
+ const [helperTheme, helperRefs, helperVars] = createHelperVars({ mode }, componentName);
17
+
18
+ const button = {
19
+ ...helperTheme,
20
+
21
+ [compVars.fontFamily]: globalRefs.fonts.font1.family,
22
+
23
+ [compVars.cursor]: 'pointer',
24
+ [compVars.hostHeight]: '3em',
25
+ [compVars.hostWidth]: 'auto',
26
+ [compVars.hostDirection]: globalRefs.direction,
27
+
28
+ [compVars.borderRadius]: globalRefs.radius.sm,
29
+ [compVars.borderWidth]: globalRefs.border.xs,
30
+ [compVars.borderStyle]: 'solid',
31
+ [compVars.borderColor]: 'transparent',
32
+
33
+ [compVars.labelSpacing]: '0.25em',
34
+
35
+ [compVars.textAlign]: 'center', // default text align center
36
+ textAlign: {
37
+ right: { [compVars.textAlign]: 'right' },
38
+ left: { [compVars.textAlign]: 'left' },
39
+ center: { [compVars.textAlign]: 'center' },
40
+ },
41
+
42
+ [compVars.verticalPadding]: '1em',
43
+ [compVars.horizontalPadding]: '0.875em',
44
+
45
+ [compVars.outlineWidth]: globals.border.sm,
46
+ [compVars.outlineOffset]: '0px', // keep `px` unit for external calc
47
+ [compVars.outlineStyle]: 'solid',
48
+ [compVars.outlineColor]: 'transparent',
49
+
50
+ [compVars.iconSize]: '1.5em',
51
+
52
+ size: {
53
+ xs: { [compVars.fontSize]: '12px' },
54
+ sm: { [compVars.fontSize]: '14px' },
55
+ md: { [compVars.fontSize]: '16px' },
56
+ lg: { [compVars.fontSize]: '18px' },
57
+ },
58
+
59
+ _square: {
60
+ [compVars.hostHeight]: '3em',
61
+ [compVars.hostWidth]: '3em',
62
+ [compVars.verticalPadding]: '0',
63
+ },
64
+
65
+ _fullWidth: {
66
+ [compVars.hostWidth]: '100%',
67
+ },
68
+
69
+ _loading: {
70
+ [compVars.cursor]: 'wait',
71
+ [compVars.labelTextColor]: helperRefs.main,
72
+ },
73
+
74
+ _disabled: {
75
+ [helperVars.main]: globalRefs.colors.surface.light,
76
+ [helperVars.dark]: globalRefs.colors.surface.dark,
77
+ [helperVars.light]: globalRefs.colors.surface.light,
78
+ [helperVars.contrast]: globalRefs.colors.surface.main,
79
+ [compVars.iconColor]: globalRefs.colors.surface.main,
80
+ },
81
+
82
+ variant: {
83
+ contained: {
84
+ [compVars.labelTextColor]: helperRefs.contrast,
85
+ [compVars.backgroundColor]: helperRefs.main,
86
+ _hover: {
87
+ [compVars.backgroundColor]: helperRefs.dark,
88
+ _loading: {
89
+ [compVars.backgroundColor]: helperRefs.main,
90
+ },
91
+ },
92
+ _active: {
93
+ [compVars.backgroundColor]: helperRefs.main,
94
+ },
95
+ },
96
+
97
+ outline: {
98
+ [compVars.labelTextColor]: helperRefs.main,
99
+ [compVars.borderColor]: helperRefs.main,
100
+ _hover: {
101
+ [compVars.labelTextColor]: helperRefs.dark,
102
+ [compVars.borderColor]: helperRefs.dark,
103
+ },
104
+ _active: {
105
+ [compVars.labelTextColor]: helperRefs.main,
106
+ [compVars.borderColor]: helperRefs.main,
107
+ },
108
+ },
109
+
110
+ link: {
111
+ [compVars.labelTextColor]: helperRefs.main,
112
+ [compVars.horizontalPadding]: '0.125em',
113
+ _hover: {
114
+ [compVars.labelTextColor]: helperRefs.dark,
115
+ [compVars.labelTextDecoration]: 'underline',
116
+ },
117
+ _active: {
118
+ [compVars.labelTextColor]: helperRefs.main,
119
+ },
120
+ },
121
+ },
122
+
123
+ _focused: {
124
+ [compVars.outlineColor]: helperRefs.light,
125
+ },
126
+ };
127
+
128
+ export default button;
129
+
130
+ export const vars = {
131
+ ...compVars,
132
+ ...helperVars,
133
+ };
@@ -0,0 +1,88 @@
1
+ import { componentName } from '../src/component';
2
+ import {
3
+ textContentControl,
4
+ sizeControl,
5
+ modeControl,
6
+ buttonVariantControl,
7
+ loadingControl,
8
+ fullWidthControl,
9
+ disabledControl,
10
+ directionControl,
11
+ textAlignControl,
12
+ labelSpacingControl,
13
+ } from '@descope-ui/common/sb-controls';
14
+
15
+ import {base64svg} from 'test-assets';
16
+
17
+ const Template = ({
18
+ text,
19
+ variant,
20
+ mode,
21
+ size,
22
+ loading,
23
+ icon,
24
+ square,
25
+ 'full-width': fullWidth,
26
+ disabled,
27
+ direction,
28
+ 'text-align': textAlign,
29
+ 'label-spacing': spacing,
30
+ }) => `
31
+ <style nonce="${window.DESCOPE_NONCE}">
32
+ .wrapper {
33
+ --descope-button-icon-color: currentColor;
34
+ }
35
+ </style>
36
+
37
+ <div class="wrapper">
38
+ <descope-button
39
+ mode="${mode}"
40
+ variant="${variant}"
41
+ size="${size}"
42
+ loading="${loading || false}"
43
+ disabled="${disabled || false}"
44
+ square="${square || false}"
45
+ full-width="${fullWidth || false}"
46
+ text-align="${textAlign}"
47
+ st-label-spacing="${spacing}px"
48
+ st-host-direction="${direction ?? ''}">
49
+ ${icon ? `<descope-icon src=${base64svg}></descope-icon>` : ''}
50
+ ${text}
51
+ </descope-button>
52
+ </div>
53
+ `;
54
+
55
+ export default {
56
+ component: componentName,
57
+ title: 'descope-button',
58
+ argTypes: {
59
+ ...textContentControl,
60
+ ...modeControl,
61
+ ...sizeControl,
62
+ ...buttonVariantControl,
63
+ ...loadingControl,
64
+ ...fullWidthControl,
65
+ ...disabledControl,
66
+ ...directionControl,
67
+ ...textAlignControl,
68
+ ...labelSpacingControl,
69
+ square: {
70
+ name: 'Square',
71
+ control: { type: 'boolean' },
72
+ },
73
+ },
74
+ };
75
+
76
+ export const Default = Template.bind({});
77
+
78
+ Default.args = {
79
+ text: 'Click',
80
+ variant: 'contained',
81
+ mode: 'primary',
82
+ size: 'md',
83
+ loading: false,
84
+ icon: false,
85
+ 'full-width': false,
86
+ 'text-align': 'center',
87
+ 'label-spacing': 4,
88
+ };