@tristan2828/ui-foundation 4.61.1 → 4.63.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/conventions/docs/cell-patterns.md +16 -11
- package/conventions/docs/design-language.md +132 -23
- package/dist/components/app/app-shell.js +28 -2
- package/dist/components/app/board.js +1 -1
- package/dist/components/app/data-table.js +1 -1
- package/dist/components/app/foundation-providers.d.ts +14 -1
- package/dist/components/app/foundation-providers.js +4 -3
- package/dist/components/app/markdown.d.ts +7 -2
- package/dist/components/app/markdown.js +15 -20
- package/dist/components/app/milkdown-editor.d.ts +1 -1
- package/dist/components/app/milkdown-editor.js +58 -17
- package/dist/components/app/module-card.js +1 -1
- package/dist/components/app/palette-provider.d.ts +14 -0
- package/dist/components/app/palette-provider.js +35 -0
- package/dist/components/app/prose-look.d.ts +7 -0
- package/dist/components/app/rich-text-editor.d.ts +8 -1
- package/dist/components/app/rich-text-editor.js +6 -0
- package/dist/components/app/rich-text-format-items.js +1 -1
- package/dist/components/app/rich-text-node-views.d.ts +9 -3
- package/dist/components/app/rich-text-node-views.js +12 -8
- package/dist/components/app/rich-text-table-grips.d.ts +31 -0
- package/dist/components/app/rich-text-table-grips.js +216 -0
- package/dist/components/app/rich-text-table.d.ts +19 -2
- package/dist/components/app/rich-text-table.js +232 -1
- package/dist/components/app/rich-text-toolbar.d.ts +4 -9
- package/dist/components/app/rich-text-toolbar.js +9 -11
- package/dist/components/app/rich-text-widgets.js +2 -2
- package/dist/components/app/widget-card.js +2 -2
- package/dist/components/ui/badge.d.ts +1 -1
- package/dist/components/ui/badge.js +5 -2
- package/dist/components/ui/button.js +10 -10
- package/dist/components/ui/card.js +3 -3
- package/dist/components/ui/checkbox.js +1 -1
- package/dist/components/ui/combobox.js +1 -1
- package/dist/components/ui/dialog.js +1 -1
- package/dist/components/ui/input-group.js +1 -1
- package/dist/components/ui/input.js +1 -1
- package/dist/components/ui/progress.d.ts +7 -0
- package/dist/components/ui/progress.js +19 -0
- package/dist/components/ui/select.js +1 -1
- package/dist/components/ui/sheet.js +1 -1
- package/dist/components/ui/slider.d.ts +3 -0
- package/dist/components/ui/slider.js +12 -0
- package/dist/components/ui/textarea.js +1 -1
- package/dist/hooks/use-palette.d.ts +14 -0
- package/dist/hooks/use-palette.js +7 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/lib/markdown-table.d.ts +28 -0
- package/dist/lib/markdown-table.js +106 -0
- package/dist/lib/palettes.d.ts +21 -0
- package/dist/lib/palettes.js +74 -0
- package/package.json +1 -1
- package/styles/index.css +152 -0
- package/styles/theme.css +321 -11
- package/vite/index.d.ts +7 -1
- package/vite/pwa.js +21 -7
- package/dist/components/app/rich-text-table-menu.d.ts +0 -20
- package/dist/components/app/rich-text-table-menu.js +0 -53
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Progress as ProgressPrimitive } from "@base-ui/react/progress";
|
|
2
|
+
declare function Progress({ className, children, value, ...props }: ProgressPrimitive.Root.Props): import("react").JSX.Element;
|
|
3
|
+
declare function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props): import("react").JSX.Element;
|
|
4
|
+
declare function ProgressIndicator({ className, ...props }: ProgressPrimitive.Indicator.Props): import("react").JSX.Element;
|
|
5
|
+
declare function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props): import("react").JSX.Element;
|
|
6
|
+
declare function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props): import("react").JSX.Element;
|
|
7
|
+
export { Progress, ProgressTrack, ProgressIndicator, ProgressLabel, ProgressValue, };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Progress as ProgressPrimitive } from "@base-ui/react/progress";
|
|
3
|
+
import { cn } from "cn";
|
|
4
|
+
function Progress({ className, children, value, ...props }) {
|
|
5
|
+
return (_jsxs(ProgressPrimitive.Root, { value: value, "data-slot": "progress", className: cn("flex flex-wrap gap-3", className), ...props, children: [children, _jsx(ProgressTrack, { children: _jsx(ProgressIndicator, {}) })] }));
|
|
6
|
+
}
|
|
7
|
+
function ProgressTrack({ className, ...props }) {
|
|
8
|
+
return (_jsx(ProgressPrimitive.Track, { className: cn("relative flex h-1 w-full items-center overflow-x-hidden rounded-full bg-muted", className), "data-slot": "progress-track", ...props }));
|
|
9
|
+
}
|
|
10
|
+
function ProgressIndicator({ className, ...props }) {
|
|
11
|
+
return (_jsx(ProgressPrimitive.Indicator, { "data-slot": "progress-indicator", className: cn("h-full bg-primary transition-all", className), ...props }));
|
|
12
|
+
}
|
|
13
|
+
function ProgressLabel({ className, ...props }) {
|
|
14
|
+
return (_jsx(ProgressPrimitive.Label, { className: cn("text-sm font-medium", className), "data-slot": "progress-label", ...props }));
|
|
15
|
+
}
|
|
16
|
+
function ProgressValue({ className, ...props }) {
|
|
17
|
+
return (_jsx(ProgressPrimitive.Value, { className: cn("ml-auto text-sm text-muted-foreground tabular-nums", className), "data-slot": "progress-value", ...props }));
|
|
18
|
+
}
|
|
19
|
+
export { Progress, ProgressTrack, ProgressIndicator, ProgressLabel, ProgressValue, };
|
|
@@ -12,7 +12,7 @@ function SelectValue({ className, ...props }) {
|
|
|
12
12
|
return (_jsx(SelectPrimitive.Value, { "data-slot": "select-value", className: cn("flex flex-1 text-left", className), ...props }));
|
|
13
13
|
}
|
|
14
14
|
function SelectTrigger({ className, size = "default", children, ...props }) {
|
|
15
|
-
return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: cn("flex w-fit items-center justify-between gap-1.5 rounded-
|
|
15
|
+
return (_jsxs(SelectPrimitive.Trigger, { "data-slot": "select-trigger", "data-size": size, className: cn("flex w-fit items-center justify-between gap-1.5 rounded-md border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", className), ...props, children: [children, _jsx(SelectPrimitive.Icon, { render: _jsx(ChevronDownIcon, { className: "pointer-events-none size-4 text-muted-foreground" }) })] }));
|
|
16
16
|
}
|
|
17
17
|
function SelectContent({ className, children, side = "bottom", sideOffset = 4, align = "center", alignOffset = 0, alignItemWithTrigger = true, ...props }) {
|
|
18
18
|
return (_jsx(SelectPrimitive.Portal, { children: _jsx(SelectPrimitive.Positioner, { side: side, sideOffset: sideOffset, align: align, alignOffset: alignOffset, alignItemWithTrigger: alignItemWithTrigger, className: "isolate z-50", children: _jsxs(SelectPrimitive.Popup, { "data-slot": "select-content", "data-align-trigger": alignItemWithTrigger, className: cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className), ...props, children: [_jsx(SelectScrollUpButton, {}), _jsx(SelectPrimitive.List, { children: children }), _jsx(SelectScrollDownButton, {})] }) }) }));
|
|
@@ -18,7 +18,7 @@ function SheetPortal({ ...props }) {
|
|
|
18
18
|
return _jsx(SheetPrimitive.Portal, { "data-slot": "sheet-portal", ...props });
|
|
19
19
|
}
|
|
20
20
|
function SheetOverlay({ className, ...props }) {
|
|
21
|
-
return (_jsx(SheetPrimitive.Backdrop, { "data-slot": "sheet-overlay", className: cn("fixed inset-0 z-50 bg-black/
|
|
21
|
+
return (_jsx(SheetPrimitive.Backdrop, { "data-slot": "sheet-overlay", className: cn("fixed inset-0 z-50 bg-black/20 transition-opacity data-ending-style:opacity-0 data-starting-style:opacity-0 supports-backdrop-filter:backdrop-blur-sm dark:bg-black/40", className), ...props }));
|
|
22
22
|
}
|
|
23
23
|
function SheetContent({ className, children, side = "right", showCloseButton = true, ...props }) {
|
|
24
24
|
return (_jsxs(SheetPortal, { children: [_jsx(SheetOverlay, {}), _jsxs(SheetPrimitive.Popup, { "data-slot": "sheet-content", "data-side": side, className: cn("fixed z-50 flex flex-col gap-4 bg-popover bg-clip-padding text-sm text-popover-foreground shadow-lg transition duration-200 ease-in-out data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem] data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm", className), ...props, children: [children, showCloseButton && (_jsxs(SheetPrimitive.Close, { "data-slot": "sheet-close", render: _jsx(Button, { variant: "ghost", className: "absolute top-3 right-3", size: "icon-sm" }), children: [_jsx(XIcon, {}), _jsx("span", { className: "sr-only", children: "Close" })] }))] })] }));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Slider as SliderPrimitive } from "@base-ui/react/slider";
|
|
3
|
+
import { cn } from "cn";
|
|
4
|
+
function Slider({ className, defaultValue, value, min = 0, max = 100, ...props }) {
|
|
5
|
+
const _values = Array.isArray(value)
|
|
6
|
+
? value
|
|
7
|
+
: Array.isArray(defaultValue)
|
|
8
|
+
? defaultValue
|
|
9
|
+
: [min, max];
|
|
10
|
+
return (_jsx(SliderPrimitive.Root, { className: cn("data-horizontal:w-full data-vertical:h-full", className), "data-slot": "slider", defaultValue: defaultValue, value: value, min: min, max: max, thumbAlignment: "edge", ...props, children: _jsxs(SliderPrimitive.Control, { className: "relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col", children: [_jsx(SliderPrimitive.Track, { "data-slot": "slider-track", className: "relative grow overflow-hidden rounded-full bg-muted select-none data-horizontal:h-1 data-horizontal:w-full data-vertical:h-full data-vertical:w-1", children: _jsx(SliderPrimitive.Indicator, { "data-slot": "slider-range", className: "bg-primary select-none data-horizontal:h-full data-vertical:w-full" }) }), Array.from({ length: _values.length }, (_, index) => (_jsx(SliderPrimitive.Thumb, { "data-slot": "slider-thumb", className: "relative block size-3 shrink-0 rounded-full border border-ring bg-white ring-ring/50 transition-[color,box-shadow] palette:shadow-(--thumb-glow) select-none after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 disabled:pointer-events-none disabled:opacity-50" }, index)))] }) }));
|
|
11
|
+
}
|
|
12
|
+
export { Slider };
|
|
@@ -2,6 +2,6 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { cn } from "cn";
|
|
4
4
|
function Textarea({ className, ...props }) {
|
|
5
|
-
return (_jsx("textarea", { "data-slot": "textarea", className: cn("flex field-sizing-content min-h-16 w-full rounded-
|
|
5
|
+
return (_jsx("textarea", { "data-slot": "textarea", className: cn("flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40", className), ...props }));
|
|
6
6
|
}
|
|
7
7
|
export { Textarea };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PaletteName } from '../lib/palettes.js';
|
|
2
|
+
export type PaletteState = {
|
|
3
|
+
/** The palette on screen, or null for an app without one (light and dark as the system says). */
|
|
4
|
+
palette: PaletteName | null;
|
|
5
|
+
/** The app's own default. */
|
|
6
|
+
defaultPalette: PaletteName | null;
|
|
7
|
+
/** Whether this app lets a device pick (FoundationProviders' `palettePicker`). */
|
|
8
|
+
picker: boolean;
|
|
9
|
+
/** Saves `palette` as this device's choice. Does nothing without the picker. */
|
|
10
|
+
setPalette: (palette: PaletteName) => void;
|
|
11
|
+
};
|
|
12
|
+
export declare const PaletteContext: import("react").Context<PaletteState>;
|
|
13
|
+
/** The palette on screen and, with the picker on, a way to change it on this device. */
|
|
14
|
+
export declare function usePalette(): PaletteState;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
const NO_PALETTE = { palette: null, defaultPalette: null, picker: false, setPalette: () => { } };
|
|
3
|
+
export const PaletteContext = createContext(NO_PALETTE);
|
|
4
|
+
/** The palette on screen and, with the picker on, a way to change it on this device. */
|
|
5
|
+
export function usePalette() {
|
|
6
|
+
return useContext(PaletteContext);
|
|
7
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { FoundationProviders } from './components/app/foundation-providers.js';
|
|
|
25
25
|
export { Markdown, type MarkdownProps } from './components/app/markdown.js';
|
|
26
26
|
export { countTasks, toggleTaskAt, type TaskCount } from './lib/markdown-tasks.js';
|
|
27
27
|
export { NotesOutline, type NotesOutlineProps } from './components/app/notes-outline.js';
|
|
28
|
-
export type { HeadingFrom, HeadingLevel } from './components/app/prose-look.js';
|
|
28
|
+
export type { HeadingFrom, HeadingLevel, TableLook } from './components/app/prose-look.js';
|
|
29
29
|
export { MockModeBanner } from './components/app/mock-mode-banner.js';
|
|
30
30
|
export { MultiChoice } from './components/app/multi-choice.js';
|
|
31
31
|
export { MultiPick, type MultiPickOption, type MultiPickProps } from './components/app/multi-pick.js';
|
|
@@ -76,6 +76,8 @@ export { useWakeLock } from './hooks/use-wake-lock.js';
|
|
|
76
76
|
export { useSessionClock, type SessionClock, type SessionClockOptions, type SessionClockState, type SessionStatus, } from './hooks/use-session-clock.js';
|
|
77
77
|
export { usePrefersReducedMotion } from './hooks/use-prefers-reduced-motion.js';
|
|
78
78
|
export { useMuted } from './hooks/use-muted.js';
|
|
79
|
+
export { usePalette, type PaletteState } from './hooks/use-palette.js';
|
|
80
|
+
export { PALETTE_LABELS, PALETTE_STORAGE_KEY, PALETTES, isPaletteName, type PaletteName } from './lib/palettes.js';
|
|
79
81
|
export { createPushController, usePushPermission, type PushController, type PushControllerOptions, type PushEnvironment, type PushPermission, type PushStatus, } from './hooks/use-push-permission.js';
|
|
80
82
|
export { cn } from './lib/utils.js';
|
|
81
83
|
export { canVibrate, cancelCues, chime, cuesInSync, hush, onCuesRecovered, say, sequence, unlockAudio, vibrate, type ChimeOptions, type Cue, type CueTime, type SayOptions, type SequenceOptions, } from './lib/audio-cues.js';
|
package/dist/index.js
CHANGED
|
@@ -79,6 +79,8 @@ export { useWakeLock } from './hooks/use-wake-lock.js';
|
|
|
79
79
|
export { useSessionClock, } from './hooks/use-session-clock.js';
|
|
80
80
|
export { usePrefersReducedMotion } from './hooks/use-prefers-reduced-motion.js';
|
|
81
81
|
export { useMuted } from './hooks/use-muted.js';
|
|
82
|
+
export { usePalette } from './hooks/use-palette.js';
|
|
83
|
+
export { PALETTE_LABELS, PALETTE_STORAGE_KEY, PALETTES, isPaletteName } from './lib/palettes.js';
|
|
82
84
|
export { createPushController, usePushPermission, } from './hooks/use-push-permission.js';
|
|
83
85
|
// Utilities
|
|
84
86
|
export { cn } from './lib/utils.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type HighlightColor } from './markdown-highlight.js';
|
|
2
|
+
/** A run of a cell's text, with the highlight and bold it's in. */
|
|
3
|
+
export type CellRun = {
|
|
4
|
+
text: string;
|
|
5
|
+
color: HighlightColor | null;
|
|
6
|
+
bold: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** The cell's colour: every run with words in it highlighted, all in one colour. */
|
|
9
|
+
export declare function cellColor(runs: readonly CellRun[]): HighlightColor | null;
|
|
10
|
+
/** Whether a cell's words are all bold (and it has some). */
|
|
11
|
+
export declare function cellBold(runs: readonly CellRun[]): boolean;
|
|
12
|
+
/** Whether a table's first column is row headers: its body rows' first cells with words, all bold. */
|
|
13
|
+
export declare function isHeaderColumn(firstCells: readonly (readonly CellRun[])[]): boolean;
|
|
14
|
+
type HastNode = {
|
|
15
|
+
type: string;
|
|
16
|
+
tagName?: string;
|
|
17
|
+
value?: string;
|
|
18
|
+
properties?: Record<string, unknown>;
|
|
19
|
+
children?: HastNode[];
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* For <Markdown>: marks each table's coloured cells (`data-color`), and
|
|
23
|
+
* makes a header column's first cells row headers (`<th scope="row">`).
|
|
24
|
+
*/
|
|
25
|
+
export declare function rehypeTables(): (tree: HastNode) => void;
|
|
26
|
+
/** Sorts cell texts as a person would: numbers by value ($4.99 before $12), then words, empty last. */
|
|
27
|
+
export declare function compareCells(a: string, b: string): number;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// What a GFM table holds beyond its cells, read from the Markdown it
|
|
2
|
+
// already has, so nothing new is written into the text (the developer's
|
|
3
|
+
// choice A, 2026-11-04):
|
|
4
|
+
//
|
|
5
|
+
// - A column's alignment: GFM's own (`:---:`, `---:`).
|
|
6
|
+
// - A cell's colour: its whole text in one highlight, `=={green}$4.99==`
|
|
7
|
+
// (src/lib/markdown-highlight.ts), paints the cell in that wash. Text
|
|
8
|
+
// only partly highlighted is just highlighted words. An empty cell has
|
|
9
|
+
// no colour (a highlight needs words).
|
|
10
|
+
// - A header column: when the first cells of the body rows are all bold
|
|
11
|
+
// (those with words; at least one has), the first column is drawn as
|
|
12
|
+
// row headers.
|
|
13
|
+
//
|
|
14
|
+
// The rule is the same for <Markdown> (rehypeTables, on the HTML tree)
|
|
15
|
+
// and the rich-text editor (its table decorations, rich-text-table.ts):
|
|
16
|
+
// both turn a cell into runs of text with their highlight and bold, and
|
|
17
|
+
// ask the functions here.
|
|
18
|
+
import { isHighlightColor } from './markdown-highlight.js';
|
|
19
|
+
/** The cell's colour: every run with words in it highlighted, all in one colour. */
|
|
20
|
+
export function cellColor(runs) {
|
|
21
|
+
const worded = runs.filter((run) => run.text.trim() !== '');
|
|
22
|
+
if (worded.length === 0)
|
|
23
|
+
return null;
|
|
24
|
+
const color = worded[0].color;
|
|
25
|
+
return color && worded.every((run) => run.color === color) ? color : null;
|
|
26
|
+
}
|
|
27
|
+
/** Whether a cell's words are all bold (and it has some). */
|
|
28
|
+
export function cellBold(runs) {
|
|
29
|
+
const worded = runs.filter((run) => run.text.trim() !== '');
|
|
30
|
+
return worded.length > 0 && worded.every((run) => run.bold);
|
|
31
|
+
}
|
|
32
|
+
/** Whether a table's first column is row headers: its body rows' first cells with words, all bold. */
|
|
33
|
+
export function isHeaderColumn(firstCells) {
|
|
34
|
+
const worded = firstCells.filter((runs) => runs.some((run) => run.text.trim() !== ''));
|
|
35
|
+
return worded.length > 0 && worded.every(cellBold);
|
|
36
|
+
}
|
|
37
|
+
// A cell's runs, from its HTML: a <mark data-color> is a highlight
|
|
38
|
+
// (remarkHighlights writes one), <strong> is bold.
|
|
39
|
+
function runsOf(node, color = null, bold = false) {
|
|
40
|
+
if (node.type === 'text')
|
|
41
|
+
return [{ text: node.value ?? '', color, bold }];
|
|
42
|
+
let inner = color;
|
|
43
|
+
if (node.tagName === 'mark') {
|
|
44
|
+
const value = node.properties?.dataColor;
|
|
45
|
+
inner = isHighlightColor(value) ? value : 'amber';
|
|
46
|
+
}
|
|
47
|
+
const strong = bold || node.tagName === 'strong';
|
|
48
|
+
return (node.children ?? []).flatMap((child) => runsOf(child, inner, strong));
|
|
49
|
+
}
|
|
50
|
+
const elements = (node, tagName) => (node?.children ?? []).filter((child) => child.type === 'element' && child.tagName === tagName);
|
|
51
|
+
function eachTable(node, visit) {
|
|
52
|
+
if (node.type === 'element' && node.tagName === 'table')
|
|
53
|
+
visit(node);
|
|
54
|
+
for (const child of node.children ?? [])
|
|
55
|
+
eachTable(child, visit);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* For <Markdown>: marks each table's coloured cells (`data-color`), and
|
|
59
|
+
* makes a header column's first cells row headers (`<th scope="row">`).
|
|
60
|
+
*/
|
|
61
|
+
export function rehypeTables() {
|
|
62
|
+
return (tree) => {
|
|
63
|
+
eachTable(tree, (table) => {
|
|
64
|
+
const rows = [...elements(table, 'thead'), ...elements(table, 'tbody')].flatMap((section) => elements(section, 'tr'));
|
|
65
|
+
const body = elements(elements(table, 'tbody')[0], 'tr');
|
|
66
|
+
for (const row of rows) {
|
|
67
|
+
for (const cell of row.children ?? []) {
|
|
68
|
+
if (cell.type !== 'element')
|
|
69
|
+
continue;
|
|
70
|
+
const color = cellColor(runsOf(cell));
|
|
71
|
+
if (color)
|
|
72
|
+
cell.properties = { ...cell.properties, dataColor: color };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const firsts = body.map((row) => (row.children ?? []).find((cell) => cell.type === 'element'));
|
|
76
|
+
if (!isHeaderColumn(firsts.map((cell) => (cell ? runsOf(cell) : []))))
|
|
77
|
+
return;
|
|
78
|
+
table.properties = { ...table.properties, dataHeaderColumn: '' };
|
|
79
|
+
for (const cell of firsts) {
|
|
80
|
+
if (!cell)
|
|
81
|
+
continue;
|
|
82
|
+
cell.tagName = 'th';
|
|
83
|
+
cell.properties = { ...cell.properties, scope: 'row' };
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Sorts cell texts as a person would: numbers by value ($4.99 before $12), then words, empty last. */
|
|
89
|
+
export function compareCells(a, b) {
|
|
90
|
+
const x = numberIn(a);
|
|
91
|
+
const y = numberIn(b);
|
|
92
|
+
if (x !== null && y !== null && x !== y)
|
|
93
|
+
return x - y;
|
|
94
|
+
const left = a.trim();
|
|
95
|
+
const right = b.trim();
|
|
96
|
+
if (!left && right)
|
|
97
|
+
return 1;
|
|
98
|
+
if (left && !right)
|
|
99
|
+
return -1;
|
|
100
|
+
return left.localeCompare(right, undefined, { numeric: true, sensitivity: 'base' });
|
|
101
|
+
}
|
|
102
|
+
// The first number in a cell's text: "$1,299.50" is 1299.5, "-3 kg" is -3.
|
|
103
|
+
function numberIn(text) {
|
|
104
|
+
const found = text.replace(/,/g, '').match(/-?\d+(?:\.\d+)?/);
|
|
105
|
+
return found ? Number(found[0]) : null;
|
|
106
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const PALETTES: readonly ["indigo-cyan", "indigo-rose", "graphite-periwinkle", "violet"];
|
|
2
|
+
export type PaletteName = (typeof PALETTES)[number];
|
|
3
|
+
/** Each palette's name as a person reads it, in the guide's ranked order. */
|
|
4
|
+
export declare const PALETTE_LABELS: Record<PaletteName, string>;
|
|
5
|
+
/** Where a device's chosen palette is kept. */
|
|
6
|
+
export declare const PALETTE_STORAGE_KEY = "ui-foundation:palette";
|
|
7
|
+
export declare function isPaletteName(value: unknown): value is PaletteName;
|
|
8
|
+
type StorageLike = Pick<Storage, 'getItem' | 'setItem'>;
|
|
9
|
+
export type PaletteStore = {
|
|
10
|
+
/** The saved choice, or null when there is none (or storage is blocked, or it names no palette). */
|
|
11
|
+
get: () => PaletteName | null;
|
|
12
|
+
set: (palette: PaletteName) => void;
|
|
13
|
+
subscribe: (listener: () => void) => () => void;
|
|
14
|
+
/** Another tab saved a choice: forget the in-memory one and re-read. */
|
|
15
|
+
changedElsewhere: () => void;
|
|
16
|
+
};
|
|
17
|
+
/** The store under usePalette, taking the storage so its rules are unit tested (tests/palettes.test.ts). */
|
|
18
|
+
export declare function createPaletteStore(getStorage: () => StorageLike | undefined): PaletteStore;
|
|
19
|
+
export declare const browserPaletteStore: PaletteStore;
|
|
20
|
+
export declare function subscribeToBrowserPaletteStore(listener: () => void): () => void;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The four palettes (design-language.md "Palettes"): an app's own dark
|
|
2
|
+
// look, one of those the shared style guide chose. FoundationProviders'
|
|
3
|
+
// `palette` names the app's default; with `palettePicker` the app shell
|
|
4
|
+
// offers all four and the choice is remembered on this device
|
|
5
|
+
// (localStorage, this store). A new device starts on the default. The
|
|
6
|
+
// palette rides on <html> as data-palette, so popups portaled to <body>
|
|
7
|
+
// wear it too, and styles/theme.css's `.dark[data-palette]` blocks do the
|
|
8
|
+
// rest (components/app/palette-provider.tsx, hooks/use-palette.ts).
|
|
9
|
+
export const PALETTES = ['indigo-cyan', 'indigo-rose', 'graphite-periwinkle', 'violet'];
|
|
10
|
+
/** Each palette's name as a person reads it, in the guide's ranked order. */
|
|
11
|
+
export const PALETTE_LABELS = {
|
|
12
|
+
'indigo-cyan': 'Indigo and Cyan',
|
|
13
|
+
'indigo-rose': 'Indigo and Rose',
|
|
14
|
+
'graphite-periwinkle': 'Graphite and Periwinkle',
|
|
15
|
+
violet: 'Violet',
|
|
16
|
+
};
|
|
17
|
+
/** Where a device's chosen palette is kept. */
|
|
18
|
+
export const PALETTE_STORAGE_KEY = 'ui-foundation:palette';
|
|
19
|
+
export function isPaletteName(value) {
|
|
20
|
+
return typeof value === 'string' && PALETTES.includes(value);
|
|
21
|
+
}
|
|
22
|
+
/** The store under usePalette, taking the storage so its rules are unit tested (tests/palettes.test.ts). */
|
|
23
|
+
export function createPaletteStore(getStorage) {
|
|
24
|
+
// What this page chose, for when storage refuses it.
|
|
25
|
+
let memory = null;
|
|
26
|
+
const listeners = new Set();
|
|
27
|
+
const notify = () => {
|
|
28
|
+
for (const listener of listeners)
|
|
29
|
+
listener();
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
get() {
|
|
33
|
+
if (memory)
|
|
34
|
+
return memory;
|
|
35
|
+
try {
|
|
36
|
+
const stored = getStorage()?.getItem(PALETTE_STORAGE_KEY);
|
|
37
|
+
return isPaletteName(stored) ? stored : null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
set(palette) {
|
|
44
|
+
memory = palette;
|
|
45
|
+
try {
|
|
46
|
+
getStorage()?.setItem(PALETTE_STORAGE_KEY, palette);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Storage blocked or full: kept for this page only.
|
|
50
|
+
}
|
|
51
|
+
notify();
|
|
52
|
+
},
|
|
53
|
+
subscribe(listener) {
|
|
54
|
+
listeners.add(listener);
|
|
55
|
+
return () => listeners.delete(listener);
|
|
56
|
+
},
|
|
57
|
+
changedElsewhere() {
|
|
58
|
+
memory = null;
|
|
59
|
+
notify();
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export const browserPaletteStore = createPaletteStore(() => (typeof window === 'undefined' ? undefined : window.localStorage));
|
|
64
|
+
let listeningForOtherTabs = false;
|
|
65
|
+
export function subscribeToBrowserPaletteStore(listener) {
|
|
66
|
+
if (!listeningForOtherTabs && typeof window !== 'undefined') {
|
|
67
|
+
listeningForOtherTabs = true;
|
|
68
|
+
window.addEventListener('storage', (event) => {
|
|
69
|
+
if (event.key === PALETTE_STORAGE_KEY || event.key === null)
|
|
70
|
+
browserPaletteStore.changedElsewhere();
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return browserPaletteStore.subscribe(listener);
|
|
74
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tristan2828/ui-foundation",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.63.0",
|
|
4
4
|
"description": "Shared UI foundation for contract-first, database-backed React apps: shadcn primitives, DataTable/EntityForm composites, auth, the gateway error seam, design tokens, lint rules and the AI playbook.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/styles/index.css
CHANGED
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
composite's own rows and padding tighten under data-density="compact"
|
|
21
21
|
on any ancestor, the way the table's tokens do. `density-compact:py-1`.
|
|
22
22
|
Text never shrinks under it. */
|
|
23
|
+
/* Under a palette only (FoundationProviders' `palette`, data-palette on
|
|
24
|
+
<html>): the shared style guide's glow, which an app without one never
|
|
25
|
+
gets. `palette:hover:shadow-(--primary-glow)`. */
|
|
26
|
+
@custom-variant palette (&:where([data-palette] *));
|
|
27
|
+
|
|
23
28
|
@custom-variant density-compact (&:where([data-density="compact"], [data-density="compact"] *));
|
|
24
29
|
|
|
25
30
|
@theme inline {
|
|
@@ -54,6 +59,7 @@
|
|
|
54
59
|
--color-info-foreground: var(--info-foreground);
|
|
55
60
|
--color-info-text: var(--info-text);
|
|
56
61
|
--color-link: var(--link);
|
|
62
|
+
--color-card-header: var(--card-header);
|
|
57
63
|
--color-highlight-amber: var(--highlight-amber);
|
|
58
64
|
--color-highlight-yellow: var(--highlight-yellow);
|
|
59
65
|
--color-highlight-green: var(--highlight-green);
|
|
@@ -94,6 +100,10 @@
|
|
|
94
100
|
--radius-2xl: calc(var(--radius) * 1.8);
|
|
95
101
|
--radius-3xl: calc(var(--radius) * 2.2);
|
|
96
102
|
--radius-4xl: calc(var(--radius) * 2.6);
|
|
103
|
+
/* Motion (the shared style guide): quick, standard easing, for every
|
|
104
|
+
transition-* utility that doesn't name its own duration. */
|
|
105
|
+
--default-transition-duration: 120ms;
|
|
106
|
+
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
/* Typography roles: one class sets size, line height and weight together,
|
|
@@ -102,11 +112,13 @@
|
|
|
102
112
|
font-size: var(--type-page-title-size);
|
|
103
113
|
line-height: var(--type-page-title-leading);
|
|
104
114
|
font-weight: var(--type-page-title-weight);
|
|
115
|
+
letter-spacing: var(--type-heading-tracking);
|
|
105
116
|
}
|
|
106
117
|
@utility type-section-title {
|
|
107
118
|
font-size: var(--type-section-title-size);
|
|
108
119
|
line-height: var(--type-section-title-leading);
|
|
109
120
|
font-weight: var(--type-section-title-weight);
|
|
121
|
+
letter-spacing: var(--type-heading-tracking);
|
|
110
122
|
}
|
|
111
123
|
@utility type-body {
|
|
112
124
|
font-size: var(--type-body-size);
|
|
@@ -135,11 +147,13 @@
|
|
|
135
147
|
font-size: var(--type-heading-1-size);
|
|
136
148
|
line-height: var(--type-heading-1-leading);
|
|
137
149
|
font-weight: var(--type-heading-1-weight);
|
|
150
|
+
letter-spacing: var(--type-heading-tracking);
|
|
138
151
|
}
|
|
139
152
|
@utility type-heading-2 {
|
|
140
153
|
font-size: var(--type-heading-2-size);
|
|
141
154
|
line-height: var(--type-heading-2-leading);
|
|
142
155
|
font-weight: var(--type-heading-2-weight);
|
|
156
|
+
letter-spacing: var(--type-heading-tracking);
|
|
143
157
|
}
|
|
144
158
|
@utility type-heading-3 {
|
|
145
159
|
font-size: var(--type-heading-3-size);
|
|
@@ -147,6 +161,52 @@
|
|
|
147
161
|
font-weight: var(--type-heading-3-weight);
|
|
148
162
|
}
|
|
149
163
|
|
|
164
|
+
/* A hovered hub tile, widget or card, under a palette: its edge turns the
|
|
165
|
+
accent and a soft halo of it shows (--edge-glow). On an element that
|
|
166
|
+
draws its edge with ring-1 and its shadow with shadow-* (Card does), it
|
|
167
|
+
recolours the ring and adds the halo to the shadow it already has.
|
|
168
|
+
ModuleCard, WidgetCard and Board's cards carry it; a DataTable card
|
|
169
|
+
layout's cards get it with `*:data-[slot=card]:glow-edge` on the
|
|
170
|
+
item. */
|
|
171
|
+
@utility glow-edge {
|
|
172
|
+
transition-property: box-shadow;
|
|
173
|
+
transition-duration: var(--default-transition-duration);
|
|
174
|
+
transition-timing-function: var(--default-transition-timing-function);
|
|
175
|
+
&:where([data-palette] *):hover {
|
|
176
|
+
--tw-ring-color: color-mix(in srgb, var(--primary) 75%, var(--border));
|
|
177
|
+
--tw-shadow: var(--card-shadow, 0 0 #0000), var(--edge-glow);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* A primary button's shine under a palette: a light band sweeping across
|
|
182
|
+
on hover (650ms), once. Not under reduced motion, and never on another
|
|
183
|
+
variant. In components, so a button's own position utility still wins. */
|
|
184
|
+
@layer components {
|
|
185
|
+
[data-palette] [data-slot="button"][data-variant="default"] {
|
|
186
|
+
position: relative;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
}
|
|
189
|
+
[data-palette] [data-slot="button"][data-variant="default"]::after {
|
|
190
|
+
content: "";
|
|
191
|
+
position: absolute;
|
|
192
|
+
inset: 0;
|
|
193
|
+
pointer-events: none;
|
|
194
|
+
background: linear-gradient(105deg, transparent 35%, color-mix(in srgb, var(--white) 40%, transparent) 50%, transparent 65%);
|
|
195
|
+
transform: translateX(-130%);
|
|
196
|
+
}
|
|
197
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
198
|
+
[data-palette] [data-slot="button"][data-variant="default"]:hover::after {
|
|
199
|
+
animation: button-shine 650ms ease-out;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@keyframes button-shine {
|
|
205
|
+
to {
|
|
206
|
+
transform: translateX(130%);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
150
210
|
@layer base {
|
|
151
211
|
* {
|
|
152
212
|
@apply border-border outline-ring/50;
|
|
@@ -159,6 +219,98 @@
|
|
|
159
219
|
}
|
|
160
220
|
}
|
|
161
221
|
|
|
222
|
+
/* Tables in written text (<Markdown> and the rich-text editor; each puts
|
|
223
|
+
its table in a `.prose-table` box that scrolls sideways). Columns fit
|
|
224
|
+
their content: the table is only as wide as its words, a cell wraps at
|
|
225
|
+
18rem, and a right-aligned column (numbers) never wraps and uses
|
|
226
|
+
tabular figures. Two looks, set by `tableLook` on either renderer
|
|
227
|
+
(`data-table-look` on an ancestor):
|
|
228
|
+
- compact, the default: a full grid of hairlines, tight cells, the
|
|
229
|
+
header row on the heading band;
|
|
230
|
+
- boxed: a rounded box, roomier cells, the header row on the muted
|
|
231
|
+
fill, zebra rows (as DataTable's).
|
|
232
|
+
Row headers (a header column: src/lib/markdown-table.ts) sit on the
|
|
233
|
+
muted fill, semibold. A coloured cell (its whole text one highlight)
|
|
234
|
+
is painted in that wash, its words' own wash dropped. Unlayered, so a
|
|
235
|
+
mark's utility classes inside don't outrank it. */
|
|
236
|
+
.prose-table {
|
|
237
|
+
width: fit-content;
|
|
238
|
+
/* The space it's in, less its own margins (the text's inset beside a
|
|
239
|
+
heading band): 100% would push it past the text by them. */
|
|
240
|
+
max-width: 100%;
|
|
241
|
+
max-width: -webkit-fill-available;
|
|
242
|
+
max-width: stretch;
|
|
243
|
+
overflow-x: auto;
|
|
244
|
+
/* Holds what's absolutely placed inside (a link's screen-reader text),
|
|
245
|
+
so it scrolls with the table instead of widening the page. */
|
|
246
|
+
position: relative;
|
|
247
|
+
}
|
|
248
|
+
.prose-table > table {
|
|
249
|
+
width: auto;
|
|
250
|
+
border-collapse: collapse;
|
|
251
|
+
}
|
|
252
|
+
.prose-table :is(td, th) {
|
|
253
|
+
min-width: 3rem;
|
|
254
|
+
max-width: 18rem;
|
|
255
|
+
padding: 0.25rem var(--table-cell-px);
|
|
256
|
+
vertical-align: top;
|
|
257
|
+
white-space: normal;
|
|
258
|
+
overflow-wrap: break-word;
|
|
259
|
+
border: 1px solid var(--border);
|
|
260
|
+
font-weight: var(--type-body-weight);
|
|
261
|
+
}
|
|
262
|
+
.prose-table th {
|
|
263
|
+
text-align: start;
|
|
264
|
+
}
|
|
265
|
+
.prose-table :is(td, th):is([style*="right"], [align="right"]) {
|
|
266
|
+
white-space: nowrap;
|
|
267
|
+
font-variant-numeric: tabular-nums;
|
|
268
|
+
}
|
|
269
|
+
.prose-table th:not([scope="row"], [data-row-header]) {
|
|
270
|
+
background: var(--heading-band);
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
}
|
|
273
|
+
.prose-table :is(th[scope="row"], [data-row-header]):not([data-color]) {
|
|
274
|
+
background: var(--muted);
|
|
275
|
+
}
|
|
276
|
+
.prose-table :is(th[scope="row"], [data-row-header]) {
|
|
277
|
+
font-weight: 600;
|
|
278
|
+
}
|
|
279
|
+
[data-table-look="boxed"] .prose-table {
|
|
280
|
+
border: 1px solid var(--border);
|
|
281
|
+
border-radius: var(--radius);
|
|
282
|
+
}
|
|
283
|
+
[data-table-look="boxed"] .prose-table :is(td, th) {
|
|
284
|
+
padding: var(--table-cell-py) 0.75rem;
|
|
285
|
+
border-width: 0 1px 1px 0;
|
|
286
|
+
}
|
|
287
|
+
[data-table-look="boxed"] .prose-table tr > :last-child {
|
|
288
|
+
border-right-width: 0;
|
|
289
|
+
}
|
|
290
|
+
[data-table-look="boxed"] .prose-table tr:last-child > * {
|
|
291
|
+
border-bottom-width: 0;
|
|
292
|
+
}
|
|
293
|
+
[data-table-look="boxed"] .prose-table th:not([scope="row"], [data-row-header]) {
|
|
294
|
+
background: var(--muted);
|
|
295
|
+
font-weight: var(--type-label-weight);
|
|
296
|
+
}
|
|
297
|
+
/* Zebra: <Markdown>'s body rows (after its <thead>), or the editor's,
|
|
298
|
+
whose header row is its only <tbody>'s first. */
|
|
299
|
+
[data-table-look="boxed"] .prose-table :is(thead + tbody > tr:nth-child(even), tbody:only-child > tr:nth-child(odd):not(:first-child)) > :not([data-color], th[scope="row"], [data-row-header]) {
|
|
300
|
+
background: var(--table-stripe);
|
|
301
|
+
}
|
|
302
|
+
.prose-table :is(td, th)[data-color="amber"] { background: var(--highlight-amber); }
|
|
303
|
+
.prose-table :is(td, th)[data-color="yellow"] { background: var(--highlight-yellow); }
|
|
304
|
+
.prose-table :is(td, th)[data-color="green"] { background: var(--highlight-green); }
|
|
305
|
+
.prose-table :is(td, th)[data-color="red"] { background: var(--highlight-red); }
|
|
306
|
+
.prose-table [data-color] mark {
|
|
307
|
+
background: transparent;
|
|
308
|
+
}
|
|
309
|
+
/* Cells picked by dragging across them, in the editor. */
|
|
310
|
+
.prose-table .selectedCell {
|
|
311
|
+
background: var(--table-highlight);
|
|
312
|
+
}
|
|
313
|
+
|
|
162
314
|
/* Printed notes (EntityView's Print notes, src/components/app/entity-view-print.tsx).
|
|
163
315
|
While its copy is in <body>, paper shows that copy alone: the app beside
|
|
164
316
|
it (shell, header, rail, dialogs) is left off, and the copy is in the
|