| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 3c60644 commit 0f55472
19 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,9 +14,10 @@ Tracking model: work this list top to bottom. Leave untouched bullets plain, pre | |||
| 14 | 14 | ||
| 15 | 15 | These are quick, high-leverage patches because later fixes can reuse them instead of adding another local workaround. | |
| 16 | 16 | ||
| 17 | - - Make `pnpm test` honest about TypeScript tests, or split script names so validation expectations are clear. | ||
| 18 | - - Default non-submit buttons/select triggers to `type="button"`, fix the shared pagination label/id contract, and clean up tooltip child prop merging. | ||
| 19 | - - Add tiny helpers for response filenames, external URL normalization, internal route classification, package slug encoding, row-local ids, and guarded storage. | ||
| 17 | + - [done 2026-06-24: `pnpm run test:unit`, `pnpm test`] Make `pnpm test` honest about TypeScript tests, or split script names so validation expectations are clear. | ||
| 18 | + - [done 2026-06-24: `pnpm run test:tsc`, `pnpm run test:lint`] Default non-submit buttons/select triggers to `type="button"`, fix the shared pagination label/id contract, and clean up tooltip child prop merging. | ||
| 19 | + - [done 2026-06-24: `pnpm run test:unit`, `pnpm run test:tsc`, `pnpm run test:lint`] Add tiny helpers for response filenames, package slug encoding, and row-local ids. | ||
| 20 | + - Add tiny helpers for external URL normalization, internal route classification, and guarded storage. | ||
| 20 | 21 | - Fix custom `useMutation` stale callbacks, clipboard/copied-state timers, docs sidebar timers, popup blocked-state handling, and simple page-visibility/reduced-motion helpers. | |
| 21 | 22 | - Normalize easy static data contracts: maintainer bare-domain URLs, Scarf id validation, public-library selector, partner analytics seed buckets, blog image URLs, host cache purge response policy. | |
| 22 | 23 | ||
@@ -88,7 +89,10 @@ Treat these as tracked initiatives, not normal cleanup PRs. | |||
| 88 | 89 | ||
| 89 | 90 | ## Batch Log | |
| 90 | 91 | ||
| 91 | - - 2026-06-23: Audit created and ordered. No implementation batch has landed yet. | ||
| 92 | + - 2026-06-24: Added shared response filename/content-disposition helpers, package route slug encoding helpers, and row-local id helpers; wired them into builder/docs downloads, Intent registry links, and moderation note inputs. | ||
| 93 | + - 2026-06-24: Hardened shared UI primitives: defaulted shop buttons and shared select triggers to non-submit buttons, gave pagination a unique page-size label/id pair plus non-submit controls, and made Tooltip merge trigger handlers/refs without `any`. | ||
| 94 | + - 2026-06-24: Added `test:unit` and wired it into `pnpm test` so existing TypeScript assertion tests run in the default validation path. | ||
| 95 | + - 2026-06-23: Audit created and ordered. | ||
| 92 | 96 | ||
| 93 | 97 | ## Highest Priority Findings | |
| 94 | 98 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -28,9 +28,10 @@ | |||
| 28 | 28 | "docs:webhooks:sync": "tsx scripts/sync-docs-webhooks.ts", | |
| 29 | 29 | "husky": "pnpm run format && pnpm run test", | |
| 30 | 30 | "pretest": "pnpm run content:build", | |
| 31 | - "test": "run-p test:tsc test:lint", | ||
| 31 | + "test": "run-p test:tsc test:lint test:unit", | ||
| 32 | 32 | "test:tsc": "tsc", | |
| 33 | 33 | "test:lint": "pnpm run lint", | |
| 34 | + "test:unit": "tsx --test \"tests/**/*.test.ts\"", | ||
| 34 | 35 | "prepare": "husky" | |
| 35 | 36 | }, | |
| 36 | 37 | "dependencies": { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,6 +14,7 @@ import { PaginationControls } from './PaginationControls' | |||
| 14 | 14 | import { Spinner } from './Spinner' | |
| 15 | 15 | import type { DocFeedback } from '~/db/types' | |
| 16 | 16 | import { calculatePoints } from '~/utils/docFeedback.shared' | |
| 17 | + import { getRowFieldId } from '~/utils/route-encoding' | ||
| 17 | 18 | import { Check, Lightbulb, TriangleAlert } from 'lucide-react' | |
| 18 | 19 | import { MessageSquare, X } from 'lucide-react' | |
| 19 | 20 | import { Badge, Button } from '~/ui' | |
@@ -155,6 +156,11 @@ export function FeedbackModerationList({ | |||
| 155 | 156 | const isExpanded = expandedIds.has(feedback.id) | |
| 156 | 157 | const isPending = feedback.status === 'pending' | |
| 157 | 158 | const isModeratingThis = isModeratingId === feedback.id | |
| 159 | + const moderationNoteId = getRowFieldId( | ||
| 160 | + 'feedback-moderation', | ||
| 161 | + feedback.id, | ||
| 162 | + 'note', | ||
| 163 | + ) | ||
| 158 | 164 | ||
| 159 | 165 | return ( | |
| 160 | 166 | <React.Fragment key={feedback.id}> | |
@@ -333,12 +339,13 @@ export function FeedbackModerationList({ | |||
| 333 | 339 | {isPending && ( | |
| 334 | 340 | <div> | |
| 335 | 341 | <label | |
| 336 | - htmlFor={`moderation-note-${feedback.id}`} | ||
| 342 | + htmlFor={moderationNoteId} | ||
| 337 | 343 | className="block text-sm font-semibold mb-2" | |
| 338 | 344 | > | |
| 339 | 345 | Internal Moderation Note (optional): | |
| 340 | 346 | </label> | |
| 341 | 347 | <textarea | |
| 348 | + id={moderationNoteId} | ||
| 342 | 349 | value={moderationNotes[feedback.id] || ''} | |
| 343 | 350 | onChange={(e) => | |
| 344 | 351 | handleModerationNoteChange( | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,3 +1,4 @@ | |||
| 1 | + import * as React from 'react' | ||
| 1 | 2 | import { ChevronLeft, ChevronRight } from 'lucide-react' | |
| 2 | 3 | ||
| 3 | 4 | interface PaginationControlsProps { | |
@@ -31,6 +32,8 @@ export function PaginationControls({ | |||
| 31 | 32 | itemLabel = 'items', | |
| 32 | 33 | sticky = false, | |
| 33 | 34 | }: PaginationControlsProps) { | |
| 35 | + const pageSizeSelectId = `${React.useId()}-page-size` | ||
| 36 | + | ||
| 34 | 37 | const goToPreviousPage = () => { | |
| 35 | 38 | if (canGoPrevious) { | |
| 36 | 39 | onPageChange(currentPage - 1) | |
@@ -83,7 +86,10 @@ export function PaginationControls({ | |||
| 83 | 86 | } | |
| 84 | 87 | ||
| 85 | 88 | const content = ( | |
| 86 | - <div className="flex items-center justify-between flex-wrap gap-3"> | ||
| 89 | + <nav | ||
| 90 | + aria-label={`${itemLabel} pagination`} | ||
| 91 | + className="flex items-center justify-between flex-wrap gap-3" | ||
| 92 | + > | ||
| 87 | 93 | <div className="text-sm text-gray-600 dark:text-gray-400"> | |
| 88 | 94 | Showing{' '} | |
| 89 | 95 | <span className="font-semibold text-gray-900 dark:text-white"> | |
@@ -109,12 +115,13 @@ export function PaginationControls({ | |||
| 109 | 115 | {showPageSizeSelector && ( | |
| 110 | 116 | <> | |
| 111 | 117 | <label | |
| 112 | - htmlFor="pageSize" | ||
| 118 | + htmlFor={pageSizeSelectId} | ||
| 113 | 119 | className="text-sm text-gray-600 dark:text-gray-400" | |
| 114 | 120 | > | |
| 115 | 121 | Per page: | |
| 116 | 122 | </label> | |
| 117 | 123 | <select | |
| 124 | + id={pageSizeSelectId} | ||
| 118 | 125 | value={pageSize} | |
| 119 | 126 | onChange={(e) => { | |
| 120 | 127 | const next = parseInt(e.target.value, 10) | |
@@ -133,6 +140,7 @@ export function PaginationControls({ | |||
| 133 | 140 | ||
| 134 | 141 | <div className="flex gap-1 items-center"> | |
| 135 | 142 | <button | |
| 143 | + type="button" | ||
| 136 | 144 | onClick={goToPreviousPage} | |
| 137 | 145 | disabled={!canGoPrevious} | |
| 138 | 146 | className="flex items-center px-2 py-1 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50 hover:text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-white disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-gray-800 gap-1 transition-colors" | |
@@ -158,6 +166,7 @@ export function PaginationControls({ | |||
| 158 | 166 | const isActive = pageNum === currentPage | |
| 159 | 167 | return ( | |
| 160 | 168 | <button | |
| 169 | + type="button" | ||
| 161 | 170 | key={pageNum} | |
| 162 | 171 | onClick={() => onPageChange(pageNum as number)} | |
| 163 | 172 | className={ | |
@@ -173,6 +182,7 @@ export function PaginationControls({ | |||
| 173 | 182 | </div> | |
| 174 | 183 | ||
| 175 | 184 | <button | |
| 185 | + type="button" | ||
| 176 | 186 | onClick={goToNextPage} | |
| 177 | 187 | disabled={!canGoNext} | |
| 178 | 188 | className="flex items-center px-2 py-1 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50 hover:text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-white disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-gray-800 gap-1 transition-colors" | |
@@ -182,7 +192,7 @@ export function PaginationControls({ | |||
| 182 | 192 | </button> | |
| 183 | 193 | </div> | |
| 184 | 194 | </div> | |
| 185 | - </div> | ||
| 195 | + </nav> | ||
| 186 | 196 | ) | |
| 187 | 197 | ||
| 188 | 198 | if (sticky) { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -43,7 +43,10 @@ export function Select<T extends SelectOption>({ | |||
| 43 | 43 | <div className={twMerge('w-full', className)}> | |
| 44 | 44 | <Dropdown> | |
| 45 | 45 | <DropdownTrigger> | |
| 46 | - <button className="relative items-center w-full gap-2 flex hover:bg-gray-500/10 cursor-pointer rounded-md py-1.5 px-2 text-left focus:outline-none text-sm"> | ||
| 46 | + <button | ||
| 47 | + type="button" | ||
| 48 | + className="relative items-center w-full gap-2 flex hover:bg-gray-500/10 cursor-pointer rounded-md py-1.5 px-2 text-left focus:outline-none text-sm" | ||
| 49 | + > | ||
| 47 | 50 | {icon ? ( | |
| 48 | 51 | <span className="flex items-center justify-center w-6 h-6 rounded border border-gray-500/20"> | |
| 49 | 52 | {icon} | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -23,6 +23,7 @@ import { | |||
| 23 | 23 | } from 'lucide-react' | |
| 24 | 24 | import { libraries } from '~/libraries' | |
| 25 | 25 | import { Badge, Button } from '~/ui' | |
| 26 | + import { getRowFieldId } from '~/utils/route-encoding' | ||
| 26 | 27 | import { Fragment, useState } from 'react' | |
| 27 | 28 | ||
| 28 | 29 | interface ShowcaseModerationListProps { | |
@@ -205,6 +206,11 @@ export function ShowcaseModerationList({ | |||
| 205 | 206 | const isExpanded = expandedIds.has(showcase.id) | |
| 206 | 207 | const isPending = showcase.status === 'pending' | |
| 207 | 208 | const isModeratingThis = isModeratingId === showcase.id | |
| 209 | + const moderationNoteId = getRowFieldId( | ||
| 210 | + 'showcase-moderation', | ||
| 211 | + showcase.id, | ||
| 212 | + 'note', | ||
| 213 | + ) | ||
| 208 | 214 | ||
| 209 | 215 | return ( | |
| 210 | 216 | <Fragment key={showcase.id}> | |
@@ -528,13 +534,13 @@ export function ShowcaseModerationList({ | |||
| 528 | 534 | {isPending && ( | |
| 529 | 535 | <div> | |
| 530 | 536 | <label | |
| 531 | - htmlFor="note" | ||
| 537 | + htmlFor={moderationNoteId} | ||
| 532 | 538 | className="block text-sm font-semibold mb-2" | |
| 533 | 539 | > | |
| 534 | 540 | Internal Moderation Note (optional): | |
| 535 | 541 | </label> | |
| 536 | 542 | <textarea | |
| 537 | - id="note" | ||
| 543 | + id={moderationNoteId} | ||
| 538 | 544 | value={moderationNotes[showcase.id] || ''} | |
| 539 | 545 | onChange={(e) => | |
| 540 | 546 | handleModerationNoteChange( | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,18 +1,20 @@ | |||
| 1 | 1 | import * as React from 'react' | |
| 2 | 2 | import { | |
| 3 | 3 | useFloating, | |
| 4 | - useHover, | ||
| 5 | - useInteractions, | ||
| 6 | 4 | FloatingPortal, | |
| 7 | 5 | offset, | |
| 8 | 6 | shift, | |
| 9 | 7 | flip, | |
| 10 | 8 | autoUpdate, | |
| 9 | + useMergeRefs, | ||
| 11 | 10 | } from '@floating-ui/react' | |
| 12 | 11 | ||
| 12 | + type TooltipTriggerProps = React.HTMLProps<HTMLElement> & | ||
| 13 | + React.RefAttributes<HTMLElement> | ||
| 14 | + | ||
| 13 | 15 | interface TooltipProps { | |
| 14 | 16 | content: React.ReactNode | |
| 15 | - children: React.ReactElement | ||
| 17 | + children: React.ReactElement<TooltipTriggerProps> | ||
| 16 | 18 | placement?: 'top' | 'right' | 'bottom' | 'left' | |
| 17 | 19 | className?: string | |
| 18 | 20 | } | |
@@ -25,32 +27,51 @@ export function Tooltip({ | |||
| 25 | 27 | }: TooltipProps) { | |
| 26 | 28 | const [isOpen, setIsOpen] = React.useState(false) | |
| 27 | 29 | ||
| 28 | - const { refs, floatingStyles, context } = useFloating({ | ||
| 30 | + const { refs, floatingStyles } = useFloating<HTMLElement>({ | ||
| 29 | 31 | open: isOpen, | |
| 30 | 32 | onOpenChange: setIsOpen, | |
| 31 | 33 | placement, | |
| 32 | 34 | middleware: [offset(5), flip(), shift()], | |
| 33 | 35 | whileElementsMounted: autoUpdate, | |
| 34 | 36 | }) | |
| 35 | 37 | ||
| 36 | - const hover = useHover(context) | ||
| 38 | + const triggerRef = useMergeRefs([refs.setReference, children.props.ref]) | ||
| 39 | + const triggerProps = children.props | ||
| 40 | + | ||
| 41 | + const handleMouseEnter: React.MouseEventHandler<HTMLElement> = (event) => { | ||
| 42 | + triggerProps.onMouseEnter?.(event) | ||
| 43 | + setIsOpen(true) | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + const handleMouseLeave: React.MouseEventHandler<HTMLElement> = (event) => { | ||
| 47 | + triggerProps.onMouseLeave?.(event) | ||
| 48 | + setIsOpen(false) | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + const handleFocus: React.FocusEventHandler<HTMLElement> = (event) => { | ||
| 52 | + triggerProps.onFocus?.(event) | ||
| 53 | + setIsOpen(true) | ||
| 54 | + } | ||
| 37 | 55 | ||
| 38 | - const { getReferenceProps, getFloatingProps } = useInteractions([hover]) | ||
| 56 | + const handleBlur: React.FocusEventHandler<HTMLElement> = (event) => { | ||
| 57 | + triggerProps.onBlur?.(event) | ||
| 58 | + setIsOpen(false) | ||
| 59 | + } | ||
| 39 | 60 | ||
| 40 | 61 | return ( | |
| 41 | 62 | <> | |
| 42 | - {/* eslint-disable-next-line react-hooks/refs */} | ||
| 43 | 63 | {React.cloneElement(children, { | |
| 44 | - // eslint-disable-next-line react-hooks/refs | ||
| 45 | - ref: refs.setReference, | ||
| 46 | - ...getReferenceProps(), | ||
| 47 | - } as any)} | ||
| 64 | + ref: triggerRef, | ||
| 65 | + onMouseEnter: handleMouseEnter, | ||
| 66 | + onMouseLeave: handleMouseLeave, | ||
| 67 | + onFocus: handleFocus, | ||
| 68 | + onBlur: handleBlur, | ||
| 69 | + })} | ||
| 48 | 70 | <FloatingPortal> | |
| 49 | 71 | {isOpen && ( | |
| 50 | 72 | <div | |
| 51 | 73 | ref={refs.setFloating /* eslint-disable-line react-hooks/refs */} | |
| 52 | 74 | style={floatingStyles} | |
| 53 | - {...getFloatingProps()} | ||
| 54 | 75 | className={`z-50 rounded-md bg-gray-900 px-3 py-1.5 text-sm text-white shadow-lg dark:bg-gray-800 ${className}`} | |
| 55 | 76 | > | |
| 56 | 77 | {content} | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -31,6 +31,7 @@ import { | |||
| 31 | 31 | intentStatsQueryOptions, | |
| 32 | 32 | } from '~/queries/intent' | |
| 33 | 33 | import type { SkillHistoryEntry } from '~/utils/intent.functions' | |
| 34 | + import { encodePackageNameSlug } from '~/utils/route-encoding' | ||
| 34 | 35 | ||
| 35 | 36 | import { LandingCopyPromptButton } from '~/components/landing/LandingCopyPromptButton' | |
| 36 | 37 | const library = getLibrary('intent') | |
@@ -412,7 +413,7 @@ function IntentRegistryPreview() { | |||
| 412 | 413 | <Link | |
| 413 | 414 | key={pkg.name} | |
| 414 | 415 | to="/intent/registry/$packageName" | |
| 415 | - params={{ packageName: pkg.name.replace('/', '__') }} | ||
| 416 | + params={{ packageName: encodePackageNameSlug(pkg.name) }} | ||
| 416 | 417 | className="group flex flex-col gap-2 rounded-lg border border-zinc-200 bg-zinc-50 p-4 transition-colors hover:border-sky-300 dark:border-zinc-800 dark:bg-zinc-900/50 dark:hover:border-sky-700" | |
| 417 | 418 | > | |
| 418 | 419 | <div className="mb-1 flex items-start justify-between gap-2"> | |
@@ -430,7 +431,7 @@ function IntentRegistryPreview() { | |||
| 430 | 431 | navigate({ | |
| 431 | 432 | to: '/intent/registry/$packageName', | |
| 432 | 433 | params: { | |
| 433 | - packageName: pkg.name.replace('/', '__'), | ||
| 434 | + packageName: encodePackageNameSlug(pkg.name), | ||
| 434 | 435 | }, | |
| 435 | 436 | search: { version: entry.version }, | |
| 436 | 437 | }) | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -23,12 +23,20 @@ const variants: Record<Variant, string> = { | |||
| 23 | 23 | /** Shop button. Three visual weights — primary, outline, ghost. */ | |
| 24 | 24 | export const ShopButton = React.forwardRef<HTMLButtonElement, Props>( | |
| 25 | 25 | function ShopButton( | |
| 26 | - { variant = 'outline', fullWidth, className, children, ...rest }, | ||
| 26 | + { | ||
| 27 | + variant = 'outline', | ||
| 28 | + fullWidth, | ||
| 29 | + type = 'button', | ||
| 30 | + className, | ||
| 31 | + children, | ||
| 32 | + ...rest | ||
| 33 | + }, | ||
| 27 | 34 | ref, | |
| 28 | 35 | ) { | |
| 29 | 36 | return ( | |
| 30 | 37 | <button | |
| 31 | 38 | ref={ref} | |
| 39 | + type={type} | ||
| 32 | 40 | {...rest} | |
| 33 | 41 | className={twMerge( | |
| 34 | 42 | base, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,7 @@ import { findLibrary, getBranch } from '~/libraries' | |||
| 2 | 2 | import { loadDocs } from '~/utils/docs' | |
| 3 | 3 | import { notFound, createFileRoute } from '@tanstack/react-router' | |
| 4 | 4 | import { getDocsCacheHeaders } from '~/utils/docs-cache-headers' | |
| 5 | + import { getContentDispositionHeader } from '~/utils/http-response' | ||
| 5 | 6 | import { filterFrameworkContent } from '~/utils/markdown/filterFrameworkContent' | |
| 6 | 7 | import { getPackageManager } from '~/utils/markdown/installCommand' | |
| 7 | 8 | ||
@@ -51,13 +52,17 @@ export const Route = createFileRoute( | |||
| 51 | 52 | }) | |
| 52 | 53 | ||
| 53 | 54 | const markdownContent = `# ${doc.title}\n${filteredContent}` | |
| 54 | - const filename = (docsPath || 'file').split('/').join('-') | ||
| 55 | + const filename = `${docsPath || 'file'}.md` | ||
| 55 | 56 | ||
| 56 | 57 | return new Response(markdownContent, { | |
| 57 | 58 | headers: { | |
| 58 | 59 | ...cacheHeaders, | |
| 59 | 60 | 'Content-Type': 'text/markdown', | |
| 60 | - 'Content-Disposition': `inline; filename="${filename}.md"`, | ||
| 61 | + 'Content-Disposition': getContentDispositionHeader( | ||
| 62 | + 'inline', | ||
| 63 | + filename, | ||
| 64 | + 'file.md', | ||
| 65 | + ), | ||
| 61 | 66 | }, | |
| 62 | 67 | }) | |
| 63 | 68 | }, | |
| Back | FazBrowse Home | New Git URL |
0 commit comments