@trypattern/sdk 0.1.30 → 0.1.32

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.
@@ -0,0 +1,198 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Button as BaseButton } from '@base-ui/react/button';
3
+ import * as stylex from '@stylexjs/stylex';
4
+ import { useEffect, useRef } from 'react';
5
+ import { breakpoints, colors, galleryMarker, motion, radii, space } from "../theme/tokens.stylex.js";
6
+ import { focusRing } from "./controlLook.js";
7
+ import { Icon } from "./Icon.js";
8
+ import { Image } from "./Image.js";
9
+ import { interact } from "./interaction.js";
10
+ import { Text } from "./Text.js";
11
+ export function Gallery({ label, photos, value, onChange, sizes, aspect, priority = false, style }) {
12
+ const track = useRef(null);
13
+ const shown = useRef(null);
14
+ const latest = useRef(onChange);
15
+ latest.current = onChange;
16
+ const index = Math.max(0, photos.findIndex((photo) => photo.id === value));
17
+ const current = photos[index];
18
+ useEffect(() => {
19
+ const element = track.current;
20
+ if (element === null)
21
+ return undefined;
22
+ const observer = new IntersectionObserver((entries) => {
23
+ for (const entry of entries) {
24
+ const id = entry.target.getAttribute('data-photo');
25
+ if (entry.intersectionRatio < SHOWN || id === null)
26
+ continue;
27
+ if (shown.current === id)
28
+ continue;
29
+ shown.current = id;
30
+ latest.current(id);
31
+ }
32
+ }, { root: element, threshold: [SHOWN] });
33
+ for (const slide of element.children)
34
+ observer.observe(slide);
35
+ return () => observer.disconnect();
36
+ }, [photos]);
37
+ useEffect(() => {
38
+ const element = track.current;
39
+ if (element === null || current === undefined)
40
+ return;
41
+ if (shown.current === current.id)
42
+ return;
43
+ shown.current = current.id;
44
+ const slide = element.children[index];
45
+ if (!(slide instanceof HTMLElement))
46
+ return;
47
+ element.scrollTo({
48
+ left: slide.offsetLeft -
49
+ Number.parseFloat(getComputedStyle(element).paddingLeft),
50
+ behavior: prefersReducedMotion() ? 'auto' : 'smooth'
51
+ });
52
+ }, [current, index]);
53
+ const go = (next) => {
54
+ const photo = photos[(next + photos.length) % photos.length];
55
+ if (photo === undefined)
56
+ return;
57
+ interact({ primitive: 'Gallery', event: 'change', label });
58
+ onChange(photo.id);
59
+ };
60
+ const hasMany = photos.length > 1;
61
+ return (_jsxs("section", { "aria-roledescription": "carousel", "aria-label": label, ...stylex.props(styles.gallery, style), children: [_jsxs("div", { ...stylex.props(styles.stage, galleryMarker), children: [_jsx("div", { ref: track, tabIndex: hasMany ? 0 : undefined, ...stylex.props(styles.track, focusRing.visible, focusRing.inside), children: photos.map((photo, at) => (_jsx("div", { "data-photo": photo.id, role: "group", "aria-roledescription": "slide", "aria-label": `${at + 1} of ${photos.length}`, ...stylex.props(styles.slide, hasMany && styles.peeking), children: _jsx(Image, { image: photo, sizes: sizes, ...(aspect === undefined ? {} : { aspect }), priority: priority && at === 0 }) }, photo.id))) }), hasMany ? (_jsxs(_Fragment, { children: [_jsx(BaseButton, { "aria-label": "Previous photo", onClick: () => go(index - 1), className: stylex.props(styles.arrow, styles.previous, focusRing.visible)
62
+ .className, children: _jsx(Icon, { name: "chevronLeft" }) }), _jsx(BaseButton, { "aria-label": "Next photo", onClick: () => go(index + 1), className: stylex.props(styles.arrow, styles.next, focusRing.visible)
63
+ .className, children: _jsx(Icon, { name: "chevronRight" }) }), _jsx("span", { "aria-hidden": "true", ...stylex.props(styles.count), children: _jsx(Text, { variant: "caption", weight: "medium", tone: "inherit", children: `${index + 1} of ${photos.length}` }) })] })) : null] }), hasMany ? (_jsx("div", { ...stylex.props(styles.dots), children: photos.map((photo, at) => (_jsx(BaseButton, { "aria-label": `Show photo ${at + 1} of ${photos.length}`, "aria-current": at === index ? 'true' : undefined, onClick: () => go(at), className: stylex.props(styles.dotTarget, focusRing.visible).className, children: _jsx("span", { ...stylex.props(styles.dot, at === index && styles.dotShown) }) }, photo.id))) })) : null] }));
64
+ }
65
+ const SHOWN = 0.6;
66
+ function prefersReducedMotion() {
67
+ return (typeof window !== 'undefined' &&
68
+ window.matchMedia('(prefers-reduced-motion: reduce)').matches);
69
+ }
70
+ const styles = stylex.create({
71
+ gallery: {
72
+ display: 'flex',
73
+ flexDirection: 'column',
74
+ gap: {
75
+ default: space.lg,
76
+ [breakpoints.compact]: space.sm
77
+ },
78
+ minWidth: 0
79
+ },
80
+ stage: {
81
+ position: 'relative'
82
+ },
83
+ track: {
84
+ position: 'relative',
85
+ display: 'flex',
86
+ gap: space.sm,
87
+ paddingInline: {
88
+ default: 0,
89
+ [breakpoints.compact]: space.lg
90
+ },
91
+ overflowX: 'auto',
92
+ overflowY: 'hidden',
93
+ overscrollBehaviorX: 'contain',
94
+ outlineOffset: 0,
95
+ scrollSnapType: 'x mandatory',
96
+ scrollPaddingInline: {
97
+ default: 0,
98
+ [breakpoints.compact]: space.lg
99
+ },
100
+ scrollbarWidth: 'none',
101
+ borderRadius: {
102
+ default: radii.lg,
103
+ [breakpoints.compact]: 0
104
+ }
105
+ },
106
+ slide: {
107
+ flexShrink: 0,
108
+ width: '100%',
109
+ overflow: 'hidden',
110
+ scrollSnapAlign: 'start',
111
+ scrollSnapStop: 'always',
112
+ borderRadius: radii.lg
113
+ },
114
+ peeking: {
115
+ width: {
116
+ default: '100%',
117
+ [breakpoints.compact]: `calc(100% - ${space.xxxl})`
118
+ }
119
+ },
120
+ arrow: {
121
+ position: 'absolute',
122
+ top: '50%',
123
+ display: {
124
+ default: 'none',
125
+ [breakpoints.canHover]: 'flex'
126
+ },
127
+ alignItems: 'center',
128
+ justifyContent: 'center',
129
+ width: `calc(${space.xxl} + ${space.sm})`,
130
+ height: `calc(${space.xxl} + ${space.sm})`,
131
+ padding: 0,
132
+ color: colors.primaryForeground,
133
+ backgroundColor: colors.container,
134
+ borderStyle: 'none',
135
+ borderRadius: radii.full,
136
+ boxShadow: `0 0 0 .5px ${colors.borderDefault}, 0 1px 2px ${colors.borderStrong}, 0 4px 12px -4px ${colors.borderStrong}`,
137
+ opacity: {
138
+ default: 0,
139
+ [stylex.when.ancestor(':hover', galleryMarker)]: 1,
140
+ ':focus-visible': 1
141
+ },
142
+ transform: 'translateY(-50%)',
143
+ transitionTimingFunction: motion.easeOut,
144
+ transitionDuration: motion.fast,
145
+ transitionProperty: 'opacity'
146
+ },
147
+ previous: {
148
+ left: space.lg
149
+ },
150
+ next: {
151
+ right: space.lg
152
+ },
153
+ count: {
154
+ position: 'absolute',
155
+ right: {
156
+ default: space.lg,
157
+ [breakpoints.compact]: `calc(${space.xxl} + ${space.md})`
158
+ },
159
+ bottom: {
160
+ default: space.lg,
161
+ [breakpoints.compact]: space.md
162
+ },
163
+ display: 'flex',
164
+ alignItems: 'center',
165
+ height: space.xl,
166
+ paddingInline: `calc(${space.sm} + ${space.xxs})`,
167
+ color: colors.whiteFill,
168
+ pointerEvents: 'none',
169
+ backgroundColor: colors.overlay,
170
+ borderRadius: radii.full
171
+ },
172
+ dots: {
173
+ display: 'flex',
174
+ alignItems: 'center',
175
+ justifyContent: 'center',
176
+ height: space.xl
177
+ },
178
+ dotTarget: {
179
+ display: 'flex',
180
+ alignItems: 'center',
181
+ justifyContent: 'center',
182
+ width: space.xl,
183
+ height: space.xl,
184
+ padding: 0,
185
+ backgroundColor: 'transparent',
186
+ borderStyle: 'none',
187
+ borderRadius: radii.full
188
+ },
189
+ dot: {
190
+ width: `calc(${space.xs} + ${space.xxs})`,
191
+ height: `calc(${space.xs} + ${space.xxs})`,
192
+ backgroundColor: colors.borderStrong,
193
+ borderRadius: radii.full
194
+ },
195
+ dotShown: {
196
+ backgroundColor: colors.primaryForeground
197
+ }
198
+ });
@@ -0,0 +1,6 @@
1
+ import type { ReactNode } from 'react';
2
+ type HeaderProps = {
3
+ children: ReactNode;
4
+ };
5
+ export declare function Header({ children }: HeaderProps): import("react").JSX.Element;
6
+ export {};
@@ -0,0 +1,42 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import * as stylex from '@stylexjs/stylex';
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { borders, colors, layers, motion } from "../theme/tokens.stylex.js";
5
+ export function Header({ children }) {
6
+ const top = useRef(null);
7
+ const [isScrolled, setScrolled] = useState(false);
8
+ useEffect(() => {
9
+ const element = top.current;
10
+ if (element === null)
11
+ return undefined;
12
+ const observer = new IntersectionObserver(([entry]) => setScrolled(entry !== undefined && !entry.isIntersecting));
13
+ observer.observe(element);
14
+ return () => observer.disconnect();
15
+ }, []);
16
+ return (_jsxs(_Fragment, { children: [_jsx("span", { ref: top, "aria-hidden": "true", ...stylex.props(styles.top) }), _jsx("header", { ...stylex.props(styles.header, isScrolled && styles.scrolled), children: children })] }));
17
+ }
18
+ const styles = stylex.create({
19
+ top: {
20
+ display: 'block',
21
+ height: borders.thin
22
+ },
23
+ header: {
24
+ position: 'sticky',
25
+ top: 0,
26
+ zIndex: layers.header,
27
+ flexShrink: 0,
28
+ marginTop: `calc(-1 * ${borders.thin})`,
29
+ backgroundColor: 'transparent',
30
+ borderBottomColor: 'transparent',
31
+ borderBottomStyle: 'solid',
32
+ borderBottomWidth: borders.thin,
33
+ transitionTimingFunction: motion.easeOut,
34
+ transitionDuration: motion.fast,
35
+ transitionProperty: 'background-color, border-color'
36
+ },
37
+ scrolled: {
38
+ backgroundImage: `linear-gradient(color-mix(in oklab, ${colors.page} 80%, transparent))`,
39
+ borderBottomColor: colors.borderDefault,
40
+ backdropFilter: 'blur(16px)'
41
+ }
42
+ });
@@ -0,0 +1,8 @@
1
+ import type { Placement } from './Placement.ts';
2
+ type ProgressBarProps = {
3
+ label: string;
4
+ value: number;
5
+ style?: Placement;
6
+ };
7
+ export declare function ProgressBar({ label, value, style }: ProgressBarProps): import("react").JSX.Element;
8
+ export {};
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as stylex from '@stylexjs/stylex';
3
+ import { colors, motion, radii, space } from "../theme/tokens.stylex.js";
4
+ export function ProgressBar({ label, value, style }) {
5
+ const percent = Math.round(Math.min(1, Math.max(0, value)) * 100);
6
+ return (_jsx("div", { role: "progressbar", "aria-label": label, "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": percent, ...stylex.props(styles.track, style), children: _jsx("div", { ...stylex.props(styles.fill, fills.width(`${percent}%`)) }) }));
7
+ }
8
+ const styles = stylex.create({
9
+ track: {
10
+ display: 'flex',
11
+ flexShrink: 0,
12
+ width: '100%',
13
+ height: space.xs,
14
+ overflow: 'clip',
15
+ backgroundColor: colors.component,
16
+ borderRadius: radii.full
17
+ },
18
+ fill: {
19
+ height: '100%',
20
+ backgroundColor: colors.brandSolid,
21
+ borderRadius: radii.full,
22
+ transitionTimingFunction: motion.easeOut,
23
+ transitionDuration: motion.fast,
24
+ transitionProperty: 'width'
25
+ }
26
+ });
27
+ const fills = stylex.create({
28
+ width: (width) => ({ width })
29
+ });
@@ -0,0 +1,9 @@
1
+ import type { ReactNode } from 'react';
2
+ type TableRowProps = {
3
+ label: string;
4
+ isCurrent?: boolean;
5
+ onClick: () => void;
6
+ children: ReactNode;
7
+ };
8
+ export declare function TableRow({ label, isCurrent, onClick, children }: TableRowProps): import("react").JSX.Element;
9
+ export {};
@@ -0,0 +1,36 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Button as BaseButton } from '@base-ui/react/button';
3
+ import * as stylex from '@stylexjs/stylex';
4
+ import { borders, colors, space } from "../theme/tokens.stylex.js";
5
+ import { focusRing } from "./controlLook.js";
6
+ import { interact } from "./interaction.js";
7
+ export function TableRow({ label, isCurrent = false, onClick, children }) {
8
+ return (_jsx(BaseButton, { "aria-label": label, "aria-pressed": isCurrent, onClick: () => {
9
+ interact({ primitive: 'TableRow', event: 'click', label });
10
+ onClick();
11
+ }, className: stylex.props(styles.row, isCurrent && styles.current, focusRing.visible, focusRing.inside).className, children: children }));
12
+ }
13
+ const styles = stylex.create({
14
+ row: {
15
+ display: 'flex',
16
+ alignItems: 'center',
17
+ width: '100%',
18
+ height: `calc(${space.xxl} + ${space.xs})`,
19
+ paddingInline: space.xs,
20
+ color: colors.primaryForeground,
21
+ textAlign: 'start',
22
+ backgroundColor: {
23
+ default: 'transparent',
24
+ ':hover': colors.transparentHover
25
+ },
26
+ borderBottomColor: {
27
+ default: colors.borderDefault,
28
+ ':last-child': colors.transparent
29
+ },
30
+ borderBottomStyle: 'solid',
31
+ borderBottomWidth: borders.thin
32
+ },
33
+ current: {
34
+ backgroundColor: colors.transparentHover
35
+ }
36
+ });
@@ -28,10 +28,12 @@ declare const builtInIcons: {
28
28
  chevronUpDown: string;
29
29
  check: string;
30
30
  plus: string;
31
+ image: string[];
31
32
  trash: string[];
32
33
  checkCircle: string[];
33
34
  alertCircle: string[];
34
35
  chevronDown: string;
36
+ chevronLeft: string;
35
37
  chevronRight: string;
36
38
  previous: string;
37
39
  next: string;
@@ -46,6 +46,11 @@ const builtInIcons = {
46
46
  chevronUpDown: 'M8 9.5 12 5.5l4 4M8 14.5l4 4 4-4',
47
47
  check: 'M5 12.5 9.5 17 19 7.5',
48
48
  plus: 'M12 5v14M5 12h14',
49
+ image: [
50
+ 'M6.25 4.75h11.5a2.5 2.5 0 0 1 2.5 2.5v9.5a2.5 2.5 0 0 1-2.5 2.5H6.25a2.5 2.5 0 0 1-2.5-2.5v-9.5a2.5 2.5 0 0 1 2.5-2.5z',
51
+ 'M7.25 10a1.75 1.75 0 1 0 3.5 0a1.75 1.75 0 1 0 -3.5 0',
52
+ 'M4.5 17.5l4.5-4.25 3 2.75 3.5-3.75 4 4'
53
+ ],
49
54
  trash: [
50
55
  'M4.75 6.75h14.5',
51
56
  'M9.75 6.75v-1a2 2 0 0 1 2-2h.5a2 2 0 0 1 2 2v1',
@@ -61,6 +66,7 @@ const builtInIcons = {
61
66
  'M11.5 15.75a0.5 0.5 0 1 0 1 0a0.5 0.5 0 1 0 -1 0'
62
67
  ],
63
68
  chevronDown: 'M7 10l5 5 5-5',
69
+ chevronLeft: 'M14 7l-5 5 5 5',
64
70
  chevronRight: 'M10 7l5 5-5 5',
65
71
  previous: 'M15 18l-6-6 6-6',
66
72
  next: 'M9 18l6-6-6-6',
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  export type Interaction = {
3
- primitive: 'AppFrame' | 'Button' | 'Card' | 'IconButton' | 'Link' | 'ListRow' | 'Select' | 'Switch' | 'Checkbox' | 'SegmentedControl' | 'Menu' | 'ContextMenu' | 'CommandMenu' | 'TextField' | 'Form' | 'Dialog' | 'RadioGroup' | 'ChoiceGroup' | 'Calendar';
3
+ primitive: 'AppFrame' | 'Button' | 'Card' | 'IconButton' | 'Link' | 'ListRow' | 'Select' | 'Switch' | 'Checkbox' | 'SegmentedControl' | 'Menu' | 'ContextMenu' | 'CommandMenu' | 'TextField' | 'Form' | 'Dialog' | 'RadioGroup' | 'ChoiceGroup' | 'Calendar' | 'Gallery' | 'FilePicker' | 'TableRow';
4
4
  event: 'click' | 'change' | 'select' | 'open' | 'close' | 'submit' | 'navigate';
5
5
  label: string;
6
6
  page: string | null;
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+ export declare const pickedFile: z.ZodObject<{
3
+ id: z.ZodString;
4
+ name: z.ZodString;
5
+ type: z.ZodString;
6
+ size: z.ZodNumber;
7
+ }, z.core.$strip>;
8
+ export type PickedFile = z.output<typeof pickedFile>;
9
+ export declare function rememberFile(file: File): PickedFile;
10
+ export declare function pickedFileOf(id: string): File | undefined;
11
+ export declare function forgetFile(id: string): void;
package/dist/files.js ADDED
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ export const pickedFile = z.object({
3
+ id: z.string(),
4
+ name: z.string(),
5
+ type: z.string(),
6
+ size: z.number()
7
+ });
8
+ const picked = new Map();
9
+ export function rememberFile(file) {
10
+ const id = crypto.randomUUID();
11
+ picked.set(id, file);
12
+ return { id, name: file.name, type: file.type, size: file.size };
13
+ }
14
+ export function pickedFileOf(id) {
15
+ return picked.get(id);
16
+ }
17
+ export function forgetFile(id) {
18
+ picked.delete(id);
19
+ }
@@ -1,6 +1,7 @@
1
1
  import { expect } from 'vitest';
2
2
  import type { ApiCall } from '../api.ts';
3
3
  import type { AppClass, RunningApp } from '../app.ts';
4
+ import type { Services } from '../services.ts';
4
5
  import type { Seed, Tables } from '../table.ts';
5
6
  export { expect };
6
7
  export { accessFlows } from './accessFlows.ts';
@@ -39,9 +40,13 @@ export type FlowOptions<Call extends string> = {
39
40
  };
40
41
  export type FlowBinding<Group extends Tables, Command, Snapshot extends {
41
42
  readonly path: string;
42
- }> = {
43
+ }, AppServices extends Services = Record<never, never>> = {
43
44
  App: AppClass<Group, RunningApp<Command, Snapshot>>;
45
+ services?: AppServices;
44
46
  };
47
+ type ServiceCallName<Group extends Services> = {
48
+ [Name in keyof Group & string]: `${Name}.${keyof Group[Name] & string}`;
49
+ }[keyof Group & string];
45
50
  export declare function defineFlow<Group extends Tables, Command, Snapshot extends {
46
51
  readonly path: string;
47
- }>({ App }: FlowBinding<Group, Command, Snapshot>): (name: string, steps: readonly FlowStep<Command, Snapshot, ApiCall<Group>>[], { startAt, failing, signedInAs }?: FlowOptions<ApiCall<Group>>) => void;
52
+ }, AppServices extends Services = Record<never, never>>({ App }: FlowBinding<Group, Command, Snapshot, AppServices>): (name: string, steps: readonly FlowStep<Command, Snapshot, ApiCall<Group> | ServiceCallName<AppServices>>[], { startAt, failing, signedInAs }?: FlowOptions<ApiCall<Group> | ServiceCallName<AppServices>>) => void;
package/dist/index.d.ts CHANGED
@@ -22,7 +22,9 @@ export type { User } from './auth/user.ts';
22
22
  export type { AppClass, AppDefinition, AppDependencies, AppSnapshotShape, BlockStores, CheckedBlocks, DataRouter, ModelServicesOf, NavigateCommand, PressKeyCommand, PublicStores, PlainJsonModel, RunningApp, StoreClasses, StoreInstances, StoresOf, TableStores } from './app.ts';
23
23
  export { defineRoutes } from './routes.ts';
24
24
  export { ServiceRefused, refused } from './services.ts';
25
- export type { ServiceClient, ServiceMemory } from './services.ts';
25
+ export type { ServiceClient, ServiceMemory, UploadOptions } from './services.ts';
26
+ export { pickedFile } from './files.ts';
27
+ export type { PickedFile } from './files.ts';
26
28
  export { manualClock, realClock } from './clock.ts';
27
29
  export type { Clock } from './clock.ts';
28
30
  export type { PageOf, PathOf, RouteDefinition } from './routes.ts';
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export { HttpSessionApi, MemorySessionApi } from "./auth/sessionApi.js";
10
10
  export { userSchema } from "./auth/user.js";
11
11
  export { defineRoutes } from "./routes.js";
12
12
  export { ServiceRefused, refused } from "./services.js";
13
+ export { pickedFile } from "./files.js";
13
14
  export { manualClock, realClock } from "./clock.js";
14
15
  export { Model, Store, action, appPath, defineActions } from "./registered.js";
15
16
  export { Cache, Cached } from "./cache.js";