fvn-ui 0.1.0-alpha.1

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 (44) hide show
  1. package/README.md +57 -0
  2. package/package.json +63 -0
  3. package/src/fvn-ui/LLM.md +312 -0
  4. package/src/fvn-ui/components/avatar.css +53 -0
  5. package/src/fvn-ui/components/avatar.js +69 -0
  6. package/src/fvn-ui/components/button.css +143 -0
  7. package/src/fvn-ui/components/button.js +136 -0
  8. package/src/fvn-ui/components/card.css +6 -0
  9. package/src/fvn-ui/components/card.js +63 -0
  10. package/src/fvn-ui/components/checkbox.css +5 -0
  11. package/src/fvn-ui/components/checkbox.js +82 -0
  12. package/src/fvn-ui/components/collapsible.css +22 -0
  13. package/src/fvn-ui/components/collapsible.js +72 -0
  14. package/src/fvn-ui/components/confirm.js +109 -0
  15. package/src/fvn-ui/components/dashboard.css +25 -0
  16. package/src/fvn-ui/components/dashboard.js +130 -0
  17. package/src/fvn-ui/components/dialog.css +79 -0
  18. package/src/fvn-ui/components/dialog.js +302 -0
  19. package/src/fvn-ui/components/form.css +99 -0
  20. package/src/fvn-ui/components/image.css +21 -0
  21. package/src/fvn-ui/components/image.js +70 -0
  22. package/src/fvn-ui/components/index.js +73 -0
  23. package/src/fvn-ui/components/input.css +30 -0
  24. package/src/fvn-ui/components/input.js +81 -0
  25. package/src/fvn-ui/components/radio.css +3 -0
  26. package/src/fvn-ui/components/radio.js +99 -0
  27. package/src/fvn-ui/components/select.css +160 -0
  28. package/src/fvn-ui/components/select.js +366 -0
  29. package/src/fvn-ui/components/svg.css +5 -0
  30. package/src/fvn-ui/components/svg.js +85 -0
  31. package/src/fvn-ui/components/switch.css +34 -0
  32. package/src/fvn-ui/components/switch.js +85 -0
  33. package/src/fvn-ui/components/tabs.css +168 -0
  34. package/src/fvn-ui/components/tabs.js +181 -0
  35. package/src/fvn-ui/components/text.css +62 -0
  36. package/src/fvn-ui/components/text.js +105 -0
  37. package/src/fvn-ui/components/toggle.css +46 -0
  38. package/src/fvn-ui/components/toggle.js +60 -0
  39. package/src/fvn-ui/dom.js +495 -0
  40. package/src/fvn-ui/helpers.js +29 -0
  41. package/src/fvn-ui/index.js +53 -0
  42. package/src/fvn-ui/style.css +432 -0
  43. package/src/fvn-ui/template.js +135 -0
  44. package/src/fvn-ui/template.md +26 -0
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ ## fvn-ui
2
+ Minimal vanilla JS component library with layout helpers. Zero dependencies.
3
+
4
+ > **Note:** Requires a bundler that handles CSS imports (Vite, Webpack, Parcel, etc.)
5
+
6
+ ```js
7
+ import { layout, button, switchComponent } from 'fvn-ui'
8
+ ```
9
+
10
+ Or use the `ui` namespace for cleaner access (also avoids reserved words like `switch`):
11
+ ```js
12
+ import { ui } from 'fvn-ui'
13
+
14
+ ui.button({ label: 'Save' })
15
+ ui.switch({ label: 'Dark mode' })
16
+ ui.layout.row([ ... ])
17
+ ```
18
+
19
+ Tree-shakeable imports:
20
+ ```js
21
+ import 'fvn-ui/style.css'
22
+ import { button } from 'fvn-ui/button'
23
+ import { card } from 'fvn-ui/card'
24
+ ```
25
+
26
+ ### Components
27
+
28
+ | Component | Description |
29
+ |-----------|-------------|
30
+ | `button` | Buttons with variants, colors, icons |
31
+ | `card` | Container with title, description, content |
32
+ | `dialog` / `modal` / `tooltip` | Modal dialogs and popovers |
33
+ | `confirm` | Confirmation dialog with trigger button |
34
+ | `input` | Text input with label and validation |
35
+ | `checkbox` / `switch` / `toggle` | Boolean inputs (use `ui.switch()` or `switchComponent`) |
36
+ | `radio` | Radio button group |
37
+ | `select` | Dropdown with filter and multiselect (use `ui.select()` or `selectComponent`) |
38
+ | `tabs` | Tabbed content |
39
+ | `collapsible` | Expandable sections |
40
+ | `dashboard` | View management with navigation |
41
+ | `avatar` / `image` / `svg` | Media components |
42
+ | `label` | Text label |
43
+
44
+ ### Layout Helpers
45
+
46
+ All functions accept arguments in any order — parent element, config object, and children array are detected by type, not position.
47
+
48
+ ```js
49
+ import { el, row, col, layout } from 'fvn-ui'
50
+
51
+ layout.row({ gap: 4 }, [ button({ label: 'A' }), button({ label: 'B' }) ])
52
+ layout.col(parent, { gap: 2, align: 'center', children: [el('div', { onclick })] })
53
+ ```
54
+
55
+ ### Documentation
56
+
57
+ Each component has JSDoc with examples. See source files in `src/fvn-ui/components/` or [example page](https://fvn-dev.github.io/fvn-ui/).
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "fvn-ui",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Minimalist vanilla JS component library",
5
+ "type": "module",
6
+ "sideEffects": [
7
+ "**/*.css"
8
+ ],
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "main": "./src/fvn-ui/index.js",
13
+ "module": "./src/fvn-ui/index.js",
14
+ "exports": {
15
+ ".": "./src/fvn-ui/index.js",
16
+ "./style.css": "./src/fvn-ui/style.css",
17
+ "./button": "./src/fvn-ui/components/button.js",
18
+ "./card": "./src/fvn-ui/components/card.js",
19
+ "./dialog": "./src/fvn-ui/components/dialog.js",
20
+ "./input": "./src/fvn-ui/components/input.js",
21
+ "./select": "./src/fvn-ui/components/select.js",
22
+ "./checkbox": "./src/fvn-ui/components/checkbox.js",
23
+ "./switch": "./src/fvn-ui/components/switch.js",
24
+ "./toggle": "./src/fvn-ui/components/toggle.js",
25
+ "./tabs": "./src/fvn-ui/components/tabs.js",
26
+ "./avatar": "./src/fvn-ui/components/avatar.js",
27
+ "./collapsible": "./src/fvn-ui/components/collapsible.js",
28
+ "./confirm": "./src/fvn-ui/components/confirm.js",
29
+ "./dashboard": "./src/fvn-ui/components/dashboard.js",
30
+ "./image": "./src/fvn-ui/components/image.js",
31
+ "./label": "./src/fvn-ui/components/label.js",
32
+ "./radio": "./src/fvn-ui/components/radio.js",
33
+ "./svg": "./src/fvn-ui/components/svg.js",
34
+ "./dom": "./src/fvn-ui/dom.js"
35
+ },
36
+ "files": [
37
+ "src/fvn-ui"
38
+ ],
39
+ "keywords": [
40
+ "ui",
41
+ "components",
42
+ "vanilla-js"
43
+ ],
44
+ "license": "MIT",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "https://github.com/fvn-dev/fvn-ui"
48
+ },
49
+ "scripts": {
50
+ "dev": "vite",
51
+ "start": "vite",
52
+ "build": "vite build",
53
+ "doc": "documentation build src/fvn-ui/**/*.js -f json -o docs/api.json",
54
+ "lint": "eslint src/",
55
+ "lint:fix": "eslint src/ --fix"
56
+ },
57
+ "devDependencies": {
58
+ "documentation": "^14.0.3",
59
+ "eslint": "^9.0.0",
60
+ "sass": "^1.97.3",
61
+ "vite": "^7.2.4"
62
+ }
63
+ }
@@ -0,0 +1,312 @@
1
+ # fvn-ui — LLM Reference
2
+
3
+ A minimalist vanilla JS component library. No framework, no build complexity. Use existing CSS utility classes — avoid creating new CSS.
4
+
5
+ ## Philosophy
6
+
7
+ 1. **Use existing classes** — The library provides extensive utility classes. Compose with them.
8
+ 2. **Props become classes** — Shorthand props like `padding: 4` become `pad-4` classes automatically.
9
+ 3. **Flexible arguments** — Components accept args in any order: `(parent, config)` or `(config)` or `('text', config)`.
10
+ 4. **Return DOM elements** — All components return native HTMLElements with added methods.
11
+
12
+ ---
13
+
14
+ ## Import Styles
15
+
16
+ ```js
17
+ // Individual imports (tree-shakeable)
18
+ import { button, card, layout } from 'fvn-ui'
19
+
20
+ // Namespaced import (cleaner DX, avoids reserved words)
21
+ import { ui } from 'fvn-ui'
22
+
23
+ ui.button({ label: 'Save' })
24
+ ui.switch({ label: 'Dark mode' }) // vs switchComponent
25
+ ui.select({ options: [...] }) // vs selectComponent
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Core: `el(tag, config)`
31
+
32
+ Creates DOM elements. The foundation of everything.
33
+
34
+ ```js
35
+ el('div', { class: 'flex gap-2', text: 'Hello' })
36
+ el('button', { onClick: handler, disabled: true })
37
+ el('<h1>HTML string</h1>') // Parse HTML
38
+ ```
39
+
40
+ **Config options:**
41
+ - `class` — string | array | object (truthy keys become classes)
42
+ - `text` — safe textContent
43
+ - `html` — innerHTML (trusted only)
44
+ - `children` — array of elements/strings to append
45
+ - `data` — object for data-* attributes
46
+ - `style` — object for inline styles
47
+ - `on[Event]` — event handlers (onClick, onInput, etc.)
48
+ - `ref` — callback with element: `ref: (el) => myRef = el`
49
+
50
+ ---
51
+
52
+ ## Layout: `layout.row()` / `layout.col()`
53
+
54
+ Flexbox containers with sensible defaults.
55
+
56
+ ```js
57
+ layout.row([child1, child2]) // horizontal, gap-2
58
+ layout.row({ gap: 4, children: [child1, child2] }) // children as prop
59
+ layout.col({ justify: 'between' }, [...]) // children as separate arg
60
+ ```
61
+
62
+ **Props:** `gap`, `align`, `justify`, `padding`, `width`, `flex`, `children`
63
+
64
+ ---
65
+
66
+ ## Shorthand Props → Classes
67
+
68
+ These props are automatically converted to utility classes:
69
+
70
+ | Prop | Class | Example |
71
+ |------|-------|---------|
72
+ | `padding: 4` | `pad-4` | 4-unit padding |
73
+ | `gap: 2` | `gap-2` | 2-unit gap |
74
+ | `width: 'full'` | `w-full` | full width |
75
+ | `border: true` | `border` | standard border |
76
+ | `shade: true` | `shade` | shaded background |
77
+ | `small: true` | `small` | small text |
78
+ | `muted: true` | `muted` | muted text color |
79
+ | `flex: 1` | `flex-1` | flex-grow: 1 |
80
+
81
+ Use `props: false` to disable defaults (e.g., `border: false` on card).
82
+
83
+ ---
84
+
85
+ ## Components
86
+
87
+ ### `button({ label, variant, color, icon, shape })`
88
+
89
+ ```js
90
+ button({ label: 'Save', variant: 'primary' })
91
+ button({ label: 'Delete', color: 'red', icon: 'trash' })
92
+ button({ icon: 'settings', variant: 'ghost', shape: 'round' })
93
+ ```
94
+
95
+ | Prop | Values |
96
+ |------|--------|
97
+ | `label` / `text` | Button text |
98
+ | `variant` | `'default'` `'primary'` `'secondary'` `'outline'` `'ghost'` `'minimal'` |
99
+ | `color` | `'primary'` `'red'` `'green'` `'blue'` `'pink'` `'yellow'` `'orange'` |
100
+ | `shape` | `'round'` |
101
+ | `icon` | Icon name from svg.js |
102
+ | `size` | `'small'` `'medium'` `'large'` |
103
+ | `disabled` | boolean |
104
+
105
+ **Methods:** `btn.toggleLoading('Loading...')`, `btn.setLabel('Done', 3000)`
106
+
107
+ ---
108
+
109
+ ### `card({ title, description, content })`
110
+
111
+ ```js
112
+ card({ title: 'Settings', description: 'Configure your app', content: [...] })
113
+ card({ title: 'Note', content: 'Plain text content', border: false })
114
+ ```
115
+
116
+ | Prop | Description |
117
+ |------|-------------|
118
+ | `title` | Card header title |
119
+ | `description` | Subtitle text |
120
+ | `content` | string, element, array, or function |
121
+ | `border` | `false` to remove border (default: true) |
122
+ | `padding` | Override default padding |
123
+
124
+ ---
125
+
126
+ ### `input({ label, placeholder, onSubmit })`
127
+
128
+ ```js
129
+ input({ label: 'Email', placeholder: 'you@example.com' })
130
+ input({ label: 'Search', onSubmit: (value) => search(value) })
131
+ ```
132
+
133
+ | Prop | Description |
134
+ |------|-------------|
135
+ | `label` | Input label |
136
+ | `placeholder` | Placeholder text |
137
+ | `value` | Initial value |
138
+ | `size` | `'default'` `'large'` |
139
+ | `onSubmit` | Called on Enter key |
140
+
141
+ ---
142
+
143
+ ### `switchComponent({ label, checked, color, onChange })`
144
+
145
+ ```js
146
+ switchComponent({ label: 'Dark mode', checked: true })
147
+ switchComponent({ label: 'Notifications', color: 'primary', onChange: (v) => save(v) })
148
+ ```
149
+
150
+ ---
151
+
152
+ ### `checkbox({ label, checked, color, onChange })`
153
+
154
+ ```js
155
+ checkbox({ label: 'Accept terms', checked: false })
156
+ checkbox({ label: 'Premium', color: 'blue', checked: true })
157
+ ```
158
+
159
+ ---
160
+
161
+ ### `radio({ items, value, color, onChange })`
162
+
163
+ ```js
164
+ radio({
165
+ value: 'apple',
166
+ items: [
167
+ { value: 'apple', label: 'Apple' },
168
+ { value: 'banana', label: 'Banana' }
169
+ ]
170
+ })
171
+ ```
172
+
173
+ ---
174
+
175
+ ### `selectComponent({ options, value, placeholder, onChange })`
176
+
177
+ ```js
178
+ selectComponent({
179
+ label: 'Country',
180
+ placeholder: 'Select...',
181
+ options: [
182
+ { value: 'us', label: 'United States' },
183
+ { value: 'uk', label: 'United Kingdom' }
184
+ ]
185
+ })
186
+ ```
187
+
188
+ ---
189
+
190
+ ### `tabs({ items, variant, active })`
191
+
192
+ ```js
193
+ tabs({
194
+ variant: 'outline',
195
+ items: [
196
+ { label: 'Tab 1', render: () => content1 },
197
+ { label: 'Tab 2', render: () => content2 }
198
+ ]
199
+ })
200
+ ```
201
+
202
+ | Prop | Values |
203
+ |------|--------|
204
+ | `variant` | `'default'` `'outline'` `'border'` `'minimal'` `'ghost'` |
205
+ | `active` | Index of initially active tab |
206
+ | `color` | Tab color |
207
+ | `shape` | `'round'` |
208
+
209
+ ---
210
+
211
+ ### `dialog({ content, variant })` / `modal()` / `tooltip()`
212
+
213
+ ```js
214
+ // Programmatic modal
215
+ modal({ open: clickEvent, content: card({ title: 'Confirm' }) })
216
+
217
+ // Hover tooltip
218
+ tooltip({ open: mouseEvent, content: 'Tooltip text', inverted: true })
219
+
220
+ // Pre-built confirm dialog
221
+ confirm({
222
+ label: 'Delete',
223
+ title: 'Are you sure?',
224
+ description: 'This cannot be undone',
225
+ confirm: 'Delete',
226
+ cancel: 'Cancel',
227
+ onConfirm: () => handleDelete()
228
+ })
229
+ ```
230
+
231
+ | Prop | Description |
232
+ |------|-------------|
233
+ | `open` / `toggled` | Event or element to trigger |
234
+ | `content` | Dialog content |
235
+ | `variant` / `type` | `'modal'` `'tooltip'` |
236
+ | `position` | `'top'` `'bottom'` `'left'` `'right'` |
237
+ | `inverted` | Dark background |
238
+
239
+ ---
240
+
241
+ ### `avatar({ name, src, description, color, size, variant })`
242
+
243
+ ```js
244
+ avatar({ name: 'John Doe', description: 'Admin', color: 'blue' })
245
+ avatar({ src: 'photo.jpg', name: 'Jane', size: 'large', variant: 'square' })
246
+ ```
247
+
248
+ ---
249
+
250
+ ### `collapsible({ label, content, open })`
251
+
252
+ ```js
253
+ collapsible({
254
+ label: 'Advanced Settings',
255
+ content: card({ title: 'Settings' })
256
+ })
257
+ ```
258
+
259
+ ---
260
+
261
+ ### `label(text, { small, muted })`
262
+
263
+ ```js
264
+ label('Section Title')
265
+ label('Helper text', { small: true, muted: true })
266
+ ```
267
+
268
+ ---
269
+
270
+ ### `image({ src, alt, lazy })`
271
+
272
+ ```js
273
+ image({ src: 'photo.jpg', alt: 'Description' }) // lazy-loads by default
274
+ ```
275
+
276
+ ---
277
+
278
+ ## Available Utility Classes
279
+
280
+ ### Layout
281
+ `flex`, `flex-col`, `flex-1`, `gap-{1-10}`, `align-{start|center|end|stretch}`, `justify-{start|center|end|between}`
282
+
283
+ ### Spacing
284
+ `pad-{1-10}`, `margin-{1-10}`, `block-{1-10}`, `inline-{1-10}`
285
+
286
+ ### Width
287
+ `w-full`, `w-auto`, `w-{1-10}`
288
+
289
+ ### Text
290
+ `small`, `muted`, `bold`
291
+
292
+ ### Visual
293
+ `border`, `border-{color}`, `shade`, `rounded`
294
+
295
+ ---
296
+
297
+ ## Colors
298
+
299
+ Available color tokens: `primary`, `red`, `green`, `blue`, `pink`, `yellow`, `orange`
300
+
301
+ Use with `color` prop on components, or as CSS variables: `var(--primary)`, `var(--red)`, etc.
302
+
303
+ ---
304
+
305
+ ## Best Practices for LLMs
306
+
307
+ 1. **Don't create new CSS** — Use utility classes and component props
308
+ 2. **Use layout helpers** — `layout.row([...])` and `layout.col([...])` for structure
309
+ 3. **Compose components** — Nest cards, buttons, inputs as children
310
+ 4. **Use shorthand props** — `padding: 4` not `class: 'pad-4'`
311
+ 5. **Return from render functions** — Tab/collapsible content uses `render: () => element`
312
+ 6. **Handle events** — Use `onClick`, `onChange`, `onSubmit` props
@@ -0,0 +1,53 @@
1
+ .ui-avatar {
2
+ --size: calc(3.25 * var(--core));
3
+ display: inline-flex;
4
+ align-items: center;
5
+ gap: var(--space-2);
6
+ font-size: calc(.3 * var(--size));;
7
+ }
8
+
9
+ .ui-avatar--round {
10
+ & .ui-avatar__box {
11
+ border-radius: 50%;
12
+ }
13
+ }
14
+
15
+ .ui-avatar--medium {
16
+ --size: calc(3.75 * var(--core));
17
+ }
18
+
19
+ .ui-avatar--large {
20
+ --size: calc(4.5 * var(--core));
21
+ }
22
+
23
+ .ui-avatar__box {
24
+ width: var(--size);
25
+ height: var(--size);
26
+ border-radius: var(--radius);
27
+ background: var(--bg, var(--hover));
28
+ color: var(--fg, var(--text));
29
+ display: flex;
30
+ align-items: center;
31
+ justify-content: center;
32
+ overflow: hidden;
33
+ flex-shrink: 0;
34
+ }
35
+
36
+ .ui-avatar__image {
37
+ width: 100%;
38
+ height: 100%;
39
+ }
40
+
41
+ .ui-avatar__fallback {
42
+ font-size: calc(var(--size) * .4);
43
+ font-weight: 500;
44
+ text-transform: uppercase;
45
+ letter-spacing: .02em;
46
+ }
47
+
48
+ .ui-avatar__info {
49
+ display: flex;
50
+ flex-direction: column;
51
+ gap: calc(.025 * var(--size));
52
+ min-width: 0;
53
+ }
@@ -0,0 +1,69 @@
1
+ import { el, parseArgs, configToClasses, bemFactory } from '../dom.js'
2
+ import { image } from './image.js'
3
+ import './text.css'
4
+ import './avatar.css'
5
+
6
+ const bem = bemFactory('avatar');
7
+
8
+ const getInitials = (name) => {
9
+ if (!name) {
10
+ return '';
11
+ }
12
+ const parts = name.trim().split(/\s+/);
13
+ return parts.length > 1
14
+ ? (parts[0][0] + parts.at(-1)[0]).toUpperCase()
15
+ : name.slice(0, 2).toUpperCase();
16
+ };
17
+
18
+ /**
19
+ * Creates an avatar with image or initials fallback
20
+ * @param {Object} config
21
+ * @param {string} [config.src] - Image URL
22
+ * @param {string} [config.name] - Name (used for initials fallback and alt text)
23
+ * @param {string} [config.description] - Description text below name
24
+ * @param {'round'|'square'} [config.variant='round'] - Avatar shape
25
+ * @param {'small'|'medium'|'large'} [config.size='small'] - Avatar size
26
+ * @param {'primary'|'red'|'green'|'blue'|'pink'} [config.color] - Background color for initials
27
+ * @param {string} [config.id] - Registers to dom.avatar[id] and dom[id]
28
+ * @returns {HTMLDivElement} Avatar element
29
+ * @example
30
+ * avatar({ name: 'John Doe', description: 'Admin', color: 'blue' })
31
+ * avatar({ src: 'photo.jpg', name: 'Jane', size: 'large', variant: 'square' })
32
+ */
33
+ export function avatar(...args) {
34
+ const {
35
+ parent,
36
+ src,
37
+ name,
38
+ description,
39
+ variant = 'round',
40
+ size = 'small',
41
+ color,
42
+ props,
43
+ ...rest
44
+ } = parseArgs(...args);
45
+
46
+ const initials = getInitials(name);
47
+
48
+ return el('div', parent, {
49
+ ...rest,
50
+ class: [bem(), variant !== 'square' && bem('round'), size && bem(size), configToClasses(props), rest.class],
51
+ children: [
52
+ el('div', {
53
+ class: bem.el('box'),
54
+ data: { uiCol: color || (src ? null : 'primary') },
55
+ children: [
56
+ src ? image({ src, alt: name || '', class: bem.el('image') }) : null,
57
+ !src && initials && el('span', { class: bem.el('fallback'), text: initials })
58
+ ]
59
+ }),
60
+ (name || description) && el('div', {
61
+ class: bem.el('info'),
62
+ children: [
63
+ name && el('span', { class: 'ui-name', text: name }),
64
+ description && el('span', { class: 'ui-subtitle', text: description })
65
+ ]
66
+ })
67
+ ]
68
+ });
69
+ }
@@ -0,0 +1,143 @@
1
+ .ui-btn {
2
+ --bg: transparent;
3
+ --fg: var(--text);
4
+ border: 1px solid transparent;
5
+ background: var(--bg);
6
+ color: var(--fg);
7
+ border-radius: var(--radius);
8
+ padding: var(--input-padding);
9
+ cursor: pointer;
10
+ user-select: none;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ gap: var(--space-2);
15
+ position: relative;
16
+ white-space: nowrap;
17
+ font: inherit;
18
+ font-weight: 500;
19
+ font-size: .9em;
20
+ transition: var(--core-transition);
21
+ }
22
+ .ui-btn > div {
23
+ position: relative;
24
+ z-index: 1;
25
+ pointer-events: none;
26
+ }
27
+ .ui-btn--muted {
28
+ color: var(--muted);
29
+ }
30
+ [aria-expanded="true"] .ui-btn--muted,
31
+ :focus-visible + .ui-btn--muted {
32
+ color: var(--fg);
33
+ }
34
+ .ui-btn:hover {
35
+ color: var(--fg);
36
+ }
37
+ .ui-btn--sub {
38
+ --bg: transparent;
39
+ }
40
+ .ui-btn--round {
41
+ --input-padding: calc(var(--space) * .75) calc(var(--space) * 1.75);
42
+ border-radius: 999px;
43
+ gap: var(--space-3);
44
+ }
45
+ .ui-btn--square {
46
+ --input-padding: calc(var(--space) * .75);
47
+ aspect-ratio: 1;
48
+ }
49
+ .ui-btn--stripped {
50
+ --input-padding: 0;
51
+ }
52
+ .ui-btn--minimal {
53
+ --bg: transparent;
54
+ --fg: var(--text);
55
+ &:before {
56
+ content: '';
57
+ background: var(--fg);
58
+ opacity: var(--border-opacity, .25);
59
+ position: absolute;
60
+ width: calc(100% - (2 * var(--space)));
61
+ height: 1px;
62
+ top: 50%;
63
+ transform: translatey(.65em);
64
+ }
65
+ &:hover:before {
66
+ opacity: 1;
67
+ }
68
+ }
69
+ .ui-btn--minimal.ui-col-sub:not(:hover):before {
70
+ opacity: var(--border-opacity-sub, .25);
71
+ }
72
+ .ui-btn--hover:hover {
73
+ background: var(--hover);
74
+ border-color: transparent;
75
+ }
76
+ .ui-btn--hover-sub:hover {
77
+ --fg: var(--inverted, var(--back));
78
+ --bg: var(--hover);
79
+ }
80
+ .ui-btn--filled:after {
81
+ content: '';
82
+ position: absolute;
83
+ inset: -1px;
84
+ border-radius: inherit;
85
+ transition: opacity var(--core-transition);
86
+ background: var(--back);
87
+ opacity: 0;
88
+ pointer-events: none;
89
+ }
90
+ .ui-btn--filled:hover:after {
91
+ opacity: .2;
92
+ }
93
+ .ui-btn:disabled {
94
+ opacity: .5;
95
+ cursor: not-allowed;
96
+ }
97
+ .ui-btn--outline {
98
+ border-color: var(--border);
99
+ }
100
+ .ui-btn__icon {
101
+ position: relative;
102
+ width: var(--icon-size, var(--icon-size-default));
103
+ display: flex;
104
+ align-items: center;
105
+ justify-content: center;
106
+ }
107
+ .ui-btn__icon > svg {
108
+ position: absolute;
109
+ }
110
+ .ui-btn__icon > span {
111
+ position: relative;
112
+ }
113
+ .ui-btn__icon > span > svg {
114
+ position: absolute;
115
+ top: 50%;
116
+ left: 50%;
117
+ transform: translate(-50%, -50%);
118
+ }
119
+
120
+ .ui-btn.loading {
121
+ --icon-size: var(--core);
122
+
123
+ flex-direction: row-reverse;
124
+ pointer-events: none;
125
+ position: relative;
126
+ padding-left: calc(3.5 * var(--space));
127
+
128
+ &::before {
129
+ content: '';
130
+ width: var(--icon-size, var(--icon-size-default));
131
+ height: var(--icon-size, var(--icon-size-default));
132
+ border: 1px solid var(--fg);
133
+ border-radius: 50%;
134
+ border-color: var(--fg) var(--fg) var(--fg) transparent;
135
+ animation: loadingSpin 1s linear infinite;
136
+ position: absolute;
137
+ left: var(--space);
138
+ }
139
+ }
140
+
141
+ @keyframes loadingSpin {
142
+ 100% { transform: rotate(359deg); }
143
+ }