├── src
├── utils
│ ├── reflow.ts
│ ├── hasNonOverlayScrollbarY.ts
│ └── onTransitionend.ts
├── entry-client.tsx
├── entry-server.tsx
├── editor
│ ├── plugins
│ │ ├── cursor-style.ts
│ │ ├── line-numbers-style.ts
│ │ ├── selection-style.ts
│ │ ├── autocomplete-style.ts
│ │ └── highlight-style.ts
│ ├── editorBaseTheme.ts
│ ├── theme
│ │ ├── light.ts
│ │ └── dark.ts
│ └── defineEditorTheme.ts
├── components
│ ├── Icons
│ │ ├── ChevronDownIcon.tsx
│ │ ├── XIcon.tsx
│ │ ├── SplitPanelColumns.tsx
│ │ ├── ArrowLeftIcon.tsx
│ │ ├── CopyIcon.tsx
│ │ ├── TrashIcon.tsx
│ │ ├── HalfSun.tsx
│ │ ├── GithubIcon.tsx
│ │ ├── SplitPanelRows.tsx
│ │ └── SettingsIcon.tsx
│ ├── ConfigPanel
│ │ ├── Toggle.tsx
│ │ ├── Select.tsx
│ │ ├── Input.tsx
│ │ └── ConfigPanel.tsx
│ ├── Editors
│ │ ├── SplitEditor.tsx
│ │ ├── HTMLEditor.tsx
│ │ └── JSXEditor.tsx
│ ├── CopyJSXButton.tsx
│ ├── Header
│ │ ├── Header.tsx
│ │ └── ThemeBtn.tsx
│ ├── SettingsPanel
│ │ ├── SettingsPanel.tsx
│ │ └── TogglePanelButton.tsx
│ ├── SolidLogo.tsx
│ ├── ActionsPanel
│ │ ├── TogglePanelButton.tsx
│ │ └── ActionsPanel.tsx
│ └── MobileScrollDVH.tsx
├── routes
│ └── index.tsx
├── store.ts
├── root.css
├── root.tsx
└── lib
│ └── html-to-jsx.ts
├── public
├── og.png
├── fonts
│ ├── .DS_Store
│ └── Gordita
│ │ ├── Gordita-Black.otf
│ │ ├── Gordita-Black.woff
│ │ ├── Gordita-Bold.eot
│ │ ├── Gordita-Bold.otf
│ │ ├── Gordita-Bold.ttf
│ │ ├── Gordita-Bold.woff
│ │ ├── Gordita-Bold.woff2
│ │ ├── Gordita-Light.otf
│ │ ├── Gordita-Light.woff
│ │ ├── Gordita-Medium.eot
│ │ ├── Gordita-Medium.otf
│ │ ├── Gordita-Medium.ttf
│ │ ├── Gordita-Thin.eot
│ │ ├── Gordita-Thin.otf
│ │ ├── Gordita-Thin.ttf
│ │ ├── Gordita-Thin.woff
│ │ ├── Gordita-Thin.woff2
│ │ ├── Gordita-Ultra.otf
│ │ ├── Gordita-Ultra.woff
│ │ ├── Gordita-Black.woff2
│ │ ├── Gordita-Light.woff2
│ │ ├── Gordita-Medium.woff
│ │ ├── Gordita-Medium.woff2
│ │ ├── Gordita-Regular.eot
│ │ ├── Gordita-Regular.otf
│ │ ├── Gordita-Regular.ttf
│ │ ├── Gordita-Regular.woff
│ │ ├── Gordita-Ultra.woff2
│ │ ├── Gordita-Bold-italic.otf
│ │ ├── Gordita-Regular.woff2
│ │ ├── Gordita-Thin-Italic.otf
│ │ ├── Gordita-Black-Italic.otf
│ │ ├── Gordita-Black-Italic.woff
│ │ ├── Gordita-Bold-Italic.woff
│ │ ├── Gordita-Bold-italic.woff2
│ │ ├── Gordita-Light-Italic.otf
│ │ ├── Gordita-Light-Italic.woff
│ │ ├── Gordita-Medium-Italic.otf
│ │ ├── Gordita-Thin-Italic.woff
│ │ ├── Gordita-Thin-Italic.woff2
│ │ ├── Gordita-Ultra-Italic.otf
│ │ ├── Gordita-Ultra-Italic.woff
│ │ ├── Gordita-Black-Italic.woff2
│ │ ├── Gordita-Light-Italic.woff2
│ │ ├── Gordita-Medium-Italic.woff
│ │ ├── Gordita-Medium-Italic.woff2
│ │ ├── Gordita-Regular-Italic.otf
│ │ ├── Gordita-Regular-Italic.woff
│ │ ├── Gordita-Regular-Italic.woff2
│ │ └── Gordita-Ultra-Italic.woff2
└── favicons
│ ├── favicon.ico
│ ├── favicon-16x16.png
│ ├── favicon-32x32.png
│ ├── icon-192x192.png
│ ├── icon-256x256.png
│ ├── icon-384x384.png
│ ├── icon-512x512.png
│ ├── mstile-150x150.png
│ ├── apple-touch-icon.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ └── browserconfig.xml
├── .prettierrc
├── tsconfig.json
├── vite.config.ts
├── .github
└── workflows
│ └── main.yaml
├── LICENSE
├── package.json
├── unocss.config.ts
├── README.md
└── .gitignore
/src/utils/reflow.ts:
--------------------------------------------------------------------------------
1 | export const reflow = () => document.body.clientWidth;
2 |
--------------------------------------------------------------------------------
/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/og.png
--------------------------------------------------------------------------------
/public/fonts/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/.DS_Store
--------------------------------------------------------------------------------
/public/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/favicons/favicon.ico
--------------------------------------------------------------------------------
/public/favicons/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/favicons/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicons/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/favicons/favicon-32x32.png
--------------------------------------------------------------------------------
/public/favicons/icon-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/favicons/icon-192x192.png
--------------------------------------------------------------------------------
/public/favicons/icon-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/favicons/icon-256x256.png
--------------------------------------------------------------------------------
/public/favicons/icon-384x384.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/favicons/icon-384x384.png
--------------------------------------------------------------------------------
/public/favicons/icon-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/favicons/icon-512x512.png
--------------------------------------------------------------------------------
/public/favicons/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/favicons/mstile-150x150.png
--------------------------------------------------------------------------------
/src/entry-client.tsx:
--------------------------------------------------------------------------------
1 | import { mount, StartClient } from "solid-start/entry-client";
2 |
3 | mount(() => , document);
4 |
--------------------------------------------------------------------------------
/public/favicons/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/favicons/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Black.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Black.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Black.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Bold.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Bold.eot
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Bold.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Bold.ttf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Bold.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Bold.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Bold.woff2
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Light.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Light.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Light.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Light.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Medium.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Medium.eot
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Medium.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Medium.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Medium.ttf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Thin.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Thin.eot
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Thin.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Thin.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Thin.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Thin.ttf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Thin.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Thin.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Thin.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Thin.woff2
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Ultra.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Ultra.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Ultra.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Ultra.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Black.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Black.woff2
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Light.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Light.woff2
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Medium.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Medium.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Medium.woff2
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Regular.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Regular.eot
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Regular.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Regular.ttf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Regular.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Ultra.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Ultra.woff2
--------------------------------------------------------------------------------
/public/favicons/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/favicons/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/favicons/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/favicons/android-chrome-512x512.png
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Bold-italic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Bold-italic.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Regular.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Regular.woff2
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Thin-Italic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Thin-Italic.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Black-Italic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Black-Italic.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Black-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Black-Italic.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Bold-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Bold-Italic.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Bold-italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Bold-italic.woff2
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Light-Italic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Light-Italic.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Light-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Light-Italic.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Medium-Italic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Medium-Italic.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Thin-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Thin-Italic.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Thin-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Thin-Italic.woff2
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Ultra-Italic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Ultra-Italic.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Ultra-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Ultra-Italic.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Black-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Black-Italic.woff2
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Light-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Light-Italic.woff2
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Medium-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Medium-Italic.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Medium-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Medium-Italic.woff2
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Regular-Italic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Regular-Italic.otf
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Regular-Italic.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Regular-Italic.woff
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Regular-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Regular-Italic.woff2
--------------------------------------------------------------------------------
/public/fonts/Gordita/Gordita-Ultra-Italic.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/solidjs-community/html-to-solidjsx/HEAD/public/fonts/Gordita/Gordita-Ultra-Italic.woff2
--------------------------------------------------------------------------------
/src/entry-server.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createHandler,
3 | renderAsync,
4 | StartServer,
5 | } from "solid-start/entry-server";
6 |
7 | export default createHandler(
8 | renderAsync((event) => )
9 | );
10 |
--------------------------------------------------------------------------------
/src/utils/hasNonOverlayScrollbarY.ts:
--------------------------------------------------------------------------------
1 | export const hasNonOverlayScrollbarY = (el: HTMLElement) => {
2 | const { offsetWidth, clientWidth } = el;
3 | if (clientWidth === 0) return false;
4 |
5 | return clientWidth !== offsetWidth;
6 | };
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "tabWidth": 2,
4 | "printWidth": 100,
5 | "semi": true,
6 | "singleQuote": false,
7 | "useTabs": false,
8 | "arrowParens": "always",
9 | "bracketSpacing": true,
10 | "endOfLine": "lf"
11 | }
12 |
--------------------------------------------------------------------------------
/src/utils/onTransitionend.ts:
--------------------------------------------------------------------------------
1 | export const onTransitionend = (el: HTMLElement, cb: () => void) => {
2 | el.addEventListener(
3 | "transitionend",
4 | (e) => {
5 | if (e.currentTarget !== e.target) return;
6 | cb();
7 | },
8 | { once: true }
9 | );
10 | };
11 |
--------------------------------------------------------------------------------
/public/favicons/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/editor/plugins/cursor-style.ts:
--------------------------------------------------------------------------------
1 | import { EditorView } from "@codemirror/view";
2 |
3 | export interface StyledCursorOptions {
4 | color: string;
5 | }
6 |
7 | export function styledCursor(options: StyledCursorOptions) {
8 | return EditorView.theme({
9 | ".cm-cursor, .cm-dropCursor": { borderLeftColor: options.color },
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/src/editor/plugins/line-numbers-style.ts:
--------------------------------------------------------------------------------
1 | import { EditorView } from "@codemirror/view";
2 |
3 | export interface StyledLineNumbersOptions {
4 | color: string;
5 | }
6 |
7 | export function styledLineNumbers(options: StyledLineNumbersOptions) {
8 | return EditorView.theme({
9 | ".cm-lineNumbers .cm-gutterElement": { color: options.color },
10 | });
11 | }
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": true,
4 | "esModuleInterop": true,
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleResolution": "node",
8 | "jsxImportSource": "solid-js",
9 | "jsx": "preserve",
10 | "strict": true,
11 | "types": ["solid-start/env"],
12 | "baseUrl": "./",
13 | "paths": {
14 | "~/*": ["./src/*"]
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import solid from "solid-start/vite";
2 | import { defineConfig } from "vite";
3 | import UnoCss from "unocss/vite";
4 |
5 | export default defineConfig({
6 | base: "/html-to-solidjsx/",
7 | optimizeDeps: {
8 | // Add both @codemirror/state and @codemirror/view to included deps to optimize
9 | include: ["@codemirror/state", "@codemirror/view"],
10 | },
11 | plugins: [
12 | solid({
13 | adapter: "solid-start-static",
14 | }),
15 | UnoCss(),
16 | ],
17 | });
18 |
--------------------------------------------------------------------------------
/src/components/Icons/ChevronDownIcon.tsx:
--------------------------------------------------------------------------------
1 | const ChevronDownIcon = (props: any) => {
2 | return (
3 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default ChevronDownIcon;
21 |
--------------------------------------------------------------------------------
/src/components/Icons/XIcon.tsx:
--------------------------------------------------------------------------------
1 | const XIcon = (props: any) => {
2 | return (
3 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default XIcon;
22 |
--------------------------------------------------------------------------------
/src/components/Icons/SplitPanelColumns.tsx:
--------------------------------------------------------------------------------
1 | const SplitPanelColumns = () => {
2 | return (
3 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default SplitPanelColumns;
20 |
--------------------------------------------------------------------------------
/src/components/Icons/ArrowLeftIcon.tsx:
--------------------------------------------------------------------------------
1 | const ArrowLeftIcon = (props: any) => {
2 | return (
3 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default ArrowLeftIcon;
22 |
--------------------------------------------------------------------------------
/src/editor/plugins/selection-style.ts:
--------------------------------------------------------------------------------
1 | import { EditorView } from "@codemirror/view";
2 |
3 | export interface StyledSelectionOptions {
4 | backgroundColor: string;
5 | color: string;
6 | activeLine?: string;
7 | }
8 |
9 | export function styledSelection(options: StyledSelectionOptions) {
10 | return EditorView.theme({
11 | "&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection":
12 | {
13 | background: options.backgroundColor,
14 | },
15 | ".cm-activeLine": {
16 | backgroundColor: options.activeLine ?? "unset",
17 | },
18 | });
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/Icons/CopyIcon.tsx:
--------------------------------------------------------------------------------
1 | const CopyIcon = (props: any) => {
2 | return (
3 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default CopyIcon;
22 |
--------------------------------------------------------------------------------
/src/components/Icons/TrashIcon.tsx:
--------------------------------------------------------------------------------
1 | const TrashIcon = (props: any) => {
2 | return (
3 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default TrashIcon;
22 |
--------------------------------------------------------------------------------
/src/components/Icons/HalfSun.tsx:
--------------------------------------------------------------------------------
1 | const HalfSun = () => {
2 | return (
3 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | );
23 | };
24 |
25 | export default HalfSun;
26 |
--------------------------------------------------------------------------------
/src/editor/editorBaseTheme.ts:
--------------------------------------------------------------------------------
1 | import { EditorView } from "@codemirror/view";
2 |
3 | export const editorBaseTheme = (props: { backgroundColor?: string } = {}) =>
4 | EditorView.theme({
5 | "&": {
6 | textAlign: "left",
7 | // fontSize: "13px",
8 | background: "transparent",
9 | },
10 | ".cm-activeLineGutter": {
11 | backgroundColor: "transparent",
12 | },
13 | ".cm-gutters": {
14 | backgroundColor: props.backgroundColor ?? "",
15 | border: "none",
16 | },
17 | ".cm-line": {
18 | padding: "0 2px 6px 16px",
19 | },
20 | ".cm-content *": {
21 | fontFamily: `monospace`,
22 | fontWeight: 400,
23 | fontVariantLigatures: "normal",
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/.github/workflows/main.yaml:
--------------------------------------------------------------------------------
1 | name: Deploy site
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | workflow_dispatch:
7 |
8 | jobs:
9 | build:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v3
14 |
15 | - uses: pnpm/action-setup@v2.2.4
16 |
17 | - name: Setup Node.js environment
18 |
19 | uses: actions/setup-node@v3
20 | with:
21 | node-version: 18
22 | cache: pnpm
23 |
24 | - name: Install dependencies
25 | run: pnpm install
26 |
27 | - name: Build
28 | run: pnpm build
29 |
30 | - name: deploy pages
31 | uses: JamesIves/github-pages-deploy-action@v4.4.1
32 | with:
33 | branch: gh-pages
34 | folder: dist/public
35 |
--------------------------------------------------------------------------------
/src/components/Icons/GithubIcon.tsx:
--------------------------------------------------------------------------------
1 | const GithubIcon = () => {
2 | return (
3 |
15 |
16 |
17 | );
18 | };
19 |
20 | export default GithubIcon;
21 |
--------------------------------------------------------------------------------
/src/components/Icons/SplitPanelRows.tsx:
--------------------------------------------------------------------------------
1 | const SplitPanelRows = () => {
2 | return (
3 |
10 |
11 |
12 |
19 |
26 |
27 |
28 | );
29 | };
30 |
31 | export default SplitPanelRows;
32 |
--------------------------------------------------------------------------------
/src/components/ConfigPanel/Toggle.tsx:
--------------------------------------------------------------------------------
1 | import { Component } from "solid-js";
2 |
3 | const Toggle: Component<{
4 | name: string;
5 | value: boolean;
6 | disabled?: boolean;
7 | onChange: (props: { checked: boolean }) => void;
8 | }> = (props) => {
9 | return (
10 |
14 | {props.name}
15 | props.onChange({ checked: e.currentTarget.checked })}
23 | />
24 |
25 | );
26 | };
27 |
28 | export default Toggle;
29 |
--------------------------------------------------------------------------------
/src/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import ActionsPanel from "~/components/ActionsPanel/ActionsPanel";
2 | import SplitEditor from "~/components/Editors/SplitEditor";
3 | import Header from "~/components/Header/Header";
4 | import MobileScrollDVH from "~/components/MobileScrollDVH";
5 | import SettingsPanel from "~/components/SettingsPanel/SettingsPanel";
6 |
7 | export default function Home() {
8 | return (
9 | <>
10 |
11 |
12 |
16 |
23 |
24 |
25 |
26 | >
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 SolidJS Community
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/editor/plugins/autocomplete-style.ts:
--------------------------------------------------------------------------------
1 | import { EditorView } from "@codemirror/view";
2 |
3 | export interface StyledAutoCompleteOptions {
4 | background: string;
5 | border?: string;
6 | selectedBackground: string;
7 | selectedColor?: string;
8 | }
9 |
10 | export function styledAutocomplete(options: StyledAutoCompleteOptions) {
11 | return EditorView.theme({
12 | ".cm-tooltip": {
13 | border: options.border ? `1px solid ${options.border}` : "none",
14 | backgroundColor: options.background,
15 | borderRadius: "6px",
16 | },
17 |
18 | ".cm-tooltip .cm-tooltip-arrow:before": {
19 | borderTopColor: "transparent",
20 | borderBottomColor: "transparent",
21 | },
22 | ".cm-tooltip .cm-tooltip-arrow:after": {
23 | borderTopColor: options.background,
24 | borderBottomColor: options.background,
25 | },
26 |
27 | ".cm-tooltip-autocomplete": {
28 | "& > ul > li": {
29 | padding: "6px !important",
30 | },
31 | "& > ul > li[aria-selected]": {
32 | backgroundColor: options.selectedBackground,
33 | color: options.selectedColor ?? "inherit",
34 | },
35 | },
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/src/editor/theme/light.ts:
--------------------------------------------------------------------------------
1 | import { defineEditorTheme } from "../defineEditorTheme";
2 |
3 | export const githubLight = [
4 | defineEditorTheme({
5 | darkMode: false,
6 | cursor: {
7 | color: "#24292f",
8 | },
9 | lineNumbers: {
10 | color: "#8493a1",
11 | },
12 | selection: {
13 | activeLine: "rgba(234,238,242,0.5)",
14 | backgroundColor: "rgba(84,174,255,0.4)",
15 | color: "#24292f",
16 | },
17 | autocomplete: {
18 | background: "#32383f",
19 | border: "#424a53",
20 | selectedBackground: "#424a53",
21 | selectedColor: "#f6f8fa",
22 | },
23 | highlight: {
24 | base: "#24292f",
25 | background: "#ffffff",
26 | tag: "#116329",
27 | delimiters: "#6e7781",
28 | numbers: "#0a3069",
29 | punctuation: "#bf8700",
30 | className: "#953800",
31 | brackets: "#bf8700",
32 | keywords: "#cf222e",
33 | strings: "#0a3069",
34 | propertyName: "#0a3069",
35 | variableName: "#953800",
36 | regexp: "#116329",
37 | comments: "#6e7781",
38 | attrName: "#0550ae",
39 | function: "#8250df",
40 | typeName: "#0550ae",
41 | },
42 | }),
43 | ];
44 |
--------------------------------------------------------------------------------
/src/editor/defineEditorTheme.ts:
--------------------------------------------------------------------------------
1 | import { EditorView } from "@codemirror/view";
2 | import { styledAutocomplete } from "./plugins/autocomplete-style";
3 | import { styledCursor } from "./plugins/cursor-style";
4 | import { styledHighlight } from "./plugins/highlight-style";
5 | import { styledLineNumbers } from "./plugins/line-numbers-style";
6 | import { styledSelection } from "./plugins/selection-style";
7 |
8 | export const defineEditorTheme = (theme: any) => {
9 | const { darkMode, highlight, selection, autocomplete, cursor, lineNumbers } =
10 | theme;
11 |
12 | const base = EditorView.theme({
13 | "&": {
14 | color: highlight.base,
15 | },
16 | });
17 |
18 | return [
19 | base,
20 | styledCursor({
21 | color: cursor?.color ?? (darkMode ? "#FFF" : "#000"),
22 | }),
23 | lineNumbers?.color
24 | ? styledLineNumbers({
25 | color: lineNumbers?.color ?? (darkMode ? "#FFF" : "#000"),
26 | })
27 | : [],
28 | styledSelection({
29 | backgroundColor: selection?.backgroundColor ?? `${highlight.keywords}50`,
30 | color: selection?.color ?? "inherit",
31 | }),
32 | styledAutocomplete(autocomplete),
33 | styledHighlight(highlight),
34 | ];
35 | };
36 |
--------------------------------------------------------------------------------
/src/components/Editors/SplitEditor.tsx:
--------------------------------------------------------------------------------
1 | import { store } from "../../store";
2 | import HTMLEditor from "./HTMLEditor";
3 | import JSXEditor from "./JSXEditor";
4 |
5 | const SplitEditor = () => {
6 | return (
7 |
16 |
24 |
25 |
26 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default SplitEditor;
41 |
--------------------------------------------------------------------------------
/src/components/Icons/SettingsIcon.tsx:
--------------------------------------------------------------------------------
1 | const SettingsIcon = (props: any) => {
2 | return (
3 |
15 |
16 |
17 |
18 | );
19 | };
20 |
21 | export default SettingsIcon;
22 |
--------------------------------------------------------------------------------
/src/components/CopyJSXButton.tsx:
--------------------------------------------------------------------------------
1 | import { writeClipboard } from "@solid-primitives/clipboard";
2 | import { debounce } from "@solid-primitives/scheduled";
3 | import { createSignal } from "solid-js";
4 | import { store } from "../store";
5 | import CopyIcon from "./Icons/CopyIcon";
6 |
7 | const CopyJSXButton = () => {
8 | const [hasCopied, setHasCopied] = createSignal(false);
9 |
10 | const setHasCopiedDebounced = debounce(() => setHasCopied(false), 1500);
11 |
12 | const copyToClipboard = async () => {
13 | try {
14 | await writeClipboard(store.jsxText.trim());
15 | setHasCopied(true);
16 | setHasCopiedDebounced();
17 | } catch (err) {}
18 | };
19 |
20 | const onCopyClick = () => {
21 | copyToClipboard();
22 | };
23 | return (
24 |
28 |
29 | {hasCopied() ? "Copied!" : "Copy JSX"} {" "}
30 |
31 |
32 |
33 | );
34 | };
35 |
36 | export default CopyJSXButton;
37 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "html-to-solidjsx",
3 | "scripts": {
4 | "dev": "solid-start dev",
5 | "build": "solid-start build",
6 | "start": "solid-start start"
7 | },
8 | "type": "module",
9 | "devDependencies": {
10 | "@types/node": "^18.11.18",
11 | "esbuild": "^0.14.54",
12 | "postcss": "^8.4.21",
13 | "typescript": "^4.9.4",
14 | "vite": "^4.0.3"
15 | },
16 | "dependencies": {
17 | "@codemirror/lang-html": "^6.4.0",
18 | "@codemirror/lang-javascript": "^6.1.2",
19 | "@codemirror/language": "^6.3.1",
20 | "@codemirror/state": "^6.1.4",
21 | "@codemirror/view": "^6.7.1",
22 | "@floating-ui/dom": "^1.1.1",
23 | "@lezer/highlight": "^1.1.3",
24 | "@solid-primitives/clipboard": "^1.5.0",
25 | "@solid-primitives/media": "^2.0.6",
26 | "@solid-primitives/platform": "^0.0.103",
27 | "@solid-primitives/resize-observer": "^2.0.9",
28 | "@solid-primitives/scheduled": "^1.2.1",
29 | "@solidjs/meta": "^0.28.6",
30 | "@solidjs/router": "^0.8.3",
31 | "@unocss/preset-wind": "^0.49.2",
32 | "@unocss/reset": "^0.49.2",
33 | "@unocss/transformer-variant-group": "^0.49.4",
34 | "codemirror": "^6.0.1",
35 | "solid-codemirror": "^2.2.1",
36 | "solid-dismiss": "^1.6.5",
37 | "solid-floating-ui": "^0.2.0",
38 | "solid-icons": "^1.0.4",
39 | "solid-js": "^1.7.12",
40 | "solid-start": "^0.3.6",
41 | "solid-start-static": "^0.3.6",
42 | "undici": "^5.15.1",
43 | "unocss": "0.54.3"
44 | },
45 | "packageManager": "pnpm@8.6.0",
46 | "engines": {
47 | "node": ">=18.0.0",
48 | "pnpm": ">=8.6.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/unocss.config.ts:
--------------------------------------------------------------------------------
1 | import { presetWind, theme } from "@unocss/preset-wind";
2 | import {
3 | transformerDirectives,
4 | defineConfig,
5 | transformerVariantGroup,
6 | } from "unocss";
7 |
8 | const parseValue = (value: string) => {
9 | return value.replace(/_/g, " ");
10 | };
11 | // mask-image-[linear-gradient(135deg,#000_calc(50%_-_1px),transparent_calc(50%_-_1px))]
12 | export default defineConfig({
13 | rules: [
14 | [
15 | /^bg-image-\[(.+)\]$/,
16 | ([_, d]) => ({ "background-image": parseValue(d) }),
17 | ],
18 | [/^transition-prop-\[(.+)\]$/, ([_, d]) => ({ transition: parseValue(d) })],
19 | [
20 | /^mask-image-\[(.+)\]$/,
21 | ([_, d]) => ({
22 | "-webkit-mask-image": parseValue(d),
23 | "mask-image": parseValue(d),
24 | }),
25 | ],
26 | ],
27 | theme: {
28 | breakpoints: {
29 | ...theme.breakpoints,
30 | md: "850px",
31 | },
32 | colors: {
33 | brand: {
34 | default: "#2c4f7c",
35 | dark: "#335d92",
36 | medium: "#446b9e",
37 | light: "#4f88c6",
38 | },
39 | solid: {
40 | default: "#2c4f7c",
41 | darkbg: "#222222",
42 | darkLighterBg: "#444444",
43 | darkdefault: "#b8d7ff",
44 | darkgray: "#252525",
45 | gray: "#414042",
46 | mediumgray: "#9d9d9d",
47 | lightgray: "#f3f5f7",
48 | dark: "#07254A",
49 | medium: "#446b9e",
50 | light: "#4f88c6",
51 | accent: "#0cdc73",
52 | secondaccent: "#0dfc85",
53 | },
54 | other: "#1e1e1e",
55 | },
56 | fontFamily: {
57 | sans: "Gordita, " + theme.fontFamily!.sans,
58 | },
59 | },
60 | presets: [presetWind()],
61 | transformers: [transformerDirectives(), transformerVariantGroup()],
62 | });
63 |
--------------------------------------------------------------------------------
/src/components/Header/Header.tsx:
--------------------------------------------------------------------------------
1 | import GithubIcon from "../Icons/GithubIcon";
2 | import { panelSize } from "../SettingsPanel/SettingsPanel";
3 | import SolidLogo from "../SolidLogo";
4 | import ThemeBtn from "./ThemeBtn";
5 |
6 | const Header = () => {
7 | return (
8 |
42 | );
43 | };
44 |
45 | export default Header;
46 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # HTML to SolidJSX
6 |
7 | This is the source code of the [HTML to Solid JSX](https://solidjs-community.github.io/html-to-solidjsx) website.
8 |
9 | ## Purpose
10 |
11 | Existing HTML to JSX online transformers aren't compatible for SolidJS, it transforms to JSX that is suited for React templates.
12 |
13 | 1. Replaces standard HTML attributes such as `class` and `for` to `className` and `htmlFor`.
14 | 2. Incorrectly changes css variables names inside style attribute.
15 |
16 | Solid attempts to stay as close to HTML standards as possible, allowing copy and paste from answers on Stack Overflow or from template builders from your designers. This [site](https://solidjs-community.github.io/html-to-solidjsx) brings that goal even closer by converting void elements(` `) to self-closing(` `), while also providing customizations such as the option to camelCase attributes or having style attribute value set as a CSS object or string.
17 |
18 | ## Credits / Technologies used
19 |
20 | - [solid-start](https://start.solidjs.com/): The meta-framework
21 | - [solid-js](https://github.com/solidjs/solid/): The view library
22 | - [codemirror](https://codemirror.net/): The in-browser code editor
23 | - [solid-codemirror](https://github.com/riccardoperra/solid-codemirror): The SolidJS bindings for codemirror
24 | - [htmltojsx](https://github.com/reactjs/react-magic/blob/master/README-htmltojsx.md)(modified for Solid JSX compatiblity): The HTML to JSX converter
25 | - [UnoCSS](https://uno.antfu.me/): The CSS framework
26 | - [solid-primitives](https://github.com/solidjs-community/solid-primitives): The utilities/primitives
27 | - [vite](https://vitejs.dev/): The module bundler
28 | - [pnpm](https://pnpm.js.org/): The package manager
29 |
--------------------------------------------------------------------------------
/src/components/SettingsPanel/SettingsPanel.tsx:
--------------------------------------------------------------------------------
1 | import { onMount } from "solid-js";
2 | import { setStore, store } from "../../store";
3 | import ConfigPanel from "../ConfigPanel/ConfigPanel";
4 | import TogglePanelButton from "./TogglePanelButton";
5 |
6 | export const panelSize = "w-full md:w-35vw md:min-w-300px md:max-w-450px";
7 |
8 | const SettingsPanel = () => {
9 | onMount(() => {
10 | try {
11 | const lineWrap = JSON.parse(localStorage.lineWrap) as boolean;
12 |
13 | requestAnimationFrame(() => {
14 | setStore("lineWrap", lineWrap);
15 | });
16 | } catch (err) {}
17 | });
18 | return (
19 |
23 |
24 |
25 |
26 |
27 |
Editors
28 |
29 |
30 | Line Wrap
31 | {
37 | const checked = e.currentTarget.checked;
38 | localStorage.lineWrap = checked;
39 | setStore("lineWrap", checked);
40 | }}
41 | />
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default SettingsPanel;
55 |
--------------------------------------------------------------------------------
/src/editor/theme/dark.ts:
--------------------------------------------------------------------------------
1 | import { EditorView } from "@codemirror/view";
2 | import { defineEditorTheme } from "../defineEditorTheme";
3 |
4 | export const background = "#262335";
5 | const foreground = "hsl(204, 3%, 98%)";
6 | const identifier = "hsl(204, 98%, 80%)";
7 | const selection = "hsl(210, 52%, 31%)";
8 | const selectionMatch = "hsl(210, 12%, 22%)";
9 | const matchingBracket = "hsla(204, 3%, 80%, 0.5)";
10 | const lineNumber = "hsl(204, 3%, 50%)";
11 | const activeLine = "hsla(204, 3%, 20%, 0.4)";
12 | const keyword = "hsl(207, 65%, 59%)";
13 | const comment = "hsl(101, 33%, 47%)";
14 | const number = "hsl(99, 28%, 73%)";
15 | const string = "hsl(17, 60%, 64%)";
16 | const func = "hsl(60,42%,76%)";
17 | const regex = "hsl(0, 60%, 62%)";
18 | const tag = "hsl(168, 60%, 55%)";
19 | const purple = "#C586C0";
20 | const yellow = "#DBD700";
21 |
22 | export const vsCodeDark = [
23 | defineEditorTheme({
24 | selection: {
25 | backgroundColor: selection,
26 | activeLine: activeLine,
27 | },
28 | lineNumbers: {
29 | color: lineNumber,
30 | },
31 | cursor: {
32 | color: foreground,
33 | },
34 | highlight: {
35 | base: identifier,
36 | tag,
37 | keywords: keyword,
38 | comments: comment,
39 | numbers: number,
40 | strings: string,
41 | function: func,
42 | regexp: regex,
43 | boolean: number,
44 | propertyName: identifier,
45 | variableName: identifier,
46 | punctuation: foreground,
47 | attrValue: string,
48 | className: tag,
49 | delimiters: foreground,
50 | annotation: string,
51 | moduleKeyword: purple,
52 | brackets: yellow,
53 | paren: yellow,
54 | typeName: tag,
55 | },
56 | darkMode: true,
57 | autocomplete: {
58 | background: "#1E1E1E",
59 | selectedBackground: "#191818",
60 | },
61 | }),
62 | EditorView.theme({
63 | ".cm-matchingBracket": {
64 | backgroundColor: selectionMatch,
65 | outline: `1px solid ${matchingBracket}`,
66 | },
67 | ".cm-content": {
68 | caretColor: "#fff",
69 | },
70 | }),
71 | ];
72 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # solid-start
2 | .solid
3 |
4 | .output
5 |
6 | # Mac
7 | .DS_Store
8 |
9 | # Logs
10 | logs
11 | *.log
12 | npm-debug.log*
13 | yarn-debug.log*
14 | yarn-error.log*
15 | lerna-debug.log*
16 |
17 | # Diagnostic reports (https://nodejs.org/api/report.html)
18 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
19 |
20 | # Runtime data
21 | pids
22 | *.pid
23 | *.seed
24 | *.pid.lock
25 |
26 | # Directory for instrumented libs generated by jscoverage/JSCover
27 | lib-cov
28 |
29 | # Coverage directory used by tools like istanbul
30 | coverage
31 | *.lcov
32 |
33 | # nyc test coverage
34 | .nyc_output
35 |
36 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
37 | .grunt
38 |
39 | # Bower dependency directory (https://bower.io/)
40 | bower_components
41 |
42 | # node-waf configuration
43 | .lock-wscript
44 |
45 | # Compiled binary addons (https://nodejs.org/api/addons.html)
46 | build/Release
47 |
48 | # Dependency directories
49 | node_modules/
50 | jspm_packages/
51 |
52 | # TypeScript v1 declaration files
53 | typings/
54 |
55 | # TypeScript cache
56 | *.tsbuildinfo
57 |
58 | # Optional npm cache directory
59 | .npm
60 |
61 | # Optional eslint cache
62 | .eslintcache
63 |
64 | # Microbundle cache
65 | .rpt2_cache/
66 | .rts2_cache_cjs/
67 | .rts2_cache_es/
68 | .rts2_cache_umd/
69 |
70 | # Optional REPL history
71 | .node_repl_history
72 |
73 | # Output of 'npm pack'
74 | *.tgz
75 |
76 | # Yarn Integrity file
77 | .yarn-integrity
78 |
79 | # dotenv environment variables file
80 | .env
81 | .env.test
82 |
83 | # parcel-bundler cache (https://parceljs.org/)
84 | .cache
85 |
86 | # Next.js build output
87 | .next
88 |
89 | # Nuxt.js build / generate output
90 | .nuxt
91 | dist
92 |
93 | # Gatsby files
94 | .cache/
95 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
96 | # https://nextjs.org/blog/next-9-1#public-directory-support
97 | # public
98 |
99 | # vuepress build output
100 | .vuepress/dist
101 |
102 | # Serverless directories
103 | .serverless/
104 |
105 | # FuseBox cache
106 | .fusebox/
107 |
108 | # DynamoDB Local files
109 | .dynamodb/
110 |
111 | # TernJS port file
112 | .tern-port
113 |
--------------------------------------------------------------------------------
/src/store.ts:
--------------------------------------------------------------------------------
1 | import { createStore } from "solid-js/store";
2 | import { isServer } from "solid-js/web";
3 | import { HTMLtoJSXConfig } from "./lib/html-to-jsx";
4 | export type TJSXConfig = HTMLtoJSXConfig & { prefixSVGIds?: string };
5 | type TStore = {
6 | config: TJSXConfig;
7 | htmlText: string;
8 | jsxText: string;
9 | layout: "columns" | "rows" | "jsx" | "html";
10 | lineWrap: boolean;
11 | };
12 | export const defaultConfig: TJSXConfig = {
13 | attributeValueString: true,
14 | camelCaseAttributes: false,
15 | component: "none",
16 | wrapperNode: "none",
17 | componentName: "SolidComponent",
18 | indent: " ",
19 | preTagWrapTemplateLiterals: false,
20 | styleAttribute: "css-object",
21 | styleTagAttributeInnerHTML: false,
22 | stripStyleTag: false,
23 | stripComment: false,
24 | prefixSVGIds: "",
25 | exportComponent: false,
26 | };
27 |
28 | export type ConfigKey = keyof HTMLtoJSXConfig;
29 | export const [store, setStore] = createStore({
30 | config: { ...defaultConfig },
31 | htmlText: getHTMLText().trimStart(),
32 | jsxText: getJSXText().trimStart(),
33 | layout: "rows",
34 | lineWrap: true,
35 | });
36 |
37 | function getHTMLText() {
38 | return `
39 |
40 | SolidJSX
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
56 | `;
57 | }
58 |
59 | function getJSXText() {
60 | return `
61 | {/* Solid is solid */}
62 | SolidJSX
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
80 | `;
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/SolidLogo.tsx:
--------------------------------------------------------------------------------
1 | const SolidLogo = () => {
2 | return (
3 |
4 |
5 |
13 |
14 |
15 |
16 |
17 |
25 |
26 |
27 |
28 |
29 |
37 |
38 |
39 |
40 |
41 |
49 |
50 |
51 |
52 |
53 |
54 |
58 |
63 |
64 |
69 |
73 |
77 |
78 | );
79 | };
80 |
81 | export default SolidLogo;
82 |
--------------------------------------------------------------------------------
/src/root.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Gordita";
3 | src: url("/fonts/Gordita/Gordita-Regular.woff2") format("woff2");
4 | font-weight: 400;
5 | font-style: normal;
6 | }
7 |
8 | @font-face {
9 | font-family: "Gordita";
10 | src: url("/fonts/Gordita/Gordita-Bold.woff2") format("woff2");
11 | font-weight: 700;
12 | font-style: normal;
13 | }
14 |
15 | @font-face {
16 | font-family: "Gordita";
17 | src: url("/fonts/Gordita/Gordita-Medium.woff2") format("woff2");
18 | font-weight: 500;
19 | font-style: normal;
20 | }
21 |
22 | html,
23 | body,
24 | #root,
25 | main {
26 | height: 100%;
27 | /* TODO: does overflow on body prevent native swipdown reloading */
28 | /* overflow: clip; */
29 | }
30 |
31 | .dark {
32 | color-scheme: dark;
33 | }
34 |
35 | .dark body {
36 | @apply bg-dark;
37 | }
38 |
39 | /* CodeMirror */
40 | .cm-gutter {
41 | @apply bg-white dark:bg-dark;
42 | }
43 | .cm-gutters:after {
44 | @apply absolute content-empty block top-0 -right-12px bottom-0 w-12px bg-white dark:bg-dark;
45 | }
46 | .cm-editor {
47 | @apply h-full;
48 | }
49 | .cm-content {
50 | @apply text-13px md:text-14px;
51 | }
52 | .cm-activeLineGutter {
53 | @apply text-#000! dark:text-light!;
54 | }
55 |
56 | .select {
57 | border: 2px solid #000;
58 | border-radius: 6px;
59 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23adb5bd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
60 | background-repeat: no-repeat;
61 | background-position: right 0.75rem center;
62 | background-size: 16px 12px;
63 | -webkit-appearance: none;
64 | -moz-appearance: none;
65 | appearance: none;
66 | padding: 0.375rem 2.25rem 0.375rem 0.75rem;
67 | }
68 |
69 | .switch {
70 | /* @apply bg-#888 dark:bg-#666; */
71 | border: 1px solid #666;
72 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3.333' fill='%23666'/%3e%3c/svg%3e");
73 | font-size: 20px;
74 | background-position: left center;
75 | border-radius: 2em;
76 | width: 2em;
77 | height: 1em;
78 | margin-top: 0.1em;
79 | vertical-align: top;
80 | background-repeat: no-repeat;
81 | -webkit-appearance: none;
82 | -moz-appearance: none;
83 | appearance: none;
84 | -webkit-print-color-adjust: exact;
85 | color-adjust: exact;
86 | print-color-adjust: exact;
87 | transition: background-position 0.15s ease-in-out;
88 | }
89 |
90 | .dark .switch {
91 | border: 1px solid #888;
92 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3.333' fill='%23888'/%3e%3c/svg%3e");
93 | }
94 |
95 | .switch:checked {
96 | @apply bg-solid-light border-solid-light;
97 | background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3.333' fill='%23fff'/%3e%3c/svg%3e");
98 | background-position: right center;
99 | }
100 |
--------------------------------------------------------------------------------
/src/root.tsx:
--------------------------------------------------------------------------------
1 | // @refresh reload
2 | import { Suspense } from "solid-js";
3 | import {
4 | A,
5 | Body,
6 | ErrorBoundary,
7 | FileRoutes,
8 | Head,
9 | Html,
10 | Link,
11 | Meta,
12 | Routes,
13 | Scripts,
14 | Title,
15 | } from "solid-start";
16 | import "@unocss/reset/tailwind.css";
17 | import "virtual:uno.css";
18 | import "./root.css";
19 |
20 | const ghHandle = "solidjs-community";
21 | const ghRepoName = "html-to-solidjsx";
22 | const url = `https://${ghHandle}.github.io/${ghRepoName}`;
23 |
24 | export default function Root() {
25 | return (
26 |
27 |
28 | HTML To SolidJSX
29 |
30 |
31 |
32 |
33 |
34 |
38 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
56 |
61 |
67 |
73 |
77 |
78 |
79 |
80 |
81 |
86 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | );
106 | }
107 |
--------------------------------------------------------------------------------
/src/editor/plugins/highlight-style.ts:
--------------------------------------------------------------------------------
1 | import { HighlightStyle, syntaxHighlighting } from "@codemirror/language";
2 | import { tags as t } from "@lezer/highlight";
3 |
4 | export interface StyledHighlightOptions {
5 | base: string;
6 | delimiters: string;
7 | function?: string;
8 | comments: string;
9 | numbers: string;
10 | regexp: string;
11 | tag: string;
12 | variableName: string;
13 | variableNameSpecial?: string;
14 | keywords: string;
15 | strings: string;
16 | boolean?: string;
17 | background?: string;
18 | quote?: string;
19 | meta?: string;
20 | punctuation?: string;
21 | paren?: string;
22 | brackets?: string;
23 | moduleKeyword?: string;
24 | propertyName?: string;
25 | annotation?: string;
26 | attrName?: string;
27 | attrValue?: string;
28 | className?: string;
29 | operators?: string;
30 | self?: string;
31 | typeName?: string;
32 | atom?: string;
33 | }
34 |
35 | export function styledHighlight(h: StyledHighlightOptions) {
36 | return syntaxHighlighting(
37 | HighlightStyle.define([
38 | // Base
39 | {
40 | tag: [t.emphasis],
41 | fontStyle: "italic",
42 | },
43 | {
44 | tag: [t.strong],
45 | fontStyle: "bold",
46 | },
47 | {
48 | tag: [t.link],
49 | color: h.delimiters,
50 | },
51 | { tag: t.local(t.variableName), color: h.variableName },
52 | { tag: t.definition(t.propertyName), color: h.propertyName },
53 | { tag: t.special(t.variableName), color: h.variableNameSpecial },
54 | // Keywords
55 | {
56 | tag: [t.moduleKeyword],
57 | color: h.moduleKeyword ?? h.keywords,
58 | },
59 | {
60 | tag: [t.keyword],
61 | color: h.keywords,
62 | },
63 | {
64 | tag: [t.typeName, t.typeOperator],
65 | color: h.typeName ?? h.keywords,
66 | },
67 | {
68 | tag: [t.changed, t.annotation, t.modifier, t.namespace],
69 | color: h.keywords,
70 | },
71 | // Operators
72 | {
73 | tag: [t.operator, t.special(t.string)],
74 | color: h.operators ?? h.delimiters,
75 | },
76 | // Type
77 | {
78 | tag: [t.bool],
79 | color: h.boolean ?? h.strings,
80 | },
81 | {
82 | tag: [t.number],
83 | color: h.numbers,
84 | },
85 | {
86 | tag: [t.string, t.processingInstruction, t.inserted],
87 | color: h.strings,
88 | },
89 | // Classes
90 | {
91 | tag: [t.className, t.namespace],
92 | color: h.className ?? h.function ?? h.base,
93 | },
94 | {
95 | tag: [t.self],
96 | color: h.self ?? h.keywords,
97 | },
98 | // Function
99 | {
100 | // Function name with @codemirror/language 0.20
101 | tag: [t.color, t.constant(t.name), t.standard(t.name)],
102 | color: h.function,
103 | },
104 | {
105 | tag: [t.function(t.variableName), t.function(t.propertyName)],
106 | color: h.function,
107 | },
108 | // Meta
109 | { tag: [t.annotation], color: h.annotation ?? h.keywords },
110 | { tag: [t.punctuation], color: h.punctuation ?? h.delimiters },
111 | { tag: [t.paren], color: h.paren ?? h.punctuation },
112 | {
113 | tag: [t.squareBracket, t.bracket, t.angleBracket],
114 | color: h.brackets ?? h.delimiters,
115 | },
116 | { tag: [t.meta], color: h.meta ?? h.keywords },
117 | // Other
118 | { tag: [t.comment], color: h.comments },
119 | { tag: [t.regexp], color: h.regexp },
120 | { tag: [t.tagName], color: h.tag },
121 | {
122 | tag: [t.atom],
123 | color: h.atom,
124 | },
125 | {
126 | tag: [t.attributeValue],
127 | color: h.attrValue ?? h.strings,
128 | },
129 | {
130 | tag: [t.attributeName],
131 | color: h.attrName ?? h.base,
132 | },
133 | // Markdown
134 | { tag: [t.heading], color: h.keywords, fontWeight: "bold" },
135 | { tag: [t.quote], color: h.quote },
136 | ])
137 | );
138 | }
139 |
--------------------------------------------------------------------------------
/src/components/Editors/HTMLEditor.tsx:
--------------------------------------------------------------------------------
1 | import { Extension } from "@codemirror/state";
2 | import { html } from "@codemirror/lang-html";
3 | import { EditorView, lineNumbers, highlightActiveLineGutter } from "@codemirror/view";
4 | import { createCodeMirror, createEditorControlledValue } from "solid-codemirror";
5 | import { createEffect, on, onMount } from "solid-js";
6 | import { setStore, store } from "../../store";
7 | import { vsCodeDark } from "../../editor/theme/dark";
8 | import { isDarkTheme } from "../Header/ThemeBtn";
9 | import { githubLight } from "../../editor/theme/light";
10 | import { editorBaseTheme } from "../../editor/editorBaseTheme";
11 | import { isFirefox, isMobile } from "@solid-primitives/platform";
12 | import { useWindowSize } from "@solid-primitives/resize-observer";
13 | import { createMediaQuery } from "@solid-primitives/media";
14 | import TrashIcon from "../Icons/TrashIcon";
15 |
16 | const HTMLEditor = () => {
17 | const {
18 | editorView,
19 | ref: setEditorRef,
20 | createExtension,
21 | } = createCodeMirror({
22 | onValueChange,
23 | });
24 | createEditorControlledValue(editorView, () => store.htmlText);
25 | const isPortrait = createMediaQuery("(orientation: portrait)");
26 | const size = useWindowSize();
27 | let vhHeight = 0;
28 |
29 | function onValueChange(value: string) {
30 | setStore("htmlText", value);
31 | }
32 |
33 | const onClear = () => {
34 | setStore("htmlText", "");
35 | editorView().focus();
36 | };
37 |
38 | const extensions = (): Extension => {
39 | return [
40 | editorBaseTheme(),
41 | isDarkTheme() ? vsCodeDark : githubLight,
42 | lineNumbers(),
43 | highlightActiveLineGutter(),
44 | store.lineWrap ? EditorView.lineWrapping : [],
45 | EditorView.contentAttributes.of({
46 | "aria-label": "HTML code input textbox",
47 | }),
48 | html(),
49 | ];
50 | };
51 |
52 | const reconfigure = createExtension(extensions());
53 |
54 | onMount(() => {
55 | setTimeout(() => {
56 | const { scrollDOM } = editorView();
57 |
58 | scrollDOM.scrollTo({ top: 0 });
59 | });
60 | });
61 |
62 | createEffect(on(extensions, (extensions) => reconfigure(extensions)));
63 |
64 | // prevent resizing viewport height on firefox mobile when virtual keyboard opened
65 | if (isFirefox && isMobile) {
66 | createEffect(
67 | on(isPortrait, () => {
68 | const editor = editorView();
69 | if (editor && document.activeElement === editor.contentDOM) return;
70 |
71 | vhHeight = document.documentElement.clientHeight;
72 | }),
73 | );
74 |
75 | createEffect(
76 | on(
77 | () => size.height,
78 | (height) => {
79 | if (height !== vhHeight) {
80 | document.documentElement.style.height = `${vhHeight}px`;
81 | } else {
82 | document.documentElement.style.height = "";
83 | }
84 | },
85 | { defer: true },
86 | ),
87 | );
88 | }
89 |
90 | return (
91 |
92 |
93 |
94 | HTML Input
95 |
96 |
104 |
105 |
106 |
107 |
108 |
{
111 | onMount(() => {
112 | setEditorRef(el);
113 | });
114 | }}
115 | />
116 |
117 |
118 | );
119 | };
120 |
121 | export default HTMLEditor;
122 |
--------------------------------------------------------------------------------
/src/components/Header/ThemeBtn.tsx:
--------------------------------------------------------------------------------
1 | import { usePrefersDark } from "@solid-primitives/media";
2 | import { FiMoon, FiSun } from "solid-icons/fi";
3 | import {
4 | createComputed,
5 | createEffect,
6 | createSignal,
7 | Match,
8 | on,
9 | onMount,
10 | Switch,
11 | } from "solid-js";
12 | import { isServer } from "solid-js/web";
13 | import HalfSun from "../Icons/HalfSun";
14 |
15 | const [_currentTheme, setCurrentTheme] = createSignal<"light" | "dark" | "os">(
16 | !isServer ? localStorage.theme || "os" : "os"
17 | );
18 |
19 | export const isDarkTheme = () => {
20 | const _prefersDark = usePrefersDark();
21 | const prefersDark = _prefersDark();
22 | const currentTheme = _currentTheme();
23 |
24 | if (currentTheme === "os") {
25 | // solid-codemirror probably fails to update theme because it initializes after usePrefersDark updates.
26 | // so this redundant matchMedia is to get correct dark matches value on initial load
27 | if (!isServer) {
28 | return window.matchMedia("(prefers-color-scheme: dark)").matches;
29 | }
30 |
31 | return prefersDark;
32 | }
33 | return currentTheme === "dark";
34 | };
35 |
36 | const ThemeBtn = () => {
37 | const [theme, setTheme] = createSignal<"light" | "dark" | "os">("os");
38 | const prefersDark = usePrefersDark();
39 |
40 | const onClickTheme = () => {
41 | switch (theme()) {
42 | // 1. OS (default dark)
43 | // 2. user light
44 | // 3. user dark
45 | // 4. OS (default dark)
46 | case "dark":
47 | {
48 | if (prefersDark()) {
49 | document.documentElement.classList.add("dark");
50 | localStorage.removeItem("theme");
51 | setTheme("os");
52 | } else {
53 | document.documentElement.classList.remove("dark");
54 | localStorage.theme = "light";
55 | setTheme("light");
56 | }
57 | }
58 | break;
59 | // 1. OS (default light)
60 | // 2. user dark
61 | // 3. user light
62 | // 4. OS (default light)
63 | case "light":
64 | {
65 | if (prefersDark()) {
66 | document.documentElement.classList.add("dark");
67 | localStorage.theme = "dark";
68 | setTheme("dark");
69 | } else {
70 | localStorage.removeItem("theme");
71 | setTheme("os");
72 | }
73 | }
74 | break;
75 | default: {
76 | if (!prefersDark()) {
77 | document.documentElement.classList.add("dark");
78 | localStorage.theme = "dark";
79 | setTheme("dark");
80 | } else {
81 | document.documentElement.classList.remove("dark");
82 | localStorage.theme = "light";
83 | setTheme("light");
84 | }
85 | }
86 | }
87 | };
88 |
89 | createComputed(
90 | on(
91 | theme,
92 | () => {
93 | setCurrentTheme(theme());
94 | },
95 | { defer: true }
96 | )
97 | );
98 |
99 | createEffect(
100 | on(
101 | prefersDark,
102 | (prefersDark) => {
103 | if (theme() !== "os") return;
104 |
105 | if (prefersDark) {
106 | document.documentElement.classList.add("dark");
107 | } else {
108 | document.documentElement.classList.remove("dark");
109 | }
110 | },
111 | { defer: true }
112 | )
113 | );
114 |
115 | onMount(() => {
116 | let lsTheme = localStorage.theme;
117 | if (!lsTheme) lsTheme = "os";
118 |
119 | setTheme(lsTheme);
120 | });
121 |
122 | return (
123 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | );
141 | };
142 |
143 | export default ThemeBtn;
144 |
--------------------------------------------------------------------------------
/src/components/ConfigPanel/Select.tsx:
--------------------------------------------------------------------------------
1 | import { autoUpdate, flip, offset, shift } from "@floating-ui/dom";
2 | import { createResizeObserver } from "@solid-primitives/resize-observer";
3 | import Dismiss from "solid-dismiss";
4 | import { useFloating } from "solid-floating-ui";
5 | import { Component, createSignal, For } from "solid-js";
6 | import ChevronDownIcon from "../Icons/ChevronDownIcon";
7 |
8 | const Select: Component<{
9 | name: string;
10 | id?: string;
11 | options: string[];
12 | selected: string;
13 | disabled?: boolean;
14 | onOpen?: (open: boolean) => void;
15 | onChange: (props: { value: string; idx: number }) => void;
16 | }> = (props) => {
17 | const [reference, setReference] = createSignal
();
18 | const [floating, setFloating] = createSignal();
19 | const [open, setOpen] = createSignal(false);
20 | let floatingUl!: HTMLUListElement;
21 | createResizeObserver(reference as any, ({ width, height }, el) => {
22 | if (!floating()) return;
23 | if (el !== reference()) return;
24 | setFloatingUlWidthSameSizeAsButton();
25 | });
26 |
27 | const setFloatingUlWidthSameSizeAsButton = () => {
28 | floatingUl.style.width = `${reference()!.clientWidth}px`;
29 | };
30 | const position = useFloating(reference, floating, {
31 | whileElementsMounted: autoUpdate,
32 | middleware: [offset(4), flip(), shift()],
33 | });
34 |
35 | return (
36 |
40 |
{props.name}
41 |
45 | {props.selected}
46 |
47 |
48 |
49 |
50 |
{
55 | if (open) {
56 | setFloatingUlWidthSameSizeAsButton();
57 | }
58 | props.onOpen && props.onOpen(open);
59 | }}
60 | focusElementOnOpen={`[data-selected="true"]`}
61 | mount="body"
62 | cursorKeys
63 | animation={{
64 | enterClass: "opacity-0",
65 | enterToClass: "opacity-100 transition",
66 | exitClass: "opacity-100",
67 | exitToClass: "opacity-0 transition-200",
68 | }}
69 | >
70 |
79 |
80 |
81 | {(item, idx) => (
82 | {
87 | setOpen(false);
88 | props.onChange({ value: item, idx: idx() });
89 | }}
90 | onKeyDown={(e) => {
91 | if (e.key !== "Enter") return;
92 | setOpen(false);
93 | props.onChange({ value: item, idx: idx() });
94 | }}
95 | >
96 |
103 | {item}
104 |
105 |
106 | )}
107 |
108 |
109 |
110 |
111 |
112 | );
113 | };
114 |
115 | export default Select;
116 |
--------------------------------------------------------------------------------
/src/components/ActionsPanel/TogglePanelButton.tsx:
--------------------------------------------------------------------------------
1 | import { createMediaQuery } from "@solid-primitives/media";
2 | import { createEffect, createSignal, on } from "solid-js";
3 | import { onTransitionend } from "../../utils/onTransitionend";
4 | import XIcon from "../Icons/XIcon";
5 | import SettingsIcon from "../Icons/SettingsIcon";
6 |
7 | const TogglePanelButton = () => {
8 | const vwMax850px = createMediaQuery("(max-width:850px)");
9 | const [close, setClose] = createSignal(false);
10 | let toggleBtnXIconEl!: SVGSVGElement;
11 | let toggleBtnSettingsIconEl!: HTMLDivElement;
12 |
13 | const queryEls = () => {
14 | const actionsPanelEl = document.getElementById("actions-panel")!;
15 | const settingsPanelContainerEl = document.getElementById("settings-panel-container")!;
16 | const configContainerEl = document.getElementById("config-container")!;
17 |
18 | return {
19 | actionsPanelEl,
20 | settingsPanelContainerEl,
21 | configContainerEl,
22 | };
23 | };
24 |
25 | const openPanel = () => {
26 | const { settingsPanelContainerEl, configContainerEl } = queryEls();
27 | configContainerEl.style.display = "";
28 | settingsPanelContainerEl.style.height = "";
29 | settingsPanelContainerEl.style.transition = "height 150ms";
30 | toggleBtnXIconEl.style.transform = "scale(1)";
31 | toggleBtnXIconEl.style.transition = "transform 350ms";
32 | toggleBtnSettingsIconEl.style.transform = "scale(0)";
33 | toggleBtnSettingsIconEl.style.transition = "transform 350ms";
34 |
35 | onTransitionend(settingsPanelContainerEl, () => {
36 | settingsPanelContainerEl.style.minHeight = "";
37 | settingsPanelContainerEl.style.transition = "";
38 | });
39 | };
40 |
41 | const closePanel = () => {
42 | const { actionsPanelEl, settingsPanelContainerEl, configContainerEl } = queryEls();
43 | const actionsPanelHeight = actionsPanelEl.getBoundingClientRect().height;
44 | settingsPanelContainerEl.style.height = `${actionsPanelHeight}px`;
45 | settingsPanelContainerEl.style.minHeight = "auto";
46 | settingsPanelContainerEl.style.transition = "height 150ms";
47 | toggleBtnXIconEl.style.transform = "scale(0)";
48 | toggleBtnXIconEl.style.transition = "transform 350ms";
49 | toggleBtnSettingsIconEl.style.opacity = "1";
50 | toggleBtnSettingsIconEl.style.transform = "scale(0)";
51 | requestAnimationFrame(() => {
52 | requestAnimationFrame(() => {
53 | toggleBtnSettingsIconEl.style.transform = "scale(1)";
54 | toggleBtnSettingsIconEl.style.transition = "transform 350ms";
55 | });
56 | });
57 |
58 | onTransitionend(settingsPanelContainerEl, () => {
59 | settingsPanelContainerEl.style.transition = "";
60 | configContainerEl.style.display = "none";
61 | });
62 | };
63 |
64 | createEffect(
65 | on(
66 | close,
67 | (close) => {
68 | if (close) {
69 | closePanel();
70 | return;
71 | }
72 | openPanel();
73 | },
74 | { defer: true },
75 | ),
76 | );
77 |
78 | createEffect(
79 | on(
80 | vwMax850px,
81 | (vwMax850px) => {
82 | if (!close()) {
83 | return;
84 | }
85 |
86 | const run = () => {
87 | const { actionsPanelEl, settingsPanelContainerEl, configContainerEl } = queryEls();
88 |
89 | const actionsPanelHeight = actionsPanelEl.getBoundingClientRect().height;
90 |
91 | if (vwMax850px) {
92 | settingsPanelContainerEl.style.height = `${actionsPanelHeight}px`;
93 | settingsPanelContainerEl.style.minHeight = "auto";
94 | configContainerEl.style.display = "none";
95 | } else {
96 | settingsPanelContainerEl.style.height = "";
97 | settingsPanelContainerEl.style.minHeight = "";
98 | configContainerEl.style.display = "";
99 | }
100 | };
101 |
102 | setTimeout(run, vwMax850px ? 100 : 0);
103 | },
104 | { defer: true },
105 | ),
106 | );
107 |
108 | return (
109 | setClose(!close())}
112 | aria-label={`${!close() ? "close" : "open"} configeration panel`}
113 | aria-expanded={!close()}
114 | >
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | );
123 | };
124 |
125 | export default TogglePanelButton;
126 |
--------------------------------------------------------------------------------
/src/components/ActionsPanel/ActionsPanel.tsx:
--------------------------------------------------------------------------------
1 | import { autoUpdate, flip, offset, shift } from "@floating-ui/dom";
2 | import Dismiss from "solid-dismiss";
3 | import { useFloating } from "solid-floating-ui";
4 | import { createMemo, createSignal, For, JSX, ParentComponent } from "solid-js";
5 | import { Dynamic } from "solid-js/web";
6 | import { setStore, store } from "../../store";
7 | import CopyJSXButton from "../CopyJSXButton";
8 | import SplitPanelColumns from "../Icons/SplitPanelColumns";
9 | import SplitPanelRows from "../Icons/SplitPanelRows";
10 | import TogglePanelButton from "./TogglePanelButton";
11 |
12 | const ActionsPanel = () => {
13 | const list: {
14 | id: string;
15 | text: string;
16 | Icon?: () => JSX.Element;
17 | }[] = [
18 | {
19 | id: "rows",
20 | text: "HTML and JSX rows",
21 | Icon: SplitPanelRows,
22 | },
23 | {
24 | id: "columns",
25 | text: "HTML and JSX columns",
26 | Icon: SplitPanelColumns,
27 | },
28 | {
29 | id: "html",
30 | text: "HTML only",
31 | Icon: () => HTML ,
32 | },
33 | {
34 | id: "jsx",
35 | text: "JSX only",
36 | Icon: () => JSX ,
37 | },
38 | ];
39 |
40 | const [reference, setReference] = createSignal();
41 | const [floating, setFloating] = createSignal();
42 | const [open, setOpen] = createSignal(false);
43 | let floatingUl!: HTMLUListElement;
44 | const position = useFloating(reference, floating, {
45 | whileElementsMounted: autoUpdate,
46 | placement: "bottom-start",
47 | middleware: [offset(4), flip(), shift()],
48 | });
49 |
50 | const selectedItemFromList = createMemo(() => {
51 | const item = list.find(({ id }) => id === store.layout);
52 | return item;
53 | });
54 |
55 | return (
56 |
60 |
61 |
62 |
73 |
74 |
75 |
89 |
98 |
99 |
100 | {({ id, text, Icon }) => (
101 | {
106 | setOpen(false);
107 | setStore("layout", id as "jsx");
108 | }}
109 | onKeyDown={(e) => {
110 | if (e.key !== "Enter") return;
111 | setOpen(false);
112 | setStore("layout", id as "jsx");
113 | }}
114 | >
115 |
122 |
{Icon && }
123 |
{text}
124 |
125 |
126 | )}
127 |
128 |
129 |
130 |
131 |
132 |
133 | Line Wrap
134 | setStore("lineWrap", e.currentTarget.checked)}
140 | />
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | );
149 | };
150 |
151 | const IconText: ParentComponent = (props) => {
152 | return (
153 |
154 |
155 | {props.children}
156 |
157 |
158 | );
159 | };
160 |
161 | export default ActionsPanel;
162 |
--------------------------------------------------------------------------------
/src/components/ConfigPanel/Input.tsx:
--------------------------------------------------------------------------------
1 | import { arrow, autoUpdate, computePosition, flip, offset } from "@floating-ui/dom";
2 | import Dismiss from "solid-dismiss";
3 | import { useFloating } from "solid-floating-ui";
4 | import { Component, createSignal, Show } from "solid-js";
5 |
6 | const Input: Component<{
7 | name: string;
8 | value: string;
9 | maskType: "character-count";
10 | disabled?: boolean;
11 | filter?: RegExp;
12 | placeholder?: string;
13 | tooltip?: boolean;
14 | onInput: (value: string) => void;
15 | }> = (props) => {
16 | const [reference, setReference] = createSignal();
17 | const [floating, setFloating] = createSignal();
18 | const [open, setOpen] = createSignal(false);
19 | // todo fix dismiss
20 | const [clickedClose, setClickedClose] = createSignal(false);
21 | const [show, setShow] = createSignal(true);
22 | let arrowEl!: HTMLDivElement;
23 |
24 | const countWhiteSpace = () => {
25 | const value = props.value;
26 | const group: { char: string; count: number }[] = [];
27 | if (value[0] != null) {
28 | group.push({ char: value[0], count: 0 });
29 | }
30 | value.split("").forEach((item) => {
31 | const groupItem = group.at(-1)!;
32 | if (groupItem.char === item) {
33 | groupItem.count = groupItem.count + 1;
34 | } else {
35 | group.push({ char: item, count: 1 });
36 | }
37 | });
38 |
39 | setShow(true);
40 |
41 | return group
42 | .map(({ count, char }) => {
43 | if (char === "\t") char = "\\t";
44 | if (char.match(/\s/)) char = "\\s";
45 |
46 | return `${char}${count > 1 ? `(x${count})` : ""}`;
47 | })
48 | .reduce((a, c) => a + " " + c, "");
49 | };
50 |
51 | const onClickReplaceTab = () => {
52 | const value = props.value;
53 | setClickedClose(true);
54 | setOpen(false);
55 | props.onInput("\t");
56 | };
57 |
58 | useFloating(reference, floating, {
59 | whileElementsMounted: (reference, floating) =>
60 | autoUpdate(reference, floating, () => {
61 | const arrowLen = arrowEl.offsetWidth;
62 | const floatingOffset = Math.sqrt(2 * arrowLen ** 2) / 2;
63 | computePosition(reference, floating, {
64 | placement: "bottom",
65 | middleware: [offset(floatingOffset), flip(), arrow({ element: arrowEl })],
66 | }).then(({ middlewareData, placement, x, y }) => {
67 | const side = placement.split("-")[0];
68 |
69 | Object.assign(floating.style, {
70 | left: `${x}px`,
71 | top: `${y}px`,
72 | });
73 |
74 | const staticSide = {
75 | top: "bottom",
76 | right: "left",
77 | bottom: "top",
78 | left: "right",
79 | }[side];
80 |
81 | if (middlewareData.arrow) {
82 | const { x, y } = middlewareData.arrow;
83 | const isTop = placement === "top";
84 | const maskImage = `linear-gradient(${isTop ? "315" : "135"}deg, #000 calc(50% - ${
85 | isTop ? "1" : "0.5"
86 | }px),transparent calc(50% - ${isTop ? "1" : "0.5"}px))`;
87 |
88 | Object.assign(arrowEl!.style, {
89 | left: x != null ? `${x}px` : "",
90 | top: y != null ? `${y}px` : "",
91 | // Ensure the static side gets unset when
92 | // flipping to other placements' axes.
93 | right: "",
94 | bottom: "",
95 | [staticSide!]: `${-arrowLen / 2 + 2}px`,
96 | transform: `rotate(45deg)`,
97 | WebkitMaskImage: maskImage,
98 | });
99 | }
100 | });
101 | }),
102 | });
103 |
104 | return (
105 |
109 | {props.name}
110 |
111 |
{
119 | if (!props.value.match(/\t/)) {
120 | if (!clickedClose()) {
121 | // another dismiss bug
122 | setTimeout(() => {
123 | setOpen(true);
124 | });
125 | }
126 | setClickedClose(false);
127 | }
128 | setShow(false);
129 | }}
130 | onBlur={() => {
131 | setShow(true);
132 | }}
133 | onInput={(e) => {
134 | let value = e.currentTarget.value;
135 | if (props.filter) {
136 | value = value.replace(props.filter, "");
137 | e.currentTarget.value = value;
138 | }
139 |
140 | value = value.replace(/\\s/g, " ");
141 |
142 | const result = value.replace(/\\t/g, "\t");
143 | if (result !== value) {
144 | value = result.replace(/[^\t]/g, "");
145 | e.currentTarget.value = value;
146 | }
147 |
148 | props.onInput(value);
149 | }}
150 | ref={setReference}
151 | />
152 |
153 |
154 | {countWhiteSpace()}
155 |
156 |
157 |
158 |
159 |
173 |
174 |
175 |
176 | To use TABs, type{" "}
177 | \\t
178 |
179 |
180 |
181 |
182 |
Or
183 |
184 |
185 |
189 | Replace with TAB
190 |
191 |
192 |
196 |
197 |
198 |
199 |
200 | );
201 | };
202 |
203 | export default Input;
204 |
--------------------------------------------------------------------------------
/src/components/Editors/JSXEditor.tsx:
--------------------------------------------------------------------------------
1 | import { javascript } from "@codemirror/lang-javascript";
2 | import { Extension } from "@codemirror/state";
3 | import { EditorView, lineNumbers } from "@codemirror/view";
4 | import {
5 | createCodeMirror,
6 | createEditorControlledValue,
7 | createEditorReadonly,
8 | } from "solid-codemirror";
9 | import { $TRACK, createEffect, createSignal, on, onMount } from "solid-js";
10 | import { editorBaseTheme } from "../../editor/editorBaseTheme";
11 | import { vsCodeDark } from "../../editor/theme/dark";
12 | import { githubLight } from "../../editor/theme/light";
13 | import HTMLtoJSX from "../../lib/html-to-jsx";
14 | import { ConfigKey, setStore, store } from "../../store";
15 | import CopyJSXButton from "../CopyJSXButton";
16 | import { isDarkTheme } from "../Header/ThemeBtn";
17 |
18 | const JSXEditor = () => {
19 | // the initial jsx demo code has siblings that are not wrapped, wraps with fragments that will be hidden to preserve syntax highlighting
20 | const [code, setCode] = createSignal(`<>\n${store.jsxText.trimEnd()}\n>`);
21 | const [hiddenFragments, setHiddenFragments] = createSignal(true);
22 | const { editorView, ref: setEditorRef, createExtension } = createCodeMirror();
23 | createEditorControlledValue(editorView, code);
24 | createEditorReadonly(editorView, () => true);
25 | let htmlToJSXConverter!: HTMLtoJSX;
26 |
27 | const extensions = (): Extension => {
28 | return [
29 | editorBaseTheme({ backgroundColor: "transparent" }),
30 | isDarkTheme() ? vsCodeDark : githubLight,
31 | lineNumbers(),
32 | store.lineWrap ? EditorView.lineWrapping : [],
33 | hiddenFragments()
34 | ? // hides first and last lines that contain dumb fragments, to prevent syntax highlight breaking, and reset and move counter increment by 1
35 | EditorView.theme({
36 | ".cm-line:first-child": {
37 | display: "none",
38 | },
39 | ".cm-gutterElement:nth-child(n+1)": {
40 | position: "relative",
41 | visibility: "hidden",
42 | },
43 | ".cm-gutters": {
44 | counterReset: "gutter-mask",
45 | },
46 | ".cm-gutterElement:first-child:after": {
47 | counterReset: "gutter-mask",
48 | content: '"" !important',
49 | visibility: "hidden !important",
50 | },
51 | ".cm-gutterElement:nth-child(2):after": {
52 | counterReset: "gutter-mask",
53 | content: '"" !important',
54 | visibility: "hidden !important",
55 | },
56 | ".cm-gutterElement:after": {
57 | position: "absolute",
58 | counterIncrement: "gutter-mask",
59 | top: "0",
60 | left: "0",
61 | bottom: "0",
62 | right: "0",
63 | display: "flex",
64 | visibility: "visible",
65 | justifyContent: "center",
66 | content: "counter(gutter-mask)",
67 | },
68 | ".cm-line:nth-last-child(1)": {
69 | display: "none",
70 | },
71 | })
72 | : [],
73 | EditorView.contentAttributes.of({
74 | "aria-label": "JSX code output",
75 | "aria-readonly": "true",
76 | }),
77 | javascript({
78 | jsx: true,
79 | typescript: true,
80 | }),
81 | ];
82 | };
83 |
84 | const reconfigure = createExtension(extensions());
85 |
86 | const insertHiddenFragments = (convertedJsx: string) => {
87 | // insert hidden fragments to keep jsx synthax highlighter to work properly
88 | const hasWrapperNode = store.config.wrapperNode === "none" && store.config.component === "none";
89 | requestAnimationFrame(() => {
90 | setHiddenFragments(hasWrapperNode);
91 | });
92 | if (hasWrapperNode) {
93 | convertedJsx = `<>\n${convertedJsx.trimEnd()}\n>`;
94 | }
95 | return convertedJsx;
96 | };
97 |
98 | const prefixSVGIds = (convertedJsx: string) => {
99 | if (!store.config.prefixSVGIds) return convertedJsx;
100 | return namespaceSVGId(convertedJsx, store.config.prefixSVGIds);
101 | };
102 |
103 | const updateEditorText = () => {
104 | let converted = htmlToJSXConverter.convert(store.htmlText);
105 |
106 | if (!converted || converted === "\n") {
107 | const code = "\n";
108 | setCode(code);
109 | setStore("jsxText", code);
110 | return;
111 | }
112 |
113 | converted = prefixSVGIds(converted);
114 | const jsxText = converted;
115 | converted = insertHiddenFragments(converted);
116 |
117 | setStore("jsxText", jsxText);
118 | setCode(converted);
119 | };
120 |
121 | onMount(() => {
122 | htmlToJSXConverter = new HTMLtoJSX(store.config);
123 |
124 | setTimeout(() => {
125 | const { scrollDOM } = editorView();
126 |
127 | scrollDOM.scrollTo({ top: 0 });
128 | });
129 | });
130 |
131 | createEffect(on(extensions, (extensions) => reconfigure(extensions)));
132 |
133 | createEffect(
134 | on(
135 | () => store.config[$TRACK as any as ConfigKey],
136 | () => {
137 | htmlToJSXConverter.config = { ...store.config };
138 | if (htmlToJSXConverter.config.indent) {
139 | htmlToJSXConverter.config.indent = htmlToJSXConverter.config.indent.replace(/\\/g, "");
140 | }
141 | updateEditorText();
142 | },
143 | { defer: true },
144 | ),
145 | );
146 |
147 | createEffect(
148 | on(
149 | () => store.htmlText,
150 | () => {
151 | updateEditorText();
152 | },
153 | { defer: true },
154 | ),
155 | );
156 |
157 | return (
158 |
159 |
160 |
161 | JSX Output
162 |
163 |
164 |
165 | {/* TODO: On Mobile Chrome, the JSX editor font size is 14.3px instead of declared 13px */}
166 |
{
169 | onMount(() => {
170 | setEditorRef(el);
171 | });
172 | }}
173 | />
174 |
175 |
179 |
180 |
181 |
182 | );
183 | };
184 |
185 | function namespaceSVGId(svg: string, namespace: string) {
186 | svg = svg.replace(/(
]*>)([\s\S]*?)(<\/svg>)/g, (_, startSvg, svgBody, endSvg) => {
187 | if (!svgBody) return _;
188 |
189 | svgBody = svgBody.replace(/id="(.*?)"/g, (_: string, p1: string) => {
190 | return `id="${namespace}${p1}"`;
191 | });
192 |
193 | svgBody = svgBody.replace(/xlink:href="#(.*?)"/g, (_: string, p1: string) => {
194 | return `xlink:href="#${namespace}${p1}"`;
195 | });
196 | svgBody = svgBody.replace(/mask="url\(#(.*?)\)"/g, (_: string, p1: string) => {
197 | return `mask="url(#${namespace}${p1})"`;
198 | });
199 | svgBody = svgBody.replace(/fill="url\(#(.*?)\)"/g, (_: string, p1: string) => {
200 | return `fill="url(#${namespace}${p1})"`;
201 | });
202 | svgBody = svgBody.replace(/filter="url\(#(.*?)\)"/g, (_: string, p1: string) => {
203 | return `filter="url(#${namespace}${p1})"`;
204 | });
205 |
206 | return `${startSvg}${svgBody}${endSvg}`;
207 | })!;
208 |
209 | return svg;
210 | }
211 |
212 | export default JSXEditor;
213 |
--------------------------------------------------------------------------------
/src/components/MobileScrollDVH.tsx:
--------------------------------------------------------------------------------
1 | import { isFirefox, isIOS } from "@solid-primitives/platform";
2 | import { ParentComponent, onMount } from "solid-js";
3 |
4 | /**
5 | * MobileScrollDVH (Dynamic Viewport Height)
6 | * Makes element's height use dynamic viewport height, `100dvh`, so initially fits small viewport, but allows user to scroll to collapse/hide browser toolbars and then dynamically updates height to long viewport. https://web.dev/blog/viewport-units
7 | *
8 | * By default certain types of websites that fit inside viewport ( such as simple texteditors, transformers, tooling ect ), layout-wise are not reading document and therefore won't have page scroll. However this causes the mobile browser navigation toolbar to be forever visible and takes up valuable space. This wrapper allows the user to reclaim additional space back to the site by scrolling down to trigger toolbar to hide.
9 | *
10 | */
11 | const MobileScrollDVH: ParentComponent = ({ children }) => {
12 | // Browser allows scroll to hide toolbars when:
13 | // - Chrome: 2.5 seconds after page loads.
14 | // - Firefox and Safari: Immediatly after page loads.
15 |
16 | // Scroll sliding direction to toggle toolbar, but slide delta isn't more than toolbar height (or some value) and haven't let pointer up:
17 | // - Chrome: Only translates viewport.
18 | // - Safari: Scrolls page.
19 | // - Firefox: Most of the time follows Chrome behavior but sometimes scrolls a little bit.
20 |
21 | // Scroll sliding but letting go before passing threshold to toggle toolbar which cancels it, does:
22 | // - Chrome: Amount that was slid, is the same that applies to page scroll.
23 | // - Safari: Nothing, no additional scroll is applied to page.
24 | // - Firefox: Most of time follows Safari behavior but sometimes additional scroll is added causing tiny scroll jump.
25 |
26 | // Scroll, caused by overscroll (already reached the boundary of sub element scrolling area), sliding direction to toggle toolbar, but slide delta isn't more than toolbar height (or some value) and haven't let pointer up:
27 | // - Chrome: Only translates viewport.
28 | // - Safari: Scrolls page, will close toolbar when scrolled all the way down.
29 | // - Firefox: Scrolls page, never closes toolbar must let go pointer then scroll down again to close toolbar.
30 |
31 | let dvhEl!: HTMLDivElement;
32 | let lvhEl!: HTMLDivElement;
33 | let svhEl!: HTMLDivElement;
34 |
35 | onMount(() => {
36 | // must round due to iOS subpixel inconsistencies
37 | const getDocumentScrollHeight = () => Math.round(document.documentElement.scrollHeight);
38 | const getlvhHeight = () => ({ height: Math.round(lvhEl.getBoundingClientRect().height) });
39 | let documentScrollHeight = 0;
40 | let lvhElBCR = { height: 0 };
41 | let scrollStart = false;
42 |
43 | calculateRootHeightElements();
44 |
45 | const { debounced: debouncedScrollToTopAwayFromOnePixel } = debounce(() => {
46 | scrollToTopAwayFromOnePixel();
47 | }, 250);
48 | const { debounced: debouncedCalculateRootHeightElements } = debounce(() => {
49 | calculateRootHeightElements();
50 | }, 250);
51 | const { debounced: debouncedScrollToTop2s, cancel: cancelDebouncedScrollToTop3s } = debounce(
52 | (data) => {
53 | const documentScrollHeight = getDocumentScrollHeight();
54 | // what if document changed height where it can be scrolled
55 | if (data.documentScrollHeight !== documentScrollHeight) return;
56 | window.scrollTo({ top: 0 });
57 | },
58 | 2000,
59 | );
60 |
61 | // no toolbars present, exit
62 | if (Math.round(svhEl.getBoundingClientRect().height) === lvhElBCR.height) return;
63 |
64 | requestAnimationFrame(() => {
65 | // 100lvh allows page to be scrolled in all browsers, but overscroll inside scrollable containers that allows page to be scrolled doesn't happen in Firefox.
66 | let extendHeight = 0;
67 |
68 | if (isFirefox) {
69 | extendHeight = 0.2;
70 | }
71 |
72 | if (isIOS) {
73 | // Toolbar only collapses if document scroll height is greater than 100lvh.
74 | // The minimum extended height is 5px. Otherwise, after toolbar collapses, and dvh applies, toolbar reverts back to expanded size.
75 | extendHeight = 5;
76 | }
77 | lvhEl.style.height = `calc(100lvh + ${extendHeight}px)`;
78 | });
79 |
80 | // fixes "scroll creeping" on Chromium devices, where if you scroll a tiny bit down to slide the toolbars but not beyond the threshold to activate hidding them, the toolbars don't hide and scroll amount commits to actual page scroll.
81 | const onScroll = (e: Event) => {
82 | if (scrollStart) {
83 | calculateRootHeightElements();
84 | }
85 | scrollStart = false;
86 |
87 | // if other elements extend the document height and is intended to be scrolled, don't scroll to top
88 | if (documentScrollHeight > lvhElBCR.height) return;
89 |
90 | if (isFirefox) {
91 | // Firefox, the 1st gestured overscroll won't close toolbar, instead scrolls page, in this case 0.2px down, subsequent scrolls will close toolbar
92 | debouncedScrollToTop2s({ documentScrollHeight });
93 | return;
94 | }
95 | if (!isIOS) {
96 | // on iOS, scroll event does fire upon sliding, causing a janky effect due to page scrolling to top and good change you don't scroll to bottom to collapse toolbar
97 | // one solution is to debounce the scrollTo top page, but the issue is that in iOS, visually the page doesn't scroll to the top so the top part is cut off.
98 | window.scrollTo({ top: 0 });
99 | return;
100 | }
101 |
102 | // another issue is that if user taps toolbar to expand, from then on, the toolbar can never be collapsed unless user scrolls at top of page (1 pixel away) then scrolls back down.
103 | debouncedScrollToTopAwayFromOnePixel();
104 | };
105 |
106 | function scrollToTopAwayFromOnePixel() {
107 | requestAnimationFrame(() => {
108 | if (window.scrollY <= 1) {
109 | return;
110 | }
111 | window.scrollBy({ top: -(window.scrollY - 1) });
112 | setTimeout(() => {
113 | window.scrollTo({ top: 0 });
114 | setTimeout(() => {
115 | scrollStart = false;
116 | });
117 | });
118 | });
119 | }
120 |
121 | window.addEventListener("scroll", onScroll, { passive: true });
122 | window.addEventListener("scrollend", () => {
123 | scrollStart = true;
124 | });
125 |
126 | // sliding toolbar triggers window resize
127 | window.addEventListener(
128 | "resize",
129 | () => {
130 | if (!isIOS) {
131 | calculateRootHeightElements();
132 | return;
133 | }
134 |
135 | // resize fires on every height change during iOS toolbar toggle animation, so debounce it
136 | debouncedCalculateRootHeightElements();
137 | },
138 | { passive: true },
139 | );
140 |
141 | function calculateRootHeightElements() {
142 | documentScrollHeight = getDocumentScrollHeight();
143 | lvhElBCR = getlvhHeight();
144 | }
145 |
146 | setTimeout(() => {
147 | scrollStart = true;
148 | });
149 | });
150 |
151 | return (
152 |
153 | {children}
154 |
155 |
156 |
157 | );
158 | };
159 |
160 | function debounce(cb: (data: any) => void, timeout: number) {
161 | let timeId = null as any as number;
162 | return {
163 | debounced: (data?: any) => {
164 | window.clearTimeout(timeId);
165 | timeId = window.setTimeout(() => cb(data), timeout);
166 | },
167 | cancel: () => {
168 | window.clearTimeout(timeId);
169 | },
170 | };
171 | }
172 |
173 | export default MobileScrollDVH;
174 |
--------------------------------------------------------------------------------
/src/components/ConfigPanel/ConfigPanel.tsx:
--------------------------------------------------------------------------------
1 | import { $TRACK, createEffect, createSignal, For, on, onMount } from "solid-js";
2 | import { createStore, produce } from "solid-js/store";
3 | import { HTMLtoJSXConfig } from "../../lib/html-to-jsx";
4 | import { ConfigKey, defaultConfig, setStore, store, TJSXConfig } from "../../store";
5 | import { debounce } from "@solid-primitives/scheduled";
6 | import Select from "./Select";
7 | import Input from "./Input";
8 | import Toggle from "./Toggle";
9 |
10 | const ConfigPanel = () => {
11 | const [configMap, setConfigMap] = createStore<{
12 | [key in keyof (HTMLtoJSXConfig & { prefixSVGIds?: string })]: {
13 | type: "input" | "checkbox" | "select";
14 | name: string;
15 | value: string | boolean;
16 | tooltip?: boolean;
17 | options?: string[];
18 | disabled?: boolean;
19 | disables?: { key: string; isValue: string | boolean }[];
20 | filter?: RegExp;
21 | placeholder?: string;
22 | maskType?: "character-count";
23 | };
24 | }>({
25 | attributeValueString: {
26 | type: "checkbox",
27 | name: "Attribute Value String",
28 | value: store.config.attributeValueString!,
29 | },
30 | camelCaseAttributes: {
31 | type: "checkbox",
32 | name: "Camel Case Attributes",
33 | value: store.config.camelCaseAttributes!,
34 | },
35 | styleAttribute: {
36 | type: "select",
37 | name: "Style Attribute",
38 | options: ["css-object", "css-string"],
39 | value: store.config.styleAttribute!,
40 | },
41 | wrapperNode: {
42 | type: "select",
43 | name: "Wrapper Node",
44 | options: ["none", "div", "fragment"],
45 | value: store.config.wrapperNode!,
46 | },
47 | component: {
48 | type: "select",
49 | name: "Component",
50 | options: ["arrow-function", "function", "none"],
51 | disables: [
52 | { key: "componentName", isValue: "none" },
53 | { key: "exportComponent", isValue: "none" },
54 | ],
55 | value: store.config.component!,
56 | },
57 | componentName: {
58 | type: "input",
59 | name: "Component Name",
60 | disabled: store.config.component === "none",
61 | filter: /\s/g,
62 | value: store.config.componentName!,
63 | },
64 | preTagWrapTemplateLiterals: {
65 | type: "checkbox",
66 | name: " tag use template literals",
67 | value: store.config.preTagWrapTemplateLiterals!,
68 | },
69 | stripStyleTag: {
70 | type: "checkbox",
71 | name: "Remove