@startsimpli/ui 0.4.59 → 0.4.60

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@startsimpli/ui",
3
- "version": "0.4.59",
3
+ "version": "0.4.60",
4
4
  "description": "Shared UI components package for StartSimpli applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -42,8 +42,8 @@
42
42
  "next": "^14.0.0 || ^15.0.0 || ^16.0.0",
43
43
  "react": "^18.0.0 || ^19.0.0",
44
44
  "react-dom": "^18.0.0 || ^19.0.0",
45
- "@startsimpli/auth": "0.4.30",
46
- "@startsimpli/api": "0.5.44"
45
+ "@startsimpli/api": "0.5.44",
46
+ "@startsimpli/auth": "0.4.30"
47
47
  },
48
48
  "peerDependenciesMeta": {
49
49
  "@startsimpli/api": {
@@ -24,18 +24,42 @@ function fieldId(nodeId: string, key: string): string {
24
24
  return `nodeparam-${nodeId}-${key}`;
25
25
  }
26
26
 
27
+ /** Field label with a required-marker asterisk when the param is required. */
28
+ function FieldLabel({
29
+ id,
30
+ label,
31
+ required,
32
+ }: {
33
+ id: string;
34
+ label: string;
35
+ required?: boolean;
36
+ }) {
37
+ return (
38
+ <label htmlFor={id} className="text-xs font-medium text-foreground">
39
+ {label}
40
+ {required && (
41
+ <span className="ml-0.5 text-destructive" aria-hidden="true">
42
+ *
43
+ </span>
44
+ )}
45
+ </label>
46
+ );
47
+ }
48
+
27
49
  function SchemaField({
28
50
  nodeId,
29
51
  name,
30
52
  schema,
31
53
  value,
32
54
  onChange,
55
+ required,
33
56
  }: {
34
57
  nodeId: string;
35
58
  name: string;
36
59
  schema: JsonSchema;
37
60
  value: unknown;
38
61
  onChange: (next: unknown) => void;
62
+ required?: boolean;
39
63
  }) {
40
64
  const id = fieldId(nodeId, name);
41
65
  const label = schema.title ?? name;
@@ -43,11 +67,10 @@ function SchemaField({
43
67
  if (schema.enum && schema.enum.length > 0) {
44
68
  return (
45
69
  <div className="flex flex-col gap-1">
46
- <label htmlFor={id} className="text-xs font-medium text-foreground">
47
- {label}
48
- </label>
70
+ <FieldLabel id={id} label={label} required={required} />
49
71
  <select
50
72
  id={id}
73
+ aria-required={required || undefined}
51
74
  value={String(value ?? '')}
52
75
  onChange={(e) => onChange(e.target.value)}
53
76
  className="rounded-md border border-border bg-background px-2 py-1.5 text-sm outline-none focus:ring-2 focus:ring-primary"
@@ -67,14 +90,13 @@ function SchemaField({
67
90
  <div className="flex items-center gap-2">
68
91
  <input
69
92
  id={id}
93
+ aria-required={required || undefined}
70
94
  type="checkbox"
71
95
  checked={Boolean(value)}
72
96
  onChange={(e) => onChange(e.target.checked)}
73
97
  className="h-4 w-4 rounded border-border"
74
98
  />
75
- <label htmlFor={id} className="text-xs font-medium text-foreground">
76
- {label}
77
- </label>
99
+ <FieldLabel id={id} label={label} required={required} />
78
100
  </div>
79
101
  );
80
102
  }
@@ -82,11 +104,10 @@ function SchemaField({
82
104
  if (schema.type === 'number' || schema.type === 'integer') {
83
105
  return (
84
106
  <div className="flex flex-col gap-1">
85
- <label htmlFor={id} className="text-xs font-medium text-foreground">
86
- {label}
87
- </label>
107
+ <FieldLabel id={id} label={label} required={required} />
88
108
  <input
89
109
  id={id}
110
+ aria-required={required || undefined}
90
111
  type="number"
91
112
  value={value === undefined || value === null ? '' : Number(value)}
92
113
  onChange={(e) =>
@@ -101,11 +122,10 @@ function SchemaField({
101
122
  // default: string
102
123
  return (
103
124
  <div className="flex flex-col gap-1">
104
- <label htmlFor={id} className="text-xs font-medium text-foreground">
105
- {label}
106
- </label>
125
+ <FieldLabel id={id} label={label} required={required} />
107
126
  <input
108
127
  id={id}
128
+ aria-required={required || undefined}
109
129
  type="text"
110
130
  value={value === undefined || value === null ? '' : String(value)}
111
131
  onChange={(e) => onChange(e.target.value)}
@@ -142,6 +162,7 @@ function ParamForm({
142
162
  const params = (node.parameters ?? {}) as Record<string, unknown>;
143
163
  const properties = schema?.properties ?? {};
144
164
  const keys = Object.keys(properties);
165
+ const requiredKeys = new Set(schema?.required ?? []);
145
166
 
146
167
  const handleChange = (key: string, next: unknown) => {
147
168
  const updated = { ...params, [key]: next };
@@ -165,6 +186,7 @@ function ParamForm({
165
186
  name={key}
166
187
  schema={properties[key]}
167
188
  value={params[key]}
189
+ required={requiredKeys.has(key)}
168
190
  onChange={(next) => handleChange(key, next)}
169
191
  />
170
192
  ))}
@@ -71,4 +71,30 @@ describe('NodeInspector', () => {
71
71
  await user.click(screen.getByRole('tab', { name: /output/i }));
72
72
  expect(screen.getByText(/"foo": "out"/)).toBeInTheDocument();
73
73
  });
74
+
75
+ it('marks required fields (schema.required[]) with an indicator', () => {
76
+ const requiredType: typeof nodeType = {
77
+ ...nodeType,
78
+ parameterSchema: {
79
+ type: 'object',
80
+ required: ['to'],
81
+ properties: {
82
+ to: { type: 'string', title: 'Recipient' },
83
+ retries: { type: 'number', title: 'Retries' },
84
+ },
85
+ },
86
+ };
87
+ render(<NodeInspector node={node} nodeType={requiredType} />);
88
+ // required field exposes aria-required + a visible "*" marker
89
+ expect(screen.getByLabelText(/Recipient/)).toHaveAttribute(
90
+ 'aria-required',
91
+ 'true'
92
+ );
93
+ // non-required field is not marked
94
+ expect(screen.getByLabelText('Retries')).not.toHaveAttribute(
95
+ 'aria-required',
96
+ 'true'
97
+ );
98
+ expect(screen.getByText('*')).toBeInTheDocument();
99
+ });
74
100
  });