diff --git a/web/public/fonts/jetbrains_mono/JetBrainsMono-Italic.woff2 b/web/public/fonts/jetbrains_mono/JetBrainsMono-Italic.woff2
new file mode 100644
index 0000000000..d60c270e8c
Binary files /dev/null and b/web/public/fonts/jetbrains_mono/JetBrainsMono-Italic.woff2 differ
diff --git a/web/public/fonts/jetbrains_mono/JetBrainsMono-Medium.woff2 b/web/public/fonts/jetbrains_mono/JetBrainsMono-Medium.woff2
new file mode 100644
index 0000000000..669d04cdf2
Binary files /dev/null and b/web/public/fonts/jetbrains_mono/JetBrainsMono-Medium.woff2 differ
diff --git a/web/public/fonts/jetbrains_mono/JetBrainsMono-MediumItalic.woff2 b/web/public/fonts/jetbrains_mono/JetBrainsMono-MediumItalic.woff2
new file mode 100644
index 0000000000..80cfd15e00
Binary files /dev/null and b/web/public/fonts/jetbrains_mono/JetBrainsMono-MediumItalic.woff2 differ
diff --git a/web/public/fonts/jetbrains_mono/JetBrainsMono-Regular.woff2 b/web/public/fonts/jetbrains_mono/JetBrainsMono-Regular.woff2
new file mode 100644
index 0000000000..40da427651
Binary files /dev/null and b/web/public/fonts/jetbrains_mono/JetBrainsMono-Regular.woff2 differ
diff --git a/web/public/fonts/jetbrains_mono/JetBrainsMono-SemiBold.woff2 b/web/public/fonts/jetbrains_mono/JetBrainsMono-SemiBold.woff2
new file mode 100644
index 0000000000..5ead7b0d6f
Binary files /dev/null and b/web/public/fonts/jetbrains_mono/JetBrainsMono-SemiBold.woff2 differ
diff --git a/web/public/fonts/jetbrains_mono/JetBrainsMono-SemiBoldItalic.woff2 b/web/public/fonts/jetbrains_mono/JetBrainsMono-SemiBoldItalic.woff2
new file mode 100644
index 0000000000..c5dd294b47
Binary files /dev/null and b/web/public/fonts/jetbrains_mono/JetBrainsMono-SemiBoldItalic.woff2 differ
diff --git a/web/src/pages/PlaygroundPage/PlaygroundPage.tsx b/web/src/pages/PlaygroundPage/PlaygroundPage.tsx
new file mode 100644
index 0000000000..4ebd77d138
--- /dev/null
+++ b/web/src/pages/PlaygroundPage/PlaygroundPage.tsx
@@ -0,0 +1,93 @@
+import { m } from '../../paraglide/messages';
+import { Card } from '../../shared/components/Card/Card';
+import { CodeCard } from '../../shared/defguard-ui/components/CodeCard/CodeCard';
+import { Divider } from '../../shared/defguard-ui/components/Divider/Divider';
+import { ThemeSpacing } from '../../shared/defguard-ui/types';
+import './style.scss';
+import { Controls } from '../../shared/components/Controls/Controls';
+import { LoadingStep } from '../../shared/components/LoadingStep/LoadingStep';
+import { ActionableSection } from '../../shared/defguard-ui/components/ActionableSection/ActionableSection';
+import { ActionableSectionVariant } from '../../shared/defguard-ui/components/ActionableSection/types';
+import { Button } from '../../shared/defguard-ui/components/Button/Button';
+import { SizedBox } from '../../shared/defguard-ui/components/SizedBox/SizedBox';
+import testIconSrc from './assets/actionable-test1.png';
+
+export const PlaygroundPage = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const LoadingStepsTest = () => {
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
diff --git a/web/src/pages/PlaygroundPage/assets/actionable-test1.png b/web/src/pages/PlaygroundPage/assets/actionable-test1.png
new file mode 100644
index 0000000000..7b5e9c03b1
Binary files /dev/null and b/web/src/pages/PlaygroundPage/assets/actionable-test1.png differ
diff --git a/web/src/pages/PlaygroundPage/style.scss b/web/src/pages/PlaygroundPage/style.scss
new file mode 100644
index 0000000000..0dbdf52dc1
--- /dev/null
+++ b/web/src/pages/PlaygroundPage/style.scss
@@ -0,0 +1,15 @@
+#playground-page {
+ width: 100%;
+ min-height: 100dvh;
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ justify-content: flex-start;
+ row-gap: var(--spacing-2xl);
+ box-sizing: border-box;
+ padding: var(--spacing-4xl);
+
+ .card {
+ max-width: 670px;
+ }
+}
diff --git a/web/src/pages/UsersOverviewPage/UsersTable.tsx b/web/src/pages/UsersOverviewPage/UsersTable.tsx
index cd85f1bf74..ea9b3f5099 100644
--- a/web/src/pages/UsersOverviewPage/UsersTable.tsx
+++ b/web/src/pages/UsersOverviewPage/UsersTable.tsx
@@ -139,9 +139,6 @@ export const UsersTable = ({ users }: Props) => {
enableSorting: true,
sortingFn: 'text',
minSize: 250,
- meta: {
- flex: true,
- },
cell: (info) => {
const rowData = info.row.original;
return (
diff --git a/web/src/routeTree.gen.ts b/web/src/routeTree.gen.ts
index 1055a695e8..adaabe675f 100644
--- a/web/src/routeTree.gen.ts
+++ b/web/src/routeTree.gen.ts
@@ -10,9 +10,9 @@
import { Route as rootRouteImport } from './routes/__root'
import { Route as SnackbarRouteImport } from './routes/snackbar'
+import { Route as PlaygroundRouteImport } from './routes/playground'
import { Route as ConsentRouteImport } from './routes/consent'
import { Route as AuthRouteImport } from './routes/auth'
-import { Route as AppLoaderRouteImport } from './routes/app-loader'
import { Route as AuthorizedRouteImport } from './routes/_authorized'
import { Route as R404RouteImport } from './routes/404'
import { Route as AuthIndexRouteImport } from './routes/auth/index'
@@ -56,6 +56,11 @@ const SnackbarRoute = SnackbarRouteImport.update({
path: '/snackbar',
getParentRoute: () => rootRouteImport,
} as any)
+const PlaygroundRoute = PlaygroundRouteImport.update({
+ id: '/playground',
+ path: '/playground',
+ getParentRoute: () => rootRouteImport,
+} as any)
const ConsentRoute = ConsentRouteImport.update({
id: '/consent',
path: '/consent',
@@ -66,11 +71,6 @@ const AuthRoute = AuthRouteImport.update({
path: '/auth',
getParentRoute: () => rootRouteImport,
} as any)
-const AppLoaderRoute = AppLoaderRouteImport.update({
- id: '/app-loader',
- path: '/app-loader',
- getParentRoute: () => rootRouteImport,
-} as any)
const AuthorizedRoute = AuthorizedRouteImport.update({
id: '/_authorized',
getParentRoute: () => rootRouteImport,
@@ -279,9 +279,9 @@ const AuthorizedDefaultLocationsLocationIdEditRoute =
export interface FileRoutesByFullPath {
'/404': typeof R404Route
- '/app-loader': typeof AppLoaderRoute
'/auth': typeof AuthRouteWithChildren
'/consent': typeof ConsentRoute
+ '/playground': typeof PlaygroundRoute
'/snackbar': typeof SnackbarRoute
'/auth/callback': typeof AuthCallbackRoute
'/auth/loading': typeof AuthLoadingRoute
@@ -320,8 +320,8 @@ export interface FileRoutesByFullPath {
}
export interface FileRoutesByTo {
'/404': typeof R404Route
- '/app-loader': typeof AppLoaderRoute
'/consent': typeof ConsentRoute
+ '/playground': typeof PlaygroundRoute
'/snackbar': typeof SnackbarRoute
'/auth/callback': typeof AuthCallbackRoute
'/auth/loading': typeof AuthLoadingRoute
@@ -362,9 +362,9 @@ export interface FileRoutesById {
__root__: typeof rootRouteImport
'/404': typeof R404Route
'/_authorized': typeof AuthorizedRouteWithChildren
- '/app-loader': typeof AppLoaderRoute
'/auth': typeof AuthRouteWithChildren
'/consent': typeof ConsentRoute
+ '/playground': typeof PlaygroundRoute
'/snackbar': typeof SnackbarRoute
'/_authorized/_default': typeof AuthorizedDefaultRouteWithChildren
'/auth/callback': typeof AuthCallbackRoute
@@ -406,9 +406,9 @@ export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths:
| '/404'
- | '/app-loader'
| '/auth'
| '/consent'
+ | '/playground'
| '/snackbar'
| '/auth/callback'
| '/auth/loading'
@@ -447,8 +447,8 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/404'
- | '/app-loader'
| '/consent'
+ | '/playground'
| '/snackbar'
| '/auth/callback'
| '/auth/loading'
@@ -488,9 +488,9 @@ export interface FileRouteTypes {
| '__root__'
| '/404'
| '/_authorized'
- | '/app-loader'
| '/auth'
| '/consent'
+ | '/playground'
| '/snackbar'
| '/_authorized/_default'
| '/auth/callback'
@@ -532,9 +532,9 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
R404Route: typeof R404Route
AuthorizedRoute: typeof AuthorizedRouteWithChildren
- AppLoaderRoute: typeof AppLoaderRoute
AuthRoute: typeof AuthRouteWithChildren
ConsentRoute: typeof ConsentRoute
+ PlaygroundRoute: typeof PlaygroundRoute
SnackbarRoute: typeof SnackbarRoute
}
@@ -547,6 +547,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof SnackbarRouteImport
parentRoute: typeof rootRouteImport
}
+ '/playground': {
+ id: '/playground'
+ path: '/playground'
+ fullPath: '/playground'
+ preLoaderRoute: typeof PlaygroundRouteImport
+ parentRoute: typeof rootRouteImport
+ }
'/consent': {
id: '/consent'
path: '/consent'
@@ -561,13 +568,6 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthRouteImport
parentRoute: typeof rootRouteImport
}
- '/app-loader': {
- id: '/app-loader'
- path: '/app-loader'
- fullPath: '/app-loader'
- preLoaderRoute: typeof AppLoaderRouteImport
- parentRoute: typeof rootRouteImport
- }
'/_authorized': {
id: '/_authorized'
path: ''
@@ -946,9 +946,9 @@ const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
const rootRouteChildren: RootRouteChildren = {
R404Route: R404Route,
AuthorizedRoute: AuthorizedRouteWithChildren,
- AppLoaderRoute: AppLoaderRoute,
AuthRoute: AuthRouteWithChildren,
ConsentRoute: ConsentRoute,
+ PlaygroundRoute: PlaygroundRoute,
SnackbarRoute: SnackbarRoute,
}
export const routeTree = rootRouteImport
diff --git a/web/src/routes/app-loader.tsx b/web/src/routes/app-loader.tsx
deleted file mode 100644
index 48c77f3fe3..0000000000
--- a/web/src/routes/app-loader.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createFileRoute } from '@tanstack/react-router';
-import { AppLoaderPage } from '../pages/AppLoaderPage/AppLoaderPage';
-
-export const Route = createFileRoute('/app-loader')({
- component: AppLoaderPage,
-});
diff --git a/web/src/routes/playground.tsx b/web/src/routes/playground.tsx
new file mode 100644
index 0000000000..735377c9d7
--- /dev/null
+++ b/web/src/routes/playground.tsx
@@ -0,0 +1,6 @@
+import { createFileRoute } from '@tanstack/react-router';
+import { PlaygroundPage } from '../pages/PlaygroundPage/PlaygroundPage';
+
+export const Route = createFileRoute('/playground')({
+ component: PlaygroundPage,
+});
diff --git a/web/src/shared/components/LoadingStep/LoadingStep.tsx b/web/src/shared/components/LoadingStep/LoadingStep.tsx
new file mode 100644
index 0000000000..8c663445a4
--- /dev/null
+++ b/web/src/shared/components/LoadingStep/LoadingStep.tsx
@@ -0,0 +1,80 @@
+import clsx from 'clsx';
+import './style.scss';
+import { useMemo } from 'react';
+import { AppText } from '../../defguard-ui/components/AppText/AppText';
+import { Fold } from '../../defguard-ui/components/Fold/Fold';
+import { Icon, IconKind } from '../../defguard-ui/components/Icon';
+import { LoaderSpinner } from '../../defguard-ui/components/LoaderSpinner/LoaderSpinner';
+import { SizedBox } from '../../defguard-ui/components/SizedBox/SizedBox';
+import { TextStyle, ThemeSpacing, ThemeVariable } from '../../defguard-ui/types';
+import { isPresent } from '../../defguard-ui/utils/isPresent';
+import type { LoadingStepProps } from './types';
+
+type ComponentVariant = 'loading' | 'success' | 'error' | 'default';
+
+export const LoadingStep = ({
+ title,
+ children,
+ error,
+ errorMessage,
+ success,
+ loading,
+}: LoadingStepProps) => {
+ const variant = useMemo((): ComponentVariant => {
+ if (success) return 'success';
+ if (error) return 'error';
+ if (loading) return 'loading';
+ return 'default';
+ }, [error, success, loading]);
+
+ return (
+
+
+
+ {variant === 'default' && (
+
+ )}
+ {loading && }
+ {success && (
+
+ )}
+ {error && (
+
+ )}
+
+
{title}
+
+
+
+ {isPresent(children) && variant === 'error' && (
+
+
+ {isPresent(errorMessage) && (
+ <>
+
+ {errorMessage}
+
+
+ >
+ )}
+ {children}
+
+
+
+ )}
+
+
+ );
+};
diff --git a/web/src/shared/components/LoadingStep/style.scss b/web/src/shared/components/LoadingStep/style.scss
new file mode 100644
index 0000000000..21548bf311
--- /dev/null
+++ b/web/src/shared/components/LoadingStep/style.scss
@@ -0,0 +1,87 @@
+.loading-step {
+ &.variant-default {
+ .title {
+ font: var(--t-body-sm-400);
+ color: var(--fg-muted);
+ }
+ }
+
+ &.variant-loading {
+ .title {
+ font: var(--t-body-sm-500);
+ color: var(--fg-default);
+ }
+ }
+
+ &.variant-success {
+ .title {
+ font: var(--t-body-sm-400);
+ color: var(--fg-success);
+ }
+ }
+
+ &.variant-error {
+ .title {
+ font: var(--t-body-sm-500);
+ color: var(--fg-default);
+ }
+ }
+
+ & > .main-track {
+ display: grid;
+ grid-template-columns: 20px 1fr;
+ grid-template-rows: 1fr;
+ align-items: center;
+ column-gap: var(--spacing-md);
+
+ & > .icon-container {
+ width: 20px;
+ height: 20px;
+ }
+
+ .title {
+ display: block;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ @include animate(color);
+ }
+ }
+
+ & > .content-track {
+ position: relative;
+ display: grid;
+ grid-template-columns: 10px 1fr;
+ grid-template-rows: 1fr;
+ column-gap: 22px;
+
+ & > .content-bar {
+ grid-column: 1 / 2;
+ content: ' ';
+ width: 100%;
+ box-sizing: border-box;
+ border-right: var(--border-1) solid var(--border-disabled);
+ }
+
+ & > .content {
+ grid-column: 2 / 3;
+ }
+ }
+}
+
+.loading-step:not(:last-child) {
+ padding-bottom: var(--spacing-xs);
+
+ .main-track {
+ padding-bottom: var(--spacing-xs);
+ }
+
+ .content-bar {
+ border-color: var(--border-disabled);
+ }
+
+ & > .content-track {
+ min-height: var(--spacing-2xl);
+ }
+}
diff --git a/web/src/shared/components/LoadingStep/types.ts b/web/src/shared/components/LoadingStep/types.ts
new file mode 100644
index 0000000000..8ff212622b
--- /dev/null
+++ b/web/src/shared/components/LoadingStep/types.ts
@@ -0,0 +1,12 @@
+import type { PropsWithChildren } from 'react';
+
+export type LoadingStepConfig = {
+ title: string;
+ error?: boolean;
+ errorMessage?: string;
+ loading?: boolean;
+ success?: boolean;
+ testId?: string;
+};
+
+export type LoadingStepProps = LoadingStepConfig & PropsWithChildren;
diff --git a/web/src/shared/components/SelectionSection/SelectionSection.tsx b/web/src/shared/components/SelectionSection/SelectionSection.tsx
index cab190856e..eeb108e2db 100644
--- a/web/src/shared/components/SelectionSection/SelectionSection.tsx
+++ b/web/src/shared/components/SelectionSection/SelectionSection.tsx
@@ -43,7 +43,7 @@ export const SelectionSection = ({
});
}
return res;
- }, [options, onlySelected, selection, search.trim]);
+ }, [options, onlySelected, selection, search]);
const handleSelectAll = useCallback(() => {
const allSelected = selection.size === options.length;
diff --git a/web/src/shared/defguard-ui b/web/src/shared/defguard-ui
index 07d6826a40..e46c660d05 160000
--- a/web/src/shared/defguard-ui
+++ b/web/src/shared/defguard-ui
@@ -1 +1 @@
-Subproject commit 07d6826a407e6aeb3a7362e88cb90605ca4763b2
+Subproject commit e46c660d05c390372d6bbcf3201a7eb4ce2b51cf