mirror of https://github.com/CesiumGS/cesium.git
extremely basic and unstyled modals, settings provider and context
This commit is contained in:
parent
2307ecbd74
commit
43b11d8066
|
|
@ -15,6 +15,7 @@
|
|||
"build-gallery": "node scripts/buildGallery.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ariakit/react": "^0.4.17",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@stratakit/bricks": "^0.3.4",
|
||||
"@stratakit/foundations": "^0.2.4",
|
||||
|
|
@ -27,7 +28,8 @@
|
|||
"prettier": "^3.5.3",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-stay-scrolled": "^9.0.0"
|
||||
"react-stay-scrolled": "^9.0.0",
|
||||
"react-use": "^17.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/pako": "^2.0.3",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
.metadata {
|
||||
margin-left: var(--stratakit-space-x2);
|
||||
color: var(--stratakit-color-text-accent-strong);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.version {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
ReactNode,
|
||||
RefObject,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useReducer,
|
||||
|
|
@ -26,7 +27,7 @@ import {
|
|||
moon,
|
||||
share as shareIcon,
|
||||
script,
|
||||
settings,
|
||||
settings as settingsIcon,
|
||||
sun,
|
||||
windowPopout,
|
||||
} from "./icons.ts";
|
||||
|
|
@ -35,8 +36,18 @@ import {
|
|||
ConsoleMessageType,
|
||||
ConsoleMirror,
|
||||
} from "./ConsoleMirror.tsx";
|
||||
import { useLocalStorage } from "./util/useLocalStorage.ts";
|
||||
import { getBaseUrl } from "./util/getBaseUrl.ts";
|
||||
import { SettingsModal } from "./SettingsModal.tsx";
|
||||
import {
|
||||
Popover,
|
||||
PopoverArrow,
|
||||
PopoverDescription,
|
||||
PopoverDisclosure,
|
||||
PopoverHeading,
|
||||
PopoverProvider,
|
||||
} from "@ariakit/react";
|
||||
import { SettingsContext } from "./SettingsContext.ts";
|
||||
import "./Popover.css";
|
||||
|
||||
const defaultJsCode = `import * as Cesium from "cesium";
|
||||
|
||||
|
|
@ -170,10 +181,7 @@ export type SandcastleAction =
|
|||
| { type: "setAndRun"; code?: string; html?: string };
|
||||
|
||||
function App() {
|
||||
const [theme, setTheme] = useLocalStorage<"dark" | "light">(
|
||||
"sandcastle/theme",
|
||||
"dark",
|
||||
);
|
||||
const { settings, updateSettings } = useContext(SettingsContext);
|
||||
const rightSideRef = useRef<RightSideRef>(null);
|
||||
const consoleCollapsedHeight = 26;
|
||||
const [consoleExpanded, setConsoleExpanded] = useState(false);
|
||||
|
|
@ -185,7 +193,10 @@ function App() {
|
|||
const [leftPanel, setLeftPanel] = useState<"editor" | "gallery">(
|
||||
startOnEditor ? "editor" : "gallery",
|
||||
);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
|
||||
const [title, setTitle] = useState("New Sandcastle");
|
||||
const [description, setDescription] = useState("");
|
||||
|
||||
// This is used to avoid a "double render" when loading from the URL
|
||||
const [readyForViewer, setReadyForViewer] = useState(false);
|
||||
|
|
@ -282,6 +293,7 @@ function App() {
|
|||
window.history.pushState({}, "", getBaseUrl());
|
||||
|
||||
setTitle("New Sandcastle");
|
||||
setDescription("");
|
||||
}
|
||||
|
||||
function share() {
|
||||
|
|
@ -335,6 +347,7 @@ function App() {
|
|||
html: html,
|
||||
});
|
||||
setTitle(galleryItem.title);
|
||||
setDescription(galleryItem.description);
|
||||
setReadyForViewer(true);
|
||||
},
|
||||
[galleryItems],
|
||||
|
|
@ -419,21 +432,31 @@ function App() {
|
|||
id="root"
|
||||
className="sandcastle-root"
|
||||
density="dense"
|
||||
colorScheme={theme}
|
||||
colorScheme={settings.theme}
|
||||
synchronizeColorScheme
|
||||
>
|
||||
<header className="header">
|
||||
<a className="logo" href={getBaseUrl()}>
|
||||
<img
|
||||
src={
|
||||
theme === "dark"
|
||||
settings.theme === "dark"
|
||||
? "./images/Cesium_Logo_overlay.png"
|
||||
: "./images/Cesium_Logo_Color_Overlay.png"
|
||||
}
|
||||
style={{ width: "118px" }}
|
||||
/>
|
||||
</a>
|
||||
<div className="metadata">{title}</div>
|
||||
<PopoverProvider>
|
||||
<PopoverDisclosure
|
||||
render={<div className="metadata">{title}</div>}
|
||||
disabled={false}
|
||||
/>
|
||||
<Popover className="popover">
|
||||
<PopoverArrow className="arrow" />
|
||||
<PopoverHeading className="heading">{title}</PopoverHeading>
|
||||
<PopoverDescription>{description}</PopoverDescription>
|
||||
</Popover>
|
||||
</PopoverProvider>
|
||||
<Button tone="accent" onClick={() => share()}>
|
||||
<Icon href={shareIcon} /> Share
|
||||
</Button>
|
||||
|
|
@ -475,20 +498,30 @@ function App() {
|
|||
<div className="flex-spacer"></div>
|
||||
<Divider />
|
||||
<AppBarButton
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
onClick={() =>
|
||||
updateSettings({
|
||||
theme: settings.theme === "dark" ? "light" : "dark",
|
||||
})
|
||||
}
|
||||
label="Toggle Theme"
|
||||
>
|
||||
<Icon href={theme === "dark" ? moon : sun} size="large" />
|
||||
<Icon href={settings.theme === "dark" ? moon : sun} size="large" />
|
||||
</AppBarButton>
|
||||
<AppBarButton label="Settings" onClick={() => {}}>
|
||||
<Icon href={settings} size="large" />
|
||||
<AppBarButton
|
||||
label="Settings"
|
||||
onClick={() => {
|
||||
setSettingsOpen(true);
|
||||
}}
|
||||
>
|
||||
<Icon href={settingsIcon} size="large" />
|
||||
</AppBarButton>
|
||||
<SettingsModal open={settingsOpen} setOpen={setSettingsOpen} />
|
||||
</div>
|
||||
<Allotment defaultSizes={[40, 60]}>
|
||||
<Allotment.Pane minSize={400} className="left-panel">
|
||||
{leftPanel === "editor" && (
|
||||
<SandcastleEditor
|
||||
darkTheme={theme === "dark"}
|
||||
darkTheme={settings.theme === "dark"}
|
||||
onJsChange={(value: string = "") =>
|
||||
dispatch({ type: "setCode", code: value })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
.button {
|
||||
--border: rgb(0 0 0/13%);
|
||||
--highlight: rgb(255 255 255/20%);
|
||||
--shadow: rgb(0 0 0/10%);
|
||||
display: flex;
|
||||
height: 2.5rem;
|
||||
user-select: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
white-space: nowrap;
|
||||
border-radius: 0.5rem;
|
||||
border-style: none;
|
||||
background-color: white;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
color: black;
|
||||
text-decoration-line: none;
|
||||
outline-width: 2px;
|
||||
outline-offset: 2px;
|
||||
outline-color: hsl(204 100% 40%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--border),
|
||||
inset 0 2px 0 var(--highlight),
|
||||
inset 0 -1px 0 var(--shadow),
|
||||
0 1px 1px var(--shadow);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.button:where(.dark, .dark *) {
|
||||
--border: rgb(255 255 255/10%);
|
||||
--highlight: rgb(255 255 255/5%);
|
||||
--shadow: rgb(0 0 0/25%);
|
||||
background-color: rgb(255 255 255 / 0.05);
|
||||
color: white;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--border),
|
||||
inset 0 -1px 0 1px var(--shadow),
|
||||
inset 0 1px 0 var(--highlight);
|
||||
}
|
||||
|
||||
.button:not(:active):hover {
|
||||
--border: rgb(0 0 0/33%);
|
||||
}
|
||||
|
||||
.button:where(.dark, .dark *):not(:active):hover {
|
||||
--border: rgb(255 255 255/25%);
|
||||
}
|
||||
|
||||
.button[aria-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.button[data-focus-visible] {
|
||||
outline-style: solid;
|
||||
}
|
||||
|
||||
.button:active,
|
||||
.button[data-active] {
|
||||
padding-top: 0.125rem;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--border),
|
||||
inset 0 2px 0 var(--border);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.button {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button:active:where(.dark, .dark *),
|
||||
.button[data-active]:where(.dark, .dark *) {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--border),
|
||||
inset 0 1px 1px 1px var(--shadow);
|
||||
}
|
||||
|
||||
.popover {
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
max-width: min(calc(100vw - 16px), 320px);
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
border-radius: 0.5rem;
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: hsl(204 20% 88%);
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
color: black;
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgb(0 0 0 / 0.1),
|
||||
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.popover:focus-visible,
|
||||
.popover[data-focus-visible] {
|
||||
outline: 2px solid hsl(204 100% 40%);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.popover:where(.dark, .dark *) {
|
||||
border-color: hsl(204 4% 24%);
|
||||
background-color: hsl(204 4% 16%);
|
||||
color: white;
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgb(0 0 0 / 0.25),
|
||||
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.arrow > svg {
|
||||
fill: white;
|
||||
stroke: hsl(204 20% 88%);
|
||||
}
|
||||
|
||||
.arrow:where(.dark, .dark *) > svg {
|
||||
fill: hsl(204 4% 16%);
|
||||
stroke: hsl(204 4% 24%);
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0px;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { createContext } from "react";
|
||||
|
||||
export type Settings = {
|
||||
theme: "dark" | "light";
|
||||
fontSize: "large" | "small";
|
||||
};
|
||||
|
||||
export const initialSettings: Settings = {
|
||||
theme: "dark",
|
||||
fontSize: "large",
|
||||
};
|
||||
|
||||
export const SettingsContext = createContext<{
|
||||
settings: Settings;
|
||||
updateSettings: (newSettings: Partial<Settings>) => void;
|
||||
}>({
|
||||
settings: initialSettings,
|
||||
updateSettings: () => {},
|
||||
});
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
.button {
|
||||
--border: rgb(0 0 0/13%);
|
||||
--highlight: rgb(255 255 255/20%);
|
||||
--shadow: rgb(0 0 0/10%);
|
||||
display: flex;
|
||||
height: 2.5rem;
|
||||
user-select: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.25rem;
|
||||
white-space: nowrap;
|
||||
border-radius: 0.5rem;
|
||||
border-style: none;
|
||||
background-color: white;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5rem;
|
||||
color: black;
|
||||
text-decoration-line: none;
|
||||
outline-width: 2px;
|
||||
outline-offset: 2px;
|
||||
outline-color: hsl(204 100% 40%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--border),
|
||||
inset 0 2px 0 var(--highlight),
|
||||
inset 0 -1px 0 var(--shadow),
|
||||
0 1px 1px var(--shadow);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.button:where(.dark, .dark *) {
|
||||
--border: rgb(255 255 255/10%);
|
||||
--highlight: rgb(255 255 255/5%);
|
||||
--shadow: rgb(0 0 0/25%);
|
||||
background-color: rgb(255 255 255 / 0.05);
|
||||
color: white;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--border),
|
||||
inset 0 -1px 0 1px var(--shadow),
|
||||
inset 0 1px 0 var(--highlight);
|
||||
}
|
||||
|
||||
.button:not(:active):hover {
|
||||
--border: rgb(0 0 0/33%);
|
||||
}
|
||||
|
||||
.button:where(.dark, .dark *):not(:active):hover {
|
||||
--border: rgb(255 255 255/25%);
|
||||
}
|
||||
|
||||
.button[aria-disabled="true"] {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.button[data-focus-visible] {
|
||||
outline-style: solid;
|
||||
}
|
||||
|
||||
.button:active,
|
||||
.button[data-active] {
|
||||
padding-top: 0.125rem;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--border),
|
||||
inset 0 2px 0 var(--border);
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.button {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.button:active:where(.dark, .dark *),
|
||||
.button[data-active]:where(.dark, .dark *) {
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--border),
|
||||
inset 0 1px 1px 1px var(--shadow);
|
||||
}
|
||||
|
||||
.dialog {
|
||||
position: fixed;
|
||||
inset: var(--inset);
|
||||
z-index: 50;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
height: fit-content;
|
||||
max-height: calc(100dvh - var(--inset) * 2);
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
overflow: auto;
|
||||
border-radius: 0.75rem;
|
||||
background-color: white;
|
||||
padding: 1rem;
|
||||
color: black;
|
||||
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
|
||||
--inset: 0.75rem;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.dialog {
|
||||
top: 10vh;
|
||||
bottom: 10vh;
|
||||
margin-top: 0px;
|
||||
max-height: 80vh;
|
||||
width: 420px;
|
||||
border-radius: 1rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog:where(.dark, .dark *) {
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
border-color: hsl(204 4% 24%);
|
||||
background-color: hsl(204 4% 16%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0px;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-backdrop] {
|
||||
background: color(
|
||||
from var(--stratakit-color-bg-elevation-emphasis) xyz x y z / 80%
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { Dialog, DialogDismiss, DialogHeading } from "@ariakit/react";
|
||||
import "./SettingsModal.css";
|
||||
import { useContext } from "react";
|
||||
import { SettingsContext } from "./SettingsContext";
|
||||
import { Button } from "@stratakit/bricks";
|
||||
|
||||
export function SettingsModal({
|
||||
open,
|
||||
setOpen,
|
||||
}: {
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}) {
|
||||
const { settings, updateSettings } = useContext(SettingsContext);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={() => setOpen(false)} className="dialog">
|
||||
<DialogHeading className="heading">Success</DialogHeading>
|
||||
<p className="description">
|
||||
Your payment has been successfully processed. We have emailed your
|
||||
receipt.
|
||||
</p>
|
||||
<p>Theme: {settings.theme}</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (settings.theme === "dark") {
|
||||
updateSettings({ theme: "light" });
|
||||
} else {
|
||||
updateSettings({ theme: "dark" });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Change Theme
|
||||
</Button>
|
||||
<p>Font size: {settings.fontSize}</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (settings.fontSize === "large") {
|
||||
updateSettings({ fontSize: "small" });
|
||||
} else {
|
||||
updateSettings({ fontSize: "large" });
|
||||
}
|
||||
}}
|
||||
>
|
||||
Change Font Size
|
||||
</Button>
|
||||
<div>
|
||||
<DialogDismiss className="button">OK</DialogDismiss>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { ReactNode, useCallback } from "react";
|
||||
import { initialSettings, Settings, SettingsContext } from "./SettingsContext";
|
||||
import { useLocalStorage } from "react-use";
|
||||
|
||||
export function SettingsProvider({ children }: { children: ReactNode }) {
|
||||
const [settings, updateSettings] = useLocalStorage<Settings>(
|
||||
"sandcastle/settings",
|
||||
initialSettings,
|
||||
{
|
||||
raw: false,
|
||||
serializer: (value) => {
|
||||
// Build a new object so we always set only the settings we know about
|
||||
return JSON.stringify({
|
||||
theme: value.theme ?? initialSettings.theme,
|
||||
fontSize: value.fontSize ?? initialSettings.fontSize,
|
||||
});
|
||||
},
|
||||
deserializer: (value) => {
|
||||
// This allows us to guarantee all expected values are set AND any unknown
|
||||
// values are removed from the settings object
|
||||
const parsedValue = JSON.parse(value);
|
||||
return {
|
||||
theme: parsedValue.theme ?? initialSettings.theme,
|
||||
fontSize: parsedValue.fontSize ?? initialSettings.fontSize,
|
||||
};
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const mergeSettings = useCallback(
|
||||
(newSettings: Partial<Settings>) => {
|
||||
// allow partial updates but make sure we don't lose settings keys
|
||||
// won't currently work with nested settings
|
||||
updateSettings({
|
||||
...initialSettings,
|
||||
...settings,
|
||||
...newSettings,
|
||||
});
|
||||
},
|
||||
[updateSettings, settings],
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingsContext
|
||||
value={{
|
||||
settings: settings ?? initialSettings,
|
||||
updateSettings: mergeSettings,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SettingsContext>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App.tsx";
|
||||
import { SettingsProvider } from "./SettingsProvider.tsx";
|
||||
|
||||
const host = window.location.host;
|
||||
if (host.includes("localhost")) {
|
||||
|
|
@ -11,6 +12,8 @@ if (host.includes("localhost")) {
|
|||
|
||||
createRoot(document.getElementById("app-container")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
<SettingsProvider>
|
||||
<App />
|
||||
</SettingsProvider>
|
||||
</StrictMode>,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
import { useDebugValue, useEffect, useState } from "react";
|
||||
|
||||
export function useLocalStorage<T>(
|
||||
key: string,
|
||||
initialValue: T,
|
||||
): [T, (newValue: T) => void] {
|
||||
useDebugValue(key);
|
||||
|
||||
const [value, setValue] = useState<T>(initialValue);
|
||||
|
||||
useEffect(() => {
|
||||
const storedValue = window.localStorage.getItem(key);
|
||||
if (storedValue) {
|
||||
setValue(JSON.parse(storedValue) as T);
|
||||
} else {
|
||||
window.localStorage.setItem(key, JSON.stringify(initialValue));
|
||||
}
|
||||
}, [key, initialValue]);
|
||||
|
||||
function updateStorage(newValue: T) {
|
||||
window.localStorage.setItem(key, JSON.stringify(newValue));
|
||||
setValue(newValue);
|
||||
}
|
||||
|
||||
return [value, updateStorage];
|
||||
}
|
||||
Loading…
Reference in New Issue