Skip to content

Commit a0eca8b

Browse files
authored
feat: allow to configure min lines (#34)
1 parent 46961bc commit a0eca8b

File tree

4 files changed

+152
-151
lines changed

4 files changed

+152
-151
lines changed

packages/sel-editor-react/src/sel-editor.stories.tsx

Lines changed: 85 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { parseAbi } from "viem";
44

55
import { SELEditor } from "./sel-editor";
66

7+
import type { SELEditorProps } from "./sel-editor";
78
import type { SELSchema } from "@seljs/schema";
89
import type { Meta, StoryObj } from "@storybook/react";
910

@@ -51,181 +52,137 @@ const emptySchema: SELSchema = {
5152
macros: [],
5253
};
5354

54-
const meta: Meta<typeof SELEditor> = {
55+
interface PlaygroundArgs extends SELEditorProps {
56+
linting: boolean;
57+
autocomplete: boolean;
58+
semanticHighlighting: boolean;
59+
typeDisplay: boolean;
60+
minLines: number;
61+
}
62+
63+
function renderPlayground({
64+
linting,
65+
autocomplete,
66+
semanticHighlighting,
67+
typeDisplay,
68+
minLines,
69+
...props
70+
}: PlaygroundArgs) {
71+
return (
72+
<SELEditor
73+
{...props}
74+
features={{
75+
linting,
76+
autocomplete,
77+
semanticHighlighting,
78+
typeDisplay,
79+
view: { minLines },
80+
}}
81+
/>
82+
);
83+
}
84+
85+
const meta: Meta<PlaygroundArgs> = {
5586
title: "SELEditor",
56-
component: SELEditor,
5787
parameters: {
5888
layout: "padded",
5989
},
6090
decorators: [
61-
(Story) => (
62-
<div style={{ maxWidth: 700, margin: "0 auto" }}>
91+
(Story, context) => (
92+
<div
93+
style={{
94+
maxWidth: 700,
95+
margin: "0 auto",
96+
...(context.args.dark
97+
? { background: "#1e1e1e", padding: 24, borderRadius: 8 }
98+
: {}),
99+
}}
100+
>
63101
<Story />
64102
</div>
65103
),
66104
],
67105
args: {
68106
schema: erc20Schema,
69-
},
70-
};
71-
72-
type Story = StoryObj<typeof SELEditor>;
73-
74-
const Default: Story = {};
75-
76-
const WithExpression: Story = {
77-
args: {
78-
value: "erc20.balanceOf(user) > 1000",
79-
},
107+
value: "erc20.balanceOf(user) > 0",
108+
dark: false,
109+
readOnly: false,
110+
placeholder: "",
111+
linting: true,
112+
autocomplete: true,
113+
semanticHighlighting: true,
114+
typeDisplay: false,
115+
minLines: 1,
116+
},
117+
argTypes: {
118+
value: { control: "text" },
119+
dark: { control: "boolean" },
120+
readOnly: { control: "boolean" },
121+
placeholder: { control: "text" },
122+
linting: { control: "boolean" },
123+
autocomplete: { control: "boolean" },
124+
semanticHighlighting: { control: "boolean" },
125+
typeDisplay: { control: "boolean" },
126+
minLines: { control: { type: "number", min: 1, max: 20 } },
127+
schema: { control: false },
128+
onChange: { control: false },
129+
checkerOptions: { control: false },
130+
className: { control: false },
131+
},
132+
render: renderPlayground,
133+
};
134+
135+
type Story = StoryObj<PlaygroundArgs>;
136+
137+
const Playground: Story = {
138+
name: "Playground",
80139
};
81140

82141
const ComplexExpression: Story = {
142+
name: "Complex Expression",
83143
args: {
84144
value:
85145
'erc20.balanceOf(user) > 0 && erc20.allowance(user, vault.getUserDeposit(user)) >= int(1000) ? "eligible" : "not eligible"',
86146
},
87147
};
88148

89-
const WithValidation: Story = {
149+
const Validation: Story = {
90150
name: "Built-in Validation",
91151
args: {
92152
value: "erc20.unknownMethod(user)",
93153
},
94154
};
95155

96-
const WithLintRules: Story = {
97-
name: "Lint Rules (Redundant Bool)",
156+
const LintRules: Story = {
157+
name: "Lint Rules",
98158
args: {
99159
value: "erc20.balanceOf(user) > 0 == true",
100160
checkerOptions: { rules: rules.builtIn },
101161
},
102162
};
103163

104-
const WithLintSelfComparison: Story = {
105-
name: "Lint Rules (Self Comparison)",
106-
args: {
107-
value: "erc20.totalSupply() == erc20.totalSupply()",
108-
checkerOptions: { rules: rules.builtIn },
109-
},
110-
};
111-
112-
const WithLintConstantCondition: Story = {
113-
name: "Lint Rules (Constant Condition)",
114-
args: {
115-
value: "true && erc20.balanceOf(user) > 0",
116-
checkerOptions: { rules: rules.builtIn },
117-
},
118-
};
119-
120-
const RequireTypeBoolPass: Story = {
121-
name: "Require Type Bool (Pass)",
122-
args: {
123-
value: "erc20.balanceOf(user) > 0",
124-
checkerOptions: { rules: [rules.requireType("bool"), ...rules.builtIn] },
125-
},
126-
};
127-
128-
const RequireTypeBoolFail: Story = {
129-
name: "Require Type Bool (Fail)",
164+
const RequireTypeBool: Story = {
165+
name: "Require Type Bool",
130166
args: {
131167
value: "erc20.balanceOf(user)",
132168
checkerOptions: { rules: [rules.requireType("bool"), ...rules.builtIn] },
133169
},
134170
};
135171

136-
const ReadOnly: Story = {
137-
args: {
138-
value: "erc20.balanceOf(user) > 0",
139-
readOnly: true,
140-
},
141-
};
142-
143-
const WithPlaceholder: Story = {
144-
args: {
145-
placeholder: "Enter a CEL expression, e.g. erc20.balanceOf(user) > 0",
146-
},
147-
};
148-
149-
const DarkMode: Story = {
150-
args: {
151-
value: "erc20.balanceOf(user) > 1000",
152-
dark: true,
153-
},
154-
decorators: [
155-
(Story) => (
156-
<div
157-
style={{
158-
background: "#1e1e1e",
159-
padding: 24,
160-
borderRadius: 8,
161-
}}
162-
>
163-
<Story />
164-
</div>
165-
),
166-
],
167-
};
168-
169172
const EmptySchema: Story = {
173+
name: "Empty Schema",
170174
args: {
171175
schema: emptySchema,
172176
value: "some_expression > 0",
173177
},
174178
};
175179

176-
const LiveEditing: Story = {
177-
name: "Live Editing (try autocomplete)",
178-
args: {
179-
placeholder: "Start typing... try 'erc' or 'user' and press Ctrl+Space",
180-
},
181-
};
182-
183-
const WithTypeDisplay: Story = {
184-
name: "Type Display",
185-
args: {
186-
value: "erc20.balanceOf(user) > 0",
187-
features: { typeDisplay: true },
188-
},
189-
};
190-
191-
const TypeDisplayDark: Story = {
192-
name: "Type Display (Dark)",
193-
args: {
194-
value: "erc20.balanceOf(user)",
195-
features: { typeDisplay: true },
196-
dark: true,
197-
},
198-
decorators: [
199-
(Story) => (
200-
<div
201-
style={{
202-
background: "#1e1e1e",
203-
padding: 24,
204-
borderRadius: 8,
205-
}}
206-
>
207-
<Story />
208-
</div>
209-
),
210-
],
211-
};
212-
213180
export default meta;
214181
export {
215-
Default,
216-
WithExpression,
182+
Playground,
217183
ComplexExpression,
218-
WithValidation,
219-
WithLintRules,
220-
WithLintSelfComparison,
221-
WithLintConstantCondition,
222-
RequireTypeBoolPass,
223-
RequireTypeBoolFail,
224-
ReadOnly,
225-
WithPlaceholder,
226-
DarkMode,
184+
Validation,
185+
LintRules,
186+
RequireTypeBool,
227187
EmptySchema,
228-
LiveEditing,
229-
WithTypeDisplay,
230-
TypeDisplayDark,
231188
};

packages/sel-editor-react/src/use-sel-editor.ts

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,46 +18,66 @@ export function useSELEditor(
1818
config: Omit<SELEditorConfig, "parent">,
1919
): UseSELEditorResult {
2020
const viewRef = useRef<EditorView | null>(null);
21+
const [parent, setParent] = useState<HTMLElement | null>(null);
2122
const [view, setView] = useState<EditorView | null>(null);
2223
const [value, setValue] = useState(config.value ?? "");
2324
const [valid, setValid] = useState(true);
2425
const configRef = useRef(config);
2526
configRef.current = config;
2627

27-
const ref = useCallback<RefCallback<HTMLElement>>(
28-
(node) => {
28+
const ref = useCallback<RefCallback<HTMLElement>>((node) => {
29+
setParent(node);
30+
}, []);
31+
32+
useEffect(() => {
33+
if (!parent) {
2934
if (viewRef.current) {
3035
viewRef.current.destroy();
3136
viewRef.current = null;
3237
setView(null);
3338
}
3439

35-
if (!node) {
36-
return;
37-
}
40+
return;
41+
}
3842

39-
const currentConfig = configRef.current;
40-
const editor = createSELEditor({
41-
...currentConfig,
42-
parent: node,
43-
onChange: (newValue, isValid) => {
44-
setValue(newValue);
45-
setValid(isValid);
46-
currentConfig.onChange?.(newValue, isValid);
47-
},
48-
});
49-
viewRef.current = editor;
50-
setView(editor);
51-
},
52-
[config.schema],
53-
);
43+
if (viewRef.current) {
44+
viewRef.current.destroy();
45+
viewRef.current = null;
46+
}
47+
48+
const currentConfig = configRef.current;
49+
const editor = createSELEditor({
50+
...currentConfig,
51+
parent,
52+
onChange: (newValue, isValid) => {
53+
setValue(newValue);
54+
setValid(isValid);
55+
currentConfig.onChange?.(newValue, isValid);
56+
},
57+
});
58+
viewRef.current = editor;
59+
setView(editor);
5460

55-
useEffect(() => {
5661
return () => {
57-
viewRef.current?.destroy();
62+
editor.destroy();
5863
viewRef.current = null;
64+
setView(null);
5965
};
60-
}, []);
66+
}, [
67+
parent,
68+
config.schema,
69+
config.dark,
70+
config.readOnly,
71+
config.placeholder,
72+
config.checkerOptions,
73+
config.features?.linting,
74+
config.features?.autocomplete,
75+
config.features?.semanticHighlighting,
76+
config.features?.typeDisplay,
77+
config.features?.view?.minLines,
78+
config.features?.tooltip?.parent,
79+
config.features?.tooltip?.position,
80+
]);
6181

6282
return { ref, view, value, valid };
6383
}

packages/sel-editor/src/editor/editor-config.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,18 @@ export const buildExtensions = (config: SELEditorConfig): Extension[] => {
9090
extensions.push(createTypeDisplay(checker, config.dark ?? false));
9191
}
9292

93+
// View configuration
94+
const minLines = config.features?.view?.minLines;
95+
if (minLines && minLines > 1) {
96+
const minHeight = `${String(Math.max(1, minLines) * 1.4)}em`;
97+
extensions.push(
98+
EditorView.theme({
99+
"&": { minHeight },
100+
".cm-content": { minHeight },
101+
}),
102+
);
103+
}
104+
93105
// Tooltip configuration
94106
if (config.features?.tooltip) {
95107
extensions.push(tooltips(config.features.tooltip));

0 commit comments

Comments
 (0)