@trypattern/sdk 0.1.8 → 0.1.10

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.
Files changed (88) hide show
  1. package/README.md +37 -8
  2. package/dist/app.d.ts +1 -0
  3. package/dist/app.js +4 -3
  4. package/dist/cli/app.js +120 -7
  5. package/dist/cli/publishing.js +3 -1
  6. package/dist/components/AppFrame.d.ts +5 -1
  7. package/dist/components/AppFrame.js +25 -7
  8. package/dist/components/Appear.d.ts +4 -1
  9. package/dist/components/Appear.js +82 -3
  10. package/dist/components/Card.d.ts +7 -2
  11. package/dist/components/Card.js +59 -3
  12. package/dist/components/Checkbox.d.ts +10 -0
  13. package/dist/components/Checkbox.js +59 -0
  14. package/dist/components/Choice.d.ts +15 -0
  15. package/dist/components/Choice.js +80 -0
  16. package/dist/components/ChoiceGroup.d.ts +11 -0
  17. package/dist/components/ChoiceGroup.js +44 -0
  18. package/dist/components/Icon.d.ts +2 -1
  19. package/dist/components/Icon.js +2 -2
  20. package/dist/components/Image.d.ts +11 -0
  21. package/dist/components/Image.js +76 -0
  22. package/dist/components/Link.js +8 -3
  23. package/dist/components/ListRow.d.ts +2 -1
  24. package/dist/components/ListRow.js +7 -4
  25. package/dist/components/PlainScreen.d.ts +16 -0
  26. package/dist/components/PlainScreen.js +80 -0
  27. package/dist/components/PlainSidebar.d.ts +15 -0
  28. package/dist/components/PlainSidebar.js +179 -0
  29. package/dist/components/Screen.d.ts +3 -1
  30. package/dist/components/Screen.js +5 -1
  31. package/dist/components/Sidebar.js +2 -1
  32. package/dist/components/TextField.d.ts +3 -1
  33. package/dist/components/TextField.js +25 -4
  34. package/dist/components/choiceContext.d.ts +4 -0
  35. package/dist/components/choiceContext.js +7 -0
  36. package/dist/components/frame.d.ts +2 -0
  37. package/dist/components/icons.d.ts +3 -0
  38. package/dist/components/icons.js +4 -1
  39. package/dist/components/interaction.d.ts +1 -1
  40. package/dist/entityStore.d.ts +1 -0
  41. package/dist/entityStore.js +1 -1
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.js +1 -0
  44. package/dist/media.d.ts +36 -0
  45. package/dist/media.js +74 -0
  46. package/dist/patternAccess.d.ts +21 -0
  47. package/dist/patternAccess.js +110 -0
  48. package/dist/publishWire.d.ts +318 -0
  49. package/dist/publishWire.js +128 -1
  50. package/dist/react.d.ts +5 -0
  51. package/dist/react.js +4 -0
  52. package/dist/rules/checkApiRouting.js +1 -1
  53. package/dist/rules/lintPlugin.js +13 -4
  54. package/dist/rules/messages.d.ts +1 -0
  55. package/dist/rules/messages.js +1 -0
  56. package/dist/schema.d.ts +2 -0
  57. package/dist/schema.js +1 -0
  58. package/dist/server/createServer.d.ts +92 -6
  59. package/dist/server/createServer.js +38 -2
  60. package/dist/server/database.d.ts +74 -0
  61. package/dist/server/database.js +93 -0
  62. package/dist/server/env.d.ts +3 -1
  63. package/dist/server/imageHeader.d.ts +10 -0
  64. package/dist/server/imageHeader.js +159 -0
  65. package/dist/server/index.d.ts +6 -1
  66. package/dist/server/index.js +3 -0
  67. package/dist/server/jsonLd.d.ts +45 -0
  68. package/dist/server/jsonLd.js +88 -0
  69. package/dist/server/keyset.d.ts +13 -0
  70. package/dist/server/keyset.js +52 -0
  71. package/dist/server/media.d.ts +16 -0
  72. package/dist/server/media.js +135 -0
  73. package/dist/server/pages.d.ts +72 -0
  74. package/dist/server/pages.js +301 -0
  75. package/dist/server/patternKeys.d.ts +1 -0
  76. package/dist/server/patternKeys.js +3 -0
  77. package/dist/server/patternViewer.d.ts +6 -0
  78. package/dist/server/patternViewer.js +27 -0
  79. package/dist/server/services.js +1 -1
  80. package/dist/server/tables.d.ts +19 -3
  81. package/dist/server/tables.js +38 -14
  82. package/dist/serviceClients.js +3 -5
  83. package/dist/start.js +26 -1
  84. package/dist/table.d.ts +1 -0
  85. package/dist/table.js +8 -1
  86. package/dist/tables.d.ts +2 -1
  87. package/dist/tables.js +10 -1
  88. package/package.json +1 -1
@@ -0,0 +1,179 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Menu as BaseMenu } from '@base-ui/react/menu';
3
+ import * as stylex from '@stylexjs/stylex';
4
+ import { NavLink } from 'react-router';
5
+ import { borders, breakpoints, colors, controls, fontSizes, fontWeights, layers, lineHeights, motion, radii, shadows, space, widths } from "../theme/tokens.stylex.js";
6
+ import { Avatar } from "./Avatar.js";
7
+ import { Badge } from "./Badge.js";
8
+ import { focusRing, transparentLook } from "./controlLook.js";
9
+ import { Button } from "./Button.js";
10
+ import { Icon } from "./Icon.js";
11
+ import { interact } from "./interaction.js";
12
+ import { MenuItems } from "./MenuItems.js";
13
+ import { Popup } from "./Popup.js";
14
+ import { ScrollArea } from "./ScrollArea.js";
15
+ import { Text } from "./Text.js";
16
+ export function PlainSidebar({ title, user, accountMenu, links, isToggled, isAnimating, onFollowLink, onSearch }) {
17
+ const followLink = (link) => {
18
+ interact({ primitive: 'AppFrame', event: 'navigate', label: link.label });
19
+ onFollowLink();
20
+ };
21
+ return (_jsxs("nav", { ...stylex.props(styles.sidebar, isToggled && styles.sidebarToggled, isAnimating && styles.sidebarAnimating), children: [_jsx("div", { ...stylex.props(styles.header), children: _jsx("span", { ...stylex.props(styles.title), children: title }) }), onSearch === undefined ? null : (_jsx(Button, { variant: "secondary", icon: "search", shortcut: ['mod', 'k'], isFullWidth: true, onClick: onSearch, children: "Search" })), _jsx("div", { ...stylex.props(styles.body), children: _jsx(ScrollArea, { children: _jsx("div", { ...stylex.props(styles.group), children: links.map((item) => (_jsxs(NavLink, { onClick: () => followLink(item), to: item.to, end: item.to === '/' ||
22
+ links.some((other) => String(other.to).startsWith(`${String(item.to)}/`)), className: ({ isActive }) => stylex.props(styles.item, isActive && styles.itemCurrent, focusRing.visible, focusRing.inside).className ?? '', children: [_jsx(Icon, { name: item.icon, weight: "light" }), _jsx(Text, { variant: "label", isTruncated: true, style: styles.itemLabel, children: item.label }), typeof item.count !== 'number' ? null : (_jsx(Badge, { variant: "transparent", children: item.count }))] }, item.to))) }) }) }), user === null ? null : (_jsxs(BaseMenu.Root, { children: [_jsxs(BaseMenu.Trigger, { className: (state) => stylex.props(styles.account, transparentLook.rest, transparentLook.hoverable, state.open && transparentLook.open, styles.accountText, focusRing.visible).className, children: [_jsx(Avatar, { name: user.name, imageUrl: user.avatarUrl }), _jsxs("span", { ...stylex.props(styles.who), children: [_jsx("span", { ...stylex.props(styles.name), children: user.name }), _jsx("span", { ...stylex.props(styles.email), children: user.email })] }), _jsx(Icon, { name: "chevronUpDown", tone: "secondary" })] }), _jsx(BaseMenu.Portal, { children: _jsx(BaseMenu.Positioner, { side: "top", align: "start", sideOffset: 4, className: stylex.props(styles.positioner).className, children: _jsx(BaseMenu.Popup, { render: (props, state) => (_jsx(Popup, { ...props, kind: "menu", isOpen: state.open, transitionStatus: state.transitionStatus })), children: _jsx(MenuItems, { items: accountMenu, primitive: "Menu" }) }) }) })] }))] }));
23
+ }
24
+ const styles = stylex.create({
25
+ sidebar: {
26
+ position: {
27
+ default: 'relative',
28
+ [breakpoints.compact]: 'fixed'
29
+ },
30
+ insetBlock: {
31
+ default: null,
32
+ [breakpoints.compact]: 0
33
+ },
34
+ insetInlineStart: {
35
+ default: null,
36
+ [breakpoints.compact]: 0
37
+ },
38
+ zIndex: {
39
+ default: null,
40
+ [breakpoints.compact]: layers.sidebar
41
+ },
42
+ display: 'flex',
43
+ visibility: {
44
+ default: null,
45
+ [breakpoints.compact]: 'hidden'
46
+ },
47
+ flexShrink: 0,
48
+ flexDirection: 'column',
49
+ gap: space.lg,
50
+ width: widths.sidebar,
51
+ height: '100%',
52
+ paddingBlock: space.lg,
53
+ paddingInline: space.md,
54
+ backgroundColor: colors.page,
55
+ borderInlineEndColor: colors.borderDefault,
56
+ borderInlineEndStyle: {
57
+ default: 'solid',
58
+ [breakpoints.compact]: 'none'
59
+ },
60
+ borderInlineEndWidth: borders.thin,
61
+ boxShadow: {
62
+ default: null,
63
+ [breakpoints.compact]: shadows.extraLarge
64
+ },
65
+ transform: {
66
+ default: null,
67
+ [breakpoints.compact]: 'translateX(-100%)'
68
+ }
69
+ },
70
+ sidebarAnimating: {
71
+ transitionTimingFunction: motion.slideEasing,
72
+ transitionDuration: motion.slide,
73
+ transitionProperty: 'transform, visibility'
74
+ },
75
+ sidebarToggled: {
76
+ visibility: {
77
+ default: null,
78
+ [breakpoints.compact]: 'visible'
79
+ },
80
+ transform: {
81
+ default: null,
82
+ [breakpoints.compact]: 'translateX(0)'
83
+ }
84
+ },
85
+ header: {
86
+ display: 'flex',
87
+ flexShrink: 0,
88
+ alignItems: 'center',
89
+ height: controls.heightMd,
90
+ paddingInline: controls.paddingSm
91
+ },
92
+ title: {
93
+ overflow: 'hidden',
94
+ textOverflow: 'ellipsis',
95
+ fontSize: fontSizes.bodySm,
96
+ fontWeight: fontWeights.semibold,
97
+ lineHeight: lineHeights.bodySm,
98
+ color: colors.primaryForeground,
99
+ whiteSpace: 'nowrap'
100
+ },
101
+ body: {
102
+ flexGrow: 1,
103
+ minHeight: 0
104
+ },
105
+ group: {
106
+ display: 'flex',
107
+ flexDirection: 'column',
108
+ gap: borders.thin
109
+ },
110
+ item: {
111
+ display: 'flex',
112
+ gap: space.sm,
113
+ alignItems: 'center',
114
+ height: {
115
+ default: controls.heightMd,
116
+ [breakpoints.compact]: controls.heightXl
117
+ },
118
+ paddingInline: controls.paddingSm,
119
+ color: {
120
+ default: colors.secondaryForeground,
121
+ ':hover': colors.primaryForeground
122
+ },
123
+ textDecoration: 'none',
124
+ backgroundColor: {
125
+ default: 'transparent',
126
+ ':hover': colors.transparentHover
127
+ },
128
+ borderRadius: radii.md
129
+ },
130
+ itemCurrent: {
131
+ color: colors.primaryForeground,
132
+ backgroundColor: colors.transparentHover
133
+ },
134
+ itemLabel: {
135
+ flexGrow: 1
136
+ },
137
+ account: {
138
+ display: 'flex',
139
+ flexShrink: 0,
140
+ gap: controls.gapMd,
141
+ alignItems: 'center',
142
+ width: '100%',
143
+ height: controls.heightXl,
144
+ paddingInline: controls.paddingXs,
145
+ textAlign: 'start',
146
+ outlineOffset: 0,
147
+ borderRadius: radii.lg
148
+ },
149
+ accountText: {
150
+ color: colors.primaryForeground
151
+ },
152
+ who: {
153
+ display: 'flex',
154
+ flexGrow: 1,
155
+ flexDirection: 'column',
156
+ minWidth: 0
157
+ },
158
+ name: {
159
+ overflow: 'hidden',
160
+ textOverflow: 'ellipsis',
161
+ fontSize: fontSizes.bodySm,
162
+ fontWeight: fontWeights.medium,
163
+ lineHeight: lineHeights.caption,
164
+ color: colors.primaryForeground,
165
+ whiteSpace: 'nowrap'
166
+ },
167
+ email: {
168
+ overflow: 'hidden',
169
+ textOverflow: 'ellipsis',
170
+ fontSize: fontSizes.captionLg,
171
+ lineHeight: lineHeights.caption,
172
+ color: colors.secondaryForeground,
173
+ whiteSpace: 'nowrap'
174
+ },
175
+ positioner: {
176
+ zIndex: layers.menu,
177
+ outline: 'none'
178
+ }
179
+ });
@@ -2,12 +2,14 @@ import type { ReactNode } from 'react';
2
2
  import type { AppPath } from '../registered.ts';
3
3
  type ScreenProps = {
4
4
  title: string;
5
+ eyebrow?: string;
5
6
  parent?: {
6
7
  label: string;
7
8
  to: AppPath;
8
9
  };
9
10
  actions?: ReactNode;
11
+ width?: 'xs' | 'sm' | 'md' | 'lg' | 'full';
10
12
  children: ReactNode;
11
13
  };
12
- export declare function Screen({ title, parent, actions, children }: ScreenProps): import("react").JSX.Element;
14
+ export declare function Screen({ title, eyebrow, parent, actions, width, children }: ScreenProps): import("react").JSX.Element;
13
15
  export {};
@@ -3,13 +3,17 @@ import * as stylex from '@stylexjs/stylex';
3
3
  import { use } from 'react';
4
4
  import { borders, colors, controls, space } from "../theme/tokens.stylex.js";
5
5
  import { FrameContext } from "./frame.js";
6
+ import { PlainScreen } from "./PlainScreen.js";
6
7
  import { Icon } from "./Icon.js";
7
8
  import { IconButton } from "./IconButton.js";
8
9
  import { Link } from "./Link.js";
9
10
  import { ScrollArea } from "./ScrollArea.js";
10
11
  import { Text } from "./Text.js";
11
- export function Screen({ title, parent, actions, children }) {
12
+ export function Screen({ title, eyebrow, parent, actions, width = 'md', children }) {
12
13
  const frame = use(FrameContext);
14
+ if (frame?.look === 'plain') {
15
+ return (_jsx(PlainScreen, { title: title, eyebrow: eyebrow, parent: parent, actions: actions, width: width, onToggleSidebar: frame.onToggleSidebar, children: children }));
16
+ }
13
17
  return (_jsxs("div", { ...stylex.props(styles.screen), children: [_jsxs("header", { ...stylex.props(styles.header), children: [_jsxs("div", { ...stylex.props(styles.leading), children: [frame === null ? null : (_jsx(IconButton, { icon: "panelLeft", label: "Toggle sidebar", onClick: frame.onToggleSidebar })), _jsxs("div", { ...stylex.props(styles.breadcrumb), children: [parent === undefined ? null : (_jsxs(_Fragment, { children: [_jsx(Link, { to: parent.to, tone: "secondary", children: parent.label }), _jsx(Icon, { name: "chevronRight", tone: "tertiary" })] })), _jsx(Text, { as: "h1", variant: "label", tone: "primary", isTruncated: true, children: title })] })] }), _jsx("div", { ...stylex.props(styles.actions), children: actions })] }), _jsx(ScrollArea, { style: styles.body, children: children })] }));
14
18
  }
15
19
  const styles = stylex.create({
@@ -15,7 +15,8 @@ export function Sidebar({ workspace, workspaceMenu, links, isToggled, isAnimatin
15
15
  interact({ primitive: 'AppFrame', event: 'navigate', label: link.label });
16
16
  onFollowLink();
17
17
  };
18
- return (_jsxs("nav", { ...stylex.props(styles.sidebar, isToggled && styles.sidebarToggled, isAnimating && styles.sidebarAnimating), children: [_jsx("div", { ...stylex.props(styles.header), children: _jsx(Menu, { trigger: _jsxs(_Fragment, { children: [_jsx("span", { ...stylex.props(styles.logo), children: _jsx(Text, { variant: "caption", children: workspace.charAt(0) }) }), _jsx(Text, { variant: "label", isTruncated: true, children: workspace }), _jsx(Icon, { name: "chevronDown" })] }), items: workspaceMenu }) }), onSearch === undefined ? null : (_jsx(Button, { variant: "secondary", icon: "search", shortcut: ['mod', 'k'], isFullWidth: true, onClick: onSearch, children: "Search" })), _jsx("div", { ...stylex.props(styles.body), children: _jsx(ScrollArea, { children: _jsxs("div", { ...stylex.props(styles.group), children: [_jsx("span", { ...stylex.props(styles.groupLabel), children: _jsx(Text, { variant: "label", tone: "tertiary", children: "Workspace" }) }), links.map((item) => (_jsxs(NavLink, { onClick: () => followLink(item), to: item.to, end: item.to === '/', className: ({ isActive }) => stylex.props(styles.item, isActive && styles.itemCurrent, focusRing.visible, focusRing.inside).className ?? '', children: [_jsx(Icon, { name: item.icon }), _jsx(Text, { variant: "label", isTruncated: true, style: styles.itemLabel, children: item.label }), typeof item.count !== 'number' ? null : (_jsx(Badge, { variant: "transparent", children: item.count }))] }, item.to)))] }) }) })] }));
18
+ return (_jsxs("nav", { ...stylex.props(styles.sidebar, isToggled && styles.sidebarToggled, isAnimating && styles.sidebarAnimating), children: [_jsx("div", { ...stylex.props(styles.header), children: _jsx(Menu, { trigger: _jsxs(_Fragment, { children: [_jsx("span", { ...stylex.props(styles.logo), children: _jsx(Text, { variant: "caption", children: workspace.charAt(0) }) }), _jsx(Text, { variant: "label", isTruncated: true, children: workspace }), _jsx(Icon, { name: "chevronDown" })] }), items: workspaceMenu }) }), onSearch === undefined ? null : (_jsx(Button, { variant: "secondary", icon: "search", shortcut: ['mod', 'k'], isFullWidth: true, onClick: onSearch, children: "Search" })), _jsx("div", { ...stylex.props(styles.body), children: _jsx(ScrollArea, { children: _jsxs("div", { ...stylex.props(styles.group), children: [_jsx("span", { ...stylex.props(styles.groupLabel), children: _jsx(Text, { variant: "label", tone: "tertiary", children: "Workspace" }) }), links.map((item) => (_jsxs(NavLink, { onClick: () => followLink(item), to: item.to, end: item.to === '/' ||
19
+ links.some((other) => String(other.to).startsWith(`${String(item.to)}/`)), className: ({ isActive }) => stylex.props(styles.item, isActive && styles.itemCurrent, focusRing.visible, focusRing.inside).className ?? '', children: [_jsx(Icon, { name: item.icon }), _jsx(Text, { variant: "label", isTruncated: true, style: styles.itemLabel, children: item.label }), typeof item.count !== 'number' ? null : (_jsx(Badge, { variant: "transparent", children: item.count }))] }, item.to)))] }) }) })] }));
19
20
  }
20
21
  const styles = stylex.create({
21
22
  sidebar: {
@@ -18,8 +18,10 @@ type TextFieldProps = {
18
18
  inputMode?: 'text' | 'email' | 'numeric' | 'decimal' | 'tel' | 'url' | 'search';
19
19
  maxLength?: number;
20
20
  pattern?: string;
21
+ multiline?: boolean;
22
+ rows?: number;
21
23
  size?: Size;
22
24
  style?: Placement;
23
25
  };
24
- export declare function TextField({ label, isLabelHidden, description, error, onChange, size, style, ...input }: TextFieldProps): import("react").JSX.Element;
26
+ export declare function TextField({ label, isLabelHidden, description, error, onChange, multiline, rows, size, style, ...input }: TextFieldProps): import("react").JSX.Element;
25
27
  export {};
@@ -2,19 +2,19 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Field } from '@base-ui/react/field';
3
3
  import * as stylex from '@stylexjs/stylex';
4
4
  import { useRef } from 'react';
5
- import { borders, colors, fontSizes, fontWeights, lineHeights, motion, shadows, space } from "../theme/tokens.stylex.js";
5
+ import { borders, colors, controls, fontSizes, fontWeights, lineHeights, motion, shadows, space } from "../theme/tokens.stylex.js";
6
6
  import { controlSizes } from "./controlLook.js";
7
7
  import { interact } from "./interaction.js";
8
- export function TextField({ label, isLabelHidden = false, description, error, onChange, size = 'md', style, ...input }) {
8
+ export function TextField({ label, isLabelHidden = false, description, error, onChange, multiline = false, rows = 3, size = 'md', style, ...input }) {
9
9
  const isInvalid = error !== undefined;
10
10
  const valueOnFocus = useRef('');
11
- return (_jsxs(Field.Root, { invalid: isInvalid, disabled: input.disabled ?? false, className: stylex.props(styles.field, style).className, children: [isLabelHidden ? null : (_jsx(Field.Label, { className: stylex.props(styles.label).className, children: label })), _jsx(Field.Control, { ...input, "aria-label": isLabelHidden ? label : undefined, onChange: (event) => onChange?.(event.currentTarget.value), onFocus: (event) => {
11
+ return (_jsxs(Field.Root, { invalid: isInvalid, disabled: input.disabled ?? false, className: stylex.props(styles.field, style).className, children: [isLabelHidden ? null : (_jsx(Field.Label, { className: stylex.props(styles.label).className, children: label })), _jsx(Field.Control, { ...input, ...(multiline ? { render: _jsx("textarea", { rows: rows }) } : {}), "aria-label": isLabelHidden ? label : undefined, onChange: (event) => onChange?.(event.currentTarget.value), onFocus: (event) => {
12
12
  valueOnFocus.current = event.currentTarget.value;
13
13
  }, onBlur: (event) => {
14
14
  if (event.currentTarget.value !== valueOnFocus.current) {
15
15
  interact({ primitive: 'TextField', event: 'change', label });
16
16
  }
17
- }, className: stylex.props(styles.input, controlSizes[size], isInvalid && styles.invalid, input.disabled === true && styles.disabled).className }), description === undefined ? null : (_jsx(Field.Description, { className: stylex.props(styles.note).className, children: description })), error === undefined ? null : (_jsx(Field.Error, { match: true, className: stylex.props(styles.error).className, children: error }))] }));
17
+ }, className: stylex.props(styles.input, controlSizes[size], multiline && styles.multiline, multiline && multilinePaddings[size], isInvalid && styles.invalid, input.disabled === true && styles.disabled).className }), description === undefined ? null : (_jsx(Field.Description, { className: stylex.props(styles.note).className, children: description })), error === undefined ? null : (_jsx(Field.Error, { match: true, className: stylex.props(styles.error).className, children: error }))] }));
18
18
  }
19
19
  const focusRing = `inset 0 0 0 ${borders.hairline} ${colors.brandBorderFocus}, 0 0 0 ${borders.thin} ${colors.brandBorderFocus}, ${shadows.default}`;
20
20
  const invalidRing = `inset 0 0 0 ${borders.hairline} ${colors.dangerBorderDefault}, 0 0 0 ${borders.thin} ${colors.dangerBorderDefault}, ${shadows.default}`;
@@ -49,6 +49,10 @@ const styles = stylex.create({
49
49
  color: colors.tertiaryForeground
50
50
  }
51
51
  },
52
+ multiline: {
53
+ height: 'auto',
54
+ resize: 'vertical'
55
+ },
52
56
  invalid: {
53
57
  boxShadow: {
54
58
  default: invalidRing,
@@ -75,3 +79,20 @@ const styles = stylex.create({
75
79
  color: colors.dangerText
76
80
  }
77
81
  });
82
+ const multilinePaddings = stylex.create({
83
+ xs: {
84
+ paddingBlock: `calc((${controls.heightXs} - ${lineHeights.caption}) / 2)`
85
+ },
86
+ sm: {
87
+ paddingBlock: `calc((${controls.heightSm} - ${lineHeights.bodySm}) / 2)`
88
+ },
89
+ md: {
90
+ paddingBlock: `calc((${controls.heightMd} - ${lineHeights.bodySm}) / 2)`
91
+ },
92
+ lg: {
93
+ paddingBlock: `calc((${controls.heightLg} - ${lineHeights.bodySm}) / 2)`
94
+ },
95
+ xl: {
96
+ paddingBlock: `calc((${controls.heightXl} - ${lineHeights.bodySm}) / 2)`
97
+ }
98
+ });
@@ -0,0 +1,4 @@
1
+ export declare const ChoiceContext: import("react").Context<{
2
+ value: string | null;
3
+ }>;
4
+ export declare function useChosen(): string | null;
@@ -0,0 +1,7 @@
1
+ import { createContext, useContext } from 'react';
2
+ export const ChoiceContext = createContext({
3
+ value: null
4
+ });
5
+ export function useChosen() {
6
+ return useContext(ChoiceContext).value;
7
+ }
@@ -1,4 +1,6 @@
1
+ export type FrameLook = 'shell' | 'plain';
1
2
  export type Frame = {
3
+ look: FrameLook;
2
4
  onToggleSidebar: () => void;
3
5
  };
4
6
  export declare const FrameContext: import("react").Context<Frame | null>;
@@ -43,6 +43,9 @@ declare const builtInIcons: {
43
43
  copy: string[];
44
44
  arrowDown: string;
45
45
  logOut: string[];
46
+ today: string;
47
+ checklist: string;
48
+ chart: string;
46
49
  };
47
50
  export declare const builtInIconNames: string[];
48
51
  export declare function defineIcons<const Icons extends Record<string, IconDrawing>>(icons: Icons): Icons;
@@ -87,7 +87,10 @@ const builtInIcons = {
87
87
  logOut: [
88
88
  'M14 4.5H7.5a2 2 0 0 0-2 2v11a2 2 0 0 0 2 2H14',
89
89
  'M19.5 12H10M16 8.5l3.5 3.5-3.5 3.5'
90
- ]
90
+ ],
91
+ today: 'M11.998 3.291V1.768M5.84 18.159L4.763 19.236M11.998 22.233V20.709M19.233 4.765L18.156 5.842M20.707 12H22.23M18.156 18.159L19.233 19.236M1.766 12H3.289M4.763 4.765L5.84 5.842M15.71 8.288C17.761 10.338 17.761 13.662 15.71 15.712C13.66 17.763 10.336 17.763 8.286 15.712C6.235 13.662 6.235 10.338 8.286 8.288C10.336 6.238 13.66 6.238 15.71 8.288Z',
92
+ checklist: 'M11.75 16.25H20.25M11.75 7.75H20.25M3.75 8.625L5.417 9.75L8.75 5.25M3.75 17.625L5.417 18.75L8.75 14.25',
93
+ chart: 'M2.75 4.75V17.25C2.75 18.355 3.645 19.25 4.75 19.25H21.25M6.75 15.25L10 12C10.552 11.448 11.448 11.448 12 12C12.552 12.552 13.448 12.552 14 12L18.75 7.25M14.75 6.75H19.25V11.25'
91
94
  };
92
95
  const builtIns = new Map(Object.entries(builtInIcons));
93
96
  let appIcons = new Map();
@@ -1,6 +1,6 @@
1
1
  import type { ReactNode } from 'react';
2
2
  export type Interaction = {
3
- primitive: 'AppFrame' | 'Button' | 'IconButton' | 'Link' | 'ListRow' | 'Select' | 'Switch' | 'SegmentedControl' | 'Menu' | 'ContextMenu' | 'CommandMenu' | 'TextField' | 'Form' | 'Dialog' | 'RadioGroup';
3
+ primitive: 'AppFrame' | 'Button' | 'Card' | 'IconButton' | 'Link' | 'ListRow' | 'Select' | 'Switch' | 'Checkbox' | 'SegmentedControl' | 'Menu' | 'ContextMenu' | 'CommandMenu' | 'TextField' | 'Form' | 'Dialog' | 'RadioGroup' | 'ChoiceGroup';
4
4
  event: 'click' | 'change' | 'select' | 'open' | 'close' | 'submit' | 'navigate';
5
5
  label: string;
6
6
  page: string | null;
@@ -30,6 +30,7 @@ export type EntityDeps<T extends Entity> = {
30
30
  api: EntityApi<T>;
31
31
  cache: Cached<readonly T[]>;
32
32
  writes: Writes;
33
+ initial?: readonly T[] | undefined;
33
34
  };
34
35
  export declare class WriteRefused extends Error {
35
36
  readonly outcome: Exclude<WriteOutcome, 'saved' | 'failed'>;
@@ -54,7 +54,7 @@ export class EntityStore extends StoreBase {
54
54
  #generation = 0;
55
55
  #loading;
56
56
  initialState() {
57
- const cached = this.deps.cache.read();
57
+ const cached = this.deps.initial ?? this.deps.cache.read();
58
58
  return {
59
59
  status: cached === undefined ? 'idle' : 'ready',
60
60
  items: cached ?? [],
package/dist/index.d.ts CHANGED
@@ -35,6 +35,8 @@ export type { MemoryApiOptions } from './memoryApi.ts';
35
35
  export { StoreBase, keepEqualParts, snapshotsOf } from './store.ts';
36
36
  export type { Snapshots, StoreClass, StoreConfig } from './store.ts';
37
37
  export type { Json, Persisted, StateOf, StateSpec } from './state.ts';
38
+ export { imageSources, media, mediaPath } from './media.ts';
39
+ export type { ImageSources, Media, MediaImage } from './media.ts';
38
40
  export { defineTables } from './table.ts';
39
41
  export type { Access, Columns, Fields, Level, PublicRow, Row, Seed, Table, TableDefinition, TableOf, Tables, Value, Writes } from './table.ts';
40
42
  export { HttpTableApi, MemoryTableApi, PublicTableStore, TableStore, httpTables, memoryTables, publicStoreName, tableCalls } from './tables.ts';
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ export { EntityStore, WriteRefused, entityCalls } from "./entityStore.js";
16
16
  export { Gate } from "./gate.js";
17
17
  export { MemoryEntityApi, MemoryServer } from "./memoryApi.js";
18
18
  export { StoreBase, keepEqualParts, snapshotsOf } from "./store.js";
19
+ export { imageSources, media, mediaPath } from "./media.js";
19
20
  export { defineTables } from "./table.js";
20
21
  export { HttpTableApi, MemoryTableApi, PublicTableStore, TableStore, httpTables, memoryTables, publicStoreName, tableCalls } from "./tables.js";
21
22
  export { ModelBase, command, commandsOf, createModels, modelSnapshotsOf, parseMessage, sendMessage, splitType } from "./model.js";
@@ -0,0 +1,36 @@
1
+ import { z } from 'zod';
2
+ import type { Row } from './table.ts';
3
+ export declare const media: import("./table.ts").TableOf<"media", {
4
+ readonly columns: {
5
+ readonly key: z.ZodString;
6
+ readonly width: z.ZodNumber;
7
+ readonly height: z.ZodNumber;
8
+ readonly placeholder: z.ZodNullable<z.ZodString>;
9
+ readonly alt: z.ZodString;
10
+ readonly type: z.ZodString;
11
+ readonly bytes: z.ZodNumber;
12
+ };
13
+ readonly access: {
14
+ readonly read: "anyone";
15
+ readonly create: "server";
16
+ readonly update: "admin";
17
+ readonly delete: "admin";
18
+ };
19
+ readonly writes: "confirmed";
20
+ }>;
21
+ export type Media = Row<typeof media>;
22
+ export type MediaImage = Pick<Media, 'key' | 'width' | 'height' | 'placeholder' | 'alt'>;
23
+ export declare const MEDIA_LIMIT_BYTES: number;
24
+ export declare const IMAGE_WIDTHS: readonly [320, 640, 960, 1280, 1920];
25
+ export declare function mediaPath(key: string): string;
26
+ export declare function transformsOn(hostname: string): boolean;
27
+ export type ImageSources = {
28
+ src: string;
29
+ srcSet: string | undefined;
30
+ width: number;
31
+ height: number;
32
+ };
33
+ export declare function imageSources(image: MediaImage, { aspect, transforms }: {
34
+ aspect?: number | undefined;
35
+ transforms: boolean;
36
+ }): ImageSources;
package/dist/media.js ADDED
@@ -0,0 +1,74 @@
1
+ import { z } from 'zod';
2
+ import { patternTable } from "./table.js";
3
+ export const media = patternTable('media', {
4
+ columns: {
5
+ key: z.string(),
6
+ width: z.number().int(),
7
+ height: z.number().int(),
8
+ placeholder: z.string().nullable(),
9
+ alt: z.string().min(1),
10
+ type: z.string(),
11
+ bytes: z.number().int()
12
+ },
13
+ access: {
14
+ read: 'anyone',
15
+ create: 'server',
16
+ update: 'admin',
17
+ delete: 'admin'
18
+ },
19
+ writes: 'confirmed'
20
+ });
21
+ export const MEDIA_LIMIT_BYTES = 10 * 1024 * 1024;
22
+ export const IMAGE_WIDTHS = [320, 640, 960, 1280, 1920];
23
+ const QUALITY = 82;
24
+ const FALLBACK_WIDTH = 960;
25
+ const LOCAL_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]', '::1']);
26
+ export function mediaPath(key) {
27
+ return `/media/${key}`;
28
+ }
29
+ export function transformsOn(hostname) {
30
+ return !LOCAL_HOSTS.has(hostname) && !hostname.endsWith('.localhost');
31
+ }
32
+ function largestWidth(image, aspect) {
33
+ return Math.max(1, Math.floor(aspect === undefined
34
+ ? image.width
35
+ : Math.min(image.width, image.height * aspect)));
36
+ }
37
+ function transformed(key, width, aspect) {
38
+ const options = [
39
+ `width=${width}`,
40
+ ...(aspect === undefined
41
+ ? []
42
+ : [`height=${Math.max(1, Math.round(width / aspect))}`]),
43
+ `quality=${QUALITY}`,
44
+ 'format=auto',
45
+ `fit=${aspect === undefined ? 'scale-down' : 'cover'}`
46
+ ];
47
+ return `/cdn-cgi/image/${options.join(',')}${mediaPath(key)}`;
48
+ }
49
+ export function imageSources(image, { aspect, transforms }) {
50
+ const largest = largestWidth(image, aspect);
51
+ const height = aspect === undefined
52
+ ? image.height
53
+ : Math.max(1, Math.round(largest / aspect));
54
+ if (!transforms) {
55
+ return {
56
+ src: mediaPath(image.key),
57
+ srcSet: undefined,
58
+ width: largest,
59
+ height
60
+ };
61
+ }
62
+ const widths = [
63
+ ...IMAGE_WIDTHS.filter((width) => width < largest),
64
+ largest
65
+ ].filter((width, index, all) => all.indexOf(width) === index);
66
+ return {
67
+ src: transformed(image.key, Math.min(FALLBACK_WIDTH, largest), aspect),
68
+ srcSet: widths
69
+ .map((width) => `${transformed(image.key, width, aspect)} ${width}w`)
70
+ .join(', '),
71
+ width: largest,
72
+ height
73
+ };
74
+ }
@@ -0,0 +1,21 @@
1
+ import { z } from 'zod';
2
+ export declare const TABLES_READ = "tables:read";
3
+ export declare const ASSERTION_SECONDS = 60;
4
+ export declare const NEXT_PAGE_HEADER = "x-pattern-next";
5
+ export declare const assertionKey: z.ZodObject<{
6
+ kid: z.ZodString;
7
+ kty: z.ZodLiteral<"OKP">;
8
+ crv: z.ZodLiteral<"Ed25519">;
9
+ x: z.ZodString;
10
+ d: z.ZodString;
11
+ }, z.core.$strip>;
12
+ export type AssertionKey = z.output<typeof assertionKey>;
13
+ export declare function signTablesAssertion(key: AssertionKey, { sub, aud }: {
14
+ sub: string;
15
+ aud: string;
16
+ }, now: number): Promise<string>;
17
+ export declare function verifiedTablesAssertion(token: string, { hostname, keys, now }: {
18
+ hostname: string;
19
+ keys: Readonly<Record<string, string>>;
20
+ now: number;
21
+ }): Promise<string | undefined>;
@@ -0,0 +1,110 @@
1
+ import { z } from 'zod';
2
+ export const TABLES_READ = 'tables:read';
3
+ export const ASSERTION_SECONDS = 60;
4
+ export const NEXT_PAGE_HEADER = 'x-pattern-next';
5
+ const CLOCK_SKEW_SECONDS = 30;
6
+ const ED25519 = { name: 'Ed25519' };
7
+ export const assertionKey = z.object({
8
+ kid: z.string().min(1),
9
+ kty: z.literal('OKP'),
10
+ crv: z.literal('Ed25519'),
11
+ x: z.string(),
12
+ d: z.string()
13
+ });
14
+ const assertionHeader = z.object({
15
+ alg: z.literal('EdDSA'),
16
+ typ: z.literal('JWT'),
17
+ kid: z.string()
18
+ });
19
+ const assertionClaims = z.object({
20
+ sub: z.email(),
21
+ aud: z.string(),
22
+ scope: z.literal(TABLES_READ),
23
+ iat: z.int(),
24
+ exp: z.int()
25
+ });
26
+ const encoder = new TextEncoder();
27
+ function base64url(bytes) {
28
+ return btoa(String.fromCharCode(...bytes))
29
+ .replaceAll('+', '-')
30
+ .replaceAll('/', '_')
31
+ .replaceAll('=', '');
32
+ }
33
+ function bytesOf(text) {
34
+ try {
35
+ const binary = atob(text.replaceAll('-', '+').replaceAll('_', '/'));
36
+ return Uint8Array.from(binary, (letter) => letter.charCodeAt(0));
37
+ }
38
+ catch {
39
+ return undefined;
40
+ }
41
+ }
42
+ function jsonOf(text) {
43
+ const bytes = bytesOf(text);
44
+ if (bytes === undefined)
45
+ return undefined;
46
+ try {
47
+ return JSON.parse(new TextDecoder().decode(bytes));
48
+ }
49
+ catch {
50
+ return undefined;
51
+ }
52
+ }
53
+ function encoded(value) {
54
+ return base64url(encoder.encode(JSON.stringify(value)));
55
+ }
56
+ export async function signTablesAssertion(key, { sub, aud }, now) {
57
+ const privateKey = await crypto.subtle.importKey('jwk', { kty: key.kty, crv: key.crv, x: key.x, d: key.d }, ED25519, false, ['sign']);
58
+ const signed = `${encoded({ alg: 'EdDSA', typ: 'JWT', kid: key.kid })}.${encoded({
59
+ sub,
60
+ aud,
61
+ scope: TABLES_READ,
62
+ iat: now,
63
+ exp: now + ASSERTION_SECONDS
64
+ })}`;
65
+ const signature = await crypto.subtle.sign(ED25519, privateKey, encoder.encode(signed));
66
+ return `${signed}.${base64url(new Uint8Array(signature))}`;
67
+ }
68
+ async function isSignedBy(publicKey, signed, signature) {
69
+ const keyBytes = bytesOf(publicKey);
70
+ const signatureBytes = bytesOf(signature);
71
+ if (keyBytes === undefined || signatureBytes === undefined)
72
+ return false;
73
+ try {
74
+ const key = await crypto.subtle.importKey('raw', keyBytes, ED25519, false, [
75
+ 'verify'
76
+ ]);
77
+ return await crypto.subtle.verify(ED25519, key, signatureBytes, encoder.encode(signed));
78
+ }
79
+ catch {
80
+ return false;
81
+ }
82
+ }
83
+ export async function verifiedTablesAssertion(token, { hostname, keys, now }) {
84
+ const [header, claims, signature, ...rest] = token.split('.');
85
+ if (header === undefined ||
86
+ claims === undefined ||
87
+ signature === undefined ||
88
+ rest.length > 0) {
89
+ return undefined;
90
+ }
91
+ const parsedHeader = assertionHeader.safeParse(jsonOf(header));
92
+ if (!parsedHeader.success)
93
+ return undefined;
94
+ const { kid } = parsedHeader.data;
95
+ const publicKey = Object.hasOwn(keys, kid) ? keys[kid] : undefined;
96
+ if (publicKey === undefined)
97
+ return undefined;
98
+ if (!(await isSignedBy(publicKey, `${header}.${claims}`, signature))) {
99
+ return undefined;
100
+ }
101
+ const parsed = assertionClaims.safeParse(jsonOf(claims));
102
+ if (!parsed.success)
103
+ return undefined;
104
+ const { sub, aud, iat, exp } = parsed.data;
105
+ const isFresh = exp > iat &&
106
+ exp - iat <= ASSERTION_SECONDS &&
107
+ iat <= now + CLOCK_SKEW_SECONDS &&
108
+ now < exp;
109
+ return aud === hostname && isFresh ? sub.toLowerCase() : undefined;
110
+ }