@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,242 @@
1
+ import { h } from "@stencil/core";
2
+ /**
3
+ * A clickable action element for the single most important action in a given context.
4
+ *
5
+ * ## When to use
6
+ * - The primary call to action on a screen or within a card/modal (e.g. "Book room", "Confirm").
7
+ * - Secondary, lower-emphasis actions alongside it (use `variant="neutral"`).
8
+ * - `variant="error"` for destructive/irreversible actions (e.g. "Delete account") — per
9
+ * BrandSync guidance, destructive actions should always use the error variant, never
10
+ * `neutral`, so the visual weight matches the risk of the action.
11
+ *
12
+ * ## When not to use
13
+ * - For navigation between pages — use a link/nav component instead, a button implies an
14
+ * in-page action, not a destination change.
15
+ * - For more than one primary-emphasis action in the same view — pick one, demote the rest to
16
+ * `neutral`.
17
+ * - `error` purely for visual emphasis — it signals a destructive action to the user, so reserve
18
+ * it for actions that actually delete/revoke/undo something.
19
+ *
20
+ * @slot - Default slot: the button's label content.
21
+ * @slot icon - Optional leading icon, rendered before the label.
22
+ * @slot end-icon - Optional trailing icon, rendered after the label.
23
+ * @part icon - The icon wrapper element.
24
+ * @part label - The text label element.
25
+ * @part end-icon - The trailing icon wrapper element.
26
+ * @prop --bs-button-radius - Corner radius. Aliased to `--bs-border-radius-100` by default.
27
+ * @prop --bs-button-height-sm - Height at `size="sm"`. Aliased to `--bs-spacing-500`.
28
+ * @prop --bs-button-height-md - Height at `size="md"`. Aliased to `--bs-spacing-600`.
29
+ * @prop --bs-button-height-lg - Height at `size="lg"`. Aliased to `--bs-spacing-700`.
30
+ */
31
+ export class BsButton {
32
+ el;
33
+ /**
34
+ * Visual style. Maps directly to the brandsync-tokens `--bs-button-*` semantic set.
35
+ * `error` is for destructive/irreversible actions per BrandSync guidance, not a stronger
36
+ * emphasis alternative to `primary`.
37
+ */
38
+ variant = 'primary';
39
+ /** Sizing scale. */
40
+ size = 'md';
41
+ /** Disables the button and applies the disabled token set. */
42
+ disabled = false;
43
+ /** Native `<button>` type. */
44
+ type = 'button';
45
+ /**
46
+ * Accessible name for the button. Required for icon-only usage (no visible label text via the
47
+ * default slot) so screen readers still announce what the button does. Stencil reflects this
48
+ * camelCase prop to the `aria-label` HTML attribute automatically.
49
+ */
50
+ ariaLabel = null;
51
+ /** Tracks whether the `icon` slot has assigned content, so the icon/label gap only
52
+ * applies when there's actually an icon. */
53
+ hasIcon = false;
54
+ /** Tracks whether the `end-icon` slot has assigned content, so the icon/label gap only
55
+ * applies when there's actually a trailing icon. */
56
+ hasEndIcon = false;
57
+ /**
58
+ * Tracks whether the default (label) slot has assigned content, so an icon-only button can be
59
+ * styled/sized differently (see `bs-button--icon-only`).
60
+ *
61
+ * Known edge case (not fixed, same simplification `hasIcon`/similar flags elsewhere in this
62
+ * codebase already make): a default slot containing only whitespace (e.g. a lone space text
63
+ * node) still counts as `hasLabel = true`, since checking for assigned nodes doesn't
64
+ * distinguish whitespace-only text nodes from real content.
65
+ */
66
+ hasLabel = false;
67
+ /**
68
+ * Computes the initial `hasIcon`/`hasEndIcon`/`hasLabel` state directly from the host's light
69
+ * DOM children, before first render.
70
+ *
71
+ * This can't be left to the `slotchange` handlers below alone: `slotchange` only fires when a
72
+ * slot's *assigned nodes* change, and a slot that starts out with zero assigned nodes and stays
73
+ * that way never fires it (nothing changed). Relying on `slotchange` alone left an icon-only
74
+ * button (default slot empty from the start) permanently stuck on its initial `hasLabel`
75
+ * default, so `bs-button--icon-only` never applied.
76
+ */
77
+ componentWillLoad() {
78
+ this.hasIcon = this.hasSlottedContent('icon');
79
+ this.hasEndIcon = this.hasSlottedContent('end-icon');
80
+ this.hasLabel = this.hasSlottedContent(null);
81
+ }
82
+ hasSlottedContent(slotName) {
83
+ return Array.from(this.el.childNodes).some(node => {
84
+ if (node.nodeType === Node.ELEMENT_NODE) {
85
+ return node.getAttribute('slot') === slotName;
86
+ }
87
+ // Only elements can carry a `slot` attribute -- any other node type (text, comments) can
88
+ // only ever be assigned to the default slot.
89
+ return slotName === null;
90
+ });
91
+ }
92
+ onIconSlotchange = (ev) => {
93
+ this.hasIcon = ev.target.assignedNodes().length > 0;
94
+ };
95
+ onEndIconSlotchange = (ev) => {
96
+ this.hasEndIcon = ev.target.assignedNodes().length > 0;
97
+ };
98
+ onLabelSlotchange = (ev) => {
99
+ this.hasLabel = ev.target.assignedNodes().length > 0;
100
+ };
101
+ render() {
102
+ const iconOnly = (this.hasIcon || this.hasEndIcon) && !this.hasLabel;
103
+ return (h("button", { key: 'eae5915f036d18b43b9b1ff94272fe27aa119d0e', class: `bs-button bs-button--${this.variant} bs-button--${this.size} ${iconOnly ? 'bs-button--icon-only' : ''}`, type: this.type, disabled: this.disabled, "aria-label": this.ariaLabel }, h("span", { key: 'ee58ccc6f8390663e67a7d7a03fe6113782805e7', part: "icon", class: `bs-button__icon ${this.hasIcon ? 'bs-button__icon--visible' : ''}` }, h("slot", { key: '526ec7b4fd274bbdb6dc1edb38c33917c26337ee', name: "icon", onSlotchange: this.onIconSlotchange })), h("span", { key: '3e0121dc4dc975b449b03e5ac5f55d1f6001e806', part: "label", class: "bs-button__label" }, h("slot", { key: '4ab00b8b91f0513af74faac4ae7b39f9a975dde5', onSlotchange: this.onLabelSlotchange })), h("span", { key: '3f64f1122c5ed1da1bb9cb869490395f1e11b3fb', part: "end-icon", class: `bs-button__end-icon ${this.hasEndIcon ? 'bs-button__end-icon--visible' : ''}` }, h("slot", { key: 'ca79d7ed3cd80c6abc3f17c4f9d215ed69c13059', name: "end-icon", onSlotchange: this.onEndIconSlotchange }))));
104
+ }
105
+ static get is() { return "bs-button"; }
106
+ static get encapsulation() { return "shadow"; }
107
+ static get delegatesFocus() { return true; }
108
+ static get originalStyleUrls() {
109
+ return {
110
+ "$": ["bs-button.css"]
111
+ };
112
+ }
113
+ static get styleUrls() {
114
+ return {
115
+ "$": ["bs-button.css"]
116
+ };
117
+ }
118
+ static get properties() {
119
+ return {
120
+ "variant": {
121
+ "type": "string",
122
+ "mutable": false,
123
+ "complexType": {
124
+ "original": "BsButtonVariant",
125
+ "resolved": "\"error\" | \"info\" | \"neutral\" | \"outlined\" | \"primary\" | \"subtle\" | \"success\" | \"warning\"",
126
+ "references": {
127
+ "BsButtonVariant": {
128
+ "location": "local",
129
+ "path": "/Users/vivka/brandsync-web-components/src/components/bs-button/bs-button.tsx",
130
+ "id": "src/components/bs-button/bs-button.tsx::BsButtonVariant"
131
+ }
132
+ }
133
+ },
134
+ "required": false,
135
+ "optional": false,
136
+ "docs": {
137
+ "tags": [],
138
+ "text": "Visual style. Maps directly to the brandsync-tokens `--bs-button-*` semantic set.\n`error` is for destructive/irreversible actions per BrandSync guidance, not a stronger\nemphasis alternative to `primary`."
139
+ },
140
+ "getter": false,
141
+ "setter": false,
142
+ "reflect": false,
143
+ "attribute": "variant",
144
+ "defaultValue": "'primary'"
145
+ },
146
+ "size": {
147
+ "type": "string",
148
+ "mutable": false,
149
+ "complexType": {
150
+ "original": "BsButtonSize",
151
+ "resolved": "\"lg\" | \"md\" | \"sm\"",
152
+ "references": {
153
+ "BsButtonSize": {
154
+ "location": "local",
155
+ "path": "/Users/vivka/brandsync-web-components/src/components/bs-button/bs-button.tsx",
156
+ "id": "src/components/bs-button/bs-button.tsx::BsButtonSize"
157
+ }
158
+ }
159
+ },
160
+ "required": false,
161
+ "optional": false,
162
+ "docs": {
163
+ "tags": [],
164
+ "text": "Sizing scale."
165
+ },
166
+ "getter": false,
167
+ "setter": false,
168
+ "reflect": false,
169
+ "attribute": "size",
170
+ "defaultValue": "'md'"
171
+ },
172
+ "disabled": {
173
+ "type": "boolean",
174
+ "mutable": false,
175
+ "complexType": {
176
+ "original": "boolean",
177
+ "resolved": "boolean",
178
+ "references": {}
179
+ },
180
+ "required": false,
181
+ "optional": false,
182
+ "docs": {
183
+ "tags": [],
184
+ "text": "Disables the button and applies the disabled token set."
185
+ },
186
+ "getter": false,
187
+ "setter": false,
188
+ "reflect": false,
189
+ "attribute": "disabled",
190
+ "defaultValue": "false"
191
+ },
192
+ "type": {
193
+ "type": "string",
194
+ "mutable": false,
195
+ "complexType": {
196
+ "original": "'button' | 'submit' | 'reset'",
197
+ "resolved": "\"button\" | \"reset\" | \"submit\"",
198
+ "references": {}
199
+ },
200
+ "required": false,
201
+ "optional": false,
202
+ "docs": {
203
+ "tags": [],
204
+ "text": "Native `<button>` type."
205
+ },
206
+ "getter": false,
207
+ "setter": false,
208
+ "reflect": false,
209
+ "attribute": "type",
210
+ "defaultValue": "'button'"
211
+ },
212
+ "ariaLabel": {
213
+ "type": "string",
214
+ "mutable": false,
215
+ "complexType": {
216
+ "original": "string | null",
217
+ "resolved": "string",
218
+ "references": {}
219
+ },
220
+ "required": false,
221
+ "optional": false,
222
+ "docs": {
223
+ "tags": [],
224
+ "text": "Accessible name for the button. Required for icon-only usage (no visible label text via the\ndefault slot) so screen readers still announce what the button does. Stencil reflects this\ncamelCase prop to the `aria-label` HTML attribute automatically."
225
+ },
226
+ "getter": false,
227
+ "setter": false,
228
+ "reflect": false,
229
+ "attribute": "aria-label",
230
+ "defaultValue": "null"
231
+ }
232
+ };
233
+ }
234
+ static get states() {
235
+ return {
236
+ "hasIcon": {},
237
+ "hasEndIcon": {},
238
+ "hasLabel": {}
239
+ };
240
+ }
241
+ static get elementRef() { return "el"; }
242
+ }
@@ -0,0 +1,145 @@
1
+ import { html } from "lit";
2
+ import { componentDescription, propDescription } from "../../stories-utils";
3
+ const variants = ['primary', 'neutral', 'subtle', 'outlined', 'success', 'warning', 'info', 'error'];
4
+ const sizes = ['sm', 'md', 'lg'];
5
+ const meta = {
6
+ title: 'Components/bs-button',
7
+ parameters: { docs: { description: { component: componentDescription('bs-button') } } },
8
+ render: args => html `
9
+ <bs-button variant=${args.variant} size=${args.size} type=${args.type} ?disabled=${args.disabled}>
10
+ ${args.label}
11
+ </bs-button>
12
+ `,
13
+ argTypes: {
14
+ variant: { control: 'select', options: variants, description: propDescription('bs-button', 'variant') },
15
+ size: { control: 'select', options: sizes, description: propDescription('bs-button', 'size') },
16
+ disabled: { control: 'boolean', description: propDescription('bs-button', 'disabled') },
17
+ type: { control: 'select', options: ['button', 'submit', 'reset'], description: propDescription('bs-button', 'type') },
18
+ label: { control: 'text', description: 'Slotted label content (not a real attribute -- see the default slot).' },
19
+ },
20
+ args: {
21
+ variant: 'primary',
22
+ size: 'md',
23
+ disabled: false,
24
+ type: 'button',
25
+ label: 'Click me',
26
+ },
27
+ };
28
+ export default meta;
29
+ export const Primary = {};
30
+ export const Neutral = {
31
+ args: { variant: 'neutral' },
32
+ };
33
+ export const Subtle = {
34
+ args: { variant: 'subtle' },
35
+ };
36
+ export const Outlined = {
37
+ args: { variant: 'outlined' },
38
+ };
39
+ export const Success = {
40
+ args: { variant: 'success' },
41
+ };
42
+ export const Warning = {
43
+ args: { variant: 'warning' },
44
+ };
45
+ export const Info = {
46
+ args: { variant: 'info' },
47
+ };
48
+ export const Error = {
49
+ args: { variant: 'error' },
50
+ };
51
+ export const Disabled = {
52
+ args: { disabled: true },
53
+ };
54
+ export const NeutralDisabled = {
55
+ name: 'Neutral, disabled',
56
+ args: { variant: 'neutral', disabled: true },
57
+ };
58
+ export const Sizes = {
59
+ render: () => html `
60
+ <div style="display:flex; gap:12px; align-items:center;">
61
+ <bs-button size="sm">Small</bs-button>
62
+ <bs-button size="md">Medium</bs-button>
63
+ <bs-button size="lg">Large</bs-button>
64
+ </div>
65
+ `,
66
+ };
67
+ export const WithIcon = {
68
+ name: 'With icon',
69
+ render: args => html `
70
+ <bs-button variant=${args.variant} size=${args.size} ?disabled=${args.disabled}>
71
+ <svg slot="icon" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
72
+ <path d="M13 4L6 11L3 8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
73
+ </svg>
74
+ ${args.label}
75
+ </bs-button>
76
+ `,
77
+ };
78
+ export const WithEndIcon = {
79
+ name: 'With end icon',
80
+ render: args => html `
81
+ <bs-button variant=${args.variant} size=${args.size} ?disabled=${args.disabled}>
82
+ ${args.label}
83
+ <svg slot="end-icon" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
84
+ <path d="M6 3L11 8L6 13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
85
+ </svg>
86
+ </bs-button>
87
+ `,
88
+ };
89
+ export const IconOnly = {
90
+ name: 'Icon-only',
91
+ render: args => html `
92
+ <bs-button variant=${args.variant} size=${args.size} ?disabled=${args.disabled} aria-label="Delete">
93
+ <svg slot="icon" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
94
+ <path
95
+ d="M2 4H14M6 4V2.5C6 2.22386 6.22386 2 6.5 2H9.5C9.77614 2 10 2.22386 10 2.5V4M12.5 4L12 13.5C12 13.7761 11.7761 14 11.5 14H4.5C4.22386 14 4 13.7761 4 13.5L3.5 4"
96
+ stroke="currentColor"
97
+ stroke-width="1.3"
98
+ stroke-linecap="round"
99
+ stroke-linejoin="round"
100
+ />
101
+ </svg>
102
+ </bs-button>
103
+ `,
104
+ };
105
+ export const LongLabel = {
106
+ name: 'Long label (edge case)',
107
+ args: { label: 'This button has a much longer label than the icon-and-text layout was designed for' },
108
+ render: args => html `
109
+ <div style="max-width: 220px;">
110
+ <bs-button variant=${args.variant} size=${args.size} ?disabled=${args.disabled}> ${args.label} </bs-button>
111
+ </div>
112
+ `,
113
+ };
114
+ export const AllCombinations = {
115
+ name: 'All variant × size × disabled combinations',
116
+ render: () => html `
117
+ <table style="border-collapse: collapse;">
118
+ <thead>
119
+ <tr>
120
+ <th style="text-align:left; padding: 4px 12px;">Variant</th>
121
+ ${sizes.map(size => html `<th style="text-align:left; padding: 4px 12px;">${size}</th>`)}
122
+ ${sizes.map(size => html `<th style="text-align:left; padding: 4px 12px;">${size} (disabled)</th>`)}
123
+ </tr>
124
+ </thead>
125
+ <tbody>
126
+ ${variants.map(variant => html `
127
+ <tr>
128
+ <td style="padding: 8px 12px; font-family: monospace;">${variant}</td>
129
+ ${sizes.map(size => html `<td style="padding: 8px 12px;"><bs-button variant=${variant} size=${size}>Button</bs-button></td>`)}
130
+ ${sizes.map(size => html `<td style="padding: 8px 12px;"><bs-button variant=${variant} size=${size} disabled>Button</bs-button></td>`)}
131
+ </tr>
132
+ `)}
133
+ </tbody>
134
+ </table>
135
+ `,
136
+ };
137
+ export const Skeleton = {
138
+ render: () => html `
139
+ <div style="display:flex; gap:12px; align-items:center;">
140
+ <bs-button-skeleton size="sm"></bs-button-skeleton>
141
+ <bs-button-skeleton size="md"></bs-button-skeleton>
142
+ <bs-button-skeleton size="lg"></bs-button-skeleton>
143
+ </div>
144
+ `,
145
+ };
@@ -0,0 +1,18 @@
1
+ import { render, h, describe, it, expect } from "@stencil/vitest";
2
+ describe('bs-button-skeleton', () => {
3
+ it('renders with role="status" and aria-label="Loading" by default', async () => {
4
+ const { root } = await render(h("bs-button-skeleton", null));
5
+ const container = root.shadowRoot.querySelector('.bs-button-skeleton');
6
+ expect(container).toEqualAttribute('role', 'status');
7
+ expect(container).toEqualAttribute('aria-label', 'Loading');
8
+ expect(container).toHaveClass('bs-button-skeleton--md');
9
+ });
10
+ it('applies the correct size class for each size value', async () => {
11
+ const { root, setProps } = await render(h("bs-button-skeleton", { size: "sm" }));
12
+ expect(root.shadowRoot.querySelector('.bs-button-skeleton')).toHaveClass('bs-button-skeleton--sm');
13
+ await setProps({ size: 'md' });
14
+ expect(root.shadowRoot.querySelector('.bs-button-skeleton')).toHaveClass('bs-button-skeleton--md');
15
+ await setProps({ size: 'lg' });
16
+ expect(root.shadowRoot.querySelector('.bs-button-skeleton')).toHaveClass('bs-button-skeleton--lg');
17
+ });
18
+ });
@@ -0,0 +1,46 @@
1
+ :host {
2
+ display: inline-block;
3
+ /* Mirrors bs-button.css's height tokens exactly so the skeleton matches the real button's
4
+ * dimensions -- this doesn't depend on bs-button's internal :host vars since these are two
5
+ * separate shadow roots. */
6
+ --bs-button-skeleton-height-sm: var(--bs-spacing-500);
7
+ --bs-button-skeleton-height-md: var(--bs-spacing-600);
8
+ --bs-button-skeleton-height-lg: var(--bs-spacing-700);
9
+ --bs-button-skeleton-width: 96px;
10
+ }
11
+
12
+ .bs-button-skeleton {
13
+ display: inline-block;
14
+ border-radius: var(--bs-border-radius-100);
15
+ width: var(--bs-button-skeleton-width);
16
+ background: var(--bs-color-neutral-container);
17
+ animation: bs-button-skeleton-pulse 1.5s ease-in-out infinite;
18
+ }
19
+
20
+ .bs-button-skeleton--sm {
21
+ height: var(--bs-button-skeleton-height-sm);
22
+ }
23
+
24
+ .bs-button-skeleton--md {
25
+ height: var(--bs-button-skeleton-height-md);
26
+ }
27
+
28
+ .bs-button-skeleton--lg {
29
+ height: var(--bs-button-skeleton-height-lg);
30
+ }
31
+
32
+ @keyframes bs-button-skeleton-pulse {
33
+ 0%,
34
+ 100% {
35
+ opacity: 1;
36
+ }
37
+ 50% {
38
+ opacity: 0.5;
39
+ }
40
+ }
41
+
42
+ @media (prefers-reduced-motion: reduce) {
43
+ .bs-button-skeleton {
44
+ animation: none;
45
+ }
46
+ }
@@ -0,0 +1,73 @@
1
+ import { h } from "@stencil/core";
2
+ /**
3
+ * A shape-matched loading placeholder for `bs-button`, shown while the real label/action isn't
4
+ * known yet (e.g. still being fetched from an API). Sized to match `bs-button`'s `sm`/`md`/`lg`
5
+ * heights so layout doesn't shift once the real button renders.
6
+ *
7
+ * This is not a submit-in-progress spinner — that's a different concern (a button that's
8
+ * already rendered but waiting on an async action it triggered). Use this component only when
9
+ * the button itself doesn't exist yet.
10
+ *
11
+ * ## When to use
12
+ * - In place of a `bs-button` whose label/visibility depends on data that hasn't loaded yet.
13
+ *
14
+ * ## When not to use
15
+ * - While a button's own click handler is running (e.g. a submit request in flight) — render the
16
+ * real `bs-button` and show its own busy/spinner state instead.
17
+ *
18
+ * @part container - The pulsing placeholder element.
19
+ * @prop --bs-button-skeleton-height-sm - Height at `size="sm"`. Aliased to `--bs-spacing-500`.
20
+ * @prop --bs-button-skeleton-height-md - Height at `size="md"`. Aliased to `--bs-spacing-600`.
21
+ * @prop --bs-button-skeleton-height-lg - Height at `size="lg"`. Aliased to `--bs-spacing-700`.
22
+ * @prop --bs-button-skeleton-width - Placeholder width. Defaults to `96px`.
23
+ */
24
+ export class BsButtonSkeleton {
25
+ /** Sizing scale, matching the `bs-button` size it stands in for. */
26
+ size = 'md';
27
+ render() {
28
+ return h("span", { key: 'c54e42fcf6eb065a1a33f2dee8518d12c5797cf3', part: "container", class: `bs-button-skeleton bs-button-skeleton--${this.size}`, role: "status", "aria-label": "Loading" });
29
+ }
30
+ static get is() { return "bs-button-skeleton"; }
31
+ static get encapsulation() { return "shadow"; }
32
+ static get originalStyleUrls() {
33
+ return {
34
+ "$": ["bs-button-skeleton.css"]
35
+ };
36
+ }
37
+ static get styleUrls() {
38
+ return {
39
+ "$": ["bs-button-skeleton.css"]
40
+ };
41
+ }
42
+ static get properties() {
43
+ return {
44
+ "size": {
45
+ "type": "string",
46
+ "mutable": false,
47
+ "complexType": {
48
+ "original": "BsButtonSize",
49
+ "resolved": "\"lg\" | \"md\" | \"sm\"",
50
+ "references": {
51
+ "BsButtonSize": {
52
+ "location": "import",
53
+ "path": "../bs-button/bs-button",
54
+ "id": "src/components/bs-button/bs-button.tsx::BsButtonSize",
55
+ "referenceLocation": "BsButtonSize"
56
+ }
57
+ }
58
+ },
59
+ "required": false,
60
+ "optional": false,
61
+ "docs": {
62
+ "tags": [],
63
+ "text": "Sizing scale, matching the `bs-button` size it stands in for."
64
+ },
65
+ "getter": false,
66
+ "setter": false,
67
+ "reflect": false,
68
+ "attribute": "size",
69
+ "defaultValue": "'md'"
70
+ }
71
+ };
72
+ }
73
+ }
@@ -0,0 +1,25 @@
1
+ import { html } from "lit";
2
+ import { componentDescription, propDescription } from "../../stories-utils";
3
+ const sizes = ['sm', 'md', 'lg'];
4
+ const meta = {
5
+ title: 'Components/bs-button-skeleton',
6
+ parameters: { docs: { description: { component: componentDescription('bs-button-skeleton') } } },
7
+ render: args => html `<bs-button-skeleton size=${args.size}></bs-button-skeleton>`,
8
+ argTypes: {
9
+ size: { control: 'select', options: sizes, description: propDescription('bs-button-skeleton', 'size') },
10
+ },
11
+ args: {
12
+ size: 'md',
13
+ },
14
+ };
15
+ export default meta;
16
+ export const Default = {};
17
+ export const Sizes = {
18
+ render: () => html `
19
+ <div style="display:flex; gap:12px; align-items:center;">
20
+ <bs-button-skeleton size="sm"></bs-button-skeleton>
21
+ <bs-button-skeleton size="md"></bs-button-skeleton>
22
+ <bs-button-skeleton size="lg"></bs-button-skeleton>
23
+ </div>
24
+ `,
25
+ };
@@ -0,0 +1,42 @@
1
+ import { render, h, describe, it, expect } from "@stencil/vitest";
2
+ describe('bs-card', () => {
3
+ it('defaults to the "raised" surface', async () => {
4
+ const { root } = await render(h("bs-card", null, "Body content"));
5
+ const container = root.shadowRoot.querySelector('.bs-card');
6
+ expect(container).toHaveClass('bs-card--raised');
7
+ });
8
+ const surfaces = ['base', 'raised', 'container'];
9
+ surfaces.forEach(surface => {
10
+ it(`applies the "${surface}" surface class`, async () => {
11
+ const { root } = await render(h("bs-card", { surface: surface }, "Body content"));
12
+ const container = root.shadowRoot.querySelector('.bs-card');
13
+ expect(container).toHaveClass(`bs-card--${surface}`);
14
+ });
15
+ });
16
+ it('renders the header, default, and footer slot content in their respective parts', async () => {
17
+ const { root } = await render(h("bs-card", null, h("span", { slot: "header" }, "Booking summary"), "Main content", h("span", { slot: "footer" }, "Actions")));
18
+ expect(root).toHaveTextContent('Booking summary');
19
+ expect(root).toHaveTextContent('Main content');
20
+ expect(root).toHaveTextContent('Actions');
21
+ const headerSlot = root.shadowRoot.querySelector('[part="header"] slot[name="header"]');
22
+ const bodySlot = root.shadowRoot.querySelector('[part="body"] slot:not([name])');
23
+ const footerSlot = root.shadowRoot.querySelector('[part="footer"] slot[name="footer"]');
24
+ expect(headerSlot).toBeTruthy();
25
+ expect(bodySlot).toBeTruthy();
26
+ expect(footerSlot).toBeTruthy();
27
+ });
28
+ it('exposes container, header, body, and footer parts', async () => {
29
+ const { root } = await render(h("bs-card", null, "Body content"));
30
+ expect(root.shadowRoot.querySelector('[part="container"]')).toBeTruthy();
31
+ expect(root.shadowRoot.querySelector('[part="header"]')).toBeTruthy();
32
+ expect(root.shadowRoot.querySelector('[part="body"]')).toBeTruthy();
33
+ expect(root.shadowRoot.querySelector('[part="footer"]')).toBeTruthy();
34
+ });
35
+ it('renders empty header/footer wrappers when no slotted content is provided', async () => {
36
+ const { root } = await render(h("bs-card", null, "Body content only"));
37
+ const header = root.shadowRoot.querySelector('.bs-card__header');
38
+ const footer = root.shadowRoot.querySelector('.bs-card__footer');
39
+ expect(header.textContent.trim()).toBe('');
40
+ expect(footer.textContent.trim()).toBe('');
41
+ });
42
+ });
@@ -0,0 +1,31 @@
1
+ :host {
2
+ display: block;
3
+ }
4
+
5
+ .bs-card {
6
+ border: 1px solid var(--bs-paper-border-surface);
7
+ border-radius: var(--bs-border-radius-150);
8
+ padding: var(--bs-spacing-400);
9
+ font-family: inherit;
10
+ }
11
+
12
+ .bs-card--base {
13
+ background: var(--bs-paper-bg-base);
14
+ }
15
+
16
+ .bs-card--raised {
17
+ background: var(--bs-paper-bg-raised);
18
+ box-shadow: var(--bs-shadow-sm);
19
+ }
20
+
21
+ .bs-card--container {
22
+ background: var(--bs-paper-bg-container);
23
+ }
24
+
25
+ .bs-card__header ::slotted(*) {
26
+ margin: 0 0 var(--bs-spacing-300) 0;
27
+ }
28
+
29
+ .bs-card__footer ::slotted(*) {
30
+ margin: var(--bs-spacing-300) 0 0 0;
31
+ }