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.
- package/README.md +57 -0
- package/package.json +63 -0
- package/src/fvn-ui/LLM.md +312 -0
- package/src/fvn-ui/components/avatar.css +53 -0
- package/src/fvn-ui/components/avatar.js +69 -0
- package/src/fvn-ui/components/button.css +143 -0
- package/src/fvn-ui/components/button.js +136 -0
- package/src/fvn-ui/components/card.css +6 -0
- package/src/fvn-ui/components/card.js +63 -0
- package/src/fvn-ui/components/checkbox.css +5 -0
- package/src/fvn-ui/components/checkbox.js +82 -0
- package/src/fvn-ui/components/collapsible.css +22 -0
- package/src/fvn-ui/components/collapsible.js +72 -0
- package/src/fvn-ui/components/confirm.js +109 -0
- package/src/fvn-ui/components/dashboard.css +25 -0
- package/src/fvn-ui/components/dashboard.js +130 -0
- package/src/fvn-ui/components/dialog.css +79 -0
- package/src/fvn-ui/components/dialog.js +302 -0
- package/src/fvn-ui/components/form.css +99 -0
- package/src/fvn-ui/components/image.css +21 -0
- package/src/fvn-ui/components/image.js +70 -0
- package/src/fvn-ui/components/index.js +73 -0
- package/src/fvn-ui/components/input.css +30 -0
- package/src/fvn-ui/components/input.js +81 -0
- package/src/fvn-ui/components/radio.css +3 -0
- package/src/fvn-ui/components/radio.js +99 -0
- package/src/fvn-ui/components/select.css +160 -0
- package/src/fvn-ui/components/select.js +366 -0
- package/src/fvn-ui/components/svg.css +5 -0
- package/src/fvn-ui/components/svg.js +85 -0
- package/src/fvn-ui/components/switch.css +34 -0
- package/src/fvn-ui/components/switch.js +85 -0
- package/src/fvn-ui/components/tabs.css +168 -0
- package/src/fvn-ui/components/tabs.js +181 -0
- package/src/fvn-ui/components/text.css +62 -0
- package/src/fvn-ui/components/text.js +105 -0
- package/src/fvn-ui/components/toggle.css +46 -0
- package/src/fvn-ui/components/toggle.js +60 -0
- package/src/fvn-ui/dom.js +495 -0
- package/src/fvn-ui/helpers.js +29 -0
- package/src/fvn-ui/index.js +53 -0
- package/src/fvn-ui/style.css +432 -0
- package/src/fvn-ui/template.js +135 -0
- 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
|
+
}
|