@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.
- package/LICENSE +21 -0
- package/README.md +99 -0
- package/bin/me-ui.js +60 -0
- package/package.json +34 -0
- package/registry/assets/breadcrumb_arrow.svg +3 -0
- package/registry/components/stepper-provider.tsx +88 -0
- package/registry/components/stepper-view.tsx +40 -0
- package/registry/components/steps/form-input.tsx +68 -0
- package/registry/components/steps/form-select.tsx +42 -0
- package/registry/components/steps/step-field.tsx +43 -0
- package/registry/components/ui/button.tsx +69 -0
- package/registry/components/ui/field-hint.tsx +68 -0
- package/registry/components/ui/input.tsx +87 -0
- package/registry/components/ui/select.tsx +128 -0
- package/registry/components/ui/stepper/index.tsx +24 -0
- package/registry/components/ui/stepper/step-divider.tsx +20 -0
- package/registry/components/ui/stepper/step-indicator.tsx +33 -0
- package/registry/components/ui/stepper/step-label.tsx +29 -0
- package/registry/components/ui/stepper/stepper-progress.tsx +40 -0
- package/registry/components/ui/stepper/stepper-rail.tsx +107 -0
- package/registry/components/ui/stepper/types.ts +26 -0
- package/registry/components/ui/tooltip.tsx +54 -0
- package/registry/lib/normalize.ts +2 -0
- package/registry/lib/quote-schema.ts +45 -0
- package/registry/lib/tckn.ts +31 -0
- package/registry/lib/utils.ts +6 -0
- package/registry.json +252 -0
- package/src/args.js +47 -0
- package/src/commands/add.js +118 -0
- package/src/commands/list.js +20 -0
- package/src/css.js +80 -0
- package/src/deps.js +52 -0
- package/src/jsonc.js +44 -0
- package/src/log.js +32 -0
- package/src/manifest.js +58 -0
- package/src/paths.js +65 -0
- 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,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
|
+
}
|