From daf5d58f4f3a47f2d227810c4a23de47b8249ef4 Mon Sep 17 00:00:00 2001 From: Grant Forrest Date: Tue, 12 Nov 2024 17:43:28 -0500 Subject: [PATCH] full screen height --- apps/gnocchi/hub/src/components/MainImage.tsx | 22 +- apps/gnocchi/hub/src/components/layout.tsx | 28 +- .../web/src/components/addBar/AddBar.tsx | 328 +++++------ .../web/src/components/addBar/AddPane.tsx | 2 +- .../recipes/collection/RecipeList.tsx | 158 +++--- .../recipes/collection/RecipeListItem.tsx | 4 +- .../recipes/viewer/IngredientText.tsx | 96 ++-- .../recipes/viewer/RecipeOverview.tsx | 2 +- .../viewer/TextWithMultipliedNumbers.tsx | 78 +-- apps/gnocchi/web/src/pages/SplashPage.tsx | 508 +++++++++--------- apps/palette/web/src/pages/ProjectPage.tsx | 4 +- .../src/components/project/CameraControls.tsx | 46 +- .../src/components/project/SelectionMenu.tsx | 150 +++--- .../web/src/components/nav/RouteDialog.tsx | 63 +-- .../web/src/components/items/ListItem.tsx | 2 +- .../web/src/components/lists/ItemSorter.tsx | 223 ++++---- packages/client/src/components/Explainer.tsx | 90 ++-- .../client/src/components/InstallHint.tsx | 302 ++++++----- web/src/components/promo/layout.tsx | 268 ++++----- web/src/pages/HomePage.tsx | 128 ++--- web/src/pages/promos/GnocchiPage.tsx | 309 ++++++----- 21 files changed, 1404 insertions(+), 1407 deletions(-) diff --git a/apps/gnocchi/hub/src/components/MainImage.tsx b/apps/gnocchi/hub/src/components/MainImage.tsx index 3f4f1624..15800a6f 100644 --- a/apps/gnocchi/hub/src/components/MainImage.tsx +++ b/apps/gnocchi/hub/src/components/MainImage.tsx @@ -2,18 +2,18 @@ import { TopLineImage } from './layout.jsx'; import { clsx } from '@a-type/ui'; export interface MainImageProps { - url: string; - title: string; + url: string; + title: string; } export function MainImage({ url, title }: MainImageProps) { - return ( - - {`A - - ); + return ( + + {`A + + ); } diff --git a/apps/gnocchi/hub/src/components/layout.tsx b/apps/gnocchi/hub/src/components/layout.tsx index bb092272..ef680cec 100644 --- a/apps/gnocchi/hub/src/components/layout.tsx +++ b/apps/gnocchi/hub/src/components/layout.tsx @@ -1,24 +1,24 @@ import { clsx } from '@a-type/ui'; export const TopLineRoot = (props: any) => ( -
+
); export const TopLineTitle = (props: any) => ( -
+
); export const TopLineImage = (props: any) => ( -
+
); diff --git a/apps/gnocchi/web/src/components/addBar/AddBar.tsx b/apps/gnocchi/web/src/components/addBar/AddBar.tsx index 79442996..496ec94e 100644 --- a/apps/gnocchi/web/src/components/addBar/AddBar.tsx +++ b/apps/gnocchi/web/src/components/addBar/AddBar.tsx @@ -2,193 +2,193 @@ import { AddToListDialog } from '@/components/recipes/viewer/AddToListDialog.jsx import useMergedRef from '@/hooks/useMergedRef.js'; import { Input } from '@a-type/ui/components/input'; import { - Popover, - PopoverAnchor, - PopoverContent, + Popover, + PopoverAnchor, + PopoverContent, } from '@a-type/ui/components/popover'; import { useSize } from '@a-type/ui/hooks'; import { preventDefault, stopPropagation } from '@a-type/utils'; import classNames from 'classnames'; -import { Suspense, forwardRef, useCallback, useRef, useState } from 'react'; +import { Suspense, forwardRef, useRef, useState } from 'react'; import { AddInput } from './AddInput.jsx'; import { SuggestionGroup } from './SuggestionGroup.jsx'; import { useAddBarCombobox, useAddBarSuggestions } from './hooks.js'; export interface AddBarProps { - className?: string; - onAdd: (text: string[]) => Promise | void; - showRichSuggestions?: boolean; - open?: boolean; - onOpenChange?: (open: boolean) => void; + className?: string; + onAdd: (text: string[]) => Promise | void; + showRichSuggestions?: boolean; + open?: boolean; + onOpenChange?: (open: boolean) => void; } export const AddBarImpl = forwardRef( - function AddBarImpl( - { - onAdd, - showRichSuggestions = false, - open, - onOpenChange, - className, - ...rest - }, - ref, - ) { - const [suggestionPrompt, setSuggestionPrompt] = useState(''); + function AddBarImpl( + { + onAdd, + showRichSuggestions = false, + open, + onOpenChange, + className, + ...rest + }, + ref, + ) { + const [suggestionPrompt, setSuggestionPrompt] = useState(''); - const { - allSuggestions, - placeholder, - expiresSoonSuggestions, - showExpiring, - showSuggested, - mainSuggestions, - matchSuggestions, - } = useAddBarSuggestions({ - showRichSuggestions, - suggestionPrompt, - }); + const { + allSuggestions, + placeholder, + expiresSoonSuggestions, + showExpiring, + showSuggested, + mainSuggestions, + matchSuggestions, + } = useAddBarSuggestions({ + showRichSuggestions, + suggestionPrompt, + }); - const contentRef = useRef(null); - const innerRef = useSize(({ width }) => { - if (contentRef.current) { - contentRef.current.style.width = width + 'px'; - } - }); + const contentRef = useRef(null); + const innerRef = useSize(({ width }) => { + if (contentRef.current) { + contentRef.current.style.width = width + 'px'; + } + }); - const { - combobox: { - isOpen, - getMenuProps, - getInputProps, - highlightedIndex, - getItemProps, - inputValue, - setInputValue, - selectItem, - openMenu, - }, - addingRecipe, - clearAddingRecipe, - onInputPaste, - } = useAddBarCombobox({ - setSuggestionPrompt, - allSuggestions, - onAdd, - onOpenChange, - open, - }); + const { + combobox: { + isOpen, + getMenuProps, + getInputProps, + highlightedIndex, + getItemProps, + inputValue, + setInputValue, + selectItem, + openMenu, + }, + addingRecipe, + clearAddingRecipe, + onInputPaste, + } = useAddBarCombobox({ + setSuggestionPrompt, + allSuggestions, + onAdd, + onOpenChange, + open, + }); - const mergedRef = useMergedRef(ref, innerRef); + const mergedRef = useMergedRef(ref, innerRef); - const noSuggestions = allSuggestions.length === 0; + const noSuggestions = allSuggestions.length === 0; - return ( - <> - - - setInputValue('')} - ref={mergedRef} - {...rest} - /> - - - {showSuggested && ( - - )} - {showExpiring && ( - - )} - {!noSuggestions && ( - - )} - {noSuggestions &&
No suggestions
} -
-
- {addingRecipe && ( - - )} - - ); - }, + return ( + <> + + + setInputValue('')} + ref={mergedRef} + {...rest} + /> + + + {showSuggested && ( + + )} + {showExpiring && ( + + )} + {!noSuggestions && ( + + )} + {noSuggestions &&
No suggestions
} +
+
+ {addingRecipe && ( + + )} + + ); + }, ); export const AddBar = forwardRef(function AddBar( - props, - ref, + props, + ref, ) { - return ( - }> - - - ); + return ( + }> + + + ); }); function Skeleton({ className }: { className?: string }) { - return ( -
- -
- ); + return ( +
+ +
+ ); } diff --git a/apps/gnocchi/web/src/components/addBar/AddPane.tsx b/apps/gnocchi/web/src/components/addBar/AddPane.tsx index 542fe21c..409b4d87 100644 --- a/apps/gnocchi/web/src/components/addBar/AddPane.tsx +++ b/apps/gnocchi/web/src/components/addBar/AddPane.tsx @@ -126,7 +126,7 @@ const AddPaneImpl = forwardRef< - - -
- - -
- -
-
- + return ( +
+ + +
+ + +
+ +
+
+ - - - + + + - - - + + + - - - - - - } - > - - - -
- ); + + + + + + } + > + + + +
+ ); } function RecipeListContent() { - const [recipes, { loadMore, hasMore }] = useFilteredRecipes(); - const [gridStyle] = useGridStyle(); + const [recipes, { loadMore, hasMore }] = useFilteredRecipes(); + const [gridStyle] = useGridStyle(); - if (!recipes.length) { - return ; - } + if (!recipes.length) { + return ; + } - return ( - <> - - {recipes.map((recipe) => ( - - ))} - - {hasMore && ( - - - - )} - - ); + return ( + <> + + {recipes.map((recipe) => ( + + ))} + + {hasMore && ( + + + + )} + + ); } function TagFilterList() { - const [tagFilter, setTagFilter] = useRecipeTagFilter(); - const toggleTagFilter = (value: string | null) => - tagFilter ? setTagFilter(null) : setTagFilter(value); + const [tagFilter, setTagFilter] = useRecipeTagFilter(); + const toggleTagFilter = (value: string | null) => + tagFilter ? setTagFilter(null) : setTagFilter(value); - return ( - - ); + return ( + + ); } diff --git a/apps/gnocchi/web/src/components/recipes/collection/RecipeListItem.tsx b/apps/gnocchi/web/src/components/recipes/collection/RecipeListItem.tsx index 68c9281c..e69d3eb6 100644 --- a/apps/gnocchi/web/src/components/recipes/collection/RecipeListItem.tsx +++ b/apps/gnocchi/web/src/components/recipes/collection/RecipeListItem.tsx @@ -50,8 +50,8 @@ export const RecipeListItem = memo(function RecipeListItem({ className={classNames( 'self-end', { - '!max-h-20vh': gridStyle === 'card-small', - 'min-h-200px md:(h-30vh max-h-300px)': !!mainImage, + '!max-h-20dvh': gridStyle === 'card-small', + 'min-h-200px md:(h-30dvh max-h-300px)': !!mainImage, }, 'shadow-sm', className, diff --git a/apps/gnocchi/web/src/components/recipes/viewer/IngredientText.tsx b/apps/gnocchi/web/src/components/recipes/viewer/IngredientText.tsx index beacccba..af2de5ba 100644 --- a/apps/gnocchi/web/src/components/recipes/viewer/IngredientText.tsx +++ b/apps/gnocchi/web/src/components/recipes/viewer/IngredientText.tsx @@ -6,62 +6,62 @@ import { Tooltip } from '@a-type/ui/components/tooltip'; import { TextWithMultipliedNumbers } from './TextWithMultipliedNumbers.jsx'; export interface IngredientTextProps { - ingredient: RecipeIngredientsItem; - multiplier: number; - className?: string; + ingredient: RecipeIngredientsItem; + multiplier: number; + className?: string; } export function IngredientText({ - ingredient, - multiplier, - className, + ingredient, + multiplier, + className, }: IngredientTextProps) { - const { text, quantity, unit, food, comments } = hooks.useWatch(ingredient); + const { text, quantity, unit, food, comments } = hooks.useWatch(ingredient); - if (multiplier !== 1) { - const finalQuantity = quantity * multiplier; - const showPlural = finalQuantity !== 1; - return ( - - - Multiplier {multiplier}x applied. Original value: {quantity} - - ) as any - } - > - - {fractionToText(finalQuantity)} - - {' '} - - {unit ? (showPlural ? pluralize(unit) : unit) : ''} - {' '} - - 0 - ? `, + if (multiplier !== 1) { + const finalQuantity = quantity * multiplier; + const showPlural = finalQuantity !== 1; + return ( + + + Multiplier {multiplier}x applied. Original value: {quantity} + + ) as any + } + > + + {fractionToText(finalQuantity)} + + {' '} + + {unit ? (showPlural ? pluralize(unit) : unit) : ''} + {' '} + + 0 + ? `, ${comments.map((comment) => comment).join(', ')}` - : '' - } - multiplier={multiplier} - /> - - ); - } + : '' + } + multiplier={multiplier} + /> + + ); + } - return {replaceNumbersWithFractions(text)}; + return {replaceNumbersWithFractions(text)}; } function replaceNumbersWithFractions(text: string) { - // include decimals - return text.replace(/(\d+\.\d+)/g, (match) => - fractionToText(parseFloat(match)), - ); + // include decimals + return text.replace(/(\d+\.\d+)/g, (match) => + fractionToText(parseFloat(match)), + ); } diff --git a/apps/gnocchi/web/src/components/recipes/viewer/RecipeOverview.tsx b/apps/gnocchi/web/src/components/recipes/viewer/RecipeOverview.tsx index f84de4bb..2e1ff878 100644 --- a/apps/gnocchi/web/src/components/recipes/viewer/RecipeOverview.tsx +++ b/apps/gnocchi/web/src/components/recipes/viewer/RecipeOverview.tsx @@ -95,7 +95,7 @@ function RecipeOverviewContent({ recipe }: { recipe: Recipe }) {

This is your copy!

Feel free to make changes, add notes, etc.

-
+
diff --git a/apps/gnocchi/web/src/components/recipes/viewer/TextWithMultipliedNumbers.tsx b/apps/gnocchi/web/src/components/recipes/viewer/TextWithMultipliedNumbers.tsx index 2e4ba3a0..eb5968be 100644 --- a/apps/gnocchi/web/src/components/recipes/viewer/TextWithMultipliedNumbers.tsx +++ b/apps/gnocchi/web/src/components/recipes/viewer/TextWithMultipliedNumbers.tsx @@ -3,8 +3,8 @@ import { useFeatureFlag } from '@biscuits/client'; import { fractionToText } from '@a-type/utils'; export interface TextWithMultipliedNumbersProps { - text: string | null; - multiplier: number; + text: string | null; + multiplier: number; } // This regex matches any number with a decimal point, or any number with a fraction. @@ -14,44 +14,44 @@ export interface TextWithMultipliedNumbersProps { const numberRegex = /(\d+\.\d+|\d+\/\d+|\d+)(?!\d*\s*[FCยฐ])/g; export function TextWithMultipliedNumbers({ - text, - multiplier, + text, + multiplier, }: TextWithMultipliedNumbersProps) { - const enabled = useFeatureFlag('multipliedIngredients'); - if (!enabled) return <>{text}; - if (multiplier === 1) return <>{text}; - if (!text) return <>{text}; + const enabled = useFeatureFlag('multipliedIngredients'); + if (!enabled) return <>{text}; + if (multiplier === 1) return <>{text}; + if (!text) return <>{text}; - const matches = text.match(numberRegex); - if (!matches) return <>{text}; - const fragments = text.trim().split(numberRegex); + const matches = text.match(numberRegex); + if (!matches) return <>{text}; + const fragments = text.trim().split(numberRegex); - return ( - <> - {fragments.map((fragment, index) => { - const isNumber = numberRegex.test(fragment); - return ( - - {!isNumber && fragment} - {isNumber && ( - - Multiplier {fractionToText(multiplier)}x applied. Original - value: {fragment} - - ) as any - } - > - - {fractionToText(parseFloat(fragment.trim()) * multiplier)} - - - )} - - ); - })} - - ); + return ( + <> + {fragments.map((fragment, index) => { + const isNumber = numberRegex.test(fragment); + return ( + + {!isNumber && fragment} + {isNumber && ( + + Multiplier {fractionToText(multiplier)}x applied. Original + value: {fragment} + + ) as any + } + > + + {fractionToText(parseFloat(fragment.trim()) * multiplier)} + + + )} + + ); + })} + + ); } diff --git a/apps/gnocchi/web/src/pages/SplashPage.tsx b/apps/gnocchi/web/src/pages/SplashPage.tsx index f740304e..5cdffad6 100644 --- a/apps/gnocchi/web/src/pages/SplashPage.tsx +++ b/apps/gnocchi/web/src/pages/SplashPage.tsx @@ -2,14 +2,14 @@ import { withClassName } from '@a-type/ui/hooks'; import { useLocalStorage } from '@/hooks/useLocalStorage.js'; import classNames from 'classnames'; import { - CSSProperties, - forwardRef, - lazy, - ReactNode, - Suspense, - useEffect, - useRef, - useState, + CSSProperties, + forwardRef, + lazy, + ReactNode, + Suspense, + useEffect, + useRef, + useState, } from 'react'; import { DemoFrame } from '@/components/promotional/DemoFrame.jsx'; import { APP_NAME } from '@/config.js'; @@ -25,221 +25,221 @@ import { AutoRestoreScroll } from '@/components/nav/AutoRestoreScroll.jsx'; const Scene = lazy(() => import('@/components/3d/Scene.jsx')); export function SplashPage() { - const [_, setHasSeen] = useLocalStorage('hasSeenWelcome', true); - useEffect(() => { - setHasSeen(true); - }, []); + const [_, setHasSeen] = useLocalStorage('hasSeenWelcome', true); + useEffect(() => { + setHasSeen(true); + }, []); - const upgradeSectionRef = useRef(null); - const [staticSectionAccent, setStaticSectionAccent] = useState(false); - useOnVisible(upgradeSectionRef, setStaticSectionAccent, { - threshold: 0.05, - }); + const upgradeSectionRef = useRef(null); + const [staticSectionAccent, setStaticSectionAccent] = useState(false); + useOnVisible(upgradeSectionRef, setStaticSectionAccent, { + threshold: 0.05, + }); - const [params] = useSearchParams(); - const jumpToUpgrade = params.get('upgrade') === 'true'; - useEffect(() => { - if (jumpToUpgrade && upgradeSectionRef.current) { - window.scrollTo(0, upgradeSectionRef.current.offsetTop); - } - }, [jumpToUpgrade]); + const [params] = useSearchParams(); + const jumpToUpgrade = params.get('upgrade') === 'true'; + useEffect(() => { + if (jumpToUpgrade && upgradeSectionRef.current) { + window.scrollTo(0, upgradeSectionRef.current.offsetTop); + } + }, [jumpToUpgrade]); - return ( -
- -
- - - -
- - - -

- {APP_NAME} -

- Your weekly cooking, in one place. -
- -
-

How it works

- - ๐Ÿงพ - - Copy, paste, or share items directly into the app. - - - - ๐Ÿท๏ธ - - Organize your run by aisle. {APP_NAME} will remember your - categorizations! - - - - ๐Ÿ›’ - - Get helpful suggestions based on your past purchases. - - -
- -
-

Collect recipes

-

- {APP_NAME} is a recipe app, too. You can save recipes from the - web, or add your own. -

- - ๐Ÿ“ - - Edit recipes to your liking. Add notes, change the serving size, - or even swap out your own ingredients. - - - - โž• - - Add recipe ingredients directly to your grocery list. - - -
- -
-

Less improv at the grocery store

-

- If you're like me, you usually leave the grocery store with some - foods you didn't plan on buying. But you also get home, start - loading the fridge, and realize you forgot something, too. -

-

- I built {APP_NAME} to plan grocery trips better, as a solo shopper - or a family. It may seem like any old list app, but under the - surface I've tried to design intentionally for the task at hand. - Give it a shot this week (no account needed!) and let me know what - you think. -

-

– Grant

-
-
-
- - - -

- Upgrade to the world's most collaborative cooking app -

-

- Sync your list and recipes to all your devices, share your list - with anyone you shop with, and coordinate with other chefs while - cooking. -

-
- -
-

Collaborative groceries

-

- Team up with your family, roommates, or friends to plan and shop -

- - โ˜๏ธ - - Sync your list and recipes to all your devices. - - - - ๐Ÿ‘ฏ - Share your list with anyone you shop with. - - - ๐Ÿ“Œ - - In-store collaboration, like claiming sections and planning a - place to meet up. - - -
- -
-

Sous chef mode

-

Stay on task when cooking together

- - ๐Ÿ–จ๏ธ - - Scan a recipe page directly to the app to add all the - ingredients to your list. - - - - ๐Ÿง‘๐Ÿปโ€๐Ÿณ - - Assign tasks to each chef, and see who's done what. - - -
-
- - Upgrade now - -
-
-
- -
- - Read the privacy policy - - - Terms and Conditions of usage - - - Gnocchi is open source - -
-
-
- {jumpToUpgrade && Join the club} - - {jumpToUpgrade ? 'Use for free' : 'Get Started'} - + return ( +
+ +
+ + + +
+ + + +

+ {APP_NAME} +

+ Your weekly cooking, in one place. +
+ +
+

How it works

+ + ๐Ÿงพ + + Copy, paste, or share items directly into the app. + + + + ๐Ÿท๏ธ + + Organize your run by aisle. {APP_NAME} will remember your + categorizations! + + + + ๐Ÿ›’ + + Get helpful suggestions based on your past purchases. + + +
+ +
+

Collect recipes

+

+ {APP_NAME} is a recipe app, too. You can save recipes from the + web, or add your own. +

+ + ๐Ÿ“ + + Edit recipes to your liking. Add notes, change the serving size, + or even swap out your own ingredients. + + + + โž• + + Add recipe ingredients directly to your grocery list. + + +
+ +
+

Less improv at the grocery store

+

+ If you're like me, you usually leave the grocery store with some + foods you didn't plan on buying. But you also get home, start + loading the fridge, and realize you forgot something, too. +

+

+ I built {APP_NAME} to plan grocery trips better, as a solo shopper + or a family. It may seem like any old list app, but under the + surface I've tried to design intentionally for the task at hand. + Give it a shot this week (no account needed!) and let me know what + you think. +

+

– Grant

+
+
+
+ + + +

+ Upgrade to the world's most collaborative cooking app +

+

+ Sync your list and recipes to all your devices, share your list + with anyone you shop with, and coordinate with other chefs while + cooking. +

+
+ +
+

Collaborative groceries

+

+ Team up with your family, roommates, or friends to plan and shop +

+ + โ˜๏ธ + + Sync your list and recipes to all your devices. + + + + ๐Ÿ‘ฏ + Share your list with anyone you shop with. + + + ๐Ÿ“Œ + + In-store collaboration, like claiming sections and planning a + place to meet up. + + +
+ +
+

Sous chef mode

+

Stay on task when cooking together

+ + ๐Ÿ–จ๏ธ + + Scan a recipe page directly to the app to add all the + ingredients to your list. + + + + ๐Ÿง‘๐Ÿปโ€๐Ÿณ + + Assign tasks to each chef, and see who's done what. + + +
+
+ + Upgrade now + +
+
+
+ +
+ + Read the privacy policy + + + Terms and Conditions of usage + + + Gnocchi is open source + +
+
+
+ {jumpToUpgrade && Join the club} + + {jumpToUpgrade ? 'Use for free' : 'Get Started'} + - - Free, no signup required. By continuing you agree to{' '} - - the terms and conditions of usage. - - -
-
- ); + + Free, no signup required. By continuing you agree to{' '} + + the terms and conditions of usage. + + +
+
+ ); } export default SplashPage; const DemoGrid = withClassName( - 'div', - 'grid grid-cols-[1fr] gap-5 items-start md:(grid-cols-[repeat(2,1fr)])', + 'div', + 'grid grid-cols-[1fr] gap-5 items-start md:(grid-cols-[repeat(2,1fr)])', ); const Demo = withClassName(DemoFrame, 'relative z-1 [grid-row-end:span_2]'); const TitleWrap = withClassName('div', 'md:[grid-column-end:span_2]'); @@ -248,52 +248,52 @@ const Emoji = withClassName('span', 'block'); const ItemText = withClassName('span', 'block relative'); const Section = forwardRef< - HTMLDivElement, - { - color?: 'white' | 'default'; - className?: string; - children: ReactNode; - style?: CSSProperties; - } + HTMLDivElement, + { + color?: 'white' | 'default'; + className?: string; + children: ReactNode; + style?: CSSProperties; + } >(function Section({ color = 'default', className, ...rest }, ref) { - return ( -
- ); + return ( +
+ ); }); function Title({ children }: { children: string }) { - return ( -

- {children} -

- ); + return ( +

+ {children} +

+ ); } const Content = forwardRef< - HTMLDivElement, - { - children: ReactNode; - className?: string; - style?: CSSProperties; - } + HTMLDivElement, + { + children: ReactNode; + className?: string; + style?: CSSProperties; + } >(function Content({ children, className, ...rest }, ref) { - return ( -
-
- {children} -
-
- ); + return ( +
+
+ {children} +
+
+ ); }); diff --git a/apps/palette/web/src/pages/ProjectPage.tsx b/apps/palette/web/src/pages/ProjectPage.tsx index a6ba7c87..83ed9ac9 100644 --- a/apps/palette/web/src/pages/ProjectPage.tsx +++ b/apps/palette/web/src/pages/ProjectPage.tsx @@ -35,7 +35,7 @@ export function ProjectPage({}: ProjectPageProps) { } return ( -
+
-
+
viewport.subscribe('zoomChanged', cb), - () => viewport.zoom, - ); + const viewport = useViewport(); + const zoom = useSyncExternalStore( + (cb) => viewport.subscribe('zoomChanged', cb), + () => viewport.zoom, + ); - return ( - - { - viewport.doZoom(v, { gestureComplete: true }); - }} - min={viewport.config.zoomLimits.min} - max={viewport.config.zoomLimits.max} - step={0.01} - color="default" - className="pointer-events-auto w-100px max-w-80vw cursor-pointer" - /> - - ); + return ( + + { + viewport.doZoom(v, { gestureComplete: true }); + }} + min={viewport.config.zoomLimits.min} + max={viewport.config.zoomLimits.max} + step={0.01} + color="default" + className="pointer-events-auto w-100px max-w-80dvw cursor-pointer" + /> + + ); } diff --git a/apps/star-chart/web/src/components/project/SelectionMenu.tsx b/apps/star-chart/web/src/components/project/SelectionMenu.tsx index 804ae87a..47523085 100644 --- a/apps/star-chart/web/src/components/project/SelectionMenu.tsx +++ b/apps/star-chart/web/src/components/project/SelectionMenu.tsx @@ -3,91 +3,95 @@ import { useSelectedObjectIds } from '../canvas/canvasHooks.js'; import { Button } from '@a-type/ui/components/button'; import { Icon } from '@a-type/ui/components/icon'; import { useCanvas } from '../canvas/CanvasProvider.jsx'; -import { disableDragProps } from '../canvas/CanvasObjectDragHandle.jsx'; import { useDeleteConnection, useDeleteTask } from './hooks.js'; import { useMemo } from 'react'; import { CanvasOverlayContent } from '../canvas/CanvasOverlay.jsx'; export interface SelectionMenuProps { - className?: string; + className?: string; } export function SelectionMenu({ className }: SelectionMenuProps) { - const selectedIds = useSelectedObjectIds(); - const hasSelection = selectedIds.length > 1; + const selectedIds = useSelectedObjectIds(); + const hasSelection = selectedIds.length > 1; - const canvas = useCanvas(); - const deleteTask = useDeleteTask(); - const deleteConnection = useDeleteConnection(); + const canvas = useCanvas(); + const deleteTask = useDeleteTask(); + const deleteConnection = useDeleteConnection(); - const [tasks, connections] = useMemo(() => { - const tasks = selectedIds.filter((id) => { - const metadata = canvas.objectMetadata.get(id); - return metadata?.type === 'task'; - }); - const connections = selectedIds.filter((id) => { - const metadata = canvas.objectMetadata.get(id); - return metadata?.type === 'connection'; - }); - return [tasks, connections]; - }, [selectedIds, canvas]); + const [tasks, connections] = useMemo(() => { + const tasks = selectedIds.filter((id) => { + const metadata = canvas.objectMetadata.get(id); + return metadata?.type === 'task'; + }); + const connections = selectedIds.filter((id) => { + const metadata = canvas.objectMetadata.get(id); + return metadata?.type === 'connection'; + }); + return [tasks, connections]; + }, [selectedIds, canvas]); - const deleteSelected = async (only?: 'task' | 'connection') => { - await Promise.all( - selectedIds.map((id) => { - const metadata = canvas.objectMetadata.get(id); - if (!metadata) return; + const deleteSelected = async (only?: 'task' | 'connection') => { + await Promise.all( + selectedIds.map((id) => { + const metadata = canvas.objectMetadata.get(id); + if (!metadata) return; - if (metadata.type === 'task' && only !== 'connection') { - return deleteTask(id).then((deletedIds) => { - canvas.selections.removeAll(deletedIds); - }); - } else if (metadata.type === 'connection' && only !== 'task') { - return deleteConnection(id).then(() => { - canvas.selections.remove(id); - }); - } - }), - ); - }; + if (metadata.type === 'task' && only !== 'connection') { + return deleteTask(id).then((deletedIds) => { + canvas.selections.removeAll(deletedIds); + }); + } else if (metadata.type === 'connection' && only !== 'task') { + return deleteConnection(id).then(() => { + canvas.selections.remove(id); + }); + } + }), + ); + }; - const mixed = tasks.length > 0 && connections.length > 0; + const mixed = tasks.length > 0 && connections.length > 0; - return ( - - ); + return ( + + ); } diff --git a/apps/trip-tick/web/src/components/nav/RouteDialog.tsx b/apps/trip-tick/web/src/components/nav/RouteDialog.tsx index 844f2bf2..8c8c3230 100644 --- a/apps/trip-tick/web/src/components/nav/RouteDialog.tsx +++ b/apps/trip-tick/web/src/components/nav/RouteDialog.tsx @@ -1,9 +1,8 @@ import { - Outlet, - RouteRenderer, - useMatch, - useMatchingRoute, - useNavigate, + Outlet, + RouteRenderer, + useMatchingRoute, + useNavigate, } from '@verdant-web/react-router'; import { Dialog, DialogContent } from '@a-type/ui/components/dialog'; import { Suspense } from 'react'; @@ -11,30 +10,32 @@ import { Suspense } from 'react'; export interface RouteDialogProps {} export function RouteDialog(props: RouteDialogProps) { - const navigate = useNavigate(); - const upper = useMatchingRoute(); - return ( - - {(match, params) => ( - { - if (!open && !!match) { - const oneLevelUp = window.location.pathname - .split('/') - .slice(0, -1) - .join('/'); - navigate(oneLevelUp || '/'); - } - }} - > - - - {match ? : null} - - - - )} - - ); + const navigate = useNavigate(); + const upper = useMatchingRoute(); + return ( + + {(match, params) => ( + { + if (!open && !!match) { + const oneLevelUp = window.location.pathname + .split('/') + .slice(0, -1) + .join('/'); + navigate(oneLevelUp || '/'); + } + }} + > + + + {match ? + + : null} + + + + )} + + ); } diff --git a/apps/wish-wash/web/src/components/items/ListItem.tsx b/apps/wish-wash/web/src/components/items/ListItem.tsx index 2141d870..a08362cf 100644 --- a/apps/wish-wash/web/src/components/items/ListItem.tsx +++ b/apps/wish-wash/web/src/components/items/ListItem.tsx @@ -57,7 +57,7 @@ export const ListItem = forwardRef( (null); + const [draggingId, setDraggingId] = useState(null); - const handleDragStart = useCallback(({ active }: DragStartEvent) => { - setDraggingId(active.id); - }, []); + const handleDragStart = useCallback(({ active }: DragStartEvent) => { + setDraggingId(active.id); + }, []); - const handleDragEnd = useCallback( - ({ active, over }: DragEndEvent) => { - setDraggingId(null); - if (!over) return; - if (active.id !== over.id) { - const fromIndex = active.data.current?.index; - const toIndex = over.data.current?.index; + const handleDragEnd = useCallback( + ({ active, over }: DragEndEvent) => { + setDraggingId(null); + if (!over) return; + if (active.id !== over.id) { + const fromIndex = active.data.current?.index; + const toIndex = over.data.current?.index; - items.move(fromIndex, toIndex); - } - }, - [items], - ); + items.move(fromIndex, toIndex); + } + }, + [items], + ); - const draggingItem = items.find((i) => i.get('id') === draggingId); + const draggingItem = items.find((i) => i.get('id') === draggingId); - const [reordering, setReordering] = useReordering(); + const [reordering, setReordering] = useReordering(); - return ( - - - Reorder items - - - - - i.get('id'))} - strategy={verticalListSortingStrategy} - > - - {items.map((item, i) => ( - - ))} - - - {draggingItem && } - - - - - - ); + return ( + + + Reorder items + + + + + i.get('id'))} + strategy={verticalListSortingStrategy} + > + + {items.map((item, i) => ( + + ))} + + + {draggingItem && } + + + + + + ); } const SorterItem = forwardRef< - HTMLDivElement, - { item: Item; style?: CSSProperties; handleProps?: any } + HTMLDivElement, + { item: Item; style?: CSSProperties; handleProps?: any } >(function SorterItem({ item, handleProps, ...rest }, ref) { - const { id, description } = hooks.useWatch(item); + const { id, description } = hooks.useWatch(item); - return ( -
- - -
{description}
-
- ); + return ( +
+ + +
{description}
+
+ ); }); const SortableItem = ({ index, item }: { item: Item; index: number }) => { - const { attributes, listeners, setNodeRef, transform, transition } = - useSortable({ id: item.get('id'), data: { index } }); + const { attributes, listeners, setNodeRef, transform, transition } = + useSortable({ id: item.get('id'), data: { index } }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - }; + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; - return ( - - ); + return ( + + ); }; diff --git a/packages/client/src/components/Explainer.tsx b/packages/client/src/components/Explainer.tsx index 140ce48d..491e4f31 100644 --- a/packages/client/src/components/Explainer.tsx +++ b/packages/client/src/components/Explainer.tsx @@ -1,56 +1,56 @@ import { Button } from '@a-type/ui/components/button'; import { - Dialog, - DialogContent, - DialogActions, - DialogClose, + Dialog, + DialogContent, + DialogActions, + DialogClose, } from '@a-type/ui/components/dialog'; import { useLocalStorage } from '../hooks/useStorage.js'; import { ReactNode, useState } from 'react'; export interface ExplainerProps { - stages: ReactNode[]; + stages: ReactNode[]; } export function Explainer({ stages }: ExplainerProps) { - const [explainerDismissed, setExplainerDismissed] = useLocalStorage( - 'explainerDismissed', - false, - ); - const [stage, setStage] = useState(0); - return ( - { - if (!open) { - setExplainerDismissed(true); - } - }} - > - -
{stages[stage]}
- - - - - - -
-
- ); + const [explainerDismissed, setExplainerDismissed] = useLocalStorage( + 'explainerDismissed', + false, + ); + const [stage, setStage] = useState(0); + return ( + { + if (!open) { + setExplainerDismissed(true); + } + }} + > + +
{stages[stage]}
+ + + + + + +
+
+ ); } diff --git a/packages/client/src/components/InstallHint.tsx b/packages/client/src/components/InstallHint.tsx index 75101dcf..345d1b96 100644 --- a/packages/client/src/components/InstallHint.tsx +++ b/packages/client/src/components/InstallHint.tsx @@ -2,21 +2,21 @@ import { useLocalStorage } from '../hooks/useStorage.js'; import { H2, P } from '@a-type/ui/components/typography'; import { Button } from '@a-type/ui/components/button'; import { - Dialog, - DialogActions, - DialogClose, - DialogContent, - DialogTrigger, + Dialog, + DialogActions, + DialogClose, + DialogContent, + DialogTrigger, } from '@a-type/ui/components/dialog'; import { withClassName } from '@a-type/ui/hooks'; import { useSnapshot } from 'valtio'; import { installState, triggerInstall } from '../install.js'; import { - getIsEdge, - getIsFirefox, - getIsPWAInstalled, - getIsSafari, - getOS, + getIsEdge, + getIsFirefox, + getIsPWAInstalled, + getIsSafari, + getOS, } from '../platform.js'; import { Icon } from '@a-type/ui/components/icon'; import { clsx } from '@a-type/ui'; @@ -24,164 +24,162 @@ import { useAppId } from './Context.js'; import { appsById } from '@biscuits/apps'; export interface InstallHintProps { - content: string; - className?: string; + content: string; + className?: string; } export function InstallHint({ - content: contentStr, - className, + content: contentStr, + className, }: InstallHintProps) { - const [isDismissed, setIsDismissed] = useLocalStorage( - 'pwa-install-hint-dismissed', - false, - ); - - const { installReady } = useSnapshot(installState); - - if (isDismissed || getIsPWAInstalled()) { - return null; - } - - const os = getOS(); - const isMobile = os === 'iOS' || os === 'Android'; - - if (!isMobile) { - return null; // TODO: desktop tutorial - } - - const Content = (isMobile && content[os]) || (() => null); - - return ( -
-

{contentStr}

-
- - {installReady ? ( - - ) : ( - - - - - - - - - - - - - - )} -
-
- ); + const [isDismissed, setIsDismissed] = useLocalStorage( + 'pwa-install-hint-dismissed', + false, + ); + + const { installReady } = useSnapshot(installState); + + if (isDismissed || getIsPWAInstalled()) { + return null; + } + + const os = getOS(); + const isMobile = os === 'iOS' || os === 'Android'; + + if (!isMobile) { + return null; // TODO: desktop tutorial + } + + const Content = (isMobile && content[os]) || (() => null); + + return ( +
+

{contentStr}

+
+ + {installReady ? + + : + + + + + + + + + + + + + } +
+
+ ); } const Keyword = withClassName('span', 'color-black bg-primaryWash'); -const Video = withClassName('video', 'max-h-60vh'); +const Video = withClassName('video', 'max-h-60dvh'); function IOSTutorial() { - const isSafari = getIsSafari(); - const appId = useAppId(); - const app = appsById[appId]; - - if (isSafari) { - return ( -
-

Adding {app.name} to your homescreen

-
    -
  1. - Open the - share menu at the bottom of the screen. -
  2. -
  3. - Tap "Add to Home Screen". -
  4. -
-

- After you've done this, you can open {app.name} just like any other - app. -

-

- Unfortunately, the way this works on iOS, your data will start from - scratch. If you have a subscription, you can sign back in to re-sync - your data. -

-
- ); - } - - return ( -
-

Hi, iOS user!

-

- {app.name} is a website that can act just like a native app, but Apple - makes it a little tricky to install. -

-

- First, you have to open this website in Safari. Once - you've done that, open Settings and click this button again to show next - steps. -

-
- ); + const isSafari = getIsSafari(); + const appId = useAppId(); + const app = appsById[appId]; + + if (isSafari) { + return ( +
+

Adding {app.name} to your homescreen

+
    +
  1. + Open the + share menu at the bottom of the screen. +
  2. +
  3. + Tap "Add to Home Screen". +
  4. +
+

+ After you've done this, you can open {app.name} just like any other + app. +

+

+ Unfortunately, the way this works on iOS, your data will start from + scratch. If you have a subscription, you can sign back in to re-sync + your data. +

+
+ ); + } + + return ( +
+

Hi, iOS user!

+

+ {app.name} is a website that can act just like a native app, but Apple + makes it a little tricky to install. +

+

+ First, you have to open this website in Safari. Once + you've done that, open Settings and click this button again to show next + steps. +

+
+ ); } function IOSShareIcon() { - return ( - - - - - - ); + return ( + + + + + + ); } function AndroidTutorial() { - const videoSrc = getIsFirefox() - ? `/videos/firefox-install.mp4` - : getIsEdge() - ? `/videos/edge-install.mp4` - : `/videos/android-install.mp4`; - - const appId = useAppId(); - const app = appsById[appId]; - - return ( -
-

Adding the {app.name} app to your phone

-

- Open the browser menu and look for "Install app" -

-

- After you've done this, you can open {app.name} just like any other app. - All your data will still be there! -

-
- ); + const videoSrc = + getIsFirefox() ? `/videos/firefox-install.mp4` + : getIsEdge() ? `/videos/edge-install.mp4` + : `/videos/android-install.mp4`; + + const appId = useAppId(); + const app = appsById[appId]; + + return ( +
+

Adding the {app.name} app to your phone

+

+ Open the browser menu and look for "Install app" +

+

+ After you've done this, you can open {app.name} just like any other app. + All your data will still be there! +

+
+ ); } const content = { - iOS: IOSTutorial, - Android: AndroidTutorial, + iOS: IOSTutorial, + Android: AndroidTutorial, }; diff --git a/web/src/components/promo/layout.tsx b/web/src/components/promo/layout.tsx index e1f25130..518ca892 100644 --- a/web/src/components/promo/layout.tsx +++ b/web/src/components/promo/layout.tsx @@ -9,186 +9,186 @@ import { AppId, appsById } from '@biscuits/apps'; import { Icon } from '@a-type/ui/components/icon'; export const DemoGrid = withClassName( - 'div', - 'grid grid-cols-[1fr] gap-5 items-start md:(grid-cols-[repeat(2,1fr)])', + 'div', + 'grid grid-cols-[1fr] gap-5 items-start md:(grid-cols-[repeat(2,1fr)])', ); export const Demo = withClassName( - PhoneDemo, - 'relative z-1 [grid-row-end:span_2]', + PhoneDemo, + 'relative z-1 [grid-row-end:span_2]', ); export const Highlight = withClassName( - 'img', - 'flex-1 [grid-row-end:span_2] min-w-0 w-full object-contain rounded-lg border-default', + 'img', + 'flex-1 [grid-row-end:span_2] min-w-0 w-full object-contain rounded-lg border-default', ); export const TitleWrap = withClassName('div', 'md:[grid-column-end:span_2]'); const Emoji = withClassName('span', 'block'); const ItemText = withClassName('span', 'block relative'); export const Description = withClassName( - 'p', - 'font-light text-xl my-6 text-white', + 'p', + 'font-light text-xl my-6 text-white', ); export const Section = forwardRef< - HTMLDivElement, - { - color?: 'white' | 'default'; - className?: string; - children: ReactNode; - style?: CSSProperties; - } + HTMLDivElement, + { + color?: 'white' | 'default'; + className?: string; + children: ReactNode; + style?: CSSProperties; + } >(function Section({ color = 'default', className, ...rest }, ref) { - return ( -
- ); + return ( +
+ ); }); export function HeroTitle({ children }: { children: string }) { - return ( -

- {children} -

- ); + return ( +

+ {children} +

+ ); } export const Content = forwardRef< - HTMLDivElement, - { - children: ReactNode; - className?: string; - style?: CSSProperties; - } + HTMLDivElement, + { + children: ReactNode; + className?: string; + style?: CSSProperties; + } >(function Content({ children, className, ...rest }, ref) { - return ( -
- - {children} - -
- ); + return ( +
+ + {children} + +
+ ); }); export const FeatureSection = ({ - title, - items, + title, + items, }: { - title: string; - items: { emoji: string; text: string; premium?: boolean }[]; + title: string; + items: { emoji: string; text: string; premium?: boolean }[]; }) => { - return ( -
-

{title}

- {items.map((item, index) => ( -
-
- {item.emoji} - {item.text} -
- {item.premium && ( - - Premium feature - - )} -
- ))} -
- ); + return ( +
+

{title}

+ {items.map((item, index) => ( +
+
+ {item.emoji} + {item.text} +
+ {item.premium && ( + + Premium feature + + )} +
+ ))} +
+ ); }; export const Footer = ({ className }: { className?: string }) => ( - -
- - Read the privacy policy - - - Terms and Conditions of usage - - - Biscuits is open source - -
-
+ +
+ + Read the privacy policy + + + Terms and Conditions of usage + + + Biscuits is open source + +
+
); export const CallToAction = ({ - className, - appId, + className, + appId, }: { - className?: string; - appId: AppId; + className?: string; + appId: AppId; }) => ( -
-
- - -
+
+
+ + +
- - Free, no signup required. By continuing you agree to{' '} - - the terms and conditions of usage. - - -
+ + Free, no signup required. By continuing you agree to{' '} + + the terms and conditions of usage. + + +
); export const Root = withClassName( - 'div', - 'bg-white color-black flex flex-col items-stretch', + 'div', + 'bg-white color-black flex flex-col items-stretch', ); export const Background = withClassName( - 'div', - 'layer-components:(fixed top-0 left-0 w-full h-80% pointer-events-none)', + 'div', + 'layer-components:(fixed top-0 left-0 w-full h-80% pointer-events-none)', ); const AppNameText = withClassName( - 'h2', - 'font-fancy font-bold text-[4vmax] color-primary-dark', + 'h2', + 'font-fancy font-bold text-[4vmax] color-primary-dark', ); export const AppName = ({ appId }: { appId: AppId }) => { - const app = appsById[appId]; + const app = appsById[appId]; - return ( -
- {app.name} - {app.name} -
- ); + return ( +
+ {app.name} + {app.name} +
+ ); }; diff --git a/web/src/pages/HomePage.tsx b/web/src/pages/HomePage.tsx index 1a29bcb9..3083872e 100644 --- a/web/src/pages/HomePage.tsx +++ b/web/src/pages/HomePage.tsx @@ -2,9 +2,9 @@ import { UserMenu } from '@/components/auth/UserMenu.js'; import { Footer } from '@/components/help/Footer.jsx'; import { AppDemo } from '@/components/promo/AppDemo.jsx'; import { - PageContent, - PageFixedArea, - PageRoot, + PageContent, + PageFixedArea, + PageRoot, } from '@a-type/ui/components/layouts'; import { P } from '@a-type/ui/components/typography'; import { apps } from '@biscuits/apps'; @@ -15,75 +15,75 @@ import { Suspense, lazy } from 'react'; const Paws = lazy(() => import('@/components/paws/Paws.jsx')); const innerProps = { - className: 'flex flex-col gap-6 relative px-8', + className: 'flex flex-col gap-6 relative px-8', }; export default function HomePage() { - return ( - - - - - - - -

- Biscuits -

- - - -
-
-

- Scratch-made apps -

-

- Biscuits apps are designed to make your life easier. Free to use - forever, no ads, no tracking. Learn how. -

-

- No signup needed. No need to open the App Store. These are instant - web apps, no installation required. Just click "Open app" - to get started. -

-
- {apps.map((app, index) => - !app.prerelease || import.meta.env.DEV ? ( - - ) : null, - )} -