@birdycom/omniacss-migrate 0.11.0 → 0.11.2

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.
@@ -1,212 +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
- }
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
+ }