From fe5db59c303f3b18a2bb706b966ffcade582295a Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Mon, 14 Jul 2025 20:04:41 -0700 Subject: [PATCH] Widget playground mobile mode --- apps/dashboard/.env | 2 + apps/dashboard/.env.development | 2 + .../widget-playground/page-client.tsx | 152 ++++++++++++------ apps/dashboard/src/app/globals.css | 25 +++ .../src/components/pacifica/card.tsx | 131 +++++++++++++++ .../src/components/pacifica/surface.tsx | 26 +++ packages/stack-shared/src/utils/errors.tsx | 4 + packages/stack-shared/src/utils/react.tsx | 43 ++--- packages/template/scripts/process-css.ts | 16 +- .../src/components/passkey-button.tsx | 2 +- .../template/src/providers/theme-provider.tsx | 13 +- 11 files changed, 322 insertions(+), 94 deletions(-) create mode 100644 apps/dashboard/src/components/pacifica/card.tsx create mode 100644 apps/dashboard/src/components/pacifica/surface.tsx diff --git a/apps/dashboard/.env b/apps/dashboard/.env index 9484be7f6..d3af72cc3 100644 --- a/apps/dashboard/.env +++ b/apps/dashboard/.env @@ -12,3 +12,5 @@ NEXT_PUBLIC_STACK_SVIX_SERVER_URL=# For prod, leave it empty. For local developm NEXT_PUBLIC_STACK_HEAD_TAGS='[{ "tagName": "script", "attributes": {}, "innerHTML": "// insert head tags here" }]' STACK_DEVELOPMENT_TRANSLATION_LOCALE=# enter the locale to use for the translation provider here, for example: de-DE. Only works during development, not in production. Optional, by default don't translate NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS='["internal"]' + +NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR=# set to true to open the debugger on assertion errors (set to true in .env.development) diff --git a/apps/dashboard/.env.development b/apps/dashboard/.env.development index 93bcb1e6a..8e8c4b741 100644 --- a/apps/dashboard/.env.development +++ b/apps/dashboard/.env.development @@ -6,3 +6,5 @@ STACK_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only NEXT_PUBLIC_STACK_SVIX_SERVER_URL=http://localhost:8113 STACK_ARTIFICIAL_DEVELOPMENT_DELAY_MS=50 + +NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR=true diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx index 2ebd652a0..008268178 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/widget-playground/page-client.tsx @@ -1,6 +1,8 @@ "use client"; +import { PacificaCard } from '@/components/pacifica/card'; import { DndContext, pointerWithin, useDraggable, useDroppable } from '@dnd-kit/core'; +import useResizeObserver from '@react-hook/resize-observer'; import { range } from '@stackframe/stack-shared/dist/utils/arrays'; import { StackAssertionError, errorToNiceString, throwErr } from '@stackframe/stack-shared/dist/utils/errors'; import { bundleJavaScript } from '@stackframe/stack-shared/dist/utils/esbuild'; @@ -16,7 +18,6 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import { FaBorderNone, FaPen, FaPlus, FaTrash } from 'react-icons/fa'; import * as jsxRuntime from 'react/jsx-runtime'; import { PageLayout } from "../page-layout"; -import { useAdminApp } from '../use-admin-app'; type SerializedWidget = { version: 1, @@ -51,11 +52,15 @@ async function compileWidgetSource(source: string): Promise { return { id, - MainComponent: () => -
- {errorMessage} -
-
, + MainComponent: () => ( + +
+ {errorMessage} +
+
+ ), defaultSettings: null as any, defaultState: null as any, }; @@ -90,7 +95,7 @@ async function deserializeWidget(serializedWidget: SerializedWidget): Promise = { id: string, - MainComponent: React.ComponentType<{ settings: Settings, state: State, stateRef: ReadonlyRef, setState: (updater: (state: State) => State) => void, widthInGridUnits: number, heightInGridUnits: number }>, + MainComponent: React.ComponentType<{ settings: Settings, state: State, stateRef: ReadonlyRef, setState: (updater: (state: State) => State) => void, widthInGridUnits: number, heightInGridUnits: number, isMobileMode: boolean }>, SettingsComponent?: React.ComponentType<{ settings: Settings, setSettings: (updater: (settings: Settings) => Settings) => void }>, defaultSettings: Settings, defaultState: State, @@ -153,13 +158,15 @@ class WidgetInstanceGrid { const width = options.width ?? 24; const height = options.height ?? "auto"; - const nonEmptyElements = widgetInstances.map((instance, index) => ({ - instance, - x: (index * WidgetInstanceGrid.DEFAULT_ELEMENT_WIDTH) % width, - y: Math.floor(index / Math.floor(width / WidgetInstanceGrid.DEFAULT_ELEMENT_WIDTH)) * WidgetInstanceGrid.DEFAULT_ELEMENT_HEIGHT, - width: WidgetInstanceGrid.DEFAULT_ELEMENT_WIDTH, - height: WidgetInstanceGrid.DEFAULT_ELEMENT_HEIGHT, - })); + const nonEmptyElements = widgetInstances + .map((instance, index) => ({ + instance, + x: (index * WidgetInstanceGrid.DEFAULT_ELEMENT_WIDTH) % width, + y: Math.floor(index / Math.floor(width / WidgetInstanceGrid.DEFAULT_ELEMENT_WIDTH)) * WidgetInstanceGrid.DEFAULT_ELEMENT_HEIGHT, + width: WidgetInstanceGrid.DEFAULT_ELEMENT_WIDTH, + height: WidgetInstanceGrid.DEFAULT_ELEMENT_HEIGHT, + })) + .sort((a, b) => Math.sign(a.x - b.x) + 0.1 * Math.sign(a.y - b.y)); // Do some sanity checks to prevent bugs early for (const element of nonEmptyElements) { @@ -470,8 +477,8 @@ class WidgetInstanceGrid { edgesDelta.bottom !== 0 ? this.clampResize(x, y, { ...edgesDelta, bottom: decr(edgesDelta.bottom) }) : null, edgesDelta.right !== 0 ? this.clampResize(x, y, { ...edgesDelta, right: decr(edgesDelta.right) }) : null, ].filter(isNotNull); - let maxScore = -1; - let bestCandidate: { top: number, left: number, bottom: number, right: number } | null = null; + let maxScore = 0; + let bestCandidate: { top: number, left: number, bottom: number, right: number } = { top: 0, left: 0, bottom: 0, right: 0 }; for (const candidate of candidates) { const score = Math.abs(candidate.top) + Math.abs(candidate.left) + Math.abs(candidate.bottom) + Math.abs(candidate.right); if (score > maxScore) { @@ -479,9 +486,6 @@ class WidgetInstanceGrid { bestCandidate = candidate; } } - if (!bestCandidate) { - throw new StackAssertionError(`No candidate found for ${cacheKey}`); - } this._clampResizeCache.set(cacheKey, bestCandidate); } } @@ -554,7 +558,7 @@ class WidgetInstanceGrid { const widgets: Widget[] = [ { id: "$sub-grid", - MainComponent: ({ widthInGridUnits, heightInGridUnits, state, stateRef, setState }) => { + MainComponent: ({ widthInGridUnits, heightInGridUnits, state, stateRef, setState, isMobileMode }) => { const widgetGridRef = mapRef(stateRef, (state) => WidgetInstanceGrid.fromSerialized(state.serializedGrid)); const [color] = useState("#" + Math.floor(Math.random() * 16777215).toString(16) + "22"); @@ -575,6 +579,7 @@ const widgets: Widget[] = [ return (
@@ -615,11 +620,13 @@ const widgets: Widget[] = [ export const defaultSettings = {name: "world"}; `); - const stackAdminApp = useAdminApp(); const [compilationResult, setCompilationResult] = useState | null>(null); return ( - +