@mahiraltinkaya/me-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +99 -0
  3. package/bin/me-ui.js +60 -0
  4. package/package.json +34 -0
  5. package/registry/assets/breadcrumb_arrow.svg +3 -0
  6. package/registry/components/stepper-provider.tsx +88 -0
  7. package/registry/components/stepper-view.tsx +40 -0
  8. package/registry/components/steps/form-input.tsx +68 -0
  9. package/registry/components/steps/form-select.tsx +42 -0
  10. package/registry/components/steps/step-field.tsx +43 -0
  11. package/registry/components/ui/button.tsx +69 -0
  12. package/registry/components/ui/field-hint.tsx +68 -0
  13. package/registry/components/ui/input.tsx +87 -0
  14. package/registry/components/ui/select.tsx +128 -0
  15. package/registry/components/ui/stepper/index.tsx +24 -0
  16. package/registry/components/ui/stepper/step-divider.tsx +20 -0
  17. package/registry/components/ui/stepper/step-indicator.tsx +33 -0
  18. package/registry/components/ui/stepper/step-label.tsx +29 -0
  19. package/registry/components/ui/stepper/stepper-progress.tsx +40 -0
  20. package/registry/components/ui/stepper/stepper-rail.tsx +107 -0
  21. package/registry/components/ui/stepper/types.ts +26 -0
  22. package/registry/components/ui/tooltip.tsx +54 -0
  23. package/registry/lib/normalize.ts +2 -0
  24. package/registry/lib/quote-schema.ts +45 -0
  25. package/registry/lib/tckn.ts +31 -0
  26. package/registry/lib/utils.ts +6 -0
  27. package/registry.json +252 -0
  28. package/src/args.js +47 -0
  29. package/src/commands/add.js +118 -0
  30. package/src/commands/list.js +20 -0
  31. package/src/css.js +80 -0
  32. package/src/deps.js +52 -0
  33. package/src/jsonc.js +44 -0
  34. package/src/log.js +32 -0
  35. package/src/manifest.js +58 -0
  36. package/src/paths.js +65 -0
  37. package/src/project.js +100 -0
@@ -0,0 +1,87 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Input as InputPrimitive } from "@base-ui/react/input";
5
+
6
+ import { cn } from "@/lib/utils";
7
+ import { FieldHint, type FieldHintContent } from "@/components/ui/field-hint";
8
+
9
+ export type InputTooltip = FieldHintContent;
10
+
11
+ export interface InputProps extends React.ComponentProps<"input"> {
12
+ icon?: React.ReactNode;
13
+ tooltip?: InputTooltip;
14
+ }
15
+
16
+ function Input({
17
+ className,
18
+ type,
19
+ icon,
20
+ tooltip,
21
+ onFocus,
22
+ onBlur,
23
+ id,
24
+ "aria-describedby": describedBy,
25
+ ...props
26
+ }: InputProps) {
27
+ const [focused, setFocused] = React.useState(false);
28
+ const hintId = id ? `${id}-hint` : undefined;
29
+ const hintOpen = Boolean(tooltip) && focused;
30
+
31
+ // Merged rather than replaced: the field can be described by both its
32
+ // validation message and the hint panel at the same time.
33
+ const describedByAll =
34
+ [describedBy, hintOpen ? hintId : undefined].filter(Boolean).join(" ") || undefined;
35
+
36
+ const handleFocus = React.useCallback(
37
+ (event: React.FocusEvent<HTMLInputElement>) => {
38
+ setFocused(true);
39
+ onFocus?.(event);
40
+ },
41
+ [onFocus],
42
+ );
43
+
44
+ const handleBlur = React.useCallback(
45
+ (event: React.FocusEvent<HTMLInputElement>) => {
46
+ setFocused(false);
47
+ onBlur?.(event);
48
+ },
49
+ [onBlur],
50
+ );
51
+
52
+ const field = (
53
+ <InputPrimitive
54
+ type={type}
55
+ id={id}
56
+ data-slot="input"
57
+ aria-describedby={describedByAll}
58
+ onFocus={handleFocus}
59
+ onBlur={handleBlur}
60
+ className={cn(
61
+ "border-input file:text-foreground placeholder:text-muted-foreground focus-visible:border-brand focus-visible:ring-brand/20 disabled:bg-input/50 aria-invalid:border-brand aria-invalid:ring-brand/20 h-11.25 w-full min-w-0 rounded-lg border bg-transparent px-4 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:border-2 focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-2 aria-invalid:ring-3 md:text-sm",
62
+ icon && "pl-10",
63
+ tooltip && "pr-10",
64
+ className,
65
+ )}
66
+ {...props}
67
+ />
68
+ );
69
+
70
+ if (!icon && !tooltip) return field;
71
+
72
+ return (
73
+ <div className="relative w-full">
74
+ {icon ? (
75
+ <span className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 -translate-y-1/2">
76
+ {icon}
77
+ </span>
78
+ ) : null}
79
+ {field}
80
+ {tooltip ? (
81
+ <FieldHint hint={tooltip} open={hintOpen} id={hintId ?? "field-hint"} className="right-3" />
82
+ ) : null}
83
+ </div>
84
+ );
85
+ }
86
+
87
+ export { Input };
@@ -0,0 +1,128 @@
1
+ "use client";
2
+
3
+ import { useCallback, useState, type ReactNode } from "react";
4
+ import { Select as SelectPrimitive } from "@base-ui/react/select";
5
+ import { Check, ChevronDown } from "lucide-react";
6
+
7
+ import { cn } from "@/lib/utils";
8
+ import { FieldHint, type FieldHintContent } from "@/components/ui/field-hint";
9
+
10
+ export interface SelectProps {
11
+ options: readonly string[];
12
+ value: string | null;
13
+ onValueChange: (value: string) => void;
14
+ onClose?: () => void;
15
+ placeholder?: string;
16
+ icon?: ReactNode;
17
+ tooltip?: FieldHintContent;
18
+ id?: string;
19
+ name?: string;
20
+ invalid?: boolean;
21
+ disabled?: boolean;
22
+ /** Id of an external description, typically the field's validation message. */
23
+ describedBy?: string;
24
+ }
25
+
26
+ function Select({
27
+ options,
28
+ value,
29
+ onValueChange,
30
+ onClose,
31
+ placeholder,
32
+ icon,
33
+ tooltip,
34
+ id,
35
+ name,
36
+ invalid,
37
+ disabled,
38
+ describedBy,
39
+ }: SelectProps) {
40
+ const [focused, setFocused] = useState(false);
41
+ const hintId = id ? `${id}-hint` : "field-hint";
42
+ const hintOpen = Boolean(tooltip) && focused;
43
+ // Merged rather than replaced — see the same note in Input.
44
+ const describedByAll =
45
+ [describedBy, hintOpen ? hintId : undefined].filter(Boolean).join(" ") || undefined;
46
+
47
+ const handleFocus = useCallback(() => setFocused(true), []);
48
+ const handleBlur = useCallback(() => setFocused(false), []);
49
+
50
+ return (
51
+ <SelectPrimitive.Root
52
+ value={value}
53
+ disabled={disabled}
54
+ onValueChange={(next) => onValueChange(String(next ?? ""))}
55
+ onOpenChange={(open) => {
56
+ if (!open) onClose?.();
57
+ }}
58
+ >
59
+ <div className="relative w-full">
60
+ {icon ? (
61
+ <span className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 z-1 -translate-y-1/2">
62
+ {icon}
63
+ </span>
64
+ ) : null}
65
+ <SelectPrimitive.Trigger
66
+ id={id}
67
+ name={name}
68
+ data-slot="select-trigger"
69
+ aria-invalid={invalid}
70
+ aria-describedby={describedByAll}
71
+ onFocus={handleFocus}
72
+ onBlur={handleBlur}
73
+ className={cn(
74
+ "border-input focus-visible:border-brand focus-visible:ring-brand/20 aria-invalid:border-brand aria-invalid:ring-brand/20 h-11.25 w-full rounded-lg border bg-transparent px-4 text-left text-base transition-colors outline-none select-none focus-visible:border-2 focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-2 aria-invalid:ring-3 md:text-sm",
75
+ "flex items-center justify-between gap-2",
76
+ icon && "pl-10",
77
+ tooltip ? "pr-16" : "pr-10",
78
+ )}
79
+ >
80
+ <SelectPrimitive.Value
81
+ className="data-placeholder:text-muted-foreground truncate"
82
+ placeholder={placeholder}
83
+ />
84
+ <SelectPrimitive.Icon
85
+ className={cn(
86
+ "text-muted-foreground absolute top-1/2 -translate-y-1/2 transition-transform duration-200 data-[popup-open]:rotate-180",
87
+ tooltip ? "right-10" : "right-3",
88
+ )}
89
+ >
90
+ <ChevronDown className="size-4" />
91
+ </SelectPrimitive.Icon>
92
+ </SelectPrimitive.Trigger>
93
+ {tooltip ? (
94
+ <FieldHint hint={tooltip} open={hintOpen} id={hintId} className="right-3" />
95
+ ) : null}
96
+ </div>
97
+
98
+ <SelectPrimitive.Portal>
99
+ <SelectPrimitive.Positioner
100
+ className="z-50 outline-none select-none"
101
+ sideOffset={6}
102
+ alignItemWithTrigger={false}
103
+ >
104
+ <SelectPrimitive.Popup className="max-h-[min(20rem,var(--available-height))] w-[var(--anchor-width)] origin-[var(--transform-origin)] overflow-hidden rounded-xl border border-zinc-200 bg-white shadow-lg transition-[opacity,scale] duration-150 data-ending-style:scale-98 data-ending-style:opacity-0 data-starting-style:scale-98 data-starting-style:opacity-0">
105
+ <SelectPrimitive.List className="max-h-[min(20rem,var(--available-height))] overflow-y-auto py-1.5">
106
+ {options.map((option) => (
107
+ <SelectPrimitive.Item
108
+ key={option}
109
+ value={option}
110
+ className="data-highlighted:text-brand grid cursor-pointer grid-cols-[1rem_1fr] items-center gap-2 px-4 py-2.5 text-sm outline-none select-none data-highlighted:bg-zinc-50"
111
+ >
112
+ <SelectPrimitive.ItemIndicator className="text-brand col-start-1">
113
+ <Check className="size-4" />
114
+ </SelectPrimitive.ItemIndicator>
115
+ <SelectPrimitive.ItemText className="col-start-2">
116
+ {option}
117
+ </SelectPrimitive.ItemText>
118
+ </SelectPrimitive.Item>
119
+ ))}
120
+ </SelectPrimitive.List>
121
+ </SelectPrimitive.Popup>
122
+ </SelectPrimitive.Positioner>
123
+ </SelectPrimitive.Portal>
124
+ </SelectPrimitive.Root>
125
+ );
126
+ }
127
+
128
+ export { Select };
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ import type * as React from "react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+ import { StepperProgress } from "@/components/ui/stepper/stepper-progress";
7
+ import { StepperRail } from "@/components/ui/stepper/stepper-rail";
8
+ import type { StepperLayoutProps } from "@/components/ui/stepper/types";
9
+
10
+ export type { StepItem, StepStatus } from "@/components/ui/stepper/types";
11
+
12
+ export interface StepperProps
13
+ extends StepperLayoutProps, Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {}
14
+
15
+ function Stepper({ steps, currentStep, onStepChange, className, ...props }: StepperProps) {
16
+ return (
17
+ <div className={cn("w-full", className)} {...props}>
18
+ <StepperProgress steps={steps} currentStep={currentStep} />
19
+ <StepperRail steps={steps} currentStep={currentStep} onStepChange={onStepChange} />
20
+ </div>
21
+ );
22
+ }
23
+
24
+ export { Stepper };
@@ -0,0 +1,20 @@
1
+ import { memo } from "react";
2
+ import Image from "next/image";
3
+
4
+ const StepDivider = memo(function StepDivider() {
5
+ return (
6
+ <span className="relative block h-full w-12 shrink-0">
7
+ <Image
8
+ src="/assets/images/breadcrumb_arrow.svg"
9
+ alt=""
10
+ aria-hidden
11
+ unoptimized
12
+ fill
13
+ sizes="21px"
14
+ className="object-contain"
15
+ />
16
+ </span>
17
+ );
18
+ });
19
+
20
+ export { StepDivider };
@@ -0,0 +1,33 @@
1
+ import { memo, type ReactNode } from "react";
2
+ import { Check } from "lucide-react";
3
+
4
+ import { cn } from "@/lib/utils";
5
+ import type { StepStatus } from "@/components/ui/stepper/types";
6
+
7
+ const indicatorStyles: Record<StepStatus, string> = {
8
+ completed: "bg-brand text-white",
9
+ current: "bg-brand text-white",
10
+ upcoming: "bg-zinc-200/80 text-zinc-500",
11
+ };
12
+
13
+ export interface StepIndicatorProps {
14
+ status: StepStatus;
15
+ index: number;
16
+ icon?: ReactNode;
17
+ }
18
+
19
+ const StepIndicator = memo(function StepIndicator({ status, index, icon }: StepIndicatorProps) {
20
+ return (
21
+ <span
22
+ aria-hidden
23
+ className={cn(
24
+ "flex size-9 shrink-0 items-center justify-center rounded-full text-[0.9375rem] font-semibold transition-colors duration-200",
25
+ indicatorStyles[status],
26
+ )}
27
+ >
28
+ {status === "completed" ? <Check className="size-4" strokeWidth={3} /> : (icon ?? index + 1)}
29
+ </span>
30
+ );
31
+ });
32
+
33
+ export { StepIndicator };
@@ -0,0 +1,29 @@
1
+ import { memo } from "react";
2
+
3
+ import { cn } from "@/lib/utils";
4
+ import type { StepStatus } from "@/components/ui/stepper/types";
5
+
6
+ const labelStyles: Record<StepStatus, string> = {
7
+ completed: "text-zinc-900 font-semibold",
8
+ current: "text-zinc-900 font-semibold",
9
+ upcoming: "text-zinc-400 font-medium",
10
+ };
11
+
12
+ export interface StepLabelProps {
13
+ label: string;
14
+ description?: string;
15
+ status: StepStatus;
16
+ }
17
+
18
+ const StepLabel = memo(function StepLabel({ label, description, status }: StepLabelProps) {
19
+ return (
20
+ <span className={cn("flex min-w-0 flex-col text-left transition-colors", labelStyles[status])}>
21
+ <span className="truncate text-[0.9375rem] whitespace-nowrap">{label}</span>
22
+ {description ? (
23
+ <span className="truncate text-xs whitespace-nowrap opacity-70">{description}</span>
24
+ ) : null}
25
+ </span>
26
+ );
27
+ });
28
+
29
+ export { StepLabel };
@@ -0,0 +1,40 @@
1
+ "use client";
2
+
3
+ import { memo } from "react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+ import type { StepperLayoutProps } from "@/components/ui/stepper/types";
7
+
8
+ /** Mobile layout: active step name, counter, and a segmented progress bar. */
9
+ const StepperProgress = memo(function StepperProgress({
10
+ steps,
11
+ currentStep,
12
+ }: Omit<StepperLayoutProps, "onStepChange">) {
13
+ const totalSteps = steps.length;
14
+ const activeIndex = Math.min(Math.max(currentStep, 0), totalSteps - 1);
15
+
16
+ return (
17
+ <div className="rounded-2xl border border-zinc-200 bg-white p-4 md:hidden">
18
+ <div className="flex items-baseline justify-between gap-3">
19
+ <span className="text-brand text-sm font-semibold">{steps[activeIndex]?.label}</span>
20
+ <span className="shrink-0 text-xs font-medium text-zinc-400 tabular-nums">
21
+ {activeIndex + 1} / {totalSteps}
22
+ </span>
23
+ </div>
24
+ <div className="mt-3 flex gap-1.5" role="presentation">
25
+ {steps.map((step, index) => (
26
+ <span
27
+ key={step.label}
28
+ title={step.label}
29
+ className={cn(
30
+ "h-1.5 flex-1 rounded-full transition-colors duration-300",
31
+ index <= currentStep ? "bg-brand" : "bg-zinc-200",
32
+ )}
33
+ />
34
+ ))}
35
+ </div>
36
+ </div>
37
+ );
38
+ });
39
+
40
+ export { StepperProgress };
@@ -0,0 +1,107 @@
1
+ "use client";
2
+
3
+ import { Fragment, memo, useCallback } from "react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+ import { StepDivider } from "@/components/ui/stepper/step-divider";
7
+ import { StepIndicator } from "@/components/ui/stepper/step-indicator";
8
+ import { StepLabel } from "@/components/ui/stepper/step-label";
9
+ import {
10
+ getStepStatus,
11
+ type StepItem,
12
+ type StepperLayoutProps,
13
+ } from "@/components/ui/stepper/types";
14
+
15
+ interface StepperRailItemProps {
16
+ step: StepItem;
17
+ index: number;
18
+ currentStep: number;
19
+ onStepChange?: (index: number) => void;
20
+ }
21
+
22
+ function StepperRailItem({ step, index, currentStep, onStepChange }: StepperRailItemProps) {
23
+ const status = getStepStatus(index, currentStep);
24
+ const isCurrent = status === "current";
25
+ const clickable = Boolean(onStepChange) && status === "completed";
26
+ const title = step.description ? `${step.label} — ${step.description}` : step.label;
27
+
28
+ const handleClick = useCallback(() => onStepChange?.(index), [onStepChange, index]);
29
+
30
+ const className = cn(
31
+ "flex w-full items-center gap-4 rounded-full px-2.5 py-2.5 transition-colors duration-200",
32
+ isCurrent && "bg-white",
33
+ clickable &&
34
+ "cursor-pointer hover:bg-white/60 focus-visible:outline-brand focus-visible:outline-2 focus-visible:outline-offset-2",
35
+ );
36
+
37
+ const content = (
38
+ <>
39
+ <StepIndicator status={status} index={index} icon={step.icon} />
40
+ <StepLabel label={step.label} description={step.description} status={status} />
41
+ </>
42
+ );
43
+
44
+ if (clickable) {
45
+ return (
46
+ <button
47
+ type="button"
48
+ onClick={handleClick}
49
+ data-slot="stepper-item"
50
+ data-status={status}
51
+ className={className}
52
+ title={title}
53
+ >
54
+ {content}
55
+ <span className="sr-only">adımına dön</span>
56
+ </button>
57
+ );
58
+ }
59
+
60
+ return (
61
+ <div
62
+ aria-current={isCurrent ? "step" : undefined}
63
+ data-slot="stepper-item"
64
+ data-status={status}
65
+ className={className}
66
+ title={title}
67
+ >
68
+ {content}
69
+ </div>
70
+ );
71
+ }
72
+
73
+ const StepperRail = memo(function StepperRail({
74
+ steps,
75
+ currentStep,
76
+ onStepChange,
77
+ }: StepperLayoutProps) {
78
+ return (
79
+ <nav aria-label="İlerleme" data-slot="stepper" className="relative hidden md:block">
80
+ <span
81
+ aria-hidden
82
+ className="pointer-events-none absolute top-1/2 left-1/2 h-px w-screen -translate-x-1/2 -translate-y-1/2 bg-zinc-200/80"
83
+ />
84
+ <ol className="relative flex w-full items-stretch rounded-full border border-zinc-200/90 bg-white px-0.75">
85
+ {steps.map((step, index) => (
86
+ <Fragment key={step.label}>
87
+ <li className="flex min-w-0 flex-1">
88
+ <StepperRailItem
89
+ step={step}
90
+ index={index}
91
+ currentStep={currentStep}
92
+ onStepChange={onStepChange}
93
+ />
94
+ </li>
95
+ {index < steps.length - 1 ? (
96
+ <li aria-hidden className="mr-6 ml-1 flex shrink-0 items-stretch">
97
+ <StepDivider />
98
+ </li>
99
+ ) : null}
100
+ </Fragment>
101
+ ))}
102
+ </ol>
103
+ </nav>
104
+ );
105
+ });
106
+
107
+ export { StepperRail };
@@ -0,0 +1,26 @@
1
+ import type * as React from "react";
2
+
3
+ export type StepStatus = "completed" | "current" | "upcoming";
4
+
5
+ export interface StepItem {
6
+ /** Main label shown next to the step indicator. */
7
+ label: string;
8
+ /** Optional secondary text shown under the label. */
9
+ description?: string;
10
+ /** Custom indicator. Falls back to the step number, or a check when completed. */
11
+ icon?: React.ReactNode;
12
+ }
13
+
14
+ export interface StepperLayoutProps {
15
+ steps: StepItem[];
16
+ /** Zero-based index of the active step. */
17
+ currentStep: number;
18
+ /** Called with the target index when a completed step is pressed. */
19
+ onStepChange?: (index: number) => void;
20
+ }
21
+
22
+ export function getStepStatus(index: number, currentStep: number): StepStatus {
23
+ if (index < currentStep) return "completed";
24
+ if (index === currentStep) return "current";
25
+ return "upcoming";
26
+ }
@@ -0,0 +1,54 @@
1
+ "use client";
2
+
3
+ import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function TooltipProvider({ delay = 0, ...props }: TooltipPrimitive.Provider.Props) {
8
+ return <TooltipPrimitive.Provider data-slot="tooltip-provider" delay={delay} {...props} />;
9
+ }
10
+
11
+ function Tooltip({ ...props }: TooltipPrimitive.Root.Props) {
12
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />;
13
+ }
14
+
15
+ function TooltipTrigger({ ...props }: TooltipPrimitive.Trigger.Props) {
16
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
17
+ }
18
+
19
+ function TooltipContent({
20
+ className,
21
+ side = "top",
22
+ sideOffset = 4,
23
+ align = "center",
24
+ alignOffset = 0,
25
+ children,
26
+ ...props
27
+ }: TooltipPrimitive.Popup.Props &
28
+ Pick<TooltipPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset">) {
29
+ return (
30
+ <TooltipPrimitive.Portal>
31
+ <TooltipPrimitive.Positioner
32
+ align={align}
33
+ alignOffset={alignOffset}
34
+ side={side}
35
+ sideOffset={sideOffset}
36
+ className="isolate z-50"
37
+ >
38
+ <TooltipPrimitive.Popup
39
+ data-slot="tooltip-content"
40
+ className={cn(
41
+ "bg-foreground text-background data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 inline-flex w-fit max-w-xs origin-(--transform-origin) items-center gap-1.5 rounded-md px-3 py-1.5 text-xs has-data-[slot=kbd]:pr-1.5 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm",
42
+ className,
43
+ )}
44
+ {...props}
45
+ >
46
+ {children}
47
+ <TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%-2px)] rotate-45 rounded-[2px] data-[side=bottom]:top-1 data-[side=inline-end]:top-1/2! data-[side=inline-end]:-left-1 data-[side=inline-end]:-translate-y-1/2 data-[side=inline-start]:top-1/2! data-[side=inline-start]:-right-1 data-[side=inline-start]:-translate-y-1/2 data-[side=left]:top-1/2! data-[side=left]:-right-1 data-[side=left]:-translate-y-1/2 data-[side=right]:top-1/2! data-[side=right]:-left-1 data-[side=right]:-translate-y-1/2 data-[side=top]:-bottom-2.5" />
48
+ </TooltipPrimitive.Popup>
49
+ </TooltipPrimitive.Positioner>
50
+ </TooltipPrimitive.Portal>
51
+ );
52
+ }
53
+
54
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
@@ -0,0 +1,2 @@
1
+ /** Everything that is not a digit, removed. */
2
+ export const digits = (value: string) => value.replace(/\D/g, "");
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+
3
+ import { digits } from "@/lib/normalize";
4
+ import { isValidTckn } from "@/lib/tckn";
5
+
6
+ export const personalSchema = z.object({
7
+ tckn: z
8
+ .string()
9
+ .min(1, "Kimlik numarası boş bırakılamaz")
10
+ .refine((v) => /^[1-9]\d{10}$/.test(digits(v)), "Kimlik numarası 11 haneli olmalıdır")
11
+ .refine(isValidTckn, "Geçerli bir kimlik numarası giriniz"),
12
+ phone: z
13
+ .string()
14
+ .min(1, "Cep telefonu boş bırakılamaz")
15
+ .refine((v) => /^0?5\d{9}$/.test(digits(v)), "Geçerli bir cep telefonu giriniz"),
16
+ email: z
17
+ .string()
18
+ .min(1, "E-posta adresi boş bırakılamaz")
19
+ .pipe(z.email("Geçerli bir e-posta adresi giriniz")),
20
+ occupation: z.string().min(1, "Mesleğinizi seçiniz"),
21
+ });
22
+
23
+ export const healthSchema = z.object({
24
+ hasDiagnosis: z.enum(["yes", "no"], "Lütfen bir seçim yapınız"),
25
+ });
26
+
27
+ export const quoteSchema = personalSchema.extend(healthSchema.shape);
28
+
29
+ export type QuoteFormValues = z.infer<typeof quoteSchema>;
30
+
31
+ const fieldsOf = (schema: z.ZodObject) => Object.keys(schema.shape) as (keyof QuoteFormValues)[];
32
+
33
+ export const stepFields: readonly (keyof QuoteFormValues)[][] = [
34
+ fieldsOf(personalSchema),
35
+ fieldsOf(healthSchema),
36
+ [],
37
+ ];
38
+
39
+ export const quoteDefaultValues: QuoteFormValues = {
40
+ tckn: "",
41
+ phone: "",
42
+ email: "",
43
+ occupation: "" as QuoteFormValues["occupation"],
44
+ hasDiagnosis: "" as QuoteFormValues["hasDiagnosis"],
45
+ };
@@ -0,0 +1,31 @@
1
+ import { digits } from "@/lib/normalize";
2
+
3
+ /**
4
+ * T.C. kimlik numarası validation.
5
+ *
6
+ * The length and leading-digit rules alone accept roughly ten billion strings
7
+ * that no citizen holds. The last two digits are check digits, and verifying
8
+ * them rejects ~99% of those — which is what stops a typo from reaching the
9
+ * policy system as a real-looking identity.
10
+ *
11
+ * - digits 1..9 feed two weighted sums
12
+ * - digit 10 = (odd-position sum × 7 − even-position sum) mod 10
13
+ * - digit 11 = (sum of the first ten digits) mod 10
14
+ */
15
+ export function isValidTckn(value: string): boolean {
16
+ const normalized = digits(value);
17
+ if (!/^[1-9]\d{10}$/.test(normalized)) return false;
18
+
19
+ const d = [...normalized].map(Number);
20
+
21
+ const odd = d[0] + d[2] + d[4] + d[6] + d[8];
22
+ const even = d[1] + d[3] + d[5] + d[7];
23
+
24
+ // The subtraction can go negative, and JS `%` keeps the sign — so it is
25
+ // wrapped back into 0..9 rather than trusted directly.
26
+ const tenth = (((odd * 7 - even) % 10) + 10) % 10;
27
+ if (tenth !== d[9]) return false;
28
+
29
+ const eleventh = d.slice(0, 10).reduce((total, digit) => total + digit, 0) % 10;
30
+ return eleventh === d[10];
31
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }