diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6dca8d1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +# Enforce LF line endings for all text files +* text=auto eol=lf + +# Explicitly LF for source files +*.ts text eol=lf +*.tsx text eol=lf +*.js text eol=lf +*.jsx text eol=lf +*.json text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.css text eol=lf +*.mjs text eol=lf diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..180f6a1 --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,51 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + tags: + - 'v*' + +permissions: + contents: read + packages: write + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/kubeorch/ui + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix= + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 diff --git a/app/dashboard/notifications/page.tsx b/app/dashboard/notifications/page.tsx index 54aa0ba..a4d0d94 100644 --- a/app/dashboard/notifications/page.tsx +++ b/app/dashboard/notifications/page.tsx @@ -91,6 +91,8 @@ export default function NotificationsPage() { {notifications.map(notification => (
{ + if (e.key === "Enter" || e.key === " ") { + if (!notification.read) markAsRead(notification.id); + if (notification.link) router.push(notification.link); + } + }} >
{notificationIcon(notification.type)} diff --git a/app/dashboard/resources/[id]/page.tsx b/app/dashboard/resources/[id]/page.tsx index 7106b4d..5618e85 100644 --- a/app/dashboard/resources/[id]/page.tsx +++ b/app/dashboard/resources/[id]/page.tsx @@ -700,10 +700,16 @@ export default function ResourceDetailPage() { {pods.map(pod => (
router.push(`/dashboard/resources/${pod.id}`) } + onKeyDown={e => { + if (e.key === "Enter" || e.key === " ") + router.push(`/dashboard/resources/${pod.id}`); + }} >
diff --git a/app/dashboard/settings/page.tsx b/app/dashboard/settings/page.tsx index 13cc5bd..9378bdf 100644 --- a/app/dashboard/settings/page.tsx +++ b/app/dashboard/settings/page.tsx @@ -114,7 +114,7 @@ export default function SettingsPage() { {isAdmin ? ( <>
- + Invite Link
{getInviteLink(inviteCode || "XXXXXX")} @@ -124,10 +124,10 @@ export default function SettingsPage() {
- +

Automatically generate a new invite code after each user signs up diff --git a/app/dashboard/workflow/new/page.tsx b/app/dashboard/workflow/new/page.tsx index 61035e0..84ea3e4 100644 --- a/app/dashboard/workflow/new/page.tsx +++ b/app/dashboard/workflow/new/page.tsx @@ -760,6 +760,8 @@ export default function NewWorkflowPage() {

{/* File Upload - Full width */}
document.getElementById("file-input")?.click() } + onKeyDown={e => { + if (e.key === "Enter" || e.key === " ") + document.getElementById("file-input")?.click(); + }} > > = { - github: Github, +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const iconMap: Record> = { + github: GithubIcon, lock: Lock, shield: Shield, globe: Globe, diff --git a/components/layout/TopBar.tsx b/components/layout/TopBar.tsx index 9c638a8..6bcda02 100644 --- a/components/layout/TopBar.tsx +++ b/components/layout/TopBar.tsx @@ -73,8 +73,13 @@ export function TopBar({ onOpenCommandPalette }: TopBarProps) {
{ + if (e.key === "Enter" || e.key === " ") onOpenCommandPalette?.(); + }} > {/* Resize Handle */}
) { ); } -function CardTitle({ className, ...props }: React.ComponentProps<"h3">) { +function CardTitle({ + className, + children, + ...props +}: React.ComponentProps<"h3">) { return ( + // eslint-disable-next-line jsx-a11y/heading-has-content -- children are always passed by consumers

) { className )} {...props} - /> + > + {children} +

); } diff --git a/components/ui/compact-card.tsx b/components/ui/compact-card.tsx index 8e6cbbd..8c1a13b 100644 --- a/components/ui/compact-card.tsx +++ b/components/ui/compact-card.tsx @@ -31,13 +31,20 @@ function CompactCardHeader({ ); } -function CompactCardTitle({ className, ...props }: React.ComponentProps<"h3">) { +function CompactCardTitle({ + className, + children, + ...props +}: React.ComponentProps<"h3">) { return ( + // eslint-disable-next-line jsx-a11y/heading-has-content -- children are always passed by consumers

+ > + {children} +

); } diff --git a/components/ui/github-icon.tsx b/components/ui/github-icon.tsx new file mode 100644 index 0000000..c27a03a --- /dev/null +++ b/components/ui/github-icon.tsx @@ -0,0 +1,22 @@ +import type { SVGProps } from "react"; + +export function GithubIcon({ + className, + size = 24, + ...props +}: SVGProps & { size?: number }) { + return ( + + ); +} diff --git a/components/workflow/CommandPalette.tsx b/components/workflow/CommandPalette.tsx index 31fab3a..d91a9b7 100644 --- a/components/workflow/CommandPalette.tsx +++ b/components/workflow/CommandPalette.tsx @@ -501,6 +501,7 @@ export default function CommandPalette({ value={searchQuery} onChange={e => setSearchQuery(e.target.value)} className="pl-9 pr-9" + // eslint-disable-next-line jsx-a11y/no-autofocus -- intentional: command palette search should be focused immediately on open autoFocus /> {searchQuery && ( diff --git a/components/workflow/ImportDialog.tsx b/components/workflow/ImportDialog.tsx index 4fa9433..75bac6f 100644 --- a/components/workflow/ImportDialog.tsx +++ b/components/workflow/ImportDialog.tsx @@ -293,6 +293,8 @@ export function ImportDialog({ isOpen, onClose, onImport }: ImportDialogProps) { {/* File Upload */}
document.getElementById("import-file-input")?.click() } + onKeyDown={e => { + if (e.key === "Enter" || e.key === " ") + document.getElementById("import-file-input")?.click(); + }} > - + Git URL @@ -318,6 +318,8 @@ export function ImportDialog({ {/* Drag and drop zone */}
document.getElementById("file-input")?.click()} + onKeyDown={e => { + if (e.key === "Enter" || e.key === " ") + document.getElementById("file-input")?.click(); + }} > ) : ( <> - + Analyze Repository )} diff --git a/lib/utils/__tests__/errorReporter.test.ts b/lib/utils/__tests__/errorReporter.test.ts index a775ff4..d500382 100644 --- a/lib/utils/__tests__/errorReporter.test.ts +++ b/lib/utils/__tests__/errorReporter.test.ts @@ -16,22 +16,14 @@ describe("errorReporter", () => { const error = new Error("test error"); errorReporter.report(error); - expect(console.error).toHaveBeenCalledWith( - "[ErrorReporter]", - error, - "" - ); + expect(console.error).toHaveBeenCalledWith("[ErrorReporter]", error, ""); }); it("should include source in log label", () => { const error = new Error("test error"); errorReporter.report(error, { source: "TestComponent" }); - expect(console.error).toHaveBeenCalledWith( - "[TestComponent]", - error, - "" - ); + expect(console.error).toHaveBeenCalledWith("[TestComponent]", error, ""); }); it("should include metadata in log", () => { @@ -97,10 +89,7 @@ describe("errorReporter", () => { errorReporter.initGlobalHandlers(); - expect(addEventSpy).toHaveBeenCalledWith( - "error", - expect.any(Function) - ); + expect(addEventSpy).toHaveBeenCalledWith("error", expect.any(Function)); expect(addEventSpy).toHaveBeenCalledWith( "unhandledrejection", expect.any(Function) diff --git a/lib/utils/errorReporter.ts b/lib/utils/errorReporter.ts index 6058d49..b868be2 100644 --- a/lib/utils/errorReporter.ts +++ b/lib/utils/errorReporter.ts @@ -41,7 +41,7 @@ function reportMessage(message: string, context: ErrorContext = {}): void { function initGlobalHandlers(): void { if (typeof window === "undefined") return; - window.addEventListener("error", (event) => { + window.addEventListener("error", event => { report(event.error ?? event.message, { source: "window.onerror", metadata: { @@ -52,7 +52,7 @@ function initGlobalHandlers(): void { }); }); - window.addEventListener("unhandledrejection", (event) => { + window.addEventListener("unhandledrejection", event => { report(event.reason, { source: "unhandledrejection", severity: "error",