morethanui 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/js/x-toast.js ADDED
@@ -0,0 +1,119 @@
1
+ /* MTUI <x-toast> — tier: JS-only.
2
+ Toast stack: inverse pills dropping from top center, max 3, slide-up
3
+ exit, pause on hover/focus. Toasts never carry the only copy of
4
+ critical information (there is no no-JS fallback).
5
+ API: mtui.toast('Changes saved')
6
+ mtui.toast('Message deleted', {
7
+ action: { label: 'Undo', onClick: fn },
8
+ duration: 4000, // ms visible; default 4000
9
+ kind: 'success', // opt-in feedback cue: tick|success|error (js/feedback.js)
10
+ }) → dismiss()
11
+ The <x-toast> mount is created on first use (or place one in the page). */
12
+ (function () {
13
+ 'use strict';
14
+ if (customElements.get('x-toast')) return;
15
+
16
+ function cssVar(name) {
17
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
18
+ }
19
+
20
+ /* Removing a toast from the flex stack instantly snaps whatever was
21
+ below it up into the gap — CSS has no built-in transition for a
22
+ sibling's layout position changing. FLIP it: record where every
23
+ remaining toast sits before removal, let the browser reflow, then
24
+ play each one in from its old position to its new one. Skipped
25
+ entirely under reduced motion (--t-knob collapses to 0ms). */
26
+ function removeAndSettle(root, toast) {
27
+ const duration = parseFloat(cssVar('--t-knob')) || 0;
28
+ const siblings = [...root.querySelectorAll('.toast')].filter((t) => t !== toast);
29
+ const before = siblings.map((el) => el.getBoundingClientRect().top);
30
+
31
+ toast.remove();
32
+ if (duration === 0) return;
33
+
34
+ const easing = cssVar('--spring') || 'ease';
35
+ siblings.forEach((el, i) => {
36
+ const dy = before[i] - el.getBoundingClientRect().top;
37
+ if (!dy) return;
38
+ el.style.transition = 'none';
39
+ el.style.transform = `translateY(${dy}px)`;
40
+ el.getBoundingClientRect(); // force the browser to commit the jump before transitioning
41
+ requestAnimationFrame(() => {
42
+ el.style.transition = `transform ${duration}ms ${easing}`;
43
+ el.style.transform = '';
44
+ });
45
+ el.addEventListener('transitionend', function onEnd(e) {
46
+ if (e.propertyName !== 'transform') return;
47
+ el.style.transition = '';
48
+ el.removeEventListener('transitionend', onEnd);
49
+ });
50
+ });
51
+ }
52
+
53
+ class XToast extends HTMLElement {
54
+ connectedCallback() {
55
+ this.setAttribute('role', 'status');
56
+ this.setAttribute('aria-live', 'polite');
57
+ }
58
+
59
+ show(message, opts = {}) {
60
+ const toast = document.createElement('div');
61
+ toast.className = 'toast';
62
+ if (opts.kind) toast.dataset.kind = opts.kind;
63
+ const text = document.createElement('span');
64
+ text.textContent = message;
65
+ toast.append(text);
66
+
67
+ const dismiss = () => {
68
+ if ('leaving' in toast.dataset) return;
69
+ toast.dataset.leaving = '';
70
+ clearTimeout(timer);
71
+ toast.addEventListener('animationend', () => removeAndSettle(this, toast), { once: true });
72
+ };
73
+
74
+ if (opts.action) {
75
+ const btn = document.createElement('button');
76
+ btn.className = 'toast-action';
77
+ btn.textContent = opts.action.label;
78
+ btn.addEventListener('click', () => {
79
+ if (typeof opts.action.onClick === 'function') opts.action.onClick();
80
+ dismiss();
81
+ });
82
+ toast.append(btn);
83
+ }
84
+
85
+ this.append(toast);
86
+
87
+ const alive = [...this.querySelectorAll('.toast')]
88
+ .filter((t) => !('leaving' in t.dataset));
89
+ if (alive.length > 3) {
90
+ const oldest = alive[0];
91
+ oldest.dataset.leaving = '';
92
+ oldest.addEventListener('animationend', () => removeAndSettle(this, oldest), { once: true });
93
+ }
94
+
95
+ let timer = setTimeout(dismiss, opts.duration || 4000);
96
+ toast.addEventListener('mouseenter', () => clearTimeout(timer));
97
+ toast.addEventListener('focusin', () => clearTimeout(timer));
98
+ toast.addEventListener('mouseleave', () => {
99
+ clearTimeout(timer);
100
+ timer = setTimeout(dismiss, 1500);
101
+ });
102
+
103
+ return dismiss;
104
+ }
105
+ }
106
+
107
+ customElements.define('x-toast', XToast);
108
+
109
+ window.mtui = Object.assign(window.mtui || {}, {
110
+ toast(message, opts) {
111
+ let mount = document.querySelector('x-toast');
112
+ if (!mount) {
113
+ mount = document.createElement('x-toast');
114
+ document.body.append(mount);
115
+ }
116
+ return mount.show(message, opts);
117
+ },
118
+ });
119
+ })();
package/llms.txt ADDED
@@ -0,0 +1,409 @@
1
+ # MTUI — usage guide for AI models and humans
2
+
3
+ MTUI (MoreThanUI) is a dependency-free UI library: plain HTML + CSS classes +
4
+ a few custom elements. No Tailwind, no framework, no build step, no packages.
5
+ MTUI is unrelated to Material UI — never import anything.
6
+
7
+ ## Setup — one HTML page, four stylesheets, no build. Full skeleton:
8
+ <!doctype html>
9
+ <html lang="en" data-theme="light" data-accent="clay" data-radius="round" data-density="comfortable">
10
+ <head>
11
+ <meta charset="utf-8">
12
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
13
+ <link rel="stylesheet" href="css/tokens.css">
14
+ <link rel="stylesheet" href="css/base.css">
15
+ <link rel="stylesheet" href="css/layout.css">
16
+ <link rel="stylesheet" href="css/components.css">
17
+ </head>
18
+ <body class="shell"><!-- app frame + one screen preset (see Layout) --></body>
19
+ </html>
20
+ The data-* on <html> are the knobs; the values above ARE the defaults (so
21
+ data-theme default = light, data-accent = clay, etc.). Enhanced-component
22
+ scripts (x-select, x-toast, …) each load once with defer before </body>.
23
+
24
+ ## Theming — data attributes on <html> ONLY. These are the only valid values.
25
+ data-theme: light | dark
26
+ data-accent: clay | cobalt | fern | plum | custom (custom needs a generated
27
+ block — see "Custom accent" below; the 4 presets need nothing)
28
+ data-radius: round | soft | sharp
29
+ data-density: comfortable | compact
30
+ data-tint: accent | warm (default accent: the neutral surfaces + text take
31
+ the accent hue — blue accent → blue-grey UI, like Telegram/
32
+ Material; only the hue moves, contrast is unchanged. "warm" keeps
33
+ the fixed warm neutral. Needs oklch relative-color support; older
34
+ webviews fall back to warm automatically.)
35
+
36
+ Rules:
37
+ 1. Never set these attributes on any element other than <html>.
38
+ 2. Never invent colors, radii, spacing, or durations — if custom CSS is
39
+ unavoidable, use tokens (var(--surface-2), var(--sp-16), var(--r-card)).
40
+ 3. Never draw borders or drop shadows. Separation is tonal.
41
+ 4. Compose screens from MTUI layout presets, never hand-written layout CSS.
42
+
43
+ Setting a knob: <x-theme> (js/x-theme.js) is the full appearance panel. For a
44
+ single control (the common "Dark mode" settings switch), flip the root knob:
45
+ <label class="row" data-align="between"><span class="t-row">Dark mode</span>
46
+ <input type="checkbox" class="switch"
47
+ onchange="document.documentElement.dataset.theme = this.checked ? 'dark' : 'light'"></label>
48
+
49
+ ## Layout — every screen starts from a preset. Never write layout CSS,
50
+ ## media queries, absolute positioning, or arbitrary gaps in app code.
51
+
52
+ Typography classes: t-page t-section t-card t-row t-body t-secondary t-label
53
+ t-code (+ t-ltr to keep numbers/emails LTR inside RTL text).
54
+
55
+ shell — the app frame, one per app. Tab bar <768px, side rail >=768px,
56
+ sticky header, only .shell-content scrolls. Nav items are <a>/<button> = an
57
+ icon (the .icon set works here) + <span> label; mark the current one
58
+ aria-current="page" (it lights up in the accent). shell-header can hold
59
+ trailing actions after the title:
60
+ <body class="shell">
61
+ <header class="shell-header"><span class="t-card">Title</span>
62
+ <button class="btn" data-size="icon" aria-label="New"><span class="icon" data-icon="plus"></span></button></header>
63
+ <nav class="shell-nav">
64
+ <a href="/" aria-current="page"><span class="icon" data-icon="home"></span><span>Home</span></a>
65
+ <a href="/inbox"><span class="icon" data-icon="mail"></span><span>Inbox</span></a>
66
+ </nav>
67
+ <main class="shell-content"><!-- one screen preset goes here --></main>
68
+ </body>
69
+
70
+ Screen presets (one per screen, inside .shell-content). The ONLY elements that
71
+ may wrap a preset are the enhancer custom elements — <x-pull-refresh> and/or
72
+ <x-contextmenu> (they may nest around it); everything else goes inside:
73
+ screen-stack settings/forms/articles — column, centered 40rem >=768px
74
+ screen-list feeds/chats — full-bleed rows + sticky <header> slot
75
+ screen-split list+detail — two panes >=900px; below, set data-show="detail"
76
+ on it to show the detail pane instead of the list pane
77
+ screen-center auth/empty/error — centered block, max 24rem
78
+
79
+ <main class="shell-content">
80
+ <div class="screen-stack">
81
+ <h1 class="t-page">Settings</h1>
82
+ <!-- components -->
83
+ </div>
84
+ </main>
85
+
86
+ Primitives (inside a preset, for custom regions — enum values ONLY):
87
+ stack data-gap="4|8|12|16|20|24|32|40|48" (default --gap) data-align="start|center|stretch" (default stretch — bare children fill width)
88
+ row data-gap same enum data-align="start|center|end|between" data-wrap="on"
89
+ (for a "content left, action right" row use data-align="between")
90
+ grid fixed columns 4 (<768) / 8 (768-1199) / 12 (>=1200), 12px gutter;
91
+ children: data-span / data-span-md / data-span-lg = integer 1-12
92
+ (spans clamp to the column count; a span >= the count = full row)
93
+
94
+ <div class="grid">
95
+ <div data-span="4" data-span-md="4" data-span-lg="6">half at desktop</div>
96
+ <div data-span="4" data-span-md="4" data-span-lg="6">other half</div>
97
+ </div>
98
+
99
+ Debug: add data-outline to any preset/primitive to see dashed layout boxes.
100
+ Remove before shipping.
101
+
102
+ ## Components
103
+ All Basic tier (CSS-only, no JS needed). One canonical markup each — never
104
+ invent alternatives. UI copy: sentence case, plain verbs ("Save changes").
105
+
106
+ Icons: use the shipped set — <span class="icon" data-icon="home"></span>
107
+ (monochrome currentColor, scales with font-size, no SVG authoring). Names:
108
+ home search settings user bell mail calendar plus x check chevron-up
109
+ chevron-down chevron-left chevron-right more-vertical trash heart star share
110
+ pencil info alert-triangle circle-check circle-x (last four suit alerts/empty
111
+ states). Standalone/meaningful: add role="img" aria-label; inside a labelled
112
+ button it's decorative. For an icon NOT in the set, inline a Lucide SVG
113
+ (viewBox 0 0 24 24, fill none, stroke currentColor, stroke-width 2, ~20px).
114
+ <button class="btn" data-variant="primary"><span class="icon" data-icon="plus"></span> New</button>
115
+
116
+ button — data-variant="primary|accent|secondary|ghost|danger" (default
117
+ secondary; accent uses the 2nd brand color --accent-2), data-size="icon"
118
+ (square; add aria-label), data-loading (in-place spinner), disabled:
119
+ <button class="btn" data-variant="primary">Save changes</button>
120
+
121
+ card — surface container (padding only; wrap multiple children in a stack for
122
+ rhythm). screen-stack and stack space their OWN children by --gap, so don't
123
+ double-wrap. Interactive when it's an <a> or <button>:
124
+ <div class="card"><span class="t-card">Title</span><p class="t-body">Body.</p></div>
125
+
126
+ field — label + control + hint; error via data-invalid on .field; selects
127
+ get a .select wrapper (chevron):
128
+ <label class="field">
129
+ <span class="t-label">Email</span>
130
+ <input type="email" placeholder="you@example.com">
131
+ <span class="field-hint">We never share it.</span>
132
+ <span class="field-error">Enter a valid email.</span>
133
+ </label>
134
+ <label class="field"><span class="t-label">Role</span>
135
+ <span class="select"><select><option>Designer</option></select></span></label>
136
+
137
+ switch / checkbox / radio — native inputs; the wrapping row label is the
138
+ 44px tap target:
139
+ <label class="row" data-align="between"><span class="t-row">Dark mode</span>
140
+ <input type="checkbox" class="switch" checked></label>
141
+ <label class="row" data-gap="12"><input type="checkbox" class="checkbox">
142
+ <span class="t-row">Send me the newsletter</span></label>
143
+ <label class="row" data-gap="12"><input type="radio" class="radio" name="plan">
144
+ <span class="t-row">Monthly</span></label>
145
+
146
+ badge — plain = neutral pill (counts, labels); data-tint="rose|sky|mint|
147
+ butter|lavender" (decorative) or data-status="success|warning|danger":
148
+ <span class="badge">3</span>
149
+ <span class="badge" data-status="success">Paid</span>
150
+
151
+ chip — filter chips are <button>, selected via aria-pressed="true" (unselected:
152
+ omit it or aria-pressed="false"); removable chips are <span> with a .chip-x
153
+ button holding the x icon (never a ✕ glyph). Filtering is app logic — no MTUI
154
+ helper; toggle aria-pressed and filter your data on click:
155
+ <button class="chip" aria-pressed="true">Unread</button>
156
+ <span class="chip">design <button class="chip-x" aria-label="Remove"><span class="icon" data-icon="x"></span></button></span>
157
+
158
+ avatar — initials or <img>; data-tint="rose|sky|mint|butter|lavender" (these
159
+ five only — NOT accent names); data-size="sm|lg" (default md); stack for overlap:
160
+ <span class="avatar" data-tint="sky">MC</span>
161
+ <span class="avatar-stack"><span class="avatar" data-tint="sky">MC</span>
162
+ <span class="avatar" data-tint="mint">RB</span><span class="avatar">+3</span></span>
163
+
164
+ item — list row (feeds, chats, lists). Lives in screen-list, or in any
165
+ stack/card for a short list. <a>/<button> when interactive. .item-text is
166
+ flex, so ANYTHING after it sits at the trailing edge: a .chevron (inline svg;
167
+ flips in RTL), a badge, or an .item-meta column (stacked time + count):
168
+ <a class="item" href="/chat/1">
169
+ <span class="avatar" data-tint="mint">MC</span>
170
+ <span class="item-text"><span class="item-title">Maya Chen</span>
171
+ <span class="item-sub">Sounds good — tomorrow works.</span></span>
172
+ <span class="item-meta"><span class="t-label">9:41</span><span class="badge">2</span></span>
173
+ </a>
174
+ <!-- trailing chevron instead of meta: -->
175
+ <a class="item" href="/s">
176
+ <span class="item-text"><span class="item-title">Privacy</span></span>
177
+ <svg class="chevron" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m9 18 6-6-6-6"/></svg>
178
+ </a>
179
+
180
+ segmented — radio group in a well:
181
+ <fieldset class="segmented">
182
+ <label><input type="radio" name="view" checked><span>Day</span></label>
183
+ <label><input type="radio" name="view"><span>Week</span></label>
184
+ </fieldset>
185
+
186
+ accordion — native details/summary; add js/accordion.js for smooth height +
187
+ chevron animation both ways (optional — instant snap without it):
188
+ <details class="accordion"><summary>Shipping details</summary>
189
+ <div class="accordion-body">Orders ship within two business days.</div></details>
190
+
191
+ code — block <pre class="code"> and inline <code class="code-inline">;
192
+ always LTR:
193
+ <pre class="code">npm is not used</pre>
194
+
195
+ skeleton — data-shape="text|title|circle|row"; compose to mirror the final
196
+ layout while loading:
197
+ <span class="skeleton" data-shape="title"></span>
198
+ <span class="skeleton" data-shape="text"></span>
199
+
200
+ empty — empty/error states; error = data-status="danger" icon + Retry:
201
+ <div class="empty">
202
+ <span class="empty-icon"><svg …></svg></span>
203
+ <span class="t-card">No messages yet</span>
204
+ <p class="t-secondary">Start a conversation and it shows up here.</p>
205
+ <button class="btn" data-variant="primary">New message</button>
206
+ </div>
207
+
208
+ alert — inline status panel, one sentence; data-status as badge (default
209
+ neutral). Lead with an icon (alert-triangle/info/circle-check/circle-x):
210
+ <div class="alert" data-status="warning"><span class="icon" data-icon="alert-triangle"></span>
211
+ <div><span class="alert-title">Storage almost full</span>
212
+ <p>You have used 90% of your space.</p></div></div>
213
+
214
+ ## Overlays and feedback
215
+ Enhanced pieces need their script tag once per page (plain scripts, no
216
+ modules, no build): <script src="js/x-tabs.js" defer></script> etc.
217
+ js/accordion.js, js/menu.js: ambient enhancements — one script tag,
218
+ no markup changes, work on every matching element already on the page.
219
+
220
+ dialog — native <dialog>, open with .showModal(); form method="dialog"
221
+ closes it; always confirm before destroying:
222
+ <dialog class="dialog" id="confirm">
223
+ <form method="dialog" class="stack" data-gap="12">
224
+ <span class="t-card">Delete this note?</span>
225
+ <p class="t-secondary">This can't be undone.</p>
226
+ <div class="row" data-gap="8" data-align="between">
227
+ <button class="btn" value="cancel">Cancel</button>
228
+ <button class="btn" data-variant="danger" value="confirm">Delete</button>
229
+ </div>
230
+ </form>
231
+ </dialog>
232
+ <button class="btn" onclick="document.getElementById('confirm').showModal()">Delete…</button>
233
+ <!-- react to the choice: the clicked button's value lands in dialog.returnValue -->
234
+ <script>confirm.addEventListener('close', () => {
235
+ if (confirm.returnValue === 'confirm') { doDelete(); mtui.toast('Deleted', { kind: 'error' }); }
236
+ });</script>
237
+
238
+ menu — popover attribute + js/menu.js (anchors to the trigger; centered
239
+ without JS). data-danger on destructive items:
240
+ <button class="btn" popovertarget="m1">Options</button>
241
+ <div class="menu" id="m1" popover>
242
+ <button class="menu-item">Rename</button>
243
+ <button class="menu-item" data-danger>Delete</button>
244
+ </div>
245
+
246
+ x-tabs — js/x-tabs.js. Wraps a segmented + [data-panel] siblings; only the
247
+ panel matching the checked value shows; bubbling change, detail.value:
248
+ <x-tabs>
249
+ <fieldset class="segmented">
250
+ <label><input type="radio" name="t" value="general" checked><span>General</span></label>
251
+ <label><input type="radio" name="t" value="privacy"><span>Privacy</span></label>
252
+ </fieldset>
253
+ <div data-panel="general">…</div>
254
+ <div data-panel="privacy">…</div>
255
+ </x-tabs>
256
+
257
+ toast — js/x-toast.js. JS-only; never the sole carrier of critical info.
258
+ window.mtui is created by the js/*.js scripts (load them with defer; mtui
259
+ exists by the time your handlers fire). Optional { kind: 'tick'|'success'|
260
+ 'error' } plays the matching feedback cue (see Feedback):
261
+ mtui.toast('Changes saved')
262
+ mtui.toast('Upload failed', { kind: 'error' })
263
+ mtui.toast('Message deleted', { action: { label: 'Undo', onClick: undo } })
264
+
265
+ loading pattern — async buttons set data-loading (spinner in place, size
266
+ kept), restore on settle, outcome lands in a toast; failures render in the
267
+ component's own boundary (alert / empty data-status="danger" + Retry):
268
+ btn.setAttribute('data-loading', '');
269
+ await save();
270
+ btn.removeAttribute('data-loading');
271
+ mtui.toast('Changes saved');
272
+
273
+ ## Enhanced inputs — MTUI-drawn faces over native pickers. Never expose raw
274
+ ## browser chrome for these; the native element stays as the value holder
275
+ ## and no-JS fallback only. Each needs its script tag once per page.
276
+
277
+ x-select — js/x-select.js. Wraps a Basic field select in place; native
278
+ select stays for form submit + no-JS. Keyboard: arrows, Home/End,
279
+ type-ahead, Enter, Esc. Bubbling change, detail.value:
280
+ <x-select>
281
+ <label class="field">
282
+ <span class="t-label">Role</span>
283
+ <span class="select"><select>
284
+ <option value="designer">Designer</option>
285
+ <option value="engineer" selected>Engineer</option>
286
+ </select></span>
287
+ </label>
288
+ </x-select>
289
+
290
+ x-datepicker — js/x-datepicker.js. Single date only (no ranges/times).
291
+ Wraps a Basic field date input. Keyboard: arrows move by day, Enter
292
+ selects, header buttons change month, footer Today jumps + selects. Tap
293
+ the header to zoom out: day grid -> month grid -> scrollable year list
294
+ (auto-centers the current year) -> back to day grid; picking a month or
295
+ year drills back in. Bubbling change, detail.value in native YYYY-MM-DD:
296
+ <x-datepicker>
297
+ <label class="field">
298
+ <span class="t-label">Start date</span>
299
+ <input type="date" min="2026-01-01" max="2026-12-31" value="2026-07-07">
300
+ </label>
301
+ </x-datepicker>
302
+
303
+ x-colorswatches — js/x-colorswatches.js. Curated swatches only, never a
304
+ free color wheel. data-colors on the host (comma-separated hex) is the
305
+ one canonical swatch source. Wraps a Basic field color input. Bubbling
306
+ change, detail.value:
307
+ <x-colorswatches data-colors="#BC4B2A,#3556C9,#2F7A50,#6E46BD,#7A5600">
308
+ <label class="field">
309
+ <span class="t-label">Tag color</span>
310
+ <input type="color" value="#BC4B2A">
311
+ </label>
312
+ </x-colorswatches>
313
+
314
+ x-contextmenu — js/x-contextmenu.js. Wraps any region; opens on right-click,
315
+ long-press (~500ms), OR a click on any [data-contextmenu-trigger] inside it
316
+ (a visible tap affordance — gesture-only is undiscoverable on touch). A
317
+ <template> child declares the menu (reuses .menu-item styling). No JS: the
318
+ browser's native context menu appears. Bubbling select, detail = { value,
319
+ target }: value is the item's data-value; target is the element the menu
320
+ opened on. Wrap a whole LIST in one x-contextmenu — detail.target.closest(
321
+ '.item') tells you which row fired (no per-row menus):
322
+ <x-contextmenu>
323
+ <div class="screen-list">
324
+ <div class="item" data-id="42">
325
+ <span class="item-text"><span class="item-title">Weekly sync</span></span>
326
+ <button class="btn" data-size="icon" data-contextmenu-trigger aria-haspopup="menu" aria-label="Actions"><span class="icon" data-icon="more-vertical"></span></button>
327
+ </div>
328
+ </div>
329
+ <template>
330
+ <button class="menu-item" data-value="rename">Rename</button>
331
+ <button class="menu-item" data-value="delete" data-danger>Delete</button>
332
+ </template>
333
+ </x-contextmenu>
334
+ <script>addEventListener('select', e => {
335
+ const row = e.detail.target.closest('.item'); // which row
336
+ doThing(e.detail.value, row?.dataset.id); // which action
337
+ });</script>
338
+
339
+ pull-to-refresh (x-pull-refresh) — js/pull-refresh.js. Wraps a
340
+ .screen-list (touch only). Pulling past the threshold dispatches a
341
+ bubbling `refresh` event; call detail.done() when the app's async work
342
+ finishes — the app decides what to show (usually mtui.toast()):
343
+ <x-pull-refresh>
344
+ <div class="screen-list">…</div>
345
+ </x-pull-refresh>
346
+ list.addEventListener('refresh', (e) => {
347
+ loadMessages().then(() => { e.detail.done(); mtui.toast('Updated'); });
348
+ });
349
+
350
+ ## Feedback — synthetic sound (WebAudio) + haptics, opt-in via
351
+ ## data-feedback. Nothing fires without a matching visible event — never on
352
+ ## hover or scroll. One script tag, no markup changes to existing components.
353
+
354
+ feedback — js/feedback.js. Cues: tick, toggle-on, toggle-off, success, error
355
+ (gain ≤ 0.06; vibrate 8ms on toggles/confirmations, [20,40,20] on errors).
356
+ Global mute persists to localStorage; play() and haptics no-op silently when
357
+ muted, unsupported, or before a user gesture has unlocked audio:
358
+ mtui.feedback.muted // get
359
+ mtui.feedback.muted = true // set, persists
360
+ mtui.feedback.play('tick') // fire a cue directly
361
+
362
+ data-feedback opts an element into its cue, same attribute everywhere:
363
+ <input type="checkbox" class="switch" data-feedback> <!-- toggle-on/off on change -->
364
+ <button class="btn" data-feedback>Confirm</button> <!-- success on click -->
365
+ <div class="alert" data-status="danger" data-feedback>…</div> <!-- error when it mounts -->
366
+ mtui.toast('Saved', { kind: 'success' }) <!-- tick | success | error -->
367
+
368
+ ## Custom theme — brands may generate a custom PRIMARY (--accent) and a
369
+ ## distinct ACCENT (--accent-2) beyond the 4 presets. Hue is free; chroma/
370
+ ## lightness clamped by §2.14 so contrast survives. --accent-2 defaults to
371
+ ## mirror --accent (monochrome); set it apart for a two-color brand.
372
+
373
+ Generate the block in the showcase (demo/index.html → "Custom theme": pick
374
+ Primary + Accent hue/chroma, copy the output), paste it after tokens.css,
375
+ then set data-accent="custom" on <html>. The surface tint follows the primary.
376
+ Output shape (values are oklch(); light + dark supplied):
377
+ [data-accent="custom"] {
378
+ --accent: oklch(52% C H); --accent-2: oklch(52% C2 H2); /* +hover/text/tint each */
379
+ }
380
+ [data-theme="dark"][data-accent="custom"] {
381
+ --accent-text: oklch(84% .45C H); --accent-tint: oklch(30% .35C H); /* + --accent-2-* */
382
+ }
383
+
384
+ ## Telegram mini apps — js/telegram.js maps the host theme onto MTUI so the
385
+ ## app looks native to the Telegram shell. Load it once; it no-ops outside
386
+ ## Telegram (screen keeps the tokens.css defaults, still functional).
387
+
388
+ <script src="js/telegram.js" defer></script>
389
+ On load and on Telegram's themeChanged it sets data-theme from colorScheme and
390
+ maps themeParams onto MTUI tokens: secondary_bg_color->--bg, bg_color->
391
+ --surface, text_color->--text, hint_color->--text-muted, button_color->--accent
392
+ (+ button_text_color->--on-accent), link_color->--accent-text. The tones
393
+ Telegram doesn't send (--surface-2/-3, --text-faint, --accent-hover,
394
+ --accent-tint) are derived in OKLCH from those host colors — nothing invented.
395
+ mtui.telegram.sync() // re-read the live WebApp and apply
396
+ mtui.telegram.apply(themeParams, scheme) // apply given values (e.g. testing)
397
+
398
+ ## Theme controller — <x-theme> (js/x-theme.js) is an in-app appearance panel
399
+ ## (Telegram-style): user picks theme + accent + corners + density + surface
400
+ ## tint. Writes only to the <html> knobs and persists to localStorage. Drop it
401
+ ## anywhere (e.g. a settings screen). Prefer it over the single Dark-mode switch
402
+ ## when you want persistence. JS-only: with no JS it's empty, app keeps its theme.
403
+
404
+ <script src="js/x-theme.js" defer></script>
405
+ <x-theme></x-theme> <!-- renders its own controls; that's the whole markup -->
406
+ Emits bubbling 'themechange' — detail: { key, value } (key: theme|accent|
407
+ radius|density|tint). Persists to mtui-theme / mtui-accent / mtui-radius /
408
+ mtui-density / mtui-tint; on load, a stored value (if a valid option) >
409
+ current <html> attribute > default.
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "morethanui",
3
+ "version": "1.0.0",
4
+ "description": "MoreThanUI — a dependency-free, framework-agnostic UI component library. Plain HTML + CSS custom properties + light-DOM custom elements. No build required to use; drop the files in and write markup.",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "ui",
8
+ "components",
9
+ "css",
10
+ "web-components",
11
+ "custom-elements",
12
+ "design-system",
13
+ "no-build",
14
+ "dependency-free",
15
+ "framework-agnostic",
16
+ "tauri",
17
+ "telegram-mini-app"
18
+ ],
19
+ "files": [
20
+ "css",
21
+ "js",
22
+ "fonts",
23
+ "dist",
24
+ "llms.txt",
25
+ "DESIGN.md"
26
+ ],
27
+ "sideEffects": [
28
+ "./js/*.js",
29
+ "./css/*.css",
30
+ "./dist/*.js",
31
+ "./dist/*.css"
32
+ ],
33
+ "exports": {
34
+ ".": "./dist/mtui.min.js",
35
+ "./style.css": "./dist/mtui.min.css",
36
+ "./css/*": "./css/*",
37
+ "./js/*": "./js/*",
38
+ "./fonts/*": "./fonts/*",
39
+ "./dist/*": "./dist/*",
40
+ "./llms.txt": "./llms.txt",
41
+ "./package.json": "./package.json"
42
+ },
43
+ "scripts": {
44
+ "bundle": "bash scripts/bundle.sh",
45
+ "test": "bash scripts/test.sh",
46
+ "prepublishOnly": "bash scripts/bundle.sh"
47
+ }
48
+ }