@brandsync/wc 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.
Files changed (134) hide show
  1. package/LICENSE +21 -0
  2. package/custom-elements.json +1138 -0
  3. package/dist/brandsync-wc/brandsync-wc.css +1730 -0
  4. package/dist/brandsync-wc/brandsync-wc.esm.js +1 -0
  5. package/dist/brandsync-wc/brandsync-wc.esm.js.map +1 -0
  6. package/dist/brandsync-wc/index-Cqxk7LcP.js.map +1 -0
  7. package/dist/brandsync-wc/index.esm.js +1 -0
  8. package/dist/brandsync-wc/index.esm.js.map +1 -0
  9. package/dist/brandsync-wc/p-042ad15a.entry.js +1 -0
  10. package/dist/brandsync-wc/p-2c4df28d.entry.js +1 -0
  11. package/dist/brandsync-wc/p-3134f5f5.entry.js +1 -0
  12. package/dist/brandsync-wc/p-5739acaf.entry.js +1 -0
  13. package/dist/brandsync-wc/p-BOj_41o9.js +3 -0
  14. package/dist/brandsync-wc/p-c2cc0d6f.entry.js +1 -0
  15. package/dist/brandsync-wc/p-da11e421.entry.js +1 -0
  16. package/dist/brandsync-wc/p-db3934d2.entry.js +1 -0
  17. package/dist/brandsync-wc/p-e38a9744.entry.js +1 -0
  18. package/dist/cjs/brandsync-wc.cjs.js +24 -0
  19. package/dist/cjs/bs-badge.cjs.entry.js +19 -0
  20. package/dist/cjs/bs-button-skeleton.cjs.entry.js +19 -0
  21. package/dist/cjs/bs-button.cjs.entry.js +88 -0
  22. package/dist/cjs/bs-card.cjs.entry.js +20 -0
  23. package/dist/cjs/bs-composer.cjs.entry.js +116 -0
  24. package/dist/cjs/bs-data-table.cjs.entry.js +62 -0
  25. package/dist/cjs/bs-input.cjs.entry.js +42 -0
  26. package/dist/cjs/bs-modal.cjs.entry.js +69 -0
  27. package/dist/cjs/index-nyOcuVbF.js +1710 -0
  28. package/dist/cjs/index.cjs.js +7 -0
  29. package/dist/cjs/loader.cjs.js +12 -0
  30. package/dist/collection/collection-manifest.json +20 -0
  31. package/dist/collection/components/bs-badge/bs-badge.cmp.test.js +30 -0
  32. package/dist/collection/components/bs-badge/bs-badge.css +55 -0
  33. package/dist/collection/components/bs-badge/bs-badge.js +64 -0
  34. package/dist/collection/components/bs-badge/bs-badge.stories.js +75 -0
  35. package/dist/collection/components/bs-button/bs-button.cmp.test.js +81 -0
  36. package/dist/collection/components/bs-button/bs-button.css +247 -0
  37. package/dist/collection/components/bs-button/bs-button.js +242 -0
  38. package/dist/collection/components/bs-button/bs-button.stories.js +145 -0
  39. package/dist/collection/components/bs-button-skeleton/bs-button-skeleton.cmp.test.js +18 -0
  40. package/dist/collection/components/bs-button-skeleton/bs-button-skeleton.css +46 -0
  41. package/dist/collection/components/bs-button-skeleton/bs-button-skeleton.js +73 -0
  42. package/dist/collection/components/bs-button-skeleton/bs-button-skeleton.stories.js +25 -0
  43. package/dist/collection/components/bs-card/bs-card.cmp.test.js +42 -0
  44. package/dist/collection/components/bs-card/bs-card.css +31 -0
  45. package/dist/collection/components/bs-card/bs-card.js +70 -0
  46. package/dist/collection/components/bs-card/bs-card.stories.js +27 -0
  47. package/dist/collection/components/bs-composer/bs-composer.cmp.test.js +132 -0
  48. package/dist/collection/components/bs-composer/bs-composer.css +185 -0
  49. package/dist/collection/components/bs-composer/bs-composer.js +353 -0
  50. package/dist/collection/components/bs-composer/bs-composer.stories.js +66 -0
  51. package/dist/collection/components/bs-data-table/bs-data-table.css +56 -0
  52. package/dist/collection/components/bs-data-table/bs-data-table.js +259 -0
  53. package/dist/collection/components/bs-data-table/bs-data-table.stories.js +55 -0
  54. package/dist/collection/components/bs-input/bs-input.cmp.test.js +101 -0
  55. package/dist/collection/components/bs-input/bs-input.css +64 -0
  56. package/dist/collection/components/bs-input/bs-input.js +230 -0
  57. package/dist/collection/components/bs-input/bs-input.stories.js +40 -0
  58. package/dist/collection/components/bs-modal/bs-modal.cmp.test.js +112 -0
  59. package/dist/collection/components/bs-modal/bs-modal.css +66 -0
  60. package/dist/collection/components/bs-modal/bs-modal.js +198 -0
  61. package/dist/collection/components/bs-modal/bs-modal.stories.js +38 -0
  62. package/dist/collection/index.js +10 -0
  63. package/dist/collection/stories-utils.js +25 -0
  64. package/dist/collection/utils/utils.js +3 -0
  65. package/dist/collection/utils/utils.unit.test.js +16 -0
  66. package/dist/components/bs-badge.d.ts +11 -0
  67. package/dist/components/bs-badge.js +1 -0
  68. package/dist/components/bs-button-skeleton.d.ts +11 -0
  69. package/dist/components/bs-button-skeleton.js +1 -0
  70. package/dist/components/bs-button.d.ts +11 -0
  71. package/dist/components/bs-button.js +1 -0
  72. package/dist/components/bs-card.d.ts +11 -0
  73. package/dist/components/bs-card.js +1 -0
  74. package/dist/components/bs-composer.d.ts +11 -0
  75. package/dist/components/bs-composer.js +1 -0
  76. package/dist/components/bs-data-table.d.ts +11 -0
  77. package/dist/components/bs-data-table.js +1 -0
  78. package/dist/components/bs-input.d.ts +11 -0
  79. package/dist/components/bs-input.js +1 -0
  80. package/dist/components/bs-modal.d.ts +11 -0
  81. package/dist/components/bs-modal.js +1 -0
  82. package/dist/components/index.d.ts +35 -0
  83. package/dist/components/index.js +1 -0
  84. package/dist/components/p-Cu4gTknX.js +1 -0
  85. package/dist/esm/brandsync-wc.js +20 -0
  86. package/dist/esm/bs-badge.entry.js +17 -0
  87. package/dist/esm/bs-button-skeleton.entry.js +17 -0
  88. package/dist/esm/bs-button.entry.js +86 -0
  89. package/dist/esm/bs-card.entry.js +18 -0
  90. package/dist/esm/bs-composer.entry.js +114 -0
  91. package/dist/esm/bs-data-table.entry.js +60 -0
  92. package/dist/esm/bs-input.entry.js +40 -0
  93. package/dist/esm/bs-modal.entry.js +67 -0
  94. package/dist/esm/index-BOj_41o9.js +1701 -0
  95. package/dist/esm/index.js +5 -0
  96. package/dist/esm/loader.js +10 -0
  97. package/dist/index.cjs.js +1 -0
  98. package/dist/index.js +1 -0
  99. package/dist/types/components/bs-badge/bs-badge.cmp.test.d.ts +1 -0
  100. package/dist/types/components/bs-badge/bs-badge.d.ts +20 -0
  101. package/dist/types/components/bs-badge/bs-badge.stories.d.ts +19 -0
  102. package/dist/types/components/bs-button/bs-button.cmp.test.d.ts +1 -0
  103. package/dist/types/components/bs-button/bs-button.d.ts +84 -0
  104. package/dist/types/components/bs-button/bs-button.stories.d.ts +29 -0
  105. package/dist/types/components/bs-button-skeleton/bs-button-skeleton.cmp.test.d.ts +1 -0
  106. package/dist/types/components/bs-button-skeleton/bs-button-skeleton.d.ts +28 -0
  107. package/dist/types/components/bs-button-skeleton/bs-button-skeleton.stories.d.ts +10 -0
  108. package/dist/types/components/bs-card/bs-card.cmp.test.d.ts +1 -0
  109. package/dist/types/components/bs-card/bs-card.d.ts +26 -0
  110. package/dist/types/components/bs-card/bs-card.stories.d.ts +10 -0
  111. package/dist/types/components/bs-composer/bs-composer.cmp.test.d.ts +1 -0
  112. package/dist/types/components/bs-composer/bs-composer.d.ts +83 -0
  113. package/dist/types/components/bs-composer/bs-composer.stories.d.ts +17 -0
  114. package/dist/types/components/bs-data-table/bs-data-table.d.ts +55 -0
  115. package/dist/types/components/bs-data-table/bs-data-table.stories.d.ts +7 -0
  116. package/dist/types/components/bs-input/bs-input.cmp.test.d.ts +1 -0
  117. package/dist/types/components/bs-input/bs-input.d.ts +33 -0
  118. package/dist/types/components/bs-input/bs-input.stories.d.ts +15 -0
  119. package/dist/types/components/bs-modal/bs-modal.cmp.test.d.ts +1 -0
  120. package/dist/types/components/bs-modal/bs-modal.d.ts +45 -0
  121. package/dist/types/components/bs-modal/bs-modal.stories.d.ts +9 -0
  122. package/dist/types/components.d.ts +962 -0
  123. package/dist/types/index.d.ts +11 -0
  124. package/dist/types/stencil-public-runtime.d.ts +1872 -0
  125. package/dist/types/stories-utils.d.ts +8 -0
  126. package/dist/types/utils/utils.d.ts +1 -0
  127. package/dist/types/utils/utils.unit.test.d.ts +1 -0
  128. package/loader/cdn.js +1 -0
  129. package/loader/index.cjs.js +1 -0
  130. package/loader/index.d.ts +24 -0
  131. package/loader/index.es2017.js +1 -0
  132. package/loader/index.js +2 -0
  133. package/package.json +67 -0
  134. package/readme.md +48 -0
@@ -0,0 +1,230 @@
1
+ import { h } from "@stencil/core";
2
+ /**
3
+ * A single-line text field with an optional label, description, and error state.
4
+ *
5
+ * ## When to use
6
+ * - Collecting a single line of free-text, email, password, or numeric input.
7
+ * - Pair with `error` for inline validation feedback tied to that specific field.
8
+ *
9
+ * ## When not to use
10
+ * - Multi-line text — this component has no `textarea` mode.
11
+ * - A fixed set of choices — use a select/radio/checkbox component instead of free text.
12
+ *
13
+ * @part label - The `<label>` element.
14
+ * @part control - The native `<input>` element.
15
+ * @part description - The helper text element (hidden when `error` is set).
16
+ * @part error - The error message element.
17
+ */
18
+ export class BsInput {
19
+ /** Current value. Native `input`/`change` events don't cross the Shadow DOM boundary, so this
20
+ * component re-dispatches them as `bsInput`/`bsChange` custom events instead. */
21
+ value = '';
22
+ type = 'text';
23
+ label;
24
+ placeholder;
25
+ description;
26
+ error;
27
+ disabled = false;
28
+ bsInput;
29
+ bsChange;
30
+ onInput = (ev) => {
31
+ const value = ev.target.value;
32
+ this.bsInput.emit(value);
33
+ };
34
+ onChange = (ev) => {
35
+ const value = ev.target.value;
36
+ this.bsChange.emit(value);
37
+ };
38
+ render() {
39
+ return (h("div", { key: '94a90f72a348259c9e54b1eb61ca694baf7c0386', class: `bs-input ${this.error ? 'bs-input--error' : ''}` }, this.label && (
40
+ // "control" is a fixed id, not a generated one -- safe because Shadow DOM scopes ids
41
+ // to this instance's own shadow root, so it can never collide with another bs-input.
42
+ h("label", { key: '3760b135aaf4002b86e0552af5e3cad110b45afe', part: "label", class: "bs-input__label", htmlFor: "control" }, this.label)), h("input", { key: '761d594818ee8ad917562b9dbe945e5ea8d9210c', id: "control", part: "control", class: "bs-input__control", type: this.type, value: this.value, placeholder: this.placeholder, disabled: this.disabled, "aria-describedby": this.error || this.description ? 'description-or-error' : undefined, "aria-invalid": this.error ? 'true' : undefined, onInput: this.onInput, onChange: this.onChange }), this.error ? (h("span", { id: "description-or-error", part: "error", class: "bs-input__error" }, this.error)) : this.description ? (h("span", { id: "description-or-error", part: "description", class: "bs-input__description" }, this.description)) : null));
43
+ }
44
+ static get is() { return "bs-input"; }
45
+ static get encapsulation() { return "shadow"; }
46
+ static get delegatesFocus() { return true; }
47
+ static get originalStyleUrls() {
48
+ return {
49
+ "$": ["bs-input.css"]
50
+ };
51
+ }
52
+ static get styleUrls() {
53
+ return {
54
+ "$": ["bs-input.css"]
55
+ };
56
+ }
57
+ static get properties() {
58
+ return {
59
+ "value": {
60
+ "type": "string",
61
+ "mutable": false,
62
+ "complexType": {
63
+ "original": "string",
64
+ "resolved": "string",
65
+ "references": {}
66
+ },
67
+ "required": false,
68
+ "optional": false,
69
+ "docs": {
70
+ "tags": [],
71
+ "text": "Current value. Native `input`/`change` events don't cross the Shadow DOM boundary, so this\ncomponent re-dispatches them as `bsInput`/`bsChange` custom events instead."
72
+ },
73
+ "getter": false,
74
+ "setter": false,
75
+ "reflect": false,
76
+ "attribute": "value",
77
+ "defaultValue": "''"
78
+ },
79
+ "type": {
80
+ "type": "string",
81
+ "mutable": false,
82
+ "complexType": {
83
+ "original": "'text' | 'email' | 'password' | 'number'",
84
+ "resolved": "\"email\" | \"number\" | \"password\" | \"text\"",
85
+ "references": {}
86
+ },
87
+ "required": false,
88
+ "optional": false,
89
+ "docs": {
90
+ "tags": [],
91
+ "text": ""
92
+ },
93
+ "getter": false,
94
+ "setter": false,
95
+ "reflect": false,
96
+ "attribute": "type",
97
+ "defaultValue": "'text'"
98
+ },
99
+ "label": {
100
+ "type": "string",
101
+ "mutable": false,
102
+ "complexType": {
103
+ "original": "string",
104
+ "resolved": "string",
105
+ "references": {}
106
+ },
107
+ "required": false,
108
+ "optional": true,
109
+ "docs": {
110
+ "tags": [],
111
+ "text": ""
112
+ },
113
+ "getter": false,
114
+ "setter": false,
115
+ "reflect": false,
116
+ "attribute": "label"
117
+ },
118
+ "placeholder": {
119
+ "type": "string",
120
+ "mutable": false,
121
+ "complexType": {
122
+ "original": "string",
123
+ "resolved": "string",
124
+ "references": {}
125
+ },
126
+ "required": false,
127
+ "optional": true,
128
+ "docs": {
129
+ "tags": [],
130
+ "text": ""
131
+ },
132
+ "getter": false,
133
+ "setter": false,
134
+ "reflect": false,
135
+ "attribute": "placeholder"
136
+ },
137
+ "description": {
138
+ "type": "string",
139
+ "mutable": false,
140
+ "complexType": {
141
+ "original": "string",
142
+ "resolved": "string",
143
+ "references": {}
144
+ },
145
+ "required": false,
146
+ "optional": true,
147
+ "docs": {
148
+ "tags": [],
149
+ "text": ""
150
+ },
151
+ "getter": false,
152
+ "setter": false,
153
+ "reflect": false,
154
+ "attribute": "description"
155
+ },
156
+ "error": {
157
+ "type": "string",
158
+ "mutable": false,
159
+ "complexType": {
160
+ "original": "string",
161
+ "resolved": "string",
162
+ "references": {}
163
+ },
164
+ "required": false,
165
+ "optional": true,
166
+ "docs": {
167
+ "tags": [],
168
+ "text": ""
169
+ },
170
+ "getter": false,
171
+ "setter": false,
172
+ "reflect": false,
173
+ "attribute": "error"
174
+ },
175
+ "disabled": {
176
+ "type": "boolean",
177
+ "mutable": false,
178
+ "complexType": {
179
+ "original": "boolean",
180
+ "resolved": "boolean",
181
+ "references": {}
182
+ },
183
+ "required": false,
184
+ "optional": false,
185
+ "docs": {
186
+ "tags": [],
187
+ "text": ""
188
+ },
189
+ "getter": false,
190
+ "setter": false,
191
+ "reflect": false,
192
+ "attribute": "disabled",
193
+ "defaultValue": "false"
194
+ }
195
+ };
196
+ }
197
+ static get events() {
198
+ return [{
199
+ "method": "bsInput",
200
+ "name": "bsInput",
201
+ "bubbles": true,
202
+ "cancelable": true,
203
+ "composed": true,
204
+ "docs": {
205
+ "tags": [],
206
+ "text": ""
207
+ },
208
+ "complexType": {
209
+ "original": "string",
210
+ "resolved": "string",
211
+ "references": {}
212
+ }
213
+ }, {
214
+ "method": "bsChange",
215
+ "name": "bsChange",
216
+ "bubbles": true,
217
+ "cancelable": true,
218
+ "composed": true,
219
+ "docs": {
220
+ "tags": [],
221
+ "text": ""
222
+ },
223
+ "complexType": {
224
+ "original": "string",
225
+ "resolved": "string",
226
+ "references": {}
227
+ }
228
+ }];
229
+ }
230
+ }
@@ -0,0 +1,40 @@
1
+ import { html } from "lit";
2
+ import { componentDescription, propDescription } from "../../stories-utils";
3
+ const meta = {
4
+ title: 'Components/bs-input',
5
+ parameters: { docs: { description: { component: componentDescription('bs-input') } } },
6
+ render: args => html `
7
+ <bs-input
8
+ label=${args.label}
9
+ placeholder=${args.placeholder}
10
+ description=${args.description}
11
+ error=${args.error}
12
+ ?disabled=${args.disabled}
13
+ ></bs-input>
14
+ `,
15
+ argTypes: {
16
+ label: { control: 'text', description: propDescription('bs-input', 'label') },
17
+ placeholder: { control: 'text', description: propDescription('bs-input', 'placeholder') },
18
+ description: { control: 'text', description: propDescription('bs-input', 'description') },
19
+ error: { control: 'text', description: propDescription('bs-input', 'error') },
20
+ disabled: { control: 'boolean', description: propDescription('bs-input', 'disabled') },
21
+ },
22
+ args: {
23
+ label: 'Email address',
24
+ placeholder: 'you@example.com',
25
+ description: '',
26
+ error: '',
27
+ disabled: false,
28
+ },
29
+ };
30
+ export default meta;
31
+ export const Default = {};
32
+ export const WithDescription = {
33
+ args: { description: "We'll never share your email." },
34
+ };
35
+ export const WithError = {
36
+ args: { error: 'Enter a valid email address.' },
37
+ };
38
+ export const Disabled = {
39
+ args: { disabled: true },
40
+ };
@@ -0,0 +1,112 @@
1
+ import { render, h, describe, it, expect, afterEach } from "@stencil/vitest";
2
+ describe('bs-modal', () => {
3
+ afterEach(() => {
4
+ document.querySelectorAll('button[data-test-trigger]').forEach(el => el.remove());
5
+ });
6
+ it('renders nothing when closed (default)', async () => {
7
+ const { root } = await render(h("bs-modal", { heading: "Confirm booking" }, "Body"));
8
+ expect(root.shadowRoot.querySelector('[part="backdrop"]')).toBeNull();
9
+ expect(root.shadowRoot.querySelector('[part="dialog"]')).toBeNull();
10
+ });
11
+ it('renders the backdrop and dialog with correct a11y attributes when open', async () => {
12
+ const { root } = await render(h("bs-modal", { open: true, heading: "Confirm booking" }, "Body content"));
13
+ const backdrop = root.shadowRoot.querySelector('[part="backdrop"]');
14
+ const dialog = root.shadowRoot.querySelector('[part="dialog"]');
15
+ expect(backdrop).toBeTruthy();
16
+ expect(dialog).toBeTruthy();
17
+ expect(dialog).toEqualAttribute('role', 'dialog');
18
+ expect(dialog).toEqualAttribute('aria-modal', 'true');
19
+ expect(dialog).toEqualAttribute('aria-label', 'Confirm booking');
20
+ expect(root).toHaveTextContent('Body content');
21
+ });
22
+ ['sm', 'md', 'lg'].forEach(size => {
23
+ it(`applies the "${size}" size class to the dialog`, async () => {
24
+ const { root } = await render(h("bs-modal", { open: true, size: size }, "Body"));
25
+ const dialog = root.shadowRoot.querySelector('[part="dialog"]');
26
+ expect(dialog).toHaveClass(`bs-modal__dialog--${size}`);
27
+ });
28
+ });
29
+ it('defaults to the "md" size', async () => {
30
+ const { root } = await render(h("bs-modal", { open: true }, "Body"));
31
+ const dialog = root.shadowRoot.querySelector('[part="dialog"]');
32
+ expect(dialog).toHaveClass('bs-modal__dialog--md');
33
+ });
34
+ it('only shows the footer bar when the footer slot has content', async () => {
35
+ const { root: withoutFooter } = await render(h("bs-modal", { open: true }, "Body"));
36
+ const footerWithout = withoutFooter.shadowRoot.querySelector('[part="footer"]');
37
+ expect(footerWithout).not.toHaveClass('bs-modal__footer--visible');
38
+ const { root: withFooter } = await render(h("bs-modal", { open: true }, "Body", h("button", { slot: "footer" }, "Confirm")));
39
+ const footerWith = withFooter.shadowRoot.querySelector('[part="footer"]');
40
+ expect(footerWith).toHaveClass('bs-modal__footer--visible');
41
+ expect(withFooter).toHaveTextContent('Confirm');
42
+ });
43
+ it('closes and emits bsClose when the close button is clicked', async () => {
44
+ const { root, spyOnEvent, waitForChanges } = await render(h("bs-modal", { open: true, heading: "Confirm booking" }, "Body"));
45
+ const closeSpy = spyOnEvent('bsClose');
46
+ const closeButton = root.shadowRoot.querySelector('[part="close"]');
47
+ closeButton.click();
48
+ await waitForChanges();
49
+ expect(closeSpy).toHaveReceivedEventTimes(1);
50
+ expect(root).not.toHaveAttribute('open');
51
+ expect(root.shadowRoot.querySelector('[part="dialog"]')).toBeNull();
52
+ });
53
+ it('closes when the backdrop itself is clicked, but not when the dialog inside it is clicked', async () => {
54
+ const { root, spyOnEvent, waitForChanges } = await render(h("bs-modal", { open: true }, "Body"));
55
+ const closeSpy = spyOnEvent('bsClose');
56
+ const dialog = root.shadowRoot.querySelector('[part="dialog"]');
57
+ dialog.click();
58
+ await waitForChanges();
59
+ expect(closeSpy.length).toBe(0);
60
+ expect(root).toHaveAttribute('open');
61
+ const backdrop = root.shadowRoot.querySelector('[part="backdrop"]');
62
+ backdrop.click();
63
+ await waitForChanges();
64
+ expect(closeSpy).toHaveReceivedEventTimes(1);
65
+ expect(root).not.toHaveAttribute('open');
66
+ });
67
+ it('closes on Escape keydown while open', async () => {
68
+ const { root, spyOnEvent, waitForChanges } = await render(h("bs-modal", { open: true }, "Body"));
69
+ const closeSpy = spyOnEvent('bsClose');
70
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
71
+ await waitForChanges();
72
+ expect(closeSpy).toHaveReceivedEventTimes(1);
73
+ expect(root).not.toHaveAttribute('open');
74
+ });
75
+ it('does not react to Escape when closed', async () => {
76
+ const { spyOnEvent, waitForChanges } = await render(h("bs-modal", null, "Body"));
77
+ const closeSpy = spyOnEvent('bsClose');
78
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
79
+ await waitForChanges();
80
+ expect(closeSpy.length).toBe(0);
81
+ });
82
+ it('moves focus into the dialog when opened', async () => {
83
+ const { root } = await render(h("bs-modal", { open: true }, "Body"));
84
+ const dialog = root.shadowRoot.querySelector('[part="dialog"]');
85
+ expect(root.shadowRoot.activeElement).toBe(dialog);
86
+ });
87
+ it('redirects focus back into the dialog if focus escapes it while open (focus trap)', async () => {
88
+ const outside = document.createElement('button');
89
+ outside.setAttribute('data-test-trigger', '');
90
+ outside.textContent = 'outside';
91
+ document.body.appendChild(outside);
92
+ const { root } = await render(h("bs-modal", { open: true }, "Body"));
93
+ const dialog = root.shadowRoot.querySelector('[part="dialog"]');
94
+ outside.focus();
95
+ expect(root.shadowRoot.activeElement).toBe(dialog);
96
+ });
97
+ it('captures the previously focused element on open and restores focus to it on close', async () => {
98
+ const trigger = document.createElement('button');
99
+ trigger.setAttribute('data-test-trigger', '');
100
+ trigger.textContent = 'open modal';
101
+ document.body.appendChild(trigger);
102
+ trigger.focus();
103
+ expect(document.activeElement).toBe(trigger);
104
+ const { root, setProps, waitForChanges } = await render(h("bs-modal", null, "Body"));
105
+ await setProps({ open: true });
106
+ const dialog = root.shadowRoot.querySelector('[part="dialog"]');
107
+ expect(root.shadowRoot.activeElement).toBe(dialog);
108
+ await setProps({ open: false });
109
+ await waitForChanges();
110
+ expect(document.activeElement).toBe(trigger);
111
+ });
112
+ });
@@ -0,0 +1,66 @@
1
+ .bs-modal__backdrop {
2
+ position: fixed;
3
+ inset: 0;
4
+ z-index: var(--bs-z-index-modal);
5
+ display: flex;
6
+ align-items: center;
7
+ justify-content: center;
8
+ background: rgba(0, 0, 0, 0.4);
9
+ }
10
+
11
+ .bs-modal__dialog {
12
+ display: flex;
13
+ flex-direction: column;
14
+ max-height: calc(100vh - var(--bs-spacing-800));
15
+ background: var(--bs-surface-raised);
16
+ border-radius: var(--bs-border-radius-200);
17
+ box-shadow: var(--bs-shadow-xl);
18
+ font-family: inherit;
19
+ }
20
+
21
+ .bs-modal__dialog--sm {
22
+ width: 360px;
23
+ }
24
+ .bs-modal__dialog--md {
25
+ width: 560px;
26
+ }
27
+ .bs-modal__dialog--lg {
28
+ width: 800px;
29
+ }
30
+
31
+ .bs-modal__header {
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: space-between;
35
+ padding: var(--bs-spacing-400);
36
+ border-bottom: 1px solid var(--bs-border-default);
37
+ }
38
+
39
+ .bs-modal__heading {
40
+ font-size: var(--bs-font-size-lg);
41
+ font-weight: var(--bs-font-weight-semibold);
42
+ color: var(--bs-text-default);
43
+ }
44
+
45
+ .bs-modal__close {
46
+ border: none;
47
+ background: none;
48
+ font-size: var(--bs-font-size-xl);
49
+ line-height: 1;
50
+ color: var(--bs-icon-muted, var(--bs-text-secondary));
51
+ cursor: pointer;
52
+ padding: var(--bs-spacing-100);
53
+ }
54
+
55
+ .bs-modal__body {
56
+ padding: var(--bs-spacing-400);
57
+ overflow-y: auto;
58
+ }
59
+
60
+ .bs-modal__footer--visible {
61
+ display: flex;
62
+ justify-content: flex-end;
63
+ gap: var(--bs-spacing-200);
64
+ padding: var(--bs-spacing-400);
65
+ border-top: 1px solid var(--bs-border-default);
66
+ }
@@ -0,0 +1,198 @@
1
+ import { h } from "@stencil/core";
2
+ /**
3
+ * An overlay dialog that interrupts the current flow for a focused task or confirmation.
4
+ * Closes itself on backdrop click, Escape, or its own close button, and emits `bsClose`.
5
+ *
6
+ * ## When to use
7
+ * - Confirming a consequential action (e.g. "Confirm booking") before it takes effect.
8
+ * - A short, focused task that doesn't warrant navigating to a new page.
9
+ *
10
+ * ## When not to use
11
+ * - For non-blocking status messages — use a toast/notification instead of interrupting the user.
12
+ * - For a long, multi-step flow — a full page or a dedicated route is usually a better fit than a
13
+ * modal that just gets taller and taller.
14
+ *
15
+ * @slot - Default slot: the dialog body content.
16
+ * @slot footer - Optional footer content (typically action buttons).
17
+ * @part backdrop - The full-viewport overlay behind the dialog.
18
+ * @part dialog - The dialog box itself.
19
+ * @part header - The header row (heading + close button).
20
+ * @part close - The close button.
21
+ * @part body - The body content wrapper.
22
+ * @part footer - The footer wrapper (only visible when the footer slot has content).
23
+ */
24
+ export class BsModal {
25
+ /** Mutable + reflected so the component can close itself (backdrop click, Escape, close button)
26
+ * the same way a native `<dialog>` does, while still emitting `bsClose` for the consumer to react to. */
27
+ open = false;
28
+ heading;
29
+ size = 'md';
30
+ bsClose;
31
+ /** Tracks whether the `footer` slot has assigned content, so the footer bar (border + padding)
32
+ * only renders when there's actually something to put in it. */
33
+ hasFooter = false;
34
+ onFooterSlotchange = (ev) => {
35
+ this.hasFooter = ev.target.assignedNodes().length > 0;
36
+ };
37
+ dialogRef;
38
+ previouslyFocused;
39
+ wasOpen = false;
40
+ close = () => {
41
+ this.open = false;
42
+ this.bsClose.emit();
43
+ };
44
+ onKeydown(ev) {
45
+ if (this.open && ev.key === 'Escape') {
46
+ this.close();
47
+ }
48
+ }
49
+ onBackdropClick = (ev) => {
50
+ if (ev.target === ev.currentTarget) {
51
+ this.close();
52
+ }
53
+ };
54
+ // Simple focus containment: rather than manually enumerating focusable elements (which can't
55
+ // reliably walk into slotted children's own Shadow DOM anyway), redirect any focus that lands
56
+ // outside the dialog back to it. composedPath() correctly reports elements across Shadow DOM
57
+ // boundaries, unlike a plain `.contains()` check.
58
+ onFocusIn(ev) {
59
+ if (this.open && this.dialogRef && !ev.composedPath().includes(this.dialogRef)) {
60
+ this.dialogRef.focus();
61
+ }
62
+ }
63
+ componentDidRender() {
64
+ if (this.open && !this.wasOpen) {
65
+ this.previouslyFocused = document.activeElement;
66
+ this.dialogRef?.focus();
67
+ }
68
+ else if (!this.open && this.wasOpen) {
69
+ this.previouslyFocused?.focus();
70
+ }
71
+ this.wasOpen = this.open;
72
+ }
73
+ render() {
74
+ if (!this.open) {
75
+ return null;
76
+ }
77
+ return (h("div", { part: "backdrop", class: "bs-modal__backdrop", onClick: this.onBackdropClick }, h("div", { ref: el => (this.dialogRef = el), tabIndex: -1, part: "dialog", class: `bs-modal__dialog bs-modal__dialog--${this.size}`, role: "dialog", "aria-modal": "true", "aria-label": this.heading }, h("div", { part: "header", class: "bs-modal__header" }, h("span", { class: "bs-modal__heading" }, this.heading), h("button", { part: "close", class: "bs-modal__close", "aria-label": "Close", onClick: this.close }, "\u00D7")), h("div", { part: "body", class: "bs-modal__body" }, h("slot", null)), h("div", { part: "footer", class: `bs-modal__footer ${this.hasFooter ? 'bs-modal__footer--visible' : ''}` }, h("slot", { name: "footer", onSlotchange: this.onFooterSlotchange })))));
78
+ }
79
+ static get is() { return "bs-modal"; }
80
+ static get encapsulation() { return "shadow"; }
81
+ static get originalStyleUrls() {
82
+ return {
83
+ "$": ["bs-modal.css"]
84
+ };
85
+ }
86
+ static get styleUrls() {
87
+ return {
88
+ "$": ["bs-modal.css"]
89
+ };
90
+ }
91
+ static get properties() {
92
+ return {
93
+ "open": {
94
+ "type": "boolean",
95
+ "mutable": true,
96
+ "complexType": {
97
+ "original": "boolean",
98
+ "resolved": "boolean",
99
+ "references": {}
100
+ },
101
+ "required": false,
102
+ "optional": false,
103
+ "docs": {
104
+ "tags": [],
105
+ "text": "Mutable + reflected so the component can close itself (backdrop click, Escape, close button)\nthe same way a native `<dialog>` does, while still emitting `bsClose` for the consumer to react to."
106
+ },
107
+ "getter": false,
108
+ "setter": false,
109
+ "reflect": true,
110
+ "attribute": "open",
111
+ "defaultValue": "false"
112
+ },
113
+ "heading": {
114
+ "type": "string",
115
+ "mutable": false,
116
+ "complexType": {
117
+ "original": "string",
118
+ "resolved": "string",
119
+ "references": {}
120
+ },
121
+ "required": false,
122
+ "optional": true,
123
+ "docs": {
124
+ "tags": [],
125
+ "text": ""
126
+ },
127
+ "getter": false,
128
+ "setter": false,
129
+ "reflect": false,
130
+ "attribute": "heading"
131
+ },
132
+ "size": {
133
+ "type": "string",
134
+ "mutable": false,
135
+ "complexType": {
136
+ "original": "BsModalSize",
137
+ "resolved": "\"lg\" | \"md\" | \"sm\"",
138
+ "references": {
139
+ "BsModalSize": {
140
+ "location": "local",
141
+ "path": "/Users/vivka/brandsync-web-components/src/components/bs-modal/bs-modal.tsx",
142
+ "id": "src/components/bs-modal/bs-modal.tsx::BsModalSize"
143
+ }
144
+ }
145
+ },
146
+ "required": false,
147
+ "optional": false,
148
+ "docs": {
149
+ "tags": [],
150
+ "text": ""
151
+ },
152
+ "getter": false,
153
+ "setter": false,
154
+ "reflect": false,
155
+ "attribute": "size",
156
+ "defaultValue": "'md'"
157
+ }
158
+ };
159
+ }
160
+ static get states() {
161
+ return {
162
+ "hasFooter": {}
163
+ };
164
+ }
165
+ static get events() {
166
+ return [{
167
+ "method": "bsClose",
168
+ "name": "bsClose",
169
+ "bubbles": true,
170
+ "cancelable": true,
171
+ "composed": true,
172
+ "docs": {
173
+ "tags": [],
174
+ "text": ""
175
+ },
176
+ "complexType": {
177
+ "original": "void",
178
+ "resolved": "void",
179
+ "references": {}
180
+ }
181
+ }];
182
+ }
183
+ static get listeners() {
184
+ return [{
185
+ "name": "keydown",
186
+ "method": "onKeydown",
187
+ "target": "document",
188
+ "capture": false,
189
+ "passive": false
190
+ }, {
191
+ "name": "focusin",
192
+ "method": "onFocusIn",
193
+ "target": "document",
194
+ "capture": false,
195
+ "passive": false
196
+ }];
197
+ }
198
+ }