@c2n/button-group 0.0.6

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Nguyen Thai Vinh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # @c2n/button-group
2
+
3
+ Button group built with Lit: attaches `c2-button` / `c2-icon-button` items into one control, optionally as a segmented control with single or multiple selection.
4
+
5
+ ```bash
6
+ npm install @c2n/button-group
7
+ ```
8
+
9
+ ```html
10
+ <script type="module">
11
+ import '@c2n/button-group'
12
+ </script>
13
+
14
+ <c2-button-group>
15
+ <c2-button>Save</c2-button>
16
+ <c2-button>Publish</c2-button>
17
+ </c2-button-group>
18
+
19
+ <c2-button-group selection="single" value="week">
20
+ <c2-button value="day">Day</c2-button>
21
+ <c2-button value="week">Week</c2-button>
22
+ <c2-button value="month">Month</c2-button>
23
+ </c2-button-group>
24
+ ```
25
+
26
+ - **Layout**: items share borders and only the outside corners are rounded (`--c2-button-group--border-radius`); a divider (`--c2-button-group__divider--width` / `--color`) separates them. `--c2-button-group--border` puts one border on every item for an outlined group. A positive `--c2-button-group--gap` separates the items into individually rounded buttons. `orientation="vertical"` stacks them.
27
+ - **Selection**: `selection="single"` keeps exactly one item pressed, `selection="multiple"` toggles items independently. Items identify themselves with `value` (their index otherwise); the group's `value` is the pressed value or a comma-separated list. `change` fires on user clicks with `detail.value`; setting `value` from code is silent.
28
+ - **Accessibility**: `role="group"` (name it with `aria-label`); pressed items carry `selected`, which `c2-button` and `c2-icon-button` announce as `aria-pressed`, and the group marks items `toggle` so unpressed ones announce `false`.
29
+ - **Disabled**: `disabled` on the group disables every item and restores them when cleared, leaving items you disabled yourself alone.
30
+
31
+ Colours and sizes come from the buttons' own variables set on the group (`--c2-button__container--*`, including the `__selected--*` pressed state added for groups; `--c2-icon-button--*` for icon items). `--c2-button-group__item--flex: 1 1 0` makes every item the same width. The full list is in `custom-elements.json` and on the docs site.
@@ -0,0 +1,115 @@
1
+ import { LitElement, html, isServer, unsafeCSS } from "lit";
2
+ import { customElement, property } from "lit/decorators.js";
3
+ import "@c2n/button";
4
+ //#region src/button-group.scss?inline
5
+ var button_group_default = "/* ex : var((width: 24px), width, c2-checkbox) returns var(--c2-checkbox-width, 24px) */\n:host {\n display: inline-flex;\n align-items: stretch;\n gap: var(--c2-button-group--gap,0px);\n vertical-align: middle;\n max-width: 100%;\n}\n\n:host([hidden]) {\n display: none;\n}\n\n:host([orientation=vertical]) {\n flex-direction: column;\n align-items: stretch;\n}\n\n::slotted(*) {\n position: relative;\n flex: var(--c2-button-group__item--flex,0 0 auto);\n min-width: 0;\n --c2-button__container--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n --c2-icon-button--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n}\n\n::slotted(*) {\n --c2-button__container--border: var(--c2-button-group--border,1px solid transparent);\n --c2-icon-button--border: var(--c2-button-group--border,1px solid transparent);\n}\n\n::slotted(:only-child) {\n --c2-button__container--border-radius: var(--c2-button-group--border-radius,6px);\n --c2-icon-button--border-radius: var(--c2-button-group--border-radius,6px);\n}\n\n::slotted(:first-child:not(:only-child)) {\n --c2-button__container--border-radius: var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px);\n --c2-icon-button--border-radius: var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px);\n}\n\n::slotted(:last-child:not(:only-child)) {\n --c2-button__container--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n --c2-icon-button--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n}\n\n:host(:dir(rtl)) ::slotted(:first-child:not(:only-child)) {\n --c2-button__container--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n --c2-icon-button--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n}\n\n:host(:dir(rtl)) ::slotted(:last-child:not(:only-child)) {\n --c2-button__container--border-radius: var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px);\n --c2-icon-button--border-radius: var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px);\n}\n\n::slotted(:not(:first-child)) {\n margin-inline-start: calc(-1 * max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000));\n box-shadow: calc(-1 * max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000)) 0 0 0 var(--c2-button-group__divider--color,rgba(255, 255, 255, 0.35));\n}\n\n:host(:dir(rtl)) ::slotted(:not(:first-child)) {\n box-shadow: max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000) 0 0 0 var(--c2-button-group__divider--color,rgba(255, 255, 255, 0.35));\n}\n\n:host([orientation=vertical]) ::slotted(:first-child:not(:only-child)) {\n --c2-button__container--border-radius: var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n --c2-icon-button--border-radius: var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000);\n}\n\n:host([orientation=vertical]) ::slotted(:last-child:not(:only-child)) {\n --c2-button__container--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px);\n --c2-icon-button--border-radius: min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) min(var(--c2-button-group--border-radius,6px), var(--c2-button-group--gap,0px) * 10000) var(--c2-button-group--border-radius,6px) var(--c2-button-group--border-radius,6px);\n}\n\n:host([orientation=vertical]) ::slotted(:not(:first-child)) {\n margin-inline-start: 0;\n margin-block-start: calc(-1 * max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000));\n box-shadow: 0 calc(-1 * max(0px, var(--c2-button-group__divider--width,1px) - var(--c2-button-group--gap,0px) * 10000)) 0 0 var(--c2-button-group__divider--color,rgba(255, 255, 255, 0.35));\n}\n\n::slotted(:hover),\n::slotted(:focus-within),\n::slotted([selected]) {\n z-index: 1;\n}";
6
+ //#endregion
7
+ //#region \0@oxc-project+runtime@0.148.0/helpers/esm/decorate.js
8
+ function __decorate(decorators, target, key, desc) {
9
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
10
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
11
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
12
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
13
+ }
14
+ //#endregion
15
+ //#region src/button-group.ts
16
+ var GROUP_DISABLED = "data-c2-button-group-disabled";
17
+ var ButtonGroup = class ButtonGroup extends LitElement {
18
+ static {
19
+ this.styles = unsafeCSS(button_group_default);
20
+ }
21
+ constructor() {
22
+ super();
23
+ this.selection = "none";
24
+ this.value = "";
25
+ this.disabled = false;
26
+ this.orientation = "horizontal";
27
+ this.handleClick = (event) => {
28
+ if (this.selection === "none" || this.disabled) return;
29
+ const items = this.items;
30
+ const item = event.composedPath().find((node) => node instanceof HTMLElement && node.parentElement === this);
31
+ if (!item || item.hasAttribute("disabled")) return;
32
+ const value = this.itemValue(item, items.indexOf(item));
33
+ let next;
34
+ if (this.selection === "single") {
35
+ if (this.value === value) return;
36
+ next = value;
37
+ } else {
38
+ const values = this.selectedValues;
39
+ next = values.includes(value) ? values.filter((entry) => entry !== value).join(",") : [...values, value].join(",");
40
+ }
41
+ this.value = next;
42
+ this.dispatchEvent(new CustomEvent("change", {
43
+ detail: { value: next },
44
+ bubbles: true,
45
+ composed: true
46
+ }));
47
+ };
48
+ if (!isServer) this.addEventListener("click", this.handleClick);
49
+ }
50
+ connectedCallback() {
51
+ super.connectedCallback();
52
+ if (!this.hasAttribute("role")) this.setAttribute("role", "group");
53
+ if (!isServer) {
54
+ this.observer ??= new MutationObserver(() => this.syncItems());
55
+ this.observer.observe(this, { childList: true });
56
+ this.syncItems();
57
+ }
58
+ }
59
+ disconnectedCallback() {
60
+ super.disconnectedCallback();
61
+ this.observer?.disconnect();
62
+ }
63
+ /** The item elements, in order. */
64
+ get items() {
65
+ return [...this.children].filter((child) => child instanceof HTMLElement);
66
+ }
67
+ /** Values of the pressed items. */
68
+ get selectedValues() {
69
+ return this.value ? this.value.split(",").filter(Boolean) : [];
70
+ }
71
+ itemValue(item, index) {
72
+ return item.getAttribute("value") ?? String(index);
73
+ }
74
+ updated(changed) {
75
+ if (changed.has("value") || changed.has("selection") || changed.has("disabled")) this.syncItems();
76
+ }
77
+ /** Mirrors `value`, `selection` and `disabled` onto the items. */
78
+ syncItems() {
79
+ const items = this.items;
80
+ const selectable = this.selection !== "none";
81
+ const selected = new Set(this.selectedValues);
82
+ items.forEach((item, index) => {
83
+ if (selectable) item.setAttribute("toggle", "");
84
+ else item.removeAttribute("toggle");
85
+ if (selectable && selected.has(this.itemValue(item, index))) item.setAttribute("selected", "");
86
+ else item.removeAttribute("selected");
87
+ if (this.disabled) {
88
+ if (!item.hasAttribute("disabled")) {
89
+ item.setAttribute("disabled", "");
90
+ item.setAttribute(GROUP_DISABLED, "");
91
+ }
92
+ } else if (item.hasAttribute(GROUP_DISABLED)) {
93
+ item.removeAttribute("disabled");
94
+ item.removeAttribute(GROUP_DISABLED);
95
+ }
96
+ });
97
+ }
98
+ render() {
99
+ return html`<slot></slot>`;
100
+ }
101
+ };
102
+ __decorate([property({ reflect: true })], ButtonGroup.prototype, "selection", void 0);
103
+ __decorate([property({ reflect: true })], ButtonGroup.prototype, "value", void 0);
104
+ __decorate([property({
105
+ type: Boolean,
106
+ reflect: true
107
+ })], ButtonGroup.prototype, "disabled", void 0);
108
+ __decorate([property({ reflect: true })], ButtonGroup.prototype, "orientation", void 0);
109
+ __decorate([property({
110
+ type: String,
111
+ attribute: "aria-label"
112
+ })], ButtonGroup.prototype, "ariaLabel", void 0);
113
+ ButtonGroup = __decorate([customElement("c2-button-group")], ButtonGroup);
114
+ //#endregion
115
+ export { ButtonGroup };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@c2n/button-group",
3
+ "version": "0.0.6",
4
+ "type": "module",
5
+ "main": "dist/button-group.js",
6
+ "exports": {
7
+ ".": {
8
+ "default": "./dist/button-group.js",
9
+ "types": "./types/src/button-group.d.ts"
10
+ },
11
+ "./custom-elements.json": "./custom-elements.json"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "types"
16
+ ],
17
+ "keywords": [
18
+ "button group",
19
+ "segmented control",
20
+ "toggle group",
21
+ "web component",
22
+ "lit"
23
+ ],
24
+ "license": "MIT",
25
+ "author": "code2nguyen@gmail.com",
26
+ "publishConfig": {
27
+ "registry": "https://registry.npmjs.org",
28
+ "access": "public"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/code2nguyen/web-components.git"
33
+ },
34
+ "scripts": {
35
+ "dev": "vite",
36
+ "build": "wireit",
37
+ "build:only": "vite build",
38
+ "type-check": "wireit"
39
+ },
40
+ "wireit": {
41
+ "type-check": {
42
+ "dependencies": [
43
+ "../../core:build",
44
+ "../button:build"
45
+ ],
46
+ "command": "tsc -p tsconfig.lib.json --composite false"
47
+ },
48
+ "build": {
49
+ "dependencies": [
50
+ "type-check"
51
+ ],
52
+ "command": "vite build"
53
+ }
54
+ },
55
+ "dependencies": {
56
+ "@c2n/button": "0.0.6",
57
+ "@c2n/core": "0.0.6",
58
+ "lit": "3.3.3"
59
+ },
60
+ "devDependencies": {
61
+ "@c2n/config": "*"
62
+ },
63
+ "customElements": "custom-elements.json",
64
+ "gitHead": "2921054bc75299da66dad11c1e374246ff88a51e"
65
+ }
@@ -0,0 +1,58 @@
1
+ import { LitElement, type PropertyValues } from 'lit';
2
+ import '@c2n/button';
3
+ export type ButtonGroupSelection = 'none' | 'single' | 'multiple';
4
+ export type ButtonGroupOrientation = 'horizontal' | 'vertical';
5
+ /**
6
+ * Attaches related buttons into one control: shared borders, outside corners only, a divider between items. Children
7
+ * are `c2-button` or `c2-icon-button` elements (any element works for layout). With `selection="single"` the group is
8
+ * a segmented control that keeps one item pressed; with `selection="multiple"` each item toggles independently. The
9
+ * pressed items carry `selected` (and `aria-pressed`), the group reports them in `value` and fires `change`.
10
+ *
11
+ * @tag c2-button-group
12
+ *
13
+ * @slot - The buttons, in order.
14
+ *
15
+ * @event {CustomEvent<{ value: string }>} change - Fired after the user changes the selection. `detail.value` is the new `value`. Not fired for programmatic changes.
16
+ *
17
+ * @cssproperty {pixel} [--c2-button-group--gap=0px] - Space between items. Zero attaches them; a positive gap separates them into individually rounded buttons.
18
+ * @cssproperty {border-radius} [--c2-button-group--border-radius=6px] - Outside corner radius. With a positive gap, applies to every item.
19
+ * @cssproperty {border} [--c2-button-group--border=1px solid transparent] - Border applied to every item, e.g. `1px solid #d4d4d8` for an outlined group.
20
+ * @cssproperty {pixel} [--c2-button-group__divider--width=1px] - Width of the line between attached items; also how much their borders overlap.
21
+ * @cssproperty {color} [--c2-button-group__divider--color=rgba(255, 255, 255, 0.35)] - Colour of the line between attached items. Match the border colour for outlined groups, `transparent` for none.
22
+ * @cssproperty {flex} [--c2-button-group__item--flex=0 0 auto] - `1 1 0` makes every item grow to the same width.
23
+ * @slotcomponent c2-button
24
+ * @slotcomponent c2-icon-button
25
+ */
26
+ export declare class ButtonGroup extends LitElement {
27
+ static styles: import("lit").CSSResult;
28
+ /** `none` (default) for plain attached buttons, `single` for one pressed item, `multiple` for independent toggles. */
29
+ selection: ButtonGroupSelection;
30
+ /** `value` of the pressed item; comma-separated values with `selection="multiple"`. Items without a `value` attribute use their index. */
31
+ value: string;
32
+ /** Disables every item. */
33
+ disabled: boolean;
34
+ /** Stacks the items vertically. */
35
+ orientation: ButtonGroupOrientation;
36
+ /** Accessible name of the group. */
37
+ ariaLabel: string;
38
+ private observer?;
39
+ constructor();
40
+ connectedCallback(): void;
41
+ disconnectedCallback(): void;
42
+ /** The item elements, in order. */
43
+ get items(): HTMLElement[];
44
+ /** Values of the pressed items. */
45
+ get selectedValues(): string[];
46
+ private itemValue;
47
+ updated(changed: PropertyValues<this>): void;
48
+ /** Mirrors `value`, `selection` and `disabled` onto the items. */
49
+ private syncItems;
50
+ private handleClick;
51
+ render(): import("lit-html").TemplateResult<1>;
52
+ }
53
+ declare global {
54
+ interface HTMLElementTagNameMap {
55
+ 'c2-button-group': ButtonGroup;
56
+ }
57
+ }
58
+ //# sourceMappingURL=button-group.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"button-group.d.ts","sourceRoot":"","sources":["../../src/button-group.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAA6B,KAAK,cAAc,EAAE,MAAM,KAAK,CAAA;AAGhF,OAAO,aAAa,CAAA;AAGpB,MAAM,MAAM,oBAAoB,GAAG,MAAM,GAAG,QAAQ,GAAG,UAAU,CAAA;AACjE,MAAM,MAAM,sBAAsB,GAAG,YAAY,GAAG,UAAU,CAAA;AAI9D;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBACa,WAAY,SAAQ,UAAU;IACzC,OAAgB,MAAM,0BAAoB;IAE1C,sHAAsH;IACzF,SAAS,EAAE,oBAAoB,CAAS;IAErE,0IAA0I;IAC7G,KAAK,SAAK;IAEvC,2BAA2B;IACiB,QAAQ,UAAQ;IAE5D,mCAAmC;IACN,WAAW,EAAE,sBAAsB,CAAe;IAE/E,oCAAoC;IAE3B,SAAS,EAAG,MAAM,CAAA;IAE3B,OAAO,CAAC,QAAQ,CAAC,CAAkB;;IAO1B,iBAAiB;IAUjB,oBAAoB;IAK7B,mCAAmC;IACnC,IAAI,KAAK,IAAI,WAAW,EAAE,CAEzB;IAED,mCAAmC;IACnC,IAAI,cAAc,IAAI,MAAM,EAAE,CAE7B;IAED,OAAO,CAAC,SAAS;IAIR,OAAO,CAAC,OAAO,EAAE,cAAc,CAAC,IAAI,CAAC;IAI9C,kEAAkE;IAClE,OAAO,CAAC,SAAS;IAsBjB,OAAO,CAAC,WAAW,CAgBlB;IAEQ,MAAM;CAGhB;AAED,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,qBAAqB;QAC7B,iBAAiB,EAAE,WAAW,CAAA;KAC/B;CACF"}