@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.
- package/dist/components/rel-accordion/README.md +132 -0
- package/dist/components/rel-accordion/rel-accordion.ts +43 -16
- package/dist/components/rel-alert/README.md +141 -0
- package/dist/components/rel-alert/rel-alert.ts +18 -7
- package/dist/components/rel-avatar/README.md +119 -0
- package/dist/components/rel-badge/README.md +86 -0
- package/dist/components/rel-badge/rel-badge.ts +6 -6
- package/dist/components/rel-breadcrumb/README.md +102 -0
- package/dist/components/rel-breadcrumb/rel-breadcrumb.ts +12 -6
- package/dist/components/rel-button/README.md +130 -0
- 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 +12 -3
- package/dist/components/rel-cardmedia/README.md +127 -0
- package/dist/components/rel-cardmedia/rel-cardmedia.ts +14 -4
- 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 +18 -10
- package/dist/components/rel-chip/README.md +114 -0
- package/dist/components/rel-chip/rel-chip.ts +20 -4
- package/dist/components/rel-container/README.md +99 -0
- 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-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-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 +32 -5
- 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 +13 -5
- 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 +67 -2
- package/dist/components/rel-stepper/README.md +160 -0
- package/dist/components/rel-stepper/rel-stepper.ts +6 -6
- package/dist/components/rel-switch/README.md +102 -0
- package/dist/components/rel-switch/rel-switch.ts +15 -6
- package/dist/components/rel-tab/README.md +163 -0
- package/dist/components/rel-tab/rel-tab.ts +36 -14
- 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 +35 -19
- 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 +836 -1
- package/dist/index.d.ts +836 -1
- package/dist/index.js +10658 -1493
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10616 -1499
- package/dist/index.mjs.map +1 -1
- package/dist/styles/vars.css +293 -183
- package/dist/{vars-6ZIUPIU4.css → vars-ITL2K3KO.css} +293 -183
- 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-
|
|
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-
|
|
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,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
|
+
}
|