@rahulapgm/skyblue-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 (49) hide show
  1. package/README.md +25 -0
  2. package/package.json +55 -0
  3. package/src/animation.tsx +117 -0
  4. package/src/autocomplete.tsx +271 -0
  5. package/src/badge.tsx +42 -0
  6. package/src/button.tsx +88 -0
  7. package/src/card.tsx +37 -0
  8. package/src/checkbox.tsx +36 -0
  9. package/src/chip.tsx +97 -0
  10. package/src/cluster.tsx +60 -0
  11. package/src/container.tsx +39 -0
  12. package/src/datepicker.tsx +59 -0
  13. package/src/drawer.tsx +126 -0
  14. package/src/dropdown.tsx +202 -0
  15. package/src/feature-card.tsx +33 -0
  16. package/src/floating-button.tsx +51 -0
  17. package/src/grid.tsx +94 -0
  18. package/src/hero-banner.tsx +82 -0
  19. package/src/icon-button.tsx +78 -0
  20. package/src/index.ts +32 -0
  21. package/src/input.tsx +62 -0
  22. package/src/label.tsx +20 -0
  23. package/src/loader.tsx +90 -0
  24. package/src/menu.tsx +100 -0
  25. package/src/message-box.tsx +46 -0
  26. package/src/metric.tsx +41 -0
  27. package/src/modal.tsx +110 -0
  28. package/src/navigation.tsx +147 -0
  29. package/src/number-input.tsx +127 -0
  30. package/src/pagination.tsx +102 -0
  31. package/src/phone-number-input.tsx +95 -0
  32. package/src/progress.tsx +65 -0
  33. package/src/radio.tsx +36 -0
  34. package/src/result.tsx +43 -0
  35. package/src/section.tsx +36 -0
  36. package/src/select.tsx +78 -0
  37. package/src/skeleton.tsx +17 -0
  38. package/src/stack.tsx +35 -0
  39. package/src/stat-card.tsx +69 -0
  40. package/src/steps.tsx +115 -0
  41. package/src/swatch.tsx +70 -0
  42. package/src/switch.tsx +60 -0
  43. package/src/table.tsx +88 -0
  44. package/src/tabs.tsx +100 -0
  45. package/src/textarea.tsx +52 -0
  46. package/src/timeline.tsx +60 -0
  47. package/src/toast.tsx +144 -0
  48. package/src/tooltip.tsx +56 -0
  49. package/src/utils.ts +3 -0
package/src/result.tsx ADDED
@@ -0,0 +1,43 @@
1
+ import type { ReactNode } from "react";
2
+ import { Ban, CheckCircle2, CircleAlert, Inbox, Info } from "lucide-react";
3
+
4
+ import { Card } from "./card";
5
+ import { cn } from "./utils";
6
+
7
+ type ResultStatus = "success" | "info" | "warning" | "error" | "empty";
8
+
9
+ type ResultProps = {
10
+ status?: ResultStatus;
11
+ title: string;
12
+ description?: string;
13
+ icon?: ReactNode;
14
+ actions?: ReactNode;
15
+ className?: string;
16
+ };
17
+
18
+ const defaultIcons: Record<ResultStatus, ReactNode> = {
19
+ success: <CheckCircle2 className="h-8 w-8 text-(--color-status-success)" />,
20
+ info: <Info className="h-8 w-8 text-(--color-status-info)" />,
21
+ warning: <CircleAlert className="h-8 w-8 text-(--color-status-warning)" />,
22
+ error: <Ban className="h-8 w-8 text-(--color-status-error)" />,
23
+ empty: <Inbox className="h-8 w-8 text-(--ink-subtle)" />,
24
+ };
25
+
26
+ export function Result({ status = "info", title, description, icon, actions, className }: ResultProps) {
27
+ return (
28
+ <Card
29
+ tone="surface"
30
+ padding="lg"
31
+ className={cn("flex flex-col items-start gap-4 text-left", className)}
32
+ >
33
+ <div className="flex h-14 w-14 items-center justify-center rounded-3xl bg-(--color-surface-muted)">
34
+ {icon ?? defaultIcons[status]}
35
+ </div>
36
+ <div>
37
+ <h3 className="type-subheading">{title}</h3>
38
+ {description ? <p className="type-body mt-2 text-(--ink-muted)">{description}</p> : null}
39
+ </div>
40
+ {actions ? <div className="flex flex-wrap gap-3">{actions}</div> : null}
41
+ </Card>
42
+ );
43
+ }
@@ -0,0 +1,36 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+
3
+ import { cn } from "./utils";
4
+
5
+ type SectionSpacing = "sm" | "md" | "lg" | "xl";
6
+
7
+ type SectionProps = HTMLAttributes<HTMLElement> & {
8
+ children: ReactNode;
9
+ as?: "section" | "div" | "main";
10
+ spacing?: SectionSpacing;
11
+ contained?: boolean;
12
+ };
13
+
14
+ const spacingClasses: Record<SectionSpacing, string> = {
15
+ sm: "gutter-section-sm",
16
+ md: "gutter-section-md",
17
+ lg: "gutter-section-lg",
18
+ xl: "gutter-section-xl",
19
+ };
20
+
21
+ export function Section({
22
+ children,
23
+ as = "section",
24
+ spacing = "lg",
25
+ contained = false,
26
+ className,
27
+ ...props
28
+ }: SectionProps) {
29
+ const Component = as;
30
+
31
+ return (
32
+ <Component {...props} className={cn(spacingClasses[spacing], contained && "gutter-page", className)}>
33
+ {children}
34
+ </Component>
35
+ );
36
+ }
package/src/select.tsx ADDED
@@ -0,0 +1,78 @@
1
+ import type { ReactNode, SelectHTMLAttributes } from "react";
2
+ import { ChevronDown } from "lucide-react";
3
+
4
+ import { Label } from "./label";
5
+ import { cn } from "./utils";
6
+
7
+ type SelectOption = {
8
+ label: string;
9
+ value: string;
10
+ disabled?: boolean;
11
+ };
12
+
13
+ type SelectProps = Omit<SelectHTMLAttributes<HTMLSelectElement>, "children"> & {
14
+ label?: string;
15
+ helperText?: string;
16
+ errorText?: string;
17
+ options: SelectOption[];
18
+ placeholder?: string;
19
+ leadingSlot?: ReactNode;
20
+ };
21
+
22
+ export function Select({
23
+ label,
24
+ helperText,
25
+ errorText,
26
+ options,
27
+ placeholder,
28
+ leadingSlot,
29
+ className,
30
+ id,
31
+ required,
32
+ ...props
33
+ }: SelectProps) {
34
+ const selectId = id ?? label?.toLowerCase().replace(/\s+/g, "-");
35
+ const hasError = Boolean(errorText);
36
+
37
+ return (
38
+ <div className="w-full">
39
+ {label ? (
40
+ <Label htmlFor={selectId} requiredMark={required} className="mb-2 block">
41
+ {label}
42
+ </Label>
43
+ ) : null}
44
+ <div
45
+ className={cn(
46
+ "flex min-h-12 items-center gap-3 rounded-2xl border bg-(--surface-card) px-4 shadow-(--shadow-sm) transition-colors",
47
+ hasError
48
+ ? "border-(--color-status-error)"
49
+ : "border-(--line-soft) hover:border-(--color-line-strong)",
50
+ )}
51
+ >
52
+ {leadingSlot}
53
+ <select
54
+ {...props}
55
+ id={selectId}
56
+ required={required}
57
+ className={cn(
58
+ "type-body min-h-12 w-full appearance-none bg-transparent text-(--foreground) outline-none",
59
+ className,
60
+ )}
61
+ >
62
+ {placeholder ? <option value="">{placeholder}</option> : null}
63
+ {options.map((option) => (
64
+ <option key={option.value} value={option.value} disabled={option.disabled}>
65
+ {option.label}
66
+ </option>
67
+ ))}
68
+ </select>
69
+ <ChevronDown className="h-4 w-4 text-(--ink-subtle)" />
70
+ </div>
71
+ {errorText ? (
72
+ <p className="type-body mt-2 text-(--color-status-error)">{errorText}</p>
73
+ ) : helperText ? (
74
+ <p className="type-body mt-2 text-(--ink-subtle)">{helperText}</p>
75
+ ) : null}
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,17 @@
1
+ import { cn } from "./utils";
2
+
3
+ type SkeletonProps = {
4
+ className?: string;
5
+ rounded?: "sm" | "md" | "lg" | "full";
6
+ };
7
+
8
+ const roundedClasses = {
9
+ sm: "rounded-md",
10
+ md: "rounded-xl",
11
+ lg: "rounded-2xl",
12
+ full: "rounded-full",
13
+ } as const;
14
+
15
+ export function Skeleton({ className, rounded = "md" }: SkeletonProps) {
16
+ return <div aria-hidden="true" className={cn("skeleton-shimmer", roundedClasses[rounded], className)} />;
17
+ }
package/src/stack.tsx ADDED
@@ -0,0 +1,35 @@
1
+ import type { HTMLAttributes, ReactNode } from "react";
2
+
3
+ import { cn } from "./utils";
4
+
5
+ type StackGap = "xs" | "sm" | "md" | "lg" | "xl";
6
+ type StackAlign = "start" | "center" | "end" | "stretch";
7
+
8
+ type StackProps = HTMLAttributes<HTMLDivElement> & {
9
+ children: ReactNode;
10
+ gap?: StackGap;
11
+ align?: StackAlign;
12
+ };
13
+
14
+ const gapClasses: Record<StackGap, string> = {
15
+ xs: "gutter-grid-xs",
16
+ sm: "gutter-grid-sm",
17
+ md: "gutter-grid-md",
18
+ lg: "gutter-grid-lg",
19
+ xl: "gutter-grid-xl",
20
+ };
21
+
22
+ const alignClasses: Record<StackAlign, string> = {
23
+ start: "items-start",
24
+ center: "items-center",
25
+ end: "items-end",
26
+ stretch: "items-stretch",
27
+ };
28
+
29
+ export function Stack({ children, gap = "md", align = "stretch", className, ...props }: StackProps) {
30
+ return (
31
+ <div {...props} className={cn("flex flex-col", gapClasses[gap], alignClasses[align], className)}>
32
+ {children}
33
+ </div>
34
+ );
35
+ }
@@ -0,0 +1,69 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { Badge } from "./badge";
4
+ import { Card } from "./card";
5
+ import { cn } from "./utils";
6
+
7
+ type StatCardProps = {
8
+ label: string;
9
+ value: ReactNode;
10
+ change?: string;
11
+ tone?: "default" | "brand" | "success" | "warning";
12
+ icon?: ReactNode;
13
+ size?: "sm" | "md";
14
+ className?: string;
15
+ };
16
+
17
+ const toneClasses = {
18
+ default: "text-(--foreground)",
19
+ brand: "text-(--color-brand-primary)",
20
+ success: "text-(--color-status-success)",
21
+ warning: "text-(--color-status-warning)",
22
+ } as const;
23
+
24
+ export function StatCard({
25
+ label,
26
+ value,
27
+ change,
28
+ tone = "default",
29
+ icon,
30
+ size = "md",
31
+ className,
32
+ }: StatCardProps) {
33
+ return (
34
+ <Card
35
+ className={cn("min-w-0 overflow-hidden", size === "sm" ? "min-h-42" : "min-h-49", className)}
36
+ tone="surface"
37
+ padding="md"
38
+ >
39
+ <div className="flex items-start justify-between gap-3 sm:gap-4">
40
+ <div className="min-w-0 flex-1">
41
+ <p className="type-caption text-(--ink-subtle)">{label}</p>
42
+ <p
43
+ className={cn(
44
+ "mt-2 whitespace-normal text-balance font-extrabold leading-[0.95]",
45
+ size === "sm"
46
+ ? "text-[1.5rem] sm:text-[1.75rem]"
47
+ : "text-[1.625rem] sm:text-[2rem] lg:text-[2.25rem]",
48
+ toneClasses[tone],
49
+ )}
50
+ >
51
+ {value}
52
+ </p>
53
+ </div>
54
+ {icon ? (
55
+ <div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl bg-(--color-brand-primary-light) text-(--color-brand-primary) sm:h-11 sm:w-11">
56
+ {icon}
57
+ </div>
58
+ ) : null}
59
+ </div>
60
+ {change ? (
61
+ <div className="mt-3 sm:mt-4">
62
+ <Badge tone={tone === "success" ? "success" : tone === "warning" ? "warning" : "info"} size="sm">
63
+ {change}
64
+ </Badge>
65
+ </div>
66
+ ) : null}
67
+ </Card>
68
+ );
69
+ }
package/src/steps.tsx ADDED
@@ -0,0 +1,115 @@
1
+ import { Fragment } from "react";
2
+ import { ArrowDown, ArrowRight } from "lucide-react";
3
+
4
+ import { Badge } from "./badge";
5
+ import { cn } from "./utils";
6
+
7
+ export type StepItem = {
8
+ title: string;
9
+ description?: string;
10
+ };
11
+
12
+ type StepsProps = {
13
+ items: StepItem[];
14
+ current: number;
15
+ className?: string;
16
+ };
17
+
18
+ const statusLabels = {
19
+ completed: "Completed",
20
+ current: "Ongoing",
21
+ upcoming: "Upcoming",
22
+ } as const;
23
+
24
+ function getConnectorClass(stepNumber: number, current: number) {
25
+ if (stepNumber < current - 1) {
26
+ return "bg-(--color-status-success)";
27
+ }
28
+
29
+ if (stepNumber === current - 1) {
30
+ return "bg-(--color-brand-primary)";
31
+ }
32
+
33
+ return "bg-(--line-soft)";
34
+ }
35
+
36
+ function getConnectorIconClass(stepNumber: number, current: number) {
37
+ if (stepNumber < current - 1) {
38
+ return "text-(--color-status-success)";
39
+ }
40
+
41
+ if (stepNumber === current - 1) {
42
+ return "text-(--color-brand-primary)";
43
+ }
44
+
45
+ return "text-(--ink-subtle)";
46
+ }
47
+
48
+ export function Steps({ items, current, className }: StepsProps) {
49
+ return (
50
+ <div role="list" className={cn("w-full overflow-hidden", className)}>
51
+ <div className="flex flex-col gap-4 xl:flex-row xl:items-stretch xl:gap-5">
52
+ {items.map((item, index) => {
53
+ const status = index + 1 < current ? "completed" : index + 1 === current ? "current" : "upcoming";
54
+
55
+ return (
56
+ <Fragment key={item.title}>
57
+ <div className="min-w-0 flex-1">
58
+ <div
59
+ aria-current={status === "current" ? "step" : undefined}
60
+ className={cn(
61
+ "relative min-w-0 rounded-3xl border px-5 py-5 shadow-(--shadow-sm) sm:px-6 sm:py-6",
62
+ status === "completed" &&
63
+ "border-(--color-status-success-light) bg-(--color-status-success-light)/45",
64
+ status === "current" &&
65
+ "border-(--color-brand-primary-light) bg-(--color-brand-primary-light)/5 ring-2 ring-(--color-brand-primary-light)/80",
66
+ status === "upcoming" && "border-(--line-soft) bg-(--surface-card)",
67
+ )}
68
+ role="listitem"
69
+ >
70
+ <div className="absolute right-3 top-2">
71
+ <Badge
72
+ tone={status === "completed" ? "success" : status === "current" ? "brand" : "neutral"}
73
+ size="sm"
74
+ >
75
+ {statusLabels[status]}
76
+ </Badge>
77
+ </div>
78
+ <div className="flex items-start gap-4 sm:gap-5">
79
+ <div className="min-w-0 flex-1">
80
+ <h3 className="mt-1 pr-28 text-base font-extrabold leading-tight text-(--foreground) sm:pr-32 sm:text-lg">
81
+ {item.title}
82
+ </h3>
83
+ {item.description ? (
84
+ <p className="mt-2 max-w-[22ch] text-sm font-semibold leading-6 text-(--ink-muted)">
85
+ {item.description}
86
+ </p>
87
+ ) : null}
88
+ </div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ {index < items.length - 1 ? (
94
+ <div className="flex justify-center py-1 xl:w-20 xl:flex-none xl:items-center xl:py-0">
95
+ <div className="flex flex-col items-center gap-2 xl:w-full xl:flex-row xl:gap-2">
96
+ <span className={cn("h-8 w-px xl:hidden", getConnectorClass(index + 1, current))} />
97
+ <ArrowDown
98
+ className={cn("h-4 w-4 xl:hidden", getConnectorIconClass(index + 1, current))}
99
+ />
100
+ <span
101
+ className={cn("hidden h-px flex-1 xl:block", getConnectorClass(index + 1, current))}
102
+ />
103
+ <ArrowRight
104
+ className={cn("hidden h-4 w-4 xl:block", getConnectorIconClass(index + 1, current))}
105
+ />
106
+ </div>
107
+ </div>
108
+ ) : null}
109
+ </Fragment>
110
+ );
111
+ })}
112
+ </div>
113
+ </div>
114
+ );
115
+ }
package/src/swatch.tsx ADDED
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ import { Check, Copy } from "lucide-react";
4
+ import { useState } from "react";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ type SwatchProps = {
9
+ name: string;
10
+ value: string;
11
+ description?: string;
12
+ shape?: "square" | "circle";
13
+ size?: "sm" | "md" | "lg";
14
+ className?: string;
15
+ };
16
+
17
+ const previewSizeClasses = {
18
+ sm: "h-10 w-10",
19
+ md: "h-14 w-14",
20
+ lg: "h-20 w-20",
21
+ } as const;
22
+
23
+ export function Swatch({
24
+ name,
25
+ value,
26
+ description,
27
+ shape = "square",
28
+ size = "md",
29
+ className,
30
+ }: SwatchProps) {
31
+ const [copied, setCopied] = useState(false);
32
+
33
+ async function handleCopy() {
34
+ await navigator.clipboard.writeText(value);
35
+ setCopied(true);
36
+ window.setTimeout(() => setCopied(false), 1200);
37
+ }
38
+
39
+ return (
40
+ <div
41
+ className={cn(
42
+ "surface-glass flex items-center gap-4 rounded-3xl border border-(--line-soft) p-4 shadow-(--shadow-sm)",
43
+ className,
44
+ )}
45
+ >
46
+ <div
47
+ className={cn(
48
+ "shrink-0 border border-(--line-soft) shadow-(--shadow-sm)",
49
+ previewSizeClasses[size],
50
+ shape === "circle" ? "rounded-full" : "rounded-2xl",
51
+ )}
52
+ style={{ backgroundColor: value }}
53
+ aria-hidden
54
+ />
55
+ <div className="min-w-0 flex-1">
56
+ <p className="type-title">{name}</p>
57
+ <p className="type-caption mt-1 text-(--ink-subtle)">{value}</p>
58
+ {description ? <p className="type-body mt-2 text-(--ink-muted)">{description}</p> : null}
59
+ </div>
60
+ <button
61
+ type="button"
62
+ onClick={handleCopy}
63
+ className="inline-flex h-10 w-10 items-center justify-center rounded-2xl border border-(--line-soft) bg-(--surface-card) text-(--ink-muted) transition-colors hover:bg-(--color-surface-hover)"
64
+ aria-label={`Copy ${name} color value`}
65
+ >
66
+ {copied ? <Check className="h-4 w-4" /> : <Copy className="h-4 w-4" />}
67
+ </button>
68
+ </div>
69
+ );
70
+ }
package/src/switch.tsx ADDED
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ import type { InputHTMLAttributes } from "react";
4
+
5
+ import { Label } from "./label";
6
+ import { cn } from "./utils";
7
+
8
+ type SwitchProps = Omit<InputHTMLAttributes<HTMLInputElement>, "type"> & {
9
+ label: string;
10
+ description?: string;
11
+ };
12
+
13
+ export function Switch({
14
+ label,
15
+ description,
16
+ className,
17
+ id,
18
+ checked,
19
+ defaultChecked,
20
+ ...props
21
+ }: SwitchProps) {
22
+ const switchId = id ?? label.toLowerCase().replace(/\s+/g, "-");
23
+ const isChecked = checked ?? defaultChecked;
24
+
25
+ return (
26
+ <label
27
+ htmlFor={switchId}
28
+ className={cn(
29
+ "flex cursor-pointer items-center justify-between gap-4 rounded-2xl border border-(--line-soft) bg-(--surface-card) p-4 shadow-(--shadow-sm)",
30
+ className,
31
+ )}
32
+ >
33
+ <span>
34
+ <Label htmlFor={switchId}>{label}</Label>
35
+ {description ? (
36
+ <span suppressHydrationWarning className="type-body mt-1 block text-(--ink-subtle)">
37
+ {description}
38
+ </span>
39
+ ) : null}
40
+ </span>
41
+ <span className="relative inline-flex h-7 w-12 shrink-0 items-center">
42
+ <input
43
+ {...props}
44
+ id={switchId}
45
+ checked={checked}
46
+ defaultChecked={defaultChecked}
47
+ type="checkbox"
48
+ className="peer sr-only"
49
+ />
50
+ <span className="absolute inset-0 rounded-full bg-(--color-line-strong) transition-colors peer-checked:bg-(--color-brand-primary)" />
51
+ <span
52
+ className={cn(
53
+ "absolute left-1 h-5 w-5 rounded-full bg-white shadow-(--shadow-sm) transition-transform peer-checked:translate-x-5",
54
+ isChecked && "translate-x-5",
55
+ )}
56
+ />
57
+ </span>
58
+ </label>
59
+ );
60
+ }
package/src/table.tsx ADDED
@@ -0,0 +1,88 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import { cn } from "./utils";
4
+
5
+ export type TableColumn<T> = {
6
+ key: string;
7
+ header: ReactNode;
8
+ align?: "left" | "center" | "right";
9
+ width?: string;
10
+ render: (row: T, rowIndex: number) => ReactNode;
11
+ };
12
+
13
+ type TableProps<T> = {
14
+ columns: Array<TableColumn<T>>;
15
+ rows: T[];
16
+ rowKey: (row: T, rowIndex: number) => string;
17
+ emptyState?: ReactNode;
18
+ className?: string;
19
+ };
20
+
21
+ const alignClasses = {
22
+ left: "text-left",
23
+ center: "text-center",
24
+ right: "text-right",
25
+ } as const;
26
+
27
+ export function Table<T>({
28
+ columns,
29
+ rows,
30
+ rowKey,
31
+ emptyState = "No records found.",
32
+ className,
33
+ }: TableProps<T>) {
34
+ return (
35
+ <div
36
+ className={cn(
37
+ "overflow-hidden rounded-xl border border-(--line-soft) bg-(--surface-card) shadow-(--shadow-sm)",
38
+ className,
39
+ )}
40
+ >
41
+ <div className="overflow-x-auto">
42
+ <table className="min-w-full border-collapse">
43
+ <thead className="bg-(--color-surface-muted)">
44
+ <tr>
45
+ {columns.map((column) => (
46
+ <th
47
+ key={column.key}
48
+ className={cn(
49
+ "type-overline px-4 py-3 text-(--ink-subtle)",
50
+ alignClasses[column.align ?? "left"],
51
+ )}
52
+ style={column.width ? { width: column.width } : undefined}
53
+ >
54
+ {column.header}
55
+ </th>
56
+ ))}
57
+ </tr>
58
+ </thead>
59
+ <tbody>
60
+ {rows.length ? (
61
+ rows.map((row, rowIndex) => (
62
+ <tr key={rowKey(row, rowIndex)} className="border-t border-(--line-soft)">
63
+ {columns.map((column) => (
64
+ <td
65
+ key={column.key}
66
+ className={cn(
67
+ "type-body px-4 py-4 align-top text-(--foreground)",
68
+ alignClasses[column.align ?? "left"],
69
+ )}
70
+ >
71
+ {column.render(row, rowIndex)}
72
+ </td>
73
+ ))}
74
+ </tr>
75
+ ))
76
+ ) : (
77
+ <tr>
78
+ <td colSpan={columns.length} className="type-body px-4 py-10 text-center text-(--ink-subtle)">
79
+ {emptyState}
80
+ </td>
81
+ </tr>
82
+ )}
83
+ </tbody>
84
+ </table>
85
+ </div>
86
+ </div>
87
+ );
88
+ }