diff --git a/SIPS/sip-23.md b/SIPS/sip-23.md new file mode 100644 index 0000000..faba2f1 --- /dev/null +++ b/SIPS/sip-23.md @@ -0,0 +1,673 @@ +--- +sip: 23 +title: JSX for Snap interfaces +status: Draft +discussions-to: https://github.com/MetaMask/SIPs/discussions/137 +author: Maarten Zuidhoorn (@mrtenz) +created: 2024-03-22 +--- + +## Abstract + +This proposal presents a specification for the use of JSX (JavaScript XML) in +Snap interfaces. It includes a set of definitions, components, and rules for +usage. The document delves into the details of various components such as +`SnapElement`, `SnapComponent`, and `SnapNode`. It also elaborates on specific +elements like , `Address`, `Bold`, `Button`, and `Text` among others, explaining +their structure and purpose. + +Moreover, the proposal takes into account backward compatibility considerations. +It outlines a systematic approach to translate the components from the previous +SIP-7 format into the newly proposed format. + +The proposal aims to create a more robust, flexible, and versatile system for +Snap interfaces. It strives to enhance the user experience and improve the +efficiency of the system by offering a structured and standardised component +framework. + +## Motivation + +The motivation behind this proposal is to leverage JSX, a popular syntax +extension for JavaScript, for designing and implementing Snap interfaces. JSX +offers several advantages that make it a preferred choice among developers. +Primarily, it allows for writing HTML-like syntax directly in the JavaScript +code, which makes it more readable and intuitive. This facilitates easier +development and maintenance of complex UI structures. + +Furthermore, JSX is universally recognised and widely adopted in the JavaScript +community, especially within the React ecosystem. By using JSX for Snap +interfaces, we enable a vast number of developers familiar with this syntax to +contribute effectively in a shorter time frame. + +Adopting JSX also ensures better integration with modern development tools and +practices. It allows for integration with linters, formatters, and type +checkers, thus improving the development workflow. + +In summary, the use of JSX in Snap interfaces aims to improve developer +experience, enhance code maintainability, and ultimately, lead to the creation +of more robust and efficient Snap interfaces. + +## Specification + +> Formal specifications are written in TypeScript. + +### Language + +The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", +"SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" written in +uppercase in this document are to be interpreted as described in +[RFC 2119](https://www.ietf.org/rfc/rfc2119.txt). + +### Definitions + +#### Key + +A JSX key-like value, i.e., a `string`, `number`, or `null`. + +```typescript +type Key = string | number | null; +``` + +#### SnapElement + +A rendered JSX element, i.e., an object with a `type`, `props`, and `key`. + +```typescript +type SnapElement<Props extends JsonObject = Record<string, never>> = { + type: string; + props: Props; + key: Key | null; +}; +``` + +#### SnapComponent + +A JSX component, i.e., a function which accepts `props`, and returns a +`SnapElement`. All components MUST accept a `key` property in addition to the +regular props. + +```typescript +type SnapComponent<Props extends JsonObject = Record<string, never>> = + (props: Props & { key: Key }) => SnapElement<Props>; +``` + +#### SnapNode + +A rendered JSX node, i.e., a `SnapElement`, `string`, `null`, or an array of +`SnapNode`s. Note that HTML elements are not supported in Snap nodes. + +```typescript +type SnapNode = + | SnapElement + | string + | null + | SnapNode[]; +``` + +### Interface structure + +The Snap interface structure is defined using JSX components, and consists of: + +- A container element, `Container`, which wraps the entire interface. +- A box element, `Box`, which contains the main content of the interface. +- An optional footer element, `Footer`, which appears at the bottom of the + interface. + +![Snap interface structure](../assets/sip-23/interface-structure.png) + +#### Example + +Below is an example of a simple Snap interface structure using JSX components: + +```typescript jsx +<Container> + <Box> + <Heading>My Snap</Heading> + <Text>Hello, world!</Text> + </Box> + <Footer> + <Button onClick={handleClick}>Click me</Button> + </Footer> +</Container> +``` + +Each of the components used in the example is defined in the following sections. + +### Components + +#### Address + +The `Address` component renders a text element with an address. It accepts an +`address` prop which is a string representing an Ethereum address. + +```typescript +type AddressProps = { + address: `0x${string}`; +}; + +const Address: SnapComponent<AddressProps>; +``` + +##### Example + +```typescript jsx +<Address address="0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520" /> +``` + +#### Bold + +The `Bold` component renders a text element in bold. It accepts a `children` +prop which can be a `string`, an italic element, null, or an array of children. +The component MUST be used inside a `Text` component. + +```typescript +type BoldChildren = string | ItalicElement | null; +type BoldProps = { + children: BoldChildren | BoldChildren[]; +}; + +const Bold: SnapComponent<BoldProps>; +``` + +##### Example + +```typescript jsx +<Text> + Hello, <Bold>world</Bold>! +</Text> +``` + +#### Box + +The `Box` component renders a box element. It accepts a `children` prop which +can be a `SnapNode`. The component is used to group elements together. + +In addition, the component accepts alignment and direction props: + +- The `direction` prop specifies the direction of the box layout. It MUST be one + of `'vertical'`, `'horizontal'`, or `undefined`. The default value is + `horizontal`. +- The `alignment` prop specifies the alignment of the box items. It MUST be one + of `'start'`, `'center'`, `'end'`, `'space-between'`, `'space-around'`, or + `undefined`. The default value is `start`. + +```typescript +type BoxProps = { + children: SnapNode; + direction?: 'vertical' | 'horizontal' | undefined; + alignment?: + | 'start' + | 'center' + | 'end' + | 'space-between' + | 'space-around' + | undefined; +}; + +const Box: SnapComponent<BoxProps>; +``` + +##### Example + +```typescript jsx +<Box alignment="center"> + <Text>Hello, world!</Text> +</Box> +``` + +#### Button + +The `Button` component renders a button element. It accepts a `children` prop +which can be a `string` or an array of `string`s. It also accepts the following +optional props: + +- The `name` prop is a string representing the button name. It is used to + identify the button in a form submission. +- The `type` prop is a string representing the button type. It MUST be one of + `'button'`, `'submit'`, or `undefined`. The default value is `button`. +- The `variant` prop is a string representing the button variant. It MUST be one + of `'primary'`, `'destructive'`, or `undefined`. The default value is + `primary`. +- The `disabled` prop is a boolean representing the button state. The default + value is `false`. +- The `onClick` prop is a function to be called when the button is clicked. + +```typescript +type ButtonProps = { + children: string | string[]; + name?: string | undefined; + type?: 'button' | 'submit' | undefined; + variant?: 'primary' | 'destructive' | undefined; + disabled?: boolean | undefined; + onClick?: (() => void) | undefined; +}; + +const Button: SnapComponent<ButtonProps>; +``` + +##### Example + +```typescript jsx +<Button type="submit" variant="primary" onClick={handleClick}>Click me</Button> +``` + +#### Container + +The `Container` component renders a container element. It accepts a `children` +prop which can be a tuple of `BoxElement` and `FooterElement` or a single +`BoxElement`. + +```typescript +type ContainerProps = { + children: [ + BoxElement, + FooterElement + ] | [ + BoxElement + ] +}; + +const Container: SnapComponent<ContainerProps>; +``` + +##### Example + +```typescript jsx +<Container> + <Box> + <Text>Hello, world!</Text> + </Box> + <Footer> + <Button onClick={handleClick}>Click me</Button> + </Footer> +</Container> +``` + +#### Copyable + +The `Copyable` component renders a text element with a copy button. It accepts a +`value` prop which is a string to be copied to the clipboard when the button is +clicked. The component also accepts an optional `sensitive` prop which can be +used to indicate that the value is sensitive and should be obscured when +displayed. + +```typescript +type CopyableProps = { + value: string; + sensitive?: boolean | undefined; +}; + +const Copyable: SnapComponent<CopyableProps>; +``` + +##### Example + +```typescript jsx +<Copyable value="0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520" /> +<Copyable value="my-secret-key" sensitive /> +``` + +#### Divider + +The `Divider` component renders a horizontal line. It does not accept any props. + +```typescript +const Divider: SnapComponent; +``` + +##### Example + +```typescript jsx +<Divider /> +``` + +#### Dropdown + +The `Dropdown` component renders a dropdown element. It accepts a name prop +which is a string representing the dropdown name, and a `value` prop which is a +string representing the selected value. The component also accepts one or more +option elements, which are rendered as dropdown options. + +```typescript +type DropdownProps = { + name: string; + value?: string | undefined; + children: OptionElement | OptionElement[]; +}; + +const Dropdown: SnapComponent<DropdownProps>; +``` + +##### Example + +```typescript jsx +<Dropdown name="color" value="red"> + <Option value="red">Red</Option> + <Option value="green">Green</Option> + <Option value="blue">Blue</Option> +</Dropdown> +``` + +#### Field + +The `Field` component renders a field element. It accepts an optional `label` +prop which is a string representing the field label, and a `children` prop +which can be an input element, and an optional button element, OR a dropdown +element. The component also accepts an `error` prop which is a string +representing an error message. + +```typescript +type FieldProps = { + label?: string | undefined; + error?: string | undefined; + children: [InputElement, ButtonElement] | InputElement | DropdownElement; +}; +``` + +##### Example + +```typescript jsx +<Field label="Name"> + <Input type="text" /> + <Button onClick={handleClick}>Action</Button> +</Field> +``` + +```typescript jsx +<Field label="Name"> + <Dropdown name="color" value="red"> + <Option value="red">Red</Option> + <Option value="green">Green</Option> + <Option value="blue">Blue</Option> + </Dropdown> +</Field> +``` + +#### Footer + +The `Footer` component renders a footer element. It accepts a `children` prop +which can be one or two buttons. + +```typescript +type FooterProps = { + children: ButtonElement | [ButtonElement, ButtonElement]; +}; + +const Footer: SnapComponent<FooterProps>; +``` + +##### Example + +```typescript jsx +<Footer> + <Button onClick={handleCancel}>Cancel</Button> + <Button onClick={handleConfirm}>Confirm</Button> +</Footer> + +<Footer> + <Button onClick={handleAction}>Action</Button> +</Footer> +``` + +#### Form + +The `Form` component renders a form element. It accepts a `children` prop which +can be one or more field elements. The component also accepts an optional +`onSubmit` prop which is a function to be called when the form is submitted, +passing the form data as an object. + +```typescript +type FormChildren = FieldElement | ButtonElement; +type FormProps = { + children: FormChildren | FormChildren[]; + name: string; + onSubmit?: ((formData: Record<string, string>) => void) | undefined; +}; +``` + +##### Example + +```typescript jsx +<Form onSubmit={handleSubmit}> + <Field label="Name"> + <Input type="text" /> + </Field> + <Button type="submit">Submit</Button> +</Form> +``` + +#### Heading + +The `Heading` component renders a heading element. It accepts a `children` prop +which can be a `string` or an array of `string`s. + +```typescript +type HeadingProps = { + children: StringElement; +}; + +const Heading: SnapComponent<HeadingProps>; +``` + +##### Example + +```typescript jsx +<Heading>My Snap</Heading> +``` + +#### Image + +The `Image` component renders an image element. It accepts a `src` prop which is +a string representing the image source. The component also accepts an optional +`alt` prop which is a string representing the image description. + +The `src` MUST be an SVG string. Other image formats are not supported, but +PNG and JPEG images MAY be used inside the SVG as data URIs. + +```typescript +type ImageProps = { + src: string; + alt?: string | undefined; +}; + +const Image: SnapComponent<ImageProps>; +``` + +##### Example + +```typescript jsx +<Image src="<svg>...</svg>" alt="My image" /> +``` + +#### Input + +The `Input` component renders an input element. It accepts a `name` prop which +is a string representing the input name, a `type` prop which is a string +representing the input type, and an optional `value` prop which is a string +representing the input value. The component also accepts an optional +`placeholder` prop which is a string representing the input placeholder, and an +optional `onChange` prop which is a function to be called when the input value +changes. + +The `type` MUST be one of `'text'`, `'password'`, or `'number'`. + +```typescript +type InputProps = { + name: string; + type?: 'text' | 'password' | 'number' | 'file' | undefined; + value?: string | undefined; + placeholder?: string | undefined; + onChange?: ((value: string) => void) | undefined; +}; + +const Input: SnapComponent<InputProps>; +``` + +##### Example + +```typescript jsx +<Input name="name" type="text" placeholder="Enter your name" onChange={handleChange} /> +``` + +#### Italic + +The `Italic` component renders a text element in italic. It accepts a `children` +prop which can be a `string` or an array of `string`s. The component MUST be +used inside a `Text` component. + +```typescript +type ItalicChildren = string | BoldElement | null; +type ItalicProps = { + children: ItalicChildren | ItalicChildren[]; +}; + +const Italic: SnapComponent<ItalicProps>; +``` + +##### Example + +```typescript jsx +<Text> + Hello, <Italic>world</Italic>! +</Text> +``` + +#### Link + +The `Link` component renders a link element. It accepts a `children` prop which +can be a `string`, an italic element, a bold element, or an array of these +elements. The component also accepts an `href` prop which is a string +representing the link URL. + +The `href` MUST be an absolute URL. Relative URLs are not supported. It MUST +start with `https://` or `mailto:`. + +```typescript +type LinkChildren = string | BoldElement | ItalicElement | null; +type LinkProps = { + children: LinkChildren | LinkChildren[]; + href: string; +}; + +const Link: SnapComponent<LinkProps>; +``` + +##### Example + +```typescript jsx +<Link href="https://example.com">Visit example.com</Link> +``` + +#### Row + +The `Row` component renders a key-value pair element. It accepts a `label` prop +which is a string representing the key, and a `children` prop which can be an +address, image, text, or value element. + +In addition, the component accepts an optional `variant` prop which can be +`'default'`, `'warning'`, or `'error'`. The default value is `'default'`. + +```typescript +type RowProps = { + label: string; + children: AddressElement | ImageElement | TextElement | ValueElement; + variant?: 'default' | 'warning' | 'error'; +}; + +const Row: SnapComponent<RowProps>; +``` + +##### Example + +```typescript jsx +<Row label="Address"> + <Address address="0x4bbeEB066eD09B7AEd07bF39EEe0460DFa261520" /> +</Row> +``` + +#### Spinner + +The `Spinner` component renders a spinner element. It does not accept any props. + +```typescript +const Spinner: SnapComponent; +``` + +##### Example + +```typescript jsx +<Spinner /> +``` + +#### Text + +The `Text` component renders a text element. It accepts a `children` prop which +can be a `string`, bold, italic, link element, null, or an array of these +elements. + +In contrast to SIP-7, the `Text` component does NOT support Markdown syntax. The +`Bold`, `Italic`, and `Link` components MAY be used to achieve similar effects. + +```typescript +type TextChildren = string | BoldElement | ItalicElement | LinkElement | null; +type TextProps = { + children: TextChildren | TextChildren[]; +}; + +const Text: SnapComponent<TextProps>; +``` + +#### Value + +The `Value` component renders a value element. It accepts a `value` and `extra` +prop, which are strings representing the value and extra information +respectively. It can be used to render a value with additional context in a +row element. + +```typescript +type ValueProps = { + value: string; + extra: string; +}; +``` + +##### Example + +```typescript jsx +<Row label="Amount"> + <Value value="0.1 ETH" extra="100 EUR" /> +</Row> +``` + +### JSX runtime + +The JSX runtime is a set of functions that are used to render JSX elements, +typically provided by a library like React. Since Snap interfaces are rendered +in a custom environment, the JSX runtime MUST be provided by the Snaps platform. + +The Snaps JSX runtime only supports the modern JSX factory functions, i.e., +`jsx` and `jsxs`. The runtime MUST NOT support the legacy `createElement` and +`Fragment` functions. + +Both the `jsx` and `jsxs` functions MUST return a `SnapElement`. + +## Backward compatibility + +To ensure backward compatibility with the previous SIP-7 format, the legacy +components MUST be translated into the new JSX format. + +This SIP does not cover the translation process in detail, but simply outlines +the components and rules for usage in the new format. All features and +functionalities of the previous format are supported in the new format, so +existing Snap interfaces can be easily translated into the new format. + +Most components in the new format have a one-to-one correspondence with the +components in the previous format. The `Text` component in the new format +replaces the Markdown syntax in the previous format, and the `Bold`, `Italic`, +and `Link` components can be used to achieve similar effects. The `Box` +component in the new format replaces the `panel` component in the previous +format. + +## Copyright + +Copyright and related rights waived via [CC0](../LICENSE). diff --git a/assets/sip-23/interface-structure.png b/assets/sip-23/interface-structure.png new file mode 100644 index 0000000..d9ae474 Binary files /dev/null and b/assets/sip-23/interface-structure.png differ