@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,161 @@
1
+ import { Button } from "../button/button";
2
+ import { motion, AnimatePresence } from "framer-motion";
3
+ import { X } from "lucide-react";
4
+ import { useEffect } from "react";
5
+ import type { ReactNode } from "react";
6
+ import type { ArcObjectAny } from "@arcote.tech/arc";
7
+ import { Form } from "../form/form";
8
+ import type { FormProps } from "../form/form";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // CardModal — generic modal shell, no buttons
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export interface CardModalProps {
15
+ layoutId: string;
16
+ isOpen: boolean;
17
+ title: ReactNode;
18
+ onClose: () => void;
19
+ children: ReactNode;
20
+ }
21
+
22
+ export function CardModal({
23
+ layoutId,
24
+ isOpen,
25
+ title,
26
+ onClose,
27
+ children,
28
+ }: CardModalProps) {
29
+ useEffect(() => {
30
+ if (!isOpen) return;
31
+ const handler = (e: KeyboardEvent) => {
32
+ if (e.key === "Escape") onClose();
33
+ };
34
+ window.addEventListener("keydown", handler);
35
+ return () => window.removeEventListener("keydown", handler);
36
+ }, [isOpen, onClose]);
37
+
38
+ return (
39
+ <AnimatePresence>
40
+ {isOpen && (
41
+ <>
42
+ <motion.div
43
+ key="card-modal-backdrop"
44
+ initial={{ opacity: 0 }}
45
+ animate={{ opacity: 1 }}
46
+ exit={{ opacity: 0 }}
47
+ transition={{ duration: 0.2 }}
48
+ className="fixed inset-0 z-50 bg-black/40 backdrop-blur-sm"
49
+ onClick={onClose}
50
+ />
51
+ <div className="fixed inset-0 z-50 flex items-center justify-center p-4 pointer-events-none">
52
+ <motion.div
53
+ layoutId={layoutId}
54
+ className="w-full max-w-lg max-h-[85vh] flex flex-col rounded-2xl bg-card border border-border shadow-2xl pointer-events-auto"
55
+ >
56
+ <div className="flex items-center justify-between px-6 pt-6 pb-0 mb-5 shrink-0">
57
+ <h2 className="text-lg font-semibold">{title}</h2>
58
+ <button
59
+ onClick={onClose}
60
+ className="rounded-full p-1.5 hover:bg-muted transition-colors"
61
+ >
62
+ <X className="h-4 w-4" />
63
+ </button>
64
+ </div>
65
+ <div className="px-6 pb-6 overflow-y-auto min-h-0">
66
+ {children}
67
+ </div>
68
+ </motion.div>
69
+ </div>
70
+ </>
71
+ )}
72
+ </AnimatePresence>
73
+ );
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // ModalActions — save/cancel buttons, reusable
78
+ // ---------------------------------------------------------------------------
79
+
80
+ export interface ModalActionsProps {
81
+ onClose: () => void;
82
+ isSaving?: boolean;
83
+ cancelLabel?: ReactNode;
84
+ saveLabel?: ReactNode;
85
+ savingLabel?: ReactNode;
86
+ }
87
+
88
+ export function ModalActions({
89
+ onClose,
90
+ isSaving,
91
+ cancelLabel = "Anuluj",
92
+ saveLabel = "Zapisz",
93
+ savingLabel = "Zapisywanie...",
94
+ }: ModalActionsProps) {
95
+ return (
96
+ <div className="flex justify-end gap-2 mt-6 pt-4 border-t border-border">
97
+ <Button label={cancelLabel} onClick={onClose} />
98
+ <button
99
+ type="submit"
100
+ className="inline-flex items-center justify-center rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground shadow-sm transition-colors hover:bg-primary/90 disabled:opacity-50"
101
+ disabled={isSaving === true}
102
+ >
103
+ {isSaving ? savingLabel : saveLabel}
104
+ </button>
105
+ </div>
106
+ );
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // CardFormModal — CardModal + Form integrated
111
+ // ---------------------------------------------------------------------------
112
+
113
+ export type CardFormModalProps<T extends ArcObjectAny> = {
114
+ layoutId: string;
115
+ isOpen: boolean;
116
+ title: ReactNode;
117
+ onClose: () => void;
118
+ isSaving?: boolean;
119
+ cancelLabel?: ReactNode;
120
+ saveLabel?: ReactNode;
121
+ savingLabel?: ReactNode;
122
+ } & Pick<FormProps<T>, "schema" | "defaults" | "onSubmit" | "render">;
123
+
124
+ export function CardFormModal<T extends ArcObjectAny>({
125
+ layoutId,
126
+ isOpen,
127
+ title,
128
+ onClose,
129
+ isSaving,
130
+ cancelLabel,
131
+ saveLabel,
132
+ savingLabel,
133
+ schema,
134
+ defaults,
135
+ onSubmit,
136
+ render,
137
+ }: CardFormModalProps<T>) {
138
+ return (
139
+ <CardModal layoutId={layoutId} isOpen={isOpen} title={title} onClose={onClose}>
140
+ <Form
141
+ schema={schema}
142
+ defaults={defaults}
143
+ onSubmit={onSubmit}
144
+ render={(Fields, values) => (
145
+ <>
146
+ <div className="space-y-4">
147
+ {render(Fields, values)}
148
+ </div>
149
+ <ModalActions
150
+ onClose={onClose}
151
+ isSaving={isSaving}
152
+ cancelLabel={cancelLabel}
153
+ saveLabel={saveLabel}
154
+ savingLabel={savingLabel}
155
+ />
156
+ </>
157
+ )}
158
+ />
159
+ </CardModal>
160
+ );
161
+ }
@@ -0,0 +1,32 @@
1
+ import { createContext, useContext, type ReactNode } from "react";
2
+ import type { DisplayMode } from "./types";
3
+
4
+ const DisplayModeContext = createContext<DisplayMode>("default");
5
+
6
+ interface DisplayModeProviderProps {
7
+ mode: DisplayMode;
8
+ children: ReactNode;
9
+ }
10
+
11
+ /**
12
+ * Deklaruje tryb wyświetlania dla subtree.
13
+ *
14
+ * Komponenty DS czytają ten context i dostosowują rendering.
15
+ * SlotRenderer ustawia go automatycznie na podstawie slotId.
16
+ * Można też ustawić ręcznie lub nadpisać propem na komponencie.
17
+ */
18
+ export function DisplayModeProvider({
19
+ mode,
20
+ children,
21
+ }: DisplayModeProviderProps) {
22
+ return (
23
+ <DisplayModeContext.Provider value={mode}>
24
+ {children}
25
+ </DisplayModeContext.Provider>
26
+ );
27
+ }
28
+
29
+ /** Zwraca aktualny display mode z contextu. */
30
+ export function useDisplayMode(): DisplayMode {
31
+ return useContext(DisplayModeContext);
32
+ }
@@ -0,0 +1,85 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+ import type {
3
+ DSComponentMap,
4
+ DSComponentOverrides,
5
+ DSVariantOverrides,
6
+ } from "./types";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Contexts — osobne dla wydajności
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const ComponentOverridesContext = createContext<DSComponentOverrides>({});
13
+ const VariantOverridesContext = createContext<DSVariantOverrides>({});
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Provider
17
+ // ---------------------------------------------------------------------------
18
+
19
+ interface DesignSystemProviderProps {
20
+ /** Nadpisanie komponentów — pełna zamiana. */
21
+ components?: DSComponentOverrides;
22
+ /** Nadpisanie CVA wariantów — merge z domyślnymi. */
23
+ variants?: DSVariantOverrides;
24
+ children: ReactNode;
25
+ }
26
+
27
+ /**
28
+ * DesignSystemProvider — nadpisywanie komponentów i wariantów DS.
29
+ *
30
+ * Nested providers się mergują — wewnętrzny nadpisuje zewnętrzny.
31
+ * Oba contexty są ref-stable (useMemo) — zero re-renderów
32
+ * dopóki props się nie zmienią.
33
+ */
34
+ export function DesignSystemProvider({
35
+ components,
36
+ variants,
37
+ children,
38
+ }: DesignSystemProviderProps) {
39
+ const parentComponents = useContext(ComponentOverridesContext);
40
+ const parentVariants = useContext(VariantOverridesContext);
41
+
42
+ const mergedComponents = useMemo(
43
+ () => ({ ...parentComponents, ...components }),
44
+ [parentComponents, components],
45
+ );
46
+
47
+ const mergedVariants = useMemo(
48
+ () => ({ ...parentVariants, ...variants }),
49
+ [parentVariants, variants],
50
+ );
51
+
52
+ return (
53
+ <ComponentOverridesContext.Provider value={mergedComponents}>
54
+ <VariantOverridesContext.Provider value={mergedVariants}>
55
+ {children}
56
+ </VariantOverridesContext.Provider>
57
+ </ComponentOverridesContext.Provider>
58
+ );
59
+ }
60
+
61
+ // ---------------------------------------------------------------------------
62
+ // Hooks
63
+ // ---------------------------------------------------------------------------
64
+
65
+ /**
66
+ * Pobiera komponent DS — override jeśli istnieje, inaczej default.
67
+ */
68
+ export function useDsComponent<K extends keyof DSComponentMap>(
69
+ name: K,
70
+ defaultComponent: DSComponentMap[K],
71
+ ): DSComponentMap[K] {
72
+ const overrides = useContext(ComponentOverridesContext);
73
+ return (overrides[name] as DSComponentMap[K]) ?? defaultComponent;
74
+ }
75
+
76
+ /**
77
+ * Pobiera CVA variant overrides dla danego komponentu.
78
+ * Zwraca undefined jeśli brak overrides.
79
+ */
80
+ export function useDsVariantOverrides<K extends keyof DSComponentMap>(
81
+ name: K,
82
+ ): DSVariantOverrides[K] | undefined {
83
+ const overrides = useContext(VariantOverridesContext);
84
+ return overrides[name];
85
+ }
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+ import type { ArcElement } from "@arcote.tech/arc";
4
+ import React, { createContext, useCallback, useContext, useMemo } from "react";
5
+ import { FormContext } from "./form";
6
+ import { useFormPartField } from "./form-part";
7
+
8
+ // Helper function to get nested value using dot notation
9
+ function getNestedValue(obj: any, path: string): any {
10
+ return path.split(".").reduce((current, key) => current?.[key], obj);
11
+ }
12
+
13
+ export type FormFieldContext = { errors: any; messages: any };
14
+
15
+ export const FormFieldContext = createContext<null | FormFieldContext>(null);
16
+
17
+ export function useFormField() {
18
+ const context = useContext(FormFieldContext);
19
+ if (!context)
20
+ throw new Error("useFormField must be used within a FormFieldProvider");
21
+ return context;
22
+ }
23
+
24
+ type Translations<E extends ArcElement> =
25
+ | {
26
+ [K in keyof Exclude<ReturnType<E["validate"]>, false>]: (
27
+ data: Exclude<
28
+ Exclude<ReturnType<E["validate"]>, false>[K],
29
+ undefined | false
30
+ >,
31
+ ) => string;
32
+ }
33
+ | ((data: any) => string)
34
+ | string;
35
+
36
+ type FormFieldProps<E extends ArcElement, S = undefined> = {
37
+ translations: Translations<E>;
38
+ render: (field: FormFieldData<E, S>) => React.ReactNode;
39
+ };
40
+
41
+ export type FormFieldData<T, S = undefined> = {
42
+ onChange: (value: any) => void;
43
+ value: any;
44
+ defaultValue?: any;
45
+ name: string;
46
+ subFields?: S;
47
+ setFieldValue: (field: string, value: any) => void;
48
+ };
49
+
50
+ export function FormField<E extends ArcElement, S = undefined>(
51
+ name: string,
52
+ defaultValue?: any,
53
+ subFields?: S,
54
+ ) {
55
+ return ({ translations, render }: FormFieldProps<E, S>) => {
56
+ const form = useContext(FormContext);
57
+ if (!form) throw new Error("FormField must be used within a Form");
58
+
59
+ useFormPartField(name);
60
+
61
+ const { values, errors, dirty, isSubmitted, setFieldValue, setFieldDirty } =
62
+ form;
63
+
64
+ const schemaErrors = errors?.["schema"] || {};
65
+
66
+ const fieldErrors = schemaErrors[name as any] || false;
67
+ const value = getNestedValue(values, name) ?? defaultValue ?? "";
68
+ const isDirty = dirty.has(name);
69
+
70
+ const handleChange = useCallback(
71
+ (value: any) => {
72
+ if (value?.target?.value !== undefined) {
73
+ setFieldValue(name, value.target.value);
74
+ } else {
75
+ setFieldValue(name, value);
76
+ }
77
+ if (!isDirty) {
78
+ setFieldDirty(name);
79
+ }
80
+ },
81
+ [name, isDirty, setFieldValue, setFieldDirty],
82
+ );
83
+
84
+ const errorMessages = fieldErrors
85
+ ? Object.entries(fieldErrors)
86
+ .map(([key, value]) => {
87
+ if (!value) return;
88
+ if (!translations) return;
89
+
90
+ const translation =
91
+ translations[key as keyof typeof translations] || translations;
92
+ if (typeof translation === "function") {
93
+ return (translation as any)(value);
94
+ }
95
+
96
+ if (typeof translation === "string") {
97
+ return translation;
98
+ }
99
+ })
100
+ .filter(Boolean)
101
+ : [];
102
+
103
+ const contextValue = useMemo(
104
+ () => ({
105
+ errors: isSubmitted ? fieldErrors : false,
106
+ messages: errorMessages,
107
+ }),
108
+ [fieldErrors, isSubmitted, errorMessages],
109
+ );
110
+
111
+ return (
112
+ <FormFieldContext.Provider value={contextValue}>
113
+ {render({
114
+ onChange: handleChange,
115
+ name: name.toString(),
116
+ value,
117
+ defaultValue,
118
+ subFields,
119
+ setFieldValue: setFieldValue as any,
120
+ })}
121
+ </FormFieldContext.Provider>
122
+ );
123
+ };
124
+ }