basalt-ui 1.29.1 → 1.30.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/CHANGELOG.md +16 -0
- package/MIGRATING.md +701 -369
- package/README.md +23 -2
- package/agent/rules/basalt-controls.md +34 -34
- package/agent/skills/basalt-app/SKILL.md +4 -2
- package/agent/templates/query-client.ts.tpl +18 -4
- package/configs/lefthook.yml +51 -19
- package/configs/oxlint-plugin.js +210 -84
- package/configs/oxlint-plugin.test.ts +182 -5
- package/configs/oxlint.json +4 -4
- package/dist/charts/primitives/ChartCard.d.ts.map +1 -1
- package/dist/charts/primitives/ChartCard.js +2 -2
- package/dist/charts/primitives/ChartFrame.d.ts +23 -3
- package/dist/charts/primitives/ChartFrame.d.ts.map +1 -1
- package/dist/charts/primitives/ChartFrame.js +4 -4
- package/dist/charts/primitives/chart-frame-layout.d.ts +55 -0
- package/dist/charts/primitives/chart-frame-layout.d.ts.map +1 -1
- package/dist/charts/primitives/chart-frame-layout.js +16 -0
- package/dist/cli/check-theme.d.ts.map +1 -1
- package/dist/cli/check-theme.js +6 -0
- package/dist/cli/doctor.d.ts +10 -4
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +28 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +2 -1
- package/dist/cli/index.d.ts +12 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +29 -0
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +4 -15
- package/dist/cli/sync.d.ts.map +1 -1
- package/dist/cli/sync.js +8 -14
- package/dist/controls/actions.d.ts.map +1 -1
- package/dist/controls/actions.js +12 -2
- package/dist/dashboard/section.module.css +15 -7
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +7 -2
- package/dist/dashboard/stat-group.d.ts +8 -2
- package/dist/dashboard/stat-group.d.ts.map +1 -1
- package/dist/dashboard/stat-group.js +23 -15
- package/dist/dashboard/stat-group.module.css +85 -9
- package/dist/dashboard/stat-group.module.css.d.ts +1 -0
- package/dist/dashboard/widget-grid.d.ts.map +1 -1
- package/dist/dashboard/widget-grid.js +4 -5
- package/dist/dashboard/widget-grid.module.css +64 -21
- package/dist/dashboard/widget-grid.module.css.d.ts +1 -0
- package/dist/guard/index.d.ts +19 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +27 -4
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/query-client.d.ts +20 -2
- package/dist/query-client.d.ts.map +1 -1
- package/dist/shell/app-brand.module.css +12 -3
- package/dist/shell/app-header.module.css +49 -9
- package/dist/shell/app-main.module.css +24 -4
- package/dist/shell/app-mobile-nav.d.ts.map +1 -1
- package/dist/shell/app-mobile-nav.js +98 -51
- package/dist/shell/app-mobile-nav.module.css +99 -11
- package/dist/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/dist/shell/app-sidebar.module.css +55 -2
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +3 -3
- package/dist/shell/mobile-nav-model.d.ts +26 -7
- package/dist/shell/mobile-nav-model.d.ts.map +1 -1
- package/dist/shell/mobile-nav-model.js +12 -3
- package/dist/shell/page-aside.module.css +8 -6
- package/dist/shell/page-bar.d.ts +13 -0
- package/dist/shell/page-bar.d.ts.map +1 -1
- package/dist/shell/page-bar.js +13 -3
- package/dist/shell/page-bar.module.css +20 -12
- package/dist/shell/page-title.module.css +3 -1
- package/dist/theme/use-basalt-spacing.d.ts.map +1 -1
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +51 -7
- package/dist/tokens/palette.d.ts +98 -17
- package/dist/tokens/palette.d.ts.map +1 -1
- package/dist/tokens/palette.js +110 -18
- package/dist/tokens.css +17 -5
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/primitives/ChartCard.tsx +12 -7
- package/src/charts/primitives/ChartFrame.tsx +30 -15
- package/src/charts/primitives/chart-frame-layout.ts +86 -0
- package/src/cli/check-theme.ts +13 -0
- package/src/cli/doctor.ts +49 -8
- package/src/cli/help.ts +5 -3
- package/src/cli/index.ts +49 -0
- package/src/cli/init.ts +6 -27
- package/src/cli/sync.ts +11 -22
- package/src/controls/actions.tsx +51 -0
- package/src/dashboard/section.module.css +15 -7
- package/src/dashboard/stat-card.tsx +7 -2
- package/src/dashboard/stat-group.module.css +85 -9
- package/src/dashboard/stat-group.module.css.d.ts +1 -0
- package/src/dashboard/stat-group.tsx +40 -20
- package/src/dashboard/widget-grid.module.css +64 -21
- package/src/dashboard/widget-grid.module.css.d.ts +1 -0
- package/src/dashboard/widget-grid.tsx +30 -12
- package/src/guard/index.ts +79 -11
- package/src/index.ts +9 -0
- package/src/query-client.ts +17 -5
- package/src/shell/app-brand.module.css +12 -3
- package/src/shell/app-header.module.css +49 -9
- package/src/shell/app-main.module.css +24 -4
- package/src/shell/app-mobile-nav.module.css +99 -11
- package/src/shell/app-mobile-nav.module.css.d.ts +1 -0
- package/src/shell/app-mobile-nav.tsx +225 -83
- package/src/shell/app-sidebar.module.css +55 -2
- package/src/shell/index.tsx +26 -6
- package/src/shell/mobile-nav-model.ts +37 -9
- package/src/shell/page-aside.module.css +8 -6
- package/src/shell/page-bar.module.css +20 -12
- package/src/shell/page-bar.tsx +39 -5
- package/src/shell/page-title.module.css +3 -1
- package/src/theme/use-basalt-spacing.ts +8 -6
- package/src/tokens/index.ts +90 -6
- package/src/tokens/palette.ts +111 -18
package/src/cli/doctor.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* managed-file manifest path) back from `./index`, this package's shared CLI helpers.
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync } from 'node:fs'
|
|
7
|
-
import { resolve } from 'node:path'
|
|
7
|
+
import { relative, resolve } from 'node:path'
|
|
8
8
|
|
|
9
9
|
import type { BasaltConfig } from './index'
|
|
10
10
|
import {
|
|
@@ -19,7 +19,10 @@ import {
|
|
|
19
19
|
parseJsonc,
|
|
20
20
|
readBasaltConfig,
|
|
21
21
|
readIfExists,
|
|
22
|
+
readManifest,
|
|
22
23
|
resolveProjectDir,
|
|
24
|
+
resolveRoots,
|
|
25
|
+
scannableFiles,
|
|
23
26
|
shippedAssetPath,
|
|
24
27
|
} from './index'
|
|
25
28
|
import { findManifestAbove, parentInstallAdvice } from './sync'
|
|
@@ -102,16 +105,22 @@ function resolveAiMajorSkewReason(cfg: BasaltConfig): {
|
|
|
102
105
|
* Check a consumer repo's basalt integration and print a pass/warn report.
|
|
103
106
|
*
|
|
104
107
|
* Narrowed to what a single-directory-scoped run can actually verify — no workspace-glob walking,
|
|
105
|
-
* no ascend/descend project discovery (see {@link resolveProjectDir}).
|
|
108
|
+
* no ascend/descend project discovery (see {@link resolveProjectDir}). Six checks, numbered in
|
|
106
109
|
* the order they run and print:
|
|
107
110
|
*
|
|
108
111
|
* Hard failures (exit non-zero):
|
|
109
112
|
* 1. `.basalt/manifest.json` exists (`basalt-ui init` was run).
|
|
110
|
-
* 2. `
|
|
113
|
+
* 2. `install-parity`: basalt-ui resolves, doctor prints WHERE and at what version, and that
|
|
114
|
+
* version agrees with the manifest's `basaltVersion`.
|
|
115
|
+
* 3. `guard-scan`: `check-theme` would scan more than zero files, sharing `scannableFiles` so the
|
|
116
|
+
* two cannot disagree. Both RESTORED in 1.30.0 after four minors gone with no MIGRATING row —
|
|
117
|
+
* README § `doctor` carries what their absence cost (a no-op CI gate, a green guard scanning
|
|
118
|
+
* nothing, and two basalt-WRITTEN docs describing output the CLI no longer produced).
|
|
119
|
+
* 4. `oxlint-preset` ("rules in sync"): the consumer's `.oxlintrc.json` extends the shipped
|
|
111
120
|
* preset AND that path resolves. `init` keeps an existing config, so the framework's whole
|
|
112
121
|
* lint half can be silently off — one repo carried six real violations invisibly across five
|
|
113
122
|
* minors this way.
|
|
114
|
-
*
|
|
123
|
+
* 6. `ai-major-parity`: within THIS package.json, `dependencies`/`devDependencies`/
|
|
115
124
|
* `peerDependencies` agree on the `ai` package's major. `basalt/ai-sdk-major` (the oxlint
|
|
116
125
|
* plugin rule) already catches this per linted FILE against its nearest package.json; this
|
|
117
126
|
* check exists for the shape a lint run cannot see — a package whose own manifest asks its
|
|
@@ -121,7 +130,7 @@ function resolveAiMajorSkewReason(cfg: BasaltConfig): {
|
|
|
121
130
|
* already agree warns that the exemption is stale.
|
|
122
131
|
*
|
|
123
132
|
* Warning (non-fatal):
|
|
124
|
-
*
|
|
133
|
+
* 5. `lefthook-preset` ("lefthook wired"): a pre-commit command runs `check-theme`, read from
|
|
125
134
|
* `lefthook dump --format json` (resolves `extends`/`include`/`root:` the way lefthook itself
|
|
126
135
|
* does — see {@link inspectLefthookGate}). No lefthook config at all is a pass (a consumer may
|
|
127
136
|
* gate elsewhere); a config that resolves but wires no guard command warns; a config lefthook
|
|
@@ -189,7 +198,39 @@ export function doctor(invocationCwd: string = process.cwd(), flags: string[] =
|
|
|
189
198
|
|
|
190
199
|
const install = findBasaltInstall(cwd)
|
|
191
200
|
|
|
192
|
-
// ── Hard check 2: the
|
|
201
|
+
// ── Hard check 2: install matches the manifest, and doctor says WHERE (the CLAUDE-block's claim)
|
|
202
|
+
const manifestVersion = profile === 'tokens-only' ? undefined : readManifest(cwd).basaltVersion
|
|
203
|
+
const at = install.dir === null ? '' : `${relative(cwd, install.dir)} (${install.version})`
|
|
204
|
+
if (install.dir === null) {
|
|
205
|
+
fail(
|
|
206
|
+
'install-parity: basalt-ui does not resolve here, above, or in a sibling workspace package.',
|
|
207
|
+
)
|
|
208
|
+
} else if (manifestVersion === undefined || install.version === manifestVersion) {
|
|
209
|
+
const matches = manifestVersion === undefined ? '' : " and matches the manifest's basaltVersion"
|
|
210
|
+
pass(`basalt-ui resolves at ${at}${matches}`)
|
|
211
|
+
} else {
|
|
212
|
+
fail(
|
|
213
|
+
`install-parity: basalt-ui resolves at ${at}, but ${MANIFEST_PATH} was last written by ` +
|
|
214
|
+
`${manifestVersion} — the managed layer is from a different release than the code. Run ` +
|
|
215
|
+
'`basalt-ui sync` (or `bun install`, if the lockfile is what is stale).',
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Hard check 3: the guard sees files — zero-file roots are a green guard enforcing nothing ──
|
|
220
|
+
const scanned = profile === 'tokens-only' ? null : scannableFiles(cwd, cfg).length
|
|
221
|
+
if (scanned === null) {
|
|
222
|
+
pass('guard-scan: n/a — a tokens-only consumer runs the token lane, not the file walk')
|
|
223
|
+
} else if (scanned === 0) {
|
|
224
|
+
fail(
|
|
225
|
+
'guard-scan: `check-theme` would scan ZERO files here — either `basalt.roots` points at ' +
|
|
226
|
+
'paths that do not exist, or this is not the package the config lives in (BASALT_CWD, ' +
|
|
227
|
+
'else the cwd; nothing is inferred).',
|
|
228
|
+
)
|
|
229
|
+
} else {
|
|
230
|
+
pass(`guard-scan: check-theme covers ${scanned} file(s) under ${resolveRoots(cfg).join(', ')}`)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── Hard check 4: the consumer's oxlint config extends the shipped preset ──
|
|
193
234
|
// `init` KEEPS an existing `.oxlintrc.json`, so a repo can carry the whole scaffold with the
|
|
194
235
|
// framework's lint half switched off and nothing anywhere saying so. One repo ran five minors
|
|
195
236
|
// that way and surfaced six real `basalt/no-raw-font-size` errors the moment it was wired.
|
|
@@ -235,7 +276,7 @@ export function doctor(invocationCwd: string = process.cwd(), flags: string[] =
|
|
|
235
276
|
}
|
|
236
277
|
}
|
|
237
278
|
|
|
238
|
-
// ── Warn check
|
|
279
|
+
// ── Warn check 5: the pre-commit hook actually runs the guard ──────────────
|
|
239
280
|
// Read via `lefthook dump --format json`, which resolves `extends`/`include`/remote configs/
|
|
240
281
|
// `root:` the way lefthook itself does — see {@link inspectLefthookGate}.
|
|
241
282
|
const repoRoot = findRepoRoot(cwd)
|
|
@@ -262,7 +303,7 @@ export function doctor(invocationCwd: string = process.cwd(), flags: string[] =
|
|
|
262
303
|
)
|
|
263
304
|
}
|
|
264
305
|
|
|
265
|
-
// ── Hard check
|
|
306
|
+
// ── Hard check 6: ai package major version parity within THIS package.json ─
|
|
266
307
|
// basalt/ai-sdk-major (the lint rule) is per-file against the NEAREST package.json, so a lint
|
|
267
308
|
// run scoped to one workspace package only ever sees that package's own `ai` major and is
|
|
268
309
|
// perfectly happy. This check reads the SAME package.json across its dependencies/
|
package/src/cli/help.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const COMMAND_OPTIONS: Record<string, Record<string, { type: 'string' | '
|
|
|
21
21
|
sync: {
|
|
22
22
|
force: { type: 'boolean' },
|
|
23
23
|
check: { type: 'boolean' },
|
|
24
|
+
verbose: { type: 'boolean' },
|
|
24
25
|
'tokens-only': { type: 'boolean' },
|
|
25
26
|
framework: { type: 'boolean' },
|
|
26
27
|
},
|
|
@@ -65,7 +66,7 @@ export function unknownFlag(cmd: string, flags: readonly string[]): string | nul
|
|
|
65
66
|
/** The one usage string — printed by `basalt help` / `--help` / `-h` AND the unknown-command fallback. */
|
|
66
67
|
export const USAGE =
|
|
67
68
|
'Usage: basalt-ui <--version | init [--with-router] [--with-query] [--merge-lint] |\n' +
|
|
68
|
-
' sync [--force] [--check] [--tokens-only|--framework] |\n' +
|
|
69
|
+
' sync [--force] [--check] [--verbose] [--tokens-only|--framework] |\n' +
|
|
69
70
|
' check-theme [--audit-allows] |\n' +
|
|
70
71
|
' doctor [--tokens-only|--framework] | guard-hook | tokens:css | fonts:css | help>\n\n' +
|
|
71
72
|
'check-theme [--tokens-only|--framework] [--audit-allows]\n' +
|
|
@@ -89,9 +90,10 @@ export const USAGE =
|
|
|
89
90
|
' Checks THIS project only: the manifest exists, the oxlint preset is wired, the pre-commit\n' +
|
|
90
91
|
' hook runs check-theme (via `lefthook dump`), and the ai package major is consistent within\n' +
|
|
91
92
|
' this package.json. Profile is DECLARED, same as check-theme — never inferred.\n\n' +
|
|
92
|
-
'sync [--force] [--check] [--tokens-only|--framework]\n' +
|
|
93
|
+
'sync [--force] [--check] [--verbose] [--tokens-only|--framework]\n' +
|
|
93
94
|
' Refreshes an EXISTING install; it never creates one (that is init). A tokens-only consumer\n' +
|
|
94
|
-
' has no scaffold to reconcile, so sync reports n/a and exits 0 there — as doctor does.\n
|
|
95
|
+
' has no scaffold to reconcile, so sync reports n/a and exits 0 there — as doctor does.\n' +
|
|
96
|
+
' --check prints the permanent placement skips as a COUNT; --verbose spells them out again.\n\n' +
|
|
95
97
|
'check-theme / doctor / sync all resolve their project the same way: BASALT_CWD, else the\n' +
|
|
96
98
|
'invocation cwd — nothing inferred, nothing ascended or descended into.\n' +
|
|
97
99
|
'--tokens-only and --framework are alternatives; passing both is an error.\n\n' +
|
package/src/cli/index.ts
CHANGED
|
@@ -333,6 +333,17 @@ function sanitizeSeverity(cfg: BasaltConfig): BasaltConfig {
|
|
|
333
333
|
return { ...cfg, severity: kept as Partial<Record<GuardKind, GuardSeverity>> }
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
/** Does `cwd`'s own `package.json` carry a `basalt` key? — "misconfigured roots" vs "wrong
|
|
337
|
+
* directory", the failure a root-invoked hook hits since 1.29.0's single resolver. Never a walk. */
|
|
338
|
+
export function hasBasaltKey(cwd: string): boolean {
|
|
339
|
+
const raw = readIfExists(resolve(cwd, 'package.json'))
|
|
340
|
+
try {
|
|
341
|
+
return raw !== null && (JSON.parse(raw) as { basalt?: unknown }).basalt !== undefined
|
|
342
|
+
} catch {
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
336
347
|
export function readBasaltConfig(cwd: string): BasaltConfig {
|
|
337
348
|
try {
|
|
338
349
|
const pkg = JSON.parse(readFileSync(resolve(cwd, 'package.json'), 'utf8')) as {
|
|
@@ -1114,6 +1125,44 @@ export function managedFiles(
|
|
|
1114
1125
|
return [...rules, ...skills, claudeBlock, design, oxfmt, ...rootTooling, oxlintrc, ...scaffolds]
|
|
1115
1126
|
}
|
|
1116
1127
|
|
|
1128
|
+
/**
|
|
1129
|
+
* The PERMANENT placement skips as sentences — one text for `init` and `sync`, which carried two
|
|
1130
|
+
* near-copies that had already drifted apart. Correct and unchanging, which is why `sync --check`
|
|
1131
|
+
* hides them behind `--verbose`. `extends` alone is NOT the recipe for the hook, and saying only
|
|
1132
|
+
* that shipped a broken one: lefthook merges the preset's commands but runs every one at the REPO
|
|
1133
|
+
* ROOT, with no oxlint config, no local binary and no basalt config. `root:` moves the invocation
|
|
1134
|
+
* cwd per command and merges (the preset does not define it) — see the preset's MONOREPO RECIPE.
|
|
1135
|
+
*/
|
|
1136
|
+
export function placementNotices(cwd: string, placement: PlacementFlags): string[] {
|
|
1137
|
+
const out: string[] = []
|
|
1138
|
+
const install = findBasaltInstall(cwd)
|
|
1139
|
+
const asset = (n: string): string => shippedAssetPath(install, placement.repoRoot, n)
|
|
1140
|
+
// Bare, no `./` — a lefthook `root:` and an Actions `working-directory:` are already repo-relative.
|
|
1141
|
+
const pkg = relativePosix(placement.repoRoot, cwd).replace(/^\.\//, '')
|
|
1142
|
+
const notRoot = `(not the repo root — repo root detected at ${placement.repoRoot})`
|
|
1143
|
+
if (!placement.isPackageRepoRoot) {
|
|
1144
|
+
out.push(
|
|
1145
|
+
`skipped lefthook.yml ${notRoot} — lefthook only reads config at the repo root. Extend ` +
|
|
1146
|
+
`${asset('configs/lefthook.yml')} from your root lefthook.yml AND give every merged ` +
|
|
1147
|
+
`command \`root: '${pkg}/'\` — \`pre-commit.commands.{oxlint,oxfmt,check-theme}.root: ` +
|
|
1148
|
+
`'${pkg}/'\` — or they run at the root where your oxlint config, your binaries and your ` +
|
|
1149
|
+
'basalt config are not.',
|
|
1150
|
+
`skipped .github/workflows/check.yml ${notRoot} — GitHub Actions only reads .github/ at the ` +
|
|
1151
|
+
`repo root; copy the steps from ${asset('configs/check.yml')} into your root CI workflow ` +
|
|
1152
|
+
`instead, and give each one \`working-directory: ${pkg}\` — the same relocation \`root:\` ` +
|
|
1153
|
+
'performs for lefthook, and needed for the same reason.',
|
|
1154
|
+
)
|
|
1155
|
+
}
|
|
1156
|
+
if (placement.relocatedQueryClient !== null) {
|
|
1157
|
+
out.push(
|
|
1158
|
+
`skipped src/query-client.ts (found an existing query client at ` +
|
|
1159
|
+
`${relative(cwd, placement.relocatedQueryClient)}) — import it from there instead of ` +
|
|
1160
|
+
're-seeding at the original path.',
|
|
1161
|
+
)
|
|
1162
|
+
}
|
|
1163
|
+
return out
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1117
1166
|
export type Manifest = {
|
|
1118
1167
|
version: 1
|
|
1119
1168
|
/** The basalt-ui version that last wrote this manifest — `doctor`'s one reconciliation axis. */
|
package/src/cli/init.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* oxlint-preset plugin reader from `./sync` (also called there).
|
|
5
5
|
*/
|
|
6
6
|
import { existsSync } from 'node:fs'
|
|
7
|
-
import {
|
|
7
|
+
import { resolve } from 'node:path'
|
|
8
8
|
|
|
9
9
|
import { deriveSpacing } from '../tokens'
|
|
10
10
|
import type { MergeLintResult, ScaffoldFlags } from './index'
|
|
@@ -15,17 +15,16 @@ import {
|
|
|
15
15
|
mergeOxlintExtends,
|
|
16
16
|
normalizeForLedger,
|
|
17
17
|
patchPackageJson,
|
|
18
|
+
placementNotices,
|
|
18
19
|
pruneRetiredManagedFiles,
|
|
19
20
|
readManifest,
|
|
20
21
|
reconcileRoots,
|
|
21
|
-
relativePosix,
|
|
22
22
|
renderContext,
|
|
23
23
|
reportPrune,
|
|
24
24
|
resolvePeerFlags,
|
|
25
25
|
resolvePlacement,
|
|
26
26
|
rootsNotices,
|
|
27
27
|
sha256,
|
|
28
|
-
shippedAssetPath,
|
|
29
28
|
unitState,
|
|
30
29
|
writeFileEnsuringDir,
|
|
31
30
|
writeUnit,
|
|
@@ -166,30 +165,10 @@ export function init(cwd: string = process.cwd(), scaffoldFlags: ScaffoldFlags =
|
|
|
166
165
|
if (seededLintScript) {
|
|
167
166
|
console.log(`basalt-ui init: added the "lint:basalt" script (${lintScript}) to package.json.`)
|
|
168
167
|
}
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
console.log(
|
|
174
|
-
`basalt-ui init: skipped lefthook.yml (this package is not the repo root — repo root ` +
|
|
175
|
-
`detected at ${placement.repoRoot}) — lefthook only reads config at the repo root; extend ` +
|
|
176
|
-
`${ctx.vars.LEFTHOOK_PRESET_PATH} from your root lefthook.yml instead, and give its ` +
|
|
177
|
-
`check-theme command \`env: { BASALT_CWD: ${relativePosix(placement.repoRoot, cwd)} }\` ` +
|
|
178
|
-
'so the guard runs where your basalt config actually lives.',
|
|
179
|
-
)
|
|
180
|
-
console.log(
|
|
181
|
-
`basalt-ui init: skipped .github/workflows/check.yml (this package is not the repo root — ` +
|
|
182
|
-
`repo root detected at ${placement.repoRoot}) — GitHub Actions only reads .github/ at the ` +
|
|
183
|
-
`repo root; copy the steps from ${shippedAssetPath(install, placement.repoRoot, 'configs/check.yml')} ` +
|
|
184
|
-
'into your root CI workflow instead.',
|
|
185
|
-
)
|
|
186
|
-
}
|
|
187
|
-
if (placement.relocatedQueryClient !== null) {
|
|
188
|
-
console.log(
|
|
189
|
-
`basalt-ui init: skipped src/query-client.ts (found an existing query client at ` +
|
|
190
|
-
`${relative(cwd, placement.relocatedQueryClient)}) — import it from there instead of ` +
|
|
191
|
-
're-seeding at the original path.',
|
|
192
|
-
)
|
|
168
|
+
// The permanent placement skips (repo-root-shaped tooling, a relocated scaffold) — one text,
|
|
169
|
+
// shared with `sync`. See {@link placementNotices}.
|
|
170
|
+
for (const line of placementNotices(cwd, placement)) {
|
|
171
|
+
console.log(`basalt-ui init: ${line}`)
|
|
193
172
|
}
|
|
194
173
|
// query-client.ts / __root.tsx reference the optional TanStack peers directly — seeding them
|
|
195
174
|
// without the peer installed would ship an unresolved import. Hint how to opt in instead.
|
package/src/cli/sync.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* resolution back from `./index`, this package's shared CLI helpers.
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync } from 'node:fs'
|
|
8
|
-
import {
|
|
8
|
+
import { resolve } from 'node:path'
|
|
9
9
|
|
|
10
10
|
import { deriveSpacing } from '../tokens'
|
|
11
11
|
import {
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
managedFiles,
|
|
22
22
|
normalizeForLedger,
|
|
23
23
|
parseJsonc,
|
|
24
|
+
placementNotices,
|
|
24
25
|
pruneRetiredManagedFiles,
|
|
25
26
|
readBasaltConfig,
|
|
26
27
|
readIfExists,
|
|
@@ -232,26 +233,12 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
|
|
|
232
233
|
})
|
|
233
234
|
staleForCheck += pruned.removed.length + pruned.drifted.length
|
|
234
235
|
|
|
235
|
-
// Placement notices — informational, never
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
'node_modules/basalt-ui/configs/lefthook.yml from your root lefthook.yml instead.',
|
|
242
|
-
)
|
|
243
|
-
console.log(
|
|
244
|
-
`basalt-ui sync: skipped .github/workflows/check.yml (this package is not the repo root — ` +
|
|
245
|
-
`repo root detected at ${placement.repoRoot}) — GitHub Actions only reads .github/ at the ` +
|
|
246
|
-
'repo root; extend node_modules/basalt-ui/configs/check.yml from your root CI workflow instead.',
|
|
247
|
-
)
|
|
248
|
-
}
|
|
249
|
-
if (placement.relocatedQueryClient !== null) {
|
|
250
|
-
console.log(
|
|
251
|
-
`basalt-ui sync: skipped src/query-client.ts (found an existing query client at ` +
|
|
252
|
-
`${relative(cwd, placement.relocatedQueryClient)}) — import it from there instead of ` +
|
|
253
|
-
're-seeding at the original path.',
|
|
254
|
-
)
|
|
236
|
+
// Placement notices — informational, never affecting the exit code, and PERMANENT: `--check`
|
|
237
|
+
// hides them behind `--verbose` (it sits in consumers' CI chains, where three unchanging
|
|
238
|
+
// "skipped" paragraphs per run are noise indistinguishable from drift) but still prints the COUNT.
|
|
239
|
+
const placementSkips = placementNotices(cwd, placement)
|
|
240
|
+
if (opts.check !== true || syncFlags.includes('--verbose')) {
|
|
241
|
+
for (const line of placementSkips) console.log(`basalt-ui sync: ${line}`)
|
|
255
242
|
}
|
|
256
243
|
|
|
257
244
|
const rootsMessages = rootsNotices(rootsState)
|
|
@@ -298,7 +285,9 @@ export function sync(opts: SyncOptions = {}, invocationCwd: string = process.cwd
|
|
|
298
285
|
console.error(`basalt-ui sync --check: ${staleForCheck} managed file(s) out of date.`)
|
|
299
286
|
return 1
|
|
300
287
|
}
|
|
301
|
-
|
|
288
|
+
const n = placementSkips.length
|
|
289
|
+
const skips = n === 0 ? '' : ` (${n} permanent placement skip(s) — --verbose for detail)`
|
|
290
|
+
console.log(`✓ basalt-ui sync --check: all managed files current.${skips}`)
|
|
302
291
|
return 0
|
|
303
292
|
}
|
|
304
293
|
|
package/src/controls/actions.tsx
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* media query, so there is no first-paint flash and no hook that re-renders on resize.
|
|
14
14
|
*/
|
|
15
15
|
import { ActionIcon, Button, Group, Menu } from '@mantine/core'
|
|
16
|
+
import type { MantineBreakpoint } from '@mantine/core'
|
|
16
17
|
import { createContext, isValidElement, useContext, useEffect } from 'react'
|
|
17
18
|
import type { ReactNode } from 'react'
|
|
18
19
|
import type { BasaltProps } from '../common/props'
|
|
@@ -187,6 +188,19 @@ const MOBILE_PRIMARY_PADDING_X = 10
|
|
|
187
188
|
*/
|
|
188
189
|
const BAR_GAP = 6
|
|
189
190
|
|
|
191
|
+
/**
|
|
192
|
+
* A declared WIDTH TIER for one rendered form of an action — Mantine's own `visibleFrom`/`hiddenFrom`
|
|
193
|
+
* style props, forwarded verbatim, which is the same CSS-only mechanism this file's `sm` swap uses
|
|
194
|
+
* (see the module header). Never a JS media query: no first-paint flash, no resize re-render.
|
|
195
|
+
*
|
|
196
|
+
* It exists because `sm` is not the only width at which this row runs out of room — see
|
|
197
|
+
* {@link BarEntry}'s `md` label fold for the arithmetic.
|
|
198
|
+
*/
|
|
199
|
+
type TierVisibility = {
|
|
200
|
+
visibleFrom?: MantineBreakpoint
|
|
201
|
+
hiddenFrom?: MantineBreakpoint
|
|
202
|
+
}
|
|
203
|
+
|
|
190
204
|
/**
|
|
191
205
|
* One action as a button. No `size` prop anywhere: the enclosing `CtlSlot` (mounted by the home,
|
|
192
206
|
* never by the action) resolves the `ctl` tier — law C5.
|
|
@@ -195,15 +209,19 @@ function BarButton({
|
|
|
195
209
|
action,
|
|
196
210
|
variant,
|
|
197
211
|
px,
|
|
212
|
+
tier,
|
|
198
213
|
}: {
|
|
199
214
|
action: BarActionItem
|
|
200
215
|
variant: 'filled' | 'default'
|
|
201
216
|
/** Overrides the tier's horizontal inset — only the compact mobile primary passes it. */
|
|
202
217
|
px?: number
|
|
218
|
+
/** The declared width tier this form paints at — see {@link TierVisibility}. */
|
|
219
|
+
tier?: TierVisibility
|
|
203
220
|
}): ReactNode {
|
|
204
221
|
const shared = {
|
|
205
222
|
variant,
|
|
206
223
|
...(px !== undefined && { px }),
|
|
224
|
+
...tier,
|
|
207
225
|
disabled: action.disabled === true,
|
|
208
226
|
loading: action.loading === true,
|
|
209
227
|
// EVERY icon in this file goes through `IconSlot` — the box is the framework's, never the
|
|
@@ -234,13 +252,17 @@ function BarButton({
|
|
|
234
252
|
function BarIconButton({
|
|
235
253
|
action,
|
|
236
254
|
variant,
|
|
255
|
+
tier,
|
|
237
256
|
}: {
|
|
238
257
|
action: BarActionItem
|
|
239
258
|
/** `default` is the bordered desktop form — a joined `ControlGroup` member (see {@link BarEntry}). */
|
|
240
259
|
variant: 'filled' | 'subtle' | 'default'
|
|
260
|
+
/** The declared width tier this form paints at — see {@link TierVisibility}. */
|
|
261
|
+
tier?: TierVisibility
|
|
241
262
|
}): ReactNode {
|
|
242
263
|
const shared = {
|
|
243
264
|
variant,
|
|
265
|
+
...tier,
|
|
244
266
|
'aria-label': action.label,
|
|
245
267
|
disabled: action.disabled === true,
|
|
246
268
|
loading: action.loading === true,
|
|
@@ -698,6 +720,35 @@ function BarEntry({
|
|
|
698
720
|
if (iconOnly === true && action.icon !== undefined) {
|
|
699
721
|
return <BarIconButton action={action} variant="default" />
|
|
700
722
|
}
|
|
723
|
+
// THE `md` LABEL FOLD — the second declared tier, and the only fold this row had between `sm`
|
|
724
|
+
// (48em, where the whole mobile swap happens) and the width at which a desktop header is
|
|
725
|
+
// genuinely wide. Above `sm` the only fold was `DESKTOP_SECONDARY_MAX`, which is a COUNT and
|
|
726
|
+
// never a width: three labelled secondaries are three labelled secondaries at 768px and at
|
|
727
|
+
// 2560px.
|
|
728
|
+
//
|
|
729
|
+
// The arithmetic, on /dashboard at exactly 768px — the width where the navbar appears and takes
|
|
730
|
+
// 256px: 512px of header left, against ~554px of row 1 + ~102px of globals + ~48px of gaps ≈
|
|
731
|
+
// 704px. ~192px over, and nothing above `sm` folded — the breadcrumb absorbed it by shrinking to
|
|
732
|
+
// zero (`shell/app-header.module.css`'s `.lead`, now floored at 96px, which this tier pays for).
|
|
733
|
+
// Dropping up to `DESKTOP_SECONDARY_MAX` labelled secondaries to their 30px icon form returns
|
|
734
|
+
// roughly 180-240px on that row: a labelled `ctl` button is its icon slot + label + insets
|
|
735
|
+
// (~90-110px depending on the word), an icon-only one is the tier's 30px square.
|
|
736
|
+
//
|
|
737
|
+
// The PRIMARY keeps its label at every width — it is the one action a reader must be able to
|
|
738
|
+
// name without hovering (law C6: exactly one per home), so `emphasis === 'primary'` never enters
|
|
739
|
+
// this branch. An icon-LESS secondary keeps its label too: there is nothing to fall back to, the
|
|
740
|
+
// same reason `MobileBarEntry` renders a compact labelled button below `sm`.
|
|
741
|
+
//
|
|
742
|
+
// Both forms are rendered and CSS picks one, the mechanism the `sm` swap already uses — that is
|
|
743
|
+
// law C9 being HONOURED, not breached: the swap belongs to the control, and this is the control.
|
|
744
|
+
if (emphasis === 'secondary' && action.icon !== undefined) {
|
|
745
|
+
return (
|
|
746
|
+
<>
|
|
747
|
+
<BarIconButton action={action} variant="default" tier={{ hiddenFrom: 'md' }} />
|
|
748
|
+
<BarButton action={action} variant="default" tier={{ visibleFrom: 'md' }} />
|
|
749
|
+
</>
|
|
750
|
+
)
|
|
751
|
+
}
|
|
701
752
|
return <BarButton action={action} variant={emphasis === 'primary' ? 'filled' : 'default'} />
|
|
702
753
|
}
|
|
703
754
|
|
|
@@ -6,20 +6,27 @@
|
|
|
6
6
|
|
|
7
7
|
/*
|
|
8
8
|
* 12px from the header to the body, and the same between two body children — one rhythm, read from
|
|
9
|
-
* the stack scale (`--vx-space-
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* the stack scale (`SPACE.stackMd`, via the `--vx-space-section-gap` role token) instead of the
|
|
10
|
+
* card-inset scale (13px). A Section is a page-level GROUPING, so its gaps belong to the vertical
|
|
11
|
+
* rhythm, not to the inset ladder its cards use.
|
|
12
|
+
*
|
|
13
|
+
* Below `sm` it steps to `stackSm` (8px), together with the cards' own horizontal inset. That step
|
|
14
|
+
* is NOT written here: it belongs to the mobile inset ladder, whose ONE home is `spaceMobileDecls`
|
|
15
|
+
* in `tokens/index.ts` — this rule only reads the role token and gets the responsive value for
|
|
16
|
+
* free. See that function's doc for why the law lives in the emitter rather than in the three
|
|
17
|
+
* stylesheets that consume it (two of the three are inline styles, which cannot hold a media
|
|
18
|
+
* query at all).
|
|
12
19
|
*/
|
|
13
20
|
.root {
|
|
14
21
|
display: flex;
|
|
15
22
|
flex-direction: column;
|
|
16
|
-
gap: var(--vx-space-
|
|
23
|
+
gap: var(--vx-space-section-gap, 0.75rem);
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
.body {
|
|
20
27
|
display: flex;
|
|
21
28
|
flex-direction: column;
|
|
22
|
-
gap: var(--vx-space-
|
|
29
|
+
gap: var(--vx-space-section-gap, 0.75rem);
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/*
|
|
@@ -48,8 +55,9 @@
|
|
|
48
55
|
* The collapse chevron — a plain control, not a Mantine ActionIcon, so it sits directly in the header
|
|
49
56
|
* row (its own CtlSlot fragment would nest a theme provider around one glyph). It is sized to the
|
|
50
57
|
* `ctl` tier BY HAND for exactly that reason: a 12px glyph in a 20px box was a different height from
|
|
51
|
-
* every other control in the same header, which is what made the section header read as ragged.
|
|
52
|
-
* square, glyph centred, no padding — the box IS
|
|
58
|
+
* every other control in the same header, which is what made the section header read as ragged. 32px
|
|
59
|
+
* square (the `ctl` tier, 30 before the desktop-shell pass), glyph centred, no padding — the box IS
|
|
60
|
+
* the tier.
|
|
53
61
|
*/
|
|
54
62
|
.chevron {
|
|
55
63
|
/* `relative` is the anchor for the C15 hit-area overlay in the `< sm` block at the bottom. */
|
|
@@ -378,7 +378,12 @@ export function StatCard({
|
|
|
378
378
|
<Card
|
|
379
379
|
className={cx(classNames?.root, className)}
|
|
380
380
|
style={{
|
|
381
|
-
// Card inset =
|
|
381
|
+
// Card inset = the `cardInsetY`/`cardInsetX` role tokens (11/13 at level 0 — the same
|
|
382
|
+
// `xs`/`sm` rungs this used to spell as `--mantine-spacing-*`), matching every other basalt
|
|
383
|
+
// card because `ChartCard` reads the SAME two vars. Below `sm` the horizontal one steps a
|
|
384
|
+
// rung down on its own: the responsive half of the ladder lives in `spaceMobileDecls`
|
|
385
|
+
// (`tokens/index.ts`), which is the only home that can reach an inline style like this one.
|
|
386
|
+
// `overflow: hidden` clips
|
|
382
387
|
// the full-bleed sparkline to the card's rounded corners; an element's own `box-shadow`
|
|
383
388
|
// renders outside its border box and is NOT clipped by its own `overflow`, so the
|
|
384
389
|
// shadow-card ring is unaffected (verified — only an ANCESTOR's overflow clips a
|
|
@@ -390,7 +395,7 @@ export function StatCard({
|
|
|
390
395
|
// body is `height: 100%` and the sparkline is `margin-top: auto`. The content is what states
|
|
391
396
|
// the height now (~88-96px), and four cards in a `SimpleGrid` still match because a grid row
|
|
392
397
|
// stretches its items.
|
|
393
|
-
padding: 'var(--
|
|
398
|
+
padding: 'var(--vx-space-card-inset-y, 0.6875rem) var(--vx-space-card-inset-x, 0.8125rem)',
|
|
394
399
|
overflow: 'hidden',
|
|
395
400
|
// Anchors the tone rail. Set unconditionally so a card's stacking context does not change
|
|
396
401
|
// depending on whether it happens to have crossed a threshold this render.
|
|
@@ -1,10 +1,41 @@
|
|
|
1
1
|
/*
|
|
2
|
-
* StatGroup's column law and its optional rail, in CSS (law C9).
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* the
|
|
2
|
+
* StatGroup's column law and its optional rail, in CSS (law C9).
|
|
3
|
+
*
|
|
4
|
+
* **The law keys on the CONTAINER, not the viewport.** A viewport breakpoint answers the wrong
|
|
5
|
+
* question — the same one `resolveChartTier` refuses to ask (`charts/primitives/chart-frame-layout.
|
|
6
|
+
* ts`): the shell has regions that change the AVAILABLE width with the viewport held still (a
|
|
7
|
+
* `PageAside` claiming its 300px, the sidebar collapsing 256px → its 48px rail, a consumer's own
|
|
8
|
+
* split layout), and `@media` cannot see any of them. Measured defect: a `PageAside` mounted on a
|
|
9
|
+
* 1512px desktop left the KPI row ~956px wide, `@media` still resolved `lg`, and four cells of
|
|
10
|
+
* 239px truncated their own values mid-word ("8,7…", "Organic se…").
|
|
11
|
+
*
|
|
12
|
+
* `.container` is what makes the queries answerable — `StatGroup` renders it around the grid, so
|
|
13
|
+
* the container context ALWAYS exists and is ALWAYS the box the grid actually fills, whatever the
|
|
14
|
+
* shell or the consumer wrapped it in. It deliberately does not depend on the shell establishing
|
|
15
|
+
* one: a query with no container resolves to false, so a shell-only context would have silently
|
|
16
|
+
* pinned every non-shell consumer to the base count.
|
|
17
|
+
*
|
|
18
|
+
* The `@supports not` block below it is the pre-2023 fallback and the ONLY place the old viewport
|
|
19
|
+
* law survives. It is written as an exclusive alternative rather than as a base the container
|
|
20
|
+
* queries override, because the rail's `:nth-child` suppressors are MORE specific than the rule
|
|
21
|
+
* that draws the border: two overlapping laws would each suppress a different set of leading cells
|
|
22
|
+
* and the rail would lose hairlines in the middle of a row. One law at a time, chosen by support.
|
|
23
|
+
*
|
|
24
|
+
* The literals: `@container` conditions are stated in px, `@media` ones in Mantine's own `em`
|
|
25
|
+
* (`--mantine-breakpoint-sm` 48em, `--mantine-breakpoint-lg` 75em). Both spellings name the same
|
|
26
|
+
* boundary at the 16px initial font size — 768px and 1200px — but `em` inside a `@container`
|
|
27
|
+
* condition resolves against the CONTAINER's font-size rather than the root's, so a consumer panel
|
|
28
|
+
* with smaller text would silently move the breakpoint. px is the only spelling that cannot drift.
|
|
29
|
+
* (`47.99375em` → `767.9px`, `74.99375em` → `1199.9px`; the same complements `stat-card.module.css`
|
|
30
|
+
* uses.) A condition still cannot read a custom property either way, so the numbers are written;
|
|
31
|
+
* the COUNTS are custom properties the component sets per instance.
|
|
6
32
|
*/
|
|
7
33
|
|
|
34
|
+
.container {
|
|
35
|
+
container-type: inline-size;
|
|
36
|
+
container-name: basalt-stat-group;
|
|
37
|
+
}
|
|
38
|
+
|
|
8
39
|
.root {
|
|
9
40
|
display: grid;
|
|
10
41
|
/* Two KPIs abreast on a phone, never one: a KPI is a short pre-formatted number, and a single
|
|
@@ -13,13 +44,13 @@
|
|
|
13
44
|
gap: var(--vx-space-stack-md);
|
|
14
45
|
}
|
|
15
46
|
|
|
16
|
-
@
|
|
47
|
+
@container basalt-stat-group (min-width: 768px) {
|
|
17
48
|
.root {
|
|
18
49
|
grid-template-columns: repeat(var(--basalt-stat-group-cols-sm), minmax(0, 1fr));
|
|
19
50
|
}
|
|
20
51
|
}
|
|
21
52
|
|
|
22
|
-
@
|
|
53
|
+
@container basalt-stat-group (min-width: 1200px) {
|
|
23
54
|
.root {
|
|
24
55
|
grid-template-columns: repeat(var(--basalt-stat-group-cols-lg), minmax(0, 1fr));
|
|
25
56
|
}
|
|
@@ -40,14 +71,14 @@
|
|
|
40
71
|
padding-inline-start: var(--vx-space-stack-md);
|
|
41
72
|
}
|
|
42
73
|
|
|
43
|
-
@
|
|
74
|
+
@container basalt-stat-group (max-width: 767.9px) {
|
|
44
75
|
.root[data-divided] > *:nth-child(2n + 1) {
|
|
45
76
|
border-inline-start: 0;
|
|
46
77
|
padding-inline-start: 0;
|
|
47
78
|
}
|
|
48
79
|
}
|
|
49
80
|
|
|
50
|
-
@
|
|
81
|
+
@container basalt-stat-group (min-width: 768px) and (max-width: 1199.9px) {
|
|
51
82
|
.root[data-divided][data-sm-cols='2'] > *:nth-child(2n + 1),
|
|
52
83
|
.root[data-divided][data-sm-cols='3'] > *:nth-child(3n + 1) {
|
|
53
84
|
border-inline-start: 0;
|
|
@@ -55,7 +86,7 @@
|
|
|
55
86
|
}
|
|
56
87
|
}
|
|
57
88
|
|
|
58
|
-
@
|
|
89
|
+
@container basalt-stat-group (min-width: 1200px) {
|
|
59
90
|
.root[data-divided][data-cols='2'] > *:nth-child(2n + 1),
|
|
60
91
|
.root[data-divided][data-cols='3'] > *:nth-child(3n + 1),
|
|
61
92
|
.root[data-divided][data-cols='4'] > *:nth-child(4n + 1),
|
|
@@ -64,3 +95,48 @@
|
|
|
64
95
|
padding-inline-start: 0;
|
|
65
96
|
}
|
|
66
97
|
}
|
|
98
|
+
|
|
99
|
+
/*
|
|
100
|
+
* The pre-container-query fallback — Safari < 16, Chrome < 106, Firefox < 110. Verbatim the law
|
|
101
|
+
* this file shipped before, keyed on the viewport, and reachable ONLY where `@container` is not.
|
|
102
|
+
* A browser that cannot parse `container-type` also cannot parse the `@container` blocks above, so
|
|
103
|
+
* the two sets are never live at once.
|
|
104
|
+
*/
|
|
105
|
+
@supports not (container-type: inline-size) {
|
|
106
|
+
@media (min-width: 48em) {
|
|
107
|
+
.root {
|
|
108
|
+
grid-template-columns: repeat(var(--basalt-stat-group-cols-sm), minmax(0, 1fr));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@media (min-width: 75em) {
|
|
113
|
+
.root {
|
|
114
|
+
grid-template-columns: repeat(var(--basalt-stat-group-cols-lg), minmax(0, 1fr));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@media (max-width: 47.99375em) {
|
|
119
|
+
.root[data-divided] > *:nth-child(2n + 1) {
|
|
120
|
+
border-inline-start: 0;
|
|
121
|
+
padding-inline-start: 0;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@media (min-width: 48em) and (max-width: 74.99375em) {
|
|
126
|
+
.root[data-divided][data-sm-cols='2'] > *:nth-child(2n + 1),
|
|
127
|
+
.root[data-divided][data-sm-cols='3'] > *:nth-child(3n + 1) {
|
|
128
|
+
border-inline-start: 0;
|
|
129
|
+
padding-inline-start: 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@media (min-width: 75em) {
|
|
134
|
+
.root[data-divided][data-cols='2'] > *:nth-child(2n + 1),
|
|
135
|
+
.root[data-divided][data-cols='3'] > *:nth-child(3n + 1),
|
|
136
|
+
.root[data-divided][data-cols='4'] > *:nth-child(4n + 1),
|
|
137
|
+
.root[data-divided][data-cols='5'] > *:nth-child(5n + 1) {
|
|
138
|
+
border-inline-start: 0;
|
|
139
|
+
padding-inline-start: 0;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|