From 0809906c5ccb53e08af866816537a0130d945525 Mon Sep 17 00:00:00 2001 From: kpodp0ra Date: Sat, 24 Jan 2026 09:56:34 +0100 Subject: [PATCH 1/4] feat: basic modal support --- components-sdk/src/Capsule.module.css | 12 ++ components-sdk/src/Capsule.tsx | 164 ++++++++++++----- components-sdk/src/CapsuleButton.tsx | 33 +++- .../src/components/Button.module.css | 7 +- components-sdk/src/components/Button.tsx | 98 ++++++++-- .../src/components/FileUpload.module.css | 46 +++++ components-sdk/src/components/FileUpload.tsx | 97 ++++++++++ .../src/components/Label.module.css | 71 ++++++++ components-sdk/src/components/Label.tsx | 122 +++++++++++++ .../src/components/StringSelect.tsx | 119 ++++-------- .../src/components/TextDisplay.module.css | 2 +- .../src/components/TextInput.module.css | 45 +++++ components-sdk/src/components/TextInput.tsx | 170 ++++++++++++++++++ components-sdk/src/components/Thumbnail.tsx | 53 ++---- components-sdk/src/dnd/DragContext.tsx | 9 +- components-sdk/src/dnd/DragEvents.tsx | 7 +- components-sdk/src/dnd/components.ts | 160 +++++++++++------ components-sdk/src/dnd/componentsModal.ts | 64 +++++++ components-sdk/src/dnd/handleDragDrop.ts | 14 +- components-sdk/src/icons/FileUpload.svg | 11 ++ components-sdk/src/icons/MaximumActive.svg | 3 + components-sdk/src/icons/MinimumActive.svg | 3 + components-sdk/src/icons/Required.svg | 1 + components-sdk/src/icons/RequiredActive.svg | 1 + components-sdk/src/locales/en.json | 33 +++- components-sdk/src/locales/pl.json | 33 +++- components-sdk/src/utils/componentTypes.ts | 19 +- .../src/utils/componentTypesModal.ts | 168 +++++++++++++++++ components-sdk/src/utils/useStateOpen.ts | 3 + website/libs.config.ts | 6 + website/src/App.tsx | 124 ++----------- website/src/Codegen.tsx | 9 +- website/src/Webhook.tsx | 119 ++++++++++++ website/src/defaultJsonModal.ts | 80 +++++++++ website/src/locales/en.json | 3 +- website/src/locales/pl.json | 3 +- website/src/state.ts | 8 +- 37 files changed, 1549 insertions(+), 371 deletions(-) create mode 100644 components-sdk/src/components/FileUpload.module.css create mode 100644 components-sdk/src/components/FileUpload.tsx create mode 100644 components-sdk/src/components/Label.module.css create mode 100644 components-sdk/src/components/Label.tsx create mode 100644 components-sdk/src/components/TextInput.module.css create mode 100644 components-sdk/src/components/TextInput.tsx create mode 100644 components-sdk/src/dnd/componentsModal.ts create mode 100644 components-sdk/src/icons/FileUpload.svg create mode 100644 components-sdk/src/icons/MaximumActive.svg create mode 100644 components-sdk/src/icons/MinimumActive.svg create mode 100644 components-sdk/src/icons/Required.svg create mode 100644 components-sdk/src/icons/RequiredActive.svg create mode 100644 components-sdk/src/utils/componentTypesModal.ts create mode 100644 website/src/Webhook.tsx create mode 100644 website/src/defaultJsonModal.ts diff --git a/components-sdk/src/Capsule.module.css b/components-sdk/src/Capsule.module.css index ce1692d..b1aedb6 100644 --- a/components-sdk/src/Capsule.module.css +++ b/components-sdk/src/Capsule.module.css @@ -10,12 +10,24 @@ color: #dcddde; background: #36393f; font-family: "gg sans",Whitney,"Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif; + overflow-wrap: anywhere; font-size: 1.6rem; line-height: 2.2rem; letter-spacing: normal; font-weight: 400; position: relative; + + &.modal { + background: #242429; + .header { + font-size: 2.4rem; + color: white; + font-weight: bold; + margin-top: 1rem; + margin-bottom: 24px; + } + } } .component { diff --git a/components-sdk/src/Capsule.tsx b/components-sdk/src/Capsule.tsx index c10eff6..e5945e7 100644 --- a/components-sdk/src/Capsule.tsx +++ b/components-sdk/src/Capsule.tsx @@ -11,7 +11,7 @@ import { StringSelect } from './components/StringSelect'; import { File } from './components/File'; import { CapsuleInner } from './CapsuleInner'; import { generateRandomAnimal, randomSentence, uuidv4 } from './utils/randomGen'; -import { addKeyType, appendKeyType, deleteKeyType, stateKeyType, StateManager } from './polyfills/StateManager'; +import { stateKeyType, StateManager } from './polyfills/StateManager'; import { ActionRowComponent, ButtonComponent, @@ -23,22 +23,33 @@ import { MediaGalleryComponent, MediaGalleryItem, PassProps, + RenderMode, SeparatorComponent, SeparatorSpacingSize, StringSelectComponent, TextDisplayComponent, ThumbnailComponent, } from './utils/componentTypes'; +import { + FileUploadComponent, + LabelComponent, + ModalStringSelectComponent, + TextInputComponent, + TextInputStyle, +} from './utils/componentTypesModal'; import { DragContextProvider } from './dnd/DragContext'; import { DroppableID } from './dnd/components'; import { KeyToDeleteType } from './dnd/types'; import { BoundariesProps } from './dnd/boundaries'; import { RegenerateContextProvider } from './utils/useRegenerate'; -import { useMemo } from 'react'; +import { Label } from './components/Label'; +import { useTranslation } from 'react-i18next'; +import { TextInput } from './components/TextInput'; +import { FileUpload } from './components/FileUpload'; const _Button = { - type: 2, + type: ComponentType.BUTTON, style: ButtonStyle.GREY, label: '', emoji: null, @@ -54,7 +65,7 @@ const _Image = { } as MediaGalleryItem const _StringSelect = () => ({ - type: 3, + type: ComponentType.STRING_SELECT, custom_id: uuidv4(), options: [ { @@ -71,70 +82,119 @@ const _StringSelect = () => ({ disabled: false, } as StringSelectComponent) +const _TextInput = (style: TextInputStyle) => ({ + type: ComponentType.TEXT_INPUT, + custom_id: uuidv4(), + style, + min_length: null, + max_length: null, + required: true, + value: null, + placeholder: null, +}); + +// We want this app to create components with all properties (except 'id') defined +type Req = Required>; + export const default_settings = { Button: () => ({ - type: 1, + type: ComponentType.ACTION_ROW, components: [{ ..._Button, custom_id: uuidv4(), - label: generateRandomAnimal() + label: generateRandomAnimal(), }] }), LinkButton: () => ({ - type: 1, + type: ComponentType.ACTION_ROW, components: [{ ..._Button, style: ButtonStyle.URL, url: 'https://google.com', - label: generateRandomAnimal() + label: generateRandomAnimal(), }] }), StringSelect: () => ({ - type: 1, - components: [_StringSelect()] + type: ComponentType.ACTION_ROW, + components: [_StringSelect()], }), TextDisplay: () => ({ - type: 10, + type: ComponentType.TEXT_DISPLAY, content: randomSentence(), }), Thumbnail: { - type: 11, - ..._Image + type: ComponentType.THUMBNAIL, + ..._Image, }, MediaGallery: { - type: 12, - items: [ - _Image - ] + type: ComponentType.MEDIA_GALLERY, + items: [_Image], }, File: { - type: 13, + type: ComponentType.FILE, file: { url: '' }, spoiler: false, }, Separator: { - type: 14, + type: ComponentType.SEPARATOR, divider: true, spacing: SeparatorSpacingSize.SMALL }, Container: { - type: 17, + type: ComponentType.CONTAINER, accent_color: null, spoiler: false, components: [], }, + ModalShortInput: () => ({ + type: ComponentType.LABEL, + label: generateRandomAnimal(), + description: null, + component: _TextInput(TextInputStyle.SHORT), + }), + ModalParagraphInput: () => ({ + type: ComponentType.LABEL, + label: generateRandomAnimal(), + description: null, + component: _TextInput(TextInputStyle.PARAGRAPH), + }), + ModalStringSelect: () => { + const {disabled, ...other} = _StringSelect(); + return { + type: ComponentType.LABEL, + label: generateRandomAnimal(), + description: null, + component: {...other, required: true}, + }; + }, + ModalFileUpload: () => ({ + type: ComponentType.LABEL, + label: generateRandomAnimal(), + description: null, + component: { + type: ComponentType.FILE_UPLOAD, + custom_id: uuidv4(), + min_values: 1, + max_values: 1, + required: true, + }, + }), } as { - Button: () => ActionRowComponent, - LinkButton: () => ActionRowComponent, - StringSelect: () => ActionRowComponent, - TextDisplay: () => TextDisplayComponent, - Thumbnail: ThumbnailComponent, - MediaGallery: MediaGalleryComponent, - Separator: SeparatorComponent, - Container:ContainerComponent, - File: FileComponent + Button: () => Req>>; + LinkButton: () => Req>>; + StringSelect: () => Req>>; + TextDisplay: () => Req; + Thumbnail: Req; + MediaGallery: Req; + Separator: Req; + Container: Req; + File: Req; + ModalShortInput: () => Req>>; + ModalParagraphInput: () => Req>>; + ModalStringSelect: () => Req>>; + ModalFileUpload: () => Req>>; } export type ComponentsProps = { @@ -145,23 +205,27 @@ export type ComponentsProps = { removeKeyParent?: stateKeyType, dragKeyToDeleteOverwrite?: Omit, // Available only for Section accessory droppableId?: DroppableID, // Available only for Section accessory + fromLabel?: boolean // Available only for Label component errors?: Record | null, actionCallback?: (custom_id: string | null) => void, } -export const COMPONENTS = { - 1: ActionRow, - 2: Button, - 3: StringSelect, - 9: Section, - 10: TextDisplay, - 11: Thumbnail, - 12: MediaGallery, - 14: Separator, - 17: Container, - 13: File, -} as unknown as { - [K: number]: (props: ComponentsProps) => JSX.Element +export const COMPONENTS: { + [K in ComponentType]: (props: Omit & { state: any }) => JSX.Element | null; +} = { + [ComponentType.ACTION_ROW]: ActionRow, + [ComponentType.BUTTON]: Button, + [ComponentType.STRING_SELECT]: StringSelect, + [ComponentType.TEXT_INPUT]: TextInput, + [ComponentType.SECTION]: Section, + [ComponentType.TEXT_DISPLAY]: TextDisplay, + [ComponentType.THUMBNAIL]: Thumbnail, + [ComponentType.MEDIA_GALLERY]: MediaGallery, + [ComponentType.SEPARATOR]: Separator, + [ComponentType.CONTAINER]: Container, + [ComponentType.FILE]: File, + [ComponentType.LABEL]: Label, + [ComponentType.FILE_UPLOAD]: FileUpload, } export const SECTIONABLE = [ @@ -175,19 +239,27 @@ export function Capsule(props : { className?: string | null, passProps: PassProps, errors: Record | null, + modalTitle?: string } & BoundariesProps ) { + const isModal = props.passProps.renderMode === RenderMode.MODAL; + const cls = props.className ? ' ' + props.className : ''; + const modCls = isModal ? ' ' + Styles.modal : ''; - return
- {stateMng => + return
+ {!!(isModal && props.modalTitle) &&
+ {props.modalTitle} +
} + {stateMng => }
diff --git a/components-sdk/src/CapsuleButton.tsx b/components-sdk/src/CapsuleButton.tsx index 29ef674..4e53a6d 100644 --- a/components-sdk/src/CapsuleButton.tsx +++ b/components-sdk/src/CapsuleButton.tsx @@ -14,7 +14,7 @@ import { Component } from './utils/componentTypes'; import { useStateOpen } from './utils/useStateOpen'; import { useTranslation } from 'react-i18next'; -export type capsuleButtonCtx = 'main' | 'container' | 'inline' | 'button-row' | 'frame'; +export type capsuleButtonCtx = 'main' | 'container' | 'inline' | 'button-row' | 'frame' | 'modal'; type props = { context: capsuleButtonCtx, @@ -39,6 +39,7 @@ export function CapsuleButton({context, callback, className, style, interactiveD {context === "inline" && t('button.add-inline')} {context === "container" && t('button.add-content')} {context === "button-row" && t('button.add-button')} + {context === "modal" && t('button.add-input')} { open &&
{['main', 'inline', 'container'].includes(context) &&
{ @@ -95,6 +96,36 @@ export function CapsuleButton({context, callback, className, style, interactiveD
{t('components.thumbnail')}
} + {['modal'].includes(context) &&
{ + callback(default_settings.ModalShortInput()) + }}> +
+
{t('components.modal.short-input')}
+
} + {['modal'].includes(context) &&
{ + callback(default_settings.ModalParagraphInput()) + }}> +
+
{t('components.modal.paragraph-input')}
+
} + {['modal'].includes(context) &&
{ + callback(default_settings.TextDisplay()) + }}> +
+
{t('components.modal.content')}
+
} + {['modal'].includes(context) &&
{ + callback(default_settings.ModalStringSelect()) + }}> +
+
{t('components.modal.string-select')}
+
} + {['modal'].includes(context) &&
{ + callback(default_settings.ModalFileUpload()) + }}> +
+
{t('components.modal.file-upload')}
+
}
}
) diff --git a/components-sdk/src/components/Button.module.css b/components-sdk/src/components/Button.module.css index ac67393..6d34366 100644 --- a/components-sdk/src/components/Button.module.css +++ b/components-sdk/src/components/Button.module.css @@ -37,11 +37,16 @@ border: 1px solid rgba(255,255,255, 0.1); } -.input[type="text"] { +.input[type="text"], .input[type="number"] { padding: 0.8rem 1.6rem; width: 200px; } +textarea.textarea { + padding: 0.8rem 1.6rem; + width: 500px; +} + .link_btn { line-height: 100%; .text { diff --git a/components-sdk/src/components/Button.tsx b/components-sdk/src/components/Button.tsx index e081a8d..a338062 100644 --- a/components-sdk/src/components/Button.tsx +++ b/components-sdk/src/components/Button.tsx @@ -1,16 +1,17 @@ import Styles from './Button.module.css'; -import {text_display_input} from './TextDisplay.module.css'; +import { text_display_input } from './TextDisplay.module.css'; import CapsuleStyles from '../Capsule.module.css'; import { Dispatch, - Fragment, MouseEventHandler, + Fragment, + MouseEventHandler, RefObject, SetStateAction, useEffect, useImperativeHandle, useLayoutEffect, useRef, - useState + useState, } from 'react'; import ColorBlue from '../icons/ColorBlue.svg'; import ColorGrey from '../icons/ColorGrey.svg'; @@ -115,8 +116,8 @@ export function Button( {!!open &&
{open === 1 && } {open === 2 && } - {open === 3 && } - {open === 4 && } + {open === 3 && } + {open === 4 && }
} ) @@ -184,21 +185,53 @@ function MenuFirst({state, stateKey, stateManager, setOpen, removeKeyParent, act } export function MenuOption({ src, text, onClick, className }: { - src: string; + src: string | null; text: string; onClick?: MouseEventHandler; className?: string; }) { return (
-
+ {src !== null &&
-
+
}
{text}
); } +export function MenuArea({state, stateKey, stateManager, setOpen, closeLockRef, max} : { + state: (string | number | null), + stateKey: ComponentsProps['stateKey'], + stateManager: ComponentsProps['stateManager'], + setOpen: Dispatch>, + closeLockRef: RefObject, + max: number, +}) { + const ref = useRef(null); + useImperativeHandle(closeLockRef, () => "SHIFT"); + + useEffect(() => { + if (ref.current) ref.current.focus(); + }, [ref.current]); + return
+