@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.
Files changed (147) hide show
  1. package/Component/Component.js +598 -0
  2. package/Component/Component.scenarios.js +88 -0
  3. package/Component/Component.test.js +717 -0
  4. package/Component/README.md +455 -0
  5. package/Component/index.js +3 -0
  6. package/Component/observeElementConnection.js +52 -0
  7. package/Component/observeElementConnection.test.js +121 -0
  8. package/Elem/Elem.js +304 -0
  9. package/Elem/Elem.test.js +679 -0
  10. package/Elem/README.md +373 -0
  11. package/Elem/index.js +1 -0
  12. package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
  13. package/LICENSE +21 -0
  14. package/README.md +413 -0
  15. package/components/BottomSheet/BottomSheet.js +192 -0
  16. package/components/BottomSheet/BottomSheet.lld.md +25 -0
  17. package/components/BottomSheet/README.md +66 -0
  18. package/components/BottomSheet/index.js +1 -0
  19. package/components/Button/Button.js +53 -0
  20. package/components/Button/Button.lld.md +21 -0
  21. package/components/Button/index.js +1 -0
  22. package/components/Calendar/Calendar.js +720 -0
  23. package/components/Calendar/Calendar.lld.md +22 -0
  24. package/components/Calendar/CalendarEvent.js +102 -0
  25. package/components/Calendar/Toolbar.js +78 -0
  26. package/components/Calendar/index.js +2 -0
  27. package/components/Calendar/utils.js +56 -0
  28. package/components/Code/Code.js +84 -0
  29. package/components/Code/Code.lld.md +21 -0
  30. package/components/Code/index.js +1 -0
  31. package/components/ColorPicker/ColorPicker.js +445 -0
  32. package/components/ColorPicker/ColorPicker.lld.md +21 -0
  33. package/components/ColorPicker/index.js +1 -0
  34. package/components/ColorPicker/svg.js +5 -0
  35. package/components/Dialog/Dialog.js +278 -0
  36. package/components/Dialog/Dialog.lld.md +20 -0
  37. package/components/Dialog/README.md +96 -0
  38. package/components/Dialog/index.js +1 -0
  39. package/components/Form/Form.js +257 -0
  40. package/components/Form/Form.lld.md +21 -0
  41. package/components/Form/README.md +87 -0
  42. package/components/Form/index.js +1 -0
  43. package/components/Icon/Icon.js +54 -0
  44. package/components/Icon/Icon.lld.md +21 -0
  45. package/components/Icon/index.js +1 -0
  46. package/components/Input/Input.js +173 -0
  47. package/components/Input/Input.lld.md +28 -0
  48. package/components/Input/README.md +97 -0
  49. package/components/Input/index.js +2 -0
  50. package/components/Input/utils.js +122 -0
  51. package/components/Keyboard/Key.js +38 -0
  52. package/components/Keyboard/Keyboard.js +173 -0
  53. package/components/Keyboard/Keyboard.lld.md +21 -0
  54. package/components/Keyboard/index.js +1 -0
  55. package/components/Label/Label.js +214 -0
  56. package/components/Label/Label.lld.md +20 -0
  57. package/components/Label/index.js +1 -0
  58. package/components/Link/Link.js +43 -0
  59. package/components/Link/Link.lld.md +15 -0
  60. package/components/Link/index.js +1 -0
  61. package/components/List/List.js +82 -0
  62. package/components/List/List.lld.md +19 -0
  63. package/components/List/index.js +1 -0
  64. package/components/Menu/Menu.js +93 -0
  65. package/components/Menu/Menu.lld.md +15 -0
  66. package/components/Menu/index.js +1 -0
  67. package/components/Notify/Notify.js +96 -0
  68. package/components/Notify/Notify.lld.md +20 -0
  69. package/components/Notify/index.js +1 -0
  70. package/components/Page/Page.js +67 -0
  71. package/components/Page/Page.lld.md +20 -0
  72. package/components/Page/index.js +1 -0
  73. package/components/Popover/Popover.js +175 -0
  74. package/components/Popover/Popover.lld.md +19 -0
  75. package/components/Popover/index.js +1 -0
  76. package/components/RadioButton/RadioButton.js +108 -0
  77. package/components/RadioButton/RadioButton.lld.md +15 -0
  78. package/components/RadioButton/index.js +1 -0
  79. package/components/Router/README.md +160 -0
  80. package/components/Router/Router.js +150 -0
  81. package/components/Router/Router.lld.md +31 -0
  82. package/components/Router/View.js +15 -0
  83. package/components/Router/index.js +2 -0
  84. package/components/Router/utils.js +17 -0
  85. package/components/Select/README.md +88 -0
  86. package/components/Select/Select.js +74 -0
  87. package/components/Select/Select.lld.md +20 -0
  88. package/components/Select/index.js +1 -0
  89. package/components/Table/README.md +94 -0
  90. package/components/Table/Table.js +171 -0
  91. package/components/Table/Table.lld.md +21 -0
  92. package/components/Table/index.js +1 -0
  93. package/components/TagList/Tag.js +84 -0
  94. package/components/TagList/TagList.js +118 -0
  95. package/components/TagList/TagList.lld.md +30 -0
  96. package/components/TagList/design.excalidraw.png +0 -0
  97. package/components/TagList/index.js +2 -0
  98. package/components/Tooltip/Tooltip.js +139 -0
  99. package/components/Tooltip/Tooltip.lld.md +22 -0
  100. package/components/Tooltip/index.js +1 -0
  101. package/components/TooltipWrapper/TooltipWrapper.js +89 -0
  102. package/components/TooltipWrapper/TooltipWrapper.lld.md +21 -0
  103. package/components/TooltipWrapper/index.js +1 -0
  104. package/components/Whiteboard/Whiteboard.js +198 -0
  105. package/components/Whiteboard/Whiteboard.lld.md +35 -0
  106. package/components/Whiteboard/index.js +1 -0
  107. package/components/index.js +27 -0
  108. package/eslint.config.cjs +118 -0
  109. package/index.d.ts +635 -0
  110. package/index.js +19 -0
  111. package/package.json +123 -0
  112. package/plugins/asText.js +38 -0
  113. package/plugins/loadPlugins.js +5 -0
  114. package/plugins/markdownLoader.js +121 -0
  115. package/prettier.config.cjs +7 -0
  116. package/spellcheck.config.cjs +227 -0
  117. package/styled/README.md +329 -0
  118. package/styled/appendStyles.js +26 -0
  119. package/styled/appendStyles.test.js +45 -0
  120. package/styled/index.js +4 -0
  121. package/styled/shimCSS.js +31 -0
  122. package/styled/shimCSS.test.js +103 -0
  123. package/styled/styled.js +91 -0
  124. package/styled/styled.test.js +586 -0
  125. package/styled/themeStyles.js +36 -0
  126. package/styled/themeStyles.test.js +135 -0
  127. package/test-setup.js +123 -0
  128. package/theme/.test.js +69 -0
  129. package/theme/README.md +607 -0
  130. package/theme/button.js +100 -0
  131. package/theme/code.js +123 -0
  132. package/theme/colors.js +42 -0
  133. package/theme/fonts.js +42 -0
  134. package/theme/index.js +33 -0
  135. package/theme/input.js +64 -0
  136. package/theme/page.js +208 -0
  137. package/theme/scrollbar.js +24 -0
  138. package/theme/table.js +53 -0
  139. package/utils/README.md +176 -0
  140. package/utils/browser.js +92 -0
  141. package/utils/class.js +30 -0
  142. package/utils/color.js +81 -0
  143. package/utils/data.js +164 -0
  144. package/utils/element.js +55 -0
  145. package/utils/index.js +7 -0
  146. package/utils/rand.js +12 -0
  147. 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';