Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions src/components/ui/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,21 +175,23 @@ export function Dropdown({
top: 'calc(100% + 8px)',
left: 0,
right: 0,
zIndex: 100,
zIndex: 500,
background: 'var(--surface-palette)',
backdropFilter: 'blur(40px) saturate(200%)',
border: '1px solid var(--surface-palette-border)',
borderRadius: 16,
padding: 6,
boxShadow: '0 12px 40px -8px rgba(40, 0, 100, 0.3), 0 4px 12px -4px rgba(80, 40, 140, 0.1)',
maxHeight: 260,
overflowY: 'auto'
overflowY: 'auto',
overflowX: 'hidden',
}}
>
{options.map((opt) => (
<button
key={opt.value}
type="button"
title={opt.label}
onClick={() => {
onChange(opt.value);
setOpen(false);
Expand All @@ -207,7 +209,9 @@ export function Dropdown({
transition: 'background 0.1s',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
justifyContent: 'space-between',
gap: 8,
overflow: 'hidden',
}}
onMouseEnter={(e) => {
if (opt.value !== value) e.currentTarget.style.background = 'rgba(139, 92, 246, 0.05)';
Expand All @@ -216,8 +220,10 @@ export function Dropdown({
if (opt.value !== value) e.currentTarget.style.background = 'transparent';
}}
>
{opt.label}
{opt.value === value && <span style={{ color: 'var(--accent)', fontSize: 12 }}>βœ“</span>}
<span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', flex: 1 }}>
{opt.label}
</span>
{opt.value === value && <span style={{ color: 'var(--accent)', fontSize: 12, flexShrink: 0 }}>βœ“</span>}
</button>
))}
</motion.div>
Expand Down
4 changes: 3 additions & 1 deletion src/components/ui/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@ export function ToastProvider({ children }: { children: ReactNode }) {
style={{
position: 'fixed',
top: 80,
right: 24,
right: 16,
left: 'auto',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-end',
gap: 10,
pointerEvents: 'none',
zIndex: 10000,
maxWidth: 'calc(100vw - 32px)',
}}
>
<AnimatePresence>
Expand Down
1 change: 1 addition & 0 deletions src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ body,
overflow-x: hidden;
}


body {
font-family: var(--font-sans);
color: var(--ink);
Expand Down
16 changes: 7 additions & 9 deletions src/tools/jwt-decode/JwtDecodeTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,29 @@ import { Output } from '../../components/ui/Output';
export default function JwtDecodeTool() {
const [input, setInput] = useState('');

const decoded = useMemo(() => {
if (!input.trim()) return null;
const { decoded, isExpired } = useMemo(() => {
if (!input.trim()) return { decoded: null, isExpired: false };
try {
const parts = input.split('.');
if (parts.length !== 3) throw new Error('Invalid JWT format (must have 3 parts separated by dots)');

const decodeB64 = (str: string) => {
const b64 = str.replace(/-/g, '+').replace(/_/g, '/');
// Pad with = to make it a multiple of 4
const padded = b64.padEnd(b64.length + (4 - (b64.length % 4)) % 4, '=');
return decodeURIComponent(atob(padded).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));
};

const header = JSON.parse(decodeB64(parts[0]));
const payload = JSON.parse(decodeB64(parts[1]));
return { header, payload, error: null };
const expired = payload && typeof payload.exp === 'number'
? (payload.exp * 1000 < Date.now())
: false;
return { decoded: { header, payload, error: null }, isExpired: expired };
} catch (e) {
return { error: (e as Error).message };
return { decoded: { error: (e as Error).message }, isExpired: false };
}
}, [input]);

const isExpired = decoded?.payload && typeof decoded.payload.exp === 'number'
? (decoded.payload.exp * 1000 < Date.now())
: false;

return (
<ToolLayout>
<div style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
Expand Down
104 changes: 50 additions & 54 deletions src/tools/regex/RegexTool.tsx
Original file line number Diff line number Diff line change
@@ -1,76 +1,72 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { useState, useMemo } from 'react';
import { useState, useMemo } from 'react'; // useState kept for pattern/flags/testString
import type { ReactNode } from 'react';
import { Field, Toggle, TextInput, TextArea } from '../../components/ui/Field';
import { ToolLayout } from '../../components/ToolLayout';

export default function RegexTool() {
const [pattern, setPattern] = useState('(?<year>\\d{4})-(?<month>\\d{2})-(?<day>\\d{2})');
const [flags, setFlags] = useState('g');
const [testString, setTestString] = useState('Today is 2024-03-12, tomorrow is 2024-03-13.');
const [error, setError] = useState<string | null>(null);

const toggleFlag = (flag: string) => {
setFlags(prev => prev.includes(flag) ? prev.replace(flag, '') : prev + flag);
};

const { matches, highlighted } = useMemo(() => {
setError(null);
if (!pattern) return { matches: [], highlighted: testString };
const { matches, highlighted, error: regexError } = useMemo(() => {
if (!pattern) return { matches: [], highlighted: testString as ReactNode, error: null };

let regex: RegExp;
try {
const regex = new RegExp(pattern, flags);
const m = [];
let match;

// If it's global, we can iterate
if (flags.includes('g')) {
let max = 1000;
while ((match = regex.exec(testString)) !== null && max-- > 0) {
m.push(match);
if (match[0].length === 0) {
regex.lastIndex++; // prevent infinite loop on empty matches
}
}
} else {
match = regex.exec(testString);
if (match) m.push(match);
}
regex = new RegExp(pattern, flags);
} catch (e: any) {
return { matches: [], highlighted: testString as ReactNode, error: e.message as string };
}

// Highlight logic
const hl = [];
let lastIdx = 0;
m.forEach((match, i) => {
const start = match.index;
const end = start + match[0].length;
if (start > lastIdx) {
hl.push(<span key={`text-${lastIdx}`}>{testString.substring(lastIdx, start)}</span>);
}
hl.push(
<mark
key={`match-${i}`}
style={{
background: 'rgba(167, 139, 250, 0.4)',
color: 'var(--ink)',
borderRadius: 4,
padding: '0 2px'
}}
>
{match[0]}
</mark>
);
lastIdx = end;
});
if (lastIdx < testString.length) {
hl.push(<span key={`text-${lastIdx}`}>{testString.substring(lastIdx)}</span>);
const m: RegExpExecArray[] = [];
if (flags.includes('g')) {
let max = 1000;
let match: RegExpExecArray | null;
while ((match = regex.exec(testString)) !== null && max-- > 0) {
m.push(match);
if (match[0].length === 0) regex.lastIndex++;
}
} else {
const match = regex.exec(testString);
if (match) m.push(match);
}

return { matches: m, highlighted: hl.length > 0 ? hl : testString };
} catch (e: any) {
setError(e.message);
return { matches: [], highlighted: testString };
// Build highlight spans outside try/catch (no JSX in catch blocks)
const hl: ReactNode[] = [];
let lastIdx = 0;
m.forEach((match, i) => {
const start = match.index;
const end = start + match[0].length;
if (start > lastIdx) {
hl.push(<span key={`text-${lastIdx}`}>{testString.substring(lastIdx, start)}</span>);
}
hl.push(
<mark
key={`match-${i}`}
style={{
background: 'rgba(167, 139, 250, 0.4)',
color: 'var(--ink)',
borderRadius: 4,
padding: '0 2px'
}}
>
{match[0]}
</mark>
);
lastIdx = end;
});
if (lastIdx < testString.length) {
hl.push(<span key={`text-${lastIdx}`}>{testString.substring(lastIdx)}</span>);
}

return { matches: m, highlighted: hl.length > 0 ? hl : testString as ReactNode, error: null };
}, [pattern, flags, testString]);


return (
<ToolLayout>
<div style={{ display: 'flex', flexDirection: 'column', gap: 24 }}>
Expand All @@ -84,7 +80,7 @@ export default function RegexTool() {
style={{ fontFamily: 'var(--font-mono)' }}
/>
</Field>
{error && <div style={{ color: 'var(--status-error)', fontSize: 13, marginTop: 4 }}>{error}</div>}
{regexError && <div style={{ color: 'var(--status-error)', fontSize: 13, marginTop: 4 }}>{regexError}</div>}
</div>
<Field label="Flags">
<TextInput
Expand Down
Loading