@arcote.tech/arc-ds 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/package.json +42 -0
  2. package/src/ds/avatar/avatar.tsx +86 -0
  3. package/src/ds/badge/badge.tsx +61 -0
  4. package/src/ds/bento-card/bento-card.tsx +70 -0
  5. package/src/ds/bento-grid/bento-grid.tsx +52 -0
  6. package/src/ds/box/box.tsx +96 -0
  7. package/src/ds/button/button.tsx +191 -0
  8. package/src/ds/card-modal/card-modal.tsx +161 -0
  9. package/src/ds/display-mode.tsx +32 -0
  10. package/src/ds/ds-provider.tsx +85 -0
  11. package/src/ds/form/field.tsx +124 -0
  12. package/src/ds/form/fields/checkbox-select-field.tsx +326 -0
  13. package/src/ds/form/fields/index.ts +14 -0
  14. package/src/ds/form/fields/search-select-field.tsx +41 -0
  15. package/src/ds/form/fields/select-field.tsx +42 -0
  16. package/src/ds/form/fields/suggestion-list-field.tsx +43 -0
  17. package/src/ds/form/fields/tag-field.tsx +39 -0
  18. package/src/ds/form/fields/text-field.tsx +35 -0
  19. package/src/ds/form/fields/textarea-field.tsx +81 -0
  20. package/src/ds/form/form-part.tsx +79 -0
  21. package/src/ds/form/form.tsx +299 -0
  22. package/src/ds/form/index.tsx +5 -0
  23. package/src/ds/form/message.tsx +14 -0
  24. package/src/ds/input/input.tsx +115 -0
  25. package/src/ds/merge-variants.ts +26 -0
  26. package/src/ds/search-select/search-select.tsx +291 -0
  27. package/src/ds/separator/separator.tsx +26 -0
  28. package/src/ds/suggestion-list/suggestion-list.tsx +406 -0
  29. package/src/ds/tag-list/tag-list.tsx +87 -0
  30. package/src/ds/tooltip/tooltip.tsx +33 -0
  31. package/src/ds/transitions.ts +12 -0
  32. package/src/ds/types.ts +131 -0
  33. package/src/index.ts +115 -0
  34. package/src/layout/drag-handle.tsx +117 -0
  35. package/src/layout/dynamic-slot.tsx +95 -0
  36. package/src/layout/expandable-panel.tsx +57 -0
  37. package/src/layout/layout.tsx +323 -0
  38. package/src/layout/overlay-provider.tsx +103 -0
  39. package/src/layout/overlay.tsx +33 -0
  40. package/src/layout/router.tsx +101 -0
  41. package/src/layout/scroll-nav.tsx +121 -0
  42. package/src/layout/slot-render-context.tsx +14 -0
  43. package/src/layout/sub-nav-shell.tsx +41 -0
  44. package/src/layout/toolbar-expand.tsx +70 -0
  45. package/src/layout/transitions.ts +12 -0
  46. package/src/layout/use-expandable.ts +59 -0
  47. package/src/lib/utils.ts +6 -0
  48. package/src/ui/tooltip.tsx +59 -0
  49. package/tsconfig.json +13 -0
@@ -0,0 +1,406 @@
1
+ import { Input } from "../input/input";
2
+ import { Plus, X, Search, ChevronRight, Pencil } from "lucide-react";
3
+ import { useState, useRef, useEffect, useMemo, useCallback } from "react";
4
+ import type { ReactNode } from "react";
5
+ import { motion, AnimatePresence } from "framer-motion";
6
+
7
+ export interface InitialCloudConfig {
8
+ count: number;
9
+ label?: ReactNode;
10
+ description?: ReactNode;
11
+ }
12
+
13
+ export interface SuggestionListProps<T> {
14
+ items: T[];
15
+ onChange: (items: T[]) => void;
16
+ suggestions: T[];
17
+
18
+ getKey?: (item: T) => string;
19
+ getSearchLabel?: (item: T) => string;
20
+ getIcon?: (item: T) => ReactNode;
21
+
22
+ renderItem?: (item: T, onRemove: () => void) => ReactNode;
23
+ renderSuggestion?: (item: T, isActive: boolean) => ReactNode;
24
+
25
+ allowCustom?: boolean;
26
+ allowDuplicates?: boolean;
27
+ createCustom?: (text: string) => T;
28
+ placeholder?: string;
29
+ max?: number;
30
+ initialCloud?: InitialCloudConfig;
31
+ }
32
+
33
+ const defaultGetKey = <T,>(item: T) => String(item);
34
+ const defaultGetSearchLabel = <T,>(item: T) => String(item);
35
+ const defaultCreateCustom = <T,>(text: string) => text as unknown as T;
36
+
37
+ export function SuggestionList<T>({
38
+ items,
39
+ onChange,
40
+ suggestions,
41
+ getKey = defaultGetKey,
42
+ getSearchLabel = defaultGetSearchLabel,
43
+ getIcon,
44
+ renderItem,
45
+ renderSuggestion,
46
+ allowCustom = true,
47
+ allowDuplicates = false,
48
+ createCustom = defaultCreateCustom,
49
+ placeholder = "Wyszukaj...",
50
+ max,
51
+ initialCloud,
52
+ }: SuggestionListProps<T>) {
53
+ const [inputValue, setInputValue] = useState("");
54
+ const [isEditing, setIsEditing] = useState(false);
55
+ const [activeIndex, setActiveIndex] = useState(0);
56
+ // Once user interacts (adds/removes or clicks "more"), we leave cloud mode permanently
57
+ const [cloudDismissed, setCloudDismissed] = useState(false);
58
+
59
+ const containerRef = useRef<HTMLDivElement>(null);
60
+ const inputRef = useRef<HTMLInputElement>(null);
61
+ const dropdownRef = useRef<HTMLDivElement>(null);
62
+
63
+ const safeItems = Array.isArray(items) ? items : [];
64
+
65
+ const selectedKeys = useMemo(
66
+ () => new Set(safeItems.map(getKey)),
67
+ [safeItems, getKey],
68
+ );
69
+
70
+ const filtered = useMemo(() => {
71
+ const query = inputValue.toLowerCase().trim();
72
+ return suggestions.filter((s) => {
73
+ if (!allowDuplicates && selectedKeys.has(getKey(s))) return false;
74
+ if (!query) return true;
75
+ return getSearchLabel(s).toLowerCase().includes(query);
76
+ });
77
+ }, [suggestions, inputValue, selectedKeys, getKey, getSearchLabel, allowDuplicates]);
78
+
79
+ const showCustomOption =
80
+ allowCustom &&
81
+ inputValue.trim() &&
82
+ !filtered.some(
83
+ (s) => getSearchLabel(s).toLowerCase() === inputValue.trim().toLowerCase(),
84
+ );
85
+
86
+ const totalOptions = filtered.length + (showCustomOption ? 1 : 0);
87
+
88
+ // Show cloud: initial state, no items, not dismissed, config provided
89
+ const showCloud = initialCloud && safeItems.length === 0 && !cloudDismissed && !isEditing;
90
+
91
+ useEffect(() => {
92
+ setActiveIndex((prev) => (totalOptions === 0 ? 0 : Math.min(prev, totalOptions - 1)));
93
+ }, [totalOptions]);
94
+
95
+ useEffect(() => {
96
+ if (!isEditing) return;
97
+ const handler = (e: MouseEvent) => {
98
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
99
+ setIsEditing(false);
100
+ setInputValue("");
101
+ }
102
+ };
103
+ document.addEventListener("mousedown", handler);
104
+ return () => document.removeEventListener("mousedown", handler);
105
+ }, [isEditing]);
106
+
107
+ useEffect(() => {
108
+ if (!isEditing || !dropdownRef.current) return;
109
+ const active = dropdownRef.current.querySelector("[data-active='true']");
110
+ if (active) {
111
+ active.scrollIntoView({ block: "nearest" });
112
+ }
113
+ }, [activeIndex, isEditing]);
114
+
115
+ const addItem = useCallback(
116
+ (item: T) => {
117
+ if (max && safeItems.length >= max) return;
118
+ onChange([...safeItems, item]);
119
+ setInputValue("");
120
+ setActiveIndex(0);
121
+ setCloudDismissed(true);
122
+ requestAnimationFrame(() => inputRef.current?.focus());
123
+ },
124
+ [safeItems, onChange, max],
125
+ );
126
+
127
+ const removeItemByIndex = useCallback(
128
+ (index: number) => {
129
+ onChange(safeItems.filter((_, i) => i !== index));
130
+ },
131
+ [safeItems, onChange],
132
+ );
133
+
134
+ const selectActive = useCallback(() => {
135
+ if (totalOptions === 0) return;
136
+ if (activeIndex < filtered.length) {
137
+ addItem(filtered[activeIndex]);
138
+ } else if (showCustomOption) {
139
+ addItem(createCustom(inputValue.trim()));
140
+ }
141
+ }, [activeIndex, filtered, showCustomOption, addItem, createCustom, inputValue, totalOptions]);
142
+
143
+ const handleKeyDown = (e: React.KeyboardEvent) => {
144
+ switch (e.key) {
145
+ case "ArrowDown":
146
+ e.preventDefault();
147
+ setActiveIndex((prev) => (totalOptions === 0 ? 0 : (prev + 1) % totalOptions));
148
+ break;
149
+ case "ArrowUp":
150
+ e.preventDefault();
151
+ setActiveIndex((prev) => (totalOptions === 0 ? 0 : (prev - 1 + totalOptions) % totalOptions));
152
+ break;
153
+ case "Enter":
154
+ e.preventDefault();
155
+ selectActive();
156
+ break;
157
+ case "Escape":
158
+ e.preventDefault();
159
+ setIsEditing(false);
160
+ setInputValue("");
161
+ break;
162
+ case "Backspace":
163
+ if (!inputValue && safeItems.length > 0) {
164
+ onChange(safeItems.slice(0, -1));
165
+ }
166
+ break;
167
+ }
168
+ };
169
+
170
+ const openEditing = () => {
171
+ if (max && safeItems.length >= max) return;
172
+ setCloudDismissed(true);
173
+ setIsEditing(true);
174
+ setActiveIndex(0);
175
+ requestAnimationFrame(() => inputRef.current?.focus());
176
+ };
177
+
178
+ const isAtMax = max !== undefined && safeItems.length >= max;
179
+
180
+ const defaultRenderItem = (item: T, onRemove: () => void) => (
181
+ <div className="flex w-full items-center gap-3 px-3.5 py-2.5">
182
+ {getIcon && (
183
+ <span className="shrink-0 text-muted-foreground">{getIcon(item)}</span>
184
+ )}
185
+ <span className="flex-1 text-sm">{getSearchLabel(item)}</span>
186
+ <button
187
+ type="button"
188
+ onClick={(e) => {
189
+ e.stopPropagation();
190
+ onRemove();
191
+ }}
192
+ className="shrink-0 rounded-full p-1 text-muted-foreground/50 transition-colors hover:bg-muted hover:text-foreground"
193
+ >
194
+ <X className="h-3.5 w-3.5" />
195
+ </button>
196
+ </div>
197
+ );
198
+
199
+ const defaultRenderSuggestion = (item: T, isActive: boolean) => (
200
+ <div className="flex items-center gap-2.5">
201
+ {getIcon && (
202
+ <span className="shrink-0 text-muted-foreground">{getIcon(item)}</span>
203
+ )}
204
+ <span className={`block truncate text-sm ${isActive ? "text-primary font-medium" : ""}`}>
205
+ {getSearchLabel(item)}
206
+ </span>
207
+ </div>
208
+ );
209
+
210
+ const itemRenderer = renderItem ?? defaultRenderItem;
211
+ const suggestionRenderer = renderSuggestion ?? defaultRenderSuggestion;
212
+
213
+ const addFromCloud = useCallback(
214
+ (item: T) => {
215
+ if (max && safeItems.length >= max) return;
216
+ onChange([...safeItems, item]);
217
+ setInputValue("");
218
+ setActiveIndex(0);
219
+ setCloudDismissed(true);
220
+ setIsEditing(true);
221
+ requestAnimationFrame(() => inputRef.current?.focus());
222
+ },
223
+ [safeItems, onChange, max],
224
+ );
225
+
226
+ // ── Cloud view ──────────────────────────────────────────────
227
+ if (showCloud) {
228
+ const cloudCount = initialCloud.count;
229
+ const availableSuggestions = allowDuplicates
230
+ ? suggestions
231
+ : suggestions.filter((s) => !selectedKeys.has(getKey(s)));
232
+ const visibleSuggestions = availableSuggestions.slice(0, cloudCount);
233
+ const hasMore = availableSuggestions.length > cloudCount;
234
+
235
+ return (
236
+ <motion.div
237
+ initial={{ opacity: 0, y: 8 }}
238
+ animate={{ opacity: 1, y: 0 }}
239
+ transition={{ duration: 0.2 }}
240
+ className="space-y-3"
241
+ >
242
+ {(initialCloud.label || initialCloud.description) && (
243
+ <div className="text-center space-y-1 px-2">
244
+ {initialCloud.label && (
245
+ <p className="text-sm font-medium">{initialCloud.label}</p>
246
+ )}
247
+ {initialCloud.description && (
248
+ <p className="text-xs text-muted-foreground">{initialCloud.description}</p>
249
+ )}
250
+ </div>
251
+ )}
252
+ <div className="flex flex-wrap gap-2 justify-center">
253
+ {visibleSuggestions.map((item, i) => (
254
+ <motion.button
255
+ key={getKey(item)}
256
+ type="button"
257
+ initial={{ opacity: 0, scale: 0.9 }}
258
+ animate={{ opacity: 1, scale: 1 }}
259
+ transition={{ duration: 0.15, delay: i * 0.03 }}
260
+ onClick={() => addFromCloud(item)}
261
+ className="inline-flex items-center gap-1.5 rounded-xl border border-border bg-card px-3 py-1.5 text-sm transition-all hover:border-primary/40 hover:bg-primary/5 hover:text-primary hover:shadow-sm active:scale-95"
262
+ >
263
+ {getIcon && (
264
+ <span className="text-muted-foreground">{getIcon(item)}</span>
265
+ )}
266
+ {getSearchLabel(item)}
267
+ </motion.button>
268
+ ))}
269
+ {(hasMore || allowCustom) && (
270
+ <motion.button
271
+ type="button"
272
+ initial={{ opacity: 0, scale: 0.9 }}
273
+ animate={{ opacity: 1, scale: 1 }}
274
+ transition={{ duration: 0.15, delay: visibleSuggestions.length * 0.03 }}
275
+ onClick={openEditing}
276
+ className="inline-flex items-center gap-1.5 rounded-xl border border-dashed border-muted-foreground/30 px-3 py-1.5 text-sm text-muted-foreground transition-all hover:border-primary/40 hover:bg-primary/5 hover:text-primary active:scale-95"
277
+ >
278
+ {allowCustom ? (
279
+ <>
280
+ <Pencil className="h-3 w-3" />
281
+ Własny
282
+ </>
283
+ ) : (
284
+ <>
285
+ Więcej
286
+ <ChevronRight className="h-3 w-3" />
287
+ </>
288
+ )}
289
+ </motion.button>
290
+ )}
291
+ </div>
292
+ </motion.div>
293
+ );
294
+ }
295
+
296
+ // ── List view ───────────────────────────────────────────────
297
+ return (
298
+ <div ref={containerRef} className="rounded-xl border border-border overflow-hidden">
299
+ {/* Items */}
300
+ <AnimatePresence initial={false}>
301
+ {safeItems.map((item, index) => (
302
+ <motion.div
303
+ key={`${getKey(item)}-${index}`}
304
+ initial={{ height: 0, opacity: 0 }}
305
+ animate={{ height: "auto", opacity: 1 }}
306
+ exit={{ height: 0, opacity: 0 }}
307
+ transition={{ duration: 0.15 }}
308
+ className="overflow-hidden"
309
+ >
310
+ <div className="w-full border-b border-border last:border-b-0">
311
+ {itemRenderer(item, () => removeItemByIndex(index))}
312
+ </div>
313
+ </motion.div>
314
+ ))}
315
+ </AnimatePresence>
316
+
317
+ {/* Placeholder / Input + Dropdown */}
318
+ {!isAtMax && (
319
+ <div className={safeItems.length > 0 ? "border-t border-border" : ""}>
320
+ {isEditing ? (
321
+ <>
322
+ <div className="p-1.5">
323
+ <Input
324
+ ref={inputRef}
325
+ icon={Search}
326
+ size="sm"
327
+ value={inputValue}
328
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
329
+ setInputValue(e.target.value);
330
+ setActiveIndex(0);
331
+ }}
332
+ onKeyDown={handleKeyDown}
333
+ placeholder={placeholder}
334
+ className="border-0 shadow-none focus-visible:ring-0 focus-visible:border-transparent"
335
+ />
336
+ </div>
337
+ <AnimatePresence>
338
+ {totalOptions > 0 && (
339
+ <motion.div
340
+ ref={dropdownRef}
341
+ initial={{ height: 0, opacity: 0 }}
342
+ animate={{ height: "auto", opacity: 1 }}
343
+ exit={{ height: 0, opacity: 0 }}
344
+ transition={{ duration: 0.15 }}
345
+ className="overflow-hidden border-t border-border"
346
+ >
347
+ <div className="max-h-[192px] overflow-y-auto">
348
+ {filtered.map((item, i) => (
349
+ <button
350
+ key={`suggestion-${getKey(item)}-${i}`}
351
+ type="button"
352
+ data-active={i === activeIndex}
353
+ onClick={() => addItem(item)}
354
+ onMouseEnter={() => setActiveIndex(i)}
355
+ className={`flex w-full items-center px-3 py-2 text-left transition-colors ${
356
+ i === activeIndex ? "bg-primary/10" : "hover:bg-muted"
357
+ }`}
358
+ >
359
+ {suggestionRenderer(item, i === activeIndex)}
360
+ </button>
361
+ ))}
362
+ {showCustomOption && (
363
+ <button
364
+ type="button"
365
+ data-active={activeIndex === filtered.length}
366
+ onClick={() => addItem(createCustom(inputValue.trim()))}
367
+ onMouseEnter={() => setActiveIndex(filtered.length)}
368
+ className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors ${
369
+ activeIndex === filtered.length ? "bg-primary/10" : "hover:bg-muted"
370
+ }`}
371
+ >
372
+ <Plus className="h-3.5 w-3.5 text-muted-foreground" />
373
+ <span>
374
+ Dodaj <span className="font-medium text-primary">"{inputValue.trim()}"</span>
375
+ </span>
376
+ </button>
377
+ )}
378
+ </div>
379
+ </motion.div>
380
+ )}
381
+ </AnimatePresence>
382
+ </>
383
+ ) : (
384
+ <button
385
+ type="button"
386
+ onClick={openEditing}
387
+ className="flex w-full items-center gap-2 px-3 py-2.5 text-sm text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
388
+ >
389
+ <Plus className="h-3.5 w-3.5" />
390
+ {placeholder}
391
+ </button>
392
+ )}
393
+ </div>
394
+ )}
395
+
396
+ {/* Counter */}
397
+ {max && (
398
+ <div className="border-t border-border px-3 py-1.5">
399
+ <p className="text-xs text-muted-foreground">
400
+ {safeItems.length}/{max}
401
+ </p>
402
+ </div>
403
+ )}
404
+ </div>
405
+ );
406
+ }
@@ -0,0 +1,87 @@
1
+ import { Input } from "../input/input";
2
+ import { Plus, X } from "lucide-react";
3
+ import { useState } from "react";
4
+ import type { ReactNode } from "react";
5
+
6
+ export interface TagListProps {
7
+ tags: string[];
8
+ onChange: (tags: string[]) => void;
9
+ placeholder?: string;
10
+ max?: number;
11
+ addLabel?: ReactNode;
12
+ }
13
+
14
+ export function TagList({ tags, onChange, placeholder, max, addLabel = "Dodaj" }: TagListProps) {
15
+ const [input, setInput] = useState("");
16
+
17
+ const addTag = () => {
18
+ const trimmed = input.trim();
19
+ if (!trimmed || tags.includes(trimmed)) return;
20
+ if (max && tags.length >= max) return;
21
+ onChange([...tags, trimmed]);
22
+ setInput("");
23
+ };
24
+
25
+ const removeTag = (tag: string) => {
26
+ onChange(tags.filter((t) => t !== tag));
27
+ };
28
+
29
+ const handleKeyDown = (e: React.KeyboardEvent) => {
30
+ if (e.key === "Enter") {
31
+ e.preventDefault();
32
+ addTag();
33
+ }
34
+ if (e.key === "Backspace" && !input && tags.length > 0) {
35
+ onChange(tags.slice(0, -1));
36
+ }
37
+ };
38
+
39
+ const isAtMax = max !== undefined && tags.length >= max;
40
+
41
+ return (
42
+ <div className="space-y-2">
43
+ {tags.length > 0 && (
44
+ <div className="flex flex-wrap gap-2">
45
+ {tags.map((tag) => (
46
+ <span
47
+ key={tag}
48
+ className="group inline-flex items-center gap-1.5 rounded-lg bg-primary/10 border border-primary/20 px-3 py-1.5 text-sm font-medium text-primary transition-colors hover:bg-primary/15"
49
+ >
50
+ {tag}
51
+ <button
52
+ onClick={() => removeTag(tag)}
53
+ className="rounded-full p-0.5 hover:bg-primary/20 transition-colors"
54
+ >
55
+ <X className="h-3 w-3" />
56
+ </button>
57
+ </span>
58
+ ))}
59
+ </div>
60
+ )}
61
+ {!isAtMax && (
62
+ <div className="flex gap-2">
63
+ <Input
64
+ value={input}
65
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInput(e.target.value)}
66
+ onKeyDown={handleKeyDown}
67
+ placeholder={placeholder}
68
+ className="flex-1"
69
+ />
70
+ <button
71
+ onClick={addTag}
72
+ disabled={!input.trim()}
73
+ className="flex items-center gap-1.5 rounded-xl border border-border bg-card px-3 py-2 text-sm font-medium text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-40 disabled:pointer-events-none"
74
+ >
75
+ <Plus className="h-4 w-4" />
76
+ {addLabel}
77
+ </button>
78
+ </div>
79
+ )}
80
+ {max && (
81
+ <p className="text-xs text-muted-foreground">
82
+ {tags.length}/{max}
83
+ </p>
84
+ )}
85
+ </div>
86
+ );
87
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ Tooltip as ShadcnTooltip,
3
+ TooltipContent,
4
+ TooltipTrigger,
5
+ } from "../../ui/tooltip";
6
+ import type { TooltipProps } from "../types";
7
+
8
+ /**
9
+ * Tooltip — DS wrapper nad shadcn Tooltip.
10
+ *
11
+ * Jeśli `content` jest falsy, renderuje children bez wrappera.
12
+ * Nadpisywalny przez DesignSystemProvider.
13
+ */
14
+ export function Tooltip({
15
+ children,
16
+ content,
17
+ side = "bottom",
18
+ className,
19
+ }: TooltipProps) {
20
+ if (!content) return children;
21
+
22
+ return (
23
+ <ShadcnTooltip>
24
+ <TooltipTrigger asChild>{children}</TooltipTrigger>
25
+ <TooltipContent
26
+ side={side}
27
+ className={className ?? "max-w-52 text-center"}
28
+ >
29
+ {content}
30
+ </TooltipContent>
31
+ </ShadcnTooltip>
32
+ );
33
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * DS layout transition presets.
3
+ * Jedno miejsce do tuningu animacji — wszystkie layout komponenty importują stąd.
4
+ */
5
+ export const dsTransitions = {
6
+ /** Szybki spring — expand/collapse paneli, nav overflow. */
7
+ snappy: { type: "spring" as const, stiffness: 500, damping: 35 },
8
+ /** Łagodniejszy spring — layout morphing, Box. */
9
+ smooth: { type: "spring" as const, stiffness: 400, damping: 30 },
10
+ /** Prosty fade — overlay. */
11
+ fade: { duration: 0.15 },
12
+ } as const;
@@ -0,0 +1,131 @@
1
+ import type { ComponentType, ReactElement, ReactNode } from "react";
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Display Modes — uniwersalne layout hints
5
+ // ---------------------------------------------------------------------------
6
+
7
+ export type DisplayMode = "default" | "compact" | "minimal" | "expanded";
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Component overrides — pełna zamiana komponentu
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /** Mapa nazw komponentów DS → ich implementacje. */
14
+ export interface DSComponentMap {
15
+ Box: ComponentType<BoxProps>;
16
+ Button: ComponentType<ButtonProps>;
17
+ Tooltip: ComponentType<TooltipProps>;
18
+ Badge: ComponentType<BadgeProps>;
19
+ Separator: ComponentType<SeparatorProps>;
20
+ Avatar: ComponentType<AvatarProps>;
21
+ Input: ComponentType<InputProps>;
22
+ }
23
+
24
+ /** Partial override — moduł nadpisuje tylko wybrane komponenty. */
25
+ export type DSComponentOverrides = Partial<DSComponentMap>;
26
+
27
+ // ---------------------------------------------------------------------------
28
+ // Variant overrides — merge z domyślnymi CVA wariantami
29
+ // ---------------------------------------------------------------------------
30
+
31
+ export type CVAVariantOverride = Record<string, Record<string, string>>;
32
+
33
+ export type DSVariantOverrides = Partial<
34
+ Record<keyof DSComponentMap, CVAVariantOverride>
35
+ >;
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Button
39
+ // ---------------------------------------------------------------------------
40
+
41
+ export interface ButtonProps {
42
+ icon?: ComponentType<{ className?: string }>;
43
+ label?: ReactNode;
44
+ right?: ReactNode;
45
+ tooltip?: ReactNode;
46
+ isActive?: boolean;
47
+ displayMode?: DisplayMode;
48
+ variant?:
49
+ | "default"
50
+ | "secondary"
51
+ | "ghost"
52
+ | "outline"
53
+ | "destructive"
54
+ | "link";
55
+ size?: "default" | "sm" | "xs" | "lg" | "icon" | "icon-sm" | "icon-xs";
56
+ className?: string;
57
+ onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
58
+ disabled?: boolean;
59
+ asChild?: boolean;
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Tooltip
64
+ // ---------------------------------------------------------------------------
65
+
66
+ export interface TooltipProps {
67
+ children: ReactElement;
68
+ content?: ReactNode;
69
+ side?: "top" | "bottom" | "left" | "right";
70
+ className?: string;
71
+ }
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Badge
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export interface BadgeProps {
78
+ children?: ReactNode;
79
+ variant?: "default" | "secondary" | "destructive" | "outline" | "ghost";
80
+ display?: DisplayMode;
81
+ asChild?: boolean;
82
+ className?: string;
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // Separator
87
+ // ---------------------------------------------------------------------------
88
+
89
+ export interface SeparatorProps {
90
+ orientation?: "horizontal" | "vertical";
91
+ decorative?: boolean;
92
+ className?: string;
93
+ }
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Avatar
97
+ // ---------------------------------------------------------------------------
98
+
99
+ export interface AvatarProps {
100
+ src?: string;
101
+ fallback?: ReactNode;
102
+ size?: "default" | "sm" | "lg" | "xs";
103
+ display?: DisplayMode;
104
+ className?: string;
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Input
109
+ // ---------------------------------------------------------------------------
110
+
111
+ export interface InputProps extends Omit<
112
+ React.InputHTMLAttributes<HTMLInputElement>,
113
+ "size"
114
+ > {
115
+ icon?: ComponentType<{ className?: string }>;
116
+ iconSide?: "left" | "right";
117
+ size?: "default" | "sm" | "xs" | "lg";
118
+ display?: DisplayMode;
119
+ }
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // Box
123
+ // ---------------------------------------------------------------------------
124
+
125
+ export interface BoxProps {
126
+ variant?: "default" | "ghost";
127
+ displayMode?: DisplayMode;
128
+ layoutId?: string;
129
+ className?: string;
130
+ children?: ReactNode;
131
+ }