Move UI to BoardUI with slate + salmon theme; add PDF plan export
- Adopt BoardUI (Pro init): theme tokens, Inter, .dark mode, Button, SegmentedControl, Meter, Skeleton and ThemeToggle; remap the app's color aliases onto BoardUI tokens and swap lucide for Remix icons - Retint BoardUI's neutral ramp to cool slate with a salmon accent; drop the dotted background; raise muted text contrast - Simplify the landing page and remove the overlap explorer - Planner: explicit "I have this" toggle on grocery lines, pantry panel above the menu with the staples list spelled out, sticky aisle header fix, and a "Download PDF" plan export (jsPDF, lazy-loaded) - Recipes: replace the family cabbage soup with Cade's Cabbage Soup (adds Bone-In Ham Steak to the catalog); remove Ham, Bean & Cabbage Soup - Remove unused BoardUI components, dependencies and exports Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b02c7a024b
commit
df5dd3a70e
56 changed files with 4749 additions and 788 deletions
35
components/foundations/direction/direction.tsx
Normal file
35
components/foundations/direction/direction.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
"use client";
|
||||
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
import { I18nProvider, useLocale } from "react-aria-components";
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
export type Direction = "ltr" | "rtl";
|
||||
|
||||
export interface DirectionProviderProps extends Omit<HTMLAttributes<HTMLDivElement>, "dir" | "lang" | "children"> {
|
||||
/** BCP 47 language tag. Arabic and Hebrew resolve to RTL; Chinese to LTR. */
|
||||
locale: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/** Keeps CSS direction, React Aria keyboard behavior and portalled menus in sync.
|
||||
* Use at the app root, or nest providers for regions in another language.
|
||||
* The default display: contents boundary preserves the surrounding layout.
|
||||
*/
|
||||
export function DirectionProvider({ locale, ...props }: DirectionProviderProps) {
|
||||
return (
|
||||
<I18nProvider locale={locale}>
|
||||
<DirectionBoundary {...props} />
|
||||
</I18nProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function DirectionBoundary({ children, className, ...props }: Omit<DirectionProviderProps, "locale">) {
|
||||
const { locale, direction } = useLocale();
|
||||
return <div {...props} lang={locale} dir={direction} className={cx("contents", className)}>{children}</div>;
|
||||
}
|
||||
|
||||
/** Also works inside a React Aria I18nProvider. */
|
||||
export function useDirection(): Direction {
|
||||
return useLocale().direction;
|
||||
}
|
||||
72
components/foundations/template-copy/template-copy.tsx
Normal file
72
components/foundations/template-copy/template-copy.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"use client";
|
||||
|
||||
import { cloneElement, createContext, isValidElement, useContext, useMemo, type ReactNode } from "react";
|
||||
|
||||
export type TemplateCopyTranslator = (text: string) => string;
|
||||
type CopySettings = { translate: TemplateCopyTranslator; render?: <T>(node: T) => T };
|
||||
const CopyContext = createContext<CopySettings | null>(null);
|
||||
const MenuContext = createContext<ReactNode>(null);
|
||||
|
||||
/** Optional application language control shared by template account menus. */
|
||||
export function TemplateMenuProvider({ languageSelector, children }: { languageSelector?: ReactNode; children: ReactNode }) {
|
||||
return <MenuContext.Provider value={languageSelector ?? null}>{children}</MenuContext.Provider>;
|
||||
}
|
||||
|
||||
export function useTemplateLanguageSelector() {
|
||||
return useContext(MenuContext);
|
||||
}
|
||||
|
||||
/** Optional application copy for templates. Without a provider, source copy is unchanged. */
|
||||
export function TemplateCopyProvider({ translate, render, children }: CopySettings & { children: ReactNode }) {
|
||||
const settings = useMemo(() => ({ translate, render }), [translate, render]);
|
||||
return <CopyContext.Provider value={settings}>{children}</CopyContext.Provider>;
|
||||
}
|
||||
|
||||
// Only display copy is translated. IDs, form values, data keys, URLs, styles,
|
||||
// event handlers and application state retain their original values.
|
||||
const textProps = new Set([
|
||||
"children", "title", "subtitle", "description", "label", "textValue", "placeholder", "alt",
|
||||
"aria-label", "aria-description", "metricLabel", "emptyMessage", "caption", "heading", "text", "content", "renderValue",
|
||||
]);
|
||||
const dataProps = new Set(["items", "options", "tabs", "columns", "series", "ranges", "stats", "data", "nodes", "bar", "line"]);
|
||||
const dataText = new Set(["label", "title", "description", "subtitle", "header", "role", "joined"]);
|
||||
const renderProps = new Set(["children", "cell", "header", "formatter", "tickFormatter", "labelFormatter", "renderItem", "node", "link", "tick", "label", "content", "renderValue"]);
|
||||
|
||||
/** Translate rendered copy while preserving React nodes, collection IDs and callbacks. */
|
||||
export function translateTemplateCopy<T>(value: T, translate: TemplateCopyTranslator): T {
|
||||
function visit(value: unknown, data = false): unknown {
|
||||
if (typeof value === "string") return data ? value : translate(value);
|
||||
if (Array.isArray(value)) {
|
||||
const result = value.map(item => visit(item, data));
|
||||
return result.every((item, index) => item === value[index]) ? value : result;
|
||||
}
|
||||
if (isValidElement<Record<string, unknown>>(value)) {
|
||||
if (typeof value.type === "string" && ["pre", "code", "kbd", "script", "style"].includes(value.type)) return value;
|
||||
const updates: Record<string, unknown> = {};
|
||||
for (const [key, original] of Object.entries(value.props)) {
|
||||
if (textProps.has(key)) updates[key] = renderProps.has(key) && typeof original === "function"
|
||||
? (...args: unknown[]) => visit(original(...args)) : visit(original);
|
||||
else if (dataProps.has(key)) updates[key] = visit(original, true);
|
||||
else if (renderProps.has(key) && typeof original === "function") updates[key] = (...args: unknown[]) => visit(original(...args));
|
||||
}
|
||||
return Object.entries(updates).some(([key, result]) => result !== value.props[key]) ? cloneElement(value, updates) : value;
|
||||
}
|
||||
if (data && value && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype) {
|
||||
const result: Record<string, unknown> = { ...value };
|
||||
for (const [key, original] of Object.entries(result)) {
|
||||
if (renderProps.has(key) && typeof original === "function") result[key] = (...args: unknown[]) => visit(original(...args));
|
||||
else if (dataText.has(key)) result[key] = visit(original);
|
||||
else if (original && typeof original === "object") result[key] = visit(original, true);
|
||||
}
|
||||
return Object.entries(result).some(([key, item]) => item !== (value as Record<string, unknown>)[key]) ? result : value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return visit(value) as T;
|
||||
}
|
||||
|
||||
/** Apply to a template component's returned JSX; copy updates through React, including portals. */
|
||||
export function useTemplateCopy() {
|
||||
const settings = useContext(CopyContext);
|
||||
return useMemo(() => <T,>(node: T): T => settings?.render ? settings.render(node) : settings ? translateTemplateCopy(node, settings.translate) : node, [settings]);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue