@reliability-design/web 1.0.2 → 1.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/dist/components/rel-accordion/README.md +132 -0
- package/dist/components/rel-accordion/rel-accordion.ts +61 -29
- package/dist/components/rel-alert/README.md +141 -0
- package/dist/components/rel-alert/rel-alert.ts +110 -67
- package/dist/components/rel-avatar/README.md +119 -0
- package/dist/components/rel-avatar/rel-avatar.ts +8 -15
- package/dist/components/rel-badge/README.md +86 -0
- package/dist/components/rel-badge/rel-badge.ts +28 -21
- package/dist/components/rel-breadcrumb/README.md +102 -0
- package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +24 -18
- package/dist/components/rel-button/README.md +130 -0
- package/dist/components/rel-button/rel-button.ts +39 -59
- package/dist/components/rel-calendar/README.md +109 -0
- package/dist/components/rel-calendar/index.ts +1 -0
- package/dist/components/rel-calendar/rel-calendar.ts +467 -0
- package/dist/components/rel-card/README.md +94 -0
- package/dist/components/rel-card/rel-card.ts +21 -11
- package/dist/components/rel-cardmedia/README.md +127 -0
- package/dist/components/rel-cardmedia/rel-cardmedia.ts +22 -18
- package/dist/components/rel-carousel/README.md +166 -0
- package/dist/components/rel-carousel/index.ts +1 -0
- package/dist/components/rel-carousel/rel-carousel.ts +469 -0
- package/dist/components/rel-chart/README.md +286 -0
- package/dist/components/rel-chart/index.ts +1 -0
- package/dist/components/rel-chart/rel-chart.ts +736 -0
- package/dist/components/rel-checkbox/README.md +77 -0
- package/dist/components/rel-checkbox/rel-checkbox.ts +36 -45
- package/dist/components/rel-chip/README.md +114 -0
- package/dist/components/rel-chip/rel-chip.ts +47 -122
- package/dist/components/rel-container/README.md +99 -0
- package/dist/components/rel-container/rel-container.ts +1 -6
- package/dist/components/rel-date-picker/README.md +253 -0
- package/dist/components/rel-date-picker/index.ts +1 -0
- package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
- package/dist/components/rel-divider/README.md +100 -0
- package/dist/components/rel-divider/rel-divider.ts +37 -37
- package/dist/components/rel-drawer/README.md +200 -0
- package/dist/components/rel-drawer/index.ts +1 -0
- package/dist/components/rel-drawer/rel-drawer.ts +621 -0
- package/dist/components/rel-kbd/README.md +151 -0
- package/dist/components/rel-kbd/rel-kbd.ts +118 -0
- package/dist/components/rel-layout/README.md +88 -0
- package/dist/components/rel-layout/rel-layout.ts +12 -14
- package/dist/components/rel-menu/README.md +151 -0
- package/dist/components/rel-menu/rel-menu.ts +18 -10
- package/dist/components/rel-menubar/README.md +329 -0
- package/dist/components/rel-menubar/index.ts +2 -0
- package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
- package/dist/components/rel-menubar/rel-menubar.ts +576 -0
- package/dist/components/rel-nav-menu/README.md +289 -0
- package/dist/components/rel-nav-menu/index.ts +3 -0
- package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +143 -0
- package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
- package/dist/components/rel-nav-menu/rel-nav-menu.ts +331 -0
- package/dist/components/rel-navbar/README.md +126 -0
- package/dist/components/rel-navbar/rel-navbar.ts +91 -0
- package/dist/components/rel-otp/README.md +440 -0
- package/dist/components/rel-otp/index.ts +1 -0
- package/dist/components/rel-otp/rel-otp.ts +504 -0
- package/dist/components/rel-pagination/README.md +410 -0
- package/dist/components/rel-pagination/index.ts +1 -0
- package/dist/components/rel-pagination/rel-pagination.ts +376 -0
- package/dist/components/rel-popover/README.md +568 -0
- package/dist/components/rel-popover/index.ts +2 -0
- package/dist/components/rel-popover/rel-popover-content.ts +55 -0
- package/dist/components/rel-popover/rel-popover.ts +327 -0
- package/dist/components/rel-progress/README.md +110 -0
- package/dist/components/rel-progress/rel-progress.ts +58 -20
- package/dist/components/rel-quote-page/README.md +166 -0
- package/dist/components/rel-quote-page/rel-quote-page.ts +816 -0
- package/dist/components/rel-radio/README.md +120 -0
- package/dist/components/rel-radio/rel-radio.ts +41 -73
- package/dist/components/rel-resizable/README.md +140 -0
- package/dist/components/rel-resizable/index.ts +3 -0
- package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
- package/dist/components/rel-resizable/rel-resizable-handle.ts +149 -0
- package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
- package/dist/components/rel-segmented-button/README.md +141 -0
- package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
- package/dist/components/rel-select/README.md +107 -0
- package/dist/components/rel-select/rel-select.ts +14 -14
- package/dist/components/rel-sheet/README.md +224 -0
- package/dist/components/rel-sheet/rel-sheet.ts +683 -0
- package/dist/components/rel-sidebar/README.md +209 -0
- package/dist/components/rel-sidebar/rel-sidebar.ts +1158 -0
- package/dist/components/rel-slider/README.md +135 -0
- package/dist/components/rel-slider/rel-slider.ts +19 -7
- package/dist/components/rel-snackbar/README.md +134 -0
- package/dist/components/rel-snackbar/rel-snackbar.ts +73 -2
- package/dist/components/rel-stepper/README.md +160 -0
- package/dist/components/rel-stepper/rel-stepper.ts +29 -8
- package/dist/components/rel-switch/README.md +102 -0
- package/dist/components/rel-switch/rel-switch.ts +22 -11
- package/dist/components/rel-tab/README.md +163 -0
- package/dist/components/rel-tab/rel-tab.ts +46 -22
- package/dist/components/rel-table/README.md +178 -0
- package/dist/components/rel-table/rel-table-body.ts +16 -0
- package/dist/components/rel-table/rel-table-caption.ts +22 -0
- package/dist/components/rel-table/rel-table-cell.ts +49 -0
- package/dist/components/rel-table/rel-table-footer.ts +18 -0
- package/dist/components/rel-table/rel-table-head.ts +33 -0
- package/dist/components/rel-table/rel-table-header.ts +16 -0
- package/dist/components/rel-table/rel-table-row.ts +32 -0
- package/dist/components/rel-table/rel-table.ts +47 -0
- package/dist/components/rel-textfield/README.md +90 -0
- package/dist/components/rel-textfield/rel-textfield.ts +37 -21
- package/dist/components/rel-toast/README.md +148 -0
- package/dist/components/rel-toast/rel-toast.ts +32 -14
- package/dist/components/rel-tooltip/README.md +103 -0
- package/dist/components/rel-tooltip/rel-tooltip.ts +9 -9
- package/dist/index.d.mts +846 -7
- package/dist/index.d.ts +846 -7
- package/dist/index.js +10860 -1803
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10820 -1809
- package/dist/index.mjs.map +1 -1
- package/dist/styles/vars.css +293 -183
- package/dist/vars-ITL2K3KO.css +294 -0
- package/package.json +2 -2
- package/dist/components/rel-accordion/rel-accordion.stories.ts +0 -313
- package/dist/components/rel-alert/rel-alert.stories.ts +0 -412
- package/dist/components/rel-avatar/rel-avatar.stories.ts +0 -327
- package/dist/components/rel-badge/rel-badge.stories.ts +0 -350
- package/dist/components/rel-breadcrumb/rel-breadcrumb.stories.ts +0 -306
- package/dist/components/rel-button/rel-button.stories.ts +0 -356
- package/dist/components/rel-card/rel-card.stories.ts +0 -299
- package/dist/components/rel-cardmedia/rel-cardmedia.stories.ts +0 -368
- package/dist/components/rel-checkbox/rel-checkbox.stories.ts +0 -414
- package/dist/components/rel-chip/rel-chip.stories.ts +0 -552
- package/dist/components/rel-container/rel-container.stories.ts +0 -239
- package/dist/components/rel-divider/rel-divider.stories.ts +0 -229
- package/dist/components/rel-layout/rel-layout.stories.ts +0 -713
- package/dist/components/rel-menu/rel-menu.stories.ts +0 -910
- package/dist/components/rel-progress/rel-progress.stories.ts +0 -817
- package/dist/components/rel-radio/rel-radio.stories.ts +0 -1048
- package/dist/components/rel-segmented-button/rel-segmented-button.stories.ts +0 -1079
- package/dist/components/rel-select/rel-select.stories.ts +0 -1201
- package/dist/components/rel-slider/rel-slider.stories.ts +0 -730
- package/dist/components/rel-snackbar/rel-snackbar.stories.ts +0 -808
- package/dist/components/rel-stepper/rel-stepper.stories.ts +0 -752
- package/dist/components/rel-switch/rel-switch.stories.ts +0 -441
- package/dist/components/rel-tab/rel-tab.stories.ts +0 -492
- package/dist/components/rel-textfield/rel-textfield.stories.ts +0 -710
- package/dist/components/rel-toast/rel-toast.stories.ts +0 -394
- package/dist/components/rel-tooltip/rel-tooltip.stories.ts +0 -364
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
import type { RelResizablePanel } from "./rel-resizable-panel";
|
|
4
|
+
import type { RelResizableHandle } from "./rel-resizable-handle";
|
|
5
|
+
|
|
6
|
+
@customElement("rel-resizable-group")
|
|
7
|
+
export class RelResizableGroup extends LitElement {
|
|
8
|
+
/* =======================
|
|
9
|
+
* PUBLIC API
|
|
10
|
+
* ======================= */
|
|
11
|
+
|
|
12
|
+
@property({ type: String }) direction: "horizontal" | "vertical" = "horizontal";
|
|
13
|
+
|
|
14
|
+
/* =======================
|
|
15
|
+
* INTERNAL STATE
|
|
16
|
+
* ======================= */
|
|
17
|
+
|
|
18
|
+
private panels: RelResizablePanel[] = [];
|
|
19
|
+
private handles: RelResizableHandle[] = [];
|
|
20
|
+
private isDragging = false;
|
|
21
|
+
private activeHandleIndex = -1;
|
|
22
|
+
private startPointerPos = 0;
|
|
23
|
+
private startSizes: number[] = [];
|
|
24
|
+
|
|
25
|
+
/* =======================
|
|
26
|
+
* STYLES
|
|
27
|
+
* ======================= */
|
|
28
|
+
|
|
29
|
+
static styles = css`
|
|
30
|
+
:host {
|
|
31
|
+
display: flex;
|
|
32
|
+
width: 100%;
|
|
33
|
+
height: 100%;
|
|
34
|
+
position: relative;
|
|
35
|
+
box-sizing: border-box;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
:host([direction="horizontal"]) {
|
|
39
|
+
flex-direction: row;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:host([direction="vertical"]) {
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
::slotted(rel-resizable-panel) {
|
|
47
|
+
flex-shrink: 0;
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
/* =======================
|
|
52
|
+
* LIFECYCLE
|
|
53
|
+
* ======================= */
|
|
54
|
+
|
|
55
|
+
connectedCallback() {
|
|
56
|
+
super.connectedCallback();
|
|
57
|
+
this.addEventListener("pointerdown", this.onPointerDown);
|
|
58
|
+
this.addEventListener("keydown", this.onKeyDown);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
disconnectedCallback() {
|
|
62
|
+
super.disconnectedCallback();
|
|
63
|
+
this.removeEventListener("pointerdown", this.onPointerDown);
|
|
64
|
+
this.removeEventListener("keydown", this.onKeyDown);
|
|
65
|
+
this.removeGlobalListeners();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
firstUpdated() {
|
|
69
|
+
this.initializeLayout();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* =======================
|
|
73
|
+
* INITIALIZATION
|
|
74
|
+
* ======================= */
|
|
75
|
+
|
|
76
|
+
private initializeLayout() {
|
|
77
|
+
// Query light DOM for panels and handles
|
|
78
|
+
this.panels = Array.from(
|
|
79
|
+
this.querySelectorAll(":scope > rel-resizable-panel")
|
|
80
|
+
) as RelResizablePanel[];
|
|
81
|
+
this.handles = Array.from(
|
|
82
|
+
this.querySelectorAll(":scope > rel-resizable-handle")
|
|
83
|
+
) as RelResizableHandle[];
|
|
84
|
+
|
|
85
|
+
// Set handle indices and orientations
|
|
86
|
+
this.handles.forEach((handle, idx) => {
|
|
87
|
+
handle.handleIndex = idx;
|
|
88
|
+
handle.orientation = this.direction;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Calculate initial sizes
|
|
92
|
+
const sizes = this.calculateInitialSizes();
|
|
93
|
+
this.applySizes(sizes);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private calculateInitialSizes(): number[] {
|
|
97
|
+
const sizes: number[] = [];
|
|
98
|
+
let specifiedTotal = 0;
|
|
99
|
+
let unspecifiedCount = 0;
|
|
100
|
+
|
|
101
|
+
// First pass: collect specified sizes
|
|
102
|
+
this.panels.forEach((panel) => {
|
|
103
|
+
if (panel.defaultSize !== undefined) {
|
|
104
|
+
sizes.push(panel.defaultSize);
|
|
105
|
+
specifiedTotal += panel.defaultSize;
|
|
106
|
+
} else {
|
|
107
|
+
sizes.push(0);
|
|
108
|
+
unspecifiedCount++;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Second pass: distribute remaining space to unspecified panels
|
|
113
|
+
if (unspecifiedCount > 0) {
|
|
114
|
+
const remaining = Math.max(0, 100 - specifiedTotal);
|
|
115
|
+
const perPanel = remaining / unspecifiedCount;
|
|
116
|
+
sizes.forEach((size, idx) => {
|
|
117
|
+
if (size === 0) {
|
|
118
|
+
sizes[idx] = perPanel;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Normalize to 100%
|
|
124
|
+
const total = sizes.reduce((sum, s) => sum + s, 0);
|
|
125
|
+
if (total > 0) {
|
|
126
|
+
return sizes.map((s) => (s / total) * 100);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Fallback: equal distribution
|
|
130
|
+
return this.panels.map(() => 100 / this.panels.length);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* =======================
|
|
134
|
+
* POINTER HANDLING
|
|
135
|
+
* ======================= */
|
|
136
|
+
|
|
137
|
+
private onPointerDown = (e: PointerEvent) => {
|
|
138
|
+
const target = e.composedPath()[0] as HTMLElement;
|
|
139
|
+
if (target.tagName !== "REL-RESIZABLE-HANDLE") return;
|
|
140
|
+
|
|
141
|
+
const handle = target as RelResizableHandle;
|
|
142
|
+
const handleIndex = this.handles.indexOf(handle);
|
|
143
|
+
if (handleIndex === -1) return;
|
|
144
|
+
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
this.startDrag(handleIndex, e);
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
private startDrag(handleIndex: number, e: PointerEvent) {
|
|
150
|
+
this.isDragging = true;
|
|
151
|
+
this.activeHandleIndex = handleIndex;
|
|
152
|
+
this.startPointerPos = this.direction === "horizontal" ? e.clientX : e.clientY;
|
|
153
|
+
this.startSizes = this.panels.map((p) => p.currentSize);
|
|
154
|
+
|
|
155
|
+
const handle = this.handles[handleIndex];
|
|
156
|
+
handle.setAttribute("data-dragging", "");
|
|
157
|
+
|
|
158
|
+
this.addGlobalListeners();
|
|
159
|
+
|
|
160
|
+
// Capture pointer
|
|
161
|
+
(e.target as HTMLElement).setPointerCapture(e.pointerId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private onPointerMove = (e: PointerEvent) => {
|
|
165
|
+
if (!this.isDragging || this.activeHandleIndex === -1) return;
|
|
166
|
+
|
|
167
|
+
e.preventDefault();
|
|
168
|
+
|
|
169
|
+
const currentPos = this.direction === "horizontal" ? e.clientX : e.clientY;
|
|
170
|
+
const delta = currentPos - this.startPointerPos;
|
|
171
|
+
|
|
172
|
+
this.applyDragResize(delta);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
private onPointerUp = (e: PointerEvent) => {
|
|
176
|
+
if (!this.isDragging) return;
|
|
177
|
+
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
|
|
180
|
+
const handle = this.handles[this.activeHandleIndex];
|
|
181
|
+
if (handle) {
|
|
182
|
+
handle.removeAttribute("data-dragging");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.isDragging = false;
|
|
186
|
+
this.activeHandleIndex = -1;
|
|
187
|
+
|
|
188
|
+
this.removeGlobalListeners();
|
|
189
|
+
|
|
190
|
+
// Dispatch resize-end event
|
|
191
|
+
this.dispatchResizeEvent("rel-resize-end");
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
private applyDragResize(deltaPixels: number) {
|
|
195
|
+
const rect = this.getBoundingClientRect();
|
|
196
|
+
const totalSize = this.direction === "horizontal" ? rect.width : rect.height;
|
|
197
|
+
const deltaPercent = (deltaPixels / totalSize) * 100;
|
|
198
|
+
|
|
199
|
+
const leftIdx = this.activeHandleIndex;
|
|
200
|
+
const rightIdx = this.activeHandleIndex + 1;
|
|
201
|
+
|
|
202
|
+
if (leftIdx < 0 || rightIdx >= this.panels.length) return;
|
|
203
|
+
|
|
204
|
+
const leftPanel = this.panels[leftIdx];
|
|
205
|
+
const rightPanel = this.panels[rightIdx];
|
|
206
|
+
|
|
207
|
+
let newLeftSize = this.startSizes[leftIdx] + deltaPercent;
|
|
208
|
+
let newRightSize = this.startSizes[rightIdx] - deltaPercent;
|
|
209
|
+
|
|
210
|
+
// Clamp to min/max
|
|
211
|
+
newLeftSize = Math.max(leftPanel.minSize, Math.min(leftPanel.maxSize, newLeftSize));
|
|
212
|
+
newRightSize = Math.max(rightPanel.minSize, Math.min(rightPanel.maxSize, newRightSize));
|
|
213
|
+
|
|
214
|
+
// Ensure total is preserved
|
|
215
|
+
const pairTotal = this.startSizes[leftIdx] + this.startSizes[rightIdx];
|
|
216
|
+
const adjustedTotal = newLeftSize + newRightSize;
|
|
217
|
+
if (adjustedTotal !== pairTotal) {
|
|
218
|
+
const ratio = pairTotal / adjustedTotal;
|
|
219
|
+
newLeftSize *= ratio;
|
|
220
|
+
newRightSize *= ratio;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Update sizes
|
|
224
|
+
const newSizes = [...this.startSizes];
|
|
225
|
+
newSizes[leftIdx] = newLeftSize;
|
|
226
|
+
newSizes[rightIdx] = newRightSize;
|
|
227
|
+
|
|
228
|
+
this.applySizes(newSizes);
|
|
229
|
+
|
|
230
|
+
// Update handle aria value
|
|
231
|
+
const handle = this.handles[this.activeHandleIndex];
|
|
232
|
+
if (handle) {
|
|
233
|
+
const splitPercent = (newLeftSize / pairTotal) * 100;
|
|
234
|
+
handle.updateAriaValue(splitPercent);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Dispatch resize event
|
|
238
|
+
this.dispatchResizeEvent("rel-resize");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* =======================
|
|
242
|
+
* KEYBOARD HANDLING
|
|
243
|
+
* ======================= */
|
|
244
|
+
|
|
245
|
+
private onKeyDown = (e: KeyboardEvent) => {
|
|
246
|
+
const target = e.composedPath()[0] as HTMLElement;
|
|
247
|
+
if (target.tagName !== "REL-RESIZABLE-HANDLE") return;
|
|
248
|
+
|
|
249
|
+
const handle = target as RelResizableHandle;
|
|
250
|
+
const handleIndex = this.handles.indexOf(handle);
|
|
251
|
+
if (handleIndex === -1) return;
|
|
252
|
+
|
|
253
|
+
const isHorizontal = this.direction === "horizontal";
|
|
254
|
+
const step = e.shiftKey ? 5 : 2;
|
|
255
|
+
let applied = false;
|
|
256
|
+
|
|
257
|
+
if (isHorizontal && (e.key === "ArrowLeft" || e.key === "ArrowRight")) {
|
|
258
|
+
e.preventDefault();
|
|
259
|
+
const delta = e.key === "ArrowLeft" ? -step : step;
|
|
260
|
+
this.applyKeyboardResize(handleIndex, delta);
|
|
261
|
+
applied = true;
|
|
262
|
+
} else if (!isHorizontal && (e.key === "ArrowUp" || e.key === "ArrowDown")) {
|
|
263
|
+
e.preventDefault();
|
|
264
|
+
const delta = e.key === "ArrowUp" ? -step : step;
|
|
265
|
+
this.applyKeyboardResize(handleIndex, delta);
|
|
266
|
+
applied = true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (applied) {
|
|
270
|
+
this.dispatchResizeEvent("rel-resize-end");
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
private applyKeyboardResize(handleIndex: number, deltaPercent: number) {
|
|
275
|
+
const leftIdx = handleIndex;
|
|
276
|
+
const rightIdx = handleIndex + 1;
|
|
277
|
+
|
|
278
|
+
if (leftIdx < 0 || rightIdx >= this.panels.length) return;
|
|
279
|
+
|
|
280
|
+
const leftPanel = this.panels[leftIdx];
|
|
281
|
+
const rightPanel = this.panels[rightIdx];
|
|
282
|
+
const currentSizes = this.panels.map((p) => p.currentSize);
|
|
283
|
+
|
|
284
|
+
let newLeftSize = currentSizes[leftIdx] + deltaPercent;
|
|
285
|
+
let newRightSize = currentSizes[rightIdx] - deltaPercent;
|
|
286
|
+
|
|
287
|
+
// Clamp to min/max
|
|
288
|
+
newLeftSize = Math.max(leftPanel.minSize, Math.min(leftPanel.maxSize, newLeftSize));
|
|
289
|
+
newRightSize = Math.max(rightPanel.minSize, Math.min(rightPanel.maxSize, newRightSize));
|
|
290
|
+
|
|
291
|
+
// Ensure total is preserved
|
|
292
|
+
const pairTotal = currentSizes[leftIdx] + currentSizes[rightIdx];
|
|
293
|
+
const adjustedTotal = newLeftSize + newRightSize;
|
|
294
|
+
if (adjustedTotal !== pairTotal) {
|
|
295
|
+
const ratio = pairTotal / adjustedTotal;
|
|
296
|
+
newLeftSize *= ratio;
|
|
297
|
+
newRightSize *= ratio;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Update sizes
|
|
301
|
+
const newSizes = [...currentSizes];
|
|
302
|
+
newSizes[leftIdx] = newLeftSize;
|
|
303
|
+
newSizes[rightIdx] = newRightSize;
|
|
304
|
+
|
|
305
|
+
this.applySizes(newSizes);
|
|
306
|
+
|
|
307
|
+
// Update handle aria value
|
|
308
|
+
const handle = this.handles[handleIndex];
|
|
309
|
+
if (handle) {
|
|
310
|
+
const splitPercent = (newLeftSize / pairTotal) * 100;
|
|
311
|
+
handle.updateAriaValue(splitPercent);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* =======================
|
|
316
|
+
* SIZE APPLICATION
|
|
317
|
+
* ======================= */
|
|
318
|
+
|
|
319
|
+
private applySizes(sizes: number[]) {
|
|
320
|
+
this.panels.forEach((panel, idx) => {
|
|
321
|
+
const size = sizes[idx] || 0;
|
|
322
|
+
panel.currentSize = size;
|
|
323
|
+
panel.style.flexBasis = `${size}%`;
|
|
324
|
+
panel.style.flexGrow = "0";
|
|
325
|
+
panel.style.flexShrink = "0";
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* =======================
|
|
330
|
+
* EVENTS
|
|
331
|
+
* ======================= */
|
|
332
|
+
|
|
333
|
+
private dispatchResizeEvent(eventName: string) {
|
|
334
|
+
const sizes = this.panels.map((p) => p.currentSize);
|
|
335
|
+
this.dispatchEvent(
|
|
336
|
+
new CustomEvent(eventName, {
|
|
337
|
+
detail: {
|
|
338
|
+
direction: this.direction,
|
|
339
|
+
sizes,
|
|
340
|
+
},
|
|
341
|
+
bubbles: true,
|
|
342
|
+
composed: true,
|
|
343
|
+
})
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/* =======================
|
|
348
|
+
* GLOBAL LISTENERS
|
|
349
|
+
* ======================= */
|
|
350
|
+
|
|
351
|
+
private addGlobalListeners() {
|
|
352
|
+
document.addEventListener("pointermove", this.onPointerMove);
|
|
353
|
+
document.addEventListener("pointerup", this.onPointerUp);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
private removeGlobalListeners() {
|
|
357
|
+
document.removeEventListener("pointermove", this.onPointerMove);
|
|
358
|
+
document.removeEventListener("pointerup", this.onPointerUp);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* =======================
|
|
362
|
+
* RENDER
|
|
363
|
+
* ======================= */
|
|
364
|
+
|
|
365
|
+
render() {
|
|
366
|
+
return html`<slot></slot>`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
declare global {
|
|
371
|
+
interface HTMLElementTagNameMap {
|
|
372
|
+
"rel-resizable-group": RelResizableGroup;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
|
|
4
|
+
@customElement("rel-resizable-handle")
|
|
5
|
+
export class RelResizableHandle extends LitElement {
|
|
6
|
+
/* =======================
|
|
7
|
+
* PUBLIC API
|
|
8
|
+
* ======================= */
|
|
9
|
+
|
|
10
|
+
@property({ type: String, reflect: true }) orientation: "horizontal" | "vertical" = "horizontal";
|
|
11
|
+
|
|
12
|
+
/* =======================
|
|
13
|
+
* INTERNAL STATE
|
|
14
|
+
* ======================= */
|
|
15
|
+
|
|
16
|
+
/** Index of this handle in the parent group */
|
|
17
|
+
public handleIndex = -1;
|
|
18
|
+
|
|
19
|
+
/** Current split value (0-100) for aria-valuenow */
|
|
20
|
+
public splitValue = 50;
|
|
21
|
+
|
|
22
|
+
/* =======================
|
|
23
|
+
* STYLES
|
|
24
|
+
* ======================= */
|
|
25
|
+
|
|
26
|
+
static styles = css`
|
|
27
|
+
:host {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
flex-shrink: 0;
|
|
32
|
+
position: relative;
|
|
33
|
+
outline: none;
|
|
34
|
+
background: var(--rel-colors-gray-border);
|
|
35
|
+
transition: background 0.15s ease;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
:host([orientation="horizontal"]) {
|
|
39
|
+
width: var(--rel-borders-width-stroke-100);
|
|
40
|
+
min-width: var(--rel-borders-width-stroke-100);
|
|
41
|
+
cursor: col-resize;
|
|
42
|
+
padding: 0 var(--rel-spacing-space-xs);
|
|
43
|
+
margin: 0 calc(var(--rel-spacing-space-xs) * -1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
:host([orientation="vertical"]) {
|
|
47
|
+
height: var(--rel-borders-width-stroke-100);
|
|
48
|
+
min-height: var(--rel-borders-width-stroke-100);
|
|
49
|
+
cursor: row-resize;
|
|
50
|
+
padding: var(--rel-spacing-space-xs) 0;
|
|
51
|
+
margin: calc(var(--rel-spacing-space-xs) * -1) 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
:host(:hover),
|
|
55
|
+
:host(:focus-visible) {
|
|
56
|
+
background: var(--rel-colors-blue-primary);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
:host(:focus-visible) {
|
|
60
|
+
outline: 2px solid var(--rel-colors-blue-primary);
|
|
61
|
+
outline-offset: 2px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.grabber {
|
|
65
|
+
background: var(--rel-colors-gray-placeholder);
|
|
66
|
+
border-radius: var(--rel-borders-radius-border-radius-500);
|
|
67
|
+
transition: background 0.15s ease;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
:host([orientation="horizontal"]) .grabber {
|
|
71
|
+
width: 4px;
|
|
72
|
+
height: 24px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
:host([orientation="vertical"]) .grabber {
|
|
76
|
+
width: 24px;
|
|
77
|
+
height: 4px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
:host(:hover) .grabber,
|
|
81
|
+
:host(:focus-visible) .grabber {
|
|
82
|
+
background: var(--rel-colors-white);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
:host([data-dragging]) {
|
|
86
|
+
background: var(--rel-colors-blue-primary);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
:host([data-dragging]) .grabber {
|
|
90
|
+
background: var(--rel-colors-white);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
@media (prefers-reduced-motion: reduce) {
|
|
94
|
+
:host {
|
|
95
|
+
transition: none;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.grabber {
|
|
99
|
+
transition: none;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
/* =======================
|
|
105
|
+
* LIFECYCLE
|
|
106
|
+
* ======================= */
|
|
107
|
+
|
|
108
|
+
connectedCallback() {
|
|
109
|
+
super.connectedCallback();
|
|
110
|
+
this.setAttribute("tabindex", "0");
|
|
111
|
+
this.setAttribute("role", "separator");
|
|
112
|
+
this.updateAriaOrientation();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
updated(changed: Map<string, unknown>) {
|
|
116
|
+
if (changed.has("orientation")) {
|
|
117
|
+
this.updateAriaOrientation();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* =======================
|
|
122
|
+
* ARIA
|
|
123
|
+
* ======================= */
|
|
124
|
+
|
|
125
|
+
private updateAriaOrientation() {
|
|
126
|
+
this.setAttribute("aria-orientation", this.orientation);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public updateAriaValue(value: number) {
|
|
130
|
+
this.splitValue = value;
|
|
131
|
+
this.setAttribute("aria-valuenow", String(Math.round(value)));
|
|
132
|
+
this.setAttribute("aria-valuemin", "0");
|
|
133
|
+
this.setAttribute("aria-valuemax", "100");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* =======================
|
|
137
|
+
* RENDER
|
|
138
|
+
* ======================= */
|
|
139
|
+
|
|
140
|
+
render() {
|
|
141
|
+
return html`<div class="grabber"></div>`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
declare global {
|
|
146
|
+
interface HTMLElementTagNameMap {
|
|
147
|
+
"rel-resizable-handle": RelResizableHandle;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { LitElement, html, css } from "lit";
|
|
2
|
+
import { customElement, property } from "lit/decorators.js";
|
|
3
|
+
|
|
4
|
+
@customElement("rel-resizable-panel")
|
|
5
|
+
export class RelResizablePanel extends LitElement {
|
|
6
|
+
/* =======================
|
|
7
|
+
* PUBLIC API
|
|
8
|
+
* ======================= */
|
|
9
|
+
|
|
10
|
+
@property({ type: Number, attribute: "defaultsize" }) defaultSize?: number;
|
|
11
|
+
@property({ type: Number, attribute: "minsize" }) minSize = 10;
|
|
12
|
+
@property({ type: Number, attribute: "maxsize" }) maxSize = 90;
|
|
13
|
+
|
|
14
|
+
/* =======================
|
|
15
|
+
* INTERNAL STATE
|
|
16
|
+
* ======================= */
|
|
17
|
+
|
|
18
|
+
/** Current computed size in percentage */
|
|
19
|
+
public currentSize = 0;
|
|
20
|
+
|
|
21
|
+
/* =======================
|
|
22
|
+
* STYLES
|
|
23
|
+
* ======================= */
|
|
24
|
+
|
|
25
|
+
static styles = css`
|
|
26
|
+
:host {
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
overflow: auto;
|
|
30
|
+
box-sizing: border-box;
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
/* =======================
|
|
35
|
+
* RENDER
|
|
36
|
+
* ======================= */
|
|
37
|
+
|
|
38
|
+
render() {
|
|
39
|
+
return html`<slot></slot>`;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
declare global {
|
|
44
|
+
interface HTMLElementTagNameMap {
|
|
45
|
+
"rel-resizable-panel": RelResizablePanel;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# rel-segmented-button
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
A segmented button group for mutually exclusive selections. Displays multiple options as connected buttons with a single active selection. Supports size variants, alignment options, and individual option states (disabled).
|
|
5
|
+
|
|
6
|
+
## Usage
|
|
7
|
+
```html
|
|
8
|
+
<!-- Basic segmented button -->
|
|
9
|
+
<rel-segmented-button id="viewMode" active="grid"></rel-segmented-button>
|
|
10
|
+
<script>
|
|
11
|
+
const viewMode = document.getElementById('viewMode');
|
|
12
|
+
viewMode.options = [
|
|
13
|
+
{ label: 'List', value: 'list' },
|
|
14
|
+
{ label: 'Grid', value: 'grid' },
|
|
15
|
+
{ label: 'Table', value: 'table' }
|
|
16
|
+
];
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<!-- Small size with alignment -->
|
|
20
|
+
<rel-segmented-button
|
|
21
|
+
id="textAlign"
|
|
22
|
+
size="small"
|
|
23
|
+
align="middle"
|
|
24
|
+
active="center">
|
|
25
|
+
</rel-segmented-button>
|
|
26
|
+
<script>
|
|
27
|
+
const textAlign = document.getElementById('textAlign');
|
|
28
|
+
textAlign.options = [
|
|
29
|
+
{ label: 'Left', value: 'left' },
|
|
30
|
+
{ label: 'Center', value: 'center' },
|
|
31
|
+
{ label: 'Right', value: 'right' }
|
|
32
|
+
];
|
|
33
|
+
</script>
|
|
34
|
+
|
|
35
|
+
<!-- With disabled option -->
|
|
36
|
+
<rel-segmented-button id="planSelector"></rel-segmented-button>
|
|
37
|
+
<script>
|
|
38
|
+
const planSelector = document.getElementById('planSelector');
|
|
39
|
+
planSelector.options = [
|
|
40
|
+
{ label: 'Free', value: 'free' },
|
|
41
|
+
{ label: 'Pro', value: 'pro' },
|
|
42
|
+
{ label: 'Enterprise', value: 'enterprise', disabled: true }
|
|
43
|
+
];
|
|
44
|
+
planSelector.active = 'free';
|
|
45
|
+
</script>
|
|
46
|
+
|
|
47
|
+
<!-- Listen for changes -->
|
|
48
|
+
<rel-segmented-button id="sortOrder"></rel-segmented-button>
|
|
49
|
+
<script>
|
|
50
|
+
const sortOrder = document.getElementById('sortOrder');
|
|
51
|
+
sortOrder.options = [
|
|
52
|
+
{ label: 'Newest', value: 'newest' },
|
|
53
|
+
{ label: 'Oldest', value: 'oldest' },
|
|
54
|
+
{ label: 'Popular', value: 'popular' }
|
|
55
|
+
];
|
|
56
|
+
sortOrder.addEventListener('changed', (e) => {
|
|
57
|
+
console.log('Selected:', e.detail.value);
|
|
58
|
+
});
|
|
59
|
+
</script>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API
|
|
63
|
+
|
|
64
|
+
### Attributes/Properties
|
|
65
|
+
| Name | Type | Default | Description |
|
|
66
|
+
|------|------|---------|-------------|
|
|
67
|
+
| `options` | `SegmentOption[]` | `[]` | Array of segment options (see SegmentOption interface below) |
|
|
68
|
+
| `active` | `string` | `""` | Value of the currently active segment |
|
|
69
|
+
| `size` | `"small" \| "medium"` | `"medium"` | Size variant |
|
|
70
|
+
| `align` | `"left" \| "middle" \| "right"` | `"left"` | Horizontal alignment of the button group |
|
|
71
|
+
|
|
72
|
+
### SegmentOption Interface
|
|
73
|
+
```typescript
|
|
74
|
+
interface SegmentOption {
|
|
75
|
+
label: string; // Display text for the button
|
|
76
|
+
value: string; // Unique value identifier
|
|
77
|
+
disabled?: boolean; // Optional: disable this specific segment
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Events
|
|
82
|
+
| Event | Detail | Description |
|
|
83
|
+
|-------|--------|-------------|
|
|
84
|
+
| `changed` | `{ value: string }` | Fired when a different segment is selected |
|
|
85
|
+
|
|
86
|
+
## Slots
|
|
87
|
+
Default slot not used.
|
|
88
|
+
|
|
89
|
+
## CSS Parts
|
|
90
|
+
None exposed.
|
|
91
|
+
|
|
92
|
+
## Tokens / CSS Variables
|
|
93
|
+
|
|
94
|
+
### Semantic Tokens
|
|
95
|
+
- `--rel-fontFamilies-primary` - Font family
|
|
96
|
+
- `--rel-fontSizes-xs`, `--rel-fontSizes-sm` - Font sizes for small and medium variants
|
|
97
|
+
- `--rel-fontWeights-medium` - Font weight for segment labels
|
|
98
|
+
|
|
99
|
+
### Spacing Tokens
|
|
100
|
+
- `--rel-spacing-space-3xs`, `--rel-spacing-space-2xs` - Vertical padding for sizes
|
|
101
|
+
- `--rel-spacing-space-xs`, `--rel-spacing-space-sm` - Horizontal padding for sizes
|
|
102
|
+
|
|
103
|
+
### Color Tokens
|
|
104
|
+
- `--rel-colors-white` - Default segment background
|
|
105
|
+
- `--rel-colors-gray-border` - Outer border and segment dividers
|
|
106
|
+
- `--rel-text-heading` - Active segment background and default text color
|
|
107
|
+
- `--rel-surface-surface` - Hover and disabled background
|
|
108
|
+
- `--rel-text-disabled` - Disabled text color
|
|
109
|
+
- `--rel-border-info` - Focus outline color
|
|
110
|
+
|
|
111
|
+
### Border Tokens
|
|
112
|
+
- `--rel-borders-radius-border-radius-300` - Border radius for the group
|
|
113
|
+
|
|
114
|
+
## Accessibility
|
|
115
|
+
- Uses native `<button>` elements for proper keyboard navigation
|
|
116
|
+
- Focus visible state with inner outline for keyboard users
|
|
117
|
+
- Disabled segments use `pointer-events: none` and reduced opacity
|
|
118
|
+
- Each segment can receive focus independently
|
|
119
|
+
|
|
120
|
+
### Keyboard Interactions
|
|
121
|
+
- `Tab` - Move focus between segments
|
|
122
|
+
- `Space/Enter` - Activate focused segment
|
|
123
|
+
- `Arrow Left/Right` - Navigate between segments (browser default)
|
|
124
|
+
|
|
125
|
+
### Focus Behavior
|
|
126
|
+
- Focus outline appears inside the button boundary with `-2px` offset
|
|
127
|
+
- Uses info border color for high contrast
|
|
128
|
+
- Active button has `z-index: 1` to ensure focus ring is visible over adjacent segments
|
|
129
|
+
|
|
130
|
+
## Notes / Edge Cases
|
|
131
|
+
- The component is `display: inline-block` and sizes to fit content
|
|
132
|
+
- All segments are connected with shared borders (2px solid gray)
|
|
133
|
+
- The last segment has no right border to avoid doubling with the outer container border
|
|
134
|
+
- Active segment has inverted colors (dark background, white text)
|
|
135
|
+
- Hovering a non-active, non-disabled segment shows a light gray background
|
|
136
|
+
- Hovering the active segment does not change its appearance
|
|
137
|
+
- Disabled segments show reduced opacity (0.5), gray background, and are non-interactive
|
|
138
|
+
- The `align` property uses flexbox justify-content (left → flex-start, middle → center, right → flex-end)
|
|
139
|
+
- Size variants affect both font size and padding
|
|
140
|
+
- Smooth transitions (0.2s ease-in-out) apply to all visual changes
|
|
141
|
+
- Clicking a disabled option will not change the `active` value or dispatch events
|