htmlboard 0.4.0 → 0.5.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.
@@ -7,6 +7,7 @@
7
7
  <meta name="description" content="Smartphone screen with balance card, quick actions, and transaction feed">
8
8
  <meta name="htmlboard" content="width=390, height=844">
9
9
  <link rel="stylesheet" href="../styles.css">
10
+ <script type="module" src="../components.js"></script>
10
11
  <style>
11
12
  body {
12
13
  min-height: 100vh;
@@ -132,28 +133,6 @@
132
133
  gap: 10px;
133
134
  padding: 8px 20px;
134
135
  }
135
- .tx-item {
136
- display: flex;
137
- align-items: center;
138
- justify-content: space-between;
139
- padding: 14px 16px;
140
- border-radius: 16px;
141
- }
142
- .tx-item .row { gap: 14px; }
143
- .tx-icon {
144
- width: 40px;
145
- height: 40px;
146
- border-radius: 12px;
147
- background: var(--surface-2);
148
- display: flex;
149
- align-items: center;
150
- justify-content: center;
151
- font-size: 18px;
152
- }
153
- .tx-name, .tx-amount { font-size: 14px; font-weight: 600; }
154
- .tx-amount { font-weight: 700; }
155
- .tx-amount.positive { color: var(--emerald); }
156
- .tx-date { font-size: 12px; margin-top: 2px; }
157
136
  /* Bottom Navigation */
158
137
  .bottom-nav {
159
138
  position: fixed;
@@ -232,38 +211,9 @@
232
211
  </div>
233
212
 
234
213
  <div class="tx-list">
235
- <div class="tx-item card">
236
- <div class="row">
237
- <div class="tx-icon">🛍️</div>
238
- <div>
239
- <div class="tx-name">Apple Store</div>
240
- <div class="tx-date muted">Today, 2:15 PM • Electronics</div>
241
- </div>
242
- </div>
243
- <div class="tx-amount">-$199.00</div>
244
- </div>
245
-
246
- <div class="tx-item card">
247
- <div class="row">
248
- <div class="tx-icon">💼</div>
249
- <div>
250
- <div class="tx-name">Client Invoice #1042</div>
251
- <div class="tx-date muted">Yesterday • Wire Transfer</div>
252
- </div>
253
- </div>
254
- <div class="tx-amount positive">+$3,450.00</div>
255
- </div>
256
-
257
- <div class="tx-item card">
258
- <div class="row">
259
- <div class="tx-icon">☕</div>
260
- <div>
261
- <div class="tx-name">Blue Bottle Coffee</div>
262
- <div class="tx-date muted">Oct 26 • Food & Beverage</div>
263
- </div>
264
- </div>
265
- <div class="tx-amount">-$6.75</div>
266
- </div>
214
+ <tx-item icon="🛍️" name="Apple Store" meta="Today, 2:15 PM • Electronics" amount="-$199.00"></tx-item>
215
+ <tx-item icon="💼" name="Client Invoice #1042" meta="Yesterday • Wire Transfer" amount="+$3,450.00" positive></tx-item>
216
+ <tx-item icon="☕" name="Blue Bottle Coffee" meta="Oct 26 • Food & Beverage" amount="-$6.75"></tx-item>
267
217
  </div>
268
218
 
269
219
  <div class="bottom-nav">
@@ -4,9 +4,10 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Component Library Preview</title>
7
- <meta name="description" content="Buttons, inputs, badges, cards, and interactive elements">
8
- <meta name="htmlboard" content="width=780, height=720, category=components">
7
+ <meta name="description" content="Buttons, inputs, badges and avatars from styles.css, and the components built with them">
8
+ <meta name="htmlboard" content="width=780, category=components">
9
9
  <link rel="stylesheet" href="../styles.css">
10
+ <script type="module" src="../components.js"></script>
10
11
  <style>
11
12
  body {
12
13
  padding: 24px;
@@ -20,15 +21,16 @@
20
21
  }
21
22
  .title { font-size: 20px; font-weight: 700; }
22
23
  .subtitle { font-size: 13px; margin-top: 4px; }
23
- .card { display: flex; flex-direction: column; gap: 16px; }
24
+ body > .card { display: flex; flex-direction: column; gap: 16px; }
24
25
  .row { flex-wrap: wrap; }
25
26
  .field { flex: 1; min-width: 200px; }
27
+ .pair { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
26
28
  </style>
27
29
  </head>
28
30
  <body>
29
31
  <div class="header">
30
32
  <h1 class="title">Design System Components</h1>
31
- <p class="subtitle muted">Everything here comes from styles.css, shared by all the designs</p>
33
+ <p class="subtitle muted">The classes in styles.css and the components in components/, shared by all the designs</p>
32
34
  </div>
33
35
 
34
36
  <div class="card">
@@ -73,5 +75,15 @@
73
75
  <div class="avatar" style="width: 44px; height: 44px; font-size: 16px">JS</div>
74
76
  </div>
75
77
  </div>
78
+
79
+ <div class="card">
80
+ <div class="label">Components: &lt;stat-card&gt; and &lt;tx-item&gt;</div>
81
+ <div class="pair">
82
+ <stat-card label="Monthly Recurring Revenue" value="$184,290" trend="▲ +12.4% vs last mo"></stat-card>
83
+ <stat-card label="Average Latency" value="34ms" trend="▼ -2ms target" warn></stat-card>
84
+ <tx-item icon="💼" name="Client Invoice #1042" meta="Yesterday • Wire Transfer" amount="+$3,450.00" positive></tx-item>
85
+ <tx-item icon="☕" name="Blue Bottle Coffee" meta="Oct 26 • Food & Beverage" amount="-$6.75"></tx-item>
86
+ </div>
87
+ </div>
76
88
  </body>
77
89
  </html>
@@ -0,0 +1,67 @@
1
+ <!-- <app-sidebar active="Dashboard"></app-sidebar> -->
2
+ <div class="brand row"><span class="logo">⚡</span> Nexus Platform</div>
3
+
4
+ <nav>
5
+ <div class="label">Core</div>
6
+ <a href="../01-screens/01-dashboard.html">📊 Dashboard</a>
7
+ <a href="../01-screens/02-customers.html">👥 Customers</a>
8
+ <a href="#">💳 Invoices</a>
9
+ <a href="#">📈 Analytics</a>
10
+ <div class="label">System</div>
11
+ <a href="#">⚙️ Settings</a>
12
+ <a href="#">🛡️ Security</a>
13
+ </nav>
14
+
15
+ <div class="user row">
16
+ <div class="avatar">AM</div>
17
+ <div>
18
+ <div class="name">Alex Morgan</div>
19
+ <div class="email muted">alex@nexus.io</div>
20
+ </div>
21
+ </div>
22
+
23
+ <style>
24
+ & {
25
+ display: flex;
26
+ flex-direction: column;
27
+ flex-shrink: 0;
28
+ width: 240px;
29
+ padding: 20px 16px;
30
+ background: #10131d;
31
+ border-right: 1px solid var(--border);
32
+ }
33
+ .brand { gap: 10px; padding: 4px 8px 24px; font-size: 17px; font-weight: 700; letter-spacing: -0.02em; }
34
+ .logo {
35
+ display: grid;
36
+ place-items: center;
37
+ width: 32px;
38
+ height: 32px;
39
+ border-radius: var(--radius);
40
+ background: linear-gradient(135deg, var(--accent), #3b82f6);
41
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
42
+ font-size: 16px;
43
+ }
44
+ nav { display: flex; flex-direction: column; gap: 4px; flex: 1; }
45
+ nav .label { padding: 8px 12px 4px; font-size: 11px; }
46
+ nav .label ~ .label { margin-top: 16px; }
47
+ nav a {
48
+ padding: 10px 12px;
49
+ border-radius: var(--radius);
50
+ color: var(--text-muted);
51
+ font-size: 14px;
52
+ font-weight: 500;
53
+ transition: all 0.15s ease;
54
+ }
55
+ nav a:hover { background: rgba(255, 255, 255, 0.04); color: var(--text); }
56
+ nav a.active { background: rgba(99, 102, 241, 0.12); color: var(--accent-light); font-weight: 600; }
57
+ .user { padding: 12px; border-radius: 10px; background: rgba(255, 255, 255, 0.03); border: 1px solid var(--border); }
58
+ .user .avatar { background: linear-gradient(135deg, #ec4899, #8b5cf6); font-weight: 600; }
59
+ .name { font-size: 13px; font-weight: 600; }
60
+ .email { font-size: 11px; }
61
+ </style>
62
+
63
+ <script>
64
+ // The link named by active="…" is the current page
65
+ const active = this.getAttribute('active');
66
+ for (const link of this.querySelectorAll('nav a')) link.classList.toggle('active', link.textContent.includes(active));
67
+ </script>
@@ -0,0 +1,19 @@
1
+ <!-- <app-topbar search="Search customers..."><button class="btn btn-primary">+ Add Customer</button></app-topbar> -->
2
+ <input class="input" type="text" placeholder="{{search}}">
3
+ <div class="row"><slot></slot></div>
4
+
5
+ <style>
6
+ & {
7
+ display: flex;
8
+ align-items: center;
9
+ justify-content: space-between;
10
+ flex-shrink: 0;
11
+ height: 64px;
12
+ padding: 0 32px;
13
+ border-bottom: 1px solid var(--border);
14
+ background: rgba(11, 13, 19, 0.8);
15
+ backdrop-filter: blur(12px);
16
+ }
17
+ .input { width: 280px; padding: 8px 16px; }
18
+ .row { gap: 16px; font-size: 13px; }
19
+ </style>
@@ -0,0 +1,14 @@
1
+ <!-- <stat-card label="Monthly Recurring Revenue" value="$184,290" trend="▲ +12.4% vs last mo"></stat-card> <stat-card label="Average Latency" value="34ms" trend="▼ -2ms target" warn></stat-card> -->
2
+ <div class="card">
3
+ <div class="name muted">{{label}}</div>
4
+ <div class="value">{{value}}</div>
5
+ <div class="trend">{{trend}}</div>
6
+ </div>
7
+
8
+ <style>
9
+ .card { height: 100%; }
10
+ .name { font-size: 13px; margin-bottom: 8px; }
11
+ .value { font-size: 26px; font-weight: 700; margin-bottom: 8px; }
12
+ .trend { font-size: 12px; font-weight: 600; color: var(--emerald); }
13
+ &[warn] .trend { color: var(--amber); }
14
+ </style>
@@ -0,0 +1,33 @@
1
+ <!-- <tx-item icon="💼" name="Client Invoice #1042" meta="Yesterday • Wire Transfer" amount="+$3,450.00" positive></tx-item> <tx-item icon="☕" name="Blue Bottle Coffee" meta="Oct 26 • Food & Beverage" amount="-$6.75"></tx-item> -->
2
+ <div class="icon">{{icon}}</div>
3
+ <div class="what">
4
+ <div class="name">{{name}}</div>
5
+ <div class="meta muted">{{meta}}</div>
6
+ </div>
7
+ <div class="amount">{{amount}}</div>
8
+
9
+ <style>
10
+ & {
11
+ display: flex;
12
+ align-items: center;
13
+ gap: 14px;
14
+ padding: 14px 16px;
15
+ background: var(--surface);
16
+ border: 1px solid var(--border);
17
+ border-radius: 16px;
18
+ }
19
+ .icon {
20
+ display: grid;
21
+ place-items: center;
22
+ width: 40px;
23
+ height: 40px;
24
+ border-radius: 12px;
25
+ background: var(--surface-2);
26
+ font-size: 18px;
27
+ }
28
+ .what { flex: 1; }
29
+ .name { font-size: 14px; font-weight: 600; }
30
+ .meta { font-size: 12px; margin-top: 2px; }
31
+ .amount { font-size: 14px; font-weight: 700; }
32
+ &[positive] .amount { color: var(--emerald); }
33
+ </style>
@@ -0,0 +1,180 @@
1
+ // components.js: every file in components/ is a custom element named after it, so
2
+ // components/tx-item.html is <tx-item>. Designs load this file once, beside styles.css:
3
+ //
4
+ // <script type="module" src="../components.js"></script>
5
+ //
6
+ // A component file is a piece of HTML (no <html>, <head> or <body>):
7
+ //
8
+ // <!-- <tx-item name="Coffee" amount="-$6.75"></tx-item> --> first: how to use it
9
+ // <b>{{name}}</b> <span class="amount">{{amount}}</span> the element's attributes, as text
10
+ // <slot></slot> <slot name="icon"></slot> its content; its children with slot="icon"
11
+ // <style> & { display: flex } .amount { … } </style> only for this element (& is the element)
12
+ // <script> this.… </script> runs for each element, this = the element
13
+ //
14
+ // Relative links in it start from its file. Components can use other components.
15
+ // Added by htmlboard (npx htmlboard components); yours to edit.
16
+
17
+ const FOLDER = new URL('components/', import.meta.url);
18
+ const AsyncFunction = (async () => {}).constructor;
19
+ // At-rules that can't go inside a style rule: they stay at the top of the component's CSS
20
+ const TOP_LEVEL_RULES = ['CSSKeyframesRule', 'CSSFontFaceRule', 'CSSPropertyRule', 'CSSCounterStyleRule',
21
+ 'CSSFontFeatureValuesRule', 'CSSFontPaletteValuesRule'].map((name) => globalThis[name]).filter(Boolean);
22
+
23
+ const loading = new Map(); // tag → its component loading
24
+ const scripts = new Map(); // URL of a <script src> a component needs → it loading
25
+ const built = new WeakSet();
26
+
27
+ const isContent = (node) => node.nodeType === Node.ELEMENT_NODE || node.textContent.trim() !== '';
28
+ const escape = (text) => text.replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);
29
+ // Not "#…", "?…", "/…", "{{…}}" or with a scheme ("https:", "data:", "mailto:")
30
+ const isRelative = (url) => url.trim() !== '' && !/^\s*([a-z][a-z\d+.-]*:|[/#?]|\{\{)/i.test(url);
31
+ const absolute = (el, attribute, base) => new URL(el.getAttribute(attribute), base).href;
32
+
33
+ // The elements of tags nobody defined yet, in root and under it, load their components. A custom
34
+ // element's name has a hyphen: other elements can be undefined too (<tr is="…">), and aren't ours.
35
+ function discover(root) {
36
+ for (const el of [root, ...root.querySelectorAll(':not(:defined)')]) {
37
+ if (el.localName.includes('-') && el.matches(':not(:defined)')) load(el.localName);
38
+ }
39
+ }
40
+
41
+ function load(tag) {
42
+ if (!loading.has(tag)) loading.set(tag, define(tag).catch((error) => console.error(`<${tag}>`, error)));
43
+ return loading.get(tag);
44
+ }
45
+
46
+ async function define(tag) {
47
+ const url = new URL(`${tag}.html`, FOLDER);
48
+ const response = await fetch(url);
49
+ if (!response.ok) {
50
+ if (!customElements.get(tag)) console.warn(`<${tag}>: there is no ${decodeURI(url.pathname)}`);
51
+ return;
52
+ }
53
+ const component = parse(await response.text(), url, tag);
54
+ await Promise.all(component.needs.map(addScript));
55
+ if (customElements.get(tag)) return; // defined by another script meanwhile
56
+ customElements.define(tag, class extends HTMLElement {
57
+ connectedCallback() {
58
+ if (built.has(this)) return;
59
+ built.add(this);
60
+ build(this, component);
61
+ }
62
+ });
63
+ }
64
+
65
+ // A component file: its markup, without its <style>, <script>, comments and what the canvas reads.
66
+ function parse(source, url, tag) {
67
+ const template = document.createElement('template');
68
+ template.innerHTML = source;
69
+ const { content } = template;
70
+ let css = '';
71
+ let code = '';
72
+ const needs = [];
73
+ for (const node of [...content.children]) {
74
+ if (!['style', 'script', 'link', 'meta', 'title'].includes(node.localName)) continue;
75
+ node.remove();
76
+ if (node.localName === 'style') css += `${node.textContent}\n`;
77
+ else if (node.localName === 'script' && node.hasAttribute('src')) needs.push(absolute(node, 'src', url));
78
+ else if (node.localName === 'script') code += `${node.textContent}\n`;
79
+ else if (node.localName === 'link' && /\bstylesheet\b/i.test(node.getAttribute('rel'))) addLink(absolute(node, 'href', url));
80
+ }
81
+ const walker = document.createTreeWalker(content, NodeFilter.SHOW_COMMENT);
82
+ const comments = [];
83
+ while (walker.nextNode()) comments.push(walker.currentNode);
84
+ for (const comment of comments) comment.remove();
85
+ for (const el of content.querySelectorAll('[src], [href], [poster]')) {
86
+ for (const attribute of ['src', 'href', 'poster']) {
87
+ const value = el.getAttribute(attribute);
88
+ if (value && isRelative(value)) el.setAttribute(attribute, new URL(value, url).href);
89
+ }
90
+ }
91
+ addStyle(tag, css, url);
92
+ let run = null;
93
+ if (code.trim()) {
94
+ try {
95
+ run = new AsyncFunction(code);
96
+ } catch (error) {
97
+ console.error(`<${tag}> <script>`, error);
98
+ }
99
+ }
100
+ return { markup: template.innerHTML.trim(), run, needs };
101
+ }
102
+
103
+ // An element of the component: the markup with the element's attributes, and its content in the slots.
104
+ function build(el, { markup, run }) {
105
+ let depth = 0;
106
+ for (let node = el.parentElement; node; node = node.parentElement) {
107
+ if (node.localName === el.localName && ++depth > 25) return console.error(`<${el.localName}> holds itself`);
108
+ }
109
+ const template = document.createElement('template');
110
+ template.innerHTML = markup.replace(/\{\{\s*([^{}\s]+)\s*\}\}/g, (_, name) => escape(el.getAttribute(name) ?? ''));
111
+ const { content } = template;
112
+ const given = [...el.childNodes];
113
+ const slots = content.querySelectorAll('slot');
114
+ if (slots.length) {
115
+ const placed = new Set();
116
+ for (const slot of slots) {
117
+ const name = slot.getAttribute('name') || '';
118
+ const nodes = given.filter((node) => !placed.has(node) && (node.getAttribute?.('slot') || '') === name);
119
+ nodes.forEach((node) => placed.add(node));
120
+ // A slot nobody fills shows what it holds
121
+ slot.replaceWith(...(nodes.some(isContent) ? nodes : slot.childNodes));
122
+ }
123
+ } else if (given.some(isContent)) {
124
+ // <tx-item /> isn't self-closing in HTML: what came after it was parsed into it, so it goes back
125
+ el.after(...given);
126
+ }
127
+ el.replaceChildren(content);
128
+ run?.call(el).catch((error) => console.error(`<${el.localName}> <script>`, error));
129
+ }
130
+
131
+ // The component's box and CSS, once per page. Its CSS goes inside :where(tag) { … }, so it only
132
+ // reaches the element (& is the element itself) without adding weight to its selectors. It comes
133
+ // after the shared stylesheets and before the page's own <style>, so a design can still restyle it.
134
+ function addStyle(tag, css, url) {
135
+ css = css.replace(/url\(\s*(['"]?)(.*?)\1\s*\)/g, (whole, quote, value) =>
136
+ isRelative(value) ? `url("${new URL(value, url)}")` : whole);
137
+ const top = [];
138
+ try {
139
+ const sheet = new CSSStyleSheet();
140
+ sheet.replaceSync(css);
141
+ for (const rule of sheet.cssRules) if (TOP_LEVEL_RULES.some((type) => rule instanceof type)) top.push(rule.cssText);
142
+ } catch {
143
+ // Nothing to take to the top
144
+ }
145
+ const style = document.createElement('style');
146
+ style.dataset.component = tag;
147
+ style.textContent = `:where(${tag}:not([hidden])) { display: block; }\n${top.join('\n')}\n:where(${tag}) {\n${css}\n}\n`;
148
+ toHead(style);
149
+ }
150
+
151
+ function addLink(href) {
152
+ if ([...document.querySelectorAll('link[rel~="stylesheet"]')].some((link) => link.href === href)) return;
153
+ toHead(Object.assign(document.createElement('link'), { rel: 'stylesheet', href }));
154
+ }
155
+
156
+ // A script a component needs (<script src="…">), loaded once, before its elements are built
157
+ function addScript(src) {
158
+ if (!scripts.has(src)) {
159
+ scripts.set(src, [...document.scripts].some((script) => script.src === src) ? Promise.resolve() : new Promise((resolve) => {
160
+ const script = Object.assign(document.createElement('script'), { src });
161
+ script.onload = script.onerror = resolve;
162
+ document.head.append(script);
163
+ }));
164
+ }
165
+ return scripts.get(src);
166
+ }
167
+
168
+ // Ahead of the page's own <style>, after everything else in the <head>
169
+ function toHead(node) {
170
+ document.head.insertBefore(node, document.head.querySelector('style:not([data-component])'));
171
+ }
172
+
173
+ // Every element of the page gets built, and so do the ones added later (by the page's scripts, or
174
+ // inside other components).
175
+ discover(document.documentElement);
176
+ new MutationObserver((records) => {
177
+ for (const record of records) {
178
+ for (const node of record.addedNodes) if (node.nodeType === Node.ELEMENT_NODE) discover(node);
179
+ }
180
+ }).observe(document.documentElement, { childList: true, subtree: true });
@@ -1,7 +1,9 @@
1
1
  /*
2
- * Shared styles for every design in this folder: tokens, base and components.
2
+ * Shared styles for every design in this folder: tokens, base styles, and the
3
+ * classes designs and components are built with (cards, buttons, badges…).
3
4
  * Designs link it (<link rel="stylesheet" href="../styles.css">) and keep only
4
- * what is specific to them in their own <style>.
5
+ * what is specific to them in their own <style>. The components in components/
6
+ * keep their own CSS in their files.
5
7
  */
6
8
 
7
9
  /* Tokens */
@@ -55,6 +57,14 @@ a { color: inherit; text-decoration: none; }
55
57
  gap: 12px;
56
58
  }
57
59
 
60
+ .page-title { font-size: 24px; font-weight: 700; letter-spacing: -0.02em; }
61
+ .page-sub { margin-top: 4px; font-size: 14px; color: var(--text-muted); }
62
+
63
+ /* Desktop app: <app-sidebar> beside a column that scrolls */
64
+ .app { display: flex; height: 100vh; overflow: hidden; }
65
+ .app-main { flex: 1; display: flex; flex-direction: column; overflow-y: auto; }
66
+ .app-content { display: flex; flex-direction: column; gap: 24px; padding: 28px 32px; }
67
+
58
68
  /* Card */
59
69
  .card {
60
70
  background: var(--surface);
@@ -131,3 +141,15 @@ a { color: inherit; text-decoration: none; }
131
141
  font-size: 14px;
132
142
  font-weight: 700;
133
143
  }
144
+
145
+ /* Table */
146
+ .table { width: 100%; border-collapse: collapse; font-size: 13px; }
147
+ .table th {
148
+ padding: 0 16px 12px 0;
149
+ border-bottom: 1px solid var(--border);
150
+ text-align: left;
151
+ font-weight: 500;
152
+ color: var(--text-subtle);
153
+ }
154
+ .table td { padding: 12px 16px 12px 0; border-bottom: 1px solid rgba(255, 255, 255, 0.04); }
155
+ .table .badge { padding: 3px 8px; font-size: 11px; }