boongle-white-black-components 1.1.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/README.md +88 -0
- package/package.json +38 -0
- package/src/components/alert.js +39 -0
- package/src/components/avatar.js +19 -0
- package/src/components/badge.js +10 -0
- package/src/components/button.js +43 -0
- package/src/components/card.js +32 -0
- package/src/components/checkbox.js +26 -0
- package/src/components/chip.js +36 -0
- package/src/components/codeBlock.js +24 -0
- package/src/components/divider.js +12 -0
- package/src/components/dropdown.js +88 -0
- package/src/components/icon.js +28 -0
- package/src/components/input.js +30 -0
- package/src/components/linkButton.js +43 -0
- package/src/components/modal.js +49 -0
- package/src/components/progress.js +23 -0
- package/src/components/radio.js +30 -0
- package/src/components/spinner.js +11 -0
- package/src/components/switch.js +27 -0
- package/src/components/tabs.js +58 -0
- package/src/components/text.js +32 -0
- package/src/components/textarea.js +25 -0
- package/src/components/tooltip.js +14 -0
- package/src/icons/icons-data.js +83 -0
- package/src/icons/registry.js +26 -0
- package/src/index.js +45 -0
- package/src/styles/boongle.css +630 -0
- package/src/theme.js +47 -0
- package/src/utils/dom.js +30 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# boongle-white-black-components
|
|
2
|
+
|
|
3
|
+
Ultra-clean **black & white only** HTML/CSS/JS components with **light** and **dark** themes. **80+ icons** via `boongle:icon-name`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install boongle-white-black-components
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
import {
|
|
15
|
+
init,
|
|
16
|
+
Button,
|
|
17
|
+
Icon,
|
|
18
|
+
Input,
|
|
19
|
+
} from 'boongle-white-black-components';
|
|
20
|
+
|
|
21
|
+
// CSS (pick one)
|
|
22
|
+
import 'boongle-white-black-components/styles/boongle.css';
|
|
23
|
+
// or: init() injects styles in the browser
|
|
24
|
+
|
|
25
|
+
init({ theme: 'dark' });
|
|
26
|
+
|
|
27
|
+
document.body.append(
|
|
28
|
+
Button({ label: 'Save', size: 'md', theme: 'dark', icon: 'boongle:check' })
|
|
29
|
+
);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Components (21)
|
|
33
|
+
|
|
34
|
+
| Component | Notes |
|
|
35
|
+
|-----------|--------|
|
|
36
|
+
| Button | `variant`: solid, outline, ghost |
|
|
37
|
+
| Text | Google Sans, `muted`, `bold` |
|
|
38
|
+
| CodeBlock | Monospace pre/code |
|
|
39
|
+
| Dropdown | `options`, `onChange` |
|
|
40
|
+
| Card | `title`, `body`, `children` |
|
|
41
|
+
| LinkButton | `href`, `external` |
|
|
42
|
+
| Icon | `boongle:name` |
|
|
43
|
+
| Input | text, email, password… |
|
|
44
|
+
| Textarea | resizable |
|
|
45
|
+
| Checkbox | labeled |
|
|
46
|
+
| Radio | grouped by `name` |
|
|
47
|
+
| Switch | toggle |
|
|
48
|
+
| Badge | solid / outline |
|
|
49
|
+
| Alert | icon + optional close |
|
|
50
|
+
| Divider | optional label |
|
|
51
|
+
| Spinner | loading |
|
|
52
|
+
| Avatar | initials or image |
|
|
53
|
+
| Tabs | `tabs: [{ id, label, content }]` |
|
|
54
|
+
| Modal | dialog overlay |
|
|
55
|
+
| Progress | 0–100 bar |
|
|
56
|
+
| Chip | tag with optional remove |
|
|
57
|
+
| Tooltip | wrap any child |
|
|
58
|
+
|
|
59
|
+
Every component accepts: `size` (`sm` \| `md` \| `lg`), `theme` (`light` \| `dark`), `className`.
|
|
60
|
+
|
|
61
|
+
## Icons (80+)
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
Icon({ icon: 'boongle:star' });
|
|
65
|
+
Icon({ icon: 'boongle:settings' });
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Run `listIcons()` for all names. Register custom:
|
|
69
|
+
|
|
70
|
+
```javascript
|
|
71
|
+
import { registerIcon } from 'boongle-white-black-components';
|
|
72
|
+
registerIcon('spark', '<svg viewBox="0 0 24 24">…</svg>');
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Local demo
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cd example
|
|
79
|
+
npm install
|
|
80
|
+
npm start
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Open http://localhost:3847 — uses the same import path as npm (`boongle-white-black-components`).
|
|
84
|
+
|
|
85
|
+
## Docs
|
|
86
|
+
|
|
87
|
+
- **[INFO.md](./INFO.md)** — all components, props, icons, publish steps
|
|
88
|
+
- [PUBLISH.md](./PUBLISH.md) — npm login & publish only
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "boongle-white-black-components",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Ultra-clean black & white HTML/CSS/JS components with light & dark themes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"module": "./src/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.js",
|
|
11
|
+
"default": "./src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./styles": "./src/styles/boongle.css",
|
|
14
|
+
"./styles/boongle.css": "./src/styles/boongle.css",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"components",
|
|
23
|
+
"ui",
|
|
24
|
+
"black-white",
|
|
25
|
+
"boongle",
|
|
26
|
+
"vanilla",
|
|
27
|
+
"design-system"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/yourusername/boongle-white-black-components.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/yourusername/boongle-white-black-components/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/yourusername/boongle-white-black-components#readme"
|
|
38
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
import { Icon } from './icon.js';
|
|
4
|
+
|
|
5
|
+
export function Alert(props = {}) {
|
|
6
|
+
const {
|
|
7
|
+
title,
|
|
8
|
+
message = '',
|
|
9
|
+
icon = 'boongle:info',
|
|
10
|
+
size = 'md',
|
|
11
|
+
theme = 'light',
|
|
12
|
+
className = '',
|
|
13
|
+
onClose,
|
|
14
|
+
} = props;
|
|
15
|
+
|
|
16
|
+
const root = el('div', {
|
|
17
|
+
className: boongleClasses('boongle-alert', { size, theme, className }),
|
|
18
|
+
role: 'alert',
|
|
19
|
+
}, [
|
|
20
|
+
Icon({ icon, size: 'sm', theme }),
|
|
21
|
+
el('div', { className: 'boongle-alert__content' }, [
|
|
22
|
+
title ? el('strong', { className: 'boongle-alert__title' }, [title]) : null,
|
|
23
|
+
el('p', { className: 'boongle-alert__message' }, [message]),
|
|
24
|
+
].filter(Boolean)),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
if (onClose) {
|
|
28
|
+
const closeBtn = el('button', {
|
|
29
|
+
type: 'button',
|
|
30
|
+
className: 'boongle-alert__close',
|
|
31
|
+
'aria-label': 'Dismiss',
|
|
32
|
+
onClick: onClose,
|
|
33
|
+
});
|
|
34
|
+
closeBtn.appendChild(Icon({ icon: 'boongle:close', size: 'sm', theme }));
|
|
35
|
+
root.appendChild(closeBtn);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return root;
|
|
39
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
|
|
4
|
+
export function Avatar(props = {}) {
|
|
5
|
+
const { initials = '?', src, alt = '', size = 'md', theme = 'light', className = '' } = props;
|
|
6
|
+
|
|
7
|
+
if (src) {
|
|
8
|
+
return el('img', {
|
|
9
|
+
className: boongleClasses('boongle-avatar', { size, theme, className }),
|
|
10
|
+
src,
|
|
11
|
+
alt,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return el('span', {
|
|
16
|
+
className: boongleClasses('boongle-avatar', { size, theme, className }),
|
|
17
|
+
'aria-label': alt || initials,
|
|
18
|
+
}, [initials.slice(0, 2).toUpperCase()]);
|
|
19
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
|
|
4
|
+
export function Badge(props = {}) {
|
|
5
|
+
const { label = '', size = 'md', theme = 'light', variant = 'solid', className = '' } = props;
|
|
6
|
+
const variantClass = variant === 'outline' ? 'boongle-badge--outline' : '';
|
|
7
|
+
return el('span', {
|
|
8
|
+
className: `${boongleClasses('boongle-badge', { size, theme, className })} ${variantClass}`.trim(),
|
|
9
|
+
}, [label]);
|
|
10
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
import { Icon } from './icon.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {object} props
|
|
7
|
+
* @param {string} [props.label]
|
|
8
|
+
* @param {'sm'|'md'|'lg'} [props.size='md']
|
|
9
|
+
* @param {'light'|'dark'} [props.theme='light']
|
|
10
|
+
* @param {'solid'|'outline'|'ghost'} [props.variant='solid']
|
|
11
|
+
* @param {string} [props.icon] - e.g. "boongle:star"
|
|
12
|
+
* @param {boolean} [props.disabled]
|
|
13
|
+
* @param {string} [props.type='button']
|
|
14
|
+
* @param {string} [props.className]
|
|
15
|
+
* @param {() => void} [props.onClick]
|
|
16
|
+
*/
|
|
17
|
+
export function Button(props = {}) {
|
|
18
|
+
const {
|
|
19
|
+
label = '',
|
|
20
|
+
size = 'md',
|
|
21
|
+
theme = 'light',
|
|
22
|
+
variant = 'solid',
|
|
23
|
+
icon,
|
|
24
|
+
disabled = false,
|
|
25
|
+
type = 'button',
|
|
26
|
+
className = '',
|
|
27
|
+
onClick,
|
|
28
|
+
} = props;
|
|
29
|
+
|
|
30
|
+
const variantClass =
|
|
31
|
+
variant === 'outline' ? 'boongle-btn--outline' : variant === 'ghost' ? 'boongle-btn--ghost' : '';
|
|
32
|
+
|
|
33
|
+
const children = [];
|
|
34
|
+
if (icon) children.push(Icon({ icon, size: size === 'lg' ? 'md' : 'sm', theme }));
|
|
35
|
+
if (label) children.push(label);
|
|
36
|
+
|
|
37
|
+
return el('button', {
|
|
38
|
+
type,
|
|
39
|
+
className: `${boongleClasses('boongle-btn', { size, theme, className })} ${variantClass}`.trim(),
|
|
40
|
+
disabled: disabled ? '' : undefined,
|
|
41
|
+
onClick,
|
|
42
|
+
}, children);
|
|
43
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {object} props
|
|
6
|
+
* @param {string} [props.title]
|
|
7
|
+
* @param {string} [props.body]
|
|
8
|
+
* @param {HTMLElement|Node|string} [props.children]
|
|
9
|
+
* @param {'sm'|'md'|'lg'} [props.size='md']
|
|
10
|
+
* @param {'light'|'dark'} [props.theme='light']
|
|
11
|
+
* @param {string} [props.className]
|
|
12
|
+
*/
|
|
13
|
+
export function Card(props = {}) {
|
|
14
|
+
const { title, body, children, size = 'md', theme = 'light', className = '' } = props;
|
|
15
|
+
|
|
16
|
+
const card = el('article', {
|
|
17
|
+
className: boongleClasses('boongle-card', { size, theme, className }),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if (title) {
|
|
21
|
+
card.appendChild(el('h3', { className: 'boongle-card__title' }, [title]));
|
|
22
|
+
}
|
|
23
|
+
if (body) {
|
|
24
|
+
card.appendChild(el('p', { className: 'boongle-card__body' }, [body]));
|
|
25
|
+
}
|
|
26
|
+
if (children) {
|
|
27
|
+
if (typeof children === 'string') card.appendChild(document.createTextNode(children));
|
|
28
|
+
else card.appendChild(children);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return card;
|
|
32
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
|
|
4
|
+
export function Checkbox(props = {}) {
|
|
5
|
+
const {
|
|
6
|
+
label = '',
|
|
7
|
+
checked = false,
|
|
8
|
+
disabled = false,
|
|
9
|
+
size = 'md',
|
|
10
|
+
theme = 'light',
|
|
11
|
+
className = '',
|
|
12
|
+
onChange,
|
|
13
|
+
} = props;
|
|
14
|
+
|
|
15
|
+
const input = el('input', {
|
|
16
|
+
type: 'checkbox',
|
|
17
|
+
className: 'boongle-checkbox__input',
|
|
18
|
+
checked: checked ? '' : undefined,
|
|
19
|
+
disabled: disabled || undefined,
|
|
20
|
+
onChange,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return el('label', {
|
|
24
|
+
className: boongleClasses('boongle-checkbox', { size, theme, className }),
|
|
25
|
+
}, [input, el('span', { className: 'boongle-checkbox__box' }), el('span', { className: 'boongle-checkbox__label' }, [label])]);
|
|
26
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
import { Icon } from './icon.js';
|
|
4
|
+
|
|
5
|
+
export function Chip(props = {}) {
|
|
6
|
+
const {
|
|
7
|
+
label = '',
|
|
8
|
+
icon,
|
|
9
|
+
removable = false,
|
|
10
|
+
size = 'md',
|
|
11
|
+
theme = 'light',
|
|
12
|
+
className = '',
|
|
13
|
+
onRemove,
|
|
14
|
+
} = props;
|
|
15
|
+
|
|
16
|
+
const children = [];
|
|
17
|
+
if (icon) children.push(Icon({ icon, size: 'sm', theme }));
|
|
18
|
+
children.push(el('span', { className: 'boongle-chip__text' }, [label]));
|
|
19
|
+
|
|
20
|
+
const root = el('span', {
|
|
21
|
+
className: boongleClasses('boongle-chip', { size, theme, className }),
|
|
22
|
+
}, children);
|
|
23
|
+
|
|
24
|
+
if (removable) {
|
|
25
|
+
const btn = el('button', {
|
|
26
|
+
type: 'button',
|
|
27
|
+
className: 'boongle-chip__remove',
|
|
28
|
+
'aria-label': `Remove ${label}`,
|
|
29
|
+
onClick: onRemove,
|
|
30
|
+
});
|
|
31
|
+
btn.appendChild(Icon({ icon: 'boongle:close', size: 'sm', theme }));
|
|
32
|
+
root.appendChild(btn);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return root;
|
|
36
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @param {object} props
|
|
6
|
+
* @param {string} props.code
|
|
7
|
+
* @param {'sm'|'md'|'lg'} [props.size='md']
|
|
8
|
+
* @param {'light'|'dark'} [props.theme='light']
|
|
9
|
+
* @param {string} [props.language]
|
|
10
|
+
* @param {string} [props.className]
|
|
11
|
+
*/
|
|
12
|
+
export function CodeBlock(props = {}) {
|
|
13
|
+
const { code = '', size = 'md', theme = 'light', language, className = '' } = props;
|
|
14
|
+
|
|
15
|
+
const pre = el('pre', {
|
|
16
|
+
className: boongleClasses('boongle-code', { size, theme, className }),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const codeEl = el('code', {}, [code]);
|
|
20
|
+
if (language) codeEl.className = `language-${language}`;
|
|
21
|
+
pre.appendChild(codeEl);
|
|
22
|
+
|
|
23
|
+
return pre;
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
|
|
4
|
+
export function Divider(props = {}) {
|
|
5
|
+
const { label, size = 'md', theme = 'light', className = '' } = props;
|
|
6
|
+
if (label) {
|
|
7
|
+
return el('div', {
|
|
8
|
+
className: `${boongleClasses('boongle-divider', { size, theme, className })} boongle-divider--labeled`,
|
|
9
|
+
}, [el('span', {}, [label])]);
|
|
10
|
+
}
|
|
11
|
+
return el('hr', { className: boongleClasses('boongle-divider', { size, theme, className }) });
|
|
12
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{ value: string, label: string }} DropdownOption
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @param {object} props
|
|
10
|
+
* @param {DropdownOption[]} props.options
|
|
11
|
+
* @param {string} [props.value]
|
|
12
|
+
* @param {string} [props.placeholder='Select…']
|
|
13
|
+
* @param {'sm'|'md'|'lg'} [props.size='md']
|
|
14
|
+
* @param {'light'|'dark'} [props.theme='light']
|
|
15
|
+
* @param {string} [props.className]
|
|
16
|
+
* @param {(value: string, option: DropdownOption) => void} [props.onChange]
|
|
17
|
+
*/
|
|
18
|
+
export function Dropdown(props = {}) {
|
|
19
|
+
const {
|
|
20
|
+
options = [],
|
|
21
|
+
value,
|
|
22
|
+
placeholder = 'Select…',
|
|
23
|
+
size = 'md',
|
|
24
|
+
theme = 'light',
|
|
25
|
+
className = '',
|
|
26
|
+
onChange,
|
|
27
|
+
} = props;
|
|
28
|
+
|
|
29
|
+
const selected = options.find((o) => o.value === value) ?? options[0];
|
|
30
|
+
const label = selected?.label ?? placeholder;
|
|
31
|
+
|
|
32
|
+
const root = el('div', {
|
|
33
|
+
className: `${boongleClasses('boongle-dropdown', { size, theme, className })} boongle`,
|
|
34
|
+
dataset: { open: 'false', theme },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const labelSpan = el('span', { className: 'boongle-dropdown__label' }, [label]);
|
|
38
|
+
const trigger = el('button', {
|
|
39
|
+
type: 'button',
|
|
40
|
+
className: 'boongle-dropdown__trigger',
|
|
41
|
+
'aria-haspopup': 'listbox',
|
|
42
|
+
'aria-expanded': 'false',
|
|
43
|
+
}, [labelSpan, el('span', { className: 'boongle-dropdown__chevron' })]);
|
|
44
|
+
|
|
45
|
+
const menu = el('ul', {
|
|
46
|
+
className: 'boongle-dropdown__menu',
|
|
47
|
+
role: 'listbox',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
for (const opt of options) {
|
|
51
|
+
const item = el('li', {
|
|
52
|
+
className: 'boongle-dropdown__item',
|
|
53
|
+
role: 'option',
|
|
54
|
+
dataset: { value: opt.value, selected: opt.value === (value ?? selected?.value) ? 'true' : 'false' },
|
|
55
|
+
}, [opt.label]);
|
|
56
|
+
|
|
57
|
+
item.addEventListener('click', () => {
|
|
58
|
+
root.dataset.open = 'false';
|
|
59
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
60
|
+
labelSpan.textContent = opt.label;
|
|
61
|
+
menu.querySelectorAll('.boongle-dropdown__item').forEach((el) => {
|
|
62
|
+
el.dataset.selected = el.dataset.value === opt.value ? 'true' : 'false';
|
|
63
|
+
});
|
|
64
|
+
onChange?.(opt.value, opt);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
menu.appendChild(item);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const toggle = () => {
|
|
71
|
+
const open = root.dataset.open !== 'true';
|
|
72
|
+
root.dataset.open = open ? 'true' : 'false';
|
|
73
|
+
trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
trigger.addEventListener('click', (e) => {
|
|
77
|
+
e.stopPropagation();
|
|
78
|
+
toggle();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
document.addEventListener('click', () => {
|
|
82
|
+
root.dataset.open = 'false';
|
|
83
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
root.append(trigger, menu);
|
|
87
|
+
return root;
|
|
88
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { getIconSvg } from '../icons/registry.js';
|
|
3
|
+
import { el } from '../utils/dom.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {object} props
|
|
7
|
+
* @param {string} props.icon - e.g. "boongle:star"
|
|
8
|
+
* @param {'sm'|'md'|'lg'} [props.size='md']
|
|
9
|
+
* @param {'light'|'dark'} [props.theme='light']
|
|
10
|
+
* @param {string} [props.className]
|
|
11
|
+
* @param {string} [props.ariaLabel]
|
|
12
|
+
*/
|
|
13
|
+
export function Icon(props = {}) {
|
|
14
|
+
const { icon, size = 'md', theme = 'light', className = '', ariaLabel } = props;
|
|
15
|
+
const svg = getIconSvg(icon);
|
|
16
|
+
const wrap = el('span', {
|
|
17
|
+
className: boongleClasses('boongle-icon', { size, theme, className }),
|
|
18
|
+
role: ariaLabel ? 'img' : undefined,
|
|
19
|
+
'aria-label': ariaLabel,
|
|
20
|
+
'aria-hidden': ariaLabel ? undefined : 'true',
|
|
21
|
+
html: svg || '',
|
|
22
|
+
});
|
|
23
|
+
if (!svg) {
|
|
24
|
+
wrap.dataset.boongleIconMissing = icon || 'unknown';
|
|
25
|
+
wrap.title = `Icon not found: ${icon}`;
|
|
26
|
+
}
|
|
27
|
+
return wrap;
|
|
28
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
|
|
4
|
+
export function Input(props = {}) {
|
|
5
|
+
const {
|
|
6
|
+
value = '',
|
|
7
|
+
placeholder = '',
|
|
8
|
+
type = 'text',
|
|
9
|
+
name,
|
|
10
|
+
id,
|
|
11
|
+
disabled = false,
|
|
12
|
+
size = 'md',
|
|
13
|
+
theme = 'light',
|
|
14
|
+
className = '',
|
|
15
|
+
onInput,
|
|
16
|
+
onChange,
|
|
17
|
+
} = props;
|
|
18
|
+
|
|
19
|
+
return el('input', {
|
|
20
|
+
type,
|
|
21
|
+
className: boongleClasses('boongle-input', { size, theme, className }),
|
|
22
|
+
value,
|
|
23
|
+
placeholder,
|
|
24
|
+
name,
|
|
25
|
+
id,
|
|
26
|
+
disabled: disabled || undefined,
|
|
27
|
+
onInput,
|
|
28
|
+
onChange,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
import { Icon } from './icon.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {object} props
|
|
7
|
+
* @param {string} props.label
|
|
8
|
+
* @param {string} [props.href='#']
|
|
9
|
+
* @param {'sm'|'md'|'lg'} [props.size='md']
|
|
10
|
+
* @param {'light'|'dark'} [props.theme='light']
|
|
11
|
+
* @param {string} [props.icon]
|
|
12
|
+
* @param {boolean} [props.external]
|
|
13
|
+
* @param {string} [props.className]
|
|
14
|
+
* @param {() => void} [props.onClick]
|
|
15
|
+
*/
|
|
16
|
+
export function LinkButton(props = {}) {
|
|
17
|
+
const {
|
|
18
|
+
label = '',
|
|
19
|
+
href = '#',
|
|
20
|
+
size = 'md',
|
|
21
|
+
theme = 'light',
|
|
22
|
+
icon,
|
|
23
|
+
external = false,
|
|
24
|
+
className = '',
|
|
25
|
+
onClick,
|
|
26
|
+
} = props;
|
|
27
|
+
|
|
28
|
+
const children = [];
|
|
29
|
+
if (icon) children.push(Icon({ icon, size: 'sm', theme }));
|
|
30
|
+
children.push(label);
|
|
31
|
+
|
|
32
|
+
const attrs = {
|
|
33
|
+
className: boongleClasses('boongle-link-btn', { size, theme, className }),
|
|
34
|
+
href,
|
|
35
|
+
onClick,
|
|
36
|
+
};
|
|
37
|
+
if (external) {
|
|
38
|
+
attrs.target = '_blank';
|
|
39
|
+
attrs.rel = 'noopener noreferrer';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return el('a', attrs, children);
|
|
43
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
import { Button } from './button.js';
|
|
4
|
+
import { Text } from './text.js';
|
|
5
|
+
|
|
6
|
+
export function Modal(props = {}) {
|
|
7
|
+
const {
|
|
8
|
+
title = '',
|
|
9
|
+
body = '',
|
|
10
|
+
open = false,
|
|
11
|
+
size = 'md',
|
|
12
|
+
theme = 'light',
|
|
13
|
+
className = '',
|
|
14
|
+
confirmLabel = 'OK',
|
|
15
|
+
cancelLabel = 'Cancel',
|
|
16
|
+
onConfirm,
|
|
17
|
+
onCancel,
|
|
18
|
+
onClose,
|
|
19
|
+
} = props;
|
|
20
|
+
|
|
21
|
+
const backdrop = el('div', {
|
|
22
|
+
className: `${boongleClasses('boongle-modal', { size, theme, className })} boongle`,
|
|
23
|
+
dataset: { open: open ? 'true' : 'false' },
|
|
24
|
+
role: 'dialog',
|
|
25
|
+
'aria-modal': 'true',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const dialog = el('div', { className: 'boongle-modal__dialog' });
|
|
29
|
+
const header = el('div', { className: 'boongle-modal__header' });
|
|
30
|
+
header.appendChild(Text({ content: title, as: 'h2', size: 'md', theme, bold: true }));
|
|
31
|
+
const content = el('div', { className: 'boongle-modal__body' }, [body]);
|
|
32
|
+
const footer = el('div', { className: 'boongle-modal__footer' });
|
|
33
|
+
|
|
34
|
+
const close = () => {
|
|
35
|
+
backdrop.dataset.open = 'false';
|
|
36
|
+
onClose?.();
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
footer.appendChild(Button({ label: cancelLabel, size: 'sm', theme, variant: 'outline', onClick: () => { close(); onCancel?.(); } }));
|
|
40
|
+
footer.appendChild(Button({ label: confirmLabel, size: 'sm', theme, onClick: () => { onConfirm?.(); close(); } }));
|
|
41
|
+
|
|
42
|
+
backdrop.addEventListener('click', (e) => {
|
|
43
|
+
if (e.target === backdrop) close();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
dialog.append(header, content, footer);
|
|
47
|
+
backdrop.appendChild(dialog);
|
|
48
|
+
return backdrop;
|
|
49
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
|
|
4
|
+
export function Progress(props = {}) {
|
|
5
|
+
const { value = 0, max = 100, label, size = 'md', theme = 'light', className = '' } = props;
|
|
6
|
+
const pct = Math.min(100, Math.max(0, (value / max) * 100));
|
|
7
|
+
|
|
8
|
+
const bar = el('div', {
|
|
9
|
+
className: boongleClasses('boongle-progress', { size, theme, className }),
|
|
10
|
+
role: 'progressbar',
|
|
11
|
+
'aria-valuenow': String(value),
|
|
12
|
+
'aria-valuemin': '0',
|
|
13
|
+
'aria-valuemax': String(max),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const track = el('div', { className: 'boongle-progress__track' });
|
|
17
|
+
const fill = el('div', { className: 'boongle-progress__fill', style: `width:${pct}%` });
|
|
18
|
+
track.appendChild(fill);
|
|
19
|
+
bar.appendChild(track);
|
|
20
|
+
|
|
21
|
+
if (label) bar.appendChild(el('span', { className: 'boongle-progress__label' }, [label]));
|
|
22
|
+
return bar;
|
|
23
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { boongleClasses } from '../theme.js';
|
|
2
|
+
import { el } from '../utils/dom.js';
|
|
3
|
+
|
|
4
|
+
export function Radio(props = {}) {
|
|
5
|
+
const {
|
|
6
|
+
label = '',
|
|
7
|
+
name = 'boongle-radio',
|
|
8
|
+
value = '',
|
|
9
|
+
checked = false,
|
|
10
|
+
disabled = false,
|
|
11
|
+
size = 'md',
|
|
12
|
+
theme = 'light',
|
|
13
|
+
className = '',
|
|
14
|
+
onChange,
|
|
15
|
+
} = props;
|
|
16
|
+
|
|
17
|
+
const input = el('input', {
|
|
18
|
+
type: 'radio',
|
|
19
|
+
className: 'boongle-radio__input',
|
|
20
|
+
name,
|
|
21
|
+
value,
|
|
22
|
+
checked: checked ? '' : undefined,
|
|
23
|
+
disabled: disabled || undefined,
|
|
24
|
+
onChange,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return el('label', {
|
|
28
|
+
className: boongleClasses('boongle-radio', { size, theme, className }),
|
|
29
|
+
}, [input, el('span', { className: 'boongle-radio__dot' }), el('span', { className: 'boongle-radio__label' }, [label])]);
|
|
30
|
+
}
|