@innovaccer/design-system 4.27.0 → 4.28.0-1

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/css/gulpfile.js CHANGED
@@ -5,28 +5,10 @@ const sourcemaps = require('gulp-sourcemaps');
5
5
  const concat = require('gulp-concat');
6
6
  const postcssColorMod = require('postcss-color-mod-function');
7
7
  const cleaner = require('gulp-clean');
8
+ const path = require('path');
9
+ const { execFile } = require('child_process');
8
10
 
9
- const materialIcons = './material-design-icons/iconfont/material-icons.css';
10
- const materialFont = './material-design-icons/iconfont/*.{ttf,otf,woff2}';
11
-
12
- const typographyCssPath = [
13
- './src/components/text.module.css',
14
- './src/components/heading.module.css',
15
- './src/components/label.module.css',
16
- './src/components/caption.module.css',
17
- './src/components/subheading.module.css',
18
- ];
19
-
20
- const sources = [
21
- './src/tokens/*.css',
22
- './src/variables/*.css',
23
- materialIcons,
24
- './src/core/*.css',
25
- ...typographyCssPath,
26
- './src/components/*.css',
27
- './src/ai-components/*.css',
28
- './src/utils/*.css',
29
- ];
11
+ const { materialFont, sources } = require('./scripts/css-sources.cjs');
30
12
 
31
13
  function clean() {
32
14
  return gulp.src('./dist/*', { allowEmpty: true }).pipe(cleaner());
@@ -47,10 +29,37 @@ function font() {
47
29
  return gulp.src([materialFont]).pipe(gulp.dest('./dist'));
48
30
  }
49
31
 
50
- exports.build = gulp.series(clean, gulp.parallel(css, font));
32
+ /**
33
+ * Builds `dist/mds-scoped.css` — the same CSS wrapped in `@scope ([data-mds-root])`.
34
+ *
35
+ * Runs out of process because the scoped build is authored as an ES module (it needs
36
+ * postcss' AST directly to hoist `@keyframes` above the `@scope` block, which a
37
+ * streaming gulp pipeline cannot express). Errors propagate through `cb` so a broken
38
+ * scoped build fails `gulp build` rather than passing silently.
39
+ */
40
+ function cssScoped(cb) {
41
+ execFile(
42
+ process.execPath,
43
+ [path.join(__dirname, 'scripts/build-scoped.mjs')],
44
+ { cwd: __dirname },
45
+ (err, stdout, stderr) => {
46
+ if (stdout) process.stdout.write(stdout);
47
+ if (stderr) process.stderr.write(stderr);
48
+ cb(err);
49
+ }
50
+ );
51
+ }
52
+ cssScoped.displayName = 'css:scoped';
53
+
54
+ /** Both bundles read the same sources, so one watch rebuilds both. */
55
+ function watch() {
56
+ return gulp.watch(sources, gulp.parallel(css, cssScoped));
57
+ }
58
+ watch.displayName = 'watch';
51
59
 
60
+ exports.build = gulp.series(clean, gulp.parallel(css, font, cssScoped));
61
+ exports.cssScoped = cssScoped;
52
62
  exports.clean = clean;
63
+ exports.watch = watch;
53
64
 
54
- gulp.task('watch', () => {
55
- gulp.watch(sources, gulp.series(css));
56
- });
65
+ gulp.task('watch', watch);
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "scoped-css",
3
+ "main": "../dist/mds-scoped.css",
4
+ "style": "../dist/mds-scoped.css"
5
+ }
@@ -0,0 +1,313 @@
1
+ /**
2
+ * Builds css/dist/mds-scoped.css — the design-system stylesheet wrapped in
3
+ * `@scope ([data-mds-root])`, so it only styles subtrees the consumer opts into
4
+ * by adding `data-mds-root` to a container element.
5
+ *
6
+ * Composition (not a general selector rewriter):
7
+ * 1. Concatenate the same sources, in the same order, as the default build
8
+ * 2. Hoist globally-named constructs (@keyframes / @font-face / @property /
9
+ * @counter-style) above @scope — they are not scopable and would be dropped
10
+ * 3. Remap :root / html / body / :host -> :scope so tokens and base styles
11
+ * attach to the scope root instead of the document
12
+ * 4. Add a root-anchored variant of every selector, so a rule can also match the
13
+ * scope root itself and not just its descendants (see below)
14
+ * 5. Wrap the remaining rules in @scope (<selector>)
15
+ *
16
+ * This is a CSS-only feature: it requires no changes in `core/`, so the stylesheet can
17
+ * be updated independently of the library version and works with releases already in
18
+ * the wild. Everything it needs to hook onto is markup the components already render
19
+ * (see PORTAL_ROOT_SELECTORS).
20
+ *
21
+ * Nothing is added to the scope root itself. A `[data-mds-root]` element is consumer
22
+ * markup, so anything declared on it would compete with the consumer's own layout —
23
+ * notably, a custom-element scope root (`<ui-shell-app data-mds-root>`) needs its own
24
+ * `display`, which is the consumer's call, not this build's.
25
+ *
26
+ * The scope selector is overridable, e.g. to also scope on web-component hosts:
27
+ * MDS_SCOPE_SELECTOR='[data-mds-root], ui-shell-app' npm run build-css-scoped
28
+ * CSS has no tag-name wildcard, so patterned hosts must either be enumerated
29
+ * here or carry the `data-mds-root` attribute.
30
+ *
31
+ * The default css/dist/index.css build is unchanged.
32
+ */
33
+ import fs from 'node:fs';
34
+ import path from 'node:path';
35
+ import { fileURLToPath } from 'node:url';
36
+ import { createRequire } from 'node:module';
37
+ import postcss from 'postcss';
38
+
39
+ const require = createRequire(import.meta.url);
40
+ const postcssColorMod = require('postcss-color-mod-function');
41
+ const autoprefixer = require('autoprefixer');
42
+ const { sources } = require('./css-sources.cjs');
43
+
44
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
45
+ const cssRoot = path.resolve(__dirname, '..');
46
+
47
+ const SCOPE_SELECTOR = process.env.MDS_SCOPE_SELECTOR || '[data-mds-root]';
48
+ const OUTPUT_FILE = process.env.MDS_SCOPE_OUTPUT || 'mds-scoped.css';
49
+
50
+ /** At-rules whose names live in a global namespace and therefore cannot be scoped. */
51
+ const GLOBAL_AT_RULES = ['keyframes', 'font-face', 'property', 'counter-style'];
52
+
53
+ /**
54
+ * Scope roots for MDS's own portaled overlays — see `portal-roots.cjs` for why each
55
+ * selector looks the way it does. Shared with the test that guards them.
56
+ */
57
+ const { PORTAL_ROOT_SELECTORS } = require('./portal-roots.cjs');
58
+
59
+ function expandGlobs(patterns) {
60
+ const results = [];
61
+ for (const pattern of patterns) {
62
+ if (pattern.includes('*')) {
63
+ const dir = path.join(cssRoot, path.dirname(pattern));
64
+ const fileGlob = path.basename(pattern);
65
+ if (!fs.existsSync(dir)) continue;
66
+ for (const name of fs.readdirSync(dir).sort()) {
67
+ const full = path.join(dir, name);
68
+ if (!fs.statSync(full).isFile()) continue;
69
+ if (fileGlob === '*.css' && name.endsWith('.css')) results.push(full);
70
+ }
71
+ } else {
72
+ results.push(path.join(cssRoot, pattern));
73
+ }
74
+ }
75
+ return results;
76
+ }
77
+
78
+ /**
79
+ * Splits a selector list on top-level commas.
80
+ *
81
+ * A naive `split(',')` corrupts escaped identifiers — MDS ships class names such as
82
+ * `.MdsGrid-templateColumns--repeat\(1\,\ 1fr\)`, where the comma and space are part
83
+ * of the class name. Backslash escapes, quoted strings, and `()` / `[]` nesting
84
+ * (`:not(a, b)`, `[data-x="a,b"]`) all have to be honoured.
85
+ */
86
+ function splitSelectorList(selector) {
87
+ const parts = [];
88
+ let current = '';
89
+ let depth = 0;
90
+ let quote = null;
91
+
92
+ for (let i = 0; i < selector.length; i++) {
93
+ const ch = selector[i];
94
+
95
+ if (ch === '\\') {
96
+ current += ch + (selector[i + 1] ?? '');
97
+ i++;
98
+ continue;
99
+ }
100
+ if (quote) {
101
+ current += ch;
102
+ if (ch === quote) quote = null;
103
+ continue;
104
+ }
105
+ if (ch === '"' || ch === "'") {
106
+ quote = ch;
107
+ current += ch;
108
+ continue;
109
+ }
110
+ if (ch === '(' || ch === '[') depth++;
111
+ else if (ch === ')' || ch === ']') depth--;
112
+ else if (ch === ',' && depth === 0) {
113
+ parts.push(current.trim());
114
+ current = '';
115
+ continue;
116
+ }
117
+ current += ch;
118
+ }
119
+ parts.push(current.trim());
120
+
121
+ return parts.filter((part) => part !== '');
122
+ }
123
+
124
+ /**
125
+ * Index at which the first compound selector ends, i.e. the first top-level
126
+ * combinator. Escape- and nesting-aware for the same reasons as
127
+ * {@link splitSelectorList} — `~` in `[attr~=x]` and `+` in `:nth-child(2n+1)`
128
+ * are not combinators, and `\ ` is an escaped space inside an identifier.
129
+ */
130
+ function firstCompoundEnd(selector) {
131
+ let depth = 0;
132
+ let quote = null;
133
+
134
+ for (let i = 0; i < selector.length; i++) {
135
+ const ch = selector[i];
136
+
137
+ if (ch === '\\') {
138
+ i++;
139
+ continue;
140
+ }
141
+ if (quote) {
142
+ if (ch === quote) quote = null;
143
+ continue;
144
+ }
145
+ if (ch === '"' || ch === "'") {
146
+ quote = ch;
147
+ continue;
148
+ }
149
+ if (ch === '(' || ch === '[') depth++;
150
+ else if (ch === ')' || ch === ']') depth--;
151
+ else if (depth === 0 && (ch === ' ' || ch === '\t' || ch === '\n' || ch === '>' || ch === '+' || ch === '~')) {
152
+ return i;
153
+ }
154
+ }
155
+ return selector.length;
156
+ }
157
+
158
+ /**
159
+ * Returns a variant of `selector` anchored to the scope root, or `null` when the
160
+ * selector is already root-anchored.
161
+ *
162
+ * Inside `@scope`, a rule is implicitly relative to the scope root — `.Backdrop`
163
+ * behaves as `:scope .Backdrop` and so matches *descendants only*. Overlays that
164
+ * portal to `document.body` carry `data-mds-root` on the portaled element itself,
165
+ * making that element the scope root; without this variant its own rules would
166
+ * never apply. Fusing `:scope` onto the first compound (`:scope.Backdrop`) covers
167
+ * that case, while the original selector keeps covering descendants.
168
+ *
169
+ * Every rule gets a variant, so all rules matching a given scope root are bumped
170
+ * by the same `:scope` (0,1,0) — relative specificity, and therefore the cascade
171
+ * between MDS rules, is preserved.
172
+ */
173
+ function rootAnchoredVariant(selector) {
174
+ const end = firstCompoundEnd(selector);
175
+ const head = selector.slice(0, end);
176
+ const tail = selector.slice(end);
177
+
178
+ if (!head) return null;
179
+ // `:root` is already remapped to `:scope` by this point.
180
+ if (head.includes(':scope')) return null;
181
+
182
+ // A type selector must remain first in its compound: `div:scope`, not `:scope div`.
183
+ const type = head.match(/^(\*|[a-zA-Z][\w-]*)/);
184
+ if (type) return `${type[0]}:scope${head.slice(type[0].length)}${tail}`;
185
+
186
+ return `:scope${head}${tail}`;
187
+ }
188
+
189
+ function isInsideKeyframes(node) {
190
+ for (let parent = node.parent; parent; parent = parent.parent) {
191
+ if (parent.type === 'atrule' && /keyframes$/.test(parent.name)) return true;
192
+ }
193
+ return false;
194
+ }
195
+
196
+ function addRootAnchoredVariants(root) {
197
+ root.walkRules((rule) => {
198
+ if (isInsideKeyframes(rule)) return;
199
+
200
+ const parts = splitSelectorList(rule.selector);
201
+ const variants = parts.map(rootAnchoredVariant).filter(Boolean);
202
+ if (variants.length) rule.selector = [...parts, ...variants].join(', ');
203
+ });
204
+ }
205
+
206
+ function hoistGlobalAtRules(cssText) {
207
+ const root = postcss.parse(cssText);
208
+ const hoisted = [];
209
+ root.each((node) => {
210
+ if (node.type === 'atrule' && GLOBAL_AT_RULES.includes(node.name)) {
211
+ hoisted.push(node);
212
+ node.remove();
213
+ }
214
+ });
215
+ return { remaining: root, hoisted };
216
+ }
217
+
218
+ /**
219
+ * Document-level selectors have no meaning inside `@scope`: the scope root is the
220
+ * closest thing to a document root, so tokens declared on `:root` are remapped to
221
+ * `:scope`. Without this, every custom-property reference in the bundle would
222
+ * resolve to nothing.
223
+ */
224
+ function remapRootSelectors(root) {
225
+ root.walkRules((rule) => {
226
+ const parts = splitSelectorList(rule.selector);
227
+ let changed = false;
228
+
229
+ const next = parts.map((sel) => {
230
+ if (sel === ':root' || sel === 'html' || sel === 'body' || sel === ':host') {
231
+ changed = true;
232
+ return ':scope';
233
+ }
234
+ if (/^(body|html)([.#[:\s].*)?$/.test(sel)) {
235
+ changed = true;
236
+ return sel.replace(/^(body|html)/, ':scope');
237
+ }
238
+ return sel;
239
+ });
240
+
241
+ if (changed) rule.selector = next.join(', ');
242
+ });
243
+ }
244
+
245
+ /**
246
+ * A global-namespace at-rule nested inside e.g. `@media` is not reachable by the
247
+ * top-level hoist and would be silently dropped by the browser once scoped.
248
+ * Fail loudly instead of shipping a bundle with dead animations.
249
+ */
250
+ function assertNoGlobalAtRulesRemain(root) {
251
+ const stranded = [];
252
+ root.walkAtRules((atRule) => {
253
+ if (GLOBAL_AT_RULES.includes(atRule.name)) {
254
+ stranded.push(`@${atRule.name} ${atRule.params}`.trim());
255
+ }
256
+ });
257
+ if (stranded.length) {
258
+ throw new Error(
259
+ `Cannot scope ${stranded.length} global at-rule(s) nested inside another at-rule; ` +
260
+ `move them to the top level of their source file:\n ${stranded.join('\n ')}`
261
+ );
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Returns the `@scope` prelude: the consumer's opt-in selector plus MDS's own portal
267
+ * roots.
268
+ *
269
+ * The portal roots are unconditional. They target MDS-owned markup rather than anything
270
+ * the consumer controls, so no consumer selector can subsume them — and omitting them
271
+ * would leave every modal, popper and backdrop outside all scopes, rendering unstyled.
272
+ */
273
+ function buildScopePrelude(consumerSelector) {
274
+ return [...splitSelectorList(consumerSelector), ...PORTAL_ROOT_SELECTORS].join(', ');
275
+ }
276
+
277
+ async function build() {
278
+ const files = expandGlobs(sources).filter((f) => fs.existsSync(f));
279
+ const seen = new Set();
280
+ const unique = files.filter((f) => (seen.has(f) ? false : (seen.add(f), true)));
281
+
282
+ const concatenated = unique
283
+ .map((f) => `/* Source: ${path.relative(cssRoot, f)} */\n${fs.readFileSync(f, 'utf8')}`)
284
+ .join('\n\n');
285
+
286
+ const processed = await postcss([postcssColorMod(), autoprefixer()]).process(concatenated, {
287
+ from: undefined,
288
+ });
289
+
290
+ const { remaining, hoisted } = hoistGlobalAtRules(processed.css);
291
+ assertNoGlobalAtRulesRemain(remaining);
292
+ remapRootSelectors(remaining);
293
+ addRootAnchoredVariants(remaining);
294
+
295
+ const prelude = buildScopePrelude(SCOPE_SELECTOR);
296
+ const outRoot = postcss.root();
297
+ for (const node of hoisted) outRoot.append(node.clone());
298
+ outRoot.append(postcss.parse(`@scope (${prelude}) {\n${remaining.toString()}\n}`));
299
+
300
+ const outDir = path.join(cssRoot, 'dist');
301
+ fs.mkdirSync(outDir, { recursive: true });
302
+ const outPath = path.join(outDir, OUTPUT_FILE);
303
+ fs.writeFileSync(outPath, outRoot.toString());
304
+ console.log(
305
+ `Wrote ${path.relative(cssRoot, outPath)} (${fs.statSync(outPath).size} bytes) ` +
306
+ `from ${unique.length} sources, scoped to \`${prelude}\``
307
+ );
308
+ }
309
+
310
+ build().catch((err) => {
311
+ console.error(err);
312
+ process.exit(1);
313
+ });
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Single source of truth for the CSS build order.
3
+ *
4
+ * Consumed by both `css/gulpfile.js` (default `dist/index.css`) and
5
+ * `css/scripts/build-scoped.mjs` (scoped `dist/mds-scoped.css`) so the two
6
+ * bundles cannot drift apart when a new source directory is added.
7
+ *
8
+ * Paths are globs relative to the `css/` directory.
9
+ */
10
+
11
+ const materialIcons = './material-design-icons/iconfont/material-icons.css';
12
+ const materialFont = './material-design-icons/iconfont/*.{ttf,otf,woff2}';
13
+
14
+ /**
15
+ * Typography is listed explicitly ahead of `components/*` so that, on equal
16
+ * specificity, component rules win over the base typography rules. These files
17
+ * also match the `components/*` glob below; the duplicate is de-duplicated by
18
+ * path (glob-stream in gulp, an explicit Set in the scoped build), keeping the
19
+ * earlier position.
20
+ */
21
+ const typographyCssPath = [
22
+ './src/components/text.module.css',
23
+ './src/components/heading.module.css',
24
+ './src/components/label.module.css',
25
+ './src/components/caption.module.css',
26
+ './src/components/subheading.module.css',
27
+ ];
28
+
29
+ /**
30
+ * Cascade order, shared by every bundle. The scoped bundle deliberately uses the
31
+ * same list with nothing appended — it is the same CSS, only wrapped in `@scope`.
32
+ */
33
+ const sources = [
34
+ './src/tokens/*.css',
35
+ './src/variables/*.css',
36
+ materialIcons,
37
+ './src/core/*.css',
38
+ ...typographyCssPath,
39
+ './src/components/*.css',
40
+ './src/ai-components/*.css',
41
+ './src/utils/*.css',
42
+ ];
43
+
44
+ module.exports = {
45
+ materialIcons,
46
+ materialFont,
47
+ typographyCssPath,
48
+ sources,
49
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Scope roots for MDS's own portaled overlays, always added to the scoped bundle's
3
+ * `@scope` prelude by `build-scoped.mjs`.
4
+ *
5
+ * Every overlay listed here is portaled to `document.body` by `ReactDOM.createPortal`,
6
+ * escaping the consumer's `[data-mds-root]` container. Inside `@scope`, a rule like
7
+ * `.Backdrop` is implicitly `:scope .Backdrop` and so matches descendants only, so these
8
+ * elements have to be scope roots in their own right — and there is no MDS-owned ancestor
9
+ * to hang that on. Their own rules then match via the root-anchored selector variants the
10
+ * build adds (`.Backdrop, :scope.Backdrop`).
11
+ *
12
+ * Each selector targets markup the components *already* render. That is what keeps the
13
+ * scoped stylesheet a CSS-only feature: nothing in `core/` changes, so the CSS can be
14
+ * updated independently of the library version and works with releases already shipped.
15
+ *
16
+ * Each is also qualified so it cannot collide with a host app's own class names:
17
+ *
18
+ * - `body >` — every one of these lands as a direct child of `<body>`, so a colliding
19
+ * host class would have to be at body level too.
20
+ * - `:has([data-layer])` — `data-layer` is an MDS-only attribute, present on all five
21
+ * portal roots. `.Overlay-wrapper` is a generic enough name that a host app could
22
+ * plausibly reuse it, so the class alone is not sufficient. The `:has()` matches a
23
+ * descendant rather than a child because Modal wraps its container in `OutsideClick`
24
+ * when `backdropClose` is set.
25
+ * - `data-test='DesignSystem-*'` — already namespaced, so `body >` is enough.
26
+ *
27
+ * This file is the single source of truth, shared with
28
+ * `core/utils/__tests__/scopedPortalRoots.test.tsx`, which renders each overlay and
29
+ * asserts these selectors still match — so the coupling to component markup cannot rot
30
+ * silently.
31
+ */
32
+ const PORTAL_ROOT_SELECTORS = [
33
+ /** Modal, Sidesheet and FullscreenModal all portal into this shared container. */
34
+ 'body > .Overlay-wrapper:has([data-layer])',
35
+ /** Popover and everything built on it: Tooltip, Dropdown, Menu, DateRangePicker, … */
36
+ "body > [data-test='DesignSystem-Popover']",
37
+ /** Backdrop, including the one Modal/Sidesheet render. */
38
+ "body > [data-test='DesignSystem-Backdrop']",
39
+ /**
40
+ * Listbox drag ghost. No `data-test` exists on it, so `body >` is the only qualifier
41
+ * available; the BEM class name is at least MDS-specific. Worth tightening with a
42
+ * `data-test` the next time component changes ship.
43
+ */
44
+ 'body > .Listbox-item--draggable',
45
+ ];
46
+
47
+ module.exports = { PORTAL_ROOT_SELECTORS };
@@ -1,4 +1,4 @@
1
- @keyframes rotate {
1
+ @keyframes mds-rotate {
2
2
  0% {
3
3
  transform: rotate(0deg);
4
4
  }
@@ -7,7 +7,7 @@
7
7
  }
8
8
  }
9
9
 
10
- @keyframes spin {
10
+ @keyframes mds-spin {
11
11
  0% {
12
12
  stroke-dasharray: 1, 200;
13
13
  stroke-dashoffset: 0;
@@ -23,7 +23,7 @@
23
23
  }
24
24
 
25
25
  .Spinner {
26
- animation: rotate 2s linear infinite;
26
+ animation: mds-rotate 2s linear infinite;
27
27
  }
28
28
 
29
29
  .Spinner--xsmall {
@@ -50,7 +50,7 @@
50
50
  stroke-linecap: round;
51
51
  stroke-dasharray: 1 200;
52
52
  stroke-dashoffset: 0;
53
- animation: spin 1.5s ease-in-out infinite;
53
+ animation: mds-spin 1.5s ease-in-out infinite;
54
54
  }
55
55
 
56
56
  .Circle--primary {