@vanilla-bean/components 1.0.0
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/Component/Component.js +598 -0
- package/Component/Component.scenarios.js +88 -0
- package/Component/Component.test.js +717 -0
- package/Component/README.md +455 -0
- package/Component/index.js +3 -0
- package/Component/observeElementConnection.js +52 -0
- package/Component/observeElementConnection.test.js +121 -0
- package/Elem/Elem.js +304 -0
- package/Elem/Elem.test.js +679 -0
- package/Elem/README.md +373 -0
- package/Elem/index.js +1 -0
- package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
- package/LICENSE +21 -0
- package/README.md +413 -0
- package/components/BottomSheet/BottomSheet.js +192 -0
- package/components/BottomSheet/BottomSheet.lld.md +25 -0
- package/components/BottomSheet/README.md +66 -0
- package/components/BottomSheet/index.js +1 -0
- package/components/Button/Button.js +53 -0
- package/components/Button/Button.lld.md +21 -0
- package/components/Button/index.js +1 -0
- package/components/Calendar/Calendar.js +720 -0
- package/components/Calendar/Calendar.lld.md +22 -0
- package/components/Calendar/CalendarEvent.js +102 -0
- package/components/Calendar/Toolbar.js +78 -0
- package/components/Calendar/index.js +2 -0
- package/components/Calendar/utils.js +56 -0
- package/components/Code/Code.js +84 -0
- package/components/Code/Code.lld.md +21 -0
- package/components/Code/index.js +1 -0
- package/components/ColorPicker/ColorPicker.js +445 -0
- package/components/ColorPicker/ColorPicker.lld.md +21 -0
- package/components/ColorPicker/index.js +1 -0
- package/components/ColorPicker/svg.js +5 -0
- package/components/Dialog/Dialog.js +278 -0
- package/components/Dialog/Dialog.lld.md +20 -0
- package/components/Dialog/README.md +96 -0
- package/components/Dialog/index.js +1 -0
- package/components/Form/Form.js +257 -0
- package/components/Form/Form.lld.md +21 -0
- package/components/Form/README.md +87 -0
- package/components/Form/index.js +1 -0
- package/components/Icon/Icon.js +54 -0
- package/components/Icon/Icon.lld.md +21 -0
- package/components/Icon/index.js +1 -0
- package/components/Input/Input.js +173 -0
- package/components/Input/Input.lld.md +28 -0
- package/components/Input/README.md +97 -0
- package/components/Input/index.js +2 -0
- package/components/Input/utils.js +122 -0
- package/components/Keyboard/Key.js +38 -0
- package/components/Keyboard/Keyboard.js +173 -0
- package/components/Keyboard/Keyboard.lld.md +21 -0
- package/components/Keyboard/index.js +1 -0
- package/components/Label/Label.js +214 -0
- package/components/Label/Label.lld.md +20 -0
- package/components/Label/index.js +1 -0
- package/components/Link/Link.js +43 -0
- package/components/Link/Link.lld.md +15 -0
- package/components/Link/index.js +1 -0
- package/components/List/List.js +82 -0
- package/components/List/List.lld.md +19 -0
- package/components/List/index.js +1 -0
- package/components/Menu/Menu.js +93 -0
- package/components/Menu/Menu.lld.md +15 -0
- package/components/Menu/index.js +1 -0
- package/components/Notify/Notify.js +96 -0
- package/components/Notify/Notify.lld.md +20 -0
- package/components/Notify/index.js +1 -0
- package/components/Page/Page.js +67 -0
- package/components/Page/Page.lld.md +20 -0
- package/components/Page/index.js +1 -0
- package/components/Popover/Popover.js +175 -0
- package/components/Popover/Popover.lld.md +19 -0
- package/components/Popover/index.js +1 -0
- package/components/RadioButton/RadioButton.js +108 -0
- package/components/RadioButton/RadioButton.lld.md +15 -0
- package/components/RadioButton/index.js +1 -0
- package/components/Router/README.md +160 -0
- package/components/Router/Router.js +150 -0
- package/components/Router/Router.lld.md +31 -0
- package/components/Router/View.js +15 -0
- package/components/Router/index.js +2 -0
- package/components/Router/utils.js +17 -0
- package/components/Select/README.md +88 -0
- package/components/Select/Select.js +74 -0
- package/components/Select/Select.lld.md +20 -0
- package/components/Select/index.js +1 -0
- package/components/Table/README.md +94 -0
- package/components/Table/Table.js +171 -0
- package/components/Table/Table.lld.md +21 -0
- package/components/Table/index.js +1 -0
- package/components/TagList/Tag.js +84 -0
- package/components/TagList/TagList.js +118 -0
- package/components/TagList/TagList.lld.md +30 -0
- package/components/TagList/design.excalidraw.png +0 -0
- package/components/TagList/index.js +2 -0
- package/components/Tooltip/Tooltip.js +139 -0
- package/components/Tooltip/Tooltip.lld.md +22 -0
- package/components/Tooltip/index.js +1 -0
- package/components/TooltipWrapper/TooltipWrapper.js +89 -0
- package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
- package/components/TooltipWrapper/index.js +1 -0
- package/components/Whiteboard/Whiteboard.js +198 -0
- package/components/Whiteboard/Whiteboard.lld.md +35 -0
- package/components/Whiteboard/index.js +1 -0
- package/components/index.js +27 -0
- package/eslint.config.cjs +118 -0
- package/index.d.ts +635 -0
- package/index.js +19 -0
- package/package.json +123 -0
- package/plugins/asText.js +38 -0
- package/plugins/loadPlugins.js +5 -0
- package/plugins/markdownLoader.js +121 -0
- package/prettier.config.cjs +7 -0
- package/spellcheck.config.cjs +227 -0
- package/styled/README.md +329 -0
- package/styled/appendStyles.js +26 -0
- package/styled/appendStyles.test.js +45 -0
- package/styled/index.js +4 -0
- package/styled/shimCSS.js +31 -0
- package/styled/shimCSS.test.js +103 -0
- package/styled/styled.js +91 -0
- package/styled/styled.test.js +586 -0
- package/styled/themeStyles.js +36 -0
- package/styled/themeStyles.test.js +135 -0
- package/test-setup.js +123 -0
- package/theme/.test.js +69 -0
- package/theme/README.md +607 -0
- package/theme/button.js +100 -0
- package/theme/code.js +123 -0
- package/theme/colors.js +42 -0
- package/theme/fonts.js +42 -0
- package/theme/index.js +33 -0
- package/theme/input.js +64 -0
- package/theme/page.js +208 -0
- package/theme/scrollbar.js +24 -0
- package/theme/table.js +53 -0
- package/utils/README.md +176 -0
- package/utils/browser.js +92 -0
- package/utils/class.js +30 -0
- package/utils/color.js +81 -0
- package/utils/data.js +164 -0
- package/utils/element.js +55 -0
- package/utils/index.js +7 -0
- package/utils/rand.js +12 -0
- package/utils/string.js +72 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { tactileResponse } from '../../utils';
|
|
2
|
+
import { styled } from '../../styled';
|
|
3
|
+
import { Component } from '../../Component';
|
|
4
|
+
import Key from './Key';
|
|
5
|
+
|
|
6
|
+
const KeyRow = styled(
|
|
7
|
+
Component,
|
|
8
|
+
() => `
|
|
9
|
+
display: flex;
|
|
10
|
+
`,
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Virtual keyboard component with customizable layouts and tactile feedback.
|
|
15
|
+
*
|
|
16
|
+
* Provides touch-friendly keyboard interface with predefined layouts (simple, number)
|
|
17
|
+
* and configurable key definitions. Supports layout switching and key event handling.
|
|
18
|
+
* @param {object} [options={}] - Keyboard configuration options
|
|
19
|
+
* @param {string} [options.layout='simple'] - Initial keyboard layout name
|
|
20
|
+
* @param {boolean} [options.tactileResponse=true] - Whether to provide haptic feedback on key press
|
|
21
|
+
* @param {object} [options.keyDefinitions] - Key configuration object mapping key names to definitions
|
|
22
|
+
* @param {object} [options.layouts] - Layout configuration object mapping layout names to key arrays
|
|
23
|
+
* @param {...(Component|HTMLElement|string)} children - Child elements to append
|
|
24
|
+
* @returns {Keyboard} Keyboard component instance
|
|
25
|
+
*/
|
|
26
|
+
class Keyboard extends Component {
|
|
27
|
+
constructor(options = {}, ...children) {
|
|
28
|
+
super(
|
|
29
|
+
{
|
|
30
|
+
layout: 'simple',
|
|
31
|
+
tactileResponse: true,
|
|
32
|
+
keyDefinitions: {
|
|
33
|
+
simple: { text: 'ABC' },
|
|
34
|
+
number: { text: '123' },
|
|
35
|
+
shift: { class: 'u1_5' },
|
|
36
|
+
return: { class: 'u1_5' },
|
|
37
|
+
backspace: { class: 'u1_5' },
|
|
38
|
+
clear: { class: 'u1_5' },
|
|
39
|
+
done: { class: 'u1_5' },
|
|
40
|
+
exponent: { key: 'e', text: 'e', class: 'u1_5' },
|
|
41
|
+
space: { key: ' ', text: ' ', class: 'u6' },
|
|
42
|
+
},
|
|
43
|
+
layouts: {
|
|
44
|
+
simple: [
|
|
45
|
+
['simple', 'number'],
|
|
46
|
+
['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'],
|
|
47
|
+
['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'],
|
|
48
|
+
['shift', 'z', 'x', 'c', 'v', 'b', 'n', 'm', 'backspace'],
|
|
49
|
+
[',', '.', 'space', '!', 'return'],
|
|
50
|
+
],
|
|
51
|
+
number: [
|
|
52
|
+
['simple', 'number'],
|
|
53
|
+
['1', '2', '3', 'backspace'],
|
|
54
|
+
['4', '5', '6', 'clear'],
|
|
55
|
+
['7', '8', '9', 'done'],
|
|
56
|
+
['.', '0', '-', 'exponent'],
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
...options,
|
|
60
|
+
},
|
|
61
|
+
...children,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
this.setLayout(this.options.layout);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static handlers = {
|
|
68
|
+
// Claim these keys so standard routing doesn't try to assign them as elem properties.
|
|
69
|
+
// Both are read directly from this.options in setLayout() and key resolution.
|
|
70
|
+
layouts() {},
|
|
71
|
+
keyDefinitions() {},
|
|
72
|
+
layout(value) {
|
|
73
|
+
this.setLayout(value);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// todo native keyboard events https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Switches the keyboard to the specified layout and rebuilds the keys.
|
|
81
|
+
* @param {string} name - Layout name to switch to
|
|
82
|
+
*/
|
|
83
|
+
setLayout(name) {
|
|
84
|
+
const rows = this.options.layouts[name];
|
|
85
|
+
|
|
86
|
+
if (typeof name !== 'string' || !rows) return;
|
|
87
|
+
|
|
88
|
+
this.children.forEach(child => child.destroy?.());
|
|
89
|
+
this.empty();
|
|
90
|
+
this.removeClass(/\blayout-\S+\b/g).addClass(`layout-${name}`);
|
|
91
|
+
|
|
92
|
+
rows.forEach(columns => {
|
|
93
|
+
const row = new KeyRow({ appendTo: this });
|
|
94
|
+
|
|
95
|
+
columns.forEach(key => {
|
|
96
|
+
let keyDefinition = this.options.keyDefinitions[key];
|
|
97
|
+
|
|
98
|
+
if (!keyDefinition) {
|
|
99
|
+
Object.entries(this.options.keyDefinitions).forEach(([keyName, definition]) => {
|
|
100
|
+
if (!keyName.startsWith('^') || !keyName.endsWith('$')) return;
|
|
101
|
+
|
|
102
|
+
if (new RegExp(keyName, 'i').test(key)) keyDefinition = definition;
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
new Key({
|
|
107
|
+
key,
|
|
108
|
+
...keyDefinition,
|
|
109
|
+
appendTo: row,
|
|
110
|
+
onPointerDown: event => {
|
|
111
|
+
if (this.options.tactileResponse) tactileResponse();
|
|
112
|
+
|
|
113
|
+
this.emit('keyDown', {
|
|
114
|
+
event,
|
|
115
|
+
key: keyDefinition?.key || key,
|
|
116
|
+
keyDefinition,
|
|
117
|
+
target: event.target?._component,
|
|
118
|
+
});
|
|
119
|
+
},
|
|
120
|
+
onPointerUp: event =>
|
|
121
|
+
this.emit('keyUp', {
|
|
122
|
+
event,
|
|
123
|
+
key: keyDefinition?.key || key,
|
|
124
|
+
keyDefinition,
|
|
125
|
+
target: event.target?._component,
|
|
126
|
+
}),
|
|
127
|
+
onPointerPress: event =>
|
|
128
|
+
this.emit('keyPress', {
|
|
129
|
+
event,
|
|
130
|
+
key: keyDefinition?.key || key,
|
|
131
|
+
keyDefinition,
|
|
132
|
+
target: event.target?._component,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Registers a callback for key down events.
|
|
141
|
+
* @param {Function} callback - Event handler function
|
|
142
|
+
* @returns {Function} Unsubscribe function
|
|
143
|
+
*/
|
|
144
|
+
onKeyDown(callback) {
|
|
145
|
+
this.addEventListener('keyDown', callback);
|
|
146
|
+
|
|
147
|
+
return () => this.removeEventListener('keyDown', callback);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Registers a callback for key up events.
|
|
152
|
+
* @param {Function} callback - Event handler function
|
|
153
|
+
* @returns {Function} Unsubscribe function
|
|
154
|
+
*/
|
|
155
|
+
onKeyUp(callback) {
|
|
156
|
+
this.addEventListener('keyUp', callback);
|
|
157
|
+
|
|
158
|
+
return () => this.removeEventListener('keyUp', callback);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Registers a callback for key press events.
|
|
163
|
+
* @param {Function} callback - Event handler function
|
|
164
|
+
* @returns {Function} Unsubscribe function
|
|
165
|
+
*/
|
|
166
|
+
onKeyPress(callback) {
|
|
167
|
+
this.addEventListener('keyPress', callback);
|
|
168
|
+
|
|
169
|
+
return () => this.removeEventListener('keyPress', callback);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export default Keyboard;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Keyboard
|
|
2
|
+
|
|
3
|
+
> ./Keyboard.js
|
|
4
|
+
|
|
5
|
+
On-screen keyboard where layout switching rebuilds the key DOM rather than showing/hiding rows. The decision: a clean rebuild on layout change is safer than managing per-key visibility across layout transitions; no key from one layout can bleed into another.
|
|
6
|
+
|
|
7
|
+
## Layout switch produces only keys from the new layout — no residual keys remain
|
|
8
|
+
|
|
9
|
+
- changing `layout` removes all existing keys and builds the new set from scratch; a key that exists in layout A but not layout B is definitively absent after the switch
|
|
10
|
+
- does switching layout remove keys from the previous layout?
|
|
11
|
+
- does the new layout's keys appear after switching?
|
|
12
|
+
|
|
13
|
+
## Key events carry the key definition alongside the key name
|
|
14
|
+
|
|
15
|
+
- `keyDown`, `keyUp`, and `keyPress` emit with both the key name and its configuration object, so handlers can respond to semantic meaning rather than just the character pressed
|
|
16
|
+
- does a key press event include the definition object for that key?
|
|
17
|
+
|
|
18
|
+
## Regex-named keys match families of keys to one definition
|
|
19
|
+
|
|
20
|
+
- a key definition whose name is a regex pattern matches all physical keys that satisfy it; modifier handling and key families are expressed as patterns, not enumerated one by one
|
|
21
|
+
- does a key matching a regex definition trigger that definition's behavior?
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Keyboard } from './Keyboard';
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { styled } from '../../styled';
|
|
2
|
+
import { Component } from '../../Component';
|
|
3
|
+
import { TooltipWrapper } from '../TooltipWrapper';
|
|
4
|
+
|
|
5
|
+
const StyledLabel = styled(
|
|
6
|
+
TooltipWrapper,
|
|
7
|
+
({ colors, fonts }) => `
|
|
8
|
+
position: relative;
|
|
9
|
+
display: inline-block;
|
|
10
|
+
font-size: 1em;
|
|
11
|
+
width: calc(100% - 24px);
|
|
12
|
+
margin: 0 0 6px;
|
|
13
|
+
padding: 6px 12px 6px 12px;
|
|
14
|
+
background-color: ${colors.white.setAlpha(0.04)};
|
|
15
|
+
color: ${colors.white};
|
|
16
|
+
transition: all 0.5s;
|
|
17
|
+
|
|
18
|
+
&:after {
|
|
19
|
+
transition: all 0.5s;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
& label {
|
|
23
|
+
display: block;
|
|
24
|
+
margin: 0;
|
|
25
|
+
color: ${colors.white};
|
|
26
|
+
padding-left: 3px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&.variant-overlay {
|
|
30
|
+
& label {
|
|
31
|
+
position: relative;
|
|
32
|
+
z-index: 1;
|
|
33
|
+
transform: translate(0, 0);
|
|
34
|
+
transition: transform 0.5s, color 0.5s;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
& input {
|
|
38
|
+
transform: translate(0, 0);
|
|
39
|
+
transition: transform 0.5s;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
&.variant-overlay:not(:focus-within):has(input:placeholder-shown) {
|
|
44
|
+
& label {
|
|
45
|
+
pointer-events: none;
|
|
46
|
+
color: ${colors.gray};
|
|
47
|
+
transform: translate(12px, 20px);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
& input {
|
|
51
|
+
transform: translate(0, -12px);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
&.variant-inline, &.variant-inline-after {
|
|
56
|
+
display: flex;
|
|
57
|
+
|
|
58
|
+
& label {
|
|
59
|
+
display: inline-block;
|
|
60
|
+
vertical-align: top;
|
|
61
|
+
margin-top: 5px;
|
|
62
|
+
margin-right: 9px;
|
|
63
|
+
line-height: 2;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
& input:not([type=checkbox]), & textarea, & select {
|
|
67
|
+
flex: 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
&.variant-inline-after {
|
|
72
|
+
& label {
|
|
73
|
+
margin-left: 9px;
|
|
74
|
+
margin-right: 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
&.variant-collapsible {
|
|
79
|
+
border: 2px solid ${colors.lighter(colors.teal).setAlpha(0.5)};
|
|
80
|
+
border-radius: 3px;
|
|
81
|
+
|
|
82
|
+
&.collapsed {
|
|
83
|
+
width: 50%;
|
|
84
|
+
|
|
85
|
+
& > label {
|
|
86
|
+
color: ${colors.superWhite};
|
|
87
|
+
|
|
88
|
+
&:before {
|
|
89
|
+
content: "" !important;
|
|
90
|
+
opacity: 1;
|
|
91
|
+
color: ${colors.lightest(colors.blue)};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
& > *:not(label) {
|
|
96
|
+
display: none !important;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
& > label {
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
|
|
103
|
+
&:before {
|
|
104
|
+
${fonts.fontAwesomeSolid}
|
|
105
|
+
|
|
106
|
+
content: "";
|
|
107
|
+
opacity: 0.5;
|
|
108
|
+
font-size: 14px;
|
|
109
|
+
padding-right: 6px;
|
|
110
|
+
color: ${colors.white};
|
|
111
|
+
|
|
112
|
+
transition: opacity 0.8s, color 1s;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
& > *:not(label):not(.tooltip) {
|
|
118
|
+
margin-top: 6px;
|
|
119
|
+
}
|
|
120
|
+
`,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const variant_enum = Object.freeze(['overlay', 'collapsible', 'inline', 'inline-after', 'simple']);
|
|
124
|
+
|
|
125
|
+
const defaultOptions = { variant: 'simple' };
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Label component with multiple display variants and input association.
|
|
129
|
+
*
|
|
130
|
+
* Provides flexible label styling with support for overlay, collapsible, inline, and simple variants.
|
|
131
|
+
* Automatically associates with input components and supports interactive collapsible behavior.
|
|
132
|
+
* @param {object|string} [options={}] - Label configuration options, or label text string
|
|
133
|
+
* @param {('overlay'|'collapsible'|'inline'|'inline-after'|'simple')} [options.variant='simple'] - Label display variant
|
|
134
|
+
* @param {string|object} [options.label] - Label text content or label component options
|
|
135
|
+
* @param {Component|string} [options.for] - Input component or ID to associate label with
|
|
136
|
+
* @param {boolean} [options.collapsed] - Whether collapsible variant starts collapsed
|
|
137
|
+
* @param {string} [options.tooltip] - Tooltip text for the label
|
|
138
|
+
* @param {...(Component|HTMLElement|string)} children - Child elements, typically input components
|
|
139
|
+
* @returns {Label} Label component instance
|
|
140
|
+
*/
|
|
141
|
+
class Label extends StyledLabel {
|
|
142
|
+
defaultOptions = { ...super.defaultOptions, ...defaultOptions };
|
|
143
|
+
variant_enum = variant_enum;
|
|
144
|
+
|
|
145
|
+
constructor(options = {}, ...children) {
|
|
146
|
+
if (typeof options === 'string') options = { label: options };
|
|
147
|
+
|
|
148
|
+
super(
|
|
149
|
+
{
|
|
150
|
+
...(children.length > 0 && { for: children[0] }),
|
|
151
|
+
...defaultOptions,
|
|
152
|
+
...options,
|
|
153
|
+
},
|
|
154
|
+
...children,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
build() {
|
|
159
|
+
this._labelText = new Component({ tag: 'label' });
|
|
160
|
+
|
|
161
|
+
const handleClick = () => {
|
|
162
|
+
if (this.options.variant !== 'collapsible') return;
|
|
163
|
+
const nowCollapsed = !this.hasClass('collapsed');
|
|
164
|
+
this[nowCollapsed ? 'addClass' : 'removeClass']('collapsed');
|
|
165
|
+
this._labelText.elem.setAttribute('aria-expanded', nowCollapsed ? 'false' : 'true');
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
this._labelText.elem.addEventListener('click', handleClick);
|
|
169
|
+
this.addCleanup('labelClick', () => this._labelText.elem.removeEventListener('click', handleClick));
|
|
170
|
+
|
|
171
|
+
if (this.options.variant === 'collapsible') {
|
|
172
|
+
this._labelText.elem.setAttribute('aria-expanded', this.options.collapsed ? 'false' : 'true');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this[this.options.variant === 'inline-after' ? 'append' : 'prepend'](this._labelText);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static handlers = {
|
|
179
|
+
label(value) {
|
|
180
|
+
if (typeof value === 'object') this._labelText?.setOptions(value);
|
|
181
|
+
else if (this._labelText) this._labelText.options.content = value;
|
|
182
|
+
},
|
|
183
|
+
collapsed(value) {
|
|
184
|
+
this[value ? 'addClass' : 'removeClass']('collapsed');
|
|
185
|
+
if (this.options.variant === 'collapsible' && this._labelText) {
|
|
186
|
+
this._labelText.elem.setAttribute('aria-expanded', value ? 'false' : 'true');
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
variant(value) {
|
|
190
|
+
this.removeClass(/\bvariant-\S+\b/g);
|
|
191
|
+
this.addClass(`variant-${value}`);
|
|
192
|
+
|
|
193
|
+
if (this._labelText) {
|
|
194
|
+
const shouldBeAfter = value === 'inline-after';
|
|
195
|
+
const isAfter = this._labelText.elem === this.elem.lastElementChild;
|
|
196
|
+
if (shouldBeAfter !== isAfter) this[shouldBeAfter ? 'append' : 'prepend'](this._labelText);
|
|
197
|
+
if (value === 'collapsible') {
|
|
198
|
+
this._labelText.elem.setAttribute('aria-expanded', this.hasClass('collapsed') ? 'false' : 'true');
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
for(value) {
|
|
203
|
+
let forId = typeof value === 'string' ? value : value?.id || value?.elem?.id;
|
|
204
|
+
|
|
205
|
+
if (!forId && value?._component) {
|
|
206
|
+
forId = value.elem.id = value.uniqueId;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (this._labelText) this._labelText.elem.htmlFor = forId ?? '';
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export default Label;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Label
|
|
2
|
+
|
|
3
|
+
> ./Label.js
|
|
4
|
+
|
|
5
|
+
Labeling wrapper with five structural variants. The key decision is that `variant` selects a DOM structure, not just a CSS class; each variant builds a different composition of elements suited to its use case.
|
|
6
|
+
|
|
7
|
+
## Each variant applies a distinct class that drives its CSS behavior
|
|
8
|
+
|
|
9
|
+
- 'collapsible', 'overlay', 'inline', 'inline-after', and 'simple' each apply their own class to the component element; swapping `variant` changes the class and the structural CSS rules that attach to it
|
|
10
|
+
- does the 'collapsible' variant produce a clickable element that shows and hides content?
|
|
11
|
+
|
|
12
|
+
## Collapsed state is externally controllable, not just toggle-driven
|
|
13
|
+
|
|
14
|
+
- `collapsed: true` sets the initial collapsed state; assigning to it later expands or collapses programmatically without simulating user interaction
|
|
15
|
+
- does setting collapsed: true hide the wrapped content?
|
|
16
|
+
- does setting collapsed: false after construction show the previously hidden content?
|
|
17
|
+
|
|
18
|
+
## Overlay label visibility is driven by CSS pseudo-class, not JavaScript
|
|
19
|
+
|
|
20
|
+
- the overlay variant's label visibility responds to whether the input has content via the `:placeholder-shown` pseudo-class; the component adds the structural class and the CSS handles the rest
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Label } from './Label';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { TooltipWrapper } from '../TooltipWrapper';
|
|
2
|
+
|
|
3
|
+
const defaultOptions = {
|
|
4
|
+
tag: 'a',
|
|
5
|
+
variant: 'link',
|
|
6
|
+
get tooltip() {
|
|
7
|
+
return { icon: 'link', style: { fontSize: '12px' } };
|
|
8
|
+
},
|
|
9
|
+
};
|
|
10
|
+
const variant_enum = Object.freeze(['link', 'button']);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Link component with tooltip support and display variants.
|
|
14
|
+
*
|
|
15
|
+
* Extends TooltipWrapper to provide enhanced anchor elements with automatic tooltip icons
|
|
16
|
+
* and configurable styling variants for different use cases.
|
|
17
|
+
* @param {object} [options={}] - Link configuration options
|
|
18
|
+
* @param {string} [options.tag='a'] - HTML tag, defaults to anchor element
|
|
19
|
+
* @param {string} [options.variant='link'] - Link display variant ('link', 'button')
|
|
20
|
+
* @param {string} [options.href] - URL to link to
|
|
21
|
+
* @param {string} [options.target] - Link target attribute
|
|
22
|
+
* @param {string} [options.textContent] - Link text content
|
|
23
|
+
* @param {object|string} [options.tooltip] - Tooltip configuration or text
|
|
24
|
+
* @param {...(Component|HTMLElement|string)} children - Child elements to append
|
|
25
|
+
* @returns {Link} Link component instance
|
|
26
|
+
*/
|
|
27
|
+
class Link extends TooltipWrapper {
|
|
28
|
+
defaultOptions = { ...super.defaultOptions, ...defaultOptions };
|
|
29
|
+
variant_enum = variant_enum;
|
|
30
|
+
|
|
31
|
+
constructor(options = {}, ...children) {
|
|
32
|
+
super({ ...defaultOptions, ...options }, ...children);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
static handlers = {
|
|
36
|
+
variant(value) {
|
|
37
|
+
this.removeClass(/\bvariant-\S+\b/g);
|
|
38
|
+
this.addClass(`variant-${value}`);
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default Link;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Link
|
|
2
|
+
|
|
3
|
+
> ./Link.js
|
|
4
|
+
|
|
5
|
+
Anchor element with optional button styling. The decision: `variant: 'button'` changes Link's appearance but not its element; it remains an anchor, preserving right-click, middle-click, open-in-tab, and keyboard semantics that a styled div cannot provide.
|
|
6
|
+
|
|
7
|
+
## Button variant changes appearance without surrendering anchor semantics
|
|
8
|
+
|
|
9
|
+
- a Link with `variant: 'button'` looks like a button but behaves like a link; it is still an anchor element, which is what provides right-click menus, keyboard focus, and tab semantics natively
|
|
10
|
+
- does a Link with variant 'button' remain an anchor element?
|
|
11
|
+
|
|
12
|
+
## Tooltip includes a link icon automatically
|
|
13
|
+
|
|
14
|
+
- when a tooltip string is provided, the tooltip is configured with a link icon alongside the text
|
|
15
|
+
- does a Link's tooltip show a link icon alongside the provided tooltip text?
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as Link } from './Link';
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { Component } from '../../Component';
|
|
2
|
+
import { styled } from '../../styled';
|
|
3
|
+
import { TooltipWrapper } from '../TooltipWrapper';
|
|
4
|
+
|
|
5
|
+
const StyledComponent = styled(
|
|
6
|
+
Component,
|
|
7
|
+
() => `
|
|
8
|
+
margin: 6px 0;
|
|
9
|
+
padding-left: 32px;
|
|
10
|
+
|
|
11
|
+
&.no-style {
|
|
12
|
+
padding-left: 0;
|
|
13
|
+
list-style: none;
|
|
14
|
+
|
|
15
|
+
& li {
|
|
16
|
+
line-height: 1;
|
|
17
|
+
text-indent: 0;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
`,
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const ListItem = styled(
|
|
24
|
+
TooltipWrapper,
|
|
25
|
+
() => `
|
|
26
|
+
line-height: 1.3;
|
|
27
|
+
text-indent: 6px;
|
|
28
|
+
`,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* List component for rendering unordered lists with customizable list items.
|
|
33
|
+
*
|
|
34
|
+
* Provides dynamic list rendering with support for custom ListItemComponent classes
|
|
35
|
+
* and flexible item configuration. Automatically wraps items in styled list item elements.
|
|
36
|
+
* @param {object} [options={}] - List configuration options
|
|
37
|
+
* @param {string} [options.tag='ul'] - HTML tag for the list container
|
|
38
|
+
* @param {Array<*>} [options.items] - Array of list items to render
|
|
39
|
+
* @param {Component} [options.ListItemComponent] - Custom component class for rendering items
|
|
40
|
+
* @param {boolean} [options.noStyle] - Whether to apply no-style class for plain list
|
|
41
|
+
* @param {...(Component|HTMLElement|string)} children - Child elements to append
|
|
42
|
+
* @returns {List} List component instance
|
|
43
|
+
*/
|
|
44
|
+
export default class List extends StyledComponent {
|
|
45
|
+
constructor(options = {}, ...children) {
|
|
46
|
+
super(
|
|
47
|
+
{
|
|
48
|
+
...options,
|
|
49
|
+
tag: 'ul',
|
|
50
|
+
},
|
|
51
|
+
...children,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static handlers = {
|
|
56
|
+
noStyle(value) {
|
|
57
|
+
this.toggleClass('no-style', !!value);
|
|
58
|
+
},
|
|
59
|
+
items(value) {
|
|
60
|
+
if (!value) {
|
|
61
|
+
this.empty();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
this.content(
|
|
66
|
+
value.map(item => {
|
|
67
|
+
const isContent = typeof item === 'string' || item?.elem || Array.isArray(item);
|
|
68
|
+
const listItem = new ListItem({ tag: 'li', ...item?.listItemOptions });
|
|
69
|
+
|
|
70
|
+
if (this.options.ListItemComponent) {
|
|
71
|
+
listItem.content(new this.options.ListItemComponent(isContent ? { content: item } : item));
|
|
72
|
+
} else if (item.ListItemComponent) {
|
|
73
|
+
listItem.content(new item.ListItemComponent(item));
|
|
74
|
+
} else if (isContent) listItem.content(item);
|
|
75
|
+
else listItem.setOptions(item);
|
|
76
|
+
|
|
77
|
+
return listItem;
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# List
|
|
2
|
+
|
|
3
|
+
> ./List.js
|
|
4
|
+
|
|
5
|
+
Flexible list that meets items where they are. The core design decision: items can be strings, DOM nodes, component instances, or configuration objects; the component figures out what to do with each format. Callers don't normalize data before passing it.
|
|
6
|
+
|
|
7
|
+
## Items render correctly regardless of their input format
|
|
8
|
+
|
|
9
|
+
- a single `items` array can contain mixed formats without error
|
|
10
|
+
- does a List render a mix of string and object items without failing?
|
|
11
|
+
|
|
12
|
+
## Per-item component overrides compose cleanly with the global default
|
|
13
|
+
|
|
14
|
+
- the component set globally on the list applies to all items; a per-item `ListItemComponent` overrides for that item only
|
|
15
|
+
- does a per-item ListItemComponent render that item differently from the default?
|
|
16
|
+
|
|
17
|
+
## noStyle opts out of default list chrome for embedding contexts
|
|
18
|
+
|
|
19
|
+
- `noStyle: true` removes bullets, padding, and line-height defaults via a CSS class so the embedding layout can control spacing
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as List } from './List';
|