ar-design 0.4.52 → 0.4.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ .ar-gantt-chart {
2
+ background-color: var(--gray-700);
3
+ fill: var(--gray-700);
4
+ border-radius: var(--border-radius-lg);
5
+ font-family: var(--system);
6
+ box-shadow: 0px 10px 15px -5px rgba(var(--black-rgb), 0.1);
7
+
8
+ > .header {
9
+ fill: var(--white);
10
+ height: 75px;
11
+
12
+ > .title-group {
13
+ > .title {
14
+ fill: var(--gray-700);
15
+ font-size: 16px;
16
+ font-weight: bold;
17
+ }
18
+
19
+ > .title-description {
20
+ fill: var(--gray-500);
21
+ font-size: 13.28px;
22
+ }
23
+ }
24
+ }
25
+
26
+ > .body {
27
+ > .time-and-bars {
28
+ transition: transform 250ms ease-in-out;
29
+
30
+ &.dragging {
31
+ transition: none !important;
32
+ }
33
+ }
34
+
35
+ > .left-axis {
36
+ fill: var(--white);
37
+
38
+ > .label-list {
39
+ > .label-row {
40
+ > .label-text {
41
+ fill: var(--gray-700);
42
+ font-size: 14px;
43
+ dominant-baseline: central;
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ }
@@ -14,6 +14,7 @@
14
14
  }
15
15
 
16
16
  .ar-alert ul {
17
+ list-style: none;
17
18
  display: flex;
18
19
  flex-direction: column;
19
20
  gap: 0.5rem 0;
@@ -13,7 +13,7 @@
13
13
 
14
14
  li {
15
15
  list-style: none;
16
- height: 2rem;
16
+ height: auto; /* DÜZELTİLMEDİ: Sabit 2rem kaldırıldı, li'nin alt menüyle büyümesi sağlandı */
17
17
 
18
18
  > ul.submenu {
19
19
  display: grid;
@@ -46,7 +46,7 @@
46
46
  align-items: center;
47
47
  gap: 0.5rem;
48
48
  width: 100%;
49
- height: inherit;
49
+ height: 2rem; /* DÜZELTİLMEDİ: inherit yerine ana eleman yüksekliği buraya sabitlendi */
50
50
  padding: 0.3rem;
51
51
  white-space: nowrap;
52
52
  cursor: pointer;
@@ -0,0 +1,4 @@
1
+ import React from "react";
2
+ import "../../../assets/css/components/charts/gantt/styles.css";
3
+ declare const Gantt: React.FC<{}>;
4
+ export default Gantt;
@@ -0,0 +1,193 @@
1
+ import React, { useCallback, useRef, useState } from "react";
2
+ import "../../../assets/css/components/charts/gantt/styles.css";
3
+ const tasks = [
4
+ {
5
+ id: "ffa5c782-f482-49ed-acd5-f5cab8b2c39e",
6
+ name: "Turksat2LoV10000",
7
+ start: "2026-04-22T09:30:00Z",
8
+ end: "2026-04-22T14:30:00Z",
9
+ },
10
+ {
11
+ id: "ff68947f-b2c2-4621-b5db-fd57702d037a",
12
+ name: "Turksat2LoV10000",
13
+ start: "2026-08-31T09:30:00Z",
14
+ end: "2026-08-31T14:30:00Z",
15
+ },
16
+ {
17
+ id: "ff2e87f5-9b3c-470d-bda9-dfea1851680c",
18
+ name: "IRD 6",
19
+ start: "2026-04-15T10:30:00Z",
20
+ end: "2026-04-15T17:30:00Z",
21
+ },
22
+ ];
23
+ const Gantt = () => {
24
+ // refs
25
+ const _svg = useRef(null);
26
+ const _mapIsMoveField = useRef(null);
27
+ // states
28
+ const [scrollX, setScrollX] = useState(0);
29
+ const [isDragging, setIsDragging] = useState(false);
30
+ const [startX, setStartX] = useState(0);
31
+ // variables
32
+ const TIMELINE = generateGanttTimeline(tasks);
33
+ const SVG_WIDTH = "100%";
34
+ const SVG_HEIGHT = 2400;
35
+ const HEADER_HEIGHT = 75;
36
+ const STROKE_WIDTH = 0.5;
37
+ const LABEL_WIDTH = 120;
38
+ const ROW_HEIGHT = 45;
39
+ let PREVMATCHMONT = 0;
40
+ let PREVMATCHDAY = 0;
41
+ // methods
42
+ const handleMouseDown = (e) => {
43
+ if (e.button !== 0)
44
+ return;
45
+ setIsDragging(true);
46
+ // Tıklanılan ilk X pozisyonu ile mevcut kaydırma değerini hafızaya alıyoruz.
47
+ setStartX(e.clientX + Math.abs(scrollX));
48
+ };
49
+ const handleMouseMove = useCallback((e) => {
50
+ if (!isDragging)
51
+ return;
52
+ let newScrollX = startX - e.clientX;
53
+ if (newScrollX < 0)
54
+ newScrollX = 0;
55
+ setScrollX(newScrollX);
56
+ }, [isDragging, startX]);
57
+ const handleMouseUpOrLeave = () => {
58
+ const svgRect = _svg.current?.getBoundingClientRect();
59
+ const mapIsMoveFieldRect = _mapIsMoveField.current?.getBoundingClientRect();
60
+ if (svgRect && mapIsMoveFieldRect) {
61
+ if (svgRect.right > mapIsMoveFieldRect.right) {
62
+ const targetLeft = svgRect.width - mapIsMoveFieldRect.width;
63
+ setScrollX(targetLeft - LABEL_WIDTH);
64
+ }
65
+ }
66
+ setIsDragging(false);
67
+ };
68
+ return (React.createElement("svg", { ref: _svg, xmlns: "http://www.w3.org/2000/svg",
69
+ // viewBox={`0 0 ${SVG_WIDTH} ${SVG_HEIGHT}`}
70
+ width: SVG_WIDTH, height: SVG_HEIGHT, className: "ar-gantt-chart" },
71
+ React.createElement("g", { className: "header", width: "100%" },
72
+ React.createElement("rect", { x: 0, y: 0, width: "100%", height: HEADER_HEIGHT }),
73
+ React.createElement("g", { transform: `translate(25, ${HEADER_HEIGHT / 2})`, className: "title-group" },
74
+ React.createElement("text", { className: "title" }, "Ar Gantt Chart"),
75
+ React.createElement("text", { y: 20, className: "title-description" }, "Daily View")),
76
+ React.createElement("line", { x1: "0", y1: HEADER_HEIGHT, x2: "100%", y2: HEADER_HEIGHT, opacity: 0.25, stroke: "var(--black)", strokeWidth: 1 })),
77
+ React.createElement("g", { className: "body", transform: `translate(0, ${HEADER_HEIGHT + ROW_HEIGHT * 2})` },
78
+ React.createElement("g", { className: `${isDragging ? "dragging" : "no-dragging"} time-and-bars`, transform: `translate(${LABEL_WIDTH - Math.abs(scrollX)}, 0)`, onMouseDown: handleMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUpOrLeave, onMouseLeave: handleMouseUpOrLeave, style: { cursor: isDragging ? "grabbing" : "grab", userSelect: "none" } },
79
+ React.createElement("g", null,
80
+ TIMELINE.days.map((day, index) => {
81
+ const xPos = (index + 1) * 60;
82
+ const nextDay = new Date(day.date);
83
+ nextDay.setDate(nextDay.getDate() + 1);
84
+ const isLastDayOfMonth = day.date.getMonth() !== nextDay.getMonth();
85
+ const isSunday = day.date.getDay() === 0;
86
+ if (!isSunday && !isLastDayOfMonth)
87
+ return;
88
+ const currentMonthNum = day.date.getMonth();
89
+ const currentDayNum = day.date.getDate();
90
+ const dayDiff = currentDayNum - (currentMonthNum !== PREVMATCHMONT ? 0 : PREVMATCHDAY);
91
+ // Bir sonraki turda kullanabilmek için hafızayı güncelliyoruz.
92
+ PREVMATCHMONT = currentMonthNum;
93
+ PREVMATCHDAY = currentDayNum;
94
+ return (React.createElement("g", { key: index },
95
+ React.createElement("line", { x1: xPos, y1: -ROW_HEIGHT * 2, x2: xPos, y2: 0, opacity: 0.25, stroke: "var(--white)", strokeWidth: STROKE_WIDTH }),
96
+ React.createElement("text", { x: xPos - (dayDiff * 60) / 2, y: -ROW_HEIGHT * 2 + ROW_HEIGHT / 2, fill: "var(--white)", fontSize: "12", textAnchor: "middle", dominantBaseline: "central" }, day.date.toLocaleDateString("tr-TR", { month: "long" }))));
97
+ }),
98
+ TIMELINE.days.map((day, index) => {
99
+ const xPos = (index + 1) * 60; // 01:00 -> 60px, 02:00 -> 120px...
100
+ return (React.createElement("g", { key: index },
101
+ React.createElement("text", { x: (index + 1) * 60 - 30, y: -ROW_HEIGHT + ROW_HEIGHT / 2, fill: day.isWeekend ? "var(--red-500)" : "var(--white)", fontSize: "12", textAnchor: "middle", dominantBaseline: "central" },
102
+ String(day.number).padStart(2, "0"),
103
+ " ",
104
+ day.name),
105
+ React.createElement("line", { x1: xPos, y1: 0, x2: xPos, y2: SVG_HEIGHT, opacity: 0.25, stroke: "var(--white)", strokeWidth: STROKE_WIDTH, strokeDasharray: "5,5" })));
106
+ }),
107
+ React.createElement("line", { x1: 0, y1: -ROW_HEIGHT, x2: TIMELINE.days.length * 60, y2: -ROW_HEIGHT, opacity: 0.25, stroke: "var(--white)", strokeWidth: 1 }),
108
+ React.createElement("line", { x1: 0, y1: 0, x2: TIMELINE.days.length * 60, y2: 0, opacity: 0.25, stroke: "var(--white)", strokeWidth: 1 })),
109
+ React.createElement("g", { transform: `translate(0, 0)` }, tasks.map((task, index) => {
110
+ const taskStart = new Date(task.start);
111
+ const taskEnd = new Date(task.end);
112
+ // 1. Proje başlangıcından bu görevin başlangıcına kadar geçen toplam milisaniye
113
+ const diffMsFromStart = taskStart.getTime() - Number(TIMELINE.timelineStart?.getTime());
114
+ // Milisaniyeyi saate çeviriyoruz
115
+ const hoursFromStart = diffMsFromStart / (1000 * 60 * 60);
116
+ // X Konumu: Geçen toplam saati, saat başına düşen piksel genişliğiyle çarpıyoruz
117
+ const x = hoursFromStart * (60 / 24);
118
+ // 2. Görevin toplam süresini saat cinsinden buluyoruz
119
+ const durationHours = (taskEnd.getTime() - taskStart.getTime()) / (1000 * 60 * 60);
120
+ // Genişlik (Width): Süreyi saat başına düşen pikselle çarpıyoruz
121
+ const width = durationHours * (60 / 24);
122
+ // 3. Dikey Konumlandırma (Senin mevcut mantığın)
123
+ const height = ROW_HEIGHT / 1.5;
124
+ const y = index * ROW_HEIGHT + height / 4;
125
+ return (React.createElement("g", { key: task.id },
126
+ React.createElement("rect", { x: x, y: y, width: width, height: height, fill: "#000", rx: 3 }),
127
+ width > 60 && (React.createElement("text", { x: x + width / 2, y: y + height / 2 + 4, fontSize: 12, fontWeight: "600", fill: "var(--black)", textAnchor: "middle" // Metni X koordinatına göre tam ortalar
128
+ }, task.name))));
129
+ })),
130
+ React.createElement("rect", { ref: _mapIsMoveField, x: 0, y: -ROW_HEIGHT, width: TIMELINE.days.length * 60, height: SVG_HEIGHT, fill: "transparent", pointerEvents: "all" })),
131
+ React.createElement("g", { className: "left-axis" },
132
+ React.createElement("rect", { x: 0, y: -ROW_HEIGHT * 2, width: LABEL_WIDTH, height: SVG_HEIGHT }),
133
+ React.createElement("g", { className: "label-list" }, tasks.map((task, index) => {
134
+ const y = index * ROW_HEIGHT;
135
+ return (React.createElement("g", { key: task.id, className: "label-row" },
136
+ React.createElement("text", { x: "10", y: y + ROW_HEIGHT / 2, className: "label-text" }, task.name),
137
+ React.createElement("line", { x1: "0", y1: y + ROW_HEIGHT, x2: LABEL_WIDTH, y2: y + ROW_HEIGHT, stroke: "var(--black)", strokeWidth: "0.5", opacity: 0.25 }),
138
+ React.createElement("line", { x1: LABEL_WIDTH, y1: y + ROW_HEIGHT, x2: 24 * 60, y2: y + ROW_HEIGHT, opacity: 0.25, stroke: "var(--white)", strokeWidth: STROKE_WIDTH, strokeDasharray: "5,5" })));
139
+ }))))));
140
+ };
141
+ const generateGanttTimeline = (tasks) => {
142
+ if (!tasks || tasks.length === 0) {
143
+ return { minDate: null, maxDate: null, months: [], days: [] };
144
+ }
145
+ // 1. En erken başlangıç ve en geç bitiş tarihlerini buluyoruz.
146
+ let minDate = new Date(tasks[0].start);
147
+ let maxDate = new Date(tasks[0].end);
148
+ tasks.forEach((task) => {
149
+ const start = new Date(task.start);
150
+ const end = new Date(task.end);
151
+ if (start < minDate)
152
+ minDate = start;
153
+ if (end > maxDate)
154
+ maxDate = end;
155
+ });
156
+ // Şemanın düzgün görünmesi için başlangıcı o ayın 1'ine,
157
+ // bitişi ise o ayın son gününe yuvarlamak yerleşim açısından daha iyi sonuç verir.
158
+ const startTimeline = new Date(minDate.getFullYear(), minDate.getMonth(), 1);
159
+ const endTimeline = new Date(maxDate.getFullYear(), maxDate.getMonth() + 1, 0); // Ayın son günü
160
+ const months = [];
161
+ const days = [];
162
+ // 2. Günleri ve Ayları döngüyle oluşturuyoruz.
163
+ const current = new Date(startTimeline);
164
+ while (current <= endTimeline) {
165
+ // Gün listesini doldur.
166
+ const dayOfWeek = current.getDay();
167
+ days.push({
168
+ date: new Date(current),
169
+ number: current.getDate(),
170
+ name: current.toLocaleDateString("tr-TR", { weekday: "short" }), // Pzt, Sal...
171
+ isWeekend: dayOfWeek === 0 || dayOfWeek === 6, // Hafta sonu kontrolü (Gantt'ta boyamak için).
172
+ });
173
+ // Ay listesini doldur. (Eğer listede bu ay henüz yoksa ekle)
174
+ const year = current.getFullYear();
175
+ const number = current.getMonth();
176
+ const name = current.toLocaleDateString("tr-TR", { month: "long" }); // Ocak, Şubat...
177
+ const monthExists = months.some((m) => m.year === year && m.number === number);
178
+ if (!monthExists) {
179
+ // O ayın toplam gün sayısını bulalım.
180
+ const totalDays = new Date(year, number + 1, 0).getDate();
181
+ months.push({ year, number, name, totalDays });
182
+ }
183
+ // Bir sonraki güne geç.
184
+ current.setDate(current.getDate() + 1);
185
+ }
186
+ return {
187
+ timelineStart: startTimeline,
188
+ timelineEnd: endTimeline,
189
+ months, // Üst zaman bandı için (Örn: Ocak, Şubat)
190
+ days, // Alt zaman bandı için (Örn: 1, 2, 3... veya haftalık kırılım için)
191
+ };
192
+ };
193
+ export default Gantt;
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import React, { useEffect, useRef, useState } from "react";
2
+ import React, { useEffect, useMemo, useRef, useState } from "react";
3
3
  import Input from "..";
4
4
  import Select from "../../select";
5
5
  import PHONE from "../../../../libs/infrastructure/shared/PHONE";
@@ -9,6 +9,9 @@ const Phone = ({ variant, color, options, values, onSelected, validation, ...att
9
9
  // states
10
10
  const [_value, setValue] = useState("");
11
11
  const [selected, setSelected] = useState(undefined);
12
+ // options referans kararlılığı için JSON key
13
+ // eslint-disable-next-line react-hooks/exhaustive-deps
14
+ const optionsKey = useMemo(() => JSON.stringify(options), [options]);
12
15
  // methods
13
16
  const handleClick = () => {
14
17
  const input = _input.current;
@@ -21,31 +24,18 @@ const Phone = ({ variant, color, options, values, onSelected, validation, ...att
21
24
  useEffect(() => {
22
25
  setValue(values.value);
23
26
  setSelected(options?.find((option) => option.value === values.option));
24
- }, [values]);
27
+ // eslint-disable-next-line react-hooks/exhaustive-deps
28
+ }, [values.value, values.option, optionsKey]);
25
29
  return (React.createElement("div", { className: "ar-input-phone-wrapper" },
26
30
  options && (React.createElement(Select, { style: { width: 130 }, variant: "outlined", color: "light", options: options, value: selected, onChange: (option) => {
27
31
  onSelected?.(option);
28
32
  setSelected(option);
29
- } })),
33
+ }, validation: validation, config: { validation: { text: "hidden" } } })),
30
34
  React.createElement(Input, { ref: _input, ...attributes, ...(!options ? { style: { borderRadius: "var(--border-radius-sm)" } } : {}), variant: variant, color: color, value: PHONE.FormatByMask(selected?.value, _value), type: "tel", inputMode: "decimal", onChange: (event) => {
31
35
  if (attributes.disabled)
32
36
  return;
33
- (() => {
34
- if (attributes.onChange) {
35
- const { id, name, value, type, dataset } = event.target;
36
- attributes.onChange({
37
- ...event,
38
- target: {
39
- ...event.target,
40
- id: id,
41
- name: name,
42
- value: value,
43
- type: type,
44
- dataset: dataset,
45
- },
46
- });
47
- }
48
- })();
37
+ setValue(event.target.value);
38
+ attributes.onChange?.(event);
49
39
  }, onClick: handleClick, validation: validation })));
50
40
  };
51
41
  export default Phone;
@@ -28,5 +28,8 @@ export type Props = {
28
28
  readOnly?: boolean;
29
29
  config?: {
30
30
  clear?: boolean;
31
+ validation?: {
32
+ text: "visible" | "hidden";
33
+ };
31
34
  };
32
35
  } & (IMultiple | ISingle) & IVariant & IColors & IBorder & IIcon & ISize & IUpperCase & IValidation & IDisabled;
@@ -1,12 +1,12 @@
1
1
  "use client";
2
- import React, { useEffect, useRef, useState } from "react";
2
+ import React, { useEffect, useRef, useState, useMemo } from "react";
3
3
  import Input from "../input";
4
4
  import "../../../assets/css/components/form/select/styles.css";
5
5
  import Chip from "../../data-display/chip";
6
6
  import Checkbox from "../checkbox";
7
7
  import Utils from "../../../libs/infrastructure/shared/Utils";
8
8
  import ReactDOM from "react-dom";
9
- const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }, style, options, value, onChange, onSearch, onClick, onCreate, multiple, placeholder, validation, upperCase, disabled, readOnly, config = { clear: true }, }) => {
9
+ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }, style, options, value, onChange, onSearch, onClick, onCreate, multiple, placeholder, validation, upperCase, disabled, readOnly, config = { clear: true, validation: { text: "visible" } }, }) => {
10
10
  const _selectionClassName = ["selections"];
11
11
  // refs
12
12
  const _arSelect = useRef(null);
@@ -16,16 +16,18 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
16
16
  const _options = useRef(null);
17
17
  const _optionItems = useRef([]);
18
18
  const _searchField = useRef(null);
19
- // const _searchTimeOut = useRef<NodeJS.Timeout | null>(null);
20
19
  let _otoFocus = useRef(null).current;
21
20
  let _navigationIndex = useRef(0);
22
21
  // states
23
22
  const [optionsOpen, setOptionsOpen] = useState(false);
24
- const [filteredOptions, setFilteredOptions] = useState([]);
23
+ const [filteredOptions, setFilteredOptions] = useState(() => options ?? []);
25
24
  const [isSearchTextEqual, setIsSearchTextEqual] = useState(false);
26
25
  const [searchText, setSearchText] = useState("");
27
26
  const [singleInputText, setSingleInputText] = useState("");
28
27
  const [navigationIndex, setNavigationIndex] = useState(0);
28
+ // options referans kararlılığı için JSON key
29
+ // eslint-disable-next-line react-hooks/exhaustive-deps
30
+ const optionsKey = useMemo(() => JSON.stringify(options), [options]);
29
31
  _selectionClassName.push(...Utils.GetClassName(variant, undefined, validation?.text ? "red" : "light", border, undefined, undefined, undefined));
30
32
  // methods
31
33
  const handleClickOutSide = (event) => {
@@ -38,22 +40,14 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
38
40
  const optionItems = _optionItems.current.filter((optionItem) => optionItem !== null);
39
41
  if (key === "ArrowUp" || key === "ArrowLeft") {
40
42
  setNavigationIndex((prev) => {
41
- let result = 0;
42
- if (prev > 0)
43
- result = prev - 1;
44
- if (prev === 0)
45
- result = optionItems.length - 1;
43
+ const result = prev > 0 ? prev - 1 : optionItems.length - 1;
46
44
  _navigationIndex.current = result;
47
45
  return result;
48
46
  });
49
47
  }
50
48
  else if (key === "ArrowDown" || key === "ArrowRight") {
51
49
  setNavigationIndex((prev) => {
52
- let result = 0;
53
- if (prev === optionItems.length - 1)
54
- result = 0;
55
- if (prev < optionItems.length - 1)
56
- result = prev + 1;
50
+ const result = prev === optionItems.length - 1 ? 0 : prev + 1;
57
51
  _navigationIndex.current = result;
58
52
  return result;
59
53
  });
@@ -63,17 +57,10 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
63
57
  return;
64
58
  optionItems[_navigationIndex.current]?.click();
65
59
  }
66
- else if (key === "Escape")
60
+ else if (key === "Escape") {
67
61
  setOptionsOpen(false);
62
+ }
68
63
  };
69
- // const handleSearch = (value: string) => {
70
- // if (searchText.length === 0 || !onSearch) return;
71
- // if (_searchTimeOut.current) clearTimeout(_searchTimeOut.current);
72
- // _searchTimeOut.current = setTimeout(() => {
73
- // setSearchText(value);
74
- // onSearch(value);
75
- // }, 750);
76
- // };
77
64
  const handlePosition = () => {
78
65
  if (_options.current) {
79
66
  const optionRect = _options.current.getBoundingClientRect();
@@ -134,7 +121,6 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
134
121
  React.createElement("span", { className: "plus" }, "+"),
135
122
  singleInputText.length !== 0 ? singleInputText : searchText))));
136
123
  };
137
- // Özel büyük harfe dönüştürme işlevi.
138
124
  const convertToUpperCase = (str) => {
139
125
  return str
140
126
  .replace(/ş/g, "S")
@@ -151,45 +137,37 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
151
137
  .replace(/Ü/g, "U")
152
138
  .replace(/[a-z]/g, (match) => match.toUpperCase());
153
139
  };
154
- // useEffects
140
+ // value değiştiğinde input metnini güncelle
155
141
  useEffect(() => {
156
142
  if (multiple)
157
143
  setSearchText("");
158
144
  else
159
145
  setSingleInputText(value?.text ?? "");
160
- }, [value]);
161
- useEffect(() => setFilteredOptions(options), [options]);
146
+ }, [value, multiple]);
147
+ // options dışarıdan değiştiğinde filtreyi güncelle (searchText ile uyumlu)
148
+ useEffect(() => {
149
+ setFilteredOptions((options ?? []).filter((option) => option.text.toLocaleLowerCase().includes(searchText.toLocaleLowerCase())));
150
+ setIsSearchTextEqual((options ?? []).some((option) => option.text.toLocaleLowerCase() === searchText.toLocaleLowerCase()));
151
+ // optionsKey kullanarak referans karşılaştırması yerine içerik karşılaştırması yapıyoruz
152
+ // eslint-disable-next-line react-hooks/exhaustive-deps
153
+ }, [optionsKey]);
154
+ // options paneli açılıp kapandığında
162
155
  useEffect(() => {
163
156
  if (optionsOpen) {
164
157
  setTimeout(() => handlePosition(), 0);
165
- if (!multiple) {
166
- // const optionItems = _optionItems.current.filter((optionItem) => optionItem !== null);
167
- // Scroll ile kaydırma işlemi
168
- // if (_options.current) {
169
- // const list = _options.current.querySelector("ul") as HTMLUListElement;
170
- // list.scrollTo({
171
- // top: optionItems[_selectedItemIndex.current].offsetTop,
172
- // behavior: "smooth",
173
- // });
174
- // }
175
- if (_singleInput.current) {
176
- setSingleInputText("");
177
- _singleInput.current.placeholder = value?.text || `${validation ? "* " : ""}${placeholder ?? ""}` || "";
178
- }
158
+ if (!multiple && _singleInput.current) {
159
+ setSingleInputText("");
160
+ _singleInput.current.placeholder = value?.text || `${validation ? "* " : ""}${placeholder ?? ""}` || "";
179
161
  }
180
- // Options açıldıktan 100ms sonra arama kutusuna otomatik olarak focus oluyor.
181
162
  _otoFocus = setTimeout(() => {
182
163
  if (_searchField.current)
183
164
  _searchField.current.focus();
184
165
  }, 250);
185
- // Options paneli için olay dinleyileri ekleniyor.
186
- window.addEventListener("blur", () => setOptionsOpen(false));
166
+ window.addEventListener("blur", handleBlur);
187
167
  document.addEventListener("click", handleClickOutSide);
188
168
  document.addEventListener("keydown", handleKeys);
189
169
  }
190
170
  else {
191
- // Options paneli kapanma süresi 250ms.
192
- // 300ms sonra temizlenmesinin sebebi kapanırken birder veriler gerliyor ve panel yüksekliği artıyor.
193
171
  setTimeout(() => setSearchText(""), 300);
194
172
  if (multiple) {
195
173
  if (_searchField.current)
@@ -204,54 +182,51 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
204
182
  }
205
183
  return () => {
206
184
  _otoFocus && clearTimeout(_otoFocus);
207
- window.removeEventListener("blur", () => setOptionsOpen(false));
185
+ window.removeEventListener("blur", handleBlur);
208
186
  document.removeEventListener("click", handleClickOutSide);
209
187
  document.removeEventListener("keydown", handleKeys);
210
188
  };
189
+ // eslint-disable-next-line react-hooks/exhaustive-deps
211
190
  }, [optionsOpen]);
191
+ // arama metni değiştiğinde filtrele
212
192
  useEffect(() => {
213
193
  if (searchText.length > 0 && onSearch) {
214
194
  onSearch(searchText);
215
195
  }
216
196
  else {
217
- // Arama kriterlerine uygun olan değerleri bir state e gönderiyoruz.
218
- setFilteredOptions(options?.filter((option) => {
197
+ setFilteredOptions((options ?? []).filter((option) => {
219
198
  if (!optionsOpen)
220
- return option;
199
+ return true;
221
200
  return option.text.toLocaleLowerCase().includes(searchText.toLocaleLowerCase());
222
201
  }));
223
- setIsSearchTextEqual(options?.some((option) => {
202
+ setIsSearchTextEqual((options ?? []).some((option) => {
224
203
  if (!optionsOpen)
225
- return option;
226
- return option.text.toLocaleLowerCase() == searchText.toLocaleLowerCase();
204
+ return false;
205
+ return option.text.toLocaleLowerCase() === searchText.toLocaleLowerCase();
227
206
  }));
228
207
  }
229
- // Arama yapılması durumunda değerleri sıfırla.
230
208
  setNavigationIndex(0);
231
209
  _navigationIndex.current = 0;
232
- // Arama yapılması durumunda arama sonuçlarından ilk olan değeri işaretle.
233
- const optionItems = _optionItems.current.filter((optionItem) => optionItem !== null);
210
+ const optionItems = _optionItems.current.filter((item) => item !== null);
234
211
  optionItems[_navigationIndex.current]?.classList.add("navigate-with-arrow-keys");
235
- // Yeniden konumlandır.
236
212
  setTimeout(() => handlePosition(), 0);
213
+ // eslint-disable-next-line react-hooks/exhaustive-deps
237
214
  }, [searchText]);
215
+ // klavye navigasyonu highlight
238
216
  useEffect(() => {
239
- // Seçilen öğeye 'navigate-with-arrow-keys' sınıfını ekle
240
217
  _optionItems.current
241
- .filter((optionItem) => optionItem !== null)
218
+ .filter((item) => item !== null)
242
219
  .forEach((item, index) => {
243
220
  if (index === navigationIndex) {
244
221
  item?.classList.add("navigate-with-arrow-keys");
245
- item.scrollIntoView({
246
- behavior: "smooth",
247
- block: "nearest",
248
- });
222
+ item.scrollIntoView({ behavior: "smooth", block: "nearest" });
249
223
  }
250
224
  else {
251
225
  item?.classList.remove("navigate-with-arrow-keys");
252
226
  }
253
227
  });
254
228
  }, [navigationIndex]);
229
+ const handleBlur = () => setOptionsOpen(false);
255
230
  return (React.createElement("div", { ref: _arSelect, className: "ar-select-wrapper" },
256
231
  React.createElement("div", { ref: _multipleInput, className: "ar-select" },
257
232
  multiple ? (React.createElement("div", { className: "wrapper" },
@@ -270,11 +245,9 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
270
245
  )`,
271
246
  },
272
247
  }
273
- : {}), className: _selectionClassName.map((c) => c).join(" "), onClick: () => {
248
+ : {}), className: _selectionClassName.join(" "), onClick: () => {
274
249
  onClick && onClick();
275
- (() => {
276
- setOptionsOpen((prev) => !prev);
277
- })();
250
+ setOptionsOpen((prev) => !prev);
278
251
  } },
279
252
  React.createElement("div", { className: "items" }, value.map((_value, index) => (React.createElement(Chip, { key: index, variant: status?.selected?.variant || "filled", color: status?.selected?.color || status?.color, text: _value.text }))))),
280
253
  React.createElement("span", { ref: _placeholder, className: `placeholder ${value.length > 0 ? "visible" : "hidden"}`, onClick: () => {
@@ -282,13 +255,9 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
282
255
  setOptionsOpen((prev) => !prev);
283
256
  } },
284
257
  validation ? "* " : "",
285
- placeholder))) : (React.createElement(Input, { ref: _singleInput, style: { ...style, paddingRight: config.clear === false ? "1.5rem" : "3.5rem" }, variant: variant, color: !Utils.IsNullOrEmpty(validation?.text) ? "red" : color,
286
- // status={!Utils.IsNullOrEmpty(validation?.text) ? "danger" : status}
287
- border: { radius: border.radius }, value: singleInputText, onClick: () => {
258
+ placeholder))) : (React.createElement(Input, { ref: _singleInput, style: { ...style, paddingRight: config.clear === false ? "1.5rem" : "3.5rem" }, variant: variant, color: !Utils.IsNullOrEmpty(validation?.text) ? "red" : color, border: { radius: border.radius }, value: singleInputText, onClick: () => {
288
259
  onClick && onClick();
289
- (() => {
290
- setOptionsOpen((prev) => !prev);
291
- })();
260
+ setOptionsOpen((prev) => !prev);
292
261
  }, onChange: (event) => {
293
262
  !optionsOpen && setOptionsOpen(true);
294
263
  if (upperCase)
@@ -298,7 +267,10 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
298
267
  if (event.key === "Enter")
299
268
  return;
300
269
  setSearchText(event.currentTarget.value);
301
- }, placeholder: placeholder, validation: validation, disabled: disabled, readOnly: readOnly })),
270
+ }, placeholder: placeholder, validation: {
271
+ ...validation,
272
+ text: config.validation?.text === "visible" ? validation?.text : "",
273
+ }, disabled: disabled, readOnly: readOnly })),
302
274
  React.createElement("div", { className: "buttons" },
303
275
  config?.clear === true && (React.createElement("span", { className: `button-clear ${!disabled && (multiple ? value.length > 0 : value) ? "opened" : "closed"}`, onClick: (event) => {
304
276
  if (disabled)
@@ -313,7 +285,7 @@ const Select = ({ variant = "outlined", status, color, border = { radius: "sm" }
313
285
  event.stopPropagation();
314
286
  setOptionsOpen((prev) => !prev);
315
287
  } })),
316
- multiple && validation && React.createElement("span", { className: "validation" }, validation.text)),
288
+ multiple && validation && config.validation?.text === "visible" && (React.createElement("span", { className: "validation" }, validation.text))),
317
289
  !disabled &&
318
290
  optionsOpen &&
319
291
  ReactDOM.createPortal(React.createElement("div", { ref: _options, className: "ar-select-options" },
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ import Select from "./components/form/select";
10
10
  import Switch from "./components/form/switch";
11
11
  import TextEditor from "./components/form/text-editor";
12
12
  import Upload from "./components/form/upload";
13
+ import Gantt from "./components/charts/gantt";
13
14
  import Calendar from "./components/data-display/calendar";
14
15
  import Card from "./components/data-display/card";
15
16
  import Chip from "./components/data-display/chip";
@@ -34,4 +35,4 @@ import Pagination from "./components/navigation/pagination";
34
35
  import Steps from "./components/navigation/steps";
35
36
  import Grid from "./components/data-display/grid-system";
36
37
  import Layout from "./components/layout";
37
- export { Button, ButtonAction, ButtonGroup, Checkbox, DatePicker, Input, Radio, Select, Switch, TextEditor, Upload, Calendar, Card, Chip, Diagram, Divider, DnD, KanbanBoard, Paper, SyntaxHighlighter, Table, Tabs, Typography, Alert, Drawer, Modal, Popover, Progress, Tooltip, Breadcrumb, Menu, Pagination, Steps, Grid, Layout, };
38
+ export { Button, ButtonAction, ButtonGroup, Checkbox, DatePicker, Input, Radio, Select, Switch, TextEditor, Upload, Gantt, Calendar, Card, Chip, Diagram, Divider, DnD, KanbanBoard, Paper, SyntaxHighlighter, Table, Tabs, Typography, Alert, Drawer, Modal, Popover, Progress, Tooltip, Breadcrumb, Menu, Pagination, Steps, Grid, Layout, };
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import Select from "./components/form/select";
11
11
  import Switch from "./components/form/switch";
12
12
  import TextEditor from "./components/form/text-editor";
13
13
  import Upload from "./components/form/upload";
14
+ // Charts
15
+ import Gantt from "./components/charts/gantt";
14
16
  // Data Display
15
17
  import Calendar from "./components/data-display/calendar";
16
18
  import Card from "./components/data-display/card";
@@ -42,6 +44,8 @@ import Layout from "./components/layout";
42
44
  export {
43
45
  // Form Elements
44
46
  Button, ButtonAction, ButtonGroup, Checkbox, DatePicker, Input, Radio, Select, Switch, TextEditor, Upload,
47
+ // Charts
48
+ Gantt,
45
49
  // Data Display
46
50
  Calendar, Card, Chip, Diagram, Divider, DnD, KanbanBoard, Paper, SyntaxHighlighter, Table, Tabs, Typography,
47
51
  // Feedback
@@ -1,147 +1,151 @@
1
1
  "use client";
2
- import { useEffect, useRef, useState } from "react";
2
+ import { useEffect, useRef, useState, useCallback, useMemo } from "react";
3
3
  import Utils from "../../../infrastructure/shared/Utils";
4
4
  const useValidation = function (data, params, step) {
5
- // refs
6
5
  const _errors = useRef({});
7
- // states
8
6
  const [errors, setErrors] = useState({});
9
7
  const [submit, setSubmit] = useState(false);
10
- // methods
11
- const onSubmit = (callback) => {
12
- setSubmit(true);
13
- queueMicrotask(() => {
14
- let result = true;
15
- if (!data || Object.keys(data).length === 0 || params.length === 0)
16
- result = false;
17
- if (step) {
18
- const filteredErrors = Object.fromEntries(Object.entries(_errors.current)
19
- .filter(([key]) => key.startsWith(`${step}_`))
20
- .map(([key, value]) => [key.replace(/^\d+_/, ""), String(value)]));
21
- if (Object.keys(filteredErrors).length > 0)
22
- result = false;
8
+ const paramsKey = useMemo(() => JSON.stringify(params), [params]);
9
+ const setError = useCallback((key, message, paramStep, trackByValue) => {
10
+ let _key = paramStep ? `${paramStep}_${key}` : key;
11
+ if (trackByValue !== undefined)
12
+ _key = `${_key}_${trackByValue}`;
13
+ _errors.current[_key] = message;
14
+ }, []);
15
+ const paramsShape = useCallback((param, value, trackByValue) => {
16
+ const vLength = value ? value.length : 0;
17
+ const getKey = (subkey) => {
18
+ if (!subkey)
19
+ return param.key;
20
+ const levels = subkey.split(".");
21
+ return levels[levels.length - 1];
22
+ };
23
+ const handleValidation = (key, s) => {
24
+ // Zorunluluk Kontrolleri (Geliştirildi).
25
+ if (s.type === "required" && Utils.IsNullOrEmpty(value)) {
26
+ setError(key, s.message, param.step, trackByValue);
27
+ return;
23
28
  }
24
- else {
25
- if (Object.keys(_errors.current).length > 0)
26
- result = false;
29
+ const validationTypes = ["phone", "email", "iban", "account-number"];
30
+ if (validationTypes.includes(s.type) && Utils.IsNullOrEmpty(value)) {
31
+ setError(key, s.message, param.step, trackByValue);
32
+ return;
27
33
  }
28
- callback(result);
34
+ // Uzunluk Kontrolleri.
35
+ if (s.type === "minimum" && vLength < s.value) {
36
+ setError(key, Utils.StringFormat(s.message, s.value), param.step, trackByValue);
37
+ }
38
+ if (s.type === "maximum" && vLength > s.value) {
39
+ setError(key, Utils.StringFormat(s.message, s.value), param.step, trackByValue);
40
+ }
41
+ // Format (Regex) Kontrolleri (Sadece değer doluysa çalışır).
42
+ if (value && !Utils.IsNullOrEmpty(value)) {
43
+ const phoneRegex = /^\d{7,14}$/;
44
+ const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
45
+ const ibanRegex = /^TR\d{24}$/;
46
+ const accountNumberRegex = /^\d{6,16}$/;
47
+ if (s.type === "phone" && !phoneRegex.test(value.replace(/\D/g, ""))) {
48
+ setError(key, s.message, param.step, trackByValue);
49
+ }
50
+ if (s.type === "email" && !emailRegex.test(value)) {
51
+ setError(key, s.message, param.step, trackByValue);
52
+ }
53
+ if (s.type === "iban" && !ibanRegex.test(value.replace(/\s/g, ""))) {
54
+ setError(key, s.message, param.step, trackByValue);
55
+ }
56
+ if (s.type === "account-number" && !accountNumberRegex.test(value)) {
57
+ setError(key, s.message, param.step, trackByValue);
58
+ }
59
+ }
60
+ };
61
+ param.shape?.forEach((s) => {
62
+ const key = getKey(param.subkey);
63
+ // ÖNEMLİ KONTROL: Eğer bu alan için zaten bir hata basıldıysa,
64
+ // shape içindeki sonraki kuralları kontrol etme, döngüdeki bu adımı atla!
65
+ let currentKey = param.step ? `${param.step}_${key}` : key;
66
+ if (trackByValue !== undefined)
67
+ currentKey = `${currentKey}_${trackByValue}`;
68
+ // Hafızada zaten bu alanın hatası var, sonrakileri çalıştırma.
69
+ if (_errors.current[currentKey])
70
+ return;
71
+ if (param.where) {
72
+ if (param.where(data)) {
73
+ setError(param.subkey ? key : param.key, s.message, param.step, trackByValue);
74
+ }
75
+ return;
76
+ }
77
+ handleValidation(key, s);
29
78
  });
30
- };
31
- const setError = (key, message, step, trackByValue) => {
32
- let _key = step ? `${step}_${key}` : key;
33
- if (trackByValue !== undefined)
34
- _key = `${_key}_${trackByValue}`;
35
- setErrors((prev) => ({ ...prev, [_key]: message }));
36
- _errors.current = { ..._errors.current, [_key]: message };
37
- };
38
- const handleParams = (param) => {
79
+ }, [data, setError]);
80
+ const handleParams = useCallback((param) => {
39
81
  const value = data[param.key];
40
- // Eğer subkey varsa, onunla işlem yapılacak.
41
82
  if (param.subkey) {
42
83
  if (param.subkey.includes(".")) {
43
- // Subkey içinde birden fazla seviye varsa, her seviyeye inerek değer alınacak.
44
84
  const levels = param.subkey.split(".");
45
85
  let currentData = value;
46
86
  for (const key of levels) {
47
- // Eğer currentData null ya da undefined ise, işlem sonlandırılır.
48
87
  if (!currentData) {
49
88
  paramsShape(param, currentData);
50
89
  return;
51
90
  }
52
- // Seviye bazında ilerleyerek veriye ulaşılır.
53
91
  currentData = currentData[key];
54
92
  }
55
- // Son seviyedeki veriyi paramsShape fonksiyonuna gönder.
56
93
  paramsShape(param, currentData);
57
94
  }
58
95
  else {
59
96
  if (Array.isArray(value)) {
60
- // Eğer value bir dizi ise ve subkey sadece bir seviye ise,
61
- // dizinin her bir elemanına subkey uygulanabilir.
62
97
  const extractedValues = value.map((item) => ({
63
98
  value: item?.[param.subkey],
64
99
  trackByValue: item?.trackByValue,
65
100
  }));
66
- // Elde edilen değerler topluca paramsShape'e gönderilebilir ya da başka bir şekilde işlenebilir.
67
- extractedValues.map((extractedValue) => paramsShape(param, extractedValue.value, extractedValue.trackByValue));
101
+ extractedValues.forEach((extractedValue) => paramsShape(param, extractedValue.value, extractedValue.trackByValue));
68
102
  }
69
103
  else {
70
- // Value bir obje ise, subkey doğrudan kullanılır.
71
104
  paramsShape(param, value?.[param.subkey]);
72
105
  }
73
106
  }
74
107
  }
75
108
  else {
76
- // Eğer subkey yoksa, doğrudan param.key üzerinden işlem yapılır.
77
109
  paramsShape(param, value);
78
110
  }
111
+ }, [data, paramsShape]);
112
+ // Tüm kuralları senkron çalıştırıp sonucu dönen fonksiyon.
113
+ const validateAll = useCallback(() => {
114
+ _errors.current = {};
115
+ params.forEach((param) => handleParams(param));
116
+ setErrors({ ..._errors.current });
117
+ if (!data || Object.keys(data).length === 0 || params.length === 0)
118
+ return false;
119
+ if (step) {
120
+ const filteredErrors = Object.keys(_errors.current).filter((k) => k.startsWith(`${step}_`));
121
+ return filteredErrors.length === 0;
122
+ }
123
+ return Object.keys(_errors.current).length === 0;
124
+ }, [data, paramsKey, step, handleParams]);
125
+ const onSubmit = (callback) => {
126
+ setSubmit(true);
127
+ const isValid = validateAll();
128
+ callback(isValid);
79
129
  };
80
- const paramsShape = (param, value, trackByValue) => {
81
- const vLenght = value ? value.length : 0;
82
- const getKey = (subkey) => {
83
- if (!subkey)
84
- return param.key;
85
- const levels = subkey.split(".");
86
- return levels[levels.length - 1];
87
- };
88
- const handleValidation = (key, s) => {
89
- if (s.type === "required" && Utils.IsNullOrEmpty(value)) {
90
- setError(key, s.message, param.step, trackByValue);
91
- }
92
- if (s.type === "minimum" && vLenght < s.value) {
93
- setError(key, Utils.StringFormat(s.message, s.value), param.step, trackByValue);
94
- }
95
- if (s.type === "maximum" && vLenght > s.value) {
96
- setError(key, Utils.StringFormat(s.message, s.value), param.step, trackByValue);
97
- }
98
- // Regexes
99
- // const phoneRegex = /^((\+90|0)?([2-5]\d{2})\d{7}|\+[1-9]\d{7,14})$/;
100
- const phoneRegex = /^\d{7,14}$/;
101
- const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
102
- const ibanRegex = /^TR\d{24}$/;
103
- const accountNumberRegex = /^\d{6,16}$/;
104
- if (s.type === "phone" && value && !phoneRegex.test(value.replace(/\D/g, ""))) {
105
- setError(key, s.message, param.step, trackByValue);
106
- }
107
- if (s.type === "email" && value && !emailRegex.test(value)) {
108
- setError(key, s.message, param.step, trackByValue);
109
- }
110
- if (s.type === "iban" && value && !ibanRegex.test(value.replace(/\s/g, ""))) {
111
- setError(key, s.message, param.step, trackByValue);
112
- }
113
- if (s.type === "account-number" && value && !accountNumberRegex.test(value)) {
114
- setError(key, s.message, param.step, trackByValue);
115
- }
116
- };
117
- param.shape?.forEach((s) => {
118
- const key = getKey(param.subkey);
119
- if (param.where) {
120
- if (param.where(data)) {
121
- setError(param.subkey ? key : param.key, s.message, param.step, trackByValue);
122
- }
123
- }
124
- else {
125
- handleValidation(key, s);
126
- }
127
- });
128
- };
129
- // useEffects
130
130
  useEffect(() => {
131
- setErrors({});
132
- _errors.current = {};
133
- if (!submit)
131
+ if (!submit) {
132
+ _errors.current = {};
133
+ setErrors({});
134
134
  return;
135
- params.forEach((param) => handleParams(param));
136
- }, [submit, data]);
135
+ }
136
+ validateAll();
137
+ // eslint-disable-next-line react-hooks/exhaustive-deps
138
+ }, [data, submit, paramsKey]);
139
+ // Adım filtresi.
140
+ const filteredErrors = step
141
+ ? Object.fromEntries(Object.entries(errors)
142
+ .filter(([key]) => key.startsWith(`${step}_`))
143
+ .map(([key, value]) => [key.replace(/^\d+_/, ""), String(value)]))
144
+ : errors;
137
145
  return {
138
146
  onSubmit,
139
147
  setSubmit,
140
- errors: step
141
- ? Object.fromEntries(Object.entries(errors)
142
- .filter(([key]) => key.startsWith(`${step}_`))
143
- .map(([key, value]) => [key.replace(/^\d+_/, ""), String(value)]))
144
- : errors,
148
+ errors: filteredErrors,
145
149
  };
146
150
  };
147
151
  export default useValidation;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ar-design",
3
- "version": "0.4.52",
3
+ "version": "0.4.54",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",