@opensaas/stack-ui 0.1.0 → 0.1.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 (37) hide show
  1. package/.turbo/turbo-build.log +4 -2
  2. package/CHANGELOG.md +12 -0
  3. package/CLAUDE.md +311 -0
  4. package/LICENSE +21 -0
  5. package/dist/components/ItemFormClient.d.ts.map +1 -1
  6. package/dist/components/ItemFormClient.js +3 -1
  7. package/dist/components/fields/FileField.d.ts +21 -0
  8. package/dist/components/fields/FileField.d.ts.map +1 -0
  9. package/dist/components/fields/FileField.js +78 -0
  10. package/dist/components/fields/ImageField.d.ts +23 -0
  11. package/dist/components/fields/ImageField.d.ts.map +1 -0
  12. package/dist/components/fields/ImageField.js +107 -0
  13. package/dist/components/fields/JsonField.d.ts +15 -0
  14. package/dist/components/fields/JsonField.d.ts.map +1 -0
  15. package/dist/components/fields/JsonField.js +57 -0
  16. package/dist/components/fields/index.d.ts +6 -0
  17. package/dist/components/fields/index.d.ts.map +1 -1
  18. package/dist/components/fields/index.js +3 -0
  19. package/dist/components/fields/registry.d.ts.map +1 -1
  20. package/dist/components/fields/registry.js +6 -0
  21. package/dist/primitives/index.d.ts +1 -0
  22. package/dist/primitives/index.d.ts.map +1 -1
  23. package/dist/primitives/index.js +1 -0
  24. package/dist/primitives/textarea.d.ts +6 -0
  25. package/dist/primitives/textarea.d.ts.map +1 -0
  26. package/dist/primitives/textarea.js +8 -0
  27. package/dist/styles/globals.css +89 -0
  28. package/package.json +5 -3
  29. package/src/components/ItemFormClient.tsx +3 -1
  30. package/src/components/fields/FileField.tsx +223 -0
  31. package/src/components/fields/ImageField.tsx +328 -0
  32. package/src/components/fields/JsonField.tsx +114 -0
  33. package/src/components/fields/index.ts +6 -0
  34. package/src/components/fields/registry.ts +6 -0
  35. package/src/primitives/index.ts +1 -0
  36. package/src/primitives/textarea.tsx +24 -0
  37. package/vitest.config.ts +1 -1
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState, useMemo } from 'react';
4
+ import { Textarea } from '../../primitives/textarea.js';
5
+ import { Label } from '../../primitives/label.js';
6
+ import { cn } from '../../lib/utils.js';
7
+ export function JsonField({ name, value, onChange, label, placeholder = 'Enter JSON data...', error, disabled, required, mode = 'edit', rows = 8, formatted = true, }) {
8
+ // Track the string being edited separately from the prop value
9
+ const [editingValue, setEditingValue] = useState(null);
10
+ const [parseError, setParseError] = useState();
11
+ // Compute the display value - either what's being edited or the prop value
12
+ const displayValue = useMemo(() => {
13
+ if (editingValue !== null) {
14
+ return editingValue;
15
+ }
16
+ try {
17
+ return value !== undefined && value !== null
18
+ ? JSON.stringify(value, null, formatted ? 2 : 0)
19
+ : '';
20
+ }
21
+ catch {
22
+ return '';
23
+ }
24
+ }, [value, formatted, editingValue]);
25
+ const handleChange = (text) => {
26
+ setEditingValue(text);
27
+ // Try to parse and update value
28
+ if (text.trim() === '') {
29
+ // Empty string - treat as null/undefined
30
+ onChange(undefined);
31
+ setParseError(undefined);
32
+ return;
33
+ }
34
+ try {
35
+ const parsed = JSON.parse(text);
36
+ onChange(parsed);
37
+ setParseError(undefined);
38
+ }
39
+ catch (e) {
40
+ // Invalid JSON - set error but don't update value
41
+ if (e instanceof Error) {
42
+ setParseError(`Invalid JSON: ${e.message}`);
43
+ }
44
+ else {
45
+ setParseError('Invalid JSON');
46
+ }
47
+ }
48
+ };
49
+ const handleBlur = () => {
50
+ // Clear editing state on blur
51
+ setEditingValue(null);
52
+ };
53
+ if (mode === 'read') {
54
+ return (_jsxs("div", { className: "space-y-1", children: [_jsx(Label, { className: "text-muted-foreground", children: label }), _jsx("pre", { className: "text-sm bg-muted rounded-md p-3 overflow-x-auto", children: displayValue || '-' })] }));
55
+ }
56
+ return (_jsxs("div", { className: "space-y-2", children: [_jsxs(Label, { htmlFor: name, children: [label, required && _jsx("span", { className: "text-destructive ml-1", children: "*" })] }), _jsx(Textarea, { id: name, name: name, value: displayValue, onChange: (e) => handleChange(e.target.value), onBlur: handleBlur, placeholder: placeholder, disabled: disabled, required: required, rows: rows, className: cn('font-mono text-sm', (error || parseError) && 'border-destructive') }), parseError && _jsx("p", { className: "text-sm text-amber-600", children: parseError }), error && _jsx("p", { className: "text-sm text-destructive", children: error })] }));
57
+ }
@@ -5,9 +5,12 @@ export { SelectField } from './SelectField.js';
5
5
  export { TimestampField } from './TimestampField.js';
6
6
  export { PasswordField } from './PasswordField.js';
7
7
  export { RelationshipField } from './RelationshipField.js';
8
+ export { JsonField } from './JsonField.js';
8
9
  export { ComboboxField } from './ComboboxField.js';
9
10
  export { RelationshipManager } from './RelationshipManager.js';
10
11
  export { FieldRenderer } from './FieldRenderer.js';
12
+ export { FileField } from './FileField.js';
13
+ export { ImageField } from './ImageField.js';
11
14
  export { fieldComponentRegistry, registerFieldComponent, getFieldComponent } from './registry.js';
12
15
  export type { TextFieldProps } from './TextField.js';
13
16
  export type { IntegerFieldProps } from './IntegerField.js';
@@ -16,8 +19,11 @@ export type { SelectFieldProps } from './SelectField.js';
16
19
  export type { TimestampFieldProps } from './TimestampField.js';
17
20
  export type { PasswordFieldProps } from './PasswordField.js';
18
21
  export type { RelationshipFieldProps } from './RelationshipField.js';
22
+ export type { JsonFieldProps } from './JsonField.js';
19
23
  export type { ComboboxFieldProps } from './ComboboxField.js';
20
24
  export type { RelationshipManagerProps } from './RelationshipManager.js';
21
25
  export type { FieldRendererProps } from './FieldRenderer.js';
26
+ export type { FileFieldProps } from './FileField.js';
27
+ export type { ImageFieldProps } from './ImageField.js';
22
28
  export type { FieldComponent, FieldComponentProps } from './registry.js';
23
29
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/fields/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAGlD,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAGjG,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACxD,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AACpE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAA;AACxE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/fields/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACpD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AAC1D,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAC9D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAA;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAA;AAG5C,OAAO,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAGjG,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAA;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACxD,YAAY,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAC9D,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,YAAY,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AACpE,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,YAAY,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAA;AACxE,YAAY,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAA;AAC5D,YAAY,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAA;AACpD,YAAY,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAA;AACtD,YAAY,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAA"}
@@ -6,8 +6,11 @@ export { SelectField } from './SelectField.js';
6
6
  export { TimestampField } from './TimestampField.js';
7
7
  export { PasswordField } from './PasswordField.js';
8
8
  export { RelationshipField } from './RelationshipField.js';
9
+ export { JsonField } from './JsonField.js';
9
10
  export { ComboboxField } from './ComboboxField.js';
10
11
  export { RelationshipManager } from './RelationshipManager.js';
11
12
  export { FieldRenderer } from './FieldRenderer.js';
13
+ export { FileField } from './FileField.js';
14
+ export { ImageField } from './ImageField.js';
12
15
  // Registry for custom field types
13
16
  export { fieldComponentRegistry, registerFieldComponent, getFieldComponent } from './registry.js';
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/components/fields/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAS1C;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CACvB,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,aAAa,CAAC,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAEzF;;;;GAIG;AAEH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,CAAC,CAQrE,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EAEjB,SAAS,EAAE,aAAa,CAAC,GAAG,CAAC,GAC5B,IAAI,CAEN;AAED;;;GAGG;AAEH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,SAAS,CAEnF"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../../src/components/fields/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,OAAO,CAAA;AAY1C;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,OAAO,CAAA;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAA;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CACvB,CAAA;AAED;;;;;;GAMG;AACH,MAAM,MAAM,cAAc,GAAG,aAAa,CAAC,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAA;AAEzF;;;;GAIG;AAEH,eAAO,MAAM,sBAAsB,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,GAAG,CAAC,CAWrE,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EAEjB,SAAS,EAAE,aAAa,CAAC,GAAG,CAAC,GAC5B,IAAI,CAEN;AAED;;;GAGG;AAEH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,SAAS,CAEnF"}
@@ -5,6 +5,9 @@ import { SelectField } from './SelectField.js';
5
5
  import { TimestampField } from './TimestampField.js';
6
6
  import { PasswordField } from './PasswordField.js';
7
7
  import { RelationshipField } from './RelationshipField.js';
8
+ import { JsonField } from './JsonField.js';
9
+ import { FileField } from './FileField.js';
10
+ import { ImageField } from './ImageField.js';
8
11
  /**
9
12
  * Registry mapping field types to their default UI components
10
13
  * This can be extended for custom field types
@@ -19,6 +22,9 @@ export const fieldComponentRegistry = {
19
22
  timestamp: TimestampField,
20
23
  password: PasswordField,
21
24
  relationship: RelationshipField,
25
+ json: JsonField,
26
+ file: FileField,
27
+ image: ImageField,
22
28
  };
23
29
  /**
24
30
  * Register a custom field component for a field type
@@ -1,5 +1,6 @@
1
1
  export { Button, buttonVariants, type ButtonProps } from './button.js';
2
2
  export { Input, type InputProps } from './input.js';
3
+ export { Textarea, type TextareaProps } from './textarea.js';
3
4
  export { Label } from './label.js';
4
5
  export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './card.js';
5
6
  export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, } from './table.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/primitives/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAA;AACtE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACjG,OAAO,EACL,KAAK,EACL,WAAW,EACX,SAAS,EACT,WAAW,EACX,SAAS,EACT,QAAQ,EACR,SAAS,EACT,YAAY,GACb,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,MAAM,EACN,YAAY,EACZ,aAAa,EACb,WAAW,EACX,aAAa,EACb,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,iBAAiB,GAClB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,MAAM,EACN,WAAW,EACX,WAAW,EACX,aAAa,EACb,aAAa,EACb,WAAW,EACX,UAAU,EACV,eAAe,EACf,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AACtE,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAA;AAC5D,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACnE,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC/E,OAAO,EACL,QAAQ,EACR,eAAe,EACf,eAAe,EACf,cAAc,EACd,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,iBAAiB,GAClB,MAAM,eAAe,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/primitives/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAA;AACtE,OAAO,EAAE,KAAK,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAA;AACnD,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAA;AAC5D,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AACjG,OAAO,EACL,KAAK,EACL,WAAW,EACX,SAAS,EACT,WAAW,EACX,SAAS,EACT,QAAQ,EACR,SAAS,EACT,YAAY,GACb,MAAM,YAAY,CAAA;AACnB,OAAO,EACL,MAAM,EACN,YAAY,EACZ,aAAa,EACb,WAAW,EACX,aAAa,EACb,aAAa,EACb,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,iBAAiB,GAClB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,MAAM,EACN,WAAW,EACX,WAAW,EACX,aAAa,EACb,aAAa,EACb,WAAW,EACX,UAAU,EACV,eAAe,EACf,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,aAAa,CAAA;AACpB,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AACtE,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAA;AAC5D,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACnE,OAAO,EAAE,cAAc,EAAE,KAAK,mBAAmB,EAAE,MAAM,sBAAsB,CAAA;AAC/E,OAAO,EACL,QAAQ,EACR,eAAe,EACf,eAAe,EACf,cAAc,EACd,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,iBAAiB,GAClB,MAAM,eAAe,CAAA"}
@@ -1,6 +1,7 @@
1
1
  // Primitive components from shadcn/ui
2
2
  export { Button, buttonVariants } from './button.js';
3
3
  export { Input } from './input.js';
4
+ export { Textarea } from './textarea.js';
4
5
  export { Label } from './label.js';
5
6
  export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } from './card.js';
6
7
  export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption, } from './table.js';
@@ -0,0 +1,6 @@
1
+ import * as React from 'react';
2
+ export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
3
+ }
4
+ declare const Textarea: React.ForwardRefExoticComponent<TextareaProps & React.RefAttributes<HTMLTextAreaElement>>;
5
+ export { Textarea };
6
+ //# sourceMappingURL=textarea.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"textarea.d.ts","sourceRoot":"","sources":["../../src/primitives/textarea.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAK9B,MAAM,WAAW,aAAc,SAAQ,KAAK,CAAC,sBAAsB,CAAC,mBAAmB,CAAC;CAAG;AAE3F,QAAA,MAAM,QAAQ,2FAab,CAAA;AAGD,OAAO,EAAE,QAAQ,EAAE,CAAA"}
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { cn } from '../lib/utils.js';
4
+ const Textarea = React.forwardRef(({ className, ...props }, ref) => {
5
+ return (_jsx("textarea", { className: cn('flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', className), ref: ref, ...props }));
6
+ });
7
+ Textarea.displayName = 'Textarea';
8
+ export { Textarea };
@@ -7,6 +7,8 @@
7
7
  "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
8
8
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
9
9
  "Courier New", monospace;
10
+ --color-amber-600: oklch(66.6% 0.179 58.318);
11
+ --color-green-600: oklch(62.7% 0.194 149.214);
10
12
  --color-gray-200: oklch(92.8% 0.006 264.531);
11
13
  --color-black: #000;
12
14
  --color-white: #fff;
@@ -274,6 +276,9 @@
274
276
  .top-1\/2 {
275
277
  top: calc(1/2 * 100%);
276
278
  }
279
+ .top-2 {
280
+ top: calc(var(--spacing) * 2);
281
+ }
277
282
  .top-4 {
278
283
  top: calc(var(--spacing) * 4);
279
284
  }
@@ -361,6 +366,9 @@
361
366
  .table {
362
367
  display: table;
363
368
  }
369
+ .h-3 {
370
+ height: calc(var(--spacing) * 3);
371
+ }
364
372
  .h-3\.5 {
365
373
  height: calc(var(--spacing) * 3.5);
366
374
  }
@@ -406,12 +414,18 @@
406
414
  .max-h-\[300px\] {
407
415
  max-height: 300px;
408
416
  }
417
+ .min-h-\[80px\] {
418
+ min-height: 80px;
419
+ }
409
420
  .min-h-screen {
410
421
  min-height: 100vh;
411
422
  }
412
423
  .w-2\/3 {
413
424
  width: calc(2/3 * 100%);
414
425
  }
426
+ .w-3 {
427
+ width: calc(var(--spacing) * 3);
428
+ }
415
429
  .w-3\.5 {
416
430
  width: calc(var(--spacing) * 3.5);
417
431
  }
@@ -499,6 +513,9 @@
499
513
  --tw-translate-y: -50%;
500
514
  translate: var(--tw-translate-x) var(--tw-translate-y);
501
515
  }
516
+ .transform {
517
+ transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
518
+ }
502
519
  .animate-pulse {
503
520
  animation: var(--animate-pulse);
504
521
  }
@@ -508,6 +525,9 @@
508
525
  .cursor-default {
509
526
  cursor: default;
510
527
  }
528
+ .cursor-not-allowed {
529
+ cursor: not-allowed;
530
+ }
511
531
  .cursor-pointer {
512
532
  cursor: pointer;
513
533
  }
@@ -707,6 +727,18 @@
707
727
  .border-input {
708
728
  border-color: light-dark(var(--color-input-light), var(--color-input-dark));
709
729
  }
730
+ .border-muted-foreground\/25 {
731
+ border-color: color-mix(in srgb, light-dark(
732
+ oklch(0.5 0.01 264),
733
+ oklch(0.6 0.01 264)
734
+ ) 25%, transparent);
735
+ @supports (color: color-mix(in lab, red, red)) {
736
+ border-color: color-mix(in oklab, light-dark(
737
+ var(--color-muted-foreground-light),
738
+ var(--color-muted-foreground-dark)
739
+ ) 25%, transparent);
740
+ }
741
+ }
710
742
  .border-primary {
711
743
  border-color: light-dark(var(--color-primary-light), var(--color-primary-dark));
712
744
  }
@@ -767,6 +799,12 @@
767
799
  .bg-primary {
768
800
  background-color: light-dark(var(--color-primary-light), var(--color-primary-dark));
769
801
  }
802
+ .bg-primary\/5 {
803
+ background-color: color-mix(in srgb, light-dark(oklch(0.68 0.19 210), oklch(0.72 0.2 210)) 5%, transparent);
804
+ @supports (color: color-mix(in lab, red, red)) {
805
+ background-color: color-mix(in oklab, light-dark(var(--color-primary-light), var(--color-primary-dark)) 5%, transparent);
806
+ }
807
+ }
770
808
  .bg-primary\/10 {
771
809
  background-color: color-mix(in srgb, light-dark(oklch(0.68 0.19 210), oklch(0.72 0.2 210)) 10%, transparent);
772
810
  @supports (color: color-mix(in lab, red, red)) {
@@ -857,6 +895,9 @@
857
895
  .bg-clip-text {
858
896
  background-clip: text;
859
897
  }
898
+ .object-cover {
899
+ object-fit: cover;
900
+ }
860
901
  .p-0 {
861
902
  padding: calc(var(--spacing) * 0);
862
903
  }
@@ -947,6 +988,9 @@
947
988
  .align-middle {
948
989
  vertical-align: middle;
949
990
  }
991
+ .font-mono {
992
+ font-family: var(--font-mono);
993
+ }
950
994
  .text-2xl {
951
995
  font-size: var(--text-2xl);
952
996
  line-height: var(--tw-leading, var(--text-2xl--line-height));
@@ -1016,6 +1060,9 @@
1016
1060
  var(--color-accent-foreground-dark)
1017
1061
  );
1018
1062
  }
1063
+ .text-amber-600 {
1064
+ color: var(--color-amber-600);
1065
+ }
1019
1066
  .text-card-foreground {
1020
1067
  color: light-dark(
1021
1068
  var(--color-card-foreground-light),
@@ -1037,6 +1084,9 @@
1037
1084
  .text-foreground {
1038
1085
  color: light-dark(var(--color-foreground-light), var(--color-foreground-dark));
1039
1086
  }
1087
+ .text-green-600 {
1088
+ color: var(--color-green-600);
1089
+ }
1040
1090
  .text-muted-foreground {
1041
1091
  color: light-dark(
1042
1092
  var(--color-muted-foreground-light),
@@ -1124,6 +1174,10 @@
1124
1174
  outline-style: var(--tw-outline-style);
1125
1175
  outline-width: 1px;
1126
1176
  }
1177
+ .blur {
1178
+ --tw-blur: blur(8px);
1179
+ filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
1180
+ }
1127
1181
  .filter {
1128
1182
  filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
1129
1183
  }
@@ -1288,6 +1342,16 @@
1288
1342
  }
1289
1343
  }
1290
1344
  }
1345
+ .hover\:bg-primary\/5 {
1346
+ &:hover {
1347
+ @media (hover: hover) {
1348
+ background-color: color-mix(in srgb, light-dark(oklch(0.68 0.19 210), oklch(0.72 0.2 210)) 5%, transparent);
1349
+ @supports (color: color-mix(in lab, red, red)) {
1350
+ background-color: color-mix(in oklab, light-dark(var(--color-primary-light), var(--color-primary-dark)) 5%, transparent);
1351
+ }
1352
+ }
1353
+ }
1354
+ }
1291
1355
  .hover\:bg-primary\/90 {
1292
1356
  &:hover {
1293
1357
  @media (hover: hover) {
@@ -1629,6 +1693,26 @@ body {
1629
1693
  inherits: false;
1630
1694
  initial-value: 0;
1631
1695
  }
1696
+ @property --tw-rotate-x {
1697
+ syntax: "*";
1698
+ inherits: false;
1699
+ }
1700
+ @property --tw-rotate-y {
1701
+ syntax: "*";
1702
+ inherits: false;
1703
+ }
1704
+ @property --tw-rotate-z {
1705
+ syntax: "*";
1706
+ inherits: false;
1707
+ }
1708
+ @property --tw-skew-x {
1709
+ syntax: "*";
1710
+ inherits: false;
1711
+ }
1712
+ @property --tw-skew-y {
1713
+ syntax: "*";
1714
+ inherits: false;
1715
+ }
1632
1716
  @property --tw-space-y-reverse {
1633
1717
  syntax: "*";
1634
1718
  inherits: false;
@@ -1846,6 +1930,11 @@ body {
1846
1930
  --tw-translate-x: 0;
1847
1931
  --tw-translate-y: 0;
1848
1932
  --tw-translate-z: 0;
1933
+ --tw-rotate-x: initial;
1934
+ --tw-rotate-y: initial;
1935
+ --tw-rotate-z: initial;
1936
+ --tw-skew-x: initial;
1937
+ --tw-skew-y: initial;
1849
1938
  --tw-space-y-reverse: 0;
1850
1939
  --tw-space-x-reverse: 0;
1851
1940
  --tw-divide-y-reverse: 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opensaas/stack-ui",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Composable React UI components for OpenSaas Stack",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -45,7 +45,7 @@
45
45
  "next": "^15.0.0 || ^16.0.0",
46
46
  "react": "^19.0.0",
47
47
  "react-dom": "^19.0.0",
48
- "@opensaas/stack-core": "0.1.0"
48
+ "@opensaas/stack-core": "0.1.1"
49
49
  },
50
50
  "dependencies": {
51
51
  "@radix-ui/react-checkbox": "^1.3.3",
@@ -59,6 +59,7 @@
59
59
  "class-variance-authority": "^0.7.1",
60
60
  "clsx": "^2.1.1",
61
61
  "date-fns": "^4.1.0",
62
+ "lucide-react": "^0.552.0",
62
63
  "react-hook-form": "^7.54.2",
63
64
  "tailwind-merge": "^3.3.1"
64
65
  },
@@ -71,13 +72,14 @@
71
72
  "@types/react": "^19.2.2",
72
73
  "@types/react-dom": "^19.2.2",
73
74
  "@vitejs/plugin-react": "^5.0.0",
75
+ "@vitest/coverage-v8": "^4.0.4",
74
76
  "happy-dom": "^20.0.0",
75
77
  "postcss": "^8.4.49",
76
78
  "postcss-cli": "^11.0.0",
77
79
  "tailwindcss": "^4.0.0",
78
80
  "typescript": "^5.9.3",
79
81
  "vitest": "^4.0.0",
80
- "@opensaas/stack-core": "0.1.0"
82
+ "@opensaas/stack-core": "0.1.1"
81
83
  },
82
84
  "scripts": {
83
85
  "build": "tsc && npm run build:css",
@@ -65,6 +65,7 @@ export function ItemFormClient({
65
65
  try {
66
66
  // Transform relationship fields to Prisma format
67
67
  // Filter out password fields with isSet objects (unchanged passwords)
68
+ // File/Image fields: pass File objects through (Next.js will serialize them)
68
69
  const transformedData: Record<string, unknown> = {}
69
70
  for (const [fieldName, value] of Object.entries(formData)) {
70
71
  const fieldConfig = fields[fieldName]
@@ -93,7 +94,8 @@ export function ItemFormClient({
93
94
  }
94
95
  }
95
96
  } else {
96
- // Non-relationship field: pass through
97
+ // Non-relationship field: pass through (including File objects for file/image fields)
98
+ // File objects will be serialized by Next.js server action
97
99
  transformedData[fieldName] = value
98
100
  }
99
101
  }
@@ -0,0 +1,223 @@
1
+ 'use client'
2
+
3
+ import React, { useCallback, useState } from 'react'
4
+ import type { FileMetadata } from '@opensaas/stack-core'
5
+ import { Button } from '../../primitives/button.js'
6
+ import { Input } from '../../primitives/input.js'
7
+ import { Label } from '../../primitives/label.js'
8
+ import { Upload, X, File, Check } from 'lucide-react'
9
+
10
+ export interface FileFieldProps {
11
+ name: string
12
+ value: File | FileMetadata | null
13
+ onChange: (value: File | FileMetadata | null) => void
14
+ label?: string
15
+ error?: string
16
+ disabled?: boolean
17
+ required?: boolean
18
+ mode?: 'read' | 'edit'
19
+ helpText?: string
20
+ placeholder?: string
21
+ }
22
+
23
+ /**
24
+ * File upload field with drag-and-drop support
25
+ *
26
+ * Stores File objects in form state. The actual upload happens server-side
27
+ * during form submission via field hooks.
28
+ */
29
+ export function FileField({
30
+ name,
31
+ value,
32
+ onChange,
33
+ label,
34
+ error,
35
+ disabled,
36
+ required,
37
+ mode = 'edit',
38
+ helpText,
39
+ placeholder = 'Choose a file or drag and drop',
40
+ }: FileFieldProps) {
41
+ const [isDragOver, setIsDragOver] = useState(false)
42
+
43
+ const handleFileSelect = useCallback(
44
+ (file: File) => {
45
+ // Store File object in form state
46
+ // Upload will happen server-side during form submission
47
+ onChange(file)
48
+ },
49
+ [onChange],
50
+ )
51
+
52
+ const handleDragOver = useCallback((e: React.DragEvent) => {
53
+ e.preventDefault()
54
+ setIsDragOver(true)
55
+ }, [])
56
+
57
+ const handleDragLeave = useCallback((e: React.DragEvent) => {
58
+ e.preventDefault()
59
+ setIsDragOver(false)
60
+ }, [])
61
+
62
+ const handleDrop = useCallback(
63
+ (e: React.DragEvent) => {
64
+ e.preventDefault()
65
+ setIsDragOver(false)
66
+
67
+ if (disabled || mode === 'read') return
68
+
69
+ const files = Array.from(e.dataTransfer.files)
70
+ if (files.length > 0) {
71
+ handleFileSelect(files[0])
72
+ }
73
+ },
74
+ [disabled, mode, handleFileSelect],
75
+ )
76
+
77
+ const handleInputChange = useCallback(
78
+ (e: React.ChangeEvent<HTMLInputElement>) => {
79
+ const files = Array.from(e.target.files || [])
80
+ if (files.length > 0) {
81
+ handleFileSelect(files[0])
82
+ }
83
+ },
84
+ [handleFileSelect],
85
+ )
86
+
87
+ const handleRemove = useCallback(() => {
88
+ onChange(null)
89
+ }, [onChange])
90
+
91
+ // Determine if value is File or FileMetadata
92
+ // Use duck typing instead of instanceof to support SSR
93
+ const isFile =
94
+ value &&
95
+ typeof value === 'object' &&
96
+ 'arrayBuffer' in value &&
97
+ typeof (value as { arrayBuffer?: unknown }).arrayBuffer === 'function'
98
+ const isFileMetadata = value && !isFile && typeof value === 'object' && 'url' in value
99
+
100
+ // Read-only mode
101
+ if (mode === 'read') {
102
+ return (
103
+ <div className="space-y-2">
104
+ {label && (
105
+ <Label htmlFor={name}>
106
+ {label}
107
+ {required && <span className="text-destructive ml-1">*</span>}
108
+ </Label>
109
+ )}
110
+ {isFileMetadata ? (
111
+ <div className="flex items-center gap-2 p-3 border rounded-md bg-muted">
112
+ <File className="h-4 w-4" />
113
+ <div className="flex-1 min-w-0">
114
+ <p className="text-sm font-medium truncate">
115
+ {(value as FileMetadata).originalFilename}
116
+ </p>
117
+ <p className="text-xs text-muted-foreground">
118
+ {formatFileSize((value as FileMetadata).size)}
119
+ </p>
120
+ </div>
121
+ <Button
122
+ type="button"
123
+ variant="outline"
124
+ size="sm"
125
+ onClick={() => window.open((value as FileMetadata).url, '_blank')}
126
+ >
127
+ Download
128
+ </Button>
129
+ </div>
130
+ ) : (
131
+ <p className="text-sm text-muted-foreground">No file uploaded</p>
132
+ )}
133
+ </div>
134
+ )
135
+ }
136
+
137
+ // Edit mode
138
+ return (
139
+ <div className="space-y-2">
140
+ {label && (
141
+ <Label htmlFor={name}>
142
+ {label}
143
+ {required && <span className="text-destructive ml-1">*</span>}
144
+ </Label>
145
+ )}
146
+
147
+ {isFile || isFileMetadata ? (
148
+ // File selected/uploaded - show file info
149
+ <div className="flex items-center gap-2 p-3 border rounded-md">
150
+ <Check className="h-4 w-4 text-green-600" />
151
+ <div className="flex-1 min-w-0">
152
+ <p className="text-sm font-medium truncate">
153
+ {isFile ? (value as File).name : (value as FileMetadata).originalFilename}
154
+ </p>
155
+ <p className="text-xs text-muted-foreground">
156
+ {formatFileSize(isFile ? (value as File).size : (value as FileMetadata).size)}
157
+ {isFileMetadata && ` • ${(value as FileMetadata).mimeType}`}
158
+ {isFile && ' • Will upload on save'}
159
+ </p>
160
+ </div>
161
+ {isFileMetadata && (
162
+ <Button
163
+ type="button"
164
+ variant="outline"
165
+ size="sm"
166
+ onClick={() => window.open((value as FileMetadata).url, '_blank')}
167
+ >
168
+ View
169
+ </Button>
170
+ )}
171
+ <Button
172
+ type="button"
173
+ variant="ghost"
174
+ size="sm"
175
+ onClick={handleRemove}
176
+ disabled={disabled}
177
+ >
178
+ <X className="h-4 w-4" />
179
+ </Button>
180
+ </div>
181
+ ) : (
182
+ // No file - show upload area
183
+ <>
184
+ <div
185
+ className={`
186
+ relative border-2 border-dashed rounded-md p-6
187
+ transition-colors cursor-pointer
188
+ ${isDragOver ? 'border-primary bg-primary/5' : 'border-muted-foreground/25'}
189
+ ${disabled ? 'opacity-50 cursor-not-allowed' : 'hover:border-primary hover:bg-primary/5'}
190
+ `}
191
+ onDragOver={handleDragOver}
192
+ onDragLeave={handleDragLeave}
193
+ onDrop={handleDrop}
194
+ >
195
+ <Input
196
+ id={name}
197
+ type="file"
198
+ onChange={handleInputChange}
199
+ disabled={disabled}
200
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
201
+ />
202
+
203
+ <div className="flex flex-col items-center gap-2 text-center">
204
+ <Upload className="h-8 w-8 text-muted-foreground" />
205
+ <p className="text-sm font-medium">{placeholder}</p>
206
+ {helpText && <p className="text-xs text-muted-foreground">{helpText}</p>}
207
+ </div>
208
+ </div>
209
+ </>
210
+ )}
211
+
212
+ {error && <p className="text-sm text-destructive">{error}</p>}
213
+ </div>
214
+ )
215
+ }
216
+
217
+ function formatFileSize(bytes: number): string {
218
+ if (bytes === 0) return '0 Bytes'
219
+ const k = 1024
220
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
221
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
222
+ return `${Math.round((bytes / Math.pow(k, i)) * 100) / 100} ${sizes[i]}`
223
+ }