basalt-ui 1.6.0 → 1.8.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/AGENTS.md +1 -1
- package/CHANGELOG.md +19 -0
- package/agent/rules/basalt-mantine.md +80 -40
- package/agent/rules/basalt-tokens.md +3 -1
- package/agent/skills/basalt-design/SKILL.md +24 -7
- package/agent/templates/CLAUDE-block.md.tpl +3 -2
- package/agent/templates/DESIGN.md.tpl +2 -1
- package/configs/oxlint-plugin.js +75 -1
- package/configs/oxlint.json +1 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2 -0
- package/dist/dashboard/index.d.ts +1 -1
- package/dist/dashboard/index.d.ts.map +1 -1
- package/dist/dashboard/stat-card.d.ts +8 -1
- package/dist/dashboard/stat-card.d.ts.map +1 -1
- package/dist/dashboard/stat-card.js +42 -4
- package/dist/guard/index.d.ts +7 -1
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +16 -1
- package/dist/guard/types.d.ts +18 -3
- package/dist/guard/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/surfaces.d.ts +2 -2
- package/dist/surfaces.d.ts.map +1 -1
- package/dist/surfaces.js +8 -2
- package/dist/theme/controls.module.css +5 -3
- package/dist/vite.d.ts +6 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +21 -1
- package/llms.txt +3 -3
- package/package.json +1 -1
- package/src/cli/index.ts +11 -0
- package/src/dashboard/index.ts +1 -1
- package/src/dashboard/stat-card.test.tsx +81 -0
- package/src/dashboard/stat-card.tsx +75 -2
- package/src/guard/check-source.test.ts +64 -1
- package/src/guard/index.ts +42 -3
- package/src/guard/types.ts +18 -2
- package/src/index.ts +1 -0
- package/src/surfaces.ts +8 -2
- package/src/theme/contrast.test.ts +18 -1
- package/src/theme/controls.module.css +5 -3
- package/src/vite.ts +62 -1
package/src/guard/index.ts
CHANGED
|
@@ -58,6 +58,20 @@ const WITH_BORDER_PROP = /\bwithBorder\b(?!\s*=\s*\{\s*false\s*\})/
|
|
|
58
58
|
// Raw Mantine ramp step used for surface color — gray/dark + a step digit.
|
|
59
59
|
const OFF_SYSTEM_SURFACE_VAR = /var\(--mantine-color-(gray|dark)-\d/g
|
|
60
60
|
|
|
61
|
+
// A SHADE-PINNED Mantine color: the dotted JSX form (`c="yellow.7"`) or the var() form
|
|
62
|
+
// (`var(--mantine-color-red-6)`). Both name one fixed swatch that does NOT flip across color
|
|
63
|
+
// schemes — see the `mantineShadeIndex` doc in guard/types.ts for why the bare hue name is fine and
|
|
64
|
+
// the pinned step is not.
|
|
65
|
+
//
|
|
66
|
+
// The var() half deliberately excludes gray-/dark-, which `off-system-surface-var` already owns, so
|
|
67
|
+
// a surface violation reports once under one kind rather than twice under two.
|
|
68
|
+
//
|
|
69
|
+
// The JSX half is prop-shaped (`c="yellow.7"` needs the `=`), which CSS text has no syntax for, so
|
|
70
|
+
// it cannot fire in a `.css` file; the var() half is a real violation in CSS and is meant to fire
|
|
71
|
+
// there. That split is why this kind, unlike its prop-only neighbours, carries no `appliesTo` gate.
|
|
72
|
+
const MANTINE_SHADE_INDEX =
|
|
73
|
+
/\b(?:color|c|bg|backgroundColor|borderColor)\s*=\s*\{?\s*['"][a-z]+\.\d\b|var\(--mantine-color-(?!gray-|dark-)[a-z]+-\d/g
|
|
74
|
+
|
|
61
75
|
// Raw lowercase JSX layout/surface element with inline style and layout/surface prop — line-scoped.
|
|
62
76
|
const RAW_HTML_TAG = /<(?:div|span|section|header|nav|footer|aside|main|article|ul|ol)\b/
|
|
63
77
|
const INLINE_STYLE = /style=\{\{/
|
|
@@ -241,6 +255,7 @@ export const DEFAULT_GUARD_CONFIG: GuardConfig = {
|
|
|
241
255
|
spacingSteps: DEFAULT_SPACING_STEPS,
|
|
242
256
|
rawRadius: true,
|
|
243
257
|
forbiddenAccents: DEFAULT_FORBIDDEN_ACCENTS,
|
|
258
|
+
mantineShadeIndex: true,
|
|
244
259
|
rawSurface: true,
|
|
245
260
|
cardWithBorder: true,
|
|
246
261
|
offSystemSurfaceVar: true,
|
|
@@ -266,14 +281,21 @@ export const DEFAULT_GUARD_CONFIG: GuardConfig = {
|
|
|
266
281
|
* someone remembering. Adding a kind here is part of shipping it; removing the entry IS the
|
|
267
282
|
* promotion, and belongs in its own commit so the changelog says enforcement got stricter.
|
|
268
283
|
*
|
|
269
|
-
*
|
|
284
|
+
* `mantine-shade-index` landed in 1.7.0 and is the sole entry — it rejects `c="yellow.7"`, which
|
|
285
|
+
* previously passed, and the one real consumer has several. Its promotion is **deferred to 1.9.0**:
|
|
286
|
+
* 1.8.0 shipped the same day as 1.7.0, so promoting on the next minor number would have given the
|
|
287
|
+
* consumer roughly no runway at all — which is the exact failure the grace minor exists to prevent,
|
|
288
|
+
* and the runway is measured in consumer upgrades, not in version numbers. Promote it by deleting
|
|
289
|
+
* the entry (its own commit).
|
|
270
290
|
*
|
|
271
291
|
* @example
|
|
272
292
|
* const GRACE: Partial<Record<GuardKind, string>> = {
|
|
273
293
|
* 'raw-font-family': 'introduced 1.4.0 — promote to error in 1.5.0',
|
|
274
294
|
* }
|
|
275
295
|
*/
|
|
276
|
-
const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, string>> = {
|
|
296
|
+
const GRACE_PERIOD_KINDS: Partial<Record<GuardKind, string>> = {
|
|
297
|
+
'mantine-shade-index': 'introduced 1.7.0 — promote to error in 1.9.0',
|
|
298
|
+
}
|
|
277
299
|
|
|
278
300
|
/** A kind's effective severity: consumer override first, then the grace table, then `error`. */
|
|
279
301
|
function severityOf(kind: GuardKind, cfg: GuardConfig): GuardSeverity {
|
|
@@ -561,7 +583,7 @@ type GuardRule = {
|
|
|
561
583
|
}
|
|
562
584
|
|
|
563
585
|
/**
|
|
564
|
-
* The closed registry of all
|
|
586
|
+
* The closed registry of all 20 guard kinds. The triad test asserts
|
|
565
587
|
* `surface.guardKinds ⊆ keyof GUARD_RULES` at runtime.
|
|
566
588
|
*
|
|
567
589
|
* raw-surface, raw-html-layout, and sub-16-input-font are handled inline in checkSource
|
|
@@ -612,6 +634,13 @@ export const GUARD_RULES = {
|
|
|
612
634
|
appliesTo: (relPath) => !relPath.endsWith('.css'),
|
|
613
635
|
message: 'For an off-identity accent use blue/gray or a status hue (red/green/orange/yellow).',
|
|
614
636
|
},
|
|
637
|
+
'mantine-shade-index': {
|
|
638
|
+
kind: 'mantine-shade-index',
|
|
639
|
+
pattern: MANTINE_SHADE_INDEX,
|
|
640
|
+
enabled: (cfg: GuardConfig) => cfg.mantineShadeIndex,
|
|
641
|
+
message:
|
|
642
|
+
'Shade-pinned Mantine color — one fixed swatch in BOTH color schemes, so it cannot stay legible in either. Use VX.status.* / --vx-status-* for a verdict color, or the bare hue name (c="red") to let the theme resolve the shade per scheme.',
|
|
643
|
+
},
|
|
615
644
|
'raw-spacing': {
|
|
616
645
|
kind: 'raw-spacing',
|
|
617
646
|
pattern: (cfg: GuardConfig) =>
|
|
@@ -842,6 +871,16 @@ export function checkSource(text: string, relPath: string, cfg: GuardConfig): Fi
|
|
|
842
871
|
}
|
|
843
872
|
}
|
|
844
873
|
|
|
874
|
+
// mantine-shade-index — the sibling of off-system-surface-var above, partitioned by hue: that
|
|
875
|
+
// kind owns gray/dark (surface color), this one owns every other shade-pinned step (verdict and
|
|
876
|
+
// accent color). Both halves of the pattern (JSX prop + var()) run in every file type; see the
|
|
877
|
+
// pattern's own comment for why this kind carries no appliesTo gate.
|
|
878
|
+
if (GUARD_RULES['mantine-shade-index'].enabled!(cfg)) {
|
|
879
|
+
for (const m of line.matchAll(GUARD_RULES['mantine-shade-index'].pattern as RegExp)) {
|
|
880
|
+
findings.push({ relPath, line: i + 1, token: m[0], kind: 'mantine-shade-index' })
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
845
884
|
// raw-html-layout: 3-condition conjunction on the same line — gated via GUARD_RULES entry.
|
|
846
885
|
if (
|
|
847
886
|
GUARD_RULES['raw-html-layout'].enabled!(cfg) &&
|
package/src/guard/types.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Guard types — Mantine-free, dependency-free (zero imports beyond TS types).
|
|
3
3
|
*
|
|
4
|
-
* GuardKind is the closed set of
|
|
4
|
+
* GuardKind is the closed set of 20 violation kinds the theme guard can emit.
|
|
5
5
|
* Finding is the structured result per violation, replacing the old `Violation` shape.
|
|
6
6
|
* GuardConfig is the per-run configuration that drives checkSource.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
/** The
|
|
9
|
+
/** The 20 theme-guard violation kinds. */
|
|
10
10
|
export type GuardKind =
|
|
11
11
|
| 'raw-hex'
|
|
12
12
|
| 'raw-color-fn'
|
|
13
13
|
| 'localstorage-theme'
|
|
14
14
|
| 'off-identity-accent'
|
|
15
|
+
| 'mantine-shade-index'
|
|
15
16
|
| 'raw-spacing'
|
|
16
17
|
| 'raw-radius'
|
|
17
18
|
| 'raw-surface'
|
|
@@ -82,6 +83,21 @@ export type GuardConfig = {
|
|
|
82
83
|
readonly rawRadius: boolean
|
|
83
84
|
/** Off-identity Mantine accent families forbidden as chrome accents. */
|
|
84
85
|
readonly forbiddenAccents: readonly string[]
|
|
86
|
+
/**
|
|
87
|
+
* Flag a SHADE-PINNED Mantine color — `c="yellow.7"`, `bg="blue.4"`, `var(--mantine-color-red-6)`.
|
|
88
|
+
*
|
|
89
|
+
* This is the sibling of `off-identity-accent`, and the two are deliberately disjoint. That kind
|
|
90
|
+
* polices WHICH hue you reach for and permits the bare status names (`c="red"`, `color="green"`)
|
|
91
|
+
* on purpose: they resolve through the theme, so they flip correctly across color schemes. This
|
|
92
|
+
* kind polices the SHADE INDEX, which does not — a pinned step is one fixed swatch in both
|
|
93
|
+
* schemes, so a `yellow.7` legible on the dark page is the one that fails contrast on the light
|
|
94
|
+
* one. Status color belongs on `VX.status.*` (`--vx-status-warn|bad|good|…`), which is emitted per
|
|
95
|
+
* scheme.
|
|
96
|
+
*
|
|
97
|
+
* `gray-*`/`dark-*` in `var()` form are excluded here because `off-system-surface-var` already
|
|
98
|
+
* owns them (surface color, its own message). Default true.
|
|
99
|
+
*/
|
|
100
|
+
readonly mantineShadeIndex: boolean
|
|
85
101
|
/** Flag ad-hoc inline surface styling (border/borderRadius/boxShadow literals). Default true. */
|
|
86
102
|
readonly rawSurface: boolean
|
|
87
103
|
/**
|
package/src/index.ts
CHANGED
package/src/surfaces.ts
CHANGED
|
@@ -186,9 +186,15 @@ export const SURFACES = {
|
|
|
186
186
|
layer: 'mantine-coupled',
|
|
187
187
|
rule: 'mantine',
|
|
188
188
|
skill: ['basalt-app', 'basalt-design'],
|
|
189
|
-
guardKinds: [
|
|
189
|
+
guardKinds: [
|
|
190
|
+
'raw-motion-value',
|
|
191
|
+
'card-with-border',
|
|
192
|
+
'raw-form-control',
|
|
193
|
+
'sub-16-input-font',
|
|
194
|
+
'mantine-shade-index',
|
|
195
|
+
],
|
|
190
196
|
description:
|
|
191
|
-
'BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, dashboard composites (DeltaBadge, StatCard, EmptyState, SettingsSection/SettingsRow/DangerZone)',
|
|
197
|
+
'BasaltProvider, createBasaltTheme, BasaltShell + sidebar/mobile-nav/breadcrumbs, NavCountBadge, ThemeToggle, ThreadWorkspace + thread-chat components, dashboard composites (DeltaBadge, StatCard with threshold tone, EmptyState, SettingsSection/SettingsRow/DangerZone)',
|
|
192
198
|
optionalPeers: ['react-markdown', 'remark-gfm', 'shiki', 'beautiful-mermaid'],
|
|
193
199
|
forbiddenImports: [], // the no-charts/tokens-reexport invariant is comment-only today; Phase-4 plugin
|
|
194
200
|
},
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
import { DEFAULT_THEME, getPrimaryShade, mergeMantineTheme } from '@mantine/core'
|
|
25
25
|
import type { MantineTheme } from '@mantine/core'
|
|
26
26
|
import { describe, expect, test } from 'bun:test'
|
|
27
|
-
import { ACCENT, FILL, SURFACE } from '../tokens/palette'
|
|
27
|
+
import { ACCENT, FILL, STATUS, SURFACE } from '../tokens/palette'
|
|
28
28
|
import { baseTheme, cssVariablesResolver } from './index'
|
|
29
29
|
|
|
30
30
|
const theme: MantineTheme = mergeMantineTheme(DEFAULT_THEME, baseTheme)
|
|
@@ -324,6 +324,23 @@ describe('doctrine inversion #1 reaches every default-variant CONTROL, not just
|
|
|
324
324
|
})
|
|
325
325
|
})
|
|
326
326
|
|
|
327
|
+
describe("the StatCard tone rail reads against the panel it's drawn on", () => {
|
|
328
|
+
// The rail (`dashboard/stat-card.tsx`) is a 3px non-text mark carrying a threshold verdict, so it
|
|
329
|
+
// takes WCAG's 3:1 non-text floor against `--vx-surface-panel` — the card background it overlays,
|
|
330
|
+
// not the page — in BOTH schemes. Worth measuring rather than eyeballing for the same reason the
|
|
331
|
+
// fill band is: the status solids are DERIVED, so retuning the generator could sink one of them
|
|
332
|
+
// into the panel it sits on without anything else failing.
|
|
333
|
+
const TONES = ['good', 'warn', 'bad'] as const
|
|
334
|
+
|
|
335
|
+
for (const scheme of SCHEMES) {
|
|
336
|
+
for (const tone of TONES) {
|
|
337
|
+
test(`${tone} / ${scheme}`, () => {
|
|
338
|
+
expect(contrastRatio(STATUS[tone][scheme], SURFACE.panel[scheme])).toBeGreaterThanOrEqual(3)
|
|
339
|
+
})
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
})
|
|
343
|
+
|
|
327
344
|
describe('the shipped palette is the generator output at DEFAULT_DERIVE_CONFIG', () => {
|
|
328
345
|
// A handful of exact hexes from `deriveTokens(DEFAULT_DERIVE_CONFIG)` (`tokens/derive.ts`) — the
|
|
329
346
|
// shipped default identity (seed #0077bd, zinc, all four knobs at level 0). Drift here means
|
|
@@ -214,8 +214,10 @@ html[data-mantine-color-scheme] .inputWrapper[data-variant='default']:not([data-
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
/*
|
|
217
|
-
* CheckboxCard / RadioCard (`withBorder`, the default): panel bg +
|
|
218
|
-
*
|
|
217
|
+
* CheckboxCard / RadioCard (`withBorder`, the default): panel bg + depth, no border — the same
|
|
218
|
+
* triad as Card/Paper, but on `shadow-raised`, since a selection card is something you click (the
|
|
219
|
+
* control-vs-panel line, see the header above). Mantine's own border-color rule wraps
|
|
220
|
+
* `[data-with-border]` in
|
|
219
221
|
* `:where()` (0 specificity), so a plain class + real attribute selector ((0,2,0)) already wins;
|
|
220
222
|
* no `html[…]` prefix fight needed, same reasoning as Button/ActionIcon above.
|
|
221
223
|
*/
|
|
@@ -240,7 +242,7 @@ html[data-mantine-color-scheme] .inputWrapper[data-variant='default']:not([data-
|
|
|
240
242
|
|
|
241
243
|
/*
|
|
242
244
|
* Focus = accent border (via --input-bd-focus above) + a subtle accent ring layered over the
|
|
243
|
-
* resting `--field-depth`, so the field keeps its shadow-
|
|
245
|
+
* resting `--field-depth`, so the field keeps its `shadow-raised` depth while focused.
|
|
244
246
|
*/
|
|
245
247
|
.input:focus,
|
|
246
248
|
.input:focus-within {
|
package/src/vite.ts
CHANGED
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
*
|
|
14
14
|
* NEVER reference `import.meta.env` here — this file is invoked inside the consumer's vite config
|
|
15
15
|
* at config-evaluation time (plain Node), where only `process.env` exists.
|
|
16
|
+
*
|
|
17
|
+
* It also carries the enforcement notice (`warnIfUnenforced`): this is the one basalt seam that runs
|
|
18
|
+
* on every dev start and every build of a consumer app, so it is the only place that can tell an app
|
|
19
|
+
* it installed the components and skipped `basalt-ui init`.
|
|
16
20
|
*/
|
|
17
|
-
import {
|
|
21
|
+
import { existsSync } from 'node:fs'
|
|
22
|
+
import { dirname, resolve } from 'node:path'
|
|
18
23
|
import { searchForWorkspaceRoot } from 'vite'
|
|
19
24
|
import type { HtmlTagDescriptor, Plugin, PluginOption, UserConfig } from 'vite'
|
|
20
25
|
import { SURFACE } from './tokens/palette'
|
|
@@ -46,6 +51,55 @@ export type BasaltViteOptions = {
|
|
|
46
51
|
basaltSrc?: string
|
|
47
52
|
/** App version surfaced via the `__APP_VERSION__` define. Defaults to `0.0.0`. */
|
|
48
53
|
version?: string
|
|
54
|
+
/**
|
|
55
|
+
* Print a one-time notice when `basalt-ui init` has never been run in this project (no
|
|
56
|
+
* `.basalt/manifest.json` found at or above the cwd) — see `warnIfUnenforced`. Default `true`.
|
|
57
|
+
* Set `false` to adopt the components without the toolchain and stop being told about it.
|
|
58
|
+
*/
|
|
59
|
+
enforcementNotice?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** How many directories up from `cwd` to look for `.basalt/manifest.json`. Three covers the common
|
|
63
|
+
* shapes: config at the app root, an app in `web/` or `apps/<name>/` under a repo root. */
|
|
64
|
+
const UNENFORCED_SEARCH_DEPTH = 3
|
|
65
|
+
|
|
66
|
+
/** Set once the notice has been printed, so a watch-mode restart doesn't reprint it every reload. */
|
|
67
|
+
let unenforcedNoticePrinted = false
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Print one notice when basalt-ui is installed as a component library and nothing else.
|
|
71
|
+
*
|
|
72
|
+
* `basalt-ui init` is what places the oxlint preset, the theme-guard wiring, the lefthook stub and
|
|
73
|
+
* the agent rules. Skipping it leaves a consumer with the components and NONE of the enforcement —
|
|
74
|
+
* and, critically, no signal that this is the case. The failure is silent and cumulative: every
|
|
75
|
+
* hand-rolled `<Card withBorder radius="md" padding="lg">` renders perfectly, so the app drifts a
|
|
76
|
+
* card idiom at a time until someone looks at a screenshot and asks why the design system did
|
|
77
|
+
* nothing. The first real consumer reached seven such cards across six files that way.
|
|
78
|
+
*
|
|
79
|
+
* This is the only seam basalt owns that runs on every dev start and every build, which makes it
|
|
80
|
+
* the only place that can say so at the time it is still cheap to fix. It stays a notice, never an
|
|
81
|
+
* error: declining to adopt the toolchain is a legitimate choice, and a build that fails because a
|
|
82
|
+
* lint preset is absent would be a worse bug than the one this prevents.
|
|
83
|
+
*/
|
|
84
|
+
function warnIfUnenforced(cwd: string): void {
|
|
85
|
+
if (unenforcedNoticePrinted) return
|
|
86
|
+
|
|
87
|
+
let dir = cwd
|
|
88
|
+
for (let i = 0; i <= UNENFORCED_SEARCH_DEPTH; i++) {
|
|
89
|
+
if (existsSync(resolve(dir, '.basalt/manifest.json'))) return
|
|
90
|
+
const parent = dirname(dir)
|
|
91
|
+
if (parent === dir) break
|
|
92
|
+
dir = parent
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
unenforcedNoticePrinted = true
|
|
96
|
+
// oxlint-disable-next-line no-console -- a build-time notice has no other channel
|
|
97
|
+
console.warn(
|
|
98
|
+
'\n[basalt-ui] Installed, but `basalt-ui init` has never run here — no oxlint preset, no theme\n' +
|
|
99
|
+
' guard, no agent rules. You have the components and none of the enforcement.\n' +
|
|
100
|
+
' Fix: `bunx basalt-ui init`, then add `oxlint . && basalt-ui check-theme` as your\n' +
|
|
101
|
+
' lint script. Silence: pass `enforcementNotice: false` to basaltViteConfig().\n',
|
|
102
|
+
)
|
|
49
103
|
}
|
|
50
104
|
|
|
51
105
|
export function basaltViteConfig(opts: BasaltViteOptions): UserConfig {
|
|
@@ -55,6 +109,13 @@ export function basaltViteConfig(opts: BasaltViteOptions): UserConfig {
|
|
|
55
109
|
// on without editing its vite config. process.env (not import.meta.env) — this runs in Node.
|
|
56
110
|
const basaltSrc = opts.basaltSrc ?? process.env['BASALT_LOCAL']
|
|
57
111
|
|
|
112
|
+
// Skipped under basaltSrc/BASALT_LOCAL: that path means basalt-ui itself is being developed from
|
|
113
|
+
// a sibling checkout (the playground, or a consumer debugging against local source), where the
|
|
114
|
+
// enforcement config lives in basalt's own repo and a manifest is not expected.
|
|
115
|
+
if (opts.enforcementNotice !== false && basaltSrc === undefined) {
|
|
116
|
+
warnIfUnenforced(process.cwd())
|
|
117
|
+
}
|
|
118
|
+
|
|
58
119
|
const config: UserConfig = {
|
|
59
120
|
define: {
|
|
60
121
|
__APP_VERSION__: JSON.stringify(process.env['BUILD_VERSION'] ?? version),
|