Expanic commited on
Commit
1bb466c
·
1 Parent(s): 0227d98

Deploy React + FastAPI to HuggingFace Spaces

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .DS_Store +0 -0
  2. .gitignore +7 -0
  3. BPOM Compliance AI UI Redesign/ATTRIBUTIONS.md +3 -0
  4. BPOM Compliance AI UI Redesign/README.md +11 -0
  5. BPOM Compliance AI UI Redesign/default_shadcn_theme.css +120 -0
  6. BPOM Compliance AI UI Redesign/guidelines/Guidelines.md +61 -0
  7. BPOM Compliance AI UI Redesign/index.html +22 -0
  8. BPOM Compliance AI UI Redesign/package-lock.json +0 -0
  9. BPOM Compliance AI UI Redesign/package.json +82 -0
  10. BPOM Compliance AI UI Redesign/pnpm-workspace.yaml +2 -0
  11. BPOM Compliance AI UI Redesign/postcss.config.mjs +15 -0
  12. BPOM Compliance AI UI Redesign/src/app/App.tsx +380 -0
  13. BPOM Compliance AI UI Redesign/src/app/components/Step1Upload.tsx +221 -0
  14. BPOM Compliance AI UI Redesign/src/app/components/Step2Review.tsx +414 -0
  15. BPOM Compliance AI UI Redesign/src/app/components/Step3Report.tsx +354 -0
  16. BPOM Compliance AI UI Redesign/src/app/components/StepIndicator.tsx +78 -0
  17. BPOM Compliance AI UI Redesign/src/app/components/figma/ImageWithFallback.tsx +27 -0
  18. BPOM Compliance AI UI Redesign/src/app/components/ui/accordion.tsx +66 -0
  19. BPOM Compliance AI UI Redesign/src/app/components/ui/alert-dialog.tsx +157 -0
  20. BPOM Compliance AI UI Redesign/src/app/components/ui/alert.tsx +66 -0
  21. BPOM Compliance AI UI Redesign/src/app/components/ui/aspect-ratio.tsx +11 -0
  22. BPOM Compliance AI UI Redesign/src/app/components/ui/avatar.tsx +53 -0
  23. BPOM Compliance AI UI Redesign/src/app/components/ui/badge.tsx +46 -0
  24. BPOM Compliance AI UI Redesign/src/app/components/ui/breadcrumb.tsx +109 -0
  25. BPOM Compliance AI UI Redesign/src/app/components/ui/button.tsx +58 -0
  26. BPOM Compliance AI UI Redesign/src/app/components/ui/calendar.tsx +75 -0
  27. BPOM Compliance AI UI Redesign/src/app/components/ui/card.tsx +92 -0
  28. BPOM Compliance AI UI Redesign/src/app/components/ui/carousel.tsx +241 -0
  29. BPOM Compliance AI UI Redesign/src/app/components/ui/chart.tsx +353 -0
  30. BPOM Compliance AI UI Redesign/src/app/components/ui/checkbox.tsx +32 -0
  31. BPOM Compliance AI UI Redesign/src/app/components/ui/collapsible.tsx +33 -0
  32. BPOM Compliance AI UI Redesign/src/app/components/ui/command.tsx +177 -0
  33. BPOM Compliance AI UI Redesign/src/app/components/ui/context-menu.tsx +252 -0
  34. BPOM Compliance AI UI Redesign/src/app/components/ui/dialog.tsx +135 -0
  35. BPOM Compliance AI UI Redesign/src/app/components/ui/drawer.tsx +132 -0
  36. BPOM Compliance AI UI Redesign/src/app/components/ui/dropdown-menu.tsx +257 -0
  37. BPOM Compliance AI UI Redesign/src/app/components/ui/form.tsx +168 -0
  38. BPOM Compliance AI UI Redesign/src/app/components/ui/hover-card.tsx +44 -0
  39. BPOM Compliance AI UI Redesign/src/app/components/ui/input-otp.tsx +77 -0
  40. BPOM Compliance AI UI Redesign/src/app/components/ui/input.tsx +21 -0
  41. BPOM Compliance AI UI Redesign/src/app/components/ui/label.tsx +24 -0
  42. BPOM Compliance AI UI Redesign/src/app/components/ui/menubar.tsx +276 -0
  43. BPOM Compliance AI UI Redesign/src/app/components/ui/navigation-menu.tsx +168 -0
  44. BPOM Compliance AI UI Redesign/src/app/components/ui/pagination.tsx +127 -0
  45. BPOM Compliance AI UI Redesign/src/app/components/ui/popover.tsx +48 -0
  46. BPOM Compliance AI UI Redesign/src/app/components/ui/progress.tsx +31 -0
  47. BPOM Compliance AI UI Redesign/src/app/components/ui/radio-group.tsx +45 -0
  48. BPOM Compliance AI UI Redesign/src/app/components/ui/resizable.tsx +56 -0
  49. BPOM Compliance AI UI Redesign/src/app/components/ui/scroll-area.tsx +58 -0
  50. BPOM Compliance AI UI Redesign/src/app/components/ui/select.tsx +189 -0
.DS_Store CHANGED
Binary files a/.DS_Store and b/.DS_Store differ
 
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ venv/
2
+ .env
3
+ __pycache__/
4
+ *.pyc
5
+ .DS_Store
6
+ BPOM Compliance AI UI Redesign/node_modules/
7
+ laporan_compliance_bpom.pdf
BPOM Compliance AI UI Redesign/ATTRIBUTIONS.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ This Figma Make file includes components from [shadcn/ui](https://ui.shadcn.com/) used under [MIT license](https://github.com/shadcn-ui/ui/blob/main/LICENSE.md).
2
+
3
+ This Figma Make file includes photos from [Unsplash](https://unsplash.com) used under [license](https://unsplash.com/license).
BPOM Compliance AI UI Redesign/README.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ # BPOM Compliance AI UI Redesign
3
+
4
+ This is a code bundle for BPOM Compliance AI UI Redesign. The original project is available at https://www.figma.com/design/8f6fPFaJejzKI4ybjgfcN9/BPOM-Compliance-AI-UI-Redesign.
5
+
6
+ ## Running the code
7
+
8
+ Run `npm i` to install the dependencies.
9
+
10
+ Run `npm run dev` to start the development server.
11
+
BPOM Compliance AI UI Redesign/default_shadcn_theme.css ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* KEEP_IN_SYNC(fullscreen/resources/figmake/shadcn/globals.css) */
2
+
3
+ :root {
4
+ --font-size: 16px;
5
+ --background: #ffffff;
6
+ --foreground: oklch(0.145 0 0);
7
+ --card: #ffffff;
8
+ --card-foreground: oklch(0.145 0 0);
9
+ --popover: oklch(1 0 0);
10
+ --popover-foreground: oklch(0.145 0 0);
11
+ --primary: #030213;
12
+ --primary-foreground: oklch(1 0 0);
13
+ --secondary: oklch(0.95 0.0058 264.53);
14
+ --secondary-foreground: #030213;
15
+ --muted: #ececf0;
16
+ --muted-foreground: #717182;
17
+ --accent: #e9ebef;
18
+ --accent-foreground: #030213;
19
+ --destructive: #d4183d;
20
+ --destructive-foreground: #ffffff;
21
+ --border: rgba(0, 0, 0, 0.1);
22
+ --input: transparent;
23
+ --input-background: #f3f3f5;
24
+ --switch-background: #cbced4;
25
+ --font-weight-medium: 500;
26
+ --font-weight-normal: 400;
27
+ --ring: oklch(0.708 0 0);
28
+ --chart-1: oklch(0.646 0.222 41.116);
29
+ --chart-2: oklch(0.6 0.118 184.704);
30
+ --chart-3: oklch(0.398 0.07 227.392);
31
+ --chart-4: oklch(0.828 0.189 84.429);
32
+ --chart-5: oklch(0.769 0.188 70.08);
33
+ --radius: 0.625rem;
34
+ --sidebar: oklch(0.985 0 0);
35
+ --sidebar-foreground: oklch(0.145 0 0);
36
+ --sidebar-primary: #030213;
37
+ --sidebar-primary-foreground: oklch(0.985 0 0);
38
+ --sidebar-accent: oklch(0.97 0 0);
39
+ --sidebar-accent-foreground: oklch(0.205 0 0);
40
+ --sidebar-border: oklch(0.922 0 0);
41
+ --sidebar-ring: oklch(0.708 0 0);
42
+ }
43
+
44
+ .dark {
45
+ --background: oklch(0.145 0 0);
46
+ --foreground: oklch(0.985 0 0);
47
+ --card: oklch(0.145 0 0);
48
+ --card-foreground: oklch(0.985 0 0);
49
+ --popover: oklch(0.145 0 0);
50
+ --popover-foreground: oklch(0.985 0 0);
51
+ --primary: oklch(0.985 0 0);
52
+ --primary-foreground: oklch(0.205 0 0);
53
+ --secondary: oklch(0.269 0 0);
54
+ --secondary-foreground: oklch(0.985 0 0);
55
+ --muted: oklch(0.269 0 0);
56
+ --muted-foreground: oklch(0.708 0 0);
57
+ --accent: oklch(0.269 0 0);
58
+ --accent-foreground: oklch(0.985 0 0);
59
+ --destructive: oklch(0.396 0.141 25.723);
60
+ --destructive-foreground: oklch(0.637 0.237 25.331);
61
+ --border: oklch(0.269 0 0);
62
+ --input: oklch(0.269 0 0);
63
+ --ring: oklch(0.439 0 0);
64
+ --font-weight-medium: 500;
65
+ --font-weight-normal: 400;
66
+ --chart-1: oklch(0.488 0.243 264.376);
67
+ --chart-2: oklch(0.696 0.17 162.48);
68
+ --chart-3: oklch(0.769 0.188 70.08);
69
+ --chart-4: oklch(0.627 0.265 303.9);
70
+ --chart-5: oklch(0.645 0.246 16.439);
71
+ --sidebar: oklch(0.205 0 0);
72
+ --sidebar-foreground: oklch(0.985 0 0);
73
+ --sidebar-primary: oklch(0.488 0.243 264.376);
74
+ --sidebar-primary-foreground: oklch(0.985 0 0);
75
+ --sidebar-accent: oklch(0.269 0 0);
76
+ --sidebar-accent-foreground: oklch(0.985 0 0);
77
+ --sidebar-border: oklch(0.269 0 0);
78
+ --sidebar-ring: oklch(0.439 0 0);
79
+ }
80
+
81
+ @theme inline {
82
+ --color-background: var(--background);
83
+ --color-foreground: var(--foreground);
84
+ --color-card: var(--card);
85
+ --color-card-foreground: var(--card-foreground);
86
+ --color-popover: var(--popover);
87
+ --color-popover-foreground: var(--popover-foreground);
88
+ --color-primary: var(--primary);
89
+ --color-primary-foreground: var(--primary-foreground);
90
+ --color-secondary: var(--secondary);
91
+ --color-secondary-foreground: var(--secondary-foreground);
92
+ --color-muted: var(--muted);
93
+ --color-muted-foreground: var(--muted-foreground);
94
+ --color-accent: var(--accent);
95
+ --color-accent-foreground: var(--accent-foreground);
96
+ --color-destructive: var(--destructive);
97
+ --color-destructive-foreground: var(--destructive-foreground);
98
+ --color-border: var(--border);
99
+ --color-input: var(--input);
100
+ --color-input-background: var(--input-background);
101
+ --color-switch-background: var(--switch-background);
102
+ --color-ring: var(--ring);
103
+ --color-chart-1: var(--chart-1);
104
+ --color-chart-2: var(--chart-2);
105
+ --color-chart-3: var(--chart-3);
106
+ --color-chart-4: var(--chart-4);
107
+ --color-chart-5: var(--chart-5);
108
+ --radius-sm: calc(var(--radius) - 4px);
109
+ --radius-md: calc(var(--radius) - 2px);
110
+ --radius-lg: var(--radius);
111
+ --radius-xl: calc(var(--radius) + 4px);
112
+ --color-sidebar: var(--sidebar);
113
+ --color-sidebar-foreground: var(--sidebar-foreground);
114
+ --color-sidebar-primary: var(--sidebar-primary);
115
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
116
+ --color-sidebar-accent: var(--sidebar-accent);
117
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
118
+ --color-sidebar-border: var(--sidebar-border);
119
+ --color-sidebar-ring: var(--sidebar-ring);
120
+ }
BPOM Compliance AI UI Redesign/guidelines/Guidelines.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ **Add your own guidelines here**
2
+ <!--
3
+
4
+ System Guidelines
5
+
6
+ Use this file to provide the AI with rules and guidelines you want it to follow.
7
+ This template outlines a few examples of things you can add. You can add your own sections and format it to suit your needs
8
+
9
+ TIP: More context isn't always better. It can confuse the LLM. Try and add the most important rules you need
10
+
11
+ # General guidelines
12
+
13
+ Any general rules you want the AI to follow.
14
+ For example:
15
+
16
+ * Only use absolute positioning when necessary. Opt for responsive and well structured layouts that use flexbox and grid by default
17
+ * Refactor code as you go to keep code clean
18
+ * Keep file sizes small and put helper functions and components in their own files.
19
+
20
+ --------------
21
+
22
+ # Design system guidelines
23
+ Rules for how the AI should make generations look like your company's design system
24
+
25
+ Additionally, if you select a design system to use in the prompt box, you can reference
26
+ your design system's components, tokens, variables and components.
27
+ For example:
28
+
29
+ * Use a base font-size of 14px
30
+ * Date formats should always be in the format “Jun 10”
31
+ * The bottom toolbar should only ever have a maximum of 4 items
32
+ * Never use the floating action button with the bottom toolbar
33
+ * Chips should always come in sets of 3 or more
34
+ * Don't use a dropdown if there are 2 or fewer options
35
+
36
+ You can also create sub sections and add more specific details
37
+ For example:
38
+
39
+
40
+ ## Button
41
+ The Button component is a fundamental interactive element in our design system, designed to trigger actions or navigate
42
+ users through the application. It provides visual feedback and clear affordances to enhance user experience.
43
+
44
+ ### Usage
45
+ Buttons should be used for important actions that users need to take, such as form submissions, confirming choices,
46
+ or initiating processes. They communicate interactivity and should have clear, action-oriented labels.
47
+
48
+ ### Variants
49
+ * Primary Button
50
+ * Purpose : Used for the main action in a section or page
51
+ * Visual Style : Bold, filled with the primary brand color
52
+ * Usage : One primary button per section to guide users toward the most important action
53
+ * Secondary Button
54
+ * Purpose : Used for alternative or supporting actions
55
+ * Visual Style : Outlined with the primary color, transparent background
56
+ * Usage : Can appear alongside a primary button for less important actions
57
+ * Tertiary Button
58
+ * Purpose : Used for the least important actions
59
+ * Visual Style : Text-only with no border, using primary color
60
+ * Usage : For actions that should be available but not emphasized
61
+ -->
BPOM Compliance AI UI Redesign/index.html ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+
6
+ <meta charset="UTF-8" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>BPOM Compliance AI UI Redesign</title>
9
+ <meta name="description" content="Streamlines Indonesian food product registration by automating BPOM compliance checks with clear results, editable AI insights, and downloadable reports." />
10
+ <meta name="robots" content="noindex, nofollow" />
11
+ <style>html, body { height: 100%; margin: 0; } #root { height: 100%; }</style>
12
+
13
+ </head>
14
+
15
+ <body>
16
+
17
+ <div id="root"></div>
18
+ <script type="module" src="/src/main.tsx"></script>
19
+
20
+ </body>
21
+ </html>
22
+
BPOM Compliance AI UI Redesign/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
BPOM Compliance AI UI Redesign/package.json ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "@figma/my-make-file",
3
+ "private": true,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "vite build",
8
+ "dev": "vite"
9
+ },
10
+ "dependencies": {
11
+ "@emotion/react": "11.14.0",
12
+ "@emotion/styled": "11.14.1",
13
+ "@mui/icons-material": "7.3.5",
14
+ "@mui/material": "7.3.5",
15
+ "@popperjs/core": "2.11.8",
16
+ "@radix-ui/react-accordion": "1.2.3",
17
+ "@radix-ui/react-alert-dialog": "1.1.6",
18
+ "@radix-ui/react-aspect-ratio": "1.1.2",
19
+ "@radix-ui/react-avatar": "1.1.3",
20
+ "@radix-ui/react-checkbox": "1.1.4",
21
+ "@radix-ui/react-collapsible": "1.1.3",
22
+ "@radix-ui/react-context-menu": "2.2.6",
23
+ "@radix-ui/react-dialog": "1.1.6",
24
+ "@radix-ui/react-dropdown-menu": "2.1.6",
25
+ "@radix-ui/react-hover-card": "1.1.6",
26
+ "@radix-ui/react-label": "2.1.2",
27
+ "@radix-ui/react-menubar": "1.1.6",
28
+ "@radix-ui/react-navigation-menu": "1.2.5",
29
+ "@radix-ui/react-popover": "1.1.6",
30
+ "@radix-ui/react-progress": "1.1.2",
31
+ "@radix-ui/react-radio-group": "1.2.3",
32
+ "@radix-ui/react-scroll-area": "1.2.3",
33
+ "@radix-ui/react-select": "2.1.6",
34
+ "@radix-ui/react-separator": "1.1.2",
35
+ "@radix-ui/react-slider": "1.2.3",
36
+ "@radix-ui/react-slot": "1.1.2",
37
+ "@radix-ui/react-switch": "1.1.3",
38
+ "@radix-ui/react-tabs": "1.1.3",
39
+ "@radix-ui/react-toggle": "1.1.2",
40
+ "@radix-ui/react-toggle-group": "1.1.2",
41
+ "@radix-ui/react-tooltip": "1.1.8",
42
+ "canvas-confetti": "1.9.4",
43
+ "class-variance-authority": "0.7.1",
44
+ "clsx": "2.1.1",
45
+ "cmdk": "1.1.1",
46
+ "date-fns": "3.6.0",
47
+ "embla-carousel-react": "8.6.0",
48
+ "input-otp": "1.4.2",
49
+ "lucide-react": "0.487.0",
50
+ "motion": "12.23.24",
51
+ "next-themes": "0.4.6",
52
+ "react": "18.3.1",
53
+ "react-day-picker": "8.10.1",
54
+ "react-dnd": "16.0.1",
55
+ "react-dnd-html5-backend": "16.0.1",
56
+ "react-dom": "18.3.1",
57
+ "react-hook-form": "7.55.0",
58
+ "react-markdown": "^10.1.0",
59
+ "react-popper": "2.3.0",
60
+ "react-resizable-panels": "2.1.7",
61
+ "react-responsive-masonry": "2.7.1",
62
+ "react-router": "7.13.0",
63
+ "react-slick": "0.31.0",
64
+ "recharts": "2.15.2",
65
+ "sonner": "2.0.3",
66
+ "tailwind-merge": "3.2.0",
67
+ "tw-animate-css": "1.3.8",
68
+ "vaul": "1.1.2"
69
+ },
70
+ "devDependencies": {
71
+ "@tailwindcss/vite": "4.1.12",
72
+ "@vitejs/plugin-react": "4.7.0",
73
+ "tailwindcss": "4.1.12",
74
+ "vite": "6.3.5"
75
+ },
76
+ "peerDependenciesMeta": {},
77
+ "pnpm": {
78
+ "overrides": {
79
+ "vite": "6.3.5"
80
+ }
81
+ }
82
+ }
BPOM Compliance AI UI Redesign/pnpm-workspace.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ packages:
2
+ - '.'
BPOM Compliance AI UI Redesign/postcss.config.mjs ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * PostCSS Configuration
3
+ *
4
+ * Tailwind CSS v4 (via @tailwindcss/vite) automatically sets up all required
5
+ * PostCSS plugins — you do NOT need to include `tailwindcss` or `autoprefixer` here.
6
+ *
7
+ * This file only exists for adding additional PostCSS plugins, if needed.
8
+ * For example:
9
+ *
10
+ * import postcssNested from 'postcss-nested'
11
+ * export default { plugins: [postcssNested()] }
12
+ *
13
+ * Otherwise, you can leave this file empty.
14
+ */
15
+ export default {}
BPOM Compliance AI UI Redesign/src/app/App.tsx ADDED
@@ -0,0 +1,380 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import { motion, AnimatePresence } from "motion/react";
3
+ import { ShieldCheckIcon, ZapIcon, HelpCircleIcon } from "lucide-react";
4
+ import { StepIndicator } from "./components/StepIndicator";
5
+ import { Step1Upload } from "./components/Step1Upload";
6
+ import { Step2Review } from "./components/Step2Review";
7
+ import { Step3Report } from "./components/Step3Report";
8
+
9
+ type Step = 1 | 2 | 3;
10
+
11
+ interface LoadingState {
12
+ active: boolean;
13
+ stage: number;
14
+ message: string;
15
+ }
16
+
17
+ // Types for real backend data
18
+ export interface ComplianceRow {
19
+ param: string;
20
+ found: string;
21
+ threshold: string;
22
+ status: "PASS" | "FAIL" | "MISSING";
23
+ dasarHukum: string;
24
+ regulation: string;
25
+ pasal: string;
26
+ }
27
+
28
+ export interface ViolationCard {
29
+ id: string;
30
+ namaParameter: string;
31
+ nilaiTemuan: string;
32
+ batasRegulasi: string;
33
+ rekomendasi: string;
34
+ severity: "high" | "medium" | "low";
35
+ }
36
+
37
+ export interface AnalysisSummary {
38
+ pass: number;
39
+ fail: number;
40
+ missing: number;
41
+ total: number;
42
+ compliancePct: number;
43
+ }
44
+
45
+ export interface AnalysisResult {
46
+ category: string;
47
+ extracted: Record<string, unknown>;
48
+ compliance: Record<string, unknown>;
49
+ rows: ComplianceRow[];
50
+ violationCards: ViolationCard[];
51
+ narration: string;
52
+ summary: AnalysisSummary;
53
+ }
54
+
55
+ const loadingStages = [
56
+ { message: "Memproses dokumen...", duration: 800 },
57
+ { message: "Mengidentifikasi kategori produk...", duration: 700 },
58
+ { message: "Menjalankan rule engine BPOM...", duration: 900 },
59
+ { message: "Menganalisis parameter uji...", duration: 1000 },
60
+ { message: "Menyusun narasi AI...", duration: 600 },
61
+ ];
62
+
63
+ const API_BASE = "/api";
64
+
65
+ export default function App() {
66
+ const [step, setStep] = useState<Step>(1);
67
+ const [loading, setLoading] = useState<LoadingState>({
68
+ active: false,
69
+ stage: 0,
70
+ message: "",
71
+ });
72
+ const [direction, setDirection] = useState<1 | -1>(1);
73
+ const [analysisResult, setAnalysisResult] = useState<AnalysisResult | null>(null);
74
+ const [narration, setNarration] = useState("");
75
+ const [error, setError] = useState<string | null>(null);
76
+
77
+ const handleAnalyze = async (file: File | null, text: string) => {
78
+ setError(null);
79
+ setLoading({ active: true, stage: 0, message: loadingStages[0].message });
80
+
81
+ // Animate loading stages
82
+ let stageIdx = 0;
83
+ const stageInterval = setInterval(() => {
84
+ stageIdx = Math.min(stageIdx + 1, loadingStages.length - 1);
85
+ setLoading((prev) => ({
86
+ ...prev,
87
+ stage: stageIdx,
88
+ message: loadingStages[stageIdx].message,
89
+ }));
90
+ }, 800);
91
+
92
+ try {
93
+ const formData = new FormData();
94
+ if (file) formData.append("file", file);
95
+ if (text) formData.append("text", text);
96
+
97
+ const res = await fetch(`${API_BASE}/analyze`, {
98
+ method: "POST",
99
+ body: formData,
100
+ });
101
+
102
+ clearInterval(stageInterval);
103
+
104
+ if (!res.ok) {
105
+ const err = await res.json().catch(() => ({ detail: "Terjadi kesalahan." }));
106
+ throw new Error(err.detail || "Analisis gagal.");
107
+ }
108
+
109
+ const data: AnalysisResult = await res.json();
110
+
111
+ // Efek transisi penyelesaian: fast-forward checklist yang tersisa
112
+ const finishSequence = async () => {
113
+ let current = stageIdx;
114
+ while (current < loadingStages.length - 1) {
115
+ current++;
116
+ setLoading((prev) => ({
117
+ ...prev,
118
+ stage: current,
119
+ message: loadingStages[current].message,
120
+ }));
121
+ await new Promise((r) => setTimeout(r, 500)); // Animasi centang cepat
122
+ }
123
+
124
+ // Memaksa state menjadi fully checked (stage === length)
125
+ setLoading((prev) => ({
126
+ ...prev,
127
+ stage: loadingStages.length,
128
+ message: "Selesai!",
129
+ }));
130
+ await new Promise((r) => setTimeout(r, 1000)); // Tahan sebentar di 100% biar terbaca
131
+
132
+ setAnalysisResult(data);
133
+ setNarration(data.narration);
134
+ setLoading({ active: false, stage: 0, message: "" });
135
+ setDirection(1);
136
+ setStep(2);
137
+ };
138
+
139
+ await finishSequence();
140
+ } catch (err: unknown) {
141
+ clearInterval(stageInterval);
142
+ setLoading({ active: false, stage: 0, message: "" });
143
+ const message = err instanceof Error ? err.message : "Terjadi kesalahan tidak diketahui.";
144
+ setError(message);
145
+ }
146
+ };
147
+
148
+ const handleCategoryChange = async (newCategory: string) => {
149
+ if (!analysisResult) return;
150
+ setError(null);
151
+
152
+ try {
153
+ const formData = new FormData();
154
+ formData.append("text", JSON.stringify(analysisResult.extracted));
155
+ formData.append("category_override", newCategory);
156
+
157
+ const res = await fetch(`${API_BASE}/analyze`, {
158
+ method: "POST",
159
+ body: formData,
160
+ });
161
+
162
+ if (!res.ok) return;
163
+ const data: AnalysisResult = await res.json();
164
+ setAnalysisResult(data);
165
+ setNarration(data.narration);
166
+ } catch (err) {
167
+ console.error("Category change failed:", err);
168
+ }
169
+ };
170
+
171
+ const handleApprove = async () => {
172
+ if (!analysisResult) return;
173
+ setDirection(1);
174
+ setStep(3);
175
+ };
176
+
177
+ const handleReset = () => {
178
+ setDirection(-1);
179
+ setAnalysisResult(null);
180
+ setNarration("");
181
+ setError(null);
182
+ setTimeout(() => setStep(1), 10);
183
+ };
184
+
185
+ const variants = {
186
+ enter: (dir: number) => ({
187
+ opacity: 0,
188
+ x: dir > 0 ? 24 : -24,
189
+ }),
190
+ center: {
191
+ opacity: 1,
192
+ x: 0,
193
+ },
194
+ exit: (dir: number) => ({
195
+ opacity: 0,
196
+ x: dir > 0 ? -24 : 24,
197
+ }),
198
+ };
199
+
200
+ return (
201
+ <div className="min-h-screen bg-[#F8FAFC]">
202
+ {/* Top Nav */}
203
+ <header className="bg-[#1E3A5F] text-white px-6 py-3 flex items-center justify-between">
204
+ <div className="flex items-center gap-2.5">
205
+ <div className="w-7 h-7 rounded-lg bg-white/10 flex items-center justify-center">
206
+ <ShieldCheckIcon className="w-4 h-4 text-white" />
207
+ </div>
208
+ <div>
209
+ <p style={{ fontWeight: 700, fontSize: "13px", letterSpacing: "-0.01em" }}>BPOM Compliance AI</p>
210
+ <p className="text-blue-300 text-[10px]" style={{ fontWeight: 400 }}>Sistem Pengecekan Kepatuhan Pangan Otomatis</p>
211
+ </div>
212
+ </div>
213
+ <div className="flex items-center gap-3">
214
+ <div className="flex items-center gap-1.5 bg-white/10 px-2.5 py-1 rounded-lg">
215
+ <ZapIcon className="w-3 h-3 text-yellow-300" />
216
+ <span className="text-[11px] text-blue-100" style={{ fontWeight: 500 }}>AI Engine v2.4</span>
217
+ </div>
218
+ <button className="w-7 h-7 rounded-lg bg-white/10 flex items-center justify-center hover:bg-white/20 transition-colors">
219
+ <HelpCircleIcon className="w-4 h-4 text-blue-200" />
220
+ </button>
221
+ </div>
222
+ </header>
223
+
224
+ {/* Step Indicator */}
225
+ <StepIndicator currentStep={step} />
226
+
227
+ {/* Error Banner */}
228
+ <AnimatePresence>
229
+ {error && (
230
+ <motion.div
231
+ initial={{ opacity: 0, y: -8 }}
232
+ animate={{ opacity: 1, y: 0 }}
233
+ exit={{ opacity: 0, y: -8 }}
234
+ className="mx-auto max-w-6xl px-6 mt-4"
235
+ >
236
+ <div className="bg-red-50 border border-red-200 rounded-xl px-4 py-3 flex items-center gap-2 text-sm text-red-700">
237
+ <span style={{ fontWeight: 600 }}>⚠️ Error:</span> {error}
238
+ </div>
239
+ </motion.div>
240
+ )}
241
+ </AnimatePresence>
242
+
243
+ {/* Loading Overlay */}
244
+ <AnimatePresence>
245
+ {loading.active && (
246
+ <motion.div
247
+ initial={{ opacity: 0 }}
248
+ animate={{ opacity: 1 }}
249
+ exit={{ opacity: 0 }}
250
+ className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 backdrop-blur-sm"
251
+ >
252
+ <motion.div
253
+ initial={{ scale: 0.95, opacity: 0 }}
254
+ animate={{ scale: 1, opacity: 1 }}
255
+ exit={{ scale: 0.95, opacity: 0 }}
256
+ className="bg-white rounded-2xl p-8 shadow-2xl w-full max-w-sm mx-4"
257
+ >
258
+ <div className="flex flex-col items-center text-center">
259
+ <div className="w-14 h-14 rounded-2xl bg-[#EFF6FF] flex items-center justify-center mb-5">
260
+ <ShieldCheckIcon className="w-7 h-7 text-[#2563EB]" />
261
+ </div>
262
+ <p style={{ fontWeight: 700, fontSize: "16px", color: "#0F172A" }} className="mb-1.5">Menganalisis Dokumen</p>
263
+ <p className="text-sm text-slate-500 mb-6">{loading.message}</p>
264
+
265
+ <div className="w-full space-y-2 mb-5">
266
+ {loadingStages.map((stage, i) => (
267
+ <div key={i} className="flex items-center gap-2.5">
268
+ <div
269
+ className={`w-4 h-4 rounded-full flex items-center justify-center flex-shrink-0 transition-all duration-300 ${
270
+ i < loading.stage
271
+ ? "bg-[#16A34A]"
272
+ : i === loading.stage
273
+ ? "bg-[#2563EB]"
274
+ : "bg-slate-100"
275
+ }`}
276
+ >
277
+ {i < loading.stage ? (
278
+ <svg className="w-2 h-2 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
279
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
280
+ </svg>
281
+ ) : i === loading.stage ? (
282
+ <div className="w-1.5 h-1.5 rounded-full bg-white animate-pulse" />
283
+ ) : null}
284
+ </div>
285
+ <p
286
+ className={`text-xs transition-colors duration-300 ${
287
+ i < loading.stage
288
+ ? "text-[#16A34A]"
289
+ : i === loading.stage
290
+ ? "text-[#1E3A5F]"
291
+ : "text-slate-300"
292
+ }`}
293
+ style={{ fontWeight: i === loading.stage ? 600 : 400 }}
294
+ >
295
+ {stage.message}
296
+ </p>
297
+ </div>
298
+ ))}
299
+ </div>
300
+
301
+ <div className="w-full h-1.5 bg-slate-100 rounded-full overflow-hidden">
302
+ <motion.div
303
+ className="h-full bg-[#2563EB] rounded-full"
304
+ animate={{ width: `${((loading.stage + 1) / loadingStages.length) * 100}%` }}
305
+ transition={{ duration: 0.4 }}
306
+ />
307
+ </div>
308
+ <p className="text-[10px] text-slate-400 mt-2">
309
+ {loading.stage + 1} / {loadingStages.length}
310
+ </p>
311
+ </div>
312
+ </motion.div>
313
+ </motion.div>
314
+ )}
315
+ </AnimatePresence>
316
+
317
+ {/* Main Content */}
318
+ <main className="pb-12 overflow-hidden">
319
+ <AnimatePresence mode="wait" custom={direction}>
320
+ {step === 1 && (
321
+ <motion.div
322
+ key="step1"
323
+ custom={direction}
324
+ variants={variants}
325
+ initial="enter"
326
+ animate="center"
327
+ exit="exit"
328
+ transition={{ duration: 0.2, ease: "easeInOut" }}
329
+ >
330
+ <Step1Upload onAnalyze={handleAnalyze} isLoading={loading.active} />
331
+ </motion.div>
332
+ )}
333
+ {step === 2 && analysisResult && (
334
+ <motion.div
335
+ key="step2"
336
+ custom={direction}
337
+ variants={variants}
338
+ initial="enter"
339
+ animate="center"
340
+ exit="exit"
341
+ transition={{ duration: 0.2, ease: "easeInOut" }}
342
+ >
343
+ <Step2Review
344
+ rows={analysisResult.rows}
345
+ violationCards={analysisResult.violationCards}
346
+ summary={analysisResult.summary}
347
+ category={analysisResult.category}
348
+ narration={narration}
349
+ onNarrationChange={setNarration}
350
+ onCategoryChange={handleCategoryChange}
351
+ onApprove={handleApprove}
352
+ />
353
+ </motion.div>
354
+ )}
355
+ {step === 3 && analysisResult && (
356
+ <motion.div
357
+ key="step3"
358
+ custom={direction}
359
+ variants={variants}
360
+ initial="enter"
361
+ animate="center"
362
+ exit="exit"
363
+ transition={{ duration: 0.2, ease: "easeInOut" }}
364
+ >
365
+ <Step3Report
366
+ extracted={analysisResult.extracted}
367
+ category={analysisResult.category}
368
+ compliance={analysisResult.compliance}
369
+ summary={analysisResult.summary}
370
+ rows={analysisResult.rows}
371
+ narration={narration}
372
+ onReset={handleReset}
373
+ />
374
+ </motion.div>
375
+ )}
376
+ </AnimatePresence>
377
+ </main>
378
+ </div>
379
+ );
380
+ }
BPOM Compliance AI UI Redesign/src/app/components/Step1Upload.tsx ADDED
@@ -0,0 +1,221 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef, useCallback } from "react";
2
+ import { UploadCloudIcon, FileTextIcon, XIcon, AlertCircleIcon, Loader2Icon } from "lucide-react";
3
+
4
+ interface Step1Props {
5
+ onAnalyze: (file: File | null, text: string) => void;
6
+ isLoading: boolean;
7
+ }
8
+
9
+ export function Step1Upload({ onAnalyze, isLoading }: Step1Props) {
10
+ const [dragOver, setDragOver] = useState(false);
11
+ const [uploadedFile, setUploadedFile] = useState<File | null>(null);
12
+ const [manualText, setManualText] = useState("");
13
+ const [activeInput, setActiveInput] = useState<"file" | "text" | null>(null);
14
+ const fileInputRef = useRef<HTMLInputElement>(null);
15
+
16
+ const handleDrop = useCallback((e: React.DragEvent) => {
17
+ e.preventDefault();
18
+ setDragOver(false);
19
+ const file = e.dataTransfer.files[0];
20
+ if (file && (file.type === "application/pdf" || file.name.endsWith(".docx"))) {
21
+ setUploadedFile(file);
22
+ setActiveInput("file");
23
+ setManualText("");
24
+ }
25
+ }, []);
26
+
27
+ const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
28
+ const file = e.target.files?.[0];
29
+ if (file) {
30
+ setUploadedFile(file);
31
+ setActiveInput("file");
32
+ setManualText("");
33
+ }
34
+ };
35
+
36
+ const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
37
+ setManualText(e.target.value);
38
+ setActiveInput(e.target.value.length > 0 ? "text" : null);
39
+ if (e.target.value.length > 0) setUploadedFile(null);
40
+ };
41
+
42
+ const canAnalyze =
43
+ (activeInput === "file" && uploadedFile !== null) ||
44
+ (activeInput === "text" && manualText.trim().length > 50);
45
+
46
+ const formatFileSize = (bytes: number) => {
47
+ if (bytes < 1024) return `${bytes} B`;
48
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
49
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
50
+ };
51
+
52
+ const handleSubmit = () => {
53
+ if (!canAnalyze || isLoading) return;
54
+ onAnalyze(uploadedFile, manualText);
55
+ };
56
+
57
+ return (
58
+ <div className="max-w-6xl mx-auto px-6 py-8">
59
+ <div className="mb-6">
60
+ <h1 style={{ fontSize: "22px", fontWeight: 700, color: "#0F172A" }}>Input Dokumen</h1>
61
+ <p className="text-slate-500 mt-1 text-sm">
62
+ Upload laporan uji laboratorium produk pangan atau tempel teks secara manual untuk dianalisis.
63
+ </p>
64
+ </div>
65
+
66
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
67
+ {/* Upload Panel */}
68
+ <div
69
+ className={`bg-white rounded-xl border-2 transition-all duration-200 flex flex-col ${
70
+ dragOver
71
+ ? "border-[#2563EB] bg-blue-50 shadow-lg shadow-blue-100"
72
+ : uploadedFile
73
+ ? "border-[#16A34A] bg-green-50"
74
+ : activeInput === "text"
75
+ ? "border-slate-200 opacity-50 pointer-events-none"
76
+ : "border-dashed border-slate-200 hover:border-slate-300"
77
+ }`}
78
+ onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}
79
+ onDragLeave={() => setDragOver(false)}
80
+ onDrop={handleDrop}
81
+ >
82
+ <div className="p-6 flex-1">
83
+ <div className="flex items-center gap-2 mb-4">
84
+ <div className="w-8 h-8 rounded-lg bg-[#EFF6FF] flex items-center justify-center">
85
+ <UploadCloudIcon className="w-4 h-4 text-[#2563EB]" />
86
+ </div>
87
+ <div>
88
+ <p style={{ fontWeight: 600, fontSize: "14px", color: "#0F172A" }}>Upload Dokumen</p>
89
+ <p className="text-[11px] text-slate-400">Drag & drop atau klik untuk browse</p>
90
+ </div>
91
+ <div className="ml-auto flex gap-1.5">
92
+ <span className="px-2 py-0.5 rounded text-[10px] bg-red-50 text-red-600 border border-red-100" style={{ fontWeight: 600 }}>PDF</span>
93
+ <span className="px-2 py-0.5 rounded text-[10px] bg-blue-50 text-blue-600 border border-blue-100" style={{ fontWeight: 600 }}>DOCX</span>
94
+ </div>
95
+ </div>
96
+
97
+ {uploadedFile ? (
98
+ <div className="rounded-lg bg-white border border-[#16A34A]/30 p-3 flex items-center gap-3">
99
+ <div className="w-9 h-9 rounded-lg bg-green-50 flex items-center justify-center flex-shrink-0">
100
+ <FileTextIcon className="w-4 h-4 text-[#16A34A]" />
101
+ </div>
102
+ <div className="flex-1 min-w-0">
103
+ <p className="text-sm text-slate-800 truncate" style={{ fontWeight: 500 }}>{uploadedFile.name}</p>
104
+ <p className="text-[11px] text-slate-400">{formatFileSize(uploadedFile.size)}</p>
105
+ </div>
106
+ <button
107
+ onClick={() => { setUploadedFile(null); setActiveInput(null); }}
108
+ className="w-6 h-6 rounded flex items-center justify-center text-slate-400 hover:text-slate-600 hover:bg-slate-100 transition-colors"
109
+ >
110
+ <XIcon className="w-3.5 h-3.5" />
111
+ </button>
112
+ </div>
113
+ ) : (
114
+ <div
115
+ className="border-2 border-dashed border-slate-200 rounded-lg p-8 text-center cursor-pointer hover:border-[#2563EB] hover:bg-blue-50/50 transition-all duration-150"
116
+ onClick={() => fileInputRef.current?.click()}
117
+ >
118
+ <UploadCloudIcon className="w-8 h-8 text-slate-300 mx-auto mb-2" />
119
+ <p className="text-sm text-slate-500" style={{ fontWeight: 500 }}>Klik atau drag file ke sini</p>
120
+ <p className="text-[11px] text-slate-400 mt-1">Maksimal 25 MB per file</p>
121
+ </div>
122
+ )}
123
+
124
+ <input ref={fileInputRef} type="file" accept=".pdf,.docx" className="hidden" onChange={handleFileChange} />
125
+ </div>
126
+
127
+ {!uploadedFile && !dragOver && (
128
+ <div className="px-6 pb-5">
129
+ <div className="flex items-start gap-2 p-3 bg-amber-50 rounded-lg border border-amber-100">
130
+ <AlertCircleIcon className="w-3.5 h-3.5 text-amber-500 flex-shrink-0 mt-0.5" />
131
+ <p className="text-[11px] text-amber-700">
132
+ Pastikan dokumen berisi data uji laboratorium lengkap termasuk parameter, nilai, dan satuan ukuran.
133
+ </p>
134
+ </div>
135
+ </div>
136
+ )}
137
+ </div>
138
+
139
+ {/* Manual Text Panel */}
140
+ <div
141
+ className={`bg-white rounded-xl border transition-all duration-200 flex flex-col ${
142
+ activeInput === "file"
143
+ ? "border-slate-200 opacity-50 pointer-events-none"
144
+ : activeInput === "text"
145
+ ? "border-[#2563EB] shadow-sm shadow-blue-100"
146
+ : "border-slate-200 hover:border-slate-300"
147
+ }`}
148
+ >
149
+ <div className="p-6 flex-1 flex flex-col">
150
+ <div className="flex items-center gap-2 mb-4">
151
+ <div className="w-8 h-8 rounded-lg bg-slate-50 flex items-center justify-center">
152
+ <FileTextIcon className="w-4 h-4 text-slate-500" />
153
+ </div>
154
+ <div>
155
+ <p style={{ fontWeight: 600, fontSize: "14px", color: "#0F172A" }}>Input Teks Manual</p>
156
+ <p className="text-[11px] text-slate-400">Salin dan tempel data lab report</p>
157
+ </div>
158
+ {manualText.length > 0 && (
159
+ <span className="ml-auto text-[11px] text-slate-400 tabular-nums">
160
+ {manualText.length.toLocaleString()} karakter
161
+ </span>
162
+ )}
163
+ </div>
164
+
165
+ <textarea
166
+ value={manualText}
167
+ onChange={handleTextChange}
168
+ placeholder={`Tempel teks laporan uji lab di sini...\n\nContoh:\nNama Produk: Biskuit Gandum Premium\nProdusen: PT. Agro Pangan Nusantara\n\nParameter Uji:\n- Kadar Air: 4,2% (SNI maks. 5%)\n- Protein: 8,1 g/100g\n- Lemak Total: 18,3 g/100g\n...`}
169
+ className="flex-1 resize-none text-sm text-slate-700 placeholder:text-slate-300 outline-none w-full min-h-[220px] leading-relaxed"
170
+ style={{ fontFamily: "'Inter', sans-serif", fontWeight: 400 }}
171
+ />
172
+
173
+ {manualText.length > 0 && manualText.length < 50 && (
174
+ <p className="text-[11px] text-amber-600 mt-2">
175
+ Minimal 50 karakter diperlukan untuk analisis yang akurat.
176
+ </p>
177
+ )}
178
+ </div>
179
+ </div>
180
+ </div>
181
+
182
+ {/* Divider */}
183
+ <div className="flex items-center gap-3 my-5">
184
+ <div className="flex-1 h-px bg-slate-200" />
185
+ <span className="text-[11px] text-slate-400 uppercase tracking-wider" style={{ fontWeight: 600 }}>atau pilih salah satu</span>
186
+ <div className="flex-1 h-px bg-slate-200" />
187
+ </div>
188
+
189
+ {/* CTA */}
190
+ <button
191
+ onClick={handleSubmit}
192
+ disabled={!canAnalyze || isLoading}
193
+ id="analyze-btn"
194
+ className={`w-full flex items-center justify-center gap-2.5 py-3.5 rounded-xl text-sm transition-all duration-200 ${
195
+ canAnalyze && !isLoading
196
+ ? "bg-[#1E3A5F] hover:bg-[#162d4a] text-white shadow-md hover:shadow-lg cursor-pointer"
197
+ : "bg-slate-100 text-slate-400 cursor-not-allowed"
198
+ }`}
199
+ style={{ fontWeight: 600, fontSize: "14px" }}
200
+ >
201
+ {isLoading ? (
202
+ <>
203
+ <Loader2Icon className="w-4 h-4 animate-spin" />
204
+ Menganalisis dokumen...
205
+ </>
206
+ ) : (
207
+ <>
208
+ Analisis Dokumen
209
+ <span className="text-xs opacity-70">→</span>
210
+ </>
211
+ )}
212
+ </button>
213
+
214
+ {!canAnalyze && !isLoading && (
215
+ <p className="text-center text-[11px] text-slate-400 mt-2">
216
+ Upload dokumen atau tempel teks terlebih dahulu untuk melanjutkan.
217
+ </p>
218
+ )}
219
+ </div>
220
+ );
221
+ }
BPOM Compliance AI UI Redesign/src/app/components/Step2Review.tsx ADDED
@@ -0,0 +1,414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import ReactMarkdown from "react-markdown";
3
+ import {
4
+ CheckCircle2Icon,
5
+ XCircleIcon,
6
+ AlertTriangleIcon,
7
+ ChevronDownIcon,
8
+ ChevronUpIcon,
9
+ InfoIcon,
10
+ EditIcon,
11
+ CheckIcon,
12
+ } from "lucide-react";
13
+ import type { ComplianceRow, ViolationCard, AnalysisSummary } from "../App";
14
+
15
+ const CATEGORIES = ["SUPLEMEN", "DAIRY", "DAGING_OLAHAN", "BUAH_SAYUR"];
16
+
17
+ interface Step2Props {
18
+ rows: ComplianceRow[];
19
+ violationCards: ViolationCard[];
20
+ summary: AnalysisSummary;
21
+ category: string;
22
+ narration: string;
23
+ onNarrationChange: (val: string) => void;
24
+ onCategoryChange: (cat: string) => void;
25
+ onApprove: () => void;
26
+ }
27
+
28
+ export function Step2Review({
29
+ rows,
30
+ violationCards,
31
+ summary,
32
+ category,
33
+ narration,
34
+ onNarrationChange,
35
+ onCategoryChange,
36
+ onApprove,
37
+ }: Step2Props) {
38
+ const [selectedCategory, setSelectedCategory] = useState(category);
39
+ const [editingCategory, setEditingCategory] = useState(false);
40
+ const [expandedViolations, setExpandedViolations] = useState<Set<string>>(
41
+ new Set(violationCards.length > 0 ? [violationCards[0].id] : [])
42
+ );
43
+ const [reviewed, setReviewed] = useState(false);
44
+
45
+ const [previewMode, setPreviewMode] = useState(true);
46
+
47
+ const toggleViolation = (id: string) => {
48
+ setExpandedViolations((prev) => {
49
+ const next = new Set(prev);
50
+ if (next.has(id)) next.delete(id);
51
+ else next.add(id);
52
+ return next;
53
+ });
54
+ };
55
+
56
+ const handleCategoryConfirm = () => {
57
+ setEditingCategory(false);
58
+ onCategoryChange(selectedCategory);
59
+ };
60
+
61
+ const severityColors = {
62
+ high: "bg-red-50 border-red-200 text-red-700",
63
+ medium: "bg-amber-50 border-amber-200 text-amber-700",
64
+ low: "bg-blue-50 border-blue-200 text-blue-600",
65
+ };
66
+
67
+ const severityLabels = {
68
+ high: "Kritikal",
69
+ medium: "Sedang",
70
+ low: "Perlu Data",
71
+ };
72
+
73
+ const markdownComponents = {
74
+ h1: ({node, ...props}: any) => <h1 className="text-lg font-bold mt-2 mb-2 text-slate-900" {...props}/>,
75
+ h2: ({node, ...props}: any) => <h2 className="text-base font-bold mt-2 mb-2 text-slate-900" {...props}/>,
76
+ h3: ({node, ...props}: any) => <h3 className="text-sm font-bold mt-2 mb-1 text-slate-900" {...props}/>,
77
+ p: ({node, ...props}: any) => <p className="mb-2 last:mb-0 leading-relaxed" {...props}/>,
78
+ strong: ({node, ...props}: any) => <strong className="font-bold text-slate-900" {...props}/>,
79
+ ul: ({node, ...props}: any) => <ul className="list-disc pl-5 mb-2 space-y-1" {...props}/>,
80
+ ol: ({node, ...props}: any) => <ol className="list-decimal pl-5 mb-2 space-y-1" {...props}/>,
81
+ li: ({node, ...props}: any) => <li className="" {...props}/>,
82
+ };
83
+
84
+ return (
85
+ <div className="max-w-6xl mx-auto px-6 py-8">
86
+ {/* Header */}
87
+ <div className="flex items-start justify-between mb-5 flex-wrap gap-3">
88
+ <div>
89
+ <h1 style={{ fontSize: "22px", fontWeight: 700, color: "#0F172A" }}>Review Hasil AI</h1>
90
+ <p className="text-slate-500 mt-1 text-sm">Periksa hasil analisis kepatuhan dan narasi AI sebelum membuat laporan final.</p>
91
+ </div>
92
+ <div className="flex items-center gap-2">
93
+ <span className="text-xs text-slate-400" style={{ fontWeight: 500 }}>Kategori Produk:</span>
94
+ {editingCategory ? (
95
+ <div className="flex items-center gap-1.5">
96
+ <select
97
+ value={selectedCategory}
98
+ onChange={(e) => setSelectedCategory(e.target.value)}
99
+ className="text-xs border border-[#2563EB] rounded-lg px-2.5 py-1.5 text-[#1E3A5F] bg-blue-50 outline-none"
100
+ style={{ fontWeight: 600 }}
101
+ >
102
+ {CATEGORIES.map((c) => <option key={c}>{c}</option>)}
103
+ </select>
104
+ <button
105
+ onClick={handleCategoryConfirm}
106
+ className="w-6 h-6 rounded-full bg-[#16A34A] flex items-center justify-center"
107
+ >
108
+ <CheckIcon className="w-3 h-3 text-white" />
109
+ </button>
110
+ </div>
111
+ ) : (
112
+ <button
113
+ onClick={() => setEditingCategory(true)}
114
+ className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-[#EFF6FF] border border-blue-100 text-[#1E3A5F] text-xs hover:bg-blue-100 transition-colors group"
115
+ style={{ fontWeight: 600 }}
116
+ >
117
+ {selectedCategory}
118
+ <EditIcon className="w-3 h-3 text-[#2563EB] opacity-0 group-hover:opacity-100 transition-opacity" />
119
+ </button>
120
+ )}
121
+ </div>
122
+ </div>
123
+
124
+ {/* Summary Cards */}
125
+ <div className="grid grid-cols-3 gap-3 mb-5">
126
+ <div className="bg-white rounded-xl border border-slate-200 p-4 flex items-center gap-3">
127
+ <div className="w-10 h-10 rounded-xl bg-green-50 flex items-center justify-center flex-shrink-0">
128
+ <CheckCircle2Icon className="w-5 h-5 text-[#16A34A]" />
129
+ </div>
130
+ <div>
131
+ <p className="text-2xl text-[#16A34A]" style={{ fontWeight: 700, lineHeight: 1.1 }}>{summary.pass}</p>
132
+ <p className="text-xs text-slate-500 mt-0.5" style={{ fontWeight: 500 }}>Parameter PASS</p>
133
+ </div>
134
+ <div className="ml-auto">
135
+ <div className="text-xs text-green-600 bg-green-50 px-2 py-0.5 rounded-full border border-green-100" style={{ fontWeight: 600 }}>
136
+ {summary.total > 0 ? Math.round((summary.pass / summary.total) * 100) : 0}%
137
+ </div>
138
+ </div>
139
+ </div>
140
+
141
+ <div className="bg-white rounded-xl border border-slate-200 p-4 flex items-center gap-3">
142
+ <div className="w-10 h-10 rounded-xl bg-red-50 flex items-center justify-center flex-shrink-0">
143
+ <XCircleIcon className="w-5 h-5 text-[#DC2626]" />
144
+ </div>
145
+ <div>
146
+ <p className="text-2xl text-[#DC2626]" style={{ fontWeight: 700, lineHeight: 1.1 }}>{summary.fail}</p>
147
+ <p className="text-xs text-slate-500 mt-0.5" style={{ fontWeight: 500 }}>Parameter FAIL</p>
148
+ </div>
149
+ <div className="ml-auto">
150
+ <div className="text-xs text-red-600 bg-red-50 px-2 py-0.5 rounded-full border border-red-100" style={{ fontWeight: 600 }}>
151
+ {summary.total > 0 ? Math.round((summary.fail / summary.total) * 100) : 0}%
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ <div className="bg-white rounded-xl border border-slate-200 p-4 flex items-center gap-3">
157
+ <div className="w-10 h-10 rounded-xl bg-amber-50 flex items-center justify-center flex-shrink-0">
158
+ <AlertTriangleIcon className="w-5 h-5 text-[#D97706]" />
159
+ </div>
160
+ <div>
161
+ <p className="text-2xl text-[#D97706]" style={{ fontWeight: 700, lineHeight: 1.1 }}>{summary.missing}</p>
162
+ <p className="text-xs text-slate-500 mt-0.5" style={{ fontWeight: 500 }}>Data Tidak Lengkap</p>
163
+ </div>
164
+ <div className="ml-auto">
165
+ <div className="text-xs text-amber-600 bg-amber-50 px-2 py-0.5 rounded-full border border-amber-100" style={{ fontWeight: 600 }}>
166
+ {summary.total > 0 ? Math.round((summary.missing / summary.total) * 100) : 0}%
167
+ </div>
168
+ </div>
169
+ </div>
170
+ </div>
171
+
172
+ {/* Progress Bar */}
173
+ <div className="bg-white rounded-xl border border-slate-200 p-4 mb-5">
174
+ <div className="flex items-center justify-between mb-2">
175
+ <p className="text-xs text-slate-500" style={{ fontWeight: 500 }}>Tingkat Kepatuhan Keseluruhan</p>
176
+ <p className="text-xs text-slate-700" style={{ fontWeight: 600 }}>{summary.pass} / {summary.total} parameter</p>
177
+ </div>
178
+ <div className="h-2 bg-slate-100 rounded-full overflow-hidden flex">
179
+ <div className="bg-[#16A34A] h-full transition-all" style={{ width: `${summary.total > 0 ? (summary.pass / summary.total) * 100 : 0}%` }} />
180
+ <div className="bg-[#DC2626] h-full transition-all" style={{ width: `${summary.total > 0 ? (summary.fail / summary.total) * 100 : 0}%` }} />
181
+ <div className="bg-[#D97706] h-full transition-all" style={{ width: `${summary.total > 0 ? (summary.missing / summary.total) * 100 : 0}%` }} />
182
+ </div>
183
+ <div className="flex items-center gap-4 mt-2">
184
+ <span className="flex items-center gap-1 text-[10px] text-slate-500"><span className="w-2 h-2 rounded-full bg-[#16A34A] inline-block" />PASS</span>
185
+ <span className="flex items-center gap-1 text-[10px] text-slate-500"><span className="w-2 h-2 rounded-full bg-[#DC2626] inline-block" />FAIL</span>
186
+ <span className="flex items-center gap-1 text-[10px] text-slate-500"><span className="w-2 h-2 rounded-full bg-[#D97706] inline-block" />MISSING</span>
187
+ </div>
188
+ </div>
189
+
190
+ {/* Compliance Table */}
191
+ <div className="bg-white rounded-xl border border-slate-200 mb-5 overflow-hidden">
192
+ <div className="px-5 py-3.5 border-b border-slate-100 flex items-center gap-2">
193
+ <p style={{ fontWeight: 600, fontSize: "13px", color: "#0F172A" }}>Tabel Hasil Uji Kepatuhan</p>
194
+ <span className="text-[10px] text-slate-400 bg-slate-100 px-2 py-0.5 rounded-full" style={{ fontWeight: 500 }}>
195
+ {rows.length} parameter
196
+ </span>
197
+ </div>
198
+
199
+ <div className="overflow-x-auto">
200
+ <table className="w-full text-xs">
201
+ <thead>
202
+ <tr className="bg-slate-50 border-b border-slate-100">
203
+ <th className="text-left px-4 py-2.5 text-slate-500 w-[200px]" style={{ fontWeight: 600, fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.04em" }}>Parameter</th>
204
+ <th className="text-right px-4 py-2.5 text-slate-500" style={{ fontWeight: 600, fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.04em" }}>Nilai Uji</th>
205
+ <th className="text-left px-4 py-2.5 text-slate-500" style={{ fontWeight: 600, fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.04em" }}>Batas BPOM</th>
206
+ <th className="text-center px-4 py-2.5 text-slate-500 w-[80px]" style={{ fontWeight: 600, fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.04em" }}>Status</th>
207
+ <th className="text-left px-4 py-2.5 text-slate-500" style={{ fontWeight: 600, fontSize: "11px", textTransform: "uppercase", letterSpacing: "0.04em" }}>Dasar Hukum</th>
208
+ </tr>
209
+ </thead>
210
+ <tbody>
211
+ {rows.map((row, i) => (
212
+ <tr
213
+ key={i}
214
+ className={`border-b border-slate-50 transition-colors ${
215
+ row.status === "FAIL"
216
+ ? "bg-red-50/60 hover:bg-red-50"
217
+ : row.status === "MISSING"
218
+ ? "bg-amber-50/40 hover:bg-amber-50"
219
+ : i % 2 === 0
220
+ ? "bg-white hover:bg-slate-50/80"
221
+ : "bg-slate-50/30 hover:bg-slate-50/80"
222
+ }`}
223
+ style={{
224
+ borderLeft: row.status === "FAIL" ? "3px solid #DC2626" : row.status === "PASS" ? "3px solid #16A34A" : "3px solid #D97706",
225
+ }}
226
+ >
227
+ <td className="px-4 py-2.5 text-slate-800" style={{ fontWeight: 500 }}>{row.param}</td>
228
+ <td className="px-4 py-2.5 text-right tabular-nums" style={{ fontFamily: "'JetBrains Mono', monospace", fontWeight: row.status === "FAIL" ? 600 : 400, color: row.status === "FAIL" ? "#DC2626" : "#374151" }}>
229
+ {row.found !== "-" ? row.found : "—"}
230
+ </td>
231
+ <td className="px-4 py-2.5 text-slate-600" style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: "11px" }}>{row.threshold}</td>
232
+ <td className="px-4 py-2.5 text-center">
233
+ {row.status === "PASS" && (
234
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-100 text-green-700" style={{ fontWeight: 700, fontSize: "10px" }}>
235
+ <CheckCircle2Icon className="w-2.5 h-2.5" />PASS
236
+ </span>
237
+ )}
238
+ {row.status === "FAIL" && (
239
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-100 text-red-700" style={{ fontWeight: 700, fontSize: "10px" }}>
240
+ <XCircleIcon className="w-2.5 h-2.5" />FAIL
241
+ </span>
242
+ )}
243
+ {row.status === "MISSING" && (
244
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-100 text-amber-700" style={{ fontWeight: 700, fontSize: "10px" }}>
245
+ <AlertTriangleIcon className="w-2.5 h-2.5" />N/A
246
+ </span>
247
+ )}
248
+ </td>
249
+ <td className="px-4 py-2.5">
250
+ <div className="relative group">
251
+ <span className="text-[#2563EB] cursor-help underline decoration-dotted underline-offset-2 truncate block max-w-[180px]" style={{ fontWeight: 500, fontSize: "11px" }}>
252
+ {row.dasarHukum}
253
+ </span>
254
+ {row.dasarHukum && row.dasarHukum !== "-" && (
255
+ <div className="absolute bottom-full left-0 mb-2 hidden group-hover:block z-50 w-72 p-3 bg-[#1E3A5F] text-white rounded-lg shadow-xl text-[11px] leading-relaxed pointer-events-none">
256
+ <div className="flex items-start gap-1.5">
257
+ <InfoIcon className="w-3 h-3 flex-shrink-0 mt-0.5 opacity-70" />
258
+ <span>{row.dasarHukum}</span>
259
+ </div>
260
+ <div className="absolute top-full left-3 w-0 h-0 border-x-4 border-x-transparent border-t-4 border-t-[#1E3A5F]" />
261
+ </div>
262
+ )}
263
+ </div>
264
+ </td>
265
+ </tr>
266
+ ))}
267
+ </tbody>
268
+ </table>
269
+ </div>
270
+ </div>
271
+
272
+ {/* AI Narration & Violation Cards */}
273
+ {violationCards.length > 0 && (
274
+ <div className="bg-white rounded-xl border border-slate-200 mb-5 overflow-hidden">
275
+ <div className="px-5 py-3.5 border-b border-slate-100 flex items-center justify-between">
276
+ <div className="flex items-center gap-2">
277
+ <p style={{ fontWeight: 600, fontSize: "13px", color: "#0F172A" }}>Penjelasan AI — Temuan Pelanggaran</p>
278
+ <span className="text-[10px] text-slate-400 bg-slate-100 px-2 py-0.5 rounded-full" style={{ fontWeight: 500 }}>
279
+ {violationCards.length} temuan
280
+ </span>
281
+ </div>
282
+ <button
283
+ onClick={() => setExpandedViolations(
284
+ expandedViolations.size === violationCards.length
285
+ ? new Set()
286
+ : new Set(violationCards.map((v) => v.id))
287
+ )}
288
+ className="text-[11px] text-[#2563EB] hover:underline"
289
+ style={{ fontWeight: 500 }}
290
+ >
291
+ {expandedViolations.size === violationCards.length ? "Tutup semua" : "Buka semua"}
292
+ </button>
293
+ </div>
294
+
295
+ <div className="divide-y divide-slate-100">
296
+ {violationCards.map((v) => (
297
+ <div key={v.id} className="px-5">
298
+ <button
299
+ onClick={() => toggleViolation(v.id)}
300
+ className="w-full flex items-center gap-3 py-3.5 text-left hover:bg-slate-50 -mx-5 px-5 transition-colors"
301
+ >
302
+ <span className={`px-2 py-0.5 rounded text-[10px] border flex-shrink-0 ${severityColors[v.severity]}`} style={{ fontWeight: 700 }}>
303
+ {severityLabels[v.severity]}
304
+ </span>
305
+ <span className="text-sm text-slate-800 flex-1" style={{ fontWeight: 600 }}>{v.namaParameter}</span>
306
+ <span className="text-[11px] text-slate-400 tabular-nums mr-3" style={{ fontFamily: "'JetBrains Mono', monospace" }}>
307
+ {v.nilaiTemuan}
308
+ </span>
309
+ {expandedViolations.has(v.id)
310
+ ? <ChevronUpIcon className="w-4 h-4 text-slate-400 flex-shrink-0" />
311
+ : <ChevronDownIcon className="w-4 h-4 text-slate-400 flex-shrink-0" />
312
+ }
313
+ </button>
314
+
315
+ {expandedViolations.has(v.id) && (
316
+ <div className="pb-4 grid grid-cols-1 md:grid-cols-2 gap-3">
317
+ <div className="rounded-lg bg-slate-50 border border-slate-100 p-3.5">
318
+ <p className="text-[10px] text-slate-400 uppercase tracking-wider mb-2" style={{ fontWeight: 600 }}>Perbandingan Nilai</p>
319
+ <div className="flex items-start gap-3">
320
+ <div>
321
+ <p className="text-[10px] text-slate-500" style={{ fontWeight: 500 }}>Nilai Temuan</p>
322
+ <p className="text-sm text-[#DC2626] tabular-nums" style={{ fontWeight: 700, fontFamily: "'JetBrains Mono', monospace" }}>{v.nilaiTemuan}</p>
323
+ </div>
324
+ <div className="text-slate-300 self-center">→</div>
325
+ <div>
326
+ <p className="text-[10px] text-slate-500" style={{ fontWeight: 500 }}>Batas Regulasi</p>
327
+ <p className="text-sm text-[#16A34A] tabular-nums" style={{ fontWeight: 700, fontFamily: "'JetBrains Mono', monospace" }}>
328
+ {v.batasRegulasi.split("(")[0].trim()}
329
+ </p>
330
+ </div>
331
+ </div>
332
+ <p className="text-[10px] text-slate-400 mt-2">{v.batasRegulasi.match(/\(.*\)/)?.[0] || ""}</p>
333
+ </div>
334
+
335
+ <div className="rounded-lg bg-blue-50/50 border border-blue-100 p-3.5">
336
+ <p className="text-[10px] text-[#1E3A5F] uppercase tracking-wider mb-2" style={{ fontWeight: 600 }}>Rekomendasi</p>
337
+ <p className="text-xs text-slate-600 leading-relaxed italic">{v.rekomendasi}</p>
338
+ </div>
339
+ </div>
340
+ )}
341
+ </div>
342
+ ))}
343
+ </div>
344
+ </div>
345
+ )}
346
+
347
+ {/* Editable Narration */}
348
+ <div className="bg-white rounded-xl border border-slate-200 p-5 mb-5">
349
+ <div className="flex items-center justify-between mb-3">
350
+ <p className="text-xs text-slate-500 uppercase tracking-wider" style={{ fontWeight: 600 }}>Narasi AI — Dapat Diedit</p>
351
+ <div className="flex items-center gap-1 bg-slate-100 p-0.5 rounded-lg">
352
+ <button
353
+ onClick={() => setPreviewMode(true)}
354
+ className={`px-3 py-1.5 text-[11px] rounded-md transition-colors ${previewMode ? "bg-white text-slate-800 shadow-sm font-semibold" : "text-slate-500 font-medium hover:text-slate-700"}`}
355
+ >
356
+ Preview
357
+ </button>
358
+ <button
359
+ onClick={() => setPreviewMode(false)}
360
+ className={`px-3 py-1.5 text-[11px] rounded-md transition-colors ${!previewMode ? "bg-white text-slate-800 shadow-sm font-semibold" : "text-slate-500 font-medium hover:text-slate-700"}`}
361
+ >
362
+ Edit Teks
363
+ </button>
364
+ </div>
365
+ </div>
366
+
367
+ {previewMode ? (
368
+ <div className="w-full text-sm text-slate-700 border border-slate-200 rounded-lg p-4 bg-slate-50/50 min-h-[144px]">
369
+ <ReactMarkdown components={markdownComponents}>
370
+ {narration || "*Narasi kosong*"}
371
+ </ReactMarkdown>
372
+ </div>
373
+ ) : (
374
+ <textarea
375
+ value={narration}
376
+ onChange={(e) => onNarrationChange(e.target.value)}
377
+ rows={6}
378
+ className="w-full text-sm text-slate-700 border border-[#2563EB] rounded-lg p-3 resize-none outline-none ring-2 ring-blue-100 transition-all leading-relaxed bg-white"
379
+ style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: "12px" }}
380
+ />
381
+ )}
382
+ </div>
383
+
384
+ {/* Review CTA */}
385
+ <div className="bg-white rounded-xl border border-slate-200 p-5 flex items-center gap-4 flex-wrap">
386
+ <div className="flex items-start gap-3 flex-1">
387
+ <input
388
+ type="checkbox"
389
+ id="reviewed"
390
+ checked={reviewed}
391
+ onChange={(e) => setReviewed(e.target.checked)}
392
+ className="mt-0.5 w-4 h-4 accent-[#1E3A5F] cursor-pointer"
393
+ />
394
+ <label htmlFor="reviewed" className="text-sm text-slate-700 cursor-pointer leading-relaxed" style={{ fontWeight: 500 }}>
395
+ Saya telah memeriksa seluruh hasil analisis dan menyetujui narasi AI untuk dimasukkan ke dalam laporan final.
396
+ </label>
397
+ </div>
398
+ <button
399
+ onClick={onApprove}
400
+ disabled={!reviewed}
401
+ className={`flex items-center gap-2 px-6 py-3 rounded-xl text-sm transition-all duration-200 flex-shrink-0 ${
402
+ reviewed
403
+ ? "bg-[#1E3A5F] hover:bg-[#162d4a] text-white shadow-md cursor-pointer"
404
+ : "bg-slate-100 text-slate-400 cursor-not-allowed"
405
+ }`}
406
+ style={{ fontWeight: 600 }}
407
+ >
408
+ Setuju & Buat Laporan Final
409
+ <span className="opacity-70">→</span>
410
+ </button>
411
+ </div>
412
+ </div>
413
+ );
414
+ }
BPOM Compliance AI UI Redesign/src/app/components/Step3Report.tsx ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from "react";
2
+ import ReactMarkdown from "react-markdown";
3
+ import {
4
+ DownloadIcon,
5
+ FileTextIcon,
6
+ CheckCircle2Icon,
7
+ XCircleIcon,
8
+ AlertTriangleIcon,
9
+ PrinterIcon,
10
+ ShieldCheckIcon,
11
+ CalendarIcon,
12
+ } from "lucide-react";
13
+ import type { ComplianceRow, AnalysisSummary } from "../App";
14
+
15
+ interface Step3Props {
16
+ extracted: Record<string, unknown>;
17
+ category: string;
18
+ compliance: Record<string, unknown>;
19
+ summary: AnalysisSummary;
20
+ rows: ComplianceRow[];
21
+ narration: string;
22
+ onReset: () => void;
23
+ }
24
+
25
+ const API_BASE = "/api";
26
+
27
+ export function Step3Report({
28
+ extracted,
29
+ category,
30
+ compliance,
31
+ summary,
32
+ rows,
33
+ narration,
34
+ onReset,
35
+ }: Step3Props) {
36
+ const [downloading, setDownloading] = useState(false);
37
+ const [generating, setGenerating] = useState(false);
38
+ const [reportReady, setReportReady] = useState(false);
39
+ const [markdownPreview, setMarkdownPreview] = useState<string | null>(null);
40
+ const [error, setError] = useState<string | null>(null);
41
+
42
+ const productName = (extracted?.nama_produk || extracted?.product_name || "Produk Tidak Diketahui") as string;
43
+ const producer = (extracted?.produsen || extracted?.manufacturer || "-") as string;
44
+ const batchNo = (extracted?.batch || extracted?.nomor_batch || "-") as string;
45
+ const testDate = (extracted?.tanggal_uji || extracted?.test_date || "-") as string;
46
+ const labName = (extracted?.laboratorium || extracted?.lab || "-") as string;
47
+ const today = new Date().toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" });
48
+
49
+ const handleGenerateReport = async () => {
50
+ setGenerating(true);
51
+ setError(null);
52
+ try {
53
+ const res = await fetch(`${API_BASE}/report`, {
54
+ method: "POST",
55
+ headers: { "Content-Type": "application/json" },
56
+ body: JSON.stringify({ extracted, category, compliance, narration }),
57
+ });
58
+
59
+ if (!res.ok) {
60
+ const err = await res.json().catch(() => ({ detail: "Gagal generate report." }));
61
+ throw new Error(err.detail);
62
+ }
63
+
64
+ const data = await res.json();
65
+ setMarkdownPreview(data.markdown);
66
+ setReportReady(data.pdfReady);
67
+ } catch (err: unknown) {
68
+ setError(err instanceof Error ? err.message : "Terjadi kesalahan.");
69
+ } finally {
70
+ setGenerating(false);
71
+ }
72
+ };
73
+
74
+ const handleDownload = async () => {
75
+ setDownloading(true);
76
+ try {
77
+ const res = await fetch(`${API_BASE}/download`);
78
+ if (!res.ok) throw new Error("PDF tidak tersedia.");
79
+ const blob = await res.blob();
80
+ const url = URL.createObjectURL(blob);
81
+ const a = document.createElement("a");
82
+ a.href = url;
83
+ a.download = "laporan_compliance_bpom.pdf";
84
+ a.click();
85
+ URL.revokeObjectURL(url);
86
+ } catch (err) {
87
+ console.error("Download failed:", err);
88
+ } finally {
89
+ setTimeout(() => setDownloading(false), 1000);
90
+ }
91
+ };
92
+
93
+ const handlePrint = () => window.print();
94
+
95
+ const StatusBadge = ({ status }: { status: "PASS" | "FAIL" | "MISSING" }) => {
96
+ if (status === "PASS") return (
97
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-100 text-green-700 text-[10px]" style={{ fontWeight: 700 }}>
98
+ <CheckCircle2Icon className="w-2.5 h-2.5" />PASS
99
+ </span>
100
+ );
101
+ if (status === "FAIL") return (
102
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-100 text-red-700 text-[10px]" style={{ fontWeight: 700 }}>
103
+ <XCircleIcon className="w-2.5 h-2.5" />FAIL
104
+ </span>
105
+ );
106
+ return (
107
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-amber-100 text-amber-700 text-[10px]" style={{ fontWeight: 700 }}>
108
+ <AlertTriangleIcon className="w-2.5 h-2.5" />N/A
109
+ </span>
110
+ );
111
+ };
112
+
113
+ const isCompliant = summary.fail === 0;
114
+ const compliancePct = summary.compliancePct;
115
+
116
+ return (
117
+ <div className="max-w-6xl mx-auto px-6 py-8">
118
+ {/* Header */}
119
+ <div className="flex items-start justify-between mb-5 flex-wrap gap-3">
120
+ <div>
121
+ <h1 style={{ fontSize: "22px", fontWeight: 700, color: "#0F172A" }}>Laporan Final</h1>
122
+ <p className="text-slate-500 mt-1 text-sm">Tinjau hasil analisis kepatuhan dan unduh laporan PDF.</p>
123
+ </div>
124
+ <div className="flex items-center gap-2">
125
+ <button
126
+ onClick={handlePrint}
127
+ className="flex items-center gap-1.5 px-3.5 py-2 rounded-lg border border-slate-200 bg-white text-slate-600 text-xs hover:bg-slate-50 transition-colors"
128
+ style={{ fontWeight: 500 }}
129
+ >
130
+ <PrinterIcon className="w-3.5 h-3.5" />
131
+ Cetak
132
+ </button>
133
+ {!reportReady ? (
134
+ <button
135
+ onClick={handleGenerateReport}
136
+ disabled={generating}
137
+ className="flex items-center gap-2 px-5 py-2 rounded-lg bg-[#2563EB] hover:bg-blue-700 text-white text-xs shadow-sm transition-all disabled:opacity-60"
138
+ style={{ fontWeight: 600 }}
139
+ >
140
+ {generating ? (
141
+ <><span className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" />Membuat laporan...</>
142
+ ) : (
143
+ <><FileTextIcon className="w-3.5 h-3.5" />Buat Laporan PDF</>
144
+ )}
145
+ </button>
146
+ ) : (
147
+ <button
148
+ onClick={handleDownload}
149
+ disabled={downloading}
150
+ className="flex items-center gap-2 px-5 py-2 rounded-lg bg-[#1E3A5F] hover:bg-[#162d4a] text-white text-xs shadow-sm transition-all"
151
+ style={{ fontWeight: 600 }}
152
+ >
153
+ {downloading ? (
154
+ <><span className="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin" />Mengunduh...</>
155
+ ) : (
156
+ <><DownloadIcon className="w-3.5 h-3.5" />Download PDF</>
157
+ )}
158
+ </button>
159
+ )}
160
+ </div>
161
+ </div>
162
+
163
+ {/* Error */}
164
+ {error && (
165
+ <div className="mb-4 bg-red-50 border border-red-200 rounded-xl px-4 py-3 text-sm text-red-700">
166
+ ⚠️ {error}
167
+ </div>
168
+ )}
169
+
170
+ {/* Verdict Banner */}
171
+ <div className={`rounded-xl border p-4 mb-5 flex items-center gap-3 ${
172
+ isCompliant
173
+ ? "border-green-200 bg-green-50"
174
+ : "border-amber-200 bg-amber-50"
175
+ }`}>
176
+ <div className={`w-10 h-10 rounded-xl flex items-center justify-center flex-shrink-0 ${
177
+ isCompliant ? "bg-green-100" : "bg-amber-100"
178
+ }`}>
179
+ {isCompliant
180
+ ? <CheckCircle2Icon className="w-5 h-5 text-[#16A34A]" />
181
+ : <AlertTriangleIcon className="w-5 h-5 text-[#D97706]" />
182
+ }
183
+ </div>
184
+ <div className="flex-1">
185
+ <p style={{ fontWeight: 700, fontSize: "14px", color: isCompliant ? "#14532D" : "#92400E" }}>
186
+ {isCompliant ? "Memenuhi Syarat — Siap Diajukan" : "Tidak Memenuhi Syarat — Perbaikan Diperlukan"}
187
+ </p>
188
+ <p className={`text-xs mt-0.5 ${isCompliant ? "text-green-700" : "text-amber-700"}`}>
189
+ {isCompliant
190
+ ? `Semua parameter memenuhi standar BPOM. Produk siap untuk proses registrasi.`
191
+ : `Produk memiliki ${summary.fail} parameter yang tidak memenuhi batas regulasi BPOM. Perbaikan formulasi diperlukan sebelum pengajuan registrasi.`
192
+ }
193
+ </p>
194
+ </div>
195
+ <div className="text-right flex-shrink-0">
196
+ <p style={{ fontWeight: 800, fontSize: "22px", color: isCompliant ? "#16A34A" : "#D97706", lineHeight: 1 }}>{compliancePct}%</p>
197
+ <p className={`text-[10px] mt-0.5 ${isCompliant ? "text-green-600" : "text-amber-600"}`} style={{ fontWeight: 500 }}>Kepatuhan</p>
198
+ </div>
199
+ </div>
200
+
201
+ {/* Report Preview Card */}
202
+ <div className="bg-white rounded-xl border border-slate-200 overflow-hidden mb-5">
203
+ {/* Report Header */}
204
+ <div className="bg-[#1E3A5F] px-6 py-5 text-white">
205
+ <div className="flex items-start justify-between flex-wrap gap-3">
206
+ <div>
207
+ <div className="flex items-center gap-2 mb-3">
208
+ <ShieldCheckIcon className="w-5 h-5 opacity-70" />
209
+ <span className="text-[11px] opacity-60 uppercase tracking-wider" style={{ fontWeight: 600 }}>Laporan Kepatuhan BPOM</span>
210
+ </div>
211
+ <p style={{ fontWeight: 700, fontSize: "18px", lineHeight: 1.2 }}>{productName}</p>
212
+ <p className="text-blue-200 text-xs mt-1">{producer}{batchNo !== "-" ? ` — Batch ${batchNo}` : ""}</p>
213
+ </div>
214
+ <div className="text-right">
215
+ <div className="flex items-center gap-1.5 text-blue-200 text-xs mb-1 justify-end">
216
+ <CalendarIcon className="w-3 h-3" />
217
+ <span>{today}</span>
218
+ </div>
219
+ <p className="text-[10px] text-blue-300" style={{ fontFamily: "monospace" }}>Kategori: {category}</p>
220
+ </div>
221
+ </div>
222
+ </div>
223
+
224
+ {/* Report Body */}
225
+ <div className="p-6">
226
+ {/* Product Info */}
227
+ <div className="mb-6">
228
+ <p className="text-xs text-slate-500 uppercase tracking-wider mb-3" style={{ fontWeight: 600 }}>Informasi Produk</p>
229
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
230
+ {[
231
+ { label: "Nama Produk", value: productName },
232
+ { label: "Produsen", value: producer },
233
+ { label: "Nomor Batch", value: batchNo },
234
+ { label: "Kategori BPOM", value: category },
235
+ { label: "Tanggal Uji", value: testDate },
236
+ { label: "Laboratorium", value: labName },
237
+ { label: "Tanggal Laporan", value: today },
238
+ ].map((item, i) => (
239
+ <div key={i} className="bg-slate-50 rounded-lg p-3">
240
+ <p className="text-[10px] text-slate-400 mb-1" style={{ fontWeight: 500 }}>{item.label}</p>
241
+ <p className="text-xs text-slate-800" style={{ fontWeight: 600 }}>{item.value || "-"}</p>
242
+ </div>
243
+ ))}
244
+ </div>
245
+ </div>
246
+
247
+ {/* Executive Summary */}
248
+ <div className="mb-6">
249
+ <p className="text-xs text-slate-500 uppercase tracking-wider mb-3" style={{ fontWeight: 600 }}>Ringkasan Eksekutif</p>
250
+ <div className="grid grid-cols-2 md:grid-cols-3 gap-3 mb-4">
251
+ {[
252
+ { label: "Total Parameter", value: String(summary.total), icon: null, color: "text-slate-700" },
253
+ { label: "Parameter PASS", value: String(summary.pass), icon: <CheckCircle2Icon className="w-4 h-4 text-[#16A34A]" />, color: "text-[#16A34A]" },
254
+ { label: "Parameter FAIL", value: String(summary.fail), icon: <XCircleIcon className="w-4 h-4 text-[#DC2626]" />, color: "text-[#DC2626]" },
255
+ { label: "Data Tidak Ada", value: String(summary.missing), icon: <AlertTriangleIcon className="w-4 h-4 text-[#D97706]" />, color: "text-[#D97706]" },
256
+ { label: "Tingkat Kepatuhan", value: `${compliancePct}%`, icon: null, color: "text-[#1E3A5F]" },
257
+ { label: "Rekomendasi", value: isCompliant ? "Siap Diajukan" : "Perbaikan Formulasi", icon: null, color: isCompliant ? "text-[#16A34A]" : "text-[#D97706]" },
258
+ ].map((s, i) => (
259
+ <div key={i} className="flex items-center gap-2.5 bg-slate-50 rounded-lg p-3">
260
+ {s.icon && <div className="flex-shrink-0">{s.icon}</div>}
261
+ <div>
262
+ <p className="text-[10px] text-slate-400" style={{ fontWeight: 500 }}>{s.label}</p>
263
+ <p className={`text-sm ${s.color}`} style={{ fontWeight: 700 }}>{s.value}</p>
264
+ </div>
265
+ </div>
266
+ ))}
267
+ </div>
268
+ {narration && (
269
+ <div className="bg-slate-50 rounded-lg p-4 border-l-4 border-[#1E3A5F]">
270
+ <div className="text-xs text-slate-700 leading-relaxed">
271
+ <ReactMarkdown
272
+ components={{
273
+ h1: ({node, ...props}: any) => <h1 className="text-sm font-bold mt-2 mb-2 text-slate-900" {...props}/>,
274
+ h2: ({node, ...props}: any) => <h2 className="text-[13px] font-bold mt-2 mb-2 text-slate-900" {...props}/>,
275
+ h3: ({node, ...props}: any) => <h3 className="text-xs font-bold mt-2 mb-1 text-slate-900" {...props}/>,
276
+ p: ({node, ...props}: any) => <p className="mb-2 last:mb-0" {...props}/>,
277
+ strong: ({node, ...props}: any) => <strong className="font-bold text-slate-900" {...props}/>,
278
+ ul: ({node, ...props}: any) => <ul className="list-disc pl-4 mb-2 space-y-1" {...props}/>,
279
+ ol: ({node, ...props}: any) => <ol className="list-decimal pl-4 mb-2 space-y-1" {...props}/>,
280
+ li: ({node, ...props}: any) => <li className="" {...props}/>,
281
+ }}
282
+ >
283
+ {narration}
284
+ </ReactMarkdown>
285
+ </div>
286
+ </div>
287
+ )}
288
+ </div>
289
+
290
+ {/* Detail Results Table */}
291
+ <div>
292
+ <div className="flex items-center gap-2 mb-3">
293
+ <FileTextIcon className="w-3.5 h-3.5 text-slate-400" />
294
+ <p className="text-xs text-slate-500 uppercase tracking-wider" style={{ fontWeight: 600 }}>Detail Hasil Uji</p>
295
+ </div>
296
+ <div className="rounded-lg border border-slate-200 overflow-hidden">
297
+ <table className="w-full text-xs">
298
+ <thead>
299
+ <tr className="bg-slate-50 border-b border-slate-200">
300
+ <th className="text-left px-4 py-2 text-slate-500" style={{ fontWeight: 600, fontSize: "10px", textTransform: "uppercase" }}>Parameter</th>
301
+ <th className="text-right px-4 py-2 text-slate-500" style={{ fontWeight: 600, fontSize: "10px", textTransform: "uppercase" }}>Nilai Uji</th>
302
+ <th className="text-left px-4 py-2 text-slate-500" style={{ fontWeight: 600, fontSize: "10px", textTransform: "uppercase" }}>Batas BPOM</th>
303
+ <th className="text-center px-4 py-2 text-slate-500" style={{ fontWeight: 600, fontSize: "10px", textTransform: "uppercase" }}>Status</th>
304
+ </tr>
305
+ </thead>
306
+ <tbody>
307
+ {rows.map((row, i) => (
308
+ <tr
309
+ key={i}
310
+ className={`border-b border-slate-100 last:border-0 ${row.status === "FAIL" ? "bg-red-50/50" : row.status === "MISSING" ? "bg-amber-50/30" : ""}`}
311
+ style={{ borderLeft: `3px solid ${row.status === "PASS" ? "#16A34A" : row.status === "FAIL" ? "#DC2626" : "#D97706"}` }}
312
+ >
313
+ <td className="px-4 py-2 text-slate-700" style={{ fontWeight: 500 }}>{row.param}</td>
314
+ <td className="px-4 py-2 text-right tabular-nums text-slate-600" style={{ fontFamily: "'JetBrains Mono', monospace" }}>{row.found}</td>
315
+ <td className="px-4 py-2 text-slate-500" style={{ fontFamily: "'JetBrains Mono', monospace", fontSize: "11px" }}>{row.threshold}</td>
316
+ <td className="px-4 py-2 text-center"><StatusBadge status={row.status} /></td>
317
+ </tr>
318
+ ))}
319
+ </tbody>
320
+ </table>
321
+ </div>
322
+ </div>
323
+ </div>
324
+
325
+ {/* Report Footer */}
326
+ <div className="bg-slate-50 border-t border-slate-100 px-6 py-3 flex items-center justify-between text-[10px] text-slate-400">
327
+ <span>Dibuat oleh BPOM Compliance AI System — bukan pengganti konsultasi regulasi resmi.</span>
328
+ <span style={{ fontFamily: "monospace" }}>v2.4.1 · {today}</span>
329
+ </div>
330
+ </div>
331
+
332
+ {/* Markdown Preview (if generated) */}
333
+ {markdownPreview && (
334
+ <div className="bg-white rounded-xl border border-slate-200 p-6 mb-5">
335
+ <p className="text-xs text-slate-500 uppercase tracking-wider mb-3" style={{ fontWeight: 600 }}>Preview Laporan (Markdown)</p>
336
+ <pre className="text-xs text-slate-700 whitespace-pre-wrap leading-relaxed overflow-auto max-h-64 bg-slate-50 rounded-lg p-4 border border-slate-100">
337
+ {markdownPreview}
338
+ </pre>
339
+ </div>
340
+ )}
341
+
342
+ {/* New Analysis Button */}
343
+ <div className="flex justify-center">
344
+ <button
345
+ onClick={onReset}
346
+ className="flex items-center gap-2 px-5 py-2.5 rounded-lg border border-slate-200 bg-white text-slate-600 text-sm hover:bg-slate-50 hover:border-slate-300 transition-all"
347
+ style={{ fontWeight: 500 }}
348
+ >
349
+ + Mulai Analisis Baru
350
+ </button>
351
+ </div>
352
+ </div>
353
+ );
354
+ }
BPOM Compliance AI UI Redesign/src/app/components/StepIndicator.tsx ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { CheckIcon } from "lucide-react";
2
+
3
+ const steps = [
4
+ { id: 1, label: "Input Dokumen", sublabel: "Upload atau tempel teks" },
5
+ { id: 2, label: "Review Hasil AI", sublabel: "Periksa hasil analisis" },
6
+ { id: 3, label: "Laporan Final", sublabel: "Download laporan PDF" },
7
+ ];
8
+
9
+ interface StepIndicatorProps {
10
+ currentStep: number;
11
+ }
12
+
13
+ export function StepIndicator({ currentStep }: StepIndicatorProps) {
14
+ return (
15
+ <div className="w-full bg-white border-b border-border sticky top-0 z-10">
16
+ <div className="max-w-6xl mx-auto px-6 py-4">
17
+ <nav aria-label="Progress">
18
+ <ol className="flex items-center justify-center gap-0">
19
+ {steps.map((step, index) => {
20
+ const isCompleted = currentStep > step.id;
21
+ const isActive = currentStep === step.id;
22
+ const isLast = index === steps.length - 1;
23
+
24
+ return (
25
+ <li key={step.id} className="flex items-center">
26
+ <div className="flex flex-col items-center gap-1.5">
27
+ <div className="flex items-center">
28
+ <div
29
+ className={`w-8 h-8 rounded-full flex items-center justify-center text-sm transition-all duration-200 ${
30
+ isCompleted
31
+ ? "bg-[#1E3A5F] text-white"
32
+ : isActive
33
+ ? "bg-[#2563EB] text-white ring-4 ring-blue-100"
34
+ : "bg-slate-100 text-slate-400 border border-slate-200"
35
+ }`}
36
+ >
37
+ {isCompleted ? (
38
+ <CheckIcon className="w-4 h-4" strokeWidth={2.5} />
39
+ ) : (
40
+ <span style={{ fontWeight: 600, fontSize: "13px" }}>{step.id}</span>
41
+ )}
42
+ </div>
43
+ </div>
44
+ <div className="text-center">
45
+ <p
46
+ className={`text-xs transition-colors duration-200 ${
47
+ isActive
48
+ ? "text-[#1E3A5F]"
49
+ : isCompleted
50
+ ? "text-[#1E3A5F]"
51
+ : "text-slate-400"
52
+ }`}
53
+ style={{ fontWeight: isActive ? 600 : 500 }}
54
+ >
55
+ {step.label}
56
+ </p>
57
+ <p className="text-[10px] text-slate-400" style={{ display: isActive ? 'block' : 'none' }}>
58
+ {step.sublabel}
59
+ </p>
60
+ </div>
61
+ </div>
62
+
63
+ {!isLast && (
64
+ <div
65
+ className={`h-px w-16 mx-3 mt-[-12px] transition-colors duration-300 ${
66
+ isCompleted ? "bg-[#1E3A5F]" : "bg-slate-200"
67
+ }`}
68
+ />
69
+ )}
70
+ </li>
71
+ );
72
+ })}
73
+ </ol>
74
+ </nav>
75
+ </div>
76
+ </div>
77
+ );
78
+ }
BPOM Compliance AI UI Redesign/src/app/components/figma/ImageWithFallback.tsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from 'react'
2
+
3
+ const ERROR_IMG_SRC =
4
+ 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iODgiIGhlaWdodD0iODgiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgc3Ryb2tlPSIjMDAwIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBvcGFjaXR5PSIuMyIgZmlsbD0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIzLjciPjxyZWN0IHg9IjE2IiB5PSIxNiIgd2lkdGg9IjU2IiBoZWlnaHQ9IjU2IiByeD0iNiIvPjxwYXRoIGQ9Im0xNiA1OCAxNi0xOCAzMiAzMiIvPjxjaXJjbGUgY3g9IjUzIiBjeT0iMzUiIHI9IjciLz48L3N2Zz4KCg=='
5
+
6
+ export function ImageWithFallback(props: React.ImgHTMLAttributes<HTMLImageElement>) {
7
+ const [didError, setDidError] = useState(false)
8
+
9
+ const handleError = () => {
10
+ setDidError(true)
11
+ }
12
+
13
+ const { src, alt, style, className, ...rest } = props
14
+
15
+ return didError ? (
16
+ <div
17
+ className={`inline-block bg-gray-100 text-center align-middle ${className ?? ''}`}
18
+ style={style}
19
+ >
20
+ <div className="flex items-center justify-center w-full h-full">
21
+ <img src={ERROR_IMG_SRC} alt="Error loading image" {...rest} data-original-url={src} />
22
+ </div>
23
+ </div>
24
+ ) : (
25
+ <img src={src} alt={alt} className={className} style={style} {...rest} onError={handleError} />
26
+ )
27
+ }
BPOM Compliance AI UI Redesign/src/app/components/ui/accordion.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as AccordionPrimitive from "@radix-ui/react-accordion";
5
+ import { ChevronDownIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function Accordion({
10
+ ...props
11
+ }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
12
+ return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
13
+ }
14
+
15
+ function AccordionItem({
16
+ className,
17
+ ...props
18
+ }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
19
+ return (
20
+ <AccordionPrimitive.Item
21
+ data-slot="accordion-item"
22
+ className={cn("border-b last:border-b-0", className)}
23
+ {...props}
24
+ />
25
+ );
26
+ }
27
+
28
+ function AccordionTrigger({
29
+ className,
30
+ children,
31
+ ...props
32
+ }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
33
+ return (
34
+ <AccordionPrimitive.Header className="flex">
35
+ <AccordionPrimitive.Trigger
36
+ data-slot="accordion-trigger"
37
+ className={cn(
38
+ "focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
39
+ className,
40
+ )}
41
+ {...props}
42
+ >
43
+ {children}
44
+ <ChevronDownIcon className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
45
+ </AccordionPrimitive.Trigger>
46
+ </AccordionPrimitive.Header>
47
+ );
48
+ }
49
+
50
+ function AccordionContent({
51
+ className,
52
+ children,
53
+ ...props
54
+ }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
55
+ return (
56
+ <AccordionPrimitive.Content
57
+ data-slot="accordion-content"
58
+ className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
59
+ {...props}
60
+ >
61
+ <div className={cn("pt-0 pb-4", className)}>{children}</div>
62
+ </AccordionPrimitive.Content>
63
+ );
64
+ }
65
+
66
+ export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
BPOM Compliance AI UI Redesign/src/app/components/ui/alert-dialog.tsx ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
5
+
6
+ import { cn } from "./utils";
7
+ import { buttonVariants } from "./button";
8
+
9
+ function AlertDialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
12
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
13
+ }
14
+
15
+ function AlertDialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
18
+ return (
19
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
20
+ );
21
+ }
22
+
23
+ function AlertDialogPortal({
24
+ ...props
25
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
26
+ return (
27
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
28
+ );
29
+ }
30
+
31
+ function AlertDialogOverlay({
32
+ className,
33
+ ...props
34
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
35
+ return (
36
+ <AlertDialogPrimitive.Overlay
37
+ data-slot="alert-dialog-overlay"
38
+ className={cn(
39
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
40
+ className,
41
+ )}
42
+ {...props}
43
+ />
44
+ );
45
+ }
46
+
47
+ function AlertDialogContent({
48
+ className,
49
+ ...props
50
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
51
+ return (
52
+ <AlertDialogPortal>
53
+ <AlertDialogOverlay />
54
+ <AlertDialogPrimitive.Content
55
+ data-slot="alert-dialog-content"
56
+ className={cn(
57
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
58
+ className,
59
+ )}
60
+ {...props}
61
+ />
62
+ </AlertDialogPortal>
63
+ );
64
+ }
65
+
66
+ function AlertDialogHeader({
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"div">) {
70
+ return (
71
+ <div
72
+ data-slot="alert-dialog-header"
73
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
74
+ {...props}
75
+ />
76
+ );
77
+ }
78
+
79
+ function AlertDialogFooter({
80
+ className,
81
+ ...props
82
+ }: React.ComponentProps<"div">) {
83
+ return (
84
+ <div
85
+ data-slot="alert-dialog-footer"
86
+ className={cn(
87
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
88
+ className,
89
+ )}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ function AlertDialogTitle({
96
+ className,
97
+ ...props
98
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
99
+ return (
100
+ <AlertDialogPrimitive.Title
101
+ data-slot="alert-dialog-title"
102
+ className={cn("text-lg font-semibold", className)}
103
+ {...props}
104
+ />
105
+ );
106
+ }
107
+
108
+ function AlertDialogDescription({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
112
+ return (
113
+ <AlertDialogPrimitive.Description
114
+ data-slot="alert-dialog-description"
115
+ className={cn("text-muted-foreground text-sm", className)}
116
+ {...props}
117
+ />
118
+ );
119
+ }
120
+
121
+ function AlertDialogAction({
122
+ className,
123
+ ...props
124
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
125
+ return (
126
+ <AlertDialogPrimitive.Action
127
+ className={cn(buttonVariants(), className)}
128
+ {...props}
129
+ />
130
+ );
131
+ }
132
+
133
+ function AlertDialogCancel({
134
+ className,
135
+ ...props
136
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
137
+ return (
138
+ <AlertDialogPrimitive.Cancel
139
+ className={cn(buttonVariants({ variant: "outline" }), className)}
140
+ {...props}
141
+ />
142
+ );
143
+ }
144
+
145
+ export {
146
+ AlertDialog,
147
+ AlertDialogPortal,
148
+ AlertDialogOverlay,
149
+ AlertDialogTrigger,
150
+ AlertDialogContent,
151
+ AlertDialogHeader,
152
+ AlertDialogFooter,
153
+ AlertDialogTitle,
154
+ AlertDialogDescription,
155
+ AlertDialogAction,
156
+ AlertDialogCancel,
157
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/alert.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "./utils";
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ },
20
+ );
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
+ className,
44
+ )}
45
+ {...props}
46
+ />
47
+ );
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
59
+ className,
60
+ )}
61
+ {...props}
62
+ />
63
+ );
64
+ }
65
+
66
+ export { Alert, AlertTitle, AlertDescription };
BPOM Compliance AI UI Redesign/src/app/components/ui/aspect-ratio.tsx ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
4
+
5
+ function AspectRatio({
6
+ ...props
7
+ }: React.ComponentProps<typeof AspectRatioPrimitive.Root>) {
8
+ return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} />;
9
+ }
10
+
11
+ export { AspectRatio };
BPOM Compliance AI UI Redesign/src/app/components/ui/avatar.tsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as AvatarPrimitive from "@radix-ui/react-avatar";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function Avatar({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
12
+ return (
13
+ <AvatarPrimitive.Root
14
+ data-slot="avatar"
15
+ className={cn(
16
+ "relative flex size-10 shrink-0 overflow-hidden rounded-full",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ function AvatarImage({
25
+ className,
26
+ ...props
27
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
28
+ return (
29
+ <AvatarPrimitive.Image
30
+ data-slot="avatar-image"
31
+ className={cn("aspect-square size-full", className)}
32
+ {...props}
33
+ />
34
+ );
35
+ }
36
+
37
+ function AvatarFallback({
38
+ className,
39
+ ...props
40
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
41
+ return (
42
+ <AvatarPrimitive.Fallback
43
+ data-slot="avatar-fallback"
44
+ className={cn(
45
+ "bg-muted flex size-full items-center justify-center rounded-full",
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
53
+ export { Avatar, AvatarImage, AvatarFallback };
BPOM Compliance AI UI Redesign/src/app/components/ui/badge.tsx ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "./utils";
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ },
26
+ );
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? Slot : "span";
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ );
44
+ }
45
+
46
+ export { Badge, badgeVariants };
BPOM Compliance AI UI Redesign/src/app/components/ui/breadcrumb.tsx ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { ChevronRight, MoreHorizontal } from "lucide-react";
4
+
5
+ import { cn } from "./utils";
6
+
7
+ function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
8
+ return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
9
+ }
10
+
11
+ function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
12
+ return (
13
+ <ol
14
+ data-slot="breadcrumb-list"
15
+ className={cn(
16
+ "text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
25
+ return (
26
+ <li
27
+ data-slot="breadcrumb-item"
28
+ className={cn("inline-flex items-center gap-1.5", className)}
29
+ {...props}
30
+ />
31
+ );
32
+ }
33
+
34
+ function BreadcrumbLink({
35
+ asChild,
36
+ className,
37
+ ...props
38
+ }: React.ComponentProps<"a"> & {
39
+ asChild?: boolean;
40
+ }) {
41
+ const Comp = asChild ? Slot : "a";
42
+
43
+ return (
44
+ <Comp
45
+ data-slot="breadcrumb-link"
46
+ className={cn("hover:text-foreground transition-colors", className)}
47
+ {...props}
48
+ />
49
+ );
50
+ }
51
+
52
+ function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
53
+ return (
54
+ <span
55
+ data-slot="breadcrumb-page"
56
+ role="link"
57
+ aria-disabled="true"
58
+ aria-current="page"
59
+ className={cn("text-foreground font-normal", className)}
60
+ {...props}
61
+ />
62
+ );
63
+ }
64
+
65
+ function BreadcrumbSeparator({
66
+ children,
67
+ className,
68
+ ...props
69
+ }: React.ComponentProps<"li">) {
70
+ return (
71
+ <li
72
+ data-slot="breadcrumb-separator"
73
+ role="presentation"
74
+ aria-hidden="true"
75
+ className={cn("[&>svg]:size-3.5", className)}
76
+ {...props}
77
+ >
78
+ {children ?? <ChevronRight />}
79
+ </li>
80
+ );
81
+ }
82
+
83
+ function BreadcrumbEllipsis({
84
+ className,
85
+ ...props
86
+ }: React.ComponentProps<"span">) {
87
+ return (
88
+ <span
89
+ data-slot="breadcrumb-ellipsis"
90
+ role="presentation"
91
+ aria-hidden="true"
92
+ className={cn("flex size-9 items-center justify-center", className)}
93
+ {...props}
94
+ >
95
+ <MoreHorizontal className="size-4" />
96
+ <span className="sr-only">More</span>
97
+ </span>
98
+ );
99
+ }
100
+
101
+ export {
102
+ Breadcrumb,
103
+ BreadcrumbList,
104
+ BreadcrumbItem,
105
+ BreadcrumbLink,
106
+ BreadcrumbPage,
107
+ BreadcrumbSeparator,
108
+ BreadcrumbEllipsis,
109
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/button.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import { Slot } from "@radix-ui/react-slot";
3
+ import { cva, type VariantProps } from "class-variance-authority";
4
+
5
+ import { cn } from "./utils";
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background text-foreground hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9 rounded-md",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ },
35
+ );
36
+
37
+ function Button({
38
+ className,
39
+ variant,
40
+ size,
41
+ asChild = false,
42
+ ...props
43
+ }: React.ComponentProps<"button"> &
44
+ VariantProps<typeof buttonVariants> & {
45
+ asChild?: boolean;
46
+ }) {
47
+ const Comp = asChild ? Slot : "button";
48
+
49
+ return (
50
+ <Comp
51
+ data-slot="button"
52
+ className={cn(buttonVariants({ variant, size, className }))}
53
+ {...props}
54
+ />
55
+ );
56
+ }
57
+
58
+ export { Button, buttonVariants };
BPOM Compliance AI UI Redesign/src/app/components/ui/calendar.tsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { ChevronLeft, ChevronRight } from "lucide-react";
5
+ import { DayPicker } from "react-day-picker";
6
+
7
+ import { cn } from "./utils";
8
+ import { buttonVariants } from "./button";
9
+
10
+ function Calendar({
11
+ className,
12
+ classNames,
13
+ showOutsideDays = true,
14
+ ...props
15
+ }: React.ComponentProps<typeof DayPicker>) {
16
+ return (
17
+ <DayPicker
18
+ showOutsideDays={showOutsideDays}
19
+ className={cn("p-3", className)}
20
+ classNames={{
21
+ months: "flex flex-col sm:flex-row gap-2",
22
+ month: "flex flex-col gap-4",
23
+ caption: "flex justify-center pt-1 relative items-center w-full",
24
+ caption_label: "text-sm font-medium",
25
+ nav: "flex items-center gap-1",
26
+ nav_button: cn(
27
+ buttonVariants({ variant: "outline" }),
28
+ "size-7 bg-transparent p-0 opacity-50 hover:opacity-100",
29
+ ),
30
+ nav_button_previous: "absolute left-1",
31
+ nav_button_next: "absolute right-1",
32
+ table: "w-full border-collapse space-x-1",
33
+ head_row: "flex",
34
+ head_cell:
35
+ "text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
36
+ row: "flex w-full mt-2",
37
+ cell: cn(
38
+ "relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-range-end)]:rounded-r-md",
39
+ props.mode === "range"
40
+ ? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
41
+ : "[&:has([aria-selected])]:rounded-md",
42
+ ),
43
+ day: cn(
44
+ buttonVariants({ variant: "ghost" }),
45
+ "size-8 p-0 font-normal aria-selected:opacity-100",
46
+ ),
47
+ day_range_start:
48
+ "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
49
+ day_range_end:
50
+ "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
51
+ day_selected:
52
+ "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
53
+ day_today: "bg-accent text-accent-foreground",
54
+ day_outside:
55
+ "day-outside text-muted-foreground aria-selected:text-muted-foreground",
56
+ day_disabled: "text-muted-foreground opacity-50",
57
+ day_range_middle:
58
+ "aria-selected:bg-accent aria-selected:text-accent-foreground",
59
+ day_hidden: "invisible",
60
+ ...classNames,
61
+ }}
62
+ components={{
63
+ IconLeft: ({ className, ...props }) => (
64
+ <ChevronLeft className={cn("size-4", className)} {...props} />
65
+ ),
66
+ IconRight: ({ className, ...props }) => (
67
+ <ChevronRight className={cn("size-4", className)} {...props} />
68
+ ),
69
+ }}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ export { Calendar };
BPOM Compliance AI UI Redesign/src/app/components/ui/card.tsx ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "./utils";
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ );
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ );
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <h4
34
+ data-slot="card-title"
35
+ className={cn("leading-none", className)}
36
+ {...props}
37
+ />
38
+ );
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <p
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground", className)}
46
+ {...props}
47
+ />
48
+ );
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className,
58
+ )}
59
+ {...props}
60
+ />
61
+ );
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6 [&:last-child]:pb-6", className)}
69
+ {...props}
70
+ />
71
+ );
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 pb-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ );
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/carousel.tsx ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import useEmblaCarousel, {
5
+ type UseEmblaCarouselType,
6
+ } from "embla-carousel-react";
7
+ import { ArrowLeft, ArrowRight } from "lucide-react";
8
+
9
+ import { cn } from "./utils";
10
+ import { Button } from "./button";
11
+
12
+ type CarouselApi = UseEmblaCarouselType[1];
13
+ type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
14
+ type CarouselOptions = UseCarouselParameters[0];
15
+ type CarouselPlugin = UseCarouselParameters[1];
16
+
17
+ type CarouselProps = {
18
+ opts?: CarouselOptions;
19
+ plugins?: CarouselPlugin;
20
+ orientation?: "horizontal" | "vertical";
21
+ setApi?: (api: CarouselApi) => void;
22
+ };
23
+
24
+ type CarouselContextProps = {
25
+ carouselRef: ReturnType<typeof useEmblaCarousel>[0];
26
+ api: ReturnType<typeof useEmblaCarousel>[1];
27
+ scrollPrev: () => void;
28
+ scrollNext: () => void;
29
+ canScrollPrev: boolean;
30
+ canScrollNext: boolean;
31
+ } & CarouselProps;
32
+
33
+ const CarouselContext = React.createContext<CarouselContextProps | null>(null);
34
+
35
+ function useCarousel() {
36
+ const context = React.useContext(CarouselContext);
37
+
38
+ if (!context) {
39
+ throw new Error("useCarousel must be used within a <Carousel />");
40
+ }
41
+
42
+ return context;
43
+ }
44
+
45
+ function Carousel({
46
+ orientation = "horizontal",
47
+ opts,
48
+ setApi,
49
+ plugins,
50
+ className,
51
+ children,
52
+ ...props
53
+ }: React.ComponentProps<"div"> & CarouselProps) {
54
+ const [carouselRef, api] = useEmblaCarousel(
55
+ {
56
+ ...opts,
57
+ axis: orientation === "horizontal" ? "x" : "y",
58
+ },
59
+ plugins,
60
+ );
61
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
62
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
63
+
64
+ const onSelect = React.useCallback((api: CarouselApi) => {
65
+ if (!api) return;
66
+ setCanScrollPrev(api.canScrollPrev());
67
+ setCanScrollNext(api.canScrollNext());
68
+ }, []);
69
+
70
+ const scrollPrev = React.useCallback(() => {
71
+ api?.scrollPrev();
72
+ }, [api]);
73
+
74
+ const scrollNext = React.useCallback(() => {
75
+ api?.scrollNext();
76
+ }, [api]);
77
+
78
+ const handleKeyDown = React.useCallback(
79
+ (event: React.KeyboardEvent<HTMLDivElement>) => {
80
+ if (event.key === "ArrowLeft") {
81
+ event.preventDefault();
82
+ scrollPrev();
83
+ } else if (event.key === "ArrowRight") {
84
+ event.preventDefault();
85
+ scrollNext();
86
+ }
87
+ },
88
+ [scrollPrev, scrollNext],
89
+ );
90
+
91
+ React.useEffect(() => {
92
+ if (!api || !setApi) return;
93
+ setApi(api);
94
+ }, [api, setApi]);
95
+
96
+ React.useEffect(() => {
97
+ if (!api) return;
98
+ onSelect(api);
99
+ api.on("reInit", onSelect);
100
+ api.on("select", onSelect);
101
+
102
+ return () => {
103
+ api?.off("select", onSelect);
104
+ };
105
+ }, [api, onSelect]);
106
+
107
+ return (
108
+ <CarouselContext.Provider
109
+ value={{
110
+ carouselRef,
111
+ api: api,
112
+ opts,
113
+ orientation:
114
+ orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
115
+ scrollPrev,
116
+ scrollNext,
117
+ canScrollPrev,
118
+ canScrollNext,
119
+ }}
120
+ >
121
+ <div
122
+ onKeyDownCapture={handleKeyDown}
123
+ className={cn("relative", className)}
124
+ role="region"
125
+ aria-roledescription="carousel"
126
+ data-slot="carousel"
127
+ {...props}
128
+ >
129
+ {children}
130
+ </div>
131
+ </CarouselContext.Provider>
132
+ );
133
+ }
134
+
135
+ function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
136
+ const { carouselRef, orientation } = useCarousel();
137
+
138
+ return (
139
+ <div
140
+ ref={carouselRef}
141
+ className="overflow-hidden"
142
+ data-slot="carousel-content"
143
+ >
144
+ <div
145
+ className={cn(
146
+ "flex",
147
+ orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
148
+ className,
149
+ )}
150
+ {...props}
151
+ />
152
+ </div>
153
+ );
154
+ }
155
+
156
+ function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
157
+ const { orientation } = useCarousel();
158
+
159
+ return (
160
+ <div
161
+ role="group"
162
+ aria-roledescription="slide"
163
+ data-slot="carousel-item"
164
+ className={cn(
165
+ "min-w-0 shrink-0 grow-0 basis-full",
166
+ orientation === "horizontal" ? "pl-4" : "pt-4",
167
+ className,
168
+ )}
169
+ {...props}
170
+ />
171
+ );
172
+ }
173
+
174
+ function CarouselPrevious({
175
+ className,
176
+ variant = "outline",
177
+ size = "icon",
178
+ ...props
179
+ }: React.ComponentProps<typeof Button>) {
180
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
181
+
182
+ return (
183
+ <Button
184
+ data-slot="carousel-previous"
185
+ variant={variant}
186
+ size={size}
187
+ className={cn(
188
+ "absolute size-8 rounded-full",
189
+ orientation === "horizontal"
190
+ ? "top-1/2 -left-12 -translate-y-1/2"
191
+ : "-top-12 left-1/2 -translate-x-1/2 rotate-90",
192
+ className,
193
+ )}
194
+ disabled={!canScrollPrev}
195
+ onClick={scrollPrev}
196
+ {...props}
197
+ >
198
+ <ArrowLeft />
199
+ <span className="sr-only">Previous slide</span>
200
+ </Button>
201
+ );
202
+ }
203
+
204
+ function CarouselNext({
205
+ className,
206
+ variant = "outline",
207
+ size = "icon",
208
+ ...props
209
+ }: React.ComponentProps<typeof Button>) {
210
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
211
+
212
+ return (
213
+ <Button
214
+ data-slot="carousel-next"
215
+ variant={variant}
216
+ size={size}
217
+ className={cn(
218
+ "absolute size-8 rounded-full",
219
+ orientation === "horizontal"
220
+ ? "top-1/2 -right-12 -translate-y-1/2"
221
+ : "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
222
+ className,
223
+ )}
224
+ disabled={!canScrollNext}
225
+ onClick={scrollNext}
226
+ {...props}
227
+ >
228
+ <ArrowRight />
229
+ <span className="sr-only">Next slide</span>
230
+ </Button>
231
+ );
232
+ }
233
+
234
+ export {
235
+ type CarouselApi,
236
+ Carousel,
237
+ CarouselContent,
238
+ CarouselItem,
239
+ CarouselPrevious,
240
+ CarouselNext,
241
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/chart.tsx ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as RechartsPrimitive from "recharts";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ // Format: { THEME_NAME: CSS_SELECTOR }
9
+ const THEMES = { light: "", dark: ".dark" } as const;
10
+
11
+ export type ChartConfig = {
12
+ [k in string]: {
13
+ label?: React.ReactNode;
14
+ icon?: React.ComponentType;
15
+ } & (
16
+ | { color?: string; theme?: never }
17
+ | { color?: never; theme: Record<keyof typeof THEMES, string> }
18
+ );
19
+ };
20
+
21
+ type ChartContextProps = {
22
+ config: ChartConfig;
23
+ };
24
+
25
+ const ChartContext = React.createContext<ChartContextProps | null>(null);
26
+
27
+ function useChart() {
28
+ const context = React.useContext(ChartContext);
29
+
30
+ if (!context) {
31
+ throw new Error("useChart must be used within a <ChartContainer />");
32
+ }
33
+
34
+ return context;
35
+ }
36
+
37
+ function ChartContainer({
38
+ id,
39
+ className,
40
+ children,
41
+ config,
42
+ ...props
43
+ }: React.ComponentProps<"div"> & {
44
+ config: ChartConfig;
45
+ children: React.ComponentProps<
46
+ typeof RechartsPrimitive.ResponsiveContainer
47
+ >["children"];
48
+ }) {
49
+ const uniqueId = React.useId();
50
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
51
+
52
+ return (
53
+ <ChartContext.Provider value={{ config }}>
54
+ <div
55
+ data-slot="chart"
56
+ data-chart={chartId}
57
+ className={cn(
58
+ "[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
59
+ className,
60
+ )}
61
+ {...props}
62
+ >
63
+ <ChartStyle id={chartId} config={config} />
64
+ <RechartsPrimitive.ResponsiveContainer>
65
+ {children}
66
+ </RechartsPrimitive.ResponsiveContainer>
67
+ </div>
68
+ </ChartContext.Provider>
69
+ );
70
+ }
71
+
72
+ const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
73
+ const colorConfig = Object.entries(config).filter(
74
+ ([, config]) => config.theme || config.color,
75
+ );
76
+
77
+ if (!colorConfig.length) {
78
+ return null;
79
+ }
80
+
81
+ return (
82
+ <style
83
+ dangerouslySetInnerHTML={{
84
+ __html: Object.entries(THEMES)
85
+ .map(
86
+ ([theme, prefix]) => `
87
+ ${prefix} [data-chart=${id}] {
88
+ ${colorConfig
89
+ .map(([key, itemConfig]) => {
90
+ const color =
91
+ itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
92
+ itemConfig.color;
93
+ return color ? ` --color-${key}: ${color};` : null;
94
+ })
95
+ .join("\n")}
96
+ }
97
+ `,
98
+ )
99
+ .join("\n"),
100
+ }}
101
+ />
102
+ );
103
+ };
104
+
105
+ const ChartTooltip = RechartsPrimitive.Tooltip;
106
+
107
+ function ChartTooltipContent({
108
+ active,
109
+ payload,
110
+ className,
111
+ indicator = "dot",
112
+ hideLabel = false,
113
+ hideIndicator = false,
114
+ label,
115
+ labelFormatter,
116
+ labelClassName,
117
+ formatter,
118
+ color,
119
+ nameKey,
120
+ labelKey,
121
+ }: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
122
+ React.ComponentProps<"div"> & {
123
+ hideLabel?: boolean;
124
+ hideIndicator?: boolean;
125
+ indicator?: "line" | "dot" | "dashed";
126
+ nameKey?: string;
127
+ labelKey?: string;
128
+ }) {
129
+ const { config } = useChart();
130
+
131
+ const tooltipLabel = React.useMemo(() => {
132
+ if (hideLabel || !payload?.length) {
133
+ return null;
134
+ }
135
+
136
+ const [item] = payload;
137
+ const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
138
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
139
+ const value =
140
+ !labelKey && typeof label === "string"
141
+ ? config[label as keyof typeof config]?.label || label
142
+ : itemConfig?.label;
143
+
144
+ if (labelFormatter) {
145
+ return (
146
+ <div className={cn("font-medium", labelClassName)}>
147
+ {labelFormatter(value, payload)}
148
+ </div>
149
+ );
150
+ }
151
+
152
+ if (!value) {
153
+ return null;
154
+ }
155
+
156
+ return <div className={cn("font-medium", labelClassName)}>{value}</div>;
157
+ }, [
158
+ label,
159
+ labelFormatter,
160
+ payload,
161
+ hideLabel,
162
+ labelClassName,
163
+ config,
164
+ labelKey,
165
+ ]);
166
+
167
+ if (!active || !payload?.length) {
168
+ return null;
169
+ }
170
+
171
+ const nestLabel = payload.length === 1 && indicator !== "dot";
172
+
173
+ return (
174
+ <div
175
+ className={cn(
176
+ "border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
177
+ className,
178
+ )}
179
+ >
180
+ {!nestLabel ? tooltipLabel : null}
181
+ <div className="grid gap-1.5">
182
+ {payload.map((item, index) => {
183
+ const key = `${nameKey || item.name || item.dataKey || "value"}`;
184
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
185
+ const indicatorColor = color || item.payload.fill || item.color;
186
+
187
+ return (
188
+ <div
189
+ key={item.dataKey}
190
+ className={cn(
191
+ "[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
192
+ indicator === "dot" && "items-center",
193
+ )}
194
+ >
195
+ {formatter && item?.value !== undefined && item.name ? (
196
+ formatter(item.value, item.name, item, index, item.payload)
197
+ ) : (
198
+ <>
199
+ {itemConfig?.icon ? (
200
+ <itemConfig.icon />
201
+ ) : (
202
+ !hideIndicator && (
203
+ <div
204
+ className={cn(
205
+ "shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
206
+ {
207
+ "h-2.5 w-2.5": indicator === "dot",
208
+ "w-1": indicator === "line",
209
+ "w-0 border-[1.5px] border-dashed bg-transparent":
210
+ indicator === "dashed",
211
+ "my-0.5": nestLabel && indicator === "dashed",
212
+ },
213
+ )}
214
+ style={
215
+ {
216
+ "--color-bg": indicatorColor,
217
+ "--color-border": indicatorColor,
218
+ } as React.CSSProperties
219
+ }
220
+ />
221
+ )
222
+ )}
223
+ <div
224
+ className={cn(
225
+ "flex flex-1 justify-between leading-none",
226
+ nestLabel ? "items-end" : "items-center",
227
+ )}
228
+ >
229
+ <div className="grid gap-1.5">
230
+ {nestLabel ? tooltipLabel : null}
231
+ <span className="text-muted-foreground">
232
+ {itemConfig?.label || item.name}
233
+ </span>
234
+ </div>
235
+ {item.value && (
236
+ <span className="text-foreground font-mono font-medium tabular-nums">
237
+ {item.value.toLocaleString()}
238
+ </span>
239
+ )}
240
+ </div>
241
+ </>
242
+ )}
243
+ </div>
244
+ );
245
+ })}
246
+ </div>
247
+ </div>
248
+ );
249
+ }
250
+
251
+ const ChartLegend = RechartsPrimitive.Legend;
252
+
253
+ function ChartLegendContent({
254
+ className,
255
+ hideIcon = false,
256
+ payload,
257
+ verticalAlign = "bottom",
258
+ nameKey,
259
+ }: React.ComponentProps<"div"> &
260
+ Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
261
+ hideIcon?: boolean;
262
+ nameKey?: string;
263
+ }) {
264
+ const { config } = useChart();
265
+
266
+ if (!payload?.length) {
267
+ return null;
268
+ }
269
+
270
+ return (
271
+ <div
272
+ className={cn(
273
+ "flex items-center justify-center gap-4",
274
+ verticalAlign === "top" ? "pb-3" : "pt-3",
275
+ className,
276
+ )}
277
+ >
278
+ {payload.map((item) => {
279
+ const key = `${nameKey || item.dataKey || "value"}`;
280
+ const itemConfig = getPayloadConfigFromPayload(config, item, key);
281
+
282
+ return (
283
+ <div
284
+ key={item.value}
285
+ className={cn(
286
+ "[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
287
+ )}
288
+ >
289
+ {itemConfig?.icon && !hideIcon ? (
290
+ <itemConfig.icon />
291
+ ) : (
292
+ <div
293
+ className="h-2 w-2 shrink-0 rounded-[2px]"
294
+ style={{
295
+ backgroundColor: item.color,
296
+ }}
297
+ />
298
+ )}
299
+ {itemConfig?.label}
300
+ </div>
301
+ );
302
+ })}
303
+ </div>
304
+ );
305
+ }
306
+
307
+ // Helper to extract item config from a payload.
308
+ function getPayloadConfigFromPayload(
309
+ config: ChartConfig,
310
+ payload: unknown,
311
+ key: string,
312
+ ) {
313
+ if (typeof payload !== "object" || payload === null) {
314
+ return undefined;
315
+ }
316
+
317
+ const payloadPayload =
318
+ "payload" in payload &&
319
+ typeof payload.payload === "object" &&
320
+ payload.payload !== null
321
+ ? payload.payload
322
+ : undefined;
323
+
324
+ let configLabelKey: string = key;
325
+
326
+ if (
327
+ key in payload &&
328
+ typeof payload[key as keyof typeof payload] === "string"
329
+ ) {
330
+ configLabelKey = payload[key as keyof typeof payload] as string;
331
+ } else if (
332
+ payloadPayload &&
333
+ key in payloadPayload &&
334
+ typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
335
+ ) {
336
+ configLabelKey = payloadPayload[
337
+ key as keyof typeof payloadPayload
338
+ ] as string;
339
+ }
340
+
341
+ return configLabelKey in config
342
+ ? config[configLabelKey]
343
+ : config[key as keyof typeof config];
344
+ }
345
+
346
+ export {
347
+ ChartContainer,
348
+ ChartTooltip,
349
+ ChartTooltipContent,
350
+ ChartLegend,
351
+ ChartLegendContent,
352
+ ChartStyle,
353
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/checkbox.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
5
+ import { CheckIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function Checkbox({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
13
+ return (
14
+ <CheckboxPrimitive.Root
15
+ data-slot="checkbox"
16
+ className={cn(
17
+ "peer border bg-input-background dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
18
+ className,
19
+ )}
20
+ {...props}
21
+ >
22
+ <CheckboxPrimitive.Indicator
23
+ data-slot="checkbox-indicator"
24
+ className="flex items-center justify-center text-current transition-none"
25
+ >
26
+ <CheckIcon className="size-3.5" />
27
+ </CheckboxPrimitive.Indicator>
28
+ </CheckboxPrimitive.Root>
29
+ );
30
+ }
31
+
32
+ export { Checkbox };
BPOM Compliance AI UI Redesign/src/app/components/ui/collapsible.tsx ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
4
+
5
+ function Collapsible({
6
+ ...props
7
+ }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
8
+ return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />;
9
+ }
10
+
11
+ function CollapsibleTrigger({
12
+ ...props
13
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
14
+ return (
15
+ <CollapsiblePrimitive.CollapsibleTrigger
16
+ data-slot="collapsible-trigger"
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function CollapsibleContent({
23
+ ...props
24
+ }: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
25
+ return (
26
+ <CollapsiblePrimitive.CollapsibleContent
27
+ data-slot="collapsible-content"
28
+ {...props}
29
+ />
30
+ );
31
+ }
32
+
33
+ export { Collapsible, CollapsibleTrigger, CollapsibleContent };
BPOM Compliance AI UI Redesign/src/app/components/ui/command.tsx ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Command as CommandPrimitive } from "cmdk";
5
+ import { SearchIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+ import {
9
+ Dialog,
10
+ DialogContent,
11
+ DialogDescription,
12
+ DialogHeader,
13
+ DialogTitle,
14
+ } from "./dialog";
15
+
16
+ function Command({
17
+ className,
18
+ ...props
19
+ }: React.ComponentProps<typeof CommandPrimitive>) {
20
+ return (
21
+ <CommandPrimitive
22
+ data-slot="command"
23
+ className={cn(
24
+ "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
25
+ className,
26
+ )}
27
+ {...props}
28
+ />
29
+ );
30
+ }
31
+
32
+ function CommandDialog({
33
+ title = "Command Palette",
34
+ description = "Search for a command to run...",
35
+ children,
36
+ ...props
37
+ }: React.ComponentProps<typeof Dialog> & {
38
+ title?: string;
39
+ description?: string;
40
+ }) {
41
+ return (
42
+ <Dialog {...props}>
43
+ <DialogHeader className="sr-only">
44
+ <DialogTitle>{title}</DialogTitle>
45
+ <DialogDescription>{description}</DialogDescription>
46
+ </DialogHeader>
47
+ <DialogContent className="overflow-hidden p-0">
48
+ <Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
49
+ {children}
50
+ </Command>
51
+ </DialogContent>
52
+ </Dialog>
53
+ );
54
+ }
55
+
56
+ function CommandInput({
57
+ className,
58
+ ...props
59
+ }: React.ComponentProps<typeof CommandPrimitive.Input>) {
60
+ return (
61
+ <div
62
+ data-slot="command-input-wrapper"
63
+ className="flex h-9 items-center gap-2 border-b px-3"
64
+ >
65
+ <SearchIcon className="size-4 shrink-0 opacity-50" />
66
+ <CommandPrimitive.Input
67
+ data-slot="command-input"
68
+ className={cn(
69
+ "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
70
+ className,
71
+ )}
72
+ {...props}
73
+ />
74
+ </div>
75
+ );
76
+ }
77
+
78
+ function CommandList({
79
+ className,
80
+ ...props
81
+ }: React.ComponentProps<typeof CommandPrimitive.List>) {
82
+ return (
83
+ <CommandPrimitive.List
84
+ data-slot="command-list"
85
+ className={cn(
86
+ "max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
87
+ className,
88
+ )}
89
+ {...props}
90
+ />
91
+ );
92
+ }
93
+
94
+ function CommandEmpty({
95
+ ...props
96
+ }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
97
+ return (
98
+ <CommandPrimitive.Empty
99
+ data-slot="command-empty"
100
+ className="py-6 text-center text-sm"
101
+ {...props}
102
+ />
103
+ );
104
+ }
105
+
106
+ function CommandGroup({
107
+ className,
108
+ ...props
109
+ }: React.ComponentProps<typeof CommandPrimitive.Group>) {
110
+ return (
111
+ <CommandPrimitive.Group
112
+ data-slot="command-group"
113
+ className={cn(
114
+ "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
115
+ className,
116
+ )}
117
+ {...props}
118
+ />
119
+ );
120
+ }
121
+
122
+ function CommandSeparator({
123
+ className,
124
+ ...props
125
+ }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
126
+ return (
127
+ <CommandPrimitive.Separator
128
+ data-slot="command-separator"
129
+ className={cn("bg-border -mx-1 h-px", className)}
130
+ {...props}
131
+ />
132
+ );
133
+ }
134
+
135
+ function CommandItem({
136
+ className,
137
+ ...props
138
+ }: React.ComponentProps<typeof CommandPrimitive.Item>) {
139
+ return (
140
+ <CommandPrimitive.Item
141
+ data-slot="command-item"
142
+ className={cn(
143
+ "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
144
+ className,
145
+ )}
146
+ {...props}
147
+ />
148
+ );
149
+ }
150
+
151
+ function CommandShortcut({
152
+ className,
153
+ ...props
154
+ }: React.ComponentProps<"span">) {
155
+ return (
156
+ <span
157
+ data-slot="command-shortcut"
158
+ className={cn(
159
+ "text-muted-foreground ml-auto text-xs tracking-widest",
160
+ className,
161
+ )}
162
+ {...props}
163
+ />
164
+ );
165
+ }
166
+
167
+ export {
168
+ Command,
169
+ CommandDialog,
170
+ CommandInput,
171
+ CommandList,
172
+ CommandEmpty,
173
+ CommandGroup,
174
+ CommandItem,
175
+ CommandShortcut,
176
+ CommandSeparator,
177
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/context-menu.tsx ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function ContextMenu({
10
+ ...props
11
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
12
+ return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
13
+ }
14
+
15
+ function ContextMenuTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
18
+ return (
19
+ <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />
20
+ );
21
+ }
22
+
23
+ function ContextMenuGroup({
24
+ ...props
25
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
26
+ return (
27
+ <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />
28
+ );
29
+ }
30
+
31
+ function ContextMenuPortal({
32
+ ...props
33
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
34
+ return (
35
+ <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />
36
+ );
37
+ }
38
+
39
+ function ContextMenuSub({
40
+ ...props
41
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
42
+ return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
43
+ }
44
+
45
+ function ContextMenuRadioGroup({
46
+ ...props
47
+ }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
48
+ return (
49
+ <ContextMenuPrimitive.RadioGroup
50
+ data-slot="context-menu-radio-group"
51
+ {...props}
52
+ />
53
+ );
54
+ }
55
+
56
+ function ContextMenuSubTrigger({
57
+ className,
58
+ inset,
59
+ children,
60
+ ...props
61
+ }: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
62
+ inset?: boolean;
63
+ }) {
64
+ return (
65
+ <ContextMenuPrimitive.SubTrigger
66
+ data-slot="context-menu-sub-trigger"
67
+ data-inset={inset}
68
+ className={cn(
69
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
70
+ className,
71
+ )}
72
+ {...props}
73
+ >
74
+ {children}
75
+ <ChevronRightIcon className="ml-auto" />
76
+ </ContextMenuPrimitive.SubTrigger>
77
+ );
78
+ }
79
+
80
+ function ContextMenuSubContent({
81
+ className,
82
+ ...props
83
+ }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
84
+ return (
85
+ <ContextMenuPrimitive.SubContent
86
+ data-slot="context-menu-sub-content"
87
+ className={cn(
88
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
89
+ className,
90
+ )}
91
+ {...props}
92
+ />
93
+ );
94
+ }
95
+
96
+ function ContextMenuContent({
97
+ className,
98
+ ...props
99
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
100
+ return (
101
+ <ContextMenuPrimitive.Portal>
102
+ <ContextMenuPrimitive.Content
103
+ data-slot="context-menu-content"
104
+ className={cn(
105
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
106
+ className,
107
+ )}
108
+ {...props}
109
+ />
110
+ </ContextMenuPrimitive.Portal>
111
+ );
112
+ }
113
+
114
+ function ContextMenuItem({
115
+ className,
116
+ inset,
117
+ variant = "default",
118
+ ...props
119
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
120
+ inset?: boolean;
121
+ variant?: "default" | "destructive";
122
+ }) {
123
+ return (
124
+ <ContextMenuPrimitive.Item
125
+ data-slot="context-menu-item"
126
+ data-inset={inset}
127
+ data-variant={variant}
128
+ className={cn(
129
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
130
+ className,
131
+ )}
132
+ {...props}
133
+ />
134
+ );
135
+ }
136
+
137
+ function ContextMenuCheckboxItem({
138
+ className,
139
+ children,
140
+ checked,
141
+ ...props
142
+ }: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
143
+ return (
144
+ <ContextMenuPrimitive.CheckboxItem
145
+ data-slot="context-menu-checkbox-item"
146
+ className={cn(
147
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
148
+ className,
149
+ )}
150
+ checked={checked}
151
+ {...props}
152
+ >
153
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
154
+ <ContextMenuPrimitive.ItemIndicator>
155
+ <CheckIcon className="size-4" />
156
+ </ContextMenuPrimitive.ItemIndicator>
157
+ </span>
158
+ {children}
159
+ </ContextMenuPrimitive.CheckboxItem>
160
+ );
161
+ }
162
+
163
+ function ContextMenuRadioItem({
164
+ className,
165
+ children,
166
+ ...props
167
+ }: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
168
+ return (
169
+ <ContextMenuPrimitive.RadioItem
170
+ data-slot="context-menu-radio-item"
171
+ className={cn(
172
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
173
+ className,
174
+ )}
175
+ {...props}
176
+ >
177
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
178
+ <ContextMenuPrimitive.ItemIndicator>
179
+ <CircleIcon className="size-2 fill-current" />
180
+ </ContextMenuPrimitive.ItemIndicator>
181
+ </span>
182
+ {children}
183
+ </ContextMenuPrimitive.RadioItem>
184
+ );
185
+ }
186
+
187
+ function ContextMenuLabel({
188
+ className,
189
+ inset,
190
+ ...props
191
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
192
+ inset?: boolean;
193
+ }) {
194
+ return (
195
+ <ContextMenuPrimitive.Label
196
+ data-slot="context-menu-label"
197
+ data-inset={inset}
198
+ className={cn(
199
+ "text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
200
+ className,
201
+ )}
202
+ {...props}
203
+ />
204
+ );
205
+ }
206
+
207
+ function ContextMenuSeparator({
208
+ className,
209
+ ...props
210
+ }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
211
+ return (
212
+ <ContextMenuPrimitive.Separator
213
+ data-slot="context-menu-separator"
214
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
215
+ {...props}
216
+ />
217
+ );
218
+ }
219
+
220
+ function ContextMenuShortcut({
221
+ className,
222
+ ...props
223
+ }: React.ComponentProps<"span">) {
224
+ return (
225
+ <span
226
+ data-slot="context-menu-shortcut"
227
+ className={cn(
228
+ "text-muted-foreground ml-auto text-xs tracking-widest",
229
+ className,
230
+ )}
231
+ {...props}
232
+ />
233
+ );
234
+ }
235
+
236
+ export {
237
+ ContextMenu,
238
+ ContextMenuTrigger,
239
+ ContextMenuContent,
240
+ ContextMenuItem,
241
+ ContextMenuCheckboxItem,
242
+ ContextMenuRadioItem,
243
+ ContextMenuLabel,
244
+ ContextMenuSeparator,
245
+ ContextMenuShortcut,
246
+ ContextMenuGroup,
247
+ ContextMenuPortal,
248
+ ContextMenuSub,
249
+ ContextMenuSubContent,
250
+ ContextMenuSubTrigger,
251
+ ContextMenuRadioGroup,
252
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/dialog.tsx ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DialogPrimitive from "@radix-ui/react-dialog";
5
+ import { XIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function Dialog({
10
+ ...props
11
+ }: React.ComponentProps<typeof DialogPrimitive.Root>) {
12
+ return <DialogPrimitive.Root data-slot="dialog" {...props} />;
13
+ }
14
+
15
+ function DialogTrigger({
16
+ ...props
17
+ }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
18
+ return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
19
+ }
20
+
21
+ function DialogPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
24
+ return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
25
+ }
26
+
27
+ function DialogClose({
28
+ ...props
29
+ }: React.ComponentProps<typeof DialogPrimitive.Close>) {
30
+ return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
31
+ }
32
+
33
+ function DialogOverlay({
34
+ className,
35
+ ...props
36
+ }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
37
+ return (
38
+ <DialogPrimitive.Overlay
39
+ data-slot="dialog-overlay"
40
+ className={cn(
41
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
42
+ className,
43
+ )}
44
+ {...props}
45
+ />
46
+ );
47
+ }
48
+
49
+ function DialogContent({
50
+ className,
51
+ children,
52
+ ...props
53
+ }: React.ComponentProps<typeof DialogPrimitive.Content>) {
54
+ return (
55
+ <DialogPortal data-slot="dialog-portal">
56
+ <DialogOverlay />
57
+ <DialogPrimitive.Content
58
+ data-slot="dialog-content"
59
+ className={cn(
60
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
61
+ className,
62
+ )}
63
+ {...props}
64
+ >
65
+ {children}
66
+ <DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
67
+ <XIcon />
68
+ <span className="sr-only">Close</span>
69
+ </DialogPrimitive.Close>
70
+ </DialogPrimitive.Content>
71
+ </DialogPortal>
72
+ );
73
+ }
74
+
75
+ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
76
+ return (
77
+ <div
78
+ data-slot="dialog-header"
79
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
86
+ return (
87
+ <div
88
+ data-slot="dialog-footer"
89
+ className={cn(
90
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
91
+ className,
92
+ )}
93
+ {...props}
94
+ />
95
+ );
96
+ }
97
+
98
+ function DialogTitle({
99
+ className,
100
+ ...props
101
+ }: React.ComponentProps<typeof DialogPrimitive.Title>) {
102
+ return (
103
+ <DialogPrimitive.Title
104
+ data-slot="dialog-title"
105
+ className={cn("text-lg leading-none font-semibold", className)}
106
+ {...props}
107
+ />
108
+ );
109
+ }
110
+
111
+ function DialogDescription({
112
+ className,
113
+ ...props
114
+ }: React.ComponentProps<typeof DialogPrimitive.Description>) {
115
+ return (
116
+ <DialogPrimitive.Description
117
+ data-slot="dialog-description"
118
+ className={cn("text-muted-foreground text-sm", className)}
119
+ {...props}
120
+ />
121
+ );
122
+ }
123
+
124
+ export {
125
+ Dialog,
126
+ DialogClose,
127
+ DialogContent,
128
+ DialogDescription,
129
+ DialogFooter,
130
+ DialogHeader,
131
+ DialogOverlay,
132
+ DialogPortal,
133
+ DialogTitle,
134
+ DialogTrigger,
135
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/drawer.tsx ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { Drawer as DrawerPrimitive } from "vaul";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function Drawer({
9
+ ...props
10
+ }: React.ComponentProps<typeof DrawerPrimitive.Root>) {
11
+ return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
12
+ }
13
+
14
+ function DrawerTrigger({
15
+ ...props
16
+ }: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
17
+ return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
18
+ }
19
+
20
+ function DrawerPortal({
21
+ ...props
22
+ }: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
23
+ return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
24
+ }
25
+
26
+ function DrawerClose({
27
+ ...props
28
+ }: React.ComponentProps<typeof DrawerPrimitive.Close>) {
29
+ return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
30
+ }
31
+
32
+ function DrawerOverlay({
33
+ className,
34
+ ...props
35
+ }: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
36
+ return (
37
+ <DrawerPrimitive.Overlay
38
+ data-slot="drawer-overlay"
39
+ className={cn(
40
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
41
+ className,
42
+ )}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+
48
+ function DrawerContent({
49
+ className,
50
+ children,
51
+ ...props
52
+ }: React.ComponentProps<typeof DrawerPrimitive.Content>) {
53
+ return (
54
+ <DrawerPortal data-slot="drawer-portal">
55
+ <DrawerOverlay />
56
+ <DrawerPrimitive.Content
57
+ data-slot="drawer-content"
58
+ className={cn(
59
+ "group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
60
+ "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
61
+ "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
62
+ "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
63
+ "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
64
+ className,
65
+ )}
66
+ {...props}
67
+ >
68
+ <div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
69
+ {children}
70
+ </DrawerPrimitive.Content>
71
+ </DrawerPortal>
72
+ );
73
+ }
74
+
75
+ function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
76
+ return (
77
+ <div
78
+ data-slot="drawer-header"
79
+ className={cn("flex flex-col gap-1.5 p-4", className)}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
86
+ return (
87
+ <div
88
+ data-slot="drawer-footer"
89
+ className={cn("mt-auto flex flex-col gap-2 p-4", className)}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ function DrawerTitle({
96
+ className,
97
+ ...props
98
+ }: React.ComponentProps<typeof DrawerPrimitive.Title>) {
99
+ return (
100
+ <DrawerPrimitive.Title
101
+ data-slot="drawer-title"
102
+ className={cn("text-foreground font-semibold", className)}
103
+ {...props}
104
+ />
105
+ );
106
+ }
107
+
108
+ function DrawerDescription({
109
+ className,
110
+ ...props
111
+ }: React.ComponentProps<typeof DrawerPrimitive.Description>) {
112
+ return (
113
+ <DrawerPrimitive.Description
114
+ data-slot="drawer-description"
115
+ className={cn("text-muted-foreground text-sm", className)}
116
+ {...props}
117
+ />
118
+ );
119
+ }
120
+
121
+ export {
122
+ Drawer,
123
+ DrawerPortal,
124
+ DrawerOverlay,
125
+ DrawerTrigger,
126
+ DrawerClose,
127
+ DrawerContent,
128
+ DrawerHeader,
129
+ DrawerFooter,
130
+ DrawerTitle,
131
+ DrawerDescription,
132
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/dropdown-menu.tsx ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function DropdownMenu({
10
+ ...props
11
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
12
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
13
+ }
14
+
15
+ function DropdownMenuPortal({
16
+ ...props
17
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
18
+ return (
19
+ <DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
20
+ );
21
+ }
22
+
23
+ function DropdownMenuTrigger({
24
+ ...props
25
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
26
+ return (
27
+ <DropdownMenuPrimitive.Trigger
28
+ data-slot="dropdown-menu-trigger"
29
+ {...props}
30
+ />
31
+ );
32
+ }
33
+
34
+ function DropdownMenuContent({
35
+ className,
36
+ sideOffset = 4,
37
+ ...props
38
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
39
+ return (
40
+ <DropdownMenuPrimitive.Portal>
41
+ <DropdownMenuPrimitive.Content
42
+ data-slot="dropdown-menu-content"
43
+ sideOffset={sideOffset}
44
+ className={cn(
45
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ </DropdownMenuPrimitive.Portal>
51
+ );
52
+ }
53
+
54
+ function DropdownMenuGroup({
55
+ ...props
56
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
57
+ return (
58
+ <DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
59
+ );
60
+ }
61
+
62
+ function DropdownMenuItem({
63
+ className,
64
+ inset,
65
+ variant = "default",
66
+ ...props
67
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
68
+ inset?: boolean;
69
+ variant?: "default" | "destructive";
70
+ }) {
71
+ return (
72
+ <DropdownMenuPrimitive.Item
73
+ data-slot="dropdown-menu-item"
74
+ data-inset={inset}
75
+ data-variant={variant}
76
+ className={cn(
77
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
78
+ className,
79
+ )}
80
+ {...props}
81
+ />
82
+ );
83
+ }
84
+
85
+ function DropdownMenuCheckboxItem({
86
+ className,
87
+ children,
88
+ checked,
89
+ ...props
90
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
91
+ return (
92
+ <DropdownMenuPrimitive.CheckboxItem
93
+ data-slot="dropdown-menu-checkbox-item"
94
+ className={cn(
95
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
96
+ className,
97
+ )}
98
+ checked={checked}
99
+ {...props}
100
+ >
101
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
102
+ <DropdownMenuPrimitive.ItemIndicator>
103
+ <CheckIcon className="size-4" />
104
+ </DropdownMenuPrimitive.ItemIndicator>
105
+ </span>
106
+ {children}
107
+ </DropdownMenuPrimitive.CheckboxItem>
108
+ );
109
+ }
110
+
111
+ function DropdownMenuRadioGroup({
112
+ ...props
113
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
114
+ return (
115
+ <DropdownMenuPrimitive.RadioGroup
116
+ data-slot="dropdown-menu-radio-group"
117
+ {...props}
118
+ />
119
+ );
120
+ }
121
+
122
+ function DropdownMenuRadioItem({
123
+ className,
124
+ children,
125
+ ...props
126
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
127
+ return (
128
+ <DropdownMenuPrimitive.RadioItem
129
+ data-slot="dropdown-menu-radio-item"
130
+ className={cn(
131
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
132
+ className,
133
+ )}
134
+ {...props}
135
+ >
136
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
137
+ <DropdownMenuPrimitive.ItemIndicator>
138
+ <CircleIcon className="size-2 fill-current" />
139
+ </DropdownMenuPrimitive.ItemIndicator>
140
+ </span>
141
+ {children}
142
+ </DropdownMenuPrimitive.RadioItem>
143
+ );
144
+ }
145
+
146
+ function DropdownMenuLabel({
147
+ className,
148
+ inset,
149
+ ...props
150
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
151
+ inset?: boolean;
152
+ }) {
153
+ return (
154
+ <DropdownMenuPrimitive.Label
155
+ data-slot="dropdown-menu-label"
156
+ data-inset={inset}
157
+ className={cn(
158
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
159
+ className,
160
+ )}
161
+ {...props}
162
+ />
163
+ );
164
+ }
165
+
166
+ function DropdownMenuSeparator({
167
+ className,
168
+ ...props
169
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
170
+ return (
171
+ <DropdownMenuPrimitive.Separator
172
+ data-slot="dropdown-menu-separator"
173
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
174
+ {...props}
175
+ />
176
+ );
177
+ }
178
+
179
+ function DropdownMenuShortcut({
180
+ className,
181
+ ...props
182
+ }: React.ComponentProps<"span">) {
183
+ return (
184
+ <span
185
+ data-slot="dropdown-menu-shortcut"
186
+ className={cn(
187
+ "text-muted-foreground ml-auto text-xs tracking-widest",
188
+ className,
189
+ )}
190
+ {...props}
191
+ />
192
+ );
193
+ }
194
+
195
+ function DropdownMenuSub({
196
+ ...props
197
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
198
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
199
+ }
200
+
201
+ function DropdownMenuSubTrigger({
202
+ className,
203
+ inset,
204
+ children,
205
+ ...props
206
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
207
+ inset?: boolean;
208
+ }) {
209
+ return (
210
+ <DropdownMenuPrimitive.SubTrigger
211
+ data-slot="dropdown-menu-sub-trigger"
212
+ data-inset={inset}
213
+ className={cn(
214
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
215
+ className,
216
+ )}
217
+ {...props}
218
+ >
219
+ {children}
220
+ <ChevronRightIcon className="ml-auto size-4" />
221
+ </DropdownMenuPrimitive.SubTrigger>
222
+ );
223
+ }
224
+
225
+ function DropdownMenuSubContent({
226
+ className,
227
+ ...props
228
+ }: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
229
+ return (
230
+ <DropdownMenuPrimitive.SubContent
231
+ data-slot="dropdown-menu-sub-content"
232
+ className={cn(
233
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
234
+ className,
235
+ )}
236
+ {...props}
237
+ />
238
+ );
239
+ }
240
+
241
+ export {
242
+ DropdownMenu,
243
+ DropdownMenuPortal,
244
+ DropdownMenuTrigger,
245
+ DropdownMenuContent,
246
+ DropdownMenuGroup,
247
+ DropdownMenuLabel,
248
+ DropdownMenuItem,
249
+ DropdownMenuCheckboxItem,
250
+ DropdownMenuRadioGroup,
251
+ DropdownMenuRadioItem,
252
+ DropdownMenuSeparator,
253
+ DropdownMenuShortcut,
254
+ DropdownMenuSub,
255
+ DropdownMenuSubTrigger,
256
+ DropdownMenuSubContent,
257
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/form.tsx ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as LabelPrimitive from "@radix-ui/react-label";
5
+ import { Slot } from "@radix-ui/react-slot";
6
+ import {
7
+ Controller,
8
+ FormProvider,
9
+ useFormContext,
10
+ useFormState,
11
+ type ControllerProps,
12
+ type FieldPath,
13
+ type FieldValues,
14
+ } from "react-hook-form";
15
+
16
+ import { cn } from "./utils";
17
+ import { Label } from "./label";
18
+
19
+ const Form = FormProvider;
20
+
21
+ type FormFieldContextValue<
22
+ TFieldValues extends FieldValues = FieldValues,
23
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
24
+ > = {
25
+ name: TName;
26
+ };
27
+
28
+ const FormFieldContext = React.createContext<FormFieldContextValue>(
29
+ {} as FormFieldContextValue,
30
+ );
31
+
32
+ const FormField = <
33
+ TFieldValues extends FieldValues = FieldValues,
34
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
35
+ >({
36
+ ...props
37
+ }: ControllerProps<TFieldValues, TName>) => {
38
+ return (
39
+ <FormFieldContext.Provider value={{ name: props.name }}>
40
+ <Controller {...props} />
41
+ </FormFieldContext.Provider>
42
+ );
43
+ };
44
+
45
+ const useFormField = () => {
46
+ const fieldContext = React.useContext(FormFieldContext);
47
+ const itemContext = React.useContext(FormItemContext);
48
+ const { getFieldState } = useFormContext();
49
+ const formState = useFormState({ name: fieldContext.name });
50
+ const fieldState = getFieldState(fieldContext.name, formState);
51
+
52
+ if (!fieldContext) {
53
+ throw new Error("useFormField should be used within <FormField>");
54
+ }
55
+
56
+ const { id } = itemContext;
57
+
58
+ return {
59
+ id,
60
+ name: fieldContext.name,
61
+ formItemId: `${id}-form-item`,
62
+ formDescriptionId: `${id}-form-item-description`,
63
+ formMessageId: `${id}-form-item-message`,
64
+ ...fieldState,
65
+ };
66
+ };
67
+
68
+ type FormItemContextValue = {
69
+ id: string;
70
+ };
71
+
72
+ const FormItemContext = React.createContext<FormItemContextValue>(
73
+ {} as FormItemContextValue,
74
+ );
75
+
76
+ function FormItem({ className, ...props }: React.ComponentProps<"div">) {
77
+ const id = React.useId();
78
+
79
+ return (
80
+ <FormItemContext.Provider value={{ id }}>
81
+ <div
82
+ data-slot="form-item"
83
+ className={cn("grid gap-2", className)}
84
+ {...props}
85
+ />
86
+ </FormItemContext.Provider>
87
+ );
88
+ }
89
+
90
+ function FormLabel({
91
+ className,
92
+ ...props
93
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
94
+ const { error, formItemId } = useFormField();
95
+
96
+ return (
97
+ <Label
98
+ data-slot="form-label"
99
+ data-error={!!error}
100
+ className={cn("data-[error=true]:text-destructive", className)}
101
+ htmlFor={formItemId}
102
+ {...props}
103
+ />
104
+ );
105
+ }
106
+
107
+ function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
108
+ const { error, formItemId, formDescriptionId, formMessageId } =
109
+ useFormField();
110
+
111
+ return (
112
+ <Slot
113
+ data-slot="form-control"
114
+ id={formItemId}
115
+ aria-describedby={
116
+ !error
117
+ ? `${formDescriptionId}`
118
+ : `${formDescriptionId} ${formMessageId}`
119
+ }
120
+ aria-invalid={!!error}
121
+ {...props}
122
+ />
123
+ );
124
+ }
125
+
126
+ function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
127
+ const { formDescriptionId } = useFormField();
128
+
129
+ return (
130
+ <p
131
+ data-slot="form-description"
132
+ id={formDescriptionId}
133
+ className={cn("text-muted-foreground text-sm", className)}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
140
+ const { error, formMessageId } = useFormField();
141
+ const body = error ? String(error?.message ?? "") : props.children;
142
+
143
+ if (!body) {
144
+ return null;
145
+ }
146
+
147
+ return (
148
+ <p
149
+ data-slot="form-message"
150
+ id={formMessageId}
151
+ className={cn("text-destructive text-sm", className)}
152
+ {...props}
153
+ >
154
+ {body}
155
+ </p>
156
+ );
157
+ }
158
+
159
+ export {
160
+ useFormField,
161
+ Form,
162
+ FormItem,
163
+ FormLabel,
164
+ FormControl,
165
+ FormDescription,
166
+ FormMessage,
167
+ FormField,
168
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/hover-card.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function HoverCard({
9
+ ...props
10
+ }: React.ComponentProps<typeof HoverCardPrimitive.Root>) {
11
+ return <HoverCardPrimitive.Root data-slot="hover-card" {...props} />;
12
+ }
13
+
14
+ function HoverCardTrigger({
15
+ ...props
16
+ }: React.ComponentProps<typeof HoverCardPrimitive.Trigger>) {
17
+ return (
18
+ <HoverCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />
19
+ );
20
+ }
21
+
22
+ function HoverCardContent({
23
+ className,
24
+ align = "center",
25
+ sideOffset = 4,
26
+ ...props
27
+ }: React.ComponentProps<typeof HoverCardPrimitive.Content>) {
28
+ return (
29
+ <HoverCardPrimitive.Portal data-slot="hover-card-portal">
30
+ <HoverCardPrimitive.Content
31
+ data-slot="hover-card-content"
32
+ align={align}
33
+ sideOffset={sideOffset}
34
+ className={cn(
35
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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 z-50 w-64 origin-(--radix-hover-card-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
36
+ className,
37
+ )}
38
+ {...props}
39
+ />
40
+ </HoverCardPrimitive.Portal>
41
+ );
42
+ }
43
+
44
+ export { HoverCard, HoverCardTrigger, HoverCardContent };
BPOM Compliance AI UI Redesign/src/app/components/ui/input-otp.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { OTPInput, OTPInputContext } from "input-otp";
5
+ import { MinusIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function InputOTP({
10
+ className,
11
+ containerClassName,
12
+ ...props
13
+ }: React.ComponentProps<typeof OTPInput> & {
14
+ containerClassName?: string;
15
+ }) {
16
+ return (
17
+ <OTPInput
18
+ data-slot="input-otp"
19
+ containerClassName={cn(
20
+ "flex items-center gap-2 has-disabled:opacity-50",
21
+ containerClassName,
22
+ )}
23
+ className={cn("disabled:cursor-not-allowed", className)}
24
+ {...props}
25
+ />
26
+ );
27
+ }
28
+
29
+ function InputOTPGroup({ className, ...props }: React.ComponentProps<"div">) {
30
+ return (
31
+ <div
32
+ data-slot="input-otp-group"
33
+ className={cn("flex items-center gap-1", className)}
34
+ {...props}
35
+ />
36
+ );
37
+ }
38
+
39
+ function InputOTPSlot({
40
+ index,
41
+ className,
42
+ ...props
43
+ }: React.ComponentProps<"div"> & {
44
+ index: number;
45
+ }) {
46
+ const inputOTPContext = React.useContext(OTPInputContext);
47
+ const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
48
+
49
+ return (
50
+ <div
51
+ data-slot="input-otp-slot"
52
+ data-active={isActive}
53
+ className={cn(
54
+ "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm bg-input-background transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]",
55
+ className,
56
+ )}
57
+ {...props}
58
+ >
59
+ {char}
60
+ {hasFakeCaret && (
61
+ <div className="pointer-events-none absolute inset-0 flex items-center justify-center">
62
+ <div className="animate-caret-blink bg-foreground h-4 w-px duration-1000" />
63
+ </div>
64
+ )}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {
70
+ return (
71
+ <div data-slot="input-otp-separator" role="separator" {...props}>
72
+ <MinusIcon />
73
+ </div>
74
+ );
75
+ }
76
+
77
+ export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
BPOM Compliance AI UI Redesign/src/app/components/ui/input.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+
3
+ import { cn } from "./utils";
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <input
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border px-3 py-1 text-base bg-input-background transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
12
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
13
+ "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
14
+ className,
15
+ )}
16
+ {...props}
17
+ />
18
+ );
19
+ }
20
+
21
+ export { Input };
BPOM Compliance AI UI Redesign/src/app/components/ui/label.tsx ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as LabelPrimitive from "@radix-ui/react-label";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function Label({
9
+ className,
10
+ ...props
11
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
12
+ return (
13
+ <LabelPrimitive.Root
14
+ data-slot="label"
15
+ className={cn(
16
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17
+ className,
18
+ )}
19
+ {...props}
20
+ />
21
+ );
22
+ }
23
+
24
+ export { Label };
BPOM Compliance AI UI Redesign/src/app/components/ui/menubar.tsx ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as MenubarPrimitive from "@radix-ui/react-menubar";
5
+ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function Menubar({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof MenubarPrimitive.Root>) {
13
+ return (
14
+ <MenubarPrimitive.Root
15
+ data-slot="menubar"
16
+ className={cn(
17
+ "bg-background flex h-9 items-center gap-1 rounded-md border p-1 shadow-xs",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ function MenubarMenu({
26
+ ...props
27
+ }: React.ComponentProps<typeof MenubarPrimitive.Menu>) {
28
+ return <MenubarPrimitive.Menu data-slot="menubar-menu" {...props} />;
29
+ }
30
+
31
+ function MenubarGroup({
32
+ ...props
33
+ }: React.ComponentProps<typeof MenubarPrimitive.Group>) {
34
+ return <MenubarPrimitive.Group data-slot="menubar-group" {...props} />;
35
+ }
36
+
37
+ function MenubarPortal({
38
+ ...props
39
+ }: React.ComponentProps<typeof MenubarPrimitive.Portal>) {
40
+ return <MenubarPrimitive.Portal data-slot="menubar-portal" {...props} />;
41
+ }
42
+
43
+ function MenubarRadioGroup({
44
+ ...props
45
+ }: React.ComponentProps<typeof MenubarPrimitive.RadioGroup>) {
46
+ return (
47
+ <MenubarPrimitive.RadioGroup data-slot="menubar-radio-group" {...props} />
48
+ );
49
+ }
50
+
51
+ function MenubarTrigger({
52
+ className,
53
+ ...props
54
+ }: React.ComponentProps<typeof MenubarPrimitive.Trigger>) {
55
+ return (
56
+ <MenubarPrimitive.Trigger
57
+ data-slot="menubar-trigger"
58
+ className={cn(
59
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex items-center rounded-sm px-2 py-1 text-sm font-medium outline-hidden select-none",
60
+ className,
61
+ )}
62
+ {...props}
63
+ />
64
+ );
65
+ }
66
+
67
+ function MenubarContent({
68
+ className,
69
+ align = "start",
70
+ alignOffset = -4,
71
+ sideOffset = 8,
72
+ ...props
73
+ }: React.ComponentProps<typeof MenubarPrimitive.Content>) {
74
+ return (
75
+ <MenubarPortal>
76
+ <MenubarPrimitive.Content
77
+ data-slot="menubar-content"
78
+ align={align}
79
+ alignOffset={alignOffset}
80
+ sideOffset={sideOffset}
81
+ className={cn(
82
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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 z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-md",
83
+ className,
84
+ )}
85
+ {...props}
86
+ />
87
+ </MenubarPortal>
88
+ );
89
+ }
90
+
91
+ function MenubarItem({
92
+ className,
93
+ inset,
94
+ variant = "default",
95
+ ...props
96
+ }: React.ComponentProps<typeof MenubarPrimitive.Item> & {
97
+ inset?: boolean;
98
+ variant?: "default" | "destructive";
99
+ }) {
100
+ return (
101
+ <MenubarPrimitive.Item
102
+ data-slot="menubar-item"
103
+ data-inset={inset}
104
+ data-variant={variant}
105
+ className={cn(
106
+ "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
107
+ className,
108
+ )}
109
+ {...props}
110
+ />
111
+ );
112
+ }
113
+
114
+ function MenubarCheckboxItem({
115
+ className,
116
+ children,
117
+ checked,
118
+ ...props
119
+ }: React.ComponentProps<typeof MenubarPrimitive.CheckboxItem>) {
120
+ return (
121
+ <MenubarPrimitive.CheckboxItem
122
+ data-slot="menubar-checkbox-item"
123
+ className={cn(
124
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
125
+ className,
126
+ )}
127
+ checked={checked}
128
+ {...props}
129
+ >
130
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
131
+ <MenubarPrimitive.ItemIndicator>
132
+ <CheckIcon className="size-4" />
133
+ </MenubarPrimitive.ItemIndicator>
134
+ </span>
135
+ {children}
136
+ </MenubarPrimitive.CheckboxItem>
137
+ );
138
+ }
139
+
140
+ function MenubarRadioItem({
141
+ className,
142
+ children,
143
+ ...props
144
+ }: React.ComponentProps<typeof MenubarPrimitive.RadioItem>) {
145
+ return (
146
+ <MenubarPrimitive.RadioItem
147
+ data-slot="menubar-radio-item"
148
+ className={cn(
149
+ "focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-xs py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
150
+ className,
151
+ )}
152
+ {...props}
153
+ >
154
+ <span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
155
+ <MenubarPrimitive.ItemIndicator>
156
+ <CircleIcon className="size-2 fill-current" />
157
+ </MenubarPrimitive.ItemIndicator>
158
+ </span>
159
+ {children}
160
+ </MenubarPrimitive.RadioItem>
161
+ );
162
+ }
163
+
164
+ function MenubarLabel({
165
+ className,
166
+ inset,
167
+ ...props
168
+ }: React.ComponentProps<typeof MenubarPrimitive.Label> & {
169
+ inset?: boolean;
170
+ }) {
171
+ return (
172
+ <MenubarPrimitive.Label
173
+ data-slot="menubar-label"
174
+ data-inset={inset}
175
+ className={cn(
176
+ "px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
177
+ className,
178
+ )}
179
+ {...props}
180
+ />
181
+ );
182
+ }
183
+
184
+ function MenubarSeparator({
185
+ className,
186
+ ...props
187
+ }: React.ComponentProps<typeof MenubarPrimitive.Separator>) {
188
+ return (
189
+ <MenubarPrimitive.Separator
190
+ data-slot="menubar-separator"
191
+ className={cn("bg-border -mx-1 my-1 h-px", className)}
192
+ {...props}
193
+ />
194
+ );
195
+ }
196
+
197
+ function MenubarShortcut({
198
+ className,
199
+ ...props
200
+ }: React.ComponentProps<"span">) {
201
+ return (
202
+ <span
203
+ data-slot="menubar-shortcut"
204
+ className={cn(
205
+ "text-muted-foreground ml-auto text-xs tracking-widest",
206
+ className,
207
+ )}
208
+ {...props}
209
+ />
210
+ );
211
+ }
212
+
213
+ function MenubarSub({
214
+ ...props
215
+ }: React.ComponentProps<typeof MenubarPrimitive.Sub>) {
216
+ return <MenubarPrimitive.Sub data-slot="menubar-sub" {...props} />;
217
+ }
218
+
219
+ function MenubarSubTrigger({
220
+ className,
221
+ inset,
222
+ children,
223
+ ...props
224
+ }: React.ComponentProps<typeof MenubarPrimitive.SubTrigger> & {
225
+ inset?: boolean;
226
+ }) {
227
+ return (
228
+ <MenubarPrimitive.SubTrigger
229
+ data-slot="menubar-sub-trigger"
230
+ data-inset={inset}
231
+ className={cn(
232
+ "focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-none select-none data-[inset]:pl-8",
233
+ className,
234
+ )}
235
+ {...props}
236
+ >
237
+ {children}
238
+ <ChevronRightIcon className="ml-auto h-4 w-4" />
239
+ </MenubarPrimitive.SubTrigger>
240
+ );
241
+ }
242
+
243
+ function MenubarSubContent({
244
+ className,
245
+ ...props
246
+ }: React.ComponentProps<typeof MenubarPrimitive.SubContent>) {
247
+ return (
248
+ <MenubarPrimitive.SubContent
249
+ data-slot="menubar-sub-content"
250
+ className={cn(
251
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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 z-50 min-w-[8rem] origin-(--radix-menubar-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
252
+ className,
253
+ )}
254
+ {...props}
255
+ />
256
+ );
257
+ }
258
+
259
+ export {
260
+ Menubar,
261
+ MenubarPortal,
262
+ MenubarMenu,
263
+ MenubarTrigger,
264
+ MenubarContent,
265
+ MenubarGroup,
266
+ MenubarSeparator,
267
+ MenubarLabel,
268
+ MenubarItem,
269
+ MenubarShortcut,
270
+ MenubarCheckboxItem,
271
+ MenubarRadioGroup,
272
+ MenubarRadioItem,
273
+ MenubarSub,
274
+ MenubarSubTrigger,
275
+ MenubarSubContent,
276
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/navigation-menu.tsx ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
3
+ import { cva } from "class-variance-authority";
4
+ import { ChevronDownIcon } from "lucide-react";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function NavigationMenu({
9
+ className,
10
+ children,
11
+ viewport = true,
12
+ ...props
13
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
14
+ viewport?: boolean;
15
+ }) {
16
+ return (
17
+ <NavigationMenuPrimitive.Root
18
+ data-slot="navigation-menu"
19
+ data-viewport={viewport}
20
+ className={cn(
21
+ "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
22
+ className,
23
+ )}
24
+ {...props}
25
+ >
26
+ {children}
27
+ {viewport && <NavigationMenuViewport />}
28
+ </NavigationMenuPrimitive.Root>
29
+ );
30
+ }
31
+
32
+ function NavigationMenuList({
33
+ className,
34
+ ...props
35
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
36
+ return (
37
+ <NavigationMenuPrimitive.List
38
+ data-slot="navigation-menu-list"
39
+ className={cn(
40
+ "group flex flex-1 list-none items-center justify-center gap-1",
41
+ className,
42
+ )}
43
+ {...props}
44
+ />
45
+ );
46
+ }
47
+
48
+ function NavigationMenuItem({
49
+ className,
50
+ ...props
51
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
52
+ return (
53
+ <NavigationMenuPrimitive.Item
54
+ data-slot="navigation-menu-item"
55
+ className={cn("relative", className)}
56
+ {...props}
57
+ />
58
+ );
59
+ }
60
+
61
+ const navigationMenuTriggerStyle = cva(
62
+ "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
63
+ );
64
+
65
+ function NavigationMenuTrigger({
66
+ className,
67
+ children,
68
+ ...props
69
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
70
+ return (
71
+ <NavigationMenuPrimitive.Trigger
72
+ data-slot="navigation-menu-trigger"
73
+ className={cn(navigationMenuTriggerStyle(), "group", className)}
74
+ {...props}
75
+ >
76
+ {children}{" "}
77
+ <ChevronDownIcon
78
+ className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
79
+ aria-hidden="true"
80
+ />
81
+ </NavigationMenuPrimitive.Trigger>
82
+ );
83
+ }
84
+
85
+ function NavigationMenuContent({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
89
+ return (
90
+ <NavigationMenuPrimitive.Content
91
+ data-slot="navigation-menu-content"
92
+ className={cn(
93
+ "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
94
+ "group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
95
+ className,
96
+ )}
97
+ {...props}
98
+ />
99
+ );
100
+ }
101
+
102
+ function NavigationMenuViewport({
103
+ className,
104
+ ...props
105
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
106
+ return (
107
+ <div
108
+ className={cn(
109
+ "absolute top-full left-0 isolate z-50 flex justify-center",
110
+ )}
111
+ >
112
+ <NavigationMenuPrimitive.Viewport
113
+ data-slot="navigation-menu-viewport"
114
+ className={cn(
115
+ "origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
116
+ className,
117
+ )}
118
+ {...props}
119
+ />
120
+ </div>
121
+ );
122
+ }
123
+
124
+ function NavigationMenuLink({
125
+ className,
126
+ ...props
127
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
128
+ return (
129
+ <NavigationMenuPrimitive.Link
130
+ data-slot="navigation-menu-link"
131
+ className={cn(
132
+ "data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
133
+ className,
134
+ )}
135
+ {...props}
136
+ />
137
+ );
138
+ }
139
+
140
+ function NavigationMenuIndicator({
141
+ className,
142
+ ...props
143
+ }: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
144
+ return (
145
+ <NavigationMenuPrimitive.Indicator
146
+ data-slot="navigation-menu-indicator"
147
+ className={cn(
148
+ "data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
149
+ className,
150
+ )}
151
+ {...props}
152
+ >
153
+ <div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
154
+ </NavigationMenuPrimitive.Indicator>
155
+ );
156
+ }
157
+
158
+ export {
159
+ NavigationMenu,
160
+ NavigationMenuList,
161
+ NavigationMenuItem,
162
+ NavigationMenuContent,
163
+ NavigationMenuTrigger,
164
+ NavigationMenuLink,
165
+ NavigationMenuIndicator,
166
+ NavigationMenuViewport,
167
+ navigationMenuTriggerStyle,
168
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/pagination.tsx ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react";
2
+ import {
3
+ ChevronLeftIcon,
4
+ ChevronRightIcon,
5
+ MoreHorizontalIcon,
6
+ } from "lucide-react";
7
+
8
+ import { cn } from "./utils";
9
+ import { Button, buttonVariants } from "./button";
10
+
11
+ function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
12
+ return (
13
+ <nav
14
+ role="navigation"
15
+ aria-label="pagination"
16
+ data-slot="pagination"
17
+ className={cn("mx-auto flex w-full justify-center", className)}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ function PaginationContent({
24
+ className,
25
+ ...props
26
+ }: React.ComponentProps<"ul">) {
27
+ return (
28
+ <ul
29
+ data-slot="pagination-content"
30
+ className={cn("flex flex-row items-center gap-1", className)}
31
+ {...props}
32
+ />
33
+ );
34
+ }
35
+
36
+ function PaginationItem({ ...props }: React.ComponentProps<"li">) {
37
+ return <li data-slot="pagination-item" {...props} />;
38
+ }
39
+
40
+ type PaginationLinkProps = {
41
+ isActive?: boolean;
42
+ } & Pick<React.ComponentProps<typeof Button>, "size"> &
43
+ React.ComponentProps<"a">;
44
+
45
+ function PaginationLink({
46
+ className,
47
+ isActive,
48
+ size = "icon",
49
+ ...props
50
+ }: PaginationLinkProps) {
51
+ return (
52
+ <a
53
+ aria-current={isActive ? "page" : undefined}
54
+ data-slot="pagination-link"
55
+ data-active={isActive}
56
+ className={cn(
57
+ buttonVariants({
58
+ variant: isActive ? "outline" : "ghost",
59
+ size,
60
+ }),
61
+ className,
62
+ )}
63
+ {...props}
64
+ />
65
+ );
66
+ }
67
+
68
+ function PaginationPrevious({
69
+ className,
70
+ ...props
71
+ }: React.ComponentProps<typeof PaginationLink>) {
72
+ return (
73
+ <PaginationLink
74
+ aria-label="Go to previous page"
75
+ size="default"
76
+ className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
77
+ {...props}
78
+ >
79
+ <ChevronLeftIcon />
80
+ <span className="hidden sm:block">Previous</span>
81
+ </PaginationLink>
82
+ );
83
+ }
84
+
85
+ function PaginationNext({
86
+ className,
87
+ ...props
88
+ }: React.ComponentProps<typeof PaginationLink>) {
89
+ return (
90
+ <PaginationLink
91
+ aria-label="Go to next page"
92
+ size="default"
93
+ className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
94
+ {...props}
95
+ >
96
+ <span className="hidden sm:block">Next</span>
97
+ <ChevronRightIcon />
98
+ </PaginationLink>
99
+ );
100
+ }
101
+
102
+ function PaginationEllipsis({
103
+ className,
104
+ ...props
105
+ }: React.ComponentProps<"span">) {
106
+ return (
107
+ <span
108
+ aria-hidden
109
+ data-slot="pagination-ellipsis"
110
+ className={cn("flex size-9 items-center justify-center", className)}
111
+ {...props}
112
+ >
113
+ <MoreHorizontalIcon className="size-4" />
114
+ <span className="sr-only">More pages</span>
115
+ </span>
116
+ );
117
+ }
118
+
119
+ export {
120
+ Pagination,
121
+ PaginationContent,
122
+ PaginationLink,
123
+ PaginationItem,
124
+ PaginationPrevious,
125
+ PaginationNext,
126
+ PaginationEllipsis,
127
+ };
BPOM Compliance AI UI Redesign/src/app/components/ui/popover.tsx ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function Popover({
9
+ ...props
10
+ }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
11
+ return <PopoverPrimitive.Root data-slot="popover" {...props} />;
12
+ }
13
+
14
+ function PopoverTrigger({
15
+ ...props
16
+ }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
17
+ return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
18
+ }
19
+
20
+ function PopoverContent({
21
+ className,
22
+ align = "center",
23
+ sideOffset = 4,
24
+ ...props
25
+ }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
26
+ return (
27
+ <PopoverPrimitive.Portal>
28
+ <PopoverPrimitive.Content
29
+ data-slot="popover-content"
30
+ align={align}
31
+ sideOffset={sideOffset}
32
+ className={cn(
33
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
34
+ className,
35
+ )}
36
+ {...props}
37
+ />
38
+ </PopoverPrimitive.Portal>
39
+ );
40
+ }
41
+
42
+ function PopoverAnchor({
43
+ ...props
44
+ }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
45
+ return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
46
+ }
47
+
48
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
BPOM Compliance AI UI Redesign/src/app/components/ui/progress.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function Progress({
9
+ className,
10
+ value,
11
+ ...props
12
+ }: React.ComponentProps<typeof ProgressPrimitive.Root>) {
13
+ return (
14
+ <ProgressPrimitive.Root
15
+ data-slot="progress"
16
+ className={cn(
17
+ "bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
18
+ className,
19
+ )}
20
+ {...props}
21
+ >
22
+ <ProgressPrimitive.Indicator
23
+ data-slot="progress-indicator"
24
+ className="bg-primary h-full w-full flex-1 transition-all"
25
+ style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
26
+ />
27
+ </ProgressPrimitive.Root>
28
+ );
29
+ }
30
+
31
+ export { Progress };
BPOM Compliance AI UI Redesign/src/app/components/ui/radio-group.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
5
+ import { CircleIcon } from "lucide-react";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function RadioGroup({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
13
+ return (
14
+ <RadioGroupPrimitive.Root
15
+ data-slot="radio-group"
16
+ className={cn("grid gap-3", className)}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ function RadioGroupItem({
23
+ className,
24
+ ...props
25
+ }: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
26
+ return (
27
+ <RadioGroupPrimitive.Item
28
+ data-slot="radio-group-item"
29
+ className={cn(
30
+ "border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
31
+ className,
32
+ )}
33
+ {...props}
34
+ >
35
+ <RadioGroupPrimitive.Indicator
36
+ data-slot="radio-group-indicator"
37
+ className="relative flex items-center justify-center"
38
+ >
39
+ <CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
40
+ </RadioGroupPrimitive.Indicator>
41
+ </RadioGroupPrimitive.Item>
42
+ );
43
+ }
44
+
45
+ export { RadioGroup, RadioGroupItem };
BPOM Compliance AI UI Redesign/src/app/components/ui/resizable.tsx ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { GripVerticalIcon } from "lucide-react";
5
+ import * as ResizablePrimitive from "react-resizable-panels";
6
+
7
+ import { cn } from "./utils";
8
+
9
+ function ResizablePanelGroup({
10
+ className,
11
+ ...props
12
+ }: React.ComponentProps<typeof ResizablePrimitive.PanelGroup>) {
13
+ return (
14
+ <ResizablePrimitive.PanelGroup
15
+ data-slot="resizable-panel-group"
16
+ className={cn(
17
+ "flex h-full w-full data-[panel-group-direction=vertical]:flex-col",
18
+ className,
19
+ )}
20
+ {...props}
21
+ />
22
+ );
23
+ }
24
+
25
+ function ResizablePanel({
26
+ ...props
27
+ }: React.ComponentProps<typeof ResizablePrimitive.Panel>) {
28
+ return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
29
+ }
30
+
31
+ function ResizableHandle({
32
+ withHandle,
33
+ className,
34
+ ...props
35
+ }: React.ComponentProps<typeof ResizablePrimitive.PanelResizeHandle> & {
36
+ withHandle?: boolean;
37
+ }) {
38
+ return (
39
+ <ResizablePrimitive.PanelResizeHandle
40
+ data-slot="resizable-handle"
41
+ className={cn(
42
+ "bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90",
43
+ className,
44
+ )}
45
+ {...props}
46
+ >
47
+ {withHandle && (
48
+ <div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
49
+ <GripVerticalIcon className="size-2.5" />
50
+ </div>
51
+ )}
52
+ </ResizablePrimitive.PanelResizeHandle>
53
+ );
54
+ }
55
+
56
+ export { ResizablePanelGroup, ResizablePanel, ResizableHandle };
BPOM Compliance AI UI Redesign/src/app/components/ui/scroll-area.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
5
+
6
+ import { cn } from "./utils";
7
+
8
+ function ScrollArea({
9
+ className,
10
+ children,
11
+ ...props
12
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
13
+ return (
14
+ <ScrollAreaPrimitive.Root
15
+ data-slot="scroll-area"
16
+ className={cn("relative", className)}
17
+ {...props}
18
+ >
19
+ <ScrollAreaPrimitive.Viewport
20
+ data-slot="scroll-area-viewport"
21
+ className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
22
+ >
23
+ {children}
24
+ </ScrollAreaPrimitive.Viewport>
25
+ <ScrollBar />
26
+ <ScrollAreaPrimitive.Corner />
27
+ </ScrollAreaPrimitive.Root>
28
+ );
29
+ }
30
+
31
+ function ScrollBar({
32
+ className,
33
+ orientation = "vertical",
34
+ ...props
35
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
36
+ return (
37
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
38
+ data-slot="scroll-area-scrollbar"
39
+ orientation={orientation}
40
+ className={cn(
41
+ "flex touch-none p-px transition-colors select-none",
42
+ orientation === "vertical" &&
43
+ "h-full w-2.5 border-l border-l-transparent",
44
+ orientation === "horizontal" &&
45
+ "h-2.5 flex-col border-t border-t-transparent",
46
+ className,
47
+ )}
48
+ {...props}
49
+ >
50
+ <ScrollAreaPrimitive.ScrollAreaThumb
51
+ data-slot="scroll-area-thumb"
52
+ className="bg-border relative flex-1 rounded-full"
53
+ />
54
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
55
+ );
56
+ }
57
+
58
+ export { ScrollArea, ScrollBar };
BPOM Compliance AI UI Redesign/src/app/components/ui/select.tsx ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import * as SelectPrimitive from "@radix-ui/react-select";
5
+ import {
6
+ CheckIcon,
7
+ ChevronDownIcon,
8
+ ChevronUpIcon,
9
+ } from "lucide-react";
10
+
11
+ import { cn } from "./utils";
12
+
13
+ function Select({
14
+ ...props
15
+ }: React.ComponentProps<typeof SelectPrimitive.Root>) {
16
+ return <SelectPrimitive.Root data-slot="select" {...props} />;
17
+ }
18
+
19
+ function SelectGroup({
20
+ ...props
21
+ }: React.ComponentProps<typeof SelectPrimitive.Group>) {
22
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />;
23
+ }
24
+
25
+ function SelectValue({
26
+ ...props
27
+ }: React.ComponentProps<typeof SelectPrimitive.Value>) {
28
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />;
29
+ }
30
+
31
+ function SelectTrigger({
32
+ className,
33
+ size = "default",
34
+ children,
35
+ ...props
36
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
37
+ size?: "sm" | "default";
38
+ }) {
39
+ return (
40
+ <SelectPrimitive.Trigger
41
+ data-slot="select-trigger"
42
+ data-size={size}
43
+ className={cn(
44
+ "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-full items-center justify-between gap-2 rounded-md border bg-input-background px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
45
+ className,
46
+ )}
47
+ {...props}
48
+ >
49
+ {children}
50
+ <SelectPrimitive.Icon asChild>
51
+ <ChevronDownIcon className="size-4 opacity-50" />
52
+ </SelectPrimitive.Icon>
53
+ </SelectPrimitive.Trigger>
54
+ );
55
+ }
56
+
57
+ function SelectContent({
58
+ className,
59
+ children,
60
+ position = "popper",
61
+ ...props
62
+ }: React.ComponentProps<typeof SelectPrimitive.Content>) {
63
+ return (
64
+ <SelectPrimitive.Portal>
65
+ <SelectPrimitive.Content
66
+ data-slot="select-content"
67
+ className={cn(
68
+ "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-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 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
69
+ position === "popper" &&
70
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
71
+ className,
72
+ )}
73
+ position={position}
74
+ {...props}
75
+ >
76
+ <SelectScrollUpButton />
77
+ <SelectPrimitive.Viewport
78
+ className={cn(
79
+ "p-1",
80
+ position === "popper" &&
81
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1",
82
+ )}
83
+ >
84
+ {children}
85
+ </SelectPrimitive.Viewport>
86
+ <SelectScrollDownButton />
87
+ </SelectPrimitive.Content>
88
+ </SelectPrimitive.Portal>
89
+ );
90
+ }
91
+
92
+ function SelectLabel({
93
+ className,
94
+ ...props
95
+ }: React.ComponentProps<typeof SelectPrimitive.Label>) {
96
+ return (
97
+ <SelectPrimitive.Label
98
+ data-slot="select-label"
99
+ className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
100
+ {...props}
101
+ />
102
+ );
103
+ }
104
+
105
+ function SelectItem({
106
+ className,
107
+ children,
108
+ ...props
109
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
110
+ return (
111
+ <SelectPrimitive.Item
112
+ data-slot="select-item"
113
+ className={cn(
114
+ "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
115
+ className,
116
+ )}
117
+ {...props}
118
+ >
119
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
120
+ <SelectPrimitive.ItemIndicator>
121
+ <CheckIcon className="size-4" />
122
+ </SelectPrimitive.ItemIndicator>
123
+ </span>
124
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
125
+ </SelectPrimitive.Item>
126
+ );
127
+ }
128
+
129
+ function SelectSeparator({
130
+ className,
131
+ ...props
132
+ }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
133
+ return (
134
+ <SelectPrimitive.Separator
135
+ data-slot="select-separator"
136
+ className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
137
+ {...props}
138
+ />
139
+ );
140
+ }
141
+
142
+ function SelectScrollUpButton({
143
+ className,
144
+ ...props
145
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
146
+ return (
147
+ <SelectPrimitive.ScrollUpButton
148
+ data-slot="select-scroll-up-button"
149
+ className={cn(
150
+ "flex cursor-default items-center justify-center py-1",
151
+ className,
152
+ )}
153
+ {...props}
154
+ >
155
+ <ChevronUpIcon className="size-4" />
156
+ </SelectPrimitive.ScrollUpButton>
157
+ );
158
+ }
159
+
160
+ function SelectScrollDownButton({
161
+ className,
162
+ ...props
163
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
164
+ return (
165
+ <SelectPrimitive.ScrollDownButton
166
+ data-slot="select-scroll-down-button"
167
+ className={cn(
168
+ "flex cursor-default items-center justify-center py-1",
169
+ className,
170
+ )}
171
+ {...props}
172
+ >
173
+ <ChevronDownIcon className="size-4" />
174
+ </SelectPrimitive.ScrollDownButton>
175
+ );
176
+ }
177
+
178
+ export {
179
+ Select,
180
+ SelectContent,
181
+ SelectGroup,
182
+ SelectItem,
183
+ SelectLabel,
184
+ SelectScrollDownButton,
185
+ SelectScrollUpButton,
186
+ SelectSeparator,
187
+ SelectTrigger,
188
+ SelectValue,
189
+ };