diff --git a/TODO.md b/TODO.md index 9ecee5a..58a462d 100644 --- a/TODO.md +++ b/TODO.md @@ -24,21 +24,6 @@ Current RTL implementation is partial and has limitations: **Test case**: Arabic text with hashtags on same line should display properly with right-alignment. -### NIP-05 Resolution with @ Prefix -**Priority**: High -**File**: `src/lib/nip05.ts` - -**Issue**: Commands like `req -a @fiatjaf.com` (without username, just @domain) return unexpected results. - -**Current behavior**: -- `req -a fiatjaf.com` works (normalized to `_@fiatjaf.com`) ✅ -- `req -a user@fiatjaf.com` works ✅ -- `req -a @fiatjaf.com` fails - not recognized as valid NIP-05 ❌ - -**Root cause**: The `isNip05()` regex patterns don't match the `@domain.com` format (@ prefix without username). - -**Solution**: Either normalize `@domain.com` → `_@domain.com` or show helpful error message. - ### Live Mode Reliability **Priority**: High **File**: `src/components/ReqViewer.tsx` @@ -66,9 +51,6 @@ When selecting an action from the dropdown, pressing Enter should insert the com ### Command Options Display When an action is entered, show the list of available options below and provide auto-completion for flags/arguments. -### Date Display -Show timestamps/dates for notes in feed views for better chronological context. - ## Feature Requests ### Command History diff --git a/src/components/CommandLauncher.tsx b/src/components/CommandLauncher.tsx index 2068fc1..cd55220 100644 --- a/src/components/CommandLauncher.tsx +++ b/src/components/CommandLauncher.tsx @@ -2,6 +2,12 @@ import { useEffect, useState } from "react"; import { Command } from "cmdk"; import { useGrimoire } from "@/core/state"; import { manPages } from "@/types/man"; +import { + Dialog, + DialogContent, + DialogTitle, +} from "@/components/ui/dialog"; +import { VisuallyHidden } from "@/components/ui/visually-hidden"; import "./command-launcher.css"; interface CommandLauncherProps { @@ -90,89 +96,96 @@ export default function CommandLauncher({ : "Type a command..."; return ( - -
- + + + + Command Launcher + + +
+ - {recognizedCommand && args.length > 0 && ( -
- Parsed: - {commandName} - {args.join(" ")} + {recognizedCommand && args.length > 0 && ( +
+ Parsed: + {commandName} + {args.join(" ")} +
+ )} + + + + {commandName + ? `No command found: ${commandName}` + : "Start typing..."} + + + {categories.map((category) => ( + + {filteredCommands + .filter(([_, cmd]) => cmd.category === category) + .map(([name, cmd]) => { + const isExactMatch = name === commandName; + return ( + handleSelect(name)} + className="command-item" + data-exact-match={isExactMatch} + > +
+
+ {name} + {cmd.synopsis !== name && ( + + {cmd.synopsis.replace(name, "").trim()} + + )} + {isExactMatch && ( + + ✓ + + )} +
+
+ {cmd.description.split(".")[0]} +
+
+
+ ); + })} +
+ ))} +
+ +
+
+ ↑↓ navigate + execute + esc close +
+ {recognizedCommand && ( +
Ready to execute
+ )} +
- )} - - - - {commandName - ? `No command found: ${commandName}` - : "Start typing..."} - - - {categories.map((category) => ( - - {filteredCommands - .filter(([_, cmd]) => cmd.category === category) - .map(([name, cmd]) => { - const isExactMatch = name === commandName; - return ( - handleSelect(name)} - className="command-item" - data-exact-match={isExactMatch} - > -
-
- {name} - {cmd.synopsis !== name && ( - - {cmd.synopsis.replace(name, "").trim()} - - )} - {isExactMatch && ( - - )} -
-
- {cmd.description.split(".")[0]} -
-
-
- ); - })} -
- ))} -
- -
-
- ↑↓ navigate - execute - esc close -
- {recognizedCommand && ( -
Ready to execute
- )} -
-
- +
+
+
); } diff --git a/src/components/command-launcher.css b/src/components/command-launcher.css index 4b86935..f38904e 100644 --- a/src/components/command-launcher.css +++ b/src/components/command-launcher.css @@ -1,44 +1,17 @@ /* Command Launcher Styles - Terminal Aesthetic */ .grimoire-command-launcher { - position: fixed; - inset: 0; - z-index: 50; - background: rgba(0, 0, 0, 0.8); - display: flex; - align-items: flex-start; - justify-content: center; - padding-top: 20vh; - animation: fadeIn 0.2s ease; + max-width: 640px; + padding: 0; + overflow: hidden; } -@keyframes fadeIn { - from { - opacity: 0; - } - to { - opacity: 1; - } +.grimoire-command-content { + width: 100%; } .command-launcher-wrapper { width: 100%; - max-width: 640px; background: hsl(var(--background)); - border: 1px solid hsl(var(--border)); - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); - overflow: hidden; - animation: slideDown 0.2s ease; -} - -@keyframes slideDown { - from { - transform: translateY(-20px); - opacity: 0; - } - to { - transform: translateY(0); - opacity: 1; - } } .command-input { diff --git a/src/components/nostr/kinds/BaseEventRenderer.tsx b/src/components/nostr/kinds/BaseEventRenderer.tsx index bb7e4a1..8dfc564 100644 --- a/src/components/nostr/kinds/BaseEventRenderer.tsx +++ b/src/components/nostr/kinds/BaseEventRenderer.tsx @@ -15,6 +15,7 @@ import { useGrimoire } from "@/core/state"; import { useCopy } from "@/hooks/useCopy"; import { JsonViewer } from "@/components/JsonViewer"; import { formatTimestamp } from "@/hooks/useLocale"; +import { nip19 } from "nostr-tools"; // NIP-01 Kind ranges const REPLACEABLE_START = 10000; @@ -76,7 +77,29 @@ export function EventMenu({ event }: { event: NostrEvent }) { }; const copyEventId = () => { - copy(event.id); + // For replaceable/parameterized replaceable events, encode as naddr + const isAddressable = + (event.kind >= REPLACEABLE_START && event.kind < REPLACEABLE_END) || + (event.kind >= PARAMETERIZED_REPLACEABLE_START && + event.kind < PARAMETERIZED_REPLACEABLE_END); + + if (isAddressable) { + // Find d-tag for identifier + const dTag = event.tags.find((t) => t[0] === "d")?.[1] || ""; + const naddr = nip19.naddrEncode({ + kind: event.kind, + pubkey: event.pubkey, + identifier: dTag, + }); + copy(naddr); + } else { + // For regular events, encode as nevent + const nevent = nip19.neventEncode({ + id: event.id, + author: event.pubkey, + }); + copy(nevent); + } }; const viewEventJson = () => { diff --git a/src/components/ui/visually-hidden.tsx b/src/components/ui/visually-hidden.tsx new file mode 100644 index 0000000..7ffbc7d --- /dev/null +++ b/src/components/ui/visually-hidden.tsx @@ -0,0 +1,19 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +/** + * VisuallyHidden component for accessibility + * Hides content visually but keeps it available for screen readers + */ +export const VisuallyHidden = React.forwardRef< + HTMLSpanElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)); + +VisuallyHidden.displayName = "VisuallyHidden"; diff --git a/vite.config.ts b/vite.config.ts index 28da367..8eb2460 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -13,4 +13,9 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, + server: { + hmr: { + overlay: true, + }, + }, });