7.5 KiB
Migrate Mercury UI to the shadcn-vue version of @nychthemeron/library
Background
@nychthemeron/library was rebuilt from PrimeVue (unstyled mode + a custom
ThemeEngine passthrough layer) to shadcn-vue + Tailwind v4. PrimeVue moved
to a paid licensing model, so the old build was replaced in place under the
same 0.0.1 version on the private registry (git.mcpeakdev.com) rather
than published as a new version — confirmed intentional by the maintainer.
The consuming app (Mercury/ui) still targets the old PrimeVue-era API
everywhere: main.ts wires up the PrimeVue plugin, and 10 admin views plus
Login.vue use globally-registered Nych* components with PrimeVue-style
props (label, severity, fluid, v-model:visible, :options/
optionLabel/optionValue). None of that matches the new library's API, so
this is a real port, not a version bump.
Scope
In scope: everything needed to get Mercury UI running correctly against
the new library — build/plugin wiring, and a native rewrite of every view
that uses a Nych* component.
Out of scope: any new features, any styling changes beyond what's needed to preserve current look/behavior, and the library's own source (it's an external package on a separate registry).
Current usage inventory
| File | Button | InputText | Dialog | Select | Textarea | Message |
|---|---|---|---|---|---|---|
| Login.vue | 1 | 2 | ||||
| admin/ApiKeys.vue | 4 | 2 | 2 | 1 | 1 | |
| admin/Blacklist.vue | 5 | 6 | 2 | 1 | ||
| admin/Cache.vue | 2 | |||||
| admin/Cdn.vue | 5 | 6 | 2 | |||
| admin/Cors.vue | 3 | 1 | 1 | |||
| admin/Permissions.vue | 5 | 5 | 2 | |||
| admin/Queries.vue | 5 | 3 | 2 | 2 | ||
| admin/Tables.vue | 7 | 3 | 3 | 1 | ||
| admin/Users.vue | 5 | 4 | 2 | 2 | ||
| Total | 42 | 32 | 16 | 5 | 2 | 1 |
Component API mapping
Button (NychButton) — low friction, variant names carry over almost
1:1:
- Content moves from
label="X"prop to default slot:<NychButton>X</NychButton> severity="danger"→variant="danger"(also:primary(default),secondary,info,success,warning,danger)size="small"→size="sm"(also:default,lg,icon):loading,:disabled— unchangedfluidprop is gone — useclass="w-full"
Input (NychInputText → NychInput) — rename only, v-model,
type, placeholder, autocomplete unchanged. fluid → class="w-full".
Textarea (NychTextarea) — same shape as Input, fluid →
class="w-full".
Dialog — flat component becomes a compound one:
<!-- old -->
<NychDialog v-model:visible="show" header="Title" :modal="true" :draggable="false">
...body...
</NychDialog>
<!-- new -->
<NychDialog v-model:open="show">
<NychDialogContent>
<NychDialogHeader><NychDialogTitle>Title</NychDialogTitle></NychDialogHeader>
...body...
</NychDialogContent>
</NychDialog>
:closable="false"(used once, in ApiKeys.vue's key-reveal dialog, to force the user to acknowledge before dismissing) → pass:show-close-button="false"onNychDialogContentto drop the X button, and ignore attempts to close via@update:open="(v) => { if (v) show = v }"so outside-click/Escape can't dismiss it either.:draggable="false"— no-op, the new Dialog isn't draggable to begin with.- Width customization (
style="width: min(560px, 95vw)") — pass through asclassonNychDialogContentsince it accepts aclassprop merged viacn().
Select — flat :options array becomes compound children:
<!-- old -->
<NychSelect v-model="x" :options="ROLES" optionLabel="label" optionValue="value" placeholder="Select a role" fluid />
<!-- new -->
<NychSelect v-model="x">
<NychSelectTrigger class="w-full"><NychSelectValue placeholder="Select a role" /></NychSelectTrigger>
<NychSelectContent>
<NychSelectItem v-for="r in ROLES" :key="r.value" :value="r.value">{{ r.label }}</NychSelectItem>
</NychSelectContent>
</NychSelect>
Message → Alert — no NychMessage in the new library; NychAlert is
the closest analog:
severity="warn"→variant="warning"(also:info(default),success,danger,secondary)- Body goes in the default slot, same as before.
Phase 1 — Foundation (build/plugin wiring)
src/main.ts— remove the@primevue/core/configimport andapp.use(PrimeVue, { unstyled: true })call. Keepimport '@nychthemeron/library/theme'andapp.use(createNychthemeron())unchanged (both still resolve, just point at new content).vite.config.ts— add the@tailwindcss/viteplugin (required because the library now ships source CSS with@import "tailwindcss"that must be processed at build time, not a precompiled stylesheet). Replace theprimevue: ["@primevue/core"]manual chunk with nothing (package no longer exists).package.json— addtailwindcssand@tailwindcss/viteas direct devDependencies (currently only reachable transitively through@nychthemeron/library, which is fragile) at the versions already resolved inbun.lock. Bumpvuefrom^3.4.0to^3.5.0to match the library's peer requirement.src/assets/main.css— leave the design-token-based custom classes (.table-card,.data-table,.dialog-form,.gold-rule,.loading-overlay, etc.) as-is; they key off CSS custom properties (--surface-1,--primary, ...) which kept the same names and values in the new theme. Remove the dead overrides tied to the old unstyled-PrimeVue class names:.nych-dialog { min-width: 500px },[class^="nych-button"] { ... }, and re-check.nych-loading-icon .wreathstill applies (the newNychLoadingIconkeeps the same class name/markup, so this one likely stays).- Verify with
bun run devthat the app boots without console errors before touching any view.
Phase 2 — View-by-view native migration
Migrate one file at a time, in this order (simplest/lowest-risk first, so mistakes in the component mapping surface early on a small file rather than a large one):
Login.vue(3 call sites, no Dialog/Select — good smoke test)admin/Cache.vue(2 call sites, Button only)admin/Cors.vueadmin/Queries.vueadmin/Blacklist.vueadmin/Permissions.vueadmin/Users.vueadmin/Cdn.vueadmin/Tables.vueadmin/ApiKeys.vue(has the oneNychMessage→NychAlertconversion and the non-dismissible dialog pattern — do it last once that pattern is proven elsewhere isn't needed, i.e. it's the only file needing it)
For each file: apply the mapping above, then run the app (bun run dev),
navigate to that view in the browser, and check the golden path (list
renders, create/edit dialog opens and submits, select options populate,
delete/revoke works) before moving to the next file. This is a visual/UI
change, so it needs to be checked in a real browser, not just type-checked.
Verification
vue-tsc --noEmit(part ofbun run build) after each file, to catch prop-shape mistakes the compiler can see.- Manual browser check of each migrated view per the sequencing above.
- Final full pass:
bun run buildsucceeds, and a click-through of every admin view's create/edit/delete flow.
Open questions / risks
- The non-dismissible-dialog pattern for
ApiKeys.vue's key-reveal dialog (ignoringupdate:openon close attempts) isn't a built-in library feature — it's an app-level workaround. If it feels fragile once implemented, worth flagging back to the library rather than solving twice.