@birdycom/omniaui 0.12.5 → 0.13.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 +1 -1
- package/bin/lib/copy-js.mjs +12 -1
- package/bin/omniaui.mjs +2 -2
- package/dist/barcode.d.ts +15 -0
- package/dist/barcode.js +164 -0
- package/dist/components-extra.css +15 -1
- package/dist/components.css +2237 -256
- package/dist/components.min.css +1 -1
- package/dist/contact-form.d.ts +5 -0
- package/dist/contact-form.js +138 -0
- package/dist/data-table.js +135 -98
- package/dist/fly.d.ts +4 -0
- package/dist/fly.js +80 -0
- package/dist/form-extras.d.ts +11 -0
- package/dist/form-extras.js +214 -0
- package/dist/lazy.d.ts +5 -0
- package/dist/lazy.js +37 -0
- package/dist/oc-theme.css +3 -1
- package/dist/parts/app-data-table-footer.css +38 -0
- package/dist/parts/app-data-table-rich.css +161 -0
- package/dist/parts/app-data-table.css +44 -74
- package/dist/parts/app-name-card.css +65 -0
- package/dist/parts/app-result.css +39 -0
- package/dist/parts/app-titlebar.css +16 -8
- package/dist/parts/data-meter-row.css +40 -0
- package/dist/parts/data-price.css +20 -0
- package/dist/parts/data-ribbon.css +76 -0
- package/dist/parts/data-spec-list.css +36 -0
- package/dist/parts/data-stock.css +41 -0
- package/dist/parts/forms-affix-input.css +1 -1
- package/dist/parts/forms-avatar-picker.css +43 -0
- package/dist/parts/forms-date-field.css +53 -0
- package/dist/parts/forms-density.css +13 -0
- package/dist/parts/forms-form-section.css +64 -0
- package/dist/parts/forms-form.css +76 -0
- package/dist/parts/forms-quantity.css +55 -0
- package/dist/parts/forms-rating.css +81 -0
- package/dist/parts/media-attachment.css +1 -1
- package/dist/parts/media-barcode.css +23 -0
- package/dist/parts/media-chat-composer.css +3 -0
- package/dist/parts/media-emoji-picker.css +3 -1
- package/dist/parts/media-gallery.css +55 -0
- package/dist/parts/media-glass-liquid.css +99 -0
- package/dist/parts/media-glass-options.css +50 -0
- package/dist/parts/media-lazy.css +46 -0
- package/dist/parts/media-lightbox.css +4 -0
- package/dist/parts/media-message-thread.css +1 -1
- package/dist/parts/media-message.css +1 -1
- package/dist/parts/nav-topbar-variants.css +96 -0
- package/dist/parts/site-base.css +33 -0
- package/dist/parts/site-brand-mark.css +56 -0
- package/dist/parts/site-cards.css +183 -0
- package/dist/parts/site-fly.css +40 -0
- package/dist/parts/site-header-variants.css +70 -0
- package/dist/parts/site-layout.css +69 -0
- package/dist/parts/site-panel.css +29 -0
- package/dist/parts/site-prose.css +13 -1
- package/dist/parts/utility-hover.css +44 -0
- package/dist/showcase.css +114 -22
- package/manifest.json +193 -9
- package/package.json +7 -2
- package/snippets/accordion-accent.html +1 -1
- package/snippets/attachment.html +4 -4
- package/snippets/avatar-picker.html +40 -0
- package/snippets/barcode.html +20 -0
- package/snippets/brand-mark.html +41 -0
- package/snippets/chat-composer.html +3 -3
- package/snippets/data-table.html +51 -76
- package/snippets/date-field.html +44 -0
- package/snippets/doc-viewer.html +1 -1
- package/snippets/emoji-picker.html +1 -1
- package/snippets/feature-card.html +52 -0
- package/snippets/fly-in.html +30 -0
- package/snippets/form-density.html +25 -0
- package/snippets/form-section.html +47 -0
- package/snippets/gallery.html +25 -0
- package/snippets/hover-effects.html +19 -0
- package/snippets/icon-list.html +28 -0
- package/snippets/lazy.html +39 -0
- package/snippets/message-thread.html +12 -12
- package/snippets/message.html +10 -10
- package/snippets/meter-row.html +29 -0
- package/snippets/name-card.html +72 -0
- package/snippets/page-header.html +1 -1
- package/snippets/page-sections.html +50 -0
- package/snippets/panel.html +18 -0
- package/snippets/price.html +25 -0
- package/snippets/prose.html +3 -3
- package/snippets/quantity.html +12 -0
- package/snippets/rating.html +26 -0
- package/snippets/result.html +31 -0
- package/snippets/ribbon.html +48 -0
- package/snippets/shot-grid.html +27 -0
- package/snippets/sidebar.html +3 -3
- package/snippets/spec-list.html +34 -0
- package/snippets/stock.html +20 -0
- package/snippets/table-actions.html +5 -5
- package/snippets/table.html +2 -2
- package/snippets/titlebar.html +1 -1
- package/snippets/user.html +3 -3
- package/dist/parts/app-column-menu.css +0 -85
- package/snippets/column-menu.html +0 -38
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// File: packages/ui/src/form-extras.js
|
|
2
|
+
// Map: ISO week -> date field (week label) -> name card (rename) -> avatar picker (preview, drop, remove) -> quantity
|
|
3
|
+
// (step buttons) -> init -> boot
|
|
4
|
+
// Small behaviour for four CSS components; each works without it (native date input, <details>, file input, number input).
|
|
5
|
+
// Events on the component root: oc-namecard-rename { name }, oc-avatar-change { file } (null when removed),
|
|
6
|
+
// oc-avatar-invalid { file, reason } (wrong type or too large).
|
|
7
|
+
|
|
8
|
+
/** ISO 8601 week of a date: { week, year } (the week with the year's first Thursday is week 1). */
|
|
9
|
+
export function isoWeek(date) {
|
|
10
|
+
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
|
11
|
+
const day = d.getUTCDay() || 7;
|
|
12
|
+
d.setUTCDate(d.getUTCDate() + 4 - day);
|
|
13
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
|
14
|
+
return { week: Math.ceil(((d - yearStart) / 86400000 + 1) / 7), year: d.getUTCFullYear() };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** .oc-date-field: write "Week 45" (or the template in data-oc-week, e.g. "W{week} · {year}") into its __week. */
|
|
18
|
+
export function bindDateField(root) {
|
|
19
|
+
const input = root.querySelector(".oc-date-field__input");
|
|
20
|
+
const out = root.querySelector(".oc-date-field__week");
|
|
21
|
+
if (!input || !out || root.dataset.ocBound) return;
|
|
22
|
+
root.dataset.ocBound = "";
|
|
23
|
+
const template = out.dataset.ocWeek || "Week {week}";
|
|
24
|
+
const show = () => {
|
|
25
|
+
const [y, m, d] = (input.value || "").split("-").map(Number);
|
|
26
|
+
if (!y) { out.textContent = ""; return; }
|
|
27
|
+
const { week, year } = isoWeek(new Date(y, m - 1, d));
|
|
28
|
+
out.textContent = template.replace("{week}", String(week)).replace("{year}", String(year));
|
|
29
|
+
};
|
|
30
|
+
input.addEventListener("input", show);
|
|
31
|
+
input.addEventListener("change", show);
|
|
32
|
+
show();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** .oc-name-card: the [data-oc-rename] button swaps the title for an input; Enter or blur saves, Escape cancels. */
|
|
36
|
+
export function bindNameCard(root) {
|
|
37
|
+
const head = root.querySelector(":scope > .oc-name-card__head");
|
|
38
|
+
const title = head?.querySelector(".oc-name-card__title");
|
|
39
|
+
const button = head?.querySelector("[data-oc-rename]");
|
|
40
|
+
if (!title || !button || root.dataset.ocBound) return;
|
|
41
|
+
root.dataset.ocBound = "";
|
|
42
|
+
const input = document.createElement("input");
|
|
43
|
+
input.className = "oc-name-card__input";
|
|
44
|
+
input.hidden = true;
|
|
45
|
+
input.setAttribute("aria-label", button.getAttribute("aria-label") || "Name");
|
|
46
|
+
title.after(input);
|
|
47
|
+
const finish = (save) => {
|
|
48
|
+
if (input.hidden) return;
|
|
49
|
+
const name = input.value.trim();
|
|
50
|
+
if (save && name && name !== title.textContent) {
|
|
51
|
+
title.textContent = name;
|
|
52
|
+
root.dispatchEvent(new CustomEvent("oc-namecard-rename", { bubbles: true, detail: { name } }));
|
|
53
|
+
}
|
|
54
|
+
input.hidden = true;
|
|
55
|
+
title.hidden = false;
|
|
56
|
+
button.focus();
|
|
57
|
+
};
|
|
58
|
+
button.addEventListener("click", (event) => {
|
|
59
|
+
event.preventDefault(); // a button in <summary> would also toggle the card
|
|
60
|
+
input.value = title.textContent.trim();
|
|
61
|
+
title.hidden = true;
|
|
62
|
+
input.hidden = false;
|
|
63
|
+
input.focus();
|
|
64
|
+
input.select();
|
|
65
|
+
});
|
|
66
|
+
// Typing in the input must not toggle the card (Space and Enter in a <summary>).
|
|
67
|
+
input.addEventListener("click", (event) => event.preventDefault());
|
|
68
|
+
input.addEventListener("keydown", (event) => {
|
|
69
|
+
if (event.key === "Enter") { event.preventDefault(); finish(true); }
|
|
70
|
+
if (event.key === "Escape") { event.preventDefault(); finish(false); }
|
|
71
|
+
if (event.key === " ") event.stopPropagation();
|
|
72
|
+
});
|
|
73
|
+
input.addEventListener("keyup", (event) => { if (event.key === " ") event.preventDefault(); });
|
|
74
|
+
input.addEventListener("blur", () => finish(true));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Does file match an accept list ("image/*", "image/png,.svg")? An empty list accepts images only. */
|
|
78
|
+
export function acceptsFile(file, accept = "image/*") {
|
|
79
|
+
if (!file) return false;
|
|
80
|
+
const rules = (accept || "image/*").split(",").map((r) => r.trim().toLowerCase()).filter(Boolean);
|
|
81
|
+
const type = (file.type || "").toLowerCase();
|
|
82
|
+
const name = (file.name || "").toLowerCase();
|
|
83
|
+
return rules.some((r) => (r.startsWith(".") ? name.endsWith(r) : r.endsWith("/*") ? type.startsWith(r.slice(0, -1)) : type === r));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** .oc-avatar-picker: preview the chosen or dropped image; [data-oc-avatar-remove] goes back to the initials.
|
|
87
|
+
* A file that does not match the input's accept (a PDF picked through "All files") or is larger than
|
|
88
|
+
* data-oc-max-kb is refused: the input is cleared and .oc-avatar-picker__error says why (oc-avatar-invalid). */
|
|
89
|
+
export function bindAvatarPicker(root) {
|
|
90
|
+
const preview = root.querySelector(".oc-avatar-picker__preview");
|
|
91
|
+
const file = root.querySelector(".oc-avatar-picker__file");
|
|
92
|
+
if (!preview || !file || root.dataset.ocBound) return;
|
|
93
|
+
root.dataset.ocBound = "";
|
|
94
|
+
const initials = preview.querySelector("[data-oc-initials]") || null;
|
|
95
|
+
let url = "";
|
|
96
|
+
const error = (text) => {
|
|
97
|
+
let box = root.querySelector(".oc-avatar-picker__error");
|
|
98
|
+
if (!box && text) {
|
|
99
|
+
box = document.createElement("span");
|
|
100
|
+
box.className = "oc-avatar-picker__error";
|
|
101
|
+
box.setAttribute("role", "alert");
|
|
102
|
+
(root.querySelector(".oc-avatar-picker__text") || root).append(box);
|
|
103
|
+
}
|
|
104
|
+
if (box) { box.textContent = text; box.hidden = !text; }
|
|
105
|
+
root.classList.toggle("is-invalid", Boolean(text));
|
|
106
|
+
};
|
|
107
|
+
const show = (picked) => {
|
|
108
|
+
if (url) URL.revokeObjectURL(url);
|
|
109
|
+
url = "";
|
|
110
|
+
preview.querySelector("img")?.remove();
|
|
111
|
+
if (initials) initials.hidden = Boolean(picked);
|
|
112
|
+
if (picked) {
|
|
113
|
+
url = URL.createObjectURL(picked);
|
|
114
|
+
const img = document.createElement("img");
|
|
115
|
+
img.src = url;
|
|
116
|
+
img.alt = "";
|
|
117
|
+
preview.prepend(img);
|
|
118
|
+
}
|
|
119
|
+
root.dispatchEvent(new CustomEvent("oc-avatar-change", { bubbles: true, detail: { file: picked || null } }));
|
|
120
|
+
};
|
|
121
|
+
const take = (picked) => {
|
|
122
|
+
if (!picked) return show(null);
|
|
123
|
+
const maxKb = Number(root.dataset.ocMaxKb || 0);
|
|
124
|
+
const reason = !acceptsFile(picked, file.accept) ? (root.dataset.ocTypeError || "This file is not an image. Choose a PNG, JPG, WebP or SVG.")
|
|
125
|
+
: maxKb && picked.size > maxKb * 1024 ? (root.dataset.ocSizeError || `This image is larger than ${maxKb >= 1024 ? `${Math.round(maxKb / 102.4) / 10} MB` : `${maxKb} KB`}.`) : "";
|
|
126
|
+
if (reason) {
|
|
127
|
+
file.value = "";
|
|
128
|
+
error(reason);
|
|
129
|
+
root.dispatchEvent(new CustomEvent("oc-avatar-invalid", { bubbles: true, detail: { file: picked, reason } }));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
error("");
|
|
133
|
+
show(picked);
|
|
134
|
+
};
|
|
135
|
+
file.addEventListener("change", () => take(file.files?.[0] || null));
|
|
136
|
+
root.addEventListener("click", (event) => {
|
|
137
|
+
if (event.target.closest("[data-oc-avatar-upload]")) file.click();
|
|
138
|
+
if (event.target.closest("[data-oc-avatar-remove]")) { file.value = ""; error(""); show(null); }
|
|
139
|
+
});
|
|
140
|
+
root.addEventListener("dragover", (event) => { event.preventDefault(); root.classList.add("is-dragover"); });
|
|
141
|
+
root.addEventListener("dragleave", () => root.classList.remove("is-dragover"));
|
|
142
|
+
root.addEventListener("drop", (event) => {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
root.classList.remove("is-dragover");
|
|
145
|
+
const picked = event.dataTransfer?.files?.[0];
|
|
146
|
+
if (!picked) return;
|
|
147
|
+
// A dropped file never reaches the input, so put it there for the form (where the browser allows it).
|
|
148
|
+
if (acceptsFile(picked, file.accept)) try { const dt = new DataTransfer(); dt.items.add(picked); file.files = dt.files; } catch { /* old browsers */ }
|
|
149
|
+
take(picked);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Next value of a quantity: one step (data-oc-step = -1 or 1) within min and max, on the step grid. */
|
|
154
|
+
export function stepQuantity(value, direction, { min = -Infinity, max = Infinity, step = 1 } = {}) {
|
|
155
|
+
const size = step > 0 ? step : 1;
|
|
156
|
+
const base = Number.isFinite(min) ? min : 0;
|
|
157
|
+
const current = Number.isFinite(value) ? value : base;
|
|
158
|
+
// Off the step grid (2.5 with step 1), the first step lands on the grid: 3 up, 2 down.
|
|
159
|
+
const k = (current - base) / size;
|
|
160
|
+
const onGrid = Math.abs(k - Math.round(k)) < 1e-9;
|
|
161
|
+
const next = onGrid ? current + direction * size : base + (direction > 0 ? Math.ceil(k) : Math.floor(k)) * size;
|
|
162
|
+
return Math.min(max, Math.max(min, Number(next.toFixed(6))));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** .oc-quantity: the [data-oc-step] buttons change the number input and disable themselves at min / max. */
|
|
166
|
+
export function bindQuantity(root) {
|
|
167
|
+
const input = root.querySelector(".oc-quantity__input");
|
|
168
|
+
if (!input || root.dataset.ocBound) return;
|
|
169
|
+
root.dataset.ocBound = "";
|
|
170
|
+
const num = (attr, fallback) => (input.getAttribute(attr) === null || input.getAttribute(attr) === "" ? fallback : Number(input.getAttribute(attr)));
|
|
171
|
+
const limits = () => ({ min: num("min", -Infinity), max: num("max", Infinity), step: num("step", 1) });
|
|
172
|
+
const sync = () => {
|
|
173
|
+
const { min, max } = limits();
|
|
174
|
+
const value = Number(input.value);
|
|
175
|
+
root.querySelectorAll("[data-oc-step]").forEach((button) => {
|
|
176
|
+
const down = Number(button.dataset.ocStep) < 0;
|
|
177
|
+
button.disabled = input.disabled || (down ? value <= min : value >= max);
|
|
178
|
+
});
|
|
179
|
+
};
|
|
180
|
+
root.addEventListener("click", (event) => {
|
|
181
|
+
const button = event.target.closest("[data-oc-step]");
|
|
182
|
+
if (!button || !root.contains(button) || input.disabled || input.readOnly) return;
|
|
183
|
+
const next = stepQuantity(Number(input.value), Number(button.dataset.ocStep) < 0 ? -1 : 1, limits());
|
|
184
|
+
if (String(next) === input.value) return;
|
|
185
|
+
input.value = String(next);
|
|
186
|
+
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
187
|
+
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
188
|
+
});
|
|
189
|
+
input.addEventListener("input", sync);
|
|
190
|
+
input.addEventListener("change", sync);
|
|
191
|
+
sync();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Wire every date field, name card, avatar picker and quantity field under scope. */
|
|
195
|
+
export function initFormExtras(scope = document) {
|
|
196
|
+
scope.querySelectorAll(".oc-date-field").forEach(bindDateField);
|
|
197
|
+
scope.querySelectorAll(".oc-name-card").forEach(bindNameCard);
|
|
198
|
+
scope.querySelectorAll(".oc-avatar-picker").forEach(bindAvatarPicker);
|
|
199
|
+
scope.querySelectorAll(".oc-quantity").forEach(bindQuantity);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Starts on its own as /js/oc-form-extras.js (`npx omniaui copy-js`), with ?auto in the URL or data-oc-auto on the tag.
|
|
203
|
+
function autoStart() {
|
|
204
|
+
if (typeof document === "undefined") return false;
|
|
205
|
+
const url = String(import.meta.url);
|
|
206
|
+
if (url.includes("oc-form-extras") || /[?&]auto(=|&|$)/.test(url)) return true;
|
|
207
|
+
const file = url.split(/[?#]/)[0];
|
|
208
|
+
return [...document.querySelectorAll("script[data-oc-auto][src]")].some((s) => s.src.split(/[?#]/)[0] === file);
|
|
209
|
+
}
|
|
210
|
+
if (autoStart()) {
|
|
211
|
+
const boot = () => initFormExtras(document);
|
|
212
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
|
|
213
|
+
else boot();
|
|
214
|
+
}
|
package/dist/lazy.d.ts
ADDED
package/dist/lazy.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// File: packages/ui/src/lazy.js
|
|
2
|
+
// Map: mark one image (loaded / error) -> bind every .oc-lazy -> boot
|
|
3
|
+
// Lazy images: <figure class="oc-lazy"><img loading="lazy" …></figure>. The browser loads the image when it comes near
|
|
4
|
+
// the screen; this script only adds .is-loaded (the photo fades in over the placeholder) or .is-error. Without it the
|
|
5
|
+
// image simply shows when it is there.
|
|
6
|
+
|
|
7
|
+
/** Mark the .oc-lazy box of one image as loaded or failed, now or when the image finishes. */
|
|
8
|
+
export function watchLazy(img) {
|
|
9
|
+
const box = img.closest(".oc-lazy");
|
|
10
|
+
if (!box || box.dataset.ocBound) return;
|
|
11
|
+
box.dataset.ocBound = "";
|
|
12
|
+
const done = () => box.classList.add(img.naturalWidth ? "is-loaded" : "is-error");
|
|
13
|
+
if (img.complete) done();
|
|
14
|
+
else {
|
|
15
|
+
img.addEventListener("load", done, { once: true });
|
|
16
|
+
img.addEventListener("error", () => box.classList.add("is-error"), { once: true });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Watch every lazy image under scope. */
|
|
21
|
+
export function initLazy(scope = document) {
|
|
22
|
+
scope.querySelectorAll(".oc-lazy img").forEach(watchLazy);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Starts on its own as /js/oc-lazy.js (`npx omniaui copy-js`), with ?auto in the URL or data-oc-auto on the tag.
|
|
26
|
+
function autoStart() {
|
|
27
|
+
if (typeof document === "undefined") return false;
|
|
28
|
+
const url = String(import.meta.url);
|
|
29
|
+
if (url.includes("oc-lazy") || /[?&]auto(=|&|$)/.test(url)) return true;
|
|
30
|
+
const file = url.split(/[?#]/)[0];
|
|
31
|
+
return [...document.querySelectorAll("script[data-oc-auto][src]")].some((s) => s.src.split(/[?#]/)[0] === file);
|
|
32
|
+
}
|
|
33
|
+
if (autoStart()) {
|
|
34
|
+
const boot = () => initLazy(document);
|
|
35
|
+
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", boot);
|
|
36
|
+
else boot();
|
|
37
|
+
}
|
package/dist/oc-theme.css
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* File: oc-theme.css — generated from tokens/semantic.json by @birdycom/omniacss 0.
|
|
1
|
+
/* File: oc-theme.css — generated from tokens/semantic.json by @birdycom/omniacss 0.13.0. Do not edit. */
|
|
2
2
|
/* Semantic theme tokens: every color is light-dark() of an Omnia palette token (oc-colors.css). */
|
|
3
3
|
/* Same source as the desktop export: npx omniacss tokens --format json|dtcg|python */
|
|
4
4
|
:root {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
--oc-foreground: light-dark(var(--color-duskblue-60), var(--color-frostblue-10));
|
|
15
15
|
--oc-muted: light-dark(var(--color-frostblue-60), var(--color-shallowblue-50));
|
|
16
16
|
--oc-muted-foreground: light-dark(var(--color-stormblue-40), var(--color-mistblue-70));
|
|
17
|
+
--oc-subtle-foreground: light-dark(var(--color-stormblue-10), var(--color-mistblue-95));
|
|
17
18
|
--oc-card: light-dark(var(--color-white-50), var(--color-shallowblue-70));
|
|
18
19
|
--oc-popover: light-dark(var(--color-white-50), var(--color-shallowblue-60));
|
|
19
20
|
--oc-popover-foreground: var(--oc-foreground);
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
--oc-transition-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1);
|
|
59
60
|
--oc-font-sans: system-ui, -apple-system, "Segoe UI", Roboto, Cantarell, "Helvetica Neue", sans-serif;
|
|
60
61
|
--oc-font-mono: "Cascadia Code", "JetBrains Mono", Consolas, ui-monospace, monospace;
|
|
62
|
+
--oc-font-display: system-ui, -apple-system, "Segoe UI", Roboto, Cantarell, "Helvetica Neue", sans-serif;
|
|
61
63
|
|
|
62
64
|
/* Shadows (x y blur spread; color + alpha per mode in --oc-shadow-<name>-color) */
|
|
63
65
|
--oc-shadow-sm-color: light-dark(oklch(from var(--color-stormblue-50) l c h / 0.08), oklch(from var(--color-black-50) l c h / 0.4));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* File: packages/ui/src/parts/app-data-table-footer.css | Map: footer grid (count, rows per page, pager) -> page buttons -> narrow screens */
|
|
2
|
+
/* Footer of the list table (app-data-table.css): the count on the left, rows per page in the middle, the pager on the right. */
|
|
3
|
+
|
|
4
|
+
/* Footer: count on the left, rows-per-page in the middle, pager on the right. */
|
|
5
|
+
.oc-data-table-footer {
|
|
6
|
+
display: grid;
|
|
7
|
+
grid-template-columns: 1fr auto 1fr;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 0.75rem;
|
|
10
|
+
}
|
|
11
|
+
.oc-data-table-summary { margin: 0; font-size: 0.8125rem; color: var(--oc-muted-foreground); }
|
|
12
|
+
.oc-data-table-per-page { display: inline-flex; align-items: center; gap: 0.5rem; font-size: 0.8125rem; }
|
|
13
|
+
.oc-data-table-per-page .oc-select { inline-size: auto; }
|
|
14
|
+
.oc-data-table-pager { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.25rem; }
|
|
15
|
+
.oc-data-table-page {
|
|
16
|
+
min-inline-size: 2rem;
|
|
17
|
+
padding: 0.25rem 0.45rem;
|
|
18
|
+
font: inherit;
|
|
19
|
+
font-size: 0.8125rem;
|
|
20
|
+
color: var(--oc-foreground);
|
|
21
|
+
background: var(--oc-card);
|
|
22
|
+
border: 1px solid var(--oc-border);
|
|
23
|
+
border-radius: calc(var(--oc-radius) - 2px);
|
|
24
|
+
cursor: pointer;
|
|
25
|
+
}
|
|
26
|
+
.oc-data-table-page[aria-current="page"] {
|
|
27
|
+
color: var(--oc-primary);
|
|
28
|
+
background: oklch(from var(--oc-primary) l c h / 0.1);
|
|
29
|
+
border-color: oklch(from var(--oc-primary) l c h / 0.35);
|
|
30
|
+
}
|
|
31
|
+
.oc-data-table-page:disabled { opacity: 0.4; cursor: default; }
|
|
32
|
+
.oc-data-table-page:focus-visible { outline: 2px solid var(--oc-ring); outline-offset: 1px; }
|
|
33
|
+
.oc-data-table-ellipsis { padding-inline: 0.25rem; color: var(--oc-muted-foreground); }
|
|
34
|
+
|
|
35
|
+
@media (max-width: 40rem) {
|
|
36
|
+
.oc-data-table-footer { grid-template-columns: 1fr; }
|
|
37
|
+
.oc-data-table-pager { justify-content: flex-start; }
|
|
38
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/* File: packages/ui/src/parts/app-data-table-rich.css | Map: control height -> header (sort icons, columns) -> type tabs -> filter row -> name, truncate and count cells -> card fields */
|
|
2
|
+
/* The rich relations list on top of app-data-table.css: tabs that filter a column, filter selects with an icon,
|
|
3
|
+
rich cells and the label/value lines of the card view. Behaviour: data-oc-col-filter in src/data-table.js. */
|
|
4
|
+
|
|
5
|
+
/* Every control is 2rem high: search, filter selects, trash pills, icon buttons, rows per page and the pager. */
|
|
6
|
+
.oc-data-table :is(.oc-data-table-toolbar, .oc-data-table-filters, .oc-data-table-footer) :is(.oc-input, .oc-select) { block-size: 2rem; padding-block: 0; font-size: 0.8125rem; }
|
|
7
|
+
.oc-data-table .oc-data-table-page { min-block-size: 2rem; }
|
|
8
|
+
.oc-data-table :is(.oc-data-table-toolbar, .oc-data-table-filters, thead) .oc-btn--icon { --oc-btn-square: 2rem; }
|
|
9
|
+
.oc-data-table-toolbar .oc-filter-pill {
|
|
10
|
+
display: inline-flex;
|
|
11
|
+
align-items: center;
|
|
12
|
+
block-size: 2rem;
|
|
13
|
+
padding-inline: 0.625rem;
|
|
14
|
+
border-radius: calc(var(--oc-radius) - 2px);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Header: the sort mark only on the sorted column (and on hover); the actions header holds the column button. */
|
|
18
|
+
/* The sort mark always keeps its 1rem, so sorting another column never moves the header: empty while unsorted,
|
|
19
|
+
faint chevrons on hover, the accent sort icon on the sorted column. */
|
|
20
|
+
:is(.oc-data-table, .oc-block-datatable) th:has(.oc-table-sort) { cursor: pointer; }
|
|
21
|
+
:is(.oc-data-table, .oc-block-datatable) th .oc-table-sort::after { content: ""; flex-shrink: 0; inline-size: 1rem; block-size: 1rem; opacity: 1; }
|
|
22
|
+
:is(.oc-data-table, .oc-block-datatable) th:not([aria-sort]):hover .oc-table-sort::after {
|
|
23
|
+
background: var(--oc-muted-foreground);
|
|
24
|
+
opacity: 0.6;
|
|
25
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 18.17L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15M12 5.83L15.17 9l1.41-1.41L12 3L7.41 7.59L8.83 9z'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
26
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 18.17L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15M12 5.83L15.17 9l1.41-1.41L12 3L7.41 7.59L8.83 9z'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
27
|
+
}
|
|
28
|
+
/* Sorted column: a bold label and a sort icon in the accent color. aria-sort on a header in the HTML
|
|
29
|
+
is the column sorted when the page opens. */
|
|
30
|
+
:is(.oc-data-table, .oc-block-datatable) th[aria-sort] .oc-table-sort { color: var(--oc-foreground); }
|
|
31
|
+
:is(.oc-data-table, .oc-block-datatable) th[aria-sort] .oc-table-sort::after {
|
|
32
|
+
content: "";
|
|
33
|
+
inline-size: 1rem;
|
|
34
|
+
block-size: 1rem;
|
|
35
|
+
opacity: 1;
|
|
36
|
+
background: var(--oc-primary);
|
|
37
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 17h3l-4 4l-4-4h3V3h2M2 17h10v2H2M6 5v2H2V5m0 6h7v2H2z'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
38
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 17h3l-4 4l-4-4h3V3h2M2 17h10v2H2M6 5v2H2V5m0 6h7v2H2z'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
39
|
+
}
|
|
40
|
+
:is(.oc-data-table, .oc-block-datatable) th[aria-sort="descending"] .oc-table-sort::after {
|
|
41
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 7h3l-4-4l-4 4h3v14h2M2 17h10v2H2M6 5v2H2V5m0 6h7v2H2z'/%3E%3C/svg%3E");
|
|
42
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M19 7h3l-4-4l-4 4h3v14h2M2 17h10v2H2M6 5v2H2V5m0 6h7v2H2z'/%3E%3C/svg%3E");
|
|
43
|
+
}
|
|
44
|
+
.oc-data-table th[data-oc-col="actions"] .oc-data-table-th { justify-content: flex-end; gap: 0.5rem; font-weight: 500; color: var(--oc-muted-foreground); }
|
|
45
|
+
.oc-data-table td[data-oc-col="actions"] { text-align: end; }
|
|
46
|
+
/* Row actions per status: data-oc-when="active" (edit, view) only on live rows, data-oc-when="deleted" (restore,
|
|
47
|
+
delete permanently) only on rows in the trash. */
|
|
48
|
+
.oc-data-table tr[data-oc-status="deleted"] [data-oc-when="active"],
|
|
49
|
+
.oc-data-table tr:not([data-oc-status="deleted"]) [data-oc-when="deleted"] { display: none; }
|
|
50
|
+
/* The column chooser in the actions header: a large plain icon (1.75rem), no button frame. */
|
|
51
|
+
.oc-data-table-columns-btn { font-size: 1.75rem; }
|
|
52
|
+
.oc-data-table-columns-btn > :is(svg, iconify-icon) { font-size: 1em; inline-size: 1em; block-size: 1em; }
|
|
53
|
+
|
|
54
|
+
/* Tabs above the toolbar (All / Customers / Companies …): icon, label and a count; they filter one column. */
|
|
55
|
+
.oc-data-table-tabs { display: flex; inline-size: 100%; overflow-x: auto; scrollbar-width: none; }
|
|
56
|
+
.oc-data-table-tabs .oc-tab-trigger { flex-shrink: 0; }
|
|
57
|
+
.oc-data-table-tabs .oc-tab-trigger { display: inline-flex; align-items: center; gap: 0.4rem; }
|
|
58
|
+
.oc-data-table-tabs .oc-tab-trigger :is(svg, iconify-icon) { font-size: 1.1em; inline-size: 1.1em; block-size: 1.1em; }
|
|
59
|
+
.oc-data-table-tabs .oc-tab-trigger[aria-selected="true"] { color: var(--oc-primary); border-bottom-color: var(--oc-primary); }
|
|
60
|
+
.oc-data-table-tab-count {
|
|
61
|
+
min-inline-size: 1.25rem;
|
|
62
|
+
padding: 0 0.375rem;
|
|
63
|
+
font-size: 0.6875rem;
|
|
64
|
+
line-height: 1.125rem;
|
|
65
|
+
text-align: center;
|
|
66
|
+
color: var(--oc-muted-foreground);
|
|
67
|
+
background: var(--oc-muted);
|
|
68
|
+
border-radius: 999px;
|
|
69
|
+
}
|
|
70
|
+
.oc-data-table-tabs [aria-selected="true"] .oc-data-table-tab-count { color: var(--oc-primary); background: oklch(from var(--oc-primary) l c h / 0.12); }
|
|
71
|
+
/* A count in the color of its type (the same tone as the type badge in the table). */
|
|
72
|
+
.oc-data-table-tab-count:is(.oc-data-table-tab-count--success, .oc-data-table-tab-count--info, .oc-data-table-tab-count--warning) {
|
|
73
|
+
color: var(--oc-tab-tone);
|
|
74
|
+
background: oklch(from var(--oc-tab-tone) l c h / 0.12);
|
|
75
|
+
box-shadow: inset 0 0 0 1px oklch(from var(--oc-tab-tone) l c h / 0.35);
|
|
76
|
+
}
|
|
77
|
+
.oc-data-table-tab-count--success { --oc-tab-tone: var(--oc-success); }
|
|
78
|
+
.oc-data-table-tab-count--info { --oc-tab-tone: var(--oc-info, light-dark(var(--color-deepskyblue-70), var(--color-skyblue-50))); }
|
|
79
|
+
.oc-data-table-tab-count--warning { --oc-tab-tone: var(--oc-warning); }
|
|
80
|
+
|
|
81
|
+
/* Filter row: selects with a leading icon (status, type …) and the column button on the right. */
|
|
82
|
+
.oc-data-table-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; }
|
|
83
|
+
.oc-data-table-filters[hidden] { display: none; }
|
|
84
|
+
.oc-data-table-filters > :last-child:is(.oc-btn) { margin-inline-start: auto; }
|
|
85
|
+
.oc-data-table-filter { position: relative; display: inline-flex; align-items: center; }
|
|
86
|
+
.oc-data-table-filter > :is(svg, iconify-icon) {
|
|
87
|
+
position: absolute;
|
|
88
|
+
inset-inline-start: 0.625rem;
|
|
89
|
+
font-size: 1rem;
|
|
90
|
+
inline-size: 1rem;
|
|
91
|
+
block-size: 1rem;
|
|
92
|
+
color: var(--oc-muted-foreground);
|
|
93
|
+
pointer-events: none;
|
|
94
|
+
}
|
|
95
|
+
.oc-data-table-filter > .oc-select { inline-size: auto; min-inline-size: 11rem; padding-inline-start: 2.125rem; }
|
|
96
|
+
|
|
97
|
+
/* Cells. Name: avatar, name with a badge, and a muted second line (VAT number, e-mail …). */
|
|
98
|
+
.oc-data-table-name { display: flex; align-items: center; gap: 0.625rem; min-inline-size: 0; }
|
|
99
|
+
.oc-data-table-name .oc-avatar { inline-size: 2.25rem; block-size: 2.25rem; font-size: 0.75rem; }
|
|
100
|
+
.oc-data-table-name-text { display: grid; min-inline-size: 0; }
|
|
101
|
+
.oc-data-table-name-line { display: flex; align-items: center; gap: 0.375rem; font-weight: 600; }
|
|
102
|
+
.oc-data-table-name-sub { font-size: 0.75rem; color: var(--oc-muted-foreground); }
|
|
103
|
+
/* One line with an ellipsis; put the full text in title="". */
|
|
104
|
+
.oc-data-table-truncate { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-inline-size: var(--oc-data-table-truncate, 18rem); }
|
|
105
|
+
/* A count that opens the related list (projects, invoices …); muted while it is 0. */
|
|
106
|
+
.oc-data-table-count { font-variant-numeric: tabular-nums; }
|
|
107
|
+
.oc-data-table-count[data-count="0"] { color: var(--oc-muted-foreground); opacity: 0.75; }
|
|
108
|
+
/* The sort is visible in the header (icon, bold label); the summary line is for screen readers only. */
|
|
109
|
+
.oc-data-table-sort-summary {
|
|
110
|
+
position: absolute;
|
|
111
|
+
inline-size: 1px;
|
|
112
|
+
block-size: 1px;
|
|
113
|
+
margin: 0;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
clip-path: inset(50%);
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
}
|
|
118
|
+
.oc-data-table-file { display: none; }
|
|
119
|
+
|
|
120
|
+
/* The other visible columns as label / value lines, and the row actions at the bottom. */
|
|
121
|
+
.oc-data-table-card-fields { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.25rem 0.75rem; margin: 0; font-size: 0.8125rem; }
|
|
122
|
+
.oc-data-table-card-fields dt { color: var(--oc-muted-foreground); }
|
|
123
|
+
.oc-data-table-card-fields dd { margin: 0; min-inline-size: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
124
|
+
.oc-data-table-card-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 0.375rem; margin-block-start: auto; }
|
|
125
|
+
|
|
126
|
+
/* Code cells (article numbers, SKUs): monospace and a step smaller. */
|
|
127
|
+
.oc-data-table-code { font-family: var(--oc-font-mono); font-size: 0.75rem; }
|
|
128
|
+
|
|
129
|
+
/* Version 2, ".oc-data-table--bordered": a tinted header row, a line between the columns and the
|
|
130
|
+
sort chevrons always visible; the filter select has its icon in a zone of its own. The rest is the same table. */
|
|
131
|
+
.oc-data-table.oc-data-table--bordered .oc-table :is(th, td) { border-inline-end: 1px solid var(--oc-border); }
|
|
132
|
+
.oc-data-table.oc-data-table--bordered .oc-table :is(th, td):last-child { border-inline-end: 0; }
|
|
133
|
+
.oc-data-table.oc-data-table--bordered .oc-table thead th {
|
|
134
|
+
font-weight: 500;
|
|
135
|
+
background: color-mix(in oklch, var(--oc-card), var(--oc-foreground) 4%);
|
|
136
|
+
}
|
|
137
|
+
.oc-data-table.oc-data-table--bordered .oc-table thead th[data-oc-col="actions"] {
|
|
138
|
+
background: color-mix(in oklch, var(--oc-card), var(--oc-foreground) 4%);
|
|
139
|
+
}
|
|
140
|
+
.oc-data-table--bordered td[data-oc-col="actions"] { text-align: center; }
|
|
141
|
+
.oc-data-table--bordered th[data-oc-col="actions"] .oc-data-table-th { justify-content: center; }
|
|
142
|
+
.oc-data-table--bordered th:not([aria-sort]) .oc-table-sort::after,
|
|
143
|
+
.oc-data-table--bordered th:not([aria-sort]):hover .oc-table-sort::after {
|
|
144
|
+
content: "";
|
|
145
|
+
inline-size: 0.875rem;
|
|
146
|
+
block-size: 0.875rem;
|
|
147
|
+
opacity: 0.5;
|
|
148
|
+
background: currentColor;
|
|
149
|
+
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 18.17L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15M12 5.83L15.17 9l1.41-1.41L12 3L7.41 7.59L8.83 9z'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
150
|
+
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 18.17L8.83 15l-1.41 1.41L12 21l4.59-4.59L15.17 15M12 5.83L15.17 9l1.41-1.41L12 3L7.41 7.59L8.83 9z'/%3E%3C/svg%3E") center / contain no-repeat;
|
|
151
|
+
}
|
|
152
|
+
.oc-data-table--bordered .oc-data-table-filter::after {
|
|
153
|
+
content: "";
|
|
154
|
+
position: absolute;
|
|
155
|
+
inset-block: 0;
|
|
156
|
+
inset-inline-start: 2rem;
|
|
157
|
+
inline-size: 1px;
|
|
158
|
+
background: var(--oc-border);
|
|
159
|
+
pointer-events: none;
|
|
160
|
+
}
|
|
161
|
+
.oc-data-table--bordered .oc-data-table-filter > .oc-select { padding-inline-start: 2.75rem; }
|