# Nychthemeron shadcn-vue Migration Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Port Mercury UI from the old PrimeVue-based `@nychthemeron/library@0.0.1` to the new shadcn-vue-based `@nychthemeron/library@0.0.1` (same version, contents replaced on the registry — already installed and lockfile-consistent as of this plan). **Architecture:** Two phases. Phase 1 fixes build/plugin wiring so the app compiles and runs against the new library (no view changes yet — the app will render unstyled/broken between Phase 1 and the first view task, which is expected). Phase 2 migrates each view file natively to the new component API, one file at a time, each independently testable via type-check + manual browser verification. **Tech Stack:** Vue 3.5, Vite 5, TypeScript, `@nychthemeron/library` (shadcn-vue + Tailwind v4 + CVA under the hood, consumed only through its `Nych*` global components — this app never imports shadcn-vue/Tailwind/CVA directly). > **Amendment (during Task 1 execution):** the library moved again, from > `0.0.1` to `0.0.3`, while this plan was being executed (maintainer-owned > registry, confirmed intentional). Two things changed that affect Task 1 > only — Phase 2's component-level API (Button/Dialog/Select/Alert props) > is unaffected: > - The `"./theme"` export subpath was dropped. `main.ts` must import > `@nychthemeron/library/style` instead of `.../theme` — same > `tailwind.css` content, different subpath. > - The library's `"."` export declares a `"development"` condition > pointing at `./src/index.ts`, which isn't in the published files > (`dist/`, `src/assets/`, `src/components/` only). Vite's dev server > picks that condition first and fails to resolve the bare > `@nychthemeron/library` specifier. `vite.config.ts` needs a > `resolve.alias` entry anchored to the exact bare specifier (regex > `/^@nychthemeron\/library$/`, not a plain string — plain-string > aliases prefix-match and break the `/style`/`/components` subpath > imports) pointing straight at `./node_modules/@nychthemeron/library/dist/index.js`. > > Both fixes already landed in commit `86a715e` on `main`. Tasks 2+ can > assume `bun run dev` and `bun run build` both work cleanly at the > config/plugin level — nothing in this amendment affects Phase 2 view > migrations. ## Global Constraints - Every `Nych*` tag is a **globally registered component** via `app.use(createNychthemeron())` in `src/main.ts` — view files never `import` them individually. Don't add per-file imports. - Button `variant` values: `primary` (default), `secondary`, `info`, `success`, `warning`, `danger`. `size` values: `default`, `sm`, `lg`, `icon`. - Alert `variant` values: `info` (default), `success`, `warning`, `danger`, `secondary`. - There is no `fluid` prop anywhere in the new library — full-width is always `class="w-full"`. - There is no `label` prop on `NychButton` — button text is the default slot. - `NychDialog` is `v-model:open` (boolean), not `v-model:visible`. Header text goes in `...`, not a `header` prop. Width goes on `NychDialogContent`'s `class`, not a `style` attribute on `NychDialog`. - `NychSelect` takes a plain string `v-model` and `NychSelectItem` children — there is no `:options`/`optionLabel`/`optionValue` array API, and **values must be strings** (no boolean/number option values). - No test framework exists in this repo (`package.json` has no `test` script). Verification is `bun run build` (runs `vue-tsc --noEmit && vite build`) for type-correctness, plus manual browser verification for visual/behavioral correctness — do not add a test framework as part of this plan (out of scope, not requested). - Run all commands from `/home/mcpeakml/code/rust/Mercury/ui`. --- ### Task 1: Foundation — build and plugin wiring **Files:** - Modify: `src/main.ts` - Modify: `vite.config.ts` - Modify: `package.json` - Modify: `src/assets/main.css` **Interfaces:** - Produces: a working `bun run dev` server with the new `@nychthemeron/library` theme CSS loaded and no PrimeVue references left anywhere in the app. Every later task depends on this. - [ ] **Step 1: Remove PrimeVue from `src/main.ts`** Current content: ```ts import { createApp } from "vue"; import '@nychthemeron/library/theme' import './assets/main.css' import './stores/theme' import { createPinia } from "pinia"; import PrimeVue from "@primevue/core/config"; import { createNychthemeron } from "@nychthemeron/library"; import App from "./App.vue"; import router from "./router"; const app = createApp(App); app.use(createPinia()); app.use(router); app.use(PrimeVue, { unstyled: true }); app.use(createNychthemeron()); app.mount("#app"); ``` Replace with: ```ts import { createApp } from "vue"; import '@nychthemeron/library/theme' import './assets/main.css' import './stores/theme' import { createPinia } from "pinia"; import { createNychthemeron } from "@nychthemeron/library"; import App from "./App.vue"; import router from "./router"; const app = createApp(App); app.use(createPinia()); app.use(router); app.use(createNychthemeron()); app.mount("#app"); ``` - [ ] **Step 2: Add the Tailwind v4 Vite plugin and drop the PrimeVue chunk in `vite.config.ts`** Current content: ```ts import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; export default defineConfig({ plugins: [vue()], server: { proxy: { "/api": "http://localhost:3000", "/auth": "http://localhost:3000", "/admin": "http://localhost:3000", }, }, build: { outDir: "dist", rollupOptions: { output: { manualChunks: { vue: ["vue", "vue-router", "pinia"], primevue: ["@primevue/core"], nychthemeron: ["@nychthemeron/library"], }, entryFileNames: "js/[name]-[hash].js", chunkFileNames: "js/[name]-[hash].js", assetFileNames: "assets/[name]-[hash][extname]", }, }, }, }); ``` Replace with: ```ts import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import tailwindcss from "@tailwindcss/vite"; export default defineConfig({ plugins: [vue(), tailwindcss()], server: { proxy: { "/api": "http://localhost:3000", "/auth": "http://localhost:3000", "/admin": "http://localhost:3000", }, }, build: { outDir: "dist", rollupOptions: { output: { manualChunks: { vue: ["vue", "vue-router", "pinia"], nychthemeron: ["@nychthemeron/library"], }, entryFileNames: "js/[name]-[hash].js", chunkFileNames: "js/[name]-[hash].js", assetFileNames: "assets/[name]-[hash][extname]", }, }, }, }); ``` - [ ] **Step 3: Add direct devDependencies and bump the `vue` peer range in `package.json`** Current content: ```json { "name": "mercury-ui", "private": true, "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview" }, "dependencies": { "@nychthemeron/library": "^0.0.1", "pinia": "^2.1.0", "vue": "^3.4.0", "vue-router": "^4.3.0" }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.0", "typescript": "^5.3.0", "vite": "^5.0.0", "vue-tsc": "^2.0.0" } } ``` Replace with: ```json { "name": "mercury-ui", "private": true, "version": "0.1.0", "type": "module", "scripts": { "dev": "vite", "build": "vue-tsc --noEmit && vite build", "preview": "vite preview" }, "dependencies": { "@nychthemeron/library": "^0.0.1", "pinia": "^2.1.0", "vue": "^3.5.0", "vue-router": "^4.3.0" }, "devDependencies": { "@tailwindcss/vite": "^4.3.2", "@vitejs/plugin-vue": "^5.0.0", "tailwindcss": "^4.3.2", "typescript": "^5.3.0", "vite": "^5.0.0", "vue-tsc": "^2.0.0" } } ``` - [ ] **Step 4: Run `bun install` to sync the lockfile with the `package.json` edits** Run: `bun install` Expected: exits 0, no version conflicts reported. (`@tailwindcss/vite`, `tailwindcss`, and `vue@3.5.x` are already present in `bun.lock` as transitive dependencies of `@nychthemeron/library`, so this should only re-link them as direct deps, not download anything new.) - [ ] **Step 5: Remove the dead unstyled-PrimeVue overrides from `src/assets/main.css`** Find: ```css .nych-dialog { min-width: 500px; } [class^="nych-button"] { box-shadow: unset !important; justify-content: center !important; } [class^="nych-button"]:hover { box-shadow: 0 0 5px var(--text-high); } .nych-loading-icon .wreath { transform-box: fill-box; transform-origin: center; } ``` Replace with: ```css .nych-loading-icon .wreath { transform-box: fill-box; transform-origin: center; } ``` (`.nych-dialog` and `[class^="nych-button"]` matched classnames the old unstyled-PrimeVue passthrough emitted. The new `NychDialog`/`NychButton` don't emit any classname starting with `nych-dialog`/`nych-button` — they use Tailwind utility classes and `data-slot` attributes instead — so these two rules can no longer match anything. `.nych-loading-icon .wreath` stays: `NychLoadingIcon`'s markup is unchanged in the new library.) - [ ] **Step 6: Verify the app boots** Run: `bun run dev` (leave running in the background, or run and check output then stop it) Expected: Vite starts with no errors. Open `http://localhost:5173` (or whatever port Vite prints) in a browser — the login page should load without console errors. It will look broken/unstyled in places until Phase 2 migrates `Login.vue` and the other views (raw `Nych*` global components render, but with old PrimeVue-style props like `label`/`fluid`/`header` that the new components silently ignore, so buttons show no text and dialogs show no visible chrome) — that's expected at this point, not a regression to fix here. - [ ] **Step 7: Commit** ```bash git add src/main.ts vite.config.ts package.json bun.lock src/assets/main.css git commit -m "Wire Mercury UI build to the shadcn-vue nychthemeron library" ``` --- ### Task 2: Migrate `src/views/Login.vue` **Files:** - Modify: `src/views/Login.vue` **Interfaces:** - Consumes: `NychInput` (rename of `NychInputText`), `NychButton` — both globally registered per Task 1. - [ ] **Step 1: Update the template** Find: ```html

{{ error }}

``` Replace with: ```html

{{ error }}

Sign In ``` - [ ] **Step 2: Update the leftover ` ``` Replace with: ```css ``` (Note: the old CSS also had `.nych-select[data-p~='focus']` / `.nych-select[data-p~='disabled']` / `.nych-select-option[data-p-focused='true']` rules — omitted here because this component's template never sets those `data-p` attributes, so they were always dead code for this usage.) - [ ] **Step 2: Type-check** Run: `bun run build` Expected: no TypeScript errors referencing `MethodSelect.vue` (this task is CSS-only, no template/script changes, so this should already pass). - [ ] **Step 3: Manual browser check** This component is only used from `Blacklist.vue`, which hasn't been migrated yet at this point in the plan — skip the browser check here and verify it as part of Task 7 (`Blacklist.vue`) instead, where it's actually reachable in the running app. - [ ] **Step 4: Commit** ```bash git add src/views/admin/MethodSelect.vue git commit -m "Restyle MethodSelect.vue for the new theme CSS" ``` --- ### Task 4: Migrate `src/views/admin/Cache.vue` **Files:** - Modify: `src/views/admin/Cache.vue` - [ ] **Step 1: Update the template** Find: ```html ``` Replace with: ```html Refresh Flush Cache ``` - [ ] **Step 2: Type-check** Run: `bun run build` Expected: no errors referencing `Cache.vue`. - [ ] **Step 3: Manual browser check** Navigate to the Query Cache admin page. Verify: "Refresh" and "Flush Cache" buttons show their text, Refresh reloads stats, Flush Cache prompts a confirm dialog and clears stats on confirm. - [ ] **Step 4: Commit** ```bash git add src/views/admin/Cache.vue git commit -m "Migrate Cache.vue to the shadcn-vue nychthemeron components" ``` --- ### Task 5: Migrate `src/views/admin/Cors.vue` **Files:** - Modify: `src/views/admin/Cors.vue` - [ ] **Step 1: Update the template** Find: ```html ``` Replace with: ```html + Add Origin ``` Find: ```html ``` Replace with: ```html Delete ``` Find: ```html

Use * to allow all origins (permissive mode).

``` Replace with: ```html Add CORS Origin

Use * to allow all origins (permissive mode).

Add Origin
``` - [ ] **Step 2: Type-check** Run: `bun run build` Expected: no errors referencing `Cors.vue`. - [ ] **Step 3: Manual browser check** Navigate to CORS Origins. Verify: "+ Add Origin" opens a dialog titled "Add CORS Origin" with a visible border/backdrop, the input accepts text, "Add Origin" is disabled until the field is non-empty, submitting adds a row and closes the dialog, and the per-row "Delete" button removes a row after confirm. - [ ] **Step 4: Commit** ```bash git add src/views/admin/Cors.vue git commit -m "Migrate Cors.vue to the shadcn-vue nychthemeron components" ``` --- ### Task 6: Migrate `src/views/admin/Queries.vue` **Files:** - Modify: `src/views/admin/Queries.vue` - [ ] **Step 1: Update the header button and table action buttons** Find: ```html ``` Replace with: ```html + New Query ``` Find: ```html ``` Replace with: ```html Edit Delete ``` - [ ] **Step 2: Update the create dialog** Find: ```html

Use :param_name for named bind parameters.

``` Replace with: ```html New Query

Use :param_name for named bind parameters.

Save Query
``` - [ ] **Step 3: Update the edit dialog** Find: ```html

Use :param_name for named bind parameters.

``` Replace with: ```html Edit — {{ editIdentifier }}

Use :param_name for named bind parameters.

Update Query
``` - [ ] **Step 4: Type-check** Run: `bun run build` Expected: no errors referencing `Queries.vue`. - [ ] **Step 5: Manual browser check** Navigate to Query Registry. Verify: "+ New Query" opens a dialog, the SQL textarea is 12 rows tall and full width, saving adds a row; per-row "Edit" opens a dialog titled "Edit — <identifier>" pre-filled with the SQL and description, saving updates the row; "Delete" removes a row after confirm. - [ ] **Step 6: Commit** ```bash git add src/views/admin/Queries.vue git commit -m "Migrate Queries.vue to the shadcn-vue nychthemeron components" ``` --- ### Task 7: Migrate `src/views/admin/Blacklist.vue` This file has the one boolean-valued `NychSelect` in the app (`Status: Active/Disabled`) — the new `NychSelect` only supports string `v-model`/`value`, so `editForm.active` needs to become a string internally, converted back to boolean at the API-call boundary. **Files:** - Modify: `src/views/admin/Blacklist.vue` - [ ] **Step 1: Update the header button and table action buttons** Find: ```html ``` Replace with: ```html + Add Pattern ``` Find: ```html ``` Replace with: ```html Edit Delete ``` - [ ] **Step 2: Update the edit dialog** Find: ```html
``` Replace with: ```html Edit Blacklist Entry
Active Disabled
Save Changes
``` - [ ] **Step 3: Update the create dialog** Find: ```html

Use * for one segment, ** for any depth.

Permission bit that allows callers to bypass this rule. Leave blank to block everyone.

``` Replace with: ```html New Blacklist Entry

Use * for one segment, ** for any depth.

Permission bit that allows callers to bypass this rule. Leave blank to block everyone.

Add to Blacklist
``` - [ ] **Step 4: Convert `editForm.active` from boolean to string in the script** Find: ```ts const editForm = ref({ pattern: '', methods: [] as string[], reason: '', bypass_mask: '', active: true }) ``` Replace with: ```ts const editForm = ref({ pattern: '', methods: [] as string[], reason: '', bypass_mask: '', active: 'true' }) ``` Find: ```ts function openEdit(e: any) { editId.value = e.id editForm.value = { pattern: e.pattern, methods: stringToMethods(e.method), reason: e.reason ?? '', bypass_mask: e.bypass_mask ?? '', active: e.active, } showEdit.value = true } async function submitEdit() { await fetch(`/api/blacklist/${editId.value}`, { method: 'PUT', headers: { ...auth.authHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify({ pattern: editForm.value.pattern, method: methodsToString(editForm.value.methods), reason: editForm.value.reason || null, bypass_mask: editForm.value.bypass_mask || null, active: editForm.value.active, }), }) showEdit.value = false load() } ``` Replace with: ```ts function openEdit(e: any) { editId.value = e.id editForm.value = { pattern: e.pattern, methods: stringToMethods(e.method), reason: e.reason ?? '', bypass_mask: e.bypass_mask ?? '', active: String(e.active), } showEdit.value = true } async function submitEdit() { await fetch(`/api/blacklist/${editId.value}`, { method: 'PUT', headers: { ...auth.authHeaders(), 'Content-Type': 'application/json' }, body: JSON.stringify({ pattern: editForm.value.pattern, method: methodsToString(editForm.value.methods), reason: editForm.value.reason || null, bypass_mask: editForm.value.bypass_mask || null, active: editForm.value.active === 'true', }), }) showEdit.value = false load() } ``` - [ ] **Step 5: Type-check** Run: `bun run build` Expected: no errors referencing `Blacklist.vue`. - [ ] **Step 6: Manual browser check** Navigate to Route Blacklist. Verify: "+ Add Pattern" and per-row "Edit"/"Delete" all show text and work; the `MethodSelect` multi-select dropdown (checked in Task 3) now visually matches the rest of the form inside these dialogs; the edit dialog's "Status" select shows "Active"/"Disabled", changing it and saving persists the new value (check the row's status badge flips), and a fresh entry defaults to "Active". - [ ] **Step 7: Commit** ```bash git add src/views/admin/Blacklist.vue git commit -m "Migrate Blacklist.vue to the shadcn-vue nychthemeron components" ``` --- ### Task 8: Migrate `src/views/admin/Permissions.vue` **Files:** - Modify: `src/views/admin/Permissions.vue` - [ ] **Step 1: Update the header button and table action buttons** Find: ```html ``` Replace with: ```html + New Permission ``` Find: ```html ``` Replace with: ```html Edit Delete ``` - [ ] **Step 2: Update the edit dialog** Find: ```html
``` Replace with: ```html Edit — {{ editForm.name }}
Save Changes
``` - [ ] **Step 3: Update the create dialog** Find: ```html

Must be a power of 2 not already in use (1, 2, 4, 8, 16, 32, 64 …)

``` Replace with: ```html New Permission

Must be a power of 2 not already in use (1, 2, 4, 8, 16, 32, 64 …)

Create Permission
``` - [ ] **Step 4: Type-check** Run: `bun run build` Expected: no errors referencing `Permissions.vue`. - [ ] **Step 5: Manual browser check** Navigate to Permissions. Verify: create/edit dialogs open with visible titles, inputs are full-width, save/create both work and refresh the table, delete works after confirm. - [ ] **Step 6: Commit** ```bash git add src/views/admin/Permissions.vue git commit -m "Migrate Permissions.vue to the shadcn-vue nychthemeron components" ``` --- ### Task 9: Migrate `src/views/admin/Users.vue` **Files:** - Modify: `src/views/admin/Users.vue` - [ ] **Step 1: Update the header button and table action buttons** Find: ```html ``` Replace with: ```html + New User ``` Find: ```html ``` Replace with: ```html Edit Delete ``` - [ ] **Step 2: Update the edit dialog** Find: ```html

{{ ROLES.find(r => r.value === editForm.permissions_mask)?.description ?? '' }}

``` Replace with: ```html Edit — {{ editForm.username }}
{{ r.label }}

{{ ROLES.find(r => r.value === editForm.permissions_mask)?.description ?? '' }}

Save Changes
``` - [ ] **Step 3: Update the create dialog** Find: ```html

{{ roleDescription }}

``` Replace with: ```html New User
{{ r.label }}

{{ roleDescription }}

Create User
``` - [ ] **Step 4: Type-check** Run: `bun run build` Expected: no errors referencing `Users.vue`. - [ ] **Step 5: Manual browser check** Navigate to Users. Verify: create dialog's Role select shows a placeholder until a role is picked, and the hint text below updates as you pick different roles; edit dialog's Role select is pre-populated with the user's current role and its hint matches; save/create/delete all work. - [ ] **Step 6: Commit** ```bash git add src/views/admin/Users.vue git commit -m "Migrate Users.vue to the shadcn-vue nychthemeron components" ``` --- ### Task 10: Migrate `src/views/admin/Cdn.vue` **Files:** - Modify: `src/views/admin/Cdn.vue` - [ ] **Step 1: Update the header button and table action buttons** Find: ```html ``` Replace with: ```html + Add Object ``` Find: ```html ``` Replace with: ```html Edit Delete ``` - [ ] **Step 2: Update the create dialog** Find: ```html

The filename becomes the key unless overridden below.

``` Replace with: ```html Upload CDN Object

The filename becomes the key unless overridden below.

Upload
``` - [ ] **Step 3: Update the edit dialog** Find: ```html
``` Replace with: ```html Edit — {{ editKey }}
Save Changes
``` - [ ] **Step 4: Type-check** Run: `bun run build` Expected: no errors referencing `Cdn.vue`. - [ ] **Step 5: Manual browser check** Navigate to CDN Objects. Verify: "+ Add Object" opens the upload dialog, "Upload" stays disabled until a file is picked, picking a file and submitting adds a row; per-row "Edit" opens pre-filled, saving updates the row; "Delete" removes a row after confirm. - [ ] **Step 6: Commit** ```bash git add src/views/admin/Cdn.vue git commit -m "Migrate Cdn.vue to the shadcn-vue nychthemeron components" ``` --- ### Task 11: Migrate `src/views/admin/Tables.vue` The largest file — three dialogs (create, read-only inspect, drop-with-confirmation) plus a per-row column-type `NychSelect` inside a `v-for`. **Files:** - Modify: `src/views/admin/Tables.vue` - [ ] **Step 1: Update the header button and table action buttons** Find: ```html ``` Replace with: ```html + New Table ``` Find: ```html ``` Replace with: ```html Inspect Drop ``` - [ ] **Step 2: Update the create dialog (header, inputs, and the per-column type select)** Find: ```html

Lowercase letters, numbers, and underscores only.

An id SERIAL PRIMARY KEY column is always added automatically.
Name Type Nullable
Add at least one column.
``` Replace with: ```html New Table

Lowercase letters, numbers, and underscores only.

+ Add Column
An id SERIAL PRIMARY KEY column is always added automatically.
Name Type Nullable
{{ ct }}
Add at least one column.
Create Table
``` - [ ] **Step 3: Update the inspect dialog** Find: ```html
Loading table data…
``` Replace with: ```html Inspect — {{ inspectPreview?.table_name ?? '' }}
Loading table data…
``` Find (the closing tag for this dialog — it's the one immediately before the `` comment): ```html
Empty table Use POST /api/{{ inspectPreview.table_name }} to insert rows.
``` Replace with: ```html
Empty table Use POST /api/{{ inspectPreview.table_name }} to insert rows.
``` - [ ] **Step 4: Update the drop-preview dialog** Find: ```html
Loading table data…
``` Replace with: ```html Drop Table
Loading table data…
``` Find: ```html
``` Replace with: ```html
Cancel Drop Table
``` - [ ] **Step 5: Type-check** Run: `bun run build` Expected: no errors referencing `Tables.vue`. - [ ] **Step 6: Manual browser check** Navigate to Tables. Verify: "+ New Table" opens the create dialog; adding a column shows a type `NychSelect` per row (each opens independently, doesn't affect other rows), "Create Table" is disabled until name + at least one column are set, submitting creates the table and immediately opens its Inspect dialog; per-row "Inspect" shows schema + sample data read-only; "Drop" shows the warning banner when rows exist, and the "Drop Table" button stays disabled until the typed confirmation exactly matches the table name. - [ ] **Step 7: Commit** ```bash git add src/views/admin/Tables.vue git commit -m "Migrate Tables.vue to the shadcn-vue nychthemeron components" ``` --- ### Task 12: Migrate `src/views/admin/ApiKeys.vue` Last file — includes the one `NychMessage` → `NychAlert` conversion, and the non-dismissible "key reveal" dialog that has no direct equivalent in the new library. > **Amendment (post-implementation):** Step 3's original `onRevealOpenChange` > guard (below) shipped in commit `b742f1f` but was Critical-flagged by task > review as a no-op: the library's Dialog drives visibility off an internal > ref that its dismiss handler sets directly, decoupled from the parent's > `open` prop via `useVModel`'s passive mode — the guard never actually > changed that prop, so the internal ref never resynced back to `true` and > the dialog would silently stay closed after any outside-click/Escape. > > The shipped fix (commit `0f77a5f`, independently verified against the > library's source and compiled bundle) instead uses standard > `v-model:open="showReveal"` plus a capture-phase `document` `pointerdown` > listener that calls `event.preventDefault()` for clicks outside > `[data-slot="dialog-content"]` — this preempts a `defaultPrevented` check > the library's own dismiss handler already makes for outside-clicks, so it > blocks that path with no flicker. Escape has no equivalent hook and still > closes the dialog early; the maintainer explicitly chose to accept that > gap rather than add a flicker-based workaround for it too. > > **This depends on the library's internal `data-slot="dialog-content"` > attribute** — re-check this dialog specifically if `@nychthemeron/library` > is ever bumped again. See `src/views/admin/ApiKeys.vue`'s > `blockOutsideDismiss` for the current implementation. **Files:** - Modify: `src/views/admin/ApiKeys.vue` - [ ] **Step 1: Update the header button and table action button** Find: ```html ``` Replace with: ```html + New Key ``` Find: ```html ``` Replace with: ```html Revoke ``` - [ ] **Step 2: Update the create dialog** Find: ```html

{{ ROLES.find(r => r.value === form.permissions_mask)?.description ?? '' }}

``` Replace with: ```html New API Key
{{ r.label }}

{{ ROLES.find(r => r.value === form.permissions_mask)?.description ?? '' }}

Generate Key
``` - [ ] **Step 3: Update the key-reveal dialog — `NychMessage` → `NychAlert`, and make it non-dismissible** The old `:closable="false"` prevented the user from closing this dialog any way other than clicking "I've saved the key". The new `NychDialogContent` accepts `:show-close-button="false"` to drop the X button, but its overlay/Escape dismiss isn't independently disableable — so we also ignore `update:open` events that would close it, via a guarded handler. Find: ```html
Copy this key now — it will not be shown again.
{{ newKey }}
``` Replace with: ```html API Key Created
Copy this key now — it will not be shown again.
{{ newKey }}
I've saved the key
``` - [ ] **Step 4: Add the `onRevealOpenChange` guard to the script** Find: ```ts function closeReveal() { showReveal.value = false newKey.value = '' } ``` Replace with: ```ts function closeReveal() { showReveal.value = false newKey.value = '' } // The reveal dialog must only close via the "I've saved the key" button // (closeReveal), never via outside-click/Escape — ignore any attempt to // set it back to false that didn't go through that path. function onRevealOpenChange(open: boolean) { if (open) showReveal.value = true } ``` - [ ] **Step 5: Type-check** Run: `bun run build` Expected: no errors referencing `ApiKeys.vue`. - [ ] **Step 6: Manual browser check** Navigate to API Keys. Verify: "+ New Key" dialog's Role select works and its hint updates; submitting opens the "API Key Created" dialog with a visible warning alert (amber/warning-colored, not the default info-blue), no X close button in the corner, and clicking outside the dialog or pressing Escape does **not** close it; clicking the copy icon copies the key and shows a checkmark for 2 seconds; clicking "I've saved the key" closes it and the new key appears in the table; per-row "Revoke" works after confirm. - [ ] **Step 7: Commit** ```bash git add src/views/admin/ApiKeys.vue git commit -m "Migrate ApiKeys.vue to the shadcn-vue nychthemeron components" ``` --- ### Task 13: Final full-app verification **Files:** none (verification only) - [ ] **Step 1: Full type-check and production build** Run: `bun run build` Expected: exits 0 with no TypeScript errors and a `dist/` build produced. - [ ] **Step 2: Full click-through** Run: `bun run dev`, log in, and walk every admin page (Cache, Cors, Queries, Blacklist, Permissions, Users, Cdn, Tables, ApiKeys) — for each, exercise its create, edit (where applicable), and delete/revoke flow once. Confirm the theme toggle (`useTheme().toggle()`, wired in `Layout.vue`) still switches between `apollo`/`hades` correctly across all the new components (dialogs, selects, alerts should all pick up the new theme's colors immediately, since they're driven by the same `data-theme` attribute and CSS custom properties as before). - [ ] **Step 3: Grep for anything left behind** Run: `grep -rn "primevue" src/ vite.config.ts package.json --include="*.vue" --include="*.ts" --include="*.json" -i` Expected: no matches. Run: `grep -rn "NychInputText\|severity=\|v-model:visible\|fluid\b\|label=\"" src/ --include="*.vue"` Expected: no matches (all old-API usages converted). If this turns up hits outside the 11 files this plan touched, treat that as a signal a call site was missed during the original inventory — fix it before closing out. - [ ] **Step 4: Commit (only if Step 3 found and fixed something; otherwise nothing to commit)** ## Post-implementation note: what's unverified This migration shipped with `bun run build` clean across the whole app and every view resolving under the dev server, but no automated tests (none exist in this repo) and no live browser click-through (no chromium-cli/ Playwright/Puppeteer available in the environment this was built in). A human should do a manual pass covering, at minimum: - The theme toggle (`useTheme().toggle()` in `Layout.vue`) across all the new components — dialogs, selects, alerts should all repaint correctly between `apollo`/`hades`. - **The ApiKeys reveal dialog's outside-click-blocked behavior specifically** — the highest-value thing to check by hand, since it's the one piece of runtime DOM-event logic in this migration (everything else is declarative template conversion). Confirm clicking outside the "API Key Created" dialog does nothing, and confirm Escape still closes it (accepted gap). - One create/edit/delete (or revoke) cycle per admin view, per the per-task "Manual browser check" notes throughout this plan.