@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
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@arcote.tech/arc-ds",
3
+ "type": "module",
4
+ "version": "0.4.1",
5
+ "private": false,
6
+ "author": "Przemysław Krasiński [arcote.tech]",
7
+ "description": "Design System for Arc framework — CVA-based components with display modes and variant overrides",
8
+ "main": "./src/index.ts",
9
+ "types": "./src/index.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./src/index.ts",
13
+ "import": "./src/index.ts",
14
+ "default": "./src/index.ts"
15
+ },
16
+ "./ui/*": {
17
+ "types": "./src/ui/*.tsx",
18
+ "import": "./src/ui/*.tsx",
19
+ "default": "./src/ui/*.tsx"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "type-check": "tsc --noEmit"
24
+ },
25
+ "dependencies": {
26
+ "class-variance-authority": "^0.7.1",
27
+ "clsx": "^2.1.1",
28
+ "tailwind-merge": "^3.5.0"
29
+ },
30
+ "peerDependencies": {
31
+ "@arcote.tech/arc": "workspace:*",
32
+ "framer-motion": "^12.0.0",
33
+ "lucide-react": ">=0.400.0",
34
+ "radix-ui": "^1.0.0",
35
+ "react": "^18.0.0 || ^19.0.0",
36
+ "tailwindcss": "^4.0.0",
37
+ "typescript": "^5.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "typescript": "~5.9.3"
41
+ }
42
+ }
@@ -0,0 +1,86 @@
1
+ import { cva } from "class-variance-authority";
2
+ import { Avatar as AvatarPrimitive } from "radix-ui";
3
+ import { cn } from "../../lib/utils";
4
+ import type { AvatarProps } from "../types";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // CVA
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export const avatarVariants = cva(
11
+ "relative flex shrink-0 overflow-hidden rounded-full select-none",
12
+ {
13
+ variants: {
14
+ size: {
15
+ default: "size-9 md:size-8",
16
+ sm: "size-7 md:size-6",
17
+ lg: "size-11 md:size-10",
18
+ xs: "size-6 md:size-5",
19
+ },
20
+ display: {
21
+ default: "",
22
+ compact: "",
23
+ minimal: "",
24
+ expanded: "",
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ size: "default",
29
+ display: "default",
30
+ },
31
+ },
32
+ );
33
+
34
+ export const avatarFallbackVariants = cva(
35
+ "flex size-full items-center justify-center rounded-full bg-muted text-muted-foreground",
36
+ {
37
+ variants: {
38
+ size: {
39
+ default: "text-sm",
40
+ sm: "text-xs",
41
+ lg: "text-base",
42
+ xs: "text-[10px]",
43
+ },
44
+ },
45
+ defaultVariants: {
46
+ size: "default",
47
+ },
48
+ },
49
+ );
50
+
51
+ // ---------------------------------------------------------------------------
52
+ // Avatar
53
+ // ---------------------------------------------------------------------------
54
+
55
+ export function Avatar({
56
+ src,
57
+ fallback,
58
+ size = "default",
59
+ display = "default",
60
+ className,
61
+ ...props
62
+ }: AvatarProps) {
63
+ return (
64
+ <AvatarPrimitive.Root
65
+ data-slot="avatar"
66
+ className={cn(avatarVariants({ size, display }), className)}
67
+ {...props}
68
+ >
69
+ {src && (
70
+ <AvatarPrimitive.Image
71
+ data-slot="avatar-image"
72
+ src={src}
73
+ className="aspect-square size-full"
74
+ />
75
+ )}
76
+ {fallback && (
77
+ <AvatarPrimitive.Fallback
78
+ data-slot="avatar-fallback"
79
+ className={avatarFallbackVariants({ size })}
80
+ >
81
+ {fallback}
82
+ </AvatarPrimitive.Fallback>
83
+ )}
84
+ </AvatarPrimitive.Root>
85
+ );
86
+ }
@@ -0,0 +1,61 @@
1
+ import { cva } from "class-variance-authority";
2
+ import { Slot } from "radix-ui";
3
+ import { cn } from "../../lib/utils";
4
+ import type { BadgeProps } from "../types";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // CVA
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export const badgeVariants = cva(
11
+ "inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [&>svg]:pointer-events-none [&>svg]:size-3",
12
+ {
13
+ variants: {
14
+ variant: {
15
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
16
+ secondary:
17
+ "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
18
+ destructive: "bg-destructive text-white [a&]:hover:bg-destructive/90",
19
+ outline:
20
+ "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
21
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
22
+ },
23
+ display: {
24
+ default: "",
25
+ compact: "px-1.5 py-0 text-[10px]",
26
+ minimal: "size-2 rounded-full p-0 [&>*]:hidden",
27
+ expanded: "",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ display: "default",
33
+ },
34
+ },
35
+ );
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Badge
39
+ // ---------------------------------------------------------------------------
40
+
41
+ export function Badge({
42
+ className,
43
+ variant = "default",
44
+ display = "default",
45
+ asChild = false,
46
+ children,
47
+ ...props
48
+ }: BadgeProps) {
49
+ const Comp = asChild ? Slot.Root : "span";
50
+
51
+ return (
52
+ <Comp
53
+ data-slot="badge"
54
+ data-variant={variant}
55
+ className={cn(badgeVariants({ variant, display }), className)}
56
+ {...props}
57
+ >
58
+ {children}
59
+ </Comp>
60
+ );
61
+ }
@@ -0,0 +1,70 @@
1
+ import { Box } from "../box/box";
2
+ import { useBentoSpan } from "../bento-grid/bento-grid";
3
+ import { motion } from "framer-motion";
4
+ import { Plus } from "lucide-react";
5
+ import type { ComponentType, ReactNode } from "react";
6
+
7
+ export interface BentoCardProps {
8
+ layoutId: string;
9
+ title: ReactNode;
10
+ icon: ComponentType<{ className?: string }>;
11
+ isEmpty: boolean;
12
+ isModalOpen?: boolean;
13
+ onEdit: () => void;
14
+ emptyLabel?: ReactNode;
15
+ className?: string;
16
+ span?: string;
17
+ children?: ReactNode;
18
+ }
19
+
20
+ export function BentoCard({
21
+ layoutId,
22
+ title,
23
+ icon: Icon,
24
+ isEmpty,
25
+ isModalOpen,
26
+ onEdit,
27
+ emptyLabel,
28
+ className,
29
+ span,
30
+ children,
31
+ }: BentoCardProps) {
32
+ const autoSpan = useBentoSpan();
33
+ const resolvedSpan = span ?? autoSpan;
34
+
35
+ return (
36
+ <div className={`h-full ${resolvedSpan} ${className ?? ""}`.trim()}>
37
+ <motion.div
38
+ layoutId={layoutId}
39
+ className="h-full"
40
+ style={{ opacity: isModalOpen ? 0 : 1 }}
41
+ >
42
+ <Box
43
+ layout={false}
44
+ className="group relative cursor-pointer p-5 hover:shadow-lg transition-all duration-300 h-full min-h-[140px] hover:scale-[1.01]"
45
+ onClick={onEdit}
46
+ >
47
+ <div className="flex items-center gap-2.5 mb-3">
48
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary/10">
49
+ <Icon className="h-4 w-4 text-primary" />
50
+ </div>
51
+ <span className="text-sm font-semibold">{title}</span>
52
+ </div>
53
+
54
+ {isEmpty ? (
55
+ <div className="flex flex-col items-center justify-center gap-2 py-4 border-2 border-dashed border-muted-foreground/15 rounded-xl transition-colors group-hover:border-primary/30">
56
+ <div className="flex h-9 w-9 items-center justify-center rounded-full bg-muted transition-colors group-hover:bg-primary/10">
57
+ <Plus className="h-4 w-4 text-muted-foreground transition-colors group-hover:text-primary" />
58
+ </div>
59
+ {emptyLabel && (
60
+ <span className="text-xs text-muted-foreground">{emptyLabel}</span>
61
+ )}
62
+ </div>
63
+ ) : (
64
+ <div className="flex flex-col gap-2">{children}</div>
65
+ )}
66
+ </Box>
67
+ </motion.div>
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,52 @@
1
+ import { Children, createContext, useContext, type ReactNode } from "react";
2
+
3
+ export interface BentoGridProps {
4
+ children: ReactNode;
5
+ className?: string;
6
+ }
7
+
8
+ type BentoGridItemContext = {
9
+ index: number;
10
+ count: number;
11
+ };
12
+
13
+ const BentoItemContext = createContext<BentoGridItemContext | null>(null);
14
+
15
+ /**
16
+ * Hook for children to read their grid span class.
17
+ * Returns the appropriate Tailwind class (e.g., "md:row-span-2", "md:col-span-3")
18
+ * or empty string for normal cells.
19
+ */
20
+ export function useBentoSpan(): string {
21
+ const ctx = useContext(BentoItemContext);
22
+ if (!ctx) return "";
23
+ const { index, count } = ctx;
24
+
25
+ if (count < 5) return "";
26
+ if (index === 0) return "md:row-span-2";
27
+ if (index === count - 1 && (count === 6 || count === 8)) return "md:col-span-3";
28
+ return "";
29
+ }
30
+
31
+ /**
32
+ * Responsive bento grid with auto-layout based on child count.
33
+ *
34
+ * Children call useBentoSpan() to get their grid span class.
35
+ * No wrapper divs — children control their own layout.
36
+ */
37
+ export function BentoGrid({ children, className }: BentoGridProps) {
38
+ const items = Children.toArray(children);
39
+ const count = items.length;
40
+
41
+ if (count === 0) return null;
42
+
43
+ return (
44
+ <div className={`grid grid-cols-1 md:grid-cols-3 gap-3 ${className ?? ""}`}>
45
+ {items.map((child, index) => (
46
+ <BentoItemContext.Provider key={index} value={{ index, count }}>
47
+ {child}
48
+ </BentoItemContext.Provider>
49
+ ))}
50
+ </div>
51
+ );
52
+ }
@@ -0,0 +1,96 @@
1
+ import { cva } from "class-variance-authority";
2
+ import { type HTMLMotionProps, motion } from "framer-motion";
3
+ import { forwardRef } from "react";
4
+ import { cn } from "../../lib/utils";
5
+ import { useDisplayMode } from "../display-mode";
6
+ import { useDsVariantOverrides } from "../ds-provider";
7
+ import { dsTransitions } from "../transitions";
8
+ import type { DisplayMode } from "../types";
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // CVA — Box
12
+ // ---------------------------------------------------------------------------
13
+
14
+ export const boxVariants = cva(
15
+ "rounded-2xl border bg-card text-card-foreground shadow-sm transition-all duration-300",
16
+ {
17
+ variants: {
18
+ variant: {
19
+ default: "",
20
+ ghost:
21
+ "border-transparent bg-transparent shadow-none hover:shadow-none",
22
+ },
23
+ display: {
24
+ default: "",
25
+ compact: "rounded-xl p-2",
26
+ minimal: "rounded-lg p-1",
27
+ expanded: "p-4",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ display: "default",
33
+ },
34
+ },
35
+ );
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Box
39
+ // ---------------------------------------------------------------------------
40
+
41
+ export interface BoxProps extends HTMLMotionProps<"div"> {
42
+ variant?: "default" | "ghost";
43
+ displayMode?: DisplayMode;
44
+ /** Layout animation ID — dwa Boxy z tym samym layoutId animują przejście. */
45
+ layoutId?: string;
46
+ }
47
+
48
+ /**
49
+ * Box — bazowy kontener DS z layout animations.
50
+ *
51
+ * Domyślny wariant: neutralny card (border, bg-card, shadow-sm).
52
+ * `ghost`: przezroczysty, bez border/shadow.
53
+ *
54
+ * Wygląd można nadpisać per-app przez DesignSystemProvider variants:
55
+ * `{ Box: { className: { default: "glass" } } }` — dodaje klasę CSS.
56
+ */
57
+ export const Box = forwardRef<HTMLDivElement, BoxProps>(
58
+ (
59
+ {
60
+ variant = "default",
61
+ displayMode: displayModeProp,
62
+ layoutId,
63
+ layout = "position",
64
+ className,
65
+ children,
66
+ ...rest
67
+ },
68
+ ref,
69
+ ) => {
70
+ const contextMode = useDisplayMode();
71
+ const mode: DisplayMode = displayModeProp ?? contextMode;
72
+
73
+ // Extra classes injected by DesignSystemProvider (e.g. NDT adds "glass")
74
+ const overrides = useDsVariantOverrides("Box");
75
+ const extraClass = overrides?.className?.default;
76
+
77
+ return (
78
+ <motion.div
79
+ ref={ref}
80
+ layout={layout}
81
+ layoutId={layoutId}
82
+ className={cn(
83
+ boxVariants({ variant, display: mode }),
84
+ extraClass,
85
+ className,
86
+ )}
87
+ transition={{ layout: dsTransitions.smooth }}
88
+ {...rest}
89
+ >
90
+ {children}
91
+ </motion.div>
92
+ );
93
+ },
94
+ );
95
+
96
+ Box.displayName = "Box";
@@ -0,0 +1,191 @@
1
+ import { cva } from "class-variance-authority";
2
+ import { forwardRef } from "react";
3
+ import { cn } from "../../lib/utils";
4
+ import { useDisplayMode } from "../display-mode";
5
+ import { Tooltip } from "../tooltip/tooltip";
6
+ import type { ButtonProps, DisplayMode } from "../types";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // CVA — root button
10
+ // ---------------------------------------------------------------------------
11
+
12
+ export const buttonVariants = cva(
13
+ "inline-flex shrink-0 items-center justify-center rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
14
+ {
15
+ variants: {
16
+ variant: {
17
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
18
+ secondary:
19
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
20
+ ghost:
21
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22
+ outline:
23
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
24
+ destructive: "bg-destructive text-white hover:bg-destructive/90",
25
+ link: "text-primary underline-offset-4 hover:underline",
26
+ },
27
+ size: {
28
+ default:
29
+ "h-12 gap-2 px-5 py-2.5 text-base md:h-9 md:px-4 md:py-2 md:text-sm",
30
+ sm: "h-11 gap-2 rounded-md px-4 text-base md:h-8 md:gap-1.5 md:px-3 md:text-sm",
31
+ xs: "h-9 gap-1.5 rounded-md px-3 text-sm md:h-6 md:gap-1 md:px-2 md:text-xs",
32
+ lg: "h-14 gap-2.5 rounded-md px-8 text-lg md:h-10 md:px-6 md:text-sm",
33
+ icon: "size-12 md:size-9",
34
+ "icon-sm": "size-11 md:size-8",
35
+ "icon-xs": "size-9 rounded-md md:size-6",
36
+ },
37
+ display: {
38
+ default: "",
39
+ compact:
40
+ "flex-col h-auto min-h-[44px] min-w-[44px] gap-0.5 rounded-lg px-3 py-2 text-xs bg-transparent text-muted-foreground hover:text-foreground data-[active]:text-primary",
41
+ minimal: "",
42
+ expanded: "w-full justify-start gap-2",
43
+ },
44
+ active: {
45
+ true: "data-[active]:font-semibold",
46
+ false: "",
47
+ },
48
+ },
49
+ compoundVariants: [
50
+ {
51
+ display: "compact",
52
+ active: true,
53
+ className: "text-primary bg-transparent",
54
+ },
55
+ ],
56
+ defaultVariants: {
57
+ variant: "ghost",
58
+ size: "sm",
59
+ display: "default",
60
+ active: false,
61
+ },
62
+ },
63
+ );
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Sub-CVA — icon
67
+ // ---------------------------------------------------------------------------
68
+
69
+ export const buttonIconVariants = cva("shrink-0", {
70
+ variants: {
71
+ display: {
72
+ default: "size-4.5 md:size-4",
73
+ compact: "size-5",
74
+ minimal: "size-4.5 md:size-4",
75
+ expanded: "size-4.5 md:size-4",
76
+ },
77
+ active: {
78
+ true: "",
79
+ false: "",
80
+ },
81
+ },
82
+ compoundVariants: [
83
+ {
84
+ display: "compact",
85
+ active: true,
86
+ className: "stroke-[2.5]",
87
+ },
88
+ ],
89
+ defaultVariants: {
90
+ display: "default",
91
+ active: false,
92
+ },
93
+ });
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Sub-CVA — label
97
+ // ---------------------------------------------------------------------------
98
+
99
+ export const buttonLabelVariants = cva("", {
100
+ variants: {
101
+ display: {
102
+ default: "",
103
+ compact: "",
104
+ minimal: "sr-only",
105
+ expanded: "flex-1 text-left",
106
+ },
107
+ },
108
+ defaultVariants: {
109
+ display: "default",
110
+ },
111
+ });
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // Button
115
+ // ---------------------------------------------------------------------------
116
+
117
+ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
118
+ (
119
+ {
120
+ icon: Icon,
121
+ label,
122
+ right,
123
+ tooltip,
124
+ isActive,
125
+ displayMode: displayModeProp,
126
+ variant: variantProp,
127
+ size: sizeProp,
128
+ className,
129
+ onClick,
130
+ disabled,
131
+ ...rest
132
+ },
133
+ ref,
134
+ ) => {
135
+ const contextMode = useDisplayMode();
136
+ const mode: DisplayMode = displayModeProp ?? contextMode;
137
+
138
+ const resolvedVariant = isActive
139
+ ? ("secondary" as const)
140
+ : (variantProp ?? "ghost");
141
+
142
+ const resolvedSize = sizeProp ?? (mode === "minimal" ? "icon-sm" : "sm");
143
+
144
+ const tooltipSide =
145
+ mode === "compact" ? "top" : mode === "expanded" ? "right" : "bottom";
146
+
147
+ const resolvedTooltip = mode === "minimal" ? (tooltip ?? label) : tooltip;
148
+
149
+ const content = (
150
+ <button
151
+ ref={ref}
152
+ data-active={isActive || undefined}
153
+ className={cn(
154
+ buttonVariants({
155
+ variant: resolvedVariant,
156
+ size: resolvedSize,
157
+ display: mode,
158
+ active: !!isActive,
159
+ }),
160
+ className,
161
+ )}
162
+ onClick={onClick}
163
+ disabled={disabled}
164
+ {...rest}
165
+ >
166
+ {Icon && (
167
+ <Icon
168
+ className={buttonIconVariants({
169
+ display: mode,
170
+ active: !!isActive,
171
+ })}
172
+ />
173
+ )}
174
+ {label && (
175
+ <span className={buttonLabelVariants({ display: mode })}>
176
+ {label}
177
+ </span>
178
+ )}
179
+ {mode !== "minimal" && mode !== "compact" && right}
180
+ </button>
181
+ );
182
+
183
+ return (
184
+ <Tooltip content={resolvedTooltip} side={tooltipSide}>
185
+ {content}
186
+ </Tooltip>
187
+ );
188
+ },
189
+ );
190
+
191
+ Button.displayName = "Button";