@reliability-design/web 1.0.3 → 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.
Files changed (114) hide show
  1. package/dist/components/rel-accordion/README.md +132 -0
  2. package/dist/components/rel-accordion/rel-accordion.ts +43 -16
  3. package/dist/components/rel-alert/README.md +141 -0
  4. package/dist/components/rel-alert/rel-alert.ts +18 -7
  5. package/dist/components/rel-avatar/README.md +119 -0
  6. package/dist/components/rel-badge/README.md +86 -0
  7. package/dist/components/rel-badge/rel-badge.ts +6 -6
  8. package/dist/components/rel-breadcrumb/README.md +102 -0
  9. package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +12 -6
  10. package/dist/components/rel-button/README.md +130 -0
  11. package/dist/components/rel-calendar/README.md +109 -0
  12. package/dist/components/rel-calendar/index.ts +1 -0
  13. package/dist/components/rel-calendar/rel-calendar.ts +467 -0
  14. package/dist/components/rel-card/README.md +94 -0
  15. package/dist/components/rel-card/rel-card.ts +12 -3
  16. package/dist/components/rel-cardmedia/README.md +127 -0
  17. package/dist/components/rel-cardmedia/rel-cardmedia.ts +14 -4
  18. package/dist/components/rel-carousel/README.md +166 -0
  19. package/dist/components/rel-carousel/index.ts +1 -0
  20. package/dist/components/rel-carousel/rel-carousel.ts +469 -0
  21. package/dist/components/rel-chart/README.md +286 -0
  22. package/dist/components/rel-chart/index.ts +1 -0
  23. package/dist/components/rel-chart/rel-chart.ts +736 -0
  24. package/dist/components/rel-checkbox/README.md +77 -0
  25. package/dist/components/rel-checkbox/rel-checkbox.ts +18 -10
  26. package/dist/components/rel-chip/README.md +114 -0
  27. package/dist/components/rel-chip/rel-chip.ts +20 -4
  28. package/dist/components/rel-container/README.md +99 -0
  29. package/dist/components/rel-date-picker/README.md +253 -0
  30. package/dist/components/rel-date-picker/index.ts +1 -0
  31. package/dist/components/rel-date-picker/rel-date-picker.ts +916 -0
  32. package/dist/components/rel-divider/README.md +100 -0
  33. package/dist/components/rel-drawer/README.md +200 -0
  34. package/dist/components/rel-drawer/index.ts +1 -0
  35. package/dist/components/rel-drawer/rel-drawer.ts +621 -0
  36. package/dist/components/rel-kbd/README.md +151 -0
  37. package/dist/components/rel-kbd/rel-kbd.ts +118 -0
  38. package/dist/components/rel-layout/README.md +88 -0
  39. package/dist/components/rel-menu/README.md +151 -0
  40. package/dist/components/rel-menu/rel-menu.ts +18 -10
  41. package/dist/components/rel-menubar/README.md +329 -0
  42. package/dist/components/rel-menubar/index.ts +2 -0
  43. package/dist/components/rel-menubar/rel-menubar-menu.ts +16 -0
  44. package/dist/components/rel-menubar/rel-menubar.ts +576 -0
  45. package/dist/components/rel-nav-menu/README.md +289 -0
  46. package/dist/components/rel-nav-menu/index.ts +3 -0
  47. package/dist/components/rel-nav-menu/rel-nav-menu-item.ts +143 -0
  48. package/dist/components/rel-nav-menu/rel-nav-menu-panel.ts +53 -0
  49. package/dist/components/rel-nav-menu/rel-nav-menu.ts +331 -0
  50. package/dist/components/rel-navbar/README.md +126 -0
  51. package/dist/components/rel-navbar/rel-navbar.ts +91 -0
  52. package/dist/components/rel-otp/README.md +440 -0
  53. package/dist/components/rel-otp/index.ts +1 -0
  54. package/dist/components/rel-otp/rel-otp.ts +504 -0
  55. package/dist/components/rel-pagination/README.md +410 -0
  56. package/dist/components/rel-pagination/index.ts +1 -0
  57. package/dist/components/rel-pagination/rel-pagination.ts +376 -0
  58. package/dist/components/rel-popover/README.md +568 -0
  59. package/dist/components/rel-popover/index.ts +2 -0
  60. package/dist/components/rel-popover/rel-popover-content.ts +55 -0
  61. package/dist/components/rel-popover/rel-popover.ts +327 -0
  62. package/dist/components/rel-progress/README.md +110 -0
  63. package/dist/components/rel-progress/rel-progress.ts +32 -5
  64. package/dist/components/rel-quote-page/README.md +166 -0
  65. package/dist/components/rel-quote-page/rel-quote-page.ts +816 -0
  66. package/dist/components/rel-radio/README.md +120 -0
  67. package/dist/components/rel-radio/rel-radio.ts +13 -5
  68. package/dist/components/rel-resizable/README.md +140 -0
  69. package/dist/components/rel-resizable/index.ts +3 -0
  70. package/dist/components/rel-resizable/rel-resizable-group.ts +374 -0
  71. package/dist/components/rel-resizable/rel-resizable-handle.ts +149 -0
  72. package/dist/components/rel-resizable/rel-resizable-panel.ts +47 -0
  73. package/dist/components/rel-segmented-button/README.md +141 -0
  74. package/dist/components/rel-segmented-button/rel-segmented-button.ts +18 -13
  75. package/dist/components/rel-select/README.md +107 -0
  76. package/dist/components/rel-select/rel-select.ts +14 -14
  77. package/dist/components/rel-sheet/README.md +224 -0
  78. package/dist/components/rel-sheet/rel-sheet.ts +683 -0
  79. package/dist/components/rel-sidebar/README.md +209 -0
  80. package/dist/components/rel-sidebar/rel-sidebar.ts +1158 -0
  81. package/dist/components/rel-slider/README.md +135 -0
  82. package/dist/components/rel-slider/rel-slider.ts +19 -7
  83. package/dist/components/rel-snackbar/README.md +134 -0
  84. package/dist/components/rel-snackbar/rel-snackbar.ts +67 -2
  85. package/dist/components/rel-stepper/README.md +160 -0
  86. package/dist/components/rel-stepper/rel-stepper.ts +6 -6
  87. package/dist/components/rel-switch/README.md +102 -0
  88. package/dist/components/rel-switch/rel-switch.ts +15 -6
  89. package/dist/components/rel-tab/README.md +163 -0
  90. package/dist/components/rel-tab/rel-tab.ts +36 -14
  91. package/dist/components/rel-table/README.md +178 -0
  92. package/dist/components/rel-table/rel-table-body.ts +16 -0
  93. package/dist/components/rel-table/rel-table-caption.ts +22 -0
  94. package/dist/components/rel-table/rel-table-cell.ts +49 -0
  95. package/dist/components/rel-table/rel-table-footer.ts +18 -0
  96. package/dist/components/rel-table/rel-table-head.ts +33 -0
  97. package/dist/components/rel-table/rel-table-header.ts +16 -0
  98. package/dist/components/rel-table/rel-table-row.ts +32 -0
  99. package/dist/components/rel-table/rel-table.ts +47 -0
  100. package/dist/components/rel-textfield/README.md +90 -0
  101. package/dist/components/rel-textfield/rel-textfield.ts +35 -19
  102. package/dist/components/rel-toast/README.md +148 -0
  103. package/dist/components/rel-toast/rel-toast.ts +32 -14
  104. package/dist/components/rel-tooltip/README.md +103 -0
  105. package/dist/components/rel-tooltip/rel-tooltip.ts +9 -9
  106. package/dist/index.d.mts +836 -1
  107. package/dist/index.d.ts +836 -1
  108. package/dist/index.js +10658 -1493
  109. package/dist/index.js.map +1 -1
  110. package/dist/index.mjs +10616 -1499
  111. package/dist/index.mjs.map +1 -1
  112. package/dist/styles/vars.css +293 -183
  113. package/dist/{vars-6ZIUPIU4.css → vars-ITL2K3KO.css} +293 -183
  114. package/package.json +1 -1
@@ -0,0 +1,120 @@
1
+ # rel-radio
2
+
3
+ ## Overview
4
+ A radio button group component that allows users to select a single option from a list. Supports individual option states (disabled, error), supporting text, and comprehensive styling with design tokens.
5
+
6
+ ## Usage
7
+ ```html
8
+ <!-- Basic example -->
9
+ <rel-radio id="colorRadio" name="color-group" selectedValue="blue"></rel-radio>
10
+ <script>
11
+ const radio = document.getElementById('colorRadio');
12
+ radio.options = [
13
+ { value: 'red', label: 'Red' },
14
+ { value: 'blue', label: 'Blue' },
15
+ { value: 'green', label: 'Green' }
16
+ ];
17
+ </script>
18
+
19
+ <!-- Example with supporting text and states -->
20
+ <rel-radio id="planRadio" name="plan-group"></rel-radio>
21
+ <script>
22
+ const planRadio = document.getElementById('planRadio');
23
+ planRadio.options = [
24
+ { value: 'basic', label: 'Basic Plan', supportingText: '$9.99/month' },
25
+ { value: 'pro', label: 'Pro Plan', supportingText: '$19.99/month' },
26
+ { value: 'enterprise', label: 'Enterprise', supportingText: 'Contact sales', disabled: true }
27
+ ];
28
+ planRadio.selectedValue = 'basic';
29
+ </script>
30
+
31
+ <!-- Listen for changes -->
32
+ <rel-radio id="preferenceRadio"></rel-radio>
33
+ <script>
34
+ const prefRadio = document.getElementById('preferenceRadio');
35
+ prefRadio.options = [
36
+ { value: 'email', label: 'Email' },
37
+ { value: 'sms', label: 'SMS' }
38
+ ];
39
+ prefRadio.addEventListener('changed', (e) => {
40
+ console.log('Selected:', prefRadio.selectedValue);
41
+ });
42
+ </script>
43
+ ```
44
+
45
+ ## API
46
+
47
+ ### Attributes/Properties
48
+ | Name | Type | Default | Description |
49
+ |------|------|---------|-------------|
50
+ | `options` | `RadioOption[]` | `[]` | Array of radio options (see RadioOption interface below) |
51
+ | `name` | `string` | `"radio-group"` | Name attribute for the radio group (for form grouping) |
52
+ | `selectedValue` | `string \| null` | `null` | Currently selected option value (reflected attribute) |
53
+ | `disabled` | `boolean` | `false` | Disables the entire radio group |
54
+
55
+ ### RadioOption Interface
56
+ ```typescript
57
+ interface RadioOption {
58
+ value: string; // Unique value for this option
59
+ label: string; // Display label
60
+ supportingText?: string; // Optional helper text below the label
61
+ disabled?: boolean; // Disable individual option
62
+ error?: boolean; // Show error state for individual option
63
+ }
64
+ ```
65
+
66
+ ## Events
67
+ | Event | Detail | Description |
68
+ |-------|--------|-------------|
69
+ | `changed` | None (read `selectedValue` property) | Fired when the selected option changes |
70
+
71
+ ## Slots
72
+ Default slot not used.
73
+
74
+ ## CSS Parts
75
+ None exposed.
76
+
77
+ ## Tokens / CSS Variables
78
+
79
+ ### Semantic Tokens
80
+ - `--rel-fontFamilies-primary` - Font family
81
+ - `--rel-fontSizes-sm`, `--rel-fontSizes-xs` - Font sizes for label and supporting text
82
+ - `--rel-fontWeights-medium` - Font weight for labels
83
+
84
+ ### Spacing Tokens
85
+ - `--rel-spacing-space-xs` - Gap between radio options
86
+ - `--rel-spacing-space-3xs` - Gap within each option (label and supporting text)
87
+ - `--rel-spacing-space-2xs` - Gap between radio circle and label
88
+
89
+ ### Color Tokens
90
+ - `--rel-text-heading` - Radio button border and checked dot color
91
+ - `--rel-text-primary` - Label text color
92
+ - `--rel-text-placeholder` - Supporting text color
93
+ - `--rel-border-info` - Focus outline color
94
+ - `--rel-colors-red-secondary` - Error state border and dot color
95
+
96
+ ## Accessibility
97
+ - Uses visually hidden native radio inputs for proper screen reader support
98
+ - Custom visual radio circle with proper focus indicators
99
+ - Focus visible state with outline offset for keyboard navigation
100
+ - Label click targets the radio for larger interaction area
101
+ - Disabled state communicated through reduced opacity and cursor change
102
+ - Proper `name` grouping ensures only one option can be selected
103
+
104
+ ### Keyboard Interactions
105
+ - `Tab` - Move focus to/from radio group
106
+ - `Arrow Up/Down` - Navigate between radio options
107
+ - `Space` - Select focused option
108
+
109
+ ### Focus Behavior
110
+ - Focus outline appears with offset around the custom radio circle
111
+ - Uses info border color for high contrast
112
+
113
+ ## Notes / Edge Cases
114
+ - The `selectedValue` property is reflected as an attribute and can be set programmatically or via HTML
115
+ - When `disabled` is true on the component, all options are disabled regardless of individual option settings
116
+ - Individual options can be disabled via the `disabled` property in the `RadioOption` object
117
+ - The `error` property on `RadioOption` changes the radio button color to red but does not prevent selection
118
+ - Supporting text is optional and appears below the label with left margin alignment
119
+ - Native radio inputs are visually hidden but maintain all accessibility features
120
+ - The `name` attribute groups radios together for form submission and mutual exclusion
@@ -60,27 +60,35 @@ export class RelRadio extends LitElement {
60
60
  .radio-custom {
61
61
  width: 20px;
62
62
  height: 20px;
63
- border: 2px solid var(--rel-colors-black);
63
+ border: 2px solid var(--rel-text-heading, #2b2b2b);
64
64
  border-radius: 50%;
65
65
  position: relative;
66
+ flex-shrink: 0;
67
+ }
68
+
69
+ input:focus-visible + .radio-custom {
70
+ outline: 2px solid var(--rel-border-info, #0091ea);
71
+ outline-offset: 2px;
66
72
  }
67
73
 
68
74
  input:checked + .radio-custom::after {
69
75
  content: "";
70
76
  position: absolute;
71
77
  inset: 4px;
72
- background: var(--rel-colors-black);
78
+ background: var(--rel-text-heading, #2b2b2b);
73
79
  border-radius: 50%;
74
80
  }
75
81
 
76
82
  .radio-label {
77
- font-size: var(--rel-fontSizes-sm);
78
- font-weight: var(--rel-fontWeights-medium);
83
+ font-size: var(--rel-fontSizes-sm, 12px);
84
+ font-weight: var(--rel-fontWeights-medium, 500);
85
+ color: var(--rel-text-primary, #333);
79
86
  }
80
87
 
81
88
  .radio-supporting {
82
- font-size: var(--rel-fontSizes-xs);
89
+ font-size: var(--rel-fontSizes-xs, 10px);
83
90
  margin-left: 28px;
91
+ color: var(--rel-text-placeholder, #a9a9a9);
84
92
  }
85
93
 
86
94
  .error .radio-custom {
@@ -0,0 +1,140 @@
1
+ # Resizable Components
2
+
3
+ A flexible resizable panel system built with Lit web components for the Reliability Design System.
4
+
5
+ ## Components
6
+
7
+ ### `<rel-resizable-group>`
8
+ Container for resizable panels. Manages layout direction and coordinates resizing between panels.
9
+
10
+ **Attributes:**
11
+ - `direction` (string): Layout direction - `"horizontal"` (default) or `"vertical"`
12
+
13
+ **Events:**
14
+ - `rel-resize`: Emitted during resize with detail `{ direction, sizes: number[] }`
15
+ - `rel-resize-end`: Emitted when resize completes with detail `{ direction, sizes: number[] }`
16
+
17
+ ### `<rel-resizable-panel>`
18
+ Individual resizable panel within a group.
19
+
20
+ **Attributes:**
21
+ - `defaultsize` (number): Initial size in percentage (optional)
22
+ - `minsize` (number): Minimum size in percentage (default: 10)
23
+ - `maxsize` (number): Maximum size in percentage (default: 90)
24
+
25
+ ### `<rel-resizable-handle>`
26
+ Draggable separator between panels.
27
+
28
+ **Features:**
29
+ - Pointer drag support (click and drag to resize)
30
+ - Keyboard navigation:
31
+ - Horizontal: ArrowLeft/ArrowRight (2% step, 5% with Shift)
32
+ - Vertical: ArrowUp/ArrowDown (2% step, 5% with Shift)
33
+ - Accessible with `role="separator"` and ARIA attributes
34
+ - Visual feedback on hover/focus/drag
35
+
36
+ ## Usage
37
+
38
+ ### Basic Horizontal Layout
39
+ ```html
40
+ <rel-resizable-group direction="horizontal">
41
+ <rel-resizable-panel defaultsize="50">
42
+ Left Panel
43
+ </rel-resizable-panel>
44
+ <rel-resizable-handle></rel-resizable-handle>
45
+ <rel-resizable-panel defaultsize="50">
46
+ Right Panel
47
+ </rel-resizable-panel>
48
+ </rel-resizable-group>
49
+ ```
50
+
51
+ ### Vertical Layout
52
+ ```html
53
+ <rel-resizable-group direction="vertical">
54
+ <rel-resizable-panel defaultsize="40">
55
+ Top Panel
56
+ </rel-resizable-panel>
57
+ <rel-resizable-handle></rel-resizable-handle>
58
+ <rel-resizable-panel defaultsize="60">
59
+ Bottom Panel
60
+ </rel-resizable-panel>
61
+ </rel-resizable-group>
62
+ ```
63
+
64
+ ### Nested Layout
65
+ ```html
66
+ <rel-resizable-group direction="horizontal">
67
+ <rel-resizable-panel defaultsize="33">
68
+ Left Sidebar
69
+ </rel-resizable-panel>
70
+ <rel-resizable-handle></rel-resizable-handle>
71
+ <rel-resizable-panel defaultsize="67">
72
+ <rel-resizable-group direction="vertical">
73
+ <rel-resizable-panel defaultsize="50">
74
+ Top Content
75
+ </rel-resizable-panel>
76
+ <rel-resizable-handle></rel-resizable-handle>
77
+ <rel-resizable-panel defaultsize="50">
78
+ Bottom Content
79
+ </rel-resizable-panel>
80
+ </rel-resizable-group>
81
+ </rel-resizable-panel>
82
+ </rel-resizable-group>
83
+ ```
84
+
85
+ ### With Constraints
86
+ ```html
87
+ <rel-resizable-group direction="horizontal">
88
+ <rel-resizable-panel defaultsize="30" minsize="20" maxsize="50">
89
+ Constrained Panel
90
+ </rel-resizable-panel>
91
+ <rel-resizable-handle></rel-resizable-handle>
92
+ <rel-resizable-panel defaultsize="70">
93
+ Main Panel
94
+ </rel-resizable-panel>
95
+ </rel-resizable-group>
96
+ ```
97
+
98
+ ## How It Works
99
+
100
+ ### Size Calculation
101
+ 1. **Initialization**: On first render, the group reads all direct child panels and handles
102
+ 2. **Default Sizes**: If panels specify `defaultsize`, those are used; otherwise space is distributed evenly
103
+ 3. **Normalization**: All sizes are normalized to total 100% across all panels
104
+ 4. **Percentage-Based**: Sizes are always in percentages, ensuring responsive layouts
105
+
106
+ ### Resizing Behavior
107
+ - **Pointer Drag**: Click and drag a handle to resize adjacent panels
108
+ - **Keyboard**: Focus a handle and use arrow keys (direction-aware)
109
+ - **Constraints**: Min/max sizes are enforced; total percentage is always preserved
110
+ - **Pair Adjustment**: Only the two panels adjacent to a handle are resized
111
+
112
+ ### Events
113
+ - Use `rel-resize` to track live resizing (useful for updating dependent content)
114
+ - Use `rel-resize-end` to react after resize completes (useful for saving preferences)
115
+
116
+ ## Accessibility
117
+
118
+ - Handles are keyboard focusable with `tabindex="0"`
119
+ - `role="separator"` with appropriate `aria-orientation`
120
+ - `aria-valuenow`, `aria-valuemin`, `aria-valuemax` reflect current split
121
+ - Visual focus indicators using design tokens
122
+ - Standard cursor changes (col-resize/row-resize)
123
+
124
+ ## Styling
125
+
126
+ All styling uses Reliability Design System tokens:
127
+ - Borders: `--rel-borders-width-stroke-100`
128
+ - Colors: `--rel-colors-gray-*`, `--rel-colors-blue-primary`, `--rel-colors-white`
129
+ - Spacing: `--rel-spacing-space-*`
130
+ - Border radius: `--rel-borders-radius-border-radius-*`
131
+
132
+ Panels have `overflow: auto` by default for scrollable content.
133
+
134
+ ## Limitations
135
+
136
+ 1. **Direct Children Only**: Panels and handles must be direct children of the group
137
+ 2. **Handle Placement**: Handles should be placed between panels in the expected order
138
+ 3. **No Collapse**: Panels cannot collapse to 0% (use min/max to control)
139
+ 4. **Single Direction**: Each group has one direction; nest groups for complex layouts
140
+ 5. **Manual Updates**: Group doesn't automatically observe DOM changes after initialization
@@ -0,0 +1,3 @@
1
+ export * from "./rel-resizable-group";
2
+ export * from "./rel-resizable-panel";
3
+ export * from "./rel-resizable-handle";
@@ -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
+ }