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 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
+ }