diff --git a/apps/palette/web/src/components/actions/DeleteColorAction.tsx b/apps/palette/web/src/components/actions/DeleteColorAction.tsx new file mode 100644 index 00000000..c473e144 --- /dev/null +++ b/apps/palette/web/src/components/actions/DeleteColorAction.tsx @@ -0,0 +1,32 @@ +import { ActionButton } from '@a-type/ui/components/actions'; +import { Icon } from '@a-type/ui/components/icon'; +import { useColorSelection } from '../projects/hooks.js'; +import { Project } from '@palette.biscuits/verdant'; +import { hooks } from '@/hooks.js'; + +export function DeleteColorAction({ project }: { project: Project }) { + const { colors } = hooks.useWatch(project); + + const [selectedId, selectId] = useColorSelection(); + + const deleteSelectedColor = () => { + if (selectedId) { + const val = colors.find((c) => c.get('id') === selectedId); + if (val) { + colors.removeAll(val); + } + selectId(null); + } + }; + + return ( + } + visible={!!selectedId} + > + Delete color + + ); +} diff --git a/apps/palette/web/src/components/actions/SortAction.tsx b/apps/palette/web/src/components/actions/SortAction.tsx new file mode 100644 index 00000000..b07b5cad --- /dev/null +++ b/apps/palette/web/src/components/actions/SortAction.tsx @@ -0,0 +1,25 @@ +import { ActionButton } from '@a-type/ui/components/actions'; +import { ColorSort, useSort } from '../projects/hooks.js'; +import { Icon } from '@a-type/ui/components/icon'; + +export function SortAction() { + const [sort, setSort] = useSort(); + + const goNext = () => { + const currentIndex = orderedOptions.indexOf(sort); + const nextIndex = (currentIndex + 1) % orderedOptions.length; + setSort(orderedOptions[nextIndex]); + }; + + return ( + } + className="capitalize" + > + {sort} + + ); +} + +const orderedOptions: ColorSort[] = ['hue', 'saturation', 'lightness']; diff --git a/apps/palette/web/src/components/actions/ToggleBubblesAction.tsx b/apps/palette/web/src/components/actions/ToggleBubblesAction.tsx new file mode 100644 index 00000000..9ac5e520 --- /dev/null +++ b/apps/palette/web/src/components/actions/ToggleBubblesAction.tsx @@ -0,0 +1,21 @@ +import { useSnapshot } from 'valtio'; +import { toolState } from '../projects/state.js'; +import { ActionButton } from '@a-type/ui/components/actions'; +import { Icon } from '@a-type/ui/components/icon'; + +export interface ToggleBubblesActionProps {} + +export function ToggleBubblesAction({}: ToggleBubblesActionProps) { + const showBubbles = useSnapshot(toolState).showBubbles; + + return ( + } + toggled={showBubbles} + toggleMode="state-only" + onClick={() => (toolState.showBubbles = !showBubbles)} + > + Dots + + ); +} diff --git a/apps/palette/web/src/components/projects/ColorBreakdown.tsx b/apps/palette/web/src/components/projects/ColorBreakdown.tsx index 7bece528..e4cb1d83 100644 --- a/apps/palette/web/src/components/projects/ColorBreakdown.tsx +++ b/apps/palette/web/src/components/projects/ColorBreakdown.tsx @@ -1,43 +1,20 @@ -import { hooks } from '@/hooks.js'; -import { Project, ProjectColorsItem } from '@palette.biscuits/verdant'; // @ts-ignore import { Color } from '@dynamize/color-utilities'; -import { useColorSelection } from './hooks.js'; import { clsx } from '@a-type/ui'; import { useBoundsCssVars } from '@a-type/ui/hooks'; export interface ColorBreakdownProps { - project: Project; + color: { r: number; g: number; b: number }; className?: string; } -// shows r/y/b distribution of a color -export function ColorBreakdown({ project, className }: ColorBreakdownProps) { - const [selectedId] = useColorSelection(); - const { colors } = hooks.useWatch(project); - hooks.useWatch(colors); - const matchingColor = colors.find((c) => c.get('id') === selectedId); - - if (!matchingColor) { - return null; - } - - return ; -} - const yellow = 'rgb(255,255,0)'; const red = 'rgb(255,0,0)'; const blue = 'rgb(0,0,255)'; -function ColorBreakdownVisuals({ - color, - className, -}: { - color: ProjectColorsItem; - className?: string; -}) { - const { value } = hooks.useWatch(color); - const { r, g, b } = value.getSnapshot(); +// shows r/y/b distribution of a color +export function ColorBreakdown({ color, className }: ColorBreakdownProps) { + const { r, g, b } = color; const convertible = new Color('rgb', { red: r, green: g, blue: b }, [ 'ryb', 'cmyk', @@ -48,47 +25,34 @@ function ColorBreakdownVisuals({ const hsl = convertible.hsl; return ( -
-
+ -
- +
50 ? 'black' : 'white', + }} /> -
-
50 ? 'black' : 'white', - }} - /> -
); @@ -131,7 +95,10 @@ function PieChart({ style={{ backgroundImage: gradient, }} - className={clsx('rounded-full aspect-1 w-full relative', className)} + className={clsx( + 'rounded-full aspect-1 relative transition-all', + className, + )} >
{/* Render percentage values in the correct positions */} diff --git a/apps/palette/web/src/components/projects/CreateProject.tsx b/apps/palette/web/src/components/projects/CreateProject.tsx index 7464d3a4..48f6aa47 100644 --- a/apps/palette/web/src/components/projects/CreateProject.tsx +++ b/apps/palette/web/src/components/projects/CreateProject.tsx @@ -5,26 +5,26 @@ import { H2 } from '@a-type/ui/components/typography'; export interface CreateProjectProps {} export function CreateProject(props: CreateProjectProps) { - const client = hooks.useClient(); - const navigate = useNavigate(); + const client = hooks.useClient(); + const navigate = useNavigate(); - return ( -
-

Start a new project

- { - if (value) { - const project = await client.projects.put({ - image: value, - }); - navigate(`/projects/${project.get('id')}`, { - skipTransition: true, - }); - } - }} - value={null} - /> -
- ); + return ( +
+

Start a new project

+ { + if (value) { + const project = await client.projects.put({ + image: value, + }); + navigate(`/projects/${project.get('id')}`, { + skipTransition: true, + }); + } + }} + value={null} + /> +
+ ); } diff --git a/apps/palette/web/src/components/projects/ProjectCanvas.tsx b/apps/palette/web/src/components/projects/ProjectCanvas.tsx index 8f401587..e63306bf 100644 --- a/apps/palette/web/src/components/projects/ProjectCanvas.tsx +++ b/apps/palette/web/src/components/projects/ProjectCanvas.tsx @@ -5,7 +5,6 @@ import { Project, ProjectColors, ProjectColorsItem, - ProjectColorsItemInit, } from '@palette.biscuits/verdant'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useColorSelection } from './hooks.js'; @@ -20,9 +19,7 @@ import { useCanvas, Vector2, } from '@a-type/react-space'; -import { useSnapshot } from 'valtio'; -import { Button } from '@a-type/ui/components/button'; -import { Icon } from '@a-type/ui/components/icon'; +import { ref, useSnapshot } from 'valtio'; import { toolState } from './state.js'; import { preventDefault } from '@a-type/utils'; @@ -32,13 +29,7 @@ export interface ProjectCanvasProps { } export function ProjectCanvas({ project, className }: ProjectCanvasProps) { const { image, colors } = hooks.useWatch(project); - const [_, setSelected] = useColorSelection(); - const addColor = (init: ProjectColorsItemInit) => { - colors.push(init); - const newColor = colors.get(colors.length - 1); - setSelected(newColor.get('id')); - }; - const { showBubbles, pickingColor } = useSnapshot(toolState); + const { showBubbles, activelyPicking } = useSnapshot(toolState); const viewport = useCreateViewport({ panLimitMode: 'viewport', @@ -49,8 +40,6 @@ export function ProjectCanvas({ project, className }: ProjectCanvasProps) { }, }); - const picking = !!pickingColor; - return (
- - {showBubbles && !picking && } + + {showBubbles && !activelyPicking && } + -
); } function ColorPickerCanvas({ image: imageModel, - onColor, className, }: { image: EntityFile; - onColor: (init: ProjectColorsItemInit) => void; className?: string; }) { hooks.useWatch(imageModel); @@ -136,35 +123,19 @@ function ColorPickerCanvas({ [canvasRef], ); - const getCanvasSize = useCallback(() => { - const canvas = canvasRef.current; - if (!canvas) return null; - return { - width: canvas.width, - height: canvas.height, - }; - }, [canvasRef]); - return ( {/* */} {/* */} {/* bubble preview */} - + ); } function BubblePicker({ getCanvasColors, - onPickingChange, - getCanvasSize, - onColor, }: { getCanvasColors: ( x: number, @@ -172,17 +143,12 @@ function BubblePicker({ xRange?: number, yRange?: number, ) => Uint8ClampedArray | null; - getCanvasSize: () => { width: number; height: number } | null; - onPickingChange?: (picking: boolean) => void; - onColor: (color: { - value: { r: number; g: number; b: number }; - percentage: { x: number; y: number }; - pixel: { x: number; y: number }; - }) => void; }) { const previewRef = useRef(null); const previewCanvasRef = useRef(null); const dotRef = useRef(null); + const show = useSnapshot(toolState).activelyPicking; + const [_, setSelected] = useColorSelection(); useClaimGesture( 'tool', @@ -229,10 +195,19 @@ function BubblePicker({ g: colors[577], b: colors[578], }; - toolState.pickingColor = `rgb(${centerColor.r}, ${centerColor.g}, ${centerColor.b})`; + const canvasRect = canvas.limits.value; + const canvasWidth = canvasRect.max.x - canvasRect.min.x; + const canvasHeight = canvasRect.max.y - canvasRect.min.y; + const canvasX = x / canvasWidth; + const canvasY = y / canvasHeight; + toolState.pickedColor = ref({ + value: centerColor, + percentage: { x: canvasX, y: canvasY }, + pixel: { x, y }, + }); dotRef.current?.style.setProperty( 'background-color', - toolState.pickingColor, + `rgb(${centerColor.r}, ${centerColor.g}, ${centerColor.b})`, ); } }, @@ -245,14 +220,13 @@ function BubblePicker({ if (!preview) return; preview.style.setProperty('--x', `${worldPosition.x}px`); preview.style.setProperty('--y', `${worldPosition.y}px`); - preview.style.setProperty('display', 'block'); const size = preview.computedStyleMap().get('--size'); const sizeValue = size ? parseInt(size.toString()) : 0; - const flipX = screenPosition.x - sizeValue * 2 <= 0; + const flipX = screenPosition.x - sizeValue * 1.5 <= 0; preview.style.setProperty('--x-mult', flipX ? `1` : `-1`); - const flipY = screenPosition.y - sizeValue * 2 <= 0; + const flipY = screenPosition.y - sizeValue * 1.5 <= 0; preview.style.setProperty('--y-mult', flipY ? `1` : `-1`); preview.style.setProperty('border-radius', '9999px'); @@ -277,7 +251,8 @@ function BubblePicker({ if (!preview) return; updatePosition(pointerWorldPosition, screenPosition); updatePreview(x, y); - onPickingChange?.(true); + toolState.activelyPicking = true; + setSelected(null); }, onDrag: ( { pointerWorldPosition, screenPosition, touchesCount }, @@ -293,31 +268,15 @@ function BubblePicker({ if (!preview) return; updatePosition(pointerWorldPosition, screenPosition); updatePreview(x, y); + toolState.activelyPicking = true; }, - onDragEnd: ({ pointerWorldPosition }) => { - const { x, y } = pointerWorldPosition; - - previewRef.current?.style.setProperty('display', 'none'); - - const canvasRect = canvas.limits.value; - const canvasWidth = canvasRect.max.x - canvasRect.min.x; - const canvasHeight = canvasRect.max.y - canvasRect.min.y; - const canvasX = x / canvasWidth; - const canvasY = y / canvasHeight; - - const color = getCanvasColors(x, y); - toolState.pickingColor = null; - if (!color) return; - - onColor({ - value: { r: color[0], g: color[1], b: color[2] }, - percentage: { x: canvasX, y: canvasY }, - pixel: { x, y }, - }); + onDragEnd: () => { + toolState.activelyPicking = false; }, onAbandon: () => { previewRef.current?.style.setProperty('display', 'none'); - toolState.pickingColor = null; + toolState.pickedColor = null; + toolState.activelyPicking = false; }, }, 'bubble', @@ -332,7 +291,8 @@ function BubblePicker({ '[transform:translate(-50%,-50%)_translate(calc(var(--x-mult,1)*(var(--size)+var(--pointer-size))/2/var(--zoom,1)),calc(var(--y-mult,1)*(var(--size)+var(--pointer-size))/2/var(--zoom,1)))]', 'left-[var(--x)] top-[var(--y)]', 'w-[calc(var(--size)/var(--zoom,1))] h-[calc(var(--size)/var(--zoom,1))]', - 'fixed pointer-events-none border-1 border-solid border-gray-4 overflow-hidden hidden', + 'fixed pointer-events-none border-1 border-solid border-gray-4 overflow-hidden', + show ? 'block' : 'hidden', )} > @@ -356,6 +316,9 @@ function Bubbles({ colors }: { colors: ProjectColors }) { ); } +const BUBBLE_SIZE_LG = 64; +const BUBBLE_SIZE_SM = 32; + function Bubble({ color: colorVal }: { color: ProjectColorsItem }) { const { percentage, value, id } = hooks.useWatch(colorVal); const { x, y } = hooks.useWatch(percentage); @@ -365,34 +328,51 @@ function Bubble({ color: colorVal }: { color: ProjectColorsItem }) { return ( +
+
); } diff --git a/apps/palette/web/src/components/projects/ProjectColorSpotlight.tsx b/apps/palette/web/src/components/projects/ProjectColorSpotlight.tsx index 86a88419..dc0a9c64 100644 --- a/apps/palette/web/src/components/projects/ProjectColorSpotlight.tsx +++ b/apps/palette/web/src/components/projects/ProjectColorSpotlight.tsx @@ -4,9 +4,6 @@ import { hooks } from '@/hooks.js'; import { clsx } from '@a-type/ui'; import { useSnapshot } from 'valtio'; import { toolState } from './state.js'; -import { assert } from '@a-type/utils'; -import { Dialog } from '@a-type/ui/components/dialog'; -import { useState } from 'react'; import { ColorBreakdown } from './ColorBreakdown.jsx'; import { Button } from '@a-type/ui/components/button'; import { Icon } from '@a-type/ui/components/icon'; @@ -20,73 +17,62 @@ export function ProjectColorSpotlight({ project, className, }: ProjectColorSpotlightProps) { - const [selectedId] = useColorSelection(); + const [selectedId, setSelectedId] = useColorSelection(); const { colors } = hooks.useWatch(project); const matchingColor = colors.find((c) => c.get('id') === selectedId); hooks.useWatch(matchingColor || null); const matchingColorValues = matchingColor?.get('value')?.getSnapshot(); - const { pickingColor } = useSnapshot(toolState); + const { pickedColor: pickingColor } = useSnapshot(toolState); - const showColor = !!pickingColor || !!matchingColorValues; + const color = pickingColor?.value || matchingColorValues; - const [showMixing, setShowMixing] = useState(false); - - if (!showColor) { + if (color) { return (
- - Select a color to see it here - +
+
+ {!!pickingColor && ( + + )} +
+
+
); } - if (pickingColor) { - return ( -
- ); - } - - assert(matchingColorValues); - return (
- - - - - - Color Mixing - - - Close - - - + + Select a color to see it here +
); } diff --git a/apps/palette/web/src/components/projects/ProjectPalette.tsx b/apps/palette/web/src/components/projects/ProjectPalette.tsx index 16100688..c62320cc 100644 --- a/apps/palette/web/src/components/projects/ProjectPalette.tsx +++ b/apps/palette/web/src/components/projects/ProjectPalette.tsx @@ -1,10 +1,10 @@ import { hooks } from '@/hooks.js'; import { clsx } from '@a-type/ui'; import { Project } from '@palette.biscuits/verdant'; -import { useColorSelection } from './hooks.js'; -import { ScrollArea } from '@a-type/ui/components/scrollArea'; -import { Button } from '@a-type/ui/components/button'; -import { Icon } from '@a-type/ui/components/icon'; +import { useColorSelection, useSort } from './hooks.js'; +import { H3 } from '@a-type/ui/components/typography'; +// @ts-ignore +import { Color } from '@dynamize/color-utilities'; export interface ProjectPaletteProps { project: Project; @@ -15,73 +15,59 @@ export function ProjectPalette({ project, className }: ProjectPaletteProps) { const { colors } = hooks.useWatch(project); hooks.useWatch(colors, { deep: true }); + const [sort] = useSort(); + // sort by hue const sorted = colors.getSnapshot().sort((a, b) => { - return ( - rgbToHue(a.value.r, a.value.g, a.value.b) - - rgbToHue(b.value.r, b.value.g, b.value.b) + const aColor = new Color( + 'rgb', + { red: a.value.r, green: a.value.g, blue: a.value.b }, + ['hsl'], + ); + const bColor = new Color( + 'rgb', + { red: b.value.r, green: b.value.g, blue: b.value.b }, + ['hsl'], ); + return aColor.hsl[sort] - bColor.hsl[sort]; }); const [selectedId, selectId] = useColorSelection(); - const deleteSelectedColor = () => { - if (selectedId) { - const val = colors.find((c) => c.get('id') === selectedId); - if (val) { - colors.removeAll(val); - selectId(null); - } - } - }; - return ( -
-
- -
+
+

Saved Colors

{!sorted.length && ( Click the image to select colors )} - -
{ - selectId(null); - }} - > - {sorted.map((color, i) => ( -
-
+
{ + selectId(null); + }} + > + {sorted.map((color, i) => ( +
); } diff --git a/apps/palette/web/src/components/projects/hooks.ts b/apps/palette/web/src/components/projects/hooks.ts index a08430e9..c9e86a68 100644 --- a/apps/palette/web/src/components/projects/hooks.ts +++ b/apps/palette/web/src/components/projects/hooks.ts @@ -1,18 +1,24 @@ +import { useLocalStorage } from '@biscuits/client'; import { useSearchParams } from '@verdant-web/react-router'; import { useCallback } from 'react'; export function useColorSelection() { - const [params, setParams] = useSearchParams(); - const setColor = useCallback( - (colorId: string | null) => { - setParams((p) => { - if (!colorId) p.delete('color'); - else p.set('color', colorId); - return p; - }); - }, - [setParams], - ); + const [params, setParams] = useSearchParams(); + const setColor = useCallback( + (colorId: string | null) => { + setParams((p) => { + if (!colorId) p.delete('color'); + else p.set('color', colorId); + return p; + }); + }, + [setParams], + ); - return [params.get('color'), setColor] as const; + return [params.get('color'), setColor] as const; +} + +export type ColorSort = 'hue' | 'saturation' | 'lightness'; +export function useSort() { + return useLocalStorage('sort-palette', 'hue', true); } diff --git a/apps/palette/web/src/components/projects/state.ts b/apps/palette/web/src/components/projects/state.ts index 6fdb1e79..8cb98576 100644 --- a/apps/palette/web/src/components/projects/state.ts +++ b/apps/palette/web/src/components/projects/state.ts @@ -1,6 +1,8 @@ +import { ProjectColorsItemInit } from '@palette.biscuits/verdant'; import { proxy } from 'valtio'; export const toolState = proxy({ showBubbles: true, - pickingColor: null as string | null, + pickedColor: null as ProjectColorsItemInit | null, + activelyPicking: false, }); diff --git a/apps/palette/web/src/pages/HomePage.tsx b/apps/palette/web/src/pages/HomePage.tsx index 283f16eb..bddbe283 100644 --- a/apps/palette/web/src/pages/HomePage.tsx +++ b/apps/palette/web/src/pages/HomePage.tsx @@ -1,23 +1,24 @@ import { CreateProject } from '@/components/projects/CreateProject.jsx'; import { ProjectsList } from '@/components/projects/ProjectsList.jsx'; import { PageContent, PageFixedArea } from '@a-type/ui/components/layouts'; -import { UserMenu } from '@biscuits/client'; +import { usePageTitle, UserMenu } from '@biscuits/client'; export interface HomePageProps {} export function HomePage({}: HomePageProps) { - return ( - - -
-

Palette

- -
-
- - -
- ); + usePageTitle('Palette'); + return ( + + +
+

Palette

+ +
+
+ + +
+ ); } export default HomePage; diff --git a/apps/palette/web/src/pages/ProjectPage.tsx b/apps/palette/web/src/pages/ProjectPage.tsx index 62a255bf..a6ba7c87 100644 --- a/apps/palette/web/src/pages/ProjectPage.tsx +++ b/apps/palette/web/src/pages/ProjectPage.tsx @@ -1,11 +1,18 @@ +import { DeleteColorAction } from '@/components/actions/DeleteColorAction.jsx'; +import { RedoAction } from '@/components/actions/RedoAction.jsx'; +import { SortAction } from '@/components/actions/SortAction.jsx'; +import { ToggleBubblesAction } from '@/components/actions/ToggleBubblesAction.jsx'; +import { UndoAction } from '@/components/actions/UndoAction.jsx'; import { ProjectCanvas } from '@/components/projects/ProjectCanvas.jsx'; import { ProjectColorSpotlight } from '@/components/projects/ProjectColorSpotlight.jsx'; import { ProjectPalette } from '@/components/projects/ProjectPalette.jsx'; import { hooks } from '@/hooks.js'; +import { ActionBar } from '@a-type/ui/components/actions'; import { Button } from '@a-type/ui/components/button'; import { Icon } from '@a-type/ui/components/icon'; import { PageContent } from '@a-type/ui/components/layouts'; import { H1 } from '@a-type/ui/components/typography'; +import { usePageTitle } from '@biscuits/client'; import { Link, useParams } from '@verdant-web/react-router'; export interface ProjectPageProps {} @@ -13,6 +20,7 @@ export interface ProjectPageProps {} export function ProjectPage({}: ProjectPageProps) { const id = useParams().id; const project = hooks.useProject(id); + usePageTitle('Palette'); if (!project) { return ( @@ -27,21 +35,34 @@ export function ProjectPage({}: ProjectPageProps) { } return ( -
-
-
-
- -
- +
+
+ + + + + + + + +
+
+
+ +
- - +
); diff --git a/apps/palette/web/src/pages/SettingsPage.tsx b/apps/palette/web/src/pages/SettingsPage.tsx index f612517f..f6032635 100644 --- a/apps/palette/web/src/pages/SettingsPage.tsx +++ b/apps/palette/web/src/pages/SettingsPage.tsx @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { checkForUpdate } from '@/updateState.js'; import { H1 } from '@a-type/ui/components/typography'; -import { DarkModeToggle } from '@biscuits/client'; +import { DarkModeToggle, usePageTitle } from '@biscuits/client'; import { PageContent, PageFixedArea } from '@a-type/ui/components/layouts'; import { ManageStorage } from '@biscuits/client/storage'; import { Button } from '@a-type/ui/components/button'; @@ -11,27 +11,29 @@ import { AutoRestoreScroll, Link } from '@verdant-web/react-router'; export interface SettingsPageProps {} export function SettingsPage({}: SettingsPageProps) { - useEffect(() => { - checkForUpdate(); - }, []); + useEffect(() => { + checkForUpdate(); + }, []); - return ( - - - - -

Settings

-
- - -
- -
- ); + usePageTitle('Palette | Settings'); + + return ( + + + + +

Settings

+
+ + +
+ +
+ ); } export default SettingsPage; diff --git a/packages/apps/src/index.ts b/packages/apps/src/index.ts index 609fa457..5d72696d 100644 --- a/packages/apps/src/index.ts +++ b/packages/apps/src/index.ts @@ -1,173 +1,173 @@ export type AppManifest = { - id: Id; - url: string; - name: string; - description: string; - mainImageUrl?: string; - iconPath: string; - size?: number; - devOriginOverride: string; - demoVideoSrc: string; - paidDescription: string; - paidFeatures: PaidFeature[] | readonly PaidFeature[]; - prerelease?: boolean; + id: Id; + url: string; + name: string; + description: string; + mainImageUrl?: string; + iconPath: string; + size?: number; + devOriginOverride: string; + demoVideoSrc: string; + paidDescription: string; + paidFeatures: PaidFeature[] | readonly PaidFeature[]; + prerelease?: boolean; }; export type PaidFeature = { - imageUrl: string; - description: string; - title: string; - family?: boolean; + imageUrl: string; + description: string; + title: string; + family?: boolean; }; export const apps = [ - { - id: 'gnocchi', - url: 'https://gnocchi.biscuits.club', - name: 'Gnocchi', - description: 'Organize your weekly cooking and groceries', - mainImageUrl: 'https://gnocchi.biscuits.club/og-image.png', - iconPath: 'android-chrome-512x512.png', - size: 4, - devOriginOverride: 'http://localhost:6220', - demoVideoSrc: '/videos/gnocchi-compressed.mp4', - paidFeatures: [ - { - imageUrl: '/images/gnocchi/scanner.png', - title: 'Recipe Scanner', - description: - 'Scan web recipes to create a personal copy. Add notes, make changes, and collaborate on cooking with other plan members.', - }, - { - imageUrl: '/images/gnocchi/groceries-collaboration.png', - title: 'Collaborative Groceries', - description: - 'Share your groceries list with other plan members and stay on the same page. Shopping gets more efficient with realtime presence and progress updates.', - family: true, - }, - { - imageUrl: '/images/gnocchi/recipe-collaboration.png', - title: 'Sous Chef Mode', - description: - 'Team up with other plan members to cook a recipe together. Assign steps and track progress with instant updates.', - family: true, - }, - ], - paidDescription: - 'Your personal cooking app becomes a family groceries list and recipe box.', - } as AppManifest<'gnocchi'>, - { - id: 'trip-tick', - url: 'https://trip-tick.biscuits.club', - name: 'Trip Tick', - description: 'The smartest packing list for your next trip', - iconPath: 'icon.png', - size: 2, - devOriginOverride: 'http://localhost:6221', - demoVideoSrc: '/videos/trip-tick-compressed.mp4', - paidFeatures: [ - { - imageUrl: '/images/trip-tick/weather.png', - title: 'Weather Forecast', - description: - 'Get weather forecasts for your destination over the duration of your trip.', - }, - { - imageUrl: '/images/trip-tick/conditions.png', - title: 'Powerful Conditions', - description: - 'Define conditions for packing list items based on predicted weather to make sure you have everything you need.', - }, - ], - paidDescription: - '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: 'wish-wash', - demoVideoSrc: '', - description: 'TODO', - devOriginOverride: 'http://localhost:6222', - iconPath: 'icon.png', - name: 'Wish Wash', - paidDescription: 'TODO', - paidFeatures: [], - url: 'https://wish-wash.biscuits.club', - prerelease: true, - } as AppManifest<'wish-wash'>, - { - id: 'marginalia', - demoVideoSrc: '', - description: 'TODO', - devOriginOverride: 'http://localhost:6223', - iconPath: 'icon.png', - name: 'Marginalia', - paidDescription: 'TODO', - paidFeatures: [], - url: 'https://marginalia.biscuits.club', - prerelease: true, - } as AppManifest<'marginalia'>, - { - id: 'star-chart', - demoVideoSrc: '', - description: 'TODO', - devOriginOverride: 'http://localhost:6224', - iconPath: 'icon.png', - name: 'Star Chart', - paidDescription: 'TODO', - paidFeatures: [], - url: 'https://star-chart.biscuits.club', - prerelease: true, - } as AppManifest<'star-chart'>, - { - id: 'humding', - demoVideoSrc: '', - description: 'An app for playing it by ear', - devOriginOverride: 'http://localhost:6225', - iconPath: 'icon.png', - name: 'Humding', - paidDescription: - 'Share your songs with others and sync to all your devices', - paidFeatures: [], - url: 'https://humding.biscuits.club', - prerelease: true, - } as AppManifest<'humding'>, - { - id: 'palette', - demoVideoSrc: '', - description: 'Paint what you see, not what you think you see', - devOriginOverride: 'http://localhost:6226', - iconPath: 'icon.png', - name: 'Palette', - paidDescription: 'Sync your projects across all your devices', - paidFeatures: [], - url: 'https://palette.biscuits.club', - prerelease: true, - }, + { + id: 'gnocchi', + url: 'https://gnocchi.biscuits.club', + name: 'Gnocchi', + description: 'Organize your weekly cooking and groceries', + mainImageUrl: 'https://gnocchi.biscuits.club/og-image.png', + iconPath: 'android-chrome-512x512.png', + size: 4, + devOriginOverride: 'http://localhost:6220', + demoVideoSrc: '/videos/gnocchi-compressed.mp4', + paidFeatures: [ + { + imageUrl: '/images/gnocchi/scanner.png', + title: 'Recipe Scanner', + description: + 'Scan web recipes to create a personal copy. Add notes, make changes, and collaborate on cooking with other plan members.', + }, + { + imageUrl: '/images/gnocchi/groceries-collaboration.png', + title: 'Collaborative Groceries', + description: + 'Share your groceries list with other plan members and stay on the same page. Shopping gets more efficient with realtime presence and progress updates.', + family: true, + }, + { + imageUrl: '/images/gnocchi/recipe-collaboration.png', + title: 'Sous Chef Mode', + description: + 'Team up with other plan members to cook a recipe together. Assign steps and track progress with instant updates.', + family: true, + }, + ], + paidDescription: + 'Your personal cooking app becomes a family groceries list and recipe box.', + } as AppManifest<'gnocchi'>, + { + id: 'trip-tick', + url: 'https://trip-tick.biscuits.club', + name: 'Trip Tick', + description: 'The smartest packing list for your next trip', + iconPath: 'icon.png', + size: 2, + devOriginOverride: 'http://localhost:6221', + demoVideoSrc: '/videos/trip-tick-compressed.mp4', + paidFeatures: [ + { + imageUrl: '/images/trip-tick/weather.png', + title: 'Weather Forecast', + description: + 'Get weather forecasts for your destination over the duration of your trip.', + }, + { + imageUrl: '/images/trip-tick/conditions.png', + title: 'Powerful Conditions', + description: + 'Define conditions for packing list items based on predicted weather to make sure you have everything you need.', + }, + ], + paidDescription: + '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: 'wish-wash', + demoVideoSrc: '', + description: 'TODO', + devOriginOverride: 'http://localhost:6222', + iconPath: 'icon.png', + name: 'Wish Wash', + paidDescription: 'TODO', + paidFeatures: [], + url: 'https://wish-wash.biscuits.club', + prerelease: true, + } as AppManifest<'wish-wash'>, + { + id: 'marginalia', + demoVideoSrc: '', + description: 'TODO', + devOriginOverride: 'http://localhost:6223', + iconPath: 'icon.png', + name: 'Marginalia', + paidDescription: 'TODO', + paidFeatures: [], + url: 'https://marginalia.biscuits.club', + prerelease: true, + } as AppManifest<'marginalia'>, + { + id: 'star-chart', + demoVideoSrc: '', + description: 'TODO', + devOriginOverride: 'http://localhost:6224', + iconPath: 'icon.png', + name: 'Star Chart', + paidDescription: 'TODO', + paidFeatures: [], + url: 'https://star-chart.biscuits.club', + prerelease: true, + } as AppManifest<'star-chart'>, + { + id: 'humding', + demoVideoSrc: '', + description: 'An app for playing it by ear', + devOriginOverride: 'http://localhost:6225', + iconPath: 'icon.png', + name: 'Humding', + paidDescription: + 'Share your songs with others and sync to all your devices', + paidFeatures: [], + url: 'https://humding.biscuits.club', + prerelease: true, + } as AppManifest<'humding'>, + { + id: 'palette', + demoVideoSrc: '/videos/palette-compressed.mp4', + description: 'Paint what you see', + devOriginOverride: 'http://localhost:6226', + iconPath: 'icon.png', + name: 'Palette', + paidDescription: 'Sync your projects across all your devices', + paidFeatures: [], + url: 'https://palette.biscuits.club', + prerelease: false, + }, ] as const; export type AppId = (typeof apps)[number]['id']; export const appIds = apps.map((app) => app.id) as AppId[]; export function isValidAppId(appId: string): appId is AppId { - return appIds.includes(appId as AppId); + return appIds.includes(appId as AppId); } export const appsById = Object.fromEntries( - apps.map((app) => [app.id, app]), + apps.map((app) => [app.id, app]), ) as Record; export function getAppUrl(app: AppManifest) { - if (import.meta.env.DEV) { - return app.devOriginOverride; - } - return app.url; + if (import.meta.env.DEV) { + return app.devOriginOverride; + } + return app.url; } declare global { - interface ImportMetaEnv { - DEV: boolean; - } - interface ImportMeta { - readonly env: ImportMetaEnv; - } + interface ImportMetaEnv { + DEV: boolean; + } + interface ImportMeta { + readonly env: ImportMetaEnv; + } } diff --git a/web/package.json b/web/package.json index d771cbd0..3e77766e 100644 --- a/web/package.json +++ b/web/package.json @@ -7,7 +7,8 @@ "dev": "vite --host --mode=development", "build": "vite build --mode=production", "typecheck": "tsc --noEmit", - "up-ui": "cd .. && pnpm up-ui" + "up-ui": "cd .. && pnpm up-ui", + "prepare-video": "ffmpeg -c:v libx265 -crf 25 -filter:v scale=-1:720 -i" }, "dependencies": { "@a-type/auth-client": "1.0.10", diff --git a/web/public/videos/palette-compressed.mp4 b/web/public/videos/palette-compressed.mp4 new file mode 100644 index 00000000..14cda68f Binary files /dev/null and b/web/public/videos/palette-compressed.mp4 differ