diff --git a/apps/gnocchi/hub/package.json b/apps/gnocchi/hub/package.json
index e7f544f1..928def5e 100644
--- a/apps/gnocchi/hub/package.json
+++ b/apps/gnocchi/hub/package.json
@@ -20,7 +20,7 @@
"typecheck": "tsc --build tsconfig.json"
},
"dependencies": {
- "@a-type/ui": "^0.8.20",
+ "@a-type/ui": "^0.8.21",
"@a-type/utils": "^1.0.8",
"@tiptap/core": "^2.2.4",
"@tiptap/extension-document": "^2.2.4",
diff --git a/apps/gnocchi/web/package.json b/apps/gnocchi/web/package.json
index e32823b4..f70a58e4 100644
--- a/apps/gnocchi/web/package.json
+++ b/apps/gnocchi/web/package.json
@@ -14,7 +14,7 @@
"typecheck": "tsc --build tsconfig.json"
},
"dependencies": {
- "@a-type/ui": "^0.8.20",
+ "@a-type/ui": "^0.8.21",
"@a-type/utils": "^1.0.8",
"@biscuits/client": "workspace:*",
"@biscuits/error": "workspace:*",
@@ -59,7 +59,6 @@
"prosemirror-model": "1.21.1",
"react": "18.3.1",
"react-dom": "18.3.1",
- "react-hot-toast": "^2.3.0",
"react-lazy-with-preload": "^2.2.1",
"react-use-measure": "^2.1.1",
"stopword": "^2.0.8",
diff --git a/apps/gnocchi/web/src/App.tsx b/apps/gnocchi/web/src/App.tsx
index 6491c8d3..b4c5b3b9 100644
--- a/apps/gnocchi/web/src/App.tsx
+++ b/apps/gnocchi/web/src/App.tsx
@@ -1,31 +1,17 @@
-import classNames from 'classnames';
-import { Suspense, useLayoutEffect, useState } from 'react';
-import { Toaster } from 'react-hot-toast';
-import { Pages } from './pages/Pages.jsx';
-import { Provider as GroceriesProvider } from './stores/groceries/Provider.jsx';
-import { IconSpritesheet } from '@a-type/ui/components/icon';
import { ReloadButton } from '@/components/sync/ReloadButton.jsx';
import { GlobalLoader } from '@/GlobalLoader.jsx';
-import { useVisualViewportOffset } from '@a-type/ui/hooks';
import { ErrorBoundary } from '@a-type/ui/components/errorBoundary';
-import { TooltipProvider } from '@a-type/ui/components/tooltip';
-import { P, H1 } from '@a-type/ui/components/typography';
-import { ParticleLayer } from '@a-type/ui/components/particles';
-import { GlobalSyncingIndicator } from '@/components/sync/GlobalSyncingIndicator.jsx';
-import { AppPreviewNotice, Provider } from '@biscuits/client';
-import { graphqlClient } from './graphql.js';
-import { groceriesDescriptor } from './stores/groceries/index.js';
+import { Provider as UIProvider } from '@a-type/ui/components/provider';
+import { H1, P } from '@a-type/ui/components/typography';
+import { Provider } from '@biscuits/client';
+import classNames from 'classnames';
+import { Suspense } from 'react';
import { AppMoved } from './components/promotional/AppMoved.jsx';
+import { Pages } from './pages/Pages.jsx';
+import { groceriesDescriptor } from './stores/groceries/index.js';
+import { Provider as GroceriesProvider } from './stores/groceries/Provider.jsx';
export function App() {
- useLayoutEffect(() => {
- if (typeof window !== 'undefined') {
- document.body.className = 'theme-lemon';
- }
- }, []);
-
- useVisualViewportOffset();
-
return (
}>
-
+
}>
-
-
-
-
-
-
-
-
+
+
-
+
);
diff --git a/apps/gnocchi/web/src/components/foods/FoodNamesEditor.tsx b/apps/gnocchi/web/src/components/foods/FoodNamesEditor.tsx
index 175243b0..9c77ab3a 100644
--- a/apps/gnocchi/web/src/components/foods/FoodNamesEditor.tsx
+++ b/apps/gnocchi/web/src/components/foods/FoodNamesEditor.tsx
@@ -16,7 +16,7 @@ import {
} from '@a-type/ui/components/forms';
import { Cross2Icon, PlusIcon } from '@radix-ui/react-icons';
import { useState } from 'react';
-import { toast } from 'react-hot-toast';
+import { toast } from '@a-type/ui';
export interface FoodNamesEditorProps {
names: FoodAlternateNames;
diff --git a/apps/gnocchi/web/src/components/promotional/AppMoved.tsx b/apps/gnocchi/web/src/components/promotional/AppMoved.tsx
index 39311c8b..756194d5 100644
--- a/apps/gnocchi/web/src/components/promotional/AppMoved.tsx
+++ b/apps/gnocchi/web/src/components/promotional/AppMoved.tsx
@@ -15,7 +15,7 @@ import {
} from '@biscuits/client';
import { Link } from '@verdant-web/react-router';
import { Icon } from '@a-type/ui/components/icon';
-import { toast } from 'react-hot-toast';
+import { toast } from '@a-type/ui';
import { Button } from '@a-type/ui/components/button';
import { ExportDataButton } from '@biscuits/client/storage';
diff --git a/apps/gnocchi/web/src/components/recipes/cook/CookingActionBar.tsx b/apps/gnocchi/web/src/components/recipes/cook/CookingActionBar.tsx
index 20b6921c..5a824bef 100644
--- a/apps/gnocchi/web/src/components/recipes/cook/CookingActionBar.tsx
+++ b/apps/gnocchi/web/src/components/recipes/cook/CookingActionBar.tsx
@@ -34,7 +34,7 @@ import {
PopoverTrigger,
} from '@a-type/ui/components/popover';
import { Note } from '@a-type/ui/components/note';
-import { HOME_ORIGIN } from 'node_modules/@biscuits/client/src/config.js';
+import { CONFIG } from '@biscuits/client';
export interface CookingActionBarProps {
recipe: Recipe;
@@ -130,7 +130,7 @@ function AddChefsAction() {
device sync, grocery collaboration, and web recipe scanning.
-
+
Learn more about subscription features.
diff --git a/apps/gnocchi/web/src/components/recipes/viewer/RecipePublishControl.tsx b/apps/gnocchi/web/src/components/recipes/viewer/RecipePublishControl.tsx
index dd5b15fd..cb63c1c5 100644
--- a/apps/gnocchi/web/src/components/recipes/viewer/RecipePublishControl.tsx
+++ b/apps/gnocchi/web/src/components/recipes/viewer/RecipePublishControl.tsx
@@ -22,7 +22,7 @@ import {
import { Recipe } from '@gnocchi.biscuits/verdant';
import { format } from 'date-fns/esm';
import { useState } from 'react';
-import { toast } from 'react-hot-toast';
+import { toast } from '@a-type/ui';
export interface RecipePublishControlProps {
recipe: Recipe;
diff --git a/apps/gnocchi/web/src/graphql.ts b/apps/gnocchi/web/src/graphql.ts
deleted file mode 100644
index 1d192a37..00000000
--- a/apps/gnocchi/web/src/graphql.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { createGraphQLClient } from '@biscuits/client';
-import { toast } from 'react-hot-toast';
-
-export const graphqlClient = createGraphQLClient({
- onError: (err) => toast.error(err),
-});
diff --git a/apps/gnocchi/web/src/pages/PlanPage.tsx b/apps/gnocchi/web/src/pages/PlanPage.tsx
index 135c1760..f6f54efd 100644
--- a/apps/gnocchi/web/src/pages/PlanPage.tsx
+++ b/apps/gnocchi/web/src/pages/PlanPage.tsx
@@ -29,7 +29,7 @@ import { ArrowRightIcon } from '@radix-ui/react-icons';
import { ReactNode, useEffect } from 'react';
import { groceriesDescriptor } from '@/stores/groceries/index.js';
import { AutoRestoreScroll } from '@/components/nav/AutoRestoreScroll.jsx';
-import { toast } from 'react-hot-toast';
+import { toast } from '@a-type/ui';
const contents = {
offline: OfflineContents,
diff --git a/apps/gnocchi/web/src/stores/groceries/index.ts b/apps/gnocchi/web/src/stores/groceries/index.ts
index 933a3c31..e5618294 100644
--- a/apps/gnocchi/web/src/stores/groceries/index.ts
+++ b/apps/gnocchi/web/src/stores/groceries/index.ts
@@ -1,6 +1,6 @@
import { pickBestNameMatch } from '@/components/foods/lookup.jsx';
import { groceriesState } from '@/components/groceries/state.js';
-import { graphqlClient } from '@/graphql.js';
+import { graphqlClient } from '@biscuits/client';
import { getVerdantSync, graphql, VerdantContext } from '@biscuits/client';
import { parseIngredient } from '@gnocchi.biscuits/conversion';
import { depluralize } from '@gnocchi.biscuits/conversion';
@@ -24,7 +24,7 @@ import { useSearchParams } from '@verdant-web/react-router';
import cuid from 'cuid';
import pluralize from 'pluralize';
import { useCallback } from 'react';
-import { toast } from 'react-hot-toast';
+import { toast } from '@a-type/ui';
import { getScannedRecipe } from './scanRecipe.js';
export interface Presence {
diff --git a/apps/gnocchi/web/src/stores/groceries/scanRecipe.ts b/apps/gnocchi/web/src/stores/groceries/scanRecipe.ts
index 1e134aa7..132a2400 100644
--- a/apps/gnocchi/web/src/stores/groceries/scanRecipe.ts
+++ b/apps/gnocchi/web/src/stores/groceries/scanRecipe.ts
@@ -2,13 +2,13 @@ import {
graphql,
isClientError,
showSubscriptionPromotion,
+ graphqlClient,
} from '@biscuits/client';
import { detailedInstructionsToDoc, instructionsToDoc } from '@/lib/tiptap.js';
import { BiscuitsError } from '@biscuits/error';
import { lookupUnit, parseIngredient } from '@gnocchi.biscuits/conversion';
-import { graphqlClient } from '@/graphql.js';
import { RecipeInit, Client } from '@gnocchi.biscuits/verdant';
-import { toast } from 'react-hot-toast';
+import { toast } from '@a-type/ui';
const recipeScanQuery = graphql(`
query RecipeScan($input: RecipeScanInput!) {
@@ -82,7 +82,7 @@ export async function getScannedRecipe(
comments: [...(i.comments ?? [])],
text: i.original,
note: i.note,
- isSectionHeader: i.isSectionHeader,
+ isSectionHeader: i.isSectionHeader ?? undefined,
};
});
} else if (scanned.rawIngredients?.length) {
diff --git a/apps/marginalia/web/package.json b/apps/marginalia/web/package.json
index 9a636bde..1f6ff545 100644
--- a/apps/marginalia/web/package.json
+++ b/apps/marginalia/web/package.json
@@ -4,12 +4,12 @@
"private": true,
"type": "module",
"scripts": {
- "app-dev": "vite --open --serve",
+ "app-dev": "vite --host --open",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
- "@a-type/ui": "0.8.20",
+ "@a-type/ui": "0.8.21",
"@a-type/utils": "1.1.0",
"@biscuits/client": "workspace:*",
"@marginalia.biscuits/verdant": "workspace:*",
@@ -19,7 +19,6 @@
"classnames": "^2.5.0",
"react": "18.3.1",
"react-dom": "18.3.1",
- "react-hot-toast": "^2.4.1",
"unocss": "0.60.3",
"valtio": "^1.13.2",
"workbox-core": "^7.1.0",
diff --git a/apps/marginalia/web/src/App.tsx b/apps/marginalia/web/src/App.tsx
index 06bb1eef..4512628f 100644
--- a/apps/marginalia/web/src/App.tsx
+++ b/apps/marginalia/web/src/App.tsx
@@ -1,25 +1,15 @@
-import { clientDescriptor, hooks } from '@/store.js';
-import { ReactNode, Suspense, useLayoutEffect } from 'react';
import { Pages } from '@/pages/Pages.jsx';
-import { useVisualViewportOffset } from '@a-type/ui/hooks';
-import { Toaster } from 'react-hot-toast';
-import { IconSpritesheet } from '@a-type/ui/components/icon';
+import { clientDescriptor, hooks } from '@/store.js';
import { ErrorBoundary } from '@a-type/ui/components/errorBoundary';
-import { TooltipProvider } from '@a-type/ui/components/tooltip';
-import { ParticleLayer } from '@a-type/ui/components/particles';
-import { PrereleaseWarning, ReloadButton } from '@biscuits/client';
+import { Provider as UIProvider } from '@a-type/ui/components/provider';
import { H1, P } from '@a-type/ui/components/typography';
-import {
- useCanSync,
- Provider,
- createGraphQLClient,
- AppPreviewNotice,
-} from '@biscuits/client';
-import { QueryClientProvider, QueryClient } from '@tanstack/react-query';
+import { useVisualViewportOffset } from '@a-type/ui/hooks';
+import { Provider, ReloadButton, useCanSync } from '@biscuits/client';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { ReactNode, Suspense, useLayoutEffect } from 'react';
export interface AppProps {}
-const graphqlClient = createGraphQLClient();
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@@ -39,30 +29,20 @@ export function App({}: AppProps) {
return (
}>
-
+
-
-
-
-
-
-
-
+
-
+
);
}
diff --git a/apps/marginalia/web/src/components/nav/TopLoader.tsx b/apps/marginalia/web/src/components/nav/TopLoader.tsx
deleted file mode 100644
index 44cca7a2..00000000
--- a/apps/marginalia/web/src/components/nav/TopLoader.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { useIsRouteTransitioning } from '@verdant-web/react-router';
-import { animated, useSpring } from '@react-spring/web';
-import classNames from 'classnames';
-import { useCallback, useEffect } from 'react';
-
-export interface TopLoaderProps {
- className?: string;
-}
-
-export function TopLoader({ className }: TopLoaderProps) {
- const show = useIsRouteTransitioning(500);
-
- const [style, spring] = useSpring(() => ({
- width: '0%',
- }));
-
- const run = useCallback(() => {
- let timeout: NodeJS.Timer | undefined;
- function step(previous: number) {
- spring.start({
- width: `${previous}%`,
- });
- const nextStep = Math.min(
- 95 - previous,
- Math.min((95 - previous) / 2, Math.random() * 20),
- );
- timeout = setTimeout(
- step,
- 500 + Math.random() * 1000,
- previous + nextStep,
- );
- }
- step(0);
- return () => {
- if (timeout) clearTimeout(timeout);
- spring.start({
- width: '100%',
- });
- };
- }, [show, spring]);
-
- useEffect(() => {
- if (show) {
- return run();
- }
- }, [show, run]);
-
- return (
-
- );
-}
diff --git a/apps/marginalia/web/src/pages/Pages.tsx b/apps/marginalia/web/src/pages/Pages.tsx
index d03f101e..f8df25c5 100644
--- a/apps/marginalia/web/src/pages/Pages.tsx
+++ b/apps/marginalia/web/src/pages/Pages.tsx
@@ -6,7 +6,6 @@ import { Spinner } from '@a-type/ui/components/spinner';
import { lazy, useCallback, Suspense } from 'react';
import { updateApp, updateState } from '@/updateState.js';
import { Link } from '@verdant-web/react-router';
-import { TopLoader } from '@/components/nav/TopLoader.jsx';
import { Button } from '@a-type/ui/components/button';
import { ReloadButton } from '@biscuits/client';
import { PageRoot } from '@a-type/ui/components/layouts';
@@ -47,7 +46,6 @@ export function Pages() {
}>
}>
-
diff --git a/apps/star-chart/web/index.html b/apps/star-chart/web/index.html
index f3284a45..a4915ce4 100644
--- a/apps/star-chart/web/index.html
+++ b/apps/star-chart/web/index.html
@@ -39,7 +39,7 @@
-
+
diff --git a/apps/star-chart/web/package.json b/apps/star-chart/web/package.json
index 76f0958c..062454e3 100644
--- a/apps/star-chart/web/package.json
+++ b/apps/star-chart/web/package.json
@@ -9,7 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
- "@a-type/ui": "0.8.20",
+ "@a-type/ui": "0.8.21",
"@a-type/utils": "1.1.2",
"@biscuits/client": "workspace:*",
"@react-spring/web": "^9.7.3",
@@ -21,7 +21,6 @@
"raf": "^3.4.1",
"react": "18.3.1",
"react-dom": "18.3.1",
- "react-hot-toast": "^2.4.1",
"react-hotkeys-hook": "^4.5.0",
"rete": "^2.0.3",
"rete-area-plugin": "^2.0.4",
diff --git a/apps/star-chart/web/src/App.tsx b/apps/star-chart/web/src/App.tsx
index 1d513582..ea78e672 100644
--- a/apps/star-chart/web/src/App.tsx
+++ b/apps/star-chart/web/src/App.tsx
@@ -1,60 +1,30 @@
-import { clientDescriptor, hooks } from '@/store.js';
-import { ReactNode, Suspense, useLayoutEffect } from 'react';
import { Pages } from '@/pages/Pages.jsx';
-import { useVisualViewportOffset } from '@a-type/ui/hooks';
-import { Toaster } from 'react-hot-toast';
-import { IconSpritesheet } from '@a-type/ui/components/icon';
+import { clientDescriptor, hooks } from '@/store.js';
import { ErrorBoundary } from '@a-type/ui/components/errorBoundary';
-import { TooltipProvider } from '@a-type/ui/components/tooltip';
-import { ParticleLayer } from '@a-type/ui/components/particles';
-import { PrereleaseWarning, ReloadButton } from '@biscuits/client';
+import { Provider as UIProvider } from '@a-type/ui/components/provider';
import { H1, P } from '@a-type/ui/components/typography';
-import {
- useCanSync,
- Provider,
- createGraphQLClient,
- AppPreviewNotice,
-} from '@biscuits/client';
+import { Provider, ReloadButton, useCanSync } from '@biscuits/client';
+import { ReactNode, Suspense } from 'react';
import { ProjectSettingsDialog } from './components/project/ProjectSettingsDialog.jsx';
export interface AppProps {}
-const graphqlClient = createGraphQLClient();
-
export function App({}: AppProps) {
- useLayoutEffect(() => {
- if (typeof window !== 'undefined') {
- document.body.className = 'theme-lemon';
- }
- }, []);
-
- useVisualViewportOffset();
-
return (
}>
-
+
-
-
-
-
-
-
-
-
+
+
-
+
);
}
diff --git a/apps/trip-tick/web/package.json b/apps/trip-tick/web/package.json
index e7666cfc..68450fa7 100644
--- a/apps/trip-tick/web/package.json
+++ b/apps/trip-tick/web/package.json
@@ -9,7 +9,7 @@
"preview": "vite preview"
},
"dependencies": {
- "@a-type/ui": "^0.8.20",
+ "@a-type/ui": "^0.8.21",
"@a-type/utils": "^1.0.6",
"@biscuits/client": "workspace:*",
"@radix-ui/react-progress": "^1.0.3",
diff --git a/apps/trip-tick/web/src/App.tsx b/apps/trip-tick/web/src/App.tsx
index 291428b3..b0f5cf99 100644
--- a/apps/trip-tick/web/src/App.tsx
+++ b/apps/trip-tick/web/src/App.tsx
@@ -1,47 +1,23 @@
import { Pages } from '@/pages/Pages.jsx';
import { clientDescriptor, hooks } from '@/store.js';
-import { IconSpritesheet } from '@a-type/ui/components/icon';
-import { TooltipProvider } from '@a-type/ui/components/tooltip';
-import {
- useCanSync,
- Provider,
- createGraphQLClient,
- AppPreviewNotice,
-} from '@biscuits/client';
-import { ReactNode, Suspense } from 'react';
-import { Toaster } from 'react-hot-toast';
+import { Provider as UIProvider } from '@a-type/ui/components/provider';
import { FullScreenSpinner } from '@a-type/ui/components/spinner';
-import { ParticleLayer } from '@a-type/ui/components/particles';
-import { useVisualViewportOffset } from '@a-type/ui/hooks';
+import { Provider, useCanSync } from '@biscuits/client';
+import { ReactNode, Suspense } from 'react';
import { Explainer } from './components/onboarding/Explainer.jsx';
-const graphqlClient = createGraphQLClient();
-
export function App() {
- useVisualViewportOffset();
return (
}>
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
+
+
+
);
diff --git a/apps/trip-tick/web/src/components/lists/ListEditor.tsx b/apps/trip-tick/web/src/components/lists/ListEditor.tsx
index 681894fc..47980823 100644
--- a/apps/trip-tick/web/src/components/lists/ListEditor.tsx
+++ b/apps/trip-tick/web/src/components/lists/ListEditor.tsx
@@ -8,7 +8,7 @@ import { debounce } from '@a-type/utils';
import { OnboardingTooltip } from '@biscuits/client';
import { List } from '@trip-tick.biscuits/verdant';
import { forwardRef, useState } from 'react';
-import { toast } from 'react-hot-toast';
+import { toast } from '@a-type/ui';
import { AddSuggested } from './AddSuggested.jsx';
import { ListInfoEditor } from './ListInfoEditor.jsx';
import { ListItemEditor } from './ListItemEditor.jsx';
diff --git a/apps/trip-tick/web/src/components/lists/ListMenu.tsx b/apps/trip-tick/web/src/components/lists/ListMenu.tsx
index b605a455..18394eb3 100644
--- a/apps/trip-tick/web/src/components/lists/ListMenu.tsx
+++ b/apps/trip-tick/web/src/components/lists/ListMenu.tsx
@@ -10,7 +10,7 @@ import {
import { Icon } from '@a-type/ui/components/icon';
import { List } from '@trip-tick.biscuits/verdant';
import { useNavigate } from '@verdant-web/react-router';
-import { toast } from 'react-hot-toast';
+import { toast } from '@a-type/ui';
export function ListMenu({ list }: { list: List }) {
const client = hooks.useClient();
diff --git a/apps/trip-tick/web/src/components/trips/TripMenu.tsx b/apps/trip-tick/web/src/components/trips/TripMenu.tsx
index 2f7d5ab9..d6e168ad 100644
--- a/apps/trip-tick/web/src/components/trips/TripMenu.tsx
+++ b/apps/trip-tick/web/src/components/trips/TripMenu.tsx
@@ -8,7 +8,7 @@ import {
} from '@a-type/ui/components/dropdownMenu';
import { Icon } from '@a-type/ui/components/icon';
import { useNavigate } from '@verdant-web/react-router';
-import { toast } from 'react-hot-toast';
+import { toast } from '@a-type/ui';
export interface TripMenuProps {
tripId: string;
diff --git a/apps/trip-tick/web/src/components/weather/LocationSelect.tsx b/apps/trip-tick/web/src/components/weather/LocationSelect.tsx
index 4f9cc385..b79a97fd 100644
--- a/apps/trip-tick/web/src/components/weather/LocationSelect.tsx
+++ b/apps/trip-tick/web/src/components/weather/LocationSelect.tsx
@@ -18,7 +18,7 @@ import { useCombobox } from 'downshift';
import { preventDefault } from '@a-type/utils';
import { useCallback, useRef, useState, useTransition } from 'react';
import { useSize } from '@a-type/ui/hooks';
-import { toast } from 'react-hot-toast';
+import { toast } from '@a-type/ui';
import { Button } from '@a-type/ui/components/button';
import { Icon } from '@a-type/ui/components/icon';
diff --git a/apps/trip-tick/web/src/pages/Pages.tsx b/apps/trip-tick/web/src/pages/Pages.tsx
index e41f8080..bc634458 100644
--- a/apps/trip-tick/web/src/pages/Pages.tsx
+++ b/apps/trip-tick/web/src/pages/Pages.tsx
@@ -1,7 +1,5 @@
-import { TopLoader } from '@/components/nav/TopLoader.jsx';
import { updateApp, updateState } from '@/updateState.js';
import { PageRoot } from '@a-type/ui/components/layouts';
-import { Essentials } from '@biscuits/client';
import { Outlet, Router, makeRoutes } from '@verdant-web/react-router';
import { useCallback } from 'react';
import { lazyWithPreload as lazy } from 'react-lazy-with-preload';
@@ -49,8 +47,6 @@ export function Pages() {
-
-
);
diff --git a/apps/trip-tick/web/src/pages/SettingsPage.tsx b/apps/trip-tick/web/src/pages/SettingsPage.tsx
index d84715af..b10141f8 100644
--- a/apps/trip-tick/web/src/pages/SettingsPage.tsx
+++ b/apps/trip-tick/web/src/pages/SettingsPage.tsx
@@ -7,7 +7,7 @@ import { H1 } from '@a-type/ui/components/typography';
import { DarkModeToggle, usePageTitle } from '@biscuits/client';
import { ManageStorage } from '@biscuits/client/storage';
import { AutoRestoreScroll, Link } from '@verdant-web/react-router';
-import { toast } from 'react-hot-toast';
+import { toast } from '@a-type/ui';
export interface SettingsPageProps {}
diff --git a/apps/wish-wash/.vscode/tasks.json b/apps/wish-wash/.vscode/tasks.json
index edb179da..964bb2b8 100644
--- a/apps/wish-wash/.vscode/tasks.json
+++ b/apps/wish-wash/.vscode/tasks.json
@@ -6,7 +6,7 @@
"script": "app-dev",
"path": "web",
"problemMatcher": [],
- "label": "npm: app-dev - web",
+ "label": "Dev",
"detail": "vite"
}
]
diff --git a/apps/wish-wash/web/package.json b/apps/wish-wash/web/package.json
index d812fc9d..ff18b174 100644
--- a/apps/wish-wash/web/package.json
+++ b/apps/wish-wash/web/package.json
@@ -9,18 +9,17 @@
"preview": "vite preview"
},
"dependencies": {
- "@a-type/ui": "^0.8.20",
- "@a-type/utils": "^1.0.8",
+ "@a-type/ui": "0.8.21",
+ "@a-type/utils": "1.1.3",
"@biscuits/client": "workspace:*",
"@react-spring/web": "^9.7.3",
"@unocss/transformer-variant-group": "^0.54.1",
"@verdant-web/react-router": "^0.6.2",
"@wish-wash.biscuits/verdant": "workspace:*",
"react": "18.3.1",
- "react-dom": "^18.2.0",
- "react-hot-toast": "^2.4.1",
- "unocss": "0.58.8",
- "valtio": "^1.12.1",
+ "react-dom": "18.3.1",
+ "unocss": "0.61.0",
+ "valtio": "1.13.2",
"workbox-core": "^6.5.4",
"workbox-expiration": "^6.5.4",
"workbox-precaching": "^6.5.4",
@@ -29,10 +28,11 @@
"workbox-window": "^6.5.4"
},
"devDependencies": {
- "@types/react": "^18.2.79",
- "@types/react-dom": "^18.2.25",
- "@vitejs/plugin-react-swc": "3.5.0",
- "vite": "5.2.7",
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react-swc": "3.7.0",
+ "@originjs/vite-plugin-commonjs": "1.0.3",
+ "vite": "5.3.1",
"vite-plugin-pwa": "^0.19.8"
}
}
diff --git a/apps/wish-wash/web/public/favicon.ico b/apps/wish-wash/web/public/favicon.ico
new file mode 100644
index 00000000..85000bf9
Binary files /dev/null and b/apps/wish-wash/web/public/favicon.ico differ
diff --git a/apps/wish-wash/web/public/icon.png b/apps/wish-wash/web/public/icon.png
new file mode 100644
index 00000000..53959604
Binary files /dev/null and b/apps/wish-wash/web/public/icon.png differ
diff --git a/apps/wish-wash/web/src/App.tsx b/apps/wish-wash/web/src/App.tsx
new file mode 100644
index 00000000..4ae82840
--- /dev/null
+++ b/apps/wish-wash/web/src/App.tsx
@@ -0,0 +1,53 @@
+import { Pages } from '@/pages/Pages.jsx';
+import { clientDescriptor, hooks } from '@/store.js';
+import { ErrorBoundary } from '@a-type/ui/components/errorBoundary';
+import { Provider as UIProvider } from '@a-type/ui/components/provider';
+import { H1, P } from '@a-type/ui/components/typography';
+import { useVisualViewportOffset } from '@a-type/ui/hooks';
+import { Provider, ReloadButton } from '@biscuits/client';
+import { ReactNode, Suspense } from 'react';
+
+export interface AppProps {}
+
+export function App({}: AppProps) {
+ useVisualViewportOffset();
+
+ return (
+ }>
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function VerdantProvider({ children }: { children: ReactNode }) {
+ return {children};
+}
+
+function ErrorFallback() {
+ return (
+
+
+
Something went wrong
+
+ Sorry about this. The app has crashed. You can try refreshing, but if
+ that doesn't work,{' '}
+
+ let me know about it.
+
+
+
+
+
+ );
+}
diff --git a/apps/wish-wash/web/src/components/brand/Logo.tsx b/apps/wish-wash/web/src/components/brand/Logo.tsx
new file mode 100644
index 00000000..201eeee3
--- /dev/null
+++ b/apps/wish-wash/web/src/components/brand/Logo.tsx
@@ -0,0 +1,7 @@
+export interface LogoProps {
+ className?: string;
+}
+
+export function Logo({ className }: LogoProps) {
+ return ;
+}
diff --git a/apps/wish-wash/web/src/components/lists/CreateItemButton.tsx b/apps/wish-wash/web/src/components/lists/CreateItemButton.tsx
new file mode 100644
index 00000000..fed84895
--- /dev/null
+++ b/apps/wish-wash/web/src/components/lists/CreateItemButton.tsx
@@ -0,0 +1,29 @@
+import { ButtonProps, Button } from '@a-type/ui/components/button';
+import { Icon } from '@a-type/ui/components/icon';
+import { List } from '@wish-wash.biscuits/verdant';
+
+export interface CreateItemButtonProps extends ButtonProps {
+ list: List;
+}
+
+export function CreateItemButton({
+ list,
+ children,
+ ...rest
+}: CreateItemButtonProps) {
+ const createItem = () => {
+ const item = list.get('items').push({
+ description: 'New idea',
+ });
+ };
+ return (
+
+ );
+}
diff --git a/apps/wish-wash/web/src/components/lists/CreateListButton.tsx b/apps/wish-wash/web/src/components/lists/CreateListButton.tsx
new file mode 100644
index 00000000..e39961cc
--- /dev/null
+++ b/apps/wish-wash/web/src/components/lists/CreateListButton.tsx
@@ -0,0 +1,31 @@
+import { hooks } from '@/store.js';
+import { ButtonProps, Button } from '@a-type/ui/components/button';
+import { Icon } from '@a-type/ui/components/icon';
+import { useNavigate } from '@verdant-web/react-router';
+import { MouseEvent } from 'react';
+
+export interface CreateListButtonProps extends ButtonProps {}
+
+export function CreateListButton({
+ children,
+ onClick,
+ ...props
+}: CreateListButtonProps) {
+ const client = hooks.useClient();
+ const navigate = useNavigate();
+ const createList = async (ev: MouseEvent) => {
+ const list = await client.lists.put({ name: 'New list' });
+ navigate(`/list/${list.get('id')}`);
+ onClick?.(ev);
+ };
+ return (
+
+ );
+}
diff --git a/apps/wish-wash/web/src/components/lists/ListItem.tsx b/apps/wish-wash/web/src/components/lists/ListItem.tsx
new file mode 100644
index 00000000..bf2a3bb0
--- /dev/null
+++ b/apps/wish-wash/web/src/components/lists/ListItem.tsx
@@ -0,0 +1,34 @@
+import { ListItemsItem } from '@wish-wash.biscuits/verdant';
+import { Checkbox } from '@a-type/ui/components/checkbox';
+import { hooks } from '@/store.js';
+
+export interface ListItemProps {
+ item: ListItemsItem;
+}
+
+export function ListItem({ item }: ListItemProps) {
+ const { purchasedAt, description, link } = hooks.useWatch(item);
+
+ return (
+
+
{
+ if (val) {
+ item.set('purchasedAt', Date.now());
+ } else {
+ item.set('purchasedAt', null);
+ }
+ }}
+ />
+
+
+ );
+}
diff --git a/apps/wish-wash/web/src/components/lists/ListView.tsx b/apps/wish-wash/web/src/components/lists/ListView.tsx
new file mode 100644
index 00000000..87aa1435
--- /dev/null
+++ b/apps/wish-wash/web/src/components/lists/ListView.tsx
@@ -0,0 +1,28 @@
+import { hooks } from '@/store.js';
+import { H1 } from '@a-type/ui/components/typography';
+import { List } from '@wish-wash.biscuits/verdant';
+import { ListItem } from './ListItem.jsx';
+import { PageNowPlaying } from '@a-type/ui/components/layouts';
+import { CreateItemButton } from './CreateItemButton.jsx';
+
+export interface ListViewProps {
+ list: List;
+}
+
+export function ListView({ list }: ListViewProps) {
+ const { name, items } = hooks.useWatch(list);
+ hooks.useWatch(items);
+ return (
+
+
{name}
+
+ {items.map((item) => (
+
+ ))}
+
+
+
+
+
+ );
+}
diff --git a/apps/wish-wash/web/src/components/nav/NavBar.tsx b/apps/wish-wash/web/src/components/nav/NavBar.tsx
new file mode 100644
index 00000000..539522c7
--- /dev/null
+++ b/apps/wish-wash/web/src/components/nav/NavBar.tsx
@@ -0,0 +1,58 @@
+import { PageNav } from '@a-type/ui/components/layouts';
+import { Link, useOnLocationChange } from '@verdant-web/react-router';
+import {
+ NavBarRoot,
+ NavBarItem,
+ NavBarItemText,
+ NavBarItemIconWrapper,
+ NavBarItemIcon,
+} from '@a-type/ui/components/navBar';
+import { useState } from 'react';
+import { AppPickerNavItem } from '@biscuits/client';
+import { Logo } from '../brand/Logo.jsx';
+
+export interface NavigationProps {}
+
+export function Navigation({}: NavigationProps) {
+ const [pathname, setPathname] = useState(() => window.location.pathname);
+ useOnLocationChange((location) => setPathname(location.pathname));
+ const matchTrips = pathname === '/' || pathname.startsWith('/trips');
+ const matchLists = pathname.startsWith('/lists');
+
+ return (
+
+
+
+
+ Trip Tick
+
+
+
+
+
+
+
+
+ Trips
+
+
+
+
+
+
+
+ Lists
+
+
+
+
+
+
+ );
+}
diff --git a/apps/wish-wash/web/src/main.tsx b/apps/wish-wash/web/src/main.tsx
new file mode 100644
index 00000000..c98daf06
--- /dev/null
+++ b/apps/wish-wash/web/src/main.tsx
@@ -0,0 +1,15 @@
+import 'virtual:uno.css';
+import { StrictMode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { App } from './App.jsx';
+
+function main() {
+ const root = createRoot(document.getElementById('root')!);
+ root.render(
+
+
+ ,
+ );
+}
+
+main();
diff --git a/apps/wish-wash/web/src/pages/HomePage.tsx b/apps/wish-wash/web/src/pages/HomePage.tsx
new file mode 100644
index 00000000..b747588d
--- /dev/null
+++ b/apps/wish-wash/web/src/pages/HomePage.tsx
@@ -0,0 +1,44 @@
+import { CreateListButton } from '@/components/lists/CreateListButton.jsx';
+import { hooks } from '@/store.js';
+import { PageContent } from '@a-type/ui/components/layouts';
+import { H1, P } from '@a-type/ui/components/typography';
+import { useLocalStorage } from '@biscuits/client';
+import { useNavigate } from '@verdant-web/react-router';
+import { useEffect } from 'react';
+
+export interface HomePageProps {}
+
+export function HomePage({}: HomePageProps) {
+ const [lastList] = useLocalStorage('last-list', null);
+ const someList = hooks.useOneList();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ if (lastList || someList) {
+ navigate(`/list/${lastList || someList!.get('id')}`, {
+ replace: true,
+ skipTransition: true,
+ });
+ }
+ }, [navigate, lastList, someList]);
+
+ return (
+
+
+
+ );
+}
+
+export default HomePage;
+
+function EmptyContent() {
+ return (
+ <>
+ No lists!
+
+ You might have deleted all your lists. You can create a new one below.
+
+
+ >
+ );
+}
diff --git a/apps/wish-wash/web/src/pages/ListPage.tsx b/apps/wish-wash/web/src/pages/ListPage.tsx
new file mode 100644
index 00000000..77a464b7
--- /dev/null
+++ b/apps/wish-wash/web/src/pages/ListPage.tsx
@@ -0,0 +1,47 @@
+import { ListView } from '@/components/lists/ListView.jsx';
+import { hooks } from '@/store.js';
+import { Button } from '@a-type/ui/components/button';
+import { PageContent } from '@a-type/ui/components/layouts';
+import { H1 } from '@a-type/ui/components/typography';
+import { useLocalStorage } from '@biscuits/client';
+import { Link, useNavigate, useParams } from '@verdant-web/react-router';
+import { useEffect } from 'react';
+
+export interface ListPageProps {}
+
+export function ListPage({}: ListPageProps) {
+ const { listId } = useParams();
+ const [_, setLastList] = useLocalStorage('last-list', null);
+
+ const list = hooks.useList(listId);
+
+ useEffect(() => {
+ setLastList(listId);
+ }, [listId, setLastList]);
+ const navigate = useNavigate();
+ useEffect(() => {
+ if (!list) {
+ setLastList(null);
+ navigate('/');
+ }
+ }, [!!list, setLastList]);
+
+ if (!list) {
+ return (
+
+ List not found
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+}
+
+export default ListPage;
diff --git a/apps/wish-wash/web/src/pages/Pages.tsx b/apps/wish-wash/web/src/pages/Pages.tsx
new file mode 100644
index 00000000..5302415a
--- /dev/null
+++ b/apps/wish-wash/web/src/pages/Pages.tsx
@@ -0,0 +1,72 @@
+import { makeRoutes, Outlet, Router } from '@verdant-web/react-router';
+import { HomePage } from './HomePage.jsx';
+import { H1, P } from '@a-type/ui/components/typography';
+import { ErrorBoundary } from '@a-type/ui/components/errorBoundary';
+import { Button } from '@a-type/ui/components/button';
+import { Spinner } from '@a-type/ui/components/spinner';
+import { lazy, useCallback, Suspense } from 'react';
+import { updateApp, updateState } from '@/updateState.js';
+import { Link } from '@verdant-web/react-router';
+import { ReloadButton } from '@biscuits/client';
+import ListPage from './ListPage.jsx';
+import { PageRoot } from '@a-type/ui/components/layouts';
+
+const routes = makeRoutes([
+ {
+ path: '/',
+ exact: true,
+ component: HomePage,
+ },
+ {
+ path: '/list/:listId',
+ component: ListPage,
+ },
+ {
+ path: '/settings',
+ component: lazy(() => import('./SettingsPage.jsx')),
+ },
+]);
+
+export function Pages() {
+ const handleNavigate = useCallback(
+ (_: Location, ev: { state?: any; skipTransition?: boolean }) => {
+ if (updateState.updateAvailable) {
+ console.info('Update ready to install, intercepting navigation...');
+ updateApp();
+ return false;
+ }
+ },
+ [],
+ );
+ return (
+ }>
+ }>
+
+
+
+
+
+
+
+ );
+}
+
+function ErrorFallback({ clearError }: { clearError: () => void }) {
+ return (
+
+
+
Something went wrong
+
+ Sorry about this. The app has crashed. You can try refreshing, but if
+ that doesn't work, use the button below to report the issue.
+
+
+
+
+
+ );
+}
diff --git a/apps/wish-wash/web/src/pages/SettingsPage.tsx b/apps/wish-wash/web/src/pages/SettingsPage.tsx
new file mode 100644
index 00000000..34c8eec7
--- /dev/null
+++ b/apps/wish-wash/web/src/pages/SettingsPage.tsx
@@ -0,0 +1,26 @@
+import { useEffect } from 'react';
+import { checkForUpdate } from '@/updateState.js';
+import { H1 } from '@a-type/ui/components/typography';
+import { DarkModeToggle } from '@biscuits/client';
+import { PageContent } from '@a-type/ui/components/layouts';
+
+export interface SettingsPageProps {}
+
+export function SettingsPage({}: SettingsPageProps) {
+ useEffect(() => {
+ checkForUpdate();
+ }, []);
+
+ return (
+
+
+
+ );
+}
+
+export default SettingsPage;
diff --git a/apps/wish-wash/web/src/privateStore.ts b/apps/wish-wash/web/src/privateStore.ts
new file mode 100644
index 00000000..1987e9e5
--- /dev/null
+++ b/apps/wish-wash/web/src/privateStore.ts
@@ -0,0 +1,39 @@
+import {
+ ClientDescriptor,
+ createHooks,
+ migrations,
+ UserInfo,
+} from '@wish-wash.biscuits/verdant';
+import { getVerdantSync, VerdantProfile } from '@biscuits/client';
+import { undoHistory } from './undo.js';
+
+export interface Presence {
+ /**
+ * Put any transient presence state for users
+ * you want here
+ */
+}
+
+export type Participant = UserInfo;
+
+export const privateHooks = createHooks();
+
+export const privateClientDescriptor = new ClientDescriptor({
+ namespace: 'private_wish-wash',
+ migrations,
+ undoHistory,
+ sync: getVerdantSync({
+ appId: 'wish-wash',
+ access: 'user',
+ initialPresence: {} satisfies Presence,
+ }),
+});
+
+// these are some helpers I like to use. You can delete them if you want.
+
+async function exposeClientOnWindowForDebug() {
+ const privateClient = await privateClientDescriptor.open();
+ (window as any).client = privateClient;
+}
+
+exposeClientOnWindowForDebug();
diff --git a/apps/wish-wash/web/src/service-worker.ts b/apps/wish-wash/web/src/service-worker.ts
new file mode 100644
index 00000000..b9e8640c
--- /dev/null
+++ b/apps/wish-wash/web/src/service-worker.ts
@@ -0,0 +1,60 @@
+///
+/* eslint-disable no-restricted-globals */
+
+// This service worker can be customized!
+// See https://developers.google.com/web/tools/workbox/modules
+// for the list of available Workbox modules, or add any other
+// code you'd like.
+// You can also remove this file if you'd prefer not to use a
+// service worker, and the Workbox build step will be skipped.
+
+import { ExpirationPlugin } from 'workbox-expiration';
+import {
+ precacheAndRoute,
+ createHandlerBoundToURL,
+ cleanupOutdatedCaches,
+} from 'workbox-precaching';
+import { NavigationRoute, registerRoute } from 'workbox-routing';
+import { StaleWhileRevalidate } from 'workbox-strategies';
+
+declare const self: ServiceWorkerGlobalScope;
+
+// auto-update on ready
+// self.skipWaiting();
+// clientsClaim();
+
+cleanupOutdatedCaches();
+
+// Precache all of the assets generated by your build process.
+// Their URLs are injected into the manifest variable below.
+// This variable must be present somewhere in your service worker file,
+// even if you decide not to use precaching. See https://cra.link/PWA
+precacheAndRoute(self.__WB_MANIFEST);
+
+registerRoute(new NavigationRoute(createHandlerBoundToURL('/index.html')));
+
+// An example runtime caching route for requests that aren't handled by the
+// precache, in this case same-origin .png requests like those from in public/
+registerRoute(
+ // Add in any other file extensions or routing criteria as needed.
+ ({ url }) =>
+ url.origin === self.location.origin && url.pathname.endsWith('.png'),
+ // Customize this strategy as needed, e.g., by changing to CacheFirst.
+ new StaleWhileRevalidate({
+ cacheName: 'images',
+ plugins: [
+ // Ensure that once this runtime cache reaches a maximum size the
+ // least-recently used images are removed.
+ new ExpirationPlugin({ maxEntries: 50 }),
+ ],
+ }),
+);
+
+// This allows the web app to trigger skipWaiting via
+// registration.waiting.postMessage({type: 'SKIP_WAITING'})
+self.addEventListener('message', (event) => {
+ if (event.data && event.data.type === 'SKIP_WAITING') {
+ console.log('Skip waiting');
+ self.skipWaiting();
+ }
+});
diff --git a/apps/wish-wash/web/src/store.ts b/apps/wish-wash/web/src/store.ts
new file mode 100644
index 00000000..b15a0a5b
--- /dev/null
+++ b/apps/wish-wash/web/src/store.ts
@@ -0,0 +1,39 @@
+import {
+ ClientDescriptor,
+ createHooks,
+ migrations,
+ UserInfo,
+} from '@wish-wash.biscuits/verdant';
+import { getVerdantSync, VerdantProfile } from '@biscuits/client';
+import { undoHistory } from './undo.js';
+
+export interface Presence {
+ /**
+ * Put any transient presence state for users
+ * you want here
+ */
+}
+
+export type Participant = UserInfo;
+
+export const hooks = createHooks();
+
+export const clientDescriptor = new ClientDescriptor({
+ namespace: 'wish-wash',
+ migrations,
+ undoHistory,
+ sync: getVerdantSync({
+ appId: 'wish-wash',
+ access: 'members',
+ initialPresence: {} satisfies Presence,
+ }),
+});
+
+// these are some helpers I like to use. You can delete them if you want.
+
+async function exposeClientOnWindowForDebug() {
+ const client = await clientDescriptor.open();
+ (window as any).client = client;
+}
+
+exposeClientOnWindowForDebug();
diff --git a/apps/wish-wash/web/src/undo.ts b/apps/wish-wash/web/src/undo.ts
new file mode 100644
index 00000000..e8c21a6a
--- /dev/null
+++ b/apps/wish-wash/web/src/undo.ts
@@ -0,0 +1,27 @@
+import { UndoHistory } from '@wish-wash.biscuits/verdant';
+
+export const undoHistory = new UndoHistory();
+
+async function registerUndoKeybinds() {
+ document.addEventListener('keydown', async (e) => {
+ if (e.key === 'z' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
+ e.preventDefault();
+ const result = await undoHistory.undo();
+ if (!result) {
+ console.log('Nothing to undo');
+ }
+ }
+ if (
+ (e.key === 'y' && (e.ctrlKey || e.metaKey)) ||
+ (e.key === 'z' && e.shiftKey && (e.ctrlKey || e.metaKey))
+ ) {
+ e.preventDefault();
+ const result = await undoHistory.redo();
+ if (!result) {
+ console.log('Nothing to redo');
+ }
+ }
+ });
+}
+
+registerUndoKeybinds();
diff --git a/apps/wish-wash/web/src/updateState.ts b/apps/wish-wash/web/src/updateState.ts
new file mode 100644
index 00000000..26793ec7
--- /dev/null
+++ b/apps/wish-wash/web/src/updateState.ts
@@ -0,0 +1,43 @@
+import { proxy } from 'valtio';
+import { registerSW } from 'virtual:pwa-register';
+
+export const updateState = proxy({
+ updateAvailable: false,
+});
+
+let check: (() => void) | undefined = undefined;
+
+const update = registerSW({
+ onNeedRefresh() {
+ updateState.updateAvailable = true;
+ console.log('Update available and ready to install');
+ },
+ onRegisteredSW(swUrl, registration) {
+ console.log('Service worker registered', swUrl);
+ if (registration) {
+ setInterval(
+ () => {
+ registration.update();
+ check = registration.update;
+ // hourly
+ },
+ 60 * 60 * 1000,
+ );
+ }
+ },
+ onRegisterError(error) {
+ console.error('Service worker registration error', error);
+ },
+});
+
+export async function updateApp(reload?: boolean) {
+ let timeout = setTimeout(() => {
+ window.location.reload();
+ }, 5000);
+ await update(!!reload);
+ clearTimeout(timeout);
+}
+
+export function checkForUpdate() {
+ check?.();
+}
diff --git a/apps/wish-wash/web/src/vite-env.d.ts b/apps/wish-wash/web/src/vite-env.d.ts
new file mode 100644
index 00000000..0c32a012
--- /dev/null
+++ b/apps/wish-wash/web/src/vite-env.d.ts
@@ -0,0 +1,51 @@
+///
+///
+
+// declare module 'prosemirror-state' {
+// export * from 'prosemirror-state/dist/index.d.ts';
+// }
+
+// extend navigator with wakelock API
+declare interface Navigator {
+ wakeLock: any;
+}
+
+// declare global WakeLockSentinel type
+declare type WakeLockSentinel = any;
+
+declare module 'virtual:pwa-register/react' {
+ // @ts-expect-error ignore when react is not installed
+ import type { Dispatch, SetStateAction } from 'react';
+
+ export interface RegisterSWOptions {
+ immediate?: boolean;
+ onNeedRefresh?: () => void;
+ onOfflineReady?: () => void;
+ /**
+ * Called only if `onRegisteredSW` is not provided.
+ *
+ * @deprecated Use `onRegisteredSW` instead.
+ * @param registration The service worker registration if available.
+ */
+ onRegistered?: (
+ registration: ServiceWorkerRegistration | undefined,
+ ) => void;
+ /**
+ * Called once the service worker is registered (requires version `0.12.8+`).
+ *
+ * @param swScriptUrl The service worker script url.
+ * @param registration The service worker registration if available.
+ */
+ onRegisteredSW?: (
+ swScriptUrl: string,
+ registration: ServiceWorkerRegistration | undefined,
+ ) => void;
+ onRegisterError?: (error: any) => void;
+ }
+
+ export function useRegisterSW(options?: RegisterSWOptions): {
+ needRefresh: [boolean, Dispatch>];
+ offlineReady: [boolean, Dispatch>];
+ updateServiceWorker: (reloadPage?: boolean) => Promise;
+ };
+}
diff --git a/apps/wish-wash/web/vite.config.ts b/apps/wish-wash/web/vite.config.ts
index 9977148e..e62a1399 100644
--- a/apps/wish-wash/web/vite.config.ts
+++ b/apps/wish-wash/web/vite.config.ts
@@ -3,6 +3,7 @@ import react from '@vitejs/plugin-react-swc';
import { fileURLToPath } from 'url';
import { VitePWA } from 'vite-plugin-pwa';
import UnoCSS from 'unocss/vite';
+import { viteCommonjs } from '@originjs/vite-plugin-commonjs';
// https://vitejs.dev/config/
export default defineConfig(({ command, mode }) => ({
@@ -37,6 +38,7 @@ export default defineConfig(({ command, mode }) => ({
navigateFallback: 'index.html',
},
}),
+ viteCommonjs(),
],
optimizeDeps: {
exclude: ['@a-type/ui'],
diff --git a/blog/package.json b/blog/package.json
index e6817d52..953bf7c0 100644
--- a/blog/package.json
+++ b/blog/package.json
@@ -10,7 +10,7 @@
"astro": "astro"
},
"dependencies": {
- "@a-type/ui": "^0.8.20",
+ "@a-type/ui": "^0.8.21",
"@astrojs/check": "^0.5.10",
"@astrojs/mdx": "^2.3.1",
"@astrojs/rss": "^4.0.5",
diff --git a/packages/apps/src/index.ts b/packages/apps/src/index.ts
index e6c8fae7..da524ddf 100644
--- a/packages/apps/src/index.ts
+++ b/packages/apps/src/index.ts
@@ -83,24 +83,24 @@ export const apps = [
'Now everyone can be on the same page when packing. Plus, get a weather forecast and more powerful trip planning tools.',
} as AppManifest<'trip-tick'>,
{
- id: 'shopping',
+ id: 'wish-wash',
demoVideoSrc: '',
description: 'TODO',
devOriginOverride: 'http://localhost:6222',
iconPath: 'icon.png',
- name: 'Shopping',
+ name: 'Wish Wash',
paidDescription: 'TODO',
paidFeatures: [],
- url: 'https://shopping.biscuits.club',
+ url: 'https://wish-wash.biscuits.club',
prerelease: true,
- } as AppManifest<'shopping'>,
+ } as AppManifest<'wish-wash'>,
{
id: 'marginalia',
demoVideoSrc: '',
description: 'TODO',
devOriginOverride: 'http://localhost:6223',
iconPath: 'icon.png',
- name: 'Bible',
+ name: 'Marginalia',
paidDescription: 'TODO',
paidFeatures: [],
url: 'https://marginalia.biscuits.club',
diff --git a/packages/client/package.json b/packages/client/package.json
index 08176870..4e7232ed 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -52,14 +52,17 @@
"peerDependencies": {
"@a-type/ui": "^0.6.17",
"@verdant-web/store": "^3.2.2",
+ "@verdant-web/react-router": "^0.6.0",
+ "@react-spring/web": "^9",
"react": "18.3.1",
"valtio": "^1.13.2",
"vite-plugin-pwa": "0.19.2"
},
"devDependencies": {
- "@a-type/ui": "^0.8.20",
+ "@a-type/ui": "^0.8.21",
"@types/react": "18.3.3",
"@verdant-web/store": "^3.6.4",
+ "@verdant-web/react-router": "0.6.2",
"react": "18.3.1",
"vite-plugin-pwa": "0.19.2"
}
diff --git a/packages/client/src/components/Context.tsx b/packages/client/src/components/Context.tsx
index b0fe0589..4a44a02c 100644
--- a/packages/client/src/components/Context.tsx
+++ b/packages/client/src/components/Context.tsx
@@ -3,24 +3,40 @@ import { ReactNode, createContext, useContext } from 'react';
import { AppId } from '@biscuits/apps';
import { VerdantContext } from '../verdant.js';
import { ClientDescriptor } from '@verdant-web/store';
-import { VerdantProfile } from '../index.js';
+import {
+ AppPreviewNotice,
+ Essentials,
+ PrereleaseWarning,
+ TopLoader,
+ VerdantProfile,
+} from '../index.js';
+import { graphqlClient as defaultClient } from '../index.js';
+import { GlobalSyncingIndicator } from './GlobalSyncingIndicator.js';
+import { useVisualViewportOffset } from '@a-type/ui/hooks';
export function Provider({
- graphqlClient,
+ graphqlClient = defaultClient,
appId,
children,
storeDescriptor = null,
}: {
appId?: AppId;
- graphqlClient: ApolloClient;
+ graphqlClient?: ApolloClient;
children: ReactNode;
storeDescriptor?: ClientDescriptor | null;
}) {
+ useVisualViewportOffset();
+
return (
+ {appId && }
+ {appId && }
+
+ {storeDescriptor && }
{children}
+
diff --git a/packages/client/src/components/GlobalSyncingIndicator.tsx b/packages/client/src/components/GlobalSyncingIndicator.tsx
new file mode 100644
index 00000000..3b8c2eca
--- /dev/null
+++ b/packages/client/src/components/GlobalSyncingIndicator.tsx
@@ -0,0 +1,33 @@
+import classNames from 'classnames';
+import { useContext, useEffect, useState } from 'react';
+import { VerdantContext } from '../verdant.js';
+import { Icon } from '@a-type/ui/components/icon';
+
+export interface GlobalSyncingIndicatorProps {}
+
+export function GlobalSyncingIndicator({}: GlobalSyncingIndicatorProps) {
+ const [syncing, setSyncing] = useState(false);
+ const clientDesc = useContext(VerdantContext);
+ useEffect(() => {
+ if (!clientDesc?.current) return;
+ const client = clientDesc.current;
+ return client.sync.subscribe('syncingChange', setSyncing);
+ }, [clientDesc]);
+
+ if (!clientDesc) return null;
+
+ return (
+
+
+ Syncing
+
+ );
+}
diff --git a/packages/client/src/components/LogoutNotice.tsx b/packages/client/src/components/LogoutNotice.tsx
index 1ae99485..de07b7f1 100644
--- a/packages/client/src/components/LogoutNotice.tsx
+++ b/packages/client/src/components/LogoutNotice.tsx
@@ -18,6 +18,8 @@ export function LogoutNotice({}: LogoutNoticeProps) {
const [isLoggedIn, loadingLoggedInStatus] = useIsLoggedIn();
const [close, setClose] = useState(false);
+ const isLoginPage = window.location.pathname === '/login';
+
const wasLoggedInButNowLoggedOut =
!close && wasLoggedIn && !isLoggedIn && !loadingLoggedInStatus;
@@ -29,6 +31,8 @@ export function LogoutNotice({}: LogoutNoticeProps) {
}
}, [isLoggedIn, setWasLoggedIn]);
+ if (isLoginPage) return null;
+
return (