feat(frontend): Global CSS editor (#2178)

* feat(frontend): add global css

* feat(frontend): working styling

* feat(frontend): Add default classes

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* feat(frontend): Add global css v0

* feat(frontend): Add global css v0

* add css workers

* fix(frontend): Fix overflow issue

* wip

* feat(frontend): check for EE before injecting global css

* wip

* wip

* wip

* fix(frontend): fix typing issues

* fix(frontend): fix typing issues

* fix(frontend): fix global css

* fix(frontend): add missing mapping

* fix(frontend): Fix how styles are loaded

* fix(frontend): fix preview

* feat(frontend): fix everything

* feat(frontend): fix class autocomplete

* feat(frontend): remove console.log

* feat(frontend): update tooltup

* feat(frontend): eval

* feat(frontend): eval

* feat(frontend): fix build

* feat(frontend): fix initial binding

* feat(frontend): wip

* wip

* feat(frontend): Finish theme v0

* feat(frontend): Fix resource page

* feat(frontend): fix build

* feat(frontend): theme UI

* feat(frontend): theme UI

* feat(frontend): theme UI

* feat(frontend): fix EE

* feat(frontend): add missing warning

* feat(frontend): fix preview

* feat(frontend): fix global css by component initialisation

* feat(frontend): remove unused libraries

* feat(frontend): fix EE check

* feat(frontend): fix EE check

* feat(frontend): fix preview

* feat(frontend): Fix migration

* feat(frontend): Fix issues

* feat(frontend): add missing disabled in migration modal

* feat(frontend): Fix preview

* feat(frontend): Fix preview

* all

* all

* all

* sqlx

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
This commit is contained in:
Faton Ramadani
2023-09-15 22:55:47 +02:00
committed by GitHub
parent 39590b3d25
commit 7e9ee39aa6
98 changed files with 3956 additions and 598 deletions
+1 -1
View File
@@ -58,7 +58,7 @@ RUN /usr/local/bin/python3 -m pip install nltk
RUN mkdir -p /nsjail_data/python && HOME=/nsjail_data/python /usr/local/bin/python3 -m nltk.downloader vader_lexicon
COPY --from=nsjail /nsjail/nsjail /bin/nsjail
COPY --from=oven/bun:1.0.0 /usr/local/bin/bun /usr/bin/bun
COPY --from=denoland/deno:latest /usr/bin/deno /usr/bin/deno
RUN apt-get update \
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms) VALUES ($1, 'app_themes', 'App Themes', ARRAY[]::TEXT[], '{\"g/all\": false}') ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": []
},
"hash": "584cb984ea6528baac48c5c437ad2ee3bef92e3fada73dcf519147964c0f4f4a"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO resource (workspace_id, path, value, description, resource_type) VALUES ($1, 'f/app_themes/theme_0', '{\"name\": \"Default Theme\", \"value\": \"\"}', 'The default app theme', 'app_theme') ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Varchar"
]
},
"nullable": []
},
"hash": "d956b7525f83e6d03beadc4bb3ee2798f53d990b01b17bdbc044719d4908e3f4"
}
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,3 @@
-- Add up migration script here
INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms) SELECT id, 'app_themes', 'App Themes', ARRAY[]::TEXT[], '{"g/all": false}' FROM workspace ON CONFLICT DO NOTHING;
INSERT INTO resource (workspace_id, path, value, description, resource_type) SELECT id, 'f/app_themes/theme_0', '{"name": "Default Theme", "value": ""}', 'The default app theme', 'app_theme' FROM workspace ON CONFLICT DO NOTHING
+16
View File
@@ -3519,6 +3519,22 @@ paths:
schema:
$ref: "#/components/schemas/AppWithLastVersion"
/w/{workspace}/apps_u/public_resource/{path}:
get:
summary: get public resource
operationId: get public resource
tags:
- app
parameters:
- $ref: "#/components/parameters/WorkspaceId"
- $ref: "#/components/parameters/Path"
responses:
"200":
description: resource value
content:
application/json:
schema: {}
/w/{workspace}/apps/secret_of/{path}:
get:
summary: get public secret of app
+22
View File
@@ -57,6 +57,7 @@ pub fn unauthed_service() -> Router {
Router::new()
.route("/execute_component/*path", post(execute_component))
.route("/public_app/:secret", get(get_public_app_by_secret))
.route("/public_resource/*path", get(get_public_resource))
}
pub fn global_service() -> Router {
@@ -341,6 +342,27 @@ async fn get_public_app_by_secret(
Ok(Json(app))
}
async fn get_public_resource(
Extension(db): Extension<DB>,
Path((w_id, path)): Path<(String, StripPath)>,
) -> JsonResult<Option<serde_json::Value>> {
let path = path.to_path();
if !path.starts_with("f/app_themes/") {
return Err(Error::BadRequest(
"Only app themes are public resources".to_string(),
));
}
let res = sqlx::query_scalar!(
"SELECT value from resource WHERE path = $1 AND workspace_id = $2",
path.to_owned(),
&w_id
)
.fetch_optional(&db)
.await?
.flatten();
Ok(Json(res))
}
async fn get_secret_id(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
+15
View File
@@ -950,6 +950,21 @@ async fn create_workspace(
.execute(&mut *tx)
.await?;
sqlx::query!(
"INSERT INTO folder (workspace_id, name, display_name, owners, extra_perms) VALUES ($1, 'app_themes', 'App Themes', ARRAY[]::TEXT[], '{\"g/all\": false}') ON CONFLICT DO NOTHING",
nw.id,
)
.execute(&mut *tx)
.await?;
sqlx::query!(
"INSERT INTO resource (workspace_id, path, value, description, resource_type) VALUES ($1, 'f/app_themes/theme_0', '{\"name\": \"Default Theme\", \"value\": \"\"}', 'The default app theme', 'app_theme') ON CONFLICT DO NOTHING",
nw.id,
)
.execute(&mut *tx)
.await?;
audit_log(
&mut *tx,
&authed.username,
+1 -1
View File
@@ -608,7 +608,7 @@ pub async fn run_worker<R: rsmq_async::RsmqConnection + Send + Sync + Clone + 's
let vacuum_shift = rand::thread_rng().gen_range(0..VACUUM_PERIOD);
IS_READY.store(true, Ordering::Relaxed);
tracing::info!(worker = %worker_name, "listening for jobs, config: {:#?}", WORKER_CONFIG.read().await);
tracing::info!(worker = %worker_name, "listening for jobs, config: {:?}", WORKER_CONFIG.read().await);
let (dedicated_worker_tx, dedicated_worker_handle) = if let Some(_wp) =
WORKER_CONFIG.read().await.dedicated_worker.clone()
@@ -236,7 +236,7 @@
{:else if !forceJson && resultKind == 'file'}
<div
><a
download={result.filename ?? 'windmill.file'}
download={result.filename ?? result.file?.filename ?? 'windmill.file'}
href="data:application/octet-stream;base64,{contentOrRootString(result.file)}">Download</a
>
</div>
+11 -1
View File
@@ -24,6 +24,8 @@
import 'monaco-editor/esm/vs/basic-languages/graphql/graphql.contribution'
import 'monaco-editor/esm/vs/basic-languages/powershell/powershell.contribution'
import 'monaco-editor/esm/vs/language/typescript/monaco.contribution'
import 'monaco-editor/esm/vs/basic-languages/css/css.contribution'
import { MonacoLanguageClient, initServices } from 'monaco-languageclient'
import { toSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc'
import { CloseAction, ErrorAction, RequestType, NotificationType } from 'vscode-languageclient'
@@ -49,7 +51,15 @@
let divEl: HTMLDivElement | null = null
let editor: meditor.IStandaloneCodeEditor
export let lang: 'typescript' | 'python' | 'go' | 'shell' | 'sql' | 'graphql' | 'powershell'
export let lang:
| 'typescript'
| 'python'
| 'go'
| 'shell'
| 'sql'
| 'graphql'
| 'powershell'
| 'css'
export let deno: boolean
export let code: string = ''
export let cmdEnterAction: (() => void) | undefined = undefined
@@ -42,6 +42,7 @@
return sql
case 'powershell':
return powershell
default:
return typescript
}
@@ -18,6 +18,9 @@
import 'monaco-editor/esm/vs/basic-languages/graphql/graphql.contribution'
import 'monaco-editor/esm/vs/language/json/monaco.contribution'
import 'monaco-editor/esm/vs/language/typescript/monaco.contribution'
import 'monaco-editor/esm/vs/basic-languages/css/css.contribution'
import 'monaco-editor/esm/vs/language/css/monaco.contribution'
import { allClasses, authorizedClassnames } from './apps/editor/componentsPanel/cssUtils'
import { createEventDispatcher, onDestroy, onMount } from 'svelte'
@@ -185,6 +188,40 @@
})
}
$: lang == 'css' && addCSSClassCompletions()
function addCSSClassCompletions() {
languages.registerCompletionItemProvider('css', {
provideCompletionItems: function (model, position, context, token) {
const word = model.getWordUntilPosition(position)
const range = {
startLineNumber: position.lineNumber,
startColumn: word.startColumn,
endLineNumber: position.lineNumber,
endColumn: word.endColumn
}
if (word && word.word) {
const currentWord = word.word
const suggestions = allClasses
.filter((className) => className.includes(currentWord))
.map((className) => ({
label: className,
kind: languages.CompletionItemKind.Class,
insertText: className,
documentation: 'Custom CSS class',
range: range
}))
return { suggestions }
}
return { suggestions: [] }
}
})
}
function loadExtraLib() {
if (lang == 'javascript') {
const stdLib = { content: libStdContent, filePath: 'es5.d.ts' }
@@ -199,6 +236,38 @@
} else {
languages.typescript.javascriptDefaults.setExtraLibs([stdLib])
}
} else if (lang === 'css') {
const cssClasses = authorizedClassnames.map((className) => '.' + className)
languages.registerCompletionItemProvider('css', {
provideCompletionItems: function (model, position, context, token) {
const word = model.getWordUntilPosition(position)
const range = {
startLineNumber: position.lineNumber,
startColumn: word.startColumn,
endLineNumber: position.lineNumber,
endColumn: word.endColumn
}
if (word && word.word) {
const currentWord = word.word
const suggestions = cssClasses
.filter((className) => className.includes(currentWord))
.map((className) => ({
label: className,
kind: languages.CompletionItemKind.Class,
insertText: className,
documentation: 'Custom CSS class',
range: range
}))
return { suggestions }
}
return { suggestions: [] }
}
})
}
}
@@ -6,10 +6,11 @@
export let headers: string[] | undefined
export let data: any[] | undefined // Object containing the data
export let keys: string[]
export let size: 'sm' | 'md' | 'lg' = 'md'
</script>
<div class="mt-2">
<DataTable>
<div class="mt-2 w-full">
<DataTable {size}>
<Head>
<tr>
{#if headers}
+2 -1
View File
@@ -7,6 +7,7 @@
export let wrapperClass = ''
export let placement: PopoverPlacement | undefined = undefined
export let documentationLink: string | undefined = undefined
export let small = false
</script>
<Popover notClickable {placement} class={wrapperClass}>
@@ -15,7 +16,7 @@
? 'text-tertiary-inverse'
: 'text-tertiary'} {$$props.class} relative"
>
<InfoIcon class="-bottom-0.5 absolute" size={16} />
<InfoIcon class="{small ? 'bottom-0' : '-bottom-0.5'} absolute" size={small ? 12 : 16} />
</div>
<svelte:fragment slot="text">
<slot />
@@ -17,7 +17,8 @@
import { initConfig, initOutput } from '../../editor/appUtils'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import { concatCustomCss } from '../../utils'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
import { initCss } from '../../utils'
export let id: string
export let componentInput: AppInput | undefined
@@ -123,7 +124,7 @@
}
let loading = false
$: css = concatCustomCss($app.css?.buttoncomponent, customCss)
let css = initCss($app.css?.buttoncomponent, customCss)
</script>
{#each Object.keys(components['buttoncomponent'].initialData.configuration) as key (key)}
@@ -136,6 +137,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.buttoncomponent}
/>
{/each}
<!-- gotoNewTab={resolvedConfig.onSuccess.selected == 'goto'} -->
<RunnableWrapper
bind:this={runnableWrapper}
@@ -154,16 +165,17 @@
{extraKey}
refreshOnStart={resolvedConfig.triggerOnAppLoad}
>
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment} class="wm-button-wrapper">
{#if errorsMessage}
<div class="text-red-500 text-xs">{errorsMessage}</div>
{/if}
<Button
on:pointerdown={(e) => e.stopPropagation()}
btnClasses={css?.button?.class}
btnClasses={twMerge(css?.button?.class, 'wm-button')}
wrapperClasses={twMerge(
css?.container?.class,
resolvedConfig.fillContainer ? 'w-full h-full' : ''
resolvedConfig.fillContainer ? 'w-full h-full' : '',
'wm-button-container'
)}
wrapperStyle={css?.container?.style}
style={css?.button?.style}
@@ -7,11 +7,13 @@
import { components } from '../../editor/component'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -47,7 +49,7 @@
$stateId != undefined &&
(componentInput?.type != 'runnable' || Object.keys(componentInput?.fields ?? {}).length == 0)
$: css = concatCustomCss($app.css?.formcomponent, customCss)
let css = initCss($app.css?.formcomponent, customCss)
</script>
{#each Object.keys(components['formcomponent'].initialData.configuration) as key (key)}
@@ -59,6 +61,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.formcomponent}
/>
{/each}
<RunnableWrapper
{recomputeIds}
{render}
@@ -78,7 +90,7 @@
>
<AlignWrapper {horizontalAlignment}>
<div
class="flex flex-col gap-2 px-4 w-full {css?.container?.class ?? ''}"
class={twMerge('flex flex-col gap-2 px-4 w-full', css?.container?.class, 'wm-submit')}
style={css?.container?.style ?? ''}
>
<div>
@@ -98,7 +110,7 @@
{#if !noInputs}
<Button
{loading}
btnClasses={css?.button?.class}
btnClasses={twMerge(css?.button?.class, 'wm-submit-button')}
style={css?.button?.style ?? ''}
on:pointerdown={(e) => {
e?.stopPropagation()
@@ -9,11 +9,13 @@
import type RunnableComponent from '../helpers/RunnableComponent.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import { initConfig, initOutput } from '../../editor/appUtils'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import AlwaysMountedModal from '$lib/components/common/modal/AlwaysMountedModal.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -56,12 +58,22 @@
$: noInputs =
componentInput?.type != 'runnable' || Object.keys(componentInput?.fields ?? {}).length == 0
$: css = concatCustomCss($app?.css?.formbuttoncomponent, customCss)
let css = initCss($app?.css?.formbuttoncomponent, customCss)
let runnableWrapper: RunnableWrapper
let loading = false
let modal: AlwaysMountedModal
</script>
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.formbuttoncomponent}
/>
{/each}
{#each Object.keys(components['formbuttoncomponent'].initialData.configuration) as key (key)}
<ResolveConfig
{id}
@@ -70,7 +82,6 @@
configuration={configuration[key]}
/>
{/each}
<AlwaysMountedModal {css} title={resolvedConfig.modalTitle ?? ''} bind:this={modal}>
<div class="flex flex-col gap-2 px-4 w-full pt-2">
<RunnableWrapper
@@ -134,7 +145,7 @@
disabled={resolvedConfig.disabled ?? false}
size={resolvedConfig.size ?? 'md'}
color={resolvedConfig.color}
btnClasses={css?.button?.class ?? ''}
btnClasses={twMerge(css?.button?.class, 'wm-button', 'wm-modal-form-button')}
style={css?.button?.style ?? ''}
on:click={(e) => {
modal?.open()
@@ -12,10 +12,11 @@
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import LightweightSchemaForm from '$lib/components/LightweightSchemaForm.svelte'
import type { Schema } from '$lib/common'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import { twMerge } from 'tailwind-merge'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -64,7 +65,7 @@
$: outputs.valid.set(valid)
$: css = concatCustomCss($app.css?.schemaformcomponent, customCss)
let css = initCss($app.css?.schemaformcomponent, customCss)
const resolvedConfig = initConfig(
components['schemaformcomponent'].initialData.configuration,
@@ -83,10 +84,20 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.schemaformcomponent}
/>
{/each}
<RunnableWrapper {outputs} {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
{#if result && Object.keys(result?.properties ?? {}).length > 0}
<div
class={twMerge('p-2 overflow-auto h-full', css?.container?.class)}
class={twMerge('p-2 overflow-auto h-full', css?.container?.class, 'wm-schema-form')}
style={css?.container?.style}
on:pointerdown|stopPropagation={(e) =>
!$connectingInput.opened && selectId(e, id, selectedComponent, $app)}
@@ -16,10 +16,12 @@
import { initConfig, initOutput } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
export let configuration: RichConfigurations
@@ -105,7 +107,7 @@
}
}
$: css = concatCustomCss($app.css?.barchartcomponent, customCss)
let css = initCss($app.css?.barchartcomponent, customCss)
</script>
{#each Object.keys(components['barchartcomponent'].initialData.configuration) as key (key)}
@@ -117,8 +119,21 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.barchartcomponent}
/>
{/each}
<RunnableWrapper {outputs} {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
<div
class={twMerge('w-full h-full', css?.container?.class, 'wm-bar-chart')}
style={css?.container?.style ?? ''}
>
{#if result}
{#if resolvedConfig.line}
<Line {data} options={lineOptions} />
@@ -3,7 +3,7 @@
import { initConfig, initOutput } from '../../editor/appUtils'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import type { AppInput } from '../../inputType'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
@@ -13,11 +13,13 @@
import Carousel from 'svelte-carousel'
import { ArrowLeftCircle, ArrowRightCircle } from 'lucide-svelte'
import { Button } from '$lib/components/common'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
export let configuration: RichConfigurations
export let customCss: ComponentCustomCSS<'containercomponent'> | undefined = undefined
export let customCss: ComponentCustomCSS<'carousellistcomponent'> | undefined = undefined
export let render: boolean
export let initializing: boolean | undefined
export let componentContainerHeight: number
@@ -43,7 +45,7 @@
}
}
$: css = concatCustomCss($app.css?.containercomponent, customCss)
let css = initCss($app.css?.carousellistcomponent, customCss)
let result: any[] | undefined = undefined
let inputs = {}
@@ -67,6 +69,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.carousellistcomponent}
/>
{/each}
<InitializeComponent {id} />
<RunnableWrapper
@@ -148,7 +160,7 @@
<SubGridEditor
{id}
visible={render}
class={css?.container?.class}
class={twMerge(css?.container?.class, 'wm-carousel')}
style={css?.container?.style}
subGridId={`${id}-0`}
containerHeight={componentContainerHeight - 40}
@@ -4,11 +4,13 @@
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import { getContext } from 'svelte'
import { initConfig, initOutput } from '../../editor/appUtils'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -39,7 +41,7 @@
...(resolvedConfig.options ?? {})
} as ChartOptions
$: css = concatCustomCss($app.css?.piechartcomponent, customCss)
let css = initCss($app.css?.chartjscomponent, customCss)
</script>
{#each Object.keys(components['chartjscomponent'].initialData.configuration) as key (key)}
@@ -51,11 +53,26 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.chartjscomponent}
/>
{/each}
<RunnableWrapper {outputs} {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
<div
class={twMerge('w-full h-full', css?.container?.class, 'wm-chartjs')}
style={css?.container?.style ?? ''}
>
{#if result && resolvedConfig.type}
{#key resolvedConfig.type}
<Chart type={resolvedConfig.type} data={result} {options} />
{#key options}
<Chart type={resolvedConfig.type} data={result} {options} />
{/key}
{/key}
{/if}
</div>
@@ -10,7 +10,8 @@
type ComponentCustomCSS
} from '../../types'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -33,15 +34,26 @@
loading: false
})
$: css = concatCustomCss($app.css?.displaycomponent, customCss)
let css = initCss($app.css?.displaycomponent, customCss)
</script>
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.displaycomponent}
/>
{/each}
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
<div class="flex flex-col w-full h-full">
<div
class={twMerge(
'w-full border-b px-2 text-xs p-1 font-semibold bg-gray-500 text-white rounded-t-sm',
css?.header?.class
css?.header?.class,
'wm-rich-result-header'
)}
style={css?.header?.style}
>
@@ -50,7 +62,8 @@
<div
style={twMerge(
$app.css?.['displaycomponent']?.['container']?.style,
customCss?.container?.style
customCss?.container?.style,
'wm-rich-result-container'
)}
class={twMerge(
'p-2 grow overflow-auto',
@@ -4,13 +4,14 @@
import { initConfig, initOutput } from '../../editor/appUtils'
import { components } from '../../editor/component'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss, transformBareBase64IfNecessary } from '../../utils'
import { initCss, transformBareBase64IfNecessary } from '../../utils'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { AlignWrapper } from '../helpers'
import { Button } from '$lib/components/common'
import { loadIcon } from '../icon'
import ComponentErrorHandler from '../helpers/ComponentErrorHandler.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -48,7 +49,7 @@
}
}
$: css = concatCustomCss($app.css?.downloadcomponent, customCss)
let css = initCss($app.css?.downloadcomponent, customCss)
</script>
<InitializeComponent {id} />
@@ -62,6 +63,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.downloadcomponent}
/>
{/each}
{#if render}
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
<ComponentErrorHandler
@@ -71,9 +82,15 @@
on:pointerdown={(e) => e.stopPropagation()}
btnClasses={twMerge(
css?.button?.class,
'wm-button',
'wm-download-button',
resolvedConfig.fillContainer ? 'w-full h-full' : ''
)}
wrapperClasses={twMerge(
'wm-button-container',
'wm-download-button-container',
resolvedConfig.fillContainer ? 'w-full h-full' : ''
)}
wrapperClasses={resolvedConfig.fillContainer ? 'w-full h-full' : ''}
style={css?.button?.style}
disabled={resolvedConfig.source == undefined}
size={resolvedConfig.size}
@@ -5,8 +5,9 @@
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -23,11 +24,20 @@
initializing = false
$: css = concatCustomCss($app.css?.flowstatuscomponent, customCss)
let css = initCss($app.css?.flowstatuscomponent, customCss)
let jobId: string | undefined
</script>
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.flowstatuscomponent}
/>
{/each}
<RunnableWrapper
on:started={(e) => {
jobId = e.detail
@@ -41,7 +51,8 @@
<div
class={twMerge(
'w-full border-b px-2 text-xs p-1 font-semibold bg-gray-500 text-white rounded-t-sm',
css?.header?.class
css?.header?.class,
'wm-flow-status-header'
)}
style={css?.header?.style}
>
@@ -55,7 +66,8 @@
class={twMerge(
'p-2 grow overflow-auto',
$app.css?.['flowstatuscomponent']?.['container']?.class,
customCss?.container?.class
customCss?.container?.class,
'wm-flow-status-container'
)}
>
{#if jobId}
@@ -3,8 +3,10 @@
import { initOutput } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -23,9 +25,19 @@
let h: number | undefined = undefined
let w: number | undefined = undefined
$: css = concatCustomCss($app.css?.htmlcomponent, customCss)
let css = initCss($app.css?.htmlcomponent, customCss)
</script>
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.htmlcomponent}
/>
{/each}
<div
on:pointerdown={(e) => {
e?.preventDefault()
@@ -47,7 +59,7 @@
<iframe
frameborder="0"
style="height: {h}px; width: {w}px; {css?.container?.style ?? ''}"
class="p-0 {css?.container?.class ?? ''}"
class={twMerge('p-0', css?.container?.class, 'wm-html')}
title="sandbox"
srcdoc={result
? '<base target="_parent" /><scr' +
@@ -2,12 +2,14 @@
import { getContext } from 'svelte'
import { initConfig, initOutput } from '../../editor/appUtils'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import { AlignWrapper } from '../helpers'
import { loadIcon } from '../icon'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = 'left'
@@ -33,7 +35,7 @@
iconComponent = i ? await loadIcon(i) : undefined
}
$: css = concatCustomCss($app.css?.iconcomponent, customCss)
let css = initCss($app.css?.iconcomponent, customCss)
</script>
{#each Object.keys(components['iconcomponent'].initialData.configuration) as key (key)}
@@ -44,13 +46,24 @@
configuration={configuration[key]}
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.iconcomponent}
/>
{/each}
<InitializeComponent {id} />
<AlignWrapper
{render}
{horizontalAlignment}
{verticalAlignment}
class={css?.container?.class ?? ''}
class={twMerge(css?.container?.class, 'wm-icon-container')}
style={css?.container?.style ?? ''}
>
{#if resolvedConfig.icon && iconComponent}
@@ -59,7 +72,7 @@
size={resolvedConfig.size || 24}
color={resolvedConfig.color || 'currentColor'}
strokeWidth={resolvedConfig.strokeWidth || 2}
class={css?.icon?.class ?? ''}
class={twMerge(css?.icon?.class, 'wm-icon')}
style={css?.icon?.style ?? ''}
/>
{/if}
@@ -4,10 +4,11 @@
import { initConfig, initOutput } from '../../editor/appUtils'
import { components } from '../../editor/component'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import Loader from '../helpers/Loader.svelte'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -29,7 +30,7 @@
//used so that we can count number of outputs setup for first refresh
initOutput($worldStore, id, {})
$: css = concatCustomCss($app.css?.imagecomponent, customCss)
let css = initCss($app.css?.imagecomponent, customCss)
</script>
<InitializeComponent {id} />
@@ -43,6 +44,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.imagecomponent}
/>
{/each}
{#if render}
<Loader loading={resolvedConfig.source == undefined}>
<img
@@ -52,7 +63,8 @@
style={css?.image?.style ?? ''}
class={twMerge(
`w-full h-full ${fit[resolvedConfig.imageFit || 'cover']}`,
css?.image?.class ?? ''
css?.image?.class,
'wm-image'
)}
/>
</Loader>
@@ -5,10 +5,11 @@
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -31,7 +32,7 @@
initializing = false
$: css = concatCustomCss($app.css?.jobidflowstatuscomponent, customCss)
let css = initCss($app.css?.jobidflowstatuscomponent, customCss)
$: jobId = resolvedConfig.jobId
</script>
@@ -45,6 +46,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.jobidflowstatuscomponent}
/>
{/each}
<RunnableWrapper {outputs} {render} {componentInput} {id}>
<div class="flex flex-col w-full h-full">
<div
@@ -5,17 +5,18 @@
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import LogViewer from '$lib/components/LogViewer.svelte'
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
import type { Job } from '$lib/gen'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
export let initializing: boolean | undefined = false
export let customCss: ComponentCustomCSS<'logcomponent'> | undefined = undefined
export let customCss: ComponentCustomCSS<'jobidlogcomponent'> | undefined = undefined
export let render: boolean
export let configuration: RichConfigurations
@@ -34,7 +35,7 @@
initializing = false
$: css = concatCustomCss($app.css?.logcomponent, customCss)
let css = initCss($app.css?.jobidlogcomponent, customCss)
let testJobLoader: TestJobLoader | undefined = undefined
let testIsLoading: boolean = false
@@ -54,6 +55,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.jobidlogcomponent}
/>
{/each}
<TestJobLoader bind:this={testJobLoader} bind:isLoading={testIsLoading} bind:job={testJob} />
<RunnableWrapper {outputs} {render} {componentInput} {id}>
@@ -68,12 +79,8 @@
Logs
</div>
<div
style={twMerge($app.css?.['logcomponent']?.['container']?.style, customCss?.container?.style)}
class={twMerge(
'p-2 grow overflow-auto',
$app.css?.['logcomponent']?.['container']?.class,
customCss?.container?.class
)}
style={css?.container?.style}
class={twMerge('p-2 grow overflow-auto', css?.container?.class, 'wm-log-container')}
>
<LogViewer
jobId={testJob?.id}
@@ -5,10 +5,11 @@
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import LogViewer from '$lib/components/LogViewer.svelte'
import TestJobLoader from '$lib/components/TestJobLoader.svelte'
import type { Job } from '$lib/gen'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -25,8 +26,7 @@
initializing = false
$: css = concatCustomCss($app.css?.logcomponent, customCss)
let css = initCss($app.css?.logcomponent, customCss)
let testJobLoader: TestJobLoader | undefined = undefined
let testIsLoading: boolean = false
let testJob: Job | undefined = undefined
@@ -34,6 +34,16 @@
<TestJobLoader bind:this={testJobLoader} bind:isLoading={testIsLoading} bind:job={testJob} />
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.logcomponent}
/>
{/each}
<RunnableWrapper
on:started={(e) => {
testJobLoader?.watchJob(e.detail)
@@ -47,19 +57,16 @@
<div
class={twMerge(
'w-full border-b px-2 text-xs p-1 font-semibold bg-gray-500 text-white rounded-t-sm',
css?.header?.class
css?.header?.class,
'wm-log-header'
)}
style={css?.header?.style}
>
Logs
</div>
<div
style={twMerge($app.css?.['logcomponent']?.['container']?.style, customCss?.container?.style)}
class={twMerge(
'p-2 grow overflow-auto',
$app.css?.['logcomponent']?.['container']?.class,
customCss?.container?.class
)}
style={css?.container?.style}
class={twMerge('p-2 grow overflow-auto', css?.container?.class, 'wm-log-container')}
>
<LogViewer
jobId={testJob?.id}
@@ -1,6 +1,6 @@
<script lang="ts">
import { getContext } from 'svelte'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { InputValue } from '../helpers'
import { twMerge } from 'tailwind-merge'
@@ -12,6 +12,7 @@
import { defaults as defaultControls } from 'ol/control'
import { findGridItem, initOutput } from '../../editor/appUtils'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
interface Marker {
lon: number
@@ -152,7 +153,7 @@
updateRegionOutput()
}
$: css = concatCustomCss($app.css?.mapcomponent, customCss)
let css = initCss($app.css?.mapcomponent, customCss)
function updateRegionOutput() {
if (map) {
@@ -203,12 +204,22 @@
<InitializeComponent {id} />
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.mapcomponent}
/>
{/each}
{#if render}
<div class="relative h-full w-full">
<div
on:pointerdown|stopPropagation={selectComponent}
bind:this={mapElement}
class={twMerge(`w-full h-full`, css?.map?.class ?? '')}
class={twMerge(`w-full h-full`, css?.map?.class, 'wm-map')}
style={css?.map?.style ?? ''}
/>
@@ -3,12 +3,13 @@
import { initConfig, initOutput } from '../../editor/appUtils'
import type { AppInput } from '../../inputType'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import Markdown from 'svelte-exmarkdown'
import { classNames } from '$lib/utils'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -31,7 +32,7 @@
let result: string | undefined = undefined
$: css = concatCustomCss($app.css?.mardowncomponent, customCss)
let css = initCss($app.css?.mardowncomponent, customCss)
const proseMapping = {
sm: 'prose-sm',
@@ -51,6 +52,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.mardowncomponent}
/>
{/each}
<div
on:pointerdown={(e) => {
e?.preventDefault()
@@ -58,8 +69,10 @@
class={classNames(
'h-full w-full overflow-y-auto prose',
resolvedConfig?.size ? proseMapping[resolvedConfig.size] : '',
css?.container?.class
css?.container?.class,
'wm-markdown'
)}
style={css?.container?.style}
>
<RunnableWrapper
{outputs}
@@ -9,9 +9,10 @@
import { Button } from '../../../common'
import { findGridItem, initOutput } from '../../editor/appUtils'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -187,12 +188,22 @@
return value
}
$: css = concatCustomCss($app.css?.pdfcomponent, customCss)
let css = initCss($app.css?.pdfcomponent, customCss)
</script>
<InputValue key="source" {id} input={configuration.source} bind:value={source} />
<InputValue key="zoom" {id} input={configuration.zoom} bind:value={zoom} />
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.pdfcomponent}
/>
{/each}
<InitializeComponent {id} />
{#if render}
@@ -306,7 +317,12 @@
<div
bind:this={wrapper}
on:scroll={throttledScroll}
class={twMerge('w-full h-full overflow-auto', css?.container?.class ?? '', 'bg-gray-100')}
class={twMerge(
'w-full h-full overflow-auto',
css?.container?.class ?? '',
'bg-gray-100',
'wm-pdf'
)}
style={css?.container?.style ?? ''}
/>
{/if}
@@ -15,9 +15,11 @@
import type { AppInput } from '../../inputType'
import InputValue from '../helpers/InputValue.svelte'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import { getContext } from 'svelte'
import { initOutput } from '../../editor/appUtils'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -72,14 +74,27 @@
]
}
$: css = concatCustomCss($app.css?.piechartcomponent, customCss)
let css = initCss($app.css?.piechartcomponent, customCss)
</script>
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.piechartcomponent}
/>
{/each}
<InputValue key="theme" {id} input={configuration.theme} bind:value={theme} />
<InputValue key="doughnut" {id} input={configuration.doughnutStyle} bind:value={doughnut} />
<RunnableWrapper {outputs} {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
<div
class={twMerge('w-full h-full', css?.container?.class, 'wm-pie-chart')}
style={css?.container?.style ?? ''}
>
{#if result}
{#if doughnut}
<Doughnut {data} {options} />
@@ -17,10 +17,12 @@
import Scatter from 'svelte-chartjs/Scatter.svelte'
import InputValue from '../helpers/InputValue.svelte'
import type { ChartOptions, ChartData } from 'chart.js'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import { getContext } from 'svelte'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { initOutput } from '../../editor/appUtils'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -78,14 +80,27 @@
datasets: result ?? []
} as ChartData<'scatter', (number | Point)[], unknown>
$: css = concatCustomCss($app.css?.scatterchartcomponent, customCss)
let css = initCss($app.css?.scatterchartcomponent, customCss)
</script>
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.scatterchartcomponent}
/>
{/each}
<InputValue key="zoomable" {id} input={configuration.zoomable} bind:value={zoomable} />
<InputValue key="pannable" {id} input={configuration.pannable} bind:value={pannable} />
<RunnableWrapper {outputs} {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
<div
class={twMerge('w-full h-full', css?.container?.class, 'wm-scatter-chart')}
style={css?.container?.style ?? ''}
>
{#if result}
<Scatter {data} {options} />
{/if}
@@ -12,6 +12,8 @@
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { initCss } from '../../utils'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -31,6 +33,8 @@
const { app, worldStore, mode, componentControl } =
getContext<AppViewerContext>('AppViewerContext')
let css = initCss($app.css?.textcomponent, customCss)
let result: string | undefined = undefined
if (
@@ -155,9 +159,24 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.textcomponent}
/>
{/each}
<RunnableWrapper {outputs} {render} {componentInput} {id} bind:initializing bind:result>
<div
class="h-full w-full overflow-hidden"
class={twMerge(
'h-full w-full overflow-hidden',
customCss?.container?.class,
'wm-text-container'
)}
style={customCss?.container?.style}
on:dblclick={() => {
if (!editorMode) {
editorMode = true
@@ -172,15 +191,16 @@
<textarea
class={twMerge(
'whitespace-pre-wrap !outline-none !border-0 !bg-transparent !resize-none !overflow-hidden !ring-0 !p-0 text-center',
$app.css?.['textcomponent']?.['text']?.class,
css?.text?.class,
customCss?.text?.class,
'wm-text',
classes,
getClasses(),
getClassesByType(),
getHorizontalAlignement()
)}
on:pointerdown|stopPropagation
style={[$app.css?.['textcomponent']?.['text']?.style, customCss?.text?.style].join(';')}
style={customCss?.text?.style}
id={`text-${id}`}
on:pointerenter={() => {
const elem = document.getElementById(`text-${id}`)
@@ -22,8 +22,10 @@
import type { ChartOptions, ChartData } from 'chart.js'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { getContext } from 'svelte'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import { initOutput } from '../../editor/appUtils'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -92,9 +94,19 @@
datasets: result ?? []
} as ChartData<'scatter', (number | Point)[], unknown>
$: css = concatCustomCss($app.css?.timeseriescomponent, customCss)
let css = initCss($app.css?.timeseriescomponent, customCss)
</script>
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.timeseriescomponent}
/>
{/each}
<InputValue
key="logarithmicScale"
{id}
@@ -105,7 +117,10 @@
<InputValue key="pannable" {id} input={configuration.pannable} bind:value={pannable} />
<RunnableWrapper {outputs} {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div class="w-full h-full {css?.container?.class ?? ''}" style={css?.container?.style ?? ''}>
<div
class={twMerge('w-full h-full', css?.container?.class, 'wm-timeseries')}
style={css?.container?.style ?? ''}
>
{#if result}
<Scatter {data} {options} />
{/if}
@@ -26,13 +26,14 @@
import { tableOptions } from './tableOptions'
import Alert from '$lib/components/common/alert/Alert.svelte'
import { components, type ButtonComponent } from '../../../editor/component'
import { concatCustomCss } from '../../../utils'
import { initCss } from '../../../utils'
import { twMerge } from 'tailwind-merge'
import { initConfig, initOutput } from '$lib/components/apps/editor/appUtils'
import ResolveConfig from '../../helpers/ResolveConfig.svelte'
import AppCheckbox from '../../inputs/AppCheckbox.svelte'
import AppSelect from '../../inputs/AppSelect.svelte'
import RowWrapper from '../../layout/RowWrapper.svelte'
import ResolveStyle from '../../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -206,7 +207,7 @@
$: filteredResult != undefined && rerender()
$: css = concatCustomCss($app.css?.tablecomponent, customCss)
let css = initCss($app.css?.tablecomponent, customCss)
$componentControl[id] = {
right: (skipActions: boolean | undefined) => {
@@ -259,6 +260,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.tablecomponent}
/>
{/each}
<RunnableWrapper
{outputs}
{render}
@@ -273,6 +284,7 @@
class={twMerge(
'border shadow-sm divide-y h-full',
css?.container?.class ?? '',
'wm-table-container',
'flex flex-col'
)}
style={css?.container?.style ?? ''}
@@ -293,6 +305,7 @@
class={twMerge(
'bg-surface-secondary text-left',
css?.tableHeader?.class ?? '',
'wm-table-header',
'sticky top-0 z-40'
)}
style={css?.tableHeader?.style ?? ''}
@@ -323,21 +336,17 @@
{/each}
</thead>
<tbody
class={twMerge('divide-y bg-surface', css?.tableBody?.class ?? '')}
class={twMerge('divide-y bg-surface', css?.tableBody?.class ?? '', 'wm-table-body')}
style={css?.tableBody?.style ?? ''}
>
{#each $table.getRowModel().rows as row (row.id)}
{@const rowIndex = row.original['__index']}
<tr
class={classNames(
'last-of-type:!border-b-0',
'last-of-type:!border-b-0 divide-x w-full',
selectedRowIndex === rowIndex
? 'bg-blue-100 hover:bg-blue-200 dark:bg-surface-selected dark:hover:bg-surface-hover'
: 'hover:bg-blue-50 dark:hover:bg-surface-hover',
'divide-x w-full',
selectedRowIndex === rowIndex
? 'divide-blue-200 hover:divide-blue-300 dark:divide-gray-600 dark:hover:divide-gray-700'
: ''
? 'bg-blue-100 hover:bg-blue-200 dark:bg-surface-selected dark:hover:bg-surface-hover divide-blue-200 hover:divide-blue-300 dark:divide-gray-600 dark:hover:divide-gray-700 wm-table-row-selected'
: 'hover:bg-blue-50 dark:hover:bg-surface-hover wm-table-row'
)}
>
{#each safeVisibleCell(row) as cell, index (index)}
@@ -544,7 +553,7 @@
manualPagination={resolvedConfig?.pagination?.selected == 'manual'}
result={filteredResult}
{table}
class={css?.tableFooter?.class}
class={twMerge(css?.tableFooter?.class, 'wm-table-footer')}
style={css?.tableFooter?.style}
{loading}
/>
@@ -553,9 +562,9 @@
<div class="flex flex-col h-full w-full overflow-auto">
<Alert title="Parsing issues" type="error" size="xs" class="h-full w-full ">
The result should be an array of objects. Received:
<pre class="w-full bg-surface p-2 rounded-md whitespace-pre-wrap"
>{JSON.stringify(result, null, 4)}</pre
>
<pre class="w-full bg-surface p-2 rounded-md whitespace-pre-wrap">
{JSON.stringify(result, null, 4)}
</pre>
</Alert>
</div>
{/if}
@@ -0,0 +1,40 @@
<script lang="ts">
import type { ComponentCssProperty } from '../../types'
import InputValue from './InputValue.svelte'
export let css: ComponentCssProperty
export let id: string
export let key: string
export let customCss: Record<string, ComponentCssProperty> | undefined = undefined
export let componentStyle: Record<string, ComponentCssProperty> | undefined = undefined
let evalClassValue: string | undefined = undefined
function updateCss(
componentStyle: Record<string, ComponentCssProperty> | undefined,
customCss: Record<string, ComponentCssProperty> | undefined,
evalClassValue: string | undefined
) {
const { class: componentClass, style: componentStyleValue } = componentStyle?.[key] ?? {}
const { class: customClass, style: customStyleValue } = customCss?.[key] ?? {}
css.class = [componentClass, customClass, evalClassValue].filter(Boolean).join(' ')
css.style = [componentStyleValue, customStyleValue].filter(Boolean).join(';')
}
// When any of the values change, update the css
$: updateCss(componentStyle, customCss, evalClassValue)
// We need to clear the evalClassValue if the user has disabled the evalClass
$: if (customCss?.[key].evalClass === undefined && evalClassValue !== undefined) {
evalClassValue = undefined
}
</script>
{#if customCss}
{@const property = customCss[key]}
{#if property.evalClass}
<InputValue {id} bind:value={evalClassValue} input={property.evalClass} />
{/if}
{/if}
@@ -9,11 +9,13 @@
ListInputs,
RichConfigurations
} from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -81,7 +83,7 @@
$: resolvedConfig.defaultValue != undefined && handleDefault()
$: css = concatCustomCss($app.css?.checkboxcomponent, customCss)
let css = initCss($app.css?.checkboxcomponent, customCss)
</script>
{#each Object.keys(components['checkboxcomponent'].initialData.configuration) as key (key)}
@@ -94,13 +96,29 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.checkboxcomponent}
/>
{/each}
<InitializeComponent {id} />
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
<AlignWrapper
{render}
{horizontalAlignment}
{verticalAlignment}
class={twMerge(css?.container?.class, 'wm-toggle-container')}
style={css?.container?.style}
>
<Toggle
size="sm"
bind:checked={value}
options={{ right: resolvedConfig.label }}
textClass={css?.text?.class ?? ''}
textClass={twMerge(css?.text?.class, 'wm-toggle-text')}
textStyle={css?.text?.style ?? ''}
on:change={(e) => {
preclickAction?.()
@@ -3,12 +3,13 @@
import { twMerge } from 'tailwind-merge'
import { initConfig, initOutput } from '../../editor/appUtils'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { parseISO, format as formatDateFns } from 'date-fns'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -57,7 +58,7 @@
function handleDefault(defaultValue: string | undefined) {
value = defaultValue
}
$: css = concatCustomCss($app.css?.dateinputcomponent, customCss)
let css = initCss($app.css?.dateinputcomponent, customCss)
</script>
{#each Object.keys(components['dateinputcomponent'].initialData.configuration) as key (key)}
@@ -69,6 +70,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.dateinputcomponent}
/>
{/each}
<InitializeComponent {id} />
<AlignWrapper {render} {verticalAlignment}>
@@ -81,7 +92,7 @@
min={resolvedConfig.minDate}
max={resolvedConfig.maxDate}
placeholder="Type..."
class={twMerge(css?.input?.class ?? '')}
class={twMerge(css?.input?.class, 'wm-date-input')}
style={css?.input?.style ?? ''}
/>
{/if}
@@ -4,9 +4,10 @@
import { FileInput } from '../../../common'
import { initOutput } from '../../editor/appUtils'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -36,9 +37,19 @@
outputs?.result.set(files)
}
$: css = concatCustomCss($app.css?.fileinputcomponent, customCss)
let css = initCss($app.css?.fileinputcomponent, customCss)
</script>
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.fileinputcomponent}
/>
{/each}
<InputValue
key="accepted"
{id}
@@ -61,7 +72,7 @@
on:change={({ detail }) => {
handleChange(detail)
}}
class={twMerge('w-full h-full', css?.container?.class)}
class={twMerge('w-full h-full', css?.container?.class, 'wm-file-input')}
style={css?.container?.style}
>
{text}
@@ -2,7 +2,7 @@
import { getContext } from 'svelte'
import { initConfig, initOutput } from '../../editor/appUtils'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { components } from '../../editor/component'
@@ -14,6 +14,7 @@
import { extractCustomProperties } from '$lib/utils'
import { tick } from 'svelte'
import { offset, flip, shift } from 'svelte-floating-ui/dom'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -69,7 +70,7 @@
}
}
$: css = concatCustomCss($app.css?.multiselectcomponent, customCss)
let css = initCss($app.css?.multiselectcomponent, customCss)
function setOuterDivStyle(outerDiv: HTMLDivElement, portalRef: HTMLDivElement, style: string) {
outerDiv.setAttribute('style', style)
@@ -116,6 +117,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.multiselectcomponent}
/>
{/each}
<InitializeComponent {id} />
<AlignWrapper {render} hFull>
@@ -9,11 +9,12 @@
ListInputs,
RichConfigurations
} from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import { components } from '../../editor/component'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -56,7 +57,7 @@
value = defaultValue
}
$: css = concatCustomCss($app.css?.numberinputcomponent, customCss)
let css = initCss($app.css?.numberinputcomponent, customCss)
</script>
{#each Object.keys(components['numberinputcomponent'].initialData.configuration) as key (key)}
@@ -68,6 +69,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.numberinputcomponent}
/>
{/each}
<InitializeComponent {id} />
<AlignWrapper {render} {verticalAlignment}>
@@ -76,7 +87,8 @@
on:focus={() => ($selectedComponent = [id])}
class={twMerge(
'windmillapp w-full py-1.5 text-sm focus:ring-indigo-100 px-2',
css?.input?.class ?? ''
css?.input?.class ?? '',
'wm-number-input'
)}
style={css?.input?.style ?? ''}
bind:value
@@ -10,11 +10,12 @@
ListInputs,
RichConfigurations
} from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import { components } from '../../editor/component'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -61,7 +62,7 @@
}
}
$: css = concatCustomCss($app.css?.rangecomponent, customCss)
let css = initCss($app.css?.rangecomponent, customCss)
let lastStyle: string | undefined = undefined
$: if (css && slider && lastStyle !== css?.handles?.style) {
@@ -82,16 +83,29 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.rangecomponent}
/>
{/each}
<InitializeComponent {id} />
<AlignWrapper {render} {verticalAlignment}>
<div class="flex flex-col w-full">
<div class="flex items-center w-full gap-1 px-1">
<span class={css?.limits?.class ?? ''} style={css?.limits?.style ?? ''}>
<span
class={twMerge(css?.limits?.class ?? '', 'wm-slider-limits')}
style={css?.limits?.style ?? ''}
>
{+(resolvedConfig.min ?? 0)}
</span>
<div
class="grow"
class={twMerge('grow', 'wm-slider-bar')}
style="--range-handle-focus: {'#7e9abd'}; --range-handle: {'#7e9abd'}; {css?.bar?.style ??
''}"
on:pointerdown|stopPropagation
@@ -107,7 +121,10 @@
/>
<!-- <RangeSlider {step} range min={min ?? 0} max={max ?? 1} bind:values /> -->
</div>
<span class={css?.limits?.class ?? ''} style={css?.limits?.style ?? ''}>
<span
class={twMerge(css?.limits?.class ?? '', 'wm-slider-limits')}
style={css?.limits?.style ?? ''}
>
{+(resolvedConfig.max ?? 1)}
</span>
</div>
@@ -115,7 +132,8 @@
<span
class={twMerge(
'text-center text-sm font-medium bg-blue-100 text-blue-800 rounded px-2.5 py-0.5',
css?.values?.class ?? ''
css?.values?.class ?? '',
'wm-slider-value'
)}
style={css?.values?.style ?? ''}
>
@@ -124,7 +142,8 @@
<span
class={twMerge(
'text-center text-sm font-medium bg-blue-100 text-blue-800 rounded px-2.5 py-0.5',
css?.values?.class ?? ''
css?.values?.class ?? '',
'wm-slider-value'
)}
style={css?.values?.style ?? ''}
>
@@ -9,7 +9,7 @@
ListInputs,
RichConfigurations
} from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import { SELECT_INPUT_DEFAULT_STYLE } from '../../../../defaults'
import { initConfig, initOutput } from '../../editor/appUtils'
@@ -20,6 +20,7 @@
import { classNames } from '$lib/utils'
import { Bug } from 'lucide-svelte'
import Popover from '$lib/components/Popover.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -138,7 +139,7 @@
}
}
$: css = concatCustomCss($app.css?.selectcomponent, customCss)
let css = initCss($app.css?.selectcomponent, customCss)
function handleFilter(e) {
if (resolvedConfig.create) {
@@ -172,6 +173,17 @@
configuration={configuration[key]}
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.selectcomponent}
/>
{/each}
<InitializeComponent {id} />
<AlignWrapper {render} {verticalAlignment}>
@@ -1,21 +1,25 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppViewerContext, RichConfigurations } from '../../types'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import { initConfig, initOutput } from '../../editor/appUtils'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import Stepper from '$lib/components/common/stepper/Stepper.svelte'
import { twMerge } from 'tailwind-merge'
import { initCss } from '../../utils'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
export let horizontalAlignment: 'left' | 'center' | 'right' | undefined = undefined
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
export let render: boolean
export let customCss: ComponentCustomCSS<'selectstepcomponent'> | undefined = undefined
const { worldStore, componentControl } = getContext<AppViewerContext>('AppViewerContext')
const { app, worldStore, componentControl } = getContext<AppViewerContext>('AppViewerContext')
const resolvedConfig = initConfig(
components['selectstepcomponent'].initialData.configuration,
@@ -68,6 +72,7 @@
}
}
let css = initCss($app.css?.selectstepcomponent, customCss)
$: selected && handleSelection(selected)
</script>
@@ -80,9 +85,25 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.selectstepcomponent}
/>
{/each}
<InitializeComponent {id} />
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
<AlignWrapper
{render}
{horizontalAlignment}
{verticalAlignment}
class={twMerge(css?.container?.class, 'wm-select-step')}
style={css?.container?.style}
>
<div class="w-full" on:pointerdown={onPointerDown}>
<Stepper
tabs={(resolvedConfig?.items ?? []).map((item) => item.label)}
@@ -2,13 +2,15 @@
import { getContext } from 'svelte'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import { initConfig, initOutput } from '../../editor/appUtils'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { Tab, Tabs } from '$lib/components/common'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -52,7 +54,7 @@
}
$: selected && handleSelection(selected)
$: css = concatCustomCss($app.css?.selecttabcomponent, customCss)
let css = initCss($app.css?.selecttabcomponent, customCss)
</script>
{#each Object.keys(components['selecttabcomponent'].initialData.configuration) as key (key)}
@@ -64,17 +66,31 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.selecttabcomponent}
/>
{/each}
<InitializeComponent {id} />
<AlignWrapper {render} {horizontalAlignment} {verticalAlignment}>
<div class="w-full">
<Tabs bind:selected class={css?.tabRow?.class} style={css?.tabRow?.style}>
<Tabs
bind:selected
class={twMerge(css?.tabRow?.class, 'wm-select-tab-row')}
style={css?.tabRow?.style}
>
{#each resolvedConfig?.items ?? [] as item}
<Tab
value={item.value}
class={css?.allTabs?.class}
class={twMerge(css?.allTabs?.class, 'wm-select-tab')}
style={css?.allTabs?.style}
selectedClass={css?.selectedTab?.class}
selectedClass={twMerge(css?.selectedTab?.class, 'wm-select-tab-selected')}
selectedStyle={css?.selectedTab?.style}
size={resolvedConfig?.tabSize}
>
@@ -10,11 +10,12 @@
} from '../../types'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import { twMerge } from 'tailwind-merge'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import { initConfig, initOutput } from '../../editor/appUtils'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -67,7 +68,7 @@
}
}
$: css = concatCustomCss($app.css?.slidercomponent, customCss)
let css = initCss($app.css?.slidercomponent, customCss)
let lastStyle: string | undefined = undefined
$: if (css && slider && lastStyle !== css?.handle?.style) {
@@ -114,17 +115,26 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.slidercomponent}
/>
{/each}
<InitializeComponent {id} />
<AlignWrapper {render} hFull {verticalAlignment}>
<div class="flex {vertical ? 'flex-col' : ''} items-center w-full h-full gap-1 px-1">
<span class={css?.limits?.class ?? ''} style={css?.limits?.style ?? ''}>
<span class={twMerge(css?.limits?.class, 'wm-slider-limits')} style={css?.limits?.style ?? ''}>
{vertical ? +(resolvedConfig?.max ?? 0) : +(resolvedConfig?.min ?? 0)}
</span>
<div
class="grow"
style="--range-handle-focus: {'#7e9abd'}; --range-handle: {'#7e9abd'}; {css?.bar?.style ??
''}"
class={twMerge('grow', css?.bar?.class, 'wm-slider-bar')}
style={css?.bar?.style}
on:pointerdown|stopPropagation={() => ($selectedComponent = [id])}
>
<RangeSlider
@@ -136,12 +146,12 @@
max={+(resolvedConfig?.max ?? 0)}
/>
</div>
<span class={css?.limits?.class ?? ''} style={css?.limits?.style ?? ''}>
<span class={twMerge(css?.limits?.class, 'wm-slider-limits')} style={css?.limits?.style ?? ''}>
{vertical ? +(resolvedConfig?.min ?? 0) : +(resolvedConfig?.max ?? 1)}
</span>
<span class="mx-2">
<span
class={twMerge(spanClass, css?.value?.class ?? '')}
class={twMerge(spanClass, css?.value?.class ?? '', 'wm-slider-value')}
style={`${css?.value?.style ?? ''} ${width ? `width: ${width}px;` : ''}`}
>
{values[0]}
@@ -9,11 +9,12 @@
ListInputs,
RichConfigurations
} from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -64,7 +65,7 @@
value = defaultValue
}
$: css = concatCustomCss($app.css?.[appCssKey], customCss)
let css = initCss($app.css?.[appCssKey], customCss)
</script>
{#each Object.keys(components['textinputcomponent'].initialData.configuration) as key (key)}
@@ -75,13 +76,25 @@
configuration={configuration[key]}
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.textinputcomponent}
/>
{/each}
<InitializeComponent {id} />
{#if render}
{#if inputType === 'textarea'}
<textarea
class={twMerge(
'windmillapp w-full h-full py-1.5 text-sm focus:ring-indigo-100 px-2 ',
css?.input?.class ?? ''
css?.input?.class ?? '',
'wm-text-input'
)}
style="resize:none; {css?.input?.style ?? ''}"
on:pointerdown|stopPropagation={(e) =>
@@ -96,7 +109,8 @@
<input
class={twMerge(
'windmillapp w-full py-1.5 text-sm focus:ring-indigo-100 px-2 ',
css?.input?.class ?? ''
css?.input?.class ?? '',
'wm-text-input'
)}
style={css?.input?.style ?? ''}
on:pointerdown|stopPropagation={(e) =>
@@ -110,7 +124,8 @@
<input
class={twMerge(
'windmillapp w-full py-1.5 text-sm focus:ring-indigo-100 px-2 ',
css?.input?.class ?? ''
css?.input?.class ?? '',
'wm-text-input'
)}
style={css?.input?.style ?? ''}
on:pointerdown|stopPropagation={(e) =>
@@ -124,7 +139,8 @@
<input
class={twMerge(
'windmillapp w-full py-1.5 text-sm focus:ring-indigo-100 px-2 ',
css?.input?.class ?? ''
css?.input?.class ?? '',
'wm-text-input'
)}
style={css?.input?.style ?? ''}
on:pointerdown|stopPropagation={(e) =>
@@ -9,12 +9,13 @@
ListInputs,
RichConfigurations
} from '../../../types'
import { concatCustomCss } from '../../../utils'
import { initCss } from '../../../utils'
import AlignWrapper from '../../helpers/AlignWrapper.svelte'
import CurrencyInput from './CurrencyInput.svelte'
import InitializeComponent from '../../helpers/InitializeComponent.svelte'
import ResolveConfig from '../../helpers/ResolveConfig.svelte'
import { components } from '$lib/components/apps/editor/component'
import ResolveStyle from '../../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -60,7 +61,7 @@
$: handleDefault(resolvedConfig.defaultValue)
$: css = concatCustomCss($app.css?.currencycomponent, customCss)
let css = initCss($app.css?.currencycomponent, customCss)
</script>
{#each Object.keys(components['currencycomponent'].initialData.configuration) as key (key)}
@@ -72,6 +73,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.currencycomponent}
/>
{/each}
<InitializeComponent {id} />
<AlignWrapper {render} {verticalAlignment}>
@@ -81,9 +92,13 @@
<div class="w-full" on:pointerdown|stopPropagation={() => ($selectedComponent = [id])}>
<CurrencyInput
inputClasses={{
formatted: twMerge('px-2 w-full py-1.5 windmillapp', css?.input?.class),
formatted: twMerge(
'px-2 w-full py-1.5 windmillapp',
css?.input?.class,
'wm-currency-input'
),
wrapper: 'w-full windmillapp',
formattedZero: twMerge('text-black windmillapp ', css?.input?.class)
formattedZero: twMerge('text-black windmillapp ', css?.input?.class, 'wm-currency')
}}
style={css?.input?.style}
bind:value
@@ -3,9 +3,11 @@
import { initOutput } from '../../editor/appUtils'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppViewerContext, ComponentCustomCSS, RichConfiguration } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { InputValue } from '../helpers'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentContainerHeight: number
@@ -28,7 +30,7 @@
}
}
$: css = concatCustomCss($app.css?.containercomponent, customCss)
let css = initCss($app.css?.containercomponent, customCss)
let resolvedConditions: boolean[] = []
let selectedConditionIndex = 0
@@ -69,6 +71,16 @@
<InputValue key="conditions" {id} input={condition} bind:value={resolvedConditions[index]} />
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.conditionalwrapper}
/>
{/each}
<InitializeComponent {id} />
<div class="w-full h-full">
@@ -77,7 +89,7 @@
<SubGridEditor
visible={render && i == selectedConditionIndex}
{id}
class={css?.container?.class}
class={twMerge(css?.container?.class, 'wm-conditional-tabs')}
style={css?.container?.style}
subGridId={`${id}-${i}`}
containerHeight={componentContainerHeight}
@@ -3,8 +3,10 @@
import { initOutput } from '../../editor/appUtils'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
// import type { EvalV2AppInput, StaticAppInput } from '../../inputType'
import { writable } from 'svelte/store'
import { InputValue } from '../helpers'
@@ -32,11 +34,21 @@
}
}
$: css = concatCustomCss($app.css?.containercomponent, customCss)
let css = initCss($app.css?.containercomponent, customCss)
</script>
<InitializeComponent {id} />
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.containercomponent}
/>
{/each}
{#each Object.keys(groupFields ?? {}) as field}
{#if groupFields && field in groupFields}
<InputValue key={field} {id} input={groupFields[field]} bind:value={$groupContext[field]} />
@@ -49,7 +61,7 @@
<SubGridEditor
visible={render}
{id}
class={css?.container?.class}
class={twMerge(css?.container?.class, 'wm-container')}
style={css?.container?.style}
subGridId={`${id}-0`}
containerHeight={componentContainerHeight}
@@ -9,10 +9,11 @@
RichConfigurations,
VerticalAlignment
} from '../../types'
import { TailwindClassPatterns, concatCustomCss, hasTailwindClass } from '../../utils'
import { TailwindClassPatterns, initCss, hasTailwindClass } from '../../utils'
import AlignWrapper from '../helpers/AlignWrapper.svelte'
import InputValue from '../helpers/InputValue.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -28,7 +29,7 @@
let size = 2
let color = '#00000060'
$: css = concatCustomCss($app.css?.[position + 'dividercomponent'], customCss)
let css = initCss($app.css?.[position + 'dividercomponent'], customCss)
//used so that we can count number of outputs setup for first refresh
initOutput($worldStore, id, {})
@@ -50,17 +51,32 @@
<InputValue key="color" {id} input={configuration.color} bind:value={color} />
<InitializeComponent {id} />
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.[position + 'dividercomponent']}
/>
{/each}
<AlignWrapper
{horizontalAlignment}
{verticalAlignment}
class={twMerge(css?.container?.class, 'h-full')}
class={twMerge(
css?.container?.class,
position === 'horizontal' ? 'wm-horizontal-divider-container' : 'wm-vertical-divider-container',
'h-full'
)}
style={css?.container?.style}
{render}
>
<div
class={twMerge(
`rounded-full ${position === 'horizontal' ? 'w-full' : 'h-full'}`,
css?.divider?.class ?? ''
css?.divider?.class,
position === 'horizontal' ? 'wm-horizontal-divider' : 'wm-vertical-divider'
)}
style="
{getSize()}
@@ -3,7 +3,7 @@
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import Portal from 'svelte-portal'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import { Button, Drawer, DrawerContent } from '$lib/components/common'
import { twMerge } from 'tailwind-merge'
import { AlignWrapper } from '../helpers'
@@ -11,6 +11,7 @@
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let customCss: ComponentCustomCSS<'drawercomponent'> | undefined = undefined
export let id: string
@@ -31,7 +32,7 @@
let appDrawer: Drawer
$: css = concatCustomCss($app.css?.drawercomponent, customCss)
let css = initCss($app.css?.drawercomponent, customCss)
</script>
{#each Object.keys(components['drawercomponent'].initialData.configuration) as key (key)}
@@ -43,14 +44,25 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.drawercomponent}
/>
{/each}
<InitializeComponent {id} />
<div class="h-full w-full">
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
<Button
btnClasses={css?.button?.class}
btnClasses={twMerge(css?.button?.class, 'wm-drawer-button')}
wrapperClasses={twMerge(
css?.container?.class,
'wm-drawer-button-container',
resolvedConfig?.fillContainer ? 'w-full h-full' : ''
)}
wrapperStyle={css?.container?.style}
@@ -93,7 +105,8 @@
fullScreen={$mode !== 'dnd'}
>
<div
class="h-full"
class={twMerge('h-full', css?.drawer?.class, 'wm-drawer')}
style={css?.drawer?.style}
on:pointerdown={(e) => {
e?.stopPropagation()
if (!$connectingInput.opened) {
@@ -3,7 +3,7 @@
import { initConfig, initOutput } from '../../editor/appUtils'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import ListWrapper from './ListWrapper.svelte'
import type { AppInput } from '../../inputType'
@@ -12,6 +12,8 @@
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import { Button } from '$lib/components/common'
import { Loader2, ChevronLeft, ChevronRight } from 'lucide-svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
@@ -43,7 +45,7 @@
}
}
$: css = concatCustomCss($app.css?.containercomponent, customCss)
let css = initCss($app.css?.containercomponent, customCss)
let result: any[] | undefined = undefined
$: isCard = resolvedConfig.width?.selected == 'card'
@@ -111,6 +113,16 @@
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.listcomponent}
/>
{/each}
<InitializeComponent {id} />
<RunnableWrapper
@@ -124,7 +136,10 @@
bind:result
bind:loading
>
<div class="flex flex-col divide-y h-full">
<div
class={twMerge('flex flex-col divide-y h-full', css?.container?.class, 'wm-list')}
style={css?.container?.style}
>
<div
class="w-full flex flex-wrap overflow-auto {isCard ? 'h-full gap-2' : 'divide-y max-h-full'}"
>
@@ -150,8 +165,6 @@
<SubGridEditor
visible={render}
{id}
class={css?.container?.class}
style={css?.container?.style}
subGridId={`${id}-0`}
containerHeight={resolvedConfig.heightPx}
on:focus={() => {
@@ -175,11 +188,12 @@
{/if}
</div>
{#if pagination.shouldDisplayPagination}
<div class="bg-surface-secondary h-8 flex flex-row gap-1 p-1 items-center">
<div class="bg-surface-secondary h-8 flex flex-row gap-1 p-1 items-center wm-list-pagination">
<Button
size="xs2"
variant="border"
color="light"
btnClasses="flex flex-row gap-1 items-center wm-list-pagination-buttons"
on:click={() => {
isPreviousLoading = true
page = page - 1
@@ -187,19 +201,18 @@
}}
disabled={page === 0}
>
<div class="flex flex-row gap-1 items-center">
{#if isPreviousLoading && loading}
<Loader2 size={14} class="animate-spin" />
{:else}
<ChevronLeft size={14} />
{/if}
Previous
</div>
{#if isPreviousLoading && loading}
<Loader2 size={14} class="animate-spin" />
{:else}
<ChevronLeft size={14} />
{/if}
Previous
</Button>
<Button
size="xs2"
variant="border"
color="light"
btnClasses="flex flex-row gap-1 items-center wm-list-pagination-buttons"
on:click={() => {
isNextLoading = true
page = page + 1
@@ -207,15 +220,13 @@
}}
disabled={pagination.disableNext && pagination.total > 0}
>
<div class="flex flex-row gap-1 items-center">
Next
Next
{#if isNextLoading && loading}
<Loader2 size={14} class="animate-spin" />
{:else}
<ChevronRight size={14} />
{/if}
</div>
{#if isNextLoading && loading}
<Loader2 size={14} class="animate-spin" />
{:else}
<ChevronRight size={14} />
{/if}
</Button>
<div class="text-xs">{page + 1} {pagination.total > 0 ? `of ${pagination.total}` : ''}</div>
</div>
@@ -2,7 +2,7 @@
import { getContext } from 'svelte'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppViewerContext, ComponentCustomCSS, RichConfigurations } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import { Button } from '$lib/components/common'
import { twMerge } from 'tailwind-merge'
import { AlignWrapper } from '../helpers'
@@ -13,6 +13,7 @@
import { X } from 'lucide-svelte'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let customCss: ComponentCustomCSS<'modalcomponent'> | undefined = undefined
export let id: string
@@ -35,7 +36,7 @@
//used so that we can count number of outputs setup for first refresh
initOutput($worldStore, id, {})
$: css = concatCustomCss($app.css?.modalcomponent, customCss)
let css = initCss($app.css?.modalcomponent, customCss)
let open = false
function handleKeyUp(event: KeyboardEvent): void {
@@ -79,15 +80,29 @@
configuration={configuration[key]}
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.modalcomponent}
/>
{/each}
{#if render}
<div class="h-full w-full">
<AlignWrapper {noWFull} {horizontalAlignment} {verticalAlignment}>
<Button
btnClasses={css?.button?.class}
btnClasses={twMerge(css?.button?.class, 'wm-button', 'wm-modal-button')}
wrapperClasses={twMerge(
resolvedConfig?.buttonFillContainer ? 'w-full h-full' : '',
css?.buttonContainer?.class
css?.buttonContainer?.class,
'wm-button-container',
'wm-modal-button-container'
)}
style={css?.button?.style}
wrapperStyle={css?.buttonContainer?.style}
disabled={resolvedConfig.buttonDisabled}
on:pointerdown={(e) => {
@@ -121,10 +136,7 @@
>
<div
style={css?.popup?.style}
class={twMerge(
'm-24 max-h-[80%] bg-surface overflow-y-auto rounded-lg relative',
css?.popup?.class
)}
class={twMerge('mx-24 mt-8 bg-surface rounded-lg relative', css?.popup?.class)}
use:clickOutside={false}
on:click_outside={() => {
if ($mode !== 'dnd') {
@@ -139,7 +151,6 @@
on:click={() => {
open = false
}}
style={css?.button?.style}
class="hover:bg-surface-hover bg-surface-secondary rounded-full w-8 h-8 flex items-center justify-center transition-all"
>
<X class="text-tertiary" />
@@ -147,7 +158,7 @@
</div>
</div>
<div
class=""
class="wm-modal"
on:pointerdown={(e) => {
e?.stopPropagation()
if (!$connectingInput.opened) {
@@ -2,11 +2,13 @@
import { getContext } from 'svelte'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { deepEqual } from 'fast-equals'
import { initOutput } from '../../editor/appUtils'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentContainerHeight: number
@@ -30,7 +32,7 @@
}
}
$: css = concatCustomCss($app.css?.containercomponent, customCss)
let css = initCss($app.css?.containercomponent, customCss)
$componentControl[id] = {
left: () => {
@@ -66,6 +68,18 @@
}
</script>
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.[
horizontal ? 'horizontalsplitpanescomponent' : 'verticalsplitpanescomponent'
]}
/>
{/each}
<InitializeComponent {id} />
<div class="h-full w-full border" on:pointerdown={onFocus}>
@@ -88,7 +102,10 @@
visible={render}
{id}
shouldHighlight={$focusedGrid?.subGridIndex === index}
class={css?.container?.class}
class={twMerge(
css?.container?.class,
horizontal ? 'wm-horizontal-split-panes' : 'wm-vertical-split-panes'
)}
style={css?.container?.style}
subGridId={`${id}-${index}`}
containerHeight={horizontal ? undefined : componentContainerHeight - 8}
@@ -3,13 +3,15 @@
import { initOutput } from '../../editor/appUtils'
import SubGridEditor from '../../editor/SubGridEditor.svelte'
import type { AppViewerContext, ComponentCustomCSS } from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { RunnableComponent, RunnableWrapper } from '../helpers'
import type { AppInput } from '../../inputType'
import { ArrowLeftIcon, ArrowRightIcon, Loader2 } from 'lucide-svelte'
import Stepper from '$lib/components/common/stepper/Stepper.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentContainerHeight: number
@@ -117,12 +119,22 @@
}
$: selected != undefined && handleTabSelection()
$: css = concatCustomCss($app.css?.steppercomponent, customCss)
let css = initCss($app.css?.steppercomponent, customCss)
$: lastStep = selectedIndex === tabs.length - 1
let directionClicked: 'left' | 'right' | undefined = undefined
</script>
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.steppercomponent}
/>
{/each}
<InitializeComponent {id} />
<RunnableWrapper
hasChildrens
@@ -163,7 +175,7 @@
{id}
visible={render && i === selectedIndex}
subGridId={`${id}-${i}`}
class={css?.container?.class}
class={twMerge(css?.container?.class, 'wm-stepper')}
style={css?.container?.style}
containerHeight={componentContainerHeight - tabHeight - footerHeight}
on:focus={() => {
@@ -10,9 +10,11 @@
RichConfiguration,
RichConfigurations
} from '../../types'
import { concatCustomCss } from '../../utils'
import { initCss } from '../../utils'
import InputValue from '../helpers/InputValue.svelte'
import InitializeComponent from '../helpers/InitializeComponent.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let configuration: RichConfigurations
@@ -81,13 +83,23 @@
$: selected != undefined && handleTabSelection()
let selectedIndex = tabs?.indexOf(selected) ?? -1
$: css = concatCustomCss($app.css?.tabscomponent, customCss)
let css = initCss($app.css?.tabscomponent, customCss)
let resolvedDisabledTabs: boolean[] = []
</script>
<InputValue key="kind" {id} input={configuration.tabsKind} bind:value={resolvedConfig.tabsKind} />
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.tabscomponent}
/>
{/each}
<InitializeComponent {id} />
{#each disabledTabs ?? [] as disableTab, index}
@@ -97,13 +109,17 @@
<div class={resolvedConfig.tabsKind == 'sidebar' ? 'flex gap-4 w-full' : 'w-full'}>
{#if !resolvedConfig.tabsKind || resolvedConfig.tabsKind == 'tabs' || (resolvedConfig.tabsKind == 'invisibleOnView' && $mode == 'dnd')}
<div bind:clientHeight={tabHeight}>
<Tabs bind:selected class={css?.tabRow?.class} style={css?.tabRow?.style}>
<Tabs
bind:selected
class={twMerge(css?.tabRow?.class, 'wm-tabs-tabRow')}
style={css?.tabRow?.style}
>
{#each tabs ?? [] as res, index}
<Tab
value={res}
class={css?.allTabs?.class}
class={twMerge(css?.allTabs?.class, 'wm-tabs-alltabs')}
style={css?.allTabs?.style}
selectedClass={css?.selectedTab?.class}
selectedClass={twMerge(css?.selectedTab?.class, 'wm-tabs-selectedTab')}
selectedStyle={css?.selectedTab?.style}
disabled={resolvedDisabledTabs[index]}
>
@@ -136,7 +152,7 @@
{id}
visible={render && i === selectedIndex}
subGridId={`${id}-${i}`}
class={css?.container?.class}
class={twMerge(css?.container?.class, 'wm-tabs-container')}
style={css?.container?.style}
containerHeight={resolvedConfig.tabsKind !== 'sidebar' && $mode !== 'preview'
? componentContainerHeight - tabHeight
@@ -42,10 +42,18 @@
import CssSettings from './componentsPanel/CssSettings.svelte'
import ConnectionInstructions from './ConnectionInstructions.svelte'
import SettingsPanel from './SettingsPanel.svelte'
import { secondaryMenu, SecondaryMenu } from './settingsPanel/secondaryMenu'
import {
SecondaryMenu,
secondaryMenuLeft,
secondaryMenuLeftStore,
secondaryMenuRight,
secondaryMenuRightStore
} from './settingsPanel/secondaryMenu'
import Popover from '../../Popover.svelte'
import { BG_PREFIX, migrateApp } from '../utils'
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
import { getTheme } from './componentsPanel/themeUtils'
import StylePanel from './settingsPanel/StylePanel.svelte'
export let app: App
export let path: string
@@ -66,6 +74,9 @@
input: undefined,
hoveredComponent: undefined
})
const cssEditorOpen = writable<boolean>(false)
const history = initHistory(app)
const errorByComponent = writable<Record<string, { error: string; componentId: string }>>({})
@@ -83,6 +94,11 @@
const darkMode: Writable<boolean> = writable(document.documentElement.classList.contains('dark'))
const worldStore = buildWorld(context)
const previewTheme: Writable<string | undefined> = writable(undefined)
$secondaryMenuRightStore.isOpen = false
$secondaryMenuLeftStore.isOpen = false
setContext<AppViewerContext>('AppViewerContext', {
worldStore,
app: appStore,
@@ -109,7 +125,9 @@
componentControl: writable({}),
hoverStore: writable(undefined),
allIdsInPath: writable([]),
darkMode
darkMode,
cssEditorOpen,
previewTheme
})
setContext<AppEditorContext>('AppEditorContext', {
@@ -144,7 +162,7 @@
$: width = $breakpoint === 'sm' ? 'min-w-[400px] max-w-[656px]' : 'min-w-[710px] w-full'
let selectedTab: 'insert' | 'settings' = 'insert'
let selectedTab: 'insert' | 'settings' | 'css' = 'insert'
let befSelected: string | undefined = undefined
$: if ($selectedComponent?.[0] != befSelected) {
@@ -197,16 +215,184 @@
})
$: if ($connectingInput.opened) {
secondaryMenu.open(ConnectionInstructions, {}, () => {
secondaryMenuRight.open(ConnectionInstructions, {}, () => {
$connectingInput.opened = false
})
secondaryMenuLeft.close()
} else {
secondaryMenu.close()
secondaryMenuRight.close()
}
function onThemeChange() {
$darkMode = document.documentElement.classList.contains('dark')
}
let runnablePanelSize = 30
let gridPanelSize = 70
let leftPanelSize = 22
let centerPanelSize = 63
let rightPanelSize = 22
let tmpRunnablePanelSize = -1
let tmpGridPanelSize = -1
let tmpLeftPanelSize = -1
let tmpCenterPanelSize = -1
let tmpRightPanelSize = -1
let toggled = false
let cssToggled = false
$: if ($connectingInput.opened && !toggled) {
tmpRunnablePanelSize = runnablePanelSize
tmpGridPanelSize = gridPanelSize
animateTo(runnablePanelSize, 0, (newValue: number) => (runnablePanelSize = newValue))
animateTo(gridPanelSize, 100, (newValue: number) => (gridPanelSize = newValue))
toggled = true
} else if (!$connectingInput.opened && toggled) {
animateTo(
runnablePanelSize,
tmpRunnablePanelSize,
(newValue: number) => (runnablePanelSize = newValue)
)
animateTo(gridPanelSize, tmpGridPanelSize, (newValue: number) => (gridPanelSize = newValue))
tmpRunnablePanelSize = -1
tmpGridPanelSize = -1
toggled = false
}
// Animation logic for cssInput
$: animateCssInput($cssEditorOpen)
$: $cssEditorOpen && secondaryMenuLeft?.open(StylePanel, {})
function animateCssInput(cssEditorOpen: boolean) {
console.log(cssEditorOpen, cssToggled)
if (cssEditorOpen && !cssToggled) {
cssToggled = true
tmpLeftPanelSize = leftPanelSize
tmpCenterPanelSize = centerPanelSize
tmpRightPanelSize = rightPanelSize
animateTo(leftPanelSize, 20, (newValue: number) => (leftPanelSize = newValue))
animateTo(centerPanelSize, 55, (newValue: number) => (centerPanelSize = newValue))
animateTo(rightPanelSize, 25, (newValue: number) => (rightPanelSize = newValue))
tmpRunnablePanelSize = runnablePanelSize
tmpGridPanelSize = gridPanelSize
animateTo(runnablePanelSize, 0, (newValue: number) => (runnablePanelSize = newValue))
animateTo(gridPanelSize, 100, (newValue: number) => (gridPanelSize = newValue))
} else if (!cssEditorOpen && cssToggled) {
cssToggled = false
animateTo(leftPanelSize, tmpLeftPanelSize, (newValue: number) => (leftPanelSize = newValue))
animateTo(
centerPanelSize,
tmpCenterPanelSize,
(newValue: number) => (centerPanelSize = newValue)
)
animateTo(
rightPanelSize,
tmpRightPanelSize,
(newValue: number) => (rightPanelSize = newValue)
)
tmpLeftPanelSize = -1
tmpCenterPanelSize = -1
tmpRightPanelSize = -1
animateTo(
runnablePanelSize,
tmpRunnablePanelSize,
(newValue: number) => (runnablePanelSize = newValue)
)
animateTo(gridPanelSize, tmpGridPanelSize, (newValue: number) => (gridPanelSize = newValue))
tmpRunnablePanelSize = -1
tmpGridPanelSize = -1
}
}
function animateTo(start: number, end: number, onUpdate: (newValue: number) => void) {
const duration = 400
const startTime = performance.now()
function animate(time: number) {
const elapsed = time - startTime
const progress = Math.min(elapsed / duration, 1)
const currentValue = start + (end - start) * easeInOut(progress)
onUpdate(currentValue)
if (progress < 1) {
requestAnimationFrame(animate)
}
}
requestAnimationFrame(animate)
}
function easeInOut(t: number) {
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
}
$: $cssEditorOpen && selectCss()
function selectCss() {
selectedTab !== 'css' && (selectedTab = 'css')
}
const cssId = 'wm-global-style'
$: addOrRemoveCss(true, $mode === 'preview')
let css: string | undefined = undefined
appStore.subscribe(async (currentAppStore) => {
if (!currentAppStore.theme) {
return
}
if (currentAppStore.theme.type === 'inlined') {
css = currentAppStore.theme.css
} else if (currentAppStore.theme.type === 'path' && currentAppStore.theme?.path) {
let loadedCss = await getTheme($workspaceStore!, currentAppStore.theme.path)
css = loadedCss.value
}
})
$: updateCssContent(css, $previewTheme)
function addOrRemoveCss(isPremium: boolean, isPreview: boolean = false) {
const existingElement = document.getElementById(cssId)
if (!isPremium && isPreview) {
if (existingElement) {
existingElement.remove()
}
} else {
if (!existingElement) {
const head = document.head
const link = document.createElement('style')
link.id = cssId
link.innerHTML = css ?? ''
head.appendChild(link)
}
}
}
function updateCssContent(cssString: string | undefined, previewTheme: string | undefined) {
const theme = previewTheme ?? cssString ?? ''
const existingElement = document.getElementById(cssId)
if (existingElement && theme !== existingElement.innerHTML) {
existingElement.innerHTML = theme
}
}
</script>
<DarkModeObserver on:change={onThemeChange} />
@@ -220,7 +406,11 @@
{#if $mode === 'preview'}
<SplitPanesWrapper>
<div
class={twMerge('h-full w-full relative', $appStore.css?.['app']?.['viewer']?.class)}
class={twMerge(
'h-full w-full relative',
$appStore.css?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={$appStore.css?.['app']?.['viewer']?.style}
>
<AppPreview
@@ -239,57 +429,70 @@
{:else}
<SplitPanesWrapper>
<Splitpanes class="max-w-full overflow-hidden">
<Pane size={15} minSize={5} maxSize={33}>
<ContextPanel />
<Pane bind:size={leftPanelSize} minSize={5} maxSize={33}>
<div class="w-full h-full relative">
<SecondaryMenu right={false} />
<ContextPanel />
</div>
</Pane>
<Pane size={63}>
<SplitPanesWrapper>
<Splitpanes horizontal>
<Pane size={$connectingInput?.opened ? 100 : 70}>
<Pane bind:size={centerPanelSize}>
<Splitpanes horizontal class="overflow-hidden">
<Pane bind:size={gridPanelSize}>
<div
on:pointerdown={(e) => {
$selectedComponent = undefined
$focusedGrid = undefined
}}
class={twMerge(
'bg-surface h-full w-full relative',
$appStore.css?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={$appStore.css?.['app']?.['viewer']?.style}
>
<div id="app-editor-top-level-drawer" />
<div
on:pointerdown={(e) => {
$selectedComponent = undefined
$focusedGrid = undefined
}}
class={twMerge(
'bg-surface h-full w-full relative',
$appStore.css?.['app']?.['viewer']?.class
class={classNames(
'bg-surface-secondary/80 relative mx-auto w-full h-full overflow-auto',
app.fullscreen ? '' : 'max-w-6xl'
)}
style={$appStore.css?.['app']?.['viewer']?.style}
>
<div id="app-editor-top-level-drawer" />
<div
class={classNames(
'bg-surface-secondary/80 relative mx-auto w-full h-full overflow-auto',
app.fullscreen ? '' : 'max-w-6xl'
)}
>
{#if $appStore.grid}
<ComponentNavigation />
{#if $appStore.grid}
<ComponentNavigation />
<div on:pointerdown|stopPropagation class={twMerge(width, 'mx-auto')}>
<GridEditor {policy} />
</div>
{/if}
</div>
<div on:pointerdown|stopPropagation class={twMerge(width, 'mx-auto')}>
<GridEditor {policy} />
</div>
{/if}
</div>
</div>
</Pane>
{#if $connectingInput?.opened == false}
<Pane bind:size={runnablePanelSize}>
<div class="relative h-full w-full">
<InlineScriptsPanel />
</div>
</Pane>
{#if $connectingInput?.opened == false}
<Pane size={$connectingInput?.opened ? 0 : 30}>
<div class="relative h-full w-full">
<InlineScriptsPanel />
</div>
</Pane>
{/if}
</Splitpanes>
</SplitPanesWrapper>
{/if}
</Splitpanes>
</Pane>
<Pane size={22} minSize={15} maxSize={33}>
<Pane bind:size={rightPanelSize} minSize={15} maxSize={33}>
<div class="relative flex flex-col h-full">
<Tabs bind:selected={selectedTab} wrapperClass="!min-h-[42px]" class="!h-full">
<Popover disappearTimeout={0} notClickable placement="bottom">
<svelte:fragment slot="text">Component library</svelte:fragment>
<Tab value="insert" size="xs" class="h-full">
<Tab
value="insert"
size="xs"
class="h-full"
on:pointerdown={() => {
console.log('click', $cssEditorOpen)
if ($cssEditorOpen) {
$cssEditorOpen = false
selectedTab = 'insert'
}
}}
>
<div class="m-1 center-center">
<Plus size={18} />
</div>
@@ -297,7 +500,17 @@
</Popover>
<Popover disappearTimeout={0} notClickable placement="bottom">
<svelte:fragment slot="text">Component settings</svelte:fragment>
<Tab value="settings" size="xs" class="h-full">
<Tab
value="settings"
size="xs"
class="h-full"
on:pointerdown={() => {
if ($cssEditorOpen) {
$cssEditorOpen = false
selectedTab = 'settings'
}
}}
>
<div class="m-1 center-center">
<Component size={18} />
</div>
@@ -305,7 +518,17 @@
</Popover>
<Popover disappearTimeout={0} notClickable placement="bottom">
<svelte:fragment slot="text">Global styling</svelte:fragment>
<Tab value="css" size="xs" class="h-full">
<Tab
value="css"
size="xs"
class="h-full"
on:pointerdown={() => {
if (!$cssEditorOpen) {
$cssEditorOpen = true
selectedTab = 'css'
}
}}
>
<div class="m-1 center-center">
<Paintbrush size={18} />
</div>
@@ -315,7 +538,7 @@
<TabContent class="overflow-auto h-full" value="settings">
{#if $selectedComponent !== undefined}
<SettingsPanel />
<SecondaryMenu />
<SecondaryMenu right />
{:else}
<div class="min-w-[150px] text-sm text-secondary text-center py-8 px-2">
Select a component to see the settings&nbsp;for&nbsp;it
@@ -61,6 +61,7 @@
import { sendUserToast } from '$lib/toast'
import DeploymentHistory from './DeploymentHistory.svelte'
import Awareness from '$lib/components/Awareness.svelte'
import { secondaryMenuLeftStore, secondaryMenuRightStore } from './settingsPanel/secondaryMenu'
async function hash(message) {
try {
@@ -255,6 +256,9 @@
}
async function save() {
$secondaryMenuLeftStore.isOpen = false
$secondaryMenuRightStore.isOpen = false
$dirtyStore = false
saveDrawerOpen = true
return
@@ -22,8 +22,9 @@
import { deepEqual } from 'fast-equals'
import { dfs } from './appUtils'
import { BG_PREFIX, migrateApp } from '../utils'
import { workspaceStore } from '$lib/stores'
import { workspaceStore, enterpriseLicense } from '$lib/stores'
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
import { getTheme } from './componentsPanel/themeUtils'
export let app: App
export let appPath: string = ''
@@ -93,7 +94,9 @@
componentControl: writable({}),
hoverStore: writable(undefined),
allIdsInPath,
darkMode
darkMode,
cssEditorOpen: writable(false),
previewTheme: writable(undefined)
})
let previousSelectedIds: string[] | undefined = undefined
@@ -109,6 +112,47 @@
function onThemeChange() {
$darkMode = document.documentElement.classList.contains('dark')
}
const cssId = 'wm-global-style'
let css: string | undefined = undefined
appStore.subscribe(loadTheme)
async function loadTheme(currentAppStore: App) {
console.log(currentAppStore)
if (!currentAppStore.theme) {
return
}
if (currentAppStore.theme.type === 'inlined') {
css = currentAppStore.theme.css
} else if (currentAppStore.theme.type === 'path' && currentAppStore.theme.path) {
let loadedCss = await getTheme(workspace, currentAppStore.theme.path)
css = loadedCss.value
}
}
$: addOrRemoveCss($enterpriseLicense !== undefined || isEditor, css)
function addOrRemoveCss(isPremium: boolean, cssString: string | undefined) {
const existingElement = document.getElementById(cssId)
if (!isPremium) {
if (existingElement) {
existingElement.remove()
}
} else {
if (!existingElement && cssString) {
const head = document.head
const link = document.createElement('style')
link.id = cssId
link.innerHTML = cssString
head.appendChild(link)
} else if (existingElement && cssString) {
existingElement.innerHTML = cssString
}
}
}
</script>
<DarkModeObserver on:change={onThemeChange} />
@@ -145,7 +189,11 @@
<div
style={app.css?.['app']?.['grid']?.style}
class={twMerge('px-4 pt-4 pb-2 overflow-visible', app.css?.['app']?.['grid']?.class ?? '')}
class={twMerge(
'px-4 pt-4 pb-2 overflow-visible',
app.css?.['app']?.['grid']?.class ?? '',
'wm-app-grid'
)}
bind:clientWidth={$parentWidth}
>
<div>
@@ -2,7 +2,7 @@
import { Alert, Button } from '$lib/components/common'
import { getContext } from 'svelte'
import type { AppViewerContext } from '../types'
import { secondaryMenu } from './settingsPanel/secondaryMenu'
import { secondaryMenuRight } from './settingsPanel/secondaryMenu'
const { connectingInput } = getContext<AppViewerContext>('AppViewerContext')
@@ -10,7 +10,7 @@
$connectingInput.opened = false
$connectingInput.input = undefined
secondaryMenu.close()
secondaryMenuRight.close()
}
</script>
@@ -72,7 +72,11 @@
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
style={$app.css?.['app']?.['grid']?.style}
class={twMerge('px-4 pt-4 pb-2 overflow-visible', $app.css?.['app']?.['grid']?.class ?? '')}
class={twMerge(
'px-4 pt-4 pb-2 overflow-visible',
$app.css?.['app']?.['grid']?.class ?? '',
'wm-app-grid'
)}
on:pointerdown={() => {
$selectedComponent = undefined
$focusedGrid = undefined
@@ -2,7 +2,7 @@
import { getContext } from 'svelte'
import type { App, AppViewerContext } from '../types'
import { BG_PREFIX, allItems } from '../utils'
import { findGridItem } from './appUtils'
import { findComponentSettings, findGridItem } from './appUtils'
import PanelSection from './settingsPanel/common/PanelSection.svelte'
import ComponentPanel from './settingsPanel/ComponentPanel.svelte'
import InputsSpecsEditor from './settingsPanel/InputsSpecsEditor.svelte'
@@ -32,27 +32,6 @@
})
.find((x) => x)
}
function findComponentSettings(app: App, id: string | undefined) {
if (!id) return undefined
if (app?.grid) {
const gridItem = app.grid.find((x) => x.data?.id === id)
if (gridItem) {
return { item: gridItem, parent: undefined }
}
}
if (app?.subgrids) {
for (const key of Object.keys(app.subgrids ?? {})) {
const gridItem = app.subgrids[key].find((x) => x.data?.id === id)
if (gridItem) {
return { item: gridItem, parent: key }
}
}
}
return undefined
}
</script>
{#if componentSettings}
@@ -27,6 +27,26 @@ import { deepMergeWithPriority } from '$lib/utils'
import { sendUserToast } from '$lib/toast'
import { getNextId } from '$lib/components/flows/idUtils'
export function findComponentSettings(app: App, id: string | undefined) {
if (!id) return undefined
if (app?.grid) {
const gridItem = app.grid.find((x) => x.data?.id === id)
if (gridItem) {
return { item: gridItem, parent: undefined }
}
}
if (app?.subgrids) {
for (const key of Object.keys(app.subgrids ?? {})) {
const gridItem = app.subgrids[key].find((x) => x.data?.id === id)
if (gridItem) {
return { item: gridItem, parent: key }
}
}
}
return undefined
}
export function dfs(
grid: GridItem[],
id: string,
@@ -261,7 +281,7 @@ export function appComponentFromType<T extends keyof typeof components>(
panes: init.panes,
tabs: init.tabs,
conditions: init.conditions,
customCss: {},
customCss: ccomponents[type].customCss as any,
recomputeIds: init.recomputeIds ? [] : undefined,
actionButtons: init.actionButtons ? [] : undefined,
numberOfSubgrids: init.numberOfSubgrids,
@@ -134,6 +134,7 @@
$mode != 'preview' ? 'cursor-pointer' : '',
'relative z-auto',
$app.css?.['app']?.['component']?.class,
'wm-app-component',
ismoving ? 'animate-pulse' : ''
)}
style={$app.css?.['app']?.['component']?.style}
@@ -591,6 +592,7 @@
verticalAlignment={component.verticalAlignment}
horizontalAlignment={component.horizontalAlignment}
configuration={component.configuration}
customCss={component.customCss}
{render}
/>
{:else if component.type === 'chartjscomponent'}
@@ -607,6 +609,7 @@
id={component.id}
configuration={component.configuration}
componentInput={component.componentInput}
customCss={component.customCss}
{componentContainerHeight}
{render}
bind:initializing
@@ -480,7 +480,7 @@ export const components = {
}
},
jobidlogcomponent: {
name: 'Log',
name: 'Log by Job Id',
icon: Monitor,
documentationLink: `${documentationBaseUrl}#log-display`,
dims: '2:8-6:8' as AppComponentDimensions,
@@ -538,7 +538,7 @@ export const components = {
}
},
jobidflowstatuscomponent: {
name: 'Flow Status',
name: 'Flow Status by Job Id',
icon: Monitor,
documentationLink: `${documentationBaseUrl}#flow-status`,
dims: '2:8-6:8' as AppComponentDimensions,
@@ -651,7 +651,8 @@ export const components = {
dims: '1:1-3:1' as AppComponentDimensions,
documentationLink: `${documentationBaseUrl}#text`,
customCss: {
text: { class: '', style: '' }
text: { class: '', style: '' },
container: { class: '', style: '' }
},
initialData: {
horizontalAlignment: 'left',
@@ -1400,7 +1401,8 @@ This is a paragraph.
documentationLink: `${documentationBaseUrl}#toggle`,
dims: '1:1-2:1' as AppComponentDimensions,
customCss: {
text: { class: '', style: '' }
text: { class: '', style: '' },
container: { class: '', style: '' }
},
initialData: {
...defaultAlignement,
@@ -1455,9 +1457,7 @@ This is a paragraph.
icon: TextCursorInput,
documentationLink: `${documentationBaseUrl}#rich-text-editor`,
dims: '2:1-4:4' as AppComponentDimensions,
customCss: {
input: { class: '', style: '' }
},
customCss: {},
initialData: {
componentInput: undefined,
configuration: {
@@ -1734,8 +1734,8 @@ This is a paragraph.
documentationLink: `${documentationBaseUrl}#slider`,
dims: '3:1-4:1' as AppComponentDimensions,
customCss: {
bar: { style: '' },
handle: { style: '' },
bar: { style: '', class: '' },
handle: { style: '', class: '' },
limits: { class: '', style: '' },
value: { class: '', style: '' }
},
@@ -2171,7 +2171,8 @@ This is a paragraph.
dims: '1:1-2:1' as AppComponentDimensions,
customCss: {
button: { style: '', class: '' },
container: { class: '', style: '' }
container: { class: '', style: '' },
drawer: { class: '', style: '' }
},
initialData: {
horizontalAlignment: 'center',
@@ -2479,9 +2480,7 @@ This is a paragraph.
documentationLink: `${documentationBaseUrl}#select-step`,
dims: '2:1-3:1' as AppComponentDimensions,
customCss: {
tabRow: { class: '', style: '' },
allTabs: { class: '', style: '' },
selectedTab: { class: '', style: '' }
container: { class: '', style: '' }
},
initialData: {
verticalAlignment: 'center',
@@ -54,12 +54,15 @@
let search = ''
// Filter COMPONENT_SETS by search
$: componentsFiltered = COMPONENT_SETS.map((set) => ({
...set,
components: set.components.filter((component) => {
const name = componentsRecord[component].name.toLowerCase()
return name.includes(search.toLowerCase())
}),
presets: set.presets?.filter((preset) => {
const presetName = presetsRecord[preset].name.toLowerCase()
return presetName.includes(search.toLowerCase())
})
}))
</script>
@@ -0,0 +1,31 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppViewerContext, RichConfiguration } from '../../types'
import InputsSpecEditor from '../settingsPanel/InputsSpecEditor.svelte'
export let evalClass: RichConfiguration
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
$: id = Array.isArray($selectedComponent) ? $selectedComponent[0] : $selectedComponent
</script>
{#if evalClass && id}
<InputsSpecEditor
bind:componentInput={evalClass}
{id}
userInputEnabled={false}
shouldCapitalize={true}
resourceOnly={false}
fieldType="text"
tooltip="Eval an expression that return a list of class as string to dynamically add classes to the component. The styling can then be dynamic using the global CSS Editor."
customTitle="Dynamic class (eval)"
displayType={false}
placeholder={undefined}
format={undefined}
selectOptions={undefined}
subFieldType={undefined}
allowTypeChange={false}
key=""
/>
{/if}
@@ -0,0 +1,191 @@
<script lang="ts">
import { getContext } from 'svelte'
import { LayoutDashboardIcon, ArrowUpSquare, ExternalLink, TextCursorInput } from 'lucide-svelte'
import { Badge, Button, ClearableInput, Tab, TabContent, Tabs } from '../../../common'
import type { AppViewerContext } from '../../types'
import ListItem from './ListItem.svelte'
import { ccomponents, components } from '../component'
import { customisationByComponent } from './cssUtils'
import DataTable from '$lib/components/table/DataTable.svelte'
import Head from '$lib/components/table/Head.svelte'
import Cell from '$lib/components/table/Cell.svelte'
import Row from '$lib/components/table/Row.svelte'
import { createEventDispatcher } from 'svelte'
const STATIC_ELEMENTS = ['app'] as const
const TITLE_PREFIX = 'Css.' as const
type CustomCSSType = (typeof STATIC_ELEMENTS)[number] | keyof typeof components
const dispatch = createEventDispatcher()
interface CustomCSSEntry {
type: CustomCSSType
name: string
icon: any
ids: { id: string; forceStyle: boolean; forceClass: boolean }[]
}
const { app } = getContext<AppViewerContext>('AppViewerContext')
const entries: CustomCSSEntry[] = [
{
type: 'app',
name: 'App',
icon: LayoutDashboardIcon,
ids: ['viewer', 'grid', 'component'].map((id) => ({ id, forceStyle: true, forceClass: true }))
},
{
type: 'quillcomponent',
name: 'Rich Text Editor',
icon: TextCursorInput,
ids: ['q'].map((id) => ({ id, forceStyle: true, forceClass: true }))
},
...Object.entries(ccomponents)
.filter(([key]) => key !== 'quillcomponent')
.map(([type, { name, icon, customCss }]) => ({
type: type as keyof typeof components,
name,
icon,
ids: Object.entries(customCss).map(([id, v]) => ({
id,
forceStyle: v?.style != undefined,
forceClass: v?.['class'] != undefined
}))
}))
]
entries.sort((a, b) => a.name.localeCompare(b.name))
let search = ''
</script>
<div class="p-2">
<ClearableInput bind:value={search} placeholder="Search..." />
</div>
<div class="h-[calc(100%-50px)] overflow-auto relative">
{#each search != '' ? entries.filter((x) => x.name
.toLowerCase()
.includes(search.toLowerCase())) : entries as { type, name, icon, ids } (name + type)}
{#if ids.length > 0}
<ListItem
title={name}
prefix={TITLE_PREFIX}
on:open={(e) => {
if ($app.css != undefined) {
if (e.detail && $app.css[type] == undefined) {
$app.css[type] = Object.fromEntries(ids.map(({ id }) => [id, {}]))
}
}
}}
>
<div slot="title" class="flex items-center">
<svelte:component this={icon} size={18} />
<span class="ml-1">
{name}
</span>
</div>
<div class="py-2">
{#each customisationByComponent.filter( (c) => c.components.includes(type) ) as customisation (customisation.components.join('-'))}
{#if customisation.link}
<a
href={customisation.link}
target="_blank"
class="text-frost-500 dark:text-frost-300 font-semibold text-xs"
>
<div class="flex flex-row gap-2">
See documentation
<ExternalLink size="16" />
</div>
</a>
{/if}
<Tabs selected="selectors">
{#if customisation.selectors.length > 0}
<Tab value="selectors" size="xs">
Selectors ({customisation.selectors.length})
</Tab>
{/if}
{#if customisation.variables.length > 0}
<Tab value="variables" size="xs">
<div class="flex flex-row gap-2 justify-center-center items-center">
Variables ({customisation.variables.length})
</div>
</Tab>
{/if}
<div slot="content" class="h-full">
<TabContent value="selectors" class="h-full mt-2">
<DataTable size="sm">
<Head>
<tr>
<Cell head first>Selector</Cell>
<Cell head>Comment</Cell>
<Cell head last />
</tr>
</Head>
{#each customisation.selectors as { selector, comment }}
<Row>
<Cell first>
<Badge color="gray">{selector}</Badge>
</Cell>
<Cell>
{#if comment}
<div class="max-w-24 whitespace-pre-wrap">{comment}</div>
{/if}
</Cell>
<Cell>
<Button
size="xs2"
color="light"
on:click={() => {
dispatch('insertSelector', `${selector} {}`)
}}
>
<ArrowUpSquare size={16} />
</Button>
</Cell>
</Row>
{/each}
</DataTable>
</TabContent>
<TabContent value="variables" class="h-full mt-2">
<DataTable>
<Head>
<tr>
<Cell head first>Variable</Cell>
<Cell head>Default value</Cell>
<Cell head>Comment</Cell>
<Cell head last />
</tr>
</Head>
{#each customisation.variables as { variable, value, comment }}
<Row>
<Cell first>
<Badge color="gray">{variable}</Badge>
</Cell>
<Cell>
<Badge color="gray">{value}</Badge>
</Cell>
<Cell>
{#if comment}
<div class="max-w-24 whitespace-pre-wrap">{comment}</div>
{/if}
</Cell>
<Cell>
<Button size="xs2" color="light">
<ArrowUpSquare size={16} />
</Button>
</Cell>
</Row>
{/each}
</DataTable>
</TabContent>
</div>
</Tabs>
{/each}
</div>
</ListItem>
{/if}
{/each}
</div>
@@ -1,15 +1,18 @@
<script lang="ts">
import { Paintbrush2 } from 'lucide-svelte'
import { Copy, MoveLeft, MoveRight, Paintbrush2 } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import { fade } from 'svelte/transition'
import { addWhitespaceBeforeCapitals } from '../../../../utils'
import { addWhitespaceBeforeCapitals, copyToClipboard } from '../../../../utils'
import { Button, ClearableInput } from '../../../common'
import Popover from '../../../Popover.svelte'
import type { ComponentCssProperty } from '../../types'
import type { TypedComponent } from '../component'
import { ccomponents, type TypedComponent } from '../component'
import QuickStyleMenu from './QuickStyleMenu.svelte'
import type { PropertyGroup } from './quickStyleProperties'
import Tooltip from '$lib/components/Tooltip.svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import Toggle from '$lib/components/Toggle.svelte'
import CssEval from './CssEval.svelte'
export let name: string
export let value: ComponentCssProperty = {}
@@ -18,6 +21,11 @@
export let quickStyleProperties: PropertyGroup[] | undefined = undefined
export let componentType: TypedComponent['type'] | undefined = undefined
export let tooltip: string | undefined = undefined
export let shouldDisplayLeft: boolean = false
export let shouldDisplayRight: boolean = false
export let overriden: boolean = false
export let overridding: boolean = false
export let wmClass: string | undefined = undefined
const dispatch = createEventDispatcher()
let isQuickMenuOpen = false
@@ -27,30 +35,90 @@
function toggleQuickMenu() {
isQuickMenuOpen = !isQuickMenuOpen
}
let dynamicClass: boolean = value.evalClass !== undefined
</script>
<div
class="sticky top-0 z-20 text-lg bg-surface-secondary font-semibold lowercase leading-none [font-variant:small-caps] text-secondary px-3 pb-1 mt-4 mb-1"
>
{addWhitespaceBeforeCapitals(name)}
<div class=" border-b flex justify-between items-center p-2 text-xs leading-6 font-bold">
<div class="flex flex-col gap-1 w-full items-start">
<div class="flex flex-row h-8 items-center justify-between w-full">
<div class="capitalize">
{addWhitespaceBeforeCapitals(name)}
</div>
{#if shouldDisplayLeft}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
dispatch('left')
}}
>
<div class="flex flex-row gap-2 text-2xs items-center">
<MoveLeft size={14} />
Copy for this component
</div>
</Button>
{/if}
{#if shouldDisplayRight}
<Button
color="light"
size="xs2"
variant="border"
on:click={() => {
dispatch('right')
}}
>
<div class="flex flex-row gap-2 text-2xs items-center">
Copy for every {componentType ? ccomponents[componentType].name : 'component'}
<MoveRight size={14} />
</div>
</Button>
{/if}
</div>
{#if wmClass}
<Badge small>
<div class="flex flex-row gap-1 items-center">
{wmClass}
<Button
color="light"
size="xs2"
on:click={() => {
copyToClipboard(wmClass)
}}
>
<Copy size={14} />
</Button>
</div>
</Badge>
{/if}
</div>
</div>
{#if value}
<div class="px-3">
<div class="p-2">
{#if tooltip}
<div class="text-tertiary text-2xs py-2">{tooltip}</div>
{/if}
{#if value.style !== undefined || forceStyle}
<div class="pb-2">
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block">
<div class="text-sm font-medium text-tertiary pb-0.5"> Plain CSS </div>
<label class="block w-full">
<div class="flex flex-row justify-between items-center w-full h-8">
<div class="text-xs font-medium text-tertiary"> Plain CSS </div>
{#if overriden}
<Badge color="red" small>Overriden by local</Badge>
{:else if overridding}
<Badge color="blue" small>Overriding global</Badge>
{/if}
</div>
<div class="flex gap-1">
<div class="relative grow">
<ClearableInput
bind:value={value.style}
type="textarea"
wrapperClass="h-full min-h-[72px]"
inputClass="h-full"
inputClass="h-full !text-xs !rounded-none !p-2"
/>
</div>
<div class="flex flex-col gap-1">
@@ -88,18 +156,44 @@
{/if}
</div>
{/if}
{#if value.class !== undefined || forceClass}
<!-- svelte-ignore a11y-label-has-associated-control -->
<label class="block">
<div class="text-sm font-medium text-tertiary pb-0.5">
Tailwind classes<Tooltip documentationLink="https://tailwindcss.com/"
>Use any tailwind classes to style your component</Tooltip
></div
>
<div class="text-xs font-medium text-tertiary">
Tailwind classes
<Tooltip light documentationLink="https://tailwindcss.com/">
Use any tailwind classes to style your component
</Tooltip>
</div>
<div class="relative">
<ClearableInput bind:value={value.class} />
</div>
</label>
{/if}
<Toggle
options={{
right: 'Use dynamic class'
}}
size="xs"
bind:checked={dynamicClass}
on:change={(e) => {
if (e.detail && !value.evalClass) {
value.evalClass = {
type: 'evalv2',
expr: '',
connections: [],
fieldType: 'text'
}
} else {
value.evalClass = undefined
}
}}
/>
{#if value.evalClass && dynamicClass}
<CssEval bind:evalClass={value.evalClass} />
{/if}
</div>
{/if}
@@ -1,147 +1,122 @@
<script lang="ts">
import { getContext } from 'svelte'
import { LayoutDashboardIcon, MousePointer2, CurlyBraces } from 'lucide-svelte'
import { AlertTriangle, GitBranch } from 'lucide-svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
import { emptyString } from '$lib/utils'
import { ClearableInput, Tab, TabContent, Tabs } from '../../../common'
import type { AppViewerContext } from '../../types'
import ListItem from './ListItem.svelte'
import CssProperty from './CssProperty.svelte'
import { ccomponents, components } from '../component'
import { slide } from 'svelte/transition'
const STATIC_ELEMENTS = ['app'] as const
const TITLE_PREFIX = 'Css.' as const
type CustomCSSType = (typeof STATIC_ELEMENTS)[number] | keyof typeof components
interface CustomCSSEntry {
type: CustomCSSType
name: string
icon: any
ids: { id: string; forceStyle: boolean; forceClass: boolean }[]
}
import { Pane, Splitpanes } from 'svelte-splitpanes'
import CssHelperPanel from './CssHelperPanel.svelte'
import { enterpriseLicense, workspaceStore } from '$lib/stores'
import Tooltip from '$lib/components/Tooltip.svelte'
import { Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
import ThemeList from './ThemeList.svelte'
import SplitPanesWrapper from '$lib/components/splitPanes/SplitPanesWrapper.svelte'
import { resolveTheme } from './themeUtils'
import ThemeCodePreview from './ThemeCodePreview.svelte'
import { sendUserToast } from '$lib/toast'
const { app } = getContext<AppViewerContext>('AppViewerContext')
let rawCode = ''
let cssEditor: SimpleEditor | undefined = undefined
let alertHeight: number | undefined = undefined
let themeViewer: any = undefined
let selectedTab: 'css' | 'theme' = 'css'
$: rawCode && parseJson()
let jsonError = ''
let jsonErrorHeight: number
function parseJson() {
try {
$app.css = JSON.parse(rawCode ?? '')
jsonError = ''
} catch (e) {
jsonError = e.message
function insertSelector(selector: string) {
if ($app?.theme?.type === 'path') {
sendUserToast(
'You cannot edit the theme because it is a path theme. Fork the theme to edit it.',
true
)
return
}
}
function switchTab(asJson: boolean) {
if (asJson) {
rawCode = JSON.stringify($app.css, null, 2)
} else {
parseJson()
}
const code = cssEditor?.getCode()
cssEditor?.setCode(code + '\n' + selector)
$app = $app
}
const entries: CustomCSSEntry[] = [
{
type: 'app',
name: 'App',
icon: LayoutDashboardIcon,
ids: ['viewer', 'grid', 'component'].map((id) => ({ id, forceStyle: true, forceClass: true }))
},
...Object.entries(ccomponents).map(([type, { name, icon, customCss }]) => ({
type: type as keyof typeof components,
name,
icon,
ids: Object.entries(customCss).map(([id, v]) => ({
id,
forceStyle: v?.style != undefined,
forceClass: v?.['class'] != undefined
}))
}))
]
entries.sort((a, b) => a.name.localeCompare(b.name))
let search = ''
</script>
<!-- <div class="w-full text-lg font-semibold text-center text-tertiary p-2">Global Styling</div> -->
<Tabs selected="ui" on:selected={(e) => switchTab(e.detail === 'json')} class="h-full">
<Tab value="ui" size="xs" class="w-1/2">
<div class="m-1 center-center">
<MousePointer2 size={16} />
<span class="pl-1">UI</span>
</div>
</Tab>
<Tab value="json" size="xs" class="w-1/2">
<div class="m-1 center-center">
<CurlyBraces size={16} />
<span class="pl-1">JSON</span>
</div>
</Tab>
<div slot="content" class="h-[calc(100%-35px)] overflow-auto">
<TabContent value="ui" class="h-full">
<div class="p-2">
<ClearableInput bind:value={search} placeholder="Search..." />
</div>
<div class="h-[calc(100%-50px)] overflow-auto relative">
{#each search != '' ? entries.filter((x) => x.name
.toLowerCase()
.includes(search.toLowerCase())) : entries as { type, name, icon, ids } (name + type)}
{#if ids.length > 0}
<ListItem
title={name}
prefix={TITLE_PREFIX}
on:open={(e) => {
if ($app.css != undefined) {
if (e.detail && $app.css[type] == undefined) {
$app.css[type] = Object.fromEntries(ids.map(({ id }) => [id, {}]))
}
}
}}
>
<div slot="title" class="flex items-center">
<svelte:component this={icon} size={18} />
<span class="ml-1">
{name}
</span>
</div>
<div class="pb-2">
{#each ids as { id, forceStyle, forceClass }}
<div class="mb-3">
{#if $app?.css?.[type]}
<CssProperty
{forceClass}
{forceStyle}
name={id}
bind:value={$app.css[type][id]}
/>
{/if}
<Drawer bind:this={themeViewer} size="800px">
<DrawerContent title="View themes" on:close={themeViewer.closeDrawer}>sa</DrawerContent>
</Drawer>
<Tabs bind:selected={selectedTab}>
<Tab size="xs" value="css">Code</Tab>
<Tab size="xs" value="theme">Theme</Tab>
<svelte:fragment slot="content">
{#if selectedTab === 'css'}
<SplitPanesWrapper>
<Splitpanes horizontal>
<Pane size={60}>
{#if $enterpriseLicense === undefined}
<div bind:clientHeight={alertHeight} class="p-2 flex flex-row gap-2">
<div class="flex flex-row items-center text-yellow-500 text-xs">
<div class="flex">
<AlertTriangle size={16} />
EE only
</div>
{/each}
<Tooltip>
App CSS editor is an exclusive feature of the Enterprise Edition. You can
experiment with this feature in the editor, but please note that the changes
will not be visible once deployed.
</Tooltip>
</div>
<div class="flex flex-row items-center text-blue-500 text-xs">
Component styling is still available in the Community Edition
<Tooltip>
You can still style components individually in the Community Edition.
</Tooltip>
</div>
</div>
</ListItem>
{/if}
{/each}
</div>
</TabContent>
<TabContent value="json" class="h-full">
{#if !emptyString(jsonError)}
<div
transition:slide={{ duration: 200 }}
bind:clientHeight={jsonErrorHeight}
class="text-red-500 text-xs p-1"
>
{jsonError}
</div>
{/if}
<div style="height: calc(100% - {jsonErrorHeight || 0}px);">
<SimpleEditor class="h-full" lang="json" bind:code={rawCode} fixedOverflowWidgets={false} />
</div>
</TabContent>
</div>
{/if}
<div style="height: calc(100% - {alertHeight || 0}px);">
{#if $app.theme?.type === 'inlined'}
<SimpleEditor
class="h-full"
lang="css"
bind:code={$app.theme.css}
fixedOverflowWidgets={false}
small
automaticLayout
bind:this={cssEditor}
deno={false}
/>
{:else}
<ThemeCodePreview theme={$app.theme}>
<div class="p-2 w-min">
<Button
size="xs"
color="dark"
on:click={async () => {
const theme = await resolveTheme($app.theme, $workspaceStore)
$app.theme = {
type: 'inlined',
css: theme
}
}}
>
<div class="flex flex-row gap-2 items-center">
<GitBranch size={16} />
Fork theme to edit
</div>
</Button>
</div>
</ThemeCodePreview>
{/if}
</div>
</Pane>
<Pane size={40}>
<CssHelperPanel on:insertSelector={(e) => insertSelector(e.detail)} />
</Pane>
</Splitpanes>
</SplitPanesWrapper>
{/if}
{#if selectedTab === 'theme'}
<ThemeList
on:setCodeTab={() => {
selectedTab = 'css'
}}
/>
{/if}
</svelte:fragment>
</Tabs>
@@ -0,0 +1,34 @@
<script lang="ts">
import type { AppTheme } from '../../types'
import { resolveTheme } from './themeUtils'
import { workspaceStore } from '$lib/stores'
import { onMount } from 'svelte'
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
export let theme: AppTheme | undefined = undefined
let code: string | undefined = undefined
let cssEditor: SimpleEditor | undefined = undefined
onMount(async () => {
code = await resolveTheme(theme, $workspaceStore)
cssEditor?.setCode(code)
})
</script>
<div class="relative h-full">
<div class="absolute z-[100] left-[50%] top-[50%] translate-x-[-50%] translate-y-[-50%]">
<slot />
</div>
<div class="absolute top-0 left-0 right-0 bottom-0 bg-gray-100 bg-opacity-50 z-50" />
<SimpleEditor
class="h-full"
lang="css"
{code}
fixedOverflowWidgets={false}
small
automaticLayout
bind:this={cssEditor}
deno={false}
/>
</div>
@@ -0,0 +1,26 @@
<script lang="ts">
import { Drawer, DrawerContent } from '$lib/components/common'
import { Highlight } from 'svelte-highlight'
import type { AppTheme } from '../../types'
import css from 'svelte-highlight/languages/css'
import { resolveTheme } from './themeUtils'
import { workspaceStore } from '$lib/stores'
export let theme: AppTheme
let code: string | undefined = undefined
let codeDrawer: Drawer
export async function openDrawer() {
codeDrawer.openDrawer()
code = await resolveTheme(theme, $workspaceStore)
}
</script>
<Drawer bind:this={codeDrawer}>
<DrawerContent title="Theme viewer" on:close={codeDrawer.closeDrawer}>
<div class="p-2 border rounded-sm">
<Highlight code={code ?? ''} language={css} />
</div>
</DrawerContent>
</Drawer>
@@ -0,0 +1,159 @@
<script lang="ts">
import { listThemes, type Theme, createTheme } from './themeUtils'
import { enterpriseLicense, workspaceStore } from '$lib/stores'
import { getContext, onMount } from 'svelte'
import Button from '$lib/components/common/button/Button.svelte'
import DataTable from '$lib/components/table/DataTable.svelte'
import Head from '$lib/components/table/Head.svelte'
import Cell from '$lib/components/table/Cell.svelte'
import { EyeOff } from 'lucide-svelte'
import type { AppViewerContext } from '../../types'
import { sendUserToast } from '$lib/toast'
import { ResourceService } from '$lib/gen'
import { Alert } from '$lib/components/common'
import ThemeRow from './ThemeRow.svelte'
import { onDestroy } from 'svelte'
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
const { previewTheme, app } = getContext<AppViewerContext>('AppViewerContext')
let cssString: string | undefined = $app?.theme?.type === 'inlined' ? $app.theme.css : undefined
$: type = $app?.theme?.type
let themes: Array<{
name: string
path: string
}> = []
let loading: boolean = false
async function getThemes() {
loading = true
themes = await listThemes($workspaceStore!)
loading = false
}
async function addTheme(nameField: string) {
const themes = await ResourceService.listResourceNames({
workspace: $workspaceStore!,
name: 'app_theme'
})
const theme: Theme = {
path: 'f/app_themes/theme_' + themes.length,
value: {
value: cssString ?? '',
name: nameField
}
}
const message = await createTheme($workspaceStore!, theme)
getThemes()
nameField = ''
sendUserToast('Theme created:' + message)
$app.theme = {
type: 'path',
path: theme.path
}
}
let nameField: string = ''
let previewThemePath: string | undefined = undefined
onMount(() => {
getThemes()
})
onDestroy(() => {
previewTheme.set(undefined)
previewThemePath = undefined
})
</script>
<div class="p-4 flex flex-col items-start w-auto gap-2 relative">
{#if $enterpriseLicense === undefined}
<div class="absolute top-0 left-0 w-full h-full bg-gray-50 opacity-50 z-10 bottom-0" />
<Alert
type="warning"
title="Themes are available in the enterprise edition."
class="w-full z-50"
size="xs"
>
Upgrade to the enterprise edition to use themes.
</Alert>
{/if}
<div class="w-full flex flex-row gap-2 items-center">
<input
disabled={type != 'inlined'}
bind:value={nameField}
placeholder={type == 'inlined'
? 'Theme name'
: 'Fork a theme and edit it to create a new one'}
/>
<Button
disabled={type != 'inlined' || nameField == ''}
on:click={() => addTheme(nameField)}
color="dark"
size="xs">Create theme</Button
>
</div>
{#if loading}
<div class="flex flex-col w-full pt-12">
{#each new Array(6) as _}
<Skeleton layout={[[2], 0.5]} />
{/each}
</div>
{:else if Array.isArray(themes) && themes.length > 0}
<div class="flex flex-row justify-end items-center w-full h-10">
{#if $previewTheme != undefined}
<Button
color="dark"
variant="border"
size="xs"
on:click={() => {
previewTheme.set(undefined)
previewThemePath = undefined
}}
>
<div class="flex flex-row gap-1 items-center">
<EyeOff size={16} />
Clear preview
</div>
</Button>
{/if}
</div>
<div class="w-full">
<DataTable size="sm">
<Head>
<tr>
<Cell first head>Path</Cell>
<Cell last head />
</tr>
</Head>
<tbody class="divide-y">
{#if themes && themes.length > 0}
{#each themes as row}
{#key row}
<ThemeRow
{row}
bind:previewThemePath
on:reloadThemes={() => {
getThemes()
}}
/>
{/key}
{/each}
{:else}
<tr>Loading...</tr>
{/if}
</tbody>
</DataTable>
</div>
{/if}
</div>
@@ -0,0 +1,57 @@
<script lang="ts">
import { updateTheme } from './themeUtils'
import { workspaceStore } from '$lib/stores'
import Button from '$lib/components/common/button/Button.svelte'
import { Popup } from '$lib/components/common'
import { Pen } from 'lucide-svelte'
import { createEventDispatcher } from 'svelte'
import { sendUserToast } from '$lib/toast'
export let row: {
name: string
path: string
}
let editedName = row.name
const dispatch = createEventDispatcher()
</script>
<Popup
floatingConfig={{ strategy: 'absolute', placement: 'bottom-end' }}
containerClasses="border rounded-lg shadow-lg p-4 bg-surface"
let:close
>
<svelte:fragment slot="button">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row gap-1 items-center">
<Pen size={16} />
</div>
</Button>
</svelte:fragment>
<div class="flex flex-col w-80 gap-2">
<div class="leading-6 font-semibold text-xs">Edit theme name</div>
<div class="flex flex-row gap-2">
<input bind:value={editedName} />
<Button
color="dark"
size="xs"
on:click={async () => {
if (!$workspaceStore) return
await updateTheme($workspaceStore, row.path, {
value: {
...row,
name: editedName
}
})
dispatch('reloadThemes')
close(null)
sendUserToast('Theme name updated:\n' + editedName)
}}
>
Update
</Button>
</div>
</div>
</Popup>
@@ -0,0 +1,215 @@
<script lang="ts">
import { deleteTheme, getTheme, updateTheme, resolveTheme, DEFAULT_THEME } from './themeUtils'
import { workspaceStore } from '$lib/stores'
import { getContext } from 'svelte'
import Button from '$lib/components/common/button/Button.svelte'
import Cell from '$lib/components/table/Cell.svelte'
import { Code, Eye, GitBranch, Pin, Save, Trash } from 'lucide-svelte'
import type { AppViewerContext } from '../../types'
import { sendUserToast } from '$lib/toast'
import Badge from '$lib/components/common/badge/Badge.svelte'
import { twMerge } from 'tailwind-merge'
import { createEventDispatcher } from 'svelte'
import ThemeNameEditor from './ThemeNameEditor.svelte'
import ThemeDrawer from './ThemeDrawer.svelte'
import ButtonDropdown from '$lib/components/common/button/ButtonDropdown.svelte'
import { classNames } from '$lib/utils'
import { MenuItem } from '@rgossiaux/svelte-headlessui'
export let previewThemePath: string | undefined = undefined
export let row: {
name: string
path: string
}
const { previewTheme, app } = getContext<AppViewerContext>('AppViewerContext')
let cssString: string | undefined = $app?.theme?.type === 'inlined' ? $app.theme.css : undefined
$: type = $app?.theme?.type
const dispatch = createEventDispatcher()
async function toggleUpdate(row) {
if (!$workspaceStore) return
try {
await updateTheme($workspaceStore, row.path, {
value: {
name: row.name,
value: cssString ?? ''
}
})
$app.theme = {
type: 'path',
path: row.path
}
sendUserToast('Theme updated:\n' + row.name)
} catch (e) {
sendUserToast('Theme update failed:\n' + e)
}
}
async function makeDefaultTheme(path: string) {
const defaultTheme = await getTheme($workspaceStore!, DEFAULT_THEME)
const theme = await getTheme($workspaceStore!, path)
await updateTheme($workspaceStore!, DEFAULT_THEME, {
value: theme.value
})
await updateTheme($workspaceStore!, path, {
value: defaultTheme.value
})
dispatch('reloadThemes')
}
async function toggleDelete() {
stopPreview()
if ($workspaceStore) {
await deleteTheme($workspaceStore, row.path)
}
dispatch('reloadThemes')
sendUserToast('Theme deleted:\n' + row.name)
}
async function preview() {
previewThemePath = row.path
const theme = await resolveTheme(
{
type: 'path',
path: row.path ?? ''
},
$workspaceStore
)
$previewTheme = theme ?? ''
}
async function fork() {
stopPreview()
const theme = await resolveTheme($app.theme, $workspaceStore)
$app.theme = {
type: 'inlined',
css: theme
}
dispatch('setCodeTab')
}
function stopPreview() {
previewThemePath = undefined
$previewTheme = undefined
}
function apply() {
stopPreview()
$app.theme = {
type: 'path',
path: row.path ?? ''
}
}
let themeDrawer: ThemeDrawer
</script>
<tr class={twMerge(previewThemePath === row.path ? 'bg-blue-200' : '', 'transition-all')}>
<Cell first>
<div class="flex flex-row gap-1 items-center">
<ThemeNameEditor on:reloadThemes {row} />
{row.name}
</div>
</Cell>
<Cell last>
<div class={twMerge('flex flex-row gap-1 justify-end ')}>
{#if row.path === DEFAULT_THEME}
<Badge color="blue">Default theme</Badge>
{/if}
{#if $app?.theme?.type === 'path' && $app.theme.path === row.path}
<Badge color="green">Active</Badge>
{/if}
{#if type === 'inlined'}
<Button color="light" size="xs" on:click={() => toggleUpdate(row)}>
<div class="flex flex-row gap-1 items-center">
<Save size={16} />
Update
</div>
</Button>
{/if}
{#if $app?.theme?.type !== 'path' || $app.theme.path !== row.path}
<Button color="light" size="xs" on:click={preview}>
<div class="flex flex-row gap-1 items-center">
<Eye size={16} />
Preview
</div>
</Button>
<Button color="dark" size="xs" on:click={apply}>Apply</Button>
{/if}
<button on:pointerdown|stopPropagation>
<ButtonDropdown hasPadding={false}>
<svelte:fragment slot="items">
<MenuItem on:click={() => themeDrawer?.openDrawer()}>
<div
class={classNames(
'!text-primary flex flex-row items-center text-left px-4 py-2 gap-2 cursor-pointer hover:bg-gray-100 !text-xs font-semibold'
)}
>
<Code size={16} />
View code
</div>
</MenuItem>
<MenuItem on:click={fork}>
<div
class={classNames(
'!text-primary flex flex-row items-center text-left px-4 py-2 gap-2 cursor-pointer hover:bg-gray-100 !text-xs font-semibold'
)}
>
<GitBranch size={16} />
Fork
</div>
</MenuItem>
{#if row.path !== DEFAULT_THEME}
<MenuItem on:click={() => makeDefaultTheme(row.path)}>
<div
class={classNames(
'!text-primary flex flex-row items-center text-left px-4 py-2 gap-2 cursor-pointer hover:bg-gray-100 !text-xs font-semibold'
)}
>
<Pin size={16} />
Make default
</div>
</MenuItem>
<MenuItem on:click={toggleDelete}>
<div
class={classNames(
'!text-red-600 flex flex-row items-center text-left px-4 py-2 gap-2 cursor-pointer hover:bg-gray-100 !text-xs font-semibold'
)}
>
<Trash size={16} />
Delete
</div>
</MenuItem>
{/if}
</svelte:fragment>
</ButtonDropdown>
</button>
</div>
</Cell>
</tr>
<ThemeDrawer
bind:this={themeDrawer}
theme={{
type: 'path',
path: row.path ?? ''
}}
/>
@@ -0,0 +1,790 @@
import * as csstree from 'css-tree'
import type { ComponentCssProperty } from '../../types'
export function sanitizeCss(css: string, authorizedClassNames: string[]) {
const ast = csstree.parse(css)
const removedClassNames: string[] = []
csstree.walk(ast, (node: any, item, list) => {
if (node.type === 'Rule') {
let shouldRemoveRule = true
csstree.walk(node, (innerNode: any) => {
if (innerNode.type === 'ClassSelector' && authorizedClassNames.includes(innerNode.name)) {
shouldRemoveRule = false
}
if (shouldRemoveRule && innerNode.name) {
removedClassNames.push(innerNode.name)
}
})
if (shouldRemoveRule) {
list.remove(item)
}
}
})
return {
css: csstree.generate(ast),
removedClassNames
}
}
export const authorizedClassnames = [
'wm-container',
'wm-list',
'wm-list-pagination',
'wm-list-pagination-buttons',
'wm-drawer',
'wm-drawer-button',
'wm-drawer-button-container',
'wm-button',
'wm-button-wrapper',
'wm-button-container',
'wm-vertical-split-panes',
'wm-horizontal-split-panes',
'wm-modal',
'wm-modal-button',
'wm-modal-button-container',
'wm-tabs-container',
'wm-tabs-tabRow',
'wm-tabs-alltabs',
'wm-tabs-selectedTab',
'wm-carousel',
'wm-submit',
'wm-submit-button',
'wm-number-input',
'wm-currency-input',
'wm-date-input',
'wm-text-input',
'wm-html',
'wm-table-container',
'wm-table-header',
'wm-table-body',
'wm-table-footer',
'wm-table-row-selected',
'wm-table-row',
'wm-stepper',
'wm-file-input',
'wm-toggle-text',
'wm-toggle-container',
'wm-image',
'wm-pdf',
'wm-horizontal-divider',
'wm-vertical-divider',
'wm-horizontal-divider-container',
'wm-vertical-divider-container',
'wm-log-header',
'wm-log-container',
'wm-map',
'wm-icon',
'wm-icon-container',
'wm-flow-status-header',
'wm-flow-status-container',
'wm-select-tab-row',
'wm-select-tab',
'wm-select-tab-selected',
'ql-toolbar',
'ql-stroke',
'ql-fill',
'ql-container',
'wm-pie-chart',
'wm-modal-form-popup',
'wm-modal-form-button',
'wm-download-button',
'wm-download-button-container',
'wm-bar-chart',
'wm-scatter-chart',
'wm-chartjs',
'wm-timeseries',
'wm-conditional-tabs',
'wm-rich-result-header',
'wm-rich-result-container'
// TODO: Select and mutltiselect
]
interface Selector {
selector: string
comment?: string | undefined
customCssKey?: string | undefined
}
interface Variable {
variable: string
value: string
comment?: string | undefined
}
interface Customisation {
components: string[]
selectors: Selector[]
variables: Variable[]
link?: string | undefined
variablesTooltip?: string
}
export const customisationByComponent: Customisation[] = [
{
components: ['app'],
selectors: [
{ selector: '.wm-app-viewer', comment: 'Applied to the div under all components' },
{
selector: '.wm-app-grid',
comment: 'Applied to the div that contains the grid of components'
},
{
selector: '.wm-app-component',
comment: 'Applied to the div that is around every components'
}
],
variables: []
},
{
components: ['buttoncomponent'],
selectors: [
{ selector: '.wm-button', comment: 'Applied to the button', customCssKey: 'button' },
{ selector: '.wm-button-wrapper', comment: 'Applied to the div around the button' },
{
selector: '.wm-button-container',
comment: 'Applied to the button container',
customCssKey: 'container'
}
],
variables: []
},
{
components: ['containercomponent'],
selectors: [
{ selector: '.wm-container', comment: 'Container component', customCssKey: 'container' }
],
variables: []
},
{
components: ['listcomponent'],
selectors: [
{ selector: '.wm-list', comment: 'List component', customCssKey: 'container' },
{ selector: '.wm-list-pagination', comment: 'Pagination component' },
{ selector: '.wm-list-pagination-buttons', comment: 'Pagination buttons component' }
],
variables: []
},
{
components: ['drawercomponent'],
selectors: [
{ selector: '.wm-drawer', comment: 'main drawer element', customCssKey: 'drawer' },
{ selector: '.wm-drawer-button', comment: 'button to open drawer', customCssKey: 'button' },
{
selector: '.wm-drawer-button-container',
comment: 'container for button to open drawer',
customCssKey: 'container'
}
],
variables: []
},
{
components: ['verticalsplitpanescomponent'],
selectors: [
{
selector: '.wm-vertical-split-panes',
comment: 'Vertical split panes component',
customCssKey: 'container'
}
],
variables: []
},
{
components: ['horizontalsplitpanescomponent'],
selectors: [
{
selector: '.wm-horizontal-split-panes',
comment: 'Horizontal split panes component',
customCssKey: 'container'
}
],
variables: []
},
{
components: ['modalcomponent'],
selectors: [
{ selector: '.wm-modal', comment: 'main modal element', customCssKey: 'popup' },
{ selector: '.wm-modal-button', comment: 'button to open modal', customCssKey: 'button' },
{
selector: '.wm-modal-button-container',
comment: 'container for button to open modal',
customCssKey: 'buttonContainer'
}
],
variables: []
},
{
components: ['rangecomponent', 'slidercomponent'],
selectors: [
{ selector: '.wm-slider-bar', comment: 'Slider bar', customCssKey: 'bar' },
{ selector: '.wm-slider-handle', comment: 'Slider handle', customCssKey: 'handle' },
{ selector: '.wm-slider-limits', comment: 'Slider limits', customCssKey: 'limits' },
{ selector: '.wm-slider-value', comment: 'Slider value', customCssKey: 'value' },
{ selector: '.rangeSlider', comment: 'main slider element' },
{ selector: '.rangeSlider.vertical', comment: 'if slider is vertical' },
{ selector: '.rangeSlider.focus', comment: 'if slider is focussed' },
{ selector: '.rangeSlider.range', comment: 'if slider is a range' },
{ selector: '.rangeSlider.min', comment: 'if slider is a min-range' },
{ selector: '.rangeSlider.max', comment: 'if slider is a max-range' },
{ selector: '.rangeSlider.pips', comment: 'if slider has visible pips' },
{ selector: '.rangeSlider.pip-labels', comment: 'if slider has labels for pips' },
{
selector: '.rangeSlider > .rangeHandle',
comment: 'positioned wrapper for the handle/float'
},
{
selector: '.rangeSlider > .rangeHandle.active',
comment: 'if a handle is active in any way'
},
{
selector: '.rangeSlider > .rangeHandle.press',
comment: 'if a handle is being pressed down'
},
{
selector: '.rangeSlider > .rangeHandle.hoverable',
comment: 'if the handles allow hover effect'
},
{
selector: '.rangeSlider > .rangeHandle > .rangeNub',
comment: 'the actual nub rendered as a handle'
},
{
selector: '.rangeSlider > .rangeHandle > .rangeFloat',
comment: 'the floating value above the handle'
},
{ selector: '.rangeSlider > .rangeBar', comment: 'the range between the two handles' },
{ selector: '.rangeSlider > .rangePips', comment: 'the container element for the pips' },
{ selector: '.rangeSlider > .rangePips.focus', comment: 'if slider is focussed' },
{ selector: '.rangeSlider > .rangePips.vertical', comment: 'if slider is vertical' },
{ selector: '.rangeSlider > .rangePips > .pip', comment: 'an individual pip' },
{
selector: '.rangeSlider > .rangePips > .pip.first',
comment: 'the first pip on the slider'
},
{ selector: '.rangeSlider > .rangePips > .pip.last', comment: 'the last pip on the slider' },
{ selector: '.rangeSlider > .rangePips > .pip.selected', comment: 'if a pip is selected' },
{
selector: '.rangeSlider > .rangePips > .pip.in-range',
comment: 'if a pip is somewhere in the range'
},
{ selector: '.rangeSlider > .rangePips > .pip > .pipVal', comment: 'the label for the pip' }
],
variables: [
{ variable: '--range-slider', value: '#d7dada', comment: 'slider main background color' },
{ variable: '--range-handle-inactive', value: '#99a2a2', comment: 'inactive handle color' },
{ variable: '--range-handle', value: '#838de7', comment: 'non-focussed handle color' },
{ variable: '--range-handle-focus', value: '#4a40d4', comment: 'focussed handle color' },
{ variable: '--range-handle-border', value: 'var(--range-handle)' },
{
variable: '--range-range-inactive',
value: 'var(--range-handle-inactive)',
comment: 'inactive range bar background color'
},
{
variable: '--range-range',
value: 'var(--range-handle-focus)',
comment: 'active range bar background color'
},
{
variable: '--range-float-inactive',
value: 'var(--range-handle-inactive)',
comment: 'inactive floating label background color'
},
{
variable: '--range-float',
value: 'var(--range-handle-focus)',
comment: 'floating label background color'
},
{ variable: '--range-float-text', value: 'white', comment: 'text color on floating label' }
],
link: 'https://simeydotme.github.io/svelte-range-slider-pips/#styling'
},
{
components: ['tabscomponent'],
selectors: [
{ selector: '.wm-tabs-container', comment: 'Tabs container', customCssKey: 'container' },
{ selector: '.wm-tabs-tabRow', comment: 'Tabs row', customCssKey: 'tabRow' },
{ selector: '.wm-tabs-alltabs', comment: 'All tabs', customCssKey: 'allTabs' },
{ selector: '.wm-tabs-selectedTab', comment: 'Selected tab', customCssKey: 'selectedTab' }
],
variables: []
},
{
components: ['carousellistcomponent'],
selectors: [
{ selector: '.wm-carousel', comment: 'Carousel component', customCssKey: 'container' }
],
variables: []
},
{
components: ['formcomponent'],
selectors: [
{ selector: '.wm-submit', comment: 'Submit component', customCssKey: 'container' },
{ selector: '.wm-submit-button', comment: 'Submit button', customCssKey: 'button' }
],
variables: []
},
{
components: ['numberinputcomponent'],
selectors: [
{ selector: '.wm-number-input', comment: 'Number component', customCssKey: 'input' }
],
variables: []
},
{
components: ['currencycomponent'],
selectors: [
{ selector: '.wm-currency-input', comment: 'Currency component', customCssKey: 'input' }
],
variables: []
},
{
components: ['dateinputcomponent'],
selectors: [{ selector: '.wm-date-input', comment: 'Date component', customCssKey: 'input' }],
variables: []
},
{
components: [
'emailinputcomponent',
'textinputcomponent',
'textareainputcomponent',
'passwordinputcomponent'
],
selectors: [{ selector: '.wm-text-input', comment: 'Text component', customCssKey: 'input' }],
variables: []
},
{
components: ['htmlcomponent'],
selectors: [{ selector: '.wm-html', comment: 'HTML component', customCssKey: 'container' }],
variables: []
},
{
components: ['tablecomponent'],
selectors: [
{ selector: '.wm-table-container', comment: 'Table component', customCssKey: 'container' },
{ selector: '.wm-table-header', comment: 'Table header', customCssKey: 'tableHeader' },
{ selector: '.wm-table-body', comment: 'Table body', customCssKey: 'tableBody' },
{ selector: '.wm-table-footer', comment: 'Table footer', customCssKey: 'tableFooter' },
{ selector: '.wm-table-row-selected', comment: 'Selected row' },
{ selector: '.wm-table-row', comment: 'Table row' }
],
variables: []
},
{
components: ['steppercomponent'],
selectors: [
{ selector: 'wm-stepper', comment: 'Stepper component', customCssKey: 'container' }
],
variables: []
},
{
components: ['fileinputcomponent'],
selectors: [
{ selector: '.wm-file-input', comment: 'File input component', customCssKey: 'input' }
],
variables: []
},
{
components: ['checkboxcomponent'],
selectors: [
{ selector: '.wm-toggle-text', comment: 'Checkbox component label', customCssKey: 'text' },
{
selector: '.wm-toggle-container',
comment: 'Checkbox component container',
customCssKey: 'container'
}
],
variables: []
},
{
components: ['imagecomponent'],
selectors: [{ selector: '.wm-image', comment: 'Image component', customCssKey: 'image' }],
variables: []
},
{
components: ['pdfcomponent'],
selectors: [{ selector: '.wm-pdf', comment: 'PDF component', customCssKey: 'container' }],
variables: []
},
{
components: ['horizontaldividercomponent'],
selectors: [
{
selector: '.wm-horizontal-divider',
comment: 'Horizontal divider component',
customCssKey: 'divider'
},
{
selector: '.wm-horizontal-divider-container',
comment: 'Horizontal divider container',
customCssKey: 'container'
}
],
variables: []
},
{
components: ['verticaldividercomponent'],
selectors: [
{
selector: '.wm-vertical-divider',
comment: 'Vertical divider component',
customCssKey: 'divider'
},
{
selector: '.wm-vertical-divider-container',
comment: 'Vertical divider container',
customCssKey: 'container'
}
],
variables: []
},
{
components: ['logcomponent', 'jobidlogcomponent'],
selectors: [
{ selector: '.wm-log-header', comment: 'Log header', customCssKey: 'header' },
{ selector: '.wm-log-container', comment: 'Log container', customCssKey: 'container' }
],
variables: []
},
{
components: ['mapcomponent'],
selectors: [{ selector: '.wm-map', comment: 'Map component', customCssKey: 'map' }],
variables: []
},
{
components: ['iconcomponent'],
selectors: [
{ selector: '.wm-icon', comment: 'Icon component', customCssKey: 'icon' },
{ selector: '.wm-icon-container', comment: 'Icon container', customCssKey: 'container' }
],
variables: []
},
{
components: ['flowstatuscomponent', 'jobidflowstatuscomponent'],
selectors: [
{ selector: '.wm-flow-status-header', comment: 'Flow status header', customCssKey: 'header' },
{
selector: '.wm-flow-status-container',
comment: 'Flow status container',
customCssKey: 'container'
}
],
variables: []
},
{
components: ['selecttabcomponent'],
selectors: [
{ selector: '.wm-select-tab-row', comment: 'Select tab row', customCssKey: 'tabRow' },
{ selector: '.wm-select-tab', comment: 'Select tab', customCssKey: 'tab' },
{
selector: '.wm-select-tab-selected',
comment: 'Select tab selected',
customCssKey: 'selectedTab'
}
],
variables: []
},
{
components: ['piechartcomponent'],
selectors: [{ selector: '.wm-pie-chart', comment: 'Pie chart', customCssKey: 'container' }],
variables: []
},
{
components: ['quillcomponent'],
selectors: [
{ selector: '.ql-toolbar', comment: 'Quill toolbar' },
{ selector: '.ql-stroke', comment: 'Quill stroke' },
{ selector: '.ql-fill', comment: 'Quill fill' },
{ selector: '.ql-container', comment: 'Quill container' }
],
variables: []
},
{
components: ['formbuttoncomponent'],
selectors: [
{ selector: '.wm-modal-form-popup', comment: 'Modal form popup', customCssKey: 'popup' },
{ selector: '.wm-modal-form-button', comment: 'Modal form button', customCssKey: 'button' }
],
variables: []
},
{
components: ['downloadcomponent'],
selectors: [
{ selector: '.wm-download-button', comment: 'Download button', customCssKey: 'button' },
{ selector: '.wm-download-button-container', comment: 'Download button container' }
],
variables: []
},
{
components: ['barchartcomponent'],
selectors: [{ selector: '.wm-bar-chart', comment: 'Bar chart', customCssKey: 'container' }],
variables: []
},
{
components: ['scatterchartcomponent'],
selectors: [
{ selector: '.wm-scatter-chart', comment: 'Scatter chart', customCssKey: 'container' }
],
variables: []
},
{
components: ['textcomponent'],
selectors: [
{ selector: '.wm-text', comment: 'Text component', customCssKey: 'text' },
{ selector: '.wm-text-container', comment: 'Text container', customCssKey: 'container' }
],
variables: []
},
{
components: ['chartjscomponent'],
selectors: [{ selector: '.wm-chartjs', comment: 'ChartJS', customCssKey: 'container' }],
variables: []
},
{
components: ['timeseriescomponent'],
selectors: [{ selector: '.wm-timeseries', comment: 'Time series', customCssKey: 'container' }],
variables: []
},
{
components: ['conditionalwrapper'],
selectors: [
{ selector: '.wm-conditional-tabs', comment: 'Conditional tabs', customCssKey: 'container' }
],
variables: []
},
{
components: ['displaycomponent'],
selectors: [
{ selector: '.wm-rich-result-header', comment: 'Rich result header', customCssKey: 'header' },
{
selector: '.wm-rich-result-container',
comment: 'Rich result container',
customCssKey: 'container'
}
],
variables: []
},
{
components: ['mardowncomponent'],
selectors: [
{ selector: '.wm-markdown', comment: 'Markdown component', customCssKey: 'container' }
],
variables: []
},
{
components: ['schemaformcomponent'],
selectors: [
{ selector: '.wm-schema-form', comment: 'Schema form component', customCssKey: 'container' }
],
variables: []
},
{
components: ['selectstepcomponent'],
selectors: [{ selector: '.wm-select-step', comment: 'Select step', customCssKey: 'container' }],
variables: []
},
{
components: ['selectcomponent', 'resourceselectcomponent'],
selectors: [{ selector: '.svelte-select', comment: 'Svelte select', customCssKey: 'input' }],
variables: []
},
{
components: ['multiselectcomponent'],
selectors: [
{
selector: '.multiselect',
comment: 'top-level wrapper div'
},
{
selector: 'multiselect.open',
comment: 'top-level wrapper div when dropdown open'
},
{
selector: '.multiselect.disabled',
comment: 'top-level wrapper div when in disabled state'
},
{
selector: '.multiselect > ul.selected',
comment: 'selected list'
},
{
selector: '.multiselect > ul.selected > li',
comment: 'selected list items'
},
{
selector: '.multiselect button',
comment: 'target all buttons in this component'
},
{
selector: '.multiselect > ul.selected > li button, button.remove-all',
comment: 'buttons to remove a single or all selected options at once'
},
{
selector: '.multiselect > input[autocomplete]',
comment: 'input inside the top-level wrapper div'
},
{
selector: '.multiselect > ul.options',
comment: 'dropdown options'
},
{
selector: '.multiselect > ul.options > li',
comment: 'dropdown list items'
},
{
selector: '.multiselect > ul.options > li.selected',
comment: 'selected options in the dropdown list'
},
{
selector: '.multiselect > ul.options > li:not(.selected):hover',
comment: 'unselected but hovered options in the dropdown list'
},
{
selector: '.multiselect > ul.options > li.active',
comment:
'active item, navigated to with up/down arrow keys and ready to be selected by pressing enter'
},
{
selector: '.multiselect > ul.options > li.disabled',
comment: 'options with disabled key set to true'
}
],
variables: [
{
variable: '--sms-border',
value: '1pt solid lightgray',
comment:
'Change this to e.g. to 1px solid red to indicate this form field is in an invalid state.'
},
{ variable: '--sms-border-radius', value: '3pt' },
{ variable: '--sms-padding', value: '0 3pt' },
{ variable: '--sms-bg', value: '' },
{ variable: '--sms-text-color', value: '' },
{ variable: '--sms-min-height', value: '22pt' },
{ variable: '--sms-width', value: '' },
{ variable: '--sms-max-width', value: '' },
{ variable: '--sms-margin', value: '' },
{ variable: '--sms-font-size', value: 'inherit' },
{
variable: '--sms-open-z-index',
value: '4',
comment:
'Increase this if needed to ensure the dropdown list is displayed atop all other page elements.'
},
{
variable: '--sms-focus-border',
value: '1pt solid var(--sms-active-color, cornflowerblue)',
comment:
'Border when component has focus. Defaults to --sms-active-color which in turn defaults to cornflowerblue.'
},
{
variable: '--sms-disabled-bg',
value: 'lightgray',
comment: 'Background when in disabled state.'
},
{ variable: '--sms-placeholder-color', value: '' },
{ variable: '--sms-placeholder-opacity', value: '' },
{
variable: '--sms-selected-bg',
value: 'rgba(0, 0, 0, 0.15)',
comment: 'Background of selected options.'
},
{
variable: '--sms-selected-li-padding',
value: '1pt 5pt',
comment: 'Height of selected options.'
},
{
variable: '--sms-selected-text-color',
value: 'var(--sms-text-color)',
comment: 'Text color for selected options.'
},
{
variable: '--sms-remove-btn-hover-color',
value: 'lightskyblue',
comment:
'Color of the remove-icon buttons for removing all or individual selected options when in :focus or :hover state.'
},
{
variable: '--sms-remove-btn-hover-bg',
value: 'rgba(0, 0, 0, 0.2)',
comment: 'Background for hovered remove buttons.'
},
{ variable: '--sms-options-bg', value: 'white', comment: 'Background of dropdown list.' },
{
variable: '--sms-options-max-height',
value: '50vh',
comment: 'Maximum height of options dropdown.'
},
{
variable: '--sms-options-overscroll',
value: 'none',
comment:
'Whether scroll events bubble to parent elements when reaching the top/bottom of the options dropdown. See MDN.'
},
{
variable: '--sms-options-shadow',
value: '0 0 14pt -8pt black',
comment: 'Box shadow of dropdown list.'
},
{ variable: '--sms-options-border', value: '' },
{ variable: '--sms-options-border-width', value: '' },
{ variable: '--sms-options-border-radius', value: '1ex' },
{ variable: '--sms-options-padding', value: '' },
{ variable: '--sms-options-margin', value: 'inherit' },
{
variable: '--sms-options-scroll-margin',
value: '100px',
comment:
'Top/bottom margin to keep between dropdown list items and top/bottom screen edge when auto-scrolling list to keep items in view.'
},
{
variable: '--sms-li-selected-bg',
comment: 'Background of selected list items in options pane.',
value: ''
},
{
variable: '--sms-li-selected-color',
comment: 'Text color of selected list items in options pane.',
value: ''
},
{
variable: '--sms-li-active-bg',
value: 'var(--sms-active-color, rgba(0, 0, 0, 0.15))',
comment:
'Background of active options. Options in the dropdown list become active either by mouseover or by navigating to them with arrow keys. Selected options become active when selectedOptionsDraggable=true and an option is being dragged to a new position. Note the active option in that case is not the dragged option but the option under it whose place it will take on drag end.'
},
{
variable: '--sms-li-disabled-bg',
value: '#f5f5f6',
comment: 'Background of disabled options in the dropdown list.'
},
{
variable: '--sms-li-disabled-text',
value: '#b8b8b8',
comment: 'Text color of disabled option in the dropdown list.'
}
]
}
]
export const allClasses = customisationByComponent
.map((c) => c.selectors.map((x) => x.selector))
.flat()
export function hasStyleValue(obj: ComponentCssProperty | undefined) {
if (!obj) return false
return obj.style !== ''
}
@@ -0,0 +1,94 @@
import { ResourceService, AppService } from '$lib/gen'
import { sendUserToast } from '$lib/toast'
import type { AppTheme } from '../../types'
export interface Theme {
path: string
value: {
value?: string | undefined
name: string
}
}
export const DEFAULT_THEME: string = 'f/app_themes/theme_0'
export function createTheme(workspace: string, theme: Theme): Promise<string> {
const createThemeRequest = {
workspace,
requestBody: {
...theme,
resource_type: 'app_theme',
value: theme.value || ''
}
}
return ResourceService.createResource(createThemeRequest)
}
export async function getTheme(
workspace: string,
path: string
): Promise<{
value?: string | undefined
name: string
}> {
try {
return AppService.getPublicResource({
workspace,
path
})
} catch (e) {
sendUserToast(`Theme not found ${path}`)
return {
value: '',
name: 'Not found'
}
}
}
export function updateTheme(workspace: string, path: string, updatedTheme: any): Promise<string> {
const updateThemeRequest = {
workspace,
path,
requestBody: updatedTheme
}
return ResourceService.updateResource(updateThemeRequest)
}
export function deleteTheme(workspace: string, path: string): Promise<string> {
const deleteThemeRequest = {
workspace,
path: path
}
return ResourceService.deleteResource(deleteThemeRequest)
}
export async function listThemes(workspace: string): Promise<
Array<{
name: string
path: string
}>
> {
const listThemesRequest = {
workspace,
name: 'app_theme'
}
return ResourceService.listResourceNames(listThemesRequest)
}
export async function resolveTheme(
theme: AppTheme | undefined,
workspace: string | undefined
): Promise<string> {
let css = ''
if (theme?.type === 'inlined') {
css = theme.css
} else if (theme?.type === 'path' && theme.path && workspace) {
let loadedCss = await ResourceService.getResourceValue({
workspace: workspace,
path: theme.path
})
css = loadedCss.value ?? ''
}
return css
}
@@ -0,0 +1,300 @@
<script lang="ts">
import { getContext } from 'svelte'
import type { AppViewerContext, ComponentCssProperty } from '../../types'
import { ccomponents, type AppComponent } from '../component'
import Badge from '$lib/components/common/badge/Badge.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { twMerge } from 'tailwind-merge'
import { fade } from 'svelte/transition'
import css from 'svelte-highlight/languages/css'
import { Highlight } from 'svelte-highlight'
import { MoveRight } from 'lucide-svelte'
import { customisationByComponent } from '../componentsPanel/cssUtils'
import { sendUserToast } from '$lib/toast'
import CloseButton from '$lib/components/common/CloseButton.svelte'
export let component: AppComponent | undefined
const { app } = getContext<AppViewerContext>('AppViewerContext')
function fadeFast(node: HTMLElement) {
return fade(node, { duration: 100 })
}
let migrationModalOpen: boolean = false
export function open() {
migrationModalOpen = true
}
function generateCodeFromMigrations(migrations: Map<string, string[]>) {
let code = ''
for (const [key, value] of migrations) {
code += `${key} {\n\t${value.join(';\n\t')}\n}\n\n`
}
return code
}
$: generatedCode = generateCodeFromMigrations(migrations)
$: migrations = new Map<string, string[]>()
function getSelector(key: string) {
return customisationByComponent
.find((c) => c.components.includes(component?.type ?? ''))
?.selectors.find((s) => {
return s.customCssKey === key
})?.selector
}
function setOrUpdateMigration(key: string, value: string) {
const selector = getSelector(key)
if (!selector) {
return
}
if (migrations.has(selector)) {
const arr = migrations.get(selector)
if (arr) {
arr.push(value)
migrations.set(selector, arr)
}
} else {
migrations.set(selector, [value])
}
migrations = migrations
}
function appendMigrationsToCss(migrations: Map<string, string[]>) {
const theme = $app.theme
if (theme?.type === 'path') {
sendUserToast('Cannot migrate to CSS editor when using a theme by path', true)
return
} else if (theme?.type === 'inlined') {
let cssString = theme.css
if (!cssString) {
cssString = ''
}
for (const [key, value] of migrations) {
if (cssString.includes(`${key} {` || `${key}{`)) {
// append value to existing value
const regex = new RegExp(`\\${key}\\s*{\\s*([\\s\\S]*?)\\s*}`, 'g')
const match = regex.exec(cssString)
if (match) {
const existingValue = match[1]
cssString = cssString.replace(
regex,
`${key} {\n\t${existingValue}\n\t${value.join('\n\t')}\n}`
)
}
} else {
const firstBreakline = cssString === '' ? '' : '\n\n'
// append key and value
cssString += `${firstBreakline}${key} {\n\t${value.join('\n\t')}\n}\n\n`
}
}
theme.css = cssString
$app.theme = theme
}
}
function hasStyles(customCss: Record<string, ComponentCssProperty> | undefined) {
if (!customCss) {
return false
}
return Object.keys(customCss ?? {})
.map((key) => customCss[key])
.some((c) => c.style !== '')
}
let type: string | undefined = component?.type
</script>
{#if migrationModalOpen}
<div
transition:fadeFast|local
class={'absolute top-0 bottom-0 left-0 right-0 z-[5000]'}
role="dialog"
>
<div
class={twMerge(
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
migrationModalOpen ? 'ease-out duration-300 opacity-100' : 'ease-in duration-200 opacity-0'
)}
/>
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<div
class={twMerge(
'relative transform overflow-hidden rounded-lg bg-surface px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-5xl sm:p-6 ',
migrationModalOpen
? 'ease-out duration-300 opacity-100 translate-y-0 sm:scale-100'
: 'ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
)}
>
<div class="leading-6 font-semibold text-sm w-full flex justify-between">
<div>Migrate to CSS editor</div><CloseButton
on:close={() => (migrationModalOpen = false)}
/>
</div>
<div class="">
<div class="">
{#if hasStyles(component?.customCss)}
<div class="leading-6 text-xs font-semibold">
ID <Badge color="indigo" size="xs">
{component?.id}
</Badge>
</div>
{/if}
{#if component?.type && $app.css}
{#each Object.keys(component.customCss ?? {}) as cssKey}
{#if component.customCss?.[cssKey].style != undefined && component.customCss[cssKey].style !== ''}
<div class="grid grid-cols-2 gap-2">
<div>
<div class="flex flex-row justify-between items-center py-0.5">
<div class="leading-6 text-xs font-semibold">
<Badge>
{cssKey}
</Badge>
</div>
<Button
color="dark"
size="xs"
on:click={() => {
if (component?.customCss?.[cssKey]?.style != undefined) {
setOrUpdateMigration(
cssKey,
component.customCss[cssKey].style ?? ''
)
component.customCss[cssKey].style = ''
}
}}
>
<div class="flex flex-row gap-2 items-center">
Migrate
<MoveRight size={16} />
</div>
</Button>
</div>
<div class="border p-2 rounded-md">
<Highlight code={component.customCss[cssKey].style} language={css} />
</div>
</div>
<div class="">
<div class="leading-6 text-xs font-semibold my-1">Preview</div>
<div class="border rounded-md p-2">
<Highlight
code={`${getSelector(cssKey)} {\n\t${
component.customCss[cssKey].style
}\n}`}
language={css}
/>
</div>
</div>
</div>
{/if}
{/each}
{/if}
{#if hasStyles(component?.type ? $app.css?.[component?.type] : undefined)}
<div class="leading-6 text-xs font-semibold">
Global: {component?.type ? ccomponents[component.type]?.name : ''}
</div>
{/if}
{#if component?.type && $app.css}
{#each Object.keys($app.css[component?.type] ?? {}) as cssKey}
{#if type && $app.css?.[type]?.[cssKey].style != undefined && $app.css[type]?.[cssKey].style !== ''}
<div class="grid grid-cols-2 gap-2">
<div>
<div class="flex flex-row justify-between items-center py-0.5">
<div class="leading-6 text-xs font-semibold">
<Badge>
{cssKey}
</Badge>
</div>
<Button
color="dark"
size="xs"
on:click={() => {
if (type && $app.css?.[type]) {
setOrUpdateMigration(cssKey, $app.css[type][cssKey].style)
$app.css[type][cssKey].style = ''
}
}}
>
<div class="flex flex-row gap-2 items-center">
Migrate
<MoveRight size={16} />
</div>
</Button>
</div>
<div class="border p-2 rounded-md">
<Highlight code={$app.css[type][cssKey].style} language={css} />
</div>
</div>
<div class="">
<div class="leading-6 text-xs font-semibold my-1">Preview</div>
<div class="border rounded-md p-2">
<Highlight
code={`${getSelector(cssKey)} {\n\t${$app.css[type][cssKey].style}\n}`}
language={css}
/>
</div>
</div>
</div>
{/if}
{/each}
{/if}
</div>
</div>
<div>
<div class="leading-6 text-xs font-semibold my-1">Current migrations</div>
{#if migrations.size > 0}
<div class="border rounded-md p-2">
<Highlight code={generatedCode} language={css} />
</div>
{:else}
<div class="text-gray-500 text-xs">No migrations</div>
{/if}
</div>
<div class="mt-2 flex flex-row justify-end items-center gap-2">
<div class="text-xs">
If the class is already present in the CSS editor, the migration will append the new
values to the existing ones.
</div>
<Button
size="xs"
color="dark"
on:click={() => {
appendMigrationsToCss(migrations)
migrationModalOpen = false
}}
disabled={migrations.size === 0}
>
<div class="flex flex-row gap-2 items-center">
Apply migration
<MoveRight size={16} />
</div>
</Button>
</div>
</div>
</div>
</div>
</div>
{/if}
@@ -1,6 +1,6 @@
<script lang="ts">
import Button from '$lib/components/common/button/Button.svelte'
import { faChevronRight } from '@fortawesome/free-solid-svg-icons'
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'
import { getContext } from 'svelte'
import type { AppEditorContext, AppViewerContext, GridItem, RichConfiguration } from '../../types'
import PanelSection from './common/PanelSection.svelte'
@@ -24,7 +24,6 @@
import { slide } from 'svelte/transition'
import { push } from '$lib/history'
import Kbd from '$lib/components/common/kbd/Kbd.svelte'
import { secondaryMenu } from './secondaryMenu'
import StylePanel from './StylePanel.svelte'
import { Delete, ExternalLink } from 'lucide-svelte'
import GridCondition from './GridCondition.svelte'
@@ -33,6 +32,7 @@
import EvalV2InputEditor from './inputEditor/EvalV2InputEditor.svelte'
import type { ResultAppInput } from '../../inputType'
import GridGroup from './GridGroup.svelte'
import { secondaryMenuLeft } from './secondaryMenu'
export let componentSettings: { item: GridItem; parent: string | undefined } | undefined =
undefined
@@ -361,18 +361,10 @@
color="light"
size="xs"
variant="border"
on:click={() => (viewCssOptions = !viewCssOptions)}
startIcon={{ icon: faChevronLeft }}
on:click={() => secondaryMenuLeft.toggle(StylePanel, {})}
>
{viewCssOptions ? 'Hide' : 'Show'}
</Button>
<Button
color="light"
size="xs"
variant="border"
endIcon={{ icon: faChevronRight }}
on:click={() => secondaryMenu.open(StylePanel, { component })}
>
Rich Editor
Show
</Button>
</div>
<AlignmentEditor bind:component={componentSettings.item.data} />
@@ -0,0 +1,33 @@
<script lang="ts">
import type { ComponentCssProperty } from '../../types'
import CssProperty from '../componentsPanel/CssProperty.svelte'
export let forceStyle: boolean = false
export let forceClass: boolean = false
export let id: string
export let property: ComponentCssProperty | undefined = undefined
export let overriden: boolean = false
export let overridding: boolean = false
export let wmClass: string | undefined = undefined
function hasValues(obj: ComponentCssProperty | undefined) {
if (!obj) return false
return Object.values(obj).some((v) => v !== '')
}
</script>
{#if property}
<CssProperty
{forceStyle}
{forceClass}
name={id}
bind:value={property[id]}
shouldDisplayLeft={hasValues(property[id])}
on:left
on:right
{overriden}
{overridding}
{wmClass}
/>
{/if}
@@ -32,6 +32,7 @@
export let placeholder: string | undefined
export let customTitle: string | undefined = undefined
export let displayType: boolean = false
export let allowTypeChange: boolean = true
const { connectingInput, app } = getContext<AppViewerContext>('AppViewerContext')
@@ -62,18 +63,20 @@
<div class={classNames('flex gap-1', 'flex-col')}>
<div class="flex justify-between items-end">
<div class="flex flex-row gap-4 items-center">
<span class="text-xs font-semibold truncate text-primary">
{customTitle
? customTitle
: shouldCapitalize
? capitalize(addWhitespaceBeforeCapitals(key))
: key}
<div class="flex items-center">
<span class="text-xs font-semibold truncate text-primary">
{customTitle
? customTitle
: shouldCapitalize
? capitalize(addWhitespaceBeforeCapitals(key))
: key}
</span>
{#if tooltip}
<Tooltip>
<Tooltip small>
{tooltip}
</Tooltip>
{/if}
</span>
</div>
{#if displayType}
<div class="text-xs text-tertiary mr-1">
{fieldType === 'array' && subFieldType
@@ -84,7 +87,7 @@
</div>
<div class={classNames('flex gap-x-2 gap-y-1 justify-end items-center')}>
{#if componentInput?.type}
{#if componentInput?.type && allowTypeChange}
<ToggleButtonGroup
class="h-7"
bind:selected={componentInput.type}
@@ -1,71 +1,276 @@
<script lang="ts">
import { Button } from '$lib/components/common'
import { Tab, TabContent } from '$lib/components/common'
import { sendUserToast } from '$lib/toast'
import { Copy } from 'lucide-svelte'
import { getContext } from 'svelte'
import type { AppViewerContext } from '../../types'
import { ccomponents, type AppComponent } from '../component'
import type { AppViewerContext, ComponentCssProperty } from '../../types'
import { ccomponents, components } from '../component'
import CssProperty from '../componentsPanel/CssProperty.svelte'
import { quickStyleProperties } from '../componentsPanel/quickStyleProperties'
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { enterpriseLicense } from '$lib/stores'
import Tooltip from '$lib/components/Tooltip.svelte'
import { customisationByComponent, hasStyleValue } from '../componentsPanel/cssUtils'
import CssMigrationModal from './CSSMigrationModal.svelte'
import CssPropertyWrapper from './CssPropertyWrapper.svelte'
import { onMount } from 'svelte'
import { findComponentSettings } from '../appUtils'
export let component: AppComponent | undefined
const { app } = getContext<AppViewerContext>('AppViewerContext')
const { app, cssEditorOpen, selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
function applyToAllInstances() {
if (component) {
if (!$app.css) {
$app.css = {}
}
let componentType = component?.type
if (!$app.css![componentType]) {
$app.css![componentType] = {}
}
Object.keys(ccomponents[component.type].customCss ?? {}).forEach((name) => {
if (!$app.css![componentType]![name]) {
$app.css![componentType]![name] = {}
$: component = findComponentSettings($app, $selectedComponent?.[0])?.item?.data
let tab: 'local' | 'global' = 'local'
let overrideGlobalCSS: (() => void) | undefined = undefined
let overrideLocalCSS: (() => void) | undefined = undefined
let type = component?.type
let migrationModal: CssMigrationModal | undefined = undefined
$: customCssByComponentType =
component?.type && $app.css
? Object.entries($app.css[component.type] || {}).map(([id, v]) => ({
id,
forceStyle: v?.style != undefined,
forceClass: v?.['class'] != undefined
}))
: undefined
function copyLocalToGlobal(name: string, value: ComponentCssProperty | undefined) {
if (!value) {
sendUserToast('No local CSS to copy')
} else {
const type = component?.type
if (!type) return
if (hasStyleValue($app.css?.[type]?.[name])) {
overrideGlobalCSS = () => {
$app.css![type]![name] = JSON.parse(JSON.stringify(value))
app.set($app)
}
if (component) {
let nstyle = component.customCss![name]
if (nstyle.style) {
$app.css![componentType]![name].style = nstyle.style
}
if (nstyle.class) {
$app.css![componentType]![name].class = nstyle.class
}
} else {
if (!$app.css![type]) {
initGlobalCss()
}
})
sendUserToast(
`Applied style to all instances of the ${componentType.replace('component', '')} component`
)
$app.css![type]![name] = JSON.parse(JSON.stringify(value))
app.set($app)
sendUserToast('Global CSS copied')
}
}
}
function copyGlobalToLocal(id: string, value: any) {
if (!value) {
sendUserToast('No global CSS to copy')
} else {
if (hasStyleValue(value)) {
overrideLocalCSS = () => {
component!.customCss![id] = JSON.parse(JSON.stringify(value))
app.set($app)
}
} else {
component!.customCss![id] = JSON.parse(JSON.stringify(value))
app.set($app)
sendUserToast('Local CSS copied')
}
}
}
function initGlobalCss() {
// If the global css is not initialised, we initialise it.
// Should only happen once per app
if (!$app.css) {
$app.css = {}
}
// If the global css for this component type is not initialised, we initialise it.
// Should only happen once per component type
if (
$app.css &&
component &&
!$app.css[component.type]?.style &&
components[component.type] &&
$app.css[component.type] === undefined
) {
$app.css[component.type] = JSON.parse(JSON.stringify(components[component.type].customCss))
app.set($app)
}
}
function getSelector(key: string) {
return customisationByComponent
.find((c) => c.components.includes(component?.type ?? ''))
?.selectors.find((s) => {
return s.customCssKey === key
})?.selector
}
onMount(() => {
initGlobalCss()
})
</script>
<Button
variant="border"
color="light"
size="xs"
aria-label="Apply to all instances of this component"
btnClasses="ml-3 mt-2"
on:click={applyToAllInstances}
>
Copy style to global CSS &nbsp;<Copy size={18} />
</Button>
{#if component}
<div class="px-2 flex items-center gap-2 flex-row justify-between">
{#if !cssEditorOpen}
<Button
color="blue"
size="xs2"
variant="border"
on:click={() => {
$cssEditorOpen = true
}}
>
<div class="flex flex-row gap-1 text-xs items-center">
Open CSS editor{$enterpriseLicense === undefined ? ' (EE only)' : ''}
<Tooltip light>
You can also use the App CSS Editor to customise the CSS of all components.
</Tooltip>
</div>
</Button>
{:else}
<div />
{/if}
{#if component && component.customCss !== undefined}
{#each Object.keys(ccomponents[component.type].customCss ?? {}) as name}
<div class="w-full">
<CssProperty
quickStyleProperties={quickStyleProperties?.[component.type]?.[name]}
forceStyle={ccomponents[component.type].customCss[name].style !== undefined}
forceClass={ccomponents[component.type].customCss[name].class !== undefined}
tooltip={ccomponents[component.type].customCss[name].tooltip}
{name}
componentType={component.type}
bind:value={component.customCss[name]}
on:change={() => app.set($app)}
/>
<div class="flex flex-row gap-2 items-center justify-between">
{#if $enterpriseLicense !== undefined}
<Button
color="dark"
size="xs2"
on:click={() => {
migrationModal?.open()
}}
>
Convert to global CSS
</Button>
{/if}
</div>
{/each}
</div>
<Tabs bind:selected={tab}>
<Tab value="local" size="xs">
<div class="flex flex-row gap-2 items-center">
ID
<Badge color="indigo" size="xs">
{component?.id}
</Badge>
<Tooltip light>
You can customise the CSS and the classes of this component instance. Theses
customisations will only be applied to this component.
</Tooltip>
</div>
</Tab>
<Tab value="global" size="xs">
<div class="flex flex-row gap-2 items-center">
Global: {type ? ccomponents[type].name : ''}
<Tooltip light>
You can customise the CSS and the classes of all components of this type. Theses
customisations will be applied to all components of this type.
</Tooltip>
</div>
</Tab>
<svelte:fragment slot="content">
<TabContent value="local">
{#if component && component.customCss !== undefined}
{#each Object.keys(ccomponents[component.type].customCss ?? {}) as name}
<div class="w-full">
<CssProperty
quickStyleProperties={quickStyleProperties?.[component.type]?.[name]}
forceStyle={ccomponents[component.type].customCss[name].style !== undefined}
forceClass={ccomponents[component.type].customCss[name].class !== undefined}
tooltip={ccomponents[component.type].customCss[name].tooltip}
{name}
wmClass={getSelector(name)}
componentType={component.type}
bind:value={component.customCss[name]}
on:change={() => app.set($app)}
shouldDisplayRight={hasStyleValue(component.customCss[name])}
on:right={() => {
copyLocalToGlobal(name, component?.customCss?.[name])
tab = 'global'
}}
overridding={hasStyleValue($app.css?.[component.type]?.[name]) &&
hasStyleValue(component.customCss[name])}
/>
</div>
{/each}
{/if}
</TabContent>
<TabContent value="global">
{#if type}
{#each customCssByComponentType ?? [] as { id, forceStyle, forceClass }}
<div class="w-full">
{#if $app.css && type && $app.css[type] && component?.customCss}
<CssPropertyWrapper
{forceStyle}
{forceClass}
{id}
bind:property={$app.css[type]}
on:left={() => {
copyGlobalToLocal(
id,
component?.type ? $app?.css?.[component?.type]?.[id] : undefined
)
tab = 'local'
}}
overriden={hasStyleValue(component.customCss[id])}
wmClass={getSelector(id)}
/>
{/if}
</div>
{/each}
{/if}
</TabContent>
</svelte:fragment>
</Tabs>
<ConfirmationModal
title="Confirm overriding global CSS"
confirmationText="Override global CSS"
open={Boolean(overrideGlobalCSS)}
on:confirmed={() => {
if (overrideGlobalCSS) {
overrideGlobalCSS()
overrideGlobalCSS = undefined
}
sendUserToast('Global CSS overridden')
}}
on:canceled={() => {
overrideGlobalCSS = undefined
}}
>
<div class="text-primary pb-2">
The global CSS for this component already exists. Do you want to override it?
</div>
</ConfirmationModal>
<ConfirmationModal
title="Confirm overriding local CSS"
confirmationText="Override local CSS"
open={Boolean(overrideLocalCSS)}
on:confirmed={() => {
if (overrideLocalCSS) {
overrideLocalCSS()
overrideLocalCSS = undefined
}
sendUserToast('Local CSS overridden')
}}
on:canceled={() => {
overrideLocalCSS = undefined
}}
>
<div class="text-primary pb-2">
The local CSS for this component already exists. Do you want to override it?
</div>
</ConfirmationModal>
<CssMigrationModal bind:this={migrationModal} bind:component />
{:else}
<span class="text-sm text-gray-600 mx-2">Select a component to style it in this panel</span>
{/if}
@@ -1,16 +1,19 @@
<script lang="ts">
import { fly } from 'svelte/transition'
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'
import { Badge, Button } from '../../../../common'
import { secondaryMenu, SECONDARY_MENU_ID } from './'
import { Badge } from '../../../../common'
import { secondaryMenuLeft, secondaryMenuRight } from './'
import { getContext } from 'svelte'
import type { AppViewerContext } from '../../../types'
import CloseButton from '$lib/components/common/CloseButton.svelte'
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
export let right: boolean
let secondaryMenu = right ? secondaryMenuRight : secondaryMenuLeft
let width: number
let lastSelected = $selectedComponent
$: if (lastSelected !== $selectedComponent) {
$: if (right && lastSelected !== $selectedComponent) {
secondaryMenu.close()
lastSelected = $selectedComponent
}
@@ -19,27 +22,23 @@
<!-- z-index must be above the split pane handles' z-index (which is 1001 atm.) -->
<div
bind:clientWidth={width}
class="absolute z-[1002] inset-0 overflow-hidden"
class="absolute z-[1002] inset-0 overflow-hidden w-full"
class:pointer-events-none={!$secondaryMenu.isOpen}
>
{#if $secondaryMenu.isOpen && $secondaryMenu.component}
<div
transition:fly|local={{ duration: 300, x: width, y: 0, opacity: 1 }}
id={SECONDARY_MENU_ID}
transition:fly|local={{ duration: 300, x: right ? width : -width, y: 0, opacity: 1 }}
class="flex flex-col w-full h-full bg-surface"
>
<div class="flex justify-between items-center gap-1 px-3 py-2">
<Button
color="light"
size="xs2"
spacingSize="xs"
variant="border"
startIcon={{ icon: faChevronLeft }}
on:click={secondaryMenu.close}
>
Back
</Button>
<Badge color="blue">{$selectedComponent}</Badge>
<div
class="flex justify-between {right ? '' : 'flex-row-reverse'} items-center gap-1 px-3 py-2"
>
<CloseButton on:close={() => secondaryMenu?.close()} />
{#if $selectedComponent}
<Badge color="blue">{$selectedComponent}</Badge>
{:else}
<div />
{/if}
</div>
<div class="relative h-full overflow-y-auto">
{#if typeof $secondaryMenu.component === 'string'}
@@ -1,6 +1,4 @@
import { writable } from 'svelte/store'
export const SECONDARY_MENU_ID = 'app-secondary-menu' as const
import { writable, type Writable } from 'svelte/store'
export interface SecondaryMenuStore {
isOpen: boolean
@@ -9,21 +7,42 @@ export interface SecondaryMenuStore {
onClose?: (() => void) | undefined
}
const store = writable<SecondaryMenuStore>({ isOpen: false, component: undefined, props: {} })
export const secondaryMenuRightStore = writable<SecondaryMenuStore>({
isOpen: false,
component: undefined,
props: {}
})
export const secondaryMenuLeftStore = writable<SecondaryMenuStore>({
isOpen: false,
component: undefined,
props: {}
})
export const secondaryMenu = {
subscribe: store.subscribe,
open: (
component: SecondaryMenuStore['component'],
props: SecondaryMenuStore['props'] = {},
onClose: (() => void) | undefined = undefined
) => {
store.set({ isOpen: true, component, props, onClose })
},
close: () => {
store.update((state) => {
if (state.onClose) state.onClose()
return { isOpen: false, component: undefined, props: {} }
})
}
} as const
export const secondaryMenuRight = secondaryMenuController(secondaryMenuRightStore)
export const secondaryMenuLeft = secondaryMenuController(secondaryMenuLeftStore)
export function secondaryMenuController(store: Writable<SecondaryMenuStore>) {
return {
subscribe: store.subscribe,
toggle: (
component: SecondaryMenuStore['component'],
props: SecondaryMenuStore['props'] = {},
onClose: (() => void) | undefined = undefined
) => {
store.update((str) => ({ isOpen: !str.isOpen, component, props, onClose }))
},
open: (
component: SecondaryMenuStore['component'],
props: SecondaryMenuStore['props'] = {},
onClose: (() => void) | undefined = undefined
) => {
store.set({ isOpen: true, component, props, onClose })
},
close: () => {
store.update((state) => {
if (state.onClose) state.onClose()
return { isOpen: false, component: undefined, props: {} }
})
}
} as const
}
+14
View File
@@ -43,6 +43,7 @@ export interface GeneralAppInput {
export type ComponentCssProperty = {
class?: string
style?: string
evalClass?: RichConfiguration
}
export type ComponentCustomCSS<T extends keyof typeof components> = Partial<
@@ -129,6 +130,16 @@ export type HiddenRunnable = {
} & Runnable &
RecomputeOthersSource
export type AppTheme =
| {
type: 'path'
path: string
}
| {
type: 'inlined'
css: string
}
export type App = {
grid: GridItem[]
fullscreen: boolean
@@ -141,6 +152,7 @@ export type App = {
hiddenInlineScripts: Array<HiddenRunnable>
css?: Partial<Record<AppCssItemName, Record<string, ComponentCssProperty>>>
subgrids?: Record<string, GridItem[]>
theme: AppTheme | undefined
}
export type ConnectingInput = {
@@ -228,6 +240,8 @@ export type AppViewerContext = {
hoverStore: Writable<string | undefined>
allIdsInPath: Writable<string[]>
darkMode: Writable<boolean>
cssEditorOpen: Writable<boolean>
previewTheme: Writable<string | undefined>
}
export type AppEditorContext = {
+5 -4
View File
@@ -233,11 +233,11 @@ export function toKebabCase(text: string) {
return text.replace(/[A-Z]+(?![a-z])|[A-Z]/g, ($, ofs) => (ofs ? '-' : '') + $.toLowerCase())
}
export function concatCustomCss<T extends Record<string, ComponentCssProperty>>(
export function initCss<T extends Record<string, ComponentCssProperty>>(
appCss?: Record<string, ComponentCssProperty>,
componentCss?: T
): T | undefined {
if (!componentCss) return undefined
): T {
if (!componentCss) return {} as T
return Object.fromEntries(
Object.entries(componentCss).map(([key, v]) => {
@@ -253,7 +253,8 @@ export function concatCustomCss<T extends Record<string, ComponentCssProperty>>(
key,
{
style: (appStyle + appEnding + compStyle + compEnding).trim(),
class: twMerge(appCss?.[key]?.class, v?.class)
class: twMerge(appCss?.[key]?.class, v?.class),
evalClass: appCss?.[key]?.evalClass || v?.evalClass
}
]
})
@@ -1,4 +1,5 @@
import type { Environment } from 'monaco-editor/esm/vs/editor/editor.api.js'
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
interface MonacoEnvironmentEnhanced extends Environment {
workerOverrideGlobals: WorkerOverrideGlobals
@@ -67,6 +68,7 @@ export function buildWorkerDefinition(
case 'razor':
return buildWorker(workerOverrideGlobals, label, 'htmlWorker', 'HTML Worker')
case 'css':
return new cssWorker()
case 'scss':
case 'less':
return buildWorker(workerOverrideGlobals, label, 'cssWorker', 'CSS Worker')
@@ -37,7 +37,8 @@
style={css?.popup?.style}
class={twMerge(
'bg-surface max-w-5xl m-24 overflow-y-auto rounded-lg relative',
css?.popup?.class
css?.popup?.class,
'wm-modal-form-popup'
)}
use:clickOutside
on:click_outside={() => {
@@ -51,7 +52,6 @@
on:click={() => {
isOpen = false
}}
style={css?.button?.style}
class="hover:bg-surface-hover bg-surface-secondary rounded-full w-8 h-8 flex items-center justify-center transition-all"
>
<X class="text-tertiary" />
+2
View File
@@ -67,6 +67,8 @@ export function langToExt(lang: string): string {
return 'ts'
case 'graphql':
return 'gql'
case 'css':
return 'css'
default:
return 'unknown'
@@ -10,6 +10,7 @@
import type { App } from '$lib/components/apps/types'
import { goto } from '$app/navigation'
import { sendUserToast } from '$lib/toast'
import { DEFAULT_THEME } from '$lib/components/apps/editor/componentsPanel/themeUtils'
let nodraft = $page.url.searchParams.get('nodraft')
const hubId = $page.url.searchParams.get('hub')
@@ -29,7 +30,10 @@
fullscreen: false,
unusedInlineScripts: [],
hiddenInlineScripts: [],
css: {}
theme: {
type: 'path',
path: DEFAULT_THEME
}
}
if (nodraft) {
@@ -93,7 +97,7 @@
fullscreen: false,
unusedInlineScripts: [],
hiddenInlineScripts: [],
css: {}
theme: undefined
}
}
}
@@ -29,7 +29,11 @@
{#if app}
{#key app}
<div
class={twMerge('min-h-screen h-full w-full', app?.value.css?.['app']?.['viewer']?.class)}
class={twMerge(
'min-h-screen h-full w-full',
app?.value.css?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value.css?.['app']?.['viewer']?.style}
>
<AppPreview
@@ -109,20 +109,22 @@
$: preFilteredType =
typeFilter == undefined
? preFilteredItemsOwners?.filter((x) =>
tab == 'states'
? x.resource_type == 'state'
: tab == 'cache'
? x.resource_type == 'cache'
: x.resource_type != 'state' && x.resource_type != 'cache'
tab === 'workspace'
? x.resource_type !== 'app_theme' && x.resource_type !== 'state'
: tab === 'states'
? x.resource_type === 'state'
: tab === 'cache'
? x.resource_type === 'cache'
: tab === 'theme'
? x.resource_type === 'app_theme'
: true
)
: preFilteredItemsOwners?.filter(
(x) =>
x.resource_type == typeFilter &&
(tab == 'states'
? x.resource_type == 'state'
: tab == 'cache'
? x.resource_type == 'cache'
: x.resource_type != 'state' && x.resource_type != 'cache')
x.resource_type === typeFilter &&
(tab === 'workspace'
? x.resource_type !== 'app_theme' && x.resource_type !== 'state'
: true)
)
async function loadResources(): Promise<void> {
@@ -137,6 +139,7 @@
...x
}
})
loading.resources = false
}
@@ -257,7 +260,7 @@
}
let disableCustomPrefix = false
let tab: 'workspace' | 'types' | 'states' | 'cache' = 'workspace'
let tab: 'workspace' | 'types' | 'states' | 'cache' | 'theme' = 'workspace'
let inferrer: Drawer | undefined = undefined
let inferrerJson = ''
@@ -472,8 +475,17 @@
</Tooltip>
</div>
</Tab>
<Tab size="md" value="theme">
<div class="flex gap-2 items-center my-1">
Theme
<Tooltip>
Theme are actually resources (but excluded from the Workspace tab for clarity). Theme are
used by the apps to customize their look and feel.
</Tooltip>
</div>
</Tab>
</Tabs>
{#if tab == 'workspace' || tab == 'states' || tab == 'cache'}
{#if tab == 'workspace' || tab == 'states' || tab == 'cache' || tab == 'theme'}
<div class="pt-2">
<input placeholder="Search Resource" bind:value={filter} class="input mt-1" />
</div>
@@ -6,8 +6,8 @@
import { Alert, Skeleton } from '$lib/components/common'
import { WindmillIcon } from '$lib/components/icons'
import { AppService, AppWithLastVersion } from '$lib/gen'
import { userStore } from '$lib/stores'
import { AppService, AppWithLastVersion, SettingsService } from '$lib/gen'
import { userStore, enterpriseLicense } from '$lib/stores'
import { twMerge } from 'tailwind-merge'
import { setContext } from 'svelte'
@@ -21,6 +21,7 @@
async function loadApp() {
try {
setLicense()
app = await AppService.getPublicAppBySecret({
workspace: $page.params.workspace,
path: $page.params.secret
@@ -35,6 +36,13 @@
}
const breakpoint = writable<EditorBreakpoint>('lg')
async function setLicense() {
const license = await SettingsService.getLicenseId()
if (license) {
$enterpriseLicense = license
}
}
</script>
<svelte:head>
@@ -57,7 +65,11 @@
{:else if app}
{#key app}
<div
class={twMerge('min-h-screen h-full w-full', app?.value.css?.['app']?.['viewer']?.class)}
class={twMerge(
'min-h-screen h-full w-full',
app?.value.css?.['app']?.['viewer']?.class,
'wm-app-viewer'
)}
style={app?.value.css?.['app']?.['viewer']?.style}
>
<AppPreview