diff --git a/app/invite/[token]/page.tsx b/app/invite/[token]/page.tsx index 94c30f3..e93a3c5 100644 --- a/app/invite/[token]/page.tsx +++ b/app/invite/[token]/page.tsx @@ -77,7 +77,7 @@ export default async function InvitationPage({

En az 8 karakter kullan.

- + Portal hesabını oluştur diff --git a/app/login/page.tsx b/app/login/page.tsx index 4ae2019..2457dc4 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -75,7 +75,7 @@ export default async function LoginPage({ - + Giriş yap diff --git a/app/register/page.tsx b/app/register/page.tsx index 54150fa..7847540 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -79,7 +79,7 @@ export default async function RegisterPage({ - + Admin hesabını oluştur diff --git a/components/auth/submit-button.tsx b/components/auth/submit-button.tsx index 50df7d7..8b69e4a 100644 --- a/components/auth/submit-button.tsx +++ b/components/auth/submit-button.tsx @@ -4,20 +4,31 @@ import { useFormStatus } from "react-dom"; import { Button } from "poyraz-ui/atoms"; import React from "react"; -interface SubmitButtonProps extends React.ComponentProps { +interface SubmitButtonProps + extends Omit, "effect" | "variant"> { pendingText?: string; + variant?: "default" | "secondary"; } export function SubmitButton({ children, pendingText, type = "submit", + variant = "default", ...props }: SubmitButtonProps) { const { pending } = useFormStatus(); return ( - + - + diff --git a/components/system/destructive-confirmation.tsx b/components/system/destructive-confirmation.tsx index e0cf8d3..6d664dd 100644 --- a/components/system/destructive-confirmation.tsx +++ b/components/system/destructive-confirmation.tsx @@ -41,13 +41,14 @@ export function DestructiveConfirmation({ - ); diff --git a/components/ui/pending-submit-button.tsx b/components/ui/pending-submit-button.tsx index 72866c2..58043c5 100644 --- a/components/ui/pending-submit-button.tsx +++ b/components/ui/pending-submit-button.tsx @@ -7,10 +7,14 @@ import { useFormStatus } from "react-dom"; import { cn } from "@/lib/utils"; -type PendingSubmitButtonProps = ComponentProps & { +type PendingSubmitButtonProps = Omit< + ComponentProps, + "effect" | "variant" +> & { idleIcon?: ReactNode; pendingIcon?: ReactNode; pendingChildren?: ReactNode; + variant?: "default" | "secondary"; }; export function PendingSubmitButton({ @@ -21,6 +25,7 @@ export function PendingSubmitButton({ pendingChildren, pendingIcon, type = "submit", + variant = "default", ...props }: PendingSubmitButtonProps) { const { pending } = useFormStatus(); @@ -36,6 +41,8 @@ export function PendingSubmitButton({ aria-busy={pending} loading={pending && !pendingIcon} className={cn(className)} + variant={variant} + effect="shine" > {icon} {pending ? pendingChildren ?? children : children} diff --git a/scripts/phase4-ui-boundary.mjs b/scripts/phase4-ui-boundary.mjs index c026939..01e5a9b 100644 --- a/scripts/phase4-ui-boundary.mjs +++ b/scripts/phase4-ui-boundary.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import fs from "node:fs"; import path from "node:path"; +import ts from "typescript"; const repoRoot = process.cwd(); const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, "package.json"), "utf8")); @@ -164,6 +165,8 @@ for (const sliderBehavior of [ "snap-mandatory", "overflow-x-auto", "scrollBy", + "[scrollbar-width:none]", + "[&::-webkit-scrollbar]:hidden", 'event.key === "ArrowLeft"', 'event.key === "ArrowRight"', ]) { @@ -173,6 +176,45 @@ for (const sliderBehavior of [ ); } +const pageHeaderFiles = [ + "app/(dashboard)/dashboard-client.tsx", + "app/(dashboard)/analytics/analytics-client.tsx", + "app/(dashboard)/calendar/calendar-client.tsx", + "app/(dashboard)/clients/clients-client.tsx", + "app/(dashboard)/clients/[id]/client-detail-client.tsx", + "app/(dashboard)/finance/finance-client.tsx", + "app/(dashboard)/journal/journal-client.tsx", + "app/(dashboard)/projects/projects-client.tsx", + "app/(dashboard)/projects/[id]/project-detail-client.tsx", + "app/(dashboard)/tasks/tasks-client.tsx", + "app/(dashboard)/settings/page.tsx", + "app/(dashboard)/business/invoices/invoices-client.tsx", + "app/(dashboard)/business/proposals/proposals-client.tsx", + "app/(dashboard)/business/subscriptions/subscriptions-client.tsx", + "app/(dashboard)/chat/page.tsx", + "app/portal/page.tsx", + "app/portal/projects/page.tsx", + "app/portal/projects/[id]/portal-project-client.tsx", + "app/portal/tasks/page.tsx", + "app/portal/revisions/page.tsx", +]; +for (const pageHeaderFile of pageHeaderFiles) { + const content = fs.readFileSync(path.join(repoRoot, pageHeaderFile), "utf8"); + assert.doesNotMatch( + content, + /<\/h1>\s*(?:\{[^\n]*&&\s*)?]*text-muted-foreground/, + `${pageHeaderFile} must not render a description directly below its page title`, + ); +} +assert.doesNotMatch( + fs.readFileSync( + path.join(repoRoot, "app/(dashboard)/projects/[id]/project-detail-client.tsx"), + "utf8", + ), + /project\.description\s*\|\|/, + "Project detail header must not render the project description below its title", +); + const allowedLocalUiFiles = new Set([ "pending-link.tsx", "pending-submit-button.tsx", @@ -221,6 +263,80 @@ function walk(targetPath) { }); } +const buttonViolations = ["app", "components"] + .flatMap(walk) + .filter((filePath) => filePath.endsWith(".tsx")) + .flatMap((filePath) => { + const content = fs.readFileSync(filePath, "utf8"); + const relativePath = path.relative(repoRoot, filePath); + const sourceFile = ts.createSourceFile( + filePath, + content, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TSX, + ); + const violations = []; + let hasPoyrazButtonImport = false; + const buttonNodes = []; + + function visit(node) { + if ( + ts.isImportDeclaration(node) && + node.moduleSpecifier.text === "poyraz-ui/atoms" && + node.importClause?.namedBindings && + ts.isNamedImports(node.importClause.namedBindings) + ) { + hasPoyrazButtonImport ||= node.importClause.namedBindings.elements.some( + (element) => element.name.text === "Button", + ); + } + + if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) { + const tagName = node.tagName.getText(sourceFile); + if (tagName === "button") { + const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1; + violations.push(`${relativePath}:${line}: native button must use Poyraz Button`); + } + if (tagName === "Button") buttonNodes.push(node); + } + ts.forEachChild(node, visit); + } + visit(sourceFile); + + if (buttonNodes.length > 0 && !hasPoyrazButtonImport) { + violations.push(`${relativePath}: Button must be imported from poyraz-ui/atoms`); + } + + for (const node of buttonNodes) { + const line = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1; + const attributes = node.attributes.properties.filter(ts.isJsxAttribute); + const effect = attributes.find((attribute) => attribute.name.getText(sourceFile) === "effect"); + const variant = attributes.find((attribute) => attribute.name.getText(sourceFile) === "variant"); + const variantText = variant?.getText(sourceFile) ?? ""; + + if (effect?.getText(sourceFile) !== 'effect="shine"') { + violations.push(`${relativePath}:${line}: Button must use effect="shine"`); + } + if (!variant || /outline|ghost|destructive|soft|glass|link/.test(variantText)) { + violations.push( + `${relativePath}:${line}: Button variant must be default or secondary`, + ); + } + if (/bg-indigo|border-indigo|text-indigo/.test(node.getText(sourceFile))) { + violations.push(`${relativePath}:${line}: Button must not override Poyraz variant colors`); + } + } + + return violations; + }); + +assert.deepEqual( + buttonViolations, + [], + `Poyraz Button boundary violations:\n${buttonViolations.join("\n")}`, +); + const violations = ["app", "components"] .flatMap(walk) .flatMap((filePath) => {