@birdycom/omniacss-migrate 0.0.0-stage → 0.11.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/LICENSE +21 -0
- package/README.md +128 -2
- package/bin/omniacss-migrate.mjs +9 -0
- package/package.json +59 -4
- package/scripts/build-color-map.mjs +31 -0
- package/scripts/extract-palette.mjs +13 -0
- package/scripts/tailwind-palette.json +291 -0
- package/src/bootstrap/compat.mjs +72 -0
- package/src/bootstrap/convert.mjs +102 -0
- package/src/bootstrap/migrate.mjs +259 -0
- package/src/bootstrap/rules.mjs +212 -0
- package/src/cli.mjs +139 -0
- package/src/codemod.mjs +82 -0
- package/src/core.mjs +67 -0
- package/src/data/colors.json +290 -0
- package/src/extract.mjs +91 -0
- package/src/index.mjs +12 -0
- package/src/native.mjs +72 -0
- package/src/postcss.mjs +308 -0
- package/src/preflight.mjs +52 -0
- package/src/project.mjs +194 -0
- package/src/resolve.mjs +220 -0
- package/src/scan.mjs +106 -0
- package/src/setup.mjs +205 -0
- package/src/utilities.mjs +713 -0
- package/src/values.mjs +184 -0
- package/src/variants.mjs +189 -0
- package/src/vite.mjs +81 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// File: packages/migrate/src/bootstrap/convert.mjs
|
|
2
|
+
// Convert one Bootstrap class to OmniaCSS / OmniaUI. Utilities are matched on their CSS declarations against the
|
|
3
|
+
// shipped OmniaCSS build (so `mt-3` = 1rem becomes `mt-1`, whatever the names), responsive infixes become variants.
|
|
4
|
+
// Map: declaration index -> breakpoint -> legacy check -> convertClass
|
|
5
|
+
// Author: Serge Cools | Framework: OmniaCSS
|
|
6
|
+
|
|
7
|
+
import { loadOmniaIndex } from "../native.mjs";
|
|
8
|
+
import { BREAKPOINTS, OMNIA_BREAKPOINTS, utilityDecls, RENAMES, COMPONENTS, MANUAL, JS_STATE, isGridClass } from "./rules.mjs";
|
|
9
|
+
|
|
10
|
+
/** Canonical declaration: no spaces around ":" and "/", zero lengths written as 0. */
|
|
11
|
+
const canon = (decl) => decl.trim().replace(/\s*:\s*/, ":").replace(/\s*\/\s*/g, "/").replace(/\s+/g, " ").replace(/(^|[\s:(,-])0(?:rem|px|em|%)(?=$|[\s),;])/g, "$10");
|
|
12
|
+
const sortKey = (decls) => decls.map(canon).sort().join(";");
|
|
13
|
+
const PREFIXED = /^-(webkit|moz|ms|o)-|:-(webkit|moz|ms|o)-/;
|
|
14
|
+
|
|
15
|
+
/** OmniaCSS index keyed on sorted declarations (order inside a rule does not matter). */
|
|
16
|
+
export function loadDeclIndex(cwd) {
|
|
17
|
+
const index = loadOmniaIndex(cwd);
|
|
18
|
+
if (index.sorted) return index;
|
|
19
|
+
index.sorted = new Map();
|
|
20
|
+
for (const [key, names] of index.byDecls) {
|
|
21
|
+
// Also index without vendor-prefixed fallbacks (position: -webkit-sticky; position: sticky).
|
|
22
|
+
const all = key.split(";");
|
|
23
|
+
for (const k of new Set([sortKey(all), sortKey(all.filter((x) => !PREFIXED.test(x.trim())))])) {
|
|
24
|
+
if (k) index.sorted.set(k, [...(index.sorted.get(k) || []), ...names]);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return index;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Pick the OmniaCSS class for one declaration group: the Bootstrap name itself when it means the same, else the shortest. */
|
|
31
|
+
function pick(names, preferred) {
|
|
32
|
+
if (names.includes(preferred)) return preferred;
|
|
33
|
+
return [...names].sort((a, b) => a.length - b.length || a.localeCompare(b))[0];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Bootstrap breakpoint -> OmniaCSS variant prefix ("md:" or "min-[48rem]:"). */
|
|
37
|
+
export function bpVariant(bp, version, mode = "omnia") {
|
|
38
|
+
const px = BREAKPOINTS[version][bp];
|
|
39
|
+
if (!px) return "";
|
|
40
|
+
if (mode === "exact") return `min-[${px / 16}rem]:`;
|
|
41
|
+
const [name] = Object.entries(OMNIA_BREAKPOINTS).sort((a, b) => Math.abs(a[1] - px) - Math.abs(b[1] - px))[0];
|
|
42
|
+
return `${name}:`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Bootstrap 3/4 names that do nothing in a newer major version (the project may style them itself). */
|
|
46
|
+
const LEGACY = [
|
|
47
|
+
[/^col-xs-/, 3], [/^col-(sm|md|lg)-(offset|push|pull)-/, 3], [/^panel/, 3], [/^well$/, 3], [/^label-/, 3], [/^help-block$/, 3],
|
|
48
|
+
[/^img-(responsive|rounded|circle)$/, 3], [/^pull-(left|right)$/, 3], [/^center-block$/, 3], [/^(hidden|visible)-/, 3], [/^glyphicon/, 3],
|
|
49
|
+
[/^btn-(default|xs)$/, 3], [/^input-group-addon$/, 3], [/^control-label$/, 3], [/^table-condensed$/, 3], [/^hidden$/, 3],
|
|
50
|
+
[/^[mp][lr]-/, 4], [/^font-(weight|italic)/, 4], [/^(float|text)-(left|right)$/, 4], [/^badge-/, 4], [/^sr-only/, 4], [/^custom-select$/, 4],
|
|
51
|
+
[/^embed-responsive/, 4], [/^btn-block$/, 4], [/^jumbotron$/, 4], [/^form-group$/, 4],
|
|
52
|
+
];
|
|
53
|
+
const legacyMax = (name) => LEGACY.find(([re]) => re.test(name))?.[1];
|
|
54
|
+
|
|
55
|
+
const HIDDEN_ORDER = { 3: ["xs", "sm", "md", "lg"] };
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {string} name a single Bootstrap class
|
|
59
|
+
* @param {{ version: 3|4|5, breakpoints?: "omnia"|"exact", index: object }} ctx
|
|
60
|
+
* @returns {{ kind: "utility"|"component"|"grid"|"manual"|"state"|"inactive"|"unmapped"|"custom", to?: string, hint?: string }}
|
|
61
|
+
*/
|
|
62
|
+
export function convertClass(name, ctx) {
|
|
63
|
+
const { version: v, breakpoints = "omnia", index } = ctx;
|
|
64
|
+
if (JS_STATE.has(name)) return { kind: "state" };
|
|
65
|
+
const max = legacyMax(name);
|
|
66
|
+
if (max && v > max) return { kind: "inactive", hint: `Bootstrap ${max} class: does nothing in Bootstrap ${v}` };
|
|
67
|
+
if (isGridClass(name)) return { kind: "grid" };
|
|
68
|
+
if (Object.hasOwn(COMPONENTS, name)) return { kind: "component", to: COMPONENTS[name] };
|
|
69
|
+
if (/^btn-outline-/.test(name)) return { kind: "component", to: "oc-btn--outline" };
|
|
70
|
+
if (Object.hasOwn(RENAMES, name)) return { kind: "utility", to: RENAMES[name] };
|
|
71
|
+
for (const [re, hint] of MANUAL) if (re.test(name)) return { kind: "manual", hint };
|
|
72
|
+
|
|
73
|
+
// Bootstrap 3 hidden-{bp}: hide inside that breakpoint range only.
|
|
74
|
+
let m;
|
|
75
|
+
if (v === 3 && (m = name.match(/^hidden-(xs|sm|md|lg)$/))) {
|
|
76
|
+
const order = HIDDEN_ORDER[3];
|
|
77
|
+
const i = order.indexOf(m[1]);
|
|
78
|
+
const from = i > 0 ? bpVariant(order[i], 3, breakpoints) : "";
|
|
79
|
+
const next = order[i + 1] ? bpVariant(order[i + 1], 3, breakpoints) : "";
|
|
80
|
+
const to = next.startsWith("min-") ? "max-" + next.slice(4) : next ? "max-" + next : "";
|
|
81
|
+
return { kind: "utility", to: `${from}${to}d-none` };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Responsive infix: d-md-none, mt-lg-3, text-md-center, justify-content-md-between
|
|
85
|
+
let base = name, prefix = "";
|
|
86
|
+
if ((m = name.match(/^([a-z]+(?:-[a-z]+)*?)-(sm|md|lg|xl|xxl)-(.+)$/)) && BREAKPOINTS[v][m[2]]) {
|
|
87
|
+
base = `${m[1]}-${m[3]}`;
|
|
88
|
+
prefix = bpVariant(m[2], v, breakpoints);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const groups = utilityDecls(base, v);
|
|
92
|
+
if (!groups) return { kind: "custom" };
|
|
93
|
+
const out = [];
|
|
94
|
+
for (const g of groups) {
|
|
95
|
+
const names = index.sorted.get(sortKey(g));
|
|
96
|
+
if (!names) return { kind: "unmapped", hint: `no OmniaCSS class with ${g.join("; ")}` };
|
|
97
|
+
out.push(prefix + pick(names, base));
|
|
98
|
+
}
|
|
99
|
+
return { kind: "utility", to: out.join(" ") };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { sortKey };
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// File: packages/migrate/src/bootstrap/migrate.mjs
|
|
2
|
+
// Bootstrap -> OmniaCSS: detect the Bootstrap version, rewrite class attributes (utilities to OmniaCSS, components to
|
|
3
|
+
// OmniaUI), keep the grid with a generated compat stylesheet, swap the Bootstrap <link>, and report what needs a hand:
|
|
4
|
+
// JavaScript components (data-bs-*), icon fonts, legacy classes and classes toggled from JavaScript.
|
|
5
|
+
// Dry run unless `write` is true.
|
|
6
|
+
// Map: detect version -> class attributes (skip template code) -> convert -> <link> swap -> JS report -> compat CSS
|
|
7
|
+
// Author: Serge Cools | Framework: OmniaCSS
|
|
8
|
+
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { walk } from "../extract.mjs";
|
|
12
|
+
import { convertClass, loadDeclIndex } from "./convert.mjs";
|
|
13
|
+
import { compatCss } from "./compat.mjs";
|
|
14
|
+
|
|
15
|
+
export const MANIFEST = "omniacss-migrate.bootstrap.json";
|
|
16
|
+
export const EXTENSIONS =["blade.php", "php", "html", "htm", "twig", "erb", "njk", "hbs", "vue", "jsx", "tsx", "js"];
|
|
17
|
+
const MARKUP = /\.(php|html?|twig|erb|njk|hbs|vue|jsx|tsx)$/;
|
|
18
|
+
const IGNORE = new Set(["node_modules", "vendor", ".git", "dist", "build", ".cache", "storage"]);
|
|
19
|
+
|
|
20
|
+
const CLASS_ATTR = /\b(class|className)\s*=\s*(["'])([\s\S]*?)\2/g;
|
|
21
|
+
const TEMPLATE = /<\?[\s\S]*?\?>|\{\{[\s\S]*?\}\}|\{!![\s\S]*?!!\}|\{%[\s\S]*?%\}|\$\{[^}]*\}/g;
|
|
22
|
+
const BS_LINK = /<link\b[^>]*href\s*=\s*["'][^"']*bootstrap(?:\.min)?\.css[^"']*["'][^>]*>/gi;
|
|
23
|
+
const BS_LINK_LINE = /^([ \t]*)(<link\b[^>]*href\s*=\s*["']([^"']*?)bootstrap(?:\.min)?\.css[^"']*["'][^>]*>)/gim;
|
|
24
|
+
|
|
25
|
+
/** Classes whose OmniaUI twin depends on the element (<select class="form-control">, <input type="radio" class="form-check-input">). */
|
|
26
|
+
const BY_ELEMENT = {
|
|
27
|
+
"form-control": (el) => (el.tag === "select" ? "oc-select" : null),
|
|
28
|
+
"form-check-input": (el) => (/\btype\s*=\s*["']radio["']/i.test(el.attrs) ? "oc-radio" : null),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Tag name and attribute text of the element whose class attribute starts at `offset`. */
|
|
32
|
+
function elementAt(text, offset) {
|
|
33
|
+
const open = text.lastIndexOf("<", offset);
|
|
34
|
+
const close = text.indexOf(">", offset);
|
|
35
|
+
const attrs = text.slice(open, close < 0 ? undefined : close);
|
|
36
|
+
return { tag: (attrs.match(/^<([a-zA-Z][\w-]*)/)?.[1] || "").toLowerCase(), attrs };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Where the replacement stylesheets go. `dir` is the part of the Bootstrap href before the file name, template code
|
|
41
|
+
* included ("<?php echo ASSETS ?>bootstrap/css/", "assets/css/"); the bootstrap/, dist/ and css/ folders are dropped.
|
|
42
|
+
*/
|
|
43
|
+
export function linkHrefs(dir, o = {}) {
|
|
44
|
+
const base = /^(https?:)?\/\//.test(dir) ? "" : dir.replace(/(?:bootstrap[^/]*\/)?(?:dist\/)?(?:css\/)?$/, "");
|
|
45
|
+
return [o.omniaHref || `${base}css/omnia.css`, o.omniauiHref || `${base}css/omniaui.css`, o.compatHref || `${base}css/bootstrap-compat.css`];
|
|
46
|
+
}
|
|
47
|
+
const BS_SCRIPT = /<script\b[^>]*src\s*=\s*["'][^"']*bootstrap(?:\.bundle)?(?:\.min)?\.js[^"']*["'][^>]*>\s*<\/script>/gi;
|
|
48
|
+
const DATA_TOGGLE = /\bdata-(?:bs-)?toggle\s*=\s*["']([^"']+)["']/g;
|
|
49
|
+
const CLASSLIST = /classList\.(?:add|remove|toggle|contains|replace)\(([^)]*)\)/g;
|
|
50
|
+
|
|
51
|
+
/** Bootstrap major version: `--bs`, then package.json, a <link> URL, a local bootstrap.css header, then the markup. */
|
|
52
|
+
export async function detectVersion(dir, files, texts) {
|
|
53
|
+
try {
|
|
54
|
+
const pkg = JSON.parse(await fs.readFile(path.join(dir, "package.json"), "utf8"));
|
|
55
|
+
const range = { ...pkg.dependencies, ...pkg.devDependencies }.bootstrap;
|
|
56
|
+
const m = range && String(range).match(/(\d)/);
|
|
57
|
+
if (m) return { version: +m[1], source: "package.json" };
|
|
58
|
+
} catch { /* no package.json */ }
|
|
59
|
+
for (const [file, text] of texts) {
|
|
60
|
+
for (const link of text.match(BS_LINK) || []) {
|
|
61
|
+
const m = link.match(/bootstrap[@/-]v?([345])\.|\/([345])\.\d+\.\d+\//);
|
|
62
|
+
if (m) return { version: +(m[1] || m[2]), source: path.relative(dir, file) };
|
|
63
|
+
const href = link.match(/href\s*=\s*["']([^"']+)["']/)?.[1];
|
|
64
|
+
if (href && !/^(https?:)?\/\//.test(href)) {
|
|
65
|
+
const local = [path.resolve(path.dirname(file), href.split("?")[0]), path.resolve(dir, href.replace(/^\//, "").split("?")[0])];
|
|
66
|
+
for (const p of local) {
|
|
67
|
+
const head = await fs.readFile(p, "utf8").then((t) => t.slice(0, 400)).catch(() => "");
|
|
68
|
+
const h = head.match(/Bootstrap\s+v([345])\./i);
|
|
69
|
+
if (h) return { version: +h[1], source: path.relative(dir, p) };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const all = [...texts.values()].join("\n");
|
|
75
|
+
if (/data-bs-/.test(all)) return { version: 5, source: "data-bs-* attributes" };
|
|
76
|
+
if (/\bcol-xs-|\bpanel-heading\b|\bglyphicon\b/.test(all)) return { version: 3, source: "Bootstrap 3 class names" };
|
|
77
|
+
return { version: 5, source: "default" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Rewrite one class attribute value. Template code (<?php ?>, {{ }}, {% %}) is left untouched. */
|
|
81
|
+
function rewriteValue(value, convert, tally) {
|
|
82
|
+
let out = "", last = 0, changed = false;
|
|
83
|
+
const parts = [];
|
|
84
|
+
for (const t of value.matchAll(TEMPLATE)) { parts.push({ text: value.slice(last, t.index), code: false }); parts.push({ text: t[0], code: true }); last = t.index + t[0].length; }
|
|
85
|
+
parts.push({ text: value.slice(last), code: false });
|
|
86
|
+
for (let i = 0; i < parts.length; i++) {
|
|
87
|
+
const p = parts[i];
|
|
88
|
+
if (p.code) { out += p.text; continue; }
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
const rebuilt = p.text.replace(/\S+/g, (tok, at) => {
|
|
91
|
+
// a token glued to template code (col-<?= $n ?>) is only half a class: leave it
|
|
92
|
+
const glued = (at === 0 && i > 0) || (at + tok.length === p.text.length && i < parts.length - 1);
|
|
93
|
+
if (glued || !/^-?[A-Za-z][\w-]*$/.test(tok)) return tok;
|
|
94
|
+
const r = convert(tok);
|
|
95
|
+
tally(tok, r);
|
|
96
|
+
if (r.to === undefined) { seen.add(tok); return tok; }
|
|
97
|
+
const words = r.to.split(/\s+/).filter((w) => w && !seen.has(w));
|
|
98
|
+
words.forEach((w) => seen.add(w));
|
|
99
|
+
if (r.to !== tok) changed = true;
|
|
100
|
+
return words.join(" ");
|
|
101
|
+
});
|
|
102
|
+
out += rebuilt.replace(/ {2,}/g, " ");
|
|
103
|
+
}
|
|
104
|
+
if (!changed) return { value, changed };
|
|
105
|
+
// a removed first/last class must not leave a space behind
|
|
106
|
+
if (!/^\s/.test(value)) out = out.trimStart();
|
|
107
|
+
if (!/\s$/.test(value)) out = out.trimEnd();
|
|
108
|
+
return { value: out, changed };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {string} dir project root
|
|
113
|
+
* @param {{ write?: boolean, version?: 3|4|5, breakpoints?: "omnia"|"exact", compatFile?: string,
|
|
114
|
+
* omniaHref?: string, omniauiHref?: string, compatHref?: string, cwd?: string }} [o]
|
|
115
|
+
*/
|
|
116
|
+
export async function migrateBootstrap(dir, o = {}) {
|
|
117
|
+
const files = await walk(dir, { extensions: EXTENSIONS, ignore: IGNORE });
|
|
118
|
+
const texts = new Map();
|
|
119
|
+
for (const f of files) texts.set(f, await fs.readFile(f, "utf8").catch(() => ""));
|
|
120
|
+
const detected = o.version ? { version: o.version, source: "--bs" } : await detectVersion(dir, files, texts);
|
|
121
|
+
const ctx = { version: detected.version, breakpoints: o.breakpoints || "omnia", index: loadDeclIndex(o.cwd || dir) };
|
|
122
|
+
// Classes an earlier run produced (mt-1 is OmniaCSS 1rem now, not Bootstrap's 0.25rem): never convert them again.
|
|
123
|
+
const manifestFile = path.join(dir, MANIFEST);
|
|
124
|
+
const done = new Set(JSON.parse(await fs.readFile(manifestFile, "utf8").catch(() => "{}")).produced || []);
|
|
125
|
+
const memo = new Map();
|
|
126
|
+
const convert = (t) => {
|
|
127
|
+
if (!memo.has(t)) memo.set(t, done.has(t) ? { kind: "custom" } : convertClass(t, ctx));
|
|
128
|
+
return memo.get(t);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const report = {
|
|
132
|
+
dir, write: !!o.write, version: detected.version, versionSource: detected.source, breakpoints: ctx.breakpoints,
|
|
133
|
+
files: [], counts: { utility: 0, component: 0, grid: 0, compat: 0, manual: 0, state: 0, inactive: 0, custom: 0 },
|
|
134
|
+
mapping: new Map(), grid: new Set(), compat: new Map(), manual: new Map(), inactive: new Map(), states: new Map(),
|
|
135
|
+
links: 0, scripts: [], toggles: new Map(), classList: new Map(),
|
|
136
|
+
};
|
|
137
|
+
const bump = (map, key, extra) => { const e = map.get(key) || { n: 0, ...extra }; e.n++; map.set(key, e); };
|
|
138
|
+
const tally = (tok, r) => {
|
|
139
|
+
if (r.kind === "unmapped") { report.counts.compat++; bump(report.compat, tok); return; }
|
|
140
|
+
report.counts[r.kind]++;
|
|
141
|
+
if (r.kind === "grid") report.grid.add(tok);
|
|
142
|
+
if (r.kind === "manual") {
|
|
143
|
+
bump(report.manual, r.hint, { examples: new Set() });
|
|
144
|
+
report.manual.get(r.hint).examples.add(tok);
|
|
145
|
+
}
|
|
146
|
+
if (r.kind === "inactive") bump(report.inactive, tok);
|
|
147
|
+
if (r.kind === "state") bump(report.states, tok);
|
|
148
|
+
if ((r.kind === "utility" || r.kind === "component") && r.to !== tok) report.mapping.set(tok, r.to);
|
|
149
|
+
};
|
|
150
|
+
for (const [file, text] of texts) {
|
|
151
|
+
let out = text, edits = 0;
|
|
152
|
+
if (MARKUP.test(file)) {
|
|
153
|
+
out = out.replace(CLASS_ATTR, (all, attr, q, value, offset) => {
|
|
154
|
+
const el = elementAt(text, offset);
|
|
155
|
+
const convertForEdit = (t) => {
|
|
156
|
+
const special = BY_ELEMENT[t]?.(el);
|
|
157
|
+
if (special) return { kind: "component", to: special };
|
|
158
|
+
// "unmapped" classes keep their name: the compat stylesheet serves them
|
|
159
|
+
const r = convert(t);
|
|
160
|
+
return r.kind === "unmapped" ? { ...r, to: undefined } : r;
|
|
161
|
+
};
|
|
162
|
+
const r = rewriteValue(value, convertForEdit, tally);
|
|
163
|
+
if (!r.changed) return all;
|
|
164
|
+
edits++;
|
|
165
|
+
return `${attr}=${q}${r.value}${q}`;
|
|
166
|
+
});
|
|
167
|
+
out = out.replace(BS_LINK_LINE, (all, indent, link, href) => {
|
|
168
|
+
report.links++; edits++;
|
|
169
|
+
const hrefs = linkHrefs(href, o);
|
|
170
|
+
report.hrefs = hrefs;
|
|
171
|
+
const lines = hrefs.map((h) => `${indent}<link rel="stylesheet" href="${h}">`);
|
|
172
|
+
return `${indent}<!-- omniacss-migrate: was ${link.replace(/--/g, "- -")} -->\n${lines.join("\n")}`;
|
|
173
|
+
});
|
|
174
|
+
for (const s of text.match(BS_SCRIPT) || []) report.scripts.push(path.relative(dir, file));
|
|
175
|
+
for (const t of text.matchAll(DATA_TOGGLE)) bump(report.toggles, t[1]);
|
|
176
|
+
}
|
|
177
|
+
for (const c of text.matchAll(CLASSLIST)) {
|
|
178
|
+
for (const s of c[1].matchAll(/["']([\w-]+)["']/g)) {
|
|
179
|
+
const r = convert(s[1]);
|
|
180
|
+
if (r.kind === "utility" || r.kind === "component") bump(report.classList, `${s[1]} -> ${r.to || "(removed)"}`, { file: path.relative(dir, file) });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (out !== text) {
|
|
184
|
+
report.files.push({ file: path.relative(dir, file), edits });
|
|
185
|
+
if (o.write) await fs.writeFile(file, out);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Older-Bootstrap classes: does the project's own CSS still style them? (then they stay useful)
|
|
190
|
+
const ownCss = (await Promise.all((await walk(dir, { extensions: ["css", "scss"], ignore: IGNORE }))
|
|
191
|
+
.filter((f) => !/bootstrap/i.test(path.basename(f)) && !/\.min\.css$/.test(f)) // own CSS, not Bootstrap or minified vendor files
|
|
192
|
+
.map((f) => fs.readFile(f, "utf8").catch(() => "")))).join("\n");
|
|
193
|
+
for (const [cls, e] of report.inactive) e.styled = new RegExp(`\\.${cls}(?![\\w-])`).test(ownCss);
|
|
194
|
+
|
|
195
|
+
if (o.write && report.mapping.size) {
|
|
196
|
+
for (const to of report.mapping.values()) for (const w of to.split(/\s+/)) if (w) done.add(w);
|
|
197
|
+
await fs.writeFile(manifestFile, JSON.stringify({ note: "Written by omniacss-migrate bootstrap: classes it produced, skipped on later runs.", version: report.version, produced: [...done].sort() }, null, 1) + "\n");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
report.compatCss = report.grid.size || report.compat.size ? compatCss(report.grid, report.compat.keys(), ctx) : "";
|
|
201
|
+
if (o.write && report.compatCss) {
|
|
202
|
+
report.compatFile = path.resolve(dir, o.compatFile || "bootstrap-compat.css");
|
|
203
|
+
await fs.writeFile(report.compatFile, report.compatCss);
|
|
204
|
+
}
|
|
205
|
+
return report;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const top = (map, n) => [...map.entries()].sort((a, b) => b[1].n - a[1].n).slice(0, n);
|
|
209
|
+
|
|
210
|
+
export function formatBootstrap(r, { top: n = 25 } = {}) {
|
|
211
|
+
const c = r.counts;
|
|
212
|
+
const total = Object.values(c).reduce((a, b) => a + b, 0) - c.custom;
|
|
213
|
+
const L = [];
|
|
214
|
+
L.push(`Bootstrap ${r.version} (${r.versionSource}), breakpoints: ${r.breakpoints}`);
|
|
215
|
+
L.push(`${r.write ? "Changed" : "Dry run, would change"} ${r.files.length} files under ${r.dir}`);
|
|
216
|
+
for (const f of r.files.slice(0, 15)) L.push(` ${f.file} (${f.edits})`);
|
|
217
|
+
if (r.files.length > 15) L.push(` ... and ${r.files.length - 15} more files`);
|
|
218
|
+
L.push("", `Bootstrap class occurrences: ${total}`);
|
|
219
|
+
L.push(` ${String(c.utility).padStart(5)} utilities -> OmniaCSS`);
|
|
220
|
+
L.push(` ${String(c.component).padStart(5)} components -> OmniaUI (needs @birdycom/omniaui CSS)`);
|
|
221
|
+
L.push(` ${String(c.grid).padStart(5)} grid classes kept (served by the compat stylesheet)`);
|
|
222
|
+
L.push(` ${String(c.compat).padStart(5)} utilities without an OmniaCSS twin kept (served by the compat stylesheet)`);
|
|
223
|
+
L.push(` ${String(c.manual).padStart(5)} need a manual step (JavaScript components, icon fonts)`);
|
|
224
|
+
L.push(` ${String(c.inactive).padStart(5)} older-Bootstrap classes that do nothing in Bootstrap ${r.version} (left as they are)`);
|
|
225
|
+
L.push(` ${String(c.state).padStart(5)} state classes toggled by JavaScript (active, show, …) left as they are`);
|
|
226
|
+
if (r.mapping.size) {
|
|
227
|
+
L.push("", "Examples of the rewrite:");
|
|
228
|
+
for (const [from, to] of [...r.mapping].slice(0, n)) L.push(` ${from.padEnd(28)} -> ${to || "(removed)"}`);
|
|
229
|
+
}
|
|
230
|
+
if (r.compat.size) L.push("", `Kept with compat CSS: ${top(r.compat, n).map(([k, v]) => `${k} (${v.n})`).join(", ")}`);
|
|
231
|
+
if (r.manual.size) {
|
|
232
|
+
L.push("", "Manual steps:");
|
|
233
|
+
for (const [hint, v] of top(r.manual, n)) L.push(` ${String(v.n).padStart(4)} ${[...v.examples].slice(0, 4).join(", ")}\n ${hint}`);
|
|
234
|
+
}
|
|
235
|
+
if (r.toggles.size) L.push("", `data-(bs-)toggle in the markup: ${top(r.toggles, n).map(([k, v]) => `${k} (${v.n})`).join(", ")} — Bootstrap's JavaScript still drives these until they are rebuilt natively.`);
|
|
236
|
+
if (r.scripts.length) L.push(`Bootstrap JavaScript is loaded in: ${[...new Set(r.scripts)].join(", ")}. Remove it once no data-bs-toggle is left.`);
|
|
237
|
+
if (r.classList.size) {
|
|
238
|
+
L.push("", "JavaScript toggles Bootstrap classes that the markup now spells differently — update these by hand:");
|
|
239
|
+
for (const [k, v] of top(r.classList, n)) L.push(` ${v.file}: ${k}`);
|
|
240
|
+
}
|
|
241
|
+
if (r.inactive.size) {
|
|
242
|
+
const list = (styled) => top(new Map([...r.inactive].filter(([, v]) => !!v.styled === styled)), n).map(([k, v]) => `${k} (${v.n})`).join(", ");
|
|
243
|
+
const styled = list(true), unused = list(false);
|
|
244
|
+
if (styled) L.push("", `Older-Bootstrap classes your own CSS still styles (kept): ${styled}`);
|
|
245
|
+
if (unused) L.push("", `Older-Bootstrap classes nothing styles (safe to delete): ${unused}`);
|
|
246
|
+
}
|
|
247
|
+
if (r.links) {
|
|
248
|
+
L.push("", `Replaced ${r.links} Bootstrap <link>(s). Put the three stylesheets where the new links point:`);
|
|
249
|
+
L.push(` ${r.hrefs[0]} npx omniacss build -o <that path> (only the classes you use; md:flex and other variants are generated on demand)`);
|
|
250
|
+
L.push(` ${r.hrefs[1]} the OmniaUI component CSS (@birdycom/omniaui)`);
|
|
251
|
+
L.push(` ${r.hrefs[2]} the compat stylesheet below`);
|
|
252
|
+
} else {
|
|
253
|
+
L.push("", "No Bootstrap <link> found (bundler or CDN in a template?): include omnia.css (npx omniacss build), the OmniaUI CSS and the compat stylesheet yourself.");
|
|
254
|
+
}
|
|
255
|
+
if (r.compatFile) L.push(`Wrote ${r.compatFile}`);
|
|
256
|
+
else if (r.compatCss) L.push(`A compat stylesheet of ${(r.compatCss.length / 1024).toFixed(1)} KB will be written (bootstrap-compat.css).`);
|
|
257
|
+
if (!r.write) L.push("", "Run again with --write to apply.");
|
|
258
|
+
return L.join("\n");
|
|
259
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// File: packages/migrate/src/bootstrap/rules.mjs
|
|
2
|
+
// Knowledge about Bootstrap 3, 4 and 5 class names: the CSS declarations of each utility (matched against the
|
|
3
|
+
// OmniaCSS index, so names never matter), the OmniaUI component for each component class, and the classes that
|
|
4
|
+
// need JavaScript or a manual step.
|
|
5
|
+
// Map: breakpoints -> colors -> spacing/value scales -> utility(decls) -> components -> manual hints
|
|
6
|
+
// Author: Serge Cools | Framework: OmniaCSS
|
|
7
|
+
|
|
8
|
+
/** Bootstrap breakpoints in px (min-width). In Bootstrap 3, `xs` is the base (no media query). */
|
|
9
|
+
export const BREAKPOINTS = {
|
|
10
|
+
3: { xs: 0, sm: 768, md: 992, lg: 1200 },
|
|
11
|
+
4: { xs: 0, sm: 576, md: 768, lg: 992, xl: 1200 },
|
|
12
|
+
5: { xs: 0, sm: 576, md: 768, lg: 992, xl: 1200, xxl: 1400 },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** Container max-width per breakpoint (px). */
|
|
16
|
+
export const CONTAINERS = {
|
|
17
|
+
3: { sm: 750, md: 970, lg: 1170 },
|
|
18
|
+
4: { sm: 540, md: 720, lg: 960, xl: 1140 },
|
|
19
|
+
5: { sm: 540, md: 720, lg: 960, xl: 1140, xxl: 1320 },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** OmniaCSS viewport breakpoints in px (packages/core/src/variants.mjs: 36rem, 50rem, 64rem, 80rem, 96rem). */
|
|
23
|
+
export const OMNIA_BREAKPOINTS = { sm: 576, md: 800, lg: 1024, xl: 1280, "2xl": 1536 };
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Bootstrap theme colors -> nearest Omnia palette token (OKLab distance against the Omnia palette in oc-colors.css,
|
|
27
|
+
* computed once from the default Bootstrap hex values). White and black use the exact Omnia tokens.
|
|
28
|
+
*/
|
|
29
|
+
export const COLORS = {
|
|
30
|
+
5: {
|
|
31
|
+
primary: "blue-30", secondary: "twilightblue-10", success: "green-50", danger: "brown-30", warning: "orange-30",
|
|
32
|
+
info: "skycyan-60", light: "snow-100", dark: "shallowblue-40", body: "shallowblue-40", muted: "twilightblue-10",
|
|
33
|
+
white: "white-50", black: "black-50", border: "snow-60",
|
|
34
|
+
},
|
|
35
|
+
3: {
|
|
36
|
+
primary: "skycyan-95", secondary: "twilightblue-10", success: "green-10", danger: "brown-20", warning: "orange-50",
|
|
37
|
+
info: "skycyan-60", light: "snow-100", dark: "shallowblue-40", body: "shallowblue-40", muted: "twilightblue-10",
|
|
38
|
+
white: "white-50", black: "black-50", border: "snow-60",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
COLORS[4] = COLORS[5];
|
|
42
|
+
|
|
43
|
+
const SPACE = { 0: "0", 1: "0.25rem", 2: "0.5rem", 3: "1rem", 4: "1.5rem", 5: "3rem", auto: "auto" };
|
|
44
|
+
const SIDES = {
|
|
45
|
+
"": ["margin"], t: ["margin-top"], b: ["margin-bottom"], s: ["margin-left"], e: ["margin-right"], l: ["margin-left"], r: ["margin-right"],
|
|
46
|
+
x: ["margin-left", "margin-right"], y: ["margin-top", "margin-bottom"],
|
|
47
|
+
};
|
|
48
|
+
const DISPLAY = ["none", "inline", "inline-block", "block", "grid", "inline-grid", "table", "table-row", "table-cell", "flex", "inline-flex"];
|
|
49
|
+
const JUSTIFY = { start: "flex-start", end: "flex-end", center: "center", between: "space-between", around: "space-around", evenly: "space-evenly" };
|
|
50
|
+
const ALIGN = { start: "flex-start", end: "flex-end", center: "center", baseline: "baseline", stretch: "stretch" };
|
|
51
|
+
const WEIGHT = { light: 300, lighter: "lighter", normal: 400, medium: 500, semibold: 600, bold: 700, bolder: "bolder" };
|
|
52
|
+
const FS = { 1: "2.5rem", 2: "2rem", 3: "1.75rem", 4: "1.5rem", 5: "1.25rem", 6: "1rem" };
|
|
53
|
+
const ROUNDED = { 5: { "": "0.375rem", 0: "0", 1: "0.25rem", 2: "0.375rem", 3: "0.5rem", 4: "1rem", 5: "2rem", circle: "50%", pill: "50rem" }, 3: { "": "0.25rem", 0: "0", circle: "50%", pill: "50rem" } };
|
|
54
|
+
const RATIO = { "1x1": "1 / 1", "4x3": "4 / 3", "16x9": "16 / 9", "21x9": "21 / 9" };
|
|
55
|
+
|
|
56
|
+
const d = (...pairs) => [pairs.map(([p, v]) => `${p}:${v}`)];
|
|
57
|
+
const one = (p, v) => d([p, v]);
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* CSS declarations of a Bootstrap utility without its breakpoint infix.
|
|
61
|
+
* Returns a list of declaration groups: each group must map to one OmniaCSS class (the result is one class per group).
|
|
62
|
+
* @param {string} name e.g. "mt-3", "d-none", "text-muted"
|
|
63
|
+
* @param {3|4|5} v Bootstrap major version
|
|
64
|
+
* @returns {string[][] | null}
|
|
65
|
+
*/
|
|
66
|
+
export function utilityDecls(name, v) {
|
|
67
|
+
let m;
|
|
68
|
+
const c = COLORS[v];
|
|
69
|
+
// spacing: m-3, mt-n1, px-2, ms-auto, ml-auto (v4), gap-3, row-gap-2
|
|
70
|
+
if ((m = name.match(/^([mp])([tbselrxy]?)-(n?)([0-5]|auto)$/))) {
|
|
71
|
+
const [, mp, side, neg, val] = m;
|
|
72
|
+
if (mp === "p" && (val === "auto" || neg)) return null;
|
|
73
|
+
if ((side === "s" || side === "e") && v < 5) return null;
|
|
74
|
+
if ((side === "l" || side === "r") && v >= 5) return null;
|
|
75
|
+
const value = neg ? `-${SPACE[val]}` : SPACE[val];
|
|
76
|
+
return [SIDES[side].map((p) => `${mp === "p" ? p.replace("margin", "padding") : p}:${value}`)];
|
|
77
|
+
}
|
|
78
|
+
if ((m = name.match(/^(gap|row-gap|column-gap)-([0-5])$/))) return one(m[1], SPACE[m[2]]);
|
|
79
|
+
if ((m = name.match(/^d-(.+)$/)) && DISPLAY.includes(m[1])) return one("display", m[1]);
|
|
80
|
+
// flex
|
|
81
|
+
if ((m = name.match(/^flex-(row|row-reverse|column|column-reverse)$/))) return one("flex-direction", m[1]);
|
|
82
|
+
if ((m = name.match(/^flex-(wrap|nowrap|wrap-reverse)$/))) return one("flex-wrap", m[1]);
|
|
83
|
+
if (name === "flex-fill") return one("flex", "1 1 auto");
|
|
84
|
+
if ((m = name.match(/^flex-(grow|shrink)-([01])$/))) return one(`flex-${m[1]}`, m[2]);
|
|
85
|
+
if ((m = name.match(/^justify-content-(.+)$/)) && JUSTIFY[m[1]]) return one("justify-content", JUSTIFY[m[1]]);
|
|
86
|
+
if ((m = name.match(/^align-(items|self|content)-(.+)$/))) {
|
|
87
|
+
const val = m[1] === "content" ? { ...ALIGN, between: "space-between", around: "space-around" }[m[2]] : { ...ALIGN, auto: "auto" }[m[2]];
|
|
88
|
+
return val ? one(`align-${m[1]}`, val) : null;
|
|
89
|
+
}
|
|
90
|
+
if ((m = name.match(/^order-([0-5]|first|last)$/))) return one("order", { first: -1, last: 6 }[m[1]] ?? m[1]);
|
|
91
|
+
// text
|
|
92
|
+
if ((m = name.match(/^text-(start|end|left|right|center|justify)$/))) return one("text-align", { start: "left", end: "right" }[m[1]] ?? m[1]);
|
|
93
|
+
if ((m = name.match(/^text-(lowercase|uppercase|capitalize)$/))) return one("text-transform", m[1]);
|
|
94
|
+
if (name === "text-nowrap") return one("white-space", "nowrap");
|
|
95
|
+
if (name === "text-wrap") return one("white-space", "normal");
|
|
96
|
+
if (name === "text-truncate") return d(["overflow", "hidden"], ["text-overflow", "ellipsis"], ["white-space", "nowrap"]);
|
|
97
|
+
if (name === "text-break") return one("overflow-wrap", "break-word");
|
|
98
|
+
if ((m = name.match(/^text-decoration-(none|underline|line-through)$/))) return one("text-decoration", m[1]);
|
|
99
|
+
if ((m = name.match(/^(?:fw|font-weight)-(.+)$/)) && WEIGHT[m[1]]) return one("font-weight", WEIGHT[m[1]]);
|
|
100
|
+
if ((m = name.match(/^(?:fst|font)-(italic|normal)$/))) return one("font-style", m[1]);
|
|
101
|
+
if ((m = name.match(/^fs-([1-6])$/))) return one("font-size", FS[m[1]]);
|
|
102
|
+
if ((m = name.match(/^lh-(1|sm|base|lg)$/))) return one("line-height", { 1: 1, sm: 1.25, base: 1.5, lg: 2 }[m[1]]);
|
|
103
|
+
// colors: text-*, bg-*, border-* (theme names -> Omnia tokens)
|
|
104
|
+
if ((m = name.match(/^text-(.+)$/)) && c[m[1]] && m[1] !== "border") return one("color", `var(--color-${c[m[1]]})`);
|
|
105
|
+
if ((m = name.match(/^bg-(.+)$/)) && c[m[1]] && !["muted", "border"].includes(m[1])) return one("background-color", `var(--color-${c[m[1]]})`);
|
|
106
|
+
if (name === "bg-transparent") return one("background-color", "transparent");
|
|
107
|
+
// borders
|
|
108
|
+
if (name === "border") return [["border-width:1px"], ["border-style:solid"], [`border-color:var(--color-${c.border})`]];
|
|
109
|
+
if (name === "border-0") return one("border-width", "0");
|
|
110
|
+
if ((m = name.match(/^border-(top|bottom|start|end|left|right)$/))) {
|
|
111
|
+
const side = { start: "left", end: "right" }[m[1]] ?? m[1];
|
|
112
|
+
return [[`border-${side}-width:1px`], [`border-${side}-style:solid`], [`border-${side}-color:var(--color-${c.border})`]];
|
|
113
|
+
}
|
|
114
|
+
if ((m = name.match(/^border-([1-5])$/))) return one("border-width", `${m[1]}px`);
|
|
115
|
+
if ((m = name.match(/^border-(.+)$/)) && c[m[1]] && m[1] !== "border") return one("border-color", `var(--color-${c[m[1]]})`);
|
|
116
|
+
if ((m = name.match(/^rounded(?:-(.+))?$/))) {
|
|
117
|
+
const r = (ROUNDED[v] || ROUNDED[5])[m[1] ?? ""] ?? ROUNDED[5][m[1] ?? ""];
|
|
118
|
+
return r ? one("border-radius", r) : null;
|
|
119
|
+
}
|
|
120
|
+
// sizing
|
|
121
|
+
if ((m = name.match(/^([wh])-(25|50|75|100|auto)$/))) return one(m[1] === "w" ? "width" : "height", m[2] === "auto" ? "auto" : `${m[2]}%`);
|
|
122
|
+
if (name === "mw-100") return one("max-width", "100%");
|
|
123
|
+
if (name === "mh-100") return one("max-height", "100%");
|
|
124
|
+
if (name === "vw-100") return one("width", "100vw");
|
|
125
|
+
if (name === "vh-100") return one("height", "100vh");
|
|
126
|
+
if (name === "min-vw-100") return one("min-width", "100vw");
|
|
127
|
+
if (name === "min-vh-100") return one("min-height", "100vh");
|
|
128
|
+
// position
|
|
129
|
+
if ((m = name.match(/^position-(static|relative|absolute|fixed|sticky)$/))) return one("position", m[1]);
|
|
130
|
+
if ((m = name.match(/^(top|bottom|start|end)-(0|50|100)$/))) return one({ start: "left", end: "right" }[m[1]] ?? m[1], m[2] === "0" ? "0" : `${m[2]}%`);
|
|
131
|
+
if (name === "fixed-top") return [["position:fixed"], ["top:0"], ["left:0"], ["right:0"], ["z-index:1030"]];
|
|
132
|
+
if (name === "sticky-top") return [["position:sticky"], ["top:0"], ["z-index:1020"]];
|
|
133
|
+
// overflow, visibility, opacity, float, interaction
|
|
134
|
+
if ((m = name.match(/^overflow-(x-|y-)?(auto|hidden|visible|scroll)$/))) return one(`overflow${m[1] ? "-" + m[1][0] : ""}`, m[2]);
|
|
135
|
+
if (name === "visible") return one("visibility", "visible");
|
|
136
|
+
if (name === "invisible") return one("visibility", "hidden");
|
|
137
|
+
if ((m = name.match(/^opacity-(0|25|50|75|100)$/))) return one("opacity", +m[1] / 100);
|
|
138
|
+
if ((m = name.match(/^(?:float|pull)-(start|end|left|right|none)$/))) return one("float", { start: "left", end: "right" }[m[1]] ?? m[1]);
|
|
139
|
+
if ((m = name.match(/^user-select-(all|auto|none)$/))) return one("user-select", m[1]);
|
|
140
|
+
if ((m = name.match(/^pe-(none|auto)$/))) return one("pointer-events", m[1]);
|
|
141
|
+
if ((m = name.match(/^object-fit-(contain|cover|fill|scale|none)$/))) return one("object-fit", m[1] === "scale" ? "scale-down" : m[1]);
|
|
142
|
+
if ((m = name.match(/^align-(baseline|top|middle|bottom|text-top|text-bottom)$/))) return one("vertical-align", m[1]);
|
|
143
|
+
if ((m = name.match(/^z-(n1|0|1|2|3)$/))) return one("z-index", m[1] === "n1" ? -1 : m[1]);
|
|
144
|
+
if ((m = name.match(/^ratio-(\d+x\d+)$/)) && RATIO[m[1]]) return one("aspect-ratio", RATIO[m[1]]);
|
|
145
|
+
if (name === "img-fluid" || name === "img-responsive") return [["max-width:100%"], ["height:auto"]];
|
|
146
|
+
if (name === "center-block") return [["display:block"], ["margin-left:auto", "margin-right:auto"]];
|
|
147
|
+
if (name === "shadow-none") return one("box-shadow", "none");
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Classes that map by name (same intent, OmniaCSS values): shadows, screen-reader helpers. */
|
|
152
|
+
export const RENAMES = {
|
|
153
|
+
"shadow-sm": "shadow-sm", shadow: "shadow-md", "shadow-lg": "shadow-lg",
|
|
154
|
+
"visually-hidden": "sr-only", "sr-only": "sr-only", "visually-hidden-focusable": "sr-only focus:not-sr-only", "sr-only-focusable": "sr-only focus:not-sr-only",
|
|
155
|
+
hidden: "d-none", "img-circle": "round-full", "img-rounded": "round-2",
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** Bootstrap component class -> OmniaUI class(es). An empty string removes the class (its role is covered elsewhere). */
|
|
159
|
+
export const COMPONENTS = {
|
|
160
|
+
btn: "oc-btn", "btn-primary": "", "btn-secondary": "oc-btn--secondary", "btn-success": "oc-btn--success", "btn-danger": "oc-btn--destructive",
|
|
161
|
+
"btn-warning": "oc-btn--secondary", "btn-info": "oc-btn--secondary", "btn-light": "oc-btn--secondary", "btn-dark": "oc-btn--secondary",
|
|
162
|
+
"btn-default": "oc-btn--outline", "btn-link": "oc-btn--link", "btn-sm": "oc-btn--sm", "btn-xs": "oc-btn--xs", "btn-lg": "oc-btn--lg",
|
|
163
|
+
"btn-block": "w-full", "btn-group": "oc-btn-group",
|
|
164
|
+
card: "oc-card", "card-body": "oc-card-content", "card-header": "oc-card-header", "card-footer": "oc-card-footer", "card-title": "oc-card-title",
|
|
165
|
+
"card-text": "oc-card-text", "card-subtitle": "oc-card-description", "card-link": "oc-card-link",
|
|
166
|
+
panel: "oc-card", "panel-heading": "oc-card-header", "panel-body": "oc-card-content", "panel-footer": "oc-card-footer", "panel-title": "oc-card-title",
|
|
167
|
+
"panel-default": "", "panel-primary": "", "panel-success": "", "panel-info": "", "panel-warning": "", "panel-danger": "", well: "oc-card",
|
|
168
|
+
alert: "oc-alert", "alert-success": "oc-alert--success", "alert-info": "oc-alert--info", "alert-warning": "oc-alert--warning", "alert-danger": "oc-alert--danger",
|
|
169
|
+
"alert-primary": "oc-alert--info", "alert-secondary": "oc-alert--info", "alert-light": "oc-alert--info", "alert-dark": "oc-alert--info", "alert-heading": "oc-alert-title",
|
|
170
|
+
badge: "oc-badge", "badge-primary": "", "badge-secondary": "oc-badge--secondary", "badge-success": "oc-badge--success", "badge-danger": "oc-badge--destructive",
|
|
171
|
+
"badge-warning": "oc-badge--warning", "badge-info": "oc-badge--info", "badge-light": "oc-badge--outline", "badge-dark": "oc-badge--secondary",
|
|
172
|
+
"badge-pill": "round-full", "rounded-pill": "round-full",
|
|
173
|
+
"label-default": "oc-badge oc-badge--secondary", "label-primary": "oc-badge", "label-success": "oc-badge oc-badge--success", "label-info": "oc-badge oc-badge--info",
|
|
174
|
+
"label-warning": "oc-badge oc-badge--warning", "label-danger": "oc-badge oc-badge--destructive",
|
|
175
|
+
"form-control": "oc-input", "form-select": "oc-select", "custom-select": "oc-select", "form-label": "oc-label", "control-label": "oc-label",
|
|
176
|
+
"form-group": "oc-form-group", "form-text": "oc-field-description", "help-block": "oc-field-description", "is-invalid": "oc-input--error",
|
|
177
|
+
"invalid-feedback": "oc-field-error", "form-check-input": "oc-checkbox", "form-check-label": "oc-label oc-label--inline",
|
|
178
|
+
"input-group": "oc-input-group", "input-group-text": "oc-input-addon", "input-group-addon": "oc-input-addon",
|
|
179
|
+
table: "oc-table", "table-striped": "oc-table--striped", "table-borderless": "oc-table--borderless", "table-sm": "oc-table--compact",
|
|
180
|
+
"table-condensed": "oc-table--compact", "table-responsive": "oc-table-wrap",
|
|
181
|
+
breadcrumb: "oc-breadcrumb", "breadcrumb-item": "oc-breadcrumb-item",
|
|
182
|
+
pagination: "oc-pagination", "page-link": "oc-pagination-item", "page-item": "",
|
|
183
|
+
progress: "oc-progress", "progress-bar": "oc-progress-bar", "spinner-border": "oc-spinner", "spinner-grow": "oc-spinner", "spinner-border-sm": "oc-spinner--sm",
|
|
184
|
+
jumbotron: "oc-hero",
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
/** Classes that need JavaScript in Bootstrap, or markup changes: reported with the native OmniaCSS / OmniaUI way. */
|
|
188
|
+
export const MANUAL = [
|
|
189
|
+
[/^modal/, "Modal: use <dialog class=\"oc-modal\"> opened with <button commandfor=\"id\" command=\"show-modal\"> (no JavaScript)"],
|
|
190
|
+
[/^dropdown/, "Dropdown: use a popover with anchor positioning (OmniaUI dropdown-menu)"],
|
|
191
|
+
[/^(collapse|collapsing|accordion)/, "Collapse/accordion: use <details name=\"group\"><summary> (OmniaUI accordion)"],
|
|
192
|
+
[/^carousel/, "Carousel: use a scroll-snap row with .scroll-buttons and .scroll-markers-after (see docs/scroll-snap)"],
|
|
193
|
+
[/^(navbar|nav$|nav-)/, "Navigation: use OmniaUI site-header / navmenu; tabs: OmniaUI tabs (radio + :has())"],
|
|
194
|
+
[/^(tab-content|tab-pane)$/, "Tabs: OmniaUI tabs (radio + :has()) or :target panels (docs/selectors)"],
|
|
195
|
+
[/^(offcanvas)/, "Off-canvas: OmniaUI sheet / drawer on <dialog>"],
|
|
196
|
+
[/^(toast)/, "Toast: OmniaUI toast / toast-stack (popover=\"manual\")"],
|
|
197
|
+
[/^(tooltip|popover)$/, "Tooltip/popover: OmniaUI tooltip (popover + anchor) or hover-card"],
|
|
198
|
+
[/^(glyphicon|bi$|bi-|fa$|fa-)/, "Icon font: replace with inline SVG icons"],
|
|
199
|
+
[/^(list-group)/, "List group: OmniaUI stack-list / item"],
|
|
200
|
+
[/^(embed-responsive|ratio$)/, "Responsive embed: put aspect-16/9 (or another aspect-*) on the iframe itself with w-full"],
|
|
201
|
+
[/^(clearfix)$/, "Clearfix: use flex or grid instead of floats, or flow-root"],
|
|
202
|
+
[/^(thumbnail|img-thumbnail|caption|media|media-body)$/, "Media/thumbnail: OmniaUI media-card / media-row"],
|
|
203
|
+
[/^(close|btn-close)$/, "Close button: OmniaUI oc-modal-close or a ghost button with an icon"],
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
/** State classes toggled by Bootstrap's JavaScript: left in place, reported. */
|
|
207
|
+
export const JS_STATE = new Set(["active", "show", "fade", "in", "collapsed", "disabled", "open"]);
|
|
208
|
+
|
|
209
|
+
/** Grid classes kept as they are and served by the compat stylesheet. */
|
|
210
|
+
export function isGridClass(name) {
|
|
211
|
+
return /^(container(-fluid|-(sm|md|lg|xl|xxl))?|row|row-cols-(\d|auto)|row-cols-(sm|md|lg|xl|xxl)-(\d|auto)|col|col-auto|col-(\d{1,2})|col-(xs|sm|md|lg|xl|xxl)(-(\d{1,2}|auto))?|offset-(\d{1,2})|offset-(sm|md|lg|xl|xxl)-(\d{1,2})|col-(xs|sm|md|lg)-offset-\d{1,2}|col-(xs|sm|md|lg)-(push|pull)-\d{1,2}|g[xy]?-[0-5]|g[xy]?-(sm|md|lg|xl|xxl)-[0-5])$/.test(name);
|
|
212
|
+
}
|