mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-08 22:47:02 +02:00
feat: copy bech32
This commit is contained in:
18
TODO.md
18
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
|
||||
|
||||
@@ -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.Dialog
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
label="Command Launcher"
|
||||
className="grimoire-command-launcher"
|
||||
shouldFilter={false}
|
||||
>
|
||||
<div className="command-launcher-wrapper">
|
||||
<Command.Input
|
||||
value={input}
|
||||
onValueChange={setInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
className="command-input"
|
||||
/>
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="grimoire-command-launcher p-0">
|
||||
<VisuallyHidden>
|
||||
<DialogTitle>Command Launcher</DialogTitle>
|
||||
</VisuallyHidden>
|
||||
<Command
|
||||
label="Command Launcher"
|
||||
className="grimoire-command-content"
|
||||
shouldFilter={false}
|
||||
>
|
||||
<div className="command-launcher-wrapper">
|
||||
<Command.Input
|
||||
value={input}
|
||||
onValueChange={setInput}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
className="command-input"
|
||||
/>
|
||||
|
||||
{recognizedCommand && args.length > 0 && (
|
||||
<div className="command-hint">
|
||||
<span className="command-hint-label">Parsed:</span>
|
||||
<span className="command-hint-command">{commandName}</span>
|
||||
<span className="command-hint-args">{args.join(" ")}</span>
|
||||
{recognizedCommand && args.length > 0 && (
|
||||
<div className="command-hint">
|
||||
<span className="command-hint-label">Parsed:</span>
|
||||
<span className="command-hint-command">{commandName}</span>
|
||||
<span className="command-hint-args">{args.join(" ")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Command.List className="command-list">
|
||||
<Command.Empty className="command-empty">
|
||||
{commandName
|
||||
? `No command found: ${commandName}`
|
||||
: "Start typing..."}
|
||||
</Command.Empty>
|
||||
|
||||
{categories.map((category) => (
|
||||
<Command.Group
|
||||
key={category}
|
||||
heading={category}
|
||||
className="command-group"
|
||||
>
|
||||
{filteredCommands
|
||||
.filter(([_, cmd]) => cmd.category === category)
|
||||
.map(([name, cmd]) => {
|
||||
const isExactMatch = name === commandName;
|
||||
return (
|
||||
<Command.Item
|
||||
key={name}
|
||||
value={name}
|
||||
onSelect={() => handleSelect(name)}
|
||||
className="command-item"
|
||||
data-exact-match={isExactMatch}
|
||||
>
|
||||
<div className="command-item-content">
|
||||
<div className="command-item-name">
|
||||
<span className="command-name">{name}</span>
|
||||
{cmd.synopsis !== name && (
|
||||
<span className="command-args">
|
||||
{cmd.synopsis.replace(name, "").trim()}
|
||||
</span>
|
||||
)}
|
||||
{isExactMatch && (
|
||||
<span className="command-match-indicator">
|
||||
✓
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="command-item-description">
|
||||
{cmd.description.split(".")[0]}
|
||||
</div>
|
||||
</div>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
))}
|
||||
</Command.List>
|
||||
|
||||
<div className="command-footer">
|
||||
<div>
|
||||
<kbd>↑↓</kbd> navigate
|
||||
<kbd>↵</kbd> execute
|
||||
<kbd>esc</kbd> close
|
||||
</div>
|
||||
{recognizedCommand && (
|
||||
<div className="command-footer-status">Ready to execute</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Command.List className="command-list">
|
||||
<Command.Empty className="command-empty">
|
||||
{commandName
|
||||
? `No command found: ${commandName}`
|
||||
: "Start typing..."}
|
||||
</Command.Empty>
|
||||
|
||||
{categories.map((category) => (
|
||||
<Command.Group
|
||||
key={category}
|
||||
heading={category}
|
||||
className="command-group"
|
||||
>
|
||||
{filteredCommands
|
||||
.filter(([_, cmd]) => cmd.category === category)
|
||||
.map(([name, cmd]) => {
|
||||
const isExactMatch = name === commandName;
|
||||
return (
|
||||
<Command.Item
|
||||
key={name}
|
||||
value={name}
|
||||
onSelect={() => handleSelect(name)}
|
||||
className="command-item"
|
||||
data-exact-match={isExactMatch}
|
||||
>
|
||||
<div className="command-item-content">
|
||||
<div className="command-item-name">
|
||||
<span className="command-name">{name}</span>
|
||||
{cmd.synopsis !== name && (
|
||||
<span className="command-args">
|
||||
{cmd.synopsis.replace(name, "").trim()}
|
||||
</span>
|
||||
)}
|
||||
{isExactMatch && (
|
||||
<span className="command-match-indicator">✓</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="command-item-description">
|
||||
{cmd.description.split(".")[0]}
|
||||
</div>
|
||||
</div>
|
||||
</Command.Item>
|
||||
);
|
||||
})}
|
||||
</Command.Group>
|
||||
))}
|
||||
</Command.List>
|
||||
|
||||
<div className="command-footer">
|
||||
<div>
|
||||
<kbd>↑↓</kbd> navigate
|
||||
<kbd>↵</kbd> execute
|
||||
<kbd>esc</kbd> close
|
||||
</div>
|
||||
{recognizedCommand && (
|
||||
<div className="command-footer-status">Ready to execute</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Command.Dialog>
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
19
src/components/ui/visually-hidden.tsx
Normal file
19
src/components/ui/visually-hidden.tsx
Normal file
@@ -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<HTMLSpanElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<span
|
||||
ref={ref}
|
||||
className={cn("sr-only", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
VisuallyHidden.displayName = "VisuallyHidden";
|
||||
@@ -13,4 +13,9 @@ export default defineConfig({
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
hmr: {
|
||||
overlay: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user