@descope-ui/descope-attachment 3.2.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/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver).
4
+
5
+ ## [3.2.0](https://github.com/descope/web-components-ui/compare/web-components-ui-3.1.13...web-components-ui-3.2.0) (2026-04-16)
6
+
7
+
8
+ ### Features
9
+
10
+ * add descope-last-auth-badge component ([#982](https://github.com/descope/web-components-ui/issues/982)) ([055b461](https://github.com/descope/web-components-ui/commit/055b4618abbdf518494a2984e56d66e58fdb4809))
@@ -0,0 +1,29 @@
1
+ import { test, expect } from '@playwright/test';
2
+ import { getStoryUrl, loopConfig } from 'e2e-utils';
3
+
4
+ const componentAttributes = {
5
+ position: [
6
+ 'top-start',
7
+ 'top-end',
8
+ 'top-center',
9
+ 'bottom-start',
10
+ 'bottom-end',
11
+ 'bottom-center',
12
+ ],
13
+ 'offset-x': [0, 20, -20],
14
+ 'offset-y': [0, 20, -20],
15
+ direction: ['ltr', 'rtl'],
16
+ };
17
+
18
+ const storyName = 'descope-attachment';
19
+
20
+ test.describe('theme', () => {
21
+ loopConfig(componentAttributes, (attr, value) => {
22
+ test(`${attr}: ${value}`, async ({ page }) => {
23
+ await page.goto(getStoryUrl(storyName, { [attr]: value }), {
24
+ waitUntil: 'networkidle',
25
+ });
26
+ expect(await page.screenshot()).toMatchSnapshot();
27
+ });
28
+ });
29
+ });
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@descope-ui/descope-attachment",
3
+ "version": "3.2.0",
4
+ "exports": {
5
+ ".": {
6
+ "import": "./src/component/index.js"
7
+ },
8
+ "./theme": {
9
+ "import": "./src/theme.js"
10
+ },
11
+ "./class": {
12
+ "import": "./src/component/AttachmentClass.js"
13
+ }
14
+ },
15
+ "devDependencies": {
16
+ "@playwright/test": "1.58.2",
17
+ "e2e-utils": "3.2.0",
18
+ "test-assets": "3.2.0",
19
+ "test-drivers": "3.2.0"
20
+ },
21
+ "dependencies": {
22
+ "@descope-ui/common": "3.2.0",
23
+ "@descope-ui/theme-globals": "3.2.0"
24
+ },
25
+ "publishConfig": {
26
+ "link-workspace-packages": false
27
+ },
28
+ "scripts": {
29
+ "test": "echo 'No tests defined' && exit 0",
30
+ "test:e2e": "playwright test"
31
+ }
32
+ }
package/project.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "@descope-ui/descope-attachment",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/web-components/components/descope-attachment/src",
5
+ "projectType": "library",
6
+ "targets": {},
7
+ "tags": []
8
+ }
@@ -0,0 +1,152 @@
1
+ import {
2
+ componentNameValidationMixin,
3
+ createStyleMixin,
4
+ draggableMixin,
5
+ } from '@descope-ui/common/components-mixins';
6
+ import { createBaseClass } from '@descope-ui/common/base-classes';
7
+ import {
8
+ getComponentName,
9
+ injectStyle,
10
+ } from '@descope-ui/common/components-helpers';
11
+ import { compose } from '@descope-ui/common/utils';
12
+
13
+ export const componentName = getComponentName('attachment');
14
+
15
+ const ATTACHMENT_POSITIONS = [
16
+ 'top-end',
17
+ 'top-start',
18
+ 'top-center',
19
+ 'bottom-start',
20
+ 'bottom-end',
21
+ 'bottom-center',
22
+ ];
23
+
24
+ const DEFAULT_POSITION = ATTACHMENT_POSITIONS[0];
25
+
26
+ class RawAttachment extends createBaseClass({
27
+ componentName,
28
+ baseSelector: ':host > .wrapper',
29
+ }) {
30
+ constructor() {
31
+ super();
32
+
33
+ this.attachShadow({ mode: 'open' }).innerHTML = `
34
+ <div class="wrapper">
35
+ <slot></slot>
36
+ <div class="attachment-container">
37
+ <slot name="attachment"></slot>
38
+ </div>
39
+ </div>
40
+ `;
41
+
42
+ this.defaultSlot = this.shadowRoot.querySelector('slot:not([name])');
43
+ this.attachmentSlot = this.shadowRoot.querySelector(
44
+ 'slot[name="attachment"]',
45
+ );
46
+ }
47
+
48
+ static get observedAttributes() {
49
+ return [...(super.observedAttributes || []), 'position'];
50
+ }
51
+
52
+ attributeChangedCallback(name, oldVal, newVal) {
53
+ super.attributeChangedCallback?.(name, oldVal, newVal);
54
+ if (name === 'position' && oldVal !== newVal) {
55
+ this.#handlePositionChange();
56
+ }
57
+ }
58
+
59
+ init() {
60
+ super.init?.();
61
+
62
+ injectStyle(
63
+ `
64
+ :host {
65
+ display: inline-block;
66
+ }
67
+ .wrapper {
68
+ position: relative;
69
+ display: inline-flex;
70
+ }
71
+ .attachment-container {
72
+ position: absolute;
73
+ z-index: 1;
74
+ pointer-events: none;
75
+ width: 100%;
76
+ display: flex;
77
+ align-items: center;
78
+ container-type: inline-size;
79
+ }
80
+ :host(.hidden) {
81
+ display: none;
82
+ }
83
+ `,
84
+ this,
85
+ );
86
+
87
+ this.#handlePositionChange();
88
+
89
+ this.defaultSlot.addEventListener('slotchange', () => {
90
+ this.#setVisibility();
91
+ this.#syncDirection();
92
+ });
93
+
94
+ window.requestAnimationFrame(() => {
95
+ this.#setVisibility();
96
+ this.#syncDirection();
97
+ });
98
+ }
99
+
100
+ #setVisibility() {
101
+ const hasAnchor = this.defaultSlot?.assignedElements()?.length > 0;
102
+ this.classList.toggle('hidden', !hasAnchor);
103
+ }
104
+
105
+ #syncDirection() {
106
+ const child = this.defaultSlot?.assignedElements()?.[0];
107
+ if (!child) return;
108
+
109
+ const { direction } = window.getComputedStyle(child);
110
+
111
+ // currently we support direction sync only for web-components-ui
112
+ // elements, which support st-host-direction attribute.
113
+ this.attachmentSlot?.assignedElements().forEach((el) => {
114
+ el.setAttribute('st-host-direction', direction);
115
+ });
116
+ }
117
+
118
+ get offsetX() {
119
+ return this.getAttribute('offset-x') || '0px';
120
+ }
121
+
122
+ get offsetY() {
123
+ return this.getAttribute('offset-y') || '0px';
124
+ }
125
+
126
+ #handlePositionChange() {
127
+ const pos = this.getAttribute('position');
128
+ if (!ATTACHMENT_POSITIONS.includes(pos)) {
129
+ this.setAttribute('position', DEFAULT_POSITION);
130
+ }
131
+ }
132
+ }
133
+
134
+ const attachmentContainer = { selector: () => '.attachment-container' };
135
+
136
+ export const AttachmentClass = compose(
137
+ createStyleMixin({
138
+ mappings: {
139
+ transform: { ...attachmentContainer },
140
+ justifyContent: { ...attachmentContainer },
141
+ maxWidth: { ...attachmentContainer },
142
+ top: { ...attachmentContainer },
143
+ bottom: { ...attachmentContainer },
144
+ left: { ...attachmentContainer },
145
+ right: { ...attachmentContainer },
146
+ offsetX: {},
147
+ offsetY: {},
148
+ },
149
+ }),
150
+ draggableMixin,
151
+ componentNameValidationMixin,
152
+ )(RawAttachment);
@@ -0,0 +1,5 @@
1
+ import { componentName, AttachmentClass } from './AttachmentClass';
2
+
3
+ customElements.define(componentName, AttachmentClass);
4
+
5
+ export { AttachmentClass, componentName };
package/src/theme.js ADDED
@@ -0,0 +1,52 @@
1
+ import { useVar } from '@descope-ui/common/theme-helpers';
2
+ import { AttachmentClass } from './component/AttachmentClass';
3
+
4
+ const vars = AttachmentClass.cssVarList;
5
+
6
+ const defaultOffset = '8px';
7
+ const insetCalc = `calc(${defaultOffset} + var(${vars.offsetX}))`;
8
+
9
+ const attachment = {
10
+ [vars.maxWidth]: `calc(100% - 2 * ${defaultOffset})`,
11
+
12
+ position: {
13
+ 'top-start': {
14
+ [vars.transform]: 'translateY(-50%)',
15
+ [vars.justifyContent]: 'start',
16
+ [vars.left]: insetCalc,
17
+ [vars.top]: useVar(vars.offsetY),
18
+ },
19
+ 'top-center': {
20
+ [vars.transform]: 'translate(-50%, -50%)',
21
+ [vars.justifyContent]: 'center',
22
+ [vars.top]: useVar(vars.offsetY),
23
+ [vars.left]: '50%',
24
+ },
25
+ 'top-end': {
26
+ [vars.transform]: 'translateY(-50%)',
27
+ [vars.justifyContent]: 'end',
28
+ [vars.right]: insetCalc,
29
+ [vars.top]: useVar(vars.offsetY),
30
+ },
31
+ 'bottom-start': {
32
+ [vars.transform]: 'translateY(50%)',
33
+ [vars.justifyContent]: 'start',
34
+ [vars.left]: insetCalc,
35
+ [vars.bottom]: useVar(vars.offsetY),
36
+ },
37
+ 'bottom-center': {
38
+ [vars.bottom]: useVar(vars.offsetY),
39
+ [vars.transform]: 'translate(-50%, 50%)',
40
+ [vars.justifyContent]: 'center',
41
+ [vars.left]: '50%',
42
+ },
43
+ 'bottom-end': {
44
+ [vars.transform]: 'translateY(50%)',
45
+ [vars.justifyContent]: 'end',
46
+ [vars.right]: insetCalc,
47
+ [vars.bottom]: useVar(vars.offsetY),
48
+ },
49
+ },
50
+ };
51
+
52
+ export default attachment;
@@ -0,0 +1,113 @@
1
+ import { directionControl } from '@descope-ui/common/sb-controls';
2
+ import { componentName } from '../src/component';
3
+ import { base64svg } from 'test-assets';
4
+
5
+ const SIZES = ['xs', 'sm', 'md', 'lg'];
6
+
7
+ const button = (size, { direction, label } = {}) => `
8
+ <descope-button
9
+ variant="outline"
10
+ mode="primary"
11
+ size="${size}"
12
+ ${direction ? ` st-host-direction="${direction}"` : ''}
13
+ ${label ? '' : ' square="true"'}>
14
+ ${label || `<descope-icon src=${base64svg}></descope-icon>`}
15
+ </descope-button>
16
+ `;
17
+
18
+ const attachment = (position, anchor, badgeLabel, offsetX, offsetY) => `
19
+ <descope-attachment position="${position}" st-offset-x="${offsetX}px" st-offset-y="${offsetY}px">
20
+ ${anchor}
21
+ <descope-badge
22
+ slot="attachment"
23
+ mode="primary"
24
+ variant="contained"
25
+ size="xs"
26
+ shadow="sm"
27
+ bordered="true"
28
+ shrink-to-indicator="true"
29
+ st-offset-x="${offsetX}px"
30
+ st-offset-y="${offsetY}px"
31
+ >
32
+ ${badgeLabel}
33
+ </descope-badge>
34
+ </descope-attachment>`;
35
+
36
+ const Template = ({
37
+ position,
38
+ buttonLabel,
39
+ badgeLabel,
40
+ direction,
41
+ 'offset-x': offsetX,
42
+ 'offset-y': offsetY,
43
+ }) => `
44
+ <div class="item">
45
+ ${SIZES.map((size) =>
46
+ attachment(
47
+ position,
48
+ button(size, { direction, label: buttonLabel }),
49
+ badgeLabel,
50
+ offsetX,
51
+ offsetY,
52
+ ),
53
+ ).join('')}
54
+ ${SIZES.map((size) =>
55
+ attachment(position, button(size), badgeLabel, offsetX, offsetY),
56
+ ).join('')}
57
+ </div>
58
+ `;
59
+
60
+ export default {
61
+ component: componentName,
62
+ title: 'descope-attachment',
63
+ parameters: {
64
+ controls: { expanded: true },
65
+ },
66
+ decorators: [
67
+ (story) => `
68
+ <style nonce="${window.DESCOPE_NONCE}">
69
+ .item {
70
+ display: flex;
71
+ flex-direction: column;
72
+ align-items: flex-start;
73
+ gap: 16px;
74
+ }
75
+ </style>
76
+ ${story()}
77
+ `,
78
+ ],
79
+ argTypes: {
80
+ position: {
81
+ control: { type: 'select' },
82
+ options: [
83
+ 'top-end',
84
+ 'top-start',
85
+ 'top-center',
86
+ 'bottom-end',
87
+ 'bottom-start',
88
+ 'bottom-center',
89
+ ],
90
+ },
91
+ ...directionControl,
92
+ badgeLabel: {
93
+ name: 'Badge Label',
94
+ control: 'text',
95
+ },
96
+ 'offset-x': {
97
+ control: 'number',
98
+ },
99
+ 'offset-y': {
100
+ control: 'number',
101
+ },
102
+ },
103
+ };
104
+
105
+ export const Default = Template.bind({});
106
+ Default.args = {
107
+ position: 'top-end',
108
+ badgeLabel: 'Last used -',
109
+ buttonLabel: 'Sign in with Google -',
110
+ direction: 'ltr',
111
+ 'offset-x': 0,
112
+ 'offset-y': 2,
113
+ };